[
  {
    "path": ".eslintrc.json",
    "content": "{\n    \"env\": {\n        \"browser\": true,\n        \"es2021\": true\n    },\n    \"extends\": \"eslint:recommended\",\n    \"parserOptions\": {\n        \"ecmaVersion\": 12\n    },\n    \"rules\": {\n    }\n}\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text eol=lf\n\n*.reg text eol=crlf\n\n*.png binary\n*.gif binary\n*.gz binary\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: '9001'\n\n---\n\n<!-- NOTE:\n**please use english, or include an english translation.** aside from that,  \ninclude all of the information that might possibly be necessary to help diagnose your issue,\nespecially things like the server config and server logs; that said,\nany parts below which are obviously and definitely irrelevant can be skipped -->\n\n### Describe the bug\na description of what the bug is\n\n### To Reproduce\nList 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\n\n### Expected behavior\na description of what you expected to happen\n\n### Screenshots\nif applicable, add screenshots to help explain your problem, such as the kickass crashpage :^)\n\n### Server details (if you are using docker/podman)\nremove the ones that are not relevant:\n* **server OS / version:** \n* **how you're running copyparty:** (docker/podman/something-else)\n* **docker image:** (variant, version, and arch if you know)\n* **copyparty arguments and/or config-file:** \n\n### Server details (if you're NOT using docker/podman)\nremove the ones that are not relevant:\n* **server OS / version:** \n* **what copyparty did you grab:** (sfx/exe/pip/arch/...)\n* **how you're running it:** (in a terminal, as a systemd-service, ...)\n* run copyparty with `--version` and grab the last 3 lines (they start with `copyparty`, `CPython`, `sqlite`) and paste them below this line:\n* **copyparty arguments and/or config-file:** \n\n### Client details\nif the issue is possibly on the client-side, then mention some of the following:\n* the device type and model: \n* OS version: \n* browser version: \n\n### The rest of the stack\nif you are connecting directly to copyparty then that's cool, otherwise please mention everything else between copyparty and the browser (reverseproxy, tunnels, etc.)\n\n### Server log\nif 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\n\n### Additional context\nany other context about the problem here\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: '9001'\n\n---\n\n<!-- NOTE:\n**please use english, or include an english translation.** aside from that,  \nall of the below are optional, consider them as inspiration, delete and rewrite at will -->\n\n**is your feature request related to a problem? Please describe.**\na description of what the problem is, for example, `I'm always frustrated when [...]` or `Why is it not possible to [...]`\n\n**Describe the idea / solution you'd like**\na description of what you want to happen\n\n**Describe any alternatives you've considered**\na description of any alternative solutions or features you've considered\n\n**Additional context**\nadd any other context or screenshots about the feature request here\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/something-else.md",
    "content": "---\nname: Something else\nabout: \"┐(ﾟ∀ﾟ)┌\"\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n\n"
  },
  {
    "path": ".github/branch-rename.md",
    "content": "modernize your local checkout of the repo like so,\n```sh\ngit branch -m master hovudstraum\ngit fetch origin\ngit branch -u origin/hovudstraum hovudstraum\ngit remote set-head origin -a\n```\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "To show that your contribution is compatible with the MIT License, please include the following text somewhere in this PR description:  \nThis PR complies with the DCO; https://developercertificate.org/  \n"
  },
  {
    "path": ".gitignore",
    "content": "# python\n__pycache__/\n*.py[cod]\n*$py.class\nMANIFEST.in\nMANIFEST\ncopyparty.egg-info/\n.venv/\n\n/buildenv/\n/build/\n/dist/\n/py2/\n/sfx*\n/pyz/\n/unt/\n/log/\n\n# ide\n*.sublime-workspace\n\n# winmerge\n*.bak\n\n# apple pls\n.DS_Store\n\n# derived\ncopyparty/res/COPYING.txt\ncopyparty/web/deps/\nsrv/\nscripts/docker/base/b/\nscripts/docker/base/cver*\nscripts/docker/base/test-aac/\nscripts/docker/base/whl/\nscripts/docker/i/\nscripts/deps-docker/uncomment.py\ncontrib/package/arch/pkg/\ncontrib/package/arch/src/\n\n# state/logs\nup.*.txt\n.hist/\nscripts/docker/*.out\nscripts/docker/*.err\n/perf.*\n\n# nix build output link\nresult\nresult-*\n\n# IDEA config\n.idea/\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"Run copyparty\",\n            \"type\": \"debugpy\",\n            \"request\": \"launch\",\n            \"module\": \"copyparty\",\n            \"console\": \"integratedTerminal\",\n            \"cwd\": \"${workspaceFolder}\",\n            \"justMyCode\": false,\n            \"env\": {\n                \"PYDEVD_DISABLE_FILE_VALIDATION\": \"1\",\n                \"PYTHONWARNINGS\": \"always\"  //error\n            },\n            \"args\": [\n                //\"-nw\",  // no-write; for testing uploads without writing to disk\n                //\"-q\",  // quiet; speedboost when console output is not needed\n\n                // # increase debugger performance:\n                //\"no-htp\",\n                //\"hash-mt=0\",\n                //\"mtag-mt=1\",\n                //\"th-mt=1\",\n\n                // # listen for FTP and TFTP\n                \"--ftp=3921\",\n                \"--ftp-pr=12000-12099\",\n                \"--tftp=3969\",\n\n                // # listen on all IPv6, all IPv4, and unix-socket\n                \"-i::,unix:777:a.sock\",\n\n                // # misc\n                \"--dedup\",\n                \"-e2dsa\",\n                \"-e2ts\",\n                \"--rss\",\n                \"--shr=/shr\",\n                \"--stats\",\n                \"-z\",\n\n                // # users + volumes\n                \"-aed:wark\",\n                \"-vdist:dist:r\",\n                \"-vsrv::r:rw,ed\",\n                \"-vsrv/junk:junk:r:A,ed\",\n                \"--ver\"\n            ]\n        },\n        {\n            \"name\": \"Run active unit test\",\n            \"type\": \"debugpy\",\n            \"request\": \"launch\",\n            \"module\": \"unittest\",\n            \"console\": \"integratedTerminal\",\n            \"cwd\": \"${workspaceFolder}\",\n            \"args\": [\n                \"-v\",\n                \"${file}\"\n            ]\n        },\n        {\n            \"name\": \"Python: Current File\",\n            \"type\": \"python\",\n            \"request\": \"launch\",\n            \"program\": \"${file}\",\n            \"console\": \"integratedTerminal\",\n            \"justMyCode\": false\n        }\n    ]\n}\n"
  },
  {
    "path": ".vscode/launch.py",
    "content": "#!/usr/bin/env python3\n\n# takes arguments from launch.json\n# is used by no_dbg in tasks.json\n# launches 10x faster than mspython debugpy\n# and is stoppable with ^C\n\nimport re\nimport os\nimport sys\n\nprint(sys.executable)\n\nimport json5\nimport shlex\nimport subprocess as sp\n\n\nwith open(\".vscode/launch.json\", \"r\", encoding=\"utf-8\") as f:\n    tj = f.read()\n\noj = json5.loads(tj)\nargv = oj[\"configurations\"][0][\"args\"]\n\ntry:\n    sargv = \" \".join([shlex.quote(x) for x in argv])\n    print(sys.executable + \" -m copyparty \" + sargv + \"\\n\")\nexcept:\n    pass\n\nargv = [os.path.expanduser(x) if x.startswith(\"~\") else x for x in argv]\n\nsfx = \"\"\nif len(sys.argv) > 1 and os.path.isfile(sys.argv[1]):\n    sfx = sys.argv[1]\n    sys.argv = [sys.argv[0]] + sys.argv[2:]\n\nargv += sys.argv[1:]\n\nif sfx:\n    argv = [sys.executable, sfx] + argv\n    sp.check_call(argv)\nelif re.search(\" -j ?[0-9]\", \" \".join(argv)):\n    argv = [sys.executable, \"-Wa\", \"-m\", \"copyparty\"] + argv\n    sp.check_call(argv)\nelse:\n    sys.path.insert(0, os.getcwd())\n    from copyparty.__main__ import main as copyparty\n\n    try:\n        copyparty([\"a\"] + argv)\n    except SystemExit as ex:\n        if ex.code:\n            raise\n\nprint(\"\\n\\033[32mokke\\033[0m\")\nsys.exit(1)\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"workbench.colorCustomizations\": {\n        // https://ocv.me/dot/bifrost.html\n        \"terminal.background\": \"#1e1e1e\",\n        \"terminal.foreground\": \"#d2d2d2\",\n        \"terminalCursor.background\": \"#93A1A1\",\n        \"terminalCursor.foreground\": \"#93A1A1\",\n        \"terminal.ansiBlack\": \"#404040\",\n        \"terminal.ansiRed\": \"#f03669\",\n        \"terminal.ansiGreen\": \"#b8e346\",\n        \"terminal.ansiYellow\": \"#ffa402\",\n        \"terminal.ansiBlue\": \"#02a2ff\",\n        \"terminal.ansiMagenta\": \"#f65be3\",\n        \"terminal.ansiCyan\": \"#3da698\",\n        \"terminal.ansiWhite\": \"#d2d2d2\",\n        \"terminal.ansiBrightBlack\": \"#606060\",\n        \"terminal.ansiBrightRed\": \"#c75b79\",\n        \"terminal.ansiBrightGreen\": \"#c8e37e\",\n        \"terminal.ansiBrightYellow\": \"#ffbe4a\",\n        \"terminal.ansiBrightBlue\": \"#71cbff\",\n        \"terminal.ansiBrightMagenta\": \"#b67fe3\",\n        \"terminal.ansiBrightCyan\": \"#9cf0ed\",\n        \"terminal.ansiBrightWhite\": \"#ffffff\",\n    },\n    \"python.terminal.activateEnvironment\": false,\n    \"python.analysis.enablePytestSupport\": false,\n    \"python.analysis.typeCheckingMode\": \"standard\",\n    \"python.testing.pytestEnabled\": false,\n    \"python.testing.unittestEnabled\": true,\n    \"python.testing.unittestArgs\": [\n        \"-v\",\n        \"-s\",\n        \"./tests\",\n        \"-p\",\n        \"test_*.py\"\n    ],\n    // 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')\n    \"editor.formatOnSave\": false,\n    \"[html]\": {\n        \"editor.formatOnSave\": false,\n        \"editor.autoIndent\": \"keep\",\n    },\n    \"[css]\": {\n        \"editor.formatOnSave\": false,\n    },\n    \"files.associations\": {\n        \"*.makefile\": \"makefile\"\n    },\n}"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n    \"version\": \"2.0.0\",\n    \"tasks\": [\n        {\n            \"label\": \"pre\",\n            \"command\": \"true;rm -rf inc/* inc/.hist/;mkdir -p inc;\",\n            \"type\": \"shell\"\n        },\n        {\n            \"label\": \"no_dbg\",\n            \"type\": \"shell\",\n            \"command\": \"${config:python.pythonPath}\",\n            \"args\": [\n                \"-Wa\", //-We\n                \".vscode/launch.py\"\n            ]\n        }\n    ]\n}"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "in the words of Abraham Lincoln:\n\n> Be excellent to each other... and... PARTY ON, DUDES!\n\nmore specifically I'll paraphrase some examples from a german automotive corporation as they cover all the bases without being too wordy\n\n## Examples of unacceptable behavior\n* intimidation, harassment, trolling\n* insulting, derogatory, harmful or prejudicial comments\n* posting private information without permission\n* political or personal attacks\n\n## Examples of expected behavior\n* being nice, friendly, welcoming, inclusive, mindful and empathetic\n* acting considerate, modest, respectful\n* using polite and inclusive language\n* criticize constructively and accept constructive criticism\n* respect different points of view\n\n## finally and even more specifically,\n* parse opinions and feedback objectively without prejudice\n  * it's the message that matters, not who said it\n\naaand that's how you say `be nice` in a way that fills half a floppy w\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "* **found a bug?** [create an issue!](https://github.com/9001/copyparty/issues) or let me know in the [discord](https://discord.gg/25J8CdTT6G) :>\n* **fixed a bug?** create a PR or post a patch! big thx in advance :>\n* **have a cool idea?** let's discuss it! anywhere's fine, you choose.\n\nbut please:\n\n\n\n# do not use AI / LLM when writing code\n\ncopyparty is 100% organic, free-range, human-written software!\n\n> ⚠ you are now entering a no-copilot zone\n\nthe *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.\n\nsorry for the harsh tone, but this is important to me 🙏\n\n\n\n# contribution ideas\n\n\n## documentation\n\nI 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.\n\n\n## crazy ideas & features\n\nassuming they won't cause too much problems or side-effects :> \n\ni think someone was working on a way to list directories over DNS for example...\n\nif 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 👍\n\n\n## others\n\naside from documentation and ideas, some other things that would be cool to have some help with is:\n\n* **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 :>\n\n  * 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\n\n* **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.\n\n* **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!\n\n* **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))\n\n  * [fpm](https://github.com/jordansissel/fpm) can probably help with the technical part of it, but someone needs to handle distro relations :-)\n\n* **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;\n\n  * [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\n\n  * [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 :>\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 ed <oss@ocv.me>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<img src=\"https://github.com/9001/copyparty/raw/hovudstraum/docs/logo.svg\" width=\"250\" align=\"right\"/>\n\n### 💾🎉 copyparty\n\nturn almost any device into a file server with resumable uploads/downloads using [*any*](#browser-support) web browser\n\n* server only needs Python (2 or 3), all dependencies optional\n* 🔌 protocols: [http(s)](#the-browser) // [webdav](#webdav-server) // [sftp](#sftp-server) // [ftp(s)](#ftp-server) // [tftp](#tftp-server) // [smb/cifs](#smb-server)\n* 📱 [android app](#android-app) // [iPhone shortcuts](#ios-shortcuts)\n\n👉 **[Get started](#quickstart)!** or visit the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running on a nuc in my basement\n\n📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer)\n\n🎬 **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))\n\nbuilt in Norway 🇳🇴 with contributions from [not-norway](https://github.com/9001/copyparty/graphs/contributors)\n\n\n## readme toc\n\n* top\n    * [quickstart](#quickstart) - just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** -- that's it! 🎉\n        * [mirrors](#mirrors) - other places to download copyparty from\n        * [at home](#at-home) - make it accessible over the internet\n        * [on servers](#on-servers) - you may also want these, especially on servers\n    * [features](#features) - also see [comparison to similar software](./docs/versus.md)\n    * [testimonials](#testimonials) - small collection of user feedback\n* [motivations](#motivations) - project goals / philosophy\n    * [notes](#notes) - general notes\n* [bugs](#bugs) - roughly sorted by chance of encounter\n    * [not my bugs](#not-my-bugs) - same order here too\n* [breaking changes](#breaking-changes) - upgrade notes\n* [FAQ](#FAQ) - \"frequently\" asked questions\n* [accounts and volumes](#accounts-and-volumes) - per-folder, per-user permissions\n    * [shadowing](#shadowing) - hiding specific subfolders\n    * [dotfiles](#dotfiles) - unix-style hidden files/folders\n* [the browser](#the-browser) - accessing a copyparty server using a web-browser\n    * [tabs](#tabs) - the main tabs in the ui\n    * [hotkeys](#hotkeys) - the browser has the following hotkeys\n    * [navpane](#navpane) - switching between breadcrumbs or navpane\n    * [thumbnails](#thumbnails) - press `g` or `田` to toggle grid-view instead of the file listing\n    * [zip downloads](#zip-downloads) - download folders (or file selections) as `zip` or `tar` files\n    * [uploading](#uploading) - drag files/folders into the web-browser to upload\n        * [file-search](#file-search) - dropping files into the browser also lets you see if they exist on the server\n        * [unpost](#unpost) - undo/delete accidental uploads\n        * [self-destruct](#self-destruct) - uploads can be given a lifetime\n        * [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))\n        * [incoming files](#incoming-files) - the control-panel shows the ETA for all incoming files\n    * [file manager](#file-manager) - cut/paste, rename, and delete files/folders (if you have permission)\n    * [shares](#shares) - share a file or folder by creating a temporary link\n    * [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI\n    * [rss feeds](#rss-feeds) - monitor a folder with your RSS reader\n    * [opds feeds](#opds-feeds) - browse and download files from your e-book reader\n    * [recent uploads](#recent-uploads) - list all recent uploads\n    * [media player](#media-player) - plays almost every audio format there is\n        * [playlists](#playlists) - create and play [m3u8](https://en.wikipedia.org/wiki/M3U) playlists\n        * [creating a playlist](#creating-a-playlist) - with a standalone mediaplayer or copyparty\n        * [audio equalizer](#audio-equalizer) - and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression)\n        * [fix unreliable playback on android](#fix-unreliable-playback-on-android) - due to phone / app settings\n    * [textfile viewer](#textfile-viewer) - with realtime streaming of logfiles and such ([demo](https://a.ocv.me/pub/demo/logtail/))\n    * [markdown viewer](#markdown-viewer) - and there are *two* editors\n        * [markdown vars](#markdown-vars) - dynamic docs with serverside variable expansion\n    * [other tricks](#other-tricks)\n    * [searching](#searching) - search by size, date, path/name, mp3-tags, ...\n* [server config](#server-config) - using arguments or config files, or a mix of both\n    * [version-checker](#version-checker) - sleep better at night\n    * [logging](#logging) - serverlog is sent to stdout by default\n    * [zeroconf](#zeroconf) - announce enabled services on the LAN ([pic](https://user-images.githubusercontent.com/241032/215344737-0eae8d98-9496-4256-9aa8-cd2f6971810d.png))\n        * [mdns](#mdns) - LAN domain-name and feature announcer\n        * [ssdp](#ssdp) - windows-explorer announcer\n    * [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\n    * [ftp server](#ftp-server) - an FTP server can be started using `--ftp 3921`\n    * [sftp server](#sftp-server) - goes roughly 700 MiB/s (slower than webdav and ftp)\n    * [webdav server](#webdav-server) - with read-write support\n        * [connecting to webdav from windows](#connecting-to-webdav-from-windows) - using the GUI\n    * [tftp server](#tftp-server) - a TFTP server (read/write) can be started using `--tftp 3969`\n    * [smb server](#smb-server) - unsafe, slow, not recommended for wan\n    * [browser ux](#browser-ux) - tweaking the ui\n    * [opengraph](#opengraph) - discord and social-media embeds\n    * [file deduplication](#file-deduplication) - enable symlink-based upload deduplication\n    * [file indexing](#file-indexing) - enable music search, upload-undo, and better dedup\n        * [exclude-patterns](#exclude-patterns) - to save some time\n        * [filesystem guards](#filesystem-guards) - avoid traversing into other filesystems\n        * [periodic rescan](#periodic-rescan) - filesystem monitoring\n    * [upload rules](#upload-rules) - set upload rules using volflags\n    * [compress uploads](#compress-uploads) - files can be autocompressed on upload\n    * [chmod and chown](#chmod-and-chown) - per-volume filesystem-permissions and ownership\n    * [other flags](#other-flags)\n    * [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else\n    * [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload\n        * [metadata from xattrs](#metadata-from-xattrs) - unix extended file attributes\n    * [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags\n    * [event hooks](#event-hooks) - trigger a program on uploads, renames etc ([examples](./bin/hooks/))\n        * [zeromq](#zeromq) - event-hooks can send zeromq messages\n        * [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/))\n    * [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/))\n    * [ip auth](#ip-auth) - autologin based on IP range (CIDR)\n        * [restrict to ip](#restrict-to-ip) - limit a user to certain IP ranges (CIDR)\n    * [identity providers](#identity-providers) - replace copyparty passwords with oauth and such\n        * [generic header auth](#generic-header-auth) - other ways to auth by header\n    * [user-changeable passwords](#user-changeable-passwords) - if permitted, users can change their own passwords\n    * [using the cloud as storage](#using-the-cloud-as-storage) - connecting to an aws s3 bucket and similar\n    * [hiding from google](#hiding-from-google) - tell search engines you don't wanna be indexed\n    * [themes](#themes)\n    * [complete examples](#complete-examples)\n    * [listen on port 80 and 443](#listen-on-port-80-and-443) - become a *real* webserver\n    * [reverse-proxy](#reverse-proxy) - running copyparty next to other websites\n        * [real-ip](#real-ip) - teaching copyparty how to see client IPs\n        * [reverse-proxy performance](#reverse-proxy-performance)\n    * [permanent cloudflare tunnel](#permanent-cloudflare-tunnel) - if you have a domain and want to get your copyparty online real quick\n    * [prometheus](#prometheus) - metrics/stats can be enabled\n    * [other extremely specific features](#other-extremely-specific-features) - you'll never find a use for these\n        * [custom mimetypes](#custom-mimetypes) - change the association of a file extension\n        * [GDPR compliance](#GDPR-compliance) - imagine using copyparty professionally...\n        * [feature chickenbits](#feature-chickenbits) - buggy feature? rip it out\n        * [feature beefybits](#feature-beefybits) - force-enable features with known issues on your OS/env\n* [packages](#packages) - the party might be closer than you think\n    * [arch package](#arch-package) - `pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))\n    * [fedora package](#fedora-package) - does not exist yet\n    * [homebrew formulae](#homebrew-formulae) - `brew install copyparty ffmpeg`\n    * [nix package](#nix-package) - `nix profile install github:9001/copyparty`\n    * [nixos module](#nixos-module)\n* [browser support](#browser-support) - TLDR: yes\n* [server hall of fame](#server-hall-of-fame) - unexpected things that run copyparty\n* [client examples](#client-examples) - interact with copyparty using non-browser clients\n    * [folder sync](#folder-sync) - sync folders to/from copyparty\n    * [mount as drive](#mount-as-drive) - a remote copyparty server as a local filesystem\n* [android app](#android-app) - upload to copyparty with one tap\n* [iOS shortcuts](#iOS-shortcuts) - there is no iPhone app, but\n* [performance](#performance) - defaults are usually fine - expect `8 GiB/s` download, `1 GiB/s` upload\n    * [client-side](#client-side) - when uploading files\n* [security](#security) - there is a [discord server](https://discord.gg/25J8CdTT6G) with announcements\n    * [gotchas](#gotchas) - behavior that might be unexpected\n    * [cors](#cors) - cross-site request config\n    * [filekeys](#filekeys) - prevent filename bruteforcing\n        * [dirkeys](#dirkeys) - share specific folders in a volume\n    * [password hashing](#password-hashing) - you can hash passwords\n    * [https](#https) - both HTTP and HTTPS are accepted\n* [recovering from crashes](#recovering-from-crashes)\n    * [client crashes](#client-crashes)\n        * [firefox wsod](#firefox-wsod) - firefox 87 can crash during uploads\n* [HTTP API](#HTTP-API) - see [devnotes](./docs/devnotes.md#http-api)\n* [dependencies](#dependencies) - mandatory deps\n    * [optional dependencies](#optional-dependencies) - enable bonus features\n        * [dependency chickenbits](#dependency-chickenbits) - prevent loading an optional dependency\n        * [dependency unvendoring](#dependency-unvendoring) - force use of system modules\n    * [optional gpl stuff](#optional-gpl-stuff)\n* [sfx](#sfx) - the self-contained \"binary\" (recommended!)\n    * [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+)\n    * [zipapp](#zipapp) - another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz)\n* [install on android](#install-on-android)\n* [install on iOS](#install-on-iOS)\n* [reporting bugs](#reporting-bugs) - ideas for context to include, and where to submit them\n* [devnotes](#devnotes) - for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md)\n\n\n## quickstart\n\njust run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** -- that's it! 🎉\n\n> ℹ️ 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\n\n* or install through [pypi](https://pypi.org/project/copyparty/): `python3 -m pip install --user -U copyparty`\n* or if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead\n* or install [on arch](#arch-package) / [homebrew](#homebrew-formulae) ╱ [on NixOS](#nixos-module) ╱ [through nix](#nix-package)\n* or if you are on android, [install copyparty in termux](#install-on-android)\n* or maybe an iPhone or iPad? [install in a-Shell on iOS](#install-on-iOS)\n* or maybe you have a [synology nas / dsm](./docs/synology-dsm.md)\n* or if you have [uv](https://docs.astral.sh/uv/) installed, run `uv tool run copyparty`\n* or if your computer is messed up and nothing else works, [try the pyz](#zipapp)\n* or if your OS is dead, give the [bootable flashdrive / cd-rom](https://a.ocv.me/pub/stuff/edcd001/enterprise-edition/) a spin\n* or if you don't trust copyparty yet and want to isolate it a little, then...\n  * ...maybe [prisonparty](./bin/prisonparty.sh) to create a tiny [chroot](https://wiki.archlinux.org/title/Chroot) (very portable),\n  * ...or [bubbleparty](./bin/bubbleparty.sh) to wrap it in [bubblewrap](https://github.com/containers/bubblewrap) (much better)\n* or if you prefer to [use docker](./scripts/docker/) 🐋 you can do that too\n  * docker has all deps built-in, so skip this step:\n\nenable thumbnails (images/audio/video), media indexing, and audio transcoding by installing some recommended deps:\n\n* **Alpine:** `apk add py3-pillow ffmpeg`\n* **Debian:** `apt install --no-install-recommends python3-pil ffmpeg`\n* **Fedora:** rpmfusion + `dnf install python3-pillow ffmpeg --allowerasing`\n* **FreeBSD:** `pkg install py39-sqlite3 py39-pillow ffmpeg`\n* **MacOS:** `port install py-Pillow ffmpeg`\n* **MacOS** (alternative): `brew install pillow ffmpeg`\n* **Windows:** `python -m pip install --user -U Pillow`\n  * install [python](https://www.python.org/downloads/windows/) and [ffmpeg](#optional-dependencies) manually; do not use `winget` or `Microsoft Store` (it breaks $PATH)\n  * copyparty.exe comes with `Pillow` and only needs [ffmpeg](#optional-dependencies) for mediatags/videothumbs\n* see [optional dependencies](#optional-dependencies) to enable even more features\n\nrunning 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)\n\nor see [some usage examples](#complete-examples) for inspiration, or the [complete windows example](./docs/examples/windows.md)\n\nsome recommended options:\n* `-e2dsa` enables general [file indexing](#file-indexing)\n* `-e2ts` enables audio metadata indexing (needs either FFprobe or Mutagen)\n* `-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`\n  * replace `:r:rw,foo` with `:r,foo` to only make the folder readable by `foo` and nobody else\n  * see [accounts and volumes](#accounts-and-volumes) (or [`--help-accounts`](https://copyparty.eu/cli/#accounts-help-page)) for the syntax and other permissions\n\n\n### mirrors\n\nother places to download copyparty from  (non-github links):\n\n* https://copyparty.eu/ (hetzner, finland, official mirror):\n  * https://copyparty.eu/py = https://copyparty.eu/copyparty-sfx.py = the sfx\n  * https://copyparty.eu/en = https://copyparty.eu/copyparty-en.py = the english-only sfx\n  * https://copyparty.eu/pyz = https://copyparty.eu/copyparty.pyz = the zipapp\n  * https://copyparty.eu/enz = https://copyparty.eu/copyparty-en.pyz = the enterprise pyz\n  * https://copyparty.eu/cli = online cli helptext\n\n\n### at home\n\nmake 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:\n\nfirst 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`\n\nas the tunnel starts, it will show a URL which you can share to let anyone browse your stash or upload files to you\n\nbut 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)\n\nsince people will be connecting through cloudflare, run copyparty with `--xff-hdr cf-connecting-ip` to detect client IPs correctly\n\n\n### on servers\n\nyou may also want these, especially on servers:\n\n* [contrib/systemd/copyparty.service](contrib/systemd/copyparty.service) to run copyparty as a systemd service (see guide inside)\n* [contrib/systemd/prisonparty.service](contrib/systemd/prisonparty.service) to run it in a chroot (for extra security)\n* [contrib/podman-systemd/](contrib/podman-systemd/) to run copyparty in a Podman container as a systemd service (see guide inside)\n* [contrib/openrc/copyparty](contrib/openrc/copyparty) to run copyparty on Alpine / Gentoo\n* [contrib/rc/copyparty](contrib/rc/copyparty) to run copyparty on FreeBSD\n* [nixos module](#nixos-module) to run copyparty on NixOS hosts\n* [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to [reverse-proxy](#reverse-proxy) behind nginx (for better https)\n\nand remember to open the ports you want; here's a complete example including every feature copyparty has to offer:\n```\nfirewall-cmd --permanent --add-port={80,443,3921,3922,3923,3945,3990}/tcp  # --zone=libvirt\nfirewall-cmd --permanent --add-port=12000-12099/tcp  # --zone=libvirt\nfirewall-cmd --permanent --add-port={69,1900,3969,5353}/udp  # --zone=libvirt\nfirewall-cmd --reload\n```\n(69:tftp, 1900:ssdp, 3921:ftp, 3922:sftp, 3923:http/https, 3945:smb, 3969:tftp, 3990:ftps, 5353:mdns, 12000:passive-ftp)\n\n\n## features\n\nalso see [comparison to similar software](./docs/versus.md)\n\n* backend stuff\n  * ☑ IPv6 + unix-sockets\n  * ☑ [multiprocessing](#performance) (actual multithreading)\n  * ☑ volumes (mountpoints)\n  * ☑ [accounts](#accounts-and-volumes)\n  * ☑ [ftp server](#ftp-server)\n  * ☑ [tftp server](#tftp-server)\n  * ☑ [webdav server](#webdav-server)\n  * ☑ [smb/cifs server](#smb-server)\n  * ☑ [qr-code](#qr-code) for quick access\n  * ☑ [upnp / zeroconf / mdns / ssdp](#zeroconf)\n  * ☑ [event hooks](#event-hooks) / script runner\n  * ☑ [reverse-proxy support](https://github.com/9001/copyparty#reverse-proxy)\n  * ☑ cross-platform (Windows, Linux, Macos, Android, iOS, FreeBSD, arm32/arm64, ppc64le, s390x, risc-v/riscv64, SGI IRIX)\n* upload\n  * ☑ basic: plain multipart, ie6 support\n  * ☑ [up2k](#uploading): js, resumable, multithreaded\n    * **no filesize limit!** even on Cloudflare\n  * ☑ stash: simple PUT filedropper\n  * ☑ filename randomizer\n  * ☑ write-only folders\n  * ☑ [unpost](#unpost): undo/delete accidental uploads\n  * ☑ [self-destruct](#self-destruct) (specified server-side or client-side)\n  * ☑ [race the beam](#race-the-beam) (almost like peer-to-peer)\n  * ☑ symlink/discard duplicates (content-matching)\n* download\n  * ☑ single files in browser\n  * ☑ [folders as zip / tar files](#zip-downloads)\n  * ☑ [FUSE client](https://github.com/9001/copyparty/tree/hovudstraum/bin#partyfusepy) (read-only)\n* browser\n  * ☑ [navpane](#navpane) (directory tree sidebar)\n  * ☑ file manager (cut/paste, delete, [batch-rename](#batch-rename))\n  * ☑ audio player (with [OS media controls](https://user-images.githubusercontent.com/241032/215347492-b4250797-6c90-4e09-9a4c-721edf2fb15c.png) and opus/mp3 transcoding)\n    * ☑ play video files as audio (converted on server)\n    * ☑ create and play [m3u8 playlists](#playlists)\n  * ☑ image gallery with webm player\n    * ☑ and cbz manga/comics reader\n  * ☑ [textfile browser](#textfile-viewer) with syntax highlighting\n    * ☑ realtime streaming of growing files (logfiles and such)\n  * ☑ [thumbnails](#thumbnails)\n    * ☑ ...of images using Pillow, pyvips, or FFmpeg\n    * ☑ ...of RAW images using rawpy\n    * ☑ ...of videos using FFmpeg\n    * ☑ ...of audio (spectrograms) using FFmpeg\n    * ☑ cache eviction (max-age; maybe max-size eventually)\n  * ☑ multilingual UI (english, norwegian, chinese, [add your own](./docs/rice/#translations)))\n  * ☑ SPA (browse while uploading)\n* server indexing\n  * ☑ [locate files by contents](#file-search)\n  * ☑ search by name/path/date/size\n  * ☑ [search by ID3-tags etc.](#searching)\n* client support\n  * ☑ [folder sync](#folder-sync) (one-way only; full sync will never be supported)\n  * ☑ [curl-friendly](https://user-images.githubusercontent.com/241032/215322619-ea5fd606-3654-40ad-94ee-2bc058647bb2.png)\n  * ☑ [opengraph](#opengraph) (discord embeds)\n* markdown\n  * ☑ [viewer](#markdown-viewer)\n  * ☑ editor (sure why not)\n  * ☑ [variables](#markdown-vars)\n\nPS: 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) 🤙\n\n\n## testimonials\n\nsmall collection of user feedback\n\n`good enough`, `surprisingly correct`, `certified good software`, `just works`, `why`, `wow this is better than nextcloud`\n\n* UI просто ужасно. Если буду описывать детально не смогу удержаться в рамках приличий\n\n\n# motivations\n\nproject goals / philosophy\n\n* inverse unix philosophy -- do all the things, and do an *okay* job\n  * quick drop-in service to get a lot of features in a pinch\n  * some of [the alternatives](./docs/versus.md) might be a better fit for you\n* run anywhere, support everything\n  * as many web-browsers and python versions as possible\n    * every browser should at least be able to browse, download, upload files\n    * be a good emergency solution for transferring stuff between ancient boxes\n  * minimal dependencies\n    * but optional dependencies adding bonus-features are ok\n    * everything being plaintext makes it possible to proofread for malicious code\n  * no preparations / setup necessary, just run the sfx (which is also plaintext)\n* adaptable, malleable, hackable\n  * no build steps; modify the js/python without needing node.js or anything like that\n\nbecoming 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!)\n\n\n## notes\n\ngeneral notes:\n* paper-printing is affected by dark/light-mode! use lightmode for color, darkmode for grayscale\n  * because no browsers currently implement the media-query to do this properly orz\n\nbrowser-specific:\n* iPhone/iPad: use Firefox to download files\n* Android-Chrome: increase \"parallel uploads\" for higher speed (android bug)\n* Android-Firefox: takes a while to select files (their fix for ☝️)\n* Desktop-Firefox: ~~may use gigabytes of RAM if your files are massive~~ *seems to be OK now*\n* 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`\n\nserver-os-specific:\n* RHEL8 / Rocky8: you can run copyparty using `/usr/libexec/platform-python`\n\nserver notes:\n* pypy is supported but regular cpython is faster if you enable the database\n\n\n# bugs\n\nroughly sorted by chance of encounter\n\n* general:\n  * `--th-ff-jpg` may fix video thumbnails on some FFmpeg versions (macos, some linux)\n  * `--th-ff-swr` may fix audio thumbnails on some FFmpeg versions\n  * 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\n    * use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db and thumbnails on a local disk instead\n    * or, if you only want to move the db (and not the thumbnails), then use `--dbpath` or the `dbpath` volflag\n  * all volumes must exist / be available on startup; up2k (mtp especially) gets funky otherwise\n  * probably more, pls let me know\n\n* python 3.4 and older (including 2.7):\n  * many rare and exciting edge-cases because [python didn't handle EINTR yet](https://peps.python.org/pep-0475/)\n    * downloads from copyparty may suddenly fail, but uploads *should* be fine\n\n* python 2.7 on Windows:\n  * cannot index non-ascii filenames with `-e2d`\n  * cannot handle filenames with mojibake\n\nif you have a new exciting bug to share, see [reporting bugs](#reporting-bugs)\n\n\n## not my bugs\n\nsame order here too\n\n* [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\n\n* [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)\n\n* [Chrome issue 383568268](https://issues.chromium.org/issues/383568268) -- filereaders in webworkers can OOM / crash the browser-tab\n  * copyparty has a workaround which seems to work well enough\n\n* [Firefox issue 1790500](https://bugzilla.mozilla.org/show_bug.cgi?id=1790500) -- entire browser can crash after uploading ~4000 small files\n\n* 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\n\n* Android: music playback randomly stops due to [battery usage settings](#fix-unreliable-playback-on-android)\n\n* 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)\n  * `AudioContext` will probably never be a viable workaround as apple introduces new issues faster than they fix current ones\n\n* iPhones: music volume goes on a rollercoaster during song changes\n  * nothing I can do about it because `AudioContext` is still broken in safari\n\n* 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\n  * just a hunch, but disabling preloading may cause playback to stop entirely, or possibly mess with bluetooth speakers\n  * tried to add a tooltip regarding this but looks like apple broke my tooltips\n\n* iPhones: preloaded awo files make safari log MEDIA_ERR_NETWORK errors as playback starts, but the song plays just fine so eh whatever\n  * awo, opus-weba, is apple's new take on opus support, replacing opus-caf which was technically limited to cbr opus\n\n* iPhones: preloading another awo file may cause playback to stop\n  * 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...\n\n* Windows: folders cannot be accessed if the name ends with `.`\n  * python or windows bug\n\n* Windows: msys2-python 3.8.6 occasionally throws `RuntimeError: release unlocked lock` when leaving a scoped mutex in up2k\n  * this is an msys2 bug, the regular windows edition of python is fine\n\n* VirtualBox: sqlite throws `Disk I/O Error` when running in a VM and the up2k database is in a vboxsf\n  * use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db and thumbnails inside the vm instead\n    * or, if you only want to move the db (and not the thumbnails), then use `--dbpath` or the `dbpath` volflag\n  * also happens on mergerfs, so put the db elsewhere\n\n* Ubuntu: dragging files from certain folders into firefox or chrome is impossible\n  * due to snap security policies -- see `snap connections firefox` for the allowlist, `removable-media` permits all of `/mnt` and `/media` apparently\n\n\n# breaking changes\n\nupgrade notes\n\n* `1.9.16` (2023-11-04):\n  * `--stats`/prometheus: `cpp_bans` renamed to `cpp_active_bans`, and that + `cpp_uptime` are gauges\n* `1.6.0` (2023-01-29):\n  * http-api: delete/move is now `POST` instead of `GET`\n  * everything other than `GET` and `HEAD` must pass [cors validation](#cors)\n* `1.5.0` (2022-12-03): [new chunksize formula](https://github.com/9001/copyparty/commit/54e1c8d261df) for files larger than 128 GiB\n  * **users:** upgrade to the latest [cli uploader](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) if you use that\n  * **devs:** update third-party up2k clients (if those even exist)\n\n\n# FAQ\n\n\"frequently\" asked questions\n\n* CopyParty?\n  * nope! the name is either copyparty (all-lowercase) or Copyparty -- it's [one word](https://en.wiktionary.org/wiki/copyparty) after all :>\n\n* what is a volflag?\n  * 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)\n\n* what is a volume?\n  * 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)\n\n* can I change the 🌲 spinning pine-tree loading animation?\n  * [yeah...](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#boring-loader-spinner) :-(\n\n* is it possible to block read-access to folders unless you know the exact URL for a particular file inside?\n  * yes, using the [`g` permission](#accounts-and-volumes), see the examples there\n  * 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\n\n* can I link someone to a password-protected volume/file by including the password in the URL?\n  * 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\n    * if you have enabled `--usernames` then do `?pw=username:password` instead\n    * `?pw` can be disabled with `--pw-urlp=A` but this breaks support for many clients\n\n* how do I stop `.hist` folders from appearing everywhere on my HDD?\n  * 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)\n\n* can I make copyparty download a file to my server if I give it a URL?\n  * yes, using [hooks](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/wget.py)\n\n* 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\n  * 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\n\n* the server keeps saying `thank you for playing` when I try to access the website\n  * 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\n\n* copyparty seems to think I am using http, even though the URL is https\n  * 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\n\n* thumbnails are broken (you get a colorful square which says the filetype instead)\n  * you need to install `FFmpeg` or `Pillow`; see [thumbnails](#thumbnails)\n\n* thumbnails are broken, specifically for photos and videos taken by iphones\n  * 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)\n\n* thumbnails are broken (some images appear, but other files just get a blank box, and/or the broken-image placeholder)\n  * probably due to a reverse-proxy messing with the request URLs and stripping the query parameters (`?th=w`), so check your URL rewrite rules\n  * 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\n  * could also be due to misbehaving privacy-related browser extensions, so try to disable those\n\n* i want to learn python and/or programming and am considering looking at the copyparty source code in that occasion\n  * ```bash\n     _|  _      __   _  _|_\n    (_| (_)     | | (_)  |_\n    ```\n\n\n# accounts and volumes\n\nper-folder, per-user permissions  - if your setup is getting complex, consider making a [config file](./docs/example.conf) instead of using arguments\n* 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)\n  * changes to the `[global]` config section requires a restart to take effect\n\na quick summary can be seen using [`--help-accounts`](https://copyparty.eu/cli/#accounts-help-page)\n\nconfiguring accounts/volumes with arguments:\n* `-a usr:pwd` adds account `usr` with password `pwd`\n* `-v .::r` adds current-folder `.` as the webroot, `r`eadable by anyone\n  * the syntax is `-v src:dst:perm:perm:...` so local-path, url-path, and one or more permissions to set\n  * granting the same permissions to multiple accounts:  \n    `-v .::r,usr1,usr2:rw,usr3,usr4` = usr1/2 read-only, 3/4 read-write\n\npermissions:\n* `r` (read): browse folder contents, download files, download as zip/tar, see filekeys/dirkeys\n* `w` (write): upload files, move/copy files *into* this folder\n* `m` (move): move files/folders *from* this folder\n* `d` (delete): delete files/folders\n* `.` (dots): user can ask to show dotfiles in directory listings\n* `g` (get): only download files, cannot see folder contents or zip/tar\n* `G` (upget): same as `g` except uploaders get to see their own [filekeys](#filekeys) (see `fk` in examples below)\n* `h` (html): same as `g` except folders return their index.html, and filekeys are not necessary for index.html\n* `a` (admin): can see upload time, uploader IPs, config-reload\n* `A` (\"all\"): same as `rwmda.` (read/write/move/delete/admin/dotfiles)\n\nexamples:\n* add accounts named u1, u2, u3 with passwords p1, p2, p3: `-a u1:p1 -a u2:p2 -a u3:p3`\n* make folder `/srv` the root of the filesystem, read-only by anyone: `-v /srv::r`\n* 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`\n  * unauthorized users accessing the webroot can see that the `music` folder exists, but cannot open it\n* make folder `/mnt/incoming` available at `/inc`, write-only for u1, read-move for u2: `-v /mnt/incoming:inc:w,u1:rm,u2`\n  * unauthorized users accessing the webroot can see that the `inc` folder exists, but cannot open it\n  * `u1` can open the `inc` folder, but cannot see the contents, only upload new files to it\n  * `u2` can browse it and move files *from* `/inc` into any folder where `u2` has write-access\n* 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`\n  * `c,fk=4` sets the `fk` ([filekey](#filekeys)) volflag to 4, meaning each file gets a 4-character accesskey\n  * `u1` can upload files, browse the folder, and see the generated filekeys\n  * other users cannot browse the folder, but can access the files if they have the full file URL with the filekey\n  * replacing the `g` permission with `wg` would let anonymous users upload files, but not see the required filekey to access it\n  * replacing the `g` permission with `wG` would let anonymous users upload files, receiving a working direct link in return\n\nif 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`\n\n* 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`\n* single users can also be subtracted from a group: `@admins,-james`\n\nanyone trying to bruteforce a password gets banned according to `--ban-pw`; default is 24h ban for 9 failed attempts in 1 hour\n\nand 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`\n\n* you can also `PRTY_CONFIG=foobar.conf python copyparty-sfx.py` (convenient in docker etc)\n\n```yaml\n[accounts]\n  u1: p1  # create account \"u1\" with password \"p1\"\n  u2: p2  #  (note that comments must have\n  u3: p3  #   two spaces before the # sign)\n\n[groups]\n  g1: u1, u2  # create a group\n\n[/]     # this URL will be mapped to...\n  /srv  # ...this folder on the server filesystem\n  accs:\n    r: *  # read-only for everyone, no account necessary\n\n[/music]       # create another volume at this URL,\n  /mnt/music   # which is mapped to this folder\n  accs:\n    r: u1, u2  # only these accounts can read,\n    r: @g1     # (exactly the same, just with a group instead)\n    r: @acct   # (alternatively, ALL users who are logged in)\n    rw: u3     # and only u3 can read-write\n\n[/inc]\n  /mnt/incoming\n  accs:\n    w: u1   # u1 can upload but not see/download any files,\n    rm: u2  # u2 can browse + move files out of this volume\n\n[/i]\n  /mnt/ss\n  accs:\n    rw: u1  # u1 can read-write,\n    g: *    # everyone can access files if they know the URL\n  flags:\n    fk: 4   # each file URL will have a 4-character password\n```\n\n\n## shadowing\n\nhiding specific subfolders  by mounting another volume on top of them\n\nfor 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`\n\nthe 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\n\n> ℹ️ this also works for single files, because files can also be volumes\n\n\n## dotfiles\n\nunix-style hidden files/folders  by starting the name with a dot\n\nanyone can access these if they know the name, but they normally don't appear in directory listings\n\na 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 `.`\n\ndotfiles do not appear in search results unless one of the above is true, **and** the global option / volflag `dotsrch` is set\n\n> 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\n\nconfig file example, where the same permission to see dotfiles is given in two different ways just for reference:\n\n```yaml\n[/foo]\n  /srv/foo\n  accs:\n    r.: ed   # user \"ed\" has read-access + dot-access in this volume;\n             # dotfiles are visible in listings, but not in searches\n  flags:\n    dotsrch  # dotfiles will now appear in search results too\n    dots     # another way to let everyone see dotfiles in this vol\n```\n\n\n# the browser\n\naccessing a copyparty server using a web-browser\n\n![copyparty-browser-fs8](https://user-images.githubusercontent.com/241032/192042695-522b3ec7-6845-494a-abdb-d1c0d0e23801.png)\n\n\n## tabs\n\nthe main tabs in the ui\n* `[🔎]` [search](#searching) by size, date, path/name, mp3-tags ...\n* `[🧯]` [unpost](#unpost): undo/delete accidental uploads\n* `[🚀]` and `[🎈]` are the [uploaders](#uploading)\n* `[📂]` mkdir: create directories\n* `[📝]` new-file: create a new textfile\n* `[📟]` send-msg: either to server-log or into textfiles if `--urlform save`\n* `[🎺]` audio-player config options\n* `[⚙️]` general client config options\n\n\n## hotkeys\n\nthe browser has the following hotkeys  (always qwerty)\n* `?` show hotkeys help\n* `B` toggle breadcrumbs / [navpane](#navpane)\n* `I/K` prev/next folder\n* `M` parent folder (or unexpand current)\n* `V` toggle folders / textfiles in the navpane\n* `G` toggle list / [grid view](#thumbnails) -- same as `田` bottom-right\n* `T` toggle thumbnails / icons\n* `ESC` close various things\n* `ctrl-K` delete selected files/folders\n* `ctrl-X` cut selected files/folders\n* `ctrl-C` copy selected files/folders to clipboard\n* `ctrl-V` paste (move/copy)\n* `Y` download selected files\n* `F2` [rename](#batch-rename) selected file/folder\n* when a file/folder is selected (in not-grid-view):\n  * `Up/Down` move cursor\n  * shift+`Up/Down` select and move cursor\n  * ctrl+`Up/Down` move cursor and scroll viewport\n  * `Space` toggle file selection\n  * `Ctrl-A` toggle select all\n* when a textfile is open:\n  * `I/K` prev/next textfile\n  * `S` toggle selection of open file\n  * `M` close textfile\n* when playing audio:\n  * `J/L` prev/next song\n  * `U/O` skip 10sec back/forward\n  * `0..9` jump to 0%..90%\n  * `P` play/pause (also starts playing the folder)\n  * `Y` download file\n* when viewing images / playing videos:\n  * `J/L, Left/Right` prev/next file\n  * `Home/End` first/last file\n  * `F` toggle fullscreen\n  * `S` toggle selection\n  * `R` rotate clockwise (shift=ccw)\n  * `Y` download file\n  * `Esc` close viewer\n  * videos:\n    * `U/O` skip 10sec back/forward\n    * `0..9` jump to 0%..90%\n    * `P/K/Space` play/pause\n    * `M` mute\n    * `C` continue playing next video\n    * `V` loop entire file\n    * `[` loop range (start)\n    * `]` loop range (end)\n* when the navpane is open:\n  * `A/D` adjust tree width\n* in the [grid view](#thumbnails):\n  * `S` toggle multiselect\n  * shift+`A/D` zoom\n* in the markdown editor:\n  * `^s` save\n  * `^h` header\n  * `^k` autoformat table\n  * `^u` jump to next unicode character\n  * `^e` toggle editor / preview\n  * `^up, ^down` jump paragraphs\n\n\n## navpane\n\nswitching between breadcrumbs or navpane\n\nclick the `🌲` or pressing the `B` hotkey to toggle between breadcrumbs path (default), or a navpane (tree-browser sidebar thing)\n\n* `[+]` and `[-]` (or hotkeys `A`/`D`) adjust the size\n* `[🎯]` jumps to the currently open folder\n* `[📃]` toggles between showing folders and textfiles\n* `[📌]` shows the name of all parent folders in a docked panel\n* `[a]` toggles automatic widening as you go deeper\n* `[↵]` toggles wordwrap\n* `[👀]` show full name on hover (if wordwrap is off)\n\n\n## thumbnails\n\npress `g` or `田` to toggle grid-view instead of the file listing  and `t` toggles icons / thumbnails\n* can be made default globally with `--grid` or per-volume with volflag `grid`\n* enable by adding `?imgs` to a link, or disable with `?imgs=0`\n\n![copyparty-thumbs-fs8](https://user-images.githubusercontent.com/241032/129636211-abd20fa2-a953-4366-9423-1c88ebb96ba9.png)\n\nit 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\n* pyvips is 3x faster than Pillow, Pillow is 3x faster than FFmpeg\n* disable thumbnails for specific volumes with volflag `dthumb` for all, or `dvthumb` / `dathumb` / `dithumb` for video/audio/images only\n* for installing FFmpeg on windows, see [optional dependencies](#optional-dependencies)\n\naudio files are converted into spectrograms using FFmpeg unless you `--no-athumb` (and some FFmpeg builds may need `--th-ff-swr`)\n\nimages with the following names (see `--th-covers`) become the thumbnail of the folder they're in: `folder.png`, `folder.jpg`, `cover.png`, `cover.jpg`\n* 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`)\n* 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)\n\nenabling `multiselect` lets you click files to select them, and then shift-click another file for range-select\n* `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\n  * the `sel` option can be made default globally with `--gsel` or per-volume with volflag `gsel`\n\nto 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`\n* optionally as separate volflags for each mapping; see config file example below\n* the supported image formats are [jpg, png, gif, webp, ico](https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types)\n  * 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\n\nnote:\n* 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)\n\nconfig file example:\n\n```yaml\n[global]\n  no-thumb   # disable ALL thumbnails and audio transcoding\n  no-vthumb  # only disable video thumbnails\n\n[/music]\n  /mnt/nas/music\n  accs:\n    r: *     # everyone can read\n  flags:\n    dthumb   # disable ALL thumbnails and audio transcoding\n    dvthumb  # only disable video thumbnails\n    ext-th:  exe=/ico/exe.png  # /ico/exe.png is the thumbnail of *.exe\n    ext-th:  elf=/ico/elf.gif  # ...and /ico/elf.gif is used for *.elf\n    th-covers:  folder.png,folder.jpg,cover.png,cover.jpg  # the default\n```\n\n\n## zip downloads\n\ndownload folders (or file selections) as `zip` or `tar` files\n\nselect which type of archive you want in the `[⚙️] config` tab:\n\n| name | url-suffix | description |\n|--|--|--|\n| `tar` | `?tar` | plain gnutar, works great with `curl \\| tar -xv` |\n| `pax` | `?tar=pax` | pax-format tar, futureproof, not as fast |\n| `tgz` | `?tar=gz` | gzip compressed gnu-tar (slow), for `curl \\| tar -xvz` |\n| `txz` | `?tar=xz` | gnu-tar with xz / lzma compression (v.slow) |\n| `zip` | `?zip` | works everywhere, glitchy filenames on win7 and older |\n| `zip_dos` | `?zip=dos` | traditional cp437 (no unicode) to fix glitchy filenames |\n| `zip_crc` | `?zip=crc` | cp437 with crc32 computed early for truly ancient software |\n\n* gzip default level is `3` (0=fast, 9=best), change with `?tar=gz:9`\n* xz default level is `1` (0=fast, 9=best), change with `?tar=xz:9`\n* bz2 default level is `2` (1=fast, 9=best), change with `?tar=bz2:9`\n* hidden files ([dotfiles](#dotfiles)) are excluded unless account is allowed to list them\n  * `up2k.db` and `dir.txt` is always excluded\n* bsdtar supports streaming unzipping: `curl foo?zip | bsdtar -xv`\n  * good, because copyparty's zip is faster than tar on small files\n    * but `?tar` is better for large files, especially if the total exceeds 4 GiB\n* `zip_crc` will take longer to download since the server has to read each file twice\n  * this is only to support MS-DOS PKZIP v2.04g (october 1993) and older\n    * how are you accessing copyparty actually\n\nyou 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\n\n![copyparty-zipsel-fs8](https://user-images.githubusercontent.com/241032/129635374-e5136e01-470a-49b1-a762-848e8a4c9cdc.png)\n\ncool 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\n* 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\n* and url-param `&nodot` skips dotfiles/dotfolders; they are included by default if your account has permission to see them\n* and url-params `&j` / `&w` produce jpeg/webm thumbnails/spectrograms instead of the original audio/video/images (`&p` for audio waveforms)\n  * can also be used to pregenerate thumbnails; combine with `--th-maxage=9999999` or `--th-clean=0`\n\n\n## uploading\n\ndrag files/folders into the web-browser to upload\n\ndragdrop is the recommended way, but you may also:\n\n* select some files (not folders) in your file explorer and press CTRL-V inside the browser window\n* use the [command-line uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy)\n* upload using [curl, sharex, ishare, ...](#client-examples)\n\nwhen uploading files through dragdrop or CTRL-V, this initiates an upload using `up2k`; there are two browser-based uploaders available:\n* `[🎈] bup`, the basic uploader, supports almost every browser since netscape 4.0\n* `[🚀] up2k`, the good / fancy one\n\nNB: 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)\n\nup2k has several advantages:\n* you can drop folders into the browser (files are added recursively)\n* files are processed in chunks, and each chunk is checksummed\n  * uploads autoresume if they are interrupted by network issues\n  * uploads resume if you reboot your browser or pc, just upload the same files again\n  * server detects any corruption; the client reuploads affected chunks\n  * the client doesn't upload anything that already exists on the server\n  * no filesize limit, even when a proxy limits the request size (for example Cloudflare)\n* much higher speeds than ftp/scp/tarpipe on some internet connections (mainly american ones) thanks to parallel connections\n* the last-modified timestamp of the file is preserved\n\n> it is perfectly safe to restart / upgrade copyparty while someone is uploading to it!  \n> all known up2k clients will resume just fine 💪\n\nsee [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)\n\n![copyparty-upload-fs8](https://user-images.githubusercontent.com/241032/129635371-48fc54ca-fa91-48e3-9b1d-ba413e4b68cb.png)\n\n**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)\n\n**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\n\nthe up2k UI is the epitome of polished intuitive experiences:\n* \"parallel uploads\" specifies how many chunks to upload at the same time\n* `[🏃]` analysis of other files should continue while one is uploading\n* `[🥔]` shows a simpler UI for faster uploads from slow devices\n* `[🛡️]` decides when to overwrite existing files on the server\n  * `🛡️` = never (generate a new filename instead)\n  * `🕒` = overwrite if the server-file is older\n  * `♻️` = always overwrite if the files are different\n* `[🎲]` generate random filenames during upload\n* `[🔎]` switch between upload and [file-search](#file-search) mode\n  * ignore `[🔎]` if you add files by dragging them into the browser\n\nand then there's the tabs below it,\n* `[ok]` is the files which completed successfully\n* `[ng]` is the ones that failed / got rejected (already exists, ...)\n* `[done]` shows a combined list of `[ok]` and `[ng]`, chronological order\n* `[busy]` files which are currently hashing, pending-upload, or uploading\n  * plus up to 3 entries each from `[done]` and `[que]` for context\n* `[que]` is all the files that are still queued\n\nnote 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)\n\nif 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\n\nif 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)\n\nif 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)\n\n\n### file-search\n\ndropping files into the browser also lets you see if they exist on the server\n\n![copyparty-fsearch-fs8](https://user-images.githubusercontent.com/241032/129635361-c79286f0-b8f1-440e-aaf4-6e929428fac9.png)\n\nwhen you drag/drop files into the browser, you will see two dropzones: `Upload` and `Search`\n\n> on a phone? toggle the `[🔎]` switch green before tapping the big yellow Search button to select your files\n\nthe files will be hashed on the client-side, and each hash is sent to the server, which checks if that file exists somewhere\n\nfiles go into `[ok]` if they exist (and you get a link to where it is), otherwise they land in `[ng]`\n* 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\n\nif 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\n\n\n### unpost\n\nundo/delete accidental uploads  using the `[🧯]` tab in the UI\n\n![copyparty-unpost-fs8](https://user-images.githubusercontent.com/241032/129635368-3afa6634-c20f-418c-90dc-ec411f3b3897.png)\n\nyou 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`\n\nconfig file example:\n\n```yaml\n[global]\n  e2d            # enable up2k database (remember uploads)\n  unpost: 43200  # 12 hours (default)\n```\n\n\n### self-destruct\n\nuploads can be given a lifetime,  after which they expire / self-destruct\n\nthe 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\n\nclients 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)\n\nspecifying 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\n\n\n### race the beam\n\ndownload 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\n\nrequires the file to be uploaded using up2k (which is the default drag-and-drop uploader), alternatively the command-line program\n\n\n### incoming files\n\nthe control-panel shows the ETA for all incoming files  , but only for files being uploaded into volumes where you have read-access\n\n![copyparty-cpanel-upload-eta-or8](https://github.com/user-attachments/assets/fd275ffa-698c-4fca-a307-4d2181269a6a)\n\n\n## file manager\n\ncut/paste, rename, and delete files/folders (if you have permission)\n\nfile selection: click somewhere on the line (not the link itself), then:\n* `space` to toggle\n* `up/down` to move\n* `shift-up/down` to move-and-select\n* `ctrl-shift-up/down` to also scroll\n* shift-click another line for range-select\n\n* cut: select some files and `ctrl-x`\n* copy: select some files and `ctrl-c`\n* paste: `ctrl-v` in another folder\n* rename: `F2`\n\nyou can copy/move files across browser tabs (cut/copy in one tab, paste in another)\n\n\n## shares\n\nshare a file or folder by creating a temporary link\n\nwhen enabled in the server settings (`--shr`), click the bottom-right `share` button to share the folder you're currently in, or alternatively:\n* select a folder first to share that folder instead\n* select one or more files to share only those files\n\nthis 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\n\nwhen creating a share, the creator can choose any of the following options:\n\n* password-protection\n* expire after a certain time; `0` or blank means infinite\n* allow visitors to upload (if the user who creates the share has write-access)\n\nsemi-intentional limitations:\n\n* cleanup of expired shares only works when global option `e2d` is set, and/or at least one volume on the server has volflag `e2d`\n* 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\n* if you change [password hashing](#password-hashing) settings after creating a password-protected share, then that share will stop working\n* 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\n* no option to \"delete after first access\" because tricky\n  * when linking something to discord (for example) it'll get accessed by their scraper and that would count as a hit\n  * browsers wouldn't be able to resume a broken download unless the requester's IP gets allowlisted for X minutes (ref. tricky)\n\nspecify `--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\n\n* you can name it whatever, `foobar` is just an example\n* if you're using config files, put `shr: /foobar` inside the `[global]` section instead\n\nusers 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\n\nthe 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) \n\nafter 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\n\n**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.\n\n\n## batch rename\n\nselect some files and press `F2` to bring up the rename UI\n\n![batch-rename-fs8](https://user-images.githubusercontent.com/241032/128434204-eb136680-3c07-4ec7-92e0-ae86af20c241.png)\n\nquick explanation of the buttons,  \n* `[✅ apply rename]` confirms and begins renaming\n* `[❌ cancel]` aborts and closes the rename window\n* `[↺ reset]` reverts any filename changes back to the original name\n* `[decode]` does a URL-decode on the filename, fixing stuff like `&amp;` and `%20`\n* `[advanced]` toggles advanced mode\n\nadvanced 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\n\nin advanced mode,  \n* `[case]` toggles case-sensitive regex\n* `regex` is the regex pattern to apply to the original filename; any files which don't match will be skipped\n* `format` is the new filename, taking values from regex capturing groups and/or from file tags\n  * very loosely based on foobar2000 syntax\n* `presets` lets you save rename rules for later\n\navailable functions:\n* `$lpad(text, length, pad_char)`\n* `$rpad(text, length, pad_char)`\n\ntwo 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\n\nso,\n\nsay 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`\n\nyou could use just regex to rename it:\n* `regex` = `(.*) - (.*) - ([0-9]{2}) (.*)`\n* `format` = `(3). (1) - (4)`\n* `output` = `07. meganeko - Sirius A.mp3`\n\nor you could use just tags:\n* `format` = `$lpad((tn),2,0). (artist) - (title).(ext)`\n* `output` = `7. meganeko - Sirius A.mp3`\n\nor a mix of both:\n* `regex` = ` - ([0-9]{2}) `\n* `format` = `(1). (artist) - (title).(ext)`\n* `output` = `07. meganeko - Sirius A.mp3`\n\nthe 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`)\n\n\n## rss feeds\n\nmonitor a folder with your RSS reader  , optionally recursive\n\nmust be enabled per-volume with volflag `rss` or globally with `--rss`\n\nthe feed includes itunes metadata for use with podcast readers such as [AntennaPod](https://antennapod.org/)\n\na feed example: https://cd.ocv.me/a/d2/d22/?rss&fext=mp3\n\nurl parameters:\n\n* `pw=hunter2` for password auth\n  * if you enabled `--usernames` then do `pw=username:password` instead\n* `nopw` disables embedding the password (if provided) into item-URLs in the feed\n* `nopw=a` disables mentioning the password anywhere at all in the feed; may break some readers\n* `recursive` to also include subfolders\n* `title=foo` changes the feed title (default: folder name)\n* `fext=mp3,opus` only include mp3 and opus files (default: all)\n* `nf=30` only show the first 30 results (default: 250)\n* `sort=m` sort by mtime (file last-modified), newest first (default)\n  * `u` = upload-time; NOTE: non-uploaded files have upload-time `0`\n  * `n` = filename\n  * `a` = filesize\n  * uppercase = reverse-sort; `M` = oldest file first\n\n\n## opds feeds\n\nbrowse and download files from your e-book reader\n\nenabled with the `opds` volflag or `--opds` global option\n\nadd `?opds` to the end of the url you would like to browse, then input that in your opds client.\nfor example: `https://copyparty.example/books/?opds`.\n\nto log in with a password, enter it into either of the username or password fields in your client.\n\n- if you've enabled `--usernames`, then you need to enter both username and password .\n\nnote: some clients (e.g. Moon+ Reader) will not send the password when downloading cover images, which will\ncause your ip to be banned by copyparty. to work around this, you can grant the [`g` permission](#accounts-and-volumes)\nto unauthenticated requests and enable [filekeys](#filekeys) to prevent guessing filenames. for example:\n`-vbooks:books:r,ed:g:c,fk,opds`\n\nby default, not all file types will be listed in opds feeds. to change this, add the extension to \n`--opds-exts` (volflag: `opds_exts`), or empty the list to list everything\n\n\n## recent uploads\n\nlist all recent uploads  by clicking \"show recent uploads\" in the controlpanel\n\nwill show uploader IP and upload-time if the visitor has the admin permission\n\n* global-option `--ups-when` makes upload-time visible to all users, and not just admins\n\n* global-option `--ups-who` (volflag `ups_who`) specifies who gets access (0=nobody, 1=admins, 2=everyone), default=2\n\nnote that the [🧯 unpost](#unpost) feature is better suited for viewing *your own* recent uploads, as it includes the option to undo/delete them\n\nconfig file example:\n\n```yaml\n[global]\n  ups-when    # everyone can see upload times\n  ups-who: 1  # but only admins can see the list,\n              # so ups-when doesn't take effect\n```\n\n\n## media player\n\nplays almost every audio format there is  (if the server has FFmpeg installed for on-demand transcoding)\n\nthe following audio formats are usually always playable, even without FFmpeg: `aac|flac|m4a|mp3|ogg|opus|wav`\n\nsome highlights:\n* 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))\n* shows the audio waveform in the seekbar\n* not perfectly gapless but can get really close (see settings + eq below); good enough to enjoy gapless albums as intended\n* videos can be played as audio, without wasting bandwidth on the video\n* adding `?v` to the end of an audio/video/image link will make it open in the mediaplayer\n\nclick 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)\n\nopen the `[🎺]` media-player-settings tab to configure it,\n* \"switches\":\n  * `[🔁]` repeats one single song forever\n  * `[🔀]` shuffles the files inside each folder\n  * `[preload]` starts loading the next track when it's about to end, reduces the silence between songs\n  * `[full]` does a full preload by downloading the entire next file; good for unreliable connections, bad for slow connections\n  * `[~s]` toggles the seekbar waveform display\n  * `[/np]` enables buttons to copy the now-playing info as an irc message\n  * `[📻]` enables buttons to create an [m3u playlist](#playlists) with the selected songs\n  * `[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))\n  * `[seek]` allows seeking with lockscreen controls (buggy on some devices)\n  * `[art]` shows album art on the lockscreen\n  * `[🎯]` keeps the playing song scrolled into view (good when using the player as a taskbar dock)\n  * `[⟎]` shrinks the playback controls\n* \"buttons\":\n  * `[uncache]` may fix songs that won't play correctly due to bad files in browser cache\n* \"at end of folder\":\n  * `[loop]` keeps looping the folder\n  * `[next]` plays into the next folder\n* \"transcode\":\n  * `[flac]` converts `flac` and `wav` files into opus (if supported by browser) or mp3\n  * `[aac]` converts `aac` and `m4a` files into opus (if supported by browser) or mp3\n  * `[oth]` converts all other known formats into opus (if supported by browser) or mp3\n    * `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`\n* \"transcode to\":\n  * `[opus]` produces an `opus` whenever transcoding is necessary (the best choice on Android and PCs)\n  * `[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\n  * `[caf]` is `opus` in a `caf` file, good for iPhones (iOS 11 through 17), technically unsupported by Apple but works for the most part\n  * `[mp3]` -- the myth, the legend, the undying master of mediocre sound quality that definitely works everywhere\n  * `[flac]` -- lossless but compressed, for LAN and/or fiber playback on electrostatic headphones\n  * `[wav]` -- lossless and uncompressed, for LAN and/or fiber playback on electrostatic headphones connected to very old equipment\n    * `flac` and `wav` must be enabled with `--allow-flac` / `--allow-wav` to allow spending the disk space\n* \"tint\" reduces the contrast of the playback bar\n\n\n### playlists\n\ncreate 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)\n\nclick a file with the extension `m3u` or `m3u8` (for example `mixtape.m3u` or `touhou.m3u8` ) and you get two choices: Play / Edit\n\nplaylists 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\n\n\n### creating a playlist\n\nwith a standalone mediaplayer or copyparty\n\nyou 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\n\nalternatively, you can create the playlist using copyparty itself:\n\n* 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\n\n* 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)\n\n* at any time, click `📻copy` to send the playlist to your clipboard\n  * you can then continue adding more songs if you'd like\n  * if you want to wipe the playlist and start from scratch, just refresh the page\n\n* create a new textfile, name it `something.m3u` and paste the playlist there\n\n\n### audio equalizer\n\nand [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression)\n\ncan also boost the volume in general, or increase/decrease stereo width (like [crossfeed](https://www.foobar2000.org/components/view/foo_dsp_meiercf) just worse)\n\nhas the convenient side-effect of reducing the pause between songs, so gapless albums play better with the eq enabled (just make it flat)\n\nnot available on iPhones / iPads because AudioContext currently breaks background audio playback on iOS (15.7.8)\n\n\n### fix unreliable playback on android\n\ndue 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)\n\n\n## textfile viewer\n\nwith realtime streaming of logfiles and such ([demo](https://a.ocv.me/pub/demo/logtail/))  , and terminal colors work too\n\nclick `-txt-` next to a textfile to open the viewer, which has the following toolbar buttons:\n\n* `✏️ edit` opens the textfile editor\n* `📡 follow` starts monitoring the file for changes, streaming new lines in realtime\n  * similar to `tail -f`\n  * [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\n\n\n## markdown viewer\n\nand there are *two* editors\n\n![copyparty-md-read-fs8](https://user-images.githubusercontent.com/241032/115978057-66419080-a57d-11eb-8539-d2be843991aa.png)\n\nthere is a built-in extension for inline clickable thumbnails;\n* enable it by adding `<!-- th -->` somewhere in the doc\n* add thumbnails with `!th[l](your.jpg)` where `l` means left-align (`r` = right-align)\n* a single line with `---` clears the float / inlining\n* in the case of README.md being displayed below a file listing, thumbnails will open in the gallery viewer\n\nother notes,\n* the document preview has a max-width which is the same as an A4 paper when printed\n\n\n### markdown vars\n\ndynamic 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\n\nsee [./srv/expand/](./srv/expand/) for usage and examples\n\n\n## other tricks\n\n* 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`\n\n* 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\n\n* get a plaintext file listing by adding `?ls=t` to a URL, or a compact colored one with `?ls=v` (for unix terminals)\n\n* 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)\n\n* click the bottom-left `π` to open a javascript prompt for debugging\n\n* files named `.prologue.html` / `.epilogue.html` will be rendered before/after directory listings unless `--no-logues`\n\n* files named `descript.ion` / `DESCRIPT.ION` are parsed and displayed in the file listing, or as the epilogue if nonstandard\n\n* files named `README.md` / `readme.md` will be rendered after directory listings unless `--no-readme` (but `.epilogue.html` takes precedence)\n\n  * and `PREADME.md` / `preadme.md` is shown above directory listings unless `--no-readme` or `.prologue.html`\n\n* `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)\n\n\n## searching\n\nsearch by size, date, path/name, mp3-tags, ...\n\n![copyparty-search-fs8](https://user-images.githubusercontent.com/241032/129635365-c0ff2a9f-0ee5-4fc3-8bb6-006033cf67b8.png)\n\nwhen 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:\n* make search queries by `size`/`date`/`directory-path`/`filename`, or...\n* drag/drop a local file to see if the same contents exist somewhere on the server, see [file-search](#file-search)\n\npath/name queries are space-separated, AND'ed together, and words are negated with a `-` prefix, so for example:\n* 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\n* name: `demetori styx` gives you [good stuff](https://www.youtube.com/watch?v=zGh0g14ZJ8I&list=PL3A147BD151EE5218&index=9)\n\nthe `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)\n\nfor the above example to work, add the commandline argument `-e2ts` to also scan/index tags from music files, which brings us over to:\n\n\n# server config\n\nusing arguments or config files, or a mix of both:\n* config files (`-c some.conf`) can set additional commandline arguments; see [./docs/example.conf](docs/example.conf) and [./docs/example2.conf](docs/example2.conf)\n* `kill -s USR1` (same as `systemctl reload copyparty`) to reload accounts and volumes from config files without restarting\n  * or click the `[reload cfg]` button in the control-panel if the user has `a`/admin in any volume\n  * changes to the `[global]` config section requires a restart to take effect\n\n**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).\n* if running in docker/podman, try this: `docker run --rm -it copyparty/ac --help`\n* or if you prefer plaintext, https://copyparty.eu/helptext.txt\n\n\n## version-checker\n\nsleep better at night  by telling copyparty to periodically check whether your version has a [known vulnerability](https://github.com/9001/copyparty/security/advisories)\n\nthis 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\n* `https://api.copyparty.eu/advisories`\n* `https://api.github.com/repos/9001/copyparty/security-advisories?per_page=9`\n\n> to see what happens when a bad version is detected, try `--vc-url https://api.copyparty.eu/advisories-test`\n\nalso consider the following options:\n* global-option `--vc-age` is how often (in hours) to check that URL; default is 3\n* global-option `--vc-exit` can be enabled to panic and immediately exit if a vulnerability is indicated\n  * if `--vc-exit` is not enabled, it just shows a warning on the controlpanel for all users with permission `a` or `A`\n\nconfig file example:\n\n```yaml\n[global]\n  vc-url: https://api.copyparty.eu/advisories\n  vc-age: 3  # how many hours to wait between each check\n  vc-exit    # emergency-exit if current version is vulnerable\n```\n\n\n## logging\n\nserverlog is sent to stdout by default  (but logging to a file is also possible)\n\n\"stdout\" usually means either the terminal, or journalctl, or whatever is collecting logs from your docker containers, so that depends on your setup\n\n* [-q](https://copyparty.eu/cli/#g-q) disables logging to stdout, and may improve performance a little bit\n  * combine it with `-lo logfolder/cpp-%Y-%m-%d.txt` to log to a file instead\n  * the `%Y-%m-%d` makes it create a new logfile every day, with the date as filename\n* `-lo whatever.txt` can be used without `-q` to log to both at the same time\n  * by default, the logfile will have colors if the terminal does (usually the case)\n  * 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\n* if you want [no colors](https://youtu.be/biW5UVGkPMA?t=148):\n  * `--flo 2` disables colors for just the logfile\n  * `--no-ansi` disables colors for both the terminal and logfile\n\nconfig file example:\n\n```yaml\n[global]\n  log-date: %Y-%m-%d  # show dates on stdout too\n  lo: /var/log/cpp/%Y-%m-%d.txt  # logfile path\n  flo: 2  # just text (no colors) in logfile\n  q       # disable stdout; use logfile only\n```\n\n\n## zeroconf\n\nannounce 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)\n\n* `--z-on` / `--z-off` limits the feature to certain networks\n\nconfig file example:\n\n```yaml\n[global]\n  z      # enable all zeroconf features (mdns, ssdp)\n  zm     # only enables mdns (does nothing since we already have z)\n  z-on: 192.168.0.0/16, 10.1.2.0/24  # restrict to certain subnets\n```\n\n\n### mdns\n\nLAN domain-name and feature announcer\n\nuses [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\n\nall enabled services ([webdav](#webdav-server), [ftp](#ftp-server), [smb](#smb-server)) will appear in mDNS-aware file managers (KDE, gnome, macOS, ...)\n\nthe domain will be `partybox.local` if the machine's hostname is `partybox` unless `--name` specifies something else\n\nand the web-UI will be available at http://partybox.local:3923/\n\n* 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)\n\n\n### ssdp\n\nwindows-explorer announcer\n\nuses [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\n\ndoubleclicking the icon opens the \"connect\" page which explains how to mount copyparty as a local filesystem\n\nif copyparty does not appear in windows explorer, use `--zsv` to see why:\n\n* maybe the discovery multicast was sent from an IP which does not intersect with the server subnets\n\n\n## qr-code\n\nprint 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\n\n* `--qr` enables it\n* `--qrs` does https instead of http\n* `--qrl lootbox/?pw=hunter2` appends to the url, linking to the `lootbox` folder with password `hunter2`\n* `--qrz 1` forces 1x zoom instead of autoscaling to fit the terminal size\n  * 1x may render incorrectly on some terminals/fonts, but 2x should always work\n* `--qr-pin 1` makes the qr-code stick to the bottom of the console (never scrolls away)\n* `--qr-file qr.txt:1:2` writes a small qr-code to `qr.txt`\n* `--qr-file qr.txt:2:2` writes a big qr-code to `qr.txt`\n* `--qr-file qr.svg:1:2` writes a vector-graphics qr-code to `qr.svg`\n* `--qr-file qr.png:8:4:333333:ffcc55` writes an 8x-magnified yellow-on-gray `qr.png`\n* `--qr-file qr.png:8:4::ffffff` writes an 8x-magnified white-on-transparent `qr.png`\n\nit 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\n\n\n## ftp server\n\nan FTP server can be started using `--ftp 3921`,  and/or `--ftps` for explicit TLS (ftpes)\n\n* based on [pyftpdlib](https://github.com/giampaolo/pyftpdlib)\n* needs a dedicated port (cannot share with the HTTP/HTTPS API)\n* uploads are not resumable -- delete and restart if necessary\n* runs in active mode by default, you probably want `--ftp-pr 12000-13000`\n  * if you enable both `ftp` and `ftps`, the port-range will be divided in half\n  * some older software (filezilla on debian-stable) cannot passive-mode with TLS\n* login with any username + your password, or put your password in the username field\n  * unless you enabled `--usernames`\n\nsome recommended FTP / FTPS clients; `wark` = example password:\n* https://winscp.net/eng/download.php\n* https://filezilla-project.org/ struggles a bit with ftps in active-mode, but is fine otherwise\n* https://rclone.org/ does FTPS with `tls=false explicit_tls=true`\n* `lftp -u k,wark -p 3921 127.0.0.1 -e ls`\n* `lftp -u k,wark -p 3990 127.0.0.1 -e 'set ssl:verify-certificate no; ls'`\n* `curl ftp://127.0.0.1:3921/` (plaintext ftp)\n* `curl --ssl-reqd ftp://127.0.0.1:3990/` (encrypted ftps)\n\nconfig file example, which restricts FTP to only use ports 3921 and 12000-12099 so all of those ports must be opened in your firewall:\n\n```yaml\n[global]\n  ftp: 3921\n  ftp-pr: 12000-12099\n```\n\n\n## sftp server\n\ngoes roughly 700 MiB/s (slower than webdav and ftp)\n\n> 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\n\nthe sftp-server requires the optional dependency [paramiko](https://pypi.org/project/paramiko/);\n* if you are **not** using docker, then install paramiko somehow\n* if you **are** using docker, then use one of the following image variants: `ac` / `im` / `iv` / `dj`\n\nenable sftpd with `--sftp 3922` to listen on port 3922;\n* use global-option `sftp-key` to associate an ssh-key with a user;\n  * commandline: `--sftp-key 'david ssh-ed25519 AAAAC3NzaC...'`\n  * config-file: `sftp-key: david ssh-ed25519 AAAAC3NzaC...`\n* `--sftp-pw` enables login with passwords (default is ssh-keys only)\n* `--sftp-anon foo` enables login with username `foo` and no password; gives the same access/permissions as the website does when not logged in\n\nsee the [sftp section in --help](https://copyparty.eu/cli/#g-sftp) for the other options\n\n\n## webdav server\n\nwith 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)\n\nclick the [connect](http://127.0.0.1:3923/?hc) button in the control-panel to see connection instructions for windows, linux, macos\n\ngeneral usage:\n* login with any username + your password, or put your password in the username field (password field can be empty/whatever)\n  * unless you enabled `--usernames`\n\non macos, connect from finder:\n* [Go] -> [Connect to Server...] -> http://192.168.123.1:3923/\n\nto 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\n\n> 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)\n\n\n### connecting to webdav from windows\n\nusing the GUI  (winXP or later):\n* rightclick [my computer] -> [map network drive] -> Folder: `http://192.168.123.1:3923/`\n  * on winXP only, click the `Sign up for online storage` hyperlink instead and put the URL there\n  * providing your password as the username is recommended; the password field can be anything or empty\n    * unless you enabled `--usernames`\n\nthe webdav client that's built into windows has the following list of bugs; you can avoid all of these by connecting with rclone instead:\n* 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\n  * or just type your password into the username field instead to get around it entirely\n* connecting to a folder which allows anonymous read will make writing impossible, as windows has decided it doesn't need to login\n  * workaround: connect twice; first to a folder which requires auth, then to the folder you actually want, and leave both of those mounted\n  * or set the server-option `--dav-auth` to force password-auth for all webdav clients\n* win7+ may open a new tcp connection for every file and sometimes forgets to close them, eventually needing a reboot\n  * maybe NIC-related (??), happens with win10-ltsc on e1000e but not virtio\n* windows cannot access folders which contain filenames with invalid unicode or forbidden characters (`<>:\"/\\|?*`), or names ending with `.`\n* winxp cannot show unicode characters outside of *some range*\n  * latin-1 is fine, hiragana is not (not even as shift-jis on japanese xp)\n\n\n## tftp server\n\na 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))\n\n> that makes this the first RTX DECT Base that has been updated using copyparty 🎉\n\n* based on [partftpy](https://github.com/9001/partftpy)\n* no accounts; read from world-readable folders, write to world-writable, overwrite in world-deletable\n* needs a dedicated port (cannot share with the HTTP/HTTPS API)\n  * run as root (or see below) to use the spec-recommended port `69` (nice)\n* can reply from a predefined portrange (good for firewalls)\n* only supports the binary/octet/image transfer mode (no netascii)\n* [RFC 7440](https://datatracker.ietf.org/doc/html/rfc7440) is **not** supported, so will be extremely slow over WAN\n  * assuming default blksize (512), expect 1100 KiB/s over 100BASE-T, 400-500 KiB/s over wifi, 200 on bad wifi\n\nmost 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;\n* on linux: `iptables -t nat -A PREROUTING -i eth0 -p udp --dport 69 -j REDIRECT --to-port 3969`\n\nsome recommended TFTP clients:\n* curl (cross-platform, read/write)\n  * get: `curl --tftp-blksize 1428 tftp://127.0.0.1:3969/firmware.bin`\n  * put: `curl --tftp-blksize 1428 -T firmware.bin tftp://127.0.0.1:3969/`\n* windows: `tftp.exe` (you probably already have it)\n  * `tftp -i 127.0.0.1 put firmware.bin`\n* linux: `tftp-hpa`, `atftp`\n  * `atftp --option \"blksize 1428\" 127.0.0.1 3969 -p -l firmware.bin -r firmware.bin`\n  * `tftp -v -m binary 127.0.0.1 3969 -c put firmware.bin`\n\n\n## smb server\n\nunsafe, slow, not recommended for wan,  enable with `--smb` for read-only or `--smbw` for read-write\n\nclick the [connect](http://127.0.0.1:3923/?hc) button in the control-panel to see connection instructions for windows, linux, macos\n\ndependencies: `python3 -m pip install --user -U impacket==0.13.0`\n* newer versions of impacket will hopefully work just fine but there is monkeypatching so maybe not\n\nsome **BIG WARNINGS** specific to SMB/CIFS, in decreasing importance:\n* not entirely confident that read-only is read-only\n* 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)\n  * 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\n  * [shadowing](#shadowing) probably works as expected but no guarantees\n* not compatible with pw-hashing or `--usernames`\n\nand some minor issues,\n* clients only see the first ~400 files in big folders;\n  * 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\n* hot-reload of server config (`/?reload=cfg`) does not include the `[global]` section (commandline args)\n* listens on the first IPv4 `-i` interface only (default = :: = 0.0.0.0 = all)\n* login doesn't work on winxp, but anonymous access is ok -- remove all accounts from copyparty config for that to work\n  * win10 onwards does not allow connecting anonymously / without accounts\n* python3 only\n* slow (the builtin webdav support in windows is 5x faster, and rclone-webdav is 30x faster)\n  * those numbers are specifically for copyparty's smb-server (because it sucks); other smb-servers should be similar to webdav\n\nknown client bugs:\n* on win7 only, `--smb1` is much faster than smb2 (default) because it keeps rescanning folders on smb2\n  * however smb1 is buggy and is not enabled by default on win10 onwards\n* windows cannot access folders which contain filenames with invalid unicode or forbidden characters (`<>:\"/\\|?*`), or names ending with `.`\n\nthe 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;\n* on linux: `iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 445 -j REDIRECT --to-port 3945`\n\nauthenticate with one of the following:\n* username `$username`, password `$password`\n* username `$password`, password `k`\n\n\n## browser ux\n\ntweaking the ui\n\n* 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\n  * 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`\n  * 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`\n  * 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`\n\nsee [./docs/rice](./docs/rice) for more, including:\n* how to [hide ui-elements](./docs/rice/README.md#hide-ui-elements)\n* [custom fonts](./docs/rice/README.md#custom-fonts)\n* [custom loading-spinner](./docs/rice/README.md#boring-loader-spinner)\n* adding stuff (css/`<meta>`/...) [to the html `<head>` tag](./docs/rice/README.md#head)\n* [adding your own translation](./docs/rice/README.md#translations)\n\n\n## opengraph\n\ndiscord and social-media embeds\n\ncan be enabled globally with `--og` or per-volume with volflag `og`\n\nnote 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`)\n\nyou can also hotlink files regardless by appending `?raw` to the url\n\n> WARNING: if you plan to use WebDAV, then `--og-ua` / `og_ua` must be configured\n\nif 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)\n\n\n## file deduplication\n\nenable symlink-based upload deduplication  globally with `--dedup` or per-volume with volflag `dedup`\n\nby 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\n\nif you enable deduplication with `--dedup` then it'll create a symlink instead of a full copy, thus reducing disk space usage\n\n* 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\n\n**warning:** when enabling dedup, you should also:\n* enable indexing with `-e2dsa` or volflag `e2dsa` (see [file indexing](#file-indexing) section below); strongly recommended\n* ...and/or `--hardlink-only` to use hardlink-based deduplication instead of symlinks; see explanation below\n* ...and/or `--reflink` to use CoW/reflink-based dedup (much safer than hardlink, but OS/FS-dependent)\n\nit 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)\n\nby default, deduplication is done based on symlinks (symbolic links); these are tiny files which are pointers to the nearest full copy of the file\n\nyou 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`\n\nadvantages of using reflinks (CoW, copy-on-write):\n* entirely safe (when your filesystem supports it correctly); either file can be edited or deleted without affecting other copies\n* 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)\n\nadvantages of using hardlinks:\n* hardlinks are more compatible with other software; they behave entirely like regular files\n* you can safely move and rename files using other file managers\n  * symlinks need to be managed by copyparty to ensure the destinations remain correct\n\nadvantages of using symlinks (default):\n* each symlink can have its own last-modified timestamp, but a single timestamp is shared by all hardlinks\n* symlinks make it more obvious to other software that the file is not a regular file, so this can be less dangerous\n  * hardlinks look like regular files, so other software may assume they are safe to edit without affecting the other copies\n\n**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\n\nglobal-option `--xlink` / volflag `xlink` additionally enables deduplication across volumes, but this is probably buggy and not recommended\n\nconfig file example:\n\n```yaml\n[global]\n  e2dsa  # scan and index filesystem on startup\n  dedup  # symlink-based deduplication for all volumes\n\n[/media]\n  /mnt/nas/media\n  flags:\n    hardlinkonly  # this vol does hardlinks instead of symlinks\n```\n\n\n## file indexing\n\nenable music search, upload-undo, and better dedup\n\nfile 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.\n\nthrough arguments:\n* `-e2d` enables file indexing on upload\n* `-e2ds` also scans writable folders for new files on startup\n* `-e2dsa` also scans all mounted volumes (including readonly ones)\n* `-e2t` enables metadata indexing on upload\n* `-e2ts` also scans for tags in all files that don't have tags yet\n* `-e2tsr` also deletes all existing tags, doing a full reindex\n* `-e2v` verifies file integrity at startup, comparing hashes from the db\n* `-e2vu` patches the database with the new hashes from the filesystem\n* `-e2vp` panics and kills copyparty instead\n\nthe same arguments can be set as volflags, in addition to `d2d`, `d2ds`, `d2t`, `d2ts`, `d2v` for disabling:\n* `-v ~/music::r:c,e2ds,e2tsr` does a full reindex of everything on startup\n* `-v ~/music::r:c,d2d` disables **all** indexing, even if any `-e2*` are on\n* `-v ~/music::r:c,d2t` disables all `-e2t*` (tags), does not affect `-e2d*`\n* `-v ~/music::r:c,d2ds` disables on-boot scans; only index new uploads\n* `-v ~/music::r:c,d2ts` same except only affecting tags\n\nnote:\n* 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)\n  * and file checksums can be shown with global-option `-e2d -mte +w` or volflag `e2d,mte=+w` (always active for users with permission `a`)\n* `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\n\nconfig file example (these options are recommended btw):\n\n```yaml\n[global]\n  e2dsa  # scan and index all files in all volumes on startup\n  e2ts   # check newly-discovered or uploaded files for media tags\n```\n\n### exclude-patterns\n\nto 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:\n* initial indexing is way faster, especially when the volume is on a network disk\n* makes it impossible to [file-search](#file-search)\n* if someone uploads the same file contents, the upload will not be detected as a dupe, so it will not get symlinked or rejected\n\nsimilarly, you can fully ignore files/folders using `--no-idx [...]` and `:c,noidx=\\.iso$`\n\nNOTE: `no-idx` and/or `no-hash` prevents deduplication of those files\n\n* when running on macos, all the usual apple metadata files are excluded by default\n\nif you set `--no-hash [...]` globally, you can enable hashing for specific volumes using flag `:c,nohash=`\n\nto 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]'`\n\nconfig file example:\n\n```yaml\n[/games]\n  /mnt/nas/games\n  flags:\n    noidx: \\.iso$  # skip indexing iso-files\n    srch_excl: password|logs/[0-9]  # filter search results\n```\n\n### filesystem guards\n\navoid traversing into other filesystems  using `--xdev` / volflag `:c,xdev`, skipping any symlinks or bind-mounts to another HDD for example\n\nand/or you can `--xvol` / `:c,xvol` to ignore all symlinks leaving the volume's top directory, but still allow bind-mounts pointing elsewhere\n\n* symlinks are permitted with `xvol` if they point into another volume where the user has the same level of access\n\nthese options will reduce performance; unlikely worst-case estimates are 14% reduction for directory listings, 35% for download-as-tar\n\nas of copyparty v1.7.0 these options also prevent file access at runtime -- in previous versions it was just hints for the indexer\n\n### periodic rescan\n\nfilesystem 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\n\nargument `--re-maxage 60` will rescan all volumes every 60 sec, same as volflag `:c,scan=60` to specify it per-volume\n\nuploads 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, ...)\n\nnote: 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)\n\nconfig file example:\n\n```yaml\n[global]\n  re-maxage: 3600\n\n[/pics]\n  /mnt/nas/pics\n  flags:\n    scan: 900\n```\n\n\n## upload rules\n\nset upload rules using volflags,  some examples:\n\n* `:c,sz=1k-3m` sets allowed filesize between 1 KiB and 3 MiB inclusive (suffixes: `b`, `k`, `m`, `g`)\n* `:c,df=4g` block uploads if there would be less than 4 GiB free disk space afterwards\n* `:c,vmaxb=1g` block uploads if total volume size would exceed 1 GiB afterwards\n* `:c,vmaxn=4k` block uploads if volume would contain more than 4096 files afterwards\n* `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`:\n* `: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)\n* `:c,rotf=%Y/%m/%d/%H` enforces files to be uploaded into a structure of subfolders according to that date format\n  * `:c,rotf_tz=Europe/Oslo` sets the timezone (default is UTC unless global-option `rotf-tz` is changed)\n  * if someone uploads to `/foo/bar` the path would be rewritten to `/foo/bar/2021/08/06/23` for example\n  * but the actual value is not verified, just the structure, so the uploader can choose any values which conform to the format string\n    * just to avoid additional complexity in up2k which is enough of a mess already\n* `:c,lifetime=300` delete uploaded files when they become 5 minutes old\n\nyou 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\n\n* `:c,maxn=250,3600` allows 250 files over 1 hour from each IP (tracked per-volume)\n* `:c,maxb=1g,300` allows 1 GiB total over 5 minutes from each IP (tracked per-volume)\n\nnotes:\n* `vmaxb` and `vmaxn` requires either the `e2ds` volflag or `-e2dsa` global-option\n\nconfig file example:\n\n```yaml\n[/inc]\n  /mnt/nas/uploads\n  accs:\n    w: *    # anyone can upload here\n    rw: ed  # only user \"ed\" can read-write\n  flags:\n    e2ds       # filesystem indexing is required for many of these:\n    sz: 1k-3m  # accept upload only if filesize in this range\n    df: 4g     # free disk space cannot go lower than this\n    vmaxb: 1g  # volume can never exceed 1 GiB\n    vmaxn: 4k  # ...or 4000 files, whichever comes first\n    nosub      # must upload to toplevel folder\n    lifetime: 300   # uploads are deleted after 5min\n    maxn: 250,3600  # each IP can upload 250 files in 1 hour\n    maxb: 1g,300    # each IP can upload 1 GiB over 5 minutes\n```\n\n\n## compress uploads\n\nfiles can be autocompressed on upload,  either on user-request (if config allows) or forced by server-config\n\n* volflag `gz` allows gz compression\n* volflag `xz` allows lzma compression\n* volflag `pk` **forces** compression on all files\n* url parameter `pk` requests compression with server-default algorithm\n* url parameter `gz` or `xz` requests compression with a specific algorithm\n* url parameter `xz` requests xz compression\n\nthings to note,\n* the `gz` and `xz` arguments take a single optional argument, the compression level (range 0 to 9)\n* the `pk` volflag takes the optional argument `ALGORITHM,LEVEL` which will then be forced for all uploads, for example `gz,9` or `xz,0`\n* default compression is gzip level 9\n* all upload methods except up2k are supported\n* the files will be indexed after compression, so dupe-detection and file-search will not work as expected\n\nsome examples,\n* `-v inc:inc:w:c,pk=xz,0`  \n  folder named inc, shared at inc, write-only for everyone, forces xz compression at level 0\n* `-v inc:inc:w:c,pk`  \n  same write-only inc, but forces gz compression (default) instead of xz\n* `-v inc:inc:w:c,gz`  \n  allows (but does not force) gz compression if client uploads to `/inc?pk` or `/inc?gz` or `/inc?gz=4`\n\n\n## chmod and chown\n\nper-volume filesystem-permissions and ownership\n\nby default:\n* all folders are chmod 755\n* files are usually chmod 644 (umask-defined)\n* user/group is whatever copyparty is running as\n\nthis can be configured per-volume:\n* volflag `chmod_f` sets file permissions; default=`644` (usually)\n* volflag `chmod_d` sets directory permissions; default=`755`\n* volflag `uid` sets the owner user-id\n* volflag `gid` sets the owner group-id\n\nnotes:\n* `gid` can only be set to one of the groups which the copyparty process is a member of\n* `uid` can only be set if copyparty is running as root (i appreciate your faith)\n\n\n## other flags\n\n* `:c,magic` enables filetype detection for nameless uploads, same as `--magic`\n  * needs https://pypi.org/project/python-magic/ `python3 -m pip install --user -U python-magic`\n  * on windows grab this instead `python3 -m pip install --user -U python-magic-bin`\n* `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)\n  * 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\n\n\n## database location\n\nin-volume (`.hist/up2k.db`, default) or somewhere else\n\ncopyparty creates a subfolder named `.hist` inside each volume where it stores the database, thumbnails, and some other stuff\n\nthis can instead be kept in a single place using the `--hist` argument, or the `hist=` volflag, or a mix of both:\n* `--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)\n\nby 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\n\nif 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)\n\nnote:\n* putting the hist-folders on an SSD is strongly recommended for performance\n* markdown edits are always stored in a local `.hist` subdirectory\n* on windows the volflag path is cyglike, so `/c/temp` means `C:\\temp` but use regular paths for `--hist`\n  * you can use cygpaths for volumes too, `-v C:\\Users::r` and `-v /c/users::r` both work\n\nconfig file example:\n\n```yaml\n[global]\n  hist: ~/.cache/copyparty  # put db/thumbs/etc. here by default\n\n[/pics]\n  /mnt/nas/pics\n  flags:\n    hist: -  # restore the default (/mnt/nas/pics/.hist/)\n    hist: /mnt/nas/cache/pics/  # can be absolute path\n    landmark: me.jpg  # /mnt/nas/pics/me.jpg must be readable to enable db\n    landmark: info/a.txt^=ok  # and this textfile must start with \"ok\"\n```\n\n\n## metadata from audio files\n\nset `-e2t` to index tags on upload\n\n`-mte` decides which tags to index and display in the browser (and also the display order), this can be changed per-volume:\n* `-v ~/music::r:c,mte=title,artist` indexes and displays *title* followed by *artist*\n\nif 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\n\nbut 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\n\n`-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`\n\ntags that start with a `.` such as `.bpm` and `.dur`(ation) indicate numeric value\n\nsee 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,)\n\n`--no-mutagen` disables Mutagen and uses FFprobe instead, which...\n* is about 20x slower than Mutagen\n* catches a few tags that Mutagen doesn't\n  * melodic key, video resolution, framerate, pixfmt\n* avoids pulling any GPL code into copyparty\n* more importantly runs FFprobe on incoming files which is bad if your FFmpeg has a cve\n\n`--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\n\n\n### metadata from xattrs\n\nunix extended file attributes  (Linux-only) can be indexed into the db and made searchable;\n\n* `--db-xattr user.foo,user.bar` will index the xattrs `user.foo` and `user.bar`,\n* `--db-xattr user.foo=foo,user.bar=bar` will index them with the names `foo` and `bar`,\n* `--db-xattr ~~user.foo,user.bar` will index everything *except* `user.foo` and `user.bar`,\n* `--db-xattr ~~` will index everything\n\nhowever note that the tags must also be enabled with `-mte` so here are some complete examples:\n* `-e2ts --db-xattr user.foo,user.bar -mte +user.foo,user.bar`\n* `-e2ts --db-xattr user.foo=foo,user.bar=bar -mte +foo,bar`\n\nas for actually adding the xattr `user.foo` to a file in the first place,\n* `setfattr -n user.foo -v 'utsikt fra fløytoppen' photo.jpg`\n\n\n## file parser plugins\n\nprovide custom parsers to index additional tags,  also see [./bin/mtag/README.md](./bin/mtag/README.md)\n\ncopyparty 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)\n\n* `-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\n* `-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,`)\n* `-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\n\n*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)\n\n* \"audio file\" also means videos btw, as long as there is an audio stream\n* `-mtp ext=an,~/bin/file-ext.py` runs `~/bin/file-ext.py` to get the `ext` tag only if file is not audio (`an`)\n* `-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\n* if you want to daisychain parsers, use the `p` flag to set processing order\n  * `-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\n* 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\n  * `c1` captures stdout only, `c2` only stderr, and `c3` (default) captures both\n* 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\n\nif something doesn't work, try `--mtag-v` for verbose error messages\n\nconfig file example; note that `mtp` is an additive option so all of the mtp options will take effect:\n\n```yaml\n[/music]\n  /mnt/nas/music\n  flags:\n    mtp: .bpm=~/bin/audio-bpm.py  # assign \".bpm\" (numeric) with script\n    mtp: key=f,t5,~/bin/audio-key.py  # force/overwrite, 5sec timeout\n    mtp: ext=an,~/bin/file-ext.py  # will only run on non-audio files\n    mtp: arch,built,ver,orig=an,eexe,edll,~/bin/exe.py  # only exe/dll\n```\n\n\n## event hooks\n\ntrigger a program on uploads, renames etc ([examples](./bin/hooks/))\n\nyou can set hooks before and/or after an event happens, and currently you can hook uploads, moves/renames, and deletes\n\nthere's a bunch of flags and stuff, see [`--help-hooks`](https://copyparty.eu/cli/#hooks-help-page)\n\nif you want to write your own hooks, see [devnotes](./docs/devnotes.md#event-hooks)\n\n\n### zeromq\n\nevent-hooks can send zeromq messages  instead of running programs\n\nto send a 0mq message every time a file is uploaded,\n\n* `--xau zmq:pub:tcp://*:5556` sends a PUB to any/all connected SUB clients\n* `--xau t3,zmq:push:tcp://*:5557` sends a PUSH to exactly one connected PULL client\n* `--xau t3,j,zmq:req:tcp://localhost:5555` sends a REQ to the connected REP client\n\nthe PUSH and REQ examples have `t3` (timeout after 3 seconds) because they block if there's no clients to talk to\n\n* the REQ example does `t3,j` to send extended upload-info as json instead of just the filesystem-path\n\nsee [zmq-recv.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/zmq-recv.py) if you need something to receive the messages with\n\nconfig file example; note that the hooks are additive options, so all of the xau options will take effect:\n\n```yaml\n[global]\n  xau: zmq:pub:tcp://*:5556`  # send a PUB to any/all connected SUB clients\n  xau: t3,zmq:push:tcp://*:5557`  # send PUSH to exactly one connected PULL cli\n  xau: t3,j,zmq:req:tcp://localhost:5555`  # send REQ to the connected REP cli\n```\n\n\n### upload events\n\nthe older, more powerful approach ([examples](./bin/mtag/)):\n\n```\n-v /mnt/inc:inc:w:c,e2d,e2t,mte=+x1:c,mtp=x1=ad,kn,/usr/bin/notify-send\n```\n\nthat was the commandline example; here's the config file example:\n\n```yaml\n[/inc]\n  /mnt/inc\n  accs:\n    w: *\n  flags:\n    e2d, e2t  # enable indexing of uploaded files and their tags\n    mte: +x1\n    mtp: x1=ad,kn,/usr/bin/notify-send\n```\n\nso 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`)\n\nthat'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)\n\nnote that this is way more complicated than the new [event hooks](#event-hooks) but this approach has the following advantages:\n* non-blocking and multithreaded; doesn't hold other uploads back\n* you get access to tags from FFmpeg and other mtp parsers\n* only trigger on new unique files, not dupes\n\nnote 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`\n\nfor reference, if you were to do this using event hooks instead, it would be like this: `-e2d --xau notify-send,hello,--`\n\n\n## handlers\n\nredefine behavior with plugins ([examples](./bin/handlers/))\n\nreplace 404 and 403 errors with something completely different (that's it for now)\n\nas for client-side stuff, there is [plugins for modifying UI/UX](./contrib/plugins/)\n\n\n## ip auth\n\nautologin based on IP range (CIDR)  , using the global-option `--ipu`\n\nfor 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:\n\n```yaml\n[global]\n  ipu: 192.168.123.0/24=spartacus\n```\n\nrepeat the option to map additional subnets\n\n**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=`\n\n\n### restrict to ip\n\nlimit a user to certain IP ranges (CIDR)  , using the global-option `--ipr`\n\nfor 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:\n\n```yaml\n[global]\n  ipr: 192.168.123.0/24,172.16.0.0/16=spartacus\n```\n\nrepeat the option to map additional users\n\n\n## identity providers\n\nreplace copyparty passwords with oauth and such\n\nyou 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\n\n* the regular config-defined users will be used as a fallback for requests which don't include a valid (trusted) IdP username header\n\n  * `--auth-ord` configured auth precedence, for example to allow overriding the IdP with a copyparty password\n\n* 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\n\n* 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\n\nsome popular identity providers are [Authelia](https://www.authelia.com/) (config-file based) and [authentik](https://goauthentik.io/) (GUI-based, more complex)\n\nthere 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)\n\na more complete example of the copyparty configuration options [look like this](./docs/examples/docker/idp/copyparty.conf)\n\nbut if you just want to let users change their own passwords, then you probably want [user-changeable passwords](#user-changeable-passwords) instead\n\n\n### generic header auth\n\nother ways to auth by header\n\nif 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:\n\n```yaml\n[global]\n  idp-hm-usr: ^Tailscale-User-Login^alice.m@forest.net^alice\n```\n\nrepeat the whole `idp-hm-usr` option to add more mappings\n\n\n## user-changeable passwords\n\nif permitted, users can change their own passwords  in the control-panel\n\n* not compatible with [identity providers](#identity-providers)\n\n* must be enabled with `--chpw` because account-sharing is a popular usecase\n\n  * 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,...`\n\n* 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\n\n* the custom passwords are kept in a textfile at filesystem-path `--chpw-db`, by default `chpw.json` in the copyparty config folder\n\n  * if you run multiple copyparty instances with different users you *almost definitely* want to specify separate DBs for each instance\n\n  * if [password hashing](#password-hashing) is enabled, the passwords in the db are also hashed\n\n    * ...which means that all user-defined passwords will be forgotten if you change password-hashing settings\n\n\n## using the cloud as storage\n\nconnecting to an aws s3 bucket and similar\n\nthere 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\n\nif 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`\n\nyou 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\n\n> 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)\n\nsomeone 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\n\nyou may improve performance by specifying larger values for `--iobuf` / `--s-rd-sz` / `--s-wr-sz`\n\n> if you've experimented with this and made interesting observations, please share your findings so we can add a section with specific recommendations :-)\n\n\n## hiding from google\n\ntell 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:\n\n* `--no-robots` adds HTTP (`X-Robots-Tag`) and HTML (`<meta>`) headers with `noindex, nofollow` globally\n* volflag `[...]:c,norobots` does the same thing for that single volume\n* volflag `[...]:c,robots` ALLOWS search-engine crawling for that volume, even if `--no-robots` is set globally\n\nalso, `--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\n\n\n## themes\n\nyou 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`\n\n<table><tr><td width=\"33%\" align=\"center\"><a href=\"https://user-images.githubusercontent.com/241032/165864907-17e2ac7d-319d-4f25-8718-2f376f614b51.png\"><img src=\"https://user-images.githubusercontent.com/241032/165867551-fceb35dd-38f0-42bb-bef3-25ba651ca69b.png\"></a>\n0. classic dark</td><td width=\"33%\" align=\"center\"><a href=\"https://user-images.githubusercontent.com/241032/168644399-68938de5-da9b-445f-8d92-b51c74b5f345.png\"><img src=\"https://user-images.githubusercontent.com/241032/168644404-8e1a2fdc-6e59-4c41-905e-ba5399ed686f.png\"></a>\n2. flat pm-monokai</td><td width=\"33%\" align=\"center\"><a href=\"https://user-images.githubusercontent.com/241032/165864901-db13a429-a5da-496d-8bc6-ce838547f69d.png\"><img src=\"https://user-images.githubusercontent.com/241032/165867560-aa834aef-58dc-4abe-baef-7e562b647945.png\"></a>\n4. vice</td></tr><tr><td align=\"center\"><a href=\"https://user-images.githubusercontent.com/241032/165864905-692682eb-6fb4-4d40-b6fe-27d2c7d3e2a7.png\"><img src=\"https://user-images.githubusercontent.com/241032/165867555-080b73b6-6d85-41bb-a7c6-ad277c608365.png\"></a>\n1. classic light</td><td align=\"center\"><a href=\"https://user-images.githubusercontent.com/241032/168645276-fb02fd19-190a-407a-b8d3-d58fee277e02.png\"><img src=\"https://user-images.githubusercontent.com/241032/168645280-f0662b3c-9764-4875-a2e2-d91cc8199b23.png\"></a>\n3. flat light\n</td><td align=\"center\"><a href=\"https://user-images.githubusercontent.com/241032/165864898-10ce7052-a117-4fcf-845b-b56c91687908.png\"><img src=\"https://user-images.githubusercontent.com/241032/165867562-f3003d45-dd2a-4564-8aae-fed44c1ae064.png\"></a>\n5. <a href=\"https://blog.codinghorror.com/a-tribute-to-the-windows-31-hot-dog-stand-color-scheme/\">hotdog stand</a></td></tr></table>\n\nthe classname of the HTML tag is set according to the selected theme, which is used to set colors as css variables ++\n\n* each theme *generally* has a dark theme (even numbers) and a light theme (odd numbers), showing in pairs\n* the first theme (theme 0 and 1) is `html.a`, second theme (2 and 3) is `html.b`\n* if a light theme is selected, `html.y` is set, otherwise `html.z` is\n* so if the dark edition of the 2nd theme is selected, you use any of `html.b`, `html.z`, `html.bz` to specify rules\n\nsee 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\n\nif you want to change the fonts, see [./docs/rice/](./docs/rice/)\n\n\n## complete examples\n\n* see [running on windows](./docs/examples/windows.md) for a fancy windows setup\n\n  * or use any of the examples below, just replace `python copyparty-sfx.py` with `copyparty.exe` if you're using the exe edition\n\n* allow anyone to download or upload files into the current folder:  \n  `python copyparty-sfx.py`\n\n  * enable searching and music indexing with `-e2dsa -e2ts`\n\n  * start an FTP server on port 3921 with `--ftp 3921`\n\n  * announce it on your LAN with `-z` so it appears in windows/Linux file managers\n\n* anyone can upload, but nobody can see any files (even the uploader):  \n  `python copyparty-sfx.py -e2dsa -v .::w`\n\n  * block uploads if there's less than 4 GiB free disk space with `--df 4`\n\n  * show a popup on new uploads with `--xau bin/hooks/notify.py`\n\n* anyone can upload, and receive \"secret\" links for each upload they do:  \n  `python copyparty-sfx.py -e2dsa -v .::wG:c,fk=8`\n\n* anyone can browse (`r`), only `kevin` (password `okgo`) can upload/move/delete (`A`) files:  \n  `python copyparty-sfx.py -e2dsa -a kevin:okgo -v .::r:A,kevin`\n\n* read-only music server:  \n  `python copyparty-sfx.py -v /mnt/nas/music:/music:r -e2dsa -e2ts --no-robots --force-js --theme 2`\n  \n  * ...with bpm and key scanning  \n    `-mtp .bpm=f,audio-bpm.py -mtp key=f,audio-key.py`\n  \n  * ...with a read-write folder for `kevin` whose password is `okgo`  \n    `-a kevin:okgo -v /mnt/nas/inc:/inc:rw,kevin`\n  \n  * ...with logging to disk  \n    `-lo log/cpp-%Y-%m%d-%H%M%S.txt.xz`\n\n\n## listen on port 80 and 443\n\nbecome a *real* webserver  which people can access by just going to your IP or domain without specifying a port\n\n**if you're on windows,** then you just need to add the commandline argument `-p 80,443` and you're done! nice\n\n**if you're on macos,** sorry, I don't know\n\n**if you're on Linux,** you have the following 4 options:\n\n* **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\n\n* **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:\n  ```bash\n  iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3923\n  iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 3923\n  ```\n\n* **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:\n  ```\n  setcap CAP_NET_BIND_SERVICE=+eip $(realpath $(which python))\n  python copyparty-sfx.py -p 80,443\n  ```\n\n* **option 4:** run copyparty as root (please don't)\n\n\n## reverse-proxy\n\nrunning copyparty next to other websites  hosted on an existing webserver such as nginx, caddy, or apache\n\nyou can either:\n* give copyparty its own domain or subdomain (recommended)\n* 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\n  * 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\n\nwhen 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:\n\n> 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\n\nNote that `--rp-loc` in particular will not work at all unless you configure the above correctly\n\nsome 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\n* **warning:** nginx-QUIC (HTTP/3) is still experimental and can make uploads much slower, so HTTP/1.1 is recommended for now\n* depending on server/client, HTTP/1.1 can also be 5x faster than HTTP/2\n\nfor 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)\n\nexample webserver / reverse-proxy configs:\n\n* [apache config](contrib/apache/copyparty.conf)\n* caddy uds: `caddy reverse-proxy --from :8080 --to unix///dev/shm/party.sock`\n* caddy tcp: `caddy reverse-proxy --from :8081 --to http://127.0.0.1:3923`\n* [haproxy config](contrib/haproxy/copyparty.conf)\n* [lighttpd subdomain](contrib/lighttpd/subdomain.conf) -- entire domain/subdomain\n* [lighttpd subpath](contrib/lighttpd/subpath.conf) -- location-based (not optimal, but in case you need it)\n* [nginx config](contrib/nginx/copyparty.conf) -- recommended\n* [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)\n\n\n### real-ip\n\nteaching copyparty how to see client IPs  when running behind a reverse-proxy, or a WAF, or another protection service such as cloudflare\n\nif 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\n\nfor 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...)\n\n\n### reverse-proxy performance\n\nmost reverse-proxies support connecting to copyparty either using uds/unix-sockets (`/dev/shm/party.sock`, faster/recommended) or using tcp (`127.0.0.1`)\n\nwith copyparty listening on a uds / unix-socket / unix-domain-socket and the reverse-proxy connecting to that:\n\n| index.html   | upload      | download    | software |\n| ------------ | ----------- | ----------- | -------- |\n| 28'900 req/s | 6'900 MiB/s | 7'400 MiB/s | no-proxy |\n| 18'750 req/s | 3'500 MiB/s | 2'370 MiB/s | haproxy |\n|  9'900 req/s | 3'750 MiB/s | 2'200 MiB/s | caddy |\n| 18'700 req/s | 2'200 MiB/s | 1'570 MiB/s | nginx |\n|  9'700 req/s | 1'750 MiB/s | 1'830 MiB/s | apache |\n|  9'900 req/s | 1'300 MiB/s | 1'470 MiB/s | lighttpd |\n\nwhen connecting the reverse-proxy to `127.0.0.1` instead (the basic and/or old-fasioned way), speeds are a bit worse:\n\n| index.html   | upload      | download    | software |\n| ------------ | ----------- | ----------- | -------- |\n| 21'200 req/s | 5'700 MiB/s | 6'700 MiB/s | no-proxy |\n| 14'500 req/s | 1'700 MiB/s | 2'170 MiB/s | haproxy |\n| 11'100 req/s | 2'750 MiB/s | 2'000 MiB/s | traefik |\n|  8'400 req/s | 2'300 MiB/s | 1'950 MiB/s | caddy |\n| 13'400 req/s | 1'100 MiB/s | 1'480 MiB/s | nginx |\n|  8'400 req/s | 1'000 MiB/s | 1'000 MiB/s | apache |\n|  6'500 req/s | 1'270 MiB/s | 1'500 MiB/s | lighttpd |\n\nin summary, `haproxy > caddy > traefik > nginx > apache > lighttpd`, and use uds when possible (traefik does not support it yet)\n\n* if these results are bullshit because my config examples are bad, please submit corrections!\n\n\n## permanent cloudflare tunnel\n\nif 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\")\n\nI'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:\n\n* `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`\n\n* 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`\n\nNOTE: 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\n\nconfig file example:\n\n```yaml\n[global]\n  xff-hdr: cf-connecting-ip\n```\n\n\n## prometheus\n\nmetrics/stats can be enabled  at URL `/.cpr/metrics` for grafana / prometheus / etc (openmetrics 1.0.0)\n\nmust be enabled with `--stats` since it reduces startup time a tiny bit, and you probably want `-e2dsa` too\n\nthe 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`\n\nfollow a guide for setting up `node_exporter` except have it read from copyparty instead; example `/etc/prometheus/prometheus.yml` below\n\n```yaml\nscrape_configs:\n  - job_name: copyparty\n    metrics_path: /.cpr/metrics\n    basic_auth:\n      password: wark\n    static_configs:\n      - targets: ['192.168.123.1:3923']\n```\n\ncurrently the following metrics are available,\n* `cpp_uptime_seconds` time since last copyparty restart\n* `cpp_boot_unixtime_seconds` same but as an absolute timestamp\n* `cpp_active_dl` number of active downloads\n* `cpp_http_conns` number of open http(s) connections\n* `cpp_http_reqs` number of http(s) requests handled\n* `cpp_sus_reqs` number of 403/422/malicious requests\n* `cpp_active_bans` number of currently banned IPs\n* `cpp_total_bans` number of IPs banned since last restart\n\nthese are available unless `--nos-vst` is specified:\n* `cpp_db_idle_seconds` time since last database activity (upload/rename/delete)\n* `cpp_db_act_seconds` same but as an absolute timestamp\n* `cpp_idle_vols` number of volumes which are idle / ready\n* `cpp_busy_vols` number of volumes which are busy / indexing\n* `cpp_offline_vols` number of volumes which are offline / unavailable\n* `cpp_hashing_files` number of files queued for hashing / indexing\n* `cpp_tagq_files` number of files queued for metadata scanning\n* `cpp_mtpq_files` number of files queued for plugin-based analysis\n\nand these are available per-volume only:\n* `cpp_disk_size_bytes` total HDD size\n* `cpp_disk_free_bytes` free HDD space\n\nand these are per-volume and `total`:\n* `cpp_vol_bytes` size of all files in volume\n* `cpp_vol_files` number of files\n* `cpp_dupe_bytes` disk space presumably saved by deduplication\n* `cpp_dupe_files` number of dupe files\n* `cpp_unf_bytes` currently unfinished / incoming uploads\n\nsome of the metrics have additional requirements to function correctly,\n* `cpp_vol_*` requires either the `e2ds` volflag or `-e2dsa` global-option\n\nthe following options are available to disable some of the metrics:\n* `--nos-hdd` disables `cpp_disk_*` which can prevent spinning up HDDs\n* `--nos-vol` disables `cpp_vol_*` which reduces server startup time\n* `--nos-vst` disables volume state, reducing the worst-case prometheus query time by 0.5 sec\n* `--nos-dup` disables `cpp_dupe_*` which reduces the server load caused by prometheus queries\n* `--nos-unf` disables `cpp_unf_*` for no particular purpose\n\nnote: 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`\n\n\n## other extremely specific features\n\nyou'll never find a use for these:\n\n\n### custom mimetypes\n\nchange the association of a file extension\n\nusing commandline args, you can do something like `--mime gif=image/jif` and `--mime ts=text/x.typescript` (can be specified multiple times)\n\nin a config file, this is the same as:\n\n```yaml\n[global]\n  mime: gif=image/jif\n  mime: ts=text/x.typescript\n```\n\nrun copyparty with `--mimes` to list all the default mappings\n\n\n### GDPR compliance\n\nimagine using copyparty professionally...  **TINLA/IANAL; EU laws are hella confusing**\n\n* remember to disable logging, or configure logrotation to an acceptable timeframe with `-lo cpp-%Y-%m%d.txt.xz` or similar\n\n* if running with the database enabled (recommended), then have it forget uploader-IPs after some time using `--forget-ip 43200`\n  * don't set it too low; [unposting](#unpost) a file is no longer possible after this takes effect\n\n* if you actually *are* a lawyer then I'm open for feedback, would be fun\n\n\n### feature chickenbits\n\nbuggy feature? rip it out  by setting any of the following environment variables to disable its associated bell or whistle,\n\n| env-var              | what it does |\n| -------------------- | ------------ |\n| `PRTY_NO_CTYPES`     | do not use features from external libraries such as kernel32 |\n| `PRTY_NO_DB_LOCK`    | do not lock session/shares-databases for exclusive access |\n| `PRTY_NO_IFADDR`     | disable ip/nic discovery by poking into your OS with ctypes |\n| `PRTY_NO_IMPRESO`    | do not try to load js/css files using `importlib.resources` |\n| `PRTY_NO_IPV6`       | disable some ipv6 support (should not be necessary since windows 2000) |\n| `PRTY_NO_LZMA`       | disable streaming xz compression of incoming uploads |\n| `PRTY_NO_MP`         | disable all use of the python `multiprocessing` module (actual multithreading, cpu-count for parsers/thumbnailers) |\n| `PRTY_NO_SQLITE`     | disable all database-related functionality (file indexing, metadata indexing, most file deduplication logic) |\n| `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 |\n| `PRTY_NO_TPOKE`      | disable systemd-tmpfilesd avoider |\n| `PRTY_UNSAFE_STATE`  | allow storing secrets into emergency-fallback locations |\n\nexample: `PRTY_NO_IFADDR=1 python3 copyparty-sfx.py`\n\n\n### feature beefybits\n\nforce-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`\n\n| env-var                  | what it does |\n| ------------------------ | ------------ |\n| `PRTY_FORCE_MP`          | force-enable multiprocessing (real multithreading) on MacOS and other broken platforms |\n| `PRTY_FORCE_MAGIC`       | use [magic](https://pypi.org/project/python-magic/) on Windows (you will segfault) |\n\n\n# packages\n\nthe party might be closer than you think\n\nif your distro/OS is not mentioned below, there might be some hints in the [«on servers»](#on-servers) section\n\n\n## arch package\n\n`pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))\n\nit 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`\n\nafter 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)\n\n> 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`\n\n\n## fedora package\n\ndoes not exist yet;  there are rumours that it is being packaged! keep an eye on this space...\n\n\n## homebrew formulae\n\n`brew install copyparty ffmpeg`  -- https://formulae.brew.sh/formula/copyparty\n\nshould work on all macs (both intel and apple silicon) and all relevant macos versions\n\nthe homebrew package is maintained by the homebrew team (thanks!)\n\n\n## nix package\n\n`nix profile install github:9001/copyparty`\n\nrequires a [flake-enabled](https://nixos.wiki/wiki/Flakes) installation of nix\n\nsome 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\n\n`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 👍\n\n\n## nixos module\n\nfor [flake-enabled](https://nixos.wiki/wiki/Flakes) installations of NixOS:\n\n```nix\n{\n  # add copyparty flake to your inputs\n  inputs.copyparty.url = \"github:9001/copyparty\";\n\n  # ensure that copyparty is an allowed argument to the outputs function\n  outputs = { self, nixpkgs, copyparty }: {\n    nixosConfigurations.yourHostName = nixpkgs.lib.nixosSystem {\n      modules = [\n        # load the copyparty NixOS module\n        copyparty.nixosModules.default\n        ({ pkgs, ... }: {\n          # add the copyparty overlay to expose the package to the module\n          nixpkgs.overlays = [ copyparty.overlays.default ];\n          # (optional) install the package globally\n          environment.systemPackages = [ pkgs.copyparty ];\n          # configure the copyparty module\n          services.copyparty.enable = true;\n        })\n      ];\n    };\n  };\n}\n```\n\nif 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:\n\n```nix\n{ pkgs, ... }:\n\nlet\n  # npins example, adjust for your setup. copyparty should be a path to the downloaded repo\n  # for niv, just replace the npins folder import with the sources.nix file\n  copyparty = (import ./npins).copyparty;\n\n  # or with fetchTarball:\n  copyparty = fetchTarball \"https://github.com/9001/copyparty/archive/hovudstraum.tar.gz\";\nin\n\n{\n  # load the copyparty NixOS module\n  imports = [ \"${copyparty}/contrib/nixos/modules/copyparty.nix\" ];\n\n  # add the copyparty overlay to expose the package to the module\n  nixpkgs.overlays = [ (import \"${copyparty}/contrib/package/nix/overlay.nix\") ];\n  # (optional) install the package globally\n  environment.systemPackages = [ pkgs.copyparty ];\n  # configure the copyparty module\n  services.copyparty.enable = true;\n}\n```\n\ncopyparty on NixOS is configured via `services.copyparty` options, for example:\n```nix\nservices.copyparty = {\n  enable = true;\n  # the user to run the service as\n  user = \"copyparty\"; \n  # the group to run the service as\n  group = \"copyparty\"; \n  # directly maps to values in the [global] section of the copyparty config.\n  # see `copyparty --help` for available options\n  settings = {\n    i = \"0.0.0.0\";\n    # use lists to set multiple values\n    p = [ 3210 3211 ];\n    # use booleans to set binary flags\n    no-reload = true;\n    # using 'false' will do nothing and omit the value when generating a config\n    ignored-flag = false;\n  };\n\n  # create users\n  accounts = {\n    # specify the account name as the key\n    ed = {\n      # provide the path to a file containing the password, keeping it out of /nix/store\n      # must be readable by the copyparty service user\n      passwordFile = \"/run/keys/copyparty/ed_password\";\n    };\n    # or do both in one go\n    k.passwordFile = \"/run/keys/copyparty/k_password\";\n  };\n\n  # create a group\n  groups = {\n    # users \"ed\" and \"k\" are part of the group g1\n    g1 = [ \"ed\" \"k\" ];\n  };\n\n  # create a volume\n  volumes = {\n    # create a volume at \"/\" (the webroot), which will\n    \"/\" = {\n      # share the contents of \"/srv/copyparty\"\n      path = \"/srv/copyparty\";\n      # see `copyparty --help-accounts` for available options\n      access = {\n        # everyone gets read-access, but\n        r = \"*\";\n        # users \"ed\" and \"k\" get read-write\n        rw = [ \"ed\" \"k\" ];\n      };\n      # see `copyparty --help-flags` for available options\n      flags = {\n        # \"fk\" enables filekeys (necessary for upget permission) (4 chars long)\n        fk = 4;\n        # scan for new files every 60sec\n        scan = 60;\n        # volflag \"e2d\" enables the uploads database\n        e2d = true;\n        # \"d2t\" disables multimedia parsers (in case the uploads are malicious)\n        d2t = true;\n        # skips hashing file contents if path matches *.iso\n        nohash = \"\\.iso$\";\n      };\n    };\n  };\n  # you may increase the open file limit for the process\n  openFilesLimit = 8192;\n};\n```\n\nthe 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\n\n\n# browser support\n\nTLDR: yes\n\n![copyparty-ie4-fs8](https://user-images.githubusercontent.com/241032/118192791-fb31fe00-b446-11eb-9647-898ea8efc1f7.png)\n\n`ie` = internet-explorer, `ff` = firefox, `c` = chrome, `iOS` = iPhone/iPad, `Andr` = Android\n\n| feature         | ie6 | ie9  | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |\n| --------------- | --- | ---- | ---- | ---- | ----- | ---- | --- | ---- |\n| browse files    | yep | yep  | yep  | yep  |  yep  | yep  | yep | yep  |\n| thumbnail view  |  -  | yep  | yep  | yep  |  yep  | yep  | yep | yep  |\n| basic uploader  | yep | yep  | yep  | yep  |  yep  | yep  | yep | yep  |\n| up2k            |  -  |  -   | `*1` | `*1` |  yep  | yep  | yep | yep  |\n| make directory  | yep | yep  | yep  | yep  |  yep  | yep  | yep | yep  |\n| send message    | yep | yep  | yep  | yep  |  yep  | yep  | yep | yep  |\n| set sort order  |  -  | yep  | yep  | yep  |  yep  | yep  | yep | yep  |\n| zip selection   |  -  | yep  | yep  | yep  |  yep  | yep  | yep | yep  |\n| file search     |  -  | yep  | yep  | yep  |  yep  | yep  | yep | yep  |\n| file rename     |  -  | yep  | yep  | yep  |  yep  | yep  | yep | yep  |\n| file cut/paste  |  -  | yep  | yep  | yep  |  yep  | yep  | yep | yep  |\n| unpost uploads  |  -  |  -   | yep  | yep  |  yep  | yep  | yep | yep  |\n| navpane         |  -  | yep  | yep  | yep  |  yep  | yep  | yep | yep  |\n| image viewer    |  -  | yep  | yep  | yep  |  yep  | yep  | yep | yep  |\n| video player    |  -  | yep  | yep  | yep  |  yep  | yep  | yep | yep  |\n| markdown editor |  -  |  -   | `*2` | `*2` |  yep  | yep  | yep | yep  |\n| markdown viewer |  -  | `*2` | `*2` | `*2` |  yep  | yep  | yep | yep  |\n| play mp3/m4a    |  -  | yep  | yep  | yep  |  yep  | yep  | yep | yep  |\n| play ogg/opus   |  -  |  -   |  -   |  -   |  yep  | yep  | `*3` | yep |\n| **= feature =** | ie6 | ie9  | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |\n\n* internet explorer 6 through 8 behave the same\n* firefox 52 and chrome 49 are the final winxp versions\n* `*1` yes, but extremely slow (ie10: `1 MiB/s`, ie11: `270 KiB/s`)\n* `*2` only able to do plaintext documents (no markdown rendering)\n* `*3` iOS 11 and newer, opus only, and requires FFmpeg on the server\n\nquick summary of more eccentric web-browsers trying to view a directory index:\n\n| browser | will it blend |\n| ------- | ------------- |\n| **links** (2.21/macports) | can browse, login, upload/mkdir/msg |\n| **lynx** (2.8.9/macports) | can browse, login, upload/mkdir/msg |\n| **w3m** (0.5.3/macports)  | can browse, login, upload at 100kB/s, mkdir/msg |\n| **netsurf** (3.10/arch)   | is basically ie6 with much better css (javascript has almost no effect) | \n| **opera** (11.60/winxp)   | OK: thumbnails, image-viewer, zip-selection, rename/cut/paste. NG: up2k, navpane, markdown, audio |\n| **ie4** and **netscape** 4.0  | can browse, upload with `?b=u`, auth with `&pw=wark` |\n| **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) |\n| **SerenityOS** (7e98457)  | hits a page fault, works with `?b=u`, file upload not-impl |\n| **sony psp** 5.50         | can browse, upload/mkdir/msg (thx dwarf) [screenshot](https://github.com/user-attachments/assets/9d21f020-1110-4652-abeb-6fc09c533d4f) |\n| **nintendo 3ds**          | can browse, upload, view thumbnails (thx bnjmn) |\n| **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 |\n\n<p align=\"center\"><img src=\"https://github.com/user-attachments/assets/88deab3d-6cad-4017-8841-2f041472b853\" /></p>\n\n\n# server hall of fame\n\nunexpected things that run copyparty:\n\n* 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\n  * 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/)!\n* 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)\n  * thanks again to the wonderful people at [Øl Telecom](http://ol-tele.com/)\n* a [wristwatch](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/clockyparty.jpg)\n\n\n# client examples\n\ninteract with copyparty using non-browser clients\n\n* javascript: dump some state into a file (two separate examples)\n  * `await fetch('//127.0.0.1:3923/', {method:\"PUT\", body: JSON.stringify(foo)});`\n  * `var xhr = new XMLHttpRequest(); xhr.open('POST', '//127.0.0.1:3923/msgs?raw'); xhr.send('foo');`\n\n* curl/wget: upload some files (post=file, chunk=stdin)\n  * `post(){ curl -F f=@\"$1\" http://127.0.0.1:3923/?pw=wark;}`  \n    `post movie.mkv`  (gives HTML in return)\n  * `post(){ curl -F f=@\"$1\" 'http://127.0.0.1:3923/?want=url&pw=wark';}`  \n    `post movie.mkv`  (gives hotlink in return)\n  * `post(){ curl -H pw:wark -H rand:8 -T \"$1\" http://127.0.0.1:3923/;}`  \n    `post movie.mkv`  (randomized filename)\n  * `post(){ wget --header='pw: wark' --post-file=\"$1\" -O- http://127.0.0.1:3923/?raw;}`  \n    `post movie.mkv`\n  * `chunk(){ curl -H pw:wark -T- http://127.0.0.1:3923/;}`  \n    `chunk <movie.mkv`\n\n* curl: append to existing file with `?apnd`\n  * `log(){ curl -H pw:wark -T- http://127.0.0.1:3923/logfile.txt?apnd;}`  \n    `echo hey | log`\n\n* bash: when curl and wget is not available or too boring\n  * `(printf 'PUT /junk?pw=wark HTTP/1.1\\r\\n\\r\\n'; cat movie.mkv) | nc 127.0.0.1 3923`\n  * `(printf 'PUT / HTTP/1.1\\r\\n\\r\\n'; cat movie.mkv) >/dev/tcp/127.0.0.1/3923`\n\n* 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)\n  * file uploads, file-search, [folder sync](#folder-sync), autoresume of aborted/broken uploads\n  * can be downloaded from copyparty: controlpanel -> connect -> [u2c.py](http://127.0.0.1:3923/.cpr/a/u2c.py)\n  * see [./bin/README.md#u2cpy](bin/README.md#u2cpy)\n\n* FUSE: mount a copyparty server as a local filesystem\n  * cross-platform python client available in [./bin/](bin/)\n  * able to mount nginx and iis directory listings too, not just copyparty\n  * can be downloaded from copyparty: controlpanel -> connect -> [partyfuse.py](http://127.0.0.1:3923/.cpr/a/partyfuse.py)\n  * [rclone](https://rclone.org/) as client can give ~5x performance, see [./docs/rclone.md](docs/rclone.md)\n\n* sharex (screenshot utility): see [./contrib/sharex.sxcu](./contrib/#sharexsxcu)\n  * and for screenshots on macos, see [./contrib/ishare.iscu](./contrib/#ishareiscu)\n  * and for screenshots on linux, see [./contrib/flameshot.sh](./contrib/flameshot.sh)\n\n* [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)\n  * works if you set UploadURL to `https://your.com/foo/?want=url&pw=hunter2` and FormDataName `f`\n\n* contextlet (web browser integration); see [contrib contextlet](contrib/#send-to-cppcontextletjson)\n\n* [igloo irc](https://iglooirc.com/): Method: `post` Host: `https://you.com/up/?want=url&pw=hunter2` Multipart: `yes` File parameter: `f`\n\ncopyparty returns a truncated sha512sum of your PUT/POST as base64; you can generate the same checksum locally to verify uploads:\n\n    b512(){ printf \"$((sha512sum||shasum -a512)|sed -E 's/ .*//;s/(..)/\\\\x\\1/g')\"|base64|tr '+/' '-_'|head -c44;}\n    b512 <movie.mkv\n\nyou can provide passwords using header `PW: hunter2`, cookie `cppwd=hunter2`, url-param `?pw=hunter2`, or with basic-authentication (either as the username or password)\n\n> for basic-authentication, all of the following are accepted: `password` / `whatever:password` / `password:whatever` (the username is ignored)\n\n* unless you've enabled `--usernames`, then it's `PW: usr:pwd`, cookie `cppwd=usr:pwd`, url-param `?pw=usr:pwd`\n\nNOTE: 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\n\n\n## folder sync\n\nsync folders to/from copyparty\n\nNOTE: 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\n\n* 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 :-)\n\nthe 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)\n\nif you want to sync with `u2c.py` then:\n* the `e2dsa` option (either globally or volflag) must be enabled on the server for the volumes you're syncing into\n* ...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\n* ...and u2c needs the delete-permission, so either `rwd` at minimum, or just `A` which is the same as `rwmd.a`\n  * quick reminder that `a` and `A` are different permissions, and `.` is very useful for sync\n\nalternatively 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\n\n* starting from rclone v1.63, rclone is faster than u2c.py on low-latency connections\n  * but this is only true for the initial upload; u2c will be faster for periodic syncing\n\n\n## mount as drive\n\na remote copyparty server as a local filesystem;  go to the control-panel and click `connect` to see a list of commands to do that\n\nalternatively, some alternatives roughly sorted by speed (unreproducible benchmark), best first:\n\n* [rclone-webdav](./docs/rclone.md) (25s), read/WRITE (rclone v1.63 or later)\n* [rclone-http](./docs/rclone.md) (26s), read-only\n* [partyfuse.py](./bin/#partyfusepy) (26s), read-only\n* [rclone-ftp](./docs/rclone.md) (47s), read/WRITE\n* davfs2 (103s), read/WRITE\n* [win10-webdav](#webdav-server) (138s), read/WRITE\n* [win10-smb2](#smb-server) (387s), read/WRITE\n\nmost 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\n\nif 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\n\n\n# android app\n\nupload to copyparty with one tap\n\n<a href=\"https://f-droid.org/packages/me.ocv.partyup/\"><img src=\"https://ocv.me/fdroid.png\" alt=\"Get it on F-Droid\" height=\"50\" /> '' <img src=\"https://img.shields.io/f-droid/v/me.ocv.partyup.svg\" alt=\"f-droid version info\" /></a> '' <a href=\"https://github.com/9001/party-up\"><img src=\"https://img.shields.io/github/release/9001/party-up.svg?logo=github\" alt=\"github version info\" /></a>\n\nthe app is **NOT** the full copyparty server! just a basic upload client, nothing fancy yet\n\nif you want to run the copyparty server on your android device, see [install on android](#install-on-android)\n\n\n# iOS shortcuts\n\nthere is no iPhone app, but  the following shortcuts are almost as good:\n\n* [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!)\n  * can strip exif, upload files, pics, vids, links, clipboard\n  * can download links and rehost the target file on copyparty (see first comment inside the shortcut)\n  * pics become lowres if you share from gallery to shortcut, so better to launch the shortcut and pick stuff from there\n\nif you want to run the copyparty server on your iPhone or iPad, see [install on iOS](#install-on-iOS)\n\n\n# performance\n\ndefaults are usually fine - expect `8 GiB/s` download, `1 GiB/s` upload\n\nbelow are some tweaks roughly ordered by usefulness:\n\n* disabling HTTP/2 and HTTP/3 can make uploads 5x faster, depending on server/client software\n* `-q` disables logging and can help a bunch, even when combined with `-lo` to redirect logs to file\n* `--hist` pointing to a fast location (ssd) will make directory listings and searches faster when `-e2d` or `-e2t` is set\n  * and also makes thumbnails load faster, regardless of e2d/e2t\n* `--dedup` enables deduplication and thus avoids writing to the HDD if someone uploads a dupe\n* `--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\n* `--no-dirsz` shows the size of folder inodes instead of the total size of the contents, giving about 30% faster folder listings\n* `--no-hash .` when indexing a network-disk if you don't care about the actual filehashes and only want the names/tags searchable\n* 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`\n* `--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)\n* 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`\n  * 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)\n* `-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:\n  * lots of connections (many users or heavy clients)\n  * simultaneous downloads and uploads saturating a 20gbps connection\n  * if `-e2d` is enabled, `-j2` gives 4x performance for directory listings; `-j4` gives 16x\n  \n  ...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\n* 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\n  * and pypy can sometimes crash on startup with `-j0` (TODO make issue)\n\n* if you are running the copyparty server **on Windows or Macos:**\n  * `--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\n    * this is the same as `casechk: n` in a config-file\n\n\n## client-side\n\nwhen uploading files,\n\n* 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\n  * don't do this on Safari (runs faster without)\n  * don't do this on older browsers; likely to provoke browser-bugs (browser eats all RAM and crashes)\n  * can be made default-enabled serverside with `--nosubtle 137` (chrome v137+) or `--nosubtle 2` (chrome+firefox)\n\n* chrome is recommended (unfortunately), at least compared to firefox:\n  * up to 90% faster when hashing, especially on SSDs\n  * up to 40% faster when uploading over extremely fast internets\n  * but [u2c.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) can be 40% faster than chrome again\n\n* if you're cpu-bottlenecked, or the browser is maxing a cpu core:\n  * up to 30% faster uploads if you hide the upload status list by switching away from the `[🚀]` up2k ui-tab (or closing it)\n    * optionally you can switch to the lightweight potato ui by clicking the `[🥔]`\n    * switching to another browser-tab also works, the favicon will update every 10 seconds in that case\n  * unlikely to be a problem, but can happen when uploading many small files, or your internet is too fast, or PC too slow\n\n\n# security\n\nthere is a [discord server](https://discord.gg/25J8CdTT6G) with announcements  ; an `@everyone` for all important updates (at the lack of better ideas)\n\nsome notes on hardening\n\n* set `--rproxy 0` *if and only if* your copyparty is directly facing the internet (not through a reverse-proxy)\n  * cors doesn't work right otherwise\n* if you allow anonymous uploads or otherwise don't trust the contents of a volume, you can prevent XSS with volflag `nohtml`\n  * this returns html documents and svg images as plaintext, and also disables markdown rendering\n  * 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\n    * \"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\n* 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)\n\nsafety profiles:\n\n* option `-s` is a shortcut to set the following options:\n  * `--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\n  * `--no-mtag-ff` uses `mutagen` to grab music tags instead of `FFmpeg`, which is safer and faster but less accurate\n  * `--dotpart` hides uploads from directory listings while they're still incoming\n  * `--no-robots` and `--force-js` makes life harder for crawlers, see [hiding from google](#hiding-from-google)\n\n* option `-ss` is a shortcut for the above plus:\n  * `--unpost 0`, `--no-del`, `--no-mv` disables all move/delete support\n  * `--hardlink` creates hardlinks instead of symlinks when deduplicating uploads, which is less maintenance\n    * however note if you edit one file it will also affect the other copies\n  * `--vague-403` returns a \"404 not found\" instead of \"401 unauthorized\" which is a common enterprise meme\n  * `-nih` removes the server hostname from directory listings\n\n* option `-sss` is a shortcut for the above plus:\n  * `--no-dav` disables webdav support\n  * `--no-logues` and `--no-readme` disables support for readme's and prologues / epilogues in directory listings, which otherwise lets people upload arbitrary (but sandboxed) `<script>` tags\n  * `-lo cpp-%Y-%m%d-%H%M%S.txt.xz` enables logging to disk\n  * `-ls **,*,ln,p,r` does a scan on startup for any dangerous symlinks\n\nother misc notes:\n\n* you can disable directory listings by giving permission `g` instead of `r`, only accepting direct URLs to files\n  * you may want [filekeys](#filekeys) to prevent filename bruteforcing\n  * permission `h` instead of `r` makes copyparty behave like a traditional webserver with directory listing/index disabled, returning index.html instead\n    * compatibility with filekeys: index.html itself can be retrieved without the correct filekey, but all other files are protected\n\n\n## gotchas\n\nbehavior that might be unexpected\n\n* users without read-access to a folder can still see the `.prologue.html` / `.epilogue.html` / `PREADME.md` / `README.md` contents, for the purpose of showing a description on how to use the uploader for example\n* users can submit `<script>`s which autorun (in a sandbox) for other visitors in a few ways;\n  * uploading a `README.md` -- avoid with `--no-readme`\n  * renaming `some.html` to `.epilogue.html` -- avoid with either `--no-logues` or `--no-dot-ren`\n  * the directory-listing embed is sandboxed (so any malicious scripts can't do any damage) but the markdown editor is not 100% safe, see below\n* markdown documents can contain html and `<script>`s; attempts are made to prevent scripts from executing (unless `-emp` is specified) but this is not 100% bulletproof, so setting the `nohtml` volflag is still the safest choice\n  * or eliminate the problem entirely by only giving write-access to trustworthy people :^)\n\n\n## cors\n\ncross-site request config\n\nby default, except for `GET` and `HEAD` operations, all requests must either:\n* not contain an `Origin` header at all\n* or have an `Origin` matching the server domain\n* or the header `PW` with your password as value\n\ncors can be configured with `--acao` and `--acam`, or the protections entirely disabled with `--allow-csrf`\n\n\n## filekeys\n\nprevent filename bruteforcing\n\nvolflag `fk` generates filekeys (per-file accesskeys) for all files; users which have full read-access (permission `r`) will then see URLs with the correct filekey `?k=...` appended to the end, and `g` users must provide that URL including the correct key to avoid a 404\n\nby default, filekeys are generated based on salt (`--fk-salt`) + filesystem-path + file-size + inode (if not windows); add volflag `fka` to generate slightly weaker filekeys which will not be invalidated if the file is edited (only salt + path)\n\npermissions `wG` (write + upget) lets users upload files and receive their own filekeys, still without being able to see other uploads\n\n### dirkeys\n\nshare specific folders in a volume  without giving away full read-access to the rest -- the visitor only needs the `g` (get) permission to view the link\n\nvolflag `dk` generates dirkeys (per-directory accesskeys) for all folders, granting read-access to that folder; by default only that folder itself, no subfolders\n\nvolflag `dky` disables the actual key-check, meaning anyone can see the contents of a folder where they have `g` access, but not its subdirectories\n\n* `dk` + `dky` gives the same behavior as if all users with `g` access have full read-access, but subfolders are hidden files (as if their names start with a dot), so `dky` is an alternative to renaming all the folders for that purpose, maybe just for some users\n\nvolflag `dks` lets people enter subfolders as well, and also enables download-as-zip/tar\n\nif you enable dirkeys, it is probably a good idea to enable filekeys too, otherwise it will be impossible to hotlink files from a folder which was accessed using a dirkey\n\ndirkeys are generated based on another salt (`--dk-salt`) + filesystem-path and have a few limitations:\n* the key does not change if the contents of the folder is modified\n  * if you need a new dirkey, either change the salt or rename the folder\n* linking to a textfile (so it opens in the textfile viewer) is not possible if recipient doesn't have read-access\n\n\n## password hashing\n\nyou can hash passwords  before putting them into config files / providing them as arguments; see [`--help-pwhash`](https://copyparty.eu/cli/#pwhash-help-page) for all the details\n\n`--ah-alg argon2` enables it, and if you have any plaintext passwords then it'll print the hashed versions on startup so you can replace them\n\noptionally also specify `--ah-cli` to enter an interactive mode where it will hash passwords without ever writing the plaintext ones to disk\n\nthe default configs take about 0.4 sec and 256 MiB RAM to process a new password on a decent laptop\n\nwhen generating hashes using `--ah-cli` for docker or systemd services, make sure it is using the same `--ah-salt` by:\n* inspecting the generated salt using `--show-ah-salt` in copyparty service configuration\n* setting the same `--ah-salt` in both environments\n\n> ⚠️ if you have enabled `--usernames` then provide the password as `username:password` when hashing it, for example `ed:hunter2`\n\n\n## https\n\nboth HTTP and HTTPS are accepted  by default, but letting a [reverse proxy](#reverse-proxy) handle the https/tls/ssl would be better (probably more secure by default)\n\ncopyparty doesn't speak HTTP/2 or QUIC, so using a reverse proxy would solve that as well -- but note that HTTP/1 is usually faster than both HTTP/2 and HTTP/3\n\nif [cfssl](https://github.com/cloudflare/cfssl/releases/latest) is installed, copyparty will automatically create a CA and server-cert on startup\n* the certs are written to `--crt-dir` for distribution, see `--help` for the other `--crt` options\n* this will be a self-signed certificate so you must install your `ca.pem` into all your browsers/devices\n* if you want to avoid the hassle of distributing certs manually, please consider using a reverse proxy\n\nto install cfssl on windows:\n* [download](https://github.com/cloudflare/cfssl/releases/latest) `cfssl_windows_amd64.exe`, `cfssljson_windows_amd64.exe`, `cfssl-certinfo_windows_amd64.exe`\n* rename them to `cfssl.exe`, `cfssljson.exe`, `cfssl-certinfo.exe`\n* put them in PATH, for example inside `c:\\windows\\system32`\n\n\n# recovering from crashes\n\n## client crashes\n\n### firefox wsod\n\nfirefox 87 can crash during uploads  -- the entire browser goes, including all other browser tabs, everything turns white\n\nhowever you can hit `F12` in the up2k tab and use the devtools to see how far you got in the uploads:\n\n* get a complete list of all uploads, organized by status (ok / no-good / busy / queued):  \n  `var tabs = { ok:[], ng:[], bz:[], q:[] }; for (var a of up2k.ui.tab) tabs[a.in].push(a); tabs`\n\n* list of filenames which failed:  \n  `​var ng = []; for (var a of up2k.ui.tab) if (a.in != 'ok') ng.push(a.hn.split('<a href=\\\"').slice(-1)[0].split('\\\">')[0]); ng`\n\n* send the list of filenames to copyparty for safekeeping:  \n  `await fetch('/inc', {method:'PUT', body:JSON.stringify(ng,null,1)})`\n\n\n# HTTP API\n\nsee [devnotes](./docs/devnotes.md#http-api)\n\n\n# dependencies\n\nmandatory deps:\n* `jinja2` (is built into the SFX)\n\n\n## optional dependencies\n\nenable bonus features  by installing these python-packages from pypi or so:\n\nenable [hashed passwords](#password-hashing) in config: `argon2-cffi`\n\nenable [ftp-server](#ftp-server):\n* for just plaintext FTP, `pyftpdlib` (is built into the SFX)\n* with TLS encryption, `pyftpdlib pyopenssl`\n\nenable [sftp-server](#sftp-server): `paramiko`\n\nenable [music tags](#metadata-from-audio-files):\n* either `mutagen` (fast, pure-python, skips a few tags, makes copyparty GPL? idk)\n* or `ffprobe` (20x slower, more accurate, possibly dangerous depending on your distro and users)\n\nenable [thumbnails](#thumbnails) of...\n* **images:** `Pillow` and/or `pyvips` and/or `ffmpeg` (requires py2.7 or py3.5+)\n* **videos/audio:** `ffmpeg` and `ffprobe` somewhere in `$PATH`\n* **HEIF pictures:** `pyvips` or `ffmpeg` or `pillow-heif`\n* **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin` or pillow v11.3+\n* **JPEG XL pictures:** `pyvips` or `ffmpeg`\n* **RAW images:** `rawpy`, plus one of `pyvips` or `Pillow` (for some formats)\n\nenable sending [zeromq messages](#zeromq) from event-hooks: `pyzmq`\n\nenable [smb](#smb-server) support (**not** recommended): `impacket==0.13.0`\n\n`pyvips` gives higher quality thumbnails than `Pillow` and is 320% faster, using 270% more ram\n* to install `pyvips` on Linux: `sudo apt install libvips42 && python3 -m pip install --user -U pyvips`\n* to install `pyvips` on windows: `pip install --user -U \"pyvips[binary]\"`\n\nto install FFmpeg on Windows, grab [a recent build](https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z) -- you need `ffmpeg.exe` and `ffprobe.exe` from inside the `bin` folder; copy them into `C:\\Windows\\System32` or any other folder that's in your `%PATH%`\n\n\n### dependency chickenbits\n\nprevent loading an optional dependency  , for example if:\n\n* you have an incompatible version installed and it causes problems\n* you just don't want copyparty to use it, maybe to save ram\n\nset any of the following environment variables to disable its associated optional feature,\n\n| env-var              | what it does |\n| -------------------- | ------------ |\n| `PRTY_NO_ARGON2`     | disable argon2-cffi password hashing |\n| `PRTY_NO_CFSSL`      | never attempt to generate self-signed certificates using [cfssl](https://github.com/cloudflare/cfssl) |\n| `PRTY_NO_FFMPEG`     | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips |\n| `PRTY_NO_FFPROBE`    | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips, **metadata-scanning** must be handled by mutagen |\n| `PRTY_NO_MAGIC`      | do not use [magic](https://pypi.org/project/python-magic/) for filetype detection |\n| `PRTY_NO_MUTAGEN`    | do not use [mutagen](https://pypi.org/project/mutagen/) for reading metadata from media files; will fallback to ffprobe |\n| `PRTY_NO_PARAMIKO`   | disable sftp server ([paramiko](https://www.paramiko.org/)-based) |\n| `PRTY_NO_PARTFTPY`   | disable tftp server ([partftpy](https://github.com/9001/partftpy)-based) |\n| `PRTY_NO_PIL`        | disable all [Pillow](https://pypi.org/project/pillow/)-based thumbnail support; will fallback to libvips or ffmpeg |\n| `PRTY_NO_PILF`       | disable Pillow `ImageFont` text rendering, used for folder thumbnails |\n| `PRTY_NO_PIL_AVIF`   | disable Pillow avif support (internal and/or [plugin](https://pypi.org/project/pillow-avif-plugin/)) |\n| `PRTY_NO_PIL_HEIF`   | disable 3rd-party Pillow plugin for [HEIF support](https://pypi.org/project/pillow-heif/) |\n| `PRTY_NO_PIL_JXL`    | disable 3rd-party Pillow plugin for [JXL support](https://pypi.org/project/pillow-jxl-plugin/) |\n| `PRTY_NO_PIL_WEBP`   | disable use of native webp support in Pillow |\n| `PRTY_NO_PSUTIL`     | do not use [psutil](https://pypi.org/project/psutil/) for reaping stuck hooks and plugins on Windows |\n| `PRTY_NO_PYFTPD`     | disable ftp(s) server ([pyftpdlib](https://pypi.org/project/pyftpdlib/)-based) |\n| `PRTY_NO_RAW`        | disable all [rawpy](https://pypi.org/project/rawpy/)-based thumbnail support for RAW images |\n| `PRTY_NO_VIPS`       | disable all [libvips](https://pypi.org/project/pyvips/)-based thumbnail support; will fallback to Pillow or ffmpeg |\n\nexample: `PRTY_NO_PIL=1 python3 copyparty-sfx.py`\n\n* `PRTY_NO_PIL` saves ram\n* `PRTY_NO_VIPS` saves ram and startup time\n* python2.7 on windows: `PRTY_NO_FFMPEG` + `PRTY_NO_FFPROBE` saves startup time\n\n\n### dependency unvendoring\n\nforce use of system modules  instead of the vendored versions:\n\n| env-var              | what it does |\n| -------------------- | ------------ |\n| `PRTY_SYS_ALL`       | all of the below |\n| `PRTY_SYS_DNSLIB`    | replace [stolen/dnslib](./copyparty/stolen/dnslib) with [upstream](https://pypi.org/project/dnslib/) |\n| `PRTY_SYS_IFADDR`    | replace [stolen/ifaddr](./copyparty/stolen/ifaddr) with [upstream](https://pypi.org/project/ifaddr/) |\n| `PRTY_SYS_QRCG`      | replace [stolen/qrcodegen.py](./copyparty/stolen/qrcodegen.py) with [upstream](https://github.com/nayuki/QR-Code-generator/blob/master/python/qrcodegen.py) |\n\nto debug, run copyparty with `PRTY_MODSPEC=1` to see where it's getting each module from\n\n\n## optional gpl stuff\n\nsome bundled tools have copyleft dependencies, see [./bin/#mtag](bin/#mtag)\n\nthese are standalone programs and will never be imported / evaluated by copyparty, and must be enabled through `-mtp` configs\n\n\n# sfx\n\nthe self-contained \"binary\" (recommended!)  [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) will unpack itself and run copyparty, assuming you have python installed of course\n\nif you only need english, [copyparty-en.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-en.py) is the same thing but smaller\n\nyou can reduce the sfx size by repacking it; see [./docs/devnotes.md#sfx-repack](./docs/devnotes.md#sfx-repack)\n\n\n## copyparty.exe\n\ndownload [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+)\n\n![copyparty-exe-fs8](https://user-images.githubusercontent.com/241032/221445946-1e328e56-8c5b-44a9-8b9f-dee84d942535.png)\n\ncan be convenient on machines where installing python is problematic, however is **not recommended** -- if possible, please use **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** instead\n\n* [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) runs on win8 or newer, was compiled on win10, does thumbnails + media tags, and is *currently* safe to use, but any future python/expat/pillow CVEs can only be remedied by downloading a newer version of the exe\n\n  * on win8 it needs [vc redist 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48145), on win10 it just works\n  * some antivirus may freak out (false-positive), possibly [Avast, AVG, and McAfee](https://www.virustotal.com/gui/file/52391a1e9842cf70ad243ef83844d46d29c0044d101ee0138fcdd3c8de2237d6/detection)\n\n* dangerous: [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) is compatible with [windows7](https://user-images.githubusercontent.com/241032/221445944-ae85d1f4-d351-4837-b130-82cab57d6cca.png), which means it uses an ancient copy of python (3.7.9) which cannot be upgraded and should never be exposed to the internet (LAN is fine)\n\n* dangerous and deprecated: [copyparty-winpe64.exe](https://github.com/9001/copyparty/releases/download/v1.8.7/copyparty-winpe64.exe) lets you [run copyparty in WinPE](https://user-images.githubusercontent.com/241032/205454984-e6b550df-3c49-486d-9267-1614078dd0dd.png) and is otherwise completely useless\n\nmeanwhile [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) instead relies on your system python which gives better performance and will stay safe as long as you keep your python install up-to-date\n\nthen again, if you are already into downloading shady binaries from the internet, you may also want my [minimal builds](./scripts/pyinstaller#ffmpeg) of [ffmpeg](https://ocv.me/stuff/bin/ffmpeg.exe) and [ffprobe](https://ocv.me/stuff/bin/ffprobe.exe) which enables copyparty to extract multimedia-info, do audio-transcoding, and thumbnails/spectrograms/waveforms, however it's much better to instead grab a [recent official build](https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z) every once ina while if you can afford the size\n\n\n## zipapp\n\nanother emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz)  has less features, is slow, requires python 3.7 or newer, worse compression, and more importantly is unable to benefit from more recent versions of jinja2 and such (which makes it less secure)... lots of drawbacks with this one really -- but, unlike the sfx, it is a completely normal zipfile which does not unpack any temporary files to disk, so it *may* just work if the regular sfx fails to start because the computer is messed up in certain funky ways, so it's worth a shot if all else fails\n\nrun it by doubleclicking it, or try typing `python copyparty.pyz` in your terminal/console/commandline/telex if that fails\n\nit is a python [zipapp](https://docs.python.org/3/library/zipapp.html) meaning it doesn't have to unpack its own python code anywhere to run, so if the filesystem is busted it has a better chance of getting somewhere\n\n> there is also [copyparty-en.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty-en.pyz), english-only and without smb support (enterprise-friendly)\n\n\n# install on android\n\ninstall [Termux](https://termux.com/) + its companion app `Termux:API` (see [ocv.me/termux](https://ocv.me/termux/)) and then copy-paste this into Termux (long-tap) all at once:\n```sh\nyes | pkg upgrade && termux-setup-storage && yes | pkg install python termux-api && python -m ensurepip && python -m pip install --user -U copyparty && { grep -qE 'PATH=.*\\.local/bin' ~/.bashrc 2>/dev/null || { echo 'PATH=\"$HOME/.local/bin:$PATH\"' >> ~/.bashrc && . ~/.bashrc; }; }\necho $?\n```\n\nafter the initial setup, you can launch copyparty at any time by running `copyparty` anywhere in Termux -- and if you run it with `--qr` you'll get a [neat qr-code](#qr-code) pointing to your external ip\n\nif you want thumbnails (photos+videos) and you're okay with spending another 132 MiB of storage, `pkg install ffmpeg && python3 -m pip install --user -U pillow`\n\n* or if you want to use `vips` for photo-thumbs instead, `pkg install libvips && python -m pip install --user -U wheel && python -m pip install --user -U pyvips && (cd /data/data/com.termux/files/usr/lib/; ln -s libgobject-2.0.so{,.0}; ln -s libvips.so{,.42})`\n\nif you are suddenly unable to access storage (permission issues), try forcequitting termux, revoke all of its permissions in android settings, and run the command `termux-setup-storage`\n\n\n# install on iOS\n\nfirst install one of the following:\n* [a-Shell mini](https://apps.apple.com/us/app/a-shell-mini/id1543537943) gives you the essential features\n* [a-Shell](https://apps.apple.com/us/app/a-shell/id1473805438) also enables audio transcoding and better thubmnails\n\nand then copypaste the following command into `a-Shell`:\n\n```sh\ncurl -L https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh | sh\n```\n\n> if you want the latest copyparty beta, then do this instead:  \n> `curl -L https://copyparty.eu/beta/setup-ashell.sh | sh`\n\nwhat this does:\n* creates a basic [config file](#accounts-and-volumes) named `cpc` which you can edit with `vim cpc`\n* adds the command `cpp` to launch copyparty with that config file\n\nknown issues:\n* cannot run in the background; it needs to be on-screen to accept connections / uploads / downloads\n* the best way to exit copyparty is to swipe away the app\n\n\n# reporting bugs\n\nideas for context to include, and where to submit them\n\nplease get in touch using any of the following URLs:\n* https://github.com/9001/copyparty/ **(primary)**\n* https://gitlab.com/9001/copyparty/ *(mirror)*\n* https://codeberg.org/9001/copyparty *(mirror)*\n\nin general, commandline arguments (and config file if any)\n\nif something broke during an upload (replacing FILENAME with a part of the filename that broke):\n```\njournalctl -aS '48 hour ago' -u copyparty | grep -C10 FILENAME | tee bug.log\n```\n\nif there's a wall of base64 in the log (thread stacks) then please include that, especially if you run into something freezing up or getting stuck, for example `OperationalError('database is locked')` -- alternatively you can visit `/?stack` to see the stacks live, so http://127.0.0.1:3923/?stack for example\n\n\n# devnotes\n\nfor build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md)\n\nspecifically you may want to [build the sfx](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#just-the-sfx) or [build from scratch](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#build-from-scratch)\n\nsee [./docs/TODO.md](./docs/TODO.md) for planned features / fixes / changes\n\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\nif you hit something extra juicy pls let me know on one of the following:\n* email -- `copyparty@ocv.ze` except `ze` should be `me`\n* [github private vulnerability report](https://github.com/9001/copyparty/security/advisories/new), wow that form is complicated\n\nno bug bounties sorry! all i can offer is greetz in the release notes\n"
  },
  {
    "path": "bin/README.md",
    "content": "# [`u2c.py`](u2c.py)\n* command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)\n* file uploads, file-search, autoresume of aborted/broken uploads\n* [sync local folder to server](https://github.com/9001/copyparty/#folder-sync)\n* generally faster than browsers\n* if something breaks just restart it\n\n\n# [`partyjournal.py`](partyjournal.py)\nproduces a chronological list of all uploads by collecting info from up2k databases and the filesystem\n* outputs a standalone html file\n* optional mapping from IP-addresses to nicknames\n\n\n# [`partyfuse.py`](partyfuse.py)\n* mount a copyparty server as a local filesystem (read-only)\n* **supports Windows!** -- expect `194 MiB/s` sequential read\n* **supports Linux** -- expect `600 MiB/s` sequential read\n* **supports macos** -- expect `85 MiB/s` sequential read\n\nnote that copyparty should run with `-ed` to enable dotfiles (hidden otherwise)\n\nand consider using [../docs/rclone.md](../docs/rclone.md) instead; usually a bit faster, especially on windows\n\n\n## to run this on windows:\n* install [winfsp](https://github.com/billziss-gh/winfsp/releases/latest) and [python 3](https://www.python.org/downloads/)\n  * [x] add python 3.x to PATH (it asks during install)\n* `python -m pip install --user fusepy` (or grab a copy of `fuse.py` from the `connect` page on your copyparty, and keep it in the same folder)\n* `python ./partyfuse.py n: http://192.168.1.69:3923/`\n\n10% faster in [msys2](https://www.msys2.org/), 700% faster if debug prints are enabled:\n* `pacman -S mingw64/mingw-w64-x86_64-python{,-pip}`\n* `/mingw64/bin/python3 -m pip install --user fusepy`\n* `/mingw64/bin/python3 ./partyfuse.py [...]`\n\nyou could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releases/latest), let me know if you [figure out how](https://github.com/dokan-dev/dokany/wiki/FUSE)  \n(winfsp's sshfs leaks, doesn't look like winfsp itself does, should be fine)\n\n\n\n# [`partyfuse2.py`](partyfuse2.py)\n* mount a copyparty server as a local filesystem (read-only)\n* does the same thing except more correct, `samba` approves\n* **supports Linux** -- expect `18 MiB/s` (wait what)\n* **supports Macos** -- probably\n\n\n\n# [`partyfuse-streaming.py`](partyfuse-streaming.py)\n* pretend this doesn't exist\n\n\n\n# [`mtag/`](mtag/)\n* standalone programs which perform misc. file analysis\n* copyparty can Popen programs like these during file indexing to collect additional metadata\n\n\n\n# [`dbtool.py`](dbtool.py)\nupgrade utility which can show db info and help transfer data between databases, for example when a new version of copyparty is incompatible with the old DB and automatically rebuilds the DB from scratch, but you have some really expensive `-mtp` parsers and want to copy over the tags from the old db\n\nfor that example (upgrading to v0.11.20), first launch the new version of copyparty like usual, let it make a backup of the old db and rebuild the new db until the point where it starts running mtp (colored messages as it adds the mtp tags), that's when you hit CTRL-C and patch in the old mtp tags from the old db instead\n\nso assuming you have `-mtp` parsers to provide the tags `key` and `.bpm`:\n\n```\ncd /mnt/nas/music/.hist\n~/src/copyparty/bin/dbtool.py -ls up2k.db\n~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -cmp\n~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy key\n~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy .bpm -vac\n```\n\n\n\n# [`prisonparty.sh`](prisonparty.sh)\n* run copyparty in a chroot, preventing any accidental file access\n* creates bindmounts for /bin, /lib, and so on, see `sysdirs=`\n\n# [`bubbleparty.sh`](bubbleparty.sh)\n* run copyparty in an isolated process, preventing any accidental file access and more\n"
  },
  {
    "path": "bin/bubbleparty.sh",
    "content": "#!/bin/bash\n# usage: ./bubbleparty.sh ./copyparty-sfx.py ....\nbwrap \\\n  --unshare-all \\\n  --ro-bind /usr /usr \\\n  --ro-bind /bin /bin \\\n  --ro-bind /lib /lib \\\n  --ro-bind /etc/resolv.conf /etc/resolv.conf \\\n  --dev-bind /dev /dev \\\n  --dir /tmp \\\n  --dir /var \\\n  --bind \"$(pwd)\" \"$(pwd)\" \\\n  --share-net \\\n  --die-with-parent \\\n  --file 11 /etc/passwd \\\n  --file 12 /etc/group \\\n  \"$@\" \\\n  11< <(getent passwd $(id -u) 65534) \\\n  12< <(getent group $(id -g) 65534) \n"
  },
  {
    "path": "bin/dbtool.py",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport sys\nimport time\nimport shutil\nimport sqlite3\nimport argparse\n\nDB_VER1 = 3\nDB_VER2 = 6\n\nBY_PATH = None\nNC = None\n\n\ndef die(msg):\n    print(\"\\033[31m\\n\" + msg + \"\\n\\033[0m\")\n    sys.exit(1)\n\n\ndef read_ver(db):\n    for tab in [\"ki\", \"kv\"]:\n        try:\n            c = db.execute(r\"select v from {} where k = 'sver'\".format(tab))\n        except:\n            continue\n\n        rows = c.fetchall()\n        if rows:\n            return int(rows[0][0])\n\n    return \"corrupt\"\n\n\ndef ls(db):\n    nfiles = next(db.execute(\"select count(w) from up\"))[0]\n    ntags = next(db.execute(\"select count(w) from mt\"))[0]\n    print(f\"{nfiles} files\")\n    print(f\"{ntags} tags\\n\")\n\n    print(\"number of occurrences for each tag,\")\n    print(\" 'x' = file has no tags\")\n    print(\" 't:mtp' = the mtp flag (file not mtp processed yet)\")\n    print()\n    for k, nk in db.execute(\"select k, count(k) from mt group by k order by k\"):\n        print(f\"{nk:9} {k}\")\n\n\ndef compare(n1, d1, n2, d2, verbose):\n    nt = next(d1.execute(\"select count(w) from up\"))[0]\n    n = 0\n    miss = 0\n    for w1, rd, fn in d1.execute(\"select w, rd, fn from up\"):\n        n += 1\n        if n % 25_000 == 0:\n            m = f\"\\033[36mchecked {n:,} of {nt:,} files in {n1} against {n2}\\033[0m\"\n            print(m)\n\n        if rd.split(\"/\", 1)[0] == \".hist\":\n            continue\n\n        if BY_PATH:\n            q = \"select w from up where rd = ? and fn = ?\"\n            hit = d2.execute(q, (rd, fn)).fetchone()\n        else:\n            q = \"select w from up where substr(w,1,16) = ? and +w = ?\"\n            hit = d2.execute(q, (w1[:16], w1)).fetchone()\n\n        if not hit:\n            miss += 1\n            if verbose:\n                print(f\"file in {n1} missing in {n2}: [{w1}] {rd}/{fn}\")\n\n    print(f\" {miss} files in {n1} missing in {n2}\\n\")\n\n    nt = next(d1.execute(\"select count(w) from mt\"))[0]\n    n = 0\n    miss = {}\n    nmiss = 0\n    for w1s, k, v in d1.execute(\"select * from mt\"):\n\n        n += 1\n        if n % 100_000 == 0:\n            m = f\"\\033[36mchecked {n:,} of {nt:,} tags in {n1} against {n2}, so far {nmiss} missing tags\\033[0m\"\n            print(m)\n\n        q = \"select w, rd, fn from up where substr(w,1,16) = ?\"\n        w1, rd, fn = d1.execute(q, (w1s,)).fetchone()\n        if rd.split(\"/\", 1)[0] == \".hist\":\n            continue\n\n        if BY_PATH:\n            q = \"select w from up where rd = ? and fn = ?\"\n            w2 = d2.execute(q, (rd, fn)).fetchone()\n        else:\n            q = \"select w from up where substr(w,1,16) = ? and +w = ?\"\n            w2 = d2.execute(q, (w1s, w1)).fetchone()\n\n        if w2:\n            w2 = w2[0]\n\n        v2 = None\n        if w2:\n            v2 = d2.execute(\n                \"select v from mt where w = ? and +k = ?\", (w2[:16], k)\n            ).fetchone()\n            if v2:\n                v2 = v2[0]\n\n        # if v != v2 and v2 and k in [\".bpm\", \"key\"] and n2 == \"src\":\n        #    print(f\"{w} [{rd}/{fn}] {k} = [{v}] / [{v2}]\")\n\n        if v2 is not None:\n            if k.startswith(\".\"):\n                try:\n                    diff = abs(float(v) - float(v2))\n                    if diff > float(v) / 0.9:\n                        v2 = None\n                    else:\n                        v2 = v\n                except:\n                    pass\n\n            if v != v2:\n                v2 = None\n\n        if v2 is None:\n            nmiss += 1\n            try:\n                miss[k] += 1\n            except:\n                miss[k] = 1\n\n            if verbose:\n                print(f\"missing in {n2}: [{w1}] [{rd}/{fn}] {k} = {v}\")\n\n    for k, v in sorted(miss.items()):\n        if v:\n            print(f\"{n1} has {v:7} more {k:<7} tags than {n2}\")\n\n    print(f\"in total, {nmiss} missing tags in {n2}\\n\")\n\n\ndef copy_mtp(d1, d2, tag, rm):\n    nt = next(d1.execute(\"select count(w) from mt where k = ?\", (tag,)))[0]\n    n = 0\n    ncopy = 0\n    nskip = 0\n    for w1s, k, v in d1.execute(\"select * from mt where k = ?\", (tag,)):\n        n += 1\n        if n % 25_000 == 0:\n            m = f\"\\033[36m{n:,} of {nt:,} tags checked, so far {ncopy} copied, {nskip} skipped\\033[0m\"\n            print(m)\n\n        q = \"select w, rd, fn from up where substr(w,1,16) = ?\"\n        w1, rd, fn = d1.execute(q, (w1s,)).fetchone()\n        if rd.split(\"/\", 1)[0] == \".hist\":\n            continue\n\n        if BY_PATH:\n            q = \"select w from up where rd = ? and fn = ?\"\n            w2 = d2.execute(q, (rd, fn)).fetchone()\n        else:\n            q = \"select w from up where substr(w,1,16) = ? and +w = ?\"\n            w2 = d2.execute(q, (w1s, w1)).fetchone()\n\n        if not w2:\n            continue\n\n        w2s = w2[0][:16]\n        hit = d2.execute(\"select v from mt where w = ? and +k = ?\", (w2s, k)).fetchone()\n        if hit:\n            hit = hit[0]\n\n        if hit != v:\n            if NC and hit is not None:\n                nskip += 1\n                continue\n\n            ncopy += 1\n            if hit is not None:\n                d2.execute(\"delete from mt where w = ? and +k = ?\", (w2s, k))\n\n            d2.execute(\"insert into mt values (?,?,?)\", (w2s, k, v))\n            if rm:\n                d2.execute(\"delete from mt where w = ? and +k = 't:mtp'\", (w2s,))\n\n    d2.commit()\n    print(f\"copied {ncopy} {tag} tags over, skipped {nskip}\")\n\n\ndef examples():\n    print(\n        \"\"\"\n# clearing the journal\n./dbtool.py up2k.db\n\n# copy tags \".bpm\" and \"key\" from old.db to up2k.db, and remove the mtp flag from matching files (so copyparty won't run any mtps on it)\n./dbtool.py -ls up2k.db\n./dbtool.py -src old.db up2k.db -cmp\n./dbtool.py -src old.v3 up2k.db -rm-mtp-flag -copy key\n./dbtool.py -src old.v3 up2k.db -rm-mtp-flag -copy .bpm -vac\n\n\"\"\"\n    )\n\n\ndef main():\n    global NC, BY_PATH  # pylint: disable=global-statement\n    os.system(\"\")\n    print()\n\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"db\", help=\"database to work on\")\n    ap.add_argument(\"-h2\", action=\"store_true\", help=\"show examples\")\n    ap.add_argument(\"-src\", metavar=\"DB\", type=str, help=\"database to copy from\")\n\n    ap2 = ap.add_argument_group(\"informational / read-only stuff\")\n    ap2.add_argument(\"-v\", action=\"store_true\", help=\"verbose\")\n    ap2.add_argument(\"-ls\", action=\"store_true\", help=\"list summary for db\")\n    ap2.add_argument(\"-cmp\", action=\"store_true\", help=\"compare databases\")\n\n    ap2 = ap.add_argument_group(\"options which modify target db\")\n    ap2.add_argument(\"-copy\", metavar=\"TAG\", type=str, help=\"mtp tag to copy over\")\n    ap2.add_argument(\n        \"-rm-mtp-flag\",\n        action=\"store_true\",\n        help=\"when an mtp tag is copied over, also mark that file as done, so copyparty won't run any mtps on those files\",\n    )\n    ap2.add_argument(\"-vac\", action=\"store_true\", help=\"optimize DB\")\n\n    ap2 = ap.add_argument_group(\"behavior modifiers\")\n    ap2.add_argument(\n        \"-nc\",\n        action=\"store_true\",\n        help=\"no-clobber; don't replace/overwrite existing tags\",\n    )\n    ap2.add_argument(\n        \"-by-path\",\n        action=\"store_true\",\n        help=\"match files based on location rather than warks (content-hash), use this if the databases have different wark salts\",\n    )\n\n    ar = ap.parse_args()\n    if ar.h2:\n        examples()\n        return\n\n    NC = ar.nc\n    BY_PATH = ar.by_path\n\n    for v in [ar.db, ar.src]:\n        if v and not os.path.exists(v):\n            die(\"database must exist\")\n\n    db = sqlite3.connect(ar.db)\n    ds = sqlite3.connect(ar.src) if ar.src else None\n\n    # revert journals\n    for d, p in [[db, ar.db], [ds, ar.src]]:\n        if not d:\n            continue\n\n        pj = \"{}-journal\".format(p)\n        if not os.path.exists(pj):\n            continue\n\n        d.execute(\"create table foo (bar int)\")\n        d.execute(\"drop table foo\")\n\n    if ar.copy:\n        db.close()\n        shutil.copy2(ar.db, \"{}.bak.dbtool.{:x}\".format(ar.db, int(time.time())))\n        db = sqlite3.connect(ar.db)\n\n    for d, n in [[ds, \"src\"], [db, \"dst\"]]:\n        if not d:\n            continue\n\n        ver = read_ver(d)\n        if ver == \"corrupt\":\n            die(\"{} database appears to be corrupt, sorry\")\n\n        iver = int(ver)\n        if iver < DB_VER1 or iver > DB_VER2:\n            m = f\"{n} db is version {ver}, this tool only supports versions between {DB_VER1} and {DB_VER2}, please upgrade it with copyparty first\"\n            die(m)\n\n    if ar.ls:\n        ls(db)\n\n    if ar.cmp:\n        if not ds:\n            die(\"need src db to compare against\")\n\n        compare(\"src\", ds, \"dst\", db, ar.v)\n        compare(\"dst\", db, \"src\", ds, ar.v)\n\n    if ar.copy:\n        copy_mtp(ds, db, ar.copy, ar.rm_mtp_flag)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/handlers/README.md",
    "content": "replace the standard 404 / 403 responses with plugins\n\n\n# usage\n\nload plugins either globally with `--on404 ~/dev/copyparty/bin/handlers/sorry.py` or for a specific volume with `:c,on404=~/handlers/sorry.py`\n\n\n# api\n\neach plugin must define a `main()` which takes 3 arguments;\n\n* `cli` is an instance of [copyparty/httpcli.py](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/httpcli.py) (the monstrosity itself)\n* `vn` is the VFS which overlaps with the requested URL, and\n* `rem` is the URL remainder below the VFS mountpoint\n    * so `vn.vpath + rem` == `cli.vpath` == original request\n\n\n# examples\n\n## on404\n\n* [redirect.py](redirect.py) sends an HTTP 301 or 302, redirecting the client to another page/file\n* [randpic.py](randpic.py) redirects `/foo/bar/randpic.jpg` to a random pic in `/foo/bar/`\n* [sorry.py](answer.py) replies with a custom message instead of the usual 404\n* [nooo.py](nooo.py) replies with an endless noooooooooooooo\n* [never404.py](never404.py) 100% guarantee that 404 will never be a thing again as it automatically creates dummy files whenever necessary\n* [caching-proxy.py](caching-proxy.py) transforms copyparty into a squid/varnish knockoff\n\n## on403\n\n* [ip-ok.py](ip-ok.py) disables security checks if client-ip is 1.2.3.4\n\n\n# notes\n\n* on403 only works for trivial stuff (basic http access) since I haven't been able to think of any good usecases for it (was just easy to add while doing on404)\n"
  },
  {
    "path": "bin/handlers/caching-proxy.py",
    "content": "# assume each requested file exists on another webserver and\n# download + mirror them as they're requested\n# (basically pretend we're warnish)\n\nimport os\nimport requests\n\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from copyparty.httpcli import HttpCli\n\n\ndef main(cli: \"HttpCli\", vn, rem):\n    url = \"https://mirrors.edge.kernel.org/alpine/\" + rem\n    abspath = os.path.join(vn.realpath, rem)\n\n    # sneaky trick to preserve a requests-session between downloads\n    # so it doesn't have to spend ages reopening https connections;\n    # luckily we can stash it inside the copyparty client session,\n    # name just has to be definitely unused so \"hacapo_req_s\" it is\n    req_s = getattr(cli.conn, \"hacapo_req_s\", None) or requests.Session()\n    setattr(cli.conn, \"hacapo_req_s\", req_s)\n\n    try:\n        os.makedirs(os.path.dirname(abspath), exist_ok=True)\n        with req_s.get(url, stream=True, timeout=69) as r:\n            r.raise_for_status()\n            with open(abspath, \"wb\", 64 * 1024) as f:\n                for buf in r.iter_content(chunk_size=64 * 1024):\n                    f.write(buf)\n    except:\n        os.unlink(abspath)\n        return \"false\"\n\n    return \"retry\"\n"
  },
  {
    "path": "bin/handlers/ip-ok.py",
    "content": "# disable permission checks and allow access if client-ip is 1.2.3.4\n\n\ndef main(cli, vn, rem):\n    if cli.ip == \"1.2.3.4\":\n        return \"allow\"\n"
  },
  {
    "path": "bin/handlers/never404.py",
    "content": "# create a dummy file and let copyparty return it\n\n\ndef main(cli, vn, rem):\n    print(\"hello\", cli.ip)\n\n    abspath = vn.canonical(rem)\n    with open(abspath, \"wb\") as f:\n        f.write(b\"404? not on MY watch!\")\n\n    return \"retry\"\n"
  },
  {
    "path": "bin/handlers/nooo.py",
    "content": "# reply with an endless \"noooooooooooooooooooooooo\"\n\n\ndef say_no():\n    yield b\"n\"\n    while True:\n        yield b\"o\" * 4096\n\n\ndef main(cli, vn, rem):\n    cli.send_headers(\"oh_f\", None, 404, \"text/plain\")\n\n    for chunk in say_no():\n        cli.s.sendall(chunk)\n\n    return \"false\"\n"
  },
  {
    "path": "bin/handlers/randpic.py",
    "content": "import os\nimport random\nfrom urllib.parse import quote\n\n\n# assuming /foo/bar/ is a valid URL but /foo/bar/randpic.png does not exist,\n# hijack the 404 with a redirect to a random pic in that folder\n#\n# thx to lia & kipu for the idea\n\n\ndef main(cli, vn, rem):\n    req_fn = rem.split(\"/\")[-1]\n    if not cli.can_read or not req_fn.startswith(\"randpic\"):\n        return\n\n    req_abspath = vn.canonical(rem)\n    req_ap_dir = os.path.dirname(req_abspath)\n    files_in_dir = os.listdir(req_ap_dir)\n\n    if \".\" in req_fn:\n        file_ext = \".\" + req_fn.split(\".\")[-1]\n        files_in_dir = [x for x in files_in_dir if x.lower().endswith(file_ext)]\n\n    if not files_in_dir:\n        return\n\n    selected_file = random.choice(files_in_dir)\n\n    req_url = \"/\".join([vn.vpath, rem]).strip(\"/\")\n    req_dir = req_url.rsplit(\"/\", 1)[0]\n    new_url = \"/\".join([req_dir, quote(selected_file)]).strip(\"/\")\n\n    cli.reply(b\"redirecting...\", 302, headers={\"Location\": \"/\" + new_url})\n    return \"true\"\n"
  },
  {
    "path": "bin/handlers/redirect.py",
    "content": "# if someone hits a 404, redirect them to another location\n\n\ndef send_http_302_temporary_redirect(cli, new_path):\n    \"\"\"\n    replies with an HTTP 302, which is a temporary redirect;\n    \"new_path\" can be any of the following:\n      - \"http://a.com/\" would redirect to another website,\n      - \"/foo/bar\" would redirect to /foo/bar on the same server;\n          note the leading '/' in the location which is important\n    \"\"\"\n    cli.reply(b\"redirecting...\", 302, headers={\"Location\": new_path})\n\n\ndef send_http_301_permanent_redirect(cli, new_path):\n    \"\"\"\n    replies with an HTTP 301, which is a permanent redirect;\n    otherwise identical to send_http_302_temporary_redirect\n    \"\"\"\n    cli.reply(b\"redirecting...\", 301, headers={\"Location\": new_path})\n\n\ndef send_errorpage_with_redirect_link(cli, new_path):\n    \"\"\"\n    replies with a website explaining that the page has moved;\n    \"new_path\" must be an absolute location on the same server\n    but without a leading '/', so for example \"foo/bar\"\n    would redirect to \"/foo/bar\"\n    \"\"\"\n    cli.redirect(new_path, click=False, msg=\"this page has moved\")\n\n\ndef main(cli, vn, rem):\n    \"\"\"\n    this is the function that gets called by copyparty;\n    note that vn.vpath and cli.vpath does not have a leading '/'\n    so we're adding the slash in the debug messages below\n    \"\"\"\n    print(f\"this client just hit a 404: {cli.ip}\")\n    print(f\"they were accessing this volume: /{vn.vpath}\")\n    print(f\"and the original request-path (straight from the URL) was /{cli.vpath}\")\n    print(f\"...which resolves to the following filesystem path: {vn.canonical(rem)}\")\n\n    new_path = \"/foo/bar/\"\n    print(f\"will now redirect the client to {new_path}\")\n\n    # uncomment one of these:\n    send_http_302_temporary_redirect(cli, new_path)\n    # send_http_301_permanent_redirect(cli, new_path)\n    # send_errorpage_with_redirect_link(cli, new_path)\n\n    return \"true\"\n"
  },
  {
    "path": "bin/handlers/sorry.py",
    "content": "# sends a custom response instead of the usual 404\n\n\ndef main(cli, vn, rem):\n    msg = f\"sorry {cli.ip} but {cli.vpath} doesn't exist\"\n\n    return str(cli.reply(msg.encode(\"utf-8\"), 404, \"text/plain\"))\n"
  },
  {
    "path": "bin/hooks/README.md",
    "content": "standalone programs which are executed by copyparty when an event happens (upload, file rename, delete, ...)\n\nthese programs either take zero arguments, or a filepath (the affected file), or a json message with filepath + additional info\n\nrun copyparty with `--help-hooks` for usage details / hook type explanations (xm/xbu/xau/xiu/xbc/xac/xbr/xar/xbd/xad/xban)\n\nin particular, if a hook is loaded into copyparty with the hook-flag `c` (\"check\") then its exit-code controls the action that launched the hook:\n* exit-code `0` = allow the action, and/or continue running the next hook\n* exit-code `100` = allow the action, and stop running any remaining consecutive hooks\n* anything else = reject/prevent the original action, and don't run the remaining hooks\n\n> **note:** in addition to event hooks (the stuff described here), copyparty has another api to run your programs/scripts while providing way more information such as audio tags / video codecs / etc and optionally daisychaining data between scripts in a processing pipeline; if that's what you want then see [mtp plugins](../mtag/) instead\n\n\n# after upload\n* [notify.py](notify.py) shows a desktop notification ([example](https://user-images.githubusercontent.com/241032/215335767-9c91ed24-d36e-4b6b-9766-fb95d12d163f.png))\n  * [notify2.py](notify2.py) uses the json API to show more context\n* [image-noexif.py](image-noexif.py) removes image exif by overwriting / directly editing the uploaded file\n* [discord-announce.py](discord-announce.py) announces new uploads on discord using webhooks ([example](https://user-images.githubusercontent.com/241032/215304439-1c1cb3c8-ec6f-4c17-9f27-81f969b1811a.png))\n* [reject-mimetype.py](reject-mimetype.py) rejects uploads unless the mimetype is acceptable\n* [into-the-cache-it-goes.py](into-the-cache-it-goes.py) avoids bugs in caching proxies by immediately downloading each file that is uploaded\n* [podcast-normalizer.py](podcast-normalizer.py) creates a second file with dynamic-range-compression whenever an audio file is uploaded\n  * good example of the `idx` [hook effect](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hook-effects) to tell copyparty about additional files to scan/index\n\n\n# upload batches\nthese are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every single file), `xiu` hooks are given a list of recent uploads on STDIN after the server has gone idle for N seconds, reducing server load + providing more context\n* [xiu.py](xiu.py) is a \"minimal\" example showing a list of filenames + total filesize\n* [xiu-sha.py](xiu-sha.py) produces a sha512 checksum list in the volume root\n\n\n# before upload\n* [reject-extension.py](reject-extension.py) rejects uploads if they match a list of file extensions\n* [reloc-by-ext.py](reloc-by-ext.py) redirects an upload to another destination based on the file extension\n  * good example of the `reloc` [hook effect](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hook-effects)\n* [reject-and-explain.py](reject-and-explain.py) shows a custom error-message when it rejects an upload\n* [reject-ramdisk.py](reject-ramdisk.py) rejects the upload if the destination is a ramdisk\n  * this hook uses the `I` flag which makes it 140x faster, but if the plugin has a bug it may crash copyparty\n\n\n# on message\n* [wget.py](wget.py) lets you download files by POSTing URLs to copyparty\n  * [wget-i.py](wget-i.py) is an import-safe modification of this hook (starts 140x faster, but higher chance of bugs)\n* [qbittorrent-magnet.py](qbittorrent-magnet.py) starts downloading a torrent if you post a magnet url\n* [usb-eject.py](usb-eject.py) adds web-UI buttons to safe-remove usb flashdrives shared through copyparty\n* [msg-log.py](msg-log.py) is a guestbook; logs messages to a doc in the same folder\n\n\n# general concept demos\n* [import-me.py](import-me.py) shows how the `I` flag makes the hook 140x faster (but you need to be Very Careful when writing the plugin)\n  * [wget-i.py](wget-i.py) is an import-safe modification of [wget.py](wget.py)\n"
  },
  {
    "path": "bin/hooks/discord-announce.py",
    "content": "#!/usr/bin/env python3\n\nimport sys\nimport json\nimport requests\nfrom copyparty.util import humansize, quotep\n\n\n_ = r\"\"\"\nannounces a new upload on discord\n\nexample usage as global config:\n    --xau f,t5,j,bin/hooks/discord-announce.py\n\nparameters explained,\n    xau = execute after upload\n    f  = fork; don't delay other hooks while this is running\n    t5 = timeout if it's still running after 5 sec\n    j  = this hook needs upload information as json (not just the filename)\n\nexample usage as a volflag (per-volume config):\n    -v srv/inc:inc:r:rw,ed:c,xau=f,t5,j,bin/hooks/discord-announce.py\n                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    (share filesystem-path srv/inc as volume /inc,\n     readable by everyone, read-write for user 'ed',\n     running this plugin on all uploads with the params explained above)\n\nexample usage as a volflag in a copyparty config file:\n    [/inc]\n      srv/inc\n      accs:\n        r: *\n        rw: ed\n      flags:\n        xau: f,t5,j,bin/hooks/discord-announce.py\n\nreplace \"xau\" with \"xbu\" to announce Before upload starts instead of After completion\n\n# how to discord:\nfirst create the webhook url; https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks\nthen use this to design your message: https://discohook.org/\n\"\"\"\n\n\ndef main():\n    WEBHOOK = \"https://discord.com/api/webhooks/1234/base64\"\n    WEBHOOK = \"https://discord.com/api/webhooks/1066830390280597718/M1TDD110hQA-meRLMRhdurych8iyG35LDoI1YhzbrjGP--BXNZodZFczNVwK4Ce7Yme5\"\n\n    # read info from copyparty\n    inf = json.loads(sys.argv[1])\n    vpath = inf[\"vp\"]\n    filename = vpath.split(\"/\")[-1]\n    url = f\"https://{inf['host']}/{quotep(vpath)}\"\n\n    # compose the message to discord\n    j = {\n        \"title\": filename,\n        \"url\": url,\n        \"description\": url.rsplit(\"/\", 1)[0],\n        \"color\": 0x449900,\n        \"fields\": [\n            {\"name\": \"Size\", \"value\": humansize(inf[\"sz\"])},\n            {\"name\": \"User\", \"value\": inf[\"user\"]},\n            {\"name\": \"IP\", \"value\": inf[\"ip\"]},\n        ],\n    }\n\n    for v in j[\"fields\"]:\n        v[\"inline\"] = True\n\n    r = requests.post(WEBHOOK, json={\"embeds\": [j]})\n    print(f\"discord: {r}\\n\", end=\"\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/hooks/image-noexif.py",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport sys\nimport subprocess as sp\n\n\n_ = r\"\"\"\nremove exif tags from uploaded images; the eventhook edition of\nhttps://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/image-noexif.py\n\ndependencies:\n    exiftool / perl-Image-ExifTool\n\nbeing an upload hook, this will take effect after upload completion\n    but before copyparty has hashed/indexed the file, which means that\n    copyparty will never index the original file, so deduplication will\n    not work as expected... which is mostly OK but ehhh\n\nnote: modifies the file in-place, so don't set the `f` (fork) flag\n\nexample usages; either as global config (all volumes) or as volflag:\n    --xau bin/hooks/image-noexif.py\n    -v srv/inc:inc:r:rw,ed:c,xau=bin/hooks/image-noexif.py\n                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nexplained:\n    share fs-path srv/inc at /inc (readable by all, read-write for user ed)\n    running this xau (execute-after-upload) plugin for all uploaded files\n\"\"\"\n\n\n# filetypes to process; ignores everything else\nEXTS = (\"jpg\", \"jpeg\", \"avif\", \"heif\", \"heic\")\n\n\ntry:\n    from copyparty.util import fsenc\nexcept:\n\n    def fsenc(p):\n        return p.encode(\"utf-8\")\n\n\ndef main():\n    fp = sys.argv[1]\n    ext = fp.lower().split(\".\")[-1]\n    if ext not in EXTS:\n        return\n\n    cwd, fn = os.path.split(fp)\n    os.chdir(cwd)\n    f1 = fsenc(fn)\n    cmd = [\n        b\"exiftool\",\n        b\"-exif:all=\",\n        b\"-iptc:all=\",\n        b\"-xmp:all=\",\n        b\"-P\",\n        b\"-overwrite_original\",\n        b\"--\",\n        f1,\n    ]\n    sp.check_output(cmd)\n    print(\"image-noexif: stripped\")\n\n\nif __name__ == \"__main__\":\n    try:\n        main()\n    except:\n        pass\n"
  },
  {
    "path": "bin/hooks/import-me.py",
    "content": "#!/usr/bin/env python3\n\nfrom typing import Any\n\n_ = r\"\"\"\nthe fastest hook in the west\n(runs directly inside copyparty, not as a subprocess)\n\nexample usage as global config:\n    --xbu I,bin/hooks/import-me.py\n\nexample usage as a volflag (per-volume config):\n    -v srv/inc:inc:r:rw,ed:c,xbu=I,bin/hooks/import-me.py\n                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    (share filesystem-path srv/inc as volume /inc,\n     readable by everyone, read-write for user 'ed',\n     running this plugin on all uploads with the params listed below)\n\nexample usage as a volflag in a copyparty config file:\n    [/inc]\n      srv/inc\n      accs:\n        r: *\n        rw: ed\n      flags:\n        xbu: I,bin/hooks/import-me.py\n\nparameters explained,\n    I = import; do not fork / subprocess\n\nIMPORTANT NOTE:\n    because this hook is running inside copyparty, you need to\n    be EXCEPTIONALLY CAREFUL to avoid side-effects, for example\n    DO NOT os.chdir() or anything like that, and also make sure\n    that the name of this file is unique (cannot be the same as\n    an existing python module/library)\n\"\"\"\n\n\ndef main(ka: dict[str, Any]) -> dict[str, Any]:\n    # \"ka\" is a dictionary with info from copyparty...\n\n    # but because we are running inside copyparty, we don't need such courtesies;\n    import inspect\n\n    cf = inspect.currentframe().f_back.f_back.f_back\n    t = \"hello from hook; I am able to peek into copyparty's memory like so:\\n  function name: %s\\n  variables:\\n    %s\\n\"\n    t2 = \"\\n    \".join([(\"%r: %r\" % (k, v))[:99] for k, v in cf.f_locals.items()][:9])\n    logger = ka[\"log\"]\n    logger(t % (cf.f_code, t2))\n\n    # must return a dictionary with:\n    #  \"rc\": the retcode; 0 is ok\n    return {\"rc\": 0}\n"
  },
  {
    "path": "bin/hooks/into-the-cache-it-goes.py",
    "content": "#!/usr/bin/env python3\n\nimport sys\nimport json\nimport shutil\nimport platform\nimport subprocess as sp\nfrom urllib.parse import quote\n\n\n_ = r\"\"\"\ntry to avoid race conditions in caching proxies\n(primarily cloudflare, but probably others too)\nby means of the most obvious solution possible:\n\njust as each file has finished uploading, use\nthe server's external URL to download the file\nso that it ends up in the cache, warm and snug\n\nthis intentionally delays the upload response\nas it waits for the file to finish downloading\nbefore copyparty is allowed to return the URL\n\nNOTE: you must edit this script before use,\n  replacing https://example.com with your URL\n\nNOTE: if the files are only accessible with a\n  password and/or filekey, you must also add\n  a cromulent password in the PASSWORD field\n\nNOTE: needs either wget, curl, or \"requests\":\n  python3 -m pip install --user -U requests\n\n\nexample usage as global config:\n    --xau j,t10,bin/hooks/into-the-cache-it-goes.py\n\nparameters explained,\n    xau = execute after upload\n    j   = this hook needs upload information as json (not just the filename)\n    t10 = abort download and continue if it takes longer than 10sec\n\nexample usage as a volflag (per-volume config):\n    -v srv/inc:inc:r:rw,ed:c,xau=j,t10,bin/hooks/into-the-cache-it-goes.py\n                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    (share filesystem-path srv/inc as volume /inc,\n     readable by everyone, read-write for user 'ed',\n     running this plugin on all uploads with params explained above)\n\nexample usage as a volflag in a copyparty config file:\n    [/inc]\n      srv/inc\n      accs:\n        r: *\n        rw: ed\n      flags:\n        xau: j,t10,bin/hooks/into-the-cache-it-goes.py\n\"\"\"\n\n\n# replace this with your site's external URL\n# (including the :portnumber if necessary)\nSITE_URL = \"https://example.com\"\n\n# if downloading is protected by passwords or filekeys,\n# specify a valid password between the quotes below:\nPASSWORD = \"\"\n\n# if file is larger than this, skip download\nMAX_MEGABYTES = 8\n\n# =============== END OF CONFIG ===============\n\n\nWINDOWS = platform.system() == \"Windows\"\n\n\ndef main():\n    fun = download_with_python\n    if shutil.which(\"curl\"):\n        fun = download_with_curl\n    elif shutil.which(\"wget\"):\n        fun = download_with_wget\n\n    inf = json.loads(sys.argv[1])\n\n    if inf[\"sz\"] > 1024 * 1024 * MAX_MEGABYTES:\n        print(\"[into-the-cache] file is too large; will not download\")\n        return\n\n    file_url = \"/\"\n    if inf[\"vp\"]:\n        file_url += inf[\"vp\"] + \"/\"\n    file_url += inf[\"ap\"].replace(\"\\\\\", \"/\").split(\"/\")[-1]\n    file_url = SITE_URL.rstrip(\"/\") + quote(file_url, safe=b\"/\")\n\n    print(\"[into-the-cache] %s(%s)\" % (fun.__name__, file_url))\n    fun(file_url, PASSWORD.strip())\n\n    print(\"[into-the-cache] Download OK\")\n\n\ndef download_with_curl(url, pw):\n    cmd = [\"curl\"]\n\n    if pw:\n        cmd += [\"-HPW:%s\" % (pw,)]\n\n    nah = sp.DEVNULL\n    sp.check_call(cmd + [url], stdout=nah, stderr=nah)\n\n\ndef download_with_wget(url, pw):\n    cmd = [\"wget\", \"-O\"]\n\n    cmd += [\"nul\" if WINDOWS else \"/dev/null\"]\n\n    if pw:\n        cmd += [\"--header=PW:%s\" % (pw,)]\n\n    nah = sp.DEVNULL\n    sp.check_call(cmd + [url], stdout=nah, stderr=nah)\n\n\ndef download_with_python(url, pw):\n    import requests\n\n    headers = {}\n    if pw:\n        headers[\"PW\"] = pw\n\n    with requests.get(url, headers=headers, stream=True) as r:\n        r.raise_for_status()\n        for _ in r.iter_content(chunk_size=1024 * 256):\n            pass\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/hooks/msg-log.py",
    "content": "#!/usr/bin/env python\n# coding: utf-8\n# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab\nfrom __future__ import print_function, unicode_literals\n\nimport json\nimport os\nimport sys\nimport time\n\ntry:\n    from datetime import datetime, timezone\nexcept:\n    from datetime import datetime\n\n\n_ = r\"\"\"\nuse copyparty as a dumb messaging server / guestbook thing;\naccepts guestbook entries from 📟 (message-to-server-log) in the web-ui\ninitially contributed by @clach04 in https://github.com/9001/copyparty/issues/35 (thanks!)\n\nexample usage as global config:\n    python copyparty-sfx.py --xm j,bin/hooks/msg-log.py\n\nparameters explained,\n    xm = execute on message (📟)\n    j  = this hook needs message information as json (not just the message-text)\n\nexample usage as a volflag (per-volume config):\n    python copyparty-sfx.py -v srv/log:log:r:c,xm=j,bin/hooks/msg-log.py\n                                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    (share filesystem-path srv/log as volume /log, readable by everyone,\n     running this plugin on all messages with the params explained above)\n\nexample usage as a volflag in a copyparty config file:\n    [/log]\n      srv/log\n      accs:\n        r: *\n      flags:\n        xm: j,bin/hooks/msg-log.py\n\"\"\"\n\n\n# output filename\nFILENAME = os.environ.get(\"COPYPARTY_MESSAGE_FILENAME\", \"\") or \"README.md\"\n\n# set True to write in descending order (newest message at top of file);\n# note that this becomes very slow/expensive as the file gets bigger\nDESCENDING = True\n\n# the message template; the following parameters are provided by copyparty and can be referenced below:\n# 'ap' = absolute filesystem path where the message was posted\n# 'vp' = virtual path (URL 'path') where the message was posted\n# 'mt' = 'at' = unix-timestamp when the message was posted\n# 'datetime' = ISO-8601 time when the message was posted\n# 'sz' = message size in bytes\n# 'host' = the server hostname which the user was accessing (URL 'host')\n# 'user' = username (if logged in), otherwise '*'\n# 'txt' = the message text itself\n# (uncomment the print(msg_info) to see if additional information has been introduced by copyparty since this was written)\nTEMPLATE = \"\"\"\n🕒 %(datetime)s, 👤 %(user)s @ %(ip)s\n%(txt)s\n\"\"\"\n\n\ndef write_ascending(filepath, msg_text):\n    with open(filepath, \"a\", encoding=\"utf-8\", errors=\"replace\") as outfile:\n        outfile.write(msg_text)\n\n\ndef write_descending(filepath, msg_text):\n    lockpath = filepath + \".lock\"\n    got_it = False\n    for _ in range(16):\n        try:\n            os.mkdir(lockpath)\n            got_it = True\n            break\n        except:\n            time.sleep(0.1)\n            continue\n\n    if not got_it:\n        return sys.exit(1)\n\n    try:\n        oldpath = filepath + \".old\"\n        os.rename(filepath, oldpath)\n        with open(oldpath, \"r\", encoding=\"utf-8\", errors=\"replace\") as infile, open(\n            filepath, \"w\", encoding=\"utf-8\", errors=\"replace\"\n        ) as outfile:\n            outfile.write(msg_text)\n            while True:\n                buf = infile.read(4096)\n                if not buf:\n                    break\n                outfile.write(buf)\n    finally:\n        try:\n            os.unlink(oldpath)\n        except:\n            pass\n        os.rmdir(lockpath)\n\n\ndef main(argv=None):\n    if argv is None:\n        argv = sys.argv\n\n    msg_info = json.loads(sys.argv[1])\n    # print(msg_info)\n\n    try:\n        dt = datetime.fromtimestamp(msg_info[\"at\"], timezone.utc)\n    except:\n        dt = datetime.utcfromtimestamp(msg_info[\"at\"])\n\n    msg_info[\"datetime\"] = dt.strftime(\"%Y-%m-%d, %H:%M:%S\")\n\n    msg_text = TEMPLATE % msg_info\n\n    filepath = os.path.join(msg_info[\"ap\"], FILENAME)\n\n    if DESCENDING and os.path.exists(filepath):\n        write_descending(filepath, msg_text)\n    else:\n        write_ascending(filepath, msg_text)\n\n    print(msg_text)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/hooks/notify.py",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport sys\nimport subprocess as sp\nfrom plyer import notification\n\n\n_ = r\"\"\"\nshow os notification on upload; works on windows, linux, macos, android\n\ndependencies:\n    windows: python3 -m pip install --user -U plyer\n    linux:   python3 -m pip install --user -U plyer\n    macos:   python3 -m pip install --user -U plyer pyobjus\n    android: just termux and termux-api\n\nexample usages; either as global config (all volumes) or as volflag:\n    --xau f,bin/hooks/notify.py\n    -v srv/inc:inc:r:rw,ed:c,xau=f,bin/hooks/notify.py\n                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    (share filesystem-path srv/inc as volume /inc,\n     readable by everyone, read-write for user 'ed',\n     running this plugin on all uploads with the params listed below)\n\nparameters explained,\n    xau = execute after upload\n    f   = fork so it doesn't block uploads\n\"\"\"\n\n\ntry:\n    from copyparty.util import humansize\nexcept:\n\n    def humansize(n):\n        return n\n\n\ndef main():\n    fp = sys.argv[1]\n    dp, fn = os.path.split(fp)\n    try:\n        sz = humansize(os.path.getsize(fp))\n    except:\n        sz = \"?\"\n\n    msg = \"{} ({})\\n📁 {}\".format(fn, sz, dp)\n    title = \"File received\"\n\n    if \"com.termux\" in sys.executable:\n        sp.run([\"termux-notification\", \"-t\", title, \"-c\", msg])\n        return\n\n    icon = \"emblem-documents-symbolic\" if sys.platform == \"linux\" else \"\"\n    notification.notify(\n        title=title,\n        message=msg,\n        app_icon=icon,\n        timeout=10,\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/hooks/notify2.py",
    "content": "#!/usr/bin/env python3\n\nimport json\nimport os\nimport sys\nimport subprocess as sp\nfrom datetime import datetime, timezone\nfrom plyer import notification\n\n\n_ = r\"\"\"\nsame as notify.py but with additional info (uploader, ...)\nand also supports --xm (notify on 📟 message)\n\nexample usages; either as global config (all volumes) or as volflag:\n    --xm  f,j,bin/hooks/notify2.py\n    --xau f,j,bin/hooks/notify2.py\n    -v srv/inc:inc:r:rw,ed:c,xm=f,j,bin/hooks/notify2.py\n    -v srv/inc:inc:r:rw,ed:c,xau=f,j,bin/hooks/notify2.py\n                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    (share filesystem-path srv/inc as volume /inc,\n     readable by everyone, read-write for user 'ed',\n     running this plugin on all uploads / msgs with the params listed below)\n\nparameters explained,\n    xau = execute after upload\n    f   = fork so it doesn't block uploads\n    j   = provide json instead of filepath list\n\"\"\"\n\n\ntry:\n    from copyparty.util import humansize\nexcept:\n\n    def humansize(n):\n        return n\n\n\ndef main():\n    inf = json.loads(sys.argv[1])\n    fp = inf[\"ap\"]\n    sz = humansize(inf[\"sz\"])\n    dp, fn = os.path.split(fp)\n    dt = datetime.fromtimestamp(inf[\"mt\"], timezone.utc)\n    mt = dt.strftime(\"%Y-%m-%d %H:%M:%S\")\n\n    msg = f\"{fn} ({sz})\\n📁 {dp}\"\n    title = \"File received\"\n    icon = \"emblem-documents-symbolic\" if sys.platform == \"linux\" else \"\"\n\n    if inf.get(\"txt\"):\n        msg = inf[\"txt\"]\n        title = \"Message received\"\n        icon = \"mail-unread-symbolic\" if sys.platform == \"linux\" else \"\"\n\n    msg += f\"\\n👤 {inf['user']} ({inf['ip']})\\n🕒 {mt}\"\n\n    if \"com.termux\" in sys.executable:\n        sp.run([\"termux-notification\", \"-t\", title, \"-c\", msg])\n        return\n\n    notification.notify(\n        title=title,\n        message=msg,\n        app_icon=icon,\n        timeout=10,\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/hooks/podcast-normalizer.py",
    "content": "#!/usr/bin/env python3\n\nimport json\nimport os\nimport sys\nimport subprocess as sp\n\n\n_ = r\"\"\"\nsends all uploaded audio files through an aggressive\ndynamic-range-compressor to even out the volume levels\n\ndependencies:\n    ffmpeg\n\nbeing an xau hook, this gets eXecuted After Upload completion\n    but before copyparty has started hashing/indexing the file, so\n    we'll create a second normalized copy in a subfolder and tell\n    copyparty to hash/index that additional file as well\n\nexample usage as global config:\n    -e2d -e2t --xau j,c1,bin/hooks/podcast-normalizer.py\n\nparameters explained,\n    e2d/e2t = enable database and metadata indexing\n    xau = execute after upload\n    j   = this hook needs upload information as json (not just the filename)\n    c1  = this hook returns json on stdout, so tell copyparty to read that\n\nexample usage as a volflag (per-volume config):\n    -v srv/inc/pods:inc/pods:r:rw,ed:c,xau=j,c1,bin/hooks/podcast-normalizer.py\n                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    (share fs-path srv/inc/pods at URL /inc/pods,\n     readable by all, read-write for user ed,\n     running this xau (exec-after-upload) plugin for all uploaded files)\n\nexample usage as a volflag in a copyparty config file:\n    [/inc/pods]\n      srv/inc/pods\n      accs:\n        r: *\n        rw: ed\n      flags:\n        e2d  # enables file indexing\n        e2t  # metadata tags too\n        xau: j,c1,bin/hooks/podcast-normalizer.py\n\n\"\"\"\n\n########################################################################\n### CONFIG\n\n# filetypes to process; ignores everything else\nEXTS = \"mp3 flac ogg oga opus m4a aac wav wma\"\n\n# the name of the subdir to put the normalized files in\nSUBDIR = \"normalized\"\n\n########################################################################\n\n\n# try to enable support for crazy filenames\ntry:\n    from copyparty.util import fsenc\nexcept:\n\n    def fsenc(p):\n        return p.encode(\"utf-8\")\n\n\ndef main():\n    # read info from copyparty\n    inf = json.loads(sys.argv[1])\n    vpath = inf[\"vp\"]\n    abspath = inf[\"ap\"]\n\n    # check if the file-extension is on the to-be-processed list\n    ext = abspath.lower().split(\".\")[-1]\n    if ext not in EXTS.split():\n        return\n\n    # jump into the folder where the file was uploaded\n    # and create the subfolder to place the normalized copy inside\n    dirpath, filename = os.path.split(abspath)\n    os.chdir(fsenc(dirpath))\n    os.makedirs(SUBDIR, exist_ok=True)\n\n    # the input and output filenames to give ffmpeg\n    fname_in = fsenc(f\"./{filename}\")\n    fname_out = fsenc(f\"{SUBDIR}/{filename}.opus\")\n\n    # fmt: off\n    # create and run the ffmpeg command\n    cmd = [\n        b\"ffmpeg\",\n        b\"-nostdin\",\n        b\"-hide_banner\",\n        b\"-i\", fname_in,\n        b\"-af\", b\"dynaudnorm=f=100:g=9\",  # the normalizer config\n        b\"-c:a\", b\"libopus\",\n        b\"-b:a\", b\"128k\",\n        fname_out,\n    ]\n    # fmt: on\n    sp.check_output(cmd)\n\n    # and finally, tell copyparty about the new file\n    # so it appears in the database and rss-feed:\n    vpath = f\"{SUBDIR}/{filename}.opus\"\n    print(json.dumps({\"idx\": {\"vp\": [vpath]}}))\n\n    # (it's fine to give it a relative path like that; it gets\n    #  resolved relative to the folder the file was uploaded into)\n\n\nif __name__ == \"__main__\":\n    try:\n        main()\n    except Exception as ex:\n        print(\"podcast-normalizer failed; %r\" % (ex,))\n"
  },
  {
    "path": "bin/hooks/qbittorrent-magnet.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\n\nimport os\nimport sys\nimport json\nimport shutil\nimport subprocess as sp\n\n\n_ = r\"\"\"\nstart downloading a torrent by POSTing a magnet URL to copyparty,\nfor example using 📟 (message-to-server-log) in the web-ui\n\nby default it will download the torrent to the folder you were in\nwhen you pasted the magnet into the message-to-server-log field\n\nyou can optionally specify another location by adding a whitespace\nafter the magnet URL followed by the name of the subfolder to DL into,\nor for example \"anime/airing\" would download to /srv/media/anime/airing\nbecause the keyword \"anime\" is in the DESTS config below\n\nneeds python3\n\nexample usage as global config (not a good idea):\n    python copyparty-sfx.py --xm aw,f,j,t60,bin/hooks/qbittorrent-magnet.py\n\nparameters explained,\n    xm = execute on message (📟)\n    aw = only users with write-access can use this\n    f = fork; don't delay other hooks while this is running\n    j = provide message information as json (not just the text)\n    t60 = abort if qbittorrent has to think about it for more than 1 min\n\nexample usage as a volflag (per-volume config, much better):\n    -v srv/qb:qb:A,ed:c,xm=aw,f,j,t60,bin/hooks/qbittorrent-magnet.py\n                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    (share filesystem-path srv/qb as volume /qb with Admin for user 'ed',\n     running this plugin on all messages with the params explained above)\n\nexample usage as a volflag in a copyparty config file:\n    [/qb]\n      srv/qb\n      accs:\n        A: ed\n      flags:\n        xm: aw,f,j,t60,bin/hooks/qbittorrent-magnet.py\n\nthe volflag examples only kicks in if you send the torrent magnet\nwhile you're in the /qb folder (or any folder below there)\n\"\"\"\n\n\n# list of usernames to allow\nALLOWLIST = [ \"ed\", \"morpheus\" ]\n\n\n# list of destination aliases to translate into full filesystem\n# paths; takes effect if the first folder component in the\n# custom download location matches anything in this dict\nDESTS = {\n    \"iso\": \"/srv/pub/linux-isos\",\n    \"anime\": \"/srv/media/anime\",\n}\n\n\ndef main():\n    inf = json.loads(sys.argv[1])\n    url = inf[\"txt\"]\n    if not url.lower().startswith(\"magnet:?\"):\n        # not a magnet, abort\n        return\n\n    if inf[\"user\"] not in ALLOWLIST:\n        print(\"🧲 denied for user\", inf[\"user\"])\n        return\n\n    # might as well run the command inside the filesystem folder\n    # which matches the URL that the magnet message was sent to\n    os.chdir(inf[\"ap\"])\n\n    # is there is a custom download location in the url?\n    dst = \"\"\n    if \" \" in url:\n        url, dst = url.split(\" \", 1)\n\n        # is the location in the predefined list of locations?\n        parts = dst.replace(\"\\\\\", \"/\").split(\"/\")\n        if parts[0] in DESTS:\n            dst = os.path.join(DESTS[parts[0]], *(parts[1:]))\n\n    else:\n        # nope, so download to the current folder instead;\n        # comment the dst line below to instead use the default\n        # download location from your qbittorrent settings\n        dst = inf[\"ap\"]\n        pass\n\n    # archlinux has a -nox suffix for qbittorrent if headless\n    # so check if we should be using that\n    if shutil.which(\"qbittorrent-nox\"):\n        torrent_bin = \"qbittorrent-nox\"\n    else:\n        torrent_bin = \"qbittorrent\"\n\n    # the command to add a new torrent, adjust if necessary\n    cmd = [torrent_bin, url]\n    if dst:\n        cmd += [\"--save-path=%s\" % (dst,)]\n\n    # if copyparty and qbittorrent are running as different users\n    # you may have to do something like the following\n    # (assuming qbittorrent* is nopasswd-allowed in sudoers):\n    #\n    # cmd = [\"sudo\", \"-u\", \"qbitter\"] + cmd\n\n    print(\"🧲\", cmd)\n\n    try:\n        sp.check_call(cmd)\n    except:\n        print(\"🧲 FAILED TO ADD\", url)\n\n\nif __name__ == \"__main__\":\n    main()\n\n"
  },
  {
    "path": "bin/hooks/reject-and-explain.py",
    "content": "#!/usr/bin/env python3\n\nimport json\nimport os\nimport re\nimport sys\n\n\n_ = r\"\"\"\nreject file upload (with a nice explanation why)\n\nexample usage as global config:\n    --xbu j,c1,bin/hooks/reject-and-explain.py\n\nexample usage as a volflag (per-volume config):\n    -v srv/inc:inc:r:rw,ed:c,xbu=j,c1,bin/hooks/reject-and-explain.py\n                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    (share filesystem-path srv/inc as volume /inc,\n     readable by everyone, read-write for user 'ed',\n     running this plugin on all uploads with the params listed below)\n\nexample usage as a volflag in a copyparty config file:\n    [/inc]\n      srv/inc\n      accs:\n        r: *\n        rw: ed\n      flags:\n        xbu: j,c1,bin/hooks/reject-and-explain.py\n\nparameters explained,\n    xbu = execute-before-upload  (can also be xau, execute-after-upload)\n    j   = this hook needs upload information as json (not just the filename)\n    c1  = this hook returns json on stdout, so tell copyparty to read that\n\"\"\"\n\n\ndef main():\n    inf = json.loads(sys.argv[1])\n    vdir, fn = os.path.split(inf[\"vp\"])\n    print(\"inf[vp] = %r\" % (inf[\"vp\"],), file=sys.stderr)\n\n    # the following is what decides if we'll accept the upload or reject it:\n    # we check if the upload-folder url matches the following regex-pattern:\n    ok = re.search(r\"(^|/)day[0-9]+$\", vdir, re.IGNORECASE)\n\n    if ok:\n        # allow the upload\n        print(\"{}\")\n        return\n\n    # the upload was rejected; display the following errortext:\n    errmsg = \"Files can only be uploaded into a folder named 'DayN' where N is a number, for example 'Day573'. This file was REJECTED: \"\n    errmsg += inf[\"vp\"]  # if you want to mention the file's url\n    print(json.dumps({\"rejectmsg\": errmsg}))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/hooks/reject-extension.py",
    "content": "#!/usr/bin/env python3\n\nimport sys\n\n\n_ = r\"\"\"\nreject file uploads by file extension\n\nexample usage as global config:\n    --xbu c,bin/hooks/reject-extension.py\n\nexample usage as a volflag (per-volume config):\n    -v srv/inc:inc:r:rw,ed:c,xbu=c,bin/hooks/reject-extension.py\n                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    (share filesystem-path srv/inc as volume /inc,\n     readable by everyone, read-write for user 'ed',\n     running this plugin on all uploads with the params listed below)\n\nparameters explained,\n    xbu = execute before upload\n    c   = check result, reject upload if error\n\"\"\"\n\n\ndef main():\n    bad = \"exe scr com pif bat ps1 jar msi\"\n\n    ext = sys.argv[1].split(\".\")[-1]\n\n    sys.exit(1 if ext in bad.split() else 0)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/hooks/reject-mimetype.py",
    "content": "#!/usr/bin/env python3\n\nimport sys\nimport magic\n\n\n_ = r\"\"\"\nreject file uploads by mimetype\n\ndependencies (linux, macos):\n    python3 -m pip install --user -U python-magic\n\ndependencies (windows):\n    python3 -m pip install --user -U python-magic-bin\n\nexample usage as global config:\n    --xau c,bin/hooks/reject-mimetype.py\n\nexample usage as a volflag (per-volume config):\n    -v srv/inc:inc:r:rw,ed:c,xau=c,bin/hooks/reject-mimetype.py\n                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    (share filesystem-path srv/inc as volume /inc,\n     readable by everyone, read-write for user 'ed',\n     running this plugin on all uploads with the params listed below)\n\nparameters explained,\n    xau = execute after upload\n    c   = check result, reject upload if error\n\"\"\"\n\n\ndef main():\n    ok = [\"image/jpeg\", \"image/png\"]\n\n    mt = magic.from_file(sys.argv[1], mime=True)\n\n    print(mt)\n\n    sys.exit(1 if mt not in ok else 0)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/hooks/reject-ramdisk.py",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport threading\nfrom argparse import Namespace\n\nfrom jinja2.nodes import Name\nfrom copyparty.fsutil import Fstab\nfrom typing import Any, Optional\n\n\n_ = r\"\"\"\nreject an upload if the target folder is on a ramdisk; useful when you\nhave a volume where some folders inside are ramdisks but others aren't\n\nexample usage as global config:\n    --xbu I,bin/hooks/reject-ramdisk.py\n\nexample usage as a volflag (per-volume config):\n    -v srv/inc:inc:r:rw,ed:c,xbu=I,bin/hooks/reject-ramdisk.py\n                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    (share filesystem-path srv/inc as volume /inc,\n     readable by everyone, read-write for user 'ed',\n     running this plugin on all uploads with the params listed below)\n\nexample usage as a volflag in a copyparty config file:\n    [/inc]\n      srv/inc\n      accs:\n        r: *\n        rw: ed\n      flags:\n        xbu: I,bin/hooks/reject-ramdisk.py\n\nparameters explained,\n    I = import; do not fork / subprocess\n\nIMPORTANT NOTE:\n    because this hook is imported inside copyparty, you need to\n    be EXCEPTIONALLY CAREFUL to avoid side-effects, for example\n    DO NOT os.chdir() or anything like that, and also make sure\n    that the name of this file is unique (cannot be the same as\n    an existing python module/library)\n\"\"\"\n\n\nmutex = threading.Lock()\nfstab: Optional[Fstab] = None\n\n\ndef main(ka: dict[str, Any]) -> dict[str, Any]:\n    global fstab\n    with mutex:\n        log = ka[\"log\"]  # this is a copyparty NamedLogger function\n        if not fstab:\n            log(\"<HOOK:RAMDISK> creating fstab\", 6)\n            args = Namespace()\n            args.mtab_age = 1  # cache the filesystem info for 1 sec\n            fstab = Fstab(log, args, False)\n\n        ap = ka[\"ap\"]  # abspath the upload is going to\n        fs, mp = fstab.get(ap)  # figure out what the filesystem is\n        ramdisk = fs in (\"tmpfs\", \"overlay\")  # looks like a ramdisk?\n\n        # log(\"<HOOK:RAMDISK> fs=%r\" % (fs,))\n\n        if ramdisk:\n            t = \"Upload REJECTED because destination is a ramdisk\"\n            return {\"rc\": 1, \"rejectmsg\": t}\n\n        return {\"rc\": 0}\n"
  },
  {
    "path": "bin/hooks/reloc-by-ext.py",
    "content": "#!/usr/bin/env python3\n\nimport json\nimport os\nimport re\nimport sys\n\n\n_ = r\"\"\"\nrelocate/redirect incoming uploads according to file extension or name\n\nexample usage as global config:\n    --xbu j,c1,bin/hooks/reloc-by-ext.py\n\nparameters explained,\n    xbu = execute before upload\n    j   = this hook needs upload information as json (not just the filename)\n    c1  = this hook returns json on stdout, so tell copyparty to read that\n\nexample usage as a volflag (per-volume config):\n    -v srv/inc:inc:r:rw,ed:c,xbu=j,c1,bin/hooks/reloc-by-ext.py\n                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    (share filesystem-path srv/inc as volume /inc,\n     readable by everyone, read-write for user 'ed',\n     running this plugin on all uploads with the params explained above)\n\nexample usage as a volflag in a copyparty config file:\n    [/inc]\n      srv/inc\n      accs:\n        r: *\n        rw: ed\n      flags:\n        xbu: j,c1,bin/hooks/reloc-by-ext.py\n\nnote: this could also work as an xau hook (after-upload), but\n  because it doesn't need to read the file contents its better\n  as xbu (before-upload) since that's safer / less buggy,\n  and only xbu works with up2k (dragdrop into browser)\n\"\"\"\n\n\nPICS = \"avif bmp gif heic heif jpeg jpg jxl png psd qoi tga tif tiff webp\"\nVIDS = \"3gp asf avi flv mkv mov mp4 mpeg mpeg2 mpegts mpg mpg2 nut ogm ogv rm ts vob webm wmv\"\nMUSIC = \"aac aif aiff alac amr ape dfpwm flac m4a mp3 ogg opus ra tak tta wav wma wv\"\n\n\ndef main():\n    inf = json.loads(sys.argv[1])\n    vdir, fn = os.path.split(inf[\"vp\"])\n\n    try:\n        fn, ext = fn.rsplit(\".\", 1)\n    except:\n        # no file extension; pretend it's \"bin\"\n        ext = \"bin\"\n\n    ext = ext.lower()\n\n    # this function must end by printing the action to perform;\n    # that's handled by the print(json.dumps(... at the bottom\n    #\n    # the action can contain the following keys:\n    # \"vp\" is the folder URL to move the upload to,\n    # \"ap\" is the filesystem-path to move it to (but \"vp\" is safer),\n    # \"fn\" overrides the final filename to use\n\n    ##\n    ## some example actions to take; pick one by\n    ## selecting it inside the print at the end:\n    ##\n\n    # move all uploads to one specific folder\n    into_junk = {\"vp\": \"/junk\"}\n\n    # create a subfolder named after the filetype and move it into there\n    into_subfolder = {\"vp\": ext}\n\n    # move it into a toplevel folder named after the filetype\n    into_toplevel = {\"vp\": \"/\" + ext}\n\n    # move it into a filetype-named folder next to the target folder\n    into_sibling = {\"vp\": \"../\" + ext}\n\n    # move images into \"/just/pics\", vids into \"/just/vids\",\n    # music into \"/just/tunes\", and anything else as-is\n    if ext in PICS.split():\n        by_category = {\"vp\": \"/just/pics\"}\n    elif ext in VIDS.split():\n        by_category = {\"vp\": \"/just/vids\"}\n    elif ext in MUSIC.split():\n        by_category = {\"vp\": \"/just/tunes\"}\n    else:\n        by_category = {}  # no action\n\n    # now choose the default effect to apply; can be any of these:\n    # into_junk  into_subfolder  into_toplevel  into_sibling  by_category\n    effect = into_sibling\n\n    ##\n    ## but we can keep going, adding more speicifc rules\n    ## which can take precedence, replacing the fallback\n    ## effect we just specified:\n    ##\n\n    fn = fn.lower()  # lowercase filename to make this easier\n\n    if \"screenshot\" in fn:\n        effect = {\"vp\": \"/ss\"}\n    if \"mpv_\" in fn:\n        effect = {\"vp\": \"/anishots\"}\n    elif \"debian\" in fn or \"biebian\" in fn:\n        effect = {\"vp\": \"/linux-ISOs\"}\n    elif re.search(r\"ep(isode |\\.)?[0-9]\", fn):\n        effect = {\"vp\": \"/podcasts\"}\n\n    # regex lets you grab a part of the matching\n    # text and use that in the upload path:\n    m = re.search(r\"\\b(op|ed)([^a-z]|$)\", fn)\n    if m:\n        # the regex matched; use \"anime-op\" or \"anime-ed\"\n        effect = {\"vp\": \"/anime-\" + m[1]}\n\n    # aaand DO IT\n    print(json.dumps({\"reloc\": effect}))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/hooks/usb-eject.js",
    "content": "// see usb-eject.py for usage\n\nfunction usbclick() {\n    var o = QS('#treeul a[dst=\"/usb/\"]') || QS('#treepar a[dst=\"/usb/\"]');\n    if (o)\n        o.click();\n}\n\nfunction eject_cb() {\n    var t = ('' + this.responseText).trim();\n    if (t.indexOf('can be safely unplugged') < 0 && t.indexOf('Device can be removed') < 0)\n        return toast.err(30, 'usb eject failed:\\n\\n' + t);\n\n    toast.ok(5, esc(t.replace(/ - /g, '\\n\\n')).trim());\n    usbclick(); setTimeout(usbclick, 10);\n};\n\nfunction add_eject_2(a) {\n    var aw = a.getAttribute('href').split(/\\//g);\n    if (aw.length != 4 || aw[3])\n        return;\n\n    var v = aw[2],\n        k = 'umount_' + v;\n\n    for (var b = 0; b < 9; b++) {\n        var o = ebi(k);\n        if (!o)\n            break;\n        o.parentNode.removeChild(o);\n    }\n\n    a.appendChild(mknod('span', k, '⏏'), a);\n    o = ebi(k);\n    o.style.cssText = 'position:absolute; right:1em; margin-top:-.2em; font-size:1.3em';\n    o.onclick = function (e) {\n        ev(e);\n        var xhr = new XHR();\n        xhr.open('POST', get_evpath(), true);\n        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8');\n        xhr.send('msg=' + uricom_enc(':usb-eject:' + v + ':'));\n        xhr.onload = xhr.onerror = eject_cb;\n        toast.inf(10, \"ejecting \" + v + \"...\");\n    };\n};\n\nfunction add_eject() {\n    var o = QSA('#treeul a[href^=\"/usb/\"]') || QSA('#treepar a[href^=\"/usb/\"]');\n    for (var a = o.length - 1; a > 0; a--)\n        add_eject_2(o[a]);\n};\n\n(function() {\n    var f0 = treectl.rendertree;\n    treectl.rendertree = function (res, ts, top0, dst, rst) {\n        var ret = f0(res, ts, top0, dst, rst);\n        add_eject();\n        return ret;\n    };\n})();\n\nsetTimeout(add_eject, 50);\n"
  },
  {
    "path": "bin/hooks/usb-eject.py",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport stat\nimport subprocess as sp\nimport sys\nfrom urllib.parse import unquote_to_bytes as unquote\n\n\n\"\"\"\nif you've found yourself using copyparty to serve flashdrives on a LAN\nand your only wish is that the web-UI had a button to unmount / safely\nremove those flashdrives, then boy howdy are you in the right place :D\n\nput usb-eject.js in the webroot (or somewhere else http-accessible)\nthen run copyparty with these args:\n\n   -v /run/media/egon:/usb:A:c,hist=/tmp/junk\n   --xm=c1,bin/hooks/usb-eject.py\n   --js-browser=/usb-eject.js\n\nwhich does the following respectively,\n\n  * share all of /run/media/egon as /usb with admin for everyone\n     and put the histpath somewhere it won't cause trouble\n  * run the usb-eject hook with stdout redirect to the web-ui\n  * add the complementary usb-eject.js to the browser\n\n\"\"\"\n\n\nMOUNT_BASE = b\"/run/media/egon/\"\n\n\ndef main():\n    try:\n        msg = sys.argv[1]\n        if msg.startswith(\"upload-queue-empty;\"):\n            return\n        label = msg.split(\":usb-eject:\")[1].split(\":\")[0]\n        mp = MOUNT_BASE + unquote(label)\n        # print(\"ejecting [%s]... \" % (mp,), end=\"\")\n        mp = os.path.abspath(os.path.realpath(mp))\n        st = os.lstat(mp)\n        if not stat.S_ISDIR(st.st_mode) or not mp.startswith(MOUNT_BASE):\n            raise Exception(\"not a regular directory\")\n\n        # if you're running copyparty as root (thx for the faith)\n        # you'll need something like this to make dbus talkative\n        cmd = b\"sudo -u egon DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus gio mount -e\"\n\n        # but if copyparty and the ui-session is running\n        # as the same user (good) then this is plenty\n        cmd = b\"gio mount -e\"\n\n        cmd = cmd.split(b\" \") + [mp]\n        ret = sp.check_output(cmd).decode(\"utf-8\", \"replace\")\n        print(ret.strip() or (label + \" can be safely unplugged\"))\n\n    except Exception as ex:\n        print(\"unmount failed: %r\" % (ex,))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/hooks/wget-i.py",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport threading\nimport subprocess as sp\n\n\n_ = r\"\"\"\nuse copyparty as a file downloader by POSTing URLs as\napplication/x-www-form-urlencoded (for example using the\n📟 message-to-server-log in the web-ui)\n\nthis hook is a modified copy of wget.py, modified to\nmake it import-safe so it can be run with the 'I' flag,\nwhich speeds up the startup time of the hook by 140x\n\nexample usage as global config:\n    --xm aw,I,bin/hooks/wget-i.py\n\nparameters explained,\n    xm = execute on message-to-server-log\n    aw = only users with write-access can use this\n    I = import; do not fork / subprocess\n\nexample usage as a volflag (per-volume config):\n    -v srv/inc:inc:r:rw,ed:c,xm=aw,I,bin/hooks/wget.py\n                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    (share filesystem-path srv/inc as volume /inc,\n     readable by everyone, read-write for user 'ed',\n     running this plugin on all messages with the params explained above)\n\nexample usage as a volflag in a copyparty config file:\n    [/inc]\n      srv/inc\n      accs:\n        r: *\n        rw: ed\n      flags:\n        xm: aw,I,bin/hooks/wget.py\n\nthe volflag examples only kicks in if you send the message\nwhile you're in the /inc folder (or any folder below there)\n\nIMPORTANT NOTE:\n    because this hook uses the 'I' flag to run inside copyparty,\n    many other flags will not work (f,j,c3,t3600 as seen in the\n    original wget.py), and furthermore + more importantly we\n    need to be EXCEPTIONALLY CAREFUL to avoid side-effects, so\n    the os.chdir has been replaced with cwd=dirpath for example\n\"\"\"\n\n\ndef do_stuff(inf):\n    \"\"\"\n    worker function which is executed in another thread to\n    avoid blocking copyparty while the download is running,\n    since we cannot use the 'f,t3600' hook-flags with 'I'\n    \"\"\"\n\n    # first things first; grab the logger-function which copyparty is letting us borrow\n    log = inf[\"log\"]\n\n    url = inf[\"txt\"]\n    if url.startswith(\"upload-queue-empty;\"):\n        return\n\n    if \"://\" not in url:\n        url = \"https://\" + url\n\n    proto = url.split(\"://\")[0].lower()\n    if proto not in (\"http\", \"https\", \"ftp\", \"ftps\"):\n        raise Exception(\"bad proto {}\".format(proto))\n\n    dirpath = inf[\"ap\"]\n\n    name = url.split(\"?\")[0].split(\"/\")[-1]\n    msg = \"-- DOWNLOADING \" + name\n    log(msg)\n    tfn = os.path.join(dirpath, msg)\n    open(tfn, \"wb\").close()\n\n    cmd = [\"wget\", \"--trust-server-names\", \"-nv\", \"--\", url]\n\n    try:\n        # two things to note here:\n        # - cannot use the `c3` hook-flag with `I` so mute output with stdout=sp.DEVNULL instead;\n        # - MUST NOT use os.chdir with 'I' so use cwd=dirpath instead \n        sp.check_call(cmd, cwd=dirpath, stdout=sp.DEVNULL)\n    except:\n        t = \"-- FAILED TO DOWNLOAD \" + name\n        log(t, 3)  # 3=yellow=warning\n        open(os.path.join(dirpath, t), \"wb\").close()\n        raise  # have copyparty scream about the details in the log\n\n    os.unlink(tfn)\n\n\ndef main(inf):\n    threading.Thread(target=do_stuff, args=(inf,), daemon=True).start()\n"
  },
  {
    "path": "bin/hooks/wget.py",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport sys\nimport json\nimport subprocess as sp\n\n\n_ = r\"\"\"\nuse copyparty as a file downloader by POSTing URLs as\napplication/x-www-form-urlencoded (for example using the\n📟 message-to-server-log in the web-ui)\n\nexample usage as global config:\n    --xm aw,f,j,t3600,bin/hooks/wget.py\n\nparameters explained,\n    xm = execute on message-to-server-log\n    aw = only users with write-access can use this\n    f = fork; don't delay other hooks while this is running\n    j = provide message information as json (not just the text)\n    c3 = mute all output\n    t3600 = timeout and abort download after 1 hour\n\nexample usage as a volflag (per-volume config):\n    -v srv/inc:inc:r:rw,ed:c,xm=aw,f,j,t3600,bin/hooks/wget.py\n                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    (share filesystem-path srv/inc as volume /inc,\n     readable by everyone, read-write for user 'ed',\n     running this plugin on all messages with the params explained above)\n\nexample usage as a volflag in a copyparty config file:\n    [/inc]\n      srv/inc\n      accs:\n        r: *\n        rw: ed\n      flags:\n        xm: aw,f,j,t3600,bin/hooks/wget.py\n\nthe volflag examples only kicks in if you send the message\nwhile you're in the /inc folder (or any folder below there)\n\"\"\"\n\n\ndef main():\n    inf = json.loads(sys.argv[1])\n    url = inf[\"txt\"]\n    if url.startswith(\"upload-queue-empty;\"):\n        return\n\n    if \"://\" not in url:\n        url = \"https://\" + url\n\n    proto = url.split(\"://\")[0].lower()\n    if proto not in (\"http\", \"https\", \"ftp\", \"ftps\"):\n        raise Exception(\"bad proto {}\".format(proto))\n\n    os.chdir(inf[\"ap\"])\n\n    name = url.split(\"?\")[0].split(\"/\")[-1]\n    tfn = \"-- DOWNLOADING \" + name\n    print(f\"{tfn}\\n\", end=\"\")\n    open(tfn, \"wb\").close()\n\n    cmd = [\"wget\", \"--trust-server-names\", \"-nv\", \"--\", url]\n\n    try:\n        sp.check_call(cmd)\n    except:\n        t = \"-- FAILED TO DOWNLOAD \" + name\n        print(f\"{t}\\n\", end=\"\")\n        open(t, \"wb\").close()\n\n    os.unlink(tfn)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/hooks/xiu-sha.py",
    "content": "#!/usr/bin/env python3\n\nimport hashlib\nimport json\nimport sys\nfrom datetime import datetime, timezone\n\n\n_ = r\"\"\"\nthis hook will produce a single sha512 file which\ncovers all recent uploads (plus metadata comments)\n\nuse this with --xiu, which makes copyparty buffer\nuploads until server is idle, providing file infos\non stdin (filepaths or json)\n\nexample usage as global config:\n    --xiu i5,j,bin/hooks/xiu-sha.py\n\nexample usage as a volflag (per-volume config):\n    -v srv/inc:inc:r:rw,ed:c,xiu=i5,j,bin/hooks/xiu-sha.py\n                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    (share filesystem-path srv/inc as volume /inc,\n     readable by everyone, read-write for user 'ed',\n     running this plugin on batches of uploads with the params listed below)\n\nparameters explained,\n    xiu = execute after uploads...\n    i5  = ...after volume has been idle for 5sec\n    j   = provide json instead of filepath list\n\nnote the \"f\" (fork) flag is not set, so this xiu\nwill block other xiu hooks while it's running\n\"\"\"\n\n\ntry:\n    from copyparty.util import fsenc\nexcept:\n\n    def fsenc(p):\n        return p\n\n\nUTC = timezone.utc\n\n\ndef humantime(ts):\n    return datetime.fromtimestamp(ts, UTC).strftime(\"%Y-%m-%d %H:%M:%S\")\n\n\ndef find_files_root(inf):\n    di = 9000\n    for f1, f2 in zip(inf, inf[1:]):\n        p1 = f1[\"ap\"].replace(\"\\\\\", \"/\").rsplit(\"/\", 1)[0]\n        p2 = f2[\"ap\"].replace(\"\\\\\", \"/\").rsplit(\"/\", 1)[0]\n        di = min(len(p1), len(p2), di)\n        di = next((i for i in range(di) if p1[i] != p2[i]), di)\n\n    return di + 1\n\n\ndef find_vol_root(inf):\n    return len(inf[0][\"ap\"][: -len(inf[0][\"vp\"])])\n\n\ndef main():\n    zb = sys.stdin.buffer.read()\n    zs = zb.decode(\"utf-8\", \"replace\")\n    inf = json.loads(zs)\n\n    # root directory (where to put the sha512 file);\n    # di = find_files_root(inf)  # next to the file closest to volume root\n    di = find_vol_root(inf)  # top of the entire volume\n\n    ret = []\n    total_sz = 0\n    for md in inf:\n        ap = md[\"ap\"]\n        rp = ap[di:]\n        total_sz += md[\"sz\"]\n        fsize = \"{:,}\".format(md[\"sz\"])\n        mtime = humantime(md[\"mt\"])\n        up_ts = humantime(md[\"at\"])\n\n        h = hashlib.sha512()\n        with open(fsenc(md[\"ap\"]), \"rb\", 512 * 1024) as f:\n            while True:\n                buf = f.read(512 * 1024)\n                if not buf:\n                    break\n\n                h.update(buf)\n\n        cksum = h.hexdigest()\n        meta = \" | \".join([md[\"wark\"], up_ts, mtime, fsize, md[\"ip\"]])\n        ret.append(\"# {}\\n{} *{}\".format(meta, cksum, rp))\n\n    ret.append(\"# {} files, {} bytes total\".format(len(inf), total_sz))\n    ret.append(\"\")\n    ftime = datetime.now(UTC).strftime(\"%Y-%m%d-%H%M%S.%f\")\n    fp = \"{}xfer-{}.sha512\".format(inf[0][\"ap\"][:di], ftime)\n    with open(fsenc(fp), \"wb\") as f:\n        f.write(\"\\n\".join(ret).encode(\"utf-8\", \"replace\"))\n\n    print(\"wrote checksums to {}\".format(fp))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/hooks/xiu.py",
    "content": "#!/usr/bin/env python3\n\nimport json\nimport sys\n\n\n_ = r\"\"\"\nthis hook prints absolute filepaths + total size\n\nuse this with --xiu, which makes copyparty buffer\nuploads until server is idle, providing file infos\non stdin (filepaths or json)\n\nexample usage as global config:\n    --xiu i1,j,bin/hooks/xiu.py\n\nexample usage as a volflag (per-volume config):\n    -v srv/inc:inc:r:rw,ed:c,xiu=i1,j,bin/hooks/xiu.py\n                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    (share filesystem-path srv/inc as volume /inc,\n     readable by everyone, read-write for user 'ed',\n     running this plugin on batches of uploads with the params listed below)\n\nparameters explained,\n    xiu = execute after uploads...\n    i1  = ...after volume has been idle for 1sec\n    j   = provide json instead of filepath list\n\nnote the \"f\" (fork) flag is not set, so this xiu\nwill block other xiu hooks while it's running\n\"\"\"\n\n\ndef main():\n    zb = sys.stdin.buffer.read()\n    zs = zb.decode(\"utf-8\", \"replace\")\n    inf = json.loads(zs)\n\n    total_sz = 0\n    for upload in inf:\n        sz = upload[\"sz\"]\n        total_sz += sz\n        print(\"{:9} {}\".format(sz, upload[\"ap\"]))\n\n    print(\"{} files, {} bytes total\".format(len(inf), total_sz))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/mtag/README.md",
    "content": "standalone programs which take an audio file as argument\n\nyou may want to forget about all this fancy complicated stuff and just use [event hooks](../hooks/) instead (which doesn't need `-e2ts` or ffmpeg) \n\n----\n\n**NOTE:** these all require `-e2ts` to be functional, meaning you need to do at least one of these: `apt install ffmpeg` or `pip3 install mutagen`\n\nsome of these rely on libraries which are not MIT-compatible\n\n* [audio-bpm.py](./audio-bpm.py) detects the BPM of music using the BeatRoot Vamp Plugin; imports GPL2\n* [audio-key.py](./audio-key.py) detects the melodic key of music using the Mixxx fork of keyfinder; imports GPL3\n\nthese invoke standalone programs which are GPL or similar, so is legally fine for most purposes:\n\n* [media-hash.py](./media-hash.py) generates checksums for audio and video streams; uses FFmpeg (LGPL or GPL)\n* [image-noexif.py](./image-noexif.py) removes exif tags from images; uses exiftool (GPLv1 or artistic-license)\n\nthese do not have any problematic dependencies at all:\n\n* [cksum.py](./cksum.py) computes various checksums\n* [exe.py](./exe.py) grabs metadata from .exe and .dll files (example for retrieving multiple tags with one parser)\n* [wget.py](./wget.py) lets you download files by POSTing URLs to copyparty\n  * also available as an [event hook](../hooks/wget.py)\n\n\n## dangerous plugins\n\nplugins in this section should only be used with appropriate precautions:\n\n* [very-bad-idea.py](./very-bad-idea.py) combined with [meadup.js](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js) converts copyparty into a janky yet extremely flexible chromecast clone\n  * also adds a virtual keyboard by @steinuil to the basic-upload tab for comfy couch crowd control\n  * anything uploaded through the [android app](https://github.com/9001/party-up) (files or links) are executed on the server, meaning anyone can infect your PC with malware... so protect this with a password and keep it on a LAN!\n  * [kamelåså](https://github.com/steinuil/kameloso) is a much better (and MUCH safer) alternative to this plugin\n    * powered by [chicken-curry-banana-pineapple-peanut pizza](https://a.ocv.me/pub/g/i/2025/01/298437ce-8351-4c8c-861c-fa131d217999.jpg?cache) so you know it's good\n    * and, unlike this plugin, kamelåså even has windows support (nice)\n\n\n# dependencies\n\nrun [`install-deps.sh`](install-deps.sh) to build/install most dependencies required by these programs (supports windows/linux/macos)\n\n*alternatively* (or preferably) use packages from your distro instead, then you'll need at least these:\n\n* from distro: `numpy vamp-plugin-sdk beatroot-vamp mixxx-keyfinder ffmpeg`\n* from pip: `keyfinder vamp`\n\n\n# usage from copyparty\n\n`copyparty -e2dsa -e2ts` followed by any combination of these:\n* `-mtp key=f,audio-key.py`\n* `-mtp .bpm=f,audio-bpm.py`\n* `-mtp ahash,vhash=f,media-hash.py`\n\n* `f,` makes the detected value replace any existing values\n* the `.` in `.bpm` indicates numeric value\n* assumes the python files are in the folder you're launching copyparty from, replace the filename with a relative/absolute path if that's not the case\n* `mtp` modules will not run if a file has existing tags in the db, so clear out the tags with `-e2tsr` the first time you launch with new `mtp` options\n\n\n## usage with volflags\n\ninstead of affecting all volumes, you can set the options for just one volume like so:\n\n`copyparty -v /mnt/nas/music:/music:r:c,e2dsa:c,e2ts` immediately followed by any combination of these:\n\n* `:c,mtp=key=f,audio-key.py`\n* `:c,mtp=.bpm=f,audio-bpm.py`\n* `:c,mtp=ahash,vhash=f,media-hash.py`\n\n\n# tips & tricks\n\n* to delete tags for all files below `blog*` and rescan that, `sqlite3 .hist/up2k.db \"delete from mt where w in (select substr(w,1,16) from up where rd like 'blog%')\";`\n"
  },
  {
    "path": "bin/mtag/audio-bpm.py",
    "content": "#!/usr/bin/env python\n\nimport os\nimport sys\nimport vamp\nimport tempfile\nimport numpy as np\nimport subprocess as sp\n\nfrom copyparty.util import fsenc\n\n\"\"\"\ndep: vamp\ndep: beatroot-vamp\ndep: ffmpeg\n\"\"\"\n\n\n# save beat timestamps to \".beats/filename.txt\"\nSAVE = False\n\n\ndef det(tf):\n    # fmt: off\n    sp.check_call([\n        b\"ffmpeg\",\n        b\"-nostdin\",\n        b\"-hide_banner\",\n        b\"-v\", b\"fatal\",\n        b\"-y\", b\"-i\", fsenc(sys.argv[1]),\n        b\"-map\", b\"0:a:0\",\n        b\"-ac\", b\"1\",\n        b\"-ar\", b\"22050\",\n        b\"-t\", b\"360\",\n        b\"-f\", b\"f32le\",\n        fsenc(tf)\n    ])\n    # fmt: on\n\n    with open(tf, \"rb\") as f:\n        d = np.fromfile(f, dtype=np.float32)\n        try:\n            # 98% accuracy on jcore\n            c = vamp.collect(d, 22050, \"beatroot-vamp:beatroot\")\n            cl = c[\"list\"]\n        except:\n            # fallback; 73% accuracy\n            plug = \"vamp-example-plugins:fixedtempo\"\n            c = vamp.collect(d, 22050, plug, parameters={\"maxdflen\": 40})\n            print(c[\"list\"][0][\"label\"].split(\" \")[0])\n            return\n\n    # throws if detection failed:\n    beats = [float(x[\"timestamp\"]) for x in cl]\n    bds = [b - a for a, b in zip(beats, beats[1:])]\n    bds.sort()\n    n0 = int(len(bds) * 0.2)\n    n1 = int(len(bds) * 0.75) + 1\n    bds = bds[n0:n1]\n    bpm = sum(bds)\n    bpm = round(60 * (len(bds) / bpm), 2)\n    print(f\"{bpm:.2f}\")\n\n    if SAVE:\n        fdir, fname = os.path.split(sys.argv[1])\n        bdir = os.path.join(fdir, \".beats\")\n        try:\n            os.mkdir(fsenc(bdir))\n        except:\n            pass\n\n        fp = os.path.join(bdir, fname) + \".txt\"\n        with open(fsenc(fp), \"wb\") as f:\n            txt = \"\\n\".join([f\"{x:.2f}\" for x in beats])\n            f.write(txt.encode(\"utf-8\"))\n\n\ndef main():\n    with tempfile.NamedTemporaryFile(suffix=\".pcm\", delete=False) as f:\n        f.write(b\"h\")\n        tf = f.name\n\n    try:\n        det(tf)\n    except:\n        pass  # mute\n    finally:\n        os.unlink(tf)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/mtag/audio-key-slicing.py",
    "content": "#!/usr/bin/env python\n\nimport re\nimport os\nimport sys\nimport tempfile\nimport subprocess as sp\n\nimport keyfinder\n\nfrom copyparty.util import fsenc\n\n\"\"\"\ndep: github/mixxxdj/libkeyfinder\ndep: pypi/keyfinder\ndep: ffmpeg\n\nnote: this is a janky edition of the regular audio-key.py,\n  slicing the files at 20sec intervals and keeping 5sec from each,\n  surprisingly accurate but still garbage (446 ok, 69 bad, 13% miss)\n\n  it is fast tho\n\"\"\"\n\n\ndef get_duration():\n    # TODO provide ffprobe tags to mtp as json\n\n    # fmt: off\n    dur = sp.check_output([\n        \"ffprobe\",\n        \"-hide_banner\",\n        \"-v\", \"fatal\",\n        \"-show_streams\",\n        \"-show_format\",\n        fsenc(sys.argv[1])\n    ])\n    # fmt: on\n\n    dur = dur.decode(\"ascii\", \"replace\").split(\"\\n\")\n    dur = [x.split(\"=\")[1] for x in dur if x.startswith(\"duration=\")]\n    dur = [float(x) for x in dur if re.match(r\"^[0-9\\.,]+$\", x)]\n    return list(sorted(dur))[-1] if dur else None\n\n\ndef get_segs(dur):\n    # keep first 5s of each 20s,\n    # keep entire last segment\n    ofs = 0\n    segs = []\n    while True:\n        seg = [ofs, 5]\n        segs.append(seg)\n        if dur - ofs < 20:\n            seg[-1] = int(dur - seg[0])\n            break\n\n        ofs += 20\n\n    return segs\n\n\ndef slice(tf):\n    dur = get_duration()\n    dur = min(dur, 600)  # max 10min\n    segs = get_segs(dur)\n\n    # fmt: off\n    cmd = [\n        \"ffmpeg\",\n        \"-nostdin\",\n        \"-hide_banner\",\n        \"-v\", \"fatal\",\n        \"-y\"\n    ]\n\n    for seg in segs:\n        cmd.extend([\n            \"-ss\", str(seg[0]),\n            \"-i\", fsenc(sys.argv[1])\n        ])\n    \n    filt = \"\"\n    for n, seg in enumerate(segs):\n        filt += \"[{}:a:0]atrim=duration={}[a{}]; \".format(n, seg[1], n)\n    \n    prev = \"a0\"\n    for n in range(1, len(segs)):\n        nxt = \"b{}\".format(n)\n        filt += \"[{}][a{}]acrossfade=d=0.5[{}]; \".format(prev, n, nxt)\n        prev = nxt\n\n    cmd.extend([\n        \"-filter_complex\", filt[:-2],\n        \"-map\", \"[{}]\".format(nxt),\n        \"-sample_fmt\", \"s16\",\n        tf\n    ])\n    # fmt: on\n\n    # print(cmd)\n    sp.check_call(cmd)\n\n\ndef det(tf):\n    slice(tf)\n    print(keyfinder.key(tf).camelot())\n\n\ndef main():\n    with tempfile.NamedTemporaryFile(suffix=\".flac\", delete=False) as f:\n        f.write(b\"h\")\n        tf = f.name\n\n    try:\n        det(tf)\n    finally:\n        os.unlink(tf)\n        pass\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/mtag/audio-key.py",
    "content": "#!/usr/bin/env python\n\nimport os\nimport sys\nimport tempfile\nimport subprocess as sp\n\ntry:\n    import keyfinder\n\n    PKF = True\nexcept:\n    PKF = False\n\nfrom copyparty.util import fsenc\n\n\"\"\"\ndep: github/mixxxdj/libkeyfinder\ndep: pypi/keyfinder  -OR-  EvanPurkhiser/keyfinder-cli\ndep: ffmpeg\n\"\"\"\n\n\n# tried trimming the first/last 5th, bad idea,\n# misdetects 9a law field (Sphere Caliber) as 10b,\n# obvious when mixing 9a ghostly parapara ship\n\n\ndef det(tf):\n    # fmt: off\n    sp.check_call([\n        b\"ffmpeg\",\n        b\"-nostdin\",\n        b\"-hide_banner\",\n        b\"-v\", b\"fatal\",\n        b\"-y\", b\"-i\", fsenc(sys.argv[1]),\n        b\"-map\", b\"0:a:0\",\n        b\"-t\", b\"300\",\n        b\"-sample_fmt\", b\"s16\",\n        fsenc(tf)\n    ])\n    # fmt: on\n\n    if PKF:\n        print(keyfinder.key(tf).camelot())\n    else:\n        # fmt: off\n        sp.check_call([\n            b\"keyfinder-cli\",\n            b\"-n\",\n            b\"camelot\",\n            fsenc(tf)\n        ])\n        # fmt: on\n\n\ndef main():\n    with tempfile.NamedTemporaryFile(suffix=\".flac\", delete=False) as f:\n        f.write(b\"h\")\n        tf = f.name\n\n    try:\n        det(tf)\n    except:\n        pass  # mute\n    finally:\n        os.unlink(tf)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/mtag/cksum.py",
    "content": "#!/usr/bin/env python3\n\nimport sys\nimport json\nimport struct\nimport base64\nimport hashlib\n\ntry:\n    from zlib_ng import zlib_ng as zlib\nexcept:\n    import zlib\n\ntry:\n    from copyparty.util import fsenc\nexcept:\n\n    def fsenc(p):\n        return p\n\n\n\"\"\"\ncalculates various checksums for uploads,\nusage: -mtp crc32,md5,sha1,sha256b=ad,bin/mtag/cksum.py\n\"\"\"\n\n\ndef main():\n    config = \"crc32 md5 md5b sha1 sha1b sha256 sha256b sha512/240 sha512b/240\"\n    # b suffix = base64 encoded\n    # slash = truncate to n bits\n\n    known = {\n        \"md5\": hashlib.md5,\n        \"sha1\": hashlib.sha1,\n        \"sha256\": hashlib.sha256,\n        \"sha512\": hashlib.sha512,\n    }\n    config = config.split()\n    hashers = {\n        k: v()\n        for k, v in known.items()\n        if k in [x.split(\"/\")[0].rstrip(\"b\") for x in known]\n    }\n    crc32 = 0 if \"crc32\" in config else None\n\n    with open(fsenc(sys.argv[1]), \"rb\", 512 * 1024) as f:\n        while True:\n            buf = f.read(64 * 1024)\n            if not buf:\n                break\n\n            for x in hashers.values():\n                x.update(buf)\n\n            if crc32 is not None:\n                crc32 = zlib.crc32(buf, crc32)\n\n    ret = {}\n    for s in config:\n        alg = s.split(\"/\")[0]\n        b64 = alg.endswith(\"b\")\n        alg = alg.rstrip(\"b\")\n        if alg in hashers:\n            v = hashers[alg].digest()\n        elif alg == \"crc32\":\n            v = crc32\n            if v < 0:\n                v &= 2 ** 32 - 1\n            v = struct.pack(\">L\", v)\n        else:\n            raise Exception(\"what is {}\".format(s))\n\n        if \"/\" in s:\n            v = v[: int(int(s.split(\"/\")[1]) / 8)]\n\n        if b64:\n            v = base64.b64encode(v).decode(\"ascii\").rstrip(\"=\")\n        else:\n            try:\n                v = v.hex()\n            except:\n                import binascii\n\n                v = binascii.hexlify(v)\n\n        ret[s] = v\n\n    print(json.dumps(ret, indent=4))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/mtag/exe.py",
    "content": "#!/usr/bin/env python\n\nimport sys\nimport time\nimport json\nimport pefile\n\n\"\"\"\nretrieve exe info,\nexample for multivalue providers\n\"\"\"\n\n\ndef unk(v):\n    return \"unk({:04x})\".format(v)\n\n\nclass PE2(pefile.PE):\n    def __init__(self, *a, **ka):\n        for k in [\n            # -- parse_data_directories:\n            \"parse_import_directory\",\n            \"parse_export_directory\",\n            # \"parse_resources_directory\",\n            \"parse_debug_directory\",\n            \"parse_relocations_directory\",\n            \"parse_directory_tls\",\n            \"parse_directory_load_config\",\n            \"parse_delay_import_directory\",\n            \"parse_directory_bound_imports\",\n            # -- full_load:\n            \"parse_rich_header\",\n        ]:\n            setattr(self, k, self.noop)\n\n        super(PE2, self).__init__(*a, **ka)\n\n    def noop(*a, **ka):\n        pass\n\n\ntry:\n    pe = PE2(sys.argv[1], fast_load=False)\nexcept:\n    sys.exit(0)\n\narch = pe.FILE_HEADER.Machine\nif arch == 0x14C:\n    arch = \"x86\"\nelif arch == 0x8664:\n    arch = \"x64\"\nelse:\n    arch = unk(arch)\n\ntry:\n    buildtime = time.gmtime(pe.FILE_HEADER.TimeDateStamp)\n    buildtime = time.strftime(\"%Y-%m-%d_%H:%M:%S\", buildtime)\nexcept:\n    buildtime = \"invalid\"\n\nui = pe.OPTIONAL_HEADER.Subsystem\nif ui == 2:\n    ui = \"GUI\"\nelif ui == 3:\n    ui = \"cmdline\"\nelse:\n    ui = unk(ui)\n\nextra = {}\nif hasattr(pe, \"FileInfo\"):\n    for v1 in pe.FileInfo:\n        for v2 in v1:\n            if v2.name != \"StringFileInfo\":\n                continue\n\n            for v3 in v2.StringTable:\n                for k, v in v3.entries.items():\n                    v = v.decode(\"utf-8\", \"replace\").strip()\n                    if not v:\n                        continue\n\n                    if k in [b\"FileVersion\", b\"ProductVersion\"]:\n                        extra[\"ver\"] = v\n\n                    if k in [b\"OriginalFilename\", b\"InternalName\"]:\n                        extra[\"orig\"] = v\n\nr = {\n    \"arch\": arch,\n    \"built\": buildtime,\n    \"ui\": ui,\n    \"cksum\": \"{:08x}\".format(pe.OPTIONAL_HEADER.CheckSum),\n}\nr.update(extra)\n\nprint(json.dumps(r, indent=4))\n"
  },
  {
    "path": "bin/mtag/file-ext.py",
    "content": "#!/usr/bin/env python\n\nimport sys\n\n\"\"\"\nexample that just prints the file extension\n\"\"\"\n\nprint(sys.argv[1].split(\".\")[-1])\n"
  },
  {
    "path": "bin/mtag/geotag.py",
    "content": "import json\nimport re\nimport sys\n\nfrom copyparty.util import fsenc, runcmd\n\n\n\"\"\"\nuses exiftool to geotag images based on embedded gps coordinates in exif data\n\nadds four new metadata keys:\n    .gps_lat = latitute\n    .gps_lon = longitude\n    .masl = meters above sea level\n    city = \"city, subregion, region\"\n\nusage: -mtp .masl,.gps_lat,.gps_lon,city=ad,t10,bin/mtag/geotag.py\n\nexample: https://a.ocv.me/pub/blog/j7/8/?grid=0\n\"\"\"\n\n\ndef main():\n    cmd = b\"exiftool -api geolocation -n\".split(b\" \")\n    rc, so, se = runcmd(cmd + [fsenc(sys.argv[1])])\n    ptn = re.compile(\"([^:]*[^ :]) *: (.*)\")\n    city = [\"\", \"\", \"\"]\n    ret = {}\n    for ln in so.split(\"\\n\"):\n        m = ptn.match(ln)\n        if not m:\n            continue\n        k, v = m.groups()\n        if k == \"Geolocation City\":\n            city[2] = v\n        elif k == \"Geolocation Subregion\":\n            city[1] = v\n        elif k == \"Geolocation Region\":\n            city[0] = v\n        elif k == \"GPS Latitude\":\n            ret[\".gps_lat\"] = \"%.04f\" % (float(v),)\n        elif k == \"GPS Longitude\":\n            ret[\".gps_lon\"] = \"%.04f\" % (float(v),)\n        elif k == \"GPS Altitude\":\n            ret[\".masl\"] = str(int(float(v)))\n    v = \", \".join(city).strip(\", \")\n    if v:\n        ret[\"city\"] = v\n    print(json.dumps(ret))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/mtag/guestbook-read.py",
    "content": "#!/usr/bin/env python3\n\n\"\"\"\nfetch latest msg from guestbook and return as tag\n\nexample copyparty config to use this:\n  --urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=guestbook=t10,ad,p,bin/mtag/guestbook-read.py:mte=+guestbook\n\nexplained:\n  for realpath srv/hello (served at /hello), write-only for everyone,\n  enable file analysis on upload (e2ts),\n  use mtp plugin \"bin/mtag/guestbook-read.py\" to provide metadata tag \"guestbook\",\n  do this on all uploads regardless of extension,\n  t10 = 10 seconds timeout for each dwonload,\n  ad = parse file regardless if FFmpeg thinks it is audio or not\n  p = request upload info as json on stdin (need ip)\n  mte=+guestbook enabled indexing of that tag for this volume\n\nPS: this requires e2ts to be functional,\n  meaning you need to do at least one of these:\n   * apt install ffmpeg\n   * pip3 install mutagen\n\"\"\"\n\n\nimport json\nimport os\nimport sqlite3\nimport sys\n\n\n# set 0 to allow infinite msgs from one IP,\n# other values delete older messages to make space,\n# so 1 only keeps latest msg\nNUM_MSGS_TO_KEEP = 1\n\n\ndef main():\n    fp = os.path.abspath(sys.argv[1])\n    fdir = os.path.dirname(fp)\n\n    zb = sys.stdin.buffer.read()\n    zs = zb.decode(\"utf-8\", \"replace\")\n    md = json.loads(zs)\n\n    ip = md[\"up_ip\"]\n\n    # can put the database inside `fdir` if you'd like,\n    # by default it saves to PWD:\n    # os.chdir(fdir)\n\n    db = sqlite3.connect(\"guestbook.db3\")\n    with db:\n        t = \"select msg from gb where ip = ? order by ts desc\"\n        r = db.execute(t, (ip,)).fetchone()\n        if r:\n            print(r[0])\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/mtag/guestbook.py",
    "content": "#!/usr/bin/env python3\n\n\"\"\"\nstore messages from users in an sqlite database\nwhich can be read from another mtp for example\n\ntakes input from application/x-www-form-urlencoded POSTs,\nfor example using the message/pager function on the website\n\nexample copyparty config to use this:\n  --urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=xgb=ebin,t10,ad,p,bin/mtag/guestbook.py:mte=+xgb\n\nexplained:\n  for realpath srv/hello (served at /hello),write-only for everyone,\n  enable file analysis on upload (e2ts),\n  use mtp plugin \"bin/mtag/guestbook.py\" to provide metadata tag \"xgb\",\n  do this on all uploads with the file extension \"bin\",\n  t300 = 300 seconds timeout for each dwonload,\n  ad = parse file regardless if FFmpeg thinks it is audio or not\n  p = request upload info as json on stdin\n  mte=+xgb enabled indexing of that tag for this volume\n\nPS: this requires e2ts to be functional,\n  meaning you need to do at least one of these:\n   * apt install ffmpeg\n   * pip3 install mutagen\n\"\"\"\n\n\nimport json\nimport os\nimport sqlite3\nimport sys\nfrom urllib.parse import unquote_to_bytes as unquote\n\n\n# set 0 to allow infinite msgs from one IP,\n# other values delete older messages to make space,\n# so 1 only keeps latest msg\nNUM_MSGS_TO_KEEP = 1\n\n\ndef main():\n    fp = os.path.abspath(sys.argv[1])\n    fdir = os.path.dirname(fp)\n    fname = os.path.basename(fp)\n    if not fname.startswith(\"put-\") or not fname.endswith(\".bin\"):\n        raise Exception(\"not a post file\")\n\n    zb = sys.stdin.buffer.read()\n    zs = zb.decode(\"utf-8\", \"replace\")\n    md = json.loads(zs)\n\n    buf = b\"\"\n    with open(fp, \"rb\") as f:\n        while True:\n            b = f.read(4096)\n            buf += b\n            if len(buf) > 4096:\n                raise Exception(\"too big\")\n\n            if not b:\n                break\n\n    if not buf:\n        raise Exception(\"file is empty\")\n\n    buf = unquote(buf.replace(b\"+\", b\" \"))\n    txt = buf.decode(\"utf-8\")\n\n    if not txt.startswith(\"msg=\"):\n        raise Exception(\"does not start with msg=\")\n\n    ip = md[\"up_ip\"]\n    ts = md[\"up_at\"]\n    txt = txt[4:]\n\n    # can put the database inside `fdir` if you'd like,\n    # by default it saves to PWD:\n    # os.chdir(fdir)\n\n    db = sqlite3.connect(\"guestbook.db3\")\n    try:\n        db.execute(\"select 1 from gb\").fetchone()\n    except:\n        with db:\n            db.execute(\"create table gb (ip text, ts real, msg text)\")\n            db.execute(\"create index gb_ip on gb(ip)\")\n\n    with db:\n        if NUM_MSGS_TO_KEEP == 1:\n            t = \"delete from gb where ip = ?\"\n            db.execute(t, (ip,))\n\n        t = \"insert into gb values (?,?,?)\"\n        db.execute(t, (ip, ts, txt))\n\n        if NUM_MSGS_TO_KEEP > 1:\n            t = \"select ts from gb where ip = ? order by ts desc\"\n            hits = db.execute(t, (ip,)).fetchall()\n\n            if len(hits) > NUM_MSGS_TO_KEEP:\n                lim = hits[NUM_MSGS_TO_KEEP][0]\n                t = \"delete from gb where ip = ? and ts <= ?\"\n                db.execute(t, (ip, lim))\n\n    print(txt)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/mtag/image-noexif.py",
    "content": "#!/usr/bin/env python3\n\n\"\"\"\nremove exif tags from uploaded images\n\ndependencies:\n  exiftool\n\nabout:\n  creates a \"noexif\" subfolder and puts exif-stripped copies of each image there,\n  the reason for the subfolder is to avoid issues with the up2k.db / deduplication:\n\n  if the original image is modified in-place, then copyparty will keep the original\n  hash in up2k.db for a while (until the next volume rescan), so if the image is\n  reuploaded after a rescan then the upload will be renamed and kept as a dupe\n\n  alternatively you could switch the logic around, making a copy of the original\n  image into a subfolder named \"exif\" and modify the original in-place, but then\n  up2k.db will be out of sync until the next rescan, so any additional uploads\n  of the same image will get symlinked (deduplicated) to the modified copy\n  instead of the original in \"exif\"\n\n  or maybe delete the original image after processing, that would kinda work too\n\nexample copyparty config to use this:\n  -v/mnt/nas/pics:pics:rwmd,ed:c,e2ts,mte=+noexif:c,mtp=noexif=ejpg,ejpeg,ad,bin/mtag/image-noexif.py\n\nexplained:\n  for realpath /mnt/nas/pics (served at /pics) with read-write-modify-delete for ed,\n  enable file analysis on upload (e2ts),\n  append \"noexif\" to the list of known tags (mtp),\n  and use mtp plugin \"bin/mtag/image-noexif.py\" to provide that tag,\n  do this on all uploads with the file extension \"jpg\" or \"jpeg\",\n  ad = parse file regardless if FFmpeg thinks it is audio or not\n\nPS: this requires e2ts to be functional,\n  meaning you need to do at least one of these:\n   * apt install ffmpeg\n   * pip3 install mutagen\n  and your python must have sqlite3 support compiled in\n\"\"\"\n\n\nimport os\nimport sys\nimport filecmp\nimport subprocess as sp\n\ntry:\n    from copyparty.util import fsenc\nexcept:\n\n    def fsenc(p):\n        return p.encode(\"utf-8\")\n\n\ndef main():\n    cwd, fn = os.path.split(sys.argv[1])\n    if os.path.basename(cwd) == \"noexif\":\n        return\n\n    os.chdir(cwd)\n    f1 = fsenc(fn)\n    f2 = fsenc(os.path.join(b\"noexif\", fn))\n    cmd = [\n        b\"exiftool\",\n        b\"-exif:all=\",\n        b\"-iptc:all=\",\n        b\"-xmp:all=\",\n        b\"-P\",\n        b\"-o\",\n        b\"noexif/\",\n        b\"--\",\n        f1,\n    ]\n    sp.check_output(cmd)\n    if not os.path.exists(f2):\n        print(\"failed\")\n        return\n\n    if filecmp.cmp(f1, f2, shallow=False):\n        print(\"clean\")\n    else:\n        print(\"exif\")\n\n    # lastmod = os.path.getmtime(f1)\n    # times = (int(time.time()), int(lastmod))\n    # os.utime(f2, times)\n\n\nif __name__ == \"__main__\":\n    try:\n        main()\n    except:\n        pass\n"
  },
  {
    "path": "bin/mtag/install-deps.sh",
    "content": "#!/bin/bash\nset -e\n\n\n# install dependencies for audio-*.py\n#\n# linux/alpine: requires gcc g++ make cmake patchelf {python3,ffmpeg,fftw,libsndfile}-dev py3-{wheel,pip} py3-numpy{,-dev}\n# linux/debian: requires libav{codec,device,filter,format,resample,util}-dev {libfftw3,python3,libsndfile1}-dev python3-{numpy,pip} vamp-{plugin-sdk,examples} patchelf cmake\n# linux/fedora: requires gcc gcc-c++ make cmake patchelf {python3,ffmpeg,fftw,libsndfile}-devel python3-numpy vamp-plugin-sdk qm-vamp-plugins\n# linux/arch:   requires gcc make cmake patchelf python3 ffmpeg fftw libsndfile python-{numpy,wheel,pip,setuptools}\n# win64: requires msys2-mingw64 environment\n# macos: requires macports\n#\n# has the following manual dependencies, especially on mac:\n#   https://www.vamp-plugins.org/pack.html\n#\n# installs stuff to the following locations:\n#   ~/pe/\n#   whatever your python uses for --user packages\n#\n# does the following terrible things:\n#   modifies the keyfinder python lib to load the .so in ~/pe\n\n\nexport FORCE_COLOR=1\n\nlinux=1\n\nwin=\n[ ! -z \"$MSYSTEM\" ] || [ -e /msys2.exe ] && {\n\t[ \"$MSYSTEM\" = MINGW64 ] || {\n\t\techo windows detected, msys2-mingw64 required\n\t\texit 1\n\t}\n\tpacman -S --needed mingw-w64-x86_64-{ffmpeg,python,python-pip,vamp-plugin-sdk}\n\twin=1\n\tlinux=\n}\n\nmac=\n[ $(uname -s) = Darwin ] && {\n\t#pybin=\"$(printf '%s\\n' /opt/local/bin/python* | (sed -E 's/(.*\\/[^/0-9]+)([0-9]?[^/]*)$/\\2 \\1/' || cat) | (sort -nr || cat) | (sed -E 's/([^ ]*) (.*)/\\2\\1/' || cat) | grep -E '/(python|pypy)[0-9\\.-]*$' | head -n 1)\"\n\tpybin=/opt/local/bin/python3.9\n\t[ -e \"$pybin\" ] || {\n\t\techo mac detected, python3 from macports required\n\t\texit 1\n\t}\n\tpkgs='ffmpeg python39 py39-wheel'\n\tninst=$(port installed | awk '/^  /{print$1}' | sort | uniq | grep -E '^('\"$(echo \"$pkgs\" | tr ' ' '|')\"')$' | wc -l)\n\t[ $ninst -eq 3 ] || {\n\t\tsudo port install $pkgs\n\t}\n\tmac=1\n\tlinux=\n}\n\nhash -r\n\n[ $mac ] || {\n\tcommand -v python3 && pybin=python3 || pybin=python\n}\n\n$pybin -c 'import numpy' ||\n$pybin -m pip install --user numpy\n\n\ncommand -v gnutar && tar() { gnutar \"$@\"; }\ncommand -v gtar && tar() { gtar \"$@\"; }\ncommand -v gsed && sed() { gsed \"$@\"; }\n\n\nneed() {\n\tcommand -v $1 >/dev/null || {\n\t\techo need $1\n\t\texit 1\n\t}\n}\nneed cmake\nneed ffmpeg\nneed $pybin\n#need patchelf\n\n\ntd=\"$(mktemp -d)\"\ncln() {\n\trm -rf \"$td\"\n}\ntrap cln EXIT\ncd \"$td\"\npwd\n\n\ndl_text() {\n\tcommand -v curl >/dev/null && exec curl \"$@\"\n\texec wget -O- \"$@\"\n}\ndl_files() {\n\tlocal yolo= ex=\n\t[ $1 = \"yolo\" ] && yolo=1 && ex=k && shift\n\tcommand -v curl >/dev/null && exec curl -${ex}JOL \"$@\"\n\t\n\t[ $yolo ] && ex=--no-check-certificate\n\texec wget --trust-server-names $ex \"$@\"\n}\nexport -f dl_files\n\n\ngithub_tarball() {\n\trm -rf g\n\tmkdir g\n\tcd g\n\tdl_text \"$1\" |\n\ttee ../json |\n\t(\n\t\t# prefer jq if available\n\t\tjq -r '.tarball_url' ||\n\n\t\t# fallback to awk (sorry)\n\t\tawk -F\\\" '/\"tarball_url\": \"/ {print$4}'\n\t) |\n\ttee /dev/stderr |\n\thead -n 1 |\n\ttr -d '\\r' | tr '\\n' '\\0' |\n\txargs -0 bash -c 'dl_files \"$@\"' _\n\tmv * ../tgz\n\tcd ..\n}\n\n\ngitlab_tarball() {\n\tdl_text \"$1\" |\n\ttee json |\n\t(\n\t\t# prefer jq if available\n\t\tjq -r '.[0].assets.sources[]|select(.format|test(\"tar.gz\")).url' ||\n\n\t\t# fallback to abomination\n\t\ttr \\\" '\\n' | grep -E '\\.tar\\.gz$' | head -n 1\n\t) |\n\ttee /dev/stderr |\n\thead -n 1 |\n\ttr -d '\\r' | tr '\\n' '\\0' |\n\ttee links |\n\txargs -0 bash -c 'dl_files \"$@\"' _\n}\n\n\ninstall_keyfinder() {\n\t# windows support:\n\t#   use msys2 in mingw-w64 mode\n\t#   pacman -S --needed mingw-w64-x86_64-{ffmpeg,python}\n\t\n\t[ -e $HOME/pe/keyfinder ] && {\n\t\techo found a keyfinder build in ~/pe, skipping\n\t\treturn\n\t}\n\n\t(cat /etc/alpine-release || echo a) 2>&1 | grep -E '3\\.2[3-9]' && {\n\t\techo \"alpine too new; ffmpeg8 is keyfinder-py incompat; giving up\"\n\t\treturn\n\t}\n\n\tcd \"$td\"\n\tgithub_tarball https://api.github.com/repos/mixxxdj/libkeyfinder/releases/latest\n\tls -al\n\n\ttar -xf tgz\n\trm tgz\n\tcd mixxxdj-libkeyfinder*\n\t\n\th=\"$HOME\"\n\tso=\"lib/libkeyfinder.so\"\n\tmemes=(-DBUILD_TESTING=OFF)\n\n\t[ $win ] &&\n\t\tso=\"bin/libkeyfinder.dll\" &&\n\t\th=\"$(printf '%s\\n' \"$USERPROFILE\" | tr '\\\\' '/')\" &&\n\t\tmemes+=(-G \"MinGW Makefiles\")\n\t\n\t[ $mac ] &&\n\t\tso=\"lib/libkeyfinder.dylib\"\n\n\tcmake -DCMAKE_INSTALL_PREFIX=\"$h/pe/keyfinder\" \"${memes[@]}\" -S . -B build\n\tcmake --build build --parallel $(nproc || echo 4)\n\tcmake --install build\n\n\tlibpath=\"$h/pe/keyfinder/$so\"\n\t[ $linux ] && [ ! -e \"$libpath\" ] &&\n\t\tso=lib64/libkeyfinder.so\n\t\n\tlibpath=\"$h/pe/keyfinder/$so\"\n\t[ -e \"$libpath\" ] || {\n\t\techo \"so not found at $sop\"\n\t\texit 1\n\t}\n\n\tx=${-//[^x]/}; set -x; cat /etc/alpine-release || true\n\t# rm -rf /Users/ed/Library/Python/3.9/lib/python/site-packages/*keyfinder*\n\tCFLAGS=\"-I$h/pe/keyfinder/include -I/opt/local/include -I/usr/include/ffmpeg\" \\\n\tCXXFLAGS=\"-I$h/pe/keyfinder/include -I/opt/local/include -I/usr/include/ffmpeg\" \\\n\tLDFLAGS=\"-L$h/pe/keyfinder/lib -L$h/pe/keyfinder/lib64 -L/opt/local/lib\" \\\n\tPKG_CONFIG_PATH=\"/c/msys64/mingw64/lib/pkgconfig:$h/pe/keyfinder/lib/pkgconfig\" \\\n\t$pybin -m pip install --user keyfinder\n\t[ \"$x\" ] || set +x\n\n\tpypath=\"$($pybin -c 'import keyfinder; print(keyfinder.__file__)')\"\n\tfor pyso in \"${pypath%/*}\"/*.so; do\n\t\t[ -e \"$pyso\" ] || break\n\t\tpatchelf --set-rpath \"${libpath%/*}\" \"$pyso\" ||\n\t\t\techo \"WARNING: patchelf failed (only fatal on musl-based distros)\"\n\tdone\n\t\n\tmv \"$pypath\"{,.bak}\n\t(\n\t\tprintf 'import ctypes\\nctypes.cdll.LoadLibrary(\"%s\")\\n' \"$libpath\"\n\t\tcat \"$pypath.bak\"\n\t) >\"$pypath\"\n\n\techo\n\techo libkeyfinder successfully installed to the following locations:\n\techo \"  $libpath\"\n\techo \"  $pypath\"\n}\n\n\nhave_beatroot() {\n\t$pybin -c 'import vampyhost, sys; plugs = vampyhost.list_plugins(); sys.exit(0 if \"beatroot-vamp:beatroot\" in plugs else 1)'\n}\n\n\ninstall_vamp() {\n\t# windows support:\n\t#   use msys2 in mingw-w64 mode\n\t#   pacman -S --needed mingw-w64-x86_64-{ffmpeg,python,python-pip,vamp-plugin-sdk}\n\t\n\t$pybin -m pip install --user vamp || {\n\t\tprintf '\\n\\033[7malright, trying something else...\\033[0m\\n'\n\t\t$pybin -m pip install --user --no-build-isolation vamp\n\t}\n\n\tcd \"$td\"\n\techo '#include <vamp-sdk/Plugin.h>' | g++ -x c++ -c -o /dev/null - || [ -e ~/pe/vamp-sdk ] || {\n\t\tprintf '\\033[33mcould not find the vamp-sdk, building from source\\033[0m\\n'\n\t\t(dl_files yolo https://ocv.me/mirror/vamp-plugin-sdk-2.10.0.tar.gz)\n\t\tsha512sum -c <(\n\t\t\techo \"153b7f2fa01b77c65ad393ca0689742d66421017fd5931d216caa0fcf6909355fff74706fabbc062a3a04588a619c9b515a1dae00f21a57afd97902a355c48ed  -\"\n\t\t) <vamp-plugin-sdk-2.10.0.tar.gz\n\t\ttar -xf vamp-plugin-sdk-2.10.0.tar.gz\n\t\trm -- *.tar.gz\n\t\tls -al\n\t\tcd vamp-plugin-sdk-*\n\t\tprintf '%s\\n' \"int main(int argc, char **argv) { return 0; }\" > host/vamp-simple-host.cpp\n\t\t./configure --disable-programs --prefix=$HOME/pe/vamp-sdk\n\t\tmake -j1 install\n\t}\n\n\tcd \"$td\"\n\thave_beatroot || {\n\t\tprintf '\\033[33mcould not find the vamp beatroot plugin, building from source\\033[0m\\n'\n\t\t(dl_files yolo https://ocv.me/mirror/beatroot-vamp-v1.0.tar.gz)\n\t\tsha512sum -c <(\n\t\t\techo \"1f444d1d58ccf565c0adfe99f1a1aa62789e19f5071e46857e2adfbc9d453037bc1c4dcb039b02c16240e9b97f444aaff3afb625c86aa2470233e711f55b6874  -\"\n\t\t) <beatroot-vamp-v1.0.tar.gz\n\t\ttar -xf beatroot-vamp-v1.0.tar.gz \n\t\trm -- *.tar.gz\n\t\tcd beatroot-vamp-v1.0\n\t\t[ -e ~/pe/vamp-sdk ] &&\n\t\t\tsed -ri 's`^(CFLAGS :=.*)`\\1 -I'$HOME'/pe/vamp-sdk/include`' Makefile.linux ||\n\t\t\tsed -ri 's`^(CFLAGS :=.*)`\\1 -I/usr/include/vamp-sdk`' Makefile.linux\n\t\tmake -f Makefile.linux -j4 LDFLAGS=\"-L$HOME/pe/vamp-sdk/lib -L/usr/lib64\"\n\t\t# /home/ed/vamp /home/ed/.vamp /usr/local/lib/vamp\n\t\tmkdir ~/vamp\n\t\tcp -pv beatroot-vamp.* ~/vamp/\n\t}\n\t\n\thave_beatroot &&\n\t\tprintf '\\033[32mfound the vamp beatroot plugin, nice\\033[0m\\n' ||\n\t\tprintf '\\033[31mWARNING: could not find the vamp beatroot plugin, please install it for optimal results\\033[0m\\n'\n}\n\n\n# not in use because it kinda segfaults, also no windows support\ninstall_soundtouch() {\n\tcd \"$td\"\n\tgitlab_tarball https://gitlab.com/api/v4/projects/soundtouch%2Fsoundtouch/releases\n\t\n\ttar -xvf soundtouch-*\n\trm -- *.tar.gz\n\tcd soundtouch-*\n\t\n\t# https://github.com/jrising/pysoundtouch\n\t./bootstrap\n\t./configure --enable-integer-samples CXXFLAGS=\"-fPIC\" --prefix=\"$HOME/pe/soundtouch\"\n\tmake -j$(nproc || echo 4)\n\tmake install\n\t\n\tCFLAGS=-I$HOME/pe/soundtouch/include/ \\\n\tLDFLAGS=-L$HOME/pe/soundtouch/lib \\\n\t$pybin -m pip install --user git+https://github.com/snowxmas/pysoundtouch.git\n\t\n\tpypath=\"$($pybin -c 'import importlib; print(importlib.util.find_spec(\"soundtouch\").origin)')\"\n\tlibpath=\"$(echo \"$HOME/pe/soundtouch/lib/\")\"\n\tpatchelf --set-rpath \"$libpath\" \"$pypath\"\n\n\techo\n\techo soundtouch successfully installed to the following locations:\n\techo \"  $libpath\"\n\techo \"  $pypath\"\n}\n\n\n[ \"$1\" = keyfinder ] && { install_keyfinder; exit $?; }\n[ \"$1\" = soundtouch ] && { install_soundtouch; exit $?; }\n[ \"$1\" = vamp ] && { install_vamp; exit $?; }\n\necho no args provided, installing keyfinder and vamp\ninstall_keyfinder\ninstall_vamp\n"
  },
  {
    "path": "bin/mtag/media-hash.py",
    "content": "#!/usr/bin/env python\n\nimport re\nimport sys\nimport json\nimport time\nimport base64\nimport hashlib\nimport subprocess as sp\n\ntry:\n    from copyparty.util import fsenc\nexcept:\n\n    def fsenc(p):\n        return p.encode(\"utf-8\")\n\n\n\"\"\"\ndep: ffmpeg\n\"\"\"\n\n\ndef det():\n    # fmt: off\n    cmd = [\n        b\"ffmpeg\",\n        b\"-nostdin\",\n        b\"-hide_banner\",\n        b\"-v\", b\"fatal\",\n        b\"-i\", fsenc(sys.argv[1]),\n        b\"-f\", b\"framemd5\",\n        b\"-\"\n    ]\n    # fmt: on\n\n    p = sp.Popen(cmd, stdout=sp.PIPE)\n    # ps = io.TextIOWrapper(p.stdout, encoding=\"utf-8\")\n    ps = p.stdout\n\n    chans = {}\n    for ln in ps:\n        if ln.startswith(b\"#stream#\"):\n            break\n\n        m = re.match(r\"^#media_type ([0-9]): ([a-zA-Z])\", ln.decode(\"utf-8\"))\n        if m:\n            chans[m.group(1)] = m.group(2)\n\n    hashers = [hashlib.sha512(), hashlib.sha512()]\n    for ln in ps:\n        n = int(ln[:1])\n        v = ln.rsplit(b\",\", 1)[-1].strip()\n        hashers[n].update(v)\n\n    r = {}\n    for k, v in chans.items():\n        dg = hashers[int(k)].digest()[:12]\n        dg = base64.urlsafe_b64encode(dg).decode(\"ascii\")\n        r[v[0].lower() + \"hash\"] = dg\n\n    print(json.dumps(r, indent=4))\n\n\ndef main():\n    try:\n        det()\n    except:\n        pass  # mute\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/mtag/mousepad.py",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport sys\nimport subprocess as sp\n\n\n\"\"\"\nmtp test -- opens a texteditor\n\nusage:\n  -vsrv/v1:v1:r:c,mte=+x1:c,mtp=x1=ad,p,bin/mtag/mousepad.py\n\nexplained:\n  c,mte: list of tags to index in this volume\n  c,mtp: add new tag provider\n     x1: dummy tag to provide\n     ad: dontcare if audio or not\n      p: priority 1 (run after initial tag-scan with ffprobe or mutagen)\n\"\"\"\n\n\ndef main():\n    env = os.environ.copy()\n    env[\"DISPLAY\"] = \":0.0\"\n\n    if False:\n        # open the uploaded file\n        fp = sys.argv[-1]\n    else:\n        # display stdin contents (`oth_tags`)\n        fp = \"/dev/stdin\"\n\n    p = sp.Popen([\"/usr/bin/mousepad\", fp])\n    p.communicate()\n\n\nmain()\n"
  },
  {
    "path": "bin/mtag/rclone-upload.py",
    "content": "#!/usr/bin/env python\n\nimport json\nimport os\nimport subprocess as sp\nimport sys\nimport time\n\ntry:\n    from copyparty.util import fsenc\nexcept:\n\n    def fsenc(p):\n        return p.encode(\"utf-8\")\n\n\n_ = r\"\"\"\nfirst checks the tag \"vidchk\" which must be \"ok\" to continue,\nthen uploads all files to some cloud storage (RCLONE_REMOTE)\nand DELETES THE ORIGINAL FILES if rclone returns 0 (\"success\")\n\ndeps:\n  rclone\n\nusage:\n  -mtp x2=t43200,ay,p2,bin/mtag/rclone-upload.py\n\nexplained:\nt43200: timeout 12h\n    ay: only process files which contain audio (including video with audio)\n    p2: set priority 2 (after vidchk's suggested priority of 1),\n          so the output of vidchk will be passed in here\n\ncomplete usage example as vflags along with vidchk:\n  -vsrv/vidchk:vidchk:r:rw,ed:c,e2dsa,e2ts,mtp=vidchk=t600,p,bin/mtag/vidchk.py:c,mtp=rupload=t43200,ay,p2,bin/mtag/rclone-upload.py:c,mte=+vidchk,rupload\n\nsetup: see https://rclone.org/drive/\n\nif you wanna use this script standalone / separately from copyparty,\neither set CONDITIONAL_UPLOAD False or provide the following stdin:\n  {\"vidchk\":\"ok\"}\n\"\"\"\n\n\nRCLONE_REMOTE = \"notmybox\"\nCONDITIONAL_UPLOAD = True\n\n\ndef main():\n    fp = sys.argv[1]\n    if CONDITIONAL_UPLOAD:\n        zb = sys.stdin.buffer.read()\n        zs = zb.decode(\"utf-8\", \"replace\")\n        md = json.loads(zs)\n\n        chk = md.get(\"vidchk\", None)\n        if chk != \"ok\":\n            print(f\"vidchk={chk}\", file=sys.stderr)\n            sys.exit(1)\n\n    dst = f\"{RCLONE_REMOTE}:\".encode(\"utf-8\")\n    cmd = [b\"rclone\", b\"copy\", b\"--\", fsenc(fp), dst]\n\n    t0 = time.time()\n    try:\n        sp.check_call(cmd)\n    except:\n        print(\"rclone failed\", file=sys.stderr)\n        sys.exit(1)\n\n    print(f\"{time.time() - t0:.1f} sec\")\n    os.unlink(fsenc(fp))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/mtag/res/twitter-unmute.user.js",
    "content": "// ==UserScript==\n// @name         twitter-unmute\n// @namespace    http://ocv.me/\n// @version      0.1\n// @description  memes\n// @author       ed <irc.rizon.net>\n// @match        https://twitter.com/*\n// @icon         https://www.google.com/s2/favicons?domain=twitter.com\n// @grant        GM_addStyle\n// ==/UserScript==\n\nfunction grunnur() {\n    setInterval(function () {\n        //document.querySelector('div[aria-label=\"Unmute\"]').click();\n        document.querySelector('video').muted = false;\n    }, 200);\n}\n\nvar scr = document.createElement('script');\nscr.textContent = '(' + grunnur.toString() + ')();';\n(document.head || document.getElementsByTagName('head')[0]).appendChild(scr);\n"
  },
  {
    "path": "bin/mtag/res/yt-ipr.conf",
    "content": "# example config file to use copyparty as a youtube manifest collector,\n# use with copyparty like:  python copyparty.py -c yt-ipr.conf\n#\n# see docs/example.conf for a better explanation of the syntax, but\n# newlines are block separators, so adding blank lines inside a volume definition is bad\n# (use comments as separators instead)\n\n\n# create user ed, password wark\nu ed:wark\n\n\n# create a volume at /ytm which stores files at ./srv/ytm\n./srv/ytm\n/ytm\n# write-only, but read-write for user ed\nw\nrw ed\n# rescan the volume on startup\nc e2dsa\n# collect tags from all new files since last scan\nc e2ts\n# optionally enable compression to make the files 50% smaller\nc pk\n# only allow uploads which are between 16k and 1m large\nc sz=16k-1m\n# allow up to 10 uploads over 5 minutes from each ip\nc maxn=10,300\n# move uploads into subfolders: YEAR-MONTH / DAY-HOUR / <upload>\nc rotf=%Y-%m/%d-%H\n# delete uploads when they are 24 hours old\nc lifetime=86400\n# add the parser and tell copyparty what tags it can expect from it\nc mtp=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires=bin/mtag/yt-ipr.py\n# decide which tags we want to index and in what order\nc mte=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires\n\n\n# create any other volumes you'd like down here, or merge this with an existing config file\n"
  },
  {
    "path": "bin/mtag/res/yt-ipr.user.js",
    "content": "// ==UserScript==\n// @name    youtube-playerdata-hub\n// @match   https://youtube.com/*\n// @match   https://*.youtube.com/*\n// @version 1.0\n// @grant   GM_addStyle\n// ==/UserScript==\n\nfunction main() {\n    var server = 'https://127.0.0.1:3923/ytm?pw=wark',\n        interval = 60; // sec\n\n    var sent = {};\n    function send(txt, mf_url, desc) {\n        if (sent[mf_url])\n            return;\n\n        fetch(server + '&_=' + Date.now(), { method: \"PUT\", body: txt });\n        console.log('[yt-pdh] yeet %d bytes, %s', txt.length, desc);\n        sent[mf_url] = 1;\n    }\n\n    function collect() {\n        try {\n            var pd = document.querySelector('ytd-watch-flexy');\n            if (!pd)\n                return console.log('[yt-pdh] no video found');\n\n            pd = pd.playerData;\n            var mu = pd.streamingData.dashManifestUrl || pd.streamingData.hlsManifestUrl;\n            if (!mu || !mu.length)\n                return console.log('[yt-pdh] no manifest found');\n\n            var desc = pd.videoDetails.videoId + ', ' + pd.videoDetails.title;\n            send(JSON.stringify(pd), mu, desc);\n        }\n        catch (ex) {\n            console.log(\"[yt-pdh]\", ex);\n        }\n    }\n    setInterval(collect, interval * 1000);\n}\n\nvar scr = document.createElement('script');\nscr.textContent = '(' + main.toString() + ')();';\n(document.head || document.getElementsByTagName('head')[0]).appendChild(scr);\nconsole.log('[yt-pdh] a');\n"
  },
  {
    "path": "bin/mtag/sleep.py",
    "content": "#!/usr/bin/env python\n\nimport time\nimport random\n\nv = random.random() * 6\ntime.sleep(v)\nprint(f\"{v:.2f}\")\n"
  },
  {
    "path": "bin/mtag/very-bad-idea.py",
    "content": "#!/usr/bin/env python3\n\n\"\"\"\nWARNING -- DANGEROUS PLUGIN --\n  if someone is able to upload files to a copyparty which is\n  running this plugin, they can execute malware on your machine\n  so please keep this on a LAN and protect it with a password\n\nhere is a MUCH BETTER ALTERNATIVE (which also works on Windows):\n  https://github.com/steinuil/kameloso\n\n----------------------------------------------------------------------\n\nuse copyparty as a chromecast replacement:\n  * post a URL and it will open in the default browser\n  * upload a file and it will open in the default application\n  * the `key` command simulates keyboard input\n  * the `x` command executes other xdotool commands\n  * the `c` command executes arbitrary unix commands\n\nthe android app makes it a breeze to post pics and links:\n  https://github.com/9001/party-up/releases\n\niOS devices can use the web-UI or the shortcut instead:\n  https://github.com/9001/copyparty#ios-shortcuts\n\nexample copyparty config to use this;\nlets the user \"kevin\" with password \"hunter2\" use this plugin:\n  -a kevin:hunter2 --urlform save,get -v.::w,kevin:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,c0,bin/mtag/very-bad-idea.py\n\nrecommended deps:\n  apt install xdotool libnotify-bin mpv\n  python3 -m pip install --user -U streamlink yt-dlp\n  https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js\n\nand you probably want `twitter-unmute.user.js` from the res folder\n\n\n-----------------------------------------------------------------------\n-- startup script:\n-----------------------------------------------------------------------\n\n#!/bin/bash\nset -e\n\n# create qr code\nip=$(ip r | awk '/^default/{print$(NF-2)}'); echo http://$ip:3923/ | qrencode -o - -s 4 >/dev/shm/cpp-qr.png\n/usr/bin/feh -x /dev/shm/cpp-qr.png &\n\n# reposition and make topmost (with janky raspbian support)\n( sleep 0.5\nxdotool search --name cpp-qr.png windowactivate --sync windowmove 1780 0\nwmctrl -r :ACTIVE: -b toggle,above || true\n\nps aux | grep -E 'sleep[ ]7\\.27' ||\nwhile true; do\n  w=$(xdotool getactivewindow)\n  xdotool search --name cpp-qr.png windowactivate windowraise windowfocus\n  xdotool windowactivate $w\n  xdotool windowfocus $w\n  sleep 7.27 || break\ndone &\nxeyes  # distraction window to prevent ^w from closing the qr-code\n) &\n\n# bail if copyparty is already running\nps aux | grep -E '[3] copy[p]arty' && exit 0\n\n# dumb chrome wrapper to allow autoplay\ncat >/usr/local/bin/chromium-browser <<'EOF'\n#!/bin/bash\nset -e\n/usr/bin/chromium-browser --autoplay-policy=no-user-gesture-required \"$@\"\nEOF\nchmod 755 /usr/local/bin/chromium-browser\n\n# start the server\n# note 1: replace hunter2 with a better password to access the server\n# note 2: replace `-v.::rw` with `-v.::w` to disallow retrieving uploaded stuff\ncd ~/Downloads; python3 copyparty-sfx.py -a kevin:hunter2 --urlform save,get -v.::rw,kevin:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,very-bad-idea.py\n\n\"\"\"\n\n\nimport os\nimport sys\nimport time\nimport shutil\nimport subprocess as sp\nfrom urllib.parse import unquote_to_bytes as unquote\nfrom urllib.parse import quote\n\nhave_mpv = shutil.which(\"mpv\")\nhave_vlc = shutil.which(\"vlc\")\n\n\ndef main():\n    if len(sys.argv) > 2 and sys.argv[1] == \"x\":\n        # invoked on commandline for testing;\n        # python3 very-bad-idea.py x msg=https://youtu.be/dQw4w9WgXcQ\n        txt = \" \".join(sys.argv[2:])\n        txt = quote(txt.replace(\" \", \"+\"))\n        return open_post(txt.encode(\"utf-8\"))\n\n    fp = os.path.abspath(sys.argv[1])\n    with open(fp, \"rb\") as f:\n        txt = f.read(4096)\n\n    if txt.startswith(b\"msg=\"):\n        open_post(txt)\n    else:\n        open_url(fp)\n\n\ndef open_post(txt):\n    txt = unquote(txt.replace(b\"+\", b\" \")).decode(\"utf-8\")[4:]\n    try:\n        k, v = txt.split(\" \", 1)\n    except:\n        return open_url(txt)\n\n    if k == \"key\":\n        sp.call([\"xdotool\", \"key\"] + v.split(\" \"))\n    elif k == \"x\":\n        sp.call([\"xdotool\"] + v.split(\" \"))\n    elif k == \"c\":\n        env = os.environ.copy()\n        while \" \" in v:\n            v1, v2 = v.split(\" \", 1)\n            if \"=\" not in v1:\n                break\n\n            ek, ev = v1.split(\"=\", 1)\n            env[ek] = ev\n            v = v2\n\n        sp.call(v.split(\" \"), env=env)\n    else:\n        open_url(txt)\n\n\ndef open_url(txt):\n    ext = txt.rsplit(\".\")[-1].lower()\n    sp.call([\"notify-send\", \"--\", txt])\n    if ext not in [\"jpg\", \"jpeg\", \"png\", \"gif\", \"webp\"]:\n        # sp.call([\"wmctrl\", \"-c\", \":ACTIVE:\"])  # closes the active window correctly\n        sp.call([\"killall\", \"vlc\"])\n        sp.call([\"killall\", \"mpv\"])\n        sp.call([\"killall\", \"feh\"])\n        time.sleep(0.5)\n        for _ in range(20):\n            sp.call([\"xdotool\", \"key\", \"ctrl+w\"])  # closes the open tab correctly\n    # else:\n    #    sp.call([\"xdotool\", \"getactivewindow\", \"windowminimize\"])  # minimizes the focused windo\n\n    # mpv is probably smart enough to use streamlink automatically\n    if try_mpv(txt):\n        print(\"mpv got it\")\n        return\n\n    # or maybe streamlink would be a good choice to open this\n    if try_streamlink(txt):\n        print(\"streamlink got it\")\n        return\n\n    # nope,\n    # close any error messages:\n    sp.call([\"xdotool\", \"search\", \"--name\", \"Error\", \"windowclose\"])\n    # sp.call([\"xdotool\", \"key\", \"ctrl+alt+d\"])  # doesnt work at all\n    # sp.call([\"xdotool\", \"keydown\", \"--delay\", \"100\", \"ctrl+alt+d\"])\n    # sp.call([\"xdotool\", \"keyup\", \"ctrl+alt+d\"])\n    sp.call([\"xdg-open\", txt])\n\n\ndef try_mpv(url):\n    t0 = time.time()\n    try:\n        print(\"trying mpv...\")\n        sp.check_call([\"mpv\", \"--fs\", url])\n        return True\n    except:\n        # if it ran for 15 sec it probably succeeded and terminated\n        t = time.time()\n        return t - t0 > 15\n\n\ndef try_streamlink(url):\n    t0 = time.time()\n    try:\n        import streamlink\n\n        print(\"trying streamlink...\")\n        streamlink.Streamlink().resolve_url(url)\n\n        if have_mpv:\n            args = \"-m streamlink -p mpv -a --fs\"\n        else:\n            args = \"-m streamlink\"\n\n        cmd = [sys.executable] + args.split() + [url, \"best\"]\n        t0 = time.time()\n        sp.check_call(cmd)\n        return True\n    except:\n        # if it ran for 10 sec it probably succeeded and terminated\n        t = time.time()\n        return t - t0 > 10\n\n\nmain()\n"
  },
  {
    "path": "bin/mtag/vidchk.py",
    "content": "#!/usr/bin/env python3\n\nimport json\nimport re\nimport os\nimport sys\nimport subprocess as sp\n\ntry:\n    from copyparty.util import fsenc\nexcept:\n\n    def fsenc(p):\n        return p.encode(\"utf-8\")\n\n\n_ = r\"\"\"\ninspects video files for errors and such\nplus stores a bunch of metadata to filename.ff.json\n\nusage:\n  -mtp vidchk=t600,ay,p,bin/mtag/vidchk.py\n\nexplained:\nt600: timeout 10min\n  ay: only process files which contain audio (including video with audio)\n   p: set priority 1 (lowest priority after initial ffprobe/mutagen for base tags),\n       makes copyparty feed base tags into this script as json\n\nif you wanna use this script standalone / separately from copyparty,\nprovide the video resolution on stdin as json:  {\"res\":\"1920x1080\"}\n\"\"\"\n\n\nFAST = True  # parse entire file at container level\n# FAST = False  # fully decode audio and video streams\n\n\n# warnings to ignore\nharmless = re.compile(\n    r\"Unsupported codec with id |Could not find codec parameters.*Attachment:|analyzeduration\"\n    + r\"|timescale not set\"\n)\n\n\ndef wfilter(lines):\n    return [x for x in lines if x.strip() and not harmless.search(x)]\n\n\ndef errchk(so, se, rc, dbg):\n    if dbg:\n        with open(dbg, \"wb\") as f:\n            f.write(b\"so:\\n\" + so + b\"\\nse:\\n\" + se + b\"\\n\")\n\n    if rc:\n        err = (so + se).decode(\"utf-8\", \"replace\").split(\"\\n\", 1)\n        err = wfilter(err) or err\n        return f\"ERROR {rc}: {err[0]}\"\n\n    if se:\n        err = se.decode(\"utf-8\", \"replace\").split(\"\\n\", 1)\n        err = wfilter(err)\n        if err:\n            return f\"Warning: {err[0]}\"\n\n    return None\n\n\ndef main():\n    fp = sys.argv[1]\n    zb = sys.stdin.buffer.read()\n    zs = zb.decode(\"utf-8\", \"replace\")\n    md = json.loads(zs)\n\n    fdir = os.path.dirname(os.path.realpath(fp))\n    flag = os.path.join(fdir, \".processed\")\n    if os.path.exists(flag):\n        return \"already processed\"\n\n    try:\n        w, h = [int(x) for x in md[\"res\"].split(\"x\")]\n        if not w + h:\n            raise Exception()\n    except:\n        return \"could not determine resolution\"\n\n    # grab streams/format metadata + 2 seconds of frames at the start and end\n    zs = \"ffprobe -hide_banner -v warning -of json -show_streams -show_format -show_packets -show_data_hash crc32 -read_intervals %+2,999999%+2\"\n    cmd = zs.encode(\"ascii\").split(b\" \") + [fsenc(fp)]\n    p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE)\n    so, se = p.communicate()\n\n    # spaces to tabs, drops filesize from 69k to 48k\n    so = b\"\\n\".join(\n        [\n            b\"\\t\" * int((len(x) - len(x.lstrip())) / 4) + x.lstrip()\n            for x in (so or b\"\").split(b\"\\n\")\n        ]\n    )\n    with open(fsenc(f\"{fp}.ff.json\"), \"wb\") as f:\n        f.write(so)\n\n    err = errchk(so, se, p.returncode, f\"{fp}.vidchk\")\n    if err:\n        return err\n\n    if max(w, h) < 1280 and min(w, h) < 720:\n        return \"resolution too small\"\n\n    zs = (\n        \"ffmpeg -y -hide_banner -nostdin -v warning\"\n        + \" -err_detect +crccheck+bitstream+buffer+careful+compliant+aggressive+explode\"\n        + \" -xerror -i\"\n    )\n\n    cmd = zs.encode(\"ascii\").split(b\" \") + [fsenc(fp)]\n\n    if FAST:\n        zs = \"-c copy -f null -\"\n    else:\n        zs = \"-vcodec rawvideo -acodec pcm_s16le -f null -\"\n\n    cmd += zs.encode(\"ascii\").split(b\" \")\n\n    p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE)\n    so, se = p.communicate()\n    return errchk(so, se, p.returncode, f\"{fp}.vidchk\")\n\n\nif __name__ == \"__main__\":\n    print(main() or \"ok\")\n"
  },
  {
    "path": "bin/mtag/wget.py",
    "content": "#!/usr/bin/env python3\n\n\"\"\"\nDEPRECATED -- replaced by event hooks;\nhttps://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/wget.py\n\n---\n\nuse copyparty as a file downloader by POSTing URLs as\napplication/x-www-form-urlencoded (for example using the\nmessage/pager function on the website)\n\nexample copyparty config to use this:\n  --urlform save,get -vsrv/wget:wget:rwmd,ed:c,e2ts,mtp=title=ebin,t300,ad,bin/mtag/wget.py\n\nexplained:\n  for realpath srv/wget (served at /wget) with read-write-modify-delete for ed,\n  enable file analysis on upload (e2ts),\n  use mtp plugin \"bin/mtag/wget.py\" to provide metadata tag \"title\",\n  do this on all uploads with the file extension \"bin\",\n  t300 = 300 seconds timeout for each dwonload,\n  ad = parse file regardless if FFmpeg thinks it is audio or not\n\nPS: this requires e2ts to be functional,\n  meaning you need to do at least one of these:\n   * apt install ffmpeg\n   * pip3 install mutagen\n\"\"\"\n\n\nimport os\nimport sys\nimport subprocess as sp\nfrom urllib.parse import unquote_to_bytes as unquote\n\n\ndef main():\n    fp = os.path.abspath(sys.argv[1])\n    fdir = os.path.dirname(fp)\n    fname = os.path.basename(fp)\n    if not fname.startswith(\"put-\") or not fname.endswith(\".bin\"):\n        raise Exception(\"not a post file\")\n\n    buf = b\"\"\n    with open(fp, \"rb\") as f:\n        while True:\n            b = f.read(4096)\n            buf += b\n            if len(buf) > 4096:\n                raise Exception(\"too big\")\n\n            if not b:\n                break\n\n    if not buf:\n        raise Exception(\"file is empty\")\n\n    buf = unquote(buf.replace(b\"+\", b\" \"))\n    url = buf.decode(\"utf-8\")\n\n    if not url.startswith(\"msg=\"):\n        raise Exception(\"does not start with msg=\")\n\n    url = url[4:]\n    if \"://\" not in url:\n        url = \"https://\" + url\n\n    proto = url.split(\"://\")[0].lower()\n    if proto not in (\"http\", \"https\", \"ftp\", \"ftps\"):\n        raise Exception(\"bad proto {}\".format(proto))\n\n    os.chdir(fdir)\n\n    name = url.split(\"?\")[0].split(\"/\")[-1]\n    tfn = \"-- DOWNLOADING \" + name\n    open(tfn, \"wb\").close()\n\n    cmd = [\"wget\", \"--trust-server-names\", \"--\", url]\n\n    try:\n        sp.check_call(cmd)\n\n        # OPTIONAL:\n        #   on success, delete the .bin file which contains the URL\n        os.unlink(fp)\n    except:\n        open(\"-- FAILED TO DOWNLOAD \" + name, \"wb\").close()\n\n    os.unlink(tfn)\n    print(url)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/mtag/yt-ipr.py",
    "content": "#!/usr/bin/env python\n\nimport re\nimport os\nimport sys\nimport gzip\nimport json\nimport base64\nimport string\nimport urllib.request\nfrom datetime import datetime\n\n\"\"\"\nyoutube initial player response\n\nit's probably best to use this through a config file; see res/yt-ipr.conf\n\nbut if you want to use plain arguments instead then:\n  -v srv/ytm:ytm:w:rw,ed\n       :c,e2ts,e2dsa\n       :c,sz=16k-1m:c,maxn=10,300:c,rotf=%Y-%m/%d-%H\n       :c,mtp=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires=bin/mtag/yt-ipr.py\n       :c,mte=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires\n\nsee res/yt-ipr.user.js for the example userscript to go with this\n\"\"\"\n\n\ndef main():\n    try:\n        with gzip.open(sys.argv[1], \"rt\", encoding=\"utf-8\", errors=\"replace\") as f:\n            txt = f.read()\n    except:\n        with open(sys.argv[1], \"r\", encoding=\"utf-8\", errors=\"replace\") as f:\n            txt = f.read()\n\n    txt = \"{\" + txt.split(\"{\", 1)[1]\n\n    try:\n        pd = json.loads(txt)\n    except json.decoder.JSONDecodeError as ex:\n        pd = json.loads(txt[: ex.pos])\n\n    # print(json.dumps(pd, indent=2))\n\n    if \"videoDetails\" in pd:\n        parse_youtube(pd)\n    else:\n        parse_freg(pd)\n\n\ndef get_expiration(url):\n    et = re.search(r\"[?&]expire=([0-9]+)\", url).group(1)\n    et = datetime.utcfromtimestamp(int(et))\n    return et.strftime(\"%Y-%m-%d, %H:%M\")\n\n\ndef parse_youtube(pd):\n    vd = pd[\"videoDetails\"]\n    sd = pd[\"streamingData\"]\n\n    et = sd[\"adaptiveFormats\"][0][\"url\"]\n    et = get_expiration(et)\n\n    mf = []\n    if \"dashManifestUrl\" in sd:\n        mf.append(\"dash\")\n    if \"hlsManifestUrl\" in sd:\n        mf.append(\"hls\")\n\n    r = {\n        \"yt-id\": vd[\"videoId\"],\n        \"yt-title\": vd[\"title\"],\n        \"yt-author\": vd[\"author\"],\n        \"yt-channel\": vd[\"channelId\"],\n        \"yt-views\": vd[\"viewCount\"],\n        \"yt-private\": vd[\"isPrivate\"],\n        # \"yt-expires\": sd[\"expiresInSeconds\"],\n        \"yt-manifest\": \",\".join(mf),\n        \"yt-expires\": et,\n    }\n    print(json.dumps(r))\n\n    freg_conv(pd)\n\n\ndef parse_freg(pd):\n    md = pd[\"metadata\"]\n    r = {\n        \"yt-id\": md[\"id\"],\n        \"yt-title\": md[\"title\"],\n        \"yt-author\": md[\"channelName\"],\n        \"yt-channel\": md[\"channelURL\"].strip(\"/\").split(\"/\")[-1],\n        \"yt-expires\": get_expiration(list(pd[\"video\"].values())[0]),\n    }\n    print(json.dumps(r))\n\n\ndef freg_conv(pd):\n    # based on getURLs.js v1.5 (2021-08-07)\n    # fmt: off\n    priority = {\n        \"video\": [\n            337, 315, 266, 138,  # 2160p60\n            313, 336,  # 2160p\n            308,  # 1440p60\n            271, 264,  # 1440p\n            335, 303, 299,  # 1080p60\n            248, 169, 137,  # 1080p\n            334, 302, 298,  # 720p60\n            247, 136  # 720p\n        ],\n        \"audio\": [\n            251, 141, 171, 140, 250, 249, 139\n        ]\n    }\n\n    vid_id = pd[\"videoDetails\"][\"videoId\"]\n    chan_id = pd[\"videoDetails\"][\"channelId\"]\n\n    try:\n        thumb_url = pd[\"microformat\"][\"playerMicroformatRenderer\"][\"thumbnail\"][\"thumbnails\"][0][\"url\"]\n        start_ts = pd[\"microformat\"][\"playerMicroformatRenderer\"][\"liveBroadcastDetails\"][\"startTimestamp\"]\n    except:\n        thumb_url = f\"https://img.youtube.com/vi/{vid_id}/maxresdefault.jpg\"\n        start_ts = \"\"\n\n    # fmt: on\n\n    metadata = {\n        \"title\": pd[\"videoDetails\"][\"title\"],\n        \"id\": vid_id,\n        \"channelName\": pd[\"videoDetails\"][\"author\"],\n        \"channelURL\": \"https://www.youtube.com/channel/\" + chan_id,\n        \"description\": pd[\"videoDetails\"][\"shortDescription\"],\n        \"thumbnailUrl\": thumb_url,\n        \"startTimestamp\": start_ts,\n    }\n\n    if [x for x in vid_id if x not in string.ascii_letters + string.digits + \"_-\"]:\n        print(f\"malicious json\", file=sys.stderr)\n        return\n\n    basepath = os.path.dirname(sys.argv[1])\n\n    thumb_fn = f\"{basepath}/{vid_id}.jpg\"\n    tmp_fn = f\"{thumb_fn}.{os.getpid()}\"\n    if not os.path.exists(thumb_fn) and (\n        thumb_url.startswith(\"https://img.youtube.com/vi/\")\n        or thumb_url.startswith(\"https://i.ytimg.com/vi/\")\n    ):\n        try:\n            with urllib.request.urlopen(thumb_url) as fi:\n                with open(tmp_fn, \"wb\") as fo:\n                    fo.write(fi.read())\n\n            os.rename(tmp_fn, thumb_fn)\n        except:\n            if os.path.exists(tmp_fn):\n                os.unlink(tmp_fn)\n\n    try:\n        with open(thumb_fn, \"rb\") as f:\n            thumb = base64.b64encode(f.read()).decode(\"ascii\")\n    except:\n        thumb = \"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=\"\n\n    metadata[\"thumbnail\"] = \"data:image/jpeg;base64,\" + thumb\n\n    ret = {\n        \"metadata\": metadata,\n        \"version\": \"1.5\",\n        \"createTime\": datetime.utcnow().strftime(\"%Y-%m-%dT%H:%M:%SZ\"),\n    }\n\n    for stream, itags in priority.items():\n        for itag in itags:\n            url = None\n            for afmt in pd[\"streamingData\"][\"adaptiveFormats\"]:\n                if itag == afmt[\"itag\"]:\n                    url = afmt[\"url\"]\n                    break\n\n            if url:\n                ret[stream] = {itag: url}\n                break\n\n    fn = f\"{basepath}/{vid_id}.urls.json\"\n    with open(fn, \"w\", encoding=\"utf-8\", errors=\"replace\") as f:\n        f.write(json.dumps(ret, indent=4))\n\n\nif __name__ == \"__main__\":\n    try:\n        main()\n    except:\n        # raise\n        pass\n"
  },
  {
    "path": "bin/partyfuse-streaming.py",
    "content": "#!/usr/bin/env python3\nfrom __future__ import print_function, unicode_literals\n\n\"\"\"partyfuse-streaming: remote copyparty as a local filesystem\"\"\"\n__author__ = \"ed <copyparty@ocv.me>\"\n__copyright__ = 2020\n__license__ = \"MIT\"\n__url__ = \"https://github.com/9001/copyparty/\"\n\n\n\"\"\"\nmount a copyparty server (local or remote) as a filesystem\n\nusage:\n  python partyfuse-streaming.py http://192.168.1.69:3923/  ./music\n\ndependencies:\n  python3 -m pip install --user fusepy\n  + on Linux: sudo apk add fuse\n  + on Macos: https://osxfuse.github.io/\n  + on Windows: https://github.com/billziss-gh/winfsp/releases/latest\n\nthis was a mistake:\n  fork of partyfuse.py with a streaming cache rather than readahead,\n  thought this was gonna be way faster (and it kind of is)\n  except the overhead of reopening connections on trunc totally kills it\n\"\"\"\n\n\nimport re\nimport os\nimport sys\nimport time\nimport stat\nimport errno\nimport struct\nimport codecs\nimport builtins\nimport platform\nimport argparse\nimport threading\nimport traceback\nimport http.client  # py2: httplib\nimport urllib.parse\nimport calendar\nfrom datetime import datetime\nfrom urllib.parse import quote_from_bytes as quote\nfrom urllib.parse import unquote_to_bytes as unquote\n\nWINDOWS = sys.platform == \"win32\"\nMACOS = platform.system() == \"Darwin\"\ninfo = log = dbg = None\n\n\ntry:\n    from fuse import FUSE, FuseOSError, Operations\nexcept:\n    if WINDOWS:\n        libfuse = \"install https://github.com/billziss-gh/winfsp/releases/latest\"\n    elif MACOS:\n        libfuse = \"install https://osxfuse.github.io/\"\n    else:\n        libfuse = \"apt install libfuse\\n    modprobe fuse\"\n\n    m = \"\"\"\\033[33m\n  could not import fuse; these may help:\n    {} -m pip install --user fusepy\n    {}\n\\033[0m\"\"\"\n    print(m.format(sys.executable, libfuse))\n    raise\n\n\ndef print(*args, **kwargs):\n    try:\n        builtins.print(*list(args), **kwargs)\n    except:\n        builtins.print(termsafe(\" \".join(str(x) for x in args)), **kwargs)\n\n\ndef termsafe(txt):\n    try:\n        return txt.encode(sys.stdout.encoding, \"backslashreplace\").decode(\n            sys.stdout.encoding\n        )\n    except:\n        return txt.encode(sys.stdout.encoding, \"replace\").decode(sys.stdout.encoding)\n\n\ndef threadless_log(msg):\n    print(msg + \"\\n\", end=\"\")\n\n\ndef boring_log(msg):\n    msg = \"\\033[36m{:012x}\\033[0m {}\\n\".format(threading.current_thread().ident, msg)\n    print(msg[4:], end=\"\")\n\n\ndef rice_tid():\n    tid = threading.current_thread().ident\n    c = struct.unpack(b\"B\" * 5, struct.pack(b\">Q\", tid)[-5:])\n    return \"\".join(\"\\033[1;37;48;5;{}m{:02x}\".format(x, x) for x in c) + \"\\033[0m\"\n\n\ndef fancy_log(msg):\n    print(\"{:6.3f} {} {}\\n\".format(time.time() % 60, rice_tid(), msg), end=\"\")\n\n\ndef null_log(msg):\n    pass\n\n\ndef hexler(binary):\n    return binary.replace(\"\\r\", \"\\\\r\").replace(\"\\n\", \"\\\\n\")\n    return \" \".join([\"{}\\033[36m{:02x}\\033[0m\".format(b, ord(b)) for b in binary])\n    return \" \".join(map(lambda b: format(ord(b), \"02x\"), binary))\n\n\ndef register_wtf8():\n    def wtf8_enc(text):\n        return str(text).encode(\"utf-8\", \"surrogateescape\"), len(text)\n\n    def wtf8_dec(binary):\n        return bytes(binary).decode(\"utf-8\", \"surrogateescape\"), len(binary)\n\n    def wtf8_search(encoding_name):\n        return codecs.CodecInfo(wtf8_enc, wtf8_dec, name=\"wtf-8\")\n\n    codecs.register(wtf8_search)\n\n\nbad_good = {}\ngood_bad = {}\n\n\ndef enwin(txt):\n    return \"\".join([bad_good.get(x, x) for x in txt])\n\n    for bad, good in bad_good.items():\n        txt = txt.replace(bad, good)\n\n    return txt\n\n\ndef dewin(txt):\n    return \"\".join([good_bad.get(x, x) for x in txt])\n\n    for bad, good in bad_good.items():\n        txt = txt.replace(good, bad)\n\n    return txt\n\n\nclass RecentLog(object):\n    def __init__(self):\n        self.mtx = threading.Lock()\n        self.f = None  # open(\"partyfuse.log\", \"wb\")\n        self.q = []\n\n        thr = threading.Thread(target=self.printer)\n        thr.daemon = True\n        thr.start()\n\n    def put(self, msg):\n        msg = \"{:6.3f} {} {}\\n\".format(time.time() % 60, rice_tid(), msg)\n        if self.f:\n            fmsg = \" \".join([datetime.utcnow().strftime(\"%H%M%S.%f\"), str(msg)])\n            self.f.write(fmsg.encode(\"utf-8\"))\n\n        with self.mtx:\n            self.q.append(msg)\n            if len(self.q) > 200:\n                self.q = self.q[-50:]\n\n    def printer(self):\n        while True:\n            time.sleep(0.05)\n            with self.mtx:\n                q = self.q\n                if not q:\n                    continue\n\n                self.q = []\n\n            print(\"\".join(q), end=\"\")\n\n\n# [windows/cmd/cpy3]  python dev\\copyparty\\bin\\partyfuse.py q: http://192.168.1.159:1234/\n# [windows/cmd/msys2] C:\\msys64\\mingw64\\bin\\python3 dev\\copyparty\\bin\\partyfuse.py q: http://192.168.1.159:1234/\n# [windows/mty/msys2] /mingw64/bin/python3 /c/Users/ed/dev/copyparty/bin/partyfuse.py q: http://192.168.1.159:1234/\n#\n# [windows] find /q/music/albums/Phant*24bit -printf '%s %p\\n' | sort -n | tail -n 8 | sed -r 's/^[0-9]+ //' | while IFS= read -r x; do dd if=\"$x\" of=/dev/null bs=4k count=8192 & done\n# [alpine]  ll t; for x in t/2020_0724_16{2,3}*; do dd if=\"$x\" of=/dev/null bs=4k count=10240 & done\n#\n#  72.4983 windows mintty msys2 fancy_log\n# 219.5781 windows cmd msys2 fancy_log\n# nope.avi windows cmd cpy3 fancy_log\n#   9.8817 windows mintty msys2 RecentLog 200 50 0.1\n#  10.2241 windows cmd cpy3 RecentLog 200 50 0.1\n#   9.8494 windows cmd msys2 RecentLog 200 50 0.1\n#   7.8061 windows mintty msys2 fancy_log <info-only>\n#   7.9961 windows mintty msys2 RecentLog <info-only>\n#   4.2603 alpine xfce4 cpy3 RecentLog\n#   4.1538 alpine xfce4 cpy3 fancy_log\n#   3.1742 alpine urxvt cpy3 fancy_log\n\n\ndef html_dec(txt):\n    return (\n        txt.replace(\"&lt;\", \"<\")\n        .replace(\"&gt;\", \">\")\n        .replace(\"&quot;\", '\"')\n        .replace(\"&#13;\", \"\\r\")\n        .replace(\"&#10;\", \"\\n\")\n        .replace(\"&amp;\", \"&\")\n    )\n\n\nclass CacheNode(object):\n    def __init__(self, tag, data):\n        self.tag = tag\n        self.data = data\n        self.ts = time.time()\n\n\nclass Gateway(object):\n    def __init__(self, ar):\n        self.base_url = ar.base_url\n        self.password = ar.a\n\n        ui = urllib.parse.urlparse(self.base_url)\n        self.web_root = ui.path.strip(\"/\")\n        try:\n            self.web_host, self.web_port = ui.netloc.split(\":\")\n            self.web_port = int(self.web_port)\n        except:\n            self.web_host = ui.netloc\n            if ui.scheme == \"http\":\n                self.web_port = 80\n            elif ui.scheme == \"https\":\n                self.web_port = 443\n            else:\n                raise Exception(\"bad url?\")\n\n        self.ssl_context = None\n        self.use_tls = ui.scheme.lower() == \"https\"\n        if self.use_tls:\n            import ssl\n\n            if ar.td:\n                self.ssl_context = ssl._create_unverified_context()\n            elif ar.te:\n                self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS)\n                self.ssl_context.load_verify_locations(ar.te)\n\n        self.conns = {}\n        if WINDOWS:\n            self.mtx = threading.Lock()\n            self.getconn = self.getconn_winfsp\n        else:\n            self.getconn = self.getconn_unix\n\n    def quotep(self, path):\n        path = path.encode(\"wtf-8\")\n        return quote(path, safe=\"/\")\n\n    def newconn(self):\n        info(\"\\033[1;37;44mnew conn, {}\\033[0m\".format(len(self.conns) + 1))\n\n        args = {}\n        if not self.use_tls:\n            C = http.client.HTTPConnection\n        else:\n            C = http.client.HTTPSConnection\n            if self.ssl_context:\n                args = {\"context\": self.ssl_context}\n\n        conn = C(self.web_host, self.web_port, timeout=260, **args)\n        conn.rx_path = None\n        conn.rx_ofs = None\n        conn.rx = None\n        conn.cnode = None\n        return conn\n\n    def getconn_unix(self, key=None):\n        tid = threading.current_thread().ident\n        try:\n            return self.conns[tid]\n        except:\n            conn = self.newconn()\n            self.conns[tid] = conn\n            return conn\n\n    def getconn_winfsp(self, key=\"x\"):\n        # hey wanna hear something fun\n        # winfsp uses a random thread for each read request\n        rm = None\n        ret = None\n        with self.mtx:\n            if dbg != null_log:\n                m = [\"getconn [{}]\".format(key)]\n                for k, v in sorted(self.conns.items()):\n                    vpath = v[2].rx_path\n                    c = 4 if not vpath else 2 if vpath in key else 3\n                    m.append(\"\\033[3{}m  [{}] [{}]\\033[0m\".format(c, v[0], k))\n                dbg(\"\\n\".join(m))\n\n            try:\n                ret = self.conns[key][2]\n                del self.conns[key]\n            except:\n                # pprint.pprint(self.conns.items())\n                for k, v in sorted(self.conns.items()):\n                    if not v[2].rx_path:\n                        del self.conns[k]\n                        ret = v[2]\n                        break\n\n            if not ret and len(self.conns) >= 8:\n                rm = sorted(self.conns.values())[0]\n                dbg(\"\\033[1;37;41mdropping \" + repr(rm) + \"\\033[0m\")\n\n        if rm:\n            self.closeconn(rm[2])\n\n        return ret or self.newconn()\n\n    def putconn_winfsp(self, c, path):\n        with self.mtx:\n            self.conns[\"{} :{}\".format(path, c.rx_ofs)] = [time.time(), id(c), c]\n\n    def closeconn(self, c):\n        try:\n            c.rx_path = None\n            c.cnode = None\n            c.close()\n            if not WINDOWS:\n                del self.conns[c]\n                return\n\n            with self.mtx:\n                for k, v in self.conns:\n                    if c == v[2]:\n                        del self.conns[k]\n                        break\n        except:\n            pass\n\n    def sendreq(self, meth, path, headers, **kwargs):\n        if self.password:\n            headers[\"Cookie\"] = \"=\".join([\"cppwd\", self.password])\n\n        c = self.getconn()\n        try:\n            if c.rx_path:\n                raise Exception()\n\n            c.request(meth, path, headers=headers, **kwargs)\n            c.rx = c.getresponse()\n            return c\n        except:\n            tid = threading.current_thread().ident\n            dbg(\n                \"\\033[1;37;44mbad conn {:x}\\n  {} {}\\n  {}\\033[0m\".format(\n                    tid, meth, path, c.rx_path if c else \"(null)\"\n                )\n            )\n\n        self.closeconn(c)\n        c = self.getconn()\n        try:\n            c.request(meth, path, headers=headers, **kwargs)\n            c.rx = c.getresponse()\n            return c\n        except:\n            info(\"http connection failed:\\n\" + traceback.format_exc())\n            if self.use_tls and not self.ssl_context:\n                import ssl\n\n                cert = ssl.get_server_certificate((self.web_host, self.web_port))\n                info(\"server certificate probably not trusted:\\n\" + cert)\n\n            raise\n\n    def listdir(self, path):\n        if bad_good:\n            path = dewin(path)\n\n        web_path = self.quotep(\"/\" + \"/\".join([self.web_root, path])) + \"?dots\"\n        c = self.sendreq(\"GET\", web_path, {})\n        if c.rx.status != 200:\n            self.closeconn(c)\n            log(\n                \"http error {} reading dir {} in {}\".format(\n                    c.rx.status, web_path, rice_tid()\n                )\n            )\n            raise FuseOSError(errno.ENOENT)\n\n        if not c.rx.getheader(\"Content-Type\", \"\").startswith(\"text/html\"):\n            log(\"listdir on file: {}\".format(path))\n            raise FuseOSError(errno.ENOENT)\n\n        try:\n            ret = self.parse_html(c.rx)\n            if WINDOWS:\n                c.rx_ofs = 0\n                self.putconn_winfsp(c, path)\n            return ret\n        except:\n            info(repr(path) + \"\\n\" + traceback.format_exc())\n            raise\n\n    def download_file_range(self, path, ofs1, ofs2):\n        c = self.getconn(\"{} :{}\".format(path, ofs1))\n        if path == c.rx_path and ofs1 == c.rx_ofs:\n            try:\n                ret = c.rx.read(ofs2 - ofs1)\n                c.rx_ofs += len(ret)\n                c.rx_rem -= len(ret)\n                if not c.rx_rem:\n                    c.rx_path = None\n                if WINDOWS:\n                    self.putconn_winfsp(c, path)\n                return ret, c\n            except:\n                log(\"download resume failed\")\n\n        if c.rx_path:\n            log(\"replacing download\")\n            self.closeconn(c)\n\n        if bad_good:\n            path = dewin(path)\n\n        web_path = self.quotep(\"/\" + \"/\".join([self.web_root, path])) + \"?raw\"\n        hdr_range = \"bytes={}-\".format(ofs1)\n        info(\n            \"DL {:4.0f}K\\033[36m{:>9}-{:<9}\\033[0m{}\".format(\n                (ofs2 - ofs1) / 1024.0, ofs1, ofs2 - 1, hexler(path)\n            )\n        )\n\n        c = self.sendreq(\"GET\", web_path, {\"Range\": hdr_range})\n        if c.rx.status != http.client.PARTIAL_CONTENT:\n            self.closeconn(c)\n            raise Exception(\n                \"http error {} reading file {} range {} in {}\".format(\n                    c.rx.status, web_path, hdr_range, rice_tid()\n                )\n            )\n\n        ret = c.rx.read(ofs2 - ofs1)\n        c.rx_rem = int(c.rx.getheader(\"Content-Length\")) - len(ret)\n        if c.rx_rem:\n            c.rx_ofs = ofs1 + len(ret)\n            c.rx_path = path\n        if WINDOWS:\n            self.putconn_winfsp(c, path)\n        return ret, c\n\n    def parse_html(self, datasrc):\n        ret = []\n        remainder = b\"\"\n        ptn = re.compile(\n            r'^<tr><td>(-|DIR|<a [^<]+</a>)</td><td><a[^>]* href=\"([^\"]+)\"[^>]*>([^<]+)</a></td><td>([^<]+)</td><td>[^<]+</td><td>([^<]+)</td></tr>$'\n        )\n\n        while True:\n            buf = remainder + datasrc.read(4096)\n            # print('[{}]'.format(buf.decode('utf-8')))\n            if not buf:\n                break\n\n            remainder = b\"\"\n            endpos = buf.rfind(b\"\\n\")\n            if endpos >= 0:\n                remainder = buf[endpos + 1 :]\n                buf = buf[:endpos]\n\n            lines = buf.decode(\"utf-8\").split(\"\\n\")\n            for line in lines:\n                m = ptn.match(line)\n                if not m:\n                    # print(line)\n                    continue\n\n                ftype, furl, fname, fsize, fdate = m.groups()\n                fname = furl.rstrip(\"/\").split(\"/\")[-1]\n                fname = unquote(fname)\n                fname = fname.decode(\"wtf-8\")\n                if bad_good:\n                    fname = enwin(fname)\n\n                sz = 1\n                ts = 60 * 60 * 24 * 2\n                try:\n                    sz = int(fsize)\n                    ts = calendar.timegm(time.strptime(fdate, \"%Y-%m-%d %H:%M:%S\"))\n                except:\n                    info(\"bad HTML or OS [{}] [{}]\".format(fdate, fsize))\n                    # python cannot strptime(1959-01-01) on windows\n\n                if ftype != \"DIR\":\n                    ret.append([fname, self.stat_file(ts, sz), 0])\n                else:\n                    ret.append([fname, self.stat_dir(ts, sz), 0])\n\n        return ret\n\n    def stat_dir(self, ts, sz=4096):\n        return {\n            \"st_mode\": stat.S_IFDIR | 0o555,\n            \"st_uid\": 1000,\n            \"st_gid\": 1000,\n            \"st_size\": sz,\n            \"st_atime\": ts,\n            \"st_mtime\": ts,\n            \"st_ctime\": ts,\n            \"st_blocks\": int((sz + 511) / 512),\n        }\n\n    def stat_file(self, ts, sz):\n        return {\n            \"st_mode\": stat.S_IFREG | 0o444,\n            \"st_uid\": 1000,\n            \"st_gid\": 1000,\n            \"st_size\": sz,\n            \"st_atime\": ts,\n            \"st_mtime\": ts,\n            \"st_ctime\": ts,\n            \"st_blocks\": int((sz + 511) / 512),\n        }\n\n\nclass CPPF(Operations):\n    def __init__(self, ar):\n        self.gw = Gateway(ar)\n        self.junk_fh_ctr = 3\n        self.n_dircache = ar.cd\n        self.n_filecache = ar.cf\n\n        self.dircache = []\n        self.dircache_mtx = threading.Lock()\n\n        self.filecache = []\n        self.filecache_mtx = threading.Lock()\n\n        info(\"up\")\n\n    def _describe(self):\n        msg = \"\"\n        with self.filecache_mtx:\n            for n, cn in enumerate(self.filecache):\n                cache_path, cache1 = cn.tag\n                cache2 = cache1 + len(cn.data)\n                msg += \"\\n{:<2} {:>7} {:>10}:{:<9} {}\".format(\n                    n,\n                    len(cn.data),\n                    cache1,\n                    cache2,\n                    cache_path.replace(\"\\r\", \"\\\\r\").replace(\"\\n\", \"\\\\n\"),\n                )\n        return msg\n\n    def clean_dircache(self):\n        \"\"\"not threadsafe\"\"\"\n        now = time.time()\n        cutoff = 0\n        for cn in self.dircache:\n            if now - cn.ts > self.n_dircache:\n                cutoff += 1\n            else:\n                break\n\n        if cutoff > 0:\n            self.dircache = self.dircache[cutoff:]\n\n    def get_cached_dir(self, dirpath):\n        with self.dircache_mtx:\n            self.clean_dircache()\n            for cn in self.dircache:\n                if cn.tag == dirpath:\n                    return cn\n\n        return None\n\n    \"\"\"\n            ,-------------------------------,  g1>=c1, g2<=c2\n            |cache1                   cache2|  buf[g1-c1:(g1-c1)+(g2-g1)]\n            `-------------------------------'\n                    ,---------------,\n                    |get1       get2|\n                    `---------------'\n    __________________________________________________________________________\n\n            ,-------------------------------,  g2<=c2, (g2>=c1)\n            |cache1                   cache2|  cdr=buf[:g2-c1]\n            `-------------------------------'  dl car; g1-512K:c1\n    ,---------------,\n    |get1       get2|\n    `---------------'\n    __________________________________________________________________________\n\n            ,-------------------------------,  g1>=c1, (g1<=c2)\n            |cache1                   cache2|  car=buf[c2-g1:]\n            `-------------------------------'  dl cdr; c2:c2+1M\n                                    ,---------------,\n                                    |get1       get2|\n                                    `---------------'\n    \"\"\"\n\n    def get_cached_file(self, path, get1, get2, file_sz):\n        car = None\n        cdr = None\n        ncn = -1\n        dbg(\"cache request {}:{} |{}|\".format(get1, get2, file_sz) + self._describe())\n        with self.filecache_mtx:\n            have_before = False\n            have_after = False\n            for cn in self.filecache:\n                ncn += 1\n\n                cache_path, cache1 = cn.tag\n                if cache_path != path:\n                    continue\n\n                cache2 = cache1 + len(cn.data)\n\n                if get1 == cache2:\n                    have_before = True\n\n                if get2 == cache1:\n                    have_after = True\n\n                if get2 <= cache1 or get1 >= cache2:\n                    # request does not overlap with cached area at all\n                    continue\n\n                if get1 < cache1 and get2 > cache2:\n                    # cached area does overlap, but must specifically contain\n                    # either the first or last byte in the requested range\n                    continue\n\n                if get1 >= cache1 and get2 <= cache2:\n                    # keep cache entry alive by moving it to the end\n                    self.filecache = (\n                        self.filecache[:ncn] + self.filecache[ncn + 1 :] + [cn]\n                    )\n                    buf_ofs = get1 - cache1\n                    buf_end = buf_ofs + (get2 - get1)\n                    dbg(\n                        \"found all (#{} {}:{} |{}|) [{}:{}] = {}\".format(\n                            ncn,\n                            cache1,\n                            cache2,\n                            len(cn.data),\n                            buf_ofs,\n                            buf_end,\n                            buf_end - buf_ofs,\n                        )\n                    )\n                    return cn.data[buf_ofs:buf_end]\n\n                if get2 <= cache2:\n                    x = cn.data[: get2 - cache1]\n                    if not cdr or len(cdr) < len(x):\n                        dbg(\n                            \"found cdr (#{} {}:{} |{}|) [:{}-{}] = [:{}] = {}\".format(\n                                ncn,\n                                cache1,\n                                cache2,\n                                len(cn.data),\n                                get2,\n                                cache1,\n                                get2 - cache1,\n                                len(x),\n                            )\n                        )\n                        cdr = x\n\n                    continue\n\n                if get1 >= cache1:\n                    x = cn.data[-(max(0, cache2 - get1)) :]\n                    if not car or len(car) < len(x):\n                        dbg(\n                            \"found car (#{} {}:{} |{}|) [-({}-{}):] = [-{}:] = {}\".format(\n                                ncn,\n                                cache1,\n                                cache2,\n                                len(cn.data),\n                                cache2,\n                                get1,\n                                cache2 - get1,\n                                len(x),\n                            )\n                        )\n                        car = x\n\n                    continue\n\n                msg = \"cache fallthrough\\n{} {} {}\\n{} {} {}\\n{} {} --\\n\".format(\n                    get1,\n                    get2,\n                    get2 - get1,\n                    cache1,\n                    cache2,\n                    cache2 - cache1,\n                    get1 - cache1,\n                    get2 - cache2,\n                )\n                msg += self._describe()\n                raise Exception(msg)\n\n        if car and cdr and len(car) + len(cdr) == get2 - get1:\n            dbg(\"<cache> have both\")\n            return car + cdr\n\n        elif cdr and (not car or len(car) < len(cdr)):\n            h_end = get1 + (get2 - get1) - len(cdr)\n            if have_before:\n                h_ofs = get1\n            else:\n                h_ofs = min(get1, h_end - 64 * 1024)\n\n            if h_ofs < 0:\n                h_ofs = 0\n\n            buf_ofs = get1 - h_ofs\n\n            dbg(\n                \"<cache> cdr {}, car {}:{} |{}| [{}:]\".format(\n                    len(cdr), h_ofs, h_end, h_end - h_ofs, buf_ofs\n                )\n            )\n\n            buf, c = self.gw.download_file_range(path, h_ofs, h_end)\n            if len(buf) == h_end - h_ofs:\n                ret = buf[buf_ofs:] + cdr\n            else:\n                ret = buf[get1 - h_ofs :]\n                info(\n                    \"remote truncated {}:{} to |{}|, will return |{}|\".format(\n                        h_ofs, h_end, len(buf), len(ret)\n                    )\n                )\n\n        elif car:\n            h_ofs = get1 + len(car)\n            buf_ofs = (get2 - get1) - len(car)\n\n            dbg(\n                \"<cache> car {}, cdr {}:{} |{}| [:{}]\".format(\n                    len(car), h_ofs, get2, get2 - h_ofs, buf_ofs\n                )\n            )\n\n            buf, c = self.gw.download_file_range(path, h_ofs, get2)\n            ret = car + buf[:buf_ofs]\n\n        else:\n            h_ofs = get1\n            if not have_before:\n                if get2 - get1 <= 1024 * 1024:\n                    h_ofs = get1 - 64 * 1024\n\n                if h_ofs < 0:\n                    h_ofs = 0\n\n            buf_ofs = get1 - h_ofs\n            buf_end = buf_ofs + get2 - get1\n\n            dbg(\n                \"<cache> {}:{} |{}| [{}:{}]\".format(\n                    h_ofs, get2, get2 - h_ofs, buf_ofs, buf_end\n                )\n            )\n\n            buf, c = self.gw.download_file_range(path, h_ofs, get2)\n            ret = buf[buf_ofs:buf_end]\n\n        if c and c.cnode and len(c.cnode.data) + len(buf) < 1024 * 1024:\n            dbg(\n                \"cache: {}(@{}) + {}(@{})\".format(\n                    len(c.cnode.data), c.cnode.tag[1], len(buf), buf_ofs, get1\n                )\n            )\n            c.cnode.data += buf\n            return ret\n\n        cn = CacheNode([path, h_ofs], buf)\n        with self.filecache_mtx:\n            if len(self.filecache) >= self.n_filecache:\n                self.filecache = self.filecache[1:] + [cn]\n            else:\n                self.filecache.append(cn)\n\n        c.cnode = cn\n        return ret\n\n    def _readdir(self, path, fh=None):\n        path = path.strip(\"/\")\n        log(\"readdir [{}] [{}]\".format(hexler(path), fh))\n\n        ret = self.gw.listdir(path)\n        if not self.n_dircache:\n            return ret\n\n        with self.dircache_mtx:\n            cn = CacheNode(path, ret)\n            self.dircache.append(cn)\n            self.clean_dircache()\n\n        return ret\n\n    def readdir(self, path, fh=None):\n        return [\".\", \"..\"] + self._readdir(path, fh)\n\n    def read(self, path, length, offset, fh=None):\n        req_max = 1024 * 1024 * 8\n        cache_max = 1024 * 1024 * 2\n        if length > req_max:\n            # windows actually doing 240 MiB read calls, sausage\n            info(\"truncate |{}| to {}MiB\".format(length, req_max >> 20))\n            length = req_max\n\n        path = path.strip(\"/\")\n        ofs2 = offset + length\n        file_sz = self.getattr(path)[\"st_size\"]\n        log(\n            \"read {} |{}| {}:{} max {}\".format(\n                hexler(path), length, offset, ofs2, file_sz\n            )\n        )\n        if ofs2 > file_sz:\n            ofs2 = file_sz\n            log(\"truncate to |{}| :{}\".format(ofs2 - offset, ofs2))\n\n        if file_sz == 0 or offset >= ofs2:\n            return b\"\"\n\n        if self.n_filecache and length <= cache_max:\n            ret = self.get_cached_file(path, offset, ofs2, file_sz)\n        else:\n            ret = self.gw.download_file_range(path, offset, ofs2)[0]\n\n        return ret\n\n        fn = \"cppf-{}-{}-{}\".format(time.time(), offset, length)\n        if False:\n            with open(fn, \"wb\", len(ret)) as f:\n                f.write(ret)\n        elif self.n_filecache:\n            ret2 = self.gw.download_file_range(path, offset, ofs2)\n            if ret != ret2:\n                info(fn)\n                for v in [ret, ret2]:\n                    try:\n                        info(len(v))\n                    except:\n                        info(\"uhh \" + repr(v))\n\n                with open(fn + \".bad\", \"wb\") as f:\n                    f.write(ret)\n                with open(fn + \".good\", \"wb\") as f:\n                    f.write(ret2)\n\n                raise Exception(\"cache bork\")\n\n        return ret\n\n    def getattr(self, path, fh=None):\n        log(\"getattr [{}]\".format(hexler(path)))\n        if WINDOWS:\n            path = enwin(path)  # windows occasionally decodes f0xx to xx\n\n        path = path.strip(\"/\")\n        try:\n            dirpath, fname = path.rsplit(\"/\", 1)\n        except:\n            dirpath = \"\"\n            fname = path\n\n        if not path:\n            ret = self.gw.stat_dir(time.time())\n            # dbg(\"=\" + repr(ret))\n            return ret\n\n        cn = self.get_cached_dir(dirpath)\n        if cn:\n            log(\"cache ok\")\n            dents = cn.data\n        else:\n            dbg(\"cache miss\")\n            dents = self._readdir(dirpath)\n\n        for cache_name, cache_stat, _ in dents:\n            # if \"qw\" in cache_name and \"qw\" in fname:\n            #     info(\n            #         \"cmp\\n  [{}]\\n  [{}]\\n\\n{}\\n\".format(\n            #             hexler(cache_name),\n            #             hexler(fname),\n            #             \"\\n\".join(traceback.format_stack()[:-1]),\n            #         )\n            #     )\n\n            if cache_name == fname:\n                # dbg(\"=\" + repr(cache_stat))\n                return cache_stat\n\n        info(\"=ENOENT ({})\".format(hexler(path)))\n        raise FuseOSError(errno.ENOENT)\n\n    access = None\n    flush = None\n    getxattr = None\n    listxattr = None\n    open = None\n    opendir = None\n    release = None\n    releasedir = None\n    statfs = None\n\n    if False:\n        # incorrect semantics but good for debugging stuff like samba and msys2\n        def access(self, path, mode):\n            log(\"@@ access [{}] [{}]\".format(path, mode))\n            return 1 if self.getattr(path) else 0\n\n        def flush(self, path, fh):\n            log(\"@@ flush [{}] [{}]\".format(path, fh))\n            return True\n\n        def getxattr(self, *args):\n            log(\"@@ getxattr [{}]\".format(\"] [\".join(str(x) for x in args)))\n            return False\n\n        def listxattr(self, *args):\n            log(\"@@ listxattr [{}]\".format(\"] [\".join(str(x) for x in args)))\n            return False\n\n        def open(self, path, flags):\n            log(\"@@ open [{}] [{}]\".format(path, flags))\n            return 42\n\n        def opendir(self, fh):\n            log(\"@@ opendir [{}]\".format(fh))\n            return 69\n\n        def release(self, ino, fi):\n            log(\"@@ release [{}] [{}]\".format(ino, fi))\n            return True\n\n        def releasedir(self, ino, fi):\n            log(\"@@ releasedir [{}] [{}]\".format(ino, fi))\n            return True\n\n        def statfs(self, path):\n            log(\"@@ statfs [{}]\".format(path))\n            return {}\n\n    if sys.platform == \"win32\":\n        # quick compat for /mingw64/bin/python3 (msys2)\n        def _open(self, path):\n            try:\n                x = self.getattr(path)\n                if x[\"st_mode\"] <= 0:\n                    raise Exception()\n\n                self.junk_fh_ctr += 1\n                if self.junk_fh_ctr > 32000:  # TODO untested\n                    self.junk_fh_ctr = 4\n\n                return self.junk_fh_ctr\n\n            except Exception as ex:\n                log(\"open ERR {}\".format(repr(ex)))\n                raise FuseOSError(errno.ENOENT)\n\n        def open(self, path, flags):\n            dbg(\"open [{}] [{}]\".format(hexler(path), flags))\n            return self._open(path)\n\n        def opendir(self, path):\n            dbg(\"opendir [{}]\".format(hexler(path)))\n            return self._open(path)\n\n        def flush(self, path, fh):\n            dbg(\"flush [{}] [{}]\".format(hexler(path), fh))\n\n        def release(self, ino, fi):\n            dbg(\"release [{}] [{}]\".format(hexler(ino), fi))\n\n        def releasedir(self, ino, fi):\n            dbg(\"releasedir [{}] [{}]\".format(hexler(ino), fi))\n\n        def access(self, path, mode):\n            dbg(\"access [{}] [{}]\".format(hexler(path), mode))\n            try:\n                x = self.getattr(path)\n                if x[\"st_mode\"] <= 0:\n                    raise Exception()\n            except:\n                raise FuseOSError(errno.ENOENT)\n\n\nclass TheArgparseFormatter(\n    argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter\n):\n    pass\n\n\ndef main():\n    global info, log, dbg\n    time.strptime(\"19970815\", \"%Y%m%d\")  # python#7980\n\n    # filecache helps for reads that are ~64k or smaller;\n    #   linux generally does 128k so the cache is a slowdown,\n    #   windows likes to use 4k and 64k so cache is required,\n    #   value is numChunks (1~3M each) to keep in the cache\n    nf = 24\n\n    # dircache is always a boost,\n    #   only want to disable it for tests etc,\n    #   value is numSec until an entry goes stale\n    nd = 1\n\n    where = \"local directory\"\n    if WINDOWS:\n        where += \" or DRIVE:\"\n\n    ex_pre = \"\\n  \" + os.path.basename(__file__) + \"  \"\n    examples = [\"http://192.168.1.69:3923/music/  ./music\"]\n    if WINDOWS:\n        examples.append(\"http://192.168.1.69:3923/music/  M:\")\n\n    ap = argparse.ArgumentParser(\n        formatter_class=TheArgparseFormatter,\n        epilog=\"example:\" + ex_pre + ex_pre.join(examples),\n    )\n    ap.add_argument(\n        \"-cd\", metavar=\"NUM_SECONDS\", type=float, default=nd, help=\"directory cache\"\n    )\n    ap.add_argument(\n        \"-cf\", metavar=\"NUM_BLOCKS\", type=int, default=nf, help=\"file cache\"\n    )\n    ap.add_argument(\"-a\", metavar=\"PASSWORD\", help=\"password\")\n    ap.add_argument(\"-d\", action=\"store_true\", help=\"enable debug\")\n    ap.add_argument(\"-te\", metavar=\"PEM_FILE\", help=\"certificate to expect/verify\")\n    ap.add_argument(\"-td\", action=\"store_true\", help=\"disable certificate check\")\n    ap.add_argument(\"base_url\", type=str, help=\"remote copyparty URL to mount\")\n    ap.add_argument(\"local_path\", type=str, help=where + \" to mount it on\")\n    ar = ap.parse_args()\n\n    if ar.d:\n        # windows terminals are slow (cmd.exe, mintty)\n        # otoh fancy_log beats RecentLog on linux\n        logger = RecentLog().put if WINDOWS else fancy_log\n\n        info = logger\n        log = logger\n        dbg = logger\n    else:\n        # debug=off, speed is dontcare\n        info = fancy_log\n        log = null_log\n        dbg = null_log\n\n    if WINDOWS:\n        os.system(\"rem\")\n\n        for ch in '<>:\"\\\\|?*':\n            # microsoft maps illegal characters to f0xx\n            # (e000 to f8ff is basic-plane private-use)\n            bad_good[ch] = chr(ord(ch) + 0xF000)\n\n        for n in range(0, 0x100):\n            # map surrogateescape to another private-use area\n            bad_good[chr(n + 0xDC00)] = chr(n + 0xF100)\n\n        for k, v in bad_good.items():\n            good_bad[v] = k\n\n    register_wtf8()\n\n    try:\n        with open(\"/etc/fuse.conf\", \"rb\") as f:\n            allow_other = b\"\\nuser_allow_other\" in f.read()\n    except:\n        allow_other = WINDOWS or MACOS\n\n    args = {\"foreground\": True, \"nothreads\": True, \"allow_other\": allow_other}\n    if not MACOS:\n        args[\"nonempty\"] = True\n\n    FUSE(CPPF(ar), ar.local_path, encoding=\"wtf-8\", **args)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/partyfuse.py",
    "content": "#!/usr/bin/env python3\n\n\"\"\"partyfuse: remote copyparty as a local filesystem\"\"\"\n__author__ = \"ed <copyparty@ocv.me>\"\n__copyright__ = 2019\n__license__ = \"MIT\"\n__url__ = \"https://github.com/9001/copyparty/\"\n\nS_VERSION = \"2.2\"\nS_BUILD_DT = \"2025-12-16\"\n\n\"\"\"\nmount a copyparty server (local or remote) as a filesystem\n\nspeeds:\n  1 GiB/s reading large files\n  27'000 files/sec: copy small files\n  700 folders/sec: copy small folders\n\nusage:\n  python partyfuse.py http://192.168.1.69:3923/  ./music\n\ndependencies:\n  python3 -m pip install --user fusepy  # or grab it from the connect page\n  + on Linux: sudo apk add fuse\n  + on Macos: https://osxfuse.github.io/\n  + on Windows: https://github.com/billziss-gh/winfsp/releases/latest\n\nnote:\n  you probably want to run this on windows clients:\n  https://github.com/9001/copyparty/blob/hovudstraum/contrib/explorer-nothumbs-nofoldertypes.reg\n\nget server cert:\n  awk '/-BEGIN CERTIFICATE-/ {a=1} a; /-END CERTIFICATE-/{exit}' <(openssl s_client -connect 127.0.0.1:3923 </dev/null 2>/dev/null) >cert.pem\n\"\"\"\n\n\nimport argparse\nimport calendar\nimport codecs\nimport errno\nimport json\nimport os\nimport platform\nimport re\nimport stat\nimport struct\nimport sys\nimport threading\nimport time\nimport traceback\nimport urllib.parse\nfrom datetime import datetime, timezone\nfrom urllib.parse import quote_from_bytes as quote\nfrom urllib.parse import unquote_to_bytes as unquote\n\nimport builtins\nimport http.client\n\nWINDOWS = sys.platform == \"win32\"\nMACOS = platform.system() == \"Darwin\"\nUTC = timezone.utc\n\n# !rm.yes>\nMON3S = \"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec\"\nMON3 = {b: a for a, b in enumerate(MON3S.split(), 1)}\n# !rm.no>\n\n\ndef print(*args, **kwargs):\n    try:\n        builtins.print(*list(args), **kwargs)\n    except:\n        builtins.print(termsafe(\" \".join(str(x) for x in args)), **kwargs)\n\n\nprint(\n    \"{} v{} @ {}\".format(\n        platform.python_implementation(),\n        \".\".join([str(x) for x in sys.version_info]),\n        sys.executable,\n    )\n)\n\n\ndef nullfun(*a):\n    pass\n\n\ninfo = dbg = nullfun\nis_dbg = False\n\n\ntry:\n    from fuse import FUSE, FuseOSError, Operations\nexcept:\n    if WINDOWS:\n        libfuse = \"install https://github.com/billziss-gh/winfsp/releases/latest\"\n    elif MACOS:\n        libfuse = \"install https://osxfuse.github.io/\"\n    else:\n        libfuse = \"apt install libfuse2\\n    modprobe fuse\"\n\n    m = \"\"\"\\033[33m\n  could not import fuse; these may help:\n    {} -m pip install --user fusepy\n    {}\n\\033[0m\"\"\"\n    print(m.format(sys.executable, libfuse))\n    raise\n\n\ndef termsafe(txt):\n    enc = sys.stdout.encoding\n    try:\n        return txt.encode(enc, \"backslashreplace\").decode(enc)\n    except:\n        return txt.encode(enc, \"replace\").decode(enc)\n\n\ndef threadless_log(fmt, *a):\n    fmt += \"\\n\"\n    print(fmt % a if a else fmt, end=\"\")\n\n\nriced_tids = {}\n\n\ndef rice_tid():\n    tid = threading.current_thread().ident\n    try:\n        return riced_tids[tid]\n    except:\n        c = struct.unpack(b\"B\" * 5, struct.pack(b\">Q\", tid)[-5:])\n        ret = \"\".join(\"\\033[1;37;48;5;%dm%02x\" % (x, x) for x in c) + \"\\033[0m\"\n        riced_tids[tid] = ret\n        return ret\n\n\ndef fancy_log(fmt, *a):\n    msg = fmt % a if a else fmt\n    print(\"%10.6f %s %s\\n\" % (time.time() % 900, rice_tid(), msg), end=\"\")\n\n\ndef register_wtf8():\n    def wtf8_enc(text):\n        return str(text).encode(\"utf-8\", \"surrogateescape\"), len(text)\n\n    def wtf8_dec(binary):\n        return bytes(binary).decode(\"utf-8\", \"surrogateescape\"), len(binary)\n\n    def wtf8_search(encoding_name):\n        return codecs.CodecInfo(wtf8_enc, wtf8_dec, name=\"wtf-8\")\n\n    codecs.register(wtf8_search)\n\n\nbad_good = {}\ngood_bad = {}\n\n\ndef enwin(txt):\n    return \"\".join([bad_good.get(x, x) for x in txt])\n\n\ndef dewin(txt):\n    return \"\".join([good_bad.get(x, x) for x in txt])\n\n\nclass RecentLog(object):\n    def __init__(self, ar):\n        self.ar = ar\n        self.mtx = threading.Lock()\n        self.f = open(ar.logf, \"wb\") if ar.logf else None\n        self.q = []\n\n        thr = threading.Thread(target=self.printer)\n        thr.daemon = True\n        thr.start()\n\n    def put(self, fmt, *a):\n        msg = fmt % a if a else fmt\n        msg = \"%10.6f %s %s\\n\" % (time.time() % 900, rice_tid(), msg)\n        if self.f:\n            zd = datetime.now(UTC)\n            fmsg = \"%d-%04d-%06d.%06d %s\" % (\n                zd.year,\n                zd.month * 100 + zd.day,\n                (zd.hour * 100 + zd.minute) * 100 + zd.second,\n                zd.microsecond,\n                msg,\n            )\n            self.f.write(fmsg.encode(\"utf-8\"))\n\n        with self.mtx:\n            self.q.append(msg)\n            if len(self.q) > 200:\n                self.q = self.q[-50:]\n\n    def printer(self):\n        while True:\n            time.sleep(0.05)\n            with self.mtx:\n                q = self.q\n                if not q:\n                    continue\n\n                self.q = []\n\n            print(\"\".join(q), end=\"\")\n\n\n# [windows/cmd/cpy3]  python dev\\copyparty\\bin\\partyfuse.py q: http://192.168.1.159:1234/\n# [windows/cmd/msys2] C:\\msys64\\mingw64\\bin\\python3 dev\\copyparty\\bin\\partyfuse.py q: http://192.168.1.159:1234/\n# [windows/mty/msys2] /mingw64/bin/python3 /c/Users/ed/dev/copyparty/bin/partyfuse.py q: http://192.168.1.159:1234/\n#\n# [windows] find /q/music/albums/Phant*24bit -printf '%s %p\\n' | sort -n | tail -n 8 | sed -r 's/^[0-9]+ //' | while IFS= read -r x; do dd if=\"$x\" of=/dev/null bs=4k count=8192 & done\n# [alpine]  ll t; for x in t/2020_0724_16{2,3}*; do dd if=\"$x\" of=/dev/null bs=4k count=10240 & done\n#\n#  72.4983 windows mintty msys2 fancy_log\n# 219.5781 windows cmd msys2 fancy_log\n# nope.avi windows cmd cpy3 fancy_log\n#   9.8817 windows mintty msys2 RecentLog 200 50 0.1\n#  10.2241 windows cmd cpy3 RecentLog 200 50 0.1\n#   9.8494 windows cmd msys2 RecentLog 200 50 0.1\n#   7.8061 windows mintty msys2 fancy_log <info-only>\n#   7.9961 windows mintty msys2 RecentLog <info-only>\n#   4.2603 alpine xfce4 cpy3 RecentLog\n#   4.1538 alpine xfce4 cpy3 fancy_log\n#   3.1742 alpine urxvt cpy3 fancy_log\n\n\ndef get_tid():\n    return threading.current_thread().ident\n\n\ndef html_dec(txt):\n    return (\n        txt.replace(\"&lt;\", \"<\")\n        .replace(\"&gt;\", \">\")\n        .replace(\"&quot;\", '\"')\n        .replace(\"&#13;\", \"\\r\")\n        .replace(\"&#10;\", \"\\n\")\n        .replace(\"&amp;\", \"&\")\n    )\n\n\nclass CacheNode(object):\n    def __init__(self, tag, data):\n        self.tag = tag\n        self.data = data\n        self.ts = time.time()\n\n\nclass Gateway(object):\n    def __init__(self, ar):\n        zs = ar.base_url\n        if \"://\" not in zs:\n            zs = \"http://\" + zs\n\n        self.base_url = zs\n        self.password = ar.a\n\n        ui = urllib.parse.urlparse(zs)\n        self.web_root = ui.path.strip(\"/\")\n        self.SRS = \"/%s/\" % (self.web_root,) if self.web_root else \"/\"\n        try:\n            self.web_host, self.web_port = ui.netloc.split(\":\")\n            self.web_port = int(self.web_port)\n        except:\n            self.web_host = ui.netloc\n            if ui.scheme == \"http\":\n                self.web_port = 80\n            elif ui.scheme == \"https\":\n                self.web_port = 443\n            else:\n                raise Exception(\"bad url?\")\n\n        self.ssl_context = None\n        self.use_tls = ui.scheme.lower() == \"https\"\n        if self.use_tls:\n            import ssl\n\n            if ar.td:\n                self.ssl_context = ssl._create_unverified_context()\n            elif ar.te:\n                self.ssl_context = ssl.create_default_context(cafile=ar.te)\n                self.ssl_context.check_hostname = ar.teh\n\n        self.conns = {}\n\n        self.fsuf = \"?raw\"\n        self.dsuf = \"?ls&lt&dots\"\n\n        # !rm.yes>\n        if not ar.html:\n            self.parse_html = None\n\n        elif ar.html == \"cpp\":\n            self.parse_html = self.parse_cpp\n            self.dsuf = \"?lt&dots\"\n            self.re_row = re.compile(\n                r'^<tr><td>(-|DIR|<a [^<]+</a>)</td><td><a[^>]* href=\"([^\"]+)\"[^>]*>([^<]+)</a></td><td>([^<]+)</td><td>.*</td><td>([^<]+)</td></tr>$'\n            )\n\n        elif ar.html == \"nginx\":\n            self.parse_html = self.parse_nginx\n            self.fsuf = \"\"\n            self.dsuf = \"\"\n            self.re_row = re.compile(\n                r'^<a href=\"([^\"]+)\">([^<]+)</a> *([0-9]{2})-([A-Z][a-z]{2})-([0-9]{4}) ([0-9]{2}:[0-9]{2}) *(-|[0-9]+)\\r?$'\n            )\n\n        elif ar.html == \"iis\":\n            self.parse_html = self.parse_iis\n            self.fsuf = \"\"\n            self.dsuf = \"\"\n            self.re_2nl = re.compile(br\"<br>|</pre>\")\n            self.re_row = re.compile(\n                r'^ *([0-9]{1,2})/([0-9]{1,2})/([0-9]{4}) {1,2}([0-9]{1,2}:[0-9]{2}) ([AP]M) +(&lt;dir&gt;|[0-9]+) <A HREF=\"([^\"]+)\">([^<>]+)</A>$'\n            )\n\n        else:\n            raise Exception(\"unknown HTML dialect: [%s]\" % (ar.html,))\n        # !rm.no>\n\n    def quotep(self, path):\n        path = path.encode(\"wtf-8\")\n        return quote(path, safe=\"/\")\n\n    def getconn(self, tid=None):\n        tid = tid or get_tid()\n        try:\n            return self.conns[tid]\n        except:\n            info(\"new conn [{}] [{}]\".format(self.web_host, self.web_port))\n\n            args = {}\n            if not self.use_tls:\n                C = http.client.HTTPConnection\n            else:\n                C = http.client.HTTPSConnection\n                if self.ssl_context:\n                    args = {\"context\": self.ssl_context}\n\n            conn = C(self.web_host, self.web_port, timeout=260, **args)\n\n            self.conns[tid] = conn\n            return conn\n\n    def closeconn(self, tid=None):\n        tid = tid or get_tid()\n        try:\n            self.conns[tid].close()\n            del self.conns[tid]\n        except:\n            pass\n\n    def sendreq(self, meth, path, headers, **kwargs):\n        tid = get_tid()\n        if self.password:\n            headers[\"PW\"] = self.password\n\n        try:\n            c = self.getconn(tid)\n            c.request(meth, path, headers=headers, **kwargs)\n            return c.getresponse()\n        except Exception as ex:\n            info(\"HTTP %r\", ex)\n\n        self.closeconn(tid)\n        try:\n            c = self.getconn(tid)\n            c.request(meth, path, headers=headers, **kwargs)\n            return c.getresponse()\n        except:\n            info(\"http connection failed:\\n\" + traceback.format_exc())\n            if self.use_tls and not self.ssl_context:\n                import ssl\n\n                cert = ssl.get_server_certificate((self.web_host, self.web_port))\n                info(\"server certificate probably not trusted:\\n\" + cert)\n\n            raise\n\n    def listdir(self, path):\n        if bad_good:\n            path = dewin(path)\n\n        zs = \"%s%s/\" if path else \"%s%s\"\n        web_path = self.quotep(zs % (self.SRS, path)) + self.dsuf\n        r = self.sendreq(\"GET\", web_path, {})\n        if r.status != 200:\n            self.closeconn()\n            info(\"http error %s reading dir %r\", r.status, web_path)\n            err = errno.ENOENT if r.status == 404 else errno.EIO\n            raise FuseOSError(err)\n\n        ctype = r.getheader(\"Content-Type\", \"\")\n        if ctype == \"application/json\":\n            parser = self.parse_jls\n            # !rm.yes>\n        elif ctype.startswith(\"text/html\"):\n            parser = self.parse_html\n            # !rm.no>\n        else:\n            info(\"listdir on file (%s): %r\", ctype, path)\n            raise FuseOSError(errno.ENOENT)\n\n        try:\n            return parser(r)\n        except:\n            info(\"parser: %r\\n%s\", path, traceback.format_exc())\n            raise FuseOSError(errno.EIO)\n\n    def download_file_range(self, path, ofs1, ofs2):\n        if bad_good:\n            path = dewin(path)\n\n        web_path = self.quotep(\"%s%s\" % (self.SRS, path)) + self.fsuf\n        hdr_range = \"bytes=%d-%d\" % (ofs1, ofs2 - 1)\n\n        t = \"DL %4.0fK\\033[36m%9d-%-9d\\033[0m%r\"\n        info(t, (ofs2 - ofs1) / 1024.0, ofs1, ofs2 - 1, path)\n\n        r = self.sendreq(\"GET\", web_path, {\"Range\": hdr_range})\n        if r.status != http.client.PARTIAL_CONTENT:\n            t = \"http error %d reading file %r range %s in %s\"\n            info(t, r.status, web_path, hdr_range, rice_tid())\n            self.closeconn()\n            raise FuseOSError(errno.EIO)\n\n        return r.read()\n\n    def parse_jls(self, sck):\n        rsp = b\"\"\n        while True:\n            buf = sck.read(1024 * 32)\n            if not buf:\n                break\n            rsp += buf\n\n        rsp = json.loads(rsp.decode(\"utf-8\"))\n        ret = {}\n        for statfun, nodes in [\n            [self.stat_dir, rsp[\"dirs\"]],\n            [self.stat_file, rsp[\"files\"]],\n        ]:\n            for n in nodes:\n                fname = unquote(n[\"href\"].split(\"?\")[0]).rstrip(b\"/\").decode(\"wtf-8\")\n                if bad_good:\n                    fname = enwin(fname)\n\n                ret[fname] = statfun(n[\"ts\"], n[\"sz\"])\n\n        return ret\n\n    # !rm.yes>\n    ####################################################################\n    ####################################################################\n\n    def parse_cpp(self, sck):\n        # https://a.ocv.me/pub/\n\n        ret = {}\n        rem = b\"\"\n        ptn = self.re_row\n\n        while True:\n            buf = sck.read(1024 * 32)\n            if not buf:\n                break\n\n            buf = rem + buf\n            rem = b\"\"\n            idx = buf.rfind(b\"\\n\")\n            if idx >= 0:\n                rem = buf[idx + 1 :]\n                buf = buf[:idx]\n\n            lines = buf.decode(\"utf-8\").split(\"\\n\")\n            for line in lines:\n                m = ptn.match(line)\n                if not m:\n                    continue\n\n                ftype, furl, fname, fsize, fdate = m.groups()\n                fname = furl.rstrip(\"/\").split(\"/\")[-1]\n                fname = unquote(fname)\n                fname = fname.decode(\"wtf-8\")\n                if bad_good:\n                    fname = enwin(fname)\n\n                sz = 1\n                ts = 60 * 60 * 24 * 2\n                try:\n                    sz = int(fsize)\n                    ts = calendar.timegm(time.strptime(fdate, \"%Y-%m-%d %H:%M:%S\"))\n                except:\n                    info(\"bad HTML or OS %r %r\\n%r\", fdate, fsize, line)\n                    # python cannot strptime(1959-01-01) on windows\n\n                if ftype != \"DIR\" and \"zip=crc\" not in ftype:\n                    ret[fname] = self.stat_file(ts, sz)\n                else:\n                    ret[fname] = self.stat_dir(ts, sz)\n\n        return ret\n\n    def parse_nginx(self, sck):\n        # https://ocv.me/stuff/  \"06-Feb-2015 15:43\"\n\n        ret = {}\n        rem = b\"\"\n        re_row = self.re_row\n\n        while True:\n            buf = sck.read(1024 * 32)\n            if not buf:\n                break\n\n            buf = rem + buf\n            rem = b\"\"\n            idx = buf.rfind(b\"\\n\")\n            if idx >= 0:\n                rem = buf[idx + 1 :]\n                buf = buf[:idx]\n\n            fdate = \"\"\n            lines = buf.decode(\"utf-8\").split(\"\\n\")\n            for line in lines:\n                m = re_row.match(line)\n                if not m:\n                    continue\n\n                furl, fname, day, smon, year, hm, fsize = m.groups()\n                fname = furl.rstrip(\"/\").split(\"/\")[-1]\n                fname = unquote(fname)\n                fname = fname.decode(\"wtf-8\")\n                if bad_good:\n                    fname = enwin(fname)\n\n                sz = 1\n                ts = 60 * 60 * 24 * 2\n                try:\n                    fdate = \"%s-%02d-%s %s\" % (year, MON3[smon], day, hm)\n                    ts = calendar.timegm(time.strptime(fdate, \"%Y-%m-%d %H:%M\"))\n                    sz = -1 if fsize == \"-\" else int(fsize)\n                except:\n                    info(\"bad HTML or OS %r %r\\n%r\", fdate, fsize, line)\n\n                if sz == -1:\n                    ret[fname] = self.stat_dir(ts, 4096)\n                else:\n                    ret[fname] = self.stat_file(ts, sz)\n\n        return ret\n\n    def parse_iis(self, sck):\n        # https://nedlasting.miljodirektoratet.no/miljodata/  \" 9/28/2024  5:24 AM\"\n        # https://grandcanyon.usgs.gov/photos/Foodbase/CISP/  \" 6/29/2012  3:12 PM\"\n\n        ret = {}\n        rem = b\"\"\n        re_row = self.re_row\n        re_2nl = self.re_2nl\n\n        while True:\n            buf = sck.read(1024 * 32)\n            if not buf:\n                break\n\n            buf = rem + buf\n            rem = b\"\"\n            buf = re_2nl.sub(b\"\\n\", buf)\n            idx = buf.rfind(b\"\\n\")\n            if idx >= 0:\n                rem = buf[idx + 1 :]\n                buf = buf[:idx]\n\n            lines = buf.decode(\"utf-8\").split(\"\\n\")\n            for line in lines:\n                m = re_row.match(line)\n                if not m:\n                    continue\n\n                mon, day, year, hm, xm, fsize, furl, fname = m.groups()\n                fname = furl.rstrip(\"/\").split(\"/\")[-1]\n                fname = unquote(fname)\n                fname = fname.decode(\"wtf-8\")\n                if bad_good:\n                    fname = enwin(fname)\n\n                sz = 1\n                ts = 60 * 60 * 24 * 2\n                fdate = \"%s-%s-%s %s %s\" % (year, mon, day, hm, xm)\n                try:\n                    ts = calendar.timegm(time.strptime(fdate, \"%Y-%m-%d %H:%M %p\"))\n                    sz = -1 if fsize == \"&lt;dir&gt;\" else int(fsize)\n                except:\n                    info(\"bad HTML or OS %r %r\\n%r\", fdate, fsize, line)\n\n                if sz == -1:\n                    ret[fname] = self.stat_dir(ts, 4096)\n                else:\n                    ret[fname] = self.stat_file(ts, sz)\n\n        return ret\n\n    ####################################################################\n    ####################################################################\n    # !rm.no>\n\n    def stat_dir(self, ts, sz):\n        return {\n            \"st_mode\": stat.S_IFDIR | 0o555,\n            \"st_uid\": 1000,\n            \"st_gid\": 1000,\n            \"st_size\": sz,\n            \"st_atime\": ts,\n            \"st_mtime\": ts,\n            \"st_ctime\": ts,\n            \"st_blocks\": int((sz + 511) / 512),\n        }\n\n    def stat_file(self, ts, sz):\n        return {\n            \"st_mode\": stat.S_IFREG | 0o444,\n            \"st_uid\": 1000,\n            \"st_gid\": 1000,\n            \"st_size\": sz,\n            \"st_atime\": ts,\n            \"st_mtime\": ts,\n            \"st_ctime\": ts,\n            \"st_blocks\": int((sz + 511) / 512),\n        }\n\n\nclass CPPF(Operations):\n    def __init__(self, ar):\n        self.gw = Gateway(ar)\n        self.junk_fh_ctr = 3\n        self.t_dircache = ar.cds\n        self.n_dircache = ar.cdn\n        self.n_filecache = ar.cf\n\n        self.dircache = []\n        self.dircache_mtx = threading.Lock()\n\n        self.filecache = []\n        self.filecache_mtx = threading.Lock()\n\n        info(\"up\")\n\n    def _describe(self):\n        msg = []\n        with self.filecache_mtx:\n            for n, cn in enumerate(self.filecache):\n                cache_path, cache1 = cn.tag\n                cache2 = cache1 + len(cn.data)\n                t = \"\\n{:<2} {:>7} {:>10}:{:<9} {}\".format(\n                    n,\n                    len(cn.data),\n                    cache1,\n                    cache2,\n                    cache_path.replace(\"\\r\", \"\\\\r\").replace(\"\\n\", \"\\\\n\"),\n                )\n                msg.append(t)\n        return \"\".join(msg)\n\n    def clean_dircache(self):\n        \"\"\"not threadsafe\"\"\"\n        now = time.time()\n        cutoff = 0\n        for cn in self.dircache:\n            if now - cn.ts <= self.t_dircache:\n                break\n            cutoff += 1\n\n        if cutoff > 0:\n            self.dircache = self.dircache[cutoff:]\n        elif len(self.dircache) > self.n_dircache:\n            self.dircache.pop(0)\n\n    def get_cached_dir(self, dirpath):\n        with self.dircache_mtx:\n            for cn in self.dircache:\n                if cn.tag == dirpath:\n                    if time.time() - cn.ts <= self.t_dircache:\n                        return cn\n                    break\n        return None\n\n    # !rm.yes>\n    \"\"\"\n            ,-------------------------------,  g1>=c1, g2<=c2\n            |cache1                   cache2|  buf[g1-c1:(g1-c1)+(g2-g1)]\n            `-------------------------------'\n                    ,---------------,\n                    |get1       get2|\n                    `---------------'\n    __________________________________________________________________________\n\n            ,-------------------------------,  g2<=c2, (g2>=c1)\n            |cache1                   cache2|  cdr=buf[:g2-c1]\n            `-------------------------------'  dl car; g1-512K:c1\n    ,---------------,\n    |get1       get2|\n    `---------------'\n    __________________________________________________________________________\n\n            ,-------------------------------,  g1>=c1, (g1<=c2)\n            |cache1                   cache2|  car=buf[c2-g1:]\n            `-------------------------------'  dl cdr; c2:c2+1M\n                                    ,---------------,\n                                    |get1       get2|\n                                    `---------------'\n    \"\"\"\n    # !rm.no>\n\n    def get_cached_file(self, path, get1, get2, file_sz):\n        car = None\n        cdr = None\n        ncn = -1\n        if is_dbg:\n            dbg(\"cache request %d:%d |%d|%s\", get1, get2, file_sz, self._describe())\n        with self.filecache_mtx:\n            for cn in self.filecache:\n                ncn += 1\n\n                cache_path, cache1 = cn.tag\n                if cache_path != path:\n                    continue\n\n                cache2 = cache1 + len(cn.data)\n                if get2 <= cache1 or get1 >= cache2:\n                    # request does not overlap with cached area at all\n                    continue\n\n                if get1 < cache1 and get2 > cache2:\n                    # cached area does overlap, but must specifically contain\n                    # either the first or last byte in the requested range\n                    continue\n\n                if get1 >= cache1 and get2 <= cache2:\n                    # keep cache entry alive by moving it to the end\n                    self.filecache = (\n                        self.filecache[:ncn] + self.filecache[ncn + 1 :] + [cn]\n                    )\n                    buf_ofs = get1 - cache1\n                    buf_end = buf_ofs + (get2 - get1)\n                    dbg(\n                        \"found all (#%d %d:%d |%d|) [%d:%d] = %d\",\n                        ncn,\n                        cache1,\n                        cache2,\n                        len(cn.data),\n                        buf_ofs,\n                        buf_end,\n                        buf_end - buf_ofs,\n                    )\n                    return cn.data[buf_ofs:buf_end]\n\n                if get2 <= cache2:\n                    x = cn.data[: get2 - cache1]\n                    if not cdr or len(cdr) < len(x):\n                        dbg(\n                            \"found cdr (#%d %d:%d |%d|) [:%d-%d] = [:%d] = %d\",\n                            ncn,\n                            cache1,\n                            cache2,\n                            len(cn.data),\n                            get2,\n                            cache1,\n                            get2 - cache1,\n                            len(x),\n                        )\n                        cdr = x\n\n                    continue\n\n                if get1 >= cache1:\n                    x = cn.data[-(max(0, cache2 - get1)) :]\n                    if not car or len(car) < len(x):\n                        dbg(\n                            \"found car (#%d %d:%d |%d|) [-(%d-%d):] = [-%d:] = %d\",\n                            ncn,\n                            cache1,\n                            cache2,\n                            len(cn.data),\n                            cache2,\n                            get1,\n                            cache2 - get1,\n                            len(x),\n                        )\n                        car = x\n\n                    continue\n\n                msg = \"cache fallthrough\\n%d %d %d\\n%d %d %d\\n%d %d --\\n%s\" % (\n                    get1,\n                    get2,\n                    get2 - get1,\n                    cache1,\n                    cache2,\n                    cache2 - cache1,\n                    get1 - cache1,\n                    get2 - cache2,\n                    self._describe(),\n                )\n                info(msg)\n                raise FuseOSError(errno.EIO)\n\n        if car and cdr and len(car) + len(cdr) == get2 - get1:\n            dbg(\"<cache> have both\")\n            return car + cdr\n\n        elif cdr and (not car or len(car) < len(cdr)):\n            h_end = get1 + (get2 - get1) - len(cdr)\n            h_ofs = min(get1, h_end - 0x80000)  # 512k\n\n            if h_ofs < 0:\n                h_ofs = 0\n\n            buf_ofs = get1 - h_ofs\n\n            if dbg:\n                t = \"<cache> cdr %d, car %d:%d |%d| [%d:]\"\n                dbg(t, len(cdr), h_ofs, h_end, h_end - h_ofs, buf_ofs)\n\n            buf = self.gw.download_file_range(path, h_ofs, h_end)\n            if len(buf) == h_end - h_ofs:\n                ret = buf[buf_ofs:] + cdr\n            else:\n                ret = buf[get1 - h_ofs :]\n                t = \"remote truncated %d:%d to |%d|, will return |%d|\"\n                info(t, h_ofs, h_end, len(buf), len(ret))\n\n        elif car:\n            h_ofs = get1 + len(car)\n            if get2 < 0x100000:\n                # already cached from 0 to 64k, now do ~64k plus 1 MiB\n                h_end = max(get2, h_ofs + 0x100000)  # 1m\n            else:\n                # after 1 MiB, bump window to 8 MiB\n                h_end = max(get2, h_ofs + 0x800000)  # 8m\n\n            if h_end > file_sz:\n                h_end = file_sz\n\n            buf_ofs = (get2 - get1) - len(car)\n\n            t = \"<cache> car %d, cdr %d:%d |%d| [:%d]\"\n            dbg(t, len(car), h_ofs, h_end, h_end - h_ofs, buf_ofs)\n\n            buf = self.gw.download_file_range(path, h_ofs, h_end)\n            ret = car + buf[:buf_ofs]\n\n        else:\n            if get2 - get1 < 0x500000:  # 5m\n                # unless the request is for the last n bytes of the file,\n                # grow the start to cache some stuff around the range\n                if get2 < file_sz - 1:\n                    h_ofs = get1 - 0x40000  # 256k\n                else:\n                    h_ofs = get1 - 0x10000  # 64k\n\n                # likewise grow the end unless start is 0\n                if get1 >= 0x100000:\n                    h_end = get2 + 0x400000  # 4m\n                elif get1 > 0:\n                    h_end = get2 + 0x100000  # 1m\n                else:\n                    h_end = get2 + 0x10000  # 64k\n            else:\n                # big enough, doesn't need pads\n                h_ofs = get1\n                h_end = get2\n\n            if h_ofs < 0:\n                h_ofs = 0\n\n            if h_end > file_sz:\n                h_end = file_sz\n\n            buf_ofs = get1 - h_ofs\n            buf_end = buf_ofs + get2 - get1\n\n            t = \"<cache> %d:%d |%d| [%d:%d]\"\n            dbg(t, h_ofs, h_end, h_end - h_ofs, buf_ofs, buf_end)\n\n            buf = self.gw.download_file_range(path, h_ofs, h_end)\n            ret = buf[buf_ofs:buf_end]\n\n        cn = CacheNode([path, h_ofs], buf)\n        with self.filecache_mtx:\n            if len(self.filecache) >= self.n_filecache:\n                self.filecache = self.filecache[1:] + [cn]\n            else:\n                self.filecache.append(cn)\n\n        return ret\n\n    def _readdir(self, path, fh=None):\n        dbg(\"dircache miss\")\n        ret = self.gw.listdir(path)\n        if not self.n_dircache:\n            return ret\n\n        with self.dircache_mtx:\n            cn = CacheNode(path, ret)\n            self.dircache.append(cn)\n            self.clean_dircache()\n\n        return ret\n\n    def readdir(self, path, fh=None):\n        dbg(\"readdir %r [%s]\", path, fh)\n        path = path.strip(\"/\")\n        cn = self.get_cached_dir(path)\n        if cn:\n            ret = cn.data\n        else:\n            ret = self._readdir(path, fh)\n        return [\".\", \"..\"] + list(ret)\n\n    def read(self, path, length, offset, fh=None):\n        req_max = 1024 * 1024 * 8\n        cache_max = 1024 * 1024 * 2\n        if length > req_max:\n            # windows actually doing 240 MiB read calls, sausage\n            info(\"truncate |%d| to %dMiB\", length, req_max >> 20)\n            length = req_max\n\n        path = path.strip(\"/\")\n        ofs2 = offset + length\n        file_sz = self.getattr(path)[\"st_size\"]\n        dbg(\"read %r |%d| %d:%d max %d\", path, length, offset, ofs2, file_sz)\n\n        if ofs2 > file_sz:\n            ofs2 = file_sz\n            dbg(\"truncate to |%d| :%d\", ofs2 - offset, ofs2)\n\n        if file_sz == 0 or offset >= ofs2:\n            return b\"\"\n\n        if self.n_filecache and length <= cache_max:\n            ret = self.get_cached_file(path, offset, ofs2, file_sz)\n        else:\n            ret = self.gw.download_file_range(path, offset, ofs2)\n\n        return ret\n\n        # !rm.yes>\n        fn = \"cppf-{}-{}-{}\".format(time.time(), offset, length)\n        if False:\n            with open(fn, \"wb\", len(ret)) as f:\n                f.write(ret)\n        elif self.n_filecache:\n            ret2 = self.gw.download_file_range(path, offset, ofs2)\n            if ret != ret2:\n                info(fn)\n                for v in [ret, ret2]:\n                    try:\n                        info(len(v))\n                    except:\n                        info(\"uhh \" + repr(v))\n\n                with open(fn + \".bad\", \"wb\") as f:\n                    f.write(ret)\n                with open(fn + \".good\", \"wb\") as f:\n                    f.write(ret2)\n\n                raise Exception(\"cache bork\")\n\n        return ret\n        # !rm.no>\n\n    def getattr(self, path, fh=None):\n        dbg(\"getattr %r\", path)\n        if WINDOWS:\n            path = enwin(path)  # windows occasionally decodes f0xx to xx\n\n        path = path.strip(\"/\")\n        if not path:\n            ret = self.gw.stat_dir(time.time(), 4096)\n            dbg(\"/=%r\", ret)\n            return ret\n\n        try:\n            dirpath, fname = path.rsplit(\"/\", 1)\n        except:\n            dirpath = \"\"\n            fname = path\n\n        cn = self.get_cached_dir(dirpath)\n        if cn:\n            dents = cn.data\n        else:\n            dents = self._readdir(dirpath)\n\n        try:\n            ret = dents[fname]\n            dbg(\"s=%r\", ret)\n            return ret\n        except:\n            pass\n\n        fun = info\n        if MACOS and path.split(\"/\")[-1].startswith(\"._\"):\n            fun = dbg\n\n        fun(\"=ENOENT %r\", path)\n        raise FuseOSError(errno.ENOENT)\n\n    access = None\n    flush = None\n    getxattr = None\n    listxattr = None\n    open = None\n    opendir = None\n    release = None\n    releasedir = None\n    statfs = None\n\n    # !rm.yes>\n    if False:\n        # incorrect semantics but good for debugging stuff like samba and msys2\n        def access(self, path, mode):\n            dbg(\"@@ access [{}] [{}]\".format(path, mode))\n            return 1 if self.getattr(path) else 0\n\n        def flush(self, path, fh):\n            dbg(\"@@ flush [{}] [{}]\".format(path, fh))\n            return True\n\n        def getxattr(self, *args):\n            dbg(\"@@ getxattr [{}]\".format(\"] [\".join(str(x) for x in args)))\n            return False\n\n        def listxattr(self, *args):\n            dbg(\"@@ listxattr [{}]\".format(\"] [\".join(str(x) for x in args)))\n            return False\n\n        def open(self, path, flags):\n            dbg(\"@@ open [{}] [{}]\".format(path, flags))\n            return 42\n\n        def opendir(self, fh):\n            dbg(\"@@ opendir [{}]\".format(fh))\n            return 69\n\n        def release(self, ino, fi):\n            dbg(\"@@ release [{}] [{}]\".format(ino, fi))\n            return True\n\n        def releasedir(self, ino, fi):\n            dbg(\"@@ releasedir [{}] [{}]\".format(ino, fi))\n            return True\n\n        def statfs(self, path):\n            dbg(\"@@ statfs [{}]\".format(path))\n            return {}\n\n    # !rm.no>\n\n    if sys.platform == \"win32\":\n        # quick compat for /mingw64/bin/python3 (msys2)\n        def _open(self, path):\n            try:\n                x = self.getattr(path)\n                if x[\"st_mode\"] <= 0:\n                    raise Exception()\n\n                self.junk_fh_ctr += 1\n                if self.junk_fh_ctr > 32000:  # TODO untested\n                    self.junk_fh_ctr = 4\n\n                return self.junk_fh_ctr\n\n            except Exception as ex:\n                info(\"open ERR %r\", ex)\n                raise FuseOSError(errno.ENOENT)\n\n        def open(self, path, flags):\n            dbg(\"open %r [%s]\", path, flags)\n            return self._open(path)\n\n        def opendir(self, path):\n            dbg(\"opendir %r\", path)\n            return self._open(path)\n\n        def flush(self, path, fh):\n            dbg(\"flush %r [%s]\", path, fh)\n\n        def release(self, ino, fi):\n            dbg(\"release %r [%s]\", ino, fi)\n\n        def releasedir(self, ino, fi):\n            dbg(\"releasedir %r [%s]\", ino, fi)\n\n        def access(self, path, mode):\n            dbg(\"access %r [%s]\", path, mode)\n            try:\n                x = self.getattr(path)\n                if x[\"st_mode\"] <= 0:\n                    raise Exception()\n            except:\n                raise FuseOSError(errno.ENOENT)\n\n\nclass TheArgparseFormatter(\n    argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter\n):\n    pass\n\n\ndef main():\n    global info, dbg, is_dbg\n    time.strptime(\"19970815\", \"%Y%m%d\")  # python#7980\n\n    ver = \"{0}, v{1}\".format(S_BUILD_DT, S_VERSION)\n    if \"--version\" in sys.argv:\n        print(\"partyfuse\", ver)\n        return\n\n    # filecache helps for reads that are ~64k or smaller;\n    #   windows likes to use 4k and 64k so cache is important,\n    #   linux generally does 128k so the cache is still nice,\n    #   value is numChunks (1~8M each) to keep in the cache\n    nf = 12\n\n    # dircache is always a boost,\n    #   only want to disable it for tests etc,\n    cdn = 24  # max num dirs; keep larger than max dir depth; 0=disable\n    cds = 1  # numsec until an entry goes stale\n\n    where = \"local directory\"\n    if WINDOWS:\n        where += \" or DRIVE:\"\n\n    ex_pre = \"\\n  \" + os.path.basename(__file__) + \"  \"\n    examples = [\"http://192.168.1.69:3923/music/  ./music\"]\n    if WINDOWS:\n        examples.append(\"http://192.168.1.69:3923/music/  M:\")\n\n    epi = \"example:\" + ex_pre + ex_pre.join(examples)\n    epi += \"\"\"\\n\nNOTE: if server has --usernames enabled, then password is \"username:password\"\n\"\"\"\n\n    ap = argparse.ArgumentParser(\n        formatter_class=TheArgparseFormatter,\n        description=\"mount a copyparty server as a local filesystem -- \" + ver,\n        epilog=epi,\n    )\n    # fmt: off\n    ap.add_argument(\"base_url\", type=str, help=\"remote copyparty URL to mount\")\n    ap.add_argument(\"local_path\", type=str, help=where + \" to mount it on\")\n    ap.add_argument(\"-a\", metavar=\"PASSWORD\", help=\"password or $filepath\")\n\n    # !rm.yes>\n    ap.add_argument(\"--html\", metavar=\"TYPE\", default=\"\", help=\"which HTML parser to use; cpp, nginx, iis\")\n    # !rm.no>\n\n    ap2 = ap.add_argument_group(\"https/TLS\")\n    ap2.add_argument(\"-te\", metavar=\"PEMFILE\", help=\"certificate to expect/verify\")\n    ap2.add_argument(\"-teh\", action=\"store_true\", help=\"require correct hostname in -te cert\")\n    ap2.add_argument(\"-td\", action=\"store_true\", help=\"disable certificate check\")\n\n    ap2 = ap.add_argument_group(\"cache/perf\")\n    ap2.add_argument(\"-cdn\", metavar=\"DIRS\", type=float, default=cdn, help=\"directory-cache, max num dirs; 0=disable\")\n    ap2.add_argument(\"-cds\", metavar=\"SECS\", type=float, default=cds, help=\"directory-cache, expiration time\")\n    ap2.add_argument(\"-cf\", metavar=\"BLOCKS\", type=int, default=nf, help=\"file cache; each block is <= 1 MiB\")\n\n    ap2 = ap.add_argument_group(\"logging\")\n    ap2.add_argument(\"-q\", action=\"store_true\", help=\"quiet\")\n    ap2.add_argument(\"-d\", action=\"store_true\", help=\"debug/verbose\")\n    ap2.add_argument(\"--slowterm\", action=\"store_true\", help=\"only most recent msgs; good for windows\")\n    ap2.add_argument(\"--logf\", metavar=\"FILE\", type=str, default=\"\", help=\"log to FILE; enables --slowterm\")\n\n    ap2 = ap.add_argument_group(\"fuse\")\n    ap2.add_argument(\"--oth\", action=\"store_true\", help=\"tell FUSE to '-o allow_other'\")\n    ap2.add_argument(\"--nonempty\", action=\"store_true\", help=\"tell FUSE to '-o nonempty'\")\n\n    ar = ap.parse_args()\n    # fmt: on\n\n    if ar.logf:\n        ar.slowterm = True\n\n    # windows terminals are slow (cmd.exe, mintty)\n    # otoh fancy_log beats RecentLog on linux\n    logger = RecentLog(ar).put if ar.slowterm else fancy_log\n    if ar.d:\n        info = logger\n        dbg = logger\n        is_dbg = True\n    elif not ar.q:\n        info = logger\n\n    if ar.a and ar.a.startswith(\"$\"):\n        fn = ar.a[1:]\n        info(\"reading password from file %r\", fn)\n        with open(fn, \"rb\") as f:\n            ar.a = f.read().decode(\"utf-8\").strip()\n\n    if WINDOWS:\n        os.system(\"rem\")\n\n        for ch in '<>:\"\\\\|?*':\n            # microsoft maps illegal characters to f0xx\n            # (e000 to f8ff is basic-plane private-use)\n            bad_good[ch] = chr(ord(ch) + 0xF000)\n\n        for n in range(0, 0x100):\n            # map surrogateescape to another private-use area\n            bad_good[chr(n + 0xDC00)] = chr(n + 0xF100)\n\n        for k, v in bad_good.items():\n            good_bad[v] = k\n\n    register_wtf8()\n\n    args = {\"foreground\": True, \"nothreads\": True}\n    if ar.oth:\n        args[\"allow_other\"] = True\n    if ar.nonempty:\n        args[\"nonempty\"] = True\n\n    FUSE(CPPF(ar), ar.local_path, encoding=\"wtf-8\", **args)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/partyfuse2.py",
    "content": "#!/usr/bin/env python3\nfrom __future__ import print_function, unicode_literals\n\n\"\"\"partyfuse2: remote copyparty as a local filesystem\"\"\"\n__author__ = \"ed <copyparty@ocv.me>\"\n__copyright__ = 2020\n__license__ = \"MIT\"\n__url__ = \"https://github.com/9001/copyparty/\"\n\nimport re\nimport os\nimport sys\nimport time\nimport json\nimport stat\nimport errno\nimport struct\nimport codecs\nimport platform\nimport threading\nimport http.client  # py2: httplib\nimport urllib.parse\nfrom datetime import datetime\nfrom urllib.parse import quote_from_bytes as quote\nfrom urllib.parse import unquote_to_bytes as unquote\n\ntry:\n    import fuse\n    from fuse import Fuse\n\n    fuse.fuse_python_api = (0, 2)\n    if not hasattr(fuse, \"__version__\"):\n        raise Exception(\"your fuse-python is way old\")\nexcept:\n    if WINDOWS:\n        libfuse = \"install https://github.com/billziss-gh/winfsp/releases/latest\"\n    elif MACOS:\n        libfuse = \"install https://osxfuse.github.io/\"\n    else:\n        libfuse = \"apt install libfuse\\n    modprobe fuse\"\n\n    m = \"\"\"\\033[33m\n  could not import fuse; these may help:\n    {} -m pip install --user fuse-python\n    {}\n\\033[0m\"\"\"\n    print(m.format(sys.executable, libfuse))\n    raise\n\n\n\"\"\"\nmount a copyparty server (local or remote) as a filesystem\n\nusage:\n  python ./partyfuse2.py -f -o allow_other,auto_unmount,nonempty,pw=wark,url=http://192.168.1.69:3923 /mnt/nas\n\ndependencies:\n  sudo apk add fuse-dev python3-dev\n  python3 -m pip install --user fuse-python\n\nfork of partyfuse.py based on fuse-python which\n  appears to be more compliant than fusepy? since this works with samba\n    (probably just my garbage code tbh)\n\"\"\"\n\n\nWINDOWS = sys.platform == \"win32\"\nMACOS = platform.system() == \"Darwin\"\n\n\ndef threadless_log(msg):\n    print(msg + \"\\n\", end=\"\")\n\n\ndef boring_log(msg):\n    msg = \"\\033[36m{:012x}\\033[0m {}\\n\".format(threading.current_thread().ident, msg)\n    print(msg[4:], end=\"\")\n\n\ndef rice_tid():\n    tid = threading.current_thread().ident\n    c = struct.unpack(b\"B\" * 5, struct.pack(b\">Q\", tid)[-5:])\n    return \"\".join(\"\\033[1;37;48;5;{}m{:02x}\".format(x, x) for x in c) + \"\\033[0m\"\n\n\ndef fancy_log(msg):\n    print(\"{} {}\\n\".format(rice_tid(), msg), end=\"\")\n\n\ndef null_log(msg):\n    pass\n\n\ninfo = fancy_log\nlog = fancy_log\ndbg = fancy_log\nlog = null_log\ndbg = null_log\n\n\ndef get_tid():\n    return threading.current_thread().ident\n\n\ndef html_dec(txt):\n    return (\n        txt.replace(\"&lt;\", \"<\")\n        .replace(\"&gt;\", \">\")\n        .replace(\"&quot;\", '\"')\n        .replace(\"&amp;\", \"&\")\n    )\n\n\ndef register_wtf8():\n    def wtf8_enc(text):\n        return str(text).encode(\"utf-8\", \"surrogateescape\"), len(text)\n\n    def wtf8_dec(binary):\n        return bytes(binary).decode(\"utf-8\", \"surrogateescape\"), len(binary)\n\n    def wtf8_search(encoding_name):\n        return codecs.CodecInfo(wtf8_enc, wtf8_dec, name=\"wtf-8\")\n\n    codecs.register(wtf8_search)\n\n\nbad_good = {}\ngood_bad = {}\n\n\ndef enwin(txt):\n    return \"\".join([bad_good.get(x, x) for x in txt])\n\n    for bad, good in bad_good.items():\n        txt = txt.replace(bad, good)\n\n    return txt\n\n\ndef dewin(txt):\n    return \"\".join([good_bad.get(x, x) for x in txt])\n\n    for bad, good in bad_good.items():\n        txt = txt.replace(good, bad)\n\n    return txt\n\n\nclass CacheNode(object):\n    def __init__(self, tag, data):\n        self.tag = tag\n        self.data = data\n        self.ts = time.time()\n\n\nclass Stat(fuse.Stat):\n    def __init__(self):\n        self.st_mode = 0\n        self.st_ino = 0\n        self.st_dev = 0\n        self.st_nlink = 1\n        self.st_uid = 1000\n        self.st_gid = 1000\n        self.st_size = 0\n        self.st_atime = 0\n        self.st_mtime = 0\n        self.st_ctime = 0\n\n\nclass Gateway(object):\n    def __init__(self, base_url, pw):\n        self.base_url = base_url\n        self.pw = pw\n\n        ui = urllib.parse.urlparse(base_url)\n        self.web_root = ui.path.strip(\"/\")\n        try:\n            self.web_host, self.web_port = ui.netloc.split(\":\")\n            self.web_port = int(self.web_port)\n        except:\n            self.web_host = ui.netloc\n            if ui.scheme == \"http\":\n                self.web_port = 80\n            elif ui.scheme == \"https\":\n                raise Exception(\"todo\")\n            else:\n                raise Exception(\"bad url?\")\n\n        self.conns = {}\n\n    def quotep(self, path):\n        path = path.encode(\"wtf-8\")\n        return quote(path, safe=\"/\")\n\n    def getconn(self, tid=None):\n        tid = tid or get_tid()\n        try:\n            return self.conns[tid]\n        except:\n            info(\"new conn [{}] [{}]\".format(self.web_host, self.web_port))\n\n            conn = http.client.HTTPConnection(self.web_host, self.web_port, timeout=260)\n\n            self.conns[tid] = conn\n            return conn\n\n    def closeconn(self, tid=None):\n        tid = tid or get_tid()\n        try:\n            self.conns[tid].close()\n            del self.conns[tid]\n        except:\n            pass\n\n    def sendreq(self, *args, **ka):\n        tid = get_tid()\n        if self.pw:\n            ck = \"cppwd=\" + self.pw\n            try:\n                ka[\"headers\"][\"Cookie\"] = ck\n            except:\n                ka[\"headers\"] = {\"Cookie\": ck}\n        try:\n            c = self.getconn(tid)\n            c.request(*list(args), **ka)\n            return c.getresponse()\n        except:\n            self.closeconn(tid)\n            c = self.getconn(tid)\n            c.request(*list(args), **ka)\n            return c.getresponse()\n\n    def listdir(self, path):\n        if bad_good:\n            path = dewin(path)\n\n        web_path = self.quotep(\"/\" + \"/\".join([self.web_root, path])) + \"?dots&ls\"\n        r = self.sendreq(\"GET\", web_path)\n        if r.status != 200:\n            self.closeconn()\n            raise Exception(\n                \"http error {} reading dir {} in {}\".format(\n                    r.status, web_path, rice_tid()\n                )\n            )\n\n        return self.parse_jls(r)\n\n    def download_file_range(self, path, ofs1, ofs2):\n        if bad_good:\n            path = dewin(path)\n\n        web_path = self.quotep(\"/\" + \"/\".join([self.web_root, path])) + \"?raw\"\n        hdr_range = \"bytes={}-{}\".format(ofs1, ofs2 - 1)\n        log(\"downloading {}\".format(hdr_range))\n\n        r = self.sendreq(\"GET\", web_path, headers={\"Range\": hdr_range})\n        if r.status != http.client.PARTIAL_CONTENT:\n            self.closeconn()\n            raise Exception(\n                \"http error {} reading file {} range {} in {}\".format(\n                    r.status, web_path, hdr_range, rice_tid()\n                )\n            )\n\n        return r.read()\n\n    def parse_jls(self, datasrc):\n        rsp = b\"\"\n        while True:\n            buf = datasrc.read(1024 * 32)\n            if not buf:\n                break\n\n            rsp += buf\n\n        rsp = json.loads(rsp.decode(\"utf-8\"))\n        ret = []\n        for statfun, nodes in [\n            [self.stat_dir, rsp[\"dirs\"]],\n            [self.stat_file, rsp[\"files\"]],\n        ]:\n            for n in nodes:\n                fname = unquote(n[\"href\"].split(\"?\")[0]).rstrip(b\"/\").decode(\"wtf-8\")\n                if bad_good:\n                    fname = enwin(fname)\n\n                ret.append([fname, statfun(n[\"ts\"], n[\"sz\"]), 0])\n\n        return ret\n\n    def stat_dir(self, ts, sz=4096):\n        ret = Stat()\n        ret.st_mode = stat.S_IFDIR | 0o555\n        ret.st_nlink = 2\n        ret.st_size = sz\n        ret.st_atime = ts\n        ret.st_mtime = ts\n        ret.st_ctime = ts\n        return ret\n\n    def stat_file(self, ts, sz):\n        ret = Stat()\n        ret.st_mode = stat.S_IFREG | 0o444\n        ret.st_size = sz\n        ret.st_atime = ts\n        ret.st_mtime = ts\n        ret.st_ctime = ts\n        return ret\n\n\nclass CPPF(Fuse):\n    def __init__(self, *args, **kwargs):\n        Fuse.__init__(self, *args, **kwargs)\n\n        self.url = None\n        self.pw = None\n\n        self.dircache = []\n        self.dircache_mtx = threading.Lock()\n\n        self.filecache = []\n        self.filecache_mtx = threading.Lock()\n\n    def init2(self):\n        # TODO figure out how python-fuse wanted this to go\n        self.gw = Gateway(self.url, self.pw)  # .decode('utf-8'))\n        info(\"up\")\n\n    def clean_dircache(self):\n        \"\"\"not threadsafe\"\"\"\n        now = time.time()\n        cutoff = 0\n        for cn in self.dircache:\n            if now - cn.ts > 1:\n                cutoff += 1\n            else:\n                break\n\n        if cutoff > 0:\n            self.dircache = self.dircache[cutoff:]\n\n    def get_cached_dir(self, dirpath):\n        # with self.dircache_mtx:\n        if True:\n            self.clean_dircache()\n            for cn in self.dircache:\n                if cn.tag == dirpath:\n                    return cn\n\n        return None\n\n    \"\"\"\n            ,-------------------------------,  g1>=c1, g2<=c2\n            |cache1                   cache2|  buf[g1-c1:(g1-c1)+(g2-g1)]\n            `-------------------------------'\n                    ,---------------,\n                    |get1       get2|\n                    `---------------'\n    __________________________________________________________________________\n\n            ,-------------------------------,  g2<=c2, (g2>=c1)\n            |cache1                   cache2|  cdr=buf[:g2-c1]\n            `-------------------------------'  dl car; g1-512K:c1\n    ,---------------,\n    |get1       get2|\n    `---------------'\n    __________________________________________________________________________\n\n            ,-------------------------------,  g1>=c1, (g1<=c2)\n            |cache1                   cache2|  car=buf[c2-g1:]\n            `-------------------------------'  dl cdr; c2:c2+1M\n                                    ,---------------,\n                                    |get1       get2|\n                                    `---------------'\n    \"\"\"\n\n    def get_cached_file(self, path, get1, get2, file_sz):\n        car = None\n        cdr = None\n        ncn = -1\n        # with self.filecache_mtx:\n        if True:\n            dbg(\"cache request from {} to {}, size {}\".format(get1, get2, file_sz))\n            for cn in self.filecache:\n                ncn += 1\n\n                cache_path, cache1 = cn.tag\n                if cache_path != path:\n                    continue\n\n                cache2 = cache1 + len(cn.data)\n                if get2 <= cache1 or get1 >= cache2:\n                    continue\n\n                if get1 >= cache1 and get2 <= cache2:\n                    # keep cache entry alive by moving it to the end\n                    self.filecache = (\n                        self.filecache[:ncn] + self.filecache[ncn + 1 :] + [cn]\n                    )\n                    buf_ofs = get1 - cache1\n                    buf_end = buf_ofs + (get2 - get1)\n                    dbg(\n                        \"found all ({}, {} to {}, len {}) [{}:{}] = {}\".format(\n                            ncn,\n                            cache1,\n                            cache2,\n                            len(cn.data),\n                            buf_ofs,\n                            buf_end,\n                            buf_end - buf_ofs,\n                        )\n                    )\n                    return cn.data[buf_ofs:buf_end]\n\n                if get2 < cache2:\n                    x = cn.data[: get2 - cache1]\n                    if not cdr or len(cdr) < len(x):\n                        dbg(\n                            \"found car ({}, {} to {}, len {}) [:{}-{}] = [:{}] = {}\".format(\n                                ncn,\n                                cache1,\n                                cache2,\n                                len(cn.data),\n                                get2,\n                                cache1,\n                                get2 - cache1,\n                                len(x),\n                            )\n                        )\n                        cdr = x\n\n                    continue\n\n                if get1 > cache1:\n                    x = cn.data[-(cache2 - get1) :]\n                    if not car or len(car) < len(x):\n                        dbg(\n                            \"found cdr ({}, {} to {}, len {}) [-({}-{}):] = [-{}:] = {}\".format(\n                                ncn,\n                                cache1,\n                                cache2,\n                                len(cn.data),\n                                cache2,\n                                get1,\n                                cache2 - get1,\n                                len(x),\n                            )\n                        )\n                        car = x\n\n                    continue\n\n                raise Exception(\"what\")\n\n        if car and cdr:\n            dbg(\"<cache> have both\")\n\n            ret = car + cdr\n            if len(ret) == get2 - get1:\n                return ret\n\n            raise Exception(\"{} + {} != {} - {}\".format(len(car), len(cdr), get2, get1))\n\n        elif cdr:\n            h_end = get1 + (get2 - get1) - len(cdr)\n            h_ofs = h_end - 512 * 1024\n\n            if h_ofs < 0:\n                h_ofs = 0\n\n            buf_ofs = (get2 - get1) - len(cdr)\n\n            dbg(\n                \"<cache> cdr {}, car {}-{}={} [-{}:]\".format(\n                    len(cdr), h_ofs, h_end, h_end - h_ofs, buf_ofs\n                )\n            )\n\n            buf = self.gw.download_file_range(path, h_ofs, h_end)\n            ret = buf[-buf_ofs:] + cdr\n\n        elif car:\n            h_ofs = get1 + len(car)\n            h_end = h_ofs + 1024 * 1024\n\n            if h_end > file_sz:\n                h_end = file_sz\n\n            buf_ofs = (get2 - get1) - len(car)\n\n            dbg(\n                \"<cache> car {}, cdr {}-{}={} [:{}]\".format(\n                    len(car), h_ofs, h_end, h_end - h_ofs, buf_ofs\n                )\n            )\n\n            buf = self.gw.download_file_range(path, h_ofs, h_end)\n            ret = car + buf[:buf_ofs]\n\n        else:\n            h_ofs = get1 - 256 * 1024\n            h_end = get2 + 1024 * 1024\n\n            if h_ofs < 0:\n                h_ofs = 0\n\n            if h_end > file_sz:\n                h_end = file_sz\n\n            buf_ofs = get1 - h_ofs\n            buf_end = buf_ofs + get2 - get1\n\n            dbg(\n                \"<cache> {}-{}={} [{}:{}]\".format(\n                    h_ofs, h_end, h_end - h_ofs, buf_ofs, buf_end\n                )\n            )\n\n            buf = self.gw.download_file_range(path, h_ofs, h_end)\n            ret = buf[buf_ofs:buf_end]\n\n        cn = CacheNode([path, h_ofs], buf)\n        # with self.filecache_mtx:\n        if True:\n            if len(self.filecache) > 6:\n                self.filecache = self.filecache[1:] + [cn]\n            else:\n                self.filecache.append(cn)\n\n        return ret\n\n    def _readdir(self, path):\n        path = path.strip(\"/\")\n        log(\"readdir {}\".format(path))\n\n        ret = self.gw.listdir(path)\n\n        # with self.dircache_mtx:\n        if True:\n            cn = CacheNode(path, ret)\n            self.dircache.append(cn)\n            self.clean_dircache()\n\n        return ret\n\n    def readdir(self, path, offset):\n        for e in self._readdir(path)[offset:]:\n            # log(\"yield [{}]\".format(e[0]))\n            yield fuse.Direntry(e[0])\n\n    def open(self, path, flags):\n        if (flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)) != os.O_RDONLY:\n            return -errno.EACCES\n\n        st = self.getattr(path)\n        try:\n            if st.st_nlink > 0:\n                return st\n        except:\n            return st  # -int(os.errcode)\n\n    def read(self, path, length, offset, fh=None, *args):\n        if args:\n            log(\"unexpected args [\" + \"] [\".join(repr(x) for x in args) + \"]\")\n            raise Exception()\n\n        path = path.strip(\"/\")\n\n        ofs2 = offset + length\n        log(\"read {} @ {} len {} end {}\".format(path, offset, length, ofs2))\n\n        st = self.getattr(path)\n        try:\n            file_sz = st.st_size\n        except:\n            return st  # -int(os.errcode)\n\n        if ofs2 > file_sz:\n            ofs2 = file_sz\n            log(\"truncate to len {} end {}\".format(ofs2 - offset, ofs2))\n\n        if file_sz == 0 or offset >= ofs2:\n            return b\"\"\n\n        # toggle cache here i suppose\n        # return self.get_cached_file(path, offset, ofs2, file_sz)\n        return self.gw.download_file_range(path, offset, ofs2)\n\n    def getattr(self, path):\n        log(\"getattr [{}]\".format(path))\n        if WINDOWS:\n            path = enwin(path)  # windows occasionally decodes f0xx to xx\n\n        path = path.strip(\"/\")\n        try:\n            dirpath, fname = path.rsplit(\"/\", 1)\n        except:\n            dirpath = \"\"\n            fname = path\n\n        if not path:\n            ret = self.gw.stat_dir(time.time())\n            dbg(\"=root\")\n            return ret\n\n        cn = self.get_cached_dir(dirpath)\n        if cn:\n            log(\"cache ok\")\n            dents = cn.data\n        else:\n            log(\"cache miss\")\n            dents = self._readdir(dirpath)\n\n        for cache_name, cache_stat, _ in dents:\n            if cache_name == fname:\n                dbg(\"=file\")\n                return cache_stat\n\n        log(\"=404\")\n        return -errno.ENOENT\n\n\ndef main():\n    time.strptime(\"19970815\", \"%Y%m%d\")  # python#7980\n    register_wtf8()\n    if WINDOWS:\n        os.system(\"rem\")\n\n        for ch in '<>:\"\\\\|?*':\n            # microsoft maps illegal characters to f0xx\n            # (e000 to f8ff is basic-plane private-use)\n            bad_good[ch] = chr(ord(ch) + 0xF000)\n\n        for n in range(0, 0x100):\n            # map surrogateescape to another private-use area\n            bad_good[chr(n + 0xDC00)] = chr(n + 0xF100)\n\n        for k, v in bad_good.items():\n            good_bad[v] = k\n\n    server = CPPF()\n    server.parser.add_option(mountopt=\"url\", metavar=\"BASE_URL\", default=None)\n    server.parser.add_option(mountopt=\"pw\", metavar=\"PASSWORD\", default=None)\n    server.parse(values=server, errex=1)\n    if not server.url or not str(server.url).startswith(\"http\"):\n        print(\"\\nerror:\")\n        print(\"  need argument: -o url=<...>\")\n        print(\"  need argument: mount-path\")\n        print(\"example:\")\n        print(\n            \"  ./partyfuse2.py -f -o allow_other,auto_unmount,nonempty,pw=wark,url=http://192.168.1.69:3923 /mnt/nas\"\n        )\n        sys.exit(1)\n\n    server.init2()\n    threading.Thread(target=server.main, daemon=True).start()\n    while True:\n        time.sleep(9001)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/partyjournal.py",
    "content": "#!/usr/bin/env python3\n\n\"\"\"\npartyjournal.py: chronological history of uploads\n2021-12-31, v0.1, ed <irc.rizon.net>, MIT-Licensed\nhttps://github.com/9001/copyparty/blob/hovudstraum/bin/partyjournal.py\n\nproduces a chronological list of all uploads,\nby collecting info from up2k databases and the filesystem\n\nspecify subnet `192.168.1.*` with argument `.=192.168.1.`,\naffecting all successive mappings\n\nusage:\n  ./partyjournal.py > partyjournal.html .=192.168.1. cart=125 steen=114 steen=131 sleepy=121 fscarlet=144 ed=101 ed=123\n\n\"\"\"\n\nimport sys\nimport base64\nimport sqlite3\nimport argparse\nfrom datetime import datetime, timezone\nfrom urllib.parse import quote_from_bytes as quote\nfrom urllib.parse import unquote_to_bytes as unquote\n\n\nFS_ENCODING = sys.getfilesystemencoding()\nUTC = timezone.utc\n\n\nclass APF(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):\n    pass\n\n\n##\n## snibbed from copyparty\n\n\ndef s3dec(v):\n    if not v.startswith(\"//\"):\n        return v\n\n    v = base64.urlsafe_b64decode(v.encode(\"ascii\")[2:])\n    return v.decode(FS_ENCODING, \"replace\")\n\n\ndef quotep(txt):\n    btxt = txt.encode(\"utf-8\", \"replace\")\n    quot1 = quote(btxt, safe=b\"/\")\n    quot1 = quot1.encode(\"ascii\")\n    quot2 = quot1.replace(b\" \", b\"+\")\n    return quot2.decode(\"utf-8\", \"replace\")\n\n\ndef html_escape(s, quote=False, crlf=False):\n    \"\"\"html.escape but also newlines\"\"\"\n    s = s.replace(\"&\", \"&amp;\").replace(\"<\", \"&lt;\").replace(\">\", \"&gt;\")\n    if quote:\n        s = s.replace('\"', \"&quot;\").replace(\"'\", \"&#x27;\")\n    if crlf:\n        s = s.replace(\"\\r\", \"&#13;\").replace(\"\\n\", \"&#10;\")\n\n    return s\n\n\n## end snibs\n##\n\n\ndef main():\n    ap = argparse.ArgumentParser(formatter_class=APF)\n    ap.add_argument(\"who\", nargs=\"*\")\n    ar = ap.parse_args()\n\n    imap = {}\n    subnet = \"\"\n    for v in ar.who:\n        if \"=\" not in v:\n            raise Exception(\"bad who: \" + v)\n\n        k, v = v.split(\"=\")\n        if k == \".\":\n            subnet = v\n            continue\n\n        imap[\"{}{}\".format(subnet, v)] = k\n\n    print(repr(imap), file=sys.stderr)\n\n    print(\n        \"\"\"\\\n<!DOCTYPE html>\n<html lang=\"en\">\n<head><meta charset=\"utf-8\"><style>\n\nhtml, body {\n    color: #ccc;\n    background: #222;\n    font-family: sans-serif;\n}\na {\n    color: #fc5;\n}\ntd, th {\n    padding: .2em .5em;\n    border: 1px solid #999;\n    border-width: 0 1px 1px 0;\n    white-space: nowrap;\n}\ntd:nth-child(1),\ntd:nth-child(2),\ntd:nth-child(3) {\n    font-family: monospace, monospace;\n    text-align: right;\n}\ntr:first-child {\n    position: sticky;\n    top: -1px;\n}\nth {\n    background: #222;\n    text-align: left;\n}\n\n</style></head><body><table><tr>\n    <th>wark</th>\n    <th>time</th>\n    <th>size</th>\n    <th>who</th>\n    <th>link</th>\n</tr>\"\"\"\n    )\n\n    db_path = \".hist/up2k.db\"\n    conn = sqlite3.connect(db_path)\n    q = r\"pragma table_info(up)\"\n    inf = conn.execute(q).fetchall()\n    cols = [x[1] for x in inf]\n    print(\"<!-- \" + str(cols) + \" -->\")\n    # ['w', 'mt', 'sz', 'rd', 'fn', 'ip', 'at']\n\n    q = r\"select * from up order by case when at > 0 then at else mt end\"\n    for w, mt, sz, rd, fn, ip, at in conn.execute(q):\n        link = \"/\".join([s3dec(x) for x in [rd, fn] if x])\n        if fn.startswith(\"put-\") and sz < 4096:\n            try:\n                with open(link, \"rb\") as f:\n                    txt = f.read().decode(\"utf-8\", \"replace\")\n            except:\n                continue\n\n            if txt.startswith(\"msg=\"):\n                txt = txt.encode(\"utf-8\", \"replace\")\n                txt = unquote(txt.replace(b\"+\", b\" \"))\n                link = txt.decode(\"utf-8\")[4:]\n\n        sz = \"{:,}\".format(sz)\n        dt = datetime.fromtimestamp(at if at > 0 else mt, UTC)\n        v = [\n            w[:16],\n            dt.strftime(\"%Y-%m-%d %H:%M:%S\"),\n            sz,\n            imap.get(ip, ip),\n        ]\n\n        row = \"<tr>\\n  \"\n        row += \"\\n  \".join([\"<td>{}</th>\".format(x) for x in v])\n        row += '\\n  <td><a href=\"{}\">{}</a></td>'.format(link, html_escape(link))\n        row += \"\\n</tr>\"\n        print(row)\n\n    print(\"</table></body></html>\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/prisonparty.sh",
    "content": "#!/bin/bash\nset -e\n\n# runs copyparty (or any other program really) in a chroot\n#\n# assumption: these directories, and everything within, are owned by root\nsysdirs=(); for v in /bin /lib /lib32 /lib64 /sbin /usr /etc/alternatives ; do\n\t[ -e $v ] && sysdirs+=($v)\ndone\n\n# error-handler\nhelp() { cat <<'EOF'\n\nusage:\n  ./prisonparty.sh <ROOTDIR> <USER|UID> <GROUP|GID> [VOLDIR [VOLDIR...]] -- python3 copyparty-sfx.py [...]\n\nexample:\n  ./prisonparty.sh /var/lib/copyparty-jail cpp cpp /mnt/nas/music -- python3 copyparty-sfx.py -v /mnt/nas/music::rwmd\n\nexample for running straight from source (instead of using an sfx):\n  PYTHONPATH=$PWD ./prisonparty.sh /var/lib/copyparty-jail cpp cpp /mnt/nas/music -- python3 -um copyparty -v /mnt/nas/music::rwmd\n\nnote that if you have python modules installed as --user (such as bpm/key detectors),\n  you should add /home/foo/.local as a VOLDIR\n\nEOF\nexit 1\n}\n\n\nerrs=\nfor c in awk chroot dirname getent lsof mknod mount realpath sed sort stat uniq; do\n\tcommand -v $c >/dev/null || {\n\t\techo ERROR: command not found: $c\n\t\terrs=1\n\t}\ndone\n[ $errs ] && exit 1\n\n\n# read arguments\ntrap help EXIT\njail=\"$(realpath \"$1\")\"; shift\nuid=\"$1\"; shift\ngid=\"$1\"; shift\n\nvols=()\nwhile true; do\n\tv=\"$1\"; shift\n\t[ \"$v\" = -- ] && break  # end of volumes\n\t[ \"$#\" -eq 0 ] && break  # invalid usage\n\tvols+=( \"$(realpath \"$v\" || echo \"$v\")\" )\ndone\npybin=\"$1\"; shift\npybin=\"$(command -v \"$pybin\")\"\npyarg=\nwhile true; do\n\tv=\"$1\"\n\t[ \"${v:0:1}\" = - ] || break\n\tpyarg=\"$pyarg $v\"\n\tshift\ndone\ncpp=\"$1\"; shift\n[ -d \"$cpp\" ] && cppdir=\"$PWD\" || {\n\t# sfx, not module\n\tcpp=\"$(realpath \"$cpp\")\"\n\tcppdir=\"$(dirname \"$cpp\")\"\n}\ntrap - EXIT\n\nusr=\"$(getent passwd $uid | cut -d: -f1)\"\n[ \"$usr\" ] || { echo \"ERROR invalid username/uid $uid\"; exit 1; }\nuid=\"$(getent passwd $uid | cut -d: -f3)\"\n\ngrp=\"$(getent group $gid | cut -d: -f1)\"\n[ \"$grp\" ] || { echo \"ERROR invalid groupname/gid $gid\"; exit 1; }\ngid=\"$(getent group $gid | cut -d: -f3)\"\n\n# debug/vis\necho\necho \"chroot-dir = $jail\"\necho \"user:group = $uid:$gid ($usr:$grp)\"\necho \" copyparty = $cpp\"\necho\nprintf '\\033[33m%s\\033[0m\\n' \"copyparty can access these folders and all their subdirectories:\"\nfor v in \"${vols[@]}\"; do\n\tprintf '\\033[36m ├─\\033[0m %s \\033[36m ── added by (You)\\033[0m\\n' \"$v\"\ndone\nprintf '\\033[36m ├─\\033[0m %s \\033[36m ── where the copyparty binary is\\033[0m\\n' \"$cppdir\"\nprintf '\\033[36m ╰─\\033[0m %s \\033[36m ── the folder you are currently in\\033[0m\\n' \"$PWD\"\nvols+=(\"$cppdir\" \"$PWD\")\necho\n\n\n# remove any trailing slashes\njail=\"${jail%/}\"\n\n\n# bind-mount system directories and volumes\nfor a in {1..30}; do mkdir \"$jail/.prisonlock\" && break; sleep 0.1; done\nprintf '%s\\n' \"${sysdirs[@]}\" \"${vols[@]}\" | sed -r 's`/$``' | LC_ALL=C sort | uniq |\nwhile IFS= read -r v; do\n\t[ -e \"$v\" ] || {\n\t\tprintf '\\033[1;31mfolder does not exist:\\033[0m %s\\n' \"$v\"\n\t\tcontinue\n\t}\n\ti1=$(stat -c%D.%i \"$v/\"      2>/dev/null || echo a)\n\ti2=$(stat -c%D.%i \"$jail$v/\" 2>/dev/null || echo b)\n\t[ $i1 = $i2 ] && continue\n\tmount | grep -qF \" $jail$v \" && echo wtf $i1 $i2 $v && continue\n\tmkdir -p \"$jail$v\"\n\tmount --bind \"$v\" \"$jail$v\"\ndone\nrmdir \"$jail/.prisonlock\" || true\n\n\ncln() {\n\ttrap - EXIT\n\twait -f -n $p && rv=0 || rv=$?\n\tcd /\n\techo \"stopping chroot...\"\n\tfor a in {1..30}; do mkdir \"$jail/.prisonlock\" && break; sleep 0.1; done\n\tlsof \"$jail\" 2>/dev/null | grep -F \"$jail\" &&\n\t\techo \"chroot is in use; will not unmount\" ||\n\t{\n\t\tmount | grep -F \" on $jail\" |\n\t\tawk '{sub(/ type .*/,\"\");sub(/.* on /,\"\");print}' |\n\t\tLC_ALL=C sort -r | while IFS= read -r v; do\n\t\t\tumount \"$v\" && echo \"umount OK: $v\"\n\t\tdone\n\t}\n\trmdir \"$jail/.prisonlock\" || true\n\texit $rv\n}\ntrap cln EXIT\n\n\n# create a tmp\nmkdir -p \"$jail/tmp\"\nchmod 777 \"$jail/tmp\"\n\n\n# create a dev\n(cd \"$jail\"; mkdir -p dev; cd dev\n[ -e null ]    || mknod -m 666 null    c 1 3\n[ -e zero ]    || mknod -m 666 zero    c 1 5\n[ -e random ]  || mknod -m 444 random  c 1 8\n[ -e urandom ] || mknod -m 444 urandom c 1 9\n)\n\n\n# run copyparty\nexport HOME=\"$(getent passwd $uid | cut -d: -f6)\"\nexport USER=\"$usr\"\nexport LOGNAME=\"$USER\"\n#echo \"pybin [$pybin]\"\n#echo \"pyarg [$pyarg]\"\n#echo \"cpp [$cpp]\"\nchroot --userspec=$uid:$gid \"$jail\" \"$pybin\" $pyarg \"$cpp\" \"$@\" &\np=$!\ntrap 'kill -USR1 $p' USR1\ntrap 'trap - INT TERM; kill $p' INT TERM\nwait\n"
  },
  {
    "path": "bin/u2c.py",
    "content": "#!/usr/bin/env python3\nfrom __future__ import print_function, unicode_literals\n\nS_VERSION = \"2.19\"\nS_BUILD_DT = \"2026-01-18\"\n\n\"\"\"\nu2c.py: upload to copyparty\n2021, ed <irc.rizon.net>, MIT-Licensed\nhttps://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py\n\n- dependencies: no\n- supports python 2.6, 2.7, and 3.3 through 3.14\n- if something breaks just try again and it'll autoresume\n\"\"\"\n\nimport atexit\nimport base64\nimport binascii\nimport datetime\nimport hashlib\nimport json\nimport math\nimport os\nimport platform\nimport re\nimport signal\nimport socket\nimport stat\nimport sys\nimport threading\nimport time\n\nEXE = bool(getattr(sys, \"frozen\", False))\n\ntry:\n    import argparse\nexcept:\n    m = \"\\n  ERROR: need 'argparse'; download it here:\\n   https://github.com/ThomasWaldmann/argparse/raw/master/argparse.py\\n\"\n    print(m)\n    raise\n\n\nPY2 = sys.version_info < (3,)\nPY27 = sys.version_info > (2, 7) and PY2\nPY37 = sys.version_info > (3, 7)\nif PY2:\n    import httplib as http_client\n    from Queue import Queue\n    from urllib import quote, unquote\n    from urlparse import urlsplit, urlunsplit\n\n    sys.dont_write_bytecode = True\n    bytes = str\n    files_decoder = lambda s: unicode(s, \"utf8\")\nelse:\n    from urllib.parse import quote_from_bytes as quote\n    from urllib.parse import unquote_to_bytes as unquote\n    from urllib.parse import urlsplit, urlunsplit\n\n    import http.client as http_client\n    from queue import Queue\n\n    unicode = str\n    files_decoder = unicode\n\n\nWTF8 = \"replace\" if PY2 else \"surrogateescape\"\n\nVT100 = platform.system() != \"Windows\"\n\n\ntry:\n    UTC = datetime.timezone.utc\nexcept:\n    TD_ZERO = datetime.timedelta(0)\n\n    class _UTC(datetime.tzinfo):\n        def utcoffset(self, dt):\n            return TD_ZERO\n\n        def tzname(self, dt):\n            return \"UTC\"\n\n        def dst(self, dt):\n            return TD_ZERO\n\n    UTC = _UTC()\n\n\ntry:\n    _b64etl = bytes.maketrans(b\"+/\", b\"-_\")\n\n    def ub64enc(bs):\n        x = binascii.b2a_base64(bs, newline=False)\n        return x.translate(_b64etl)\n\n    ub64enc(b\"a\")\nexcept:\n    ub64enc = base64.urlsafe_b64encode\n\n\nclass Fatal(Exception):\n    pass\n\n\nclass Daemon(threading.Thread):\n    def __init__(self, target, name=None, a=None):\n        threading.Thread.__init__(self, name=name)\n        self.a = a or ()\n        self.fun = target\n        self.daemon = True\n        self.start()\n\n    def run(self):\n        try:\n            signal.pthread_sigmask(signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM])\n        except:\n            pass\n\n        self.fun(*self.a)\n\n\nclass HSQueue(Queue):\n    def _init(self, maxsize):\n        from collections import deque\n\n        self.q = deque()\n\n    def _qsize(self):\n        return len(self.q)\n\n    def _put(self, item):\n        if item and item.nhs:\n            self.q.appendleft(item)\n        else:\n            self.q.append(item)\n\n    def _get(self):\n        return self.q.popleft()\n\n\nclass HCli(object):\n    def __init__(self, ar):\n        self.ar = ar\n        url = urlsplit(ar.url)\n        tls = url.scheme.lower() == \"https\"\n        try:\n            addr, port = url.netloc.split(\":\")\n        except:\n            addr = url.netloc\n            port = 443 if tls else 80\n\n        self.addr = addr\n        self.port = int(port)\n        self.tls = tls\n        self.verify = ar.te or not ar.td\n        self.conns = []\n        self.hconns = []\n        if tls:\n            import ssl\n\n            if not self.verify:\n                self.ctx = ssl._create_unverified_context()\n            elif self.verify is True:\n                self.ctx = None\n            else:\n                self.ctx = ssl.create_default_context(cafile=self.verify)\n                self.ctx.check_hostname = ar.teh\n\n        self.base_hdrs = {\n            \"Accept\": \"*/*\",\n            \"Connection\": \"keep-alive\",\n            \"Host\": url.netloc,\n            \"Origin\": self.ar.burl,\n            \"User-Agent\": \"u2c/%s\" % (S_VERSION,),\n        }\n\n    def _connect(self, timeout):\n        args = {}\n        if PY37:\n            args[\"blocksize\"] = 1048576\n\n        if not self.tls:\n            C = http_client.HTTPConnection\n        else:\n            C = http_client.HTTPSConnection\n            if self.ctx:\n                args = {\"context\": self.ctx}\n\n        return C(self.addr, self.port, timeout=timeout, **args)\n\n    def req(self, meth, vpath, hdrs, body=None, ctype=None):\n        now = time.time()\n\n        hdrs.update(self.base_hdrs)\n        if self.ar.a:\n            hdrs[\"PW\"] = self.ar.a\n        if self.ar.ba:\n            hdrs[\"Authorization\"] = self.ar.ba\n        if ctype:\n            hdrs[\"Content-Type\"] = ctype\n        if meth == \"POST\" and CLEN not in hdrs:\n            hdrs[CLEN] = (\n                0 if not body else body.len if hasattr(body, \"len\") else len(body)\n            )\n\n        # large timeout for handshakes (safededup)\n        conns = self.hconns if ctype == MJ else self.conns\n        while conns and self.ar.cxp < now - conns[0][0]:\n            conns.pop(0)[1].close()\n        c = conns.pop()[1] if conns else self._connect(999 if ctype == MJ else 128)\n        try:\n            c.request(meth, vpath, body, hdrs)\n            if PY27:\n                rsp = c.getresponse(buffering=True)\n            else:\n                rsp = c.getresponse()\n\n            data = rsp.read()\n            conns.append((time.time(), c))\n            return rsp.status, data.decode(\"utf-8\")\n        except http_client.BadStatusLine:\n            if self.ar.cxp > 4:\n                t = \"\\nWARNING: --cxp probably too high; reducing from %d to 4\"\n                print(t % (self.ar.cxp,))\n                self.ar.cxp = 4\n            c.close()\n            raise\n        except:\n            c.close()\n            raise\n\n\nMJ = \"application/json\"\nMO = \"application/octet-stream\"\nMM = \"application/x-www-form-urlencoded\"\nCLEN = \"Content-Length\"\n\nweb = None  # type: HCli\n\nlinks = []  # type: list[str]\nlinkmtx = threading.Lock()\nlinkfile = None\n\n\nclass File(object):\n    \"\"\"an up2k upload task; represents a single file\"\"\"\n\n    def __init__(self, top, rel, size, lmod):\n        self.top = top  # type: bytes\n        self.rel = rel.replace(b\"\\\\\", b\"/\")  # type: bytes\n        self.size = size  # type: int\n        self.lmod = lmod  # type: float\n\n        self.abs = os.path.join(top, rel)  # type: bytes\n        self.name = self.rel.split(b\"/\")[-1].decode(\"utf-8\", WTF8)  # type: str\n\n        # set by get_hashlist\n        self.cids = []  # type: list[tuple[str, int, int]]  # [ hash, ofs, sz ]\n        self.kchunks = {}  # type: dict[str, tuple[int, int]]  # hash: [ ofs, sz ]\n        self.t_hash = 0.0  # type: float\n\n        # set by handshake\n        self.recheck = False  # duplicate; redo handshake after all files done\n        self.ucids = []  # type: list[str]  # chunks which need to be uploaded\n        self.wark = \"\"  # type: str\n        self.url = \"\"  # type: str\n        self.nhs = 0  # type: int\n\n        # set by upload\n        self.t0_up = 0.0  # type: float\n        self.t1_up = 0.0  # type: float\n        self.nojoin = 0  # type: int\n        self.up_b = 0  # type: int\n        self.up_c = 0  # type: int\n        self.cd = 0  # type: int\n\n\nclass FileSlice(object):\n    \"\"\"file-like object providing a fixed window into a file\"\"\"\n\n    def __init__(self, file, cids):\n        # type: (File, str) -> None\n\n        self.file = file\n        self.cids = cids\n\n        self.car, tlen = file.kchunks[cids[0]]\n        for cid in cids[1:]:\n            ofs, clen = file.kchunks[cid]\n            if ofs != self.car + tlen:\n                raise Exception(9)\n            tlen += clen\n\n        self.len = self.tlen = tlen\n        self.cdr = self.car + self.len\n        self.ofs = 0  # type: int\n\n        self.f = None\n        self.seek = self._seek0\n        self.read = self._read0\n\n    def subchunk(self, maxsz, nth):\n        if self.tlen <= maxsz:\n            return -1\n\n        if not nth:\n            self.car0 = self.car\n            self.cdr0 = self.cdr\n\n        self.car = self.car0 + maxsz * nth\n        if self.car >= self.cdr0:\n            return -2\n\n        self.cdr = self.car + min(self.cdr0 - self.car, maxsz)\n        self.len = self.cdr - self.car\n        self.seek(0)\n        return nth\n\n    def unsub(self):\n        self.car = self.car0\n        self.cdr = self.cdr0\n        self.len = self.tlen\n\n    def _open(self):\n        self.seek = self._seek\n        self.read = self._read\n\n        self.f = open(self.file.abs, \"rb\", 512 * 1024)\n        self.f.seek(self.car)\n\n        # https://stackoverflow.com/questions/4359495/what-is-exactly-a-file-like-object-in-python\n        # IOBase, RawIOBase, BufferedIOBase\n        funs = \"close closed __enter__ __exit__ __iter__ isatty __next__ readable seekable writable\"\n        try:\n            for fun in funs.split():\n                setattr(self, fun, getattr(self.f, fun))\n        except:\n            pass  # py27 probably\n\n    def close(self, *a, **ka):\n        return  # until _open\n\n    def tell(self):\n        return self.ofs\n\n    def _seek(self, ofs, wh=0):\n        assert self.f  # !rm\n\n        if wh == 1:\n            ofs = self.ofs + ofs\n        elif wh == 2:\n            ofs = self.len + ofs  # provided ofs is negative\n\n        if ofs < 0:\n            ofs = 0\n        elif ofs >= self.len:\n            ofs = self.len - 1\n\n        self.ofs = ofs\n        self.f.seek(self.car + ofs)\n\n    def _read(self, sz):\n        assert self.f  # !rm\n\n        sz = min(sz, self.len - self.ofs)\n        ret = self.f.read(sz)\n        self.ofs += len(ret)\n        return ret\n\n    def _seek0(self, ofs, wh=0):\n        self._open()\n        return self.seek(ofs, wh)\n\n    def _read0(self, sz):\n        self._open()\n        return self.read(sz)\n\n\nclass MTHash(object):\n    def __init__(self, cores):\n        self.f = None\n        self.sz = 0\n        self.csz = 0\n        self.omutex = threading.Lock()\n        self.imutex = threading.Lock()\n        self.work_q = Queue()\n        self.done_q = Queue()\n        self.thrs = []\n        for _ in range(cores):\n            self.thrs.append(Daemon(self.worker))\n\n    def hash(self, f, fsz, chunksz, pcb=None, pcb_opaque=None):\n        with self.omutex:\n            self.f = f\n            self.sz = fsz\n            self.csz = chunksz\n\n            chunks = {}\n            nchunks = int(math.ceil(fsz / chunksz))\n            for nch in range(nchunks):\n                self.work_q.put(nch)\n\n            ex = \"\"\n            for nch in range(nchunks):\n                qe = self.done_q.get()\n                try:\n                    nch, dig, ofs, csz = qe\n                    chunks[nch] = [dig, ofs, csz]\n                except:\n                    ex = ex or qe\n\n                if pcb:\n                    pcb(pcb_opaque, chunksz * nch)\n\n            if ex:\n                raise Exception(ex)\n\n            ret = []\n            for n in range(nchunks):\n                ret.append(chunks[n])\n\n            self.f = None\n            self.csz = 0\n            self.sz = 0\n            return ret\n\n    def worker(self):\n        while True:\n            ofs = self.work_q.get()\n            try:\n                v = self.hash_at(ofs)\n            except Exception as ex:\n                v = str(ex)\n\n            self.done_q.put(v)\n\n    def hash_at(self, nch):\n        f = self.f\n        assert f\n        ofs = ofs0 = nch * self.csz\n        hashobj = hashlib.sha512()\n        chunk_sz = chunk_rem = min(self.csz, self.sz - ofs)\n        while chunk_rem > 0:\n            with self.imutex:\n                f.seek(ofs)\n                buf = f.read(min(chunk_rem, 1024 * 1024 * 12))\n\n            if not buf:\n                raise Exception(\"EOF at \" + str(ofs))\n\n            hashobj.update(buf)\n            chunk_rem -= len(buf)\n            ofs += len(buf)\n\n        digest = ub64enc(hashobj.digest()[:33]).decode(\"utf-8\")\n        return nch, digest, ofs0, chunk_sz\n\n\n_print = print\n\n\ndef safe_print(*a, **ka):\n    ka[\"end\"] = \"\"\n    zs = \" \".join([unicode(x) for x in a])\n    _print(zs + \"\\n\", **ka)\n\n\ndef eprint(*a, **ka):\n    ka[\"file\"] = sys.stderr\n    ka[\"end\"] = \"\"\n    if not PY2:\n        ka[\"flush\"] = True\n\n    _print(*a, **ka)\n    if PY2 or not VT100:\n        sys.stderr.flush()\n\n\ndef flushing_print(*a, **ka):\n    try:\n        safe_print(*a, **ka)\n    except:\n        v = \" \".join(str(x) for x in a)\n        v = v.encode(\"ascii\", \"replace\").decode(\"ascii\")\n        safe_print(v, **ka)\n\n    if \"flush\" not in ka:\n        sys.stdout.flush()\n\n\nprint = safe_print if VT100 else flushing_print\n\n\ndef termsize():\n    try:\n        w, h = os.get_terminal_size()\n        return w, h\n    except:\n        pass\n\n    env = os.environ\n\n    def ioctl_GWINSZ(fd):\n        try:\n            import fcntl\n            import struct\n            import termios\n\n            r = struct.unpack(b\"hh\", fcntl.ioctl(fd, termios.TIOCGWINSZ, b\"AAAA\"))\n            return r[::-1]\n        except:\n            return None\n\n    cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)\n    if not cr:\n        try:\n            fd = os.open(os.ctermid(), os.O_RDONLY)\n            cr = ioctl_GWINSZ(fd)\n            os.close(fd)\n        except:\n            pass\n\n    try:\n        return cr or (int(env[\"COLUMNS\"]), int(env[\"LINES\"]))\n    except:\n        return 80, 25\n\n\nclass CTermsize(object):\n    def __init__(self):\n        self.ev = False\n        self.margin = None\n        self.g = None\n        self.w, self.h = termsize()\n\n        try:\n            signal.signal(signal.SIGWINCH, self.ev_sig)\n        except:\n            return\n\n        Daemon(self.worker)\n\n    def worker(self):\n        while True:\n            time.sleep(0.5)\n            if not self.ev:\n                continue\n\n            self.ev = False\n            self.w, self.h = termsize()\n\n            if self.margin is not None:\n                self.scroll_region(self.margin)\n\n    def ev_sig(self, *a, **ka):\n        self.ev = True\n\n    def scroll_region(self, margin):\n        self.margin = margin\n        if margin is None:\n            self.g = None\n            eprint(\"\\033[s\\033[r\\033[u\")\n        else:\n            self.g = 1 + self.h - margin\n            t = \"%s\\033[%dA\" % (\"\\n\" * margin, margin)\n            eprint(\"%s\\033[s\\033[1;%dr\\033[u\" % (t, self.g - 1))\n\n\nss = CTermsize()\n\n\ndef undns(url):\n    usp = urlsplit(url)\n    hn = usp.hostname\n    gai = None\n    eprint(\"resolving host [%s] ...\" % (hn,))\n    try:\n        gai = socket.getaddrinfo(hn, None)\n        hn = gai[0][4][0]\n    except KeyboardInterrupt:\n        raise\n    except:\n        t = \"\\n\\033[31mfailed to resolve upload destination host;\\033[0m\\ngai=%r\\n\"\n        eprint(t % (gai,))\n        raise\n\n    if usp.port:\n        hn = \"%s:%s\" % (hn, usp.port)\n    if usp.username or usp.password:\n        hn = \"%s:%s@%s\" % (usp.username, usp.password, hn)\n\n    usp = usp._replace(netloc=hn)\n    url = urlunsplit(usp)\n    eprint(\" %s\\n\" % (url,))\n    return url\n\n\ndef _scd(err, top):\n    \"\"\"non-recursive listing of directory contents, along with stat() info\"\"\"\n    top_ = os.path.join(top, b\"\")\n    with os.scandir(top) as dh:\n        for fh in dh:\n            abspath = top_ + fh.name\n            try:\n                yield [abspath, fh.stat()]\n            except Exception as ex:\n                err.append((abspath, str(ex)))\n\n\ndef _lsd(err, top):\n    \"\"\"non-recursive listing of directory contents, along with stat() info\"\"\"\n    top_ = os.path.join(top, b\"\")\n    for name in os.listdir(top):\n        abspath = top_ + name\n        try:\n            yield [abspath, os.stat(abspath)]\n        except Exception as ex:\n            err.append((abspath, str(ex)))\n\n\nif hasattr(os, \"scandir\") and sys.version_info > (3, 6):\n    statdir = _scd\nelse:\n    statdir = _lsd\n\n\ndef walkdir(err, top, excl, seen):\n    \"\"\"recursive statdir\"\"\"\n    atop = os.path.abspath(os.path.realpath(top))\n    if atop in seen:\n        err.append((top, \"recursive-symlink\"))\n        return\n\n    seen = seen[:] + [atop]\n    for ap, inf in sorted(statdir(err, top)):\n        if excl.match(ap):\n            continue\n        if stat.S_ISDIR(inf.st_mode):\n            yield ap, inf\n            try:\n                for x in walkdir(err, ap, excl, seen):\n                    yield x\n            except Exception as ex:\n                err.append((ap, str(ex)))\n        elif stat.S_ISREG(inf.st_mode):\n            yield ap, inf\n        else:\n            err.append((ap, \"irregular filetype 0%o\" % (inf.st_mode,)))\n\n\ndef walkdirs(err, tops, excl):\n    \"\"\"recursive statdir for a list of tops, yields [top, relpath, stat]\"\"\"\n    sep = \"{0}\".format(os.sep).encode(\"ascii\")\n    if not VT100:\n        excl = excl.replace(\"/\", r\"\\\\\")\n        za = []\n        for td in tops:\n            try:\n                ap = os.path.abspath(os.path.realpath(td))\n                if td[-1:] in (b\"\\\\\", b\"/\"):\n                    ap += sep\n            except:\n                # maybe cpython #88013 (ok)\n                ap = td\n\n            za.append(ap)\n\n        za = [x if x.startswith(b\"\\\\\\\\\") else b\"\\\\\\\\?\\\\\" + x for x in za]\n        za = [x.replace(b\"/\", b\"\\\\\") for x in za]\n        tops = za\n\n    ptn = re.compile(excl.encode(\"utf-8\") or b\"\\n\", re.I)\n\n    for top in tops:\n        isdir = os.path.isdir(top)\n        if top[-1:] == sep:\n            stop = top.rstrip(sep)\n            yield stop, b\"\", os.stat(stop)\n        else:\n            stop, dn = os.path.split(top)\n            if isdir:\n                yield stop, dn, os.stat(stop)\n\n        if isdir:\n            for ap, inf in walkdir(err, top, ptn, []):\n                yield stop, ap[len(stop) :].lstrip(sep), inf\n        else:\n            d, n = top.rsplit(sep, 1)\n            yield d or b\"/\", n, os.stat(top)\n\n\n# mostly from copyparty/util.py\ndef quotep(btxt):\n    # type: (bytes) -> bytes\n    quot1 = quote(btxt, safe=b\"/\")\n    if not PY2:\n        quot1 = quot1.encode(\"ascii\")\n\n    return quot1.replace(b\" \", b\"%20\")  # type: ignore\n\n\n# from copyparty/util.py\ndef humansize(sz, terse=False):\n    \"\"\"picks a sensible unit for the given extent\"\"\"\n    for unit in [\"B\", \"KiB\", \"MiB\", \"GiB\", \"TiB\"]:\n        if sz < 1024:\n            break\n\n        sz /= 1024.0\n\n    ret = \" \".join([str(sz)[:4].rstrip(\".\"), unit])\n\n    if not terse:\n        return ret\n\n    return ret.replace(\"iB\", \"\").replace(\" \", \"\")\n\n\n# from copyparty/up2k.py\ndef up2k_chunksize(filesize):\n    \"\"\"gives The correct chunksize for up2k hashing\"\"\"\n    chunksize = 1024 * 1024\n    stepsize = 512 * 1024\n    while True:\n        for mul in [1, 2]:\n            nchunks = math.ceil(filesize * 1.0 / chunksize)\n            if nchunks <= 256 or (chunksize >= 32 * 1024 * 1024 and nchunks <= 4096):\n                return chunksize\n\n            chunksize += stepsize\n            stepsize *= mul\n\n\n# mostly from copyparty/up2k.py\ndef get_hashlist(file, pcb, mth):\n    # type: (File, Any, Any) -> None\n    \"\"\"generates the up2k hashlist from file contents, inserts it into `file`\"\"\"\n\n    chunk_sz = up2k_chunksize(file.size)\n    file_rem = file.size\n    file_ofs = 0\n    ret = []\n    with open(file.abs, \"rb\", 512 * 1024) as f:\n        t0 = time.time()\n\n        if mth and file.size >= 1024 * 512:\n            ret = mth.hash(f, file.size, chunk_sz, pcb, file)\n            file_rem = 0\n\n        while file_rem > 0:\n            # same as `hash_at` except for `imutex` / bufsz\n            hashobj = hashlib.sha512()\n            chunk_sz = chunk_rem = min(chunk_sz, file_rem)\n            while chunk_rem > 0:\n                buf = f.read(min(chunk_rem, 64 * 1024))\n                if not buf:\n                    raise Exception(\"EOF at \" + str(f.tell()))\n\n                hashobj.update(buf)\n                chunk_rem -= len(buf)\n\n            digest = ub64enc(hashobj.digest()[:33]).decode(\"utf-8\")\n\n            ret.append([digest, file_ofs, chunk_sz])\n            file_ofs += chunk_sz\n            file_rem -= chunk_sz\n\n            if pcb:\n                pcb(file, file_ofs)\n\n    file.t_hash = time.time() - t0\n    file.cids = ret\n    file.kchunks = {}\n    for k, v1, v2 in ret:\n        if k not in file.kchunks:\n            file.kchunks[k] = [v1, v2]\n\n\ndef printlink(ar, purl, name, fk):\n    if not name:\n        url = purl  # srch\n    else:\n        name = quotep(name.encode(\"utf-8\", WTF8)).decode(\"utf-8\")\n        if fk:\n            url = \"%s%s?k=%s\" % (purl, name, fk)\n        else:\n            url = \"%s%s\" % (purl, name)\n\n    url = \"%s/%s\" % (ar.burl, url.lstrip(\"/\"))\n\n    with linkmtx:\n        if ar.u:\n            links.append(url)\n        if ar.ud:\n            print(url)\n        if linkfile:\n            zs = \"%s\\n\" % (url,)\n            zb = zs.encode(\"utf-8\", \"replace\")\n            linkfile.write(zb)\n\n\ndef handshake(ar, file, search):\n    # type: (argparse.Namespace, File, bool) -> tuple[list[str], bool]\n    \"\"\"\n    performs a handshake with the server; reply is:\n      if search, a list of search results\n      otherwise, a list of chunks to upload\n    \"\"\"\n\n    req = {\n        \"hash\": [x[0] for x in file.cids],\n        \"name\": file.name,\n        \"lmod\": file.lmod,\n        \"size\": file.size,\n    }\n    if search:\n        req[\"srch\"] = 1\n    else:\n        if ar.touch:\n            req[\"umod\"] = True\n        if ar.owo:\n            req[\"replace\"] = \"mt\"\n        elif ar.ow:\n            req[\"replace\"] = True\n\n    file.recheck = False\n    if file.url:\n        url = file.url\n    else:\n        if b\"/\" in file.rel:\n            url = quotep(file.rel.rsplit(b\"/\", 1)[0]).decode(\"utf-8\")\n        else:\n            url = \"\"\n        url = ar.vtop + url\n\n    t0 = time.time()\n    tmax = t0 + ar.t_hs\n    while True:\n        sc = 600\n        txt = \"\"\n        t1 = time.time()\n        if t1 >= tmax:\n            print(\"\\nERROR: server offline for longer than --t-hs; giving up\")\n            raise Fatal()\n        try:\n            zs = json.dumps(req, separators=(\",\\n\", \": \"))\n            sc, txt = web.req(\"POST\", url, {}, zs.encode(\"utf-8\"), MJ)\n            if sc < 400:\n                break\n\n            raise Exception(\"http %d: %s\" % (sc, txt))\n\n        except Exception as ex:\n            em = str(ex).split(\"SSLError(\")[-1].split(\"\\nURL: \")[0].strip()\n\n            if (\n                sc == 422\n                or \"<pre>partial upload exists at a different\" in txt\n                or \"<pre>source file busy; please try again\" in txt\n            ):\n                file.recheck = True\n                return [], False\n            elif sc == 409 or \"<pre>upload rejected, file already exists\" in txt:\n                return [], False\n            elif sc == 403 or sc == 401:\n                print(\"\\nERROR: login required, or wrong password:\\n%s\" % (txt,))\n                raise Fatal()\n\n            t = \"handshake failed, retrying: %s\\n  t0=%.3f t1=%.3f t2=%.3f td1=%.3f td2=%.3f\\n  %s\\n\\n\"\n            now = time.time()\n            eprint(t % (file.name, t0, t1, now, now - t0, now - t1, em))\n            time.sleep(ar.cd)\n\n    try:\n        r = json.loads(txt)\n    except:\n        raise Exception(txt)\n\n    if search:\n        if ar.uon and r[\"hits\"]:\n            printlink(ar, r[\"hits\"][0][\"rp\"], \"\", \"\")\n        return r[\"hits\"], False\n\n    file.url = quotep(r[\"purl\"].encode(\"utf-8\", WTF8)).decode(\"utf-8\")\n    file.name = r[\"name\"]\n    file.wark = r[\"wark\"]\n\n    if ar.uon and not r[\"hash\"]:\n        printlink(ar, file.url, r[\"name\"], r.get(\"fk\"))\n\n    return r[\"hash\"], r[\"sprs\"]\n\n\ndef upload(fsl, stats, maxsz):\n    # type: (FileSlice, str, int) -> None\n    \"\"\"upload a range of file data, defined by one or more `cid` (chunk-hash)\"\"\"\n\n    ctxt = fsl.cids[0]\n    if len(fsl.cids) > 1:\n        n = 192 // len(fsl.cids)\n        n = 9 if n > 9 else 2 if n < 2 else n\n        zsl = [zs[:n] for zs in fsl.cids[1:]]\n        ctxt += \",%d,%s\" % (n, \"\".join(zsl))\n\n    headers = {\n        \"X-Up2k-Hash\": ctxt,\n        \"X-Up2k-Wark\": fsl.file.wark,\n    }\n\n    if stats:\n        headers[\"X-Up2k-Stat\"] = stats\n\n    nsub = 0\n    try:\n        while nsub != -1:\n            nsub = fsl.subchunk(maxsz, nsub)\n            if nsub == -2:\n                return\n            if nsub >= 0:\n                headers[\"X-Up2k-Subc\"] = str(maxsz * nsub)\n                headers.pop(CLEN, None)\n                nsub += 1\n\n            sc, txt = web.req(\"POST\", fsl.file.url, headers, fsl, MO)\n\n            if sc == 400:\n                if (\n                    \"already being written\" in txt\n                    or \"already got that\" in txt\n                    or \"only sibling chunks\" in txt\n                ):\n                    fsl.file.nojoin = 1\n\n            if sc >= 400:\n                raise Exception(\"http %s: %s\" % (sc, txt))\n    finally:\n        if fsl.f:\n            fsl.f.close()\n            if nsub != -1:\n                fsl.unsub()\n\n\nclass Ctl(object):\n    \"\"\"\n    the coordinator which runs everything in parallel\n    (hashing, handshakes, uploads)\n    \"\"\"\n\n    def _scan(self):\n        ar = self.ar\n        eprint(\"\\nscanning %d locations\\n\" % (len(ar.files),))\n        nfiles = 0\n        nbytes = 0\n        err = []\n        for _, _, inf in walkdirs(err, ar.files, ar.x):\n            if stat.S_ISDIR(inf.st_mode):\n                continue\n\n            nfiles += 1\n            nbytes += inf.st_size\n\n        if err:\n            eprint(\"\\n# failed to access %d paths:\\n\" % (len(err),))\n            for ap, msg in err:\n                if ar.v:\n                    eprint(\"%s\\n `-%s\\n\\n\" % (ap.decode(\"utf-8\", \"replace\"), msg))\n                else:\n                    eprint(ap.decode(\"utf-8\", \"replace\") + \"\\n\")\n\n            eprint(\"^ failed to access those %d paths ^\\n\\n\" % (len(err),))\n\n            if not ar.v:\n                eprint(\"hint: set -v for detailed error messages\\n\")\n\n            if not ar.ok:\n                eprint(\"hint: aborting because --ok is not set\\n\")\n                return\n\n        eprint(\"found %d files, %s\\n\\n\" % (nfiles, humansize(nbytes)))\n        return nfiles, nbytes\n\n    def __init__(self, ar, stats=None):\n        self.ok = False\n        self.panik = 0\n        self.errs = 0\n        self.ar = ar\n        self.stats = stats or self._scan()\n        if not self.stats:\n            return\n\n        self.nfiles, self.nbytes = self.stats\n        self.filegen = walkdirs([], ar.files, ar.x)\n        self.recheck = []  # type: list[File]\n        self.last_file = None\n\n        if ar.safe:\n            self._safe()\n        else:\n            self.at_hash = 0.0\n            self.at_up = 0.0\n            self.at_upr = 0.0\n            self.hash_f = 0\n            self.hash_c = 0\n            self.hash_b = 0\n            self.up_f = 0\n            self.up_c = 0\n            self.up_b = 0  # num bytes handled\n            self.up_br = 0  # num bytes actually transferred\n            self.uploader_busy = 0\n            self.serialized = False\n\n            self.t0 = time.time()\n            self.t0_up = None\n            self.spd = None\n            self.eta = \"99:99:99\"\n\n            self.mutex = threading.Lock()\n            self.exit_cond = threading.Condition()\n            self.uploader_alive = ar.j\n            self.handshaker_alive = ar.j\n            self.q_handshake = HSQueue()  # type: Queue[File]\n            self.q_upload = Queue()  # type: Queue[FileSlice]\n\n            self.st_hash = [None, \"(idle, starting...)\"]  # type: tuple[File, int]\n            self.st_up = [None, \"(idle, starting...)\"]  # type: tuple[File, int]\n\n            self.mth = MTHash(ar.J) if ar.J > 1 else None\n\n            self._fancy()\n\n        file = self.last_file\n        if self.up_br and file:\n            zs = quotep(file.name.encode(\"utf-8\", WTF8))\n            web.req(\"POST\", file.url, {}, b\"msg=upload-queue-empty;\" + zs, MM)\n\n        self.ok = not self.errs\n\n    def _safe(self):\n        \"\"\"minimal basic slow boring fallback codepath\"\"\"\n        search = self.ar.s\n        nf = 0\n        for top, rel, inf in self.filegen:\n            if stat.S_ISDIR(inf.st_mode) or not rel:\n                continue\n\n            nf += 1\n            file = File(top, rel, inf.st_size, inf.st_mtime)\n            upath = file.abs.decode(\"utf-8\", \"replace\")\n\n            print(\"%d %s\\n  hash...\" % (self.nfiles - nf, upath))\n            get_hashlist(file, None, None)\n\n            while True:\n                print(\"  hs...\")\n                try:\n                    hs, _ = handshake(self.ar, file, search)\n                except Fatal:\n                    sys.exit(1)\n\n                if search:\n                    if hs:\n                        for hit in hs:\n                            print(\"  found: %s/%s\" % (self.ar.burl, hit[\"rp\"]))\n                    else:\n                        print(\"  NOT found\")\n                    break\n\n                file.ucids = hs\n                if not hs:\n                    break\n\n                print(\"%d %s\" % (self.nfiles - nf, upath))\n                ncs = len(hs)\n                for nc, cid in enumerate(hs):\n                    print(\"  %d up %s\" % (ncs - nc, cid))\n                    stats = \"%d/0/0/%d\" % (nf, self.nfiles - nf)\n                    fslice = FileSlice(file, [cid])\n                    upload(fslice, stats, self.ar.szm)\n\n            print(\"  ok!\")\n            if file.recheck:\n                self.recheck.append(file)\n\n        if not self.recheck:\n            return\n\n        eprint(\"finalizing %d duplicate files\\n\" % (len(self.recheck),))\n        for file in self.recheck:\n            handshake(self.ar, file, False)\n\n    def _fancy(self):\n        atexit.register(self.cleanup_vt100)\n        if VT100 and not self.ar.ns:\n            ss.scroll_region(3)\n\n        Daemon(self.hasher)\n        for _ in range(self.ar.j):\n            Daemon(self.handshaker)\n            Daemon(self.uploader)\n\n        last_sp = -1\n        while True:\n            with self.exit_cond:\n                self.exit_cond.wait(0.07)\n            if self.panik:\n                sys.exit(1)\n            with self.mutex:\n                if not self.handshaker_alive and not self.uploader_alive:\n                    break\n                st_hash = self.st_hash[:]\n                st_up = self.st_up[:]\n\n            if VT100 and not self.ar.ns:\n                maxlen = ss.w - len(str(self.nfiles)) - 14\n                txt = \"\\033[s\\033[%dH\" % (ss.g,)\n                for y, k, st, f in [\n                    [0, \"hash\", st_hash, self.hash_f],\n                    [1, \"send\", st_up, self.up_f],\n                ]:\n                    txt += \"\\033[%dH%s:\" % (ss.g + y, k)\n                    file, arg = st\n                    if not file:\n                        txt += \" %s\\033[K\" % (arg,)\n                    else:\n                        if y:\n                            p = 100 * file.up_b / file.size\n                        else:\n                            p = 100 * arg / file.size\n\n                        name = file.abs.decode(\"utf-8\", \"replace\")[-maxlen:]\n                        if \"/\" in name:\n                            name = \"\\033[36m%s\\033[0m/%s\" % tuple(name.rsplit(\"/\", 1))\n\n                        txt += \"%6.1f%% %d %s\\033[K\" % (p, self.nfiles - f, name)\n\n                txt += \"\\033[%dH \" % (ss.g + 2,)\n            else:\n                txt = \" \"\n\n            if not VT100:  # OSC9;4 (taskbar-progress)\n                sp = int(self.up_b * 100 / self.nbytes) or 1\n                if last_sp != sp:\n                    last_sp = sp\n                    txt += \"\\033]9;4;1;%d\\033\\\\\" % (sp,)\n\n            if not self.up_br:\n                spd = self.hash_b / ((time.time() - self.t0) or 1)\n                eta = (self.nbytes - self.hash_b) / (spd or 1)\n            else:\n                spd = self.up_br / ((time.time() - self.t0_up) or 1)\n                spd = self.spd = (self.spd or spd) * 0.9 + spd * 0.1\n                eta = (self.nbytes - self.up_b) / (spd or 1)\n\n            spd = humansize(spd)\n            self.eta = str(datetime.timedelta(seconds=int(eta)))\n            if eta > 2591999:\n                self.eta = self.eta.split(\",\")[0]  # truncate HH:MM:SS\n            sleft = humansize(self.nbytes - self.up_b)\n            nleft = self.nfiles - self.up_f\n            tail = \"\\033[K\\033[u\" if VT100 and not self.ar.ns else \"\\r\"\n\n            t = \"%s eta @ %s/s, %s, %d# left\" % (self.eta, spd, sleft, nleft)\n            if not self.hash_b:\n                t = \" now hashing...\"\n            eprint(txt + \"\\033]0;{0}\\033\\\\\\r{0}{1}\".format(t, tail))\n\n        if self.ar.wlist:\n            self.at_hash = time.time() - self.t0\n\n        if self.hash_b and self.at_hash:\n            spd = humansize(self.hash_b / self.at_hash)\n            eprint(\"\\nhasher: %.2f sec, %s/s\\n\" % (self.at_hash, spd))\n        if self.up_br and self.at_up:\n            spd = humansize(self.up_br / self.at_up)\n            eprint(\"upload: %.2f sec, %s/s\\n\" % (self.at_up, spd))\n\n        if not self.recheck:\n            return\n\n        eprint(\"finalizing %d duplicate files\\n\" % (len(self.recheck),))\n        for file in self.recheck:\n            handshake(self.ar, file, False)\n\n    def cleanup_vt100(self):\n        if VT100:\n            ss.scroll_region(None)\n        else:\n            eprint(\"\\033]9;4;0\\033\\\\\")\n        eprint(\"\\033[J\\033]0;\\033\\\\\")\n\n    def cb_hasher(self, file, ofs):\n        self.st_hash = [file, ofs]\n\n    def hasher(self):\n        ptn = re.compile(self.ar.x.encode(\"utf-8\"), re.I) if self.ar.x else None\n        sep = \"{0}\".format(os.sep).encode(\"ascii\")\n        prd = None\n        ls = {}\n        for top, rel, inf in self.filegen:\n            isdir = stat.S_ISDIR(inf.st_mode)\n            if self.ar.z or self.ar.drd:\n                rd = rel if isdir else os.path.dirname(rel)\n                srd = rd.decode(\"utf-8\", \"replace\").replace(\"\\\\\", \"/\").rstrip(\"/\")\n                if srd:\n                    srd += \"/\"\n                if prd != rd:\n                    prd = rd\n                    ls = {}\n                    try:\n                        print(\"      ls ~{0}\".format(srd))\n                        zt = (\n                            self.ar.vtop,\n                            quotep(rd.replace(b\"\\\\\", b\"/\")).decode(\"utf-8\"),\n                        )\n                        sc, txt = web.req(\"GET\", \"%s%s?ls&lt&dots\" % zt, {})\n                        if sc >= 400:\n                            raise Exception(\"http %s\" % (sc,))\n\n                        j = json.loads(txt)\n                        for f in j[\"dirs\"] + j[\"files\"]:\n                            rfn = f[\"href\"].split(\"?\")[0].rstrip(\"/\")\n                            ls[unquote(rfn.encode(\"utf-8\", WTF8))] = f\n                    except Exception as ex:\n                        print(\"   mkdir ~{0}  ({1})\".format(srd, ex))\n\n                    if self.ar.drd:\n                        dp = os.path.join(top, rd)\n                        try:\n                            lnodes = set(os.listdir(dp))\n                        except:\n                            lnodes = list(ls)  # fs eio; don't delete\n                        if ptn:\n                            zs = dp.replace(sep, b\"/\").rstrip(b\"/\") + b\"/\"\n                            zls = [zs + x for x in lnodes]\n                            zls = [x for x in zls if not ptn.match(x)]\n                            lnodes = [x.split(b\"/\")[-1] for x in zls]\n                        bnames = [x for x in ls if x not in lnodes and x != b\".hist\"]\n                        vpath = self.ar.url.split(\"://\")[-1].split(\"/\", 1)[-1]\n                        names = [x.decode(\"utf-8\", WTF8) for x in bnames]\n                        locs = [vpath + srd + x for x in names]\n                        while locs:\n                            req = locs\n                            while req:\n                                print(\"DELETING ~%s#%s\" % (srd, len(req)))\n                                body = json.dumps(req).encode(\"utf-8\")\n                                sc, txt = web.req(\"POST\", \"/?delete\", {}, body, MJ)\n                                if sc == 413 and \"json 2big\" in txt:\n                                    print(\" (delete request too big; slicing...)\")\n                                    req = req[: len(req) // 2]\n                                    continue\n                                elif sc >= 400:\n                                    t = \"delete request failed: %s %s\"\n                                    raise Exception(t % (sc, txt))\n                                break\n                            locs = locs[len(req) :]\n\n            if isdir:\n                continue\n\n            if self.ar.z:\n                rf = ls.get(os.path.basename(rel), None)\n                if rf and rf[\"sz\"] == inf.st_size and abs(rf[\"ts\"] - inf.st_mtime) <= 2:\n                    self.nfiles -= 1\n                    self.nbytes -= inf.st_size\n                    continue\n\n            file = File(top, rel, inf.st_size, inf.st_mtime)\n            while True:\n                with self.mutex:\n                    if (\n                        self.hash_f - self.up_f == 1\n                        or (\n                            self.hash_b - self.up_b < 1024 * 1024 * 1024\n                            and self.hash_c - self.up_c < 512\n                        )\n                    ) and (\n                        not self.ar.nh\n                        or (\n                            self.q_upload.empty()\n                            and self.q_handshake.empty()\n                            and not self.uploader_busy\n                        )\n                    ):\n                        break\n\n                time.sleep(0.05)\n\n            get_hashlist(file, self.cb_hasher, self.mth)\n            with self.mutex:\n                self.hash_f += 1\n                self.hash_c += len(file.cids)\n                self.hash_b += file.size\n                if self.ar.wlist:\n                    self.up_f = self.hash_f\n                    self.up_c = self.hash_c\n                    self.up_b = self.hash_b\n\n            if self.ar.wlist:\n                vp = file.rel.decode(\"utf-8\")\n                if self.ar.chs:\n                    zsl = [\n                        \"%s %d %d\" % (zsii[0], n, zsii[1])\n                        for n, zsii in enumerate(file.cids)\n                    ]\n                    print(\"chs: %s\\n%s\" % (vp, \"\\n\".join(zsl)))\n                zsl = [self.ar.wsalt, str(file.size)] + [x[0] for x in file.cids]\n                zb = hashlib.sha512(\"\\n\".join(zsl).encode(\"utf-8\")).digest()[:33]\n                wark = ub64enc(zb).decode(\"utf-8\")\n                if self.ar.jw:\n                    print(\"%s  %s\" % (wark, vp))\n                else:\n                    zd = datetime.datetime.fromtimestamp(max(0, file.lmod), UTC)\n                    dt = \"%04d-%02d-%02d %02d:%02d:%02d\" % (\n                        zd.year,\n                        zd.month,\n                        zd.day,\n                        zd.hour,\n                        zd.minute,\n                        zd.second,\n                    )\n                    print(\"%s %12d %s %s\" % (dt, file.size, wark, vp))\n                continue\n\n            self.q_handshake.put(file)\n\n        self.st_hash = [None, \"(finished)\"]\n        self._check_if_done()\n\n    def _check_if_done(self):\n        with self.mutex:\n            if self.nfiles - self.up_f:\n                return\n        for _ in range(self.ar.j):\n            self.q_handshake.put(None)\n\n    def handshaker(self):\n        search = self.ar.s\n        while True:\n            file = self.q_handshake.get()\n            if not file:\n                with self.mutex:\n                    self.handshaker_alive -= 1\n                self.q_upload.put(None)\n                return\n\n            chunksz = up2k_chunksize(file.size)\n            upath = file.abs.decode(\"utf-8\", \"replace\")\n            if not VT100:\n                upath = upath.lstrip(\"\\\\?\")\n\n            file.nhs += 1\n            if file.nhs > 32:\n                print(\"ERROR: giving up on file %s\" % (upath))\n                self.errs += 1\n                continue\n\n            while time.time() < file.cd:\n                time.sleep(0.1)\n\n            try:\n                hs, sprs = handshake(self.ar, file, search)\n            except Fatal:\n                self.panik = 1\n                break\n\n            if search:\n                if hs:\n                    for hit in hs:\n                        print(\"found: %s\\n  %s/%s\" % (upath, self.ar.burl, hit[\"rp\"]))\n                else:\n                    print(\"NOT found: {0}\".format(upath))\n\n                with self.mutex:\n                    self.up_f += 1\n                    self.up_c += len(file.cids)\n                    self.up_b += file.size\n\n                self._check_if_done()\n                continue\n\n            if file.recheck:\n                self.recheck.append(file)\n\n            with self.mutex:\n                if hs and not sprs and not self.serialized:\n                    t = \"server filesystem does not support sparse files; serializing uploads\\n\"\n                    eprint(t)\n                    self.serialized = True\n                    for _ in range(self.ar.j - 1):\n                        self.q_upload.put(None)\n                if not hs:\n                    # all chunks done\n                    self.up_f += 1\n                    self.up_c += len(file.cids) - file.up_c\n                    self.up_b += file.size - file.up_b\n\n                    if not file.recheck:\n                        self.up_done(file)\n\n                if hs and file.up_c:\n                    # some chunks failed\n                    self.up_c -= len(hs)\n                    file.up_c -= len(hs)\n                    for cid in hs:\n                        sz = file.kchunks[cid][1]\n                        self.up_br -= sz\n                        self.up_b -= sz\n                        file.up_b -= sz\n\n                if hs and not file.up_b:\n                    # first hs of this file; is this an upload resume?\n                    file.up_b = chunksz * max(0, len(file.kchunks) - len(hs))\n\n                file.ucids = hs\n\n            if not hs:\n                self.at_hash += file.t_hash\n\n                if self.ar.spd:\n                    if VT100:\n                        c1 = \"\\033[36m\"\n                        c2 = \"\\033[0m\"\n                    else:\n                        c1 = c2 = \"\"\n\n                    spd_h = humansize(file.size / file.t_hash, True)\n                    if file.up_c:\n                        t_up = file.t1_up - file.t0_up\n                        spd_u = humansize(file.size / t_up, True)\n\n                        t = \"uploaded %s %s(h:%.2fs,%s/s,up:%.2fs,%s/s)%s\"\n                        print(t % (upath, c1, file.t_hash, spd_h, t_up, spd_u, c2))\n                    else:\n                        t = \"   found %s %s(%.2fs,%s/s)%s\"\n                        print(t % (upath, c1, file.t_hash, spd_h, c2))\n                else:\n                    kw = \"uploaded\" if file.up_c else \"   found\"\n                    print(\"{0} {1}\".format(kw, upath))\n\n                self._check_if_done()\n                continue\n\n            njoin = self.ar.sz // chunksz\n            cs = hs[:]\n            while cs:\n                fsl = FileSlice(file, cs[:1])\n                try:\n                    if file.nojoin:\n                        raise Exception()\n                    for n in range(2, min(len(cs), njoin + 1)):\n                        fsl = FileSlice(file, cs[:n])\n                except:\n                    pass\n                cs = cs[len(fsl.cids) :]\n                self.q_upload.put(fsl)\n\n    def uploader(self):\n        while True:\n            fsl = self.q_upload.get()\n            if not fsl:\n                done = False\n                with self.mutex:\n                    self.uploader_alive -= 1\n                    if not self.uploader_alive:\n                        done = not self.handshaker_alive\n                        self.st_up = [None, \"(finished)\"]\n                if done:\n                    with self.exit_cond:\n                        self.exit_cond.notify_all()\n                return\n\n            file = fsl.file\n            cids = fsl.cids\n            self.last_file = file\n\n            with self.mutex:\n                if not self.uploader_busy:\n                    self.at_upr = time.time()\n                self.uploader_busy += 1\n                if not file.t0_up:\n                    file.t0_up = time.time()\n                    if not self.t0_up:\n                        self.t0_up = file.t0_up\n\n            stats = \"%d/%d/%d/%d %d/%d %s\" % (\n                self.up_f,\n                len(self.recheck),\n                self.uploader_busy,\n                self.nfiles - self.up_f,\n                self.nbytes // (1024 * 1024),\n                (self.nbytes - self.up_b) // (1024 * 1024),\n                self.eta,\n            )\n\n            try:\n                upload(fsl, stats, self.ar.szm)\n            except Exception as ex:\n                t = \"upload failed, retrying: %s #%s+%d (%s)\\n\"\n                eprint(t % (file.name, cids[0][:8], len(cids) - 1, ex))\n                file.cd = time.time() + self.ar.cd\n                # handshake will fix it\n\n            with self.mutex:\n                sz = fsl.len\n                file.ucids = [x for x in file.ucids if x not in cids]\n                if not file.ucids:\n                    file.t1_up = time.time()\n                    self.q_handshake.put(file)\n\n                self.st_up = [file, cids[0]]\n                file.up_b += sz\n                self.up_b += sz\n                self.up_br += sz\n                file.up_c += 1\n                self.up_c += 1\n                self.uploader_busy -= 1\n                if not self.uploader_busy:\n                    self.at_up += time.time() - self.at_upr\n\n    def up_done(self, file):\n        if self.ar.dl:\n            os.unlink(file.abs)\n\n\nclass APF(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):\n    pass\n\n\ndef main():\n    global web, linkfile\n\n    time.strptime(\"19970815\", \"%Y%m%d\")  # python#7980\n    \"\".encode(\"idna\")  # python#29288\n    if not VT100:\n        os.system(\"rem\")  # enables colors\n\n    cores = (os.cpu_count() if hasattr(os, \"cpu_count\") else 0) or 2\n    hcores = min(cores, 3)  # 4% faster than 4+ on py3.9 @ r5-4500U\n\n    ver = \"{0}, v{1}\".format(S_BUILD_DT, S_VERSION)\n    if \"--version\" in sys.argv:\n        print(ver)\n        return\n\n    sys.argv = [x for x in sys.argv if x != \"--ws\"]\n\n    # fmt: off\n    ap = app = argparse.ArgumentParser(formatter_class=APF, description=\"copyparty up2k uploader / filesearch tool  \" + ver, epilog=\"\"\"\nNOTE: source file/folder selection uses rsync syntax, meaning that:\n  \"foo\" uploads the entire folder to URL/foo/\n  \"foo/\" uploads the CONTENTS of the folder into URL/\nNOTE: if server has --usernames enabled, then password is \"username:password\"\n\"\"\")\n\n    ap.add_argument(\"url\", type=unicode, help=\"server url, including destination folder\")\n    ap.add_argument(\"files\", type=files_decoder, nargs=\"+\", help=\"files and/or folders to process\")\n    ap.add_argument(\"-v\", action=\"store_true\", help=\"verbose\")\n    ap.add_argument(\"-a\", metavar=\"PASSWD\", default=\"\", help=\"password (or $filepath) for copyparty (is sent in header 'PW')\")\n    ap.add_argument(\"--ba\", metavar=\"PASSWD\", default=\"\", help=\"password (or $filepath) for basic-auth (usually not necessary)\")\n    ap.add_argument(\"-s\", action=\"store_true\", help=\"file-search (disables upload)\")\n    ap.add_argument(\"-x\", type=unicode, metavar=\"REGEX\", action=\"append\", help=\"skip file if filesystem-abspath matches REGEX (option can be repeated), example: '.*/\\\\.hist/.*'\")\n    ap.add_argument(\"--ok\", action=\"store_true\", help=\"continue even if some local files are inaccessible\")\n    ap.add_argument(\"--touch\", action=\"store_true\", help=\"if last-modified timestamps differ, push local to server (need write+delete perms)\")\n    ap.add_argument(\"--ow\", action=\"store_true\", help=\"overwrite existing files instead of autorenaming\")\n    ap.add_argument(\"--owo\", action=\"store_true\", help=\"overwrite existing files if server-file is older\")\n    ap.add_argument(\"--spd\", action=\"store_true\", help=\"print speeds for each file\")\n    ap.add_argument(\"--version\", action=\"store_true\", help=\"show version and exit\")\n\n    ap = app.add_argument_group(\"print links\")\n    ap.add_argument(\"-u\", action=\"store_true\", help=\"print list of download-links after all uploads finished\")\n    ap.add_argument(\"-ud\", action=\"store_true\", help=\"print download-link after each upload finishes\")\n    ap.add_argument(\"-uf\", type=unicode, metavar=\"PATH\", help=\"print list of download-links to file\")\n\n    ap = app.add_argument_group(\"compatibility\")\n    ap.add_argument(\"--cls\", action=\"store_true\", help=\"clear screen before start\")\n    ap.add_argument(\"--rh\", type=int, metavar=\"TRIES\", default=0, help=\"resolve server hostname before upload (good for buggy networks, but TLS certs will break)\")\n\n    ap = app.add_argument_group(\"folder sync\")\n    ap.add_argument(\"--dl\", action=\"store_true\", help=\"delete local files after uploading\")\n    ap.add_argument(\"--dr\", action=\"store_true\", help=\"delete remote files which don't exist locally (implies --ow)\")\n    ap.add_argument(\"--drd\", action=\"store_true\", help=\"delete remote files during upload instead of afterwards; reduces peak disk space usage, but will reupload instead of detecting renames\")\n\n    ap = app.add_argument_group(\"file-ID calculator; enable with url '-' to list warks (file identifiers) instead of upload/search\")\n    ap.add_argument(\"--wsalt\", type=unicode, metavar=\"S\", default=\"hunter2\", help=\"salt to use when creating warks; must match server config\")\n    ap.add_argument(\"--chs\", action=\"store_true\", help=\"verbose (print the hash/offset of each chunk in each file)\")\n    ap.add_argument(\"--jw\", action=\"store_true\", help=\"just identifier+filepath, not mtime/size too\")\n\n    ap = app.add_argument_group(\"performance tweaks\")\n    ap.add_argument(\"-j\", type=int, metavar=\"CONNS\", default=2, help=\"parallel connections\")\n    ap.add_argument(\"-J\", type=int, metavar=\"CORES\", default=hcores, help=\"num cpu-cores to use for hashing; set 0 or 1 for single-core hashing\")\n    ap.add_argument(\"--sz\", type=int, metavar=\"MiB\", default=64, help=\"try to make each POST this big\")\n    ap.add_argument(\"--szm\", type=int, metavar=\"MiB\", default=96, help=\"max size of each POST (default is cloudflare max)\")\n    ap.add_argument(\"-nh\", action=\"store_true\", help=\"disable hashing while uploading\")\n    ap.add_argument(\"-ns\", action=\"store_true\", help=\"no status panel (for slow consoles and macos)\")\n    ap.add_argument(\"--cxp\", type=float, metavar=\"SEC\", default=57, help=\"assume http connections expired after SEConds\")\n    ap.add_argument(\"--cd\", type=float, metavar=\"SEC\", default=5, help=\"delay before reattempting a failed handshake/upload\")\n    ap.add_argument(\"--t-hs\", type=float, metavar=\"SEC\", default=186, help=\"crash if handshakes fail due to server-offline for this long\")\n    ap.add_argument(\"--safe\", action=\"store_true\", help=\"use simple fallback approach\")\n    ap.add_argument(\"-z\", action=\"store_true\", help=\"ZOOMIN' (skip uploading files if they exist at the destination with the ~same last-modified timestamp, so same as yolo / turbo with date-chk but even faster)\")\n\n    ap = app.add_argument_group(\"tls\")\n    ap.add_argument(\"-te\", metavar=\"PATH\", help=\"path to ca.pem or cert.pem to expect/verify\")\n    ap.add_argument(\"-teh\", action=\"store_true\", help=\"require correct hostname in -te cert\")\n    ap.add_argument(\"-td\", action=\"store_true\", help=\"disable certificate check\")\n    # fmt: on\n\n    try:\n        ar = app.parse_args()\n    finally:\n        if EXE and not sys.argv[1:]:\n            eprint(\"*** hit enter to exit ***\")\n            try:\n                input()\n            except:\n                pass\n\n    # msys2 doesn't uncygpath absolute paths with whitespace\n    if not VT100:\n        zsl = []\n        for fn in ar.files:\n            if re.search(\"^/[a-z]/\", fn):\n                fn = r\"%s:\\%s\" % (fn[1:2], fn[3:])\n            zsl.append(fn.replace(\"/\", \"\\\\\"))\n        ar.files = zsl\n\n    fok = []\n    fng = []\n    for fn in ar.files:\n        if os.path.exists(fn):\n            fok.append(fn)\n        elif VT100:\n            fng.append(fn)\n        else:\n            # windows leaves glob-expansion to the invoked process... okayyy let's get to work\n            from glob import glob\n\n            fns = glob(fn)\n            if fns:\n                fok.extend(fns)\n            else:\n                fng.append(fn)\n\n    if fng:\n        t = \"some files/folders were not found:\\n  %s\"\n        raise Exception(t % (\"\\n  \".join(fng),))\n\n    ar.files = fok\n\n    if ar.drd:\n        ar.dr = True\n\n    if ar.dr:\n        ar.ow = True\n\n    ar.sz *= 1024 * 1024\n    ar.szm *= 1024 * 1024\n\n    ar.x = \"|\".join(ar.x or [])\n\n    setattr(ar, \"wlist\", ar.url == \"-\")\n    setattr(ar, \"uon\", ar.u or ar.ud or ar.uf)\n\n    if ar.uf:\n        linkfile = open(ar.uf, \"wb\")\n\n    for k in \"dl dr drd wlist\".split():\n        errs = []\n        if ar.safe and getattr(ar, k):\n            errs.append(k)\n\n        if errs:\n            raise Exception(\"--safe is incompatible with \" + str(errs))\n\n    ar.files = [\n        os.path.abspath(os.path.realpath(x.encode(\"utf-8\")))\n        + (x[-1:] if x[-1:] in (\"\\\\\", \"/\") else \"\").encode(\"utf-8\")\n        for x in ar.files\n    ]\n\n    # urlsplit needs scheme;\n    zs = ar.url.rstrip(\"/\") + \"/\"\n    if \"://\" not in zs:\n        zs = \"http://\" + zs\n    ar.url = zs\n\n    url = urlsplit(zs)\n    ar.burl = \"%s://%s\" % (url.scheme, url.netloc)\n    ar.vtop = url.path\n\n    if \"https://\" in ar.url.lower():\n        try:\n            import ssl\n            import zipfile\n        except:\n            t = \"ERROR: https is not available for some reason; please use http\"\n            print(\"\\n\\n   %s\\n\\n\" % (t,))\n            raise\n\n    for k in (\"a\", \"ba\"):\n        zs = getattr(ar, k)\n        if zs.startswith(\"$\"):\n            print(\"reading password from file [%s]\" % (zs[1:],))\n            with open(zs[1:], \"rb\") as f:\n                setattr(ar, k, f.read().decode(\"utf-8\").strip())\n\n    if ar.ba:\n        ar.ba = \"Basic \" + base64.b64encode(ar.ba.encode(\"utf-8\")).decode(\"utf-8\")\n\n    for n in range(ar.rh):\n        try:\n            ar.url = undns(ar.url)\n            break\n        except KeyboardInterrupt:\n            raise\n        except:\n            if n > ar.rh - 2:\n                raise\n\n    if ar.cls:\n        eprint(\"\\033[H\\033[2J\\033[3J\", end=\"\")\n\n    web = HCli(ar)\n    ctl = Ctl(ar)\n\n    if ar.dr and not ar.drd and ctl.ok:\n        print(\"\\npass 2/2: delete\")\n        ar.drd = True\n        ar.z = True\n        ctl = Ctl(ar, ctl.stats)\n\n    if links:\n        print()\n        print(\"\\n\".join(links))\n    if linkfile:\n        linkfile.close()\n\n    if ctl.errs:\n        print(\"WARNING: %d errors\" % (ctl.errs))\n\n    sys.exit(0 if ctl.ok else 1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/unforget.py",
    "content": "#!/usr/bin/env python3\n\n\"\"\"\nunforget.py: rebuild db from logfiles\n2022-09-07, v0.1, ed <irc.rizon.net>, MIT-Licensed\nhttps://github.com/9001/copyparty/blob/hovudstraum/bin/unforget.py\n\nonly makes sense if running copyparty with --no-forget\n(e.g. immediately shifting uploads to other storage)\n\nusage:\n  xz -d < log | ./unforget.py .hist/up2k.db\n\n\"\"\"\n\nimport re\nimport sys\nimport json\nimport base64\nimport sqlite3\nimport argparse\n\n\nFS_ENCODING = sys.getfilesystemencoding()\n\n\nclass APF(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):\n    pass\n\n\nmem_cur = sqlite3.connect(\":memory:\").cursor()\nmem_cur.execute(r\"create table a (b text)\")\n\n\ndef s3enc(rd: str, fn: str) -> tuple[str, str]:\n    ret: list[str] = []\n    for v in [rd, fn]:\n        try:\n            mem_cur.execute(\"select * from a where b = ?\", (v,))\n            ret.append(v)\n        except:\n            wtf8 = v.encode(FS_ENCODING, \"surrogateescape\")\n            ret.append(\"//\" + base64.urlsafe_b64encode(wtf8).decode(\"ascii\"))\n\n    return ret[0], ret[1]\n\n\ndef main():\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"db\")\n    ar = ap.parse_args()\n\n    db = sqlite3.connect(ar.db).cursor()\n    ptn_times = re.compile(r\"no more chunks, setting times \\(([0-9]+)\")\n    at = 0\n    ctr = 0\n\n    for ln in [x.decode(\"utf-8\", \"replace\").rstrip() for x in sys.stdin.buffer]:\n        if \"no more chunks, setting times (\" in ln:\n            m = ptn_times.search(ln)\n            if m:\n                at = int(m.group(1))\n\n        if '\"hash\": []' in ln:\n            try:\n                ofs = ln.find(\"{\")\n                j = json.loads(ln[ofs:])\n            except:\n                continue\n\n            w = j[\"wark\"]\n            if db.execute(\"select w from up where w = ?\", (w,)).fetchone():\n                continue\n\n            # PYTHONPATH=/home/ed/dev/copyparty/ python3 -m copyparty -e2dsa  -v foo:foo:rwmd,ed -aed:wark --no-forget\n            # 05:34:43.845 127.0.0.1 42496       no more chunks, setting times (1662528883, 1658001882)\n            # 05:34:43.863 127.0.0.1 42496       {\"name\": \"f\\\"2\", \"purl\": \"/foo/bar/baz/\", \"size\": 1674, \"lmod\": 1658001882, \"sprs\": true, \"hash\": [], \"wark\": \"LKIWpp2jEAh9dH3fu-DobuURFGEKlODXDGTpZ1otMhUg\"}\n            # |                      w                       |     mt     |  sz  |   rd    | fn  |    ip     |     at     |\n            # | LKIWpp2jEAh9dH3fu-DobuURFGEKlODXDGTpZ1otMhUg | 1658001882 | 1674 | bar/baz | f\"2 | 127.0.0.1 | 1662528883 |\n\n            rd, fn = s3enc(j[\"purl\"].strip(\"/\"), j[\"name\"])\n            ip = ln.split(\" \")[1].split(\"m\")[-1]\n\n            q = \"insert into up values (?,?,?,?,?,?,?)\"\n            v = (w, int(j[\"lmod\"]), int(j[\"size\"]), rd, fn, ip, at)\n            db.execute(q, v)\n            ctr += 1\n            if ctr % 1024 == 1023:\n                print(f\"{ctr} commit...\")\n                db.connection.commit()\n\n    if ctr:\n        db.connection.commit()\n\n    print(f\"unforgot {ctr} files\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "bin/up2k.sh",
    "content": "#!/bin/bash\nset -e\n\n# upload $datalen worth of generated data with a random filename,\n# using a single connection for all chunks\n\n\n##\n## config\n\ndatalen=$((128*1024*1024))\ntarget=127.0.0.1\nposturl=/inc\npasswd=wark\n\n\n##\n## derived/generated\n\nsalt=$(head -c 21 /dev/urandom | base64 -w0 | tr '+/' '-_')\ndatalen=$((datalen-3))\n\nopenssl enc -aes-256-ctr -pass pass:$salt -nosalt < /dev/zero 2>/dev/null |\nhead -c 524287 > /dev/shm/$salt.bin  #2147483647\n\n\n##\n## functions\n\n# produce a stream of data\ngendata() {\n    while true; do\n        cat /dev/shm/$salt.bin{,,,,,,,,,,,,,,,,,,}\n    done\n}\n\n# pipe a chunk, get the base64 checksum\ngethash() {\n    printf $(\n        sha512sum | cut -c-66 |\n        sed -r 's/ .*//;s/(..)/\\\\x\\1/g'\n    ) |\n    base64 -w0 | cut -c-44 |\n    tr '+/' '-_'\n}\n\n# division except ceil() instead of floor()\nceildiv() {\n    local num=$1\n    local div=$2\n    echo $(((num+div-1)/div))\n}\n\n# provide filesize, get correct chunksize\ngetchunksize() {\n    local filesize=$1\n    local chunksize=$((1024 * 1024))\n    local stepsize=$((512 * 1024))\n    while true; do\n        for mul in 1 2; do\n            local nchunks=$(ceildiv filesize chunksize)\n            \n            [ $nchunks -le 256 ] ||\n            [ $chunksize -ge $((32 * 1024 * 1024)) ] && {\n                echo $chunksize\n                return\n            }\n\n            chunksize=$((chunksize+stepsize))\n            stepsize=$((stepsize*mul))\n        done\n    done\n}\n\n# pipe data + provide full length, get all chunk checksums\ngethashes() {\n    local datalen=$1\n    local chunksize=$(getchunksize $datalen)\n    local nchunks=$(ceildiv $datalen $chunksize)\n    \n    echo >&2\n    while [ $nchunks -gt 0 ]\n    do\n        head -c $chunksize | gethash\n        nchunks=$((nchunks-1))\n        printf '\\033[Ahash %s \\n' $nchunks >&2\n    done\n}\n\n# pipe handshake response, get wark\ngetwark() {\n    awk '/\"wark\": ?\"/ {sub(/.*\"wark\": ?\"/,\"\");sub(/\".*/,\"\");print}'\n}\n\n\n##\n## create handshake json\n\nchunksize=$(getchunksize $datalen)\n\nprintf '{\"name\":\"%s.bin\",\"size\":%d,\"hash\":[' $salt $datalen >/dev/shm/$salt.hs\n\nprintf '\\033[33m'\ngendata |\nhead -c $datalen |\ngethashes $datalen > /dev/shm/$salt.hl\nprintf '\\033[0m'\n\nIFS=$'\\n' GLOBIGNORE='*' command eval 'HASHES=($(cat \"/dev/shm/$salt.hl\"))'\n\nawk '{printf \"%s\\\"%s\\\"\", v, $0; v=\",\"}' /dev/shm/$salt.hl >> /dev/shm/$salt.hs\n\nprintf ']}' >> /dev/shm/$salt.hs\n\n\n##\n## post handshake\n\nprintf '\\033[36m'\n\n#curl \"http://$target:3923$posturl/handshake.php\" -H \"Content-Type: text/plain;charset=UTF-8\" -H \"Cookie: cppwd=$passwd\" --data \"$(cat \"/dev/shm/$salt.hs\")\" | tee /dev/shm/$salt.res\n\n{\n    {\n        cat <<EOF\nPOST $posturl/ HTTP/1.1\nConnection: Close\nCookie: cppwd=$passwd\nContent-Type: text/plain;charset=UTF-8\nContent-Length: $(cat /dev/shm/$salt.hs | wc -c)\n\nEOF\n    } | sed -r 's/$/\\r/'\n\n    cat /dev/shm/$salt.hs\n} |\ntee /dev/shm/$salt.hsb |\nncat $target 3923 |\ntee /dev/shm/$salt.hs1r\n\nwark=\"$(cat /dev/shm/$salt.hs1r | getwark)\"\nprintf '\\033[0m\\nwark: %s\\n' $wark\n\n\n##\n## wait for signal to continue\n\ntrue || {\n    w8=/dev/shm/$salt.w8\n    touch $w8\n\n    echo \"ready;  rm -f $w8\"\n\n    while [ -e $w8 ]; do\n        sleep 0.2\n    done\n}\n\n\n##\n## post chunks\n\nt0=$(date +%s.%N)\nremains=$datalen\nnchunk=0\n\ngendata |\nhead -c $datalen |\nwhile [ $remains -gt 0 ]; do\n    [ $remains -ge $chunksize ] &&\n        postlen=$chunksize ||\n        postlen=$remains\n    \n    printf '\\n\\n=> %s %s\\n' $remains $postlen >&2\n\n    remains=$((remains-postlen))\n    \n    {\n        cat <<EOF\nPOST $posturl/ HTTP/1.1\nConnection: Keep-Alive\nCookie: cppwd=$passwd\nContent-Type: application/octet-stream\nContent-Length: $postlen\nX-Up2k-Hash: ${HASHES[$nchunk]}\nX-Up2k-Wark: $wark\n\nEOF\n    } | sed -r 's/$/\\r/'\n\n    head -c $postlen\n    nchunk=$((nchunk+1))\n\ndone |\nncat $target 3923 |\ntee /dev/shm/$salt.pr\n\nt=$(date +%s.%N)\n\n\n##\n## handshake again\n\nprintf '\\033[36m'\n\nncat $target 3923 < /dev/shm/$salt.hsb |\ntee /dev/shm/$salt.hs2r |\ngrep -E '\"hash\": ?\\[ *\\]'\n\nrv=$?\n\n[ $rv -eq 0 ] &&\n    printf '\\033[32mok\\033[0m\\n' ||\n    printf '\\033[31mERROR\\033[0m\\n'\n\ntd=$(echo \"scale=3; $t-$t0\" | bc)\nspd=$(echo \"scale=3; ($datalen/$td)/(1024.0*1024)\" | bc)\n\nprintf '%.3f sec, %.3f MiB/s\\n' $td $spd |\ntee /dev/shm/$salt.spd\n\nexit $rv\n\n# find /dev/shm/ -maxdepth 1 -type f | grep -E '/[a-zA-Z0-9_-]{28}\\.[a-z0-9]+$' | xargs rm; for n in {1..8}; do ./up2k.sh & done; wait; cat /dev/shm/*.spd | sort -n\n"
  },
  {
    "path": "bin/zmq-recv.py",
    "content": "#!/usr/bin/env python3\n\nimport sys\nimport zmq\n\n\"\"\"\nzmq-recv.py: demo zmq receiver\n2025-01-22, v1.0, ed <irc.rizon.net>, MIT-Licensed\nhttps://github.com/9001/copyparty/blob/hovudstraum/bin/zmq-recv.py\n\nbasic zmq-server to receive events from copyparty; try one of\nthe below and then \"send a message to serverlog\" in the web-ui:\n\n1) dumb fire-and-forget to any and all listeners;\nrun this script with \"sub\" and run copyparty with this:\n  --xm zmq:pub:tcp://*:5556\n\n2) one lucky listener gets the message, blocks if no listeners:\nrun this script with \"pull\" and run copyparty with this:\n  --xm t3,zmq:push:tcp://*:5557\n\n3) blocking syn/ack mode, client must ack each message;\nrun this script with \"rep\" and run copyparty with this:\n  --xm t3,zmq:req:tcp://localhost:5555\n\nnote: to conditionally block uploads based on message contents,\nuse rep_server to answer with \"return 1\" and run copyparty with\n  --xau t3,c,zmq:req:tcp://localhost:5555\n\"\"\"\n\n\nctx = zmq.Context()\n\n\ndef sub_server():\n    # PUB/SUB allows any number of servers/clients, and\n    # messages are fire-and-forget\n    sck = ctx.socket(zmq.SUB)\n    sck.connect(\"tcp://localhost:5556\")\n    sck.setsockopt_string(zmq.SUBSCRIBE, \"\")\n    while True:\n        print(\"copyparty says %r\" % (sck.recv_string(),))\n\n\ndef pull_server():\n    # PUSH/PULL allows any number of servers/clients, and\n    # each message is sent to a exactly one PULL client\n    sck = ctx.socket(zmq.PULL)\n    sck.connect(\"tcp://localhost:5557\")\n    while True:\n        print(\"copyparty says %r\" % (sck.recv_string(),))\n\n\ndef rep_server():\n    # REP/REQ is a server/client pair where each message must be\n    # acked by the other before another message can be sent, so\n    # copyparty will do a blocking-wait for the ack\n    sck = ctx.socket(zmq.REP)\n    sck.bind(\"tcp://*:5555\")\n    while True:\n        print(\"copyparty says %r\" % (sck.recv_string(),))\n        reply = b\"thx\"\n        # reply = b\"return 1\"  # non-zero to block an upload\n        # reply = b'{\"rc\":1}'  # or as json, that's fine too\n        # reply = b'{\"rejectmsg\":\"naw dude\"}'  # or custom message\n        sck.send(reply)\n\n\nmode = sys.argv[1].lower() if len(sys.argv) > 1 else \"\"\n\nif mode == \"sub\":\n    sub_server()\nelif mode == \"pull\":\n    pull_server()\nelif mode == \"rep\":\n    rep_server()\nelse:\n    print(\"specify mode as first argument:  SUB | PULL | REP\")\n"
  },
  {
    "path": "contrib/README.md",
    "content": "### [`plugins/`](plugins/)\n* example extensions\n\n### [`copyparty.bat`](copyparty.bat)\n* launches copyparty with no arguments (anon read+write within same folder)\n* intended for windows machines with no python.exe in PATH\n* works on windows, linux and macos\n* assumes `copyparty-sfx.py` was renamed to `copyparty.py` in the same folder as `copyparty.bat`\n\n### [`setup-ashell.sh`](setup-ashell.sh)\n* run copyparty on an iPhone/iPad using [a-Shell](https://holzschu.github.io/a-Shell_iOS/)\n* not very useful due to limitations in iOS:\n  * not able to share all of your phone's storage\n  * cannot run in the background\n\n### [`index.html`](index.html)\n* drop-in redirect from an httpd to copyparty\n* assumes the webserver and copyparty is running on the same server/IP\n* modify `10.13.1.1` as necessary if you wish to support browsers without javascript\n\n### [`sharex.sxcu`](sharex.sxcu) - Windows screenshot uploader\n* [sharex](https://getsharex.com/) config file to upload screenshots and grab the URL\n* `RequestURL`: full URL to the target folder\n* `pw`: password (remove the `pw` line if anon-write)\n* the `act:bput` thing is optional since copyparty v1.9.29\n* using an older sharex version, maybe sharex v12.1.1 for example? dw fam i got your back 👉😎👉 [`sharex12.sxcu`](sharex12.sxcu)\n\n### [`ishare.iscu`](ishare.iscu) - MacOS screenshot uploader\n* [ishare](https://isharemac.app/) config file to upload screenshots and grab the URL\n* `RequestURL`: full URL to the target folder\n* `pw`: password (remove the `pw` line if anon-write)\n\n### [`flameshot.sh`](flameshot.sh) - Linux screenshot uploader\n* takes a screenshot with [flameshot](https://flameshot.org/) on Linux, uploads it, and writes the URL to clipboard\n\n### [`send-to-cpp.contextlet.json`](send-to-cpp.contextlet.json)\n* browser integration, kind of? custom rightclick actions and stuff\n* rightclick a pic and send it to copyparty straight from your browser\n* for the [contextlet](https://addons.mozilla.org/en-US/firefox/addon/contextlets/) firefox extension\n\n### [`media-osd-bgone.ps1`](media-osd-bgone.ps1)\n* disables the [windows OSD popup](https://user-images.githubusercontent.com/241032/122821375-0e08df80-d2dd-11eb-9fd9-184e8aacf1d0.png) (the thing on the left) which appears every time you hit media hotkeys to adjust volume or change song while playing music with the copyparty web-ui, or most other audio players really\n\n### [`explorer-nothumbs-nofoldertypes.reg`](explorer-nothumbs-nofoldertypes.reg)\n* disables thumbnails and folder-type detection in windows explorer\n* makes it way faster (especially for slow/networked locations (such as partyfuse))\n\n### [`webdav-cfg.reg`](webdav-cfg.bat)\n* improves the native webdav support in windows;\n  * removes the 47.6 MiB filesize limit when downloading from webdav\n  * optionally enables webdav basic-auth over plaintext http\n  * optionally helps disable wpad, removing the 10sec latency\n\n### [`cfssl.sh`](cfssl.sh)\n* creates CA and server certificates using cfssl\n* give a 3rd argument to install it to your copyparty config\n* systemd service at [`systemd/cfssl.service`](systemd/cfssl.service)\n\n### [`zfs-tune.py`](zfs-tune.py)\n* optimizes databases for optimal performance when stored on a zfs filesystem; also see [openzfs docs](https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Workload%20Tuning.html#database-workloads) and specifically the SQLite subsection\n\n# OS integration\ninit-scripts to start copyparty as a service\n* [`systemd/copyparty.service`](systemd/copyparty.service) runs the sfx normally\n* [`rc/copyparty`](rc/copyparty) runs sfx normally on freebsd, create a `copyparty` user\n* [`systemd/prisonparty.service`](systemd/prisonparty.service) runs the sfx in a chroot\n* [`openrc/copyparty`](openrc/copyparty)\n\n# Reverse-proxy\ncopyparty supports running behind another webserver\n* [`apache/copyparty.conf`](apache/copyparty.conf)\n* [`haproxy/copyparty.conf`](haproxy/copyparty.conf)\n* [`lighttpd/subdomain.conf`](lighttpd/subdomain.conf)\n* [`lighttpd/subpath.conf`](lighttpd/subpath.conf)\n* [`nginx/copyparty.conf`](nginx/copyparty.conf) -- recommended\n* [`traefik/copyparty.yaml`](traefik/copyparty.yaml)\n"
  },
  {
    "path": "contrib/apache/copyparty.conf",
    "content": "# if you would like to use unix-sockets (recommended),\n# you must run copyparty with one of the following:\n#\n#   -i unix:777:/dev/shm/party.sock\n#   -i unix:777:/dev/shm/party.sock,127.0.0.1\n#\n# if you are doing location-based proxying (such as `/stuff` below)\n# you must run copyparty with --rp-loc=stuff\n#\n# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1\n\n\nLoadModule proxy_module modules/mod_proxy.so\n\nRequestHeader set \"X-Forwarded-Proto\" expr=%{REQUEST_SCHEME}\n# NOTE: do not specify ProxyPassReverse\n\n\n##\n## then, enable one of the below:\n\n# use subdomain proxying to unix-socket (best)\nProxyPass \"/\" \"unix:///dev/shm/party.sock|http://whatever/\"\n\n# use subdomain proxying to 127.0.0.1 (slower)\n#ProxyPass \"/\" \"http://127.0.0.1:3923/\"\n\n# use subpath proxying to 127.0.0.1 (slow and maybe buggy)\n#ProxyPass \"/stuff\" \"http://127.0.0.1:3923/stuff\"\n"
  },
  {
    "path": "contrib/cfssl.sh",
    "content": "#!/bin/bash\nset -e\n\ncat >/dev/null <<'EOF'\n\nNOTE: copyparty is now able to do this automatically;\nhowever you may wish to use this script instead if\nyou have specific needs (or if copyparty breaks)\n\nthis script generates a new self-signed TLS certificate and\nreplaces the default insecure one that comes with copyparty\n\nas it is trivial to impersonate a copyparty server using the\ndefault certificate, it is highly recommended to do this\n\nthis will create a self-signed CA, and a Server certificate\nwhich gets signed by that CA -- you can run it multiple times\nwith different server-FQDNs / IPs to create additional certs\nfor all your different servers / (non-)copyparty services\n\nEOF\n\n\n# ca-name and server-fqdn\nca_name=\"$1\"\nsrv_fqdn=\"$2\"\n\n[ -z \"$srv_fqdn\" ] && { cat <<'EOF'\nneed arg 1: ca name\nneed arg 2: server fqdn and/or IPs, comma-separated\noptional arg 3: if set, write cert into copyparty cfg\n\nexample:\n  ./cfssl.sh PartyCo partybox.local y\nEOF\n\texit 1\n}\n\n\ncommand -v cfssljson 2>/dev/null || {\n\techo please install cfssl and try again\n\texit 1\n}\n\n\ngen_ca() {\n\t(tee /dev/stderr <<EOF\n{\"CN\": \"$ca_name ca\",\n\"CA\": {\"expiry\":\"87600h\", \"pathlen\":0},\n\"key\": {\"algo\":\"rsa\", \"size\":4096},\n\"names\": [{\"O\":\"$ca_name ca\"}]}\nEOF\n\t)|\n\tcfssl gencert -initca - |\n\tcfssljson -bare ca\n\t\n\tmv ca-key.pem ca.key\n\trm ca.csr\n}\n\n\ngen_srv() {\n\t(tee /dev/stderr <<EOF\n{\"key\": {\"algo\":\"rsa\", \"size\":4096},\n\"names\": [{\"O\":\"$ca_name - $srv_fqdn\"}]}\nEOF\n\t)|\n\tcfssl gencert -ca ca.pem -ca-key ca.key \\\n\t\t-profile=www -hostname=\"$srv_fqdn\" - |\n\tcfssljson -bare \"$srv_fqdn\"\n\n\tmv \"$srv_fqdn-key.pem\" \"$srv_fqdn.key\"\n\trm \"$srv_fqdn.csr\"\n}\n\n\n# create ca if not exist\n[ -e ca.key ] ||\n\tgen_ca\n\n# always create server cert\ngen_srv\n\n\n# dump cert info\nshow() {\n\topenssl x509 -text -noout -in $1 |\n\tawk '!o; {o=0} /[0-9a-f:]{16}/{o=1}'\n}\nshow ca.pem\nshow \"$srv_fqdn.pem\"\necho\necho \"successfully generated new certificates\"\n\n# write cert into copyparty config\n[ -z \"$3\" ] || {\n\tmkdir -p ~/.config/copyparty\n\tcat \"$srv_fqdn\".{key,pem} ca.pem >~/.config/copyparty/cert.pem \n\techo \"successfully replaced copyparty certificate\"\n}\n\n\n# rm *.key *.pem\n# cfssl print-defaults config\n# cfssl print-defaults csr\n"
  },
  {
    "path": "contrib/copyparty.bat",
    "content": "exec python \"$(dirname \"$0\")\"/copyparty.py\n\n@rem on linux, the above will execute and the script will terminate\n@rem on windows, the rest of this script will run\n\n@echo off\ncls\n\nset py=\nfor /f %%i in ('where python 2^>nul') do (\n    set \"py=%%i\"\n    goto c1\n)\n:c1\n\nif [%py%] == [] (\n    for /f %%i in ('where /r \"%localappdata%\\programs\\python\" python 2^>nul') do (\n        set \"py=%%i\"\n        goto c2\n    )\n)\n:c2\n\nif [%py%] == [] set \"py=c:\\python27\\python.exe\"\n\nif not exist \"%py%\" (\n    echo could not find python\n    echo(\n    pause\n    exit /b\n)\n\nstart cmd /c %py% \"%~dp0\\copyparty.py\"\n"
  },
  {
    "path": "contrib/explorer-nothumbs-nofoldertypes.reg",
    "content": "﻿Windows Registry Editor Version 5.00\r\n\r\n; this will do 3 things, all optional:\r\n;  1) disable thumbnails\r\n;  2) delete all existing folder type settings/detections\r\n;  3) disable folder type detection (force default columns)\r\n;\r\n; this makes the file explorer way faster,\r\n; especially on slow/networked locations\r\n\r\n\r\n; =====================================================================\r\n; 1) disable thumbnails\r\n\r\n[HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced]\r\n\"IconsOnly\"=dword:00000001\r\n\r\n\r\n; =====================================================================\r\n; 2) delete all existing folder type settings/detections\r\n\r\n[-HKEY_CURRENT_USER\\Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\Shell\\Bags]\r\n\r\n[-HKEY_CURRENT_USER\\Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\Shell\\BagMRU]\r\n\r\n\r\n; =====================================================================\r\n; 3) disable folder type detection\r\n\r\n[HKEY_CURRENT_USER\\Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\Shell\\Bags\\AllFolders\\Shell]\r\n\"FolderType\"=\"NotSpecified\"\r\n"
  },
  {
    "path": "contrib/flameshot.sh",
    "content": "#!/bin/bash\nset -e\n\n# take a screenshot with flameshot and send it to copyparty;\n# the image url will be placed on your clipboard\n\npassword=wark\nurl=https://a.ocv.me/up/\nfilename=$(date +%Y-%m%d-%H%M%S).png\n\nflameshot gui -s -r |\ncurl -T- $url$filename?pw=$password |\ntail -n 1 |\nxsel -ib\n"
  },
  {
    "path": "contrib/haproxy/copyparty.conf",
    "content": "# this config is essentially two separate examples;\n#\n#   foo1 connects to copyparty using tcp, and\n#   foo2 uses unix-sockets for 27% higher performance\n#\n# to use foo2 you must run copyparty with one of the following:\n#\n#   -i unix:777:/dev/shm/party.sock\n#   -i unix:777:/dev/shm/party.sock,127.0.0.1\n\ndefaults\n  mode http\n  option forwardfor\n  timeout connect 1s\n  timeout client 610s\n  timeout server 610s\n\nlisten foo1\n  bind *:8081\n  server srv1 127.0.0.1:3923 maxconn 512\n\nlisten foo2\n  bind *:8082\n  server srv1 /dev/shm/party.sock maxconn 512\n"
  },
  {
    "path": "contrib/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n\t<meta charset=\"utf-8\">\n\t<title>💾🎉 redirect</title>\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<style>\n\nhtml, body {\n\tfont-family: sans-serif;\n}\nbody {\n\tpadding: 1em 2em;\n\tfont-size: 1.5em;\n}\na {\n\tfont-size: 1.2em;\n\tpadding: .1em;\n}\n\n</style>\n</head>\n<body>\n\t<span id=\"desc\">you probably want</span> <a id=\"redir\" href=\"//10.13.1.1:3923/\">copyparty</a>\n\t<script>\n\nvar a = document.getElementById('redir'),\n\tproto = location.protocol.indexOf('https') === 0 ? 'https' : 'http',\n\tloc = location.hostname || '127.0.0.1',\n\tport = a.getAttribute('href').split(':').pop().split('/')[0],\n\turl = proto + '://' + loc + ':' + port + '/';\n\na.setAttribute('href', url);\ndocument.getElementById('desc').innerHTML = 'redirecting to';\n\nsetTimeout(function() {\n\tlocation.href = url;\n}, 500);\n\n</script>\n</body>\n</html>\n"
  },
  {
    "path": "contrib/ishare.iscu",
    "content": "{\n  \"Name\": \"copyparty\",\n  \"RequestURL\": \"http://127.0.0.1:3923/screenshots/\",\n  \"Headers\": {\n    \"pw\": \"PUT_YOUR_PASSWORD_HERE_MY_DUDE\",\n    \"accept\": \"json\"\n  },\n  \"FileFormName\": \"f\",\n  \"ResponseURL\": \"{{fileurl}}\"\n}\n"
  },
  {
    "path": "contrib/lighttpd/subdomain.conf",
    "content": "# example usage for benchmarking:\n#\n#   taskset -c 1 lighttpd -Df ~/dev/copyparty/contrib/lighttpd/subdomain.conf\n#\n# lighttpd can connect to copyparty using either tcp (127.0.0.1)\n# or a unix-socket, but unix-sockets are 37% faster because\n# lighttpd doesn't reuse tcp connections, so we're doing unix-sockets\n#\n# this means we must run copyparty with one of the following:\n#\n#   -i unix:777:/dev/shm/party.sock\n#   -i unix:777:/dev/shm/party.sock,127.0.0.1\n#\n# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1\n\nserver.port = 80\nserver.document-root = \"/var/empty\"\nserver.upload-dirs = ( \"/dev/shm\", \"/tmp\" )\nserver.modules = ( \"mod_proxy\" )\nproxy.forwarded = ( \"for\" => 1, \"proto\" => 1 )\nproxy.server = ( \"\" => ( ( \"host\" => \"/dev/shm/party.sock\" ) ) )\n\n# if you really need to use tcp instead of unix-sockets, do this instead:\n#proxy.server = ( \"\" => ( ( \"host\" => \"127.0.0.1\", \"port\" => \"3923\" ) ) )\n"
  },
  {
    "path": "contrib/lighttpd/subpath.conf",
    "content": "# example usage for benchmarking:\n#\n#   taskset -c 1 lighttpd -Df ~/dev/copyparty/contrib/lighttpd/subpath.conf\n#\n# lighttpd can connect to copyparty using either tcp (127.0.0.1)\n# or a unix-socket, but unix-sockets are 37% faster because\n# lighttpd doesn't reuse tcp connections, so we're doing unix-sockets\n#\n# this means we must run copyparty with one of the following:\n#\n#   -i unix:777:/dev/shm/party.sock\n#   -i unix:777:/dev/shm/party.sock,127.0.0.1\n#\n# also since this example proxies a subpath instead of the\n# recommended subdomain-proxying, we must also specify this:\n#\n#   --rp-loc files\n#\n# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1\n\nserver.port = 80\nserver.document-root = \"/var/empty\"\nserver.upload-dirs = ( \"/dev/shm\", \"/tmp\" )\nserver.modules = ( \"mod_proxy\" )\n$HTTP[\"url\"] =~ \"^/files\" {\n    proxy.forwarded = ( \"for\" => 1, \"proto\" => 1 )\n    proxy.server = ( \"\" => ( ( \"host\" => \"/dev/shm/party.sock\" ) ) )\n\n    # if you really need to use tcp instead of unix-sockets, do this instead:\n    #proxy.server = ( \"\" => ( ( \"host\" => \"127.0.0.1\", \"port\" => \"3923\" ) ) )\n}\n"
  },
  {
    "path": "contrib/media-osd-bgone.ps1",
    "content": "﻿# media-osd-bgone.ps1: disable media-control OSD on win10do\n# v1.1, 2021-06-25, ed <irc.rizon.net>, MIT-licensed\n# https://github.com/9001/copyparty/blob/hovudstraum/contrib/media-osd-bgone.ps1\n#\n# locates the first window that looks like the media OSD and minimizes it;\n# doing this once after each reboot should do the trick\n# (adjust the width/height filter if it doesn't work)\n#\n# ---------------------------------------------------------------------\n#\n# tip: save the following as \"media-osd-bgone.bat\" next to this script:\n#   start cmd /c \"powershell -command \"\"set-executionpolicy -scope process bypass; .\\media-osd-bgone.ps1\"\" & ping -n 2 127.1 >nul\"\n#\n# then create a shortcut to that bat-file and move the shortcut here:\n#   %appdata%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\n#\n# and now this will autorun on bootup\n\n\nAdd-Type -TypeDefinition @\"\nusing System;\nusing System.IO;\nusing System.Threading;\nusing System.Diagnostics;\nusing System.Runtime.InteropServices;\nusing System.Windows.Forms;\n\nnamespace A {\n  public class B : Control {\n\n    [DllImport(\"user32.dll\")]\n    static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, int dwExtraInfo);\n\n    [DllImport(\"user32.dll\", SetLastError = true)]\n    static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);\n\n    [DllImport(\"user32.dll\", SetLastError=true)]\n    static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);\n\n    [DllImport(\"user32.dll\")]\n    static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);\n\n    [StructLayout(LayoutKind.Sequential)]\n    public struct RECT {\n      public int x;\n      public int y;\n      public int x2;\n      public int y2;\n    }\n    \n    bool fa() {\n      RECT r;\n      IntPtr it = IntPtr.Zero;\n      while ((it = FindWindowEx(IntPtr.Zero, it, \"NativeHWNDHost\", \"\")) != IntPtr.Zero) {\n        if (FindWindowEx(it, IntPtr.Zero, \"DirectUIHWND\", \"\") == IntPtr.Zero)\n          continue;\n        \n        if (!GetWindowRect(it, out r))\n          continue;\n\n        int w = r.x2 - r.x + 1;\n        int h = r.y2 - r.y + 1;\n\n        Console.WriteLine(\"[*] hwnd {0:x} @ {1}x{2} sz {3}x{4}\", it, r.x, r.y, w, h);\n        if (h != 141)\n          continue;\n        \n        ShowWindow(it, 6);\n        Console.WriteLine(\"[+] poof\");\n        return true;\n      }\n      return false;\n    }\n\n    void fb() {\n      keybd_event((byte)Keys.VolumeMute, 0, 0, 0);\n      keybd_event((byte)Keys.VolumeMute, 0, 2, 0);\n      Thread.Sleep(500);\n      keybd_event((byte)Keys.VolumeMute, 0, 0, 0);\n      keybd_event((byte)Keys.VolumeMute, 0, 2, 0);\n\n      while (true) {\n        if (fa()) {\n          break;\n        }\n        Console.WriteLine(\"[!] not found\");\n        Thread.Sleep(1000);\n      }\n      this.Invoke((MethodInvoker)delegate {\n        Application.Exit();\n      });\n    }\n\n    public void Run() {\n      Console.WriteLine(\"[+] hi\");\n      new Thread(new ThreadStart(fb)).Start();\n      Application.Run();\n      Console.WriteLine(\"[+] bye\");\n    }\n  }\n}\n\"@ -ReferencedAssemblies System.Windows.Forms\n\n(New-Object -TypeName A.B).Run()\n"
  },
  {
    "path": "contrib/nginx/copyparty.conf",
    "content": "# look for \"max clients:\" when starting copyparty, as nginx should\n# not accept more consecutive clients than what copyparty is able to;\n# nginx default is 512  (worker_processes 1, worker_connections 512)\n#\n# ======================================================================\n#\n# to reverse-proxy a specific path/subpath/location below a domain\n# (rather than a complete subdomain), for example \"/qw/er\", you must\n# run copyparty with --rp-loc /qw/er and also change the following:\n# \tlocation / {\n# \t\tproxy_pass http://cpp_tcp;\n# to this:\n# \tlocation /qw/er/ {\n# \t\tproxy_pass http://cpp_tcp/qw/er/;\n#\n# ======================================================================\n#\n# rarely, in some extreme usecases, it can be good to add -j0\n# (40'000 requests per second, or 20gbps upload/download in parallel)\n# but this is usually counterproductive and slightly buggy\n#\n# ======================================================================\n#\n# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1\n#\n# ======================================================================\n#\n# if you are behind cloudflare (or another CDN/WAF/protection service),\n# remember to reject all connections which are not coming from your\n# protection service -- for cloudflare in particular, you can\n# generate the list of permitted IP ranges like so:\n#   (curl -s https://www.cloudflare.com/ips-v{4,6} | sed 's/^/allow /; s/$/;/'; echo; echo \"deny all;\") > /etc/nginx/cloudflare-only.conf\n#\n# and then enable it below by uncommenting the cloudflare-only.conf line\n#\n# ======================================================================\n\n\nupstream cpp_tcp {\n\t# alternative 1: connect to copyparty using tcp;\n\t# cpp_uds is slightly faster and more secure, but\n\t# cpp_tcp is easier to setup and \"just works\"\n\t# ...you should however restrict copyparty to only\n\t# accept connections from nginx by adding these args:\n\t# -i 127.0.0.1\n\n\tserver 127.0.0.1:3923 fail_timeout=1s;\n\tkeepalive 1;\n}\n\n\nupstream cpp_uds {\n\t# alternative 2: unix-socket, aka. \"unix domain socket\";\n\t# 5-10% faster, and better isolation from other software,\n\t# but there must be at least one unix-group which both\n\t# nginx and copyparty is a member of; if that group is\n\t# \"www\" then run copyparty with the following args:\n\t# -i unix:770:www:/dev/shm/party.sock\n\n\tserver unix:/dev/shm/party.sock fail_timeout=1s;\n\tkeepalive 1;\n}\n\n\nserver {\n\tlisten 443 ssl;\n\tlisten [::]:443 ssl;\n\n\tserver_name fs.example.com;\n\n\t# uncomment the following line to reject non-cloudflare connections, ensuring client IPs cannot be spoofed:\n\t#include /etc/nginx/cloudflare-only.conf;\n\n\tlocation / {\n\t\t# recommendation: replace cpp_tcp with cpp_uds below\n\t\tproxy_pass http://cpp_tcp;\n\t\tproxy_redirect off;\n\t\t# disable buffering (next 4 lines)\n\t\tproxy_http_version 1.1;\n\t\tclient_max_body_size 0;\n\t\tproxy_buffering off;\n\t\tproxy_request_buffering off;\n\t\t# improve download speed from 600 to 1500 MiB/s\n\t\tproxy_buffers 32 8k;\n\t\tproxy_buffer_size 16k;\n\t\tproxy_busy_buffers_size 24k;\n\n\t\tproxy_set_header   Connection        \"Keep-Alive\";\n\t\tproxy_set_header   Host              $host;\n\t\tproxy_set_header   X-Real-IP         $remote_addr;\n\t\tproxy_set_header   X-Forwarded-Proto $scheme;\n\t\tproxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;\n\t\t# NOTE: with cloudflare you want this X-Forwarded-For instead:\n\t\t#proxy_set_header   X-Forwarded-For   $http_cf_connecting_ip;\n\t}\n}\n\n\n# default client_max_body_size (1M) blocks uploads larger than 256 MiB\nclient_max_body_size 1024M;\nclient_header_timeout 610m;\nclient_body_timeout 610m;\nsend_timeout 610m;\n"
  },
  {
    "path": "contrib/nixos/modules/copyparty.nix",
    "content": "{\n  config,\n  pkgs,\n  lib,\n  ...\n}:\nwith lib;\nlet\n  mkKeyValue =\n    key: value:\n    if value == true then\n      # sets with a true boolean value are coerced to just the key name\n      key\n    else if value == false then\n      # or omitted completely when false\n      \"\"\n    else\n      (generators.mkKeyValueDefault { inherit mkValueString; } \": \" key value);\n\n  mkAttrsString = value: (generators.toKeyValue { inherit mkKeyValue; } value);\n\n  mkValueString =\n    value:\n    if isList value then\n      (concatStringsSep \",\" (map mkValueString value))\n    else if isAttrs value then\n      \"\\n\" + (mkAttrsString value)\n    else\n      (generators.mkValueStringDefault { } value);\n\n  mkSectionName = value: \"[\" + (escape [ \"[\" \"]\" ] value) + \"]\";\n\n  mkSection = name: attrs: ''\n    ${mkSectionName name}\n    ${mkAttrsString attrs}\n  '';\n\n  mkVolume = name: attrs: ''\n    ${mkSectionName name}\n    ${attrs.path}\n    ${mkAttrsString {\n      accs = attrs.access;\n      flags = attrs.flags;\n    }}\n  '';\n\n  passwordPlaceholder = name: \"{{password-${name}}}\";\n\n  accountsWithPlaceholders = mapAttrs (name: attrs: passwordPlaceholder name);\n\n  volumesWithoutVariables = filterAttrs (k: v: !(hasInfix \"\\${\" v.path)) cfg.volumes;\n\n  configStr = ''\n    ${mkSection \"global\" cfg.settings}\n    ${cfg.globalExtraConfig}\n    ${mkSection \"accounts\" (accountsWithPlaceholders cfg.accounts)}\n    ${mkSection \"groups\" cfg.groups}\n    ${concatStringsSep \"\\n\" (mapAttrsToList mkVolume cfg.volumes)}\n  '';\n\n  cfg = config.services.copyparty;\n  configFile = pkgs.writeText \"copyparty.conf\" configStr;\n  runtimeConfigPath = \"/run/copyparty/copyparty.conf\";\n  externalCacheDir = \"/var/cache/copyparty\";\n  externalStateDir = \"/var/lib/copyparty\";\n  defaultShareDir = \"${externalStateDir}/data\";\nin\n{\n  options.services.copyparty = {\n    enable = mkEnableOption \"web-based file manager\";\n\n    package = mkPackageOption pkgs \"copyparty\" {\n      extraDescription = ''\n        Package of the application to run, exposed for overriding purposes.\n      '';\n    };\n\n    mkHashWrapper = mkOption {\n      type = types.bool;\n      default = true;\n      description = ''\n        Make a shell script wrapper called {command}`copyparty-hash` with all options set here,\n        that launches the hashing cli.\n      '';\n    };\n\n    user = mkOption {\n      type = types.str;\n      default = \"copyparty\";\n      description = ''\n        The user that copyparty will run under.\n\n        If changed from default, you are responsible for making sure the user exists.\n      '';\n    };\n\n    group = mkOption {\n      type = types.str;\n      default = \"copyparty\";\n      description = ''\n        The group that copyparty will run under.\n\n        If changed from default, you are responsible for making sure the user exists.\n      '';\n    };\n\n    openFilesLimit = mkOption {\n      default = 4096;\n      type = types.either types.int types.str;\n      description = \"Number of files to allow copyparty to open.\";\n    };\n\n    settings = mkOption {\n      type = types.attrs;\n      description = ''\n        Global settings to apply.\n        Directly maps to values in the `[global]` section of the copyparty config.\n        Cannot set \"c\" or \"hist\", those are set by this module.\n        See {command}`copyparty --help` for more details.\n      '';\n      default = {\n        i = \"127.0.0.1\";\n        no-reload = true;\n        hist = externalCacheDir;\n      };\n      example = literalExpression ''\n        {\n          i = \"0.0.0.0\";\n          no-reload = true;\n          hist = ${externalCacheDir};\n        }\n      '';\n    };\n\n    globalExtraConfig = mkOption {\n      type = types.str;\n      default = \"\";\n      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.\";\n    };\n\n    accounts = mkOption {\n      type = types.attrsOf (\n        types.submodule (\n          { ... }:\n          {\n            options = {\n              passwordFile = mkOption {\n                type = types.str;\n                description = ''\n                  Runtime file path to a file containing the user password.\n                  Must be readable by the copyparty user.\n                '';\n                example = \"/run/keys/copyparty/ed\";\n              };\n            };\n          }\n        )\n      );\n      description = ''\n        A set of copyparty accounts to create.\n      '';\n      default = { };\n      example = literalExpression ''\n        {\n          ed.passwordFile = \"/run/keys/copyparty/ed\";\n        };\n      '';\n    };\n\n    groups = mkOption {\n      type = types.attrsOf (types.listOf types.str);\n      description = ''\n        A set of copyparty groups to create and the users that should be part of each group.\n      '';\n      default = { };\n      example = literalExpression ''\n        {\n          group_name = [ \"user1\" \"user2\" ];\n        };\n      '';\n    };\n\n    volumes = mkOption {\n      type = types.attrsOf (\n        types.submodule (\n          { ... }:\n          {\n            options = {\n              path = mkOption {\n                type = types.path;\n                description = ''\n                  Path of a directory to share.\n                '';\n              };\n              access = mkOption {\n                type = types.attrs;\n                description = ''\n                  Attribute list of permissions and the users to apply them to.\n\n                  The key must be a string containing any combination of allowed permission:\n                  * \"r\" (read):   list folder contents, download files\n                  * \"w\" (write):  upload files; need \"r\" to see the uploads\n                  * \"m\" (move):   move files and folders; need \"w\" at destination\n                  * \"d\" (delete): permanently delete files and folders\n                  * \"g\" (get):    download files, but cannot see folder contents\n                  * \"G\" (upget):  \"get\", but can see filekeys of their own uploads\n                  * \"h\" (html):   \"get\", but folders return their index.html\n                  * \"a\" (admin):  can see uploader IPs, config-reload\n\n                  For example: \"rwmd\"\n\n                  The value must be one of:\n                  * an account name, defined in `accounts`\n                  * a list of account names\n                  * \"*\", which means \"any account\"\n                '';\n                example = literalExpression ''\n                  {\n                    # wG = write-upget = see your own uploads only\n                    wG = \"*\";\n                    # read-write-modify-delete for users \"ed\" and \"k\"\n                    rwmd = [\"ed\" \"k\"];\n                  };\n                '';\n              };\n              flags = mkOption {\n                type = types.attrs;\n                description = ''\n                  Attribute list of volume flags to apply.\n                  See {command}`copyparty --help-flags` for more details.\n                '';\n                example = literalExpression ''\n                  {\n                    # \"fk\" enables filekeys (necessary for upget permission) (4 chars long)\n                    fk = 4;\n                    # scan for new files every 60sec\n                    scan = 60;\n                    # volflag \"e2d\" enables the uploads database\n                    e2d = true;\n                    # \"d2t\" disables multimedia parsers (in case the uploads are malicious)\n                    d2t = true;\n                    # skips hashing file contents if path matches *.iso\n                    nohash = \"\\.iso$\";\n                  };\n                '';\n                default = { };\n              };\n            };\n          }\n        )\n      );\n      description = \"A set of copyparty volumes to create\";\n      default = {\n        \"/\" = {\n          path = defaultShareDir;\n          access = {\n            r = \"*\";\n          };\n        };\n      };\n      example = literalExpression ''\n        {\n          \"/\" = {\n            path = ${defaultShareDir};\n            access = {\n              # wG = write-upget = see your own uploads only\n              wG = \"*\";\n              # read-write-modify-delete for users \"ed\" and \"k\"\n              rwmd = [\"ed\" \"k\"];\n            };\n          };\n        };\n      '';\n    };\n  };\n\n  config = mkIf cfg.enable (\n    let\n      command = \"${getExe cfg.package} -c ${runtimeConfigPath}\";\n    in\n    {\n      systemd.services.copyparty = {\n        description = \"http file sharing hub\";\n        wantedBy = [ \"multi-user.target\" ];\n\n        environment = {\n          PYTHONUNBUFFERED = \"true\";\n          XDG_CONFIG_HOME = externalStateDir;\n        };\n\n        preStart =\n          let\n            replaceSecretCommand =\n              name: attrs:\n              \"${getExe pkgs.replace-secret} '${passwordPlaceholder name}' '${attrs.passwordFile}' ${runtimeConfigPath}\";\n          in\n          ''\n            set -euo pipefail\n            install -m 600 ${configFile} ${runtimeConfigPath}\n            ${concatStringsSep \"\\n\" (mapAttrsToList replaceSecretCommand cfg.accounts)}\n          '';\n\n        serviceConfig = {\n          Type = \"simple\";\n          ExecStart = command;\n          # Hardening options\n          User = cfg.user;\n          Group = cfg.group;\n          RuntimeDirectory = [ \"copyparty\" ];\n          RuntimeDirectoryMode = \"0700\";\n          StateDirectory = [ \"copyparty\" ];\n          StateDirectoryMode = \"0700\";\n          CacheDirectory = lib.mkIf (cfg.settings ? hist) [ \"copyparty\" ];\n          CacheDirectoryMode = lib.mkIf (cfg.settings ? hist) \"0700\";\n          WorkingDirectory = externalStateDir;\n          BindReadOnlyPaths = [\n            \"/nix/store\"\n            \"-/etc/resolv.conf\"\n            \"-/etc/nsswitch.conf\"\n            \"-/etc/group\"\n            \"-/etc/hosts\"\n            \"-/etc/localtime\"\n          ] ++ (mapAttrsToList (k: v: \"-${v.passwordFile}\") cfg.accounts);\n          BindPaths =\n            (if cfg.settings ? hist then [ cfg.settings.hist ] else [ ])\n            ++ [ externalStateDir ]\n            ++ (mapAttrsToList (k: v: v.path) volumesWithoutVariables);\n          # ProtectSystem = \"strict\";\n          # Note that unlike what 'ro' implies,\n          # this actually makes it impossible to read anything in the root FS,\n          # except for things explicitly mounted via `RuntimeDirectory`, `StateDirectory`, `CacheDirectory`, and `BindReadOnlyPaths`.\n          # This is because TemporaryFileSystem creates a *new* *empty* filesystem for the process, so only bindmounts are visible.\n          TemporaryFileSystem = \"/:ro\";\n          PrivateTmp = true;\n          PrivateDevices = true;\n          ProtectKernelTunables = true;\n          ProtectControlGroups = true;\n          RestrictSUIDSGID = true;\n          PrivateMounts = true;\n          ProtectKernelModules = true;\n          ProtectKernelLogs = true;\n          ProtectHostname = true;\n          ProtectClock = true;\n          ProtectProc = \"invisible\";\n          ProcSubset = \"pid\";\n          RestrictNamespaces = true;\n          RemoveIPC = true;\n          UMask = \"0077\";\n          LimitNOFILE = cfg.openFilesLimit;\n          NoNewPrivileges = true;\n          LockPersonality = true;\n          RestrictRealtime = true;\n          MemoryDenyWriteExecute = true;\n        };\n      };\n\n      # ensure volumes exist:\n      systemd.tmpfiles.settings.\"copyparty\" = (\n        lib.attrsets.mapAttrs' (\n          name: value:\n          lib.attrsets.nameValuePair (value.path) {\n            d = {\n              #: in front of things means it wont change it if the directory already exists.\n              group = \":${cfg.group}\";\n              user = \":${cfg.user}\";\n              mode = \":${\n                # Use volume permissions if set\n                if (value.flags ? chmod_d) then\n                  value.flags.chmod_d\n                # Else, use global permission if set\n                else if (cfg.settings ? chmod-d) then\n                  cfg.settings.chmod-d\n                # Else, use the default permission\n                else\n                  \"755\"\n              }\";\n            };\n          }\n        ) volumesWithoutVariables\n      );\n\n      users.groups = lib.mkIf (cfg.group == \"copyparty\") {\n        copyparty = { };\n      };\n      users.users = lib.mkIf (cfg.user == \"copyparty\") {\n        copyparty = {\n          description = \"Service user for copyparty\";\n          group = cfg.group;\n          home = externalStateDir;\n          isSystemUser = true;\n        };\n      };\n      environment.systemPackages = lib.mkIf cfg.mkHashWrapper [\n        (pkgs.writeShellScriptBin \"copyparty-hash\" ''\n          set -a  # automatically export variables\n          # set same environment variables as the systemd service\n          ${lib.pipe config.systemd.services.copyparty.environment [\n            (lib.filterAttrs (n: v: v != null && n != \"PATH\"))\n            (lib.mapAttrs (_: v: \"${v}\"))\n            (lib.toShellVars)\n          ]}\n          PATH=${config.systemd.services.copyparty.environment.PATH}:$PATH\n\n          exec ${command} --ah-cli\n        '')\n      ];\n    }\n  );\n}\n"
  },
  {
    "path": "contrib/openrc/copyparty",
    "content": "#!/sbin/openrc-run\n\n# this will start `/usr/local/bin/copyparty-sfx.py`\n# and share '/mnt' with anonymous read+write\n#\n# installation:\n#   cp -pv copyparty /etc/init.d && rc-update add copyparty\n#\n# you may want to:\n#   change '/usr/bin/python' to another interpreter\n#   change '/mnt::rw' to another location or permission-set\n#   use a config file instead of command arguments, e.g.:\n#      command_args=\"-c /etc/copyparty.conf\"\n\nname=\"$SVCNAME\"\ncommand_background=true\nextra_commands=\"checkconfig\"\npidfile=\"/var/run/$SVCNAME.pid\"\n\ncommand=\"/usr/bin/python3 /usr/local/bin/copyparty-sfx.py\"\ncommand_args=\"-q -v /mnt::rw\"\n\ncheckconfig() {\n        ebegin \"Checking $RC_SVCNAME configuration\"\n        $command $command_args --exit cfg\n        eend $?\n}\n"
  },
  {
    "path": "contrib/package/arch/PKGBUILD",
    "content": "# Maintainer: icxes <dev.null@need.moe>\n# Contributor: Morgan Adamiec <morganamilo@archlinux.org>\n# NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead.\n\npkgname=copyparty\npkgver=\"1.20.12\"\npkgrel=1\npkgdesc=\"File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++\"\narch=(\"any\")\nurl=\"https://github.com/9001/${pkgname}\"\nlicense=('MIT')\ndepends=(\"bash\" \"python\" \"lsof\" \"python-jinja\")\nmakedepends=(\"python-wheel\" \"python-setuptools\" \"python-build\" \"python-installer\" \"make\" \"pigz\")\noptdepends=(\"ffmpeg: thumbnails for videos, images (slower) and audio, music tags\"\n            \"cfssl: generate TLS certificates on startup\"\n            \"python-mutagen: music tags (alternative)\"\n            \"python-paramiko: sftp server\",\n            \"python-pillow: thumbnails for images\"\n            \"python-pyvips: thumbnails for images (higher quality, faster, uses more ram)\"\n            \"libkeyfinder: detection of musical keys\"\n            \"python-pyopenssl: ftps functionality\"\n            \"python-pyzmq: send zeromq messages from event-hooks\"\n            \"python-argon2-cffi: hashed passwords in config\"\n)\nsource=(\"https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz\")\nbackup=(\"etc/${pkgname}/copyparty.conf\" )\nsha256sums=(\"3acb98e9c577245517db92e77519caeac8a3e382dbc8704ec4a8701c7d58ba76\")\n\nbuild() {\n    cd \"${srcdir}/${pkgname}-${pkgver}/copyparty/web\"\n    make\n\n    cd \"${srcdir}/${pkgname}-${pkgver}\"\n    python -m build --wheel --no-isolation\n}\n\npackage() {\n    cd \"${srcdir}/${pkgname}-${pkgver}\"\n    python -m installer --destdir=\"$pkgdir\" dist/*.whl\n\n    install -dm755 \"${pkgdir}/etc/${pkgname}\"\n    install -Dm755 \"bin/prisonparty.sh\" \"${pkgdir}/usr/bin/prisonparty\"\n    install -Dm644 \"contrib/systemd/${pkgname}.conf\" \"${pkgdir}/etc/${pkgname}/copyparty.conf\"\n    install -Dm644 \"contrib/systemd/${pkgname}@.service\" \"${pkgdir}/usr/lib/systemd/system/${pkgname}@.service\"\n    install -Dm644 \"contrib/systemd/${pkgname}-user.service\" \"${pkgdir}/usr/lib/systemd/user/${pkgname}.service\"\n    install -Dm644 \"contrib/systemd/prisonparty@.service\" \"${pkgdir}/usr/lib/systemd/system/prisonparty@.service\"\n    install -Dm644 \"contrib/systemd/index.md\" \"${pkgdir}/var/lib/${pkgname}-jail/README.md\"\n    install -Dm644 \"LICENSE\" \"${pkgdir}/usr/share/licenses/${pkgname}/LICENSE\"\n}\n"
  },
  {
    "path": "contrib/package/makedeb-mpr/PKGBUILD",
    "content": "# Contributor: Beethoven <beethovenisadog@protonmail.com>\n\n\npkgname=copyparty\npkgver=1.20.12\npkgrel=1\npkgdesc=\"File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++\"\narch=(\"any\")\nurl=\"https://github.com/9001/${pkgname}\"\nlicense=('MIT')\ndepends=(\"bash\" \"python3\" \"lsof\" \"python3-jinja2\")\nmakedepends=(\"python3-wheel\" \"python3-setuptools\" \"python3-build\" \"python3-installer\" \"make\" \"pigz\")\noptdepends=(\"ffmpeg: thumbnails for videos, images (slower) and audio, music tags\"\n            \"golang-cfssl: generate TLS certificates on startup\"\n            \"python3-mutagen: music tags (alternative)\"\n            \"python3-paramiko: sftp server\"\n            \"python3-pil: thumbnails for images\"\n            \"python3-openssl: ftps functionality\"\n            \"python3-zmq: send zeromq messages from event-hooks\"\n            \"python3-argon2: hashed passwords in config\"\n)\nsource=(\"https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz\")\nbackup=(\"/etc/${pkgname}.d/init\" )\nsha256sums=(\"3acb98e9c577245517db92e77519caeac8a3e382dbc8704ec4a8701c7d58ba76\")\n\nbuild() {\n    cd \"${srcdir}/${pkgname}-${pkgver}/copyparty/web\"\n    make\n\n    cd \"${srcdir}/${pkgname}-${pkgver}\"\n    python -m build --wheel --no-isolation\n}\n\npackage() {\n    cd \"${srcdir}/${pkgname}-${pkgver}\"\n    python -m installer --destdir=\"$pkgdir\" dist/*.whl\n\n    install -dm755 \"${pkgdir}/etc/${pkgname}.d\"\n    install -Dm755 \"bin/prisonparty.sh\" \"${pkgdir}/usr/bin/prisonparty\"\n    install -Dm644 \"contrib/package/makedeb-mpr/${pkgname}.conf\" \"${pkgdir}/etc/${pkgname}.d/init\"\n    install -Dm644 \"contrib/package/makedeb-mpr/${pkgname}.service\" \"${pkgdir}/usr/lib/systemd/system/${pkgname}.service\"\n    install -Dm644 \"contrib/package/makedeb-mpr/prisonparty.service\" \"${pkgdir}/usr/lib/systemd/system/prisonparty.service\"\n    install -Dm644 \"contrib/package/makedeb-mpr/index.md\" \"${pkgdir}/var/lib/${pkgname}-jail/README.md\"\n    install -Dm644 \"LICENSE\" \"${pkgdir}/usr/share/licenses/${pkgname}/LICENSE\"\n}\n"
  },
  {
    "path": "contrib/package/makedeb-mpr/copyparty.conf",
    "content": "## import all *.conf files from the current folder (/etc/copyparty.d)\n% ./\n\n# add additional .conf files to this folder;\n# see example config files for reference:\n# https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf\n# https://github.com/9001/copyparty/tree/hovudstraum/docs/copyparty.d\n"
  },
  {
    "path": "contrib/package/makedeb-mpr/copyparty.service",
    "content": "# this will start `/usr/bin/copyparty-sfx.py`\n# and read config from `/etc/copyparty.d/*.conf`\n#\n# you probably want to:\n#   change \"User=cpp\" and \"/home/cpp/\" to another user\n#\n# unless you add -q to disable logging, you may want to remove the\n#   following line to allow buffering (slightly better performance):\n#   Environment=PYTHONUNBUFFERED=x\n\n[Unit]\nDescription=copyparty file server\n\n[Service]\nType=notify\nSyslogIdentifier=copyparty\nEnvironment=PYTHONUNBUFFERED=x\nWorkingDirectory=/var/lib/copyparty-jail\nExecReload=/bin/kill -s USR1 $MAINPID\n\n# user to run as + where the TLS certificate is (if any)\nUser=cpp\nEnvironment=XDG_CONFIG_HOME=/home/cpp/.config\n\n# stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running\nExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo \"x /tmp/pe-copyparty*\" > /run/tmpfiles.d/copyparty.conf'\n\n# run copyparty\nExecStart=/usr/bin/python3 /usr/local/bin/copyparty -c /etc/copyparty.d/init\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "contrib/package/makedeb-mpr/index.md",
    "content": "this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured\n\nplease add some `*.conf` files to `/etc/copyparty.d/`\n"
  },
  {
    "path": "contrib/package/makedeb-mpr/prisonparty.service",
    "content": "# this will start `/usr/bin/copyparty-sfx.py`\n# in a chroot, preventing accidental access elsewhere,\n# and read copyparty config from `/etc/copyparty.d/*.conf`\n#\n# expose additional filesystem locations to copyparty\n#   by listing them between the last `cpp` and `--`\n#\n# `cpp cpp` = user/group to run copyparty as; can be IDs (1000 1000)\n#\n# unless you add -q to disable logging, you may want to remove the\n#   following line to allow buffering (slightly better performance):\n#   Environment=PYTHONUNBUFFERED=x\n\n[Unit]\nDescription=copyparty file server\n\n[Service]\nSyslogIdentifier=prisonparty\nEnvironment=PYTHONUNBUFFERED=x\nWorkingDirectory=/var/lib/copyparty-jail\nExecReload=/bin/kill -s USR1 $MAINPID\n\n# stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running\nExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo \"x /tmp/pe-copyparty*\" > /run/tmpfiles.d/copyparty.conf'\n\n# run copyparty\nExecStart=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail cpp cpp \\\n  /etc/copyparty.d \\\n  -- \\\n  /usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "contrib/package/nix/copyparty/default.nix",
    "content": "{\n  lib,\n  buildPythonApplication,\n  fetchurl,\n  util-linux,\n  python,\n  setuptools,\n  jinja2,\n  impacket,\n  pyopenssl,\n  cfssl,\n  argon2-cffi,\n  pillow,\n  pyvips,\n  pyzmq,\n  ffmpeg,\n  mutagen,\n  paramiko,\n  pyftpdlib,\n  magic,\n  partftpy,\n  fusepy, # for partyfuse\n\n  # use argon2id-hashed passwords in config files (sha2 is always available)\n  withHashedPasswords ? true,\n\n  # generate TLS certificates on startup (pointless when reverse-proxied)\n  withCertgen ? false,\n\n  # create thumbnails with Pillow; faster than FFmpeg / MediaProcessing\n  withThumbnails ? true,\n\n  # create thumbnails with PyVIPS; even faster, uses more memory\n  # -- can be combined with Pillow to support more filetypes\n  withFastThumbnails ? false,\n\n  # enable FFmpeg; thumbnails for most filetypes (also video and audio), extract audio metadata, transcode audio to opus\n  # -- possibly dangerous if you allow anonymous uploads, since FFmpeg has a huge attack surface\n  # -- can be combined with Thumbnails and/or FastThumbnails, since FFmpeg is slower than both\n  withMediaProcessing ? true,\n\n  # if MediaProcessing is not enabled, you probably want this instead (less accurate, but much safer and faster)\n  withBasicAudioMetadata ? false,\n\n  # send ZeroMQ messages from event-hooks\n  withZeroMQ ? true,\n\n  # enable SFTP server\n  withSFTP ? false,\n\n  # enable FTP server\n  withFTP ? true,\n\n  # enable FTPS support in the FTP server\n  withFTPS ? false,\n\n  # enable TFTP server\n  withTFTP ? false,\n\n  # samba/cifs server; dangerous and buggy, enable if you really need it\n  withSMB ? false,\n\n  # enables filetype detection for nameless uploads\n  withMagic ? false,\n\n  # extra packages to add to the PATH\n  extraPackages ? [ ],\n\n  # function that accepts a python packageset and returns a list of packages to\n  # be added to the python venv. useful for scripts and such that require\n  # additional dependencies\n  extraPythonPackages ? (_p: [ ]),\n\n  # to build stable + unstable with the same file\n  stable ? true,\n\n  # for commit date, only used when stable = false\n  copypartyFlake ? null,\n\n  nix-gitignore,\n}:\n\nlet\n  pinData = lib.importJSON ./pin.json;\n  runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg);\n  inherit (copypartyFlake) lastModifiedDate;\n  # ex: \"1970\" \"01\" \"01\"\n  dateStringsZeroPrefixed = {\n    year = builtins.substring 0 4 lastModifiedDate;\n    month = builtins.substring 4 2 lastModifiedDate;\n    day = builtins.substring 6 2 lastModifiedDate;\n  };\n  # ex: \"1970\" \"1\" \"1\"\n  dateStringsShort = builtins.mapAttrs (_: val: toString (lib.toIntBase10 val)) dateStringsZeroPrefixed;\n  unstableVersion =\n    if copypartyFlake == null then\n      \"${pinData.version}-unstable\"\n    else\n      with dateStringsZeroPrefixed; \"${pinData.version}-unstable-${year}-${month}-${day}\"\n  ;\n  version = if stable then pinData.version else unstableVersion;\n  stableSrc = fetchurl {\n    inherit (pinData) url hash;\n  };\n  root = ../../../..;\n  unstableSrc = nix-gitignore.gitignoreSource [] root;\n  src = if stable then stableSrc else unstableSrc;\n  rev = copypartyFlake.shortRev or copypartyFlake.dirtyShortRev or \"unknown\";\n  unstableCodename = \"unstable\" + (lib.optionalString (copypartyFlake != null) \"-${rev}\");\nin\nbuildPythonApplication {\n  pname = \"copyparty\";\n  inherit version src;\n  postPatch = lib.optionalString (!stable) ''\n    old_src=\"$(mktemp -d)\"\n    tar -C \"$old_src\" -xf ${stableSrc}\n    declare -a folders\n    folders=(\"$old_src\"/*)\n    count_folders=\"''${#folders[@]}\"\n    if [[ $count_folders != 1 ]]; then\n      declare -p folders\n      echo \"Expected 1 folder, found $count_folders\" >&2\n      exit 1\n    fi\n    old_src_folder=\"''${folders[0]}\"\n    cp -r \"$old_src_folder\"/copyparty/web/deps copyparty/web/deps\n    sed -i 's/^CODENAME =.*$/CODENAME = \"${unstableCodename}\"/' copyparty/__version__.py\n    ${lib.optionalString (copypartyFlake != null) (with dateStringsShort; ''\n      sed -i 's/^BUILD_DT =.*$/BUILD_DT = (${year}, ${month}, ${day})/' copyparty/__version__.py\n    '')}\n  '';\n  dependencies =\n    [\n      jinja2\n      fusepy\n    ]\n    ++ lib.optional withSMB impacket\n    ++ lib.optional withSFTP paramiko\n    ++ lib.optional withFTP pyftpdlib\n    ++ lib.optional withFTPS pyopenssl\n    ++ lib.optional withTFTP partftpy\n    ++ lib.optional withCertgen cfssl\n    ++ lib.optional withThumbnails pillow\n    ++ lib.optional withFastThumbnails pyvips\n    ++ lib.optional withMediaProcessing ffmpeg\n    ++ lib.optional withBasicAudioMetadata mutagen\n    ++ lib.optional withHashedPasswords argon2-cffi\n    ++ lib.optional withZeroMQ pyzmq\n    ++ lib.optional withMagic magic\n    ++ (extraPythonPackages python.pkgs);\n  makeWrapperArgs = [ \"--prefix PATH : ${lib.makeBinPath runtimeDeps}\" ];\n\n  pyproject = true;\n  build-system = [\n    setuptools\n  ];\n  meta = {\n    description = \"Turn almost any device into a file server\";\n    longDescription = ''\n      Portable file server with accelerated resumable uploads, dedup, WebDAV, SFTP,\n      FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps\n    '';\n    homepage = \"https://github.com/9001/copyparty\";\n    changelog = \"https://github.com/9001/copyparty/releases/tag/v${pinData.version}\";\n    license = lib.licenses.mit;\n    mainProgram = \"copyparty\";\n    sourceProvenance = [ lib.sourceTypes.fromSource ];\n  };\n}\n"
  },
  {
    "path": "contrib/package/nix/copyparty/pin.json",
    "content": "{\n    \"url\": \"https://github.com/9001/copyparty/releases/download/v1.20.12/copyparty-1.20.12.tar.gz\",\n    \"version\": \"1.20.12\",\n    \"hash\": \"sha256-OsuY6cV3JFUX25LndRnK6sij44LbyHBOxKhwHH1YunY=\"\n}"
  },
  {
    "path": "contrib/package/nix/copyparty/update.py",
    "content": "#!/usr/bin/env python3\n\n# Update the Nix package pin\n#\n# Usage: ./update.sh [PATH]\n# When the [PATH] is not set, it will fetch the latest release from the repo.\n# With [PATH] set, it will hash the given file and generate the URL,\n# base on the version contained within the file\n\nimport base64\nimport json\nimport hashlib\nimport sys\nimport tarfile\nfrom pathlib import Path\n\nOUTPUT_FILE = Path(\"pin.json\")\nTARGET_ASSET = lambda version: f\"copyparty-{version}.tar.gz\"\nHASH_TYPE = \"sha256\"\nLATEST_RELEASE_URL = \"https://api.github.com/repos/9001/copyparty/releases/latest\"\nDOWNLOAD_URL = lambda version: f\"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET(version)}\"\n\n\ndef get_formatted_hash(binary):\n    hasher = hashlib.new(\"sha256\")\n    hasher.update(binary)\n    asset_hash = hasher.digest()\n    encoded_hash = base64.b64encode(asset_hash).decode(\"ascii\")\n    return f\"{HASH_TYPE}-{encoded_hash}\"\n\n\ndef version_from_tar_gz(path):\n    with tarfile.open(path) as tarball:\n        release_name = tarball.getmembers()[0].name\n        prefix = \"copyparty-\"\n\n        if release_name.startswith(prefix):\n            return release_name.replace(prefix, \"\")\n    raise ValueError(\"version not found in provided file\")\n\n\ndef remote_release_pin():\n    import requests\n\n    response = requests.get(LATEST_RELEASE_URL).json()\n    version = response[\"tag_name\"].lstrip(\"v\")\n    asset_info = [a for a in response[\"assets\"] if a[\"name\"] == TARGET_ASSET(version)][0]\n    download_url = asset_info[\"browser_download_url\"]\n    asset = requests.get(download_url)\n    formatted_hash = get_formatted_hash(asset.content)\n\n    result = {\"url\": download_url, \"version\": version, \"hash\": formatted_hash}\n    return result\n\n\ndef local_release_pin(path):\n    version = version_from_tar_gz(path)\n    download_url = DOWNLOAD_URL(version)\n    formatted_hash = get_formatted_hash(path.read_bytes())\n\n    result = {\"url\": download_url, \"version\": version, \"hash\": formatted_hash}\n    return result\n\n\ndef main():\n    if len(sys.argv) > 1:\n        asset_path = Path(sys.argv[1])\n        result = local_release_pin(asset_path)\n    else:\n        result = remote_release_pin()\n\n    print(result)\n    json_result = json.dumps(result, indent=4)\n    OUTPUT_FILE.write_text(json_result)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "contrib/package/nix/overlay.nix",
    "content": "final: prev:\nlet\n  fullAttrs = {\n    withHashedPasswords = true;\n    withCertgen = true;\n    withThumbnails = true;\n    withFastThumbnails = true;\n    withMediaProcessing = true;\n    withBasicAudioMetadata = true;\n    withZeroMQ = true;\n    withSFTP = true;\n    withFTP = true;\n    withFTPS = true;\n    withTFTP = true;\n    withSMB = true;\n    withMagic = true;\n  };\n\n  call = attrs: final.python3.pkgs.callPackage ./copyparty ({ ffmpeg = final.ffmpeg-full; } // attrs);\nin\n{\n  copyparty = call { stable = true; };\n  copyparty-unstable = call { stable = false; };\n  copyparty-full = call (fullAttrs // { stable = true; });\n  copyparty-unstable-full = call (fullAttrs // { stable = false; });\n\n  python3 = prev.python3.override {\n    packageOverrides = pyFinal: pyPrev: {\n      partftpy = pyFinal.callPackage ./partftpy { };\n    };\n  };\n}\n"
  },
  {
    "path": "contrib/package/nix/partftpy/default.nix",
    "content": "{\n  lib,\n  buildPythonPackage,\n  fetchurl,\n  setuptools,\n}:\nlet\n  pinData = lib.importJSON ./pin.json;\nin\n\nbuildPythonPackage rec {\n  pname = \"partftpy\";\n  inherit (pinData) version;\n  pyproject = true;\n\n  src = fetchurl {\n    inherit (pinData) url hash;\n  };\n\n  build-system = [ setuptools ];\n\n  pythonImportsCheck = [ \"partftpy.TftpServer\" ];\n\n  meta = {\n    description = \"Pure Python TFTP library  (copyparty edition)\";\n    homepage = \"https://github.com/9001/partftpy\";\n    changelog = \"https://github.com/9001/partftpy/releases/tag/${version}\";\n    license = lib.licenses.mit;\n  };\n}\n"
  },
  {
    "path": "contrib/package/nix/partftpy/pin.json",
    "content": "{\n    \"url\": \"https://github.com/9001/partftpy/releases/download/v0.4.0/partftpy-0.4.0.tar.gz\",\n    \"version\": \"0.4.0\",\n    \"hash\": \"sha256-5Q2zyuJ892PGZmb+YXg0ZPW/DK8RDL1uE0j5HPd4We0=\"\n}"
  },
  {
    "path": "contrib/package/nix/partftpy/update.py",
    "content": "#!/usr/bin/env python3\n\n# Update the Nix package pin\n#\n# Usage: ./update.sh\n\nimport base64\nimport json\nimport hashlib\nimport sys\nfrom pathlib import Path\n\nOUTPUT_FILE = Path(\"pin.json\")\nTARGET_ASSET = lambda version: f\"partftpy-{version}.tar.gz\"\nHASH_TYPE = \"sha256\"\nLATEST_RELEASE_URL = \"https://api.github.com/repos/9001/partftpy/releases/latest\"\n\n\ndef get_formatted_hash(binary):\n    hasher = hashlib.new(\"sha256\")\n    hasher.update(binary)\n    asset_hash = hasher.digest()\n    encoded_hash = base64.b64encode(asset_hash).decode(\"ascii\")\n    return f\"{HASH_TYPE}-{encoded_hash}\"\n\n\ndef remote_release_pin():\n    import requests\n\n    response = requests.get(LATEST_RELEASE_URL).json()\n    version = response[\"tag_name\"].lstrip(\"v\")\n    asset_info = [a for a in response[\"assets\"] if a[\"name\"] == TARGET_ASSET(version)][0]\n    download_url = asset_info[\"browser_download_url\"]\n    asset = requests.get(download_url)\n    formatted_hash = get_formatted_hash(asset.content)\n\n    result = {\"url\": download_url, \"version\": version, \"hash\": formatted_hash}\n    return result\n\n\ndef main():\n    result = remote_release_pin()\n\n    print(result)\n    json_result = json.dumps(result, indent=4)\n    OUTPUT_FILE.write_text(json_result)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "contrib/package/rpm/copyparty.spec",
    "content": "Name:           copyparty\nVersion:        $pkgver\nRelease:        $pkgrel\nLicense:        MIT\nGroup:          Utilities\nURL:            https://github.com/9001/copyparty\nSource0:        copyparty-$pkgver.tar.gz\nSummary:        File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++\nBuildArch:      noarch\nBuildRequires:  python3, python3-devel, pyproject-rpm-macros, python-setuptools, python-wheel, make\nRequires:       python3, (python3-jinja2 or python-jinja2), lsof\nRecommends:     ffmpeg, (golang-github-cloudflare-cfssl or cfssl), python-mutagen, python-pillow, python-pyvips\nRecommends:     qm-vamp-plugins, python-argon2-cffi, (python-pyopenssl or pyopenssl), python-paramiko, python-impacket\n\n%description\nPortable file server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps\n\nSee release at https://github.com/9001/copyparty/releases\n\n%global debug_package %{nil}\n\n%generate_buildrequires\n%pyproject_buildrequires\n\n%prep\n%setup -q\n\n%build\ncd \"copyparty/web\"\nmake\ncd -\n%pyproject_wheel\n\n%install\nmkdir -p %{buildroot}%{_bindir}\nmkdir -p %{buildroot}%{_libdir}/systemd/{system,user}\nmkdir -p %{buildroot}/etc/%{name}\nmkdir -p %{buildroot}/var/lib/%{name}-jail\nmkdir -p %{buildroot}%{_datadir}/licenses/%{name}\n\n%pyproject_install\n%pyproject_save_files copyparty\n\ninstall -m 0755 bin/prisonparty.sh                       %{buildroot}%{_bindir}/prisonpary.sh\ninstall -m 0644 contrib/systemd/%{name}.conf             %{buildroot}/etc/%{name}/%{name}.conf\ninstall -m 0644 contrib/systemd/%{name}@.service         %{buildroot}%{_libdir}/systemd/system/%{name}@.service\ninstall -m 0644 contrib/systemd/%{name}-user.service     %{buildroot}%{_libdir}/systemd/user/%{name}.service\ninstall -m 0644 contrib/systemd/prisonparty@.service     %{buildroot}%{_libdir}/systemd/system/prisonparty@.service\ninstall -m 0644 contrib/systemd/index.md                 %{buildroot}/var/lib/%{name}-jail/README.md\ninstall -m 0644 LICENSE                                  %{buildroot}%{_datadir}/licenses/%{name}/LICENSE\n\n%files -n copyparty -f %{pyproject_files}\n%license LICENSE\n%{_bindir}/copyparty\n%{_bindir}/partyfuse\n%{_bindir}/u2c\n%{_bindir}/prisonpary.sh\n/etc/%{name}/%{name}.conf\n%{_libdir}/systemd/system/%{name}@.service\n%{_libdir}/systemd/user/%{name}.service\n%{_libdir}/systemd/system/prisonparty@.service\n/var/lib/%{name}-jail/README.md\n"
  },
  {
    "path": "contrib/plugins/README.md",
    "content": "# example resource files\n\ncan be provided to copyparty to tweak things\n\n\n\n## example `.epilogue.html`\nsave one of these as `.epilogue.html` inside a folder to customize it:\n\n* [`minimal-up2k.html`](minimal-up2k.html) will [simplify the upload ui](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png)\n\n\n\n## example browser-js\npoint `--js-browser` to one of these by URL:\n\n* [`minimal-up2k.js`](minimal-up2k.js) is similar to the above `minimal-up2k.html` except it applies globally to all write-only folders\n* [`quickmove.js`](quickmove.js) adds a hotkey to move selected files into a subfolder\n* [`up2k-hooks.js`](up2k-hooks.js) lets you specify a ruleset for files to skip uploading\n  * [`up2k-hook-ytid.js`](up2k-hook-ytid.js) is a more specific example checking youtube-IDs against some API\n\n\n\n## example any-js\npoint `--js-browser` and/or `--js-other` to one of these by URL:\n\n* [`banner.js`](banner.js) shows a very enterprise [legal-banner](https://github.com/user-attachments/assets/8ae8e087-b209-449c-b08d-74e040f0284b)\n\n\n\n## example browser-css\npoint `--css-browser` to one of these by URL:\n\n* [`browser-icons.css`](browser-icons.css) adds filetype icons\n\n\n\n## meadup.js\n\n* turns copyparty into chromecast just more flexible (and probably way more buggy)\n* usage: put the js somewhere in the webroot and `--js-browser /memes/meadup.js`\n\n\n\n# junk\n\n* [**rave.js**](./rave.js): april-fools joke, [demo (epilepsy warning)](https://cd.ocv.me/b/d2/d21/#af-9b927c42,sorthref), not maintained, very buggy"
  },
  {
    "path": "contrib/plugins/banner.js",
    "content": "(function() {\n\n// usage: copy this to '.banner.js' in your webroot,\n// and run copyparty with the following arguments:\n// --js-browser /.banner.js  --js-other /.banner.js\n\n\n\n// had to pick the most chuuni one as the default\nvar bannertext = '' +\n'<h3>You are accessing a U.S. Government (USG) Information System (IS) that is provided for USG-authorized use only.</h3>' +\n'<p>By using this IS (which includes any device attached to this IS), you consent to the following conditions:</p>' +\n'<ul>' +\n'<li>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.</li>' +\n'<li>At any time, the USG may inspect and seize data stored on this IS.</li>' +\n'<li>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.</li>' +\n'<li>This IS includes security measures (e.g., authentication and access controls) to protect USG interests -- not for your personal benefit or privacy.</li>' +\n'<li>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.</li>' +\n'</ul>';\n\n\n\n// fancy div to insert into pages\nfunction bannerdiv(border) {\n    var ret = mknod('div', null, bannertext);\n    if (border)\n        ret.setAttribute(\"style\", \"border:1em solid var(--fg); border-width:.3em 0; margin:3em 0\");\n    return ret;\n}\n\n\n\n// keep all of these false and then selectively enable them in the if-blocks below\nvar show_msgbox = false,\n    login_top = false,\n    top = false,\n    bottom = false,\n    top_bordered = false,\n    bottom_bordered = false;\n\nif (QS(\"h1#cc\") && QS(\"a#k\")) {\n    // this is the controlpanel\n    // (you probably want to keep just one of these enabled)\n    show_msgbox = true;\n    login_top = true;\n    bottom = true;\n}\nelse if (ebi(\"swin\") && ebi(\"smac\")) {\n    // this is the connect-page, same deal here\n    show_msgbox = true;\n    top_bordered = true;\n    bottom_bordered = true;\n}\nelse if (ebi(\"op_cfg\") || ebi(\"div#mw\") ) {\n    // we're running in the main filebrowser (op_cfg) or markdown-viewer/editor (div#mw),\n    // fragile pages which break if you do something too fancy\n    show_msgbox = true;\n}\n\n\n\n// shows a fullscreen messagebox; works on all pages\nif (show_msgbox) {\n    var now = Math.floor(Date.now() / 1000),\n        last_shown = sread(\"bannerts\") || 0;\n\n    // 60 * 60 * 17 = 17 hour cooldown\n    if (now - last_shown > 60 * 60 * 17) {\n        swrite(\"bannerts\", now);\n        modal.confirm(bannertext, null, function () {\n            location = 'https://this-page-intentionally-left-blank.org/';\n        });\n    }\n}\n\n// show a message on the page footer; only works on the connect-page\nif (top || top_bordered) {\n    var dst = ebi('wrap');\n    dst.insertBefore(bannerdiv(top_bordered), dst.firstChild);\n}\n\n// show a message on the page footer; only works on the controlpanel and connect-page\nif (bottom || bottom_bordered) {\n    ebi('wrap').appendChild(bannerdiv(bottom_bordered));\n}\n\n// show a message on the top of the page; only works on the controlpanel\nif (login_top) {\n    var dst = QS('h1');\n    dst.parentNode.insertBefore(bannerdiv(false), dst);\n}\n\n})();\n"
  },
  {
    "path": "contrib/plugins/browser-icons.css",
    "content": "/* video, alternative 1:\n   top-left icon, just like the other formats\n=======================================================================\n\n#ggrid>a:is(\n[href$=\".mkv\"i],\n[href$=\".mp4\"i],\n[href$=\".webm\"i],\n):before {\n    content: '📺';\n}\n*/\n\n\n\n/* video, alternative 2:\n   play-icon in the middle of the thumbnail\n=======================================================================\n*/\n#ggrid>a:is(\n[href$=\".mkv\"i],\n[href$=\".mp4\"i],\n[href$=\".webm\"i],\n) {\n\tposition: relative;\n\toverflow: hidden;\n}\n#ggrid>a:is(\n[href$=\".mkv\"i],\n[href$=\".mp4\"i],\n[href$=\".webm\"i],\n):before {\n    content: '▶';\n\topacity: .8;\n\tmargin: 0;\n\tpadding: 1em .5em 1em .7em;\n\tborder-radius: 9em;\n\tline-height: 0;\n\tcolor: #fff;\n\ttext-shadow: none;\n\tbackground: rgba(0, 0, 0, 0.7);\n\tleft: calc(50% - 1em);\n\ttop: calc(50% - 1.4em);\n}\n\n\n\n/* audio */\n#ggrid>a:is(\n[href$=\".mp3\"i],\n[href$=\".ogg\"i],\n[href$=\".opus\"i],\n[href$=\".flac\"i],\n[href$=\".m4a\"i],\n[href$=\".aac\"i],\n):before {\n    content: '🎵';\n}\n\n\n\n/* image */\n#ggrid>a:is(\n[href$=\".jpg\"i],\n[href$=\".jpeg\"i],\n[href$=\".png\"i],\n[href$=\".gif\"i],\n[href$=\".webp\"i],\n):before {\n    content: '🎨';\n}\n"
  },
  {
    "path": "contrib/plugins/graft-thumbs.js",
    "content": "// USAGE:\n//   place this file somewhere in the webroot and then\n//   python3 -m copyparty --js-browser /.res/graft-thumbs.js\n//\n// DESCRIPTION:\n//   this is a gridview plugin which, for each file in a folder,\n//   looks for another file with the same filename (but with a\n//   different file extension)\n//\n//   if one of those files is an image and the other is not,\n//   then this plugin assumes the image is a \"sidecar thumbnail\"\n//   for the other file, and it will graft the image thumbnail\n//   onto the non-image file (for example an mp3)\n//\n//   optional feature 1, default-enabled:\n//   the image-file is then hidden from the directory listing\n//\n//   optional feature 2, default-enabled:\n//   when clicking the audio file, the image will also open\n\n\n(function() {\n\n\t// `graft_thumbs` assumes the gridview has just been rendered;\n\t// it looks for sidecars, and transplants those thumbnails onto\n\t// the other file with the same basename (filename sans extension)\n\n\tvar graft_thumbs = function () {\n\t\tif (!thegrid.en)\n\t\t\treturn;  // not in grid mode\n\n\t\tvar files = msel.getall(),\n\t\t\tpairs = {};\n\n\t\tconsole.log(files);\n\n\t\tfor (var a = 0; a < files.length; a++) {\n\t\t\tvar file = files[a],\n\t\t\t\tis_pic = /\\.(jpe?g|png|gif|webp|jxl)$/i.exec(file.vp),\n\t\t\t\tis_audio = re_au_all.exec(file.vp),\n\t\t\t\tbasename = file.vp.replace(/\\.[^\\.]+$/, \"\"),\n\t\t\t\tentry = pairs[basename];\n\n\t\t\tif (!entry)\n\t\t\t\t// first time seeing this basename; create a new entry in pairs\n\t\t\t\tentry = pairs[basename] = {};\n\n\t\t\tif (is_pic)\n\t\t\t\tentry.thumb = file;\n\t\t\telse if (is_audio)\n\t\t\t\tentry.audio = file;\n\t\t}\n\n\t\tvar basenames = Object.keys(pairs);\n\t\tfor (var a = 0; a < basenames.length; a++)\n\t\t\t(function(a) {\n\t\t\t\tvar pair = pairs[basenames[a]];\n\n\t\t\t\tif (!pair.thumb || !pair.audio)\n\t\t\t\t\treturn;  // not a matching pair of files\n\n\t\t\t\tvar img_thumb = QS('#ggrid a[ref=\"' + pair.thumb.id + '\"] img[onload]'),\n\t\t\t\t\timg_audio = QS('#ggrid a[ref=\"' + pair.audio.id + '\"] img[onload]');\n\n\t\t\t\tif (!img_thumb || !img_audio)\n\t\t\t\t\treturn;  // something's wrong... let's bail\n\n\t\t\t\t// alright, graft the thumb...\n\t\t\t\timg_audio.src = img_thumb.src;\n\n\t\t\t\t// ...and hide the sidecar\n\t\t\t\timg_thumb.closest('a').style.display = 'none';\n\n\t\t\t\t// ...and add another onclick-handler to the audio,\n\t\t\t\t// so it also opens the pic while playing the song\n\t\t\t\timg_audio.addEventListener('click', function() {\n\t\t\t\t\timg_thumb.click();\n\t\t\t\t\treturn false;  // let it bubble to the next listener\n\t\t\t\t});\n\n\t\t\t})(a);\n\t};\n\n\t// ...and then the trick! near the end of loadgrid,\n\t// thegrid.bagit is called to initialize the baguettebox\n\t// (image/video gallery); this is the perfect function to\n\t// \"hook\" (hijack) so we can run our code :^)\n\n\t// need to grab a backup of the original function first,\n\tvar orig_func = thegrid.bagit;\n\n\t// and then replace it with our own:\n\tthegrid.bagit = function (isrc) {\n\n\t\tif (isrc !== '#ggrid')\n\t\t\t// we only want to modify the grid, so\n\t\t\t// let the original function handle this one\n\t\t\treturn orig_func(isrc);\n\n\t\tgraft_thumbs();\n\n\t\t// when changing directories, the grid is\n\t\t// rendered before msel returns the correct\n\t\t// filenames, so schedule another run:\n\t\tsetTimeout(graft_thumbs, 1);\n\n\t\t// and finally, call the original thegrid.bagit function\n\t\treturn orig_func(isrc);\n\t};\n\n\tif (ls0) {\n\t\t// the server included an initial listing json (ls0),\n\t\t// so the grid has already been rendered without our hook\n\t\tgraft_thumbs();\n\t}\n\n})();\n"
  },
  {
    "path": "contrib/plugins/meadup.js",
    "content": "// USAGE:\n//   place this file somewhere in the webroot and then\n//   python3 -m copyparty --js-browser /memes/meadup.js\n//\n// FEATURES:\n// * adds an onscreen keyboard for operating a media center remotely,\n//    relies on https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/very-bad-idea.py\n// * adds an interactive anime girl (if you can find the dependencies)\n\nvar hambagas = [\n    \"https://www.youtube.com/watch?v=pFA3KGp4GuU\"\n];\n\n// keybaord,\n//   onscreen keyboard by @steinuil\nfunction initKeybaord(BASE_URL, HAMBAGA, consoleLog, consoleError) {\n    document.querySelector('.keybaord-container').innerHTML = `\n      <div class=\"keybaord-body\">\n        <div class=\"keybaord-row keybaord-row-1\">\n          <div class=\"keybaord-key\" data-keybaord-key=\"Escape\">\n            esc\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"F1\">\n            F1\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"F2\">\n            F2\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"F3\">\n            F3\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"F4\">\n            F4\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"F5\">\n            F5\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"F6\">\n            F6\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"F7\">\n            F7\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"F8\">\n            F8\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"F9\">\n            F9\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"F10\">\n            F10\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"F11\">\n            F11\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"F12\">\n            F12\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"Insert\">\n            ins\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"Delete\">\n            del\n          </div>\n        </div>\n        <div class=\"keybaord-row keybaord-row-2\">\n          <div class=\"keybaord-key\" data-keybaord-key=\"\\`\">\n            \\`\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"1\">\n            1\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"2\">\n            2\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"3\">\n            3\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"4\">\n            4\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"5\">\n            5\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"6\">\n            6\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"7\">\n            7\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"8\">\n            8\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"9\">\n            9\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"0\">\n            0\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"-\">\n            -\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"=\">\n            =\n          </div>\n          <div class=\"keybaord-key keybaord-backspace\" data-keybaord-key=\"BackSpace\">\n            backspace\n          </div>\n        </div>\n        <div class=\"keybaord-row keybaord-row-3\">\n          <div class=\"keybaord-key keybaord-tab\" data-keybaord-key=\"Tab\">\n            tab\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"q\">\n            q\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"w\">\n            w\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"e\">\n            e\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"r\">\n            r\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"t\">\n            t\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"y\">\n            y\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"u\">\n            u\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"i\">\n            i\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"o\">\n            o\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"p\">\n            p\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"[\">\n            [\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"]\">\n            ]\n          </div>\n          <div class=\"keybaord-key keybaord-enter\" data-keybaord-key=\"Return\">\n            enter\n          </div>\n        </div>\n        <div class=\"keybaord-row keybaord-row-4\">\n          <div class=\"keybaord-key keybaord-capslock\" data-keybaord-key=\"HAMBAGA\">\n            🍔\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"a\">\n            a\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"s\">\n            s\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"d\">\n            d\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"f\">\n            f\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"g\">\n            g\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"h\">\n            h\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"j\">\n            j\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"k\">\n            k\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"l\">\n            l\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\";\">\n            ;\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"'\">\n            '\n          </div>\n          <div class=\"keybaord-key keybaord-backslash\" data-keybaord-key=\"\\\\\">\n            \\\\\n          </div>\n        </div>\n        <div class=\"keybaord-row keybaord-row-5\">\n          <div class=\"keybaord-key keybaord-lshift\" data-keybaord-key=\"Shift_L\">\n            shift\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"\\\\\">\n            \\\\\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"z\">\n            z\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"x\">\n            x\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"c\">\n            c\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"v\">\n            v\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"b\">\n            b\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"n\">\n            n\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"m\">\n            m\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\",\">\n            ,\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\".\">\n            .\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"/\">\n            /\n          </div>\n          <div class=\"keybaord-key keybaord-rshift\" data-keybaord-key=\"Shift_R\">\n            shift\n          </div>\n        </div>\n        <div class=\"keybaord-row keybaord-row-6\">\n          <div class=\"keybaord-key keybaord-lctrl\" data-keybaord-key=\"Control_L\">\n            ctrl\n          </div>\n          <div class=\"keybaord-key keybaord-super\" data-keybaord-key=\"Meta_L\">\n            win\n          </div>\n          <div class=\"keybaord-key keybaord-alt\" data-keybaord-key=\"Alt_L\">\n            alt\n          </div>\n          <div class=\"keybaord-key keybaord-spacebar\" data-keybaord-key=\"space\">\n            space\n          </div>\n          <div class=\"keybaord-key keybaord-altgr\" data-keybaord-key=\"Alt_R\">\n            altgr\n          </div>\n          <div class=\"keybaord-key keybaord-what\" data-keybaord-key=\"Menu\">\n            menu\n          </div>\n          <div class=\"keybaord-key keybaord-rctrl\" data-keybaord-key=\"Control_R\">\n            ctrl\n          </div>\n        </div>\n        <div class=\"keybaord-row\">\n          <div class=\"keybaord-key\" data-keybaord-key=\"XF86AudioLowerVolume\">\n            🔉\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"XF86AudioRaiseVolume\">\n            🔊\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"Left\">\n            ⬅️\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"Down\">\n            ⬇️\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"Up\">\n            ⬆️\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"Right\">\n            ➡️\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"Page_Up\">\n            PgUp\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"Page_Down\">\n            PgDn\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"Home\">\n            🏠\n          </div>\n          <div class=\"keybaord-key\" data-keybaord-key=\"End\">\n            End\n          </div>\n        </div>\n      <div>\n    `;\n\n    function arraySample(array) {\n        return array[Math.floor(Math.random() * array.length)];\n    }\n\n    function sendMessage(msg) {\n        return fetch(BASE_URL, {\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/x-www-form-urlencoded;charset=UTF-8\",\n            },\n            body: \"msg=\" + encodeURIComponent(msg),\n        }).then(\n            (r) => r.text(), // so the response body shows up in network tab\n            (err) => consoleError(err)\n        );\n    }\n    const MODIFIER_ON_CLASS = \"keybaord-modifier-on\";\n    const KEY_DATASET = \"data-keybaord-key\";\n    const KEY_CLASS = \"keybaord-key\";\n\n    const modifiers = new Set()\n\n    function toggleModifier(button, key) {\n        button.classList.toggle(MODIFIER_ON_CLASS);\n        if (modifiers.has(key)) {\n            modifiers.delete(key);\n        } else {\n            modifiers.add(key);\n        }\n    }\n\n    function popModifiers() {\n        let modifierString = \"\";\n\n        modifiers.forEach((mod) => {\n            document.querySelector(\"[\" + KEY_DATASET + \"='\" + mod + \"']\")\n                .classList.remove(MODIFIER_ON_CLASS);\n\n            modifierString += mod + \"+\";\n        });\n\n        modifiers.clear();\n\n        return modifierString;\n    }\n\n    Array.from(document.querySelectorAll(\".\" + KEY_CLASS)).forEach((button) => {\n        const key = button.dataset.keybaordKey;\n\n        button.addEventListener(\"click\", (ev) => {\n            switch (key) {\n                case \"HAMBAGA\":\n                    sendMessage(arraySample(HAMBAGA));\n                    break;\n\n                case \"Shift_L\":\n                case \"Shift_R\":\n\n                case \"Control_L\":\n                case \"Control_R\":\n\n                case \"Meta_L\":\n\n                case \"Alt_L\":\n                case \"Alt_R\":\n                    toggleModifier(button, key);\n                    break;\n\n                default: {\n                    const keyWithModifiers = popModifiers() + key;\n\n                    consoleLog(keyWithModifiers);\n\n                    sendMessage(\"key \" + keyWithModifiers)\n                        .then(() => consoleLog(keyWithModifiers + \" OK\"));\n                }\n            }\n        });\n    });\n}\n\n\n// keybaord integration\n(function () {\n    var o = mknod('div');\n    clmod(o, 'keybaord-container', 1);\n    ebi('op_msg').appendChild(o);\n\n    o = mknod('style');\n    o.innerHTML = `\n.keybaord-body {\n\tdisplay: flex;\n\tflex-flow: column nowrap;\n    margin: .6em 0;\n}\n\n.keybaord-row {\n\tdisplay: flex;\n}\n\n.keybaord-key {\n\tborder: 1px solid rgba(128,128,128,0.2);\n\twidth: 41px;\n\theight: 40px;\n\n\tdisplay: flex;\n\tjustify-content: center;\n\talign-items: center;\n}\n\n.keybaord-key:active {\n\tbackground-color: lightgrey;\n}\n\n.keybaord-key.keybaord-modifier-on {\n\tbackground-color: lightblue;\n}\n\n.keybaord-key.keybaord-backspace {\n\twidth: 82px;\n}\n\n.keybaord-key.keybaord-tab {\n\twidth: 55px;\n}\n\n.keybaord-key.keybaord-enter {\n\twidth: 69px;\n}\n\n.keybaord-key.keybaord-capslock {\n\twidth: 80px;\n}\n\n.keybaord-key.keybaord-backslash {\n\twidth: 88px;\n}\n\n.keybaord-key.keybaord-lshift {\n\twidth: 65px;\n}\n\n.keybaord-key.keybaord-rshift {\n\twidth: 103px;\n}\n\n.keybaord-key.keybaord-lctrl {\n\twidth: 55px;\n}\n\n.keybaord-key.keybaord-super {\n\twidth: 55px;\n}\n\n.keybaord-key.keybaord-alt {\n\twidth: 55px;\n}\n\n.keybaord-key.keybaord-altgr {\n\twidth: 55px;\n}\n\n.keybaord-key.keybaord-what {\n\twidth: 55px;\n}\n\n.keybaord-key.keybaord-rctrl {\n\twidth: 55px;\n}\n\n.keybaord-key.keybaord-spacebar {\n\twidth: 302px;\n}\n`;\n    document.head.appendChild(o);\n\n    initKeybaord('/', hambagas,\n        (msg) => { toast.inf(2, msg.toString()) },\n        (msg) => { toast.err(30, msg.toString()) });\n})();\n\n\n// live2d (dumb pointless meme)\n//   dependencies for this part are not tracked in git\n//   so delete this section if you wanna use this file\n//   (or supply your own l2d model and js)\n(function () {\n    var o = mknod('link');\n    o.setAttribute('rel', 'stylesheet');\n    o.setAttribute('href', \"/bad-memes/pio.css\");\n    document.head.appendChild(o);\n\n    o = mknod('style');\n    o.innerHTML = '.pio-container{text-shadow:none;z-index:1}';\n    document.head.appendChild(o);\n\n    o = mknod('div');\n    clmod(o, 'pio-container', 1);\n    o.innerHTML = '<div class=\"pio-action\"></div><canvas id=\"pio\" width=\"280\" height=\"500\"></canvas>';\n    document.body.appendChild(o);\n\n    var remaining = 3;\n    for (var a of ['pio', 'l2d', 'fireworks']) {\n        import_js(`/bad-memes/${a}.js`, function () {\n            if (remaining --> 1)\n                return;\n\n            o = mknod('script');\n            o.innerHTML = 'var pio = new Paul_Pio({\"selector\":[],\"mode\":\"fixed\",\"hidden\":false,\"content\":{\"close\":\"ok bye\"},\"model\":[\"/bad-memes/sagiri/model.json\"]});';\n            document.body.appendChild(o);\n        });\n    }\n})();\n"
  },
  {
    "path": "contrib/plugins/minimal-up2k.html",
    "content": "<!--\n  NOTE: DEPRECATED; please use the javascript version instead:\n  https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/minimal-up2k.js\n\n  ----\n\n  save this as .epilogue.html inside a write-only folder to declutter the UI,  makes it look like\n  https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png\n\n  only works if you disable the prologue/epilogue sandbox with --no-sb-lg\n  which should probably be combined with --no-dot-ren to prevent damage\n  (`no_sb_lg` can also be set per-volume with volflags)\n-->\n\n<style>\n\n    /* make the up2k ui REALLY minimal by hiding a bunch of stuff: */\n\n    #ops, #tree, #path, #wfp,  /* main tabs and navigators (tree/breadcrumbs) */\n\n    #u2conf tr:first-child>td[rowspan]:not(#u2btn_cw),  /* most of the config options */\n\n    #srch_dz, #srch_zd,  /* the filesearch dropzone */\n\n    #u2cards, #u2etaw  /* and the upload progress tabs */\n\n    {display: none !important}  /* do it! */\n\n\n\n    /* add some margins because now it's weird */\n    .opview {margin-top: 2.5em}\n    #op_up2k {margin-top: 6em}\n\n    /* and embiggen the upload button */\n    #u2conf #u2btn, #u2btn {padding:1.5em 0}\n\n    /* adjust the button area a bit */\n    #u2conf.w, #u2conf.ww {width: 35em !important; margin: 5em auto}\n\n    /* a */\n    #op_up2k {min-height: 0}\n\n</style>\n\n<a href=\"#\" onclick=\"this.parentNode.innerHTML='';\">show advanced options</a>\n"
  },
  {
    "path": "contrib/plugins/minimal-up2k.js",
    "content": "/*\n\nmakes the up2k ui REALLY minimal by hiding a bunch of stuff\n\nalmost the same as minimal-up2k.html except this one...:\n\n -- applies to every write-only folder when used with --js-browser\n\n -- only applies if javascript is enabled\n\n -- doesn't hide the total upload ETA display\n\n -- looks slightly better\n\n\n========================\n== USAGE INSTRUCTIONS ==\n\n1. create a volume which anyone can read from (if you haven't already)\n2. copy this file into that volume, so anyone can download it\n3. enable the plugin by telling the webbrowser to load this file;\n    assuming the URL to the public volume is /res/, and\n    assuming you're using config-files, then add this to your config:\n\n    [global]\n      js-browser: /res/minimal-up2k.js\n\nalternatively, if you're not using config-files, then\nadd the following commandline argument instead:\n  --js-browser=/res/minimal-up2k.js\n\n*/\n\nvar u2min = `\n<style>\n\n#ops, #path, #tree, #files, #wfp,\n#u2conf td.c+.c, #u2cards, #srch_dz, #srch_zd {\n  display: none !important;\n}\n#u2conf {margin:5em auto 0 auto !important}\n#u2conf.ww {width:70em}\n#u2conf.w {width:50em}\n#u2conf.w .c,\n#u2conf.w #u2btn_cw {text-align:left}\n#u2conf.w #u2btn_cw {width:70%}\n#u2etaw {margin:3em auto}\n#u2etaw.w {\n  text-align: center;\n  margin: -3.5em auto 5em auto;\n}\n#u2etaw.w #u2etas {margin-right:-37em}\n#u2etaw.w #u2etas.o {margin-top:-2.2em}\n#u2etaw.ww {margin:-1em auto}\n#u2etaw.ww #u2etas {padding-left:4em}\n#u2etas {\n  background: none !important;\n  border: none !important;\n}\n#wrap {margin-left:2em !important}\n.logue {\n  border: none !important;\n  margin: 2em auto !important;\n}\n.logue:before {content:'' !important}\n\n</style>\n\n<a href=\"#\" onclick=\"this.parentNode.innerHTML='';\">show advanced options</a>\n`;\n\nif (!has(perms, 'read')) {\n  var e2 = mknod('div');\n  e2.innerHTML = u2min;\n  ebi('wrap').insertBefore(e2, QS('#wfp'));\n}\n"
  },
  {
    "path": "contrib/plugins/quickmove.js",
    "content": "\"use strict\";\n\n\n// USAGE:\n//   place this file somewhere in the webroot, \n//   for example in a folder named \".res\" to hide it, and then\n//   python3 copyparty-sfx.py -v .::A --js-browser /.res/quickmove.js\n//\n// DESCRIPTION:\n//   the command above launches copyparty with one single volume;\n//   \".::A\" = current folder as webroot, and everyone has Admin\n//\n//   the plugin adds hotkey \"W\" which moves all selected files\n//   into a subfolder named \"foobar\" inside the current folder\n\n\n(function() {\n\n    var action_to_perform = ask_for_confirmation_and_then_move;\n    // this decides what the new hotkey should do;\n    //  ask_for_confirmation_and_then_move  =  show a yes/no box,\n    //  move_selected_files  =  just move the files immediately\n\n    var move_destination = \"foobar\";\n    // this is the target folder to move files to;\n    // by default it is a subfolder of the current folder,\n    // but it can also be an absolute path like \"/foo/bar\"\n\n    // ===\n    // ===   END OF CONFIG\n    // ===\n\n    var main_hotkey_handler,  // copyparty's original hotkey handler\n        plugin_enabler,  // timer to engage this plugin when safe\n        files_to_move;  // list of files to move\n\n    function ask_for_confirmation_and_then_move() {\n        var num_files = msel.getsel().length,\n            msg = \"move the selected \" + num_files + \" files?\";\n\n        if (!num_files)\n            return toast.warn(2, 'no files were selected to be moved');\n\n        modal.confirm(msg, move_selected_files, null);\n    }\n\n    function move_selected_files() {\n        var selection = msel.getsel();\n\n        if (!selection.length)\n            return toast.warn(2, 'no files were selected to be moved');\n\n        if (thegrid.bbox) {\n            // close image/video viewer\n            thegrid.bbox = null;\n            baguetteBox.destroy();\n        }\n\n        files_to_move = [];\n        for (var a = 0; a < selection.length; a++)\n            files_to_move.push(selection[a].vp);\n\n        move_next_file();\n    }\n\n    function move_next_file() {\n        var num_files = files_to_move.length,\n            filepath = files_to_move.pop(),\n            filename = vsplit(filepath)[1];\n\n        toast.inf(10, \"moving \" + num_files + \" files...\\n\\n\" + filename);\n\n        var dst = move_destination;\n\n        if (!dst.endsWith('/'))\n            // must have a trailing slash, so add it\n            dst += '/';\n\n        if (!dst.startsWith('/'))\n            // destination is a relative path, so prefix current folder path\n            dst = get_evpath() + dst;\n\n        // and finally append the filename\n        dst += '/' + filename;\n\n        // prepare the move-request to be sent\n        var xhr = new XHR();\n        xhr.onload = xhr.onerror = function() {\n            if (this.status !== 201)\n                return toast.err(30, 'move failed: ' + esc(this.responseText));\n\n            if (files_to_move.length)\n                return move_next_file();  // still more files to go\n\n            toast.ok(1, 'move OK');\n            treectl.goto(); // reload the folder contents\n        };\n        xhr.open('POST', filepath + '?move=' + dst);\n        xhr.send();\n    }\n\n    function our_hotkey_handler(e) {\n        // bail if either ALT, CTRL, or SHIFT is pressed\n        if (anymod(e))\n            return main_hotkey_handler(e);  // let copyparty handle this keystroke\n\n        var keycode = (e.key || e.code) + '',\n    \t\tae = document.activeElement,\n\t\t    aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : '';\n\n        // check the current aet (active element type),\n        // only continue if one of the following currently has input focus:\n        //   nothing | link | button | table-row | table-cell | div | text\n        if (aet && !/^(a|button|tr|td|div|pre)$/.test(aet))\n            return main_hotkey_handler(e);  // let copyparty handle this keystroke\n\n        if (keycode == 'w' || keycode == 'KeyW') {\n            // okay, this one's for us... do the thing\n            action_to_perform();\n            return ev(e);\n        }\n\n        return main_hotkey_handler(e);  // let copyparty handle this keystroke\n    }\n\n    function enable_plugin() {\n        if (!window.hotkeys_attached)\n            return console.log('quickmove is waiting for the page to finish loading');\n\n        clearInterval(plugin_enabler);\n        main_hotkey_handler = document.onkeydown;\n        document.onkeydown = our_hotkey_handler;\n        console.log('quickmove is now enabled');\n    }\n\n    // copyparty doesn't enable its hotkeys until the page\n    // has finished loading, so we'll wait for that too\n    plugin_enabler = setInterval(enable_plugin, 100);\n\n})();\n"
  },
  {
    "path": "contrib/plugins/rave.js",
    "content": "/* untz untz untz untz */\n\n(function () {\n\n    var can, ctx, W, H, fft, buf, bars, barw, pv,\n        hue = 0,\n        ibeat = 0,\n        beats = [9001],\n        beats_url = '',\n        uofs = 0,\n        ops = ebi('ops'),\n        raving = false,\n        recalc = 0,\n        cdown = 0,\n        FC = 0.9,\n        css = `<style>\n\n#fft {\n    position: fixed;\n    top: 0;\n    left: 0;\n    z-index: -1;\n}\nbody {\n    box-shadow: inset 0 0 0 white;\n}\n#ops>a,\n#path>a {\n    display: inline-block;\n}\n/*\nbody.untz {\n    animation: untz-body 200ms ease-out;\n}\n@keyframes untz-body {\n\t0% {inset 0 0 20em white}\n\t100% {inset 0 0 0 white}\n}\n*/\n:root, html.a, html.b, html.c, html.d, html.e {\n    --row-alt: rgba(48,52,78,0.2);\n}\n#files td {\n    background: none;\n}\n\n</style>`;\n\n    QS('body').appendChild(mknod('div', null, css));\n\n    function rave_load() {\n        console.log('rave_load');\n        can = mknod('canvas', 'fft');\n        QS('body').appendChild(can);\n        ctx = can.getContext('2d');\n\n        fft = new AnalyserNode(actx, {\n            \"fftSize\": 2048,\n            \"maxDecibels\": 0,\n            \"smoothingTimeConstant\": 0.7,\n        });\n        ibeat = 0;\n        beats = [9001];\n        buf = new Uint8Array(fft.frequencyBinCount);\n        bars = buf.length * FC;\n        afilt.filters.push(fft);\n        if (!raving) {\n            raving = true;\n            raver();\n        }\n        beats_url = mp.au.src.split('?')[0].replace(/(.*\\/)(.*)/, '$1.beats/$2.txt');\n        console.log(\"reading beats from\", beats_url);\n        var xhr = new XHR();\n        xhr.open('GET', beats_url, true);\n        xhr.onload = readbeats;\n        xhr.url = beats_url;\n        xhr.send();\n    }\n\n    function rave_unload() {\n        qsr('#fft');\n        can = null;\n    }\n\n    function readbeats() {\n        if (this.url != beats_url)\n            return console.log('old beats??', this.url, beats_url);\n\n        var sbeats = this.responseText.replace(/\\r/g, '').split(/\\n/g);\n        if (sbeats.length < 3)\n            return;\n\n        beats = [];\n        for (var a = 0; a < sbeats.length; a++)\n            beats.push(parseFloat(sbeats[a]));\n\n        var end = beats.slice(-2),\n            t = end[1],\n            d = t - end[0];\n\n        while (d > 0.1 && t < 1200)\n            beats.push(t += d);\n    }\n\n    function hrand() {\n        return Math.random() - 0.5;\n    }\n\n    function raver() {\n        if (!can) {\n            raving = false;\n            return;\n        }\n\n        requestAnimationFrame(raver);\n        if (!mp || !mp.au || mp.au.paused)\n            return;\n\n        if (--uofs >= 0) {\n            document.body.style.marginLeft = hrand() * uofs + 'px';\n            ebi('tree').style.marginLeft = hrand() * uofs + 'px';\n            for (var a of QSA('#ops>a, #path>a, #pctl>a'))\n                a.style.transform = 'translate(' + hrand() * uofs * 1 + 'px, ' + hrand() * uofs * 0.7 + 'px) rotate(' + Math.random() * uofs * 0.7 + 'deg)'\n        }\n\n        if (--recalc < 0) {\n            recalc = 60;\n            var tree = ebi('tree'),\n                x = tree.style.display == 'none' ? 0 : tree.offsetWidth;\n\n            //W = can.width = window.innerWidth - x;\n            //H = can.height = window.innerHeight;\n            //H = ebi('widget').offsetTop;\n            W = can.width = bars;\n            H = can.height = 512;\n            barw = 1; //parseInt(0.8 + W / bars);\n            can.style.left = x + 'px';\n            can.style.width = (window.innerWidth - x) + 'px';\n            can.style.height = ebi('widget').offsetTop + 'px';\n        }\n\n        //if (--cdown == 1)\n        //    clmod(ops, 'untz');\n\n        fft.getByteFrequencyData(buf);\n\n        var imax = 0, vmax = 0;\n        for (var a = 10; a < 50; a++)\n            if (vmax < buf[a]) {\n                vmax = buf[a];\n                imax = a;\n            }\n\n        hue = hue * 0.93 + imax * 0.07;\n\n        ctx.fillStyle = 'rgba(0,0,0,0)';\n        ctx.fillRect(0, 0, W, H);\n        ctx.clearRect(0, 0, W, H);\n        ctx.fillStyle = 'hsla(' + (hue * 2.5) + ',100%,50%,0.7)';\n\n        var x = 0, mul = (H / 256) * 0.5;\n        for (var a = 0; a < buf.length * FC; a++) {\n            var v = buf[a] * mul * (1 + 0.69 * a / buf.length);\n            ctx.fillRect(x, H - v, barw, v);\n            x += barw;\n        }\n\n        var t = mp.au.currentTime + 0.05;\n\n        if (ibeat >= beats.length || beats[ibeat] > t)\n            return;\n\n        while (ibeat < beats.length && beats[ibeat++] < t)\n            continue;\n\n        return untz();\n\n        var cv = 0;\n        for (var a = 0; a < 128; a++)\n            cv += buf[a];\n\n        if (cv - pv > 1000) {\n            console.log(pv, cv, cv - pv);\n            if (cdown < 0) {\n                clmod(ops, 'untz', 1);\n                cdown = 20;\n            }\n        }\n        pv = cv;\n    }\n\n    function untz() {\n        console.log('untz');\n        uofs = 14;\n        document.body.animate([\n            { boxShadow: 'inset 0 0 1em #f0c' },\n            { boxShadow: 'inset 0 0 20em #f0c', offset: 0.2 },\n            { boxShadow: 'inset 0 0 0 #f0c' },\n        ], { duration: 200, iterations: 1 });\n    }\n\n    afilt.plugs.push({\n        \"en\": true,\n        \"load\": rave_load,\n        \"unload\": rave_unload\n    });\n\n})();\n"
  },
  {
    "path": "contrib/plugins/up2k-hook-ytid.js",
    "content": "// way more specific example --\n// assumes all files dropped into the uploader have a youtube-id somewhere in the filename,\n// locates the youtube-ids and passes them to an API which returns a list of IDs which should be uploaded\n//\n// also tries to find the youtube-id in the embedded metadata\n//\n// assumes copyparty is behind nginx as /ytq is a standalone service which must be rproxied in place\n\nfunction up2k_namefilter(good_files, nil_files, bad_files, hooks) {\n    var passthru = up2k.uc.fsearch;\n    if (passthru)\n        return hooks[0](good_files, nil_files, bad_files, hooks.slice(1));\n\n    a_up2k_namefilter(good_files, nil_files, bad_files, hooks).then(() => { });\n}\n\n// ebi('op_up2k').appendChild(mknod('input','unick'));\n\nfunction bstrpos(buf, ptn) {\n    var ofs = 0,\n        ch0 = ptn[0],\n        sz = buf.byteLength;\n\n    while (true) {\n        ofs = buf.indexOf(ch0, ofs);\n        if (ofs < 0 || ofs >= sz)\n            return -1;\n\n        for (var a = 1; a < ptn.length; a++)\n            if (buf[ofs + a] !== ptn[a])\n                break;\n\n        if (a === ptn.length)\n            return ofs;\n\n        ++ofs;\n    }\n}\n\nasync function a_up2k_namefilter(good_files, nil_files, bad_files, hooks) {\n    var t0 = Date.now(),\n        yt_ids = new Set(),\n        textdec = new TextDecoder('latin1'),\n        md_ptn = new TextEncoder().encode('youtube.com/watch?v='),\n        file_ids = [],  // all IDs found for each good_files\n        md_only = [],  // `${id} ${fn}` where ID was only found in metadata\n        mofs = 0,\n        mnchk = 0,\n        mfile = '',\n        myid = localStorage.getItem('ytid_t0');\n\n    if (!myid)\n        localStorage.setItem('ytid_t0', myid = Date.now());\n\n    for (var a = 0; a < good_files.length; a++) {\n        var [fobj, name] = good_files[a],\n            cname = name,  // will clobber\n            sz = fobj.size,\n            ids = [],\n            fn_ids = [],\n            md_ids = [],\n            id_ok = false,\n            m;\n\n        // all IDs found in this file\n        file_ids.push(ids);\n\n        // look for ID in filename; reduce the\n        // metadata-scan intensity if the id looks safe\n        m = /[\\[(-]([\\w-]{11})[\\])]?\\.(?:mp4|webm|mkv|flv|opus|ogg|mp3|m4a|aac)$/i.exec(name);\n        id_ok = !!m;\n\n        while (true) {\n            // fuzzy catch-all;\n            // some ytdl fork did %(title)-%(id).%(ext) ...\n            m = /(?:^|[^\\w])([\\w-]{11})(?:$|[^\\w-])/.exec(cname);\n            if (!m)\n                break;\n\n            cname = cname.replace(m[1], '');\n            yt_ids.add(m[1]);\n            fn_ids.unshift(m[1]);\n        }\n\n        // look for IDs in video metadata,\n        if (/\\.(mp4|webm|mkv|flv|opus|ogg|mp3|m4a|aac)$/i.exec(name)) {\n            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}`);\n\n            // check first and last 128 MiB;\n            // pWxOroN5WCo.mkv @  6edb98 (6.92M)\n            // Nf-nN1wF5Xo.mp4 @ 4a98034 (74.6M)\n            var chunksz = 1024 * 1024 * 2,  // byte\n                aspan = id_ok ? 128 : 512;  // MiB\n\n            aspan = parseInt(Math.min(sz / 2, aspan * 1024 * 1024) / chunksz) * chunksz;\n            if (!aspan)\n                aspan = Math.min(sz, chunksz);\n\n            for (var side = 0; side < 2; side++) {\n                var ofs = side ? Math.max(0, sz - aspan) : 0,\n                    nchunks = aspan / chunksz;\n\n                for (var chunk = 0; chunk < nchunks; chunk++) {\n                    var bchunk = await fobj.slice(ofs, ofs + chunksz + 16).arrayBuffer(),\n                        uchunk = new Uint8Array(bchunk, 0, bchunk.byteLength),\n                        bofs = bstrpos(uchunk, md_ptn),\n                        absofs = Math.min(ofs + bofs, (sz - ofs) + bofs),\n                        txt = bofs < 0 ? '' : textdec.decode(uchunk.subarray(bofs)),\n                        m;\n\n                    //console.log(`side ${ side }, chunk ${ chunk }, ofs ${ ofs }, bchunk ${ bchunk.byteLength }, txt ${ txt.length }`);\n                    while (true) {\n                        // mkv/webm have [a-z] immediately after url\n                        m = /(youtube\\.com\\/watch\\?v=[\\w-]{11})/.exec(txt);\n                        if (!m)\n                            break;\n\n                        txt = txt.replace(m[1], '');\n                        m = m[1].slice(-11);\n\n                        console.log(`found ${m} @${bofs}, ${name} `);\n                        yt_ids.add(m);\n                        if (!has(fn_ids, m) && !has(md_ids, m)) {\n                            md_ids.push(m);\n                            md_only.push(`${m} ${name}`);\n                        }\n                        else\n                            // id appears several times; make it preferred\n                            md_ids.unshift(m);\n\n                        // bail after next iteration\n                        chunk = nchunks - 1;\n                        side = 9;\n\n                        if (mofs < absofs) {\n                            mofs = absofs;\n                            mfile = name;\n                        }\n                    }\n                    ofs += chunksz;\n                    if (ofs >= sz)\n                        break;\n                }\n            }\n        }\n\n        for (var yi of md_ids)\n            ids.push(yi);\n\n        for (var yi of fn_ids)\n            if (!has(ids, yi))\n                ids.push(yi);\n    }\n\n    if (md_only.length)\n        console.log('recovered the following youtube-IDs by inspecting metadata:\\n\\n' + md_only.join('\\n'));\n    else if (yt_ids.size)\n        console.log('did not discover any additional youtube-IDs by inspecting metadata; all the IDs also existed in the filenames');\n    else\n        console.log('failed to find any youtube-IDs at all, sorry');\n\n    if (false) {\n        var msg = `finished analysing ${mnchk} files in ${(Date.now() - t0) / 1000} seconds,\\n\\nbiggest offset was ${mofs} in this file:\\n\\n${mfile}`,\n            mfun = function () { toast.ok(0, msg); };\n\n        mfun();\n        setTimeout(mfun, 200);\n\n        return hooks[0]([], [], [], hooks.slice(1));\n    }\n\n    var el = ebi('unick'), unick = el ? el.value : '';\n    if (unick) {\n        console.log(`sending uploader nickname [${unick}]`);\n        fetch(document.location, {\n            method: 'POST',\n            headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },\n            body: 'msg=' + encodeURIComponent(unick)\n        });\n    }\n\n    toast.inf(5, `running query for ${yt_ids.size} youtube-IDs...`);\n\n    var xhr = new XHR();\n    xhr.open('POST', '/ytq', true);\n    xhr.setRequestHeader('Content-Type', 'text/plain');\n    xhr.onload = xhr.onerror = function () {\n        if (this.status != 200)\n            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}`);\n\n        process_id_list(this.responseText);\n    };\n    xhr.send(Array.from(yt_ids).join('\\n'));\n\n    function process_id_list(txt) {\n        var wanted_ids = new Set(txt.trim().split('\\n')),\n            name_id = {},\n            wanted_names = new Set(),  // basenames with a wanted ID -- not including relpath\n            wanted_names_scoped = {},  // basenames with a wanted ID -> list of dirs to search under\n            wanted_files = new Set();  // filedrops\n\n        for (var a = 0; a < good_files.length; a++) {\n            var name = good_files[a][1];\n            for (var b = 0; b < file_ids[a].length; b++)\n                if (wanted_ids.has(file_ids[a][b])) {\n                    // let the next stage handle this to prevent dupes\n                    //wanted_files.add(good_files[a]);\n\n                    var m = /(.*)\\.(mp4|webm|mkv|flv|opus|ogg|mp3|m4a|aac)$/i.exec(name);\n                    if (!m)\n                        continue;\n\n                    var [rd, fn] = vsplit(m[1]);\n\n                    if (fn in wanted_names_scoped)\n                        wanted_names_scoped[fn].push(rd);\n                    else\n                        wanted_names_scoped[fn] = [rd];\n\n                    wanted_names.add(fn);\n                    name_id[m[1]] = file_ids[a][b];\n\n                    break;\n                }\n        }\n\n        // add all files with the same basename as each explicitly wanted file\n        // (infojson/chatlog/etc when ID was discovered from metadata)\n        for (var a = 0; a < good_files.length; a++) {\n            var [rd, name] = vsplit(good_files[a][1]);\n            for (var b = 0; b < 3; b++) {\n                name = name.replace(/\\.[^\\.]+$/, '');\n                if (!wanted_names.has(name))\n                    continue;\n\n                var vid_fp = false;\n                for (var c of wanted_names_scoped[name])\n                    if (rd.startsWith(c))\n                        vid_fp = c + name;\n\n                if (!vid_fp)\n                    continue;\n\n                var subdir = name_id[vid_fp];\n                subdir = `v${subdir.slice(0, 1)}/${subdir}-${myid}`;\n                var newpath = subdir + '/' + good_files[a][1].split(/\\//g).pop();\n\n                // check if this file is a dupe\n                for (var c of good_files)\n                    if (c[1] == newpath)\n                        newpath = null;\n\n                if (!newpath)\n                    break;\n\n                good_files[a][1] = newpath;\n                wanted_files.add(good_files[a]);\n                break;\n            }\n        }\n\n        function upload_filtered() {\n            if (!wanted_files.size)\n                return modal.alert('Good news -- turns out we already have all those.\\n\\nBut thank you for checking in!');\n\n            hooks[0](Array.from(wanted_files), nil_files, bad_files, hooks.slice(1));\n        }\n\n        function upload_all() {\n            hooks[0](good_files, nil_files, bad_files, hooks.slice(1));\n        }\n\n        var n_skip = good_files.length - wanted_files.size,\n            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\\n<code>OK</code> / <code>Enter</code> = continue uploading just the ${wanted_files.size} files we definitely need\\n\\n<code>Cancel</code> / <code>ESC</code> = override the filter; upload ALL the files you added`;\n\n        if (!n_skip)\n            upload_filtered();\n        else\n            modal.confirm(msg, upload_filtered, upload_all);\n    };\n}\n\nup2k_hooks.push(function () {\n    up2k.gotallfiles.unshift(up2k_namefilter);\n});\n\n// persist/restore nickname field if present\nsetInterval(function () {\n    var o = ebi('unick');\n    if (!o || document.activeElement == o)\n        return;\n\n    o.oninput = function () {\n        localStorage.setItem('unick', o.value);\n    };\n    o.value = localStorage.getItem('unick') || '';\n}, 1000);\n"
  },
  {
    "path": "contrib/plugins/up2k-hooks.js",
    "content": "// hooks into up2k\n\nfunction up2k_namefilter(good_files, nil_files, bad_files, hooks) {\n    // is called when stuff is dropped into the browser,\n    // after iterating through the directory tree and discovering all files,\n    // before the upload confirmation dialogue is shown\n\n    // good_files will successfully upload\n    // nil_files are empty files and will show an alert in the final hook\n    // bad_files are unreadable and cannot be uploaded\n    var file_lists = [good_files, nil_files, bad_files];\n\n    // build a list of filenames\n    var filenames = [];\n    for (var lst of file_lists)\n        for (var ent of lst)\n            filenames.push(ent[1]);\n\n    toast.inf(5, \"running database query...\");\n\n    // simulate delay while passing the list to some api for checking\n    setTimeout(function () {\n\n        // only keep webm files as an example\n        var new_lists = [];\n        for (var lst of file_lists) {\n            var keep = [];\n            new_lists.push(keep);\n\n            for (var ent of lst)\n                if (/\\.webm$/.test(ent[1]))\n                    keep.push(ent);\n        }\n\n        // finally, call the next hook in the chain\n        [good_files, nil_files, bad_files] = new_lists;\n        hooks[0](good_files, nil_files, bad_files, hooks.slice(1));\n\n    }, 1000);\n}\n\n// register\nup2k_hooks.push(function () {\n    up2k.gotallfiles.unshift(up2k_namefilter);\n});\n"
  },
  {
    "path": "contrib/podman-systemd/README.md",
    "content": "# copyparty with Podman and Systemd\n\nUse this configuration if you want to run copyparty in a Podman container, with the reliability of running the container under a systemd service.\n\nDocumentation 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.\n\nTo run copyparty in this way, you must already have podman installed. To install Podman, see: https://podman.io/docs/installation\n\nThere is a sample configuration file in the same directory as this file (`copyparty.conf`).\n\n## Run the container as root\n\nRunning 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.\n\nFirst, 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.\n\n```\n# Change /mnt to something you want to share\nVolume=/mnt:/w:z\n```\n\nNote 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`.\n\nTo install and start copyparty with Podman and systemd as the root user, run the following:\n\n```shell\nsudo mkdir -pv /etc/containers/systemd/ /etc/copyparty/\nsudo cp -v copyparty.container /etc/containers/systemd/\nsudo cp -v copyparty.conf /etc/copyparty/\nsudo systemctl daemon-reload\nsudo systemctl start copyparty\n```\n\nNote: 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.\n\nYou can see the status of the service with:\n\n```shell\nsudo systemctl status -a copyparty\n```\n\nYou can see (and follow) the logs with either of these commands:\n\n```shell\nsudo podman logs -f copyparty\n\n# -a is required or else you'll get output like: copyparty[549025]: [649B blob data]\nsudo journalctl -a -f -u copyparty\n```\n\n## Run the container as a non-root user\n\nThis 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.\n\nFirst, you need a user to run the container as. In this example we'll create a \"podman\" user with UID=1001 and GID=1001.\n\n```shell\nsudo groupadd -g 1001 podman\nsudo useradd -u 1001 -m podman\nsudo usermod -aG podman podman\nsudo loginctl enable-linger podman\n# Set a strong password for this user\nsudo -u podman passwd\n```\n\nThe `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.\n\nNext, 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:\n\n```\n# Change to reflect your non-root user's home directory\nVolume=/home/podman/copyparty/config:/cfg:z\n\n# Change to the directory you want to share\nVolume=/home/podman/copyparty/sharing:/w:z\n```\n\nMake sure the podman user has read/write access to both of these directories.\n\nNext, **log in to the server as the podman user**.\n\nTo install and start copyparty as the non-root podman user, run the following:\n\n```shell\nmkdir -pv /home/podman/.config/containers/systemd/ /home/podman/copyparty/config\ncp -v copyparty.container /home/podman/.config/containers/systemd/copyparty.container\ncp -v copyparty.conf /home/podman/copyparty/config\nsystemctl --user daemon-reload\nsystemctl --user start copyparty\n```\n\n**Important note: Never use `sudo` with `systemctl --user`!**\n\nYou can check the status of the user service with:\n\n```shell\nsystemctl --user status -a copyparty\n```\n\nYou can see (and follow) the logs with:\n\n```shell\npodman logs -f copyparty\n\njournalctl --user -a -f -u copyparty\n```\n\n## Troubleshooting\n\nIf 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:\n\n```shell\nsudo /usr/lib/systemd/system-generators/podman-system-generator --dryrun\n```\n\n## Allowing Traffic from Outside your Server\n\nTo allow traffic on port 3923 of your server, you should run:\n\n```shell\nsudo firewall-cmd --permanent --add-port=3923/tcp\nsudo firewall-cmd --reload\n```\n\nOtherwise, you won't be able to access the copyparty server from anywhere other than the server itself.\n\n## Updating copyparty\n\nTo update the version of copyparty used in the container, you can:\n\n```shell\n# If root:\nsudo podman pull docker.io/copyparty/ac:latest\nsudo systemctl restart copyparty\n\n# If non-root:\npodman pull docker.io/copyparty/ac:latest\nsystemctl --user restart copyparty\n```\n\nOr, you can change the pinned version of the image in the `[Container]` section of the `.container` file and run:\n\n```shell\n# If root:\nsudo systemctl daemon-reload\nsudo systemctl restart copyparty\n\n# If non-root:\nsystemctl --user daemon-reload\nsystemctl --user restart copyparty\n```\n\nPodman 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.\n\n### Enabling auto-update\n\nAlternatively, you can enable auto-updates by un-commenting this line:\n\n```\n# AutoUpdate=registry\n```\n\nYou will also need to enable the [podman auto-updater service](https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html) with:\n\n```shell\n# If root:\nsudo systemctl enable podman-auto-update.timer podman-auto-update.service\n\n# If non-root:\nsystemctl --user enable podman-auto-update.timer podman-auto-update.service\n```\n\nThis works best if you always want the latest version of copyparty. The auto-updater runs once every 24 hours.\n"
  },
  {
    "path": "contrib/podman-systemd/copyparty.conf",
    "content": "[global]\n  e2dsa  # enable file indexing and filesystem scanning\n  e2ts   # and enable multimedia indexing\n  ansi   # and colors in log messages\n\n  # uncomment the line starting with q, lo: to log to a file instead of stdout/journalctl;\n  # $LOGS_DIRECTORY is usually /var/log/copyparty (comes from systemd)\n  # and copyparty replaces %Y-%m%d with Year-MonthDay, so the\n  # full path will be something like /var/log/copyparty/2023-1130.txt\n  # (note: enable compression by adding .xz at the end)\n  # q, lo: $LOGS_DIRECTORY/%Y-%m%d.log\n\n  # p: 80,443,3923   # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE)\n  # i: 127.0.0.1     # only allow connections from localhost (reverse-proxies)\n  # ftp: 3921        # enable ftp server on port 3921\n  # p: 3939          # listen on another port\n  # df: 16           # stop accepting uploads if less than 16 GB free disk space\n  # ver              # show copyparty version in the controlpanel\n  # grid             # show thumbnails/grid-view by default\n  # theme: 2         # monokai\n  # name: datasaver  # change the server-name that's displayed in the browser\n  # stats, nos-dup   # enable the prometheus endpoint, but disable the dupes counter (too slow)\n  # no-robots, force-js  # make it harder for search engines to read your server\n\n  # enable version-checking by uncommenting one of the vc-url lines below;\n  # shows a warning-banner in the controlpanel if your version has a known vulnerability\n  #vc-url: https://api.github.com/repos/9001/copyparty/security-advisories?per_page=9\n  #vc-url: https://api.copyparty.eu/advisories\n  vc-exit  # panic and shutdown instead of just showing the warning\n\n\n[accounts]\n  ed: wark  # username: password\n\n\n[/]            # create a volume at \"/\" (the webroot), which will\n  /w           # share the contents of the \"/w\" folder\n  accs:\n    rw: *      # everyone gets read-write access, but\n    rwmda: ed  # the user \"ed\" gets read-write-move-delete-admin\n    # uid: 1000  # If you're running as root, you can change the owner of this volume here\n    # gid: 1000  # If you're running as root, you can change the group of this volume here\n"
  },
  {
    "path": "contrib/podman-systemd/copyparty.container",
    "content": "[Container]\n# It's recommended to replace :latest with a specific version\n# for example: docker.io/copyparty/ac:1.19.15\nImage=docker.io/copyparty/ac:latest\nContainerName=copyparty\n\n# Uncomment to enable auto-updates\n# AutoUpdate=registry\n\n# Environment variables\n# enable mimalloc by replacing \"NOPE\" with \"2\" for a nice speed-boost (will use twice as much ram)\nEnvironment=LD_PRELOAD=/usr/lib/libmimalloc-secure.so.NOPE\n# ensures log-messages are not delayed (but can reduce speed a tiny bit)\nEnvironment=PYTHONUNBUFFERED=1\n\n# Ports\nPublishPort=3923:3923\n\n\n# Volumes (PLEASE LOOK!)\n\n# Rootful setup:\n#   Leave as-is\n# Non-root setup:\n#   Change /etc/copyparty to /home/<USER>/copyparty/config\nVolume=/etc/copyparty:/cfg:z\n\n# Rootful setup:\n#   Change /mnt to the directory you want to share\n# Non-root setup:\n#   Change /mnt to something owned by your user, e.g., /home/<USER>/copyparty/sharing:/w:z\nVolume=/mnt:/w:z\n\n\n# Give the container time to stop in case the thumbnailer is still running.\n# It's allowed to continue finishing up for 10s after the shutdown signal, give it a 5s buffer\nStopTimeout=15\n\n# hide it from logs with \"/._\" so it matches the default --lf-url filter\nHealthCmd=\"wget --spider -q 127.0.0.1:3923/?reset=/._\"\nHealthInterval=1m\nHealthTimeout=2s\nHealthRetries=5\nHealthStartPeriod=15s\n\n[Unit]\nAfter=default.target\n\n[Install]\n# Start by default on boot\nWantedBy=default.target\n\n[Service]\n# Give the container time to start in case it needs to pull the image\nTimeoutStartSec=600\n"
  },
  {
    "path": "contrib/rc/copyparty",
    "content": "#!/bin/sh\n#\n# PROVIDE: copyparty\n# REQUIRE: networking\n# KEYWORD:\n\n. /etc/rc.subr\n\nname=\"copyparty\"\nrcvar=\"copyparty_enable\"\ncopyparty_user=\"copyparty\"\ncopyparty_args=\"-e2dsa -v /storage:/storage:r\" # change as you see fit\ncopyparty_command=\"/usr/local/bin/python3.11 /usr/local/copyparty/copyparty-sfx.py ${copyparty_args}\"\npidfile=\"/var/run/copyparty/${name}.pid\"\ncommand=\"/usr/sbin/daemon\"\ncommand_args=\"-P ${pidfile} -r -f ${copyparty_command}\"\n\nstop_postcmd=\"copyparty_shutdown\"\n\ncopyparty_shutdown()\n{\n        if [ -e \"${pidfile}\" ]; then\n                echo \"Stopping supervising daemon.\"\n                kill -s TERM `cat ${pidfile}`\n        fi\n}\n\nload_rc_config $name\n: ${copyparty_enable:=no}\n\nrun_rc_command \"$1\"\n"
  },
  {
    "path": "contrib/send-to-cpp.contextlet.json",
    "content": "{\n    \"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\",\n    \"contexts\": [\n        \"link\"\n    ],\n    \"icons\": null,\n    \"patterns\": \"\",\n    \"scope\": \"background\",\n    \"title\": \"send to cpp\",\n    \"type\": \"normal\"\n}"
  },
  {
    "path": "contrib/setup-ashell.sh",
    "content": "#!/bin/bash\n#\n# this script will install copyparty onto an iOS device (iPhone/iPad)\n#\n# step 1: install a-Shell:\n#   https://apps.apple.com/us/app/a-shell/id1473805438\n#\n# step 2: copypaste the following command into a-Shell:\n#   curl -L https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh | sh\n#\n# step 3: launch copyparty with this command: cpp\n#\n# if you ever want to upgrade copyparty, just repeat step 2\n\n\n\ncd \"$HOME/Documents\"\ncurl -Locopyparty https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py\n\n\n\n# create the config file? (cannot use heredoc because body too large)\n[ -e cpc ] || {\necho '[global]' >cpc\necho '  p: 80, 443, 3923  # enable http and https on these ports' >>cpc\necho '  e2dsa      # enable file indexing and filesystem scanning' >>cpc\necho '  e2ts       # and enable multimedia indexing' >>cpc\necho '  ver        # show copyparty version in the controlpanel' >>cpc\necho '  qrz: 2     # enable qr-code and make it big' >>cpc\necho '  qrp: 1     # reduce qr-code padding' >>cpc\necho '  qr-fg: -1  # optimize for basic/simple terminals' >>cpc\necho '  qr-wait: 0.3  # less chance of getting scrolled away' >>cpc\necho '' >>cpc\necho '  # enable these by uncommenting them:' >>cpc\necho '  # ftp: 21    # enable ftp server on port 21' >>cpc\necho '  # tftp: 69   # enable tftp server on port 69' >>cpc\necho '' >>cpc\necho '[/]' >>cpc\necho '  ~/Documents' >>cpc\necho '  accs:' >>cpc\necho '    A: *' >>cpc\n}\n\n\n\n# create the launcher?\n[ -e cpp ] || {\necho '#!/bin/sh' >cpp\necho '' >>cpp\necho '# change the font so the qr-code draws correctly:' >>cpp\necho 'config -n \"Menlo\"  # name' >>cpp\necho 'config -s 8  # size' >>cpp\necho '' >>cpp\necho '# launch copyparty' >>cpp\necho 'exec copyparty -c cpc \"$@\"' >>cpp\n}\n\n\n\nchmod 755 copyparty cpp\necho\necho =================================\necho\necho 'okay, all done!'\necho\necho 'you can edit your config'\necho 'with this command: vim cpc'\necho\necho 'you can run copyparty'\necho 'with this command: cpp'\necho\n"
  },
  {
    "path": "contrib/sharex.sxcu",
    "content": "{\n  \"Version\": \"15.0.0\",\n  \"Name\": \"copyparty\",\n  \"DestinationType\": \"ImageUploader\",\n  \"RequestMethod\": \"POST\",\n  \"RequestURL\": \"http://127.0.0.1:3923/sharex\",\n  \"Parameters\": {\n    \"j\": null\n  },\n  \"Headers\": {\n    \"pw\": \"PUT_YOUR_PASSWORD_HERE_MY_DUDE\"\n  },\n  \"Body\": \"MultipartFormData\",\n  \"Arguments\": {\n    \"act\": \"bput\"\n  },\n  \"FileFormName\": \"f\",\n  \"URL\": \"{json:files[0].url}\"\n}\n"
  },
  {
    "path": "contrib/sharex12.sxcu",
    "content": "{\n  \"Name\": \"copyparty\",\n  \"DestinationType\": \"ImageUploader, TextUploader, FileUploader\",\n  \"RequestURL\": \"http://127.0.0.1:3923/sharex\",\n  \"FileFormName\": \"f\",\n  \"Arguments\": {\n    \"act\": \"bput\"\n  },\n  \"Headers\": {\n    \"accept\": \"url\",\n    \"pw\": \"PUT_YOUR_PASSWORD_HERE_MY_DUDE\"\n  }\n}\n"
  },
  {
    "path": "contrib/systemd/cfssl.service",
    "content": "# NOTE: this is now a built-in feature in copyparty\n# but you may still want this if you have specific needs\n#\n# systemd service which generates a new TLS certificate on each boot,\n# that way the one-year expiry time won't cause any issues --\n# just have everyone trust the ca.pem once every 10 years\n#\n# assumptions/placeholder values:\n#  * this script and copyparty runs as user \"cpp\"\n#  * copyparty repo is at ~cpp/dev/copyparty\n#  * CA is named partylan\n#  * server IPs = 10.1.2.3 and 192.168.123.1\n#  * server hostname = party.lan\n\n[Unit]\nDescription=copyparty certificate generator\nBefore=copyparty.service\n\n[Service]\nUser=cpp\nType=oneshot\nSyslogIdentifier=cpp-cert\nExecStart=/bin/bash -c 'cd ~/dev/copyparty/contrib && ./cfssl.sh partylan 10.1.2.3,192.168.123.1,party.lan y'\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "contrib/systemd/copyparty-user.service",
    "content": "# this will start `/usr/bin/copyparty`\n# and read config from `$HOME/.config/copyparty.conf`\n#\n# unless you add -q to disable logging, you may want to remove the\n#   following line to allow buffering (slightly better performance):\n#   Environment=PYTHONUNBUFFERED=x\n\n[Unit]\nDescription=copyparty file server\n\n[Service]\nType=notify\nSyslogIdentifier=copyparty\nWorkingDirectory=/var/lib/copyparty-jail\nEnvironment=PYTHONUNBUFFERED=x\nEnvironment=PRTY_CONFIG=%h/.config/copyparty/copyparty.conf\nExecReload=/bin/kill -s USR1 $MAINPID\n\n# ensure there is a config\nExecStartPre=/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'\n\n# run copyparty\nExecStart=/usr/bin/python3 /usr/bin/copyparty\n\n[Install]\nWantedBy=default.target\n"
  },
  {
    "path": "contrib/systemd/copyparty.conf",
    "content": "[global]\n  i: 127.0.0.1\n\n[accounts]\n  user: password\n\n[/]\n  /var/lib/copyparty-jail\n  accs:\n    r: *\n    rwdma: user\n  flags:\n    grid"
  },
  {
    "path": "contrib/systemd/copyparty.example.conf",
    "content": "# not actually YAML but lets pretend:\n# -*- mode: yaml -*-\n# vim: ft=yaml:\n\n\n# put this file in /etc/\n\n\n[global]\n  e2dsa  # enable file indexing and filesystem scanning\n  e2ts   # and enable multimedia indexing\n  ansi   # and colors in log messages\n\n  # disable logging to stdout/journalctl and log to a file instead;\n  # $LOGS_DIRECTORY is usually /var/log/copyparty (comes from systemd)\n  # and copyparty replaces %Y-%m%d with Year-MonthDay, so the\n  # full path will be something like /var/log/copyparty/2023-1130.txt\n  # (note: enable compression by adding .xz at the end)\n  q, lo: $LOGS_DIRECTORY/%Y-%m%d.log\n\n  # enable version-checker by uncommenting one of the 'vc-url' lines below; this will\n  # periodically check if your copyparty version has a known security vulnerability,\n  # showing a warning on /?h (control-panel) for all users with permission 'a' or 'A'\n  #vc-url: https://api.github.com/repos/9001/copyparty/security-advisories?per_page=9\n  #vc-url: https://api.copyparty.eu/advisories\n\n  vc-age: 3  # how many hours to wait between each version-check\n  vc-exit  # emergency-exit if a version-check indicates that the current version is vulnerable\n\n  # p: 80,443,3923   # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE)\n  # i: 127.0.0.1     # only allow connections from localhost (reverse-proxies)\n  # ftp: 3921        # enable ftp server on port 3921\n  # p: 3939          # listen on another port\n  # df: 16           # stop accepting uploads if less than 16 GB free disk space\n  # ver              # show copyparty version in the controlpanel\n  # grid             # show thumbnails/grid-view by default\n  # theme: 2         # monokai\n  # name: datasaver  # change the server-name that's displayed in the browser\n  # stats, nos-dup   # enable the prometheus endpoint, but disable the dupes counter (too slow)\n  # no-robots, force-js  # make it harder for search engines to read your server\n\n\n[accounts]\n  ed: wark  # username: password\n\n\n[/]            # create a volume at \"/\" (the webroot), which will\n  /mnt         # share the contents of the \"/mnt\" folder\n  accs:\n    rw: *      # everyone gets read-write access, but\n    rwmda: ed  # the user \"ed\" gets read-write-move-delete-admin\n"
  },
  {
    "path": "contrib/systemd/copyparty.service",
    "content": "# this will start `/usr/local/bin/copyparty-sfx.py` and\n# read copyparty config from `/etc/copyparty.conf`, for example:\n# https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.conf\n#\n# installation:\n#   wget https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py -O /usr/local/bin/copyparty-sfx.py\n#   useradd -r -s /sbin/nologin -m -d /var/lib/copyparty copyparty\n#   firewall-cmd --permanent --add-port=3923/tcp  # --zone=libvirt\n#   firewall-cmd --reload\n#   cp -pv copyparty.service /etc/systemd/system/\n#   cp -pv copyparty.conf /etc/\n#   restorecon -vr /etc/systemd/system/copyparty.service  # on fedora/rhel\n#   systemctl daemon-reload && systemctl enable --now copyparty\n#\n# every time you edit this file, you must \"systemctl daemon-reload\"\n# for the changes to take effect and then \"systemctl restart copyparty\"\n#\n# if it fails to start, first check this: systemctl status copyparty\n# then try starting it while viewing logs:\n#   journalctl -fan 100\n#   tail -Fn 100 /var/log/copyparty/$(date +%Y-%m%d.log)\n#\n# if you run into any issues, for example thumbnails not working,\n# try removing the \"some quick hardening\" section and then please\n# let me know if that actually helped so we can look into it\n#\n# you may want to:\n#  - change \"User=copyparty\" and \"/var/lib/copyparty/\" to another user\n#  - edit /etc/copyparty.conf to configure copyparty\n# and in the ExecStart= line:\n#  - change '/usr/bin/python3' to another interpreter\n#\n# with `Type=notify`, copyparty will signal systemd when it is ready to\n#   accept connections; correctly delaying units depending on copyparty.\n#   But note that journalctl will get the timestamps wrong due to\n#   python disabling line-buffering, so messages are out-of-order:\n#   https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png\n#\n########################################################################\n########################################################################\n\n\n[Unit]\nDescription=copyparty file server\n\n[Service]\nType=notify\nSyslogIdentifier=copyparty\nEnvironment=PYTHONUNBUFFERED=x\nExecReload=/bin/kill -s USR1 $MAINPID\nPermissionsStartOnly=true\n\n## user to run as + where the TLS certificate is (if any)\n##\nUser=copyparty\nGroup=copyparty\nWorkingDirectory=/var/lib/copyparty\nEnvironment=XDG_CONFIG_HOME=/var/lib/copyparty/.config\n\n## OPTIONAL: allow copyparty to listen on low ports (like 80/443);\n##   you need to uncomment the \"p: 80,443,3923\" in the config too\n##   ------------------------------------------------------------\n##   a slightly safer alternative is to enable partyalone.service\n##   which does portforwarding with nftables instead, but an even\n##   better option is to use a reverse-proxy (nginx/caddy/...)\n##\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n\n## some quick hardening; TODO port more from the nixos package\n##\nMemoryMax=50%\nMemorySwapMax=50%\nProtectClock=true\nProtectControlGroups=true\nProtectHostname=true\nProtectKernelLogs=true\nProtectKernelModules=true\nProtectKernelTunables=true\nProtectProc=invisible\nRemoveIPC=true\nRestrictNamespaces=true\nRestrictRealtime=true\nRestrictSUIDSGID=true\n\n## create a directory for logfiles;\n##   this defines $LOGS_DIRECTORY which is used in copyparty.conf\n##\nLogsDirectory=copyparty\n\n## finally, start copyparty and give it the config file:\n##\nExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -c /etc/copyparty.conf\n\n# NOTE: if you installed copyparty from an OS package repo (nice)\n#   then you probably want something like this instead:\n#ExecStart=/usr/bin/copyparty -c /etc/copyparty.conf\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "contrib/systemd/copyparty@.service",
    "content": "# this will start `/usr/bin/copyparty`\n# and read config from `/etc/copyparty/copyparty.conf`\n#\n# the %i refers to whatever you put after the copyparty@\n#   so with copyparty@foo.service, %i == foo\n#\n# unless you add -q to disable logging, you may want to remove the\n#   following line to allow buffering (slightly better performance):\n#   Environment=PYTHONUNBUFFERED=x\n\n[Unit]\nDescription=copyparty file server\n\n[Service]\nType=notify\nSyslogIdentifier=copyparty\nWorkingDirectory=/var/lib/copyparty-jail\nEnvironment=PYTHONUNBUFFERED=x\nEnvironment=PRTY_CONFIG=/etc/copyparty/copyparty.conf\nExecReload=/bin/kill -s USR1 $MAINPID\n\n# user to run as + where the TLS certificate is (if any)\nUser=%i\nEnvironment=XDG_CONFIG_HOME=/home/%i/.config\n\n# run copyparty\nExecStart=/usr/bin/python3 /usr/bin/copyparty\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "contrib/systemd/index.md",
    "content": "this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured\n\nplease edit `/etc/copyparty/copyparty.conf` (if running as a system service)\nor `$HOME/.config/copyparty/copyparty.conf` if running as a user service\n\na basic configuration example is available at https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf\na configuration example that explains most flags is available at https://github.com/9001/copyparty/blob/hovudstraum/docs/chungus.conf\n\nthe full list of configuration options can be seen at https://ocv.me/copyparty/helptext.html \nor by running `copyparty --help`\n"
  },
  {
    "path": "contrib/systemd/prisonparty.service",
    "content": "# this will start `/usr/local/bin/copyparty-sfx.py`\n# in a chroot, preventing accidental access elsewhere,\n# and share '/mnt' with anonymous read+write\n#\n# installation:\n#   1) put copyparty-sfx.py and prisonparty.sh in /usr/local/bin\n#   2) cp -pv prisonparty.service /etc/systemd/system && systemctl enable --now prisonparty\n#\n# expose additional filesystem locations to copyparty\n#   by listing them between the last `cpp` and `--`\n#\n# `cpp cpp` = user/group to run copyparty as; can be IDs (1000 1000)\n#\n# you may want to:\n#   change '/mnt::rw' to another location or permission-set\n#    (remember to change the '/mnt' chroot arg too)\n#\n# unless you add -q to disable logging, you may want to remove the\n#   following line to allow buffering (slightly better performance):\n#   Environment=PYTHONUNBUFFERED=x\n\n[Unit]\nDescription=copyparty file server\n\n[Service]\nSyslogIdentifier=prisonparty\nEnvironment=PYTHONUNBUFFERED=x\nWorkingDirectory=/var/lib/copyparty-jail\nExecReload=/bin/kill -s USR1 $MAINPID\n\n# stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running\nExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo \"x /tmp/pe-copyparty*\" > /run/tmpfiles.d/copyparty.conf'\n\n# run copyparty\nExecStart=/bin/bash /usr/local/bin/prisonparty.sh /var/lib/copyparty-jail cpp cpp \\\n  /mnt \\\n  -- \\\n  /usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "contrib/systemd/prisonparty@.service",
    "content": "# this will start `/usr/bin/copyparty`\n# in a chroot, preventing accidental access elsewhere,\n# and read copyparty config from `/etc/copyparty/copyparty.conf`\n#\n# expose additional filesystem locations to copyparty\n#   by listing them between the last `%i` and `--`\n#\n# `%i %i` = user/group to run copyparty as; can be IDs (1000 1000)\n#   the %i refers to whatever you put after the prisonparty@\n#   so with prisonparty@foo.service, %i == foo\n#\n# unless you add -q to disable logging, you may want to remove the\n#   following line to allow buffering (slightly better performance):\n#   Environment=PYTHONUNBUFFERED=x\n\n[Unit]\nDescription=copyparty file server\n\n[Service]\nType=notify\nSyslogIdentifier=prisonparty\nWorkingDirectory=/var/lib/copyparty-jail\nEnvironment=PYTHONUNBUFFERED=x\nEnvironment=PRTY_CONFIG=/etc/copyparty/copyparty.conf\nExecReload=/bin/kill -s USR1 $MAINPID\n\n# user to run as + where the TLS certificate is (if any)\nUser=%i\nEnvironment=XDG_CONFIG_HOME=/home/%i/.config\n\n# run copyparty\nExecStart=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail %i %i \\\n  /etc/copyparty \\\n  -- \\\n  /usr/bin/python3 /usr/bin/copyparty\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "contrib/themes/bsod.css",
    "content": "/* copy bsod.* into a folder named \".themes\" in your webroot and then\n     --themes=10 --theme=9 --css-browser=/.themes/bsod.css\n*/\n\nhtml.ey {\n    --w2: #3d7bbc;\n    --w3: #5fcbec;\n\n    --fg: #fff;\n    --fg-max: #fff;\n    --fg-weak: var(--w3);\n\n    --bg: #2067b2;\n    --bg-d3: var(--bg);\n    --bg-d2: var(--w2);\n    --bg-d1: var(--fg-weak);\n    --bg-u2: var(--bg);\n    --bg-u3: var(--bg);\n    --bg-u5: var(--w2);\n\n    --tab-alt: var(--fg-weak);\n\t--row-alt: var(--w2);\n\n    --scroll: var(--w3);\n\n    --a: #fff;\n    --a-b: #fff;\n    --a-hil: #fff;\n    --a-h-bg: var(--fg-weak);\n    --a-dark: var(--a);\n    --a-gray: var(--fg-weak);\n\n\t--btn-fg: var(--a);\n\t--btn-bg: var(--w2);\n    --btn-h-fg: var(--w2);\n\t--btn-1-fg: var(--bg);\n\t--btn-1-bg: var(--a);\n    --txt-sh: a;\n    --txt-bg: var(--w2);\n\n\t--u2-b1-bg: var(--w2);\n\t--u2-b2-bg: var(--w2);\n    --u2-txt-bg: var(--w2);\n\t--u2-tab-bg: a;\n\t--u2-tab-1-bg: var(--w2);\n\n    --sort-1: var(--a);\n    --sort-1: var(--fg-weak);\n\n    --tree-bg: var(--bg);\n\n\t--g-b1: a;\n\t--g-b2: a;\n    --g-f-bg: var(--w2);\n\n    --f-sh1: 0.1;\n    --f-sh2: 0.02;\n    --f-sh3: 0.1;\n    --f-h-b1: a;\n\n    --srv-1: var(--a);\n    --srv-3: var(--a);\n\n    --mp-sh: a;\n}\n\nhtml.ey {\n    background: url('bsod.png') top 5em right 4.5em no-repeat fixed var(--bg);\n}\nhtml.ey body#b {\n    background: var(--bg);  /*sandbox*/\n}\nhtml.ey #ops {\n    margin: 1.7em 1.5em 0 1.5em;\n\tborder-radius: .3em;\n\tborder-width: 1px 0;\n}\nhtml.ey #ops a {\n    text-shadow: 1px 1px 0 rgba(0,0,0,0.5);\n}\nhtml.ey .opbox {\n\tmargin: 1.5em 0 0 0;\n}\nhtml.ey #tree {\n    box-shadow: none;\n}\nhtml.ey #tt {\n    border-color: var(--w2);\n    background: var(--w2);\n}\nhtml.ey .mdo a {\n    background: none;\n    text-decoration: underline;\n}\nhtml.ey .mdo pre,\nhtml.ey .mdo code {\n    color: #fff;\n    background: var(--w2);\n    border: none;\n}\nhtml.ey .mdo h1,\nhtml.ey .mdo h2 {\n    background: none;\n    border-color: var(--w2);\n}\nhtml.ey .mdo ul ul,\nhtml.ey .mdo ul ol,\nhtml.ey .mdo ol ul,\nhtml.ey .mdo ol ol {\n\tborder-color: var(--w2);\n}\nhtml.ey .mdo p>em,\nhtml.ey .mdo li>em,\nhtml.ey .mdo td>em {\n\tcolor: #fd0;\n}"
  },
  {
    "path": "contrib/traefik/copyparty.yaml",
    "content": "# ./traefik --configFile=copyparty.yaml\n\nentryPoints:\n  web:\n    address: :8080\n    transport:\n      # don't disconnect during big uploads\n      respondingTimeouts:\n        readTimeout: \"0s\"\nlog:\n  level: DEBUG\nproviders:\n  file:\n    # WARNING: must be same filename as current file\n    filename: \"copyparty.yaml\"\nhttp:\n  services:\n    service-cpp:\n      loadBalancer:\n        servers:\n          - url: \"http://127.0.0.1:3923/\"\n  routers:\n    my-router:\n      rule: \"PathPrefix(`/`)\"\n      service: service-cpp\n"
  },
  {
    "path": "contrib/webdav-cfg.bat",
    "content": "@echo off\nrem removes the 47.6 MiB filesize limit when downloading from webdav\nrem + optionally allows/enables password-auth over plaintext http\nrem + optionally helps disable wpad, removing the 10sec latency\n\nnet session >nul 2>&1\nif %errorlevel% neq 0 (\n    echo sorry, you must run this as administrator\n    pause\n    exit /b\n)\n\nreg add HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\services\\WebClient\\Parameters /v FileSizeLimitInBytes /t REG_DWORD /d 0xffffffff /f\nreg add HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\WebClient\\Parameters /v FsCtlRequestTimeoutInSec /t REG_DWORD /d 0xffffffff /f\n\necho(\necho OK;\necho allow webdav basic-auth over plaintext http?\necho Y: login works, but the password will be visible in wireshark etc\necho N: login will NOT work unless you use https and valid certificates\nchoice\nif %errorlevel% equ 1 (\n    reg add HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\services\\WebClient\\Parameters /v BasicAuthLevel /t REG_DWORD /d 0x2 /f\n    rem default is 1 (require tls)\n)\n\necho(\necho OK;\necho do you want to disable wpad?\necho can give a HUGE speed boost depending on network settings\nchoice\nif %errorlevel% equ 1 (\n    echo(\n    echo i'm about to open the [Connections] tab in [Internet Properties] for you;\n    echo please click [LAN settings] and disable [Automatically detect settings]\n    echo(\n    pause\n    control inetcpl.cpl,,4\n)\n\nnet stop webclient\nnet start webclient\necho(\necho OK; all done\npause\n"
  },
  {
    "path": "contrib/windows/copyparty-ctmp.bat",
    "content": "rem run copyparty.exe on machines with busted environment variables\ncmd /v /c \"set TMP=\\tmp && copyparty.exe\"\n"
  },
  {
    "path": "contrib/zfs-tune.py",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport sqlite3\nimport sys\nimport traceback\n\n\n\"\"\"\nwhen the up2k-database is stored on a zfs volume, this may give\nslightly higher performance (actual gains not measured yet)\n\nNOTE: must be applied in combination with the related advice in the openzfs documentation;\nhttps://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Workload%20Tuning.html#database-workloads\nand see specifically the SQLite subsection\n\nit is assumed that all databases are stored in a single location,\nfor example with `--hist /var/store/hists`\n\nthree alternatives for running this script:\n\n1. copy it into /var/store/hists and run \"python3 zfs-tune.py s\"\n    (s = modify all databases below folder containing script)\n\n2. cd into /var/store/hists and run \"python3 ~/zfs-tune.py w\"\n    (w = modify all databases below current working directory)\n\n3. python3 ~/zfs-tune.py /var/store/hists\n\nif you use docker, run copyparty with `--hist /cfg/hists`, copy this script into /cfg, and run this:\npodman run --rm -it --entrypoint /usr/bin/python3 ghcr.io/9001/copyparty-ac /cfg/zfs-tune.py s\n\n\"\"\"\n\n\nPAGESIZE = 65536\n\n\n# borrowed from copyparty; short efficient stacktrace for errors\ndef min_ex(max_lines: int = 8, reverse: bool = False) -> str:\n    et, ev, tb = sys.exc_info()\n    stb = traceback.extract_tb(tb) if tb else traceback.extract_stack()[:-1]\n    fmt = \"%s:%d <%s>: %s\"\n    ex = [fmt % (fp.split(os.sep)[-1], ln, fun, txt) for fp, ln, fun, txt in stb]\n    if et or ev or tb:\n        ex.append(\"[%s] %s\" % (et.__name__ if et else \"(anonymous)\", ev))\n    return \"\\n\".join(ex[-max_lines:][:: -1 if reverse else 1])\n\n\ndef set_pagesize(db_path):\n    try:\n        # check current page_size\n        with sqlite3.connect(db_path) as db:\n            v = db.execute(\"pragma page_size\").fetchone()[0]\n            if v == PAGESIZE:\n                print(\" `-- OK\")\n                return\n\n        # https://www.sqlite.org/pragma.html#pragma_page_size\n        #  `- disable wal; set pagesize; vacuum\n        #      (copyparty will reenable wal if necessary)\n\n        with sqlite3.connect(db_path) as db:\n            db.execute(\"pragma journal_mode=delete\")\n            db.commit()\n\n        with sqlite3.connect(db_path) as db:\n            db.execute(f\"pragma page_size = {PAGESIZE}\")\n            db.execute(\"vacuum\")\n\n        print(\" `-- new pagesize OK\")\n\n    except Exception:\n        err = min_ex().replace(\"\\n\", \"\\n -- \")\n        print(f\"FAILED: {db_path}\\n -- {err}\")\n\n\ndef main():\n    top = os.path.dirname(os.path.abspath(__file__))\n    cwd = os.path.abspath(os.getcwd())\n    try:\n        x = sys.argv[1]\n    except:\n        print(f\"\"\"\nthis script takes one mandatory argument:\nspecify 's' to start recursing from folder containing this script file ({top})\nspecify 'w' to start recursing from the current working directory ({cwd})\nspecify a path to start recursing from there\n\"\"\")\n        sys.exit(1)\n\n    if x.lower() == \"w\":\n        top = cwd\n    elif x.lower() != \"s\":\n        top = x\n\n    for dirpath, dirs, files in os.walk(top):\n        for fname in files:\n            if not fname.endswith(\".db\"):\n                continue\n            db_path = os.path.join(dirpath, fname)\n            print(db_path)\n            set_pagesize(db_path)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "copyparty/__init__.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport os\nimport platform\nimport sys\nimport time\n\n# fmt: off\n_: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 :-)  ************************************************************************************************************************************************\n# fmt: on\n\ntry:\n    from typing import TYPE_CHECKING\nexcept:\n    TYPE_CHECKING = False\n\nif True:\n    from typing import Any, Callable, Optional\n\nPY2 = sys.version_info < (3,)\nPY36 = sys.version_info > (3, 6)\nif not PY2:\n    unicode: Callable[[Any], str] = str\nelse:\n    sys.dont_write_bytecode = True\n    unicode = unicode  # type: ignore\n\nWINDOWS: Any = (\n    [int(x) for x in platform.version().split(\".\")]\n    if platform.system() == \"Windows\"\n    else False\n)\n\nVT100 = \"--ansi\" in sys.argv or (\n    os.environ.get(\"NO_COLOR\", \"\").lower() in (\"\", \"0\", \"false\")\n    and sys.stdout.isatty()\n    and \"--no-ansi\" not in sys.argv\n    and (not WINDOWS or WINDOWS >= [10, 0, 14393])\n)\n# introduced in anniversary update\n\nANYWIN = WINDOWS or sys.platform in [\"msys\", \"cygwin\"]\n\nMACOS = platform.system() == \"Darwin\"\n\nGRAAL = platform.python_implementation() == \"GraalVM\"\n\nEXE = bool(getattr(sys, \"frozen\", False))\n\ntry:\n    CORES = len(os.sched_getaffinity(0))\nexcept:\n    CORES = (os.cpu_count() if hasattr(os, \"cpu_count\") else 0) or 2\n\n# all embedded resources to be retrievable over http\nzs = \"\"\"\nweb/a/partyfuse.py\nweb/a/u2c.py\nweb/a/webdav-cfg.txt\nweb/baguettebox.js\nweb/browser.css\nweb/browser.html\nweb/browser.js\nweb/browser2.html\nweb/cf.html\nweb/copyparty.gif\nweb/deps/busy.mp3\nweb/deps/easymde.css\nweb/deps/easymde.js\nweb/deps/marked.js\nweb/deps/fuse.py\nweb/deps/mini-fa.css\nweb/deps/mini-fa.woff\nweb/deps/prism.css\nweb/deps/prism.js\nweb/deps/prismd.css\nweb/deps/scp.woff2\nweb/deps/sha512.ac.js\nweb/deps/sha512.hw.js\nweb/idp.html\nweb/iiam.gif\nweb/md.css\nweb/md.html\nweb/md.js\nweb/md2.css\nweb/md2.js\nweb/mde.css\nweb/mde.html\nweb/mde.js\nweb/msg.html\nweb/opds.xml\nweb/rups.css\nweb/rups.html\nweb/rups.js\nweb/shares.css\nweb/shares.html\nweb/shares.js\nweb/splash.css\nweb/splash.html\nweb/splash.js\nweb/svcs.html\nweb/svcs.js\nweb/tl/chi.js\nweb/tl/cze.js\nweb/tl/deu.js\nweb/tl/epo.js\nweb/tl/fin.js\nweb/tl/fra.js\nweb/tl/grc.js\nweb/tl/hun.js\nweb/tl/ita.js\nweb/tl/jpn.js\nweb/tl/kor.js\nweb/tl/nld.js\nweb/tl/nno.js\nweb/tl/nor.js\nweb/tl/pol.js\nweb/tl/por.js\nweb/tl/rus.js\nweb/tl/spa.js\nweb/tl/swe.js\nweb/tl/tur.js\nweb/tl/ukr.js\nweb/tl/vie.js\nweb/ui.css\nweb/up2k.js\nweb/util.js\nweb/w.hash.js\n\"\"\"\nRES = set(zs.strip().split(\"\\n\"))\nRESM = {\n    \"web/a/partyfuse.txt\": \"web/a/partyfuse.py\",\n    \"web/a/u2c.txt\": \"web/a/u2c.py\",\n    \"web/a/webdav-cfg.bat\": \"web/a/webdav-cfg.txt\",\n}\n\n\nclass EnvParams(object):\n    def __init__(self) -> None:\n        self.t0 = time.time()\n        self.mod = \"\"\n        self.mod_ = \"\"\n        self.cfg = \"\"\n        self.scfg = True\n\n\nE = EnvParams()\n"
  },
  {
    "path": "copyparty/__main__.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\n\"\"\"copyparty: http file sharing hub (py2/py3)\"\"\"\n__author__ = \"ed <copyparty@ocv.me>\"\n__copyright__ = 2019\n__license__ = \"MIT\"\n__url__ = \"https://github.com/9001/copyparty/\"\n\nimport argparse\nimport base64\nimport locale\nimport os\nimport re\nimport select\nimport socket\nimport sys\nimport threading\nimport time\nimport traceback\nimport uuid\n\nfrom .__init__ import (\n    ANYWIN,\n    CORES,\n    EXE,\n    MACOS,\n    PY2,\n    PY36,\n    VT100,\n    WINDOWS,\n    E,\n    EnvParams,\n    unicode,\n)\nfrom .__version__ import CODENAME, S_BUILD_DT, S_VERSION\nfrom .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt\nfrom .bos import bos\nfrom .cfg import flagcats, onedash\nfrom .svchub import SvcHub\nfrom .util import (\n    APPLESAN_TXT,\n    BAD_BOTS,\n    DEF_EXP,\n    DEF_MTE,\n    DEF_MTH,\n    HAVE_IPV6,\n    IMPLICATIONS,\n    JINJA_VER,\n    MIKO_VER,\n    MIMES,\n    PARTFTPY_VER,\n    PY_DESC,\n    PYFTPD_VER,\n    RAM_AVAIL,\n    RAM_TOTAL,\n    RE_ANSI,\n    SQLITE_VER,\n    UNPLICATIONS,\n    URL_BUG,\n    URL_PRJ,\n    Daemon,\n    align_tab,\n    b64enc,\n    ctypes,\n    dedent,\n    has_resource,\n    load_resource,\n    min_ex,\n    pybin,\n    read_utf8,\n    termsize,\n    wk32,\n    wrap,\n)\n\nif True:  # pylint: disable=using-constant-test\n    from collections.abc import Callable\n    from types import FrameType\n\n    from typing import Any, Optional\n\nif PY2:\n    range = xrange  # type: ignore\n\ntry:\n    if os.environ.get(\"PRTY_NO_TLS\"):\n        raise Exception()\n\n    HAVE_SSL = True\n    import ssl\nexcept:\n    HAVE_SSL = False\n\nu = unicode\nprinted: list[str] = []\nzsid = uuid.uuid4().urn[4:]\n\nCFG_DEF = [os.environ.get(\"PRTY_CONFIG\", \"\")]\nif not CFG_DEF[0]:\n    CFG_DEF.pop()\n\n\nclass RiceFormatter(argparse.HelpFormatter):\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        if PY2:\n            kwargs[\"width\"] = termsize()[0]\n\n        super(RiceFormatter, self).__init__(*args, **kwargs)\n\n    def _get_help_string(self, action: argparse.Action) -> str:\n        \"\"\"\n        same as ArgumentDefaultsHelpFormatter(HelpFormatter)\n        except the help += [...] line now has colors\n        \"\"\"\n        fmt = \"\\033[36m (default: \\033[35m%(default)s\\033[36m)\\033[0m\"\n        if not VT100:\n            fmt = \" (default: %(default)s)\"\n\n        ret = unicode(action.help)\n        if \"%(default)\" not in ret:\n            if action.default is not argparse.SUPPRESS:\n                defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE]\n                if action.option_strings or action.nargs in defaulting_nargs:\n                    ret += fmt\n\n        if not VT100:\n            ret = re.sub(\"\\033\\\\[[0-9;]+m\", \"\", ret)\n\n        return ret  # type: ignore\n\n    def _fill_text(self, text: str, width: int, indent: str) -> str:\n        \"\"\"same as RawDescriptionHelpFormatter(HelpFormatter)\"\"\"\n        return \"\".join(indent + line + \"\\n\" for line in text.splitlines())\n\n    def __add_whitespace(self, idx: int, iWSpace: int, text: str) -> str:\n        return (\" \" * iWSpace) + text if idx else text\n\n    def _split_lines(self, text: str, width: int) -> list[str]:\n        # https://stackoverflow.com/a/35925919\n        textRows = text.splitlines()\n        ptn = re.compile(r\"\\s*[0-9\\-]{0,}\\.?\\s*\")\n        for idx, line in enumerate(textRows):\n            search = ptn.search(line)\n            if not line.strip():\n                textRows[idx] = \" \"\n            elif search:\n                lWSpace = search.end()\n                lines = [\n                    self.__add_whitespace(i, lWSpace, x)\n                    for i, x in enumerate(wrap(line, width, width - 1))\n                ]\n                textRows[idx] = lines  # type: ignore\n\n        return [item for sublist in textRows for item in sublist]\n\n\nclass Dodge11874(RiceFormatter):\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        kwargs[\"width\"] = 9003\n        super(Dodge11874, self).__init__(*args, **kwargs)\n\n\nclass BasicDodge11874(\n    argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter\n):\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        kwargs[\"width\"] = 9003\n        super(BasicDodge11874, self).__init__(*args, **kwargs)\n\n\ndef lprint(*a: Any, **ka: Any) -> None:\n    eol = ka.pop(\"end\", \"\\n\")\n    txt: str = \" \".join(unicode(x) for x in a) + eol\n    printed.append(txt)\n    if not VT100:\n        txt = RE_ANSI.sub(\"\", txt)\n\n    print(txt, end=\"\", **ka)\n\n\ndef warn(msg: str) -> None:\n    lprint(\"\\033[1mwarning:\\033[0;33m {}\\033[0m\\n\".format(msg))\n\n\ndef init_E(EE: EnvParams) -> None:\n    # some cpython alternatives (such as pyoxidizer) can\n    # __init__ several times, so do expensive stuff here\n\n    E = EE  # pylint: disable=redefined-outer-name\n\n    def get_unixdir() -> tuple[str, bool]:\n        paths: list[tuple[Callable[..., Any], str]] = [\n            (os.environ.get, \"XDG_CONFIG_HOME\"),\n            (os.path.expanduser, \"~/.config\"),\n            (os.environ.get, \"TMPDIR\"),\n            (os.environ.get, \"TEMP\"),\n            (os.environ.get, \"TMP\"),\n            (unicode, \"/tmp\"),\n        ]\n        errs = []\n        for npath, (pf, pa) in enumerate(paths):\n            priv = npath < 2  # private/trusted location\n            ram = npath > 1  # \"nonvolatile\"; not semantically same as `not priv`\n            p = \"\"\n            try:\n                p = pf(pa)\n                if not p or p.startswith(\"~\"):\n                    continue\n\n                p = os.path.normpath(p)\n                mkdir = not os.path.isdir(p)\n                if mkdir:\n                    os.mkdir(p, 0o700)\n\n                p = os.path.join(p, \"copyparty\")\n                if not priv and os.path.isdir(p):\n                    uid = os.geteuid()\n                    if os.stat(p).st_uid != uid:\n                        p += \".%s\" % (uid,)\n                        if os.path.isdir(p) and os.stat(p).st_uid != uid:\n                            raise Exception(\"filesystem has broken unix permissions\")\n                try:\n                    os.listdir(p)\n                except:\n                    os.mkdir(p, 0o700)\n\n                if ram:\n                    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/\"\n                    errs.append(t % (pa, p))\n                elif mkdir:\n                    t = \"Using %s/copyparty [%s] for config%s (Warning: %s did not exist and was created just now)\"\n                    errs.append(t % (pa, p, \" instead\" if npath else \"\", pa))\n                elif errs:\n                    errs.append(\"Using %s/copyparty [%s] instead\" % (pa, p))\n\n                if errs:\n                    warn(\". \".join(errs))\n\n                return p, priv\n            except Exception as ex:\n                if p:\n                    t = \"Unable to store config in %s [%s] due to %r\"\n                    errs.append(t % (pa, p, ex))\n\n        t = \"could not find a writable path for runtime state:\\n> %s\"\n        raise Exception(t % (\"\\n> \".join(errs)))\n\n    E.mod = os.path.dirname(os.path.realpath(__file__))\n    if E.mod.endswith(\"__init__\"):\n        E.mod = os.path.dirname(E.mod)\n    E.mod_ = os.path.join(E.mod, \"\")\n\n    try:\n        p = os.environ.get(\"XDG_CONFIG_HOME\")\n        if not p:\n            raise Exception()\n        if p.startswith(\"~\"):\n            p = os.path.expanduser(p)\n        p = os.path.abspath(os.path.realpath(p))\n        p = os.path.join(p, \"copyparty\")\n        if not os.path.isdir(p):\n            os.mkdir(p, 0o700)\n        os.listdir(p)\n    except:\n        p = \"\"\n\n    if p:\n        E.cfg = p\n    elif sys.platform == \"win32\":\n        bdir = os.environ.get(\"APPDATA\") or os.environ.get(\"TEMP\") or \".\"\n        E.cfg = os.path.normpath(bdir + \"/copyparty\")\n    elif sys.platform == \"darwin\":\n        E.cfg = os.path.expanduser(\"~/Library/Preferences/copyparty\")\n    else:\n        E.cfg, E.scfg = get_unixdir()\n\n    E.cfg = E.cfg.replace(\"\\\\\", \"/\")\n    try:\n        bos.makedirs(E.cfg, bos.MKD_700)\n    except:\n        if not os.path.isdir(E.cfg):\n            raise\n\n\ndef get_srvname(verbose) -> str:\n    try:\n        ret: str = unicode(socket.gethostname()).split(\".\")[0]\n    except:\n        ret = \"\"\n\n    if ret not in [\"\", \"localhost\"]:\n        return ret\n\n    fp = os.path.join(E.cfg, \"name.txt\")\n    if verbose:\n        lprint(\"using hostname from {}\\n\".format(fp))\n    try:\n        return read_utf8(None, fp, True).strip()\n    except:\n        ret = \"\"\n        namelen = 5\n        while len(ret) < namelen:\n            ret += base64.b32encode(os.urandom(4))[:7].decode(\"utf-8\").lower()\n            ret = re.sub(\"[234567=]\", \"\", ret)[:namelen]\n        with open(fp, \"wb\") as f:\n            f.write(ret.encode(\"utf-8\") + b\"\\n\")\n        return ret\n\n\ndef get_salt(name: str, nbytes: int) -> str:\n    fp = os.path.join(E.cfg, \"%s-salt.txt\" % (name,))\n    try:\n        return read_utf8(None, fp, True).strip()\n    except:\n        ret = b64enc(os.urandom(nbytes))\n        with open(fp, \"wb\") as f:\n            f.write(ret + b\"\\n\")\n        return ret.decode(\"utf-8\")\n\n\ndef ensure_locale() -> None:\n    if ANYWIN and PY2:\n        return  # maybe XP, so busted 65001\n\n    safe = \"en_US.UTF-8\"\n    for x in [\n        safe,\n        \"English_United States.UTF8\",\n        \"English_United States.1252\",\n    ]:\n        try:\n            locale.setlocale(locale.LC_ALL, x)\n            if x != safe:\n                lprint(\"Locale: {}\\n\".format(x))\n            return\n        except:\n            continue\n\n    t = \"setlocale {} failed,\\n  sorting and dates might get funky\\n\"\n    warn(t.format(safe))\n\n\ndef ensure_webdeps() -> None:\n    if has_resource(E, \"web/deps/mini-fa.woff\"):\n        return\n\n    t = \"\"\"could not find webdeps;\n  if you are running the sfx, or exe, or pypi package, or docker image,\n  then this is a bug! Please let me know so I can fix it, thanks :-)\n  %s\n\n  however, if you are a dev, or running copyparty from source, and you want\n  full client functionality, you will need to build or obtain the webdeps:\n  %s/blob/hovudstraum/docs/devnotes.md#building\n    \"\"\"\n    warn(t % (URL_BUG, URL_PRJ))\n\n\ndef configure_ssl_ver(al: argparse.Namespace) -> None:\n    def terse_sslver(txt: str) -> str:\n        txt = txt.lower()\n        for c in [\"_\", \"v\", \".\"]:\n            txt = txt.replace(c, \"\")\n\n        return txt.replace(\"tls10\", \"tls1\")\n\n    # oh man i love openssl\n    # check this out\n    # hold my beer\n    assert ssl  # type: ignore  # !rm\n    ptn = re.compile(r\"^OP_NO_(TLS|SSL)v\")\n    sslver = terse_sslver(al.ssl_ver).split(\",\")\n    flags = [k for k in ssl.__dict__ if ptn.match(k)]\n    # SSLv2 SSLv3 TLSv1 TLSv1_1 TLSv1_2 TLSv1_3\n    if \"help\" in sslver:\n        avail1 = [terse_sslver(x[6:]) for x in flags]\n        avail = \" \".join(sorted(avail1) + [\"all\"])\n        lprint(\"\\navailable ssl/tls versions:\\n  \" + avail)\n        sys.exit(0)\n\n    al.ssl_flags_en = 0\n    al.ssl_flags_de = 0\n    for flag in sorted(flags):\n        ver = terse_sslver(flag[6:])\n        num = getattr(ssl, flag)\n        if ver in sslver:\n            al.ssl_flags_en |= num\n        else:\n            al.ssl_flags_de |= num\n\n    if sslver == [\"all\"]:\n        x = al.ssl_flags_en\n        al.ssl_flags_en = al.ssl_flags_de\n        al.ssl_flags_de = x\n\n    for k in [\"ssl_flags_en\", \"ssl_flags_de\"]:\n        num = getattr(al, k)\n        lprint(\"{0}: {1:8x} ({1})\".format(k, num))\n\n    # think i need that beer now\n\n\ndef configure_ssl_ciphers(al: argparse.Namespace) -> None:\n    assert ssl  # type: ignore  # !rm\n    ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)\n    if al.ssl_ver:\n        ctx.options &= ~al.ssl_flags_en\n        ctx.options |= al.ssl_flags_de\n\n    is_help = al.ciphers == \"help\"\n\n    if al.ciphers and not is_help:\n        try:\n            ctx.set_ciphers(al.ciphers)\n        except:\n            lprint(\"\\n\\033[1;31mfailed to set ciphers\\033[0m\\n\")\n\n    if not hasattr(ctx, \"get_ciphers\"):\n        lprint(\"cannot read cipher list: openssl or python too old\")\n    else:\n        ciphers = [x[\"description\"] for x in ctx.get_ciphers()]\n        lprint(\"\\n  \".join([\"\\nenabled ciphers:\"] + align_tab(ciphers) + [\"\"]))\n\n    if is_help:\n        sys.exit(0)\n\n\ndef args_from_cfg(cfg_path: str) -> list[str]:\n    lines: list[str] = []\n    expand_config_file(None, lines, cfg_path, \"\")\n    lines = upgrade_cfg_fmt(None, argparse.Namespace(vc=False), lines, \"\")\n\n    ret: list[str] = []\n    skip = True\n    for ln in lines:\n        sn = ln.split(\"  #\")[0].strip()\n        if sn.startswith(\"[\"):\n            skip = True\n        if sn.startswith(\"[global]\"):\n            skip = False\n            continue\n        if skip or not sn.split(\"#\")[0].strip():\n            continue\n        for k, v in split_cfg_ln(sn).items():\n            k = k.lstrip(\"-\")\n            if not k:\n                continue\n            prefix = \"-\" if k in onedash else \"--\"\n            if v is True:\n                ret.append(prefix + k)\n            else:\n                ret.append(prefix + k + \"=\" + v)\n\n    return ret\n\n\ndef expand_cfg(argv) -> list[str]:\n    if CFG_DEF:\n        supp = args_from_cfg(CFG_DEF[0])\n        argv = argv[:1] + supp + argv[1:]\n\n    n = 0\n    while n < len(argv):\n        v1 = argv[n]\n        v1v = v1[2:].lstrip(\"=\")\n        try:\n            v2 = argv[n + 1]\n        except:\n            v2 = \"\"\n\n        n += 1\n        if v1 == \"-c\" and v2 and os.path.isfile(v2):\n            n += 1\n            argv = argv[:n] + args_from_cfg(v2) + argv[n:]\n        elif v1.startswith(\"-c\") and v1v and os.path.isfile(v1v):\n            argv = argv[:n] + args_from_cfg(v1v) + argv[n:]\n    return argv\n\n\ndef quotecheck(al):\n    for zs1, zco in vars(al).items():\n        zsl = [u(x) for x in zco] if isinstance(zco, list) else [u(zco)]\n        for zs2 in zsl:\n            zs2 = zs2.strip()\n            zs3 = zs2.strip(\"\\\"'\")\n            if zs2 == zs3 or len(zs2) - len(zs3) < 2:\n                continue\n            if al.c:\n                t = \"found the following global-config:   %s: %s\\n values should not be quoted; did you mean:   %s: %s\"\n            else:\n                t = 'found the following config-option:  \"--%s=%s\"\\n values should not be quoted; did you mean:  \"--%s=%s\"'\n            warn(t % (zs1, zs2, zs1, zs3))\n\n\ndef sighandler(sig: Optional[int] = None, frame: Optional[FrameType] = None) -> None:\n    msg = [\"\"] * 5\n    for th in threading.enumerate():\n        stk = sys._current_frames()[th.ident]  # type: ignore\n        msg.append(str(th))\n        msg.extend(traceback.format_stack(stk))\n\n    msg.append(\"\\n\")\n    print(\"\\n\".join(msg))\n\n\ndef disable_quickedit() -> None:\n    if not ctypes:\n        raise Exception(\"no ctypes\")\n\n    import atexit\n    from ctypes import wintypes\n\n    def ecb(ok: bool, fun: Any, args: list[Any]) -> list[Any]:\n        if not ok:\n            err: int = ctypes.get_last_error()  # type: ignore\n            if err:\n                raise ctypes.WinError(err)  # type: ignore\n        return args\n\n    if PY2:\n        wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD)\n\n    k32 = wk32\n    k32.GetStdHandle.errcheck = ecb  # type: ignore\n    k32.GetConsoleMode.errcheck = ecb  # type: ignore\n    k32.SetConsoleMode.errcheck = ecb  # type: ignore\n    k32.GetConsoleMode.argtypes = (wintypes.HANDLE, wintypes.LPDWORD)\n    k32.SetConsoleMode.argtypes = (wintypes.HANDLE, wintypes.DWORD)\n\n    def cmode(out: bool, mode: Optional[int] = None) -> int:\n        h = k32.GetStdHandle(-11 if out else -10)\n        if mode:\n            return k32.SetConsoleMode(h, mode)  # type: ignore\n\n        cmode = wintypes.DWORD()\n        k32.GetConsoleMode(h, ctypes.byref(cmode))\n        return cmode.value\n\n    # disable quickedit\n    mode = orig_in = cmode(False)\n    quickedit = 0x40\n    extended = 0x80\n    mask = quickedit + extended\n    if mode & mask != extended:\n        atexit.register(cmode, False, orig_in)\n        cmode(False, mode & ~mask | extended)\n\n    # enable colors in case the os.system(\"rem\") trick ever stops working\n    if VT100:\n        mode = orig_out = cmode(True)\n        if mode & 4 != 4:\n            atexit.register(cmode, True, orig_out)\n            cmode(True, mode | 4)\n\n\ndef sfx_tpoke(top: str):\n    if os.environ.get(\"PRTY_NO_TPOKE\"):\n        return\n\n    files = [top] + [\n        os.path.join(dp, p) for dp, dd, df in os.walk(top) for p in dd + df\n    ]\n    while True:\n        t = int(time.time())\n        for f in list(files):\n            try:\n                os.utime(f, (t, t))\n            except Exception as ex:\n                lprint(\"<TPOKE> [%s] %r\" % (f, ex))\n                files.remove(f)\n\n        time.sleep(78123)\n\n\ndef showlic() -> None:\n    try:\n        with load_resource(E, \"res/COPYING.txt\") as f:\n            buf = f.read()\n    except:\n        buf = b\"\"\n\n    if buf:\n        print(buf.decode(\"utf-8\", \"replace\"))\n    else:\n        print(\"no relevant license info to display\")\n        return\n\n\ndef get_sects():\n    return [\n        [\n            \"bind\",\n            \"configure listening\",\n            dedent(\n                \"\"\"\n            \\033[33m-i\\033[0m takes a comma-separated list of interfaces to listen on;\n            IP-addresses, unix-sockets, and/or open file descriptors\n\n            the default (\\033[32m-i ::\\033[0m) means all IPv4 and IPv6 addresses\n\n            \\033[32m-i 0.0.0.0\\033[0m    listens on all IPv4 NICs/subnets\n            \\033[32m-i 127.0.0.1\\033[0m  listens on IPv4 localhost only\n            \\033[32m-i 127.1\\033[0m      listens on IPv4 localhost only\n            \\033[32m-i 127.1,192.168.123.1\\033[0m = IPv4 localhost and 192.168.123.1\n\n            \\033[33m-p\\033[0m takes a comma-separated list of tcp ports to listen on;\n            the default is \\033[32m-p 3923\\033[0m but as root you can \\033[32m-p 80,443,3923\\033[0m\n\n            when running behind a reverse-proxy, it's recommended to\n            use unix-sockets for improved performance and security;\n\n            \\033[32m-i unix:770:www:\\033[33m/dev/shm/party.sock\\033[0m listens on\n            \\033[33m/dev/shm/party.sock\\033[0m with permissions \\033[33m0770\\033[0m;\n            only accessible to members of the \\033[33mwww\\033[0m group.\n            This is the best approach. Alternatively,\n\n            \\033[32m-i unix:777:\\033[33m/dev/shm/party.sock\\033[0m sets perms \\033[33m0777\\033[0m so anyone\n            can access it; bad unless it's inside a restricted folder\n\n            \\033[32m-i unix:\\033[33m/dev/shm/party.sock\\033[0m keeps umask-defined permission\n            (usually \\033[33m0600\\033[0m) and the same user/group as copyparty\n\n            \\033[32m-i fd:\\033[33m3\\033[0m uses the socket passed to copyparty on file descriptor 3\n\n            \\033[33m-p\\033[0m (tcp ports) is ignored for unix-sockets and FDs\n            \"\"\"\n            ),\n        ],\n        [\n            \"accounts\",\n            \"accounts and volumes\",\n            dedent(\n                \"\"\"\n            -a takes username:password,\n            -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:...\n                * \"\\033[33mperm\\033[0m\" is \"permissions,username1,username2,...\"\n                * \"\\033[32mvolflag\\033[0m\" is config flags to set on this volume\n\n            --grp takes groupname:username1,username2,...\n            and groupnames can be used instead of usernames in -v\n            by prefixing the groupname with @\n\n            list of permissions:\n              \"r\" (read):   list folder contents, download files\n              \"w\" (write):  upload files; need \"r\" to see the uploads\n              \"m\" (move):   move files and folders; need \"w\" at destination\n              \"d\" (delete): permanently delete files and folders\n              \"g\" (get):    download files, but cannot see folder contents\n              \"G\" (upget):  \"get\", but can see filekeys of their own uploads\n              \"h\" (html):   \"get\", but folders return their index.html\n              \".\" (dots):   user can ask to show dotfiles in listings\n              \"a\" (admin):  can see uploader IPs, config-reload\n              \"A\" (\"all\"):  same as \"rwmda.\" (read/write/move/delete/admin/dotfiles)\n\n            too many volflags to list here, see --help-flags\n\n            example:\\033[35m\n              -a ed:hunter2 -v .::r:rw,ed -v ../inc:dump:w:rw,ed:c,nodupe  \\033[36m\n              mount current directory at \"/\" with\n               * r (read-only) for everyone\n               * rw (read+write) for ed\n              mount ../inc at \"/dump\" with\n               * w (write-only) for everyone\n               * rw (read+write) for ed\n               * reject duplicate files  \\033[0m\n\n            if no accounts or volumes are configured,\n            current folder will be read/write for everyone\n\n            the group \\033[33m@acct\\033[0m will always have every user with an account\n            (the name of that group can be changed with \\033[32m--grp-all\\033[0m)\n\n            to hide a volume from authenticated users, specify \\033[33m*,-@acct\\033[0m\n            to subtract \\033[33m@acct\\033[0m from \\033[33m*\\033[0m (can subtract users from groups too)\n\n            consider the config file for more flexible account/volume management,\n            including dynamic reload at runtime (and being more readable w)\n\n            see \\033[32m--help-auth\\033[0m for ways to provide the password in requests;\n            see \\033[32m--help-idp\\033[0m for replacing it with SSO and auth-middlewares\n            \"\"\"\n            ),\n        ],\n        [\n            \"auth\",\n            \"how to login from a client\",\n            dedent(\n                \"\"\"\n            different ways to provide the password so you become authenticated:\n\n            login with the ui:\n              go to \\033[36mhttp://127.0.0.1:3923/?h\\033[0m and login there\n\n            send the password in the '\\033[36mPW\\033[0m' http-header:\n              \\033[36mPW: \\033[35mhunter2\\033[0m\n            or if you have \\033[33m--usernames\\033[0m enabled,\n              \\033[36mPW: \\033[35med:hunter2\\033[0m\n\n            send the password in the URL itself:\n              \\033[36mhttp://127.0.0.1:3923/\\033[35m?pw=hunter2\\033[0m\n            or if you have \\033[33m--usernames\\033[0m enabled,\n              \\033[36mhttp://127.0.0.1:3923/\\033[35m?pw=ed:hunter2\\033[0m\n\n            use basic-authentication:\n              \\033[36mhttp://\\033[35med:hunter2\\033[36m@127.0.0.1:3923/\\033[0m\n            which should be the same as this header:\n              \\033[36mAuthorization: Basic \\033[35mZWQ6aHVudGVyMg==\\033[0m\n            \"\"\"\n            ),\n        ],\n        [\n            \"auth-ord\",\n            \"authentication precedence\",\n            dedent(\n                \"\"\"\n            \\033[33m--auth-ord\\033[0m is a comma-separated list of auth options\n            (one or more of the [\\033[35moptions\\033[0m] below); first one wins\n\n            [\\033[35mpw\\033[0m] is conventional login, for example the \"\\033[36mPW\\033[0m\" header,\n              or the \\033[36m?pw=\\033[0m[...] URL-suffix, or a valid session cookie\n              (see \\033[33m--help-auth\\033[0m)\n\n            [\\033[35midp\\033[0m] is a username provided in the http-request-header\n              defined by \\033[33m--idp-h-usr\\033[0m and/or \\033[33m--idp-hm-usr\\033[0m, which is\n              provided by an authentication middleware such as\n              authentik, authelia, tailscale, ... (see \\033[33m--help-idp\\033[0m)\n\n            [\\033[35midp-h\\033[0m] is specifically an \\033[33m--idp-h-usr\\033[0m header,\n            [\\033[35midp-hm\\033[0m] is specifically an \\033[33m--idp-hm-usr\\033[0m header;\n            [\\033[35midp\\033[0m] is the same as [\\033[35midp-hm,idp-h\\033[0m]\n\n            [\\033[35mipu\\033[0m] is a mapping from an IP-address to a username,\n              auto-authing that client-IP to that account\n              (see the description of \\033[36m--ipu\\033[0m in \\033[33m--help\\033[0m)\n\n            NOTE: even if an option (\\033[35mpw\\033[0m/\\033[35mipu\\033[0m/...) is not in the list,\n              it may still be enabled and can still take effect if\n              none of the other alternatives identify the user\n\n            NOTE: if [\\033[35mipu\\033[0m] is in the list, it must be FIRST or LAST\n\n            NOTE: if [\\033[35mpw\\033[0m] is not in the list, the logout-button\n              will be hidden when any idp feature is enabled\n            \"\"\"\n            ),\n        ],\n        [\n            \"flags\",\n            \"list of volflags\",\n            dedent(\n                \"\"\"\n            volflags are appended to volume definitions, for example,\n            to create a write-only volume with the \\033[33mnodupe\\033[0m and \\033[32mnosub\\033[0m flags:\n              \\033[35m-v /mnt/inc:/inc:w\\033[33m:c,nodupe\\033[32m:c,nosub\\033[0m\n\n            if global config defines a volflag for all volumes,\n            you can unset it for a specific volume with -flag\n            \"\"\"\n            ).rstrip()\n            + build_flags_desc(),\n        ],\n        [\n            \"handlers\",\n            \"use plugins to handle certain events\",\n            dedent(\n                \"\"\"\n            usually copyparty returns a \\033[33m404\\033[0m if a file does not exist, and\n            \\033[33m403\\033[0m if a user tries to access a file they don't have access to\n\n            you can load a plugin which will be invoked right before this\n            happens, and the plugin can choose to override this behavior\n\n            load the plugin using --args or volflags; for example \\033[36m\n             --on404 ~/partyhandlers/not404.py\n             -v .::r:c,on404=~/partyhandlers/not404.py\n            \\033[0m\n            the file must define the function \\033[35mmain(cli,vn,rem)\\033[0m:\n             \\033[35mcli\\033[0m: the copyparty HttpCli instance\n             \\033[35mvn\\033[0m:  the VFS which overlaps with the requested URL\n             \\033[35mrem\\033[0m: the remainder of the URL below the VFS mountpoint\n\n            `main` must return a string; one of the following:\n\n            > \\033[32m\"true\"\\033[0m: the plugin has responded to the request,\n                and the TCP connection should be kept open\n\n            > \\033[32m\"false\"\\033[0m: the plugin has responded to the request,\n                and the TCP connection should be terminated\n\n            > \\033[32m\"retry\"\\033[0m: the plugin has done something to resolve the 404\n                situation, and copyparty should reattempt reading the file.\n                if it still fails, a regular 404 will be returned\n\n            > \\033[32m\"allow\"\\033[0m: should ignore the insufficient permissions\n                and let the client continue anyways\n\n            > \\033[32m\"\"\\033[0m: the plugin has not handled the request;\n                try the next plugin or return the usual 404 or 403\n\n            \\033[1;35mPS!\\033[0m the folder that contains the python file should ideally\n              not contain many other python files, and especially nothing\n              with filenames that overlap with modules used by copyparty\n            \"\"\"\n            ),\n        ],\n        [\n            \"hooks\",\n            \"execute commands before/after various events\",\n            dedent(\n                \"\"\"\n            execute a command (a program or script) before or after various events;\n             \\033[36mxbu\\033[35m executes CMD before a file upload starts\n             \\033[36mxau\\033[35m executes CMD after  a file upload finishes\n             \\033[36mxiu\\033[35m executes CMD after  all uploads finish and volume is idle\n             \\033[36mxbc\\033[35m executes CMD before a file copy\n             \\033[36mxac\\033[35m executes CMD after  a file copy\n             \\033[36mxbr\\033[35m executes CMD before a file rename/move\n             \\033[36mxar\\033[35m executes CMD after  a file rename/move\n             \\033[36mxbd\\033[35m executes CMD before a file delete\n             \\033[36mxad\\033[35m executes CMD after  a file delete\n             \\033[36mxm\\033[35m executes CMD on message\n             \\033[36mxban\\033[35m executes CMD if someone gets banned\n            \\033[0m\n            can be defined as --args or volflags; for example \\033[36m\n             --xau foo.py\n             -v .::r:c,xau=bar.py\n            \\033[0m\n            hooks specified as commandline --args are appended to volflags;\n            each commandline --arg and volflag can be specified multiple times,\n            each hook will execute in order unless one returns non-zero, or\n            \"100\" which means \"stop daisychaining and return 0 (success/OK)\"\n\n            optionally prefix the command with comma-sep. flags similar to -mtp:\n\n             \\033[36mf\\033[35m forks the process, doesn't wait for completion\n             \\033[36mc\\033[35m checks return code, blocks the action if non-zero\n             \\033[36mj\\033[35m provides json with info as 1st arg instead of filepath\n             \\033[36ms\\033[35m provides input data on stdin (instead of 1st arg)\n             \\033[36mwN\\033[35m waits N sec after command has been started before continuing\n             \\033[36mtN\\033[35m sets an N sec timeout before the command is abandoned\n             \\033[36miN\\033[35m xiu only: volume must be idle for N sec (default = 5)\n             \\033[36mI\\033[35m import and run as module, not as subprocess\n\n             \\033[36mar\\033[35m only run hook if user has read-access\n             \\033[36marw\\033[35m only run hook if user has read-write-access\n             \\033[36marwmd\\033[35m ...and so on... (doesn't work for xiu or xban)\n\n             \\033[36mkt\\033[35m kills the entire process tree on timeout (default),\n             \\033[36mkm\\033[35m kills just the main process\n             \\033[36mkn\\033[35m lets it continue running until copyparty is terminated\n\n             \\033[36mc0\\033[35m show all process output (default)\n             \\033[36mc1\\033[35m show only stderr\n             \\033[36mc2\\033[35m show only stdout\n             \\033[36mc3\\033[35m mute all process output\n            \\033[0m\n            examples:\n\n             \\033[36m--xm some.py\\033[35m runs \\033[33msome.py msgtxt\\033[35m on each 📟 message;\n              \\033[33mmsgtxt\\033[35m is the message that was written into the web-ui\n\n             \\033[36m--xm j,some.py\\033[35m runs \\033[33msome.py jsontext\\033[35m on each 📟 message;\n              \\033[33mjsontext\\033[35m is the message info (ip, user, ..., msg-text)\n\n             \\033[36m--xm aw,j,some.py\\033[35m requires user to have write-access\n\n             \\033[36m--xm aw,,notify-send,hey,--\\033[35m shows an OS alert on linux;\n              the \\033[33m,,\\033[35m stops copyparty from reading the rest as flags and\n              the \\033[33m--\\033[35m stops notify-send from reading the message as args\n              and the alert will be \"hey\" followed by the messagetext\n\n             \\033[36m--xm s,,tee,-a,log.txt\\033[35m appends each msg to log.txt;\n             \\033[36m--xm s,j,,tee,-a,log.txt\\033[35m writes it as json instead\n\n             \\033[36m--xau zmq:pub:tcp://*:5556\\033[35m announces uploads on zeromq;\n             \\033[36m--xau t3,zmq:push:tcp://*:5557\\033[35m also works, and you can\n             \\033[36m--xau t3,j,zmq:req:tcp://localhost:5555\\033[35m too for example\n            \\033[0m\n            each hook is executed once for each event, except for \\033[36mxiu\\033[0m\n            which builds up a backlog of uploads, running the hook just once\n            as soon as the volume has been idle for iN seconds (5 by default)\n\n            \\033[36mxiu\\033[0m is also unique in that it will pass the metadata to the\n            executed program on STDIN instead of as argv arguments (so\n            just like the \\033[36ms\\033[0m option does for the other hook types), and\n            it also includes the wark (file-id/hash) as a json property\n\n            \\033[36mxban\\033[0m can be used to overrule / cancel a user ban event;\n            if the program returns 0 (true/OK) then the ban will NOT happen\n\n            effects can be used to redirect uploads into other\n            locations, and to delete or index other files based\n            on new uploads, but with certain limitations. See\n            bin/hooks/reloc* and docs/devnotes.md#hook-effects\n\n            the \\033[36mI\\033[0m option will override most other options, because\n            it entirely hands over control to the hook, which is\n            then able to tamper with copyparty's internal memory\n            and wreck havoc if it wants to -- but this is worh it\n            because it makes the hook 140x faster\n\n            except for \\033[36mxm\\033[0m, only one hook / one action can run at a time,\n            so it's recommended to use the \\033[36mf\\033[0m flag unless you really need\n            to wait for the hook to finish before continuing (without \\033[36mf\\033[0m\n            the upload speed can easily drop to 10% for small files)\"\"\"\n            ),\n        ],\n        [\n            \"idp\",\n            \"replacing the login system with fancy middleware\",\n            dedent(\n                \"\"\"\n            if you already have a centralized service which handles\n            user-authentication for other services already, you can\n            integrate copyparty with that for automatic login\n\n            if the middleware is providing the username in an http-header\n            named '\\033[35mtheUsername\\033[0m' then do this: \\033[36m--idp-h-usr theUsername\\033[0m\n\n            if the middleware is providing a list of groups in the header\n            named '\\033[35mtheGroups\\033[0m' then do this: \\033[36m--idp-h-grp theGroup\\033[0m\n\n            if the list of groups is separated by '\\033[35m%\\033[0m' then \\033[36m--idp-gsep %\\033[0m\n\n            if the middleware is providing a header named '\\033[35mAccount\\033[0m'\n            and the value is '\\033[35malice@forest.net\\033[0m' but the username is\n            actually '\\033[35mmarisa\\033[0m' then do this for each user:\n            \\033[36m--idp-hm-usr ^Account^alice@forest.net^marisa\\033[0m\n            (the separator '\\033[35m^\\033[0m' can be any character)\n\n            make ABSOLUTELY SURE that the header can only be set by your\n            middleware and not by clients! and, as an extra precaution,\n            send a header named '\\033[36mfinalmasterspark\\033[0m' (a secret keyword)\n            and then \\033[36m--idp-h-key finalmasterspark\\033[0m to require that\n\n            the login/logout links/buttons can be replaced with links\n            going to your IdP's UI; \\033[36m--idp-login /login/?redir={dst}\\033[0m\n            will expand \\033[36m{dst}\\033[0m to the URL of the current page, so\n            the IdP can redirect the user back to where they were\n            \"\"\"\n            ),\n        ],\n        [\n            \"urlform\",\n            \"how to handle url-form POSTs\",\n            dedent(\n                \"\"\"\n            values for --urlform:\n              \\033[36mstash\\033[35m dumps the data to file and returns length + checksum\n              \\033[36msave,get\\033[35m dumps to file and returns the page like a GET\n              \\033[36mprint    \\033[35m prints the data to log and returns an error\n              \\033[36mprint,xm \\033[35m prints the data to log and returns --xm output\n              \\033[36mprint,get\\033[35m prints the data to log and returns GET\\033[0m\n\n            note that the \\033[35m--xm\\033[0m hook will only run if \\033[35m--urlform\\033[0m is\n              either \\033[36mprint\\033[0m or \\033[36mprint,get\\033[0m or the default \\033[36mprint,xm\\033[0m\n\n            if an \\033[35m--xm\\033[0m hook returns text, then\n              the response code will be HTTP 202;\n              http/get responses will be HTTP 200\n\n            if there are multiple \\033[35m--xm\\033[0m hooks defined, then\n              the first hook that produced output is returned\n\n            if there are no \\033[35m--xm\\033[0m hooks defined, then the default\n              \\033[36mprint,xm\\033[0m behaves like \\033[36mprint,get\\033[0m (returning html)\n            \"\"\"\n            ),\n        ],\n        [\n            \"exp\",\n            \"text expansion\",\n            dedent(\n                \"\"\"\n            specify --exp or the \"exp\" volflag to enable placeholder expansions\n            in README.md / PREADME.md / .prologue.html / .epilogue.html\n\n            --exp-md (volflag exp_md) holds the list of placeholders which can be\n            expanded in READMEs, and --exp-lg (volflag exp_lg) likewise for logues;\n            any placeholder not given in those lists will be ignored and shown as-is\n\n            the default list will expand the following placeholders:\n            \\033[36m{{self.ip}}     \\033[35mclient ip\n            \\033[36m{{self.ua}}     \\033[35mclient user-agent\n            \\033[36m{{self.uname}}  \\033[35mclient username\n            \\033[36m{{self.host}}   \\033[35mthe \"Host\" header, or the server's external IP otherwise\n            \\033[36m{{cfg.name}}    \\033[35mthe --name global-config\n            \\033[36m{{cfg.logout}}  \\033[35mthe --logout global-config\n            \\033[36m{{vf.scan}}     \\033[35mthe \"scan\" volflag\n            \\033[36m{{vf.thsize}}   \\033[35mthumbnail size\n            \\033[36m{{srv.itime}}   \\033[35mserver time in seconds\n            \\033[36m{{srv.htime}}   \\033[35mserver time as YY-mm-dd, HH:MM:SS (UTC)\n            \\033[36m{{hdr.cf-ipcountry}} \\033[35mthe \"CF-IPCountry\" client header (probably blank)\n            \\033[0m\n            so the following types of placeholders can be added to the lists:\n            * any client header can be accessed through {{hdr.*}}\n            * any variable in httpcli.py can be accessed through {{self.*}}\n            * any global server setting can be accessed through {{cfg.*}}\n            * any volflag can be accessed through {{vf.*}}\n\n            remove vf.scan from default list using --exp-md /vf.scan\n            add \"accept\" header to def. list using --exp-md +hdr.accept\n\n            for performance reasons, expansion only happens while embedding\n            documents into directory listings, and when accessing a ?doc=...\n            link, but never otherwise, so if you click a -txt- link you'll\n            have to refresh the page to apply expansion\n            \"\"\"\n            ),\n        ],\n        [\n            \"ls\",\n            \"volume inspection\",\n            dedent(\n                \"\"\"\n            \\033[35m--ls USR,VOL,FLAGS\n              \\033[36mUSR\\033[0m is a user to browse as; * is anonymous, ** is all users\n              \\033[36mVOL\\033[0m is a single volume to scan, default is * (all vols)\n              \\033[36mFLAG\\033[0m is flags;\n                \\033[36mv\\033[0m in addition to realpaths, print usernames and vpaths\n                \\033[36mln\\033[0m only prints symlinks leaving the volume mountpoint\n                \\033[36mp\\033[0m exits 1 if any such symlinks are found\n                \\033[36mr\\033[0m resumes startup after the listing\n\n            examples:\n              --ls '**'          # list all files which are possible to read\n              --ls '**,*,ln'     # check for dangerous symlinks\n              --ls '**,*,ln,p,r' # check, then start normally if safe\n            \"\"\"\n            ),\n        ],\n        [\n            \"dbd\",\n            \"database durability profiles\",\n            dedent(\n                \"\"\"\n            mainly affects uploads of many small files on slow HDDs; speeds measured uploading 520 files on a WD20SPZX (SMR 2.5\" 5400rpm 4kb)\n\n            \\033[32macid\\033[0m = extremely safe but slow; the old default. Should never lose any data no matter what\n\n            \\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\n\n            \\033[32mwal\\033[0m = another 21x faster on HDDs yet 90% as safe; same pitfall as \\033[33mswal\\033[0m except more likely\n\n            \\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\n\n            profiles can be set globally (--dbd=yolo), or per-volume with volflags: -v ~/Music:music:r:c,dbd=acid\n            \"\"\"\n            ),\n        ],\n        [\n            \"chmod\",\n            \"file/folder permissions\",\n            dedent(\n                \"\"\"\n            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\n\n            similarly, \\033[33m--chmod-d\\033[0m and \\033[33mchmod_d\\033[0m sets the directory/folder perm\n\n            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.\n\n            if 3 digits: User, Group, Other\n            if 4 digits: Special, User, Group, Other\n\n            \"Special\" digit (sum of the following):\n            \\033[32m1\\033[0m = sticky (files: n/a; dirs: files in directory can be deleted only by their owners)\n            \\033[32m2\\033[0m = setgid (files: run executable as file group; dirs: files inherit group from directory)\n            \\033[32m4\\033[0m = setuid (files: run executable as file owner; dirs: n/a)\n\n            \"User\", \"Group\", \"Other\" digits for files:\n            \\033[32m0\\033[0m = \\033[35m---\\033[0m = no access\n            \\033[32m1\\033[0m = \\033[35m--x\\033[0m = can execute the file as a program\n            \\033[32m2\\033[0m = \\033[35m-w-\\033[0m = can write\n            \\033[32m3\\033[0m = \\033[35m-wx\\033[0m = can write and execute\n            \\033[32m4\\033[0m = \\033[35mr--\\033[0m = can read\n            \\033[32m5\\033[0m = \\033[35mr-x\\033[0m = can read and execute\n            \\033[32m6\\033[0m = \\033[35mrw-\\033[0m = can read and write\n            \\033[32m7\\033[0m = \\033[35mrwx\\033[0m = can read, write, execute\n\n            \"User\", \"Group\", \"Other\" digits for directories/folders:\n            \\033[32m0\\033[0m = \\033[35m---\\033[0m = no access\n            \\033[32m1\\033[0m = \\033[35m--x\\033[0m = can read files in folder but not list contents\n            \\033[32m2\\033[0m = \\033[35m-w-\\033[0m = n/a\n            \\033[32m3\\033[0m = \\033[35m-wx\\033[0m = can create files but not list\n            \\033[32m4\\033[0m = \\033[35mr--\\033[0m = can list, but not read/write\n            \\033[32m5\\033[0m = \\033[35mr-x\\033[0m = can list and read files\n            \\033[32m6\\033[0m = \\033[35mrw-\\033[0m = n/a\n            \\033[32m7\\033[0m = \\033[35mrwx\\033[0m = can read, write, list\n            \"\"\"\n            ),\n        ],\n        [\n            \"pwhash\",\n            \"password hashing\",\n            dedent(\n                \"\"\"\n            when \\033[36m--ah-alg\\033[0m is not the default [\\033[32mnone\\033[0m], all account passwords must be hashed\n\n            passwords can be hashed on the commandline with \\033[36m--ah-gen\\033[0m, but\n            copyparty will also hash and print any passwords that are non-hashed\n            (password which do not start with '+') and then terminate afterwards\n\n            if you have enabled --usernames then the password\n            must be provided as username:password for hashing\n\n            \\033[36m--ah-alg\\033[0m specifies the hashing algorithm and a\n               list of optional comma-separated arguments:\n\n            \\033[36m--ah-alg argon2\\033[0m  # which is the same as:\n            \\033[36m--ah-alg argon2,3,256,4,19\\033[0m\n            use argon2id with timecost 3, 256 MiB, 4 threads, version 19 (0x13/v1.3)\n\n            \\033[36m--ah-alg scrypt\\033[0m  # which is the same as:\n            \\033[36m--ah-alg scrypt,13,2,8,4,32\\033[0m\n            use scrypt with cost 2**13, 2 iterations, blocksize 8, 4 threads,\n              and allow using up to 32 MiB RAM (ram=cost*blksz roughly)\n\n            \\033[36m--ah-alg sha2\\033[0m  # which is the same as:\n            \\033[36m--ah-alg sha2,424242\\033[0m\n            use sha2-512 with 424242 iterations\n\n            recommended: \\033[32m--ah-alg argon2\\033[0m\n              (takes about 0.4 sec and 256M RAM to process a new password)\n\n            argon2 needs python-package argon2-cffi,\n            scrypt needs openssl,\n            sha2 is always available\n            \"\"\"\n            ),\n        ],\n        [\n            \"zm\",\n            \"mDNS debugging\",\n            dedent(\n                \"\"\"\n            the mDNS protocol is multicast-based, which means there are thousands\n            of fun and interesting ways for it to break unexpectedly\n\n            things to check if it does not work at all:\n\n            * is there a firewall blocking port 5353 on either the server or client?\n              (for example, clients may be able to send queries to copyparty,\n               but the replies could get lost)\n\n            * is multicast accidentally disabled on either the server or client?\n              (look for mDNS log messages saying \"new client on [...]\")\n\n            * the router/switch must be multicast and igmp capable\n\n            things to check if it works for a while but then it doesn't:\n\n            * is there a firewall blocking port 5353 on either the server or client?\n              (copyparty may be unable to see the queries from the clients, but the\n               clients may still be able to see the initial unsolicited announce,\n               so it works for about 2 minutes after startup until TTL expires)\n\n            * does the client have multiple IPs on its interface, and some of the\n              IPs are in subnets which the copyparty server is not a member of?\n\n            for both of the above intermittent issues, try --zm-spam 30\n            (not spec-compliant but nothing will mind)\n            \"\"\"\n            ),\n        ],\n    ]\n\n\ndef build_flags_desc():\n    ret = \"\"\n    for grp, flags in flagcats.items():\n        ret += \"\\n\\n\\033[0m\" + grp\n        for k, v in flags.items():\n            v = v.replace(\"\\n\", \"\\n    \")\n            ret += \"\\n  \\033[36m{}\\033[35m {}\".format(k, v)\n\n    return ret\n\n\n# fmt: off\n\n\ndef add_general(ap, nc, srvname):\n    ap2 = ap.add_argument_group(\"general options\")\n    ap2.add_argument(\"-c\", metavar=\"PATH\", type=u, default=CFG_DEF, action=\"append\", help=\"\\033[34mREPEATABLE:\\033[0m add config file\")\n    ap2.add_argument(\"-nc\", metavar=\"NUM\", type=int, default=nc, help=\"max num clients\")\n    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]\")\n    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\")\n    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]\")\n    ap2.add_argument(\"--usernames\", action=\"store_true\", help=\"require username and password for login; default is just password\")\n    ap2.add_argument(\"--chdir\", metavar=\"PATH\", type=u, help=\"change working-directory to \\033[33mPATH\\033[0m before mapping volumes\")\n    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)\")\n    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\")\n    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-]\")\n    ap2.add_argument(\"--name\", metavar=\"TXT\", type=u, default=srvname, help=\"server name (displayed topleft in browser and in mDNS)\")\n    ap2.add_argument(\"--name-url\", metavar=\"TXT\", type=u, help=\"URL for server name hyperlink (displayed topleft in browser)\")\n    ap2.add_argument(\"--name-html\", type=u, help=argparse.SUPPRESS)\n    ap2.add_argument(\"--site\", metavar=\"URL\", type=u, default=\"\", help=\"public URL to assume when creating links; example: [\\033[32mhttps://example.com/\\033[0m]\")\n    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]\")\n    ap2.add_argument(\"--mimes\", action=\"store_true\", help=\"list default mimetype mapping and exit\")\n    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)\")\n    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\")\n    ap2.add_argument(\"--vc-url\", metavar=\"URL\", type=u, default=\"\", help=\"URL to check for vulnerable versions (default-disabled)\")\n    ap2.add_argument(\"--vc-age\", metavar=\"HOURS\", type=int, default=3, help=\"how many hours to wait between vulnerability checks\")\n    ap2.add_argument(\"--vc-exit\", action=\"store_true\", help=\"panic and exit if current version is vulnerable\")\n    ap2.add_argument(\"--license\", action=\"store_true\", help=\"show licenses and exit\")\n    ap2.add_argument(\"--version\", action=\"store_true\", help=\"show versions and exit\")\n    ap2.add_argument(\"--versionb\", action=\"store_true\", help=\"show version and exit\")\n\n\ndef add_qr(ap, tty):\n    ap2 = ap.add_argument_group(\"qr options\")\n    ap2.add_argument(\"--qr\", action=\"store_true\", help=\"show QR-code on startup\")\n    ap2.add_argument(\"--qrs\", action=\"store_true\", help=\"change the QR-code URL to https://\")\n    ap2.add_argument(\"--qrl\", metavar=\"PATH\", type=u, default=\"\", help=\"location to include in the url, for example [\\033[32mpriv/?pw=hunter2\\033[0m]\")\n    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\")\n    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\")\n    ap2.add_argument(\"--qr-bg\", metavar=\"COLOR\", type=int, default=229, help=\"background (white=255)\")\n    ap2.add_argument(\"--qrp\", metavar=\"CELLS\", type=int, default=4, help=\"padding (spec says 4 or more, but 1 is usually fine)\")\n    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)\")\n    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\")\n    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\")\n    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)\")\n    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\")\n    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\")\n    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\")\n    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\")\n\n\ndef add_fs(ap):\n    ap2 = ap.add_argument_group(\"filesystem options\")\n    rm_re_def = \"15/0.1\" if ANYWIN else \"0/0\"\n    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)\")\n    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)\")\n    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)\")\n    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)\")\n    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)\")\n    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\")\n\n\ndef add_share(ap):\n    db_path = os.path.join(E.cfg, \"shares.db\")\n    ap2 = ap.add_argument_group(\"share-url options\")\n    ap2.add_argument(\"--shr\", metavar=\"DIR\", type=u, default=\"\", help=\"toplevel virtual folder for shared files/folders, for example [\\033[32m/share\\033[0m]\")\n    ap2.add_argument(\"--shr-db\", metavar=\"FILE\", type=u, default=db_path, help=\"database to store shares in\")\n    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)\")\n    ap2.add_argument(\"--shr-adm\", metavar=\"U,U\", type=u, default=\"\", help=\"comma-separated list of users allowed to view/delete any share\")\n    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\")\n    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]\")\n    ap2.add_argument(\"--shr-v\", action=\"store_true\", help=\"debug\")\n\n\ndef add_upload(ap):\n    ap2 = ap.add_argument_group(\"upload options\")\n    ap2.add_argument(\"--dotpart\", action=\"store_true\", help=\"dotfile incomplete uploads, hiding them from clients unless \\033[33m-ed\\033[0m\")\n    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\")\n    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]\")\n    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)\")\n    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)\")\n    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)\")\n    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\")\n    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)\")\n    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)\")\n    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)\")\n    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)\")\n    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\")\n    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)\")\n    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, ...)\")\n    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)\")\n    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)\")\n    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)\")\n    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)\")\n    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)\")\n    ap2.add_argument(\"--dedup\", action=\"store_true\", help=\"enable symlink-based upload deduplication (volflag=dedup)\")\n    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)\")\n    ap2.add_argument(\"--hardlink\", action=\"store_true\", help=\"enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)\")\n    ap2.add_argument(\"--hardlink-only\", action=\"store_true\", help=\"do not fallback to symlinks when a hardlink cannot be made (volflag=hardlinkonly)\")\n    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)\")\n    ap2.add_argument(\"--no-dupe\", action=\"store_true\", help=\"reject duplicate files during upload; only matches within the same volume (volflag=nodupe)\")\n    ap2.add_argument(\"--no-dupe-m\", action=\"store_true\", help=\"also reject dupes when moving a file into another volume (volflag=nodupem)\")\n    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)\")\n    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\")\n    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\")\n    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)\")\n    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)\")\n    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)\")\n    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)\")\n    ap2.add_argument(\"--rand\", action=\"store_true\", help=\"force randomized filenames, \\033[33m--nrand\\033[0m chars long (volflag=rand)\")\n    ap2.add_argument(\"--nrand\", metavar=\"NUM\", type=int, default=9, help=\"randomized filenames length (volflag=nrand)\")\n    ap2.add_argument(\"--magic\", action=\"store_true\", help=\"enable filetype detection on nameless uploads (volflag=magic)\")\n    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)\")\n    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\")\n    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\")\n    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)\")\n    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\")\n    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]\")\n    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)\")\n    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\")\n    ap2.add_argument(\"--write-uplog\", action=\"store_true\", help=\"write POST reports to textfiles in working-directory\")\n\n\ndef add_network(ap):\n    ap2 = ap.add_argument_group(\"network options\")\n    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\")\n    ap2.add_argument(\"-p\", metavar=\"PORT\", type=u, default=\"3923\", help=\"ports to listen on (comma/range); ignored for unix-sockets\")\n    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)\")\n    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\")\n    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\")\n    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\")\n    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'\")\n    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'\")\n    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)\")\n    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\")\n    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\")\n    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]\")\n    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)\")\n    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\")\n    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\")\n    if ANYWIN:\n        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\")\n    elif not MACOS:\n        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)\")\n    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\")\n    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\")\n    ap2.add_argument(\"--s-thead\", metavar=\"SEC\", type=int, default=120, help=\"socket timeout (read request header)\")\n    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\")\n    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)\")\n    ap2.add_argument(\"--s-wr-sz\", metavar=\"B\", type=int, default=256*1024, help=\"socket write size in bytes\")\n    ap2.add_argument(\"--s-wr-slp\", metavar=\"SEC\", type=float, default=0.0, help=\"debug: socket write delay in seconds\")\n    ap2.add_argument(\"--rsp-slp\", metavar=\"SEC\", type=float, default=0.0, help=\"debug: response delay in seconds\")\n    ap2.add_argument(\"--rsp-jtr\", metavar=\"SEC\", type=float, default=0.0, help=\"debug: response delay, random duration 0..\\033[33mSEC\\033[0m\")\n\n\ndef add_tls(ap, cert_path):\n    ap2 = ap.add_argument_group(\"SSL/TLS options\")\n    ap2.add_argument(\"--http-only\", action=\"store_true\", help=\"disable ssl/tls -- force plaintext\")\n    ap2.add_argument(\"--https-only\", action=\"store_true\", help=\"disable plaintext -- force tls\")\n    ap2.add_argument(\"--cert\", metavar=\"PATH\", type=u, default=cert_path, help=\"path to file containing a concatenation of TLS key and certificate chain\")\n    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\")\n    ap2.add_argument(\"--ciphers\", metavar=\"LIST\", type=u, default=\"\", help=\"set allowed ssl/tls ciphers; [\\033[32mhelp\\033[0m] shows available ciphers\")\n    ap2.add_argument(\"--ssl-dbg\", action=\"store_true\", help=\"dump some tls info\")\n    ap2.add_argument(\"--ssl-log\", metavar=\"PATH\", type=u, default=\"\", help=\"log master secrets for later decryption in wireshark\")\n\n\ndef add_cert(ap, cert_path):\n    cert_dir = os.path.dirname(cert_path)\n    ap2 = ap.add_argument_group(\"TLS certificate generator options\")\n    ap2.add_argument(\"--no-crt\", action=\"store_true\", help=\"disable automatic certificate creation\")\n    ap2.add_argument(\"--crt-ns\", metavar=\"N,N\", type=u, default=\"\", help=\"comma-separated list of FQDNs (domains) to add into the certificate\")\n    ap2.add_argument(\"--crt-exact\", action=\"store_true\", help=\"do not add wildcard entries for each \\033[33m--crt-ns\\033[0m\")\n    ap2.add_argument(\"--crt-noip\", action=\"store_true\", help=\"do not add autodetected IP addresses into cert\")\n    ap2.add_argument(\"--crt-nolo\", action=\"store_true\", help=\"do not add 127.0.0.1 / localhost into cert\")\n    ap2.add_argument(\"--crt-nohn\", action=\"store_true\", help=\"do not add mDNS names / hostname into cert\")\n    ap2.add_argument(\"--crt-dir\", metavar=\"PATH\", default=cert_dir, help=\"where to save the CA cert\")\n    ap2.add_argument(\"--crt-cdays\", metavar=\"D\", type=float, default=3650.0, help=\"ca-certificate expiration time in days\")\n    ap2.add_argument(\"--crt-sdays\", metavar=\"D\", type=float, default=365.0, help=\"server-cert expiration time in days\")\n    ap2.add_argument(\"--crt-cn\", metavar=\"TXT\", type=u, default=\"partyco\", help=\"CA/server-cert common-name\")\n    ap2.add_argument(\"--crt-cnc\", metavar=\"TXT\", type=u, default=\"--crt-cn\", help=\"override CA name\")\n    ap2.add_argument(\"--crt-cns\", metavar=\"TXT\", type=u, default=\"--crt-cn cpp\", help=\"override server-cert name\")\n    ap2.add_argument(\"--crt-back\", metavar=\"HRS\", type=float, default=72.0, help=\"backdate in hours\")\n    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\")\n\n\ndef add_auth(ap):\n    idp_db = os.path.join(E.cfg, \"idp.db\")\n    ses_db = os.path.join(E.cfg, \"sessions.db\")\n    ap2 = ap.add_argument_group(\"IdP / identity provider / user authentication options\")\n    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\")\n    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\")\n    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\")\n    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\")\n    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\")\n    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 _\")\n    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)\")\n    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\")\n    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)\")\n    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)\")\n    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\")\n    ap2.add_argument(\"--idp-login-t\", metavar=\"T\", type=u, default=\"Login with SSO\", help=\"the label/text for the idp-login button\")\n    ap2.add_argument(\"--idp-logout\", metavar=\"L\", type=u, default=\"\", help=\"replace all logout-buttons with a link to URL \\033[33mL\\033[0m\")\n    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\")\n    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\")\n    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\")\n    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\")\n    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\")\n    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)\")\n    ap2.add_argument(\"--ses-len\", metavar=\"CHARS\", type=int, default=20, help=\"session key length; default is 120 bits ((20//4)*4*6)\")\n    ap2.add_argument(\"--no-ses\", action=\"store_true\", help=\"disable sessions; use plaintext passwords in cookies\")\n    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\")\n    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]\")\n    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]\")\n    ap2.add_argument(\"--have-idp-hdrs\", type=u, default=\"\", help=argparse.SUPPRESS)\n    ap2.add_argument(\"--have-ipu-or-ipr\", type=u, default=\"\", help=argparse.SUPPRESS)\n    ap2.add_argument(\"--ao-idp-before-pw\", type=u, default=\"\", help=argparse.SUPPRESS)\n    ap2.add_argument(\"--ao-h-before-hm\", type=u, default=\"\", help=argparse.SUPPRESS)\n    ap2.add_argument(\"--ao-ipu-wins\", type=u, default=\"\", help=argparse.SUPPRESS)\n    ap2.add_argument(\"--ao-have-pw\", type=u, default=\"\", help=argparse.SUPPRESS)\n\n\ndef add_chpw(ap):\n    db_path = os.path.join(E.cfg, \"chpw.json\")\n    ap2 = ap.add_argument_group(\"user-changeable passwords options\")\n    ap2.add_argument(\"--chpw\", action=\"store_true\", help=\"allow users to change their own passwords\")\n    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\")\n    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)\")\n    ap2.add_argument(\"--chpw-len\", metavar=\"N\", type=int, default=8, help=\"minimum password length\")\n    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\")\n\n\ndef add_zeroconf(ap):\n    ap2 = ap.add_argument_group(\"Zeroconf options\")\n    ap2.add_argument(\"-z\", action=\"store_true\", help=\"enable all zeroconf backends (mdns, ssdp)\")\n    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\")\n    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\")\n    ap2.add_argument(\"--z-chk\", metavar=\"SEC\", type=int, default=10, help=\"check for network changes every \\033[33mSEC\\033[0m seconds (0=disable)\")\n    ap2.add_argument(\"-zv\", action=\"store_true\", help=\"verbose all zeroconf backends\")\n    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)\")\n\n\ndef add_zc_mdns(ap):\n    ap2 = ap.add_argument_group(\"Zeroconf-mDNS options; also see --help-zm\")\n    ap2.add_argument(\"--zm\", action=\"store_true\", help=\"announce the enabled protocols over mDNS (multicast DNS-SD) -- compatible with KDE, gnome, macOS, ...\")\n    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\")\n    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\")\n    ap2.add_argument(\"--zm4\", action=\"store_true\", help=\"IPv4 only -- try this if some clients can't connect\")\n    ap2.add_argument(\"--zm6\", action=\"store_true\", help=\"IPv6 only\")\n    ap2.add_argument(\"--zmv\", action=\"store_true\", help=\"verbose mdns\")\n    ap2.add_argument(\"--zmvv\", action=\"store_true\", help=\"verboser mdns\")\n    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\")\n    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\")\n    ap2.add_argument(\"--zm-no-pe\", action=\"store_true\", help=\"mute parser errors (invalid incoming MDNS packets)\")\n    ap2.add_argument(\"--zm-nwa-1\", action=\"store_true\", help=\"disable workaround for avahi-bug #379 (corruption in Avahi's mDNS reflection feature)\")\n    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)\")\n    ap2.add_argument(\"--zm-ld\", metavar=\"PATH\", type=u, default=\"\", help=\"link a specific folder for webdav shares\")\n    ap2.add_argument(\"--zm-lh\", metavar=\"PATH\", type=u, default=\"\", help=\"link a specific folder for http shares\")\n    ap2.add_argument(\"--zm-lf\", metavar=\"PATH\", type=u, default=\"\", help=\"link a specific folder for ftp shares\")\n    ap2.add_argument(\"--zm-ls\", metavar=\"PATH\", type=u, default=\"\", help=\"link a specific folder for smb shares\")\n    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\")\n    ap2.add_argument(\"--zm-mnic\", action=\"store_true\", help=\"merge NICs which share subnets; assume that same subnet means same network\")\n    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\")\n    ap2.add_argument(\"--zm-noneg\", action=\"store_true\", help=\"disable NSEC replies -- try this if some clients don't see copyparty\")\n    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\")\n\n\ndef add_zc_ssdp(ap):\n    ap2 = ap.add_argument_group(\"Zeroconf-SSDP options\")\n    ap2.add_argument(\"--zs\", action=\"store_true\", help=\"announce the enabled protocols over SSDP -- compatible with Windows\")\n    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\")\n    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\")\n    ap2.add_argument(\"--zsv\", action=\"store_true\", help=\"verbose SSDP\")\n    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)\")\n    ap2.add_argument(\"--zsid\", metavar=\"UUID\", type=u, default=zsid, help=\"USN (device identifier) to announce\")\n\n\ndef add_sftp(ap):\n    ap2 = ap.add_argument_group(\"SFTP options\")\n    ap2.add_argument(\"--sftp\", metavar=\"PORT\", type=int, default=0, help=\"enable SFTP server on \\033[33mPORT\\033[0m, for example \\033[32m3922\")\n    ap2.add_argument(\"--sftpv\", action=\"store_true\", help=\"verbose\")\n    ap2.add_argument(\"--sftpvv\", action=\"store_true\", help=\"verboser\")\n    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\")\n    ap2.add_argument(\"--sftp4\", action=\"store_true\", help=\"only listen on IPv4\")\n    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...\")\n    ap2.add_argument(\"--sftp-key2u\", action=\"append\", help=argparse.SUPPRESS)\n    ap2.add_argument(\"--sftp-pw\", action=\"store_true\", help=\"allow password-authentication with sftp (not just ssh-keys)\")\n    ap2.add_argument(\"--sftp-anon\", metavar=\"TXT\", type=u, default=\"\", help=\"allow anonymous/unauthenticated connections with \\033[33mTXT\\033[0m as username\")\n    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\")\n    ap2.add_argument(\"--sftp-banner\", metavar=\"T\", type=u, default=\"\", help=\"bannertext to send when someone connects; can be @filepath\")\n    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]\")\n\n\ndef add_ftp(ap):\n    ap2 = ap.add_argument_group(\"FTP options (TCP only)\")\n    ap2.add_argument(\"--ftp\", metavar=\"PORT\", type=int, default=0, help=\"enable FTP server on \\033[33mPORT\\033[0m, for example \\033[32m3921\")\n    ap2.add_argument(\"--ftps\", metavar=\"PORT\", type=int, default=0, help=\"enable FTPS server on \\033[33mPORT\\033[0m, for example \\033[32m3990\")\n    ap2.add_argument(\"--ftpv\", action=\"store_true\", help=\"verbose\")\n    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\")\n    ap2.add_argument(\"--ftp4\", action=\"store_true\", help=\"only listen on IPv4\")\n    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]\")\n    ap2.add_argument(\"--ftp-no-ow\", action=\"store_true\", help=\"if target file exists, reject upload instead of overwrite\")\n    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)\")\n    ap2.add_argument(\"--ftp-nat\", metavar=\"ADDR\", type=u, default=\"\", help=\"the NAT address to use for passive connections\")\n    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\")\n\n\ndef add_webdav(ap):\n    ap2 = ap.add_argument_group(\"WebDAV options\")\n    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)\")\n    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\")\n    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)\")\n    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)\")\n    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)\")\n    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\")\n    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\")\n    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\")\n    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\")\n\n\ndef add_tftp(ap):\n    ap2 = ap.add_argument_group(\"TFTP options (UDP only)\")\n    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\")\n    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\")\n    ap2.add_argument(\"--tftp4\", action=\"store_true\", help=\"only listen on IPv4\")\n    ap2.add_argument(\"--tftpv\", action=\"store_true\", help=\"verbose\")\n    ap2.add_argument(\"--tftpvv\", action=\"store_true\", help=\"verboser\")\n    ap2.add_argument(\"--tftp-no-fast\", action=\"store_true\", help=\"debug: disable optimizations\")\n    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, ...\")\n    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\")\n    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]\")\n    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\")\n\n\ndef add_smb(ap):\n    ap2 = ap.add_argument_group(\"SMB/CIFS options\")\n    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!\")\n    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\")\n    ap2.add_argument(\"--smbw\", action=\"store_true\", help=\"enable write support (please dont)\")\n    ap2.add_argument(\"--smb1\", action=\"store_true\", help=\"disable SMBv2, only enable SMBv1 (CIFS)\")\n    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\")\n    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\")\n    ap2.add_argument(\"--smb-nwa-2\", action=\"store_true\", help=\"disable impacket workaround for filecopy globs\")\n    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)\")\n    ap2.add_argument(\"--smbv\", action=\"store_true\", help=\"verbose\")\n    ap2.add_argument(\"--smbvv\", action=\"store_true\", help=\"verboser\")\n    ap2.add_argument(\"--smbvvv\", action=\"store_true\", help=\"verbosest\")\n\n\ndef add_opds(ap):\n    ap2 = ap.add_argument_group(\"OPDS options\")\n    ap2.add_argument(\"--opds\", action=\"store_true\", help=\"enable opds -- allows e-book readers to browse and download files (volflag=opds)\")\n    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)\")\n\n\ndef add_handlers(ap):\n    ap2 = ap.add_argument_group(\"handlers (see --help-handlers)\")\n    ap2.add_argument(\"--on404\", metavar=\"PY\", type=u, action=\"append\", help=\"\\033[34mREPEATABLE:\\033[0m handle 404s by executing \\033[33mPY\\033[0m file\")\n    ap2.add_argument(\"--on403\", metavar=\"PY\", type=u, action=\"append\", help=\"\\033[34mREPEATABLE:\\033[0m handle 403s by executing \\033[33mPY\\033[0m file\")\n    ap2.add_argument(\"--hot-handlers\", action=\"store_true\", help=\"recompile handlers on each request -- expensive but convenient when hacking on stuff\")\n\n\ndef add_hooks(ap):\n    ap2 = ap.add_argument_group(\"event hooks (see --help-hooks)\")\n    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\")\n    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\")\n    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\")\n    ap2.add_argument(\"--xbc\", metavar=\"CMD\", type=u, action=\"append\", help=\"\\033[34mREPEATABLE:\\033[0m execute \\033[33mCMD\\033[0m before a file copy\")\n    ap2.add_argument(\"--xac\", metavar=\"CMD\", type=u, action=\"append\", help=\"\\033[34mREPEATABLE:\\033[0m execute \\033[33mCMD\\033[0m after  a file copy\")\n    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\")\n    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\")\n    ap2.add_argument(\"--xbd\", metavar=\"CMD\", type=u, action=\"append\", help=\"\\033[34mREPEATABLE:\\033[0m execute \\033[33mCMD\\033[0m before a file delete\")\n    ap2.add_argument(\"--xad\", metavar=\"CMD\", type=u, action=\"append\", help=\"\\033[34mREPEATABLE:\\033[0m execute \\033[33mCMD\\033[0m after  a file delete\")\n    ap2.add_argument(\"--xm\", metavar=\"CMD\", type=u, action=\"append\", help=\"\\033[34mREPEATABLE:\\033[0m execute \\033[33mCMD\\033[0m on message\")\n    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)\")\n    ap2.add_argument(\"--hook-v\", action=\"store_true\", help=\"verbose hooks\")\n\n\ndef add_stats(ap):\n    ap2 = ap.add_argument_group(\"grafana/prometheus metrics endpoint\")\n    ap2.add_argument(\"--stats\", action=\"store_true\", help=\"enable openmetrics at /.cpr/metrics for admin accounts\")\n    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\")\n    ap2.add_argument(\"--nos-hdd\", action=\"store_true\", help=\"disable disk-space metrics (used/free space)\")\n    ap2.add_argument(\"--nos-vol\", action=\"store_true\", help=\"disable volume size metrics (num files, total bytes, vmaxb/vmaxn)\")\n    ap2.add_argument(\"--nos-vst\", action=\"store_true\", help=\"disable volume state metrics (indexing, analyzing, activity)\")\n    ap2.add_argument(\"--nos-dup\", action=\"store_true\", help=\"disable dupe-files metrics (good idea; very slow)\")\n    ap2.add_argument(\"--nos-unf\", action=\"store_true\", help=\"disable unfinished-uploads metrics\")\n\n\ndef add_yolo(ap):\n    ap2 = ap.add_argument_group(\"yolo options\")\n    ap2.add_argument(\"--allow-csrf\", action=\"store_true\", help=\"disable csrf protections; let other domains/sites impersonate you through cross-site requests\")\n    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\")\n    ap2.add_argument(\"--no-fnugg\", action=\"store_true\", help=\"disable the smoketest for caching-related issues in the web-UI\")\n    ap2.add_argument(\"--getmod\", action=\"store_true\", help=\"permit ?move=[...] and ?delete as GET\")\n    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)\")\n    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\")\n\n\ndef add_optouts(ap):\n    ap2 = ap.add_argument_group(\"opt-outs\")\n    ap2.add_argument(\"-nw\", action=\"store_true\", help=\"never write anything to disk (debug/benchmark)\")\n    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)\")\n    ap2.add_argument(\"--no-dav\", action=\"store_true\", help=\"disable webdav support\")\n    ap2.add_argument(\"--no-del\", action=\"store_true\", help=\"disable delete operations\")\n    ap2.add_argument(\"--no-mv\", action=\"store_true\", help=\"disable move/rename operations\")\n    ap2.add_argument(\"--no-cp\", action=\"store_true\", help=\"disable copy operations\")\n    ap2.add_argument(\"--no-fs-abrt\", action=\"store_true\", help=\"disable ability to abort ongoing copy/move\")\n    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\")\n    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\")\n    ap2.add_argument(\"-nb\", action=\"store_true\", help=\"no powered-by-copyparty branding in UI\")\n    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\")\n    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)\")\n    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)\")\n    ap2.add_argument(\"--zipmaxt\", metavar=\"TXT\", type=u, default=\"\", help=\"custom errormessage when download size exceeds max (volflag=zipmaxt)\")\n    ap2.add_argument(\"--zipmaxu\", action=\"store_true\", help=\"authenticated users bypass the zip size limit (volflag=zipmaxu)\")\n    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\")\n    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\")\n    ap2.add_argument(\"--no-zip\", action=\"store_true\", help=\"disable download as zip/tar; same as \\033[33m--zip-who=0\\033[0m\")\n    ap2.add_argument(\"--no-tarcmp\", action=\"store_true\", help=\"disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)\")\n    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\")\n    ap2.add_argument(\"--no-pipe\", action=\"store_true\", help=\"disable race-the-beam (lockstep download of files which are currently being uploaded) (volflag=nopipe)\")\n    ap2.add_argument(\"--no-tail\", action=\"store_true\", help=\"disable streaming a growing files with ?tail (volflag=notail)\")\n    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)\")\n    ap2.add_argument(\"--no-zls\", action=\"store_true\", help=\"disable browsing the contents of zip/cbz files, does not affect thumbnails\")\n\n\ndef add_safety(ap):\n    ap2 = ap.add_argument_group(\"safety options\")\n    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\")\n    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\")\n    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\")\n    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]\")\n    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)\")\n    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)\")\n    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)\")\n    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)\")\n    ap2.add_argument(\"--no-dot-mv\", action=\"store_true\", help=\"disallow moving dotfiles; makes it impossible to move folders containing dotfiles\")\n    ap2.add_argument(\"--no-dot-ren\", action=\"store_true\", help=\"disallow renaming dotfiles; makes it impossible to turn something into a dotfile\")\n    ap2.add_argument(\"--no-logues\", action=\"store_true\", help=\"disable rendering .prologue/.epilogue.html into directory listings\")\n    ap2.add_argument(\"--no-readme\", action=\"store_true\", help=\"disable rendering readme/preadme.md into directory listings\")\n    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\")\n    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\")\n    ap2.add_argument(\"--no-robots\", action=\"store_true\", help=\"adds http and html headers asking search engines to not index anything (volflag=norobots)\")\n    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)\")\n    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\")\n    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\")\n    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]\")\n    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]\")\n    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\")\n    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\")\n    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 ++)\")\n    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)\")\n    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]\")\n    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]\")\n    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\")\n    ap2.add_argument(\"--cookie-nmax\", metavar=\"N\", type=int, default=50, help=\"reject HTTP-request from client if they send more than N cookies\")\n    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\")\n    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\")\n    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]\")\n    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\")\n    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)\")\n\n\ndef add_salt(ap, fk_salt, dk_salt, ah_salt):\n    ap2 = ap.add_argument_group(\"salting options\")\n    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)\")\n    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)\")\n    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]\")\n    ap2.add_argument(\"--ah-cli\", action=\"store_true\", help=\"launch an interactive shell which hashes passwords without ever storing or displaying the original passwords\")\n    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\")\n    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\")\n    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)\")\n    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)\")\n    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)\")\n    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)\")\n\n\ndef add_shutdown(ap):\n    ap2 = ap.add_argument_group(\"shutdown options\")\n    ap2.add_argument(\"--ign-ebind\", action=\"store_true\", help=\"continue running even if it's impossible to listen on some of the requested endpoints\")\n    ap2.add_argument(\"--ign-ebind-all\", action=\"store_true\", help=\"continue running even if it's impossible to receive connections at all\")\n    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\")\n\n\ndef add_logging(ap):\n    ap2 = ap.add_argument_group(\"logging options\")\n    ap2.add_argument(\"-q\", action=\"store_true\", help=\"quiet; disable most STDOUT messages\")\n    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)\")\n    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\")\n    ap2.add_argument(\"--no-ansi\", action=\"store_true\", default=not VT100, help=\"disable colors; same as environment-variable NO_COLOR\")\n    ap2.add_argument(\"--ansi\", action=\"store_true\", help=\"force colors; overrides environment-variable NO_COLOR\")\n    ap2.add_argument(\"--no-logflush\", action=\"store_true\", help=\"don't flush the logfile after each write; tiny bit faster\")\n    ap2.add_argument(\"--no-voldump\", action=\"store_true\", help=\"do not list volumes and permissions on startup\")\n    ap2.add_argument(\"--log-utc\", action=\"store_true\", help=\"do not use local timezone; assume the TZ env-var is UTC (tiny bit faster)\")\n    ap2.add_argument(\"--log-tdec\", metavar=\"N\", type=int, default=3, help=\"timestamp resolution / number of timestamp decimals\")\n    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)\")\n    ap2.add_argument(\"--log-badpwd\", metavar=\"N\", type=int, default=2, help=\"log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed\")\n    ap2.add_argument(\"--log-badxml\", action=\"store_true\", help=\"log any invalid XML received from a client\")\n    ap2.add_argument(\"--log-conn\", action=\"store_true\", help=\"debug: print tcp-server msgs\")\n    ap2.add_argument(\"--log-htp\", action=\"store_true\", help=\"debug: print http-server threadpool scaling\")\n    ap2.add_argument(\"--ihead\", metavar=\"HEADER\", type=u, action='append', help=\"print request \\033[33mHEADER\\033[0m; [\\033[32m*\\033[0m]=all\")\n    ap2.add_argument(\"--ohead\", metavar=\"HEADER\", type=u, action='append', help=\"print response \\033[33mHEADER\\033[0m; [\\033[32m*\\033[0m]=all\")\n    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\")\n    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\")\n    ap2.add_argument(\"--scan-pr-r\", metavar=\"SEC\", type=float, default=10, help=\"fs-indexing: wait \\033[33mSEC\\033[0m between each 'progress:' message\")\n    ap2.add_argument(\"--scan-pr-s\", metavar=\"MiB\", type=float, default=1, help=\"fs-indexing: say 'file: <name>' when a file larger than \\033[33mMiB\\033[0m is about to be hashed\")\n\n\ndef add_admin(ap):\n    ap2 = ap.add_argument_group(\"admin panel options\")\n    ap2.add_argument(\"--no-reload\", action=\"store_true\", help=\"disable ?reload=cfg (reload users/volumes/volflags from config file)\")\n    ap2.add_argument(\"--no-rescan\", action=\"store_true\", help=\"disable ?scan (volume reindexing)\")\n    ap2.add_argument(\"--no-stack\", action=\"store_true\", help=\"disable ?stack (list all stacks); same as --stack-who=no\")\n    ap2.add_argument(\"--no-ups-page\", action=\"store_true\", help=\"disable ?ru (list of recent uploads)\")\n    ap2.add_argument(\"--no-up-list\", action=\"store_true\", help=\"don't show list of incoming files in controlpanel\")\n    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\")\n    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)\")\n    ap2.add_argument(\"--ups-when\", action=\"store_true\", help=\"let everyone see upload timestamps on the ?ru page, not just admins\")\n    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\")\n    ap2.add_argument(\"--stack-v\", action=\"store_true\", help=\"verbose ?stack\")\n\n\ndef add_thumbnail(ap):\n    th_ram = (RAM_AVAIL or RAM_TOTAL or 9) * 0.6\n    th_ram = int(max(min(th_ram, 6), 0.3) * 10) / 10\n    ap2 = ap.add_argument_group(\"thumbnail options\")\n    ap2.add_argument(\"--no-thumb\", action=\"store_true\", help=\"disable all thumbnails (volflag=dthumb)\")\n    ap2.add_argument(\"--no-vthumb\", action=\"store_true\", help=\"disable video thumbnails (volflag=dvthumb)\")\n    ap2.add_argument(\"--no-athumb\", action=\"store_true\", help=\"disable audio thumbnails (spectrograms) (volflag=dathumb)\")\n    ap2.add_argument(\"--th-size\", metavar=\"WxH\", default=\"320x256\", help=\"thumbnail res (volflag=thsize)\")\n    ap2.add_argument(\"--th-mt\", metavar=\"CORES\", type=int, default=CORES, help=\"num cpu cores to use for generating thumbnails\")\n    ap2.add_argument(\"--th-convt\", metavar=\"SEC\", type=float, default=60.0, help=\"convert-to-image timeout in seconds (volflag=convt)\")\n    ap2.add_argument(\"--ac-convt\", metavar=\"SEC\", type=float, default=150.0, help=\"convert-to-audio timeout in seconds (volflag=aconvt)\")\n    ap2.add_argument(\"--th-ram-max\", metavar=\"GB\", type=float, default=th_ram, help=\"max memory usage (GiB) permitted by thumbnailer; not very accurate\")\n    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)\")\n    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)\")\n    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)\")\n    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)\")\n    ap2.add_argument(\"--th-dec\", metavar=\"LIBS\", default=\"vips,raw,pil,ff\", help=\"image decoders, in order of preference\")\n    ap2.add_argument(\"--th-no-jpg\", action=\"store_true\", help=\"disable jpg output\")\n    ap2.add_argument(\"--th-no-webp\", action=\"store_true\", help=\"disable webp output\")\n    ap2.add_argument(\"--th-no-jxl\", action=\"store_true\", help=\"disable jpeg-xl output\")\n    ap2.add_argument(\"--th-ff-jpg\", action=\"store_true\", help=\"force jpg output for video thumbs (avoids issues on some FFmpeg builds)\")\n    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)\")\n    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\")\n    ap2.add_argument(\"--th-clean\", metavar=\"SEC\", type=int, default=43200, help=\"cleanup interval; 0=disabled\")\n    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\")\n    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\")\n    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\")\n    # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html\n    # https://github.com/libvips/libvips\n    # https://stackoverflow.com/a/47612661\n    # 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:'\n    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\")\n    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\")\n    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\")\n    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\")\n    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\")\n    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\")\n    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)\")\n    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\")\n\n\ndef add_transcoding(ap):\n    ap2 = ap.add_argument_group(\"transcoding options\")\n    ap2.add_argument(\"--q-opus\", metavar=\"KBPS\", type=int, default=128, help=\"target bitrate for transcoding to opus; set 0 to disable\")\n    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\")\n    ap2.add_argument(\"--allow-wav\", action=\"store_true\", help=\"allow transcoding to wav (lossless, uncompressed)\")\n    ap2.add_argument(\"--allow-flac\", action=\"store_true\", help=\"allow transcoding to flac (lossless, compressed)\")\n    ap2.add_argument(\"--no-caf\", action=\"store_true\", help=\"disable transcoding to caf-opus (affects iOS v12~v17), will use mp3 instead\")\n    ap2.add_argument(\"--no-owa\", action=\"store_true\", help=\"disable transcoding to webm-opus (iOS v18 and later), will use mp3 instead\")\n    ap2.add_argument(\"--no-acode\", action=\"store_true\", help=\"disable audio transcoding\")\n    ap2.add_argument(\"--no-bacode\", action=\"store_true\", help=\"disable batch audio transcoding by folder download (zip/tar)\")\n    ap2.add_argument(\"--ac-maxage\", metavar=\"SEC\", type=int, default=86400, help=\"delete cached transcode output after \\033[33mSEC\\033[0m seconds\")\n\n\ndef add_tail(ap):\n    ap2 = ap.add_argument_group(\"tailing options (realtime streaming of a growing file)\")\n    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)\")\n    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\")\n    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)\")\n    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)\")\n    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\")\n    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)\")\n\n\ndef add_rss(ap):\n    ap2 = ap.add_argument_group(\"RSS options\")\n    ap2.add_argument(\"--rss\", action=\"store_true\", help=\"enable RSS output (experimental) (volflag=rss)\")\n    ap2.add_argument(\"--rss-nf\", metavar=\"HITS\", type=int, default=250, help=\"default number of files to return (url-param 'nf')\")\n    ap2.add_argument(\"--rss-fext\", metavar=\"E,E\", type=u, default=\"\", help=\"default list of file extensions to include (url-param 'fext'); blank=all\")\n    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)\")\n    ap2.add_argument(\"--rss-fmt-t\", metavar=\"TXT\", type=u, default=\"{fname}\", help=\"title format (url-param 'rss_fmt_t') (volflag=rss_fmt_t)\")\n    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)\")\n\n\ndef add_db_general(ap, hcores):\n    noidx = APPLESAN_TXT if MACOS else \"\"\n    ap2 = ap.add_argument_group(\"general db options\")\n    ap2.add_argument(\"-e2d\", action=\"store_true\", help=\"enable up2k database; this enables file search, upload-undo, improves deduplication\")\n    ap2.add_argument(\"-e2ds\", action=\"store_true\", help=\"scan writable folders for new files on startup; sets \\033[33m-e2d\\033[0m\")\n    ap2.add_argument(\"-e2dsa\", action=\"store_true\", help=\"scans all folders on startup; sets \\033[33m-e2ds\\033[0m\")\n    ap2.add_argument(\"-e2v\", action=\"store_true\", help=\"verify file integrity; rehash all files and compare with db\")\n    ap2.add_argument(\"-e2vu\", action=\"store_true\", help=\"on hash mismatch: update the database with the new hash\")\n    ap2.add_argument(\"-e2vp\", action=\"store_true\", help=\"on hash mismatch: panic and quit copyparty\")\n    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)\")\n    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)\")\n    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\")\n    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)\")\n    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)\")\n    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)\")\n    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\")\n    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\")\n    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)\")\n    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)\")\n    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)\")\n    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)\")\n    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)\")\n    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\")\n    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)\")\n    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, ...)\")\n    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\")\n    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\")\n    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\")\n    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)\")\n    ap2.add_argument(\"--dotsrch\", action=\"store_true\", help=\"show dotfiles in search results (volflags: dotsrch | nodotsrch)\")\n\n\ndef add_db_metadata(ap):\n    ap2 = ap.add_argument_group(\"metadata db options\")\n    ap2.add_argument(\"-e2t\", action=\"store_true\", help=\"enable metadata indexing; makes it possible to search for artist/title/codec/resolution/...\")\n    ap2.add_argument(\"-e2ts\", action=\"store_true\", help=\"scan newly discovered files for metadata on startup; sets \\033[33m-e2t\\033[0m\")\n    ap2.add_argument(\"-e2tsr\", action=\"store_true\", help=\"delete all metadata from DB and do a full rescan; sets \\033[33m-e2ts\\033[0m\")\n    ap2.add_argument(\"--no-mutagen\", action=\"store_true\", help=\"use FFprobe for tags instead; will detect more tags\")\n    ap2.add_argument(\"--no-mtag-ff\", action=\"store_true\", help=\"never use FFprobe as tag reader; is probably safer\")\n    ap2.add_argument(\"--mtag-to\", metavar=\"SEC\", type=int, default=60, help=\"timeout for FFprobe tag-scan\")\n    ap2.add_argument(\"--mtag-mt\", metavar=\"CORES\", type=int, default=CORES, help=\"num cpu cores to use for tag scanning\")\n    ap2.add_argument(\"--mtag-v\", action=\"store_true\", help=\"verbose tag scanning; print errors from mtp subprocesses and such\")\n    ap2.add_argument(\"--mtag-vv\", action=\"store_true\", help=\"debug mtp settings and mutagen/FFprobe parsers\")\n    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)\")\n    ap2.add_argument(\"-mtm\", metavar=\"M=t,t,t\", type=u, action=\"append\", help=\"\\033[34mREPEATABLE:\\033[0m add/replace metadata mapping\")\n    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)\n    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)\n    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\")\n    ap2.add_argument(\"--have-db-xattr\", action=\"store_true\", help=argparse.SUPPRESS)\n\n\ndef add_txt(ap):\n    ap2 = ap.add_argument_group(\"textfile options\")\n    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)\")\n    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)\")\n    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)\")\n    ap2.add_argument(\"--txt-eol\", metavar=\"TYPE\", type=u, default=\"\", help=\"enable EOL conversion when writing documents; supported: CRLF, LF (volflag=txt_eol)\")\n    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\")\n    ap2.add_argument(\"-emp\", action=\"store_true\", help=\"enable markdown plugins -- neat but dangerous, big XSS risk\")\n    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)\")\n    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)\")\n    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)\")\n    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\")\n\n\ndef add_og(ap):\n    ap2 = ap.add_argument_group(\"og / open graph / discord-embed options\")\n    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)\")\n    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)\")\n    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)\")\n    ap2.add_argument(\"--og-no-head\", action=\"store_true\", help=\"do not automatically add OG entries into <head> (useful if you're doing this yourself in a template or such) (volflag=og_no_head)\")\n    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)\")\n    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)\")\n    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)\")\n    ap2.add_argument(\"--og-title-v\", metavar=\"T\", type=u, default=\"{{ title }}\", help=\"video title format; takes any metadata key (volflag=og_title_v)\")\n    ap2.add_argument(\"--og-title-i\", metavar=\"T\", type=u, default=\"{{ title }}\", help=\"image title format; takes any metadata key (volflag=og_title_i)\")\n    ap2.add_argument(\"--og-s-title\", action=\"store_true\", help=\"force default title; do not read from tags (volflag=og_s_title)\")\n    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)\")\n    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)\")\n    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)\")\n    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\")\n\n\ndef add_ui(ap, retry: int):\n    THEMES = 10\n    ap2 = ap.add_argument_group(\"ui options\")\n    ap2.add_argument(\"--grid\", action=\"store_true\", help=\"show grid/thumbnails by default (volflag=grid)\")\n    ap2.add_argument(\"--gsel\", action=\"store_true\", help=\"select files in grid by ctrl-click (volflag=gsel)\")\n    ap2.add_argument(\"--localtime\", action=\"store_true\", help=\"default to local timezone instead of UTC\")\n    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)\")\n    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\")\n    ap2.add_argument(\"--lang\", metavar=\"LANG\", type=u, default=\"eng\", help=\"language, for example \\033[32meng\\033[0m / \\033[32mnor\\033[0m / ...\")\n    ap2.add_argument(\"--theme\", metavar=\"NUM\", type=int, default=0, help=\"default theme to use (0..%d)\" % (THEMES - 1,))\n    ap2.add_argument(\"--themes\", metavar=\"NUM\", type=int, default=THEMES, help=\"number of themes installed\")\n    ap2.add_argument(\"--au-vol\", metavar=\"0-100\", type=int, default=50, choices=range(0, 101), help=\"default audio/video volume percent\")\n    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)\")\n    ap2.add_argument(\"--nsort\", action=\"store_true\", help=\"default-enable natural sort of filenames with leading numbers (volflag=nsort)\")\n    ap2.add_argument(\"--hsortn\", metavar=\"N\", type=int, default=2, help=\"number of sorting rules to include in media URLs by default (volflag=hsortn)\")\n    ap2.add_argument(\"--see-dots\", action=\"store_true\", help=\"default-enable seeing dotfiles; only takes effect if user has the necessary permissions\")\n    ap2.add_argument(\"--qdel\", metavar=\"LVL\", type=int, default=2, help=\"number of confirmations to show when deleting files (2/1/0)\")\n    ap2.add_argument(\"--dlni\", action=\"store_true\", help=\"force download (don't show inline) when files are clicked (volflag:dlni)\")\n    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)\")\n    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\")\n    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)\")\n    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)\")\n    ap2.add_argument(\"--mpmc\", type=u, default=\"\", help=argparse.SUPPRESS)\n    ap2.add_argument(\"--notooltips\", action=\"store_true\", help=\"tooltips disabled as default\")\n    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]\")\n    ap2.add_argument(\"--css-browser\", metavar=\"L\", type=u, default=\"\", help=\"URL to additional CSS to include in the filebrowser html\")\n    ap2.add_argument(\"--js-browser\", metavar=\"L\", type=u, default=\"\", help=\"URL to additional JS to include in the filebrowser html\")\n    ap2.add_argument(\"--js-other\", metavar=\"L\", type=u, default=\"\", help=\"URL to additional JS to include in all other pages\")\n    ap2.add_argument(\"--html-head\", metavar=\"TXT\", type=u, default=\"\", help=\"text to append to the <head> 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)\")\n    ap2.add_argument(\"--html-head-s\", metavar=\"T\", type=u, default=\"\", help=\"text to append to the <head> 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)\")\n    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)\")\n    ap2.add_argument(\"--textfiles\", metavar=\"CSV\", type=u, default=\"txt,nfo,diz,cue,readme\", help=\"file extensions to present as plaintext\")\n    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)\")\n    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)\")\n    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)\")\n    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)\")\n    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)\")\n    ap2.add_argument(\"--doctitle\", metavar=\"TXT\", type=u, default=\"copyparty @ --name\", help=\"title / service-name to show in html documents\")\n    ap2.add_argument(\"--bname\", metavar=\"TXT\", type=u, default=\"--name\", help=\"server name (displayed in filebrowser document title)\")\n    ap2.add_argument(\"--pb-url\", metavar=\"URL\", type=u, default=URL_PRJ, help=\"powered-by link; disable with \\033[33m-nb\\033[0m\")\n    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\")\n    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\")\n    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)\")\n    ap2.add_argument(\"--ver-iwho\", type=int, default=0, help=argparse.SUPPRESS)\n    ap2.add_argument(\"--du-iwho\", type=int, default=0, help=argparse.SUPPRESS)\n    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\")\n    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\")\n    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\")\n    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\")\n    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)\")\n    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)\")\n    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\")\n    ap2.add_argument(\"--no-sb-md\", action=\"store_true\", help=\"don't sandbox README/PREADME.md documents (volflags: no_sb_md | sb_md)\")\n    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\")\n    ap2.add_argument(\"--ui-nombar\", action=\"store_true\", help=\"hide top-menu in the UI (volflag=ui_nombar)\")\n    ap2.add_argument(\"--ui-noacci\", action=\"store_true\", help=\"hide account-info in the UI (volflag=ui_noacci)\")\n    ap2.add_argument(\"--ui-nosrvi\", action=\"store_true\", help=\"hide server-info in the UI (volflag=ui_nosrvi)\")\n    ap2.add_argument(\"--ui-nonav\", action=\"store_true\", help=\"hide navpane+breadcrumbs (volflag=ui_nonav)\")\n    ap2.add_argument(\"--ui-notree\", action=\"store_true\", help=\"hide navpane in the UI (volflag=ui_notree)\")\n    ap2.add_argument(\"--ui-nocpla\", action=\"store_true\", help=\"hide cpanel-link in the UI (volflag=ui_nocpla)\")\n    ap2.add_argument(\"--ui-nolbar\", action=\"store_true\", help=\"hide link-bar in the UI (volflag=ui_nolbar)\")\n    ap2.add_argument(\"--ui-noctxb\", action=\"store_true\", help=\"hide context-buttons in the UI (volflag=ui_noctxb)\")\n    ap2.add_argument(\"--ui-norepl\", action=\"store_true\", help=\"hide repl-button in the UI (volflag=ui_norepl)\")\n    ap2.add_argument(\"--have-unlistc\", action=\"store_true\", help=argparse.SUPPRESS)\n\n\ndef add_debug(ap):\n    ap2 = ap.add_argument_group(\"debug options\")\n    ap2.add_argument(\"--vc\", action=\"store_true\", help=\"verbose config file parser (explain config)\")\n    ap2.add_argument(\"--cgen\", action=\"store_true\", help=\"generate config file from current config (best-effort; probably buggy)\")\n    ap2.add_argument(\"--deps\", action=\"store_true\", help=\"list information about detected optional dependencies\")\n    if hasattr(select, \"poll\"):\n        ap2.add_argument(\"--no-poll\", action=\"store_true\", help=\"kernel-bug workaround: disable poll; use select instead (limits max num clients to ~700)\")\n    ap2.add_argument(\"--no-sendfile\", action=\"store_true\", help=\"kernel-bug workaround: disable sendfile; do a safe and slow read-send-loop instead\")\n    ap2.add_argument(\"--no-scandir\", action=\"store_true\", help=\"kernel-bug workaround: disable scandir; do a listdir + stat on each file instead\")\n    ap2.add_argument(\"--no-fastboot\", action=\"store_true\", help=\"wait for initial filesystem indexing before accepting client requests\")\n    ap2.add_argument(\"--no-htp\", action=\"store_true\", help=\"disable httpserver threadpool, create threads as-needed instead\")\n    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\")\n    ap2.add_argument(\"--srch-dbg\", action=\"store_true\", help=\"explain search processing, and do some extra expensive sanity checks\")\n    ap2.add_argument(\"--rclone-mdns\", action=\"store_true\", help=\"use mdns-domain instead of server-ip on /?hc\")\n    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\")\n    ap2.add_argument(\"--log-thrs\", metavar=\"SEC\", type=float, default=0.0, help=\"list active threads every \\033[33mSEC\\033[0m\")\n    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\")\n    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\")\n    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)\")\n    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\")\n    ap2.add_argument(\"--bf-log\", metavar=\"PATH\", type=u, default=\"\", help=\"bak-flips: log corruption info to a textfile at \\033[33mPATH\\033[0m\")\n    ap2.add_argument(\"--no-cfg-cmt-warn\", action=\"store_true\", help=argparse.SUPPRESS)\n\n\n# fmt: on\n\n\ndef run_argparse(\n    argv: list[str], formatter: Any, retry: int, nc: int, verbose=True\n) -> argparse.Namespace:\n    ap = argparse.ArgumentParser(\n        formatter_class=formatter,\n        usage=argparse.SUPPRESS,\n        prog=\"copyparty\",\n        description=\"http file sharing hub v{} ({})\".format(S_VERSION, S_BUILD_DT),\n    )\n\n    cert_path = os.path.join(E.cfg, \"cert.pem\")\n\n    fk_salt = get_salt(\"fk\", 18)\n    dk_salt = get_salt(\"dk\", 30)\n    ah_salt = get_salt(\"ah\", 18)\n\n    # alpine peaks at 5 threads for some reason,\n    # all others scale past that (but try to avoid SMT),\n    # 5 should be plenty anyways (3 GiB/s on most machines)\n    hcores = min(CORES, 5 if CORES > 8 else 4)\n\n    tty = os.environ.get(\"TERM\", \"\").lower() == \"linux\"\n\n    srvname = get_srvname(verbose)\n\n    add_general(ap, nc, srvname)\n    add_network(ap)\n    add_tls(ap, cert_path)\n    add_cert(ap, cert_path)\n    add_auth(ap)\n    add_chpw(ap)\n    add_qr(ap, tty)\n    add_zeroconf(ap)\n    add_zc_mdns(ap)\n    add_zc_ssdp(ap)\n    add_fs(ap)\n    add_share(ap)\n    add_upload(ap)\n    add_db_general(ap, hcores)\n    add_db_metadata(ap)\n    add_thumbnail(ap)\n    add_transcoding(ap)\n    add_rss(ap)\n    add_sftp(ap)\n    add_ftp(ap)\n    add_webdav(ap)\n    add_tftp(ap)\n    add_smb(ap)\n    add_opds(ap)\n    add_safety(ap)\n    add_salt(ap, fk_salt, dk_salt, ah_salt)\n    add_optouts(ap)\n    add_shutdown(ap)\n    add_yolo(ap)\n    add_handlers(ap)\n    add_hooks(ap)\n    add_stats(ap)\n    add_txt(ap)\n    add_tail(ap)\n    add_og(ap)\n    add_ui(ap, retry)\n    add_admin(ap)\n    add_logging(ap)\n    add_debug(ap)\n\n    ap2 = ap.add_argument_group(\"help sections\")\n    sects = get_sects()\n    for k, h, _ in sects:\n        ap2.add_argument(\"--help-\" + k, action=\"store_true\", help=h)\n\n    if retry:\n        a = [\"ascii\", \"replace\"]\n        for x in ap._actions:\n            try:\n                x.default = x.default.encode(*a).decode(*a)\n            except:\n                pass\n\n            try:\n                if x.help and x.help is not argparse.SUPPRESS:\n                    x.help = x.help.replace(\"└─\", \"`-\").encode(*a).decode(*a)\n                    if retry > 2:\n                        x.help = RE_ANSI.sub(\"\", x.help)\n            except:\n                pass\n\n    ret = ap.parse_args(args=argv[1:])\n    for k, h, t in sects:\n        k2 = \"help_\" + k.replace(\"-\", \"_\")\n        if vars(ret)[k2]:\n            lprint(\"# %s help page (%s)\" % (k, h))\n            lprint(t.rstrip() + \"\\033[0m\")\n            sys.exit(0)\n\n    return ret\n\n\ndef main(argv: Optional[list[str]] = None) -> None:\n    if argv is None:\n        argv = sys.argv\n\n    if \"--versionb\" in argv:\n        print(S_VERSION)\n        sys.exit(0)\n\n    time.strptime(\"19970815\", \"%Y%m%d\")  # python#7980\n    if WINDOWS:\n        os.system(\"rem\")  # enables colors\n\n    init_E(E)\n\n    f = '\\033[36mcopyparty v{} \"\\033[35m{}\\033[36m\" ({})\\n{}\\033[0;36m\\n   sqlite {} | jinja {} | pyftpd {} | tftp {} | miko {}\\n\\033[0m'\n    f = f.format(\n        S_VERSION,\n        CODENAME,\n        S_BUILD_DT,\n        PY_DESC.replace(\"[\", \"\\033[90m[\"),\n        SQLITE_VER,\n        JINJA_VER,\n        PYFTPD_VER,\n        PARTFTPY_VER,\n        MIKO_VER,\n    )\n    lprint(f)\n\n    if \"--version\" in argv:\n        sys.exit(0)\n\n    if \"--license\" in argv:\n        showlic()\n        sys.exit(0)\n\n    if \"--mimes\" in argv:\n        print(\"\\n\".join(\"%8s %s\" % (k, v) for k, v in sorted(MIMES.items())))\n        sys.exit(0)\n\n    if EXE:\n        print(\"pybin: {}\\n\".format(pybin), end=\"\")\n\n    for n, zs in enumerate(argv):\n        if zs.startswith(\"--sfx-tpoke=\"):\n            Daemon(sfx_tpoke, \"sfx-tpoke\", (zs.split(\"=\", 1)[1],))\n            argv.pop(n)\n            break\n\n    ensure_locale()\n\n    ensure_webdeps()\n\n    argv = expand_cfg(argv)\n\n    deprecated: list[tuple[str, str]] = [\n        (\"--salt\", \"--warksalt\"),\n        (\"--hdr-au-usr\", \"--idp-h-usr\"),\n        (\"--idp-h-sep\", \"--idp-gsep\"),\n        (\"--th-no-crop\", \"--th-crop=n\"),\n        (\"--never-symlink\", \"--hardlink-only\"),\n    ]\n    for dk, nk in deprecated:\n        idx = -1\n        ov = \"\"\n        for n, k in enumerate(argv):\n            if k == dk or k.startswith(dk + \"=\"):\n                idx = n\n                if \"=\" in k:\n                    ov = \"=\" + k.split(\"=\", 1)[1]\n\n        if idx < 0:\n            continue\n\n        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\"\n        lprint(msg.format(dk, nk))\n        argv[idx] = nk + ov\n        time.sleep(2)\n\n    da = len(argv) == 1 and not CFG_DEF\n    try:\n        if da:\n            argv.extend([\"--qr\"])\n            if ANYWIN or not os.geteuid():\n                # win10 allows symlinks if admin; can be unexpected\n                argv.extend([\"-p80,443,3923\", \"--ign-ebind\"])\n    except:\n        pass\n\n    if da:\n        t = \"no arguments provided; will use {}\\n\"\n        lprint(t.format(\" \".join(argv[1:])))\n\n    nc = 1024\n    try:\n        import resource\n\n        _, hard = resource.getrlimit(resource.RLIMIT_NOFILE)\n        if hard > 0:  # -1 == infinite\n            nc = min(nc, int(hard / 4))\n    except:\n        nc = 486  # mdns/ssdp restart headroom; select() maxfd is 512 on windows\n\n    retry = 0\n    for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]:\n        try:\n            al = run_argparse(argv, fmtr, retry, nc)\n            dal = run_argparse([], fmtr, retry, nc, False)\n            break\n        except SystemExit:\n            raise\n        except:\n            retry += 1\n            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\"\n            lprint(t % (fmtr, min_ex()))\n\n    try:\n        assert al  # type: ignore\n        assert dal  # type: ignore\n        al.E = E  # __init__ is not shared when oxidized\n    except:\n        sys.exit(1)\n\n    quotecheck(al)\n\n    if al.chdir:\n        os.chdir(al.chdir)\n\n    if al.ansi:\n        al.no_ansi = False\n    elif not al.no_ansi:\n        al.ansi = VT100\n\n    if WINDOWS and not al.keep_qem and not al.ah_cli:\n        try:\n            disable_quickedit()\n        except:\n            lprint(\"\\nfailed to disable quick-edit-mode:\\n\" + min_ex() + \"\\n\")\n\n    if not al.ansi:\n        al.wintitle = \"\"\n\n    # propagate implications\n    for k1, k2 in IMPLICATIONS:\n        if getattr(al, k1, None):\n            setattr(al, k2, True)\n\n    # propagate unplications\n    for k1, k2 in UNPLICATIONS:\n        if getattr(al, k1):\n            setattr(al, k2, False)\n\n    protos = \"ftp tftp sftp smb\".split()\n    opts = [\"%s_i\" % (zs,) for zs in protos]\n    for opt in opts:\n        if getattr(al, opt) == \"-i\":\n            setattr(al, opt, al.i)\n    for opt in [\"i\"] + opts:\n        zs = getattr(al, opt)\n        if not HAVE_IPV6 and zs == \"::\":\n            zs = \"0.0.0.0\"\n        setattr(al, opt, [x.strip() for x in zs.split(\",\")])\n\n    try:\n        if \"-\" in al.p:\n            lo, hi = [int(x) for x in al.p.split(\"-\")]\n            al.p = list(range(lo, hi + 1))\n        else:\n            al.p = [int(x) for x in al.p.split(\",\")]\n    except:\n        raise Exception(\"invalid value for -p\")\n\n    for arg, kname, okays in [[\"--u2sort\", \"u2sort\", \"s n fs fn\"]]:\n        val = unicode(getattr(al, kname))\n        if val not in okays.split():\n            zs = \"argument {} cannot be '{}'; try one of these: {}\"\n            raise Exception(zs.format(arg, val, okays))\n\n    if not al.qrs and [k for k in argv if k.startswith(\"--qr\")]:\n        al.qr = True\n\n    if al.ihead:\n        al.ihead = [x.lower() for x in al.ihead]\n\n    if al.ohead:\n        al.ohead = [x.lower() for x in al.ohead]\n\n    if HAVE_SSL:\n        if al.ssl_ver:\n            configure_ssl_ver(al)\n\n        if al.ciphers:\n            configure_ssl_ciphers(al)\n    else:\n        warn(\"ssl module does not exist; cannot enable https\")\n        al.http_only = True\n\n    if PY2 and WINDOWS and al.e2d:\n        warn(\n            \"windows py2 cannot do unicode filenames with -e2d\\n\"\n            + \"  (if you crash with codec errors then that is why)\"\n        )\n\n    if PY2 and al.smb:\n        print(\"error: python2 cannot --smb\")\n        return\n\n    if not PY36:\n        al.no_scandir = True\n\n    if not hasattr(os, \"sendfile\"):\n        al.no_sendfile = True\n\n    if not hasattr(select, \"poll\"):\n        al.no_poll = True\n\n    # signal.signal(signal.SIGINT, sighandler)\n\n    SvcHub(al, dal, argv, \"\".join(printed)).run()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "copyparty/__version__.py",
    "content": "# coding: utf-8\n\nVERSION = (1, 20, 12)\nCODENAME = \"sftp is fine too\"\nBUILD_DT = (2026, 3, 11)\n\nS_VERSION = \".\".join(map(str, VERSION))\nS_BUILD_DT = \"{0:04d}-{1:02d}-{2:02d}\".format(*BUILD_DT)\n\n__version__ = S_VERSION\n__build_dt__ = S_BUILD_DT\n\n# I'm all ears\n"
  },
  {
    "path": "copyparty/authsrv.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport argparse\nimport base64\nimport hashlib\nimport json\nimport os\nimport re\nimport stat\nimport sys\nimport threading\nimport time\nfrom datetime import datetime\n\nfrom .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, WINDOWS, E\nfrom .bos import bos\nfrom .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap\nfrom .pwhash import PWHash\nfrom .util import (\n    DEF_MTE,\n    DEF_MTH,\n    EXTS,\n    FAVICON_MIMES,\n    FN_EMB,\n    HAVE_SQLITE3,\n    IMPLICATIONS,\n    META_NOBOTS,\n    MIMES,\n    SQLITE_VER,\n    UNPLICATIONS,\n    UTC,\n    ODict,\n    Pebkac,\n    absreal,\n    afsenc,\n    get_df,\n    humansize,\n    json_hesc,\n    min_ex,\n    odfusion,\n    read_utf8,\n    relchk,\n    statdir,\n    ub64enc,\n    uncyg,\n    undot,\n    unhumanize,\n    vjoin,\n    vsplit,\n)\n\nif HAVE_SQLITE3:\n    import sqlite3\n\nif True:  # pylint: disable=using-constant-test\n    from collections.abc import Iterable\n\n    from typing import Any, Generator, Optional, Sequence, Union\n\n    from .util import NamedLogger, RootLogger\n\nif TYPE_CHECKING:\n    from .broker_mp import BrokerMp\n    from .broker_thr import BrokerThr\n    from .broker_util import BrokerCli\n\n    # Vflags: TypeAlias = dict[str, str | bool | float | list[str]]\n    # Vflags: TypeAlias = dict[str, Any]\n    # Mflags: TypeAlias = dict[str, Vflags]\n\nif PY2:\n    range = xrange  # type: ignore\n\n\nLEELOO_DALLAS = \"leeloo_dallas\"\n##\n## you might be curious what Leeloo Dallas is doing here, so let me explain:\n##\n## certain daemonic tasks, namely:\n##  * deletion of expired files, running on a timer\n##  * deletion of sidecar files, initiated by plugins\n## need to skip the usual permission-checks to do their thing,\n## so we let Leeloo handle these\n##\n## and also, the smb-server has really shitty support for user-accounts\n## so one popular way to avoid issues is by running copyparty without users;\n## this makes all smb-clients identify as LD to gain unrestricted access\n##\n## Leeloo, being a fictional character from The Fifth Element,\n## obviously does not exist and will never be able to access any copyparty\n## instances from the outside (the username is rejected at every entrypoint)\n##\n## thanks for coming to my ted talk\n\n\nUP_MTE_MAP = {  # db-order\n    \"w\": \"substr(w,1,16)\",\n    \"up_ip\": \"ip\",\n    \".up_at\": \"at\",\n    \"up_by\": \"un\",\n}\n\nSEE_LOG = \"see log for details\"\nSEESLOG = \" (see fileserver log for details)\"\nSSEELOG = \" ({})\".format(SEE_LOG)\nBAD_CFG = \"invalid config; {}\".format(SEE_LOG)\nSBADCFG = \" ({})\".format(BAD_CFG)\n\nPTN_U_GRP = re.compile(r\"\\$\\{u(%[+-][^}]+)\\}\")\nPTN_G_GRP = re.compile(r\"\\$\\{g(%[+-][^}]+)\\}\")\nPTN_U_ANY = re.compile(r\"(\\${[u][}%])\")\nPTN_G_ANY = re.compile(r\"(\\${[g][}%])\")\nPTN_SIGIL = re.compile(r\"(\\${[ug][}%])\")\n\n\nclass CfgEx(Exception):\n    pass\n\n\nclass AXS(object):\n    def __init__(\n        self,\n        uread: Optional[Union[list[str], set[str]]] = None,\n        uwrite: Optional[Union[list[str], set[str]]] = None,\n        umove: Optional[Union[list[str], set[str]]] = None,\n        udel: Optional[Union[list[str], set[str]]] = None,\n        uget: Optional[Union[list[str], set[str]]] = None,\n        upget: Optional[Union[list[str], set[str]]] = None,\n        uhtml: Optional[Union[list[str], set[str]]] = None,\n        uadmin: Optional[Union[list[str], set[str]]] = None,\n        udot: Optional[Union[list[str], set[str]]] = None,\n    ) -> None:\n        self.uread: set[str] = set(uread or [])\n        self.uwrite: set[str] = set(uwrite or [])\n        self.umove: set[str] = set(umove or [])\n        self.udel: set[str] = set(udel or [])\n        self.uget: set[str] = set(uget or [])\n        self.upget: set[str] = set(upget or [])\n        self.uhtml: set[str] = set(uhtml or [])\n        self.uadmin: set[str] = set(uadmin or [])\n        self.udot: set[str] = set(udot or [])\n\n    def __repr__(self) -> str:\n        ks = \"uread uwrite umove udel uget upget uhtml uadmin udot\".split()\n        return \"AXS(%s)\" % (\", \".join(\"%s=%r\" % (k, self.__dict__[k]) for k in ks),)\n\n\nclass Lim(object):\n    def __init__(\n        self, args: argparse.Namespace, log_func: Optional[\"RootLogger\"]\n    ) -> None:\n        self.log_func = log_func\n        self.use_scandir = not args.no_scandir\n\n        self.reg: Optional[dict[str, dict[str, Any]]] = None  # up2k registry\n\n        self.chmod_d = 0o755\n        self.uid = self.gid = -1\n        self.chown = False\n\n        self.nups: dict[str, list[float]] = {}  # num tracker\n        self.bups: dict[str, list[tuple[float, int]]] = {}  # byte tracker list\n        self.bupc: dict[str, int] = {}  # byte tracker cache\n\n        self.nosub = False  # disallow subdirectories\n\n        self.dfl = 0  # free disk space limit\n        self.dft = 0  # last-measured time\n        self.dfv = 0  # currently free\n        self.vbmax = 0  # volume bytes max\n        self.vnmax = 0  # volume max num files\n\n        self.c_vb_v = 0  # cache: volume bytes used (value)\n        self.c_vb_r = 0  # cache: volume bytes used (ref)\n\n        self.smin = 0  # filesize min\n        self.smax = 0  # filesize max\n\n        self.bwin = 0  # bytes window\n        self.bmax = 0  # bytes max\n        self.nwin = 0  # num window\n        self.nmax = 0  # num max\n\n        self.rotn = 0  # rot num files\n        self.rotl = 0  # rot depth\n        self.rotf = \"\"  # rot datefmt\n        self.rotf_tz = UTC  # rot timezone\n        self.rot_re = re.compile(\"\")  # rotf check\n\n    def log(self, msg: str, c: Union[int, str] = 0) -> None:\n        if self.log_func:\n            self.log_func(\"up-lim\", msg, c)\n\n    def set_rotf(self, fmt: str, tz: str) -> None:\n        self.rotf = fmt.rstrip(\"/\\\\\")\n        if tz != \"UTC\":\n            from zoneinfo import ZoneInfo\n\n            self.rotf_tz = ZoneInfo(tz)\n        r = re.escape(self.rotf).replace(\"%Y\", \"[0-9]{4}\").replace(\"%j\", \"[0-9]{3}\")\n        r = re.sub(\"%[mdHMSWU]\", \"[0-9]{2}\", r)\n        self.rot_re = re.compile(\"(^|/)\" + r + \"$\")\n\n    def all(\n        self,\n        ip: str,\n        rem: str,\n        sz: int,\n        ptop: str,\n        abspath: str,\n        broker: Optional[Union[\"BrokerCli\", \"BrokerMp\", \"BrokerThr\"]] = None,\n        reg: Optional[dict[str, dict[str, Any]]] = None,\n        volgetter: str = \"up2k.get_volsize\",\n    ) -> tuple[str, str]:\n        if reg is not None and self.reg is None:\n            self.reg = reg\n            self.dft = 0\n\n        self.chk_nup(ip)\n        self.chk_bup(ip)\n        self.chk_rem(rem)\n        if sz != -1:\n            self.chk_sz(sz)\n        else:\n            sz = 0\n\n        self.chk_vsz(broker, ptop, sz, volgetter)\n        self.chk_df(abspath, sz)  # side effects; keep last-ish\n\n        ap2, vp2 = self.rot(abspath)\n        if abspath == ap2:\n            return ap2, rem\n\n        return ap2, (\"{}/{}\".format(rem, vp2) if rem else vp2)\n\n    def chk_sz(self, sz: int) -> None:\n        if sz < self.smin:\n            raise Pebkac(400, \"file too small\")\n\n        if self.smax and sz > self.smax:\n            raise Pebkac(400, \"file too big\")\n\n    def chk_vsz(\n        self,\n        broker: Optional[Union[\"BrokerCli\", \"BrokerMp\", \"BrokerThr\"]],\n        ptop: str,\n        sz: int,\n        volgetter: str = \"up2k.get_volsize\",\n    ) -> None:\n        if not broker or not self.vbmax + self.vnmax:\n            return\n\n        x = broker.ask(volgetter, ptop)\n        self.c_vb_v, nfiles = x.get()\n\n        if self.vbmax and self.vbmax < self.c_vb_v + sz:\n            raise Pebkac(400, \"volume has exceeded max size\")\n\n        if self.vnmax and self.vnmax < nfiles + 1:\n            raise Pebkac(400, \"volume has exceeded max num.files\")\n\n    def chk_df(self, abspath: str, sz: int, already_written: bool = False) -> None:\n        if not self.dfl:\n            return\n\n        if self.dft < time.time():\n            self.dft = int(time.time()) + 300\n\n            df, du, err = get_df(abspath, True)\n            if err:\n                t = \"failed to read disk space usage for %r: %s\"\n                self.log(t % (abspath, err), 3)\n                self.dfv = 0xAAAAAAAAA  # 42.6 GiB\n            else:\n                self.dfv = df or 0\n\n            for j in list(self.reg.values()) if self.reg else []:\n                self.dfv -= int(j[\"size\"] / (len(j[\"hash\"]) or 999) * len(j[\"need\"]))\n\n            if already_written:\n                sz = 0\n\n        if self.dfv - sz < self.dfl:\n            self.dft = min(self.dft, int(time.time()) + 10)\n            t = \"server HDD is full; {} free, need {}\"\n            raise Pebkac(500, t.format(humansize(self.dfv - self.dfl), humansize(sz)))\n\n        self.dfv -= int(sz)\n\n    def chk_rem(self, rem: str) -> None:\n        if self.nosub and rem:\n            raise Pebkac(500, \"no subdirectories allowed\")\n\n    def rot(self, path: str) -> tuple[str, str]:\n        if not self.rotf and not self.rotn:\n            return path, \"\"\n\n        if self.rotf:\n            path = path.rstrip(\"/\\\\\")\n            if self.rot_re.search(path.replace(\"\\\\\", \"/\")):\n                return path, \"\"\n\n            suf = datetime.now(self.rotf_tz).strftime(self.rotf)\n            if path:\n                path += \"/\"\n\n            return path + suf, suf\n\n        ret = self.dive(path, self.rotl)\n        if not ret:\n            raise Pebkac(500, \"no available slots in volume\")\n\n        d = ret[len(path) :].strip(\"/\\\\\").replace(\"\\\\\", \"/\")\n        return ret, d\n\n    def dive(self, path: str, lvs: int) -> Optional[str]:\n        if not lvs:\n            # at leaf level\n            items = statdir(self.log_func, self.use_scandir, False, path, True)\n            items = [\n                x\n                for x in items\n                if not stat.S_ISDIR(x[1].st_mode) and not x[0].endswith(\".PARTIAL\")\n            ]\n            return None if len(items) >= self.rotn else \"\"\n\n        items = bos.listdir(path)\n        dirs = [int(x) for x in items if x and all(y in \"1234567890\" for y in x)]\n        dirs.sort()\n\n        if not dirs:\n            # no branches yet; make one\n            sub = os.path.join(path, \"0\")\n            bos.mkdir(sub, self.chmod_d)\n            if self.chown:\n                os.chown(sub, self.uid, self.gid)\n        else:\n            # try newest branch only\n            sub = os.path.join(path, str(dirs[-1]))\n\n        ret = self.dive(sub, lvs - 1)\n        if ret is not None:\n            return os.path.join(sub, ret)\n\n        if len(dirs) >= self.rotn:\n            # full branch or root\n            return None\n\n        # make a branch\n        sub = os.path.join(path, str(dirs[-1] + 1))\n        bos.mkdir(sub, self.chmod_d)\n        if self.chown:\n            os.chown(sub, self.uid, self.gid)\n        ret = self.dive(sub, lvs - 1)\n        if ret is None:\n            raise Pebkac(500, \"rotation bug\")\n\n        return os.path.join(sub, ret)\n\n    def nup(self, ip: str) -> None:\n        try:\n            self.nups[ip].append(time.time())\n        except:\n            self.nups[ip] = [time.time()]\n\n    def bup(self, ip: str, nbytes: int) -> None:\n        v = (time.time(), nbytes)\n        try:\n            self.bups[ip].append(v)\n            self.bupc[ip] += nbytes\n        except:\n            self.bups[ip] = [v]\n            self.bupc[ip] = nbytes\n\n    def chk_nup(self, ip: str) -> None:\n        if not self.nmax or ip not in self.nups:\n            return\n\n        nups = self.nups[ip]\n        cutoff = time.time() - self.nwin\n        while nups and nups[0] < cutoff:\n            nups.pop(0)\n\n        if len(nups) >= self.nmax:\n            raise Pebkac(429, \"too many uploads\")\n\n    def chk_bup(self, ip: str) -> None:\n        if not self.bmax or ip not in self.bups:\n            return\n\n        bups = self.bups[ip]\n        cutoff = time.time() - self.bwin\n        mark = self.bupc[ip]\n        while bups and bups[0][0] < cutoff:\n            mark -= bups.pop(0)[1]\n\n        self.bupc[ip] = mark\n        if mark >= self.bmax:\n            raise Pebkac(429, \"upload size limit exceeded\")\n\n\nclass VFS(object):\n    \"\"\"single level in the virtual fs\"\"\"\n\n    def __init__(\n        self,\n        log: Optional[\"RootLogger\"],\n        realpath: str,\n        vpath: str,\n        vpath0: str,\n        axs: AXS,\n        flags: dict[str, Any],\n    ) -> None:\n        self.log = log\n        self.realpath = realpath  # absolute path on host filesystem\n        self.vpath = vpath  # absolute path in the virtual filesystem\n        self.vpath0 = vpath0  # original vpath (before idp expansion)\n        self.axs = axs\n        self.uaxs: dict[\n            str, tuple[bool, bool, bool, bool, bool, bool, bool, bool, bool]\n        ] = {}\n        self.flags = flags  # config options\n        self.root = self\n        self.dev = 0  # st_dev\n        self.nodes: dict[str, VFS] = {}  # child nodes\n        self.histtab: dict[str, str] = {}  # all realpath->histpath\n        self.dbpaths: dict[str, str] = {}  # all realpath->dbpath\n        self.dbv: Optional[VFS] = None  # closest full/non-jump parent\n        self.lim: Optional[Lim] = None  # upload limits; only set for dbv\n        self.shr_src: Optional[tuple[VFS, str]] = None  # source vfs+rem of a share\n        self.shr_files: set[str] = set()  # filenames to include from shr_src\n        self.shr_owner: str = \"\"  # uname\n        self.shr_all_aps: list[tuple[str, list[VFS]]] = []\n        self.aread: dict[str, list[str]] = {}\n        self.awrite: dict[str, list[str]] = {}\n        self.amove: dict[str, list[str]] = {}\n        self.adel: dict[str, list[str]] = {}\n        self.aget: dict[str, list[str]] = {}\n        self.apget: dict[str, list[str]] = {}\n        self.ahtml: dict[str, list[str]] = {}\n        self.aadmin: dict[str, list[str]] = {}\n        self.adot: dict[str, list[str]] = {}\n        self.js_ls = {}\n        self.js_htm = \"\"\n        self.all_vols: dict[str, VFS] = {}  # flattened recursive\n        self.all_nodes: dict[str, VFS] = {}  # also jumpvols/shares\n        self.all_fvols: dict[str, VFS] = {}  # volumes which are files\n\n        if realpath:\n            rp = realpath + (\"\" if realpath.endswith(os.sep) else os.sep)\n            vp = vpath + (\"/\" if vpath else \"\")\n            self.histpath = os.path.join(realpath, \".hist\")  # db / thumbcache\n            self.dbpath = self.histpath\n            self.all_vols[vpath] = self\n            self.all_nodes[vpath] = self\n            self.all_aps = [(rp, [self])]\n            self.all_vps = [(vp, self)]\n            self.canonical = self._canonical\n            self.dcanonical = self._dcanonical\n        else:\n            self.histpath = self.dbpath = \"\"\n            self.all_aps = []\n            self.all_vps = []\n            self.canonical = self._canonical_null\n            self.dcanonical = self._dcanonical_null\n\n        self.get_dbv = self._get_dbv\n        self.ls = self._ls\n\n    def __repr__(self) -> str:\n        return \"VFS(%s)\" % (\n            \", \".join(\n                \"%s=%r\" % (k, self.__dict__[k])\n                for k in \"realpath vpath axs flags\".split()\n            )\n        )\n\n    def get_all_vols(\n        self,\n        vols: dict[str, \"VFS\"],\n        nodes: dict[str, \"VFS\"],\n        aps: list[tuple[str, list[\"VFS\"]]],\n        vps: list[tuple[str, \"VFS\"]],\n    ) -> None:\n        nodes[self.vpath] = self\n        if self.realpath:\n            vols[self.vpath] = self\n            rp = self.realpath\n            rp += \"\" if rp.endswith(os.sep) else os.sep\n            vp = self.vpath + (\"/\" if self.vpath else \"\")\n            hit = next((x[1] for x in aps if x[0] == rp), None)\n            if hit:\n                hit.append(self)\n            else:\n                aps.append((rp, [self]))\n            vps.append((vp, self))\n\n        for v in self.nodes.values():\n            v.get_all_vols(vols, nodes, aps, vps)\n\n    def add(self, src: str, dst: str, dst0: str) -> \"VFS\":\n        \"\"\"get existing, or add new path to the vfs\"\"\"\n        assert src == \"/\" or not src.endswith(\"/\")  # nosec\n        assert not dst.endswith(\"/\")  # nosec\n\n        if \"/\" in dst:\n            # requires breadth-first population (permissions trickle down)\n            name, dst = dst.split(\"/\", 1)\n            name0, dst0 = dst0.split(\"/\", 1)\n            if name in self.nodes:\n                # exists; do not manipulate permissions\n                return self.nodes[name].add(src, dst, dst0)\n\n            vn = VFS(\n                self.log,\n                os.path.join(self.realpath, name) if self.realpath else \"\",\n                \"{}/{}\".format(self.vpath, name).lstrip(\"/\"),\n                \"{}/{}\".format(self.vpath0, name0).lstrip(\"/\"),\n                self.axs,\n                self._copy_flags(name),\n            )\n            vn.dbv = self.dbv or self\n            self.nodes[name] = vn\n            return vn.add(src, dst, dst0)\n\n        if dst in self.nodes:\n            # leaf exists; return as-is\n            return self.nodes[dst]\n\n        # leaf does not exist; create and keep permissions blank\n        vp = \"{}/{}\".format(self.vpath, dst).lstrip(\"/\")\n        vp0 = \"{}/{}\".format(self.vpath0, dst0).lstrip(\"/\")\n        vn = VFS(self.log, src, vp, vp0, AXS(), {})\n        vn.dbv = self.dbv or self\n        self.nodes[dst] = vn\n        return vn\n\n    def _copy_flags(self, name: str) -> dict[str, Any]:\n        flags = {k: v for k, v in self.flags.items()}\n\n        hist = flags.get(\"hist\")\n        if hist and hist != \"-\":\n            zs = \"{}/{}\".format(hist.rstrip(\"/\"), name)\n            flags[\"hist\"] = os.path.expandvars(os.path.expanduser(zs))\n\n        dbp = flags.get(\"dbpath\")\n        if dbp and dbp != \"-\":\n            zs = \"{}/{}\".format(dbp.rstrip(\"/\"), name)\n            flags[\"dbpath\"] = os.path.expandvars(os.path.expanduser(zs))\n\n        return flags\n\n    def bubble_flags(self) -> None:\n        if self.dbv:\n            for k, v in self.dbv.flags.items():\n                if k not in (\"hist\", \"dbpath\"):\n                    self.flags[k] = v\n\n        for n in self.nodes.values():\n            n.bubble_flags()\n\n    def _find(self, vpath: str) -> tuple[\"VFS\", str]:\n        \"\"\"return [vfs,remainder]\"\"\"\n        if not vpath:\n            return self, \"\"\n\n        if \"/\" in vpath:\n            name, rem = vpath.split(\"/\", 1)\n        else:\n            name = vpath\n            rem = \"\"\n\n        if name in self.nodes:\n            return self.nodes[name]._find(rem)\n\n        return self, vpath\n\n    def can_access(\n        self, vpath: str, uname: str\n    ) -> tuple[bool, bool, bool, bool, bool, bool, bool, bool, bool]:\n        \"\"\"can Read,Write,Move,Delete,Get,Upget,Html,Admin,Dot\"\"\"\n        # NOTE: only used by get_perms, which is only used by hooks; the lowest of fruits\n        if vpath:\n            vn, _ = self._find(undot(vpath))\n        else:\n            vn = self\n\n        return vn.uaxs[uname]\n\n    def get_perms(self, vpath: str, uname: str) -> str:\n        zbl = self.can_access(vpath, uname)\n        ret = \"\".join(ch for ch, ok in zip(\"rwmdgGha.\", zbl) if ok)\n        if \"rwmd\" in ret and \"a.\" in ret:\n            ret += \"A\"\n        return ret\n\n    def get(\n        self,\n        vpath: str,\n        uname: str,\n        will_read: bool,\n        will_write: bool,\n        will_move: bool = False,\n        will_del: bool = False,\n        will_get: bool = False,\n        err: int = 403,\n    ) -> tuple[\"VFS\", str]:\n        \"\"\"returns [vfsnode,fs_remainder] if user has the requested permissions\"\"\"\n        if relchk(vpath):\n            if self.log:\n                self.log(\"vfs\", \"invalid relpath %r @%s\" % (vpath, uname))\n            raise Pebkac(422)\n\n        cvpath = undot(vpath)\n        vn, rem = self._find(cvpath)\n        c: AXS = vn.axs\n\n        for req, d, msg in [\n            (will_read, c.uread, \"read\"),\n            (will_write, c.uwrite, \"write\"),\n            (will_move, c.umove, \"move\"),\n            (will_del, c.udel, \"delete\"),\n            (will_get, c.uget, \"get\"),\n        ]:\n            if req and uname not in d and uname != LEELOO_DALLAS:\n                if vpath != cvpath and vpath != \".\" and self.log:\n                    ap = vn.canonical(rem)\n                    t = \"%s has no %s in %r => %r => %r\"\n                    self.log(\"vfs\", t % (uname, msg, vpath, cvpath, ap), 6)\n\n                t = \"you don't have %s-access in %r or below %r\"\n                raise Pebkac(err, t % (msg, \"/\" + cvpath, \"/\" + vn.vpath))\n\n        return vn, rem\n\n    def _get_share_src(self, vrem: str) -> tuple[\"VFS\", str]:\n        src = self.shr_src\n        if not src:\n            return self._get_dbv(vrem)\n\n        shv, srem = src\n        return shv._get_dbv(vjoin(srem, vrem))\n\n    def _get_dbv(self, vrem: str) -> tuple[\"VFS\", str]:\n        dbv = self.dbv\n        if not dbv:\n            return self, vrem\n\n        vrem = vjoin(self.vpath[len(dbv.vpath) :].lstrip(\"/\"), vrem)\n        return dbv, vrem\n\n    def casechk(self, rem: str, do_stat: bool) -> bool:\n        ap = self.canonical(rem, False)\n        if do_stat and not bos.path.exists(ap):\n            return True  # doesn't exist at all; good to go\n        dp, fn = os.path.split(ap)\n        if not fn:\n            return True  # filesystem root\n        try:\n            fns = os.listdir(dp)\n        except:\n            return True  # maybe chmod 111; assume ok\n        if fn in fns:\n            return True\n        hit = \"<?>\"\n        lfn = fn.lower()\n        for zs in fns:\n            if lfn == zs.lower():\n                hit = zs\n                break\n        if not hit:\n            return True  # NFC/NFD or something, can't be helped either way\n        if self.log:\n            t = \"returning 404 due to underlying case-insensitive filesystem:\\n  http-req: %r\\n  local-fs: %r\"\n            self.log(\"vfs\", t % (fn, hit))\n        return False\n\n    def _canonical_null(self, rem: str, resolve: bool = True) -> str:\n        return \"\"\n\n    def _dcanonical_null(self, rem: str) -> str:\n        return \"\"\n\n    def _canonical(self, rem: str, resolve: bool = True) -> str:\n        \"\"\"returns the canonical path (fully-resolved absolute fs path)\"\"\"\n        ap = self.realpath\n        if rem:\n            ap += \"/\" + rem\n\n        return absreal(ap) if resolve else ap\n\n    def _dcanonical(self, rem: str) -> str:\n        \"\"\"resolves until the final component (filename)\"\"\"\n        ap = self.realpath\n        if rem:\n            ap += \"/\" + rem\n\n        ad, fn = os.path.split(ap)\n        return os.path.join(absreal(ad), fn)\n\n    def _canonical_shr(self, rem: str, resolve: bool = True) -> str:\n        \"\"\"returns the canonical path (fully-resolved absolute fs path)\"\"\"\n        ap = self.realpath\n        if rem:\n            ap += \"/\" + rem\n\n        rap = \"\"\n        if self.shr_files:\n            assert self.shr_src  # !rm\n            if rem and rem not in self.shr_files:\n                return \"\\n\\n\"\n            if resolve:\n                rap = absreal(ap)\n                vn, rem = self.shr_src\n                chk = absreal(os.path.join(vn.realpath, rem))\n                if chk != rap:\n                    # not the dir itself; assert file allowed\n                    ad, fn = os.path.split(rap)\n                    if chk != ad or fn not in self.shr_files:\n                        return \"\\n\\n\"\n\n        return (rap or absreal(ap)) if resolve else ap\n\n    def _dcanonical_shr(self, rem: str) -> str:\n        \"\"\"resolves until the final component (filename)\"\"\"\n        ap = self.realpath\n        if rem:\n            ap += \"/\" + rem\n\n        ad, fn = os.path.split(ap)\n        ad = absreal(ad)\n        if self.shr_files:\n            assert self.shr_src  # !rm\n            vn, rem = self.shr_src\n            chk = absreal(os.path.join(vn.realpath, rem))\n            if chk != absreal(ap):\n                # not the dir itself; assert file allowed\n                if ad != chk or fn not in self.shr_files:\n                    return \"\\n\\n\"\n\n        return os.path.join(ad, fn)\n\n    def _ls_nope(\n        self, *a, **ka\n    ) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, \"VFS\"]]:\n        raise Pebkac(500, \"nope.avi\")\n\n    def _ls_shr(\n        self,\n        rem: str,\n        uname: str,\n        scandir: bool,\n        permsets: list[list[bool]],\n        lstat: bool = False,\n        throw: bool = False,\n    ) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, \"VFS\"]]:\n        \"\"\"replaces _ls for certain shares (single-file, or file selection)\"\"\"\n        vn, rem = self.shr_src  # type: ignore\n        abspath, real, _ = vn.ls(rem, \"\\n\", scandir, permsets, lstat, throw)\n        real = [x for x in real if os.path.basename(x[0]) in self.shr_files]\n        return abspath, real, {}\n\n    def _ls(\n        self,\n        rem: str,\n        uname: str,\n        scandir: bool,\n        permsets: list[list[bool]],\n        lstat: bool = False,\n        throw: bool = False,\n    ) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, \"VFS\"]]:\n        \"\"\"return user-readable [fsdir,real,virt] items at vpath\"\"\"\n        virt_vis = {}  # nodes readable by user\n        abspath = self.canonical(rem)\n        if abspath:\n            real = list(statdir(self.log, scandir, lstat, abspath, throw))\n            real.sort()\n        else:\n            real = []\n\n        if not rem:\n            # no vfs nodes in the list of real inodes\n            real = [x for x in real if x[0] not in self.nodes]\n\n            dbv = self.dbv or self\n            for name, vn2 in sorted(self.nodes.items()):\n                if vn2.dbv == dbv and self.flags.get(\"dk\"):\n                    virt_vis[name] = vn2\n                    continue\n\n                u_has = vn2.uaxs.get(uname) or [False] * 9\n                for pset in permsets:\n                    ok = True\n                    for req, zb in zip(pset, u_has):\n                        if req and not zb:\n                            ok = False\n                            break\n                    if ok:\n                        virt_vis[name] = vn2\n                        break\n\n        if \".hist\" in abspath:\n            p = abspath.replace(\"\\\\\", \"/\") if WINDOWS else abspath\n            if p.endswith(\"/.hist\"):\n                real = [x for x in real if not x[0].startswith(\"up2k.\")]\n            elif \"/.hist/th/\" in p:\n                real = [x for x in real if not x[0].endswith(\"dir.txt\")]\n\n        return abspath, real, virt_vis\n\n    def walk(\n        self,\n        rel: str,\n        rem: str,\n        seen: list[str],\n        uname: str,\n        permsets: list[list[bool]],\n        wantdots: int,\n        scandir: bool,\n        lstat: bool,\n        subvols: bool = True,\n    ) -> Generator[\n        tuple[\n            \"VFS\",\n            str,\n            str,\n            str,\n            list[tuple[str, os.stat_result]],\n            list[tuple[str, os.stat_result]],\n            dict[str, \"VFS\"],\n        ],\n        None,\n        None,\n    ]:\n        \"\"\"\n        recursively yields from ./rem;\n        rel is a unix-style user-defined vpath (not vfs-related)\n\n        NOTE: don't invoke this function from a dbv; subvols are only\n          descended into if rem is blank due to the _ls `if not rem:`\n          which intention is to prevent unintended access to subvols\n        \"\"\"\n\n        fsroot, vfs_ls, vfs_virt = self.ls(rem, uname, scandir, permsets, lstat=lstat)\n        dbv, vrem = self.get_dbv(rem)\n\n        if (\n            seen\n            and (not fsroot.startswith(seen[-1]) or fsroot == seen[-1])\n            and fsroot in seen\n        ):\n            if self.log:\n                t = \"bailing from symlink loop,\\n  prev: %r\\n  curr: %r\\n  from: %r / %r\"\n                self.log(\"vfs.walk\", t % (seen[-1], fsroot, self.vpath, rem), 3)\n            return\n\n        if \"xdev\" in self.flags or \"xvol\" in self.flags:\n            rm1 = []\n            for le in vfs_ls:\n                ap = absreal(os.path.join(fsroot, le[0]))\n                vn2 = self.chk_ap(ap)\n                if not vn2 or not vn2.get(\"\", uname, True, False):\n                    rm1.append(le)\n            _ = [vfs_ls.remove(x) for x in rm1]  # type: ignore\n\n        dots_ok = wantdots and (wantdots == 2 or uname in dbv.axs.udot)\n        if not dots_ok:\n            vfs_ls = [x for x in vfs_ls if \"/.\" not in \"/\" + x[0]]\n\n        seen = seen[:] + [fsroot]\n        rfiles = [x for x in vfs_ls if not stat.S_ISDIR(x[1].st_mode)]\n        rdirs = [x for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]\n        # if lstat: ignore folder symlinks since copyparty will never make those\n        #            (and we definitely don't want to descend into them)\n\n        rfiles.sort()\n        rdirs.sort()\n\n        yield dbv, vrem, rel, fsroot, rfiles, rdirs, vfs_virt\n\n        for rdir, _ in rdirs:\n            if not dots_ok and rdir.startswith(\".\"):\n                continue\n\n            wrel = (rel + \"/\" + rdir).lstrip(\"/\")\n            wrem = (rem + \"/\" + rdir).lstrip(\"/\")\n            for x in self.walk(\n                wrel, wrem, seen, uname, permsets, wantdots, scandir, lstat, subvols\n            ):\n                yield x\n\n        if not subvols:\n            return\n\n        for n, vfs in sorted(vfs_virt.items()):\n            if not dots_ok and n.startswith(\".\"):\n                continue\n\n            wrel = (rel + \"/\" + n).lstrip(\"/\")\n            for x in vfs.walk(\n                wrel, \"\", seen, uname, permsets, wantdots, scandir, lstat\n            ):\n                yield x\n\n    def zipgen(\n        self,\n        vpath: str,\n        vrem: str,\n        flt: set[str],\n        uname: str,\n        dirs: bool,\n        dots: int,\n        scandir: bool,\n        wrap: bool = True,\n    ) -> Generator[dict[str, Any], None, None]:\n\n        # if multiselect: add all items to archive root\n        # if single folder: the folder itself is the top-level item\n        folder = \"\" if flt or not wrap else (vpath.split(\"/\")[-1].lstrip(\".\") or \"top\")\n\n        g = self.walk(folder, vrem, [], uname, [[True, False]], dots, scandir, False)\n        for _, _, vpath, apath, files, rd, vd in g:\n            if flt:\n                files = [x for x in files if x[0] in flt]\n\n                rm1 = [x for x in rd if x[0] not in flt]\n                _ = [rd.remove(x) for x in rm1]  # type: ignore\n\n                rm2 = [x for x in vd.keys() if x not in flt]\n                _ = [vd.pop(x) for x in rm2]\n\n                flt = set()\n\n            # print(repr([vpath, apath, [x[0] for x in files]]))\n            fnames = [n[0] for n in files]\n            vpaths = [vpath + \"/\" + n for n in fnames] if vpath else fnames\n            apaths = [os.path.join(apath, n) for n in fnames]\n            ret = list(zip(vpaths, apaths, files))\n\n            for f in [{\"vp\": v, \"ap\": a, \"st\": n[1]} for v, a, n in ret]:\n                yield f\n\n            if not dirs:\n                continue\n\n            ts = int(time.time())\n            st = os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, ts, ts, ts))\n            dnames = [n[0] for n in rd]\n            dstats = [n[1] for n in rd]\n            dnames += list(vd.keys())\n            dstats += [st] * len(vd)\n            vpaths = [vpath + \"/\" + n for n in dnames] if vpath else dnames\n            apaths = [os.path.join(apath, n) for n in dnames]\n            ret2 = list(zip(vpaths, apaths, dstats))\n            for d in [{\"vp\": v, \"ap\": a, \"st\": n} for v, a, n in ret2]:\n                yield d\n\n    def chk_ap(self, ap: str, st: Optional[os.stat_result] = None) -> Optional[\"VFS\"]:\n        aps = ap + os.sep\n        if \"xdev\" in self.flags and not ANYWIN:\n            if not st:\n                ap2 = ap.replace(\"\\\\\", \"/\") if ANYWIN else ap\n                while ap2:\n                    try:\n                        st = bos.stat(ap2)\n                        break\n                    except:\n                        if \"/\" not in ap2:\n                            raise\n                        ap2 = ap2.rsplit(\"/\", 1)[0]\n                assert st\n\n            vdev = self.dev\n            if not vdev:\n                vdev = self.dev = bos.stat(self.realpath).st_dev\n\n            if vdev != st.st_dev:\n                if self.log:\n                    t = \"xdev: %s[%r] => %s[%r]\"\n                    self.log(\"vfs\", t % (vdev, self.realpath, st.st_dev, ap), 3)\n\n                return None\n\n        if \"xvol\" in self.flags:\n            self_ap = self.realpath + os.sep\n            if aps.startswith(self_ap):\n                vp = aps[len(self_ap) :]\n                if ANYWIN:\n                    vp = vp.replace(os.sep, \"/\")\n                vn2, _ = self._find(vp)\n                if self == vn2:\n                    return self\n\n            all_aps = self.shr_all_aps or self.root.all_aps\n\n            for vap, vns in all_aps:\n                if aps.startswith(vap):\n                    return self if self in vns else vns[0]\n\n            if self.log:\n                self.log(\"vfs\", \"xvol: %r\" % (ap,), 3)\n\n            return None\n\n        return self\n\n    def check_landmarks(self) -> bool:\n        if self.dbv:\n            return True\n\n        vps = self.flags.get(\"landmark\") or []\n        if not vps:\n            return True\n\n        failed = \"\"\n        for vp in vps:\n            if \"^=\" in vp:\n                vp, zs = vp.split(\"^=\", 1)\n                expect = zs.encode(\"utf-8\")\n            else:\n                expect = b\"\"\n\n            if self.log:\n                t = \"checking [/%s] landmark [%s]\"\n                self.log(\"vfs\", t % (self.vpath, vp), 6)\n\n            ap = \"?\"\n            try:\n                ap = self.canonical(vp)\n                with open(ap, \"rb\") as f:\n                    buf = f.read(4096)\n                    if not buf.startswith(expect):\n                        t = \"file [%s] does not start with the expected bytes %s\"\n                        failed = t % (ap, expect)\n                        break\n            except Exception as ex:\n                t = \"%r while trying to read [%s] => [%s]\"\n                failed = t % (ex, vp, ap)\n                break\n\n        if not failed:\n            return True\n\n        if self.log:\n            t = \"WARNING: landmark verification failed; %s; will now disable up2k database for volume [/%s]\"\n            self.log(\"vfs\", t % (failed, self.vpath), 3)\n\n        for rm in \"e2d e2t e2v\".split():\n            self.flags = {k: v for k, v in self.flags.items() if not k.startswith(rm)}\n        self.flags[\"d2d\"] = True\n        self.flags[\"d2t\"] = True\n        return False\n\n\nif WINDOWS:\n    re_vol = re.compile(r\"^([a-zA-Z]:[\\\\/][^:]*|[^:]*):([^:]*):(.*)$\")\nelse:\n    re_vol = re.compile(r\"^([^:]*):([^:]*):(.*)$\")\n\n\nclass AuthSrv(object):\n    \"\"\"verifies users against given paths\"\"\"\n\n    def __init__(\n        self,\n        args: argparse.Namespace,\n        log_func: Optional[\"RootLogger\"],\n        warn_anonwrite: bool = True,\n        dargs: Optional[argparse.Namespace] = None,\n    ) -> None:\n        self.ah = PWHash(args)\n        self.args = args\n        self.dargs = dargs or args\n        self.log_func = log_func\n        self.warn_anonwrite = warn_anonwrite\n        self.line_ctr = 0\n        self.indent = \"\"\n        self.is_lxc = args.c == [\"/z/initcfg\"]\n\n        oh = \"X-Content-Type-Options: nosniff\\r\\n\"\n        if self.args.http_vary:\n            oh += \"Vary: %s\\r\\n\" % (self.args.http_vary,)\n        self._vf0b = {\n            \"oh_g\": oh + \"\\r\\n\",\n            \"oh_f\": oh + \"\\r\\n\",\n            \"cachectl\": self.args.cachectl,\n            \"tcolor\": self.args.tcolor,\n            \"du_iwho\": self.args.du_iwho,\n            \"shr_who\": self.args.shr_who if self.args.shr else \"no\",\n            \"emb_all\": FN_EMB,\n            \"ls_q_m\": (\"\", \"\"),\n        }\n        self._vf0 = self._vf0b.copy()\n        self._vf0[\"d2d\"] = True\n\n        # fwd-decl\n        self.vfs = VFS(log_func, \"\", \"\", \"\", AXS(), {})\n        self.acct: dict[str, str] = {}  # uname->pw\n        self.iacct: dict[str, str] = {}  # pw->uname\n        self.ases: dict[str, str] = {}  # uname->session\n        self.sesa: dict[str, str] = {}  # session->uname\n        self.defpw: dict[str, str] = {}\n        self.grps: dict[str, list[str]] = {}\n        self.re_pwd: Optional[re.Pattern] = None\n        self.cfg_files_loaded: list[str] = []\n        self.badcfg1 = False\n\n        # all volumes observed since last restart\n        self.idp_vols: dict[str, str] = {}  # vpath->abspath\n\n        # all users/groups observed since last restart\n        self.idp_accs: dict[str, list[str]] = {}  # username->groupnames\n        self.idp_usr_gh: dict[str, str] = {}  # username->group-header-value (cache)\n\n        self.hid_cache: dict[str, str] = {}\n        self.mutex = threading.Lock()\n        self.reload()\n\n    def log(self, msg: str, c: Union[int, str] = 0) -> None:\n        if self.log_func:\n            self.log_func(\"auth\", msg, c)\n\n    def laggy_iter(self, iterable: Iterable[Any]) -> Generator[Any, None, None]:\n        \"\"\"returns [value,isFinalValue]\"\"\"\n        it = iter(iterable)\n        prev = next(it)\n        for x in it:\n            yield prev, False\n            prev = x\n\n        yield prev, True\n\n    def vf0(self):\n        return self._vf0.copy()\n\n    def vf0b(self):\n        return self._vf0b.copy()\n\n    def idp_checkin(\n        self, broker: Optional[\"BrokerCli\"], uname: str, gname: str\n    ) -> bool:\n        if self.idp_usr_gh.get(uname) == gname:\n            return False\n\n        gnames = [x.strip() for x in self.args.idp_gsep.split(gname)]\n        gnames.sort()\n\n        with self.mutex:\n            self.idp_usr_gh[uname] = gname\n            if self.idp_accs.get(uname) == gnames:\n                return False\n\n            self.idp_accs[uname] = gnames\n            try:\n                self._update_idp_db(uname, gname)\n            except:\n                self.log(\"failed to update the --idp-db:\\n%s\" % (min_ex(),), 3)\n\n            t = \"reinitializing due to new user from IdP: [%r:%r]\"\n            self.log(t % (uname, gnames), 3)\n\n            if not broker:\n                # only true for tests\n                self._reload()\n                return True\n\n        broker.ask(\"reload\", False, True).get()\n        return True\n\n    def _update_idp_db(self, uname: str, gname: str) -> None:\n        if not self.args.idp_store:\n            return\n\n        assert sqlite3  # type: ignore  # !rm\n\n        db = sqlite3.connect(self.args.idp_db)\n        cur = db.cursor()\n\n        cur.execute(\"delete from us where un = ?\", (uname,))\n        cur.execute(\"insert into us values (?,?)\", (uname, gname))\n\n        db.commit()\n        cur.close()\n        db.close()\n\n    def _map_volume_idp(\n        self,\n        src: str,\n        dst: str,\n        mount: dict[str, tuple[str, str]],\n        daxs: dict[str, AXS],\n        mflags: dict[str, dict[str, Any]],\n        un_gns: dict[str, list[str]],\n    ) -> list[tuple[str, str, str, str]]:\n        ret: list[tuple[str, str, str, str]] = []\n        visited = set()\n        src0 = src  # abspath\n        dst0 = dst  # vpath\n\n        zsl = []\n        for ptn, sigil in ((PTN_U_ANY, \"${u}\"), (PTN_G_ANY, \"${g}\")):\n            if bool(ptn.search(src)) != bool(ptn.search(dst)):\n                zsl.append(sigil)\n        if zsl:\n            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]\"\n            t = \"\\n\".join([t % (x, src, dst) for x in zsl])\n            self.log(t, 1)\n            raise Exception(t)\n\n        un_gn = [(un, gn) for un, gns in un_gns.items() for gn in gns]\n        if not un_gn:\n            # ensure volume creation if there's no users\n            un_gn = [(\"\", \"\")]\n\n        for un, gn in un_gn:\n            rejected = False\n            for ptn in [PTN_U_GRP, PTN_G_GRP]:\n                m = ptn.search(dst0)\n                if not m:\n                    continue\n                zs = m.group(1)\n                zs = zs.replace(\",%+\", \"\\n%+\")\n                zs = zs.replace(\",%-\", \"\\n%-\")\n                for rule in zs.split(\"\\n\"):\n                    gnc = rule[2:]\n                    if ptn == PTN_U_GRP:\n                        # is user member of group?\n                        hit = gnc in (un_gns.get(un) or [])\n                    else:\n                        # is it this specific group?\n                        hit = gn == gnc\n\n                    if rule.startswith(\"%+\") != hit:\n                        rejected = True\n            if rejected:\n                continue\n\n            if gn == self.args.grp_all:\n                gn = \"\"\n\n            # if ap/vp has a user/group placeholder, make sure to keep\n            # track so the same user/group is mapped when setting perms;\n            # otherwise clear un/gn to indicate it's a regular volume\n\n            src1 = src0.replace(\"${u}\", un or \"\\n\")\n            dst1 = dst0.replace(\"${u}\", un or \"\\n\")\n            src1 = PTN_U_GRP.sub(un or \"\\n\", src1)\n            dst1 = PTN_U_GRP.sub(un or \"\\n\", dst1)\n            if src0 == src1 and dst0 == dst1:\n                un = \"\"\n\n            src = src1.replace(\"${g}\", gn or \"\\n\")\n            dst = dst1.replace(\"${g}\", gn or \"\\n\")\n            src = PTN_G_GRP.sub(gn or \"\\n\", src)\n            dst = PTN_G_GRP.sub(gn or \"\\n\", dst)\n            if src == src1 and dst == dst1:\n                gn = \"\"\n\n            if \"\\n\" in (src + dst):\n                continue\n\n            label = \"%s\\n%s\" % (src, dst)\n            if label in visited:\n                continue\n            visited.add(label)\n\n            src, dst = self._map_volume(src, dst, dst0, mount, daxs, mflags)\n            if src:\n                ret.append((src, dst, un, gn))\n                if un or gn:\n                    self.idp_vols[dst] = src\n\n        return ret\n\n    def _map_volume(\n        self,\n        src: str,\n        dst: str,\n        dst0: str,\n        mount: dict[str, tuple[str, str]],\n        daxs: dict[str, AXS],\n        mflags: dict[str, dict[str, Any]],\n    ) -> tuple[str, str]:\n        src = os.path.expandvars(os.path.expanduser(src))\n        src = absreal(src)\n        dst = dst.strip(\"/\")\n\n        if dst in mount:\n            t = \"multiple filesystem-paths mounted at [/{}]:\\n  [{}]\\n  [{}]\"\n            self.log(t.format(dst, mount[dst][0], src), c=1)\n            raise Exception(BAD_CFG)\n\n        if src in mount.values():\n            t = \"filesystem-path [{}] mounted in multiple locations:\"\n            t = t.format(src)\n            for v in [k for k, v in mount.items() if v[0] == src] + [dst]:\n                t += \"\\n  /{}\".format(v)\n\n            self.log(t, c=3)\n            raise Exception(BAD_CFG)\n\n        if not bos.path.exists(src):\n            self.log(\"warning: filesystem-path did not exist: %r\" % (src,), 3)\n\n        vf = {}\n        if dst.startswith(\".\") or \"/.\" in dst:\n            vf[\"unlistcr\"] = True\n            vf[\"unlistcw\"] = True\n\n        mount[dst] = (src, dst0)\n        daxs[dst] = AXS()\n        mflags[dst] = vf\n        return (src, dst)\n\n    def _e(self, desc: Optional[str] = None) -> None:\n        if not self.args.vc or not self.line_ctr:\n            return\n\n        if not desc and not self.indent:\n            self.log(\"\")\n            return\n\n        desc = desc or \"\"\n        desc = desc.replace(\"[\", \"[\\033[0m\").replace(\"]\", \"\\033[90m]\")\n        self.log(\" >>> {}{}\".format(self.indent, desc), \"90\")\n\n    def _l(self, ln: str, c: int, desc: str) -> None:\n        if not self.args.vc or not self.line_ctr:\n            return\n\n        if c < 10:\n            c += 30\n\n        t = \"\\033[97m{:4} \\033[{}m{}{}\"\n        if desc:\n            t += \"  \\033[0;90m# {}\\033[0m\"\n            desc = desc.replace(\"[\", \"[\\033[0m\").replace(\"]\", \"\\033[90m]\")\n\n        self.log(t.format(self.line_ctr, c, self.indent, ln, desc))\n\n    def _all_un_gn(\n        self,\n        acct: dict[str, str],\n        grps: dict[str, list[str]],\n    ) -> dict[str, list[str]]:\n        \"\"\"\n        generate list of all confirmed pairs of username/groupname seen since last restart;\n        in case of conflicting group memberships then it is selected as follows:\n         * any non-zero value from IdP group header\n         * otherwise take --grps / [groups]\n        \"\"\"\n        self.load_idp_db(bool(self.idp_accs))\n        ret = {un: gns[:] for un, gns in self.idp_accs.items()}\n        ret.update({zs: [\"\"] for zs in acct if zs not in ret})\n        grps[self.args.grp_all] = list(ret.keys())\n        for gn, uns in grps.items():\n            for un in uns:\n                try:\n                    ret[un].append(gn)\n                except:\n                    ret[un] = [gn]\n\n        return ret\n\n    def _parse_config_file(\n        self,\n        fp: str,\n        cfg_lines: list[str],\n        acct: dict[str, str],\n        grps: dict[str, list[str]],\n        daxs: dict[str, AXS],\n        mflags: dict[str, dict[str, Any]],\n        mount: dict[str, tuple[str, str]],\n    ) -> None:\n        self.line_ctr = 0\n\n        expand_config_file(self.log, cfg_lines, fp, \"\")\n        if self.args.vc:\n            lns = [\"{:4}: {}\".format(n, s) for n, s in enumerate(cfg_lines, 1)]\n            self.log(\"expanded config file (unprocessed):\\n\" + \"\\n\".join(lns))\n\n        cfg_lines = upgrade_cfg_fmt(self.log, self.args, cfg_lines, fp)\n\n        # due to IdP, volumes must be parsed after users and groups;\n        # do volumes in a 2nd pass to allow arbitrary order in config files\n        for npass in range(1, 3):\n            if self.args.vc:\n                self.log(\"parsing config files; pass %d/%d\" % (npass, 2))\n            self._parse_config_file_2(cfg_lines, acct, grps, daxs, mflags, mount, npass)\n\n    def _parse_config_file_2(\n        self,\n        cfg_lines: list[str],\n        acct: dict[str, str],\n        grps: dict[str, list[str]],\n        daxs: dict[str, AXS],\n        mflags: dict[str, dict[str, Any]],\n        mount: dict[str, tuple[str, str]],\n        npass: int,\n    ) -> None:\n        self.line_ctr = 0\n        all_un_gn = self._all_un_gn(acct, grps)\n\n        cat = \"\"\n        catg = \"[global]\"\n        cata = \"[accounts]\"\n        catgrp = \"[groups]\"\n        catx = \"accs:\"\n        catf = \"flags:\"\n        ap: Optional[str] = None\n        vp: Optional[str] = None\n        vols: list[tuple[str, str, str, str]] = []\n        for ln in cfg_lines:\n            self.line_ctr += 1\n            ln = ln.split(\"  #\")[0].strip()\n            if not ln.split(\"#\")[0].strip():\n                continue\n\n            if re.match(r\"^\\[.*\\]:$\", ln):\n                ln = ln[:-1]\n\n            subsection = ln in (catx, catf)\n            if ln.startswith(\"[\") or subsection:\n                self._e()\n                if npass > 1 and ap is None and vp is not None:\n                    t = \"the first line after [/{}] must be a filesystem path to share on that volume\"\n                    raise Exception(t.format(vp))\n\n                cat = ln\n                if not subsection:\n                    ap = vp = None\n                    self.indent = \"\"\n                else:\n                    self.indent = \"  \"\n\n                if ln == catg:\n                    t = \"begin commandline-arguments (anything from --help; dashes are optional)\"\n                    self._l(ln, 6, t)\n                elif ln == cata:\n                    self._l(ln, 5, \"begin user-accounts section\")\n                elif ln == catgrp:\n                    self._l(ln, 5, \"begin user-groups section\")\n                elif ln.startswith(\"[/\"):\n                    vp = ln[1:-1].strip(\"/\")\n                    self._l(ln, 2, \"define volume at URL [/{}]\".format(vp))\n                elif subsection:\n                    if ln == catx:\n                        self._l(ln, 5, \"volume access config:\")\n                    else:\n                        t = \"volume-specific config (anything from --help-flags)\"\n                        self._l(ln, 6, t)\n                else:\n                    raise Exception(\"invalid section header\" + SBADCFG)\n\n                self.indent = \"    \" if subsection else \"  \"\n                continue\n\n            if cat == catg:\n                self._l(ln, 6, \"\")\n                zt = split_cfg_ln(ln)\n                for zs, za in zt.items():\n                    zs = zs.lstrip(\"-\")\n                    if \"=\" in zs:\n                        t = \"WARNING: found an option named [%s] in your [global] config; did you mean to say [%s: %s] instead?\"\n                        zs1, zs2 = zs.split(\"=\", 1)\n                        self.log(t % (zs, zs1, zs2), 1)\n                    if za is True:\n                        self._e(\"└─argument [{}]\".format(zs))\n                    else:\n                        self._e(\"└─argument [{}] with value [{}]\".format(zs, za))\n                continue\n\n            if cat == cata:\n                try:\n                    u, p = [zs.strip() for zs in ln.split(\":\", 1)]\n                    if \"=\" in u and not p:\n                        t = \"WARNING: found username [%s] in your [accounts] config; did you mean to say [%s: %s] instead?\"\n                        zs1, zs2 = u.split(\"=\", 1)\n                        self.log(t % (u, zs1, zs2), 3)\n                    self._l(ln, 5, \"account [{}], password [{}]\".format(u, p))\n                    acct[u] = p\n                except:\n                    t = 'lines inside the [accounts] section must be \"username: password\"'\n                    raise Exception(t + SBADCFG)\n                continue\n\n            if cat == catgrp:\n                try:\n                    gn, zs1 = [zs.strip() for zs in ln.split(\":\", 1)]\n                    uns = [zs.strip() for zs in zs1.split(\",\")]\n                    t = \"group [%s] = \" % (gn,)\n                    t += \", \".join(\"user [%s]\" % (x,) for x in uns)\n                    self._l(ln, 5, t)\n                    if gn in grps:\n                        grps[gn].extend(uns)\n                    else:\n                        grps[gn] = uns\n                except:\n                    t = 'lines inside the [groups] section must be \"groupname: user1, user2, user...\"'\n                    raise Exception(t + SBADCFG)\n                continue\n\n            if vp is not None and ap is None:\n                if npass != 2:\n                    continue\n\n                ap = ln\n                self._l(ln, 2, \"bound to filesystem-path [{}]\".format(ap))\n                vols = self._map_volume_idp(ap, vp, mount, daxs, mflags, all_un_gn)\n                if not vols:\n                    ap = vp = None\n                    self._l(ln, 2, \"└─no users/groups known; was not mapped\")\n                elif len(vols) > 1:\n                    for vol in vols:\n                        self._l(ln, 2, \"└─mapping: [%s] => [%s]\" % (vol[1], vol[0]))\n                continue\n\n            if cat == catx:\n                if npass != 2 or not ap:\n                    # not stage2, or unmapped ${u}/${g}\n                    continue\n\n                err = \"\"\n                try:\n                    self._l(ln, 5, \"volume access config:\")\n                    sk, sv = ln.split(\":\")\n                    if re.sub(\"[rwmdgGhaA.]\", \"\", sk) or not sk:\n                        err = \"invalid accs permissions list; \"\n                        raise Exception(err)\n                    if \" \" in re.sub(\", *\", \"\", sv).strip():\n                        err = \"list of users is not comma-separated; \"\n                        raise Exception(err)\n                    sv = sv.replace(\" \", \"\")\n                    self._read_vol_str_idp(sk, sv, vols, all_un_gn, daxs, mflags)\n                    continue\n                except CfgEx:\n                    raise\n                except:\n                    err += \"accs entries must be 'rwmdgGhaA.: user1, user2, ...'\"\n                    raise CfgEx(err + SBADCFG)\n\n            if cat == catf:\n                if npass != 2 or not ap:\n                    # not stage2, or unmapped ${u}/${g}\n                    continue\n\n                err = \"\"\n                try:\n                    self._l(ln, 6, \"volume-specific config:\")\n                    zd = split_cfg_ln(ln)\n                    fstr = \"\"\n                    for sk, sv in zd.items():\n                        if \"=\" in sk:\n                            t = \"WARNING: found a volflag named [%s] in your config; did you mean to say [%s: %s] instead?\"\n                            zs1, zs2 = sk.split(\"=\", 1)\n                            self.log(t % (sk, zs1, zs2), 3)\n                        bad = re.sub(r\"[a-z0-9_-]\", \"\", sk).lstrip(\"-\")\n                        if bad:\n                            err = \"bad characters [{}] in volflag name [{}]; \"\n                            err = err.format(bad, sk)\n                            raise Exception(err + SBADCFG)\n                        if sv is True:\n                            fstr += \",\" + sk\n                        else:\n                            fstr += \",{}={}\".format(sk, sv)\n                            assert vp is not None\n                            self._read_vol_str_idp(\n                                \"c\", fstr[1:], vols, all_un_gn, daxs, mflags\n                            )\n                            fstr = \"\"\n                    if fstr:\n                        self._read_vol_str_idp(\n                            \"c\", fstr[1:], vols, all_un_gn, daxs, mflags\n                        )\n                    continue\n                except:\n                    err += \"flags entries (volflags) must be one of the following:\\n  'flag1, flag2, ...'\\n  'key: value'\\n  'flag1, flag2, key: value'\"\n                    raise Exception(err + SBADCFG)\n\n            raise Exception(\"unprocessable line in config\" + SBADCFG)\n\n        self._e()\n        self.line_ctr = 0\n\n    def _read_vol_str_idp(\n        self,\n        lvl: str,\n        uname: str,\n        vols: list[tuple[str, str, str, str]],\n        un_gns: dict[str, list[str]],\n        axs: dict[str, AXS],\n        flags: dict[str, dict[str, Any]],\n    ) -> None:\n        if lvl.strip(\"crwmdgGhaA.\"):\n            t = \"%s,%s\" % (lvl, uname) if uname else lvl\n            raise CfgEx(\"invalid config value (volume or volflag): %s\" % (t,))\n\n        if lvl == \"c\":\n            # here, 'uname' is not a username; it is a volflag name... sorry\n            cval: Union[bool, str] = True\n            try:\n                # volflag with arguments, possibly with a preceding list of bools\n                uname, cval = uname.split(\"=\", 1)\n            except:\n                # just one or more bools\n                pass\n\n            while \",\" in uname:\n                # one or more bools before the final flag; eat them\n                n1, uname = uname.split(\",\", 1)\n                for _, vp, _, _ in vols:\n                    self._read_volflag(vp, flags[vp], n1, True, False)\n\n            for _, vp, _, _ in vols:\n                self._read_volflag(vp, flags[vp], uname, cval, False)\n\n            return\n\n        if uname == \"\":\n            uname = \"*\"\n\n        unames = []\n        for un in uname.replace(\",\", \" \").strip().split():\n            if un.startswith(\"@\"):\n                grp = un[1:]\n                uns = [x[0] for x in un_gns.items() if grp in x[1]]\n                if grp == \"${g}\":\n                    unames.append(un)\n                elif not uns and not self.args.idp_h_grp and grp != self.args.grp_all:\n                    t = \"group [%s] must be defined with --grp argument (or in a [groups] config section)\"\n                    raise CfgEx(t % (grp,))\n\n                unames.extend(uns)\n            else:\n                unames.append(un)\n\n        # unames may still contain ${u} and ${g} so now expand those;\n        un_gn = [(un, gn) for un, gns in un_gns.items() for gn in gns]\n\n        for src, dst, vu, vg in vols:\n            unames2 = set(unames)\n\n            if \"${u}\" in unames:\n                if not vu:\n                    t = \"cannot use ${u} in accs of volume [%s] because the volume url does not contain ${u}\"\n                    raise CfgEx(t % (src,))\n                unames2.add(vu)\n\n            if \"@${g}\" in unames:\n                if not vg:\n                    t = \"cannot use @${g} in accs of volume [%s] because the volume url does not contain @${g}\"\n                    raise CfgEx(t % (src,))\n                unames2.update([un for un, gn in un_gn if gn == vg])\n\n            if \"${g}\" in unames:\n                t = 'the accs of volume [%s] contains \"${g}\" but the only supported way of specifying that is \"@${g}\"'\n                raise CfgEx(t % (src,))\n\n            unames2.discard(\"${u}\")\n            unames2.discard(\"@${g}\")\n\n            self._read_vol_str(lvl, list(unames2), axs[dst])\n\n    def _read_vol_str(self, lvl: str, unames: list[str], axs: AXS) -> None:\n        junkset = set()\n        for un in unames:\n            for alias, mapping in [\n                (\"h\", \"gh\"),\n                (\"G\", \"gG\"),\n                (\"A\", \"rwmda.A\"),\n            ]:\n                expanded = \"\"\n                for ch in mapping:\n                    if ch not in lvl:\n                        expanded += ch\n                    lvl = lvl.replace(alias, expanded + alias)\n\n            for ch, al in [\n                (\"r\", axs.uread),\n                (\"w\", axs.uwrite),\n                (\"m\", axs.umove),\n                (\"d\", axs.udel),\n                (\".\", axs.udot),\n                (\"a\", axs.uadmin),\n                (\"A\", junkset),\n                (\"g\", axs.uget),\n                (\"G\", axs.upget),\n                (\"h\", axs.uhtml),\n            ]:\n                if ch in lvl:\n                    if un == \"*\":\n                        t = \"└─add permission [{0}] for [everyone] -- {2}\"\n                    else:\n                        t = \"└─add permission [{0}] for user [{1}] -- {2}\"\n\n                    desc = permdescs.get(ch, \"?\")\n                    self._e(t.format(ch, un, desc))\n                    al.add(un)\n\n    def _read_volflag(\n        self,\n        vpath: str,\n        flags: dict[str, Any],\n        name: str,\n        value: Union[str, bool, list[str]],\n        is_list: bool,\n    ) -> None:\n        if name not in flagdescs:\n            name = name.lower()\n\n            # volflags are snake_case, but a leading dash is the removal operator\n            stripped = name.lstrip(\"-\")\n            zi = len(name) - len(stripped)\n            if zi > 1:\n                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]\"\n                self.log(t % (vpath, name, stripped), 3)\n                name = stripped\n                zi = 0\n\n            if stripped not in flagdescs and \"-\" in stripped:\n                name = (\"-\" * zi) + stripped.replace(\"-\", \"_\")\n\n        desc = flagdescs.get(name.lstrip(\"-\"), \"?\").replace(\"\\n\", \" \")\n\n        if not name:\n            self._e(\"└─unreadable-line\")\n            t = \"WARNING: the config for volume [/%s] indicated that a volflag was to be defined, but the volflag name was blank\"\n            self.log(t % (vpath,), 3)\n            return\n\n        if re.match(\"^-[^-]+$\", name):\n            t = \"└─unset volflag [{}]  ({})\"\n            self._e(t.format(name[1:], desc))\n            flags[name] = True\n            return\n\n        zs = \"ext_th landmark mtp on403 on404 xbu xau xiu xbc xac xbr xar xbd xad xm xban\"\n        if name not in zs.split():\n            if value is True:\n                t = \"└─add volflag [{}] = {}  ({})\"\n            else:\n                t = \"└─add volflag [{}] = [{}]  ({})\"\n            self._e(t.format(name, value, desc))\n            flags[name] = value\n            return\n\n        vals = flags.get(name, [])\n        if not value:\n            return\n        elif is_list:\n            vals += value\n        else:\n            vals += [value]\n\n        flags[name] = vals\n        self._e(\"volflag [{}] += {}  ({})\".format(name, vals, desc))\n\n    def reload(self, verbosity: int = 9) -> None:\n        \"\"\"\n        construct a flat list of mountpoints and usernames\n        first from the commandline arguments\n        then supplementing with config files\n        before finally building the VFS\n        \"\"\"\n        with self.mutex:\n            self._reload(verbosity)\n\n    def _reload(self, verbosity: int = 9) -> None:\n        acct: dict[str, str] = {}  # username:password\n        grps: dict[str, list[str]] = {}  # groupname:usernames\n        daxs: dict[str, AXS] = {}\n        mflags: dict[str, dict[str, Any]] = {}  # vpath:flags\n        mount: dict[str, tuple[str, str]] = {}  # dst:src (vp:(ap,vp0))\n        cfg_files_loaded: list[str] = []\n\n        self.idp_vols = {}  # yolo\n        self.badcfg1 = False\n\n        if self.args.a:\n            # list of username:password\n            for x in self.args.a:\n                try:\n                    u, p = x.split(\":\", 1)\n                    acct[u] = p\n                except:\n                    t = '\\n  invalid value \"{}\" for argument -a, must be username:password'\n                    raise Exception(t.format(x))\n\n        if self.args.grp:\n            # list of groupname:username,username,...\n            for x in self.args.grp:\n                try:\n                    # accept both = and : as separator between groupname and usernames,\n                    # accept both , and : as separators between usernames\n                    zs1, zs2 = x.replace(\"=\", \":\").split(\":\", 1)\n                    grps[zs1] = zs2.replace(\":\", \",\").split(\",\")\n                    grps[zs1] = [x.strip() for x in grps[zs1]]\n                except:\n                    t = '\\n  invalid value \"{}\" for argument --grp, must be groupname:username1,username2,...'\n                    raise Exception(t.format(x))\n\n        if self.args.v:\n            # list of src:dst:permset:permset:...\n            # permset is <rwmdgGhaA.>[,username][,username] or <c>,<flag>[=args]\n            all_un_gn = self._all_un_gn(acct, grps)\n            for v_str in self.args.v:\n                m = re_vol.match(v_str)\n                if not m:\n                    raise Exception(\"invalid -v argument: [{}]\".format(v_str))\n\n                src, dst, perms = m.groups()\n                if WINDOWS:\n                    src = uncyg(src)\n\n                vols = self._map_volume_idp(src, dst, mount, daxs, mflags, all_un_gn)\n\n                for x in perms.split(\":\"):\n                    lvl, uname = x.split(\",\", 1) if \",\" in x else [x, \"\"]\n                    self._read_vol_str_idp(lvl, uname, vols, all_un_gn, daxs, mflags)\n\n        if self.args.c:\n            for cfg_fn in self.args.c:\n                lns: list[str] = []\n                try:\n                    self._parse_config_file(\n                        cfg_fn, lns, acct, grps, daxs, mflags, mount\n                    )\n\n                    zs = \"#\\033[36m cfg files in \"\n                    zst = [x[len(zs) :] for x in lns if x.startswith(zs)]\n                    for zs in list(set(zst)):\n                        self.log(\"discovered config files in \" + zs, 6)\n\n                    zs = \"#\\033[36m opening cfg file\"\n                    zstt = [x.split(\" -> \") for x in lns if x.startswith(zs)]\n                    zst = [(max(0, len(x) - 2) * \" \") + \"└\" + x[-1] for x in zstt]\n                    t = \"loaded {} config files:\\n{}\"\n                    self.log(t.format(len(zst), \"\\n\".join(zst)))\n                    cfg_files_loaded = zst\n\n                except:\n                    lns = lns[: self.line_ctr]\n                    slns = [\"{:4}: {}\".format(n, s) for n, s in enumerate(lns, 1)]\n                    t = \"\\033[1;31m\\nerror @ line {}, included from {}\\033[0m\"\n                    t = t.format(self.line_ctr, cfg_fn)\n                    self.log(\"\\n{0}\\n{1}{0}\".format(t, \"\\n\".join(slns)))\n                    raise\n\n        derive_args(self.args)\n        self.setup_auth_ord()\n\n        if self.args.ipu and not self.args.have_idp_hdrs:\n            # syntax (CIDR=UNAME) is verified in load_ipu\n            zsl = [x.split(\"=\", 1)[1] for x in self.args.ipu]\n            zsl = [x for x in zsl if x and x not in acct]\n            if zsl:\n                t = \"ERROR: unknown users in ipu: %s\" % (zsl,)\n                self.log(t, 1)\n                raise Exception(t)\n\n        self.setup_pwhash(acct)\n        defpw = acct.copy()\n        self.setup_chpw(acct)\n\n        # case-insensitive; normalize\n        if WINDOWS:\n            cased = {}\n            for vp, (ap, vp0) in mount.items():\n                cased[vp] = (absreal(ap), vp0)\n\n            mount = cased\n\n        if not mount and not self.args.have_idp_hdrs:\n            # -h says our defaults are CWD at root and read/write for everyone\n            axs = AXS([\"*\"], [\"*\"], None, None)\n            ehint = \"\"\n            if self.is_lxc:\n                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\"\n                if len(cfg_files_loaded) == 1:\n                    self.log(t % (\"no config-file was provided\",), 1)\n                    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\"\n                    self.log(t, 3)\n                else:\n                    self.log(t % (\"the config does not define any volumes\",), 1)\n                axs = AXS()\n                ehint = \"; please try moving them up one level, into the parent folder:\"\n            elif self.args.c:\n                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\"\n                self.log(t, 1)\n                axs = AXS()\n                ehint = \":\"\n            if ehint:\n                try:\n                    files = os.listdir(E.cfg)\n                except:\n                    files = []\n                hits = [\n                    x\n                    for x in files\n                    if x.lower().endswith(\".conf\") and not x.startswith(\".\")\n                ]\n                if hits:\n                    t = \"Hint: Found some config files in [%s], but these were not automatically loaded because they are in the wrong place%s %s\\n\"\n                    self.log(t % (E.cfg, ehint, \", \".join(hits)), 3)\n            vfs = VFS(self.log_func, absreal(\".\"), \"\", \"\", axs, self.vf0b())\n            if not axs.uread:\n                self.badcfg1 = True\n        elif \"\" not in mount:\n            # there's volumes but no root; make root inaccessible\n            vfs = VFS(self.log_func, \"\", \"\", \"\", AXS(), self.vf0())\n\n        maxdepth = 0\n        for dst in sorted(mount.keys(), key=lambda x: (x.count(\"/\"), len(x))):\n            depth = dst.count(\"/\")\n            assert maxdepth <= depth  # nosec\n            maxdepth = depth\n            src, dst0 = mount[dst]\n\n            if dst == \"\":\n                # rootfs was mapped; fully replaces the default CWD vfs\n                vfs = VFS(self.log_func, src, dst, dst0, daxs[dst], mflags[dst])\n                continue\n\n            assert vfs  # type: ignore\n            zv = vfs.add(src, dst, dst0)\n            zv.axs = daxs[dst]\n            zv.flags = mflags[dst]\n            zv.dbv = None\n\n        assert vfs  # type: ignore\n        vfs.all_vols = {}\n        vfs.all_nodes = {}\n        vfs.all_aps = []\n        vfs.all_vps = []\n        vfs.get_all_vols(vfs.all_vols, vfs.all_nodes, vfs.all_aps, vfs.all_vps)\n        for vol in vfs.all_nodes.values():\n            vol.all_aps.sort(key=lambda x: len(x[0]), reverse=True)\n            vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)\n            vol.root = vfs\n\n        zs = \"du_iwho emb_all ls_q_m neversymlink oh_f oh_g\"\n        k_ign = set(zs.split())\n        for vol in vfs.all_vols.values():\n            unknown_flags = set()\n            for k, v in vol.flags.items():\n                ks = k.lstrip(\"-\")\n                if ks not in flagdescs and ks not in k_ign:\n                    unknown_flags.add(k)\n            if unknown_flags:\n                t = \"WARNING: the config for volume [/%s] has unrecognized volflags; will ignore: '%s'\"\n                self.log(t % (vol.vpath, \"', '\".join(unknown_flags)), 3)\n\n        enshare = self.args.shr\n        shr = enshare[1:-1]\n        shrs = enshare[1:]\n        if enshare:\n            assert sqlite3  # type: ignore  # !rm\n\n            shv = VFS(self.log_func, \"\", shr, shr, AXS(), self.vf0())\n\n            db_path = self.args.shr_db\n            db = sqlite3.connect(db_path)\n            cur = db.cursor()\n            cur2 = db.cursor()\n            now = time.time()\n            for row in cur.execute(\"select * from sh\"):\n                s_k, s_pw, s_vp, s_pr, s_nf, s_un, s_t0, s_t1 = row\n                if s_t1 and s_t1 < now:\n                    continue\n\n                if self.args.shr_v:\n                    t = \"loading %s share %r by %r => %r\"\n                    self.log(t % (s_pr, s_k, s_un, s_vp))\n\n                if s_pw:\n                    # gotta reuse the \"account\" for all shares with this pw,\n                    # so do a light scramble as this appears in the web-ui\n                    zb = hashlib.sha512(s_pw.encode(\"utf-8\")).digest()\n                    sun = \"s_%s\" % (ub64enc(zb)[4:16].decode(\"ascii\"),)\n                    acct[sun] = s_pw\n                else:\n                    sun = \"*\"\n\n                s_axs = AXS(\n                    [sun] if \"r\" in s_pr else [],\n                    [sun] if \"w\" in s_pr else [],\n                    [sun] if \"m\" in s_pr else [],\n                    [sun] if \"d\" in s_pr else [],\n                    [sun] if \"g\" in s_pr else [],\n                )\n\n                # don't know the abspath yet + wanna ensure the user\n                # still has the privs they granted, so nullmap it\n                vp = \"%s/%s\" % (shr, s_k)\n                shv.nodes[s_k] = VFS(self.log_func, \"\", vp, vp, s_axs, shv.flags.copy())\n\n            vfs.nodes[shr] = vfs.all_vols[shr] = shv\n            for vol in shv.nodes.values():\n                vfs.all_vols[vol.vpath] = vfs.all_nodes[vol.vpath] = vol\n                vol.get_dbv = vol._get_share_src\n                vol.ls = vol._ls_nope\n\n        zss = set(acct)\n        zss.update(self.idp_accs)\n        zss.discard(\"*\")\n        unames = [\"*\"] + list(sorted(zss))\n\n        for perm in \"read write move del get pget html admin dot\".split():\n            axs_key = \"u\" + perm\n            for vp, vol in vfs.all_vols.items():\n                zx = getattr(vol.axs, axs_key)\n                if \"*\" in zx and \"-@acct\" not in zx:\n                    for usr in unames:\n                        zx.add(usr)\n                for zs in list(zx):\n                    if zs.startswith(\"-\"):\n                        zx.discard(zs)\n                        zs = zs[1:]\n                        zx.discard(zs)\n                        if zs.startswith(\"@\"):\n                            zs = zs[1:]\n                            for zs in grps.get(zs) or []:\n                                zx.discard(zs)\n\n            # aread,... = dict[uname, list[volnames] or []]\n            umap: dict[str, list[str]] = {x: [] for x in unames}\n            for usr in unames:\n                for vp, vol in vfs.all_vols.items():\n                    zx = getattr(vol.axs, axs_key)\n                    if usr in zx and (not enshare or not vp.startswith(shrs)):\n                        umap[usr].append(vp)\n                umap[usr].sort()\n            setattr(vfs, \"a\" + perm, umap)\n\n        for vol in vfs.all_nodes.values():\n            za = vol.axs\n            vol.uaxs = {\n                un: (\n                    un in za.uread,\n                    un in za.uwrite,\n                    un in za.umove,\n                    un in za.udel,\n                    un in za.uget,\n                    un in za.upget,\n                    un in za.uhtml,\n                    un in za.uadmin,\n                    un in za.udot,\n                )\n                for un in unames\n            }\n\n        all_users = {}\n        missing_users = {}\n        associated_users = {}\n        for axs in daxs.values():\n            for d in [\n                axs.uread,\n                axs.uwrite,\n                axs.umove,\n                axs.udel,\n                axs.uget,\n                axs.upget,\n                axs.uhtml,\n                axs.uadmin,\n                axs.udot,\n            ]:\n                for usr in d:\n                    all_users[usr] = 1\n                    if usr != \"*\" and usr not in acct and usr not in self.idp_accs:\n                        missing_users[usr] = 1\n                    if \"*\" not in d:\n                        associated_users[usr] = 1\n\n        if missing_users:\n            zs = \", \".join(k for k in sorted(missing_users))\n            if self.args.have_idp_hdrs:\n                t = \"the following users are unknown, and assumed to come from IdP: \"\n                self.log(t + zs, c=6)\n            else:\n                t = \"you must -a the following users: \"\n                self.log(t + zs, c=1)\n                raise Exception(BAD_CFG)\n\n        if LEELOO_DALLAS in all_users:\n            raise Exception(\"sorry, reserved username: \" + LEELOO_DALLAS)\n\n        zsl = []\n        for usr in list(acct)[:]:\n            zs = acct[usr].strip()\n            if not zs:\n                zs = ub64enc(os.urandom(48)).decode(\"ascii\")\n                zsl.append(usr)\n            acct[usr] = zs\n        if zsl:\n            self.log(\"generated random passwords for users %r\" % (zsl,), 6)\n\n        seenpwds = {}\n        for usr, pwd in acct.items():\n            if pwd in seenpwds:\n                t = \"accounts [{}] and [{}] have the same password; this is not supported\"\n                self.log(t.format(seenpwds[pwd], usr), 1)\n                raise Exception(BAD_CFG)\n            seenpwds[pwd] = usr\n\n        for usr in acct:\n            if usr not in associated_users:\n                if enshare and usr.startswith(\"s_\"):\n                    continue\n                if len(vfs.all_vols) > 1:\n                    # user probably familiar enough that the verbose message is not necessary\n                    t = \"account [%s] is not mentioned in any volume definitions; see --help-accounts\"\n                    self.log(t % (usr,), 1)\n                else:\n                    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\"\n                    self.log(t % (usr, usr), 1)\n\n        dropvols = []\n        errors = False\n        for vol in vfs.all_vols.values():\n            if (\n                not vol.realpath\n                or (\n                    \"assert_root\" not in vol.flags\n                    and \"nospawn\" not in vol.flags\n                    and not self.args.vol_or_crash\n                    and not self.args.vol_nospawn\n                )\n                or bos.path.exists(vol.realpath)\n            ):\n                pass\n            elif \"assert_root\" in vol.flags or self.args.vol_or_crash:\n                t = \"ERROR: volume [/%s] root folder %r does not exist on server HDD; will now crash due to volflag 'assert_root'\"\n                self.log(t % (vol.vpath, vol.realpath), 1)\n                errors = True\n            else:\n                t = \"WARNING: volume [/%s] root folder %r does not exist on server HDD; volume will be unavailable due to volflag 'nospawn'\"\n                self.log(t % (vol.vpath, vol.realpath), 3)\n                dropvols.append(vol)\n        if errors:\n            sys.exit(1)\n        for vol in dropvols:\n            vol.axs = AXS()\n            vol.uaxs = {}\n            vfs.all_vols.pop(vol.vpath, None)\n            vfs.all_nodes.pop(vol.vpath, None)\n            for zv in vfs.all_nodes.values():\n                try:\n                    zv.all_aps.remove(vol.realpath)\n                    zv.all_vps.remove(vol.vpath)\n                    # pointless but might as well:\n                    zv.all_vols.pop(vol.vpath)\n                    zv.all_nodes.pop(vol.vpath)\n                except:\n                    pass\n                zs = next((x for x, y in zv.nodes.items() if y == vol), \"\")\n                if zs:\n                    zv.nodes.pop(zs)\n            vol.realpath = \"\"\n\n        promote = []\n        demote = []\n        for vol in vfs.all_vols.values():\n            if not vol.realpath:\n                continue\n            hid = self.hid_cache.get(vol.realpath)\n            if not hid:\n                zb = hashlib.sha512(afsenc(vol.realpath)).digest()\n                hid = base64.b32encode(zb).decode(\"ascii\").lower()\n                self.hid_cache[vol.realpath] = hid\n\n            vflag = vol.flags.get(\"hist\")\n            if vflag == \"-\":\n                pass\n            elif vflag:\n                vflag = os.path.expandvars(os.path.expanduser(vflag))\n                vol.histpath = vol.dbpath = uncyg(vflag) if WINDOWS else vflag\n            elif self.args.hist:\n                for nch in range(len(hid)):\n                    hpath = os.path.join(self.args.hist, hid[: nch + 1])\n                    bos.makedirs(hpath)\n\n                    powner = os.path.join(hpath, \"owner.txt\")\n                    try:\n                        with open(powner, \"rb\") as f:\n                            owner = f.read().rstrip()\n                    except:\n                        owner = None\n\n                    me = afsenc(vol.realpath).rstrip()\n                    if owner not in [None, me]:\n                        continue\n\n                    if owner is None:\n                        with open(powner, \"wb\") as f:\n                            f.write(me)\n\n                    vol.histpath = vol.dbpath = hpath\n                    break\n\n            vol.histpath = absreal(vol.histpath)\n\n        for vol in vfs.all_vols.values():\n            if not vol.realpath:\n                continue\n            hid = self.hid_cache[vol.realpath]\n            vflag = vol.flags.get(\"dbpath\")\n            if vflag == \"-\":\n                pass\n            elif vflag:\n                vflag = os.path.expandvars(os.path.expanduser(vflag))\n                vol.dbpath = uncyg(vflag) if WINDOWS else vflag\n            elif self.args.dbpath:\n                for nch in range(len(hid)):\n                    hpath = os.path.join(self.args.dbpath, hid[: nch + 1])\n                    bos.makedirs(hpath)\n\n                    powner = os.path.join(hpath, \"owner.txt\")\n                    try:\n                        with open(powner, \"rb\") as f:\n                            owner = f.read().rstrip()\n                    except:\n                        owner = None\n\n                    me = afsenc(vol.realpath).rstrip()\n                    if owner not in [None, me]:\n                        continue\n\n                    if owner is None:\n                        with open(powner, \"wb\") as f:\n                            f.write(me)\n\n                    vol.dbpath = hpath\n                    break\n\n            vol.dbpath = absreal(vol.dbpath)\n            if vol.dbv:\n                if bos.path.exists(os.path.join(vol.dbpath, \"up2k.db\")):\n                    promote.append(vol)\n                    vol.dbv = None\n                else:\n                    demote.append(vol)\n\n        # discard jump-vols\n        for zv in demote:\n            vfs.all_vols.pop(zv.vpath)\n\n        if promote:\n            ta = [\n                \"\\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:\"\n            ]\n            for vol in promote:\n                ta.append(\"  /%s  (%s)  (%s)\" % (vol.vpath, vol.realpath, vol.dbpath))\n\n            self.log(\"\\n\\n\".join(ta) + \"\\n\", c=3)\n\n        rhisttab = {}\n        vfs.histtab = {}\n        for zv in vfs.all_vols.values():\n            histp = zv.histpath\n            is_shr = shr and zv.vpath.split(\"/\")[0] == shr\n            if histp and not is_shr and histp in rhisttab:\n                zv2 = rhisttab[histp]\n                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]\"\n                t = t % (histp, zv2.vpath, zv2.realpath, zv.vpath, zv.realpath)\n                self.log(t, 1)\n                raise Exception(t)\n            rhisttab[histp] = zv\n            vfs.histtab[zv.realpath] = histp\n\n        rdbpaths = {}\n        vfs.dbpaths = {}\n        for zv in vfs.all_vols.values():\n            dbp = zv.dbpath\n            is_shr = shr and zv.vpath.split(\"/\")[0] == shr\n            if dbp and not is_shr and dbp in rdbpaths:\n                zv2 = rdbpaths[dbp]\n                t = \"invalid config; multiple volumes share the same dbpath (database location):\\n  dbpath: %s\\n  volume 1: /%s  [%s]\\n  volume 2: /%s  [%s]\"\n                t = t % (dbp, zv2.vpath, zv2.realpath, zv.vpath, zv.realpath)\n                self.log(t, 1)\n                raise Exception(t)\n            rdbpaths[dbp] = zv\n            vfs.dbpaths[zv.realpath] = dbp\n\n        for vol in vfs.all_vols.values():\n            use = False\n            for k in [\"zipmaxn\", \"zipmaxs\"]:\n                try:\n                    zs = vol.flags[k]\n                except:\n                    zs = getattr(self.args, k)\n                if zs in (\"\", \"0\"):\n                    vol.flags[k] = 0\n                    continue\n\n                zf = unhumanize(zs)\n                vol.flags[k + \"_v\"] = zf\n                if zf:\n                    use = True\n            if use:\n                vol.flags[\"zipmax\"] = True\n\n        for vol in vfs.all_vols.values():\n            lim = Lim(self.args, self.log_func)\n            use = False\n\n            if vol.flags.get(\"nosub\"):\n                use = True\n                lim.nosub = True\n\n            zs = vol.flags.get(\"df\") or self.args.df or \"\"\n            if zs not in (\"\", \"0\"):\n                use = True\n                try:\n                    _ = float(zs)\n                    zs = \"%sg\" % (zs,)\n                except:\n                    pass\n                lim.dfl = unhumanize(zs)\n\n            zs = vol.flags.get(\"sz\")\n            if zs:\n                use = True\n                lim.smin, lim.smax = [unhumanize(x) for x in zs.split(\"-\")]\n\n            zs = vol.flags.get(\"rotn\")\n            if zs:\n                use = True\n                lim.rotn, lim.rotl = [int(x) for x in zs.split(\",\")]\n\n            zs = vol.flags.get(\"rotf\")\n            if zs:\n                use = True\n                lim.set_rotf(zs, vol.flags.get(\"rotf_tz\") or \"UTC\")\n\n            zs = vol.flags.get(\"maxn\")\n            if zs:\n                use = True\n                lim.nmax, lim.nwin = [int(x) for x in zs.split(\",\")]\n\n            zs = vol.flags.get(\"maxb\")\n            if zs:\n                use = True\n                lim.bmax, lim.bwin = [unhumanize(x) for x in zs.split(\",\")]\n\n            zs = vol.flags.get(\"vmaxb\")\n            if zs:\n                use = True\n                lim.vbmax = unhumanize(zs)\n\n            zs = vol.flags.get(\"vmaxn\")\n            if zs:\n                use = True\n                lim.vnmax = unhumanize(zs)\n\n            if use:\n                vol.lim = lim\n\n        if self.args.no_robots:\n            for vol in vfs.all_nodes.values():\n                # volflag \"robots\" overrides global \"norobots\", allowing indexing by search engines for this vol\n                if not vol.flags.get(\"robots\"):\n                    vol.flags[\"norobots\"] = True\n\n        for vol in vfs.all_nodes.values():\n            if self.args.no_vthumb:\n                vol.flags[\"dvthumb\"] = True\n            if self.args.no_athumb:\n                vol.flags[\"dathumb\"] = True\n            if self.args.no_thumb or vol.flags.get(\"dthumb\", False):\n                vol.flags[\"dthumb\"] = True\n                vol.flags[\"dvthumb\"] = True\n                vol.flags[\"dathumb\"] = True\n                vol.flags[\"dithumb\"] = True\n\n        have_fk = False\n        for vol in vfs.all_nodes.values():\n            fk = vol.flags.get(\"fk\")\n            fka = vol.flags.get(\"fka\")\n            if fka and not fk:\n                fk = fka\n            if fk:\n                fk = 8 if fk is True else int(fk)\n                if fk > 72:\n                    t = \"max filekey-length is 72; volume /%s specified %d (anything higher than 16 is pointless btw)\"\n                    raise Exception(t % (vol.vpath, fk))\n                vol.flags[\"fk\"] = fk\n                have_fk = True\n\n            dk = vol.flags.get(\"dk\")\n            dks = vol.flags.get(\"dks\")\n            dky = vol.flags.get(\"dky\")\n            if dks is not None and dky is not None:\n                t = \"WARNING: volume /%s has both dks and dky enabled; this is too yolo and not permitted\"\n                raise Exception(t % (vol.vpath,))\n\n            if dks and not dk:\n                dk = dks\n            if dky and not dk:\n                dk = dky\n            if dk:\n                vol.flags[\"dk\"] = int(dk) if dk is not True else 8\n\n        if have_fk and re.match(r\"^[0-9\\.]+$\", self.args.fk_salt):\n            self.log(\"filekey salt: {}\".format(self.args.fk_salt))\n\n        fk_len = len(self.args.fk_salt)\n        if have_fk and fk_len < 14:\n            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\"\n            zs = os.path.join(E.cfg, \"fk-salt.txt\")\n            self.log(t % (fk_len, 16, zs), 3)\n\n        for vol in vfs.all_nodes.values():\n            if \"pk\" in vol.flags and \"gz\" not in vol.flags and \"xz\" not in vol.flags:\n                vol.flags[\"gz\"] = False  # def.pk\n\n            if \"scan\" in vol.flags:\n                vol.flags[\"scan\"] = int(vol.flags[\"scan\"])\n            elif self.args.re_maxage:\n                vol.flags[\"scan\"] = self.args.re_maxage\n\n        self.args.have_unlistc = False\n\n        all_mte = {}\n        errors = False\n        free_umask = False\n        have_reflink = False\n        for vol in vfs.all_nodes.values():\n            if os.path.isfile(vol.realpath):\n                vol.flags[\"is_file\"] = True\n                vol.flags[\"d2d\"] = True\n\n            if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa:\n                vol.flags[\"e2ds\"] = True\n\n            if self.args.e2d or \"e2ds\" in vol.flags:\n                vol.flags[\"e2d\"] = True\n\n            for ga, vf in [\n                [\"no_hash\", \"nohash\"],\n                [\"no_idx\", \"noidx\"],\n                [\"og_ua\", \"og_ua\"],\n                [\"srch_excl\", \"srch_excl\"],\n            ]:\n                if vf in vol.flags:\n                    ptn = re.compile(vol.flags.pop(vf))\n                else:\n                    ptn = getattr(self.args, ga)\n\n                if ptn:\n                    vol.flags[vf] = ptn\n\n            for ga, vf in vf_bmap().items():\n                if getattr(self.args, ga):\n                    vol.flags[vf] = True\n\n            for ve, vd in (\n                (\"nodotsrch\", \"dotsrch\"),\n                (\"sb_lg\", \"no_sb_lg\"),\n                (\"sb_md\", \"no_sb_md\"),\n            ):\n                if ve in vol.flags:\n                    vol.flags.pop(vd, None)\n\n            for ga, vf in vf_vmap().items():\n                if vf not in vol.flags:\n                    vol.flags[vf] = getattr(self.args, ga)\n\n            zs = \"forget_ip gid nrand tail_who th_qv th_qvx th_spec_p u2abort u2ow uid unp_who ups_who zip_who\"\n            for k in zs.split():\n                if k in vol.flags:\n                    vol.flags[k] = int(vol.flags[k])\n\n            zs = \"aconvt convt tail_fd tail_rate tail_tmax\"\n            for k in zs.split():\n                if k in vol.flags:\n                    vol.flags[k] = float(vol.flags[k])\n\n            for k in (\"mv_re\", \"rm_re\"):\n                try:\n                    zs1, zs2 = vol.flags[k + \"try\"].split(\"/\")\n                    vol.flags[k + \"_t\"] = float(zs1)\n                    vol.flags[k + \"_r\"] = float(zs2)\n                except:\n                    t = 'volume \"/%s\" has invalid %stry [%s]'\n                    raise Exception(t % (vol.vpath, k, vol.flags.get(k + \"try\")))\n\n            for k in (\"chmod_d\", \"chmod_f\"):\n                is_d = k == \"chmod_d\"\n                zs = vol.flags.get(k, \"\")\n                if not zs and is_d:\n                    zs = \"755\"\n                if not zs:\n                    vol.flags.pop(k, None)\n                    continue\n                if not re.match(\"^[0-7]{3,4}$\", zs):\n                    t = \"config-option '%s' must be a three- or four-digit octal value such as [0755] or [644] but the value was [%s]\"\n                    t = t % (k, zs)\n                    self.log(t, 1)\n                    raise Exception(t)\n                zi = int(zs, 8)\n                vol.flags[k] = zi\n                if (is_d and zi != 0o755) or not is_d:\n                    free_umask = True\n\n            vol.flags.pop(\"chown\", None)\n            if vol.flags[\"uid\"] != -1 or vol.flags[\"gid\"] != -1:\n                vol.flags[\"chown\"] = True\n            vol.flags.pop(\"fperms\", None)\n            if \"chown\" in vol.flags or vol.flags.get(\"chmod_f\"):\n                vol.flags[\"fperms\"] = True\n            if vol.lim:\n                vol.lim.chmod_d = vol.flags[\"chmod_d\"]\n                vol.lim.chown = \"chown\" in vol.flags\n                vol.lim.uid = vol.flags[\"uid\"]\n                vol.lim.gid = vol.flags[\"gid\"]\n\n            vol.flags[\"du_iwho\"] = n_du_who(vol.flags[\"du_who\"])\n\n            if not enshare:\n                vol.flags[\"shr_who\"] = self.args.shr_who = \"no\"\n\n            if vol.flags.get(\"og\"):\n                self.args.uqe = True\n\n            if \"unlistcr\" in vol.flags or \"unlistcw\" in vol.flags:\n                self.args.have_unlistc = True\n\n            if \"reflink\" in vol.flags:\n                have_reflink = True\n\n            zs = str(vol.flags.get(\"tcolor\", \"\")).lstrip(\"#\")\n            if len(zs) == 3:  # fc5 => ffcc55\n                vol.flags[\"tcolor\"] = \"\".join([x * 2 for x in zs])\n\n            # volflag syntax currently doesn't allow for ':' in value\n            zs = vol.flags[\"put_name\"]\n            vol.flags[\"put_name2\"] = zs.replace(\"{now.\", \"{now:.\")\n\n            if vol.flags.get(\"neversymlink\"):\n                vol.flags[\"hardlinkonly\"] = True  # was renamed\n            if vol.flags.get(\"hardlinkonly\"):\n                vol.flags[\"hardlink\"] = True\n\n            for k1, k2 in IMPLICATIONS:\n                if k1 in vol.flags:\n                    vol.flags[k2] = True\n\n            for k1, k2 in UNPLICATIONS:\n                if k1 in vol.flags:\n                    vol.flags[k2] = False\n\n            dbds = \"acid|swal|wal|yolo\"\n            vol.flags[\"dbd\"] = dbd = vol.flags.get(\"dbd\") or self.args.dbd\n            if dbd not in dbds.split(\"|\"):\n                t = 'volume \"/%s\" has invalid dbd [%s]; must be one of [%s]'\n                raise Exception(t % (vol.vpath, dbd, dbds))\n\n            # default tag cfgs if unset\n            for k in (\"mte\", \"mth\", \"exp_md\", \"exp_lg\"):\n                if k not in vol.flags:\n                    vol.flags[k] = getattr(self.args, k).copy()\n                else:\n                    vol.flags[k] = odfusion(getattr(self.args, k), vol.flags[k])\n\n            # append additive args from argv to volflags\n            hooks = \"xbu xau xiu xbc xac xbr xar xbd xad xm xban\".split()\n            for name in \"ext_th mtp on404 on403\".split() + hooks:\n                self._read_volflag(\n                    vol.vpath, vol.flags, name, getattr(self.args, name), True\n                )\n\n            for hn in hooks:\n                cmds = vol.flags.get(hn)\n                if not cmds:\n                    continue\n\n                ncmds = []\n                for cmd in cmds:\n                    hfs = []\n                    ocmd = cmd\n                    while \",\" in cmd[:6]:\n                        zs, cmd = cmd.split(\",\", 1)\n                        hfs.append(zs)\n\n                    if \"c\" in hfs and \"f\" in hfs:\n                        t = \"cannot combine flags c and f; removing f from eventhook [{}]\"\n                        self.log(t.format(ocmd), 1)\n                        hfs = [x for x in hfs if x != \"f\"]\n                        ocmd = \",\".join(hfs + [cmd])\n\n                    if \"c\" not in hfs and \"f\" not in hfs and hn == \"xban\":\n                        hfs = [\"c\"] + hfs\n                        ocmd = \",\".join(hfs + [cmd])\n\n                    ncmds.append(ocmd)\n                vol.flags[hn] = ncmds\n\n            ext_th = vol.flags[\"ext_th_d\"] = {}\n            etv = \"(?)\"\n            try:\n                for etv in vol.flags.get(\"ext_th\") or []:\n                    k, v = etv.split(\"=\")\n                    ext_th[k] = v\n            except:\n                t = \"WARNING: volume [/%s]: invalid value specified for ext-th: %s\"\n                self.log(t % (vol.vpath, etv), 3)\n\n            zsl = [x.strip() for x in vol.flags[\"rw_edit\"].split(\",\")]\n            zsl = [x for x in zsl if x]\n            vol.flags[\"rw_edit\"] = \",\".join(zsl)\n            vol.flags[\"rw_edit_set\"] = set(x for x in zsl if x)\n\n            emb_all = vol.flags[\"emb_all\"] = set()\n\n            zsl1 = [x for x in vol.flags[\"preadmes\"].split(\",\") if x]\n            zsl2 = [x for x in vol.flags[\"readmes\"].split(\",\") if x]\n            zsl3 = list(set([x.lower() for x in zsl1]))\n            zsl4 = list(set([x.lower() for x in zsl2]))\n            emb_all.update(zsl3)\n            emb_all.update(zsl4)\n            vol.flags[\"emb_mds\"] = [[0, zsl1, zsl3], [1, zsl2, zsl4]]\n\n            zsl1 = [x for x in vol.flags[\"prologues\"].split(\",\") if x]\n            zsl2 = [x for x in vol.flags[\"epilogues\"].split(\",\") if x]\n            zsl3 = list(set([x.lower() for x in zsl1]))\n            zsl4 = list(set([x.lower() for x in zsl2]))\n            emb_all.update(zsl3)\n            emb_all.update(zsl4)\n            vol.flags[\"emb_lgs\"] = [[0, zsl1, zsl3], [1, zsl2, zsl4]]\n\n            zs = str(vol.flags.get(\"html_head\") or \"\")\n            if zs and zs[:1] in \"%@\":\n                vol.flags[\"html_head_d\"] = zs\n                head_s = str(vol.flags.get(\"html_head_s\") or \"\")\n            else:\n                zs2 = str(vol.flags.get(\"html_head_s\") or \"\")\n                if zs2 and zs:\n                    head_s = \"%s\\n%s\\n\" % (zs2.strip(), zs.strip())\n                else:\n                    head_s = zs2 or zs\n\n            if head_s and not head_s.endswith(\"\\n\"):\n                head_s += \"\\n\"\n\n            zs = \"X-Content-Type-Options: nosniff\\r\\n\"\n            if \"norobots\" in vol.flags:\n                head_s += META_NOBOTS\n                zs += \"X-Robots-Tag: noindex, nofollow\\r\\n\"\n            if self.args.http_vary:\n                zs += \"Vary: %s\\r\\n\" % (self.args.http_vary,)\n            vol.flags[\"oh_g\"] = zs + \"\\r\\n\"\n\n            if \"noscript\" in vol.flags:\n                zs += \"Content-Security-Policy: script-src 'none';\\r\\n\"\n            vol.flags[\"oh_f\"] = zs + \"\\r\\n\"\n\n            ico_url = vol.flags.get(\"ufavico\")\n            if ico_url:\n                ico_h = \"\"\n                ico_ext = ico_url.split(\"?\")[0].split(\".\")[-1].lower()\n                if ico_ext in FAVICON_MIMES:\n                    zs = '<link rel=\"icon\" type=\"%s\" href=\"%s\">\\n'\n                    ico_h = zs % (FAVICON_MIMES[ico_ext], ico_url)\n                elif ico_ext == \"ico\":\n                    zs = '<link rel=\"shortcut icon\" href=\"%s\">\\n'\n                    ico_h = zs % (ico_url,)\n                if ico_h:\n                    vol.flags[\"ufavico_h\"] = ico_h\n                    head_s += ico_h\n\n            if head_s:\n                vol.flags[\"html_head_s\"] = head_s\n            else:\n                vol.flags.pop(\"html_head_s\", None)\n\n            if not vol.flags.get(\"html_head_d\"):\n                vol.flags.pop(\"html_head_d\", None)\n\n            vol.check_landmarks()\n\n            if vol.flags.get(\"db_xattr\"):\n                self.args.have_db_xattr = True\n                zs = str(vol.flags[\"db_xattr\"])\n                neg = zs.startswith(\"~~\")\n                if neg:\n                    zs = zs[2:]\n                zsl = [x.strip() for x in zs.split(\",\")]\n                zsl = [x for x in zsl if x]\n                if neg:\n                    vol.flags[\"db_xattr_no\"] = set(zsl)\n                else:\n                    vol.flags[\"db_xattr_yes\"] = zsl\n\n            # d2d drops all database features for a volume\n            for grp, rm in [[\"d2d\", \"e2d\"], [\"d2t\", \"e2t\"], [\"d2d\", \"e2v\"]]:\n                if not vol.flags.get(grp, False):\n                    continue\n\n                vol.flags[\"d2t\"] = True\n                vol.flags = {k: v for k, v in vol.flags.items() if not k.startswith(rm)}\n\n            # d2ds drops all onboot scans for a volume\n            for grp, rm in [[\"d2ds\", \"e2ds\"], [\"d2ts\", \"e2ts\"]]:\n                if not vol.flags.get(grp, False):\n                    continue\n\n                vol.flags[\"d2ts\"] = True\n                vol.flags = {k: v for k, v in vol.flags.items() if not k.startswith(rm)}\n\n            # mt* needs e2t so drop those too\n            for grp, rm in [[\"e2t\", \"mt\"]]:\n                if vol.flags.get(grp, False):\n                    continue\n\n                vol.flags = {\n                    k: v\n                    for k, v in vol.flags.items()\n                    if not k.startswith(rm) or k == \"mte\"\n                }\n\n            for grp, rm in [[\"d2v\", \"e2v\"]]:\n                if not vol.flags.get(grp, False):\n                    continue\n\n                vol.flags = {k: v for k, v in vol.flags.items() if not k.startswith(rm)}\n\n            ints = [\"lifetime\"]\n            for k in list(vol.flags):\n                if k in ints:\n                    vol.flags[k] = int(vol.flags[k])\n\n            if \"e2d\" not in vol.flags:\n                zs = \"lifetime rss\"\n                drop = [x for x in zs.split() if x in vol.flags]\n\n                zs = \"xau xiu\"\n                drop += [x for x in zs.split() if vol.flags.get(x)]\n\n                for k in drop:\n                    t = 'cannot enable [%s] for volume \"/%s\" because this requires one of the following: e2d / e2ds / e2dsa  (either as volflag or global-option)'\n                    self.log(t % (k, vol.vpath), 1)\n                    vol.flags.pop(k)\n\n            zi = vol.flags.get(\"lifetime\") or 0\n            zi2 = time.time() // (86400 * 365)\n            zi3 = zi2 * 86400 * 365\n            if zi < 0 or zi > zi3:\n                t = \"the lifetime of volume [/%s] (%d) exceeds max value (%d years; %d)\"\n                t = t % (vol.vpath, zi, zi2, zi3)\n                self.log(t, 1)\n                raise Exception(t)\n\n            if (\n                \"dedup\" in vol.flags\n                and \"reflink\" not in vol.flags\n                and vol.flags[\"apnd_who\"] != \"no\"\n            ):\n                vol.flags[\"apnd_who\"] = \"ndd\"\n\n            # verify tags mentioned by -mt[mp] are used by -mte\n            local_mtp = {}\n            local_only_mtp = {}\n            tags = vol.flags.get(\"mtp\", []) + vol.flags.get(\"mtm\", [])\n            tags = [x.split(\"=\")[0] for x in tags]\n            tags = [y for x in tags for y in x.split(\",\")]\n            for a in tags:\n                local_mtp[a] = True\n                local = True\n                for b in self.args.mtp or []:\n                    b = b.split(\"=\")[0]\n                    if a == b:\n                        local = False\n\n                if local:\n                    local_only_mtp[a] = True\n\n            local_mte = ODict()\n            for a in vol.flags.get(\"mte\", {}).keys():\n                local = True\n                all_mte[a] = True\n                local_mte[a] = True\n                for b in self.args.mte.keys():\n                    if not a or not b:\n                        continue\n\n                    if a == b:\n                        local = False\n\n            for mtp in local_only_mtp:\n                if mtp not in local_mte:\n                    t = 'volume \"/{}\" defines metadata tag \"{}\", but doesnt use it in \"-mte\" (or with \"cmte\" in its volflags)'\n                    self.log(t.format(vol.vpath, mtp), 1)\n                    errors = True\n\n            mte = vol.flags.get(\"mte\") or {}\n            up_m = [x for x in UP_MTE_MAP if x in mte]\n            up_q = [UP_MTE_MAP[x] for x in up_m]\n            zs = \"select %s from up where rd=? and fn=?\" % (\", \".join(up_q),)\n            vol.flags[\"ls_q_m\"] = (zs if up_m else \"\", up_m)\n\n        vfs.all_fvols = {\n            zs: vol for zs, vol in vfs.all_vols.items() if \"is_file\" in vol.flags\n        }\n\n        for vol in vfs.all_nodes.values():\n            if not vol.flags.get(\"is_file\"):\n                continue\n            zs = \"og opds xlink\"\n            for zs in zs.split():\n                vol.flags.pop(zs, None)\n\n        for vol in vfs.all_vols.values():\n            if not vol.realpath or vol.flags.get(\"is_file\"):\n                continue\n            ccs = vol.flags[\"casechk\"][:1].lower()\n            if ccs in (\"y\", \"n\"):\n                if ccs == \"y\":\n                    vol.flags[\"bcasechk\"] = True\n                continue\n            try:\n                bos.makedirs(vol.realpath, vf=vol.flags)\n                files = os.listdir(vol.realpath)\n                for fn in files:\n                    fn2 = fn.lower()\n                    if fn == fn2:\n                        fn2 = fn.upper()\n                    if fn == fn2 or fn2 in files:\n                        continue\n                    is_ci = os.path.exists(os.path.join(vol.realpath, fn2))\n                    ccs = \"y\" if is_ci else \"n\"\n                    break\n                if ccs not in (\"y\", \"n\"):\n                    ap = os.path.join(vol.realpath, \"casechk\")\n                    open(ap, \"wb\").close()\n                    ccs = \"y\" if os.path.exists(ap[:-1] + \"K\") else \"n\"\n                    os.unlink(ap)\n            except Exception as ex:\n                if ANYWIN:\n                    zs = \"Windows\"\n                    ccs = \"y\"\n                elif MACOS:\n                    zs = \"Macos\"\n                    ccs = \"y\"\n                else:\n                    zs = \"Linux\"\n                    ccs = \"n\"\n                t = \"unable to determine if filesystem at %r is case-insensitive due to %r; assuming casechk=%s due to %s\"\n                self.log(t % (vol.realpath, ex, ccs, zs), 3)\n            vol.flags[\"casechk\"] = ccs\n            if ccs == \"y\":\n                vol.flags[\"bcasechk\"] = True\n\n        tags = self.args.mtp or []\n        tags = [x.split(\"=\")[0] for x in tags]\n        tags = [y for x in tags for y in x.split(\",\")]\n        for mtp in tags:\n            if mtp not in all_mte:\n                t = 'metadata tag \"{}\" is defined by \"-mtm\" or \"-mtp\", but is not used by \"-mte\" (or by any \"cmte\" volflag)'\n                self.log(t.format(mtp), 1)\n                errors = True\n\n        for vol in vfs.all_vols.values():\n            re1: Optional[re.Pattern] = vol.flags.get(\"srch_excl\")\n            excl = [re1.pattern] if re1 else []\n\n            vpaths = []\n            vtop = vol.vpath\n            for vp2 in vfs.all_vols.keys():\n                if vp2.startswith((vtop + \"/\").lstrip(\"/\")) and vtop != vp2:\n                    vpaths.append(re.escape(vp2[len(vtop) :].lstrip(\"/\")))\n            if vpaths:\n                excl.append(\"^(%s)/\" % (\"|\".join(vpaths),))\n\n            vol.flags[\"srch_re_dots\"] = re.compile(\"|\".join(excl or [\"^$\"]))\n            excl.extend([r\"^\\.\", r\"/\\.\"])\n            vol.flags[\"srch_re_nodot\"] = re.compile(\"|\".join(excl))\n\n        have_daw = False\n        for vol in vfs.all_nodes.values():\n            daw = vol.flags.get(\"daw\") or self.args.daw\n            if daw:\n                vol.flags[\"daw\"] = True\n                have_daw = True\n\n        if have_daw and self.args.no_dav:\n            t = 'volume \"/{}\" has volflag \"daw\" (webdav write-access), but --no-dav is set'\n            self.log(t, 1)\n            errors = True\n\n        if self.args.smb and self.ah.on and acct:\n            self.log(\"--smb can only be used when --ah-alg is none\", 1)\n            errors = True\n\n        for vol in vfs.all_nodes.values():\n            for k in list(vol.flags.keys()):\n                if re.match(\"^-[^-]+$\", k):\n                    vol.flags.pop(k)\n                    zs = k[1:]\n                    if zs in vol.flags:\n                        vol.flags.pop(k[1:])\n                    else:\n                        t = \"WARNING: the config for volume [/%s] tried to remove volflag [%s] by specifying [%s] but that volflag was not already set\"\n                        self.log(t % (vol.vpath, zs, k), 3)\n\n            if vol.flags.get(\"dots\"):\n                for name in vol.axs.uread:\n                    vol.axs.udot.add(name)\n\n        if errors:\n            sys.exit(1)\n\n        setattr(self.args, \"free_umask\", free_umask)\n        if free_umask:\n            os.umask(0)\n\n        vfs.bubble_flags()\n\n        have_e2d = False\n        have_e2t = False\n        have_dedup = False\n        have_symdup = False\n        unsafe_dedup = []\n        t = \"volumes and permissions:\\n\"\n        for zv in vfs.all_vols.values():\n            if not self.warn_anonwrite or verbosity < 5:\n                break\n\n            if enshare and (zv.vpath == shr or zv.vpath.startswith(shrs)):\n                continue\n\n            t += '\\n\\033[36m\"/{}\"  \\033[33m{}\\033[0m'.format(zv.vpath, zv.realpath)\n            for txt, attr in [\n                [\"  read\", \"uread\"],\n                [\" write\", \"uwrite\"],\n                [\"  move\", \"umove\"],\n                [\"delete\", \"udel\"],\n                [\"  dots\", \"udot\"],\n                [\"   get\", \"uget\"],\n                [\" upGet\", \"upget\"],\n                [\"  html\", \"uhtml\"],\n                [\"uadmin\", \"uadmin\"],\n            ]:\n                u = list(sorted(getattr(zv.axs, attr)))\n                if u == [\"*\"] and acct:\n                    u = [\"\\033[35monly-anonymous\\033[0m\"]\n                elif \"*\" in u:\n                    u = [\"\\033[35meverybody\\033[0m\"]\n                if not u:\n                    u = [\"\\033[36m--none--\\033[0m\"]\n                u = \", \".join(u)\n                t += \"\\n|  {}:  {}\".format(txt, u)\n\n            if \"e2d\" in zv.flags:\n                have_e2d = True\n\n            if \"e2t\" in zv.flags:\n                have_e2t = True\n\n            if \"dedup\" in zv.flags:\n                have_dedup = True\n                if \"hardlink\" not in zv.flags and \"reflink\" not in zv.flags:\n                    have_symdup = True\n                    if \"e2d\" not in zv.flags:\n                        unsafe_dedup.append(\"/\" + zv.vpath)\n\n            t += \"\\n\"\n\n        if have_symdup and self.args.fika:\n            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\"\n            # self.args.fika = self.args.fika.replace(\"m\", \"\").replace(\"d\", \"\")  # probably not enough\n            self.args.fika = \"\"\n\n        if self.warn_anonwrite and verbosity > 4:\n            if not self.args.no_voldump:\n                self.log(t)\n\n            if have_e2d or self.args.have_idp_hdrs:\n                t = self.chk_sqlite_threadsafe()\n                if t:\n                    self.log(\"\\n\\033[{}\\033[0m\\n\".format(t))\n            if have_e2d:\n                if not have_e2t:\n                    t = \"hint: enable multimedia indexing (artist/title/...) with argument -e2ts\"\n                    self.log(t, 6)\n            else:\n                t = \"hint: enable searching and upload-undo with argument -e2dsa\"\n                self.log(t, 6)\n\n            if unsafe_dedup:\n                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\"\n                self.log(t % (\", \".join(unsafe_dedup)), 3)\n            elif not have_dedup:\n                t = \"hint: enable upload deduplication with --dedup (but see readme for consequences)\"\n                self.log(t, 6)\n\n            zv, _ = vfs.get(\"/\", \"*\", False, False)\n            zs = zv.realpath.lower()\n            if zs in (\"/\", \"c:\\\\\") or zs.startswith(r\"c:\\windows\"):\n                t = \"you are sharing a system directory: {}\\n\"\n                self.log(t.format(zv.realpath), c=1)\n\n        try:\n            zv, _ = vfs.get(\"\", \"*\", False, True, err=999)\n            if self.warn_anonwrite and verbosity > 4 and os.getcwd() == zv.realpath:\n                t = \"anyone can write to the current directory: {}\\n\"\n                self.log(t.format(zv.realpath), c=1)\n\n            self.warn_anonwrite = False\n        except Pebkac:\n            self.warn_anonwrite = True\n\n        self.idp_warn = []\n        self.idp_err = []\n        for idp_vp in self.idp_vols:\n            idp_vn, _ = vfs.get(idp_vp, \"*\", False, False)\n            idp_vp0 = idp_vn.vpath0\n\n            sigils = set(PTN_SIGIL.findall(idp_vp0))\n            if len(sigils) > 1:\n                t = '\\nWARNING: IdP-volume \"/%s\" created by \"/%s\" has multiple IdP placeholders: %s'\n                self.idp_warn.append(t % (idp_vp, idp_vp0, list(sigils)))\n                continue\n\n            sigil = sigils.pop()\n            par_vp = idp_vp\n            while par_vp:\n                par_vp = vsplit(par_vp)[0]\n                par_vn, _ = vfs.get(par_vp, \"*\", False, False)\n                if sigil in par_vn.vpath0:\n                    continue  # parent was spawned for and by same user\n\n                oth_read = []\n                oth_write = []\n                for usr in par_vn.axs.uread:\n                    if usr not in idp_vn.axs.uread:\n                        oth_read.append(usr)\n                for usr in par_vn.axs.uwrite:\n                    if usr not in idp_vn.axs.uwrite:\n                        oth_write.append(usr)\n\n                if \"*\" in oth_read:\n                    taxs = \"WORLD-READABLE\"\n                elif \"*\" in oth_write:\n                    taxs = \"WORLD-WRITABLE\"\n                elif oth_read:\n                    taxs = \"READABLE BY %r\" % (oth_read,)\n                elif oth_write:\n                    taxs = \"WRITABLE BY %r\" % (oth_write,)\n                else:\n                    break  # no sigil; not idp; safe to stop\n\n                t = '\\nWARNING: IdP-volume \"/%s\" created by \"/%s\" has parent/grandparent \"/%s\" and would be %s'\n                self.idp_err.append(t % (idp_vp, idp_vp0, par_vn.vpath, taxs))\n\n        if self.idp_warn:\n            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.\"\n            self.log(t + \"\".join(self.idp_warn), 1)\n\n        if self.idp_err:\n            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.\"\n            self.log(t + \"\".join(self.idp_err), 1)\n\n        if have_reflink:\n            t = \"WARNING: Reflink-based dedup was requested, but %s. This will not work; files will be full copies instead.\"\n            if not sys.platform.startswith(\"linux\"):\n                self.log(t % \"your OS is not Linux\", 1)\n\n        self.vfs = vfs\n        self.acct = acct\n        self.defpw = defpw\n        self.grps = grps\n        self.iacct = {v: k for k, v in acct.items()}\n        self.cfg_files_loaded = cfg_files_loaded\n\n        self.load_sessions()\n\n        self.re_pwd = None\n        pwds = [re.escape(x) for x in self.iacct.keys()]\n        pwds.extend(list(self.sesa))\n        if self.args.usernames:\n            pwds.extend([x.split(\":\", 1)[1] for x in pwds if \":\" in x])\n        if pwds:\n            if self.ah.on:\n                zs = r\"(\\[H\\] %s:.*|[?&]%s=)([^&]+)\"\n                zs = zs % (self.args.pw_hdr, self.args.pw_urlp)\n            else:\n                zs = r\"(\\[H\\] %s:.*|=)(\" % (self.args.pw_hdr,)\n                zs += \"|\".join(pwds) + r\")([]&; ]|$)\"\n\n            self.re_pwd = re.compile(zs)\n\n        # to ensure it propagates into tcpsrv with mp on\n        if self.args.mime:\n            for zs in self.args.mime:\n                ext, mime = zs.split(\"=\", 1)\n                MIMES[ext] = mime\n            EXTS.update({v: k for k, v in MIMES.items()})\n\n        if enshare:\n            # hide shares from controlpanel\n            vfs.all_vols = {\n                x: y\n                for x, y in vfs.all_vols.items()\n                if x != shr and not x.startswith(shrs)\n            }\n\n            assert db and cur and cur2 and shv  # type: ignore\n            for row in cur.execute(\"select * from sh\"):\n                s_k, s_pw, s_vp, s_pr, s_nf, s_un, s_t0, s_t1 = row\n                shn = shv.nodes.get(s_k, None)\n                if not shn:\n                    continue\n\n                try:\n                    s_vfs, s_rem = vfs.get(\n                        s_vp, s_un, \"r\" in s_pr, \"w\" in s_pr, \"m\" in s_pr, \"d\" in s_pr\n                    )\n                except Exception as ex:\n                    t = \"removing share [%s] by [%s] to [%s] due to %r\"\n                    self.log(t % (s_k, s_un, s_vp, ex), 3)\n                    shv.nodes.pop(s_k)\n                    continue\n\n                fns = []\n                if s_nf:\n                    q = \"select vp from sf where k = ?\"\n                    for (s_fn,) in cur2.execute(q, (s_k,)):\n                        fns.append(s_fn)\n\n                    shn.shr_files = set(fns)\n                    shn.ls = shn._ls_shr\n                    shn.canonical = shn._canonical_shr\n                    shn.dcanonical = shn._dcanonical_shr\n                else:\n                    shn.ls = shn._ls\n                    shn.canonical = shn._canonical\n                    shn.dcanonical = shn._dcanonical\n\n                shn.shr_owner = s_un\n                shn.shr_src = (s_vfs, s_rem)\n                shn.realpath = s_vfs.canonical(s_rem)\n\n                o_vn, _ = shn._get_share_src(\"\")\n                shn.lim = o_vn.lim\n                shn.flags = o_vn.flags.copy()\n                shn.dbpath = o_vn.dbpath\n                shn.histpath = o_vn.histpath\n\n                # root.all_aps doesn't include any shares, so make a copy where the\n                # share appears in all abspaths it can provide (for example for chk_ap)\n                ap = shn.realpath\n                if not ap.endswith(os.sep):\n                    ap += os.sep\n                shn.shr_all_aps = [(x, y[:]) for x, y in vfs.all_aps]\n                exact = False\n                for ap2, vns in shn.shr_all_aps:\n                    if ap == ap2:\n                        exact = True\n                    if ap2.startswith(ap):\n                        try:\n                            vp2 = vjoin(s_rem, ap2[len(ap) :])\n                            vn2, _ = s_vfs.get(vp2, \"*\", False, False)\n                            if vn2 == s_vfs or vn2.dbv == s_vfs:\n                                vns.append(shn)\n                        except:\n                            pass\n                if not exact:\n                    shn.shr_all_aps.append((ap, [shn]))\n                shn.shr_all_aps.sort(key=lambda x: len(x[0]), reverse=True)\n\n                if self.args.shr_v:\n                    t = \"mapped %s share [%s] by [%s] => [%s] => [%s]\"\n                    self.log(t % (s_pr, s_k, s_un, s_vp, shn.realpath))\n\n            # transplant shadowing into shares\n            for vn in shv.nodes.values():\n                svn, srem = vn.shr_src  # type: ignore\n                if srem:\n                    continue  # free branch, safe\n                ap = svn.canonical(srem)\n                if bos.path.isfile(ap):\n                    continue  # also fine\n                for zs in svn.nodes.keys():\n                    # hide subvolume\n                    vn.nodes[zs] = VFS(self.log_func, \"\", \"\", \"\", AXS(), self.vf0())\n\n            cur2.close()\n            cur.close()\n            db.close()\n\n        self.js_ls = {}\n        self.js_htm = {}\n        for vp, vn in self.vfs.all_nodes.items():\n            if enshare and vp.startswith(shrs):\n                continue  # propagates later in this func\n            vf = vn.flags\n            vn.js_ls = {\n                \"idx\": \"e2d\" in vf,\n                \"itag\": \"e2t\" in vf,\n                \"dlni\": \"dlni\" in vf,\n                \"dgrid\": \"grid\" in vf,\n                \"dnsort\": \"nsort\" in vf,\n                \"dhsortn\": vf[\"hsortn\"],\n                \"dsort\": vf[\"sort\"],\n                \"dcrop\": vf[\"crop\"],\n                \"dth3x\": vf[\"th3x\"],\n                \"u2ts\": vf[\"u2ts\"],\n                \"shr_who\": vf[\"shr_who\"],\n                \"frand\": bool(vf.get(\"rand\")),\n                \"lifetime\": vf.get(\"lifetime\") or 0,\n                \"unlist\": vf.get(\"unlist\") or \"\",\n                \"sb_lg\": \"\" if \"no_sb_lg\" in vf else (vf.get(\"lg_sbf\") or \"y\"),\n                \"sb_md\": \"\" if \"no_sb_md\" in vf else (vf.get(\"md_sbf\") or \"y\"),\n                \"rw_edit\": vf[\"rw_edit\"],\n            }\n            if \"ufavico_h\" in vf:\n                vn.js_ls[\"ufavico\"] = vf[\"ufavico_h\"]\n            js_htm = {\n                \"SPINNER\": self.args.spinner,\n                \"s_name\": self.args.bname,\n                \"idp_login\": self.args.idp_login,\n                \"have_up2k_idx\": \"e2d\" in vf,\n                \"have_acode\": not self.args.no_acode,\n                \"have_c2flac\": self.args.allow_flac,\n                \"have_c2wav\": self.args.allow_wav,\n                \"have_shr\": self.args.shr,\n                \"shr_who\": vf[\"shr_who\"],\n                \"have_zip\": not self.args.no_zip,\n                \"have_zls\": not self.args.no_zls,\n                \"have_mv\": not self.args.no_mv,\n                \"have_del\": not self.args.no_del,\n                \"have_unpost\": int(self.args.unpost),\n                \"have_emp\": int(self.args.emp),\n                \"md_no_br\": int(vf.get(\"md_no_br\") or 0),\n                \"ext_th\": vf.get(\"ext_th_d\") or {},\n                \"sb_lg\": vn.js_ls[\"sb_lg\"],\n                \"sb_md\": vn.js_ls[\"sb_md\"],\n                \"sba_md\": vf.get(\"md_sba\") or \"\",\n                \"sba_lg\": vf.get(\"lg_sba\") or \"\",\n                \"rw_edit\": vf[\"rw_edit\"],\n                \"txt_ext\": self.args.textfiles.replace(\",\", \" \"),\n                \"def_hcols\": list(vf.get(\"mth\") or []),\n                \"unlist0\": vf.get(\"unlist\") or \"\",\n                \"see_dots\": self.args.see_dots,\n                \"dqdel\": self.args.qdel,\n                \"dlni\": vn.js_ls[\"dlni\"],\n                \"dgrid\": vn.js_ls[\"dgrid\"],\n                \"dgsel\": \"gsel\" in vf,\n                \"dnsort\": \"nsort\" in vf,\n                \"dhsortn\": vf[\"hsortn\"],\n                \"dsort\": vf[\"sort\"],\n                \"dcrop\": vf[\"crop\"],\n                \"dth3x\": vf[\"th3x\"],\n                \"drcm\": self.args.rcm,\n                \"dvol\": self.args.au_vol,\n                \"idxh\": int(self.args.ih),\n                \"dutc\": not self.args.localtime,\n                \"dfszf\": self.args.ui_filesz.strip(\"-\"),\n                \"themes\": self.args.themes,\n                \"turbolvl\": self.args.turbo,\n                \"nosubtle\": self.args.nosubtle,\n                \"u2j\": self.args.u2j,\n                \"u2sz\": self.args.u2sz,\n                \"u2ts\": vf[\"u2ts\"],\n                \"u2ow\": vf[\"u2ow\"],\n                \"frand\": bool(vf.get(\"rand\")),\n                \"lifetime\": vn.js_ls[\"lifetime\"],\n                \"u2sort\": self.args.u2sort,\n            }\n            zs = \"ui_noacci ui_nocpla ui_noctxb ui_nolbar ui_nombar ui_nonav ui_notree ui_norepl ui_nosrvi\"\n            for zs in zs.split():\n                if vf.get(zs):\n                    js_htm[zs] = 1\n            zs = \"notooltips\"\n            for zs in zs.split():\n                if getattr(self.args, zs, False):\n                    js_htm[zs] = 1\n            zs = \"up_site\"\n            for zs in zs.split():\n                zs2 = getattr(self.args, zs, \"\")\n                if zs2:\n                    js_htm[zs] = zs2\n            vn.js_htm = json_hesc(json.dumps(js_htm))\n\n        vols = list(vfs.all_nodes.values())\n        if enshare:\n            assert shv  # type: ignore  # !rm\n            for vol in shv.nodes.values():\n                if vol.vpath not in vfs.all_nodes:\n                    self.log(\"BUG: /%s not in all_nodes\" % (vol.vpath,), 1)\n                    vols.append(vol)\n            if shr in vfs.all_nodes:\n                t = \"invalid config: a volume is overlapping with the --shr global-option (/%s)\"\n                t = t % (shr,)\n                self.log(t, 1)\n                raise Exception(t)\n\n        for vol in vols:\n            dbv = vol.get_dbv(\"\")[0]\n            vol.js_ls = vol.js_ls or dbv.js_ls or {}\n            vol.js_htm = vol.js_htm or dbv.js_htm or \"{}\"\n\n            zs = str(vol.flags.get(\"tcolor\") or self.args.tcolor)\n            vol.flags[\"tcolor\"] = zs.lstrip(\"#\")\n\n    def setup_auth_ord(self) -> None:\n        ao = [x.strip() for x in self.args.auth_ord.split(\",\")]\n        if \"idp\" in ao:\n            zi = ao.index(\"idp\")\n            ao = ao[:zi] + [\"idp-hm\", \"idp-h\"] + ao[zi:]\n        zsl = \"pw idp-h idp-hm ipu\".split()\n        pw, h, hm, ipu = [ao.index(x) if x in ao else 99 for x in zsl]\n        self.args.ao_idp_before_pw = min(h, hm) < pw\n        self.args.ao_h_before_hm = h < hm\n        self.args.ao_ipu_wins = ipu == 0\n        self.args.ao_have_pw = pw < 99 or not self.args.have_idp_hdrs\n\n    def load_idp_db(self, quiet=False) -> None:\n        # mutex me\n        level = self.args.idp_store\n        if level < 2 or not self.args.have_idp_hdrs:\n            return\n\n        assert sqlite3  # type: ignore  # !rm\n\n        db = sqlite3.connect(self.args.idp_db)\n        cur = db.cursor()\n        from_cache = cur.execute(\"select un, gs from us\").fetchall()\n        cur.close()\n        db.close()\n\n        old_accs = self.idp_accs.copy()\n        self.idp_accs.clear()\n        self.idp_usr_gh.clear()\n\n        gsep = self.args.idp_gsep\n        groupless = (None, [\"\"])\n        n = []\n        for uname, gname in from_cache:\n            if level < 3:\n                if uname in self.idp_accs:\n                    continue\n                if old_accs.get(uname) in groupless:\n                    gname = \"\"\n            gnames = [x.strip() for x in gsep.split(gname)]\n            gnames.sort()\n\n            self.idp_accs[uname] = gnames\n            n.append(uname)\n\n        if n and not quiet:\n            t = \", \".join(n[:9])\n            if len(n) > 9:\n                t += \"...\"\n            self.log(\"found %d IdP users in db (%s)\" % (len(n), t))\n\n    def load_sessions(self, quiet=False) -> None:\n        # mutex me\n        if self.args.no_ses:\n            self.ases = {}\n            self.sesa = {}\n            return\n\n        assert sqlite3  # type: ignore  # !rm\n\n        ases = {}\n        blen = (self.args.ses_len // 4) * 4  # 3 bytes in 4 chars\n        blen = (blen * 3) // 4  # bytes needed for ses_len chars\n\n        db = sqlite3.connect(self.args.ses_db)\n        cur = db.cursor()\n\n        for uname, sid in cur.execute(\"select un, si from us\"):\n            if uname in self.acct:\n                ases[uname] = sid\n\n        n = []\n        q = \"insert into us values (?,?,?)\"\n        accs = list(self.acct)\n        if self.args.have_idp_hdrs and self.args.idp_cookie:\n            accs.extend(self.idp_accs.keys())\n        for uname in accs:\n            if uname not in ases:\n                sid = ub64enc(os.urandom(blen)).decode(\"ascii\")\n                cur.execute(q, (uname, sid, int(time.time())))\n                ases[uname] = sid\n                n.append(uname)\n\n        if n:\n            db.commit()\n\n        cur.close()\n        db.close()\n\n        self.ases = ases\n        self.sesa = {v: k for k, v in ases.items()}\n        if n and not quiet:\n            t = \", \".join(n[:3])\n            if len(n) > 3:\n                t += \"...\"\n            self.log(\"added %d new sessions (%s)\" % (len(n), t))\n\n    def forget_session(self, broker: Optional[\"BrokerCli\"], uname: str) -> None:\n        with self.mutex:\n            self._forget_session(uname)\n\n        if broker:\n            broker.ask(\"_reload_sessions\").get()\n\n    def _forget_session(self, uname: str) -> None:\n        if self.args.no_ses:\n            return\n\n        assert sqlite3  # type: ignore  # !rm\n\n        db = sqlite3.connect(self.args.ses_db)\n        cur = db.cursor()\n        cur.execute(\"delete from us where un = ?\", (uname,))\n        db.commit()\n        cur.close()\n        db.close()\n\n        self.sesa.pop(self.ases.get(uname, \"\"), \"\")\n        self.ases.pop(uname, \"\")\n\n    def chpw(self, broker: Optional[\"BrokerCli\"], uname, pw) -> tuple[bool, str]:\n        if not self.args.chpw:\n            return False, \"feature disabled in server config\"\n\n        if uname == \"*\" or uname not in self.defpw:\n            return False, \"not logged in\"\n\n        if uname in self.args.chpw_no:\n            return False, \"not allowed for this account\"\n\n        if len(pw) < self.args.chpw_len:\n            t = \"minimum password length: %d characters\"\n            return False, t % (self.args.chpw_len,)\n\n        if self.args.usernames:\n            pw = \"%s:%s\" % (uname, pw)\n\n        hpw = self.ah.hash(pw) if self.ah.on else pw\n\n        if hpw == self.acct[uname]:\n            return False, \"that's already your password my dude\"\n\n        if hpw in self.iacct or hpw in self.sesa:\n            return False, \"password is taken\"\n\n        with self.mutex:\n            ap = self.args.chpw_db\n            if not bos.path.exists(ap):\n                pwdb = {}\n            else:\n                jtxt = read_utf8(self.log, ap, True)\n                pwdb = json.loads(jtxt) if jtxt.strip() else {}\n\n            pwdb = [x for x in pwdb if x[0] != uname]\n            pwdb.append((uname, self.defpw[uname], hpw))\n\n            with open(ap, \"w\", encoding=\"utf-8\") as f:\n                json.dump(pwdb, f, separators=(\",\\n\", \": \"))\n\n            self.log(\"reinitializing due to password-change for user [%s]\" % (uname,))\n\n            if not broker:\n                # only true for tests\n                self._reload()\n                return True, \"new password OK\"\n\n        broker.ask(\"reload\", False, False).get()\n        return True, \"new password OK\"\n\n    def setup_chpw(self, acct: dict[str, str]) -> None:\n        ap = self.args.chpw_db\n        if not self.args.chpw or not bos.path.exists(ap):\n            return\n\n        jtxt = read_utf8(self.log, ap, True)\n        pwdb = json.loads(jtxt) if jtxt.strip() else {}\n\n        useen = set()\n        urst = set()\n        uok = set()\n        for usr, orig, mod in pwdb:\n            useen.add(usr)\n            if usr not in acct:\n                # previous user, no longer known\n                continue\n            if acct[usr] != orig:\n                urst.add(usr)\n                continue\n            uok.add(usr)\n            acct[usr] = mod\n\n        if not self.args.chpw_v:\n            return\n\n        for usr in acct:\n            if usr not in useen:\n                urst.add(usr)\n\n        for zs in uok:\n            urst.discard(zs)\n\n        if self.args.chpw_v == 1 or (self.args.chpw_v == 2 and not urst):\n            t = \"chpw: %d changed, %d unchanged\"\n            self.log(t % (len(uok), len(urst)))\n            return\n\n        elif self.args.chpw_v == 2:\n            t = \"chpw: %d changed\" % (len(uok),)\n            if urst:\n                t += \", \\033[0munchanged:\\033[35m %s\" % (\", \".join(list(urst)))\n\n            self.log(t, 6)\n            return\n\n        msg = \"\"\n        if uok:\n            t = \"\\033[0mchanged: \\033[32m%s\"\n            msg += t % (\", \".join(list(uok)),)\n        if urst:\n            t = \"%s\\033[0munchanged: \\033[35m%s\"\n            msg += t % (\n                \", \" if msg else \"\",\n                \", \".join(list(urst)),\n            )\n\n        self.log(\"chpw: \" + msg, 6)\n\n    def setup_pwhash(self, acct: dict[str, str]) -> None:\n        if self.args.usernames:\n            for uname, pw in list(acct.items())[:]:\n                if pw.startswith(\"+\") and len(pw) == 33:\n                    continue\n                acct[uname] = \"%s:%s\" % (uname, pw)\n\n        self.ah = PWHash(self.args)\n        if not self.ah.on:\n            if self.args.ah_cli or self.args.ah_gen:\n                t = \"\\n  BAD CONFIG:\\n    cannot --ah-cli or --ah-gen without --ah-alg\"\n                raise Exception(t)\n            return\n\n        if self.args.ah_cli:\n            self.ah.cli()\n            sys.exit()\n        elif self.args.ah_gen == \"-\":\n            self.ah.stdin()\n            sys.exit()\n        elif self.args.ah_gen:\n            print(self.ah.hash(self.args.ah_gen))\n            sys.exit()\n\n        if not acct:\n            return\n\n        changed = False\n        for uname, pw in list(acct.items())[:]:\n            if pw.startswith(\"+\") and len(pw) == 33:\n                continue\n\n            changed = True\n            hpw = self.ah.hash(pw)\n            acct[uname] = hpw\n            t = \"hashed password for account {}: {}\"\n            self.log(t.format(uname, hpw), 3)\n\n        if not changed:\n            return\n\n        lns = []\n        for uname, pw in acct.items():\n            lns.append(\"  {}: {}\".format(uname, pw))\n\n        t = \"please use the following hashed passwords in your config:\\n{}\"\n        self.log(t.format(\"\\n\".join(lns)), 3)\n\n    def chk_sqlite_threadsafe(self) -> str:\n        v = SQLITE_VER[-1:]\n\n        if v == \"1\":\n            # threadsafe (linux, windows)\n            return \"\"\n\n        if v == \"2\":\n            # module safe, connections unsafe (macos)\n            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\"\n\n        if v == \"0\":\n            # everything unsafe\n            return \"31m  your sqlite3 was compiled WITHOUT thread-safety!\\n   database features (-e2d, -e2t) will PROBABLY cause crashes!\"\n\n        return \"36m  cannot verify sqlite3 thread-safety; strange but probably fine\"\n\n    def dbg_ls(self) -> None:\n        users = self.args.ls\n        vol = \"*\"\n        flags: Sequence[str] = []\n\n        try:\n            users, vol = users.split(\",\", 1)\n        except:\n            pass\n\n        try:\n            vol, zf = vol.split(\",\", 1)\n            flags = zf.split(\",\")\n        except:\n            pass\n\n        if users == \"**\":\n            users = list(self.acct.keys()) + [\"*\"]\n        else:\n            users = [users]\n\n        for u in users:\n            if u not in self.acct and u != \"*\":\n                raise Exception(\"user not found: \" + u)\n\n        if vol == \"*\":\n            vols = [\"/\" + x for x in self.vfs.all_vols]\n        else:\n            vols = [vol]\n\n        for zs in vols:\n            if not zs.startswith(\"/\"):\n                raise Exception(\"volumes must start with /\")\n\n            if zs[1:] not in self.vfs.all_vols:\n                raise Exception(\"volume not found: \" + zs)\n\n        self.log(str({\"users\": users, \"vols\": vols, \"flags\": flags}))\n        t = \"/{}: read({}) write({}) move({}) del({}) dots({}) get({}) upGet({}) html({}) uadmin({})\"\n        for k, zv in self.vfs.all_vols.items():\n            vc = zv.axs\n            vs = [\n                k,\n                vc.uread,\n                vc.uwrite,\n                vc.umove,\n                vc.udel,\n                vc.udot,\n                vc.uget,\n                vc.upget,\n                vc.uhtml,\n                vc.uadmin,\n            ]\n            self.log(t.format(*vs))\n\n        flag_v = \"v\" in flags\n        flag_ln = \"ln\" in flags\n        flag_p = \"p\" in flags\n        flag_r = \"r\" in flags\n\n        bads = []\n        for v in vols:\n            v = v[1:]\n            vtop = \"/{}/\".format(v) if v else \"/\"\n            for u in users:\n                self.log(\"checking /{} as {}\".format(v, u))\n                try:\n                    vn, _ = self.vfs.get(v, u, True, False, False, False, False)\n                except:\n                    continue\n\n                atop = vn.realpath\n                safeabs = atop + os.sep\n                g = vn.walk(\n                    vn.vpath,\n                    \"\",\n                    [],\n                    u,\n                    [[True, False]],\n                    1,\n                    not self.args.no_scandir,\n                    False,\n                    False,\n                )\n                for _, _, vpath, apath, files1, dirs, _ in g:\n                    fnames = [n[0] for n in files1]\n                    zsl = [vpath + \"/\" + n for n in fnames] if vpath else fnames\n                    vpaths = [vtop + x for x in zsl]\n                    apaths = [os.path.join(apath, n) for n in fnames]\n                    files = [(vpath + \"/\", apath + os.sep)] + list(\n                        [(zs1, zs2) for zs1, zs2 in zip(vpaths, apaths)]\n                    )\n\n                    if flag_ln:\n                        files = [x for x in files if not x[1].startswith(safeabs)]\n                        if files:\n                            dirs[:] = []  # stop recursion\n                            bads.append(files[0][0])\n\n                    if not files:\n                        continue\n                    elif flag_v:\n                        ta = [\"\"] + [\n                            '# user \"{}\", vpath \"{}\"\\n{}'.format(u, vp, ap)\n                            for vp, ap in files\n                        ]\n                    else:\n                        ta = [\"user {}, vol {}: {} =>\".format(u, vtop, files[0][0])]\n                        ta += [x[1] for x in files]\n\n                    self.log(\"\\n\".join(ta))\n\n                if bads:\n                    self.log(\"\\n  \".join([\"found symlinks leaving volume:\"] + bads))\n\n                if bads and flag_p:\n                    raise Exception(\n                        \"\\033[31m\\n  [--ls] found a safety issue and prevented startup:\\n    found symlinks leaving volume, and strict is set\\n\\033[0m\"\n                    )\n\n        if not flag_r:\n            sys.exit(0)\n\n    def cgen(self) -> None:\n        ret = [\n            \"## WARNING:\",\n            \"##  there will probably be mistakes in\",\n            \"##  commandline-args (and maybe volflags)\",\n            \"\",\n        ]\n\n        csv = set(\"i p th_covers zm_on zm_off zs_on zs_off\".split())\n        zs = \"c ihead ohead mtm mtp on403 on404 xac xad xar xau xiu xban xbc xbd xbr xbu xm\"\n        lst = set(zs.split())\n        askip = set(\"a v c vc cgen exp_lg exp_md theme\".split())\n\n        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\"\n        fskip = set(t.split())\n\n        # keymap from argv to vflag\n        amap = vf_bmap()\n        amap.update(vf_vmap())\n        amap.update(vf_cmap())\n        vmap = {v: k for k, v in amap.items()}\n\n        args = {k: v for k, v in vars(self.args).items()}\n        pops = []\n        for k1, k2 in IMPLICATIONS:\n            if args.get(k1):\n                pops.append(k2)\n        for pop in pops:\n            args.pop(pop, None)\n\n        if args:\n            ret.append(\"[global]\")\n            for k, v in args.items():\n                if k in askip:\n                    continue\n\n                try:\n                    v = v.pattern\n                    if k in (\"idp_gsep\", \"tftp_lsf\"):\n                        v = v[1:-1]  # close enough\n                except:\n                    pass\n\n                skip = False\n                for k2, defstr in ((\"mte\", DEF_MTE), (\"mth\", DEF_MTH)):\n                    if k != k2:\n                        continue\n                    s1 = list(sorted(list(v)))\n                    s2 = list(sorted(defstr.split(\",\")))\n                    if s1 == s2:\n                        skip = True\n                        break\n                    v = \",\".join(s1)\n\n                if skip:\n                    continue\n\n                if k in csv:\n                    v = \", \".join([str(za) for za in v])\n                try:\n                    v2 = getattr(self.dargs, k)\n                    if k == \"tcolor\" and len(v2) == 3:\n                        v2 = \"\".join([x * 2 for x in v2])\n                    if v == v2 or v.replace(\", \", \",\") == v2:\n                        continue\n                except:\n                    continue\n\n                dk = \"  \" + k.replace(\"_\", \"-\")\n                if k in lst:\n                    for ve in v:\n                        ret.append(\"{}: {}\".format(dk, ve))\n                else:\n                    if v is True:\n                        ret.append(dk)\n                    elif v not in (False, None, \"\"):\n                        ret.append(\"{}: {}\".format(dk, v))\n            ret.append(\"\")\n\n        if self.acct:\n            ret.append(\"[accounts]\")\n            for u, p in self.acct.items():\n                ret.append(\"  {}: {}\".format(u, p))\n            ret.append(\"\")\n\n        if self.grps:\n            ret.append(\"[groups]\")\n            for gn, uns in self.grps.items():\n                ret.append(\"  %s: %s\" % (gn, \", \".join(uns)))\n            ret.append(\"\")\n\n        for vol in self.vfs.all_vols.values():\n            ret.append(\"[/{}]\".format(vol.vpath))\n            ret.append(\"  \" + vol.realpath)\n            ret.append(\"  accs:\")\n            perms = {\n                \"r\": \"uread\",\n                \"w\": \"uwrite\",\n                \"m\": \"umove\",\n                \"d\": \"udel\",\n                \".\": \"udot\",\n                \"g\": \"uget\",\n                \"G\": \"upget\",\n                \"h\": \"uhtml\",\n                \"a\": \"uadmin\",\n            }\n            users = {}\n            for pkey in perms.values():\n                for uname in getattr(vol.axs, pkey):\n                    try:\n                        users[uname] += 1\n                    except:\n                        users[uname] = 1\n            lusers = [(v, k) for k, v in users.items()]\n            vperms = {}\n            for _, uname in sorted(lusers):\n                pstr = \"\"\n                for pchar, pkey in perms.items():\n                    if uname in getattr(vol.axs, pkey):\n                        pstr += pchar\n                if \"g\" in pstr and \"G\" in pstr:\n                    pstr = pstr.replace(\"g\", \"\")\n                pstr = pstr.replace(\"rwmd.a\", \"A\")\n                try:\n                    vperms[pstr].append(uname)\n                except:\n                    vperms[pstr] = [uname]\n            for pstr, uname in vperms.items():\n                ret.append(\"    {}: {}\".format(pstr, \", \".join(uname)))\n            trues = []\n            vals = []\n            for k, v in sorted(vol.flags.items()):\n                if k in fskip:\n                    continue\n\n                try:\n                    v = v.pattern\n                except:\n                    pass\n\n                try:\n                    ak = vmap[k]\n                    v2 = getattr(self.args, ak)\n\n                    try:\n                        v2 = v2.pattern\n                    except:\n                        pass\n\n                    if v2 is v:\n                        continue\n                except:\n                    pass\n\n                skip = False\n                for k2, defstr in ((\"mte\", DEF_MTE), (\"mth\", DEF_MTH)):\n                    if k != k2:\n                        continue\n                    s1 = list(sorted(list(v)))\n                    s2 = list(sorted(defstr.split(\",\")))\n                    if s1 == s2:\n                        skip = True\n                        break\n                    v = \",\".join(s1)\n\n                if skip:\n                    continue\n\n                if k in lst:\n                    for ve in v:\n                        vals.append(\"{}: {}\".format(k, ve))\n                elif v is True:\n                    trues.append(k)\n                elif v is not False:\n                    vals.append(\"{}: {}\".format(k, v))\n            pops = []\n            for k1, k2 in IMPLICATIONS:\n                if k1 in trues:\n                    pops.append(k2)\n            trues = [x for x in trues if x not in pops]\n            if trues:\n                vals.append(\", \".join(trues))\n            if vals:\n                ret.append(\"  flags:\")\n                for zs in vals:\n                    ret.append(\"    \" + zs)\n            ret.append(\"\")\n\n        self.log(\"generated config:\\n\\n\" + \"\\n\".join(ret))\n\n\ndef derive_args(args: argparse.Namespace) -> None:\n    args.have_idp_hdrs = bool(args.idp_h_usr or args.idp_hm_usr)\n    args.have_ipu_or_ipr = bool(args.ipu or args.ipr)\n\n\ndef n_du_who(s: str) -> int:\n    if s == \"all\":\n        return 9\n    if s == \"auth\":\n        return 7\n    if s == \"w\":\n        return 5\n    if s == \"rw\":\n        return 4\n    if s == \"a\":\n        return 3\n    return 0\n\n\ndef n_ver_who(s: str) -> int:\n    if s == \"all\":\n        return 9\n    if s == \"auth\":\n        return 6\n    if s == \"a\":\n        return 3\n    return 0\n\n\ndef split_cfg_ln(ln: str) -> dict[str, Any]:\n    # \"a, b, c: 3\" => {a:true, b:true, c:3}\n    ret = {}\n    while True:\n        ln = ln.strip()\n        if not ln:\n            break\n        ofs_sep = ln.find(\",\") + 1\n        ofs_var = ln.find(\":\") + 1\n        if not ofs_sep and not ofs_var:\n            ret[ln] = True\n            break\n        if ofs_sep and (ofs_sep < ofs_var or not ofs_var):\n            k, ln = ln.split(\",\", 1)\n            ret[k.strip()] = True\n        else:\n            k, ln = ln.split(\":\", 1)\n            ret[k.strip()] = ln.strip()\n            break\n    return ret\n\n\ndef expand_config_file(\n    log: Optional[\"NamedLogger\"], ret: list[str], fp: str, ipath: str\n) -> None:\n    \"\"\"expand all % file includes\"\"\"\n    fp = absreal(fp)\n    if len(ipath.split(\" -> \")) > 64:\n        raise Exception(\"hit max depth of 64 includes\")\n\n    if os.path.isdir(fp):\n        names = list(sorted(os.listdir(fp)))\n        cnames = [\n            x for x in names if x.lower().endswith(\".conf\") and not x.startswith(\".\")\n        ]\n        if not cnames:\n            t = \"warning: tried to read config-files from folder '%s' but it does not contain any \"\n            if names:\n                t += \".conf files; the following files/subfolders were ignored: %s\"\n                t = t % (fp, \", \".join(names[:8]))\n            else:\n                t += \"files at all\"\n                t = t % (fp,)\n\n            if log:\n                log(t, 3)\n\n            ret.append(\"#\\033[33m %s\\033[0m\" % (t,))\n        else:\n            zs = \"#\\033[36m cfg files in %s => %s\\033[0m\" % (fp, cnames)\n            ret.append(zs)\n\n        for fn in cnames:\n            fp2 = os.path.join(fp, fn)\n            if fp2 in ipath:\n                continue\n\n            expand_config_file(log, ret, fp2, ipath)\n\n        return\n\n    if not os.path.exists(fp):\n        t = \"warning: tried to read config from '%s' but the file/folder does not exist\"\n        t = t % (fp,)\n        if log:\n            log(t, 3)\n\n        ret.append(\"#\\033[31m %s\\033[0m\" % (t,))\n        return\n\n    ipath += \" -> \" + fp\n    ret.append(\"#\\033[36m opening cfg file{}\\033[0m\".format(ipath))\n\n    cfg_lines = read_utf8(log, fp, True).replace(\"\\t\", \" \").split(\"\\n\")\n    if True:  # diff-golf\n        for oln in [x.rstrip() for x in cfg_lines]:\n            ln = oln.split(\"  #\")[0].strip()\n            if ln.startswith(\"% \"):\n                pad = \" \" * len(oln.split(\"%\")[0])\n                fp2 = ln[1:].strip()\n                fp2 = os.path.join(os.path.dirname(fp), fp2)\n                ofs = len(ret)\n                expand_config_file(log, ret, fp2, ipath)\n                for n in range(ofs, len(ret)):\n                    ret[n] = pad + ret[n]\n                continue\n\n            ret.append(oln)\n\n    ret.append(\"#\\033[36m closed{}\\033[0m\".format(ipath))\n\n    zsl = []\n    for ln in ret:\n        zs = ln.split(\"  #\")[0]\n        if \" #\" in zs and zs.split(\"#\")[0].strip():\n            zsl.append(ln)\n    if zsl and \"no-cfg-cmt-warn\" not in \"\\n\".join(ret):\n        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\"\n        t = t % (\"\\n>>> \".join(zsl),)\n        if log:\n            log(t)\n        else:\n            print(t, file=sys.stderr)\n\n\ndef upgrade_cfg_fmt(\n    log: Optional[\"NamedLogger\"], args: argparse.Namespace, orig: list[str], cfg_fp: str\n) -> list[str]:\n    \"\"\"convert from v1 to v2 format\"\"\"\n    zst = [x.split(\"#\")[0].strip() for x in orig]\n    zst = [x for x in zst if x]\n    if (\n        \"[global]\" in zst\n        or \"[accounts]\" in zst\n        or \"accs:\" in zst\n        or \"flags:\" in zst\n        or [x for x in zst if x.startswith(\"[/\")]\n        or len(zst) == len([x for x in zst if x.startswith(\"%\")])\n    ):\n        return orig\n\n    zst = [x for x in orig if \"#\\033[36m opening cfg file\" not in x]\n    incl = len(zst) != len(orig) - 1\n\n    t = \"upgrading config file [{}] from v1 to v2\"\n    if not args.vc:\n        t += \". Run with argument '--vc' to see the converted config if you want to upgrade\"\n    if incl:\n        t += \". Please don't include v1 configs from v2 files or vice versa! Upgrade all of them at the same time.\"\n    if log:\n        log(t.format(cfg_fp), 3)\n\n    ret = []\n    vp = \"\"\n    ap = \"\"\n    cat = \"\"\n    catg = \"[global]\"\n    cata = \"[accounts]\"\n    catx = \"  accs:\"\n    catf = \"  flags:\"\n    for ln in orig:\n        sn = ln.strip()\n        if not sn:\n            cat = vp = ap = \"\"\n        if not sn.split(\"#\")[0]:\n            ret.append(ln)\n        elif sn.startswith(\"-\") and cat in (\"\", catg):\n            if cat != catg:\n                cat = catg\n                ret.append(cat)\n            sn = sn.lstrip(\"-\")\n            zst = sn.split(\" \", 1)\n            if len(zst) > 1:\n                sn = \"{}: {}\".format(zst[0], zst[1].strip())\n            ret.append(\"  \" + sn)\n        elif sn.startswith(\"u \") and cat in (\"\", catg, cata):\n            if cat != cata:\n                cat = cata\n                ret.append(cat)\n            s1, s2 = sn[1:].split(\":\", 1)\n            ret.append(\"  {}: {}\".format(s1.strip(), s2.strip()))\n        elif not ap:\n            ap = sn\n        elif not vp:\n            vp = \"/\" + sn.strip(\"/\")\n            cat = \"[{}]\".format(vp)\n            ret.append(cat)\n            ret.append(\"  \" + ap)\n        elif sn.startswith(\"c \"):\n            if cat != catf:\n                cat = catf\n                ret.append(cat)\n            sn = sn[1:].strip()\n            if \"=\" in sn:\n                zst = sn.split(\"=\", 1)\n                sn = zst[0].replace(\",\", \", \")\n                sn += \": \" + zst[1]\n            else:\n                sn = sn.replace(\",\", \", \")\n            ret.append(\"    \" + sn)\n        elif sn[:1] in \"rwmdgGhaA.\":\n            if cat != catx:\n                cat = catx\n                ret.append(cat)\n            zst = sn.split(\" \")\n            zst = [x for x in zst if x]\n            if len(zst) == 1:\n                zst.append(\"*\")\n            ret.append(\"    {}: {}\".format(zst[0], \", \".join(zst[1:])))\n        else:\n            t = \"did not understand line {} in the config\"\n            t1 = t\n            n = 0\n            for ln in orig:\n                n += 1\n                t += \"\\n{:4} {}\".format(n, ln)\n            if log:\n                log(t, 1)\n            else:\n                print(\"\\033[31m\" + t)\n            raise Exception(t1)\n\n    if args.vc and log:\n        t = \"new config syntax (copy/paste this to upgrade your config):\\n\"\n        t += \"\\n# ======================[ begin upgraded config ]======================\\n\\n\"\n        for ln in ret:\n            t += ln + \"\\n\"\n        t += \"\\n# ======================[ end of upgraded config ]======================\\n\"\n        log(t)\n\n    return ret\n"
  },
  {
    "path": "copyparty/bos/__init__.py",
    "content": ""
  },
  {
    "path": "copyparty/bos/bos.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport os\nimport time\n\nfrom ..util import SYMTIME, fsdec, fsenc\nfrom . import path as path\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Any, Optional, Union\n\n    from ..util import NamedLogger\n\nMKD_755 = {\"chmod_d\": 0o755}\nMKD_700 = {\"chmod_d\": 0o700}\nUTIME_CLAMPS = ((max, -2147483647), (max, 1), (min, 4294967294), (min, 2147483646))\n\n_ = (path, MKD_755, MKD_700, UTIME_CLAMPS)\n__all__ = [\"path\", \"MKD_755\", \"MKD_700\", \"UTIME_CLAMPS\"]\n\n# grep -hRiE '(^|[^a-zA-Z_\\.-])os\\.' . | gsed -r 's/ /\\n/g;s/\\(/(\\n/g' | grep -hRiE '(^|[^a-zA-Z_\\.-])os\\.' | sort | uniq -c\n# printf 'os\\.(%s)' \"$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\\(.*//' | tr '\\n' '|' | gsed -r 's/.$//')\"\n\n\ndef chmod(p: str, mode: int) -> None:\n    return os.chmod(fsenc(p), mode)\n\n\ndef chown(p: str, uid: int, gid: int) -> None:\n    return os.chown(fsenc(p), uid, gid)\n\n\ndef listdir(p: str = \".\") -> list[str]:\n    return [fsdec(x) for x in os.listdir(fsenc(p))]\n\n\ndef makedirs(name: str, vf: dict[str, Any] = MKD_755, exist_ok: bool = True) -> bool:\n    # os.makedirs does 777 for all but leaf; this does mode on all\n    todo = []\n    bname = fsenc(name)\n    while bname:\n        if os.path.isdir(bname) or bname in todo:\n            break\n        todo.append(bname)\n        bname = os.path.dirname(bname)\n    if not todo:\n        if not exist_ok:\n            os.mkdir(bname)  # to throw\n        return False\n    mode = vf[\"chmod_d\"]\n    chown = \"chown\" in vf\n    for zb in todo[::-1]:\n        try:\n            os.mkdir(zb, mode)\n            if chown:\n                os.chown(zb, vf[\"uid\"], vf[\"gid\"])\n        except:\n            if os.path.isdir(zb):\n                continue\n            raise\n    return True\n\n\ndef mkdir(p: str, mode: int = 0o755) -> None:\n    return os.mkdir(fsenc(p), mode)\n\n\ndef open(p: str, *a, **ka) -> int:\n    return os.open(fsenc(p), *a, **ka)\n\n\ndef readlink(p: str) -> str:\n    return fsdec(os.readlink(fsenc(p)))\n\n\ndef rename(src: str, dst: str) -> None:\n    return os.rename(fsenc(src), fsenc(dst))\n\n\ndef replace(src: str, dst: str) -> None:\n    return os.replace(fsenc(src), fsenc(dst))\n\n\ndef rmdir(p: str) -> None:\n    return os.rmdir(fsenc(p))\n\n\ndef stat(p: str) -> os.stat_result:\n    return os.stat(fsenc(p))\n\n\ndef unlink(p: str) -> None:\n    return os.unlink(fsenc(p))\n\n\ndef utime(\n    p: str, times: Optional[tuple[float, float]] = None, follow_symlinks: bool = True\n) -> None:\n    if SYMTIME:\n        return os.utime(fsenc(p), times, follow_symlinks=follow_symlinks)\n    else:\n        return os.utime(fsenc(p), times)\n\n\ndef utime_c(\n    log: Union[\"NamedLogger\", Any],\n    p: str,\n    ts: float,\n    follow_symlinks: bool = True,\n    throw: bool = False,\n) -> Optional[float]:\n    clamp = 0\n    ov = ts\n    bp = fsenc(p)\n    now = time.time()\n    while True:\n        try:\n            if SYMTIME:\n                os.utime(bp, (now, ts), follow_symlinks=follow_symlinks)\n            else:\n                os.utime(bp, (now, ts))\n            if clamp:\n                t = \"filesystem rejected utime(%r); clamped %s to %s\"\n                log(t % (p, ov, ts))\n            return ts\n        except Exception as ex:\n            pv = ts\n            while clamp < len(UTIME_CLAMPS):\n                fun, cv = UTIME_CLAMPS[clamp]\n                ts = fun(ts, cv)\n                clamp += 1\n                if ts != pv:\n                    break\n            if clamp >= len(UTIME_CLAMPS):\n                if throw:\n                    raise\n                else:\n                    t = \"could not utime(%r) to %s; %s, %r\"\n                    log(t % (p, ov, ex, ex))\n                    return None\n\n\nif hasattr(os, \"lstat\"):\n\n    def lstat(p: str) -> os.stat_result:\n        return os.lstat(fsenc(p))\n\nelse:\n    lstat = stat\n"
  },
  {
    "path": "copyparty/bos/path.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport os\n\nfrom ..util import SYMTIME, fsdec, fsenc\n\n\ndef abspath(p: str) -> str:\n    return fsdec(os.path.abspath(fsenc(p)))\n\n\ndef exists(p: str) -> bool:\n    return os.path.exists(fsenc(p))\n\n\ndef getmtime(p: str, follow_symlinks: bool = True) -> float:\n    if not follow_symlinks and SYMTIME:\n        return os.lstat(fsenc(p)).st_mtime\n    else:\n        return os.path.getmtime(fsenc(p))\n\n\ndef getsize(p: str) -> int:\n    return os.path.getsize(fsenc(p))\n\n\ndef isfile(p: str) -> bool:\n    return os.path.isfile(fsenc(p))\n\n\ndef isdir(p: str) -> bool:\n    return os.path.isdir(fsenc(p))\n\n\ndef islink(p: str) -> bool:\n    return os.path.islink(fsenc(p))\n\n\ndef lexists(p: str) -> bool:\n    return os.path.lexists(fsenc(p))\n\n\ndef realpath(p: str) -> str:\n    return fsdec(os.path.realpath(fsenc(p)))\n"
  },
  {
    "path": "copyparty/broker_mp.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport threading\nimport time\nimport traceback\n\nimport queue\n\nfrom .__init__ import CORES, TYPE_CHECKING\nfrom .broker_mpw import MpWorker\nfrom .broker_util import ExceptionalQueue, NotExQueue, try_exec\nfrom .util import Daemon, mp\n\nif TYPE_CHECKING:\n    from .svchub import SvcHub\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Any, Union\n\n\nclass MProcess(mp.Process):\n    def __init__(\n        self,\n        q_pend: queue.Queue[tuple[int, str, list[Any]]],\n        q_yield: queue.Queue[tuple[int, str, list[Any]]],\n        target: Any,\n        args: Any,\n    ) -> None:\n        super(MProcess, self).__init__(target=target, args=args)\n        self.q_pend = q_pend\n        self.q_yield = q_yield\n\n\nclass BrokerMp(object):\n    \"\"\"external api; manages MpWorkers\"\"\"\n\n    def __init__(self, hub: \"SvcHub\") -> None:\n        self.hub = hub\n        self.log = hub.log\n        self.args = hub.args\n\n        self.procs = []\n        self.mutex = threading.Lock()\n\n        self.retpend: dict[int, Any] = {}\n        self.retpend_mutex = threading.Lock()\n\n        self.num_workers = self.args.j or CORES\n        self.log(\"broker\", \"booting {} subprocesses\".format(self.num_workers))\n        for n in range(1, self.num_workers + 1):\n            q_pend: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(1)  # type: ignore\n            q_yield: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(64)  # type: ignore\n\n            proc = MProcess(q_pend, q_yield, MpWorker, (q_pend, q_yield, self.args, n))\n            Daemon(self.collector, \"mp-sink-{}\".format(n), (proc,))\n            self.procs.append(proc)\n            proc.start()\n\n        Daemon(self.periodic, \"mp-periodic\")\n\n    def shutdown(self) -> None:\n        self.log(\"broker\", \"shutting down\")\n        for n, proc in enumerate(self.procs):\n            name = \"mp-shut-%d-%d\" % (n, len(self.procs))\n            Daemon(proc.q_pend.put, name, ((0, \"shutdown\", []),))\n\n        with self.mutex:\n            procs = self.procs\n            self.procs = []\n\n        while procs:\n            if procs[-1].is_alive():\n                time.sleep(0.05)\n                continue\n\n            procs.pop()\n\n    def reload(self) -> None:\n        self.log(\"broker\", \"reloading\")\n        for _, proc in enumerate(self.procs):\n            proc.q_pend.put((0, \"reload\", []))\n\n    def reload_sessions(self) -> None:\n        for _, proc in enumerate(self.procs):\n            proc.q_pend.put((0, \"reload_sessions\", []))\n\n    def collector(self, proc: MProcess) -> None:\n        \"\"\"receive message from hub in other process\"\"\"\n        while True:\n            msg = proc.q_yield.get()\n            retq_id, dest, args = msg\n\n            if dest == \"log\":\n                self.log(*args)\n\n            elif dest == \"retq\":\n                with self.retpend_mutex:\n                    retq = self.retpend.pop(retq_id)\n\n                retq.put(args[0])\n\n            else:\n                # new ipc invoking managed service in hub\n                try:\n                    obj = self.hub\n                    for node in dest.split(\".\"):\n                        obj = getattr(obj, node)\n\n                    # TODO will deadlock if dest performs another ipc\n                    rv = try_exec(retq_id, obj, *args)\n                except:\n                    rv = [\"exception\", \"stack\", traceback.format_exc()]\n\n                if retq_id:\n                    proc.q_pend.put((retq_id, \"retq\", rv))\n\n    def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]:\n        # new non-ipc invoking managed service in hub\n        obj = self.hub\n        for node in dest.split(\".\"):\n            obj = getattr(obj, node)\n\n        rv = try_exec(True, obj, *args)\n\n        retq = ExceptionalQueue(1)\n        retq.put(rv)\n        return retq\n\n    def wask(self, dest: str, *args: Any) -> list[Union[ExceptionalQueue, NotExQueue]]:\n        # call from hub to workers\n        ret = []\n        for p in self.procs:\n            retq = ExceptionalQueue(1)\n            retq_id = id(retq)\n            with self.retpend_mutex:\n                self.retpend[retq_id] = retq\n\n            p.q_pend.put((retq_id, dest, list(args)))\n            ret.append(retq)\n        return ret\n\n    def say(self, dest: str, *args: Any) -> None:\n        \"\"\"\n        send message to non-hub component in other process,\n        returns a Queue object which eventually contains the response if want_retval\n        (not-impl here since nothing uses it yet)\n        \"\"\"\n        if dest == \"httpsrv.listen\":\n            for p in self.procs:\n                p.q_pend.put((0, dest, [args[0], len(self.procs)]))\n\n        elif dest == \"httpsrv.set_netdevs\":\n            for p in self.procs:\n                p.q_pend.put((0, dest, list(args)))\n\n        elif dest == \"cb_httpsrv_up\":\n            self.hub.cb_httpsrv_up()\n\n        elif dest == \"httpsrv.set_bad_ver\":\n            for p in self.procs:\n                p.q_pend.put((0, dest, list(args)))\n\n        else:\n            raise Exception(\"what is \" + str(dest))\n\n    def periodic(self) -> None:\n        while True:\n            time.sleep(1)\n\n            tdli = {}\n            tdls = {}\n            qs = self.wask(\"httpsrv.read_dls\")\n            for q in qs:\n                qr = q.get()\n                dli, dls = qr\n                tdli.update(dli)\n                tdls.update(dls)\n            tdl = (tdli, tdls)\n            for p in self.procs:\n                p.q_pend.put((0, \"httpsrv.write_dls\", tdl))\n"
  },
  {
    "path": "copyparty/broker_mpw.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport argparse\nimport os\nimport signal\nimport sys\nimport threading\n\nimport queue\n\nfrom .__init__ import ANYWIN\nfrom .authsrv import AuthSrv\nfrom .broker_util import BrokerCli, ExceptionalQueue, NotExQueue\nfrom .fsutil import ramdisk_chk\nfrom .httpsrv import HttpSrv\nfrom .util import FAKE_MP, Daemon, HMaccas\n\nif True:  # pylint: disable=using-constant-test\n    from types import FrameType\n\n    from typing import Any, Optional, Union\n\n\nclass MpWorker(BrokerCli):\n    \"\"\"one single mp instance\"\"\"\n\n    def __init__(\n        self,\n        q_pend: queue.Queue[tuple[int, str, list[Any]]],\n        q_yield: queue.Queue[tuple[int, str, list[Any]]],\n        args: argparse.Namespace,\n        n: int,\n    ) -> None:\n        super(MpWorker, self).__init__()\n\n        self.q_pend = q_pend\n        self.q_yield = q_yield\n        self.args = args\n        self.n = n\n\n        self.log = self._log_disabled if args.q and not args.lo else self._log_enabled\n\n        self.retpend: dict[int, Any] = {}\n        self.retpend_mutex = threading.Lock()\n        self.mutex = threading.Lock()\n\n        # we inherited signal_handler from parent,\n        # replace it with something harmless\n        if not FAKE_MP:\n            sigs = [signal.SIGINT, signal.SIGTERM]\n            if not ANYWIN:\n                sigs.append(signal.SIGUSR1)\n\n            for sig in sigs:\n                signal.signal(sig, self.signal_handler)\n\n        # starting to look like a good idea\n        self.asrv = AuthSrv(args, None, False)\n        ramdisk_chk(self.asrv)\n\n        # instantiate all services here (TODO: inheritance?)\n        self.iphash = HMaccas(os.path.join(self.args.E.cfg, \"iphash\"), 8)\n        self.httpsrv = HttpSrv(self, n)\n\n        # on winxp and some other platforms,\n        # use thr.join() to block all signals\n        Daemon(self.main, \"mpw-main\").join()\n\n    def signal_handler(self, sig: Optional[int], frame: Optional[FrameType]) -> None:\n        # print('k')\n        pass\n\n    def _log_enabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None:\n        self.q_yield.put((0, \"log\", [src, msg, c]))\n\n    def _log_disabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None:\n        pass\n\n    def logw(self, msg: str, c: Union[int, str] = 0) -> None:\n        self.log(\"mp%d\" % (self.n,), msg, c)\n\n    def main(self) -> None:\n        while True:\n            retq_id, dest, args = self.q_pend.get()\n\n            if dest == \"retq\":\n                # response from previous ipc call\n                with self.retpend_mutex:\n                    retq = self.retpend.pop(retq_id)\n\n                retq.put(args)\n                continue\n\n            if dest == \"shutdown\":\n                self.httpsrv.shutdown()\n                self.logw(\"ok bye\")\n                sys.exit(0)\n                return\n\n            if dest == \"reload\":\n                self.logw(\"mpw.asrv reloading\")\n                self.asrv.reload()\n                ramdisk_chk(self.asrv)\n                self.logw(\"mpw.asrv reloaded\")\n                continue\n\n            if dest == \"reload_sessions\":\n                with self.asrv.mutex:\n                    self.asrv.load_sessions()\n                continue\n\n            obj = self\n            for node in dest.split(\".\"):\n                obj = getattr(obj, node)\n\n            rv = obj(*args)  # type: ignore\n            if retq_id:\n                self.say(\"retq\", rv, retq_id=retq_id)\n\n    def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]:\n        retq = ExceptionalQueue(1)\n        retq_id = id(retq)\n        with self.retpend_mutex:\n            self.retpend[retq_id] = retq\n\n        self.q_yield.put((retq_id, dest, list(args)))\n        return retq\n\n    def say(self, dest: str, *args: Any, retq_id=0) -> None:\n        self.q_yield.put((retq_id, dest, list(args)))\n"
  },
  {
    "path": "copyparty/broker_thr.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport os\nimport threading\n\nfrom .__init__ import TYPE_CHECKING\nfrom .broker_util import BrokerCli, ExceptionalQueue, NotExQueue\nfrom .httpsrv import HttpSrv\nfrom .util import HMaccas\n\nif TYPE_CHECKING:\n    from .svchub import SvcHub\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Any, Union\n\n\nclass BrokerThr(BrokerCli):\n    \"\"\"external api; behaves like BrokerMP but using plain threads\"\"\"\n\n    def __init__(self, hub: \"SvcHub\") -> None:\n        super(BrokerThr, self).__init__()\n\n        self.hub = hub\n        self.log = hub.log\n        self.args = hub.args\n        self.asrv = hub.asrv\n\n        self.mutex = threading.Lock()\n        self.num_workers = 1\n\n        # instantiate all services here (TODO: inheritance?)\n        self.iphash = HMaccas(os.path.join(self.args.E.cfg, \"iphash\"), 8)\n        self.httpsrv = HttpSrv(self, None)\n        self.reload = self.noop\n        self.reload_sessions = self.noop\n\n    def shutdown(self) -> None:\n        # self.log(\"broker\", \"shutting down\")\n        self.httpsrv.shutdown()\n\n    def noop(self) -> None:\n        pass\n\n    def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]:\n\n        # new ipc invoking managed service in hub\n        obj = self.hub\n        for node in dest.split(\".\"):\n            obj = getattr(obj, node)\n\n        return NotExQueue(obj(*args))  # type: ignore\n\n    def say(self, dest: str, *args: Any) -> None:\n        if dest.startswith(\"httpsrv.\"):\n            if dest == \"httpsrv.listen\":\n                self.httpsrv.listen(args[0], 1)\n                return\n\n            if dest == \"httpsrv.set_netdevs\":\n                self.httpsrv.set_netdevs(args[0])\n                return\n\n            if dest == \"httpsrv.set_bad_ver\":\n                self.httpsrv.set_bad_ver()\n                return\n\n        # new ipc invoking managed service in hub\n        obj = self.hub\n        for node in dest.split(\".\"):\n            obj = getattr(obj, node)\n\n        obj(*args)  # type: ignore\n"
  },
  {
    "path": "copyparty/broker_util.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport argparse\n\nfrom queue import Queue\n\nfrom .__init__ import TYPE_CHECKING\nfrom .authsrv import AuthSrv\nfrom .util import HMaccas, Pebkac\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Any, Optional, Union\n\n    from .util import RootLogger\n\nif TYPE_CHECKING:\n    from .httpsrv import HttpSrv\n\n\nclass ExceptionalQueue(Queue, object):\n    def get(self, block: bool = True, timeout: Optional[float] = None) -> Any:\n        rv = super(ExceptionalQueue, self).get(block, timeout)\n\n        if isinstance(rv, list):\n            if rv[0] == \"exception\":\n                if rv[1] == \"pebkac\":\n                    raise Pebkac(*rv[2:])\n                else:\n                    raise rv[2]\n\n        return rv\n\n\nclass NotExQueue(object):\n    \"\"\"\n    BrokerThr uses this instead of ExceptionalQueue; 7x faster\n    \"\"\"\n\n    def __init__(self, rv: Any) -> None:\n        self.rv = rv\n\n    def get(self) -> Any:\n        return self.rv\n\n\nclass BrokerCli(object):\n    \"\"\"\n    helps mypy understand httpsrv.broker but still fails a few levels deeper,\n    for example resolving httpconn.* in httpcli -- see lines tagged #mypy404\n    \"\"\"\n\n    log: \"RootLogger\"\n    args: argparse.Namespace\n    asrv: AuthSrv\n    httpsrv: \"HttpSrv\"\n    iphash: HMaccas\n\n    def __init__(self) -> None:\n        pass\n\n    def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]:\n        return ExceptionalQueue(1)\n\n    def say(self, dest: str, *args: Any) -> None:\n        pass\n\n\ndef try_exec(want_retval: Union[bool, int], func: Any, *args: list[Any]) -> Any:\n    try:\n        return func(*args)\n\n    except Pebkac as ex:\n        if not want_retval:\n            raise\n\n        return [\"exception\", \"pebkac\", ex.code, str(ex)]\n\n    except Exception as ex:\n        if not want_retval:\n            raise\n\n        return [\"exception\", \"stack\", ex]\n"
  },
  {
    "path": "copyparty/cert.py",
    "content": "import calendar\nimport errno\nimport json\nimport os\nimport shutil\nimport time\n\nfrom .__init__ import ANYWIN\nfrom .util import Netdev, atomic_move, load_resource, runcmd, wunlink\n\nHAVE_CFSSL = not os.environ.get(\"PRTY_NO_CFSSL\")\n\nif True:  # pylint: disable=using-constant-test\n    from .util import NamedLogger, RootLogger\n\n\nif ANYWIN:\n    VF = {\"mv_re_t\": 5, \"rm_re_t\": 5, \"mv_re_r\": 0.1, \"rm_re_r\": 0.1}\nelse:\n    VF = {\"mv_re_t\": 0, \"rm_re_t\": 0}\n\n\ndef _sp_err(exe, what, rc, so, se, sin):\n    try:\n        zs = shutil.which(exe)\n    except:\n        zs = \"<?>\"\n    try:\n        zi = os.path.getsize(zs)\n    except:\n        zi = 0\n    t = \"failed to %s; error %s using %s (%s):\\n  STDOUT: %s\\n  STDERR: %s\\n  STDIN: %s\\n\"\n    raise Exception(t % (what, rc, zs, zi, so, se, sin.decode(\"utf-8\")))\n\n\ndef ensure_cert(log: \"RootLogger\", args) -> None:\n    \"\"\"\n    the default cert (and the entire TLS support) is only here to enable the\n    crypto.subtle javascript API, which is necessary due to the webkit guys\n    being massive memers (https://www.chromium.org/blink/webcrypto)\n\n    i feel awful about this and so should they\n    \"\"\"\n    with load_resource(args.E, \"res/insecure.pem\") as f:\n        cert_insec = f.read()\n    cert_appdata = os.path.join(args.E.cfg, \"cert.pem\")\n    if not os.path.isfile(args.cert):\n        if cert_appdata != args.cert:\n            raise Exception(\"certificate file does not exist: \" + args.cert)\n\n        with open(args.cert, \"wb\") as f:\n            f.write(cert_insec)\n\n    with open(args.cert, \"rb\") as f:\n        buf = f.read()\n        o1 = buf.find(b\" PRIVATE KEY-\")\n        o2 = buf.find(b\" CERTIFICATE-\")\n        m = \"unsupported certificate format: \"\n        if o1 < 0:\n            raise Exception(m + \"no private key inside pem\")\n        if o2 < 0:\n            raise Exception(m + \"no server certificate inside pem\")\n        if o1 > o2:\n            raise Exception(m + \"private key must appear before server certificate\")\n\n    try:\n        with open(args.cert, \"rb\") as f:\n            active_cert = f.read()\n        if active_cert == cert_insec:\n            t = \"using default TLS certificate; https will be insecure:\\033[36m {}\"\n            log(\"cert\", t.format(args.cert), 3)\n    except:\n        pass\n\n    # speaking of the default cert,\n    # 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\n\n\ndef _read_crt(args, fn):\n    try:\n        if not os.path.exists(os.path.join(args.crt_dir, fn)):\n            return 0, {}\n\n        acmd = [\"cfssl-certinfo\", \"-cert\", fn]\n        rc, so, se = runcmd(acmd, cwd=args.crt_dir)\n        if rc:\n            return 0, {}\n\n        inf = json.loads(so)\n        zs = inf[\"not_after\"]\n        expiry = calendar.timegm(time.strptime(zs, \"%Y-%m-%dT%H:%M:%SZ\"))\n        return expiry, inf\n    except OSError as ex:\n        if ex.errno == errno.ENOENT:\n            raise\n        return 0, {}\n    except:\n        return 0, {}\n\n\ndef _gen_ca(log: \"RootLogger\", args):\n    nlog: \"NamedLogger\" = lambda msg, c=0: log(\"cert-gen-ca\", msg, c)\n\n    expiry = _read_crt(args, \"ca.pem\")[0]\n    if time.time() + args.crt_cdays * 60 * 60 * 24 * 0.1 < expiry:\n        return\n\n    backdate = \"{}m\".format(int(args.crt_back * 60))\n    expiry = \"{}m\".format(int(args.crt_cdays * 60 * 24))\n    cn = args.crt_cnc.replace(\"--crt-cn\", args.crt_cn)\n    algo, ksz = args.crt_alg.split(\"-\")\n    req = {\n        \"CN\": cn,\n        \"CA\": {\"backdate\": backdate, \"expiry\": expiry, \"pathlen\": 0},\n        \"key\": {\"algo\": algo, \"size\": int(ksz)},\n        \"names\": [{\"O\": cn}],\n    }\n    sin = json.dumps(req).encode(\"utf-8\")\n    log(\"cert\", \"creating new ca ...\", 6)\n\n    cmd = \"cfssl gencert -initca -\"\n    rc, so, se = runcmd(cmd.split(), 30, sin=sin)\n    if rc:\n        _sp_err(\"cfssl\", \"create ca-cert\", rc, so, se, sin)\n\n    cmd = \"cfssljson -bare ca\"\n    sin = so.encode(\"utf-8\")\n    rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir)\n    if rc:\n        _sp_err(\"cfssljson\", \"translate ca-cert\", rc, so, se, sin)\n\n    bname = os.path.join(args.crt_dir, \"ca\")\n    try:\n        wunlink(nlog, bname + \".key\", VF)\n    except:\n        pass\n    atomic_move(nlog, bname + \"-key.pem\", bname + \".key\", VF)\n    wunlink(nlog, bname + \".csr\", VF)\n\n    log(\"cert\", \"new ca OK\", 2)\n\n\ndef _gen_srv(log: \"RootLogger\", args, netdevs: dict[str, Netdev]):\n    nlog: \"NamedLogger\" = lambda msg, c=0: log(\"cert-gen-srv\", msg, c)\n\n    names = args.crt_ns.split(\",\") if args.crt_ns else []\n    names = [x.strip() for x in names]\n    if not args.crt_exact:\n        for n in names[:]:\n            names.append(\"*.{}\".format(n))\n    if not args.crt_noip:\n        for ip in netdevs.keys():\n            names.append(ip.split(\"/\")[0])\n    if args.crt_nolo:\n        names = [x for x in names if x not in (\"localhost\", \"127.0.0.1\", \"::1\")]\n    if not args.crt_nohn:\n        names.append(args.name)\n        names.append(args.name + \".local\")\n    if not names:\n        names = [\"127.0.0.1\"]\n    if \"127.0.0.1\" in names or \"::1\" in names:\n        names.append(\"localhost\")\n    names = list({x: 1 for x in names}.keys())\n\n    try:\n        expiry, inf = _read_crt(args, \"srv.pem\")\n        if \"sans\" not in inf:\n            raise Exception(\"no useable cert found\")\n\n        expired = time.time() + args.crt_sdays * 60 * 60 * 24 * 0.5 > expiry\n        if expired:\n            raise Exception(\"old server-cert has expired\")\n\n        for n in names:\n            if n not in inf[\"sans\"]:\n                raise Exception(\"does not have {}\".format(n))\n\n        with load_resource(args.E, \"res/insecure.pem\") as f:\n            cert_insec = f.read()\n\n        with open(args.cert, \"rb\") as f:\n            active_cert = f.read()\n\n        if active_cert and active_cert != cert_insec:\n            return\n\n    except Exception as ex:\n        log(\"cert\", \"will create new server-cert; {}\".format(ex))\n\n    log(\"cert\", \"creating server-cert ...\", 6)\n\n    backdate = \"{}m\".format(int(args.crt_back * 60))\n    expiry = \"{}m\".format(int(args.crt_sdays * 60 * 24))\n    cfg = {\n        \"signing\": {\n            \"default\": {\n                \"backdate\": backdate,\n                \"expiry\": expiry,\n                \"usages\": [\"signing\", \"key encipherment\", \"server auth\"],\n            }\n        }\n    }\n    with open(os.path.join(args.crt_dir, \"cfssl.json\"), \"wb\") as f:\n        f.write(json.dumps(cfg).encode(\"utf-8\"))\n\n    cn = args.crt_cns.replace(\"--crt-cn\", args.crt_cn)\n    algo, ksz = args.crt_alg.split(\"-\")\n    req = {\n        \"key\": {\"algo\": algo, \"size\": int(ksz)},\n        \"names\": [{\"O\": cn}],\n    }\n    sin = json.dumps(req).encode(\"utf-8\")\n\n    cmd = \"cfssl gencert -config=cfssl.json -ca ca.pem -ca-key ca.key -profile=www\"\n    acmd = cmd.split() + [\"-hostname=\" + \",\".join(names), \"-\"]\n    rc, so, se = runcmd(acmd, 30, sin=sin, cwd=args.crt_dir)\n    if rc:\n        _sp_err(\"cfssl\", \"create cert\", rc, so, se, sin)\n\n    cmd = \"cfssljson -bare srv\"\n    sin = so.encode(\"utf-8\")\n    rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir)\n    if rc:\n        _sp_err(\"cfssljson\", \"translate cert\", rc, so, se, sin)\n\n    bname = os.path.join(args.crt_dir, \"srv\")\n    try:\n        wunlink(nlog, bname + \".key\", VF)\n    except:\n        pass\n    atomic_move(nlog, bname + \"-key.pem\", bname + \".key\", VF)\n    wunlink(nlog, bname + \".csr\", VF)\n\n    with open(os.path.join(args.crt_dir, \"ca.pem\"), \"rb\") as f:\n        ca = f.read()\n\n    with open(bname + \".key\", \"rb\") as f:\n        skey = f.read()\n\n    with open(bname + \".pem\", \"rb\") as f:\n        scrt = f.read()\n\n    with open(args.cert, \"wb\") as f:\n        f.write(skey + scrt + ca)\n\n    log(\"cert\", \"new server-cert OK\", 2)\n\n\ndef gencert(log: \"RootLogger\", args, netdevs: dict[str, Netdev]):\n    global HAVE_CFSSL\n\n    if args.http_only:\n        return\n\n    if args.no_crt or not HAVE_CFSSL:\n        ensure_cert(log, args)\n        return\n\n    try:\n        _gen_ca(log, args)\n        _gen_srv(log, args, netdevs)\n    except Exception as ex:\n        HAVE_CFSSL = False\n        log(\"cert\", \"could not create TLS certificates: {}\".format(ex), 3)\n        if getattr(ex, \"errno\", 0) == errno.ENOENT:\n            t = \"install cfssl if you want to fix this; https://github.com/cloudflare/cfssl/releases/latest  (cfssl, cfssljson, cfssl-certinfo)\"\n            log(\"cert\", t, 6)\n\n        ensure_cert(log, args)\n"
  },
  {
    "path": "copyparty/cfg.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\n# awk -F\\\" '/add_argument\\(\"-[^-]/{print(substr($2,2))}' copyparty/__main__.py | sort | tr '\\n' ' '\nzs = \"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\"\nonedash = set(zs.split())\n\n# verify that all volflags are documented here:\n# 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\n\n\ndef vf_bmap() -> dict[str, str]:\n    \"\"\"argv-to-volflag: simple bools\"\"\"\n    ret = {\n        \"dav_auth\": \"davauth\",\n        \"dav_rt\": \"davrt\",\n        \"ed\": \"dots\",\n        \"hardlink_only\": \"hardlinkonly\",\n        \"no_clone\": \"noclone\",\n        \"no_dirsz\": \"nodirsz\",\n        \"no_dupe\": \"nodupe\",\n        \"no_dupe_m\": \"nodupem\",\n        \"no_forget\": \"noforget\",\n        \"no_pipe\": \"nopipe\",\n        \"no_robots\": \"norobots\",\n        \"no_tail\": \"notail\",\n        \"no_thumb\": \"dthumb\",\n        \"no_vthumb\": \"dvthumb\",\n        \"no_athumb\": \"dathumb\",\n        \"vol_nospawn\": \"nospawn\",\n        \"vol_or_crash\": \"assert_root\",\n    }\n    for k in (\n        \"dedup\",\n        \"dlni\",\n        \"dotsrch\",\n        \"e2d\",\n        \"e2ds\",\n        \"e2dsa\",\n        \"e2t\",\n        \"e2ts\",\n        \"e2tsr\",\n        \"e2v\",\n        \"e2vu\",\n        \"e2vp\",\n        \"exp\",\n        \"grid\",\n        \"gsel\",\n        \"hardlink\",\n        \"magic\",\n        \"md_no_br\",\n        \"no_db_ip\",\n        \"no_sb_md\",\n        \"no_sb_lg\",\n        \"nsort\",\n        \"og\",\n        \"og_no_head\",\n        \"og_s_title\",\n        \"opds\",\n        \"rand\",\n        \"reflink\",\n        \"rm_partial\",\n        \"rmagic\",\n        \"rss\",\n        \"ui_noacci\",\n        \"ui_nocpla\",\n        \"ui_nolbar\",\n        \"ui_nombar\",\n        \"ui_nonav\",\n        \"ui_notree\",\n        \"ui_norepl\",\n        \"ui_nosrvi\",\n        \"ui_noctxb\",\n        \"wo_up_readme\",\n        \"wram\",\n        \"xdev\",\n        \"xlink\",\n        \"xvol\",\n        \"zipmaxu\",\n    ):\n        ret[k] = k\n    return ret\n\n\ndef vf_vmap() -> dict[str, str]:\n    \"\"\"argv-to-volflag: simple values\"\"\"\n    ret = {\n        \"ac_convt\": \"aconvt\",\n        \"no_hash\": \"nohash\",\n        \"no_idx\": \"noidx\",\n        \"re_maxage\": \"scan\",\n        \"safe_dedup\": \"safededup\",\n        \"th_convt\": \"convt\",\n        \"th_size\": \"thsize\",\n        \"th_crop\": \"crop\",\n        \"th_x3\": \"th3x\",\n    }\n    for k in (\n        \"apnd_who\",\n        \"bup_ck\",\n        \"cachectl\",\n        \"casechk\",\n        \"chmod_d\",\n        \"chmod_f\",\n        \"dbd\",\n        \"db_xattr\",\n        \"du_who\",\n        \"epilogues\",\n        \"ufavico\",\n        \"forget_ip\",\n        \"fsnt\",\n        \"hsortn\",\n        \"html_head\",\n        \"html_head_s\",\n        \"lg_sbf\",\n        \"md_sbf\",\n        \"lg_sba\",\n        \"md_sba\",\n        \"md_hist\",\n        \"nrand\",\n        \"u2ow\",\n        \"og_desc\",\n        \"og_site\",\n        \"og_th\",\n        \"og_title\",\n        \"og_title_a\",\n        \"og_title_v\",\n        \"og_title_i\",\n        \"og_tpl\",\n        \"og_ua\",\n        \"opds_exts\",\n        \"prologues\",\n        \"preadmes\",\n        \"put_ck\",\n        \"put_name\",\n        \"readmes\",\n        \"mv_retry\",\n        \"rm_retry\",\n        \"rss_sort\",\n        \"rss_fmt_t\",\n        \"rss_fmt_d\",\n        \"rw_edit\",\n        \"shr_who\",\n        \"sort\",\n        \"tail_fd\",\n        \"tail_rate\",\n        \"tail_tmax\",\n        \"tail_who\",\n        \"tcolor\",\n        \"th_qv\",\n        \"th_qvx\",\n        \"th_spec_p\",\n        \"txt_eol\",\n        \"unlist\",\n        \"u2abort\",\n        \"u2ts\",\n        \"uid\",\n        \"gid\",\n        \"unp_who\",\n        \"ups_who\",\n        \"zip_who\",\n        \"zipmaxn\",\n        \"zipmaxs\",\n        \"zipmaxt\",\n    ):\n        ret[k] = k\n    return ret\n\n\ndef vf_cmap() -> dict[str, str]:\n    \"\"\"argv-to-volflag: complex/lists\"\"\"\n    ret = {}\n    for k in (\n        \"exp_lg\",\n        \"exp_md\",\n        \"ext_th\",\n        \"mte\",\n        \"mth\",\n        \"mtp\",\n        \"xac\",\n        \"xad\",\n        \"xar\",\n        \"xau\",\n        \"xban\",\n        \"xbc\",\n        \"xbd\",\n        \"xbr\",\n        \"xbu\",\n        \"xiu\",\n        \"xm\",\n    ):\n        ret[k] = k\n    return ret\n\n\npermdescs = {\n    \"r\": \"read; list folder contents, download files\",\n    \"w\": 'write; upload files; need \"r\" to see the uploads',\n    \"m\": 'move; move files and folders; need \"w\" at destination',\n    \"d\": \"delete; permanently delete files and folders\",\n    \".\": \"dots; user can ask to show dotfiles in listings\",\n    \"g\": \"get; download files, but cannot see folder contents\",\n    \"G\": 'upget; same as \"g\" but can see filekeys of their own uploads',\n    \"h\": 'html; same as \"g\" but folders return their index.html',\n    \"a\": \"admin; can see uploader IPs, config-reload\",\n    \"A\": \"all; same as 'rwmda.' (read/write/move/delete/dotfiles)\",\n}\n\n\nflagcats = {\n    \"uploads, general\": {\n        \"dedup\": \"enable symlink-based file deduplication\",\n        \"hardlink\": \"enable hardlink-based file deduplication,\\nwith fallback on symlinks when that is impossible\",\n        \"hardlinkonly\": \"dedup with hardlink only, never symlink;\\nmake a full copy if hardlink is impossible\",\n        \"reflink\": \"enable reflink-based file deduplication,\\nwith fallback on full copy when that is impossible\",\n        \"safededup\": \"verify on-disk data before using it for dedup\",\n        \"noclone\": \"take dupe data from clients, even if available on HDD\",\n        \"nodupe\": \"rejects existing files (instead of linking/cloning them)\",\n        \"nodupem\": \"rejects existing files during moves as well\",\n        \"casechk=auto\": \"actively prevent case-insensitive filesystem? y/n\",\n        \"chmod_d=755\": \"unix-permission for new dirs/folders\",\n        \"chmod_f=644\": \"unix-permission for new files\",\n        \"uid=573\": \"change owner of new files/folders to unix-user 573\",\n        \"gid=999\": \"change owner of new files/folders to unix-group 999\",\n        \"fsnt=auto\": \"filesystem filename traits (lin/win/mac/auto)\",\n        \"wram\": \"allow uploading into ramdisks\",\n        \"sparse\": \"force use of sparse files, mainly for s3-backed storage\",\n        \"nosparse\": \"deny use of sparse files, mainly for slow storage\",\n        \"rm_partial\": \"delete unfinished uploads from HDD when they timeout\",\n        \"daw\": \"enable full WebDAV write support (dangerous);\\nPUT-operations will now \\033[1;31mOVERWRITE\\033[0;35m existing files\",\n        \"nosub\": \"forces all uploads into the top folder of the vfs\",\n        \"magic\": \"enables filetype detection for nameless uploads\",\n        \"put_name\": \"fallback filename for nameless uploads\",\n        \"put_ck\": \"default checksum-hasher for PUT/WebDAV uploads\",\n        \"bup_ck\": \"default checksum-hasher for bup/basic uploads\",\n        \"gz\": \"allows server-side gzip compression of uploads with ?gz\",\n        \"xz\": \"allows server-side lzma compression of uploads with ?xz\",\n        \"pk\": \"forces server-side compression, optional arg: xz,9\",\n    },\n    \"upload rules\": {\n        \"apnd_who=dw\": \"who can append? (aw/dw/w/no)\",\n        \"maxn=250,600\": \"max 250 uploads over 15min\",\n        \"maxb=1g,300\": \"max 1 GiB over 5min (suffixes: b, k, m, g, t)\",\n        \"vmaxb=1g\": \"total volume size max 1 GiB (suffixes: b, k, m, g, t)\",\n        \"vmaxn=4k\": \"max 4096 files in volume (suffixes: b, k, m, g, t)\",\n        \"medialinks\": \"return medialinks for non-up2k uploads (not hotlinks)\",\n        \"wo_up_readme\": \"write-only users can upload logues without getting renamed\",\n        \"rand\": \"force randomized filenames, 9 chars long by default\",\n        \"nrand=N\": \"randomized filenames are N chars long\",\n        \"u2ow=N\": \"overwrite existing files? 0=no 1=if-older 2=always\",\n        \"u2ts=fc\": \"[f]orce [c]lient-last-modified or [u]pload-time\",\n        \"u2abort=1\": \"allow aborting unfinished uploads? 0=no 1=strict 2=ip-chk 3=acct-chk\",\n        \"sz=1k-3m\": \"allow filesizes between 1 KiB and 3MiB\",\n        \"df=1g\": \"ensure 1 GiB free disk space\",\n    },\n    \"upload rotation\\n(moves all uploads into the specified folder structure)\": {\n        \"rotn=100,3\": \"3 levels of subfolders with 100 entries in each\",\n        \"rotf=%Y-%m/%d-%H\": \"date-formatted organizing\",\n        \"rotf_tz=Europe/Oslo\": \"timezone (default=UTC)\",\n        \"lifetime=3600\": \"uploads are deleted after 1 hour\",\n    },\n    \"database, general\": {\n        \"e2d\": \"enable database; makes files searchable + enables upload-undo\",\n        \"e2ds\": \"scan writable folders for new files on startup; also sets -e2d\",\n        \"e2dsa\": \"scans all folders for new files on startup; also sets -e2d\",\n        \"e2t\": \"enable multimedia indexing; makes it possible to search for tags\",\n        \"e2ts\": \"scan existing files for tags on startup; also sets -e2t\",\n        \"e2tsr\": \"delete all metadata from DB (full rescan); also sets -e2ts\",\n        \"d2ts\": \"disables metadata collection for existing files\",\n        \"e2v\": \"verify integrity on startup by hashing files and comparing to db\",\n        \"e2vu\": \"when e2v fails, update the db (assume on-disk files are good)\",\n        \"e2vp\": \"when e2v fails, panic and quit copyparty\",\n        \"d2ds\": \"disables onboot indexing, overrides -e2ds*\",\n        \"d2t\": \"disables metadata collection, overrides -e2t*\",\n        \"d2v\": \"disables file verification, overrides -e2v*\",\n        \"d2d\": \"disables all database stuff, overrides -e2*\",\n        \"hist=/tmp/cdb\": \"puts thumbnails and indexes at that location\",\n        \"dbpath=/tmp/cdb\": \"puts indexes at that location\",\n        \"landmark=foo\": \"disable db if file foo doesn't exist\",\n        \"scan=60\": \"scan for new files every 60sec, same as --re-maxage\",\n        \"nohash=\\\\.iso$\": \"skips hashing file contents if path matches *.iso\",\n        \"noidx=\\\\.iso$\": \"fully ignores the contents at paths matching *.iso\",\n        \"noforget\": \"don't forget files when deleted from disk\",\n        \"forget_ip=43200\": \"forget uploader-IP after 30 days (GDPR)\",\n        \"no_db_ip\": \"never store uploader-IP in the db; disables unpost\",\n        \"fat32\": \"avoid excessive reindexing on android sdcardfs\",\n        \"dbd=[acid|swal|wal|yolo]\": \"database speed-durability tradeoff\",\n        \"xlink\": \"cross-volume dupe detection / linking (dangerous)\",\n        \"xdev\": \"do not descend into other filesystems\",\n        \"xvol\": \"do not follow symlinks leaving the volume root\",\n        \"dotsrch\": \"show dotfiles in search results\",\n        \"nodotsrch\": \"hide dotfiles in search results (default)\",\n        \"srch_excl\": \"exclude search results with URL matching this regex\",\n        \"db_xattr=user.foo,user.bar\": \"index file xattrs as media-tags\",\n    },\n    'database, audio tags\\n\"mte\", \"mth\", \"mtp\", \"mtm\" all work the same as -mte, -mth, ...': {\n        \"mte=artist,title\": \"media-tags to index/display\",\n        \"mth=fmt,res,ac\": \"media-tags to hide by default\",\n        \"mtp=.bpm=f,audio-bpm.py\": 'uses the \"audio-bpm.py\" program to\\ngenerate \".bpm\" tags from uploads (f = overwrite tags)',\n        \"mtp=ahash,vhash=media-hash.py\": \"collects two tags at once\",\n    },\n    \"thumbnails\": {\n        \"dthumb\": \"disables all thumbnails\",\n        \"dvthumb\": \"disables video thumbnails\",\n        \"dathumb\": \"disables audio thumbnails (spectrograms)\",\n        \"dithumb\": \"disables image thumbnails\",\n        \"pngquant\": \"compress audio waveforms 33% better\",\n        \"thsize\": \"thumbnail res; WxH\",\n        \"crop\": \"center-cropping (y/n/fy/fn)\",\n        \"th3x\": \"3x resolution (y/n/fy/fn)\",\n        \"th_qv=40\": \"webp/jpg thumbnail quality (10~90)\",\n        \"th_qvx=40\": \"jxl thumbnail quality (10~90)\",\n        \"convt\": \"convert-to-image timeout in seconds\",\n        \"aconvt\": \"convert-to-audio timeout in seconds\",\n        \"th_spec_p=1\": \"make spectrograms? 0=never 1=fallback 2=always\",\n        \"ext_th=s=/b.png\": \"use /b.png as thumbnail for file-extension s\",\n    },\n    \"handlers\\n(better explained in --help-handlers)\": {\n        \"on404=PY\": \"handle 404s by executing PY file\",\n        \"on403=PY\": \"handle 403s by executing PY file\",\n    },\n    \"event hooks\\n(better explained in --help-hooks)\": {\n        \"xbu=CMD\": \"execute CMD before a file upload starts\",\n        \"xau=CMD\": \"execute CMD after  a file upload finishes\",\n        \"xiu=CMD\": \"execute CMD after  all uploads finish and volume is idle\",\n        \"xbc=CMD\": \"execute CMD before a file copy\",\n        \"xac=CMD\": \"execute CMD after  a file copy\",\n        \"xbr=CMD\": \"execute CMD before a file rename/move\",\n        \"xar=CMD\": \"execute CMD after  a file rename/move\",\n        \"xbd=CMD\": \"execute CMD before a file delete\",\n        \"xad=CMD\": \"execute CMD after  a file delete\",\n        \"xm=CMD\": \"execute CMD on message\",\n        \"xban=CMD\": \"execute CMD if someone gets banned\",\n    },\n    \"client and ux\": {\n        \"grid\": \"show grid/thumbnails by default\",\n        \"gsel\": \"select files in grid by ctrl-click\",\n        \"sort\": \"default sort order\",\n        \"nsort\": \"natural-sort of leading digits in filenames\",\n        \"hsortn\": \"number of sort-rules to add to media URLs\",\n        \"ufavico=URL\": \"per-volume favicon (.ico/png/gif/svg)\",\n        \"unlist\": \"dont list files matching REGEX\",\n        \"dlni\": \"force-download (no-inline) files on click\",\n        \"html_head=TXT\": \"includes TXT in the <head>, or @PATH for file at PATH\",\n        \"html_head_s=TXT\": \"additional static text in the html <head>\",\n        \"tcolor=#fc0\": \"theme color (a hint for webbrowsers, discord, etc.)\",\n        \"nodirsz\": \"don't show total folder size\",\n        \"du_who=all\": \"show disk-usage info to everyone\",\n        \"robots\": \"allows indexing by search engines (default)\",\n        \"norobots\": \"kindly asks search engines to leave\",\n        \"unlistcr\": \"don't list read-access in controlpanel\",\n        \"unlistcw\": \"don't list write-access in controlpanel\",\n        \"prologues=.prologue.html\": \"files to embed above/before files\",\n        \"epilogues=.epilogue.html\": \"files to embed below/after files\",\n        \"readmes=readme.md,README.md\": \"files to embed as readmes\",\n        \"preadmes=preadme.md,PREADME.md\": \"files to embed as preadmes\",\n        \"no_sb_md\": \"disable js sandbox for markdown files\",\n        \"no_sb_lg\": \"disable js sandbox for prologue/epilogue\",\n        \"sb_md\": \"enable js sandbox for markdown files (default)\",\n        \"sb_lg\": \"enable js sandbox for prologue/epilogue (default)\",\n        \"md_sbf\": \"list of markdown-sandbox safeguards to disable\",\n        \"lg_sbf\": \"list of *logue-sandbox safeguards to disable\",\n        \"md_sba\": \"value of iframe allow-prop for markdown-sandbox\",\n        \"lg_sba\": \"value of iframe allow-prop for *logue-sandbox\",\n        \"nohtml\": \"return html and markdown as text/html\",\n        \"noscript\": \"disable most javascript by use of CSP\",\n        \"ui_noacci\": \"hide account-info in the UI\",\n        \"ui_nocpla\": \"hide cpanel-link in the UI\",\n        \"ui_nolbar\": \"hide link-bar in the UI\",\n        \"ui_nombar\": \"hide top-menu in the UI\",\n        \"ui_nonav\": \"hide navpane+breadcrumbs in the UI\",\n        \"ui_notree\": \"hide navpane in the UI\",\n        \"ui_norepl\": \"hide repl-button in the UI\",\n        \"ui_nosrvi\": \"hide server-info in the UI\",\n        \"ui_noctxb\": \"hide context-buttons in the UI\",\n    },\n    \"opengraph (discord embeds)\": {\n        \"og\": \"enable OG (disables hotlinking)\",\n        \"og_site\": \"sitename; defaults to --name, disable with '-'\",\n        \"og_desc\": \"description text for all files; disable with '-'\",\n        \"og_th=jf\": \"thumbnail format; j / jf / jf3 / w / w3 / ...\",\n        \"og_title_a\": \"audio title format; default: {{ artist }} - {{ title }}\",\n        \"og_title_v\": \"video title format; default: {{ title }}\",\n        \"og_title_i\": \"image title format; default: {{ title }}\",\n        \"og_title=foo\": \"fallback title if there's nothing in the db\",\n        \"og_s_title\": \"force default title; do not read from tags\",\n        \"og_tpl\": \"custom html; see --og-tpl in --help\",\n        \"og_no_head\": \"you want to add tags manually with og_tpl\",\n        \"og_ua\": \"if defined: only send OG html if useragent matches this regex\",\n    },\n    \"opds\": {\n        \"opds\": \"enable OPDS\",\n        \"opds_exts\": \"file formats to list in OPDS feeds; leave empty to show everything\",\n    },\n    \"textfiles\": {\n        \"rw_edit=md,txt\": \"only require read+write to edit .md and .txt\",\n        \"md_no_br\": \"newline only on double-newline or two tailing spaces\",\n        \"md_hist\": \"where to put markdown backups; s=subfolder, v=volHist, n=nope\",\n        \"exp\": \"enable textfile expansion; see --help-exp\",\n        \"exp_md\": \"placeholders to expand in markdown files; see --help\",\n        \"exp_lg\": \"placeholders to expand in prologue/epilogue; see --help\",\n        \"txt_eol=lf\": \"enable EOL conversion when writing docs (LF or CRLF)\",\n    },\n    \"tailing\": {\n        \"notail\": \"disable ?tail (download a growing file continuously)\",\n        \"tail_fd=1\": \"check if file was replaced (new fd) every 1 sec\",\n        \"tail_rate=0.2\": \"check for new data every 0.2 sec\",\n        \"tail_tmax=30\": \"kill connection after 30 sec\",\n        \"tail_who=2\": \"restrict ?tail access (1=admins,2=authed,3=everyone)\",\n    },\n    \"rss\": {\n        \"rss\": \"allow '?rss' URL suffix (experimental)\",\n        \"rss_sort=m\": \"default sort-order (m/u/n/s)\",\n        \"rss_fmt_t={fname}\": \"default title-format\",\n        \"rss_fmt_d={album},{.tn}\": \"default description-format\",\n    },\n    \"others\": {\n        \"dots\": \"allow all users with read-access to\\nenable the option to show dotfiles in listings\",\n        \"fk=8\": 'generates per-file accesskeys,\\nwhich are then required at the \"g\" permission;\\nkeys are invalidated if filesize or inode changes',\n        \"fka=8\": 'generates slightly weaker per-file accesskeys,\\nwhich are then required at the \"g\" permission;\\nnot affected by filesize or inode numbers',\n        \"dk=8\": 'generates per-directory accesskeys,\\nwhich are then required at the \"g\" permission;\\nkeys are invalidated if filesize or inode changes',\n        \"dks\": \"per-directory accesskeys allow browsing into subdirs\",\n        \"dky\": 'allow seeing files (not folders) inside a specific folder\\nwith \"g\" perm, and does not require a valid dirkey to do so',\n        \"rmagic\": \"expensive analysis for mimetype accuracy\",\n        \"shr_who=auth\": \"who can create shares? no/auth/a\",\n        \"unp_who=2\": \"unpost only if same... 1=ip+name, 2=ip, 3=name\",\n        \"ups_who=2\": \"restrict viewing the list of recent uploads\",\n        \"zip_who=2\": \"restrict access to download-as-zip/tar\",\n        \"zipmaxn=9k\": \"reject download-as-zip if more than 9000 files\",\n        \"zipmaxs=2g\": \"reject download-as-zip if size over 2 GiB\",\n        \"zipmaxt=no\": \"reply with 'no' if download-as-zip exceeds max\",\n        \"zipmaxu\": \"zip-size-limit does not apply to authenticated users\",\n        \"nopipe\": \"disable race-the-beam (download unfinished uploads)\",\n        \"cachectl=no-cache\": \"controls caching in webbrowsers\",\n        \"mv_retry\": \"ms-windows: timeout for renaming busy files\",\n        \"rm_retry\": \"ms-windows: timeout for deleting busy files\",\n        \"nospawn\": \"don't create volume's folder if not exist\",\n        \"assert_root\": \"crash on startup if volume's folder not exist\",\n        \"davauth\": \"ask webdav clients to login for all folders\",\n        \"davrt\": \"show lastmod time of symlink destination, not the link itself\\n(note: this option is always enabled for recursive listings)\",\n    },\n}\n\n\nflagdescs = {k.split(\"=\")[0]: v for tab in flagcats.values() for k, v in tab.items()}\n\n\nif True:  # so it gets removed in release-builds\n    for fun in [vf_bmap, vf_cmap, vf_vmap]:\n        for k in fun().values():\n            if k not in flagdescs:\n                raise Exception(\"undocumented volflag: \" + k)\n"
  },
  {
    "path": "copyparty/dxml.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport importlib\nimport sys\nimport xml.etree.ElementTree as ET\n\nfrom .__init__ import PY2\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Any, Optional\n\n\nclass BadXML(Exception):\n    pass\n\n\ndef get_ET() -> ET.XMLParser:\n    pn = \"xml.etree.ElementTree\"\n    cn = \"_elementtree\"\n\n    cmod = sys.modules.pop(cn, None)\n    if not cmod:\n        return ET.XMLParser  # type: ignore\n\n    pmod = sys.modules.pop(pn)\n    sys.modules[cn] = None  # type: ignore\n\n    ret = importlib.import_module(pn)\n    for name, mod in ((pn, pmod), (cn, cmod)):\n        if mod:\n            sys.modules[name] = mod\n        else:\n            sys.modules.pop(name, None)\n\n    sys.modules[\"xml.etree\"].ElementTree = pmod  # type: ignore\n    ret.ParseError = ET.ParseError  # type: ignore\n    return ret.XMLParser  # type: ignore\n\n\nXMLParser: ET.XMLParser = get_ET()\n\n\nclass _DXMLParser(XMLParser):  # type: ignore\n    def __init__(self) -> None:\n        tb = ET.TreeBuilder()\n        super(DXMLParser, self).__init__(target=tb)\n\n        p = self._parser if PY2 else self.parser\n        p.StartDoctypeDeclHandler = self.nope\n        p.EntityDeclHandler = self.nope\n        p.UnparsedEntityDeclHandler = self.nope\n        p.ExternalEntityRefHandler = self.nope\n\n    def nope(self, *a: Any, **ka: Any) -> None:\n        raise BadXML(\"{}, {}\".format(a, ka))\n\n\nclass _NG(XMLParser):  # type: ignore\n    def __int__(self) -> None:\n        raise BadXML(\"dxml selftest failed\")\n\n\nDXMLParser = _DXMLParser\n\n\ndef parse_xml(txt: str) -> ET.Element:\n    \"\"\"\n    Parse XML into an xml.etree.ElementTree.Element while defusing some unsafe parts.\n    \"\"\"\n    parser = DXMLParser()\n    parser.feed(txt)\n    return parser.close()  # type: ignore\n\n\ndef selftest() -> bool:\n    qbe = r\"\"\"<!DOCTYPE d [\n<!ENTITY a \"nice_bakuretsu\">\n]>\n<root>&a;&a;&a;</root>\"\"\"\n\n    emb = r\"\"\"<!DOCTYPE d [\n<!ENTITY a SYSTEM \"file:///etc/hostname\">\n]>\n<root>&a;</root>\"\"\"\n\n    # future-proofing; there's never been any known vulns\n    # regarding DTDs and ET.XMLParser, but might as well\n    # block them since webdav-clients don't use them\n    dtd = r\"\"\"<!DOCTYPE d SYSTEM \"a.dtd\">\n<root>a</root>\"\"\"\n\n    for txt in (qbe, emb, dtd):\n        try:\n            parse_xml(txt)\n            t = \"WARNING: dxml selftest failed:\\n%s\\n\"\n            print(t % (txt,), file=sys.stderr)\n            return False\n        except BadXML:\n            pass\n\n    return True\n\n\nDXML_OK = selftest()\nif not DXML_OK:\n    DXMLParser = _NG\n\n\ndef mktnod(name: str, text: str) -> ET.Element:\n    el = ET.Element(name)\n    el.text = text\n    return el\n\n\ndef mkenod(name: str, sub_el: Optional[ET.Element] = None) -> ET.Element:\n    el = ET.Element(name)\n    if sub_el is not None:\n        el.append(sub_el)\n    return el\n"
  },
  {
    "path": "copyparty/fsutil.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport argparse\nimport json\nimport os\nimport re\nimport time\n\nfrom .__init__ import ANYWIN, MACOS\nfrom .authsrv import AXS, VFS, AuthSrv\nfrom .bos import bos\nfrom .util import chkcmd, json_hesc, min_ex, undot\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Optional, Union\n\n    from .util import RootLogger, undot\n\n\nclass Fstab(object):\n    def __init__(self, log: \"RootLogger\", args: argparse.Namespace, verbose: bool):\n        self.log_func = log\n        self.verbose = verbose\n\n        self.warned = False\n        self.trusted = False\n        self.tab: Optional[VFS] = None\n        self.oldtab: Optional[VFS] = None\n        self.srctab = \"a\"\n        self.cache: dict[str, tuple[str, str]] = {}\n        self.age = 0.0\n        self.maxage = args.mtab_age\n\n    def log(self, msg: str, c: Union[int, str] = 0) -> None:\n        if not c or self.verbose:\n            return\n        self.log_func(\"fstab\", msg, c)\n\n    def get(self, path: str) -> tuple[str, str]:\n        now = time.time()\n        if now - self.age > self.maxage or len(self.cache) > 9000:\n            self.age = now\n            self.oldtab = self.tab or self.oldtab\n            self.tab = None\n            self.cache = {}\n\n        mp = \"\"\n        fs = \"ext4\"\n        msg = \"failed to determine filesystem at %r; assuming %s\\n%s\"\n\n        if ANYWIN:\n            fs = \"vfat\"\n            try:\n                path = self._winpath(path)\n            except:\n                self.log(msg % (path, fs, min_ex()), 3)\n                return fs, \"\"\n\n        path = undot(path)\n        try:\n            return self.cache[path]\n        except:\n            pass\n\n        try:\n            fs, mp = self.get_w32(path) if ANYWIN else self.get_unix(path)\n        except:\n            self.log(msg % (path, fs, min_ex()), 3)\n\n        fs = fs.lower()\n        self.cache[path] = (fs, mp)\n        self.log(\"found %s at %r, %r\" % (fs, mp, path))\n        return fs, mp\n\n    def _winpath(self, path: str) -> str:\n        # try to combine volume-label + st_dev (vsn)\n        path = path.replace(\"/\", \"\\\\\")\n        vid = path.split(\":\", 1)[0].strip(\"\\\\\").split(\"\\\\\", 1)[0]\n        try:\n            return \"{}*{}\".format(vid, bos.stat(path).st_dev)\n        except:\n            return vid\n\n    def build_fallback(self) -> None:\n        self.tab = VFS(self.log_func, \"idk\", \"/\", \"/\", AXS(), {})\n        self.trusted = False\n\n    def _from_sp_mount(self) -> dict[str, str]:\n        sptn = r\"^.*? on (.*) type ([^ ]+) \\(.*\"\n        if MACOS:\n            sptn = r\"^.*? on (.*) \\(([^ ]+), .*\"\n\n        ptn = re.compile(sptn)\n        so, _ = chkcmd([\"mount\"])\n        dtab: dict[str, str] = {}\n        for ln in so.split(\"\\n\"):\n            m = ptn.match(ln)\n            if not m:\n                continue\n\n            zs1, zs2 = m.groups()\n            dtab[str(zs1)] = str(zs2)\n\n        return dtab\n\n    def _from_proc(self) -> dict[str, str]:\n        ret: dict[str, str] = {}\n        with open(\"/proc/self/mounts\", \"rb\", 262144) as f:\n            src = f.read(262144).decode(\"utf-8\", \"replace\").split(\"\\n\")\n        for zsl in [x.split(\" \") for x in src]:\n            if len(zsl) < 3:\n                continue\n            zs = zsl[1]\n            zs = zs.replace(\"\\\\011\", \"\\t\").replace(\"\\\\040\", \" \").replace(\"\\\\134\", \"\\\\\")\n            ret[zs] = zsl[2]\n        return ret\n\n    def build_tab(self) -> None:\n        self.log(\"inspecting mtab for changes\")\n        dtab = self._from_sp_mount() if MACOS else self._from_proc()\n\n        # keep empirically-correct values if mounttab unchanged\n        srctab = str(sorted(dtab.items()))\n        if srctab == self.srctab:\n            self.tab = self.oldtab\n            return\n\n        self.log(\"mtab has changed; reevaluating support for sparse files\")\n\n        try:\n            fuses = [mp for mp, fs in dtab.items() if fs == \"fuseblk\"]\n            if not fuses or MACOS:\n                raise Exception()\n            try:\n                so, _ = chkcmd([\"lsblk\", \"-nrfo\", \"FSTYPE,MOUNTPOINT\"])  # centos6\n            except:\n                so, _ = chkcmd([\"lsblk\", \"-nrfo\", \"FSTYPE,MOUNTPOINTS\"])  # future\n            for ln in so.split(\"\\n\"):\n                zsl = ln.split(\" \", 1)\n                if len(zsl) != 2:\n                    continue\n                fs, mp = zsl\n                if mp in fuses:\n                    dtab[mp] = fs\n        except:\n            pass\n\n        tab1 = list(dtab.items())\n        tab1.sort(key=lambda x: (len(x[0]), x[0]))\n        path1, fs1 = tab1[0]\n        tab = VFS(self.log_func, fs1, path1, path1, AXS(), {})\n        for path, fs in tab1[1:]:\n            zs = path.lstrip(\"/\")\n            tab.add(fs, zs, zs)\n\n        self.tab = tab\n        self.srctab = srctab\n\n    def relabel(self, path: str, nval: str) -> None:\n        assert self.tab  # !rm\n        self.cache = {}\n        if ANYWIN:\n            path = self._winpath(path)\n\n        path = undot(path)\n        ptn = re.compile(r\"^[^\\\\/]*\")\n        vn, rem = self.tab._find(path)\n        if not self.trusted:\n            # no mtab access; have to build as we go\n            if \"/\" in rem:\n                zs = os.path.join(vn.vpath, rem.split(\"/\")[0])\n                self.tab.add(\"idk\", zs, zs)\n            if rem:\n                self.tab.add(nval, path, path)\n            else:\n                vn.realpath = nval\n\n            return\n\n        visit = [vn]\n        while visit:\n            vn = visit.pop()\n            vn.realpath = ptn.sub(nval, vn.realpath)\n            visit.extend(list(vn.nodes.values()))\n\n    def get_unix(self, path: str) -> tuple[str, str]:\n        if not self.tab:\n            try:\n                self.build_tab()\n                self.trusted = True\n            except:\n                # prisonparty or other restrictive environment\n                if not self.warned:\n                    self.warned = True\n                    t = \"failed to associate fs-mounts with the VFS (this is fine):\\n%s\"\n                    self.log(t % (min_ex(),), 6)\n                self.build_fallback()\n\n        assert self.tab  # !rm\n        ret = self.tab._find(path)[0]\n        if self.trusted or path == ret.vpath:\n            return ret.realpath.split(\"/\")[0], ret.vpath\n        else:\n            return \"idk\", \"\"\n\n    def get_w32(self, path: str) -> tuple[str, str]:\n        if not self.tab:\n            self.build_fallback()\n\n        assert self.tab  # !rm\n        ret = self.tab._find(path)[0]\n        return ret.realpath, \"\"\n\n\n_fstab: Optional[Fstab] = None\nwinfs = set((\"msdos\", \"vfat\", \"ntfs\", \"exfat\"))\n# \"msdos\" = vfat on macos\n\n\ndef ramdisk_chk(asrv: AuthSrv) -> None:\n    # should have been in authsrv but that's a circular import\n    global _fstab\n    mods = []\n    ramfs = (\"tmpfs\", \"overlay\")\n    log = asrv.log_func or print\n    if not _fstab:\n        _fstab = Fstab(log, asrv.args, False)\n    for vn in asrv.vfs.all_nodes.values():\n        if not vn.axs.uwrite or \"wram\" in vn.flags:\n            continue\n        ap = vn.realpath\n        if not ap or os.path.isfile(ap):\n            continue\n        fs, mp = _fstab.get(ap)\n        mp = \"/\" + mp.strip(\"/\")\n        if fs == \"tmpfs\" or (mp == \"/\" and fs in ramfs):\n            mods.append((vn.vpath, ap, fs, mp))\n            vn.axs.uwrite.clear()\n            vn.axs.umove.clear()\n            for un, ztsp in list(vn.uaxs.items()):\n                zsl = list(ztsp)\n                zsl[1] = False\n                zsl[2] = False\n                vn.uaxs[un] = tuple(zsl)  # type: ignore\n    if mods:\n        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:\"\n        t2 = [\"\\n  volume=[/%s], abspath=%r, type=%s, root=%r\" % x for x in mods]\n        log(\"vfs\", t + \"\".join(t2) + \"\\n\", 1)\n\n    assume = \"mac\" if MACOS else \"lin\"\n    for vol in asrv.vfs.all_nodes.values():\n        if not vol.realpath or vol.flags.get(\"is_file\"):\n            continue\n        zs = vol.flags[\"fsnt\"].strip()[:3].lower()\n        if ANYWIN and not zs:\n            zs = \"win\"\n        if zs in (\"lin\", \"win\", \"mac\"):\n            vol.flags[\"fsnt\"] = zs\n            continue\n        fs = _fstab.get(vol.realpath)[0]\n        fs = \"win\" if fs in winfs else assume\n        htm = json.loads(vol.js_htm)\n        vol.flags[\"fsnt\"] = vol.js_ls[\"fsnt\"] = htm[\"fsnt\"] = fs\n        vol.js_htm = json_hesc(json.dumps(htm))\n"
  },
  {
    "path": "copyparty/ftpd.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport argparse\nimport errno\nimport logging\nimport os\nimport stat\nimport sys\nimport time\n\nfrom pyftpdlib.authorizers import AuthenticationFailed, DummyAuthorizer\nfrom pyftpdlib.filesystems import AbstractedFS, FilesystemError\nfrom pyftpdlib.handlers import FTPHandler\nfrom pyftpdlib.ioloop import IOLoop\nfrom pyftpdlib.servers import FTPServer\n\nfrom .__init__ import PY2, TYPE_CHECKING\nfrom .authsrv import VFS\nfrom .bos import bos\nfrom .util import (\n    VF_CAREFUL,\n    Daemon,\n    ODict,\n    Pebkac,\n    exclude_dotfiles,\n    fsenc,\n    ipnorm,\n    pybin,\n    relchk,\n    runhook,\n    sanitize_fn,\n    set_fperms,\n    vjoin,\n    wunlink,\n)\n\nif TYPE_CHECKING:\n    from .svchub import SvcHub\n\nif True:  # pylint: disable=using-constant-test\n    import typing\n    from typing import Any, Optional, Union\n\nif PY2:\n    range = xrange  # type: ignore\n\n\nclass FSE(FilesystemError):\n    def __init__(self, msg: str, severity: int = 0) -> None:\n        super(FilesystemError, self).__init__(msg)\n        self.severity = severity\n\n\nclass FtpAuth(DummyAuthorizer):\n    def __init__(self, hub: \"SvcHub\") -> None:\n        super(FtpAuth, self).__init__()\n        self.hub = hub\n\n    def validate_authentication(\n        self, username: str, password: str, handler: Any\n    ) -> None:\n        handler.username = \"{}:{}\".format(username, password)\n        handler.uname = \"*\"\n\n        ip = handler.addr[0]\n        if ip.startswith(\"::ffff:\"):\n            ip = ip[7:]\n\n        ipn = ipnorm(ip)\n        bans = self.hub.bans\n        if ipn in bans:\n            rt = bans[ipn] - time.time()\n            if rt < 0:\n                logging.info(\"client unbanned\")\n                del bans[ipn]\n            else:\n                raise AuthenticationFailed(\"banned\")\n\n        args = self.hub.args\n        asrv = self.hub.asrv\n        uname = \"*\"\n        if username != \"anonymous\":\n            uname = \"\"\n            if args.usernames:\n                alts = [\"%s:%s\" % (username, password)]\n            else:\n                alts = [password, username]\n\n            for zs in alts:\n                zs = asrv.iacct.get(asrv.ah.hash(zs), \"\")\n                if zs:\n                    uname = zs\n                    break\n\n        if args.ipu and uname == \"*\":\n            uname = args.ipu_iu[args.ipu_nm.map(ip)]\n        if args.ipr and uname in args.ipr_u:\n            if not args.ipr_u[uname].map(ip):\n                logging.warning(\"username [%s] rejected by --ipr\", uname)\n                uname = \"*\"\n\n        if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):\n            g = self.hub.gpwd\n            if g.lim:\n                bonk, ip = g.bonk(ip, handler.username)\n                if bonk:\n                    logging.warning(\"client banned: invalid passwords\")\n                    bans[ip] = bonk\n                    try:\n                        # only possible if multiprocessing disabled\n                        self.hub.broker.httpsrv.bans[ip] = bonk  # type: ignore\n                        self.hub.broker.httpsrv.nban += 1  # type: ignore\n                    except:\n                        pass\n\n            raise AuthenticationFailed(\"Authentication failed.\")\n\n        handler.uname = handler.username = uname\n\n    def get_home_dir(self, username: str) -> str:\n        return \"/\"\n\n    def has_user(self, username: str) -> bool:\n        asrv = self.hub.asrv\n        return username in asrv.acct or username in asrv.iacct\n\n    def has_perm(self, username: str, perm: int, path: Optional[str] = None) -> bool:\n        return True  # handled at filesystem layer\n\n    def get_perms(self, username: str) -> str:\n        return \"elradfmwMT\"\n\n    def get_msg_login(self, username: str) -> str:\n        return \"sup {}\".format(username)\n\n    def get_msg_quit(self, username: str) -> str:\n        return \"cya\"\n\n\nclass FtpFs(AbstractedFS):\n    def __init__(\n        self, root: str, cmd_channel: Any\n    ) -> None:  # pylint: disable=super-init-not-called\n        self.h = cmd_channel  # type: FTPHandler\n        self.cmd_channel = cmd_channel  # type: FTPHandler\n        self.hub: \"SvcHub\" = cmd_channel.hub\n        self.args = cmd_channel.args\n        self.uname = cmd_channel.uname\n\n        self.cwd = \"/\"  # pyftpdlib convention of leading slash\n        self.root = \"/var/lib/empty\"\n\n        self.listdirinfo = self.listdir\n        self.chdir(\".\")\n\n    def log(self, msg: str, c: Union[int, str] = 0) -> None:\n        self.hub.log(\"ftpd\", msg, c)\n\n    def v2a(\n        self,\n        vpath: str,\n        r: bool = False,\n        w: bool = False,\n        m: bool = False,\n        d: bool = False,\n    ) -> tuple[str, VFS, str]:\n        try:\n            vpath = vpath.replace(\"\\\\\", \"/\").strip(\"/\")\n            rd, fn = os.path.split(vpath)\n            if relchk(rd):\n                logging.warning(\"malicious vpath: %s\", vpath)\n                t = \"Unsupported characters in [{}]\"\n                raise FSE(t.format(vpath), 1)\n\n            fn = sanitize_fn(fn or \"\")\n            vpath = vjoin(rd, fn)\n            vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)\n            if (\n                w\n                and fn.lower() in vfs.flags[\"emb_all\"]\n                and self.h.uname not in vfs.axs.uread\n                and \"wo_up_readme\" not in vfs.flags\n            ):\n                fn = \"_wo_\" + fn\n                vpath = vjoin(rd, fn)\n                vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)\n\n            if not vfs.realpath:\n                t = \"No filesystem mounted at [{}]\"\n                raise FSE(t.format(vpath))\n\n            if \"xdev\" in vfs.flags or \"xvol\" in vfs.flags:\n                ap = vfs.canonical(rem)\n                avfs = vfs.chk_ap(ap)\n                t = \"Permission denied in [{}]\"\n                if not avfs:\n                    raise FSE(t.format(vpath), 1)\n\n                cr, cw, cm, cd, _, _, _, _, _ = avfs.uaxs[self.h.uname]\n                if r and not cr or w and not cw or m and not cm or d and not cd:\n                    raise FSE(t.format(vpath), 1)\n            else:\n                ap = vfs.canonical(rem, False)\n\n            if \"bcasechk\" in vfs.flags and not vfs.casechk(rem, True):\n                raise FSE(\"No such file or directory\", 1)\n\n            return ap, vfs, rem\n        except Pebkac as ex:\n            raise FSE(str(ex))\n\n    def rv2a(\n        self,\n        vpath: str,\n        r: bool = False,\n        w: bool = False,\n        m: bool = False,\n        d: bool = False,\n    ) -> tuple[str, VFS, str]:\n        return self.v2a(join(self.cwd, vpath), r, w, m, d)\n\n    def ftp2fs(self, ftppath: str) -> str:\n        # return self.v2a(ftppath)\n        return ftppath  # self.cwd must be vpath\n\n    def fs2ftp(self, fspath: str) -> str:\n        # raise NotImplementedError()\n        return fspath\n\n    def validpath(self, path: str) -> bool:\n        if \"/.hist/\" in path:\n            if \"/up2k.\" in path or path.endswith(\"/dir.txt\"):\n                raise FSE(\"Access to this file is forbidden\", 1)\n\n        return True\n\n    def open(self, filename: str, mode: str) -> typing.IO[Any]:\n        r = \"r\" in mode\n        w = \"w\" in mode or \"a\" in mode or \"+\" in mode\n\n        ap, vfs, _ = self.rv2a(filename, r, w)\n        self.validpath(ap)\n        if w:\n            try:\n                st = bos.stat(ap)\n                td = time.time() - st.st_mtime\n                need_unlink = True\n            except:\n                need_unlink = False\n                td = 0\n\n            xbu = vfs.flags.get(\"xbu\")\n            if xbu:\n                hr = runhook(\n                    self.log,\n                    None,\n                    self.hub.up2k,\n                    \"xbu.ftp\",\n                    xbu,\n                    ap,\n                    filename,\n                    \"\",\n                    \"\",\n                    \"\",\n                    0,\n                    0,\n                    \"1.3.8.7\",\n                    time.time(),\n                    None,\n                )\n                t = hr.get(\"rejectmsg\") or \"\"\n                if t or hr.get(\"rc\") != 0:\n                    if not t:\n                        t = \"upload blocked by xbu server config: %r\" % (filename,)\n                    self.log(t, 3)\n                    raise FSE(t)\n\n        if w and need_unlink:  # type: ignore  # !rm\n            assert td  # type: ignore  # !rm\n            if td >= -1 and td <= self.args.ftp_wt:\n                # within permitted timeframe; allow overwrite or resume\n                do_it = True\n            elif self.args.no_del or self.args.ftp_no_ow:\n                # file too old, or overwrite not allowed; reject\n                do_it = False\n            else:\n                # allow overwrite if user has delete permission\n                # (avoids win2000 freaking out and deleting the server copy without uploading its own)\n                try:\n                    self.rv2a(filename, False, True, False, True)\n                    do_it = True\n                except:\n                    do_it = False\n\n            if not do_it:\n                raise FSE(\"File already exists\")\n\n            # Don't unlink file for append mode\n            elif \"a\" not in mode:\n                wunlink(self.log, ap, VF_CAREFUL)\n\n        ret = open(fsenc(ap), mode, self.args.iobuf)\n        if w and \"fperms\" in vfs.flags:\n            set_fperms(ret, vfs.flags)\n\n        return ret\n\n    def chdir(self, path: str) -> None:\n        nwd = join(self.cwd, path)\n        vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)\n        if not vfs.realpath:\n            self.cwd = nwd\n            return\n\n        ap = vfs.canonical(rem)\n        try:\n            st = bos.stat(ap)\n            if not stat.S_ISDIR(st.st_mode):\n                raise Exception()\n        except:\n            # returning 550 is library-default and suitable\n            raise FSE(\"No such file or directory\")\n\n        avfs = vfs.chk_ap(ap, st)\n        if not avfs:\n            raise FSE(\"Permission denied\", 1)\n\n        self.cwd = nwd\n\n    def mkdir(self, path: str) -> None:\n        ap, vfs, _ = self.rv2a(path, w=True)\n        bos.makedirs(ap, vf=vfs.flags)  # filezilla expects this\n\n    def listdir(self, path: str) -> list[str]:\n        vpath = join(self.cwd, path)\n        try:\n            ap, vfs, rem = self.v2a(vpath, True, False)\n            if not bos.path.isdir(ap):\n                raise FSE(\"No such file or directory\", 1)\n\n            fsroot, vfs_ls1, vfs_virt = vfs.ls(\n                rem,\n                self.uname,\n                not self.args.no_scandir,\n                [[True, False], [False, True]],\n                throw=True,\n            )\n            vfs_ls = [x[0] for x in vfs_ls1]\n            vfs_ls.extend(vfs_virt.keys())\n\n            if self.uname not in vfs.axs.udot:\n                vfs_ls = exclude_dotfiles(vfs_ls)\n\n            vfs_ls.sort()\n            return vfs_ls\n        except Exception as ex:\n            # panic on malicious names\n            if getattr(ex, \"severity\", 0):\n                raise\n\n            if vpath.strip(\"/\"):\n                # display write-only folders as empty\n                return []\n\n            # return list of accessible volumes\n            ret = []\n            for vn in self.hub.asrv.vfs.all_vols.values():\n                if \"/\" in vn.vpath or not vn.vpath:\n                    continue  # only include toplevel-mounted vols\n\n                try:\n                    self.hub.asrv.vfs.get(vn.vpath, self.uname, True, False)\n                    ret.append(vn.vpath)\n                except:\n                    pass\n\n            ret.sort()\n            return ret\n\n    def rmdir(self, path: str) -> None:\n        ap = self.rv2a(path, d=True)[0]\n        try:\n            bos.rmdir(ap)\n        except OSError as e:\n            if e.errno != errno.ENOENT:\n                raise\n\n    def remove(self, path: str) -> None:\n        if self.args.no_del:\n            raise FSE(\"The delete feature is disabled in server config\")\n\n        vp = join(self.cwd, path).lstrip(\"/\")\n        try:\n            self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [], False, False)\n        except Exception as ex:\n            raise FSE(str(ex))\n\n    def rename(self, src: str, dst: str) -> None:\n        if self.args.no_mv:\n            raise FSE(\"The rename/move feature is disabled in server config\")\n\n        svp = join(self.cwd, src).lstrip(\"/\")\n        dvp = join(self.cwd, dst).lstrip(\"/\")\n        try:\n            self.hub.up2k.handle_mv(\"\", self.uname, self.h.cli_ip, svp, dvp)\n        except Exception as ex:\n            raise FSE(str(ex))\n\n    def chmod(self, path: str, mode: str) -> None:\n        pass\n\n    def stat(self, path: str) -> os.stat_result:\n        try:\n            ap = self.rv2a(path, r=True)[0]\n            return bos.stat(ap)\n        except FSE as ex:\n            if ex.severity:\n                raise\n\n            ap = self.rv2a(path)[0]\n            st = bos.stat(ap)\n            if not stat.S_ISDIR(st.st_mode):\n                raise\n\n            return st\n\n    def utime(self, path: str, timeval: float) -> None:\n        ap = self.rv2a(path, w=True)[0]\n        bos.utime_c(logging.warning, ap, int(timeval), False)\n\n    def lstat(self, path: str) -> os.stat_result:\n        ap = self.rv2a(path)[0]\n        return bos.stat(ap)\n\n    def isfile(self, path: str) -> bool:\n        try:\n            st = self.stat(path)\n            return stat.S_ISREG(st.st_mode)\n        except Exception as ex:\n            if getattr(ex, \"severity\", 0):\n                raise\n\n            return False  # expected for mojibake in ftp_SIZE()\n\n    def islink(self, path: str) -> bool:\n        ap = self.rv2a(path)[0]\n        return bos.path.islink(ap)\n\n    def isdir(self, path: str) -> bool:\n        try:\n            st = self.stat(path)\n            return stat.S_ISDIR(st.st_mode)\n        except Exception as ex:\n            if getattr(ex, \"severity\", 0):\n                raise\n\n            return True\n\n    def getsize(self, path: str) -> int:\n        ap = self.rv2a(path)[0]\n        return bos.path.getsize(ap)\n\n    def getmtime(self, path: str) -> float:\n        ap = self.rv2a(path)[0]\n        return bos.path.getmtime(ap)\n\n    def realpath(self, path: str) -> str:\n        return path\n\n    def lexists(self, path: str) -> bool:\n        ap = self.rv2a(path)[0]\n        return bos.path.lexists(ap)\n\n    def get_user_by_uid(self, uid: int) -> str:\n        return \"root\"\n\n    def get_group_by_uid(self, gid: int) -> str:\n        return \"root\"\n\n\nclass FtpHandler(FTPHandler):\n    abstracted_fs = FtpFs\n    hub: \"SvcHub\"\n    args: argparse.Namespace\n    uname: str\n\n    def __init__(self, conn: Any, server: Any, ioloop: Any = None) -> None:\n        self.hub: \"SvcHub\" = FtpHandler.hub\n        self.args: argparse.Namespace = FtpHandler.args\n        self.uname = \"*\"\n\n        if PY2:\n            FTPHandler.__init__(self, conn, server, ioloop)\n        else:\n            super(FtpHandler, self).__init__(conn, server, ioloop)\n\n        cip = self.remote_ip\n        if cip.startswith(\"::ffff:\"):\n            cip = cip[7:]\n\n        if self.args.ftp_ipa_nm and not self.args.ftp_ipa_nm.map(cip):\n            logging.warning(\"client rejected (--ftp-ipa): %s\", cip)\n            self.connected = False\n            conn.close()\n            return\n\n        self.cli_ip = cip\n\n        # abspath->vpath mapping to resolve log_transfer paths\n        self.vfs_map: dict[str, str] = {}\n\n        # reduce non-debug logging\n        self.log_cmds_list = [x for x in self.log_cmds_list if x not in (\"CWD\", \"XCWD\")]\n\n    def ftp_STOR(self, file: str, mode: str = \"w\") -> Any:\n        # Optional[str]\n        vp = join(self.fs.cwd, file).lstrip(\"/\")\n        try:\n            ap, vfs, rem = self.fs.v2a(vp, w=True)\n        except Exception as ex:\n            self.respond(\"550 %s\" % (ex,), logging.info)\n            return\n        self.vfs_map[ap] = vp\n        xbu = vfs.flags.get(\"xbu\")\n        if xbu:\n            hr = runhook(\n                None,\n                None,\n                self.hub.up2k,\n                \"xbu.ftpd\",\n                xbu,\n                ap,\n                vp,\n                \"\",\n                self.uname,\n                self.hub.asrv.vfs.get_perms(vp, self.uname),\n                0,\n                0,\n                self.cli_ip,\n                time.time(),\n                None,\n            )\n            t = hr.get(\"rejectmsg\") or \"\"\n            if t or hr.get(\"rc\") != 0:\n                if not t:\n                    t = \"Upload blocked by xbu server config: %r\" % (vp,)\n                self.respond(\"550 %s\" % (t,), logging.info)\n                return\n\n        # print(\"ftp_STOR: {} {} => {}\".format(vp, mode, ap))\n        ret = FTPHandler.ftp_STOR(self, file, mode)\n        # print(\"ftp_STOR: {} {} OK\".format(vp, mode))\n        return ret\n\n    def log_transfer(\n        self,\n        cmd: str,\n        filename: bytes,\n        receive: bool,\n        completed: bool,\n        elapsed: float,\n        bytes: int,\n    ) -> Any:\n        # None\n        ap = filename.decode(\"utf-8\", \"replace\")\n        vp = self.vfs_map.pop(ap, None)\n        # print(\"xfer_end: {} => {}\".format(ap, vp))\n        if vp:\n            vp, fn = os.path.split(vp)\n            vfs, rem = self.hub.asrv.vfs.get(vp, self.uname, False, True)\n            vfs, rem = vfs.get_dbv(rem)\n            self.hub.up2k.hash_file(\n                vfs.realpath,\n                vfs.vpath,\n                vfs.flags,\n                rem,\n                fn,\n                self.cli_ip,\n                time.time(),\n                self.uname,\n            )\n\n        return FTPHandler.log_transfer(\n            self, cmd, filename, receive, completed, elapsed, bytes\n        )\n\n\ntry:\n    from pyftpdlib.handlers import TLS_FTPHandler\n\n    class SftpHandler(FtpHandler, TLS_FTPHandler):\n        pass\n\nexcept:\n    pass\n\n\nclass Ftpd(object):\n    def __init__(self, hub: \"SvcHub\") -> None:\n        self.hub = hub\n        self.args = hub.args\n\n        hs = []\n        if self.args.ftp:\n            hs.append([FtpHandler, self.args.ftp])\n        if self.args.ftps:\n            try:\n                h1 = SftpHandler\n            except:\n                t = \"\\nftps requires pyopenssl;\\nplease run the following:\\n\\n  {} -m pip install --user pyopenssl\\n\"\n                print(t.format(pybin))\n                sys.exit(1)\n\n            h1.certfile = self.args.cert\n            h1.tls_control_required = True\n            h1.tls_data_required = True\n\n            hs.append([h1, self.args.ftps])\n\n        for h_lp in hs:\n            h2, lp = h_lp\n            FtpHandler.hub = h2.hub = hub\n            FtpHandler.args = h2.args = hub.args\n            FtpHandler.authorizer = h2.authorizer = FtpAuth(hub)\n\n            if self.args.ftp_pr:\n                p1, p2 = [int(x) for x in self.args.ftp_pr.split(\"-\")]\n                if self.args.ftp and self.args.ftps:\n                    # divide port range in half\n                    d = int((p2 - p1) / 2)\n                    if lp == self.args.ftp:\n                        p2 = p1 + d\n                    else:\n                        p1 += d + 1\n\n                h2.passive_ports = list(range(p1, p2 + 1))\n\n            if self.args.ftp_nat:\n                h2.masquerade_address = self.args.ftp_nat\n\n        lgr = logging.getLogger(\"pyftpdlib\")\n        lgr.setLevel(logging.DEBUG if self.args.ftpv else logging.INFO)\n\n        ips = self.args.ftp_i\n        if \"::\" in ips:\n            ips.append(\"0.0.0.0\")\n\n        ips = [x for x in ips if not x.startswith((\"unix:\", \"fd:\"))]\n\n        if self.args.ftp4:\n            ips = [x for x in ips if \":\" not in x]\n\n        if not ips:\n            lgr.fatal(\"cannot start ftp-server; no compatible IPs in -i\")\n            return\n\n        ips = list(ODict.fromkeys(ips))  # dedup\n\n        ioloop = IOLoop()\n        for ip in ips:\n            for h, lp in hs:\n                try:\n                    FTPServer((ip, int(lp)), h, ioloop)\n                except:\n                    if ip != \"0.0.0.0\" or \"::\" not in ips:\n                        raise\n\n        Daemon(ioloop.loop, \"ftp\")\n\n\ndef join(p1: str, p2: str) -> str:\n    w = os.path.join(p1, p2.replace(\"\\\\\", \"/\"))\n    return os.path.normpath(w).replace(\"\\\\\", \"/\")\n"
  },
  {
    "path": "copyparty/httpcli.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport argparse  # typechk\nimport copy\nimport errno\nimport hashlib\nimport itertools\nimport json\nimport os\nimport random\nimport re\nimport socket\nimport stat\nimport sys\nimport threading  # typechk\nimport time\nimport uuid\nfrom datetime import datetime\nfrom operator import itemgetter\n\nimport jinja2  # typechk\nfrom ipaddress import IPv6Network\n\ntry:\n    if os.environ.get(\"PRTY_NO_LZMA\"):\n        raise Exception()\n\n    import lzma\nexcept:\n    pass\n\nfrom .__init__ import ANYWIN, RES, RESM, TYPE_CHECKING, EnvParams, unicode\nfrom .__version__ import S_VERSION\nfrom .authsrv import LEELOO_DALLAS, VFS  # typechk\nfrom .bos import bos\nfrom .qrkode import QrCode, qr2svg, qrgen\nfrom .star import StreamTar\nfrom .sutil import StreamArc, gfilter\nfrom .szip import StreamZip\nfrom .up2k import up2k_chunksize\nfrom .util import unquote  # type: ignore\nfrom .util import (\n    APPLESAN_RE,\n    BITNESS,\n    DAV_ALLPROPS,\n    E_SCK_WR,\n    HAVE_SQLITE3,\n    HTTPCODE,\n    SAFE_MIMES,\n    UTC,\n    VPTL_MAC,\n    VPTL_OS,\n    VPTL_WIN,\n    Garda,\n    MultipartParser,\n    ODict,\n    Pebkac,\n    UnrecvEOF,\n    WrongPostKey,\n    absreal,\n    afsenc,\n    alltrace,\n    atomic_move,\n    b64dec,\n    eol_conv,\n    exclude_dotfiles,\n    exclude_dotfiles_ls,\n    formatdate,\n    fsenc,\n    gen_content_disposition,\n    gen_filekey,\n    gen_filekey_dbg,\n    gencookie,\n    get_df,\n    get_spd,\n    guess_mime,\n    gzip,\n    gzip_file_orig_sz,\n    gzip_orig_sz,\n    has_resource,\n    hashcopy,\n    hidedir,\n    html_bescape,\n    html_escape,\n    html_sh_esc,\n    humansize,\n    ipnorm,\n    json_hesc,\n    justcopy,\n    load_resource,\n    loadpy,\n    log_reloc,\n    min_ex,\n    open_nolock,\n    pathmod,\n    quotep,\n    rand_name,\n    read_header,\n    read_socket,\n    read_socket_chunked,\n    read_socket_unbounded,\n    read_utf8,\n    relchk,\n    ren_open,\n    runhook,\n    s2hms,\n    s3enc,\n    safe_mime,\n    sanitize_fn,\n    sanitize_vpath,\n    sendfile_kern,\n    sendfile_py,\n    set_fperms,\n    stat_resource,\n    str_anchor,\n    ub64dec,\n    ub64enc,\n    ujoin,\n    undot,\n    unescape_cookie,\n    unquotep,\n    vjoin,\n    vol_san,\n    vroots,\n    vsplit,\n    wunlink,\n    yieldfile,\n)\n\nif True:  # pylint: disable=using-constant-test\n    import typing\n    from typing import (\n        Any,\n        Generator,\n        Iterable,\n        Match,\n        Optional,\n        Pattern,\n        Sequence,\n        Type,\n        Union,\n    )\n\nif TYPE_CHECKING:\n    from .httpconn import HttpConn\n\nif not hasattr(socket, \"AF_UNIX\"):\n    setattr(socket, \"AF_UNIX\", -9001)\n\n_ = (argparse, threading)\n\nUSED4SEC = {\"usedforsecurity\": False} if sys.version_info > (3, 9) else {}\n\nALL_COOKIES = \"k304 no304 js idxh dots cppwd cppws\".split()\n\nBADXFF = \" due to dangerous misconfiguration (the http-header specified by --xff-hdr was received from an untrusted reverse-proxy)\"\nBADXFF2 = \". Some copyparty features are now disabled as a safety measure.\\n\\n\\n\"\nBADXFP = ', 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\"'\nBADXFFB = \"<b>NOTE: serverlog has a message regarding your reverse-proxy config</b>\"\nBADVER = '<a class=\"r\" href=\"https://github.com/9001/copyparty/security/advisories\">Please upgrade copyparty; Your version has a vulnerability</a><p>(only users with permission \"a\" or \"A\" can see this message)</p>'\n\nH_CONN_KEEPALIVE = \"Connection: Keep-Alive\"\nH_CONN_CLOSE = \"Connection: Close\"\n\nRSS_SORT = {\"m\": \"mt\", \"u\": \"at\", \"n\": \"fn\", \"s\": \"sz\"}\nACODE2_FMT = set([\"opus\", \"owa\", \"caf\", \"mp3\", \"flac\", \"wav\"])\nIDX_HTML = set([\"index.htm\", \"index.html\"])\n\nA_FILE = os.stat_result(\n    (0o644, -1, -1, 1, 1000, 1000, 8, 0x39230101, 0x39230101, 0x39230101)\n)\n\nRE_CC = re.compile(r\"[\\x00-\\x1f\\x7f]\")  # search always faster\nRE_USAFE = re.compile(r'[\\x00-\\x1f\\x7f<>\"]')  # search always faster\nRE_HSAFE = re.compile(r\"[\\x00-\\x1f\\x7f<>\\\"'&]\")  # search always much faster\nRE_HOST = re.compile(r\"[^][0-9a-zA-Z.:_-]\")  # search faster <=17ch\nRE_MHOST = re.compile(r\"^[][0-9a-zA-Z.:_-]+$\")  # match faster >=18ch\nRE_K = re.compile(r\"[^0-9a-zA-Z_-]\")  # search faster <=17ch\nRE_HTTP1 = re.compile(r\"(GET|HEAD|POST|PUT) [^ ]+ HTTP/1.1$\")\nRE_HR = re.compile(r\"[<>\\\"'&]\")\nRE_MDV = re.compile(r\"(.*)\\.([0-9]+\\.[0-9]{3})(\\.[Mm][Dd])$\")\nRE_RSS_KW = re.compile(r\"(\\{[^} ]+\\})\")\nRE_SETCK = re.compile(r\"[^0-9a-z=]\")\n\nUPARAM_CC_OK = set(\"doc move tree\".split())\n\nPERMS_rwh = [\n    [True, False],\n    [False, True],\n    [False, False, False, False, False, False, True],\n]\n\n\ndef _build_zip_xcode() -> Sequence[str]:\n    ret = \"opus mp3 flac wav p\".split()\n    for codec in (\"j\", \"w\", \"x\"):\n        for suf in (\"\", \"f\", \"f3\", \"3\"):\n            ret.append(\"%s%s\" % (codec, suf))\n    return ret\n\n\nZIP_XCODE_L = _build_zip_xcode()\nZIP_XCODE_S = set(ZIP_XCODE_L)\n\n\ndef _arg2cfg(txt: str) -> str:\n    return re.sub(r' \"--([^=]{3,12})=', r' global-option \"\\1: ', txt)\n\n\nclass HttpCli(object):\n    \"\"\"\n    Spawned by HttpConn to process one http transaction\n    \"\"\"\n\n    def __init__(self, conn: \"HttpConn\") -> None:\n        assert conn.sr  # !rm\n\n        empty_stringlist: list[str] = []\n\n        self.t0 = time.time()\n        self.conn = conn\n        self.u2mutex = conn.u2mutex  # mypy404\n        self.s = conn.s\n        self.sr = conn.sr\n        self.ip = conn.addr[0]\n        self.addr: tuple[str, int] = conn.addr\n        self.args = conn.args  # mypy404\n        self.E: EnvParams = self.args.E\n        self.asrv = conn.asrv  # mypy404\n        self.ico = conn.ico  # mypy404\n        self.thumbcli = conn.thumbcli  # mypy404\n        self.u2fh = conn.u2fh  # mypy404\n        self.pipes = conn.pipes  # mypy404\n        self.log_func = conn.log_func  # mypy404\n        self.log_src = conn.log_src  # mypy404\n        self.gen_fk = self._gen_fk if self.args.log_fk else gen_filekey\n        self.tls = self.is_https = hasattr(self.s, \"cipher\")\n        self.is_vproxied = bool(self.args.R)\n\n        # placeholders; assigned by run()\n        self.keepalive = False\n        self.in_hdr_recv = True\n        self.headers: dict[str, str] = {}\n        self.mode = \" \"  # http verb\n        self.req = \" \"\n        self.http_ver = \"\"\n        self.hint = \"\"\n        self.host = \" \"\n        self.ua = \" \"\n        self.is_rclone = False\n        self.ouparam: dict[str, str] = {}\n        self.uparam: dict[str, str] = {}\n        self.cookies: dict[str, str] = {}\n        self.avn: Optional[VFS] = None\n        self.vn = self.asrv.vfs\n        self.rem = \" \"\n        self.vpath = \" \"\n        self.vpaths = \" \"\n        self.dl_id = \"\"\n        self.gctx = \" \"  # additional context for garda\n        self.trailing_slash = True\n        self.uname = \"*\"\n        self.pw = \"\"\n        self.rvol = self.wvol = self.avol = empty_stringlist\n        self.do_log = True\n        self.can_read = False\n        self.can_write = False\n        self.can_move = False\n        self.can_delete = False\n        self.can_get = False\n        self.can_upget = False\n        self.can_html = False\n        self.can_admin = False\n        self.can_dot = False\n        self.out_headerlist: list[tuple[str, str]] = []\n        self.out_headers: dict[str, str] = {}\n        # post\n        self.parser: Optional[MultipartParser] = None\n        # end placeholders\n\n        self.html_head = \"\"\n\n    def log(self, msg: str, c: Union[int, str] = 0) -> None:\n        ptn = self.asrv.re_pwd\n        if ptn and ptn.search(msg):\n            if self.asrv.ah.on:\n                msg = ptn.sub(\"\\033[7m pw \\033[27m\", msg)\n            else:\n                msg = ptn.sub(self.unpwd, msg)\n\n        self.log_func(self.log_src, msg, c)\n\n    def unpwd(self, m: Match[str]) -> str:\n        a, b, c = m.groups()\n        uname = self.asrv.iacct.get(b) or self.asrv.sesa.get(b)\n        return \"%s\\033[7m %s \\033[27m%s\" % (a, uname, c)\n\n    def _assert_safe_rem(self, rem: str) -> None:\n        # sanity check to prevent any disasters\n        # (this function hopefully serves no purpose; validation has already happened at this point, this only exists as a last-ditch effort just in case)\n        if rem.startswith((\"/\", \"../\")) or \"/../\" in rem:\n            raise Exception(\"that was close\")\n\n    def _gen_fk(self, alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:\n        return gen_filekey_dbg(\n            alg, salt, fspath, fsize, inode, self.log, self.args.log_fk\n        )\n\n    def j2s(self, name: str, **ka: Any) -> str:\n        tpl = self.conn.hsrv.j2[name]\n        ka[\"r\"] = self.args.SR if self.is_vproxied else \"\"\n        ka[\"ts\"] = self.conn.hsrv.cachebuster()\n        ka[\"lang\"] = self.cookies.get(\"cplng\") or self.args.lang\n        ka[\"favico\"] = self.args.favico\n        ka[\"s_doctitle\"] = self.args.doctitle\n        ka[\"tcolor\"] = self.vn.flags[\"tcolor\"]\n\n        if self.args.js_other and \"js\" not in ka:\n            zs = self.args.js_other\n            zs += \"&\" if \"?\" in zs else \"?\"\n            ka[\"js\"] = zs\n\n        if \"html_head_d\" in self.vn.flags:\n            ka[\"this\"] = self\n            self._build_html_head(ka)\n\n        ka[\"html_head\"] = self.html_head\n        return tpl.render(**ka)  # type: ignore\n\n    def j2j(self, name: str) -> jinja2.Template:\n        return self.conn.hsrv.j2[name]\n\n    def run(self) -> bool:\n        \"\"\"returns true if connection can be reused\"\"\"\n        self.out_headers = {\n            \"Cache-Control\": \"no-store, max-age=0\",\n        }\n\n        if self.args.early_ban and self.is_banned():\n            return False\n\n        if self.conn.ipa_nm and not self.conn.ipa_nm.map(self.conn.addr[0]):\n            self.log(\"client rejected (--ipa)\", 3)\n            self.terse_reply(b\"\", 500)\n            return False\n\n        try:\n            self.s.settimeout(2)\n            headerlines = read_header(self.sr, self.args.s_thead, self.args.s_thead)\n            self.in_hdr_recv = False\n            if not headerlines:\n                return False\n\n            try:\n                self.mode, self.req, self.http_ver = headerlines[0].split(\" \")\n\n                # normalize incoming headers to lowercase;\n                # outgoing headers however are Correct-Case\n                for header_line in headerlines[1:]:\n                    k, zs = header_line.split(\":\", 1)\n                    self.headers[k.lower()] = zs.strip()\n                    if zs.endswith(\" HTTP/1.1\") and RE_HTTP1.search(zs):\n                        raise Exception()\n            except:\n                headerlines = [repr(x) for x in headerlines]\n                msg = \"#[ \" + \" ]\\n#[ \".join(headerlines) + \" ]\"\n                raise Pebkac(400, \"bad headers\", log=msg)\n\n        except Pebkac as ex:\n            self.mode = \"GET\"\n            self.req = \"[junk]\"\n            self.http_ver = \"HTTP/1.1\"\n            # self.log(\"pebkac at httpcli.run #1: \" + repr(ex))\n            self.keepalive = False\n            h = {\"WWW-Authenticate\": 'Basic realm=\"a\"'} if ex.code == 401 else {}\n            try:\n                self.loud_reply(unicode(ex), status=ex.code, headers=h, volsan=True)\n            except:\n                pass\n\n            if ex.log:\n                self.log(\"additional error context:\\n\" + ex.log, 6)\n\n            return False\n\n        self.sr.nb = 0\n        self.conn.hsrv.nreq += 1\n\n        self.ua = self.headers.get(\"user-agent\", \"\")\n        self.is_rclone = self.ua.startswith(\"rclone/\")\n\n        zs = self.headers.get(\"connection\", \"\").lower()\n        self.keepalive = \"close\" not in zs and (\n            self.http_ver != \"HTTP/1.0\" or zs == \"keep-alive\"\n        )\n\n        if (\n            \"transfer-encoding\" in self.headers\n            and self.headers[\"transfer-encoding\"].lower() != \"identity\"\n        ):\n            self.sr.te = 1\n            if \"content-length\" in self.headers:\n                # rfc9112:6.2: ignore CL if TE\n                self.keepalive = False\n                self.headers.pop(\"content-length\")\n                t = \"suspicious request (has both TE and CL); ignoring CL and disabling keepalive\"\n                self.log(t, 3)\n\n        self.host = self.headers.get(\"host\") or \"\"\n        if not self.host:\n            if self.s.family == socket.AF_UNIX:\n                self.host = self.args.name\n            else:\n                zs = \"%s:%s\" % self.s.getsockname()[:2]\n                self.host = zs[7:] if zs.startswith(\"::ffff:\") else zs\n\n        trusted_xff = False\n        n = self.args.rproxy\n        if n:\n            zso = self.headers.get(self.args.xff_hdr)\n            if zso:\n                if n > 0:\n                    n -= 1\n\n                zsl = zso.split(\",\")\n                try:\n                    cli_ip = zsl[n].strip()\n                except:\n                    cli_ip = self.ip\n                    self.bad_xff = True\n                    if self.args.rproxy != 9999999:\n                        t = \"global-option --rproxy %d could not be used (out-of-bounds) for the received header [%s]\"\n                        self.log(t % (self.args.rproxy, zso) + BADXFF2, c=3)\n                    else:\n                        zsl = [\n                            \"  rproxy: %d   if this client's IP-address is [%s]\"\n                            % (-1 - zd, zs.strip())\n                            for zd, zs in enumerate(zsl[::-1])\n                        ]\n                        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:'\n                        t = t % (self.args.xff_hdr, zso)\n                        t = \"%s\\n\\n%s\\n\" % (t, \"\\n\".join(zsl))\n\n                        zs = self.headers.get(self.args.xf_proto)\n                        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'\"\n                        t += t2 % (self.args.xf_proto, zs or \"NOT-PROVIDED\")\n                        if not zs:\n                            t += \". Because the header is not provided by the reverse-proxy, you must either fix the reverseproxy config\"\n                            t += BADXFP\n                        zs = self.headers.get(self.args.xf_host)\n                        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\"\n                        t += t2 % (self.args.xf_host, zs or \"NOT-PROVIDED\")\n                        if not zs:\n                            zs = self.headers.get(\"host\")\n                            t2 = \". Because the header is not provided by the reverse-proxy, copyparty is using the standard [Host] header which has the value [%s]\"\n                            t += t2 % (zs or \"NOT-PROVIDED\")\n                            if zs:\n                                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)\"\n                        if self.args.c:\n                            t = _arg2cfg(t)\n                        self.log(t + \"\\n\\n\\n\", 3)\n\n                pip = self.conn.addr[0]\n                xffs = self.conn.xff_nm\n                if xffs and not xffs.map(pip):\n                    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'\n                    if self.headers.get(\"cf-connecting-ip\"):\n                        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\"'\n                    else:\n                        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\"'\n                    t += BADXFF2\n\n                    if \".\" in pip:\n                        zs = \".\".join(pip.split(\".\")[:2]) + \".0.0/16\"\n                    else:\n                        zs = IPv6Network(pip + \"/64\", False).compressed\n\n                    zs2 = ' or \"--xff-src=lan\"' if self.conn.xff_lan.map(pip) else \"\"\n                    t = t % (self.args.xff_hdr, pip, cli_ip, zso, zs, zs2)\n                    if self.args.c:\n                        t = _arg2cfg(t)\n                    self.log(t, 3)\n                    self.bad_xff = True\n                else:\n                    self.ip = cli_ip\n                    self.log_src = self.conn.set_rproxy(self.ip)\n                    self.host = self.headers.get(self.args.xf_host, self.host)\n                    try:\n                        self.is_https = len(self.headers[self.args.xf_proto]) == 5\n                    except:\n                        if self.args.xf_proto_fb:\n                            self.is_https = len(self.args.xf_proto_fb) == 5\n                        else:\n                            self.bad_xff = True\n                            self.host = \"example.com\"\n                            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'\n                            t = t % (self.args.xf_proto, BADXFP, BADXFF2)\n                            if self.args.c:\n                                t = _arg2cfg(t)\n                            self.log(t, 3)\n\n                    # the semantics of trusted_xff and bad_xff are different;\n                    # trusted_xff is whether the connection came from a trusted reverseproxy,\n                    # regardless of whether the client ip detection is correctly configured\n                    # (the primary safeguard for idp is --idp-h-key)\n                    trusted_xff = True\n\n        m = RE_HOST.search(self.host)\n        if m and self.host != self.args.name:\n            zs = self.host\n            t = \"malicious user; illegal Host header; req(%r) host(%r) => %r\"\n            self.log(t % (self.req, zs, zs[m.span()[0] :]), 1)\n            self.cbonk(self.conn.hsrv.gmal, zs, \"bad_host\", \"illegal Host header\")\n            self.terse_reply(b\"illegal Host header\", 400)\n            return False\n\n        if self.is_banned():\n            return False\n\n        if self.conn.ipar_nm and not self.conn.ipar_nm.map(self.ip):\n            self.log(\"client rejected (--ipar)\", 3)\n            self.terse_reply(b\"\", 500)\n            return False\n\n        if self.conn.aclose:\n            nka = self.conn.aclose\n            ip = ipnorm(self.ip)\n            if ip in nka:\n                rt = nka[ip] - time.time()\n                if rt < 0:\n                    self.log(\"client uncapped\", 3)\n                    del nka[ip]\n                else:\n                    self.keepalive = False\n\n        ptn: Optional[Pattern[str]] = self.conn.lf_url  # mypy404\n        self.do_log = not ptn or not ptn.search(self.req)\n\n        if self.args.ihead and self.do_log:\n            keys = self.args.ihead\n            if \"*\" in keys:\n                keys = list(sorted(self.headers.keys()))\n\n            for k in keys:\n                zso = self.headers.get(k)\n                if zso is not None:\n                    self.log(\"[H] {}: \\033[33m[{}]\".format(k, zso), 6)\n\n        if \"&\" in self.req and \"?\" not in self.req:\n            self.hint = \"did you mean '?' instead of '&'\"\n\n        if self.args.uqe and \"/.uqe/\" in self.req:\n            try:\n                vpath, query = self.req.split(\"?\")[0].split(\"/.uqe/\")\n                query = query.split(\"/\")[0]  # discard trailing junk\n                # (usually a \"filename\" to trick discord into behaving)\n                query = ub64dec(query.encode(\"utf-8\")).decode(\"utf-8\", \"replace\")\n                if query.startswith(\"/\"):\n                    self.req = \"%s/?%s\" % (vpath, query[1:])\n                else:\n                    self.req = \"%s?%s\" % (vpath, query)\n            except Exception as ex:\n                t = \"bad uqe in request [%s]: %r\" % (self.req, ex)\n                self.loud_reply(t, status=400)\n                return False\n\n        m = RE_USAFE.search(self.req)\n        if m:\n            zs = self.req\n            t = \"malicious user; Cc in req0 %r => %r\"\n            self.log(t % (zs, zs[m.span()[0] :]), 1)\n            self.cbonk(self.conn.hsrv.gmal, zs, \"cc_r0\", \"Cc in req0\")\n            self.terse_reply(b\"\", 500)\n            return False\n\n        # split req into vpath + uparam\n        uparam = {}\n        if \"?\" not in self.req:\n            vpath = unquotep(self.req)  # not query, so + means +\n            self.trailing_slash = vpath.endswith(\"/\")\n            vpath = undot(vpath)\n        else:\n            vpath, arglist = self.req.split(\"?\", 1)\n            vpath = unquotep(vpath)\n            self.trailing_slash = vpath.endswith(\"/\")\n            vpath = undot(vpath)\n\n            re_k = RE_K\n            ptn_cc = RE_CC\n            k_safe = UPARAM_CC_OK\n            for k in arglist.split(\"&\"):\n                sv = \"\"\n                if \"=\" in k:\n                    k, zs = k.split(\"=\", 1)\n                    # x-www-form-urlencoded (url query part) uses\n                    # either + or %20 for 0x20 so handle both\n                    sv = unquotep(zs.strip().replace(\"+\", \" \"))\n\n                m = re_k.search(k)\n                if m:\n                    t = \"malicious user; bad char in query key; req(%r) qk(%r) => %r\"\n                    self.log(t % (self.req, k, k[m.span()[0] :]), 1)\n                    self.cbonk(self.conn.hsrv.gmal, self.req, \"bc_q\", \"illegal qkey\")\n                    self.terse_reply(b\"\", 500)\n                    return False\n\n                k = k.lower()\n                uparam[k] = sv\n\n                if k in k_safe:\n                    continue\n\n                zs = \"%s=%s\" % (k, sv)\n                m = ptn_cc.search(zs)\n                if not m:\n                    continue\n\n                t = \"malicious user; Cc in query; req(%r) qp(%r) => %r\"\n                self.log(t % (self.req, zs, zs[m.span()[0] :]), 1)\n                self.cbonk(self.conn.hsrv.gmal, self.req, \"cc_q\", \"Cc in query\")\n                self.terse_reply(b\"\", 500)\n                return False\n\n            if \"k\" in uparam:\n                m = re_k.search(uparam[\"k\"])\n                if m:\n                    zs = uparam[\"k\"]\n                    t = \"malicious user; illegal filekey; req(%r) k(%r) => %r\"\n                    self.log(t % (self.req, zs, zs[m.span()[0] :]), 1)\n                    self.cbonk(self.conn.hsrv.gmal, zs, \"bad_k\", \"illegal filekey\")\n                    self.terse_reply(b\"illegal filekey\", 400)\n                    return False\n\n        if self.is_vproxied:\n            if vpath.startswith(self.args.R):\n                vpath = vpath[len(self.args.R) + 1 :]\n            else:\n                t = \"incorrect --rp-loc or webserver config; expected vpath starting with %r but got %r\"\n                self.log(t % (self.args.R, vpath), 1)\n                self.is_vproxied = False\n\n        self.ouparam = uparam.copy()\n\n        if self.args.rsp_slp:\n            time.sleep(self.args.rsp_slp)\n            if self.args.rsp_jtr:\n                time.sleep(random.random() * self.args.rsp_jtr)\n\n        zso = self.headers.get(\"cookie\")\n        if zso:\n            if len(zso) > self.args.cookie_cmax:\n                self.loud_reply(\"cookie header too big\", status=400)\n                return False\n            zsll = [x.lstrip().split(\"=\", 1) for x in zso.split(\";\") if \"=\" in x]\n            cookies = {k.rstrip(): unescape_cookie(zs.strip(), k) for k, zs in zsll}\n            cookie_pw = cookies.get(\"cppws\" if self.is_https else \"cppwd\") or \"\"\n            if \"b\" in cookies and \"b\" not in uparam:\n                uparam[\"b\"] = cookies[\"b\"]\n            if len(cookies) > self.args.cookie_nmax:\n                self.loud_reply(\"too many cookies\", status=400)\n        else:\n            cookies = {}\n            cookie_pw = \"\"\n\n        if len(uparam) > 12:\n            t = \"http-request rejected; num.params: %d %r\"\n            self.log(t % (len(uparam), self.req), 3)\n            self.loud_reply(\"u wot m8\", status=400)\n            return False\n\n        if VPTL_OS:\n            vpath = vpath.translate(VPTL_OS)\n\n        self.uparam = uparam\n        self.cookies = cookies\n        self.vpath = vpath\n        self.vpaths = vpath + \"/\" if self.trailing_slash and vpath else vpath\n\n        if \"qr\" in uparam:\n            return self.tx_qr()\n\n        if \"\\x00\" in vpath or (ANYWIN and (\"\\n\" in vpath or \"\\r\" in vpath)):\n            self.log(\"illegal relpath; req(%r) => %r\" % (self.req, \"/\" + self.vpath))\n            self.cbonk(self.conn.hsrv.gmal, self.req, \"bad_vp\", \"invalid relpaths\")\n            return self.tx_404() and False\n\n        zso = self.headers.get(\"authorization\")\n        bauth = \"\"\n        if (\n            zso\n            and not self.args.no_bauth\n            and (not cookie_pw or not self.args.bauth_last)\n        ):\n            try:\n                zb = zso.split(\" \")[1].encode(\"ascii\")\n                zs = b64dec(zb).decode(\"utf-8\")\n                # try \"pwd\", \"x:pwd\", \"pwd:x\"\n                for bauth in [zs] + zs.split(\":\", 1)[::-1]:\n                    if bauth in self.asrv.sesa:\n                        break\n                    hpw = self.asrv.ah.hash(bauth)\n                    if self.asrv.iacct.get(hpw):\n                        break\n            except:\n                pass\n\n        self.pw = (\n            uparam.get(self.args.pw_urlp)\n            or self.headers.get(self.args.pw_hdr)\n            or bauth\n            or cookie_pw\n        )\n        self.uname = (\n            self.asrv.sesa.get(self.pw)\n            or self.asrv.iacct.get(self.asrv.ah.hash(self.pw))\n            or \"*\"\n        )\n\n        if self.args.have_idp_hdrs and (\n            self.uname == \"*\" or self.args.ao_idp_before_pw\n        ):\n            idp_usr = \"\"\n            if self.args.idp_hm_usr:\n                for hn, hmv in self.args.idp_hm_usr_p.items():\n                    zs = self.headers.get(hn)\n                    if zs:\n                        for zs1, zs2 in hmv.items():\n                            if zs == zs1:\n                                idp_usr = zs2\n                                break\n                    if idp_usr:\n                        break\n            for hn in self.args.idp_h_usr:\n                if idp_usr and not self.args.ao_h_before_hm:\n                    break\n                idp_usr = self.headers.get(hn) or idp_usr\n            if idp_usr:\n                idp_grp = (\n                    self.headers.get(self.args.idp_h_grp) or \"\"\n                    if self.args.idp_h_grp\n                    else \"\"\n                )\n                if self.args.idp_chsub:\n                    idp_usr = idp_usr.translate(self.args.idp_chsub_tr)\n                    idp_grp = idp_grp.translate(self.args.idp_chsub_tr)\n\n                if not trusted_xff:\n                    pip = self.conn.addr[0]\n                    xffs = self.conn.xff_nm\n                    trusted_xff = xffs and xffs.map(pip)\n\n                trusted_key = (\n                    not self.args.idp_h_key\n                ) or self.args.idp_h_key in self.headers\n\n                if trusted_key and trusted_xff:\n                    if idp_usr.lower() == LEELOO_DALLAS:\n                        self.loud_reply(\"send her back\", status=403)\n                        return False\n                    self.asrv.idp_checkin(self.conn.hsrv.broker, idp_usr, idp_grp)\n                else:\n                    if not trusted_key:\n                        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\"'\n                        self.log(t % (self.args.idp_h_key, idp_usr, idp_grp), 3)\n\n                    if not trusted_xff:\n                        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'\n                        if not self.args.idp_h_key:\n                            t += \"  Note: you probably also want to specify --idp-h-key <SECRET-HEADER-NAME> for additional security\"\n\n                        pip = self.conn.addr[0]\n                        zs = (\n                            \".\".join(pip.split(\".\")[:2]) + \".\"\n                            if \".\" in pip\n                            else \":\".join(pip.split(\":\")[:4]) + \":\"\n                        ) + \"0.0/16\"\n                        zs2 = (\n                            ' or \"--xff-src=lan\"' if self.conn.xff_lan.map(pip) else \"\"\n                        )\n                        self.log(t % (pip, idp_usr, idp_grp, zs, zs2), 3)\n\n                    idp_usr = \"*\"\n                    idp_grp = \"\"\n\n                if idp_usr in self.asrv.vfs.aread:\n                    self.pw = \"\"\n                    self.uname = idp_usr\n                    if self.args.ao_have_pw or self.args.idp_logout:\n                        self.html_head += \"<script>var is_idp=1</script>\\n\"\n                    else:\n                        self.html_head += \"<script>var is_idp=2</script>\\n\"\n                    zs = self.asrv.ases.get(idp_usr)\n                    if zs:\n                        self.set_idp_cookie(zs)\n                else:\n                    self.log(\"unknown username: %r\" % (idp_usr,), 1)\n\n        if self.args.have_ipu_or_ipr:\n            if self.args.ipu and (self.uname == \"*\" or self.args.ao_ipu_wins):\n                self.uname = self.conn.ipu_iu[self.conn.ipu_nm.map(self.ip)]\n            ipr = self.conn.hsrv.ipr\n            if ipr and self.uname in ipr:\n                if not ipr[self.uname].map(self.ip):\n                    self.log(\"username [%s] rejected by --ipr\" % (self.uname,), 3)\n                    self.uname = \"*\"\n\n        self.rvol = self.asrv.vfs.aread[self.uname]\n        self.wvol = self.asrv.vfs.awrite[self.uname]\n        self.avol = self.asrv.vfs.aadmin[self.uname]\n\n        if self.pw and (\n            self.pw != cookie_pw or self.conn.freshen_pwd + 30 < time.time()\n        ):\n            self.conn.freshen_pwd = time.time()\n            self.get_pwd_cookie(self.pw)\n\n        if self.is_rclone:\n            # dots: always include dotfiles if permitted\n            # 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\n            # b: basic-browser if it tries to parse the html listing\n            uparam[\"dots\"] = \"\"\n            uparam[\"lt\"] = \"\"\n            uparam[\"b\"] = \"\"\n            cookies[\"b\"] = \"\"\n\n        vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)\n        if vn.realpath and (\"xdev\" in vn.flags or \"xvol\" in vn.flags):\n            ap = vn.canonical(rem)\n            avn = vn.chk_ap(ap)\n        else:\n            avn = vn\n\n        if \"bcasechk\" in vn.flags and not vn.casechk(rem, True):\n            return self.tx_404() and False\n\n        try:\n            assert avn  # type: ignore  # !rm\n            (\n                self.can_read,\n                self.can_write,\n                self.can_move,\n                self.can_delete,\n                self.can_get,\n                self.can_upget,\n                self.can_html,\n                self.can_admin,\n                self.can_dot,\n            ) = avn.uaxs[self.uname]\n        except:\n            pass  # default is all-false\n\n        self.avn = avn\n        self.vn = vn  # note: do not dbv due to walk/zipgen\n        self.rem = rem\n\n        self.s.settimeout(self.args.s_tbody or None)\n\n        if \"html_head_s\" in vn.flags:\n            self.html_head += vn.flags[\"html_head_s\"]\n\n        try:\n            cors_k = self._cors()\n            if self.mode in (\"GET\", \"HEAD\"):\n                return self.handle_get() and self.keepalive\n            if self.mode == \"OPTIONS\":\n                return self.handle_options() and self.keepalive\n\n            if not cors_k:\n                host = self.headers.get(\"host\", \"<?>\")\n                origin = self.headers.get(\"origin\", \"<?>\")\n                proto = \"https://\" if self.is_https else \"http://\"\n                guess = \"modifying\" if (origin and host) else \"stripping\"\n                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)\"\n                self.log(t % (self.mode, origin, proto, self.host, host, guess), 3)\n                raise Pebkac(403, \"rejected by cors-check (see fileserver log)\")\n\n            # getattr(self.mode) is not yet faster than this\n            if self.mode == \"POST\":\n                return self.handle_post() and self.keepalive\n            elif self.mode == \"PUT\":\n                return self.handle_put() and self.keepalive\n            elif self.mode == \"PROPFIND\":\n                return self.handle_propfind() and self.keepalive\n            elif self.mode == \"DELETE\":\n                return self.handle_delete() and self.keepalive\n            elif self.mode == \"PROPPATCH\":\n                return self.handle_proppatch() and self.keepalive\n            elif self.mode == \"LOCK\":\n                return self.handle_lock() and self.keepalive\n            elif self.mode == \"UNLOCK\":\n                return self.handle_unlock() and self.keepalive\n            elif self.mode == \"MKCOL\":\n                return self.handle_mkcol() and self.keepalive\n            elif self.mode in (\"MOVE\", \"COPY\"):\n                return self.handle_cpmv() and self.keepalive\n            else:\n                raise Pebkac(400, \"invalid HTTP verb %r\" % (self.mode,))\n\n        except Exception as ex:\n            if not isinstance(ex, Pebkac):\n                pex = Pebkac(500)\n            else:\n                pex: Pebkac = ex  # type: ignore\n\n            try:\n                if pex.code == 999:\n                    self.terse_reply(b\"\", 500)\n                    return False\n\n                post = (\n                    self.mode in (\"POST\", \"PUT\")\n                    or \"content-length\" in self.headers\n                    or self.sr.te\n                )\n                if pex.code >= (300 if post else 400):\n                    self.keepalive = False\n\n                em = str(ex)\n                msg = em if pex is ex else min_ex()\n\n                if pex.code != 404 or self.do_log:\n                    self.log(\n                        \"http%d: %s\\033[0m, %r\" % (pex.code, msg, \"/\" + self.vpath),\n                        6 if em.startswith(\"client d/c \") else 3,\n                    )\n\n                if self.hint and self.hint.startswith(\"<xml> \"):\n                    if self.args.log_badxml:\n                        t = \"invalid XML received from client: %r\"\n                        self.log(t % (self.hint[6:],), 6)\n                    else:\n                        t = \"received invalid XML from client; enable --log-badxml to see the whole XML in the log\"\n                        self.log(t, 6)\n                    self.hint = \"\"\n\n                msg = \"%s\\r\\nURL: %s\\r\\n\" % (em, self.vpath)\n                if self.hint:\n                    msg += \"hint: %s\\r\\n\" % (self.hint,)\n\n                if \"database is locked\" in em:\n                    self.conn.hsrv.broker.say(\"log_stacks\")\n                    msg += \"hint: important info in the server log\\r\\n\"\n\n                zb = b\"<pre>\" + html_escape(msg).encode(\"utf-8\", \"replace\")\n                h = {\"WWW-Authenticate\": 'Basic realm=\"a\"'} if pex.code == 401 else {}\n                self.reply(zb, status=pex.code, headers=h, volsan=True)\n                if pex.log:\n                    self.log(\"additional error context:\\n\" + pex.log, 6)\n\n                return self.keepalive\n            except Pebkac:\n                return False\n\n        finally:\n            if self.dl_id:\n                self.conn.hsrv.dli.pop(self.dl_id, None)\n                self.conn.hsrv.dls.pop(self.dl_id, None)\n\n    def dip(self) -> str:\n        if self.args.plain_ip:\n            return self.ip.replace(\":\", \".\")\n        else:\n            return self.conn.iphash.s(self.ip)\n\n    def cbonk(self, g: Garda, v: str, reason: str, descr: str) -> bool:\n        cond = self.args.dont_ban\n        if (\n            cond == \"any\"\n            or (cond == \"auth\" and self.uname != \"*\")\n            or (cond == \"aa\" and self.avol)\n            or (cond == \"av\" and self.can_admin)\n            or (cond == \"rw\" and self.can_read and self.can_write)\n        ):\n            return False\n\n        self.conn.hsrv.nsus += 1\n        if not g.lim:\n            return False\n\n        bonk, ip = g.bonk(self.ip, v + self.gctx)\n        if not bonk:\n            return False\n\n        xban = self.vn.flags.get(\"xban\")\n        if xban:\n            hr = runhook(\n                self.log,\n                self.conn.hsrv.broker,\n                None,\n                \"xban\",\n                xban,\n                self.vn.canonical(self.rem),\n                self.vpath,\n                self.host,\n                self.uname,\n                \"\",\n                time.time(),\n                0,\n                self.ip,\n                time.time(),\n                [reason, reason],\n            )\n            if hr.get(\"rv\") == 0:\n                return False\n\n        self.log(\"client banned: %s\" % (descr,), 1)\n        self.conn.hsrv.bans[ip] = bonk\n        self.conn.hsrv.nban += 1\n        return True\n\n    def is_banned(self) -> bool:\n        if not self.conn.bans:\n            return False\n\n        bans = self.conn.bans\n        ip = ipnorm(self.ip)\n        if ip not in bans:\n            return False\n\n        rt = bans[ip] - time.time()\n        if rt < 0:\n            del bans[ip]\n            self.log(\"client unbanned\", 3)\n            return False\n\n        self.log(\"banned for {:.0f} sec\".format(rt), 6)\n        self.terse_reply(self.args.banmsg_b, 403)\n        return True\n\n    def permit_caching(self) -> None:\n        cache = self.uparam.get(\"cache\")\n        if cache is None:\n            self.out_headers[\"Cache-Control\"] = self.vn.flags[\"cachectl\"]\n            return\n\n        n = 69 if not cache else 604869 if cache == \"i\" else int(cache)\n        self.out_headers[\"Cache-Control\"] = \"max-age=\" + str(n)\n\n    def k304(self) -> bool:\n        k304 = self.cookies.get(\"k304\")\n        return k304 == \"y\" or (self.args.k304 == 2 and k304 != \"n\")\n\n    def no304(self) -> bool:\n        no304 = self.cookies.get(\"no304\")\n        return no304 == \"y\" or (self.args.no304 == 2 and no304 != \"n\")\n\n    def _build_html_head(self, kv: dict[str, Any]) -> None:\n        html = str(self.vn.flags[\"html_head_d\"])\n        is_jinja = html[:2] in \"%@%\"\n        if is_jinja:\n            html = html.replace(\"%\", \"\", 1)\n\n        if html.startswith(\"@\"):\n            html = read_utf8(self.log, html[1:], True)\n\n        if html.startswith(\"%\"):\n            html = html[1:]\n            is_jinja = True\n\n        if is_jinja:\n            with self.conn.hsrv.mutex:\n                if html not in self.conn.hsrv.j2:\n                    j2env = jinja2.Environment()\n                    tpl = j2env.from_string(html)\n                    self.conn.hsrv.j2[html] = tpl\n                html = self.conn.hsrv.j2[html].render(**kv)\n\n        self.html_head += html + \"\\n\"\n\n    def send_headers(\n        self,\n        oh_k: str,\n        length: Optional[int],\n        status: int = 200,\n        mime: Optional[str] = None,\n        headers: Optional[dict[str, str]] = None,\n    ) -> None:\n        response = [\"%s %s %s\" % (self.http_ver, status, HTTPCODE[status])]\n\n        # headers{} overrides anything set previously\n        if headers:\n            self.out_headers.update(headers)\n\n        if status == 304:\n            self.out_headers.pop(\"Content-Length\", None)\n            self.out_headers.pop(\"Content-Type\", None)\n            self.out_headerlist[:] = []\n            if self.k304():\n                self.keepalive = False\n        else:\n            if length is not None:\n                response.append(\"Content-Length: \" + unicode(length))\n\n            if mime:\n                self.out_headers[\"Content-Type\"] = mime\n            elif \"Content-Type\" not in self.out_headers:\n                self.out_headers[\"Content-Type\"] = \"text/html; charset=utf-8\"\n\n        # close if unknown length, otherwise take client's preference\n        response.append(H_CONN_KEEPALIVE if self.keepalive else H_CONN_CLOSE)\n        response.append(\"Date: \" + formatdate())\n\n        for k, zs in list(self.out_headers.items()) + self.out_headerlist:\n            response.append(\"%s: %s\" % (k, zs))\n\n        ptn_cc = RE_CC\n        for zs in response:\n            m = ptn_cc.search(zs)\n            if m:\n                t = \"malicious user; Cc in out-hdr; req(%r) hdr(%r) => %r\"\n                self.log(t % (self.req, zs, zs[m.span()[0] :]), 1)\n                self.cbonk(self.conn.hsrv.gmal, zs, \"cc_hdr\", \"Cc in out-hdr\")\n                raise Pebkac(999)\n\n        response.append(self.vn.flags[oh_k])\n\n        if self.args.ohead and self.do_log:\n            zs = response.pop()[:-4]\n            response.extend(zs.split(\"\\r\\n\"))\n            keys = self.args.ohead\n            if \"*\" in keys:\n                lines = response[1:]\n            else:\n                lines = []\n                for zs in response[1:]:\n                    if zs.split(\":\")[0].lower() in keys:\n                        lines.append(zs)\n            for zs in lines:\n                hk, hv = zs.split(\": \")\n                self.log(\"[O] {}: \\033[33m[{}]\".format(hk, hv), 5)\n            response.append(\"\\r\\n\")\n\n        try:\n            self.s.sendall(\"\\r\\n\".join(response).encode(\"utf-8\"))\n        except:\n            raise Pebkac(400, \"client d/c while replying headers\")\n\n    def reply(\n        self,\n        body: bytes,\n        status: int = 200,\n        mime: Optional[str] = None,\n        headers: Optional[dict[str, str]] = None,\n        volsan: bool = False,\n    ) -> bytes:\n        if (\n            status > 400\n            and status in (403, 404, 422)\n            and (\n                status != 422\n                or (\n                    not body.startswith(b\"<pre>partial upload exists\")\n                    and not body.startswith(b\"<pre>source file busy\")\n                )\n            )\n            and (status != 404 or (self.can_get and not self.can_read))\n        ):\n            if status == 404:\n                g = self.conn.hsrv.g404\n            elif status == 403:\n                g = self.conn.hsrv.g403\n            else:\n                g = self.conn.hsrv.g422\n\n            gurl = self.conn.hsrv.gurl\n            if (\n                gurl.lim\n                and (not g.lim or gurl.lim < g.lim)\n                and self.args.sus_urls.search(self.vpath)\n            ):\n                g = self.conn.hsrv.gurl\n\n            if g.lim and (\n                g == self.conn.hsrv.g422\n                or not self.args.nonsus_urls\n                or not self.args.nonsus_urls.search(self.vpath)\n            ):\n                self.cbonk(g, self.vpath, str(status), \"%ss\" % (status,))\n\n        if volsan:\n            vols = list(self.asrv.vfs.all_vols.values())\n            body = vol_san(vols, body)\n            try:\n                zs = absreal(__file__).rsplit(os.path.sep, 2)[0]\n                body = body.replace(zs.encode(\"utf-8\"), b\"PP\")\n            except:\n                pass\n\n        self.send_headers(\"oh_g\", len(body), status, mime, headers)\n\n        try:\n            if self.mode != \"HEAD\":\n                self.s.sendall(body)\n        except:\n            raise Pebkac(400, \"client d/c while replying body\")\n\n        return body\n\n    def loud_reply(self, body: str, *args: Any, **kwargs: Any) -> None:\n        if not kwargs.get(\"mime\"):\n            kwargs[\"mime\"] = \"text/plain; charset=utf-8\"\n\n        self.log(body.rstrip())\n        self.reply(body.encode(\"utf-8\") + b\"\\r\\n\", *list(args), **kwargs)\n\n    def terse_reply(self, body: bytes, status: int = 200) -> None:\n        self.keepalive = False\n\n        lines = [\n            \"%s %s %s\" % (self.http_ver or \"HTTP/1.1\", status, HTTPCODE[status]),\n            H_CONN_CLOSE,\n        ]\n\n        if body:\n            lines.append(\n                \"Content-Type: text/html; charset=utf-8\\r\\nContent-Length: \"\n                + unicode(len(body))\n            )\n\n        lines.append(\"\\r\\n\")\n        self.s.sendall(\"\\r\\n\".join(lines).encode(\"utf-8\") + body)\n\n    def urlq(self, add: dict[str, str], rm: list[str]) -> str:\n        \"\"\"\n        generates url query based on uparam (b, pw, all others)\n        removing anything in rm, adding pairs in add\n\n        also list faster than set until ~20 items\n        \"\"\"\n\n        if self.is_rclone:\n            return \"\"\n\n        kv = {k: zs for k, zs in self.uparam.items() if k not in rm}\n        # no reason to consider args.pw_urlp\n        if \"pw\" in kv:\n            pw = self.cookies.get(\"cppws\") or self.cookies.get(\"cppwd\")\n            if kv[\"pw\"] == pw:\n                del kv[\"pw\"]\n\n        kv.update(add)\n        if not kv:\n            return \"\"\n\n        r = [\"%s=%s\" % (quotep(k), quotep(zs)) if zs else k for k, zs in kv.items()]\n        return \"?\" + \"&amp;\".join(r)\n\n    def ourlq(self) -> str:\n        # no reason to consider args.pw_urlp\n        skip = (\"pw\", \"h\", \"k\")\n        ret = []\n        for k, v in self.ouparam.items():\n            if k in skip:\n                continue\n\n            t = \"%s=%s\" % (quotep(k), quotep(v))\n            ret.append(t.replace(\" \", \"+\").rstrip(\"=\"))\n\n        if not ret:\n            return \"\"\n\n        return \"?\" + \"&\".join(ret)\n\n    def redirect(\n        self,\n        vpath: str,\n        suf: str = \"\",\n        msg: str = \"aight\",\n        flavor: str = \"go to\",\n        click: bool = True,\n        status: int = 200,\n        use302: bool = False,\n    ) -> bool:\n        vp = self.args.SRS + vpath\n        html = self.j2s(\n            \"msg\",\n            h2='<a href=\"{}\">{} {}</a>'.format(\n                quotep(vp) + suf, flavor, html_escape(vp, crlf=True) + suf\n            ),\n            pre=msg,\n            click=click,\n        ).encode(\"utf-8\", \"replace\")\n\n        if use302:\n            self.reply(html, status=302, headers={\"Location\": vp})\n        else:\n            self.reply(html, status=status)\n\n        return True\n\n    def _cors(self) -> bool:\n        ih = self.headers\n        origin = ih.get(\"origin\")\n        if not origin:\n            sfsite = ih.get(\"sec-fetch-site\")\n            if sfsite and sfsite.lower().startswith(\"cross\"):\n                origin = \":|\"  # sandboxed iframe\n            else:\n                return True\n\n        host = self.host.lower()\n        if host.startswith(\"[\"):\n            if \"]:\" in host:\n                host = host.split(\"]:\")[0] + \"]\"\n        else:\n            host = host.split(\":\")[0]\n\n        oh = self.out_headers\n        origin = origin.lower()\n        proto = \"https\" if self.is_https else \"http\"\n        good_origins = self.args.acao + [\"%s://%s\" % (proto, host)]\n\n        if (\n            self.args.pw_hdr in ih\n            or re.sub(r\"(:[0-9]{1,5})?/?$\", \"\", origin) in good_origins\n        ):\n            good_origin = True\n            bad_hdrs = (\"\",)\n        else:\n            good_origin = False\n            bad_hdrs = (\"\", self.args.pw_hdr)\n\n        # '*' blocks auth through cookies / WWW-Authenticate;\n        # exact-match for Origin is necessary to unlock those,\n        # but the ?pw= param and PW: header are always allowed\n        acah = ih.get(\"access-control-request-headers\", \"\")\n        acao = (origin if good_origin else None) or (\n            \"*\" if \"*\" in good_origins else None\n        )\n        if self.args.allow_csrf:\n            acao = origin or acao or \"*\"  # explicitly permit impersonation\n            acam = \", \".join(self.conn.hsrv.mallow)  # and all methods + headers\n            oh[\"Access-Control-Allow-Credentials\"] = \"true\"\n            good_origin = True\n        else:\n            acam = \", \".join(self.args.acam)\n            # wash client-requested headers and roll with that\n            if \"range\" not in acah.lower():\n                acah += \",Range\"  # firefox\n            req_h = acah.split(\",\")\n            req_h = [x.strip() for x in req_h]\n            req_h = [x for x in req_h if x.lower() not in bad_hdrs]\n            acah = \", \".join(req_h)\n\n        if not acao:\n            return False\n\n        oh[\"Access-Control-Allow-Origin\"] = acao\n        oh[\"Access-Control-Allow-Methods\"] = acam.upper()\n        if acah:\n            oh[\"Access-Control-Allow-Headers\"] = acah\n\n        return good_origin\n\n    def handle_get(self) -> bool:\n        if self.do_log:\n            logmsg = \"%-4s %s @%s\" % (self.mode, self.req, self.uname)\n\n            if \"range\" in self.headers:\n                try:\n                    rval = self.headers[\"range\"].split(\"=\", 1)[1]\n                except:\n                    rval = self.headers[\"range\"]\n\n                logmsg += \" [\\033[36m\" + rval + \"\\033[0m]\"\n\n            self.log(logmsg)\n            if \"%\" in self.req:\n                self.log(\" `-- %r\" % (self.vpath,))\n\n        # \"embedded\" resources\n        if self.vpath.startswith(\".cpr\"):\n            if self.vpath.startswith(\".cpr/ico/\"):\n                return self.tx_ico(self.vpath.split(\"/\")[-1], exact=True)\n\n            if self.vpath.startswith(\".cpr/ssdp\"):\n                if self.conn.hsrv.ssdp:\n                    return self.conn.hsrv.ssdp.reply(self)\n                else:\n                    self.reply(b\"ssdp is disabled in server config\", 404)\n                    return False\n\n            if self.vpath == \".cpr/metrics\":\n                return self.conn.hsrv.metrics.tx(self)\n\n            if self.vpath.startswith(\".cpr/w/\"):\n                res_path = \"web/\" + self.vpath[7:]\n            else:\n                res_path = \"web/\" + self.vpath[5:]\n\n            if res_path in RES:\n                ap = self.E.mod_ + res_path\n                if bos.path.exists(ap) or bos.path.exists(ap + \".gz\"):\n                    return self.tx_file(\"oh_g\", ap)\n                else:\n                    return self.tx_res(res_path)\n\n            if res_path in RESM:\n                ap = self.E.mod_ + RESM[res_path]\n                if (\n                    \"txt\" not in self.uparam\n                    and \"mime\" not in self.uparam\n                    and not self.ouparam.get(\"dl\")\n                ):\n                    # return mimetype matching request extension\n                    self.ouparam[\"dl\"] = res_path.split(\"/\")[-1]\n                if bos.path.exists(ap) or bos.path.exists(ap + \".gz\"):\n                    return self.tx_file(\"oh_g\", ap)\n                else:\n                    return self.tx_res(res_path)\n\n            self.tx_404()\n            return False\n\n        if \"cf_challenge\" in self.uparam:\n            self.reply(self.j2s(\"cf\").encode(\"utf-8\", \"replace\"))\n            return True\n\n        if not self.can_read and not self.can_write and not self.can_get:\n            t = \"@%s has no access to %r\"\n\n            if self.vn.realpath and \"on403\" in self.vn.flags:\n                t += \" (on403)\"\n                self.log(t % (self.uname, \"/\" + self.vpath))\n                ret = self.on40x(self.vn.flags[\"on403\"], self.vn, self.rem)\n                if ret == \"true\":\n                    return True\n                elif ret == \"false\":\n                    return False\n                elif ret == \"home\":\n                    self.uparam[\"h\"] = \"\"\n                elif ret == \"allow\":\n                    self.log(\"plugin override; access permitted\")\n                    self.can_read = self.can_write = self.can_move = True\n                    self.can_delete = self.can_get = self.can_upget = True\n                    self.can_admin = True\n                else:\n                    return self.tx_404(True)\n            else:\n                if (\n                    self.asrv.badcfg1\n                    and \"h\" not in self.ouparam\n                    and \"hc\" not in self.ouparam\n                ):\n                    zs1 = \"copyparty refused to start due to a failsafe: invalid server config; check server log\"\n                    zs2 = 'you may <a href=\"/?h\">access the controlpanel</a> but nothing will work until you shutdown the copyparty container and %s config-file (or provide the configuration as command-line arguments)'\n                    if self.asrv.is_lxc and len(self.asrv.cfg_files_loaded) == 1:\n                        zs2 = zs2 % (\"add a\",)\n                    else:\n                        zs2 = zs2 % (\"fix the\",)\n\n                    html = self.j2s(\"msg\", h1=zs1, h2=zs2)\n                    self.reply(html.encode(\"utf-8\", \"replace\"), 500)\n                    return True\n\n                if \"ls\" in self.uparam:\n                    return self.tx_ls_vols()\n\n                if self.vpath:\n                    ptn = self.args.nonsus_urls\n                    if not ptn or not ptn.search(self.vpath):\n                        self.log(t % (self.uname, \"/\" + self.vpath))\n\n                    return self.tx_404(True)\n\n                self.uparam[\"h\"] = \"\"\n\n        if \"smsg\" in self.uparam:\n            return self.handle_smsg()\n\n        if \"tree\" in self.uparam:\n            return self.tx_tree()\n\n        if \"scan\" in self.uparam:\n            return self.scanvol()\n\n        if self.args.getmod:\n            if \"delete\" in self.uparam:\n                return self.handle_rm([])\n\n            if \"move\" in self.uparam:\n                return self.handle_mv()\n\n            if \"copy\" in self.uparam:\n                return self.handle_cp()\n\n        if not self.vpath and self.ouparam:\n            if \"reload\" in self.uparam:\n                return self.handle_reload()\n\n            if \"stack\" in self.uparam:\n                return self.tx_stack()\n\n            if \"setck\" in self.uparam:\n                return self.setck()\n\n            if \"reset\" in self.uparam:\n                return self.set_cfg_reset()\n\n            if \"hc\" in self.uparam:\n                return self.tx_svcs()\n\n            if \"shares\" in self.uparam:\n                return self.tx_shares()\n\n            if \"dls\" in self.uparam:\n                return self.tx_dls()\n\n            if \"ru\" in self.uparam:\n                return self.tx_rups()\n\n            if \"idp\" in self.uparam:\n                return self.tx_idp()\n\n        if \"h\" in self.uparam:\n            return self.tx_mounts()\n\n        if \"ups\" in self.uparam:\n            # vpath is used for share translation\n            return self.tx_ups()\n\n        if \"rss\" in self.uparam:\n            return self.tx_rss()\n\n        return self.tx_browser()\n\n    def tx_rss(self) -> bool:\n        if self.do_log:\n            self.log(\"RSS  %s @%s\" % (self.req, self.uname))\n\n        if not self.can_read:\n            return self.tx_404(True)\n\n        vn = self.vn\n        if not vn.flags.get(\"rss\"):\n            raise Pebkac(405, \"RSS is disabled in server config\")\n\n        rem = self.rem\n        idx = self.conn.get_u2idx()\n        if not idx or not hasattr(idx, \"p_end\"):\n            if not HAVE_SQLITE3:\n                raise Pebkac(500, \"sqlite3 not found on server; rss is disabled\")\n            raise Pebkac(500, \"server busy, cannot generate rss; please retry in a bit\")\n\n        uv = [rem]\n        if \"recursive\" in self.uparam:\n            uq = \"up.rd like ?||'%'\"\n        else:\n            uq = \"up.rd == ?\"\n\n        zs = str(self.uparam.get(\"fext\", self.args.rss_fext))\n        if zs in (\"True\", \"False\"):\n            zs = \"\"\n        if zs:\n            zsl = []\n            for ext in zs.split(\",\"):\n                zsl.append(\"+up.fn like '%.'||?\")\n                uv.append(ext)\n            uq += \" and ( %s )\" % (\" or \".join(zsl),)\n\n        zs1 = self.uparam.get(\"sort\") or self.args.rss_sort\n        zs2 = zs1.lower()\n        zs = RSS_SORT.get(zs2)\n        if not zs:\n            raise Pebkac(400, \"invalid sort key; must be m/u/n/s\")\n\n        uq += \" order by up.\" + zs\n        if zs1 == zs2:\n            uq += \" desc\"\n\n        nmax = int(self.uparam.get(\"nf\") or self.args.rss_nf)\n\n        hits = idx.run_query(self.uname, [self.vn], uq, uv, False, False, nmax)[0]\n\n        q_pw = a_pw = \"\"\n        pwk = self.args.pw_urlp\n        if pwk in self.ouparam and \"nopw\" not in self.ouparam:\n            zs = self.ouparam[pwk]\n            q_pw = \"?%s=%s\" % (pwk, quotep(zs))\n            a_pw = \"&%s=%s\" % (pwk, quotep(zs))\n            for i in hits:\n                i[\"rp\"] += a_pw if \"?\" in i[\"rp\"] else q_pw\n\n        title = self.uparam.get(\"title\") or self.vpath.split(\"/\")[-1]\n        etitle = html_escape(title, True, True)\n\n        baseurl = \"%s://%s/\" % (\n            \"https\" if self.is_https else \"http\",\n            self.host,\n        )\n        feed = baseurl + self.req[1:]\n        if pwk in self.ouparam and self.ouparam.get(\"nopw\") == \"a\":\n            feed = re.sub(r\"&%s=[^&]*\" % (pwk,), \"\", feed)\n        if self.is_vproxied:\n            baseurl += self.args.RS\n        efeed = html_escape(feed, True, True)\n        edirlink = efeed.split(\"?\")[0] + q_pw\n\n        ret = [\n            \"\"\"\\\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\" xmlns:itunes=\"http://www.itunes.com/dtds/podcast-1.0.dtd\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n\\t<channel>\n\\t\\t<atom:link href=\"%s\" rel=\"self\" type=\"application/rss+xml\" />\n\\t\\t<title>%s</title>\n\\t\\t<description></description>\n\\t\\t<link>%s</link>\n\\t\\t<generator>copyparty-2</generator>\n\"\"\"\n            % (efeed, etitle, edirlink)\n        ]\n\n        q = \"select fn from cv where rd=? and dn=?\"\n        crd, cdn = rem.rsplit(\"/\", 1) if \"/\" in rem else (\"\", rem)\n        try:\n            cfn = idx.cur[self.vn.realpath].execute(q, (crd, cdn)).fetchone()[0]\n            bos.stat(os.path.join(vn.canonical(rem), cfn))\n            cv_url = \"%s%s?th=jf%s\" % (baseurl, vjoin(self.vpath, cfn), a_pw)\n            cv_url = html_escape(cv_url, True, True)\n            zs = \"\"\"\\\n\\t\\t<image>\n\\t\\t\\t<url>%s</url>\n\\t\\t\\t<title>%s</title>\n\\t\\t\\t<link>%s</link>\n\\t\\t</image>\n\"\"\"\n            ret.append(zs % (cv_url, etitle, edirlink))\n        except:\n            pass\n\n        ap = \"\"\n        use_magic = \"rmagic\" in self.vn.flags\n\n        tpl_t = self.uparam.get(\"fmt_t\") or self.vn.flags[\"rss_fmt_t\"]\n        tpl_d = self.uparam.get(\"fmt_d\") or self.vn.flags[\"rss_fmt_d\"]\n        kw_t = [[x, x[1:-1]] for x in RE_RSS_KW.findall(tpl_t)]\n        kw_d = [[x, x[1:-1]] for x in RE_RSS_KW.findall(tpl_d)]\n\n        for i in hits:\n            if use_magic:\n                ap = os.path.join(self.vn.realpath, i[\"rp\"])\n\n            tags = i[\"tags\"]\n            iurl = html_escape(\"%s%s\" % (baseurl, i[\"rp\"]), True, True)\n            fname = tags[\"fname\"] = unquotep(i[\"rp\"].split(\"?\")[0].split(\"/\")[-1])\n            title = tpl_t\n            desc = tpl_d\n            for zs1, zs2 in kw_t:\n                title = title.replace(zs1, str(tags.get(zs2, \"\")))\n            for zs1, zs2 in kw_d:\n                desc = desc.replace(zs1, str(tags.get(zs2, \"\")))\n            title = html_escape(title.strip(), True, True)\n            if desc.strip(\" -,\"):\n                desc = html_escape(desc.strip(), True, True)\n            else:\n                desc = title\n\n            mime = html_escape(guess_mime(fname, ap))\n            lmod = formatdate(max(0, i[\"ts\"]))\n            zsa = (iurl, iurl, title, desc, lmod, iurl, mime, i[\"sz\"])\n            zs = (\n                \"\"\"\\\n\\t\\t<item>\n\\t\\t\\t<guid>%s</guid>\n\\t\\t\\t<link>%s</link>\n\\t\\t\\t<title>%s</title>\n\\t\\t\\t<description>%s</description>\n\\t\\t\\t<pubDate>%s</pubDate>\n\\t\\t\\t<enclosure url=\"%s\" type=\"%s\" length=\"%d\"/>\n\"\"\"\n                % zsa\n            )\n            dur = i[\"tags\"].get(\".dur\")\n            if dur:\n                zs += \"\\t\\t\\t<itunes:duration>%d</itunes:duration>\\n\" % (dur,)\n            ret.append(zs + \"\\t\\t</item>\\n\")\n\n        ret.append(\"\\t</channel>\\n</rss>\\n\")\n        bret = \"\".join(ret).encode(\"utf-8\", \"replace\")\n        self.reply(bret, 200, \"text/xml; charset=utf-8\")\n        self.log(\"rss: %d hits, %d bytes\" % (len(hits), len(bret)))\n        return True\n\n    def tx_zls(self, abspath) -> bool:\n        if self.do_log:\n            self.log(\"zls %s @%s\" % (self.req, self.uname))\n        if self.args.no_zls:\n            raise Pebkac(405, \"zip browsing is disabled in server config\")\n\n        import zipfile\n\n        try:\n            with zipfile.ZipFile(abspath, \"r\") as zf:\n                filelist = [{\"fn\": f.filename} for f in zf.infolist()]\n                ret = json.dumps(filelist).encode(\"utf-8\", \"replace\")\n                self.reply(ret, mime=\"application/json\")\n                return True\n        except (zipfile.BadZipfile, RuntimeError):\n            raise Pebkac(404, \"requested file is not a valid zip file\")\n\n    def tx_zget(self, abspath) -> bool:\n        maxsz = 1024 * 1024 * 64\n\n        inner_path = self.uparam.get(\"zget\")\n        if not inner_path:\n            raise Pebkac(405, \"inner path is required\")\n        if self.do_log:\n            self.log(\n                \"zget %s \\033[35m%s\\033[0m @%s\" % (self.req, inner_path, self.uname)\n            )\n        if self.args.no_zls:\n            raise Pebkac(405, \"zip browsing is disabled in server config\")\n\n        import zipfile\n\n        try:\n            with zipfile.ZipFile(abspath, \"r\") as zf:\n                zi = zf.getinfo(inner_path)\n                if zi.file_size >= maxsz:\n                    raise Pebkac(404, \"zip bomb defused\")\n                with zf.open(zi, \"r\") as fi:\n                    mime = guess_mime(inner_path)\n                    if mime not in SAFE_MIMES and \"nohtml\" in self.vn.flags:\n                        mime = safe_mime(mime)\n                    self.send_headers(\"oh_f\", length=zi.file_size, mime=mime)\n\n                    sendfile_py(\n                        self.log,\n                        0,\n                        zi.file_size,\n                        fi,\n                        self.s,\n                        self.args.s_wr_sz,\n                        self.args.s_wr_slp,\n                        not self.args.no_poll,\n                        {},\n                        \"\",\n                    )\n        except KeyError:\n            raise Pebkac(404, \"no such file in archive\")\n        except (zipfile.BadZipfile, RuntimeError):\n            raise Pebkac(404, \"requested file is not a valid zip file\")\n        return True\n\n    def handle_propfind(self) -> bool:\n        if self.do_log:\n            self.log(\"PFIND %s @%s\" % (self.req, self.uname))\n            if \"%\" in self.req:\n                self.log(\"  `-- %r\" % (self.vpath,))\n\n        if self.args.no_dav:\n            raise Pebkac(405, \"WebDAV is disabled in server config\")\n\n        vn = self.vn\n        rem = self.rem\n        tap = vn.canonical(rem)\n\n        if \"davauth\" in vn.flags and self.uname == \"*\":\n            raise Pebkac(401, \"authenticate\")\n\n        from .dxml import parse_xml\n\n        # enc = \"windows-31j\"\n        # enc = \"shift_jis\"\n        enc = \"utf-8\"\n        uenc = enc.upper()\n        props = DAV_ALLPROPS\n\n        clen = int(self.headers.get(\"content-length\", 0))\n        if clen:\n            buf = b\"\"\n            for rbuf in self.get_body_reader()[0]:\n                buf += rbuf\n                if not rbuf or len(buf) >= 32768:\n                    break\n\n            sbuf = buf.decode(enc, \"replace\")\n            self.hint = \"<xml> \" + sbuf\n            xroot = parse_xml(sbuf)\n            xtag = next((x for x in xroot if x.tag.split(\"}\")[-1] == \"prop\"), None)\n            if xtag is not None:\n                props = set([y.tag.split(\"}\")[-1] for y in xtag])\n            # assume <allprop/> otherwise; nobody ever gonna <propname/>\n            self.hint = \"\"\n\n        zi = int(time.time())\n        vst = os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, zi, zi, zi))\n\n        try:\n            st = bos.stat(tap)\n        except OSError as ex:\n            if ex.errno not in (errno.ENOENT, errno.ENOTDIR):\n                raise\n            if tap:\n                raise Pebkac(404)\n            st = vst\n\n        topdir = {\"vp\": \"\", \"st\": st}\n        fgen: Iterable[dict[str, Any]] = []\n\n        depth = self.headers.get(\"depth\", \"infinity\").lower()\n        if depth == \"infinity\":\n            # allow depth:0 from unmapped root, but require read-axs otherwise\n            if not self.can_read and (self.vpath or self.asrv.vfs.realpath):\n                t = \"depth:infinity requires read-access in %r\"\n                t = t % (\"/\" + self.vpath,)\n                self.log(t, 3)\n                raise Pebkac(401, t)\n\n            if not stat.S_ISDIR(topdir[\"st\"].st_mode):\n                t = \"depth:infinity can only be used on folders; %r is 0o%o\"\n                t = t % (\"/\" + self.vpath, topdir[\"st\"])\n                self.log(t, 3)\n                raise Pebkac(400, t)\n\n            if not self.args.dav_inf:\n                self.log(\"client wants --dav-inf\", 3)\n                zb = b'<?xml version=\"1.0\" encoding=\"utf-8\"?>\\n<D:error xmlns:D=\"DAV:\"><D:propfind-finite-depth/></D:error>'\n                self.reply(zb, 403, \"application/xml; charset=utf-8\")\n                return True\n\n            # this will return symlink-target timestamps\n            # because lstat=true would not recurse into subfolders\n            # and this is a rare case where we actually want that\n            fgen = vn.zipgen(\n                rem,\n                rem,\n                set(),\n                self.uname,\n                True,\n                1,\n                not self.args.no_scandir,\n                wrap=False,\n            )\n\n        elif depth == \"0\" or not stat.S_ISDIR(st.st_mode):\n            if depth == \"0\" and not self.vpath and not vn.realpath:\n                # rootless server; give dummy listing\n                self.can_read = True\n            # propfind on a file; return as topdir\n            if not self.can_read and not self.can_get:\n                self.log(\"inaccessible: %r\" % (\"/\" + self.vpath,))\n                raise Pebkac(401, \"authenticate\")\n\n        elif depth == \"1\":\n            _, vfs_ls, vfs_virt = vn.ls(\n                rem,\n                self.uname,\n                not self.args.no_scandir,\n                [[True, False]],\n                lstat=\"davrt\" not in vn.flags,\n                throw=True,\n            )\n            if not self.can_read:\n                vfs_ls = []\n            if not self.can_dot:\n                vfs_ls = exclude_dotfiles_ls(vfs_ls)\n            fgen = [{\"vp\": vp, \"st\": st} for vp, st in vfs_ls]\n\n            if vfs_virt:\n                zsl = list(vfs_virt)\n                if not self.can_dot:\n                    zsl = exclude_dotfiles(zsl)\n                fgen += [{\"vp\": v, \"st\": vst} for v in zsl]\n\n        else:\n            t = \"invalid depth value '{}' (must be either '0' or '1'{})\"\n            t2 = \" or 'infinity'\" if self.args.dav_inf else \"\"\n            raise Pebkac(412, t.format(depth, t2))\n\n        if not self.can_read and not self.can_write and not fgen:\n            self.log(\"inaccessible: %r\" % (\"/\" + self.vpath,))\n            raise Pebkac(401, \"authenticate\")\n\n        zi = (\n            vn.flags[\"du_iwho\"]\n            if vn.realpath\n            and \"quota-available-bytes\" in props\n            and \"quotaused\" not in props  # macos finder; ingnore it\n            else 0\n        )\n        if zi and (\n            zi == 9\n            or (zi == 7 and self.uname != \"*\")\n            or (zi == 5 and self.can_write)\n            or (zi == 4 and self.can_write and self.can_read)\n            or (zi == 3 and self.can_admin)\n        ):\n            bfree, btot, _ = get_df(vn.realpath, False)\n            if btot:\n                if \"vmaxb\" in vn.flags:\n                    assert vn.lim  # type: ignore  # !rm\n                    btot = vn.lim.vbmax\n                    if bfree == vn.lim.c_vb_r:\n                        bfree = min(bfree, max(0, vn.lim.vbmax - vn.lim.c_vb_v))\n                    else:\n                        try:\n                            zi, _ = self.conn.hsrv.broker.ask(\n                                \"up2k.get_volsizes\", [vn.realpath]\n                            ).get()[0]\n                            vn.lim.c_vb_v = zi\n                            vn.lim.c_vb_r = bfree\n                            bfree = min(bfree, max(0, vn.lim.vbmax - zi))\n                        except:\n                            pass\n                df = {\n                    \"quota-available-bytes\": str(bfree),\n                    \"quota-used-bytes\": str(btot - bfree),\n                }\n            else:\n                df = {}\n        else:\n            df = {}\n\n        fgen = itertools.chain([topdir], fgen)\n        vtop = vjoin(self.args.R, vjoin(vn.vpath, rem))\n\n        chunksz = 0x7FF8  # preferred by nginx or cf (dunno which)\n\n        self.send_headers(\n            \"oh_f\",\n            None,\n            207,\n            \"text/xml; charset=\" + enc,\n            {\"Transfer-Encoding\": \"chunked\"},\n        )\n\n        ap = \"\"\n        use_magic = \"rmagic\" in vn.flags\n\n        ret = '<?xml version=\"1.0\" encoding=\"{}\"?>\\n<D:multistatus xmlns:D=\"DAV:\">'\n        ret = ret.format(uenc)\n        for x in fgen:\n            rp = vjoin(vtop, x[\"vp\"])\n            st: os.stat_result = x[\"st\"]\n            mtime = max(0, st.st_mtime)\n            if stat.S_ISLNK(st.st_mode):\n                try:\n                    st = bos.stat(os.path.join(tap, x[\"vp\"]))\n                except:\n                    continue\n\n            isdir = stat.S_ISDIR(st.st_mode)\n\n            ret += \"<D:response><D:href>/%s%s</D:href><D:propstat><D:prop>\" % (\n                quotep(rp),\n                \"/\" if isdir and rp else \"\",\n            )\n\n            pvs: dict[str, str] = {\n                \"displayname\": html_escape(rp.split(\"/\")[-1]),\n                \"getlastmodified\": formatdate(mtime),\n                \"resourcetype\": '<D:collection xmlns:D=\"DAV:\"/>' if isdir else \"\",\n                \"supportedlock\": '<D:lockentry xmlns:D=\"DAV:\"><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry>',\n            }\n            if not isdir:\n                if use_magic:\n                    ap = os.path.join(tap, x[\"vp\"])\n                pvs[\"getcontenttype\"] = html_escape(guess_mime(rp, ap))\n                pvs[\"getcontentlength\"] = str(st.st_size)\n            elif df:\n                pvs.update(df)\n\n            for k, v in pvs.items():\n                if k not in props:\n                    continue\n                elif v:\n                    ret += \"<D:%s>%s</D:%s>\" % (k, v, k)\n                else:\n                    ret += \"<D:%s/>\" % (k,)\n\n            ret += \"</D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat>\"\n\n            missing = [\"<D:%s/>\" % (x,) for x in props if x not in pvs]\n            if missing and clen:\n                t = \"<D:propstat><D:prop>{}</D:prop><D:status>HTTP/1.1 404 Not Found</D:status></D:propstat>\"\n                ret += t.format(\"\".join(missing))\n\n            ret += \"</D:response>\"\n            while len(ret) >= chunksz:\n                ret = self.send_chunk(ret, enc, chunksz)\n\n        ret += \"</D:multistatus>\"\n        while ret:\n            ret = self.send_chunk(ret, enc, chunksz)\n\n        self.send_chunk(\"\", enc, chunksz)\n        # self.reply(ret.encode(enc, \"replace\"),207, \"text/xml; charset=\" + enc)\n        return True\n\n    def handle_proppatch(self) -> bool:\n        if self.do_log:\n            self.log(\"PPATCH %s @%s\" % (self.req, self.uname))\n            if \"%\" in self.req:\n                self.log(\"   `-- %r\" % (self.vpath,))\n\n        if self.args.no_dav:\n            raise Pebkac(405, \"WebDAV is disabled in server config\")\n\n        if not self.can_write:\n            self.log(\"%s tried to proppatch %r\" % (self.uname, \"/\" + self.vpath))\n            raise Pebkac(401, \"authenticate\")\n\n        from xml.etree import ElementTree as ET\n\n        from .dxml import mkenod, mktnod, parse_xml\n\n        buf = b\"\"\n        for rbuf in self.get_body_reader()[0]:\n            buf += rbuf\n            if not rbuf or len(buf) >= 128 * 1024:\n                break\n\n        if self._applesan():\n            return True\n\n        txt = buf.decode(\"ascii\", \"replace\").lower()\n        enc = self.get_xml_enc(txt)\n        uenc = enc.upper()\n\n        txt = buf.decode(enc, \"replace\")\n        self.hint = \"<xml> \" + txt\n        ET.register_namespace(\"D\", \"DAV:\")\n        xroot = mkenod(\"D:orz\")\n        xroot.insert(0, parse_xml(txt))\n        xprop = xroot.find(r\"./{DAV:}propertyupdate/{DAV:}set/{DAV:}prop\")\n        assert xprop  # !rm\n        for ze in xprop:\n            ze.clear()\n        self.hint = \"\"\n\n        txt = \"\"\"<multistatus xmlns=\"DAV:\"><response><propstat><status>HTTP/1.1 403 Forbidden</status></propstat></response></multistatus>\"\"\"\n        xroot = parse_xml(txt)\n\n        el = xroot.find(r\"./{DAV:}response\")\n        assert el  # !rm\n        e2 = mktnod(\"D:href\", quotep(self.args.SRS + self.vpath))\n        el.insert(0, e2)\n\n        el = xroot.find(r\"./{DAV:}response/{DAV:}propstat\")\n        assert el  # !rm\n        el.insert(0, xprop)\n\n        ret = '<?xml version=\"1.0\" encoding=\"{}\"?>\\n'.format(uenc)\n        ret += ET.tostring(xroot).decode(\"utf-8\")\n\n        self.reply(ret.encode(enc, \"replace\"), 207, \"text/xml; charset=\" + enc)\n        return True\n\n    def handle_lock(self) -> bool:\n        if self.do_log:\n            self.log(\"LOCK %s @%s\" % (self.req, self.uname))\n            if \"%\" in self.req:\n                self.log(\" `-- %r\" % (self.vpath,))\n\n        if self.args.no_dav:\n            raise Pebkac(405, \"WebDAV is disabled in server config\")\n\n        # win7+ deadlocks if we say no; just smile and nod\n        if not self.can_write and \"Microsoft-WebDAV\" not in self.ua:\n            self.log(\"%s tried to lock %r\" % (self.uname, \"/\" + self.vpath))\n            raise Pebkac(401, \"authenticate\")\n\n        from xml.etree import ElementTree as ET\n\n        from .dxml import mkenod, mktnod, parse_xml\n\n        abspath = self.vn.dcanonical(self.rem)\n\n        buf = b\"\"\n        for rbuf in self.get_body_reader()[0]:\n            buf += rbuf\n            if not rbuf or len(buf) >= 128 * 1024:\n                break\n\n        if self._applesan():\n            return True\n\n        txt = buf.decode(\"ascii\", \"replace\").lower()\n        enc = self.get_xml_enc(txt)\n        uenc = enc.upper()\n\n        txt = buf.decode(enc, \"replace\")\n        self.hint = \"<xml> \" + txt\n        ET.register_namespace(\"D\", \"DAV:\")\n        lk = parse_xml(txt)\n        assert lk.tag == \"{DAV:}lockinfo\"\n        self.hint = \"\"\n\n        token = str(uuid.uuid4())\n\n        if lk.find(r\"./{DAV:}depth\") is None:\n            depth = self.headers.get(\"depth\", \"infinity\")\n            lk.append(mktnod(\"D:depth\", depth))\n\n        lk.append(mktnod(\"D:timeout\", \"Second-3310\"))\n        lk.append(mkenod(\"D:locktoken\", mktnod(\"D:href\", token)))\n        lk.append(\n            mkenod(\"D:lockroot\", mktnod(\"D:href\", quotep(self.args.SRS + self.vpath)))\n        )\n\n        lk2 = mkenod(\"D:activelock\")\n        xroot = mkenod(\"D:prop\", mkenod(\"D:lockdiscovery\", lk2))\n        for a in lk:\n            lk2.append(a)\n\n        ret = '<?xml version=\"1.0\" encoding=\"{}\"?>\\n'.format(uenc)\n        ret += ET.tostring(xroot).decode(\"utf-8\")\n\n        rc = 200\n        if self.can_write and not bos.path.isfile(abspath):\n            with open(fsenc(abspath), \"wb\") as _:\n                rc = 201\n\n        self.out_headers[\"Lock-Token\"] = \"<{}>\".format(token)\n        self.reply(ret.encode(enc, \"replace\"), rc, \"text/xml; charset=\" + enc)\n        return True\n\n    def handle_unlock(self) -> bool:\n        if self.do_log:\n            self.log(\"UNLOCK %s @%s\" % (self.req, self.uname))\n            if \"%\" in self.req:\n                self.log(\"   `-- %r\" % (self.vpath,))\n\n        if self.args.no_dav:\n            raise Pebkac(405, \"WebDAV is disabled in server config\")\n\n        if not self.can_write and \"Microsoft-WebDAV\" not in self.ua:\n            self.log(\"%s tried to lock %r\" % (self.uname, \"/\" + self.vpath))\n            raise Pebkac(401, \"authenticate\")\n\n        self.send_headers(\"oh_f\", None, 204)\n        return True\n\n    def handle_mkcol(self) -> bool:\n        if self._applesan():\n            return True\n\n        if self.do_log:\n            self.log(\"MKCOL %s @%s\" % (self.req, self.uname))\n            if \"%\" in self.req:\n                self.log(\"  `-- %r\" % (self.vpath,))\n\n        if self.args.no_dav:\n            raise Pebkac(405, \"WebDAV is disabled in server config\")\n\n        if not self.can_write:\n            raise Pebkac(401, \"authenticate\")\n\n        try:\n            return self._mkdir(self.vpath, True)\n        except Pebkac as ex:\n            if ex.code >= 500:\n                raise\n\n            self.reply(b\"\", ex.code)\n            return True\n\n    def handle_cpmv(self) -> bool:\n        dst = self.headers[\"destination\"]\n\n        # dolphin (kioworker/6.10) \"webdav://127.0.0.1:3923/a/b.txt\"\n        dst = re.sub(\"^[a-zA-Z]+://[^/]+\", \"\", dst).lstrip()\n\n        if self.is_vproxied and dst.startswith(self.args.SRS):\n            dst = dst[len(self.args.RS) :]\n\n        if self.do_log:\n            self.log(\"%s %s  --//>  %s @%s\" % (self.mode, self.req, dst, self.uname))\n            if \"%\" in self.req:\n                self.log(\"  `-- %r\" % (self.vpath,))\n\n        if self.args.no_dav:\n            raise Pebkac(405, \"WebDAV is disabled in server config\")\n\n        dst = unquotep(dst)\n\n        # overwrite=True is default; rfc4918 9.8.4\n        zs = self.headers.get(\"overwrite\", \"\").lower()\n        overwrite = zs not in [\"f\", \"false\"]\n\n        try:\n            fun = self._cp if self.mode == \"COPY\" else self._mv\n            return fun(self.vpath, dst.lstrip(\"/\"), overwrite)\n        except Pebkac as ex:\n            if ex.code == 403:\n                ex.code = 401\n            raise\n\n    def _applesan(self) -> bool:\n        if self.args.dav_mac or \"Darwin/\" not in self.ua:\n            return False\n\n        vp = \"/\" + self.vpath\n        if re.search(APPLESAN_RE, vp):\n            zt = '<?xml version=\"1.0\" encoding=\"utf-8\"?>\\n<D:error xmlns:D=\"DAV:\"><D:lock-token-submitted><D:href>{}</D:href></D:lock-token-submitted></D:error>'\n            zb = zt.format(vp).encode(\"utf-8\", \"replace\")\n            self.reply(zb, 423, \"text/xml; charset=utf-8\")\n            return True\n\n        return False\n\n    def send_chunk(self, txt: str, enc: str, bmax: int) -> str:\n        orig_len = len(txt)\n        buf = txt[:bmax].encode(enc, \"replace\")[:bmax]\n        try:\n            _ = buf.decode(enc)\n        except UnicodeDecodeError as ude:\n            buf = buf[: ude.start]\n\n        txt = txt[len(buf.decode(enc)) :]\n        if txt and len(txt) == orig_len:\n            raise Pebkac(500, \"chunk slicing failed\")\n\n        buf = (\"%x\\r\\n\" % (len(buf),)).encode(enc) + buf\n        self.s.sendall(buf + b\"\\r\\n\")\n        return txt\n\n    def handle_options(self) -> bool:\n        if self.do_log:\n            self.log(\"OPTIONS %s @%s\" % (self.req, self.uname))\n            if \"%\" in self.req:\n                self.log(\"    `-- %r\" % (self.vpath,))\n\n        oh = self.out_headers\n        oh[\"Allow\"] = \", \".join(self.conn.hsrv.mallow)\n\n        if not self.args.no_dav:\n            # PROPPATCH, LOCK, UNLOCK, COPY: noop (spec-must)\n            oh[\"Dav\"] = \"1, 2\"\n            oh[\"Ms-Author-Via\"] = \"DAV\"\n\n        # winxp-webdav doesnt know what 204 is\n        self.send_headers(\"oh_f\", 0, 200)\n        return True\n\n    def handle_delete(self) -> bool:\n        self.log(\"DELETE %s @%s\" % (self.req, self.uname))\n        if \"%\" in self.req:\n            self.log(\"   `-- %r\" % (self.vpath,))\n        return self.handle_rm([])\n\n    def handle_put(self) -> bool:\n        self.log(\"PUT  %s @%s\" % (self.req, self.uname))\n        if \"%\" in self.req:\n            self.log(\" `-- %r\" % (self.vpath,))\n\n        if not self.can_write:\n            t = \"user %s does not have write-access under /%s\"\n            raise Pebkac(403 if self.pw else 401, t % (self.uname, self.vn.vpath))\n\n        if not self.args.no_dav and self._applesan():\n            return False\n\n        if self.headers.get(\"expect\", \"\").lower() == \"100-continue\":\n            try:\n                self.s.sendall(b\"HTTP/1.1 100 Continue\\r\\n\\r\\n\")\n            except:\n                raise Pebkac(400, \"client d/c before 100 continue\")\n\n        return self.handle_stash(True)\n\n    def handle_post(self) -> bool:\n        self.log(\"POST %s @%s\" % (self.req, self.uname))\n        if \"%\" in self.req:\n            self.log(\" `-- %r\" % (self.vpath,))\n\n        if self.headers.get(\"expect\", \"\").lower() == \"100-continue\":\n            try:\n                self.s.sendall(b\"HTTP/1.1 100 Continue\\r\\n\\r\\n\")\n            except:\n                raise Pebkac(400, \"client d/c before 100 continue\")\n\n        if \"raw\" in self.uparam:\n            return self.handle_stash(False)\n\n        ctype = self.headers.get(\"content-type\", \"\").lower()\n\n        if \"multipart/form-data\" in ctype:\n            return self.handle_post_multipart()\n\n        if (\n            \"application/json\" in ctype\n            or \"text/plain\" in ctype\n            or \"application/xml\" in ctype\n        ):\n            return self.handle_post_json()\n\n        if \"smsg\" in self.uparam:\n            return self.handle_smsg()\n\n        if \"move\" in self.uparam:\n            return self.handle_mv()\n\n        if \"copy\" in self.uparam:\n            return self.handle_cp()\n\n        if \"delete\" in self.uparam:\n            return self.handle_rm([])\n\n        if \"eshare\" in self.uparam:\n            return self.handle_eshare()\n\n        if \"fs_abrt\" in self.uparam:\n            return self.handle_fs_abrt()\n\n        if \"application/octet-stream\" in ctype:\n            return self.handle_post_binary()\n\n        if \"application/x-www-form-urlencoded\" in ctype:\n            opt = self.args.urlform\n            if \"stash\" in opt:\n                return self.handle_stash(False)\n\n            xm = []\n            xm_rsp = {}\n\n            if \"save\" in opt:\n                post_sz, _, _, _, _, path, _ = self.dump_to_file(False)\n                self.log(\"urlform: %d bytes, %r\" % (post_sz, path))\n            elif \"print\" in opt:\n                reader, _ = self.get_body_reader()\n                buf = b\"\"\n                for rbuf in reader:\n                    buf += rbuf\n                    if not rbuf or len(buf) >= 32768:\n                        break\n\n                if buf:\n                    orig = buf.decode(\"utf-8\", \"replace\")\n                    t = \"urlform_raw %d @ %r\\n  %r\\n\"\n                    self.log(t % (len(orig), \"/\" + self.vpath, orig))\n                    try:\n                        zb = unquote(buf.replace(b\"+\", b\" \").replace(b\"&\", b\"\\n\"))\n                        plain = zb.decode(\"utf-8\", \"replace\")\n                        if buf.startswith(b\"msg=\"):\n                            plain = plain[4:]\n                            xm = self.vn.flags.get(\"xm\")\n                            if xm:\n                                xm_rsp = runhook(\n                                    self.log,\n                                    self.conn.hsrv.broker,\n                                    None,\n                                    \"xm\",\n                                    xm,\n                                    self.vn.canonical(self.rem),\n                                    self.vpath,\n                                    self.host,\n                                    self.uname,\n                                    self.asrv.vfs.get_perms(self.vpath, self.uname),\n                                    time.time(),\n                                    len(buf),\n                                    self.ip,\n                                    time.time(),\n                                    [plain, orig],\n                                )\n\n                        t = \"urlform_dec %d @ %r\\n  %r\\n\"\n                        self.log(t % (len(plain), \"/\" + self.vpath, plain))\n\n                    except Exception as ex:\n                        self.log(repr(ex))\n\n            if \"xm\" in opt:\n                if xm:\n                    self.loud_reply(xm_rsp.get(\"stdout\") or \"\", status=202)\n                    return True\n                else:\n                    return self.handle_get()\n\n            if \"get\" in opt:\n                return self.handle_get()\n\n            raise Pebkac(405, \"POST(%r) is disabled in server config\" % (ctype,))\n\n        raise Pebkac(405, \"don't know how to handle POST(%r)\" % (ctype,))\n\n    def handle_smsg(self) -> bool:\n        if self.mode not in self.args.smsg_set:\n            raise Pebkac(403, \"smsg is disabled for this http-method in server config\")\n\n        msg = self.uparam[\"smsg\"]\n        self.log(\"smsg %d @ %r\\n  %r\\n\" % (len(msg), \"/\" + self.vpath, msg))\n\n        xm = self.vn.flags.get(\"xm\")\n        if xm:\n            xm_rsp = runhook(\n                self.log,\n                self.conn.hsrv.broker,\n                None,\n                \"xm\",\n                xm,\n                self.vn.canonical(self.rem),\n                self.vpath,\n                self.host,\n                self.uname,\n                self.asrv.vfs.get_perms(self.vpath, self.uname),\n                time.time(),\n                len(msg),\n                self.ip,\n                time.time(),\n                [msg, msg],\n            )\n            self.loud_reply(xm_rsp.get(\"stdout\") or \"\", status=202)\n        else:\n            self.loud_reply(\"k\", status=202)\n        return True\n\n    def get_xml_enc(self, txt: str) -> str:\n        ofs = txt[:512].find(' encoding=\"')\n        enc = \"\"\n        if ofs + 1:\n            enc = txt[ofs + 6 :].split('\"')[1]\n        else:\n            enc = self.headers.get(\"content-type\", \"\").lower()\n            ofs = enc.find(\"charset=\")\n            if ofs + 1:\n                enc = enc[ofs + 4].split(\"=\")[1].split(\";\")[0].strip(\"\\\"'\")\n            else:\n                enc = \"\"\n\n        return enc or \"utf-8\"\n\n    def get_body_reader(self) -> tuple[Generator[bytes, None, None], int]:\n        bufsz = self.args.s_rd_sz\n        if \"chunked\" in self.headers.get(\"transfer-encoding\", \"\").lower():\n            return read_socket_chunked(self.sr, bufsz), -1\n\n        remains = int(self.headers.get(\"content-length\", -1))\n        if remains == -1:\n            self.keepalive = False\n            self.in_hdr_recv = True\n            self.s.settimeout(max(self.args.s_tbody // 20, 1))\n            return read_socket_unbounded(self.sr, bufsz), remains\n        else:\n            return read_socket(self.sr, bufsz, remains), remains\n\n    def dump_to_file(self, is_put: bool) -> tuple[int, str, str, str, int, str, str]:\n        # post_sz, halg, sha_hex, sha_b64, remains, path, url\n        reader, remains = self.get_body_reader()\n        vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)\n        rnd, lifetime, xbu, xau = self.upload_flags(vfs)\n        lim = vfs.get_dbv(rem)[0].lim\n        fdir = vfs.canonical(rem)\n        fn = None\n        if rem and not self.trailing_slash and not bos.path.isdir(fdir):\n            fdir, fn = os.path.split(fdir)\n            rem, _ = vsplit(rem)\n\n        if lim:\n            fdir, rem = lim.all(\n                self.ip, rem, remains, vfs.realpath, fdir, self.conn.hsrv.broker\n            )\n\n        bos.makedirs(fdir, vf=vfs.flags)\n\n        open_ka: dict[str, Any] = {\"fun\": open}\n        open_a = [\"wb\", self.args.iobuf]\n\n        # user-request || config-force\n        if (\"gz\" in vfs.flags or \"xz\" in vfs.flags) and (\n            \"pk\" in vfs.flags\n            or \"pk\" in self.uparam\n            or \"gz\" in self.uparam\n            or \"xz\" in self.uparam\n        ):\n            fb = {\"gz\": 9, \"xz\": 0}  # default/fallback level\n            lv = {}  # selected level\n            alg = \"\"  # selected algo (gz=preferred)\n\n            # user-prefs first\n            if \"gz\" in self.uparam or \"pk\" in self.uparam:  # def.pk\n                alg = \"gz\"\n            if \"xz\" in self.uparam:\n                alg = \"xz\"\n            if alg:\n                zso = self.uparam.get(alg)\n                lv[alg] = fb[alg] if zso is None else int(zso)\n\n            if alg not in vfs.flags:\n                alg = \"gz\" if \"gz\" in vfs.flags else \"xz\"\n\n            # then server overrides\n            pk = vfs.flags.get(\"pk\")\n            if pk is not None:\n                # config-forced on\n                alg = alg or \"gz\"  # def.pk\n                try:\n                    # config-forced opts\n                    alg, nlv = pk.split(\",\")\n                    lv[alg] = int(nlv)\n                except:\n                    pass\n\n            lv[alg] = lv.get(alg) or fb.get(alg) or 0\n\n            self.log(\"compressing with {} level {}\".format(alg, lv.get(alg)))\n            if alg == \"gz\":\n                open_ka[\"fun\"] = gzip.GzipFile\n                open_a = [\"wb\", lv[alg], None, 0x5FEE6600]  # 2021-01-01\n            elif alg == \"xz\":\n                assert lzma  # type: ignore  # !rm\n                open_ka = {\"fun\": lzma.open, \"preset\": lv[alg]}\n                open_a = [\"wb\"]\n            else:\n                self.log(\"fallthrough? thats a bug\", 1)\n\n        suffix = \"-{:.6f}-{}\".format(time.time(), self.dip())\n        nameless = not fn\n        if nameless:\n            fn = vfs.flags[\"put_name2\"].format(now=time.time(), cip=self.dip())\n\n        params = {\"suffix\": suffix, \"fdir\": fdir, \"vf\": vfs.flags}\n        if self.args.nw:\n            params = {}\n            fn = os.devnull\n\n        params.update(open_ka)\n        assert fn  # !rm\n\n        if not self.args.nw:\n            if rnd:\n                fn = rand_name(fdir, fn, rnd)\n\n            fn = sanitize_fn(fn or \"\")\n\n        path = os.path.join(fdir, fn)\n\n        if xbu:\n            at = time.time() - lifetime\n            vp = vjoin(self.vpath, fn) if nameless else self.vpath\n            hr = runhook(\n                self.log,\n                self.conn.hsrv.broker,\n                None,\n                \"xbu.http.dump\",\n                xbu,\n                path,\n                vp,\n                self.host,\n                self.uname,\n                self.asrv.vfs.get_perms(self.vpath, self.uname),\n                at,\n                remains,\n                self.ip,\n                at,\n                None,\n            )\n            t = hr.get(\"rejectmsg\") or \"\"\n            if t or hr.get(\"rc\") != 0:\n                if not t:\n                    t = \"upload blocked by xbu server config: %r\" % (vp,)\n                self.log(t, 1)\n                raise Pebkac(403, t)\n            if hr.get(\"reloc\"):\n                x = pathmod(self.asrv.vfs, path, vp, hr[\"reloc\"])\n                if x:\n                    if self.args.hook_v:\n                        log_reloc(self.log, hr[\"reloc\"], x, path, vp, fn, vfs, rem)\n                    fdir, self.vpath, fn, (vfs, rem) = x\n                    if self.args.nw:\n                        fn = os.devnull\n                    else:\n                        bos.makedirs(fdir, vf=vfs.flags)\n                        path = os.path.join(fdir, fn)\n                        if not nameless:\n                            self.vpath = vjoin(self.vpath, fn)\n                        params[\"fdir\"] = fdir\n\n        if (\n            is_put\n            and not (self.args.no_dav or self.args.nw)\n            and \"append\" not in self.uparam\n            and bos.path.exists(path)\n        ):\n            # allow overwrite if...\n            #  * volflag 'daw' is set, or client is definitely webdav\n            #  * and account has delete-access\n            # or...\n            #  * file exists, is empty, sufficiently new\n            #  * and there is no .PARTIAL\n\n            tnam = fn + \".PARTIAL\"\n            if self.args.dotpart:\n                tnam = \".\" + tnam\n\n            if (\n                self.can_delete\n                and (\n                    vfs.flags.get(\"daw\")\n                    or \"replace\" in self.headers\n                    or \"x-oc-mtime\" in self.headers\n                    or (\n                        self.args.dav_port\n                        and self.args.dav_port == self.s.getsockname()[1]\n                    )\n                )\n            ) or (\n                not bos.path.exists(os.path.join(fdir, tnam))\n                and not bos.path.getsize(path)\n                and bos.path.getmtime(path) >= time.time() - self.args.blank_wt\n            ):\n                # small toctou, but better than clobbering a hardlink\n                wunlink(self.log, path, vfs.flags)\n\n        hasher = None\n        copier = hashcopy\n        halg = self.ouparam.get(\"ck\") or self.headers.get(\"ck\") or vfs.flags[\"put_ck\"]\n        if halg == \"sha512\":\n            pass\n        elif halg == \"no\":\n            copier = justcopy\n            halg = \"\"\n        elif halg == \"md5\":\n            hasher = hashlib.md5(**USED4SEC)\n        elif halg == \"sha1\":\n            hasher = hashlib.sha1(**USED4SEC)\n        elif halg == \"sha256\":\n            hasher = hashlib.sha256(**USED4SEC)\n        elif halg in (\"blake2\", \"b2\"):\n            hasher = hashlib.blake2b(**USED4SEC)\n        elif halg in (\"blake2s\", \"b2s\"):\n            hasher = hashlib.blake2s(**USED4SEC)\n        else:\n            raise Pebkac(500, \"unknown hash alg\")\n\n        if \"apnd\" in self.uparam and not self.args.nw and bos.path.exists(path):\n            zs = vfs.flags[\"apnd_who\"]\n            if (\n                zs == \"w\"\n                or (zs == \"aw\" and self.can_admin)\n                or (zs == \"dw\" and self.can_delete)\n            ):\n                pass\n            elif zs == \"ndd\":\n                raise Pebkac(400, \"append is denied here due to non-reflink dedup\")\n            else:\n                raise Pebkac(400, \"you do not have permission to append\")\n            zs = os.path.join(params[\"fdir\"], fn)\n            self.log(\"upload will append to [%s]\" % (zs,))\n            f = open(zs, \"ab\")\n        else:\n            f, fn = ren_open(fn, *open_a, **params)\n\n        max_sz = 0\n        if lim and remains < 0:\n            if lim.vbmax:\n                max_sz = lim.c_vb_v\n            if lim.smax and (not max_sz or max_sz > lim.smax):\n                max_sz = lim.smax\n\n        try:\n            path = os.path.join(fdir, fn)\n            post_sz, sha_hex, sha_b64 = copier(\n                reader, f, hasher, max_sz, self.args.s_wr_slp\n            )\n        except:\n            if max_sz and self.sr.nb >= max_sz:\n                f.close()  # windows\n                wunlink(self.log, path, vfs.flags)\n            raise\n        finally:\n            f.close()\n\n        if lim:\n            lim.nup(self.ip)\n            lim.bup(self.ip, post_sz)\n            try:\n                lim.chk_sz(post_sz)\n                lim.chk_vsz(self.conn.hsrv.broker, vfs.realpath, post_sz)\n            except:\n                wunlink(self.log, path, vfs.flags)\n                raise\n\n        if self.args.nw:\n            return post_sz, halg, sha_hex, sha_b64, remains, path, \"\"\n\n        at = mt = time.time() - lifetime\n        cli_mt = self.headers.get(\"x-oc-mtime\")\n        if cli_mt:\n            bos.utime_c(self.log, path, float(cli_mt), False)\n\n        if nameless and \"magic\" in vfs.flags:\n            try:\n                ext = self.conn.hsrv.magician.ext(path)\n            except Exception as ex:\n                self.log(\"filetype detection failed for %r: %s\" % (path, ex), 6)\n                ext = None\n\n            if ext:\n                if rnd:\n                    fn2 = rand_name(fdir, \"a.\" + ext, rnd)\n                else:\n                    fn2 = fn.rsplit(\".\", 1)[0] + \".\" + ext\n\n                params[\"suffix\"] = suffix[:-4]\n                f, fn2 = ren_open(fn2, *open_a, **params)\n                f.close()\n\n                path2 = os.path.join(fdir, fn2)\n                atomic_move(self.log, path, path2, vfs.flags)\n                fn = fn2\n                path = path2\n\n        if xau:\n            vp = vjoin(self.vpath, fn) if nameless else self.vpath\n            hr = runhook(\n                self.log,\n                self.conn.hsrv.broker,\n                None,\n                \"xau.http.dump\",\n                xau,\n                path,\n                vp,\n                self.host,\n                self.uname,\n                self.asrv.vfs.get_perms(self.vpath, self.uname),\n                mt,\n                post_sz,\n                self.ip,\n                at,\n                None,\n            )\n            t = hr.get(\"rejectmsg\") or \"\"\n            if t or hr.get(\"rc\") != 0:\n                if not t:\n                    t = \"upload blocked by xau server config: %r\" % (vp,)\n                self.log(t, 1)\n                wunlink(self.log, path, vfs.flags)\n                raise Pebkac(403, t)\n            if hr.get(\"reloc\"):\n                x = pathmod(self.asrv.vfs, path, vp, hr[\"reloc\"])\n                if x:\n                    if self.args.hook_v:\n                        log_reloc(self.log, hr[\"reloc\"], x, path, vp, fn, vfs, rem)\n                    fdir, self.vpath, fn, (vfs, rem) = x\n                    bos.makedirs(fdir, vf=vfs.flags)\n                    path2 = os.path.join(fdir, fn)\n                    atomic_move(self.log, path, path2, vfs.flags)\n                    path = path2\n                    if not nameless:\n                        self.vpath = vjoin(self.vpath, fn)\n            sz = bos.path.getsize(path)\n        else:\n            sz = post_sz\n\n        vfs, rem = vfs.get_dbv(rem)\n        self.conn.hsrv.broker.say(\n            \"up2k.hash_file\",\n            vfs.realpath,\n            vfs.vpath,\n            vfs.flags,\n            rem,\n            fn,\n            self.ip,\n            at,\n            self.uname,\n            True,\n        )\n\n        vsuf = \"\"\n        if (self.can_read or self.can_upget) and \"fk\" in vfs.flags:\n            alg = 2 if \"fka\" in vfs.flags else 1\n            vsuf = \"?k=\" + self.gen_fk(\n                alg,\n                self.args.fk_salt,\n                path,\n                sz,\n                0 if ANYWIN else bos.stat(path).st_ino,\n            )[: vfs.flags[\"fk\"]]\n\n        if \"media\" in self.uparam or \"medialinks\" in vfs.flags:\n            vsuf += \"&v\" if vsuf else \"?v\"\n\n        vpath = \"/\".join([x for x in [vfs.vpath, rem, fn] if x])\n        vpath = quotep(vpath)\n\n        if self.args.up_site:\n            url = \"%s%s%s%s\" % (\n                self.args.up_site,\n                self.args.RS,\n                vpath,\n                vsuf,\n            )\n        else:\n            url = \"%s://%s/%s%s%s\" % (\n                \"https\" if self.is_https else \"http\",\n                self.host,\n                self.args.RS,\n                vpath,\n                vsuf,\n            )\n\n        return post_sz, halg, sha_hex, sha_b64, remains, path, url\n\n    def handle_stash(self, is_put: bool) -> bool:\n        post_sz, halg, sha_hex, sha_b64, remains, path, url = self.dump_to_file(is_put)\n        spd = self._spd(post_sz)\n        t = \"%s wrote %d/%d bytes to %r  # %s\"\n        self.log(t % (spd, post_sz, remains, path, sha_b64[:28]))  # 21\n\n        mime = \"text/plain; charset=utf-8\"\n        ac = self.uparam.get(\"want\") or self.headers.get(\"accept\") or \"\"\n        if ac:\n            ac = ac.split(\";\", 1)[0].lower()\n            if ac == \"application/json\":\n                ac = \"json\"\n        if ac == \"url\":\n            t = url\n        elif ac == \"json\" or \"j\" in self.uparam:\n            jmsg = {\"fileurl\": url, \"filesz\": post_sz}\n            if halg:\n                jmsg[halg] = sha_hex[:56]\n                jmsg[\"sha_b64\"] = sha_b64\n\n            mime = \"application/json\"\n            t = json.dumps(jmsg, indent=2, sort_keys=True)\n        else:\n            t = \"{}\\n{}\\n{}\\n{}\\n\".format(post_sz, sha_b64, sha_hex[:56], url)\n\n        h = {\"Location\": url} if is_put and url else {}\n\n        if \"x-oc-mtime\" in self.headers:\n            h[\"X-OC-MTime\"] = \"accepted\"\n            t = \"\"  # some webdav clients expect/prefer this\n\n        self.reply(t.encode(\"utf-8\", \"replace\"), 201, mime=mime, headers=h)\n        return True\n\n    def bakflip(\n        self,\n        f: typing.BinaryIO,\n        ap: str,\n        ofs: int,\n        sz: int,\n        good_sha: str,\n        bad_sha: str,\n        flags: dict[str, Any],\n    ) -> None:\n        now = time.time()\n        t = \"bad-chunk:  %.3f  %s  %s  %d  %s  %s  %r\"\n        t = t % (now, bad_sha, good_sha, ofs, self.ip, self.uname, ap)\n        self.log(t, 5)\n\n        if self.args.bf_log:\n            try:\n                with open(self.args.bf_log, \"ab+\") as f2:\n                    f2.write((t + \"\\n\").encode(\"utf-8\", \"replace\"))\n            except Exception as ex:\n                self.log(\"append %s failed: %r\" % (self.args.bf_log, ex))\n\n        if not self.args.bak_flips or self.args.nw:\n            return\n\n        sdir = self.args.bf_dir\n        fp = os.path.join(sdir, bad_sha)\n        if bos.path.exists(fp):\n            return self.log(\"no bakflip; have it\", 6)\n\n        if not bos.path.isdir(sdir):\n            bos.makedirs(sdir)\n\n        if len(bos.listdir(sdir)) >= self.args.bf_nc:\n            return self.log(\"no bakflip; too many\", 3)\n\n        nrem = sz\n        f.seek(ofs)\n        with open(fp, \"wb\") as fo:\n            while nrem:\n                buf = f.read(min(nrem, self.args.iobuf))\n                if not buf:\n                    break\n\n                nrem -= len(buf)\n                fo.write(buf)\n\n        if nrem:\n            self.log(\"bakflip truncated; {} remains\".format(nrem), 1)\n            atomic_move(self.log, fp, fp + \".trunc\", flags)\n        else:\n            self.log(\"bakflip ok\", 2)\n\n    def _spd(self, nbytes: int, add: bool = True) -> str:\n        if add:\n            self.conn.nbyte += nbytes\n\n        spd1 = get_spd(nbytes, self.t0)\n        spd2 = get_spd(self.conn.nbyte, self.conn.t0)\n        return \"%s %s n%s\" % (spd1, spd2, self.conn.nreq)\n\n    def handle_post_multipart(self) -> bool:\n        self.parser = MultipartParser(self.log, self.args, self.sr, self.headers)\n        self.parser.parse()\n\n        file0: list[tuple[str, Optional[str], Generator[bytes, None, None]]] = []\n        try:\n            act = self.parser.require(\"act\", 64)\n        except WrongPostKey as ex:\n            if ex.got == \"f\" and ex.fname:\n                self.log(\"missing 'act', but looks like an upload so assuming that\")\n                file0 = [(ex.got, ex.fname, ex.datagen)]\n                act = \"bput\"\n            else:\n                raise\n\n        if act == \"login\":\n            return self.handle_login()\n\n        if act == \"mkdir\":\n            return self.handle_mkdir()\n\n        if act == \"new_md\":\n            # kinda silly but has the least side effects\n            return self.handle_new_md()\n\n        if act in (\"bput\", \"uput\"):\n            return self.handle_plain_upload(file0, act == \"uput\")\n\n        if act == \"tput\":\n            return self.handle_text_upload()\n\n        if act == \"zip\":\n            return self.handle_zip_post()\n\n        if act == \"chpw\":\n            return self.handle_chpw()\n\n        if act == \"logout\":\n            return self.handle_logout()\n\n        raise Pebkac(422, \"invalid action %r\" % (act,))\n\n    def handle_zip_post(self) -> bool:\n        assert self.parser  # !rm\n        try:\n            k = next(x for x in self.uparam if x in (\"zip\", \"tar\"))\n        except:\n            raise Pebkac(422, \"need zip or tar keyword\")\n\n        v = self.uparam[k]\n\n        if self._use_dirkey(self.vn, \"\"):\n            vn = self.vn\n            rem = self.rem\n        else:\n            vn, rem = self.asrv.vfs.get(self.vpath, self.uname, True, False)\n\n        zs = self.parser.require(\"files\", 1024 * 1024)\n        if not zs:\n            raise Pebkac(422, \"need files list\")\n\n        items = zs.replace(\"\\r\", \"\").split(\"\\n\")\n        items = [unquotep(x) for x in items if items]\n\n        self.parser.drop()\n        return self.tx_zip(k, v, \"\", vn, rem, items)\n\n    def handle_post_json(self) -> bool:\n        try:\n            remains = int(self.headers[\"content-length\"])\n        except:\n            raise Pebkac(411)\n\n        if remains > 1024 * 1024:\n            raise Pebkac(413, \"json 2big\")\n\n        enc = \"utf-8\"\n        ctype = self.headers.get(\"content-type\", \"\").lower()\n        if \"charset\" in ctype:\n            enc = ctype.split(\"charset\")[1].strip(\" =\").split(\";\")[0].strip()\n\n        try:\n            json_buf = self.sr.recv_ex(remains)\n        except UnrecvEOF:\n            raise Pebkac(422, \"client disconnected while posting JSON\")\n\n        try:\n            body = json.loads(json_buf.decode(enc, \"replace\"))\n            try:\n                zds = {k: v for k, v in body.items()}\n                zds[\"hash\"] = \"%d chunks\" % (len(body[\"hash\"]),)\n            except:\n                zds = body\n            t = \"POST len=%d type=%s ip=%s user=%s req=%r json=%s\"\n            self.log(t % (len(json_buf), enc, self.ip, self.uname, self.req, zds))\n        except:\n            raise Pebkac(422, \"you POSTed %d bytes of invalid json\" % (len(json_buf),))\n\n        # self.reply(b\"cloudflare\", 503)\n        # return True\n\n        if \"srch\" in self.uparam or \"srch\" in body:\n            return self.handle_search(body)\n\n        if \"share\" in self.uparam:\n            return self.handle_share(body)\n\n        if \"delete\" in self.uparam:\n            return self.handle_rm(body)\n\n        name = undot(body[\"name\"])\n        if \"/\" in name:\n            raise Pebkac(400, \"your client is old; press CTRL-SHIFT-R and try again\")\n\n        vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)\n        fsnt = vfs.flags[\"fsnt\"]\n        if fsnt != \"lin\":\n            tl = VPTL_WIN if fsnt == \"win\" else VPTL_MAC\n            rem = rem.translate(tl)\n            name = name.translate(tl)\n        dbv, vrem = vfs.get_dbv(rem)\n\n        name = sanitize_fn(name)\n        if (\n            not self.can_read\n            and self.can_write\n            and name.lower() in dbv.flags[\"emb_all\"]\n            and \"wo_up_readme\" not in dbv.flags\n        ):\n            name = \"_wo_\" + name\n\n        body[\"name\"] = name\n        body[\"vtop\"] = dbv.vpath\n        body[\"ptop\"] = dbv.realpath\n        body[\"prel\"] = vrem\n        body[\"host\"] = self.host\n        body[\"user\"] = self.uname\n        body[\"addr\"] = self.ip\n        body[\"vcfg\"] = dbv.flags\n\n        if not self.can_delete and not body.get(\"replace\") == \"skip\":\n            body.pop(\"replace\", None)\n\n        if rem:\n            dst = vfs.canonical(rem)\n            try:\n                if not bos.path.isdir(dst):\n                    bos.makedirs(dst, vf=vfs.flags)\n            except OSError as ex:\n                self.log(\"makedirs failed %r\" % (dst,))\n                if not bos.path.isdir(dst):\n                    if ex.errno == errno.EACCES:\n                        raise Pebkac(500, \"the server OS denied write-access\")\n\n                    if ex.errno == errno.EEXIST:\n                        raise Pebkac(400, \"some file got your folder name\")\n\n                    raise Pebkac(500, min_ex())\n            except:\n                raise Pebkac(500, min_ex())\n\n        # not to protect u2fh, but to prevent handshakes while files are closing\n        with self.u2mutex:\n            x = self.conn.hsrv.broker.ask(\"up2k.handle_json\", body, self.u2fh.aps)\n            ret = x.get()\n\n        if self.args.shr and self.vpath.startswith(self.args.shr1):\n            # strip common suffix (uploader's folder structure)\n            vp_req, vp_vfs = vroots(self.vpath, vjoin(dbv.vpath, vrem))\n            if not ret[\"purl\"].startswith(vp_vfs):\n                t = \"share-mapping failed; req=%r dbv=%r vrem=%r n1=%r n2=%r purl=%r\"\n                zt = (self.vpath, dbv.vpath, vrem, vp_req, vp_vfs, ret[\"purl\"])\n                raise Pebkac(500, t % zt)\n            ret[\"purl\"] = vp_req + ret[\"purl\"][len(vp_vfs) :]\n\n        if self.is_vproxied:\n            if \"purl\" in ret:\n                ret[\"purl\"] = self.args.SR + ret[\"purl\"]\n\n        ret = json.dumps(ret)\n        self.log(ret)\n        self.reply(ret.encode(\"utf-8\"), mime=\"application/json\")\n        return True\n\n    def handle_search(self, body: dict[str, Any]) -> bool:\n        idx = self.conn.get_u2idx()\n        if not idx or not hasattr(idx, \"p_end\"):\n            if not HAVE_SQLITE3:\n                raise Pebkac(500, \"sqlite3 not found on server; search is disabled\")\n            raise Pebkac(500, \"server busy, cannot search; please retry in a bit\")\n\n        vols: list[VFS] = []\n        seen: dict[VFS, bool] = {}\n        for vtop in self.rvol:\n            vfs, _ = self.asrv.vfs.get(vtop, self.uname, True, False)\n            vfs = vfs.dbv or vfs\n            if vfs in seen:\n                continue\n\n            seen[vfs] = True\n            vols.append(vfs)\n\n        t0 = time.time()\n        if idx.p_end:\n            penalty = 0.7\n            t_idle = t0 - idx.p_end\n            if idx.p_dur > 0.7 and t_idle < penalty:\n                t = \"rate-limit {:.1f} sec, cost {:.2f}, idle {:.2f}\"\n                raise Pebkac(429, t.format(penalty, idx.p_dur, t_idle))\n\n        if \"srch\" in body:\n            # search by up2k hashlist\n            vbody = copy.deepcopy(body)\n            vbody[\"hash\"] = len(vbody[\"hash\"])\n            self.log(\"qj: \" + repr(vbody))\n            hits = idx.fsearch(self.uname, vols, body)\n            msg: Any = repr(hits)\n            taglist: list[str] = []\n            trunc = False\n        else:\n            # search by query params\n            q = body[\"q\"]\n            n = body.get(\"n\", self.args.srch_hits)\n            self.log(\"qj: %r |%d|\" % (q, n))\n            hits, taglist, trunc = idx.search(self.uname, vols, q, n)\n            msg = len(hits)\n\n        idx.p_end = time.time()\n        idx.p_dur = idx.p_end - t0\n        self.log(\"q#: %r (%.2fs)\" % (msg, idx.p_dur))\n\n        order = []\n        for t in self.args.mte:\n            if t in taglist:\n                order.append(t)\n        for t in taglist:\n            if t not in order:\n                order.append(t)\n\n        if self.is_vproxied:\n            for hit in hits:\n                hit[\"rp\"] = self.args.RS + hit[\"rp\"]\n\n        rj = {\"hits\": hits, \"tag_order\": order, \"trunc\": trunc}\n        r = json.dumps(rj).encode(\"utf-8\")\n        self.reply(r, mime=\"application/json\")\n        return True\n\n    def handle_post_binary(self) -> bool:\n        try:\n            postsize = remains = int(self.headers[\"content-length\"])\n        except:\n            raise Pebkac(400, \"you must supply a content-length for binary POST\")\n\n        try:\n            chashes = self.headers[\"x-up2k-hash\"].split(\",\")\n            wark = self.headers[\"x-up2k-wark\"]\n        except KeyError:\n            raise Pebkac(400, \"need hash and wark headers for binary POST\")\n\n        chashes = [x.strip() for x in chashes]\n        if len(chashes) == 3 and len(chashes[1]) == 1:\n            # the first hash, then length of consecutive hashes,\n            # then a list of stitched hashes as one long string\n            clen = int(chashes[1])\n            siblings = chashes[2]\n            chashes = [chashes[0]]\n            for n in range(0, len(siblings), clen):\n                chashes.append(siblings[n : n + clen])\n\n        vfs, _ = self.asrv.vfs.get(self.vpath, self.uname, False, True)\n        ptop = vfs.get_dbv(\"\")[0].realpath\n        # if this is a share, then get_dbv has been overridden to return\n        # the dbv (which does not exist as a property). And its realpath\n        # could point into the middle of its origin vfs node, meaning it\n        # is not necessarily registered with up2k, so get_dbv is crucial\n\n        broker = self.conn.hsrv.broker\n        x = broker.ask(\"up2k.handle_chunks\", ptop, wark, chashes)\n        response = x.get()\n        chashes, chunksize, cstarts, path, lastmod, fsize, sprs = response\n        maxsize = chunksize * len(chashes)\n        cstart0 = cstarts[0]\n        locked = chashes  # remaining chunks to be received in this request\n        written = []  # chunks written to disk, but not yet released by up2k\n        num_left = -1  # num chunks left according to most recent up2k release\n        bail1 = False  # used in sad path to avoid contradicting error-text\n        treport = time.time()  # ratelimit up2k reporting to reduce overhead\n\n        try:\n            if \"x-up2k-subc\" in self.headers:\n                sc_ofs = int(self.headers[\"x-up2k-subc\"])\n                chash = chashes[0]\n\n                u2sc = self.conn.hsrv.u2sc\n                try:\n                    sc_pofs, hasher = u2sc[chash]\n                    if not sc_ofs:\n                        t = \"client restarted the chunk; forgetting subchunk offset %d\"\n                        self.log(t % (sc_pofs,))\n                        raise Exception()\n                except:\n                    sc_pofs = 0\n                    hasher = hashlib.sha512()\n\n                et = \"subchunk protocol error; resetting chunk \"\n                if sc_pofs != sc_ofs:\n                    u2sc.pop(chash, None)\n                    t = \"%s[%s]: the expected resume-point was %d, not %d\"\n                    raise Pebkac(400, t % (et, chash, sc_pofs, sc_ofs))\n                if len(cstarts) > 1:\n                    u2sc.pop(chash, None)\n                    t = \"%s[%s]: only a single subchunk can be uploaded in one request; you are sending %d chunks\"\n                    raise Pebkac(400, t % (et, chash, len(cstarts)))\n                csize = min(chunksize, fsize - cstart0[0])\n                cstart0[0] += sc_ofs  # also sets cstarts[0][0]\n                sc_next_ofs = sc_ofs + postsize\n                if sc_next_ofs > csize:\n                    u2sc.pop(chash, None)\n                    t = \"%s[%s]: subchunk offset (%d) plus postsize (%d) exceeds chunksize (%d)\"\n                    raise Pebkac(400, t % (et, chash, sc_ofs, postsize, csize))\n                else:\n                    final_subchunk = sc_next_ofs == csize\n                    t = \"subchunk %s %d:%d/%d %s\"\n                    zs = \"END\" if final_subchunk else \"\"\n                    self.log(t % (chash[:15], sc_ofs, sc_next_ofs, csize, zs), 6)\n                    if final_subchunk:\n                        u2sc.pop(chash, None)\n                    else:\n                        u2sc[chash] = (sc_next_ofs, hasher)\n            else:\n                hasher = None\n                final_subchunk = True\n\n            if self.args.nw:\n                path = os.devnull\n\n            if remains > maxsize:\n                t = \"your client is sending %d bytes which is too much (server expected %d bytes at most)\"\n                raise Pebkac(400, t % (remains, maxsize))\n\n            t = \"writing %r %s+%d #%d+%d %s\"\n            chunkno = cstart0[0] // chunksize\n            zs = \" \".join([chashes[0][:15]] + [x[:9] for x in chashes[1:]])\n            self.log(t % (path, cstart0, remains, chunkno, len(chashes), zs))\n\n            f = None\n            fpool = not self.args.no_fpool and sprs\n            if fpool:\n                with self.u2mutex:\n                    try:\n                        f = self.u2fh.pop(path)\n                    except:\n                        pass\n\n            f = f or open(fsenc(path), \"rb+\", self.args.iobuf)\n\n            try:\n                for chash, cstart in zip(chashes, cstarts):\n                    f.seek(cstart[0])\n                    reader = read_socket(\n                        self.sr, self.args.s_rd_sz, min(remains, chunksize)\n                    )\n                    post_sz, _, sha_b64 = hashcopy(\n                        reader, f, hasher, 0, self.args.s_wr_slp\n                    )\n\n                    if sha_b64 != chash and final_subchunk:\n                        try:\n                            self.bakflip(\n                                f, path, cstart[0], post_sz, chash, sha_b64, vfs.flags\n                            )\n                        except:\n                            self.log(\"bakflip failed: \" + min_ex())\n\n                        t = \"your chunk got corrupted somehow (received {} bytes); expected vs received hash:\\n{}\\n{}\"\n                        raise Pebkac(400, t.format(post_sz, chash, sha_b64))\n\n                    remains -= post_sz\n\n                    if len(cstart) > 1 and path != os.devnull:\n                        t = \" & \".join(unicode(x) for x in cstart[1:])\n                        self.log(\"clone %s to %s\" % (cstart[0], t))\n                        ofs = 0\n                        while ofs < chunksize:\n                            bufsz = max(4 * 1024 * 1024, self.args.iobuf)\n                            bufsz = min(chunksize - ofs, bufsz)\n                            f.seek(cstart[0] + ofs)\n                            buf = f.read(bufsz)\n                            for wofs in cstart[1:]:\n                                f.seek(wofs + ofs)\n                                f.write(buf)\n\n                            ofs += len(buf)\n\n                        self.log(\"clone {} done\".format(cstart[0]))\n\n                    # be quick to keep the tcp winsize scale;\n                    # if we can't confirm rn then that's fine\n                    if final_subchunk:\n                        written.append(chash)\n                    now = time.time()\n                    if now - treport < 1:\n                        continue\n                    treport = now\n                    x = broker.ask(\n                        \"up2k.fast_confirm_chunks\", ptop, wark, written, locked\n                    )\n                    num_left, t = x.get()\n                    if num_left < -1:\n                        self.loud_reply(t, status=500)\n                        locked = written = []\n                        return False\n                    elif num_left >= 0:\n                        t = \"got %d more chunks, %d left\"\n                        self.log(t % (len(written), num_left), 6)\n                        locked = locked[len(written) :]\n                        written = []\n\n                if not fpool:\n                    f.close()\n                else:\n                    with self.u2mutex:\n                        self.u2fh.put(path, f)\n            except:\n                # maybe busted handle (eg. disk went full)\n                f.close()\n                raise\n        finally:\n            if locked:\n                # now block until all chunks released+confirmed\n                x = broker.ask(\"up2k.confirm_chunks\", ptop, wark, written, locked)\n                num_left, t = x.get()\n                if num_left < 0:\n                    self.loud_reply(t, status=500)\n                    bail1 = True\n                else:\n                    t = \"got %d more chunks, %d left\"\n                    self.log(t % (len(written), num_left), 6)\n\n        if num_left < 0:\n            if bail1:\n                return False\n            raise Pebkac(500, \"unconfirmed; see fileserver log\")\n\n        if not num_left and fpool:\n            with self.u2mutex:\n                self.u2fh.close(path)\n\n        if not num_left and not self.args.nw:\n            broker.ask(\"up2k.finish_upload\", ptop, wark, self.u2fh.aps).get()\n\n        cinf = self.headers.get(\"x-up2k-stat\", \"\")\n\n        spd = self._spd(postsize)\n        self.log(\"%70s thank %r\" % (spd, cinf))\n\n        if remains:\n            t = \"incorrect content-length from client\"\n            self.log(\"%s; header=%d, remains=%d\" % (t, postsize, remains), 3)\n            raise Pebkac(400, t)\n\n        self.reply(b\"thank\")\n        return True\n\n    def handle_chpw(self) -> bool:\n        assert self.parser  # !rm\n        if self.args.usernames:\n            self.parser.require(\"uname\", 64)\n        pwd = self.parser.require(\"pw\", 64)\n        self.parser.drop()\n\n        ok, msg = self.asrv.chpw(self.conn.hsrv.broker, self.uname, pwd)\n        if ok:\n            self.cbonk(self.conn.hsrv.gpwc, pwd, \"pw\", \"too many password changes\")\n            if self.args.usernames:\n                pwd = \"%s:%s\" % (self.uname, pwd)\n            ok, msg = self.get_pwd_cookie(pwd)\n            if ok:\n                msg = \"new password OK\"\n\n        redir = (self.args.SRS + \"?h\") if ok else \"\"\n        h2 = '<a href=\"' + self.args.SRS + '?h\">continue</a>'\n        html = self.j2s(\"msg\", h1=msg, h2=h2, redir=redir)\n        self.reply(html.encode(\"utf-8\"))\n        return True\n\n    def handle_login(self) -> bool:\n        assert self.parser  # !rm\n        if self.args.usernames and not (\n            self.args.shr and self.vpath.startswith(self.args.shr1)\n        ):\n            try:\n                un = self.parser.require(\"uname\", 64)\n            except:\n                un = \"\"\n        else:\n            un = \"\"\n        pwd = self.parser.require(\"cppwd\", 64)\n        try:\n            uhash = self.parser.require(\"uhash\", 256)\n        except:\n            uhash = \"\"\n        self.parser.drop()\n\n        if not pwd:\n            raise Pebkac(422, \"password cannot be blank\")\n\n        if un:\n            pwd = \"%s:%s\" % (un, pwd)\n\n        dst = self.args.SRS\n        if self.vpath:\n            dst += quotep(self.vpaths)\n\n        dst += self.ourlq()\n\n        uhash = uhash.lstrip(\"#\")\n        if uhash not in (\"\", \"-\"):\n            dst += \"&\" if \"?\" in dst else \"?\"\n            dst += \"_=1#\" + html_escape(uhash, True, True)\n\n        _, msg = self.get_pwd_cookie(pwd)\n        h2 = '<a href=\"' + dst + '\">continue</a>'\n        html = self.j2s(\"msg\", h1=msg, h2=h2, redir=dst)\n        self.reply(html.encode(\"utf-8\"))\n        return True\n\n    def handle_logout(self) -> bool:\n        assert self.parser  # !rm\n        self.parser.drop()\n\n        self.log(\"logout \" + self.uname)\n        if not self.uname.startswith(\"s_\"):\n            self.asrv.forget_session(self.conn.hsrv.broker, self.uname)\n        self.get_pwd_cookie(\"x\")\n\n        dst = self.args.idp_logout or (self.args.SRS + \"?h\")\n        h2 = '<a href=\"' + dst + '\">continue</a>'\n        html = self.j2s(\"msg\", h1=\"ok bye\", h2=h2, redir=dst)\n        self.reply(html.encode(\"utf-8\"))\n        return True\n\n    def get_pwd_cookie(self, pwd: str) -> tuple[bool, str]:\n        uname = self.asrv.sesa.get(pwd)\n        if not uname:\n            hpwd = self.asrv.ah.hash(pwd)\n            uname = self.asrv.iacct.get(hpwd)\n            if uname:\n                pwd = self.asrv.ases.get(uname) or pwd\n        if uname and self.conn.hsrv.ipr:\n            znm = self.conn.hsrv.ipr.get(uname)\n            if znm and not znm.map(self.ip):\n                self.log(\"username [%s] rejected by --ipr\" % (self.uname,), 3)\n                uname = \"\"\n        if uname:\n            msg = \"hi \" + uname\n            dur = int(60 * 60 * self.args.logout)\n        else:\n            logpwd = pwd\n            if self.args.log_badpwd == 0:\n                logpwd = \"\"\n            elif self.args.log_badpwd == 2:\n                zb = hashlib.sha512(pwd.encode(\"utf-8\", \"replace\")).digest()\n                logpwd = \"%\" + ub64enc(zb[:12]).decode(\"ascii\")\n\n            if pwd != \"x\":\n                self.log(\"invalid password: %r\" % (logpwd,), 3)\n                self.cbonk(self.conn.hsrv.gpwd, pwd, \"pw\", \"invalid passwords\")\n\n            msg = \"naw dude\"\n            pwd = \"x\"  # nosec\n            dur = 0\n\n        if pwd == \"x\":\n            # reset both plaintext and tls\n            # (only affects active tls cookies when tls)\n            for k in (\"cppwd\", \"cppws\") if self.is_https else (\"cppwd\",):\n                ck = gencookie(k, pwd, self.args.R, self.args.cookie_lax, False)\n                self.out_headerlist.append((\"Set-Cookie\", ck))\n            self.out_headers.pop(\"Set-Cookie\", None)  # drop keepalive\n        else:\n            k = \"cppws\" if self.is_https else \"cppwd\"\n            ck = gencookie(\n                k,\n                pwd,\n                self.args.R,\n                self.args.cookie_lax,\n                self.is_https,\n                dur,\n                \"; HttpOnly\",\n            )\n            self.out_headers[\"Set-Cookie\"] = ck\n\n        return dur > 0, msg\n\n    def set_idp_cookie(self, ases) -> None:\n        k = \"cppws\" if self.is_https else \"cppwd\"\n        ck = gencookie(\n            k,\n            ases,\n            self.args.R,\n            self.args.cookie_lax,\n            self.is_https,\n            self.args.idp_cookie,\n            \"; HttpOnly\",\n        )\n        self.out_headers[\"Set-Cookie\"] = ck\n\n    def handle_mkdir(self) -> bool:\n        assert self.parser  # !rm\n        new_dir = self.parser.require(\"name\", 512)\n        self.parser.drop()\n\n        return self._mkdir(vjoin(self.vpath, new_dir))\n\n    def _mkdir(self, vpath: str, dav: bool = False) -> bool:\n        nullwrite = self.args.nw\n        self.gctx = vpath\n        vpath = undot(vpath)\n        vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True)\n        if \"nosub\" in vfs.flags:\n            raise Pebkac(403, \"mkdir is forbidden below this folder\")\n\n        rem = sanitize_vpath(rem)\n        fn = vfs.canonical(rem)\n\n        if not nullwrite:\n            fdir = os.path.dirname(fn)\n\n            if dav and not bos.path.isdir(fdir):\n                raise Pebkac(409, \"parent folder does not exist\")\n\n            if bos.path.isdir(fn):\n                raise Pebkac(405, 'folder \"/%s\" already exists' % (vpath,))\n\n            try:\n                bos.makedirs(fn, vf=vfs.flags)\n            except OSError as ex:\n                if ex.errno == errno.EACCES:\n                    raise Pebkac(500, \"the server OS denied write-access\")\n\n                raise Pebkac(500, \"mkdir failed:\\n\" + min_ex())\n            except:\n                raise Pebkac(500, min_ex())\n\n        self.out_headers[\"X-New-Dir\"] = quotep(self.args.RS + vpath)\n\n        if dav:\n            self.reply(b\"\", 201)\n        else:\n            self.redirect(vpath, status=201)\n\n        return True\n\n    def handle_new_md(self) -> bool:\n        assert self.parser  # !rm\n        new_file = self.parser.require(\"name\", 512)\n        self.parser.drop()\n\n        nullwrite = self.args.nw\n        vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)\n        self._assert_safe_rem(rem)\n\n        if not self.can_delete and (\n            \".\" not in new_file\n            or new_file.rsplit(\".\", 1)[1].lower() not in vfs.flags[\"rw_edit_set\"]\n        ):\n            t = \"you can only create %s files because you don't have the delete-permission\"\n            raise Pebkac(400, t % (vfs.flags[\"rw_edit\"].replace(\",\", \"/\")))\n\n        sanitized = sanitize_fn(new_file)\n        fdir = vfs.canonical(rem)\n        fn = os.path.join(fdir, sanitized)\n\n        for hn in (\"xbu\", \"xau\"):\n            xxu = vfs.flags.get(hn)\n            if xxu:\n                hr = runhook(\n                    self.log,\n                    self.conn.hsrv.broker,\n                    None,\n                    \"%s.http.new-md\" % (hn,),\n                    xxu,\n                    fn,\n                    vjoin(self.vpath, sanitized),\n                    self.host,\n                    self.uname,\n                    self.asrv.vfs.get_perms(self.vpath, self.uname),\n                    time.time(),\n                    0,\n                    self.ip,\n                    time.time(),\n                    None,\n                )\n                t = hr.get(\"rejectmsg\") or \"\"\n                if t or hr.get(\"rc\") != 0:\n                    if not t:\n                        t = \"new-md blocked by \" + hn + \" server config: %r\"\n                        t = t % (vjoin(vfs.vpath, rem),)\n                    self.log(t, 1)\n                    raise Pebkac(403, t)\n\n        if not nullwrite:\n            if bos.path.exists(fn):\n                raise Pebkac(500, \"that file exists already\")\n\n            with open(fsenc(fn), \"wb\") as f:\n                if \"fperms\" in vfs.flags:\n                    set_fperms(f, vfs.flags)\n\n            dbv, vrem = vfs.get_dbv(rem)\n            self.conn.hsrv.broker.say(\n                \"up2k.hash_file\",\n                dbv.realpath,\n                dbv.vpath,\n                dbv.flags,\n                vrem,\n                sanitized,\n                self.ip,\n                bos.stat(fn).st_mtime,\n                self.uname,\n                True,\n            )\n\n        vpath = \"{}/{}\".format(self.vpath, sanitized).lstrip(\"/\")\n        self.redirect(vpath, \"?edit\")\n        return True\n\n    def upload_flags(self, vfs: VFS) -> tuple[int, int, list[str], list[str]]:\n        if self.args.nw:\n            rnd = 0\n        else:\n            rnd = int(self.uparam.get(\"rand\") or self.headers.get(\"rand\") or 0)\n            if vfs.flags.get(\"rand\"):  # force-enable\n                rnd = max(rnd, vfs.flags[\"nrand\"])\n\n        zs = self.uparam.get(\"life\", self.headers.get(\"life\", \"\"))\n        if zs:\n            vlife = vfs.flags.get(\"lifetime\") or 0\n            lifetime = max(0, int(vlife - int(zs)))\n        else:\n            lifetime = 0\n\n        return (\n            rnd,\n            lifetime,\n            vfs.flags.get(\"xbu\") or [],\n            vfs.flags.get(\"xau\") or [],\n        )\n\n    def handle_plain_upload(\n        self,\n        file0: list[tuple[str, Optional[str], Generator[bytes, None, None]]],\n        nohash: bool,\n    ) -> bool:\n        assert self.parser\n        nullwrite = self.args.nw\n        vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)\n        self._assert_safe_rem(rem)\n\n        hasher = None\n        if nohash:\n            halg = \"\"\n            copier = justcopy\n        else:\n            copier = hashcopy\n            halg = (\n                self.ouparam.get(\"ck\") or self.headers.get(\"ck\") or vfs.flags[\"bup_ck\"]\n            )\n            if halg == \"sha512\":\n                pass\n            elif halg == \"no\":\n                copier = justcopy\n                halg = \"\"\n            elif halg == \"md5\":\n                hasher = hashlib.md5(**USED4SEC)\n            elif halg == \"sha1\":\n                hasher = hashlib.sha1(**USED4SEC)\n            elif halg == \"sha256\":\n                hasher = hashlib.sha256(**USED4SEC)\n            elif halg in (\"blake2\", \"b2\"):\n                hasher = hashlib.blake2b(**USED4SEC)\n            elif halg in (\"blake2s\", \"b2s\"):\n                hasher = hashlib.blake2s(**USED4SEC)\n            else:\n                raise Pebkac(500, \"unknown hash alg\")\n\n        upload_vpath = self.vpath\n        lim = vfs.get_dbv(rem)[0].lim\n        fdir_base = vfs.canonical(rem)\n        if lim:\n            fdir_base, rem = lim.all(\n                self.ip, rem, -1, vfs.realpath, fdir_base, self.conn.hsrv.broker\n            )\n            upload_vpath = \"{}/{}\".format(vfs.vpath, rem).strip(\"/\")\n            if not nullwrite:\n                bos.makedirs(fdir_base, vf=vfs.flags)\n\n        rnd, lifetime, xbu, xau = self.upload_flags(vfs)\n        zs = self.uparam.get(\"want\") or self.headers.get(\"accept\") or \"\"\n        if zs:\n            zs = zs.split(\";\", 1)[0].lower()\n            if zs == \"application/json\":\n                zs = \"json\"\n        want_url = zs == \"url\"\n        want_json = zs == \"json\" or \"j\" in self.uparam\n\n        files: list[tuple[int, str, str, str, str, str]] = []\n        # sz, sha_hex, sha_b64, p_file, fname, abspath\n        errmsg = \"\"\n        tabspath = \"\"\n        dip = self.dip()\n        t0 = time.time()\n        try:\n            assert self.parser.gen\n            gens = itertools.chain(file0, self.parser.gen)\n            for nfile, (p_field, p_file, p_data) in enumerate(gens):\n                if not p_file:\n                    self.log(\"discarding incoming file without filename\")\n                    # fallthrough\n\n                fdir = fdir_base\n                fname = sanitize_fn(p_file or \"\")\n                abspath = os.path.join(fdir, fname)\n                suffix = \"-%.6f-%s\" % (time.time(), dip)\n                if p_file and not nullwrite:\n                    if rnd:\n                        fname = rand_name(fdir, fname, rnd)\n\n                    open_args = {\"fdir\": fdir, \"suffix\": suffix, \"vf\": vfs.flags}\n\n                    if \"replace\" in self.uparam or \"replace\" in self.headers:\n                        if not self.can_delete:\n                            self.log(\"user not allowed to overwrite with ?replace\")\n                        elif bos.path.exists(abspath):\n                            try:\n                                wunlink(self.log, abspath, vfs.flags)\n                                t = \"overwriting file with new upload: %r\"\n                            except:\n                                t = \"toctou while deleting for ?replace: %r\"\n                            self.log(t % (abspath,))\n                else:\n                    open_args = {}\n                    tnam = fname = os.devnull\n                    fdir = abspath = \"\"\n\n                if xbu:\n                    at = time.time() - lifetime\n                    hr = runhook(\n                        self.log,\n                        self.conn.hsrv.broker,\n                        None,\n                        \"xbu.http.bup\",\n                        xbu,\n                        abspath,\n                        vjoin(upload_vpath, fname),\n                        self.host,\n                        self.uname,\n                        self.asrv.vfs.get_perms(upload_vpath, self.uname),\n                        at,\n                        0,\n                        self.ip,\n                        at,\n                        None,\n                    )\n                    t = hr.get(\"rejectmsg\") or \"\"\n                    if t or hr.get(\"rc\") != 0:\n                        if not t:\n                            t = \"upload blocked by xbu server config: %r\"\n                            t = t % (vjoin(upload_vpath, fname),)\n                        self.log(t, 1)\n                        raise Pebkac(403, t)\n                    if hr.get(\"reloc\"):\n                        zs = vjoin(upload_vpath, fname)\n                        x = pathmod(self.asrv.vfs, abspath, zs, hr[\"reloc\"])\n                        if x:\n                            if self.args.hook_v:\n                                log_reloc(\n                                    self.log,\n                                    hr[\"reloc\"],\n                                    x,\n                                    abspath,\n                                    zs,\n                                    fname,\n                                    vfs,\n                                    rem,\n                                )\n                            fdir, upload_vpath, fname, (vfs, rem) = x\n                            abspath = os.path.join(fdir, fname)\n                            if nullwrite:\n                                fdir = abspath = \"\"\n                            else:\n                                open_args[\"fdir\"] = fdir\n\n                if p_file and not nullwrite:\n                    bos.makedirs(fdir, vf=vfs.flags)\n\n                    # reserve destination filename\n                    f, fname = ren_open(fname, \"wb\", fdir=fdir, suffix=suffix)\n                    f.close()\n\n                    tnam = fname + \".PARTIAL\"\n                    if self.args.dotpart:\n                        tnam = \".\" + tnam\n\n                    abspath = os.path.join(fdir, fname)\n                else:\n                    open_args = {}\n                    tnam = fname = os.devnull\n                    fdir = abspath = \"\"\n\n                if lim:\n                    lim.chk_bup(self.ip)\n                    lim.chk_nup(self.ip)\n\n                try:\n                    max_sz = 0\n                    if lim:\n                        v1 = lim.smax\n                        v2 = lim.dfv - lim.dfl\n                        max_sz = min(v1, v2) if v1 and v2 else v1 or v2\n\n                    f, tnam = ren_open(tnam, \"wb\", self.args.iobuf, **open_args)\n                    try:\n                        tabspath = os.path.join(fdir, tnam)\n                        self.log(\"writing to %r\" % (tabspath,))\n                        sz, sha_hex, sha_b64 = copier(\n                            p_data, f, hasher, max_sz, self.args.s_wr_slp\n                        )\n                    finally:\n                        f.close()\n\n                    if lim:\n                        lim.nup(self.ip)\n                        lim.bup(self.ip, sz)\n                        try:\n                            lim.chk_df(tabspath, sz, True)\n                            lim.chk_sz(sz)\n                            lim.chk_vsz(self.conn.hsrv.broker, vfs.realpath, sz)\n                            lim.chk_bup(self.ip)\n                            lim.chk_nup(self.ip)\n                        except:\n                            if not nullwrite:\n                                wunlink(self.log, tabspath, vfs.flags)\n                                wunlink(self.log, abspath, vfs.flags)\n                            fname = os.devnull\n                            raise\n\n                    if not nullwrite:\n                        atomic_move(self.log, tabspath, abspath, vfs.flags)\n\n                    tabspath = \"\"\n\n                    at = time.time() - lifetime\n                    if xau:\n                        hr = runhook(\n                            self.log,\n                            self.conn.hsrv.broker,\n                            None,\n                            \"xau.http.bup\",\n                            xau,\n                            abspath,\n                            vjoin(upload_vpath, fname),\n                            self.host,\n                            self.uname,\n                            self.asrv.vfs.get_perms(upload_vpath, self.uname),\n                            at,\n                            sz,\n                            self.ip,\n                            at,\n                            None,\n                        )\n                        t = hr.get(\"rejectmsg\") or \"\"\n                        if t or hr.get(\"rc\") != 0:\n                            if not t:\n                                t = \"upload blocked by xau server config: %r\"\n                                t = t % (vjoin(upload_vpath, fname),)\n                            self.log(t, 1)\n                            wunlink(self.log, abspath, vfs.flags)\n                            raise Pebkac(403, t)\n                        if hr.get(\"reloc\"):\n                            zs = vjoin(upload_vpath, fname)\n                            x = pathmod(self.asrv.vfs, abspath, zs, hr[\"reloc\"])\n                            if x:\n                                if self.args.hook_v:\n                                    log_reloc(\n                                        self.log,\n                                        hr[\"reloc\"],\n                                        x,\n                                        abspath,\n                                        zs,\n                                        fname,\n                                        vfs,\n                                        rem,\n                                    )\n                                fdir, upload_vpath, fname, (vfs, rem) = x\n                                ap2 = os.path.join(fdir, fname)\n                                if nullwrite:\n                                    fdir = ap2 = \"\"\n                                else:\n                                    bos.makedirs(fdir, vf=vfs.flags)\n                                    atomic_move(self.log, abspath, ap2, vfs.flags)\n                                abspath = ap2\n                        sz = bos.path.getsize(abspath)\n\n                    files.append(\n                        (sz, sha_hex, sha_b64, p_file or \"(discarded)\", fname, abspath)\n                    )\n                    dbv, vrem = vfs.get_dbv(rem)\n                    self.conn.hsrv.broker.say(\n                        \"up2k.hash_file\",\n                        dbv.realpath,\n                        vfs.vpath,\n                        dbv.flags,\n                        vrem,\n                        fname,\n                        self.ip,\n                        at,\n                        self.uname,\n                        True,\n                    )\n                    self.conn.nbyte += sz\n\n                except Pebkac:\n                    self.parser.drop()\n                    raise\n\n        except Pebkac as ex:\n            errmsg = vol_san(\n                list(self.asrv.vfs.all_vols.values()), unicode(ex).encode(\"utf-8\")\n            ).decode(\"utf-8\")\n            try:\n                got = bos.path.getsize(tabspath)\n                t = \"connection lost after receiving %s of the file\"\n                self.log(t % (humansize(got),), 3)\n            except:\n                pass\n\n        td = max(0.1, time.time() - t0)\n        sz_total = sum(x[0] for x in files)\n        spd = (sz_total / td) / (1024 * 1024)\n\n        status = \"OK\"\n        if errmsg:\n            self.log(errmsg, 3)\n            status = \"ERROR\"\n\n        msg = \"{} // {} bytes // {:.3f} MiB/s\\n\".format(status, sz_total, spd)\n        jmsg: dict[str, Any] = {\n            \"status\": status,\n            \"sz\": sz_total,\n            \"mbps\": round(spd, 3),\n            \"files\": [],\n        }\n\n        if errmsg:\n            msg += errmsg + \"\\n\"\n            jmsg[\"error\"] = errmsg\n            errmsg = \"ERROR: \" + errmsg\n\n        if halg:\n            file_fmt = '{0}: {1} // {2} // {3} bytes // <a href=\"{4}\">{5}</a> {6}\\n'\n        else:\n            file_fmt = '{3} bytes // <a href=\"{4}\">{5}</a> {6}\\n'\n\n        for sz, sha_hex, sha_b64, ofn, lfn, ap in files:\n            vsuf = \"\"\n            if (self.can_read or self.can_upget) and \"fk\" in vfs.flags:\n                st = A_FILE if nullwrite else bos.stat(ap)\n                alg = 2 if \"fka\" in vfs.flags else 1\n                vsuf = \"?k=\" + self.gen_fk(\n                    alg,\n                    self.args.fk_salt,\n                    ap,\n                    st.st_size,\n                    0 if ANYWIN or not ap else st.st_ino,\n                )[: vfs.flags[\"fk\"]]\n\n            if \"media\" in self.uparam or \"medialinks\" in vfs.flags:\n                vsuf += \"&v\" if vsuf else \"?v\"\n\n            vpath = vjoin(upload_vpath, lfn)\n            if self.args.up_site:\n                ah_url = j_url = self.args.up_site + self.args.RS + quotep(vpath) + vsuf\n                rel_url = \"/\" + j_url.split(\"//\", 1)[-1].split(\"/\", 1)[-1]\n            else:\n                ah_url = rel_url = \"/%s%s%s\" % (self.args.RS, quotep(vpath), vsuf)\n                j_url = \"%s://%s%s\" % (\n                    \"https\" if self.is_https else \"http\",\n                    self.host,\n                    rel_url,\n                )\n\n            msg += file_fmt.format(\n                halg,\n                sha_hex[:56],\n                sha_b64,\n                sz,\n                ah_url,\n                html_escape(ofn, crlf=True),\n                vsuf,\n            )\n            # truncated SHA-512 prevents length extension attacks;\n            # using SHA-512/224, optionally SHA-512/256 = :64\n            jpart = {\n                \"url\": j_url,\n                \"sz\": sz,\n                \"fn\": lfn,\n                \"fn_orig\": ofn,\n                \"path\": rel_url,\n            }\n            if halg:\n                jpart[halg] = sha_hex[:56]\n                jpart[\"sha_b64\"] = sha_b64\n            jmsg[\"files\"].append(jpart)\n\n        vspd = self._spd(sz_total, False)\n        self.log(\"%s %r\" % (vspd, msg))\n\n        suf = \"\"\n        if not nullwrite and self.args.write_uplog:\n            try:\n                log_fn = \"up.{:.6f}.txt\".format(t0)\n                with open(log_fn, \"wb\") as f:\n                    ft = \"{}:{}\".format(self.ip, self.addr[1])\n                    ft = \"{}\\n{}\\n{}\\n\".format(ft, msg.rstrip(), errmsg)\n                    f.write(ft.encode(\"utf-8\"))\n                    if \"fperms\" in vfs.flags:\n                        set_fperms(f, vfs.flags)\n            except Exception as ex:\n                suf = \"\\nfailed to write the upload report: {}\".format(ex)\n\n        sc = 400 if errmsg else 201\n        if want_url:\n            msg = \"\\n\".join([x[\"url\"] for x in jmsg[\"files\"]])\n            if errmsg:\n                msg += \"\\n\" + errmsg\n\n            self.reply(msg.encode(\"utf-8\", \"replace\"), status=sc)\n        elif want_json:\n            if len(jmsg[\"files\"]) == 1:\n                jmsg[\"fileurl\"] = jmsg[\"files\"][0][\"url\"]\n            jtxt = json.dumps(jmsg, indent=2, sort_keys=True).encode(\"utf-8\", \"replace\")\n            self.reply(jtxt, mime=\"application/json\", status=sc)\n        else:\n            self.redirect(\n                self.vpath,\n                msg=msg + suf,\n                flavor=\"return to\",\n                click=False,\n                status=sc,\n            )\n\n        if errmsg:\n            return False\n\n        self.parser.drop()\n        return True\n\n    def handle_text_upload(self) -> bool:\n        assert self.parser  # !rm\n        try:\n            cli_lastmod3 = int(self.parser.require(\"lastmod\", 16))\n        except:\n            raise Pebkac(400, \"could not read lastmod from request\")\n\n        nullwrite = self.args.nw\n        vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, True, True)\n        self._assert_safe_rem(rem)\n\n        clen = int(self.headers.get(\"content-length\", -1))\n        if clen == -1:\n            raise Pebkac(411)\n\n        rp, fn = vsplit(rem)\n        fp = vfs.canonical(rp)\n        lim = vfs.get_dbv(rem)[0].lim\n        if lim:\n            fp, rp = lim.all(self.ip, rp, clen, vfs.realpath, fp, self.conn.hsrv.broker)\n            bos.makedirs(fp, vf=vfs.flags)\n\n        fp = os.path.join(fp, fn)\n        rem = \"{}/{}\".format(rp, fn).strip(\"/\")\n        dbv, vrem = vfs.get_dbv(rem)\n\n        if not self.can_delete and (\n            \".\" not in rem\n            or rem.rsplit(\".\", 1)[1].lower() not in vfs.flags[\"rw_edit_set\"]\n        ):\n            t = \"you can only edit %s files because you don't have the delete-permission\"\n            raise Pebkac(400, t % (vfs.flags[\"rw_edit\"].replace(\",\", \"/\")))\n\n        if nullwrite:\n            response = json.dumps({\"ok\": True, \"lastmod\": 0})\n            self.log(response)\n            # TODO reply should parser.drop()\n            self.parser.drop()\n            self.reply(response.encode(\"utf-8\"))\n            return True\n\n        srv_lastmod = -1.0\n        srv_lastmod3 = -1\n        try:\n            st = bos.stat(fp)\n            srv_lastmod = st.st_mtime\n            srv_lastmod3 = int(srv_lastmod * 1000)\n        except OSError as ex:\n            if ex.errno != errno.ENOENT:\n                raise\n\n        # if file exists, check that timestamp matches the client's\n        if srv_lastmod >= 0:\n            same_lastmod = cli_lastmod3 in [-1, srv_lastmod3]\n            if not same_lastmod:\n                # some filesystems/transports limit precision to 1sec, hopefully floored\n                same_lastmod = (\n                    srv_lastmod == int(cli_lastmod3 / 1000)\n                    and cli_lastmod3 > srv_lastmod3\n                    and cli_lastmod3 - srv_lastmod3 < 1000\n                )\n\n            if not same_lastmod:\n                response = json.dumps(\n                    {\n                        \"ok\": False,\n                        \"lastmod\": srv_lastmod3,\n                        \"now\": int(time.time() * 1000),\n                    }\n                )\n                self.log(\n                    \"{} - {} = {}\".format(\n                        srv_lastmod3, cli_lastmod3, srv_lastmod3 - cli_lastmod3\n                    )\n                )\n                self.log(response)\n                self.parser.drop()\n                self.reply(response.encode(\"utf-8\"))\n                return True\n\n            mdir, mfile = os.path.split(fp)\n            fname, fext = mfile.rsplit(\".\", 1) if \".\" in mfile else (mfile, \"md\")\n            mfile2 = \"{}.{:.3f}.{}\".format(fname, srv_lastmod, fext)\n\n            dp = \"\"\n            hist_cfg = dbv.flags[\"md_hist\"]\n            if hist_cfg == \"v\":\n                vrd = vsplit(vrem)[0]\n                zb = hashlib.sha512(afsenc(vrd)).digest()\n                zs = ub64enc(zb).decode(\"ascii\")[:24].lower()\n                dp = \"%s/md/%s/%s/%s\" % (dbv.histpath, zs[:2], zs[2:4], zs)\n                self.log(\"moving old version to %s/%s\" % (dp, mfile2))\n                if bos.makedirs(dp, vf=vfs.flags):\n                    with open(os.path.join(dp, \"dir.txt\"), \"wb\") as f:\n                        f.write(afsenc(vrd))\n                        if \"fperms\" in vfs.flags:\n                            set_fperms(f, vfs.flags)\n            elif hist_cfg == \"s\":\n                dp = os.path.join(mdir, \".hist\")\n                try:\n                    bos.mkdir(dp, vfs.flags[\"chmod_d\"])\n                    if \"chown\" in vfs.flags:\n                        bos.chown(dp, vfs.flags[\"uid\"], vfs.flags[\"gid\"])\n                    hidedir(dp)\n                except:\n                    pass\n            if dp:\n                atomic_move(self.log, fp, os.path.join(dp, mfile2), vfs.flags)\n\n        assert self.parser.gen  # !rm\n        p_field, _, p_data = next(self.parser.gen)\n        if p_field != \"body\":\n            raise Pebkac(400, \"expected body, got %r\" % (p_field,))\n\n        if \"txt_eol\" in vfs.flags:\n            p_data = eol_conv(p_data, vfs.flags[\"txt_eol\"])\n\n        xbu = vfs.flags.get(\"xbu\")\n        if xbu:\n            hr = runhook(\n                self.log,\n                self.conn.hsrv.broker,\n                None,\n                \"xbu.http.txt\",\n                xbu,\n                fp,\n                self.vpath,\n                self.host,\n                self.uname,\n                self.asrv.vfs.get_perms(self.vpath, self.uname),\n                time.time(),\n                0,\n                self.ip,\n                time.time(),\n                None,\n            )\n            t = hr.get(\"rejectmsg\") or \"\"\n            if t or hr.get(\"rc\") != 0:\n                if not t:\n                    t = \"save blocked by xbu server config\"\n                self.log(t, 1)\n                raise Pebkac(403, t)\n\n        if bos.path.exists(fp):\n            wunlink(self.log, fp, vfs.flags)\n\n        with open(fsenc(fp), \"wb\", self.args.iobuf) as f:\n            if \"fperms\" in vfs.flags:\n                set_fperms(f, vfs.flags)\n            sz, sha512, _ = hashcopy(p_data, f, None, 0, self.args.s_wr_slp)\n\n        if lim:\n            lim.nup(self.ip)\n            lim.bup(self.ip, sz)\n            try:\n                lim.chk_sz(sz)\n                lim.chk_vsz(self.conn.hsrv.broker, vfs.realpath, sz)\n            except:\n                wunlink(self.log, fp, vfs.flags)\n                raise\n\n        new_lastmod = bos.stat(fp).st_mtime\n        new_lastmod3 = int(new_lastmod * 1000)\n        sha512 = sha512[:56]\n\n        xau = vfs.flags.get(\"xau\")\n        if xau:\n            hr = runhook(\n                self.log,\n                self.conn.hsrv.broker,\n                None,\n                \"xau.http.txt\",\n                xau,\n                fp,\n                self.vpath,\n                self.host,\n                self.uname,\n                self.asrv.vfs.get_perms(self.vpath, self.uname),\n                new_lastmod,\n                sz,\n                self.ip,\n                new_lastmod,\n                None,\n            )\n            t = hr.get(\"rejectmsg\") or \"\"\n            if t or hr.get(\"rc\") != 0:\n                if not t:\n                    t = \"save blocked by xau server config\"\n                self.log(t, 1)\n                wunlink(self.log, fp, vfs.flags)\n                raise Pebkac(403, t)\n\n        self.conn.hsrv.broker.say(\n            \"up2k.hash_file\",\n            dbv.realpath,\n            dbv.vpath,\n            dbv.flags,\n            vsplit(vrem)[0],\n            fn,\n            self.ip,\n            new_lastmod,\n            self.uname,\n            True,\n        )\n\n        response = json.dumps(\n            {\"ok\": True, \"lastmod\": new_lastmod3, \"size\": sz, \"sha512\": sha512}\n        )\n        self.log(response)\n        self.parser.drop()\n        self.reply(response.encode(\"utf-8\"))\n        return True\n\n    def _chk_lastmod(self, file_ts: int) -> tuple[str, bool, bool]:\n        # ret: lastmod, do_send, can_range\n        file_lastmod = formatdate(file_ts)\n        c_ifrange = self.headers.get(\"if-range\")\n        c_lastmod = self.headers.get(\"if-modified-since\")\n\n        if not c_ifrange and not c_lastmod:\n            return file_lastmod, True, True\n\n        if c_ifrange and c_ifrange != file_lastmod:\n            t = \"sending entire file due to If-Range; cli(%s) file(%s)\"\n            self.log(t % (c_ifrange, file_lastmod), 6)\n            return file_lastmod, True, False\n\n        do_send = c_lastmod != file_lastmod\n        if do_send and c_lastmod:\n            t = \"sending body due to If-Modified-Since cli(%s) file(%s)\"\n            self.log(t % (c_lastmod, file_lastmod), 6)\n        elif not do_send and self.no304():\n            do_send = True\n            self.log(\"sending body due to no304\")\n\n        return file_lastmod, do_send, True\n\n    def _use_dirkey(self, vn: VFS, ap: str) -> bool:\n        if self.can_read or not self.can_get:\n            return False\n\n        if vn.flags.get(\"dky\"):\n            return True\n\n        req = self.uparam.get(\"k\") or \"\"\n        if not req:\n            return False\n\n        dk_len = vn.flags.get(\"dk\")\n        if not dk_len:\n            return False\n\n        if not ap:\n            ap = vn.canonical(self.rem)\n\n        zs = self.gen_fk(2, self.args.dk_salt, ap, 0, 0)[:dk_len]\n        if req == zs:\n            return True\n\n        t = \"wrong dirkey, want %s, got %s\\n  vp: %r\\n  ap: %r\"\n        self.log(t % (zs, req, self.req, ap), 6)\n        return False\n\n    def _use_filekey(self, vn: VFS, ap: str, st: os.stat_result) -> bool:\n        if self.can_read or not self.can_get:\n            return False\n\n        req = self.uparam.get(\"k\") or \"\"\n        if not req:\n            return False\n\n        fk_len = vn.flags.get(\"fk\")\n        if not fk_len:\n            return False\n\n        if not ap:\n            ap = self.vn.canonical(self.rem)\n\n        alg = 2 if \"fka\" in vn.flags else 1\n\n        zs = self.gen_fk(\n            alg, self.args.fk_salt, ap, st.st_size, 0 if ANYWIN else st.st_ino\n        )[:fk_len]\n\n        if req == zs:\n            return True\n\n        t = \"wrong filekey, want %s, got %s\\n  vp: %r\\n  ap: %r\"\n        self.log(t % (zs, req, self.req, ap), 6)\n        return False\n\n    def _add_logues(\n        self, vn: VFS, abspath: str, lnames: Optional[dict[str, str]]\n    ) -> tuple[list[str], list[str]]:\n        logues = [\"\", \"\"]\n        for n, fns1, fns2 in [] if self.args.no_logues else vn.flags[\"emb_lgs\"]:\n            for fn in fns1 if lnames is None else fns2:\n                if lnames is not None:\n                    fn = lnames.get(fn)\n                    if not fn:\n                        continue\n                fn = \"%s/%s\" % (abspath, fn)\n                if not bos.path.isfile(fn):\n                    continue\n                logues[n] = read_utf8(self.log, fsenc(fn), False)\n                if \"exp\" in vn.flags:\n                    logues[n] = self._expand(logues[n], vn.flags.get(\"exp_lg\") or [])\n                break\n\n        readmes = [\"\", \"\"]\n        for n, fns1, fns2 in [] if self.args.no_readme else vn.flags[\"emb_mds\"]:\n            if logues[n]:\n                continue\n            for fn in fns1 if lnames is None else fns2:\n                if lnames is not None:\n                    fn = lnames.get(fn.lower())\n                    if not fn:\n                        continue\n                fn = \"%s/%s\" % (abspath, fn)\n                if not bos.path.isfile(fn):\n                    continue\n                readmes[n] = read_utf8(self.log, fsenc(fn), False)\n                if \"exp\" in vn.flags:\n                    readmes[n] = self._expand(readmes[n], vn.flags.get(\"exp_md\") or [])\n                break\n\n        return logues, readmes\n\n    def _expand(self, txt: str, phs: list[str]) -> str:\n        ptn_hsafe = RE_HSAFE\n        for ph in phs:\n            if ph.startswith(\"hdr.\"):\n                sv = str(self.headers.get(ph[4:], \"\"))\n            elif ph.startswith(\"self.\"):\n                sv = str(getattr(self, ph[5:], \"\"))\n            elif ph.startswith(\"cfg.\"):\n                sv = str(getattr(self.args, ph[4:], \"\"))\n            elif ph.startswith(\"vf.\"):\n                sv = str(self.vn.flags.get(ph[3:]) or \"\")\n            elif ph == \"srv.itime\":\n                sv = str(int(time.time()))\n            elif ph == \"srv.htime\":\n                sv = datetime.now(UTC).strftime(\"%Y-%m-%d, %H:%M:%S\")\n            else:\n                self.log(\"unknown placeholder in server config: [%s]\" % (ph,), 3)\n                continue\n\n            sv = ptn_hsafe.sub(\"_\", sv)\n            txt = txt.replace(\"{{%s}}\" % (ph,), sv)\n\n        return txt\n\n    def _can_tail(self, volflags: dict[str, Any]) -> bool:\n        zp = self.args.ua_nodoc\n        if zp and zp.search(self.ua):\n            t = \"this URL contains no valuable information for bots/crawlers\"\n            raise Pebkac(403, t)\n        lvl = volflags[\"tail_who\"]\n        if \"notail\" in volflags or not lvl:\n            raise Pebkac(400, \"tail is disabled in server config\")\n        elif lvl <= 1 and not self.can_admin:\n            raise Pebkac(400, \"tail is admin-only on this server\")\n        elif lvl <= 2 and self.uname in (\"\", \"*\"):\n            raise Pebkac(400, \"you must be authenticated to use ?tail on this server\")\n        return True\n\n    def _can_zip(self, volflags: dict[str, Any]) -> str:\n        lvl = volflags[\"zip_who\"]\n        if self.args.no_zip or not lvl:\n            return \"download-as-zip/tar is disabled in server config\"\n        elif lvl <= 1 and not self.can_admin:\n            return \"download-as-zip/tar is admin-only on this server\"\n        elif lvl <= 2 and self.uname in (\"\", \"*\"):\n            return \"you must be authenticated to download-as-zip/tar on this server\"\n        elif self.args.ua_nozip and self.args.ua_nozip.search(self.ua):\n            t = \"this URL contains no valuable information for bots/crawlers\"\n            raise Pebkac(403, t)\n        return \"\"\n\n    def tx_res(self, req_path: str) -> bool:\n        status = 200\n        logmsg = \"{:4} {} \".format(\"\", self.req)\n        logtail = \"\"\n\n        editions = {}\n        file_ts = 0\n\n        if has_resource(self.E, req_path):\n            st = stat_resource(self.E, req_path)\n            if st:\n                file_ts = max(file_ts, st.st_mtime)\n            editions[\"plain\"] = req_path\n\n        if has_resource(self.E, req_path + \".gz\"):\n            st = stat_resource(self.E, req_path + \".gz\")\n            if st:\n                file_ts = max(file_ts, st.st_mtime)\n            if not st or st.st_mtime > file_ts:\n                editions[\".gz\"] = req_path + \".gz\"\n\n        if not editions:\n            return self.tx_404()\n\n        #\n        # force download\n\n        if \"dl\" in self.ouparam:\n            cdis = self.ouparam[\"dl\"] or req_path\n            zs = gen_content_disposition(os.path.basename(cdis))\n            self.out_headers[\"Content-Disposition\"] = zs\n        else:\n            cdis = req_path\n\n        #\n        # if-modified\n\n        if file_ts > 0:\n            file_lastmod, do_send, _ = self._chk_lastmod(int(file_ts))\n            self.out_headers[\"Last-Modified\"] = file_lastmod\n            if not do_send:\n                status = 304\n\n            if self.can_write:\n                self.out_headers[\"X-Lastmod3\"] = str(int(file_ts * 1000))\n        else:\n            do_send = True\n\n        #\n        # Accept-Encoding and UA decides which edition to send\n\n        decompress = False\n        supported_editions = [\n            x.strip()\n            for x in self.headers.get(\"accept-encoding\", \"\").lower().split(\",\")\n        ]\n        if \".gz\" in editions:\n            is_compressed = True\n            selected_edition = \".gz\"\n\n            if \"gzip\" not in supported_editions:\n                decompress = True\n            else:\n                if re.match(r\"MSIE [4-6]\\.\", self.ua) and \" SV1\" not in self.ua:\n                    decompress = True\n\n            if not decompress:\n                self.out_headers[\"Content-Encoding\"] = \"gzip\"\n        else:\n            is_compressed = False\n            selected_edition = \"plain\"\n\n        res_path = editions[selected_edition]\n        logmsg += \"{} \".format(selected_edition.lstrip(\".\"))\n\n        res = load_resource(self.E, res_path)\n\n        if decompress:\n            file_sz = gzip_file_orig_sz(res)\n            res = gzip.open(res)\n        else:\n            res.seek(0, os.SEEK_END)\n            file_sz = res.tell()\n            res.seek(0, os.SEEK_SET)\n\n        #\n        # send reply\n\n        if is_compressed:\n            self.out_headers[\"Cache-Control\"] = \"max-age=604869\"\n        else:\n            self.permit_caching()\n\n        if \"txt\" in self.uparam:\n            mime = \"text/plain; charset={}\".format(self.uparam[\"txt\"] or \"utf-8\")\n        elif \"mime\" in self.uparam:\n            mime = str(self.uparam.get(\"mime\"))\n        else:\n            mime = guess_mime(cdis)\n\n        logmsg += unicode(status) + logtail\n\n        if self.mode == \"HEAD\" or not do_send:\n            res.close()\n            if self.do_log:\n                self.log(logmsg)\n\n            self.send_headers(\"oh_g\", length=file_sz, status=status, mime=mime)\n            return True\n\n        ret = True\n        self.send_headers(\"oh_g\", length=file_sz, status=status, mime=mime)\n        remains = sendfile_py(\n            self.log,\n            0,\n            file_sz,\n            res,\n            self.s,\n            self.args.s_wr_sz,\n            self.args.s_wr_slp,\n            not self.args.no_poll,\n            {},\n            \"\",\n        )\n        res.close()\n\n        if remains > 0:\n            logmsg += \" \\033[31m\" + unicode(file_sz - remains) + \"\\033[0m\"\n            ret = False\n\n        spd = self._spd(file_sz - remains)\n        if self.do_log:\n            self.log(\"{},  {}\".format(logmsg, spd))\n\n        return ret\n\n    def tx_file(self, oh_k: str, req_path: str, ptop: Optional[str] = None) -> bool:\n        status = 200\n        logmsg = \"{:4} {} \".format(\"\", self.req)\n        logtail = \"\"\n\n        is_tail = \"tail\" in self.uparam and self._can_tail(self.vn.flags)\n\n        if ptop is not None:\n            ap_data = \"<%s>\" % (req_path,)\n            try:\n                dp, fn = os.path.split(req_path)\n                tnam = fn + \".PARTIAL\"\n                if self.args.dotpart:\n                    tnam = \".\" + tnam\n                ap_data = os.path.join(dp, tnam)\n                st_data = bos.stat(ap_data)\n                if not st_data.st_size:\n                    raise Exception(\"partial is empty\")\n                x = self.conn.hsrv.broker.ask(\"up2k.find_job_by_ap\", ptop, req_path)\n                job = json.loads(x.get())\n                if not job:\n                    raise Exception(\"not found in registry\")\n                self.pipes.set(req_path, job)\n            except Exception as ex:\n                if getattr(ex, \"errno\", 0) != errno.ENOENT:\n                    self.log(\"will not pipe %r; %s\" % (ap_data, ex), 6)\n                ptop = None\n\n        #\n        # if request is for foo.js, check if we have foo.js.gz\n\n        file_ts = 0.0\n        editions: dict[str, tuple[str, int]] = {}\n        for ext in (\"\", \".gz\"):\n            if ptop is not None:\n                assert job and ap_data  # type: ignore  # !rm\n                sz = job[\"size\"]\n                file_ts = max(0, job[\"lmod\"])\n                editions[\"plain\"] = (ap_data, sz)\n                break\n\n            try:\n                fs_path = req_path + ext\n                st = bos.stat(fs_path)\n                if stat.S_ISDIR(st.st_mode):\n                    continue\n\n                sz = st.st_size\n                if stat.S_ISBLK(st.st_mode):\n                    fd = bos.open(fs_path, os.O_RDONLY)\n                    try:\n                        sz = os.lseek(fd, 0, os.SEEK_END)\n                    finally:\n                        os.close(fd)\n\n                file_ts = max(file_ts, st.st_mtime)\n                editions[ext or \"plain\"] = (fs_path, sz)\n            except:\n                pass\n            if not self.vpath.startswith(\".cpr/\"):\n                break\n\n        if not editions:\n            return self.tx_404()\n\n        #\n        # force download\n\n        if \"dl\" in self.ouparam:\n            cdis = self.ouparam[\"dl\"] or req_path\n            zs = gen_content_disposition(os.path.basename(cdis))\n            self.out_headers[\"Content-Disposition\"] = zs\n        else:\n            cdis = req_path\n\n        #\n        # if-modified\n\n        file_lastmod, do_send, can_range = self._chk_lastmod(int(file_ts))\n        self.out_headers[\"Last-Modified\"] = file_lastmod\n        if not do_send:\n            status = 304\n\n        if self.can_write:\n            self.out_headers[\"X-Lastmod3\"] = str(int(file_ts * 1000))\n\n        #\n        # Accept-Encoding and UA decides which edition to send\n\n        decompress = False\n        supported_editions = [\n            x.strip()\n            for x in self.headers.get(\"accept-encoding\", \"\").lower().split(\",\")\n        ]\n        if \".gz\" in editions:\n            is_compressed = True\n            selected_edition = \".gz\"\n            fs_path, file_sz = editions[\".gz\"]\n            if \"gzip\" not in supported_editions:\n                decompress = True\n            else:\n                if re.match(r\"MSIE [4-6]\\.\", self.ua) and \" SV1\" not in self.ua:\n                    decompress = True\n\n            if not decompress:\n                self.out_headers[\"Content-Encoding\"] = \"gzip\"\n        else:\n            is_compressed = False\n            selected_edition = \"plain\"\n\n        fs_path, file_sz = editions[selected_edition]\n        logmsg += \"{} \".format(selected_edition.lstrip(\".\"))\n\n        #\n        # partial\n\n        lower = 0\n        upper = file_sz\n        hrange = self.headers.get(\"range\")\n\n        # let's not support 206 with compression\n        # and multirange / multipart is also not-impl (mostly because calculating contentlength is a pain)\n        if (\n            do_send\n            and not is_compressed\n            and hrange\n            and can_range\n            and file_sz\n            and \",\" not in hrange\n            and not is_tail\n        ):\n            try:\n                if not hrange.lower().startswith(\"bytes\"):\n                    raise Exception()\n\n                a, b = hrange.split(\"=\", 1)[1].split(\"-\")\n\n                if a.strip():\n                    lower = int(a.strip())\n                else:\n                    lower = 0\n\n                if b.strip():\n                    upper = int(b.strip()) + 1\n                else:\n                    upper = file_sz\n\n                if upper > file_sz:\n                    upper = file_sz\n\n                if lower < 0 or lower >= upper:\n                    raise Exception()\n\n            except:\n                err = \"invalid range ({}), size={}\".format(hrange, file_sz)\n                self.loud_reply(\n                    err,\n                    status=416,\n                    headers={\"Content-Range\": \"bytes */{}\".format(file_sz)},\n                )\n                return True\n\n            status = 206\n            self.out_headers[\"Content-Range\"] = \"bytes {}-{}/{}\".format(\n                lower, upper - 1, file_sz\n            )\n\n            logtail += \" [\\033[36m{}-{}\\033[0m]\".format(lower, upper)\n\n        use_sendfile = False\n        if decompress:\n            open_func: Any = gzip.open\n            open_args: list[Any] = [fsenc(fs_path), \"rb\"]\n            # Content-Length := original file size\n            upper = gzip_orig_sz(fs_path)\n        else:\n            open_func = open\n            open_args = [fsenc(fs_path), \"rb\", self.args.iobuf]\n            use_sendfile = (\n                # fmt: off\n                not self.tls\n                and not self.args.no_sendfile\n                and (BITNESS > 32 or file_sz < 0x7fffFFFF)\n                # fmt: on\n            )\n\n        #\n        # send reply\n\n        if is_compressed:\n            self.out_headers[\"Cache-Control\"] = \"max-age=604869\"\n        else:\n            self.permit_caching()\n\n        if \"txt\" in self.uparam:\n            mime = \"text/plain; charset={}\".format(self.uparam[\"txt\"] or \"utf-8\")\n        elif \"mime\" in self.uparam:\n            mime = str(self.uparam.get(\"mime\"))\n        elif \"rmagic\" in self.vn.flags:\n            mime = guess_mime(req_path, fs_path)\n        else:\n            mime = guess_mime(cdis)\n\n        if mime not in SAFE_MIMES and \"nohtml\" in self.vn.flags and oh_k != \"oh_g\":\n            mime = safe_mime(mime)\n\n        self.out_headers[\"Accept-Ranges\"] = \"bytes\"\n        logmsg += unicode(status) + logtail\n\n        if self.mode == \"HEAD\" or not do_send:\n            if self.do_log:\n                self.log(logmsg)\n\n            self.send_headers(oh_k, length=upper - lower, status=status, mime=mime)\n            return True\n\n        dls = self.conn.hsrv.dls\n        if is_tail:\n            upper = 1 << 30\n            if len(dls) > self.args.tail_cmax:\n                raise Pebkac(400, \"too many active downloads to start a new tail\")\n\n        if upper - lower > 0x400000:  # 4m\n            now = time.time()\n            self.dl_id = \"%s:%s\" % (self.ip, self.addr[1])\n            dls[self.dl_id] = (now, 0)\n            self.conn.hsrv.dli[self.dl_id] = (\n                now,\n                0 if is_tail else upper - lower,\n                self.vn,\n                self.vpath,\n                self.uname,\n            )\n\n        if ptop is not None:\n            assert job and ap_data  # type: ignore  # !rm\n            return self.tx_pipe(\n                ptop, req_path, ap_data, job, lower, upper, status, mime, logmsg\n            )\n        elif is_tail:\n            self.tx_tail(open_args, status, mime)\n            return False\n\n        ret = True\n        with open_func(*open_args) as f:\n            self.send_headers(oh_k, length=upper - lower, status=status, mime=mime)\n\n            sendfun = sendfile_kern if use_sendfile else sendfile_py\n            remains = sendfun(\n                self.log,\n                lower,\n                upper,\n                f,\n                self.s,\n                self.args.s_wr_sz,\n                self.args.s_wr_slp,\n                not self.args.no_poll,\n                dls,\n                self.dl_id,\n            )\n\n        if remains > 0:\n            logmsg += \" \\033[31m\" + unicode(upper - remains) + \"\\033[0m\"\n            ret = False\n\n        spd = self._spd((upper - lower) - remains)\n        if self.do_log:\n            self.log(\"{},  {}\".format(logmsg, spd))\n\n        return ret\n\n    def tx_tail(\n        self,\n        open_args: list[Any],\n        status: int,\n        mime: str,\n    ) -> None:\n        vf = self.vn.flags\n        self.send_headers(\"oh_f\", length=None, status=status, mime=mime)\n        abspath: bytes = open_args[0]\n        sec_rate = vf[\"tail_rate\"]\n        sec_max = vf[\"tail_tmax\"]\n        sec_fd = vf[\"tail_fd\"]\n        sec_ka = self.args.tail_ka\n        wr_slp = self.args.s_wr_slp\n        wr_sz = self.args.s_wr_sz\n        dls = self.conn.hsrv.dls\n        dl_id = self.dl_id\n\n        # non-numeric = full file from start\n        # positive = absolute offset from start\n        # negative = start that many bytes from eof\n        try:\n            ofs = int(self.uparam[\"tail\"])\n        except:\n            ofs = 0\n\n        t0 = time.time()\n        ofs0 = ofs\n        f = None\n        try:\n            st = os.stat(abspath)\n            f = open_nolock(*open_args)\n            f.seek(0, os.SEEK_END)\n            eof = f.tell()\n            f.seek(0)\n            if ofs < 0:\n                ofs = max(0, ofs + eof)\n\n            self.log(\"tailing from byte %d: %r\" % (ofs, abspath), 6)\n\n            # send initial data asap\n            remains = sendfile_py(\n                self.log,  # d/c\n                ofs,\n                eof,\n                f,\n                self.s,\n                wr_sz,\n                wr_slp,\n                False,  # d/c\n                dls,\n                dl_id,\n            )\n            sent = (eof - ofs) - remains\n            ofs = eof - remains\n            f.seek(ofs)\n\n            try:\n                st2 = os.stat(open_args[0])\n                if st.st_ino == st2.st_ino:\n                    st = st2  # for filesize\n            except:\n                pass\n\n            gone = 0\n            unsent = False\n            t_fd = t_ka = time.time()\n            while True:\n                assert f  # !rm\n                buf = f.read(4096)\n                now = time.time()\n\n                if sec_max and now - t0 >= sec_max:\n                    self.log(\"max duration exceeded; kicking client\", 6)\n                    zb = b\"\\n\\n*** max duration exceeded; disconnecting ***\\n\"\n                    self.s.sendall(zb)\n                    break\n\n                if buf:\n                    t_fd = t_ka = now\n                    self.s.sendall(buf)\n                    sent += len(buf)\n                    unsent = False\n                    dls[dl_id] = (time.time(), sent)\n                    continue\n\n                time.sleep(sec_rate)\n                if t_ka < now - sec_ka:\n                    t_ka = now\n                    self.s.send(b\"\\x00\")\n                if t_fd < now - sec_fd:\n                    try:\n                        st2 = os.stat(open_args[0])\n                        szd = st2.st_size - st.st_size\n                        if (\n                            st2.st_ino != st.st_ino\n                            or st2.st_size < sent\n                            or szd < 0\n                            or unsent\n                        ):\n                            assert f  # !rm\n                            # open new file before closing previous to avoid toctous (open may fail; cannot null f before)\n                            f2 = open_nolock(*open_args)\n                            f.close()\n                            f = f2\n                            f.seek(0, os.SEEK_END)\n                            eof = f.tell()\n                            if eof < sent:\n                                ofs = sent = 0  # shrunk; send from start\n                                zb = b\"\\n\\n*** file size decreased -- rewinding to the start of the file ***\\n\\n\"\n                                self.s.sendall(zb)\n                                if ofs0 < 0 and eof > -ofs0:\n                                    ofs = eof + ofs0\n                            else:\n                                ofs = sent  # just new fd? resume from same ofs\n                            f.seek(ofs)\n                            self.log(\"reopened at byte %d: %r\" % (ofs, abspath), 6)\n                            unsent = False\n                            gone = 0\n                        elif szd:\n                            unsent = True\n                        st = st2\n                    except:\n                        gone += 1\n                        if gone > 3:\n                            self.log(\"file deleted; disconnecting\")\n                            break\n        except IOError as ex:\n            if ex.errno not in E_SCK_WR:\n                raise\n        finally:\n            if f:\n                f.close()\n\n    def tx_pipe(\n        self,\n        ptop: str,\n        req_path: str,\n        ap_data: str,\n        job: dict[str, Any],\n        lower: int,\n        upper: int,\n        status: int,\n        mime: str,\n        logmsg: str,\n    ) -> bool:\n        M = 1048576\n        self.send_headers(\"oh_f\", length=upper - lower, status=status, mime=mime)\n        wr_slp = self.args.s_wr_slp\n        wr_sz = self.args.s_wr_sz\n        file_size = job[\"size\"]\n        chunk_size = up2k_chunksize(file_size)\n        num_need = -1\n        data_end = 0\n        remains = upper - lower\n        broken = False\n        spins = 0\n        tier = 0\n        tiers = [\"uncapped\", \"reduced speed\", \"one byte per sec\"]\n\n        while lower < upper and not broken:\n            with self.u2mutex:\n                job = self.pipes.get(req_path)\n                if not job:\n                    x = self.conn.hsrv.broker.ask(\"up2k.find_job_by_ap\", ptop, req_path)\n                    job = json.loads(x.get())\n                    if job:\n                        self.pipes.set(req_path, job)\n\n            if not job:\n                t = \"pipe: OK, upload has finished; yeeting remainder\"\n                self.log(t, 2)\n                data_end = file_size\n                break\n\n            if num_need != len(job[\"need\"]) and data_end - lower < 8 * M:\n                num_need = len(job[\"need\"])\n                data_end = 0\n                for cid in job[\"hash\"]:\n                    if cid in job[\"need\"]:\n                        break\n                    data_end += chunk_size\n                t = \"pipe: can stream %.2f MiB; requested range is %.2f to %.2f\"\n                self.log(t % (data_end / M, lower / M, upper / M), 6)\n                with self.u2mutex:\n                    if data_end > self.u2fh.aps.get(ap_data, data_end):\n                        fhs: Optional[set[typing.BinaryIO]] = None\n                        try:\n                            fhs = self.u2fh.cache[ap_data].all_fhs\n                            for fh in fhs:\n                                fh.flush()\n                            self.u2fh.aps[ap_data] = data_end\n                            self.log(\"pipe: flushed %d up2k-FDs\" % (len(fhs),))\n                        except Exception as ex:\n                            if fhs is None:\n                                err = \"file is not being written to right now\"\n                            else:\n                                err = repr(ex)\n                            self.log(\"pipe: u2fh flush failed: \" + err)\n\n            if lower >= data_end:\n                if data_end:\n                    t = \"pipe: uploader is too slow; aborting download at %.2f MiB\"\n                    self.log(t % (data_end / M,))\n                    raise Pebkac(416, \"uploader is too slow\")\n\n                raise Pebkac(416, \"no data available yet; please retry in a bit\")\n\n            slack = data_end - lower\n            if slack >= 8 * M:\n                ntier = 0\n                winsz = M\n                bufsz = wr_sz\n                slp = wr_slp\n            else:\n                winsz = max(40, int(M * (slack / (12 * M))))\n                base_rate = M if not wr_slp else wr_sz / wr_slp\n                if winsz > base_rate:\n                    ntier = 0\n                    bufsz = wr_sz\n                    slp = wr_slp\n                elif winsz > 300:\n                    ntier = 1\n                    bufsz = winsz // 5\n                    slp = 0.2\n                else:\n                    ntier = 2\n                    bufsz = winsz = slp = 1\n\n            if tier != ntier:\n                tier = ntier\n                self.log(\"moved to tier %d (%s)\" % (tier, tiers[tier]))\n\n            try:\n                with open_nolock(ap_data, \"rb\", self.args.iobuf) as f:\n                    f.seek(lower)\n                    page = f.read(min(winsz, data_end - lower, upper - lower))\n                if not page:\n                    raise Exception(\"got 0 bytes (EOF?)\")\n            except Exception as ex:\n                self.log(\"pipe: read failed at %.2f MiB: %s\" % (lower / M, ex), 3)\n                with self.u2mutex:\n                    self.pipes.c.pop(req_path, None)\n                spins += 1\n                if spins > 3:\n                    raise Pebkac(500, \"file became unreadable\")\n                time.sleep(2)\n                continue\n\n            spins = 0\n            pofs = 0\n            while pofs < len(page):\n                if slp:\n                    time.sleep(slp)\n\n                try:\n                    buf = page[pofs : pofs + bufsz]\n                    self.s.sendall(buf)\n                    zi = len(buf)\n                    remains -= zi\n                    lower += zi\n                    pofs += zi\n                except:\n                    broken = True\n                    break\n\n        if lower < upper and not broken:\n            with open_nolock(req_path, \"rb\") as f:\n                remains = sendfile_py(\n                    self.log,\n                    lower,\n                    upper,\n                    f,\n                    self.s,\n                    wr_sz,\n                    wr_slp,\n                    not self.args.no_poll,\n                    self.conn.hsrv.dls,\n                    self.dl_id,\n                )\n\n        spd = self._spd((upper - lower) - remains)\n        if self.do_log:\n            self.log(\"{},  {}\".format(logmsg, spd))\n\n        return not broken\n\n    def tx_zip(\n        self,\n        fmt: str,\n        uarg: str,\n        vpath: str,\n        vn: VFS,\n        rem: str,\n        items: list[str],\n    ) -> bool:\n        t = self._can_zip(vn.flags)\n        if t:\n            raise Pebkac(400, t)\n\n        logmsg = \"{:4} {} \".format(\"\", self.req)\n        self.keepalive = False\n\n        cancmp = not self.args.no_tarcmp\n\n        if fmt == \"tar\":\n            packer: Type[StreamArc] = StreamTar\n            if cancmp and \"gz\" in uarg:\n                mime = \"application/gzip\"\n                ext = \"tar.gz\"\n            elif cancmp and \"bz2\" in uarg:\n                mime = \"application/x-bzip\"\n                ext = \"tar.bz2\"\n            elif cancmp and \"xz\" in uarg:\n                mime = \"application/x-xz\"\n                ext = \"tar.xz\"\n            else:\n                mime = \"application/x-tar\"\n                ext = \"tar\"\n        else:\n            mime = \"application/zip\"\n            packer = StreamZip\n            ext = \"zip\"\n\n        dots = 0 if \"nodot\" in self.uparam else 1\n        scandir = not self.args.no_scandir\n\n        fn = self.vpath.split(\"/\")[-1] or self.host.split(\":\")[0]\n        if items:\n            fn = \"sel-\" + fn\n\n        if vn.flags.get(\"zipmax\") and not (\n            vn.flags.get(\"zipmaxu\") and self.uname != \"*\"\n        ):\n            maxs = vn.flags.get(\"zipmaxs_v\") or 0\n            maxn = vn.flags.get(\"zipmaxn_v\") or 0\n            nf = 0\n            nb = 0\n            fgen = vn.zipgen(vpath, rem, set(items), self.uname, False, dots, scandir)\n            t = \"total size exceeds a limit specified in server config\"\n            t = vn.flags.get(\"zipmaxt\") or t\n            if maxs and maxn:\n                for zd in fgen:\n                    nf += 1\n                    nb += zd[\"st\"].st_size\n                    if maxs < nb or maxn < nf:\n                        raise Pebkac(400, t)\n            elif maxs:\n                for zd in fgen:\n                    nb += zd[\"st\"].st_size\n                    if maxs < nb:\n                        raise Pebkac(400, t)\n            elif maxn:\n                for zd in fgen:\n                    nf += 1\n                    if maxn < nf:\n                        raise Pebkac(400, t)\n\n        cdis = gen_content_disposition(\"%s.%s\" % (fn, ext))\n        self.log(repr(cdis))\n        self.send_headers(\n            \"oh_f\", None, mime=mime, headers={\"Content-Disposition\": cdis}\n        )\n\n        fgen = vn.zipgen(vpath, rem, set(items), self.uname, False, dots, scandir)\n        # for f in fgen: print(repr({k: f[k] for k in [\"vp\", \"ap\"]}))\n        cfmt = \"\"\n        if self.thumbcli and not self.args.no_bacode:\n            if uarg in ZIP_XCODE_S:\n                cfmt = uarg\n            else:\n                for zs in ZIP_XCODE_L:\n                    if zs in self.ouparam:\n                        cfmt = zs\n\n            if cfmt:\n                self.log(\"transcoding to [{}]\".format(cfmt))\n                fgen = gfilter(fgen, self.thumbcli, self.uname, vpath, cfmt)\n\n        now = time.time()\n        self.dl_id = \"%s:%s\" % (self.ip, self.addr[1])\n        self.conn.hsrv.dli[self.dl_id] = (\n            now,\n            0,\n            self.vn,\n            \"%s :%s\" % (self.vpath, ext),\n            self.uname,\n        )\n        dls = self.conn.hsrv.dls\n        dls[self.dl_id] = (time.time(), 0)\n\n        bgen = packer(\n            self.log,\n            self.asrv,\n            fgen,\n            utf8=\"utf\" in uarg or not uarg,\n            pre_crc=\"crc\" in uarg,\n            cmp=uarg if cancmp or uarg == \"pax\" else \"\",\n        )\n        n = 0\n        bsent = 0\n        for buf in bgen.gen():\n            if not buf:\n                break\n\n            try:\n                self.s.sendall(buf)\n                bsent += len(buf)\n            except:\n                logmsg += \" \\033[31m\" + unicode(bsent) + \"\\033[0m\"\n                bgen.stop()\n                break\n\n            n += 1\n            if n >= 4:\n                n = 0\n                dls[self.dl_id] = (time.time(), bsent)\n\n        spd = self._spd(bsent)\n        self.log(\"{},  {}\".format(logmsg, spd))\n        return True\n\n    def tx_ico(self, ext: str, exact: bool = False) -> bool:\n        self.permit_caching()\n        if ext.endswith(\"/\"):\n            ext = \"folder\"\n            exact = True\n\n        bad = re.compile(r\"[](){}/ []|^[0-9_-]*$\")\n        n = ext.split(\".\")[::-1]\n        if not exact:\n            n = n[:-1]\n\n        ext = \"\"\n        for v in n:\n            if len(v) > 7 or bad.search(v):\n                break\n\n            ext = \"{}.{}\".format(v, ext)\n\n        ext = ext.rstrip(\".\") or \"unk\"\n        if len(ext) > 11:\n            ext = \"~\" + ext[-9:]\n\n        return self.tx_svg(ext, exact)\n\n    def tx_svg(self, txt: str, small: bool = False) -> bool:\n        # chrome cannot handle more than ~2000 unique SVGs\n        # so url-param \"raster\" returns a png/webp instead\n        # (useragent-sniffing kinshi due to caching proxies)\n        mime, ico = self.ico.get(txt, not small, \"raster\" in self.uparam)\n\n        lm = formatdate(self.E.t0)\n        self.reply(ico, mime=mime, headers={\"Last-Modified\": lm})\n        return True\n\n    def tx_qr(self):\n        url = \"%s://%s%s%s\" % (\n            \"https\" if self.is_https else \"http\",\n            self.host,\n            self.args.SRS,\n            self.vpaths,\n        )\n        uhash = \"\"\n        uparams = []\n        if self.ouparam:\n            for k, v in self.ouparam.items():\n                if k == \"qr\":\n                    continue\n                if k == \"uhash\":\n                    uhash = v\n                    continue\n                uparams.append(k if v == \"\" else \"%s=%s\" % (k, v))\n        if uparams:\n            url += \"?\" + \"&\".join(uparams)\n        if uhash:\n            url += \"#\" + uhash\n\n        self.log(\"qrcode(%r)\" % (url,))\n        ret = qr2svg(qrgen(url.encode(\"utf-8\")), 2)\n        self.reply(ret.encode(\"utf-8\"), mime=\"image/svg+xml\")\n        return True\n\n    def tx_md(self, vn: VFS, fs_path: str) -> bool:\n        logmsg = \"     %s @%s \" % (self.req, self.uname)\n\n        if not self.can_write:\n            if \"edit\" in self.uparam or \"edit2\" in self.uparam:\n                return self.tx_404(True)\n\n        tpl = \"mde\" if \"edit2\" in self.uparam else \"md\"\n        template = self.j2j(tpl)\n\n        st = bos.stat(fs_path)\n        ts_md = st.st_mtime\n\n        max_sz = 1024 * self.args.txt_max\n        sz_md = 0\n        lead = b\"\"\n        fullfile = b\"\"\n        for buf in yieldfile(fs_path, self.args.iobuf):\n            if sz_md < max_sz:\n                fullfile += buf\n            else:\n                fullfile = b\"\"\n\n            if not sz_md and buf.startswith((b\"\\n\", b\"\\r\\n\")):\n                lead = b\"\\n\" if buf.startswith(b\"\\n\") else b\"\\r\\n\"\n                sz_md += len(lead)\n\n            sz_md += len(buf)\n            for c, v in [(b\"&\", 4), (b\"<\", 3), (b\">\", 3)]:\n                sz_md += (len(buf) - len(buf.replace(c, b\"\"))) * v\n\n        if (\n            fullfile\n            and \"exp\" in vn.flags\n            and \"edit\" not in self.uparam\n            and \"edit2\" not in self.uparam\n            and vn.flags.get(\"exp_md\")\n        ):\n            fulltxt = fullfile.decode(\"utf-8\", \"replace\")\n            fulltxt = self._expand(fulltxt, vn.flags.get(\"exp_md\") or [])\n            fullfile = fulltxt.encode(\"utf-8\", \"replace\")\n\n        if fullfile:\n            fullfile = html_bescape(fullfile)\n            sz_md = len(lead) + len(fullfile)\n\n        file_ts = int(max(ts_md, self.E.t0))\n        file_lastmod, do_send, _ = self._chk_lastmod(file_ts)\n        self.out_headers[\"Last-Modified\"] = file_lastmod\n        self.out_headers[\"Cache-Control\"] = \"no-cache\"\n        status = 200 if do_send else 304\n\n        arg_base = \"?\"\n        if \"k\" in self.uparam:\n            arg_base = \"?k={}&\".format(self.uparam[\"k\"])\n\n        boundary = \"\\roll\\tide\"\n        targs = {\n            \"r\": self.args.SR if self.is_vproxied else \"\",\n            \"ts\": self.conn.hsrv.cachebuster(),\n            \"edit\": \"edit\" in self.uparam,\n            \"title\": html_escape(self.vpath, crlf=True),\n            \"lastmod\": int(ts_md * 1000),\n            \"lang\": self.cookies.get(\"cplng\") or self.args.lang,\n            \"favico\": self.args.favico,\n            \"have_emp\": int(self.args.emp),\n            \"md_no_br\": int(vn.flags.get(\"md_no_br\") or 0),\n            \"md_chk_rate\": self.args.mcr,\n            \"md\": boundary,\n            \"arg_base\": arg_base,\n        }\n\n        if self.args.js_other and \"js\" not in targs:\n            zs = self.args.js_other\n            zs += \"&\" if \"?\" in zs else \"?\"\n            targs[\"js\"] = zs\n\n        if \"html_head_d\" in self.vn.flags:\n            targs[\"this\"] = self\n            self._build_html_head(targs)\n\n        targs[\"html_head\"] = self.html_head\n        zs = template.render(**targs).encode(\"utf-8\", \"replace\")\n        html = zs.split(boundary.encode(\"utf-8\"))\n        if len(html) != 2:\n            raise Exception(\"boundary appears in \" + tpl)\n\n        self.send_headers(\"oh_g\", sz_md + len(html[0]) + len(html[1]), status)\n\n        logmsg += unicode(status)\n        if self.mode == \"HEAD\" or not do_send:\n            if self.do_log:\n                self.log(logmsg)\n\n            return True\n\n        try:\n            self.s.sendall(html[0] + lead)\n            if fullfile:\n                self.s.sendall(fullfile)\n            else:\n                for buf in yieldfile(fs_path, self.args.iobuf):\n                    self.s.sendall(html_bescape(buf))\n\n            self.s.sendall(html[1])\n\n        except:\n            self.log(logmsg + \" \\033[31md/c\\033[0m\")\n            return False\n\n        if self.do_log:\n            self.log(logmsg + \" \" + unicode(len(html)))\n\n        return True\n\n    def tx_svcs(self) -> bool:\n        aname = re.sub(\"[^0-9a-zA-Z]+\", \"\", self.args.vname) or \"a\"\n        ep = self.host\n        sep = \"]:\" if \"]\" in ep else \":\"\n        if sep in ep:\n            host, hport = ep.rsplit(\":\", 1)\n            hport = \":\" + hport\n        else:\n            host = ep\n            hport = \"\"\n\n        if host.endswith(\".local\") and self.args.zm and not self.args.rclone_mdns:\n            rip = self.conn.hsrv.nm.map(self.ip) or host\n            if \":\" in rip and \"[\" not in rip:\n                rip = \"[%s]\" % (rip,)\n        else:\n            rip = host\n\n        defpw = \"dave:hunter2\" if self.args.usernames else \"hunter2\"\n\n        vp = (self.uparam[\"hc\"] or \"\").lstrip(\"/\")\n        pw = self.ouparam.get(self.args.pw_urlp) or defpw\n        if pw in self.asrv.sesa:\n            pw = defpw\n\n        unpw = pw\n        try:\n            un, pw = unpw.split(\":\")\n        except:\n            un = \"\"\n            if self.args.usernames:\n                un = \"dave\"\n\n        html = self.j2s(\n            \"svcs\",\n            args=self.args,\n            accs=bool(self.asrv.acct),\n            s=\"s\" if self.is_https else \"\",\n            rip=html_sh_esc(rip),\n            ep=html_sh_esc(ep),\n            vp=html_sh_esc(vp),\n            rvp=html_sh_esc(vjoin(self.args.R, vp)),\n            host=html_sh_esc(host),\n            hport=html_sh_esc(hport),\n            aname=aname,\n            b_un=(\"<b>%s</b>\" % (html_sh_esc(un),)) if un else \"k\",\n            un=html_sh_esc(un),\n            pw=html_sh_esc(pw),\n            unpw=html_sh_esc(unpw),\n        )\n        self.reply(html.encode(\"utf-8\"))\n        return True\n\n    def tx_mounts(self) -> bool:\n        suf = self.urlq({}, [\"h\"])\n        rvol, wvol, avol = [\n            [(\"/\" + x).rstrip(\"/\") + \"/\" for x in y]\n            for y in [self.rvol, self.wvol, self.avol]\n        ]\n        for zs in self.asrv.vfs.all_fvols:\n            if not zs:\n                continue  # webroot\n            zs2 = (\"/\" + zs).rstrip(\"/\") + \"/\"\n            for zsl in (rvol, wvol, avol):\n                if zs2 in zsl:\n                    zsl[zsl.index(zs2)] = zs2[:-1]\n\n        ups = []\n        now = time.time()\n        get_vst = self.avol and not self.args.no_rescan\n        get_ups = self.rvol and not self.args.no_up_list and self.uname or \"\"\n        if get_vst or get_ups:\n            x = self.conn.hsrv.broker.ask(\"up2k.get_state\", get_vst, get_ups)\n            vs = json.loads(x.get())\n            vstate = {(\"/\" + k).rstrip(\"/\") + \"/\": v for k, v in vs[\"volstate\"].items()}\n            try:\n                for rem, sz, t0, poke, vp in vs[\"ups\"]:\n                    fdone = max(0.001, 1 - rem)\n                    td = max(0.1, now - t0)\n                    rd, fn = vsplit(vp.replace(os.sep, \"/\"))\n                    if rd:\n                        rds = rd.replace(\"/\", \" / \")\n                        erd = \"/%s/\" % (quotep(rd),)\n                    else:\n                        erd = rds = \"/\"\n                    spd = humansize(sz * fdone / td, True) + \"/s\"\n                    eta = s2hms((td / fdone) - td, True) if rem < 1 else \"--\"\n                    idle = s2hms(now - poke, True)\n                    ups.append((int(100 * fdone), spd, eta, idle, erd, rds, fn))\n            except Exception as ex:\n                self.log(\"failed to list upload progress: %r\" % (ex,), 1)\n        if not get_vst:\n            vstate = {}\n            vs = {\n                \"scanning\": None,\n                \"hashq\": None,\n                \"tagq\": None,\n                \"mtpq\": None,\n                \"dbwt\": None,\n            }\n\n        assert vstate is not None and vstate.items and vs  # type: ignore  # !rm\n\n        dls = dl_list = []\n        if self.conn.hsrv.tdls:\n            zi = self.args.dl_list\n            if zi == 2 or (zi == 1 and self.avol):\n                dl_list = self.get_dls()\n        for t0, t1, sent, sz, vp, dl_id, uname in dl_list:\n            td = max(0.1, now - t0)\n            rd, fn = vsplit(vp)\n            if rd:\n                rds = rd.replace(\"/\", \" / \")\n                erd = \"/%s/\" % (quotep(rd),)\n            else:\n                erd = rds = \"/\"\n            spd = humansize(sent / td, True) + \"/s\"\n            hsent = humansize(sent, True)\n            idle = s2hms(now - t1, True)\n            usr = \"%s @%s\" % (dl_id, uname) if dl_id else uname\n            if sz and sent and td:\n                eta = s2hms((sz - sent) / (sent / td), True)\n                perc = int(100 * sent / sz)\n            else:\n                eta = perc = \"--\"\n\n            fn = html_escape(fn) if fn else self.conn.hsrv.iiam\n            dls.append((perc, hsent, spd, eta, idle, usr, erd, rds, fn))\n\n        if self.args.have_unlistc:\n            allvols = self.asrv.vfs.all_nodes\n            rvol = [x for x in rvol if \"unlistcr\" not in allvols[x.strip(\"/\")].flags]\n            wvol = [x for x in wvol if \"unlistcw\" not in allvols[x.strip(\"/\")].flags]\n\n        fmt = self.uparam.get(\"ls\", \"\")\n        if not fmt and self.ua.startswith((\"curl/\", \"fetch\")):\n            fmt = \"v\"\n\n        if fmt in [\"v\", \"t\", \"txt\"]:\n            if self.uname == \"*\":\n                txt = \"howdy stranger (you're not logged in)\"\n            else:\n                txt = \"welcome back {}\".format(self.uname)\n\n            if vstate:\n                txt += \"\\nstatus:\"\n                for k in [\"scanning\", \"hashq\", \"tagq\", \"mtpq\", \"dbwt\"]:\n                    txt += \" {}({})\".format(k, vs[k])\n\n            if ups:\n                txt += \"\\n\\nincoming files:\"\n                for zt in ups:\n                    txt += \"\\n%s\" % (\", \".join((str(x) for x in zt)),)\n                txt += \"\\n\"\n\n            if dls:\n                txt += \"\\n\\nactive downloads:\"\n                for zt in dls:\n                    txt += \"\\n%s\" % (\", \".join((str(x) for x in zt)),)\n                txt += \"\\n\"\n\n            if rvol:\n                txt += \"\\nyou can browse:\"\n                for v in rvol:\n                    txt += \"\\n  \" + v\n\n            if wvol:\n                txt += \"\\nyou can upload to:\"\n                for v in wvol:\n                    txt += \"\\n  \" + v\n\n            zb = txt.encode(\"utf-8\", \"replace\") + b\"\\n\"\n            self.reply(zb, mime=\"text/plain; charset=utf-8\")\n            return True\n\n        re_btn = \"\"\n        nre = self.args.ctl_re\n        if \"re\" in self.uparam:\n            self.out_headers[\"Refresh\"] = str(nre)\n        elif nre:\n            re_btn = \"&re=%s\" % (nre,)\n\n        zi = self.args.ver_iwho\n        show_ver = zi and (\n            zi == 9 or (zi == 6 and self.uname != \"*\") or (zi == 3 and avol)\n        )\n\n        html = self.j2s(\n            \"splash\",\n            this=self,\n            qvpath=quotep(self.vpaths) + self.ourlq(),\n            rvol=rvol,\n            wvol=wvol,\n            avol=avol,\n            in_shr=self.args.shr and self.vpath.startswith(self.args.shr1),\n            vstate=vstate,\n            dls=dls,\n            ups=ups,\n            scanning=vs[\"scanning\"],\n            hashq=vs[\"hashq\"],\n            tagq=vs[\"tagq\"],\n            mtpq=vs[\"mtpq\"],\n            dbwt=vs[\"dbwt\"],\n            url_suf=suf,\n            re=re_btn,\n            k304=self.k304(),\n            no304=self.no304(),\n            k304vis=self.args.k304 > 0,\n            no304vis=self.args.no304 > 0,\n            msg=(\n                BADVER\n                if self.conn.hsrv.bad_ver and avol\n                else BADXFFB\n                if hasattr(self, \"bad_xff\")\n                else \"\"\n            ),\n            ver=S_VERSION if show_ver else \"\",\n            chpw=self.args.chpw and self.uname != \"*\",\n            ahttps=\"\" if self.is_https else \"https://\" + self.host + self.req,\n        )\n        self.reply(html.encode(\"utf-8\"))\n        return True\n\n    def setck(self) -> bool:\n        zs = self.uparam[\"setck\"]\n        if len(zs) > 9 or RE_SETCK.search(zs):\n            raise Pebkac(400, \"illegal value\")\n        k, v = zs.split(\"=\")\n        t = 0 if v in (\"\", \"x\") else 86400 * 299\n        ck = gencookie(k, v, self.args.R, True, False, t)\n        self.out_headerlist.append((\"Set-Cookie\", ck))\n        if \"cc\" in self.ouparam:\n            self.redirect(\"\", \"?h#cc\")\n        else:\n            self.reply(b\"o7\\n\")\n        return True\n\n    def set_cfg_reset(self) -> bool:\n        for k in ALL_COOKIES:\n            if k not in self.cookies:\n                continue\n            cookie = gencookie(k, \"x\", self.args.R, True, False)\n            self.out_headerlist.append((\"Set-Cookie\", cookie))\n\n        self.redirect(\"\", \"?h#cc\")\n        return True\n\n    def tx_404(self, is_403: bool = False) -> bool:\n        rc = 404\n        if self.args.vague_403:\n            t = '<h1 id=\"n\">404 not found &nbsp;┐( ´ -`)┌</h1><p id=\"o\">or maybe you don\\'t have access -- try a password or <a href=\"{}/?h\">go home</a></p>'\n            pt = \"404 not found  ┐( ´ -`)┌   (or maybe you don't have access -- try a password)\"\n        elif is_403:\n            t = '<h1 id=\"p\">403 forbiddena &nbsp;~┻━┻</h1><p id=\"q\">use a password or <a href=\"{}/?h\">go home</a></p>'\n            pt = \"403 forbiddena ~┻━┻   (you'll have to log in)\"\n            rc = 403\n        else:\n            t = '<h1 id=\"n\">404 not found &nbsp;┐( ´ -`)┌</h1><p><a id=\"r\" href=\"{}/?h\">go home</a></p>'\n            pt = \"404 not found  ┐( ´ -`)┌\"\n\n        if self.ua.startswith((\"curl/\", \"fetch\")):\n            pt = \"# acct: %s\\n%s\\n\" % (self.uname, pt)\n            self.reply(pt.encode(\"utf-8\"), status=rc)\n            return True\n\n        if \"th\" in self.ouparam and str(self.ouparam[\"th\"])[:1] in \"jw\":\n            return self.tx_svg(\"e\" + pt[:3])\n\n        # most webdav clients will not send credentials until they\n        # get 401'd, so send a challenge if we're Absolutely Sure\n        # that the client is not a graphical browser\n        if rc == 403 and self.uname == \"*\":\n            sport = self.s.getsockname()[1]\n            if self.args.dav_port == sport or (\n                \"sec-fetch-site\" not in self.headers\n                and self.cookies.get(\"js\") != \"y\"\n                and sport not in self.args.p_nodav\n                and (\n                    not self.args.ua_nodav.search(self.ua)\n                    or (self.args.dav_ua1 and self.args.dav_ua1.search(self.ua))\n                )\n            ):\n                rc = 401\n                self.out_headers[\"WWW-Authenticate\"] = 'Basic realm=\"a\"'\n\n        t = t.format(self.args.SR)\n        qv = quotep(self.vpaths) + self.ourlq()\n        html = self.j2s(\n            \"splash\",\n            this=self,\n            qvpath=qv,\n            msg=t,\n            in_shr=self.args.shr and self.vpath.startswith(self.args.shr1),\n            ahttps=\"\" if self.is_https else \"https://\" + self.host + self.req,\n        )\n        self.reply(html.encode(\"utf-8\"), status=rc)\n        return True\n\n    def on40x(self, mods: list[str], vn: VFS, rem: str) -> str:\n        for mpath in mods:\n            try:\n                mod = loadpy(mpath, self.args.hot_handlers)\n            except Exception as ex:\n                self.log(\"import failed: {!r}\".format(ex))\n                continue\n\n            ret = mod.main(self, vn, rem)\n            if ret:\n                return ret.lower()\n\n        return \"\"  # unhandled / fallthrough\n\n    def scanvol(self) -> bool:\n        if self.args.no_rescan:\n            raise Pebkac(403, \"the rescan feature is disabled in server config\")\n\n        vpaths = self.uparam[\"scan\"].split(\",/\")\n        if vpaths == [\"\"]:\n            vpaths = [self.vpath]\n\n        vols = []\n        for vpath in vpaths:\n            vn, _ = self.asrv.vfs.get(vpath, self.uname, True, True)\n            vols.append(vn.vpath)\n            if self.uname not in vn.axs.uadmin:\n                self.log(\"rejected scanning [%s] => [%s];\" % (vpath, vn.vpath), 3)\n                raise Pebkac(403, \"'scanvol' not allowed for user \" + self.uname)\n\n        self.log(\"trying to rescan %d volumes: %r\" % (len(vols), vols))\n\n        args = [self.asrv.vfs.all_vols, vols, False, True]\n\n        x = self.conn.hsrv.broker.ask(\"up2k.rescan\", *args)\n        err = x.get()\n        if not err:\n            self.redirect(\"\", \"?h\")\n            return True\n\n        raise Pebkac(500, err)\n\n    def handle_reload(self) -> bool:\n        act = self.uparam.get(\"reload\")\n        if act != \"cfg\":\n            raise Pebkac(400, \"only config files ('cfg') can be reloaded rn\")\n\n        if not self.avol:\n            raise Pebkac(403, \"'reload' not allowed for user \" + self.uname)\n\n        if self.args.no_reload:\n            raise Pebkac(403, \"the reload feature is disabled in server config\")\n\n        x = self.conn.hsrv.broker.ask(\"reload\", True, True)\n        return self.redirect(\"\", \"?h\", x.get(), \"return to\", False)\n\n    def tx_stack(self) -> bool:\n        zs = self.args.stack_who\n        if zs == \"all\" or (\n            (zs == \"a\" and self.avol)\n            or (zs == \"rw\" and [x for x in self.wvol if x in self.rvol])\n        ):\n            pass\n        else:\n            raise Pebkac(403, \"'stack' not allowed for user \" + self.uname)\n\n        ret = html_escape(alltrace(self.args.stack_v))\n        if self.args.stack_v:\n            ret = \"<pre>%s\\n%s\" % (time.time(), ret)\n        else:\n            ret = \"<pre>%s\" % (ret,)\n        self.reply(ret.encode(\"utf-8\"))\n        return True\n\n    def tx_tree(self) -> bool:\n        top = self.uparam[\"tree\"] or \"\"\n        dst = self.vpath\n        if top in [\".\", \"..\"]:\n            top = undot(self.vpath + \"/\" + top)\n\n        if top == dst:\n            dst = \"\"\n        elif top:\n            if not dst.startswith(top + \"/\"):\n                raise Pebkac(422, \"arg funk\")\n\n            dst = dst[len(top) + 1 :]\n\n        ret = self.gen_tree(top, dst, self.uparam.get(\"k\", \"\"))\n        if self.is_vproxied and not self.uparam[\"tree\"]:\n            # uparam is '' on initial load, which is\n            # the only time we gotta fill in the blanks\n            parents = self.args.R.split(\"/\")\n            for parent in reversed(parents):\n                ret = {\"k%s\" % (parent,): ret, \"a\": []}\n\n        zs = json.dumps(ret)\n        self.reply(zs.encode(\"utf-8\"), mime=\"application/json\")\n        return True\n\n    def gen_tree(self, top: str, target: str, dk: str) -> dict[str, Any]:\n        ret: dict[str, Any] = {}\n        excl = None\n        if target:\n            excl, target = (target.split(\"/\", 1) + [\"\"])[:2]\n            sub = self.gen_tree(\"/\".join([top, excl]).strip(\"/\"), target, dk)\n            ret[\"k\" + quotep(excl)] = sub\n\n        vfs = self.asrv.vfs\n        dk_sz = False\n        if dk:\n            vn, rem = vfs.get(top, self.uname, False, False)\n            if vn.flags.get(\"dks\") and self._use_dirkey(vn, vn.canonical(rem)):\n                dk_sz = vn.flags.get(\"dk\")\n\n        dots = False\n        fsroot = \"\"\n        try:\n            vn, rem = vfs.get(top, self.uname, not dk_sz, False)\n            fsroot, vfs_ls, vfs_virt = vn.ls(\n                rem,\n                self.uname,\n                not self.args.no_scandir,\n                PERMS_rwh,\n            )\n            dots = self.uname in vn.axs.udot and \"dots\" in self.uparam\n            dk_sz = vn.flags.get(\"dk\")\n        except:\n            dk_sz = None\n            vfs_ls = []\n            vfs_virt = {}\n            for v in self.rvol:\n                d1, d2 = v.rsplit(\"/\", 1) if \"/\" in v else [\"\", v]\n                if d1 == top:\n                    vfs_virt[d2] = vfs  # typechk, value never read\n\n        dirs = [x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]\n\n        if not dots:\n            dirs = exclude_dotfiles(dirs)\n\n        dirs = [quotep(x) for x in dirs if x != excl]\n\n        if dk_sz and fsroot:\n            kdirs = []\n            fsroot_ = os.path.join(fsroot, \"\")\n            for dn in dirs:\n                ap = fsroot_ + dn\n                zs = self.gen_fk(2, self.args.dk_salt, ap, 0, 0)[:dk_sz]\n                kdirs.append(dn + \"?k=\" + zs)\n            dirs = kdirs\n\n        if vfs_virt:\n            for x in vfs_virt:\n                if x == excl:\n                    continue\n                try:\n                    dvn, drem = vfs.get(vjoin(top, x), self.uname, False, False)\n                    if (\n                        self.uname not in dvn.axs.uread\n                        and self.uname not in dvn.axs.uwrite\n                        and self.uname not in dvn.axs.uhtml\n                    ):\n                        raise Exception()\n                    bos.stat(dvn.canonical(drem, False))\n                except:\n                    x += \"\\n\"\n                dirs.append(quotep(x))\n            if not dots:\n                dirs = exclude_dotfiles(dirs)\n\n        ret[\"a\"] = dirs\n        return ret\n\n    def get_dls(self) -> list[list[Any]]:\n        ret = []\n        dls = self.conn.hsrv.tdls\n        enshare = self.args.shr\n        shrs = enshare[1:]\n        for dl_id, (t0, sz, vn, vp, uname) in self.conn.hsrv.tdli.items():\n            t1, sent = dls[dl_id]\n            if sent > 0x100000:  # 1m; buffers 2~4\n                sent -= 0x100000\n            if self.uname not in vn.axs.uread:\n                vp = \"\"\n            elif self.uname not in vn.axs.udot and (vp.startswith(\".\") or \"/.\" in vp):\n                vp = \"\"\n            elif (\n                enshare\n                and vp.startswith(shrs)\n                and self.uname != vn.shr_owner\n                and self.uname not in vn.axs.uadmin\n                and self.uname not in self.args.shr_adm\n                and not dl_id.startswith(self.ip + \":\")\n            ):\n                vp = \"\"\n            if self.uname not in vn.axs.uadmin:\n                dl_id = uname = \"\"\n\n            ret.append([t0, t1, sent, sz, vp, dl_id, uname])\n        return ret\n\n    def tx_dls(self) -> bool:\n        ret = [\n            {\n                \"t0\": x[0],\n                \"t1\": x[1],\n                \"sent\": x[2],\n                \"size\": x[3],\n                \"path\": x[4],\n                \"conn\": x[5],\n                \"uname\": x[6],\n            }\n            for x in self.get_dls()\n        ]\n        zs = json.dumps(ret, separators=(\",\\n\", \": \"))\n        self.reply(zs.encode(\"utf-8\", \"replace\"), mime=\"application/json\")\n        return True\n\n    def tx_ups(self) -> bool:\n        idx = self.conn.get_u2idx()\n        if not idx or not hasattr(idx, \"p_end\"):\n            if not HAVE_SQLITE3:\n                raise Pebkac(500, \"sqlite3 not found on server; unpost is disabled\")\n            raise Pebkac(500, \"server busy, cannot unpost; please retry in a bit\")\n\n        sfilt = self.uparam.get(\"filter\") or \"\"\n        nfi, vfi = str_anchor(sfilt)\n        lm = \"ups %d%r\" % (nfi, sfilt)\n\n        if self.args.shr and self.vpath.startswith(self.args.shr1):\n            shr_dbv, shr_vrem = self.vn.get_dbv(self.rem)\n        else:\n            shr_dbv = None\n\n        wret: dict[str, Any] = {}\n        ret: list[dict[str, Any]] = []\n        t0 = time.time()\n        lim = time.time() - self.args.unpost\n        fk_vols = {\n            vol: (vol.flags[\"fk\"], 2 if \"fka\" in vol.flags else 1)\n            for vp, vol in self.asrv.vfs.all_vols.items()\n            if \"fk\" in vol.flags\n            and (self.uname in vol.axs.uread or self.uname in vol.axs.upget)\n        }\n\n        if hasattr(self, \"bad_xff\"):\n            allvols = []\n            t = \"will not return list of recent uploads\" + BADXFF\n            self.log(t, 1)\n            if self.avol:\n                raise Pebkac(500, t)\n\n            x = self.conn.hsrv.broker.ask(\"up2k.get_unfinished_by_user\", self.uname, \"\")\n        else:\n            x = self.conn.hsrv.broker.ask(\n                \"up2k.get_unfinished_by_user\", self.uname, self.ip\n            )\n        zdsa: dict[str, Any] = x.get()\n        uret: list[dict[str, Any]] = []\n        if \"timeout\" in zdsa:\n            wret[\"nou\"] = 1\n        else:\n            uret = zdsa[\"f\"]\n        nu = len(uret)\n\n        if not self.args.unpost:\n            allvols = []\n        else:\n            allvols = list(self.asrv.vfs.all_vols.values())\n\n        allvols = [\n            x\n            for x in allvols\n            if \"e2d\" in x.flags\n            and (\"*\" in x.axs.uwrite or self.uname in x.axs.uwrite or x == shr_dbv)\n        ]\n\n        q = \"\"\n        qp = (0,)\n        q_c = -1\n\n        for vol in allvols:\n            cur = idx.get_cur(vol)\n            if not cur:\n                continue\n\n            nfk, fk_alg = fk_vols.get(vol) or (0, 0)\n\n            zi = vol.flags[\"unp_who\"]\n            if q_c != zi:\n                q_c = zi\n                q = \"select sz, rd, fn, at from up where \"\n                if zi == 1:\n                    q += \"ip=? and un=?\"\n                    qp = (self.ip, self.uname, lim)\n                elif zi == 2:\n                    q += \"ip=?\"\n                    qp = (self.ip, lim)\n                if zi == 3:\n                    q += \"un=?\"\n                    qp = (self.uname, lim)\n                q += \" and at>? order by at desc\"\n\n            n = 2000\n            for sz, rd, fn, at in cur.execute(q, qp):\n                vp = \"/\" + \"/\".join(x for x in [vol.vpath, rd, fn] if x)\n                if nfi == 0 or (nfi == 1 and vfi in vp.lower()):\n                    pass\n                elif nfi == 2:\n                    if not vp.lower().startswith(vfi):\n                        continue\n                elif nfi == 3:\n                    if not vp.lower().endswith(vfi):\n                        continue\n                else:\n                    continue\n\n                n -= 1\n                if not n:\n                    break\n\n                rv = {\"vp\": vp, \"sz\": sz, \"at\": at, \"nfk\": nfk}\n                if nfk:\n                    rv[\"ap\"] = vol.canonical(vjoin(rd, fn))\n                    rv[\"fk_alg\"] = fk_alg\n\n                ret.append(rv)\n                if len(ret) > 3000:\n                    ret.sort(key=lambda x: x[\"at\"], reverse=True)  # type: ignore\n                    ret = ret[:2000]\n\n        ret.sort(key=lambda x: x[\"at\"], reverse=True)  # type: ignore\n\n        if len(ret) > 2000:\n            ret = ret[:2000]\n        if len(ret) >= 2000:\n            wret[\"oc\"] = 1\n\n        for rv in ret:\n            rv[\"vp\"] = quotep(rv[\"vp\"])\n            nfk = rv.pop(\"nfk\")\n            if not nfk:\n                continue\n\n            alg = rv.pop(\"fk_alg\")\n            ap = rv.pop(\"ap\")\n            try:\n                st = bos.stat(ap)\n            except:\n                continue\n\n            fk = self.gen_fk(\n                alg, self.args.fk_salt, ap, st.st_size, 0 if ANYWIN else st.st_ino\n            )\n            rv[\"vp\"] += \"?k=\" + fk[:nfk]\n\n        if not allvols:\n            wret[\"noc\"] = 1\n            ret = []\n\n        nc = len(ret)\n        ret = uret + ret\n\n        if shr_dbv:\n            # translate vpaths from share-target to share-url\n            # to satisfy access checks\n            assert shr_vrem is not None and shr_vrem.split  # type: ignore  # !rm\n            vp_shr, vp_vfs = vroots(self.vpath, vjoin(shr_dbv.vpath, shr_vrem))\n            for v in ret:\n                vp = v[\"vp\"]\n                if vp.startswith(vp_vfs):\n                    v[\"vp\"] = vp_shr + vp[len(vp_vfs) :]\n\n        if self.is_vproxied:\n            for v in ret:\n                v[\"vp\"] = self.args.SR + v[\"vp\"]\n\n        wret[\"f\"] = ret\n        wret[\"nu\"] = nu\n        wret[\"nc\"] = nc\n        jtxt = json.dumps(wret, separators=(\",\\n\", \": \"))\n        self.log(\"%s #%d+%d %.2fsec\" % (lm, nu, nc, time.time() - t0))\n        self.reply(jtxt.encode(\"utf-8\", \"replace\"), mime=\"application/json\")\n        return True\n\n    def tx_rups(self) -> bool:\n        if self.args.no_ups_page:\n            raise Pebkac(500, \"listing of recent uploads is disabled in server config\")\n\n        idx = self.conn.get_u2idx()\n        if not idx or not hasattr(idx, \"p_end\"):\n            if not HAVE_SQLITE3:\n                raise Pebkac(500, \"sqlite3 not found on server; recent-uploads n/a\")\n            raise Pebkac(500, \"server busy, cannot list recent uploads; please retry\")\n\n        sfilt = self.uparam.get(\"filter\") or \"\"\n        nfi, vfi = str_anchor(sfilt)\n        lm = \"ru %d%r\" % (nfi, sfilt)\n        self.log(lm)\n\n        ret: list[dict[str, Any]] = []\n        t0 = time.time()\n        allvols = [\n            x\n            for x in self.asrv.vfs.all_vols.values()\n            if \"e2d\" in x.flags and (\"*\" in x.axs.uread or self.uname in x.axs.uread)\n        ]\n        fk_vols = {\n            vol: (vol.flags[\"fk\"], 2 if \"fka\" in vol.flags else 1)\n            for vol in allvols\n            if \"fk\" in vol.flags and \"*\" not in vol.axs.uread\n        }\n\n        for vol in allvols:\n            cur = idx.get_cur(vol)\n            if not cur:\n                continue\n\n            nfk, fk_alg = fk_vols.get(vol) or (0, 0)\n            adm = \"*\" in vol.axs.uadmin or self.uname in vol.axs.uadmin\n            dots = \"*\" in vol.axs.udot or self.uname in vol.axs.udot\n\n            lvl = vol.flags[\"ups_who\"]\n            if not lvl:\n                continue\n            elif lvl == 1 and not adm:\n                continue\n\n            n = 1000\n            q = \"select sz, rd, fn, ip, at, un from up where at>0 order by at desc\"\n            for sz, rd, fn, ip, at, un in cur.execute(q):\n                vp = \"/\" + \"/\".join(x for x in [vol.vpath, rd, fn] if x)\n                if nfi == 0 or (nfi == 1 and vfi in vp.lower()):\n                    pass\n                elif nfi == 2:\n                    if not vp.lower().startswith(vfi):\n                        continue\n                elif nfi == 3:\n                    if not vp.lower().endswith(vfi):\n                        continue\n                else:\n                    continue\n\n                if not dots and \"/.\" in vp:\n                    continue\n\n                rv = {\n                    \"vp\": vp,\n                    \"sz\": sz,\n                    \"ip\": ip,\n                    \"at\": at,\n                    \"un\": un,\n                    \"nfk\": nfk,\n                    \"adm\": adm,\n                }\n                if nfk:\n                    rv[\"ap\"] = vol.canonical(vjoin(rd, fn))\n                    rv[\"fk_alg\"] = fk_alg\n\n                ret.append(rv)\n                if len(ret) > 2000:\n                    ret.sort(key=lambda x: x[\"at\"], reverse=True)  # type: ignore\n                    ret = ret[:1000]\n\n                n -= 1\n                if not n:\n                    break\n\n        ret.sort(key=lambda x: x[\"at\"], reverse=True)  # type: ignore\n\n        if len(ret) > 1000:\n            ret = ret[:1000]\n\n        for rv in ret:\n            rv[\"vp\"] = quotep(rv[\"vp\"])\n            nfk = rv.pop(\"nfk\")\n            if not nfk:\n                continue\n\n            alg = rv.pop(\"fk_alg\")\n            ap = rv.pop(\"ap\")\n            try:\n                st = bos.stat(ap)\n            except:\n                continue\n\n            fk = self.gen_fk(\n                alg, self.args.fk_salt, ap, st.st_size, 0 if ANYWIN else st.st_ino\n            )\n            rv[\"vp\"] += \"?k=\" + fk[:nfk]\n\n        if self.args.ups_when:\n            for rv in ret:\n                adm = rv.pop(\"adm\")\n                if not adm:\n                    rv[\"ip\"] = \"(You)\" if rv[\"ip\"] == self.ip else \"(?)\"\n                    if rv[\"un\"] not in (\"*\", self.uname):\n                        rv[\"un\"] = \"(?)\"\n        else:\n            for rv in ret:\n                adm = rv.pop(\"adm\")\n                if not adm:\n                    rv[\"ip\"] = \"(You)\" if rv[\"ip\"] == self.ip else \"(?)\"\n                    rv[\"at\"] = 0\n                    if rv[\"un\"] not in (\"*\", self.uname):\n                        rv[\"un\"] = \"(?)\"\n\n        if self.is_vproxied:\n            for v in ret:\n                v[\"vp\"] = self.args.SR + v[\"vp\"]\n\n        now = time.time()\n        self.log(\"%s #%d %.2fsec\" % (lm, len(ret), now - t0))\n\n        ret2 = {\"now\": int(now), \"filter\": sfilt, \"ups\": ret}\n        jtxt = json.dumps(ret2, separators=(\",\\n\", \": \"))\n        if \"j\" in self.ouparam:\n            self.reply(jtxt.encode(\"utf-8\", \"replace\"), mime=\"application/json\")\n            return True\n\n        html = self.j2s(\"rups\", this=self, v=json_hesc(jtxt))\n        self.reply(html.encode(\"utf-8\"), status=200)\n        return True\n\n    def tx_idp(self) -> bool:\n        if self.uname.lower() not in self.args.idp_adm_set:\n            raise Pebkac(403, \"'idp' not allowed for user \" + self.uname)\n\n        cmd = self.uparam[\"idp\"]\n        if cmd.startswith(\"rm=\"):\n            import sqlite3\n\n            db = sqlite3.connect(self.args.idp_db)\n            db.execute(\"delete from us where un=?\", (cmd[3:],))\n            db.commit()\n            db.close()\n\n            self.conn.hsrv.broker.ask(\"reload\", False, True).get()\n\n            self.redirect(\"\", \"?idp\")\n            return True\n\n        rows = [\n            [k, \"[%s]\" % (\"], [\".join(v))]\n            for k, v in sorted(self.asrv.idp_accs.items())\n        ]\n        html = self.j2s(\"idp\", this=self, rows=rows, now=int(time.time()))\n        self.reply(html.encode(\"utf-8\"), status=200)\n        return True\n\n    def tx_shares(self) -> bool:\n        if self.uname == \"*\":\n            self.loud_reply(\"you're not logged in\")\n            return True\n\n        idx = self.conn.get_u2idx()\n        if not idx or not hasattr(idx, \"p_end\"):\n            if not HAVE_SQLITE3:\n                raise Pebkac(500, \"sqlite3 not found on server; sharing is disabled\")\n            raise Pebkac(500, \"server busy, cannot list shares; please retry in a bit\")\n\n        cur = idx.get_shr()\n        if not cur:\n            raise Pebkac(400, \"huh, sharing must be disabled in the server config...\")\n\n        rows = cur.execute(\"select * from sh\").fetchall()\n        rows = [list(x) for x in rows]\n\n        if self.uname != self.args.shr_adm:\n            rows = [x for x in rows if x[5] == self.uname]\n\n        q = \"select vp from sf where k=? limit 99\"\n        for r in rows:\n            if not r[4]:\n                r[4] = \"---\"\n            else:\n                zstl = cur.execute(q, (r[0],)).fetchall()\n                zsl = [html_escape(zst[0]) for zst in zstl]\n                r[4] = \"<br />\".join(zsl)\n\n        if self.args.shr_site:\n            site = self.args.shr_site[:-1]\n        else:\n            site = \"\"\n        if self.is_vproxied:\n            site += self.args.SR\n\n        html = self.j2s(\n            \"shares\",\n            this=self,\n            shr=self.args.shr,\n            site=site,\n            rows=rows,\n            now=int(time.time()),\n        )\n        self.reply(html.encode(\"utf-8\"), status=200)\n        return True\n\n    def handle_eshare(self) -> bool:\n        idx = self.conn.get_u2idx()\n        if not idx or not hasattr(idx, \"p_end\"):\n            if not HAVE_SQLITE3:\n                raise Pebkac(500, \"sqlite3 not found on server; sharing is disabled\")\n            raise Pebkac(500, \"server busy, cannot create share; please retry in a bit\")\n\n        skey = self.uparam.get(\"skey\") or self.vpath.split(\"/\")[-1]\n\n        if self.args.shr_v:\n            self.log(\"handle_eshare: \" + skey)\n\n        cur = idx.get_shr()\n        if not cur:\n            raise Pebkac(400, \"huh, sharing must be disabled in the server config...\")\n\n        rows = cur.execute(\"select un, t1 from sh where k = ?\", (skey,)).fetchall()\n        un = rows[0][0] if rows and rows[0] else \"\"\n\n        if not un:\n            raise Pebkac(400, \"that sharekey didn't match anything\")\n\n        expiry = rows[0][1]\n\n        if un != self.uname and self.uname != self.args.shr_adm:\n            t = \"your username (%r) does not match the sharekey's owner (%r) and you're not admin\"\n            raise Pebkac(400, t % (self.uname, un))\n\n        reload = False\n        act = self.uparam[\"eshare\"]\n        if act == \"rm\":\n            cur.execute(\"delete from sh where k = ?\", (skey,))\n            if skey in self.asrv.vfs.nodes[self.args.shr.strip(\"/\")].nodes:\n                reload = True\n        else:\n            now = time.time()\n            if expiry < now:\n                expiry = now\n                reload = True\n            expiry += int(act) * 60\n            cur.execute(\"update sh set t1 = ? where k = ?\", (expiry, skey))\n\n        cur.connection.commit()\n        if reload:\n            self.conn.hsrv.broker.ask(\"reload\", False, True).get()\n            self.conn.hsrv.broker.ask(\"up2k.wake_rescanner\").get()\n\n        self.redirect(\"\", \"?shares\")\n        return True\n\n    def handle_share(self, req: dict[str, str]) -> bool:\n        idx = self.conn.get_u2idx()\n        if not idx or not hasattr(idx, \"p_end\"):\n            if not HAVE_SQLITE3:\n                raise Pebkac(500, \"sqlite3 not found on server; sharing is disabled\")\n            raise Pebkac(500, \"server busy, cannot create share; please retry in a bit\")\n\n        if self.args.shr_v:\n            self.log(\"handle_share: \" + json.dumps(req, indent=4))\n\n        skey = req[\"k\"]\n        vps = req[\"vp\"]\n        fns = []\n        if len(vps) == 1:\n            vp = vps[0]\n            if not vp.endswith(\"/\"):\n                vp, zs = vp.rsplit(\"/\", 1)\n                fns = [zs]\n        else:\n            for zs in vps:\n                if zs.endswith(\"/\"):\n                    t = \"you cannot select more than one folder, or mix files and folders in one selection\"\n                    raise Pebkac(400, t)\n            vp = vps[0].rsplit(\"/\", 1)[0]\n            for zs in vps:\n                vp2, fn = zs.rsplit(\"/\", 1)\n                fns.append(fn)\n                if vp != vp2:\n                    t = \"mismatching base paths in selection:\\n  %r\\n  %r\"\n                    raise Pebkac(400, t % (vp, vp2))\n\n        vp = vp.strip(\"/\")\n        if self.is_vproxied and (vp == self.args.R or vp.startswith(self.args.RS)):\n            vp = vp[len(self.args.RS) :]\n\n        m = re.search(r\"([^0-9a-zA-Z_-])\", skey)\n        if m:\n            raise Pebkac(400, \"sharekey has illegal character %r\" % (m[1],))\n\n        if vp.startswith(self.args.shr1):\n            raise Pebkac(400, \"yo dawg...\")\n\n        cur = idx.get_shr()\n        if not cur:\n            raise Pebkac(400, \"huh, sharing must be disabled in the server config...\")\n\n        q = \"select * from sh where k = ?\"\n        qr = cur.execute(q, (skey,)).fetchall()\n        if qr and qr[0]:\n            self.log(\"sharekey taken by %r\" % (qr,))\n            raise Pebkac(400, \"sharekey %r is already in use\" % (skey,))\n\n        # ensure user has requested perms\n        s_rd = \"read\" in req[\"perms\"]\n        s_wr = \"write\" in req[\"perms\"]\n        s_get = \"get\" in req[\"perms\"]\n        s_axs = [s_rd, s_wr, False, False, s_get]\n\n        if s_axs == [False] * 5:\n            raise Pebkac(400, \"select at least one permission\")\n\n        try:\n            vfs, rem = self.asrv.vfs.get(vp, self.uname, *s_axs)\n        except:\n            raise Pebkac(400, \"you dont have all the perms you tried to grant\")\n\n        zs = vfs.flags[\"shr_who\"]\n        if zs == \"auth\" and self.uname != \"*\":\n            pass\n        elif zs == \"a\" and self.uname in vfs.axs.uadmin:\n            pass\n        else:\n            raise Pebkac(400, \"you dont have perms to create shares from this volume\")\n\n        ap, reals, _ = vfs.ls(rem, self.uname, not self.args.no_scandir, [s_axs])\n        rfns = set([x[0] for x in reals])\n        for fn in fns:\n            if fn not in rfns:\n                raise Pebkac(400, \"selected file not found on disk: %r\" % (fn,))\n\n        pw = req.get(\"pw\") or \"\"\n        pw = self.asrv.ah.hash(pw)\n        now = int(time.time())\n        sexp = req[\"exp\"]\n        exp = int(sexp) if sexp else 0\n        exp = now + exp * 60 if exp else 0\n        pr = \"\".join(zc for zc, zb in zip(\"rwmdg\", s_axs) if zb)\n\n        q = \"insert into sh values (?,?,?,?,?,?,?,?)\"\n        cur.execute(q, (skey, pw, vp, pr, len(fns), self.uname, now, exp))\n\n        q = \"insert into sf values (?,?)\"\n        for fn in fns:\n            cur.execute(q, (skey, fn))\n\n        cur.connection.commit()\n        self.conn.hsrv.broker.ask(\"reload\", False, True).get()\n        self.conn.hsrv.broker.ask(\"up2k.wake_rescanner\").get()\n\n        fn = quotep(fns[0]) if len(fns) == 1 else \"\"\n\n        # NOTE: several clients (frontend, party-up) expect url at response[15:]\n        if self.args.shr_site:\n            surl = \"created share: %s%s%s%s/%s\" % (\n                self.args.shr_site[:-1],\n                self.args.SR,\n                self.args.shr,\n                skey,\n                fn,\n            )\n        else:\n            surl = \"created share: %s://%s%s%s%s/%s\" % (\n                \"https\" if self.is_https else \"http\",\n                self.host,\n                self.args.SR,\n                self.args.shr,\n                skey,\n                fn,\n            )\n        self.loud_reply(surl, status=201)\n        return True\n\n    def handle_rm(self, req: list[str]) -> bool:\n        if not req and not self.can_delete:\n            if self.mode == \"DELETE\" and self.uname == \"*\":\n                raise Pebkac(401, \"authenticate\")  # webdav\n            raise Pebkac(403, \"'delete' not allowed for user \" + self.uname)\n\n        if self.args.no_del:\n            raise Pebkac(403, \"the delete feature is disabled in server config\")\n\n        unpost = \"unpost\" in self.uparam\n        if unpost and hasattr(self, \"bad_xff\"):\n            self.log(\"unpost was denied\" + BADXFF, 1)\n            raise Pebkac(403, \"the delete feature is disabled in server config\")\n\n        if not unpost and self.vn.shr_src:\n            raise Pebkac(403, \"files in shares can only be deleted with unpost\")\n\n        if not req:\n            req = [self.vpath]\n        elif self.is_vproxied:\n            req = [x[len(self.args.SR) :] for x in req]\n\n        nlim = int(self.uparam.get(\"lim\") or 0)\n        lim = [nlim, nlim] if nlim else []\n\n        x = self.conn.hsrv.broker.ask(\n            \"up2k.handle_rm\", self.uname, self.ip, req, lim, False, unpost\n        )\n        self.loud_reply(x.get())\n        return True\n\n    def handle_mv(self) -> bool:\n        # full path of new loc (incl filename)\n        dst = self.uparam.get(\"move\")\n\n        if self.is_vproxied and dst and dst.startswith(self.args.SR):\n            dst = dst[len(self.args.RS) :]\n\n        if not dst:\n            raise Pebkac(400, \"need dst vpath\")\n\n        return self._mv(self.vpath, dst.lstrip(\"/\"), False)\n\n    def _mv(self, vsrc: str, vdst: str, overwrite: bool) -> bool:\n        if self.args.no_mv:\n            raise Pebkac(403, \"the rename/move feature is disabled in server config\")\n\n        # `handle_cpmv` will catch 403 from these and raise 401\n        svn, srem = self.asrv.vfs.get(vsrc, self.uname, True, False, True)\n        dvn, drem = self.asrv.vfs.get(vdst, self.uname, False, True)\n\n        if overwrite:\n            dabs = dvn.canonical(drem)\n            if bos.path.exists(dabs):\n                self.log(\"overwriting %s\" % (dabs,))\n                self.asrv.vfs.get(vdst, self.uname, False, True, False, True)\n                wunlink(self.log, dabs, dvn.flags)\n\n        x = self.conn.hsrv.broker.ask(\n            \"up2k.handle_mv\", self.ouparam.get(\"akey\"), self.uname, self.ip, vsrc, vdst\n        )\n        self.loud_reply(x.get(), status=201)\n        return True\n\n    def handle_cp(self) -> bool:\n        # full path of new loc (incl filename)\n        dst = self.uparam.get(\"copy\")\n\n        if self.is_vproxied and dst and dst.startswith(self.args.SR):\n            dst = dst[len(self.args.RS) :]\n\n        if not dst:\n            raise Pebkac(400, \"need dst vpath\")\n\n        return self._cp(self.vpath, dst.lstrip(\"/\"), False)\n\n    def _cp(self, vsrc: str, vdst: str, overwrite: bool) -> bool:\n        if self.args.no_cp:\n            raise Pebkac(403, \"the copy feature is disabled in server config\")\n\n        svn, srem = self.asrv.vfs.get(vsrc, self.uname, True, False)\n        dvn, drem = self.asrv.vfs.get(vdst, self.uname, False, True)\n\n        if overwrite:\n            dabs = dvn.canonical(drem)\n            if bos.path.exists(dabs):\n                self.log(\"overwriting %s\" % (dabs,))\n                self.asrv.vfs.get(vdst, self.uname, False, True, False, True)\n                wunlink(self.log, dabs, dvn.flags)\n\n        x = self.conn.hsrv.broker.ask(\n            \"up2k.handle_cp\", self.ouparam.get(\"akey\"), self.uname, self.ip, vsrc, vdst\n        )\n        self.loud_reply(x.get(), status=201)\n        return True\n\n    def handle_fs_abrt(self):\n        if self.args.no_fs_abrt:\n            t = \"aborting an ongoing copy/move is disabled in server config\"\n            raise Pebkac(403, t)\n\n        self.conn.hsrv.broker.say(\"up2k.handle_fs_abrt\", self.uparam[\"fs_abrt\"])\n        self.loud_reply(\"aborting\", status=200)\n        return True\n\n    def tx_ls_vols(self) -> bool:\n        e_d = {}\n        eses = [\"\", \"\"]\n        rvol = self.rvol\n        wvol = self.wvol\n        allvols = self.asrv.vfs.all_nodes\n        if self.args.have_unlistc:\n            rvol = [x for x in rvol if \"unlistcr\" not in allvols[x].flags]\n            wvol = [x for x in wvol if \"unlistcw\" not in allvols[x].flags]\n        vols = [(x, allvols[x]) for x in list(set(rvol + wvol))]\n        if self.vpath:\n            zs = \"%s/\" % (self.vpath,)\n            vols = [(x[len(zs) :], y) for x, y in vols if x.startswith(zs)]\n        vols = [(x.split(\"/\", 1)[0], y) for x, y in vols]\n        vols = list(({x: y for x, y in vols if x}).items())\n        if not vols and self.vpath:\n            return self.tx_404(True)\n        dirs = [\n            {\n                \"lead\": \"\",\n                \"href\": \"%s/\" % (x,),\n                \"ext\": \"---\",\n                \"sz\": 0,\n                \"ts\": 0,\n                \"tags\": e_d,\n                \"dt\": 0,\n                \"name\": 0,\n                \"perms\": vn.get_perms(\"\", self.uname),\n            }\n            for x, vn in sorted(vols)\n        ]\n        ls = {\n            \"dirs\": dirs,\n            \"files\": [],\n            \"acct\": self.uname,\n            \"perms\": [],\n            \"taglist\": [],\n            \"logues\": eses,\n            \"readmes\": eses,\n            \"srvinf\": \"\" if self.args.nih else self.args.name,\n        }\n        return self.tx_ls(ls)\n\n    def tx_ls(self, ls: dict[str, Any]) -> bool:\n        dirs = ls[\"dirs\"]\n        files = ls[\"files\"]\n        arg = self.uparam[\"ls\"]\n        if arg in [\"v\", \"t\", \"txt\"]:\n            try:\n                biggest = max(ls[\"files\"] + ls[\"dirs\"], key=itemgetter(\"sz\"))[\"sz\"]\n            except:\n                biggest = 0\n\n            if arg == \"v\":\n                fmt = \"\\033[0;7;36m{{}}{{:>{}}}\\033[0m {{}}\"\n                nfmt = \"{}\"\n                biggest = 0\n                f2 = \"\".join(\n                    \"{}{{}}\".format(x)\n                    for x in [\n                        \"\\033[7m\",\n                        \"\\033[27m\",\n                        \"\",\n                        \"\\033[0;1m\",\n                        \"\\033[0;36m\",\n                        \"\\033[0m\",\n                    ]\n                )\n                ctab = {\"B\": 6, \"K\": 5, \"M\": 1, \"G\": 3}\n                for lst in [dirs, files]:\n                    for x in lst:\n                        a = x[\"dt\"].replace(\"-\", \" \").replace(\":\", \" \").split(\" \")\n                        x[\"dt\"] = f2.format(*list(a))\n                        sz = humansize(x[\"sz\"], True)\n                        x[\"sz\"] = \"\\033[0;3{}m {:>5}\".format(ctab.get(sz[-1:], 0), sz)\n            else:\n                fmt = \"{{}}  {{:{},}}  {{}}\"\n                nfmt = \"{:,}\"\n\n            for x in dirs:\n                n = x[\"name\"] + \"/\"\n                if arg == \"v\":\n                    n = \"\\033[94m\" + n\n\n                x[\"name\"] = n\n\n            fmt = fmt.format(len(nfmt.format(biggest)))\n            retl = [\n                (\"# %s: %s\" % (x, ls[x])).replace(r\"</span> // <span>\", \" // \")\n                for x in [\"acct\", \"perms\", \"srvinf\"]\n                if x in ls\n            ]\n            retl += [\n                fmt.format(x[\"dt\"], x[\"sz\"], x[\"name\"])\n                for y in [dirs, files]\n                for x in y\n            ]\n            ret = \"\\n\".join(retl)\n            mime = \"text/plain; charset=utf-8\"\n        else:\n            [x.pop(k) for k in [\"name\", \"dt\"] for y in [dirs, files] for x in y]\n\n            # nonce (tlnote: norwegian for flake as in snowflake)\n            if self.args.no_fnugg:\n                ls[\"fnugg\"] = \"nei\"\n            elif \"fnugg\" in self.headers:\n                ls[\"fnugg\"] = self.headers[\"fnugg\"]\n\n            ret = json.dumps(ls)\n            mime = \"application/json\"\n\n        ret += \"\\n\\033[0m\" if arg == \"v\" else \"\\n\"\n        self.reply(ret.encode(\"utf-8\", \"replace\"), mime=mime)\n        return True\n\n    def tx_browser(self) -> bool:\n        vpath = \"\"\n        vpnodes = [[\"\", \"/\"]]\n        if self.vpath:\n            for node in self.vpath.split(\"/\"):\n                if not vpath:\n                    vpath = node\n                else:\n                    vpath += \"/\" + node\n\n                vpnodes.append([quotep(vpath) + \"/\", html_escape(node, crlf=True)])\n\n        vn = self.vn\n        rem = self.rem\n        abspath = vn.dcanonical(rem)\n        dbv, vrem = vn.get_dbv(rem)\n\n        try:\n            st = bos.stat(abspath)\n        except:\n            if \"on404\" not in vn.flags:\n                return self.tx_404(not self.can_read)\n\n            ret = self.on40x(vn.flags[\"on404\"], vn, rem)\n            if ret == \"true\":\n                return True\n            elif ret == \"false\":\n                return False\n            elif ret == \"retry\":\n                try:\n                    st = bos.stat(abspath)\n                except:\n                    return self.tx_404(not self.can_read)\n            else:\n                return self.tx_404(not self.can_read)\n\n        if rem.startswith(\".hist/up2k.\") or (\n            rem.endswith(\"/dir.txt\") and rem.startswith(\".hist/th/\")\n        ):\n            raise Pebkac(403)\n\n        e2d = \"e2d\" in vn.flags\n        e2t = \"e2t\" in vn.flags\n\n        add_og = \"og\" in vn.flags\n        if add_og:\n            if \"th\" in self.uparam or \"raw\" in self.uparam or \"opds\" in self.uparam:\n                add_og = False\n            elif vn.flags[\"og_ua\"]:\n                add_og = vn.flags[\"og_ua\"].search(self.ua)\n            og_fn = \"\"\n\n        if \"v\" in self.uparam:\n            add_og = True\n            og_fn = \"\"\n\n        if \"b\" in self.uparam and \"norobots\" not in vn.flags:\n            self.out_headers[\"X-Robots-Tag\"] = \"noindex, nofollow\"\n\n        is_dir = stat.S_ISDIR(st.st_mode)\n        is_dk = False\n        fk_pass = False\n        icur = None\n        if (e2t or e2d) and (is_dir or add_og):\n            idx = self.conn.get_u2idx()\n            if idx and hasattr(idx, \"p_end\"):\n                icur = idx.get_cur(dbv)\n\n        if \"k\" in self.uparam or \"dky\" in vn.flags:\n            if is_dir:\n                use_dirkey = self._use_dirkey(vn, abspath)\n                use_filekey = False\n            else:\n                use_filekey = self._use_filekey(vn, abspath, st)\n                use_dirkey = False\n        else:\n            use_dirkey = use_filekey = False\n\n        th_fmt = self.uparam.get(\"th\")\n        if self.can_read or (\n            self.can_get\n            and (use_filekey or use_dirkey or (not is_dir and \"fk\" not in vn.flags))\n        ):\n            if th_fmt is not None:\n                nothumb = \"dthumb\" in dbv.flags\n                if is_dir:\n                    vrem = vrem.rstrip(\"/\")\n                    if nothumb:\n                        pass\n                    elif icur and vrem:\n                        q = \"select fn from cv where rd=? and dn=?\"\n                        crd, cdn = vrem.rsplit(\"/\", 1) if \"/\" in vrem else (\"\", vrem)\n                        # no mojibake support:\n                        try:\n                            cfn = icur.execute(q, (crd, cdn)).fetchone()\n                            if cfn:\n                                fn = cfn[0]\n                                fp = os.path.join(abspath, fn)\n                                st = bos.stat(fp)\n                                vrem = \"{}/{}\".format(vrem, fn).strip(\"/\")\n                                is_dir = False\n                        except:\n                            pass\n                    else:\n                        for fn in self.args.th_covers:\n                            fp = os.path.join(abspath, fn)\n                            try:\n                                st = bos.stat(fp)\n                                vrem = \"{}/{}\".format(vrem, fn).strip(\"/\")\n                                is_dir = False\n                                break\n                            except:\n                                pass\n\n                    if is_dir:\n                        return self.tx_svg(\"folder\")\n\n                thp = None\n                if self.thumbcli and not nothumb:\n                    try:\n                        thp = self.thumbcli.get(dbv, vrem, int(st.st_mtime), th_fmt)\n                    except Pebkac as ex:\n                        if ex.code == 500 and th_fmt[:1] in \"jw\":\n                            self.log(\"failed to convert [%s]:\\n%s\" % (abspath, ex), 3)\n                            return self.tx_svg(\"--error--\\ncheck\\nserver\\nlog\")\n                        raise\n\n                if thp:\n                    return self.tx_file(\"oh_f\", thp)\n\n                if th_fmt == \"p\":\n                    raise Pebkac(404)\n                elif th_fmt in ACODE2_FMT:\n                    raise Pebkac(415)\n\n                return self.tx_ico(rem)\n\n        elif self.can_write and th_fmt is not None:\n            return self.tx_svg(\"upload\\nonly\")\n\n        if not self.can_read and self.can_get and self.avn:\n            if not self.can_html:\n                pass\n            elif is_dir:\n                for fn in (\"index.htm\", \"index.html\"):\n                    ap2 = os.path.join(abspath, fn)\n                    try:\n                        st2 = bos.stat(ap2)\n                    except:\n                        continue\n\n                    # might as well be extra careful\n                    if not stat.S_ISREG(st2.st_mode):\n                        continue\n\n                    if not self.trailing_slash:\n                        return self.redirect(\n                            self.vpath + \"/\", flavor=\"redirecting to\", use302=True\n                        )\n\n                    fk_pass = True\n                    is_dir = False\n                    add_og = False\n                    rem = vjoin(rem, fn)\n                    vrem = vjoin(vrem, fn)\n                    abspath = ap2\n                    break\n            elif self.vpath.rsplit(\"/\", 1)[-1] in IDX_HTML:\n                fk_pass = True\n\n        if not is_dir and (self.can_read or self.can_get):\n            if (\n                not self.can_read\n                and not fk_pass\n                and \"fk\" in vn.flags\n                and not use_filekey\n                and not self.vpath.startswith(self.args.shr1 or \"\\n\")\n            ):\n                return self.tx_404(True)\n\n            is_md = abspath.lower().endswith(\".md\")\n            if add_og and not is_md:\n                if self.host not in self.headers.get(\"referer\", \"\"):\n                    self.vpath, og_fn = vsplit(self.vpath)\n                    vpath = self.vpath\n                    vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)\n                    abspath = vn.dcanonical(rem)\n                    dbv, vrem = vn.get_dbv(rem)\n                    is_dir = stat.S_ISDIR(st.st_mode)\n                    is_dk = True\n                    vpnodes.pop()\n\n            if (\n                (\n                    (is_md and \"v\" in self.uparam)\n                    or \"edit\" in self.uparam\n                    or \"edit2\" in self.uparam\n                )\n                and \"nohtml\" not in vn.flags\n                and (\n                    is_md\n                    or self.can_delete\n                    or (\n                        \".\" in abspath\n                        and abspath.rsplit(\".\", 1)[1].lower() in vn.flags[\"rw_edit_set\"]\n                    )\n                )\n            ):\n                return self.tx_md(vn, abspath)\n\n            if \"zls\" in self.uparam:\n                return self.tx_zls(abspath)\n            if \"zget\" in self.uparam:\n                return self.tx_zget(abspath)\n\n            if not add_og or not og_fn:\n                if st.st_size or \"nopipe\" in vn.flags:\n                    return self.tx_file(\"oh_f\", abspath, None)\n                else:\n                    return self.tx_file(\"oh_f\", abspath, vn.get_dbv(\"\")[0].realpath)\n\n        elif is_dir and not self.can_read:\n            if use_dirkey:\n                is_dk = True\n            elif self.can_get and \"doc\" in self.uparam:\n                zs = vjoin(self.vpath, self.uparam[\"doc\"]) + \"?v\"\n                return self.redirect(zs, flavor=\"redirecting to\", use302=True)\n            elif not self.can_write:\n                return self.tx_404(True)\n\n        srv_info = []\n\n        try:\n            if not self.args.nih:\n                srv_info.append(self.args.name_html)\n        except:\n            self.log(\"#wow #whoa\")\n\n        zi = vn.flags[\"du_iwho\"]\n        if zi and (\n            zi == 9\n            or (zi == 7 and self.uname != \"*\")\n            or (zi == 5 and self.can_write)\n            or (zi == 4 and self.can_write and self.can_read)\n            or (zi == 3 and self.can_admin)\n        ):\n            free, total, zs = get_df(abspath, False)\n            if total:\n                if \"vmaxb\" in vn.flags:\n                    assert vn.lim  # type: ignore  # !rm\n                    total = vn.lim.vbmax\n                    if free == vn.lim.c_vb_r:\n                        free = min(free, max(0, vn.lim.vbmax - vn.lim.c_vb_v))\n                    else:\n                        try:\n                            zi, _ = self.conn.hsrv.broker.ask(\n                                \"up2k.get_volsizes\", [vn.realpath]\n                            ).get()[0]\n                            vn.lim.c_vb_v = zi\n                            vn.lim.c_vb_r = free\n                            free = min(free, max(0, vn.lim.vbmax - zi))\n                        except:\n                            pass\n                h1 = humansize(free or 0)\n                h2 = humansize(total)\n                srv_info.append(\"{} free of {}\".format(h1, h2))\n            elif zs:\n                self.log(\"diskfree(%r): %s\" % (abspath, zs), 3)\n\n        srv_infot = \"</span> // <span>\".join(srv_info)\n\n        perms = []\n        if self.can_read or is_dk:\n            perms.append(\"read\")\n        if self.can_write:\n            perms.append(\"write\")\n        if self.can_move:\n            perms.append(\"move\")\n        if self.can_delete:\n            perms.append(\"delete\")\n        if self.can_get:\n            perms.append(\"get\")\n        if self.can_upget:\n            perms.append(\"upget\")\n        if self.can_admin:\n            perms.append(\"admin\")\n\n        url_suf = self.urlq({}, [\"k\"])\n        is_ls = \"ls\" in self.uparam\n        is_opds = \"opds\" in self.uparam\n        is_js = self.args.force_js or self.cookies.get(\"js\") == \"y\"\n\n        if not is_ls and not add_og and self.ua.startswith((\"curl/\", \"fetch\")):\n            self.uparam[\"ls\"] = \"v\"\n            is_ls = True\n\n        tpl = \"browser\"\n        if \"b\" in self.uparam:\n            tpl = \"browser2\"\n            is_js = False\n        elif is_opds:\n            # Display directory listing as OPDS v1.2 catalog feed\n            if not (self.args.opds or \"opds\" in self.vn.flags):\n                raise Pebkac(405, \"OPDS is disabled in server config\")\n            if not self.can_read:\n                raise Pebkac(401, \"OPDS requires read permission\")\n            is_js = is_ls = False\n\n        vf = vn.flags\n        ls_ret = {\n            \"dirs\": [],\n            \"files\": [],\n            \"taglist\": [],\n            \"srvinf\": srv_infot,\n            \"acct\": self.uname,\n            \"perms\": perms,\n            \"cfg\": vn.js_ls,\n        }\n        cgv = {\n            \"ls0\": None,\n            \"acct\": self.uname,\n            \"perms\": perms,\n        }\n        # also see `js_htm` in authsrv.py\n        j2a = {\n            \"cgv1\": vn.js_htm,\n            \"cgv\": cgv,\n            \"vpnodes\": vpnodes,\n            \"files\": [],\n            \"ls0\": None,\n            \"taglist\": [],\n            \"have_tags_idx\": int(e2t),\n            \"have_b_u\": (self.can_write and self.uparam.get(\"b\") == \"u\"),\n            \"sb_lg\": vn.js_ls[\"sb_lg\"],\n            \"url_suf\": url_suf,\n            \"title\": html_escape(\"%s %s\" % (self.args.bname, self.vpath), crlf=True),\n            \"srv_info\": srv_infot,\n            \"dtheme\": self.args.theme,\n        }\n\n        if self.args.js_browser:\n            zs = self.args.js_browser\n            zs += \"&\" if \"?\" in zs else \"?\"\n            j2a[\"js\"] = zs\n\n        if self.args.css_browser:\n            zs = self.args.css_browser\n            zs += \"&\" if \"?\" in zs else \"?\"\n            j2a[\"css\"] = zs\n\n        if not self.conn.hsrv.prism:\n            j2a[\"no_prism\"] = True\n\n        if not self.can_read and not is_dk:\n            logues, readmes = self._add_logues(vn, abspath, None)\n            ls_ret[\"logues\"] = j2a[\"logues\"] = logues\n            ls_ret[\"readmes\"] = cgv[\"readmes\"] = readmes\n\n            if is_ls:\n                return self.tx_ls(ls_ret)\n\n            if not stat.S_ISDIR(st.st_mode):\n                return self.tx_404(True)\n\n            if \"zip\" in self.uparam or \"tar\" in self.uparam:\n                raise Pebkac(403)\n\n            zsl = j2a[\"files\"] = []\n            if is_js:\n                j2a[\"ls0\"] = cgv[\"ls0\"] = {\n                    \"dirs\": zsl,\n                    \"files\": zsl,\n                    \"taglist\": zsl,\n                }\n\n            html = self.j2s(tpl, **j2a)\n            self.reply(html.encode(\"utf-8\", \"replace\"))\n            return True\n\n        for k in [\"zip\", \"tar\"]:\n            v = self.uparam.get(k)\n            if v is not None and (not add_og or not og_fn):\n                if is_dk and \"dks\" not in vn.flags:\n                    t = \"server config does not allow download-as-zip/tar; only dk is specified, need dks too\"\n                    raise Pebkac(403, t)\n                return self.tx_zip(k, v, self.vpath, vn, rem, [])\n\n        fsroot, vfs_ls, vfs_virt = vn.ls(\n            rem,\n            self.uname,\n            not self.args.no_scandir,\n            PERMS_rwh,\n            lstat=\"lt\" in self.uparam,\n            throw=True,\n        )\n        stats = {k: v for k, v in vfs_ls}\n        ls_names = [x[0] for x in vfs_ls]\n        ls_names.extend(list(vfs_virt))\n\n        if add_og and og_fn and not self.can_read:\n            ls_names = [og_fn]\n            is_js = True\n\n        # check for old versions of files,\n        # [num-backups, most-recent, hist-path]\n        hist: dict[str, tuple[int, float, str]] = {}\n        try:\n            if vf[\"md_hist\"] != \"s\":\n                raise Exception()\n            histdir = os.path.join(fsroot, \".hist\")\n            ptn = RE_MDV\n            for hfn in bos.listdir(histdir):\n                m = ptn.match(hfn)\n                if not m:\n                    continue\n\n                fn = m.group(1) + m.group(3)\n                n, ts, _ = hist.get(fn, (0, 0, \"\"))\n                hist[fn] = (n + 1, max(ts, float(m.group(2))), hfn)\n        except:\n            pass\n\n        lnames = {x.lower(): x for x in ls_names}\n\n        # show dotfiles if permitted and requested\n        if not self.can_dot or (\n            \"dots\" not in self.uparam and (is_ls or \"dots\" not in self.cookies)\n        ):\n            ls_names = exclude_dotfiles(ls_names)\n\n        add_dk = vf.get(\"dk\")\n        add_fk = vf.get(\"fk\")\n        fk_alg = 2 if \"fka\" in vf else 1\n        if add_dk:\n            if vf.get(\"dky\"):\n                add_dk = False\n            else:\n                zs = self.gen_fk(2, self.args.dk_salt, abspath, 0, 0)[:add_dk]\n                ls_ret[\"dk\"] = cgv[\"dk\"] = zs\n\n        no_zip = bool(self._can_zip(vf))\n\n        dirs = []\n        files = []\n        ptn_hr = RE_HR\n        use_abs_url = (\n            not is_opds\n            and not is_ls\n            and not is_js\n            and not self.trailing_slash\n            and vpath\n        )\n        for fn in ls_names:\n            base = \"\"\n            href = fn\n            if use_abs_url:\n                base = \"/\" + vpath + \"/\"\n                href = base + fn\n\n            if fn in vfs_virt:\n                fspath = vfs_virt[fn].realpath\n            else:\n                fspath = fsroot + \"/\" + fn\n\n            try:\n                linf = stats.get(fn) or bos.lstat(fspath)\n                inf = bos.stat(fspath) if stat.S_ISLNK(linf.st_mode) else linf\n            except:\n                self.log(\"broken symlink: %r\" % (fspath,))\n                continue\n\n            is_dir = stat.S_ISDIR(inf.st_mode)\n            if is_dir:\n                href += \"/\"\n                if no_zip:\n                    margin = \"DIR\"\n                elif add_dk:\n                    zs = absreal(fspath)\n                    margin = '<a href=\"%s?k=%s&zip=crc\" rel=\"nofollow\">zip</a>' % (\n                        quotep(href),\n                        self.gen_fk(2, self.args.dk_salt, zs, 0, 0)[:add_dk],\n                    )\n                else:\n                    margin = '<a href=\"%s?zip=crc\" rel=\"nofollow\">zip</a>' % (\n                        quotep(href),\n                    )\n            elif fn in hist:\n                margin = '<a href=\"%s.hist/%s\" rel=\"nofollow\">#%s</a>' % (\n                    base,\n                    html_escape(hist[fn][2], quot=True, crlf=True),\n                    hist[fn][0],\n                )\n            else:\n                margin = \"-\"\n\n            sz = inf.st_size\n            zd = datetime.fromtimestamp(max(0, linf.st_mtime), UTC)\n            dt = \"%04d-%02d-%02d %02d:%02d:%02d\" % (\n                zd.year,\n                zd.month,\n                zd.day,\n                zd.hour,\n                zd.minute,\n                zd.second,\n            )\n\n            if is_dir:\n                ext = \"---\"\n            elif \".\" in fn:\n                ext = ptn_hr.sub(\"@\", fn.rsplit(\".\", 1)[1])\n                if len(ext) > 16:\n                    ext = ext[:16]\n            else:\n                ext = \"%\"\n\n            if add_fk and not is_dir:\n                href = \"%s?k=%s\" % (\n                    quotep(href),\n                    self.gen_fk(\n                        fk_alg,\n                        self.args.fk_salt,\n                        fspath,\n                        sz,\n                        0 if ANYWIN else inf.st_ino,\n                    )[:add_fk],\n                )\n            elif add_dk and is_dir:\n                href = \"%s?k=%s\" % (\n                    quotep(href),\n                    self.gen_fk(2, self.args.dk_salt, fspath, 0, 0)[:add_dk],\n                )\n            else:\n                href = quotep(href)\n\n            item = {\n                \"lead\": margin,\n                \"href\": href,\n                \"name\": fn,\n                \"sz\": sz,\n                \"ext\": ext,\n                \"dt\": dt,\n                \"ts\": int(linf.st_mtime),\n            }\n            if is_dir:\n                dirs.append(item)\n            else:\n                files.append(item)\n\n        if is_dk and not vf.get(\"dks\"):\n            dirs = []\n\n        if (\n            self.cookies.get(\"idxh\") == \"y\"\n            and \"ls\" not in self.uparam\n            and \"v\" not in self.uparam\n            and not is_opds\n        ):\n            for item in files:\n                if item[\"name\"] in IDX_HTML:\n                    # do full resolve in case of shadowed file\n                    vp = vjoin(self.vpath.split(\"?\")[0], item[\"name\"])\n                    vn, rem = self.asrv.vfs.get(vp, self.uname, True, False)\n                    ap = vn.canonical(rem)\n                    if not self.trailing_slash and bos.path.isfile(ap):\n                        return self.redirect(\n                            self.vpath + \"/\", flavor=\"redirecting to\", use302=True\n                        )\n                    return self.tx_file(\"oh_f\", ap)  # is no-cache\n\n        if icur:\n            mte = vn.flags.get(\"mte\") or {}\n            tagset: set[str] = set()\n            rd = vrem\n            if self.can_admin:\n                up_q = \"select substr(w,1,16), ip, at, un from up where rd=? and fn=?\"\n                up_m = [\"w\", \"up_ip\", \".up_at\", \"up_by\"]\n            else:\n                up_q, up_m = vn.flags[\"ls_q_m\"]\n\n            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'\"\n            for fe in files:\n                fn = fe[\"name\"]\n                erd_efn = (rd, fn)\n                try:\n                    r = icur.execute(mt_q, erd_efn)\n                except Exception as ex:\n                    if \"database is locked\" in str(ex):\n                        break\n\n                    try:\n                        erd_efn = s3enc(idx.mem_cur, rd, fn)\n                        r = icur.execute(mt_q, erd_efn)\n                    except:\n                        self.log(\"tag read error, %r / %r\\n%s\" % (rd, fn, min_ex()))\n                        break\n\n                tags = {k: v for k, v in r}\n\n                if up_q:\n                    try:\n                        up_v = icur.execute(up_q, erd_efn).fetchone()\n                        for zs1, zs2 in zip(up_m, up_v):\n                            if zs2:\n                                tags[zs1] = zs2\n                    except:\n                        pass\n\n                _ = [tagset.add(k) for k in tags]\n                fe[\"tags\"] = tags\n\n            for fe in dirs:\n                fe[\"tags\"] = ODict()\n\n            lmte = list(mte)\n            if self.can_admin:\n                lmte.extend((\"w\", \"up_by\", \"up_ip\", \".up_at\"))\n\n            if \"nodirsz\" not in vf:\n                tagset.add(\".files\")\n                vdir = \"%s/\" % (rd,) if rd else \"\"\n                q = \"select sz, nf from ds where rd=? limit 1\"\n                for fe in dirs:\n                    try:\n                        hit = icur.execute(q, (vdir + fe[\"name\"],)).fetchone()\n                        (fe[\"sz\"], fe[\"tags\"][\".files\"]) = hit\n                    except:\n                        pass  # 404 or mojibake\n\n            taglist = [k for k in lmte if k in tagset]\n        else:\n            taglist = []\n\n        logues, readmes = self._add_logues(vn, abspath, lnames)\n        ls_ret[\"logues\"] = j2a[\"logues\"] = logues\n        ls_ret[\"readmes\"] = cgv[\"readmes\"] = readmes\n\n        if (\n            not files\n            and not dirs\n            and not readmes[0]\n            and not readmes[1]\n            and not logues[0]\n            and not logues[1]\n        ):\n            logues[1] = \"this folder is empty\"\n\n        if \"descript.ion\" in lnames and os.path.isfile(\n            os.path.join(abspath, lnames[\"descript.ion\"])\n        ):\n            rem = []\n            items = {x[\"name\"].lower(): x for x in files + dirs}\n            with open(os.path.join(abspath, lnames[\"descript.ion\"]), \"rb\") as f:\n                for bln in [x.strip() for x in f]:\n                    try:\n                        if bln.endswith(b\"\\x04\\xc2\"):\n                            # multiline comment; replace literal r\"\\n\" with \" // \"\n                            bln = bln.replace(br\"\\\\n\", b\" // \")[:-2]\n                        ln = bln.decode(\"utf-8\", \"replace\")\n                        if ln.startswith('\"'):\n                            fn, desc = ln.split('\" ', 1)\n                            fn = fn[1:]\n                        else:\n                            fn, desc = ln.split(\" \", 1)\n                        try:\n                            item = items[fn.lower().strip(\"/\")]\n                        except:\n                            t = \"<li><code>%s</code> %s</li>\"\n                            rem.append(t % (html_escape(fn), html_escape(desc)))\n                            continue\n                        try:\n                            item[\"tags\"][\"descript.ion\"] = desc\n                        except:\n                            item[\"tags\"] = {\"descript.ion\": desc}\n                    except:\n                        pass\n            if \"descript.ion\" not in taglist:\n                taglist.insert(0, \"descript.ion\")\n            if rem and not logues[1]:\n                t = \"<h3>descript.ion</h3><ul>\\n\"\n                logues[1] = t + \"\\n\".join(rem) + \"</ul>\"\n\n        if is_ls:\n            ls_ret[\"dirs\"] = dirs\n            ls_ret[\"files\"] = files\n            ls_ret[\"taglist\"] = taglist\n            return self.tx_ls(ls_ret)\n\n        doc = self.uparam.get(\"doc\") if self.can_read else None\n        if doc:\n            zp = self.args.ua_nodoc\n            if zp and zp.search(self.ua):\n                t = \"this URL contains no valuable information for bots/crawlers\"\n                raise Pebkac(403, t)\n            j2a[\"docname\"] = doc\n            doctxt = None\n            dfn = lnames.get(doc.lower())\n            if dfn and dfn != doc:\n                # found Foo but want FOO\n                dfn = next((x for x in files if x[\"name\"] == doc), None)\n            if dfn:\n                docpath = os.path.join(abspath, doc)\n                sz = bos.path.getsize(docpath)\n                if sz < 1024 * self.args.txt_max:\n                    doctxt = read_utf8(self.log, fsenc(docpath), False)\n                    if doc.lower().endswith(\".md\") and \"exp\" in vn.flags:\n                        doctxt = self._expand(doctxt, vn.flags.get(\"exp_md\") or [])\n                else:\n                    self.log(\"doc 2big: %r\" % (doc,), 6)\n                    doctxt = \"( size of textfile exceeds serverside limit )\"\n                    # NOTE: browser.js expects this exact message\n            else:\n                self.log(\"doc 404: %r\" % (doc,), 6)\n                doctxt = \"( textfile not found )\"\n\n            if doctxt is not None:\n                j2a[\"doc\"] = doctxt\n\n        for d in dirs:\n            d[\"name\"] += \"/\"\n\n        dirs.sort(key=itemgetter(\"name\"))\n\n        if is_opds:\n            # OpenSearch Description format requires a full-qualified URL and a \"Short Name\" under 16 characters\n            # which will be the longname truncated in the template.\n            # Relevant specs:\n            # https://specs.opds.io/opds-1.2#3-search\n            # https://developer.mozilla.org/en-US/docs/Web/XML/Guides/OpenSearch\n            if \"osd\" in self.uparam:\n                j2a[\"longname\"] = \"%s %s\" % (self.args.bname, self.vpath)\n                j2a[\"search_url\"] = self.args.SRS + vpath\n\n                xml = self.j2s(\"opds_osd\", **j2a)\n                self.reply(\n                    xml.encode(\"utf-8\"), mime=\"application/opensearchdescription+xml\"\n                )\n                return True\n\n            if \"q\" in self.uparam:\n                q = self.uparam[\"q\"]\n                idx = self.conn.get_u2idx()\n                if not idx:\n                    raise Pebkac(500, \"indexer not available\")\n\n                # generate a raw query similar to web interface for multiple words\n                r = \" and \".join((\"name like *%s*\" % (x,)) for x in q.split())\n\n                hits, _, _ = idx.search(self.uname, [self.vn], r, 1000)\n\n                files = []\n                dirs = []\n\n                prefix = quotep(vpath + \"/\" if vpath else \"\")\n\n                for h in hits:\n                    rp = h[\"rp\"]\n                    if not rp.startswith(prefix):\n                        continue\n\n                    zd = datetime.fromtimestamp(h[\"ts\"], UTC)\n                    dt = \"%04d-%02d-%02d %02d:%02d:%02d\" % (\n                        zd.year,\n                        zd.month,\n                        zd.day,\n                        zd.hour,\n                        zd.minute,\n                        zd.second,\n                    )\n\n                    item = {\n                        \"href\": self.args.SRS + rp,\n                        \"name\": unquotep(rp[len(prefix) :].split(\"?\")[0]),\n                        \"sz\": h[\"sz\"],\n                        \"dt\": dt,\n                        \"ts\": h[\"ts\"],\n                    }\n                    files.append(item)\n\n            # exclude files which don't match --opds-exts\n            allowed_exts = vf.get(\"opds_exts\") or self.args.opds_exts\n            if allowed_exts:\n                files = [\n                    x for x in files if x[\"name\"].rsplit(\".\", 1)[-1] in allowed_exts\n                ]\n\n            j2a[\"opds_osd\"] = \"%s%s?opds&osd\" % (self.args.SRS, quotep(vpath))\n\n            for item in dirs:\n                href = item[\"href\"]\n                href += (\"&\" if \"?\" in href else \"?\") + \"opds\"\n                item[\"href\"] = href\n                item[\"iso8601\"] = \"%sZ\" % (item[\"dt\"].replace(\" \", \"T\"),)\n\n            for item in files:\n                href = item[\"href\"]\n                href += (\"&\" if \"?\" in href else \"?\") + \"dl\"\n                item[\"href\"] = href\n                item[\"iso8601\"] = \"%sZ\" % (item[\"dt\"].replace(\" \", \"T\"),)\n\n                if \"rmagic\" in self.vn.flags:\n                    ap = \"%s/%s\" % (fsroot, item[\"name\"])\n                    item[\"mime\"] = guess_mime(item[\"name\"], ap)\n                else:\n                    item[\"mime\"] = guess_mime(item[\"name\"])\n\n                # Make sure we can actually generate JPEG thumbnails\n                if (\n                    not self.args.th_no_jpg\n                    and self.thumbcli\n                    and \"dthumb\" not in dbv.flags\n                    and \"dithumb\" not in dbv.flags\n                ):\n                    item[\"jpeg_thumb_href\"] = href + \"&th=jf\"\n                    item[\"jpeg_thumb_href_hires\"] = item[\"jpeg_thumb_href\"] + \"3\"\n\n            j2a[\"files\"] = files\n            j2a[\"dirs\"] = dirs\n            html = self.j2s(\"opds\", **j2a)\n            mime = \"application/atom+xml;profile=opds-catalog\"\n            self.reply(html.encode(\"utf-8\", \"replace\"), mime=mime)\n            return True\n\n        if is_js:\n            j2a[\"ls0\"] = cgv[\"ls0\"] = {\n                \"dirs\": dirs,\n                \"files\": files,\n                \"taglist\": taglist,\n            }\n            j2a[\"files\"] = []\n        else:\n            j2a[\"files\"] = dirs + files\n\n        j2a[\"taglist\"] = taglist\n\n        if add_og and \"raw\" not in self.uparam:\n            j2a[\"this\"] = self\n            cgv[\"og_fn\"] = og_fn\n            if og_fn and vn.flags.get(\"og_tpl\"):\n                tpl = vn.flags[\"og_tpl\"]\n                if \"EXT\" in tpl:\n                    zs = og_fn.split(\".\")[-1].lower()\n                    tpl2 = tpl.replace(\"EXT\", zs)\n                    if os.path.exists(tpl2):\n                        tpl = tpl2\n                with self.conn.hsrv.mutex:\n                    if tpl not in self.conn.hsrv.j2:\n                        tdir, tname = os.path.split(tpl)\n                        j2env = jinja2.Environment()\n                        j2env.loader = jinja2.FileSystemLoader(tdir)\n                        self.conn.hsrv.j2[tpl] = j2env.get_template(tname)\n            thumb = \"\"\n            is_pic = is_vid = is_au = False\n            for fn in self.args.th_coversd:\n                if fn in lnames:\n                    thumb = lnames[fn]\n                    break\n            if og_fn:\n                ext = og_fn.split(\".\")[-1].lower()\n                if self.thumbcli and ext in self.thumbcli.thumbable:\n                    is_pic = (\n                        ext in self.thumbcli.fmt_pil\n                        or ext in self.thumbcli.fmt_vips\n                        or ext in self.thumbcli.fmt_ffi\n                    )\n                    is_vid = ext in self.thumbcli.fmt_ffv\n                    is_au = ext in self.thumbcli.fmt_ffa\n                    if not thumb or not is_au:\n                        thumb = og_fn\n                file = next((x for x in files if x[\"name\"] == og_fn), None)\n            else:\n                file = None\n\n            url_base = \"%s://%s/%s\" % (\n                \"https\" if self.is_https else \"http\",\n                self.host,\n                self.args.RS + quotep(vpath),\n            )\n            j2a[\"og_is_pic\"] = is_pic\n            j2a[\"og_is_vid\"] = is_vid\n            j2a[\"og_is_au\"] = is_au\n            if thumb:\n                fmt = vn.flags.get(\"og_th\", \"j\")\n                th_base = ujoin(url_base, quotep(thumb))\n                query = \"th=%s&cache\" % (fmt,)\n                if use_filekey:\n                    query += \"&k=\" + self.uparam[\"k\"]\n                query = ub64enc(query.encode(\"utf-8\")).decode(\"ascii\")\n                # discord looks at file extension, not content-type...\n                query += \"/th.jpg\" if \"j\" in fmt else \"/th.webp\"\n                j2a[\"og_thumb\"] = \"%s/.uqe/%s\" % (th_base, query)\n\n            j2a[\"og_fn\"] = og_fn\n            j2a[\"og_file\"] = file\n            if og_fn:\n                og_fn_q = quotep(og_fn)\n                query = \"raw\"\n                if use_filekey:\n                    query += \"&k=\" + self.uparam[\"k\"]\n                query = ub64enc(query.encode(\"utf-8\")).decode(\"ascii\")\n                query += \"/%s\" % (og_fn_q,)\n                j2a[\"og_url\"] = ujoin(url_base, og_fn_q)\n                j2a[\"og_raw\"] = j2a[\"og_url\"] + \"/.uqe/\" + query\n            else:\n                j2a[\"og_url\"] = j2a[\"og_raw\"] = url_base\n\n            if not vn.flags.get(\"og_no_head\"):\n                ogh = {\"twitter:card\": \"summary\"}\n\n                title = str(vn.flags.get(\"og_title\") or \"\")\n\n                if thumb:\n                    ogh[\"og:image\"] = j2a[\"og_thumb\"]\n\n                zso = vn.flags.get(\"og_desc\") or \"\"\n                if zso != \"-\":\n                    ogh[\"og:description\"] = str(zso)\n\n                zs = vn.flags.get(\"og_site\") or self.args.name\n                if zs not in (\"\", \"-\"):\n                    ogh[\"og:site_name\"] = zs\n\n                try:\n                    assert file is not None  # type: ignore  # !rm\n                    zs1, zs2 = file[\"tags\"][\"res\"].split(\"x\")\n                    file[\"tags\"][\".resw\"] = zs1\n                    file[\"tags\"][\".resh\"] = zs2\n                except:\n                    pass\n\n                tagmap = {}\n\n                if is_au:\n                    title = str(vn.flags.get(\"og_title_a\") or \"\")\n                    ogh[\"og:type\"] = \"music.song\"\n                    ogh[\"og:audio\"] = j2a[\"og_raw\"]\n                    tagmap = {\n                        \"artist\": \"og:music:musician\",\n                        \"album\": \"og:music:album\",\n                        \".dur\": \"og:music:duration\",\n                    }\n                elif is_vid:\n                    title = str(vn.flags.get(\"og_title_v\") or \"\")\n                    ogh[\"og:type\"] = \"video.other\"\n                    ogh[\"og:video\"] = j2a[\"og_raw\"]\n\n                    tagmap = {\n                        \"title\": \"og:title\",\n                        \".dur\": \"og:video:duration\",\n                        \".resw\": \"og:video:width\",\n                        \".resh\": \"og:video:height\",\n                    }\n                elif is_pic:\n                    title = str(vn.flags.get(\"og_title_i\") or \"\")\n                    ogh[\"twitter:card\"] = \"summary_large_image\"\n                    ogh[\"twitter:image\"] = ogh[\"og:image\"] = j2a[\"og_raw\"]\n\n                    tagmap = {\n                        \".resw\": \"og:image:width\",\n                        \".resh\": \"og:image:height\",\n                    }\n\n                try:\n                    assert file is not None  # type: ignore  # !rm\n                    for k, v in file[\"tags\"].items():\n                        zs = \"{{ %s }}\" % (k,)\n                        title = title.replace(zs, str(v))\n                except:\n                    pass\n                title = re.sub(r\"\\{\\{ [^}]+ \\}\\}\", \"\", title)\n                while title.startswith(\" - \"):\n                    title = title[3:]\n                while title.endswith(\" - \"):\n                    title = title[:3]\n\n                if vn.flags.get(\"og_s_title\") or not title:\n                    title = str(vn.flags.get(\"og_title\") or \"\")\n\n                for tag, hname in tagmap.items():\n                    try:\n                        assert file is not None  # type: ignore  # !rm\n                        v = file[\"tags\"][tag]\n                        if not v:\n                            continue\n                        ogh[hname] = int(v) if tag == \".dur\" else v\n                    except:\n                        pass\n\n                ogh[\"og:title\"] = title\n\n                oghs = [\n                    '\\t<meta property=\"%s\" content=\"%s\">'\n                    % (k, html_escape(str(v), True, True))\n                    for k, v in ogh.items()\n                ]\n                zs = self.html_head + \"\\n%s\\n\" % (\"\\n\".join(oghs),)\n                self.html_head = zs.replace(\"\\n\\n\", \"\\n\")\n\n        html = self.j2s(tpl, **j2a)\n        self.reply(html.encode(\"utf-8\", \"replace\"))\n        return True\n"
  },
  {
    "path": "copyparty/httpconn.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport argparse  # typechk\nimport os\nimport re\nimport socket\nimport threading  # typechk\nimport time\n\ntry:\n    if os.environ.get(\"PRTY_NO_TLS\"):\n        raise Exception()\n\n    HAVE_SSL = True\n    import ssl\nexcept:\n    HAVE_SSL = False\n\nfrom . import util as Util\nfrom .__init__ import TYPE_CHECKING, EnvParams\nfrom .authsrv import AuthSrv  # typechk\nfrom .httpcli import HttpCli\nfrom .ico import Ico\nfrom .mtag import HAVE_FFMPEG\nfrom .th_cli import ThumbCli\nfrom .th_srv import HAVE_PIL, HAVE_VIPS\nfrom .u2idx import U2idx\nfrom .util import HMaccas, NetMap, shut_socket\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Optional, Pattern, Union\n\nif TYPE_CHECKING:\n    from .httpsrv import HttpSrv\n\n\nPTN_HTTP = re.compile(br\"[A-Z]{3}[A-Z ]\")\n\n\nclass HttpConn(object):\n    \"\"\"\n    spawned by HttpSrv to handle an incoming client connection,\n    creates an HttpCli for each request (Connection: Keep-Alive)\n    \"\"\"\n\n    def __init__(\n        self, sck: socket.socket, addr: tuple[str, int], hsrv: \"HttpSrv\"\n    ) -> None:\n        self.s = sck\n        self.sr: Optional[Util._Unrecv] = None\n        self.cli: Optional[HttpCli] = None\n        self.addr = addr\n        self.hsrv = hsrv\n\n        self.u2mutex: threading.Lock = hsrv.u2mutex  # mypy404\n        self.args: argparse.Namespace = hsrv.args  # mypy404\n        self.E: EnvParams = self.args.E\n        self.asrv: AuthSrv = hsrv.asrv  # mypy404\n        self.u2fh: Util.FHC = hsrv.u2fh  # mypy404\n        self.pipes: Util.CachedDict = hsrv.pipes  # mypy404\n        self.ipu_iu: Optional[dict[str, str]] = hsrv.ipu_iu\n        self.ipu_nm: Optional[NetMap] = hsrv.ipu_nm\n        self.ipa_nm: Optional[NetMap] = hsrv.ipa_nm\n        self.ipar_nm: Optional[NetMap] = hsrv.ipar_nm\n        self.xff_nm: Optional[NetMap] = hsrv.xff_nm\n        self.xff_lan: NetMap = hsrv.xff_lan  # type: ignore\n        self.iphash: HMaccas = hsrv.broker.iphash\n        self.bans: dict[str, int] = hsrv.bans\n        self.aclose: dict[str, int] = hsrv.aclose\n\n        enth = (HAVE_PIL or HAVE_VIPS or HAVE_FFMPEG) and not self.args.no_thumb\n        self.thumbcli: Optional[ThumbCli] = ThumbCli(hsrv) if enth else None  # mypy404\n        self.ico: Ico = Ico(self.args)  # mypy404\n\n        self.t0: float = time.time()  # mypy404\n        self.freshen_pwd: float = 0.0\n        self.stopping = False\n        self.nreq: int = -1  # mypy404\n        self.nbyte: int = 0  # mypy404\n        self.u2idx: Optional[U2idx] = None\n        self.log_func: \"Util.RootLogger\" = hsrv.log  # mypy404\n        self.log_src: str = \"httpconn\"  # mypy404\n        self.lf_url: Optional[Pattern[str]] = (\n            re.compile(self.args.lf_url) if self.args.lf_url else None\n        )  # mypy404\n        self.set_rproxy()\n\n    def shutdown(self) -> None:\n        self.stopping = True\n        try:\n            shut_socket(self.log, self.s, 1)\n        except:\n            pass\n\n    def set_rproxy(self, ip: Optional[str] = None) -> str:\n        if ip is None:\n            color = 36\n            ip = self.addr[0]\n            self.rproxy = None\n        else:\n            color = 34\n            self.rproxy = ip\n\n        self.ip = ip\n        self.log_src = (\"%s \\033[%dm%d\" % (ip, color, self.addr[1])).ljust(26)\n        return self.log_src\n\n    def log(self, msg: str, c: Union[int, str] = 0) -> None:\n        self.log_func(self.log_src, msg, c)\n\n    def get_u2idx(self) -> Optional[U2idx]:\n        # grab from a pool of u2idx instances;\n        # sqlite3 fully parallelizes under python threads\n        # but avoid running out of FDs by creating too many\n        if not self.u2idx:\n            self.u2idx = self.hsrv.get_u2idx(str(self.addr))\n\n        return self.u2idx\n\n    def _detect_https(self) -> bool:\n        try:\n            method = self.s.recv(4, socket.MSG_PEEK)\n        except socket.timeout:\n            return False\n        except AttributeError:\n            # jython does not support msg_peek; forget about https\n            method = self.s.recv(4)\n            self.sr = Util.Unrecv(self.s, self.log)\n            self.sr.buf = method\n\n            # jython used to do this, they stopped since it's broken\n            # but reimplementing sendall is out of scope for now\n            if not getattr(self.s, \"sendall\", None):\n                self.s.sendall = self.s.send  # type: ignore\n\n        if len(method) != 4:\n            err = \"need at least 4 bytes in the first packet; got {}\".format(\n                len(method)\n            )\n            if method:\n                self.log(err)\n\n            self.s.send(b\"HTTP/1.1 400 Bad Request\\r\\n\\r\\n\" + err.encode(\"utf-8\"))\n            return False\n\n        return not method or not bool(PTN_HTTP.match(method))\n\n    def run(self) -> None:\n        self.s.settimeout(10)\n\n        self.sr = None\n        if self.args.https_only:\n            is_https = True\n        elif self.args.http_only:\n            is_https = False\n        else:\n            # raise Exception(\"asdf\")\n            is_https = self._detect_https()\n\n        if is_https:\n            if self.sr:\n                self.log(\"TODO: cannot do https in jython\", c=\"1;31\")\n                return\n\n            self.log_src = self.log_src.replace(\"[36m\", \"[35m\")\n            try:\n                assert ssl  # type: ignore  # !rm\n                ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)\n                ctx.load_cert_chain(self.args.cert)\n                if self.args.ssl_ver:\n                    ctx.options &= ~self.args.ssl_flags_en\n                    ctx.options |= self.args.ssl_flags_de\n                    # print(repr(ctx.options))\n\n                if self.args.ssl_log:\n                    try:\n                        ctx.keylog_filename = self.args.ssl_log\n                    except:\n                        self.log(\"keylog failed; openssl or python too old\")\n\n                if self.args.ciphers:\n                    ctx.set_ciphers(self.args.ciphers)\n\n                self.s = ctx.wrap_socket(self.s, server_side=True)\n                msg = [\n                    \"\\033[1;3%dm%s\" % (c, s)\n                    for c, s in zip([0, 5, 0], self.s.cipher())  # type: ignore\n                ]\n                self.log(\" \".join(msg) + \"\\033[0m\")\n\n                if self.args.ssl_dbg and hasattr(self.s, \"shared_ciphers\"):\n                    ciphers = self.s.shared_ciphers()\n                    assert ciphers  # !rm\n                    overlap = [str(y[::-1]) for y in ciphers]\n                    self.log(\"TLS cipher overlap:\" + \"\\n\".join(overlap))\n                    for k, v in [\n                        [\"compression\", self.s.compression()],\n                        [\"ALPN proto\", self.s.selected_alpn_protocol()],\n                        [\"NPN proto\", self.s.selected_npn_protocol()],\n                    ]:\n                        self.log(\"TLS {}: {}\".format(k, v or \"nah\"))\n\n            except Exception as ex:\n                em = str(ex)\n\n                if \"ALERT_CERTIFICATE_UNKNOWN\" in em:\n                    # android-chrome keeps doing this\n                    pass\n\n                else:\n                    self.log(\"handshake\\033[0m \" + em, c=5)\n\n                return\n\n        if not self.sr:\n            self.sr = Util.Unrecv(self.s, self.log)\n\n        while not self.stopping:\n            self.nreq += 1\n            self.cli = HttpCli(self)\n            if not self.cli.run():\n                return\n\n            if self.sr.te == 1:\n                self.log(\"closing socket (leftover TE)\", \"90\")\n                return\n\n            if (\n                \"content-length\" in self.cli.headers\n                and int(self.cli.headers[\"content-length\"]) != self.sr.nb\n            ):\n                self.log(\"closing socket (CL mismatch)\", \"90\")\n                return\n\n            # note: proxies reject PUT sans Content-Length; illegal for HTTP/1.1\n\n            self.sr.nb = self.sr.te = 0\n\n            if self.u2idx:\n                self.hsrv.put_u2idx(str(self.addr), self.u2idx)\n                self.u2idx = None\n\n            if self.rproxy:\n                self.set_rproxy()\n"
  },
  {
    "path": "copyparty/httpsrv.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport hashlib\nimport math\nimport os\nimport re\nimport socket\nimport sys\nimport threading\nimport time\n\nimport queue\n\nfrom .__init__ import ANYWIN, CORES, EXE, MACOS, PY2, TYPE_CHECKING, EnvParams, unicode\n\ntry:\n    MNFE = ModuleNotFoundError\nexcept:\n    MNFE = ImportError\n\ntry:\n    import jinja2\nexcept MNFE:\n    if EXE:\n        raise\n\n    print(\n        \"\"\"\\033[1;31m\n  you do not have jinja2 installed,\\033[33m\n  choose one of these:\\033[0m\n   * apt install python-jinja2\n   * {} -m pip install --user jinja2\n   * (try another python version, if you have one)\n   * (try copyparty.sfx instead)\n\"\"\".format(\n            sys.executable\n        )\n    )\n    sys.exit(1)\nexcept SyntaxError:\n    if EXE:\n        raise\n\n    print(\n        \"\"\"\\033[1;31m\n  your jinja2 version is incompatible with your python version;\\033[33m\n  please try to replace it with an older version:\\033[0m\n   * {} -m pip install --user jinja2==2.11.3\n   * (try another python version, if you have one)\n   * (try copyparty.sfx instead)\n\"\"\".format(\n            sys.executable\n        )\n    )\n    sys.exit(1)\n\nfrom .httpconn import HttpConn\nfrom .metrics import Metrics\nfrom .u2idx import U2idx\nfrom .util import (\n    E_SCK,\n    FHC,\n    CachedDict,\n    Daemon,\n    Garda,\n    Magician,\n    Netdev,\n    NetMap,\n    build_netmap,\n    has_resource,\n    ipnorm,\n    load_ipr,\n    load_ipu,\n    load_resource,\n    min_ex,\n    shut_socket,\n    spack,\n    start_log_thrs,\n    start_stackmon,\n    ub64enc,\n)\n\nif TYPE_CHECKING:\n    from .authsrv import VFS\n    from .broker_util import BrokerCli\n    from .ssdp import SSDPr\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Any, Optional\n\nif PY2:\n    range = xrange  # type: ignore\n\nif not hasattr(socket, \"AF_UNIX\"):\n    setattr(socket, \"AF_UNIX\", -9001)\n\n\ndef load_jinja2_resource(E: EnvParams, name: str):\n    with load_resource(E, \"web/\" + name, \"r\") as f:\n        return f.read()\n\n\nclass HttpSrv(object):\n    \"\"\"\n    handles incoming connections using HttpConn to process http,\n    relying on MpSrv for performance (HttpSrv is just plain threads)\n    \"\"\"\n\n    def __init__(self, broker: \"BrokerCli\", nid: Optional[int]) -> None:\n        self.broker = broker\n        self.nid = nid\n        self.args = broker.args\n        self.E: EnvParams = self.args.E\n        self.log = broker.log\n        self.asrv = broker.asrv\n\n        # redefine in case of multiprocessing\n        socket.setdefaulttimeout(120)\n\n        self.t0 = time.time()\n        nsuf = \"-n{}-i{:x}\".format(nid, os.getpid()) if nid else \"\"\n        self.magician = Magician()\n        self.nm = NetMap([], [])\n        self.ssdp: Optional[\"SSDPr\"] = None\n        self.gpwd = Garda(self.args.ban_pw)\n        self.gpwc = Garda(self.args.ban_pwc)\n        self.g404 = Garda(self.args.ban_404)\n        self.g403 = Garda(self.args.ban_403)\n        self.g422 = Garda(self.args.ban_422, False)\n        self.gmal = Garda(self.args.ban_422)\n        self.gurl = Garda(self.args.ban_url)\n        self.bans: dict[str, int] = {}\n        self.aclose: dict[str, int] = {}\n\n        dli: dict[str, tuple[float, int, \"VFS\", str, str]] = {}  # info\n        dls: dict[str, tuple[float, int]] = {}  # state\n        self.dli = self.tdli = dli\n        self.dls = self.tdls = dls\n        self.iiam = '<img src=\"%s.cpr/w/iiam.gif?cache=i\" />' % (self.args.SRS,)\n\n        self.bound: set[tuple[str, int]] = set()\n        self.name = \"hsrv\" + nsuf\n        self.mutex = threading.Lock()\n        self.u2mutex = threading.Lock()\n        self.bad_ver = False\n        self.stopping = False\n\n        self.tp_nthr = 0  # actual\n        self.tp_ncli = 0  # fading\n        self.tp_time = 0.0  # latest worker collect\n        self.tp_q: Optional[queue.LifoQueue[Any]] = (\n            None if self.args.no_htp else queue.LifoQueue()\n        )\n        self.t_periodic: Optional[threading.Thread] = None\n\n        self.u2fh = FHC()\n        self.u2sc: dict[str, tuple[int, \"hashlib._Hash\"]] = {}\n        self.pipes = CachedDict(0.2)\n        self.metrics = Metrics(self)\n        self.nreq = 0\n        self.nsus = 0\n        self.nban = 0\n        self.srvs: list[socket.socket] = []\n        self.ncli = 0  # exact\n        self.clients: set[HttpConn] = set()  # laggy\n        self.nclimax = 0\n        self.cb_ts = 0.0\n        self.cb_v = \"\"\n\n        self.u2idx_free: dict[str, U2idx] = {}\n        self.u2idx_n = 0\n\n        assert jinja2  # type: ignore  # !rm\n        env = jinja2.Environment()\n        env.loader = jinja2.FunctionLoader(lambda f: load_jinja2_resource(self.E, f))\n        jn = [\n            \"browser\",\n            \"browser2\",\n            \"cf\",\n            \"idp\",\n            \"md\",\n            \"mde\",\n            \"msg\",\n            \"rups\",\n            \"shares\",\n            \"splash\",\n            \"svcs\",\n        ]\n        self.j2 = {x: env.get_template(x + \".html\") for x in jn}\n        self.j2[\"opds\"] = env.get_template(\"opds.xml\")\n        self.j2[\"opds_osd\"] = env.get_template(\"opds_osd.xml\")\n        self.prism = has_resource(self.E, \"web/deps/prism.js.gz\")\n\n        if self.args.ipu:\n            self.ipu_iu, self.ipu_nm = load_ipu(self.log, self.args.ipu)\n        else:\n            self.ipu_iu = self.ipu_nm = None\n\n        if self.args.ipr:\n            self.ipr = load_ipr(self.log, self.args.ipr)\n        else:\n            self.ipr = None\n\n        self.ipa_nm = build_netmap(self.args.ipa)\n        self.ipar_nm = build_netmap(self.args.ipar)\n        self.xff_nm = build_netmap(self.args.xff_src)\n        self.xff_lan = build_netmap(\"lan\")\n\n        self.mallow = \"GET HEAD POST PUT DELETE OPTIONS\".split()\n        if not self.args.no_dav:\n            zs = \"PROPFIND PROPPATCH LOCK UNLOCK MKCOL COPY MOVE\"\n            self.mallow += zs.split()\n\n        if self.args.zs:\n            from .ssdp import SSDPr\n\n            self.ssdp = SSDPr(broker)\n\n        if self.tp_q:\n            self.start_threads(4)\n\n        if nid:\n            self.tdli = {}\n            self.tdls = {}\n\n            if self.args.stackmon:\n                start_stackmon(self.args.stackmon, nid)\n\n            if self.args.log_thrs:\n                start_log_thrs(self.log, self.args.log_thrs, nid)\n\n        self.th_cfg: dict[str, set[str]] = {}\n        Daemon(self.post_init, \"hsrv-init2\")\n\n    def post_init(self) -> None:\n        try:\n            x = self.broker.ask(\"thumbsrv.getcfg\")\n            self.th_cfg = x.get()\n        except:\n            pass\n\n    def set_bad_ver(self) -> None:\n        self.bad_ver = True\n\n    def set_netdevs(self, netdevs: dict[str, Netdev]) -> None:\n        ips = set()\n        for ip, _ in self.bound:\n            ips.add(ip)\n\n        self.nm = NetMap(list(ips), list(netdevs))\n\n    def start_threads(self, n: int) -> None:\n        self.tp_nthr += n\n        if self.args.log_htp:\n            self.log(self.name, \"workers += {} = {}\".format(n, self.tp_nthr), 6)\n\n        for _ in range(n):\n            Daemon(self.thr_poolw, self.name + \"-poolw\")\n\n    def stop_threads(self, n: int) -> None:\n        self.tp_nthr -= n\n        if self.args.log_htp:\n            self.log(self.name, \"workers -= {} = {}\".format(n, self.tp_nthr), 6)\n\n        assert self.tp_q  # !rm\n        for _ in range(n):\n            self.tp_q.put(None)\n\n    def periodic(self) -> None:\n        while True:\n            time.sleep(2 if self.tp_ncli or self.ncli else 10)\n            with self.u2mutex, self.mutex:\n                self.u2fh.clean()\n                if self.tp_q:\n                    self.tp_ncli = max(self.ncli, self.tp_ncli - 2)\n                    if self.tp_nthr > self.tp_ncli + 8:\n                        self.stop_threads(4)\n\n                if not self.ncli and not self.u2fh.cache and self.tp_nthr <= 8:\n                    self.t_periodic = None\n                    return\n\n    def listen(self, sck: socket.socket, nlisteners: int) -> None:\n        tcp = sck.family != socket.AF_UNIX\n\n        if self.args.j != 1:\n            # lost in the pickle; redefine\n            if not ANYWIN or self.args.reuseaddr:\n                sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n\n            if tcp:\n                sck.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)\n\n            sck.settimeout(None)  # < does not inherit, ^ opts above do\n\n        if tcp:\n            ip, port = sck.getsockname()[:2]\n        else:\n            ip = re.sub(r\"\\.[0-9]+$\", \"\", sck.getsockname().split(\"/\")[-1])\n            port = 0\n\n        self.srvs.append(sck)\n        self.bound.add((ip, port))\n        self.nclimax = math.ceil(self.args.nc * 1.0 / nlisteners)\n        Daemon(\n            self.thr_listen,\n            \"httpsrv-n{}-listen-{}-{}\".format(self.nid or \"0\", ip, port),\n            (sck,),\n        )\n\n    def thr_listen(self, srv_sck: socket.socket) -> None:\n        \"\"\"listens on a shared tcp server\"\"\"\n        fno = srv_sck.fileno()\n        if srv_sck.family == socket.AF_UNIX:\n            ip = re.sub(r\"\\.[0-9]+$\", \"\", srv_sck.getsockname())\n            msg = \"subscribed @ %s  f%d p%d\" % (ip, fno, os.getpid())\n            ip = ip.split(\"/\")[-1]\n            port = 0\n            tcp = False\n        else:\n            tcp = True\n            ip, port = srv_sck.getsockname()[:2]\n            hip = \"[%s]\" % (ip,) if \":\" in ip else ip\n            msg = \"subscribed @ %s:%d  f%d p%d\" % (hip, port, fno, os.getpid())\n\n        self.log(self.name, msg)\n\n        Daemon(self.broker.say, \"sig-hsrv-up1\", (\"cb_httpsrv_up\",))\n\n        saddr = (\"\", 0)  # fwd-decl for `except TypeError as ex:`\n\n        while not self.stopping:\n            if self.args.log_conn:\n                self.log(self.name, \"|%sC-ncli\" % (\"-\" * 1,), c=\"90\")\n\n            spins = 0\n            while self.ncli >= self.nclimax:\n                if not spins:\n                    t = \"at connection limit (global-option 'nc'); waiting\"\n                    self.log(self.name, t, 3)\n\n                spins += 1\n                time.sleep(0.1)\n                if spins != 50 or not self.args.aclose:\n                    continue\n\n                ipfreq: dict[str, int] = {}\n                with self.mutex:\n                    for c in self.clients:\n                        ip = ipnorm(c.ip)\n                        try:\n                            ipfreq[ip] += 1\n                        except:\n                            ipfreq[ip] = 1\n\n                ip, n = sorted(ipfreq.items(), key=lambda x: x[1], reverse=True)[0]\n                if n < self.nclimax / 2:\n                    continue\n\n                self.aclose[ip] = int(time.time() + self.args.aclose * 60)\n                nclose = 0\n                nloris = 0\n                nconn = 0\n                with self.mutex:\n                    for c in self.clients:\n                        cip = ipnorm(c.ip)\n                        if ip != cip:\n                            continue\n\n                        nconn += 1\n                        try:\n                            if (\n                                c.nreq >= 1\n                                or not c.cli\n                                or c.cli.in_hdr_recv\n                                or c.cli.keepalive\n                            ):\n                                Daemon(c.shutdown)\n                                nclose += 1\n                                if c.nreq <= 0 and (not c.cli or c.cli.in_hdr_recv):\n                                    nloris += 1\n                        except:\n                            pass\n\n                t = \"{} downgraded to connection:close for {} min; dropped {}/{} connections\"\n                self.log(self.name, t.format(ip, self.args.aclose, nclose, nconn), 1)\n\n                if nloris < nconn / 2:\n                    continue\n\n                t = \"slow%s (idle-conn): %s banned for %d min\"  # slowloris\n                self.log(self.name, t % (\"loris\", ip, self.args.loris), 1)\n                self.bans[ip] = int(time.time() + self.args.loris * 60)\n\n            if self.args.log_conn:\n                self.log(self.name, \"|%sC-acc1\" % (\"-\" * 2,), c=\"90\")\n\n            try:\n                sck, saddr = srv_sck.accept()\n                if tcp:\n                    cip = unicode(saddr[0])\n                    if cip.startswith(\"::ffff:\"):\n                        cip = cip[7:]\n                    addr = (cip, saddr[1])\n                else:\n                    addr = (\"127.8.3.7\", sck.fileno())\n            except (OSError, socket.error) as ex:\n                if self.stopping:\n                    break\n\n                self.log(self.name, \"accept({}): {}\".format(fno, ex), c=6)\n                time.sleep(0.02)\n                continue\n            except TypeError as ex:\n                # on macOS, accept() may return a None saddr if blocked by LittleSnitch;\n                # unicode(saddr[0]) ==> TypeError: 'NoneType' object is not subscriptable\n                if tcp and not saddr:\n                    t = \"accept(%s): failed to accept connection from client due to firewall or network issue\"\n                    self.log(self.name, t % (fno,), c=3)\n                    try:\n                        sck.close()  # type: ignore\n                    except:\n                        pass\n                    time.sleep(0.02)\n                    continue\n                raise\n\n            if self.args.log_conn:\n                t = \"|{}C-acc2 \\033[0;36m{} \\033[3{}m{}\".format(\n                    \"-\" * 3, ip, port % 8, port\n                )\n                self.log(\"%s %s\" % addr, t, c=\"90\")\n\n            self.accept(sck, addr)\n\n    def accept(self, sck: socket.socket, addr: tuple[str, int]) -> None:\n        \"\"\"takes an incoming tcp connection and creates a thread to handle it\"\"\"\n        now = time.time()\n\n        if now - (self.tp_time or now) > 300:\n            t = \"httpserver threadpool died: tpt {:.2f}, now {:.2f}, nthr {}, ncli {}\"\n            self.log(self.name, t.format(self.tp_time, now, self.tp_nthr, self.ncli), 1)\n            self.tp_time = 0\n            self.tp_q = None\n\n        with self.mutex:\n            self.ncli += 1\n            if not self.t_periodic:\n                name = \"hsrv-pt\"\n                if self.nid:\n                    name += \"-%d\" % (self.nid,)\n\n                self.t_periodic = Daemon(self.periodic, name)\n\n            if self.tp_q:\n                self.tp_time = self.tp_time or now\n                self.tp_ncli = max(self.tp_ncli, self.ncli)\n                if self.tp_nthr < self.ncli + 4:\n                    self.start_threads(8)\n\n                self.tp_q.put((sck, addr))\n                return\n\n        if not self.args.no_htp:\n            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\"\n            self.log(self.name, t, 1)\n\n        Daemon(\n            self.thr_client,\n            \"httpconn-%s-%d\" % (addr[0].split(\".\", 2)[-1][-6:], addr[1]),\n            (sck, addr),\n        )\n\n    def thr_poolw(self) -> None:\n        assert self.tp_q  # !rm\n        while True:\n            task = self.tp_q.get()\n            if not task:\n                break\n\n            with self.mutex:\n                self.tp_time = 0\n\n            try:\n                sck, addr = task\n                me = threading.current_thread()\n                me.name = \"httpconn-%s-%d\" % (addr[0].split(\".\", 2)[-1][-6:], addr[1])\n                self.thr_client(sck, addr)\n                me.name = self.name + \"-poolw\"\n            except Exception as ex:\n                if str(ex).startswith(\"client d/c \"):\n                    self.log(self.name, \"thr_client: \" + str(ex), 6)\n                else:\n                    self.log(self.name, \"thr_client: \" + min_ex(), 3)\n\n    def shutdown(self) -> None:\n        self.stopping = True\n        for srv in self.srvs:\n            try:\n                srv.close()\n            except:\n                pass\n\n        thrs = []\n        clients = list(self.clients)\n        for cli in clients:\n            t = threading.Thread(target=cli.shutdown)\n            thrs.append(t)\n            t.start()\n\n        if self.tp_q:\n            self.stop_threads(self.tp_nthr)\n            for _ in range(10):\n                time.sleep(0.05)\n                if self.tp_q.empty():\n                    break\n\n        for t in thrs:\n            t.join()\n\n        self.log(self.name, \"ok bye\")\n\n    def thr_client(self, sck: socket.socket, addr: tuple[str, int]) -> None:\n        \"\"\"thread managing one tcp client\"\"\"\n        cli = HttpConn(sck, addr, self)\n        with self.mutex:\n            self.clients.add(cli)\n\n        # print(\"{}\\n\".format(len(self.clients)), end=\"\")\n        fno = sck.fileno()\n        try:\n            if self.args.log_conn:\n                self.log(\"%s %s\" % addr, \"|%sC-crun\" % (\"-\" * 4,), c=\"90\")\n\n            cli.run()\n\n        except (OSError, socket.error) as ex:\n            if ex.errno not in E_SCK:\n                self.log(\n                    \"%s %s\" % addr,\n                    \"run({}): {}\".format(fno, ex),\n                    c=6,\n                )\n\n        finally:\n            sck = cli.s\n            if self.args.log_conn:\n                self.log(\"%s %s\" % addr, \"|%sC-cdone\" % (\"-\" * 5,), c=\"90\")\n\n            try:\n                fno = sck.fileno()\n                shut_socket(cli.log, sck)\n            except (OSError, socket.error) as ex:\n                if not MACOS:\n                    self.log(\n                        \"%s %s\" % addr,\n                        \"shut({}): {}\".format(fno, ex),\n                        c=\"90\",\n                    )\n                if ex.errno not in E_SCK:\n                    raise\n            finally:\n                with self.mutex:\n                    self.clients.remove(cli)\n                    self.ncli -= 1\n\n                if cli.u2idx:\n                    self.put_u2idx(str(addr), cli.u2idx)\n\n    def cachebuster(self) -> str:\n        if time.time() - self.cb_ts < 1:\n            return self.cb_v\n\n        with self.mutex:\n            if time.time() - self.cb_ts < 1:\n                return self.cb_v\n\n            v = self.E.t0\n            try:\n                with os.scandir(self.E.mod_ + \"web\") as dh:\n                    for fh in dh:\n                        inf = fh.stat()\n                        v = max(v, inf.st_mtime)\n            except:\n                pass\n\n            # spack gives 4 lsb, take 3 lsb, get 4 ch\n            self.cb_v = ub64enc(spack(b\">L\", int(v))[1:]).decode(\"ascii\")\n            self.cb_ts = time.time()\n            return self.cb_v\n\n    def get_u2idx(self, ident: str) -> Optional[U2idx]:\n        utab = self.u2idx_free\n        for _ in range(100):  # 5/0.05 = 5sec\n            with self.mutex:\n                if utab:\n                    if ident in utab:\n                        return utab.pop(ident)\n\n                    return utab.pop(list(utab.keys())[0])\n\n                if self.u2idx_n < CORES:\n                    self.u2idx_n += 1\n                    return U2idx(self)\n\n            time.sleep(0.05)\n            # not using conditional waits, on a hunch that\n            # average performance will be faster like this\n            # since most servers won't be fully saturated\n\n        return None\n\n    def put_u2idx(self, ident: str, u2idx: U2idx) -> None:\n        with self.mutex:\n            while ident in self.u2idx_free:\n                ident += \"a\"\n\n            self.u2idx_free[ident] = u2idx\n\n    def read_dls(\n        self,\n    ) -> tuple[\n        dict[str, tuple[float, int, str, str, str]], dict[str, tuple[float, int]]\n    ]:\n        \"\"\"\n        mp-broker asking for local dl-info + dl-state;\n        reduce overhead by sending just the vfs vpath\n        \"\"\"\n        dli = {k: (a, b, c.vpath, d, e) for k, (a, b, c, d, e) in self.dli.items()}\n        return (dli, self.dls)\n\n    def write_dls(\n        self,\n        sdli: dict[str, tuple[float, int, str, str, str]],\n        dls: dict[str, tuple[float, int]],\n    ) -> None:\n        \"\"\"\n        mp-broker pushing total dl-info + dl-state;\n        swap out the vfs vpath with the vfs node\n        \"\"\"\n        dli: dict[str, tuple[float, int, \"VFS\", str, str]] = {}\n        for k, (a, b, c, d, e) in sdli.items():\n            vn = self.asrv.vfs.all_nodes[c]\n            dli[k] = (a, b, vn, d, e)\n\n        self.tdli = dli\n        self.tdls = dls\n"
  },
  {
    "path": "copyparty/ico.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport argparse  # typechk\nimport colorsys\nimport hashlib\nimport re\n\nfrom .__init__ import PY2\nfrom .th_srv import HAVE_PIL, HAVE_PILF\nfrom .util import BytesIO, html_escape  # type: ignore\n\n\nclass Ico(object):\n    def __init__(self, args: argparse.Namespace) -> None:\n        self.args = args\n\n    def get(self, ext: str, as_thumb: bool, chrome: bool) -> tuple[str, bytes]:\n        \"\"\"placeholder to make thumbnails not break\"\"\"\n\n        bext = ext.encode(\"ascii\", \"replace\")\n        ext = bext.decode(\"utf-8\")\n        zb = hashlib.sha1(bext).digest()[2:4]\n        if PY2:\n            zb = [ord(x) for x in zb]  # type: ignore\n\n        c1 = colorsys.hsv_to_rgb(zb[0] / 256.0, 1, 0.3)\n        c2 = colorsys.hsv_to_rgb(zb[0] / 256.0, 0.8 if HAVE_PILF else 1, 1)\n        ci = [int(x * 255) for x in list(c1) + list(c2)]\n        c = \"\".join([\"%02x\" % (x,) for x in ci])\n\n        w = 100\n        h = 30\n        if as_thumb:\n            sw, sh = self.args.th_size.split(\"x\")\n            h = int(100.0 / (float(sw) / float(sh)))\n\n        if chrome:\n            # cannot handle more than ~2000 unique SVGs\n            if HAVE_PILF:\n                # pillow 10.1 made this the default font;\n                # svg: 3.7s, this: 36s\n                try:\n                    from PIL import Image, ImageDraw\n\n                    # [.lt] are hard to see lowercase / unspaced\n                    ext2 = re.sub(\"(.)\", \"\\\\1 \", ext).upper()\n\n                    h = int(128.0 * h / w)\n                    w = 128\n                    img = Image.new(\"RGB\", (w, h), \"#\" + c[:6])\n                    pb = ImageDraw.Draw(img)\n                    _, _, tw, th = pb.textbbox((0, 0), ext2, font_size=16)\n                    xy = (int((w - tw) / 2), int((h - th) / 2))\n                    pb.text(xy, ext2, fill=\"#\" + c[6:], font_size=16)\n\n                    img = img.resize((w * 2, h * 2), Image.NEAREST)\n\n                    buf = BytesIO()\n                    img.save(buf, format=\"PNG\", compress_level=1)\n                    return \"image/png\", buf.getvalue()\n\n                except:\n                    pass\n\n            if HAVE_PIL:\n                # svg: 3s, cache: 6s, this: 8s\n                from PIL import Image, ImageDraw\n\n                h = int(64.0 * h / w)\n                w = 64\n                img = Image.new(\"RGB\", (w, h), \"#\" + c[:6])\n                pb = ImageDraw.Draw(img)\n                try:\n                    _, _, tw, th = pb.textbbox((0, 0), ext)\n                except:\n                    tw, th = pb.textsize(ext)  # type: ignore\n\n                tw += len(ext)\n                cw = tw // len(ext)\n                x = ((w - tw) // 2) - (cw * 2) // 3\n                fill = \"#\" + c[6:]\n                for ch in ext:\n                    pb.text((x, (h - th) // 2), \" %s \" % (ch,), fill=fill)\n                    x += cw\n\n                img = img.resize((w * 3, h * 3), Image.NEAREST)\n\n                buf = BytesIO()\n                img.save(buf, format=\"PNG\", compress_level=1)\n                return \"image/png\", buf.getvalue()\n\n        svg = \"\"\"\\\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg version=\"1.1\" viewBox=\"0 0 100 {}\" xmlns=\"http://www.w3.org/2000/svg\"><g>\n<rect width=\"100%\" height=\"100%\" fill=\"#{}\" />\n<text x=\"50%\" y=\"{}\" dominant-baseline=\"middle\" text-anchor=\"middle\" xml:space=\"preserve\"\n  fill=\"#{}\" font-family=\"monospace\" font-size=\"14px\" style=\"letter-spacing:.5px\">{}</text>\n</g></svg>\n\"\"\"\n\n        txt = html_escape(ext, True)\n        if \"\\n\" in txt:\n            lines = txt.split(\"\\n\")\n            n = len(lines)\n            y = \"20%\" if n == 2 else \"10%\" if n == 3 else \"0\"\n            zs = '<tspan x=\"50%%\" dy=\"1.2em\">%s</tspan>'\n            txt = \"\".join([zs % (x,) for x in lines])\n        else:\n            y = \"50%\"\n\n        svg = svg.format(h, c[:6], y, c[6:], txt)\n\n        return \"image/svg+xml\", svg.encode(\"utf-8\")\n"
  },
  {
    "path": "copyparty/mdns.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport errno\nimport os\nimport random\nimport select\nimport socket\nimport time\n\nfrom ipaddress import IPv4Network, IPv6Network\n\nfrom .__init__ import TYPE_CHECKING\nfrom .__init__ import unicode as U\nfrom .multicast import MC_Sck, MCast\nfrom .util import IP6_LL, CachedSet, Daemon, Netdev, list_ips, min_ex\n\ntry:\n    if os.environ.get(\"PRTY_SYS_ALL\") or os.environ.get(\"PRTY_SYS_DNSLIB\"):\n        raise ImportError()\n    from .stolen.dnslib import (\n        AAAA,\n    )\n    from .stolen.dnslib import CLASS as DC\n    from .stolen.dnslib import (\n        NSEC,\n        PTR,\n        QTYPE,\n        RR,\n        SRV,\n        TXT,\n        A,\n        DNSHeader,\n        DNSQuestion,\n        DNSRecord,\n        set_avahi_379,\n    )\n\n    DNS_VND = True\nexcept ImportError:\n    DNS_VND = False\n    from dnslib import (\n        AAAA,\n    )\n    from dnslib import CLASS as DC\n    from dnslib import (\n        NSEC,\n        PTR,\n        QTYPE,\n        RR,\n        SRV,\n        TXT,\n        A,\n        Bimap,\n        DNSHeader,\n        DNSQuestion,\n        DNSRecord,\n    )\n\n    DC.forward[0x8001] = \"F_IN\"\n    DC.reverse[\"F_IN\"] = 0x8001\n\nif TYPE_CHECKING:\n    from .svchub import SvcHub\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Any, Optional, Union\n\nif os.environ.get(\"PRTY_MODSPEC\"):\n    from inspect import getsourcefile\n\n    print(\"PRTY_MODSPEC: dnslib:\", getsourcefile(A))\n\n\nMDNS4 = \"224.0.0.251\"\nMDNS6 = \"ff02::fb\"\n\n\nclass MDNS_Sck(MC_Sck):\n    def __init__(\n        self,\n        sck: socket.socket,\n        nd: Netdev,\n        grp: str,\n        ip: str,\n        net: Union[IPv4Network, IPv6Network],\n    ):\n        super(MDNS_Sck, self).__init__(sck, nd, grp, ip, net)\n\n        self.bp_probe = b\"\"\n        self.bp_ip = b\"\"\n        self.bp_svc = b\"\"\n        self.bp_bye = b\"\"\n\n        self.last_tx = 0.0\n        self.tx_ex = False\n\n\nclass MDNS(MCast):\n    def __init__(self, hub: \"SvcHub\", ngen: int) -> None:\n        al = hub.args\n        grp4 = \"\" if al.zm6 else MDNS4\n        grp6 = \"\" if al.zm4 else MDNS6\n        super(MDNS, self).__init__(\n            hub, MDNS_Sck, al.zm_on, al.zm_off, grp4, grp6, 5353, hub.args.zmv\n        )\n        self.srv: dict[socket.socket, MDNS_Sck] = {}\n        self.logsrc = \"mDNS-{}\".format(ngen)\n        self.ngen = ngen\n        self.ttl = 300\n\n        if not self.args.zm_nwa_1 and DNS_VND:\n            set_avahi_379()  # type: ignore\n\n        zs = self.args.zm_fqdn or (self.args.name + \".local\")\n        zs = zs.replace(\"--name\", self.args.name).rstrip(\".\") + \".\"\n        zs = zs.encode(\"ascii\", \"replace\").decode(\"ascii\", \"replace\")\n        self.hn = \"-\".join(x for x in zs.split(\"?\") if x) or (\n            \"vault-{}\".format(random.randint(1, 255))\n        )\n        self.lhn = self.hn.lower()\n\n        # requester ip -> (response deadline, srv, body):\n        self.q: dict[str, tuple[float, MDNS_Sck, bytes]] = {}\n        self.rx4 = CachedSet(0.42)  # 3 probes @ 250..500..750 => 500ms span\n        self.rx6 = CachedSet(0.42)\n        self.svcs, self.sfqdns = self.build_svcs()\n        self.lsvcs = {k.lower(): v for k, v in self.svcs.items()}\n        self.lsfqdns = set([x.lower() for x in self.sfqdns])\n\n        self.probing = 0.0\n        self.unsolicited: list[float] = []  # scheduled announces on all nics\n        self.defend: dict[MDNS_Sck, float] = {}  # server -> deadline\n\n    def log(self, msg: str, c: Union[int, str] = 0) -> None:\n        self.log_func(self.logsrc, msg, c)\n\n    def build_svcs(self) -> tuple[dict[str, dict[str, Any]], set[str]]:\n        ar = self.args\n        zms = self.args.zms\n\n        zi = ar.zm_http\n        http = {\"port\": zi if zi != -1 else 80 if 80 in ar.p else ar.p[0]}\n        zi = ar.zm_https\n        https = {\"port\": zi if zi != -1 else 443 if 443 in ar.p else ar.p[0]}\n\n        webdav = http.copy()\n        webdavs = https.copy()\n        webdav[\"u\"] = webdavs[\"u\"] = \"u\"  # KDE requires username\n        ftp = {\"port\": (self.args.ftp if \"f\" in zms else self.args.ftps)}\n        smb = {\"port\": self.args.smb_port}\n\n        # some gvfs require path\n        zs = self.args.zm_ld or \"/\"\n        if zs:\n            webdav[\"path\"] = zs\n            webdavs[\"path\"] = zs\n\n        if self.args.zm_lh:\n            http[\"path\"] = self.args.zm_lh\n            https[\"path\"] = self.args.zm_lh\n\n        if self.args.zm_lf:\n            ftp[\"path\"] = self.args.zm_lf\n\n        if self.args.zm_ls:\n            smb[\"path\"] = self.args.zm_ls\n\n        svcs: dict[str, dict[str, Any]] = {}\n\n        if \"d\" in zms and http[\"port\"]:\n            svcs[\"_webdav._tcp.local.\"] = webdav\n\n        if \"D\" in zms and https[\"port\"]:\n            svcs[\"_webdavs._tcp.local.\"] = webdavs\n\n        if \"h\" in zms and http[\"port\"]:\n            svcs[\"_http._tcp.local.\"] = http\n\n        if \"H\" in zms and https[\"port\"]:\n            svcs[\"_https._tcp.local.\"] = https\n\n        if \"f\" in zms.lower():\n            svcs[\"_ftp._tcp.local.\"] = ftp\n\n        if \"s\" in zms.lower():\n            svcs[\"_smb._tcp.local.\"] = smb\n\n        sfqdns: set[str] = set()\n        for k, v in svcs.items():\n            name = \"{}-c-{}\".format(self.args.name, k.split(\".\")[0][1:])\n            v[\"name\"] = name\n            sfqdns.add(\"{}.{}\".format(name, k))\n\n        return svcs, sfqdns\n\n    def build_replies(self) -> None:\n        for srv in self.srv.values():\n            probe = DNSRecord(DNSHeader(0, 0), q=DNSQuestion(self.hn, QTYPE.ANY))\n            areply = DNSRecord(DNSHeader(0, 0x8400))\n            sreply = DNSRecord(DNSHeader(0, 0x8400))\n            bye = DNSRecord(DNSHeader(0, 0x8400))\n\n            have4 = have6 = False\n            for s2 in self.srv.values():\n                if srv.idx != s2.idx:\n                    continue\n\n                if s2.v6:\n                    have6 = True\n                else:\n                    have4 = True\n\n            for ip in srv.ips:\n                if \":\" in ip:\n                    qt = QTYPE.AAAA\n                    ar = {\"rclass\": DC.F_IN, \"rdata\": AAAA(ip)}\n                else:\n                    qt = QTYPE.A\n                    ar = {\"rclass\": DC.F_IN, \"rdata\": A(ip)}\n\n                r0 = RR(self.hn, qt, ttl=0, **ar)\n                r120 = RR(self.hn, qt, ttl=120, **ar)\n                # rfc-10:\n                #   SHOULD rr ttl 120sec for A/AAAA/SRV\n                #   (and recommend 75min for all others)\n\n                probe.add_auth(r120)\n                areply.add_answer(r120)\n                sreply.add_answer(r120)\n                bye.add_answer(r0)\n\n            for sclass, props in self.svcs.items():\n                sname = props[\"name\"]\n                sport = props[\"port\"]\n                sfqdn = sname + \".\" + sclass\n\n                k = \"_services._dns-sd._udp.local.\"\n                r = RR(k, QTYPE.PTR, DC.IN, 4500, PTR(sclass))\n                sreply.add_answer(r)\n\n                r = RR(sclass, QTYPE.PTR, DC.IN, 4500, PTR(sfqdn))\n                sreply.add_answer(r)\n\n                r = RR(sfqdn, QTYPE.SRV, DC.F_IN, 120, SRV(0, 0, sport, self.hn))\n                sreply.add_answer(r)\n                areply.add_answer(r)\n\n                r = RR(sfqdn, QTYPE.SRV, DC.F_IN, 0, SRV(0, 0, sport, self.hn))\n                bye.add_answer(r)\n\n                txts = []\n                for k in (\"u\", \"path\"):\n                    if k not in props:\n                        continue\n\n                    zb = \"{}={}\".format(k, props[k]).encode(\"utf-8\")\n                    if len(zb) > 255:\n                        t = \"value too long for mdns: [{}]\"\n                        raise Exception(t.format(props[k]))\n\n                    txts.append(zb)\n\n                # gvfs really wants txt even if they're empty\n                r = RR(sfqdn, QTYPE.TXT, DC.F_IN, 4500, TXT(txts))\n                sreply.add_answer(r)\n\n            if not (have4 and have6) and not self.args.zm_noneg:\n                ns = NSEC(self.hn, [\"AAAA\" if have6 else \"A\"])\n                r = RR(self.hn, QTYPE.NSEC, DC.F_IN, 120, ns)\n                areply.add_ar(r)\n                if len(sreply.pack()) < 1400:\n                    sreply.add_ar(r)\n\n            srv.bp_probe = probe.pack()\n            srv.bp_ip = areply.pack()\n            srv.bp_svc = sreply.pack()\n            srv.bp_bye = bye.pack()\n\n            # since all replies are small enough to fit in one packet,\n            # always send full replies rather than just a/aaaa records\n            srv.bp_ip = srv.bp_svc\n\n    def send_probes(self) -> None:\n        slp = random.random() * 0.25\n        for _ in range(3):\n            time.sleep(slp)\n            slp = 0.25\n            if not self.running:\n                break\n\n            if self.args.zmv:\n                self.log(\"sending hostname probe...\")\n\n            # ipv4: need to probe each ip (each server)\n            # ipv6: only need to probe each set of looped nics\n            probed6: set[str] = set()\n            for srv in self.srv.values():\n                if srv.ip in probed6:\n                    continue\n\n                try:\n                    srv.sck.sendto(srv.bp_probe, (srv.grp, 5353))\n                    if srv.v6:\n                        for ip in srv.ips:\n                            probed6.add(ip)\n                except Exception as ex:\n                    self.log(\"sendto failed: {} ({})\".format(srv.ip, ex), \"90\")\n\n    def run(self) -> None:\n        try:\n            bound = self.create_servers()\n        except:\n            t = \"no server IP matches the mdns config\\n{}\"\n            self.log(t.format(min_ex()), 1)\n            bound = []\n\n        if not bound:\n            self.log(\"failed to announce copyparty services on the network\", 3)\n            return\n\n        self.build_replies()\n        Daemon(self.send_probes)\n        zf = time.time() + 2\n        self.probing = zf  # cant unicast so give everyone an extra sec\n        self.unsolicited = [zf, zf + 1, zf + 3, zf + 7]  # rfc-8.3\n\n        try:\n            self.run2()\n        except OSError as ex:\n            if ex.errno != errno.EBADF:\n                raise\n\n            self.log(\"stopping due to {}\".format(ex), \"90\")\n\n        self.log(\"stopped\", 2)\n\n    def run2(self) -> None:\n        last_hop = time.time()\n        ihop = self.args.mc_hop\n\n        try:\n            if self.args.no_poll:\n                raise Exception()\n            fd2sck = {}\n            srvpoll = select.poll()\n            for sck in self.srv:\n                fd = sck.fileno()\n                fd2sck[fd] = sck\n                srvpoll.register(fd, select.POLLIN)\n        except Exception as ex:\n            srvpoll = None\n            if not self.args.no_poll:\n                t = \"WARNING: failed to poll(), will use select() instead: %r\"\n                self.log(t % (ex,), 3)\n\n        while self.running:\n            timeout = (\n                0.02 + random.random() * 0.07\n                if self.probing or self.q or self.defend\n                else max(0.05, self.unsolicited[0] - time.time())\n                if self.unsolicited\n                else (last_hop + ihop if ihop else 180)\n            )\n            if srvpoll:\n                pr = srvpoll.poll(timeout * 1000)\n                rx = [fd2sck[x[0]] for x in pr if x[1] & select.POLLIN]\n            else:\n                rdy = select.select(self.srv, [], [], timeout)\n                rx: list[socket.socket] = rdy[0]  # type: ignore\n\n            self.rx4.cln()\n            self.rx6.cln()\n            buf = b\"\"\n            addr = (\"0\", 0)\n            for sck in rx:\n                try:\n                    buf, addr = sck.recvfrom(4096)\n                    self.eat(buf, addr, sck)\n                except:\n                    if not self.running:\n                        self.log(\"stopped\", 2)\n                        return\n\n                    if self.args.zm_no_pe:\n                        continue\n\n                    t = \"{} {} \\033[33m|{}| {}\\n{}\".format(\n                        self.srv[sck].name, addr, len(buf), repr(buf)[2:-1], min_ex()\n                    )\n                    self.log(t, 6)\n\n            if not self.probing:\n                self.process()\n                continue\n\n            if self.probing < time.time():\n                t = \"probe ok; announcing [{}]\"\n                self.log(t.format(self.hn[:-1]), 2)\n                self.probing = 0\n\n    def stop(self, panic=False) -> None:\n        self.running = False\n        for srv in self.srv.values():\n            try:\n                if panic:\n                    srv.sck.close()\n                else:\n                    srv.sck.sendto(srv.bp_bye, (srv.grp, 5353))\n            except:\n                pass\n\n        self.srv.clear()\n\n    def eat(self, buf: bytes, addr: tuple[str, int], sck: socket.socket) -> None:\n        cip = addr[0]\n        v6 = \":\" in cip\n        if (cip.startswith(\"169.254\") and not self.ll_ok) or (\n            v6 and not cip.startswith(IP6_LL)\n        ):\n            return\n\n        cache = self.rx6 if v6 else self.rx4\n        if buf in cache.c:\n            return\n\n        srv: Optional[MDNS_Sck] = self.srv[sck] if v6 else self.map_client(cip)  # type: ignore\n        if not srv:\n            return\n\n        cache.add(buf)\n        now = time.time()\n\n        if self.args.zmv and cip != srv.ip and cip not in srv.ips:\n            t = \"{} [{}] \\033[36m{} \\033[0m|{}|\"\n            self.log(t.format(srv.name, srv.ip, cip, len(buf)), \"90\")\n\n        p = DNSRecord.parse(buf)\n        if self.args.zmvv:\n            self.log(str(p))\n\n        # check for incoming probes for our hostname\n        cips = [U(x.rdata) for x in p.auth if U(x.rname).lower() == self.lhn]\n        if cips and self.sips.isdisjoint(cips):\n            if not [x for x in cips if x not in (\"::1\", \"127.0.0.1\")]:\n                # avahi broadcasting 127.0.0.1-only packets\n                return\n\n            self.log(\"someone trying to steal our hostname: {}\".format(cips), 3)\n            # immediately unicast\n            if not self.probing:\n                srv.sck.sendto(srv.bp_ip, (cip, 5353))\n\n            # and schedule multicast\n            self.defend[srv] = self.defend.get(srv, now + 0.1)\n            return\n\n        # check for someone rejecting our probe / hijacking our hostname\n        cips = [\n            U(x.rdata)\n            for x in p.rr\n            if U(x.rname).lower() == self.lhn and x.rclass == DC.F_IN\n        ]\n        if cips and self.sips.isdisjoint(cips):\n            if not [x for x in cips if x not in (\"::1\", \"127.0.0.1\")]:\n                # avahi broadcasting 127.0.0.1-only packets\n                return\n\n            # check if we've been given additional IPs\n            for ip in list_ips():\n                if ip in cips:\n                    self.sips.add(ip)\n\n            if not self.sips.isdisjoint(cips):\n                return\n\n            t = \"mdns zeroconf: \"\n            if self.probing:\n                t += \"Cannot start; hostname '{}' is occupied\"\n            else:\n                t += \"Emergency stop; hostname '{}' got stolen\"\n\n            t += \" on {}! Use --name to set another hostname.\\n\\nName taken by {}\\n\\nYour IPs: {}\\n\"\n            self.log(t.format(self.args.name, srv.name, cips, list(self.sips)), 1)\n            self.stop(True)\n            return\n\n        # then rfc-6.7; dns pretending to be mdns (android...)\n        if p.header.id or addr[1] != 5353:\n            rsp: Optional[DNSRecord] = None\n            for r in p.questions:\n                try:\n                    lhn = U(r.qname).lower()\n                except:\n                    self.log(\"invalid question: {}\".format(r))\n                    continue\n\n                if lhn != self.lhn:\n                    continue\n\n                if p.header.id and r.qtype in (QTYPE.A, QTYPE.AAAA):\n                    rsp = rsp or DNSRecord(DNSHeader(p.header.id, 0x8400))\n                    rsp.add_question(r)\n                    for ip in srv.ips:\n                        qt = r.qtype\n                        v6 = \":\" in ip\n                        if v6 == (qt == QTYPE.AAAA):\n                            rd = AAAA(ip) if v6 else A(ip)\n                            rr = RR(self.hn, qt, DC.IN, 10, rd)\n                            rsp.add_answer(rr)\n            if rsp:\n                srv.sck.sendto(rsp.pack(), addr[:2])\n                # but don't return in case it's a differently broken client\n\n        # then a/aaaa records\n        for r in p.questions:\n            try:\n                lhn = U(r.qname).lower()\n            except:\n                self.log(\"invalid question: {}\".format(r))\n                continue\n\n            if lhn != self.lhn:\n                continue\n\n            # gvfs keeps repeating itself\n            found = False\n            unicast = False\n            for rr in p.rr:\n                try:\n                    rname = U(rr.rname).lower()\n                except:\n                    self.log(\"invalid rr: {}\".format(rr))\n                    continue\n\n                if rname == self.lhn:\n                    if rr.ttl > 60:\n                        found = True\n                    if rr.rclass == DC.F_IN:\n                        unicast = True\n\n            if unicast:\n                # spec-compliant mDNS-over-unicast\n                srv.sck.sendto(srv.bp_ip, (cip, 5353))\n            elif addr[1] != 5353:\n                # just in case some clients use (and want us to use) invalid ports\n                srv.sck.sendto(srv.bp_ip, addr[:2])\n\n            if not found:\n                self.q[cip] = (0, srv, srv.bp_ip)\n                return\n\n        deadline = now + (0.5 if p.header.tc else 0.02)  # rfc-7.2\n\n        # and service queries\n        for r in p.questions:\n            if not r or not r.qname:\n                continue\n\n            qname = U(r.qname).lower()\n            if qname in self.lsvcs or qname == \"_services._dns-sd._udp.local.\":\n                self.q[cip] = (deadline, srv, srv.bp_svc)\n                break\n        # heed rfc-7.1 if there was an announce in the past 12sec\n        # (workaround gvfs race-condition where it occasionally\n        #  doesn't read/decode the full response...)\n        if now < srv.last_tx + 12:\n            for rr in p.rr:\n                if not rr.rdata:\n                    continue\n\n                rdata = U(rr.rdata).lower()\n                if rdata in self.lsfqdns:\n                    if rr.ttl > 2250:\n                        self.q.pop(cip, None)\n                    break\n\n    def process(self) -> None:\n        tx = set()\n        now = time.time()\n        cooldown = 0.9  # rfc-6: 1\n        if self.unsolicited and self.unsolicited[0] < now:\n            self.unsolicited.pop(0)\n            cooldown = 0.1\n            for srv in self.srv.values():\n                tx.add(srv)\n\n            if not self.unsolicited and self.args.zm_spam:\n                zf = time.time() + self.args.zm_spam + random.random() * 0.07\n                self.unsolicited.append(zf)\n\n        for srv, deadline in list(self.defend.items()):\n            if now < deadline:\n                continue\n\n            if self._tx(srv, srv.bp_ip, 0.02):  # rfc-6: 0.25\n                self.defend.pop(srv)\n\n        for cip, (deadline, srv, msg) in list(self.q.items()):\n            if now < deadline:\n                continue\n\n            self.q.pop(cip)\n            self._tx(srv, msg, cooldown)\n\n        for srv in tx:\n            self._tx(srv, srv.bp_svc, cooldown)\n\n    def _tx(self, srv: MDNS_Sck, msg: bytes, cooldown: float) -> bool:\n        now = time.time()\n        if now < srv.last_tx + cooldown:\n            return False\n\n        try:\n            srv.sck.sendto(msg, (srv.grp, 5353))\n            srv.last_tx = now\n        except Exception as ex:\n            if srv.tx_ex:\n                return True\n\n            srv.tx_ex = True\n            t = \"tx({},|{}|,{}): {}\"\n            self.log(t.format(srv.ip, len(msg), cooldown, ex), 3)\n\n        return True\n"
  },
  {
    "path": "copyparty/metrics.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport json\nimport time\n\nfrom .__init__ import TYPE_CHECKING\nfrom .util import Pebkac, get_df, unhumanize\n\nif TYPE_CHECKING:\n    from .httpcli import HttpCli\n    from .httpsrv import HttpSrv\n\n\nclass Metrics(object):\n    def __init__(self, hsrv: \"HttpSrv\") -> None:\n        self.hsrv = hsrv\n\n    def tx(self, cli: \"HttpCli\") -> bool:\n        args = cli.args\n        if not cli.avol and cli.uname.lower() not in args.stats_u_set:\n            raise Pebkac(403, \"'stats' not allowed for user \" + cli.uname)\n\n        if not args.stats:\n            raise Pebkac(403, \"the stats feature is not enabled in server config\")\n\n        conn = cli.conn\n        vfs = conn.asrv.vfs\n        allvols = list(sorted(vfs.all_vols.items()))\n\n        idx = conn.get_u2idx()\n        if not idx or not hasattr(idx, \"p_end\"):\n            idx = None\n\n        ret: list[str] = []\n\n        def addc(k: str, v: str, desc: str) -> None:\n            zs = \"# TYPE %s counter\\n# HELP %s %s\\n%s_created %s\\n%s_total %s\"\n            ret.append(zs % (k, k, desc, k, int(self.hsrv.t0), k, v))\n\n        def adduc(k: str, unit: str, v: str, desc: str) -> None:\n            k += \"_\" + unit\n            zs = \"# TYPE %s counter\\n# UNIT %s %s\\n# HELP %s %s\\n%s_created %s\\n%s_total %s\"\n            ret.append(zs % (k, k, unit, k, desc, k, int(self.hsrv.t0), k, v))\n\n        def addg(k: str, v: str, desc: str) -> None:\n            zs = \"# TYPE %s gauge\\n# HELP %s %s\\n%s %s\"\n            ret.append(zs % (k, k, desc, k, v))\n\n        def addug(k: str, unit: str, v: str, desc: str) -> None:\n            k += \"_\" + unit\n            zs = \"# TYPE %s gauge\\n# UNIT %s %s\\n# HELP %s %s\\n%s %s\"\n            ret.append(zs % (k, k, unit, k, desc, k, v))\n\n        def addh(k: str, typ: str, desc: str) -> None:\n            zs = \"# TYPE %s %s\\n# HELP %s %s\"\n            ret.append(zs % (k, typ, k, desc))\n\n        def addbh(k: str, desc: str) -> None:\n            zs = \"# TYPE %s gauge\\n# UNIT %s bytes\\n# HELP %s %s\"\n            ret.append(zs % (k, k, k, desc))\n\n        def addv(k: str, v: str) -> None:\n            ret.append(\"%s %s\" % (k, v))\n\n        t = \"time since last copyparty restart\"\n        v = \"{:.3f}\".format(time.time() - self.hsrv.t0)\n        addug(\"cpp_uptime\", \"seconds\", v, t)\n\n        # timestamps are gauges because initial value is not zero\n        t = \"unixtime of last copyparty restart\"\n        v = \"{:.3f}\".format(self.hsrv.t0)\n        addug(\"cpp_boot_unixtime\", \"seconds\", v, t)\n\n        t = \"number of active downloads\"\n        addg(\"cpp_active_dl\", str(len(self.hsrv.tdls)), t)\n\n        t = \"number of open http(s) client connections\"\n        addg(\"cpp_http_conns\", str(self.hsrv.ncli), t)\n\n        t = \"number of http(s) requests since last restart\"\n        addc(\"cpp_http_reqs\", str(self.hsrv.nreq), t)\n\n        t = \"number of 403/422/malicious reqs since restart\"\n        addc(\"cpp_sus_reqs\", str(self.hsrv.nsus), t)\n\n        v = str(len(conn.bans or []))\n        addg(\"cpp_active_bans\", v, \"number of currently banned IPs\")\n\n        t = \"number of IPs banned since last restart\"\n        addg(\"cpp_total_bans\", str(self.hsrv.nban), t)\n\n        if not args.nos_vst:\n            x = self.hsrv.broker.ask(\"up2k.get_state\", True, \"\")\n            vs = json.loads(x.get())\n\n            nvidle = 0\n            nvbusy = 0\n            nvoffline = 0\n            for v in vs[\"volstate\"].values():\n                if v == \"online, idle\":\n                    nvidle += 1\n                elif \"OFFLINE\" in v:\n                    nvoffline += 1\n                else:\n                    nvbusy += 1\n\n            addg(\"cpp_idle_vols\", str(nvidle), \"number of idle/ready volumes\")\n            addg(\"cpp_busy_vols\", str(nvbusy), \"number of busy/indexing volumes\")\n            addg(\"cpp_offline_vols\", str(nvoffline), \"number of offline volumes\")\n\n            t = \"time since last database activity (upload/rename/delete)\"\n            addug(\"cpp_db_idle\", \"seconds\", str(vs[\"dbwt\"]), t)\n\n            t = \"unixtime of last database activity (upload/rename/delete)\"\n            addug(\"cpp_db_act\", \"seconds\", str(vs[\"dbwu\"]), t)\n\n            t = \"number of files queued for hashing/indexing\"\n            addg(\"cpp_hashing_files\", str(vs[\"hashq\"]), t)\n\n            t = \"number of files queued for metadata scanning\"\n            addg(\"cpp_tagq_files\", str(vs[\"tagq\"]), t)\n\n            try:\n                t = \"number of files queued for plugin-based analysis\"\n                addg(\"cpp_mtpq_files\", str(int(vs[\"mtpq\"])), t)\n            except:\n                pass\n\n        if not args.nos_hdd:\n            addbh(\"cpp_disk_size_bytes\", \"total HDD size of volume\")\n            addbh(\"cpp_disk_free_bytes\", \"free HDD space in volume\")\n            for vpath, vol in allvols:\n                free, total, _ = get_df(vol.realpath, False)\n                if free is None or total is None:\n                    continue\n\n                addv('cpp_disk_size_bytes{vol=\"/%s\"}' % (vpath), str(total))\n                addv('cpp_disk_free_bytes{vol=\"/%s\"}' % (vpath), str(free))\n\n        if idx and not args.nos_vol:\n            addbh(\"cpp_vol_bytes\", \"num bytes of data in volume\")\n            addh(\"cpp_vol_files\", \"gauge\", \"num files in volume\")\n            addbh(\"cpp_vol_free_bytes\", \"free space (vmaxb) in volume\")\n            addh(\"cpp_vol_free_files\", \"gauge\", \"free space (vmaxn) in volume\")\n            tnbytes = 0\n            tnfiles = 0\n\n            volsizes = []\n            try:\n                ptops = [x.realpath for _, x in allvols]\n                x = self.hsrv.broker.ask(\"up2k.get_volsizes\", ptops)\n                volsizes = x.get()\n            except Exception as ex:\n                cli.log(\"tx_stats get_volsizes: {!r}\".format(ex), 3)\n\n            for (vpath, vol), (nbytes, nfiles) in zip(allvols, volsizes):\n                tnbytes += nbytes\n                tnfiles += nfiles\n                addv('cpp_vol_bytes{vol=\"/%s\"}' % (vpath), str(nbytes))\n                addv('cpp_vol_files{vol=\"/%s\"}' % (vpath), str(nfiles))\n\n                if vol.flags.get(\"vmaxb\") or vol.flags.get(\"vmaxn\"):\n\n                    zi = unhumanize(vol.flags.get(\"vmaxb\") or \"0\")\n                    if zi:\n                        v = str(zi - nbytes)\n                        addv('cpp_vol_free_bytes{vol=\"/%s\"}' % (vpath), v)\n\n                    zi = unhumanize(vol.flags.get(\"vmaxn\") or \"0\")\n                    if zi:\n                        v = str(zi - nfiles)\n                        addv('cpp_vol_free_files{vol=\"/%s\"}' % (vpath), v)\n\n            if volsizes:\n                addv('cpp_vol_bytes{vol=\"total\"}', str(tnbytes))\n                addv('cpp_vol_files{vol=\"total\"}', str(tnfiles))\n\n        if idx and not args.nos_dup:\n            addbh(\"cpp_dupe_bytes\", \"num dupe bytes in volume\")\n            addh(\"cpp_dupe_files\", \"gauge\", \"num dupe files in volume\")\n            tnbytes = 0\n            tnfiles = 0\n            for vpath, vol in allvols:\n                cur = idx.get_cur(vol)\n                if not cur:\n                    continue\n\n                nbytes = 0\n                nfiles = 0\n                q = \"select sz, count(*)-1 c from up group by w having c\"\n                for sz, c in cur.execute(q):\n                    nbytes += sz * c\n                    nfiles += c\n\n                tnbytes += nbytes\n                tnfiles += nfiles\n                addv('cpp_dupe_bytes{vol=\"/%s\"}' % (vpath), str(nbytes))\n                addv('cpp_dupe_files{vol=\"/%s\"}' % (vpath), str(nfiles))\n\n            addv('cpp_dupe_bytes{vol=\"total\"}', str(tnbytes))\n            addv('cpp_dupe_files{vol=\"total\"}', str(tnfiles))\n\n        if not args.nos_unf:\n            addbh(\"cpp_unf_bytes\", \"incoming/unfinished uploads (num bytes)\")\n            addh(\"cpp_unf_files\", \"gauge\", \"incoming/unfinished uploads (num files)\")\n            tnbytes = 0\n            tnfiles = 0\n            try:\n                x = self.hsrv.broker.ask(\"up2k.get_unfinished\")\n                xs = x.get()\n                if not xs:\n                    raise Exception(\"up2k mutex acquisition timed out\")\n\n                xj = json.loads(xs)\n                for ptop, (nbytes, nfiles) in xj.items():\n                    tnbytes += nbytes\n                    tnfiles += nfiles\n                    vol = next((x[1] for x in allvols if x[1].realpath == ptop), None)\n                    if not vol:\n                        t = \"tx_stats get_unfinished: could not map {}\"\n                        cli.log(t.format(ptop), 3)\n                        continue\n\n                    addv('cpp_unf_bytes{vol=\"/%s\"}' % (vol.vpath), str(nbytes))\n                    addv('cpp_unf_files{vol=\"/%s\"}' % (vol.vpath), str(nfiles))\n\n                addv('cpp_unf_bytes{vol=\"total\"}', str(tnbytes))\n                addv('cpp_unf_files{vol=\"total\"}', str(tnfiles))\n\n            except Exception as ex:\n                cli.log(\"tx_stats get_unfinished: {!r}\".format(ex), 3)\n\n        ret.append(\"# EOF\")\n\n        mime = \"application/openmetrics-text; version=1.0.0; charset=utf-8\"\n        mime = cli.uparam.get(\"mime\") or mime\n        cli.reply(\"\\n\".join(ret).encode(\"utf-8\"), mime=mime)\n        return True\n"
  },
  {
    "path": "copyparty/mtag.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport argparse\nimport json\nimport os\nimport re\nimport shutil\nimport subprocess as sp\nimport sys\nimport tempfile\n\nfrom .__init__ import ANYWIN, EXE, PY2, WINDOWS, E, unicode\nfrom .authsrv import VFS\nfrom .bos import bos\nfrom .util import (\n    FFMPEG_URL,\n    REKOBO_LKEY,\n    VF_CAREFUL,\n    fsenc,\n    gzip,\n    min_ex,\n    pybin,\n    retchk,\n    runcmd,\n    sfsenc,\n    uncyg,\n    wunlink,\n)\n\nif True:  # pylint: disable=using-constant-test\n    from typing import IO, Any, Optional, Union\n\n    from .util import NamedLogger, RootLogger\n\n\ntry:\n    if os.environ.get(\"PRTY_NO_MUTAGEN\"):\n        raise Exception()\n\n    from mutagen import version  # noqa: F401\n\n    HAVE_MUTAGEN = True\nexcept:\n    HAVE_MUTAGEN = False\n\n\ndef have_ff(scmd: str) -> bool:\n    if ANYWIN:\n        scmd += \".exe\"\n\n    if PY2:\n        print(\"# checking {}\".format(scmd))\n        acmd = (scmd + \" -version\").encode(\"ascii\").split(b\" \")\n        try:\n            sp.Popen(acmd, stdout=sp.PIPE, stderr=sp.PIPE).communicate()\n            return True\n        except:\n            return False\n    else:\n        return bool(shutil.which(scmd))\n\n\nHAVE_FFMPEG = not os.environ.get(\"PRTY_NO_FFMPEG\") and have_ff(\"ffmpeg\")\nHAVE_FFPROBE = not os.environ.get(\"PRTY_NO_FFPROBE\") and have_ff(\"ffprobe\")\n\nCBZ_PICS = set(\"png jpg jpeg gif bmp tga tif tiff webp avif jxl\".split())\nCBZ_01 = re.compile(r\"(^|[^0-9v])0+[01]\\b\")\n\nFMT_AU = set(\"mp3 ogg flac wav\".split())\nM4A = set(\"aac m4a m4b m4r\".split())\n\n\nclass MParser(object):\n    def __init__(self, cmdline: str) -> None:\n        self.tag, args = cmdline.split(\"=\", 1)\n        self.tags = self.tag.split(\",\")\n\n        self.timeout = 60\n        self.force = False\n        self.kill = \"t\"  # tree; all children recursively\n        self.capture = 3  # outputs to consume\n        self.audio = \"y\"\n        self.pri = 0  # priority; higher = later\n        self.ext = []\n\n        while True:\n            try:\n                bp = os.path.expanduser(args)\n                if WINDOWS:\n                    bp = uncyg(bp)\n\n                if bos.path.exists(bp):\n                    self.bin = bp\n                    return\n            except:\n                pass\n\n            arg, args = args.split(\",\", 1)\n            arg = arg.lower()\n\n            if arg.startswith(\"a\"):\n                self.audio = arg[1:]  # [r]equire [n]ot [d]ontcare\n                continue\n\n            if arg.startswith(\"k\"):\n                self.kill = arg[1:]  # [t]ree [m]ain [n]one\n                continue\n\n            if arg.startswith(\"c\"):\n                self.capture = int(arg[1:])  # 0=none 1=stdout 2=stderr 3=both\n                continue\n\n            if arg == \"f\":\n                self.force = True\n                continue\n\n            if arg.startswith(\"t\"):\n                self.timeout = int(arg[1:])\n                continue\n\n            if arg.startswith(\"e\"):\n                self.ext.append(arg[1:])\n                continue\n\n            if arg.startswith(\"p\"):\n                self.pri = int(arg[1:] or \"1\")\n                continue\n\n            raise Exception()\n\n\ndef au_unpk(\n    log: \"NamedLogger\", fmt_map: dict[str, str], abspath: str, vn: Optional[VFS] = None\n) -> str:\n    fd = 0\n    ret = \"\"\n    maxsz = 1024 * 1024 * 64\n    try:\n        ext = abspath.split(\".\")[-1].lower()\n        au, pk = fmt_map[ext].split(\".\")\n\n        fd, ret = tempfile.mkstemp(\".\" + au)\n\n        if pk == \"gz\":\n            fi = gzip.GzipFile(abspath, mode=\"rb\")\n\n        elif pk == \"xz\":\n            import lzma\n\n            fi = lzma.open(abspath, \"rb\")\n\n        elif pk == \"zip\":\n            import zipfile\n\n            zf = zipfile.ZipFile(abspath, \"r\")\n            zil = zf.infolist()\n            zil = [x for x in zil if x.filename.lower().split(\".\")[-1] == au]\n            if not zil:\n                raise Exception(\"no audio inside zip\")\n            fi = zf.open(zil[0])\n\n        elif pk == \"cbz\":\n            import zipfile\n\n            zf = zipfile.ZipFile(abspath, \"r\")\n            znil = [(x.filename.lower(), x) for x in zf.infolist()]\n            nf = len(znil)\n            znil = [x for x in znil if x[0].split(\".\")[-1] in CBZ_PICS]\n            znil = [x for x in znil if \"cover\" in x[0]] or znil\n            znil = [x for x in znil if CBZ_01.search(x[0])] or znil\n            t = \"cbz: %d files, %d hits\" % (nf, len(znil))\n            if not znil:\n                raise Exception(\"no images inside cbz\")\n            using = sorted(znil)[0][1].filename\n            if znil:\n                t += \", using \" + using\n            log(t)\n            fi = zf.open(using)\n\n        elif pk == \"epub\":\n            fi = get_cover_from_epub(log, abspath)\n            assert fi  # !rm\n\n        else:\n            raise Exception(\"unknown compression %s\" % (pk,))\n\n        fsz = 0\n        with os.fdopen(fd, \"wb\") as fo:\n            fd = 0\n            while True:\n                buf = fi.read(32768)\n                if not buf:\n                    break\n\n                fsz += len(buf)\n                if fsz > maxsz:\n                    raise Exception(\"zipbomb defused\")\n\n                fo.write(buf)\n\n        return ret\n\n    except Exception as ex:\n        if fd:\n            os.close(fd)\n        if ret:\n            t = \"failed to decompress file %r: %r\"\n            log(t % (abspath, ex))\n            wunlink(log, ret, vn.flags if vn else VF_CAREFUL)\n            return \"\"\n\n        return abspath\n\n\ndef ffprobe(\n    abspath: str, timeout: int = 60\n) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:\n    cmd = [\n        b\"ffprobe\",\n        b\"-hide_banner\",\n        b\"-show_streams\",\n        b\"-show_format\",\n        b\"--\",\n        fsenc(abspath),\n    ]\n    rc, so, se = runcmd(cmd, timeout=timeout, nice=True, oom=200)\n    retchk(rc, cmd, se)\n    return parse_ffprobe(so)\n\n\ndef parse_ffprobe(\n    txt: str,\n) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:\n    \"\"\"\n    txt: output from ffprobe -show_format -show_streams\n    returns:\n     * normalized tags\n     * original/raw tags\n     * list of streams\n     * format props\n    \"\"\"\n    streams = []\n    fmt = {}\n    g = {}\n    for ln in [x.rstrip(\"\\r\") for x in txt.split(\"\\n\")]:\n        try:\n            sk, sv = ln.split(\"=\", 1)\n            g[sk] = sv\n            continue\n        except:\n            pass\n\n        if ln == \"[STREAM]\":\n            g = {}\n            streams.append(g)\n\n        if ln == \"[FORMAT]\":\n            g = {\"codec_type\": \"format\"}  # heh\n            fmt = g\n\n    streams = [fmt] + streams\n    ret: dict[str, Any] = {}  # processed\n    md: dict[str, list[Any]] = {}  # raw tags\n\n    is_audio = fmt.get(\"format_name\") in FMT_AU\n    if fmt.get(\"filename\", \"\").split(\".\")[-1].lower() in M4A:\n        is_audio = True\n\n    # if audio file, ensure audio stream appears first\n    if (\n        is_audio\n        and len(streams) > 2\n        and streams[1].get(\"codec_type\") != \"audio\"\n        and streams[2].get(\"codec_type\") == \"audio\"\n    ):\n        streams = [fmt, streams[2], streams[1]] + streams[3:]\n\n    have = {}\n    for strm in streams:\n        typ = strm.get(\"codec_type\")\n        if typ in have:\n            continue\n\n        have[typ] = True\n        kvm = []\n\n        if typ == \"audio\":\n            kvm = [\n                [\"codec_name\", \"ac\"],\n                [\"channel_layout\", \"chs\"],\n                [\"sample_rate\", \".hz\"],\n                [\"bit_rate\", \".aq\"],\n                [\"bits_per_sample\", \".bps\"],\n                [\"bits_per_raw_sample\", \".bprs\"],\n                [\"duration\", \".dur\"],\n            ]\n\n        if typ == \"video\":\n            if strm.get(\"DISPOSITION:attached_pic\") == \"1\" or is_audio:\n                continue\n\n            kvm = [\n                [\"codec_name\", \"vc\"],\n                [\"pix_fmt\", \"pixfmt\"],\n                [\"r_frame_rate\", \".fps\"],\n                [\"bit_rate\", \".vq\"],\n                [\"width\", \".resw\"],\n                [\"height\", \".resh\"],\n                [\"duration\", \".dur\"],\n            ]\n\n        if typ == \"format\":\n            kvm = [[\"duration\", \".dur\"], [\"bit_rate\", \".q\"], [\"format_name\", \"fmt\"]]\n\n        for sk, rk in kvm:\n            v1 = strm.get(sk)\n            if v1 is None:\n                continue\n\n            if rk.startswith(\".\"):\n                try:\n                    zf = float(v1)\n                    v2 = ret.get(rk)\n                    if v2 is None or zf > v2:\n                        ret[rk] = zf\n                except:\n                    # sqlite doesnt care but the code below does\n                    if v1 not in [\"N/A\"]:\n                        ret[rk] = v1\n            else:\n                ret[rk] = v1\n\n    if ret.get(\"vc\") == \"ansi\":  # shellscript\n        return {}, {}, [], {}\n\n    for strm in streams:\n        for sk, sv in strm.items():\n            if not sk.startswith(\"TAG:\"):\n                continue\n\n            sk = sk[4:].strip()\n            sv = sv.strip()\n            if sk and sv and sk not in md:\n                md[sk] = [sv]\n\n    for sk in [\".q\", \".vq\", \".aq\"]:\n        if sk in ret:\n            ret[sk] /= 1000  # bit_rate=320000\n\n    for sk in [\".q\", \".vq\", \".aq\", \".resw\", \".resh\"]:\n        if sk in ret:\n            ret[sk] = int(ret[sk])\n\n    if \".fps\" in ret:\n        fps = ret[\".fps\"]\n        if \"/\" in fps:\n            fa, fb = fps.split(\"/\")\n            try:\n                fps = float(fa) / float(fb)\n            except:\n                fps = 9001\n\n        if fps < 1000 and fmt.get(\"format_name\") not in [\"image2\", \"png_pipe\"]:\n            ret[\".fps\"] = round(fps, 3)\n        else:\n            del ret[\".fps\"]\n\n    if \".dur\" in ret:\n        if ret[\".dur\"] < 0.1:\n            del ret[\".dur\"]\n            if \".q\" in ret:\n                del ret[\".q\"]\n\n    if \"fmt\" in ret:\n        ret[\"fmt\"] = ret[\"fmt\"].split(\",\")[0]\n\n    if \".resw\" in ret and \".resh\" in ret:\n        ret[\"res\"] = \"{}x{}\".format(ret[\".resw\"], ret[\".resh\"])\n\n    zero = int(\"0\")\n    zd = {k: (zero, v) for k, v in ret.items()}\n\n    return zd, md, streams, fmt\n\n\ndef get_cover_from_epub(log: \"NamedLogger\", abspath: str) -> Optional[IO[bytes]]:\n    import zipfile\n\n    from .dxml import parse_xml\n\n    try:\n        from urlparse import urljoin  # type: ignore  # Python2\n    except ImportError:\n        from urllib.parse import urljoin  # Python3\n\n    with zipfile.ZipFile(abspath, \"r\") as z:\n        # First open the container file to find the package document (.opf file)\n        try:\n            container_root = parse_xml(z.read(\"META-INF/container.xml\").decode())\n        except KeyError:\n            log(\"epub: no container file found in %s\" % (abspath,))\n            return None\n\n        # https://www.w3.org/TR/epub-33/#sec-container.xml-rootfile-elem\n        container_ns = {\"\": \"urn:oasis:names:tc:opendocument:xmlns:container\"}\n        # One file could contain multiple package documents, default to the first one\n        rootfile_path = container_root.find(\"./rootfiles/rootfile\", container_ns).get(\n            \"full-path\"\n        )\n\n        # Then open the first package document to find the path of the cover image\n        try:\n            package_root = parse_xml(z.read(rootfile_path).decode())\n        except KeyError:\n            log(\"epub: no package document found in %s\" % (abspath,))\n            return None\n\n        # https://www.w3.org/TR/epub-33/#sec-package-doc\n        package_ns = {\"\": \"http://www.idpf.org/2007/opf\"}\n        # https://www.w3.org/TR/epub-33/#sec-cover-image\n        coverimage_path_node = package_root.find(\n            \"./manifest/item[@properties='cover-image']\", package_ns\n        )\n        if coverimage_path_node is not None:\n            coverimage_path = coverimage_path_node.get(\"href\")\n        else:\n            # This might be an EPUB2 file, try the legacy way of specifying covers\n            coverimage_path = _get_cover_from_epub2(log, package_root, package_ns)\n\n        if not coverimage_path:\n            raise Exception(\"no cover inside epub\")\n\n        # This url is either absolute (in the .epub) or relative to the package document\n        adjusted_cover_path = urljoin(rootfile_path, coverimage_path)\n\n        try:\n            return z.open(adjusted_cover_path)\n        except KeyError:\n            t = \"epub: cover specified in package document, but doesn't exist: %s\"\n            log(t % (adjusted_cover_path,))\n\n\ndef _get_cover_from_epub2(\n    log: \"NamedLogger\", package_root, package_ns\n) -> Optional[str]:\n    # <meta name=\"cover\" content=\"id-to-cover-image\"> in <metadata>, then\n    # <item> in <manifest>\n    xn = package_root.find(\"./metadata/meta[@name='cover']\", package_ns)\n    cover_id = xn.get(\"content\") if xn is not None else None\n\n    if not cover_id:\n        return None\n\n    for node in package_root.iterfind(\"./manifest/item\", package_ns):\n        if node.get(\"id\") == cover_id:\n            cover_path = node.get(\"href\")\n            return cover_path\n\n    return None\n\n\nclass MTag(object):\n    def __init__(self, log_func: \"RootLogger\", args: argparse.Namespace) -> None:\n        self.log_func = log_func\n        self.args = args\n        self.usable = True\n        self.prefer_mt = not args.no_mtag_ff\n        self.backend = (\n            \"ffprobe\" if args.no_mutagen or (HAVE_FFPROBE and EXE) else \"mutagen\"\n        )\n        self.can_ffprobe = HAVE_FFPROBE and not args.no_mtag_ff\n        self.read_xattrs = args.have_db_xattr\n        self.get = self._get_xattr if self.read_xattrs else self._get_main\n        mappings = args.mtm\n        or_ffprobe = \" or FFprobe\"\n\n        if self.backend == \"mutagen\":\n            self._get = self.get_mutagen\n            if not HAVE_MUTAGEN:\n                self.log(\"could not load Mutagen, trying FFprobe instead\", c=3)\n                self.backend = \"ffprobe\"\n\n        if self.backend == \"ffprobe\":\n            self.usable = self.can_ffprobe\n            self._get = self.get_ffprobe\n            self.prefer_mt = True\n\n            if not HAVE_FFPROBE:\n                pass\n\n            elif args.no_mtag_ff:\n                msg = \"found FFprobe but it was disabled by --no-mtag-ff\"\n                self.log(msg, c=3)\n\n        if self.read_xattrs and not self.usable:\n            t = \"don't have the necessary dependencies to read conventional media tags, but will read xattrs\"\n            self.log(t)\n            self.usable = True\n\n        if not self.usable:\n            self._get = None\n            if EXE:\n                t = \"copyparty.exe cannot use mutagen; need ffprobe.exe to read media tags: \"\n                self.log(t + FFMPEG_URL)\n                return\n\n            msg = \"need Mutagen{} to read media tags so please run this:\\n{}{} -m pip install --user mutagen\\n\"\n            pyname = os.path.basename(pybin)\n            self.log(msg.format(or_ffprobe, \" \" * 37, pyname), c=1)\n            return\n\n        # https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html\n        tagmap = {\n            \"album\": [\"album\", \"talb\", \"\\u00a9alb\", \"original-album\", \"toal\"],\n            \"artist\": [\n                \"artist\",\n                \"tpe1\",\n                \"\\u00a9art\",\n                \"composer\",\n                \"performer\",\n                \"arranger\",\n                \"\\u00a9wrt\",\n                \"tcom\",\n                \"tpe3\",\n                \"original-artist\",\n                \"tope\",\n            ],\n            \"title\": [\"title\", \"tit2\", \"\\u00a9nam\"],\n            \"circle\": [\n                \"album-artist\",\n                \"tpe2\",\n                \"aart\",\n                \"organization\",\n                \"band\",\n            ],\n            \".tn\": [\"tracknumber\", \"trck\", \"trkn\", \"track\"],\n            \"genre\": [\"genre\", \"tcon\", \"\\u00a9gen\"],\n            \"tdate\": [\n                \"original-release-date\",\n                \"release-date\",\n                \"date\",\n                \"tdrc\",\n                \"\\u00a9day\",\n                \"original-date\",\n                \"original-year\",\n                \"tyer\",\n                \"tdor\",\n                \"tory\",\n                \"year\",\n                \"creation-time\",\n            ],\n            \".bpm\": [\"bpm\", \"tbpm\", \"tmpo\", \"tbp\"],\n            \"key\": [\"initial-key\", \"tkey\", \"key\"],\n            \"comment\": [\"comment\", \"comm\", \"\\u00a9cmt\", \"comments\", \"description\"],\n        }\n\n        if mappings:\n            for k, v in [x.split(\"=\") for x in mappings]:\n                tagmap[k] = v.split(\",\")\n\n        self.tagmap = {}\n        for k, vs in tagmap.items():\n            vs2 = []\n            for v in vs:\n                if \"-\" not in v:\n                    vs2.append(v)\n                    continue\n\n                vs2.append(v.replace(\"-\", \" \"))\n                vs2.append(v.replace(\"-\", \"_\"))\n                vs2.append(v.replace(\"-\", \"\"))\n\n            self.tagmap[k] = vs2\n\n        self.rmap = {\n            v: [n, k] for k, vs in self.tagmap.items() for n, v in enumerate(vs)\n        }\n        # self.get = self.compare\n\n    def log(self, msg: str, c: Union[int, str] = 0) -> None:\n        self.log_func(\"mtag\", msg, c)\n\n    def normalize_tags(\n        self, parser_output: dict[str, tuple[int, Any]], md: dict[str, list[Any]]\n    ) -> dict[str, Union[str, float]]:\n        for sk, tv in dict(md).items():\n            if not tv:\n                continue\n\n            sk = sk.lower().split(\"::\")[0].strip()\n            key_mapping = self.rmap.get(sk)\n            if not key_mapping:\n                continue\n\n            priority, alias = key_mapping\n            if alias not in parser_output or parser_output[alias][0] > priority:\n                parser_output[alias] = (priority, tv[0])\n\n        # take first value (lowest priority / most preferred)\n        ret: dict[str, Union[str, float]] = {\n            sk: unicode(tv[1]).strip() for sk, tv in parser_output.items()\n        }\n\n        # track 3/7 => track 3\n        for sk, zv in ret.items():\n            if sk[0] == \".\":\n                sv = str(zv).split(\"/\")[0].strip().lstrip(\"0\")\n                ret[sk] = sv or 0\n\n        # normalize key notation to rekobo\n        okey = ret.get(\"key\")\n        if okey:\n            key = str(okey).replace(\" \", \"\").replace(\"maj\", \"\").replace(\"min\", \"m\")\n            ret[\"key\"] = REKOBO_LKEY.get(key.lower(), okey)\n\n        if self.args.mtag_vv:\n            zl = \" \".join(\"\\033[36m{} \\033[33m{}\".format(k, v) for k, v in ret.items())\n            self.log(\"norm: {}\\033[0m\".format(zl), \"90\")\n\n        return ret\n\n    def compare(self, abspath: str) -> dict[str, Union[str, float]]:\n        if abspath.endswith(\".au\"):\n            return {}\n\n        print(\"\\n\" + abspath)\n        r1 = self.get_mutagen(abspath)\n        r2 = self.get_ffprobe(abspath)\n\n        keys = {}\n        for d in [r1, r2]:\n            for k in d.keys():\n                keys[k] = True\n\n        diffs = []\n        l1 = []\n        l2 = []\n        for k in sorted(keys.keys()):\n            if k in [\".q\", \".dur\"]:\n                continue  # lenient\n\n            v1 = r1.get(k)\n            v2 = r2.get(k)\n            if v1 == v2:\n                print(\"  \", k, v1)\n            elif v1 != \"0000\":  # FFprobe date=0\n                diffs.append(k)\n                print(\" 1\", k, v1)\n                print(\" 2\", k, v2)\n                if v1:\n                    l1.append(k)\n                if v2:\n                    l2.append(k)\n\n        if diffs:\n            raise Exception()\n\n        return r1\n\n    def _get_xattr(\n        self, abspath: str, vf: dict[str, Any]\n    ) -> dict[str, Union[str, float]]:\n        ret = self._get_main(abspath, vf) if self._get else {}\n        if \"db_xattr_no\" in vf:\n            try:\n                neg = vf[\"db_xattr_no\"]\n                zsl = os.listxattr(abspath)\n                zsl = [x for x in zsl if x not in neg]\n                for xattr in zsl:\n                    zb = os.getxattr(abspath, xattr)\n                    ret[xattr] = zb.decode(\"utf-8\", \"replace\")\n            except:\n                self.log(\"failed to read xattrs from [%s]\\n%s\" % (abspath, min_ex()), 3)\n        elif \"db_xattr_yes\" in vf:\n            for xattr in vf[\"db_xattr_yes\"]:\n                if \"=\" in xattr:\n                    xattr, name = xattr.split(\"=\", 1)\n                else:\n                    name = xattr\n                try:\n                    zs = os.getxattr(abspath, xattr)\n                    ret[name] = zs.decode(\"utf-8\", \"replace\")\n                except:\n                    pass\n        return ret\n\n    def _get_main(\n        self, abspath: str, vf: dict[str, Any]\n    ) -> dict[str, Union[str, float]]:\n        ext = abspath.split(\".\")[-1].lower()\n        if ext not in self.args.au_unpk:\n            return self._get(abspath)\n\n        ap = au_unpk(self.log, self.args.au_unpk, abspath)\n        if not ap:\n            return {}\n\n        ret = self._get(ap)\n        if ap != abspath:\n            wunlink(self.log, ap, VF_CAREFUL)\n        return ret\n\n    def get_mutagen(self, abspath: str) -> dict[str, Union[str, float]]:\n        ret: dict[str, tuple[int, Any]] = {}\n\n        if not bos.path.isfile(abspath):\n            return {}\n\n        from mutagen import File\n\n        try:\n            md = File(fsenc(abspath), easy=True)\n            assert md\n            if self.args.mtag_vv:\n                for zd in (md.info.__dict__, dict(md.tags)):\n                    zl = [\"\\033[36m{} \\033[33m{}\".format(k, v) for k, v in zd.items()]\n                    self.log(\"mutagen: {}\\033[0m\".format(\" \".join(zl)), \"90\")\n            if not md.info.length and not md.info.codec:\n                raise Exception()\n        except Exception as ex:\n            if self.args.mtag_v:\n                self.log(\"mutagen-err [%s] @ %r\" % (ex, abspath), \"90\")\n\n            return self.get_ffprobe(abspath) if self.can_ffprobe else {}\n\n        sz = bos.path.getsize(abspath)\n        try:\n            ret[\".q\"] = (0, int((sz / md.info.length) / 128))\n        except:\n            pass\n\n        for attr, k, norm in [\n            [\"codec\", \"ac\", unicode],\n            [\"channels\", \"chs\", int],\n            [\"sample_rate\", \".hz\", int],\n            [\"bitrate\", \".aq\", int],\n            [\"length\", \".dur\", int],\n        ]:\n            try:\n                v = getattr(md.info, attr)\n            except:\n                if k != \"ac\":\n                    continue\n\n                try:\n                    v = str(md.info).split(\".\")[1]\n                    if v.startswith(\"ogg\"):\n                        v = v[3:]\n                except:\n                    continue\n\n            if not v:\n                continue\n\n            if k == \".aq\":\n                v /= 1000  # type: ignore\n\n            if k == \"ac\" and v.startswith(\"mp4a.40.\"):\n                v = \"aac\"\n\n            ret[k] = (0, norm(v))\n\n        return self.normalize_tags(ret, md)\n\n    def get_ffprobe(self, abspath: str) -> dict[str, Union[str, float]]:\n        if not bos.path.isfile(abspath):\n            return {}\n\n        ret, md, _, _ = ffprobe(abspath, self.args.mtag_to)\n\n        if self.args.mtag_vv:\n            for zd in (ret, dict(md)):\n                zl = [\"\\033[36m{} \\033[33m{}\".format(k, v) for k, v in zd.items()]\n                self.log(\"ffprobe: {}\\033[0m\".format(\" \".join(zl)), \"90\")\n\n        return self.normalize_tags(ret, md)\n\n    def get_bin(\n        self, parsers: dict[str, MParser], abspath: str, oth_tags: dict[str, Any]\n    ) -> dict[str, Any]:\n        if not bos.path.isfile(abspath):\n            return {}\n\n        env = os.environ.copy()\n        try:\n            if EXE:\n                raise Exception()\n\n            pypath = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))\n            zsl = [str(pypath)] + [str(x) for x in sys.path if x]\n            pypath = str(os.pathsep.join(zsl))\n            env[\"PYTHONPATH\"] = pypath\n        except:\n            raise  # might be expected outside cpython\n\n        ext = abspath.split(\".\")[-1].lower()\n        if ext in self.args.au_unpk:\n            ap = au_unpk(self.log, self.args.au_unpk, abspath)\n        else:\n            ap = abspath\n\n        ret: dict[str, Any] = {}\n        if not ap:\n            return ret\n\n        for tagname, parser in sorted(parsers.items(), key=lambda x: (x[1].pri, x[0])):\n            try:\n                cmd = [parser.bin, ap]\n                if parser.bin.endswith(\".py\"):\n                    cmd = [pybin] + cmd\n\n                args = {\n                    \"env\": env,\n                    \"nice\": True,\n                    \"oom\": 300,\n                    \"timeout\": parser.timeout,\n                    \"kill\": parser.kill,\n                    \"capture\": parser.capture,\n                }\n\n                if parser.pri:\n                    zd = oth_tags.copy()\n                    zd.update(ret)\n                    args[\"sin\"] = json.dumps(zd).encode(\"utf-8\", \"replace\")\n\n                bcmd = [sfsenc(x) for x in cmd[:-1]] + [fsenc(cmd[-1])]\n                rc, v, err = runcmd(bcmd, **args)  # type: ignore\n                retchk(rc, bcmd, err, self.log, 5, self.args.mtag_v)\n                v = v.strip()\n                if not v:\n                    continue\n\n                if \",\" not in tagname:\n                    ret[tagname] = v\n                else:\n                    zj = json.loads(v)\n                    for tag in tagname.split(\",\"):\n                        if tag and tag in zj:\n                            ret[tag] = zj[tag]\n            except:\n                if self.args.mtag_v:\n                    t = \"mtag error: tagname %r, parser %r, file %r => %r\"\n                    self.log(t % (tagname, parser.bin, abspath, min_ex()), 6)\n\n        if ap != abspath:\n            wunlink(self.log, ap, VF_CAREFUL)\n\n        return ret\n"
  },
  {
    "path": "copyparty/multicast.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport socket\nimport time\n\nimport ipaddress\nfrom ipaddress import (\n    IPv4Address,\n    IPv4Network,\n    IPv6Address,\n    IPv6Network,\n    ip_address,\n    ip_network,\n)\n\nfrom .__init__ import MACOS, TYPE_CHECKING\nfrom .util import IP6_LL, IP64_LL, Daemon, Netdev, find_prefix, min_ex, spack\n\nif TYPE_CHECKING:\n    from .svchub import SvcHub\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Optional, Union\n\nif not hasattr(socket, \"IPPROTO_IPV6\"):\n    setattr(socket, \"IPPROTO_IPV6\", 41)\n\n\nclass NoIPs(Exception):\n    pass\n\n\nclass MC_Sck(object):\n    \"\"\"there is one socket for each server ip\"\"\"\n\n    def __init__(\n        self,\n        sck: socket.socket,\n        nd: Netdev,\n        grp: str,\n        ip: str,\n        net: Union[IPv4Network, IPv6Network],\n    ):\n        self.sck = sck\n        self.idx = nd.idx\n        self.name = nd.name\n        self.grp = grp\n        self.mreq = b\"\"\n        self.ip = ip\n        self.net = net\n        self.ips = {ip: net}\n        self.v6 = \":\" in ip\n        self.have4 = \":\" not in ip\n        self.have6 = \":\" in ip\n\n\nclass MCast(object):\n    def __init__(\n        self,\n        hub: \"SvcHub\",\n        Srv: type[MC_Sck],\n        on: list[str],\n        off: list[str],\n        mc_grp_4: str,\n        mc_grp_6: str,\n        port: int,\n        vinit: bool,\n    ) -> None:\n        \"\"\"disable ipv%d by setting mc_grp_%d empty\"\"\"\n        self.hub = hub\n        self.Srv = Srv\n        self.args = hub.args\n        self.asrv = hub.asrv\n        self.log_func = hub.log\n        self.on = on\n        self.off = off\n        self.grp4 = mc_grp_4\n        self.grp6 = mc_grp_6\n        self.port = port\n        self.vinit = vinit\n\n        self.srv: dict[socket.socket, MC_Sck] = {}  # listening sockets\n        self.sips: set[str] = set()  # all listening ips (including failed attempts)\n        self.ll_ok: set[str] = set()  # fallback linklocal IPv4 and IPv6 addresses\n        self.b2srv: dict[bytes, MC_Sck] = {}  # binary-ip -> server socket\n        self.b4: list[bytes] = []  # sorted list of binary-ips\n        self.b6: list[bytes] = []  # sorted list of binary-ips\n        self.cscache: dict[str, Optional[MC_Sck]] = {}  # client ip -> server cache\n\n        self.running = True\n\n    def log(self, msg: str, c: Union[int, str] = 0) -> None:\n        self.log_func(\"multicast\", msg, c)\n\n    def create_servers(self) -> list[str]:\n        bound: list[str] = []\n        netdevs = self.hub.tcpsrv.netdevs\n        blist = self.hub.tcpsrv.bound\n        if self.args.http_no_tcp:\n            blist = self.hub.tcpsrv.seen_eps\n        ips = [x[0] for x in blist]\n\n        if \"::\" in ips:\n            ips = [x for x in ips if x != \"::\"] + list(\n                [x.split(\"/\")[0] for x in netdevs if \":\" in x]\n            )\n            ips.append(\"0.0.0.0\")\n\n        if \"0.0.0.0\" in ips:\n            ips = [x for x in ips if x != \"0.0.0.0\"] + list(\n                [x.split(\"/\")[0] for x in netdevs if \":\" not in x]\n            )\n\n        ips = [x for x in ips if x not in (\"::1\", \"127.0.0.1\")]\n        ips = find_prefix(ips, list(netdevs))\n\n        on = self.on[:]\n        off = self.off[:]\n        for lst in (on, off):\n            for av in list(lst):\n                try:\n                    arg_net = ip_network(av, False)\n                except:\n                    arg_net = None\n\n                for sk, sv in netdevs.items():\n                    if arg_net:\n                        net_ip = ip_address(sk.split(\"/\")[0])\n                        if net_ip in arg_net and sk not in lst:\n                            lst.append(sk)\n\n                    if (av == str(sv.idx) or av == sv.name) and sk not in lst:\n                        lst.append(sk)\n\n        if on:\n            ips = [x for x in ips if x in on]\n        elif off:\n            ips = [x for x in ips if x not in off]\n\n        if not self.grp4:\n            ips = [x for x in ips if \":\" in x]\n\n        if not self.grp6:\n            ips = [x for x in ips if \":\" not in x]\n\n        ips = list(set(ips))\n        all_selected = ips[:]\n\n        # discard non-linklocal ipv6\n        ips = [x for x in ips if \":\" not in x or x.startswith(IP6_LL)]\n\n        if not ips:\n            raise NoIPs()\n\n        for ip in ips:\n            v6 = \":\" in ip\n            netdev = netdevs[ip]\n            if not netdev.idx:\n                t = \"using INADDR_ANY for ip [{}], netdev [{}]\"\n                if not self.srv and ip not in [\"::\", \"0.0.0.0\"]:\n                    self.log(t.format(ip, netdev), 3)\n\n            ipv = socket.AF_INET6 if v6 else socket.AF_INET\n            sck = socket.socket(ipv, socket.SOCK_DGRAM, socket.IPPROTO_UDP)\n            sck.settimeout(None)\n            sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n            try:\n                # safe for this purpose; https://lwn.net/Articles/853637/\n                sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)\n            except:\n                pass\n\n            # most ipv6 clients expect multicast on linklocal ip only;\n            # add a/aaaa records for the other nic IPs\n            other_ips: set[str] = set()\n            if v6:\n                for nd in netdevs.values():\n                    if nd.idx == netdev.idx and nd.ip in all_selected and \":\" in nd.ip:\n                        other_ips.add(nd.ip)\n\n            net = ipaddress.ip_network(ip, False)\n            ip = ip.split(\"/\")[0]\n            srv = self.Srv(sck, netdev, self.grp6 if \":\" in ip else self.grp4, ip, net)\n            for oth_ip in other_ips:\n                srv.ips[oth_ip.split(\"/\")[0]] = ipaddress.ip_network(oth_ip, False)\n\n            # gvfs breaks if a linklocal ip appears in a dns reply\n            ll = {k: v for k, v in srv.ips.items() if k.startswith(IP64_LL)}\n            rt = {k: v for k, v in srv.ips.items() if k not in ll}\n\n            if self.args.ll or not rt:\n                self.ll_ok.update(list(ll))\n\n            if not self.args.ll:\n                srv.ips = rt or ll\n\n            if not srv.ips:\n                self.log(\"no IPs on {}; skipping [{}]\".format(netdev, ip), 3)\n                continue\n\n            try:\n                self.setup_socket(srv)\n                self.srv[sck] = srv\n                bound.append(ip)\n            except:\n                t = \"announce failed on {} [{}]:\\n{}\"\n                self.log(t.format(netdev, ip, min_ex()), 3)\n                sck.close()\n\n        if self.args.zm_msub:\n            for s1 in self.srv.values():\n                for s2 in self.srv.values():\n                    if s1.idx != s2.idx:\n                        continue\n\n                    if s1.ip not in s2.ips:\n                        s2.ips[s1.ip] = s1.net\n\n        if self.args.zm_mnic:\n            for s1 in self.srv.values():\n                for s2 in self.srv.values():\n                    for ip1, net1 in list(s1.ips.items()):\n                        for ip2, net2 in list(s2.ips.items()):\n                            if net1 == net2 and ip1 != ip2:\n                                s1.ips[ip2] = net2\n\n        self.sips = set([x.split(\"/\")[0] for x in all_selected])\n        for srv in self.srv.values():\n            assert srv.ip in self.sips\n\n        Daemon(self.hopper, \"mc-hop\")\n        return bound\n\n    def setup_socket(self, srv: MC_Sck) -> None:\n        sck = srv.sck\n        if srv.v6:\n            if self.vinit:\n                zsl = list(srv.ips.keys())\n                self.log(\"v6({}) idx({}) {}\".format(srv.ip, srv.idx, zsl), 6)\n\n            for ip in srv.ips:\n                bip = socket.inet_pton(socket.AF_INET6, ip)\n                self.b2srv[bip] = srv\n                self.b6.append(bip)\n\n            grp = self.grp6 if srv.idx else \"\"\n            try:\n                if MACOS:\n                    raise Exception()\n\n                sck.bind((grp, self.port, 0, srv.idx))\n            except:\n                sck.bind((\"\", self.port, 0, srv.idx))\n\n            bgrp = socket.inet_pton(socket.AF_INET6, self.grp6)\n            dev = spack(b\"@I\", srv.idx)\n            srv.mreq = bgrp + dev\n            if srv.idx != socket.INADDR_ANY:\n                sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, dev)\n\n            try:\n                sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 255)\n                sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 1)\n            except:\n                # macos\n                t = \"failed to set IPv6 TTL/LOOP; announcements may not survive multiple switches/routers\"\n                self.log(t, 3)\n        else:\n            if self.vinit:\n                self.log(\"v4({}) idx({})\".format(srv.ip, srv.idx), 6)\n\n            bip = socket.inet_aton(srv.ip)\n            self.b2srv[bip] = srv\n            self.b4.append(bip)\n\n            grp = self.grp4 if srv.idx else \"\"\n            try:\n                if MACOS:\n                    raise Exception()\n\n                sck.bind((grp, self.port))\n            except:\n                sck.bind((\"\", self.port))\n\n            bgrp = socket.inet_aton(self.grp4)\n            dev = (\n                spack(b\"=I\", socket.INADDR_ANY)\n                if srv.idx == socket.INADDR_ANY\n                else socket.inet_aton(srv.ip)\n            )\n            srv.mreq = bgrp + dev\n            if srv.idx != socket.INADDR_ANY:\n                sck.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, dev)\n\n            try:\n                sck.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255)\n                sck.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1)\n            except:\n                # probably can't happen but dontcare if it does\n                t = \"failed to set IPv4 TTL/LOOP; announcements may not survive multiple switches/routers\"\n                self.log(t, 3)\n\n        if self.hop(srv, False):\n            self.log(\"igmp was already joined?? chilling for a sec\", 3)\n            time.sleep(1.2)\n\n        self.hop(srv, True)\n        self.b4.sort(reverse=True)\n        self.b6.sort(reverse=True)\n\n    def hop(self, srv: MC_Sck, on: bool) -> bool:\n        \"\"\"rejoin to keepalive on routers/switches without igmp-snooping\"\"\"\n        sck = srv.sck\n        req = srv.mreq\n        if \":\" in srv.ip:\n            if not on:\n                try:\n                    sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_LEAVE_GROUP, req)\n                    return True\n                except:\n                    return False\n            else:\n                sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, req)\n        else:\n            if not on:\n                try:\n                    sck.setsockopt(socket.IPPROTO_IP, socket.IP_DROP_MEMBERSHIP, req)\n                    return True\n                except:\n                    return False\n            else:\n                # t = \"joining {} from ip {} idx {} with mreq {}\"\n                # self.log(t.format(srv.grp, srv.ip, srv.idx, repr(srv.mreq)), 6)\n                sck.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, req)\n\n        return True\n\n    def hopper(self):\n        while self.args.mc_hop and self.running:\n            time.sleep(self.args.mc_hop)\n            if not self.running:\n                return\n\n            for srv in self.srv.values():\n                self.hop(srv, False)\n\n            # linux does leaves/joins twice with 0.2~1.05s spacing\n            time.sleep(1.2)\n            if not self.running:\n                return\n\n            for srv in self.srv.values():\n                self.hop(srv, True)\n\n    def map_client(self, cip: str) -> Optional[MC_Sck]:\n        try:\n            return self.cscache[cip]\n        except:\n            pass\n\n        ret: Optional[MC_Sck] = None\n        v6 = \":\" in cip\n        ci = IPv6Address(cip) if v6 else IPv4Address(cip)\n        for x in self.b6 if v6 else self.b4:\n            srv = self.b2srv[x]\n            if any([x for x in srv.ips.values() if ci in x]):\n                ret = srv\n                break\n\n        if not ret and cip in (\"127.0.0.1\", \"::1\"):\n            # just give it something\n            ret = list(self.srv.values())[0]\n\n        if not ret and cip.startswith(\"169.254\"):\n            # idk how to map LL IPv4 msgs to nics;\n            # just pick one and hope for the best\n            lls = (\n                x\n                for x in self.srv.values()\n                if next((y for y in x.ips if y in self.ll_ok), None)\n            )\n            ret = next(lls, None)\n\n        if ret:\n            t = \"new client on {} ({}): {}\"\n            self.log(t.format(ret.name, ret.net, cip), 6)\n        else:\n            t = \"could not map client {} to known subnet; maybe forwarded from another network?\"\n            self.log(t.format(cip), 3)\n\n        if len(self.cscache) > 9000:\n            self.cscache = {}\n\n        self.cscache[cip] = ret\n        return ret\n"
  },
  {
    "path": "copyparty/pwhash.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport argparse\nimport base64\nimport hashlib\nimport os\nimport sys\nimport threading\n\nfrom .__init__ import unicode\n\ntry:\n    if os.environ.get(\"PRTY_NO_ARGON2\"):\n        raise Exception()\n\n    HAVE_ARGON2 = True\n    from argon2 import exceptions as argon2ex\nexcept:\n    HAVE_ARGON2 = False\n\n\nclass PWHash(object):\n    def __init__(self, args: argparse.Namespace):\n        self.args = args\n\n        zsl = args.ah_alg.split(\",\")\n        zsl = [x.strip() for x in zsl]\n        alg = zsl[0]\n        if alg == \"none\":\n            alg = \"\"\n\n        self.alg = alg\n        self.ac = zsl[1:]\n        if not alg:\n            self.on = False\n            self.hash = unicode\n            return\n\n        self.on = True\n        self.salt = args.ah_salt.encode(\"utf-8\")\n        self.cache: dict[str, str] = {}\n        self.mutex = threading.Lock()\n        self.hash = self._cache_hash\n\n        if alg == \"sha2\":\n            self._hash = self._gen_sha2\n        elif alg == \"scrypt\":\n            self._hash = self._gen_scrypt\n        elif alg == \"argon2\":\n            self._hash = self._gen_argon2\n        else:\n            t = \"unsupported password hashing algorithm [{}], must be one of these: argon2 scrypt sha2 none\"\n            raise Exception(t.format(alg))\n\n    def _cache_hash(self, plain: str) -> str:\n        with self.mutex:\n            try:\n                return self.cache[plain]\n            except:\n                pass\n\n            if not plain:\n                return \"\"\n\n            if len(plain) > 255:\n                raise Exception(\"password too long\")\n\n            if len(self.cache) > 9000:\n                self.cache = {}\n\n            ret = self._hash(plain)\n            self.cache[plain] = ret\n            return ret\n\n    def _gen_sha2(self, plain: str) -> str:\n        its = int(self.ac[0]) if self.ac else 424242\n        bplain = plain.encode(\"utf-8\")\n        ret = b\"\\n\"\n        for _ in range(its):\n            ret = hashlib.sha512(self.salt + bplain + ret).digest()\n\n        return \"+\" + base64.urlsafe_b64encode(ret[:24]).decode(\"utf-8\")\n\n    def _gen_scrypt(self, plain: str) -> str:\n        cost = 2 << 13\n        its = 2\n        blksz = 8\n        para = 4\n        ramcap = 0  # openssl 1.1 = 32 MiB\n        try:\n            cost = 2 << int(self.ac[0])\n            its = int(self.ac[1])\n            blksz = int(self.ac[2])\n            para = int(self.ac[3])\n            ramcap = int(self.ac[4]) * 1024 * 1024\n        except:\n            pass\n\n        cfg = {\"salt\": self.salt, \"n\": cost, \"r\": blksz, \"p\": para, \"dklen\": 24}\n        if ramcap:\n            cfg[\"maxmem\"] = ramcap\n\n        ret = plain.encode(\"utf-8\")\n        for _ in range(its):\n            ret = hashlib.scrypt(ret, **cfg)\n\n        return \"+\" + base64.urlsafe_b64encode(ret).decode(\"utf-8\")\n\n    def _gen_argon2(self, plain: str) -> str:\n        from argon2.low_level import Type as ArgonType\n        from argon2.low_level import hash_secret\n\n        time_cost = 3\n        mem_cost = 256\n        parallelism = 4\n        version = 19\n        try:\n            time_cost = int(self.ac[0])\n            mem_cost = int(self.ac[1])\n            parallelism = int(self.ac[2])\n            version = int(self.ac[3])\n        except:\n            pass\n\n        bplain = plain.encode(\"utf-8\")\n\n        bret = hash_secret(\n            secret=bplain,\n            salt=self.salt,\n            time_cost=time_cost,\n            memory_cost=mem_cost * 1024,\n            parallelism=parallelism,\n            hash_len=24,\n            type=ArgonType.ID,\n            version=version,\n        )\n        ret = bret.split(b\"$\")[-1].decode(\"utf-8\")\n        return \"+\" + ret.replace(\"/\", \"_\").replace(\"+\", \"-\")\n\n    def stdin(self) -> None:\n        while True:\n            ln = sys.stdin.readline().strip()\n            if not ln:\n                break\n            print(self.hash(ln))\n\n    def cli(self) -> None:\n        import getpass\n\n        if self.args.usernames:\n            t = \"since you have enabled --usernames, please provide username:password\"\n            print(t)\n\n        while True:\n            try:\n                p1 = getpass.getpass(\"password> \")\n                p2 = getpass.getpass(\"again or just hit ENTER> \")\n            except EOFError:\n                return\n\n            if p2 and p1 != p2:\n                print(\"\\033[31minputs don't match; try again\\033[0m\", file=sys.stderr)\n                continue\n            print(self.hash(p1))\n            print()\n"
  },
  {
    "path": "copyparty/qrkode.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport os\n\ntry:\n    if os.environ.get(\"PRTY_SYS_ALL\") or os.environ.get(\"PRTY_SYS_QRCG\"):\n        raise ImportError()\n    from .stolen.qrcodegen import QrCode\n\n    qrgen = QrCode.encode_binary\n    VENDORED = True\nexcept ImportError:\n    VENDORED = False\n    from qrcodegen import QrCode\n\nif os.environ.get(\"PRTY_MODSPEC\"):\n    from inspect import getsourcefile\n\n    print(\"PRTY_MODSPEC: qrcode:\", getsourcefile(QrCode))\n\nif True:  # pylint: disable=using-constant-test\n    import typing\n    from typing import Any, Optional, Sequence, Union\n\n\nif not VENDORED:\n\n    def _qrgen(data: Union[bytes, Sequence[int]]) -> \"QrCode\":\n        ret = None\n        V = QrCode.Ecc\n        for e in [V.HIGH, V.QUARTILE, V.MEDIUM, V.LOW]:\n            qr = QrCode.encode_binary(data, e)\n            qr.size = qr._size\n            qr.modules = qr._modules\n            if not ret or ret.size > qr.size:\n                ret = qr\n        return ret\n\n    qrgen = _qrgen\n\n\ndef qr2txt(qr: QrCode, zoom: int = 1, pad: int = 4) -> str:\n    tab = qr.modules\n    sz = qr.size\n    if sz % 2 and zoom == 1:\n        tab.append([False] * sz)\n\n    tab = [[False] * sz] * pad + tab + [[False] * sz] * pad\n    tab = [[False] * pad + x + [False] * pad for x in tab]\n\n    rows: list[str] = []\n    if zoom == 1:\n        for y in range(0, len(tab), 2):\n            row = \"\"\n            for x in range(len(tab[y])):\n                v = 2 if tab[y][x] else 0\n                v += 1 if tab[y + 1][x] else 0\n                row += \" ▄▀█\"[v]\n            rows.append(row)\n    else:\n        for tr in tab:\n            row = \"\"\n            for zb in tr:\n                row += \" █\"[int(zb)] * 2\n            rows.append(row)\n\n    return \"\\n\".join(rows)\n\n\ndef qr2png(\n    qr: QrCode,\n    zoom: int,\n    pad: int,\n    bg: Optional[tuple[int, int, int]],\n    fg: Optional[tuple[int, int, int]],\n    ap: str,\n) -> None:\n    from PIL import Image\n\n    tab = qr.modules\n    sz = qr.size\n    psz = sz + pad * 2\n    if bg:\n        img = Image.new(\"RGB\", (psz, psz), bg)\n    else:\n        img = Image.new(\"RGBA\", (psz, psz), (0, 0, 0, 0))\n        fg = (fg[0], fg[1], fg[2], 255)\n    for y in range(sz):\n        for x in range(sz):\n            if tab[y][x]:\n                img.putpixel((x + pad, y + pad), fg)\n    if zoom != 1:\n        img = img.resize((sz * zoom, sz * zoom), Image.Resampling.NEAREST)\n    img.save(ap)\n\n\ndef qr2svg(qr: QrCode, border: int) -> str:\n    parts: list[str] = []\n    for y in range(qr.size):\n        sy = border + y\n        for x in range(qr.size):\n            if qr.modules[y][x]:\n                parts.append(\"M%d,%dh1v1h-1z\" % (border + x, sy))\n    t = \"\"\"\\\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {0} {0}\" stroke=\"none\">\n<rect width=\"100%\" height=\"100%\" fill=\"#F7F7F7\"/>\n<path d=\"{1}\" fill=\"#111111\"/>\n</svg>\n\"\"\"\n    return t.format(qr.size + border * 2, \" \".join(parts))\n"
  },
  {
    "path": "copyparty/res/__init__.py",
    "content": ""
  },
  {
    "path": "copyparty/res/insecure.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDD84b31Brvmfcc\nGTwXuf5n5NW6KxIhLWVZgsCJq+lwGTGl1Cqm9vnzZqD7MDLNzztWk51FDXxeJy9Z\nVos1F+n3qfDi4zdd8zWIJxLTsIEkqZjSWiQqqmFijXoaEPBsQ32qRZprh/FfNw3P\nTagroIgpgzd196eoWdDdggenYBG8/PbktaBKgOo6jz6Q5/UzNTmvcbtlxlsxPos8\naTqzezCgA/cArg8mxmfvRPrw8umC8ATiVLvGrXeMiSvcr8Els5air/idhYC8u5zO\nzGZCePxoKUptQvh/06jsye1S3JOf+RsfAWvu7DgWvSJU2dHfVmtb4ZMSa6cjqTPP\ncW7S1WyLAgMBAAECggEAAwabqvAHinOiMTjiiKtClnAeLMXFfeWpjvxJ5NZWwHhj\nH+Bq2DEwIuYOzlIsNqlgjTGyWAKhTQLl5EdF1wgLgNuK8LX5gOXkibmwvLwZAmvs\nBDOII3CGGHN+0zA3xjQ0mJCCle5/d6zt9amJU0MjVyDDlnrAiAT7CLCdVaRSIczv\nSETM90NBVHNMraB+Tz9kR1FhyDDlzqpRn/19Hm4BG6/iSmPYpL3jgCRWNweXSXdd\nx9EitJCz6Yo9+0qPD8DayrH5sKviZw0QDXUezWCcQvgXMsv5MJa7+UaawLk6bq5B\nOu1TFGuFdNeNmWofAwPE8UOSIXoKhBK84z5lpTs44QKBgQDg9bRrn3a2u8vpx3r7\nB+v4dlVq5I3lbVwLwEo6vnw2UiIOtQaQMiWX8HoHR3rmzCVMZq8xNNhEYSboFgtB\nbbQaxoApdqVTF700c5Le82tj3rBXN0vBIUm6Nzpz0IKQw8HWGz1q/sHd8uZdmtAO\nm4JrZIJqMItcLhyjYCp8hRUkvwKBgQDe/SZVCBMocHASyB4RgeAkNKSjFEYe2yAl\n23W9Me3koJqTvHEZnv3Xk5O3HF0dxElktJW5slyrA2BcrzuusO+pge/29OV4ypFa\nkI51ebMgO3hm2BKKMwxR3mT4KVAbKveLi7hdlnbHVCcZelygkw+11tzIfaikeswR\nc4FoyPpvNQKBgDEA1OBsyCteFTlDnuJ4A0sIW+sBBnfnrplQtdq+C8i5c3nIrTlT\n8yR52dskEv2bkrRl2dvaKxIaJ6N+yczi3MzIWLqvgavsC+cVFfVDCS2kIL2e6f2U\nBr9tsGnyDb8DJYJCRMq92/VBKDVTt+a2sV47cr02/eSClvJvzFF7m/N5AoGAJYzD\nk7YUY87rUH5aceBI+k/TGZMka7XCqB1Yqk9qHAHfhdlJwmK/pDm5ujAQjh6rrUWr\noOWkLTgYVgM8LaKl+Qlke1Wp/rk92N5W3vlrbJYXJFpmZNdLz81/ezqZvrlxjhIt\nLbVUsyQ8oVG1n2SkVJ6l9y0R5QC4tIea1yZg5bECgYEAhF2EOtBCJ3EDEpHvnlK7\nOuLRVjce4Vy+Mj3nFbHQMJf+A1B7+B43Ce1fn94OfGJldLDjvaZb8O91JAkHAwJQ\nGCpOVj9go0ffg/2hurtXW3bMLXIXqcn1538MB5NdlcL22+T0CCprAeuRQm4saMhz\nmkt5VszEuF/c88usk0ZMm4Y=\n-----END PRIVATE KEY-----\n-----BEGIN CERTIFICATE-----\nMIIDNTCCAh2gAwIBAgIUWWDVQNmrPPo2HlyhYuSYY2uKPbEwDQYJKoZIhvcNAQEL\nBQAwKjELMAkGA1UEBhMCTk8xGzAZBgNVBAMMEmNvcHlwYXJ0eS1pbnNlY3VyZTAe\nFw0wMDAxMDEwMDAwMDBaFw0zODAxMTkwMDAwMDBaMCoxCzAJBgNVBAYTAk5PMRsw\nGQYDVQQDDBJjb3B5cGFydHktaW5zZWN1cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IB\nDwAwggEKAoIBAQDD84b31BrvmfccGTwXuf5n5NW6KxIhLWVZgsCJq+lwGTGl1Cqm\n9vnzZqD7MDLNzztWk51FDXxeJy9ZVos1F+n3qfDi4zdd8zWIJxLTsIEkqZjSWiQq\nqmFijXoaEPBsQ32qRZprh/FfNw3PTagroIgpgzd196eoWdDdggenYBG8/PbktaBK\ngOo6jz6Q5/UzNTmvcbtlxlsxPos8aTqzezCgA/cArg8mxmfvRPrw8umC8ATiVLvG\nrXeMiSvcr8Els5air/idhYC8u5zOzGZCePxoKUptQvh/06jsye1S3JOf+RsfAWvu\n7DgWvSJU2dHfVmtb4ZMSa6cjqTPPcW7S1WyLAgMBAAGjUzBRMB0GA1UdDgQWBBQC\nuhBjOit55cEz7jMIiJq2BljqxDAfBgNVHSMEGDAWgBQCuhBjOit55cEz7jMIiJq2\nBljqxDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCu7VE/Pt4H\nIOHAacQFJN9rdUtI1lcvbJEMdpPglHgFa546pgzDjS8QGvULmppb4mkMbezZ2u+v\nZsWMfLxATYgfqEPO/fnT/UqFX2TI2pmmZhqSV+eW8AGRPD5LYyfXpvU4ciSz4Fyl\nFlrcCOStbeOjHwpeF+n92sF6/LL3n4yy1jmLnTaZDdd59PcWpQkX8omBWlXPdCPR\not0ZI2OpK5mDUG2FglCXl29k1q/zLsKPyfmG7T6VOMB8HO7EylzgSTke4WH9QLR/\nbkn7AZU+JEIdC6SgR0WSQAntqM+b/OuYSBzXDX0XLwnyY2Dx8SshKNF9v6GwTq7j\nyQev95xStUTw\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "copyparty/sftpd.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport errno\nimport hashlib\nimport logging\nimport os\nimport select\nimport socket\nimport time\nfrom threading import ExceptHookArgs\n\nimport paramiko\nimport paramiko.common\nimport paramiko.sftp_attr\nfrom paramiko.common import AUTH_FAILED, AUTH_SUCCESSFUL\nfrom paramiko.sftp import (\n    SFTP_FAILURE,\n    SFTP_NO_SUCH_FILE,\n    SFTP_OK,\n    SFTP_OP_UNSUPPORTED,\n    SFTP_PERMISSION_DENIED,\n)\n\nfrom .__init__ import ANYWIN, TYPE_CHECKING\nfrom .authsrv import LEELOO_DALLAS, VFS, AuthSrv\nfrom .bos import bos\nfrom .util import (\n    VF_CAREFUL,\n    Daemon,\n    ODict,\n    Pebkac,\n    ipnorm,\n    min_ex,\n    read_utf8,\n    relchk,\n    runhook,\n    sanitize_fn,\n    ub64enc,\n    undot,\n    vjoin,\n    wunlink,\n)\n\nif TYPE_CHECKING:\n    from .svchub import SvcHub\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Any, BinaryIO, Optional, Union\n\nSATTR = paramiko.sftp_attr.SFTPAttributes\n\n\nclass SSH_Srv(paramiko.ServerInterface):\n    def __init__(self, hub: \"SvcHub\", addr: Any):\n        self.hub = hub\n        self.args = args = hub.args\n        self.log_func = hub.log\n        self.uname = \"*\"\n\n        self.addr = addr\n        self.ip = addr[0]\n        if self.ip.startswith(\"::ffff:\"):\n            self.ip = self.ip[7:]\n\n        zsl = []\n        if args.sftp_anon:\n            zsl.append(\"none\")\n        if args.sftp_key2u:\n            zsl.append(\"publickey\")\n        if args.sftp_pw or args.sftp_anon:\n            zsl.append(\"password\")\n        self._auths = \",\".join(zsl)\n\n    def log(self, msg: str, c: Union[int, str] = 0) -> None:\n        self.hub.log(\"sftp:%s\" % (self.ip,), msg, c)\n\n    def get_allowed_auths(self, username: str) -> str:\n        return self._auths\n\n    def get_banner(self) -> tuple[Optional[str], Optional[str]]:\n        if self.args.sftpv:\n            self.log(\"get_banner\")\n        t = self.args.sftp_banner\n        if not t:\n            return (None, None)\n        if t.startswith(\"@\"):\n            t = read_utf8(self.log, t[1:], False)\n        if t and not t.endswith(\"\\n\"):\n            t += \"\\n\"\n        return (t, \"en-US\")\n\n    def check_channel_request(self, kind: str, chanid: int) -> int:\n        if self.args.sftpv:\n            self.log(\"channel-request: %r, %r\" % (kind, chanid))\n        if kind == \"session\":\n            return paramiko.common.OPEN_SUCCEEDED\n        return paramiko.common.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED\n\n    def check_auth_none(self, username: str) -> int:\n        try:\n            return self._check_auth_none(username)\n        except:\n            self.log(\"unhandled exception: %s\" % (min_ex(),), 1)\n            return AUTH_FAILED\n\n    def _check_auth_none(self, uname: str) -> int:\n        args = self.args\n        if uname != args.sftp_anon or not uname:\n            return AUTH_FAILED\n\n        ipn = ipnorm(self.ip)\n        bans = self.hub.bans\n        if ipn in bans:\n            rt = bans[ipn] - time.time()\n            if rt < 0:\n                self.log(\"client unbanned\")\n                del bans[ipn]\n            else:\n                self.log(\"client is banned\")\n                return AUTH_FAILED\n\n        self.uname = \"*\"\n        self.log(\"auth-none OK: *\")\n        return AUTH_SUCCESSFUL\n\n    def check_auth_password(self, username: str, password: str) -> int:\n        try:\n            return self._check_auth_password(username, password)\n        except:\n            self.log(\"unhandled exception: %s\" % (min_ex(),), 1)\n            return AUTH_FAILED\n\n    def _check_auth_password(self, uname: str, pw: str) -> int:\n        args = self.args\n        if args.sftpv:\n            logpw = pw\n            if args.log_badpwd == 0:\n                logpw = \"\"\n            elif args.log_badpwd == 2:\n                zb = hashlib.sha512(pw.encode(\"utf-8\", \"replace\")).digest()\n                logpw = \"%\" + ub64enc(zb[:12]).decode(\"ascii\")\n            self.log(\"auth-pw: %r, %r\" % (uname, logpw))\n\n        ipn = ipnorm(self.ip)\n        bans = self.hub.bans\n        if ipn in bans:\n            rt = bans[ipn] - time.time()\n            if rt < 0:\n                self.log(\"client unbanned\")\n                del bans[ipn]\n            else:\n                self.log(\"client is banned\")\n                return AUTH_FAILED\n\n        anon = args.sftp_anon\n        if anon and uname == anon:\n            self.uname = \"*\"\n            self.log(\"auth-pw OK: *\")\n            return AUTH_SUCCESSFUL\n\n        if not args.sftp_pw:\n            return AUTH_FAILED\n\n        if args.usernames:\n            alts = [\"%s:%s\" % (uname, pw)]\n        else:\n            alts = [pw, uname]\n\n        attempt = \"%s:%s\" % (uname, pw)\n        uname = \"\"\n        asrv = self.hub.asrv\n        for zs in alts:\n            zs = asrv.iacct.get(asrv.ah.hash(zs), \"\")\n            if zs:\n                uname = zs\n                break\n\n        if args.ipu and uname == \"*\":\n            uname = args.ipu_iu[args.ipu_nm.map(self.ip)]\n        if args.ipr and uname in args.ipr_u:\n            if not args.ipr_u[uname].map(self.ip):\n                logging.warning(\"username [%s] rejected by --ipr\", uname)\n                return AUTH_FAILED\n\n        if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):\n            g = self.hub.gpwd\n            if g.lim:\n                bonk, ip = g.bonk(self.ip, attempt)\n                if bonk:\n                    logging.warning(\"client banned: invalid passwords\")\n                    bans[self.ip] = bonk\n                    try:\n                        # only possible if multiprocessing disabled\n                        self.hub.broker.httpsrv.bans[ip] = bonk  # type: ignore\n                        self.hub.broker.httpsrv.nban += 1  # type: ignore\n                    except:\n                        pass\n            return AUTH_FAILED\n\n        self.uname = uname\n        self.log(\"auth-pw OK: %s\" % (uname,))\n        return AUTH_SUCCESSFUL\n\n    def check_auth_publickey(self, username: str, key: paramiko.PKey) -> int:\n        try:\n            return self._check_auth_publickey(username, key)\n        except:\n            self.log(\"unhandled exception: %s\" % (min_ex(),), 1)\n            return AUTH_FAILED\n\n    def _check_auth_publickey(self, uname: str, key: paramiko.PKey) -> int:\n        args = self.args\n        if args.sftpv:\n            zs = key.get_name() + \",\" + key.get_base64()[:32]\n            self.log(\"auth-key: %r, %r\" % (uname, zs))\n\n        ipn = ipnorm(self.ip)\n        bans = self.hub.bans\n        if ipn in bans:\n            rt = bans[ipn] - time.time()\n            if rt < 0:\n                self.log(\"client unbanned\")\n                del bans[ipn]\n            else:\n                self.log(\"client is banned\")\n                return AUTH_FAILED\n\n        anon = args.sftp_anon\n        if anon and uname == anon:\n            self.uname = \"*\"\n            self.log(\"auth-key OK: *\")\n            return AUTH_SUCCESSFUL\n\n        attempt = \"%s %s\" % (key.get_name(), key.get_base64())\n        ok = args.sftp_key2u.get(attempt) == uname\n\n        if ok and args.ipr and uname in args.ipr_u:\n            if not args.ipr_u[uname].map(self.ip):\n                logging.warning(\"username [%s] rejected by --ipr\", uname)\n                return AUTH_FAILED\n\n        asrv = self.hub.asrv\n        if not ok or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):\n            self.log(\"auth-key REJECTED: %s\" % (uname,))\n            return AUTH_FAILED\n\n        self.uname = uname\n        self.log(\"auth-key OK: %s\" % (uname,))\n        return AUTH_SUCCESSFUL\n\n\nclass SFTP_FH(paramiko.SFTPHandle):\n    def __init__(self, flags: int = 0) -> None:\n        self.filename = \"\"\n        self.readfile: Optional[BinaryIO] = None\n        self.writefile: Optional[BinaryIO] = None\n        super(SFTP_FH, self).__init__(flags)\n\n    def stat(self):\n        try:\n            f = self.readfile or self.writefile\n            return SATTR.from_stat(os.fstat(f.fileno()))\n        except OSError as ex:\n            return paramiko.SFTPServer.convert_errno(ex.errno)\n\n    def chattr(self, attr):\n        # python doesn't have equivalents to fchown or fchmod, so we have to\n        # use the stored filename\n        if not self.writefile:\n            return SFTP_PERMISSION_DENIED\n        try:\n            paramiko.SFTPServer.set_file_attr(self.filename, attr)\n            return SFTP_OK\n        except OSError as ex:\n            return paramiko.SFTPServer.convert_errno(ex.errno)\n\n\nclass SFTP_Srv(paramiko.SFTPServerInterface):\n    def __init__(self, ssh: paramiko.ServerInterface, *a, **ka):\n        super(SFTP_Srv, self).__init__(ssh, *a, **ka)\n        self.ssh = ssh\n        self.ip: str = ssh.ip  # type: ignore\n        self.hub: \"SvcHub\" = ssh.hub  # type: ignore\n        self.uname: str = ssh.uname  # type: ignore\n        self.args = self.hub.args\n        self.asrv: \"AuthSrv\" = self.hub.asrv\n        self.v = self.args.sftpv\n        self.vv = self.args.sftpvv\n\n        if self.uname == LEELOO_DALLAS:\n            raise Exception(\"send her back\")\n\n        self.vols = [\n            vp\n            for vp, vn in self.asrv.vfs.all_vols.items()\n            if self.uname in vn.axs.uread\n            or self.uname in vn.axs.uwrite\n            or self.uname in vn.axs.uget\n        ]\n        self.vis = set()\n        for zs in self.vols:\n            self.vis.add(zs)\n            while zs:\n                zs = zs.rsplit(\"/\", 1)[0] if \"/\" in zs else \"\"\n                self.vis.add(zs)\n\n    def log(self, msg: str, c: Union[int, str] = 0) -> None:\n        self.hub.log(\"sftp:%s\" % (self.ip,), msg, c)\n\n    def v2a(\n        self,\n        vpath: str,\n        r: bool = False,\n        w: bool = False,\n        m: bool = False,\n        d: bool = False,\n    ) -> tuple[str, VFS, str]:\n        vpath = vpath.replace(os.sep, \"/\").strip(\"/\")\n        rd, fn = os.path.split(vpath)\n        if relchk(rd):\n            self.log(\"malicious vpath: %s\", vpath)\n            raise Exception(\"Unsupported characters in [%s]\" % (vpath,))\n\n        fn = sanitize_fn(fn or \"\")\n        vpath = vjoin(rd, fn)\n        vn, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)\n        if (\n            w\n            and fn.lower() in vn.flags[\"emb_all\"]\n            and self.uname not in vn.axs.uread\n            and \"wo_up_readme\" not in vn.flags\n        ):\n            fn = \"_wo_\" + fn\n            vpath = vjoin(rd, fn)\n            vn, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)\n\n        if not vn.realpath:\n            # return \"\", vn, rem\n            raise OSError(errno.ENOENT, \"no filesystem mounted at [/%s]\" % (vpath,))\n\n        if \"xdev\" in vn.flags or \"xvol\" in vn.flags:\n            ap = vn.canonical(rem)\n            avn = vn.chk_ap(ap)\n            t = \"Permission denied in [{}]\"\n            if not avn:\n                raise OSError(errno.EPERM, \"permission denied in [/%s]\" % (vpath,))\n\n            cr, cw, cm, cd, _, _, _, _, _ = avn.uaxs[self.uname]\n            if r and not cr or w and not cw or m and not cm or d and not cd:\n                raise OSError(errno.EPERM, \"permission denied in [/%s]\" % (vpath,))\n        else:\n            ap = vn.canonical(rem, False)\n\n        if \"bcasechk\" in vn.flags and not vn.casechk(rem, True):\n            raise OSError(errno.ENOENT, \"file does not exist case-sensitively\")\n\n        return ap, vn, rem\n\n    def list_folder(self, path: str) -> list[SATTR] | int:\n        try:\n            return self._list_folder(path)\n        except Pebkac as ex:\n            if ex.code == 404:\n                self.log(\"folder 404: %s\" % (path,))\n                return SFTP_NO_SUCH_FILE\n            return SFTP_PERMISSION_DENIED\n        except:\n            self.log(\"unhandled exception: %s\" % (min_ex(),), 1)\n            return SFTP_FAILURE\n\n    def _list_folder(self, path: str) -> list[SATTR] | int:\n        if self.v:\n            self.log(\"ls(%s):\" % (path,))\n        path = path.strip(\"/\")\n        try:\n            ap, vn, rem = self.v2a(path, r=True)\n        except Pebkac:\n            try:\n                self.v2a(path, w=True)\n                self.log(\"ls(%s): [] (write-only)\" % (path,))\n                return []  # display write-only folders as empty\n            except:\n                pass\n            if path not in self.vis:\n                self.log(\"ls(%s): EPERM\" % (path,))\n                return SFTP_PERMISSION_DENIED\n            # list of accessible volumes\n            ret = []\n            zi = int(time.time())\n            vst = os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, zi, zi, zi))\n            prefix = path + \"/\"\n            for vn in self.asrv.vfs.all_nodes.values():\n                if path and not vn.vpath.startswith(prefix):\n                    continue  # vn is parent\n                vname = vn.vpath[len(prefix) :]\n                if \"/\" in vname or not vname:\n                    continue  # only include vols at current level\n                ret.append(SATTR.from_stat(vst, filename=vn.vpath))\n            ret.sort(key=lambda x: x.filename)\n            self.log(\"ls(%s): vfs-vols; |%d|\" % (path, len(ret)))\n            return ret\n\n        _, vfs_ls, vfs_virt = vn.ls(\n            rem,\n            self.uname,\n            not self.args.no_scandir,\n            [[True, False], [False, True]],\n            throw=True,\n        )\n        ret = [SATTR.from_stat(x[1], filename=x[0]) for x in vfs_ls]\n        for zs, vn2 in vfs_virt.items():\n            if not vn2.realpath:\n                continue\n            st = bos.stat(vn2.realpath)\n            ret.append(SATTR.from_stat(st, filename=zs))\n        if self.uname not in vn.axs.udot:\n            ret = [x for x in ret if not x.filename.split(\"/\")[-1].startswith(\".\")]\n        ret.sort(key=lambda x: x.filename)\n        self.log(\"ls(%s): |%d|\" % (path, len(ret)))\n        return ret\n\n    def stat(self, path: str) -> SATTR | int:\n        try:\n            return self._stat(path)\n        except:\n            self.log(\"unhandled exception: %s\" % (min_ex(),), 1)\n            return SFTP_FAILURE\n\n    def lstat(self, path: str) -> SATTR | int:\n        try:\n            return self._stat(path)\n        except:\n            self.log(\"unhandled exception: %s\" % (min_ex(),), 1)\n            return SFTP_FAILURE\n\n    def _stat(self, vp: str) -> SATTR | int:\n        vp = vp.strip(\"/\")\n        try:\n            ap, vn, _ = self.v2a(vp)\n            if (\n                self.uname not in vn.axs.uread\n                and self.uname not in vn.axs.uwrite\n                and self.uname not in vn.axs.uget\n            ):\n                self.log(\"stat(%s): EPERM\" % (vp,))\n                return SFTP_PERMISSION_DENIED\n            st = bos.stat(ap)\n            self.log(\"stat(%s): %s\" % (vp, st))\n        except:\n            if vp not in self.vis:\n                self.log(\"stat(%s): ENOENT\" % (vp,))\n                return SFTP_NO_SUCH_FILE\n            zi = int(time.time())\n            st = os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, zi, zi, zi))\n            self.log(\"stat(%s): vfs-vols\")\n        return SATTR.from_stat(st)\n\n    def open(self, path: str, flags: int, attr: SATTR) -> paramiko.SFTPHandle | int:\n        try:\n            return self._open(path, flags, attr)\n        except:\n            self.log(\"unhandled exception: %s\" % (min_ex(),), 1)\n            return SFTP_FAILURE\n\n    def _open(self, vp: str, iflag: int, attr: SATTR) -> paramiko.SFTPHandle | int:\n        if ANYWIN:\n            iflag |= os.O_BINARY\n        if iflag & os.O_WRONLY:\n            rd = False\n            wr = True\n            if iflag & os.O_APPEND:\n                smode = \"ab\"\n            else:\n                smode = \"wb\"\n        elif iflag & os.O_RDWR:\n            rd = wr = True\n            if iflag & os.O_APPEND:\n                smode = \"a+b\"\n            else:\n                smode = \"r+b\"\n        else:\n            rd = True\n            wr = False\n            smode = \"rb\"\n\n        try:\n            vn, rem = self.asrv.vfs.get(vp, self.uname, rd, wr)\n            ap = vn.canonical(rem, False)\n            vf = vn.flags\n        except Pebkac as ex:\n            t = \"denied open file [%s], iflag=%s, read=%s, write=%s: %s\"\n            self.log(t % (vp, iflag, rd, wr, ex))\n            return SFTP_PERMISSION_DENIED\n\n        self.log(\"open(%s, %x, %s)\" % (vp, iflag, smode))\n\n        if wr:\n            try:\n                st = bos.stat(ap)\n                td = time.time() - st.st_mtime\n                need_unlink = True\n            except:\n                need_unlink = False\n                td = 0\n\n            xbu = vn.flags.get(\"xbu\")\n            if xbu:\n                hr = runhook(\n                    self.log,\n                    None,\n                    self.hub.up2k,\n                    \"xbu.sftp\",\n                    xbu,\n                    ap,\n                    vp,\n                    \"\",\n                    \"\",\n                    \"\",\n                    0,\n                    0,\n                    \"7.3.8.7\",\n                    time.time(),\n                    None,\n                )\n                t = hr.get(\"rejectmsg\") or \"\"\n                if t or hr.get(\"rc\") != 0:\n                    if not t:\n                        t = \"upload blocked by xbu server config: %r\" % (vp,)\n                    self.log(t, 3)\n                    return SFTP_PERMISSION_DENIED\n\n            self.log(\"writing to [%s] => [%s]\" % (vp, ap))\n\n        if wr and need_unlink:  # type: ignore  # !rm\n            assert td  # type: ignore  # !rm\n            if td >= -1 and td <= self.args.ftp_wt:\n                # within permitted timeframe; allow overwrite or resume\n                do_it = True\n            elif self.args.no_del or self.args.ftp_no_ow:\n                # file too old, or overwrite not allowed; reject\n                do_it = False\n            else:\n                # allow overwrite if user has delete permission\n                do_it = self.uname in vn.axs.udel\n\n            if not do_it:\n                t = \"file already exists and no permission to overwrite: %s\"\n                self.log(t % (vp,))\n                return SFTP_PERMISSION_DENIED\n\n            # Don't unlink file for append mode\n            elif \"a\" not in smode:\n                wunlink(self.log, ap, VF_CAREFUL)\n\n        chmod = getattr(attr, \"st_mode\", None)\n        if chmod is None:\n            chmod = vf.get(\"chmod_f\", 0o644)\n            self.log(\"open(%s, %x): client did not chmod\" % (vp, iflag))\n        else:\n            self.log(\"open(%s, %x): client set chmod 0%o\" % (vp, iflag, chmod))\n\n        try:\n            fd = os.open(ap, iflag, chmod)\n        except OSError as ex:\n            t = \"failed to os.open [%s] -> [%s] with iflag [%s] and chmod [%s]: %r\"\n            self.log(t % (vp, ap, iflag, chmod, ex), 3)\n            return paramiko.SFTPServer.convert_errno(ex.errno)\n\n        if iflag & os.O_CREAT:\n            paramiko.SFTPServer.set_file_attr(ap, attr)\n\n        try:\n            f = os.fdopen(fd, smode)\n        except OSError as ex:\n            t = \"failed to os.fdpen [%s] -> [%s] with smode [%s]: %r\"\n            self.log(t % (vp, ap, smode, ex), 3)\n            return paramiko.SFTPServer.convert_errno(ex.errno)\n\n        ret = SFTP_FH(iflag)\n        ret.filename = ap\n        ret.readfile = f if rd else None\n        ret.writefile = f if wr else None\n        return ret\n\n    def remove(self, path: str) -> int:\n        try:\n            return self._remove(path)\n        except:\n            self.log(\"unhandled exception: %s\" % (min_ex(),), 1)\n            return SFTP_FAILURE\n\n    def _remove(self, vp: str) -> int:\n        self.log(\"rm(%s)\" % (vp,))\n        if self.args.no_del:\n            self.log(\"The delete feature is disabled in server config\")\n            return SFTP_PERMISSION_DENIED\n        try:\n            self.hub.up2k.handle_rm(self.uname, self.ip, [vp], [], False, False)\n            self.log(\"rm(%s): ok\" % (vp,))\n            return SFTP_OK\n        except Pebkac as ex:\n            t = \"denied delete [%s]: %s\"\n            self.log(t % (vp, ex))\n            if str(ex).startswith(\"file not found\"):\n                return SFTP_NO_SUCH_FILE\n            try:\n                # write-only client trying to rm before upload?\n                ap, vn, _ = self.v2a(vp)\n                if (\n                    self.uname not in vn.axs.uread\n                    and self.uname not in vn.axs.uwrite\n                    and self.uname not in vn.axs.uget\n                ):\n                    self.log(\"rm(%s): EPERM\" % (vp,))\n                    return SFTP_PERMISSION_DENIED\n                if not bos.path.exists(ap):\n                    self.log(\" `- file didn't exist; returning ENOENT\")\n                    return SFTP_NO_SUCH_FILE\n            except:\n                pass\n            return SFTP_PERMISSION_DENIED\n        except OSError as ex:\n            self.log(\"failed: rm(%s): %r\" % (vp, ex))\n            return paramiko.SFTPServer.convert_errno(ex.errno)\n\n    def rename(self, oldpath: str, newpath: str) -> int:\n        try:\n            return self._rename(oldpath, newpath)\n        except:\n            self.log(\"unhandled exception: %s\" % (min_ex(),), 1)\n            return SFTP_FAILURE\n\n    def _rename(self, svp: str, dvp: str) -> int:\n        self.log(\"mv(%s, %s)\" % (svp, dvp))\n        if self.args.no_mv:\n            self.log(\"The rename/move feature is disabled in server config\")\n        svp = svp.strip(\"/\")\n        dvp = dvp.strip(\"/\")\n        try:\n            self.hub.up2k.handle_mv(\"\", self.uname, self.ip, svp, dvp)\n            return SFTP_OK\n        except Pebkac as ex:\n            t = \"denied rename [%s] to [%s]: %s\"\n            self.log(t % (svp, dvp, ex))\n            return SFTP_PERMISSION_DENIED\n        except OSError as ex:\n            self.log(\"mv(%s, %s): %r\" % (svp, dvp, ex))\n            return paramiko.SFTPServer.convert_errno(ex.errno)\n\n    def mkdir(self, path: str, attr: SATTR) -> int:\n        try:\n            return self._mkdir(path, attr)\n        except:\n            self.log(\"unhandled exception: %s\" % (min_ex(),), 1)\n            return SFTP_FAILURE\n\n    def _mkdir(self, vp: str, attr: SATTR) -> int:\n        self.log(\"mkdir(%s)\" % (vp,))\n        try:\n            vn, rem = self.asrv.vfs.get(vp, self.uname, False, True)\n            ap = os.path.join(vn.realpath, rem)\n            bos.makedirs(ap, vf=vn.flags)  # filezilla expects this\n            if attr is not None:\n                paramiko.SFTPServer.set_file_attr(ap, attr)\n            return SFTP_OK\n        except Pebkac as ex:\n            t = \"denied mkdir [%s]: %s\"\n            self.log(t % (vp, ex))\n            return SFTP_PERMISSION_DENIED\n        except OSError as ex:\n            self.log(\"mkdir(%s): %r\" % (vp, ex))\n            return paramiko.SFTPServer.convert_errno(ex.errno)\n\n    def rmdir(self, path: str) -> int:\n        try:\n            return self._rmdir(path)\n        except:\n            self.log(\"unhandled exception: %s\" % (min_ex(),), 1)\n            return SFTP_FAILURE\n\n    def _rmdir(self, vp: str) -> int:\n        self.log(\"rmdir(%s)\" % (vp,))\n        try:\n            vn, rem = self.asrv.vfs.get(vp, self.uname, False, False, will_del=True)\n            ap = os.path.join(vn.realpath, rem)\n            bos.rmdir(ap)\n            return SFTP_OK\n        except Pebkac as ex:\n            t = \"denied rmdir [%s]: %s\"\n            self.log(t % (vp, ex))\n            return SFTP_PERMISSION_DENIED\n        except OSError as ex:\n            self.log(\"rmdir(%s): %r\" % (vp, ex))\n            return paramiko.SFTPServer.convert_errno(ex.errno)\n\n    def chattr(self, path: str, attr: SATTR) -> int:\n        try:\n            return self._chattr(path, attr)\n        except:\n            self.log(\"unhandled exception: %s\" % (min_ex(),), 1)\n            return SFTP_FAILURE\n\n    def _chattr(self, vp: str, attr: SATTR) -> int:\n        self.log(\"chattr(%s, %s)\" % (vp, attr))\n        try:\n            vn, rem = self.asrv.vfs.get(vp, self.uname, False, True, will_del=True)\n            ap = os.path.join(vn.realpath, rem)\n            paramiko.SFTPServer.set_file_attr(ap, attr)\n            return SFTP_OK\n        except Pebkac as ex:\n            t = \"denied chattr [%s]: %s\"\n            self.log(t % (vp, ex))\n            return SFTP_PERMISSION_DENIED\n        except OSError as ex:\n            self.log(\"chattr(%s): %r\" % (vp, ex))\n            return paramiko.SFTPServer.convert_errno(ex.errno)\n\n    def symlink(self, target_path: str, path: str) -> int:\n        return SFTP_OP_UNSUPPORTED\n\n    def readlink(self, path: str) -> str | int:\n        return path\n\n    def canonicalize(self, path: str) -> str:\n        return \"/%s\" % (undot(path),)\n\n\nclass Sftpd(object):\n    def __init__(self, hub: \"SvcHub\") -> None:\n        self.hub = hub\n        self.args = args = hub.args\n        self.log_func = hub.log\n        self.srv: list[socket.socket] = []\n        self.bound: list[str] = []\n        self.sessions = {}\n\n        ips = args.sftp_i\n        if \"::\" in ips:\n            ips.append(\"0.0.0.0\")\n\n        ips = [x for x in ips if not x.startswith((\"unix:\", \"fd:\"))]\n\n        if args.sftp4:\n            ips = [x for x in ips if \":\" not in x]\n\n        if not ips:\n            self.log(\"cannot start sftp-server; no compatible IPs in -i\", 1)\n            return\n\n        self.hostkeys = []\n        hostkeytypes = (\n            (\"ed25519\", \"Ed25519Key\", {}),  # best\n            (\"ecdsa\", \"ECDSAKey\", {\"bits\": 384}),\n            (\"rsa\", \"RSAKey\", {\"bits\": 4096}),\n            (\"dsa\", \"DSSKey\", {}),  # worst\n        )\n        for fname, aname, opts in hostkeytypes:\n            fpath = \"%s/ssh_host_%s_key\" % (args.sftp_hostk, fname.lower())\n            try:\n                pkey = getattr(paramiko, aname).from_private_key_file(fpath)\n            except Exception as ex:\n                try:\n                    genfun = getattr(paramiko, aname).generate\n                except Exception as ex2:\n                    if args.sftpv or fname not in (\"dsa\", \"ed25519\"):\n                        # dsa dropped in 4.0\n                        # ed25519 not supported yet\n                        self.log(\"cannot generate %s hostkey: %r\" % (aname, ex2), 3)\n                    continue\n                self.log(\"generating hostkey [%s] due to %r\" % (fpath, ex))\n                pkey = genfun(**opts)\n                pkey.write_private_key_file(fpath)\n                pkey = getattr(paramiko, aname).from_private_key_file(fpath)\n            self.hostkeys.append(pkey)\n            if args.sftpv:\n                self.log(\"loaded hostkey %r\" % (pkey,))\n\n        ips = list(ODict.fromkeys(ips))  # dedup\n\n        for ip in ips:\n            self._bind(ip)\n\n        self.log(\"listening @ %s port %s\" % (self.bound, args.sftp))\n\n    def log(self, msg: str, c: Union[int, str] = 0) -> None:\n        self.hub.log(\"sftp\", msg, c)\n\n    def _bind(self, ip: str) -> None:\n        port = self.args.sftp\n        try:\n            ipv = socket.AF_INET6 if \":\" in ip else socket.AF_INET\n            srv = socket.socket(ipv, socket.SOCK_STREAM)\n            if not ANYWIN or self.args.reuseaddr:\n                srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n            srv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)\n            srv.settimeout(0)  # == srv.setblocking(False)\n            try:\n                srv.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False)\n            except:\n                pass  # will create another ipv4 socket instead\n            if getattr(self.args, \"freebind\", False):\n                srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1)\n            srv.bind((ip, port))\n            srv.listen(10)\n            self.srv.append(srv)\n            self.bound.append(ip)\n        except Exception as ex:\n            if ip == \"0.0.0.0\" and \"::\" in self.bound:\n                try:\n                    srv.close()  # type: ignore\n                except:\n                    pass\n                return  # dualstack\n            self.log(\"could not listen on (%s,%s): %r\" % (ip, port, ex), 3)\n\n    def _accept(self, srv: socket.socket) -> None:\n        cli, addr = srv.accept()\n        # cli.settimeout(0)  # == srv.setblocking(False)\n        self.log(\"%r is connecting\" % (addr,))\n        zs = \"sftp-%s\" % (addr[0],)\n        # Daemon(self._accept2, zs, (cli, addr))\n        self._accept2(cli, addr)\n\n    def _accept2(self, cli, addr) -> None:\n        tra = paramiko.Transport(cli)\n        for hkey in self.hostkeys:\n            tra.add_server_key(hkey)\n        tra.set_subsystem_handler(\"sftp\", paramiko.SFTPServer, SFTP_Srv)\n        psrv = SSH_Srv(self.hub, addr)\n        try:\n            tra.start_server(server=psrv)\n        except Exception as ex:\n            self.log(\"%r could not establish connection: %r\" % (addr, ex), 3)\n            cli.close()\n            return\n\n        chan = tra.accept()\n        if chan is None:\n            self.log(\"%r did not open an sftp channel\" % (addr,), 3)\n            cli.close()\n            return\n\n        self.sessions[addr] = (chan, tra, psrv)\n        # tra.join()\n        # self.log(\"%r disconnected\" % (addr,))\n\n    def run(self):\n        lgr = logging.getLogger(\"paramiko.transport\")\n        lgr.setLevel(logging.DEBUG if self.args.sftpvv else logging.INFO)\n\n        if self.args.no_poll:\n            fun = self._run_select\n        else:\n            fun = self._run_poll\n        Daemon(fun, \"sftpd\")\n\n    def _run_select(self):\n        while not self.hub.stopping:\n            rx, _, _ = select.select(self.srv, [], [], 180)\n            for sck in rx:\n                self._accept(sck)\n\n    def _run_poll(self):\n        fd2sck = {}\n        poll = select.poll()\n        for sck in self.srv:\n            fd = sck.fileno()\n            fd2sck[fd] = sck\n            poll.register(fd, select.POLLIN)\n        while not self.hub.stopping:\n            pr = poll.poll(180 * 1000)\n            rx = [fd2sck[x[0]] for x in pr if x[1] & select.POLLIN]\n            for sck in rx:\n                self._accept(sck)\n"
  },
  {
    "path": "copyparty/smbd.py",
    "content": "# coding: utf-8\n\nimport inspect\nimport logging\nimport os\nimport random\nimport stat\nimport sys\nimport time\nfrom types import SimpleNamespace\n\nfrom .__init__ import ANYWIN, EXE, TYPE_CHECKING\nfrom .authsrv import LEELOO_DALLAS, VFS\nfrom .bos import bos\nfrom .util import Daemon, absreal, min_ex, pybin, runhook, vjoin\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Any, Union\n\nif TYPE_CHECKING:\n    from .svchub import SvcHub\n\n\nlg = logging.getLogger(\"smb\")\ndebug, info, warning, error = (lg.debug, lg.info, lg.warning, lg.error)\n\n\nclass SMB(object):\n    def __init__(self, hub: \"SvcHub\") -> None:\n        self.hub = hub\n        self.args = hub.args\n        self.asrv = hub.asrv\n        self.log = hub.log\n        self.files: dict[int, tuple[float, str]] = {}\n        self.noacc = self.args.smba\n        self.accs = not self.args.smba\n\n        lg.setLevel(logging.DEBUG if self.args.smbvvv else logging.INFO)\n        for x in [\"impacket\", \"impacket.smbserver\"]:\n            lgr = logging.getLogger(x)\n            lgr.setLevel(logging.DEBUG if self.args.smbvv else logging.INFO)\n\n        try:\n            from impacket import smbserver\n            from impacket.ntlm import compute_lmhash, compute_nthash\n        except ImportError:\n            if EXE:\n                print(\"copyparty.exe cannot do SMB\")\n                sys.exit(1)\n\n            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\"\n            print(m.format(min_ex(), pybin))\n            sys.exit(1)\n\n        # patch vfs into smbserver.os\n        fos = SimpleNamespace()\n        for k in os.__dict__:\n            try:\n                setattr(fos, k, getattr(os, k))\n            except:\n                pass\n        fos.close = self._close\n        fos.listdir = self._listdir\n        fos.mkdir = self._mkdir\n        fos.open = self._open\n        fos.remove = self._unlink\n        fos.rename = self._rename\n        fos.stat = self._stat\n        fos.unlink = self._unlink\n        fos.utime = self._utime\n        smbserver.os = fos\n\n        # ...and smbserver.os.path\n        fop = SimpleNamespace()\n        for k in os.path.__dict__:\n            try:\n                setattr(fop, k, getattr(os.path, k))\n            except:\n                pass\n        fop.exists = self._p_exists\n        fop.getsize = self._p_getsize\n        fop.isdir = self._p_isdir\n        smbserver.os.path = fop\n\n        if not self.args.smb_nwa_2:\n            fop.join = self._p_join\n\n        # other patches\n        smbserver.isInFileJail = self._is_in_file_jail\n        self._disarm()\n\n        ip = next((x for x in self.args.smb_i if \":\" not in x), None)\n        if not ip:\n            self.log(\"smb\", \"IPv6 not supported for SMB; listening on 0.0.0.0\", 3)\n            ip = \"0.0.0.0\"\n\n        port = int(self.args.smb_port)\n        srv = smbserver.SimpleSMBServer(listenAddress=ip, listenPort=port)\n        try:\n            if self.accs:\n                srv.setAuthCallback(self._auth_cb)\n        except:\n            self.accs = False\n            self.noacc = True\n            t = \"impacket too old; access permissions will not work! all accounts are admin!\"\n            self.log(\"smb\", t, 1)\n\n        ro = \"no\" if self.args.smbw else \"yes\"  # (does nothing)\n        srv.addShare(\"A\", \"/\", readOnly=ro)\n        srv.setSMB2Support(not self.args.smb1)\n\n        for name, pwd in self.asrv.acct.items():\n            for u, p in ((name, pwd), (pwd, \"k\")):\n                lmhash = compute_lmhash(p)\n                nthash = compute_nthash(p)\n                srv.addCredential(u, 0, lmhash, nthash)\n\n        chi = [random.randint(0, 255) for x in range(8)]\n        cha = \"\".join([\"{:02x}\".format(x) for x in chi])\n        srv.setSMBChallenge(cha)\n\n        self.srv = srv\n        self.stop = srv.stop\n        self.log(\"smb\", \"listening @ {}:{}\".format(ip, port))\n\n    def nlog(self, msg: str, c: Union[int, str] = 0) -> None:\n        self.log(\"smb\", msg, c)\n\n    def start(self) -> None:\n        Daemon(self.srv.start, \"smbd\")\n\n    def _auth_cb(self, *a, **ka):\n        debug(\"auth-result: %s %s\", a, ka)\n        conndata = ka[\"connData\"]\n        auth_ok = conndata[\"Authenticated\"]\n        uname = ka[\"user_name\"] if auth_ok else \"*\"\n        uname = self.asrv.iacct.get(uname, uname) or \"*\"\n        oldname = conndata.get(\"partygoer\", \"*\") or \"*\"\n        cli_ip = conndata[\"ClientIP\"]\n        cli_hn = ka[\"host_name\"]\n        if uname != \"*\":\n            conndata[\"partygoer\"] = uname\n            info(\"client %s [%s] authed as %s\", cli_ip, cli_hn, uname)\n        elif oldname != \"*\":\n            info(\"client %s [%s] keeping old auth as %s\", cli_ip, cli_hn, oldname)\n        elif auth_ok:\n            info(\"client %s [%s] authed as [*] (anon)\", cli_ip, cli_hn)\n        else:\n            info(\"client %s [%s] rejected\", cli_ip, cli_hn)\n\n    def _uname(self) -> str:\n        if self.noacc:\n            return LEELOO_DALLAS\n        if not self.asrv.acct:\n            return \"*\"\n\n        try:\n            # you found it! my single worst bit of code so far\n            # (if you can think of a better way to track users through impacket i'm all ears)\n            cf0 = inspect.currentframe().f_back.f_back\n            cf = cf0.f_back\n            for n in range(3):\n                cl = cf.f_locals\n                if \"connData\" in cl:\n                    return cl[\"connData\"][\"partygoer\"]\n                cf = cf.f_back\n            raise Exception()\n        except:\n            warning(\n                \"nyoron... %s <<-- %s <<-- %s <<-- %s\",\n                cf0.f_code.co_name,\n                cf0.f_back.f_code.co_name,\n                cf0.f_back.f_back.f_code.co_name,\n                cf0.f_back.f_back.f_back.f_code.co_name,\n            )\n            return \"*\"\n\n    def _v2a(\n        self, caller: str, vpath: str, *a: Any, uname=\"\", perms=None\n    ) -> tuple[VFS, str]:\n        vpath = vpath.replace(\"\\\\\", \"/\").lstrip(\"/\")\n        # cf = inspect.currentframe().f_back\n        # c1 = cf.f_back.f_code.co_name\n        # c2 = cf.f_code.co_name\n        if not uname:\n            uname = self._uname()\n        if not perms:\n            perms = [True, True]\n\n        debug('%s(\"%s\", %s) %s @%s\\033[K\\033[0m', caller, vpath, str(a), perms, uname)\n        vfs, rem = self.asrv.vfs.get(vpath, uname, *perms)\n        if not vfs.realpath:\n            raise Exception(\"unmapped vfs\")\n        return vfs, vfs.canonical(rem, False)\n\n    def _listdir(self, vpath: str, *a: Any, **ka: Any) -> list[str]:\n        vpath = vpath.replace(\"\\\\\", \"/\").lstrip(\"/\")\n        # caller = inspect.currentframe().f_back.f_code.co_name\n        uname = self._uname()\n        # debug('listdir(\"%s\", %s) @%s\\033[K\\033[0m', vpath, str(a), uname)\n        vfs, rem = self.asrv.vfs.get(vpath, uname, False, False)\n        if not vfs.realpath:\n            raise Exception(\"unmapped vfs\")\n        _, vfs_ls, vfs_virt = vfs.ls(\n            rem, uname, not self.args.no_scandir, [[False, False]]\n        )\n        dirs = [x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]\n        fils = [x[0] for x in vfs_ls if x[0] not in dirs]\n        ls = list(vfs_virt.keys()) + dirs + fils\n        if self.args.smb_nwa_1:\n            return ls\n\n        # clients crash somewhere around 65760 byte\n        ret = []\n        sz = 112 * 2  # ['.', '..']\n        for n, fn in enumerate(ls):\n            if sz >= 64000:\n                t = \"listing only %d of %d files (%d byte) in /%s for performance; see --smb-nwa-1\"\n                warning(t, n, len(ls), sz, vpath)\n                break\n\n            nsz = len(fn.encode(\"utf-16\", \"replace\"))\n            nsz = ((nsz + 7) // 8) * 8\n            sz += 104 + nsz\n            ret.append(fn)\n\n        return ret\n\n    def _open(\n        self, vpath: str, flags: int, *a: Any, chmod: int = 0o777, **ka: Any\n    ) -> Any:\n        f_ro = os.O_RDONLY\n        if ANYWIN:\n            f_ro |= os.O_BINARY\n\n        wr = flags != f_ro\n        if wr and not self.args.smbw:\n            yeet(\"blocked write (no --smbw): \" + vpath)\n\n        uname = self._uname()\n        vfs, ap = self._v2a(\"open\", vpath, *a, uname=uname, perms=[True, wr])\n        if wr:\n            if not vfs.axs.uwrite:\n                t = \"blocked write (no-write-acc %s): /%s @%s\"\n                yeet(t % (vfs.axs.uwrite, vpath, uname))\n\n            ap = absreal(ap)\n            xbu = vfs.flags.get(\"xbu\")\n            if xbu:\n                hr = runhook(\n                    self.nlog,\n                    None,\n                    self.hub.up2k,\n                    \"xbu.smb\",\n                    xbu,\n                    ap,\n                    vpath,\n                    \"\",\n                    \"\",\n                    \"\",\n                    0,\n                    0,\n                    \"1.7.6.2\",\n                    time.time(),\n                    None,\n                )\n                t = hr.get(\"rejectmsg\") or \"\"\n                if t or hr.get(\"rc\") != 0:\n                    if not t:\n                        t = \"blocked by xbu server config: %r\" % (vpath,)\n                    yeet(t)\n\n        ret = bos.open(ap, flags, *a, mode=chmod, **ka)\n        if wr:\n            now = time.time()\n            nf = len(self.files)\n            if nf > 9000:\n                oldest = min([x[0] for x in self.files.values()])\n                cutoff = oldest + (now - oldest) / 2\n                self.files = {k: v for k, v in self.files.items() if v[0] > cutoff}\n                info(\"was tracking %d files, now %d\", nf, len(self.files))\n\n            vpath = vpath.replace(\"\\\\\", \"/\").lstrip(\"/\")\n            self.files[ret] = (now, vpath)\n\n        return ret\n\n    def _close(self, fd: int) -> None:\n        os.close(fd)\n        if fd not in self.files:\n            return\n\n        _, vp = self.files.pop(fd)\n        vp, fn = os.path.split(vp)\n        vfs, rem = self.hub.asrv.vfs.get(vp, self._uname(), False, True)\n        vfs, rem = vfs.get_dbv(rem)\n        self.hub.up2k.hash_file(\n            vfs.realpath,\n            vfs.vpath,\n            vfs.flags,\n            rem,\n            fn,\n            \"1.7.6.2\",\n            time.time(),\n            \"\",\n        )\n\n    def _rename(self, vp1: str, vp2: str) -> None:\n        if not self.args.smbw:\n            yeet(\"blocked rename (no --smbw): \" + vp1)\n\n        vp1 = vp1.lstrip(\"/\")\n        vp2 = vp2.lstrip(\"/\")\n\n        uname = self._uname()\n        vfs2, ap2 = self._v2a(\"rename\", vp2, vp1, uname=uname)\n        if not vfs2.axs.uwrite:\n            t = \"blocked write (no-write-acc %s): /%s @%s\"\n            yeet(t % (vfs2.axs.uwrite, vp2, uname))\n\n        vfs1, _ = self.asrv.vfs.get(vp1, uname, True, True, True)\n        if not vfs1.axs.umove:\n            t = \"blocked rename (no-move-acc %s): /%s @%s\"\n            yeet(t % (vfs1.axs.umove, vp1, uname))\n\n        self.hub.up2k.handle_mv(\"\", uname, \"1.7.6.2\", vp1, vp2)\n        try:\n            bos.makedirs(ap2, vf=vfs2.flags)\n        except:\n            pass\n\n    def _mkdir(self, vpath: str) -> None:\n        if not self.args.smbw:\n            yeet(\"blocked mkdir (no --smbw): \" + vpath)\n\n        uname = self._uname()\n        vfs, ap = self._v2a(\"mkdir\", vpath, uname=uname)\n        if not vfs.axs.uwrite:\n            t = \"blocked mkdir (no-write-acc %s): /%s @%s\"\n            yeet(t % (vfs.axs.uwrite, vpath, uname))\n\n        return bos.mkdir(ap, vfs.flags[\"chmod_d\"])\n\n    def _stat(self, vpath: str, *a: Any, **ka: Any) -> os.stat_result:\n        try:\n            ap = self._v2a(\"stat\", vpath, *a, perms=[True, False])[1]\n            ret = bos.stat(ap, *a, **ka)\n            # debug(\" `-stat:ok\")\n            return ret\n        except:\n            # white lie: windows freaks out if we raise due to an offline volume\n            # debug(\" `-stat:NOPE (faking a directory)\")\n            ts = int(time.time())\n            return os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, ts, ts, ts))\n\n    def _unlink(self, vpath: str) -> None:\n        if not self.args.smbw:\n            yeet(\"blocked delete (no --smbw): \" + vpath)\n\n        # return bos.unlink(self._v2a(\"stat\", vpath, *a)[1])\n        uname = self._uname()\n        vfs, ap = self._v2a(\n            \"delete\", vpath, uname=uname, perms=[True, False, False, True]\n        )\n        if not vfs.axs.udel:\n            yeet(\"blocked delete (no-del-acc): \" + vpath)\n\n        vpath = vpath.replace(\"\\\\\", \"/\").lstrip(\"/\")\n        self.hub.up2k.handle_rm(uname, \"1.7.6.2\", [vpath], [], False, False)\n\n    def _utime(self, vpath: str, times: tuple[float, float]) -> None:\n        if not self.args.smbw:\n            yeet(\"blocked utime (no --smbw): \" + vpath)\n\n        uname = self._uname()\n        vfs, ap = self._v2a(\"utime\", vpath, uname=uname)\n        if not vfs.axs.uwrite:\n            t = \"blocked utime (no-write-acc %s): /%s @%s\"\n            yeet(t % (vfs.axs.uwrite, vpath, uname))\n\n        bos.utime_c(info, ap, int(times[1]), False)\n\n    def _p_exists(self, vpath: str) -> bool:\n        # ap = \"?\"\n        try:\n            ap = self._v2a(\"p.exists\", vpath, perms=[True, False])[1]\n            bos.stat(ap)\n            # debug(\" `-exists((%s)->(%s)):ok\", vpath, ap)\n            return True\n        except:\n            # debug(\" `-exists((%s)->(%s)):NOPE\", vpath, ap)\n            return False\n\n    def _p_getsize(self, vpath: str) -> int:\n        st = bos.stat(self._v2a(\"p.getsize\", vpath, perms=[True, False])[1])\n        return st.st_size\n\n    def _p_isdir(self, vpath: str) -> bool:\n        try:\n            st = bos.stat(self._v2a(\"p.isdir\", vpath, perms=[True, False])[1])\n            ret = stat.S_ISDIR(st.st_mode)\n            # debug(\" `-isdir:%s:%s\", st.st_mode, ret)\n            return ret\n        except:\n            return False\n\n    def _p_join(self, *a) -> str:\n        # impacket.smbserver reads globs from queryDirectoryRequest['Buffer']\n        # where somehow `fds.*` becomes `fds\"*` so lets fix that\n        ret = os.path.join(*a)\n        return ret.replace('\"', \".\")  # type: ignore\n\n    def _hook(self, *a: Any, **ka: Any) -> None:\n        src = inspect.currentframe().f_back.f_code.co_name\n        error(\"\\033[31m%s:hook(%s)\\033[0m\", src, a)\n        raise Exception(\"nope\")\n\n    def _disarm(self) -> None:\n        from impacket import smbserver\n\n        smbserver.os.chmod = self._hook\n        smbserver.os.chown = self._hook\n        smbserver.os.ftruncate = self._hook\n        smbserver.os.lchown = self._hook\n        smbserver.os.link = self._hook\n        smbserver.os.lstat = self._hook\n        smbserver.os.replace = self._hook\n        smbserver.os.scandir = self._hook\n        smbserver.os.symlink = self._hook\n        smbserver.os.truncate = self._hook\n        smbserver.os.walk = self._hook\n\n        smbserver.os.path.abspath = self._hook\n        smbserver.os.path.expanduser = self._hook\n        smbserver.os.path.expandvars = self._hook\n        smbserver.os.path.getatime = self._hook\n        smbserver.os.path.getctime = self._hook\n        smbserver.os.path.getmtime = self._hook\n        smbserver.os.path.isabs = self._hook\n        smbserver.os.path.isfile = self._hook\n        smbserver.os.path.islink = self._hook\n        smbserver.os.path.realpath = self._hook\n\n    def _is_in_file_jail(self, *a: Any) -> bool:\n        # handled by vfs\n        return True\n\n\ndef yeet(msg: str) -> None:\n    info(msg)\n    raise Exception(msg)\n"
  },
  {
    "path": "copyparty/ssdp.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport errno\nimport re\nimport select\nimport socket\nimport time\n\nfrom .__init__ import TYPE_CHECKING\nfrom .multicast import MC_Sck, MCast\nfrom .util import CachedSet, formatdate, html_escape, min_ex\n\nif TYPE_CHECKING:\n    from .broker_util import BrokerCli\n    from .httpcli import HttpCli\n    from .svchub import SvcHub\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Optional, Union\n\n\nGRP = \"239.255.255.250\"\n\n\nclass SSDP_Sck(MC_Sck):\n    def __init__(self, *a):\n        super(SSDP_Sck, self).__init__(*a)\n        self.hport = 0\n\n\nclass SSDPr(object):\n    \"\"\"generates http responses for httpcli\"\"\"\n\n    def __init__(self, broker: \"BrokerCli\") -> None:\n        self.broker = broker\n        self.args = broker.args\n\n    def reply(self, hc: \"HttpCli\") -> bool:\n        if hc.vpath.endswith(\"device.xml\"):\n            return self.tx_device(hc)\n\n        hc.reply(b\"unknown request\", 400)\n        return False\n\n    def tx_device(self, hc: \"HttpCli\") -> bool:\n        zs = \"\"\"\n<?xml version=\"1.0\"?>\n<root xmlns=\"urn:schemas-upnp-org:device-1-0\">\n    <specVersion>\n        <major>1</major>\n        <minor>0</minor>\n    </specVersion>\n    <URLBase>{}</URLBase>\n    <device>\n        <presentationURL>{}</presentationURL>\n        <deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>\n        <friendlyName>{}</friendlyName>\n        <modelDescription>file server</modelDescription>\n        <manufacturer>ed</manufacturer>\n        <manufacturerURL>https://ocv.me/</manufacturerURL>\n        <modelName>copyparty</modelName>\n        <modelURL>https://github.com/9001/copyparty/</modelURL>\n        <UDN>{}</UDN>\n        <serviceList>\n            <service>\n                <serviceType>urn:schemas-upnp-org:device:Basic:1</serviceType>\n                <serviceId>urn:schemas-upnp-org:device:Basic</serviceId>\n                <controlURL>/.cpr/ssdp/services.xml</controlURL>\n                <eventSubURL>/.cpr/ssdp/services.xml</eventSubURL>\n                <SCPDURL>/.cpr/ssdp/services.xml</SCPDURL>\n            </service>\n        </serviceList>\n    </device>\n</root>\"\"\"\n\n        c = html_escape\n        sip, sport = hc.s.getsockname()[:2]\n        sip = sip.replace(\"::ffff:\", \"\")\n        proto = \"https\" if self.args.https_only else \"http\"\n        ubase = \"{}://{}:{}\".format(proto, sip, sport)\n        zsl = self.args.zsl\n        url = zsl if \"://\" in zsl else ubase + \"/\" + zsl.lstrip(\"/\")\n        name = self.args.doctitle\n        zs = zs.strip().format(c(ubase), c(url), c(name), c(self.args.zsid))\n        hc.reply(zs.encode(\"utf-8\", \"replace\"))\n        return False  # close connection\n\n\nclass SSDPd(MCast):\n    \"\"\"communicates with ssdp clients over multicast\"\"\"\n\n    def __init__(self, hub: \"SvcHub\", ngen: int) -> None:\n        al = hub.args\n        vinit = al.zsv and not al.zmv\n        super(SSDPd, self).__init__(\n            hub, SSDP_Sck, al.zs_on, al.zs_off, GRP, \"\", 1900, vinit\n        )\n        self.srv: dict[socket.socket, SSDP_Sck] = {}\n        self.logsrc = \"SSDP-{}\".format(ngen)\n        self.ngen = ngen\n\n        self.rxc = CachedSet(0.7)\n        self.txc = CachedSet(5)  # win10: every 3 sec\n        self.ptn_st = re.compile(b\"\\nst: *upnp:rootdevice\", re.I)\n\n    def log(self, msg: str, c: Union[int, str] = 0) -> None:\n        self.log_func(self.logsrc, msg, c)\n\n    def run(self) -> None:\n        try:\n            bound = self.create_servers()\n        except:\n            t = \"no server IP matches the ssdp config\\n{}\"\n            self.log(t.format(min_ex()), 1)\n            bound = []\n\n        if not bound:\n            self.log(\"failed to announce copyparty services on the network\", 3)\n            return\n\n        # find http port for this listening ip\n        for srv in self.srv.values():\n            tcps = self.hub.tcpsrv.bound\n            hp = next((x[1] for x in tcps if x[0] in (\"0.0.0.0\", srv.ip)), 0)\n            hp = hp or next((x[1] for x in tcps if x[0] == \"::\"), 0)\n            if not hp:\n                hp = tcps[0][1]\n                self.log(\"assuming port {} for {}\".format(hp, srv.ip), 3)\n            srv.hport = hp\n\n        self.log(\"listening\")\n        try:\n            self.run2()\n        except OSError as ex:\n            if ex.errno != errno.EBADF:\n                raise\n\n            self.log(\"stopping due to {}\".format(ex), \"90\")\n\n        self.log(\"stopped\", 2)\n\n    def run2(self) -> None:\n        try:\n            if self.args.no_poll:\n                raise Exception()\n            fd2sck = {}\n            srvpoll = select.poll()\n            for sck in self.srv:\n                fd = sck.fileno()\n                fd2sck[fd] = sck\n                srvpoll.register(fd, select.POLLIN)\n        except Exception as ex:\n            srvpoll = None\n            if not self.args.no_poll:\n                t = \"WARNING: failed to poll(), will use select() instead: %r\"\n                self.log(t % (ex,), 3)\n\n        while self.running:\n            if srvpoll:\n                pr = srvpoll.poll((self.args.z_chk or 180) * 1000)\n                rx = [fd2sck[x[0]] for x in pr if x[1] & select.POLLIN]\n            else:\n                rdy = select.select(self.srv, [], [], self.args.z_chk or 180)\n                rx: list[socket.socket] = rdy[0]  # type: ignore\n\n            self.rxc.cln()\n            buf = b\"\"\n            addr = (\"0\", 0)\n            for sck in rx:\n                try:\n                    buf, addr = sck.recvfrom(4096)\n                    self.eat(buf, addr)\n                except:\n                    if not self.running:\n                        break\n\n                    t = \"{} {} \\033[33m|{}| {}\\n{}\".format(\n                        self.srv[sck].name, addr, len(buf), repr(buf)[2:-1], min_ex()\n                    )\n                    self.log(t, 6)\n\n    def stop(self) -> None:\n        self.running = False\n        for srv in self.srv.values():\n            try:\n                srv.sck.close()\n            except:\n                pass\n\n        self.srv.clear()\n\n    def eat(self, buf: bytes, addr: tuple[str, int]) -> None:\n        cip = addr[0]\n        if cip.startswith(\"169.254\") and not self.ll_ok:\n            return\n\n        if buf in self.rxc.c:\n            return\n\n        srv: Optional[SSDP_Sck] = self.map_client(cip)  # type: ignore\n        if not srv:\n            return\n\n        self.rxc.add(buf)\n        if not buf.startswith(b\"M-SEARCH * HTTP/1.\"):\n            return\n\n        if not self.ptn_st.search(buf):\n            return\n\n        if self.args.zsv:\n            t = \"{} [{}] \\033[36m{} \\033[0m|{}|\"\n            self.log(t.format(srv.name, srv.ip, cip, len(buf)), \"90\")\n\n        zs = \"\"\"\nHTTP/1.1 200 OK\nCACHE-CONTROL: max-age=1800\nDATE: {0}\nEXT:\nLOCATION: http://{1}:{2}/.cpr/ssdp/device.xml\nOPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\n01-NLS: {3}\nSERVER: UPnP/1.0\nST: upnp:rootdevice\nUSN: {3}::upnp:rootdevice\nBOOTID.UPNP.ORG: 0\nCONFIGID.UPNP.ORG: 1\n\n\"\"\"\n        v4 = srv.ip.replace(\"::ffff:\", \"\")\n        zs = zs.format(formatdate(), v4, srv.hport, self.args.zsid)\n        zb = zs[1:].replace(\"\\n\", \"\\r\\n\").encode(\"utf-8\", \"replace\")\n        srv.sck.sendto(zb, addr[:2])\n\n        if cip not in self.txc.c:\n            self.log(\"{} [{}] --> {}\".format(srv.name, srv.ip, cip), 6)\n\n        self.txc.add(cip)\n        self.txc.cln()\n"
  },
  {
    "path": "copyparty/star.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport re\nimport stat\nimport tarfile\n\nfrom queue import Queue\n\nfrom .authsrv import AuthSrv\nfrom .bos import bos\nfrom .sutil import StreamArc, errdesc\nfrom .util import Daemon, fsenc, min_ex\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Any, Generator, Optional\n\n    from .util import NamedLogger\n\n\nclass QFile(object):  # inherit io.StringIO for painful typing\n    \"\"\"file-like object which buffers writes into a queue\"\"\"\n\n    def __init__(self) -> None:\n        self.q: Queue[Optional[bytes]] = Queue(64)\n        self.bq: list[bytes] = []\n        self.nq = 0\n\n    def write(self, buf: Optional[bytes]) -> None:\n        if buf is None or self.nq >= 240 * 1024:\n            self.q.put(b\"\".join(self.bq))\n            self.bq = []\n            self.nq = 0\n\n        if buf is None:\n            self.q.put(None)\n        else:\n            self.bq.append(buf)\n            self.nq += len(buf)\n\n\nclass StreamTar(StreamArc):\n    \"\"\"construct in-memory tar file from the given path\"\"\"\n\n    def __init__(\n        self,\n        log: \"NamedLogger\",\n        asrv: AuthSrv,\n        fgen: Generator[dict[str, Any], None, None],\n        cmp: str = \"\",\n        **kwargs: Any\n    ):\n        super(StreamTar, self).__init__(log, asrv, fgen)\n\n        self.ci = 0\n        self.co = 0\n        self.qfile = QFile()\n        self.errf: dict[str, Any] = {}\n\n        # python 3.8 changed to PAX_FORMAT as default;\n        # slower, bigger, and no particular advantage\n        fmt = tarfile.GNU_FORMAT\n        if \"pax\" in cmp:\n            # unless a client asks for it (currently\n            # gnu-tar has wider support than pax-tar)\n            fmt = tarfile.PAX_FORMAT\n            cmp = re.sub(r\"[^a-z0-9]*pax[^a-z0-9]*\", \"\", cmp)\n\n        try:\n            cmp, zs = cmp.replace(\":\", \",\").split(\",\")\n            lv = int(zs)\n        except:\n            lv = -1\n\n        arg = {\"name\": None, \"fileobj\": self.qfile, \"mode\": \"w\", \"format\": fmt}\n        if cmp == \"gz\":\n            fun = tarfile.TarFile.gzopen\n            arg[\"compresslevel\"] = lv if lv >= 0 else 3\n        elif cmp == \"bz2\":\n            fun = tarfile.TarFile.bz2open\n            arg[\"compresslevel\"] = lv if lv >= 0 else 2\n        elif cmp == \"xz\":\n            fun = tarfile.TarFile.xzopen\n            arg[\"preset\"] = lv if lv >= 0 else 1\n        else:\n            fun = tarfile.open\n            arg[\"mode\"] = \"w|\"\n\n        self.tar = fun(**arg)\n\n        Daemon(self._gen, \"star-gen\")\n\n    def gen(self) -> Generator[Optional[bytes], None, None]:\n        buf = b\"\"\n        try:\n            while True:\n                buf = self.qfile.q.get()\n                if not buf:\n                    break\n\n                self.co += len(buf)\n                yield buf\n\n            yield None\n        finally:\n            while buf:\n                try:\n                    buf = self.qfile.q.get()\n                except:\n                    pass\n\n            if self.errf:\n                bos.unlink(self.errf[\"ap\"])\n\n    def ser(self, f: dict[str, Any]) -> None:\n        name = f[\"vp\"]\n        src = f[\"ap\"]\n        fsi = f[\"st\"]\n\n        if stat.S_ISDIR(fsi.st_mode):\n            return\n\n        inf = tarfile.TarInfo(name=name)\n        inf.mode = fsi.st_mode\n        inf.size = fsi.st_size\n        inf.mtime = fsi.st_mtime\n        inf.uid = 0\n        inf.gid = 0\n\n        self.ci += inf.size\n        with open(fsenc(src), \"rb\", self.args.iobuf) as fo:\n            self.tar.addfile(inf, fo)\n\n    def _gen(self) -> None:\n        errors = []\n        for f in self.fgen:\n            if \"err\" in f:\n                errors.append((f[\"vp\"], f[\"err\"]))\n                continue\n\n            if self.stopped:\n                break\n\n            try:\n                self.ser(f)\n            except:\n                ex = min_ex(5, True).replace(\"\\n\", \"\\n-- \")\n                errors.append((f[\"vp\"], ex))\n\n        if errors:\n            self.errf, txt = errdesc(self.asrv.vfs, errors)\n            self.log(\"\\n\".join(([repr(self.errf)] + txt[1:])))\n            self.ser(self.errf)\n\n        self.tar.close()\n        self.qfile.write(None)\n"
  },
  {
    "path": "copyparty/stolen/__init__.py",
    "content": ""
  },
  {
    "path": "copyparty/stolen/dnslib/README.md",
    "content": "`dnslib` but heavily simplified/feature-stripped\n\nL: MIT\nCopyright (c) 2010 - 2017 Paul Chakravarti\nhttps://github.com/paulc/dnslib/\n"
  },
  {
    "path": "copyparty/stolen/dnslib/__init__.py",
    "content": "# coding: utf-8\n\n\"\"\"\nL: MIT\nCopyright (c) 2010 - 2017 Paul Chakravarti\nhttps://github.com/paulc/dnslib/tree/0.9.23\n\"\"\"\n\nfrom .dns import *\n\nversion = \"0.9.23\"\n"
  },
  {
    "path": "copyparty/stolen/dnslib/bimap.py",
    "content": "# coding: utf-8\n\nimport types\n\n\nclass BimapError(Exception):\n    pass\n\n\nclass Bimap(object):\n    def __init__(self, name, forward, error=AttributeError):\n        self.name = name\n        self.error = error\n        self.forward = forward.copy()\n        self.reverse = dict([(v, k) for (k, v) in list(forward.items())])\n\n    def get(self, k, default=None):\n        try:\n            return self.forward[k]\n        except KeyError:\n            return default or str(k)\n\n    def __getitem__(self, k):\n        try:\n            return self.forward[k]\n        except KeyError:\n            if isinstance(self.error, types.FunctionType):\n                return self.error(self.name, k, True)\n            else:\n                raise self.error(\"%s: Invalid forward lookup: [%s]\" % (self.name, k))\n\n    def __getattr__(self, k):\n        try:\n            if k == \"__wrapped__\":\n                raise AttributeError()\n            return self.reverse[k]\n        except KeyError:\n            if isinstance(self.error, types.FunctionType):\n                return self.error(self.name, k, False)\n            else:\n                raise self.error(\"%s: Invalid reverse lookup: [%s]\" % (self.name, k))\n"
  },
  {
    "path": "copyparty/stolen/dnslib/bit.py",
    "content": "# coding: utf-8\n\nfrom __future__ import print_function\n\n\ndef get_bits(data, offset, bits=1):\n    mask = ((1 << bits) - 1) << offset\n    return (data & mask) >> offset\n\n\ndef set_bits(data, value, offset, bits=1):\n    mask = ((1 << bits) - 1) << offset\n    clear = 0xFFFF ^ mask\n    data = (data & clear) | ((value << offset) & mask)\n    return data\n"
  },
  {
    "path": "copyparty/stolen/dnslib/buffer.py",
    "content": "# coding: utf-8\n\nimport binascii\nimport struct\n\n\nclass BufferError(Exception):\n    pass\n\n\nclass Buffer(object):\n    def __init__(self, data=b\"\"):\n        self.data = bytearray(data)\n        self.offset = 0\n\n    def remaining(self):\n        return len(self.data) - self.offset\n\n    def get(self, length):\n        if length > self.remaining():\n            raise BufferError(\n                \"Not enough bytes [offset=%d,remaining=%d,requested=%d]\"\n                % (self.offset, self.remaining(), length)\n            )\n        start = self.offset\n        end = self.offset + length\n        self.offset += length\n        return bytes(self.data[start:end])\n\n    def hex(self):\n        return binascii.hexlify(self.data)\n\n    def pack(self, fmt, *args):\n        self.offset += struct.calcsize(fmt)\n        self.data += struct.pack(fmt, *args)\n\n    def append(self, s):\n        self.offset += len(s)\n        self.data += s\n\n    def update(self, ptr, fmt, *args):\n        s = struct.pack(fmt, *args)\n        self.data[ptr : ptr + len(s)] = s\n\n    def unpack(self, fmt):\n        try:\n            data = self.get(struct.calcsize(fmt))\n            return struct.unpack(fmt, data)\n        except struct.error:\n            raise BufferError(\n                \"Error unpacking struct '%s' <%s>\"\n                % (fmt, binascii.hexlify(data).decode())\n            )\n\n    def __len__(self):\n        return len(self.data)\n"
  },
  {
    "path": "copyparty/stolen/dnslib/dns.py",
    "content": "# coding: utf-8\n\nfrom __future__ import print_function\n\nimport binascii\nfrom itertools import chain\n\nfrom .bimap import Bimap, BimapError\nfrom .bit import get_bits, set_bits\nfrom .buffer import BufferError\nfrom .label import DNSBuffer, DNSLabel, set_avahi_379\nfrom .ranges import IP4, IP6, H, I, check_bytes\n\n\ntry:\n    range = xrange\nexcept:\n    pass\n\n\nclass DNSError(Exception):\n    pass\n\n\ndef unknown_qtype(name, key, forward):\n    if forward:\n        try:\n            return \"TYPE%d\" % (key,)\n        except:\n            raise DNSError(\"%s: Invalid forward lookup: [%s]\" % (name, key))\n    else:\n        if key.startswith(\"TYPE\"):\n            try:\n                return int(key[4:])\n            except:\n                pass\n        raise DNSError(\"%s: Invalid reverse lookup: [%s]\" % (name, key))\n\n\nQTYPE = Bimap(\n    \"QTYPE\",\n    {1: \"A\", 12: \"PTR\", 16: \"TXT\", 28: \"AAAA\", 33: \"SRV\", 47: \"NSEC\", 255: \"ANY\"},\n    unknown_qtype,\n)\n\nCLASS = Bimap(\"CLASS\", {1: \"IN\", 254: \"None\", 255: \"*\", 0x8001: \"F_IN\"}, DNSError)\n\nQR = Bimap(\"QR\", {0: \"QUERY\", 1: \"RESPONSE\"}, DNSError)\n\nRCODE = Bimap(\n    \"RCODE\",\n    {\n        0: \"NOERROR\",\n        1: \"FORMERR\",\n        2: \"SERVFAIL\",\n        3: \"NXDOMAIN\",\n        4: \"NOTIMP\",\n        5: \"REFUSED\",\n        6: \"YXDOMAIN\",\n        7: \"YXRRSET\",\n        8: \"NXRRSET\",\n        9: \"NOTAUTH\",\n        10: \"NOTZONE\",\n    },\n    DNSError,\n)\n\nOPCODE = Bimap(\n    \"OPCODE\", {0: \"QUERY\", 1: \"IQUERY\", 2: \"STATUS\", 4: \"NOTIFY\", 5: \"UPDATE\"}, DNSError\n)\n\n\ndef label(label, origin=None):\n    if label.endswith(\".\"):\n        return DNSLabel(label)\n    else:\n        return (origin if isinstance(origin, DNSLabel) else DNSLabel(origin)).add(label)\n\n\nclass DNSRecord(object):\n    @classmethod\n    def parse(cls, packet) -> \"DNSRecord\":\n        buffer = DNSBuffer(packet)\n        try:\n            header = DNSHeader.parse(buffer)\n            questions = []\n            rr = []\n            auth = []\n            ar = []\n            for i in range(header.q):\n                questions.append(DNSQuestion.parse(buffer))\n            for i in range(header.a):\n                rr.append(RR.parse(buffer))\n            for i in range(header.auth):\n                auth.append(RR.parse(buffer))\n            for i in range(header.ar):\n                ar.append(RR.parse(buffer))\n            return cls(header, questions, rr, auth=auth, ar=ar)\n        except (BufferError, BimapError) as e:\n            raise DNSError(\n                \"Error unpacking DNSRecord [offset=%d]: %s\" % (buffer.offset, e)\n            )\n\n    @classmethod\n    def question(cls, qname, qtype=\"A\", qclass=\"IN\"):\n        return DNSRecord(\n            q=DNSQuestion(qname, getattr(QTYPE, qtype), getattr(CLASS, qclass))\n        )\n\n    def __init__(\n        self, header=None, questions=None, rr=None, q=None, a=None, auth=None, ar=None\n    ) -> None:\n        self.header = header or DNSHeader()\n        self.questions: list[DNSQuestion] = questions or []\n        self.rr: list[RR] = rr or []\n        self.auth: list[RR] = auth or []\n        self.ar: list[RR] = ar or []\n\n        if q:\n            self.questions.append(q)\n        if a:\n            self.rr.append(a)\n        self.set_header_qa()\n\n    def reply(self, ra=1, aa=1):\n        return DNSRecord(\n            DNSHeader(id=self.header.id, bitmap=self.header.bitmap, qr=1, ra=ra, aa=aa),\n            q=self.q,\n        )\n\n    def add_question(self, *q) -> None:\n        self.questions.extend(q)\n        self.set_header_qa()\n\n    def add_answer(self, *rr) -> None:\n        self.rr.extend(rr)\n        self.set_header_qa()\n\n    def add_auth(self, *auth) -> None:\n        self.auth.extend(auth)\n        self.set_header_qa()\n\n    def add_ar(self, *ar) -> None:\n        self.ar.extend(ar)\n        self.set_header_qa()\n\n    def set_header_qa(self) -> None:\n        self.header.q = len(self.questions)\n        self.header.a = len(self.rr)\n        self.header.auth = len(self.auth)\n        self.header.ar = len(self.ar)\n\n    def get_q(self):\n        return self.questions[0] if self.questions else DNSQuestion()\n\n    q = property(get_q)\n\n    def get_a(self):\n        return self.rr[0] if self.rr else RR()\n\n    a = property(get_a)\n\n    def pack(self) -> bytes:\n        self.set_header_qa()\n        buffer = DNSBuffer()\n        self.header.pack(buffer)\n        for q in self.questions:\n            q.pack(buffer)\n        for rr in self.rr:\n            rr.pack(buffer)\n        for auth in self.auth:\n            auth.pack(buffer)\n        for ar in self.ar:\n            ar.pack(buffer)\n        return buffer.data\n\n    def truncate(self):\n        return DNSRecord(DNSHeader(id=self.header.id, bitmap=self.header.bitmap, tc=1))\n\n    def format(self, prefix=\"\", sort=False):\n        s = sorted if sort else lambda x: x\n        sections = [repr(self.header)]\n        sections.extend(s([repr(q) for q in self.questions]))\n        sections.extend(s([repr(rr) for rr in self.rr]))\n        sections.extend(s([repr(rr) for rr in self.auth]))\n        sections.extend(s([repr(rr) for rr in self.ar]))\n        return prefix + (\"\\n\" + prefix).join(sections)\n\n    short = format\n\n    def __repr__(self):\n        return self.format()\n\n    __str__ = __repr__\n\n\nclass DNSHeader(object):\n    id = H(\"id\")\n    bitmap = H(\"bitmap\")\n    q = H(\"q\")\n    a = H(\"a\")\n    auth = H(\"auth\")\n    ar = H(\"ar\")\n\n    @classmethod\n    def parse(cls, buffer):\n        try:\n            (id, bitmap, q, a, auth, ar) = buffer.unpack(\"!HHHHHH\")\n            return cls(id, bitmap, q, a, auth, ar)\n        except (BufferError, BimapError) as e:\n            raise DNSError(\n                \"Error unpacking DNSHeader [offset=%d]: %s\" % (buffer.offset, e)\n            )\n\n    def __init__(self, id=None, bitmap=None, q=0, a=0, auth=0, ar=0, **args) -> None:\n        self.id = id if id else 0\n        if bitmap is None:\n            self.bitmap = 0\n        else:\n            self.bitmap = bitmap\n        self.q = q\n        self.a = a\n        self.auth = auth\n        self.ar = ar\n        for k, v in args.items():\n            if k.lower() == \"qr\":\n                self.qr = v\n            elif k.lower() == \"opcode\":\n                self.opcode = v\n            elif k.lower() == \"aa\":\n                self.aa = v\n            elif k.lower() == \"tc\":\n                self.tc = v\n            elif k.lower() == \"rd\":\n                self.rd = v\n            elif k.lower() == \"ra\":\n                self.ra = v\n            elif k.lower() == \"z\":\n                self.z = v\n            elif k.lower() == \"ad\":\n                self.ad = v\n            elif k.lower() == \"cd\":\n                self.cd = v\n            elif k.lower() == \"rcode\":\n                self.rcode = v\n\n    def get_qr(self):\n        return get_bits(self.bitmap, 15)\n\n    def set_qr(self, val):\n        self.bitmap = set_bits(self.bitmap, val, 15)\n\n    qr = property(get_qr, set_qr)\n\n    def get_opcode(self):\n        return get_bits(self.bitmap, 11, 4)\n\n    def set_opcode(self, val):\n        self.bitmap = set_bits(self.bitmap, val, 11, 4)\n\n    opcode = property(get_opcode, set_opcode)\n\n    def get_aa(self):\n        return get_bits(self.bitmap, 10)\n\n    def set_aa(self, val):\n        self.bitmap = set_bits(self.bitmap, val, 10)\n\n    aa = property(get_aa, set_aa)\n\n    def get_tc(self):\n        return get_bits(self.bitmap, 9)\n\n    def set_tc(self, val):\n        self.bitmap = set_bits(self.bitmap, val, 9)\n\n    tc = property(get_tc, set_tc)\n\n    def get_rd(self):\n        return get_bits(self.bitmap, 8)\n\n    def set_rd(self, val):\n        self.bitmap = set_bits(self.bitmap, val, 8)\n\n    rd = property(get_rd, set_rd)\n\n    def get_ra(self):\n        return get_bits(self.bitmap, 7)\n\n    def set_ra(self, val):\n        self.bitmap = set_bits(self.bitmap, val, 7)\n\n    ra = property(get_ra, set_ra)\n\n    def get_z(self):\n        return get_bits(self.bitmap, 6)\n\n    def set_z(self, val):\n        self.bitmap = set_bits(self.bitmap, val, 6)\n\n    z = property(get_z, set_z)\n\n    def get_ad(self):\n        return get_bits(self.bitmap, 5)\n\n    def set_ad(self, val):\n        self.bitmap = set_bits(self.bitmap, val, 5)\n\n    ad = property(get_ad, set_ad)\n\n    def get_cd(self):\n        return get_bits(self.bitmap, 4)\n\n    def set_cd(self, val):\n        self.bitmap = set_bits(self.bitmap, val, 4)\n\n    cd = property(get_cd, set_cd)\n\n    def get_rcode(self):\n        return get_bits(self.bitmap, 0, 4)\n\n    def set_rcode(self, val):\n        self.bitmap = set_bits(self.bitmap, val, 0, 4)\n\n    rcode = property(get_rcode, set_rcode)\n\n    def pack(self, buffer):\n        buffer.pack(\"!HHHHHH\", self.id, self.bitmap, self.q, self.a, self.auth, self.ar)\n\n    def __repr__(self):\n        f = [\n            self.aa and \"AA\",\n            self.tc and \"TC\",\n            self.rd and \"RD\",\n            self.ra and \"RA\",\n            self.z and \"Z\",\n            self.ad and \"AD\",\n            self.cd and \"CD\",\n        ]\n        if OPCODE.get(self.opcode) == \"UPDATE\":\n            f1 = \"zo\"\n            f2 = \"pr\"\n            f3 = \"up\"\n            f4 = \"ad\"\n        else:\n            f1 = \"q\"\n            f2 = \"a\"\n            f3 = \"ns\"\n            f4 = \"ar\"\n        return (\n            \"<DNS Header: id=0x%x type=%s opcode=%s flags=%s \"\n            \"rcode='%s' %s=%d %s=%d %s=%d %s=%d>\"\n            % (\n                self.id,\n                QR.get(self.qr),\n                OPCODE.get(self.opcode),\n                \",\".join(filter(None, f)),\n                RCODE.get(self.rcode),\n                f1,\n                self.q,\n                f2,\n                self.a,\n                f3,\n                self.auth,\n                f4,\n                self.ar,\n            )\n        )\n\n    __str__ = __repr__\n\n\nclass DNSQuestion(object):\n    @classmethod\n    def parse(cls, buffer):\n        try:\n            qname = buffer.decode_name()\n            qtype, qclass = buffer.unpack(\"!HH\")\n            return cls(qname, qtype, qclass)\n        except (BufferError, BimapError) as e:\n            raise DNSError(\n                \"Error unpacking DNSQuestion [offset=%d]: %s\" % (buffer.offset, e)\n            )\n\n    def __init__(self, qname=None, qtype=1, qclass=1) -> None:\n        self.qname = qname\n        self.qtype = qtype\n        self.qclass = qclass\n\n    def set_qname(self, qname):\n        if isinstance(qname, DNSLabel):\n            self._qname = qname\n        else:\n            self._qname = DNSLabel(qname)\n\n    def get_qname(self):\n        return self._qname\n\n    qname = property(get_qname, set_qname)\n\n    def pack(self, buffer):\n        buffer.encode_name(self.qname)\n        buffer.pack(\"!HH\", self.qtype, self.qclass)\n\n    def __repr__(self):\n        return \"<DNS Question: '%s' qtype=%s qclass=%s>\" % (\n            self.qname,\n            QTYPE.get(self.qtype),\n            CLASS.get(self.qclass),\n        )\n\n    __str__ = __repr__\n\n\nclass RR(object):\n    rtype = H(\"rtype\")\n    rclass = H(\"rclass\")\n    ttl = I(\"ttl\")\n    rdlength = H(\"rdlength\")\n\n    @classmethod\n    def parse(cls, buffer):\n        try:\n            rname = buffer.decode_name()\n            rtype, rclass, ttl, rdlength = buffer.unpack(\"!HHIH\")\n            if rdlength:\n                rdata = RDMAP.get(QTYPE.get(rtype), RD).parse(buffer, rdlength)\n            else:\n                rdata = RD(b\"a\")\n            return cls(rname, rtype, rclass, ttl, rdata)\n        except (BufferError, BimapError) as e:\n            raise DNSError(\"Error unpacking RR [offset=%d]: %s\" % (buffer.offset, e))\n\n    def __init__(self, rname=None, rtype=1, rclass=1, ttl=0, rdata=None) -> None:\n        self.rname = rname\n        self.rtype = rtype\n        self.rclass = rclass\n        self.ttl = ttl\n        self.rdata = rdata\n\n    def set_rname(self, rname):\n        if isinstance(rname, DNSLabel):\n            self._rname = rname\n        else:\n            self._rname = DNSLabel(rname)\n\n    def get_rname(self):\n        return self._rname\n\n    rname = property(get_rname, set_rname)\n\n    def pack(self, buffer):\n        buffer.encode_name(self.rname)\n        buffer.pack(\"!HHI\", self.rtype, self.rclass, self.ttl)\n        rdlength_ptr = buffer.offset\n        buffer.pack(\"!H\", 0)\n        start = buffer.offset\n        self.rdata.pack(buffer)\n        end = buffer.offset\n        buffer.update(rdlength_ptr, \"!H\", end - start)\n\n    def __repr__(self):\n        return \"<DNS RR: '%s' rtype=%s rclass=%s ttl=%d rdata='%s'>\" % (\n            self.rname,\n            QTYPE.get(self.rtype),\n            CLASS.get(self.rclass),\n            self.ttl,\n            self.rdata,\n        )\n\n    __str__ = __repr__\n\n\nclass RD(object):\n    @classmethod\n    def parse(cls, buffer, length):\n        try:\n            data = buffer.get(length)\n            return cls(data)\n        except (BufferError, BimapError) as e:\n            raise DNSError(\"Error unpacking RD [offset=%d]: %s\" % (buffer.offset, e))\n\n    def __init__(self, data=b\"\") -> None:\n        check_bytes(\"data\", data)\n        self.data = bytes(data)\n\n    def pack(self, buffer):\n        buffer.append(self.data)\n\n    def __repr__(self):\n        if len(self.data) > 0:\n            return \"\\\\# %d %s\" % (\n                len(self.data),\n                binascii.hexlify(self.data).decode().upper(),\n            )\n        else:\n            return \"\\\\# 0\"\n\n    attrs = (\"data\",)\n\n\ndef _force_bytes(x):\n    if isinstance(x, bytes):\n        return x\n    else:\n        return x.encode()\n\n\nclass TXT(RD):\n    @classmethod\n    def parse(cls, buffer, length):\n        try:\n            data = list()\n            start_bo = buffer.offset\n            now_length = 0\n            while buffer.offset < start_bo + length:\n                (txtlength,) = buffer.unpack(\"!B\")\n\n                if now_length + txtlength < length:\n                    now_length += txtlength\n                    data.append(buffer.get(txtlength))\n                else:\n                    raise DNSError(\n                        \"Invalid TXT record: len(%d) > RD len(%d)\" % (txtlength, length)\n                    )\n            return cls(data)\n        except (BufferError, BimapError) as e:\n            raise DNSError(\"Error unpacking TXT [offset=%d]: %s\" % (buffer.offset, e))\n\n    def __init__(self, data) -> None:\n        if type(data) in (tuple, list):\n            self.data = [_force_bytes(x) for x in data]\n        else:\n            self.data = [_force_bytes(data)]\n        if any([len(x) > 255 for x in self.data]):\n            raise DNSError(\"TXT record too long: %s\" % self.data)\n\n    def pack(self, buffer):\n        for ditem in self.data:\n            if len(ditem) > 255:\n                raise DNSError(\"TXT record too long: %s\" % ditem)\n            buffer.pack(\"!B\", len(ditem))\n            buffer.append(ditem)\n\n    def __repr__(self):\n        return \",\".join([repr(x) for x in self.data])\n\n\nclass A(RD):\n\n    data = IP4(\"data\")\n\n    @classmethod\n    def parse(cls, buffer, length):\n        try:\n            data = buffer.unpack(\"!BBBB\")\n            return cls(data)\n        except (BufferError, BimapError) as e:\n            raise DNSError(\"Error unpacking A [offset=%d]: %s\" % (buffer.offset, e))\n\n    def __init__(self, data) -> None:\n        if type(data) in (tuple, list):\n            self.data = tuple(data)\n        else:\n            self.data = tuple(map(int, data.rstrip(\".\").split(\".\")))\n\n    def pack(self, buffer):\n        buffer.pack(\"!BBBB\", *self.data)\n\n    def __repr__(self):\n        return \"%d.%d.%d.%d\" % self.data\n\n\ndef _parse_ipv6(a):\n    l, _, r = a.partition(\"::\")\n    l_groups = list(chain(*[divmod(int(x, 16), 256) for x in l.split(\":\") if x]))\n    r_groups = list(chain(*[divmod(int(x, 16), 256) for x in r.split(\":\") if x]))\n    zeros = [0] * (16 - len(l_groups) - len(r_groups))\n    return tuple(l_groups + zeros + r_groups)\n\n\ndef _format_ipv6(a):\n    left = []\n    right = []\n    current = \"left\"\n    for i in range(0, 16, 2):\n        group = (a[i] << 8) + a[i + 1]\n        if current == \"left\":\n            if group == 0 and i < 14:\n                if (a[i + 2] << 8) + a[i + 3] == 0:\n                    current = \"right\"\n                else:\n                    left.append(\"0\")\n            else:\n                left.append(\"%x\" % group)\n        else:\n            if group == 0 and len(right) == 0:\n                pass\n            else:\n                right.append(\"%x\" % group)\n    if len(left) < 8:\n        return \":\".join(left) + \"::\" + \":\".join(right)\n    else:\n        return \":\".join(left)\n\n\nclass AAAA(RD):\n    data = IP6(\"data\")\n\n    @classmethod\n    def parse(cls, buffer, length):\n        try:\n            data = buffer.unpack(\"!16B\")\n            return cls(data)\n        except (BufferError, BimapError) as e:\n            raise DNSError(\"Error unpacking AAAA [offset=%d]: %s\" % (buffer.offset, e))\n\n    def __init__(self, data) -> None:\n        if type(data) in (tuple, list):\n            self.data = tuple(data)\n        else:\n            self.data = _parse_ipv6(data)\n\n    def pack(self, buffer):\n        buffer.pack(\"!16B\", *self.data)\n\n    def __repr__(self):\n        return _format_ipv6(self.data)\n\n\nclass CNAME(RD):\n    @classmethod\n    def parse(cls, buffer, length):\n        try:\n            label = buffer.decode_name()\n            return cls(label)\n        except (BufferError, BimapError) as e:\n            raise DNSError(\"Error unpacking CNAME [offset=%d]: %s\" % (buffer.offset, e))\n\n    def __init__(self, label=None) -> None:\n        self.label = label\n\n    def set_label(self, label):\n        if isinstance(label, DNSLabel):\n            self._label = label\n        else:\n            self._label = DNSLabel(label)\n\n    def get_label(self):\n        return self._label\n\n    label = property(get_label, set_label)\n\n    def pack(self, buffer):\n        buffer.encode_name(self.label)\n\n    def __repr__(self):\n        return \"%s\" % (self.label)\n\n    attrs = (\"label\",)\n\n\nclass PTR(CNAME):\n    pass\n\n\nclass SRV(RD):\n    priority = H(\"priority\")\n    weight = H(\"weight\")\n    port = H(\"port\")\n\n    @classmethod\n    def parse(cls, buffer, length):\n        try:\n            priority, weight, port = buffer.unpack(\"!HHH\")\n            target = buffer.decode_name()\n            return cls(priority, weight, port, target)\n        except (BufferError, BimapError) as e:\n            raise DNSError(\"Error unpacking SRV [offset=%d]: %s\" % (buffer.offset, e))\n\n    def __init__(self, priority=0, weight=0, port=0, target=None) -> None:\n        self.priority = priority\n        self.weight = weight\n        self.port = port\n        self.target = target\n\n    def set_target(self, target):\n        if isinstance(target, DNSLabel):\n            self._target = target\n        else:\n            self._target = DNSLabel(target)\n\n    def get_target(self):\n        return self._target\n\n    target = property(get_target, set_target)\n\n    def pack(self, buffer):\n        buffer.pack(\"!HHH\", self.priority, self.weight, self.port)\n        buffer.encode_name(self.target)\n\n    def __repr__(self):\n        return \"%d %d %d %s\" % (self.priority, self.weight, self.port, self.target)\n\n    attrs = (\"priority\", \"weight\", \"port\", \"target\")\n\n\ndef decode_type_bitmap(type_bitmap):\n    rrlist = []\n    buf = DNSBuffer(type_bitmap)\n    while buf.remaining():\n        winnum, winlen = buf.unpack(\"BB\")\n        bitmap = bytearray(buf.get(winlen))\n        for (pos, value) in enumerate(bitmap):\n            for i in range(8):\n                if (value << i) & 0x80:\n                    bitpos = (256 * winnum) + (8 * pos) + i\n                    rrlist.append(QTYPE[bitpos])\n    return rrlist\n\n\ndef encode_type_bitmap(rrlist):\n    rrlist = sorted([getattr(QTYPE, rr) for rr in rrlist])\n    buf = DNSBuffer()\n    curWindow = rrlist[0] // 256\n    bitmap = bytearray(32)\n    n = len(rrlist) - 1\n    for i, rr in enumerate(rrlist):\n        v = rr - curWindow * 256\n        bitmap[v // 8] |= 1 << (7 - v % 8)\n\n        if i == n or rrlist[i + 1] >= (curWindow + 1) * 256:\n            while bitmap[-1] == 0:\n                bitmap = bitmap[:-1]\n            buf.pack(\"BB\", curWindow, len(bitmap))\n            buf.append(bitmap)\n\n            if i != n:\n                curWindow = rrlist[i + 1] // 256\n                bitmap = bytearray(32)\n\n    return buf.data\n\n\nclass NSEC(RD):\n    @classmethod\n    def parse(cls, buffer, length):\n        try:\n            end = buffer.offset + length\n            name = buffer.decode_name()\n            rrlist = decode_type_bitmap(buffer.get(end - buffer.offset))\n            return cls(name, rrlist)\n        except (BufferError, BimapError) as e:\n            raise DNSError(\"Error unpacking NSEC [offset=%d]: %s\" % (buffer.offset, e))\n\n    def __init__(self, label, rrlist) -> None:\n        self.label = label\n        self.rrlist = rrlist\n\n    def set_label(self, label):\n        if isinstance(label, DNSLabel):\n            self._label = label\n        else:\n            self._label = DNSLabel(label)\n\n    def get_label(self):\n        return self._label\n\n    label = property(get_label, set_label)\n\n    def pack(self, buffer):\n        buffer.encode_name(self.label)\n        buffer.append(encode_type_bitmap(self.rrlist))\n\n    def __repr__(self):\n        return \"%s %s\" % (self.label, \" \".join(self.rrlist))\n\n    attrs = (\"label\", \"rrlist\")\n\n\nRDMAP = {\"A\": A, \"AAAA\": AAAA, \"TXT\": TXT, \"PTR\": PTR, \"SRV\": SRV, \"NSEC\": NSEC}\n"
  },
  {
    "path": "copyparty/stolen/dnslib/label.py",
    "content": "# coding: utf-8\n\nfrom __future__ import print_function\n\nimport re\n\nfrom .bit import get_bits, set_bits\nfrom .buffer import Buffer, BufferError\n\nLDH = set(range(33, 127))\nESCAPE = re.compile(r\"\\\\([0-9][0-9][0-9])\")\n\n\navahi_379 = 0\n\n\ndef set_avahi_379():\n    global avahi_379\n    avahi_379 = 1\n\n\ndef log_avahi_379(args):\n    global avahi_379\n    if avahi_379 == 2:\n        return\n    avahi_379 = 2\n    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\"\n    raise BufferError(t % args)\n\n\nclass DNSLabelError(Exception):\n    pass\n\n\nclass DNSLabel(object):\n    def __init__(self, label):\n        if type(label) == DNSLabel:\n            self.label = label.label\n        elif type(label) in (list, tuple):\n            self.label = tuple(label)\n        else:\n            if not label or label in (b\".\", \".\"):\n                self.label = ()\n            elif type(label) is not bytes:\n                if type(\"\") != type(b\"\"):\n\n                    label = ESCAPE.sub(lambda m: chr(int(m[1])), label)\n                self.label = tuple(label.encode(\"idna\").rstrip(b\".\").split(b\".\"))\n            else:\n                if type(\"\") == type(b\"\"):\n\n                    label = ESCAPE.sub(lambda m: chr(int(m.groups()[0])), label)\n                self.label = tuple(label.rstrip(b\".\").split(b\".\"))\n\n    def add(self, name):\n        new = DNSLabel(name)\n        if self.label:\n            new.label += self.label\n        return new\n\n    def idna(self):\n        return \".\".join([s.decode(\"idna\") for s in self.label]) + \".\"\n\n    def _decode(self, s):\n        if set(s).issubset(LDH):\n\n            return s.decode()\n        else:\n\n            return \"\".join([(chr(c) if (c in LDH) else \"\\\\%03d\" % c) for c in s])\n\n    def __str__(self):\n        return \".\".join([self._decode(bytearray(s)) for s in self.label]) + \".\"\n\n    def __repr__(self):\n        return \"<DNSLabel: '%s'>\" % str(self)\n\n    def __hash__(self):\n        return hash(tuple(map(lambda x: x.lower(), self.label)))\n\n    def __ne__(self, other):\n        return not self == other\n\n    def __eq__(self, other):\n        if type(other) != DNSLabel:\n            return self.__eq__(DNSLabel(other))\n        else:\n            return [l.lower() for l in self.label] == [l.lower() for l in other.label]\n\n    def __len__(self):\n        return len(b\".\".join(self.label))\n\n\nclass DNSBuffer(Buffer):\n    def __init__(self, data=b\"\"):\n        super(DNSBuffer, self).__init__(data)\n        self.names = {}\n\n    def decode_name(self, last=-1):\n        label = []\n        done = False\n        while not done:\n            (length,) = self.unpack(\"!B\")\n            if get_bits(length, 6, 2) == 3:\n\n                self.offset -= 1\n                pointer = get_bits(self.unpack(\"!H\")[0], 0, 14)\n                save = self.offset\n                if last == save:\n                    raise BufferError(\n                        \"Recursive pointer in DNSLabel [offset=%d,pointer=%d,length=%d]\"\n                        % (self.offset, pointer, len(self.data))\n                    )\n                if pointer < self.offset:\n                    self.offset = pointer\n                elif avahi_379:\n                    log_avahi_379((self.offset, pointer, len(self.data)))\n                    label.extend(b\"a\")\n                    break\n                else:\n                    raise BufferError(\n                        \"Invalid pointer in DNSLabel [offset=%d,pointer=%d,length=%d]\"\n                        % (self.offset, pointer, len(self.data))\n                    )\n                label.extend(self.decode_name(save).label)\n                self.offset = save\n                done = True\n            else:\n                if length > 0:\n                    l = self.get(length)\n                    try:\n                        l.decode()\n                    except UnicodeDecodeError:\n                        raise BufferError(\"Invalid label <%s>\" % l)\n                    label.append(l)\n                else:\n                    done = True\n        return DNSLabel(label)\n\n    def encode_name(self, name):\n        if not isinstance(name, DNSLabel):\n            name = DNSLabel(name)\n        if len(name) > 253:\n            raise DNSLabelError(\"Domain label too long: %r\" % name)\n        name = list(name.label)\n        while name:\n            if tuple(name) in self.names:\n\n                pointer = self.names[tuple(name)]\n                pointer = set_bits(pointer, 3, 14, 2)\n                self.pack(\"!H\", pointer)\n                return\n            else:\n                self.names[tuple(name)] = self.offset\n                element = name.pop(0)\n                if len(element) > 63:\n                    raise DNSLabelError(\"Label component too long: %r\" % element)\n                self.pack(\"!B\", len(element))\n                self.append(element)\n        self.append(b\"\\x00\")\n\n    def encode_name_nocompress(self, name):\n        if not isinstance(name, DNSLabel):\n            name = DNSLabel(name)\n        if len(name) > 253:\n            raise DNSLabelError(\"Domain label too long: %r\" % name)\n        name = list(name.label)\n        while name:\n            element = name.pop(0)\n            if len(element) > 63:\n                raise DNSLabelError(\"Label component too long: %r\" % element)\n            self.pack(\"!B\", len(element))\n            self.append(element)\n        self.append(b\"\\x00\")\n"
  },
  {
    "path": "copyparty/stolen/dnslib/lex.py",
    "content": "# coding: utf-8\n\nfrom __future__ import print_function\n\nimport collections\n\ntry:\n    from StringIO import StringIO\nexcept ImportError:\n    from io import StringIO\n\n\nclass Lexer(object):\n\n    escape_chars = \"\\\\\"\n    escape = {\"n\": \"\\n\", \"t\": \"\\t\", \"r\": \"\\r\"}\n\n    def __init__(self, f, debug=False):\n        if hasattr(f, \"read\"):\n            self.f = f\n        elif type(f) == str:\n            self.f = StringIO(f)\n        elif type(f) == bytes:\n            self.f = StringIO(f.decode())\n        else:\n            raise ValueError(\"Invalid input\")\n        self.debug = debug\n        self.q = collections.deque()\n        self.state = self.lexStart\n        self.escaped = False\n        self.eof = False\n\n    def __iter__(self):\n        return self.parse()\n\n    def next_token(self):\n        if self.debug:\n            print(\"STATE\", self.state)\n        (tok, self.state) = self.state()\n        return tok\n\n    def parse(self):\n        while self.state is not None and not self.eof:\n            tok = self.next_token()\n            if tok:\n                yield tok\n\n    def read(self, n=1):\n        s = \"\"\n        while self.q and n > 0:\n            s += self.q.popleft()\n            n -= 1\n        s += self.f.read(n)\n        if s == \"\":\n            self.eof = True\n        if self.debug:\n            print(\"Read: >%s<\" % repr(s))\n        return s\n\n    def peek(self, n=1):\n        s = \"\"\n        i = 0\n        while len(self.q) > i and n > 0:\n            s += self.q[i]\n            i += 1\n            n -= 1\n        r = self.f.read(n)\n        if n > 0 and r == \"\":\n            self.eof = True\n        self.q.extend(r)\n        if self.debug:\n            print(\"Peek : >%s<\" % repr(s + r))\n        return s + r\n\n    def pushback(self, s):\n        p = collections.deque(s)\n        p.extend(self.q)\n        self.q = p\n\n    def readescaped(self):\n        c = self.read(1)\n        if c in self.escape_chars:\n            self.escaped = True\n            n = self.peek(3)\n            if n.isdigit():\n                n = self.read(3)\n                if self.debug:\n                    print(\"Escape: >%s<\" % n)\n                return chr(int(n, 8))\n            elif n[0] in \"x\":\n                x = self.read(3)\n                if self.debug:\n                    print(\"Escape: >%s<\" % x)\n                return chr(int(x[1:], 16))\n            else:\n                c = self.read(1)\n                if self.debug:\n                    print(\"Escape: >%s<\" % c)\n                return self.escape.get(c, c)\n        else:\n            self.escaped = False\n            return c\n\n    def lexStart(self):\n        return (None, None)\n"
  },
  {
    "path": "copyparty/stolen/dnslib/ranges.py",
    "content": "# coding: utf-8\n\nimport sys\n\nif sys.version_info < (3,):\n    int_types = (\n        int,\n        long,\n    )\n    byte_types = (str, bytearray)\nelse:\n    int_types = (int,)\n    byte_types = (bytes, bytearray)\n\n\ndef check_instance(name, val, types):\n    if not isinstance(val, types):\n        raise ValueError(\n            \"Attribute '%s' must be instance of %s [%s]\" % (name, types, type(val))\n        )\n\n\ndef check_bytes(name, val):\n    return check_instance(name, val, byte_types)\n\n\ndef range_property(attr, min, max):\n    def getter(obj):\n        return getattr(obj, \"_%s\" % attr)\n\n    def setter(obj, val):\n        if isinstance(val, int_types) and min <= val <= max:\n            setattr(obj, \"_%s\" % attr, val)\n        else:\n            raise ValueError(\n                \"Attribute '%s' must be between %d-%d [%s]\" % (attr, min, max, val)\n            )\n\n    return property(getter, setter)\n\n\ndef B(attr):\n    return range_property(attr, 0, 255)\n\n\ndef H(attr):\n    return range_property(attr, 0, 65535)\n\n\ndef I(attr):\n    return range_property(attr, 0, 4294967295)\n\n\ndef ntuple_range(attr, n, min, max):\n    f = lambda x: isinstance(x, int_types) and min <= x <= max\n\n    def getter(obj):\n        return getattr(obj, \"_%s\" % attr)\n\n    def setter(obj, val):\n        if len(val) != n:\n            raise ValueError(\n                \"Attribute '%s' must be tuple with %d elements [%s]\" % (attr, n, val)\n            )\n        if all(map(f, val)):\n            setattr(obj, \"_%s\" % attr, val)\n        else:\n            raise ValueError(\n                \"Attribute '%s' elements must be between %d-%d [%s]\"\n                % (attr, min, max, val)\n            )\n\n    return property(getter, setter)\n\n\ndef IP4(attr):\n    return ntuple_range(attr, 4, 0, 255)\n\n\ndef IP6(attr):\n    return ntuple_range(attr, 16, 0, 255)\n"
  },
  {
    "path": "copyparty/stolen/ifaddr/README.md",
    "content": "`ifaddr` with py2.7 support enabled by make-sfx.sh which strips py3 hints using strip_hints and removes the `^if True:` blocks\n\nL: BSD-2-Clause\nCopyright (c) 2014 Stefan C. Mueller\nhttps://github.com/pydron/ifaddr/\n"
  },
  {
    "path": "copyparty/stolen/ifaddr/__init__.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\n\"\"\"\nL: BSD-2-Clause\nCopyright (c) 2014 Stefan C. Mueller\nhttps://github.com/pydron/ifaddr/tree/0.2.0\n\"\"\"\n\nimport os\nimport platform\n\nfrom ._shared import IP, Adapter\n\n\ndef nope(include_unconfigured=False):\n    return []\n\n\nhost_os = platform.system()\nmachine = platform.machine()\npy_impl = platform.python_implementation()\n\n\nif os.environ.get(\"PRTY_NO_IFADDR\"):\n    get_adapters = nope\nelif machine in (\"s390x\",) or host_os in (\"IRIX32\",):\n    # s390x deadlocks at libc.getifaddrs\n    # irix libc does not have getifaddrs at all\n    print(\"ifaddr unavailable; can't determine LAN IP: unsupported OS\")\n    get_adapters = nope\nelif py_impl in (\"GraalVM\",):\n    print(\"ifaddr unavailable; can't determine LAN IP: unsupported interpreter\")\n    get_adapters = nope\nelif os.name == \"nt\":\n    from ._win32 import get_adapters\nelif os.name == \"posix\":\n    from ._posix import get_adapters\nelse:\n    print(\"ifaddr unavailable; can't determine LAN IP: unsupported OS\")\n    get_adapters = nope\n\n\n__all__ = [\"Adapter\", \"IP\", \"get_adapters\"]\n"
  },
  {
    "path": "copyparty/stolen/ifaddr/_posix.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport collections\nimport ctypes.util\nimport os\nimport socket\n\nimport ipaddress\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Iterable, Optional\n\nfrom . import _shared as shared\nfrom ._shared import U\n\n\nclass ifaddrs(ctypes.Structure):\n    pass\n\n\nifaddrs._fields_ = [\n    (\"ifa_next\", ctypes.POINTER(ifaddrs)),\n    (\"ifa_name\", ctypes.c_char_p),\n    (\"ifa_flags\", ctypes.c_uint),\n    (\"ifa_addr\", ctypes.POINTER(shared.sockaddr)),\n    (\"ifa_netmask\", ctypes.POINTER(shared.sockaddr)),\n]\n\nlibc = ctypes.CDLL(ctypes.util.find_library(\"socket\" if os.uname()[0] == \"SunOS\" else \"c\"), use_errno=True)  # type: ignore\n\n\ndef get_adapters(include_unconfigured: bool = False) -> Iterable[shared.Adapter]:\n\n    addr0 = addr = ctypes.POINTER(ifaddrs)()\n    retval = libc.getifaddrs(ctypes.byref(addr))\n    if retval != 0:\n        eno = ctypes.get_errno()\n        raise OSError(eno, os.strerror(eno))\n\n    ips = collections.OrderedDict()\n\n    def add_ip(adapter_name: str, ip: Optional[shared.IP]) -> None:\n        if adapter_name not in ips:\n            index = None  # type: Optional[int]\n            try:\n                # Mypy errors on this when the Windows CI runs:\n                #     error: Module has no attribute \"if_nametoindex\"\n                index = socket.if_nametoindex(adapter_name)  # type: ignore\n            except (OSError, AttributeError):\n                pass\n            ips[adapter_name] = shared.Adapter(\n                adapter_name, adapter_name, [], index=index\n            )\n        if ip is not None:\n            ips[adapter_name].ips.append(ip)\n\n    while addr:\n        name = addr[0].ifa_name.decode(encoding=\"UTF-8\")\n        ip_addr = shared.sockaddr_to_ip(addr[0].ifa_addr)\n        if ip_addr:\n            if addr[0].ifa_netmask and not addr[0].ifa_netmask[0].sa_familiy:\n                addr[0].ifa_netmask[0].sa_familiy = addr[0].ifa_addr[0].sa_familiy\n            netmask = shared.sockaddr_to_ip(addr[0].ifa_netmask)\n            if isinstance(netmask, tuple):\n                netmaskStr = U(netmask[0])\n                prefixlen = shared.ipv6_prefixlength(ipaddress.IPv6Address(netmaskStr))\n            else:\n                if netmask is None:\n                    t = \"sockaddr_to_ip({}) returned None\"\n                    raise Exception(t.format(addr[0].ifa_netmask))\n\n                netmaskStr = U(\"0.0.0.0/\" + netmask)\n                prefixlen = ipaddress.IPv4Network(netmaskStr).prefixlen\n            ip = shared.IP(ip_addr, prefixlen, name)\n            add_ip(name, ip)\n        else:\n            if include_unconfigured:\n                add_ip(name, None)\n        addr = addr[0].ifa_next\n\n    libc.freeifaddrs(addr0)\n\n    return ips.values()\n"
  },
  {
    "path": "copyparty/stolen/ifaddr/_shared.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport ctypes\nimport platform\nimport socket\nimport sys\n\nimport ipaddress\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Callable, List, Optional, Union\n\n\nPY2 = sys.version_info < (3,)\nif not PY2:\n    U: Callable[[str], str] = str\nelse:\n    U = unicode  # noqa: F821  # pylint: disable=undefined-variable,self-assigning-variable\n    range = xrange  # noqa: F821  # pylint: disable=undefined-variable,self-assigning-variable\n\n\nclass Adapter(object):\n    \"\"\"\n    Represents a network interface device controller (NIC), such as a\n    network card. An adapter can have multiple IPs.\n\n    On Linux aliasing (multiple IPs per physical NIC) is implemented\n    by creating 'virtual' adapters, each represented by an instance\n    of this class. Each of those 'virtual' adapters can have both\n    a IPv4 and an IPv6 IP address.\n    \"\"\"\n\n    def __init__(\n        self, name: str, nice_name: str, ips: List[\"IP\"], index: Optional[int] = None\n    ) -> None:\n\n        #: Unique name that identifies the adapter in the system.\n        #: On Linux this is of the form of `eth0` or `eth0:1`, on\n        #: Windows it is a UUID in string representation, such as\n        #: `{846EE342-7039-11DE-9D20-806E6F6E6963}`.\n        self.name = name\n\n        #: Human readable name of the adpater. On Linux this\n        #: is currently the same as :attr:`name`. On Windows\n        #: this is the name of the device.\n        self.nice_name = nice_name\n\n        #: List of :class:`ifaddr.IP` instances in the order they were\n        #: reported by the system.\n        self.ips = ips\n\n        #: Adapter index as used by some API (e.g. IPv6 multicast group join).\n        self.index = index\n\n    def __repr__(self) -> str:\n        return \"Adapter(name={name}, nice_name={nice_name}, ips={ips}, index={index})\".format(\n            name=repr(self.name),\n            nice_name=repr(self.nice_name),\n            ips=repr(self.ips),\n            index=repr(self.index),\n        )\n\n\nif True:  # pylint: disable=using-constant-test\n    # Type of an IPv4 address (a string in \"xxx.xxx.xxx.xxx\" format)\n    _IPv4Address = str\n\n    # Type of an IPv6 address (a three-tuple `(ip, flowinfo, scope_id)`)\n    _IPv6Address = tuple[str, int, int]\n\n\nclass IP(object):\n    \"\"\"\n    Represents an IP address of an adapter.\n    \"\"\"\n\n    def __init__(\n        self, ip: Union[_IPv4Address, _IPv6Address], network_prefix: int, nice_name: str\n    ) -> None:\n\n        #: IP address. For IPv4 addresses this is a string in\n        #: \"xxx.xxx.xxx.xxx\" format. For IPv6 addresses this\n        #: is a three-tuple `(ip, flowinfo, scope_id)`, where\n        #: `ip` is a string in the usual collon separated\n        #: hex format.\n        self.ip = ip\n\n        #: Number of bits of the IP that represent the\n        #: network. For a `255.255.255.0` netmask, this\n        #: number would be `24`.\n        self.network_prefix = network_prefix\n\n        #: Human readable name for this IP.\n        #: On Linux is this currently the same as the adapter name.\n        #: On Windows this is the name of the network connection\n        #: as configured in the system control panel.\n        self.nice_name = nice_name\n\n    @property\n    def is_IPv4(self) -> bool:\n        \"\"\"\n        Returns `True` if this IP is an IPv4 address and `False`\n        if it is an IPv6 address.\n        \"\"\"\n        return not isinstance(self.ip, tuple)\n\n    @property\n    def is_IPv6(self) -> bool:\n        \"\"\"\n        Returns `True` if this IP is an IPv6 address and `False`\n        if it is an IPv4 address.\n        \"\"\"\n        return isinstance(self.ip, tuple)\n\n    def __repr__(self) -> str:\n        return \"IP(ip={ip}, network_prefix={network_prefix}, nice_name={nice_name})\".format(\n            ip=repr(self.ip),\n            network_prefix=repr(self.network_prefix),\n            nice_name=repr(self.nice_name),\n        )\n\n\nif platform.system() == \"Darwin\" or \"BSD\" in platform.system():\n\n    # BSD derived systems use marginally different structures\n    # than either Linux or Windows.\n    # I still keep it in `shared` since we can use\n    # both structures equally.\n\n    class sockaddr(ctypes.Structure):\n        _fields_ = [\n            (\"sa_len\", ctypes.c_uint8),\n            (\"sa_familiy\", ctypes.c_uint8),\n            (\"sa_data\", ctypes.c_uint8 * 14),\n        ]\n\n    class sockaddr_in(ctypes.Structure):\n        _fields_ = [\n            (\"sa_len\", ctypes.c_uint8),\n            (\"sa_familiy\", ctypes.c_uint8),\n            (\"sin_port\", ctypes.c_uint16),\n            (\"sin_addr\", ctypes.c_uint8 * 4),\n            (\"sin_zero\", ctypes.c_uint8 * 8),\n        ]\n\n    class sockaddr_in6(ctypes.Structure):\n        _fields_ = [\n            (\"sa_len\", ctypes.c_uint8),\n            (\"sa_familiy\", ctypes.c_uint8),\n            (\"sin6_port\", ctypes.c_uint16),\n            (\"sin6_flowinfo\", ctypes.c_uint32),\n            (\"sin6_addr\", ctypes.c_uint8 * 16),\n            (\"sin6_scope_id\", ctypes.c_uint32),\n        ]\n\nelse:\n\n    class sockaddr(ctypes.Structure):  # type: ignore\n        _fields_ = [(\"sa_familiy\", ctypes.c_uint16), (\"sa_data\", ctypes.c_uint8 * 14)]\n\n    class sockaddr_in(ctypes.Structure):  # type: ignore\n        _fields_ = [\n            (\"sin_familiy\", ctypes.c_uint16),\n            (\"sin_port\", ctypes.c_uint16),\n            (\"sin_addr\", ctypes.c_uint8 * 4),\n            (\"sin_zero\", ctypes.c_uint8 * 8),\n        ]\n\n    class sockaddr_in6(ctypes.Structure):  # type: ignore\n        _fields_ = [\n            (\"sin6_familiy\", ctypes.c_uint16),\n            (\"sin6_port\", ctypes.c_uint16),\n            (\"sin6_flowinfo\", ctypes.c_uint32),\n            (\"sin6_addr\", ctypes.c_uint8 * 16),\n            (\"sin6_scope_id\", ctypes.c_uint32),\n        ]\n\n\ndef sockaddr_to_ip(\n    sockaddr_ptr: \"ctypes.pointer[sockaddr]\",\n) -> Optional[Union[_IPv4Address, _IPv6Address]]:\n    if sockaddr_ptr:\n        if sockaddr_ptr[0].sa_familiy == socket.AF_INET:\n            ipv4 = ctypes.cast(sockaddr_ptr, ctypes.POINTER(sockaddr_in))\n            ippacked = bytes(bytearray(ipv4[0].sin_addr))\n            ip = U(ipaddress.ip_address(ippacked))\n            return ip\n        elif sockaddr_ptr[0].sa_familiy == socket.AF_INET6:\n            ipv6 = ctypes.cast(sockaddr_ptr, ctypes.POINTER(sockaddr_in6))\n            flowinfo = ipv6[0].sin6_flowinfo\n            ippacked = bytes(bytearray(ipv6[0].sin6_addr))\n            ip = U(ipaddress.ip_address(ippacked))\n            scope_id = ipv6[0].sin6_scope_id\n            return (ip, flowinfo, scope_id)\n    return None\n\n\ndef ipv6_prefixlength(address: ipaddress.IPv6Address) -> int:\n    prefix_length = 0\n    for i in range(address.max_prefixlen):\n        if int(address) >> i & 1:\n            prefix_length = prefix_length + 1\n    return prefix_length\n"
  },
  {
    "path": "copyparty/stolen/ifaddr/_win32.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport ctypes\nfrom ctypes import wintypes\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Iterable, List\n\nfrom . import _shared as shared\n\nNO_ERROR = 0\nERROR_BUFFER_OVERFLOW = 111\nMAX_ADAPTER_NAME_LENGTH = 256\nMAX_ADAPTER_DESCRIPTION_LENGTH = 128\nMAX_ADAPTER_ADDRESS_LENGTH = 8\nAF_UNSPEC = 0\n\n\nclass SOCKET_ADDRESS(ctypes.Structure):\n    _fields_ = [\n        (\"lpSockaddr\", ctypes.POINTER(shared.sockaddr)),\n        (\"iSockaddrLength\", wintypes.INT),\n    ]\n\n\nclass IP_ADAPTER_UNICAST_ADDRESS(ctypes.Structure):\n    pass\n\n\nIP_ADAPTER_UNICAST_ADDRESS._fields_ = [\n    (\"Length\", wintypes.ULONG),\n    (\"Flags\", wintypes.DWORD),\n    (\"Next\", ctypes.POINTER(IP_ADAPTER_UNICAST_ADDRESS)),\n    (\"Address\", SOCKET_ADDRESS),\n    (\"PrefixOrigin\", ctypes.c_uint),\n    (\"SuffixOrigin\", ctypes.c_uint),\n    (\"DadState\", ctypes.c_uint),\n    (\"ValidLifetime\", wintypes.ULONG),\n    (\"PreferredLifetime\", wintypes.ULONG),\n    (\"LeaseLifetime\", wintypes.ULONG),\n    (\"OnLinkPrefixLength\", ctypes.c_uint8),\n]\n\n\nclass IP_ADAPTER_ADDRESSES(ctypes.Structure):\n    pass\n\n\nIP_ADAPTER_ADDRESSES._fields_ = [\n    (\"Length\", wintypes.ULONG),\n    (\"IfIndex\", wintypes.DWORD),\n    (\"Next\", ctypes.POINTER(IP_ADAPTER_ADDRESSES)),\n    (\"AdapterName\", ctypes.c_char_p),\n    (\"FirstUnicastAddress\", ctypes.POINTER(IP_ADAPTER_UNICAST_ADDRESS)),\n    (\"FirstAnycastAddress\", ctypes.c_void_p),\n    (\"FirstMulticastAddress\", ctypes.c_void_p),\n    (\"FirstDnsServerAddress\", ctypes.c_void_p),\n    (\"DnsSuffix\", ctypes.c_wchar_p),\n    (\"Description\", ctypes.c_wchar_p),\n    (\"FriendlyName\", ctypes.c_wchar_p),\n]\n\n\niphlpapi = ctypes.windll.LoadLibrary(\"Iphlpapi\")  # type: ignore\n\n\ndef enumerate_interfaces_of_adapter(\n    nice_name: str, address: IP_ADAPTER_UNICAST_ADDRESS\n) -> Iterable[shared.IP]:\n\n    # Iterate through linked list and fill list\n    addresses = []  # type: List[IP_ADAPTER_UNICAST_ADDRESS]\n    while True:\n        addresses.append(address)\n        if not address.Next:\n            break\n        address = address.Next[0]\n\n    for address in addresses:\n        ip = shared.sockaddr_to_ip(address.Address.lpSockaddr)\n        if ip is None:\n            t = \"sockaddr_to_ip({}) returned None\"\n            raise Exception(t.format(address.Address.lpSockaddr))\n\n        network_prefix = address.OnLinkPrefixLength\n        yield shared.IP(ip, network_prefix, nice_name)\n\n\ndef get_adapters(include_unconfigured: bool = False) -> Iterable[shared.Adapter]:\n\n    # Call GetAdaptersAddresses() with error and buffer size handling\n\n    addressbuffersize = wintypes.ULONG(15 * 1024)\n    retval = ERROR_BUFFER_OVERFLOW\n    while retval == ERROR_BUFFER_OVERFLOW:\n        addressbuffer = ctypes.create_string_buffer(addressbuffersize.value)\n        retval = iphlpapi.GetAdaptersAddresses(\n            wintypes.ULONG(AF_UNSPEC),\n            wintypes.ULONG(0),\n            None,\n            ctypes.byref(addressbuffer),\n            ctypes.byref(addressbuffersize),\n        )\n    if retval != NO_ERROR:\n        raise ctypes.WinError()  # type: ignore\n\n    # Iterate through adapters fill array\n    address_infos = []  # type: List[IP_ADAPTER_ADDRESSES]\n    address_info = IP_ADAPTER_ADDRESSES.from_buffer(addressbuffer)\n    while True:\n        address_infos.append(address_info)\n        if not address_info.Next:\n            break\n        address_info = address_info.Next[0]\n\n    # Iterate through unicast addresses\n    result = []  # type: List[shared.Adapter]\n    for adapter_info in address_infos:\n\n        # We don't expect non-ascii characters here, so encoding shouldn't matter\n        name = adapter_info.AdapterName.decode()\n        nice_name = adapter_info.Description\n        index = adapter_info.IfIndex\n\n        if adapter_info.FirstUnicastAddress:\n            ips = enumerate_interfaces_of_adapter(\n                adapter_info.FriendlyName, adapter_info.FirstUnicastAddress[0]\n            )\n            ips = list(ips)\n            result.append(shared.Adapter(name, nice_name, ips, index=index))\n        elif include_unconfigured:\n            result.append(shared.Adapter(name, nice_name, [], index=index))\n\n    return result\n"
  },
  {
    "path": "copyparty/stolen/qrcodegen.py",
    "content": "# coding: utf-8\n\n# modified copy of Project Nayuki's qrcodegen (MIT-licensed);\n# https://github.com/nayuki/QR-Code-generator/blob/daa3114/python/qrcodegen.py\n# the original ^ is extremely well commented so refer to that for explanations\n\n# hacks: binary-only, auto-ecc, py2-compat\n\nfrom __future__ import print_function, unicode_literals\n\nimport collections\nimport itertools\n\nif True:  # pylint: disable=using-constant-test\n    from collections.abc import Sequence\n\n    from typing import Callable, List, Optional, Tuple, Union\n\ntry:\n    range = xrange\nexcept:\n    pass\n\n\ndef num_char_count_bits(ver: int) -> int:\n    return 16 if (ver + 7) // 17 else 8\n\n\nclass Ecc(object):\n    ordinal: int\n    formatbits: int\n\n    def __init__(self, i: int, fb: int) -> None:\n        self.ordinal = i\n        self.formatbits = fb\n\n    LOW: \"Ecc\"\n    MEDIUM: \"Ecc\"\n    QUARTILE: \"Ecc\"\n    HIGH: \"Ecc\"\n\n\nEcc.LOW = Ecc(0, 1)\nEcc.MEDIUM = Ecc(1, 0)\nEcc.QUARTILE = Ecc(2, 3)\nEcc.HIGH = Ecc(3, 2)\n\n\nclass QrSegment(object):\n    @staticmethod\n    def make_seg(data: Union[bytes, Sequence[int]]) -> \"QrSegment\":\n        bb = _BitBuffer()\n        for b in data:\n            bb.append_bits(b, 8)\n        return QrSegment(len(data), bb)\n\n    numchars: int  # num bytes, not the same as the data's bit length\n    bitdata: List[int]  # The data bits of this segment\n\n    def __init__(self, numch: int, bitdata: Sequence[int]) -> None:\n        if numch < 0:\n            raise ValueError()\n        self.numchars = numch\n        self.bitdata = list(bitdata)\n\n    @staticmethod\n    def get_total_bits(segs: Sequence[\"QrSegment\"], ver: int) -> Optional[int]:\n        result = 0\n        for seg in segs:\n            ccbits: int = num_char_count_bits(ver)\n            if seg.numchars >= (1 << ccbits):\n                return None  # segment length doesn't fit the field's bit width\n            result += 4 + ccbits + len(seg.bitdata)\n        return result\n\n\nclass QrCode(object):\n    @staticmethod\n    def encode_binary(data: Union[bytes, Sequence[int]]) -> \"QrCode\":\n        return QrCode.encode_segments([QrSegment.make_seg(data)])\n\n    @staticmethod\n    def encode_segments(\n        segs: Sequence[QrSegment],\n        ecl: Ecc = Ecc.LOW,\n        minver: int = 2,\n        maxver: int = 40,\n        mask: int = -1,\n    ) -> \"QrCode\":\n        for ver in range(minver, maxver + 1):\n            datacapacitybits: int = QrCode._get_num_data_codewords(ver, ecl) * 8\n            datausedbits: Optional[int] = QrSegment.get_total_bits(segs, ver)\n            if (datausedbits is not None) and (datausedbits <= datacapacitybits):\n                break\n\n        assert datausedbits\n\n        for newecl in (\n            Ecc.MEDIUM,\n            Ecc.QUARTILE,\n            Ecc.HIGH,\n        ):\n            if datausedbits <= QrCode._get_num_data_codewords(ver, newecl) * 8:\n                ecl = newecl\n\n        # Concatenate all segments to create the data bit string\n        bb = _BitBuffer()\n        for seg in segs:\n            bb.append_bits(4, 4)\n            bb.append_bits(seg.numchars, num_char_count_bits(ver))\n            bb.extend(seg.bitdata)\n        assert len(bb) == datausedbits\n\n        # Add terminator and pad up to a byte if applicable\n        datacapacitybits = QrCode._get_num_data_codewords(ver, ecl) * 8\n        assert len(bb) <= datacapacitybits\n        bb.append_bits(0, min(4, datacapacitybits - len(bb)))\n        bb.append_bits(0, -len(bb) % 8)\n        assert len(bb) % 8 == 0\n\n        # Pad with alternating bytes until data capacity is reached\n        for padbyte in itertools.cycle((0xEC, 0x11)):\n            if len(bb) >= datacapacitybits:\n                break\n            bb.append_bits(padbyte, 8)\n\n        # Pack bits into bytes in big endian\n        datacodewords = bytearray([0] * (len(bb) // 8))\n        for (i, bit) in enumerate(bb):\n            datacodewords[i >> 3] |= bit << (7 - (i & 7))\n\n        return QrCode(ver, ecl, datacodewords, mask)\n\n    ver: int\n    size: int  # w/h; 21..177 (ver * 4 + 17)\n    ecclvl: Ecc\n    mask: int  # 0..7\n    modules: List[List[bool]]\n    unmaskable: List[List[bool]]\n\n    def __init__(\n        self,\n        ver: int,\n        ecclvl: Ecc,\n        datacodewords: Union[bytes, Sequence[int]],\n        msk: int,\n    ) -> None:\n        self.ver = ver\n        self.size = ver * 4 + 17\n        self.ecclvl = ecclvl\n\n        self.modules = [[False] * self.size for _ in range(self.size)]\n        self.unmaskable = [[False] * self.size for _ in range(self.size)]\n\n        # Compute ECC, draw modules\n        self._draw_function_patterns()\n        allcodewords: bytes = self._add_ecc_and_interleave(bytearray(datacodewords))\n        self._draw_codewords(allcodewords)\n\n        if msk == -1:  # automask\n            minpenalty: int = 1 << 32\n            for i in range(8):\n                self._apply_mask(i)\n                self._draw_format_bits(i)\n                penalty = self._get_penalty_score()\n                if penalty < minpenalty:\n                    msk = i\n                    minpenalty = penalty\n                self._apply_mask(i)  # xor/undo\n\n        assert 0 <= msk <= 7\n        self.mask = msk\n        self._apply_mask(msk)  # Apply the final choice of mask\n        self._draw_format_bits(msk)  # Overwrite old format bits\n\n    def _draw_function_patterns(self) -> None:\n        # Draw horizontal and vertical timing patterns\n        for i in range(self.size):\n            self._set_function_module(6, i, i % 2 == 0)\n            self._set_function_module(i, 6, i % 2 == 0)\n\n        # Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules)\n        self._draw_finder_pattern(3, 3)\n        self._draw_finder_pattern(self.size - 4, 3)\n        self._draw_finder_pattern(3, self.size - 4)\n\n        # Draw numerous alignment patterns\n        alignpatpos: List[int] = self._get_alignment_pattern_positions()\n        numalign: int = len(alignpatpos)\n        skips: Sequence[Tuple[int, int]] = (\n            (0, 0),\n            (0, numalign - 1),\n            (numalign - 1, 0),\n        )\n        for i in range(numalign):\n            for j in range(numalign):\n                if (i, j) not in skips:  # avoid finder corners\n                    self._draw_alignment_pattern(alignpatpos[i], alignpatpos[j])\n\n        # draw config data with dummy mask value; ctor overwrites it\n        self._draw_format_bits(0)\n        self._draw_ver()\n\n    def _draw_format_bits(self, mask: int) -> None:\n        # Calculate error correction code and pack bits; ecclvl is uint2, mask is uint3\n        data: int = self.ecclvl.formatbits << 3 | mask\n        rem: int = data\n        for _ in range(10):\n            rem = (rem << 1) ^ ((rem >> 9) * 0x537)\n        bits: int = (data << 10 | rem) ^ 0x5412  # uint15\n        assert bits >> 15 == 0\n\n        # first copy\n        for i in range(0, 6):\n            self._set_function_module(8, i, _get_bit(bits, i))\n        self._set_function_module(8, 7, _get_bit(bits, 6))\n        self._set_function_module(8, 8, _get_bit(bits, 7))\n        self._set_function_module(7, 8, _get_bit(bits, 8))\n        for i in range(9, 15):\n            self._set_function_module(14 - i, 8, _get_bit(bits, i))\n\n        # second copy\n        for i in range(0, 8):\n            self._set_function_module(self.size - 1 - i, 8, _get_bit(bits, i))\n        for i in range(8, 15):\n            self._set_function_module(8, self.size - 15 + i, _get_bit(bits, i))\n        self._set_function_module(8, self.size - 8, True)  # Always dark\n\n    def _draw_ver(self) -> None:\n        if self.ver < 7:\n            return\n\n        # Calculate error correction code and pack bits\n        rem: int = self.ver  # ver is uint6, 7..40\n        for _ in range(12):\n            rem = (rem << 1) ^ ((rem >> 11) * 0x1F25)\n        bits: int = self.ver << 12 | rem  # uint18\n        assert bits >> 18 == 0\n\n        # Draw two copies\n        for i in range(18):\n            bit: bool = _get_bit(bits, i)\n            a: int = self.size - 11 + i % 3\n            b: int = i // 3\n            self._set_function_module(a, b, bit)\n            self._set_function_module(b, a, bit)\n\n    def _draw_finder_pattern(self, x: int, y: int) -> None:\n        for dy in range(-4, 5):\n            for dx in range(-4, 5):\n                xx, yy = x + dx, y + dy\n                if (0 <= xx < self.size) and (0 <= yy < self.size):\n                    # Chebyshev/infinity norm\n                    self._set_function_module(\n                        xx, yy, max(abs(dx), abs(dy)) not in (2, 4)\n                    )\n\n    def _draw_alignment_pattern(self, x: int, y: int) -> None:\n        for dy in range(-2, 3):\n            for dx in range(-2, 3):\n                self._set_function_module(x + dx, y + dy, max(abs(dx), abs(dy)) != 1)\n\n    def _set_function_module(self, x: int, y: int, isdark: bool) -> None:\n        self.modules[y][x] = isdark\n        self.unmaskable[y][x] = True\n\n    def _add_ecc_and_interleave(self, data: bytearray) -> bytes:\n        ver: int = self.ver\n        assert len(data) == QrCode._get_num_data_codewords(ver, self.ecclvl)\n\n        # Calculate parameter numbers\n        numblocks: int = QrCode._NUM_ERROR_CORRECTION_BLOCKS[self.ecclvl.ordinal][ver]\n        blockecclen: int = QrCode._ECC_CODEWORDS_PER_BLOCK[self.ecclvl.ordinal][ver]\n        rawcodewords: int = QrCode._get_num_raw_data_modules(ver) // 8\n        numshortblocks: int = numblocks - rawcodewords % numblocks\n        shortblocklen: int = rawcodewords // numblocks\n\n        # Split data into blocks and append ECC to each block\n        blocks: List[bytes] = []\n        rsdiv: bytes = QrCode._reed_solomon_compute_divisor(blockecclen)\n        k: int = 0\n        for i in range(numblocks):\n            dat: bytearray = data[\n                k : k + shortblocklen - blockecclen + (0 if i < numshortblocks else 1)\n            ]\n            k += len(dat)\n            ecc: bytes = QrCode._reed_solomon_compute_remainder(dat, rsdiv)\n            if i < numshortblocks:\n                dat.append(0)\n            blocks.append(dat + ecc)\n        assert k == len(data)\n\n        # Interleave (not concatenate) the bytes from every block into a single sequence\n        result = bytearray()\n        for i in range(len(blocks[0])):\n            for (j, blk) in enumerate(blocks):\n                # Skip the padding byte in short blocks\n                if (i != shortblocklen - blockecclen) or (j >= numshortblocks):\n                    result.append(blk[i])\n        assert len(result) == rawcodewords\n        return result\n\n    def _draw_codewords(self, data: bytes) -> None:\n        assert len(data) == QrCode._get_num_raw_data_modules(self.ver) // 8\n\n        i: int = 0  # Bit index into the data\n        for right in range(self.size - 1, 0, -2):\n            # idx of right column in each column pair\n            if right <= 6:\n                right -= 1\n            for vert in range(self.size):  # Vertical counter\n                for j in range(2):\n                    x: int = right - j\n                    upward: bool = (right + 1) & 2 == 0\n                    y: int = (self.size - 1 - vert) if upward else vert\n                    if (not self.unmaskable[y][x]) and (i < len(data) * 8):\n                        self.modules[y][x] = _get_bit(data[i >> 3], 7 - (i & 7))\n                        i += 1\n                    # any remainder bits (0..7) were set 0/false/light by ctor\n\n        assert i == len(data) * 8\n\n    def _apply_mask(self, mask: int) -> None:\n        masker: Callable[[int, int], int] = QrCode._MASK_PATTERNS[mask]\n        for y in range(self.size):\n            for x in range(self.size):\n                self.modules[y][x] ^= (masker(x, y) == 0) and (\n                    not self.unmaskable[y][x]\n                )\n\n    def _get_penalty_score(self) -> int:\n        result: int = 0\n        size: int = self.size\n        modules: List[List[bool]] = self.modules\n\n        # Adjacent modules in row having same color, and finder-like patterns\n        for y in range(size):\n            runcolor: bool = False\n            runx: int = 0\n            runhistory = collections.deque([0] * 7, 7)\n            for x in range(size):\n                if modules[y][x] == runcolor:\n                    runx += 1\n                    if runx == 5:\n                        result += QrCode._PENALTY_N1\n                    elif runx > 5:\n                        result += 1\n                else:\n                    self._finder_penalty_add_history(runx, runhistory)\n                    if not runcolor:\n                        result += (\n                            self._finder_penalty_count_patterns(runhistory)\n                            * QrCode._PENALTY_N3\n                        )\n                    runcolor = modules[y][x]\n                    runx = 1\n            result += (\n                self._finder_penalty_terminate_and_count(runcolor, runx, runhistory)\n                * QrCode._PENALTY_N3\n            )\n\n        # Adjacent modules in column having same color, and finder-like patterns\n        for x in range(size):\n            runcolor = False\n            runy = 0\n            runhistory = collections.deque([0] * 7, 7)\n            for y in range(size):\n                if modules[y][x] == runcolor:\n                    runy += 1\n                    if runy == 5:\n                        result += QrCode._PENALTY_N1\n                    elif runy > 5:\n                        result += 1\n                else:\n                    self._finder_penalty_add_history(runy, runhistory)\n                    if not runcolor:\n                        result += (\n                            self._finder_penalty_count_patterns(runhistory)\n                            * QrCode._PENALTY_N3\n                        )\n                    runcolor = modules[y][x]\n                    runy = 1\n            result += (\n                self._finder_penalty_terminate_and_count(runcolor, runy, runhistory)\n                * QrCode._PENALTY_N3\n            )\n\n        # 2*2 blocks of modules having same color\n        for y in range(size - 1):\n            for x in range(size - 1):\n                if (\n                    modules[y][x]\n                    == modules[y][x + 1]\n                    == modules[y + 1][x]\n                    == modules[y + 1][x + 1]\n                ):\n                    result += QrCode._PENALTY_N2\n\n        # Balance of dark and light modules\n        dark: int = sum((1 if cell else 0) for row in modules for cell in row)\n        total: int = size ** 2  # Note that size is odd, so dark/total != 1/2\n\n        # Compute the smallest integer k >= 0 such that (45-5k)% <= dark/total <= (55+5k)%\n        k: int = (abs(dark * 20 - total * 10) + total - 1) // total - 1\n        assert 0 <= k <= 9\n        result += k * QrCode._PENALTY_N4\n        assert 0 <= result <= 2568888\n        # ^ Non-tight upper bound based on default values of PENALTY_N1, ..., N4\n\n        return result\n\n    def _get_alignment_pattern_positions(self) -> List[int]:\n        ver: int = self.ver\n        if ver == 1:\n            return []\n\n        numalign: int = ver // 7 + 2\n        step: int = (\n            26\n            if (ver == 32)\n            else (ver * 4 + numalign * 2 + 1) // (numalign * 2 - 2) * 2\n        )\n        result: List[int] = [\n            (self.size - 7 - i * step) for i in range(numalign - 1)\n        ] + [6]\n        return list(reversed(result))\n\n    @staticmethod\n    def _get_num_raw_data_modules(ver: int) -> int:\n        result: int = (16 * ver + 128) * ver + 64\n        if ver >= 2:\n            numalign: int = ver // 7 + 2\n            result -= (25 * numalign - 10) * numalign - 55\n            if ver >= 7:\n                result -= 36\n        assert 208 <= result <= 29648\n        return result\n\n    @staticmethod\n    def _get_num_data_codewords(ver: int, ecl: Ecc) -> int:\n        return (\n            QrCode._get_num_raw_data_modules(ver) // 8\n            - QrCode._ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver]\n            * QrCode._NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver]\n        )\n\n    @staticmethod\n    def _reed_solomon_compute_divisor(degree: int) -> bytes:\n        if not (1 <= degree <= 255):\n            raise ValueError(\"Degree out of range\")\n\n        # Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1.\n        # For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array [255, 8, 93].\n        result = bytearray([0] * (degree - 1) + [1])  # start with monomial x^0\n\n        # Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}),\n        # and drop the highest monomial term which is always 1x^degree.\n        # Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D).\n        root: int = 1\n        for _ in range(degree):\n            # Multiply the current product by (x - r^i)\n            for j in range(degree):\n                result[j] = QrCode._reed_solomon_multiply(result[j], root)\n                if j + 1 < degree:\n                    result[j] ^= result[j + 1]\n            root = QrCode._reed_solomon_multiply(root, 0x02)\n\n        return result\n\n    @staticmethod\n    def _reed_solomon_compute_remainder(data: bytes, divisor: bytes) -> bytes:\n        result = bytearray([0] * len(divisor))\n        for b in data:  # Polynomial division\n            factor: int = b ^ result.pop(0)\n            result.append(0)\n            for (i, coef) in enumerate(divisor):\n                result[i] ^= QrCode._reed_solomon_multiply(coef, factor)\n\n        return result\n\n    @staticmethod\n    def _reed_solomon_multiply(x: int, y: int) -> int:\n        if (x >> 8 != 0) or (y >> 8 != 0):\n            raise ValueError(\"Byte out of range\")\n        z: int = 0  # Russian peasant multiplication\n        for i in reversed(range(8)):\n            z = (z << 1) ^ ((z >> 7) * 0x11D)\n            z ^= ((y >> i) & 1) * x\n        assert z >> 8 == 0\n        return z\n\n    def _finder_penalty_count_patterns(self, runhistory: collections.deque[int]) -> int:\n        n: int = runhistory[1]\n        assert n <= self.size * 3\n        core: bool = (\n            n > 0\n            and (runhistory[2] == runhistory[4] == runhistory[5] == n)\n            and runhistory[3] == n * 3\n        )\n        return (\n            1 if (core and runhistory[0] >= n * 4 and runhistory[6] >= n) else 0\n        ) + (1 if (core and runhistory[6] >= n * 4 and runhistory[0] >= n) else 0)\n\n    def _finder_penalty_terminate_and_count(\n        self,\n        currentruncolor: bool,\n        currentrunlength: int,\n        runhistory: collections.deque[int],\n    ) -> int:\n        if currentruncolor:  # Terminate dark run\n            self._finder_penalty_add_history(currentrunlength, runhistory)\n            currentrunlength = 0\n        currentrunlength += self.size  # Add light border to final run\n        self._finder_penalty_add_history(currentrunlength, runhistory)\n        return self._finder_penalty_count_patterns(runhistory)\n\n    def _finder_penalty_add_history(\n        self, currentrunlength: int, runhistory: collections.deque[int]\n    ) -> None:\n        if runhistory[0] == 0:\n            currentrunlength += self.size  # Add light border to initial run\n\n        runhistory.appendleft(currentrunlength)\n\n    _PENALTY_N1: int = 3\n    _PENALTY_N2: int = 3\n    _PENALTY_N3: int = 40\n    _PENALTY_N4: int = 10\n\n    # fmt: off\n    _ECC_CODEWORDS_PER_BLOCK: Sequence[Sequence[int]] = (\n        (-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\n        (-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\n        (-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\n        (-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\n\n    _NUM_ERROR_CORRECTION_BLOCKS: Sequence[Sequence[int]] = (\n        (-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\n        (-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\n        (-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\n        (-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\n    # fmt: on\n\n    _MASK_PATTERNS: Sequence[Callable[[int, int], int]] = (\n        (lambda x, y: (x + y) % 2),\n        (lambda x, y: y % 2),\n        (lambda x, y: x % 3),\n        (lambda x, y: (x + y) % 3),\n        (lambda x, y: (x // 3 + y // 2) % 2),\n        (lambda x, y: x * y % 2 + x * y % 3),\n        (lambda x, y: (x * y % 2 + x * y % 3) % 2),\n        (lambda x, y: ((x + y) % 2 + x * y % 3) % 2),\n    )\n\n\nclass _BitBuffer(list):  # type: ignore\n    def append_bits(self, val: int, n: int) -> None:\n        if (n < 0) or (val >> n != 0):\n            raise ValueError(\"Value out of range\")\n\n        self.extend(((val >> i) & 1) for i in reversed(range(n)))\n\n\ndef _get_bit(x: int, i: int) -> bool:\n    return (x >> i) & 1 != 0\n\n\nclass DataTooLongError(ValueError):\n    pass\n"
  },
  {
    "path": "copyparty/stolen/surrogateescape.py",
    "content": "# coding: utf-8\n\n\"\"\"\nThis is Victor Stinner's pure-Python implementation of PEP 383: the \"surrogateescape\" error\nhandler of Python 3.\n\nScissored from the python-future module to avoid 4.4MB of additional dependencies:\nhttps://github.com/PythonCharmers/python-future/blob/e12549c42ed3a38ece45b9d88c75f5f3ee4d658d/src/future/utils/surrogateescape.py\n\nOriginal source: misc/python/surrogateescape.py in https://bitbucket.org/haypo/misc\n\"\"\"\n\n# This code is released under the Python license and the BSD 2-clause license\n\nimport codecs\nimport platform\nimport sys\n\nPY3 = sys.version_info > (3,)\nWINDOWS = platform.system() == \"Windows\"\nFS_ERRORS = \"surrogateescape\"\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Any\n\n\nif PY3:\n    _unichr = chr\n    bytes_chr = lambda code: bytes((code,))\nelse:\n    _unichr = unichr\n    bytes_chr = chr\n\n\ndef surrogateescape_handler(exc: Any) -> tuple[str, int]:\n    \"\"\"\n    Pure Python implementation of the PEP 383: the \"surrogateescape\" error\n    handler of Python 3. Undecodable bytes will be replaced by a Unicode\n    character U+DCxx on decoding, and these are translated into the\n    original bytes on encoding.\n    \"\"\"\n    mystring = exc.object[exc.start : exc.end]\n\n    try:\n        if isinstance(exc, UnicodeDecodeError):\n            # mystring is a byte-string in this case\n            decoded = replace_surrogate_decode(mystring)\n        elif isinstance(exc, UnicodeEncodeError):\n            # In the case of u'\\udcc3'.encode('ascii',\n            # 'this_surrogateescape_handler'), both Python 2.x and 3.x raise an\n            # exception anyway after this function is called, even though I think\n            # it's doing what it should. It seems that the strict encoder is called\n            # to encode the unicode string that this function returns ...\n            decoded = replace_surrogate_encode(mystring)\n        else:\n            raise exc\n    except NotASurrogateError:\n        raise exc\n    return (decoded, exc.end)\n\n\nclass NotASurrogateError(Exception):\n    pass\n\n\ndef replace_surrogate_encode(mystring: str) -> str:\n    \"\"\"\n    Returns a (unicode) string, not the more logical bytes, because the codecs\n    register_error functionality expects this.\n    \"\"\"\n    decoded = []\n    for ch in mystring:\n        code = ord(ch)\n\n        # The following magic comes from Py3.3's Python/codecs.c file:\n        if not 0xD800 <= code <= 0xDCFF:\n            # Not a surrogate. Fail with the original exception.\n            raise NotASurrogateError\n        # mybytes = [0xe0 | (code >> 12),\n        #            0x80 | ((code >> 6) & 0x3f),\n        #            0x80 | (code & 0x3f)]\n        # Is this a good idea?\n        if 0xDC00 <= code <= 0xDC7F:\n            decoded.append(_unichr(code - 0xDC00))\n        elif code <= 0xDCFF:\n            decoded.append(_unichr(code - 0xDC00))\n        else:\n            raise NotASurrogateError\n    return str().join(decoded)\n\n\ndef replace_surrogate_decode(mybytes: bytes) -> str:\n    \"\"\"\n    Returns a (unicode) string\n    \"\"\"\n    decoded = []\n    for ch in mybytes:\n        # We may be parsing newbytes (in which case ch is an int) or a native\n        # str on Py2\n        if isinstance(ch, int):\n            code = ch\n        else:\n            code = ord(ch)\n        if 0x80 <= code <= 0xFF:\n            decoded.append(_unichr(0xDC00 + code))\n        elif code <= 0x7F:\n            decoded.append(_unichr(code))\n        else:\n            raise NotASurrogateError\n    return str().join(decoded)\n\n\ndef encodefilename(fn: str) -> bytes:\n    if FS_ENCODING == \"ascii\":\n        # ASCII encoder of Python 2 expects that the error handler returns a\n        # Unicode string encodable to ASCII, whereas our surrogateescape error\n        # handler has to return bytes in 0x80-0xFF range.\n        encoded = []\n        for index, ch in enumerate(fn):\n            code = ord(ch)\n            if code < 128:\n                ch = bytes_chr(code)\n            elif 0xDC80 <= code <= 0xDCFF:\n                ch = bytes_chr(code - 0xDC00)\n            else:\n                raise UnicodeEncodeError(\n                    FS_ENCODING, fn, index, index + 1, \"ordinal not in range(128)\"\n                )\n            encoded.append(ch)\n        return bytes().join(encoded)\n    elif FS_ENCODING == \"utf-8\":\n        # UTF-8 encoder of Python 2 encodes surrogates, so U+DC80-U+DCFF\n        # doesn't go through our error handler\n        encoded = []\n        for index, ch in enumerate(fn):\n            code = ord(ch)\n            if 0xD800 <= code <= 0xDFFF:\n                if 0xDC80 <= code <= 0xDCFF:\n                    ch = bytes_chr(code - 0xDC00)\n                    encoded.append(ch)\n                else:\n                    raise UnicodeEncodeError(\n                        FS_ENCODING, fn, index, index + 1, \"surrogates not allowed\"\n                    )\n            else:\n                ch_utf8 = ch.encode(\"utf-8\")\n                encoded.append(ch_utf8)\n        return bytes().join(encoded)\n    else:\n        return fn.encode(FS_ENCODING, FS_ERRORS)\n\n\ndef decodefilename(fn: bytes) -> str:\n    return fn.decode(FS_ENCODING, FS_ERRORS)\n\n\nFS_ENCODING = sys.getfilesystemencoding()\n\n\nif WINDOWS and not PY3:\n    # py2 thinks win* is mbcs, probably a bug? anyways this works\n    FS_ENCODING = \"utf-8\"\n\n\n# normalize the filesystem encoding name.\n# For example, we expect \"utf-8\", not \"UTF8\".\nFS_ENCODING = codecs.lookup(FS_ENCODING).name\n\n\ndef register_surrogateescape() -> None:\n    \"\"\"\n    Registers the surrogateescape error handler on Python 2 (only)\n    \"\"\"\n    if PY3:\n        return\n    try:\n        codecs.lookup_error(FS_ERRORS)\n    except LookupError:\n        codecs.register_error(FS_ERRORS, surrogateescape_handler)\n"
  },
  {
    "path": "copyparty/sutil.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport os\nimport tempfile\nfrom datetime import datetime\n\nfrom .__init__ import CORES\nfrom .authsrv import VFS, AuthSrv\nfrom .bos import bos\nfrom .th_cli import ThumbCli\nfrom .th_srv import TH_CH\nfrom .util import UTC, vjoin, vol_san\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Any, Generator, Optional\n\n    from .util import NamedLogger\n\n\nTAR_NO_OPUS = set(\"aac|m4a|m4b|m4r|mp3|oga|ogg|opus|wma\".split(\"|\"))\n\n\nclass StreamArc(object):\n    def __init__(\n        self,\n        log: \"NamedLogger\",\n        asrv: AuthSrv,\n        fgen: Generator[dict[str, Any], None, None],\n        **kwargs: Any\n    ):\n        self.log = log\n        self.asrv = asrv\n        self.args = asrv.args\n        self.fgen = fgen\n        self.stopped = False\n\n    def gen(self) -> Generator[Optional[bytes], None, None]:\n        raise Exception(\"override me\")\n\n    def stop(self) -> None:\n        self.stopped = True\n\n\ndef gfilter(\n    fgen: Generator[dict[str, Any], None, None],\n    thumbcli: ThumbCli,\n    uname: str,\n    vtop: str,\n    fmt: str,\n) -> Generator[dict[str, Any], None, None]:\n    from concurrent.futures import ThreadPoolExecutor\n\n    pend = []\n    with ThreadPoolExecutor(max_workers=CORES) as tp:\n        try:\n            for f in fgen:\n                task = tp.submit(enthumb, thumbcli, uname, vtop, f, fmt)\n                pend.append((task, f))\n                if pend[0][0].done() or len(pend) > CORES * 4:\n                    task, f = pend.pop(0)\n                    try:\n                        f = task.result(600)\n                    except:\n                        pass\n                    yield f\n\n            for task, f in pend:\n                try:\n                    f = task.result(600)\n                except:\n                    pass\n                yield f\n        except Exception as ex:\n            thumbcli.log(\"gfilter flushing ({})\".format(ex))\n            for task, f in pend:\n                try:\n                    task.result(600)\n                except:\n                    pass\n            thumbcli.log(\"gfilter flushed\")\n\n\ndef enthumb(\n    thumbcli: ThumbCli, uname: str, vtop: str, f: dict[str, Any], fmt: str\n) -> dict[str, Any]:\n    rem = f[\"vp\"]\n    ext = rem.rsplit(\".\", 1)[-1].lower()\n    if (fmt == \"mp3\" and ext == \"mp3\") or (fmt == \"opus\" and ext in TAR_NO_OPUS):\n        raise Exception()\n\n    vp = vjoin(vtop, rem.split(\"/\", 1)[1])\n    vn, rem = thumbcli.asrv.vfs.get(vp, uname, True, False)\n    dbv, vrem = vn.get_dbv(rem)\n    thp = thumbcli.get(dbv, vrem, f[\"st\"].st_mtime, fmt)\n    if not thp:\n        raise Exception()\n\n    ext = fmt if fmt == \"wav\" else TH_CH.get(fmt[:1], fmt)\n    sz = bos.path.getsize(thp)\n    st: os.stat_result = f[\"st\"]\n    ts = st.st_mtime\n    f[\"ap\"] = thp\n    f[\"vp\"] = f[\"vp\"].rsplit(\".\", 1)[0] + \".\" + ext\n    f[\"st\"] = os.stat_result((st.st_mode, -1, -1, 1, 1000, 1000, sz, ts, ts, ts))\n    return f\n\n\ndef errdesc(\n    vfs: VFS, errors: list[tuple[str, str]]\n) -> tuple[dict[str, Any], list[str]]:\n    report = [\"copyparty failed to add the following files to the archive:\", \"\"]\n\n    for fn, err in errors:\n        report.extend([\" file: %r\" % (fn,), \"error: %s\" % (err,), \"\"])\n\n    btxt = \"\\r\\n\".join(report).encode(\"utf-8\", \"replace\")\n    btxt = vol_san(list(vfs.all_vols.values()), btxt)\n\n    with tempfile.NamedTemporaryFile(prefix=\"copyparty-\", delete=False) as tf:\n        tf_path = tf.name\n        tf.write(btxt)\n\n    dt = datetime.now(UTC).strftime(\"%Y-%m%d-%H%M%S\")\n\n    bos.chmod(tf_path, 0o444)\n    return {\n        \"vp\": \"archive-errors-{}.txt\".format(dt),\n        \"ap\": tf_path,\n        \"st\": bos.stat(tf_path),\n    }, report\n"
  },
  {
    "path": "copyparty/svchub.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport argparse\nimport atexit\nimport errno\nimport json\nimport logging\nimport os\nimport re\nimport shlex\nimport signal\nimport socket\nimport string\nimport sys\nimport threading\nimport time\nfrom datetime import datetime\n\n# from inspect import currentframe\n# print(currentframe().f_lineno)\n\n\nif True:  # pylint: disable=using-constant-test\n    from types import FrameType\n\n    import typing\n    from typing import Any, Optional, Union\n\nfrom .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode\nfrom .__version__ import S_VERSION, VERSION\nfrom .authsrv import BAD_CFG, AuthSrv, derive_args, n_du_who, n_ver_who\nfrom .bos import bos\nfrom .cert import ensure_cert\nfrom .fsutil import ramdisk_chk\nfrom .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN\nfrom .pwhash import HAVE_ARGON2\nfrom .tcpsrv import TcpSrv\nfrom .th_srv import (\n    H_PIL_AVIF,\n    H_PIL_HEIF,\n    H_PIL_WEBP,\n    HAVE_FFMPEG,\n    HAVE_FFPROBE,\n    HAVE_PIL,\n    HAVE_RAW,\n    HAVE_VIPS,\n    ThumbSrv,\n)\nfrom .up2k import Up2k\nfrom .util import (\n    DEF_EXP,\n    DEF_MTE,\n    DEF_MTH,\n    FFMPEG_URL,\n    HAVE_PSUTIL,\n    HAVE_SQLITE3,\n    HAVE_ZMQ,\n    RE_ANSI,\n    URL_BUG,\n    UTC,\n    VERSIONS,\n    Daemon,\n    Garda,\n    HLog,\n    HMaccas,\n    ODict,\n    alltrace,\n    build_netmap,\n    expat_ver,\n    gzip,\n    html_escape,\n    load_ipr,\n    load_ipu,\n    lock_file,\n    min_ex,\n    mp,\n    odfusion,\n    pybin,\n    read_utf8,\n    start_log_thrs,\n    start_stackmon,\n    termsize,\n    ub64enc,\n    umktrans,\n)\n\nif HAVE_SQLITE3:\n    import sqlite3\n\nif TYPE_CHECKING:\n    try:\n        from .mdns import MDNS\n        from .ssdp import SSDPd\n    except:\n        pass\n\nif PY2:\n    range = xrange  # type: ignore\n\nif PY2:\n    from urllib2 import Request, urlopen\nelse:\n    from urllib.request import Request, urlopen\n\n\nVER_IDP_DB = 1\nVER_SESSION_DB = 1\nVER_SHARES_DB = 2\n\n\nclass SvcHub(object):\n    \"\"\"\n    Hosts all services which cannot be parallelized due to reliance on monolithic resources.\n    Creates a Broker which does most of the heavy stuff; hosted services can use this to perform work:\n        hub.broker.<say|ask>(destination, args_list).\n\n    Either BrokerThr (plain threads) or BrokerMP (multiprocessing) is used depending on configuration.\n    Nothing is returned synchronously; if you want any value returned from the call,\n    put() can return a queue (if want_reply=True) which has a blocking get() with the response.\n    \"\"\"\n\n    def __init__(\n        self,\n        args: argparse.Namespace,\n        dargs: argparse.Namespace,\n        argv: list[str],\n        printed: str,\n    ) -> None:\n        self.args = args\n        self.dargs = dargs\n        self.argv = argv\n        self.E: EnvParams = args.E\n        self.no_ansi = args.no_ansi\n        self.flo = args.flo\n        self.tz = UTC if args.log_utc else None\n        self.logf: Optional[typing.TextIO] = None\n        self.logf_base_fn = \"\"\n        self.is_dut = False  # running in unittest; always False\n        self.stop_req = False\n        self.stopping = False\n        self.stopped = False\n        self.reload_req = False\n        self.reload_mutex = threading.Lock()\n        self.stop_cond = threading.Condition()\n        self.nsigs = 3\n        self.retcode = 0\n        self.httpsrv_up = 0\n        self.qr_tsz = None\n\n        self.log_mutex = threading.Lock()\n        self.cday = 0\n        self.cmon = 0\n        self.tstack = 0.0\n\n        self.iphash = HMaccas(os.path.join(self.E.cfg, \"iphash\"), 8)\n\n        if args.sss or args.s >= 3:\n            args.ss = True\n            args.no_dav = True\n            args.no_logues = True\n            args.no_readme = True\n            args.lo = args.lo or \"cpp-%Y-%m%d-%H%M%S.txt.xz\"\n            args.ls = args.ls or \"**,*,ln,p,r\"\n\n        if args.ss or args.s >= 2:\n            args.s = True\n            args.unpost = 0\n            args.no_del = True\n            args.no_mv = True\n            args.reflink = True\n            args.dav_auth = True\n            args.vague_403 = True\n            args.nih = True\n\n        if args.s:\n            args.dotpart = True\n            args.no_thumb = True\n            args.no_mtag_ff = True\n            args.no_robots = True\n            args.force_js = True\n\n        if not self._process_config():\n            raise Exception(BAD_CFG)\n\n        # for non-http clients (ftp, tftp)\n        self.bans: dict[str, int] = {}\n        self.gpwd = Garda(self.args.ban_pw)\n        self.gpwc = Garda(self.args.ban_pwc)\n        self.g404 = Garda(self.args.ban_404)\n        self.g403 = Garda(self.args.ban_403)\n        self.g422 = Garda(self.args.ban_422, False)\n        self.gmal = Garda(self.args.ban_422)\n        self.gurl = Garda(self.args.ban_url)\n\n        self.log_div = 10 ** (6 - args.log_tdec)\n        self.log_efmt = \"%02d:%02d:%02d.%0{}d\".format(args.log_tdec)\n        self.log_dfmt = \"%04d-%04d-%06d.%0{}d\".format(args.log_tdec)\n\n        if args.q:\n            self.log = self._log_disabled\n        elif args.lo and args.flo == 2 and not self.no_ansi:\n            self.log = self._log_en_f2\n        else:\n            self.log = self._log_enabled\n\n        if args.lo:\n            self._setup_logfile(printed)\n\n        lg = logging.getLogger()\n        lh = HLog(self.log)\n        lg.handlers = [lh]\n        lg.setLevel(logging.DEBUG)\n\n        self._check_env()\n\n        if args.stackmon:\n            start_stackmon(args.stackmon, 0)\n\n        if args.log_thrs:\n            start_log_thrs(self.log, args.log_thrs, 0)\n\n        if not args.use_fpool and args.j != 1:\n            args.no_fpool = True\n            t = \"multithreading enabled with -j {}, so disabling fpool -- this can reduce upload performance on some filesystems, and make some antivirus-softwares \"\n            c = 0\n            if ANYWIN:\n                t += \"(especially Microsoft Defender) stress your CPU and HDD severely during big uploads\"\n                c = 3\n            else:\n                t += \"consume more resources (CPU/HDD) than normal\"\n            self.log(\"root\", t.format(args.j), c)\n\n        if not args.no_fpool and args.j != 1:\n            t = \"WARNING: ignoring --use-fpool because multithreading (-j{}) is enabled\"\n            self.log(\"root\", t.format(args.j), c=3)\n            args.no_fpool = True\n\n        args.p_nodav = [int(x.strip()) for x in args.p_nodav.split(\",\") if x]\n        if args.dav_port and args.dav_port not in args.p:\n            args.p.append(args.dav_port)\n\n        for name, arg in (\n            (\"iobuf\", \"iobuf\"),\n            (\"s-rd-sz\", \"s_rd_sz\"),\n            (\"s-wr-sz\", \"s_wr_sz\"),\n        ):\n            zi = getattr(args, arg)\n            if zi < 32768:\n                t = \"WARNING: expect very poor performance because you specified a very low value (%d) for --%s\"\n                self.log(\"root\", t % (zi, name), 3)\n                zi = 2\n            zi2 = 2 ** (zi - 1).bit_length()\n            if zi != zi2:\n                zi3 = 2 ** ((zi - 1).bit_length() - 1)\n                t = \"WARNING: expect poor performance because --%s is not a power-of-two; consider using %d or %d instead of %d\"\n                self.log(\"root\", t % (name, zi2, zi3, zi), 3)\n\n        if args.s_rd_sz > args.iobuf:\n            t = \"WARNING: --s-rd-sz (%d) is larger than --iobuf (%d); this may lead to reduced performance\"\n            self.log(\"root\", t % (args.s_rd_sz, args.iobuf), 3)\n\n        zs = \"\"\n        if args.th_ram_max < 0.22:\n            zs = \"generate thumbnails\"\n        elif args.th_ram_max < 1:\n            zs = \"generate audio waveforms or spectrograms\"\n        if zs:\n            t = \"WARNING: --th-ram-max is very small (%.2f GiB); will not be able to %s\"\n            self.log(\"root\", t % (args.th_ram_max, zs), 3)\n\n        if args.chpw and args.have_idp_hdrs and \"pw\" not in args.auth_ord.split(\",\"):\n            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\"\n            self.log(\"root\", t, 1)\n            raise Exception(t)\n\n        noch = set()\n        for zs in args.chpw_no or []:\n            zsl = [x.strip() for x in zs.split(\",\")]\n            noch.update([x for x in zsl if x])\n        args.chpw_no = noch\n\n        if args.ipu:\n            iu, nm = load_ipu(self.log, args.ipu, True)\n            setattr(args, \"ipu_iu\", iu)\n            setattr(args, \"ipu_nm\", nm)\n\n        if args.ipr:\n            ipr = load_ipr(self.log, args.ipr, True)\n            setattr(args, \"ipr_u\", ipr)\n\n        for zs in \"ah_salt fk_salt dk_salt\".split():\n            if getattr(args, \"show_%s\" % (zs,)):\n                self.log(\"root\", \"effective %s is %s\" % (zs, getattr(args, zs)))\n\n        if args.ah_cli or args.ah_gen:\n            args.idp_store = 0\n            args.no_ses = True\n            args.shr = \"\"\n\n        if args.idp_store and args.have_idp_hdrs:\n            self.setup_db(\"idp\")\n\n        if not self.args.no_ses:\n            self.setup_db(\"ses\")\n\n        args.shr1 = \"\"\n        if args.shr:\n            self.setup_share_db()\n\n        bri = \"zy\"[args.theme % 2 :][:1]\n        ch = \"abcdefghijklmnopqrstuvwx\"[int(args.theme / 2)]\n        args.theme = \"{0}{1} {0} {1}\".format(ch, bri)\n\n        if args.no_stack:\n            args.stack_who = \"no\"\n\n        if args.nid:\n            args.du_who = \"no\"\n        args.du_iwho = n_du_who(args.du_who)\n\n        if args.ver and args.ver_who == \"no\":\n            args.ver_who = \"all\"\n        args.ver_iwho = n_ver_who(args.ver_who)\n\n        if args.nih:\n            args.vname = \"\"\n            args.doctitle = args.doctitle.replace(\" @ --name\", \"\")\n        else:\n            args.vname = args.name\n        args.doctitle = args.doctitle.replace(\"--name\", args.vname)\n        args.bname = args.bname.replace(\"--name\", args.vname) or args.vname\n\n        for zs in \"shr_site up_site\".split():\n            if getattr(args, zs) == \"--site\":\n                setattr(args, zs, args.site)\n\n        if args.log_fk:\n            args.log_fk = re.compile(args.log_fk)\n\n        # initiate all services to manage\n        self.asrv = AuthSrv(self.args, self.log, dargs=self.dargs)\n        ramdisk_chk(self.asrv)\n\n        if args.cgen:\n            self.asrv.cgen()\n\n        if args.exit == \"cfg\":\n            sys.exit(0)\n\n        if args.ls:\n            self.asrv.dbg_ls()\n\n        if not ANYWIN:\n            self._setlimits()\n\n        self.log(\"root\", \"max clients: {}\".format(self.args.nc))\n\n        self.tcpsrv = TcpSrv(self)\n\n        if not self.tcpsrv.srv and self.args.ign_ebind_all:\n            self.args.no_fastboot = True\n\n        self.up2k = Up2k(self)\n\n        self._feature_test()\n\n        decs = {k.strip(): 1 for k in self.args.th_dec.split(\",\")}\n        if not HAVE_VIPS:\n            decs.pop(\"vips\", None)\n        if not HAVE_PIL:\n            decs.pop(\"pil\", None)\n        if not HAVE_RAW:\n            decs.pop(\"raw\", None)\n        if not HAVE_FFMPEG or not HAVE_FFPROBE:\n            decs.pop(\"ff\", None)\n\n        # compressed formats; \"s3z=s3m.zip, s3gz=s3m.gz, ...\"\n        zlss = [x.strip().lower().split(\"=\", 1) for x in args.au_unpk.split(\",\")]\n        args.au_unpk = {x[0]: x[1] for x in zlss}\n\n        self.args.th_dec = list(decs.keys())\n        self.thumbsrv = None\n        want_ff = False\n        if not args.no_thumb:\n            t = \", \".join(self.args.th_dec) or \"(None available)\"\n            self.log(\"thumb\", \"decoder preference: {}\".format(t))\n\n            if self.args.th_dec:\n                self.thumbsrv = ThumbSrv(self)\n            else:\n                want_ff = True\n                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\"\n                msg = msg.format(\" \" * 37, os.path.basename(pybin))\n                if EXE:\n                    msg = \"copyparty.exe cannot use Pillow or pyvips; need ffprobe.exe and ffmpeg.exe to create thumbnails\"\n\n                self.log(\"thumb\", msg, c=3)\n\n        if not args.no_acode and args.no_thumb:\n            msg = \"setting --no-acode because --no-thumb (sorry)\"\n            self.log(\"thumb\", msg, c=6)\n            args.no_acode = True\n\n        if not args.no_acode and (not HAVE_FFMPEG or not HAVE_FFPROBE):\n            msg = \"setting --no-acode because either FFmpeg or FFprobe is not available\"\n            self.log(\"thumb\", msg, c=6)\n            args.no_acode = True\n            want_ff = True\n\n        if want_ff and ANYWIN:\n            self.log(\"thumb\", \"download FFmpeg to fix it:\\033[0m \" + FFMPEG_URL, 3)\n\n        if not args.no_acode:\n            if not re.match(\"^(0|[qv][0-9]|[0-9]{2,3}k)$\", args.q_mp3.lower()):\n                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]\"\n                raise Exception(t % (args.q_mp3,))\n        else:\n            zss = set(args.th_r_ffa.split(\",\") + args.th_r_ffv.split(\",\"))\n            args.au_unpk = {\n                k: v for k, v in args.au_unpk.items() if v.split(\".\")[0] not in zss\n            }\n\n        args.th_poke = min(args.th_poke, args.th_maxage, args.ac_maxage)\n\n        zms = \"\"\n        if not args.https_only:\n            zms += \"d\"\n        if not args.http_only:\n            zms += \"D\"\n\n        if args.sftp:\n            from .sftpd import Sftpd\n\n            self.sftpd: Optional[Sftpd] = None\n\n        if args.ftp or args.ftps:\n            from .ftpd import Ftpd\n\n            self.ftpd: Optional[Ftpd] = None\n            zms += \"f\" if args.ftp else \"F\"\n\n        if args.tftp:\n            from .tftpd import Tftpd\n\n            self.tftpd: Optional[Tftpd] = None\n\n        if args.sftp or args.ftp or args.ftps or args.tftp:\n            Daemon(self.start_ftpd, \"start_tftpd\")\n\n        if args.smb:\n            # impacket.dcerpc is noisy about listen timeouts\n            sto = socket.getdefaulttimeout()\n            socket.setdefaulttimeout(None)\n\n            from .smbd import SMB\n\n            self.smbd = SMB(self)\n            socket.setdefaulttimeout(sto)\n            self.smbd.start()\n            zms += \"s\"\n\n        if not args.zms:\n            args.zms = zms\n\n        self.zc_ngen = 0\n        self.mdns: Optional[\"MDNS\"] = None\n        self.ssdp: Optional[\"SSDPd\"] = None\n\n        # decide which worker impl to use\n        if self.check_mp_enable():\n            from .broker_mp import BrokerMp as Broker\n        else:\n            from .broker_thr import BrokerThr as Broker  # type: ignore\n\n        self.broker = Broker(self)\n\n        # create netmaps early to avoid firewall gaps,\n        # but the mutex blocks multiprocessing startup\n        for zs in \"ipu_nm ftp_ipa_nm tftp_ipa_nm\".split():\n            try:\n                getattr(args, zs).mutex = threading.Lock()\n            except:\n                pass\n        if args.ipr:\n            for nm in args.ipr_u.values():\n                nm.mutex = threading.Lock()\n\n    def _db_onfail_ses(self) -> None:\n        self.args.no_ses = True\n\n    def _db_onfail_idp(self) -> None:\n        self.args.idp_store = 0\n\n    def setup_db(self, which: str) -> None:\n        \"\"\"\n        the \"non-mission-critical\" databases; if something looks broken then just nuke it\n        \"\"\"\n        if which == \"ses\":\n            native_ver = VER_SESSION_DB\n            db_path = self.args.ses_db\n            desc = \"sessions-db\"\n            pathopt = \"ses-db\"\n            sanchk_q = \"select count(*) from us\"\n            createfun = self._create_session_db\n            failfun = self._db_onfail_ses\n        elif which == \"idp\":\n            native_ver = VER_IDP_DB\n            db_path = self.args.idp_db\n            desc = \"idp-db\"\n            pathopt = \"idp-db\"\n            sanchk_q = \"select count(*) from us\"\n            createfun = self._create_idp_db\n            failfun = self._db_onfail_idp\n        else:\n            raise Exception(\"unknown cachetype\")\n\n        if not db_path.endswith(\".db\"):\n            zs = \"config option --%s (the %s) was configured to [%s] which is invalid; must be a filepath ending with .db\"\n            self.log(\"root\", zs % (pathopt, desc, db_path), 1)\n            raise Exception(BAD_CFG)\n\n        if not HAVE_SQLITE3:\n            failfun()\n            if which == \"ses\":\n                zs = \"disabling sessions, will use plaintext passwords in cookies\"\n            elif which == \"idp\":\n                zs = \"disabling idp-db, will be unable to remember IdP-volumes after a restart\"\n            self.log(\"root\", \"WARNING: sqlite3 not available; %s\" % (zs,), 3)\n            return\n\n        assert sqlite3  # type: ignore  # !rm\n\n        db_lock = db_path + \".lock\"\n        try:\n            create = not os.path.getsize(db_path)\n        except:\n            create = True\n        zs = \"creating new\" if create else \"opening\"\n        self.log(\"root\", \"%s %s %s\" % (zs, desc, db_path))\n\n        for tries in range(2):\n            sver = 0\n            try:\n                db = sqlite3.connect(db_path)\n                cur = db.cursor()\n                try:\n                    zs = \"select v from kv where k='sver'\"\n                    sver = cur.execute(zs).fetchall()[0][0]\n                    if sver > native_ver:\n                        zs = \"this version of copyparty only understands %s v%d and older; the db is v%d\"\n                        raise Exception(zs % (desc, native_ver, sver))\n\n                    cur.execute(sanchk_q).fetchone()\n                except:\n                    if sver:\n                        raise\n                    sver = createfun(cur)\n\n                err = self._verify_db(\n                    cur, which, pathopt, db_path, desc, sver, native_ver\n                )\n                if err:\n                    tries = 99\n                    self.args.no_ses = True\n                    self.log(\"root\", err, 3)\n                break\n\n            except Exception as ex:\n                if tries or sver > native_ver:\n                    raise\n                t = \"%s is unusable; deleting and recreating: %r\"\n                self.log(\"root\", t % (desc, ex), 3)\n                try:\n                    cur.close()  # type: ignore\n                except:\n                    pass\n                try:\n                    db.close()  # type: ignore\n                except:\n                    pass\n                try:\n                    os.unlink(db_lock)\n                except:\n                    pass\n                os.unlink(db_path)\n\n    def _create_session_db(self, cur: \"sqlite3.Cursor\") -> int:\n        sch = [\n            r\"create table kv (k text, v int)\",\n            r\"create table us (un text, si text, t0 int)\",\n            # username, session-id, creation-time\n            r\"create index us_un on us(un)\",\n            r\"create index us_si on us(si)\",\n            r\"create index us_t0 on us(t0)\",\n            r\"insert into kv values ('sver', 1)\",\n        ]\n        for cmd in sch:\n            cur.execute(cmd)\n        self.log(\"root\", \"created new sessions-db\")\n        return 1\n\n    def _create_idp_db(self, cur: \"sqlite3.Cursor\") -> int:\n        sch = [\n            r\"create table kv (k text, v int)\",\n            r\"create table us (un text, gs text)\",\n            # username, groups\n            r\"create index us_un on us(un)\",\n            r\"insert into kv values ('sver', 1)\",\n        ]\n        for cmd in sch:\n            cur.execute(cmd)\n        self.log(\"root\", \"created new idp-db\")\n        return 1\n\n    def _verify_db(\n        self,\n        cur: \"sqlite3.Cursor\",\n        which: str,\n        pathopt: str,\n        db_path: str,\n        desc: str,\n        sver: int,\n        native_ver: int,\n    ) -> str:\n        # ensure writable (maybe owned by other user)\n        db = cur.connection\n\n        try:\n            zil = cur.execute(\"select v from kv where k='pid'\").fetchall()\n            if len(zil) > 1:\n                raise Exception()\n            owner = zil[0][0]\n        except:\n            owner = 0\n\n        if which == \"ses\":\n            cons = \"Will now disable sessions and instead use plaintext passwords in cookies.\"\n        elif which == \"idp\":\n            cons = \"Each IdP-volume will not become available until its associated user sends their first request.\"\n        else:\n            raise Exception()\n\n        if not lock_file(db_path + \".lock\"):\n            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\"\n            return t % (desc, db_path, owner, pathopt, cons)\n\n        vars = ((\"pid\", os.getpid()), (\"ts\", int(time.time() * 1000)))\n        if owner:\n            # wear-estimate: 2 cells; offsets 0x10, 0x50, 0x19720\n            for k, v in vars:\n                cur.execute(\"update kv set v=? where k=?\", (v, k))\n        else:\n            # wear-estimate: 3~4 cells; offsets 0x10, 0x50, 0x19180, 0x19710, 0x36000, 0x360b0, 0x36b90\n            for k, v in vars:\n                cur.execute(\"insert into kv values(?, ?)\", (k, v))\n\n        if sver < native_ver:\n            cur.execute(\"delete from kv where k='sver'\")\n            cur.execute(\"insert into kv values('sver',?)\", (native_ver,))\n\n        db.commit()\n        cur.close()\n        db.close()\n        return \"\"\n\n    def setup_share_db(self) -> None:\n        al = self.args\n        if not HAVE_SQLITE3:\n            self.log(\"root\", \"sqlite3 not available; disabling --shr\", 1)\n            al.shr = \"\"\n            return\n\n        assert sqlite3  # type: ignore  # !rm\n\n        al.shr = al.shr.strip(\"/\")\n        if \"/\" in al.shr or not al.shr:\n            t = \"config error: --shr must be the name of a virtual toplevel directory to put shares inside\"\n            self.log(\"root\", t, 1)\n            raise Exception(t)\n\n        al.shr = \"/%s/\" % (al.shr,)\n        al.shr1 = al.shr[1:]\n\n        # policy:\n        # the shares-db is important, so panic if something is wrong\n\n        db_path = self.args.shr_db\n        db_lock = db_path + \".lock\"\n        try:\n            create = not os.path.getsize(db_path)\n        except:\n            create = True\n        zs = \"creating new\" if create else \"opening\"\n        self.log(\"root\", \"%s shares-db %s\" % (zs, db_path))\n\n        sver = 0\n        try:\n            db = sqlite3.connect(db_path)\n            cur = db.cursor()\n            if not create:\n                zs = \"select v from kv where k='sver'\"\n                sver = cur.execute(zs).fetchall()[0][0]\n                if sver > VER_SHARES_DB:\n                    zs = \"this version of copyparty only understands shares-db v%d and older; the db is v%d\"\n                    raise Exception(zs % (VER_SHARES_DB, sver))\n\n                cur.execute(\"select count(*) from sh\").fetchone()\n        except Exception as ex:\n            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\"\n            self.log(\"root\", t % (db_path, ex, min_ex()), 1)\n            raise\n\n        try:\n            zil = cur.execute(\"select v from kv where k='pid'\").fetchall()\n            if len(zil) > 1:\n                raise Exception()\n            owner = zil[0][0]\n        except:\n            owner = 0\n\n        if not lock_file(db_lock):\n            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.\"\n            t = t % (db_path, owner)\n            self.log(\"root\", t, 1)\n            raise Exception(t)\n\n        sch1 = [\n            r\"create table kv (k text, v int)\",\n            r\"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)\",\n            # sharekey, password, src, perms, numFiles, owner, created, expires\n        ]\n        sch2 = [\n            r\"create table sf (k text, vp text)\",\n            r\"create index sf_k on sf(k)\",\n            r\"create index sh_k on sh(k)\",\n            r\"create index sh_t1 on sh(t1)\",\n            r\"insert into kv values ('sver', 2)\",\n        ]\n\n        assert db  # type: ignore  # !rm\n        assert cur  # type: ignore  # !rm\n        if not sver:\n            sver = VER_SHARES_DB\n            for cmd in sch1 + sch2:\n                cur.execute(cmd)\n            self.log(\"root\", \"created new shares-db\")\n\n        if sver == 1:\n            for cmd in sch2:\n                cur.execute(cmd)\n            cur.execute(\"update sh set st = 0\")\n            self.log(\"root\", \"shares-db schema upgrade ok\")\n\n        if sver < VER_SHARES_DB:\n            cur.execute(\"delete from kv where k='sver'\")\n            cur.execute(\"insert into kv values('sver',?)\", (VER_SHARES_DB,))\n\n        vars = ((\"pid\", os.getpid()), (\"ts\", int(time.time() * 1000)))\n        if owner:\n            # wear-estimate: same as sessions-db\n            for k, v in vars:\n                cur.execute(\"update kv set v=? where k=?\", (v, k))\n        else:\n            for k, v in vars:\n                cur.execute(\"insert into kv values(?, ?)\", (k, v))\n\n        db.commit()\n        cur.close()\n        db.close()\n\n    def start_ftpd(self) -> None:\n        time.sleep(30)\n\n        if hasattr(self, \"sftpd\") and not self.sftpd:\n            self.restart_sftpd()\n\n        if hasattr(self, \"ftpd\") and not self.ftpd:\n            self.restart_ftpd()\n\n        if hasattr(self, \"tftpd\") and not self.tftpd:\n            self.restart_tftpd()\n\n    def restart_sftpd(self) -> None:\n        if not hasattr(self, \"sftpd\"):\n            return\n\n        from .sftpd import Sftpd\n\n        if self.sftpd:\n            return  # todo\n\n        self.sftpd = Sftpd(self)\n        self.sftpd.run()\n        self.log(\"root\", \"started SFTPd\")\n\n    def restart_ftpd(self) -> None:\n        if not hasattr(self, \"ftpd\"):\n            return\n\n        from .ftpd import Ftpd\n\n        if self.ftpd:\n            return  # todo\n\n        if not os.path.exists(self.args.cert):\n            ensure_cert(self.log, self.args)\n\n        self.ftpd = Ftpd(self)\n        self.log(\"root\", \"started FTPd\")\n\n    def restart_tftpd(self) -> None:\n        if not hasattr(self, \"tftpd\"):\n            return\n\n        from .tftpd import Tftpd\n\n        if self.tftpd:\n            return  # todo\n\n        self.tftpd = Tftpd(self)\n\n    def thr_httpsrv_up(self) -> None:\n        time.sleep(1 if self.args.ign_ebind_all else 5)\n        expected = self.broker.num_workers * self.tcpsrv.nsrv\n        failed = expected - self.httpsrv_up\n        if not failed:\n            return\n\n        if self.args.ign_ebind_all:\n            if not self.tcpsrv.srv:\n                for _ in range(self.broker.num_workers):\n                    self.broker.say(\"cb_httpsrv_up\")\n            return\n\n        if self.args.ign_ebind and self.tcpsrv.srv:\n            return\n\n        t = \"{}/{} workers failed to start\"\n        t = t.format(failed, expected)\n        self.log(\"root\", t, 1)\n\n        self.retcode = 1\n        self.sigterm()\n\n    def sigterm(self) -> None:\n        self.signal_handler(signal.SIGTERM, None)\n\n    def sticky_qr(self) -> None:\n        self._sticky_qr()\n\n    def _unsticky_qr(self, flush=True) -> None:\n        print(\"\\033[s\\033[J\\033[r\\033[u\", file=sys.stderr, end=\"\")\n        if flush:\n            sys.stderr.flush()\n\n    def _sticky_qr(self, force: bool = False) -> None:\n        sz = termsize()\n        if self.qr_tsz == sz:\n            if not force:\n                return\n        else:\n            force = False\n\n        if self.qr_tsz:\n            self._unsticky_qr(False)\n        else:\n            atexit.register(self._unsticky_qr)\n\n        tw, th = self.qr_tsz = sz\n        zs1, qr = self.tcpsrv.qr.split(\"\\n\", 1)\n        url, colr = zs1.split(\" \", 1)\n        nl = len(qr.split(\"\\n\"))  # numlines\n        lp = 3 if nl * 2 + 4 < tw else 0  # leftpad\n        lp0 = lp\n        if self.args.qr_pin == 2:\n            url = \"\"\n        else:\n            while lp and (nl + lp) * 2 + len(url) + 1 > tw:\n                lp -= 1\n            if (nl + lp) * 2 + len(url) + 1 > tw:\n                qr = url + \"\\n\" + qr\n                url = \"\"\n                nl += 1\n                lp = lp0\n        sh = 1 + th - nl\n        if lp:\n            zs = \" \" * lp\n            qr = zs + qr.replace(\"\\n\", \"\\n\" + zs)\n        if url:\n            url = \"%s\\033[%d;%dH%s\\033[0m\" % (colr, sh + 1, (nl + lp) * 2, url)\n        qr = colr + qr\n\n        t = \"%s\\033[%dA\" % (\"\\n\" * nl, nl)\n        t = \"%s\\033[s\\033[1;%dr\\033[%dH%s%s\\033[u\" % (t, sh - 1, sh, qr, url)\n        if not force:\n            self.log(\"qr\", \"sticky-qrcode %sx%s,%s\" % (tw, th, sh), 6)\n        self.pr(t, file=sys.stderr, end=\"\")\n\n    def _qr_thr(self):\n        qr = self.tcpsrv.qr\n        w8 = self.args.qr_wait\n        if w8:\n            time.sleep(w8)\n            self.log(\"qr-code\", qr)\n        if self.args.qr_stdout:\n            self.pr(self.tcpsrv.qr)\n        if self.args.qr_stderr:\n            self.pr(self.tcpsrv.qr, file=sys.stderr)\n        w8 = self.args.qr_every\n        msg = \"%s\\033[%dA\" % (qr, len(qr.split(\"\\n\")))\n        while w8:\n            time.sleep(w8)\n            if self.stopping:\n                break\n            if self.args.qr_pin:\n                self._sticky_qr(True)\n            else:\n                self.log(\"qr-code\", msg)\n        w8 = self.args.qr_winch\n        while w8:\n            time.sleep(w8)\n            if self.stopping:\n                break\n            self._sticky_qr()\n\n    def cb_httpsrv_up(self) -> None:\n        self.httpsrv_up += 1\n        if self.httpsrv_up != self.broker.num_workers:\n            return\n\n        ar = self.args\n        for _ in range(10 if ar.sftp or ar.ftp or ar.ftps else 0):\n            time.sleep(0.03)\n            if self.ftpd if ar.ftp or ar.ftps else ar.sftp:\n                break\n\n        if self.tcpsrv.qr:\n            if self.args.qr_pin:\n                self.sticky_qr()\n            if self.args.qr_wait or self.args.qr_every or self.args.qr_winch:\n                Daemon(self._qr_thr, \"qr\")\n            else:\n                if not self.args.qr_pin:\n                    self.log(\"qr-code\", self.tcpsrv.qr)\n                if self.args.qr_stdout:\n                    self.pr(self.tcpsrv.qr)\n                if self.args.qr_stderr:\n                    self.pr(self.tcpsrv.qr, file=sys.stderr)\n        else:\n            self.log(\"root\", \"workers OK\\n\")\n\n        self.after_httpsrv_up()\n\n    def after_httpsrv_up(self) -> None:\n        self.up2k.init_vols()\n\n        Daemon(self.sd_notify, \"sd-notify\")\n\n    def _feature_test(self) -> None:\n        fok = []\n        fng = []\n        t_ff = \"transcode audio, create spectrograms, video thumbnails\"\n        to_check = [\n            (HAVE_SQLITE3, \"sqlite\", \"sessions and file/media indexing\"),\n            (HAVE_PIL, \"pillow\", \"image thumbnails (plenty fast)\"),\n            (HAVE_VIPS, \"vips\", \"image thumbnails (faster, eats more ram)\"),\n            (H_PIL_WEBP, \"pillow-webp\", \"create thumbnails as webp files\"),\n            (HAVE_FFMPEG, \"ffmpeg\", t_ff + \", good-but-slow image thumbnails\"),\n            (HAVE_FFPROBE, \"ffprobe\", t_ff + \", read audio/media tags\"),\n            (HAVE_MUTAGEN, \"mutagen\", \"read audio tags (ffprobe is better but slower)\"),\n            (HAVE_ARGON2, \"argon2\", \"secure password hashing (advanced users only)\"),\n            (HAVE_ZMQ, \"pyzmq\", \"send zeromq messages from event-hooks\"),\n            (H_PIL_HEIF, \"pillow-heif\", \"read .heif pics with pillow (rarely useful)\"),\n            (H_PIL_AVIF, \"pillow-avif\", \"read .avif pics with pillow (rarely useful)\"),\n            (HAVE_RAW, \"rawpy\", \"read RAW images\"),\n        ]\n        if ANYWIN:\n            to_check += [\n                (HAVE_PSUTIL, \"psutil\", \"improved plugin cleanup  (rarely useful)\")\n            ]\n\n        verbose = self.args.deps\n        if verbose:\n            self.log(\"dependencies\", \"\")\n\n        for have, feat, what in to_check:\n            lst = fok if have else fng\n            lst.append((feat, what))\n            if verbose:\n                zi = 2 if have else 5\n                sgot = \"found\" if have else \"missing\"\n                t = \"%7s: %s \\033[36m(%s)\"\n                self.log(\"dependencies\", t % (sgot, feat, what), zi)\n\n        if verbose:\n            self.log(\"dependencies\", \"\")\n            return\n\n        sok = \", \".join(x[0] for x in fok)\n        sng = \", \".join(x[0] for x in fng)\n\n        t = \"\"\n        if sok:\n            t += \"OK: \\033[32m\" + sok\n        if sng:\n            if t:\n                t += \", \"\n            t += \"\\033[0mNG: \\033[35m\" + sng\n\n        t += \"\\033[0m, see --deps (this is fine btw)\"\n        self.log(\"optional-dependencies\", t, 6)\n\n    def _check_env(self) -> None:\n        al = self.args\n\n        if self.args.no_bauth:\n            t = \"WARNING: --no-bauth disables support for the Android app; you may want to use --bauth-last instead\"\n            self.log(\"root\", t, 3)\n            if self.args.bauth_last:\n                self.log(\"root\", \"WARNING: ignoring --bauth-last due to --no-bauth\", 3)\n\n        have_tcp = False\n        for zs in al.i:\n            if not zs.startswith((\"unix:\", \"fd:\")):\n                have_tcp = True\n        if not have_tcp:\n            zb = False\n            zs = \"z zm zm4 zm6 zmv zmvv zs zsv zv\"\n            for zs in zs.split():\n                if getattr(al, zs, False):\n                    setattr(al, zs, False)\n                    zb = True\n            if zb:\n                t = \"not listening on any ip-addresses (only unix-sockets and/or FDs); cannot enable zeroconf/mdns/ssdp as requested\"\n                self.log(\"root\", t, 3)\n\n        if not self.args.no_dav:\n            from .dxml import DXML_OK\n\n            if not DXML_OK:\n                if not self.args.no_dav:\n                    self.args.no_dav = True\n                    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\"\n                    self.log(\"root\", t % (URL_BUG, VERSIONS, expat_ver()), 1)\n\n        if (\n            not E.scfg\n            and not al.unsafe_state\n            and not os.environ.get(\"PRTY_UNSAFE_STATE\")\n        ):\n            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.\"\n            if not al.no_ses:\n                al.no_ses = True\n                t2 = \"A consequence of this misconfiguration is that passwords will now be sent in the HTTP-header of every request!\"\n                self.log(\"root\", \"WARNING:\\nWill disable sessions %s %s\" % (t, t2), 1)\n            if al.idp_store == 1:\n                al.idp_store = 0\n                self.log(\"root\", \"WARNING:\\nDisabling --idp-store %s\" % (t,), 3)\n            if al.idp_store:\n                t2 = \"ERROR: Cannot enable --idp-store %s\" % (t,)\n                self.log(\"root\", t2, 1)\n                raise Exception(t2)\n            if al.shr:\n                t2 = \"ERROR: Cannot enable shares %s\" % (t,)\n                self.log(\"root\", t2, 1)\n                raise Exception(t2)\n\n    def _process_config(self) -> bool:\n        al = self.args\n\n        al.zm_on = al.zm_on or al.z_on\n        al.zs_on = al.zs_on or al.z_on\n        al.zm_off = al.zm_off or al.z_off\n        al.zs_off = al.zs_off or al.z_off\n        ns = \"zm_on zm_off zs_on zs_off acao acam\"\n        for n in ns.split(\" \"):\n            vs = getattr(al, n).split(\",\")\n            vs = [x.strip() for x in vs]\n            vs = [x for x in vs if x]\n            setattr(al, n, vs)\n\n        ns = \"acao acam\"\n        for n in ns.split(\" \"):\n            vs = getattr(al, n)\n            vd = {zs: 1 for zs in vs}\n            setattr(al, n, vd)\n\n        ns = \"acao\"\n        for n in ns.split(\" \"):\n            vs = getattr(al, n)\n            vs = [x.lower() for x in vs]\n            setattr(al, n, vs)\n\n        ns = \"ihead ohead\"\n        for n in ns.split(\" \"):\n            vs = getattr(al, n) or []\n            vs = [x.lower() for x in vs]\n            setattr(al, n, vs)\n\n        R = al.rp_loc\n        if \"//\" in R or \":\" in R:\n            t = \"found URL in --rp-loc; it should be just the location, for example /foo/bar\"\n            raise Exception(t)\n\n        al.R = R = R.strip(\"/\")\n        al.SR = \"/\" + R if R else \"\"\n        al.RS = R + \"/\" if R else \"\"\n        al.SRS = \"/\" + R + \"/\" if R else \"/\"\n\n        if al.rsp_jtr:\n            al.rsp_slp = 0.000001\n\n        zsl = al.th_covers.split(\",\")\n        zsl = [x.strip() for x in zsl]\n        zsl = [x for x in zsl if x]\n        al.th_covers = zsl\n        al.th_coversd = zsl + [\".\" + x for x in zsl]\n        al.th_covers_set = set(al.th_covers)\n        al.th_coversd_set = set(al.th_coversd)\n\n        for k in \"c\".split(\" \"):\n            vl = getattr(al, k)\n            if not vl:\n                continue\n\n            vl = [os.path.expandvars(os.path.expanduser(x)) for x in vl]\n            setattr(al, k, vl)\n\n        for k in \"lo hist dbpath ssl_log\".split(\" \"):\n            vs = getattr(al, k)\n            if vs:\n                vs = os.path.expandvars(os.path.expanduser(vs))\n                setattr(al, k, vs)\n\n        for k in \"idp_adm stats_u\".split(\" \"):\n            vs = getattr(al, k)\n            vsa = [x.strip() for x in vs.split(\",\")]\n            vsa = [x.lower() for x in vsa if x]\n            setattr(al, k + \"_set\", set(vsa))\n\n        for k in \"smsg\".split(\" \"):\n            vs = getattr(al, k)\n            vsa = [x.strip() for x in vs.split(\",\")]\n            vsa = [x.upper() for x in vsa if x]\n            setattr(al, k + \"_set\", set(vsa))\n\n        zs = \"dav_ua1 sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip\"\n        for k in zs.split(\" \"):\n            vs = getattr(al, k)\n            if not vs or vs == \"no\":\n                setattr(al, k, None)\n            else:\n                setattr(al, k, re.compile(vs))\n\n        for k in \"tftp_lsf\".split(\" \"):\n            vs = getattr(al, k)\n            if not vs or vs == \"no\":\n                setattr(al, k, None)\n            else:\n                setattr(al, k, re.compile(\"^\" + vs + \"$\"))\n\n        if al.banmsg.startswith(\"@\"):\n            with open(al.banmsg[1:], \"rb\") as f:\n                al.banmsg_b = f.read()\n        else:\n            al.banmsg_b = al.banmsg.encode(\"utf-8\") + b\"\\n\"\n\n        if not al.sus_urls:\n            al.ban_url = \"no\"\n        elif al.ban_url == \"no\":\n            al.sus_urls = None\n\n        zs = \"fika idp_h_grp idp_h_key pw_hdr pw_urlp xf_host xf_proto xf_proto_fb xff_hdr\"\n        for k in zs.split(\" \"):\n            setattr(al, k, str(getattr(al, k)).lower().strip())\n\n        al.idp_h_usr = [x.lower() for x in al.idp_h_usr or []]\n\n        al.idp_hm_usr_p = {}\n        for zs0 in al.idp_hm_usr or []:\n            try:\n                sep = zs0[:1]\n                hn, zs1, zs2 = zs0[1:].split(sep)\n                hn = hn.lower()\n                if hn in al.idp_hm_usr_p:\n                    al.idp_hm_usr_p[hn][zs1] = zs2\n                else:\n                    al.idp_hm_usr_p[hn] = {zs1: zs2}\n            except:\n                raise Exception(\"invalid --idp-hm-usr [%s]\" % (zs0,))\n\n        zs1 = \"\"\n        zs2 = \"\"\n        zs = al.idp_chsub\n        while zs:\n            if zs[:1] != \"|\":\n                raise Exception(\"invalid --idp-chsub; expected another | but got \" + zs)\n            zs1 += zs[1:2]\n            zs2 += zs[2:3]\n            zs = zs[3:]\n        al.idp_chsub_tr = umktrans(zs1, zs2)\n\n        al.sftp_ipa_nm = build_netmap(al.sftp_ipa or al.ipa or al.ipar, True)\n        al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa or al.ipar, True)\n        al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa or al.ipar, True)\n\n        al.sftp_key2u = {\n            \"%s %s\" % (x[1], x[2]): x[0]\n            for x in [x.split(\" \") for x in al.sftp_key or []]\n        }\n\n        mte = ODict.fromkeys(DEF_MTE.split(\",\"), True)\n        al.mte = odfusion(mte, al.mte)\n\n        mth = ODict.fromkeys(DEF_MTH.split(\",\"), True)\n        al.mth = odfusion(mth, al.mth)\n\n        exp = ODict.fromkeys(DEF_EXP.split(\" \"), True)\n        al.exp_md = odfusion(exp, al.exp_md.replace(\" \", \",\"))\n        al.exp_lg = odfusion(exp, al.exp_lg.replace(\" \", \",\"))\n\n        for k in [\"no_hash\", \"no_idx\", \"og_ua\", \"srch_excl\"]:\n            ptn = getattr(self.args, k)\n            if ptn:\n                setattr(self.args, k, re.compile(ptn))\n\n        for k in [\"idp_gsep\"]:\n            ptn = getattr(self.args, k)\n            if \"]\" in ptn:\n                ptn = \"]\" + ptn.replace(\"]\", \"\")\n            if \"[\" in ptn:\n                ptn = ptn.replace(\"[\", \"\") + \"[\"\n            if \"-\" in ptn:\n                ptn = ptn.replace(\"-\", \"\") + \"-\"\n\n            ptn = ptn.replace(\"\\\\\", \"\\\\\\\\\").replace(\"^\", \"\\\\^\")\n            setattr(self.args, k, re.compile(\"[%s]\" % (ptn,)))\n\n        try:\n            zf1, zf2 = self.args.rm_retry.split(\"/\")\n            self.args.rm_re_t = float(zf1)\n            self.args.rm_re_r = float(zf2)\n        except:\n            raise Exception(\"invalid --rm-retry [%s]\" % (self.args.rm_retry,))\n\n        try:\n            zf1, zf2 = self.args.mv_retry.split(\"/\")\n            self.args.mv_re_t = float(zf1)\n            self.args.mv_re_r = float(zf2)\n        except:\n            raise Exception(\"invalid --mv-retry [%s]\" % (self.args.mv_retry,))\n\n        if self.args.vc_url:\n            zi = max(1, int(self.args.vc_age))\n            if zi < 3 and \"api.copyparty.eu\" in self.args.vc_url:\n                zi = 3\n                self.log(\"root\", \"vc-age too low for copyparty.eu; will use 3 hours\")\n            self.args.vc_age = zi\n\n        al.js_utc = \"false\" if al.localtime else \"true\"\n\n        al.tcolor = al.tcolor.lstrip(\"#\")\n        if len(al.tcolor) == 3:  # fc5 => ffcc55\n            al.tcolor = \"\".join([x * 2 for x in al.tcolor])\n\n        if self.args.name_url:\n            zs = html_escape(self.args.name_url, True, True)\n            zs = '<a href=\"%s\">%s</a>' % (zs, self.args.name)\n        else:\n            zs = self.args.name\n        self.args.name_html = zs\n\n        zs = al.u2sz\n        zsl = [x.strip() for x in zs.split(\",\")]\n        if len(zsl) not in (1, 3):\n            t = \"invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)\"\n            raise Exception(t)\n        if len(zsl) < 3:\n            zsl = [\"1\", zs, zs]\n        zi2 = 1\n        for zs in zsl:\n            zi = int(zs)\n            # arbitrary constraint (anything above 2 GiB is probably unintended)\n            if zi < 1 or zi > 2047:\n                raise Exception(\"invalid --u2sz; minimum is 1, max is 2047\")\n            if zi < zi2:\n                raise Exception(\"invalid --u2sz; values must be equal or ascending\")\n            zi2 = zi\n        al.u2sz = \",\".join(zsl)\n\n        derive_args(al)\n        return True\n\n    def _ipa2re(self, txt) -> Optional[re.Pattern]:\n        if txt in (\"any\", \"0\", \"\"):\n            return None\n\n        zs = txt.replace(\" \", \"\").replace(\".\", \"\\\\.\").replace(\",\", \"|\")\n        return re.compile(\"^(?:\" + zs + \")\")\n\n    def _setlimits(self) -> None:\n        try:\n            import resource\n\n            soft, hard = [\n                int(x) if x > 0 else 1024 * 1024\n                for x in list(resource.getrlimit(resource.RLIMIT_NOFILE))\n            ]\n        except:\n            self.log(\"root\", \"failed to read rlimits from os\", 6)\n            return\n\n        if not soft or not hard:\n            t = \"got bogus rlimits from os ({}, {})\"\n            self.log(\"root\", t.format(soft, hard), 6)\n            return\n\n        want = self.args.nc * 4\n        new_soft = min(hard, want)\n        if new_soft < soft:\n            return\n\n        # t = \"requesting rlimit_nofile({}), have {}\"\n        # self.log(\"root\", t.format(new_soft, soft), 6)\n\n        try:\n            import resource\n\n            resource.setrlimit(resource.RLIMIT_NOFILE, (new_soft, hard))\n            soft = new_soft\n        except:\n            t = \"rlimit denied; max open files: {}\"\n            self.log(\"root\", t.format(soft), 3)\n            return\n\n        if soft < want:\n            t = \"max open files: {} (wanted {} for -nc {})\"\n            self.log(\"root\", t.format(soft, want, self.args.nc), 3)\n            self.args.nc = min(self.args.nc, soft // 2)\n\n    def _logname(self) -> str:\n        dt = datetime.now(self.tz)\n        fn = str(self.args.lo)\n        for fs in \"YmdHMS\":\n            fs = \"%\" + fs\n            if fs in fn:\n                fn = fn.replace(fs, dt.strftime(fs))\n\n        return fn\n\n    def _setup_logfile(self, printed: str) -> None:\n        base_fn = fn = sel_fn = self._logname()\n        do_xz = fn.lower().endswith(\".xz\")\n        if fn != self.args.lo:\n            ctr = 0\n            # yup this is a race; if started sufficiently concurrently, two\n            # copyparties can grab the same logfile (considered and ignored)\n            while os.path.exists(sel_fn):\n                ctr += 1\n                sel_fn = \"{}.{}\".format(fn, ctr)\n\n        fn = sel_fn\n        try:\n            bos.makedirs(os.path.dirname(fn))\n        except:\n            pass\n\n        try:\n            if do_xz:\n                import lzma\n\n                lh = lzma.open(fn, \"wt\", encoding=\"utf-8\", errors=\"replace\", preset=0)\n                self.args.no_logflush = True\n            else:\n                lh = open(fn, \"wt\", encoding=\"utf-8\", errors=\"replace\")\n        except:\n            import codecs\n\n            lh = codecs.open(fn, \"w\", encoding=\"utf-8\", errors=\"replace\")\n\n        if getattr(self.args, \"free_umask\", False):\n            os.fchmod(lh.fileno(), 0o644)\n\n        argv = [pybin] + self.argv\n        if hasattr(shlex, \"quote\"):\n            argv = [shlex.quote(x) for x in argv]\n        else:\n            argv = ['\"{}\"'.format(x) for x in argv]\n\n        msg = \"[+] opened logfile [{}]\\n\".format(fn)\n        printed += msg\n        t = \"t0: {:.3f}\\nargv: {}\\n\\n{}\"\n        lh.write(t.format(self.E.t0, \" \".join(argv), printed))\n        self.logf = lh\n        self.logf_base_fn = base_fn\n        print(msg, end=\"\")\n\n    def run(self) -> None:\n        self.tcpsrv.run()\n        if getattr(self.args, \"z_chk\", 0) and (\n            getattr(self.args, \"zm\", False) or getattr(self.args, \"zs\", False)\n        ):\n            Daemon(self.tcpsrv.netmon, \"netmon\")\n\n        Daemon(self.thr_httpsrv_up, \"sig-hsrv-up2\")\n        if self.args.vc_url:\n            Daemon(self.check_ver, \"ver-chk\")\n\n        sigs = [signal.SIGINT, signal.SIGTERM]\n        if not ANYWIN:\n            sigs.append(signal.SIGUSR1)\n\n        for sig in sigs:\n            signal.signal(sig, self.signal_handler)\n\n        # macos hangs after shutdown on sigterm with while-sleep,\n        # windows cannot ^c stop_cond (and win10 does the macos thing but winxp is fine??)\n        # linux is fine with both,\n        # never lucky\n        if ANYWIN:\n            # msys-python probably fine but >msys-python\n            Daemon(self.stop_thr, \"svchub-sig\")\n\n            try:\n                while not self.stop_req:\n                    time.sleep(1)\n            except:\n                pass\n\n            self.shutdown()\n            # cant join; eats signals on win10\n            while not self.stopped:\n                time.sleep(0.1)\n        else:\n            self.stop_thr()\n\n    def start_zeroconf(self) -> None:\n        self.zc_ngen += 1\n\n        if getattr(self.args, \"zm\", False):\n            try:\n                from .mdns import MDNS\n\n                if self.mdns:\n                    self.mdns.stop(True)\n\n                self.mdns = MDNS(self, self.zc_ngen)\n                Daemon(self.mdns.run, \"mdns\")\n            except:\n                self.log(\"root\", \"mdns startup failed;\\n\" + min_ex(), 3)\n\n        if getattr(self.args, \"zs\", False):\n            try:\n                from .ssdp import SSDPd\n\n                if self.ssdp:\n                    self.ssdp.stop()\n\n                self.ssdp = SSDPd(self, self.zc_ngen)\n                Daemon(self.ssdp.run, \"ssdp\")\n            except:\n                self.log(\"root\", \"ssdp startup failed;\\n\" + min_ex(), 3)\n\n    def reload(self, rescan_all_vols: bool, up2k: bool) -> str:\n        t = \"users, volumes, and volflags have been reloaded\"\n        with self.reload_mutex:\n            self.log(\"root\", \"reloading config\")\n            self.asrv.reload(9 if up2k else 4)\n            ramdisk_chk(self.asrv)\n            if up2k:\n                self.up2k.reload(rescan_all_vols)\n                t += \"; volumes are now reinitializing\"\n            else:\n                self.log(\"root\", \"reload done\")\n            t += \"\\n\\nchanges to global options (if any) require a restart of copyparty to take effect\"\n            self.broker.reload()\n        return t\n\n    def _reload_sessions(self) -> None:\n        with self.asrv.mutex:\n            self.asrv.load_sessions(True)\n        self.broker.reload_sessions()\n\n    def stop_thr(self) -> None:\n        while not self.stop_req:\n            with self.stop_cond:\n                self.stop_cond.wait(9001)\n\n            if self.reload_req:\n                self.reload_req = False\n                self.reload(True, True)\n\n        self.shutdown()\n\n    def kill9(self, delay: float = 0.0) -> None:\n        if delay > 0.01:\n            time.sleep(delay)\n            print(\"component stuck; issuing sigkill\")\n            time.sleep(0.1)\n\n        if ANYWIN:\n            os.system(\"taskkill /f /pid {}\".format(os.getpid()))\n        else:\n            os.kill(os.getpid(), signal.SIGKILL)\n\n    def signal_handler(self, sig: int, frame: Optional[FrameType]) -> None:\n        if self.stopping:\n            if self.nsigs <= 0:\n                try:\n                    threading.Thread(target=self.pr, args=(\"OMBO BREAKER\",)).start()\n                    time.sleep(0.1)\n                except:\n                    pass\n\n                self.kill9()\n            else:\n                self.nsigs -= 1\n                return\n\n        if not ANYWIN and sig == signal.SIGUSR1:\n            self.reload_req = True\n        else:\n            self.stop_req = True\n\n        with self.stop_cond:\n            self.stop_cond.notify_all()\n\n    def shutdown(self) -> None:\n        if self.stopping:\n            return\n\n        # start_log_thrs(print, 0.1, 1)\n\n        self.stopping = True\n        self.stop_req = True\n        with self.stop_cond:\n            self.stop_cond.notify_all()\n\n        ret = 1\n        try:\n            self.pr(\"OPYTHAT\")\n            tasks = []\n            slp = 0.0\n\n            if self.mdns:\n                tasks.append(Daemon(self.mdns.stop, \"mdns\"))\n                slp = time.time() + 0.5\n\n            if self.ssdp:\n                tasks.append(Daemon(self.ssdp.stop, \"ssdp\"))\n                slp = time.time() + 0.5\n\n            self.broker.shutdown()\n            self.tcpsrv.shutdown()\n            self.up2k.shutdown()\n\n            if hasattr(self, \"smbd\"):\n                slp = max(slp, time.time() + 0.5)\n                tasks.append(Daemon(self.smbd.stop, \"smbd\"))\n\n            if self.thumbsrv:\n                self.thumbsrv.shutdown()\n\n                for n in range(200):  # 10s\n                    time.sleep(0.05)\n                    if self.thumbsrv.stopped():\n                        break\n\n                    if n == 3:\n                        self.log(\"root\", \"waiting for thumbsrv (10sec)...\")\n\n            if hasattr(self, \"smbd\"):\n                zf = max(time.time() - slp, 0)\n                Daemon(self.kill9, a=(zf + 0.5,))\n\n            while time.time() < slp:\n                if not next((x for x in tasks if x.is_alive), None):\n                    break\n\n                time.sleep(0.05)\n\n            self.log(\"root\", \"nailed it\")\n            ret = self.retcode\n        except:\n            self.pr(\"\\033[31m[ error during shutdown ]\\n{}\\033[0m\".format(min_ex()))\n            raise\n        finally:\n            if self.args.wintitle:\n                print(\"\\033]0;\\033\\\\\", file=sys.stderr, end=\"\")\n                sys.stderr.flush()\n\n            self.pr(\"\\033[0m\", end=\"\")\n            if self.logf:\n                self.logf.close()\n\n            self.stopped = True\n            sys.exit(ret)\n\n    def _log_disabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None:\n        if not self.logf:\n            return\n\n        with self.log_mutex:\n            dt = datetime.now(self.tz)\n            ts = self.log_dfmt % (\n                dt.year,\n                dt.month * 100 + dt.day,\n                (dt.hour * 100 + dt.minute) * 100 + dt.second,\n                dt.microsecond // self.log_div,\n            )\n\n            if self.flo == 1:\n                fmt = \"@%s [%-21s] %s\\n\"\n                if not c:\n                    if \"\\033\" in msg:\n                        msg += \"\\033[0m\"\n                elif isinstance(c, int):\n                    msg = \"\\033[3%sm%s\\033[0m\" % (c, msg)\n                elif \"\\033\" not in c:\n                    msg = \"\\033[%sm%s\\033[0m\" % (c, msg)\n                else:\n                    msg = \"%s%s\\033[0m\" % (c, msg)\n\n                if \"\\033\" in src:\n                    src += \"\\033[0m\"\n            else:\n                if not c:\n                    fmt = \"@%s  LOG [%-21s] %s\\n\"\n                elif c == 1:\n                    fmt = \"@%s CRIT [%-21s] %s\\n\"\n                elif c == 3:\n                    fmt = \"@%s WARN [%-21s] %s\\n\"\n                elif c == 6:\n                    fmt = \"@%s  BTW [%-21s] %s\\n\"\n                else:\n                    fmt = \"@%s  LOG [%-21s] %s\\n\"\n\n                if \"\\033\" in src:\n                    src = RE_ANSI.sub(\"\", src)\n                if \"\\033\" in msg:\n                    msg = RE_ANSI.sub(\"\", msg)\n\n            self.logf.write(fmt % (ts, src, msg))\n            if not self.args.no_logflush:\n                self.logf.flush()\n\n            if dt.day != self.cday or dt.month != self.cmon:\n                self._set_next_day(dt)\n\n    def _set_next_day(self, dt: datetime) -> None:\n        if self.cday and self.logf and self.logf_base_fn != self._logname():\n            self.logf.close()\n            self._setup_logfile(\"\")\n\n        self.cday = dt.day\n        self.cmon = dt.month\n\n    def _log_enabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None:\n        \"\"\"handles logging from all components\"\"\"\n        with self.log_mutex:\n            dt = datetime.now(self.tz)\n            if dt.day != self.cday or dt.month != self.cmon:\n                if self.args.log_date:\n                    zs = dt.strftime(self.args.log_date)\n                    self.log_efmt = \"%s %s\" % (zs, self.log_efmt.split(\" \")[-1])\n                zs = \"{}\\n\" if self.no_ansi else \"\\033[36m{}\\033[0m\\n\"\n                zs = zs.format(dt.strftime(\"%Y-%m-%d\"))\n                print(zs, end=\"\")\n                self._set_next_day(dt)\n                if self.logf:\n                    self.logf.write(zs)\n\n            if self.no_ansi:\n                if not c:\n                    fmt = \"%s %-21s  LOG: %s\\n\"\n                elif c == 1:\n                    fmt = \"%s %-21s CRIT: %s\\n\"\n                elif c == 3:\n                    fmt = \"%s %-21s WARN: %s\\n\"\n                elif c == 6:\n                    fmt = \"%s %-21s  BTW: %s\\n\"\n                else:\n                    fmt = \"%s %-21s  LOG: %s\\n\"\n\n                if \"\\033\" in msg:\n                    msg = RE_ANSI.sub(\"\", msg)\n                if \"\\033\" in src:\n                    src = RE_ANSI.sub(\"\", src)\n            else:\n                fmt = \"\\033[36m%s \\033[33m%-21s \\033[0m%s\\n\"\n                if not c:\n                    pass\n                elif isinstance(c, int):\n                    msg = \"\\033[3%sm%s\\033[0m\" % (c, msg)\n                elif \"\\033\" not in c:\n                    msg = \"\\033[%sm%s\\033[0m\" % (c, msg)\n                else:\n                    msg = \"%s%s\\033[0m\" % (c, msg)\n\n            ts = self.log_efmt % (\n                dt.hour,\n                dt.minute,\n                dt.second,\n                dt.microsecond // self.log_div,\n            )\n            msg = fmt % (ts, src, msg)\n            try:\n                print(msg, end=\"\")\n            except UnicodeEncodeError:\n                try:\n                    print(msg.encode(\"utf-8\", \"replace\").decode(), end=\"\")\n                except:\n                    print(msg.encode(\"ascii\", \"replace\").decode(), end=\"\")\n            except OSError as ex:\n                if ex.errno != errno.EPIPE:\n                    raise\n\n            if self.logf:\n                self.logf.write(msg)\n                if not self.args.no_logflush:\n                    self.logf.flush()\n\n    def _log_en_f2(self, src: str, msg: str, c: Union[int, str] = 0) -> None:\n        with self.log_mutex:\n            dt = datetime.now(self.tz)\n            if dt.day != self.cday or dt.month != self.cmon:\n                if self.args.log_date:\n                    zs = dt.strftime(self.args.log_date)\n                    self.log_efmt = \"%s %s\" % (zs, self.log_efmt.split(\" \")[-1])\n                zs = \"{}\\n\" if self.no_ansi else \"\\033[36m{}\\033[0m\\n\"\n                zs = zs.format(dt.strftime(\"%Y-%m-%d\"))\n                print(zs, end=\"\")\n                self._set_next_day(dt)\n                if self.logf:\n                    self.logf.write(zs)\n\n            ts = self.log_efmt % (\n                dt.hour,\n                dt.minute,\n                dt.second,\n                dt.microsecond // self.log_div,\n            )\n\n            # logfile:\n            if not c:\n                fmt = \"%s %-21s  LOG: %s\\n\"\n            elif c == 1:\n                fmt = \"%s %-21s CRIT: %s\\n\"\n            elif c == 3:\n                fmt = \"%s %-21s WARN: %s\\n\"\n            elif c == 6:\n                fmt = \"%s %-21s  BTW: %s\\n\"\n            else:\n                fmt = \"%s %-21s  LOG: %s\\n\"\n            fsrc = RE_ANSI.sub(\"\", src) if \"\\033\" in src else src\n            fmsg = RE_ANSI.sub(\"\", msg) if \"\\033\" in msg else msg\n            fmsg = fmt % (ts, fsrc, fmsg)\n\n            # stdout ansi:\n            fmt = \"\\033[36m%s \\033[33m%-21s \\033[0m%s\\n\"\n            if not c:\n                pass\n            elif isinstance(c, int):\n                msg = \"\\033[3%sm%s\\033[0m\" % (c, msg)\n            elif \"\\033\" not in c:\n                msg = \"\\033[%sm%s\\033[0m\" % (c, msg)\n            else:\n                msg = \"%s%s\\033[0m\" % (c, msg)\n\n            msg = fmt % (ts, src, msg)\n            try:\n                print(msg, end=\"\")\n            except UnicodeEncodeError:\n                try:\n                    print(msg.encode(\"utf-8\", \"replace\").decode(), end=\"\")\n                except:\n                    print(msg.encode(\"ascii\", \"replace\").decode(), end=\"\")\n            except OSError as ex:\n                if ex.errno != errno.EPIPE:\n                    raise\n\n            self.logf.write(fmsg)\n            if not self.args.no_logflush:\n                self.logf.flush()\n\n    def pr(self, *a: Any, **ka: Any) -> None:\n        try:\n            with self.log_mutex:\n                print(*a, **ka)\n        except OSError as ex:\n            if ex.errno != errno.EPIPE:\n                raise\n\n    def check_mp_support(self) -> str:\n        if MACOS and not os.environ.get(\"PRTY_FORCE_MP\"):\n            return \"multiprocessing is wonky on mac osx;\"\n        elif sys.version_info < (3, 3):\n            return \"need python 3.3 or newer for multiprocessing;\"\n\n        try:\n            x: mp.Queue[tuple[str, str]] = mp.Queue(1)\n            x.put((\"foo\", \"bar\"))\n            if x.get()[0] != \"foo\":\n                raise Exception()\n        except:\n            return \"multiprocessing is not supported on your platform;\"\n\n        return \"\"\n\n    def check_mp_enable(self) -> bool:\n        if self.args.j == 1:\n            return False\n\n        try:\n            if mp.cpu_count() <= 1 and not os.environ.get(\"PRTY_FORCE_MP\"):\n                raise Exception()\n        except:\n            self.log(\"svchub\", \"only one CPU detected; multiprocessing disabled\")\n            return False\n\n        try:\n            # support vscode debugger (bonus: same behavior as on windows)\n            mp.set_start_method(\"spawn\", True)\n        except AttributeError:\n            # py2.7 probably, anyways dontcare\n            pass\n\n        err = self.check_mp_support()\n        if not err:\n            return True\n        else:\n            self.log(\"svchub\", err)\n            self.log(\"svchub\", \"cannot efficiently use multiple CPU cores\")\n            return False\n\n    def sd_notify(self) -> None:\n        try:\n            zb = os.environ.get(\"NOTIFY_SOCKET\")\n            if not zb:\n                return\n\n            addr = unicode(zb)\n            if addr.startswith(\"@\"):\n                addr = \"\\0\" + addr[1:]\n\n            t = \"\".join(x for x in addr if x in string.printable)\n            self.log(\"sd_notify\", t)\n\n            sck = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)\n            sck.connect(addr)\n            sck.sendall(b\"READY=1\")\n        except:\n            self.log(\"sd_notify\", min_ex())\n\n    def log_stacks(self) -> None:\n        td = time.time() - self.tstack\n        if td < 300:\n            self.log(\"stacks\", \"cooldown {}\".format(td))\n            return\n\n        self.tstack = time.time()\n        zs = \"{}\\n{}\".format(VERSIONS, alltrace())\n        zb = zs.encode(\"utf-8\", \"replace\")\n        zb = gzip.compress(zb)\n        zs = ub64enc(zb).decode(\"ascii\")\n        self.log(\"stacks\", zs)\n\n    def check_ver(self) -> None:\n        next_chk = 0\n        # self.args.vc_age = 2 / 60\n        fpath = os.path.join(self.E.cfg, \"vuln_advisory.json\")\n        while not self.stopping:\n            now = time.time()\n            if now < next_chk:\n                time.sleep(min(999, next_chk - now))\n                continue\n\n            age = 0\n            jtxt = \"\"\n            src = \"[cache] \"\n            try:\n                mtime = os.path.getmtime(fpath)\n                age = time.time() - mtime\n                if age < self.args.vc_age * 3600 - 69:\n                    zs, jtxt = read_utf8(None, fpath, True).split(\"\\n\", 1)\n                    if zs != self.args.vc_url:\n                        jtxt = \"\"\n            except Exception as e:\n                t = \"will download advisory because cache-file %r could not be read: %s\"\n                self.log(\"ver-chk\", t % (fpath, e), 6)\n\n            if not jtxt:\n                src = \"\"\n                age = 0\n                try:\n                    req = Request(self.args.vc_url)\n                    with urlopen(req, timeout=30) as f:\n                        jtxt = f.read().decode(\"utf-8\")\n                    try:\n                        with open(fpath, \"wb\") as f:\n                            zs = self.args.vc_url + \"\\n\" + jtxt\n                            f.write(zs.encode(\"utf-8\"))\n                    except Exception as e:\n                        t = \"failed to write advisory to cache; %s\"\n                        self.log(\"ver-chk\", t % (e,), 3)\n                except Exception as e:\n                    t = \"failed to fetch vulnerability advisory; %s\"\n                    self.log(\"ver-chk\", t % (e,), 1)\n\n            next_chk = time.time() + 699\n            if not jtxt:\n                continue\n\n            try:\n                advisories = json.loads(jtxt)\n                for adv in advisories:\n                    if adv.get(\"state\") == \"closed\":\n                        continue\n                    vuln = {}\n                    for x in adv[\"vulnerabilities\"]:\n                        if x[\"package\"][\"name\"].lower() == \"copyparty\":\n                            vuln = x\n                            break\n                    if not vuln:\n                        continue\n                    sver = vuln[\"patched_versions\"].strip(\".v\")\n                    tver = tuple([int(x) for x in sver.split(\".\")])\n                    if VERSION < tver:\n                        zs = json.dumps(adv, indent=2)\n                        t = \"your version (%s) has a vulnerability! please upgrade:\\n%s\"\n                        self.log(\"ver-chk\", t % (S_VERSION, zs), 1)\n                        self.broker.say(\"httpsrv.set_bad_ver\")\n                        if self.args.vc_exit:\n                            self.sigterm()\n                        return\n                    else:\n                        t = \"%sok; v%s and newer is safe\"\n                        self.log(\"ver-chk\", t % (src, sver), 2)\n                next_chk = time.time() + self.args.vc_age * 3600 - age\n            except Exception as e:\n                t = \"failed to process vulnerability advisory; %s\"\n                self.log(\"ver-chk\", t % (min_ex()), 1)\n                try:\n                    os.unlink(fpath)\n                except:\n                    pass\n"
  },
  {
    "path": "copyparty/szip.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport calendar\nimport stat\nimport time\n\nfrom .authsrv import AuthSrv\nfrom .bos import bos\nfrom .sutil import StreamArc, errdesc\nfrom .util import VPTL_WIN, min_ex, sanitize_to, spack, sunpack, yieldfile, zlib\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Any, Generator, Optional\n\n    from .util import NamedLogger\n\n\ndef dostime2unix(buf: bytes) -> int:\n    t, d = sunpack(b\"<HH\", buf)\n\n    ts = (t & 0x1F) * 2\n    tm = (t >> 5) & 0x3F\n    th = t >> 11\n\n    dd = d & 0x1F\n    dm = (d >> 5) & 0xF\n    dy = (d >> 9) + 1980\n\n    tt = (dy, dm, dd, th, tm, ts)\n    tf = \"{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}\"\n    iso = tf.format(*tt)\n\n    dt = time.strptime(iso, \"%Y-%m-%d %H:%M:%S\")\n    return int(calendar.timegm(dt))\n\n\ndef unixtime2dos(ts: int) -> bytes:\n    dy, dm, dd, th, tm, ts, _, _, _ = time.gmtime(ts + 1)\n    bd = ((dy - 1980) << 9) + (dm << 5) + dd\n    bt = (th << 11) + (tm << 5) + ts // 2\n    try:\n        return spack(b\"<HH\", bt, bd)\n    except:\n        return b\"\\x00\\x00\\x21\\x00\"\n\n\ndef gen_fdesc(sz: int, crc32: int, z64: bool) -> bytes:\n    ret = b\"\\x50\\x4b\\x07\\x08\"\n    fmt = b\"<LQQ\" if z64 else b\"<LLL\"\n    ret += spack(fmt, crc32, sz, sz)\n    return ret\n\n\ndef gen_hdr(\n    h_pos: Optional[int],\n    z64: bool,\n    fn: str,\n    sz: int,\n    lastmod: int,\n    utf8: bool,\n    icrc32: int,\n    pre_crc: bool,\n) -> bytes:\n    \"\"\"\n    does regular file headers\n    and the central directory meme if h_pos is set\n    (h_pos = absolute position of the regular header)\n    \"\"\"\n\n    # appnote 4.5 / zip 3.0 (2008) / unzip 6.0 (2009) says to add z64\n    # extinfo for values which exceed H, but that becomes an off-by-one\n    # (can't tell if it was clamped or exactly maxval), make it obvious\n    z64v = [sz, sz] if z64 else []\n    if h_pos and h_pos >= 0xFFFFFFFF:\n        # central, also consider ptr to original header\n        z64v.append(h_pos)\n\n    # confusingly this doesn't bump if h_pos\n    req_ver = b\"\\x2d\\x00\" if z64 else b\"\\x0a\\x00\"\n\n    if icrc32:\n        crc32 = spack(b\"<L\", icrc32)\n    else:\n        crc32 = b\"\\x00\" * 4\n\n    if h_pos is None:\n        # 4b magic, 2b min-ver\n        ret = b\"\\x50\\x4b\\x03\\x04\" + req_ver\n    else:\n        # 4b magic, 2b spec-ver (1b compat, 1b os (00 dos, 03 unix)), 2b min-ver\n        ret = b\"\\x50\\x4b\\x01\\x02\\x1e\\x03\" + req_ver\n\n    ret += b\"\\x00\" if pre_crc else b\"\\x08\"  # streaming\n    ret += b\"\\x08\" if utf8 else b\"\\x00\"  # appnote 6.3.2 (2007)\n\n    # 2b compression, 4b time, 4b crc\n    ret += b\"\\x00\\x00\" + unixtime2dos(lastmod) + crc32\n\n    # spec says to put zeros when !crc if bit3 (streaming)\n    # however infozip does actual sz and it even works on winxp\n    # (same reasoning for z64 extradata later)\n    vsz = 0xFFFFFFFF if z64 else sz\n    ret += spack(b\"<LL\", vsz, vsz)\n\n    # windows support (the \"?\" replace below too)\n    fn = sanitize_to(fn, VPTL_WIN)\n    bfn = fn.encode(\"utf-8\" if utf8 else \"cp437\", \"replace\").replace(b\"?\", b\"_\")\n\n    # add ntfs (0x24) and/or unix (0x10) extrafields for utc, add z64 if requested\n    z64_len = len(z64v) * 8 + 4 if z64v else 0\n    ret += spack(b\"<HH\", len(bfn), 0x10 + z64_len)\n\n    if h_pos is not None:\n        # 2b comment, 2b diskno\n        ret += b\"\\x00\" * 4\n\n        # 2b internal.attr, 4b external.attr\n        # infozip-macos: 0100 0000 a481 (spec-ver 1e03) file:644\n        # infozip-macos: 0100 0100 0080 (spec-ver 1e03) file:000\n        #     win10-zip: 0000 2000 0000 (spec-ver xx00) FILE_ATTRIBUTE_ARCHIVE\n        ret += b\"\\x00\\x00\\x00\\x00\\xa4\\x81\"  # unx\n        # ret += b\"\\x00\\x00\\x20\\x00\\x00\\x00\"  # fat\n\n        # 4b local-header-ofs\n        ret += spack(b\"<L\", min(h_pos, 0xFFFFFFFF))\n\n    ret += bfn\n\n    # ntfs: type 0a, size 20, rsvd, attr1, len 18, mtime, atime, ctime\n    # b\"\\xa3\\x2f\\x82\\x41\\x55\\x68\\xd8\\x01\"  1652616838.798941100  ~5.861518  132970904387989411  ~58615181\n    # nt = int((lastmod + 11644473600) * 10000000)\n    # ret += spack(b\"<HHLHHQQQ\", 0xA, 0x20, 0, 1, 0x18, nt, nt, nt)\n\n    # unix: type 0d, size 0c, atime, mtime, uid, gid\n    ret += spack(b\"<HHLLHH\", 0xD, 0xC, int(lastmod), int(lastmod), 1000, 1000)\n\n    if z64v:\n        ret += spack(b\"<HH\" + b\"Q\" * len(z64v), 1, len(z64v) * 8, *z64v)\n\n    return ret\n\n\ndef gen_ecdr(\n    items: list[tuple[str, int, int, int, int]], cdir_pos: int, cdir_end: int\n) -> tuple[bytes, bool]:\n    \"\"\"\n    summary of all file headers,\n    usually the zipfile footer unless something clamps\n    \"\"\"\n\n    ret = b\"\\x50\\x4b\\x05\\x06\"\n\n    # 2b ndisk, 2b disk0\n    ret += b\"\\x00\" * 4\n\n    cdir_sz = cdir_end - cdir_pos\n\n    nitems = min(0xFFFF, len(items))\n    csz = min(0xFFFFFFFF, cdir_sz)\n    cpos = min(0xFFFFFFFF, cdir_pos)\n\n    need_64 = nitems == 0xFFFF or 0xFFFFFFFF in [csz, cpos]\n\n    # 2b tnfiles, 2b dnfiles, 4b dir sz, 4b dir pos\n    ret += spack(b\"<HHLL\", nitems, nitems, csz, cpos)\n\n    # 2b comment length\n    ret += b\"\\x00\\x00\"\n\n    return ret, need_64\n\n\ndef gen_ecdr64(\n    items: list[tuple[str, int, int, int, int]], cdir_pos: int, cdir_end: int\n) -> bytes:\n    \"\"\"\n    z64 end of central directory\n    added when numfiles or a headerptr clamps\n    \"\"\"\n\n    ret = b\"\\x50\\x4b\\x06\\x06\"\n\n    # 8b own length from hereon\n    ret += b\"\\x2c\" + b\"\\x00\" * 7\n\n    # 2b spec-ver, 2b min-ver\n    ret += b\"\\x1e\\x03\\x2d\\x00\"\n\n    # 4b ndisk, 4b disk0\n    ret += b\"\\x00\" * 8\n\n    # 8b tnfiles, 8b dnfiles, 8b dir sz, 8b dir pos\n    cdir_sz = cdir_end - cdir_pos\n    ret += spack(b\"<QQQQ\", len(items), len(items), cdir_sz, cdir_pos)\n\n    return ret\n\n\ndef gen_ecdr64_loc(ecdr64_pos: int) -> bytes:\n    \"\"\"\n    z64 end of central directory locator\n    points to ecdr64\n    why\n    \"\"\"\n\n    ret = b\"\\x50\\x4b\\x06\\x07\"\n\n    # 4b cdisk, 8b start of ecdr64, 4b ndisks\n    ret += spack(b\"<LQL\", 0, ecdr64_pos, 1)\n\n    return ret\n\n\nclass StreamZip(StreamArc):\n    def __init__(\n        self,\n        log: \"NamedLogger\",\n        asrv: AuthSrv,\n        fgen: Generator[dict[str, Any], None, None],\n        utf8: bool = False,\n        pre_crc: bool = False,\n        **kwargs: Any\n    ) -> None:\n        super(StreamZip, self).__init__(log, asrv, fgen)\n\n        self.utf8 = utf8\n        self.pre_crc = pre_crc\n\n        self.pos = 0\n        self.items: list[tuple[str, int, int, int, int]] = []\n\n    def _ct(self, buf: bytes) -> bytes:\n        self.pos += len(buf)\n        return buf\n\n    def ser(self, f: dict[str, Any]) -> Generator[bytes, None, None]:\n        name = f[\"vp\"]\n        src = f[\"ap\"]\n        st = f[\"st\"]\n\n        if stat.S_ISDIR(st.st_mode):\n            return\n\n        sz = st.st_size\n        ts = st.st_mtime\n        h_pos = self.pos\n\n        crc = 0\n        if self.pre_crc:\n            for buf in yieldfile(src, self.args.iobuf):\n                crc = zlib.crc32(buf, crc)\n\n            crc &= 0xFFFFFFFF\n\n        # some unzip-programs expect a 64bit data-descriptor\n        # even if the only 32bit-exceeding value is the offset,\n        # so force that by placeholdering the filesize too\n        z64 = h_pos >= 0xFFFFFFFF or sz >= 0xFFFFFFFF\n\n        buf = gen_hdr(None, z64, name, sz, ts, self.utf8, crc, self.pre_crc)\n        yield self._ct(buf)\n\n        for buf in yieldfile(src, self.args.iobuf):\n            if not self.pre_crc:\n                crc = zlib.crc32(buf, crc)\n\n            yield self._ct(buf)\n\n        crc &= 0xFFFFFFFF\n\n        self.items.append((name, sz, ts, crc, h_pos))\n\n        if z64 or not self.pre_crc:\n            buf = gen_fdesc(sz, crc, z64)\n            yield self._ct(buf)\n\n    def gen(self) -> Generator[bytes, None, None]:\n        errf: dict[str, Any] = {}\n        errors = []\n        mbuf = b\"\"\n        try:\n            for f in self.fgen:\n                if \"err\" in f:\n                    errors.append((f[\"vp\"], f[\"err\"]))\n                    continue\n\n                try:\n                    for x in self.ser(f):\n                        mbuf += x\n                        if len(mbuf) >= 16384:\n                            yield mbuf\n                            mbuf = b\"\"\n                except GeneratorExit:\n                    raise\n                except:\n                    ex = min_ex(5, True).replace(\"\\n\", \"\\n-- \")\n                    errors.append((f[\"vp\"], ex))\n\n            if mbuf:\n                yield mbuf\n                mbuf = b\"\"\n\n            if errors:\n                errf, txt = errdesc(self.asrv.vfs, errors)\n                self.log(\"\\n\".join(([repr(errf)] + txt[1:])))\n                for x in self.ser(errf):\n                    yield x\n\n            cdir_pos = self.pos\n            for name, sz, ts, crc, h_pos in self.items:\n                z64 = h_pos >= 0xFFFFFFFF or sz >= 0xFFFFFFFF\n                buf = gen_hdr(h_pos, z64, name, sz, ts, self.utf8, crc, self.pre_crc)\n                mbuf += self._ct(buf)\n                if len(mbuf) >= 16384:\n                    yield mbuf\n                    mbuf = b\"\"\n            cdir_end = self.pos\n\n            _, need_64 = gen_ecdr(self.items, cdir_pos, cdir_end)\n            if need_64:\n                ecdir64_pos = self.pos\n                buf = gen_ecdr64(self.items, cdir_pos, cdir_end)\n                mbuf += self._ct(buf)\n\n                buf = gen_ecdr64_loc(ecdir64_pos)\n                mbuf += self._ct(buf)\n\n            ecdr, _ = gen_ecdr(self.items, cdir_pos, cdir_end)\n            yield mbuf + self._ct(ecdr)\n        finally:\n            if errf:\n                bos.unlink(errf[\"ap\"])\n"
  },
  {
    "path": "copyparty/tcpsrv.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport os\nimport re\nimport socket\nimport sys\nimport time\n\nfrom .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode\nfrom .cert import gencert\nfrom .qrkode import QrCode, qr2png, qr2svg, qr2txt, qrgen\nfrom .util import (\n    E_ACCESS,\n    E_ADDR_IN_USE,\n    E_ADDR_NOT_AVAIL,\n    E_UNREACH,\n    HAVE_IPV6,\n    IP6_LL,\n    IP6ALL,\n    VF_CAREFUL,\n    Netdev,\n    atomic_move,\n    get_adapters,\n    min_ex,\n    sunpack,\n    termsize,\n)\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Generator, Optional, Union\n\nif TYPE_CHECKING:\n    from .svchub import SvcHub\n\nif not hasattr(socket, \"AF_UNIX\"):\n    setattr(socket, \"AF_UNIX\", -9001)\n\nif not hasattr(socket, \"IPPROTO_IPV6\"):\n    setattr(socket, \"IPPROTO_IPV6\", 41)\n\nif not hasattr(socket, \"IP_FREEBIND\"):\n    setattr(socket, \"IP_FREEBIND\", 15)\n\n\nclass TcpSrv(object):\n    \"\"\"\n    tcplistener which forwards clients to Hub\n    which then uses the least busy HttpSrv to handle it\n    \"\"\"\n\n    def __init__(self, hub: \"SvcHub\"):\n        self.hub = hub\n        self.args = hub.args\n        self.log = hub.log\n\n        # mp-safe since issue6056\n        socket.setdefaulttimeout(120)\n\n        self.stopping = False\n        self.srv: list[socket.socket] = []\n        self.bound: list[tuple[str, int]] = []\n        self.seen_eps: list[tuple[str, int]] = []  # also skipped by uds-only\n        self.netdevs: dict[str, Netdev] = {}\n        self.netlist = \"\"\n        self.nsrv = 0\n        self.qr = \"\"\n        pad = False\n        ok: dict[str, list[int]] = {}\n        for ip in self.args.i:\n            if ip == \"::\":\n                if socket.has_ipv6:\n                    ips = [\"::\", \"0.0.0.0\"]\n                    dual = True\n                else:\n                    ips = [\"0.0.0.0\"]\n                    dual = False\n            else:\n                ips = [ip]\n                dual = False\n\n            for ipa in ips:\n                ok[ipa] = []\n\n            for port in self.args.p:\n                successful_binds = 0\n                try:\n                    for ipa in ips:\n                        try:\n                            self._listen(ipa, port)\n                            ok[ipa].append(port)\n                            successful_binds += 1\n                        except:\n                            if dual and \":\" in ipa:\n                                t = \"listen on IPv6 [{}] failed; trying IPv4 {}...\\n{}\"\n                                self.log(\"tcpsrv\", t.format(ipa, ips[1], min_ex()), 3)\n                                pad = True\n                                continue\n\n                            # binding 0.0.0.0 after :: fails on dualstack\n                            # but is necessary on non-dualstack\n                            if successful_binds:\n                                continue\n\n                            raise\n\n                except Exception as ex:\n                    if self.args.ign_ebind or self.args.ign_ebind_all:\n                        t = \"could not listen on {}:{}: {}\"\n                        self.log(\"tcpsrv\", t.format(ip, port, ex), c=3)\n                        pad = True\n                    else:\n                        raise\n\n        if not self.srv and not self.args.ign_ebind_all:\n            raise Exception(\"could not listen on any of the given interfaces\")\n\n        if pad:\n            self.log(\"tcpsrv\", \"\")\n\n        eps = {\n            \"127.0.0.1\": Netdev(\"127.0.0.1\", 0, \"\", \"local only\"),\n        }\n        if HAVE_IPV6:\n            eps[\"::1\"] = Netdev(\"::1\", 0, \"\", \"local only\")\n\n        nonlocals = [x for x in self.args.i if x not in [k.split(\"/\")[0] for k in eps]]\n        if nonlocals:\n            try:\n                self.netdevs = self.detect_interfaces(self.args.i)\n            except:\n                t = \"failed to discover server IP addresses\\n\"\n                self.log(\"tcpsrv\", t + min_ex(), 3)\n                self.netdevs = {}\n\n            eps.update({k.split(\"/\")[0]: v for k, v in self.netdevs.items()})\n            if not eps:\n                for x in nonlocals:\n                    eps[x] = Netdev(x, 0, \"\", \"external\")\n        else:\n            self.netdevs = {}\n\n        # keep IPv6 LL-only nics\n        ll_ok: set[str] = set()\n        for ip, nd in self.netdevs.items():\n            if not ip.startswith(IP6_LL):\n                continue\n\n            just_ll = True\n            for ip2, nd2 in self.netdevs.items():\n                if nd == nd2 and \":\" in ip2 and not ip2.startswith(IP6_LL):\n                    just_ll = False\n\n            if just_ll or self.args.ll:\n                ll_ok.add(ip.split(\"/\")[0])\n\n        listening_on = []\n        for ip, ports in sorted(ok.items()):\n            for port in sorted(ports):\n                listening_on.append(\"%s %s\" % (ip, port))\n\n        qr1: dict[str, list[int]] = {}\n        qr2: dict[str, list[int]] = {}\n        msgs = []\n        accessible_on = []\n        title_tab: dict[str, dict[str, int]] = {}\n        title_vars = [x[1:] for x in self.args.wintitle.split(\" \") if x.startswith(\"$\")]\n        t = \"available @ {}://{}:{}/  (\\033[33m{}\\033[0m)\"\n        for ip, desc in sorted(eps.items(), key=lambda x: x[1]):\n            if ip.startswith(IP6_LL) and ip not in ll_ok:\n                continue\n\n            for port in sorted(self.args.p):\n                if (\n                    port not in ok.get(ip, [])\n                    and port not in ok.get(\"::\", [])\n                    and port not in ok.get(\"0.0.0.0\", [])\n                ):\n                    continue\n\n                zs = \"%s %s\" % (ip, port)\n                if zs not in accessible_on:\n                    accessible_on.append(zs)\n\n                proto = \" http\"\n                if self.args.http_only:\n                    pass\n                elif self.args.https_only or port == 443:\n                    proto = \"https\"\n\n                hip = \"[{}]\".format(ip) if \":\" in ip else ip\n                msgs.append(t.format(proto, hip, port, desc))\n\n                is_ext = \"external\" in unicode(desc)\n                qrt = qr1 if is_ext else qr2\n                try:\n                    qrt[ip].append(port)\n                except:\n                    qrt[ip] = [port]\n\n                if not self.args.wintitle:\n                    continue\n\n                if port in [80, 443]:\n                    ep = ip\n                else:\n                    ep = \"{}:{}\".format(ip, port)\n\n                hits = []\n                if \"pub\" in title_vars and is_ext:\n                    hits.append((\"pub\", ep))\n\n                if \"pub\" in title_vars or \"all\" in title_vars:\n                    hits.append((\"all\", ep))\n\n                for var in title_vars:\n                    if var.startswith(\"ip-\") and ep.startswith(var[3:]):\n                        hits.append((var, ep))\n\n                for tk, tv in hits:\n                    try:\n                        title_tab[tk][tv] = 1\n                    except:\n                        title_tab[tk] = {tv: 1}\n\n        if msgs:\n            for t in msgs:\n                self.log(\"tcpsrv\", t)\n\n        if self.args.wintitle:\n            self._set_wintitle(title_tab)\n        else:\n            print(\"\\n\", end=\"\")\n\n        for fn, ls in (\n            (self.args.wr_h_eps, listening_on),\n            (self.args.wr_h_aon, accessible_on),\n        ):\n            if fn:\n                with open(fn, \"wb\") as f:\n                    f.write((\"\\n\".join(ls)).encode(\"utf-8\"))\n\n        if self.args.qr or self.args.qrs:\n            self.qr = self._qr(qr1, qr2)\n\n    def nlog(self, msg: str, c: Union[int, str] = 0) -> None:\n        self.log(\"tcpsrv\", msg, c)\n\n    def _listen(self, ip: str, port: int) -> None:\n        uds_perm = uds_gid = -1\n        bound: Optional[socket.socket] = None\n        tcp = False\n\n        if \"unix:\" in ip:\n            ipv = socket.AF_UNIX\n            uds = ip.split(\":\")\n            ip = uds[-1]\n            if len(uds) > 2:\n                uds_perm = int(uds[1], 8)\n            if len(uds) > 3:\n                try:\n                    uds_gid = int(uds[2])\n                except:\n                    import grp\n\n                    uds_gid = grp.getgrnam(uds[2]).gr_gid\n        elif \"fd:\" in ip:\n            fd = ip[3:]\n            bound = socket.socket(fileno=int(fd))\n\n            tcp = bound.proto == socket.IPPROTO_TCP\n            ipv = bound.family\n        elif \":\" in ip:\n            tcp = True\n            ipv = socket.AF_INET6\n        else:\n            tcp = True\n            ipv = socket.AF_INET\n\n        srv = bound or socket.socket(ipv, socket.SOCK_STREAM)\n\n        if not ANYWIN or self.args.reuseaddr:\n            srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n\n        if tcp:\n            srv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)\n\n        srv.settimeout(None)  # < does not inherit, ^ opts above do\n\n        try:\n            srv.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False)\n        except:\n            pass  # will create another ipv4 socket instead\n\n        if getattr(self.args, \"freebind\", False):\n            srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1)\n\n        if bound:\n            self.srv.append(srv)\n            return\n\n        try:\n            if tcp:\n                if self.args.http_no_tcp:\n                    self.seen_eps.append((ip, port))\n                    return\n                srv.bind((ip, port))\n            else:\n                if ANYWIN or self.args.rm_sck:\n                    if os.path.exists(ip):\n                        os.unlink(ip)\n                    srv.bind(ip)\n                    if uds_gid != -1:\n                        os.chown(ip, -1, uds_gid)\n                    if uds_perm != -1:\n                        os.chmod(ip, uds_perm)\n                else:\n                    tf = \"%s.%d\" % (ip, os.getpid())\n                    if os.path.exists(tf):\n                        os.unlink(tf)\n                    srv.bind(tf)\n                    if uds_gid != -1:\n                        os.chown(tf, -1, uds_gid)\n                    if uds_perm != -1:\n                        os.chmod(tf, uds_perm)\n                    atomic_move(self.nlog, tf, ip, VF_CAREFUL)\n\n            sport = srv.getsockname()[1] if tcp else port\n            if port != sport:\n                # linux 6.0.16 lets you bind a port which is in use\n                # except it just gives you a random port instead\n                raise OSError(E_ADDR_IN_USE[0], \"\")\n            self.srv.append(srv)\n        except (OSError, socket.error) as ex:\n            try:\n                srv.close()\n            except:\n                pass\n\n            e = \"\"\n            if ex.errno in E_ADDR_IN_USE:\n                e = \"\\033[1;31mport {} is busy on interface {}\\033[0m\".format(port, ip)\n                if not tcp:\n                    e = \"\\033[1;31munix-socket {} is busy\\033[0m\".format(ip)\n            elif ex.errno in E_ADDR_NOT_AVAIL:\n                e = \"\\033[1;31minterface {} does not exist\\033[0m\".format(ip)\n\n            if not e:\n                if not tcp:\n                    t = \"\\n\\n\\n  NOTE: this crash may be due to a unix-socket bug; try --rm-sck\\n\"\n                    self.log(\"tcpsrv\", t, 2)\n                raise\n\n            if not tcp and not self.args.rm_sck:\n                e += \"; maybe this is a bug? try --rm-sck\"\n\n            raise Exception(e)\n\n    def run(self) -> None:\n        all_eps = [x.getsockname()[:2] for x in self.srv]\n        bound: list[tuple[str, int]] = []\n        srvs: list[socket.socket] = []\n        for srv in self.srv:\n            if srv.family == socket.AF_UNIX:\n                tcp = False\n                ip = re.sub(r\"\\.[0-9]+$\", \"\", srv.getsockname())\n                port = 0\n            else:\n                tcp = True\n                ip, port = srv.getsockname()[:2]\n\n            if ip == IP6ALL:\n                ip = \"::\"  # jython\n\n            try:\n                srv.listen(self.args.nc)\n                try:\n                    ok = srv.getsockopt(socket.SOL_SOCKET, socket.SO_ACCEPTCONN)\n                except:\n                    ok = 1  # macos\n\n                if not ok:\n                    # some linux don't throw on listen(0.0.0.0) after listen(::)\n                    raise Exception(\"failed to listen on {}\".format(srv.getsockname()))\n            except:\n                if ip == \"0.0.0.0\" and (\"::\", port) in bound:\n                    # dualstack\n                    srv.close()\n                    continue\n\n                if ip == \"::\" and (\"0.0.0.0\", port) in all_eps:\n                    # no ipv6\n                    srv.close()\n                    continue\n\n                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\"\n                self.log(\"tcpsrv\", t.format(port, ip), 1)\n                raise\n\n            bound.append((ip, port))\n            srvs.append(srv)\n            fno = srv.fileno()\n            if tcp:\n                hip = \"[{}]\".format(ip) if \":\" in ip else ip\n                msg = \"listening @ {}:{}  f{} p{}\".format(hip, port, fno, os.getpid())\n            else:\n                msg = \"listening @ {}  f{} p{}\".format(ip, fno, os.getpid())\n\n            self.log(\"tcpsrv\", msg)\n            if self.args.q:\n                print(msg)\n\n            self.hub.broker.say(\"httpsrv.listen\", srv)\n\n        self.srv = srvs\n        self.bound = bound\n        self.seen_eps = list(set(self.seen_eps + bound))\n        self.nsrv = len(srvs)\n        self._distribute_netdevs()\n\n    def _distribute_netdevs(self):\n        self.hub.broker.say(\"httpsrv.set_netdevs\", self.netdevs)\n        self.hub.start_zeroconf()\n        gencert(self.log, self.args, self.netdevs)\n        self.hub.restart_sftpd()\n        self.hub.restart_ftpd()\n        self.hub.restart_tftpd()\n\n    def shutdown(self) -> None:\n        self.stopping = True\n        try:\n            for srv in self.srv:\n                srv.close()\n        except:\n            pass\n\n        self.log(\"tcpsrv\", \"ok bye\")\n\n    def netmon(self):\n        while not self.stopping:\n            time.sleep(self.args.z_chk)\n            netdevs = self.detect_interfaces(self.args.i)\n            if not netdevs:\n                continue\n\n            add = []\n            rem = []\n            for k, v in netdevs.items():\n                if k not in self.netdevs:\n                    add.append(\"\\n\\033[32m  added %s = %s\" % (k, v))\n            for k, v in self.netdevs.items():\n                if k not in netdevs:\n                    rem.append(\"\\n\\033[33mremoved %s = %s\" % (k, v))\n\n            t = \"network change detected:%s%s\"\n            self.log(\"tcpsrv\", t % (\"\".join(add), \"\".join(rem)), 3)\n            self.netdevs = netdevs\n            self._distribute_netdevs()\n\n    def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]:\n        listen_ips = [x for x in listen_ips if not x.startswith((\"unix:\", \"fd:\"))]\n\n        nics = get_adapters(True)\n        eps: dict[str, Netdev] = {}\n        for nic in nics:\n            for nip in nic.ips:\n                ipa = nip.ip[0] if \":\" in str(nip.ip) else nip.ip\n                sip = \"{}/{}\".format(ipa, nip.network_prefix)\n                nd = Netdev(sip, nic.index or 0, nic.nice_name, \"\")\n                eps[sip] = nd\n                try:\n                    idx = socket.if_nametoindex(nd.name)\n                    if idx and idx != nd.idx:\n                        t = \"netdev idx mismatch; ifaddr={} cpython={}\"\n                        self.log(\"tcpsrv\", t.format(nd.idx, idx), 3)\n                        nd.idx = idx\n                except:\n                    pass\n\n        netlist = str(sorted(eps.items()))\n        if netlist == self.netlist and self.netdevs:\n            return {}\n\n        self.netlist = netlist\n\n        if \"0.0.0.0\" not in listen_ips and \"::\" not in listen_ips:\n            eps = {k: v for k, v in eps.items() if k.split(\"/\")[0] in listen_ips}\n\n        try:\n            ext_devs = list(self._extdevs_nix())\n            ext_ips = [k for k, v in eps.items() if v.name in ext_devs]\n            ext_ips = [x.split(\"/\")[0] for x in ext_ips]\n            if not ext_ips:\n                raise Exception()\n        except:\n            rt = self._defroute()\n            ext_ips = [rt] if rt else []\n\n        for lip in listen_ips:\n            if not ext_ips or lip not in [\"0.0.0.0\", \"::\"] + ext_ips:\n                continue\n\n            desc = \"\\033[32mexternal\"\n            ips = ext_ips if lip in [\"0.0.0.0\", \"::\"] else [lip]\n            for ip in ips:\n                ip = next((x for x in eps if x.startswith(ip + \"/\")), \"\")\n                if ip and \"external\" not in eps[ip].desc:\n                    eps[ip].desc += \", \" + desc\n\n        return eps\n\n    def _extdevs_nix(self) -> Generator[str, None, None]:\n        with open(\"/proc/net/route\", \"rb\") as f:\n            next(f)\n            for ln in f:\n                r = ln.decode(\"utf-8\").strip().split()\n                if r[1] == \"0\" * 8 and int(r[3], 16) & 2:\n                    yield r[0]\n\n    def _defroute(self) -> str:\n        ret = \"\"\n        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n        for ip in [\n            \"10.254.39.23\",\n            \"172.31.39.23\",\n            \"192.168.39.23\",\n            \"239.254.39.23\",\n            \"169.254.39.23\",\n            # could add 1.1.1.1 as a final fallback\n            # but external connections is kinshi\n        ]:\n            try:\n                s.connect((ip, 1))\n                ret = s.getsockname()[0]\n                break\n            except (OSError, socket.error) as ex:\n                if ex.errno in E_ACCESS:\n                    self.log(\"tcpsrv\", \"eaccess {} (trying next)\".format(ip))\n                elif ex.errno not in E_UNREACH:\n                    self.log(\"tcpsrv\", \"route lookup failed; err {}\".format(ex.errno))\n\n        s.close()\n        return ret\n\n    def _set_wintitle(self, vs: dict[str, dict[str, int]]) -> None:\n        vs[\"all\"] = vs.get(\"all\", {\"Local-Only\": 1})\n        vs[\"pub\"] = vs.get(\"pub\", vs[\"all\"])\n\n        vs2 = {}\n        for k, eps in vs.items():\n            filt = {ep: 1 for ep in eps if \":\" not in ep}\n            have = set(filt)\n            for ep in sorted(eps):\n                ip = ep.split(\":\")[0]\n                if ip not in have:\n                    have.add(ip)\n                    filt[ep] = 1\n\n            lo = [x for x in filt if x.startswith(\"127.\")]\n            if len(filt) > 3 and lo:\n                for ip in lo:\n                    filt.pop(ip)\n\n            vs2[k] = filt\n\n        title = \"\"\n        vs = vs2\n        for p in self.args.wintitle.split(\" \"):\n            if p.startswith(\"$\"):\n                seps = list(sorted(vs.get(p[1:], {\"(None)\": 1}).keys()))\n                p = \", \".join(seps[:3])\n                if len(seps) > 3:\n                    p += \", ...\"\n\n            title += \"{} \".format(p)\n\n        print(\"\\033]0;{}\\033\\\\\\n\".format(title), file=sys.stderr, end=\"\")\n        sys.stderr.flush()\n\n    def _qr(self, t1: dict[str, list[int]], t2: dict[str, list[int]]) -> str:\n        t2c = {zs: zli for zs, zli in t2.items() if zs in (\"127.0.0.1\", \"::1\")}\n        t2b = {zs: zli for zs, zli in t2.items() if \":\" in zs and zs not in t2c}\n        t2 = {zs: zli for zs, zli in t2.items() if zs not in t2b and zs not in t2c}\n        t2.update(t2b)  # first ipv4, then ipv6...\n        t2.update(t2c)  # ...and finally localhost\n\n        ip = None\n        ips = list(t1) + list(t2)\n        qri = self.args.qri\n        if self.args.zm and not qri and ips:\n            name = self.args.name + \".local\"\n            t1[name] = next(v for v in (t1 or t2).values())\n            ips = [name] + ips\n\n        for ip in ips:\n            if ip.startswith(qri) or qri == \".\":\n                break\n            ip = \"\"\n\n        if not ip:\n            # maybe /bin/ip is missing or smth\n            ip = qri\n\n        if not ip:\n            return \"\"\n\n        hip = \"[%s]\" % (ip,) if \":\" in ip else ip\n\n        if self.args.http_only:\n            https = \"\"\n        elif self.args.https_only:\n            https = \"s\"\n        else:\n            https = \"s\" if self.args.qrs else \"\"\n\n        ports = t1.get(ip, t2.get(ip, []))\n        dport = 443 if https else 80\n        port = \"\" if dport in ports or not ports else \":{}\".format(ports[0])\n        txt = \"http{}://{}{}/{}\".format(https, hip, port, self.args.qrl)\n\n        btxt = txt.encode(\"utf-8\")\n        if PY2:\n            btxt = sunpack(b\"B\" * len(btxt), btxt)\n\n        fg = self.args.qr_fg\n        bg = self.args.qr_bg\n        nocolor = fg == -1\n        if nocolor:\n            fg = 0\n\n        pad = self.args.qrp\n        zoom = self.args.qrz\n        qrc = qrgen(btxt)\n\n        for zs in self.args.qr_file or []:\n            self._qr2file(qrc, zs)\n\n        if zoom == 0:\n            try:\n                tw, th = termsize()\n                tsz = min(tw // 2, th)\n                zoom = 1 if qrc.size + pad * 2 >= tsz else 2\n            except:\n                zoom = 1\n\n        qr = qr2txt(qrc, zoom, pad)\n        if self.args.no_ansi:\n            return \"{}\\n{}\".format(txt, qr)\n\n        halfc = \"\\033[40;48;5;{0}m{1}\\033[47;48;5;{2}m\"\n        if not fg:\n            halfc = \"\\033[0;40m{1}\\033[0;47m\"\n        if nocolor:\n            halfc = \"\\033[0;7m{1}\\033[0m\"\n\n        def ansify(m: re.Match) -> str:\n            return halfc.format(fg, \" \" * len(m.group(1)), bg)\n\n        if zoom > 1:\n            qr = re.sub(\"(█+)\", ansify, qr)\n\n        qr = qr.replace(\"\\n\", \"\\033[K\\n\") + \"\\033[K\"  # win10do\n        cc = \" \\033[0;38;5;{0};47;48;5;{1}m\" if fg else \" \\033[0;30;47m\"\n        if nocolor:\n            cc = \" \\033[0m\"\n        t = cc + \"\\n{2}\\033[999G\\033[0m\\033[J\"\n        t = t.format(fg, bg, qr)\n        if ANYWIN:\n            # prevent color loss on terminal resize\n            t = t.replace(\"\\n\", \"`\\n`\")\n\n        return txt + t\n\n    def _qr2file(self, qrc: QrCode, txt: str):\n        if \".txt:\" in txt or \".svg:\" in txt:\n            ap, zs1, zs2 = txt.rsplit(\":\", 2)\n            bg = fg = \"\"\n        else:\n            ap, zs1, zs2, bg, fg = txt.rsplit(\":\", 4)\n        zoom = int(zs1)\n        pad = int(zs2)\n\n        if ap.endswith(\".txt\"):\n            if zoom not in (1, 2):\n                raise Exception(\"invalid zoom for qr.txt; must be 1 or 2\")\n            with open(ap, \"wb\") as f:\n                f.write(qr2txt(qrc, zoom, pad).encode(\"utf-8\"))\n        elif ap.endswith(\".svg\"):\n            with open(ap, \"wb\") as f:\n                f.write(qr2svg(qrc, pad).encode(\"utf-8\"))\n        else:\n            qr2png(qrc, zoom, pad, self._h2i(bg), self._h2i(fg), ap)\n\n    def _h2i(self, hs):\n        try:\n            return tuple(int(hs[i : i + 2], 16) for i in (0, 2, 4))\n        except:\n            return None\n"
  },
  {
    "path": "copyparty/tftpd.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\ntry:\n    from types import SimpleNamespace\nexcept:\n\n    class SimpleNamespace(object):\n        def __init__(self, **attr):\n            self.__dict__.update(attr)\n\n\nimport logging\nimport os\nimport re\nimport socket\nimport stat\nimport threading\nimport time\nfrom datetime import datetime\n\ntry:\n    import inspect\nexcept:\n    pass\n\nfrom partftpy import (\n    TftpContexts,\n    TftpPacketFactory,\n    TftpPacketTypes,\n    TftpServer,\n    TftpStates,\n)\nfrom partftpy.TftpShared import TftpException\n\nfrom .__init__ import EXE, PY2, TYPE_CHECKING\nfrom .authsrv import VFS\nfrom .bos import bos\nfrom .util import (\n    UTC,\n    BytesIO,\n    Daemon,\n    ODict,\n    exclude_dotfiles,\n    min_ex,\n    runhook,\n    set_fperms,\n    undot,\n    vjoin,\n    vsplit,\n)\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Any, Union\n\nif TYPE_CHECKING:\n    from .svchub import SvcHub\n\nif PY2:\n    range = xrange  # type: ignore\n\n\nlg = logging.getLogger(\"tftp\")\ndebug, info, warning, error = (lg.debug, lg.info, lg.warning, lg.error)\n\n\ndef noop(*a, **ka) -> None:\n    pass\n\n\ndef _serverInitial(self, pkt: Any, raddress: str, rport: int) -> bool:\n    info(\"connection from %s:%s\", raddress, rport)\n    ret = _sinitial[0](self, pkt, raddress, rport)\n    nm = _hub[0].args.tftp_ipa_nm\n    if nm and not nm.map(raddress):\n        yeet(\"client rejected (--tftp-ipa): %s\" % (raddress,))\n    return ret\n\n\n# patch ipa-check into partftpd (part 1/2)\n_hub: list[\"SvcHub\"] = []\n_sinitial: list[Any] = []\n\n\nclass Tftpd(object):\n    def __init__(self, hub: \"SvcHub\") -> None:\n        self.hub = hub\n        self.args = hub.args\n        self.asrv = hub.asrv\n        self.log = hub.log\n        self.mutex = threading.Lock()\n\n        _hub[:] = []\n        _hub.append(hub)\n\n        lg.setLevel(logging.DEBUG if self.args.tftpv else logging.INFO)\n        for x in [\"partftpy\", \"partftpy.TftpStates\", \"partftpy.TftpServer\"]:\n            lgr = logging.getLogger(x)\n            lgr.setLevel(logging.DEBUG if self.args.tftpv else logging.INFO)\n\n        if not self.args.tftpv and not self.args.tftpvv:\n            # contexts -> states -> packettypes -> shared\n            # contexts -> packetfactory\n            # packetfactory -> packettypes\n            Cs = [\n                TftpPacketTypes,\n                TftpPacketFactory,\n                TftpStates,\n                TftpContexts,\n                TftpServer,\n            ]\n            cbak = []\n            if not self.args.tftp_no_fast and not EXE and not PY2:\n                try:\n                    ptn = re.compile(r\"(^\\s*)log\\.debug\\(.*\\)$\")\n                    for C in Cs:\n                        cbak.append(C.__dict__)\n                        src1 = inspect.getsource(C).split(\"\\n\")\n                        src2 = \"\\n\".join([ptn.sub(\"\\\\1pass\", ln) for ln in src1])\n                        cfn = C.__spec__.origin\n                        exec (compile(src2, filename=cfn, mode=\"exec\"), C.__dict__)\n                except Exception:\n                    t = \"failed to optimize tftp code; run with --tftp-no-fast if there are issues:\\n\"\n                    self.log(\"tftp\", t + min_ex(), 3)\n                    for n, zd in enumerate(cbak):\n                        Cs[n].__dict__ = zd\n\n            for C in Cs:\n                C.log.debug = noop\n\n        # patch ipa-check into partftpd (part 2/2)\n        _sinitial[:] = []\n        _sinitial.append(TftpStates.TftpServerState.serverInitial)\n        TftpStates.TftpServerState.serverInitial = _serverInitial\n\n        # patch vfs into partftpy\n        TftpContexts.open = self._open\n        TftpStates.open = self._open\n\n        fos = SimpleNamespace()\n        for k in os.__dict__:\n            try:\n                setattr(fos, k, getattr(os, k))\n            except:\n                pass\n        fos.access = self._access\n        fos.mkdir = self._mkdir\n        fos.unlink = self._unlink\n        fos.sep = \"/\"\n        TftpContexts.os = fos\n        TftpServer.os = fos\n        TftpStates.os = fos\n\n        fop = SimpleNamespace()\n        for k in os.path.__dict__:\n            try:\n                setattr(fop, k, getattr(os.path, k))\n            except:\n                pass\n        fop.abspath = self._p_abspath\n        fop.exists = self._p_exists\n        fop.isdir = self._p_isdir\n        fop.normpath = self._p_normpath\n        fos.path = fop\n\n        self._disarm(fos)\n\n        self.port = int(self.args.tftp)\n        self.srv = []\n        self.ips = []\n\n        ports = []\n        if self.args.tftp_pr:\n            p1, p2 = [int(x) for x in self.args.tftp_pr.split(\"-\")]\n            ports = list(range(p1, p2 + 1))\n\n        ips = self.args.tftp_i\n        if \"::\" in ips:\n            ips.append(\"0.0.0.0\")\n\n        ips = [x for x in ips if not x.startswith((\"unix:\", \"fd:\"))]\n\n        if self.args.tftp4:\n            ips = [x for x in ips if \":\" not in x]\n\n        if not ips:\n            t = \"cannot start tftp-server; no compatible IPs in -i\"\n            self.nlog(t, 1)\n            return\n\n        ips = list(ODict.fromkeys(ips))  # dedup\n\n        for ip in ips:\n            name = \"tftp_%s\" % (ip,)\n            Daemon(self._start, name, [ip, ports])\n            time.sleep(0.2)  # give dualstack a chance\n\n    def nlog(self, msg: str, c: Union[int, str] = 0) -> None:\n        self.log(\"tftp\", msg, c)\n\n    def _start(self, ip, ports):\n        fam = socket.AF_INET6 if \":\" in ip else socket.AF_INET\n        have_been_alive = False\n        while True:\n            srv = TftpServer.TftpServer(\"/\", self._ls)\n            with self.mutex:\n                self.srv.append(srv)\n                self.ips.append(ip)\n\n            try:\n                # this is the listen loop; it should block forever\n                srv.listen(ip, self.port, af_family=fam, ports=ports)\n            except:\n                with self.mutex:\n                    self.srv.remove(srv)\n                    self.ips.remove(ip)\n\n                try:\n                    srv.sock.close()\n                except:\n                    pass\n\n                try:\n                    bound = bool(srv.listenport)\n                except:\n                    bound = False\n\n                if bound:\n                    # this instance has managed to bind at least once\n                    have_been_alive = True\n\n                if have_been_alive:\n                    t = \"tftp server [%s]:%d crashed; restarting in 3 sec:\\n%s\"\n                    error(t, ip, self.port, min_ex())\n                    time.sleep(3)\n                    continue\n\n                # server failed to start; could be due to dualstack (ipv6 managed to bind and this is ipv4)\n                if ip != \"0.0.0.0\" or \"::\" not in self.ips:\n                    # nope, it's fatal\n                    t = \"tftp server [%s]:%d failed to start:\\n%s\"\n                    error(t, ip, self.port, min_ex())\n\n                # yep; ignore\n                # (TODO: move the \"listening @ ...\" infolog in partftpy to\n                #   after the bind attempt so it doesn't print twice)\n                return\n\n            info(\"tftp server [%s]:%d terminated\", ip, self.port)\n            break\n\n    def stop(self):\n        with self.mutex:\n            srvs = self.srv[:]\n\n        for srv in srvs:\n            srv.stop()\n\n    def _v2a(\n        self, caller: str, vpath: str, perms: list, *a: Any\n    ) -> tuple[VFS, str, str]:\n        vpath = vpath.replace(\"\\\\\", \"/\").lstrip(\"/\")\n        if not perms:\n            perms = [True, True]\n\n        debug('%s(\"%s\", %s) %s\\033[K\\033[0m', caller, vpath, str(a), perms)\n        vfs, rem = self.asrv.vfs.get(vpath, \"*\", *perms)\n        if perms[1] and \"*\" not in vfs.axs.uread and \"wo_up_readme\" not in vfs.flags:\n            zs, fn = vsplit(vpath)\n            if fn.lower() in vfs.flags[\"emb_all\"]:\n                vpath = vjoin(zs, \"_wo_\" + fn)\n                vfs, rem = self.asrv.vfs.get(vpath, \"*\", *perms)\n\n        if not vfs.realpath:\n            raise Exception(\"unmapped vfs\")\n\n        return vfs, vpath, vfs.canonical(rem)\n\n    def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any:\n        # generate file listing if vpath is dir.txt and return as file object\n        if not force:\n            vpath, fn = os.path.split(vpath.replace(\"\\\\\", \"/\"))\n            ptn = self.args.tftp_lsf\n            if not ptn or not ptn.match(fn.lower()):\n                return None\n\n        tsdt = datetime.fromtimestamp\n        vn, rem = self.asrv.vfs.get(vpath, \"*\", True, False)\n        fsroot, vfs_ls, vfs_virt = vn.ls(\n            rem,\n            \"*\",\n            not self.args.no_scandir,\n            [[True, False]],\n            throw=True,\n        )\n        dnames = set([x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)])\n        dirs1 = [(v.st_mtime, v.st_size, k + \"/\") for k, v in vfs_ls if k in dnames]\n        fils1 = [(v.st_mtime, v.st_size, k) for k, v in vfs_ls if k not in dnames]\n        real1 = dirs1 + fils1\n        realt = [(tsdt(max(0, mt), UTC), sz, fn) for mt, sz, fn in real1]\n        reals = [\n            (\n                \"%04d-%02d-%02d %02d:%02d:%02d\"\n                % (\n                    zd.year,\n                    zd.month,\n                    zd.day,\n                    zd.hour,\n                    zd.minute,\n                    zd.second,\n                ),\n                sz,\n                fn,\n            )\n            for zd, sz, fn in realt\n        ]\n        virs = [(\"????-??-?? ??:??:??\", 0, k + \"/\") for k in vfs_virt.keys()]\n        ls = virs + reals\n\n        if \"*\" not in vn.axs.udot:\n            names = set(exclude_dotfiles([x[2] for x in ls]))\n            ls = [x for x in ls if x[2] in names]\n\n        try:\n            biggest = max([x[1] for x in ls])\n        except:\n            biggest = 0\n\n        perms = []\n        if \"*\" in vn.axs.uread:\n            perms.append(\"read\")\n        if \"*\" in vn.axs.udot:\n            perms.append(\"hidden\")\n        if \"*\" in vn.axs.uwrite:\n            if \"*\" in vn.axs.udel:\n                perms.append(\"overwrite\")\n            else:\n                perms.append(\"write\")\n\n        fmt = \"{{}}  {{:{},}}  {{}}\"\n        fmt = fmt.format(len(\"{:,}\".format(biggest)))\n        retl = [\"# permissions: %s\" % (\", \".join(perms),)]\n        retl += [fmt.format(*x) for x in ls]\n        ret = \"\\n\".join(retl).encode(\"utf-8\", \"replace\")\n        return BytesIO(ret + b\"\\n\")\n\n    def _open(self, vpath: str, mode: str, *a: Any, **ka: Any) -> Any:\n        rd = wr = False\n        if mode == \"rb\":\n            rd = True\n        elif mode == \"wb\":\n            wr = True\n        else:\n            raise Exception(\"bad mode %s\" % (mode,))\n\n        vfs, vpath, ap = self._v2a(\"open\", vpath, [rd, wr])\n        if wr:\n            if \"*\" not in vfs.axs.uwrite:\n                yeet(\"blocked write; folder not world-writable: /%s\" % (vpath,))\n\n            if bos.path.exists(ap) and \"*\" not in vfs.axs.udel:\n                yeet(\"blocked write; folder not world-deletable: /%s\" % (vpath,))\n\n            xbu = vfs.flags.get(\"xbu\")\n            if xbu:\n                hr = runhook(\n                    self.nlog,\n                    None,\n                    self.hub.up2k,\n                    \"xbu.tftpd\",\n                    xbu,\n                    ap,\n                    vpath,\n                    \"\",\n                    \"\",\n                    \"\",\n                    0,\n                    0,\n                    \"8.3.8.7\",\n                    time.time(),\n                    None,\n                )\n                t = hr.get(\"rejectmsg\") or \"\"\n                if t or hr.get(\"rc\") != 0:\n                    if not t:\n                        t = \"upload blocked by xbu server config: %r\" % (vpath,)\n                    yeet(t)\n\n        if not self.args.tftp_nols and bos.path.isdir(ap):\n            return self._ls(vpath, \"\", 0, True)\n\n        if not a:\n            a = (self.args.iobuf,)\n\n        ret = open(ap, mode, *a, **ka)\n        if wr and \"fperms\" in vfs.flags:\n            set_fperms(ret, vfs.flags)\n\n        return ret\n\n    def _mkdir(self, vpath: str, *a) -> None:\n        vfs, _, ap = self._v2a(\"mkdir\", vpath, [False, True])\n        if \"*\" not in vfs.axs.uwrite:\n            yeet(\"blocked mkdir; folder not world-writable: /%s\" % (vpath,))\n\n        bos.mkdir(ap, vfs.flags[\"chmod_d\"])\n        if \"chown\" in vfs.flags:\n            bos.chown(ap, vfs.flags[\"uid\"], vfs.flags[\"gid\"])\n\n    def _unlink(self, vpath: str) -> None:\n        # return bos.unlink(self._v2a(\"stat\", vpath, *a)[1])\n        vfs, _, ap = self._v2a(\"delete\", vpath, [True, False, False, True])\n\n        try:\n            inf = bos.stat(ap)\n        except:\n            return\n\n        if not stat.S_ISREG(inf.st_mode) or inf.st_size:\n            yeet(\"attempted delete of non-empty file\")\n\n        vpath = vpath.replace(\"\\\\\", \"/\").lstrip(\"/\")\n        self.hub.up2k.handle_rm(\"*\", \"8.3.8.7\", [vpath], [], False, False)\n\n    def _access(self, *a: Any) -> bool:\n        return True\n\n    def _p_abspath(self, vpath: str) -> str:\n        return \"/\" + undot(vpath)\n\n    def _p_normpath(self, *a: Any) -> str:\n        return \"\"\n\n    def _p_exists(self, vpath: str) -> bool:\n        try:\n            ap = self._v2a(\"p.exists\", vpath, [False, False])[2]\n            bos.stat(ap)\n            return True\n        except:\n            return vpath == \"/\"\n\n    def _p_isdir(self, vpath: str) -> bool:\n        try:\n            st = bos.stat(self._v2a(\"p.isdir\", vpath, [False, False])[2])\n            ret = stat.S_ISDIR(st.st_mode)\n            return ret\n        except:\n            return vpath == \"/\"\n\n    def _hook(self, *a: Any, **ka: Any) -> None:\n        src = inspect.currentframe().f_back.f_code.co_name\n        error(\"\\033[31m%s:hook(%s)\\033[0m\", src, a)\n        raise Exception(\"nope\")\n\n    def _disarm(self, fos: SimpleNamespace) -> None:\n        fos.chmod = self._hook\n        fos.chown = self._hook\n        fos.close = self._hook\n        fos.ftruncate = self._hook\n        fos.lchown = self._hook\n        fos.link = self._hook\n        fos.listdir = self._hook\n        fos.lstat = self._hook\n        fos.open = self._hook\n        fos.remove = self._hook\n        fos.rename = self._hook\n        fos.replace = self._hook\n        fos.scandir = self._hook\n        fos.stat = self._hook\n        fos.symlink = self._hook\n        fos.truncate = self._hook\n        fos.utime = self._hook\n        fos.walk = self._hook\n\n        fos.path.expanduser = self._hook\n        fos.path.expandvars = self._hook\n        fos.path.getatime = self._hook\n        fos.path.getctime = self._hook\n        fos.path.getmtime = self._hook\n        fos.path.getsize = self._hook\n        fos.path.isabs = self._hook\n        fos.path.isfile = self._hook\n        fos.path.islink = self._hook\n        fos.path.realpath = self._hook\n\n\ndef yeet(msg: str) -> None:\n    warning(msg)\n    raise TftpException(msg)\n"
  },
  {
    "path": "copyparty/th_cli.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport errno\nimport os\nimport stat\n\nfrom .__init__ import TYPE_CHECKING\nfrom .authsrv import VFS\nfrom .bos import bos\nfrom .th_srv import EXTS_AC, H_PIL_JXL, H_PIL_WEBP, thumb_path\nfrom .util import Cooldown, Pebkac\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Optional, Union\n\nif TYPE_CHECKING:\n    from .httpsrv import HttpSrv\n\n\nIOERROR = \"reading the file was denied by the server os; either due to filesystem permissions, selinux, apparmor, or similar:\\n%r\"\n\nIMG_EXTS = set([\"webp\", \"jpg\", \"png\", \"jxl\"])\n\n\nclass ThumbCli(object):\n    def __init__(self, hsrv: \"HttpSrv\") -> None:\n        self.broker = hsrv.broker\n        self.log_func = hsrv.log\n        self.args = hsrv.args\n        self.asrv = hsrv.asrv\n\n        # cache on both sides for less broker spam\n        self.cooldown = Cooldown(self.args.th_poke)\n\n        try:\n            c = hsrv.th_cfg\n            if not c:\n                raise Exception()\n        except:\n            c = {\n                k: set()\n                for k in [\"thumbable\", \"pil\", \"vips\", \"raw\", \"ffi\", \"ffv\", \"ffa\"]\n            }\n\n        self.thumbable = c[\"thumbable\"]\n        self.fmt_pil = c[\"pil\"]\n        self.fmt_vips = c[\"vips\"]\n        self.fmt_raw = c[\"raw\"]\n        self.fmt_ffi = c[\"ffi\"]\n        self.fmt_ffv = c[\"ffv\"]\n        self.fmt_ffa = c[\"ffa\"]\n\n        # defer args.th_ff_jpg, can change at runtime\n        nonpil = next((x for x in self.args.th_dec if x in (\"vips\", \"ff\")), None)\n        self.can_webp = H_PIL_WEBP or nonpil\n        self.can_jxl = H_PIL_JXL or nonpil\n\n    def log(self, msg: str, c: Union[int, str] = 0) -> None:\n        self.log_func(\"thumbcli\", msg, c)\n\n    def get(self, dbv: VFS, rem: str, mtime: float, fmt: str) -> Optional[str]:\n        ptop = dbv.realpath\n        ext = rem.rsplit(\".\")[-1].lower()\n        if ext not in self.thumbable or \"dthumb\" in dbv.flags:\n            return None\n\n        is_vid = ext in self.fmt_ffv\n        if is_vid and \"dvthumb\" in dbv.flags:\n            return None\n\n        want_opus = fmt in EXTS_AC\n        is_au = ext in self.fmt_ffa\n        is_vau = want_opus and ext in self.fmt_ffv\n        if is_au or is_vau:\n            if want_opus:\n                if self.args.no_acode:\n                    return None\n                elif fmt == \"caf\" and self.args.no_caf:\n                    fmt = \"mp3\"\n                elif fmt == \"owa\" and self.args.no_owa:\n                    fmt = \"mp3\"\n            else:\n                if \"dathumb\" in dbv.flags:\n                    return None\n        elif want_opus:\n            return None\n\n        is_img = not is_vid and not is_au\n        if is_img and \"dithumb\" in dbv.flags:\n            return None\n\n        preferred = self.args.th_dec[0] if self.args.th_dec else \"\"\n\n        if rem.startswith(\".hist/th/\") and rem.split(\".\")[-1] in IMG_EXTS:\n            return os.path.join(ptop, rem)\n\n        if fmt[:1] in \"jwx\" and fmt != \"wav\":\n            sfmt = fmt[:1]\n\n            if sfmt == \"j\" and self.args.th_no_jpg:\n                sfmt = \"w\"\n\n            if sfmt == \"w\":\n                if (\n                    self.args.th_no_webp\n                    or not self.can_webp\n                    or (self.args.th_ff_jpg and (not is_img or preferred == \"ff\"))\n                ):\n                    sfmt = \"j\"\n\n            if sfmt == \"x\":\n                if self.args.th_no_jxl or not self.can_jxl:\n                    sfmt = \"w\"\n\n            vf_crop = dbv.flags[\"crop\"]\n            vf_th3x = dbv.flags[\"th3x\"]\n\n            if \"f\" in vf_crop:\n                sfmt += \"f\" if \"n\" in vf_crop else \"\"\n            else:\n                sfmt += \"f\" if \"f\" in fmt else \"\"\n\n            if \"f\" in vf_th3x:\n                sfmt += \"3\" if \"y\" in vf_th3x else \"\"\n            else:\n                sfmt += \"3\" if \"3\" in fmt else \"\"\n\n            fmt = sfmt\n\n        elif fmt[:1] == \"p\" and not is_au and not is_vid:\n            t = \"cannot thumbnail %r: png only allowed for waveforms\"\n            self.log(t % (rem,), 6)\n            return None\n\n        histpath = self.asrv.vfs.histtab.get(ptop)\n        if not histpath:\n            self.log(\"no histpath for %r\" % (ptop,))\n            return None\n\n        tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa)\n        tpaths = [tpath]\n        fmtc = fmt[:1]\n        if fmtc == \"w\" and fmt != \"wav\":\n            # also check for jpg (maybe webp is unavailable)\n            tpaths.append(tpath.rsplit(\".\", 1)[0] + \".jpg\")\n        elif fmtc == \"x\":\n            tpaths.append(tpath.rsplit(\".\", 1)[0] + \".webp\")\n\n        ret = None\n        abort = False\n        for tp in tpaths:\n            try:\n                st = bos.stat(tp)\n                if st.st_size:\n                    ret = tpath = tp\n                    fmt = ret.rsplit(\".\")[1]\n                    break\n                else:\n                    abort = True\n            except:\n                pass\n\n        if ret:\n            tdir = os.path.dirname(tpath)\n            if self.cooldown.poke(tdir):\n                self.broker.say(\"thumbsrv.poke\", tdir)\n\n            if want_opus:\n                # audio files expire individually\n                if self.cooldown.poke(tpath):\n                    self.broker.say(\"thumbsrv.poke\", tpath)\n\n            return ret\n\n        if abort:\n            return None\n\n        ap = os.path.join(ptop, rem)\n        try:\n            st = bos.stat(ap)\n            if not st.st_size or not stat.S_ISREG(st.st_mode):\n                return None\n\n            with open(ap, \"rb\", 4) as f:\n                if not f.read(4):\n                    raise Exception()\n        except OSError as ex:\n            if ex.errno == errno.ENOENT:\n                raise Pebkac(404)\n            else:\n                raise Pebkac(500, IOERROR % (ex,))\n        except Exception as ex:\n            raise Pebkac(500, IOERROR % (ex,))\n\n        x = self.broker.ask(\"thumbsrv.get\", ptop, rem, mtime, fmt)\n        return x.get()  # type: ignore\n"
  },
  {
    "path": "copyparty/th_srv.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport hashlib\nimport io\nimport logging\nimport os\nimport re\nimport shlex\nimport shutil\nimport subprocess as sp\nimport tempfile\nimport threading\nimport time\n\nfrom queue import Queue\n\nfrom .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode\nfrom .authsrv import VFS\nfrom .bos import bos\nfrom .mtag import HAVE_FFMPEG, HAVE_FFPROBE, au_unpk, ffprobe\nfrom .util import BytesIO  # type: ignore\nfrom .util import (\n    FFMPEG_URL,\n    VF_CAREFUL,\n    Cooldown,\n    Daemon,\n    afsenc,\n    atomic_move,\n    fsenc,\n    min_ex,\n    runcmd,\n    statdir,\n    ub64enc,\n    vsplit,\n    wunlink,\n)\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Any, Callable, Optional, Union\n\nif TYPE_CHECKING:\n    from .svchub import SvcHub\n\nif PY2:\n    range = xrange  # type: ignore\n\nHAVE_PIL = False\nHAVE_PILF = False\nH_PIL_HEIF = False\nH_PIL_AVIF = False\nH_PIL_WEBP = False\nH_PIL_JXL = False\n\nTH_CH = {\"j\": \"jpg\", \"p\": \"png\", \"w\": \"webp\", \"x\": \"jxl\"}\n\nEXTS_TH = set([\"jpg\", \"webp\", \"jxl\", \"png\"])\nEXTS_AC = set([\"opus\", \"owa\", \"caf\", \"mp3\", \"flac\", \"wav\"])\nEXTS_SPEC_SAFE = set(\"aif aiff flac mp3 opus wav\".split())\n\nPTN_TS = re.compile(\"^-?[0-9a-f]{8,10}$\")\n\n# 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\n# filesize-equivalent, not quality (ff looks much shittier)\nFF_JPG_Q = {\n    0: b\"30\",  # 0\n    1: b\"30\",  # 5\n    2: b\"30\",  # 10\n    3: b\"30\",  # 15\n    4: b\"28\",  # 20\n    5: b\"21\",  # 25\n    6: b\"17\",  # 30\n    7: b\"15\",  # 35\n    8: b\"13\",  # 40\n    9: b\"12\",  # 45\n    10: b\"11\",  # 50\n    11: b\"10\",  # 55\n    12: b\"9\",  # 60\n    13: b\"8\",  # 65\n    14: b\"7\",  # 70\n    15: b\"6\",  # 75\n    16: b\"5\",  # 80\n    17: b\"4\",  # 85\n    18: b\"3\",  # 90\n    19: b\"2\",  # 95\n    20: b\"2\",  # 100\n}\n# FF_JPG_Q = {xn: (\"%d\" % (xn,)).encode(\"ascii\") for xn in range(2, 33)}\nVIPS_JPG_Q = {\n    0: 4,  # 0\n    1: 7,  # 5\n    2: 12,  # 10\n    3: 17,  # 15\n    4: 22,  # 20\n    5: 27,  # 25\n    6: 32,  # 30\n    7: 37,  # 35\n    8: 42,  # 40\n    9: 47,  # 45\n    10: 52,  # 50\n    11: 56,  # 55\n    12: 61,  # 60\n    13: 66,  # 65\n    14: 71,  # 70\n    15: 75,  # 75\n    16: 80,  # 80\n    17: 85,  # 85\n    18: 89,  # 90 (vips explodes past this point)\n    19: 91,  # 95\n    20: 97,  # 100\n}\n\n\ntry:\n    if os.environ.get(\"PRTY_NO_PIL\"):\n        raise Exception()\n\n    from PIL import ExifTags, Image, ImageFont, ImageOps\n\n    HAVE_PIL = True\n    try:\n        if os.environ.get(\"PRTY_NO_PILF\"):\n            raise Exception()\n\n        ImageFont.load_default(size=16)\n        HAVE_PILF = True\n    except:\n        pass\n\n    try:\n        if os.environ.get(\"PRTY_NO_PIL_WEBP\"):\n            raise Exception()\n\n        Image.new(\"RGB\", (2, 2)).save(BytesIO(), format=\"webp\")\n        H_PIL_WEBP = True\n    except:\n        pass\n\n    try:\n        if os.environ.get(\"PRTY_NO_PIL_JXL\"):\n            raise Exception()\n\n        try:\n            import pillow_jxl\n        except ImportError:\n            pass\n\n        Image.new(\"RGB\", (2, 2)).save(BytesIO(), format=\"jxl\")\n        H_PIL_JXL = True\n    except:\n        pass\n\n    try:\n        if os.environ.get(\"PRTY_NO_PIL_HEIF\"):\n            raise Exception()\n\n        try:\n            from pillow_heif import register_heif_opener\n        except ImportError:\n            from pyheif_pillow_opener import register_heif_opener\n\n        register_heif_opener()\n        H_PIL_HEIF = True\n    except:\n        pass\n\n    try:\n        if os.environ.get(\"PRTY_NO_PIL_AVIF\"):\n            raise Exception()\n\n        if \".avif\" in Image.registered_extensions():\n            H_PIL_AVIF = True\n            raise Exception()\n\n        import pillow_avif  # noqa: F401  # pylint: disable=unused-import\n\n        H_PIL_AVIF = True\n    except:\n        pass\n\n    logging.getLogger(\"PIL\").setLevel(logging.WARNING)\nexcept:\n    pass\n\ntry:\n    if os.environ.get(\"PRTY_NO_VIPS\"):\n        raise ImportError()\n\n    HAVE_VIPS = True\n    import pyvips\n\n    pyvips.cache_set_max(0)\n    logging.getLogger(\"pyvips\").setLevel(logging.WARNING)\nexcept Exception as e:\n    HAVE_VIPS = False\n    if not isinstance(e, ImportError):\n        logging.warning(\"libvips found, but failed to load: \" + str(e))\n\n\ntry:\n    if os.environ.get(\"PRTY_NO_RAW\"):\n        raise Exception()\n\n    HAVE_RAW = True\n    import rawpy\n\n    logging.getLogger(\"rawpy\").setLevel(logging.WARNING)\nexcept:\n    HAVE_RAW = False\n\n\nth_dir_cache = {}\n\n\ndef thumb_path(histpath: str, rem: str, mtime: float, fmt: str, ffa: set[str]) -> str:\n    # base16 = 16 = 256\n    # b64-lc = 38 = 1444\n    # base64 = 64 = 4096\n    rd, fn = vsplit(rem)\n    if not rd:\n        rd = \"\\ntop\"\n\n    # spectrograms are never cropped; strip fullsize flag\n    ext = rem.split(\".\")[-1].lower()\n    if ext in ffa and fmt[:2] in (\"wf\", \"jf\", \"xf\"):\n        fmt = fmt.replace(\"f\", \"\")\n\n    dcache = th_dir_cache\n    rd_key = rd + \"\\n\" + fmt\n    rd = dcache.get(rd_key)\n    if not rd:\n        h = hashlib.sha512(afsenc(rd_key)).digest()\n        b64 = ub64enc(h).decode(\"ascii\")[:24]\n        rd = (\"%s/%s/\" % (b64[:2], b64[2:4])).lower() + b64\n        if len(dcache) > 9001:\n            dcache.clear()\n        dcache[rd_key] = rd\n\n    # could keep original filenames but this is safer re pathlen\n    h = hashlib.sha512(afsenc(fn)).digest()\n    fn = ub64enc(h).decode(\"ascii\")[:24]\n\n    if fmt in EXTS_AC:\n        cat = \"ac\"\n    else:\n        fmt = TH_CH[fmt[:1]]\n        cat = \"th\"\n\n    return \"%s/%s/%s/%s.%x.%s\" % (histpath, cat, rd, fn, int(mtime), fmt)\n\n\nclass ThumbSrv(object):\n    def __init__(self, hub: \"SvcHub\") -> None:\n        self.hub = hub\n        self.asrv = hub.asrv\n        self.args = hub.args\n        self.log_func = hub.log\n\n        self.poke_cd = Cooldown(self.args.th_poke)\n\n        self.mutex = threading.Lock()\n        self.busy: dict[str, list[threading.Condition]] = {}\n        self.untemp: dict[str, list[str]] = {}\n        self.ram: dict[str, float] = {}\n        self.memcond = threading.Condition(self.mutex)\n        self.stopping = False\n        self.rm_nullthumbs = True  # forget failed conversions on startup\n        self.nthr = max(1, self.args.th_mt)\n\n        self.exts_spec_unsafe = set(self.args.th_spec_cnv.split(\",\"))\n\n        self.q: Queue[Optional[tuple[str, str, str, VFS]]] = Queue(self.nthr * 4)\n        for n in range(self.nthr):\n            Daemon(self.worker, \"thumb-{}-{}\".format(n, self.nthr))\n\n        want_ff = not self.args.no_vthumb or not self.args.no_athumb\n        if want_ff and (not HAVE_FFMPEG or not HAVE_FFPROBE):\n            missing = []\n            if not HAVE_FFMPEG:\n                missing.append(\"FFmpeg\")\n\n            if not HAVE_FFPROBE:\n                missing.append(\"FFprobe\")\n\n            msg = \"cannot create audio/video thumbnails because some of the required programs are not available: \"\n            msg += \", \".join(missing)\n            self.log(msg, c=3)\n            if ANYWIN and self.args.no_acode:\n                self.log(\"download FFmpeg to fix it:\\033[0m \" + FFMPEG_URL, 3)\n\n        if self.args.th_clean:\n            Daemon(self.cleaner, \"thumb.cln\")\n\n        (\n            self.fmt_pil,\n            self.fmt_vips,\n            self.fmt_raw,\n            self.fmt_ffi,\n            self.fmt_ffv,\n            self.fmt_ffa,\n        ) = [\n            set(y.split(\",\"))\n            for y in [\n                self.args.th_r_pil,\n                self.args.th_r_vips,\n                self.args.th_r_raw,\n                self.args.th_r_ffi,\n                self.args.th_r_ffv,\n                self.args.th_r_ffa,\n            ]\n        ]\n\n        if not H_PIL_HEIF:\n            for f in \"heif heifs heic heics\".split(\" \"):\n                self.fmt_pil.discard(f)\n\n        if not H_PIL_AVIF:\n            for f in \"avif avifs\".split(\" \"):\n                self.fmt_pil.discard(f)\n\n        if not H_PIL_WEBP:\n            for f in \"webp\".split(\" \"):\n                self.fmt_pil.discard(f)\n\n        if not H_PIL_JXL:\n            for f in \"jxl\".split(\" \"):\n                self.fmt_pil.discard(f)\n\n        self.thumbable: set[str] = set()\n\n        if \"pil\" in self.args.th_dec:\n            self.thumbable |= self.fmt_pil\n\n        if \"vips\" in self.args.th_dec:\n            self.thumbable |= self.fmt_vips\n\n        if \"raw\" in self.args.th_dec:\n            self.thumbable |= self.fmt_raw\n\n        if \"ff\" in self.args.th_dec:\n            for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:\n                self.thumbable |= zss\n\n    def log(self, msg: str, c: Union[int, str] = 0) -> None:\n        self.log_func(\"thumb\", msg, c)\n\n    def shutdown(self) -> None:\n        self.stopping = True\n        Daemon(self._fire_sentinels, \"thumbstopper\")\n\n    def _fire_sentinels(self):\n        for _ in range(self.nthr):\n            self.q.put(None)\n\n    def stopped(self) -> bool:\n        with self.mutex:\n            return not self.nthr\n\n    def getres(self, vn: VFS, fmt: str) -> tuple[int, int]:\n        mul = 3 if \"3\" in fmt else 1\n        w, h = vn.flags[\"thsize\"].split(\"x\")\n        return int(w) * mul, int(h) * mul\n\n    def get(self, ptop: str, rem: str, mtime: float, fmt: str) -> Optional[str]:\n        histpath = self.asrv.vfs.histtab.get(ptop)\n        if not histpath:\n            self.log(\"no histpath for %r\" % (ptop,))\n            return None\n\n        tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa)\n        abspath = os.path.join(ptop, rem)\n        cond = threading.Condition(self.mutex)\n        do_conv = False\n        with self.mutex:\n            try:\n                self.busy[tpath].append(cond)\n                self.log(\"joined waiting room for %r\" % (tpath,))\n            except:\n                thdir = os.path.dirname(tpath)\n                chmod = bos.MKD_700 if self.args.free_umask else bos.MKD_755\n                bos.makedirs(os.path.join(thdir, \"w\"), vf=chmod)\n\n                inf_path = os.path.join(thdir, \"dir.txt\")\n                if not bos.path.exists(inf_path):\n                    with open(inf_path, \"wb\") as f:\n                        f.write(afsenc(os.path.dirname(abspath)))\n                    self.writevolcfg(histpath)\n\n                self.busy[tpath] = [cond]\n                do_conv = True\n\n        if do_conv:\n            allvols = list(self.asrv.vfs.all_vols.values())\n            vn = next((x for x in allvols if x.realpath == ptop), None)\n            if not vn:\n                self.log(\"ptop %r not in %s\" % (ptop, allvols), 3)\n                vn = self.asrv.vfs.all_aps[0][1][0]\n\n            self.q.put((abspath, tpath, fmt, vn))\n            self.log(\"conv %r :%s \\033[0m%r\" % (tpath, fmt, abspath), 6)\n\n        while not self.stopping:\n            with self.mutex:\n                if tpath not in self.busy:\n                    break\n\n            with cond:\n                cond.wait(3)\n\n        try:\n            st = bos.stat(tpath)\n            if st.st_size:\n                self.poke(tpath)\n                return tpath\n        except:\n            pass\n\n        return None\n\n    def getcfg(self) -> dict[str, set[str]]:\n        return {\n            \"thumbable\": self.thumbable,\n            \"pil\": self.fmt_pil,\n            \"vips\": self.fmt_vips,\n            \"raw\": self.fmt_raw,\n            \"ffi\": self.fmt_ffi,\n            \"ffv\": self.fmt_ffv,\n            \"ffa\": self.fmt_ffa,\n        }\n\n    def volcfgi(self, vn: VFS) -> str:\n        ret = []\n        zs = \"th_dec th_no_webp th_no_jpg\"\n        for zs in zs.split(\" \"):\n            ret.append(\"%s(%s)\\n\" % (zs, getattr(self.args, zs)))\n        zs = \"th_qv th_qvx thsize th_spec_p convt\"\n        for zs in zs.split(\" \"):\n            ret.append(\"%s(%s)\\n\" % (zs, vn.flags.get(zs)))\n        return \"\".join(ret)\n\n    def volcfga(self, vn: VFS) -> str:\n        ret = []\n        zs = \"q_opus q_mp3\"\n        for zs in zs.split(\" \"):\n            ret.append(\"%s(%s)\\n\" % (zs, getattr(self.args, zs)))\n        zs = \"aconvt\"\n        for zs in zs.split(\" \"):\n            ret.append(\"%s(%s)\\n\" % (zs, vn.flags.get(zs)))\n        return \"\".join(ret)\n\n    def writevolcfg(self, histpath: str) -> None:\n        try:\n            bos.stat(os.path.join(histpath, \"th\", \"cfg.txt\"))\n            bos.stat(os.path.join(histpath, \"ac\", \"cfg.txt\"))\n            return\n        except:\n            pass\n        cfgi = cfga = \"\"\n        for vn in self.asrv.vfs.all_vols.values():\n            if vn.histpath == histpath:\n                cfgi = self.volcfgi(vn)\n                cfga = self.volcfga(vn)\n                break\n        t = \"writing thumbnailer-config %d,%d to %s\"\n        self.log(t % (len(cfgi), len(cfga), histpath))\n        chmod = bos.MKD_700 if self.args.free_umask else bos.MKD_755\n        for cfg, cat in ((cfgi, \"th\"), (cfga, \"ac\")):\n            bos.makedirs(os.path.join(histpath, cat), vf=chmod)\n            with open(os.path.join(histpath, cat, \"cfg.txt\"), \"wb\") as f:\n                f.write(cfg.encode(\"utf-8\"))\n\n    def wait4ram(self, need: float, ttpath: str) -> None:\n        ram = self.args.th_ram_max\n        if need > ram * 0.99:\n            t = \"file too big; need %.2f GiB RAM, but --th-ram-max is only %.1f\"\n            raise Exception(t % (need, ram))\n\n        while True:\n            with self.mutex:\n                used = sum([v for k, v in self.ram.items() if k != ttpath]) + need\n                if used < ram:\n                    # self.log(\"XXX self.ram: %s\" % (self.ram,), 5)\n                    self.ram[ttpath] = need\n                    return\n            with self.memcond:\n                # self.log(\"at RAM limit; used %.2f GiB, need %.2f more\" % (used-need, need), 1)\n                self.memcond.wait(3)\n\n    def worker(self) -> None:\n        while not self.stopping:\n            task = self.q.get()\n            if not task:\n                break\n\n            abspath, tpath, fmt, vn = task\n            ext = abspath.split(\".\")[-1].lower()\n            png_ok = False\n            funs = []\n\n            if ext in self.args.au_unpk:\n                ap_unpk = au_unpk(self.log, self.args.au_unpk, abspath, vn)\n            else:\n                ap_unpk = abspath\n\n            if ap_unpk and not bos.path.exists(tpath):\n                tex = tpath.rsplit(\".\", 1)[-1]\n                want_mp3 = tex == \"mp3\"\n                want_opus = tex in (\"opus\", \"owa\", \"caf\")\n                want_flac = tex == \"flac\"\n                want_wav = tex == \"wav\"\n                want_png = tex == \"png\"\n                want_au = want_mp3 or want_opus or want_flac or want_wav\n                for lib in self.args.th_dec:\n                    can_au = lib == \"ff\" and (\n                        ext in self.fmt_ffa or ext in self.fmt_ffv\n                    )\n\n                    if lib == \"pil\" and ext in self.fmt_pil and tex in self.fmt_pil:\n                        funs.append(self.conv_pil)\n                    elif lib == \"vips\" and ext in self.fmt_vips:\n                        funs.append(self.conv_vips)\n                    elif lib == \"raw\" and ext in self.fmt_raw:\n                        funs.append(self.conv_raw)\n                    elif can_au and (want_png or want_au):\n                        if want_opus:\n                            funs.append(self.conv_opus)\n                        elif want_mp3:\n                            funs.append(self.conv_mp3)\n                        elif want_flac:\n                            funs.append(self.conv_flac)\n                        elif want_wav:\n                            funs.append(self.conv_wav)\n                        elif want_png:\n                            funs.append(self.conv_waves)\n                            png_ok = True\n                    elif lib == \"ff\" and (ext in self.fmt_ffi or ext in self.fmt_ffv):\n                        funs.append(self.conv_ffmpeg)\n                    elif lib == \"ff\" and ext in self.fmt_ffa and not want_au:\n                        funs.append(self.conv_spec)\n\n            tdir, tfn = os.path.split(tpath)\n            ttpath = os.path.join(tdir, \"w\", tfn)\n            try:\n                wunlink(self.log, ttpath, vn.flags)\n            except:\n                pass\n\n            conv_ok = False\n            for fun in funs:\n                try:\n                    if not png_ok and tpath.endswith(\".png\"):\n                        raise Exception(\"png only allowed for waveforms\")\n\n                    fun(ap_unpk, ttpath, fmt, vn)\n                    conv_ok = True\n                    break\n                except Exception as ex:\n                    r321 = getattr(ex, \"returncode\", 0) == 321\n                    msg = \"%s could not create thumbnail of %r\\n%s\"\n                    msg = msg % (fun.__name__, abspath, ex if r321 else min_ex())\n                    c: Union[str, int] = 1 if \"<Signals.SIG\" in msg else \"90\"\n                    self.log(msg, c)\n                    if not r321:\n                        if fun == funs[-1]:\n                            try:\n                                with open(ttpath, \"wb\") as _:\n                                    pass\n                            except Exception as ex:\n                                t = \"failed to create the file [%s]: %r\"\n                                self.log(t % (ttpath, ex), 3)\n                    else:\n                        # ffmpeg may spawn empty files on windows\n                        try:\n                            wunlink(self.log, ttpath, vn.flags)\n                        except:\n                            pass\n\n            if abspath != ap_unpk and ap_unpk:\n                wunlink(self.log, ap_unpk, vn.flags)\n\n            try:\n                atomic_move(self.log, ttpath, tpath, vn.flags)\n            except Exception as ex:\n                if conv_ok and not os.path.exists(tpath):\n                    t = \"failed to move  [%s]  to  [%s]:  %r\"\n                    self.log(t % (ttpath, tpath, ex), 3)\n                elif not conv_ok:\n                    try:\n                        open(tpath, \"ab\").close()\n                    except:\n                        pass\n\n            untemp = []\n            with self.mutex:\n                subs = self.busy[tpath]\n                del self.busy[tpath]\n                self.ram.pop(ttpath, None)\n                untemp = self.untemp.pop(ttpath, None) or []\n\n            for ap in untemp:\n                try:\n                    wunlink(self.log, ap, VF_CAREFUL)\n                except:\n                    pass\n\n            for x in subs:\n                with x:\n                    x.notify_all()\n\n            with self.memcond:\n                self.memcond.notify_all()\n\n        with self.mutex:\n            self.nthr -= 1\n\n    def fancy_pillow(self, im: \"Image.Image\", fmt: str, vn: VFS) -> \"Image.Image\":\n        # exif_transpose is expensive (loads full image + unconditional copy)\n        res = self.getres(vn, fmt)\n        r = max(*res) * 2\n        im.thumbnail((r, r), resample=Image.LANCZOS)\n        try:\n            k = next(k for k, v in ExifTags.TAGS.items() if v == \"Orientation\")\n            exif = im.getexif()\n            rot = int(exif[k])\n            del exif[k]\n        except:\n            rot = 1\n\n        rots = {8: Image.ROTATE_90, 3: Image.ROTATE_180, 6: Image.ROTATE_270}\n        if rot in rots:\n            im = im.transpose(rots[rot])\n\n        if \"f\" in fmt:\n            im.thumbnail(res, resample=Image.LANCZOS)\n        else:\n            iw, ih = im.size\n            dw, dh = res\n            res = (min(iw, dw), min(ih, dh))\n            im = ImageOps.fit(im, res, method=Image.LANCZOS)\n\n        return im\n\n    def conv_image_pil(self, im: \"Image.Image\", tpath: str, fmt: str, vn: VFS) -> None:\n        try:\n            im = self.fancy_pillow(im, fmt, vn)\n        except Exception as ex:\n            self.log(\"fancy_pillow {}\".format(ex), \"90\")\n            im.thumbnail(self.getres(vn, fmt))\n\n        fmts = [\"RGB\", \"L\"]\n        zs = \"th_qvx\" if tpath.endswith(\".jxl\") else \"th_qv\"\n        args = {\"quality\": vn.flags[zs]}\n\n        if tpath.endswith(\".webp\"):\n            # quality 80 = pillow-default\n            # quality 75 = ffmpeg-default\n            # method 0 = pillow-default, fast\n            # method 4 = ffmpeg-default\n            # method 6 = max, slow\n            fmts.extend((\"RGBA\", \"LA\"))\n            args[\"method\"] = 6\n        else:\n            # default q = 75\n            args[\"progressive\"] = True\n\n        if im.mode not in fmts:\n            # print(\"conv {}\".format(im.mode))\n            im = im.convert(\"RGB\")\n\n        im.save(tpath, **args)\n\n    def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:\n        self.wait4ram(0.2, tpath)\n        with Image.open(fsenc(abspath)) as im:\n            self.conv_image_pil(im, tpath, fmt, vn)\n\n    def conv_image_vips(\n        self, loader: \"Callable[[int, dict], Any]\", tpath: str, fmt: str, vn: VFS\n    ) -> None:\n        crops = [\"centre\", \"none\"]\n        if \"f\" in fmt:\n            crops = [\"none\"]\n\n        w, h = self.getres(vn, fmt)\n        kw = {\"height\": h, \"size\": \"down\", \"intent\": \"relative\"}\n\n        img = None\n        for c in crops:\n            try:\n                kw[\"crop\"] = c\n                img = loader(w, kw)\n                break\n            except:\n                if c == crops[-1]:\n                    raise\n\n        assert img  # type: ignore  # !rm\n        args = {}\n        qv = vn.flags[\"th_qv\"]\n        if tpath.endswith(\"jpg\"):\n            qv = VIPS_JPG_Q[qv // 5]\n            args[\"optimize_coding\"] = True\n        elif tpath.endswith(\"jxl\"):\n            qv = vn.flags[\"th_qvx\"]\n            # args[\"effort\"] = 8\n            #  `- not worth it; twice as slow, size drops 12%, no visual improvement unlike ffmpeg\n        img.write_to_file(tpath, Q=qv, strip=True, **args)\n        img.invalidate()\n\n    def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:\n        self.wait4ram(0.2, tpath)\n\n        def _loader(w: int, kw: dict) -> Any:\n            return pyvips.Image.thumbnail(abspath, w, **kw)\n\n        self.conv_image_vips(_loader, tpath, fmt, vn)\n\n    def conv_raw(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:\n        self.wait4ram(0.2, tpath)\n        with rawpy.imread(abspath) as raw:\n            thumb = raw.extract_thumb()\n        if thumb.format == rawpy.ThumbFormat.JPEG and tpath.endswith(\".jpg\"):\n            # if we have a jpg thumbnail and no webp output is available,\n            # just write the jpg directly (it'll be the wrong size, but it's fast)\n            with open(tpath, \"wb\") as f:\n                f.write(thumb.data)\n        if HAVE_VIPS:\n\n            def _loader(w: int, kw: dict) -> Any:\n                if thumb.format == rawpy.ThumbFormat.BITMAP:\n                    img = pyvips.Image.new_from_array(thumb.data, interpretation=\"rgb\")\n                    return img.thumbnail_image(w, **kw)\n                else:\n                    return pyvips.Image.thumbnail_buffer(thumb.data, w, **kw)\n\n            self.conv_image_vips(_loader, tpath, fmt, vn)\n        elif HAVE_PIL:\n            if thumb.format == rawpy.ThumbFormat.BITMAP:\n                im = Image.fromarray(thumb.data, \"RGB\")\n            else:\n                im = Image.open(io.BytesIO(thumb.data))\n            self.conv_image_pil(im, tpath, fmt, vn)\n        else:\n            raise Exception(\n                \"either pil or vips is needed to process embedded bitmap thumbnails in raw files\"\n            )\n\n    def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:\n        self.wait4ram(0.2, tpath)\n        ret, _, _, _ = ffprobe(abspath, int(vn.flags[\"convt\"] / 2))\n        if not ret:\n            return\n\n        ext = abspath.rsplit(\".\")[-1].lower()\n        if ext in [\"h264\", \"h265\"] or ext in self.fmt_ffi:\n            seek: list[bytes] = []\n        else:\n            dur = ret[\".dur\"][1] if \".dur\" in ret else 4\n            seek = [b\"-ss\", \"{:.0f}\".format(dur / 3).encode(\"utf-8\")]\n\n        self._ffmpeg_im(abspath, tpath, fmt, vn, seek, b\"0:v:0\")\n\n    def _ffmpeg_im(\n        self,\n        abspath: str,\n        tpath: str,\n        fmt: str,\n        vn: VFS,\n        seek: list[bytes],\n        imap: bytes,\n    ) -> None:\n        scale = \"scale={0}:{1}:force_original_aspect_ratio=\"\n        if \"f\" in fmt:\n            scale += \"decrease,setsar=1:1\"\n        else:\n            scale += \"increase,crop={0}:{1},setsar=1:1\"\n\n        res = self.getres(vn, fmt)\n        bscale = scale.format(*list(res)).encode(\"utf-8\")\n        # fmt: off\n        cmd = [\n            b\"ffmpeg\",\n            b\"-nostdin\",\n            b\"-v\", b\"error\",\n            b\"-hide_banner\"\n        ]\n        cmd += seek\n        cmd += [\n            b\"-i\", fsenc(abspath),\n            b\"-map\", imap,\n            b\"-vf\", bscale,\n            b\"-frames:v\", b\"1\",\n            b\"-metadata:s:v:0\", b\"rotate=0\",\n        ]\n        # fmt: on\n\n        self._ffmpeg_im_o(tpath, vn, cmd)\n\n    def _ffmpeg_im_o(self, tpath: str, vn: VFS, cmd: list[bytes]) -> None:\n        if tpath.endswith(\".jpg\"):\n            cmd += [\n                b\"-q:v\",\n                FF_JPG_Q[vn.flags[\"th_qv\"] // 5],  # default=??\n            ]\n        elif tpath.endswith(\".jxl\"):\n            cmd += [\n                b\"-q:v\",\n                unicode(vn.flags[\"th_qvx\"]).encode(\"ascii\"),  # default=??\n                b\"-effort:v\",\n                b\"8\",  # default=7, 1=fast, 9=max, 9~=8 but slower\n            ]\n        else:\n            cmd += [\n                b\"-q:v\",\n                unicode(vn.flags[\"th_qv\"]).encode(\"ascii\"),  # default=75\n                b\"-compression_level:v\",\n                b\"6\",  # default=4, 0=fast, 6=max\n            ]\n\n        cmd += [fsenc(tpath)]\n        self._run_ff(cmd, vn, \"convt\")\n\n    def _run_ff(self, cmd: list[bytes], vn: VFS, kto: str, oom: int = 400) -> None:\n        # self.log((b\" \".join(cmd)).decode(\"utf-8\"))\n        ret, _, serr = runcmd(cmd, timeout=vn.flags[kto], nice=True, oom=oom)\n        if not ret:\n            return\n\n        c: Union[str, int] = \"90\"\n        t = \"FFmpeg failed (probably a corrupt file):\\n\"\n\n        if \"but no decoder found for: hevc\" in serr:\n            t = \"thumbnail cannot be created due to legal reasons; https://github.com/9001/copyparty/blob/hovudstraum/docs/bad-codecs.md \\033[0;90m\\n\"\n            ret = 321\n            c = 3\n\n        elif (\n            (not self.args.th_ff_jpg or time.time() - int(self.args.th_ff_jpg) < 60)\n            and cmd[-1].lower().endswith(b\".webp\")\n            and (\n                \"Error selecting an encoder\" in serr\n                or \"Automatic encoder selection failed\" in serr\n                or \"Default encoder for format webp\" in serr\n                or \"Please choose an encoder manually\" in serr\n            )\n        ):\n            self.args.th_ff_jpg = time.time()\n            t = \"FFmpeg failed because it was compiled without libwebp; enabling --th-ff-jpg to force jpeg output:\\n\"\n            ret = 321\n            c = 1\n\n        elif (\n            not self.args.th_ff_swr or time.time() - int(self.args.th_ff_swr) < 60\n        ) and (\n            \"Requested resampling engine is unavailable\" in serr\n            or \"output pad on Parsed_aresample_\" in serr\n        ):\n            self.args.th_ff_swr = time.time()\n            t = \"FFmpeg failed because it was compiled without libsox; enabling --th-ff-swr to force swr resampling:\\n\"\n            ret = 321\n            c = 1\n\n        lines = serr.strip(\"\\n\").split(\"\\n\")\n        if len(lines) > 50:\n            lines = lines[:25] + [\"[...]\"] + lines[-25:]\n\n        txt = \"\\n\".join([\"ff: \" + unicode(x) for x in lines])\n        if len(txt) > 5000:\n            txt = txt[:2500] + \"...\\nff: [...]\\nff: ...\" + txt[-2500:]\n\n        try:\n            zs = shlex.join([x.decode(\"utf-8\", \"replace\") for x in cmd])\n        except:\n            zs = \"'\" + (b\"' '\".join(cmd)).decode(\"utf-8\", \"replace\") + \"'\"\n\n        self.log(\"%scmd: %s\\n%s\" % (t, zs, txt), c=c)\n        raise sp.CalledProcessError(ret, (cmd[0], b\"...\", cmd[-1]))\n\n    def conv_waves(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:\n        ret, _, _, _ = ffprobe(abspath, int(vn.flags[\"convt\"] / 2))\n        if \"ac\" not in ret:\n            raise Exception(\"not audio\")\n\n        # jt_versi.xm: 405M/839s\n        dur = ret[\".dur\"][1] if \".dur\" in ret else 300\n        need = 0.2 + dur / 3000\n        speedup = b\"\"\n        if need > self.args.th_ram_max * 0.7:\n            self.log(\"waves too big (need %.2f GiB); trying to optimize\" % (need,))\n            need = 0.2 + dur / 4200  # only helps about this much...\n            speedup = b\"aresample=8000,\"\n        if need > self.args.th_ram_max * 0.96:\n            raise Exception(\"file too big; cannot waves\")\n\n        self.wait4ram(need, tpath)\n\n        flt = b\"[0:a:0]\" + speedup\n        flt += (\n            b\"compand=.3|.3:1|1:-90/-60|-60/-40|-40/-30|-20/-20:6:0:-90:0.2\"\n            b\",volume=2\"\n            b\",showwavespic=s=2048x64:colors=white\"\n            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\n        )\n\n        # fmt: off\n        cmd = [\n            b\"ffmpeg\",\n            b\"-nostdin\",\n            b\"-v\", b\"error\",\n            b\"-hide_banner\",\n            b\"-i\", fsenc(abspath),\n            b\"-filter_complex\", flt,\n            b\"-frames:v\", b\"1\",\n        ]\n        # fmt: on\n\n        cmd += [fsenc(tpath)]\n        self._run_ff(cmd, vn, \"convt\")\n\n        if \"pngquant\" in vn.flags:\n            wtpath = tpath + \".png\"\n            cmd = [\n                b\"pngquant\",\n                b\"--strip\",\n                b\"--nofs\",\n                b\"--output\",\n                fsenc(wtpath),\n                fsenc(tpath),\n            ]\n            ret = runcmd(cmd, timeout=vn.flags[\"convt\"], nice=True, oom=400)[0]\n            if ret:\n                try:\n                    wunlink(self.log, wtpath, vn.flags)\n                except:\n                    pass\n            else:\n                atomic_move(self.log, wtpath, tpath, vn.flags)\n\n    def conv_emb_cv(\n        self, abspath: str, tpath: str, fmt: str, vn: VFS, strm: dict[str, Any]\n    ) -> None:\n        self.wait4ram(0.2, tpath)\n        self._ffmpeg_im(\n            abspath, tpath, fmt, vn, [], b\"0:\" + strm[\"index\"].encode(\"ascii\")\n        )\n\n    def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:\n        ret, raw, strms, ctnr = ffprobe(abspath, int(vn.flags[\"convt\"] / 2))\n        if \"ac\" not in ret:\n            raise Exception(\"not audio\")\n\n        want_spec = vn.flags.get(\"th_spec_p\", 1)\n        if want_spec < 2:\n            for strm in strms:\n                if (\n                    strm.get(\"codec_type\") == \"video\"\n                    and strm.get(\"DISPOSITION:attached_pic\") == \"1\"\n                ):\n                    return self.conv_emb_cv(abspath, tpath, fmt, vn, strm)\n\n        if not want_spec:\n            raise Exception(\"spectrograms forbidden by volflag\")\n\n        fext = abspath.split(\".\")[-1].lower()\n\n        # https://trac.ffmpeg.org/ticket/10797\n        # expect 1 GiB every 600 seconds when duration is tricky;\n        # simple filetypes are generally safer so let's special-case those\n        coeff = 1800 if fext in EXTS_SPEC_SAFE else 600\n        dur = ret[\".dur\"][1] if \".dur\" in ret else 900\n        need = 0.2 + dur / coeff\n        self.wait4ram(need, tpath)\n\n        infile = abspath\n        if dur >= 900 or fext in self.exts_spec_unsafe:\n            with tempfile.NamedTemporaryFile(suffix=\".spec.flac\", delete=False) as f:\n                f.write(b\"h\")\n                infile = f.name\n                try:\n                    self.untemp[tpath].append(infile)\n                except:\n                    self.untemp[tpath] = [infile]\n\n            # fmt: off\n            cmd = [\n                b\"ffmpeg\",\n                b\"-nostdin\",\n                b\"-v\", b\"error\",\n                b\"-hide_banner\",\n                b\"-i\", fsenc(abspath),\n                b\"-map\", b\"0:a:0\",\n                b\"-ac\", b\"1\",\n                b\"-ar\", b\"48000\",\n                b\"-sample_fmt\", b\"s16\",\n                b\"-t\", b\"900\",\n                b\"-y\", fsenc(infile),\n            ]\n            # fmt: on\n            self._run_ff(cmd, vn, \"convt\")\n\n        fc = \"[0:a:0]aresample=48000{},showspectrumpic=s=\"\n        if \"3\" in fmt:\n            fc += \"1280x1024,crop=1420:1056:70:48[o]\"\n        else:\n            fc += \"640x512,crop=780:544:70:48[o]\"\n\n        if self.args.th_ff_swr:\n            fco = \":filter_size=128:cutoff=0.877\"\n        else:\n            fco = \":resampler=soxr\"\n\n        fc = fc.format(fco)\n\n        # fmt: off\n        cmd = [\n            b\"ffmpeg\",\n            b\"-nostdin\",\n            b\"-v\", b\"error\",\n            b\"-hide_banner\",\n            b\"-i\", fsenc(infile),\n            b\"-filter_complex\", fc.encode(\"utf-8\"),\n            b\"-map\", b\"[o]\",\n            b\"-frames:v\", b\"1\",\n        ]\n        # fmt: on\n\n        self._ffmpeg_im_o(tpath, vn, cmd)\n\n    def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:\n        quality = self.args.q_mp3.lower()\n        if self.args.no_acode or not quality:\n            raise Exception(\"disabled in server config\")\n\n        self.wait4ram(0.2, tpath)\n        tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags[\"convt\"] / 2))\n        if \"ac\" not in tags:\n            raise Exception(\"not audio\")\n\n        if quality.endswith(\"k\"):\n            qk = b\"-b:a\"\n            qv = quality.encode(\"ascii\")\n        else:\n            qk = b\"-q:a\"\n            qv = quality[1:].encode(\"ascii\")\n\n        # extremely conservative choices for output format\n        # (always 2ch 44k1) because if a device is old enough\n        # to not support opus then it's probably also super picky\n\n        # fmt: off\n        cmd = [\n            b\"ffmpeg\",\n            b\"-nostdin\",\n            b\"-v\", b\"error\",\n            b\"-hide_banner\",\n            b\"-i\", fsenc(abspath),\n        ] + self.big_tags(rawtags) + [\n            b\"-map\", b\"0:a:0\",\n            b\"-ar\", b\"44100\",\n            b\"-ac\", b\"2\",\n            b\"-c:a\", b\"libmp3lame\",\n            qk, qv,\n            fsenc(tpath)\n        ]\n        # fmt: on\n        self._run_ff(cmd, vn, \"aconvt\", oom=300)\n\n    def conv_flac(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:\n        if self.args.no_acode or not self.args.allow_flac:\n            raise Exception(\"flac not permitted in server config\")\n\n        self.wait4ram(0.2, tpath)\n        tags, _, _, _ = ffprobe(abspath, int(vn.flags[\"convt\"] / 2))\n        if \"ac\" not in tags:\n            raise Exception(\"not audio\")\n\n        self.log(\"conv2 flac\", 6)\n\n        # fmt: off\n        cmd = [\n            b\"ffmpeg\",\n            b\"-nostdin\",\n            b\"-v\", b\"error\",\n            b\"-hide_banner\",\n            b\"-i\", fsenc(abspath),\n            b\"-map\", b\"0:a:0\",\n            b\"-c:a\", b\"flac\",\n            fsenc(tpath)\n        ]\n        # fmt: on\n        self._run_ff(cmd, vn, \"aconvt\", oom=300)\n\n    def conv_wav(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:\n        if self.args.no_acode or not self.args.allow_wav:\n            raise Exception(\"wav not permitted in server config\")\n\n        self.wait4ram(0.2, tpath)\n        tags, _, _, _ = ffprobe(abspath, int(vn.flags[\"convt\"] / 2))\n        if \"ac\" not in tags:\n            raise Exception(\"not audio\")\n\n        bits = tags[\".bps\"][1]\n        if bits == 0.0:\n            bits = tags[\".bprs\"][1]\n\n        codec = b\"pcm_s32le\"\n        if bits <= 16.0:\n            codec = b\"pcm_s16le\"\n        elif bits <= 24.0:\n            codec = b\"pcm_s24le\"\n\n        self.log(\"conv2 wav\", 6)\n\n        # fmt: off\n        cmd = [\n            b\"ffmpeg\",\n            b\"-nostdin\",\n            b\"-v\", b\"error\",\n            b\"-hide_banner\",\n            b\"-i\", fsenc(abspath),\n            b\"-map\", b\"0:a:0\",\n            b\"-c:a\", codec,\n            fsenc(tpath)\n        ]\n        # fmt: on\n        self._run_ff(cmd, vn, \"aconvt\", oom=300)\n\n    def conv_opus(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:\n        if self.args.no_acode or not self.args.q_opus:\n            raise Exception(\"disabled in server config\")\n\n        self.wait4ram(0.2, tpath)\n        tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags[\"convt\"] / 2))\n        if \"ac\" not in tags:\n            raise Exception(\"not audio\")\n\n        sq = \"%dk\" % (self.args.q_opus,)\n        bq = sq.encode(\"ascii\")\n        if tags[\"ac\"][1] == \"opus\":\n            enc = \"-c:a copy\"\n        else:\n            enc = \"-c:a libopus -b:a \" + sq\n\n        fun = self._conv_caf if fmt == \"caf\" else self._conv_owa\n\n        fun(abspath, tpath, tags, rawtags, enc, bq, vn)\n\n    def _conv_owa(\n        self,\n        abspath: str,\n        tpath: str,\n        tags: dict[str, tuple[int, Any]],\n        rawtags: dict[str, list[Any]],\n        enc: str,\n        bq: bytes,\n        vn: VFS,\n    ) -> None:\n        if tpath.endswith(\".owa\"):\n            container = b\"webm\"\n            tagset = [b\"-map_metadata\", b\"-1\"]\n        else:\n            container = b\"opus\"\n            tagset = self.big_tags(rawtags)\n\n        self.log(\"conv2 %s [%s]\" % (container, enc), 6)\n        benc = enc.encode(\"ascii\").split(b\" \")\n\n        # fmt: off\n        cmd = [\n            b\"ffmpeg\",\n            b\"-nostdin\",\n            b\"-v\", b\"error\",\n            b\"-hide_banner\",\n            b\"-i\", fsenc(abspath),\n        ] + tagset + [\n            b\"-map\", b\"0:a:0\",\n        ] + benc + [\n            b\"-f\", container,\n            fsenc(tpath)\n        ]\n        # fmt: on\n        self._run_ff(cmd, vn, \"aconvt\", oom=300)\n\n    def _conv_caf(\n        self,\n        abspath: str,\n        tpath: str,\n        tags: dict[str, tuple[int, Any]],\n        rawtags: dict[str, list[Any]],\n        enc: str,\n        bq: bytes,\n        vn: VFS,\n    ) -> None:\n        tmp_opus = tpath + \".opus\"\n        try:\n            wunlink(self.log, tmp_opus, vn.flags)\n        except:\n            pass\n\n        try:\n            dur = tags[\".dur\"][1]\n        except:\n            dur = 0\n\n        self.log(\"conv2 caf-tmp [%s]\" % (enc,), 6)\n        benc = enc.encode(\"ascii\").split(b\" \")\n\n        # fmt: off\n        cmd = [\n            b\"ffmpeg\",\n            b\"-nostdin\",\n            b\"-v\", b\"error\",\n            b\"-hide_banner\",\n            b\"-i\", fsenc(abspath),\n            b\"-map_metadata\", b\"-1\",\n            b\"-map\", b\"0:a:0\",\n        ] + benc + [\n            b\"-f\", b\"opus\",\n            fsenc(tmp_opus)\n        ]\n        # fmt: on\n        self._run_ff(cmd, vn, \"aconvt\", oom=300)\n\n        # iOS fails to play some \"insufficiently complex\" files\n        # (average file shorter than 8 seconds), so of course we\n        # fix that by mixing in some inaudible pink noise :^)\n        # 6.3 sec seems like the cutoff so lets do 7, and\n        # 7 sec of psyqui-musou.opus @ 3:50 is 174 KiB\n        sz = bos.path.getsize(tmp_opus)\n        if dur < 20 or sz < 256 * 1024:\n            zs = bq.decode(\"ascii\")\n            self.log(\"conv2 caf-transcode; dur=%d sz=%d q=%s\" % (dur, sz, zs), 6)\n            # fmt: off\n            cmd = [\n                b\"ffmpeg\",\n                b\"-nostdin\",\n                b\"-v\", b\"error\",\n                b\"-hide_banner\",\n                b\"-i\", fsenc(abspath),\n                b\"-filter_complex\", b\"anoisesrc=a=0.001:d=7:c=pink,asplit[l][r]; [l][r]amerge[s]; [0:a:0][s]amix\",\n                b\"-map_metadata\", b\"-1\",\n                b\"-ac\", b\"2\",\n                b\"-c:a\", b\"libopus\",\n                b\"-b:a\", bq,\n                b\"-f\", b\"caf\",\n                fsenc(tpath)\n            ]\n            # fmt: on\n            self._run_ff(cmd, vn, \"aconvt\", oom=300)\n\n        else:\n            # simple remux should be safe\n            self.log(\"conv2 caf-remux; dur=%d sz=%d\" % (dur, sz), 6)\n            # fmt: off\n            cmd = [\n                b\"ffmpeg\",\n                b\"-nostdin\",\n                b\"-v\", b\"error\",\n                b\"-hide_banner\",\n                b\"-i\", fsenc(tmp_opus),\n                b\"-map_metadata\", b\"-1\",\n                b\"-map\", b\"0:a:0\",\n                b\"-c:a\", b\"copy\",\n                b\"-f\", b\"caf\",\n                fsenc(tpath)\n            ]\n            # fmt: on\n            self._run_ff(cmd, vn, \"aconvt\", oom=300)\n\n        try:\n            wunlink(self.log, tmp_opus, vn.flags)\n        except:\n            pass\n\n    def big_tags(self, raw_tags: dict[str, list[str]]) -> list[bytes]:\n        ret = []\n        for k, vs in raw_tags.items():\n            for v in vs:\n                if len(unicode(v)) >= 1024:\n                    bv = k.encode(\"utf-8\", \"replace\")\n                    ret += [b\"-metadata\", bv + b\"=\"]\n                    break\n        return ret\n\n    def poke(self, tdir: str) -> None:\n        if not self.poke_cd.poke(tdir):\n            return\n\n        ts = int(time.time())\n        try:\n            for _ in range(4):\n                bos.utime(tdir, (ts, ts))\n                tdir = os.path.dirname(tdir)\n        except:\n            pass\n\n    def cleaner(self) -> None:\n        interval = self.args.th_clean\n        while True:\n            ndirs = 0\n            for vol, histpath in self.asrv.vfs.histtab.items():\n                if histpath.startswith(vol):\n                    self.log(\"\\033[Jcln {}/\\033[A\".format(histpath))\n                else:\n                    self.log(\"\\033[Jcln {} ({})/\\033[A\".format(histpath, vol))\n\n                try:\n                    ndirs += self.clean(histpath)\n                except Exception as ex:\n                    self.log(\"\\033[Jcln err in %s: %r\" % (histpath, ex), 3)\n\n            self.log(\"\\033[Jcln ok; rm {} dirs\".format(ndirs))\n            self.rm_nullthumbs = False\n            time.sleep(interval)\n\n    def clean(self, histpath: str) -> int:\n        cfgi = cfga = \"\"\n        for vn in self.asrv.vfs.all_vols.values():\n            if vn.histpath == histpath:\n                cfgi = self.volcfgi(vn)\n                cfga = self.volcfga(vn)\n                break\n        for cfg, cat in ((cfgi, \"th\"), (cfga, \"ac\")):\n            if not cfg:\n                continue\n            try:\n                with open(os.path.join(histpath, cat, \"cfg.txt\"), \"rb\") as f:\n                    oldcfg = f.read().decode(\"utf-8\")\n            except:\n                oldcfg = \"\"\n            if cfg == oldcfg:\n                continue\n            zs = os.path.join(histpath, cat)\n            if not os.path.exists(zs):\n                continue\n            self.log(\"thumbnailer-config changed; deleting %s\" % (zs,), 3)\n            shutil.rmtree(zs)\n\n        ret = 0\n        for cat in [\"th\", \"ac\"]:\n            top = os.path.join(histpath, cat)\n            if not bos.path.isdir(top):\n                continue\n\n            ret += self._clean(cat, top)\n\n        return ret\n\n    def _clean(self, cat: str, thumbpath: str) -> int:\n        # self.log(\"cln {}\".format(thumbpath))\n        exts = EXTS_TH if cat == \"th\" else EXTS_AC\n        maxage = getattr(self.args, cat + \"_maxage\")\n        now = time.time()\n        prev_b64 = None\n        prev_fp = \"\"\n        try:\n            t1 = statdir(\n                self.log_func, not self.args.no_scandir, False, thumbpath, False\n            )\n            ents = sorted(list(t1))\n        except:\n            return 0\n\n        ndirs = 0\n        for f, inf in ents:\n            fp = os.path.join(thumbpath, f)\n            cmp = fp.lower().replace(\"\\\\\", \"/\")\n\n            # \"top\" or b64 prefix/full (a folder)\n            if len(f) <= 3 or len(f) == 24:\n                age = now - inf.st_mtime\n                if age > maxage:\n                    with self.mutex:\n                        safe = True\n                        for k in self.busy:\n                            if k.lower().replace(\"\\\\\", \"/\").startswith(cmp):\n                                safe = False\n                                break\n\n                        if safe:\n                            ndirs += 1\n                            self.log(\"rm -rf [{}]\".format(fp))\n                            shutil.rmtree(fp, ignore_errors=True)\n                else:\n                    ndirs += self._clean(cat, fp)\n\n                continue\n\n            # thumb file\n            try:\n                b64, ts, ext = f.split(\".\")\n                if len(ts) > 8 and PTN_TS.match(ts):\n                    ts = \"yeahokay\"\n                if len(b64) != 24 or len(ts) != 8 or ext not in exts:\n                    raise Exception()\n            except:\n                if f != \"dir.txt\" and f != \"cfg.txt\":\n                    self.log(\"foreign file in thumbs dir: [{}]\".format(fp), 1)\n\n                continue\n\n            if self.rm_nullthumbs and not inf.st_size:\n                bos.unlink(fp)\n                continue\n\n            if b64 == prev_b64:\n                self.log(\"rm replaced [{}]\".format(fp))\n                bos.unlink(prev_fp)\n\n            if cat != \"th\" and inf.st_mtime + maxage < now:\n                self.log(\"rm expired [{}]\".format(fp))\n                bos.unlink(fp)\n\n            prev_b64 = b64\n            prev_fp = fp\n\n        return ndirs\n"
  },
  {
    "path": "copyparty/u2idx.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport calendar\nimport os\nimport re\nimport threading\nimport time\nfrom operator import itemgetter\n\nfrom .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode\nfrom .authsrv import LEELOO_DALLAS, VFS\nfrom .bos import bos\nfrom .up2k import up2k_wark_from_hashlist\nfrom .util import (\n    HAVE_SQLITE3,\n    Daemon,\n    Pebkac,\n    absreal,\n    gen_filekey,\n    min_ex,\n    quotep,\n    s3dec,\n    vjoin,\n)\n\nif HAVE_SQLITE3:\n    import sqlite3\n\ntry:\n    from pathlib import Path\nexcept:\n    pass\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Any, Optional, Union\n\nif TYPE_CHECKING:\n    from .httpsrv import HttpSrv\n\nif PY2:\n    range = xrange  # type: ignore\n\n\nclass U2idx(object):\n    def __init__(self, hsrv: \"HttpSrv\") -> None:\n        self.log_func = hsrv.log\n        self.asrv = hsrv.asrv\n        self.args = hsrv.args\n        self.timeout = self.args.srch_time\n\n        if not HAVE_SQLITE3:\n            self.log(\"your python does not have sqlite3; searching will be disabled\")\n            return\n\n        if self.args.srch_icase:\n            self._open_db = self._open_db_icase\n        else:\n            self._open_db = self._open_db_std\n\n        assert sqlite3  # type: ignore  # !rm\n\n        self.active_id = \"\"\n        self.active_cur: Optional[\"sqlite3.Cursor\"] = None\n        self.cur: dict[str, \"sqlite3.Cursor\"] = {}\n        self.mem_cur = sqlite3.connect(\":memory:\", check_same_thread=False).cursor()\n        self.mem_cur.execute(r\"create table a (b text)\")\n\n        self.sh_cur: Optional[\"sqlite3.Cursor\"] = None\n\n        self.p_end = 0.0\n        self.p_dur = 0.0\n\n    def log(self, msg: str, c: Union[int, str] = 0) -> None:\n        self.log_func(\"u2idx\", msg, c)\n\n    def _open_db_std(self, *args, **kwargs):\n        assert sqlite3  # type: ignore  # !rm\n        kwargs[\"check_same_thread\"] = False\n        return sqlite3.connect(*args, **kwargs)\n\n    def _open_db_icase(self, *args, **kwargs):\n        db = self._open_db_std(*args, **kwargs)\n        db.create_function(\"casefold\", 1, lambda x: x.casefold() if x else x)\n        return db\n\n    def shutdown(self) -> None:\n        if not HAVE_SQLITE3:\n            return\n\n        for cur in self.cur.values():\n            db = cur.connection\n            try:\n                db.interrupt()\n            except:\n                pass\n\n            cur.close()\n            db.close()\n\n        for cur in (self.mem_cur, self.sh_cur):\n            if cur:\n                db = cur.connection\n                cur.close()\n                db.close()\n\n    def fsearch(\n        self, uname: str, vols: list[VFS], body: dict[str, Any]\n    ) -> list[dict[str, Any]]:\n        \"\"\"search by up2k hashlist\"\"\"\n        if not HAVE_SQLITE3:\n            return []\n\n        fsize = body[\"size\"]\n        fhash = body[\"hash\"]\n        wark = up2k_wark_from_hashlist(self.args.warksalt, fsize, fhash)\n\n        uq = \"substr(w,1,16) = ? and w = ?\"\n        uv: list[Union[str, int]] = [wark[:16], wark]\n\n        try:\n            return self.run_query(uname, vols, uq, uv, False, True, 99999)[0]\n        except:\n            raise Pebkac(500, min_ex())\n\n    def get_shr(self) -> Optional[\"sqlite3.Cursor\"]:\n        if self.sh_cur:\n            return self.sh_cur\n\n        if not HAVE_SQLITE3 or not self.args.shr:\n            return None\n\n        assert sqlite3  # type: ignore  # !rm\n\n        db = sqlite3.connect(self.args.shr_db, timeout=2, check_same_thread=False)\n        cur = db.cursor()\n        cur.execute('pragma table_info(\"sh\")').fetchall()\n        self.sh_cur = cur\n        return cur\n\n    def get_cur(self, vn: VFS) -> Optional[\"sqlite3.Cursor\"]:\n        cur = self.cur.get(vn.realpath)\n        if cur:\n            return cur\n\n        if not HAVE_SQLITE3 or \"e2d\" not in vn.flags:\n            return None\n\n        assert sqlite3  # type: ignore  # !rm\n\n        ptop = vn.realpath\n        histpath = self.asrv.vfs.dbpaths.get(ptop)\n        if not histpath:\n            self.log(\"no dbpath for %r\" % (ptop,))\n            return None\n\n        db_path = os.path.join(histpath, \"up2k.db\")\n        if not bos.path.exists(db_path):\n            return None\n\n        cur = None\n        if ANYWIN and not bos.path.exists(db_path + \"-wal\"):\n            uri = \"\"\n            try:\n                uri = \"{}?mode=ro&nolock=1\".format(Path(db_path).as_uri())\n                cur = self._open_db(uri, timeout=2, uri=True).cursor()\n                cur.execute('pragma table_info(\"up\")').fetchone()\n                self.log(\"ro: %r\" % (db_path,))\n            except:\n                self.log(\"could not open read-only: {}\\n{}\".format(uri, min_ex()))\n                # may not fail until the pragma so unset it\n                cur = None\n\n        if not cur:\n            # on windows, this steals the write-lock from up2k.deferred_init --\n            # seen on win 10.0.17763.2686, py 3.10.4, sqlite 3.37.2\n            cur = self._open_db(db_path, timeout=2).cursor()\n            self.log(\"opened %r\" % (db_path,))\n\n        self.cur[ptop] = cur\n        return cur\n\n    def search(\n        self, uname: str, vols: list[VFS], uq: str, lim: int\n    ) -> tuple[list[dict[str, Any]], list[str], bool]:\n        \"\"\"search by query params\"\"\"\n        if not HAVE_SQLITE3:\n            return [], [], False\n\n        icase = self.args.srch_icase\n\n        q = \"\"\n        v: Union[str, int] = \"\"\n        va: list[Union[str, int]] = []\n        have_mt = False\n        is_key = True\n        is_size = False\n        is_date = False\n        is_wark = False\n        field_end = \"\"  # closing parenthesis or whatever\n        kw_key = [\"(\", \")\", \"and \", \"or \", \"not \"]\n        kw_val = [\"==\", \"=\", \"!=\", \">\", \">=\", \"<\", \"<=\", \"like \"]\n        ptn_mt = re.compile(r\"^\\.?[a-z_-]+$\")\n        ptn_lc = re.compile(r\" (mt\\.v) ([=<!>]+) \\? \\) $\")\n        ptn_lcv = re.compile(r\"[a-zA-Z]\")\n\n        while True:\n            uq = uq.strip()\n            if not uq:\n                break\n\n            ok = False\n            for kw in kw_key + kw_val:\n                if uq.startswith(kw):\n                    is_key = kw in kw_key\n                    uq = uq[len(kw) :]\n                    ok = True\n                    if is_wark:\n                        kw = \"= \"\n                    q += kw\n                    break\n\n            if ok:\n                continue\n\n            if uq.startswith('\"'):\n                v, uq = uq[1:].split('\"', 1)\n                while v.endswith(\"\\\\\"):\n                    v2, uq = uq.split('\"', 1)\n                    v = v[:-1] + '\"' + v2\n                uq = uq.strip()\n            else:\n                v, uq = (uq + \" \").split(\" \", 1)\n                v = v.replace('\\\\\"', '\"')\n\n            if is_key:\n                is_key = False\n\n                if v == \"size\":\n                    v = \"up.sz\"\n                    is_size = True\n\n                elif v == \"date\":\n                    v = \"up.mt\"\n                    is_date = True\n\n                elif v == \"up_at\":\n                    v = \"up.at\"\n                    is_date = True\n\n                elif v == \"path\":\n                    v = \"trim(?||up.rd,'/')\"\n                    va.append(\"\\nrd\")\n                    if icase:\n                        v = \"casefold(%s)\" % (v,)\n\n                elif v == \"name\":\n                    v = \"up.fn\"\n                    if icase:\n                        v = \"casefold(%s)\" % (v,)\n\n                elif v == \"w\":\n                    v = \"substr(up.w,1,16)\"\n                    is_wark = True\n\n                elif v == \"tags\" or ptn_mt.match(v):\n                    have_mt = True\n                    field_end = \") \"\n                    if v == \"tags\":\n                        vq = \"mt.v\"\n                    else:\n                        vq = \"+mt.k = '{}' and mt.v\".format(v)\n\n                    v = \"exists(select 1 from mt where mt.w = mtw and \" + vq\n\n                else:\n                    raise Pebkac(400, \"invalid key %r\" % (v,))\n\n                q += v + \" \"\n                continue\n\n            head = \"\"\n            tail = \"\"\n\n            if is_date:\n                is_date = False\n                v = re.sub(r\"[tzTZ, ]+\", \" \", v).strip()\n                for fmt in [\n                    \"%Y-%m-%d %H:%M:%S\",\n                    \"%Y-%m-%d %H:%M\",\n                    \"%Y-%m-%d %H\",\n                    \"%Y-%m-%d\",\n                    \"%Y-%m\",\n                    \"%Y\",\n                ]:\n                    try:\n                        v = calendar.timegm(time.strptime(str(v), fmt))\n                        break\n                    except:\n                        pass\n\n            elif is_size:\n                is_size = False\n                v = int(float(v) * 1024 * 1024)\n\n            elif is_wark:\n                is_wark = False\n                v = v.strip(\"*\")\n                if len(v) > 16:\n                    v = v[:16]\n                if len(v) < 16:\n                    raise Pebkac(400, \"w/filehash must be 16+ chars\")\n\n            else:\n                if v.startswith(\"*\"):\n                    head = \"'%'||\"\n                    v = v[1:]\n\n                if v.endswith(\"*\"):\n                    tail = \"||'%'\"\n                    v = v[:-1]\n\n            if icase and \"casefold(\" in q:\n                try:\n                    v = unicode(v).casefold()\n                except:\n                    v = unicode(v).lower()\n\n            q += \" {}?{} \".format(head, tail)\n            va.append(v)\n            is_key = True\n\n            if field_end:\n                q += field_end\n                field_end = \"\"\n\n            # lowercase tag searches\n            m = ptn_lc.search(q)\n            zs = unicode(v)\n            if not m or not ptn_lcv.search(zs):\n                continue\n\n            va.pop()\n            va.append(zs.lower())\n            q = q[: m.start()]\n\n            field, oper = m.groups()\n            if oper in [\"=\", \"==\"]:\n                q += \" {} like ? ) \".format(field)\n            else:\n                q += \" lower({}) {} ? ) \".format(field, oper)\n\n        try:\n            return self.run_query(uname, vols, q, va, have_mt, True, lim)\n        except Exception as ex:\n            raise Pebkac(500, repr(ex))\n\n    def run_query(\n        self,\n        uname: str,\n        vols: list[VFS],\n        uq: str,\n        uv: Union[list[str], list[Union[str, int]]],\n        have_mt: bool,\n        sort: bool,\n        lim: int,\n    ) -> tuple[list[dict[str, Any]], list[str], bool]:\n        dbg = self.args.srch_dbg\n        if dbg:\n            t = \"searching across all %s volumes in which the user has 'r' (full read access):\\n  %s\"\n            zs = \"\\n  \".join([\"/%s = %s\" % (x.vpath, x.realpath) for x in vols])\n            self.log(t % (len(vols), zs), 5)\n\n        done_flag: list[bool] = []\n        self.active_id = \"{:.6f}_{}\".format(\n            time.time(), threading.current_thread().ident\n        )\n        Daemon(self.terminator, \"u2idx-terminator\", (self.active_id, done_flag))\n\n        if not uq or not uv:\n            uq = \"select * from up\"\n            uv = []\n        elif have_mt:\n            uq = \"select up.*, substr(up.w,1,16) mtw from up where \" + uq\n        else:\n            uq = \"select up.* from up where \" + uq\n\n        self.log(\"qs: {!r} {!r}\".format(uq, uv))\n\n        ret = []\n        seen_rps: set[str] = set()\n        clamp = int(self.args.srch_hits)\n        if lim >= clamp:\n            lim = clamp\n            clamped = True\n        else:\n            clamped = False\n\n        taglist = {}\n        for vol in vols:\n            if lim < 0:\n                break\n\n            vtop = vol.vpath\n            ptop = vol.realpath\n            flags = vol.flags\n\n            cur = self.get_cur(vol)\n            if not cur:\n                continue\n\n            dots = flags.get(\"dotsrch\") and uname in vol.axs.udot\n            zs = \"srch_re_dots\" if dots else \"srch_re_nodot\"\n            rex: re.Pattern = flags.get(zs)  # type: ignore\n\n            if dbg:\n                t = \"searching in volume /%s (%s), excluding %s\"\n                self.log(t % (vtop, ptop, rex.pattern), 5)\n                rex_cfg: Optional[re.Pattern] = flags.get(\"srch_excl\")\n\n            self.active_cur = cur\n\n            vuv = []\n            for v in uv:\n                if v == \"\\nrd\":\n                    v = vtop + \"/\"\n\n                vuv.append(v)\n\n            sret = []\n            fk = flags.get(\"fk\")\n            fk_alg = 2 if \"fka\" in flags else 1\n            c = cur.execute(uq, tuple(vuv))\n            for hit in c:\n                w, ts, sz, rd, fn = hit[:5]\n\n                if rd.startswith(\"//\") or fn.startswith(\"//\"):\n                    rd, fn = s3dec(rd, fn)\n\n                vp = vjoin(vjoin(vtop, rd), fn)\n\n                if vp in seen_rps:\n                    continue\n\n                if rex.search(vp):\n                    if dbg:\n                        if rex_cfg and rex_cfg.search(vp):  # type: ignore\n                            self.log(\"filtered by srch_excl: %s\" % (vp,), 6)\n                        elif not dots and \"/.\" in (\"/\" + vp):\n                            pass\n                        else:\n                            t = \"database inconsistency in volume '/%s'; ignoring: %s\"\n                            self.log(t % (vtop, vp), 1)\n                    continue\n\n                rp = quotep(vp)\n                if not fk:\n                    suf = \"\"\n                else:\n                    try:\n                        ap = absreal(os.path.join(ptop, rd, fn))\n                        ino = 0 if ANYWIN or fk_alg == 2 else bos.stat(ap).st_ino\n                    except:\n                        continue\n\n                    suf = \"?k=\" + gen_filekey(\n                        fk_alg,\n                        self.args.fk_salt,\n                        ap,\n                        sz,\n                        ino,\n                    )[:fk]\n\n                lim -= 1\n                if lim < 0:\n                    break\n\n                if dbg:\n                    t = \"in volume '/%s': hit: %s\"\n                    self.log(t % (vtop, rp), 5)\n\n                    zs = vjoin(vtop, rp)\n                    chk_vn, _ = self.asrv.vfs.get(zs, LEELOO_DALLAS, True, False)\n                    chk_vn = chk_vn.dbv or chk_vn\n                    if chk_vn.vpath != vtop:\n                        raise Exception(\n                            \"database inconsistency! in volume '/%s' (%s), found file [%s] which belongs to volume '/%s' (%s)\"\n                            % (vtop, ptop, zs, chk_vn.vpath, chk_vn.realpath)\n                        )\n\n                seen_rps.add(rp)\n                sret.append({\"ts\": int(ts), \"sz\": sz, \"rp\": rp + suf, \"w\": w[:16]})\n\n            for hit in sret:\n                w = hit[\"w\"]\n                del hit[\"w\"]\n                tags = {}\n                q2 = \"select k, v from mt where w = ? and +k != 'x'\"\n                for k, v2 in cur.execute(q2, (w,)):\n                    taglist[k] = True\n                    tags[k] = v2\n\n                hit[\"tags\"] = tags\n\n            ret.extend(sret)\n            # print(\"[{}] {}\".format(ptop, sret))\n\n            if dbg:\n                t = \"in volume '/%s': got %d hits, %d total so far\"\n                self.log(t % (vtop, len(sret), len(ret)), 5)\n\n        done_flag.append(True)\n        self.active_id = \"\"\n\n        if sort:\n            ret.sort(key=itemgetter(\"rp\"))\n\n        return ret, list(taglist.keys()), lim < 0 and not clamped\n\n    def terminator(self, identifier: str, done_flag: list[bool]) -> None:\n        for _ in range(self.timeout):\n            time.sleep(1)\n            if done_flag:\n                return\n\n        if identifier == self.active_id:\n            assert self.active_cur  # !rm\n            self.active_cur.connection.interrupt()\n"
  },
  {
    "path": "copyparty/up2k.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport errno\nimport hashlib\nimport json\nimport math\nimport os\nimport re\nimport shutil\nimport stat\nimport subprocess as sp\nimport sys\nimport tempfile\nimport threading\nimport time\nimport traceback\nfrom copy import deepcopy\n\nfrom queue import Queue\n\nfrom .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, WINDOWS, E\nfrom .authsrv import LEELOO_DALLAS, SEESLOG, VFS, AuthSrv\nfrom .bos import bos\nfrom .cfg import vf_bmap, vf_cmap, vf_vmap\nfrom .fsutil import Fstab\nfrom .mtag import MParser, MTag\nfrom .util import (\n    E_FS_CRIT,\n    E_FS_MEH,\n    HAVE_FICLONE,\n    HAVE_SQLITE3,\n    SYMTIME,\n    VF_CAREFUL,\n    Daemon,\n    MTHash,\n    Pebkac,\n    ProgressPrinter,\n    absreal,\n    alltrace,\n    atomic_move,\n    db_ex_chk,\n    dir_is_empty,\n    djoin,\n    fsenc,\n    gen_filekey,\n    gen_filekey_dbg,\n    gzip,\n    hidedir,\n    humansize,\n    min_ex,\n    pathmod,\n    quotep,\n    rand_name,\n    ren_open,\n    rmdirs,\n    rmdirs_up,\n    runhook,\n    runihook,\n    s2hms,\n    s3dec,\n    s3enc,\n    sanitize_fn,\n    set_ap_perms,\n    set_fperms,\n    sfsenc,\n    spack,\n    statdir,\n    trystat_shutil_copy2,\n    ub64enc,\n    unhumanize,\n    vjoin,\n    vsplit,\n    w8b64dec,\n    w8b64enc,\n    wunlink,\n)\n\ntry:\n    from pathlib import Path\nexcept:\n    pass\n\nif HAVE_SQLITE3:\n    import sqlite3\n\nDB_VER = 6\n\nif True:  # pylint: disable=using-constant-test\n    from typing import Any, Optional, Pattern, Union\n\nif TYPE_CHECKING:\n    from .svchub import SvcHub\n\nUSE_FICLONE = HAVE_FICLONE and sys.version_info < (3, 14)\nif USE_FICLONE:\n    import fcntl\n\nzsg = \"avif,avifs,bmp,gif,heic,heics,heif,heifs,ico,j2p,j2k,jp2,jpeg,jpg,jpx,png,tga,tif,tiff,webp\"\nICV_EXTS = set(zsg.split(\",\"))\n\nzsg = \"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\"\nVCV_EXTS = set(zsg.split(\",\"))\n\nzsg = \"aif,aiff,alac,ape,flac,m4a,m4b,m4r,mp3,oga,ogg,opus,tak,tta,wav,wma,wv,cbz,epub\"\nACV_EXTS = set(zsg.split(\",\"))\n\nzsg = \"nohash noidx xdev xvol\"\nVF_AFFECTS_INDEXING = set(zsg.split(\" \"))\n\n\nSBUSY = \"cannot receive uploads right now;\\nserver busy with %s.\\nPlease wait; the client will retry...\"\n\nHINT_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\"\n\n\nNULLSTAT = os.stat_result((0, -1, -1, 0, 0, 0, 0, 0, 0, 0))\n\n\nclass Dbw(object):\n    def __init__(self, c: \"sqlite3.Cursor\", n: int, nf: int, t: float) -> None:\n        self.c = c\n        self.n = n\n        self.nf = nf\n        self.t = t\n\n\nclass Mpqe(object):\n    \"\"\"pending files to tag-scan\"\"\"\n\n    def __init__(\n        self,\n        mtp: dict[str, MParser],\n        entags: set[str],\n        vf: dict[str, Any],\n        w: str,\n        abspath: str,\n        oth_tags: dict[str, Any],\n    ):\n        # mtp empty = mtag\n        self.mtp = mtp\n        self.entags = entags\n        self.vf = vf\n        self.w = w\n        self.abspath = abspath\n        self.oth_tags = oth_tags\n\n\nclass Up2k(object):\n    def __init__(self, hub: \"SvcHub\") -> None:\n        self.hub = hub\n        self.asrv: AuthSrv = hub.asrv\n        self.args = hub.args\n        self.log_func = hub.log\n\n        self.vfs = self.asrv.vfs\n        self.acct = self.asrv.acct\n        self.iacct = self.asrv.iacct\n        self.grps = self.asrv.grps\n\n        self.salt = self.args.warksalt\n        self.r_hash = re.compile(\"^[0-9a-zA-Z_-]{44}$\")\n        self.abrt_key = \"\"\n\n        self.gid = 0\n        self.gt0 = 0\n        self.gt1 = 0\n        self.stop = False\n        self.fika = \"\"\n        self.mutex = threading.Lock()\n        self.reload_mutex = threading.Lock()\n        self.reload_flag = 0\n        self.reloading = False\n        self.blocked: Optional[str] = None\n        self.pp: Optional[ProgressPrinter] = None\n        self.rescan_cond = threading.Condition()\n        self.need_rescan: set[str] = set()\n        self.db_act = 0.0\n\n        self.reg_mutex = threading.Lock()\n        self.registry: dict[str, dict[str, dict[str, Any]]] = {}\n        self.flags: dict[str, dict[str, Any]] = {}\n        self.droppable: dict[str, list[str]] = {}\n        self.volnfiles: dict[\"sqlite3.Cursor\", int] = {}\n        self.volsize: dict[\"sqlite3.Cursor\", int] = {}\n        self.volstate: dict[str, str] = {}\n        self.vol_act: dict[str, float] = {}\n        self.busy_aps: dict[str, int] = {}\n        self.dupesched: dict[str, list[tuple[str, str, float]]] = {}\n        self.snap_prev: dict[str, Optional[tuple[int, float]]] = {}\n\n        self.mtag: Optional[MTag] = None\n        self.entags: dict[str, set[str]] = {}\n        self.mtp_parsers: dict[str, dict[str, MParser]] = {}\n        self.pending_tags: list[tuple[set[str], str, str, dict[str, Any]]] = []\n        self.hashq: Queue[\n            tuple[str, str, dict[str, Any], str, str, str, float, str, bool]\n        ] = Queue()\n        self.tagq: Queue[tuple[str, str, str, str, int, str, float]] = Queue()\n        self.tag_event = threading.Condition()\n        self.hashq_mutex = threading.Lock()\n        self.n_hashq = 0\n        self.n_tagq = 0\n        self.mpool_used = False\n\n        self.xiu_ptn = re.compile(r\"(?:^|,)i([0-9]+)\")\n        self.xiu_busy = False  # currently running hook\n        self.xiu_asleep = True  # needs rescan_cond poke to schedule self\n        self.fx_backlog: list[tuple[str, dict[str, str], str]] = []\n\n        self.cur: dict[str, \"sqlite3.Cursor\"] = {}\n        self.mem_cur = None\n        self.sqlite_ver = None\n        self.no_expr_idx = False\n        self.timeout = int(max(self.args.srch_time, 50) * 1.2) + 1\n        self.spools: set[tempfile.SpooledTemporaryFile[bytes]] = set()\n        if HAVE_SQLITE3:\n            # mojibake detector\n            assert sqlite3  # type: ignore  # !rm\n            self.mem_cur = self._orz(\":memory:\")\n            self.mem_cur.execute(r\"create table a (b text)\")\n            self.sqlite_ver = tuple([int(x) for x in sqlite3.sqlite_version.split(\".\")])\n            if self.sqlite_ver < (3, 9):\n                self.no_expr_idx = True\n        else:\n            t = \"could not initialize sqlite3, will use in-memory registry only\"\n            self.log(t, 3)\n\n        self.fstab = Fstab(self.log_func, self.args, True)\n        self.gen_fk = self._gen_fk if self.args.log_fk else gen_filekey\n\n        if self.args.hash_mt < 2:\n            self.mth: Optional[MTHash] = None\n        else:\n            self.mth = MTHash(self.args.hash_mt)\n\n        if self.args.no_fastboot:\n            self.deferred_init()\n\n    def init_vols(self) -> None:\n        if self.args.no_fastboot:\n            return\n\n        Daemon(self.deferred_init, \"up2k-deferred-init\")\n\n    def unpp(self) -> None:\n        self.gt1 = time.time()\n        if self.pp:\n            self.pp.end = True\n            self.pp = None\n\n    def reload(self, rescan_all_vols: bool) -> None:\n        n = 2 if rescan_all_vols else 1\n        with self.reload_mutex:\n            if self.reload_flag < n:\n                self.reload_flag = n\n        with self.rescan_cond:\n            self.rescan_cond.notify_all()\n\n    def _reload_thr(self) -> None:\n        while self.pp:\n            time.sleep(0.1)\n        while True:\n            with self.reload_mutex:\n                if not self.reload_flag:\n                    break\n                rav = self.reload_flag == 2\n                self.reload_flag = 0\n            gt1 = self.gt1\n            with self.mutex:\n                self._reload(rav)\n            while gt1 == self.gt1 or self.pp:\n                time.sleep(0.1)\n\n        self.reloading = False\n\n    def _reload(self, rescan_all_vols: bool) -> None:\n        \"\"\"mutex(main) me\"\"\"\n        self.log(\"reload #{} scheduled\".format(self.gid + 1))\n        all_vols = self.asrv.vfs.all_vols\n\n        with self.reg_mutex:\n            scan_vols = [\n                k for k, v in all_vols.items() if v.realpath not in self.registry\n            ]\n\n        if rescan_all_vols:\n            scan_vols = list(all_vols.keys())\n\n        self._rescan(all_vols, scan_vols, True, False)\n\n    def deferred_init(self) -> None:\n        all_vols = self.asrv.vfs.all_vols\n        have_e2d = self.init_indexes(all_vols, [], False)\n\n        if self.stop:\n            # up-mt consistency not guaranteed if init is interrupted;\n            # drop caches for a full scan on next boot\n            with self.mutex, self.reg_mutex:\n                self._drop_caches()\n\n            self.unpp()\n            return\n\n        if not self.pp and self.args.exit == \"idx\":\n            return self.hub.sigterm()\n\n        if self.hub.is_dut:\n            return\n\n        Daemon(self._snapshot, \"up2k-snapshot\")\n        if have_e2d:\n            Daemon(self._hasher, \"up2k-hasher\")\n            Daemon(self._sched_rescan, \"up2k-rescan\")\n            if self.mtag:\n                for n in range(max(1, self.args.mtag_mt)):\n                    Daemon(self._tagger, \"tagger-{}\".format(n))\n\n    def log(self, msg: str, c: Union[int, str] = 0) -> None:\n        if self.pp:\n            msg += \"\\033[K\"\n\n        self.log_func(\"up2k\", msg, c)\n\n    def _gen_fk(self, alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:\n        return gen_filekey_dbg(\n            alg, salt, fspath, fsize, inode, self.log, self.args.log_fk\n        )\n\n    def _block(self, why: str) -> None:\n        self.blocked = why\n        self.log(\"uploads temporarily blocked due to \" + why, 3)\n\n    def _unblock(self) -> None:\n        if self.blocked is not None:\n            self.blocked = None\n            if not self.stop:\n                self.log(\"uploads are now possible\", 2)\n\n    def get_state(self, get_q: bool, uname: str) -> str:\n        mtpq: Union[int, str] = 0\n        ups = []\n        up_en = not self.args.no_up_list\n        q = \"select count(w) from mt where k = 't:mtp'\"\n        got_lock = False if PY2 else self.mutex.acquire(timeout=0.5)\n        if got_lock:\n            try:\n                for cur in self.cur.values() if get_q else []:\n                    try:\n                        mtpq += cur.execute(q).fetchone()[0]\n                    except:\n                        pass\n                if uname and up_en:\n                    ups = self._active_uploads(uname)\n            finally:\n                self.mutex.release()\n        else:\n            mtpq = \"(?)\"\n            if up_en:\n                ups = [(1, 0, 0, time.time(), \"cannot show list (server too busy)\")]\n                if PY2:\n                    ups = []\n\n        ups.sort(reverse=True)\n\n        ret = {\n            \"volstate\": self.volstate,\n            \"scanning\": bool(self.pp),\n            \"hashq\": self.n_hashq,\n            \"tagq\": self.n_tagq,\n            \"mtpq\": mtpq,\n            \"ups\": ups,\n            \"dbwu\": \"{:.2f}\".format(self.db_act),\n            \"dbwt\": \"{:.2f}\".format(\n                min(1000 * 24 * 60 * 60 - 1, time.time() - self.db_act)\n            ),\n        }\n        return json.dumps(ret, separators=(\",\\n\", \": \"))\n\n    def _active_uploads(self, uname: str) -> list[tuple[float, int, int, str]]:\n        ret = []\n        for vtop in self.vfs.aread.get(uname) or []:\n            vfs = self.vfs.all_vols.get(vtop)\n            if not vfs:  # dbv only\n                continue\n            ptop = vfs.realpath\n            tab = self.registry.get(ptop)\n            if not tab:\n                continue\n            for job in tab.values():\n                ineed = len(job[\"need\"])\n                ihash = len(job[\"hash\"])\n                if ineed == ihash or not ineed:\n                    continue\n\n                poke = job[\"poke\"]\n                zt = (\n                    ineed / ihash,\n                    job[\"size\"],\n                    int(job.get(\"t0c\", poke)),\n                    int(poke),\n                    djoin(vtop, job[\"prel\"], job[\"name\"]),\n                )\n                ret.append(zt)\n        return ret\n\n    def find_job_by_ap(self, ptop: str, ap: str) -> str:\n        try:\n            if ANYWIN:\n                ap = ap.replace(\"\\\\\", \"/\")\n\n            vp = ap[len(ptop) :].strip(\"/\")\n            dn, fn = vsplit(vp)\n            with self.reg_mutex:\n                tab2 = self.registry[ptop]\n                for job in tab2.values():\n                    if job[\"prel\"] == dn and job[\"name\"] == fn:\n                        return json.dumps(job, separators=(\",\\n\", \": \"))\n        except:\n            pass\n\n        return \"{}\"\n\n    def get_unfinished_by_user(self, uname, ip) -> dict[str, Any]:\n        # returns dict due to ExceptionalQueue\n        if PY2 or not self.reg_mutex.acquire(timeout=2):\n            return {\"timeout\": 1}\n\n        ret: list[tuple[int, str, int, int, int]] = []\n        userset = set([(uname or \"\\n\"), \"*\"])\n        e_d = {}\n        n = 1000\n        try:\n            for ptop, tab2 in self.registry.items():\n                cfg = self.flags.get(ptop, e_d).get(\"u2abort\", 1)\n                if not cfg:\n                    continue\n                addr = (ip or \"\\n\") if cfg in (1, 2) else \"\"\n                user = userset if cfg in (1, 3) else None\n                for job in tab2.values():\n                    if (\n                        \"done\" in job\n                        or (user and job[\"user\"] not in user)\n                        or (addr and addr != job[\"addr\"])\n                    ):\n                        continue\n                    zt5 = (\n                        int(job[\"t0\"]),\n                        djoin(job[\"vtop\"], job[\"prel\"], job[\"name\"]),\n                        job[\"size\"],\n                        len(job[\"need\"]),\n                        len(job[\"hash\"]),\n                    )\n                    ret.append(zt5)\n                    n -= 1\n                    if not n:\n                        break\n        finally:\n            self.reg_mutex.release()\n\n        if ANYWIN:\n            ret = [(x[0], x[1].replace(\"\\\\\", \"/\"), x[2], x[3], x[4]) for x in ret]\n\n        ret.sort(reverse=True)\n        ret2 = [\n            {\n                \"at\": at,\n                \"vp\": \"/\" + quotep(vp),\n                \"pd\": 100 - ((nn * 100) // (nh or 1)),\n                \"sz\": sz,\n            }\n            for (at, vp, sz, nn, nh) in ret\n        ]\n        return {\"f\": ret2}\n\n    def get_unfinished(self) -> str:\n        if PY2 or not self.reg_mutex.acquire(timeout=0.5):\n            return \"\"\n\n        ret: dict[str, tuple[int, int]] = {}\n        try:\n            for ptop, tab2 in self.registry.items():\n                nbytes = 0\n                nfiles = 0\n                for job in tab2.values():\n                    if \"done\" in job:\n                        continue\n\n                    nfiles += 1\n                    try:\n                        # close enough on average\n                        nbytes += len(job[\"need\"]) * job[\"size\"] // len(job[\"hash\"])\n                    except:\n                        pass\n\n                ret[ptop] = (nbytes, nfiles)\n        finally:\n            self.reg_mutex.release()\n\n        return json.dumps(ret, separators=(\",\\n\", \": \"))\n\n    def get_volsize(self, ptop: str) -> tuple[int, int]:\n        with self.reg_mutex:\n            return self._get_volsize(ptop)\n\n    def get_volsizes(self, ptops: list[str]) -> list[tuple[int, int]]:\n        ret = []\n        with self.reg_mutex:\n            for ptop in ptops:\n                ret.append(self._get_volsize(ptop))\n\n        return ret\n\n    def _get_volsize(self, ptop: str) -> tuple[int, int]:\n        if \"e2ds\" not in self.flags.get(ptop, {}):\n            return (0, 0)\n\n        cur = self.cur[ptop]\n        nbytes = self.volsize[cur]\n        nfiles = self.volnfiles[cur]\n        for j in list(self.registry.get(ptop, {}).values()):\n            nbytes += j[\"size\"]\n            nfiles += 1\n\n        return (nbytes, nfiles)\n\n    def rescan(\n        self, all_vols: dict[str, VFS], scan_vols: list[str], wait: bool, fscan: bool\n    ) -> str:\n        with self.mutex:\n            return self._rescan(all_vols, scan_vols, wait, fscan)\n\n    def _rescan(\n        self, all_vols: dict[str, VFS], scan_vols: list[str], wait: bool, fscan: bool\n    ) -> str:\n        \"\"\"mutex(main) me\"\"\"\n        if not wait and self.pp:\n            return \"cannot initiate; scan is already in progress\"\n\n        self.gid += 1\n        Daemon(\n            self.init_indexes,\n            \"up2k-rescan-{}\".format(scan_vols[0] if scan_vols else \"all\"),\n            (all_vols, scan_vols, fscan, self.gid),\n        )\n        return \"\"\n\n    def _sched_rescan(self) -> None:\n        volage = {}\n        cooldown = timeout = time.time() + 3.0\n        while not self.stop:\n            now = time.time()\n            timeout = max(timeout, cooldown)\n            wait = timeout - time.time()\n            # self.log(\"SR in {:.2f}\".format(wait), 5)\n            with self.rescan_cond:\n                self.rescan_cond.wait(wait)\n\n            if self.stop:\n                return\n\n            with self.reload_mutex:\n                if self.reload_flag and not self.reloading:\n                    self.reloading = True\n                    zs = \"up2k-reload-%d\" % (self.gid,)\n                    Daemon(self._reload_thr, zs)\n\n            now = time.time()\n            if now < cooldown:\n                # self.log(\"SR: cd - now = {:.2f}\".format(cooldown - now), 5)\n                timeout = cooldown  # wakeup means stuff to do, forget timeout\n                continue\n\n            if self.pp:\n                # self.log(\"SR: pp; cd := 1\", 5)\n                cooldown = now + 1\n                continue\n\n            cooldown = now + 3\n            # self.log(\"SR\", 5)\n\n            if self.args.no_lifetime and not self.args.shr:\n                timeout = now + 9001\n            else:\n                # important; not deferred by db_act\n                timeout = self._check_lifetimes()\n                timeout = min(self._check_forget_ip(), timeout)\n                try:\n                    if self.args.shr:\n                        timeout = min(self._check_shares(), timeout)\n                except Exception as ex:\n                    timeout = min(timeout, now + 60)\n                    t = \"could not check for expiring shares: %r\"\n                    self.log(t % (ex,), 1)\n\n            try:\n                timeout = min(timeout, now + self._check_xiu())\n            except Exception as ex:\n                if \"closed cursor\" in str(ex):\n                    self.log(\"sched_rescan: lost db\")\n                    return\n                raise\n\n            with self.mutex:\n                for vp, vol in sorted(self.vfs.all_vols.items()):\n                    maxage = vol.flags.get(\"scan\")\n                    if not maxage:\n                        continue\n\n                    if vp not in volage:\n                        volage[vp] = now\n\n                    deadline = volage[vp] + maxage\n                    if deadline <= now:\n                        self.need_rescan.add(vp)\n\n                    timeout = min(timeout, deadline)\n\n            if self.db_act > now - self.args.db_act and self.need_rescan:\n                # recent db activity; defer volume rescan\n                act_timeout = self.db_act + self.args.db_act\n                if self.need_rescan:\n                    timeout = now\n\n                if timeout < act_timeout:\n                    timeout = act_timeout\n                    t = \"volume rescan deferred {:.1f} sec, due to database activity\"\n                    self.log(t.format(timeout - now))\n\n                continue\n\n            with self.mutex:\n                vols = list(sorted(self.need_rescan))\n                self.need_rescan.clear()\n\n            if vols:\n                cooldown = now + 10\n                err = self.rescan(self.vfs.all_vols, vols, False, False)\n                if err:\n                    for v in vols:\n                        self.need_rescan.add(v)\n\n                    continue\n\n                for v in vols:\n                    volage[v] = now\n\n    def _check_forget_ip(self) -> float:\n        now = time.time()\n        timeout = now + 9001\n        for vp, vol in sorted(self.vfs.all_vols.items()):\n            maxage = vol.flags[\"forget_ip\"]\n            if not maxage:\n                continue\n\n            cur = self.cur.get(vol.realpath)\n            if not cur:\n                continue\n\n            cutoff = now - maxage * 60\n\n            for _ in range(2):\n                q = \"select ip, at from up where ip > '' order by +at limit 1\"\n                with self.mutex:\n                    hits = cur.execute(q).fetchall()\n                    if not hits:\n                        break\n\n                    remains = hits[0][1] - cutoff\n                    if remains > 0:\n                        timeout = min(timeout, now + remains)\n                        break\n\n                    q = \"update up set ip = '' where ip > '' and at <= %d\"\n                    cur.execute(q % (cutoff,))\n                    zi = cur.rowcount\n                    cur.connection.commit()\n\n                t = \"forget-ip(%d) removed %d IPs from db [/%s]\"\n                self.log(t % (maxage, zi, vol.vpath))\n\n            timeout = min(timeout, now + 900)\n\n        return timeout\n\n    def _check_lifetimes(self) -> float:\n        now = time.time()\n        timeout = now + 9001\n        for vp, vol in sorted(self.vfs.all_vols.items()):\n            lifetime = vol.flags.get(\"lifetime\")\n            if not lifetime:\n                continue\n\n            cur = self.cur.get(vol.realpath)\n            if not cur:\n                continue\n\n            nrm = 0\n            deadline = time.time() - lifetime\n            timeout = min(timeout, now + lifetime)\n            q = \"select rd, fn from up where at > 0 and at < ? limit 100\"\n            while True:\n                with self.mutex:\n                    hits = cur.execute(q, (deadline,)).fetchall()\n\n                if not hits:\n                    break\n\n                for rd, fn in hits:\n                    if rd.startswith(\"//\") or fn.startswith(\"//\"):\n                        rd, fn = s3dec(rd, fn)\n\n                    fvp = (\"%s/%s\" % (rd, fn)).strip(\"/\")\n                    if vp:\n                        fvp = \"%s/%s\" % (vp, fvp)\n\n                    self._handle_rm(LEELOO_DALLAS, \"\", fvp, [], True, False)\n                    nrm += 1\n\n            if nrm:\n                self.log(\"%d files graduated in /%s\" % (nrm, vp))\n\n            if timeout < 10:\n                continue\n\n            q = \"select at from up where at > 0 order by at limit 1\"\n            with self.mutex:\n                hits = cur.execute(q).fetchone()\n\n            if hits:\n                timeout = min(timeout, now + lifetime - (now - hits[0]))\n\n        return timeout\n\n    def _check_shares(self) -> float:\n        assert sqlite3  # type: ignore  # !rm\n\n        now = time.time()\n        timeout = now + 9001\n        maxage = self.args.shr_rt * 60\n        low = now - maxage\n\n        vn = self.vfs.nodes.get(self.args.shr.strip(\"/\"))\n        active = vn and vn.nodes\n\n        db = sqlite3.connect(self.args.shr_db, timeout=2)\n        cur = db.cursor()\n\n        q = \"select k from sh where t1 and t1 <= ?\"\n        rm = [x[0] for x in cur.execute(q, (now,))] if active else []\n        if rm:\n            assert vn and vn.nodes  # type: ignore\n            # self.log(\"chk_shr: %d\" % (len(rm),))\n            zss = set(rm)\n            rm = [zs for zs in vn.nodes if zs in zss]\n        reload = bool(rm)\n        if reload:\n            self.log(\"disabling expired shares %s\" % (rm,))\n\n        rm = [x[0] for x in cur.execute(q, (low,))]\n        if rm:\n            self.log(\"forgetting expired shares %s\" % (rm,))\n            cur.executemany(\"delete from sh where k=?\", [(x,) for x in rm])\n            cur.executemany(\"delete from sf where k=?\", [(x,) for x in rm])\n            db.commit()\n\n        if reload:\n            Daemon(self.hub.reload, \"sharedrop\", (False, False))\n\n        q = \"select min(t1) from sh where t1 > ?\"\n        (earliest,) = cur.execute(q, (1,)).fetchone()\n        if earliest:\n            # deadline for revoking regular access\n            timeout = min(timeout, earliest + maxage)\n\n            (earliest,) = cur.execute(q, (now - 2,)).fetchone()\n            if earliest:\n                # deadline for revival; drop entirely\n                timeout = min(timeout, earliest)\n\n        cur.close()\n        db.close()\n\n        if self.args.shr_v:\n            self.log(\"next shr_chk = %d (%d)\" % (timeout, timeout - time.time()))\n\n        return timeout\n\n    def _check_xiu(self) -> float:\n        if self.xiu_busy:\n            return 2\n\n        ret = 9001\n        for _, vol in sorted(self.vfs.all_vols.items()):\n            rp = vol.realpath\n            cur = self.cur.get(rp)\n            if not cur:\n                continue\n\n            with self.mutex:\n                q = \"select distinct c from iu\"\n                cds = cur.execute(q).fetchall()\n                if not cds:\n                    continue\n\n            run_cds: list[int] = []\n            for cd in sorted([x[0] for x in cds]):\n                delta = cd - (time.time() - self.vol_act[rp])\n                if delta > 0:\n                    ret = min(ret, delta)\n                    break\n\n                run_cds.append(cd)\n\n            if run_cds:\n                self.xiu_busy = True\n                Daemon(self._run_xius, \"xiu\", (vol, run_cds))\n                return 2\n\n        return ret\n\n    def _run_xius(self, vol: VFS, cds: list[int]):\n        for cd in cds:\n            self._run_xiu(vol, cd)\n\n        self.xiu_busy = False\n        self.xiu_asleep = True\n\n    def _run_xiu(self, vol: VFS, cd: int):\n        rp = vol.realpath\n        cur = self.cur[rp]\n\n        # t0 = time.time()\n        with self.mutex:\n            q = \"select w,rd,fn from iu where c={} limit 80386\"\n            wrfs = cur.execute(q.format(cd)).fetchall()\n            if not wrfs:\n                return\n\n            # dont wanna rebox so use format instead of prepared\n            q = \"delete from iu where w=? and +rd=? and +fn=? and +c={}\"\n            cur.executemany(q.format(cd), wrfs)\n            cur.connection.commit()\n\n            q = \"select * from up where substr(w,1,16)=? and +rd=? and +fn=?\"\n            ups = []\n            for wrf in wrfs:\n                up = cur.execute(q, wrf).fetchone()\n                if up:\n                    ups.append(up)\n\n        # t1 = time.time()\n        # self.log(\"mapped {} warks in {:.3f} sec\".format(len(wrfs), t1 - t0))\n        # \"mapped 10989 warks in 0.126 sec\"\n\n        cmds = self.flags[rp][\"xiu\"]\n        for cmd in cmds:\n            m = self.xiu_ptn.search(cmd)\n            ccd = int(m.group(1)) if m else 5\n            if ccd != cd:\n                continue\n\n            self.log(\"xiu: %d# %r\" % (len(wrfs), cmd))\n            runihook(self.log, self.args.hook_v, cmd, vol, ups)\n\n    def _vis_job_progress(self, job: dict[str, Any]) -> str:\n        perc = 100 - (len(job[\"need\"]) * 100.0 / (len(job[\"hash\"]) or 1))\n        path = djoin(job[\"ptop\"], job[\"prel\"], job[\"name\"])\n        return \"{:5.1f}% {}\".format(perc, path)\n\n    def _vis_reg_progress(self, reg: dict[str, dict[str, Any]]) -> list[str]:\n        ret = []\n        for _, job in reg.items():\n            if job[\"need\"]:\n                ret.append(self._vis_job_progress(job))\n\n        return ret\n\n    def _expr_idx_filter(self, flags: dict[str, Any]) -> tuple[bool, dict[str, Any]]:\n        if not self.no_expr_idx:\n            return False, flags\n\n        ret = {k: v for k, v in flags.items() if not k.startswith(\"e2t\")}\n        if len(ret) == len(flags):\n            return False, flags\n\n        return True, ret\n\n    def init_indexes(\n        self, all_vols: dict[str, VFS], scan_vols: list[str], fscan: bool, gid: int = 0\n    ) -> bool:\n        if not gid:\n            with self.mutex:\n                gid = self.gid\n\n        self.gt0 = time.time()\n\n        nspin = 0\n        while True:\n            nspin += 1\n            if nspin > 1:\n                time.sleep(0.1)\n\n            with self.mutex:\n                if gid != self.gid:\n                    return False\n\n                if self.pp:\n                    continue\n\n                self.pp = ProgressPrinter(self.log, self.args)\n                if not self.hub.is_dut:\n                    self.pp.start()\n\n            break\n\n        if gid:\n            self.log(\"reload #%d running\" % (gid,))\n\n        self.vfs = self.asrv.vfs\n        self.acct = self.asrv.acct\n        self.iacct = self.asrv.iacct\n        self.grps = self.asrv.grps\n\n        have_e2d = self.args.have_idp_hdrs or self.args.chpw or self.args.shr\n        vols = list(all_vols.values())\n        t0 = time.time()\n\n        if self.no_expr_idx:\n            modified = False\n            for vol in vols:\n                m, f = self._expr_idx_filter(vol.flags)\n                if m:\n                    vol.flags = f\n                    modified = True\n\n            if modified:\n                msg = \"disabling -e2t because your sqlite belongs in a museum\"\n                self.log(msg, c=3)\n\n        live_vols = []\n        with self.mutex, self.reg_mutex:\n            # only need to protect register_vpath but all in one go feels right\n            for vol in vols:\n                if bos.path.isfile(vol.realpath):\n                    self.volstate[vol.vpath] = \"online (just-a-file)\"\n                    t = \"NOTE: volume [/%s] is a file, not a folder\"\n                    self.log(t % (vol.vpath,))\n                    continue\n\n                try:\n                    # mkdir gonna happen at snap anyways;\n                    bos.makedirs(vol.realpath, vf=vol.flags)\n                    dir_is_empty(self.log_func, not self.args.no_scandir, vol.realpath)\n                except Exception as ex:\n                    self.volstate[vol.vpath] = \"OFFLINE (cannot access folder)\"\n                    self.log(\"cannot access %s: %r\" % (vol.realpath, ex), c=1)\n                    continue\n\n                if scan_vols and vol.vpath not in scan_vols:\n                    continue\n\n                if not self.register_vpath(vol.realpath, vol.flags):\n                    # self.log(\"db not enable for {}\".format(m, vol.realpath))\n                    continue\n\n                live_vols.append(vol)\n\n                if vol.vpath not in self.volstate:\n                    self.volstate[vol.vpath] = \"OFFLINE (pending initialization)\"\n\n        vols = live_vols\n        need_vac = {}\n\n        need_mtag = False\n        for vol in vols:\n            if \"e2t\" in vol.flags:\n                need_mtag = True\n\n        if need_mtag and not self.mtag:\n            self.mtag = MTag(self.log_func, self.args)\n            if not self.mtag.usable:\n                self.mtag = None\n\n        # e2ds(a) volumes first\n        if next((zv for zv in vols if \"e2ds\" in zv.flags), None):\n            self._block(\"indexing\")\n\n        if self.args.re_dhash or [zv for zv in vols if \"e2tsr\" in zv.flags]:\n            self.args.re_dhash = False\n            with self.mutex, self.reg_mutex:\n                self._drop_caches()\n\n        for vol in vols:\n            if self.stop or gid != self.gid:\n                break\n\n            en = set(vol.flags.get(\"mte\", {}))\n            self.entags[vol.realpath] = en\n\n            if \"e2d\" in vol.flags:\n                have_e2d = True\n\n            if \"e2ds\" in vol.flags or fscan:\n                self.volstate[vol.vpath] = \"busy (hashing files)\"\n                _, vac = self._build_file_index(vol, list(all_vols.values()))\n                if vac:\n                    need_vac[vol] = True\n\n            if \"e2v\" in vol.flags:\n                t = \"online (integrity-check pending)\"\n            elif \"e2ts\" in vol.flags:\n                t = \"online (tags pending)\"\n            else:\n                t = \"online, idle\"\n\n            self.volstate[vol.vpath] = t\n\n        self._unblock()\n\n        # file contents verification\n        for vol in vols:\n            if self.stop:\n                break\n\n            if \"e2v\" not in vol.flags:\n                continue\n\n            t = \"online (verifying integrity)\"\n            self.volstate[vol.vpath] = t\n            self.log(\"{} [{}]\".format(t, vol.realpath))\n\n            nmod = self._verify_integrity(vol)\n            if nmod:\n                self.log(\"modified {} entries in the db\".format(nmod), 3)\n                need_vac[vol] = True\n\n            if \"e2ts\" in vol.flags:\n                t = \"online (tags pending)\"\n            else:\n                t = \"online, idle\"\n\n            self.volstate[vol.vpath] = t\n\n        # open the rest + do any e2ts(a)\n        needed_mutagen = False\n        for vol in vols:\n            if self.stop:\n                break\n\n            if \"e2ts\" not in vol.flags:\n                continue\n\n            t = \"online (reading tags)\"\n            self.volstate[vol.vpath] = t\n            self.log(\"{} [{}]\".format(t, vol.realpath))\n\n            nadd, nrm, success = self._build_tags_index(vol)\n            if not success:\n                needed_mutagen = True\n\n            if nadd or nrm:\n                need_vac[vol] = True\n\n            self.volstate[vol.vpath] = \"online (mtp soon)\"\n\n        for vol in need_vac:\n            with self.mutex, self.reg_mutex:\n                reg = self.register_vpath(vol.realpath, vol.flags)\n\n            assert reg  # !rm\n            cur, _ = reg\n            with self.mutex:\n                cur.connection.commit()\n                cur.execute(\"vacuum\")\n\n        if self.stop:\n            return False\n\n        for vol in all_vols.values():\n            if vol.flags[\"dbd\"] == \"acid\":\n                continue\n\n            with self.mutex, self.reg_mutex:\n                reg = self.register_vpath(vol.realpath, vol.flags)\n\n            try:\n                assert reg  # !rm\n                cur, db_path = reg\n                if bos.path.getsize(db_path + \"-wal\") < 1024 * 1024 * 5:\n                    continue\n            except:\n                continue\n\n            try:\n                with self.mutex:\n                    cur.execute(\"pragma wal_checkpoint(truncate)\")\n                    try:\n                        cur.execute(\"commit\")  # absolutely necessary! for some reason\n                    except:\n                        pass\n\n                    cur.connection.commit()  # this one maybe not\n            except Exception as ex:\n                self.log(\"checkpoint failed: {}\".format(ex), 3)\n\n        if self.stop:\n            return False\n\n        self.pp.end = True\n\n        msg = \"{} volumes in {:.2f} sec\"\n        self.log(msg.format(len(vols), time.time() - t0))\n\n        if needed_mutagen:\n            msg = \"could not read tags because no backends are available (Mutagen or FFprobe)\"\n            self.log(msg, c=1)\n\n        t = \"online (running mtp)\" if self.mtag else \"online, idle\"\n        for vol in vols:\n            self.volstate[vol.vpath] = t\n\n        if self.mtag:\n            Daemon(self._run_all_mtp, \"up2k-mtp-scan\", (gid,))\n        else:\n            self.unpp()\n\n        return have_e2d\n\n    def register_vpath(\n        self, ptop: str, flags: dict[str, Any]\n    ) -> Optional[tuple[\"sqlite3.Cursor\", str]]:\n        \"\"\"mutex(main,reg) me\"\"\"\n        histpath = self.vfs.dbpaths.get(ptop)\n        if not histpath:\n            self.log(\"no dbpath for %r\" % (ptop,))\n            return None\n\n        db_path = os.path.join(histpath, \"up2k.db\")\n        if ptop in self.registry:\n            try:\n                return self.cur[ptop], db_path\n            except:\n                return None\n\n        vpath = \"?\"\n        for k, v in self.vfs.all_vols.items():\n            if v.realpath == ptop:\n                vpath = k\n\n        _, flags = self._expr_idx_filter(flags)\n        n4g = bool(flags.get(\"noforget\"))\n\n        ft = \"\\033[0;32m{}{:.0}\"\n        ff = \"\\033[0;35m{}{:.0}\"\n        fv = \"\\033[0;36m{}:\\033[90m{}\"\n        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\"\n        fx = set(zs.split())\n        fd = vf_bmap()\n        fd.update(vf_cmap())\n        fd.update(vf_vmap())\n        fd = {v: k for k, v in fd.items()}\n        fl = {\n            k: v\n            for k, v in flags.items()\n            if k not in fd\n            or (\n                v != getattr(self.args, fd[k])\n                and str(v) != str(getattr(self.args, fd[k]))\n            )\n        }\n        for k1, k2 in vf_cmap().items():\n            if k1 not in fl or k1 in fx:\n                continue\n            if str(fl[k1]) == str(getattr(self.args, k2)):\n                del fl[k1]\n            else:\n                fl[k1] = \",\".join(x for x in fl[k1])\n\n        if fl[\"chmod_d\"] == int(self.args.chmod_d, 8):\n            fl.pop(\"chmod_d\")\n        try:\n            if fl[\"chmod_f\"] == int(self.args.chmod_f or \"-1\", 8):\n                fl.pop(\"chmod_f\")\n        except:\n            pass\n        for k in (\"chmod_f\", \"chmod_d\"):\n            try:\n                fl[k] = \"%o\" % (fl[k])\n            except:\n                pass\n\n        a = [\n            (ft if v is True else ff if v is False else fv).format(k, str(v))\n            for k, v in fl.items()\n            if k not in fx\n        ]\n        if not a:\n            a = [\"\\033[90mall-default\"]\n\n        if a:\n            zs = \" \".join(sorted(a))\n            zs = zs.replace(\"90mre.compile(\", \"90m(\")  # nohash\n            self.log(\"/{} {}\".format(vpath + (\"/\" if vpath else \"\"), zs), \"35\")\n\n        reg = {}\n        drp = None\n        emptylist = []\n        dotpart = \".\" if self.args.dotpart else \"\"\n        snap = os.path.join(histpath, \"up2k.snap\")\n        if bos.path.exists(snap):\n            with gzip.GzipFile(snap, \"rb\") as f:\n                j = f.read().decode(\"utf-8\")\n\n            reg2 = json.loads(j)\n            try:\n                drp = reg2[\"droppable\"]\n                reg2 = reg2[\"registry\"]\n            except:\n                pass\n\n            reg = reg2  # diff-golf\n\n            if reg2 and \"dwrk\" not in reg2[next(iter(reg2))]:\n                for job in reg2.values():\n                    job[\"dwrk\"] = job[\"wark\"]\n\n            rm = []\n            for k, job in reg2.items():\n                job[\"ptop\"] = ptop\n                is_done = \"done\" in job\n                if is_done:\n                    job[\"need\"] = job[\"hash\"] = emptylist\n                else:\n                    if \"need\" not in job:\n                        job[\"need\"] = []\n                    if \"hash\" not in job:\n                        job[\"hash\"] = []\n\n                if is_done:\n                    fp = djoin(ptop, job[\"prel\"], job[\"name\"])\n                else:\n                    fp = djoin(ptop, job[\"prel\"], dotpart + job[\"name\"] + \".PARTIAL\")\n\n                if bos.path.exists(fp):\n                    if is_done:\n                        continue\n                    job[\"poke\"] = time.time()\n                    job[\"busy\"] = {}\n                else:\n                    self.log(\"ign deleted file in snap: %r\" % (fp,))\n                    if not n4g:\n                        rm.append(k)\n\n            for x in rm:\n                del reg2[x]\n\n            # optimize pre-1.15.4 entries\n            if next((x for x in reg.values() if \"done\" in x and \"poke\" in x), None):\n                zsl = \"host tnam busy sprs poke t0c\".split()\n                for job in reg.values():\n                    if \"done\" in job:\n                        for k in zsl:\n                            job.pop(k, None)\n\n            if drp is None:\n                drp = [k for k, v in reg.items() if not v[\"need\"]]\n            else:\n                drp = [x for x in drp if x in reg]\n\n            t = \"loaded snap {} |{}| ({})\".format(snap, len(reg.keys()), len(drp or []))\n            ta = [t] + self._vis_reg_progress(reg)\n            self.log(\"\\n\".join(ta))\n\n        self.flags[ptop] = flags\n        self.vol_act[ptop] = 0.0\n        self.registry[ptop] = reg\n        self.droppable[ptop] = drp or []\n        self.regdrop(ptop, \"\")\n        if not HAVE_SQLITE3 or \"e2d\" not in flags or \"d2d\" in flags:\n            return None\n\n        try:\n            if bos.makedirs(histpath):\n                hidedir(histpath)\n        except Exception as ex:\n            t = \"failed to initialize volume '/%s': %s\"\n            self.log(t % (vpath, ex), 1)\n            return None\n\n        try:\n            cur = self._open_db_wd(db_path)\n\n            # speeds measured uploading 520 small files on a WD20SPZX (SMR 2.5\" 5400rpm 4kb)\n            dbd = flags[\"dbd\"]\n            if dbd == \"acid\":\n                # 217.5s; python-defaults\n                zs = \"delete\"\n                sync = \"full\"\n            elif dbd == \"swal\":\n                # 88.0s; still 99.9% safe (can lose a bit of on OS crash)\n                zs = \"wal\"\n                sync = \"full\"\n            elif dbd == \"yolo\":\n                # 2.7s; may lose entire db on OS crash\n                zs = \"wal\"\n                sync = \"off\"\n            else:\n                # 4.1s; corruption-safe but more likely to lose wal\n                zs = \"wal\"\n                sync = \"normal\"\n\n            try:\n                amode = cur.execute(\"pragma journal_mode=\" + zs).fetchone()[0]\n                if amode.lower() != zs.lower():\n                    t = \"sqlite failed to set journal_mode {}; got {}\"\n                    raise Exception(t.format(zs, amode))\n            except Exception as ex:\n                if sync != \"off\":\n                    sync = \"full\"\n                    t = \"reverting to sync={} because {}\"\n                    self.log(t.format(sync, ex))\n\n            cur.execute(\"pragma synchronous=\" + sync)\n            cur.connection.commit()\n\n            self._verify_db_cache(cur, vpath)\n\n            self.cur[ptop] = cur\n            self.volsize[cur] = 0\n            self.volnfiles[cur] = 0\n\n            return cur, db_path\n        except:\n            msg = \"ERROR: cannot use database at [%s]:\\n%s\\n\\033[33mhint: %s\\n\"\n            self.log(msg % (db_path, traceback.format_exc(), HINT_HISTPATH), 1)\n\n        return None\n\n    def _verify_db_cache(self, cur: \"sqlite3.Cursor\", vpath: str) -> None:\n        # check if list of intersecting volumes changed since last use; drop caches if so\n        prefix = (vpath + \"/\").lstrip(\"/\")\n        vps = [x for x in self.vfs.all_vols if x.startswith(prefix)]\n        vps.sort()\n        seed = [x[len(prefix) :] for x in vps]\n\n        # also consider volflags which affect indexing\n        for vp in vps:\n            vf = self.vfs.all_vols[vp].flags\n            vf = {k: v for k, v in vf.items() if k in VF_AFFECTS_INDEXING}\n            seed.append(str(sorted(vf.items())))\n\n        zb = hashlib.sha1(\"\\n\".join(seed).encode(\"utf-8\", \"replace\")).digest()\n        vcfg = ub64enc(zb[:18]).decode(\"ascii\")\n\n        c = cur.execute(\"select v from kv where k = 'volcfg'\")\n        try:\n            (oldcfg,) = c.fetchone()\n        except:\n            oldcfg = \"\"\n\n        if oldcfg != vcfg:\n            cur.execute(\"delete from kv where k = 'volcfg'\")\n            cur.execute(\"delete from dh\")\n            cur.execute(\"delete from cv\")\n            cur.execute(\"insert into kv values ('volcfg',?)\", (vcfg,))\n            cur.connection.commit()\n\n    def _build_file_index(self, vol: VFS, all_vols: list[VFS]) -> tuple[bool, bool]:\n        do_vac = False\n        top = vol.realpath\n        rei = vol.flags.get(\"noidx\")\n        reh = vol.flags.get(\"nohash\")\n        n4g = bool(vol.flags.get(\"noforget\"))\n        ffat = \"fat32\" in vol.flags\n        cst = bos.stat(top)\n        dev = cst.st_dev if vol.flags.get(\"xdev\") else 0\n\n        with self.mutex:\n            with self.reg_mutex:\n                reg = self.register_vpath(top, vol.flags)\n\n            assert reg and self.pp  # !rm\n            cur, db_path = reg\n\n            db = Dbw(cur, 0, 0, time.time())\n            self.pp.n = next(db.c.execute(\"select count(w) from up\"))[0]\n\n            excl = [\n                vol.realpath + \"/\" + d.vpath[len(vol.vpath) :].lstrip(\"/\")\n                for d in all_vols\n                if d != vol and (d.vpath.startswith(vol.vpath + \"/\") or not vol.vpath)\n            ]\n            excl += [absreal(x) for x in excl]\n            excl += list(self.vfs.histtab.values())\n            excl += list(self.vfs.dbpaths.values())\n            if WINDOWS:\n                excl = [x.replace(\"/\", \"\\\\\") for x in excl]\n            else:\n                # ~/.wine/dosdevices/z:/ and such\n                excl.extend((\"/dev\", \"/proc\", \"/run\", \"/sys\"))\n\n            excl = list({k: 1 for k in excl})\n\n            if self.args.re_dirsz:\n                db.c.execute(\"delete from ds\")\n                db.n += 1\n\n            rtop = absreal(top)\n            n_add = n_rm = 0\n            try:\n                if dir_is_empty(self.log_func, not self.args.no_scandir, rtop):\n                    t = \"volume /%s at [%s] is empty; will not be indexed as this could be due to an offline filesystem\"\n                    self.log(t % (vol.vpath, rtop), 6)\n                    return True, False\n                if not vol.check_landmarks():\n                    t = \"volume /%s at [%s] will not be indexed due to bad landmarks\"\n                    self.log(t % (vol.vpath, rtop), 6)\n                    return True, False\n\n                n_add, _, _ = self._build_dir(\n                    db,\n                    top,\n                    set(excl),\n                    top,\n                    rtop,\n                    rei,\n                    reh,\n                    n4g,\n                    ffat,\n                    [],\n                    cst,\n                    dev,\n                    bool(vol.flags.get(\"xvol\")),\n                )\n                if not n4g:\n                    n_rm = self._drop_lost(db.c, top, excl)\n            except Exception as ex:\n                t = \"failed to index volume [{}]:\\n{}\"\n                self.log(t.format(top, min_ex()), c=1)\n                if db_ex_chk(self.log, ex, db_path):\n                    self.hub.log_stacks()\n\n            if db.n:\n                self.log(\"commit %d new files; %d updates\" % (db.nf, db.n))\n\n            if self.args.no_dhash:\n                if db.c.execute(\"select d from dh\").fetchone():\n                    db.c.execute(\"delete from dh\")\n                    self.log(\"forgetting dhashes in {}\".format(top))\n            elif n_add or n_rm:\n                self._set_tagscan(db.c, True)\n\n            db.c.connection.commit()\n\n            if (\n                vol.flags.get(\"vmaxb\")\n                or vol.flags.get(\"vmaxn\")\n                or (self.args.stats and not self.args.nos_vol)\n            ):\n                zs = \"select count(sz), sum(sz) from up\"\n                vn, vb = db.c.execute(zs).fetchone()\n                vb = vb or 0\n                vb += vn * 2048\n                self.volsize[db.c] = vb\n                self.volnfiles[db.c] = vn\n                vmaxb = unhumanize(vol.flags.get(\"vmaxb\") or \"0\")\n                vmaxn = unhumanize(vol.flags.get(\"vmaxn\") or \"0\")\n                t = \"{:>5} / {:>5}  ( {:>5} / {:>5} files) in {}\".format(\n                    humansize(vb, True),\n                    humansize(vmaxb, True),\n                    humansize(vn, True).rstrip(\"B\"),\n                    humansize(vmaxn, True).rstrip(\"B\"),\n                    vol.realpath,\n                )\n                self.log(t)\n\n            return True, bool(n_add or n_rm or do_vac)\n\n    def _fika(self, db: Dbw) -> None:\n        zs = self.fika\n        self.fika = \"\"\n        if zs not in self.args.fika:\n            return\n\n        t = \"fika(%s); commit %d new files; %d updates\"\n        self.log(t % (zs, db.nf, db.n))\n        db.c.connection.commit()\n        db.n = db.nf = 0\n        db.t = time.time()\n\n        self.mutex.release()\n        time.sleep(0.5)\n        self.mutex.acquire()\n        db.t = time.time()\n\n    def _build_dir(\n        self,\n        db: Dbw,\n        top: str,\n        excl: set[str],\n        cdir: str,\n        rcdir: str,\n        rei: Optional[Pattern[str]],\n        reh: Optional[Pattern[str]],\n        n4g: bool,\n        ffat: bool,\n        seen: list[str],\n        cst: os.stat_result,\n        dev: int,\n        xvol: bool,\n    ) -> tuple[int, int, int]:\n        if xvol and not rcdir.startswith(top):\n            self.log(\"skip xvol: %r -> %r\" % (cdir, rcdir), 6)\n            return 0, 0, 0\n\n        if rcdir in seen:\n            t = \"bailing from symlink loop,\\n  prev: %r\\n  curr: %r\\n  from: %r\"\n            self.log(t % (seen[-1], rcdir, cdir), 3)\n            return 0, 0, 0\n\n        # total-files-added, total-num-files, recursive-size\n        tfa = tnf = rsz = 0\n        seen = seen + [rcdir]\n        unreg: list[str] = []\n        files: list[tuple[int, int, str]] = []\n        fat32 = True\n        cv = vcv = acv = \"\"\n        e_d = {}\n\n        th_cvd = self.args.th_coversd\n        th_cvds = self.args.th_coversd_set\n        scan_pr_s = self.args.scan_pr_s\n\n        assert self.pp and self.mem_cur  # !rm\n        self.pp.msg = \"a%d %s\" % (self.pp.n, cdir)\n\n        rd = cdir[len(top) :].strip(\"/\")\n        if WINDOWS:\n            rd = rd.replace(\"\\\\\", \"/\").strip(\"/\")\n\n        rds = rd + \"/\" if rd else \"\"\n        cdirs = cdir + os.sep\n\n        g = statdir(self.log_func, not self.args.no_scandir, True, cdir, False)\n        gl = sorted(g)\n        partials = set([x[0] for x in gl if \"PARTIAL\" in x[0]])\n        for iname, inf in gl:\n            if self.fika:\n                if not self.stop:\n                    self._fika(db)\n                if self.stop:\n                    self.fika = \"f\"\n                    return -1, 0, 0\n\n            rp = rds + iname\n            abspath = cdirs + iname\n\n            if rei and rei.search(abspath):\n                unreg.append(rp)\n                continue\n\n            lmod = int(inf.st_mtime)\n            if stat.S_ISLNK(inf.st_mode):\n                try:\n                    inf = bos.stat(abspath)\n                except:\n                    continue\n\n            sz = inf.st_size\n            if fat32 and not ffat and inf.st_mtime % 2:\n                fat32 = False\n\n            if stat.S_ISDIR(inf.st_mode):\n                rap = absreal(abspath)\n                if (\n                    dev\n                    and inf.st_dev != dev\n                    and not (ANYWIN and bos.stat(rap).st_dev == dev)\n                ):\n                    self.log(\"skip xdev %s->%s: %r\" % (dev, inf.st_dev, abspath), 6)\n                    continue\n                if abspath in excl or rap in excl:\n                    unreg.append(rp)\n                    continue\n                if iname == \".th\" and bos.path.isdir(os.path.join(abspath, \"top\")):\n                    # abandoned or foreign, skip\n                    continue\n                # self.log(\" dir: {}\".format(abspath))\n                try:\n                    i1, i2, i3 = self._build_dir(\n                        db,\n                        top,\n                        excl,\n                        abspath,\n                        rap,\n                        rei,\n                        reh,\n                        n4g,\n                        fat32,\n                        seen,\n                        inf,\n                        dev,\n                        xvol,\n                    )\n                    tfa += i1\n                    tnf += i2\n                    rsz += i3\n                except:\n                    t = \"failed to index subdir %r:\\n%s\"\n                    self.log(t % (abspath, min_ex()), 1)\n            elif not stat.S_ISREG(inf.st_mode):\n                self.log(\"skip type-0%o file %r\" % (inf.st_mode, abspath))\n            else:\n                # self.log(\"file: {}\".format(abspath))\n                if rp.endswith(\".PARTIAL\") and time.time() - lmod < 60:\n                    # rescan during upload\n                    continue\n\n                if not sz and (\n                    \"%s.PARTIAL\" % (iname,) in partials\n                    or \".%s.PARTIAL\" % (iname,) in partials\n                ):\n                    # placeholder for unfinished upload\n                    continue\n\n                rsz += sz\n                files.append((sz, lmod, iname))\n                if sz:\n                    liname = iname.lower()\n                    ext = liname.rsplit(\".\", 1)[-1]\n                    if (\n                        liname in th_cvds\n                        or (not cv and ext in ICV_EXTS and not iname.startswith(\".\"))\n                    ) and (\n                        not cv\n                        or liname not in th_cvds\n                        or cv.lower() not in th_cvds\n                        or th_cvd.index(liname) < th_cvd.index(cv.lower())\n                    ):\n                        cv = iname\n                    elif not vcv and ext in VCV_EXTS and not iname.startswith(\".\"):\n                        vcv = iname\n                    elif not acv and ext in ACV_EXTS and not iname.startswith(\".\"):\n                        acv = iname\n\n        if not cv:\n            cv = vcv or acv\n\n        if not self.args.no_dirsz:\n            tnf += len(files)\n            q = \"select sz, nf from ds where rd=? limit 1\"\n            try:\n                db_sz, db_nf = db.c.execute(q, (rd,)).fetchone() or (-1, -1)\n                if rsz != db_sz or tnf != db_nf:\n                    db.c.execute(\"delete from ds where rd=?\", (rd,))\n                    db.c.execute(\"insert into ds values (?,?,?)\", (rd, rsz, tnf))\n                    db.n += 1\n            except:\n                pass  # mojibake rd\n\n        # folder of 1000 files = ~1 MiB RAM best-case (tiny filenames);\n        # free up stuff we're done with before dhashing\n        gl = []\n        partials.clear()\n        if not self.args.no_dhash:\n            if len(files) < 9000:\n                zh = hashlib.sha1(str(files).encode(\"utf-8\", \"replace\"))\n            else:\n                zh = hashlib.sha1()\n                _ = [zh.update(str(x).encode(\"utf-8\", \"replace\")) for x in files]\n\n            zh.update(cv.encode(\"utf-8\", \"replace\"))\n            zh.update(spack(b\"<d\", cst.st_mtime))\n            dhash = ub64enc(zh.digest()[:12]).decode(\"ascii\")\n            sql = \"select d from dh where d=? and +h=?\"\n            try:\n                c = db.c.execute(sql, (rd, dhash))\n                drd = rd\n            except:\n                drd = \"//\" + w8b64enc(rd)\n                c = db.c.execute(sql, (drd, dhash))\n\n            if c.fetchone():\n                return tfa, tnf, rsz\n\n        if cv and rd:\n            # mojibake not supported (for performance / simplicity):\n            try:\n                q = \"select * from cv where rd=? and dn=? and +fn=?\"\n                crd, cdn = rd.rsplit(\"/\", 1) if \"/\" in rd else (\"\", rd)\n                if not db.c.execute(q, (crd, cdn, cv)).fetchone():\n                    db.c.execute(\"delete from cv where rd=? and dn=?\", (crd, cdn))\n                    db.c.execute(\"insert into cv values (?,?,?)\", (crd, cdn, cv))\n                    db.n += 1\n            except Exception as ex:\n                self.log(\"cover %r/%r failed: %s\" % (rd, cv, ex), 6)\n\n        seen_files = set([x[2] for x in files])  # for dropcheck\n        for sz, lmod, fn in files:\n            if self.fika:\n                if not self.stop:\n                    self._fika(db)\n                if self.stop:\n                    self.fika = \"f\"\n                    return -1, 0, 0\n\n            rp = rds + fn\n            abspath = cdirs + fn\n            nohash = reh.search(abspath) if reh else False\n\n            sql = \"select w, mt, sz, ip, at, un from up where rd = ? and fn = ?\"\n            try:\n                c = db.c.execute(sql, (rd, fn))\n            except:\n                c = db.c.execute(sql, s3enc(self.mem_cur, rd, fn))\n\n            in_db = list(c.fetchall())\n            if in_db:\n                self.pp.n -= 1\n                dw, dts, dsz, ip, at, un = in_db[0]\n                if len(in_db) > 1:\n                    t = \"WARN: multiple entries: %r => %r |%d|\\n%r\"\n                    rep_db = \"\\n\".join([repr(x) for x in in_db])\n                    self.log(t % (top, rp, len(in_db), rep_db))\n                    dts = -1\n\n                if fat32 and abs(dts - lmod) == 1:\n                    dts = lmod\n\n                if dts == lmod and dsz == sz and (nohash or dw[0] != \"#\" or not sz):\n                    continue\n\n                if un is None:\n                    un = \"\"\n\n                t = \"reindex %r => %r mtime(%s/%s) size(%s/%s)\"\n                self.log(t % (top, rp, dts, lmod, dsz, sz))\n                self.db_rm(db.c, e_d, rd, fn, 0)\n                tfa += 1\n                db.n += 1\n                in_db = []\n            else:\n                dw = \"\"\n                ip = \"\"\n                at = 0\n                un = \"\"\n\n            self.pp.msg = \"a%d %s\" % (self.pp.n, abspath)\n\n            if nohash or not sz:\n                wark = up2k_wark_from_metadata(self.salt, sz, lmod, rd, fn)\n            else:\n                if sz > 1024 * 1024 * scan_pr_s:\n                    self.log(\"file: %r\" % (abspath,))\n\n                try:\n                    hashes, _ = self._hashlist_from_file(\n                        abspath, \"a{}, \".format(self.pp.n)\n                    )\n                except Exception as ex:\n                    self._ex_hash(ex, abspath)\n                    continue\n\n                if not hashes:\n                    return -1, 0, 0\n\n                wark = up2k_wark_from_hashlist(self.salt, sz, hashes)\n\n            if dw and dw != wark:\n                ip = \"\"\n                at = 0\n                un = \"\"\n\n            # skip upload hooks by not providing vflags\n            self.db_add(db.c, e_d, rd, fn, lmod, sz, \"\", \"\", wark, wark, \"\", un, ip, at)\n            db.n += 1\n            db.nf += 1\n            tfa += 1\n            td = time.time() - db.t\n            if db.n >= 4096 or td >= 60:\n                self.log(\"commit %d new files; %d updates\" % (db.nf, db.n))\n                db.c.connection.commit()\n                db.n = db.nf = 0\n                db.t = time.time()\n\n        if not self.args.no_dhash:\n            db.c.execute(\"delete from dh where d = ?\", (drd,))  # type: ignore\n            db.c.execute(\"insert into dh values (?,?)\", (drd, dhash))  # type: ignore\n\n        if self.stop:\n            return -1, 0, 0\n\n        # drop shadowed folders\n        for sh_rd in unreg:\n            n = 0\n            q = \"select count(w) from up where (rd=? or rd like ?||'/%')\"\n            for sh_erd in [sh_rd, \"//\" + w8b64enc(sh_rd)]:\n                try:\n                    erd_erd = (sh_erd, sh_erd)\n                    n = db.c.execute(q, erd_erd).fetchone()[0]\n                    break\n                except:\n                    pass\n\n            assert erd_erd  # type: ignore  # !rm\n\n            if n:\n                t = \"forgetting %d shadowed autoindexed files in %r > %r\"\n                self.log(t % (n, top, sh_rd))\n\n                q = \"delete from dh where (d = ? or d like ?||'/%')\"\n                db.c.execute(q, erd_erd)\n\n                q = \"delete from up where (rd=? or rd like ?||'/%')\"\n                db.c.execute(q, erd_erd)\n                tfa += n\n\n            q = \"delete from ds where (rd=? or rd like ?||'/%')\"\n            db.c.execute(q, erd_erd)\n\n        if n4g:\n            return tfa, tnf, rsz\n\n        # drop missing files\n        q = \"select fn from up where rd = ?\"\n        try:\n            c = db.c.execute(q, (rd,))\n        except:\n            c = db.c.execute(q, (\"//\" + w8b64enc(rd),))\n\n        hits = [w8b64dec(x[2:]) if x.startswith(\"//\") else x for (x,) in c]\n        rm_files = [x for x in hits if x not in seen_files]\n        n_rm = len(rm_files)\n        for fn in rm_files:\n            self.db_rm(db.c, e_d, rd, fn, 0)\n\n        if n_rm:\n            self.log(\"forgot {} deleted files\".format(n_rm))\n\n        return tfa, tnf, rsz\n\n    def _drop_lost(self, cur: \"sqlite3.Cursor\", top: str, excl: list[str]) -> int:\n        rm = []\n        n_rm = 0\n        nchecked = 0\n        assert self.pp\n\n        # `_build_dir` did all unshadowed files; first do dirs:\n        ndirs = next(cur.execute(\"select count(distinct rd) from up\"))[0]\n        c = cur.execute(\"select distinct rd from up order by rd desc\")\n        for (drd,) in c:\n            nchecked += 1\n            if drd.startswith(\"//\"):\n                rd = w8b64dec(drd[2:])\n            else:\n                rd = drd\n\n            abspath = djoin(top, rd)\n            self.pp.msg = \"b%d %s\" % (ndirs - nchecked, abspath)\n            try:\n                if os.path.isdir(abspath):\n                    continue\n            except:\n                pass\n\n            rm.append(drd)\n\n        if rm:\n            q = \"select count(w) from up where rd = ?\"\n            for rd in rm:\n                n_rm += next(cur.execute(q, (rd,)))[0]\n\n            self.log(\"forgetting {} deleted dirs, {} files\".format(len(rm), n_rm))\n            for rd in rm:\n                cur.execute(\"delete from dh where d = ?\", (rd,))\n                cur.execute(\"delete from up where rd = ?\", (rd,))\n\n        # then shadowed deleted files\n        n_rm2 = 0\n        c2 = cur.connection.cursor()\n        excl = [x[len(top) + 1 :] for x in excl if x.startswith(top + \"/\")]\n        q = \"select rd, fn from up where (rd = ? or rd like ?||'%') order by rd\"\n        for rd in excl:\n            for erd in [rd, \"//\" + w8b64enc(rd)]:\n                try:\n                    c = cur.execute(q, (erd, erd + \"/\"))\n                    break\n                except:\n                    pass\n\n            crd = \"///\"\n            cdc: set[str] = set()\n            for drd, dfn in c:\n                rd, fn = s3dec(drd, dfn)\n\n                if crd != rd:\n                    crd = rd\n                    try:\n                        cdc = set(os.listdir(djoin(top, rd)))\n                    except:\n                        cdc.clear()\n\n                if fn not in cdc:\n                    q = \"delete from up where rd = ? and fn = ?\"\n                    c2.execute(q, (drd, dfn))\n                    n_rm2 += 1\n\n        if n_rm2:\n            self.log(\"forgetting {} shadowed deleted files\".format(n_rm2))\n\n        c2.connection.commit()\n\n        # then covers\n        n_rm3 = 0\n        qu = \"select 1 from up where rd=? and fn=? limit 1\"\n        q = \"delete from cv where rd=? and dn=? and +fn=?\"\n        for crd, cdn, fn in cur.execute(\"select * from cv\"):\n            urd = vjoin(crd, cdn)\n            if not c2.execute(qu, (urd, fn)).fetchone():\n                c2.execute(q, (crd, cdn, fn))\n                n_rm3 += 1\n\n        if n_rm3:\n            self.log(\"forgetting {} deleted covers\".format(n_rm3))\n\n        c2.connection.commit()\n        c2.close()\n        return n_rm + n_rm2\n\n    def _verify_integrity(self, vol: VFS) -> int:\n        \"\"\"expensive; blocks database access until finished\"\"\"\n        ptop = vol.realpath\n        assert self.pp\n\n        cur = self.cur[ptop]\n        rei = vol.flags.get(\"noidx\")\n        reh = vol.flags.get(\"nohash\")\n        e2vu = \"e2vu\" in vol.flags\n        e2vp = \"e2vp\" in vol.flags\n\n        excl = [\n            d[len(vol.vpath) :].lstrip(\"/\")\n            for d in self.vfs.all_vols\n            if d != vol.vpath and (d.startswith(vol.vpath + \"/\") or not vol.vpath)\n        ]\n        qexa: list[str] = []\n        pexa: list[str] = []\n        for vpath in excl:\n            qexa.append(\"up.rd != ? and not up.rd like ?||'%'\")\n            pexa.extend([vpath, vpath])\n\n        pex: tuple[Any, ...] = tuple(pexa)\n        qex = \" and \".join(qexa)\n        if qex:\n            qex = \" where \" + qex\n\n        rewark: list[tuple[str, str, str, int, int]] = []\n        f404: list[tuple[str, str, str]] = []\n\n        with self.mutex:\n            b_left = 0\n            n_left = 0\n            q = \"select sz from up\" + qex\n            for (sz,) in cur.execute(q, pex):\n                b_left += sz  # sum() can overflow according to docs\n                n_left += 1\n\n            tf, _ = self._spool_warks(cur, \"select w, rd, fn from up\" + qex, pex, 0)\n\n        with gzip.GzipFile(mode=\"rb\", fileobj=tf) as gf:\n            for zb in gf:\n                if self.stop:\n                    return -1\n\n                zs = zb[:-1].decode(\"utf-8\").replace(\"\\x00\\x02\", \"\\n\")\n                w, drd, dfn = zs.split(\"\\x00\\x01\")\n                with self.mutex:\n                    q = \"select mt, sz from up where rd=? and fn=? and +w=?\"\n                    try:\n                        mt, sz = cur.execute(q, (drd, dfn, w)).fetchone()\n                    except:\n                        # file moved/deleted since spooling\n                        continue\n\n                n_left -= 1\n                b_left -= sz\n                if drd.startswith(\"//\") or dfn.startswith(\"//\"):\n                    rd, fn = s3dec(drd, dfn)\n                else:\n                    rd = drd\n                    fn = dfn\n\n                abspath = djoin(ptop, rd, fn)\n                if rei and rei.search(abspath):\n                    continue\n\n                nohash = reh.search(abspath) if reh else False\n\n                pf = \"v{}, {:.0f}+\".format(n_left, b_left / 1024 / 1024)\n                self.pp.msg = pf + abspath\n\n                try:\n                    stl = bos.lstat(abspath)\n                    st = bos.stat(abspath) if stat.S_ISLNK(stl.st_mode) else stl\n                except Exception as ex:\n                    self.log(\"missing file: %r\" % (abspath,), 3)\n                    f404.append((drd, dfn, w))\n                    continue\n\n                mt2 = int(stl.st_mtime)\n                sz2 = st.st_size\n\n                if nohash or not sz2:\n                    w2 = up2k_wark_from_metadata(self.salt, sz2, mt2, rd, fn)\n                else:\n                    if sz2 > 1024 * 1024 * 32:\n                        self.log(\"file: %r\" % (abspath,))\n\n                    try:\n                        hashes, _ = self._hashlist_from_file(abspath, pf)\n                    except Exception as ex:\n                        self._ex_hash(ex, abspath)\n                        continue\n\n                    if not hashes:\n                        return -1\n\n                    w2 = up2k_wark_from_hashlist(self.salt, sz2, hashes)\n\n                if w == w2:\n                    continue\n\n                # symlink mtime was inconsistent before v1.9.4; check if that's it\n                if st != stl and (nohash or not sz2):\n                    mt2b = int(st.st_mtime)\n                    w2b = up2k_wark_from_metadata(self.salt, sz2, mt2b, rd, fn)\n                    if w == w2b:\n                        continue\n\n                rewark.append((drd, dfn, w2, sz2, mt2))\n\n                t = \"hash mismatch: %r\\n  db: %s (%d byte, %d)\\n  fs: %s (%d byte, %d)\"\n                self.log(t % (abspath, w, sz, mt, w2, sz2, mt2), 1)\n\n        if e2vp and (rewark or f404):\n            self.hub.retcode = 1\n            Daemon(self.hub.sigterm)\n            t = \"in volume /%s:  %s files missing, %s files have incorrect hashes\"\n            t = t % (vol.vpath, len(f404), len(rewark))\n            self.log(t, 1)\n            raise Exception(t)\n\n        if not e2vu or (not rewark and not f404):\n            return 0\n\n        with self.mutex:\n            q = \"update up set w=?, sz=?, mt=? where rd=? and fn=?\"\n            for rd, fn, w, sz, mt in rewark:\n                cur.execute(q, (w, sz, int(mt), rd, fn))\n\n            if f404:\n                q = \"delete from up where rd=? and fn=? and +w=?\"\n                cur.executemany(q, f404)\n\n            cur.connection.commit()\n\n        return len(rewark) + len(f404)\n\n    def _build_tags_index(self, vol: VFS) -> tuple[int, int, bool]:\n        ptop = vol.realpath\n        with self.mutex, self.reg_mutex:\n            reg = self.register_vpath(ptop, vol.flags)\n\n        assert reg and self.pp\n        cur = self.cur[ptop]\n\n        if not self.args.no_dhash:\n            with self.mutex:\n                c = cur.execute(\"select k from kv where k = 'tagscan'\")\n                if not c.fetchone():\n                    return 0, 0, bool(self.mtag)\n\n        ret = self._build_tags_index_2(ptop)\n\n        with self.mutex:\n            self._set_tagscan(cur, False)\n            cur.connection.commit()\n\n        return ret\n\n    def _drop_caches(self) -> None:\n        \"\"\"mutex(main,reg) me\"\"\"\n        self.log(\"dropping caches for a full filesystem scan\")\n        for vol in self.vfs.all_vols.values():\n            reg = self.register_vpath(vol.realpath, vol.flags)\n            if not reg:\n                continue\n\n            cur, _ = reg\n            self._set_tagscan(cur, True)\n            cur.execute(\"delete from dh\")\n            cur.execute(\"delete from cv\")\n            cur.connection.commit()\n\n    def _set_tagscan(self, cur: \"sqlite3.Cursor\", need: bool) -> bool:\n        if self.args.no_dhash:\n            return False\n\n        c = cur.execute(\"select k from kv where k = 'tagscan'\")\n        if bool(c.fetchone()) == need:\n            return False\n\n        if need:\n            cur.execute(\"insert into kv values ('tagscan',1)\")\n        else:\n            cur.execute(\"delete from kv where k = 'tagscan'\")\n\n        return True\n\n    def _build_tags_index_2(self, ptop: str) -> tuple[int, int, bool]:\n        entags = self.entags[ptop]\n        flags = self.flags[ptop]\n        cur = self.cur[ptop]\n\n        n_add = 0\n        n_rm = 0\n        if \"e2tsr\" in flags:\n            with self.mutex:\n                n_rm = cur.execute(\"select count(w) from mt\").fetchone()[0]\n                if n_rm:\n                    self.log(\"discarding {} media tags for a full rescan\".format(n_rm))\n                    cur.execute(\"delete from mt\")\n\n        # integrity: drop tags for tracks that were deleted\n        if \"e2t\" in flags:\n            with self.mutex:\n                n = 0\n                c2 = cur.connection.cursor()\n                up_q = \"select w from up where substr(w,1,16) = ?\"\n                rm_q = \"delete from mt where w = ?\"\n                for (w,) in cur.execute(\"select w from mt\"):\n                    if not c2.execute(up_q, (w,)).fetchone():\n                        c2.execute(rm_q, (w[:16],))\n                        n += 1\n\n                c2.close()\n                if n:\n                    t = \"discarded media tags for {} deleted files\"\n                    self.log(t.format(n))\n                    n_rm += n\n\n        with self.mutex:\n            cur.connection.commit()\n\n        # bail if a volflag disables indexing\n        if \"d2t\" in flags or \"d2d\" in flags:\n            return 0, n_rm, True\n\n        # add tags for new files\n        if \"e2ts\" in flags:\n            if not self.mtag:\n                return 0, n_rm, False\n\n            nq = 0\n            with self.mutex:\n                tf, nq = self._spool_warks(\n                    cur, \"select w from up order by rd, fn\", (), 1\n                )\n\n            if not nq:\n                # self.log(\"tags ok\")\n                self._unspool(tf)\n                return 0, n_rm, True\n\n            if nq == -1:\n                return -1, -1, True\n\n            with gzip.GzipFile(mode=\"rb\", fileobj=tf) as gf:\n                n_add = self._e2ts_q(gf, nq, cur, ptop, entags)\n\n            self._unspool(tf)\n\n        return n_add, n_rm, True\n\n    def _e2ts_q(\n        self,\n        qf: gzip.GzipFile,\n        nq: int,\n        cur: \"sqlite3.Cursor\",\n        ptop: str,\n        entags: set[str],\n    ) -> int:\n        assert self.pp and self.mtag\n\n        flags = self.flags[ptop]\n        mpool: Optional[Queue[Mpqe]] = None\n        if self.mtag.prefer_mt and self.args.mtag_mt > 1:\n            mpool = self._start_mpool()\n\n        n_add = 0\n        n_buf = 0\n        last_write = time.time()\n        for bw in qf:\n            if self.stop:\n                return -1\n\n            w = bw[:-1].decode(\"ascii\")\n            w16 = w[:16]\n\n            with self.mutex:\n                try:\n                    q = \"select rd, fn, ip, at, un from up where substr(w,1,16)=? and +w=?\"\n                    rd, fn, ip, at, un = cur.execute(q, (w16, w)).fetchone()\n                except:\n                    # file modified/deleted since spooling\n                    continue\n\n                if rd.startswith(\"//\") or fn.startswith(\"//\"):\n                    rd, fn = s3dec(rd, fn)\n\n                if \"mtp\" in flags:\n                    q = \"select 1 from mt where w=? and +k='t:mtp' limit 1\"\n                    if cur.execute(q, (w16,)).fetchone():\n                        continue\n\n                    q = \"insert into mt values (?,'t:mtp','a')\"\n                    cur.execute(q, (w16,))\n\n            abspath = djoin(ptop, rd, fn)\n            self.pp.msg = \"c%d %s\" % (nq, abspath)\n            if not mpool:\n                n_tags = self._tagscan_file(cur, entags, flags, w, abspath, ip, at, un)\n            else:\n                oth_tags = {}\n                if ip:\n                    oth_tags[\"up_ip\"] = ip\n                if at:\n                    oth_tags[\"up_at\"] = at\n                if un:\n                    oth_tags[\"up_by\"] = un\n\n                mpool.put(Mpqe({}, entags, flags, w, abspath, oth_tags))\n                with self.mutex:\n                    n_tags = len(self._flush_mpool(cur))\n\n            n_add += n_tags\n            n_buf += n_tags\n            nq -= 1\n\n            td = time.time() - last_write\n            if n_buf >= 4096 or td >= self.timeout / 2:\n                self.log(\"commit {} new tags\".format(n_buf))\n                with self.mutex:\n                    cur.connection.commit()\n\n                last_write = time.time()\n                n_buf = 0\n\n        if mpool:\n            self._stop_mpool(mpool)\n            with self.mutex:\n                n_add += len(self._flush_mpool(cur))\n\n        with self.mutex:\n            cur.connection.commit()\n\n        return n_add\n\n    def _spool_warks(\n        self,\n        cur: \"sqlite3.Cursor\",\n        q: str,\n        params: tuple[Any, ...],\n        flt: int,\n    ) -> tuple[tempfile.SpooledTemporaryFile[bytes], int]:\n        \"\"\"mutex(main) me\"\"\"\n        n = 0\n        c2 = cur.connection.cursor()\n        tf = tempfile.SpooledTemporaryFile(1024 * 1024 * 8, \"w+b\", prefix=\"cpp-tq-\")\n        with gzip.GzipFile(mode=\"wb\", fileobj=tf) as gf:\n            for row in cur.execute(q, params):\n                if self.stop:\n                    return tf, -1\n\n                if flt == 1:\n                    q = \"select 1 from mt where w=? and +k != 't:mtp'\"\n                    if c2.execute(q, (row[0][:16],)).fetchone():\n                        continue\n\n                zs = \"\\x00\\x01\".join(row).replace(\"\\n\", \"\\x00\\x02\")\n                gf.write((zs + \"\\n\").encode(\"utf-8\"))\n                n += 1\n\n        c2.close()\n        tf.seek(0)\n        self.spools.add(tf)\n        return tf, n\n\n    def _unspool(self, tf: tempfile.SpooledTemporaryFile[bytes]) -> None:\n        try:\n            self.spools.remove(tf)\n        except:\n            return\n\n        try:\n            tf.close()\n        except Exception as ex:\n            self.log(\"failed to delete spool: {}\".format(ex), 3)\n\n    def _flush_mpool(self, wcur: \"sqlite3.Cursor\") -> list[str]:\n        ret = []\n        for x in self.pending_tags:\n            self._tag_file(wcur, *x)\n            ret.append(x[1])\n\n        self.pending_tags = []\n        return ret\n\n    def _run_all_mtp(self, gid: int) -> None:\n        t0 = time.time()\n        for ptop, flags in self.flags.items():\n            if \"mtp\" in flags:\n                if ptop not in self.entags:\n                    t = \"skipping mtp for unavailable volume {}\"\n                    self.log(t.format(ptop), 1)\n                else:\n                    self._run_one_mtp(ptop, gid)\n\n            vtop = \"\\n\"\n            for vol in self.vfs.all_vols.values():\n                if vol.realpath == ptop:\n                    vtop = vol.vpath\n            if \"running mtp\" in self.volstate.get(vtop, \"\"):\n                self.volstate[vtop] = \"online, idle\"\n\n        td = time.time() - t0\n        msg = \"mtp finished in {:.2f} sec ({})\"\n        self.log(msg.format(td, s2hms(td, True)))\n\n        self.unpp()\n        if self.args.exit == \"idx\":\n            self.hub.sigterm()\n\n    def _run_one_mtp(self, ptop: str, gid: int) -> None:\n        if gid != self.gid:\n            return\n\n        entags = self.entags[ptop]\n        vf = self.flags[ptop]\n\n        parsers = {}\n        for parser in vf[\"mtp\"]:\n            try:\n                parser = MParser(parser)\n            except:\n                self.log(\"invalid argument (could not find program): \" + parser, 1)\n                return\n\n            for tag in entags:\n                if tag in parser.tags:\n                    parsers[parser.tag] = parser\n\n        if self.args.mtag_vv:\n            t = \"parsers for {}: \\033[0m{}\"\n            self.log(t.format(ptop, list(parsers.keys())), \"90\")\n\n        self.mtp_parsers[ptop] = parsers\n\n        q = \"select count(w) from mt where k = 't:mtp'\"\n        with self.mutex:\n            cur = self.cur[ptop]\n            cur = cur.connection.cursor()\n            wcur = cur.connection.cursor()\n            n_left = cur.execute(q).fetchone()[0]\n\n        mpool = self._start_mpool()\n        batch_sz = mpool.maxsize * 3\n        t_prev = time.time()\n        n_prev = n_left\n        n_done = 0\n        to_delete = {}\n        in_progress = {}\n        while True:\n            did_nothing = True\n            with self.mutex:\n                if gid != self.gid:\n                    break\n\n                q = \"select w from mt where k = 't:mtp' limit ?\"\n                zq = cur.execute(q, (batch_sz,)).fetchall()\n                warks = [str(x[0]) for x in zq]\n                jobs = []\n                for w in warks:\n                    if w in in_progress:\n                        continue\n\n                    q = \"select rd, fn, ip, at, un from up where substr(w,1,16)=? limit 1\"\n                    rd, fn, ip, at, un = cur.execute(q, (w,)).fetchone()\n                    rd, fn = s3dec(rd, fn)\n                    abspath = djoin(ptop, rd, fn)\n\n                    q = \"select k from mt where w = ?\"\n                    zq = cur.execute(q, (w,)).fetchall()\n                    have: dict[str, Union[str, float]] = {x[0]: 1 for x in zq}\n\n                    did_nothing = False\n                    parsers = self._get_parsers(ptop, have, abspath)\n                    if not parsers:\n                        to_delete[w] = True\n                        n_left -= 1\n                        continue\n\n                    if next((x for x in parsers.values() if x.pri), None):\n                        q = \"select k, v from mt where w = ?\"\n                        zq2 = cur.execute(q, (w,)).fetchall()\n                        oth_tags = {str(k): v for k, v in zq2}\n                    else:\n                        oth_tags = {}\n\n                    if ip:\n                        oth_tags[\"up_ip\"] = ip\n                    if at:\n                        oth_tags[\"up_at\"] = at\n                    if un:\n                        oth_tags[\"up_by\"] = un\n\n                    jobs.append(Mpqe(parsers, set(), vf, w, abspath, oth_tags))\n                    in_progress[w] = True\n\n            with self.mutex:\n                done = self._flush_mpool(wcur)\n                for w in done:\n                    to_delete[w] = True\n                    did_nothing = False\n                    in_progress.pop(w)\n                    n_done += 1\n\n                for w in to_delete:\n                    q = \"delete from mt where w = ? and +k = 't:mtp'\"\n                    cur.execute(q, (w,))\n\n                to_delete = {}\n\n            if not warks:\n                break\n\n            if did_nothing:\n                with self.tag_event:\n                    self.tag_event.wait(0.2)\n\n            if not jobs:\n                continue\n\n            try:\n                now = time.time()\n                s = ((now - t_prev) / (n_prev - n_left)) * n_left\n                h, s = divmod(s, 3600)\n                m, s = divmod(s, 60)\n                n_prev = n_left\n                t_prev = now\n            except:\n                h = 1\n                m = 1\n\n            msg = \"mtp: {} done, {} left, eta {}h {:02d}m\"\n            with self.mutex:\n                msg = msg.format(n_done, n_left, int(h), int(m))\n                self.log(msg, c=6)\n\n            for j in jobs:\n                n_left -= 1\n                mpool.put(j)\n\n            with self.mutex:\n                cur.connection.commit()\n\n        self._stop_mpool(mpool)\n        with self.mutex:\n            done = self._flush_mpool(wcur)\n            for w in done:\n                q = \"delete from mt where w = ? and +k = 't:mtp'\"\n                cur.execute(q, (w,))\n\n            cur.connection.commit()\n            if n_done:\n                self.log(\"mtp: scanned {} files in {}\".format(n_done, ptop), c=6)\n                cur.execute(\"vacuum\")\n\n            wcur.close()\n            cur.close()\n\n    def _get_parsers(\n        self, ptop: str, have: dict[str, Union[str, float]], abspath: str\n    ) -> dict[str, MParser]:\n        try:\n            all_parsers = self.mtp_parsers[ptop]\n        except:\n            if self.args.mtag_vv:\n                self.log(\"no mtp defined for {}\".format(ptop), \"90\")\n            return {}\n\n        entags = self.entags[ptop]\n        parsers = {}\n        for k, v in all_parsers.items():\n            if \"ac\" in entags or \".aq\" in entags:\n                if \"ac\" in have or \".aq\" in have:\n                    # is audio, require non-audio?\n                    if v.audio == \"n\":\n                        if self.args.mtag_vv:\n                            t = \"skip mtp {}; want no-audio, got audio\"\n                            self.log(t.format(k), \"90\")\n                        continue\n                # is not audio, require audio?\n                elif v.audio == \"y\":\n                    if self.args.mtag_vv:\n                        t = \"skip mtp {}; want audio, got no-audio\"\n                        self.log(t.format(k), \"90\")\n                    continue\n\n            if v.ext:\n                match = False\n                for ext in v.ext:\n                    if abspath.lower().endswith(\".\" + ext):\n                        match = True\n                        break\n\n                if not match:\n                    if self.args.mtag_vv:\n                        t = \"skip mtp {}; want file-ext {}, got {}\"\n                        self.log(t.format(k, v.ext, abspath.rsplit(\".\")[-1]), \"90\")\n                    continue\n\n            parsers[k] = v\n\n        parsers = {k: v for k, v in parsers.items() if v.force or k not in have}\n        return parsers\n\n    def _start_mpool(self) -> Queue[Mpqe]:\n        # mp.pool.ThreadPool and concurrent.futures.ThreadPoolExecutor\n        # both do crazy runahead so lets reinvent another wheel\n        nw = max(1, self.args.mtag_mt)\n        assert self.mtag  # !rm\n        if not self.mpool_used:\n            self.mpool_used = True\n            self.log(\"using {}x {}\".format(nw, self.mtag.backend))\n\n        mpool: Queue[Mpqe] = Queue(nw)\n        for _ in range(nw):\n            Daemon(self._tag_thr, \"up2k-mpool\", (mpool,))\n\n        return mpool\n\n    def _stop_mpool(self, mpool: Queue[Mpqe]) -> None:\n        if not mpool:\n            return\n\n        for _ in range(mpool.maxsize):\n            mpool.put(Mpqe({}, set(), {}, \"\", \"\", {}))\n\n        mpool.join()\n\n    def _tag_thr(self, q: Queue[Mpqe]) -> None:\n        assert self.mtag\n        while True:\n            qe = q.get()\n            if not qe.w:\n                q.task_done()\n                return\n\n            try:\n                st = bos.stat(qe.abspath)\n                if not qe.mtp:\n                    if self.args.mtag_vv:\n                        t = \"tag-thr: {}({})\"\n                        self.log(t.format(self.mtag.backend, qe.abspath), \"90\")\n\n                    tags = self.mtag.get(qe.abspath, qe.vf) if st.st_size else {}\n                else:\n                    if self.args.mtag_vv:\n                        t = \"tag-thr: {}({})\"\n                        self.log(t.format(list(qe.mtp.keys()), qe.abspath), \"90\")\n\n                    tags = self.mtag.get_bin(qe.mtp, qe.abspath, qe.oth_tags)\n                    vtags = [\n                        \"\\033[36m{} \\033[33m{}\".format(k, v) for k, v in tags.items()\n                    ]\n                    if vtags:\n                        self.log(\"{}\\033[0m [{}]\".format(\" \".join(vtags), qe.abspath))\n\n                with self.mutex:\n                    self.pending_tags.append((qe.entags, qe.w, qe.abspath, tags))\n            except:\n                ex = traceback.format_exc()\n                self._log_tag_err(qe.mtp or self.mtag.backend, qe.abspath, ex)\n            finally:\n                if qe.mtp:\n                    with self.tag_event:\n                        self.tag_event.notify_all()\n\n            q.task_done()\n\n    def _log_tag_err(self, parser: Any, abspath: str, ex: Any) -> None:\n        msg = \"%s failed to read tags from %r:\\n%s\" % (parser, abspath, ex)\n        self.log(msg.lstrip(), c=1 if \"<Signals.SIG\" in msg else 3)\n\n    def _tagscan_file(\n        self,\n        write_cur: \"sqlite3.Cursor\",\n        entags: set[str],\n        vf: dict[str, Any],\n        wark: str,\n        abspath: str,\n        ip: str,\n        at: float,\n        un: Optional[str],\n    ) -> int:\n        \"\"\"will mutex(main)\"\"\"\n        assert self.mtag  # !rm\n\n        try:\n            st = bos.stat(abspath)\n        except:\n            return 0\n\n        if not stat.S_ISREG(st.st_mode):\n            return 0\n\n        try:\n            tags = self.mtag.get(abspath, vf) if st.st_size else {}\n        except Exception as ex:\n            self._log_tag_err(\"\", abspath, ex)\n            return 0\n\n        if ip:\n            tags[\"up_ip\"] = ip\n        if at:\n            tags[\"up_at\"] = at\n        if un:\n            tags[\"up_by\"] = un\n\n        with self.mutex:\n            return self._tag_file(write_cur, entags, wark, abspath, tags)\n\n    def _tag_file(\n        self,\n        write_cur: \"sqlite3.Cursor\",\n        entags: set[str],\n        wark: str,\n        abspath: str,\n        tags: dict[str, Union[str, float]],\n    ) -> int:\n        \"\"\"mutex(main) me\"\"\"\n        assert self.mtag  # !rm\n\n        if not bos.path.isfile(abspath):\n            return 0\n\n        if entags:\n            tags = {k: v for k, v in tags.items() if k in entags}\n            if not tags:\n                # indicate scanned without tags\n                tags = {\"x\": 0}\n\n        if not tags:\n            return 0\n\n        for k in tags.keys():\n            q = \"delete from mt where w = ? and ({})\".format(\n                \" or \".join([\"+k = ?\"] * len(tags))\n            )\n            args = [wark[:16]] + list(tags.keys())\n            write_cur.execute(q, tuple(args))\n\n        ret = 0\n        for k, v in tags.items():\n            q = \"insert into mt values (?,?,?)\"\n            write_cur.execute(q, (wark[:16], k, v))\n            ret += 1\n\n        self._set_tagscan(write_cur, True)\n        return ret\n\n    def _trace(self, msg: str) -> None:\n        self.log(\"ST: {}\".format(msg))\n\n    def _open_db_wd(self, db_path: str) -> \"sqlite3.Cursor\":\n        ok: list[int] = []\n        if not self.hub.is_dut:\n            Daemon(self._open_db_timeout, \"opendb_watchdog\", [db_path, ok])\n\n        try:\n            return self._open_db(db_path)\n        finally:\n            ok.append(1)\n\n    def _open_db_timeout(self, db_path, ok: list[int]) -> None:\n        # give it plenty of time due to the count statement (and wisdom from byte's box)\n        for _ in range(60):\n            time.sleep(1)\n            if ok:\n                return\n\n        t = \"WARNING:\\n\\n  initializing an up2k database is taking longer than one minute; something has probably gone wrong:\\n\\n\"\n        self._log_sqlite_incompat(db_path, t)\n\n    def _log_sqlite_incompat(self, db_path, t0) -> None:\n        txt = t0 or \"\"\n        digest = hashlib.sha512(db_path.encode(\"utf-8\", \"replace\")).digest()\n        stackname = ub64enc(digest[:9]).decode(\"ascii\")\n        stackpath = os.path.join(E.cfg, \"stack-%s.txt\" % (stackname,))\n\n        t = \"  the filesystem at %s may not support locking, or is otherwise incompatible with sqlite\\n\\n  %s\\n\\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\"\n        txt += t % (db_path, HINT_HISTPATH, stackpath)\n        self.log(txt, 3)\n\n        try:\n            stk = alltrace()\n            with open(stackpath, \"wb\") as f:\n                f.write(stk.encode(\"utf-8\", \"replace\"))\n        except Exception as ex:\n            self.log(\"warning: failed to write %s: %s\" % (stackpath, ex), 3)\n\n        if self.args.q:\n            t = \"-\" * 72\n            raise Exception(\"%s\\n%s\\n%s\" % (t, txt, t))\n\n    def _orz(self, db_path: str) -> \"sqlite3.Cursor\":\n        assert sqlite3  # type: ignore  # !rm\n        c = sqlite3.connect(\n            db_path, timeout=self.timeout, check_same_thread=False\n        ).cursor()\n        # c.connection.set_trace_callback(self._trace)\n        return c\n\n    def _open_db(self, db_path: str) -> \"sqlite3.Cursor\":\n        existed = bos.path.exists(db_path)\n        cur = self._orz(db_path)\n        ver = self._read_ver(cur)\n        if not existed and ver is None:\n            return self._try_create_db(db_path, cur)\n\n        for upver in (4, 5):\n            if ver != upver:\n                continue\n            try:\n                t = \"creating backup before upgrade: \"\n                cur = self._backup_db(db_path, cur, ver, t)\n                getattr(self, \"_upgrade_v%d\" % (upver,))(cur)\n                ver += 1  # type: ignore\n            except:\n                self.log(\"WARN: failed to upgrade from v%d\" % (ver,), 3)\n\n        if ver == DB_VER:\n            # these no longer serve their intended purpose but they're great as additional sanchks\n            self._add_dhash_tab(cur)\n            self._add_xiu_tab(cur)\n            self._add_cv_tab(cur)\n            self._add_idx_up_vp(cur, db_path)\n            self._add_ds_tab(cur)\n\n            try:\n                nfiles = next(cur.execute(\"select count(w) from up\"))[0]\n                self.log(\"  {} |{}|\".format(db_path, nfiles), \"90\")\n                return cur\n            except:\n                self.log(\"WARN: could not list files; DB corrupt?\\n\" + min_ex())\n\n        if (ver or 0) > DB_VER:\n            t = \"database is version {}, this copyparty only supports versions <= {}\"\n            raise Exception(t.format(ver, DB_VER))\n\n        msg = \"creating new DB (old is bad); backup: \"\n        if ver:\n            msg = \"creating new DB (too old to upgrade); backup: \"\n\n        cur = self._backup_db(db_path, cur, ver, msg)\n        db = cur.connection\n        cur.close()\n        db.close()\n        self._delete_db(db_path)\n        return self._try_create_db(db_path, None)\n\n    def _delete_db(self, db_path: str):\n        for suf in (\"\", \"-shm\", \"-wal\", \"-journal\"):\n            try:\n                bos.unlink(db_path + suf)\n            except:\n                if not suf:\n                    raise\n\n    def _backup_db(\n        self, db_path: str, cur: \"sqlite3.Cursor\", ver: Optional[int], msg: str\n    ) -> \"sqlite3.Cursor\":\n        assert sqlite3  # type: ignore  # !rm\n        bak = \"{}.bak.{:x}.v{}\".format(db_path, int(time.time()), ver)\n        self.log(msg + bak)\n        try:\n            c2 = sqlite3.connect(bak)\n            with c2:\n                cur.connection.backup(c2)\n            return cur\n        except:\n            t = \"native sqlite3 backup failed; using fallback method:\\n\"\n            self.log(t + min_ex())\n        finally:\n            c2.close()  # type: ignore\n\n        db = cur.connection\n        cur.close()\n        db.close()\n\n        trystat_shutil_copy2(self.log, fsenc(db_path), fsenc(bak))\n        return self._orz(db_path)\n\n    def _read_ver(self, cur: \"sqlite3.Cursor\") -> Optional[int]:\n        for tab in [\"ki\", \"kv\"]:\n            try:\n                c = cur.execute(r\"select v from {} where k = 'sver'\".format(tab))\n            except:\n                continue\n\n            rows = c.fetchall()\n            if rows:\n                return int(rows[0][0])\n        return None\n\n    def _try_create_db(\n        self, db_path: str, cur: Optional[\"sqlite3.Cursor\"]\n    ) -> \"sqlite3.Cursor\":\n        try:\n            return self._create_db(db_path, cur)\n        except:\n            try:\n                self._delete_db(db_path)\n            except:\n                pass\n            raise\n\n    def _create_db(\n        self, db_path: str, cur: Optional[\"sqlite3.Cursor\"]\n    ) -> \"sqlite3.Cursor\":\n        \"\"\"\n        collision in 2^(n/2) files where n = bits (6 bits/ch)\n          10*6/2 = 2^30 =       1'073'741'824, 24.1mb idx  1<<(3*10)\n          12*6/2 = 2^36 =      68'719'476'736, 24.8mb idx\n          16*6/2 = 2^48 = 281'474'976'710'656, 26.1mb idx\n        \"\"\"\n        if not cur:\n            cur = self._orz(db_path)\n\n        idx = r\"create index up_w on up(substr(w,1,16))\"\n        if self.no_expr_idx:\n            idx = r\"create index up_w on up(w)\"\n\n        for cmd in [\n            r\"create table up (w text, mt int, sz int, rd text, fn text, ip text, at int, un text)\",\n            r\"create index up_vp on up(rd, fn)\",\n            r\"create index up_fn on up(fn)\",\n            r\"create index up_ip on up(ip)\",\n            r\"create index up_at on up(at)\",\n            idx,\n            r\"create table mt (w text, k text, v int)\",\n            r\"create index mt_w on mt(w)\",\n            r\"create index mt_k on mt(k)\",\n            r\"create index mt_v on mt(v)\",\n            r\"create table kv (k text, v int)\",\n            r\"insert into kv values ('sver', {})\".format(DB_VER),\n        ]:\n            cur.execute(cmd)\n\n        self._add_dhash_tab(cur)\n        self._add_xiu_tab(cur)\n        self._add_cv_tab(cur)\n        self._add_ds_tab(cur)\n        self.log(\"created DB at {}\".format(db_path))\n        return cur\n\n    def _upgrade_v4(self, cur: \"sqlite3.Cursor\") -> None:\n        for cmd in [\n            r\"alter table up add column ip text\",\n            r\"alter table up add column at int\",\n            r\"create index up_ip on up(ip)\",\n            r\"update kv set v=5 where k='sver'\",\n        ]:\n            cur.execute(cmd)\n\n        cur.connection.commit()\n\n    def _upgrade_v5(self, cur: \"sqlite3.Cursor\") -> None:\n        for cmd in [\n            r\"alter table up add column un text\",\n            r\"update kv set v=6 where k='sver'\",\n        ]:\n            cur.execute(cmd)\n\n        cur.connection.commit()\n\n    def _add_dhash_tab(self, cur: \"sqlite3.Cursor\") -> None:\n        # v5 -> v5a\n        try:\n            cur.execute(\"select d, h from dh limit 1\").fetchone()\n            return\n        except:\n            pass\n\n        for cmd in [\n            r\"create table dh (d text, h text)\",\n            r\"create index dh_d on dh(d)\",\n            r\"insert into kv values ('tagscan',1)\",\n        ]:\n            cur.execute(cmd)\n\n        cur.connection.commit()\n\n    def _add_xiu_tab(self, cur: \"sqlite3.Cursor\") -> None:\n        # v5a -> v5b\n        # store rd+fn rather than warks to support nohash vols\n        try:\n            cur.execute(\"select c, w, rd, fn from iu limit 1\").fetchone()\n            return\n        except:\n            pass\n\n        try:\n            cur.execute(\"drop table iu\")\n        except:\n            pass\n\n        for cmd in [\n            r\"create table iu (c int, w text, rd text, fn text)\",\n            r\"create index iu_c on iu(c)\",\n            r\"create index iu_w on iu(w)\",\n        ]:\n            cur.execute(cmd)\n\n        cur.connection.commit()\n\n    def _add_cv_tab(self, cur: \"sqlite3.Cursor\") -> None:\n        # v5b -> v5c\n        try:\n            cur.execute(\"select rd, dn, fn from cv limit 1\").fetchone()\n            return\n        except:\n            pass\n\n        for cmd in [\n            r\"create table cv (rd text, dn text, fn text)\",\n            r\"create index cv_i on cv(rd, dn)\",\n        ]:\n            cur.execute(cmd)\n\n        try:\n            cur.execute(\"delete from dh\")\n        except:\n            pass\n\n        cur.connection.commit()\n\n    def _add_idx_up_vp(self, cur: \"sqlite3.Cursor\", db_path: str) -> None:\n        # v5c -> v5d\n        try:\n            cur.execute(\"drop index up_rd\")\n        except:\n            return\n\n        for cmd in [\n            r\"create index up_vp on up(rd, fn)\",\n            r\"create index up_at on up(at)\",\n        ]:\n            self.log(\"upgrading db [%s]: %s\" % (db_path, cmd[:18]))\n            cur.execute(cmd)\n\n        self.log(\"upgrading db [%s]: writing to disk...\" % (db_path,))\n        cur.connection.commit()\n        cur.execute(\"vacuum\")\n\n    def _add_ds_tab(self, cur: \"sqlite3.Cursor\") -> None:\n        # v5d -> v5e\n        try:\n            cur.execute(\"select rd, sz from ds limit 1\").fetchone()\n            return\n        except:\n            pass\n\n        for cmd in [\n            r\"create table ds (rd text, sz int, nf int)\",\n            r\"create index ds_rd on ds(rd)\",\n        ]:\n            cur.execute(cmd)\n\n        cur.connection.commit()\n\n    def wake_rescanner(self):\n        with self.rescan_cond:\n            self.rescan_cond.notify_all()\n\n    def handle_json(\n        self, cj: dict[str, Any], busy_aps: dict[str, int]\n    ) -> dict[str, Any]:\n        # busy_aps is u2fh (always undefined if -j0) so this is safe\n        self.busy_aps = busy_aps\n        if self.reload_flag or self.reloading:\n            raise Pebkac(503, SBUSY % (\"fs-reload\",))\n\n        got_lock = False\n        self.fika = \"u\"\n        try:\n            # bit expensive; 3.9=10x 3.11=2x\n            if self.mutex.acquire(timeout=10):\n                got_lock = True\n                with self.reg_mutex:\n                    ret = self._handle_json(cj)\n            else:\n                raise Pebkac(503, SBUSY % (self.blocked or \"[unknown]\",))\n        except TypeError:\n            if not PY2:\n                raise\n            with self.mutex, self.reg_mutex:\n                ret = self._handle_json(cj)\n        finally:\n            if got_lock:\n                self.mutex.release()\n\n        if self.fx_backlog:\n            self.do_fx_backlog()\n\n        return ret\n\n    def _handle_json(self, cj: dict[str, Any], depth: int = 1) -> dict[str, Any]:\n        if depth > 16:\n            raise Pebkac(500, \"too many xbu relocs, giving up\")\n\n        ptop = cj[\"ptop\"]\n        if not self.register_vpath(ptop, cj[\"vcfg\"]):\n            if ptop not in self.registry:\n                raise Pebkac(410, \"location unavailable\")\n\n        cj[\"poke\"] = now = self.db_act = self.vol_act[ptop] = time.time()\n        wark = dwark = self._get_wark(cj)\n        job = None\n        pdir = djoin(ptop, cj[\"prel\"])\n        inc_ap = djoin(pdir, cj[\"name\"])\n        try:\n            dev = bos.stat(pdir).st_dev\n        except:\n            dev = 0\n\n        # check if filesystem supports sparse files;\n        # refuse out-of-order / multithreaded uploading if sprs False\n        sprs = self.fstab.get(pdir)[0] != \"ng\"\n\n        if True:\n            jcur = self.cur.get(ptop)\n            reg = self.registry[ptop]\n            vfs = self.vfs.all_vols[cj[\"vtop\"]]\n            n4g = bool(vfs.flags.get(\"noforget\"))\n            noclone = bool(vfs.flags.get(\"noclone\"))\n            rand = vfs.flags.get(\"rand\") or cj.get(\"rand\")\n            lost: list[tuple[\"sqlite3.Cursor\", str, str]] = []\n\n            safe_dedup = vfs.flags.get(\"safededup\") or 50\n            data_ok = safe_dedup < 10 or n4g\n\n            vols = [(ptop, jcur)] if jcur else []\n            if vfs.flags.get(\"xlink\"):\n                vols += [(k, v) for k, v in self.cur.items() if k != ptop]\n\n            if noclone:\n                wark = up2k_wark_from_metadata(\n                    self.salt, cj[\"size\"], cj[\"lmod\"], cj[\"prel\"], cj[\"name\"]\n                )\n\n            zi = cj[\"lmod\"]\n            bad_mt = zi <= 0 or zi > 0xAAAAAAAA\n            if bad_mt or vfs.flags.get(\"up_ts\", \"\") == \"fu\":\n                # force upload time rather than last-modified\n                cj[\"lmod\"] = int(time.time())\n                if zi and bad_mt:\n                    t = \"ignoring impossible last-modified time from client: %s\"\n                    self.log(t % (zi,), 6)\n\n            alts: list[tuple[int, int, dict[str, Any], \"sqlite3.Cursor\", str, str]] = []\n            for ptop, cur in vols:\n                allv = self.vfs.all_vols\n                cvfs = next((v for v in allv.values() if v.realpath == ptop), vfs)\n                vtop = cj[\"vtop\"] if cur == jcur else cvfs.vpath\n\n                if self.no_expr_idx:\n                    q = r\"select * from up where w = ?\"\n                    argv = [dwark]\n                else:\n                    q = r\"select * from up where substr(w,1,16)=? and +w=?\"\n                    argv = [dwark[:16], dwark]\n\n                c2 = cur.execute(q, tuple(argv))\n                for _, dtime, dsize, dp_dir, dp_fn, ip, at, _ in c2:\n                    if dp_dir.startswith(\"//\") or dp_fn.startswith(\"//\"):\n                        dp_dir, dp_fn = s3dec(dp_dir, dp_fn)\n\n                    dp_abs = djoin(ptop, dp_dir, dp_fn)\n                    if noclone and dp_abs != inc_ap:\n                        continue\n\n                    try:\n                        st = bos.stat(dp_abs)\n                        if stat.S_ISLNK(st.st_mode):\n                            # broken symlink\n                            raise Exception()\n                        if st.st_size != dsize:\n                            t = \"candidate ignored (db/fs desync): {}, size fs={} db={}, mtime fs={} db={}, file: {}\"\n                            t = t.format(\n                                dwark, st.st_size, dsize, st.st_mtime, dtime, dp_abs\n                            )\n                            self.log(t)\n                            raise Exception()\n                    except Exception as ex:\n                        if n4g:\n                            st = NULLSTAT\n                        else:\n                            lost.append((cur, dp_dir, dp_fn))\n                            continue\n\n                    j = {\n                        \"name\": dp_fn,\n                        \"prel\": dp_dir,\n                        \"vtop\": vtop,\n                        \"ptop\": ptop,\n                        \"sprs\": sprs,  # dontcare; finished anyways\n                        \"size\": dsize,\n                        \"lmod\": dtime,\n                        \"host\": cj[\"host\"],\n                        \"user\": cj[\"user\"],\n                        \"addr\": ip,\n                        \"at\": at,\n                    }\n                    for k in [\"life\"]:\n                        if k in cj:\n                            j[k] = cj[k]\n\n                    # offset of 1st diff in vpaths\n                    zig = (\n                        n + 1\n                        for n, (c1, c2) in enumerate(\n                            zip(dp_dir + \"\\r\", cj[\"prel\"] + \"\\n\")\n                        )\n                        if c1 != c2\n                    )\n                    score = (\n                        (6969 if st.st_dev == dev else 0)\n                        + (3210 if dp_dir == cj[\"prel\"] else next(zig))\n                        + (1 if dp_fn == cj[\"name\"] else 0)\n                    )\n                    alts.append((score, -len(alts), j, cur, dp_dir, dp_fn))\n\n            job = None\n            for dupe in sorted(alts, reverse=True):\n                rj = dupe[2]\n                orig_ap = djoin(rj[\"ptop\"], rj[\"prel\"], rj[\"name\"])\n                if data_ok or inc_ap == orig_ap:\n                    data_ok = True\n                    job = rj\n                    break\n                else:\n                    self.log(\"asserting contents of %r\" % (orig_ap,))\n                    hashes2, st = self._hashlist_from_file(orig_ap)\n                    wark2 = up2k_wark_from_hashlist(self.salt, st.st_size, hashes2)\n                    if dwark != wark2:\n                        t = \"will not dedup (fs index desync): fs=%s, db=%s, file: %r\\n%s\"\n                        self.log(t % (wark2, dwark, orig_ap, rj))\n                        lost.append(dupe[3:])\n                        continue\n                    data_ok = True\n                    job = rj\n                    break\n\n            if job:\n                if wark in reg:\n                    del reg[wark]\n                job[\"hash\"] = job[\"need\"] = []\n                job[\"done\"] = 1\n                job[\"busy\"] = {}\n\n            if lost:\n                c2 = None\n                for cur, dp_dir, dp_fn in lost:\n                    t = \"forgetting desynced db entry: %r\"\n                    self.log(t % (\"/\" + vjoin(vjoin(vfs.vpath, dp_dir), dp_fn)))\n                    self.db_rm(cur, vfs.flags, dp_dir, dp_fn, cj[\"size\"])\n                    if c2 and c2 != cur:\n                        c2.connection.commit()\n\n                    c2 = cur\n\n                assert c2  # !rm\n                c2.connection.commit()\n\n            cur = jcur\n            ptop = None  # use cj or job as appropriate\n\n            if not job and wark in reg:\n                # ensure the files haven't been edited or deleted\n                path = \"\"\n                st = None\n                rj = reg[wark]\n                names = [rj[x] for x in [\"name\", \"tnam\"] if x in rj]\n                for fn in names:\n                    path = djoin(rj[\"ptop\"], rj[\"prel\"], fn)\n                    try:\n                        st = bos.stat(path)\n                        if st.st_size > 0 or \"done\" in rj:\n                            # upload completed or both present\n                            break\n                    except:\n                        # missing; restart\n                        if not self.args.nw and not n4g:\n                            t = \"forgetting deleted partial upload at %r\"\n                            self.log(t % (path,))\n                            del reg[wark]\n                        break\n\n                inc_ap = djoin(cj[\"ptop\"], cj[\"prel\"], cj[\"name\"])\n                orig_ap = djoin(rj[\"ptop\"], rj[\"prel\"], rj[\"name\"])\n\n                if self.args.nw or n4g or not st or \"done\" not in rj:\n                    pass\n\n                elif st.st_size != rj[\"size\"]:\n                    t = \"will not dedup (fs index desync): %s, size fs=%d db=%d, mtime fs=%d db=%d, file: %r\\n%s\"\n                    t = t % (\n                        wark,\n                        st.st_size,\n                        rj[\"size\"],\n                        st.st_mtime,\n                        rj[\"lmod\"],\n                        path,\n                        rj,\n                    )\n                    self.log(t)\n                    del reg[wark]\n\n                elif inc_ap != orig_ap and not data_ok and \"done\" in reg[wark]:\n                    self.log(\"asserting contents of %r\" % (orig_ap,))\n                    hashes2, _ = self._hashlist_from_file(orig_ap)\n                    wark2 = up2k_wark_from_hashlist(self.salt, st.st_size, hashes2)\n                    if wark != wark2:\n                        t = \"will not dedup (fs index desync): fs=%s, idx=%s, file: %r\\n%s\"\n                        self.log(t % (wark2, wark, orig_ap, rj))\n                        del reg[wark]\n\n            if job or wark in reg:\n                job = job or reg[wark]\n                if (\n                    job[\"ptop\"] != cj[\"ptop\"]\n                    or job[\"prel\"] != cj[\"prel\"]\n                    or job[\"name\"] != cj[\"name\"]\n                ):\n                    # file contents match, but not the path\n                    src = djoin(job[\"ptop\"], job[\"prel\"], job[\"name\"])\n                    dst = djoin(cj[\"ptop\"], cj[\"prel\"], cj[\"name\"])\n                    vsrc = djoin(job[\"vtop\"], job[\"prel\"], job[\"name\"])\n                    vsrc = vsrc.replace(\"\\\\\", \"/\")  # just for prints anyways\n                    if vfs.lim:\n                        dst, cj[\"prel\"] = vfs.lim.all(\n                            cj[\"addr\"],\n                            cj[\"prel\"],\n                            cj[\"size\"],\n                            cj[\"ptop\"],\n                            djoin(cj[\"ptop\"], cj[\"prel\"]),\n                            self.hub.broker,\n                            reg,\n                            \"up2k._get_volsize\",\n                        )\n                        bos.makedirs(dst, vf=vfs.flags)\n                        vfs.lim.nup(cj[\"addr\"])\n                        vfs.lim.bup(cj[\"addr\"], cj[\"size\"])\n\n                    if \"done\" not in job:\n                        self.log(\"unfinished:\\n  %r\\n  %r\" % (src, dst))\n                        err = \"partial upload exists at a different location; please resume uploading here instead:\\n\"\n                        err += \"/\" + quotep(vsrc) + \" \"\n\n                        # registry is size-constrained + can only contain one unique wark;\n                        # let want_recheck trigger symlink (if still in reg) or reupload\n                        if cur:\n                            dupe = (cj[\"prel\"], cj[\"name\"], cj[\"lmod\"])\n                            try:\n                                if dupe not in self.dupesched[src]:\n                                    self.dupesched[src].append(dupe)\n                            except:\n                                self.dupesched[src] = [dupe]\n\n                        raise Pebkac(422, err)\n\n                    elif \"nodupe\" in vfs.flags:\n                        self.log(\"dupe-reject:\\n  %r\\n  %r\" % (src, dst))\n                        err = \"upload rejected, file already exists:\\n\"\n                        err += \"/\" + quotep(vsrc) + \" \"\n                        raise Pebkac(409, err)\n                    else:\n                        # symlink to the client-provided name,\n                        # returning the previous upload info\n                        psrc = src + \".PARTIAL\"\n                        if self.args.dotpart:\n                            m = re.match(r\"(.*[\\\\/])(.*)\", psrc)\n                            if m:  # always true but...\n                                zs1, zs2 = m.groups()\n                                psrc = zs1 + \".\" + zs2\n\n                        if (\n                            src in self.busy_aps\n                            or psrc in self.busy_aps\n                            or (wark in reg and \"done\" not in reg[wark])\n                        ):\n                            raise Pebkac(\n                                422, \"source file busy; please try again later\"\n                            )\n\n                        job = deepcopy(job)\n                        job[\"wark\"] = wark\n                        job[\"dwrk\"] = dwark\n                        job[\"at\"] = cj.get(\"at\") or now\n                        zs = \"vtop ptop prel name lmod host user addr poke\"\n                        for k in zs.split():\n                            job[k] = cj.get(k) or \"\"\n                        for k in (\"life\", \"replace\"):\n                            if k in cj:\n                                job[k] = cj[k]\n\n                        pdir = djoin(cj[\"ptop\"], cj[\"prel\"])\n                        if rand:\n                            job[\"name\"] = rand_name(\n                                pdir, cj[\"name\"], vfs.flags[\"nrand\"]\n                            )\n\n                        dst = djoin(job[\"ptop\"], job[\"prel\"], job[\"name\"])\n                        xbu = vfs.flags.get(\"xbu\")\n                        if xbu:\n                            vp = djoin(job[\"vtop\"], job[\"prel\"], job[\"name\"])\n                            hr = runhook(\n                                self.log,\n                                None,\n                                self,\n                                \"xbu.up2k.dupe\",\n                                xbu,  # type: ignore\n                                dst,\n                                vp,\n                                job[\"host\"],\n                                job[\"user\"],\n                                self.vfs.get_perms(job[\"vtop\"], job[\"user\"]),\n                                job[\"lmod\"],\n                                job[\"size\"],\n                                job[\"addr\"],\n                                job[\"at\"],\n                                None,\n                            )\n                            t = hr.get(\"rejectmsg\") or \"\"\n                            if t or hr.get(\"rc\") != 0:\n                                if not t:\n                                    t = \"upload blocked by xbu server config: %r\"\n                                    t = t % (vp,)\n                                self.log(t, 1)\n                                raise Pebkac(403, t)\n                            if hr.get(\"reloc\"):\n                                x = pathmod(self.vfs, dst, vp, hr[\"reloc\"])\n                                if x:\n                                    ud1 = (vfs.vpath, job[\"prel\"], job[\"name\"])\n                                    pdir, _, job[\"name\"], (vfs, rem) = x\n                                    dst = os.path.join(pdir, job[\"name\"])\n                                    job[\"vcfg\"] = vfs.flags\n                                    job[\"ptop\"] = vfs.realpath\n                                    job[\"vtop\"] = vfs.vpath\n                                    job[\"prel\"] = rem\n                                    job[\"name\"] = sanitize_fn(job[\"name\"])\n                                    ud2 = (vfs.vpath, job[\"prel\"], job[\"name\"])\n                                    if ud1 != ud2:\n                                        # print(json.dumps(job, sort_keys=True, indent=4))\n                                        job[\"hash\"] = cj[\"hash\"]\n                                        self.log(\"xbu reloc1:%d...\" % (depth,), 6)\n                                        return self._handle_json(job, depth + 1)\n\n                        job[\"name\"] = self._untaken(pdir, job, now)\n                        dst = djoin(job[\"ptop\"], job[\"prel\"], job[\"name\"])\n\n                        if not self.args.nw:\n                            dvf: dict[str, Any] = vfs.flags\n                            try:\n                                dvf = self.flags[job[\"ptop\"]]\n                                self._symlink(src, dst, dvf, lmod=cj[\"lmod\"], rm=True)\n                            except:\n                                if bos.path.exists(dst):\n                                    wunlink(self.log, dst, dvf)\n                                if not n4g:\n                                    raise\n\n                        if cur and not self.args.nw:\n                            zs = \"prel name lmod size ptop vtop wark dwrk host user addr at\"\n                            a = [job[x] for x in zs.split()]\n                            self.db_add(cur, vfs.flags, *a)\n                            cur.connection.commit()\n                elif wark in reg:\n                    # checks out, but client may have hopped IPs\n                    job[\"addr\"] = cj[\"addr\"]\n\n            if not job:\n                ap1 = djoin(cj[\"ptop\"], cj[\"prel\"])\n                if rand:\n                    cj[\"name\"] = rand_name(ap1, cj[\"name\"], vfs.flags[\"nrand\"])\n\n                if vfs.lim:\n                    ap2, cj[\"prel\"] = vfs.lim.all(\n                        cj[\"addr\"],\n                        cj[\"prel\"],\n                        cj[\"size\"],\n                        cj[\"ptop\"],\n                        ap1,\n                        self.hub.broker,\n                        reg,\n                        \"up2k._get_volsize\",\n                    )\n                    bos.makedirs(ap2, vf=vfs.flags)\n                    vfs.lim.nup(cj[\"addr\"])\n                    vfs.lim.bup(cj[\"addr\"], cj[\"size\"])\n\n                job = {\n                    \"wark\": wark,\n                    \"dwrk\": dwark,\n                    \"t0\": now,\n                    \"sprs\": sprs,\n                    \"hash\": deepcopy(cj[\"hash\"]),\n                    \"need\": [],\n                    \"busy\": {},\n                }\n                # client-provided, sanitized by _get_wark: name, size, lmod\n                zs = \"vtop ptop prel name size lmod host user addr poke\"\n                for k in zs.split():\n                    job[k] = cj[k]\n\n                for k in [\"life\", \"replace\"]:\n                    if k in cj:\n                        job[k] = cj[k]\n\n                # one chunk may occur multiple times in a file;\n                # filter to unique values for the list of missing chunks\n                # (preserve order to reduce disk thrashing)\n                lut = set()\n                for k in cj[\"hash\"]:\n                    if k not in lut:\n                        job[\"need\"].append(k)\n                        lut.add(k)\n\n                try:\n                    ret = self._new_upload(job, vfs, depth)\n                    if ret:\n                        return ret  # xbu recursed\n                except:\n                    self.registry[job[\"ptop\"]].pop(job[\"wark\"], None)\n                    raise\n\n            purl = \"{}/{}\".format(job[\"vtop\"], job[\"prel\"]).strip(\"/\")\n            purl = \"/{}/\".format(purl) if purl else \"/\"\n\n            ret = {\n                \"name\": job[\"name\"],\n                \"purl\": purl,\n                \"size\": job[\"size\"],\n                \"lmod\": job[\"lmod\"],\n                \"sprs\": job.get(\"sprs\", sprs),\n                \"hash\": job[\"need\"],\n                \"dwrk\": dwark,\n                \"wark\": wark,\n            }\n\n            if (\n                not ret[\"hash\"]\n                and \"fk\" in vfs.flags\n                and not self.args.nw\n                and (cj[\"user\"] in vfs.axs.uread or cj[\"user\"] in vfs.axs.upget)\n            ):\n                alg = 2 if \"fka\" in vfs.flags else 1\n                ap = absreal(djoin(job[\"ptop\"], job[\"prel\"], job[\"name\"]))\n                ino = 0 if ANYWIN else bos.stat(ap).st_ino\n                fk = self.gen_fk(alg, self.args.fk_salt, ap, job[\"size\"], ino)\n                ret[\"fk\"] = fk[: vfs.flags[\"fk\"]]\n\n            if (\n                not ret[\"hash\"]\n                and cur\n                and cj.get(\"umod\")\n                and int(cj[\"lmod\"]) != int(job[\"lmod\"])\n                and not self.args.nw\n                and cj[\"user\"] in vfs.axs.uwrite\n                and cj[\"user\"] in vfs.axs.udel\n            ):\n                sql = \"update up set mt=? where substr(w,1,16)=? and +rd=? and +fn=?\"\n                try:\n                    cur.execute(sql, (cj[\"lmod\"], dwark[:16], job[\"prel\"], job[\"name\"]))\n                    cur.connection.commit()\n\n                    ap = djoin(job[\"ptop\"], job[\"prel\"], job[\"name\"])\n                    mt = bos.utime_c(self.log, ap, int(cj[\"lmod\"]), False, True)\n\n                    self.log(\"touched %r from %d to %d\" % (ap, job[\"lmod\"], mt))\n                except Exception as ex:\n                    self.log(\"umod failed, %r\" % (ex,), 3)\n\n            return ret\n\n    def _untaken(self, fdir: str, job: dict[str, Any], ts: float) -> str:\n        fname = job[\"name\"]\n        ip = job[\"addr\"]\n\n        if self.args.nw:\n            return fname\n\n        fp = djoin(fdir, fname)\n\n        ow = job.get(\"replace\") and bos.path.exists(fp)\n        if ow:\n            replace_arg = str(job[\"replace\"]).lower()\n\n        if ow and \"skip\" in replace_arg:  # type: ignore\n            self.log(\"skipping upload, filename already exists: %r\" % fp)\n            err = \"upload rejected, a file with that name already exists\"\n            raise Pebkac(409, err)\n\n        if ow and \"mt\" in replace_arg:  # type: ignore\n            mts = bos.stat(fp).st_mtime\n            mtc = job[\"lmod\"]\n            if mtc < mts:\n                t = \"will not overwrite; server %d sec newer than client; %d > %d %r\"\n                self.log(t % (mts - mtc, mts, mtc, fp))\n                ow = False\n\n        ptop = job[\"ptop\"]\n        vf = self.flags.get(ptop) or {}\n        if ow:\n            self.log(\"replacing existing file at %r\" % (fp,))\n            cur = None\n            st = bos.stat(fp)\n            try:\n                vrel = vjoin(job[\"prel\"], fname)\n                xlink = bool(vf.get(\"xlink\"))\n                cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, vrel)\n                self._forget_file(ptop, vrel, vf, cur, wark, True, st.st_size, xlink)\n            except Exception as ex:\n                self.log(\"skipping replace-relink: %r\" % (ex,))\n            finally:\n                if cur:\n                    cur.connection.commit()\n\n            wunlink(self.log, fp, vf)\n\n        if self.args.plain_ip:\n            dip = ip.replace(\":\", \".\")\n        else:\n            dip = self.hub.iphash.s(ip)\n\n        f, ret = ren_open(\n            fname,\n            \"wb\",\n            fdir=fdir,\n            suffix=\"-%.6f-%s\" % (ts, dip),\n            vf=vf,\n        )\n        f.close()\n        return ret\n\n    def _symlink(\n        self,\n        src: str,\n        dst: str,\n        flags: dict[str, Any],\n        verbose: bool = True,\n        rm: bool = False,\n        lmod: float = 0,\n        fsrc: Optional[str] = None,\n        is_mv: bool = False,\n    ) -> None:\n        if src == dst or (fsrc and fsrc == dst):\n            t = \"symlinking a file to itself?? orig(%s) fsrc(%s) link(%s)\"\n            raise Exception(t % (src, fsrc, dst))\n\n        if verbose:\n            t = \"linking dupe:\\n  point-to: {0!r}\\n  link-loc: {1!r}\"\n            if fsrc:\n                t += \"\\n  data-src: {2!r}\"\n            self.log(t.format(src, dst, fsrc))\n\n        if self.args.nw:\n            return\n\n        linked = 0\n        try:\n            if rm and bos.path.exists(dst):\n                wunlink(self.log, dst, flags)\n\n            if not is_mv and not flags.get(\"dedup\"):\n                raise Exception(\"dedup is disabled in config\")\n\n            if \"reflink\" in flags:\n                if not USE_FICLONE:\n                    raise Exception(\"reflink\")  # python 3.14 or newer; no need\n                try:\n                    with open(fsenc(src), \"rb\") as fi, open(fsenc(dst), \"wb\") as fo:\n                        fcntl.ioctl(fo.fileno(), fcntl.FICLONE, fi.fileno())\n                        if \"fperms\" in flags:\n                            set_fperms(fo, flags)\n                except:\n                    if bos.path.exists(dst):\n                        wunlink(self.log, dst, flags)\n                    raise\n                if lmod:\n                    bos.utime_c(self.log, dst, int(lmod), False)\n                return\n\n            lsrc = src\n            ldst = dst\n            fs1 = bos.stat(os.path.dirname(src)).st_dev\n            fs2 = bos.stat(os.path.dirname(dst)).st_dev\n            if fs1 == 0 or fs2 == 0:\n                # py2 on winxp or other unsupported combination\n                raise OSError(errno.ENOSYS, \"filesystem does not have st_dev\")\n            elif fs1 == fs2:\n                # same fs; make symlink as relative as possible\n                spl = r\"[\\\\/]\" if WINDOWS else \"/\"\n                nsrc = re.split(spl, src)\n                ndst = re.split(spl, dst)\n                nc = 0\n                for a, b in zip(nsrc, ndst):\n                    if a != b:\n                        break\n                    nc += 1\n                if nc > 1:\n                    zsl = nsrc[nc:]\n                    hops = len(ndst[nc:]) - 1\n                    lsrc = \"../\" * hops + \"/\".join(zsl)\n\n            if WINDOWS:\n                lsrc = lsrc.replace(\"/\", \"\\\\\")\n                ldst = ldst.replace(\"/\", \"\\\\\")\n\n            try:\n                if \"hardlink\" in flags:\n                    os.link(fsenc(absreal(src)), fsenc(dst))\n                    linked = 2\n            except Exception as ex:\n                self.log(\"cannot hardlink: \" + repr(ex))\n                if \"hardlinkonly\" in flags:\n                    raise Exception(\"symlink-fallback disabled in cfg\")\n\n            if not linked:\n                if ANYWIN:\n                    Path(ldst).symlink_to(lsrc)\n                    if not bos.path.exists(dst):\n                        try:\n                            wunlink(self.log, dst, flags)\n                        except:\n                            pass\n                        t = \"the created symlink [%s] did not resolve to [%s]\"\n                        raise Exception(t % (ldst, lsrc))\n                else:\n                    os.symlink(fsenc(lsrc), fsenc(ldst))\n\n                linked = 1\n        except Exception as ex:\n            if str(ex) != \"reflink\":\n                self.log(\"cannot link; creating copy: \" + repr(ex))\n            if bos.path.isfile(src):\n                csrc = src\n            elif fsrc and bos.path.isfile(fsrc):\n                csrc = fsrc\n            else:\n                t = \"BUG: no valid sources to link from! orig(%r) fsrc(%r) link(%r)\"\n                self.log(t, 1)\n                raise Exception(t % (src, fsrc, dst))\n            trystat_shutil_copy2(self.log, fsenc(csrc), fsenc(dst))\n\n        if linked < 2 and (linked < 1 or SYMTIME):\n            if lmod:\n                bos.utime_c(self.log, dst, int(lmod), False)\n            if \"fperms\" in flags:\n                set_ap_perms(dst, flags)\n\n    def handle_chunks(\n        self, ptop: str, wark: str, chashes: list[str]\n    ) -> tuple[list[str], int, list[list[int]], str, float, int, bool]:\n        self.fika = \"u\"\n        with self.mutex, self.reg_mutex:\n            self.db_act = self.vol_act[ptop] = time.time()\n            job = self.registry[ptop].get(wark)\n            if not job:\n                known = \" \".join([x for x in self.registry[ptop].keys()])\n                self.log(\"unknown wark [{}], known: {}\".format(wark, known))\n                raise Pebkac(400, \"unknown wark\" + SEESLOG)\n\n            if \"t0c\" not in job:\n                job[\"t0c\"] = time.time()\n\n            if len(chashes) > 1 and len(chashes[1]) < 44:\n                # first hash is full-length; expand remaining ones\n                uniq = []\n                lut = set()\n                for chash in job[\"hash\"]:\n                    if chash not in lut:\n                        uniq.append(chash)\n                        lut.add(chash)\n                try:\n                    nchunk = uniq.index(chashes[0])\n                except:\n                    raise Pebkac(400, \"unknown chunk0 [%s]\" % (chashes[0],))\n                expanded = [chashes[0]]\n                for prefix in chashes[1:]:\n                    nchunk += 1\n                    chash = uniq[nchunk]\n                    if not chash.startswith(prefix):\n                        t = \"next sibling chunk does not start with expected prefix [%s]: [%s]\"\n                        raise Pebkac(400, t % (prefix, chash))\n                    expanded.append(chash)\n                chashes = expanded\n\n            for chash in chashes:\n                if chash not in job[\"need\"]:\n                    msg = \"chash = {} , need:\\n\".format(chash)\n                    msg += \"\\n\".join(job[\"need\"])\n                    self.log(msg)\n                    t = \"already got that (%s) but thanks??\"\n                    if chash not in job[\"hash\"]:\n                        t = \"unknown chunk wtf: %s\"\n                    raise Pebkac(400, t % (chash,))\n\n                if chash in job[\"busy\"]:\n                    nh = len(job[\"hash\"])\n                    idx = job[\"hash\"].index(chash)\n                    t = \"that chunk is already being written to:\\n  {}\\n  {} {}/{}\\n  {}\"\n                    raise Pebkac(400, t.format(wark, chash, idx, nh, job[\"name\"]))\n\n            assert chash  # type: ignore  # !rm\n            chunksize = up2k_chunksize(job[\"size\"])\n\n            coffsets = []\n            nchunks = []\n            for chash in chashes:\n                nchunk = [n for n, v in enumerate(job[\"hash\"]) if v == chash]\n                if not nchunk:\n                    raise Pebkac(400, \"unknown chunk %s\" % (chash,))\n\n                ofs = [chunksize * x for x in nchunk]\n                coffsets.append(ofs)\n                nchunks.append(nchunk)\n\n            for ofs1, ofs2 in zip(coffsets, coffsets[1:]):\n                gap = (ofs2[0] - ofs1[0]) - chunksize\n                if gap:\n                    t = \"only sibling chunks can be stitched; gap of %d bytes between offsets %d and %d in %s\"\n                    raise Pebkac(400, t % (gap, ofs1[0], ofs2[0], job[\"name\"]))\n\n            path = djoin(job[\"ptop\"], job[\"prel\"], job[\"tnam\"])\n\n            if not job[\"sprs\"]:\n                cur_sz = bos.path.getsize(path)\n                if coffsets[0][0] > cur_sz:\n                    t = \"please upload sequentially using one thread;\\nserver filesystem does not support sparse files.\\n  file: {}\\n  chunk: {}\\n  cofs: {}\\n  flen: {}\"\n                    t = t.format(job[\"name\"], nchunks[0][0], coffsets[0][0], cur_sz)\n                    raise Pebkac(400, t)\n\n            for chash in chashes:\n                job[\"busy\"][chash] = 1\n\n        job[\"poke\"] = time.time()\n\n        return chashes, chunksize, coffsets, path, job[\"lmod\"], job[\"size\"], job[\"sprs\"]\n\n    def fast_confirm_chunks(\n        self, ptop: str, wark: str, chashes: list[str], locked: list[str]\n    ) -> tuple[int, str]:\n        if not self.mutex.acquire(False):\n            return -1, \"\"\n        if not self.reg_mutex.acquire(False):\n            self.mutex.release()\n            return -1, \"\"\n        try:\n            return self._confirm_chunks(ptop, wark, chashes, locked, False)\n        finally:\n            self.reg_mutex.release()\n            self.mutex.release()\n\n    def confirm_chunks(\n        self, ptop: str, wark: str, written: list[str], locked: list[str]\n    ) -> tuple[int, str]:\n        self.fika = \"u\"\n        with self.mutex, self.reg_mutex:\n            return self._confirm_chunks(ptop, wark, written, locked, True)\n\n    def _confirm_chunks(\n        self, ptop: str, wark: str, written: list[str], locked: list[str], final: bool\n    ) -> tuple[int, str]:\n        if True:\n            self.db_act = self.vol_act[ptop] = time.time()\n            try:\n                job = self.registry[ptop][wark]\n                pdir = djoin(job[\"ptop\"], job[\"prel\"])\n                src = djoin(pdir, job[\"tnam\"])\n                dst = djoin(pdir, job[\"name\"])\n            except Exception as ex:\n                return -2, \"confirm_chunk, wark(%r)\" % (ex,)  # type: ignore\n\n            for chash in locked if final else written:\n                job[\"busy\"].pop(chash, None)\n\n            try:\n                for chash in written:\n                    job[\"need\"].remove(chash)\n            except Exception as ex:\n                for zs in locked:\n                    if job[\"busy\"].pop(zs, None):\n                        self.log(\"panic-unlock wark(%s) chunk(%s)\" % (wark, zs), 1)\n                return -2, \"confirm_chunk, chash(%s) %r\" % (chash, ex)  # type: ignore\n\n            ret = len(job[\"need\"])\n            if ret > 0:\n                return ret, src\n\n            if self.args.nw:\n                self.regdrop(ptop, wark)\n\n        return ret, dst\n\n    def finish_upload(self, ptop: str, wark: str, busy_aps: dict[str, int]) -> None:\n        self.busy_aps = busy_aps\n        self.fika = \"u\"\n        with self.mutex, self.reg_mutex:\n            self._finish_upload(ptop, wark)\n\n        if self.fx_backlog:\n            self.do_fx_backlog()\n\n    def _finish_upload(self, ptop: str, wark: str) -> None:\n        \"\"\"mutex(main,reg) me\"\"\"\n        try:\n            job = self.registry[ptop][wark]\n            pdir = djoin(job[\"ptop\"], job[\"prel\"])\n            src = djoin(pdir, job[\"tnam\"])\n            dst = djoin(pdir, job[\"name\"])\n        except Exception as ex:\n            self.log(min_ex(), 1)\n            raise Pebkac(500, \"finish_upload, wark, %r%s\" % (ex, SEESLOG))\n\n        if job[\"need\"]:\n            self.log(min_ex(), 1)\n            t = \"finish_upload %s with remaining chunks %s%s\"\n            raise Pebkac(500, t % (wark, job[\"need\"], SEESLOG))\n\n        upt = job.get(\"at\") or time.time()\n        vflags = self.flags[ptop]\n\n        atomic_move(self.log, src, dst, vflags)\n\n        times = (int(time.time()), int(job[\"lmod\"]))\n        t = \"no more chunks, setting times %s (%d) on %r\"\n        self.log(t % (times, bos.path.getsize(dst), dst))\n        bos.utime_c(self.log, dst, times[1], False)\n        # the above logmsg (and associated logic) is retained due to unforget.py\n\n        zs = \"prel name lmod size ptop vtop wark dwrk host user addr\"\n        z2 = [job[x] for x in zs.split()]\n        wake_sr = False\n        try:\n            flt = job[\"life\"]\n            vfs = self.vfs.all_vols[job[\"vtop\"]]\n            vlt = vfs.flags[\"lifetime\"]\n            if vlt and flt > 1 and flt < vlt:\n                upt -= vlt - flt\n                wake_sr = True\n                t = \"using client lifetime; at={:.0f} ({}-{})\"\n                self.log(t.format(upt, vlt, flt))\n        except:\n            pass\n\n        z2.append(upt)\n        if self.idx_wark(vflags, *z2):\n            del self.registry[ptop][wark]\n        else:\n            for k in \"host tnam busy sprs poke\".split():\n                del job[k]\n            job.pop(\"t0c\", None)\n            job[\"t0\"] = int(job[\"t0\"])\n            job[\"hash\"] = []\n            job[\"done\"] = 1\n            self.regdrop(ptop, wark)\n\n        if wake_sr:\n            with self.rescan_cond:\n                self.rescan_cond.notify_all()\n\n        dupes = self.dupesched.pop(dst, [])\n        if not dupes:\n            return\n\n        cur = self.cur.get(ptop)\n        for rd, fn, lmod in dupes:\n            d2 = djoin(ptop, rd, fn)\n            if os.path.exists(d2):\n                continue\n\n            self._symlink(dst, d2, self.flags[ptop], lmod=lmod)\n            if cur:\n                self.db_add(cur, vflags, rd, fn, lmod, *z2[3:])\n\n        if cur:\n            cur.connection.commit()\n\n    def regdrop(self, ptop: str, wark: str) -> None:\n        \"\"\"mutex(main,reg) me\"\"\"\n        olds = self.droppable[ptop]\n        if wark:\n            olds.append(wark)\n\n        if len(olds) <= self.args.reg_cap:\n            return\n\n        n = len(olds) - int(self.args.reg_cap / 2)\n        t = \"up2k-registry [{}] has {} droppables; discarding {}\"\n        self.log(t.format(ptop, len(olds), n))\n        for k in olds[:n]:\n            self.registry[ptop].pop(k, None)\n        self.droppable[ptop] = olds[n:]\n\n    def idx_wark(\n        self,\n        vflags: dict[str, Any],\n        rd: str,\n        fn: str,\n        lmod: float,\n        sz: int,\n        ptop: str,\n        vtop: str,\n        wark: str,\n        dwark: str,\n        host: str,\n        usr: str,\n        ip: str,\n        at: float,\n        skip_xau: bool = False,\n    ) -> bool:\n        cur = self.cur.get(ptop)\n        if not cur:\n            return False\n\n        self.db_act = self.vol_act[ptop] = time.time()\n        try:\n            self.db_add(\n                cur,\n                vflags,\n                rd,\n                fn,\n                lmod,\n                sz,\n                ptop,\n                vtop,\n                wark,\n                dwark,\n                host,\n                usr,\n                ip,\n                at,\n                skip_xau,\n            )\n            cur.connection.commit()\n        except Exception as ex:\n            x = self.register_vpath(ptop, {})\n            assert x  # !rm\n            db_ex_chk(self.log, ex, x[1])\n            raise\n\n        if \"e2t\" in self.flags[ptop]:\n            self.tagq.put((ptop, dwark, rd, fn, sz, ip, at))\n            self.n_tagq += 1\n\n        return True\n\n    def db_rm(\n        self, db: \"sqlite3.Cursor\", vflags: dict[str, Any], rd: str, fn: str, sz: int\n    ) -> None:\n        sql = \"delete from up where rd = ? and fn = ?\"\n        try:\n            r = db.execute(sql, (rd, fn))\n        except:\n            assert self.mem_cur  # !rm\n            r = db.execute(sql, s3enc(self.mem_cur, rd, fn))\n\n        if not r.rowcount:\n            return\n\n        self.volsize[db] -= sz\n        self.volnfiles[db] -= 1\n\n        if \"nodirsz\" not in vflags:\n            try:\n                q = \"update ds set nf=nf-1, sz=sz-? where rd=?\"\n                while True:\n                    db.execute(q, (sz, rd))\n                    if not rd:\n                        break\n                    rd = rd.rsplit(\"/\", 1)[0] if \"/\" in rd else \"\"\n            except:\n                pass\n\n    def db_add(\n        self,\n        db: \"sqlite3.Cursor\",\n        vflags: dict[str, Any],\n        rd: str,\n        fn: str,\n        ts: float,\n        sz: int,\n        ptop: str,\n        vtop: str,\n        wark: str,\n        dwark: str,\n        host: str,\n        usr: str,\n        ip: str,\n        at: float,\n        skip_xau: bool = False,\n    ) -> None:\n        \"\"\"mutex(main) me\"\"\"\n        self.db_rm(db, vflags, rd, fn, sz)\n\n        if not ip:\n            db_ip = \"\"\n        else:\n            # plugins may expect this to look like an actual IP\n            db_ip = \"1.1.1.1\" if \"no_db_ip\" in vflags else ip\n\n        sql = \"insert into up values (?,?,?,?,?,?,?,?)\"\n        v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0), usr)\n        try:\n            db.execute(sql, v)\n        except:\n            assert self.mem_cur  # !rm\n            rd, fn = s3enc(self.mem_cur, rd, fn)\n            v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0), usr)\n            db.execute(sql, v)\n\n        self.volsize[db] += sz\n        self.volnfiles[db] += 1\n\n        xau = False if skip_xau else vflags.get(\"xau\")\n        dst = djoin(ptop, rd, fn)\n        if xau:\n            hr = runhook(\n                self.log,\n                None,\n                self,\n                \"xau.up2k\",\n                xau,\n                dst,\n                djoin(vtop, rd, fn),\n                host,\n                usr,\n                self.vfs.get_perms(djoin(vtop, rd, fn), usr),\n                ts,\n                sz,\n                ip,\n                at or time.time(),\n                None,\n            )\n            t = hr.get(\"rejectmsg\") or \"\"\n            if t or hr.get(\"rc\") != 0:\n                if not t:\n                    t = \"upload blocked by xau server config: %r\"\n                    t = t % (djoin(vtop, rd, fn),)\n                self.log(t, 1)\n                wunlink(self.log, dst, vflags)\n                self.registry[ptop].pop(wark, None)\n                raise Pebkac(403, t)\n\n        xiu = vflags.get(\"xiu\")\n        if xiu:\n            cds: set[int] = set()\n            for cmd in xiu:\n                m = self.xiu_ptn.search(cmd)\n                cds.add(int(m.group(1)) if m else 5)\n\n            q = \"insert into iu values (?,?,?,?)\"\n            for cd in cds:\n                # one for each unique cooldown duration\n                try:\n                    db.execute(q, (cd, dwark[:16], rd, fn))\n                except:\n                    assert self.mem_cur  # !rm\n                    rd, fn = s3enc(self.mem_cur, rd, fn)\n                    db.execute(q, (cd, dwark[:16], rd, fn))\n\n            if self.xiu_asleep:\n                self.xiu_asleep = False\n                with self.rescan_cond:\n                    self.rescan_cond.notify_all()\n\n        if rd and sz and fn.lower() in self.args.th_coversd_set:\n            # wasteful; db_add will re-index actual covers\n            # but that won't catch existing files\n            crd, cdn = rd.rsplit(\"/\", 1) if \"/\" in rd else (\"\", rd)\n            try:\n                q = \"select fn from cv where rd=? and dn=?\"\n                db_cv = db.execute(q, (crd, cdn)).fetchone()[0]\n                db_lcv = db_cv.lower()\n                if db_lcv in self.args.th_coversd_set:\n                    idx_db = self.args.th_coversd.index(db_lcv)\n                    idx_fn = self.args.th_coversd.index(fn.lower())\n                    add_cv = idx_fn < idx_db\n                else:\n                    add_cv = True\n            except:\n                add_cv = True\n\n            if add_cv:\n                try:\n                    db.execute(\"delete from cv where rd=? and dn=?\", (crd, cdn))\n                    db.execute(\"insert into cv values (?,?,?)\", (crd, cdn, fn))\n                except:\n                    pass\n\n        if \"nodirsz\" not in vflags:\n            try:\n                q = \"update ds set nf=nf+1, sz=sz+? where rd=?\"\n                q2 = \"insert into ds values(?,?,1)\"\n                while True:\n                    if not db.execute(q, (sz, rd)).rowcount:\n                        db.execute(q2, (rd, sz))\n                    if not rd:\n                        break\n                    rd = rd.rsplit(\"/\", 1)[0] if \"/\" in rd else \"\"\n            except:\n                pass\n\n    def handle_fs_abrt(self, akey: str) -> None:\n        self.abrt_key = akey\n\n    def handle_rm(\n        self,\n        uname: str,\n        ip: str,\n        vpaths: list[str],\n        lim: list[int],\n        rm_up: bool,\n        unpost: bool,\n    ) -> str:\n        n_files = 0\n        ok = {}\n        ng = {}\n        for vp in vpaths:\n            if lim and lim[0] <= 0:\n                self.log(\"hit delete limit of {} files\".format(lim[1]), 3)\n                break\n\n            a, b, c = self._handle_rm(uname, ip, vp, lim, rm_up, unpost)\n            n_files += a\n            for k in b:\n                ok[k] = 1\n            for k in c:\n                ng[k] = 1\n\n        ng = {k: 1 for k in ng if k not in ok}\n        iok = len(ok)\n        ing = len(ng)\n\n        return \"deleted {} files (and {}/{} folders)\".format(n_files, iok, iok + ing)\n\n    def _handle_rm(\n        self, uname: str, ip: str, vpath: str, lim: list[int], rm_up: bool, unpost: bool\n    ) -> tuple[int, list[str], list[str]]:\n        self.db_act = time.time()\n        partial = \"\"\n        if not unpost:\n            permsets = [[True, False, False, True]]\n            vn0, rem0 = self.vfs.get(vpath, uname, *permsets[0])\n            vn, rem = vn0.get_dbv(rem0)\n        else:\n            # unpost with missing permissions? verify with db\n            permsets = [[False, True]]\n            vn0, rem0 = self.vfs.get(vpath, uname, *permsets[0])\n            vn, rem = vn0.get_dbv(rem0)\n            ptop = vn.realpath\n            self.fika = \"d\"\n            with self.mutex, self.reg_mutex:\n                abrt_cfg = vn.flags.get(\"u2abort\", 1)\n                addr = (ip or \"\\n\") if abrt_cfg in (1, 2) else \"\"\n                user = ((uname or \"\\n\"), \"*\") if abrt_cfg in (1, 3) else None\n                reg = self.registry.get(ptop, {}) if abrt_cfg else {}\n                for wark, job in reg.items():\n                    if (addr and addr != job[\"addr\"]) or (\n                        user and job[\"user\"] not in user\n                    ):\n                        continue\n                    jrem = djoin(job[\"prel\"], job[\"name\"])\n                    if ANYWIN:\n                        jrem = jrem.replace(\"\\\\\", \"/\")\n                    if jrem == rem:\n                        if job[\"ptop\"] != ptop:\n                            t = \"job.ptop [%s] != vol.ptop [%s] ??\"\n                            raise Exception(t % (job[\"ptop\"], ptop))\n                        partial = vn.canonical(vjoin(job[\"prel\"], job[\"tnam\"]))\n                        break\n                if partial:\n                    dip = ip\n                    dat = time.time()\n                    dun = uname\n                    un_cfg = 1\n                else:\n                    un_cfg = vn.flags[\"unp_who\"]\n                    if not self.args.unpost or not un_cfg:\n                        t = \"the unpost feature is disabled in server config\"\n                        raise Pebkac(400, t)\n\n                    _, _, _, _, dip, dat, dun = self._find_from_vpath(ptop, rem)\n\n            t = \"you cannot delete this: \"\n            if not dip:\n                t += \"file not found\"\n            elif dip != ip and un_cfg in (1, 2):\n                t += \"not uploaded by (You)\"\n            elif dun != uname and un_cfg in (1, 3):\n                t += \"not uploaded by (You)\"\n            elif dat < time.time() - self.args.unpost:\n                t += \"uploaded too long ago\"\n            else:\n                t = \"\"\n\n            if t:\n                raise Pebkac(400, t)\n\n        ptop = vn.realpath\n        atop = vn.canonical(rem, False)\n        self.vol_act[ptop] = self.db_act\n        adir, fn = os.path.split(atop)\n        try:\n            st = bos.lstat(atop)\n            is_dir = stat.S_ISDIR(st.st_mode)\n        except:\n            # NOTE: \"file not found\" *sftpd\n            raise Pebkac(400, \"file not found on disk (already deleted?)\")\n\n        if \"bcasechk\" in vn.flags and not vn.casechk(rem, False):\n            raise Pebkac(400, \"file does not exist case-sensitively\")\n\n        scandir = not self.args.no_scandir\n        if is_dir:\n            # note: deletion inside shares would require a rewrite here;\n            #   shares necessitate get_dbv which is incompatible with walk\n            g = vn0.walk(\"\", rem0, [], uname, permsets, 2, scandir, True)\n            if unpost:\n                raise Pebkac(400, \"cannot unpost folders\")\n        elif stat.S_ISLNK(st.st_mode) or stat.S_ISREG(st.st_mode):\n            voldir = vsplit(rem)[0]\n            vpath_dir = vsplit(vpath)[0]\n            g = [(vn, voldir, vpath_dir, adir, [(fn, 0)], [], {})]  # type: ignore\n        else:\n            self.log(\"rm: skip type-0%o file %r\" % (st.st_mode, atop))\n            return 0, [], []\n\n        xbd = vn.flags.get(\"xbd\")\n        xad = vn.flags.get(\"xad\")\n        n_files = 0\n        for dbv, vrem, _, adir, files, rd, vd in g:\n            for fn in [x[0] for x in files]:\n                if lim:\n                    lim[0] -= 1\n                    if lim[0] < 0:\n                        self.log(\"hit delete limit of {} files\".format(lim[1]), 3)\n                        break\n\n                abspath = djoin(adir, fn)\n                st = stl = bos.lstat(abspath)\n                if stat.S_ISLNK(st.st_mode):\n                    try:\n                        st = bos.stat(abspath)\n                    except:\n                        pass\n\n                volpath = (\"%s/%s\" % (vrem, fn)).strip(\"/\")\n                vpath = (\"%s/%s\" % (dbv.vpath, volpath)).strip(\"/\")\n                self.log(\"rm %r\\n  %r\" % (vpath, abspath))\n                if not unpost:\n                    # recursion-only sanchk\n                    _ = dbv.get(volpath, uname, *permsets[0])\n\n                if xbd:\n                    hr = runhook(\n                        self.log,\n                        None,\n                        self,\n                        \"xbd\",\n                        xbd,\n                        abspath,\n                        vpath,\n                        \"\",\n                        uname,\n                        self.vfs.get_perms(vpath, uname),\n                        stl.st_mtime,\n                        st.st_size,\n                        ip,\n                        time.time(),\n                        None,\n                    )\n                    t = hr.get(\"rejectmsg\") or \"\"\n                    if t or hr.get(\"rc\") != 0:\n                        if not t:\n                            t = \"delete blocked by xbd server config: %r\" % (abspath,)\n                        self.log(t, 1)\n                        continue\n\n                n_files += 1\n                self.fika = \"d\"\n                with self.mutex, self.reg_mutex:\n                    cur = None\n                    try:\n                        ptop = dbv.realpath\n                        xlink = bool(dbv.flags.get(\"xlink\"))\n                        cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, volpath)\n                        self._forget_file(\n                            ptop, volpath, dbv.flags, cur, wark, True, st.st_size, xlink\n                        )\n                    finally:\n                        if cur:\n                            cur.connection.commit()\n\n                wunlink(self.log, abspath, dbv.flags)\n                if partial:\n                    wunlink(self.log, partial, dbv.flags)\n                    partial = \"\"\n                if xad:\n                    runhook(\n                        self.log,\n                        None,\n                        self,\n                        \"xad\",\n                        xad,\n                        abspath,\n                        vpath,\n                        \"\",\n                        uname,\n                        self.vfs.get_perms(vpath, uname),\n                        stl.st_mtime,\n                        st.st_size,\n                        ip,\n                        time.time(),\n                        None,\n                    )\n\n        if is_dir:\n            ok, ng = rmdirs(self.log_func, scandir, True, atop, 1)\n        else:\n            ok = ng = []\n\n        if rm_up:\n            ok2, ng2 = rmdirs_up(os.path.dirname(atop), ptop)\n        else:\n            ok2 = ng2 = []\n\n        return n_files, ok + ok2, ng + ng2\n\n    def handle_cp(self, abrt: str, uname: str, ip: str, svp: str, dvp: str) -> str:\n        if svp == dvp or dvp.startswith(svp + \"/\"):\n            raise Pebkac(400, \"cp: cannot copy parent into subfolder\")\n\n        svn, srem = self.vfs.get(svp, uname, True, False)\n        dvn, drem = self.vfs.get(dvp, uname, False, True)\n        svn_dbv, _ = svn.get_dbv(srem)\n        sabs = svn.canonical(srem, False)\n        curs: set[\"sqlite3.Cursor\"] = set()\n        self.db_act = self.vol_act[svn_dbv.realpath] = time.time()\n\n        st = bos.stat(sabs)\n        if \"bcasechk\" in svn.flags and not svn.casechk(srem, False):\n            raise Pebkac(400, \"file does not exist case-sensitively\")\n\n        if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):\n            self.fika = \"c\"\n            with self.mutex:\n                try:\n                    ret = self._cp_file(uname, ip, svp, dvp, curs)\n                finally:\n                    for v in curs:\n                        v.connection.commit()\n\n                return ret\n\n        if not stat.S_ISDIR(st.st_mode):\n            raise Pebkac(400, \"cannot copy type-0%o file\" % (st.st_mode,))\n\n        permsets = [[True, False]]\n        scandir = not self.args.no_scandir\n\n        # if user can see dotfiles in target volume, only include\n        # dots from source vols where user also has the dot perm\n        dots = 1 if uname in dvn.axs.udot else 2\n\n        # don't use svn_dbv; would skip subvols due to _ls `if not rem:`\n        g = svn.walk(\"\", srem, [], uname, permsets, dots, scandir, True)\n        self.fika = \"c\"\n        with self.mutex:\n            try:\n                for dbv, vrem, _, atop, files, rd, vd in g:\n                    for fn in files:\n                        self.db_act = self.vol_act[dbv.realpath] = time.time()\n                        svpf = \"/\".join(x for x in [dbv.vpath, vrem, fn[0]] if x)\n                        if not svpf.startswith(svp + \"/\"):  # assert\n                            self.log(min_ex(), 1)\n                            t = \"cp: bug at %r, top %r%s\"\n                            raise Pebkac(500, t % (svpf, svp, SEESLOG))\n\n                        dvpf = dvp + svpf[len(svp) :]\n                        self._cp_file(uname, ip, svpf, dvpf, curs)\n                        if abrt and abrt == self.abrt_key:\n                            raise Pebkac(400, \"filecopy aborted by http-api\")\n\n                    for v in curs:\n                        v.connection.commit()\n                    curs.clear()\n            finally:\n                for v in curs:\n                    v.connection.commit()\n\n        return \"k\"\n\n    def _cp_file(\n        self, uname: str, ip: str, svp: str, dvp: str, curs: set[\"sqlite3.Cursor\"]\n    ) -> str:\n        \"\"\"mutex(main) me;  will mutex(reg)\"\"\"\n        svn, srem = self.vfs.get(svp, uname, True, False)\n        svn_dbv, srem_dbv = svn.get_dbv(srem)\n\n        dvn, drem = self.vfs.get(dvp, uname, False, True)\n        dvn, drem = dvn.get_dbv(drem)\n\n        sabs = svn.canonical(srem, False)\n        dabs = dvn.canonical(drem)\n        drd, dfn = vsplit(drem)\n\n        if bos.path.exists(dabs):\n            raise Pebkac(400, \"cp2: target file exists\")\n\n        st = stl = bos.lstat(sabs)\n        if stat.S_ISLNK(stl.st_mode):\n            is_link = True\n            try:\n                st = bos.stat(sabs)\n            except:\n                pass  # broken symlink; keep as-is\n        elif not stat.S_ISREG(st.st_mode):\n            self.log(\"skipping type-0%o file %r\" % (st.st_mode, sabs))\n            return \"\"\n        else:\n            is_link = False\n\n        ftime = stl.st_mtime\n        fsize = st.st_size\n\n        xbc = svn.flags.get(\"xbc\")\n        xac = dvn.flags.get(\"xac\")\n        if xbc:\n            hr = runhook(\n                self.log,\n                None,\n                self,\n                \"xbc\",\n                xbc,\n                sabs,\n                svp,\n                \"\",\n                uname,\n                self.vfs.get_perms(svp, uname),\n                ftime,\n                fsize,\n                ip,\n                time.time(),\n                None,\n            )\n            t = hr.get(\"rejectmsg\") or \"\"\n            if t or hr.get(\"rc\") != 0:\n                if not t:\n                    t = \"copy blocked by xbr server config: %r\" % (svp,)\n                self.log(t, 1)\n                raise Pebkac(405, t)\n\n        bos.makedirs(os.path.dirname(dabs), vf=dvn.flags)\n\n        c1, w, ftime_, fsize_, ip, at, un = self._find_from_vpath(\n            svn_dbv.realpath, srem_dbv\n        )\n        c2 = self.cur.get(dvn.realpath)\n\n        if w:\n            assert c1  # !rm\n            if c2 and c2 != c1:\n                self._copy_tags(c1, c2, w)\n\n            curs.add(c1)\n\n            if c2:\n                self.db_add(\n                    c2,\n                    {},  # skip upload hooks\n                    drd,\n                    dfn,\n                    ftime,\n                    fsize,\n                    dvn.realpath,\n                    dvn.vpath,\n                    w,\n                    w,\n                    \"\",\n                    un or \"\",\n                    ip or \"\",\n                    at or 0,\n                )\n                curs.add(c2)\n        else:\n            self.log(\"not found in src db: %r\" % (svp,))\n\n        try:\n            if is_link and st != stl:\n                # relink non-broken symlinks to still work after the move,\n                # but only resolve 1st level to maintain relativity\n                dlink = bos.readlink(sabs)\n                dlink = os.path.join(os.path.dirname(sabs), dlink)\n                dlink = bos.path.abspath(dlink)\n                self._symlink(dlink, dabs, dvn.flags, lmod=ftime)\n            else:\n                self._symlink(sabs, dabs, dvn.flags, lmod=ftime)\n\n        except OSError as ex:\n            if ex.errno != errno.EXDEV:\n                raise\n\n            self.log(\"using plain copy (%s):\\n  %r\\n  %r\" % (ex.strerror, sabs, dabs))\n            b1, b2 = fsenc(sabs), fsenc(dabs)\n            is_link = os.path.islink(b1)  # due to _relink\n            try:\n                trystat_shutil_copy2(self.log, b1, b2)\n            except:\n                try:\n                    wunlink(self.log, dabs, dvn.flags)\n                except:\n                    pass\n\n                if not is_link:\n                    raise\n\n                # broken symlink? keep it as-is\n                try:\n                    zb = os.readlink(b1)\n                    os.symlink(zb, b2)\n                except:\n                    wunlink(self.log, dabs, dvn.flags)\n                    raise\n\n            if is_link:\n                try:\n                    times = (int(time.time()), int(ftime))\n                    bos.utime(dabs, times, False)\n                except:\n                    pass\n            if \"fperms\" in dvn.flags:\n                set_ap_perms(dabs, dvn.flags)\n\n        if xac:\n            runhook(\n                self.log,\n                None,\n                self,\n                \"xac\",\n                xac,\n                dabs,\n                dvp,\n                \"\",\n                uname,\n                self.vfs.get_perms(dvp, uname),\n                ftime,\n                fsize,\n                ip,\n                time.time(),\n                None,\n            )\n\n        return \"k\"\n\n    def handle_mv(self, abrt: str, uname: str, ip: str, svp: str, dvp: str) -> str:\n        if svp == dvp or dvp.startswith(svp + \"/\"):\n            raise Pebkac(400, \"mv: cannot move parent into subfolder\")\n\n        svn, srem = self.vfs.get(svp, uname, True, False, True)\n        jail, jail_rem = svn.get_dbv(srem)\n        sabs = svn.canonical(srem, False)\n        curs: set[\"sqlite3.Cursor\"] = set()\n        self.db_act = self.vol_act[jail.realpath] = time.time()\n\n        if not jail_rem:\n            raise Pebkac(400, \"mv: cannot move a mountpoint\")\n\n        st = bos.lstat(sabs)\n        if \"bcasechk\" in svn.flags and not svn.casechk(srem, False):\n            raise Pebkac(400, \"file does not exist case-sensitively\")\n\n        if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):\n            self.fika = \"m\"\n            with self.mutex:\n                try:\n                    ret = self._mv_file(uname, ip, svp, dvp, curs)\n                finally:\n                    for v in curs:\n                        v.connection.commit()\n\n                return ret\n\n        if not stat.S_ISDIR(st.st_mode):\n            raise Pebkac(400, \"cannot move type-0%o file\" % (st.st_mode,))\n\n        permsets = [[True, False, True]]\n        scandir = not self.args.no_scandir\n\n        # following symlinks is too scary\n        g = svn.walk(\"\", srem, [], uname, permsets, 2, scandir, True)\n        for dbv, vrem, _, atop, files, rd, vd in g:\n            if dbv != jail:\n                # fail early (prevent partial moves)\n                raise Pebkac(400, \"mv: source folder contains other volumes\")\n\n        g = svn.walk(\"\", srem, [], uname, permsets, 2, scandir, True)\n        self.fika = \"m\"\n        with self.mutex:\n            try:\n                for dbv, vrem, _, atop, files, rd, vd in g:\n                    if dbv != jail:\n                        # the actual check (avoid toctou)\n                        raise Pebkac(400, \"mv: source folder contains other volumes\")\n\n                    for fn in files:\n                        self.db_act = self.vol_act[dbv.realpath] = time.time()\n                        svpf = \"/\".join(x for x in [dbv.vpath, vrem, fn[0]] if x)\n                        if not svpf.startswith(svp + \"/\"):  # assert\n                            self.log(min_ex(), 1)\n                            t = \"mv: bug at %r, top %r%s\"\n                            raise Pebkac(500, t % (svpf, svp, SEESLOG))\n\n                        dvpf = dvp + svpf[len(svp) :]\n                        self._mv_file(uname, ip, svpf, dvpf, curs)\n                        if abrt and abrt == self.abrt_key:\n                            raise Pebkac(400, \"filemove aborted by http-api\")\n\n                    for v in curs:\n                        v.connection.commit()\n                    curs.clear()\n            finally:\n                for v in curs:\n                    v.connection.commit()\n\n        rm_ok, rm_ng = rmdirs(self.log_func, scandir, True, sabs, 1)\n\n        for zsl in (rm_ok, rm_ng):\n            for ap in reversed(zsl):\n                if not ap.startswith(sabs):\n                    self.log(min_ex(), 1)\n                    t = \"mv_d: bug at %r, top %r%s\"\n                    raise Pebkac(500, t % (ap, sabs, SEESLOG))\n\n                rem = ap[len(sabs) :].replace(os.sep, \"/\").lstrip(\"/\")\n                vp = vjoin(dvp, rem)\n                try:\n                    dvn, drem = self.vfs.get(vp, uname, False, True)\n                    dap = dvn.canonical(drem)\n                    bos.mkdir(dap, dvn.flags[\"chmod_d\"])\n                    if \"chown\" in dvn.flags:\n                        bos.chown(dap, dvn.flags[\"uid\"], dvn.flags[\"gid\"])\n                except:\n                    pass\n\n        return \"k\"\n\n    def _mv_file(\n        self, uname: str, ip: str, svp: str, dvp: str, curs: set[\"sqlite3.Cursor\"]\n    ) -> str:\n        \"\"\"mutex(main) me;  will mutex(reg)\"\"\"\n        svn, srem = self.vfs.get(svp, uname, True, False, True)\n        svn, srem = svn.get_dbv(srem)\n\n        dvn, drem = self.vfs.get(dvp, uname, False, True)\n        dvn, drem = dvn.get_dbv(drem)\n\n        sabs = svn.canonical(srem, False)\n        dabs = dvn.canonical(drem)\n        drd, dfn = vsplit(drem)\n\n        n1 = svp.split(\"/\")[-1]\n        n2 = dvp.split(\"/\")[-1]\n        if n1.startswith(\".\") or n2.startswith(\".\"):\n            if self.args.no_dot_mv:\n                raise Pebkac(400, \"moving dotfiles is disabled in server config\")\n            elif self.args.no_dot_ren and n1 != n2:\n                raise Pebkac(400, \"renaming dotfiles is disabled in server config\")\n\n        if bos.path.exists(dabs):\n            raise Pebkac(400, \"mv2: target file exists\")\n\n        is_link = is_dirlink = False\n        st = stl = bos.lstat(sabs)\n        if stat.S_ISLNK(stl.st_mode):\n            is_link = True\n            try:\n                st = bos.stat(sabs)\n                is_dirlink = stat.S_ISDIR(st.st_mode)\n            except:\n                pass  # broken symlink; keep as-is\n\n        ftime = stl.st_mtime\n        fsize = st.st_size\n\n        xbr = svn.flags.get(\"xbr\")\n        xar = dvn.flags.get(\"xar\")\n        if xbr:\n            hr = runhook(\n                self.log,\n                None,\n                self,\n                \"xbr\",\n                xbr,\n                sabs,\n                svp,\n                \"\",\n                uname,\n                self.vfs.get_perms(svp, uname),\n                ftime,\n                fsize,\n                ip,\n                time.time(),\n                None,\n            )\n            t = hr.get(\"rejectmsg\") or \"\"\n            if t or hr.get(\"rc\") != 0:\n                if not t:\n                    t = \"move blocked by xbr server config: %r\" % (svp,)\n                self.log(t, 1)\n                raise Pebkac(405, t)\n\n        is_xvol = svn.realpath != dvn.realpath\n\n        bos.makedirs(os.path.dirname(dabs), vf=dvn.flags)\n\n        if is_dirlink:\n            dlabs = absreal(sabs)\n            t = \"moving symlink from %r to %r, target %r\"\n            self.log(t % (sabs, dabs, dlabs))\n            mt = bos.path.getmtime(sabs, False)\n            wunlink(self.log, sabs, svn.flags)\n            self._symlink(dlabs, dabs, dvn.flags, False, lmod=mt)\n\n            # folders are too scary, schedule rescan of both vols\n            self.need_rescan.add(svn.vpath)\n            self.need_rescan.add(dvn.vpath)\n            with self.rescan_cond:\n                self.rescan_cond.notify_all()\n\n            if xar:\n                runhook(\n                    self.log,\n                    None,\n                    self,\n                    \"xar.ln\",\n                    xar,\n                    dabs,\n                    dvp,\n                    \"\",\n                    uname,\n                    self.vfs.get_perms(dvp, uname),\n                    ftime,\n                    fsize,\n                    ip,\n                    time.time(),\n                    None,\n                )\n\n            return \"k\"\n\n        c1, w, ftime_, fsize_, ip, at, un = self._find_from_vpath(svn.realpath, srem)\n        c2 = self.cur.get(dvn.realpath)\n\n        has_dupes = False\n        if w:\n            assert c1  # !rm\n            if c2 and c2 != c1:\n                if \"nodupem\" in dvn.flags:\n                    q = \"select w from up where substr(w,1,16) = ?\"\n                    for (w2,) in c2.execute(q, (w[:16],)):\n                        if w == w2:\n                            t = \"file exists in target volume, and dupes are forbidden in config\"\n                            raise Pebkac(400, t)\n                self._copy_tags(c1, c2, w)\n\n            xlink = bool(svn.flags.get(\"xlink\"))\n\n            with self.reg_mutex:\n                has_dupes = self._forget_file(\n                    svn.realpath,\n                    srem,\n                    svn.flags,\n                    c1,\n                    w,\n                    is_xvol,\n                    fsize_ or fsize,\n                    xlink,\n                )\n\n            if not is_xvol:\n                has_dupes = self._relink(w, svn.realpath, srem, dabs, c1, xlink)\n\n            curs.add(c1)\n\n            if c2:\n                self.db_add(\n                    c2,\n                    {},  # skip upload hooks\n                    drd,\n                    dfn,\n                    ftime,\n                    fsize,\n                    dvn.realpath,\n                    dvn.vpath,\n                    w,\n                    w,\n                    \"\",\n                    un or \"\",\n                    ip or \"\",\n                    at or 0,\n                )\n                curs.add(c2)\n        else:\n            self.log(\"not found in src db: %r\" % (svp,))\n\n        try:\n            if is_xvol and has_dupes:\n                raise OSError(errno.EXDEV, \"src is symlink\")\n\n            if is_link and st != stl:\n                # relink non-broken symlinks to still work after the move,\n                # but only resolve 1st level to maintain relativity\n                dlink = bos.readlink(sabs)\n                dlink = os.path.join(os.path.dirname(sabs), dlink)\n                dlink = bos.path.abspath(dlink)\n                self._symlink(dlink, dabs, dvn.flags, lmod=ftime, is_mv=True)\n                wunlink(self.log, sabs, svn.flags)\n            else:\n                atomic_move(self.log, sabs, dabs, svn.flags)\n                if svn != dvn and \"fperms\" in dvn.flags:\n                    set_ap_perms(dabs, dvn.flags)\n\n        except OSError as ex:\n            if ex.errno != errno.EXDEV:\n                raise\n\n            self.log(\"using copy+delete (%s):\\n  %r\\n  %r\" % (ex.strerror, sabs, dabs))\n            b1, b2 = fsenc(sabs), fsenc(dabs)\n            is_link = os.path.islink(b1)  # due to _relink\n            try:\n                trystat_shutil_copy2(self.log, b1, b2)\n            except:\n                try:\n                    wunlink(self.log, dabs, dvn.flags)\n                except:\n                    pass\n\n                if not is_link:\n                    raise\n\n                # broken symlink? keep it as-is\n                try:\n                    zb = os.readlink(b1)\n                    os.symlink(zb, b2)\n                except:\n                    wunlink(self.log, dabs, dvn.flags)\n                    raise\n\n            if is_link:\n                try:\n                    times = (int(time.time()), int(ftime))\n                    bos.utime(dabs, times, False)\n                except:\n                    pass\n            if \"fperms\" in dvn.flags:\n                set_ap_perms(dabs, dvn.flags)\n\n            wunlink(self.log, sabs, svn.flags)\n\n        if xar:\n            runhook(\n                self.log,\n                None,\n                self,\n                \"xar.mv\",\n                xar,\n                dabs,\n                dvp,\n                \"\",\n                uname,\n                self.vfs.get_perms(dvp, uname),\n                ftime,\n                fsize,\n                ip,\n                time.time(),\n                None,\n            )\n\n        return \"k\"\n\n    def _copy_tags(\n        self, csrc: \"sqlite3.Cursor\", cdst: \"sqlite3.Cursor\", wark: str\n    ) -> None:\n        \"\"\"copy all tags for wark from src-db to dst-db\"\"\"\n        w = wark[:16]\n\n        if cdst.execute(\"select * from mt where w=? limit 1\", (w,)).fetchone():\n            return  # existing tags in dest db\n\n        for _, k, v in csrc.execute(\"select * from mt where w=?\", (w,)):\n            cdst.execute(\"insert into mt values(?,?,?)\", (w, k, v))\n\n    def _find_from_vpath(\n        self, ptop: str, vrem: str\n    ) -> tuple[\n        Optional[\"sqlite3.Cursor\"],\n        Optional[str],\n        Optional[int],\n        Optional[int],\n        str,\n        Optional[int],\n        str,\n    ]:\n        cur = self.cur.get(ptop)\n        if not cur:\n            return None, None, None, None, \"\", None, \"\"\n\n        rd, fn = vsplit(vrem)\n        q = \"select w, mt, sz, ip, at, un from up where rd=? and fn=? limit 1\"\n        try:\n            c = cur.execute(q, (rd, fn))\n        except:\n            assert self.mem_cur  # !rm\n            c = cur.execute(q, s3enc(self.mem_cur, rd, fn))\n\n        hit = c.fetchone()\n        if hit:\n            wark, ftime, fsize, ip, at, un = hit\n            return cur, wark, ftime, fsize, ip, at, un\n        return cur, None, None, None, \"\", None, \"\"\n\n    def _forget_file(\n        self,\n        ptop: str,\n        vrem: str,\n        vflags: dict[str, Any],\n        cur: Optional[\"sqlite3.Cursor\"],\n        wark: Optional[str],\n        drop_tags: bool,\n        sz: int,\n        xlink: bool,\n    ) -> bool:\n        \"\"\"\n        mutex(main,reg) me\n        forgets file in db, fixes symlinks, does not delete\n        \"\"\"\n        srd, sfn = vsplit(vrem)\n        has_dupes = False\n        self.log(\"forgetting %r\" % (vrem,))\n        if wark and cur:\n            self.log(\"found {} in db\".format(wark))\n            if drop_tags:\n                if self._relink(wark, ptop, vrem, \"\", cur, xlink):\n                    has_dupes = True\n                    drop_tags = False\n\n            if drop_tags:\n                q = \"delete from mt where w=?\"\n                cur.execute(q, (wark[:16],))\n\n            self.db_rm(cur, vflags, srd, sfn, sz)\n\n        reg = self.registry.get(ptop)\n        if reg:\n            vdir = vsplit(vrem)[0]\n            wark = wark or next(\n                (\n                    x\n                    for x, y in reg.items()\n                    if sfn in [y[\"name\"], y.get(\"tnam\")] and y[\"prel\"] == vdir\n                ),\n                \"\",\n            )\n            job = reg.get(wark) if wark else None\n            if job:\n                if job[\"need\"]:\n                    t = \"forgetting partial upload {} ({})\"\n                    p = self._vis_job_progress(job)\n                    self.log(t.format(wark, p))\n\n                src = djoin(ptop, vrem)\n                zi = len(self.dupesched.pop(src, []))\n                if zi:\n                    t = \"...and forgetting %d links in dupesched\"\n                    self.log(t % (zi,))\n\n                assert wark\n                del reg[wark]\n\n        return has_dupes\n\n    def _relink(\n        self,\n        wark: str,\n        sptop: str,\n        srem: str,\n        dabs: str,\n        vcur: Optional[\"sqlite3.Cursor\"],\n        xlink: bool,\n    ) -> int:\n        \"\"\"\n        update symlinks from file at svn/srem to dabs (rename),\n        or to first remaining full if no dabs (delete)\n        \"\"\"\n        dupes = []\n        sabs = djoin(sptop, srem)\n\n        if self.no_expr_idx:\n            q = r\"select rd, fn from up where w = ?\"\n            argv = (wark,)\n        else:\n            q = r\"select rd, fn from up where substr(w,1,16)=? and +w=?\"\n            argv = (wark[:16], wark)\n\n        for ptop, cur in self.cur.items():\n            if not xlink and cur and cur != vcur:\n                continue\n            for rd, fn in cur.execute(q, argv):\n                if rd.startswith(\"//\") or fn.startswith(\"//\"):\n                    rd, fn = s3dec(rd, fn)\n\n                dvrem = vjoin(rd, fn).strip(\"/\")\n                if ptop != sptop or srem != dvrem:\n                    dupes.append([ptop, dvrem])\n                    self.log(\"found %s dupe: %r %r\" % (wark, ptop, dvrem))\n\n        if not dupes:\n            return 0\n\n        full: dict[str, tuple[str, str]] = {}\n        links: dict[str, tuple[str, str]] = {}\n        for ptop, vp in dupes:\n            ap = djoin(ptop, vp)\n            try:\n                d = links if bos.path.islink(ap) else full\n                d[ap] = (ptop, vp)\n            except:\n                self.log(\"relink: not found: %r\" % (ap,))\n\n        # self.log(\"full:\\n\" + \"\\n\".join(\"  {:90}: {}\".format(*x) for x in full.items()))\n        # self.log(\"links:\\n\" + \"\\n\".join(\"  {:90}: {}\".format(*x) for x in links.items()))\n        if not dabs and not full and links:\n            # deleting final remaining full copy; swap it with a symlink\n            slabs = list(sorted(links.keys()))[0]\n            ptop, rem = links.pop(slabs)\n            self.log(\"linkswap %r and %r\" % (sabs, slabs))\n            mt = bos.path.getmtime(slabs, False)\n            flags = self.flags.get(ptop) or {}\n            atomic_move(self.log, sabs, slabs, flags)\n            try:\n                bos.utime(slabs, (int(time.time()), int(mt)), False)\n            except:\n                self.log(\"relink: failed to utime(%r, %s)\" % (slabs, mt), 3)\n            self._symlink(slabs, sabs, flags, False, is_mv=True)\n            full[slabs] = (ptop, rem)\n            sabs = slabs\n\n        if not dabs:\n            dabs = list(sorted(full.keys()))[0]\n\n        for alink, parts in links.items():\n            lmod = 0.0\n            try:\n                faulty = False\n                ldst = alink\n                try:\n                    for n in range(40):  # MAXSYMLINKS\n                        zs = bos.readlink(ldst)\n                        ldst = os.path.join(os.path.dirname(ldst), zs)\n                        ldst = bos.path.abspath(ldst)\n                        if not bos.path.islink(ldst):\n                            break\n\n                        if ldst == sabs:\n                            t = \"relink because level %d would break:\"\n                            self.log(t % (n,), 6)\n                            faulty = True\n                except Exception as ex:\n                    self.log(\"relink because walk failed: %s; %r\" % (ex, ex), 3)\n                    faulty = True\n\n                zs = absreal(alink)\n                if ldst != zs:\n                    t = \"relink because computed != actual destination:\\n  %r\\n  %r\"\n                    self.log(t % (ldst, zs), 3)\n                    ldst = zs\n                    faulty = True\n\n                if bos.path.islink(ldst):\n                    raise Exception(\"broken symlink: %s\" % (alink,))\n\n                if alink != sabs and ldst != sabs and not faulty:\n                    continue  # original symlink OK; leave it be\n\n            except Exception as ex:\n                t = \"relink because symlink verification failed: %s; %r\"\n                self.log(t % (ex, ex), 3)\n\n            self.log(\"relinking %r to %r\" % (alink, dabs))\n            flags = self.flags.get(parts[0]) or {}\n            try:\n                lmod = bos.path.getmtime(alink, False)\n                wunlink(self.log, alink, flags)\n            except:\n                pass\n\n            # this creates a link pointing from dabs to alink; alink may\n            # not exist yet, which becomes problematic if the symlinking\n            # fails and it has to fall back on hardlinking/copying files\n            # (for example a volume with symlinked dupes but no --dedup);\n            # fsrc=sabs is then a source that currently resolves to copy\n\n            self._symlink(\n                dabs, alink, flags, False, lmod=lmod or 0, fsrc=sabs, is_mv=True\n            )\n\n        return len(full) + len(links)\n\n    def _get_wark(self, cj: dict[str, Any]) -> str:\n        if len(cj[\"name\"]) > 1024 or len(cj[\"hash\"]) > 512 * 1024:  # 16TiB\n            raise Pebkac(400, \"name or numchunks not according to spec\")\n\n        for k in cj[\"hash\"]:\n            if not self.r_hash.match(k):\n                raise Pebkac(\n                    400, \"at least one hash is not according to spec: %r\" % (k,)\n                )\n\n        # try to use client-provided timestamp, don't care if it fails somehow\n        try:\n            cj[\"lmod\"] = int(cj[\"lmod\"])\n        except:\n            cj[\"lmod\"] = int(time.time())\n\n        if cj[\"hash\"]:\n            wark = up2k_wark_from_hashlist(self.salt, cj[\"size\"], cj[\"hash\"])\n        else:\n            wark = up2k_wark_from_metadata(\n                self.salt, cj[\"size\"], cj[\"lmod\"], cj[\"prel\"], cj[\"name\"]\n            )\n\n        return wark\n\n    def _hashlist_from_file(\n        self, path: str, prefix: str = \"\"\n    ) -> tuple[list[str], os.stat_result]:\n        st = bos.stat(path)\n        fsz = st.st_size\n        csz = up2k_chunksize(fsz)\n        ret = []\n        suffix = \" MB, {}\".format(path)\n        with open(fsenc(path), \"rb\", self.args.iobuf) as f:\n            if self.mth and fsz >= 1024 * 512:\n                tlt = self.mth.hash(f, fsz, csz, self.pp, prefix, suffix)\n                ret = [x[0] for x in tlt]\n                fsz = 0\n\n            while fsz > 0:\n                # same as `hash_at` except for `imutex` / bufsz\n                if self.stop:\n                    return [], st\n\n                if self.pp:\n                    mb = fsz // (1024 * 1024)\n                    self.pp.msg = prefix + str(mb) + suffix\n\n                hashobj = hashlib.sha512()\n                rem = min(csz, fsz)\n                fsz -= rem\n                while rem > 0:\n                    buf = f.read(min(rem, 64 * 1024))\n                    if not buf:\n                        raise Exception(\"EOF at \" + str(f.tell()))\n\n                    hashobj.update(buf)\n                    rem -= len(buf)\n\n                digest = hashobj.digest()[:33]\n                ret.append(ub64enc(digest).decode(\"ascii\"))\n\n        return ret, st\n\n    def _ex_hash(self, ex: Exception, ap: str) -> None:\n        eno = getattr(ex, \"errno\", 0)\n        if eno in E_FS_MEH:\n            return self.log(\"hashing failed; %r @ %r\" % (ex, ap))\n        if eno not in E_FS_CRIT:\n            return self.log(\"hashing failed; %r @ %r\\n%s\" % (ex, ap, min_ex()), 3)\n        t = \"hashing failed; %r @ %r\\n%s\\nWARNING: This MAY indicate a serious issue with your harddisk or filesystem! Please investigate %sOS-logs\\n\"\n        t2 = \"\" if ANYWIN or MACOS else \"dmesg and \"\n        return self.log(t % (ex, ap, min_ex(), t2), 1)\n\n    def _new_upload(self, job: dict[str, Any], vfs: VFS, depth: int) -> dict[str, str]:\n        pdir = djoin(job[\"ptop\"], job[\"prel\"])\n        if not job[\"size\"]:\n            try:\n                inf = bos.stat(djoin(pdir, job[\"name\"]))\n                if stat.S_ISREG(inf.st_mode):\n                    job[\"lmod\"] = inf.st_size\n                    return {}\n            except:\n                pass\n\n        vf = self.flags[job[\"ptop\"]]\n        xbu = vf.get(\"xbu\")\n        ap_chk = djoin(pdir, job[\"name\"])\n        vp_chk = djoin(job[\"vtop\"], job[\"prel\"], job[\"name\"])\n        if xbu:\n            hr = runhook(\n                self.log,\n                None,\n                self,\n                \"xbu.up2k\",\n                xbu,\n                ap_chk,\n                vp_chk,\n                job[\"host\"],\n                job[\"user\"],\n                self.vfs.get_perms(vp_chk, job[\"user\"]),\n                job[\"lmod\"],\n                job[\"size\"],\n                job[\"addr\"],\n                job[\"t0\"],\n                None,\n            )\n            t = hr.get(\"rejectmsg\") or \"\"\n            if t or hr.get(\"rc\") != 0:\n                if not t:\n                    t = \"upload blocked by xbu server config: %r\" % (vp_chk,)\n                self.log(t, 1)\n                raise Pebkac(403, t)\n            if hr.get(\"reloc\"):\n                x = pathmod(self.vfs, ap_chk, vp_chk, hr[\"reloc\"])\n                if x:\n                    ud1 = (vfs.vpath, job[\"prel\"], job[\"name\"])\n                    pdir, _, job[\"name\"], (vfs, rem) = x\n                    job[\"vcfg\"] = vf = vfs.flags\n                    job[\"ptop\"] = vfs.realpath\n                    job[\"vtop\"] = vfs.vpath\n                    job[\"prel\"] = rem\n                    job[\"name\"] = sanitize_fn(job[\"name\"])\n                    ud2 = (vfs.vpath, job[\"prel\"], job[\"name\"])\n                    if ud1 != ud2:\n                        self.log(\"xbu reloc2:%d...\" % (depth,), 6)\n                        return self._handle_json(job, depth + 1)\n\n        job[\"name\"] = self._untaken(pdir, job, job[\"t0\"])\n        self.registry[job[\"ptop\"]][job[\"wark\"]] = job\n\n        tnam = job[\"name\"] + \".PARTIAL\"\n        if self.args.dotpart:\n            tnam = \".\" + tnam\n\n        if self.args.nw:\n            job[\"tnam\"] = tnam\n            if not job[\"hash\"]:\n                del self.registry[job[\"ptop\"]][job[\"wark\"]]\n            return {}\n\n        if self.args.plain_ip:\n            dip = job[\"addr\"].replace(\":\", \".\")\n        else:\n            dip = self.hub.iphash.s(job[\"addr\"])\n\n        f, job[\"tnam\"] = ren_open(\n            tnam,\n            \"wb\",\n            fdir=pdir,\n            suffix=\"-%.6f-%s\" % (job[\"t0\"], dip),\n            vf=vf,\n        )\n        try:\n            abspath = djoin(pdir, job[\"tnam\"])\n            sprs = job[\"sprs\"]\n            sz = job[\"size\"]\n            relabel = False\n            if (\n                ANYWIN\n                and sprs\n                and self.args.sparse\n                and self.args.sparse * 1024 * 1024 <= sz\n            ):\n                try:\n                    sp.check_call([\"fsutil\", \"sparse\", \"setflag\", abspath])\n                except:\n                    self.log(\"could not sparse %r\" % (abspath,), 3)\n                    relabel = True\n                    sprs = False\n\n            if not ANYWIN and sprs and sz > 1024 * 1024:\n                fs, mnt = self.fstab.get(pdir)\n                if fs == \"ok\":\n                    pass\n                elif \"nosparse\" in vf:\n                    t = \"volflag 'nosparse' is preventing creation of sparse files for uploads to [%s]\"\n                    self.log(t % (job[\"ptop\"],))\n                    relabel = True\n                    sprs = False\n                elif \"sparse\" in vf:\n                    t = \"volflag 'sparse' is forcing creation of sparse files for uploads to [%s]\"\n                    self.log(t % (job[\"ptop\"],))\n                    relabel = True\n                else:\n                    relabel = True\n                    f.seek(1024 * 1024 - 1)\n                    f.write(b\"e\")\n                    f.flush()\n                    try:\n                        nblk = bos.stat(abspath).st_blocks\n                        sprs = nblk < 2048\n                    except:\n                        sprs = False\n\n            if relabel:\n                t = \"sparse files {} on {} filesystem at {}\"\n                nv = \"ok\" if sprs else \"ng\"\n                self.log(t.format(nv, self.fstab.get(pdir), pdir))\n                self.fstab.relabel(pdir, nv)\n                job[\"sprs\"] = sprs\n\n            if job[\"hash\"] and sprs:\n                f.seek(sz - 1)\n                f.write(b\"e\")\n        finally:\n            f.close()\n\n        if not job[\"hash\"]:\n            self._finish_upload(job[\"ptop\"], job[\"wark\"])\n\n        return {}\n\n    def _snapshot(self) -> None:\n        slp = self.args.snap_wri\n        if not slp or self.args.no_snap:\n            return\n\n        while True:\n            time.sleep(slp)\n            if self.pp:\n                slp = 5\n            else:\n                slp = self.args.snap_wri\n                self.do_snapshot()\n\n    def do_snapshot(self) -> None:\n        self.fika = \"u\"\n        with self.mutex, self.reg_mutex:\n            for k, reg in self.registry.items():\n                self._snap_reg(k, reg)\n\n    def _snap_reg(self, ptop: str, reg: dict[str, dict[str, Any]]) -> None:\n        now = time.time()\n        histpath = self.vfs.dbpaths.get(ptop)\n        if not histpath:\n            return\n\n        idrop = self.args.snap_drop * 60\n        rm = [x for x in reg.values() if x[\"need\"] and now - x[\"poke\"] >= idrop]\n\n        if self.args.nw:\n            lost = []\n        else:\n            lost = [\n                x\n                for x in reg.values()\n                if x[\"need\"]\n                and not bos.path.exists(djoin(x[\"ptop\"], x[\"prel\"], x[\"name\"]))\n            ]\n\n        if rm or lost:\n            t = \"dropping {} abandoned, {} deleted uploads in {}\"\n            t = t.format(len(rm), len(lost), ptop)\n            rm.extend(lost)\n            vis = [self._vis_job_progress(x) for x in rm]\n            self.log(\"\\n\".join([t] + vis))\n            for job in rm:\n                del reg[job[\"wark\"]]\n                rsv_cleared = False\n                try:\n                    # remove the filename reservation\n                    path = djoin(job[\"ptop\"], job[\"prel\"], job[\"name\"])\n                    if bos.path.getsize(path) == 0:\n                        bos.unlink(path)\n                        rsv_cleared = True\n                except:\n                    pass\n\n                try:\n                    if len(job[\"hash\"]) == len(job[\"need\"]) or (\n                        rsv_cleared and \"rm_partial\" in self.flags[job[\"ptop\"]]\n                    ):\n                        # PARTIAL is empty (hash==need) or --rm-partial, so delete that too\n                        path = djoin(job[\"ptop\"], job[\"prel\"], job[\"tnam\"])\n                        bos.unlink(path)\n                except:\n                    pass\n\n        if self.args.nw or self.args.no_snap:\n            return\n\n        path = os.path.join(histpath, \"up2k.snap\")\n        if not reg:\n            if ptop not in self.snap_prev or self.snap_prev[ptop] is not None:\n                self.snap_prev[ptop] = None\n                if bos.path.exists(path):\n                    bos.unlink(path)\n            return\n\n        newest = float(\n            max(x[\"t0\"] if \"done\" in x else x[\"poke\"] for _, x in reg.items())\n            if reg\n            else 0\n        )\n        etag = (len(reg), newest)\n        if etag == self.snap_prev.get(ptop):\n            return\n\n        if bos.makedirs(histpath):\n            hidedir(histpath)\n\n        path2 = \"{}.{}\".format(path, os.getpid())\n        body = {\"droppable\": self.droppable[ptop], \"registry\": reg}\n        j = json.dumps(body, sort_keys=True, separators=(\",\\n\", \": \")).encode(\"utf-8\")\n        # j = re.sub(r'\"(need|hash)\": \\[\\],\\n', \"\", j)  # bytes=slow, utf8=hungry\n        j = j.replace(b'\"need\": [],\\n', b\"\")  # surprisingly optimal\n        j = j.replace(b'\"hash\": [],\\n', b\"\")\n        with gzip.GzipFile(path2, \"wb\") as f:\n            f.write(j)\n\n        atomic_move(self.log, path2, path, VF_CAREFUL)\n\n        self.log(\"snap: %s |%d| %.2fs\" % (path, len(reg), time.time() - now))\n        self.snap_prev[ptop] = etag\n\n    def _tagger(self) -> None:\n        with self.mutex:\n            self.n_tagq += 1\n\n        assert self.mtag\n        while True:\n            with self.mutex:\n                self.n_tagq -= 1\n\n            ptop, wark, rd, fn, sz, ip, at = self.tagq.get()\n            if \"e2t\" not in self.flags[ptop]:\n                continue\n\n            # self.log(\"\\n  \" + repr([ptop, rd, fn]))\n            abspath = djoin(ptop, rd, fn)\n            try:\n                tags = self.mtag.get(abspath, self.flags[ptop]) if sz else {}\n                ntags1 = len(tags)\n                parsers = self._get_parsers(ptop, tags, abspath)\n                if self.args.mtag_vv:\n                    t = \"parsers({}): {}\\n{} {} tags: {}\".format(\n                        ptop, list(parsers.keys()), ntags1, self.mtag.backend, tags\n                    )\n                    self.log(t)\n\n                if parsers:\n                    tags[\"up_ip\"] = ip\n                    tags[\"up_at\"] = at\n                    tags.update(self.mtag.get_bin(parsers, abspath, tags))\n            except Exception as ex:\n                self._log_tag_err(\"\", abspath, ex)\n                continue\n\n            with self.mutex:\n                cur = self.cur[ptop]\n                if not cur:\n                    self.log(\"no cursor to write tags with??\", c=1)\n                    continue\n\n                # TODO is undef if vol 404 on startup\n                entags = self.entags.get(ptop)\n                if not entags:\n                    self.log(\"no entags okay.jpg\", c=3)\n                    continue\n\n                self._tag_file(cur, entags, wark, abspath, tags)\n                cur.connection.commit()\n\n            self.log(\"tagged %r (%d+%d)\" % (abspath, ntags1, len(tags) - ntags1))\n\n    def _hasher(self) -> None:\n        with self.hashq_mutex:\n            self.n_hashq += 1\n\n        while True:\n            with self.hashq_mutex:\n                self.n_hashq -= 1\n            # self.log(\"hashq {}\".format(self.n_hashq))\n\n            task = self.hashq.get()\n            if len(task) != 9:\n                raise Exception(\"invalid hash task\")\n\n            try:\n                if not self._hash_t(task) and self.stop:\n                    return\n            except Exception as ex:\n                self.log(\"failed to hash %r: %s\" % (task, ex), 1)\n\n    def _hash_t(\n        self, task: tuple[str, str, dict[str, Any], str, str, str, float, str, bool]\n    ) -> bool:\n        ptop, vtop, flags, rd, fn, ip, at, usr, skip_xau = task\n        # self.log(\"hashq {} pop {}/{}/{}\".format(self.n_hashq, ptop, rd, fn))\n        with self.mutex, self.reg_mutex:\n            if not self.register_vpath(ptop, flags):\n                return True\n\n        abspath = djoin(ptop, rd, fn)\n        self.log(\"hashing %r\" % (abspath,))\n        inf = bos.stat(abspath)\n        if not inf.st_size:\n            wark = up2k_wark_from_metadata(\n                self.salt, inf.st_size, int(inf.st_mtime), rd, fn\n            )\n        else:\n            hashes, _ = self._hashlist_from_file(abspath)\n            if not hashes:\n                return False\n\n            wark = up2k_wark_from_hashlist(self.salt, inf.st_size, hashes)\n\n        with self.mutex, self.reg_mutex:\n            self.idx_wark(\n                self.flags[ptop],\n                rd,\n                fn,\n                inf.st_mtime,\n                inf.st_size,\n                ptop,\n                vtop,\n                wark,\n                wark,\n                \"\",\n                usr,\n                ip,\n                at,\n                skip_xau,\n            )\n\n        if at and time.time() - at > 30:\n            with self.rescan_cond:\n                self.rescan_cond.notify_all()\n\n        if self.fx_backlog:\n            self.do_fx_backlog()\n\n        return True\n\n    def hash_file(\n        self,\n        ptop: str,\n        vtop: str,\n        flags: dict[str, Any],\n        rd: str,\n        fn: str,\n        ip: str,\n        at: float,\n        usr: str,\n        skip_xau: bool = False,\n    ) -> None:\n        if \"e2d\" not in flags:\n            return\n\n        if self.n_hashq > 1024:\n            t = \"%d files in hashq; taking a nap\"\n            self.log(t % (self.n_hashq,), 6)\n\n            for _ in range(self.n_hashq // 1024):\n                time.sleep(0.1)\n                if self.n_hashq < 1024:\n                    break\n\n        zt = (ptop, vtop, flags, rd, fn, ip, at, usr, skip_xau)\n        with self.hashq_mutex:\n            self.hashq.put(zt)\n            self.n_hashq += 1\n\n    def do_fx_backlog(self):\n        with self.mutex, self.reg_mutex:\n            todo = self.fx_backlog\n            self.fx_backlog = []\n        for act, hr, req_vp in todo:\n            self.hook_fx(act, hr, req_vp)\n\n    def hook_fx(self, act: str, hr: dict[str, str], req_vp: str) -> None:\n        bad = [k for k in hr if k != \"vp\"]\n        if bad:\n            t = \"got unsupported key in %s from hook: %s\"\n            raise Exception(t % (act, bad))\n\n        for fvp in hr.get(\"vp\") or []:\n            # expect vpath including filename; either absolute\n            # or relative to the client's vpath (request url)\n            if fvp.startswith(\"/\"):\n                fvp, fn = vsplit(fvp[1:])\n                fvp = \"/\" + fvp\n            else:\n                fvp, fn = vsplit(fvp)\n\n            x = pathmod(self.vfs, \"\", req_vp, {\"vp\": fvp, \"fn\": fn})\n            if not x:\n                t = \"hook_fx(%s): failed to resolve %r based on %r\"\n                self.log(t % (act, fvp, req_vp))\n                continue\n\n            ap, rd, fn, (vn, rem) = x\n            vp = vjoin(rd, fn)\n            if not vp:\n                raise Exception(\"hook_fx: blank vp from pathmod\")\n\n            if act == \"idx\":\n                rd = rd[len(vn.vpath) :].strip(\"/\")\n                self.hash_file(\n                    vn.realpath, vn.vpath, vn.flags, rd, fn, \"\", time.time(), \"\", True\n                )\n\n            if act == \"del\":\n                self._handle_rm(LEELOO_DALLAS, \"\", vp, [], False, False)\n\n    def shutdown(self) -> None:\n        self.stop = True\n        self.fika = \"f\"\n\n        if self.mth:\n            self.mth.stop = True\n\n        # in case we're killed early\n        for x in list(self.spools):\n            self._unspool(x)\n\n        if not self.args.no_snap:\n            self.log(\"writing snapshot\")\n            self.do_snapshot()\n\n        t0 = time.time()\n        while self.pp:\n            time.sleep(0.1)\n            if time.time() - t0 >= 1:\n                break\n\n        # if there is time\n        for x in list(self.spools):\n            self._unspool(x)\n\n        for cur in self.cur.values():\n            db = cur.connection\n            try:\n                db.interrupt()\n            except:\n                pass\n\n            cur.close()\n            db.close()\n\n        if self.mem_cur:\n            db = self.mem_cur.connection\n            self.mem_cur.close()\n            db.close()\n\n        self.registry = {}\n\n\ndef up2k_chunksize(filesize: int) -> int:\n    chunksize = 1024 * 1024\n    stepsize = 512 * 1024\n    while True:\n        for mul in [1, 2]:\n            nchunks = math.ceil(filesize * 1.0 / chunksize)\n            if nchunks <= 256 or (chunksize >= 32 * 1024 * 1024 and nchunks <= 4096):\n                return chunksize\n\n            chunksize += stepsize\n            stepsize *= mul\n\n\ndef up2k_wark_from_hashlist(salt: str, filesize: int, hashes: list[str]) -> str:\n    \"\"\"server-reproducible file identifier, independent of name or location\"\"\"\n    values = [salt, str(filesize)] + hashes\n    vstr = \"\\n\".join(values)\n\n    wark = hashlib.sha512(vstr.encode(\"utf-8\")).digest()[:33]\n    return ub64enc(wark).decode(\"ascii\")\n\n\ndef up2k_wark_from_metadata(salt: str, sz: int, lastmod: int, rd: str, fn: str) -> str:\n    ret = sfsenc(\"%s\\n%d\\n%d\\n%s\\n%s\" % (salt, lastmod, sz, rd, fn))\n    ret = ub64enc(hashlib.sha512(ret).digest())\n    return (\"#%s\" % (ret.decode(\"ascii\"),))[:44]\n"
  },
  {
    "path": "copyparty/util.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport argparse\nimport base64\nimport binascii\nimport codecs\nimport errno\nimport hashlib\nimport hmac\nimport json\nimport logging\nimport math\nimport mimetypes\nimport os\nimport platform\nimport re\nimport select\nimport shutil\nimport signal\nimport socket\nimport stat\nimport struct\nimport subprocess as sp  # nosec\nimport sys\nimport threading\nimport time\nimport traceback\nfrom collections import Counter\n\nfrom ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network\nfrom queue import Queue\n\ntry:\n    from zlib_ng import gzip_ng as gzip\n    from zlib_ng import zlib_ng as zlib\n\n    sys.modules[\"gzip\"] = gzip\n    # sys.modules[\"zlib\"] = zlib\n    # `- somehow makes tarfile 3% slower with default malloc, and barely faster with mimalloc\nexcept:\n    import gzip\n    import zlib\n\nfrom .__init__ import (\n    ANYWIN,\n    EXE,\n    GRAAL,\n    MACOS,\n    PY2,\n    PY36,\n    TYPE_CHECKING,\n    VT100,\n    WINDOWS,\n    EnvParams,\n    unicode,\n)\nfrom .__version__ import S_BUILD_DT, S_VERSION\n\n\ndef noop(*a, **ka):\n    pass\n\n\ntry:\n    from datetime import datetime, timezone\n\n    UTC = timezone.utc\nexcept:\n    from datetime import datetime, timedelta, tzinfo\n\n    TD_ZERO = timedelta(0)\n\n    class _UTC(tzinfo):\n        def utcoffset(self, dt):\n            return TD_ZERO\n\n        def tzname(self, dt):\n            return \"UTC\"\n\n        def dst(self, dt):\n            return TD_ZERO\n\n    UTC = _UTC()\n\n\nif PY2:\n    range = xrange  # type: ignore\n    from .stolen import surrogateescape\n\n    surrogateescape.register_surrogateescape()\n\n\nif sys.version_info >= (3, 7) or (\n    PY36 and platform.python_implementation() == \"CPython\"\n):\n    ODict = dict\nelse:\n    from collections import OrderedDict as ODict\n\n\ndef _ens(want: str) -> tuple[int, ...]:\n    ret: list[int] = []\n    for v in want.split():\n        try:\n            ret.append(getattr(errno, v))\n        except:\n            pass\n\n    return tuple(ret)\n\n\n# WSAECONNRESET - foribly closed by remote\n# WSAENOTSOCK - no longer a socket\n# EUNATCH - can't assign requested address (wifi down)\nE_SCK = _ens(\"ENOTCONN EUNATCH EBADF WSAENOTSOCK WSAECONNRESET\")\nE_SCK_WR = _ens(\"EPIPE ESHUTDOWN EBADFD\")\nE_ADDR_NOT_AVAIL = _ens(\"EADDRNOTAVAIL WSAEADDRNOTAVAIL\")\nE_ADDR_IN_USE = _ens(\"EADDRINUSE WSAEADDRINUSE\")\nE_ACCESS = _ens(\"EACCES WSAEACCES\")\nE_UNREACH = _ens(\"EHOSTUNREACH WSAEHOSTUNREACH ENETUNREACH WSAENETUNREACH\")\nE_FS_MEH = _ens(\"EPERM EACCES ENOENT ENOTCAPABLE\")\nE_FS_CRIT = _ens(\"EIO EFAULT EUCLEAN ENOTBLK\")\n\nIP6ALL = \"0:0:0:0:0:0:0:0\"\nIP6_LL = (\"fe8\", \"fe9\", \"fea\", \"feb\")\nIP64_LL = (\"fe8\", \"fe9\", \"fea\", \"feb\", \"169.254\")\n\nUC_CDISP = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._\"\nBC_CDISP = UC_CDISP.encode(\"ascii\")\nUC_CDISP_SET = set(UC_CDISP)\nBC_CDISP_SET = set(BC_CDISP)\n\ntry:\n    import fcntl\n\n    HAVE_FCNTL = True\n    HAVE_FICLONE = hasattr(fcntl, \"FICLONE\")\nexcept:\n    HAVE_FCNTL = False\n    HAVE_FICLONE = False\n\ntry:\n    import termios\nexcept:\n    pass\n\ntry:\n    if os.environ.get(\"PRTY_NO_CTYPES\"):\n        raise Exception()\n\n    import ctypes\nexcept:\n    ctypes = None\n\ntry:\n    wk32 = ctypes.WinDLL(str(\"kernel32\"), use_last_error=True)  # type: ignore\nexcept:\n    wk32 = None\n\ntry:\n    if os.environ.get(\"PRTY_NO_IFADDR\"):\n        raise Exception()\n    try:\n        if os.environ.get(\"PRTY_SYS_ALL\") or os.environ.get(\"PRTY_SYS_IFADDR\"):\n            raise ImportError()\n\n        from .stolen.ifaddr import get_adapters\n    except ImportError:\n        from ifaddr import get_adapters\n\n    HAVE_IFADDR = True\nexcept:\n    HAVE_IFADDR = False\n\n    def get_adapters(include_unconfigured=False):\n        return []\n\n\ntry:\n    if os.environ.get(\"PRTY_NO_SQLITE\"):\n        raise Exception()\n\n    HAVE_SQLITE3 = True\n    import sqlite3\n\n    assert hasattr(sqlite3, \"connect\")  # graalpy\nexcept:\n    HAVE_SQLITE3 = False\n\ntry:\n    import importlib.util\n\n    HAVE_ZMQ = bool(importlib.util.find_spec(\"zmq\"))\nexcept:\n    HAVE_ZMQ = False\n\ntry:\n    if os.environ.get(\"PRTY_NO_PSUTIL\"):\n        raise Exception()\n\n    HAVE_PSUTIL = True\n    import psutil\nexcept:\n    HAVE_PSUTIL = False\n\ntry:\n    if os.environ.get(\"PRTY_NO_MAGIC\") or (\n        ANYWIN and not os.environ.get(\"PRTY_FORCE_MAGIC\")\n    ):\n        raise Exception()\n\n    import magic\nexcept:\n    pass\n\nif os.environ.get(\"PRTY_MODSPEC\"):\n    from inspect import getsourcefile\n\n    print(\"PRTY_MODSPEC: ifaddr:\", getsourcefile(get_adapters))\n\nif True:  # pylint: disable=using-constant-test\n    import types\n    from collections.abc import Callable, Iterable\n\n    import typing\n    from typing import IO, Any, Generator, Optional, Pattern, Protocol, Union\n\n    try:\n        from typing import LiteralString\n    except:\n        pass\n\n    class RootLogger(Protocol):\n        def __call__(self, src: str, msg: str, c: Union[int, str] = 0) -> None:\n            return None\n\n    class NamedLogger(Protocol):\n        def __call__(self, msg: str, c: Union[int, str] = 0) -> None:\n            return None\n\n\nif TYPE_CHECKING:\n    from .authsrv import VFS\n    from .broker_util import BrokerCli\n    from .up2k import Up2k\n\nFAKE_MP = False\n\ntry:\n    if os.environ.get(\"PRTY_NO_MP\"):\n        raise ImportError()\n\n    import multiprocessing as mp\n\n    # import multiprocessing.dummy as mp\nexcept ImportError:\n    # support jython\n    mp = None  # type: ignore\n\nif not PY2:\n    from io import BytesIO\nelse:\n    from StringIO import StringIO as BytesIO  # type: ignore\n\n\ntry:\n    if os.environ.get(\"PRTY_NO_IPV6\"):\n        raise Exception()\n\n    socket.inet_pton(socket.AF_INET6, \"::1\")\n    HAVE_IPV6 = True\n\n    if GRAAL:\n        try:\n            # --python.PosixModuleBackend=java throws OSError: illegal IP address\n            socket.inet_pton(socket.AF_INET, \"127.0.0.1\")\n        except:\n            _inet_pton = socket.inet_pton\n\n            def inet_pton(fam, ip):\n                if fam == socket.AF_INET:\n                    return socket.inet_aton(ip)\n                return _inet_pton(fam, ip)\n\n            socket.inet_pton = inet_pton\nexcept:\n\n    def inet_pton(fam, ip):\n        return socket.inet_aton(ip)\n\n    socket.inet_pton = inet_pton\n    HAVE_IPV6 = False\n\n\ntry:\n    struct.unpack(b\">i\", b\"idgi\")\n    spack = struct.pack  # type: ignore\n    sunpack = struct.unpack  # type: ignore\nexcept:\n\n    def spack(fmt: bytes, *a: Any) -> bytes:\n        return struct.pack(fmt.decode(\"ascii\"), *a)\n\n    def sunpack(fmt: bytes, a: bytes) -> tuple[Any, ...]:\n        return struct.unpack(fmt.decode(\"ascii\"), a)\n\n\ntry:\n    BITNESS = struct.calcsize(b\"P\") * 8\nexcept:\n    BITNESS = struct.calcsize(\"P\") * 8\n\n\nCAN_SIGMASK = not (ANYWIN or PY2 or GRAAL)\n\n\nRE_ANSI = re.compile(\"\\033\\\\[[^mK]*[mK]\")\nRE_HTML_SH = re.compile(r\"[<>&$?`\\\"';]\")\nRE_CTYPE = re.compile(r\"^content-type: *([^; ]+)\", re.IGNORECASE)\nRE_CDISP = re.compile(r\"^content-disposition: *([^; ]+)\", re.IGNORECASE)\nRE_CDISP_FIELD = re.compile(\n    r'^content-disposition:(?: *|.*; *)name=\"([^\"]+)\"', re.IGNORECASE\n)\nRE_CDISP_FILE = re.compile(\n    r'^content-disposition:(?: *|.*; *)filename=\"(.*)\"', re.IGNORECASE\n)\nRE_MEMTOTAL = re.compile(\"^MemTotal:.* kB\")\nRE_MEMAVAIL = re.compile(\"^MemAvailable:.* kB\")\n\n\nif PY2:\n\n    def umktrans(s1, s2):\n        return {ord(c1): ord(c2) for c1, c2 in zip(s1, s2)}\n\nelse:\n    umktrans = str.maketrans\n\nFNTL_WIN = umktrans('<>:|?*\"\\\\/', \"＜＞：｜？＊＂＼／\")\nVPTL_WIN = umktrans('<>:|?*\"\\\\', \"＜＞：｜？＊＂＼\")\nAPTL_WIN = umktrans('<>:|?*\"/', \"＜＞：｜？＊＂／\")\nFNTL_MAC = VPTL_MAC = APTL_MAC = umktrans(\":\", \"：\")\nFNTL_OS = FNTL_WIN if ANYWIN else FNTL_MAC if MACOS else None\nVPTL_OS = VPTL_WIN if ANYWIN else VPTL_MAC if MACOS else None\nAPTL_OS = APTL_WIN if ANYWIN else APTL_MAC if MACOS else None\n\n\nBOS_SEP = (\"%s\" % (os.sep,)).encode(\"ascii\")\n\n\nif WINDOWS and PY2:\n    FS_ENCODING = \"utf-8\"\nelse:\n    FS_ENCODING = sys.getfilesystemencoding()\n\n\nSYMTIME = PY36 and os.utime in os.supports_follow_symlinks\n\nMETA_NOBOTS = '<meta name=\"robots\" content=\"noindex, nofollow\">\\n'\n\n# smart enough to understand javascript while also ignoring rel=\"nofollow\"\nBAD_BOTS = r\"Barkrowler|bingbot|BLEXBot|Googlebot|GoogleOther|GPTBot|PetalBot|SeekportBot|SemrushBot|YandexBot\"\n\nFFMPEG_URL = \"https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z\"\n\nURL_PRJ = \"https://github.com/9001/copyparty\"\n\nURL_BUG = URL_PRJ + \"/issues/new?labels=bug&template=bug_report.md\"\n\nHTTPCODE = {\n    200: \"OK\",\n    201: \"Created\",\n    202: \"Accepted\",\n    204: \"No Content\",\n    206: \"Partial Content\",\n    207: \"Multi-Status\",\n    301: \"Moved Permanently\",\n    302: \"Found\",\n    304: \"Not Modified\",\n    400: \"Bad Request\",\n    401: \"Unauthorized\",\n    403: \"Forbidden\",\n    404: \"Not Found\",\n    405: \"Method Not Allowed\",\n    409: \"Conflict\",\n    411: \"Length Required\",\n    412: \"Precondition Failed\",\n    413: \"Payload Too Large\",\n    415: \"Unsupported Media Type\",\n    416: \"Requested Range Not Satisfiable\",\n    422: \"Unprocessable Entity\",\n    423: \"Locked\",\n    429: \"Too Many Requests\",\n    500: \"Internal Server Error\",\n    501: \"Not Implemented\",\n    503: \"Service Unavailable\",\n    999: \"MissingNo\",\n}\n\n\nIMPLICATIONS = [\n    [\"e2dsa\", \"e2ds\"],\n    [\"e2ds\", \"e2d\"],\n    [\"e2tsr\", \"e2ts\"],\n    [\"e2ts\", \"e2t\"],\n    [\"e2t\", \"e2d\"],\n    [\"e2vu\", \"e2v\"],\n    [\"e2vp\", \"e2v\"],\n    [\"e2v\", \"e2d\"],\n    [\"hardlink_only\", \"hardlink\"],\n    [\"hardlink\", \"dedup\"],\n    [\"tftpvv\", \"tftpv\"],\n    [\"nodupem\", \"nodupe\"],\n    [\"no_dupe_m\", \"no_dupe\"],\n    [\"nohtml\", \"noscript\"],\n    [\"sftpvv\", \"sftpv\"],\n    [\"smbw\", \"smb\"],\n    [\"smb1\", \"smb\"],\n    [\"smbvvv\", \"smbvv\"],\n    [\"smbvv\", \"smbv\"],\n    [\"smbv\", \"smb\"],\n    [\"zv\", \"zmv\"],\n    [\"zv\", \"zsv\"],\n    [\"z\", \"zm\"],\n    [\"z\", \"zs\"],\n    [\"zmvv\", \"zmv\"],\n    [\"zm4\", \"zm\"],\n    [\"zm6\", \"zm\"],\n    [\"zmv\", \"zm\"],\n    [\"zms\", \"zm\"],\n    [\"zsv\", \"zs\"],\n]\nif ANYWIN:\n    IMPLICATIONS.extend([[\"z\", \"zm4\"]])\n\n\nUNPLICATIONS = [[\"no_dav\", \"daw\"]]\n\n\nDAV_ALLPROP_L = [\n    \"contentclass\",\n    \"creationdate\",\n    \"defaultdocument\",\n    \"displayname\",\n    \"getcontentlanguage\",\n    \"getcontentlength\",\n    \"getcontenttype\",\n    \"getlastmodified\",\n    \"href\",\n    \"iscollection\",\n    \"ishidden\",\n    \"isreadonly\",\n    \"isroot\",\n    \"isstructureddocument\",\n    \"lastaccessed\",\n    \"name\",\n    \"parentname\",\n    \"resourcetype\",\n    \"supportedlock\",\n]\nDAV_ALLPROPS = set(DAV_ALLPROP_L)\n\n\nFAVICON_MIMES = {\n    \"gif\": \"image/gif\",\n    \"png\": \"image/png\",\n    \"svg\": \"image/svg+xml\",\n}\n\n\nMIMES = {\n    \"opus\": \"audio/ogg; codecs=opus\",\n    \"owa\": \"audio/webm; codecs=opus\",\n}\n\n\ndef _add_mimes() -> set[str]:\n    # `mimetypes` is woefully unpopulated on windows\n    # but will be used as fallback on linux\n\n    for ln in \"\"\"text css html csv\napplication json wasm xml pdf rtf zip jar fits wasm\nimage webp jpeg png gif bmp jxl jp2 jxs jxr tiff bpg heic heif avif\naudio aac ogg wav flac ape amr\nvideo webm mp4 mpeg\nfont woff woff2 otf ttf\n\"\"\".splitlines():\n        k, vs = ln.split(\" \", 1)\n        for v in vs.strip().split():\n            MIMES[v] = \"{}/{}\".format(k, v)\n\n    for ln in \"\"\"text md=plain txt=plain js=javascript\napplication 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\napplication msi=x-ms-installer cab=vnd.ms-cab-compressed rpm=x-rpm crx=x-chrome-extension\napplication epub=epub+zip mobi=x-mobipocket-ebook lit=x-ms-reader rss=rss+xml atom=atom+xml torrent=x-bittorrent\napplication p7s=pkcs7-signature dcm=dicom shx=vnd.shx shp=vnd.shp dbf=x-dbf gml=gml+xml gpx=gpx+xml amf=x-amf\napplication swf=x-shockwave-flash m3u=vnd.apple.mpegurl db3=vnd.sqlite3 sqlite=vnd.sqlite3\ntext ass=plain ssa=plain\nimage jpg=jpeg xpm=x-xpixmap psd=vnd.adobe.photoshop jpf=jpx tif=tiff ico=x-icon djvu=vnd.djvu\nimage heics=heic-sequence heifs=heif-sequence hdr=vnd.radiance svg=svg+xml\nimage arw=x-sony-arw cr2=x-canon-cr2 crw=x-canon-crw dcr=x-kodak-dcr dng=x-adobe-dng erf=x-epson-erf\nimage k25=x-kodak-k25 kdc=x-kodak-kdc mrw=x-minolta-mrw nef=x-nikon-nef orf=x-olympus-orf\nimage pef=x-pentax-pef raf=x-fuji-raf raw=x-panasonic-raw sr2=x-sony-sr2 srf=x-sony-srf x3f=x-sigma-x3f\naudio caf=x-caf mp3=mpeg m4a=mp4 m4b=mp4 m4r=mp4 mid=midi mpc=musepack aif=aiff au=basic qcp=qcelp\nvideo mkv=x-matroska mov=quicktime avi=x-msvideo m4v=x-m4v ts=mp2t\nvideo asf=x-ms-asf flv=x-flv 3gp=3gpp 3g2=3gpp2 rmvb=vnd.rn-realmedia-vbr\nfont ttc=collection\n\"\"\".splitlines():\n        k, ems = ln.split(\" \", 1)\n        for em in ems.strip().split():\n            ext, mime = em.split(\"=\")\n            MIMES[ext] = \"{}/{}\".format(k, mime)\n\n    ptn = re.compile(\"html|script|tension|wasm|xml\")\n    return {x for x in MIMES.values() if not ptn.search(x)}\n\n\nSAFE_MIMES = _add_mimes()\n\n\nEXTS: dict[str, str] = {v: k for k, v in MIMES.items()}\n\nEXTS[\"vnd.mozilla.apng\"] = \"png\"\n\nMAGIC_MAP = {\"jpeg\": \"jpg\"}\n\n\nDEF_EXP = \"self.ip self.ua self.uname self.host cfg.name cfg.logout vf.scan vf.thsize hdr.cf-ipcountry srv.itime srv.htime\"\n\nDEF_MTE = \".files,circle,album,.tn,artist,title,tdate,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash\"\n\nDEF_MTH = \"tdate,.vq,.aq,vc,ac,fmt,res,.fps\"\n\n\nREKOBO_KEY = {\n    v: ln.split(\" \", 1)[0]\n    for ln in \"\"\"\n1B 6d B\n2B 7d Gb F#\n3B 8d Db C#\n4B 9d Ab G#\n5B 10d Eb D#\n6B 11d Bb A#\n7B 12d F\n8B 1d C\n9B 2d G\n10B 3d D\n11B 4d A\n12B 5d E\n1A 6m Abm G#m\n2A 7m Ebm D#m\n3A 8m Bbm A#m\n4A 9m Fm\n5A 10m Cm\n6A 11m Gm\n7A 12m Dm\n8A 1m Am\n9A 2m Em\n10A 3m Bm\n11A 4m Gbm F#m\n12A 5m Dbm C#m\n\"\"\".strip().split(\n        \"\\n\"\n    )\n    for v in ln.strip().split(\" \")[1:]\n    if v\n}\n\nREKOBO_LKEY = {k.lower(): v for k, v in REKOBO_KEY.items()}\n\n\n_exestr = \"python3 python ffmpeg ffprobe cfssl cfssljson cfssl-certinfo\"\nCMD_EXEB = set(_exestr.encode(\"utf-8\").split())\nCMD_EXES = set(_exestr.split())\n\n\n# mostly from https://github.com/github/gitignore/blob/main/Global/macOS.gitignore\nAPPLESAN_TXT = r\"/(__MACOS|Icon\\r\\r)|/\\.(_|DS_Store|AppleDouble|LSOverride|DocumentRevisions-|fseventsd|Spotlight-|TemporaryItems|Trashes|VolumeIcon\\.icns|com\\.apple\\.timemachine\\.donotpresent|AppleDB|AppleDesktop|apdisk)\"\nAPPLESAN_RE = re.compile(APPLESAN_TXT)\n\n\nHUMANSIZE_UNITS = (\"B\", \"KiB\", \"MiB\", \"GiB\", \"TiB\", \"PiB\", \"EiB\")\n\nUNHUMANIZE_UNITS = {\n    \"b\": 1,\n    \"k\": 1024,\n    \"m\": 1024 * 1024,\n    \"g\": 1024 * 1024 * 1024,\n    \"t\": 1024 * 1024 * 1024 * 1024,\n    \"p\": 1024 * 1024 * 1024 * 1024 * 1024,\n    \"e\": 1024 * 1024 * 1024 * 1024 * 1024 * 1024,\n}\n\nVF_CAREFUL = {\"mv_re_t\": 5, \"rm_re_t\": 5, \"mv_re_r\": 0.1, \"rm_re_r\": 0.1}\n\nFN_EMB = set([\".prologue.html\", \".epilogue.html\", \"readme.md\", \"preadme.md\"])\n\n\ndef read_ram() -> tuple[float, float]:\n    # NOTE: apparently no need to consider /sys/fs/cgroup/memory.max\n    #  (cgroups2) since the limit is synced to /proc/meminfo\n    a = b = 0\n    try:\n        with open(\"/proc/meminfo\", \"rb\", 0x10000) as f:\n            zsl = f.read(0x10000).decode(\"ascii\", \"replace\").split(\"\\n\")\n\n        p = RE_MEMTOTAL\n        zs = next((x for x in zsl if p.match(x)))\n        a = int((int(zs.split()[1]) / 0x100000) * 100) / 100\n\n        p = RE_MEMAVAIL\n        zs = next((x for x in zsl if p.match(x)))\n        b = int((int(zs.split()[1]) / 0x100000) * 100) / 100\n    except:\n        pass\n    return a, b\n\n\nRAM_TOTAL, RAM_AVAIL = read_ram()\n\n\npybin = sys.executable or \"\"\nif EXE:\n    pybin = \"\"\n    for zsg in \"python3 python\".split():\n        try:\n            if ANYWIN:\n                zsg += \".exe\"\n\n            zsg = shutil.which(zsg)\n            if zsg:\n                pybin = zsg\n                break\n        except:\n            pass\n\n\ndef py_desc() -> str:\n    interp = platform.python_implementation()\n    py_ver = \".\".join([str(x) for x in sys.version_info])\n    ofs = py_ver.find(\".final.\")\n    if ofs > 0:\n        py_ver = py_ver[:ofs]\n    if \"free-threading\" in sys.version:\n        py_ver += \"t\"\n\n    host_os = platform.system()\n    compiler = platform.python_compiler().split(\"http\")[0]\n\n    m = re.search(r\"([0-9]+\\.[0-9\\.]+)\", platform.version())\n    os_ver = m.group(1) if m else \"\"\n\n    return \"{:>9} v{} on {}{} {} [{}]\".format(\n        interp, py_ver, host_os, BITNESS, os_ver, compiler\n    )\n\n\ndef expat_ver() -> str:\n    try:\n        import pyexpat\n\n        return \".\".join([str(x) for x in pyexpat.version_info])\n    except:\n        return \"?\"\n\n\ndef _sqlite_ver() -> str:\n    assert sqlite3  # type: ignore  # !rm\n    try:\n        co = sqlite3.connect(\":memory:\")\n        cur = co.cursor()\n        try:\n            vs = cur.execute(\"select * from pragma_compile_options\").fetchall()\n        except:\n            vs = cur.execute(\"pragma compile_options\").fetchall()\n\n        v = next(x[0].split(\"=\")[1] for x in vs if x[0].startswith(\"THREADSAFE=\"))\n        cur.close()\n        co.close()\n    except:\n        v = \"W\"\n\n    return \"{}*{}\".format(sqlite3.sqlite_version, v)\n\n\ntry:\n    SQLITE_VER = _sqlite_ver()\nexcept:\n    SQLITE_VER = \"(None)\"\n\ntry:\n    from jinja2 import __version__ as JINJA_VER\nexcept:\n    JINJA_VER = \"(None)\"\n\ntry:\n    if os.environ.get(\"PRTY_NO_PYFTPD\"):\n        raise Exception()\n\n    from pyftpdlib.__init__ import __ver__ as PYFTPD_VER\nexcept:\n    PYFTPD_VER = \"(None)\"\n\ntry:\n    if os.environ.get(\"PRTY_NO_PARTFTPY\"):\n        raise Exception()\n\n    from partftpy.__init__ import __version__ as PARTFTPY_VER\nexcept:\n    PARTFTPY_VER = \"(None)\"\n\ntry:\n    if os.environ.get(\"PRTY_NO_PARAMIKO\"):\n        raise Exception()\n\n    from paramiko import __version__ as MIKO_VER\nexcept:\n    MIKO_VER = \"(None)\"\n\n\nPY_DESC = py_desc()\n\nVERSIONS = \"copyparty v{} ({})\\n{}\\n   sqlite {} | jinja {} | pyftpd {} | tftp {} | miko {}\".format(\n    S_VERSION,\n    S_BUILD_DT,\n    PY_DESC,\n    SQLITE_VER,\n    JINJA_VER,\n    PYFTPD_VER,\n    PARTFTPY_VER,\n    MIKO_VER,\n)\n\n\ntry:\n    _b64_enc_tl = bytes.maketrans(b\"+/\", b\"-_\")\n    _b64_dec_tl = bytes.maketrans(b\"-_\", b\"+/\")\n\n    def ub64enc(bs: bytes) -> bytes:\n        x = binascii.b2a_base64(bs, newline=False)\n        return x.translate(_b64_enc_tl)\n\n    def ub64dec(bs: bytes) -> bytes:\n        bs = bs.translate(_b64_dec_tl)\n        return binascii.a2b_base64(bs)\n\n    def b64enc(bs: bytes) -> bytes:\n        return binascii.b2a_base64(bs, newline=False)\n\n    def b64dec(bs: bytes) -> bytes:\n        return binascii.a2b_base64(bs)\n\n    zb = b\">>>????\"\n    zb2 = base64.urlsafe_b64encode(zb)\n    if zb2 != ub64enc(zb) or zb != ub64dec(zb2):\n        raise Exception(\"bad smoke\")\n\nexcept Exception as ex:\n    ub64enc = base64.urlsafe_b64encode  # type: ignore\n    ub64dec = base64.urlsafe_b64decode  # type: ignore\n    b64enc = base64.b64encode  # type: ignore\n    b64dec = base64.b64decode  # type: ignore\n    if PY36:\n        print(\"using fallback base64 codec due to %r\" % (ex,))\n\n\nclass NotUTF8(Exception):\n    pass\n\n\ndef read_utf8(log: Optional[\"NamedLogger\"], ap: Union[str, bytes], strict: bool) -> str:\n    with open(ap, \"rb\") as f:\n        buf = f.read()\n\n    if buf.startswith(b\"\\xef\\xbb\\xbf\"):\n        buf = buf[3:]\n\n    try:\n        return buf.decode(\"utf-8\", \"strict\")\n    except UnicodeDecodeError as ex:\n        eo = ex.start\n        eb = buf[eo : eo + 1]\n\n    if not strict:\n        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.\"\n        t = t % (ap, eb, eo)\n        if log:\n            log(t, 3)\n        else:\n            print(t)\n        return buf.decode(\"utf-8\", \"replace\")\n\n    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.\"\n    t = t % (ap, eb, eo)\n    if log:\n        log(t, 3)\n    else:\n        print(t)\n    raise NotUTF8(t)\n\n\nclass Daemon(threading.Thread):\n    def __init__(\n        self,\n        target: Any,\n        name: Optional[str] = None,\n        a: Optional[Iterable[Any]] = None,\n        r: bool = True,\n        ka: Optional[dict[Any, Any]] = None,\n    ) -> None:\n        threading.Thread.__init__(self, name=name)\n        self.a = a or ()\n        self.ka = ka or {}\n        self.fun = target\n        self.daemon = True\n        if r:\n            self.start()\n\n    def run(self):\n        if CAN_SIGMASK:\n            signal.pthread_sigmask(\n                signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM, signal.SIGUSR1]\n            )\n\n        self.fun(*self.a, **self.ka)\n\n\nclass Netdev(object):\n    def __init__(self, ip: str, idx: int, name: str, desc: str):\n        self.ip = ip\n        self.idx = idx\n        self.name = name\n        self.desc = desc\n\n    def __str__(self):\n        return \"{}-{}{}\".format(self.idx, self.name, self.desc)\n\n    def __repr__(self):\n        return \"'{}-{}'\".format(self.idx, self.name)\n\n    def __lt__(self, rhs):\n        return str(self) < str(rhs)\n\n    def __eq__(self, rhs):\n        return str(self) == str(rhs)\n\n\nclass Cooldown(object):\n    def __init__(self, maxage: float) -> None:\n        self.maxage = maxage\n        self.mutex = threading.Lock()\n        self.hist: dict[str, float] = {}\n        self.oldest = 0.0\n\n    def poke(self, key: str) -> bool:\n        with self.mutex:\n            now = time.time()\n\n            ret = False\n            pv: float = self.hist.get(key, 0)\n            if now - pv > self.maxage:\n                self.hist[key] = now\n                ret = True\n\n            if self.oldest - now > self.maxage * 2:\n                self.hist = {\n                    k: v for k, v in self.hist.items() if now - v < self.maxage\n                }\n                self.oldest = sorted(self.hist.values())[0]\n\n            return ret\n\n\nclass HLog(logging.Handler):\n    def __init__(self, log_func: \"RootLogger\") -> None:\n        logging.Handler.__init__(self)\n        self.log_func = log_func\n        self.ptn_ftp = re.compile(r\"^([0-9a-f:\\.]+:[0-9]{1,5})-\\[\")\n        self.ptn_smb_ign = re.compile(r\"^(Callback added|Config file parsed)\")\n\n    def __repr__(self) -> str:\n        level = logging.getLevelName(self.level)\n        return \"<%s cpp(%s)>\" % (self.__class__.__name__, level)\n\n    def flush(self) -> None:\n        pass\n\n    def emit(self, record: logging.LogRecord) -> None:\n        msg = self.format(record)\n        lv = record.levelno\n        if lv < logging.INFO:\n            c = 6\n        elif lv < logging.WARNING:\n            c = 0\n        elif lv < logging.ERROR:\n            c = 3\n        else:\n            c = 1\n\n        if record.name == \"pyftpdlib\":\n            m = self.ptn_ftp.match(msg)\n            if m:\n                ip = m.group(1)\n                msg = msg[len(ip) + 1 :]\n                if ip.startswith(\"::ffff:\"):\n                    record.name = ip[7:]\n                else:\n                    record.name = ip\n        elif record.name.startswith(\"impacket\"):\n            if self.ptn_smb_ign.match(msg):\n                return\n        elif record.name.startswith(\"partftpy.\"):\n            record.name = record.name[9:]\n\n        self.log_func(record.name[-21:], msg, c)\n\n\nclass NetMap(object):\n    def __init__(\n        self,\n        ips: list[str],\n        cidrs: list[str],\n        keep_lo=False,\n        strict_cidr=False,\n        defer_mutex=False,\n    ) -> None:\n        \"\"\"\n        ips: list of plain ipv4/ipv6 IPs, not cidr\n        cidrs: list of cidr-notation IPs (ip/prefix)\n        \"\"\"\n\n        # fails multiprocessing; defer assignment\n        self.mutex: Optional[threading.Lock] = None if defer_mutex else threading.Lock()\n\n        if \"::\" in ips:\n            ips = [x for x in ips if x != \"::\"] + list(\n                [x.split(\"/\")[0] for x in cidrs if \":\" in x]\n            )\n            ips.append(\"0.0.0.0\")\n\n        if \"0.0.0.0\" in ips:\n            ips = [x for x in ips if x != \"0.0.0.0\"] + list(\n                [x.split(\"/\")[0] for x in cidrs if \":\" not in x]\n            )\n\n        if not keep_lo:\n            ips = [x for x in ips if x not in (\"::1\", \"127.0.0.1\")]\n\n        ips = find_prefix(ips, cidrs)\n\n        self.cache: dict[str, str] = {}\n        self.b2sip: dict[bytes, str] = {}\n        self.b2net: dict[bytes, Union[IPv4Network, IPv6Network]] = {}\n        self.bip: list[bytes] = []\n        for ip in ips:\n            v6 = \":\" in ip\n            fam = socket.AF_INET6 if v6 else socket.AF_INET\n            bip = socket.inet_pton(fam, ip.split(\"/\")[0])\n            self.bip.append(bip)\n            self.b2sip[bip] = ip.split(\"/\")[0]\n            self.b2net[bip] = (IPv6Network if v6 else IPv4Network)(ip, strict_cidr)\n\n        self.bip.sort(reverse=True)\n\n    def map(self, ip: str) -> str:\n        if ip.startswith(\"::ffff:\"):\n            ip = ip[7:]\n\n        try:\n            return self.cache[ip]\n        except:\n            # intentionally crash the calling thread if unset:\n            assert self.mutex  # type: ignore  # !rm\n\n            with self.mutex:\n                return self._map(ip)\n\n    def _map(self, ip: str) -> str:\n        v6 = \":\" in ip\n        ci = IPv6Address(ip) if v6 else IPv4Address(ip)\n        bip = next((x for x in self.bip if ci in self.b2net[x]), None)\n        ret = self.b2sip[bip] if bip else \"\"\n        if len(self.cache) > 9000:\n            self.cache = {}\n        self.cache[ip] = ret\n        return ret\n\n\nclass UnrecvEOF(OSError):\n    pass\n\n\nclass _Unrecv(object):\n    \"\"\"\n    undo any number of socket recv ops\n    \"\"\"\n\n    def __init__(self, s: socket.socket, log: Optional[\"NamedLogger\"]) -> None:\n        self.s = s\n        self.log = log\n        self.buf: bytes = b\"\"\n        self.nb = 0\n        self.te = 0\n\n    def recv(self, nbytes: int, spins: int = 1) -> bytes:\n        if self.buf:\n            ret = self.buf[:nbytes]\n            self.buf = self.buf[nbytes:]\n            self.nb += len(ret)\n            return ret\n\n        while True:\n            try:\n                ret = self.s.recv(nbytes)\n                break\n            except socket.timeout:\n                spins -= 1\n                if spins <= 0:\n                    ret = b\"\"\n                    break\n                continue\n            except:\n                ret = b\"\"\n                break\n\n        if not ret:\n            raise UnrecvEOF(\"client stopped sending data\")\n\n        self.nb += len(ret)\n        return ret\n\n    def recv_ex(self, nbytes: int, raise_on_trunc: bool = True) -> bytes:\n        \"\"\"read an exact number of bytes\"\"\"\n        ret = b\"\"\n        try:\n            while nbytes > len(ret):\n                ret += self.recv(nbytes - len(ret))\n        except OSError:\n            t = \"client stopped sending data; expected at least %d more bytes\"\n            if not ret:\n                t = t % (nbytes,)\n            else:\n                t += \", only got %d\"\n                t = t % (nbytes, len(ret))\n                if len(ret) <= 16:\n                    t += \"; %r\" % (ret,)\n\n            if raise_on_trunc:\n                raise UnrecvEOF(5, t)\n            elif self.log:\n                self.log(t, 3)\n\n        return ret\n\n    def unrecv(self, buf: bytes) -> None:\n        self.buf = buf + self.buf\n        self.nb -= len(buf)\n\n\n# !rm.yes>\nclass _LUnrecv(object):\n    \"\"\"\n    with expensive debug logging\n    \"\"\"\n\n    def __init__(self, s: socket.socket, log: Optional[\"NamedLogger\"]) -> None:\n        self.s = s\n        self.log = log\n        self.buf = b\"\"\n        self.nb = 0\n\n    def recv(self, nbytes: int, spins: int) -> bytes:\n        if self.buf:\n            ret = self.buf[:nbytes]\n            self.buf = self.buf[nbytes:]\n            t = \"\\033[0;7mur:pop:\\033[0;1;32m {}\\n\\033[0;7mur:rem:\\033[0;1;35m {}\\033[0m\"\n            print(t.format(ret, self.buf))\n            self.nb += len(ret)\n            return ret\n\n        ret = self.s.recv(nbytes)\n        t = \"\\033[0;7mur:recv\\033[0;1;33m {}\\033[0m\"\n        print(t.format(ret))\n        if not ret:\n            raise UnrecvEOF(\"client stopped sending data\")\n\n        self.nb += len(ret)\n        return ret\n\n    def recv_ex(self, nbytes: int, raise_on_trunc: bool = True) -> bytes:\n        \"\"\"read an exact number of bytes\"\"\"\n        try:\n            ret = self.recv(nbytes, 1)\n            err = False\n        except:\n            ret = b\"\"\n            err = True\n\n        while not err and len(ret) < nbytes:\n            try:\n                ret += self.recv(nbytes - len(ret), 1)\n            except OSError:\n                err = True\n\n        if err:\n            t = \"client only sent {} of {} expected bytes\".format(len(ret), nbytes)\n            if raise_on_trunc:\n                raise UnrecvEOF(t)\n            elif self.log:\n                self.log(t, 3)\n\n        return ret\n\n    def unrecv(self, buf: bytes) -> None:\n        self.buf = buf + self.buf\n        self.nb -= len(buf)\n        t = \"\\033[0;7mur:push\\033[0;1;31m {}\\n\\033[0;7mur:rem:\\033[0;1;35m {}\\033[0m\"\n        print(t.format(buf, self.buf))\n\n\n# !rm.no>\n\n\nUnrecv = _Unrecv\n\n\nclass CachedSet(object):\n    def __init__(self, maxage: float) -> None:\n        self.c: dict[Any, float] = {}\n        self.maxage = maxage\n        self.oldest = 0.0\n\n    def add(self, v: Any) -> None:\n        self.c[v] = time.time()\n\n    def cln(self) -> None:\n        now = time.time()\n        if now - self.oldest < self.maxage:\n            return\n\n        c = self.c = {k: v for k, v in self.c.items() if now - v < self.maxage}\n        try:\n            self.oldest = c[min(c, key=c.get)]  # type: ignore\n        except:\n            self.oldest = now\n\n\nclass CachedDict(object):\n    def __init__(self, maxage: float) -> None:\n        self.c: dict[str, tuple[float, Any]] = {}\n        self.maxage = maxage\n        self.oldest = 0.0\n\n    def set(self, k: str, v: Any) -> None:\n        now = time.time()\n        self.c[k] = (now, v)\n        if now - self.oldest < self.maxage:\n            return\n\n        c = self.c = {k: v for k, v in self.c.items() if now - v[0] < self.maxage}\n        try:\n            self.oldest = min([x[0] for x in c.values()])\n        except:\n            self.oldest = now\n\n    def get(self, k: str) -> Optional[tuple[str, Any]]:\n        try:\n            ts, ret = self.c[k]\n            now = time.time()\n            if now - ts > self.maxage:\n                del self.c[k]\n                return None\n            return ret\n        except:\n            return None\n\n\nclass FHC(object):\n    class CE(object):\n        def __init__(self, fh: typing.BinaryIO) -> None:\n            self.ts: float = 0\n            self.fhs = [fh]\n            self.all_fhs = set([fh])\n\n    def __init__(self) -> None:\n        self.cache: dict[str, FHC.CE] = {}\n        self.aps: dict[str, int] = {}\n\n    def close(self, path: str) -> None:\n        try:\n            ce = self.cache[path]\n        except:\n            return\n\n        for fh in ce.fhs:\n            fh.close()\n\n        del self.cache[path]\n        del self.aps[path]\n\n    def clean(self) -> None:\n        if not self.cache:\n            return\n\n        keep = {}\n        now = time.time()\n        for path, ce in self.cache.items():\n            if now < ce.ts + 5:\n                keep[path] = ce\n            else:\n                for fh in ce.fhs:\n                    fh.close()\n\n        self.cache = keep\n\n    def pop(self, path: str) -> typing.BinaryIO:\n        return self.cache[path].fhs.pop()\n\n    def put(self, path: str, fh: typing.BinaryIO) -> None:\n        if path not in self.aps:\n            self.aps[path] = 0\n\n        try:\n            ce = self.cache[path]\n            ce.all_fhs.add(fh)\n            ce.fhs.append(fh)\n        except:\n            ce = self.CE(fh)\n            self.cache[path] = ce\n\n        ce.ts = time.time()\n\n\nclass ProgressPrinter(threading.Thread):\n    \"\"\"\n    periodically print progress info without linefeeds\n    \"\"\"\n\n    def __init__(self, log: \"NamedLogger\", args: argparse.Namespace) -> None:\n        threading.Thread.__init__(self, name=\"pp\")\n        self.daemon = True\n        self.log = log\n        self.args = args\n        self.msg = \"\"\n        self.end = False\n        self.n = -1\n\n    def run(self) -> None:\n        sigblock()\n        tp = 0\n        msg = None\n        slp_pr = self.args.scan_pr_r\n        slp_ps = min(slp_pr, self.args.scan_st_r)\n        no_stdout = self.args.q or slp_pr == slp_ps\n        fmt = \" {}\\033[K\\r\" if VT100 else \" {} $\\r\"\n        while not self.end:\n            time.sleep(slp_ps)\n            if msg == self.msg or self.end:\n                continue\n\n            msg = self.msg\n            now = time.time()\n            if msg and now - tp >= slp_pr:\n                tp = now\n                self.log(\"progress: %r\" % (msg,), 6)\n\n            if no_stdout:\n                continue\n\n            uprint(fmt.format(msg))\n            if PY2:\n                sys.stdout.flush()\n\n        if no_stdout:\n            return\n\n        if VT100:\n            print(\"\\033[K\", end=\"\")\n        elif msg:\n            print(\"------------------------\")\n\n        sys.stdout.flush()  # necessary on win10 even w/ stderr btw\n\n\nclass MTHash(object):\n    def __init__(self, cores: int):\n        self.pp: Optional[ProgressPrinter] = None\n        self.f: Optional[typing.BinaryIO] = None\n        self.sz = 0\n        self.csz = 0\n        self.stop = False\n        self.readsz = 1024 * 1024 * (2 if (RAM_AVAIL or 2) < 1 else 12)\n        self.omutex = threading.Lock()\n        self.imutex = threading.Lock()\n        self.work_q: Queue[int] = Queue()\n        self.done_q: Queue[tuple[int, str, int, int]] = Queue()\n        self.thrs = []\n        for n in range(cores):\n            t = Daemon(self.worker, \"mth-\" + str(n))\n            self.thrs.append(t)\n\n    def hash(\n        self,\n        f: typing.BinaryIO,\n        fsz: int,\n        chunksz: int,\n        pp: Optional[ProgressPrinter] = None,\n        prefix: str = \"\",\n        suffix: str = \"\",\n    ) -> list[tuple[str, int, int]]:\n        with self.omutex:\n            self.f = f\n            self.sz = fsz\n            self.csz = chunksz\n\n            chunks: dict[int, tuple[str, int, int]] = {}\n            nchunks = int(math.ceil(fsz / chunksz))\n            for nch in range(nchunks):\n                self.work_q.put(nch)\n\n            ex: Optional[Exception] = None\n            for nch in range(nchunks):\n                qe = self.done_q.get()\n                try:\n                    nch, dig, ofs, csz = qe\n                    chunks[nch] = (dig, ofs, csz)\n                except:\n                    ex = ex or qe  # type: ignore\n\n                if pp:\n                    mb = (fsz - nch * chunksz) // (1024 * 1024)\n                    pp.msg = prefix + str(mb) + suffix\n\n            if ex:\n                raise ex\n\n            ret = []\n            for n in range(nchunks):\n                ret.append(chunks[n])\n\n            self.f = None\n            self.csz = 0\n            self.sz = 0\n            return ret\n\n    def worker(self) -> None:\n        while True:\n            ofs = self.work_q.get()\n            try:\n                v = self.hash_at(ofs)\n            except Exception as ex:\n                v = ex  # type: ignore\n\n            self.done_q.put(v)\n\n    def hash_at(self, nch: int) -> tuple[int, str, int, int]:\n        f = self.f\n        ofs = ofs0 = nch * self.csz\n        chunk_sz = chunk_rem = min(self.csz, self.sz - ofs)\n        if self.stop:\n            return nch, \"\", ofs0, chunk_sz\n\n        assert f  # !rm\n        hashobj = hashlib.sha512()\n        while chunk_rem > 0:\n            with self.imutex:\n                f.seek(ofs)\n                buf = f.read(min(chunk_rem, self.readsz))\n\n            if not buf:\n                raise Exception(\"EOF at \" + str(ofs))\n\n            hashobj.update(buf)\n            chunk_rem -= len(buf)\n            ofs += len(buf)\n\n        bdig = hashobj.digest()[:33]\n        udig = ub64enc(bdig).decode(\"ascii\")\n        return nch, udig, ofs0, chunk_sz\n\n\nclass HMaccas(object):\n    def __init__(self, keypath: str, retlen: int) -> None:\n        self.retlen = retlen\n        self.cache: dict[bytes, str] = {}\n        try:\n            with open(keypath, \"rb\") as f:\n                self.key = f.read()\n                if len(self.key) != 64:\n                    raise Exception()\n        except:\n            self.key = os.urandom(64)\n            with open(keypath, \"wb\") as f:\n                f.write(self.key)\n\n    def b(self, msg: bytes) -> str:\n        try:\n            return self.cache[msg]\n        except:\n            if len(self.cache) > 9000:\n                self.cache = {}\n\n            zb = hmac.new(self.key, msg, hashlib.sha512).digest()\n            zs = ub64enc(zb)[: self.retlen].decode(\"ascii\")\n            self.cache[msg] = zs\n            return zs\n\n    def s(self, msg: str) -> str:\n        return self.b(msg.encode(\"utf-8\", \"replace\"))\n\n\nclass Magician(object):\n    def __init__(self) -> None:\n        self.bad_magic = False\n        self.mutex = threading.Lock()\n        self.magic: Optional[\"magic.Magic\"] = None\n\n    def ext(self, fpath: str) -> str:\n        try:\n            if self.bad_magic:\n                raise Exception()\n\n            if not self.magic:\n                try:\n                    with self.mutex:\n                        if not self.magic:\n                            self.magic = magic.Magic(uncompress=False, extension=True)\n                except:\n                    self.bad_magic = True\n                    raise\n\n            with self.mutex:\n                ret = self.magic.from_file(fpath)\n        except:\n            ret = \"?\"\n\n        ret = ret.split(\"/\")[0]\n        ret = MAGIC_MAP.get(ret, ret)\n        if \"?\" not in ret:\n            return ret\n\n        mime = magic.from_file(fpath, mime=True)\n        mime = re.split(\"[; ]\", mime, maxsplit=1)[0]\n        try:\n            return EXTS[mime]\n        except:\n            pass\n\n        mg = mimetypes.guess_extension(mime)\n        if mg:\n            return mg[1:]\n        else:\n            raise Exception()\n\n\nclass Garda(object):\n    \"\"\"ban clients for repeated offenses\"\"\"\n\n    def __init__(self, cfg: str, uniq: bool = True) -> None:\n        self.uniq = uniq\n        try:\n            a, b, c = cfg.strip().split(\",\")\n            self.lim = int(a)\n            self.win = int(b) * 60\n            self.pen = int(c) * 60\n        except:\n            self.lim = self.win = self.pen = 0\n\n        self.ct: dict[str, list[int]] = {}\n        self.prev: dict[str, str] = {}\n        self.last_cln = 0\n\n    def cln(self, ip: str) -> None:\n        n = 0\n        ok = int(time.time() - self.win)\n        for v in self.ct[ip]:\n            if v < ok:\n                n += 1\n            else:\n                break\n        if n:\n            te = self.ct[ip][n:]\n            if te:\n                self.ct[ip] = te\n            else:\n                del self.ct[ip]\n                try:\n                    del self.prev[ip]\n                except:\n                    pass\n\n    def allcln(self) -> None:\n        for k in list(self.ct):\n            self.cln(k)\n\n        self.last_cln = int(time.time())\n\n    def bonk(self, ip: str, prev: str) -> tuple[int, str]:\n        if not self.lim:\n            return 0, ip\n\n        if \":\" in ip:\n            # assume /64 clients; drop 4 groups\n            ip = IPv6Address(ip).exploded[:-20]\n\n        if prev and self.uniq:\n            if self.prev.get(ip) == prev:\n                return 0, ip\n\n            self.prev[ip] = prev\n\n        now = int(time.time())\n        try:\n            self.ct[ip].append(now)\n        except:\n            self.ct[ip] = [now]\n\n        if now - self.last_cln > 300:\n            self.allcln()\n        else:\n            self.cln(ip)\n\n        if len(self.ct[ip]) >= self.lim:\n            return now + self.pen, ip\n        else:\n            return 0, ip\n\n\nif WINDOWS and sys.version_info < (3, 8):\n    _popen = sp.Popen\n\n    def _spopen(c, *a, **ka):\n        enc = sys.getfilesystemencoding()\n        c = [x.decode(enc, \"replace\") if hasattr(x, \"decode\") else x for x in c]\n        return _popen(c, *a, **ka)\n\n    sp.Popen = _spopen\n\n\ndef uprint(msg: str) -> None:\n    try:\n        print(msg, end=\"\")\n    except UnicodeEncodeError:\n        try:\n            print(msg.encode(\"utf-8\", \"replace\").decode(), end=\"\")\n        except:\n            print(msg.encode(\"ascii\", \"replace\").decode(), end=\"\")\n\n\ndef nuprint(msg: str) -> None:\n    uprint(\"%s\\n\" % (msg,))\n\n\ndef dedent(txt: str) -> str:\n    pad = 64\n    lns = txt.replace(\"\\r\", \"\").split(\"\\n\")\n    for ln in lns:\n        zs = ln.lstrip()\n        pad2 = len(ln) - len(zs)\n        if zs and pad > pad2:\n            pad = pad2\n    return \"\\n\".join([ln[pad:] for ln in lns])\n\n\ndef rice_tid() -> str:\n    tid = threading.current_thread().ident\n    c = sunpack(b\"B\" * 5, spack(b\">Q\", tid)[-5:])\n    return \"\".join(\"\\033[1;37;48;5;{0}m{0:02x}\".format(x) for x in c) + \"\\033[0m\"\n\n\ndef trace(*args: Any, **kwargs: Any) -> None:\n    t = time.time()\n    stack = \"\".join(\n        \"\\033[36m%s\\033[33m%s\" % (x[0].split(os.sep)[-1][:-3], x[1])\n        for x in traceback.extract_stack()[3:-1]\n    )\n    parts = [\"%.6f\" % (t,), rice_tid(), stack]\n\n    if args:\n        parts.append(repr(args))\n\n    if kwargs:\n        parts.append(repr(kwargs))\n\n    msg = \"\\033[0m \".join(parts)\n    # _tracebuf.append(msg)\n    nuprint(msg)\n\n\ndef alltrace(verbose: bool = True) -> str:\n    threads: dict[str, types.FrameType] = {}\n    names = dict([(t.ident, t.name) for t in threading.enumerate()])\n    for tid, stack in sys._current_frames().items():\n        if verbose:\n            name = \"%s (%x)\" % (names.get(tid), tid)\n        else:\n            name = str(names.get(tid))\n        threads[name] = stack\n\n    rret: list[str] = []\n    bret: list[str] = []\n    np = -3 if verbose else -2\n    for name, stack in sorted(threads.items()):\n        ret = [\"\\n\\n# %s\" % (name,)]\n        pad = None\n        for fn, lno, name, line in traceback.extract_stack(stack):\n            fn = os.sep.join(fn.split(os.sep)[np:])\n            ret.append('File: \"%s\", line %d, in %s' % (fn, lno, name))\n            if line:\n                ret.append(\"  \" + str(line.strip()))\n                if \"self.not_empty.wait()\" in line:\n                    pad = \" \" * 4\n\n        if pad:\n            bret += [ret[0]] + [pad + x for x in ret[1:]]\n        else:\n            rret.extend(ret)\n\n    return \"\\n\".join(rret + bret) + \"\\n\"\n\n\ndef start_stackmon(arg_str: str, nid: int) -> None:\n    suffix = \"-{}\".format(nid) if nid else \"\"\n    fp, f = arg_str.rsplit(\",\", 1)\n    zi = int(f)\n    Daemon(stackmon, \"stackmon\" + suffix, (fp, zi, suffix))\n\n\ndef stackmon(fp: str, ival: float, suffix: str) -> None:\n    ctr = 0\n    fp0 = fp\n    while True:\n        ctr += 1\n        fp = fp0\n        time.sleep(ival)\n        st = \"{}, {}\\n{}\".format(ctr, time.time(), alltrace())\n        buf = st.encode(\"utf-8\", \"replace\")\n\n        if fp.endswith(\".gz\"):\n            # 2459b 2304b 2241b 2202b 2194b 2191b lv3..8\n            # 0.06s 0.08s 0.11s 0.13s 0.16s 0.19s\n            buf = gzip.compress(buf, compresslevel=6)\n\n        elif fp.endswith(\".xz\"):\n            import lzma\n\n            # 2276b 2216b 2200b 2192b 2168b lv0..4\n            # 0.04s 0.10s 0.22s 0.41s 0.70s\n            buf = lzma.compress(buf, preset=0)\n\n        if \"%\" in fp:\n            dt = datetime.now(UTC)\n            for fs in \"YmdHMS\":\n                fs = \"%\" + fs\n                if fs in fp:\n                    fp = fp.replace(fs, dt.strftime(fs))\n\n        if \"/\" in fp:\n            try:\n                os.makedirs(fp.rsplit(\"/\", 1)[0])\n            except:\n                pass\n\n        with open(fp + suffix, \"wb\") as f:\n            f.write(buf)\n\n\ndef start_log_thrs(\n    logger: Callable[[str, str, int], None], ival: float, nid: int\n) -> None:\n    ival = float(ival)\n    tname = lname = \"log-thrs\"\n    if nid:\n        tname = \"logthr-n{}-i{:x}\".format(nid, os.getpid())\n        lname = tname[3:]\n\n    Daemon(log_thrs, tname, (logger, ival, lname))\n\n\ndef log_thrs(log: Callable[[str, str, int], None], ival: float, name: str) -> None:\n    while True:\n        time.sleep(ival)\n        tv = [x.name for x in threading.enumerate()]\n        tv = [\n            x.split(\"-\")[0]\n            if x.split(\"-\")[0] in [\"httpconn\", \"thumb\", \"tagger\"]\n            else \"listen\"\n            if \"-listen-\" in x\n            else x\n            for x in tv\n            if not x.startswith(\"pydevd.\")\n        ]\n        tv = [\"{}\\033[36m{}\".format(v, k) for k, v in sorted(Counter(tv).items())]\n        log(name, \"\\033[0m \\033[33m\".join(tv), 3)\n\n\ndef _sigblock():\n    signal.pthread_sigmask(\n        signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM, signal.SIGUSR1]\n    )\n\n\nsigblock = _sigblock if CAN_SIGMASK else noop\n\n\ndef vol_san(vols: list[\"VFS\"], txt: bytes) -> bytes:\n    txt0 = txt\n    for vol in vols:\n        bap = vol.realpath.encode(\"utf-8\")\n        bhp = vol.histpath.encode(\"utf-8\")\n        bvp = vol.vpath.encode(\"utf-8\")\n        bvph = b\"$hist(/\" + bvp + b\")\"\n\n        if bap:\n            txt = txt.replace(bap, bvp)\n            txt = txt.replace(bap.replace(b\"\\\\\", b\"\\\\\\\\\"), bvp)\n        if bhp:\n            txt = txt.replace(bhp, bvph)\n            txt = txt.replace(bhp.replace(b\"\\\\\", b\"\\\\\\\\\"), bvph)\n\n        if vol.histpath != vol.dbpath:\n            bdp = vol.dbpath.encode(\"utf-8\")\n            bdph = b\"$db(/\" + bvp + b\")\"\n            txt = txt.replace(bdp, bdph)\n            txt = txt.replace(bdp.replace(b\"\\\\\", b\"\\\\\\\\\"), bdph)\n\n    if txt != txt0:\n        txt += b\"\\r\\nNOTE: filepaths sanitized; see serverlog for correct values\"\n\n    return txt\n\n\ndef min_ex(max_lines: int = 8, reverse: bool = False) -> str:\n    et, ev, tb = sys.exc_info()\n    stb = traceback.extract_tb(tb) if tb else traceback.extract_stack()[:-1]\n    fmt = \"%s:%d <%s>: %s\"\n    ex = [fmt % (fp.split(os.sep)[-1], ln, fun, txt) for fp, ln, fun, txt in stb]\n    if et or ev or tb:\n        ex.append(\"[%s] %s\" % (et.__name__ if et else \"(anonymous)\", ev))\n    return \"\\n\".join(ex[-max_lines:][:: -1 if reverse else 1])\n\n\ndef ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str]:\n    fun = kwargs.pop(\"fun\", open)\n    fdir = kwargs.pop(\"fdir\", None)\n    suffix = kwargs.pop(\"suffix\", None)\n    vf = kwargs.pop(\"vf\", None)\n    fperms = vf and \"fperms\" in vf\n\n    if fname == os.devnull:\n        return fun(fname, *args, **kwargs), fname\n\n    if suffix:\n        ext = fname.split(\".\")[-1]\n        if len(ext) < 7:\n            suffix += \".\" + ext\n\n    orig_name = fname\n    bname = fname\n    ext = \"\"\n    while True:\n        ofs = bname.rfind(\".\")\n        if ofs < 0 or ofs < len(bname) - 7:\n            # doesn't look like an extension anymore\n            break\n\n        ext = bname[ofs:] + ext\n        bname = bname[:ofs]\n\n    asciified = False\n    b64 = \"\"\n    while True:\n        f = None\n        try:\n            if fdir:\n                fpath = os.path.join(fdir, fname)\n            else:\n                fpath = fname\n\n            if suffix and os.path.lexists(fsenc(fpath)):\n                fpath += suffix\n                fname += suffix\n                ext += suffix\n\n            f = fun(fsenc(fpath), *args, **kwargs)\n            if b64:\n                assert fdir  # !rm\n                fp2 = \"fn-trunc.%s.txt\" % (b64,)\n                fp2 = os.path.join(fdir, fp2)\n                with open(fsenc(fp2), \"wb\") as f2:\n                    f2.write(orig_name.encode(\"utf-8\"))\n                    if fperms:\n                        set_fperms(f2, vf)\n\n            if fperms:\n                set_fperms(f, vf)\n\n            return f, fname\n\n        except OSError as ex_:\n            ex = ex_\n            if f:\n                f.close()\n\n            # EPERM: android13\n            if ex.errno in (errno.EINVAL, errno.EPERM) and not asciified:\n                asciified = True\n                zsl = []\n                for zs in (bname, fname):\n                    zs = zs.encode(\"ascii\", \"replace\").decode(\"ascii\")\n                    zs = re.sub(r\"[^][a-zA-Z0-9(){}.,+=!-]\", \"_\", zs)\n                    zsl.append(zs)\n                bname, fname = zsl\n                continue\n\n            # ENOTSUP: zfs on ubuntu 20.04\n            if ex.errno not in (errno.ENAMETOOLONG, errno.ENOSR, errno.ENOTSUP) and (\n                not WINDOWS or ex.errno != errno.EINVAL\n            ):\n                raise\n\n        if not b64:\n            zs = (\"%s\\n%s\" % (orig_name, suffix)).encode(\"utf-8\", \"replace\")\n            b64 = ub64enc(hashlib.sha512(zs).digest()[:12]).decode(\"ascii\")\n\n        badlen = len(fname)\n        while len(fname) >= badlen:\n            if len(bname) < 8:\n                raise ex\n\n            if len(bname) > len(ext):\n                # drop the last letter of the filename\n                bname = bname[:-1]\n            else:\n                try:\n                    # drop the leftmost sub-extension\n                    _, ext = ext.split(\".\", 1)\n                except:\n                    # okay do the first letter then\n                    ext = \".\" + ext[2:]\n\n            fname = \"%s~%s%s\" % (bname, b64, ext)\n\n\nclass MultipartParser(object):\n    def __init__(\n        self,\n        log_func: \"NamedLogger\",\n        args: argparse.Namespace,\n        sr: Unrecv,\n        http_headers: dict[str, str],\n    ):\n        self.sr = sr\n        self.log = log_func\n        self.args = args\n        self.headers = http_headers\n        try:\n            self.clen = int(http_headers[\"content-length\"])\n            sr.nb = 0\n        except:\n            self.clen = 0\n\n        self.re_ctype = RE_CTYPE\n        self.re_cdisp = RE_CDISP\n        self.re_cdisp_field = RE_CDISP_FIELD\n        self.re_cdisp_file = RE_CDISP_FILE\n\n        self.boundary = b\"\"\n        self.gen: Optional[\n            Generator[\n                tuple[str, Optional[str], Generator[bytes, None, None]], None, None\n            ]\n        ] = None\n\n    def _read_header(self) -> tuple[str, Optional[str]]:\n        \"\"\"\n        returns [fieldname, filename] after eating a block of multipart headers\n        while doing a decent job at dealing with the absolute mess that is\n        rfc1341/rfc1521/rfc2047/rfc2231/rfc2388/rfc6266/the-real-world\n        (only the fallback non-js uploader relies on these filenames)\n        \"\"\"\n        for ln in read_header(self.sr, 2, 2592000):\n            self.log(repr(ln))\n\n            m = self.re_ctype.match(ln)\n            if m:\n                if m.group(1).lower() == \"multipart/mixed\":\n                    # rfc-7578 overrides rfc-2388 so this is not-impl\n                    # (opera >=9 <11.10 is the only thing i've ever seen use it)\n                    raise Pebkac(\n                        400,\n                        \"you can't use that browser to upload multiple files at once\",\n                    )\n\n                continue\n\n            # the only other header we care about is content-disposition\n            m = self.re_cdisp.match(ln)\n            if not m:\n                continue\n\n            if m.group(1).lower() != \"form-data\":\n                raise Pebkac(400, \"not form-data: %r\" % (ln,))\n\n            try:\n                field = self.re_cdisp_field.match(ln).group(1)  # type: ignore\n            except:\n                raise Pebkac(400, \"missing field name: %r\" % (ln,))\n\n            try:\n                fn = self.re_cdisp_file.match(ln).group(1)  # type: ignore\n            except:\n                # this is not a file upload, we're done\n                return field, None\n\n            try:\n                is_webkit = \"applewebkit\" in self.headers[\"user-agent\"].lower()\n            except:\n                is_webkit = False\n\n            # chromes ignore the spec and makes this real easy\n            if is_webkit:\n                # quotes become %22 but they don't escape the %\n                # so unescaping the quotes could turn messi\n                return field, fn.split('\"')[0]\n\n            # also ez if filename doesn't contain \"\n            if not fn.split('\"')[0].endswith(\"\\\\\"):\n                return field, fn.split('\"')[0]\n\n            # this breaks on firefox uploads that contain \\\"\n            # since firefox escapes \" but forgets to escape \\\n            # so it'll truncate after the \\\n            ret = \"\"\n            esc = False\n            for ch in fn:\n                if esc:\n                    esc = False\n                    if ch not in ['\"', \"\\\\\"]:\n                        ret += \"\\\\\"\n                    ret += ch\n                elif ch == \"\\\\\":\n                    esc = True\n                elif ch == '\"':\n                    break\n                else:\n                    ret += ch\n\n            return field, ret\n\n        raise Pebkac(400, \"server expected a multipart header but you never sent one\")\n\n    def _read_data(self) -> Generator[bytes, None, None]:\n        blen = len(self.boundary)\n        bufsz = self.args.s_rd_sz\n        while True:\n            try:\n                buf = self.sr.recv(bufsz)\n            except:\n                # abort: client disconnected\n                raise Pebkac(400, \"client d/c during multipart post\")\n\n            while True:\n                ofs = buf.find(self.boundary)\n                if ofs != -1:\n                    self.sr.unrecv(buf[ofs + blen :])\n                    yield buf[:ofs]\n                    return\n\n                d = len(buf) - blen\n                if d > 0:\n                    # buffer growing large; yield everything except\n                    # the part at the end (maybe start of boundary)\n                    yield buf[:d]\n                    buf = buf[d:]\n\n                # look for boundary near the end of the buffer\n                n = 0\n                for n in range(1, len(buf) + 1):\n                    if not buf[-n:] in self.boundary:\n                        n -= 1\n                        break\n\n                if n == 0 or not self.boundary.startswith(buf[-n:]):\n                    # no boundary contents near the buffer edge\n                    break\n\n                if blen == n:\n                    # EOF: found boundary\n                    yield buf[:-n]\n                    return\n\n                try:\n                    buf += self.sr.recv(bufsz)\n                except:\n                    # abort: client disconnected\n                    raise Pebkac(400, \"client d/c during multipart post\")\n\n            yield buf\n\n    def _run_gen(\n        self,\n    ) -> Generator[tuple[str, Optional[str], Generator[bytes, None, None]], None, None]:\n        \"\"\"\n        yields [fieldname, unsanitized_filename, fieldvalue]\n        where fieldvalue yields chunks of data\n        \"\"\"\n        run = True\n        while run:\n            fieldname, filename = self._read_header()\n            yield (fieldname, filename, self._read_data())\n\n            tail = self.sr.recv_ex(2, False)\n\n            if tail == b\"--\":\n                # EOF indicated by this immediately after final boundary\n                if self.clen == self.sr.nb:\n                    tail = b\"\\r\\n\"  # dillo doesn't terminate with trailing \\r\\n\n                else:\n                    tail = self.sr.recv_ex(2, False)\n                run = False\n\n            if tail != b\"\\r\\n\":\n                t = \"protocol error after field value: want b'\\\\r\\\\n', got {!r}\"\n                raise Pebkac(400, t.format(tail))\n\n    def _read_value(self, iterable: Iterable[bytes], max_len: int) -> bytes:\n        ret = b\"\"\n        for buf in iterable:\n            ret += buf\n            if len(ret) > max_len:\n                raise Pebkac(422, \"field length is too long\")\n\n        return ret\n\n    def parse(self) -> None:\n        boundary = get_boundary(self.headers)\n        if boundary.startswith('\"') and boundary.endswith('\"'):\n            boundary = boundary[1:-1]  # dillo uses quotes\n        self.log(\"boundary=%r\" % (boundary,))\n\n        # spec says there might be junk before the first boundary,\n        # can't have the leading \\r\\n if that's not the case\n        self.boundary = b\"--\" + boundary.encode(\"utf-8\")\n\n        # discard junk before the first boundary\n        for junk in self._read_data():\n            if not junk:\n                continue\n\n            jtxt = junk.decode(\"utf-8\", \"replace\")\n            self.log(\"discarding preamble |%d| %r\" % (len(junk), jtxt))\n\n        # nice, now make it fast\n        self.boundary = b\"\\r\\n\" + self.boundary\n        self.gen = self._run_gen()\n\n    def require(self, field_name: str, max_len: int) -> str:\n        \"\"\"\n        returns the value of the next field in the multipart body,\n        raises if the field name is not as expected\n        \"\"\"\n        assert self.gen  # !rm\n        p_field, p_fname, p_data = next(self.gen)\n        if p_field != field_name:\n            raise WrongPostKey(field_name, p_field, p_fname, p_data)\n\n        return self._read_value(p_data, max_len).decode(\"utf-8\", \"surrogateescape\")\n\n    def drop(self) -> None:\n        \"\"\"discards the remaining multipart body\"\"\"\n        assert self.gen  # !rm\n        for _, _, data in self.gen:\n            for _ in data:\n                pass\n\n\ndef get_boundary(headers: dict[str, str]) -> str:\n    # boundaries contain a-z A-Z 0-9 ' ( ) + _ , - . / : = ?\n    # (whitespace allowed except as the last char)\n    ptn = r\"^multipart/form-data *; *(.*; *)?boundary=([^;]+)\"\n    ct = headers[\"content-type\"]\n    m = re.match(ptn, ct, re.IGNORECASE)\n    if not m:\n        raise Pebkac(400, \"invalid content-type for a multipart post: %r\" % (ct,))\n\n    return m.group(2)\n\n\ndef read_header(sr: Unrecv, t_idle: int, t_tot: int) -> list[str]:\n    t0 = time.time()\n    ret = b\"\"\n    while True:\n        if time.time() - t0 >= t_tot:\n            return []\n\n        try:\n            ret += sr.recv(1024, t_idle // 2)\n        except:\n            if not ret:\n                return []\n\n            raise Pebkac(\n                400,\n                \"protocol error while reading headers\",\n                log=ret.decode(\"utf-8\", \"replace\"),\n            )\n\n        ofs = ret.find(b\"\\r\\n\\r\\n\")\n        if ofs < 0:\n            if len(ret) > 1024 * 32:\n                raise Pebkac(400, \"header 2big\")\n            else:\n                continue\n\n        if len(ret) > ofs + 4:\n            sr.unrecv(ret[ofs + 4 :])\n\n        return ret[:ofs].decode(\"utf-8\", \"surrogateescape\").lstrip(\"\\r\\n\").split(\"\\r\\n\")\n\n\ndef rand_name(fdir: str, fn: str, rnd: int) -> str:\n    ok = False\n    try:\n        ext = \".\" + fn.rsplit(\".\", 1)[1]\n    except:\n        ext = \"\"\n\n    for extra in range(16):\n        for _ in range(16):\n            if ok:\n                break\n\n            nc = rnd + extra\n            nb = (6 + 6 * nc) // 8\n            zb = ub64enc(os.urandom(nb))\n            fn = zb[:nc].decode(\"ascii\") + ext\n            ok = not os.path.exists(fsenc(os.path.join(fdir, fn)))\n\n    return fn\n\n\ndef _gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:\n    if alg == 1:\n        zs = \"%s %s %s %s\" % (salt, fspath, fsize, inode)\n    else:\n        zs = \"%s %s\" % (salt, fspath)\n\n    zb = zs.encode(\"utf-8\", \"replace\")\n    return ub64enc(hashlib.sha512(zb).digest()).decode(\"ascii\")\n\n\ndef _gen_filekey_w(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:\n    return _gen_filekey(alg, salt, fspath.replace(\"/\", \"\\\\\"), fsize, inode)\n\n\ngen_filekey = _gen_filekey_w if ANYWIN else _gen_filekey\n\n\ndef gen_filekey_dbg(\n    alg: int,\n    salt: str,\n    fspath: str,\n    fsize: int,\n    inode: int,\n    log: \"NamedLogger\",\n    log_ptn: Optional[Pattern[str]],\n) -> str:\n    ret = gen_filekey(alg, salt, fspath, fsize, inode)\n\n    assert log_ptn  # !rm\n    if log_ptn.search(fspath):\n        try:\n            import inspect\n\n            ctx = \",\".join(inspect.stack()[n].function for n in range(2, 5))\n        except:\n            ctx = \"\"\n\n        p2 = \"a\"\n        try:\n            p2 = absreal(fspath)\n            if p2 != fspath:\n                raise Exception()\n        except:\n            t = \"maybe wrong abspath for filekey;\\norig: %r\\nreal: %r\"\n            log(t % (fspath, p2), 1)\n\n        t = \"fk(%s) salt(%s) size(%d) inode(%d) fspath(%r) at(%s)\"\n        log(t % (ret[:8], salt, fsize, inode, fspath, ctx), 5)\n\n    return ret\n\n\nWKDAYS = \"Mon Tue Wed Thu Fri Sat Sun\".split()\nMONTHS = \"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec\".split()\nRFC2822 = \"%s, %02d %s %04d %02d:%02d:%02d GMT\"\n\n\ndef formatdate(ts: Optional[float] = None) -> str:\n    # gmtime ~= datetime.fromtimestamp(ts, UTC).timetuple()\n    y, mo, d, h, mi, s, wd, _, _ = time.gmtime(ts)\n    return RFC2822 % (WKDAYS[wd], d, MONTHS[mo - 1], y, h, mi, s)\n\n\ndef gencookie(\n    k: str, v: str, r: str, lax: bool, tls: bool, dur: int = 0, txt: str = \"\"\n) -> str:\n    v = v.replace(\"%\", \"%25\").replace(\";\", \"%3B\")\n    if dur:\n        exp = formatdate(time.time() + dur)\n    else:\n        exp = \"Fri, 15 Aug 1997 01:00:00 GMT\"\n\n    t = \"%s=%s; Path=/%s; Expires=%s%s%s; SameSite=%s\"\n    return t % (\n        k,\n        v,\n        r,\n        exp,\n        \"; Secure\" if tls else \"\",\n        txt,\n        \"Lax\" if lax else \"Strict\",\n    )\n\n\ndef gen_content_disposition(fn: str) -> str:\n    safe = UC_CDISP_SET\n    bsafe = BC_CDISP_SET\n    fn = fn.replace(\"/\", \"_\").replace(\"\\\\\", \"_\")\n    zb = fn.encode(\"utf-8\", \"xmlcharrefreplace\")\n    if not PY2:\n        zbl = [\n            chr(x).encode(\"utf-8\")\n            if x in bsafe\n            else \"%{:02X}\".format(x).encode(\"ascii\")\n            for x in zb\n        ]\n    else:\n        zbl = [unicode(x) if x in bsafe else \"%{:02X}\".format(ord(x)) for x in zb]\n\n    ufn = b\"\".join(zbl).decode(\"ascii\")\n    afn = \"\".join([x if x in safe else \"_\" for x in fn]).lstrip(\".\")\n    while \"..\" in afn:\n        afn = afn.replace(\"..\", \".\")\n\n    return \"attachment; filename=\\\"%s\\\"; filename*=UTF-8''%s\" % (afn, ufn)\n\n\ndef humansize(sz: float, terse: bool = False) -> str:\n    for unit in HUMANSIZE_UNITS:\n        if sz < 1024:\n            break\n\n        sz /= 1024.0\n\n    assert unit  # type: ignore  # !rm\n    if terse:\n        return \"%s%s\" % (str(sz)[:4].rstrip(\".\"), unit[:1])\n    else:\n        return \"%s %s\" % (str(sz)[:4].rstrip(\".\"), unit)\n\n\ndef unhumanize(sz: str) -> int:\n    try:\n        return int(sz)\n    except:\n        pass\n\n    mc = sz[-1:].lower()\n    mi = UNHUMANIZE_UNITS.get(mc, 1)\n    return int(float(sz[:-1]) * mi)\n\n\ndef get_spd(nbyte: int, t0: float, t: Optional[float] = None) -> str:\n    if t is None:\n        t = time.time()\n\n    bps = nbyte / ((t - t0) or 0.001)\n    s1 = humansize(nbyte).replace(\" \", \"\\033[33m\").replace(\"iB\", \"\")\n    s2 = humansize(bps).replace(\" \", \"\\033[35m\").replace(\"iB\", \"\")\n    return \"%s \\033[0m%s/s\\033[0m\" % (s1, s2)\n\n\ndef s2hms(s: float, optional_h: bool = False) -> str:\n    s = int(s)\n    h, s = divmod(s, 3600)\n    m, s = divmod(s, 60)\n    if not h and optional_h:\n        return \"%d:%02d\" % (m, s)\n\n    return \"%d:%02d:%02d\" % (h, m, s)\n\n\ndef djoin(*paths: str) -> str:\n    \"\"\"joins without adding a trailing slash on blank args\"\"\"\n    return os.path.join(*[x for x in paths if x])\n\n\ndef uncyg(path: str) -> str:\n    if len(path) < 2 or not path.startswith(\"/\"):\n        return path\n\n    if len(path) > 2 and path[2] != \"/\":\n        return path\n\n    return \"%s:\\\\%s\" % (path[1], path[3:])\n\n\ndef undot(path: str) -> str:\n    ret: list[str] = []\n    for node in path.split(\"/\"):\n        if node == \".\" or not node:\n            continue\n\n        if node == \"..\":\n            if ret:\n                ret.pop()\n            continue\n\n        ret.append(node)\n\n    return \"/\".join(ret)\n\n\ndef sanitize_fn(fn: str) -> str:\n    fn = fn.replace(\"\\\\\", \"/\").split(\"/\")[-1]\n    if APTL_OS:\n        fn = sanitize_to(fn, APTL_OS)\n    return fn.strip()\n\n\ndef sanitize_to(fn: str, tl: dict[int, int]) -> str:\n    fn = fn.translate(tl)\n    if ANYWIN:\n        bad = [\"con\", \"prn\", \"aux\", \"nul\"]\n        for n in range(1, 10):\n            bad += (\"com%s lpt%s\" % (n, n)).split(\" \")\n\n        if fn.lower().split(\".\")[0] in bad:\n            fn = \"_\" + fn\n    return fn\n\n\ndef sanitize_vpath(vp: str) -> str:\n    if not APTL_OS:\n        return vp\n    parts = vp.replace(os.sep, \"/\").split(\"/\")\n    ret = [sanitize_to(x, APTL_OS) for x in parts]\n    return \"/\".join(ret)\n\n\ndef relchk(rp: str) -> str:\n    if \"\\x00\" in rp:\n        return \"[nul]\"\n\n    if ANYWIN:\n        if \"\\n\" in rp or \"\\r\" in rp:\n            return \"x\\nx\"\n\n        p = re.sub(r'[\\\\:*?\"<>|]', \"\", rp)\n        if p != rp:\n            return \"[{}]\".format(p)\n\n    return \"\"\n\n\ndef absreal(fpath: str) -> str:\n    try:\n        return fsdec(os.path.abspath(os.path.realpath(afsenc(fpath))))\n    except:\n        if not WINDOWS:\n            raise\n\n        # cpython bug introduced in 3.8, still exists in 3.9.1,\n        # some win7sp1 and win10:20H2 boxes cannot realpath a\n        # networked drive letter such as b\"n:\" or b\"n:\\\\\"\n        return os.path.abspath(os.path.realpath(fpath))\n\n\ndef u8safe(txt: str) -> str:\n    try:\n        return txt.encode(\"utf-8\", \"xmlcharrefreplace\").decode(\"utf-8\", \"replace\")\n    except:\n        return txt.encode(\"utf-8\", \"replace\").decode(\"utf-8\", \"replace\")\n\n\ndef exclude_dotfiles(filepaths: list[str]) -> list[str]:\n    return [x for x in filepaths if not x.split(\"/\")[-1].startswith(\".\")]\n\n\ndef exclude_dotfiles_ls(\n    vfs_ls: list[tuple[str, os.stat_result]]\n) -> list[tuple[str, os.stat_result]]:\n    return [x for x in vfs_ls if not x[0].split(\"/\")[-1].startswith(\".\")]\n\n\ndef odfusion(\n    base: Union[ODict[str, bool], ODict[\"LiteralString\", bool]], oth: str\n) -> ODict[str, bool]:\n    # merge an \"ordered set\" (just a dict really) with another list of keys\n    words0 = [x for x in oth.split(\",\") if x]\n    words1 = [x for x in oth[1:].split(\",\") if x]\n\n    ret = base.copy()\n    if oth.startswith(\"+\"):\n        for k in words1:\n            ret[k] = True  # type: ignore\n    elif oth[:1] in (\"-\", \"/\"):\n        for k in words1:\n            ret.pop(k, None)  # type: ignore\n    else:\n        ret = ODict.fromkeys(words0, True)\n\n    return ret  # type: ignore\n\n\ndef ipnorm(ip: str) -> str:\n    if \":\" in ip:\n        # assume /64 clients; drop 4 groups\n        return IPv6Address(ip).exploded[:-20]\n\n    return ip\n\n\ndef find_prefix(ips: list[str], cidrs: list[str]) -> list[str]:\n    ret = []\n    for ip in ips:\n        hit = next((x for x in cidrs if x.startswith(ip + \"/\") or ip == x), None)\n        if hit:\n            ret.append(hit)\n    return ret\n\n\ndef html_sh_esc(s: str) -> str:\n    s = re.sub(RE_HTML_SH, \"_\", s).replace(\" \", \"%20\")\n    s = s.replace(\"\\r\", \"_\").replace(\"\\n\", \"_\")\n    return s\n\n\ndef json_hesc(s: str) -> str:\n    return s.replace(\"<\", \"\\\\u003c\").replace(\">\", \"\\\\u003e\").replace(\"&\", \"\\\\u0026\")\n\n\ndef html_escape(s: str, quot: bool = False, crlf: bool = False) -> str:\n    \"\"\"html.escape but also newlines\"\"\"\n    s = s.replace(\"&\", \"&amp;\").replace(\"<\", \"&lt;\").replace(\">\", \"&gt;\")\n    if quot:\n        s = s.replace('\"', \"&quot;\").replace(\"'\", \"&#x27;\")\n    if crlf:\n        s = s.replace(\"\\r\", \"&#13;\").replace(\"\\n\", \"&#10;\")\n\n    return s\n\n\ndef html_bescape(s: bytes, quot: bool = False, crlf: bool = False) -> bytes:\n    \"\"\"html.escape but bytestrings\"\"\"\n    s = s.replace(b\"&\", b\"&amp;\").replace(b\"<\", b\"&lt;\").replace(b\">\", b\"&gt;\")\n    if quot:\n        s = s.replace(b'\"', b\"&quot;\").replace(b\"'\", b\"&#x27;\")\n    if crlf:\n        s = s.replace(b\"\\r\", b\"&#13;\").replace(b\"\\n\", b\"&#10;\")\n\n    return s\n\n\ndef _quotep2(txt: str) -> str:\n    \"\"\"url quoter which deals with bytes correctly\"\"\"\n    if not txt:\n        return \"\"\n    btxt = w8enc(txt)\n    quot = quote(btxt, safe=b\"/\")\n    return w8dec(quot.replace(b\" \", b\"+\"))  # type: ignore\n\n\ndef _quotep3(txt: str) -> str:\n    \"\"\"url quoter which deals with bytes correctly\"\"\"\n    if not txt:\n        return \"\"\n    btxt = w8enc(txt)\n    quot = quote(btxt, safe=b\"/\").encode(\"utf-8\")\n    return w8dec(quot.replace(b\" \", b\"+\"))\n\n\nif not PY2:\n    _uqsb = b\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-~/\"\n    _uqtl = {\n        n: (\"%%%02X\" % (n,) if n not in _uqsb else chr(n)).encode(\"utf-8\")\n        for n in range(256)\n    }\n    _uqtl[b\" \"] = b\"+\"\n\n    def _quotep3b(txt: str) -> str:\n        \"\"\"url quoter which deals with bytes correctly\"\"\"\n        if not txt:\n            return \"\"\n        btxt = w8enc(txt)\n        if btxt.rstrip(_uqsb):\n            lut = _uqtl\n            btxt = b\"\".join([lut[ch] for ch in btxt])\n        return w8dec(btxt)\n\n    quotep = _quotep3b\n\n    _hexd = \"0123456789ABCDEFabcdef\"\n    _hex2b = {(a + b).encode(): bytes.fromhex(a + b) for a in _hexd for b in _hexd}\n\n    def unquote(btxt: bytes) -> bytes:\n        h2b = _hex2b\n        parts = iter(btxt.split(b\"%\"))\n        ret = [next(parts)]\n        for item in parts:\n            c = h2b.get(item[:2])\n            if c is None:\n                ret.append(b\"%\")\n                ret.append(item)\n            else:\n                ret.append(c)\n                ret.append(item[2:])\n        return b\"\".join(ret)\n\n    from urllib.parse import quote_from_bytes as quote\nelse:\n    from urllib import quote  # type: ignore # pylint: disable=no-name-in-module\n    from urllib import unquote  # type: ignore # pylint: disable=no-name-in-module\n\n    quotep = _quotep2\n\n\ndef unquotep(txt: str) -> str:\n    \"\"\"url unquoter which deals with bytes correctly\"\"\"\n    btxt = w8enc(txt)\n    unq2 = unquote(btxt)\n    return w8dec(unq2)\n\n\ndef vroots(vp1: str, vp2: str) -> tuple[str, str]:\n    \"\"\"\n    input(\"q/w/e/r\",\"a/s/d/e/r\") output(\"/q/w/\",\"/a/s/d/\")\n    \"\"\"\n    while vp1 and vp2:\n        zt1 = vp1.rsplit(\"/\", 1) if \"/\" in vp1 else (\"\", vp1)\n        zt2 = vp2.rsplit(\"/\", 1) if \"/\" in vp2 else (\"\", vp2)\n        if zt1[1] != zt2[1]:\n            break\n        vp1 = zt1[0]\n        vp2 = zt2[0]\n    return (\n        \"/%s/\" % (vp1,) if vp1 else \"/\",\n        \"/%s/\" % (vp2,) if vp2 else \"/\",\n    )\n\n\ndef vsplit(vpath: str) -> tuple[str, str]:\n    if \"/\" not in vpath:\n        return \"\", vpath\n\n    return vpath.rsplit(\"/\", 1)  # type: ignore\n\n\n# vpath-join\ndef vjoin(rd: str, fn: str) -> str:\n    if rd and fn:\n        return rd + \"/\" + fn\n    else:\n        return rd or fn\n\n\n# url-join\ndef ujoin(rd: str, fn: str) -> str:\n    if rd and fn:\n        return rd.rstrip(\"/\") + \"/\" + fn.lstrip(\"/\")\n    else:\n        return rd or fn\n\n\ndef str_anchor(txt) -> tuple[int, str]:\n    if not txt:\n        return 0, \"\"\n    txt = txt.lower()\n    a = txt.startswith(\"^\")\n    b = txt.endswith(\"$\")\n    if not b:\n        if not a:\n            return 1, txt  # ~\n        return 2, txt[1:]  # ^\n    if not a:\n        return 3, txt[:-1]  # $\n    return 4, txt[1:-1]  # ^$\n\n\ndef log_reloc(\n    log: \"NamedLogger\",\n    re: dict[str, str],\n    pm: tuple[str, str, str, tuple[\"VFS\", str]],\n    ap: str,\n    vp: str,\n    fn: str,\n    vn: \"VFS\",\n    rem: str,\n) -> None:\n    nap, nvp, nfn, (nvn, nrem) = pm\n    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\"\n    log(t % (re, ap, nap, nfn, vp, nvp, nfn, fn, nfn, vn.vpath, nvn.vpath, rem, nrem))\n\n\ndef pathmod(\n    vfs: \"VFS\", ap: str, vp: str, mod: dict[str, str]\n) -> Optional[tuple[str, str, str, tuple[\"VFS\", str]]]:\n    # vfs: authsrv.vfs\n    # ap: original abspath to a file\n    # vp: original urlpath to a file\n    # mod: modification (ap/vp/fn)\n\n    nvp = \"\\n\"  # new vpath\n    ap = os.path.dirname(ap)\n    vp, fn = vsplit(vp)\n    if mod.get(\"fn\"):\n        fn = mod[\"fn\"]\n        nvp = vp\n\n    for ref, k in ((ap, \"ap\"), (vp, \"vp\")):\n        if k not in mod:\n            continue\n\n        ms = mod[k].replace(os.sep, \"/\")\n        if ms.startswith(\"/\"):\n            np = ms\n        elif k == \"vp\":\n            np = undot(vjoin(ref, ms))\n        else:\n            np = os.path.abspath(os.path.join(ref, ms))\n\n        if k == \"vp\":\n            nvp = np.lstrip(\"/\")\n            continue\n\n        # try to map abspath to vpath\n        np = np.replace(\"/\", os.sep)\n        for vn_ap, vns in vfs.all_aps:\n            if not np.startswith(vn_ap):\n                continue\n            zs = np[len(vn_ap) :].replace(os.sep, \"/\")\n            nvp = vjoin(vns[0].vpath, zs)\n            break\n\n    if nvp == \"\\n\":\n        return None\n\n    vn, rem = vfs.get(nvp, \"*\", False, False)\n    if not vn.realpath:\n        raise Exception(\"unmapped vfs\")\n\n    ap = vn.canonical(rem)\n    return ap, nvp, fn, (vn, rem)\n\n\ndef _w8dec2(txt: bytes) -> str:\n    \"\"\"decodes filesystem-bytes to wtf8\"\"\"\n    return surrogateescape.decodefilename(txt)  # type: ignore\n\n\ndef _w8enc2(txt: str) -> bytes:\n    \"\"\"encodes wtf8 to filesystem-bytes\"\"\"\n    return surrogateescape.encodefilename(txt)  # type: ignore\n\n\ndef _w8dec3(txt: bytes) -> str:\n    \"\"\"decodes filesystem-bytes to wtf8\"\"\"\n    return txt.decode(FS_ENCODING, \"surrogateescape\")\n\n\ndef _w8enc3(txt: str) -> bytes:\n    \"\"\"encodes wtf8 to filesystem-bytes\"\"\"\n    return txt.encode(FS_ENCODING, \"surrogateescape\")\n\n\ndef _msdec(txt: bytes) -> str:\n    ret = txt.decode(FS_ENCODING, \"surrogateescape\")\n    return ret[4:] if ret.startswith(\"\\\\\\\\?\\\\\") else ret\n\n\ndef _msaenc(txt: str) -> bytes:\n    return txt.replace(\"/\", \"\\\\\").encode(FS_ENCODING, \"surrogateescape\")\n\n\ndef _uncify(txt: str) -> str:\n    txt = txt.replace(\"/\", \"\\\\\")\n    if \":\" not in txt and not txt.startswith(\"\\\\\\\\\"):\n        txt = absreal(txt)\n\n    return txt if txt.startswith(\"\\\\\\\\\") else \"\\\\\\\\?\\\\\" + txt\n\n\ndef _msenc(txt: str) -> bytes:\n    txt = txt.replace(\"/\", \"\\\\\")\n    if \":\" not in txt and not txt.startswith(\"\\\\\\\\\"):\n        txt = absreal(txt)\n\n    ret = txt.encode(FS_ENCODING, \"surrogateescape\")\n    return ret if ret.startswith(b\"\\\\\\\\\") else b\"\\\\\\\\?\\\\\" + ret\n\n\nw8dec = _w8dec3 if not PY2 else _w8dec2\nw8enc = _w8enc3 if not PY2 else _w8enc2\n\n\ndef w8b64dec(txt: str) -> str:\n    \"\"\"decodes base64(filesystem-bytes) to wtf8\"\"\"\n    return w8dec(ub64dec(txt.encode(\"ascii\")))\n\n\ndef w8b64enc(txt: str) -> str:\n    \"\"\"encodes wtf8 to base64(filesystem-bytes)\"\"\"\n    return ub64enc(w8enc(txt)).decode(\"ascii\")\n\n\nif not PY2 and WINDOWS:\n    sfsenc = w8enc\n    afsenc = _msaenc\n    fsenc = _msenc\n    fsdec = _msdec\n    uncify = _uncify\nelif not PY2 or not WINDOWS:\n    fsenc = afsenc = sfsenc = w8enc\n    fsdec = w8dec\n    uncify = str\nelse:\n    # moonrunes become \\x3f with bytestrings,\n    # losing mojibake support is worth\n    def _not_actually_mbcs_enc(txt: str) -> bytes:\n        return txt  # type: ignore\n\n    def _not_actually_mbcs_dec(txt: bytes) -> str:\n        return txt  # type: ignore\n\n    fsenc = afsenc = sfsenc = _not_actually_mbcs_enc\n    fsdec = _not_actually_mbcs_dec\n    uncify = str\n\n\ndef s3enc(mem_cur: \"sqlite3.Cursor\", rd: str, fn: str) -> tuple[str, str]:\n    ret: list[str] = []\n    for v in [rd, fn]:\n        try:\n            mem_cur.execute(\"select * from a where b = ?\", (v,))\n            ret.append(v)\n        except:\n            ret.append(\"//\" + w8b64enc(v))\n            # self.log(\"mojien [{}] {}\".format(v, ret[-1][2:]))\n\n    return ret[0], ret[1]\n\n\ndef s3dec(rd: str, fn: str) -> tuple[str, str]:\n    return (\n        w8b64dec(rd[2:]) if rd.startswith(\"//\") else rd,\n        w8b64dec(fn[2:]) if fn.startswith(\"//\") else fn,\n    )\n\n\ndef db_ex_chk(log: \"NamedLogger\", ex: Exception, db_path: str) -> bool:\n    if str(ex) != \"database is locked\":\n        return False\n\n    Daemon(lsof, \"dbex\", (log, db_path))\n    return True\n\n\ndef lsof(log: \"NamedLogger\", abspath: str) -> None:\n    try:\n        rc, so, se = runcmd([b\"lsof\", b\"-R\", fsenc(abspath)], timeout=45)\n        zs = (so.strip() + \"\\n\" + se.strip()).strip()\n        log(\"lsof %r = %s\\n%s\" % (abspath, rc, zs), 3)\n    except:\n        log(\"lsof failed; \" + min_ex(), 3)\n\n\ndef set_fperms(f: Union[typing.BinaryIO, typing.IO[Any]], vf: dict[str, Any]) -> None:\n    fno = f.fileno()\n    if \"chmod_f\" in vf:\n        os.fchmod(fno, vf[\"chmod_f\"])\n    if \"chown\" in vf:\n        os.fchown(fno, vf[\"uid\"], vf[\"gid\"])\n\n\ndef set_ap_perms(ap: str, vf: dict[str, Any]) -> None:\n    zb = fsenc(ap)\n    if \"chmod_f\" in vf:\n        os.chmod(zb, vf[\"chmod_f\"])\n    if \"chown\" in vf:\n        os.chown(zb, vf[\"uid\"], vf[\"gid\"])\n\n\ndef trystat_shutil_copy2(log: \"NamedLogger\", src: bytes, dst: bytes) -> bytes:\n    try:\n        return shutil.copy2(src, dst)\n    except:\n        # ignore failed mtime on linux+ntfs; for example:\n        # shutil.py:437 <copy2>: copystat(src, dst, follow_symlinks=follow_symlinks)\n        # shutil.py:376 <copystat>: lookup(\"utime\")(dst, ns=(st.st_atime_ns, st.st_mtime_ns),\n        # [PermissionError] [Errno 1] Operation not permitted, '/windows/_videos'\n        _, _, tb = sys.exc_info()\n        for _, _, fun, _ in traceback.extract_tb(tb):\n            if fun == \"copystat\":\n                if log:\n                    t = \"warning: failed to retain some file attributes (timestamp and/or permissions) during copy from %r to %r:\\n%s\"\n                    log(t % (src, dst, min_ex()), 3)\n                return dst  # close enough\n        raise\n\n\ndef _fs_mvrm(\n    log: \"NamedLogger\", src: str, dst: str, atomic: bool, flags: dict[str, Any]\n) -> bool:\n    bsrc = fsenc(src)\n    bdst = fsenc(dst)\n    if atomic:\n        k = \"mv_re_\"\n        act = \"atomic-rename\"\n        osfun = os.replace\n        args = [bsrc, bdst]\n    elif dst:\n        k = \"mv_re_\"\n        act = \"rename\"\n        osfun = os.rename\n        args = [bsrc, bdst]\n    else:\n        k = \"rm_re_\"\n        act = \"delete\"\n        osfun = os.unlink\n        args = [bsrc]\n\n    maxtime = flags.get(k + \"t\", 0.0)\n    chill = flags.get(k + \"r\", 0.0)\n    if chill < 0.001:\n        chill = 0.1\n\n    ino = 0\n    t0 = now = time.time()\n    for attempt in range(90210):\n        try:\n            if ino and os.stat(bsrc).st_ino != ino:\n                t = \"src inode changed; aborting %s %r\"\n                log(t % (act, src), 1)\n                return False\n            if (dst and not atomic) and os.path.exists(bdst):\n                t = \"something appeared at dst; aborting rename %r ==> %r\"\n                log(t % (src, dst), 1)\n                return False\n            osfun(*args)  # type: ignore\n            if attempt:\n                now = time.time()\n                t = \"%sd in %.2f sec, attempt %d: %r\"\n                log(t % (act, now - t0, attempt + 1, src))\n            return True\n        except OSError as ex:\n            now = time.time()\n            if ex.errno == errno.ENOENT:\n                return False\n            if not attempt and ex.errno == errno.EXDEV:\n                t = \"using copy+delete (%s)\\n  %s\\n  %s\"\n                log(t % (ex.strerror, src, dst))\n                osfun = shutil.move\n                continue\n            if now - t0 > maxtime or attempt == 90209:\n                raise\n            if not attempt:\n                if not PY2:\n                    ino = os.stat(bsrc).st_ino\n                t = \"%s failed (err.%d); retrying for %d sec: %r\"\n                log(t % (act, ex.errno, maxtime + 0.99, src))\n\n        time.sleep(chill)\n\n    return False  # makes pylance happy\n\n\ndef atomic_move(log: \"NamedLogger\", src: str, dst: str, flags: dict[str, Any]) -> None:\n    bsrc = fsenc(src)\n    bdst = fsenc(dst)\n    if PY2:\n        if os.path.exists(bdst):\n            _fs_mvrm(log, dst, \"\", False, flags)  # unlink\n\n        _fs_mvrm(log, src, dst, False, flags)  # rename\n    elif flags.get(\"mv_re_t\"):\n        _fs_mvrm(log, src, dst, True, flags)\n    else:\n        try:\n            os.replace(bsrc, bdst)\n        except OSError as ex:\n            if ex.errno != errno.EXDEV:\n                raise\n            t = \"using copy+delete (%s);\\n  %s\\n  %s\"\n            log(t % (ex.strerror, src, dst))\n            try:\n                os.unlink(bdst)\n            except:\n                pass\n            shutil.move(bsrc, bdst)  # type: ignore\n\n\ndef wunlink(log: \"NamedLogger\", abspath: str, flags: dict[str, Any]) -> bool:\n    if not flags.get(\"rm_re_t\"):\n        os.unlink(fsenc(abspath))\n        return True\n\n    return _fs_mvrm(log, abspath, \"\", False, flags)\n\n\ndef get_df(abspath: str, prune: bool) -> tuple[int, int, str]:\n    try:\n        ap = fsenc(abspath)\n        while prune and not os.path.isdir(ap) and BOS_SEP in ap:\n            # strip leafs until it hits an existing folder\n            ap = ap.rsplit(BOS_SEP, 1)[0]\n\n        if ANYWIN:\n            assert ctypes  # type: ignore  # !rm\n            abspath = fsdec(ap)\n            bfree = ctypes.c_ulonglong(0)\n            btotal = ctypes.c_ulonglong(0)\n            bavail = ctypes.c_ulonglong(0)\n            wk32.GetDiskFreeSpaceExW(  # type: ignore\n                ctypes.c_wchar_p(abspath),\n                ctypes.pointer(bavail),\n                ctypes.pointer(btotal),\n                ctypes.pointer(bfree),\n            )\n            return (bavail.value, btotal.value, \"\")\n        else:\n            sv = os.statvfs(ap)\n            free = sv.f_frsize * sv.f_bavail\n            total = sv.f_frsize * sv.f_blocks\n            return (free, total, \"\")\n    except Exception as ex:\n        return (0, 0, repr(ex))\n\n\nif not ANYWIN and not MACOS:\n\n    def siocoutq(sck: socket.socket) -> int:\n        assert fcntl  # type: ignore  # !rm\n        assert termios  # type: ignore  # !rm\n        # SIOCOUTQ^sockios.h == TIOCOUTQ^ioctl.h\n        try:\n            zb = fcntl.ioctl(sck.fileno(), termios.TIOCOUTQ, b\"AAAA\")\n            return sunpack(b\"I\", zb)[0]  # type: ignore\n        except:\n            return 1\n\nelse:\n    # macos: getsockopt(fd, SOL_SOCKET, SO_NWRITE, ...)\n    # windows: TcpConnectionEstatsSendBuff\n\n    def siocoutq(sck: socket.socket) -> int:\n        return 1\n\n\ndef shut_socket(log: \"NamedLogger\", sck: socket.socket, timeout: int = 3) -> None:\n    t0 = time.time()\n    fd = sck.fileno()\n    if fd == -1:\n        sck.close()\n        return\n\n    try:\n        sck.settimeout(timeout)\n        sck.shutdown(socket.SHUT_WR)\n        try:\n            while time.time() - t0 < timeout:\n                if not siocoutq(sck):\n                    # kernel says tx queue empty, we good\n                    break\n\n                # on windows in particular, drain rx until client shuts\n                if not sck.recv(32 * 1024):\n                    break\n\n            sck.shutdown(socket.SHUT_RDWR)\n        except:\n            pass\n    except OSError as ex:\n        log(\"shut(%d): ok; client has already disconnected; %s\" % (fd, ex.errno), \"90\")\n    except Exception as ex:\n        log(\"shut({}): {}\".format(fd, ex), \"90\")\n    finally:\n        td = time.time() - t0\n        if td >= 1:\n            log(\"shut({}) in {:.3f} sec\".format(fd, td), \"90\")\n\n        sck.close()\n\n\ndef read_socket(\n    sr: Unrecv, bufsz: int, total_size: int\n) -> Generator[bytes, None, None]:\n    remains = total_size\n    while remains > 0:\n        if bufsz > remains:\n            bufsz = remains\n\n        try:\n            buf = sr.recv(bufsz)\n        except OSError:\n            t = \"client d/c during binary post after {} bytes, {} bytes remaining\"\n            raise Pebkac(400, t.format(total_size - remains, remains))\n\n        remains -= len(buf)\n        yield buf\n\n\ndef read_socket_unbounded(sr: Unrecv, bufsz: int) -> Generator[bytes, None, None]:\n    try:\n        while True:\n            yield sr.recv(bufsz)\n    except:\n        return\n\n\ndef read_socket_chunked(\n    sr: Unrecv, bufsz: int, log: Optional[\"NamedLogger\"] = None\n) -> Generator[bytes, None, None]:\n    err = \"upload aborted: expected chunk length, got [{}] |{}| instead\"\n    while True:\n        buf = b\"\"\n        while b\"\\r\" not in buf:\n            try:\n                buf += sr.recv(2)\n                if len(buf) > 16:\n                    raise Exception()\n            except:\n                err = err.format(buf.decode(\"utf-8\", \"replace\"), len(buf))\n                raise Pebkac(400, err)\n\n        if not buf.endswith(b\"\\n\"):\n            sr.recv(1)\n\n        try:\n            chunklen = int(buf.rstrip(b\"\\r\\n\"), 16)\n        except:\n            err = err.format(buf.decode(\"utf-8\", \"replace\"), len(buf))\n            raise Pebkac(400, err)\n\n        if chunklen == 0:\n            x = sr.recv_ex(2, False)\n            if x == b\"\\r\\n\":\n                sr.te = 2\n                return\n\n            t = \"protocol error after final chunk: want b'\\\\r\\\\n', got {!r}\"\n            raise Pebkac(400, t.format(x))\n\n        if log:\n            log(\"receiving %d byte chunk\" % (chunklen,))\n\n        for chunk in read_socket(sr, bufsz, chunklen):\n            yield chunk\n\n        x = sr.recv_ex(2, False)\n        if x != b\"\\r\\n\":\n            t = \"protocol error in chunk separator: want b'\\\\r\\\\n', got {!r}\"\n            raise Pebkac(400, t.format(x))\n\n\ndef list_ips() -> list[str]:\n    ret: set[str] = set()\n    for nic in get_adapters():\n        for ipo in nic.ips:\n            if len(ipo.ip) < 7:\n                ret.add(ipo.ip[0])  # ipv6 is (ip,0,0)\n            else:\n                ret.add(ipo.ip)\n\n    return list(ret)\n\n\ndef build_netmap(csv: str, defer_mutex: bool = False):\n    csv = csv.lower().strip()\n\n    if csv in (\"any\", \"all\", \"no\", \",\", \"\"):\n        return None\n\n    srcs = [x.strip() for x in csv.split(\",\") if x.strip()]\n\n    expanded_shorthands = False\n    for shorthand in (\"lan\", \"local\", \"private\", \"prvt\"):\n        if shorthand in srcs:\n            if not expanded_shorthands:\n                srcs += [\n                    # lan:\n                    \"10.0.0.0/8\",\n                    \"172.16.0.0/12\",\n                    \"192.168.0.0/16\",\n                    \"fd00::/8\",\n                    # link-local:\n                    \"169.254.0.0/16\",\n                    \"fe80::/10\",\n                    # loopback:\n                    \"127.0.0.0/8\",\n                    \"::1/128\",\n                ]\n                expanded_shorthands = True\n\n            srcs.remove(shorthand)\n\n    if not HAVE_IPV6:\n        srcs = [x for x in srcs if \":\" not in x]\n\n    cidrs = []\n    for zs in srcs:\n        if not zs.endswith(\".\"):\n            cidrs.append(zs)\n            continue\n\n        # translate old syntax \"172.19.\" => \"172.19.0.0/16\"\n        words = len(zs.rstrip(\".\").split(\".\"))\n        if words == 1:\n            zs += \"0.0.0/8\"\n        elif words == 2:\n            zs += \"0.0/16\"\n        elif words == 3:\n            zs += \"0/24\"\n        else:\n            raise Exception(\"invalid config value [%s]\" % (zs,))\n\n        cidrs.append(zs)\n\n    ips = [x.split(\"/\")[0] for x in cidrs]\n    return NetMap(ips, cidrs, True, False, defer_mutex)\n\n\ndef load_ipu(\n    log: \"RootLogger\", ipus: list[str], defer_mutex: bool = False\n) -> tuple[dict[str, str], NetMap]:\n    ip_u = {\"\": \"*\"}\n    cidr_u = {}\n    for ipu in ipus:\n        try:\n            cidr, uname = ipu.split(\"=\")\n            cip, csz = cidr.split(\"/\")\n        except:\n            t = \"\\n  invalid value %r for argument --ipu; must be CIDR=UNAME (192.168.0.0/16=amelia)\"\n            raise Exception(t % (ipu,))\n        uname2 = cidr_u.get(cidr)\n        if uname2 is not None:\n            t = \"\\n  invalid value %r for argument --ipu; cidr %s already mapped to %r\"\n            raise Exception(t % (ipu, cidr, uname2))\n        cidr_u[cidr] = uname\n        ip_u[cip] = uname\n    try:\n        nm = NetMap([\"::\"], list(cidr_u.keys()), True, True, defer_mutex)\n    except Exception as ex:\n        t = \"failed to translate --ipu into netmap, probably due to invalid config: %r\"\n        log(\"root\", t % (ex,), 1)\n        raise\n    return ip_u, nm\n\n\ndef load_ipr(\n    log: \"RootLogger\", iprs: list[str], defer_mutex: bool = False\n) -> dict[str, NetMap]:\n    ret = {}\n    for ipr in iprs:\n        try:\n            zs, uname = ipr.split(\"=\")\n            cidrs = zs.split(\",\")\n        except:\n            t = \"\\n  invalid value %r for argument --ipr; must be CIDR[,CIDR[,...]]=UNAME (192.168.0.0/16=amelia)\"\n            raise Exception(t % (ipr,))\n        try:\n            nm = NetMap([\"::\"], cidrs, True, True, defer_mutex)\n        except Exception as ex:\n            t = \"failed to translate --ipr into netmap, probably due to invalid config: %r\"\n            log(\"root\", t % (ex,), 1)\n            raise\n        ret[uname] = nm\n    return ret\n\n\ndef yieldfile(fn: str, bufsz: int) -> Generator[bytes, None, None]:\n    readsz = min(bufsz, 128 * 1024)\n    with open(fsenc(fn), \"rb\", bufsz) as f:\n        while True:\n            buf = f.read(readsz)\n            if not buf:\n                break\n\n            yield buf\n\n\ndef justcopy(\n    fin: Generator[bytes, None, None],\n    fout: Union[typing.BinaryIO, typing.IO[Any]],\n    hashobj: Optional[\"hashlib._Hash\"],\n    max_sz: int,\n    slp: float,\n) -> tuple[int, str, str]:\n    tlen = 0\n    for buf in fin:\n        tlen += len(buf)\n        if max_sz and tlen > max_sz:\n            continue\n\n        fout.write(buf)\n        if slp:\n            time.sleep(slp)\n\n    return tlen, \"checksum-disabled\", \"checksum-disabled\"\n\n\ndef eol_conv(\n    fin: Generator[bytes, None, None], conv: str\n) -> Generator[bytes, None, None]:\n    crlf = conv.lower() == \"crlf\"\n    for buf in fin:\n        buf = buf.replace(b\"\\r\", b\"\")\n        if crlf:\n            buf = buf.replace(b\"\\n\", b\"\\r\\n\")\n        yield buf\n\n\ndef hashcopy(\n    fin: Generator[bytes, None, None],\n    fout: Union[typing.BinaryIO, typing.IO[Any]],\n    hashobj: Optional[\"hashlib._Hash\"],\n    max_sz: int,\n    slp: float,\n) -> tuple[int, str, str]:\n    if not hashobj:\n        hashobj = hashlib.sha512()\n    tlen = 0\n    for buf in fin:\n        tlen += len(buf)\n        if max_sz and tlen > max_sz:\n            continue\n\n        hashobj.update(buf)\n        fout.write(buf)\n        if slp:\n            time.sleep(slp)\n\n    digest_b64 = ub64enc(hashobj.digest()[:33]).decode(\"ascii\")\n\n    return tlen, hashobj.hexdigest(), digest_b64\n\n\ndef sendfile_py(\n    log: \"NamedLogger\",\n    lower: int,\n    upper: int,\n    f: typing.BinaryIO,\n    s: socket.socket,\n    bufsz: int,\n    slp: float,\n    use_poll: bool,\n    dls: dict[str, tuple[float, int]],\n    dl_id: str,\n) -> int:\n    sent = 0\n    remains = upper - lower\n    f.seek(lower)\n    while remains > 0:\n        if slp:\n            time.sleep(slp)\n\n        buf = f.read(min(bufsz, remains))\n        if not buf:\n            return remains\n\n        try:\n            s.sendall(buf)\n            remains -= len(buf)\n        except:\n            return remains\n\n        if dl_id:\n            sent += len(buf)\n            dls[dl_id] = (time.time(), sent)\n\n    return 0\n\n\ndef sendfile_kern(\n    log: \"NamedLogger\",\n    lower: int,\n    upper: int,\n    f: typing.BinaryIO,\n    s: socket.socket,\n    bufsz: int,\n    slp: float,\n    use_poll: bool,\n    dls: dict[str, tuple[float, int]],\n    dl_id: str,\n) -> int:\n    out_fd = s.fileno()\n    in_fd = f.fileno()\n    ofs = lower\n    stuck = 0.0\n    if use_poll:\n        poll = select.poll()\n        poll.register(out_fd, select.POLLOUT)\n\n    while ofs < upper:\n        stuck = stuck or time.time()\n        try:\n            req = min(0x2000000, upper - ofs)  # 32 MiB\n            if use_poll:\n                poll.poll(10000)  # type: ignore\n            else:\n                select.select([], [out_fd], [], 10)\n            n = os.sendfile(out_fd, in_fd, ofs, req)\n            stuck = 0\n        except OSError as ex:\n            # client stopped reading; do another select\n            d = time.time() - stuck\n            if d < 3600 and ex.errno == errno.EWOULDBLOCK:\n                time.sleep(0.02)\n                continue\n\n            n = 0\n        except Exception as ex:\n            n = 0\n            d = time.time() - stuck\n            log(\"sendfile failed after {:.3f} sec: {!r}\".format(d, ex))\n\n        if n <= 0:\n            return upper - ofs\n\n        ofs += n\n        if dl_id:\n            dls[dl_id] = (time.time(), ofs - lower)\n\n        # print(\"sendfile: ok, sent {} now, {} total, {} remains\".format(n, ofs - lower, upper - ofs))\n\n    return 0\n\n\ndef statdir(\n    logger: Optional[\"RootLogger\"], scandir: bool, lstat: bool, top: str, throw: bool\n) -> Generator[tuple[str, os.stat_result], None, None]:\n    if lstat and ANYWIN:\n        lstat = False\n\n    if lstat and (PY2 or os.stat not in os.supports_follow_symlinks):\n        scandir = False\n\n    src = \"statdir\"\n    try:\n        btop = fsenc(top)\n        if scandir and hasattr(os, \"scandir\"):\n            src = \"scandir\"\n            with os.scandir(btop) as dh:\n                for fh in dh:\n                    try:\n                        yield (fsdec(fh.name), fh.stat(follow_symlinks=not lstat))\n                    except Exception as ex:\n                        if not logger:\n                            continue\n\n                        logger(src, \"[s] {} @ {}\".format(repr(ex), fsdec(fh.path)), 6)\n        else:\n            src = \"listdir\"\n            fun: Any = os.lstat if lstat else os.stat\n            btop_ = os.path.join(btop, b\"\")\n            for name in os.listdir(btop):\n                abspath = btop_ + name\n                try:\n                    yield (fsdec(name), fun(abspath))\n                except Exception as ex:\n                    if not logger:\n                        continue\n\n                    logger(src, \"[s] {} @ {}\".format(repr(ex), fsdec(abspath)), 6)\n\n    except Exception as ex:\n        if throw:\n            zi = getattr(ex, \"errno\", 0)\n            if zi == errno.ENOENT:\n                raise Pebkac(404, str(ex))\n            raise\n\n        t = \"{} @ {}\".format(repr(ex), top)\n        if logger:\n            logger(src, t, 1)\n        else:\n            print(t)\n\n\ndef dir_is_empty(logger: \"RootLogger\", scandir: bool, top: str):\n    for _ in statdir(logger, scandir, False, top, False):\n        return False\n    return True\n\n\ndef rmdirs(\n    logger: \"RootLogger\", scandir: bool, lstat: bool, top: str, depth: int\n) -> tuple[list[str], list[str]]:\n    \"\"\"rmdir all descendants, then self\"\"\"\n    if not os.path.isdir(fsenc(top)):\n        top = os.path.dirname(top)\n        depth -= 1\n\n    stats = statdir(logger, scandir, lstat, top, False)\n    dirs = [x[0] for x in stats if stat.S_ISDIR(x[1].st_mode)]\n    if dirs:\n        top_ = os.path.join(top, \"\")\n        dirs = [top_ + x for x in dirs]\n    ok = []\n    ng = []\n    for d in reversed(dirs):\n        a, b = rmdirs(logger, scandir, lstat, d, depth + 1)\n        ok += a\n        ng += b\n\n    if depth:\n        try:\n            os.rmdir(fsenc(top))\n            ok.append(top)\n        except:\n            ng.append(top)\n\n    return ok, ng\n\n\ndef rmdirs_up(top: str, stop: str) -> tuple[list[str], list[str]]:\n    \"\"\"rmdir on self, then all parents\"\"\"\n    if top == stop:\n        return [], [top]\n\n    try:\n        os.rmdir(fsenc(top))\n    except:\n        return [], [top]\n\n    par = os.path.dirname(top)\n    if not par or par == stop:\n        return [top], []\n\n    ok, ng = rmdirs_up(par, stop)\n    return [top] + ok, ng\n\n\ndef unescape_cookie(orig: str, name: str) -> str:\n    # mw=idk; doot=qwe%2Crty%3Basd+fgh%2Bjkl%25zxc%26vbn  # qwe,rty;asd fgh+jkl%zxc&vbn\n    if not name.startswith(\"cppw\"):\n        orig = orig[:3]\n    ret = []\n    esc = \"\"\n    for ch in orig:\n        if ch == \"%\":\n            if esc:\n                ret.append(esc)\n            esc = ch\n\n        elif esc:\n            esc += ch\n            if len(esc) == 3:\n                try:\n                    ret.append(chr(int(esc[1:], 16)))\n                except:\n                    ret.append(esc)\n                esc = \"\"\n\n        else:\n            ret.append(ch)\n\n    if esc:\n        ret.append(esc)\n\n    return \"\".join(ret)\n\n\ndef guess_mime(\n    url: str, path: str = \"\", fallback: str = \"application/octet-stream\"\n) -> str:\n    try:\n        ext = url.rsplit(\".\", 1)[1].lower()\n    except:\n        ext = \"\"\n\n    ret = MIMES.get(ext)\n\n    if not ret:\n        x = mimetypes.guess_type(url)\n        ret = \"application/{}\".format(x[1]) if x[1] else x[0]\n\n    if not ret and path:\n        try:\n            with open(fsenc(path), \"rb\", 0) as f:\n                ret = magic.from_buffer(f.read(4096), mime=True)\n                if ret.startswith(\"text/htm\"):\n                    # avoid serving up HTML content unless there was actually a .html extension\n                    ret = \"text/plain\"\n        except Exception as ex:\n            pass\n\n    if not ret:\n        ret = fallback\n\n    if \";\" not in ret:\n        if ret.startswith(\"text/\") or ret.endswith(\"/javascript\"):\n            ret += \"; charset=utf-8\"\n\n    return ret\n\n\ndef safe_mime(mime: str) -> str:\n    if \"text/\" in mime or \"xml\" in mime:\n        return \"text/plain; charset=utf-8\"\n    else:\n        return \"application/octet-stream\"\n\n\ndef getalive(pids: list[int], pgid: int) -> list[int]:\n    alive = []\n    for pid in pids:\n        try:\n            if pgid:\n                # check if still one of ours\n                if os.getpgid(pid) == pgid:\n                    alive.append(pid)\n            else:\n                # windows doesn't have pgroups; assume\n                assert psutil  # type: ignore  # !rm\n                psutil.Process(pid)\n                alive.append(pid)\n        except:\n            pass\n\n    return alive\n\n\ndef killtree(root: int) -> None:\n    \"\"\"still racy but i tried\"\"\"\n    try:\n        # limit the damage where possible (unixes)\n        pgid = os.getpgid(os.getpid())\n    except:\n        pgid = 0\n\n    if HAVE_PSUTIL:\n        assert psutil  # type: ignore  # !rm\n        pids = [root]\n        parent = psutil.Process(root)\n        for child in parent.children(recursive=True):\n            pids.append(child.pid)\n            child.terminate()\n        parent.terminate()\n        parent = None\n    elif pgid:\n        # linux-only\n        pids = []\n        chk = [root]\n        while chk:\n            pid = chk[0]\n            chk = chk[1:]\n            pids.append(pid)\n            _, t, _ = runcmd([\"pgrep\", \"-P\", str(pid)])\n            chk += [int(x) for x in t.strip().split(\"\\n\") if x]\n\n        pids = getalive(pids, pgid)  # filter to our pgroup\n        for pid in pids:\n            os.kill(pid, signal.SIGTERM)\n    else:\n        # windows gets minimal effort sorry\n        os.kill(root, signal.SIGTERM)\n        return\n\n    for n in range(10):\n        time.sleep(0.1)\n        pids = getalive(pids, pgid)\n        if not pids or n > 3 and pids == [root]:\n            break\n\n    for pid in pids:\n        try:\n            os.kill(pid, signal.SIGKILL)\n        except:\n            pass\n\n\ndef _find_nice() -> str:\n    if WINDOWS:\n        return \"\"  # use creationflags\n\n    try:\n        zs = shutil.which(\"nice\")\n        if zs:\n            return zs\n    except:\n        pass\n\n    # busted PATHs and/or py2\n    for zs in (\"/bin\", \"/sbin\", \"/usr/bin\", \"/usr/sbin\"):\n        zs += \"/nice\"\n        if os.path.exists(zs):\n            return zs\n\n    return \"\"\n\n\nNICES = _find_nice()\nNICEB = NICES.encode(\"utf-8\")\n\n\ndef runcmd(\n    argv: Union[list[bytes], list[str], list[\"LiteralString\"]],\n    timeout: Optional[float] = None,\n    **ka: Any\n) -> tuple[int, str, str]:\n    isbytes = isinstance(argv[0], (bytes, bytearray))\n    oom = ka.pop(\"oom\", 0)  # 0..1000\n    kill = ka.pop(\"kill\", \"t\")  # [t]ree [m]ain [n]one\n    capture = ka.pop(\"capture\", 3)  # 0=none 1=stdout 2=stderr 3=both\n\n    sin: Optional[bytes] = ka.pop(\"sin\", None)\n    if sin:\n        ka[\"stdin\"] = sp.PIPE\n\n    cout = sp.PIPE if capture in [1, 3] else None\n    cerr = sp.PIPE if capture in [2, 3] else None\n    bout: bytes\n    berr: bytes\n\n    if ANYWIN:\n        if isbytes:\n            if argv[0] in CMD_EXEB:\n                argv[0] += b\".exe\"  # type: ignore\n        else:\n            if argv[0] in CMD_EXES:\n                argv[0] += \".exe\"  # type: ignore\n\n    if ka.pop(\"nice\", None):\n        if WINDOWS:\n            ka[\"creationflags\"] = 0x4000\n        elif NICEB:\n            if isbytes:\n                argv = [NICEB] + argv  # type: ignore\n            else:\n                argv = [NICES] + argv  # type: ignore\n\n    p = sp.Popen(argv, stdout=cout, stderr=cerr, **ka)\n\n    if oom and not ANYWIN and not MACOS:\n        try:\n            with open(\"/proc/%d/oom_score_adj\" % (p.pid,), \"wb\") as f:\n                f.write((\"%d\\n\" % (oom,)).encode(\"utf-8\"))\n        except:\n            pass\n\n    if not timeout or PY2:\n        bout, berr = p.communicate(sin)  # type: ignore\n    else:\n        try:\n            bout, berr = p.communicate(sin, timeout=timeout)  # type: ignore\n        except sp.TimeoutExpired:\n            if kill == \"n\":\n                return -18, \"\", \"\"  # SIGCONT; leave it be\n            elif kill == \"m\":\n                p.kill()\n            else:\n                killtree(p.pid)\n\n            try:\n                bout, berr = p.communicate(timeout=1)  # type: ignore\n            except:\n                bout = b\"\"\n                berr = b\"\"\n\n    stdout = bout.decode(\"utf-8\", \"replace\") if cout else \"\"\n    stderr = berr.decode(\"utf-8\", \"replace\") if cerr else \"\"\n\n    rc: int = p.returncode\n    if rc is None:\n        rc = -14  # SIGALRM; failed to kill\n\n    return rc, stdout, stderr\n\n\ndef chkcmd(argv: Union[list[bytes], list[str]], **ka: Any) -> tuple[str, str]:\n    ok, sout, serr = runcmd(argv, **ka)\n    if ok != 0:\n        retchk(ok, argv, serr)\n        raise Exception(serr)\n\n    return sout, serr\n\n\ndef mchkcmd(argv: Union[list[bytes], list[str]], timeout: float = 10) -> None:\n    if PY2:\n        with open(os.devnull, \"wb\") as f:\n            rv = sp.call(argv, stdout=f, stderr=f)\n    else:\n        rv = sp.call(argv, stdout=sp.DEVNULL, stderr=sp.DEVNULL, timeout=timeout)\n\n    if rv:\n        raise sp.CalledProcessError(rv, (argv[0], b\"...\", argv[-1]))\n\n\ndef retchk(\n    rc: int,\n    cmd: Union[list[bytes], list[str]],\n    serr: str,\n    logger: Optional[\"NamedLogger\"] = None,\n    color: Union[int, str] = 0,\n    verbose: bool = False,\n) -> None:\n    if rc < 0:\n        rc = 128 - rc\n\n    if not rc or rc < 126 and not verbose:\n        return\n\n    s = None\n    if rc > 128:\n        try:\n            s = str(signal.Signals(rc - 128))\n        except:\n            pass\n    elif rc == 126:\n        s = \"invalid program\"\n    elif rc == 127:\n        s = \"program not found\"\n    elif verbose:\n        s = \"unknown\"\n    else:\n        s = \"invalid retcode\"\n\n    if s:\n        t = \"{} <{}>\".format(rc, s)\n    else:\n        t = str(rc)\n\n    try:\n        c = \" \".join([fsdec(x) for x in cmd])  # type: ignore\n    except:\n        c = str(cmd)\n\n    t = \"error {} from [{}]\".format(t, c)\n    if serr:\n        if len(serr) > 8192:\n            zs = \"%s\\n[ ...TRUNCATED... ]\\n%s\\n[ NOTE: full msg was %d chars ]\"\n            serr = zs % (serr[:4096], serr[-4096:].rstrip(), len(serr))\n        serr = serr.replace(\"\\n\", \"\\nstderr: \")\n        t += \"\\nstderr: \" + serr\n\n    if logger:\n        logger(t, color)\n    else:\n        raise Exception(t)\n\n\ndef _parsehook(\n    log: Optional[\"NamedLogger\"], cmd: str\n) -> tuple[str, bool, bool, bool, bool, bool, float, dict[str, Any], list[str]]:\n    areq = \"\"\n    chk = False\n    fork = False\n    jtxt = False\n    imp = False\n    sin = False\n    wait = 0.0\n    tout = 0.0\n    kill = \"t\"\n    cap = 0\n    ocmd = cmd\n    while \",\" in cmd[:6]:\n        arg, cmd = cmd.split(\",\", 1)\n        if arg == \"c\":\n            chk = True\n        elif arg == \"f\":\n            fork = True\n        elif arg == \"j\":\n            jtxt = True\n        elif arg == \"I\":\n            imp = True\n        elif arg == \"s\":\n            sin = True\n        elif arg.startswith(\"w\"):\n            wait = float(arg[1:])\n        elif arg.startswith(\"t\"):\n            tout = float(arg[1:])\n        elif arg.startswith(\"c\"):\n            cap = int(arg[1:])  # 0=none 1=stdout 2=stderr 3=both\n        elif arg.startswith(\"k\"):\n            kill = arg[1:]  # [t]ree [m]ain [n]one\n        elif arg.startswith(\"a\"):\n            areq = arg[1:]  # required perms\n        elif arg.startswith(\"i\"):\n            pass\n        elif not arg:\n            break\n        else:\n            t = \"hook: invalid flag {} in {}\"\n            (log or print)(t.format(arg, ocmd))\n\n    env = os.environ.copy()\n    try:\n        if EXE:\n            raise Exception()\n\n        pypath = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))\n        zsl = [str(pypath)] + [str(x) for x in sys.path if x]\n        pypath = str(os.pathsep.join(zsl))\n        env[\"PYTHONPATH\"] = pypath\n    except:\n        if not EXE:\n            raise\n\n    sp_ka = {\n        \"env\": env,\n        \"nice\": True,\n        \"oom\": 300,\n        \"timeout\": tout,\n        \"kill\": kill,\n        \"capture\": cap,\n    }\n\n    argv = cmd.split(\",\") if \",\" in cmd else [cmd]\n\n    argv[0] = os.path.expandvars(os.path.expanduser(argv[0]))\n\n    return areq, chk, imp, fork, sin, jtxt, wait, sp_ka, argv\n\n\ndef runihook(\n    log: Optional[\"NamedLogger\"],\n    verbose: bool,\n    cmd: str,\n    vol: \"VFS\",\n    ups: list[tuple[str, int, int, str, str, str, int, str]],\n) -> bool:\n    _, chk, _, fork, _, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd)\n    bcmd = [sfsenc(x) for x in acmd]\n    if acmd[0].endswith(\".py\"):\n        bcmd = [sfsenc(pybin)] + bcmd\n\n    vps = [vjoin(*list(s3dec(x[3], x[4]))) for x in ups]\n    aps = [djoin(vol.realpath, x) for x in vps]\n    if jtxt:\n        # 0w 1mt 2sz 3rd 4fn 5ip 6at\n        ja = [\n            {\n                \"ap\": uncify(ap),  # utf8 for json\n                \"vp\": vp,\n                \"wark\": x[0][:16],\n                \"mt\": x[1],\n                \"sz\": x[2],\n                \"ip\": x[5],\n                \"at\": x[6],\n            }\n            for x, vp, ap in zip(ups, vps, aps)\n        ]\n        sp_ka[\"sin\"] = json.dumps(ja).encode(\"utf-8\", \"replace\")\n    else:\n        sp_ka[\"sin\"] = b\"\\n\".join(fsenc(x) for x in aps)\n\n    if acmd[0].startswith(\"zmq:\"):\n        try:\n            msg = sp_ka[\"sin\"].decode(\"utf-8\", \"replace\")\n            _zmq_hook(log, verbose, \"xiu\", acmd[0][4:].lower(), msg, wait, sp_ka)\n            if verbose and log:\n                log(\"hook(xiu) %r OK\" % (cmd,), 6)\n        except Exception as ex:\n            if log:\n                log(\"zeromq failed: %r\" % (ex,))\n        return True\n\n    t0 = time.time()\n    if fork:\n        Daemon(runcmd, cmd, bcmd, ka=sp_ka)\n    else:\n        rc, v, err = runcmd(bcmd, **sp_ka)  # type: ignore\n        if chk and rc:\n            retchk(rc, bcmd, err, log, 5)\n            return False\n\n    if wait:\n        wait -= time.time() - t0\n        if wait > 0:\n            time.sleep(wait)\n\n    return True\n\n\nZMQ = {}\nZMQ_DESC = {\n    \"pub\": \"fire-and-forget to all/any connected SUB-clients\",\n    \"push\": \"fire-and-forget to one of the connected PULL-clients\",\n    \"req\": \"send messages to a REP-server and blocking-wait for ack\",\n}\n\n\ndef _zmq_hook(\n    log: Optional[\"NamedLogger\"],\n    verbose: bool,\n    src: str,\n    cmd: str,\n    msg: str,\n    wait: float,\n    sp_ka: dict[str, Any],\n) -> tuple[int, str]:\n    import zmq\n\n    try:\n        mtx = ZMQ[\"mtx\"]\n    except:\n        ZMQ[\"mtx\"] = threading.Lock()\n        time.sleep(0.1)\n        mtx = ZMQ[\"mtx\"]\n\n    ret = \"\"\n    nret = 0\n    t0 = time.time()\n    if verbose and log:\n        log(\"hook(%s) %r entering zmq-main-lock\" % (src, cmd), 6)\n\n    with mtx:\n        try:\n            mode, sck, mtx = ZMQ[cmd]\n        except:\n            mode, uri = cmd.split(\":\", 1)\n            try:\n                desc = ZMQ_DESC[mode]\n                if log:\n                    t = \"libzmq(%s) pyzmq(%s) init(%s); %s\"\n                    log(t % (zmq.zmq_version(), zmq.__version__, cmd, desc))\n            except:\n                raise Exception(\"the only supported ZMQ modes are REQ PUB PUSH\")\n\n            try:\n                ctx = ZMQ[\"ctx\"]\n            except:\n                ctx = ZMQ[\"ctx\"] = zmq.Context()\n\n            timeout = sp_ka[\"timeout\"]\n\n            if mode == \"pub\":\n                sck = ctx.socket(zmq.PUB)\n                sck.setsockopt(zmq.LINGER, 0)\n                sck.bind(uri)\n                time.sleep(1)  # give clients time to connect; avoids losing first msg\n            elif mode == \"push\":\n                sck = ctx.socket(zmq.PUSH)\n                if timeout:\n                    sck.SNDTIMEO = int(timeout * 1000)\n                sck.setsockopt(zmq.LINGER, 0)\n                sck.bind(uri)\n            elif mode == \"req\":\n                sck = ctx.socket(zmq.REQ)\n                if timeout:\n                    sck.RCVTIMEO = int(timeout * 1000)\n                sck.setsockopt(zmq.LINGER, 0)\n                sck.connect(uri)\n            else:\n                raise Exception()\n\n            mtx = threading.Lock()\n            ZMQ[cmd] = (mode, sck, mtx)\n\n    if verbose and log:\n        log(\"hook(%s) %r entering socket-lock\" % (src, cmd), 6)\n\n    with mtx:\n        if verbose and log:\n            log(\"hook(%s) %r sending |%d|\" % (src, cmd, len(msg)), 6)\n\n        sck.send_string(msg)  # PUSH can safely timeout here\n\n        if mode == \"req\":\n            if verbose and log:\n                log(\"hook(%s) %r awaiting ack from req\" % (src, cmd), 6)\n            try:\n                ret = sck.recv().decode(\"utf-8\", \"replace\")\n                if ret.startswith(\"return \"):\n                    m = re.search(\"^return ([0-9]+)\", ret[:12])\n                    if m:\n                        nret = int(m.group(1))\n            except:\n                sck.close()\n                del ZMQ[cmd]  # bad state; must reset\n                raise Exception(\"ack timeout; zmq socket killed\")\n\n    if ret and log:\n        log(\"hook(%s) %r ACK: %r\" % (src, cmd, ret), 6)\n\n    if wait:\n        wait -= time.time() - t0\n        if wait > 0:\n            time.sleep(wait)\n\n    return nret, ret\n\n\ndef _runhook(\n    log: Optional[\"NamedLogger\"],\n    verbose: bool,\n    src: str,\n    cmd: str,\n    ap: str,\n    vp: str,\n    host: str,\n    uname: str,\n    perms: str,\n    mt: float,\n    sz: int,\n    ip: str,\n    at: float,\n    txt: Optional[list[str]],\n) -> dict[str, Any]:\n    ret = {\"rc\": 0}\n    areq, chk, imp, fork, sin, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd)\n    if areq:\n        for ch in areq:\n            if ch not in perms:\n                t = \"user %s not allowed to run hook %s; need perms %s, have %s\"\n                if log:\n                    log(t % (uname, cmd, areq, perms))\n                return ret  # fallthrough to next hook\n    if imp or jtxt:\n        ja = {\n            \"ap\": ap,\n            \"vp\": vp,\n            \"mt\": mt,\n            \"sz\": sz,\n            \"ip\": ip,\n            \"at\": at or time.time(),\n            \"host\": host,\n            \"user\": uname,\n            \"perms\": perms,\n            \"src\": src,\n        }\n        if txt:\n            ja[\"txt\"] = txt[0]\n            ja[\"body\"] = txt[1]\n        if imp:\n            ja[\"log\"] = log\n            mod = loadpy(acmd[0], False)\n            return mod.main(ja)\n        arg = json.dumps(ja)\n    else:\n        arg = txt[0] if txt else ap\n\n    if acmd[0].startswith(\"zmq:\"):\n        zi, zs = _zmq_hook(log, verbose, src, acmd[0][4:].lower(), arg, wait, sp_ka)\n        if zi:\n            raise Exception(\"zmq says %d\" % (zi,))\n        try:\n            ret = json.loads(zs)\n            if \"rc\" not in ret:\n                ret[\"rc\"] = 0\n            return ret\n        except:\n            return {\"rc\": 0, \"stdout\": zs}\n\n    if sin:\n        sp_ka[\"sin\"] = (arg + \"\\n\").encode(\"utf-8\", \"replace\")\n    else:\n        acmd += [arg]\n\n    if acmd[0].endswith(\".py\"):\n        acmd = [pybin] + acmd\n\n    bcmd = [fsenc(x) if x == ap else sfsenc(x) for x in acmd]\n\n    t0 = time.time()\n    if fork:\n        Daemon(runcmd, cmd, [bcmd], ka=sp_ka)\n    else:\n        rc, v, err = runcmd(bcmd, **sp_ka)  # type: ignore\n        if chk and rc:\n            ret[\"rc\"] = rc\n            zi = 0 if rc == 100 else rc\n            retchk(zi, bcmd, err, log, 5)\n        else:\n            try:\n                ret = json.loads(v)\n            except:\n                pass\n\n            try:\n                if \"stdout\" not in ret:\n                    ret[\"stdout\"] = v\n                if \"stderr\" not in ret:\n                    ret[\"stderr\"] = err\n                if \"rc\" not in ret:\n                    ret[\"rc\"] = rc\n            except:\n                ret = {\"rc\": rc, \"stdout\": v, \"stderr\": err}\n\n    if wait:\n        wait -= time.time() - t0\n        if wait > 0:\n            time.sleep(wait)\n\n    return ret\n\n\ndef runhook(\n    log: Optional[\"NamedLogger\"],\n    broker: Optional[\"BrokerCli\"],\n    up2k: Optional[\"Up2k\"],\n    src: str,\n    cmds: list[str],\n    ap: str,\n    vp: str,\n    host: str,\n    uname: str,\n    perms: str,\n    mt: float,\n    sz: int,\n    ip: str,\n    at: float,\n    txt: Optional[list[str]],\n) -> dict[str, Any]:\n    assert broker or up2k  # !rm\n    args = (broker or up2k).args  # type: ignore\n    verbose = args.hook_v\n    vp = vp.replace(\"\\\\\", \"/\")\n    ret = {\"rc\": 0}\n    stop = False\n    for cmd in cmds:\n        try:\n            hr = _runhook(\n                log, verbose, src, cmd, ap, vp, host, uname, perms, mt, sz, ip, at, txt\n            )\n            if verbose and log:\n                log(\"hook(%s) %r => \\033[32m%s\" % (src, cmd, hr), 6)\n            for k, v in hr.items():\n                if k in (\"idx\", \"del\") and v:\n                    if broker:\n                        broker.say(\"up2k.hook_fx\", k, v, vp)\n                    else:\n                        assert up2k  # !rm\n                        up2k.fx_backlog.append((k, v, vp))\n                elif k == \"reloc\" and v:\n                    # idk, just take the last one ig\n                    ret[\"reloc\"] = v\n                elif k == \"rc\" and v:\n                    stop = True\n                    ret[k] = 0 if v == 100 else v\n                elif k in ret:\n                    if k == \"stdout\" and v and not ret[k]:\n                        ret[k] = v\n                else:\n                    ret[k] = v\n        except Exception as ex:\n            (log or print)(\"hook: %r, %s\" % (ex, ex))\n            if \",c,\" in \",\" + cmd:\n                return {\"rc\": 1}\n            break\n        if stop:\n            break\n\n    return ret\n\n\ndef loadpy(ap: str, hot: bool) -> Any:\n    \"\"\"\n    a nice can of worms capable of causing all sorts of bugs\n    depending on what other inconveniently named files happen\n    to be in the same folder\n    \"\"\"\n    ap = os.path.expandvars(os.path.expanduser(ap))\n    mdir, mfile = os.path.split(absreal(ap))\n    mname = mfile.rsplit(\".\", 1)[0]\n    sys.path.insert(0, mdir)\n\n    if PY2:\n        mod = __import__(mname)\n        if hot:\n            reload(mod)  # type: ignore\n    else:\n        import importlib\n\n        mod = importlib.import_module(mname)\n        if hot:\n            importlib.reload(mod)\n\n    sys.path.remove(mdir)\n    return mod\n\n\ndef gzip_orig_sz(fn: str) -> int:\n    with open(fsenc(fn), \"rb\") as f:\n        return gzip_file_orig_sz(f)\n\n\ndef gzip_file_orig_sz(f) -> int:\n    start = f.tell()\n    f.seek(-4, 2)\n    rv = f.read(4)\n    f.seek(start, 0)\n    return sunpack(b\"I\", rv)[0]  # type: ignore\n\n\ndef align_tab(lines: list[str]) -> list[str]:\n    rows = []\n    ncols = 0\n    for ln in lines:\n        row = [x for x in ln.split(\" \") if x]\n        ncols = max(ncols, len(row))\n        rows.append(row)\n\n    lens = [0] * ncols\n    for row in rows:\n        for n, col in enumerate(row):\n            lens[n] = max(lens[n], len(col))\n\n    return [\"\".join(x.ljust(y + 2) for x, y in zip(row, lens)) for row in rows]\n\n\ndef visual_length(txt: str) -> int:\n    # from r0c\n    eoc = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n    clen = 0\n    pend = None\n    counting = True\n    for ch in txt:\n\n        # escape sequences can never contain ESC;\n        # treat pend as regular text if so\n        if ch == \"\\033\" and pend:\n            clen += len(pend)\n            counting = True\n            pend = None\n\n        if not counting:\n            if ch in eoc:\n                counting = True\n        else:\n            if pend:\n                pend += ch\n                if pend.startswith(\"\\033[\"):\n                    counting = False\n                else:\n                    clen += len(pend)\n                    counting = True\n                pend = None\n            else:\n                if ch == \"\\033\":\n                    pend = \"%s\" % (ch,)\n                else:\n                    co = ord(ch)\n                    # the safe parts of latin1 and cp437 (no greek stuff)\n                    if (\n                        co < 0x100  # ascii + lower half of latin1\n                        or (co >= 0x2500 and co <= 0x25A0)  # box drawings\n                        or (co >= 0x2800 and co <= 0x28FF)  # braille\n                    ):\n                        clen += 1\n                    else:\n                        # assume moonrunes or other double-width\n                        clen += 2\n    return clen\n\n\ndef wrap(txt: str, maxlen: int, maxlen2: int) -> list[str]:\n    # from r0c\n    words = re.sub(r\"([, ])\", r\"\\1\\n\", txt.rstrip()).split(\"\\n\")\n    pad = maxlen - maxlen2\n    ret = []\n    for word in words:\n        if len(word) * 2 < maxlen or visual_length(word) < maxlen:\n            ret.append(word)\n        else:\n            while visual_length(word) >= maxlen:\n                ret.append(word[: maxlen - 1] + \"-\")\n                word = word[maxlen - 1 :]\n            if word:\n                ret.append(word)\n\n    words = ret\n    ret = []\n    ln = \"\"\n    spent = 0\n    for word in words:\n        wl = visual_length(word)\n        if spent + wl > maxlen:\n            ret.append(ln)\n            maxlen = maxlen2\n            spent = 0\n            ln = \" \" * pad\n        ln += word\n        spent += wl\n    if ln:\n        ret.append(ln)\n\n    return ret\n\n\ndef termsize() -> tuple[int, int]:\n    try:\n        w, h = os.get_terminal_size()\n        return w, h\n    except:\n        pass\n\n    env = os.environ\n\n    def ioctl_GWINSZ(fd: int) -> Optional[tuple[int, int]]:\n        assert fcntl  # type: ignore  # !rm\n        assert termios  # type: ignore  # !rm\n        try:\n            cr = sunpack(b\"hh\", fcntl.ioctl(fd, termios.TIOCGWINSZ, b\"AAAA\"))\n            return cr[::-1]\n        except:\n            return None\n\n    cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)\n    if not cr:\n        try:\n            fd = os.open(os.ctermid(), os.O_RDONLY)\n            cr = ioctl_GWINSZ(fd)\n            os.close(fd)\n        except:\n            pass\n\n    try:\n        return cr or (int(env[\"COLUMNS\"]), int(env[\"LINES\"]))\n    except:\n        return 80, 25\n\n\ndef hidedir(dp) -> None:\n    if ANYWIN:\n        try:\n            assert wk32  # type: ignore  # !rm\n            k32 = wk32\n            attrs = k32.GetFileAttributesW(dp)\n            if attrs >= 0:\n                k32.SetFileAttributesW(dp, attrs | 2)\n        except:\n            pass\n\n\n_flocks = {}\n\n\ndef _lock_file_noop(ap: str) -> bool:\n    return True\n\n\ndef _lock_file_ioctl(ap: str) -> bool:\n    assert fcntl  # type: ignore  # !rm\n    try:\n        fd = _flocks.pop(ap)\n        os.close(fd)\n    except:\n        pass\n\n    fd = os.open(ap, os.O_RDWR | os.O_CREAT, 438)\n    # NOTE: the fcntl.lockf identifier is (pid,node);\n    #  the lock will be dropped if os.close(os.open(ap))\n    #  is performed anywhere else in this thread\n\n    try:\n        fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)\n        _flocks[ap] = fd\n        return True\n    except Exception as ex:\n        eno = getattr(ex, \"errno\", -1)\n        try:\n            os.close(fd)\n        except:\n            pass\n        if eno in (errno.EAGAIN, errno.EACCES):\n            return False\n        print(\"WARNING: unexpected errno %d from fcntl.lockf; %r\" % (eno, ex))\n        return True\n\n\ndef _lock_file_windows(ap: str) -> bool:\n    try:\n        import msvcrt\n\n        try:\n            fd = _flocks.pop(ap)\n            os.close(fd)\n        except:\n            pass\n\n        fd = os.open(ap, os.O_RDWR | os.O_CREAT, 438)\n        msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)\n        return True\n    except Exception as ex:\n        eno = getattr(ex, \"errno\", -1)\n        if eno == errno.EACCES:\n            return False\n        print(\"WARNING: unexpected errno %d from msvcrt.locking; %r\" % (eno, ex))\n        return True\n\n\nif os.environ.get(\"PRTY_NO_DB_LOCK\"):\n    lock_file = _lock_file_noop\nelif ANYWIN:\n    lock_file = _lock_file_windows\nelif HAVE_FCNTL:\n    lock_file = _lock_file_ioctl\nelse:\n    lock_file = _lock_file_noop\n\n\ndef _open_nolock_windows(bap: Union[str, bytes], *a, **ka) -> typing.BinaryIO:\n    assert ctypes  # !rm\n    assert wk32  # !rm\n    import msvcrt\n\n    try:\n        ap = bap.decode(\"utf-8\", \"replace\")  # type: ignore\n    except:\n        ap = bap\n\n    fh = wk32.CreateFileW(ap, 0x80000000, 7, None, 3, 0x80, None)\n    # `-ap, GENERIC_READ, FILE_SHARE_READ|WRITE|DELETE, None, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, None\n    if fh == -1:\n        ec = ctypes.get_last_error()  # type: ignore\n        raise ctypes.WinError(ec)  # type: ignore\n    fd = msvcrt.open_osfhandle(fh, os.O_RDONLY)  # type: ignore\n    return os.fdopen(fd, \"rb\")\n\n\nif ANYWIN:\n    open_nolock = _open_nolock_windows\nelse:\n    open_nolock = open\n\n\ntry:\n    if sys.version_info < (3, 10) or os.environ.get(\"PRTY_NO_IMPRESO\"):\n        # py3.8 doesn't have .files\n        # py3.9 has broken .is_file\n        raise ImportError()\n    import importlib.resources as impresources\nexcept ImportError:\n    try:\n        import importlib_resources as impresources\n    except ImportError:\n        impresources = None\ntry:\n    if sys.version_info > (3, 10):\n        raise ImportError()\n    import pkg_resources\nexcept ImportError:\n    pkg_resources = None\n\n\ndef _pkg_resource_exists(pkg: str, name: str) -> bool:\n    if not pkg_resources:\n        return False\n    try:\n        return pkg_resources.resource_exists(pkg, name)\n    except NotImplementedError:\n        return False\n\n\ndef stat_resource(E: EnvParams, name: str):\n    path = E.mod_ + name\n    if os.path.exists(path):\n        return os.stat(fsenc(path))\n    return None\n\n\ndef _find_impresource(pkg: types.ModuleType, name: str):\n    assert impresources  # !rm\n    try:\n        files = impresources.files(pkg)\n    except ImportError:\n        return None\n\n    return files.joinpath(name)\n\n\n_rescache_has = {}\n\n\ndef _has_resource(name: str):\n    try:\n        return _rescache_has[name]\n    except:\n        pass\n\n    if len(_rescache_has) > 999:\n        _rescache_has.clear()\n\n    assert __package__  # !rm\n    pkg = sys.modules[__package__]\n\n    if impresources:\n        res = _find_impresource(pkg, name)\n        if res and res.is_file():\n            _rescache_has[name] = True\n            return True\n\n    if pkg_resources:\n        if _pkg_resource_exists(pkg.__name__, name):\n            _rescache_has[name] = True\n            return True\n\n    _rescache_has[name] = False\n    return False\n\n\ndef has_resource(E: EnvParams, name: str):\n    return _has_resource(name) or os.path.exists(E.mod_ + name)\n\n\ndef load_resource(E: EnvParams, name: str, mode=\"rb\") -> IO[bytes]:\n    enc = None if \"b\" in mode else \"utf-8\"\n\n    if impresources:\n        assert __package__  # !rm\n        res = _find_impresource(sys.modules[__package__], name)\n        if res and res.is_file():\n            if enc:\n                return res.open(mode, encoding=enc)\n            else:\n                # throws if encoding= is mentioned at all\n                return res.open(mode)\n\n    if pkg_resources:\n        assert __package__  # !rm\n        pkg = sys.modules[__package__]\n        if _pkg_resource_exists(pkg.__name__, name):\n            stream = pkg_resources.resource_stream(pkg.__name__, name)\n            if enc:\n                stream = codecs.getreader(enc)(stream)\n            return stream\n\n    ap = E.mod_ + name\n\n    if PY2:\n        return codecs.open(ap, \"r\", encoding=enc)  # type: ignore\n\n    return open(ap, mode, encoding=enc)\n\n\nclass Pebkac(Exception):\n    def __init__(\n        self, code: int, msg: Optional[str] = None, log: Optional[str] = None\n    ) -> None:\n        super(Pebkac, self).__init__(msg or HTTPCODE[code])\n        self.code = code\n        self.log = log\n\n    def __repr__(self) -> str:\n        return \"Pebkac({}, {})\".format(self.code, repr(self.args))\n\n\nclass WrongPostKey(Pebkac):\n    def __init__(\n        self,\n        expected: str,\n        got: str,\n        fname: Optional[str],\n        datagen: Generator[bytes, None, None],\n    ) -> None:\n        msg = 'expected field \"{}\", got \"{}\"'.format(expected, got)\n        super(WrongPostKey, self).__init__(422, msg)\n\n        self.expected = expected\n        self.got = got\n        self.fname = fname\n        self.datagen = datagen\n\n\n_: Any = (\n    gzip,\n    mp,\n    zlib,\n    BytesIO,\n    quote,\n    unquote,\n    SQLITE_VER,\n    JINJA_VER,\n    PYFTPD_VER,\n    PARTFTPY_VER,\n)\n__all__ = [\n    \"gzip\",\n    \"mp\",\n    \"zlib\",\n    \"BytesIO\",\n    \"quote\",\n    \"unquote\",\n    \"SQLITE_VER\",\n    \"JINJA_VER\",\n    \"PYFTPD_VER\",\n    \"PARTFTPY_VER\",\n]\n"
  },
  {
    "path": "copyparty/web/Makefile",
    "content": "# run me to zopfli all the static files\n# which should help on really slow connections\n# but then why are you using copyparty in the first place\n\npk: $(addsuffix .gz, $(wildcard tl/*.js *.js *.css) \\\n\ta/webdav-cfg.txt )\nun: $(addsuffix .un, $(wildcard tl/*.gz *.gz a/*.gz))\n\ntl/%.js.gz: tl/%.js\n\t./Makefile.s1 <$< | pigz -c11 -J 34 -I 573 >$@\n\ttouch -r $< $@\n\trm $<\n\n%.gz: %\n\tpigz -c11 -J 34 -I 573 <$< >$@\n\ttouch -r $< $@\n\trm $<\n\n%.un: %\n\tpigz -d $<\n"
  },
  {
    "path": "copyparty/web/Makefile.s1",
    "content": "sed -r 's/^\\t+//;s/^\"([a-zA-Z][a-zA-Z0-9_]*)\": /\\1:/; /^\\/\\//d; s/([^\\][\"'\\''],) *\\/\\/.*/\\1/; /^$/d'"
  },
  {
    "path": "copyparty/web/a/__init__.py",
    "content": ""
  },
  {
    "path": "copyparty/web/baguettebox.js",
    "content": "\"use strict\";\n\n/*!\n * baguetteBox.js\n * @author  feimosi\n * @version 1.11.1-mod\n * @url https://github.com/feimosi/baguetteBox.js\n */\n\nvar J_BBX = 1;\n\nwindow.baguetteBox = (function () {\n    'use strict';\n\n    var options = {},\n        defaults = {\n            captions: true,\n            buttons: 'auto',\n            noScrollbars: false,\n            bodyClass: 'bbox-open',\n            titleTag: false,\n            async: false,\n            preload: 2,\n            refocus: true,\n            afterShow: null,\n            afterHide: null,\n            duringHide: null,\n            onChange: null,\n            readDirRtl: false,\n        },\n        overlay, slider, btnPrev, btnNext, btnHelp, btnAnim, btnRotL, btnRotR, btnSel, btnFull, btnZoom, btnVmode, btnReadDir, btnClose,\n        currentGallery = [],\n        currentIndex = 0,\n        isOverlayVisible = false,\n        touch = {},  // start-pos\n        touchFlag = false,  // busy\n        scrollCSS = ['', ''],\n        scrollTimer = 0,\n        re_i = APPLE ?\n            /^[^?]+\\.(a?png|avif|bmp|gif|hei[cf]s?|jfif|jpe?g|jxl|svg|tiff?|webp)(\\?|$)/i :\n            /^[^?]+\\.(a?png|avif|bmp|gif|jfif|jpe?g|jxl|svg|tiff?|webp)(\\?|$)/i,\n        re_v = /^[^?]+\\.(webm|mkv|mp4|m4v|mov)(\\?|$)/i,\n        re_cbz = /^[^?]+\\.(cbz)(\\?|$)/i,\n        anims = ['slideIn', 'fadeIn', 'none'],\n        data = {},  // all galleries\n        imagesElements = [],\n        documentLastFocus = null,\n        isFullscreen = false,\n        vmute = false,\n        vloop = sread('vmode') == 'L',\n        vnext = sread('vmode') == 'C',\n        loopA = null,\n        loopB = null,\n        url_ts = null,\n        un_pp = 0,\n        resume_mp = false;\n\n    var onFSC = function (e) {\n        isFullscreen = !!document.fullscreenElement;\n        clmod(document.documentElement, 'bb_fsc', isFullscreen);\n    };\n\n    var overlayClickHandler = function (e) {\n        if (e.target.id.indexOf('baguette-img') !== -1)\n            hideOverlay();\n    };\n\n    var vtouch = function (e) {\n        var v = vid(),\n            bv = v.getBoundingClientRect(),\n            tp = e.changedTouches[0];\n\n        if (bv.bottom - tp.clientY < 90)\n            touchFlag = true;\n    };\n\n    var touchstartHandler = function (e) {\n        touch.count = e.touches.length;\n        if (touch.count > 1)\n            touch.multitouch = true;\n\n        touch.startX = e.changedTouches[0].pageX;\n        touch.startY = e.changedTouches[0].pageY;\n    };\n    var touchmoveHandler = function (e) {\n        if (touchFlag || touch.multitouch)\n            return;\n\n        e.preventDefault ? e.preventDefault() : e.returnValue = false;\n        var touchEvent = e.touches[0] || e.changedTouches[0];\n        if (touchEvent.pageX - touch.startX > 40) {\n            touchFlag = true;\n            showLeftImage();\n        } else if (touchEvent.pageX - touch.startX < -40) {\n            touchFlag = true;\n            showRightImage();\n        } else if (touch.startY - touchEvent.pageY > 100) {\n            hideOverlay();\n        }\n    };\n    var touchendHandler = function (e) {\n        touch.count--;\n        if (e && e.touches)\n            touch.count = e.touches.length;\n\n        if (touch.count <= 0)\n            touch.multitouch = false;\n\n        touchFlag = false;\n    };\n    var contextmenuHandler = function () {\n        touchendHandler();\n    };\n\n    var overlayWheelHandler = function (e) {\n        if (!options.noScrollbars || anymod(e))\n            return;\n\n        ev(e);\n\n        var x = e.deltaX,\n            y = e.deltaY,\n            d = Math.abs(x) > Math.abs(y) ? x : y;\n\n        if (e.deltaMode)\n            d *= 10;\n\n        if (Date.now() - scrollTimer < (Math.abs(d) > 20 ? 100 : 300))\n            return;\n\n        scrollTimer = Date.now();\n\n        if (d > 0)\n            showNextImageIgnoreReadDir();\n        else\n            showPreviousImageIgnoreReadDir();\n    };\n\n    var trapFocusInsideOverlay = function (e) {\n        if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(e.target))) {\n            e.stopPropagation();\n            btnClose.focus();\n        }\n    };\n\n    function run(selector, userOptions) {\n        buildOverlay();\n        removeFromCache(selector);\n        return bindImageClickListeners(selector, userOptions);\n    }\n\n    function bindImageClickListeners(selector, userOptions) {\n        var galleryNodeList = QSA(selector);\n        var selectorData = {\n            galleries: [],\n            nodeList: galleryNodeList\n        };\n        data[selector] = selectorData;\n\n        [].forEach.call(galleryNodeList, function (galleryElement) {\n            var tagsNodeList = [];\n            if (galleryElement.tagName === 'A')\n                tagsNodeList = [galleryElement];\n            else\n                tagsNodeList = galleryElement.getElementsByTagName('a');\n            if (have_zls)\n                bindCbzClickListeners(tagsNodeList, userOptions);\n\n            tagsNodeList = [].filter.call(tagsNodeList, function (element) {\n                if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1)\n                    return re_i.test(element.href) || re_v.test(element.href);\n            });\n            if (!tagsNodeList.length)\n                return;\n\n            var gallery = [];\n            [].forEach.call(tagsNodeList, function (imageElement, imageIndex) {\n                var imageElementClickHandler = function (e) {\n                    if (ctrl(e) || e && e.shiftKey)\n                        return true;\n\n                    e.preventDefault ? e.preventDefault() : e.returnValue = false;\n                    prepareOverlay(gallery, userOptions);\n                    showOverlay(imageIndex);\n                };\n                var imageItem = {\n                    eventHandler: imageElementClickHandler,\n                    imageElement: imageElement,\n                };\n                bind(imageElement, 'click', imageElementClickHandler);\n                gallery.push(imageItem);\n            });\n            selectorData.galleries.push(gallery);\n        });\n\n        return [selectorData.galleries, options];\n    }\n\n    function bindCbzClickListeners(tagsNodeList, userOptions) {\n        var cbzNodes = [].filter.call(tagsNodeList, function (element) {\n            return re_cbz.test(element.href);\n        });\n        if (!tagsNodeList.length) {\n            return;\n        }\n\n        [].forEach.call(cbzNodes, function (cbzElement, index) {\n            var gallery = [];\n            var eventHandler = function (e) {\n                if (ctrl(e) || e && e.shiftKey)\n                    return true;\n\n                e.preventDefault ? e.preventDefault() : e.returnValue = false;\n                fillCbzGallery(gallery, cbzElement, eventHandler).then(function () {\n                        prepareOverlay(gallery, userOptions);\n                        showOverlay(0);\n                    }\n                ).catch(function (reason) {\n                    console.error(\"cbz-ded\", reason);\n                    var t;\n                    try {\n                        t = uricom_dec(cbzElement.href.split('/').pop());\n                    } catch (ex) { }\n\n                    var msg = \"Could not browse \" + (t ? t : 'archive');\n                    try {\n                        msg += \"\\n\\n\" + reason.message;\n                    } catch (ex) { }\n                    toast.err(20, msg, 'cbz-ded');\n                });\n            }\n\n            bind(cbzElement, \"click\", eventHandler);\n        })\n    }\n\n    function fillCbzGallery(gallery, cbzElement, eventHandler) {\n        if (gallery.length !== 0) {\n            return Promise.resolve();\n        }\n        var href = cbzElement.href;\n        var zlsHref = href + (href.indexOf(\"?\") === -1 ? \"?\" : \"&\") + \"zls\";\n        return fetch(zlsHref)\n            .then(function (response) {\n                if (response.ok) {\n                    return response.json();\n                } else {\n                    throw new Error(\"Archive is invalid\");\n                }\n            })\n            .then(function (fileList) {\n                var imagesList = fileList.map(function (file) {\n                    return file[\"fn\"];\n                }).filter(function (file) {\n                    return re_i.test(file);\n                }).sort();\n\n                if (imagesList.length === 0) {\n                    throw new Error(\"Archive does not contain any images\");\n                }\n\n                imagesList.forEach(function (imageName, index) {\n                    var imageHref = href\n                        + (href.indexOf(\"?\") === -1 ? \"?\" : \"&\")\n                        + \"zget=\"\n                        + encodeURIComponent(imageName);\n\n                    var galleryItem = {\n                        href: imageHref,\n                        imageElement: cbzElement,\n                        eventHandler: eventHandler,\n                    };\n                    gallery.push(galleryItem);\n                });\n            });\n    }\n\n    function clearCachedData() {\n        for (var selector in data)\n            if (data.hasOwnProperty(selector))\n                removeFromCache(selector);\n    }\n\n    function removeFromCache(selector) {\n        if (!data.hasOwnProperty(selector))\n            return;\n\n        var galleries = data[selector].galleries;\n        [].forEach.call(galleries, function (gallery) {\n            [].forEach.call(gallery, function (imageItem) {\n                unbind(imageItem.imageElement, 'click', imageItem.eventHandler);\n            });\n\n            if (currentGallery === gallery)\n                currentGallery = [];\n        });\n\n        delete data[selector];\n    }\n\n    function buildOverlay() {\n        overlay = ebi('bbox-overlay');\n        if (!overlay) {\n            var ctr = mknod('div');\n            ctr.innerHTML = (\n                '<div id=\"bbox-overlay\" role=\"dialog\">' +\n                '<div id=\"bbox-slider\"></div>' +\n                '<button id=\"bbox-prev\" class=\"bbox-btn\" type=\"button\" aria-label=\"Previous\">&lt;</button>' +\n                '<button id=\"bbox-next\" class=\"bbox-btn\" type=\"button\" aria-label=\"Next\">&gt;</button>' +\n                '<div id=\"bbox-btns\">' +\n                '<button id=\"bbox-help\" type=\"button\">?</button>' +\n                '<button id=\"bbox-anim\" type=\"button\" tt=\"a\">-</button>' +\n                '<button id=\"bbox-readdir\" type=\"button\" tt=\"a\">ltr</button>' +\n                '<button id=\"bbox-rotl\" type=\"button\">↶</button>' +\n                '<button id=\"bbox-rotr\" type=\"button\">↷</button>' +\n                '<button id=\"bbox-tsel\" type=\"button\">sel</button>' +\n                '<button id=\"bbox-full\" type=\"button\" tt=\"full-screen\">⛶</button>' +\n                '<button id=\"bbzoom\" type=\"button\" tt=\"zoom/stretch\">z</button>' +\n                '<button id=\"bbox-vmode\" type=\"button\" tt=\"a\"></button>' +\n                '<button id=\"bbox-close\" type=\"button\" aria-label=\"Close\">X</button>' +\n                '</div></div>'\n            );\n            overlay = ctr.firstChild;\n            QS('body').appendChild(overlay);\n            tt.att(overlay);\n        }\n        slider = ebi('bbox-slider');\n        btnPrev = ebi('bbox-prev');\n        btnNext = ebi('bbox-next');\n        btnHelp = ebi('bbox-help');\n        btnAnim = ebi('bbox-anim');\n        btnReadDir = ebi('bbox-readdir');\n        btnRotL = ebi('bbox-rotl');\n        btnRotR = ebi('bbox-rotr');\n        btnSel = ebi('bbox-tsel');\n        btnFull = ebi('bbox-full');\n        btnZoom = ebi('bbzoom');\n        btnVmode = ebi('bbox-vmode');\n        btnClose = ebi('bbox-close');\n\n        bcfg_bind(options, 'bbzoom', 'bbzoom', false, setzoom);\n        setzoom();\n    }\n\n    function halp() {\n        if (ebi('bbox-halp'))\n            return;\n\n        var list = [\n            ['<b># hotkey</b>', '<b># operation</b>'],\n            ['escape', 'close'],\n            ['left, J', 'previous file'],\n            ['right, L', 'next file'],\n            ['home', 'first file'],\n            ['end', 'last file'],\n            ['R', 'rotate (shift=ccw)'],\n            ['F', 'toggle fullscreen'],\n            ['Z', 'toggle zoom/stretch'],\n            ['S', 'toggle file selection'],\n            ['space, P, K', 'video: play / pause'],\n            ['U', 'video: seek 10sec back'],\n            ['O', 'video: seek 10sec ahead'],\n            ['0..9', 'video: seek 0%..90%'],\n            ['M', 'video: toggle mute'],\n            ['V', 'video: toggle loop'],\n            ['C', 'video: toggle auto-next'],\n            ['<code>[</code>, <code>]</code>', 'video: loop start / end'],\n        ],\n            d = mknod('table', 'bbox-halp'),\n            html = ['<tbody>'];\n\n        for (var a = 0; a < list.length; a++)\n            html.push('<tr><td>' + list[a][0] + '</td><td>' + list[a][1] + '</td></tr>');\n\n        html.push('<tr><td colspan=\"2\">tap middle of img to hide btns</td></tr>');\n        html.push('<tr><td colspan=\"2\">tap left/right sides for prev/next</td></tr>');\n        d.innerHTML = html.join('\\n') + '</tbody>';\n        d.onclick = function () {\n            overlay.removeChild(d);\n        };\n        overlay.appendChild(d);\n    }\n\n    function keyDownHandler(e) {\n        if (modal.busy)\n            return;\n\n        if (anymod(e, true))\n            return;\n\n        var k = (e.key || e.code) + '', v = vid();\n\n        if (k.startsWith('Key'))\n            k = k.slice(3);\n        else if (k.startsWith('Digit'))\n            k = k.slice(5);\n\n        var kl = k.toLowerCase();\n\n        if (k == '?')\n            return halp();\n\n        if (k == \"[\" || k == \"BracketLeft\")\n            setloop(1);\n        else if (k == \"]\" || k == \"BracketRight\")\n            setloop(2);\n        else if (e.shiftKey && kl != \"r\")\n            return;\n        else if (k == \"ArrowLeft\" || k == \"Left\" || kl == \"j\")\n            showLeftImage();\n        else if (k == \"ArrowRight\" || k == \"Right\" || kl == \"l\")\n            showRightImage();\n        else if (k == \"Escape\" || k == \"Esc\")\n            hideOverlay();\n        else if (k == \"Home\")\n            showFirstImage(e);\n        else if (k == \"End\")\n            showLastImage(e);\n        else if (k == \"Space\" || k == \"Spacebar\" || kl == \" \" || kl == \"p\" || kl == \"k\")\n            playpause();\n        else if (kl == \"u\" || kl == \"o\")\n            relseek(kl == \"u\" ? -10 : 10);\n        else if (v && /^[0-9]$/.test(k) && isNum(v.duration))\n            v.currentTime = v.duration * parseInt(k) * 0.1;\n        else if (kl == \"m\" && v) {\n            v.muted = vmute = !vmute;\n            mp_ctl();\n        }\n        else if (kl == \"v\" && v) {\n            vloop = !vloop;\n            vnext = vnext && !vloop;\n            setVmode();\n        }\n        else if (kl == \"c\" && v) {\n            vnext = !vnext;\n            vloop = vloop && !vnext;\n            setVmode();\n        }\n        else if (kl == \"f\")\n            tglfull();\n        else if (kl == \"z\")\n            btnZoom.click();\n        else if (kl == \"s\")\n            tglsel();\n        else if (kl == \"r\")\n            rotn(e.shiftKey ? -1 : 1);\n        else if (kl == \"y\")\n            dlpic();\n        else\n            return;\n        return ev(e);\n    }\n\n    function anim() {\n        var i = (anims.indexOf(options.animation) + 1) % anims.length,\n            o = options;\n        swrite('ganim', anims[i]);\n        options = {};\n        setOptions(o);\n        if (tt.en)\n            tt.show.call(this);\n    }\n\n    function toggleReadDir() {\n        var o = options,\n            next = options.readDirRtl ? \"ltr\" : \"rtl\";\n        swrite('greaddir', next);\n        slider.className = \"no-transition\";\n        options = {};\n        setOptions(o);\n        updateOffset(true);\n        window.getComputedStyle(slider).opacity; // force a restyle\n        slider.className = \"\";\n\n        if (tt.en)\n            tt.show.call(this);\n    }\n\n    function setVmode() {\n        var v = vid();\n        ebi('bbox-vmode').style.display = v ? '' : 'none';\n        if (!v)\n            return;\n\n        var msg = 'When video ends, ', tts = '', lbl;\n        if (vloop) {\n            lbl = 'Loop';\n            msg += 'repeat it';\n            tts = '$NHotkey: V';\n        }\n        else if (vnext) {\n            lbl = 'Cont';\n            msg += 'continue to next';\n            tts = '$NHotkey: C';\n        }\n        else {\n            lbl = 'Stop';\n            msg += 'just stop'\n        }\n        btnVmode.setAttribute('aria-label', msg);\n        btnVmode.setAttribute('tt', msg + tts);\n        btnVmode.textContent = lbl;\n        swrite('vmode', lbl[0]);\n\n        v.loop = vloop\n        if (vloop && v.paused)\n            v.play();\n    }\n\n    function tglVmode() {\n        if (vloop) {\n            vnext = true;\n            vloop = false;\n        }\n        else if (vnext)\n            vnext = false;\n        else\n            vloop = true;\n\n        setVmode();\n        if (tt.en)\n            tt.show.call(this);\n    }\n\n    function findfile() {\n        var thumb = currentGallery[currentIndex].imageElement,\n            name = vsplit(thumb.href)[1].split('?')[0],\n            files = msel.getall();\n\n        for (var a = 0; a < files.length; a++)\n            if (vsplit(files[a].vp)[1] == name)\n                return [name, a, files, ebi(files[a].id)];\n    }\n\n    function tglfull() {\n        try {\n            if (isFullscreen)\n                document.exitFullscreen();\n            else\n                ebi('bbox-overlay').requestFullscreen();\n        }\n        catch (ex) {\n            if (IPHONE)\n                alert('sorry, apple decided to make this impossible on iphones (should work on ipad tho)');\n            else\n                alert(ex);\n        }\n    }\n\n    function setzoom() {\n        var sel = clgot(btnZoom, 'on')\n        clmod(ebi('bbox-overlay'), 'fill', sel);\n        btnState(btnZoom, sel);\n    }\n\n    function tglsel() {\n        var o = findfile()[3];\n        clmod(o.closest('tr'), 'sel', 't');\n        msel.selui();\n        selbg();\n    }\n\n    function dlpic() {\n        var url = addq(findfile()[3].href, 'cache');\n        dl_file(url);\n    }\n\n    function selbg() {\n        var img = vidimg(),\n            thumb = currentGallery[currentIndex].imageElement,\n            name = vsplit(thumb.href)[1].split('?')[0],\n            files = msel.getsel(),\n            sel = false;\n\n        for (var a = 0; a < files.length; a++)\n            if (vsplit(files[a].vp)[1] == name)\n                sel = true;\n\n        ebi('bbox-overlay').style.background = sel ?\n            'rgba(153,34,85,0.7)' : '';\n\n        img.style.borderRadius = sel ? '1em' : '';\n        btnState(btnSel, sel);\n    }\n\n    function btnState(btn, sel) {\n        btn.style.color = sel ? '#fff' : '';\n        btn.style.background = sel ? '#d48' : '';\n        btn.style.textShadow = sel ? '1px 1px 0 #b38' : '';\n        btn.style.boxShadow = sel ? '.15em .15em 0 #502' : '';\n    }\n\n    function keyUpHandler(e) {\n        if (anymod(e))\n            return;\n\n        var k = (e.key || e.code) + '';\n\n        if (k == \"Space\" || k == \"Spacebar\" || k == \" \") {\n            un_pp = Date.now();\n            return ev(e);\n        }\n    }\n\n    var passiveSupp = false;\n    try {\n        var opts = {\n            get passive() {\n                passiveSupp = true;\n                return false;\n            }\n        };\n        window.addEventListener('test', null, opts);\n        window.removeEventListener('test', null, opts);\n    }\n    catch (ex) {\n        passiveSupp = false;\n    }\n    var passiveEvent = passiveSupp ? { passive: false } : null;\n    var nonPassiveEvent = passiveSupp ? { passive: true } : null;\n\n    function bindEvents() {\n        bind(document, 'keydown', keyDownHandler);\n        bind(document, 'keyup', keyUpHandler);\n        bind(document, 'fullscreenchange', onFSC);\n        bind(overlay, 'click', overlayClickHandler);\n        bind(overlay, 'wheel', overlayWheelHandler);\n        bind(btnPrev, 'click', showLeftImage);\n        bind(btnNext, 'click', showRightImage);\n        bind(btnClose, 'click', hideOverlay);\n        bind(btnVmode, 'click', tglVmode);\n        bind(btnHelp, 'click', halp);\n        bind(btnAnim, 'click', anim);\n        bind(btnReadDir, 'click', toggleReadDir);\n        bind(btnRotL, 'click', rotl);\n        bind(btnRotR, 'click', rotr);\n        bind(btnSel, 'click', tglsel);\n        bind(btnFull, 'click', tglfull);\n        bind(slider, 'contextmenu', contextmenuHandler);\n        bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);\n        bind(overlay, 'touchmove', touchmoveHandler, passiveEvent);\n        bind(overlay, 'touchend', touchendHandler);\n        bind(document, 'focus', trapFocusInsideOverlay, true);\n    }\n\n    function unbindEvents() {\n        unbind(document, 'keydown', keyDownHandler);\n        unbind(document, 'keyup', keyUpHandler);\n        unbind(document, 'fullscreenchange', onFSC);\n        unbind(overlay, 'click', overlayClickHandler);\n        unbind(overlay, 'wheel', overlayWheelHandler);\n        unbind(btnPrev, 'click', showLeftImage);\n        unbind(btnNext, 'click', showRightImage);\n        unbind(btnClose, 'click', hideOverlay);\n        unbind(btnVmode, 'click', tglVmode);\n        unbind(btnHelp, 'click', halp);\n        unbind(btnAnim, 'click', anim);\n        unbind(btnReadDir, 'click', toggleReadDir);\n        unbind(btnRotL, 'click', rotl);\n        unbind(btnRotR, 'click', rotr);\n        unbind(btnSel, 'click', tglsel);\n        unbind(btnFull, 'click', tglfull);\n        unbind(slider, 'contextmenu', contextmenuHandler);\n        unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);\n        unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);\n        unbind(overlay, 'touchend', touchendHandler);\n        unbind(document, 'focus', trapFocusInsideOverlay, true);\n        timer.rm(rotn);\n    }\n\n    function prepareOverlay(gallery, userOptions) {\n        if (currentGallery === gallery)\n            return;\n\n        currentGallery = gallery;\n        setOptions(userOptions);\n        slider.innerHTML = '';\n        imagesElements.length = 0;\n\n        var imagesFiguresIds = [];\n        var imagesCaptionsIds = [];\n        for (var i = 0, fullImage; i < gallery.length; i++) {\n            fullImage = mknod('div', 'baguette-img-' + i);\n            fullImage.className = 'full-image';\n            imagesElements.push(fullImage);\n\n            imagesFiguresIds.push('bbox-figure-' + i);\n            imagesCaptionsIds.push('bbox-figcaption-' + i);\n            slider.appendChild(imagesElements[i]);\n        }\n        overlay.setAttribute('aria-labelledby', imagesFiguresIds.join(' '));\n        overlay.setAttribute('aria-describedby', imagesCaptionsIds.join(' '));\n    }\n\n    function setOptions(newOptions) {\n        if (!newOptions)\n            newOptions = {};\n\n        for (var item in defaults) {\n            options[item] = defaults[item];\n            if (typeof newOptions[item] !== 'undefined')\n                options[item] = newOptions[item];\n        }\n\n        var an = options.animation = sread('ganim', anims) || anims[ANIM ? 0 : 2];\n        btnAnim.textContent = ['⇄', '⮺', '⚡'][anims.indexOf(an)];\n        btnAnim.setAttribute('tt', 'animation: ' + an);\n\n        options.readDirRtl = sread('greaddir') === \"rtl\";\n        var msg;\n        if (options.readDirRtl) {\n            btnReadDir.innerText = \"rtl\";\n            msg = \"browse from right to left\";\n            slider.style.display = \"flex\";\n            slider.style.flexDirection = \"row-reverse\";\n        } else {\n            btnReadDir.innerText = \"ltr\";\n            msg = \"browse from left to right\";\n            slider.style.flexDirection = \"\";\n            slider.style.display = \"block\";\n        }\n        btnReadDir.setAttribute(\"tt\", msg);\n        btnReadDir.setAttribute(\"aria-label\", msg);\n\n\n        slider.style.transition = (options.animation === 'fadeIn' ? 'opacity .3s ease' :\n            options.animation === 'slideIn' ? '' : 'none');\n\n        if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1))\n            options.buttons = false;\n\n        btnPrev.style.display = btnNext.style.display = (options.buttons ? '' : 'none');\n    }\n\n    function showOverlay(chosenImageIndex) {\n        if (options.noScrollbars) {\n            var a = document.documentElement.style.overflowY,\n                b = document.body.style.overflowY;\n\n            if (a != 'hidden' || b != 'scroll')\n                scrollCSS = [a, b];\n\n            document.documentElement.style.overflowY = 'hidden';\n            document.body.style.overflowY = 'scroll';\n        }\n        if (overlay.style.display === 'block')\n            return;\n\n        bindEvents();\n        currentIndex = chosenImageIndex;\n        touch = {\n            count: 0,\n            startX: null,\n            startY: null\n        };\n        loadImage(currentIndex, function () {\n            preloadNext(currentIndex);\n            preloadPrev(currentIndex);\n        });\n\n        show_buttons(0);\n\n        updateOffset();\n        overlay.style.display = 'block';\n        // Fade in overlay\n        setTimeout(function () {\n            clmod(overlay, 'visible', 1);\n            if (options.bodyClass && document.body.classList)\n                document.body.classList.add(options.bodyClass);\n\n            if (options.afterShow)\n                options.afterShow();\n        }, 50);\n\n        if (options.onChange && !url_ts)\n            options.onChange.call(currentGallery, currentIndex, imagesElements.length);\n\n        url_ts = null;\n        documentLastFocus = document.activeElement;\n        btnClose.focus();\n        isOverlayVisible = true;\n    }\n\n    function hideOverlay(e, dtor) {\n        ev(e);\n        playvid(false);\n        removeFromCache('#files');\n        if (options.noScrollbars) {\n            document.documentElement.style.overflowY = scrollCSS[0];\n            document.body.style.overflowY = scrollCSS[1];\n        }\n\n        try {\n            if (document.fullscreenElement)\n                document.exitFullscreen();\n        }\n        catch (ex) { }\n        isFullscreen = false;\n\n        if (toast.tag == 'bb-ded')\n            toast.hide();\n\n        if (dtor || overlay.style.display === 'none')\n            return;\n\n        if (options.duringHide)\n            options.duringHide();\n\n        sethash('');\n        unbindEvents();\n\n        // Fade out and hide the overlay\n        clmod(overlay, 'visible');\n        setTimeout(function () {\n            overlay.style.display = 'none';\n            if (options.bodyClass && document.body.classList)\n                document.body.classList.remove(options.bodyClass);\n\n            qsr('#bbox-halp');\n\n            if (options.afterHide)\n                options.afterHide();\n\n            options.refocus && documentLastFocus && documentLastFocus.focus();\n            isOverlayVisible = false;\n            unvid();\n            unfig();\n        }, 250);\n    }\n\n    function unvid(keep) {\n        var vids = QSA('#bbox-overlay video');\n        for (var a = vids.length - 1; a >= 0; a--) {\n            var v = vids[a];\n            if (v == keep)\n                continue;\n\n            unbind(v, 'touchstart', vtouch, nonPassiveEvent);\n            unbind(v, 'error', lerr);\n            v.src = '';\n            v.load();\n\n            var p = v.parentNode;\n            p.removeChild(v);\n            p.parentNode.removeChild(p);\n        }\n    }\n\n    function unfig(keep) {\n        var figs = QSA('#bbox-overlay figure'),\n            npre = options.preload || 0,\n            k = [];\n\n        if (keep === undefined)\n            keep = -9;\n\n        for (var a = keep - npre; a <= keep + npre; a++)\n            k.push('bbox-figure-' + a);\n\n        for (var a = figs.length - 1; a >= 0; a--) {\n            var f = figs[a];\n            if (!has(k, f.getAttribute('id')))\n                f.parentNode.removeChild(f);\n        }\n    }\n\n    function lerr() {\n        var t;\n        try {\n            t = this.getAttribute('src');\n            t = uricom_dec(t.split('/').pop().split('?')[0]);\n        }\n        catch (ex) { }\n\n        t = 'Failed to open ' + (t?t:'file');\n        console.log('bb-ded', t);\n        t += '\\n\\nEither the file is corrupt, or your browser does not understand the file format or codec';\n\n        try {\n            t += \"\\n\\nerr#\" + this.error.code + \", \" + this.error.message;\n        }\n        catch (ex) { }\n \n        this.ded = esc(t);\n        if (this === vidimg())\n            toast.err(20, this.ded, 'bb-ded');\n    }\n\n    function loadImage(index, callback) {\n        var imageContainer = imagesElements[index];\n        var galleryItem = currentGallery[index];\n\n        if (typeof imageContainer === 'undefined' || typeof galleryItem === 'undefined')\n            return;  // out-of-bounds or gallery dirty\n\n        if (imageContainer.querySelector('img, video'))\n            // was loaded, cb and bail\n            return callback ? callback() : null;\n\n        // maybe unloaded video\n        while (imageContainer.firstChild)\n            imageContainer.removeChild(imageContainer.firstChild);\n\n        var imageElement = galleryItem.imageElement,\n            imageSrc = galleryItem.href || imageElement.href,\n            is_vid = re_v.test(imageSrc),\n            thumbnailElement = imageElement.querySelector('img, video'),\n            imageCaption = typeof options.captions === 'function' ?\n                options.captions.call(currentGallery, imageElement, index) :\n                imageElement.getAttribute('data-caption') || imageElement.title;\n\n        imageSrc = addq(imageSrc, 'cache');\n\n        if (is_vid && index != currentIndex)\n            return;  // no preload\n\n        var figure = mknod('figure', 'bbox-figure-' + index);\n        figure.innerHTML = '<div class=\"bbox-spinner\">' +\n            '<div class=\"bbox-double-bounce1\"></div>' +\n            '<div class=\"bbox-double-bounce2\"></div>' +\n            '</div>';\n\n        if (options.captions && imageCaption) {\n            var figcaption = mknod('figcaption', 'bbox-figcaption-' + index);\n            figcaption.innerHTML = imageCaption;\n            figure.appendChild(figcaption);\n        }\n        imageContainer.appendChild(figure);\n\n        var image = mknod(is_vid ? 'video' : 'img');\n        clmod(imageContainer, 'vid', is_vid);\n\n        bind(image, 'error', lerr);\n        bind(image, is_vid ? 'loadedmetadata' : 'load', function () {\n            // Remove loader element\n            qsr('#baguette-img-' + index + ' .bbox-spinner');\n            if (!options.async && callback)\n                callback();\n        });\n        image.setAttribute('src', imageSrc);\n        if (is_vid) {\n            image.volume = clamp(fcfg_get('vol', dvol / 100), 0, 1);\n            image.setAttribute('controls', 'controls');\n            image.setAttribute('playsinline', '1');\n            // ios ignores poster\n            image.onended = vidEnd;\n            image.onplay = image.onpause = ppHandler;\n        }\n        image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';\n        if (options.titleTag && imageCaption)\n            image.title = imageCaption;\n\n        figure.appendChild(image);\n\n        if (is_vid && window.afilt)\n            afilt.apply(undefined, image);\n\n        if (options.async && callback)\n            callback();\n    }\n\n    function ppHandler() {\n        var now = Date.now();\n        if (now - un_pp < 50) {\n            un_pp = 0;\n            return playpause();  // browser undid space hotkey\n        }\n        show_buttons(this.paused ? 0 : 1);\n    }\n\n    function showRightImage(e) {\n        ev(e);\n        var dir = options.readDirRtl ? -1 : 1;\n        return show(currentIndex + dir);\n    }\n\n    function showLeftImage(e) {\n        ev(e);\n        var dir = options.readDirRtl ? 1 : -1;\n        return show(currentIndex + dir);\n    }\n\n    function showNextImageIgnoreReadDir(e) {\n        ev(e);\n        return show(currentIndex + 1);\n    }\n\n    function showPreviousImageIgnoreReadDir(e) {\n        ev(e);\n        return show(currentIndex - 1);\n    }\n\n    function showFirstImage(e) {\n        if (e)\n            e.preventDefault();\n\n        return show(0);\n    }\n\n    function showLastImage(e) {\n        if (e)\n            e.preventDefault();\n\n        return show(currentGallery.length - 1);\n    }\n\n    function show(index, gallery) {\n        gallery = gallery || currentGallery;\n        if (!isOverlayVisible && index >= 0 && index < gallery.length) {\n            prepareOverlay(gallery, options);\n            showOverlay(index);\n            return true;\n        }\n\n        if (index < 0)\n            return bounceAnimation(options.readDirRtl ? 'right' : 'left');\n\n        if (index >= imagesElements.length)\n            return bounceAnimation(options.readDirRtl ? 'left' : 'right');\n\n        var orot;\n        try {\n            orot = vidimg().getAttribute('rot');\n            vid().pause();\n        }\n        catch (ex) { }\n\n        currentIndex = index;\n        loadImage(currentIndex, function () {\n            preloadNext(currentIndex);\n            preloadPrev(currentIndex);\n        });\n        updateOffset();\n\n        var im = vidimg();\n        if (im && im.ded)\n            toast.err(20, im.ded, 'bb-ded');\n        else if (toast.tag == 'bb-ded')\n            toast.hide();\n\n        if (orot && im.getAttribute('rot') === null)\n            rotn(orot / 90, 1);\n\n        if (options.animation == 'none')\n            unvid(vid());\n        else\n            setTimeout(function () {\n                unvid(vid());\n            }, 100);\n\n        unfig(index);\n\n        if (options.onChange)\n            options.onChange.call(currentGallery, currentIndex, imagesElements.length);\n\n        return true;\n    }\n\n    var prev_cw = 0, prev_ch = 0, unrot_timer = null;\n    function rotn(n, asap) {\n        var el = vidimg(),\n            orot = parseInt(el.getAttribute('rot') || 0),\n            frot = orot + (n || 0) * 90;\n\n        if (!frot && !orot)\n            return;  // reflow noop\n\n        var co = ebi('bbox-overlay'),\n            cw = co.clientWidth,\n            ch = co.clientHeight;\n\n        if (!n && prev_cw === cw && prev_ch === ch)\n            return;  // reflow noop\n\n        clmod(el, 'asap', asap);\n\n        prev_cw = cw;\n        prev_ch = ch;\n        var rot = frot,\n            iw = el.naturalWidth || el.videoWidth,\n            ih = el.naturalHeight || el.videoHeight,\n            magic = 4,  // idk, works in enough browsers\n            dl = el.closest('div').querySelector('figcaption a'),\n            vw = cw,\n            vh = ch - dl.offsetHeight + magic,\n            pmag = Math.min(vw / ih, vh / iw),\n            wmag = Math.min(vw / iw, vh / ih);\n\n        if (!options.bbzoom) {\n            pmag = Math.min(1, pmag);\n            wmag = Math.min(1, wmag);\n        }\n\n        while (rot < 0) rot += 360;\n        while (rot >= 360) rot -= 360;\n        var q = rot == 90 || rot == 270 ? 1 : 0,\n            mag = q ? pmag : wmag;\n\n        el.style.cssText = 'max-width:none; max-height:none; position:absolute; display:block; margin:0';\n        if (!orot) {\n            el.style.width = iw * wmag + 'px';\n            el.style.height = ih * wmag + 'px';\n            el.style.left = (vw - iw * wmag) / 2 + 'px';\n            el.style.top = (vh - ih * wmag) / 2 - magic + 'px';\n            q = el.offsetHeight;\n        }\n        el.style.width = iw * mag + 'px';\n        el.style.height = ih * mag + 'px';\n        el.style.left = (vw - iw * mag) / 2 + 'px';\n        el.style.top = (vh - ih * mag) / 2 - magic + 'px';\n        el.style.transform = 'rotate(' + frot + 'deg)';\n        el.setAttribute('rot', frot);\n        timer.add(rotn);\n        if (!rot) {\n            clearTimeout(unrot_timer);\n            unrot_timer = setTimeout(unrot, 300);\n        }\n    }\n    function rotl() {\n        rotn(-1);\n    }\n    function rotr() {\n        rotn(1);\n    }\n    function unrot() {\n        var el = vidimg(),\n            orot = el.getAttribute('rot'),\n            rot = parseInt(orot || 0);\n\n        while (rot < 0) rot += 360;\n        while (rot >= 360) rot -= 360;\n        if (rot || orot === null)\n            return;\n\n        clmod(el, 'nt', 1);\n        el.setAttribute('rot', 0);\n        el.removeAttribute(\"style\");\n        rot = el.offsetHeight;\n        clmod(el, 'nt');\n        timer.rm(rotn);\n    }\n\n    function vid() {\n        if (currentIndex >= imagesElements.length)\n            return;\n\n        return imagesElements[currentIndex].querySelector('video');\n    }\n\n    function vidimg() {\n        if (currentIndex >= imagesElements.length)\n            return;\n\n        return imagesElements[currentIndex].querySelector('img, video');\n    }\n\n    function playvid(play) {\n        if (!play) {\n            timer.rm(loopchk);\n            loopA = loopB = null;\n        }\n\n        var v = vid();\n        if (!v)\n            return;\n\n        v[play ? 'play' : 'pause']();\n        if (play && loopA !== null && v.currentTime < loopA)\n            v.currentTime = loopA;\n    }\n\n    function playpause() {\n        var v = vid();\n        if (v)\n            v[v.paused ? \"play\" : \"pause\"]();\n    }\n\n    function relseek(sec) {\n        var v = vid(),\n            t = v && v.currentTime;\n\n        if (isNum(t))\n            v.currentTime = t + sec;\n    }\n\n    function vidEnd() {\n        if (this == vid() && vnext)\n            showNextImageIgnoreReadDir();\n    }\n\n    function setloop(side) {\n        var v = vid();\n        if (!v)\n            return;\n\n        var t = v.currentTime;\n        if (side == 1) loopA = t;\n        if (side == 2) loopB = t;\n        if (side)\n            toast.inf(5, 'Loop' + (side == 1 ? 'A' : 'B') + ': ' + f2f(t, 2));\n\n        if (loopB !== null) {\n            timer.add(loopchk);\n            sethash(location.hash.slice(1).split('&')[0] + '&t=' + (loopA || 0) + '-' + loopB);\n        }\n    }\n\n    function loopchk() {\n        if (loopB === null)\n            return;\n\n        var v = vid();\n        if (!v || v.paused || v.currentTime < loopB)\n            return;\n\n        v.currentTime = loopA || 0;\n    }\n\n    function urltime(txt) {\n        url_ts = txt;\n    }\n\n    function mp_ctl() {\n        var v = vid();\n        if (!vmute && v && mp.au && !mp.au.paused) {\n            mp.fade_out();\n            resume_mp = true;\n        }\n        else if (resume_mp && (vmute || !v) && mp.au && mp.au.paused) {\n            mp.fade_in();\n            resume_mp = false;\n        }\n    }\n\n    function show_buttons(v) {\n        clmod(ebi('bbox-btns'), 'off', v);\n        clmod(btnPrev, 'off', v);\n        clmod(btnNext, 'off', v);\n    }\n\n    function bounceAnimation(direction) {\n        slider.className = options.animation == 'slideIn' ? 'bounce-from-' + direction : 'eog';\n        setTimeout(function () {\n            slider.className = '';\n        }, 300);\n        return false;\n    }\n\n    function updateOffset(noTransition) {\n        var dir = options.readDirRtl ? 1 : -1,\n            offset = dir * currentIndex * 100 + '%',\n            xform = slider.style.perspective !== undefined;\n\n        if (options.animation === 'fadeIn' && !noTransition) {\n            slider.style.opacity = 0;\n            setTimeout(function () {\n                xform ?\n                    slider.style.transform = 'translate3d(' + offset + ',0,0)' :\n                    slider.style.left = offset;\n                slider.style.opacity = 1;\n            }, 100);\n        } else {\n            xform ?\n                slider.style.transform = 'translate3d(' + offset + ',0,0)' :\n                slider.style.left = offset;\n        }\n        playvid(false);\n        var v = vid();\n        if (v) {\n            playvid(true);\n            v.muted = vmute;\n            v.loop = vloop;\n            if (url_ts) {\n                var seek = ('' + url_ts).split('-');\n                v.currentTime = seek[0];\n                if (seek.length > 1) {\n                    loopA = parseFloat(seek[0]);\n                    loopB = parseFloat(seek[1]);\n                    setloop();\n                }\n            }\n            bind(v, 'touchstart', vtouch, nonPassiveEvent);\n        }\n        selbg();\n        mp_ctl();\n        setVmode();\n\n        var el = vidimg();\n        if (el.getAttribute('rot'))\n            timer.add(rotn);\n        else\n            timer.rm(rotn);\n\n        var ctime = 0;\n        el.onclick = v ? null : function (e) {\n            var rc = e.target.getBoundingClientRect(),\n                x = e.clientX - rc.left,\n                fx = x / (rc.right - rc.left);\n\n            if (fx < 0.3)\n                return showLeftImage();\n\n            if (fx > 0.7)\n                return showRightImage();\n\n            show_buttons('t');\n\n            if (Date.now() - ctime <= 500 && !IPHONE)\n                tglfull();\n\n            ctime = Date.now();\n        };\n\n        var prev = QS('.full-image.vis');\n        if (prev)\n            clmod(prev, 'vis');\n\n        clmod(el.closest('div'), 'vis', 1);\n    }\n\n    function preloadNext(index) {\n        if (index - currentIndex >= options.preload)\n            return;\n\n        loadImage(index + 1, function () {\n            preloadNext(index + 1);\n        });\n    }\n\n    function preloadPrev(index) {\n        if (currentIndex - index >= options.preload)\n            return;\n\n        loadImage(index - 1, function () {\n            preloadPrev(index - 1);\n        });\n    }\n\n    function bind(element, event, callback, options) {\n        element.addEventListener(event, callback, options);\n    }\n\n    function unbind(element, event, callback, options) {\n        element.removeEventListener(event, callback, options);\n    }\n\n    function destroyPlugin() {\n        hideOverlay(undefined, true);\n        unbindEvents();\n        clearCachedData();\n        document.getElementsByTagName('body')[0].removeChild(ebi('bbox-overlay'));\n        data = {};\n        currentGallery = [];\n        currentIndex = 0;\n    }\n\n    return {\n        run: run,\n        show: show,\n        showNext: showRightImage,\n        showPrevious: showLeftImage,\n        relseek: relseek,\n        urltime: urltime,\n        playpause: playpause,\n        hide: hideOverlay,\n        destroy: destroyPlugin\n    };\n})();\n\nJ_BBX = 2;\n"
  },
  {
    "path": "copyparty/web/browser.css",
    "content": ":root {\n\tcolor-scheme: dark;\n\n\t--grid-sz: 10em;\n\t--grid-ln: 3;\n\t--nav-sz: 16em;\n\t--sbw: 0.5em;\n\t--sbh: 0.5em;\n\n\t--fg: #ccc;\n\t--fg-max: #fff;\n\t--fg2-max: #fff;\n\t--fg-weak: #bbb;\n\n\t--bg-u6: #4c4c4c;\n\t--bg-u5: #444;\n\t--bg-u4: #383838;\n\t--bg-u3: #333;\n\t--bg-u2: #2b2b2b;\n\t--bg-u1: #282828;\n\t--bg: #222;\n\t--bgg: var(--bg);\n\t--bg-d1: #1c1c1c;\n\t--bg-d2: #181818;\n\t--bg-d3: #111;\n\t--bg-max: #000;\n\n\t--tab-alt: #f5a;\n\t--row-alt: #282828;\n\n\t--scroll: #eb0;\n\t--sel-fg: var(--bg-d1);\n\t--sel-bg: var(--fg);\n\n\t--a: #fc5;\n\t--a-b: #c90;\n\t--a-hil: #fd9;\n\t--a-dark: #e70;\n\t--a-gray: #666;\n\n\t--btn-fg: var(--a);\n\t--btn-bg: rgba(128,128,128,0.15);\n\t--btn-h-fg: var(--a-hil);\n\t--btn-h-bg: #805;\n\t--btn-1-fg: #400;\n\t--btn-1-bg: var(--a);\n\t--btn-h-bs: var(--btn-bs);\n\t--btn-h-bb: var(--btn-bb);\n\t--btn-1-bs: var(--btn-bs);\n\t--btn-1-bb: var(--btn-bb);\n\t--btn-1h-fg: var(--btn-1-fg);\n\t--btn-1h-bg: #fe8;\n\t--btn-1h-bs: var(--btn-1-bs);\n\t--btn-1h-bb: var(--btn-1-bb);\n\t--chk-fg: var(--tab-alt);\n\t--txt-sh: var(--bg-d2);\n\t--txt-bg: var(--btn-bg);\n\n\t--op-aa-fg: var(--a);\n\t--op-aa-bg: var(--bg-d2);\n\t--op-a-sh: rgba(0,0,0,0.5);\n\n\t--u2-btn-b1: #999;\n\t--u2-sbtn-b1: #999;\n\t--u2-txt-bg: var(--bg-u5);\n\t--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));\n\t--u2-tab-b1: rgba(128,128,128,0.8);\n\t--u2-tab-1-fg: #fd7;\n\t--u2-tab-1-bg: linear-gradient(to bottom, #353, var(--bg) 80%);\n\t--u2-tab-1-b1: #7c5;\n\t--u2-tab-1-b2: #583;\n\t--u2-tab-1-sh: #280;\n\t--u2-b-fg: #fff;\n\t--u2-b1-bg: #c38;\n\t--u2-b2-bg: #d80;\n\t--u2-inf-bg: #07a;\n\t--u2-inf-b1: #0be;\n\t--u2-ok-bg: #380;\n\t--u2-ok-b1: #8e4;\n\t--u2-err-bg: #900;\n\t--u2-err-b1: #d06;\n\t--ud-b1: #888;\n\n\t--sort-1: #fb0;\n\t--sort-2: #d09;\n\n\t--sz-b: #aaa;\n\t--sz-k: #4ff;\n\t--sz-m: var(--tab-alt);\n\t--sz-g: var(--a);\n\t--sz-t: var(--sz-g);\n\t--sz-p: var(--sz-t);\n\n\t--srv-1: #aaa;\n\t--srv-2: #a73;\n\t--srv-3: #f4c;\n\t--srv-3b: rgba(255,68,204,0.6);\n\n\t--tree-bg: #2b2b2b;\n\n\t--g-play-bg: #750;\n\t--g-play-b1: #c90;\n\t--g-play-b2: #da4;\n\t--g-play-sh: #b83;\n\n\t--g-sel-fg: #fff;\n\t--g-sel-bg: #925;\n\t--g-sel-b1: #e39;\n\t--g-sel-sh: #b36;\n\t--g-fsel-bg: #d39;\n\t--g-fsel-b1: #f4a;\n\t--g-fsel-ts: #804;\n\t--g-dfg: var(--srv-3);\n\t--g-fg: var(--a-hil);\n\t--g-bg: var(--bg-u2);\n\t--g-b1: var(--bg-u4);\n\t--g-b2: var(--bg-u5);\n\t--g-g1: var(--bg-u2);\n\t--g-g2: var(--bg-u5);\n\t--g-f-bg: var(--bg-u4);\n\t--g-f-b1: var(--bg-u5);\n\t--g-f-fg: var(--a-hil);\n\t--g-sh: rgba(0,0,0,0.3);\n\n\t--f-sh1: 0.33;\n\t--f-sh2: 0.02;\n\t--f-sh3: 0.2;\n\t--f-h-b1: rgba(128,128,128,0.7);\n\n\t--f-play-bg: #fc5;\n\t--f-play-fg: #000;\n\t--f-sel-sh: #fc0;\n\t--f-gray: #999;\n\n\t--fm-off: #f6c;\n\t--mp-sh: var(--bg-d3);\n\t--mp-b-bg: rgba(0,0,0,0.2);\n\n\t--err-fg: #fff;\n\t--err-bg: #a20;\n\t--err-b1: #f00;\n\t--err-ts: #500;\n}\nhtml.y {\n\tcolor-scheme: light;\n\n\t--fg: #222;\n\t--fg-max: #000;\n\t--fg-weak: #555;\n\n\t--bg-d3: #fff;\n\t--bg-d2: #fff;\n\t--bg-d1: #fff;\n\t--bg: #eaeaea;\n\t--bg-u1: #fff;\n\t--bg-u2: #f7f7f7;\n\t--bg-u3: #eaeaea;\n\t--bg-u4: #fff;\n\t--bg-u5: #ccc;\n\t--bg-u6: #ddd;\n\t--bg-max: #fff;\n\n\t--tab-alt: #c07;\n\t--row-alt: #f2f2f2;\n\n\t--scroll: #490;\n\n\t--a: #06a;\n\t--a-b: #08b;\n\t--a-hil: #058;\n\t--a-gray: #bbb;\n\t--a-dark: #c0f;\n\n\t--btn-fg: #555;\n\t--btn-h-fg: #222;\n\t--btn-h-bg: #caf;\n\t--btn-1-fg: #fff;\n\t--btn-1-bg: #4a0;\n\t--btn-1h-bg: #5c0;\n\t--chk-fg: var(--fg);\n\t--txt-sh: #aaa;\n\t--txt-bg: rgba(255,255,255,0.6);\n\n\t--op-a-sh: #fff;\n\n\t--u2-txt-bg: var(--bg-max);\n\t--u2-tab-1-sh: #0ad;\n\t--u2-tab-1-b1: #09c;\n\t--u2-tab-1-b2: #05a;\n\t--u2-tab-1-fg: var(--fg-max);\n\t--u2-tab-1-bg: inherit;\n\t--ud-b1: #bbb;\n\n\t--sort-1: #059;\n\t--sort-2: #f5d;\n\n\t--sz-b: #777;\n\t--sz-k: #380;\n\n\t--srv-1: #555;\n\t--srv-2: #c83;\n\t--srv-3: #c0a;\n\n\t--tree-bg: #fff;\n\n\t--g-fg: var(--a);\n\t--g-bg: var(--bg-u2);\n\t--g-b1: var(--bg-u6);\n\t--g-b2: var(--bg-u6);\n\t--g-g1: var(--bg-u2);\n\t--g-g2: var(--bg-u5);\n\t--g-f-bg: var(--bg-u4);\n\t--g-f-b1: var(--bg-u5);\n\t--g-sh: rgba(0,0,0,0.07);\n\n\t--f-sh1: 0.3;\n\t--f-sh2: 0.5;\n\t--f-sh3: 0.02;\n\n\t--f-sel-sh: #e80;\n\n\t--fm-off: #c4a;\n\t--mp-sh: #bbb;\n\t--mp-b-bg: transparent;\n\n\ttext-shadow: none;\n}\nhtml.a {\n\t--op-aa-sh: 0 0 .2em var(--bg-d3) inset;\n\n\t--btn-bs: 0 0 .2em var(--bg-d3);\n}\nhtml.az {\n\t--btn-1-bs: 0 0 .1em var(--fg) inset;\n}\nhtml.ay {\n\t--op-aa-sh: 0 .1em .2em #ccc;\n\t--op-aa-bg: var(--bg-max);\n}\nhtml.b {\n\t--btn-bs: 0 .05em 0 var(--bg-d3) inset;\n\t--btn-1-bs: 0 .05em 0 var(--btn-1h-bg) inset;\n\n\t--tree-bg: var(--bg);\n\n\t--g-bg: var(--bg);\n\t--g-b1: var(--bg);\n\t--g-b2: var(--bg);\n\t--g-g1: var(--bg);\n\t--g-sh: rgba(0,0,0,0);\n\n\t--op-aa-bg: rgba(255,255,255,0.06);\n\n\t--u2-sbtn-b1: #fc0;\n\t--u2-txt-bg: transparent;\n\t--u2-tab-1-sh: var(--bg);\n\t--u2-b1-bg: rgba(128,128,128,0.15);\n\t--u2-b2-bg: var(--u2-b1-bg);\n\n\t--f-sh1: 0.1;\n\t--mp-b-bg: transparent;\n}\nhtml.bz {\n\t--fg: #cce;\n\t--fg-weak: #bbd;\n\n\t--bg-u5: #3b3f58;\n\t--bg-u4: #1e2130;\n\t--bg-u3: #1e2130;\n\t--bg-u1: #1e2130;\n\t--bg: #11121d;\n\t--bg-d1: #232536;\n\t--bg-d2: #34384e;\n\t--bg-d3: #34384e;\n\n\t--row-alt: #181a27;\n\n\t--a-b: #fb4;\n\n\t--btn-bg: #202231;\n\t--btn-h-bg: #2d2f45;\n\t--btn-1-bg: #eb6;\n\t--btn-1-fg: #000;\n\t--btn-1h-fg: #000;\n\t--btn-1h-bg: #ff9;\n\t--txt-sh: a;\n\n\t--u2-tab-b1: var(--bg-u5);\n\t--u2-tab-1-fg: var(--fg-max);\n\t--u2-tab-1-bg: var(--bg);\n\n\t--srv-1: #79b;\n\n\t--g-sel-bg: #ba2959;\n\t--g-fsel-bg: #e6336e;\n\n\t--f-h-b1: #34384e;\n\t--mp-sh: #11121d;\n\t/*--mp-b-bg: #2c3044;*/\n\t--f-play-bg: var(--btn-1-bg);\n}\nhtml.by {\n\t--bg: #f2f2f2;\n\n\t--row-alt: #f9f9f9;\n\n\t--scroll: var(--a);\n\n\t--btn-1-bg: #07a;\n\t--btn-1h-bg: var(--a-hil);\n\n\t--op-aa-bg: #fff;\n\n\t--u2-sbtn-b1: #c70;\n\t--u2-tab-1-b1: #999;\n\t--u2-tab-1-b2: #aaa;\n\t--u2-b-fg: #444;\n}\nhtml.c {\n\tfont-weight: bold;\n\n\t--fg: #fff;\n\t--fg-weak: #cef;\n\t--bg-u5: #409;\n\t--bg-u2: linear-gradient(-35deg, #fd7233, #cd27a0, #5d47a5 49.5%, #16e9fb 50%, #3b6cc8 50.4%, #0e51ac);\n\t--bg: #37235d;\n\t--bg-u3: #407;\n\n\t--a: #f9dc22;\n\t--a-gray: #0ae;\n\n\t--tab-alt: #6ef;\n\t--row-alt: #47237d;\n\t--scroll: #ff0;\n\n\t--btn-fg: #fff;\n\t--btn-bg: #9019bf;\n\t--btn-h-bg: #a039ff;\n\t--chk-fg: #d90;\n\n\t--op-aa-bg: #f9dd22;\n\n\t--srv-1: #ea0;\n\t--mp-b-bg: transparent;\n}\nhtml.cz {\n\t--bgg: var(--bg-u2);\n\n\t--sel-bg: var(--bg-u5);\n\t--sel-fg: var(--fg);\n\n\t--btn-bb: .2em solid #709;\n\t--btn-bs: 0 .1em .6em rgba(255,0,185,0.5);\n\t--btn-1-bb: .2em solid #e90;\n\t--btn-1-bs: 0 .1em .8em rgba(255,205,0,0.9);\n\n\t--sz-b: #ddd;\n\t--sz-k: #c9f;\n\n\t--srv-3: #fff;\n\n\t--u2-tab-b1: var(--bg-d3);\n\t--u2-tab-1-bg: a;\n}\nhtml.cy {\n\t--fg: #fff;\n\t--fg-weak: #fff;\n\t--bg: #ff0;\n\t--bg-u2: #f00;\n\t--bg-u3: #f00;\n\t--bg-u5: #999;\n\t--bg-d3: #f77;\n\t--bg-d2: #ff0;\n\n\t--sel-bg: #f77;\n\n\t--a: #fff;\n\t--a-hil: #fff;\n\t--a-h-bg: #000;\n\n\t--tab-alt: #f00;\n\t--row-alt: #fff;\n\t--scroll: #fff;\n\n\t--btn-bg: #000;\n\t--btn-fg: #ff0;\n\t--btn-h-fg: #fff;\n\t--btn-1-bg: #ff0;\n\t--btn-1-fg: #000;\n\t--btn-bs: 0 .25em 0 #f00;\n\t--chk-fg: #fd0;\n\n\t--txt-bg: #000;\n\t--srv-1: #f00;\n\t--srv-3: #fff;\n\t--op-aa-bg: #fff;\n\n\t--u2-b1-bg: #f00;\n\t--u2-b2-bg: #f00;\n\n\t--g-sel-fg: #fff;\n\t--g-sel-bg: #aaa;\n\t--g-fsel-bg: #aaa;\n}\nhtml.dz {\n\t--fg: #4d4;\n\t--fg-weak: #2a2;\n\n\t--bg-u6: #020;\n\t--bg-u5: #050;\n\t--bg-u4: #020;\n\t--bg-u3: #020;\n\t--bg-u2: #020;\n\t--bg-u1: #020;\n\t--bg: #010;\n\t--bg-d1: #000;\n\t--bg-d2: #020;\n\t--bg-d3: #000;\n\n\t--tab-alt: #6f6;\n\t--row-alt: #030;\n\n\t--scroll: #0f0;\n\n\t--a: #9f9;\n\t--a-b: #cfc;\n\t--a-hil: #cfc;\n\t--a-dark: #afa;\n\t--a-gray: #2a2;\n\n\t--btn-bg: rgba(64,128,64,0.15);\n\t--btn-h-bg: #050;\n\t--btn-1-fg: #000;\n\t--btn-1-bg: #4f4;\n\t--btn-1h-bg: #3f3;\n\t--btn-bs: 0 0 0 .1em #080 inset;\n\t--btn-1-bs: a;\n\n\t--u2-btn-b1: var(--fg-weak);\n\t--u2-sbtn-b1: var(--fg-weak);\n\t--u2-tab-b1: var(--fg-weak);\n\t--u2-tab-1-fg: #fff;\n\t--u2-tab-1-bg: linear-gradient(to bottom, #151, var(--bg) 80%);\n\t--u2-b1-bg: #3a3;\n\t--u2-b2-bg: #3a3;\n\n\t--sort-1: #fff;\n\t--sort-2: #3f3;\n\n\t--srv-1: #3e3;\n\t--srv-2: #1a1;\n\t--srv-3: #0f0;\n\t--srv-3b: #070;\n\n\t--tree-bg: #010;\n\n\t--g-sel-b1: #c37;\n\t--g-sel-sh: #b36;\n\t--g-fsel-b1: #d48;\n\n\t--f-h-b1: #3b3;\n\n\ttext-shadow: none;\n\tfont-family: 'scp', monospace, monospace;\n\tfont-family: var(--font-mono), 'scp', monospace, monospace;\n}\nhtml.dy {\n\t--fg: #000;\n\t--fg-max: #000;\n\t--fg-weak: #000;\n\n\t--bg-d3: #fff;\n\t--bg-d2: #fff;\n\t--bg-d1: #fff;\n\t--bg: #fff;\n\t--bg-u1: #fff;\n\t--bg-u2: #fff;\n\t--bg-u3: #fff;\n\t--bg-u4: #fff;\n\t--bg-u5: #fff;\n\t--bg-u6: #fff;\n\t--bg-max: #fff;\n\n\t--tab-alt: #000;\n\t--row-alt: #eee;\n\n\t--scroll: #000;\n\n\t--a: #000;\n\t--a-b: #000;\n\t--a-hil: #000;\n\t--a-gray: #bbb;\n\t--a-dark: #000;\n\n\t--btn-fg: #000;\n\t--btn-h-fg: #000;\n\t--btn-h-bg: #fff;\n\t--btn-1-fg: #fff;\n\t--btn-1-bg: #000;\n\t--btn-1h-bg: #555;\n\t--chk-fg: a;\n\t--txt-sh: a;\n\t--txt-bg: a;\n\n\t--op-a-sh: a;\n\n\t--u2-txt-bg: a;\n\t--u2-tab-1-sh: a;\n\t--u2-tab-1-b1: a;\n\t--u2-tab-1-b2: a;\n\t--u2-tab-1-fg: a;\n\t--u2-tab-1-bg: a;\n\t--u2-b1-bg: #000;\n\t--u2-b2-bg: #000;\n\n\t--ud-b1: a;\n\n\t--sort-1: a;\n\t--sort-2: a;\n\n\t--srv-1: a;\n\t--srv-2: a;\n\t--srv-3: a;\n\t--srv-3b: a;\n\n\t--tree-bg: #fff;\n\n\t--g-sel-bg: #000;\n\t--g-fsel-bg: #444;\n\t--g-fsel-ts: #000;\n\t--g-fg: a;\n\t--g-bg: a;\n\t--g-b1: a;\n\t--g-b2: a;\n\t--g-g1: a;\n\t--g-g2: a;\n\t--g-f-bg: a;\n\t--g-f-b1: a;\n\t--g-sh: a;\n\n\t--f-sh1: a;\n\t--f-sh2: a;\n\t--f-sh3: a;\n\n\t--f-sel-sh: #000;\n\n\t--fm-off: a;\n\t--mp-sh: a;\n\t--mp-b-bg: #fff;\n}\n* {\n\tline-height: 1.2em;\n}\n::selection {\n\tcolor: var(--sel-fg);\n\tbackground: var(--sel-bg);\n\ttext-shadow: none;\n}\nhtml,body,tr,th,td,#files,a,#blogout {\n\tcolor: inherit;\n\tbackground: none;\n\tfont-weight: inherit;\n\tfont-size: inherit;\n\tpadding: 0;\n\tborder: none;\n}\nhtml {\n\tcolor: var(--fg);\n\tbackground: var(--bgg);\n\tfont-family: sans-serif;\n\tfont-family: var(--font-main), sans-serif;\n\ttext-shadow: 1px 1px 0px var(--bg-max);\n}\nhtml, body {\n\tmargin: 0;\n\tpadding: 0;\n}\npre, code, tt, #doc, #doc>code {\n\tfont-family: 'scp', monospace, monospace;\n\tfont-family: var(--font-mono), 'scp', monospace, monospace;\n}\n.ayjump {\n\tposition: fixed;\n\toverflow: hidden;\n\twidth: 0;\n\theight: 0;\n\tleft: -10em;\n\tcolor: var(--bg);\n}\nhtml .ayjump:focus {\n\tz-index: 80386;\n\tcolor: #fff;\n\tcolor: var(--a-hil);\n\tbackground: #069;\n\tbackground: var(--bg-u2);\n\tborder: .2em solid var(--a);\n\tbox-shadow: none;\n\toutline: none;\n\twidth: auto;\n\theight: auto;\n\ttop: .5em;\n\tleft: .5em;\n\tpadding: .5em .7em;\n}\n#path,\n#path * {\n\tfont-size: 1em;\n}\n#path {\n\tcolor: var(--fg);\n\ttext-shadow: 1px 1px 0 var(--bg-max);\n\tfont-weight: normal;\n\tdisplay: inline-block;\n\tpadding: .35em .5em .2em .5em;\n\tmargin: 1.3em 0 -.2em 0;\n\tfont-size: 1.4em;\n}\nhtml.y #path {\n\ttext-shadow: none;\n}\n#path #entree {\n\tmargin-left: -.7em;\n}\n#files {\n\tz-index: 1;\n\ttop: -.3em;\n\tborder-spacing: 0;\n\tposition: relative;\n}\n#files tbody a {\n\tdisplay: block;\n\tpadding: .5em 0;\n\tmargin: -.3em 0;\n\tscroll-margin-top: 45vh;\n}\n#files tr {\n\tscroll-margin-top: 25vh;\n\tscroll-margin-bottom: 20vh;\n}\n#files tbody div a {\n\tcolor: var(--tab-alt);\n}\na, #blogout, #files tbody div a:last-child {\n\tcolor: var(--a);\n\tpadding: .2em;\n\ttext-decoration: none;\n}\n#blogout {\n\tmargin: -.2em;\n}\n#blogout:hover,\na:hover {\n\tcolor: var(--a-hil);\n\tbackground: var(--a-h-bg);\n}\n#files a:hover {\n\tcolor: var(--fg-max);\n\tbackground: var(--bg-d3);\n\ttext-decoration: underline;\n}\n#files thead th {\n\tposition: sticky;\n\ttop: -1px;\n}\n#files thead a {\n\tcolor: var(--f-gray);\n\tfont-weight: normal;\n}\n.s0:after,\n.s1:after {\n\tcontent: '⌄';\n\tmargin-left: -.15em;\n}\n.s0r:after,\n.s1r:after {\n\tcontent: '⌃';\n\tmargin-left: -.15em;\n}\n.s0:after,\n.s0r:after {\n\tcolor: var(--sort-1);\n}\n.s1:after,\n.s1r:after {\n\tcolor: var(--sort-2);\n}\n#files thead th:after {\n\tmargin-right: -.5em;\n}\n#files tbody tr:hover td,\n#files tbody tr:hover td+td {\n\tbackground: var(--bg-d1);\n\tbox-shadow: 0 1px 0 var(--bg-u5) inset, 0 -1px 0 var(--bg-u5) inset;\n}\n#files thead th {\n\tpadding: .3em;\n\tbackground: var(--bg);\n\tborder-bottom: 1px solid var(--f-h-b1);\n\tcursor: pointer;\n}\nhtml.y #files thead th {\n\tbox-shadow: 0 1px 0 rgba(0,0,0,0.12);\n}\nhtml #files.hhpick thead th {\n\tcolor: #f7d;\n\tbackground: #000;\n\tbox-shadow: .1em .2em 0 #f6c inset, -.1em -.1em 0 #f6c inset;\n}\n#files td {\n\tmargin: 0;\n\tpadding: .3em .5em;\n\tbackground: var(--bg);\n\tmax-width: var(--file-td-w);\n\tword-wrap: break-word;\n\toverflow: hidden;\n}\n#files tr.fade a {\n\tcolor: #999;\n\tcolor: rgba(255, 255, 255, 0.4);\n\tfont-style: italic;\n}\nhtml.y #files tr.fade a {\n\tcolor: #999;\n\tcolor: rgba(0, 0, 0, 0.4);\n}\n#files tr:nth-child(2n) td {\n\tbackground: var(--row-alt);\n}\n#files td+td {\n\tbox-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;\n}\n#files tr:nth-child(2n+1) td+td {\n\tbox-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;\n}\n#files td:first-child {\n\tborder-radius: .25em 0 0 .25em;\n\twhite-space: nowrap;\n}\n#files td:last-child {\n\tborder-radius: 0 .25em .25em 0;\n}\n#files tbody td:nth-child(3) {\n\tfont-family: 'scp', monospace, monospace;\n\tfont-family: var(--font-mono), 'scp', monospace, monospace;\n\ttext-align: right;\n\tpadding-right: 1em;\n\twhite-space: nowrap;\n}\n#files tbody td:first-child {\n\tcolor: var(--f-gray);\n\ttext-align: center;\n}\n#files tbody tr td:last-child {\n\twhite-space: nowrap;\n}\n#files span.fsz_B { color: var(--sz-b); }\n#files span.fsz_K { color: var(--sz-k); }\n#files span.fsz_M { color: var(--sz-m); }\n#files span.fsz_G { color: var(--sz-g); }\n#files span.fsz_T { color: var(--sz-t); }\n#files span.fsz_P { color: var(--sz-p); }\nhtml.y #files span.fsz_G,\nhtml.y #files span.fsz_T,\nhtml.y #files span.fsz_P { font-weight: bold }\n#files thead th[style] {\n\twidth: auto !important;\n}\n#files .srch_hdr a {\n\tdisplay: inline;\n}\n#path a {\n\tpadding: 0 .35em;\n\tposition: relative;\n\tz-index: 1;\n\t/* ie: */\n\tborder-bottom: .1em solid #777\\9;\n\tmargin-right: 1em\\9;\n}\n#path a:first-child {\n\tpadding-left: .8em;\n}\n#path i {\n\twidth: 1.05em;\n\theight: 1.05em;\n\tmargin: -.5em .15em -.15em -.7em;\n\tdisplay: inline-block;\n\tborder: 1px solid rgba(255,224,192,0.3);\n\tborder-width: .05em .05em 0 0;\n\ttransform: rotate(45deg);\n\tbackground: linear-gradient(45deg, rgba(0,0,0,0) 40%, rgba(0,0,0,0.25) 75%, rgba(0,0,0,0.35));\n}\nhtml.y #path i {\n\tbackground: none;\n\tborder-color: rgba(0,0,0,0.2);\n\tborder-width: .1em .1em 0 0;\n}\n#path a:hover {\n\tcolor: var(--fg-max);\n\tbackground: linear-gradient(90deg, rgba(0,0,0,0), rgba(0,0,0,0.2), rgba(0,0,0,0));\n}\nhtml.y #path a:hover {\n\tbackground: none;\n}\n.logue {\n\tpadding: .2em 0;\n\tposition: relative;\n\tz-index: 1;\n}\n.logue.hidden,\n.logue:empty {\n\tdisplay: none;\n}\n.logue.raw {\n\twhite-space: pre;\n\tfont-family: 'scp', 'consolas', monospace;\n\tfont-family: var(--font-mono), 'scp', 'consolas', monospace;\n}\n#doc>iframe,\n.logue>iframe {\n\tbackground: var(--bgg);\n\tborder: 1px solid var(--bgg);\n\tborder-width: 0 .3em 0 .3em;\n\tborder-radius: .5em;\n\tvisibility: hidden;\n\tmargin: 0 -.3em;\n\twidth: 100%;\n\theight: 0;\n}\n#doc>iframe.focus,\n.logue>iframe.focus {\n\tbox-shadow: 0 0 .1em .1em var(--a);\n}\n#pro.logue>iframe {\n\theight: 100vh;\n}\n#pro.logue {\n\tmargin-bottom: .8em;\n}\n#epi.logue {\n\tmargin: .8em 0;\n}\n#epi.logue.mdo:before {\n\tcontent: 'README.md';\n\ttext-align: center;\n\tdisplay: block;\n\tmargin-top: -1.5em;\n}\n#epi.logue.mdo {\n\tborder-top: 1px solid var(--bg-u5);\n\tmargin-top: 2.5em;\n}\n.mdo>h1:first-child,\n.mdo>h2:first-child,\n.mdo>h3:first-child {\n\tmargin-top: 1.5rem;\n}\n.mdo {\n\tmax-width: 52em;\n}\n.mdo.sb,\n.logue.mdo>iframe {\n\tmax-width: 54em;\n}\n.mdo,\n.mdo * {\n\tline-height: 1.5em;\n}\n#srv_info,\n#srv_info2,\n#acc_info {\n\tcolor: var(--srv-2);\n\tbackground: var(--bg);\n\twhite-space: nowrap;\n}\n#srv_info,\n#acc_info {\n\tposition: absolute;\n\tfont-size: .8em;\n\ttop: .5em;\n}\n#srv_info {\n\tleft: 2em;\n\tpadding-right: .5em;\n}\n#acc_info,\n#srv_info span,\n#srv_info2 span {\n\tcolor: var(--srv-1);\n}\n#srv_info2 a {\n\tpadding: 0;\n}\n#srv_info2 {\n\tdisplay: none;\n}\n#acc_info {\n\tright: 2em;\n}\n#acc_info > span:not([id]) {\n\tcolor: var(--srv-1);\n\tmargin-right: .6em;\n}\n#acc_info span.warn {\n\tcolor: var(--srv-3);\n\tborder-bottom: 1px solid var(--srv-3b);\n}\n#flogout {\n\tdisplay: inline;\n}\nhtml.dz #flogout {\n\tmargin-left: 1em;\n}\n#goh+span {\n\tcolor: var(--bg-u5);\n\tpadding-left: .5em;\n\tmargin-left: .5em;\n\tborder-left: .2em solid var(--bg-u5);\n}\n#repl {\n\tpadding: .33em;\n}\n#files a.doc {\n\tcolor: var(--a-gray);\n}\n#files a.doc.bri {\n\tcolor: var(--tab-alt);\n}\n#files a.play {\n\tcolor: var(--a-dark);\n\tpadding: .3em;\n\tmargin: -.3em;\n}\n#files tbody tr.play td,\n#files tbody tr.play td+td,\n#files tbody tr.play div a {\n\tbackground: #fc0;\n\tbackground: var(--f-play-bg);\n\tcolor: var(--f-play-fg);\n\ttext-shadow: none;\n}\n#files tbody tr.play a {\n\tcolor: inherit;\n}\n#files tbody tr.play a:hover {\n\tcolor: var(--btn-1h-fg);\n\tbackground: var(--btn-1h-bg);\n\tbox-shadow: var(--btn-1h-bs);\n\tborder-bottom: var(--btn-1h-bb);\n}\n#ggrid {\n\tmargin: -.2em -.5em;\n}\n#ggrid>a>span {\n\toverflow: hidden;\n\tdisplay: block;\n\tdisplay: -webkit-box;\n\tline-clamp: var(--grid-ln);\n\t-webkit-line-clamp: var(--grid-ln);\n\t-webkit-box-orient: vertical;\n\tpadding-top: .3em;\n}\n#ggrid>a {\n\tdisplay: inline-block;\n\twidth: 10em;\n\twidth: var(--grid-sz);\n\tvertical-align: top;\n\toverflow-wrap: break-word;\n\tborder-radius: .3em;\n\tpadding: .3em;\n\tmargin: .5em;\n\tcolor: var(--g-fg);\n\tbackground: var(--g-bg);\n\tborder: 1px solid var(--g-b1);\n\tborder-top: 1px solid var(--g-b2);\n\tbox-shadow: 0 .1em .3em var(--g-sh);\n}\n#ggrid>a:focus,\n#ggrid>a:hover {\n\tcolor: var(--g-f-fg);\n\tbackground: var(--g-f-bg);\n\tborder-color: var(--g-f-b1);\n\tbox-shadow: 0 .1em .3em var(--g-sh);\n}\n#ggrid>a img {\n\tborder-radius: .2em;\n\tmax-width: 10em;\n\tmax-width: var(--grid-sz);\n\tmax-height: 8em;\n\tmax-height: calc(var(--grid-sz)/1.25);\n\tmargin: 0 auto;\n\tdisplay: block;\n}\n#ggrid.nocrop>a img {\n\tmax-height: 20em;\n\tmax-height: calc(var(--grid-sz)*2);\n}\n#ggrid>a.dir:before {\n\tcontent: '📂';\n}\n#ggrid>a.dir>span {\n\tcolor: var(--g-dfg);\n}\n#ggrid>a.au:before {\n\tcontent: '▶';\n}\n#ggrid>a:before {\n\tdisplay: block;\n\tposition: absolute;\n\tpadding: .3em 0;\n\tmargin: -.4em;\n\ttext-shadow: 0 0 .1em var(--bg-max);\n\tbackground: linear-gradient(135deg,rgba(255,255,255,0) 50%,rgba(255,255,255,0.2));\n\tborder-radius: .3em;\n\tfont-size: 2em;\n\ttransition: font-size .15s, margin .15s;\n}\n#ggrid>a:focus:before,\n#ggrid>a:hover:before {\n\tfont-size: 2.5em;\n\tmargin: -.2em;\n}\n#ggrid>a[tt] {\n\tbackground: linear-gradient(135deg, var(--g-g1) 95%, var(--g-g2) 95%);\n}\n#ggrid>a[tt]:focus,\n#ggrid>a[tt]:hover {\n\tbackground: var(--g-f-bg);\n}\n#ggrid>a.play,\n#ggrid>a[tt].play {\n\tcolor: var(--g-sel-fg);\n\tbackground: #fc0;\n\tbackground: var(--g-play-bg);\n\tborder-color: var(--g-play-b1);\n\tborder-top: 1px solid var(--g-play-b2);\n\tbox-shadow: 0 .1em 1.2em var(--g-play-sh);\n}\n#files tbody tr.sel td,\n#files tbody tr.sel span,\n#ggrid>a.sel,\n#ggrid>a[tt].sel {\n\tcolor: var(--g-sel-fg);\n\tbackground: #f3c;\n\tbackground: var(--g-sel-bg);\n\tborder-color: var(--g-sel-b1);\n}\n#ggrid>a.sel>span {\n\tcolor: var(--g-sel-fg);\n}\n#ggrid>a.sel,\n#ggrid>a[tt].sel {\n\tborder-top: 1px solid var(--g-fsel-b1);\n\tbox-shadow: 0 .1em 1.2em var(--g-sel-sh);\n\ttransition: all 0.2s cubic-bezier(.2, 2.2, .5, 1); /* https://cubic-bezier.com/#.4,2,.7,1 */\n}\n#files tbody tr.sel:hover td,\n#files tbody tr.sel:focus td,\n#ggrid>a.sel:hover,\n#ggrid>a.sel:focus {\n\tcolor: var(--g-sel-fg);\n\tbackground: var(--g-fsel-bg);\n\tborder-color: var(--g-fsel-b1);\n\ttext-shadow: 1px 1px 0 var(--g-fsel-ts);\n}\n#ggrid>a.sel img,\n#ggrid>a.play img {\n\topacity: .7;\n\tfilter: contrast(130%) brightness(107%);\n}\n#ggrid>a.sel img {\n\tbox-shadow: 0 0 1em var(--g-sel-sh);\n}\n#ggrid>a.play img {\n\tbox-shadow: 0 0 1em var(--g-play-sh);\n}\n#ggrid a {\n\tscroll-margin-top: 25vh;\n\tscroll-margin-bottom: 20vh;\n}\n#files tr.sel a,\n#files tr.sel a.play {\n\tcolor: var(--fg2-max);\n}\n#files tr.sel a:hover {\n\tcolor: var(--fg-max);\n\ttext-shadow: none;\n}\n#files tr.sel a.play.act {\n\ttext-shadow: 0 0 1px var(--fg2-max);\n}\n#files tr:focus {\n\toutline: none;\n\tposition: relative;\n}\n#files tr:focus td+td {\n\tbackground: var(--bg-d3);\n\tbox-shadow: 0 .2em 0 var(--f-sel-sh), 0 -.2em 0 var(--f-sel-sh);\n}\n#files tr:focus:not(.play):not(.sel) td:first-child {\n\tbackground: var(--bg-d3);\n\tbox-shadow: -.2em .2em 0 var(--f-sel-sh), -.2em -.2em 0 var(--f-sel-sh);\n}\n#player {\n\tdisplay: none;\n}\n#widget {\n\tposition: fixed;\n\tfont-size: 1.4em;\n\tleft: 0;\n\tright: 0;\n\tbottom: -6em;\n\theight: 6em;\n\twidth: 100%;\n\tz-index: 3;\n\ttouch-action: none;\n}\n#widget.anim {\n\ttransition: bottom 0.15s;\n}\n#widget.open {\n\tbox-shadow: 0 0 1em rgba(0,48,64,0.2);\n\tbottom: 0;\n}\nhtml.y #widget.open {\n\tborder-top: .2em solid var(--bg-u2);\n}\n#widgeti {\n\tposition: relative;\n\tz-index: 10;\n\twidth: 100%;\n\theight: 100%;\n}\n#fshr,\n#wtgrid,\n#wtico {\n\tposition: relative;\n\tfont-size: .9em;\n\ttop: -.04em;\n}\n#wtgrid {\n\tfont-size: .75em;\n\tpadding: .1em;\n\ttop: -.12em;\n}\n#wtico {\n\tcursor: pointer;\n}\n@keyframes spin {\n\t100% {transform: rotate(360deg)}\n}\n@keyframes fadein {\n\t0% {opacity: 0}\n\t100% {opacity: 1}\n}\n#wtoggle {\n\tposition: absolute;\n\twhite-space: nowrap;\n\ttop: -1em;\n\tright: 0;\n\theight: 1em;\n\tfont-size: 2em;\n\tline-height: 1em;\n\ttext-align: center;\n\ttext-shadow: none;\n\tbox-shadow: 0 0 .5em var(--mp-sh);\n\tborder-radius: .3em 0 0 0;\n\tpadding: 0 0 0 .1em;\n\tcolor: var(--fg-max);\n}\n#wtoggle,\n#widgeti {\n\tbackground: #fff;\n\tbackground: var(--bg-u3);\n}\n#wfs, #wfm, #wzip, #wnp, #wm3u {\n\tdisplay: none;\n}\n#wfs, #wzip, #wnp, #wm3u {\n\tmargin-right: .2em;\n\tpadding-right: .2em;\n\tborder: 1px solid var(--bg-u5);\n\tborder-width: 0 .1em 0 0;\n}\n#wzip1 {\n\tmargin-right: .2em;\n}\n#wfm.act+#wzip1+#wzip,\n#wfm.act+#wzip1+#wzip+#wnp {\n\tmargin-left: .2em;\n\tpadding-left: .2em;\n\tborder-left-width: .1em;\n}\n#wfs.act,\n#wfm.act {\n\tdisplay: inline-block;\n}\n#wtoggle,\n#wtoggle * {\n\tline-height: 1em;\n}\n#wtoggle.sel #wzip,\n#wtoggle.m3u #wm3u,\n#wtoggle.np #wnp {\n\tdisplay: inline-block;\n}\n#wtoggle.sel #wzip1,\n#wtoggle.sel.np #wnp {\n\tdisplay: none;\n}\n#wfm a,\n#wnp a,\n#wm3u a,\n#zip1,\n#wzip a {\n\tfont-size: .5em;\n\tpadding: 0 .3em;\n\tmargin: -.3em .1em;\n\tposition: relative;\n\tdisplay: inline-block;\n}\n#zip1 {\n\tfont-size: .38em;\n}\n#wm3u a {\n\tmargin: -.2em .1em;\n\tfont-size: .45em;\n}\n#wfs {\n\tfont-size: .36em;\n\ttext-align: right;\n\tline-height: 1.3em;\n\tpadding: 0 .3em 0 0;\n\tborder-width: 0 .25em 0 0;\n}\n#wfm span,\n#wm3u span,\n#zip1 span,\n#wnp span {\n\tfont-size: .6em;\n\tdisplay: block;\n}\n#zip1 span {\n\tfont-size: .9em;\n}\n#wnp span {\n\tfont-size: .7em;\n}\n#wm3u span {\n\tfont-size: .77em;\n\tpadding-top: .2em;\n}\n#wfm a:not(.en) {\n\topacity: .3;\n\tcolor: var(--fm-off);\n}\n#wfm a.hide {\n\tdisplay: none;\n}\n#files tbody tr.fcut td,\n#ggrid>a.fcut {\n\tanimation: fcut .5s ease-out;\n}\n@keyframes fcut {\n\t0% {opacity:0}\n\t100% {opacity:1}\n}\n#ggrid>a.glow {\n\tanimation: gexit .6s ease-out;\n}\n@keyframes gexit {\n\t0% {box-shadow: 0 0 0 2em var(--a)}\n\t100% {box-shadow: 0 0 0em 0em var(--a)}\n}\n#wzip a {\n\tfont-size: .4em;\n\tmargin: -.3em .1em;\n}\n#wtoggle.sel .l1 {\n\ttop: -.6em;\n\tpadding: .4em .3em;\n}\n#barpos,\n#barbuf {\n\tposition: absolute;\n\tbottom: 1em;\n\tleft: 1em;\n\theight: 2em;\n\tborder-radius: 9em;\n\twidth: calc(100% - 2em);\n}\n#barbuf {\n\tbackground: var(--mp-b-bg);\n\tz-index: 21;\n}\n#barpos {\n\tbox-shadow: -.03em -.03em .7em rgba(0,0,0,0.5) inset;\n\tz-index: 22;\n}\n#pctl {\n\tposition: absolute;\n\ttop: .5em;\n\tleft: 1em;\n}\n#pctl a {\n\tdisplay: inline-block;\n\tfont-size: 1.25em;\n\twidth: 1.3em;\n\theight: 1.2em;\n\ttext-align: center;\n\tborder-radius: .3em;\n}\n#pvol {\n\tposition: absolute;\n\ttop: .7em;\n\tright: 1em;\n\theight: 1.6em;\n\tborder-radius: 9em;\n\tmax-width: 12em;\n\twidth: calc(100% - 10.5em);\n\tbackground: rgba(0,0,0,0.2);\n}\n#widget.cmp {\n\theight: 1.6em;\n\tbottom: -1.6em;\n}\n#widget.cmp.open {\n\tbottom: 0;\n}\n#widget.cmp #wtoggle {\n\tfont-size: 1.2em;\n}\n#widget.cmp #fshr,\n#widget.cmp #wtgrid {\n\tdisplay: none;\n}\n#widget.cmp #pctl {\n\ttop: 0;\n\tleft: 0;\n\tfont-size: .75em;\n}\n#widget.cmp #pctl a {\n\tmargin: 0;\n}\n#widget.cmp #barpos,\n#widget.cmp #barbuf {\n\theight: 1.6em;\n\twidth: calc(100% - 11em);\n\tborder-radius: 0;\n\tleft: 5em;\n\ttop: 0;\n}\n#widget.cmp #pvol {\n\ttop: 0;\n\tright: 0;\n\tmax-width: 5.8em;\n\tborder-radius: 0;\n}\n.opview {\n\tdisplay: none;\n}\n.opview.act {\n\tdisplay: block;\n}\n#ops a {\n\tcolor: var(--a);\n\ttext-shadow: 1px 1px 1px var(--op-a-sh);\n\tfont-size: 1.5em;\n\tpadding: .25em .4em;\n\tmargin: 0;\n}\n#ops a.act {\n\tcolor: #fff;\n\tcolor: var(--op-aa-fg);\n\tbackground: #000;\n\tbackground: var(--op-aa-bg);\n\tborder-radius: 0 0 .2em .2em;\n\tborder-bottom: .3em solid var(--a-b);\n\tbox-shadow: var(--op-aa-sh);\n}\n#ops a svg {\n\twidth: 1.75em;\n\theight: 1.75em;\n\tmargin: -.5em -.3em;\n}\nhtml.y #ops svg circle {\n\tstroke: black;\n}\n#ops {\n\tpadding: .3em .6em;\n\twhite-space: nowrap;\n}\n#noie {\n\tcolor: #b60;\n\tmargin: 0 0 0 .5em;\n}\n.opbox {\n\tpadding: .5em;\n\tborder-radius: 0 .3em .3em 0;\n\tborder-width: 1px 1px 1px 0;\n\tmax-width: 41em;\n\tmax-width: min(41em, calc(100% - 2.6em));\n}\n.opbox input {\n\tposition: relative;\n\tmargin: .5em;\n}\n#op_cfg input[type=text] {\n\ttop: -.3em;\n}\n.opview select,\n.opview input[type=text] {\n\tcolor: var(--fg);\n\tbackground: var(--txt-bg);\n\tborder: none;\n\tbox-shadow: 0 0 2px var(--txt-sh);\n\tborder-bottom: 1px solid #999;\n\tborder-color: var(--a);\n\tborder-radius: .2em;\n\tpadding: .2em .3em;\n}\n.opview select {\n\tpadding: .3em;\n\tmargin: .2em .4em;\n\tbackground: var(--bg-u3);\n}\n.opview input.err {\n\tcolor: var(--err-fg);\n\tbackground: var(--err-bg);\n\tborder-color: var(--err-b1);\n\tbox-shadow: 0 0 .7em var(--err-b1);\n\ttext-shadow: 1px 1px 0 var(--err-ts);\n\toutline: none;\n}\ninput[type=\"checkbox\"]+label {\n\tcolor: var(--chk-fg);\n}\ninput[type=\"radio\"]:checked+label,\ninput[type=\"checkbox\"]:checked+label {\n\tcolor: #0e0;\n\tcolor: var(--btn-1-bg);\n}\ninput[type=\"checkbox\"]:checked+label {\n\tbox-shadow: var(--btn-1-bs);\n\tborder-bottom: var(--btn-1-bb);\n}\nhtml.dz input {\n\tfont-family: 'scp', monospace, monospace;\n\tfont-family: var(--font-mono), 'scp', monospace, monospace;\n}\n.opwide div>span>input+label {\n\tpadding: .3em 0 .3em .3em;\n\tmargin: 0 0 0 -.3em;\n\tcursor: pointer;\n}\n.opview input.i {\n\twidth: calc(100% - 16.2em);\n}\ninput.drc_v,\ninput.eq_gain,\ninput.ssconf_v {\n\twidth: 3em;\n\ttext-align: center;\n\tmargin: 0 .6em;\n}\n#audio_drc table,\n#audio_eq table {\n\tborder-collapse: collapse;\n}\n#audio_drc td,\n#audio_eq td,\n#audio_ss td {\n\ttext-align: center;\n}\n#audio_eq a.eq_step {\n\tfont-size: 1.5em;\n\tdisplay: block;\n\tpadding: 0;\n}\n#au_ss,\n#au_drc,\n#au_eq {\n\tdisplay: block;\n\tmargin-top: .5em;\n\tpadding: 1.3em .3em;\n}\n#au_ss,\n#au_drc {\n\tpadding: .4em .3em;\n}\n#au_ss.on {\n    width: 3em;\n\theight: 1em;\n\toverflow: hidden;\n}\n#ico1 {\n\tcursor: pointer;\n}\n\n\n\n#srch_form {\n\tpadding: 0 .5em .5em 0;\n}\n#srch_form table {\n\tdisplay: inline-block;\n}\n#srch_form td {\n\tpadding: .6em .6em;\n}\n#srch_form td:first-child {\n\twidth: 3em;\n\tpadding-right: .2em;\n\ttext-align: right;\n}\n#srch_form:not(.tags) #tsrch_tags,\n#srch_form:not(.tags) #tsrch_adv {\n\tdisplay: none;\n}\n#op_search input {\n\tmargin: .1em 0 0 0;\n}\n#srch_q {\n\twhite-space: pre;\n\tcolor: var(--a-b);\n\tmin-height: 1em;\n\tmargin: .2em 0 -1em 1.6em;\n}\n#srch_q.err {\n\tcolor: var(--srv-3);\n}\n#tq_raw {\n\twidth: calc(100% - 2em);\n\tmargin: .3em 0 0 1.4em;\n}\n@media (max-width: 130em) { #srch_form.tags #tq_raw { width: calc(100% - 34em) } }\n@media (max-width: 95em) { #srch_form.tags #tq_raw { width: calc(100% - 2em) } }\n#tq_raw td+td {\n\twidth: 100%;\n}\n#op_search #q_raw {\n\twidth: 100%;\n\tdisplay: block;\n}\n#files td div span {\n\tcolor: var(--fg-max);\n\tpadding: 0 .4em;\n\tfont-weight: bold;\n\tfont-style: italic;\n}\n#files td div a:hover {\n\tbackground: var(--bg-u5);\n\tcolor: var(--fg-max);\n}\n#files td div a {\n\tdisplay: inline-block;\n\twhite-space: nowrap;\n}\n#files td div a:last-child {\n\twidth: 100%;\n}\n#files td div {\n\tborder-collapse: collapse;\n\twidth: 100%;\n}\n#wrap {\n\tmargin: 1.8em 1.5em 0 1.5em;\n\tmin-height: 70vh;\n\tpadding-bottom: 7em;\n}\n#tree {\n\tdisplay: none;\n\tposition: absolute;\n\tleft: 0;\n\tbottom: 0;\n\ttop: 7em;\n\toverflow-x: hidden;\n\toverflow-y: auto;\n\t-ms-scroll-chaining: none;\n\toverscroll-behavior-y: none;\n\tbox-shadow: 0 0 1em var(--bg-d2), 0 -1px 0 rgba(128,128,128,0.3);\n}\n#tree,\nhtml {\n\tscrollbar-color: var(--scroll) var(--bg-u3);\n}\n#treeh {\n\tposition: sticky;\n\tz-index: 1;\n\ttop: 0;\n\theight: 2.2em;\n\tline-height: 2.2em;\n\tbackground: var(--tree-bg);\n\tborder-bottom: 1px solid var(--bg-d3);\n\toverflow: hidden;\n}\n#treepar {\n\tz-index: 1;\n\tposition: fixed;\n\tbackground: #fff;\n\tbackground: var(--tree-bg);\n\tleft: -.96em;\n\twidth: calc(.3em + var(--nav-sz) - var(--sbw));\n\tborder-bottom: 1px solid var(--bg-u5);\n\toverflow: hidden;\n\tborder-right: .5em solid #999\\9;\n}\n#treepar.off {\n\tdisplay: none;\n}\n.np_open #thx_ff {\n\tpadding: 4.5em 0;\n\t/* widget */\n}\n#tree::-webkit-scrollbar-track,\n#tree::-webkit-scrollbar {\n\tbackground: var(--bg-u3);\n}\n#tree::-webkit-scrollbar-thumb {\n\tbackground: var(--scroll);\n}\n#tree:hover {\n\tz-index: 2;\n}\n#treeul {\n\tposition: relative;\n\tleft: -2.2em;\n\twidth: calc(100% + 2em);\n}\n.btn {\n\tcolor: var(--btn-fg);\n\tbackground: #eee;\n\tbackground: var(--btn-bg);\n\tbox-shadow: var(--btn-bs);\n\tborder-bottom: var(--btn-bb);\n\tborder-radius: .3em;\n\tpadding: .2em .4em;\n\tfont-size: 1.2em;\n\tmargin: .2em;\n\tdisplay: inline-block;\n\twhite-space: pre;\n\tposition: relative;\n\ttop: -.12em;\n}\nhtml.c .btn,\nhtml.a .btn {\n\tborder-radius: .2em;\n}\nhtml.dz .btn {\n\tfont-size: 1em;\n}\n.btn:hover {\n\tcolor: var(--btn-h-fg);\n\tbackground: var(--btn-h-bg);\n\tbox-shadow: var(--btn-h-bs);\n\tborder-bottom: var(--btn-h-bb);\n}\n.tgl.btn.on {\n\tbackground: #000;\n\tbackground: var(--btn-1-bg);\n\tcolor: #fff;\n\tcolor: var(--btn-1-fg);\n\ttext-shadow: none;\n\tbox-shadow: var(--btn-1-bs);\n\tborder-bottom: var(--btn-1-bb);\n}\n.tgl.btn.on:hover {\n\tcolor: var(--btn-1h-fg);\n\tbackground: var(--btn-1h-bg);\n\tbox-shadow: var(--btn-1h-bs);\n\tborder-bottom: var(--btn-1h-bb);\n}\n#detree {\n\tpadding: .3em .5em;\n\tfont-size: 1.5em;\n\tline-height: 1.5em;\n}\n#tree ul,\n#tree li {\n\tpadding: 0;\n\tmargin: 0;\n}\n#tree ul {\n\tborder-left: .2em solid var(--bg-u5);\n\tmargin-left: .44em;\n}\n#tree li {\n\tmargin-left: .6em;\n\tlist-style: none;\n\tborder-top: 1px solid var(--bg-u5);\n}\n#tree li.offline>a:first-child:before {\n\tcontent: '❌';\n\tposition: absolute;\n\tmargin-left: -.25em;\n\tz-index: 3;\n}\n#tree ul a.sel {\n\tbackground: #000;\n\tbackground: var(--bg-d3);\n\tbox-shadow: -.8em 0 0 var(--g-sel-b1) inset;\n\tcolor: #fff;\n\tcolor: var(--fg-max);\n}\n#tree ul a.hl {\n\tcolor: #fff;\n\tcolor: var(--btn-1-fg);\n\tbackground: #000;\n\tbackground: var(--btn-1-bg);\n\ttext-shadow: none;\n}\n#tree ul a.ld::before {\n\tfont-weight: bold;\n\tfont-family: sans-serif;\n\tdisplay: inline-block;\n\ttext-align: center;\n\twidth: 1em;\n\tmargin: 0 .3em 0 -1.3em;\n\tcolor: var(--fg-max);\n\topacity: 0;\n\tcontent: '◠';\n\tanimation: .5s linear infinite forwards spin, ease .25s 1 forwards fadein;\n}\n#tree ul a.par {\n\tcolor: var(--fg-max);\n}\n#tree ul a {\n\tborder-radius: .3em;\n\tdisplay: inline-block;\n}\n.ntree a+a {\n\twidth: calc(100% - 2.2em);\n\tline-height: 1em;\n}\n#tree.nowrap li {\n\tmin-height: 1.4em;\n\twhite-space: nowrap;\n}\n#tree.nowrap .ntree a+a:hover {\n\tbackground: rgba(16, 16, 16, 0.67);\n\tmin-width: calc(var(--nav-sz) - 2em);\n\twidth: auto;\n}\nhtml.y #tree.nowrap .ntree a+a:hover {\n\tbackground: rgba(255, 255, 255, 0.67);\n\tcolor: var(--fg-max);\n}\n#docul a:hover,\n#tree .ntree a+a:hover {\n\tbackground: var(--bg-d2);\n\tcolor: var(--fg-max);\n}\n.ntree a:first-child {\n\tfont-family: 'scp', monospace, monospace;\n\tfont-family: var(--font-mono), 'scp', monospace, monospace;\n\tfont-size: 1.2em;\n\tline-height: 0;\n}\n.dumb_loader_thing {\n\tdisplay: block;\n\tmargin: 1em .3em 1em 1em;\n\tpadding: 0 1.2em 0 0;\n\tfont-size: 4em;\n\tmin-width: 1em;\n\tmin-height: 1em;\n\topacity: 0;\n\tanimation: 1s linear .15s infinite forwards spin, .2s ease .15s 1 forwards fadein;\n\tposition: fixed;\n\ttop: .3em;\n\tz-index: 9;\n}\n#dlt_t {\n\tleft: 0;\n}\n#dlt_f {\n\tright: .5em;\n}\n#files .cfg {\n\tdisplay: none;\n\tfont-size: 2em;\n\twhite-space: nowrap;\n}\n#files th:hover .cfg {\n\tdisplay: block;\n\twidth: 1em;\n\tborder-radius: .2em;\n\tmargin: -1.2em auto 0 auto;\n\tbackground: var(--bg-u5);\n}\n#files th span {\n\tposition: relative;\n\twhite-space: nowrap;\n}\n#files>thead>tr>th.min,\n#files td.min {\n\tdisplay: none;\n}\n#files td:nth-child(2n) {\n\tcolor: var(--tab-alt);\n}\n#plazy {\n\twidth: 1px;\n\theight: 1px;\n\toverflow: hidden;\n\twhite-space: nowrap;\n}\n#blazy {\n\ttext-align: center;\n\tfont-size: 1.2em;\n\tmargin: 1em 0;\n}\n#blazy code,\n#blazy a {\n\tfont-size: 1.1em;\n\tpadding: 0 .2em;\n}\n.opwide,\n#op_unpost,\n#srch_form {\n\tmax-width: none;\n\tmargin-right: 1.5em;\n}\n.opwide>div {\n\tdisplay: inline-block;\n\tvertical-align: top;\n\tborder-left: .4em solid var(--bg-u5);\n\tmargin: .7em 0 .7em .5em;\n\tpadding-left: .5em;\n}\n.opwide>div>h3 {\n\tcolor: var(--fg-weak);\n\tmargin: 0 .4em;\n\tpadding: 0;\n}\n#op_cfg>div>div>span {\n\tdisplay: inline-block;\n\tpadding: .2em .4em;\n}\n.opbox h3 {\n\tmargin: .8em 0 0 .6em;\n\tpadding: 0;\n}\n#thumbs,\n#au_prescan,\n#au_fullpre,\n#au_os_seek,\n#au_osd_cv,\n#u2tdate {\n\topacity: .3;\n}\n#griden.on+#thumbs,\n#au_preload.on+#au_prescan,\n#au_preload.on+#au_prescan+#au_fullpre,\n#au_os_ctl.on+#au_os_seek,\n#au_os_ctl.on+#au_os_seek+#au_osd_cv,\n#u2turbo.on+#u2tdate {\n\topacity: 1;\n}\n#wraptree.on+#hovertree {\n\tdisplay: none;\n}\n.ghead {\n\tbackground: #fff;\n\tbackground: var(--bg-u2);\n\tborder-radius: .3em;\n\tpadding: .2em .5em;\n\tline-height: 2.3em;\n\tmargin-bottom: 1.5em;\n}\n#hdoc,\n#ghead {\n\tposition: sticky;\n\ttop: -.3em;\n\tz-index: 2;\n}\n.ghead .btn {\n\tposition: relative;\n\ttop: 0;\n}\n.ghead>span {\n\twhite-space: pre;\n\tpadding-left: .3em;\n}\n#tailbtns {\n\tdisplay: none;\n}\n#taildoc.on+#tailbtns {\n\tdisplay: inherit;\n\tdisplay: unset;\n}\n#op_unpost {\n\tpadding: 1em;\n}\n#op_unpost td {\n\tpadding: .2em .4em;\n}\n#op_unpost a {\n\tmargin: 0;\n\tpadding: 0;\n}\n#unpost td:nth-child(3),\n#unpost td:nth-child(4) {\n\ttext-align: right;\n}\n#shui,\n#rui {\n\tbackground: #fff;\n\tbackground: var(--bg);\n\tposition: fixed;\n\ttop: 0;\n\tleft: 0;\n\twidth: calc(100% - 2em);\n\theight: auto;\n\toverflow: auto;\n\tmax-height: calc(100% - 2em);\n\tborder-bottom: .5em solid var(--f-gray);\n\tbox-shadow: 0 0 5em rgba(0,0,0,0.8);\n\tpadding: 1em;\n\tz-index: 765;\n}\n#shui div+div,\n#rui div+div {\n\tmargin-top: 1em;\n}\n#shui table,\n#rui table {\n\twidth: 100%;\n\tborder-collapse: collapse;\n}\n#shui button {\n\tmargin: 0 1em 0 0;\n}\n#shui .btn {\n\tfont-size: 1em;\n}\n#shui td {\n\tpadding: .8em 0;\n}\n#shui td+td,\n#rui td+td {\n\tpadding: .2em 0 .2em .5em;\n}\n#rn_vadv input {\n\tfont-family: 'scp', monospace, monospace;\n\tfont-family: var(--font-mono), 'scp', monospace, monospace;\n}\n#shui td+td,\n#rui td+td,\n#shui td input[type=\"text\"],\n#rui td input[type=\"text\"] {\n\twidth: 100%;\n}\n#rui #rn_n_d,\n#rui #rn_n_s,\n#shui td.exs input[type=\"text\"] {\n\twidth: 3em;\n}\n#rn_f.m td:first-child {\n\twhite-space: nowrap;\n}\n#rn_f.m td+td {\n\twidth: 50%;\n}\n#rn_f .err td,\n#rn_f .err input[readonly],\n#rui .ng input[readonly] {\n\tcolor: var(--err-fg);\n\tbackground: var(--err-bg);\n}\n#rui input[readonly] {\n\tcolor: var(--fg-max);\n\tbackground: var(--bg-u5);\n\tborder: 1px solid rgba(255,255,255,0.2);\n\tpadding: .2em .25em;\n}\n#rui h1 {\n\tmargin: 0 0 .3em 0;\n\tpadding: 0;\n\tfont-size: 1.5em;\n}\n#fs_abrt {\n\tmargin-top: 1em;\n\ttext-shadow: 0;\n\tbox-shadow: 1px 1px 0 var(--bg-d3);\n}\n#doc {\n\toverflow: visible;\n\tbackground: #fff;\n\tbackground: var(--bg);\n\tmargin: -1em 0 .5em 0;\n\tpadding: 1em 0 1em 0;\n\tborder-radius: .3em;\n}\n#doc.wrap {\n\twhite-space: pre-wrap;\n}\nhtml.y #doc {\n\tbox-shadow: 0 0 .3em var(--bg-u5);\n\tbackground: #f7f7f7;\n}\n#docul {\n\tposition: relative;\n}\n#docul li.bn {\n\ttext-align: center;\n\tpadding: .5em;\n}\n#docul li.bn span {\n\tfont-weight: bold;\n\tcolor: var(--fg-max);\n}\n#doc.prism {\n\tpadding-left: 3em;\n}\n#doc>code {\n\tbackground: none;\n\tbox-shadow: none;\n\tz-index: 1;\n}\n#doc.mdo {\n\twhite-space: normal;\n\tfont-family: sans-serif;\n\tfont-family: var(--font-main), sans-serif;\n}\n#doc.prism * {\n\tline-height: 1.5em;\n}\n#doc .line-highlight {\n\tborder-radius: .3em;\n\tbox-shadow: 0 0 .5em rgba(128,128,128,0.2);\n\tbackground: linear-gradient(90deg, var(--bg-d3), var(--bg));\n}\nhtml.y #doc .line-highlight {\n\tbackground: linear-gradient(90deg, var(--bg-max), var(--bg));\n}\n#docul li {\n\tmargin: 0;\n}\n#tree #docul li+li a {\n\tdisplay: block;\n}\n#seldoc.sel {\n\tcolor: var(--fg2-max);\n\tbackground: #f0f;\n\tbackground: var(--g-sel-b1);\n}\n#pvol,\n#barbuf,\n#barpos,\na.btn,\n#u2btn,\n#u2conf label,\n#rui label,\n#modal-ok,\n#modal-ng,\n#ops,\n#ico1 {\n\t-webkit-user-select: none;\n\t-moz-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none;\n}\n#hkhelp {\n\tbackground: #fff;\n\tbackground: var(--bg);\n}\n#hkhelp table {\n\tmargin: 2em 2em 0 2em;\n\tfloat: left;\n}\n#hkhelp th {\n\tborder-bottom: 1px solid var(--bg-u5);\n\tbackground: var(--bg-u1);\n\tfont-weight: bold;\n\ttext-align: right;\n}\n#hkhelp tr+tr th {\n\tborder-top: 1.5em solid var(--bg);\n}\n#hkhelp td {\n\tpadding: .2em .3em;\n}\n#hkhelp td:first-child {\n\tfont-family: 'scp', monospace, monospace;\n\tfont-family: var(--font-mono), 'scp', monospace, monospace;\n}\n#hkhelp b {\n\ttext-shadow: 1px 0 0 var(--fg), -1px 0 0 var(--fg), 0 -1px 0 var(--fg);\n}\nhtml.noscroll,\nhtml.noscroll .sbar {\n\tscrollbar-width: none;\n}\nhtml.noscroll::-webkit-scrollbar,\nhtml.noscroll .sbar::-webkit-scrollbar {\n\tdisplay: none;\n}\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/* bbox */\n\n#bbox-overlay {\n\tdisplay: none;\n\topacity: 0;\n\tposition: fixed;\n\toverflow: hidden;\n\ttouch-action: pinch-zoom;\n\ttop: 0;\n\tleft: 0;\n\twidth: 100%;\n\theight: 100%;\n\tz-index: 10;\n\tbackground: rgba(0, 0, 0, 0.8);\n\ttransition: opacity .3s ease;\n}\n#bbox-overlay.visible {\n\topacity: 1;\n}\n.full-image {\n\tdisplay: inline-block;\n\tposition: relative;\n\twidth: 100%;\n\theight: 100%;\n\ttext-align: center;\n\tflex: none;\n}\n.full-image figure {\n\tdisplay: inline;\n\tmargin: 0;\n\theight: 100%;\n}\n.full-image img,\n.full-image video {\n\tdisplay: inline-block;\n\toutline: none;\n\twidth: auto;\n\theight: auto;\n\tmax-width: 100%;\n\tmax-height: 100%;\n\tmax-height: calc(100% - 1.4em);\n\tmargin-bottom: 1.4em;\n\tvertical-align: middle;\n\ttransition: transform .23s, left .23s, top .23s, width .23s, height .23s;\n}\n.full-image img.asap,\n.full-image video.asap {\n\ttransition: none;\n}\n#bbox-overlay.fill .full-image img,\n#bbox-overlay.fill .full-image video {\n    width: 100%;\n    height: 100%;\n    object-fit: contain;\n}\nhtml.bb_fsc .full-image img,\nhtml.bb_fsc .full-image video {\n\tmax-height: 100%;\n}\nhtml.bb_fsc figcaption {\n\tdisplay: none;\n}\n.full-image img.nt,\n.full-image video.nt {\n\ttransition: none;\n}\n.full-image.vis img,\n.full-image.vis video {\n\tbox-shadow: 0 0 8px rgba(0, 0, 0, 0.6);\n}\n.full-image video {\n\tbackground: #222;\n}\n.full-image figcaption {\n\tdisplay: block;\n\tposition: fixed;\n\tbottom: .1em;\n\twidth: 100%;\n\ttext-align: center;\n\twhite-space: normal;\n\tcolor: var(--fg);\n\tz-index: 1;\n}\n#bbox-overlay figcaption a {\n\tbackground: rgba(0, 0, 0, 0.6);\n\tborder-radius: .4em;\n\tpadding: .3em .6em;\n}\nhtml.y #bbox-overlay figcaption a {\n\tcolor: #0bf;\n}\n.full-image:before {\n\tcontent: \"\";\n\tdisplay: inline-block;\n\theight: 50%;\n\twidth: 1px;\n\tmargin-right: -1px;\n}\n#bbox-slider {\n\tposition: fixed;\n\tleft: 0;\n\ttop: 0;\n\theight: 100%;\n\twidth: 100%;\n\twhite-space: nowrap;\n\ttransition: left .2s ease, transform .2s ease;\n}\n.bounce-from-right {\n\tanimation: bounceFromRight .3s ease-out;\n}\n.bounce-from-left {\n\tanimation: bounceFromLeft .3s ease-out;\n}\n.eog {\n\tanimation: eog .2s;\n}\n@keyframes bounceFromRight {\n\t0% {margin-left: 0}\n\t50% {margin-left: -30px}\n\t100% {margin-left: 0}\n}\n@keyframes bounceFromLeft {\n\t0% {margin-left: 0}\n\t50% {margin-left: 30px}\n\t100% {margin-left: 0}\n}\n@keyframes eog {\n\t0% {filter: brightness(1.5)}\n}\n#bbox-next,\n#bbox-prev {\n\ttop: 50%;\n\ttop: calc(50% - 30px);\n\twidth: 44px;\n\theight: 60px;\n\ttransition: background-color .3s ease, color .3s ease, left .3s ease, right .3s ease;\n}\n#bbox-btns button {\n\ttransition: background-color .3s ease, color .3s ease;\n}\n#bbox-btns {\n\ttransition: top .3s ease;\n}\n.bbox-btn {\n\tposition: fixed;\n}\n#bbox-next.off {\n\tright: -2.6em;\n}\n#bbox-prev.off {\n\tleft: -2.6em;\n}\n#bbox-btns.off {\n\ttop: -2.2em;\n}\n#bbox-overlay button {\n\tcursor: pointer;\n\toutline: none;\n\tpadding: 0 .3em;\n\tmargin: 0 .4em;\n\tborder: 0;\n\tborder-radius: 15%;\n\tbackground: rgba(50, 50, 50, 0.5);\n\tcolor: rgba(255,255,255,0.7);\n\tfont-size: 1.4em;\n\tline-height: 1.4em;\n\tvertical-align: top;\n\tfont-variant: small-caps;\n}\n#bbox-overlay button:focus,\n#bbox-overlay button:hover {\n\tcolor: rgba(255,255,255,0.9);\n\tbackground: rgba(50, 50, 50, 0.9);\n}\n#bbox-next {\n\tright: 1%;\n}\n#bbox-prev {\n\tleft: 1%;\n}\n#bbox-btns {\n\ttop: .5em;\n\tright: 2%;\n\tposition: fixed;\n}\n#bbox-halp {\n\tcolor: var(--fg-max);\n\tbackground: #fff;\n\tbackground: var(--bg);\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\tz-index: 20;\n\tpadding: .4em;\n}\n#bbox-halp td {\n\tpadding: .2em .5em;\n}\n#bbox-halp td:first-child:not([colspan]) {\n\ttext-align: right;\n}\n.bbox-spinner {\n\twidth: 40px;\n\theight: 40px;\n\tdisplay: inline-block;\n\tposition: absolute;\n\ttop: 50%;\n\tleft: 50%;\n\tmargin-top: -20px;\n\tmargin-left: -20px;\n}\n.bbox-double-bounce1,\n.bbox-double-bounce2 {\n\twidth: 100%;\n\theight: 100%;\n\tborder-radius: 50%;\n\tbackground-color: #fff;\n\topacity: .6;\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\tanimation: bounce 2s infinite ease-in-out;\n}\n.bbox-double-bounce2 {\n\tanimation-delay: -1s;\n}\n@keyframes bounce {\n\t0%, 100% {transform: scale(0)}\n\t50% {transform: scale(1)}\n}\n.no-transition {\n\ttransition: none !important;\n}\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/* upload.css */\n\n#op_up2k {\n\tpadding: 0 1em 1em 1em;\n}\n#drops {\n\tdisplay: none;\n\tz-index: 3;\n\tbackground: rgba(48, 48, 48, 0.7);\n}\n#drops.vis,\n.dropzone {\n\tdisplay: block;\n\tposition: fixed;\n\ttop: 0;\n\tleft: 0;\n\tright: 0;\n\tbottom: 0;\n}\n.dropdesc {\n\tposition: fixed;\n\tdisplay: table;\n\tleft: 10%;\n\twidth: 78%;\n\theight: 26%;\n\tmargin: 0;\n\tfont-size: 3em;\n\tfont-weight: bold;\n\ttext-shadow: .05em .05em .1em #333;\n\tbackground: rgba(224, 224, 224, 0.2);\n\tbox-shadow: 0 0 0 #999;\n\tborder: .5em solid var(--ud-b1);\n\tborder-radius: .5em;\n\tborder-width: 1vw;\n\tcolor: #fff;\n\ttransition: all 0.12s;\n}\n.dropdesc.hl.ok {\n\tborder-color: #fff;\n\tbox-shadow: 0 0 1em .4em #cf5, 0 0 1em #000 inset;\n\tbackground: rgba(24, 24, 24, 0.7);\n\tleft: 8%;\n\twidth: 82%;\n\theight: 32%;\n\tmargin: -3vh 0;\n}\n.dropdesc.hl.err {\n\tbackground: rgba(224, 32, 65, 0.2);\n\tbox-shadow: 0 0 1em .4em #f26;\n\tborder-color: #fab;\n}\n.dropdesc>div {\n\tdisplay: table-cell;\n\tvertical-align: middle;\n\ttext-align: center;\n}\n.dropdesc>div>div {\n\tposition: absolute;\n\ttop: 40%;\n\ttop: calc(50% - .5em);\n\ttop: calc(50% - 1.2vw);\n\tleft: -.8em;\n\ttransition: top 0.12s;\n}\n.dropdesc>div>div+div {\n\tleft: auto;\n\tright: -.8em;\n}\n.dropdesc b {\n\tfont-size: .5em;\n\tfont-size: 2vw;\n\tmargin: 0 .8em;\n\tmargin: 0 1.25vw;\n\ttransition: font-size 0.12s;\n}\n.dropdesc.hl.ok b {\n\tborder-bottom: .1em solid #fff;\n\tfont-size: .6em;\n\tfont-size: 2.5vw;\n}\n.dropdesc.hl.ok>div>div {\n\ttop: calc(50% - 1.7vw);\n}\n.dropzone {\n\tz-index: 80386;\n\theight: 50%;\n}\n#up_dz {\n\tbottom: 50%;\n}\n#srch_dz {\n\ttop: 50%;\n}\n#up_zd {\n\ttop: 12%;\n}\n#srch_zd {\n\tbottom: 12%;\n}\n.dropdesc span {\n\tcolor: #fff;\n\tbackground: #490;\n\tborder-bottom: .3em solid #6d2;\n\ttext-shadow: 1px 1px 1px #000;\n\tborder-radius: .3em;\n\tpadding: .4em .8em;\n\tfont-size: .4em;\n}\n.dropdesc.err span {\n\tbackground: #904;\n\tborder-color: #d26;\n}\n#u2form {\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\twidth: 2px;\n\theight: 2px;\n\toverflow: hidden;\n}\n#u2form input {\n\tbackground: var(--bg-u5);\n\tborder: 0px solid var(--bg-u5);\n}\n#u2err.err {\n\tcolor: var(--a-dark);\n\tpadding: .5em;\n}\n#u2err.msg {\n\tcolor: var(--fg-weak);\n\tpadding: .5em;\n\tfont-size: .9em;\n}\n#u2btn {\n\tline-height: 1.3em;\n\tborder: .15em dashed var(--u2-btn-b1);\n\tborder-radius: .4em;\n\ttext-align: center;\n\tfont-size: 1.5em;\n\tmargin: .5em auto;\n\tpadding: .8em 0;\n\twidth: 16em;\n\tcursor: pointer;\n}\n#op_up2k.srch #u2btn {\n\tborder-color: var(--u2-sbtn-b1);\n}\n#u2conf.ww #u2btn {\n\tline-height: 1em;\n\tpadding: .5em 0;\n\tmargin: -2em 1em -3em 0;\n}\n#u2conf #u2btn {\n\tpadding: .4em 0;\n\tmargin: -2em 0;\n\tfont-size: 1.25em;\n\twidth: 100%;\n\tmax-width: 12em;\n\tdisplay: inline-block;\n}\n#u2conf #u2btn_cw {\n\ttext-align: right;\n}\n#u2bm {\n\tdisplay: block;\n}\n#u2bm sup {\n\tfont-weight: bold;\n}\n#u2notbtn {\n\tdisplay: none;\n\ttext-align: center;\n\tbackground: var(--bg);\n\tpadding-top: 1em;\n}\n#u2notbtn * {\n\tline-height: 1.3em;\n}\n#u2mu div {\n\theight: 1.2em;\n\toverflow: hidden;\n}\n#u2tabw {\n\tmin-height: 0;\n\ttransition: min-height .2s;\n\tmargin: 2em 0;\n}\n#u2tabw.na>table {\n\tdisplay: none;\n}\n#u2tab {\n\ttable-layout: fixed;\n\tborder-collapse: collapse;\n\twidth: calc(100% - 2em);\n\tmax-width: 100em;\n\tmargin: 0 auto;\n}\n#op_up2k.srch #u2tabf {\n\tmax-width: none;\n}\n#u2tab td {\n\tword-wrap: break-word;\n\tborder: 1px solid rgba(128,128,128,0.8);\n\tborder-width: 0 0px 1px 0;\n\tpadding: .2em .3em;\n}\n#u2tab td:nth-child(2) {\n\twidth: 5em;\n\twhite-space: nowrap;\n}\n#u2tab td:nth-child(3) {\n\twidth: 40%;\n}\n#u2tab.up.ok td:nth-child(3),\n#u2tab.up.bz td:nth-child(3),\n#u2tab.up.q  td:nth-child(3) {\n\twidth: 18em;\n}\n@media (max-width: 65em) {\n\t#u2tab {\n\t\tfont-size: .9em;\n\t}\n}\n@media (max-width: 50em) {\n\t#u2tab.up.ok td:nth-child(3),\n\t#u2tab.up.bz td:nth-child(3),\n\t#u2tab.up.q  td:nth-child(3) {\n\t\twidth: 16em;\n\t}\n}\n#op_up2k.srch td.prog {\n\tfont-family: sans-serif;\n\tfont-family: var(--font-main), sans-serif;\n\tfont-size: 1em;\n\twidth: auto;\n}\n#u2tab tbody tr:hover td {\n\tbackground: var(--bg-u2);\n}\n#u2etas {\n\tpadding: .2em .5em;\n\twidth: 17em;\n\tcursor: pointer;\n\ttext-align: center;\n\twhite-space: nowrap;\n\tdisplay: inline-block;\n\tfont-family: 'scp', monospace, monospace;\n\tfont-family: var(--font-mono), 'scp', monospace, monospace;\n}\n#u2etas.o {\n\twidth: 20em;\n}\n#u2etas .o {\n\tdisplay: none;\n}\n#u2etas.o .o {\n\tdisplay: inherit;\n\tdisplay: unset;\n}\n#u2etaw {\n\twidth: 18em;\n\tfont-size: .94em;\n\tmargin: 1.8em auto .5em auto;\n}\n#u2etas.o #u2etaw {\n\twidth: 21em;\n}\n#u2cards {\n\tpadding: 1em 1em .42em 1em;\n\tmargin: 0 auto;\n\twhite-space: nowrap;\n\ttext-align: center;\n\toverflow: hidden;\n\tmin-width: 24em;\n}\n#u2cards.w {\n\twidth: 48em;\n\ttext-align: left;\n}\n#u2cards.ww {\n\tdisplay: inline-block;\n}\n#u2etaw.w {\n\twidth: 55em;\n\ttext-align: right;\n\tmargin: 2em auto -2.7em auto;\n}\n#u2etaw.ww {\n\tmargin: -1em 2em 1em 1em;\n}\n#u2cards a {\n\tpadding: .2em 1em;\n\tbackground: var(--u2-tab-bg);\n\tborder: 1px solid #999;\n\tborder-color: var(--u2-tab-b1);\n\tborder-width: 0 0 1px 0;\n}\n#u2cards a:first-child {\n\tborder-radius: .4em 0 0 0;\n}\n#u2cards a:last-child {\n\tborder-radius: 0 .4em 0 0;\n}\n#u2cards a.act {\n\tpadding-bottom: .5em;\n\tborder-width: 1px 1px .1em 1px;\n\tborder-radius: .3em .3em 0 0;\n\tmargin-left: -1px;\n\tbackground: var(--u2-tab-1-bg);\n\tbox-shadow: 0 -.17em .67em var(--u2-tab-1-sh);\n\tborder-color: var(--u2-tab-1-b1) var(--u2-tab-1-b2) var(--bg) var(--u2-tab-1-b2);\n\tcolor: var(--u2-tab-1-fg);\n\tposition: relative;\n}\n#u2cards span {\n\tcolor: var(--fg-max);\n\tfont-family: 'scp', monospace;\n\tfont-family: var(--font-mono), 'scp', monospace;\n}\n#u2cards > a:nth-child(4) > span {\n\tdisplay: inline-block;\n\ttext-align: center;\n\tmin-width: 1.3em;\n}\n#u2conf {\n\tmargin: 1em auto;\n\twidth: 30em;\n}\n#u2conf.w {\n\twidth: 51em;\n}\n#u2conf.ww {\n\twidth: 82em;\n}\n#u2conf.ww #u2c3w {\n\twidth: 29em;\n}\n#u2conf.ww #u2c3w.s {\n\twidth: 39em;\n}\n#u2conf .c,\n#u2conf .c * {\n\ttext-align: center;\n\tline-height: 1em;\n\tmargin: 0;\n\tpadding: 0;\n\tborder: none;\n}\n#u2conf .txtbox {\n\twidth: 3em;\n\tcolor: var(--fg-max);\n\tbackground: var(--u2-txt-bg);\n\tborder: 1px solid #777;\n\tfont-size: 1.2em;\n\tpadding: .15em 0;\n\theight: 1.05em;\n}\n#u2conf .txtbox.err {\n\tcolor: var(--err-fg);\n\tbackground: var(--err-bg);\n}\n#u2conf a.b {\n\tcolor: var(--u2-b-fg);\n\tbackground: var(--u2-b1-bg);\n\ttext-decoration: none;\n\tborder-radius: .1em;\n\tfont-size: 1.5em;\n\tpadding: .1em 0;\n\tmargin: 0 -1px;\n\twidth: 1.5em;\n\theight: 1em;\n\tdisplay: inline-block;\n\tposition: relative;\n\tbottom: -0.08em;\n}\n#u2conf input+a.b {\n\tbackground: var(--u2-b2-bg);\n}\nhtml.b #u2conf a.b:hover {\n\tbackground: var(--btn-h-bg);\n}\n#u2conf .c label {\n\tfont-size: 1.6em;\n\twidth: 2em;\n\theight: 1em;\n\tpadding: .4em 0;\n\tdisplay: block;\n\tborder-radius: .25em;\n}\n#u2conf input[type=\"checkbox\"] {\n\tposition: relative;\n\topacity: .02;\n\ttop: 2em;\n}\n#u2conf input[type=\"checkbox\"]+label,\n#u2conf input[type=\"checkbox\"]:checked+label {\n\tposition: relative;\n\tcursor: pointer;\n\tbackground: var(--btn-bg);\n\tbox-shadow: var(--btn-bs);\n\tborder-bottom: var(--btn-bb);\n\ttext-shadow: 1px 1px 1px #000, 1px -1px 1px #000, -1px -1px 1px #000, -1px 1px 1px #000;\n}\n#u2conf input[type=\"checkbox\"]:checked+label {\n\tbackground: var(--btn-1-bg);\n\tbox-shadow: var(--btn-1-bs);\n\tborder-bottom: var(--btn-1-bb);\n}\n#u2conf input[type=\"checkbox\"]+label:hover {\n\tbackground: var(--btn-h-bg);\n\tbox-shadow: var(--btn-h-bs);\n\tborder-bottom: var(--btn-h-bb);\n}\n#u2conf input[type=\"checkbox\"]:checked+label:hover {\n\tbackground: var(--btn-1h-bg);\n\tbox-shadow: var(--btn-1h-bs);\n\tborder-bottom: var(--btn-1h-bb);\n}\n#op_up2k.srch #u2conf td:nth-child(2)>*,\n#op_up2k.srch #u2conf td:nth-child(3)>* {\n\tbackground: #777;\n\tborder-color: var(--fg);\n\tbox-shadow: none;\n\topacity: .2;\n}\n#u2flagblock {\n\tdisplay: none;\n\tmargin: 2em auto 4em auto;\n\tpadding: .5em;\n\tmax-width: 27em;\n\ttext-align: center;\n\tborder: .2em dashed rgba(128, 128, 128, 0.3);\n}\n#u2foot,\n#u2life {\n\tcolor: var(--fg-max);\n\ttext-align: center;\n\tmargin: .8em 0;\n}\n#u2life {\n\tmargin: 2.5em 0;\n\tline-height: 1.5em;\n}\n#u2life div {\n\tdisplay: inline-block;\n\twhite-space: nowrap;\n\tmargin: 0 2em;\n}\n#u2life div:first-child {\n\tmargin-bottom: .2em;\n}\n#u2life small {\n\topacity: .6;\n}\n#lifew {\n\tborder-bottom: 1px dotted var(--fg-max);\n}\n#u2foot {\n\tfont-size: 1.2em;\n\tfont-style: italic;\n}\n#u2foot .warn {\n\tfont-size: 1.2em;\n\tpadding: .5em .8em;\n\tmargin: 1em -.6em;\n\tborder-width: .1em 0;\n\ttext-align: center;\n}\n#u2foot .warn span {\n\tcolor: var(--srv-3);\n}\n#u2foot span {\n\tcolor: #999;\n\tfont-size: .9em;\n\tfont-weight: normal;\n}\n#u2foot>*+* {\n\tmargin-top: 1.5em;\n}\n#u2life input {\n\twidth: 4em;\n\ttext-align: right;\n}\n.prog {\n\tfont-family: 'scp', monospace, monospace;\n\tfont-family: var(--font-mono), 'scp', monospace, monospace;\n}\n#u2tab span.inf,\n#u2tab span.ok,\n#u2tab span.err {\n\tcolor: #fff;\n\tpadding: .22em;\n\tborder-radius: .2em;\n\tborder: 2px solid #f0f;\n}\n#u2tab span.inf {\n\tbackground: var(--u2-inf-bg);\n\tborder-color: var(--u2-inf-b1);\n}\n#u2tab span.ok {\n\tbackground: var(--u2-ok-bg);\n\tborder-color: var(--u2-ok-b1);\n}\n#u2tab span.err {\n\tbackground: var(--u2-err-bg);\n\tborder-color: var(--u2-err-b1);\n}\n#u2tab a>span,\n#docul .bn a>span,\n#unpost a>span {\n\tfont-weight: bold;\n\tfont-style: italic;\n\tcolor: var(--fg-max);\n\tpadding-left: .2em;\n}\n.fsearch_explain {\n\tcolor: var(--a-dark);\n\tpadding-left: .7em;\n\tfont-size: 1.1em;\n\tline-height: 0;\n}\n\n\n\n\n\n\n\n\n\n\n\n\nhtml.c #path,\nhtml.a #path {\n\tborder-radius: 0 .3em .3em 0;\n}\nhtml.c #pctl a,\nhtml.a #pctl a {\n\tbackground: rgba(0,0,0,0.1);\n\tmargin-right: .5em;\n\tbox-shadow: -.02em -.02em .3em rgba(0,0,0,0.2) inset;\n}\nhtml.d #pctl,\nhtml.b #pctl {\n\tleft: .5em;\n}\nhtml.d #ops,\nhtml.c #ops,\nhtml.a #ops {\n\tmargin: 1.7em 1.5em 0 1.5em;\n\tborder-radius: .3em;\n\tborder-width: 1px 0;\n}\nhtml.c .opbox,\nhtml.a .opbox {\n\tmargin: 1.5em 0 0 0;\n}\nhtml.dz .opview input.i {\n\twidth: calc(100% - 18em);\n}\nhtml.c #tree,\nhtml.c #treeh,\nhtml.a #tree,\nhtml.a #treeh {\n\tborder-radius: 0 .3em 0 0;\n\tbackground: var(--bg-u2);\n}\nhtml.c #treepar,\nhtml.a #treepar {\n\tbackground: var(--bg-u2);\n}\nhtml.c #tree li,\nhtml.a #tree li {\n\tborder-top: 1px solid var(--bg-u5);\n\tborder-bottom: 1px solid var(--bg-d3);\n}\nhtml.c #tree li:last-child,\nhtml.a #tree li:last-child {\n\tborder-bottom: none;\n}\nhtml.c .opbox h3,\nhtml.a .opbox h3 {\n\tborder-bottom: 1px solid var(--bg-u5);\n}\nhtml.c #ops,\nhtml.c .opbox,\nhtml.c #path,\nhtml.c #srch_form,\nhtml.c .ghead,\n\nhtml.a #ops,\nhtml.a .opbox,\nhtml.a #path,\nhtml.a #srch_form,\nhtml.a .ghead {\n\tbackground: var(--bg-u2);\n\tborder: 1px solid var(--bg-u3);\n\tbox-shadow: 0 0 .3em var(--bg-d3);\n}\nhtml.c #u2btn,\nhtml.a #u2btn {\n\tcolor: #eee;\n\tbackground: var(--bg-u5);\n\tbackground: -moz-linear-gradient(top, #367 0%, #489 50%, #38788a 51%, #367 100%);\n\tbackground: -webkit-linear-gradient(top, #367 0%, #489 50%, #38788a 51%, #367 100%);\n\tbackground: linear-gradient(to bottom, #367 0%, #489 50%, #38788a 51%, #367 100%);\n\tbox-shadow: .4em .4em 0 var(--bg-d3);\n\tborder: 1px solid #222;\n}\nhtml.ay #u2btn {\n\tbox-shadow: .4em .4em 0 #ccc;\n}\nhtml.dz #u2btn {\n\tletter-spacing: -.033em;\n}\nhtml.c #u2conf.ww #u2btn,\nhtml.a #u2conf.ww #u2btn {\n\tmargin: -2em .5em -3em 0;\n\tpadding: .9em 0;\n}\nhtml.c #op_up2k.srch #u2btn,\nhtml.a #op_up2k.srch #u2btn {\n\tbackground: linear-gradient(to bottom, #ca3 0%, #fd8 50%, #fc6 51%, #b92 100%);\n\ttext-shadow: 1px 1px 1px #fc6;\n\tcolor: #333;\n}\nhtml.c #u2conf #u2btn,\nhtml.a #u2conf #u2btn {\n\tpadding: .6em 0;\n\tmargin-top: -2.6em;\n}\nhtml.c #u2etas,\nhtml.a #u2etas {\n\tbackground: var(--bg-d1);\n\tborder: 1px solid var(--bg-u1);\n\tborder-width: .1em 0;\n\tborder-radius: .5em;\n\tborder-width: .25em 0;\n}\nhtml.c #u2cards,\nhtml.a #u2cards {\n\tmargin: 0 auto -1em auto;\n}\nhtml.c #u2foot:empty,\nhtml.a #u2foot:empty {\n\tmargin-bottom: -1em;\n}\nhtml.ay #ops,\nhtml.ay .opbox,\nhtml.ay #path,\nhtml.ay #doc,\nhtml.ay #srch_form,\nhtml.ay .ghead,\nhtml.ay #u2etas {\n\tborder-color: var(--bg-u2);\n\tbox-shadow: 0 0 .3em var(--bg-u5);\n}\nhtml.ay #tree li,\nhtml.ay #treepar {\n\tborder-color: #f7f7f7 var(--bg-max) #ddd var(--bg-max);\n}\nhtml.ay #path {\n\tbackground: #f7f7f7;\n\tbox-shadow: 0 0 .3em #bbb;\n}\nhtml.ay #treeh,\nhtml.ay #treepar {\n\tbackground: #f7f7f7;\n\tborder-color: #ddd;\n}\nhtml.ay #tree {\n\tborder-color: #ddd;\n\tbox-shadow: 0 0 1em #ddd;\n\tbackground: #f7f7f7;\n}\n\n\n\n\n\nhtml.b #path {\n\tmargin: .3em 0;\n\tline-height: 1.7em;\n}\nhtml.b.op_open #path {\n\tmargin-top: .2em;\n}\nhtml.b #srv_info {\n\tdisplay: none;\n}\nhtml.b #srv_info2 {\n\tdisplay: inline-block;\n}\nhtml.b #srv_info2:after {\n\tcontent: '//';\n\tmargin: 0 .4em;\n}\nhtml.b #acc_info {\n\tright: .5em;\n}\nhtml.b #wtoggle {\n\tborder-radius: .1em 0 0 0;\n}\nhtml.d #barpos,\nhtml.d #barbuf,\nhtml.d #pvol,\nhtml.b #barpos,\nhtml.b #barbuf,\nhtml.b #pvol {\n\tborder-radius: .2em;\n}\nhtml.b #barpos {\n\tbox-shadow: 0 0 0 1px rgba(0,0,0,0.4);\n}\nhtml.by #barpos {\n\tbox-shadow: 0 0 0 1px rgba(0,0,0,0.2) inset;\n}\nhtml.b #ops {\n\tmax-width: 1em;\n\tmargin-bottom: 1.7em;\n\tposition: relative;\n\tz-index: 2;\n}\nhtml.b #ops a {\n\tbackground: var(--bg);\n}\nhtml.b .opview {\n\tmargin: 1em 0;\n}\nhtml.b #srch_q {\n\tmargin: .2em 0 0 1.6em;\n}\nhtml.b #srch_q:empty {\n\tmargin-bottom: -1em;\n}\nhtml.b #op_up2k {\n\tmargin-top: 3em;\n}\nhtml.b #tree {\n\tbox-shadow: 0 -1px 0 rgba(128,128,128,0.4);\n}\nhtml.bz #tree {\n\tbox-shadow: 0 -1px 0 var(--bg-d3);\n}\nhtml.b #treeh,\nhtml.b #tree li {\n\tborder: none;\n}\nhtml.b #tree li {\n\tmargin-left: .8em;\n}\nhtml.b #docul a,\nhtml.b .ntree a {\n\tpadding: .6em .2em;\n}\nhtml.b #treepar {\n\tmargin-left: .63em;\n\twidth: calc(.1em + var(--nav-sz) - var(--sbw));\n\tborder-bottom: .2em solid var(--f-h-b1);\n}\nhtml.b #wrap {\n\tmargin-top: 2em;\n}\nhtml.by .ghead,\nhtml.bz .ghead {\n\tbackground: var(--bg);\n\tpadding: .2em 0;\n}\nhtml.b #files td {\n\tpadding: .5em .7em;\n}\nhtml.b .btn {\n\ttop: -.1em;\n}\nhtml.b #op_up2k.srch sup {\n\tcolor: #fc0;\n}\nhtml.by #u2btn sup {\n\tcolor: #06b;\n}\nhtml.by #op_up2k.srch sup {\n\tcolor: #b70;\n}\nhtml.bz #u2cards a.act {\n\tbox-shadow: 0 -.1em .2em var(--bg-d2);\n}\nhtml.b #u2conf {\n\tmargin: 2em auto 0 auto;\n}\nhtml.b #u2conf .txtbox {\n\tborder: none;\n}\nhtml.b #u2conf a.b {\n\tborder-radius: .2em;\n}\nhtml.by #u2cards a.act {\n\tborder-width: 2px;\n}\n\n\n\n\n\nhtml.cy .mdo a {\n\tbackground: #f00;\n}\nhtml.cy #wrap,\nhtml.cy #acc_info a,\nhtml.cy #op_up2k,\nhtml.cy #files,\nhtml.cy #files a,\nhtml.cy #files tbody div a:last-child {\n\tcolor: #000;\n}\nhtml.cy #u2tab a,\nhtml.cy #u2cards a {\n\tcolor: #f00;\n}\nhtml.cy #unpost a {\n\tcolor: #ff0;\n}\nhtml.cy #barbuf {\n\tfilter: hue-rotate(267deg) brightness(0.8) contrast(4);\n}\nhtml.cy #pvol {\n\tfilter: hue-rotate(4deg) contrast(2.2);\n}\n\n\n\n\n\nhtml.dz * {\n\tborder-radius: 0 !important;\n}\nhtml.d #treepar {\n\tborder-bottom: .2em solid var(--f-h-b1);\n}\n\n\n\n\n@media (max-width: 32em) {\n\t#u2conf {\n\t\tfont-size: .9em;\n\t}\n}\n@media (max-width: 28em) {\n\t#u2conf {\n\t\tfont-size: .8em;\n\t}\n}\n@media (min-width: 70em) {\n\t#barpos,\n\t#barbuf {\n\t\twidth: calc(100% - 21em);\n\t\tleft: 9.8em;\n\t\ttop: .7em;\n\t\theight: 1.6em;\n\t\tbottom: auto;\n\t}\n\thtml.d #barpos,\n\thtml.b #barpos,\n\thtml.d #barbuf,\n\thtml.b #barbuf {\n\t\twidth: calc(100% - 19em);\n\t\tleft: 8em;\n\t}\n\t#widget {\n\t\tbottom: -3.2em;\n\t\theight: 3.2em;\n\t}\n\t#pvol {\n\t\tmax-width: 9em;\n\t}\n\thtml.d #ops,\n\thtml.b #ops {\n\t\tpadding-left: 1.7em;\n\t}\n\thtml.d .opview,\n\thtml.b .opview {\n\t\tmargin: 1em;\n\t}\n\thtml.d #path,\n\thtml.b #path {\n\t\tpadding-left: 1.3em;\n\t}\n}\n@media (max-width: 35em) {\n\t#ops>a[data-dest=\"new_md\"],\n\t#ops>a[data-dest=\"msg\"] {\n\t\tdisplay: none;\n\t}\n\t#op_mkdir.act+div,\n\t#op_mkdir.act+div+div {\n\t\tdisplay: block;\n\t\tmargin-top: 1em;\n\t}\n}\n@media (max-width: 54em) {\n\thtml.b #ops {\n\t\tmargin-top: 1.7em;\n\t}\n}\n@supports (display: grid) and (gap: 1em) {\n\t#ggrid {\n\t\tdisplay: grid;\n\t\tmargin: 0em 0.25em;\n\t\tpadding: unset;\n\t\tgrid-template-columns: repeat(auto-fit,var(--grid-sz));\n\t\tjustify-content: center;\n\t\tgap: 1em;\n\t}\n\n\thtml.b #ggrid {\n\t\tpadding: 0 0 2em 0;\n\t\tgap: 1em 1.5em;\n\t}\n\n\t#ggrid > a {\n\t\tmargin: unset;\n\t\tpadding: unset;\n  \t}\n\n  \t#ggrid>a>span {\n\t\ttext-align: center;\n\t\tpadding: .2em .2em .15em .2em;\n\t}\n}\n\n\n\n\n\n@media (prefers-reduced-motion) {\n\t@keyframes spin { }\n\t@keyframes gexit { }\n\t@keyframes bounce { }\n\t@keyframes bounceFromLeft { }\n\t@keyframes bounceFromRight { }\n\n\t#ggrid>a:before,\n\t#widget.anim,\n\t#u2tabw,\n\t.dropdesc,\n\t.dropdesc b,\n\t.dropdesc>div>div {\n\t\ttransition: none;\n\t}\n\t#bbox-next,\n\t#bbox-prev,\n\t#bbox-btns {\n\t\ttransition: background-color .3s ease, color .3s ease;\n\t}\n}\nhtml.ey {\n  --negative-space: 0em; /* Use this to change the global spacing of the 95 theme */\n  --font-main: consolas;\n  --font-serif: consolas;\n  --font-mono: consolas;\n  --w: #fff;\n  --w2: #dfdfdf;\n  --w3: grey;\n  --fg: #000;\n  --fg-max: #0000ff;\n  --fg-weak: #0000ff;\n  --bg: #c6c3c6;\n  --bg-d3: #ff0;\n  --bg-d2: var(--w3);\n  --bg-d1: var(--bg);\n  --bg-u2: var(--bg);\n  --bg-u3: var(--bg);\n  --bg-u5: var(--shadow-color-2);\n  --tab-alt: #00f;\n  --g-fsel-bg: #00f;\n  --g-sel-bg: #00f;\n  --g-fsel-b1: #fff;\n  --row-alt: var(--w);\n  --scroll: var(--silver);\n  --f-sel-sh: transparent;\n  --a: #000;\n  --a-b: #fff;\n  --a-hil: #fff;\n  --a-h-bg: var(--bg);\n  --a-dark: var(--a);\n  --a-gray: var(--fg-weak);\n  --btn-fg: var(--fg);\n  --btn-bg: var(--bg);\n  --btn-h-fg: var(--fg);\n  --btn-h-bg: var(--bg);\n  --btn-1-fg: var(--fg);\n  --btn-1-bg: var(--bg);\n  --btn-1h-bg: var(--bg-d3);\n  --txt-sh: a;\n  --txt-bg: var(--white);\n  --u2-b1-bg: var(--w2);\n  --u2-b2-bg: var(--w2);\n  --u2-txt-bg: var(--w2);\n  --u2-tab-bg: a;\n  --u2-tab-1-bg: var(--w2);\n  --sort-1: var(--fg-weak);\n  --tree-bg: var(--w);\n  --g-b1: a;\n  --g-b2: a;\n  --g-f-bg: var(--w2);\n  --f-sh1: 0.1;\n  --f-sh2: 0.02;\n  --f-sh3: 0.1;\n  --f-h-b1: a;\n  --srv-1: var(--w);\n  --srv-3: var(--a);\n  --mp-sh: a;\n  --black: #000;\n  --white: #fff;\n  --grey: grey;\n  --silver: silver;\n  --transparent: transparent;\n  --shadow-color-1: #0a0a0a;\n  --shadow-color-2: #808080;\n  --border-dashed-black: 1px dashed var(--black);\n  --radius: 0;\n  --focus-outline: 1px dashed var(--black);\n  --hover-outline: 1px dotted var(--black);\n  --fm-off: var(--w3);\n  --ttlbar: linear-gradient(90deg, navy, #1084d0);\n  --inset-bg: var(--white);\n  --scroll-bkg: var(--white);\n\n  /*All sides*/\n  --shadow-outset: inset -1px -1px var(--shadow-color-1),\n    inset 1px 1px var(--white), inset -2px -2px var(--grey),\n    inset 2px 2px var(--w2);\n\n  --shadow-inset: inset -1px -1px var(--white),\n    inset 1px 1px var(--shadow-color-1), inset -2px -2px var(--w2),\n    inset 2px 2px var(--shadow-color-2);\n\n  --shadow-input: inset -1px -1px var(--white), inset 1px 1px var(--grey),\n    inset -2px -2px var(--w2), inset 2px 2px var(--shadow-color-1);\n\n  /*Indiv sides*/\n  --shadow-outset-bottom: inset 0 -1px var(--shadow-color-1),\n    inset 0 -2px var(--grey);\n  --shadow-outset-right: inset -1px 0 var(--shadow-color-1),\n    inset -2px 0 var(--grey);\n  --shadow-outset-left: inset 1px 0 var(--white), inset 2px 0 var(--w2);\n  --shadow-outset-top: inset 0 1px var(--white), inset 0 2px var(--w2);\n\n  --shadow-inset-bottom: inset 0 -1px var(--white), inset 0 -2px var(--w2);\n  --shadow-inset-right: inset -1px 0 var(--white), inset -2px 0 var(--w2);\n  --shadow-inset-left: inset 1px 0 var(--shadow-color-1),\n    inset 2px 0 var(--shadow-color-2);\n  --shadow-inset-top: inset 0 1px var(--shadow-color-1),\n    inset 0 2px var(--shadow-color-2);\n}\nhtml.ez {\n  --negative-space: 0em; /* Use this to change the global spacing of your theme :) */\n  --font-main: consolas;\n  --font-serif: consolas;\n  --font-mono: consolas;\n  --w: #fff;\n  --w2: var(--inset-bg);\n  --w3: grey;\n  --fg: #cfcfcf;\n  --fg-max: #47b8ff;\n  --fg-weak: #47b8ff;\n  --bg: #383838;\n  --bg-d3: #600000;\n  --bg-d2: var(--shadow-color-1);\n  --bg-d1: var(--bg);\n  --u2-tab-1-fg: #ff0;\n  --bg-u2: var(--bg);\n  --bg-u3: var(--bg);\n  --bg-u5: var(--shadow-color-2);\n  --tab-alt: #47b8ff;\n  --g-fsel-bg: #0000b7;\n  --g-sel-bg: #00f;\n  --g-fsel-b1: #fff;\n  --row-alt: #555555;\n  --scroll: #555555;\n  --f-sel-sh: transparent;\n  --a: var(--fg);\n  --a-b: var(--fg);\n  --a-hil: var(--fg);\n  --btn-1h-bg: var(--bg-d3);\n  --a-h-bg: var(--bg);\n  --a-dark: var(--a);\n  --a-gray: var(--fg-weak);\n  --btn-fg: var(--white);\n  --btn-bg: var(--bg);\n  --btn-h-fg: var(--white);\n  --btn-h-bg: var(--bg);\n  --btn-1-fg: var(--white);\n  --btn-1-bg: var(--bg);\n  --txt-sh: a;\n  --u2-b1-bg: var(--w2);\n  --u2-b2-bg: var(--w2);\n  --u2-txt-bg: var(--w2);\n  --u2-tab-bg: a;\n  --u2-tab-1-bg: var(--w2);\n  --sort-1: var(--fg-weak);\n  --g-b1: a;\n  --g-b2: a;\n  --g-f-bg: var(--w2);\n  --f-sh1: 0.1;\n  --f-sh2: 0.02;\n  --f-sh3: 0.1;\n  --f-h-b1: a;\n  --srv-1: var(--w);\n  --srv-3: var(--a);\n  --mp-sh: a;\n  --black: #000;\n  --white: #fff;\n  --grey: grey;\n  --silver: #858585;\n  --transparent: transparent;\n  --shadow-color-1: #101010;\n  --shadow-color-2: #1f1f1f;\n  --border-dashed-black: 1px dashed var(--shadow-color-1);\n  --radius: 0;\n  --focus-outline: 1px dashed var(--white);\n  --hover-outline: 1px dotted var(--white);\n  --fm-off: var(--w3);\n  --ttlbar: linear-gradient(90deg, var(--shadow-color-1) 20%, #888888);\n  --inset-bg: #3f3f3f;\n  --tree-bg: var(--inset-bg);\n  --txt-bg: var(--inset-bg);\n  --scroll-bkg: var(--black);\n\n  /*All sides*/\n  --shadow-outset: inset -1px -1px var(--shadow-color-1), inset 1px 1px #878787,\n    inset -2px -2px var(--shadow-color-2), inset 2px 2px #575757;\n\n  --shadow-inset: inset -1px -1px #878787, inset 1px 1px var(--shadow-color-1),\n    inset -2px -2px #575757, inset 2px 2px var(--shadow-color-2);\n\n  --shadow-input: inset -1px -1px var(--white),\n    inset 1px 1px var(--shadow-color-2), inset -2px -2px #575757,\n    inset 2px 2px var(--shadow-color-1);\n\n  --shadow-outset-bottom: inset 0 -1px var(--shadow-color-1),\n    inset 0 -2px var(--shadow-color-2);\n  --shadow-outset-right: inset -1px 0 var(--shadow-color-1),\n    inset -2px 0 var(--shadow-color-2);\n  --shadow-outset-left: inset 1px 0 #878787, inset 2px 0 #575757;\n  --shadow-outset-top: inset 0 1px #878787, inset 0 2px #575757;\n\n  --shadow-inset-bottom: inset 0 -1px #878787, inset 0 -2px #575757;\n  --shadow-inset-right: inset -1px 0 #878787, inset -2px 0 #575757;\n  --shadow-inset-left: inset 1px 0 var(--shadow-color-1),\n    inset 2px 0 var(--shadow-color-2);\n  --shadow-inset-top: inset 0 1px var(--shadow-color-1),\n    inset 0 2px var(--shadow-color-2);\n}\n\nhtml.e {\n  text-shadow: none;\n}\nhtml.e #files,\nhtml.e #u2conf input[type=\"checkbox\"]:hover + label,\nhtml.e .tgl.btn.on:hover,\nhtml.e body {\n  background: var(--bg);\n}\nhtml.e #pctl a,\nhtml.e #repl,\nhtml.e #u2conf a,\nhtml.e #u2conf input[type=\"checkbox\"] + label,\nhtml.e #wfp a,\nhtml.e .btn,\nhtml.e .eq_step,\nhtml.e input[type=\"submit\"] {\n  box-shadow: var(--shadow-outset);\n  border-radius: var(--radius);\n  background: var(--bg);\n  border: 0;\n}\na.s0r,\nhtml.e #ghead a.s0,\nhtml.e #u2conf input[type=\"checkbox\"]:checked + label,\nhtml.e .tgl.btn.on,\nhtml.e input[type=\"submit\"]:active {\n  box-shadow: var(--shadow-inset) !important;\n}\nhtml.e #ops a:hover,\nhtml.e #pctl a:hover,\nhtml.e #repl:hover,\nhtml.e #u2conf a:hover,\nhtml.e #u2conf input[type=\"checkbox\"]:hover + label,\nhtml.e #wfp a:hover,\nhtml.e .btn:hover,\nhtml.e .eq_step:hover,\nhtml.e input[type=\"submit\"]:hover {\n  outline: var(--hover-outline);\n  outline-offset: -4px;\n}\nhtml.e .ntree a:hover,\nhtml.e :focus,\nhtml.e :focus + label,\nhtml.e a:active,\nhtml.e tr:focus,\ninput[type=\"text\"]:focus {\n  outline: var(--focus-outline) !important;\n}\nhtml.e tr:focus {\n  box-shadow: none;\n}\nhtml.e #pctl a:focus,\nhtml.e #repl:hover,\nhtml.e #u2conf input[type=\"checkbox\"]:focus + label,\nhtml.e #wfp a:focus,\nhtml.e .btn:focus,\nhtml.e .eq_step:focus {\n  border: 0 !important;\n  outline: var(--focus-outline) !important;\n  outline-offset: 2px;\n  box-shadow: var(--shadow-outset) !important;\n}\nhtml.e #files tbody,\nhtml.e #u2cards a.act {\n  box-shadow: var(--shadow-inset);\n}\nhtml.e #files {\n  border: 2px groove var(--transparent);\n  box-sizing: border-box;\n  width: 100%;\n  padding: 0.3em;\n  top: 0;\n  border: 0;\n}\nhtml.e #files tbody tr td,\nhtml.e #files thead th {\n  border-radius: var(--radius);\n}\n#files td {\n  background: var(--w2);\n}\nhtml.e #files tr {\n  background-color: var(--black);\n}\nhtml.e #srv_info span,\nhtml.e label {\n  color: var(--btn-fg) !important;\n}\nhtml.e #acc_info {\n  background: var(--transparent);\n  color: var(--white);\n  height: 2em;\n  left: 1em;\n  width: fit-content;\n}\nhtml.e #acc_info,\nhtml.e #ops,\nhtml.e #srv_info {\n  display: flex;\n  align-items: center;\n}\nhtml.e #acc_info span.warn,\nhtml.e #acc_info a {\n  color: var(--white);\n}\nhtml.e #flogout:before {\n  padding-left: 0.2em;\n  padding-right: 0.4em;\n  content: \" | \";\n}\nhtml.e #blogout {\n  color: var(--w);\n  box-shadow: none;\n  background: transparent;\n}\nhtml.e .opwide > div {\n  border-left: 1px solid var(--fg);\n}\nhtml.e #srv_info {\n  background: var(--transparent);\n  color: var(--white);\n  height: fit-content;\n  top: 3.2em;\n  left: 1em;\n  gap: 0.2em;\n}\nhtml.e #u2cards a.act {\n  padding: 0.2em 1em;\n}\nhtml.e #u2btn {\n  border: var(--border-dashed-black);\n  border-radius: var(--border-radius);\n  transform: translateY(30%);\n}\nhtml.e #ops,\nhtml.e #ops a {\n  border-radius: var(--radius);\n}\n@media only screen and (max-width: 600px) {\n  html.e #acc_info {\n    background: var(--transparent);\n    color: var(--white);\n    height: fit-content;\n    align-items: center;\n    top: 3.2em;\n    right: 1em;\n    left: auto;\n    display: flex;\n    gap: 0.2em;\n  }\n  html.e #u2btn {\n    transform: none;\n  }\n}\nhtml.e #ops {\n  background: var(--ttlbar);\n  /*HC*/\n  box-shadow: inset 0-1px grey, inset 0-2px var(--shadow-color-1);\n  height: 2em;\n  gap: 0.6em;\n  padding: 0.2em;\n  flex-direction: row-reverse;\n  margin-bottom: 1.2em;\n}\nhtml.e #srch_form,\nhtml.e .opbox {\n  padding-bottom: 1em;\n  padding-top: 1em;\n  max-width: 100vw;\n}\nhtml.e #ghead,\nhtml.e #ops a {\n  align-items: center;\n  display: flex;\n}\nhtml.e #ops a {\n  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5);\n  height: 1.4em;\n  padding: 0;\n  box-shadow: var(--shadow-outset);\n  background: var(--bg);\n  aspect-ratio: 1/1;\n  justify-content: center;\n  font-size: 1.25em;\n  z-index: 4;\n}\nhtml.e #blogout:focus,\nhtml.e #ops a:focus {\n  outline: 1px dashed var(--w) !important;\n}\n\nhtml.e #blogout:hover {\n  text-decoration: underline;\n}\n\nhtml.e #ops > a:not(:first-child).act {\n  height: 1.4em;\n  width: 1.4em;\n  padding-bottom: 0.3em;\n  margin-top: 0.3em;\n  border-top-left-radius: 3px;\n  border-top-right-radius: 3px;\n  box-shadow: var(--shadow-inset-left), var(--shadow-inset-top),\n    var(--shadow-inset-right);\n  z-index: 6;\n}\nhtml.e #ops a.act {\n  box-shadow: var(--shadow-inset);\n  border-bottom: 0;\n}\nhtml.e a:active {\n  border: 0;\n}\nhtml.e :focus,\nhtml.e :focus + label {\n  border: 0 !important;\n  outline-offset: 1px;\n  border-radius: var(--radius) !important;\n  box-shadow: inherit;\n}\nhtml.e #opa_x {\n  text-shadow: 0 0 0 var(--transparent) !important;\n  color: var(--bg) !important;\n  display: flex;\n}\nhtml.e #opa_x:before {\n  content: \"⨯\";\n  color: var(--fg) !important;\n  margin-top: -0.1em;\n  font-size: 1.75em;\n  position: absolute;\n}\nhtml.e .opbox {\n  margin: -1.2em 0 0;\n  box-shadow: var(--shadow-inset-bottom), var(--shadow-inset-left),\n    var(--shadow-inset-right);\n  border-radius: var(--radius);\n  z-index: 5;\n  background: var(--bg);\n}\nhtml.e #srch_form {\n  margin: 0;\n  border-radius: var(--radius);\n}\nhtml.e #op_unpost {\n  max-width: 100vw;\n  margin: 0;\n}\nhtml.e label:focus {\n  box-shadow: 0 0;\n}\nhtml.e #tree {\n  box-shadow: none;\n  padding-right: 5px;\n}\nhtml.e #tt {\n  background: var(--w2);\n}\nhtml.e .mdo a {\n  background: 0 0;\n  text-decoration: underline;\n}\nhtml.e .mdo code,\nhtml.e .mdo pre {\n  color: var(--white);\n  background: var(--w2);\n  border: 0;\n}\nhtml.e .mdo h1,\nhtml.e .mdo h2 {\n  background: 0 0;\n  border-color: var(--w2);\n}\nhtml.e #tt,\nhtml.e .mdo ol ol,\nhtml.e .mdo ol ul,\nhtml.e .mdo ul ol,\nhtml.e .mdo ul ul {\n  border-color: var(--w2);\n}\nhtml.e .mdo li > em,\nhtml.e .mdo p > em,\nhtml.e .mdo td > em {\n  color: #fd0;\n}\nhtml.e input.txtbox,\nhtml.e input[type=\"text\"],\nhtml.e select {\n  background-color: var(--txt-bg);\n  box-shadow: var(--shadow-input) !important;\n  box-sizing: border-box;\n  padding: 3px 4px;\n  border-radius: var(--radius);\n  border: 0;\n}\nhtml.e #gfiles {\n  box-shadow: var(--shadow-outset);\n  background: var(--bg);\n  padding: 0.4em;\n  display: flex;\n  flex-direction: column;\n  gap: 0.3em;\n}\nhtml.e #ggrid {\n  background-color: var(--inset-bg);\n  box-shadow: var(--shadow-input);\n  padding: 1.5em;\n  margin: 0;\n  overflow-x: scroll;\n}\nhtml.e #ghead {\n  margin: 0;\n  justify-content: flex-end;\n  gap: 0.4em;\n  padding: 0;\n  overflow: auto;\n  top: 0px;\n  border-radius: 0px;\n}\nhtml.e #ghead a {\n  margin: 0;\n  border-radius: var(--radius);\n}\nhtml.e ::-webkit-scrollbar,\nhtml.e::-webkit-scrollbar {\n  width: 16px !important;\n  height: 16px !important;\n  background: var(--transparent) !important;\n}\nhtml.e ::-webkit-scrollbar-button,\nhtml.e ::-webkit-scrollbar-thumb,\nhtml.e::-webkit-scrollbar-button,\nhtml.e::-webkit-scrollbar-thumb {\n  width: 16px !important;\n  height: 16px !important;\n  background: var(--scroll) !important;\n  /*HC*/\n  box-shadow: var(--shadow-outset);\n  border: 1px solid !important;\n  border-color: var(--silver) var(--black) var(--black) var(--silver) !important;\n}\nhtml.e ::-webkit-scrollbar-track,\nhtml.e::-webkit-scrollbar-track {\n  image-rendering: optimize-contrast !important;\n  background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAyIDIiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyI+CjxtZXRhZGF0YT5NYWRlIHdpdGggUGl4ZWxzIHRvIFN2ZyBodHRwczovL2NvZGVwZW4uaW8vc2hzaGF3L3Blbi9YYnh2Tmo8L21ldGFkYXRhPgo8cGF0aCBzdHJva2U9IiNjMGMwYzAiIGQ9Ik0wIDBoMU0xIDFoMSIgLz4KPC9zdmc+) !important;\n  background-position: 0 0 !important;\n  background-repeat: repeat !important;\n  background-size: 2px !important;\n  background: var(--scroll-bkg);\n}\n#tree::-webkit-scrollbar,\n#tree::-webkit-scrollbar-track {\n  background: var(--scroll-bkg);\n}\nhtml.e ::-webkit-scrollbar-button,\nhtml.e::-webkit-scrollbar-button {\n  background-repeat: no-repeat !important;\n  background-size: 16px !important;\n}\nhtml.e ::-webkit-scrollbar-button:single-button:vertical:decrement,\nhtml.e::-webkit-scrollbar-button:single-button:vertical:decrement {\n  background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTcgNWgxTTYgNmgzTTUgN2g1TTQgOGg3IiAvPgo8L3N2Zz4=) !important;\n}\nhtml.e ::-webkit-scrollbar-button:single-button:vertical:increment,\nhtml.e::-webkit-scrollbar-button:single-button:vertical:increment {\n  background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTQgNWg3TTUgNmg1TTYgN2gzTTcgOGgxIiAvPgo8L3N2Zz4=) !important;\n}\nhtml.e ::-webkit-scrollbar-button:single-button:horizontal:decrement,\nhtml.e::-webkit-scrollbar-button:single-button:horizontal:decrement {\n  background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTggM2gxTTcgNGgyTTYgNWgzTTUgNmg0TTYgN2gzTTcgOGgyTTggOWgxIiAvPgo8L3N2Zz4=) !important;\n}\nhtml.e ::-webkit-scrollbar-button:single-button:horizontal:increment,\nhtml.e::-webkit-scrollbar-button:single-button:horizontal:increment {\n  background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTYgM2gxTTYgNGgyTTYgNWgzTTYgNmg0TTYgN2gzTTYgOGgyTTYgOWgxIiAvPgo8L3N2Zz4=) !important;\n}\nhtml.e ::-webkit-scrollbar-corner,\nhtml.e::-webkit-scrollbar-corner {\n  background: var(--silver) !important;\n}\nhtml,\nhtml.e #tree {\n  scrollbar-color: inherit !important;\n}\nhtml.e #tree {\n  background: var(--bg);\n  padding-left: 0.4em;\n  padding-top: 0;\n  margin-left: var(--negative-space);\n}\nhtml.e.noscroll #tree {\n  /*HC*/\n  box-shadow: 1px 1px var(--grey), 2px 2px var(--shadow-color-1),\n    var(--shadow-outset-bottom);\n}\nhtml.e #treeh {\n  background: var(--bg);\n  box-shadow: var(--shadow-outset-top), var(--shadow-outset-bottom);\n  width: calc(1.5em + var(--nav-sz) - var(--sbw));\n  height: 2.4em;\n  border: none;\n  top: -2px;\n  display: flex;\n  align-items: center;\n  gap: 0.6em;\n}\nhtml.e #treeh .btn {\n  margin: 0px;\n  top: auto;\n}\nhtml.e #tree ul {\n  border-left: var(--border-dashed-black);\n  margin-left: 2.15em;\n}\nhtml.e .ntree a:first-child {\n  font-family: scp, monospace, monospace;\n  font-size: 1.2em;\n  line-height: 0;\n  background: var(--inset-bg);\n  aspect-ratio: 1/1;\n  text-align: center;\n  align-content: center;\n  border-radius: var(--radius) !important;\n  padding: 0.057em;\n  border: 1px solid var(--black);\n}\nhtml.e .ntree a:first-child:after {\n  content: \".\";\n  position: absolute;\n  border-top: var(--border-dashed-black);\n  color: var(--transparent);\n  font-size: 0.9em;\n  margin-left: 0.13em;\n}\nhtml.e #treeul {\n  border: 0 !important;\n  position: static;\n  margin: 0 !important;\n  min-height: 100%;\n  height: max-content;\n}\nhtml.e .ntree a:last-of-type:before {\n  content: \"📁\";\n  margin-left: 0.3em;\n}\nhtml.e .ntree {\n  padding-left: 1em !important;\n  padding-top: 0.3em !important;\n  background: var(--inset-bg);\n  box-shadow: var(--shadow-inset-left), var(--shadow-inset-bottom);\n}\nhtml.e #tree li {\n  margin-left: -0.5em;\n  border-top: 0;\n}\nhtml.e .ntree a:hover {\n  outline-offset: -2px;\n  color: var(--fg);\n  border-radius: var(--radius) !important;\n}\nhtml.e #treepar {\n  width: calc(-1em + var(--nav-sz) - var(--sbw));\n  overflow: hidden;\n  left: -0.7em;\n  box-shadow: var(--shadow-inset-left), var(--shadow-inset-top);\n  border-left: 0 !important;\n  border-bottom: var(--border-dashed-black);\n  margin-left: calc(2.1em - (1em - var(--negative-space))) !important;\n}\nhtml.e #path,\nhtml.e #widgeti,\nhtml.e #wtoggle,\nhtml.e #wtoggle a,\nhtml.e #files,\nhtml.e #files thead th,\nhtml.e #ghead a,\nhtml.e #tree {\n  box-shadow: var(--shadow-outset);\n}\nhtml.e.noscroll #treepar {\n  width: calc(var(--nav-sz) - 1em);\n}\nhtml.e #docul {\n  border-left: 0 !important;\n  margin-left: 0 !important;\n}\nhtml.e #wrap {\n  transform: translateX(calc((var(--negative-space) * 2) - 1.2em));\n  padding-right: var(--negative-space);\n  position: relative;\n  margin-right: calc((var(--negative-space) * 2) - 1.2em);\n  margin-top: var(--negative-space);\n  margin-left: 1.2em;\n  /*overflow-x: auto; fix for OOB table when screen space is limited (mobile), but removes sticky header*/\n}\nhtml.e input[type=\"radio\"] {\n  accent-color: #232323;\n}\nhtml.e #path {\n  width: calc(100% - 0.4em);\n  display: flex;\n  align-items: center;\n  margin: 0;\n  padding: 0.2em;\n  overflow-x: auto;\n}\nhtml.e #path i {\n  border: 1px solid var(--w);\n  border-color: var(--w);\n  margin: 0;\n  border-width: 0.1em 0.1em 0 0;\n  height: 0.5em;\n  width: 0.5em;\n}/*\nhtml.e #hovertree:after {\n  color: red;\n  content: \"BUGGY\";\nhtml.ez #hovertree:after {\n  color: rgb(255 98 98);\n  content: \"BUGGY\";\n}\n}*/\nhtml.e #widget {\n  box-shadow: 0 0;\n  border: 0 !important;\n}\nhtml.e #wtico,\nhtml.e #zip1 {\n  box-shadow: 0 0 !important;\n}\nhtml.e #wtgrid {\n  top: -0.09em;\n}\nhtml.e #wfs,\nhtml.e #wm3u,\nhtml.e #wnp,\nhtml.e #wzip {\n  border-width: 0 1px 0 0;\n}\nhtml.e #wfm.act + #wzip1 + #wzip,\nhtml.e #wfm.act + #wzip1 + #wzip + #wnp {\n  border-left-width: 1px;\n}\nhtml.e #barpos {\n  /* border-radius: var(--radius); */\n  box-shadow: var(--shadow-inset);\n}\nhtml.e #goh + span {\n  border-left: 0.1em solid var(--bg-u5);\n}\nhtml.e #wfp {\n  margin: var(--negative-space);\n  font-size: 0;\n  display: inline-block;\n}\nhtml.e #wfp a {\n  font-size: large;\n  display: inline-block;\n}\nhtml.e #repl {\n  font-size: large;\n  padding: 0.33em;\n  right: calc(var(--negative-space) * 0.89);\n  position: absolute;\n}\nhtml.e #epi {\n  text-align: center;\n  text-wrap-mode: nowrap;\n  margin: 0px;\n}\n\nhtml.e #epi.logue:not(.mdo) {\n  padding: 0.8em;\n  box-shadow: var(--shadow-outset);\n}\n\nhtml.e #epi.logue.mdo {\n  padding-left: 3px;\n}\n\nhtml.e #doc {\n  box-shadow: var(--shadow-inset);\n  background: var(--inset-bg);\n  margin: 0.2em;\n  border-radius: var(--radius);\n}\n\nhtml.e #detree {\n  padding: 0px;\n}\n\n#rcm {\n\tposition: absolute;\n\tdisplay: none;\n\tbackground: #fff;\n\tbackground: var(--bg);\n\tborder: 1px solid var(--bg-u3);\n\toutline: none;\n\tborder-radius: .3rem;\n\tbox-shadow: 0 0 .3rem var(--bg-d3);\n\tz-index: 3;\n}\n\n#rcm > * {\n\tdisplay: block;\n}\n\n#rcm > a {\n\tpadding: .2rem .3rem;\n}\n\n#rcm > .hide {\n\tdisplay: none;\n}\n\n#rcm > a:hover {\n\tbackground: var(--bg-d2);\n}\n\n#rcm > .sep {\n\tmargin: 0 .2rem;\n\tborder-bottom: 1px solid var(--a-gray);\n}\n\n#tempname {\n\tcolor: var(--fg);\n\tbackground: var(--txt-bg);\n\tborder: none;\n\tbox-shadow: 0 0 2px var(--txt-sh);\n\tborder-bottom: 1px solid #999;\n\tborder-color: var(--a);\n\tborder-radius: .2em;\n\tpadding: .2em .3em;\n}\n\n.selbox {\n    position: fixed;\n    border: .5em solid #f0f;\n    border: .2em solid var(--btn-1h-bg);\n    background-color: rgba(128, 128, 128, 0.6);\n    background-color: rgb(from var(--btn-1h-bg) r g b / 0.5);\n    pointer-events: none;\n\tz-index: 99;\n}\n"
  },
  {
    "path": "copyparty/web/browser.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" id=\"ht_brw\">\n\n<head>\n\t<meta charset=\"utf-8\">\n\t<title>{{ title }}</title>\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=0.8, minimum-scale=0.6\">\n\t<meta name=\"theme-color\" content=\"#{{ tcolor }}\">\n\t<link rel=\"stylesheet\" media=\"screen\" href=\"{{ r }}/.cpr/w/ui.css?_={{ ts }}\">\n\t<link rel=\"stylesheet\" media=\"screen\" href=\"{{ r }}/.cpr/w/browser.css?_={{ ts }}\">\n{{ html_head }}\n\t{%- if css %}\n\t<link rel=\"stylesheet\" media=\"screen\" href=\"{{ css }}_={{ ts }}\">\n\t{%- endif %}\n</head>\n\n<body>\n\t<div id=\"ops\"></div>\n\n\t<div id=\"op_search\" class=\"opview\">\n\t\t{%- if have_tags_idx %}\n\t\t<div id=\"srch_form\" class=\"tags opbox\"></div>\n\t\t{%- else %}\n\t\t<div id=\"srch_form\" class=\"opbox\"></div>\n\t\t{%- endif %}\n\t\t<div id=\"srch_q\"></div>\n\t</div>\n\n\t<div id=\"op_player\" class=\"opview opbox opwide\"></div>\n\n\t<div id=\"op_bup\" class=\"opview opbox {% if not ls0 %}act{% endif %}\">\n\t\t<div id=\"u2err\"></div>\n\t\t<form method=\"post\" enctype=\"multipart/form-data\" accept-charset=\"utf-8\" action=\"{{ url_suf }}\">\n\t\t\t<input type=\"hidden\" name=\"act\" value=\"bput\" />\n\t\t\t<input type=\"file\" name=\"f\" multiple /><br />\n\t\t\t<input type=\"submit\" value=\"start upload\">\n\t\t</form>\n\t\t<a id=\"bbsw\" href=\"?b=u\" rel=\"nofollow\"><br />switch to basic browser</a>\n\t</div>\n\n\t<div id=\"op_mkdir\" class=\"opview opbox {% if not ls0 %}act{% endif %}\">\n\t\t<form method=\"post\" enctype=\"multipart/form-data\" accept-charset=\"utf-8\" action=\"{{ url_suf }}\">\n\t\t\t<input type=\"hidden\" name=\"act\" value=\"mkdir\" />\n\t\t\t📂<input type=\"text\" name=\"name\" class=\"i\" placeholder=\"awesome mix vol.1\">\n\t\t\t<input type=\"submit\" value=\"make directory\">\n\t\t</form>\n\t</div>\n\n\t<div id=\"op_new_md\" class=\"opview opbox\">\n\t\t<form method=\"post\" enctype=\"multipart/form-data\" accept-charset=\"utf-8\" action=\"{{ url_suf }}\">\n\t\t\t<input type=\"hidden\" name=\"act\" value=\"new_md\" />\n\t\t\t📝<input type=\"text\" name=\"name\" class=\"i\" placeholder=\"weekend-plans\">\n\t\t\t<input type=\"submit\" value=\"new file\">\n\t\t</form>\n\t\t<span id=\"new_mdi\"></span>\n\t</div>\n\n\t<div id=\"op_msg\" class=\"opview opbox {% if not ls0 %}act{% endif %}\">\n\t\t<form method=\"post\" enctype=\"application/x-www-form-urlencoded\" accept-charset=\"utf-8\" action=\"{{ url_suf }}\">\n\t\t\t📟<input type=\"text\" name=\"msg\" class=\"i\" placeholder=\"lorem ipsum dolor sit amet\">\n\t\t\t<input type=\"submit\" value=\"send msg to srv log\">\n\t\t</form>\n\t</div>\n\n\t<div id=\"op_unpost\" class=\"opview opbox\"></div>\n\n\t<div id=\"op_up2k\" class=\"opview\"></div>\n\n\t<div id=\"op_cfg\" class=\"opview opbox opwide\"></div>\n\n\t<h1 id=\"path\">\n\t\t<a href=\"#\" id=\"entree\">🌲</a>\n\t\t{%- for n in vpnodes %}\n\t\t<a href=\"{{ r }}/{{ n[0] }}\">{{ n[1] }}</a>\n\t\t{%- endfor %}\n\t</h1>\n\n\t<div id=\"tree\"></div>\n\n<div id=\"wrap\">\n\n\t{%- if doc %}\n\t<div id=\"bdoc\"><pre>{{ doc|e }}</pre></div>\n\t{%- else %}\n\t<div id=\"bdoc\"></div>\n\t{%- endif %}\n\n\t<div id=\"pro\" class=\"logue\">{{ \"\" if sb_lg else logues[0] }}</div>\n\n\t<table id=\"files\">\n\t\t<thead>\n\t\t\t<tr>\n\t\t\t\t<th name=\"lead\"><span>c</span></th>\n\t\t\t\t<th name=\"href\"><span>File Name</span></th>\n\t\t\t\t<th name=\"sz\" sort=\"int\"><span>Size</span></th>\n\t\t\t\t{%- for k in taglist %}\n\t\t\t\t\t{%- if k.startswith('.') %}\n\t\t\t\t<th name=\"tags/{{ k }}\" sort=\"int\"><span>{{ k[1:] }}</span></th>\n\t\t\t\t\t{%- else %}\n\t\t\t\t<th name=\"tags/{{ k }}\"><span>{{ k[0]|upper }}{{ k[1:] }}</span></th>\n\t\t\t\t\t{%- endif %}\n\t\t\t\t{%- endfor %}\n\t\t\t\t<th name=\"ext\"><span>T</span></th>\n\t\t\t\t<th name=\"ts\"><span>Date</span></th>\n\t\t\t</tr>\n\t\t</thead>\n<tbody>\n\n{%- for f in files %}\n<tr><td>{{ f.lead }}</td><td><a href=\"{{ f.href }}\">{{ f.name|e }}</a></td><td>{{ f.sz }}</td>\n{%- if f.tags is defined %}\n\t{%- for k in taglist %}<td>{{ f.tags[k]|e }}</td>{%- endfor %}\n{%- endif %}<td>{{ f.ext }}</td><td>{{ f.dt }}</td></tr>\n{%- endfor %}\n\n\t\t</tbody>\n\t</table>\n\n\t<div id=\"epi\" class=\"logue\">{{ \"\" if sb_lg else logues[1] }}</div>\n\n\t<h2 id=\"wfp\"><a href=\"{{ r }}/?h\" id=\"goh\">control-panel</a></h2>\n\n\t<a href=\"#\" id=\"repl\">π</a>\n\n</div>\n\n\t<div id=\"srv_info\"><span>{{ srv_info }}</span></div>\n\n\t<div id=\"widget\"></div>\n\n\t<div id=\"rcm\" tabindex=\"0\"></div>\n\n\t<script>\n\t\tvar SR = \"{{ r }}\",\n\t\t\tCGV1 = {{ cgv1 }},\n\t\t\tCGV = {{ cgv|tojson }},\n\t\t\tTS = \"{{ ts }}\",\n\t\t\tdtheme = \"{{ dtheme }}\",\n\t\t\tlang = \"{{ lang }}\",\n\t\t\tdfavico = \"{{ favico }}\",\n\t\t\thave_tags_idx = {{ have_tags_idx }},\n\t\t\tlogues = {{ logues|tojson if sb_lg else \"[]\" }},\n\t\t\tls0 = {{ ls0|tojson }};\n\n\t\tvar STG = window.localStorage;\n\t\tdocument.documentElement.className = (STG && STG.cpp_thm) || dtheme;\n\t</script>\n\t<script src=\"{{ r }}/.cpr/w/util.js?_={{ ts }}\"></script>\n\t{%- if lang != \"eng\" %}\n\t<script src=\"{{ r }}/.cpr/w/tl/{{ lang }}.js?_={{ ts }}\"></script>\n\t{%- endif %}\n\t<script src=\"{{ r }}/.cpr/w/baguettebox.js?_={{ ts }}\"></script>\n\t<script src=\"{{ r }}/.cpr/w/browser.js?_={{ ts }}\"></script>\n\t<script src=\"{{ r }}/.cpr/w/up2k.js?_={{ ts }}\"></script>\n\t{%- if js %}\n\t<script src=\"{{ js }}_={{ ts }}\"></script>\n\t{%- endif %}\n\t<script>\n\t\tDate.now();function jsldp(a,b){2!=window[a]&&alert(\"FATAL ERROR: cannot load \"+b+\".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R\")}\n\t\tjsldp(\"J_UTL\",\"util\");\n\t\tjsldp(\"J_BBX\",\"baguettebox\");\n\t\tjsldp(\"J_BRW\",\"browser\");\n\t\tjsldp(\"J_U2K\",\"up2k\");\n\t</script>\n</body>\n</html>\n\n"
  },
  {
    "path": "copyparty/web/browser.js",
    "content": "\"use strict\";\n\nvar J_BRW = 1;\n\nif (window.rw_edit === undefined)\n\talert('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');\n\nvar XHR = XMLHttpRequest;\n\nif (1)\n\tLs.eng = {\n\t\t\"tt\": \"English\",\n\n\t\t\"cols\": {\n\t\t\t\"c\": \"action buttons\",\n\t\t\t\"dur\": \"duration\",\n\t\t\t\"q\": \"quality / bitrate\",\n\t\t\t\"Ac\": \"audio codec\",\n\t\t\t\"Vc\": \"video codec\",\n\t\t\t\"Fmt\": \"format / container\",\n\t\t\t\"Ahash\": \"audio checksum\",\n\t\t\t\"Vhash\": \"video checksum\",\n\t\t\t\"Res\": \"resolution\",\n\t\t\t\"T\": \"filetype\",\n\t\t\t\"aq\": \"audio quality / bitrate\",\n\t\t\t\"vq\": \"video quality / bitrate\",\n\t\t\t\"pixfmt\": \"subsampling / pixel structure\",\n\t\t\t\"resw\": \"horizontal resolution\",\n\t\t\t\"resh\": \"vertical resolution\",\n\t\t\t\"chs\": \"audio channels\",\n\t\t\t\"hz\": \"sample rate\",\n\t\t},\n\n\t\t\"hks\": [\n\t\t\t[\n\t\t\t\t\"misc\",\n\t\t\t\t[\"ESC\", \"close various things\"],\n\n\t\t\t\t\"file-manager\",\n\t\t\t\t[\"G\", \"toggle list / grid view\"],\n\t\t\t\t[\"T\", \"toggle thumbnails / icons\"],\n\t\t\t\t[\"⇧ A/D\", \"thumbnail size\"],\n\t\t\t\t[\"ctrl-K\", \"delete selected\"],\n\t\t\t\t[\"ctrl-X\", \"cut selection to clipboard\"],\n\t\t\t\t[\"ctrl-C\", \"copy selection to clipboard\"],\n\t\t\t\t[\"ctrl-V\", \"paste (move/copy) here\"],\n\t\t\t\t[\"Y\", \"download selected\"],\n\t\t\t\t[\"F2\", \"rename selected\"],\n\n\t\t\t\t\"file-list-sel\",\n\t\t\t\t[\"space\", \"toggle file selection\"],\n\t\t\t\t[\"↑/↓\", \"move selection cursor\"],\n\t\t\t\t[\"ctrl ↑/↓\", \"move cursor and viewport\"],\n\t\t\t\t[\"⇧ ↑/↓\", \"select prev/next file\"],\n\t\t\t\t[\"ctrl-A\", \"select all files / folders\"],\n\t\t\t], [\n\t\t\t\t\"navigation\",\n\t\t\t\t[\"B\", \"toggle breadcrumbs / navpane\"],\n\t\t\t\t[\"I/K\", \"prev/next folder\"],\n\t\t\t\t[\"M\", \"parent folder (or unexpand current)\"],\n\t\t\t\t[\"V\", \"toggle folders / textfiles in navpane\"],\n\t\t\t\t[\"A/D\", \"navpane size\"],\n\t\t\t], [\n\t\t\t\t\"audio-player\",\n\t\t\t\t[\"J/L\", \"prev/next song\"],\n\t\t\t\t[\"U/O\", \"skip 10sec back/fwd\"],\n\t\t\t\t[\"0..9\", \"jump to 0%..90%\"],\n\t\t\t\t[\"P\", \"play/pause (also initiates)\"],\n\t\t\t\t[\"S\", \"select playing song\"],\n\t\t\t\t[\"Y\", \"download song\"],\n\t\t\t], [\n\t\t\t\t\"image-viewer\",\n\t\t\t\t[\"J/L, ←/→\", \"prev/next pic\"],\n\t\t\t\t[\"Home/End\", \"first/last pic\"],\n\t\t\t\t[\"F\", \"fullscreen\"],\n\t\t\t\t[\"R\", \"rotate clockwise\"],\n\t\t\t\t[\"⇧ R\", \"rotate ccw\"],\n\t\t\t\t[\"S\", \"select pic\"],\n\t\t\t\t[\"Y\", \"download pic\"],\n\t\t\t], [\n\t\t\t\t\"video-player\",\n\t\t\t\t[\"U/O\", \"skip 10sec back/fwd\"],\n\t\t\t\t[\"P/K/Space\", \"play/pause\"],\n\t\t\t\t[\"C\", \"continue playing next\"],\n\t\t\t\t[\"V\", \"loop\"],\n\t\t\t\t[\"M\", \"mute\"],\n\t\t\t\t[\"[ and ]\", \"set loop interval\"],\n\t\t\t], [\n\t\t\t\t\"textfile-viewer\",\n\t\t\t\t[\"I/K\", \"prev/next file\"],\n\t\t\t\t[\"M\", \"close textfile\"],\n\t\t\t\t[\"E\", \"edit textfile\"],\n\t\t\t\t[\"S\", \"select file (for cut/copy/rename)\"],\n\t\t\t\t[\"Y\", \"download textfile\"],\n\t\t\t\t[\"⇧ J\", \"beautify json\"],\n\t\t\t]\n\t\t],\n\n\t\t\"m_ok\": \"OK\",\n\t\t\"m_ng\": \"Cancel\",\n\n\t\t\"enable\": \"Enable\",\n\t\t\"danger\": \"DANGER\",\n\t\t\"clipped\": \"copied to clipboard\",\n\n\t\t\"ht_s1\": \"second\",\n\t\t\"ht_s2\": \"seconds\",\n\t\t\"ht_m1\": \"minute\",\n\t\t\"ht_m2\": \"minutes\",\n\t\t\"ht_h1\": \"hour\",\n\t\t\"ht_h2\": \"hours\",\n\t\t\"ht_d1\": \"day\",\n\t\t\"ht_d2\": \"days\",\n\t\t\"ht_and\": \" and \",\n\n\t\t\"goh\": \"control-panel\",\n\t\t\"gop\": 'previous sibling\">prev',\n\t\t\"gou\": 'parent folder\">up',\n\t\t\"gon\": 'next folder\">next',\n\t\t\"logout\": \"Logout \",\n\t\t\"login\": \"Login\",\n\t\t\"access\": \" access\",\n\t\t\"ot_close\": \"close submenu\",\n\t\t\"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`&quot;try unite&quot;` = contain exactly «try unite»$N$Nthe date format is iso-8601, like$N`2009-12-31` or `2020-09-12 23:30:00`\",\n\t\t\"ot_unpost\": \"unpost: delete your recent uploads, or abort unfinished ones\",\n\t\t\"ot_bup\": \"bup: basic uploader, even supports netscape 4.0\",\n\t\t\"ot_mkdir\": \"mkdir: create a new directory\",\n\t\t\"ot_md\": \"new-file: create a new textfile\",\n\t\t\"ot_msg\": \"msg: send a message to the server log\",\n\t\t\"ot_mp\": \"media player options\",\n\t\t\"ot_cfg\": \"configuration options\",\n\t\t\"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 [🎈]&nbsp; (the basic uploader)<br /><br />during uploads, this icon becomes a progress indicator!',\n\t\t\"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 [🎈]&nbsp; (the basic uploader)<br /><br />during uploads, this icon becomes a progress indicator!',\n\t\t\"ot_noie\": 'Please use Chrome / Firefox / Edge',\n\n\t\t\"ab_mkdir\": \"make directory\",\n\t\t\"ab_mkdoc\": \"new textfile\",\n\t\t\"ab_msg\": \"send msg to srv log\",\n\n\t\t\"ay_path\": \"skip to folders\",\n\t\t\"ay_files\": \"skip to files\",\n\n\t\t\"wt_ren\": \"rename selected items$NHotkey: F2\",\n\t\t\"wt_del\": \"delete selected items$NHotkey: ctrl-K\",\n\t\t\"wt_cut\": \"cut selected items &lt;small&gt;(then paste somewhere else)&lt;/small&gt;$NHotkey: ctrl-X\",\n\t\t\"wt_cpy\": \"copy selected items to clipboard$N(to paste them somewhere else)$NHotkey: ctrl-C\",\n\t\t\"wt_pst\": \"paste a previously cut / copied selection$NHotkey: ctrl-V\",\n\t\t\"wt_selall\": \"select all files$NHotkey: ctrl-A (when file focused)\",\n\t\t\"wt_selinv\": \"invert selection\",\n\t\t\"wt_zip1\": \"download this folder as archive\",\n\t\t\"wt_selzip\": \"download selection as archive\",\n\t\t\"wt_seldl\": \"download selection as separate files$NHotkey: Y\",\n\t\t\"wt_npirc\": \"copy irc-formatted track info\",\n\t\t\"wt_nptxt\": \"copy plaintext track info\",\n\t\t\"wt_m3ua\": \"add to m3u playlist (click <code>📻copy</code> later)\",\n\t\t\"wt_m3uc\": \"copy m3u playlist to clipboard\",\n\t\t\"wt_grid\": \"toggle grid / list view$NHotkey: G\",\n\t\t\"wt_prev\": \"previous track$NHotkey: J\",\n\t\t\"wt_play\": \"play / pause$NHotkey: P\",\n\t\t\"wt_next\": \"next track$NHotkey: L\",\n\n\t\t\"ul_par\": \"parallel uploads:\",\n\t\t\"ut_rand\": \"randomize filenames\",\n\t\t\"ut_u2ts\": \"copy the last-modified timestamp$Nfrom your filesystem to the server\\\">📅\",\n\t\t\"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\",\n\t\t\"ut_mt\": \"continue hashing other files while uploading$N$Nmaybe disable if your CPU or HDD is a bottleneck\",\n\t\t\"ut_ask\": 'ask for confirmation before upload starts\">💭',\n\t\t\"ut_pot\": \"improve upload speed on slow devices$Nby making the UI less complex\",\n\t\t\"ut_srch\": \"don't actually upload, instead check if the files already $N exist on the server (will scan all folders you can read)\",\n\t\t\"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\",\n\t\t\"ul_btn\": \"drop files / folders<br>here (or click me)\",\n\t\t\"ul_btnu\": \"U P L O A D\",\n\t\t\"ul_btns\": \"S E A R C H\",\n\n\t\t\"ul_hash\": \"hash\",\n\t\t\"ul_send\": \"send\",\n\t\t\"ul_done\": \"done\",\n\t\t\"ul_idle1\": \"no uploads are queued yet\",\n\t\t\"ut_etah\": \"average &lt;em&gt;hashing&lt;/em&gt; speed, and estimated time until finish\",\n\t\t\"ut_etau\": \"average &lt;em&gt;upload&lt;/em&gt; speed and estimated time until finish\",\n\t\t\"ut_etat\": \"average &lt;em&gt;total&lt;/em&gt; speed and estimated time until finish\",\n\n\t\t\"uct_ok\": \"completed successfully\",\n\t\t\"uct_ng\": \"no-good: failed / rejected / not-found\",\n\t\t\"uct_done\": \"ok and ng combined\",\n\t\t\"uct_bz\": \"hashing or uploading\",\n\t\t\"uct_q\": \"idle, pending\",\n\n\t\t\"utl_name\": \"filename\",\n\t\t\"utl_ulist\": \"list\",\n\t\t\"utl_ucopy\": \"copy\",\n\t\t\"utl_links\": \"links\",\n\t\t\"utl_stat\": \"status\",\n\t\t\"utl_prog\": \"progress\",\n\n\t\t// keep short:\n\t\t\"utl_404\": \"404\",\n\t\t\"utl_err\": \"ERROR\",\n\t\t\"utl_oserr\": \"OS-error\",\n\t\t\"utl_found\": \"found\",\n\t\t\"utl_defer\": \"defer\",\n\t\t\"utl_yolo\": \"YOLO\",\n\t\t\"utl_done\": \"done\",\n\n\t\t\"ul_flagblk\": \"the files were added to the queue</b><br>however there is a busy up2k in another browser tab,<br>so waiting for that to finish first\",\n\t\t\"ul_btnlk\": \"the server configuration has locked this switch into this state\",\n\n\t\t\"udt_up\": \"Upload\",\n\t\t\"udt_srch\": \"Search\",\n\t\t\"udt_drop\": \"drop it here\",\n\n\t\t\"u_nav_m\": '<h6>aight, what do you have?</h6><code>Enter</code> = Files (one or more)\\n<code>ESC</code> = One folder (including subfolders)',\n\t\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Files</a><a href=\"#\" id=\"modal-ng\">One folder</a>',\n\n\t\t\"cl_opts\": \"switches\",\n\t\t\"cl_hfsz\": \"filesize\",\n\t\t\"cl_themes\": \"theme\",\n\t\t\"cl_langs\": \"language\",\n\t\t\"cl_ziptype\": \"folder download\",\n\t\t\"cl_uopts\": \"up2k switches\",\n\t\t\"cl_favico\": \"favicon\",\n\t\t\"cl_bigdir\": \"big dirs\",\n\t\t\"cl_hsort\": \"#sort\",\n\t\t\"cl_keytype\": \"key notation\",\n\t\t\"cl_hiddenc\": \"hidden columns\",\n\t\t\"cl_hidec\": \"hide\",\n\t\t\"cl_reset\": \"reset\",\n\t\t\"cl_hpick\": \"tap on column headers to hide in the table below\",\n\t\t\"cl_hcancel\": \"column hiding aborted\",\n\t\t\"cl_rcm\": \"right-click menu\",\n\n\t\t\"ct_grid\": '田 the grid',\n\t\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ tooltips',\n\t\t\"ct_thumb\": 'in grid-view, toggle icons or thumbnails$NHotkey: T\">🖼️ thumbs',\n\t\t\"ct_csel\": 'use CTRL and SHIFT for file selection in grid-view\">sel',\n\t\t\"ct_dsel\": 'use drag-selection in grid-view\">dsel',\n\t\t\"ct_dl\": 'force download (don\\'t display inline) when a file is clicked\">dl',\n\t\t\"ct_ihop\": 'when the image viewer is closed, scroll down to the last viewed file\">g⮯',\n\t\t\"ct_dots\": 'show hidden files (if server permits)\">dotfiles',\n\t\t\"ct_qdel\": 'when deleting files, only ask for confirmation once\">qdel',\n\t\t\"ct_dir1st\": 'sort folders before files\">📁 first',\n\t\t\"ct_nsort\": 'natural sort (for filenames with leading digits)\">nsort',\n\t\t\"ct_utc\": 'show all datetimes in UTC\">UTC',\n\t\t\"ct_readme\": 'show README.md in folder listings\">📜 readme',\n\t\t\"ct_idxh\": 'show index.html instead of folder listing\">htm',\n\t\t\"ct_sbars\": 'show scrollbars\">⟊',\n\n\t\t\"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📅\",\n\n\t\t\"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 <em>&quot;does this have the same filesize on the server?&quot;</em> 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 &quot;upload&quot; the same files again to let the client verify them\\\">turbo\",\n\n\t\t\"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 <em>theoretically</em> catch most unfinished / corrupted uploads, but is not a substitute for doing a verification pass with turbo disabled afterwards\\\">date-chk\",\n\n\t\t\"cut_u2sz\": \"size (in MiB) of each upload chunk; big values fly better across the atlantic. Try low values on very unreliable connections\",\n\n\t\t\"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\",\n\n\t\t\"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\",\n\n\t\t\"cut_nag\": \"OS notification when upload completes$N(only if the browser or tab is not active)\",\n\t\t\"cut_sfx\": \"audible alert when upload completes$N(only if the browser or tab is not active)\",\n\n\t\t\"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\",\n\n\t\t\"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\",\n\n\t\t\"cft_text\": \"favicon text (blank and refresh to disable)\",\n\t\t\"cft_fg\": \"foreground color\",\n\t\t\"cft_bg\": \"background color\",\n\n\t\t\"cdt_lim\": \"max number of files to show in a folder\",\n\t\t\"cdt_ask\": \"when scrolling to the bottom,$Ninstead of loading more files,$Nask what to do\",\n\t\t\"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\",\n\t\t\"cdt_ren\": \"enable custom right-click menu, you can still access the regular menu by pressing the shift key and right-clicking\\\">enable\",\n\t\t\"cdt_rdb\": \"show the regular right-click menu when the custom one is already open and right-clicking again\\\">double\",\n\n\t\t\"tt_entree\": \"show navpane (directory tree sidebar)$NHotkey: B\",\n\t\t\"tt_detree\": \"show breadcrumbs$NHotkey: B\",\n\t\t\"tt_visdir\": \"scroll to selected folder\",\n\t\t\"tt_ftree\": \"toggle folder-tree / textfiles$NHotkey: V\",\n\t\t\"tt_pdock\": \"show parent folders in a docked pane at the top\",\n\t\t\"tt_dynt\": \"autogrow as tree expands\",\n\t\t\"tt_wrap\": \"word wrap\",\n\t\t\"tt_hover\": \"reveal overflowing lines on hover$N( breaks scrolling unless mouse $N&nbsp; cursor is in the left gutter )\",\n\n\t\t\"ml_pmode\": \"at end of folder...\",\n\t\t\"ml_btns\": \"cmds\",\n\t\t\"ml_tcode\": \"transcode\",\n\t\t\"ml_tcode2\": \"transcode to\",\n\t\t\"ml_tint\": \"tint\",\n\t\t\"ml_eq\": \"audio equalizer\",\n\t\t\"ml_drc\": \"dynamic range compressor\",\n\t\t\"ml_ss\": \"skip silence\",\n\n\t\t\"mt_loop\": \"loop/repeat one song\\\">🔁\",\n\t\t\"mt_one\": \"stop after one song\\\">1️⃣\",\n\t\t\"mt_shuf\": \"shuffle the songs in each folder\\\">🔀\",\n\t\t\"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▶\",\n\t\t\"mt_preload\": \"start loading the next song near the end for gapless playback\\\">preload\",\n\t\t\"mt_prescan\": \"go to the next folder before the last song$Nends, keeping the webbrowser happy$Nso it doesn't stop the playback\\\">nav\",\n\t\t\"mt_fullpre\": \"try to preload the entire song;$N✅ enable on <b>unreliable</b> connections,$N❌ <b>disable</b> on slow connections probably\\\">full\",\n\t\t\"mt_fau\": \"on phones, prevent music from stopping if the next song doesn't preload fast enough (can make tags display glitchy)\\\">☕️\",\n\t\t\"mt_waves\": \"waveform seekbar:$Nshow audio amplitude in the scrubber\\\">~s\",\n\t\t\"mt_npclip\": \"show buttons for clipboarding the currently playing song\\\">/np\",\n\t\t\"mt_m3u_c\": \"show buttons for clipboarding the$Nselected songs as m3u8 playlist entries\\\">📻\",\n\t\t\"mt_octl\": \"os integration (media hotkeys / osd)\\\">os-ctl\",\n\t\t\"mt_oseek\": \"allow seeking through os integration$N$Nnote: on some devices (iPhones),$Nthis replaces the next-song button\\\">seek\",\n\t\t\"mt_oscv\": \"show album cover in osd\\\">art\",\n\t\t\"mt_follow\": \"keep the playing track scrolled into view\\\">🎯\",\n\t\t\"mt_compact\": \"compact controls\\\">⟎\",\n\t\t\"mt_uncache\": \"clear cache &nbsp;(try this if your browser cached$Na broken copy of a song so it refuses to play)\\\">uncache\",\n\t\t\"mt_mloop\": \"loop the open folder\\\">🔁 loop\",\n\t\t\"mt_mnext\": \"load the next folder and continue\\\">📂 next\",\n\t\t\"mt_mstop\": \"stop playback\\\">⏸ stop\",\n\t\t\"mt_cflac\": \"convert flac / wav to {0}\\\">flac\",\n\t\t\"mt_caac\": \"convert aac / m4a to {0}\\\">aac\",\n\t\t\"mt_coth\": \"convert all others (not mp3) to {0}\\\">oth\",\n\t\t\"mt_c2opus\": \"best choice for desktops, laptops, android\\\">opus\",\n\t\t\"mt_c2owa\": \"opus-weba, for iOS 17.5 and newer\\\">owa\",\n\t\t\"mt_c2caf\": \"opus-caf, for iOS 11 through 17\\\">caf\",\n\t\t\"mt_c2mp3\": \"use this on very old devices\\\">mp3\",\n\t\t\"mt_c2flac\": \"best sound quality, but huge downloads\\\">flac\",\n\t\t\"mt_c2wav\": \"uncompressed playback (even bigger)\\\">wav\",\n\t\t\"mt_c2ok\": \"nice, good choice\",\n\t\t\"mt_c2nd\": \"that's not the recommended output format for your device, but that's fine\",\n\t\t\"mt_c2ng\": \"your device does not seem to support this output format, but let's try anyways\",\n\t\t\"mt_xowa\": \"there are bugs in iOS preventing background playback using this format; please use caf or mp3 instead\",\n\t\t\"mt_tint\": \"background level (0-100) on the seekbar$Nto make buffering less distracting\",\n\t\t\"mt_eq\": \"`enables the equalizer and gain control;$N$Nboost `0` = standard 100% volume (unmodified)$N$Nwidth `1 &nbsp;` = standard stereo (unmodified)$Nwidth `0.5` = 50% left-right crossfeed$Nwidth `0 &nbsp;` = mono$N$Nboost `-0.8` &amp; 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\",\n\t\t\"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)\",\n\t\t\"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\",\n\t\t\"mt_ssvt\": \"volume threshold (0-255)\\\">vol\",\n\t\t\"mt_ssts\": \"active threshold (% of track, start)\\\">start\",\n\t\t\"mt_sste\": \"active threshold (% of track, end)\\\">end\",\n\t\t\"mt_sssm\": \"playback speed multiplier (range: 0.15 to 8)\\\">ffwd\",\n\n\t\t\"mb_play\": \"play\",\n\t\t\"mm_hashplay\": \"play this audio file?\",\n\t\t\"mm_m3u\": \"press <code>Enter/OK</code> to Play\\npress <code>ESC/Cancel</code> to Edit\",\n\t\t\"mp_breq\": \"need firefox 82+ or chrome 73+ or iOS 15+\",\n\t\t\"mm_bload\": \"now loading...\",\n\t\t\"mm_bconv\": \"converting to {0}, please wait...\",\n\t\t\"mm_opusen\": \"your browser cannot play aac / m4a files;\\ntranscoding to opus is now enabled\",\n\t\t\"mm_playerr\": \"playback failed: \",\n\t\t\"mm_eabrt\": \"The playback attempt was cancelled\",\n\t\t\"mm_enet\": \"Your internet connection is wonky\",\n\t\t\"mm_edec\": \"This file is supposedly corrupted??\",\n\t\t\"mm_esupp\": \"Your browser does not understand this audio format\",\n\t\t\"mm_eunk\": \"Unknown Errol\",\n\t\t\"mm_e404\": \"Could not play audio; error 404: File not found.\",\n\t\t\"mm_e403\": \"Could not play audio; error 403: Access denied.\\n\\nTry pressing F5 to reload, maybe you got logged out\",\n\t\t\"mm_e415\": \"Could not play audio; error 415: File transcoding failed; check server logs.\",\n\t\t\"mm_e500\": \"Could not play audio; error 500: Check server logs.\",\n\t\t\"mm_e5xx\": \"Could not play audio; server error \",\n\t\t\"mm_nof\": \"not finding any more audio files nearby\",\n\t\t\"mm_prescan\": \"Looking for music to play next...\",\n\t\t\"mm_scank\": \"Found the next song:\",\n\t\t\"mm_uncache\": \"cache cleared; all songs will redownload on next playback\",\n\t\t\"mm_hnf\": \"that song no longer exists\",\n\n\t\t\"im_hnf\": \"that image no longer exists\",\n\n\t\t\"f_empty\": 'this folder is empty',\n\t\t\"f_chide\": 'this will hide the column «{0}»\\n\\nyou can unhide columns in the settings tab',\n\t\t\"f_bigtxt\": \"this file is {0} MiB large -- really view as text?\",\n\t\t\"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\",\n\t\t\"fbd_more\": '<div id=\"blazy\">showing <code>{0}</code> of <code>{1}</code> files; <a href=\"#\" id=\"bd_more\">show {2}</a> or <a href=\"#\" id=\"bd_all\">show all</a></div>',\n\t\t\"fbd_all\": '<div id=\"blazy\">showing <code>{0}</code> of <code>{1}</code> files; <a href=\"#\" id=\"bd_all\">show all</a></div>',\n\t\t\"f_anota\": \"only {0} of the {1} items were selected;\\nto select the full folder, first scroll to the bottom\",\n\n\t\t\"f_dls\": 'the file links in the current folder have\\nbeen changed into download links',\n\t\t\"f_dl_nd\": 'skipping folder (use zip/tar download instead):\\n',\n\n\t\t\"f_partial\": \"To safely download a file which is currently being uploaded, please click the file which has the same filename, but without the <code>.PARTIAL</code> file extension. Please press CANCEL or Escape to do this.\\n\\nPressing OK / Enter will ignore this warning and continue downloading the <code>.PARTIAL</code> scratchfile instead, which will almost definitely give you corrupted data.\",\n\n\t\t\"ft_paste\": \"paste {0} items$NHotkey: ctrl-V\",\n\t\t\"fr_eperm\": 'cannot rename:\\nyou do not have “move” permission in this folder',\n\t\t\"fd_eperm\": 'cannot delete:\\nyou do not have “delete” permission in this folder',\n\t\t\"fc_eperm\": 'cannot cut:\\nyou do not have “move” permission in this folder',\n\t\t\"fp_eperm\": 'cannot paste:\\nyou do not have “write” permission in this folder',\n\t\t\"fr_emore\": \"select at least one item to rename\",\n\t\t\"fd_emore\": \"select at least one item to delete\",\n\t\t\"fc_emore\": \"select at least one item to cut\",\n\t\t\"fcp_emore\": \"select at least one item to copy to clipboard\",\n\n\t\t\"fs_sc\": \"share the folder you're in\",\n\t\t\"fs_ss\": \"share the selected files\",\n\t\t\"fs_just1d\": \"you cannot select more than one folder,\\nor mix files and folders in one selection\",\n\t\t\"fs_abrt\": \"❌ abort\",\n\t\t\"fs_rand\": \"🎲 rand.name\",\n\t\t\"fs_go\": \"✅ create share\",\n\t\t\"fs_name\": \"name\",\n\t\t\"fs_src\": \"source\",\n\t\t\"fs_pwd\": \"passwd\",\n\t\t\"fs_exp\": \"expiry\",\n\t\t\"fs_tmin\": \"min\",\n\t\t\"fs_thrs\": \"hours\",\n\t\t\"fs_tdays\": \"days\",\n\t\t\"fs_never\": \"eternal\",\n\t\t\"fs_pname\": \"optional link name; will be random if blank\",\n\t\t\"fs_tsrc\": \"the file or folder to share\",\n\t\t\"fs_ppwd\": \"optional password\",\n\t\t\"fs_w8\": \"creating share...\",\n\t\t\"fs_ok\": \"press <code>Enter/OK</code> to Clipboard\\npress <code>ESC/Cancel</code> to Close\",\n\n\t\t\"frt_dec\": \"may fix some cases of broken filenames\\\">url-decode\",\n\t\t\"frt_rst\": \"reset modified filenames back to the original ones\\\">↺ reset\",\n\t\t\"frt_abrt\": \"abort and close this window\\\">❌ cancel\",\n\t\t\"frb_apply\": \"APPLY RENAME\",\n\t\t\"fr_adv\": \"batch / metadata / pattern renaming\\\">advanced\",\n\t\t\"fr_case\": \"case-sensitive regex\\\">case\",\n\t\t\"fr_win\": \"windows-safe names; replace <code>&lt;&gt;:&quot;\\\\|?*</code> with japanese fullwidth characters\\\">win\",\n\t\t\"fr_slash\": \"replace <code>/</code> with a character that doesn't cause new folders to be created\\\">no /\",\n\t\t\"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\",\n\t\t\"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\",\n\t\t\"fr_pdel\": \"delete\",\n\t\t\"fr_pnew\": \"save as\",\n\t\t\"fr_pname\": \"provide a name for your new preset\",\n\t\t\"fr_aborted\": \"aborted\",\n\t\t\"fr_lold\": \"old name\",\n\t\t\"fr_lnew\": \"new name\",\n\t\t\"fr_tags\": \"tags for the selected files (read-only, just for reference):\",\n\t\t\"fr_busy\": \"renaming {0} items...\\n\\n{1}\",\n\t\t\"fr_efail\": \"rename failed:\\n\",\n\t\t\"fr_nchg\": \"{0} of the new names were altered due to <code>win</code> and/or <code>no /</code>\\n\\nOK to continue with these altered new names?\",\n\n\t\t\"fd_ok\": \"delete OK\",\n\t\t\"fd_err\": \"delete failed:\\n\",\n\t\t\"fd_none\": \"nothing was deleted; maybe blocked by server config (xbd)?\",\n\t\t\"fd_busy\": \"deleting {0} items...\\n\\n{1}\",\n\t\t\"fd_warn1\": \"DELETE these {0} items?\",\n\t\t\"fd_warn2\": \"<b>Last chance!</b> No way to undo. Delete?\",\n\n\t\t\"fc_ok\": \"cut {0} items\",\n\t\t\"fc_warn\": 'cut {0} items\\n\\nbut: only <b>this</b> browser-tab can paste them\\n(since the selection is so absolutely massive)',\n\n\t\t\"fcc_ok\": \"copied {0} items to clipboard\",\n\t\t\"fcc_warn\": 'copied {0} items to clipboard\\n\\nbut: only <b>this</b> browser-tab can paste them\\n(since the selection is so absolutely massive)',\n\n\t\t\"fp_apply\": \"use these names\",\n\t\t\"fp_skip\": \"skip conflicts\",  // TLNote: \"skip existing names\" (filenames taken in target folder)\n\t\t\"fp_ecut\": \"first cut or copy some files / folders to paste / move\\n\\nnote: you can cut / paste across different browser tabs\",\n\t\t\"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:\",\n\t\t\"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:\",\n\t\t\"fp_emore\": \"there are still some filename collisions left to fix\",\n\t\t\"fp_ok\": \"move OK\",\n\t\t\"fcp_ok\": \"copy OK\",\n\t\t\"fp_busy\": \"moving {0} items...\\n\\n{1}\",\n\t\t\"fcp_busy\": \"copying {0} items...\\n\\n{1}\",\n\t\t\"fp_abrt\": \"aborting...\",\n\t\t\"fp_err\": \"move failed:\\n\",\n\t\t\"fcp_err\": \"copy failed:\\n\",\n\t\t\"fp_confirm\": \"move these {0} items here?\",\n\t\t\"fcp_confirm\": \"copy these {0} items here?\",\n\t\t\"fp_etab\": 'failed to read clipboard from other browser tab',\n\t\t\"fp_name\": \"uploading a file from your device. Give it a name:\",\n\t\t\"fp_both_m\": '<h6>choose what to paste</h6><code>Enter</code> = Move {0} files from «{1}»\\n<code>ESC</code> = Upload {2} files from your device',\n\t\t\"fcp_both_m\": '<h6>choose what to paste</h6><code>Enter</code> = Copy {0} files from «{1}»\\n<code>ESC</code> = Upload {2} files from your device',\n\t\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Move</a><a href=\"#\" id=\"modal-ng\">Upload</a>',\n\t\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Copy</a><a href=\"#\" id=\"modal-ng\">Upload</a>',\n\n\t\t\"mk_noname\": \"type a name into the text field on the left before you do that :p\",\n\t\t\"nmd_i1\": \"also add the file extension you want, for example <code>.md</code>\",\n\t\t\"nmd_i2\": \"you can only create <code>.{0}</code> files because you don't have the delete-permission\",\n\n\t\t\"tv_load\": \"Loading text document:\\n\\n{0}\\n\\n{1}% ({2} of {3} MiB loaded)\",\n\t\t\"tv_xe1\": \"could not load textfile:\\n\\nerror \",\n\t\t\"tv_xe2\": \"404, file not found\",\n\t\t\"tv_lst\": \"list of textfiles in\",\n\t\t\"tvt_close\": \"return to folder view$NHotkey: M (or Esc)\\\">❌ close\",\n\t\t\"tvt_dl\": \"download this file$NHotkey: Y\\\">💾 download\",\n\t\t\"tvt_prev\": \"show previous document$NHotkey: i\\\">⬆ prev\",\n\t\t\"tvt_next\": \"show next document$NHotkey: K\\\">⬇ next\",\n\t\t\"tvt_sel\": \"select file &nbsp; ( for cut / copy / delete / ... )$NHotkey: S\\\">sel\",\n\t\t\"tvt_j\": \"beautify json$NHotkey: shift-J\\\">j\",\n\t\t\"tvt_edit\": \"open file in text editor$NHotkey: E\\\">✏️ edit\",\n\t\t\"tvt_tail\": \"monitor file for changes; show new lines in real time\\\">📡 follow\",\n\t\t\"tvt_wrap\": \"word-wrap\\\">↵\",\n\t\t\"tvt_atail\": \"lock scroll to bottom of page\\\">⚓\",\n\t\t\"tvt_ctail\": \"decode terminal colors (ansi escape codes)\\\">🌈\",\n\t\t\"tvt_ntail\": \"scrollback limit (how many bytes of text to keep loaded)\",\n\n\t\t\"m3u_add1\": \"song added to m3u playlist\",\n\t\t\"m3u_addn\": \"{0} songs added to m3u playlist\",\n\t\t\"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\",\n\n\t\t\"gt_vau\": \"don't show videos, just play the audio\\\">🎧\",\n\t\t\"gt_msel\": \"enable file selection; ctrl-click a file to override$N$N&lt;em&gt;when active: doubleclick a file / folder to open it&lt;/em&gt;$N$NHotkey: S\\\">multiselect\",\n\t\t\"gt_crop\": \"center-crop thumbnails\\\">crop\",\n\t\t\"gt_3x\": \"hi-res thumbnails\\\">3x\",\n\t\t\"gt_zoom\": \"zoom\",\n\t\t\"gt_chop\": \"chop\",\n\t\t\"gt_sort\": \"sort by\",\n\t\t\"gt_name\": \"name\",\n\t\t\"gt_sz\": \"size\",\n\t\t\"gt_ts\": \"date\",\n\t\t\"gt_ext\": \"type\",\n\t\t\"gt_c1\": \"truncate filenames more (show less)\",\n\t\t\"gt_c2\": \"truncate filenames less (show more)\",\n\n\t\t\"sm_w8\": \"searching...\",\n\t\t\"sm_prev\": \"search results below are from a previous query:\\n  \",\n\t\t\"sl_close\": \"close search results\",\n\t\t\"sl_hits\": \"showing {0} hits\",\n\t\t\"sl_moar\": \"load more\",\n\n\t\t\"s_sz\": \"size\",\n\t\t\"s_dt\": \"date\",\n\t\t\"s_rd\": \"path\",\n\t\t\"s_fn\": \"name\",\n\t\t\"s_ta\": \"tags\",\n\t\t\"s_ua\": \"up@\",\n\t\t\"s_ad\": \"adv.\",\n\t\t\"s_s1\": \"minimum MiB\",\n\t\t\"s_s2\": \"maximum MiB\",\n\t\t\"s_d1\": \"min. iso8601\",\n\t\t\"s_d2\": \"max. iso8601\",\n\t\t\"s_u1\": \"uploaded after\",\n\t\t\"s_u2\": \"and/or before\",\n\t\t\"s_r1\": \"path contains &nbsp; (space-separated)\",\n\t\t\"s_f1\": \"name contains &nbsp; (negate with -nope)\",\n\t\t\"s_t1\": \"tags contains &nbsp; (^=start, end=$)\",\n\t\t\"s_a1\": \"specific metadata properties\",\n\n\t\t\"md_eshow\": \"cannot render \",\n\t\t\"md_off\": \"[📜<em>readme</em>] disabled in [⚙️] -- document hidden\",\n\n\t\t\"badreply\": \"Failed to parse reply from server\",\n\n\t\t\"xhr403\": \"403: Access denied\\n\\ntry pressing F5, maybe you got logged out\",\n\t\t\"xhr0\": \"unknown (probably lost connection to server, or server is offline)\",\n\t\t\"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\",\n\t\t\"tl_xe1\": \"could not list subfolders:\\n\\nerror \",\n\t\t\"tl_xe2\": \"404: Folder not found\",\n\t\t\"fl_xe1\": \"could not list files in folder:\\n\\nerror \",\n\t\t\"fl_xe2\": \"404: Folder not found\",\n\t\t\"fd_xe1\": \"could not create subfolder:\\n\\nerror \",\n\t\t\"fd_xe2\": \"404: Parent folder not found\",\n\t\t\"fsm_xe1\": \"could not send message:\\n\\nerror \",\n\t\t\"fsm_xe2\": \"404: Parent folder not found\",\n\t\t\"fu_xe1\": \"failed to load unpost list from server:\\n\\nerror \",\n\t\t\"fu_xe2\": \"404: File not found??\",\n\n\t\t\"fz_tar\": \"uncompressed gnu-tar file (linux / mac)\",\n\t\t\"fz_pax\": \"uncompressed pax-format tar (slower)\",\n\t\t\"fz_targz\": \"gnu-tar with gzip level 3 compression$N$Nthis is usually very slow, so$Nuse uncompressed tar instead\",\n\t\t\"fz_tarxz\": \"gnu-tar with xz level 1 compression$N$Nthis is usually very slow, so$Nuse uncompressed tar instead\",\n\t\t\"fz_zip8\": \"zip with utf8 filenames (maybe wonky on windows 7 and older)\",\n\t\t\"fz_zipd\": \"zip with traditional cp437 filenames, for really old software\",\n\t\t\"fz_zipc\": \"cp437 with crc32 computed early,$Nfor MS-DOS PKZIP v2.04g (october 1993)$N(takes longer to process before download can start)\",\n\n\t\t\"un_m1\": \"you can delete your recent uploads (or abort unfinished ones) below\",\n\t\t\"un_upd\": \"refresh\",\n\t\t\"un_m4\": \"or share the files visible below:\",\n\t\t\"un_ulist\": \"show\",\n\t\t\"un_ucopy\": \"copy\",\n\t\t\"un_flt\": \"optional filter:&nbsp; URL must contain\",\n\t\t\"un_fclr\": \"clear filter\",\n\t\t\"un_derr\": 'unpost-delete failed:\\n',\n\t\t\"un_f5\": 'something broke, please try a refresh or hit F5',\n\t\t\"un_uf5\": \"sorry but you have to refresh the page (for example by pressing F5 or CTRL-R) before this upload can be aborted\",\n\t\t\"un_nou\": '<b>warning:</b> server too busy to show unfinished uploads; click the \"refresh\" link in a bit',\n\t\t\"un_noc\": '<b>warning:</b> unpost of fully uploaded files is not enabled/permitted in server config',\n\t\t\"un_max\": \"showing first 2000 files (use the filter)\",\n\t\t\"un_avail\": \"{0} recent uploads can be deleted<br />{1} unfinished ones can be aborted\",\n\t\t\"un_m2\": \"sorted by upload time; most recent first:\",\n\t\t\"un_no1\": \"sike! no uploads are sufficiently recent\",\n\t\t\"un_no2\": \"sike! no uploads matching that filter are sufficiently recent\",\n\t\t\"un_next\": \"delete the next {0} files below\",\n\t\t\"un_abrt\": \"abort\",\n\t\t\"un_del\": \"delete\",\n\t\t\"un_m3\": \"loading your recent uploads...\",\n\t\t\"un_busy\": \"deleting {0} files...\",\n\t\t\"un_clip\": \"{0} links copied to clipboard\",\n\n\t\t\"u_https1\": \"you should\",\n\t\t\"u_https2\": \"switch to https\",\n\t\t\"u_https3\": \"for better performance\",\n\t\t\"u_ancient\": 'your browser is impressively ancient -- maybe you should <a href=\"#\" onclick=\"goto(\\'bup\\')\">use bup instead</a>',\n\t\t\"u_nowork\": \"need firefox 53+ or chrome 57+ or iOS 11+\",\n\t\t\"tail_2old\": \"need firefox 105+ or chrome 71+ or iOS 14.5+\",\n\t\t\"u_nodrop\": 'your browser is too old for drag-and-drop uploading',\n\t\t\"u_notdir\": \"that's not a folder!\\n\\nyour browser is too old,\\nplease try dragdrop instead\",\n\t\t\"u_uri\": \"to dragdrop images from other browser windows,\\nplease drop it onto the big upload button\",\n\t\t\"u_enpot\": 'switch to <a href=\"#\">potato UI</a> (may improve upload speed)',\n\t\t\"u_depot\": 'switch to <a href=\"#\">fancy UI</a> (may reduce upload speed)',\n\t\t\"u_gotpot\": 'switching to the potato UI for improved upload speed,\\n\\nfeel free to disagree and switch back!',\n\t\t\"u_pott\": \"<p>files: &nbsp; <b>{0}</b> finished, &nbsp; <b>{1}</b> failed, &nbsp; <b>{2}</b> busy, &nbsp; <b>{3}</b> queued</p>\",\n\t\t\"u_ever\": \"this is the basic uploader; up2k needs at least<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1\",\n\t\t\"u_su2k\": 'this is the basic uploader; <a href=\"#\" id=\"u2yea\">up2k</a> is better',\n\t\t\"u_uput\": 'optimize for speed (skip checksum)',\n\t\t\"u_ewrite\": 'you do not have write-access to this folder',\n\t\t\"u_eread\": 'you do not have read-access to this folder',\n\t\t\"u_enoi\": 'file-search is not enabled in server config',\n\t\t\"u_enoow\": \"overwrite will not work here; need Delete-permission\",\n\t\t\"u_badf\": 'These {0} files (of {1} total) were skipped, possibly due to filesystem permissions:\\n\\n',\n\t\t\"u_blankf\": 'These {0} files (of {1} total) are blank / empty; upload them anyways?\\n\\n',\n\t\t\"u_applef\": 'These {0} files (of {1} total) are probably undesirable;\\nPress <code>OK/Enter</code> to SKIP the following files,\\nPress <code>Cancel/ESC</code> to NOT exclude, and UPLOAD those as well:\\n\\n',\n\t\t\"u_just1\": '\\nMaybe it works better if you select just one file',\n\t\t\"u_ff_many\": \"if you're using <b>Linux / MacOS / Android,</b> then this amount of files <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\"><em>may</em> crash Firefox!</a>\\nif that happens, please try again (or use Chrome).\",\n\t\t\"u_up_life\": \"This upload will be deleted from the server\\n{0} after it completes\",\n\t\t\"u_asku\": 'upload these {0} files to <code>{1}</code>',\n\t\t\"u_unpt\": \"you can undo / delete this upload using the top-left 🧯\",\n\t\t\"u_bigtab\": 'about to show {0} files\\n\\nthis may crash your browser, are you sure?',\n\t\t\"u_scan\": 'Scanning files...',\n\t\t\"u_dirstuck\": 'directory iterator got stuck trying to access the following {0} items; will skip:',\n\t\t\"u_etadone\": 'Done ({0}, {1} files)',\n\t\t\"u_etaprep\": '(preparing to upload)',\n\t\t\"u_hashdone\": 'hashing done',\n\t\t\"u_hashing\": 'hash',\n\t\t\"u_hs\": 'handshaking...',\n\t\t\"u_started\": \"the files are now being uploaded; see [🚀]\",\n\t\t\"u_dupdefer\": \"duplicate; will be processed after all other files\",\n\t\t\"u_actx\": \"click this text to prevent loss of<br />performance when switching to other windows/tabs\",\n\t\t\"u_fixed\": \"OK!&nbsp; Fixed it 👍\",\n\t\t\"u_cuerr\": \"failed to upload chunk {0} of {1};\\nprobably harmless, continuing\\n\\nfile: {2}\",\n\t\t\"u_cuerr2\": \"server rejected upload (chunk {0} of {1});\\nwill retry later\\n\\nfile: {2}\\n\\nerror \",\n\t\t\"u_ehstmp\": \"will retry; see bottom-right\",\n\t\t\"u_ehsfin\": \"server rejected the request to finalize upload; retrying...\",\n\t\t\"u_ehssrch\": \"server rejected the request to perform search; retrying...\",\n\t\t\"u_ehsinit\": \"server rejected the request to initiate upload; retrying...\",\n\t\t\"u_eneths\": \"network error while performing upload handshake; retrying...\",\n\t\t\"u_enethd\": \"network error while testing target existence; retrying...\",\n\t\t\"u_cbusy\": \"waiting for server to trust us again after a network glitch...\",\n\t\t\"u_ehsdf\": \"server ran out of disk space!\\n\\nwill keep retrying, in case someone\\nfrees up enough space to continue\",\n\t\t\"u_emtleak1\": \"it looks like your webbrowser may have a memory leak;\\nplease\",\n\t\t\"u_emtleak2\": ' <a href=\"{0}\">switch to https (recommended)</a> or ',\n\t\t\"u_emtleak3\": ' ',\n\t\t\"u_emtleakc\": 'try the following:\\n<ul><li>hit <code>F5</code> to refresh the page</li><li>then disable the &nbsp;<code>mt</code>&nbsp; button in the &nbsp;<code>⚙️ settings</code></li><li>and try that upload again</li></ul>Uploads will be a bit slower, but oh well.\\nSorry for the trouble !\\n\\nPS: chrome v107 <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">has a bugfix</a> for this',\n\t\t\"u_emtleakf\": 'try the following:\\n<ul><li>hit <code>F5</code> to refresh the page</li><li>then enable <code>🥔</code> (potato) in the upload UI<li>and try that upload again</li></ul>\\nPS: firefox <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">will hopefully have a bugfix</a> at some point',\n\t\t\"u_s404\": \"not found on server\",\n\t\t\"u_expl\": \"explain\",\n\t\t\"u_maxconn\": \"most browsers limit this to 6, but firefox lets you raise it with <code>connections-per-server</code> in <code>about:config</code>\",\n\t\t\"u_tu\": '<p class=\"warn\">WARNING: turbo enabled, <span>&nbsp;client may not detect and resume incomplete uploads; see turbo-button tooltip</span></p>',\n\t\t\"u_ts\": '<p class=\"warn\">WARNING: turbo enabled, <span>&nbsp;search results can be incorrect; see turbo-button tooltip</span></p>',\n\t\t\"u_turbo_c\": \"turbo is disabled in server config\",\n\t\t\"u_turbo_g\": \"disabling turbo because you don't have\\ndirectory listing privileges within this volume\",\n\t\t\"u_life_cfg\": 'autodelete after <input id=\"lifem\" p=\"60\" /> min (or <input id=\"lifeh\" p=\"3600\" /> hours)',\n\t\t\"u_life_est\": 'upload will be deleted <span id=\"lifew\" tt=\"local time\">---</span>',\n\t\t\"u_life_max\": 'this folder enforces a\\nmax lifetime of {0}',\n\t\t\"u_unp_ok\": 'unpost is allowed for {0}',\n\t\t\"u_unp_ng\": 'unpost will NOT be allowed',\n\t\t\"ue_ro\": 'your access to this folder is Read-Only\\n\\n',\n\t\t\"ue_nl\": 'you are currently not logged in',\n\t\t\"ue_la\": 'you are currently logged in as \"{0}\"',\n\t\t\"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',\n\t\t\"ue_ta\": 'try uploading again, it should work now',\n\t\t\"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 🧯\",\n\t\t\"ur_1uo\": \"OK: File uploaded successfully\",\n\t\t\"ur_auo\": \"OK: All {0} files uploaded successfully\",\n\t\t\"ur_1so\": \"OK: File found on server\",\n\t\t\"ur_aso\": \"OK: All {0} files found on server\",\n\t\t\"ur_1un\": \"Upload failed, sorry\",\n\t\t\"ur_aun\": \"All {0} uploads failed, sorry\",\n\t\t\"ur_1sn\": \"File was NOT found on server\",\n\t\t\"ur_asn\": \"The {0} files were NOT found on server\",\n\t\t\"ur_um\": \"Finished;\\n{0} uploads OK,\\n{1} uploads failed, sorry\",\n\t\t\"ur_sm\": \"Finished;\\n{0} files found on server,\\n{1} files NOT found on server\",\n\n\t\t\"rc_opn\": \"open\",\n\t\t\"rc_ply\": \"play\",\n\t\t\"rc_pla\": \"play as audio\",\n\t\t\"rc_txt\": \"open in textfile viewer\",\n\t\t\"rc_md\": \"open in markdown viewer\",\n\t\t\"rc_dl\": \"download\",\n\t\t\"rc_zip\": \"download as archive\",\n\t\t\"rc_cpl\": \"copy link\",\n\t\t\"rc_del\": \"delete\",\n\t\t\"rc_cut\": \"cut\",\n\t\t\"rc_cpy\": \"copy\",\n\t\t\"rc_pst\": \"paste\",\n\t\t\"rc_rnm\": \"rename\",\n\t\t\"rc_nfo\": \"new folder\",\n\t\t\"rc_nfi\": \"new file\",\n\t\t\"rc_sal\": \"select all\",\n\t\t\"rc_sin\": \"invert selection\",\n\t\t\"rc_shf\": \"share this folder\",\n\t\t\"rc_shs\": \"share selection\",\n\n\t\t\"lang_set\": \"refresh to make the change take effect?\",\n};\n\nvar LANGN = [\n\t[\"eng\", \"English\"],\n\t[\"nor\", \"Norsk\"],\n\t[\"chi\", \"中文\"],\n\t[\"cze\", \"Čeština\"],\n\t[\"deu\", \"Deutsch\"],\n\t[\"epo\", \"Esperanto\"],\n\t[\"fin\", \"Suomi\"],\n\t[\"fra\", \"français\"],\n\t[\"grc\", \"Ελληνικά\"],\n\t[\"hun\", \"Magyar\"],\n\t[\"ita\", \"Italiano\"],\n\t[\"jpn\", \"日本語\"],\n\t[\"kor\", \"한국어\"],\n\t[\"nld\", \"Nederlands\"],\n\t[\"nno\", \"Nynorsk\"],\n\t[\"pol\", \"Polski\"],\n\t[\"por\", \"Português\"],\n\t[\"rus\", \"Русский\"],\n\t[\"spa\", \"Español\"],\n\t[\"swe\", \"Svenska\"],\n\t[\"tur\", \"Türkçe\"],\n\t[\"ukr\", \"Українська\"],\n\t[\"vie\", \"Tiếng Việt\"],\n];\n\nif (window.langmod)\n\tlangmod();\n\nvar L = Ls[lang] || Ls.eng, LANGS = [];\nfor (var a = 0; a < LANGN.length; a++)\n\tLANGS.push(LANGN[a][0]);\n\n\nfunction langtest() {\n\tvar n = LANGS.length - 1;\n\tfor (var a = 1; a < LANGS.length; a++)\n\t\timport_js(SR + '/.cpr/w/tl/' + LANGS[a] + '.js', function () { if (!--n) langtest2(); });\n}\nfunction langtest2() {\nfor (var a = 0; a < LANGS.length; a++) {\n\tfor (var b = a + 1; b < LANGS.length; b++) {\n\t\tvar i1 = Object.keys(Ls[LANGS[a]]).length > Object.keys(Ls[LANGS[b]]).length ? a : b,\n\t\t\ti2 = i1 == a ? b : a,\n\t\t\tt1 = Ls[LANGS[i1]],\n\t\t\tt2 = Ls[LANGS[i2]];\n\n\t\tfor (var k in t1)\n\t\t\tif (!t2[k] && !/^ht_.5$/.test(k)) {\n\t\t\t\tconsole.log(\"E missing TL\", LANGS[i2], k);\n\t\t\t\tt2[k] = t1[k];\n\t\t\t}\n\t}\n}\n}\n\n\n\nif (!Ls[lang])\n\talert('unsupported --lang \"' + lang + '\" specified in server args;\\nplease use one of these: ' + LANGS);\n\nmodal.load();\n\n\n// toolbar\nebi('ops').innerHTML = (\n\t'<a href=\"#\" id=\"opa_x\" data-dest=\"\" tt=\"' + L.ot_close + '\">--</a>' +\n\t'<a href=\"#\" id=\"opa_srch\" data-perm=\"read\" data-dep=\"idx\" data-dest=\"search\" tt=\"' + L.ot_search + '\">🔎</a>' +\n\t(have_del ? '<a href=\"#\" id=\"opa_del\" data-perm=\"write\" data-dest=\"unpost\" tt=\"' + L.ot_unpost + '\">🧯</a>' : '') +\n\t'<a href=\"#\" id=\"opa_up\" data-dest=\"up2k\">🚀</a>' +\n\t'<a href=\"#\" id=\"opa_bup\" data-perm=\"write\" data-dest=\"bup\" tt=\"' + L.ot_bup + '\">🎈</a>' +\n\t'<a href=\"#\" id=\"opa_mkd\" data-perm=\"write\" data-dest=\"mkdir\" tt=\"' + L.ot_mkdir + '\">📂</a>' +\n\t'<a href=\"#\" id=\"opa_md\" data-perm=\"read write\" data-dest=\"new_md\" tt=\"' + L.ot_md + '\">📝</a>' +\n\t'<a href=\"#\" id=\"opa_msg\" data-dest=\"msg\" tt=\"' + L.ot_msg + '\">📟</a>' +\n\t'<a href=\"#\" id=\"opa_auc\" data-dest=\"player\" tt=\"' + L.ot_mp + '\">🎺</a>' +\n\t'<a href=\"#\" id=\"opa_cfg\" data-dest=\"cfg\" tt=\"' + L.ot_cfg + '\">⚙️</a>' +\n\t(IE ? '<span id=\"noie\">' + L.ot_noie + '</span>' : '') +\n\t'<div id=\"opdesc\"></div>'\n);\n\n\n// media player\nebi('widget').innerHTML = (\n\t'<div id=\"wtoggle\">' +\n\t'<span id=\"wfs\"></span>' +\n\t'<span id=\"wfm\"><a' +\n\t' href=\"#\" id=\"fshr\" tt=\"' + L.wt_shr + '\">📨<span>share</span></a><a' +\n\t' href=\"#\" id=\"fren\" tt=\"' + L.wt_ren + '\">✎<span>name</span></a><a' +\n\t' href=\"#\" id=\"fdel\" tt=\"' + L.wt_del + '\">⌫<span>del.</span></a><a' +\n\t' href=\"#\" id=\"fcut\" tt=\"' + L.wt_cut + '\">✂<span>cut</span></a><a' +\n\t' href=\"#\" id=\"fcpy\" tt=\"' + L.wt_cpy + '\">⧉<span>copy</span></a><a' +\n\t' href=\"#\" id=\"fpst\" tt=\"' + L.wt_pst + '\">📋<span>paste</span></a>' +\n\t'</span><span id=\"wzip1\"><a' +\n\t' href=\"#\" id=\"zip1\" tt=\"' + L.wt_zip1 + '\">📦<span>zip</span></a>' +\n\t'</span><span id=\"wzip\"><a' +\n\t' href=\"#\" id=\"selall\" tt=\"' + L.wt_selall + '\">sel.<br />all</a><a' +\n\t' href=\"#\" id=\"selinv\" tt=\"' + L.wt_selinv + '\">sel.<br />inv.</a><a' +\n\t' href=\"#\" id=\"selzip\" class=\"l1\" tt=\"' + L.wt_selzip + '\">zip</a><a' +\n\t' href=\"#\" id=\"seldl\" class=\"l1\" tt=\"' + L.wt_seldl + '\">dl</a>' +\n\t'</span><span id=\"wnp\"><a' +\n\t' href=\"#\" id=\"npirc\" tt=\"' + L.wt_npirc + '\">📋<span>irc</span></a><a' +\n\t' href=\"#\" id=\"nptxt\" tt=\"' + L.wt_nptxt + '\">📋<span>txt</span></a>' +\n\t'</span><span id=\"wm3u\"><a' +\n\t' href=\"#\" id=\"m3ua\" tt=\"' + L.wt_m3ua + '\">📻<span>add</span></a><a' +\n\t' href=\"#\" id=\"m3uc\" tt=\"' + L.wt_m3uc + '\">📻<span>copy</span></a>' +\n\t'</span><a' +\n\t'\thref=\"#\" id=\"wtgrid\" tt=\"' + L.wt_grid + '\">田</a><a' +\n\t'\thref=\"#\" id=\"wtico\">♫</a>' +\n\t'</div>' +\n\t'<div id=\"widgeti\">' +\n\t'\t<div id=\"pctl\"><a href=\"#\" id=\"bprev\" tt=\"' + L.wt_prev + '\">⏮</a><a href=\"#\" id=\"bplay\" tt=\"' + L.wt_play + '\">▶</a><a href=\"#\" id=\"bnext\" tt=\"' + L.wt_next + '\">⏭</a></div>' +\n\t'\t<canvas id=\"pvol\" width=\"288\" height=\"38\"></canvas>' +\n\t'\t<canvas id=\"barpos\"></canvas>' +\n\t'\t<canvas id=\"barbuf\"></canvas>' +\n\t'</div>' +\n\t'<div id=\"np_inf\">' +\n\t'\t<img id=\"np_img\" />' +\n\t'\t<span id=\"np_url\"></span>' +\n\t'\t<span id=\"np_circle\"></span>' +\n\t'\t<span id=\"np_album\"></span>' +\n\t'\t<span id=\"np_tn\"></span>' +\n\t'\t<span id=\"np_artist\"></span>' +\n\t'\t<span id=\"np_title\"></span>' +\n\t'\t<span id=\"np_pos\"></span>' +\n\t'\t<span id=\"np_dur\"></span>' +\n\t'</div>'\n);\n\n\n// up2k ui\nebi('op_up2k').innerHTML = (\n\t'<form id=\"u2form\" method=\"post\" enctype=\"multipart/form-data\" onsubmit=\"return false;\"></form>\\n' +\n\n\t'<table id=\"u2conf\">\\n' +\n\t'\t<tr>\\n' +\n\t'\t\t<td class=\"c\" data-perm=\"read\"><br />' + L.ul_par + '</td>\\n' +\n\t'\t\t<td class=\"c\" rowspan=\"2\">\\n' +\n\t'\t\t\t<input type=\"checkbox\" id=\"multitask\" />\\n' +\n\t'\t\t\t<label for=\"multitask\" tt=\"' + L.ut_mt + '\">🏃</label>\\n' +\n\t'\t\t</td>\\n' +\n\t'\t\t<td class=\"c\" rowspan=\"2\">\\n' +\n\t'\t\t\t<input type=\"checkbox\" id=\"potato\" />\\n' +\n\t'\t\t\t<label for=\"potato\" tt=\"' + L.ut_pot + '\">🥔</label>\\n' +\n\t'\t\t</td>\\n' +\n\t'\t\t<td class=\"c\" rowspan=\"2\">\\n' +\n\t'\t\t\t<input type=\"checkbox\" id=\"u2rand\" />\\n' +\n\t'\t\t\t<label for=\"u2rand\" tt=\"' + L.ut_rand + '\">🎲</label>\\n' +\n\t'\t\t</td>\\n' +\n\t'\t\t<td class=\"c\" rowspan=\"2\">\\n' +\n\t'\t\t\t<input type=\"checkbox\" id=\"u2ow\" />\\n' +\n\t'\t\t\t<label for=\"u2ow\" tt=\"' + L.ut_ow + '\">?</a>\\n' +\n\t'\t\t</td>\\n' +\n\t'\t\t<td class=\"c\" data-perm=\"read\" data-dep=\"idx\" rowspan=\"2\">\\n' +\n\t'\t\t\t<input type=\"checkbox\" id=\"fsearch\" />\\n' +\n\t'\t\t\t<label for=\"fsearch\" tt=\"' + L.ut_srch + '\">🔎</label>\\n' +\n\t'\t\t</td>\\n' +\n\t'\t\t<td data-perm=\"read\" rowspan=\"2\" id=\"u2btn_cw\"></td>\\n' +\n\t'\t\t<td data-perm=\"read\" rowspan=\"2\" id=\"u2c3w\"></td>\\n' +\n\t'\t</tr>\\n' +\n\t'\t<tr>\\n' +\n\t'\t\t<td class=\"c\" data-perm=\"read\">\\n' +\n\t'\t\t\t<a href=\"#\" class=\"b\" id=\"nthread_sub\">&ndash;</a><input\\n' +\n\t'\t\t\t\tclass=\"txtbox\" id=\"nthread\" value=\"\" tt=\"' + L.ut_par + '\"/><a\\n' +\n\t'\t\t\t\thref=\"#\" class=\"b\" id=\"nthread_add\">+</a><br />&nbsp;\\n' +\n\t'\t\t</td>\\n' +\n\t'\t</tr>\\n' +\n\t'</table>\\n' +\n\n\t'<div id=\"u2notbtn\"></div>\\n' +\n\n\t'<div id=\"u2btn_ct\">\\n' +\n\t'\t<div id=\"u2btn\" tabindex=\"0\">\\n' +\n\t'\t\t<span id=\"u2bm\"></span>\\n' + L.ul_btn +\n\t'\t</div>\\n' +\n\t'</div>\\n' +\n\n\t'<div id=\"u2c3t\">\\n' +\n\n\t'<div id=\"u2etaw\"><div id=\"u2etas\"><div class=\"o\">\\n' +\n\tL.ul_hash + ': <span id=\"u2etah\" tt=\"' + L.ut_etah + '\">(' + L.ul_idle1 + ')</span><br />\\n' +\n\tL.ul_send + ': <span id=\"u2etau\" tt=\"' + L.ut_etau + '\">(' + L.ul_idle1 + ')</span><br />\\n' +\n\t'\t</div><span class=\"o\">' +\n\tL.ul_done + ': </span><span id=\"u2etat\" tt=\"' + L.ut_etat + '\">(' + L.ul_idle1 + ')</span>\\n' +\n\t'</div></div>\\n' +\n\n\t'<div id=\"u2cards\">\\n' +\n\t'\t<a href=\"#\" act=\"ok\" tt=\"' + L.uct_ok + '\">ok <span>0</span></a><a\\n' +\n\t'\thref=\"#\" act=\"ng\" tt=\"' + L.uct_ng + '\">ng <span>0</span></a><a\\n' +\n\t'\thref=\"#\" act=\"done\" tt=\"' + L.uct_done + '\">done <span>0</span></a><a\\n' +\n\t'\thref=\"#\" act=\"bz\" tt=\"' + L.uct_bz + '\" class=\"act\">busy <span>0</span></a><a\\n' +\n\t'\thref=\"#\" act=\"q\" tt=\"' + L.uct_q + '\">que <span>0</span></a>\\n' +\n\t'</div>\\n' +\n\n\t'</div>\\n' +\n\n\t'<div id=\"u2tabw\" class=\"na\"><table id=\"u2tab\">\\n' +\n\t'\t<thead>\\n' +\n\t'\t\t<tr>\\n' +\n\t'\t\t\t<td>' + L.utl_name + ' &nbsp;(<a href=\"#\" id=\"luplinks\">' + L.utl_ulist + '</a>/<a href=\"#\" id=\"cuplinks\">' + L.utl_ucopy + '</a>' + L.utl_links + ')</td>\\n' +\n\t'\t\t\t<td>' + L.utl_stat + '</td>\\n' +\n\t'\t\t\t<td>' + L.utl_prog + '</td>\\n' +\n\t'\t\t</tr>\\n' +\n\t'\t</thead>\\n' +\n\t'\t<tbody></tbody>\\n' +\n\t'</table><div id=\"u2mu\"></div></div>\\n' +\n\n\t'<p id=\"u2flagblock\"><b>' + L.ul_flagblk + '</p>\\n' +\n\t'<div id=\"u2life\"></div>' +\n\t'<div id=\"u2foot\"></div>'\n);\n\n\nebi('wrap').insertBefore(mknod('div', 'lazy'), ebi('epi'));\n\nvar x = ebi('bbsw');\nx.parentNode.insertBefore(mknod('div', null,\n\t'<input type=\"checkbox\" id=\"uput\" name=\"uput\"><label for=\"uput\">' + L.u_uput + '</label>'), x);\n\n\n(function () {\n\tvar o = mknod('div');\n\to.innerHTML = (\n\t\t'<div id=\"drops\">\\n' +\n\t\t'\t<div class=\"dropdesc\" id=\"up_zd\"><div>🚀 ' + L.udt_up + '<br /><span></span><div>🚀<b>' + L.udt_up + '</b></div><div><b>' + L.udt_up + '</b>🚀</div></div></div>\\n' +\n\t\t'\t<div class=\"dropdesc\" id=\"srch_zd\"><div>🔎 ' + L.udt_srch + '<br /><span></span><div>🔎<b>' + L.udt_srch + '</b></div><div><b>' + L.udt_srch + '</b>🔎</div></div></div>\\n' +\n\t\t'\t<div class=\"dropzone\" id=\"up_dz\" v=\"up_zd\"></div>\\n' +\n\t\t'\t<div class=\"dropzone\" id=\"srch_dz\" v=\"srch_zd\"></div>\\n' +\n\t\t'</div>'\n\t);\n\tdocument.body.appendChild(o);\n})();\n\n\n// config panel\nebi('op_cfg').innerHTML = (\n\t'<div>\\n' +\n\t'\t<h3>' + L.cl_opts + '</h3>\\n' +\n\t'\t<div>\\n' +\n\t'\t\t<a id=\"tooltips\" class=\"tgl btn\" href=\"#\" tt=\"' + L.ct_ttips + '</a>\\n' +\n\t'\t\t<a id=\"griden\" class=\"tgl btn\" href=\"#\" tt=\"' + L.wt_grid + '\">' + L.ct_grid + '</a>\\n' +\n\t'\t\t<a id=\"thumbs\" class=\"tgl btn\" href=\"#\" tt=\"' + L.ct_thumb + '</a>\\n' +\n\t'\t\t<a id=\"csel\" class=\"tgl btn\" href=\"#\" tt=\"' + L.ct_csel + '</a>\\n' +\n\t'\t\t<a id=\"dsel\" class=\"tgl btn\" href=\"#\" tt=\"' + L.ct_dsel + '</a>\\n' +\n\t'\t\t<a id=\"dlni\" class=\"tgl btn\" href=\"#\" tt=\"' + L.ct_dl + '</a>\\n' +\n\t'\t\t<a id=\"ihop\" class=\"tgl btn\" href=\"#\" tt=\"' + L.ct_ihop + '</a>\\n' +\n\t'\t\t<a id=\"dotfiles\" class=\"tgl btn\" href=\"#\" tt=\"' + L.ct_dots + '</a>\\n' +\n\t'\t\t<a id=\"qdel\" class=\"tgl btn\" href=\"#\" tt=\"' + L.ct_qdel + '</a>\\n' +\n\t'\t\t<a id=\"dir1st\" class=\"tgl btn\" href=\"#\" tt=\"' + L.ct_dir1st + '</a>\\n' +\n\t'\t\t<a id=\"nsort\" class=\"tgl btn\" href=\"#\" tt=\"' + L.ct_nsort + '</a>\\n' +\n\t'\t\t<a id=\"utctid\" class=\"tgl btn\" href=\"#\" tt=\"' + L.ct_utc + '</a>\\n' +\n\t'\t\t<a id=\"ireadme\" class=\"tgl btn\" href=\"#\" tt=\"' + L.ct_readme + '</a>\\n' +\n\t'\t\t<a id=\"idxh\" class=\"tgl btn\" href=\"#\" tt=\"' + L.ct_idxh + '</a>\\n' +\n\t'\t\t<a id=\"sbars\" class=\"tgl btn\" href=\"#\" tt=\"' + L.ct_sbars + '</a>\\n' +\n\t'\t</div>\\n' +\n\t'</div>\\n' +\n\t'<div>\\n' +\n\t'\t<h3>' + L.cl_hfsz + '</h3>\\n' +\n\t'\t<div><select id=\"fszfmt\">\\n' +\n\t'\t\t<option value=\"0\">0 ┃ 1234567</option>\\n' +\n\t'\t\t<option value=\"1\">1 ┃ 1 234 567</option>\\n' +\n\t'\t\t<option value=\"2\">2- ┃ 1.18 M</option>\\n' +\n\t'\t\t<option value=\"2c\">2c ┃ 1.18 M</option>\\n' +\n\t'\t\t<option value=\"3\">3- ┃ 1.2 M</option>\\n' +\n\t'\t\t<option value=\"3c\">3c ┃ 1.2 M</option>\\n' +\n\t'\t\t<option value=\"4\">4- ┃ 1.18 MB</option>\\n' +\n\t'\t\t<option value=\"4c\">4c ┃ 1.18 MB</option>\\n' +\n\t'\t\t<option value=\"5\">5- ┃ 1.2 MB</option>\\n' +\n\t'\t\t<option value=\"5c\">5c ┃ 1.2 MB</option>\\n' +\n\t'\t\t<option value=\"fuzzy\">fuzzy</option>\\n' +\n\t'\t</select></div>\\n' +\n\t'</div>\\n' +\n\t'<div>\\n' +\n\t'\t<h3>' + L.cl_themes + '</h3>\\n' +\n\t'\t<div><select id=\"themes\"></select></div>\\n' +\n\t'\t</div>\\n' +\n\t'</div>\\n' +\n\t'<div>\\n' +\n\t'\t<h3>' + L.cl_langs + '</h3>\\n' +\n\t'\t<div><select id=\"langs\"></select></div>\\n' +\n\t'</div>\\n' +\n\t(have_zip ? (\n\t\t'<div><h3>' + L.cl_ziptype + '</h3><div id=\"arc_fmt\"></div></div>\\n'\n\t) : '') +\n\t'<div>\\n' +\n\t'\t<h3>' + L.cl_uopts + '</h3>\\n' +\n\t'\t<div>\\n' +\n\t'\t\t<a id=\"ask_up\" class=\"tgl btn\" href=\"#\" tt=\"' + L.ut_ask + '</a>\\n' +\n\t'\t\t<a id=\"u2ts\" class=\"tgl btn\" href=\"#\" tt=\"' + L.ut_u2ts + '</a>\\n' +\n\t'\t\t<a id=\"umod\" class=\"tgl btn\" href=\"#\" tt=\"' + L.cut_umod + '</a>\\n' +\n\t'\t\t<a id=\"hashw\" class=\"tgl btn\" href=\"#\" tt=\"' + L.cut_mt + '</a>\\n' +\n\t'\t\t<a id=\"nosubtle\" class=\"tgl btn\" href=\"#\" tt=\"' + L.cut_wasm + '</a>\\n' +\n\t'\t\t<a id=\"u2turbo\" class=\"tgl btn ttb\" href=\"#\" tt=\"' + L.cut_turbo + '</a>\\n' +\n\t'\t\t<a id=\"u2tdate\" class=\"tgl btn ttb\" href=\"#\" tt=\"' + L.cut_datechk + '</a>\\n' +\n\t'\t\t<input type=\"text\" id=\"u2szg\" value=\"\" ' + NOAC + ' style=\"width:3em\" tt=\"' + L.cut_u2sz + '\" />' +\n\t'\t\t<a id=\"flag_en\" class=\"tgl btn\" href=\"#\" tt=\"' + L.cut_flag + '\">💤</a>\\n' +\n\t'\t\t<a id=\"u2sort\" class=\"tgl btn\" href=\"#\" tt=\"' + L.cut_az + '\">az</a>\\n' +\n\t'\t\t<a id=\"upnag\" class=\"tgl btn\" href=\"#\" tt=\"' + L.cut_nag + '\">🔔</a>\\n' +\n\t'\t\t<a id=\"upsfx\" class=\"tgl btn\" href=\"#\" tt=\"' + L.cut_sfx + '\">🔊</a>\\n' +\n\t'\t\t</td>\\n' +\n\t'\t</div>\\n' +\n\t'</div>\\n' +\n\t'<div>\\n' +\n\t'\t<h3>' + L.cl_favico + ' <span id=\"ico1\">🎉</span></h3>\\n' +\n\t'\t<div>\\n' +\n\t'\t\t<input type=\"text\" id=\"icot\" value=\"\" ' + NOAC + ' style=\"width:1.3em\" tt=\"' + L.cft_text + '\" />' +\n\t'\t\t<input type=\"text\" id=\"icof\" value=\"\" ' + NOAC + ' style=\"width:2em\" tt=\"' + L.cft_fg + '\" />' +\n\t'\t\t<input type=\"text\" id=\"icob\" value=\"\" ' + NOAC + ' style=\"width:2em\" tt=\"' + L.cft_bg + '\" />' +\n\t'\t\t</td>\\n' +\n\t'\t</div>\\n' +\n\t'</div>\\n' +\n\t'<div>\\n' +\n\t'\t<h3>' + L.cl_bigdir + '</h3>\\n' +\n\t'\t<div>\\n' +\n\t'\t\t<input type=\"text\" id=\"bd_lim\" value=\"250\" ' + NOAC + ' style=\"width:4em\" tt=\"' + L.cdt_lim + '\" />' +\n\t'\t\t<a id=\"bd_ask\" class=\"tgl btn\" href=\"#\" tt=\"' + L.cdt_ask + '\">ask</a>\\n' +\n\t'\t\t</td>\\n' +\n\t'\t</div>\\n' +\n\t'</div>\\n' +\n\t'<div>\\n' +\n\t'\t<h3>' + L.cl_hsort + '</h3>\\n' +\n\t'\t<div>\\n' +\n\t'\t\t<input type=\"text\" id=\"hsortn\" value=\"\" ' + NOAC + ' style=\"width:3em\" tt=\"' + L.cdt_hsort + '\" />' +\n\t'\t\t</td>\\n' +\n\t'\t</div>\\n' +\n\t'</div>\\n' +\n\t'<div><h3>' + L.cl_keytype + '</h3><div><select id=\"key_notation\"></select></div></div>\\n' +\n\t(!MOBILE ? '<div><h3>' + L.cl_rcm + '</h3><div><a id=\"rcm_en\" class=\"tgl btn\" href=\"#\" tt=\"' + L.cdt_ren + '</a><a id=\"rcm_db\" class=\"tgl btn\" href=\"#\" tt=\"' + L.cdt_rdb + '</a></div></div>' : '') +\n\t'<div><h3>' + L.cl_hiddenc + ' &nbsp;' + (MOBILE ? '<a href=\"#\" id=\"hcolsh\">' + L.cl_hidec + '</a> / ' : '') + '<a href=\"#\" id=\"hcolsr\">' + L.cl_reset + '</a></h3><div id=\"hcols\"></div></div>'\n);\n\n\n// navpane\nebi('tree').innerHTML = (\n\t'<div id=\"treeh\">\\n' +\n\t'\t<a href=\"#\" id=\"detree\" tt=\"' + L.tt_detree + '\">🍞...</a>\\n' +\n\t'\t<a href=\"#\" class=\"btn\" step=\"2\" id=\"twobytwo\" tt=\"Hotkey: D\">+</a>\\n' +\n\t'\t<a href=\"#\" class=\"btn\" step=\"-2\" id=\"twig\" tt=\"Hotkey: A\">&ndash;</a>\\n' +\n\t'\t<a href=\"#\" class=\"btn\" id=\"visdir\" tt=\"' + L.tt_visdir + '\">🎯</a>\\n' +\n\t'\t<a href=\"#\" class=\"tgl btn\" id=\"filetree\" tt=\"' + L.tt_ftree + '\">📃</a>\\n' +\n\t'\t<a href=\"#\" class=\"tgl btn\" id=\"parpane\" tt=\"' + L.tt_pdock + '\">📌</a>\\n' +\n\t'\t<a href=\"#\" class=\"tgl btn\" id=\"dyntree\" tt=\"' + L.tt_dynt + '\">a</a>\\n' +\n\t'\t<a href=\"#\" class=\"tgl btn\" id=\"wraptree\" tt=\"' + L.tt_wrap + '\">↵</a>\\n' +\n\t'\t<a href=\"#\" class=\"tgl btn\" id=\"hovertree\" tt=\"' + L.tt_hover + '\">👀</a>\\n' +\n\t'</div>\\n' +\n\t'<ul id=\"docul\"></ul>\\n' +\n\t'<ul class=\"ntree\" id=\"treepar\"></ul>\\n' +\n\t'<ul class=\"ntree\" id=\"treeul\"></ul>\\n' +\n\t'<div id=\"thx_ff\">&nbsp;</div>'\n);\nclmod(ebi('tree'), 'sbar', 1);\nebi('entree').setAttribute('tt', L.tt_entree);\nebi('goh').textContent = L.goh;\nQS('#op_mkdir input[type=\"submit\"]').value = L.ab_mkdir;\nQS('#op_new_md input[type=\"submit\"]').value = L.ab_mkdoc;\nQS('#op_msg input[type=\"submit\"]').value = L.ab_msg;\n\n// right-click menu\nebi('rcm').innerHTML = (\n\t'<a href=\"#\" id=\"ropn\">' + L.rc_opn + '</a>' +\n\t'<a href=\"#\" id=\"rply\">' + L.rc_ply + '</a>' +\n\t'<a href=\"#\" id=\"rpla\">' + L.rc_pla + '</a>' +\n\t'<a href=\"#\" id=\"rtxt\">' + L.rc_txt + '</a>' +\n\t'<a href=\"#\" id=\"rmd\">' + L.rc_md + '</a>' +\n\t'<div id=\"rs1\" class=\"sep\"></div>' +\n\t'<a href=\"#\" id=\"rcpl\">' + L.rc_cpl + '</a>' +\n\t'<a href=\"#\" id=\"rdl\">' + L.rc_dl + '</a>' +\n\t(have_zip ?\n\t\t'<a href=\"#\" id=\"rzip\">' + L.rc_zip + '</a>'\n\t: '') +\n\t'<div id=\"rs2\" class=\"sep\"></div>' +\n\t(have_del ? '<a href=\"#\" id=\"rdel\">' + L.rc_del + '</a>' : '') +\n\t(have_mv ? '<a href=\"#\" id=\"rcut\">' + L.rc_cut + '</a>' : '') +\n\t'<a href=\"#\" id=\"rcpy\">' + L.rc_cpy + '</a>' +\n\t(has(perms, \"write\") ?\n\t\t'<a href=\"#\" id=\"rpst\">' + L.rc_pst + '</a>' +\n\t\t(have_mv ? '<a href=\"#\" id=\"rrnm\">' + L.rc_rnm + '</a>' : '') +\n\t\t'<div id=\"rs3\" class=\"sep\"></div>' +\n\t\t'<a href=\"#\" id=\"rnfo\">' + L.rc_nfo + '</a>' +\n\t\t'<a href=\"#\" id=\"rnfi\">' + L.rc_nfi + '</a>'\n\t: '') +\n\t'<div id=\"rs4\" class=\"sep\"></div>' +\n\t'<a href=\"#\" id=\"rsal\">' + L.rc_sal + '</a>' +\n\t'<a href=\"#\" id=\"rsin\">' + L.rc_sin + '</a>' +\n\t'<a id=\"rshr\" href=\"#\"></a>'\n);\n\n(function () {\n\tvar ops = QSA('#ops>a');\n\tfor (var a = 0; a < ops.length; a++) {\n\t\tops[a].onclick = opclick;\n\t\tvar v = ops[a].getAttribute('data-dest');\n\t\tif (v)\n\t\t\tops[a].href = '#v=' + v;\n\t}\n})();\n\n\nfunction opclick(e) {\n\tvar dest = this.getAttribute('data-dest');\n\tif (QS('#op_' + dest + '.act'))\n\t\tdest = '';\n\n\tswrite('opmode', dest || null);\n\tif (ctrl(e))\n\t\treturn;\n\n\tev(e);\n\tgoto(dest);\n\n\tvar input = QS('.opview.act input:not([type=\"hidden\"])')\n\tif (input && !TOUCH) {\n\t\ttt.skip = true;\n\t\tinput.focus();\n\t}\n}\n\n\nfunction goto(dest) {\n\tvar obj = QSA('.opview.act');\n\tfor (var a = obj.length - 1; a >= 0; a--)\n\t\tclmod(obj[a], 'act');\n\n\tobj = QSA('#ops>a');\n\tfor (var a = obj.length - 1; a >= 0; a--)\n\t\tclmod(obj[a], 'act');\n\n\tif (dest) {\n\t\tvar lnk = QS('#ops>a[data-dest=' + dest + ']'),\n\t\t\tnps = lnk.getAttribute('data-perm');\n\n\t\tnps = nps && nps.length ? nps.split(' ') : [];\n\n\t\tif (perms.length)\n\t\t\tfor (var a = 0; a < nps.length; a++)\n\t\t\t\tif (!has(perms, nps[a]))\n\t\t\t\t\treturn;\n\n\t\tif (!has(perms, 'read') && !has(perms, 'write') && (dest == 'up2k'))\n\t\t\treturn;\n\n\t\tclmod(ebi('op_' + dest), 'act', 1);\n\t\tclmod(lnk, 'act', 1);\n\n\t\tvar fn = window['goto_' + dest];\n\t\tif (fn)\n\t\t\tfn();\n\t}\n\n\tclmod(document.documentElement, 'op_open', dest);\n\n\tif (treectl)\n\t\ttreectl.onscroll();\n}\n\n\nvar m = SPINNER.split(','),\n\tSPINNER_CSS = SPINNER.slice(1 + m[0].length);\nSPINNER = m[0];\n\n\nvar SBW, SBH;  // scrollbar size\nfunction read_sbw() {\n\tvar el = mknod('div');\n\tel.style.cssText = 'overflow:scroll;width:100px;height:100px;position:absolute;top:0;left:0';\n\tdocument.body.appendChild(el);\n\tSBW = el.offsetWidth - el.clientWidth;\n\tSBH = el.offsetHeight - el.clientHeight;\n\tdocument.body.removeChild(el);\n\tsetcvar('--sbw', SBW + 'px');\n\tsetcvar('--sbh', SBH + 'px');\n}\nonresize100.add(read_sbw, true);\n\n\nfunction check_image_support(format, uri) {\n\tvar cached\n\t    = window['have_' + format]\n\t    = sread('have_' + format);\n\tif (cached !== null)\n\t\treturn;\n\n\tvar img = new Image();\n\timg.onload = function () {\n\t\twindow['have_' + format] = img.width > 0 && img.height > 0;\n\t\tswrite('have_' + format, 'ya');\n\t};\n\timg.onerror = function () {\n\t\twindow['have_' + format] = false;\n\t\tswrite('have_' + format, '');\n\t};\n\timg.src = uri;\n}\ncheck_image_support('webp', \"data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==\");\ncheck_image_support('jxl', \"data:image/jxl;base64,/woIAAAMABKIAgC4AF3lEgA=\");\n\n\nvar img_re = APPLE ?\n\t/\\.(a?png|avif|bmp|gif|hei[cf]s?|jpe?g|jfif|svg|webp|webm|mkv|mp4|m4v|mov)(\\?|$)/i :\n\t/\\.(a?png|avif|bmp|gif|jpe?g|jfif|svg|webp|webm|mkv|mp4|m4v|mov)(\\?|$)/i;\n\n\nfunction set_files_html(html) {\n\tvar files = ebi('files');\n\ttry {\n\t\tfiles.innerHTML = html;\n\t\treturn files;\n\t}\n\tcatch (e) {\n\t\tvar par = files.parentNode;\n\t\tpar.removeChild(files);\n\t\tfiles = mknod('div');\n\t\tfiles.innerHTML = '<table id=\"files\">' + html + '</table>';\n\t\tpar.insertBefore(files.childNodes[0], ebi('lazy'));\n\t\tfiles = ebi('files');\n\t\treturn files;\n\t}\n}\n\n\n// actx breaks background album playback on ios\nvar ACtx = !IPHONE && (window.AudioContext || window.webkitAudioContext),\n\tACB = sread('au_cbv') || 1,\n\thash0 = location.hash,\n\tsloc0 = '' + location,\n\tnoih = /[?&]v\\b/.exec(sloc0),\n\tfullui = /[?&]fullui\\b/.exec(sloc0),\n\tnonav = !fullui && (/[?&]nonav\\b/.exec(sloc0) || window.ui_nonav),\n\tnotree = !fullui && (/[?&]notree\\b/.exec(sloc0) || window.ui_notree || nonav),\n\tdbg_kbd = /[?&]dbgkbd\\b/.exec(sloc0),\n\tabrt_key = \"\",\n\tcan_shr = false,\n\tin_shr = false,\n\trtt = null,\n\tsrvinf = \"\",\n\tldks = [],\n\tdks = {},\n\tdk, mp;\n\n\nvar x = '';\nif (!fullui) {\n\tif (window.ui_nombar || /[?&]nombar\\b/.exec(sloc0)) x += '#ops,';\n\tif (window.ui_noacci || /[?&]noacci\\b/.exec(sloc0)) x += '#acc_info,';\n\tif (window.ui_nosrvi || /[?&]nosrvi\\b/.exec(sloc0)) x += '#srv_info,#srv_info2,';\n\tif (window.ui_nocpla || /[?&]nocpla\\b/.exec(sloc0)) x += '#goh,';\n\tif (window.ui_nolbar || /[?&]nolbar\\b/.exec(sloc0)) x += '#wfp,';\n\tif (window.ui_noctxb || /[?&]noctxb\\b/.exec(sloc0)) x += '#wtoggle,';\n\tif (window.ui_norepl || /[?&]norepl\\b/.exec(sloc0)) x += '#repl,';\n}\nif (x)\n\tdocument.head.appendChild(mknod('style', '', x.slice(0, -1) + '{display:none!important}'));\n\n\nif (location.pathname.indexOf('//') === 0)\n\thist_replace(location.pathname.replace(/^\\/+/, '/'));\n\n\nif (window.og_fn) {\n\thash0 = 1;\n\thist_replace(vsplit(get_evpath())[0]);\n}\n\n\nvar hsortn = ebi('hsortn').value = icfg_get('hsortn', dhsortn);\nebi('hsortn').oninput = function (e) {\n\tvar n = parseInt(this.value);\n\tswrite('hsortn', hsortn = (isNum(n) ? n : dhsortn));\n};\n(function() {\n\tvar args = ('' + hash0).split(/,sort/g);\n\tif (args.length < 2)\n\t\treturn;\n\n\tvar ret = [];\n\tfor (var a = 1; a < args.length; a++) {\n\t\tvar t = '', n = 1, z = args[a].split(',')[0];\n\t\tif (z.startsWith('-')) {\n\t\t\tz = z.slice(1);\n\t\t\tn = -1;\n\t\t}\n\t\tif (z == \"sz\" || z.indexOf('/.') + 1)\n\t\t\tt = \"int\";\n\t\tret.push([z, n, t]);\n\t}\n\tn = Math.min(ret.length, hsortn);\n\tif (n) {\n\t\tvar cmp = jread('fsort', []);\n\t\tif (JSON.stringify(ret.slice(0, n) !=\n\t\t\tJSON.stringify(cmp.slice(0, n))))\n\t\t\tjwrite('fsort', ret);\n\t}\n})();\n\n\nvar mpl = (function () {\n\tvar have_mctl = 'mediaSession' in navigator && window.MediaMetadata;\n\n\tebi('op_player').innerHTML = (\n\t\t'<div><h3>' + L.cl_opts + '</h3><div>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"au_loop\" tt=\"' + L.mt_loop + '</a>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"au_one\" tt=\"' + L.mt_one + '</a>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"au_shuf\" tt=\"' + L.mt_shuf + '</a>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"au_aplay\" tt=\"' + L.mt_aplay + '</a>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"au_preload\" tt=\"' + L.mt_preload + '</a>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"au_prescan\" tt=\"' + L.mt_prescan + '</a>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"au_fullpre\" tt=\"' + L.mt_fullpre + '</a>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"au_fau\" tt=\"' + L.mt_fau + '</a>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"au_waves\" tt=\"' + L.mt_waves + '</a>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"au_npclip\" tt=\"' + L.mt_npclip + '</a>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"au_m3u_c\" tt=\"' + L.mt_m3u_c + '</a>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"au_os_ctl\" tt=\"' + L.mt_octl + '</a>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"au_os_seek\" tt=\"' + L.mt_oseek + '</a>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"au_osd_cv\" tt=\"' + L.mt_oscv + '</a>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"au_follow\" tt=\"' + L.mt_follow + '</a>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"au_compact\" tt=\"' + L.mt_compact + '</a>' +\n\t\t'</div></div>' +\n\n\t\t'<div><h3>' + L.ml_btns + '</h3><div>' +\n\t\t'<a href=\"#\" class=\"btn\" id=\"au_uncache\" tt=\"' + L.mt_uncache + '</a>' +\n\t\t'</div></div>' +\n\n\t\t'<div><h3>' + L.ml_pmode + '</h3><div id=\"pb_mode\">' +\n\t\t'<a href=\"#\" class=\"tgl btn\" m=\"loop\" tt=\"' + L.mt_mloop + '</a>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" m=\"next\" tt=\"' + L.mt_mnext + '</a>' +\n\t\t'<a href=\"#\" class=\"tgl btn\" m=\"stop\" tt=\"' + L.mt_mstop + '</a>' +\n\t\t'</div></div>' +\n\n\t\t(have_acode ? (\n\t\t\t'<div><h3>' + L.ml_tcode + '</h3><div>' +\n\t\t\t'<a href=\"#\" id=\"ac_flac\" class=\"tgl btn\" tt=\"' + L.mt_cflac + '</a>' +\n\t\t\t'<a href=\"#\" id=\"ac_aac\" class=\"tgl btn\" tt=\"' + L.mt_caac + '</a>' +\n\t\t\t'<a href=\"#\" id=\"ac_oth\" class=\"tgl btn\" tt=\"' + L.mt_coth + '</a>' +\n\t\t\t'</div></div>' +\n\t\t\t'<div><h3>' + L.ml_tcode2 + '</h3><div>' +\n\t\t\t'<a href=\"#\" id=\"ac2opus\" class=\"tgl btn\" tt=\"' + L.mt_c2opus + '</a>' +\n\t\t\t'<a href=\"#\" id=\"ac2owa\" class=\"tgl btn\" tt=\"' + L.mt_c2owa + '</a>' +\n\t\t\t'<a href=\"#\" id=\"ac2caf\" class=\"tgl btn\" tt=\"' + L.mt_c2caf + '</a>' +\n\t\t\t'<a href=\"#\" id=\"ac2mp3\" class=\"tgl btn\" tt=\"' + L.mt_c2mp3 + '</a>' +\n\t\t\t'<a href=\"#\" id=\"ac2flac\" class=\"tgl btn\" tt=\"' + L.mt_c2flac + '</a>' +\n\t\t\t'<a href=\"#\" id=\"ac2wav\" class=\"tgl btn\" tt=\"' + L.mt_c2wav + '</a>' +\n\t\t\t'</div></div>'\n\t\t) : '') +\n\n\t\t'<div><h3>' + L.ml_tint + '</h3><div>' +\n\t\t'<input type=\"text\" id=\"pb_tint\" value=\"0\" ' + NOAC + ' style=\"width:2.4em\" tt=\"' + L.mt_tint + '\" />' +\n\t\t'</div></div>' +\n\n\t\t'<div><h3 id=\"h_drc\">' + L.ml_drc + '</h3><div id=\"audio_drc\"></div></div>' +\n\t\t'<div><h3>' + L.ml_eq + '</h3><div id=\"audio_eq\"></div></div>' +\n\t\t'<div><h3>' + L.ml_ss + '</h3><div id=\"audio_ss\"></div></div>' +\n\t\t'');\n\n\tvar r = {\n\t\t\"pb_mode\": (sread('pb_mode', ['loop', 'next', 'stop']) || 'next').split('-')[0],\n\t\t\"os_ctl\": bcfg_get('au_os_ctl', have_mctl) && have_mctl,\n\t\t'traversals': 0,\n\t\t'm3ut': '#EXTM3U\\n',\n\t\t'np': [{'file': 'nothing'}, ['file']],\n\t};\n\tbcfg_bind(r, 'one', 'au_one', false, function (v) {\n\t\tif (mp.au)\n\t\t\tmp.au.loop = !v && r.loop;\n\t});\n\tbcfg_bind(r, 'loop', 'au_loop', false, function (v) {\n\t\tif (mp.au)\n\t\t\tmp.au.loop = v;\n\t});\n\tbcfg_bind(r, 'shuf', 'au_shuf', false, function () {\n\t\tmp.read_order();  // don't bind\n\t});\n\tbcfg_bind(r, 'aplay', 'au_aplay', true);\n\tbcfg_bind(r, 'preload', 'au_preload', true);\n\tbcfg_bind(r, 'prescan', 'au_prescan', true);\n\tbcfg_bind(r, 'fullpre', 'au_fullpre', false);\n\tbcfg_bind(r, 'fau', 'au_fau', MOBILE && !IPHONE, function (v) {\n\t\tmp.nopause();\n\t\tif (mp.fau) {\n\t\t\tmp.fau.pause();\n\t\t\tmp.fau = mpo.fau = null;\n\t\t\tconsole.log('stop fau');\n\t\t}\n\t\tmp.init_fau();\n\t});\n\tbcfg_bind(r, 'waves', 'au_waves', true, function (v) {\n\t\tif (!v) pbar.unwave();\n\t});\n\tbcfg_bind(r, 'os_seek', 'au_os_seek', !IPHONE, announce);\n\tbcfg_bind(r, 'osd_cv', 'au_osd_cv', true, announce);\n\tbcfg_bind(r, 'clip', 'au_npclip', false, function (v) {\n\t\tclmod(ebi('wtoggle'), 'np', v && mp.au);\n\t});\n\tbcfg_bind(r, 'm3uen', 'au_m3u_c', false, function (v) {\n\t\tclmod(ebi('wtoggle'), 'm3u', v && (mp.au || msel.getsel().length));\n\t});\n\tbcfg_bind(r, 'follow', 'au_follow', false, setaufollow);\n\tbcfg_bind(r, 'ac_flac', 'ac_flac', true);\n\tbcfg_bind(r, 'ac_aac', 'ac_aac', false);\n\tbcfg_bind(r, 'ac_oth', 'ac_oth', true, reload_mp);\n\tif (!have_acode)\n\t\tr.ac_flac = r.ac_aac = r.ac_oth = false;\n\n\tif (IPHONE) {\n\t\tebi('au_fullpre').style.display = 'none';\n\t\tr.fullpre = false;\n\t}\n\n\tebi('au_uncache').onclick = function (e) {\n\t\tev(e);\n\t\tACB = (Date.now() % 46656).toString(36);\n\t\tswrite('au_cbv', ACB);\n\t\treload_mp();\n\t\ttoast.inf(5, L.mm_uncache);\n\t};\n\n\tebi('au_os_ctl').onclick = function (e) {\n\t\tev(e);\n\t\tr.os_ctl = !r.os_ctl && have_mctl;\n\t\tbcfg_set('au_os_ctl', r.os_ctl);\n\t\tif (!have_mctl)\n\t\t\ttoast.err(5, L.mp_breq);\n\t};\n\n\tfunction draw_pb_mode() {\n\t\tvar btns = QSA('#pb_mode>a');\n\t\tfor (var a = 0, aa = btns.length; a < aa; a++) {\n\t\t\tclmod(btns[a], 'on', btns[a].getAttribute(\"m\") == r.pb_mode);\n\t\t\tbtns[a].onclick = set_pb_mode;\n\t\t}\n\t}\n\tdraw_pb_mode();\n\n\tfunction set_pb_mode(e) {\n\t\tev(e);\n\t\tr.pb_mode = this.getAttribute('m');\n\t\tswrite('pb_mode', r.pb_mode);\n\t\tdraw_pb_mode();\n\t}\n\n\tfunction set_tint() {\n\t\tvar tint = icfg_get('pb_tint', 0);\n\t\tif (!tint)\n\t\t\tebi('barbuf').style.removeProperty('background');\n\t\telse\n\t\t\tebi('barbuf').style.background = 'rgba(126,163,75,' + (tint / 100.0) + ')';\n\t}\n\tebi('pb_tint').oninput = function (e) {\n\t\tswrite('pb_tint', this.value);\n\t\tset_tint();\n\t};\n\tset_tint();\n\n\tr.acode = function (url) {\n\t\tvar c = true,\n\t\t\tcs = url.split('?')[0];\n\n\t\tif (!have_acode)\n\t\t\tc = false;\n\t\telse if (/\\.(wav|flac)$/i.exec(cs))\n\t\t\tc = r.ac_flac;\n\t\telse if (/\\.(aac|m4[abr])$/i.exec(cs))\n\t\t\tc = r.ac_aac;\n\t\telse if (/\\.(oga|ogg|opus)$/i.exec(cs) && (!can_ogg || mpl.ac2 == 'mp3'))\n\t\t\tc = true;\n\t\telse if (re_au_native.exec(cs))\n\t\t\tc = false;\n\n\t\t// allow flac->flac (bitstream fixup)\n\t\tif (!c)\n\t\t\treturn url;\n\n\t\treturn addq(url, 'th=' + r.ac2);\n\t};\n\n\tr.set_ac2 = function () {\n\t\tr.init_ac2(this.getAttribute('id').split('ac2')[1]);\n\t};\n\n\tr.init_ac2 = function (v) {\n\t\tif (!window.have_acode) {\n\t\t\tr.ac2 = 'opus';\n\t\t\treturn;\n\t\t}\n\n\t\tvar dv = can_ogg ? 'opus' :\n\t\t\t\tcan_caf ? 'caf' : 'mp3',\n\t\t\tfmts = ['opus', 'owa', 'caf', 'mp3', 'flac', 'wav'],\n\t\t\tbtns = [];\n\n\t\tif (v === dv)\n\t\t\ttoast.ok(5, L.mt_c2ok);\n\t\telse if (v)\n\t\t\ttoast.inf(10, L.mt_c2nd);\n\n\t\tif ((v == 'opus' && !can_ogg) ||\n\t\t\t(v == 'caf' && !can_caf) ||\n\t\t\t(v == 'owa' && !can_owa) ||\n\t\t\t(v == 'flac' && !can_flac))\n\t\t\ttoast.warn(15, L.mt_c2ng);\n\n\t\tif (v == 'owa' && IPHONE)\n\t\t\ttoast.err(30, L.mt_xowa);\n\n\t\tfor (var a = 0; a < fmts.length; a++) {\n\t\t\tvar btn = ebi('ac2' + fmts[a]);\n\t\t\tif (!btn)\n\t\t\t\treturn console.log('!btn', fmts[a]);\n\t\t\tbtn.onclick = r.set_ac2;\n\t\t\tbtns.push(btn);\n\t\t}\n\t\tif (!IPHONE)\n\t\t\tbtns[1].style.display = btns[2].style.display = 'none';\n\t\tbtns[4].style.display = have_c2flac ? '' : 'none';\n\t\tbtns[5].style.display = have_c2wav ? '' : 'none';\n\n\t\tif (v)\n\t\t\tswrite('acode2', v);\n\t\telse\n\t\t\tv = dv;\n\n\t\tv = sread('acode2', fmts) || v;\n\t\tfor (var a = 0; a < fmts.length; a++)\n\t\t\tclmod(btns[a], 'on', fmts[a] == v)\n\n\t\tr.ac2 = v;\n\t\tebi('ac_flac').setAttribute('tt', L.mt_cflac.split('\"')[0].format(v));\n\t\tebi('ac_aac').setAttribute('tt', L.mt_caac.split('\"')[0].format(v));\n\t\tebi('ac_oth').setAttribute('tt', L.mt_coth.split('\"')[0].format(v));\n\t};\n\n\tr.pp = function () {\n\t\tvar adur, apos, playing = mp.au && !mp.au.paused;\n\n\t\tclearTimeout(mpl.t_eplay);\n\n\t\tclmod(ebi('np_inf'), 'playing', playing);\n\n\t\tif (mp.au && isNum(adur = mp.au.duration) && isNum(apos = mp.au.currentTime) && apos >= 0)\n\t\t\tebi('np_pos').textContent = s2ms(apos);\n\n\t\tif (!r.os_ctl)\n\t\t\treturn;\n\n\t\tnavigator.mediaSession.playbackState = playing ? \"playing\" : \"paused\";\n\t};\n\n\tfunction setaufollow() {\n\t\twindow[(r.follow ? \"add\" : \"remove\") + \"EventListener\"](\"resize\", scroll2playing);\n\t}\n\tsetaufollow();\n\n\tfunction announce() {\n\t\tif (!r.os_ctl || !mp.au)\n\t\t\treturn;\n\n\t\tvar np = mpl.np[0],\n\t\t\tfns = np.file.split(' - '),\n\t\t\tartist = (np.circle && np.circle != np.artist ? np.circle + ' // ' : '') + (np.artist || (fns.length > 1 ? fns[0] : '')),\n\t\t\ttitle = np.title || fns.pop(),\n\t\t\tcover = '',\n\t\t\ttags = { title: title };\n\n\t\tif (artist)\n\t\t\ttags.artist = artist;\n\n\t\tif (np.album)\n\t\t\ttags.album = np.album;\n\n\t\tif (r.osd_cv) {\n\t\t\tvar files = QSA(\"#files tr>td:nth-child(2)>a[id]\"),\n\t\t\t\tcover = null;\n\n\t\t\tfor (var a = 0, aa = files.length; a < aa; a++) {\n\t\t\t\tif (/^(cover|folder)\\.(jpe?g|png|gif)$/i.test(files[a].textContent)) {\n\t\t\t\t\tcover = files[a].getAttribute('href');\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcover = addq(cover || mp.au.osrc, 'th=j');\n\t\t\ttags.artwork = [{ \"src\": cover, type: \"image/jpeg\" }];\n\t\t}\n\n\t\tebi('np_circle').textContent = np.circle || '';\n\t\tebi('np_album').textContent = np.album || '';\n\t\tebi('np_tn').textContent = np['.tn'] || '';\n\t\tebi('np_artist').textContent = np.artist || (fns.length > 1 ? fns[0] : '');\n\t\tebi('np_title').textContent = np.title || '';\n\t\tebi('np_dur').textContent = np['.dur'] || '';\n\t\tebi('np_url').textContent = uricom_dec(get_evpath()) + np.file.split('?')[0];\n\t\tif (!MOBILE && cover)\n\t\t\tebi('np_img').setAttribute('src', cover);\n\t\telse\n\t\t\tebi('np_img').removeAttribute('src');\n\n\t\tnavigator.mediaSession.metadata = new MediaMetadata(tags);\n\t\tnavigator.mediaSession.setActionHandler('play', mplay);\n\t\tnavigator.mediaSession.setActionHandler('pause', mpause);\n\t\tnavigator.mediaSession.setActionHandler('seekbackward', r.os_seek ? function () { seek_au_rel(-10); } : null);\n\t\tnavigator.mediaSession.setActionHandler('seekforward', r.os_seek ? function () { seek_au_rel(10); } : null);\n\t\tnavigator.mediaSession.setActionHandler('previoustrack', prev_song);\n\t\tnavigator.mediaSession.setActionHandler('nexttrack', next_song);\n\t\tr.pp();\n\t}\n\tr.announce = announce;\n\n\tr.stop = function () {\n\t\tif (!r.os_ctl)\n\t\t\treturn;\n\n\t\t// dead code; left for debug\n\t\tnavigator.mediaSession.metadata = null;\n\t\tnavigator.mediaSession.playbackState = \"paused\";\n\n\t\tvar hs = 'play pause seekbackward seekforward previoustrack nexttrack'.split(/ /g);\n\t\tfor (var a = 0; a < hs.length; a++)\n\t\t\tnavigator.mediaSession.setActionHandler(hs[a], null);\n\n\t\tnavigator.mediaSession.setPositionState();\n\t};\n\n\tr.unbuffer = function (url) {\n\t\tif (mp.au2 && (!url || mp.au2.rsrc == url)) {\n\t\t\tmp.au2.src = mp.au2.rsrc = '';\n\t\t\tmp.au2.ld = 0; //owa\n\t\t\tmp.au2.load();\n\t\t}\n\t\tif (!url)\n\t\t\tmpl.preload_url = null;\n\t}\n\n\treturn r;\n})();\n\n\nvar za,\n\tcan_ogg = true,\n\tcan_owa = false,\n\tcan_flac = false,\n\tcan_caf = APPLE && !/ OS ([1-9]|1[01])_/.test(UA);\ntry {\n\tza = new Audio();\n\tcan_ogg = za.canPlayType('audio/ogg; codecs=opus') === 'probably';\n\tcan_owa = za.canPlayType('audio/webm; codecs=opus') === 'probably';\n\tcan_flac = za.canPlayType('audio/flac') === 'probably';\n\tcan_caf = za.canPlayType('audio/x-caf') && can_caf; //'maybe'\n}\ncatch (ex) { }\nza = null;\n\nif (can_owa && APPLE && / OS ([1-9]|1[0-7])_/.test(UA))\n\tcan_owa = false;\n\nmpl.init_ac2();\n\n\nvar re_m3u = /\\.(m3u8?)$/i;\nvar re_au_native = (can_ogg || have_acode) ? /\\.(aac|flac|m4[abr]|mp3|oga|ogg|opus|wav)$/i : /\\.(aac|flac|m4[abr]|mp3|wav)$/i,\n\tre_au_vid = /\\.(3gp|asf|avi|flv|m4v|mkv|mov|mp4|mpeg|mpeg2|mpegts|mpg|mpg2|nut|ogm|ogv|rm|ts|vob|webm|wmv)$/i,\n\tre_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;\n\n\n// extract songs + add play column\nvar mpo = { \"au\": null, \"au2\": null, \"acs\": null, \"fau\": null };\nfunction MPlayer() {\n\tvar r = this;\n\tr.id = Date.now();\n\tr.au = mpo.au;\n\tr.au2 = mpo.au2;\n\tr.acs = mpo.acs;\n\tr.fau = mpo.fau;\n\tr.tracks = {};\n\tr.order = [];\n\tr.cd_pause = 0;\n\n\tvar re_audio = have_acode && mpl.ac_oth ? re_au_all : re_au_native,\n\t\ttrs = QSA('#files tbody tr');\n\n\tfor (var a = 0, aa = trs.length; a < aa; a++) {\n\t\tvar tds = trs[a].getElementsByTagName('td'),\n\t\t\tlink = tds[1].getElementsByTagName('a');\n\n\t\tlink = link[link.length - 1];\n\t\tvar url = link.getAttribute('href'),\n\t\t\tfn = url.split('?')[0];\n\n\t\tif (re_audio.exec(fn)) {\n\t\t\tvar tid = link.getAttribute('id'),\n\t\t\t\ttxt = re_au_vid.exec(fn) ? '(🎧)' : L.mb_play;\n\t\t\tr.order.push(tid);\n\t\t\tr.tracks[tid] = url;\n\t\t\ttds[0].innerHTML = '<a id=\"a' + tid + '\" href=\"#a' + tid + '\" class=\"play\">' + txt + '</a></td>';\n\t\t\tebi('a' + tid).onclick = ev_play;\n\t\t\tclmod(trs[a], 'au', 1);\n\t\t}\n\t\telse if (re_m3u.exec(fn)) {\n\t\t\tvar tid = link.getAttribute('id');\n\t\t\ttds[0].innerHTML = '<a id=\"a' + tid + '\" href=\"#a' + tid + '\" class=\"play\">' + L.mb_play + '</a></td>';\n\t\t\tebi('a' + tid).onclick = ev_load_m3u;\n\t\t}\n\t}\n\n\tr.vol = clamp(fcfg_get('vol', IPHONE ? 1 : dvol / 100), 0, 1);\n\n\tr.expvol = function (v) {\n\t\treturn 0.5 * v + 0.5 * v * v;\n\t};\n\n\tr.setvol = function (vol) {\n\t\tr.vol = clamp(vol, 0, 1);\n\t\tswrite('vol', vol);\n\t\tr.stopfade(true);\n\n\t\tif (r.au)\n\t\t\tr.au.volume = r.expvol(r.vol);\n\t};\n\n\tr.shuffle = function () {\n\t\tif (!mpl.shuf)\n\t\t\treturn;\n\n\t\t// durstenfeld\n\t\tfor (var a = r.order.length - 1; a > 0; a--) {\n\t\t\tvar b = Math.floor(Math.random() * (a + 1)),\n\t\t\t\tc = r.order[a];\n\t\t\tr.order[a] = r.order[b];\n\t\t\tr.order[b] = c;\n\t\t}\n\t};\n\tr.shuffle();\n\n\tr.read_order = function () {\n\t\tvar order = [],\n\t\t\tlinks = QSA('#files>tbody>tr>td:nth-child(1)>a');\n\n\t\tfor (var a = 0, aa = links.length; a < aa; a++) {\n\t\t\tvar tid = links[a].getAttribute('id');\n\t\t\tif (!tid || tid.indexOf('af-') !== 0)\n\t\t\t\tcontinue;\n\n\t\t\torder.push(tid.slice(1));\n\t\t}\n\t\tr.order = order;\n\t\tr.shuffle();\n\t};\n\n\tr.fdir = 0;\n\tr.fvol = -1;\n\tr.ftid = -1;\n\tr.ftimer = null;\n\tr.fade_in = function () {\n\t\tr.nopause();\n\t\tstart_actx();\n\t\tr.fvol = 0;\n\t\tr.fdir = 0.025 * r.vol * (CHROME ? 1.5 : 1);\n\t\tif (r.au) {\n\t\t\tr.ftid = r.au.tid;\n\t\t\tr.au.play();\n\t\t\tmpl.pp();\n\t\t\tfader();\n\t\t}\n\t};\n\tr.fade_out = function () {\n\t\tmpss.stop();\n\t\tr.fvol = r.vol;\n\t\tr.fdir = -0.05 * r.vol * (CHROME ? 2 : 1);\n\t\tr.ftid = r.au.tid;\n\t\tfader();\n\t};\n\tr.stopfade = function (hard) {\n\t\tclearTimeout(r.ftimer);\n\t\tif (hard)\n\t\t\tr.ftid = -1;\n\t}\n\tfunction fader() {\n\t\tr.stopfade();\n\t\tif (!r.au || r.au.tid !== r.ftid)\n\t\t\treturn;\n\n\t\tvar done = true;\n\t\tr.fvol += r.fdir / (r.fdir < 0 && r.fvol < r.vol / 4 ? 2 : 1);\n\t\tif (r.fvol < 0) {\n\t\t\tr.fvol = 0;\n\t\t\tr.au.pause();\n\t\t\tmpl.pp();\n\n\t\t\tvar t = r.au.currentTime - 0.8;\n\t\t\tif (isNum(t))\n\t\t\t\tr.au.currentTime = Math.max(t, 0);\n\t\t}\n\t\telse if (r.fvol > r.vol) {\n\t\t\tr.fvol = r.vol;\n\t\t\tmpss.go();\n\t\t}\n\t\telse\n\t\t\tdone = false;\n\n\t\tr.au.volume = r.expvol(r.fvol);\n\t\tif (!done)\n\t\t\tsetTimeout(fader, 10);\n\t}\n\n\tr.preload = function (url, full) {\n\t\tvar t0 = Date.now(),\n\t\t\tfname = uricom_dec(url.split('/').pop().split('?')[0]);\n\n\t\turl = addq(mpl.acode(url), 'cache=987&_=' + ACB);\n\t\tmpl.preload_url = full ? url : null;\n\n\t\tif (mpl.waves)\n\t\t\tfetch(url.replace(/\\bth=(opus|mp3)&/, '') + '&th=p').then(function (x) {\n\t\t\t\tx.body.getReader().read();\n\t\t\t});\n\n\t\tif (full)\n\t\t\treturn fetch(url).then(function (x) {\n\t\t\t\tvar rd = x.body.getReader(), n = 0;\n\t\t\t\tfunction spd() {\n\t\t\t\t\treturn humansize(n / ((Date.now() + 1 - t0) / 1000)) + '/s';\n\t\t\t\t}\n\t\t\t\tfunction drop(x) {\n\t\t\t\t\tif (x && x.done)\n\t\t\t\t\t\treturn console.log('xhr-preload finished, ' + spd());\n\n\t\t\t\t\tif (x && x.value && x.value.length)\n\t\t\t\t\t\tn += x.value.length;\n\n\t\t\t\t\tif (mpl.preload_url !== url || n >= 128 * 1024 * 1024) {\n\t\t\t\t\t\tconsole.log('xhr-preload aborted at ' + Math.floor(n / 1024) + ' KiB, ' + spd() + ' for ' + url);\n\t\t\t\t\t\treturn rd.cancel();\n\t\t\t\t\t}\n\n\t\t\t\t\treturn rd.read().then(drop);\n\t\t\t\t}\n\t\t\t\tdrop();\n\t\t\t});\n\n\t\tr.nopause();\n\t\tr.au2.ld = 0; //owa\n\t\tr.au2.onloadeddata = r.au2.onloadedmetadata = r.onpreload;\n\t\tr.au2.preload = \"auto\";\n\t\tr.au2.src = r.au2.rsrc = url;\n\n\t\tif (mpl.prescan_evp) {\n\t\t\tmpl.prescan_evp = null;\n\t\t\ttoast.ok(7, L.mm_scank + \"\\n\" + esc(fname));\n\t\t}\n\t\tconsole.log(\"preloading \" + fname);\n\t};\n\n\tr.nopause = function () {\n\t\tr.cd_pause = Date.now();\n\t};\n\n\tr.onpreload = function () {\n\t\tr.nopause();\n\t\tthis.ld++;\n\t};\n\n\tr.init_fau = function () {\n\t\tif (r.fau || !mpl.fau)\n\t\t\treturn;\n\n\t\t// breaks touchbar-macs\n\t\tconsole.log('init fau');\n\t\tr.fau = new Audio(SR + '/.cpr/w/deps/busy.mp3?_=' + TS);\n\t\tr.fau.loop = true;\n\t\tr.fau.play();\n\t};\n\n\tr.set_ev = function () {\n\t\tmp.au.onended = evau_end;\n\t\tmp.au.onerror = evau_error;\n\t\tmp.au.onprogress = pbar.drawpos;\n\t\tmp.au.onplaying = mpui.progress_updater;\n\t\tmp.au.onloadeddata = mp.au.onloadedmetadata = mp.nopause;\n\t};\n}\n\n\nfunction ft2dict(tr, skip) {\n\tvar th = ebi('files').tHead.rows[0].cells,\n\t\trv = [],\n\t\trh = [],\n\t\tra = [],\n\t\trt = {};\n\n\tskip = skip || {};\n\n\tfor (var a = 1, aa = th.length; a < aa; a++) {\n\t\tvar tv = tr.cells[a].textContent,\n\t\t\ttk = a == 1 ? 'file' : th[a].getAttribute('name').split('/').pop().toLowerCase(),\n\t\t\tvis = th[a].className.indexOf('min') === -1;\n\n\t\tif (!tv || skip[tk])\n\t\t\tcontinue;\n\n\t\t(vis ? rv : rh).push(tk);\n\t\tra.push(tk);\n\t\trt[tk] = tv;\n\t}\n\treturn [rt, rv, rh, ra];\n}\n\n\n// toggle player widget\nvar widget = (function () {\n\tvar r = {},\n\t\twidget = ebi('widget'),\n\t\twtico = ebi('wtico'),\n\t\tnptxt = ebi('nptxt'),\n\t\tnpirc = ebi('npirc'),\n\t\tm3ua = ebi('m3ua'),\n\t\tm3uc = ebi('m3uc'),\n\t\ttouchmode = false,\n\t\twas_paused = true;\n\n\tr.open = function () {\n\t\treturn r.set(true);\n\t};\n\tr.close = function () {\n\t\treturn r.set(false);\n\t};\n\tr.set = function (is_open) {\n\t\tif (r.is_open == is_open)\n\t\t\treturn false;\n\n\t\tclmod(document.documentElement, 'np_open', is_open);\n\t\tclmod(widget, 'open', is_open);\n\t\tbcfg_set('au_open', r.is_open = is_open);\n\t\tif (vbar) {\n\t\t\tpbar.onresize();\n\t\t\tvbar.onresize();\n\t\t}\n\t\treturn true;\n\t};\n\tr.toggle = function (e) {\n\t\tr.open() || r.close();\n\t\tev(e);\n\t\treturn false;\n\t};\n\tr.paused = function (paused) {\n\t\tif (was_paused != paused) {\n\t\t\twas_paused = paused;\n\t\t\tebi('bplay').innerHTML = paused ? '▶' : '⏸';\n\t\t}\n\t};\n\tr.setvis = function () {\n\t\twidget.style.display = !has(perms, \"read\") || showfile.abrt ? 'none' : '';\n\t};\n\twtico.onclick = function (e) {\n\t\tif (!touchmode)\n\t\t\tr.toggle(e);\n\n\t\treturn false;\n\t};\n\tnpirc.onclick = nptxt.onclick = function (e) {\n\t\tev(e);\n\t\tvar irc = this.getAttribute('id') == 'npirc',\n\t\t\tck = irc ? '\u000306' : '',\n\t\t\tcv = irc ? '\u000307' : '',\n\t\t\tm = ck + 'np: ',\n\t\t\tnpk = mpl.np[1],\n\t\t\tnp = mpl.np[0];\n\n\t\tfor (var a = 0; a < npk.length; a++)\n\t\t\tm += (npk[a] == 'file' ? '' : npk[a]).replace(/^\\./, '') + '(' + cv + np[npk[a]] + ck + ') // ';\n\n\t\tm += '[' + cv + s2ms(mp.au.currentTime) + ck + '/' + cv + s2ms(mp.au.duration) + ck + ']';\n\n\t\tcliptxt(m, function () {\n\t\t\ttoast.ok(1, L.clipped, null, 'top');\n\t\t});\n\t};\n\tm3ua.onclick = function (e) {\n\t\tev(e);\n\t\tvar el,\n\t\t\tfiles = [],\n\t\t\tsel = msel.getsel();\n\n\t\tfor (var a = 0; a < sel.length; a++) {\n\t\t\tel = ebi(sel[a].id).closest('tr');\n\t\t\tif (clgot(el, 'au'))\n\t\t\t\tfiles.push(el);\n\t\t}\n\t\tel = QS('#files tr.play');\n\t\tif (!sel.length && el)\n\t\t\tfiles.push(el);\n\n\t\tfor (var a = 0; a < files.length; a++) {\n\t\t\tvar md = ft2dict(files[a])[0],\n\t\t\t\tdur = md['.dur'] || '1',\n\t\t\t\ttag = '';\n\n\t\t\tif (md.artist && md.title)\n\t\t\t\ttag = md.artist + ' - ' + md.title;\n\t\t\telse if (md.artist)\n\t\t\t\ttag = md.artist + ' - ' + md.file;\n\t\t\telse if (md.title)\n\t\t\t\ttag = md.title;\n\n\t\t\tif (dur.indexOf(':') > 0) {\n\t\t\t\tdur = dur.split(':');\n\t\t\t\tdur = 60 * parseInt(dur[0]) + parseInt(dur[1]);\n\t\t\t}\n\t\t\telse dur = parseInt(dur);\n\n\t\t\tmpl.m3ut += '#EXTINF:' + dur + ',' + tag + '\\n' + uricom_dec(get_evpath()) + md.file + '\\n';\n\t\t}\n\t\ttoast.ok(2, files.length == 1 ? L.m3u_add1 : L.m3u_addn.format(files.length), null, 'top');\n\t};\n\tm3uc.onclick = function (e) {\n\t\tev(e);\n\t\tcliptxt(mpl.m3ut, function () {\n\t\t\ttoast.ok(15, L.m3u_clip, null, 'top');\n\t\t});\n\t};\n\tr.set(sread('au_open') == 1);\n\tsetTimeout(function () {\n\t\tclmod(widget, 'anim', 1);\n\t}, 10);\n\treturn r;\n})();\n\n\nfunction canvas_cfg(can) {\n\tvar r = {},\n\t\tb = can.getBoundingClientRect(),\n\t\tmul = window.devicePixelRatio || 1;\n\n\tr.w = b.width;\n\tr.h = b.height;\n\tcan.width = r.w * mul;\n\tcan.height = r.h * mul;\n\n\tr.can = can;\n\tr.ctx = can.getContext('2d');\n\tr.ctx.scale(mul, mul);\n\treturn r;\n}\n\n\nfunction glossy_grad(can, h, s, l) {\n\tvar g = can.ctx.createLinearGradient(0, 0, 0, can.h),\n\t\tp = [0, 0.49, 0.50, 1];\n\n\tfor (var a = 0; a < p.length; a++)\n\t\tg.addColorStop(p[a], 'hsl(' + h + ',' + s[a] + '%,' + l[a] + '%)');\n\n\treturn g;\n}\n\n\n// buffer/position bar\nvar pbar = (function () {\n\tvar r = {},\n\t\tbau = null,\n\t\thtml_txt = 'a',\n\t\tlastmove = 0,\n\t\tmousepos = 0,\n\t\tt_redraw = 0,\n\t\tgradh = -1,\n\t\tgrad;\n\n\tr.onresize = function () {\n\t\tif (!widget.is_open && r.buf)\n\t\t\treturn;\n\n\t\tr.buf = canvas_cfg(ebi('barbuf'));\n\t\tr.pos = canvas_cfg(ebi('barpos'));\n\t\tr.buf.ctx.font = '.5em sans-serif';\n\t\tr.pos.ctx.font = '.9em sans-serif';\n\t\tr.pos.ctx.strokeStyle = 'rgba(24,56,0,0.5)';\n\t\tr.drawbuf();\n\t\tr.drawpos();\n\t\tif (!r.pos.can.onmouseleave)\n\t\t\tmleave();\n\t};\n\n\tr.loadwaves = function (url) {\n\t\tr.wurl = url;\n\t\tvar img = new Image();\n\t\timg.onload = function () {\n\t\t\tif (r.wurl != url)\n\t\t\t\treturn;\n\n\t\t\tr.wimg = img;\n\t\t\tr.onresize();\n\t\t};\n\t\timg.src = url;\n\t};\n\n\tr.unwave = function () {\n\t\tr.wurl = r.wimg = null;\n\t}\n\n\tfunction mmove(e) {\n\t\tvar adur;\n\t\tif (e.buttons || !mp || !mp.au || !isNum(adur = mp.au.duration))\n\t\t\treturn;\n\n\t\tvar rect = r.pos.can.getBoundingClientRect(),\n\t\t\tx = e.clientX - rect.left,\n\t\t\tmul = x * 1.0 / rect.width;\n\n\t\tmousepos = adur * mul;\n\t\tlastmove = Date.now();\n\t\tr.drawpos();\n\t}\n\tfunction menter() {\n\t\tr.pos.can.onmousemove = mmove;\n\t\tr.pos.can.onmouseleave = mleave;\n\t}\n\tfunction mleave() {\n\t\tr.pos.can.onmousemove = null;\n\t\tr.pos.can.onmouseleave = null;\n\t\tr.pos.can.onmouseenter = menter;\n\t\tif (lastmove) {\n\t\t\tlastmove = 0;\n\t\t\tr.drawpos();\n\t\t}\n\t}\n\n\tr.drawbuf = function () {\n\t\tvar bc = r.buf,\n\t\t\tpc = r.pos,\n\t\t\tbctx = bc.ctx,\n\t\t\tapos, adur;\n\n\t\tif (!widget.is_open)\n\t\t\treturn;\n\n\t\tbctx.clearRect(0, 0, bc.w, bc.h);\n\n\t\tif (!mp || !mp.au || !isNum(adur = mp.au.duration) || !isNum(apos = mp.au.currentTime) || apos < 0 || adur < apos)\n\t\t\treturn;  // not-init || unsupp-codec\n\n\t\tbau = mp.au;\n\n\t\tvar sm = bc.w * 1.0 / mp.au.duration,\n\t\t\tgk = bc.h + '/' + themen,\n\t\t\tdz = themen == 'dz',\n\t\t\tdy = themen == 'dy';\n\n\t\tif (gradh != gk) {\n\t\t\tgradh = gk;\n\t\t\tgrad = glossy_grad(bc, dz ? 120 : 85,\n\t\t\t\tdy ? [0, 0, 0, 0] : [35, 40, 37, 35],\n\t\t\t\tdy ? [20, 24, 22, 20] : light ? [45, 56, 50, 45] : [42, 51, 47, 42]);\n\t\t}\n\t\tbctx.fillStyle = grad;\n\t\tfor (var a = 0; a < mp.au.buffered.length; a++) {\n\t\t\tvar x1 = sm * mp.au.buffered.start(a),\n\t\t\t\tx2 = sm * mp.au.buffered.end(a);\n\n\t\t\tbctx.fillRect(x1, 0, x2 - x1, bc.h);\n\t\t}\n\t\tif (r.wimg) {\n\t\t\tbctx.globalAlpha = 0.6;\n\t\t\tbctx.filter = light ? '' : 'invert(1)';\n\t\t\tbctx.drawImage(r.wimg, 0, 0, bc.w, bc.h);\n\t\t\tbctx.filter = 'invert(0)';\n\t\t\tbctx.globalAlpha = 1;\n\t\t}\n\n\t\tvar step = sm > 1 ? 1 : sm > 0.4 ? 3 : sm > 0.05 ? 30 : 720;\n\t\tbctx.fillStyle = light && !dy ? 'rgba(0,64,0,0.15)' : 'rgba(204,255,128,0.15)';\n\t\tfor (var p = step, mins = adur / 10; p <= mins; p += step)\n\t\t\tbctx.fillRect(Math.floor(sm * p * 10), 0, 2, pc.h);\n\n\t\tstep = sm > 0.15 ? 1 : sm > 0.05 ? 10 : 360;\n\t\tbctx.fillStyle = light && !dy ? 'rgba(0,64,0,0.5)' : 'rgba(192,255,96,0.5)';\n\t\tfor (var p = step, mins = adur / 60; p <= mins; p += step)\n\t\t\tbctx.fillRect(Math.floor(sm * p * 60), 0, 2, pc.h);\n\n\t\tstep = sm > 0.33 ? 1 : sm > 0.15 ? 5 : sm > 0.05 ? 10 : sm > 0.01 ? 60 : 720;\n\t\tbctx.fillStyle = dz ? '#0f0' : dy ? '#999' : light ? 'rgba(0,64,0,0.9)' : 'rgba(192,255,96,1)';\n\t\tfor (var p = step, mins = adur / 60; p <= mins; p += step) {\n\t\t\tbctx.fillText(p, Math.floor(sm * p * 60 + 3), pc.h / 3);\n\t\t}\n\n\t\tstep = sm > 0.2 ? 10 : sm > 0.1 ? 30 : sm > 0.01 ? 60 : sm > 0.005 ? 720 : 1440;\n\t\tbctx.fillStyle = light ? 'rgba(0,0,0,1)' : 'rgba(255,255,255,1)';\n\t\tfor (var p = step, mins = adur / 60; p <= mins; p += step)\n\t\t\tbctx.fillRect(Math.floor(sm * p * 60), 0, 2, pc.h);\n\t};\n\n\tr.drawpos = function () {\n\t\tvar bc = r.buf,\n\t\t\tpc = r.pos,\n\t\t\tpctx = pc.ctx,\n\t\t\tw = 8,\n\t\t\tapos, adur;\n\n\t\tif (t_redraw) {\n\t\t\tclearTimeout(t_redraw);\n\t\t\tt_redraw = 0;\n\t\t}\n\t\tpctx.clearRect(0, 0, pc.w, pc.h);\n\n\t\tif (!mp || !mp.au)\n\t\t\treturn;  // not-init\n\n\t\tif (!isNum(adur = mp.au.duration) || !isNum(apos = mp.au.currentTime) || apos < 0 || adur < apos) {\n\t\t\tif (Date.now() - mp.au.pt0 < 500)\n\t\t\t\treturn;\n\n\t\t\tpctx.fillStyle = light ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.5)';\n\t\t\tvar m = /[?&]th=(opus|owa|caf|mp3)/.exec('' + mp.au.rsrc),\n\t\t\t\ttxt = mp.au.ded ? L.mm_playerr.replace(':', ' ;_;') :\n\t\t\t\t\tm ? L.mm_bconv.format(m[1]) : L.mm_bload;\n\n\t\t\tpctx.fillText(txt, 16, pc.h / 1.5);\n\t\t\treturn;  // not-init || unsupp-codec\n\t\t}\n\n\t\tif (bau != mp.au)\n\t\t\tr.drawbuf();\n\n\t\tif (Date.now() - lastmove < 400) {\n\t\t\tapos = mousepos;\n\t\t\tw = 0;\n\t\t}\n\n\t\tvar sm = bc.w * 1.0 / adur,\n\t\t\tt1 = s2ms(adur),\n\t\t\tt2 = s2ms(apos),\n\t\t\tx = sm * apos;\n\n\t\tif (w && html_txt != t2) {\n\t\t\tebi('np_pos').textContent = html_txt = t2;\n\t\t\tif (mpl.os_ctl)\n\t\t\t\tnavigator.mediaSession.setPositionState({\n\t\t\t\t\t'duration': adur,\n\t\t\t\t\t'position': apos,\n\t\t\t\t\t'playbackRate': 1\n\t\t\t\t});\n\t\t}\n\n\t\tif (!widget.is_open)\n\t\t\treturn;\n\n\t\tpctx.fillStyle = '#573'; pctx.fillRect((x - w / 2) - 1, 0, w + 2, pc.h);\n\t\tpctx.fillStyle = '#dfc'; pctx.fillRect((x - w / 2), 0, w, pc.h);\n\n\t\tpctx.lineWidth = 2.5;\n\t\tpctx.fillStyle = '#fff';\n\n\t\tvar m1 = pctx.measureText(t1),\n\t\t\tm1b = pctx.measureText(t1 + \":88\"),\n\t\t\tm2 = pctx.measureText(t2),\n\t\t\tyt = pc.h * 0.94,\n\t\t\txt1 = pc.w - (m1.width + 12),\n\t\t\txt2 = x < m1.width * 1.4 ? (x + 12) : (Math.min(pc.w - m1b.width, x - 12) - m2.width);\n\n\t\tpctx.strokeText(t1, xt1 + 1, yt + 1);\n\t\tpctx.strokeText(t2, xt2 + 1, yt + 1);\n\t\tpctx.strokeText(t1, xt1, yt);\n\t\tpctx.strokeText(t2, xt2, yt);\n\t\tpctx.fillText(t1, xt1, yt);\n\t\tpctx.fillText(t2, xt2, yt);\n\n\t\tif (sm > 10)\n\t\t\tt_redraw = setTimeout(r.drawpos, sm > 50 ? 20 : 50);\n\t};\n\n\tonresize100.add(r.onresize, true);\n\treturn r;\n})();\n\n\n// volume bar\nvar vbar = (function () {\n\tvar r = {},\n\t\tgradh = -1,\n\t\tlastv = -1,\n\t\tuntext = -1,\n\t\tcan, ctx, w, h, grad1, grad2;\n\n\tr.onresize = function () {\n\t\tif (!widget.is_open && r.can)\n\t\t\treturn;\n\n\t\tr.can = canvas_cfg(ebi('pvol'));\n\t\tcan = r.can.can;\n\t\tctx = r.can.ctx;\n\t\tctx.font = '.7em sans-serif';\n\t\tctx.fontVariantCaps = 'small-caps';\n\t\tw = r.can.w;\n\t\th = r.can.h;\n\t\tr.draw();\n\t}\n\n\tr.draw = function () {\n\t\tif (!mp)\n\t\t\treturn;\n\n\t\tvar gh = h + '' + light,\n\t\t\tdz = themen == 'dz',\n\t\t\tdy = themen == 'dy';\n\n\t\tif (gradh != gh) {\n\t\t\tgradh = gh;\n\t\t\tgrad1 = glossy_grad(r.can, dz ? 120 : 50,\n\t\t\t\tdy ? [0, 0, 0, 0] : light ? [50, 55, 52, 48] : [45, 52, 47, 43],\n\t\t\t\tdy ? [20, 24, 22, 20] : light ? [54, 60, 52, 47] : [42, 51, 47, 42]);\n\t\t\tgrad2 = glossy_grad(r.can, dz ? 120 : 205,\n\t\t\t\tdz ? [100, 100, 100, 100] : dy ? [0, 0, 0, 0] : [10, 15, 13, 10],\n\t\t\t\tdz ? [10, 14, 12, 10] : dy ? [90, 90, 90, 90] : [16, 20, 18, 16]);\n\t\t}\n\t\tctx.fillStyle = grad2; ctx.fillRect(0, 0, w, h);\n\t\tctx.fillStyle = grad1; ctx.fillRect(0, 0, w * mp.vol, h);\n\n\t\tvar vt = 'volume ' + Math.floor(mp.vol * 100),\n\t\t\ttw = ctx.measureText(vt).width,\n\t\t\tx = w * mp.vol - tw - 8,\n\t\t\tli = dy;\n\n\t\tif (mp.vol < 0.5) {\n\t\t\tx += tw + 16;\n\t\t\tli = !li;\n\t\t}\n\n\t\tctx.fillStyle = li ? '#fff' : '#210';\n\t\tctx.fillText(vt, x, h / 3 * 2);\n\n\t\tclearTimeout(untext);\n\t\tuntext = setTimeout(r.draw, 1000);\n\t};\n\tonresize100.add(r.onresize, true);\n\n\tvar rect;\n\tfunction mousedown(e) {\n\t\trect = can.getBoundingClientRect();\n\t\tmousemove(e);\n\t}\n\tfunction mousemove(e) {\n\t\tif (e.changedTouches && e.changedTouches.length > 0) {\n\t\t\te = e.changedTouches[0];\n\t\t}\n\t\telse if (e.buttons === 0) {\n\t\t\tcan.onmousemove = null;\n\t\t\treturn;\n\t\t}\n\n\t\tvar x = e.clientX - rect.left,\n\t\t\tmul = x * 1.0 / rect.width;\n\n\t\tif (mul > 0.98)\n\t\t\tmul = 1;\n\n\t\tlastv = Date.now();\n\t\tmp.setvol(mul);\n\t\tr.draw();\n\n\t\tsetTimeout(function () {\n\t\t\tif (IPHONE && mp.au && mul < 0.9 && mp.au.volume == 1)\n\t\t\t\ttoast.inf(6, 'volume doesnt work because <a href=\"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\" target=\"_blank\">apple says no</a>');\n\t\t}, 1);\n\t}\n\tcan.onmousedown = function (e) {\n\t\tif (e.button !== 0)\n\t\t\treturn;\n\n\t\tcan.onmousemove = mousemove;\n\t\tmousedown(e);\n\t};\n\tcan.onmouseup = function (e) {\n\t\tif (e.button === 0)\n\t\t\tcan.onmousemove = null;\n\t};\n\tif (TOUCH) {\n\t\tcan.ontouchstart = mousedown;\n\t\tcan.ontouchmove = mousemove;\n\t}\n\treturn r;\n})();\n\n\nfunction seek_au_mul(mul) {\n\tif (mp.au)\n\t\tseek_au_sec(mp.au.duration * mul);\n}\n\nfunction seek_au_rel(sec) {\n\tif (mp.au)\n\t\tseek_au_sec(mp.au.currentTime + sec);\n}\n\nfunction seek_au_sec(seek) {\n\tif (!mp.au)\n\t\treturn;\n\n\tconsole.log('seek: ' + seek);\n\tif (!isNum(seek))\n\t\treturn;\n\n\tmp.nopause();\n\tmp.au.currentTime = seek;\n\n\tif (mp.au.paused)\n\t\tmp.fade_in();\n\n\tmpui.progress_updater();\n}\n\n\nfunction song_skip(n, dirskip) {\n\tvar tid = mp.au && mp.au.evp == get_evpath() ? mp.au.tid : null,\n\t\tofs = tid ? mp.order.indexOf(tid) : -1;\n\n\tif (dirskip && ofs + 1 && ofs > mp.order.length - 2) {\n\t\ttoast.inf(10, L.mm_nof);\n\t\tconsole.log(\"mm_nof1\");\n\t\tmpl.traversals = 0;\n\t\treturn;\n\t}\n\n\tif (tid && !dirskip)\n\t\tplay(ofs + n);\n\telse\n\t\tplay(mp.order[n == -1 ? mp.order.length - 1 : 0]);\n}\nfunction next_song(e) {\n\tev(e);\n\tif (QS('.dumb_loader_thing')) {\n\t\ttreectl.ls_cb = next_song;\n\t\treturn;\n\t}\n\tif (mp.order.length) {\n\t\tvar dirskip = mpl.traversals;\n\t\tmpl.traversals = 0;\n\t\treturn song_skip(1, dirskip);\n\t}\n\tif (mpl.traversals++ < 5) {\n\t\ttreectl.ls_cb = next_song;\n\t\treturn tree_neigh(1);\n\t}\n\ttoast.inf(10, L.mm_nof);\n\tconsole.log(\"mm_nof2\");\n\tmpl.traversals = 0;\n}\nfunction last_song(e) {\n\tev(e);\n\tif (mp.order.length) {\n\t\tmpl.traversals = 0;\n\t\treturn song_skip(-1, true);\n\t}\n\tif (mpl.traversals++ < 5) {\n\t\ttreectl.ls_cb = last_song;\n\t\treturn tree_neigh(-1);\n\t}\n\ttoast.inf(10, L.mm_nof);\n\tconsole.log(\"mm_nof2\");\n\tmpl.traversals = 0;\n}\nfunction prev_song(e) {\n\tev(e);\n\n\tif (mp.au && !mp.au.paused && mp.au.currentTime > 3)\n\t\treturn seek_au_sec(0);\n\n\tif (QS('.dumb_loader_thing')) {\n\t\ttreectl.ls_cb = function () { song_skip(-1); };\n\t\treturn;\n\t}\n\treturn song_skip(-1);\n}\nfunction dl_song() {\n\tif (!mp || !mp.au) {\n\t\tvar o = QSA('#files a[id]');\n\t\tfor (var a = 0; a < o.length; a++)\n\t\t\to[a].setAttribute('download', '');\n\n\t\treturn toast.inf(10, L.f_dls);\n\t}\n\n\tvar url = addq(mp.au.osrc, 'cache=987&_=' + ACB);\n\tdl_file(url);\n}\nfunction sel_song() {\n\tvar o = QS('#files tr.play');\n\tif (!o)\n\t\treturn;\n\tclmod(o, 'sel', 't');\n\tmsel.origin_tr(o);\n\tmsel.selui();\n}\n\n\nfunction playpause(e) {\n\t// must be event-chain\n\tev(e);\n\tif (mp.au) {\n\t\tif (mp.au.paused)\n\t\t\tmp.fade_in();\n\t\telse\n\t\t\tmp.fade_out();\n\n\t\tmpui.progress_updater();\n\t}\n\telse\n\t\tplay(0, true);\n\n\tmpl.pp();\n};\n\n\nfunction mplay(e) {\n\tif (mp.au && !mp.au.paused)\n\t\treturn;\n\n\tplaypause(e);\n}\n\n\nfunction mpause(e) {\n\tif (mp.cd_pause > Date.now() - 100)\n\t\treturn;\n\n\tif (mp.au && mp.au.paused)\n\t\treturn;\n\n\tplaypause(e);\n}\n\n\n// hook up the widget buttons\n(function () {\n\tebi('bplay').onclick = playpause;\n\tebi('bprev').onclick = prev_song;\n\tebi('bnext').onclick = next_song;\n\n\tvar bar = ebi('barpos');\n\n\tbar.onclick = function (e) {\n\t\tif (!mp.au) {\n\t\t\tplay(0, true);\n\t\t\treturn mp.fade_in();\n\t\t}\n\n\t\tvar rect = pbar.buf.can.getBoundingClientRect(),\n\t\t\tx = e.clientX - rect.left;\n\n\t\tseek_au_mul(x * 1.0 / rect.width);\n\t};\n\n\tif (!TOUCH) {\n\t\tbar.onwheel = function (e) {\n\t\t\tvar dist = Math.sign(e.deltaY) * 10;\n\t\t\tif (Math.abs(e.deltaY) < 30 && !e.deltaMode)\n\t\t\t\tdist = e.deltaY;\n\n\t\t\tif (!dist || !mp.au)\n\t\t\t\treturn true;\n\n\t\t\tseek_au_rel(dist);\n\t\t\tev(e);\n\t\t};\n\t\tebi('pvol').onwheel = function (e) {\n\t\t\tvar dist = Math.sign(e.deltaY) * 10;\n\t\t\tif (Math.abs(e.deltaY) < 30 && !e.deltaMode)\n\t\t\t\tdist = e.deltaY;\n\n\t\t\tif (!dist || !mp.au)\n\t\t\t\treturn true;\n\n\t\t\tdist *= -1;\n\t\t\tmp.setvol(Math.round((mp.vol + dist / 500) * 100) / 100 );\n\t\t\tvbar.draw();\n\t\t\tev(e);\n\t\t};\n\t}\n})();\n\n\n// periodic tasks\nvar mpui = (function () {\n\tvar r = {},\n\t\tnth = 0,\n\t\tpreloaded = null,\n\t\tfpreloaded = null;\n\n\tr.progress_updater = function () {\n\t\t//console.trace();\n\t\ttimer.add(updater_impl, true);\n\t};\n\n\tfunction repreload() {\n\t\tpreloaded = fpreloaded = null;\n\t}\n\n\tfunction updater_impl() {\n\t\tif (!mp.au) {\n\t\t\twidget.paused(true);\n\t\t\ttimer.rm(updater_impl);\n\t\t\treturn;\n\t\t}\n\n\t\tvar paint = !MOBILE || document.hasFocus();\n\n\t\tvar pos = mp.au.currentTime;\n\t\tif (!isNum(pos))\n\t\t\tpos = 0;\n\n\t\t// indicate playback state in ui\n\t\twidget.paused(mp.au.paused);\n\n\t\tif (paint && ++nth > 69) {\n\t\t\t// android-chrome breaks aspect ratio with unannounced viewport changes\n\t\t\tnth = 0;\n\t\t\tif (MOBILE) {\n\t\t\t\tnth = 1;\n\t\t\t\tpbar.onresize();\n\t\t\t\tvbar.onresize();\n\t\t\t}\n\t\t}\n\t\telse if (paint) {\n\t\t\t// draw current position in song\n\t\t\tif (!mp.au.paused)\n\t\t\t\tpbar.drawpos();\n\n\t\t\t// occasionally draw buffered regions\n\t\t\tif (nth % 5 == 0)\n\t\t\t\tpbar.drawbuf();\n\t\t}\n\n\t\t// preload next song\n\t\tif (!mpl.one && mpl.preload && preloaded != mp.au.rsrc) {\n\t\t\tvar len = mp.au.duration,\n\t\t\t\trem = pos > 1 ? len - pos : 999,\n\t\t\t\tfull = null;\n\n\t\t\tif (rem < 7 || (!mpl.fullpre && (rem < 40 || (rem < 90 && pos > 10)))) {\n\t\t\t\tpreloaded = fpreloaded = mp.au.rsrc;\n\t\t\t\tfull = false;\n\t\t\t}\n\t\t\telse if (rem < 60 && mpl.fullpre && fpreloaded != mp.au.rsrc) {\n\t\t\t\tfpreloaded = mp.au.rsrc;\n\t\t\t\tfull = true;\n\t\t\t}\n\n\t\t\tif (full !== null)\n\t\t\t\ttry {\n\t\t\t\t\tvar oi = mp.order.indexOf(mp.au.tid) + 1,\n\t\t\t\t\t\tevp = get_evpath();\n\n\t\t\t\t\tif (oi >= mp.order.length && (\n\t\t\t\t\t\t\tmpl.one ||\n\t\t\t\t\t\t\tmpl.pb_mode != 'next' ||\n\t\t\t\t\t\t\tmp.au.evp != evp ||\n\t\t\t\t\t\t\tebi('unsearch'))\n\t\t\t\t\t\t)\n\t\t\t\t\t\toi = 0;\n\n\t\t\t\t\tif (oi >= mp.order.length) {\n\t\t\t\t\t\tif (!mpl.prescan)\n\t\t\t\t\t\t\tthrow \"prescan disabled\";\n\n\t\t\t\t\t\tif (mpl.prescan_evp == evp)\n\t\t\t\t\t\t\tthrow \"evp match\";\n\n\t\t\t\t\t\tif (treectl.trunc)\n\t\t\t\t\t\t\treturn treectl.showmore(99999, repreload);\n\n\t\t\t\t\t\tif (mpl.traversals++ > 4) {\n\t\t\t\t\t\t\tmpl.prescan_evp = null;\n\t\t\t\t\t\t\ttoast.inf(10, L.mm_nof);\n\t\t\t\t\t\t\tthrow L.mm_nof;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tmpl.prescan_evp = evp;\n\t\t\t\t\t\ttoast.inf(10, L.mm_prescan);\n\t\t\t\t\t\ttreectl.ls_cb = repreload;\n\t\t\t\t\t\ttree_neigh(1);\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t\tmp.preload(mp.tracks[mp.order[oi]], full);\n\t\t\t\t}\n\t\t\t\tcatch (ex) {\n\t\t\t\t\tconsole.log(\"preload failed\", ex);\n\t\t\t\t}\n\t\t}\n\n\t\tif (mp.au.paused)\n\t\t\ttimer.rm(updater_impl);\n\t}\n\treturn r;\n})();\n\n\n// event from play button next to a file in the list\nfunction ev_play(e) {\n\tev(e);\n\n\tvar fade = !mp.au || mp.au.paused;\n\tplay(this.getAttribute('id').slice(1), true);\n\tif (fade)\n\t\tmp.fade_in();\n\n\treturn false;\n}\n\n\nvar actx = null;\n\nfunction start_actx() {\n\t// bonus: speedhack for unfocused file hashing (removes 1sec delay on subtle.digest resolves)\n\tif (!actx) {\n\t\tif (!ACtx)\n\t\t\treturn;\n\n\t\tactx = new ACtx();\n\t\tconsole.log('actx created');\n\t}\n\ttry {\n\t\tif (actx.state == 'suspended') {\n\t\t\tactx.resume();\n\t\t\tsetTimeout(function () {\n\t\t\t\tconsole.log('actx is ' + actx.state);\n\t\t\t}, 500);\n\t\t}\n\t}\n\tcatch (ex) {\n\t\tconsole.log('actx start failed; ' + ex);\n\t}\n}\n\nvar afilt = (function () {\n\tvar r = {\n\t\t\"eqen\": false,\n\t\t\"drcen\": false,\n\t\t\"ssen\": false,\n\t\t\"bands\": [31.25, 62.5, 125, 250, 500, 1000, 2000, 4000, 8000, 16000],\n\t\t\"gains\": [4, 3, 2, 1, 0, 0, 1, 2, 3, 4],\n\t\t\"drcv\": [-24, 30, 12, 0.01, 0.25],\n\t\t\"drch\": ['tresh', 'knee', 'ratio', 'atk', 'rls'],\n\t\t\"drck\": ['threshold', 'knee', 'ratio', 'attack', 'release'],\n\t\t\"sscl\": [L.mt_ssvt, L.mt_ssts, L.mt_sste, L.mt_sssm],\n\t\t\"sscv\": [1, 5, 5, 5.0],\n\t\t\"drcn\": null,\n\t\t\"filters\": [],\n\t\t\"filterskip\": [],\n\t\t\"plugs\": [],\n\t\t\"amp\": 0,\n\t\t\"chw\": 1,\n\t\t\"last_au\": null,\n\t\t\"acst\": {}\n\t};\n\n\tfunction setvis(vis) {\n\t\tebi('audio_eq').parentNode.style.display =\n\t\tebi('audio_drc').parentNode.style.display =\n\t\tebi('audio_ss').parentNode.style.display =\n\t\t(vis ? '' : 'none');\n\t}\n\n\tsetvis(ACtx);\n\n\tr.init = function () {\n\t\tstart_actx();\n\t\tif (r.cfg)\n\t\t\treturn;\n\n\t\tsetvis(actx);\n\n\t\t// some browsers have insane high-frequency boost\n\t\t// (or rather the actual problem is Q but close enough)\n\t\tr.cali = (function () {\n\t\t\ttry {\n\t\t\t\tvar fi = actx.createBiquadFilter(),\n\t\t\t\t\tfreqs = new Float32Array(1),\n\t\t\t\t\tmag = new Float32Array(1),\n\t\t\t\t\tphase = new Float32Array(1);\n\n\t\t\t\tfreqs[0] = 14000;\n\t\t\t\tfi.type = 'peaking';\n\t\t\t\tfi.frequency.value = 18000;\n\t\t\t\tfi.Q.value = 0.8;\n\t\t\t\tfi.gain.value = 1;\n\t\t\t\tfi.getFrequencyResponse(freqs, mag, phase);\n\n\t\t\t\treturn mag[0];  // 1.0407 good, 1.0563 bad\n\t\t\t}\n\t\t\tcatch (ex) {\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t})();\n\t\tconsole.log('eq cali: ' + r.cali);\n\n\t\tvar e1 = r.cali < 1.05;\n\n\t\tr.cfg = [ // hz, q, g\n\t\t\t[31.25 * 0.88, 0, 1.4],  // shelf\n\t\t\t[31.25 * 1.04, 0.7, 0.96],  // peak\n\t\t\t[62.5, 0.7, 1],\n\t\t\t[125, 0.8, 1],\n\t\t\t[250, 0.9, 1.03],\n\t\t\t[500, 0.9, 1.1],\n\t\t\t[1000, 0.9, 1.1],\n\t\t\t[2000, 0.9, 1.105],\n\t\t\t[4000, 0.88, 1.05],\n\t\t\t[8000 * 1.006, 0.73, e1 ? 1.24 : 1.2],\n\t\t\t[16000 * 0.89, 0.7, e1 ? 1.26 : 1.2],  // peak\n\t\t\t[16000 * 1.13, 0.82, e1 ? 1.09 : 0.75],  // peak\n\t\t\t[16000 * 1.205, 0, e1 ? 1.9 : 1.85]  // shelf\n\t\t];\n\t};\n\n\ttry {\n\t\tr.amp = fcfg_get('au_eq_amp', r.amp);\n\t\tr.chw = fcfg_get('au_eq_chw', r.chw);\n\t\tvar gains = jread('au_eq_gain', r.gains);\n\t\tif (r.gains.length == gains.length)\n\t\t\tr.gains = gains;\n\n\t\tr.drcv = jread('au_drcv', r.drcv);\n\t\tr.sscv = jread('au_sscv', r.sscv);\n\t\tmpss.load();\n\t}\n\tcatch (ex) { }\n\n\tr.draw = function () {\n\t\tjwrite('au_eq_gain', r.gains);\n\t\tswrite('au_eq_amp', r.amp);\n\t\tswrite('au_eq_chw', r.chw);\n\n\t\tvar txt = QSA('input.eq_gain');\n\t\tfor (var a = 0; a < r.bands.length; a++)\n\t\t\ttxt[a].value = r.gains[a];\n\n\t\tQS('input.eq_gain[band=\"amp\"]').value = r.amp;\n\t\tQS('input.eq_gain[band=\"chw\"]').value = r.chw;\n\t};\n\n\tr.stop = function () {\n\t\tmpss.stop();\n\t\tif (r.filters.length)\n\t\t\tfor (var a = 0; a < r.filters.length; a++)\n\t\t\t\tr.filters[a].disconnect();\n\n\t\tr.filters = [];\n\t\tr.filterskip = [];\n\n\t\tfor (var a = 0; a < r.plugs.length; a++)\n\t\t\tr.plugs[a].unload();\n\n\t\tif (!mp)\n\t\t\treturn;\n\n\t\tif (mp.acs)\n\t\t\tmp.acs.disconnect();\n\n\t\tmp.acs = mpo.acs = null;\n\t};\n\n\tr.apply = function (v, au) {\n\t\tr.init();\n\t\tr.draw();\n\n\t\tif (!actx) {\n\t\t\tbcfg_set('au_eq', r.eqen = false);\n\t\t\tbcfg_set('au_drc', r.drcen = false);\n\t\t}\n\t\telse if (v === true && (r.drcen || r.ssen) && !r.eqen)\n\t\t\tbcfg_set('au_eq', r.eqen = true);\n\t\telse if (v === false && !r.eqen) {\n\t\t\tbcfg_set('au_drc', r.drcen = false);\n\t\t\tbcfg_set('au_ss', r.ssen = false);\n\t\t}\n\n\t\tr.drcn = null;\n\n\t\tvar plug = false;\n\t\tfor (var a = 0; a < r.plugs.length; a++)\n\t\t\tif (r.plugs[a].en)\n\t\t\t\tplug = true;\n\n\t\tau = au || (mp && mp.au);\n\t\tif (!actx || !au || (!r.eqen && !plug && !mp.acs))\n\t\t\treturn;\n\n\t\tr.stop();\n\t\tau.id = au.id || Date.now();\n\t\tmp.acs = r.acst[au.id] = r.acst[au.id] || actx.createMediaElementSource(au);\n\n\t\tif (r.ssen)\n\t\t\tadd_ss();\n\n\t\tif (r.eqen)\n\t\t\tadd_eq();\n\n\t\tfor (var a = 0; a < r.plugs.length; a++)\n\t\t\tif (r.plugs[a].en)\n\t\t\t\tr.plugs[a].load();\n\n\t\tfor (var a = 0; a < r.filters.length; a++)\n\t\t\tif (!has(r.filterskip, a))\n\t\t\t\tr.filters[a].connect(a ? r.filters[a - 1] : actx.destination);\n\n\t\tmp.acs.connect(r.filters.length ?\n\t\t\tr.filters[r.filters.length - 1] : actx.destination);\n\n\t\tif (!au.paused)\n\t\t\tmpss.go();\n\t}\n\n\tfunction add_ss() {\n\t\tr.filters.push(afilt.ssg = actx.createGain());\n\t\tr.filters.push(afilt.ssa = actx.createAnalyser());\n\t\tafilt.ssa.fftSize = 256;\n\t}\n\n\tfunction add_eq() {\n\t\tvar min, max;\n\t\tmin = max = r.gains[0];\n\t\tfor (var a = 1; a < r.gains.length; a++) {\n\t\t\tmin = Math.min(min, r.gains[a]);\n\t\t\tmax = Math.max(max, r.gains[a]);\n\t\t}\n\n\t\tvar gains = [];\n\t\tfor (var a = 0; a < r.gains.length; a++)\n\t\t\tgains.push(r.gains[a] - max);\n\n\t\tvar t = gains[gains.length - 1];\n\t\tgains.push(t);\n\t\tgains.push(t);\n\t\tgains.unshift(gains[0]);\n\n\t\tfor (var a = 0; a < r.cfg.length && min != max; a++) {\n\t\t\tvar fi = actx.createBiquadFilter(), c = r.cfg[a];\n\t\t\tfi.frequency.value = c[0];\n\t\t\tfi.gain.value = c[2] * gains[a];\n\t\t\tfi.Q.value = c[1];\n\t\t\tfi.type = a == 0 ? 'lowshelf' : a == r.cfg.length - 1 ? 'highshelf' : 'peaking';\n\t\t\tr.filters.push(fi);\n\t\t}\n\n\t\t// pregain, keep first in chain\n\t\tfi = actx.createGain();\n\t\tfi.gain.value = r.amp + 0.94;  // +.137 dB measured; now -.25 dB and almost bitperfect\n\t\tr.filters.push(fi);\n\n\t\t// wait nevermind, drc goes first\n\t\ttimer.rm(showdrc);\n\t\tif (r.drcen) {\n\t\t\tfi = r.drcn = actx.createDynamicsCompressor();\n\t\t\tfor (var a = 0; a < r.drcv.length; a++)\n\t\t\t\tfi[r.drck[a]].value = r.drcv[a];\n\n\t\t\tif (r.drcv[3] < 0.02) {\n\t\t\t\t// avoid static at decode start\n\t\t\t\tfi.attack.value = 0.02;\n\t\t\t\tsetTimeout(function () {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tfi.attack.value = r.drcv[3];\n\t\t\t\t\t}\n\t\t\t\t\tcatch (ex) { }\n\t\t\t\t}, 200);\n\t\t\t}\n\n\t\t\tr.filters.push(fi);\n\t\t\ttimer.add(showdrc);\n\t\t}\n\n\t\tif (Math.round(r.chw * 25) != 25) {\n\t\t\tvar split = actx.createChannelSplitter(2),\n\t\t\t\tmerge = actx.createChannelMerger(2),\n\t\t\t\tlg1 = actx.createGain(),\n\t\t\t\tlg2 = actx.createGain(),\n\t\t\t\trg1 = actx.createGain(),\n\t\t\t\trg2 = actx.createGain(),\n\t\t\t\tvg1 = 1 - (1 - r.chw) / 2,\n\t\t\t\tvg2 = 1 - vg1;\n\n\t\t\tconsole.log('chw', vg1, vg2);\n\n\t\t\tmerge.connect(r.filters[r.filters.length - 1]);\n\t\t\tlg1.gain.value = rg2.gain.value = vg1;\n\t\t\tlg2.gain.value = rg1.gain.value = vg2;\n\t\t\tlg1.connect(merge, 0, 0);\n\t\t\trg1.connect(merge, 0, 0);\n\t\t\tlg2.connect(merge, 0, 1);\n\t\t\trg2.connect(merge, 0, 1);\n\n\t\t\tsplit.connect(lg1, 0);\n\t\t\tsplit.connect(lg2, 0);\n\t\t\tsplit.connect(rg1, 1);\n\t\t\tsplit.connect(rg2, 1);\n\t\t\tr.filterskip.push(r.filters.length);\n\t\t\tr.filters.push(split);\n\t\t\tmp.acs.channelCountMode = 'explicit';\n\t\t}\n\t}\n\n\tfunction eq_step(e) {\n\t\tev(e);\n\t\tvar sb = this.getAttribute('band'),\n\t\t\tband = parseInt(sb),\n\t\t\tstep = parseFloat(this.getAttribute('step'));\n\n\t\tif (sb == 'amp')\n\t\t\tr.amp = Math.round((r.amp + step * 0.2) * 100) / 100;\n\t\telse if (sb == 'chw')\n\t\t\tr.chw = Math.round((r.chw + step * 0.2) * 100) / 100;\n\t\telse\n\t\t\tr.gains[band] += step;\n\n\t\tr.apply();\n\t}\n\n\tfunction adj_band(that, step) {\n\t\tvar err = false;\n\t\ttry {\n\t\t\tvar sb = that.getAttribute('band'),\n\t\t\t\tband = parseInt(sb),\n\t\t\t\tvs = that.value,\n\t\t\t\tv = parseFloat(vs);\n\n\t\t\tif (!isNum(v) || v + '' != vs)\n\t\t\t\tthrow new Error('inval band');\n\n\t\t\tif (sb == 'amp')\n\t\t\t\tr.amp = Math.round((v + step * 0.2) * 100) / 100;\n\t\t\telse if (sb == 'chw')\n\t\t\t\tr.chw = Math.round((v + step * 0.2) * 100) / 100;\n\t\t\telse\n\t\t\t\tr.gains[band] = v + step;\n\n\t\t\tr.apply();\n\t\t}\n\t\tcatch (ex) {\n\t\t\terr = true;\n\t\t}\n\t\tclmod(that, 'err', err);\n\t}\n\n\tfunction adj_drc() {\n\t\tvar err = false;\n\t\ttry {\n\t\t\tvar n = this.getAttribute('k'),\n\t\t\t\tov = r.drcv[n],\n\t\t\t\tvs = this.value,\n\t\t\t\tv = parseFloat(vs);\n\n\t\t\tif (!isNum(v) || v + '' != vs)\n\t\t\t\tthrow new Error('inval v');\n\n\t\t\tif (v == ov)\n\t\t\t\treturn;\n\n\t\t\tr.drcv[n] = v;\n\t\t\tjwrite('au_drcv', r.drcv);\n\t\t\tif (r.drcn)\n\t\t\t\tr.drcn[r.drck[n]].value = v;\n\t\t}\n\t\tcatch (ex) {\n\t\t\terr = true;\n\t\t}\n\t\tclmod(this, 'err', err);\n\t}\n\n\tfunction adj_ss() {\n\t\tvar err = false;\n\t\ttry {\n\t\t\tvar n = this.getAttribute('k'),\n\t\t\tov = r.sscv[n],\n\t\t\tvs = this.value,\n\t\t\tv = parseFloat(vs);\n\t\t\tif (!isNum(v) || v + '' != vs)\n\t\t\t\tthrow new Error('inval v');\n\n\t\t\tif (v == ov)\n\t\t\t\treturn;\n\n\t\t\tr.sscv[n] = v;\n\t\t\tjwrite('au_sscv', r.sscv);\n\t\t\tmpss.load();\n\t\t}\n\t\tcatch (ex) {\n\t\t\terr = true;\n\t\t}\n\t\tclmod(this, 'err', err);\n\t}\n\n\tfunction eq_mod(e) {\n\t\tev(e);\n\t\tadj_band(this, 0);\n\t}\n\n\tfunction eq_keydown(e) {\n\t\tvar step = e.key == 'ArrowUp' ? 0.25 : e.key == 'ArrowDown' ? -0.25 : 0;\n\t\tif (step != 0)\n\t\t\tadj_band(this, step);\n\t}\n\n\tfunction showdrc() {\n\t\tif (!r.drcn)\n\t\t\treturn timer.rm(showdrc);\n\n\t\tebi('h_drc').textContent = f2f(r.drcn.reduction, 1);\n\t}\n\n\tvar html = ['<table><tr><td rowspan=\"4\">',\n\t\t'<a id=\"au_eq\" class=\"tgl btn\" href=\"#\" tt=\"' + L.mt_eq + '\">' + L.enable + '</a></td>'],\n\t\th2 = [], h3 = [], h4 = [];\n\n\tvar vs = [];\n\tfor (var a = 0; a < r.bands.length; a++) {\n\t\tvar hz = r.bands[a];\n\t\tif (hz >= 1000)\n\t\t\thz = (hz / 1000) + 'k';\n\n\t\thz = (hz + '').split('.')[0];\n\t\tvs.push([a, hz, r.gains[a]]);\n\t}\n\tvs.push([\"amp\", \"boost\", r.amp]);\n\tvs.push([\"chw\", \"width\", r.chw]);\n\n\tfor (var a = 0; a < vs.length; a++) {\n\t\tvar b = vs[a][0];\n\t\thtml.push('<td><a href=\"#\" class=\"eq_step\" step=\"0.5\" band=\"' + b + '\">+</a></td>');\n\t\th2.push('<td>' + vs[a][1] + '</td>');\n\t\th4.push('<td><a href=\"#\" class=\"eq_step\" step=\"-0.5\" band=\"' + b + '\">&ndash;</a></td>');\n\t\th3.push('<td><input type=\"text\" class=\"eq_gain\" ' + NOAC + ' band=\"' + b + '\" value=\"' + vs[a][2] + '\" /></td>');\n\t}\n\thtml = html.join('\\n') + '</tr><tr>';\n\thtml += h2.join('\\n') + '</tr><tr>';\n\thtml += h3.join('\\n') + '</tr><tr>';\n\thtml += h4.join('\\n') + '</tr><table>';\n\tebi('audio_eq').innerHTML = html;\n\n\th2 = [];\n\thtml = ['<table><tr><td rowspan=\"2\">',\n\t\t'<a id=\"au_drc\" class=\"tgl btn\" href=\"#\" tt=\"' + L.mt_drc + '\">' + L.enable + '</a></td>'];\n\n\tfor (var a = 0; a < r.drch.length; a++) {\n\t\thtml.push('<td>' + r.drch[a] + '</td>');\n\t\th2.push('<td><input type=\"text\" class=\"drc_v\" ' + NOAC + ' k=\"' + a + '\" value=\"' + r.drcv[a] + '\" /></td>');\n\t}\n\thtml = html.join('\\n') + '</tr><tr>';\n\thtml += h2.join('\\n') + '</tr><table>';\n\tebi('audio_drc').innerHTML = html;\n\n\th2 = [];\n\thtml = ['<table><tr><td rowspan=\"2\">',\n\t\t'<a id=\"au_ss\" class=\"tgl btn\" href=\"#\" tt=\"' + L.mt_ss + '\">' + L.enable + '</a></td>'];\n\n\tfor (var a = 0; a < r.sscl.length; a++) {\n\t\thtml.push('<td tt=\"' + r.sscl[a] + '</td>');\n\t\th2.push('<td><input type=\"text\" class=\"ssconf_v\" ' + NOAC + ' k=\"' + a + '\" value=\"' + r.sscv[a] + '\" /></td>');\n\t}\n\n\thtml = html.join('\\n') + '</tr><tr>';\n\thtml += h2.join('\\n') + '</tr><table>';\n\tebi('audio_ss').innerHTML = html;\n\n\tvar stp = QSA('a.eq_step');\n\tfor (var a = 0, aa = stp.length; a < aa; a++)\n\t\tstp[a].onclick = eq_step;\n\n\tvar txt = QSA('input.eq_gain');\n\tfor (var a = 0; a < txt.length; a++) {\n\t\ttxt[a].oninput = eq_mod;\n\t\ttxt[a].onkeydown = eq_keydown;\n\t}\n\ttxt = QSA('input.drc_v');\n\tfor (var a = 0; a < txt.length; a++)\n\t\ttxt[a].oninput = txt[a].onkeydown = adj_drc;\n\n\ttxt = QSA('input.ssconf_v');\n\tfor (var a = 0; a < txt.length; a++)\n\t\ttxt[a].oninput = txt[a].onkeydown = adj_ss;\n\n\tbcfg_bind(r, 'eqen', 'au_eq', false, r.apply);\n\tbcfg_bind(r, 'drcen', 'au_drc', false, r.apply);\n\tbcfg_bind(r, 'ssen', 'au_ss', false, r.apply);\n\n\tr.draw();\n\treturn r;\n})();\n\n\n// plays the tid'th audio file on the page\nfunction play(tid, is_ev, seek) {\n\tclearTimeout(mpl.t_eplay);\n\tif (mp.order.length == 0)\n\t\treturn console.log('no audio found wait what');\n\n\tif (crashed)\n\t\treturn;\n\n\tmpl.preload_url = null;\n\tmp.nopause();\n\tmp.stopfade(true);\n\n\tvar tn = tid;\n\tif ((tn + '').indexOf('f-') === 0) {\n\t\ttn = mp.order.indexOf(tn);\n\t\tif (tn < 0)\n\t\t\treturn toast.warn(10, L.mm_hnf);\n\t}\n\n\tif (tn >= mp.order.length) {\n\t\tif (treectl.trunc)\n\t\t\treturn treectl.showmore(99999, next_song);\n\n\t\tif (mpl.pb_mode == 'loop' || ebi('unsearch')) {\n\t\t\ttn = 0;\n\t\t}\n\t\telse if (mpl.pb_mode == 'next') {\n\t\t\ttreectl.ls_cb = next_song;\n\t\t\treturn tree_neigh(1);\n\t\t}\n\t\telse return;\n\t}\n\n\tif (tn < 0) {\n\t\tif (mpl.pb_mode == 'loop') {\n\t\t\ttn = mp.order.length - 1;\n\t\t}\n\t\telse if (mpl.pb_mode == 'next') {\n\t\t\ttreectl.ls_cb = last_song;\n\t\t\treturn tree_neigh(-1);\n\t\t}\n\t\telse return;\n\t}\n\n\ttid = mp.order[tn];\n\n\tif (mp.au) {\n\t\tmp.au.pause();\n\t\tvar el = ebi('a' + mp.au.tid);\n\t\tif (el)\n\t\t\tclmod(el, 'act');\n\t}\n\telse {\n\t\tmp.au = new Audio();\n\t\tmp.au2 = new Audio();\n\t\tmp.set_ev();\n\t\twidget.open();\n\t}\n\tmp.init_fau();\n\n\tvar url = addq(mpl.acode(mp.tracks[tid]), 'cache=987&_=' + ACB);\n\n\tif (mp.au.rsrc == url)\n\t\tmp.au.currentTime = 0;\n\telse if (mp.au2.rsrc == url) {\n\t\tvar t = mp.au;\n\t\tmp.au = mp.au2;\n\t\tmp.au2 = t;\n\t\tt.onerror = t.onprogress = t.onended = t.loop = null;\n\t\tt.ld = 0; //owa\n\t\tmp.set_ev();\n\t\tt = mp.au.currentTime;\n\t\tif (isNum(t) && t > 0.1)\n\t\t\tmp.au.currentTime = 0;\n\t}\n\telse {\n\t\tconsole.log('get ' + url.split('/').pop());\n\t\tmp.au.src = mp.au.rsrc = url;\n\t}\n\n\tmp.au.osrc = mp.tracks[tid];\n\tafilt.apply();\n\n\tsetTimeout(function () {\n\t\tmpl.unbuffer(url);\n\t}, 500);\n\n\tmp.au.ded = 0;\n\tmp.au.tid = tid;\n\tmp.au.pt0 = Date.now();\n\tmp.au.evp = get_evpath();\n\tmp.au.volume = mp.expvol(mp.vol);\n\tvar trs = QSA('#files tr.play');\n\tfor (var a = 0, aa = trs.length; a < aa; a++)\n\t\tclmod(trs[a], 'play');\n\n\tvar oid = 'a' + tid,\n\t\tt_a = ebi(oid),\n\t\tt_tr = t_a.closest('tr');\n\n\tclmod(t_a, 'act', 1);\n\tclmod(t_tr, 'play', 1);\n\tclmod(ebi('wtoggle'), 'np', mpl.clip);\n\tclmod(ebi('wtoggle'), 'm3u', mpl.m3uen);\n\tif (thegrid)\n\t\tthegrid.loadsel();\n\n\tif (mpl.follow)\n\t\tscroll2playing();\n\n\ttry {\n\t\tmp.nopause();\n\t\tmp.au.loop = mpl.loop && !mpl.one;\n\t\tif (mpl.aplay || is_ev !== -1)\n\t\t\tmp.au.play();\n\n\t\tif (mp.au.paused)\n\t\t\tautoplay_blocked(seek);\n\t\telse if (seek) {\n\t\t\tseek_au_sec(seek);\n\t\t}\n\n\t\tif (!seek && !ebi('unsearch')) {\n\t\t\tt_a.setAttribute('id', 'thx_js');\n\t\t\tif (mpl.aplay)\n\t\t\t\tsethash(oid + getsort());\n\t\t\tt_a.setAttribute('id', oid);\n\t\t}\n\t\tmpl.np = ft2dict(t_tr, { 'up_ip': 1 });\n\n\t\tpbar.unwave();\n\t\tif (mpl.waves)\n\t\t\tpbar.loadwaves(url.replace(/\\bth=(opus|mp3)&/, '') + '&th=p');\n\n\t\tmpss.go();\n\t\tmpui.progress_updater();\n\t\tpbar.onresize();\n\t\tvbar.onresize();\n\t\tmpl.announce();\n\t\treturn true;\n\t}\n\tcatch (ex) {\n\t\ttoast.err(0, esc(L.mm_playerr + basenames(ex)));\n\t}\n\tclmod(t_a, 'act');\n\tmpl.t_eplay = setTimeout(next_song, 5000);\n}\n\n\nfunction scroll2playing() {\n\ttry {\n\t\tQS((!thegrid || !thegrid.en) ?\n\t\t\t'tr.play' : '#ggrid a.play').scrollIntoView();\n\t}\n\tcatch (ex) { }\n}\n\n\nfunction evau_end(e) {\n\tif (mpl.one)\n\t\treturn;\n\tif (!mpl.loop)\n\t\treturn next_song(e);\n\tev(e);\n\tmp.au.currentTime = 0;\n\tmp.au.play();\n}\n\n\n// event from the audio object if something breaks\nfunction evau_error(e) {\n\tvar err = '',\n\t\teplaya = (e && e.target) || (window.event && window.event.srcElement);\n\n\teplaya.ded = 1;\n\n\tswitch (eplaya.error.code) {\n\t\tcase eplaya.error.MEDIA_ERR_ABORTED:\n\t\t\terr = L.mm_eabrt;\n\t\t\tbreak;\n\t\tcase eplaya.error.MEDIA_ERR_NETWORK:\n\t\t\tif (IPHONE && eplaya.ld === 1 && mpl.ac2 == 'owa' && !eplaya.paused && !eplaya.currentTime) {\n\t\t\t\teplaya.ded = 0;\n\t\t\t\tif (!mpl.owaw) {\n\t\t\t\t\tmpl.owaw = 1;\n\t\t\t\t\tconsole.log('ignored iOS bug; spurious error sent in parallel with preloaded songs starting to play just fine');\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\terr = L.mm_enet;\n\t\t\tbreak;\n\t\tcase eplaya.error.MEDIA_ERR_DECODE:\n\t\t\terr = L.mm_edec;\n\t\t\tbreak;\n\t\tcase eplaya.error.MEDIA_ERR_SRC_NOT_SUPPORTED:\n\t\t\terr = L.mm_esupp;\n\t\t\tif (/\\.(aac|m4[abr])(\\?|$)/i.exec(eplaya.rsrc) && !mpl.ac_aac) {\n\t\t\t\ttry {\n\t\t\t\t\tebi('ac_aac').click();\n\t\t\t\t\tQS('a.play.act').click();\n\t\t\t\t\ttoast.warn(10, L.mm_opusen);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcatch (ex) { }\n\t\t\t}\n\t\t\tbreak;\n\t\tdefault:\n\t\t\terr = L.mm_eunk;\n\t\t\tbreak;\n\t}\n\tvar em = '' + eplaya.error.message,\n\t\tmfile = '\\n\\nFile: «' + uricom_dec(eplaya.src.split('/').pop()) + '»',\n\t\te500 = L.mm_e500,\n\t\te415 = L.mm_e415,\n\t\te404 = L.mm_e404,\n\t\te403 = L.mm_e403;\n\n\tif (em)\n\t\terr += '\\n\\n' + em;\n\n\tif (em.startsWith('403: '))\n\t\terr = e403;\n\n\tif (em.startsWith('404: '))\n\t\terr = e404;\n\n\tif (em.startsWith('415: '))\n\t\terr = e415;\n\n\tif (em.startsWith('500: '))\n\t\terr = e500;\n\n\ttoast.warn(15, esc(basenames(err + mfile)));\n\tconsole.log(basenames(err + mfile));\n\n\tif (em.startsWith('MEDIA_ELEMENT_ERROR:')) {\n\t\t// chromish for 40x\n\t\tvar xhr = new XHR();\n\t\txhr.open('HEAD', eplaya.src, true);\n\t\txhr.onload = xhr.onerror = function () {\n\t\t\tif (this.status < 400)\n\t\t\t\treturn;\n\n\t\t\terr = this.status == 403 ? e403 :\n\t\t\t\tthis.status == 404 ? e404 :\n\t\t\t\tthis.status == 415 ? e415 :\n\t\t\t\tthis.status == 500 ? e500 :\n\t\t\t\tL.mm_e5xx + this.status;\n\n\t\t\ttoast.warn(15, esc(basenames(err + mfile)));\n\t\t};\n\t\txhr.send();\n\t\treturn;\n\t}\n\n\tmpl.t_eplay = setTimeout(next_song, 15000);\n}\n\n\n// show ui to manually start playback of a linked song\nfunction autoplay_blocked(seek) {\n\tvar tid = mp.au.tid,\n\t\tfn = mp.tracks[tid].split(/\\//).pop();\n\n\tfn = uricom_dec(fn.replace(/\\+/g, ' ').split('?')[0]);\n\n\tmodal.confirm('<h6>' + L.mm_hashplay + '</h6>\\n«' + esc(fn) + '»', function () {\n\t\t// chrome 91 may permanently taint on a failed play()\n\t\t// depending on win10 settings or something? idk\n\t\tmp.au = mpo.au = null;\n\n\t\tplay(tid, true, seek);\n\t\tmp.fade_in();\n\t}, function () {\n\t\tsethash('');\n\t\tclmod(QS('#files tr.play'), 'play');\n\t\treturn reload_mp();\n\t});\n}\n\n\nfunction scan_hash(v) {\n\tif (!v)\n\t\treturn null;\n\n\tvar m = /^#([ag])(f-[0-9a-f]{8,16})(&.+)?/.exec(v + '');\n\tif (!m)\n\t\treturn null;\n\n\tvar mtype = m[1],\n\t\tid = m[2],\n\t\tts = null;\n\n\tif (m.length > 3) {\n\t\tvar tm = /^&[Tt=0]*([0-9]+[Mm:])?0*([0-9\\.]+)[Ss]?$/.exec(m[3]);\n\t\tif (tm) {\n\t\t\tts = parseInt(tm[1] || 0) * 60 + parseFloat(tm[2] || 0);\n\t\t}\n\t\ttm = /^&[Tt=0]*([0-9\\.]+)-([0-9\\.]+)$/.exec(m[3]);\n\t\tif (tm) {\n\t\t\tts = '' + tm[1] + '-' + tm[2];\n\t\t}\n\t}\n\n\treturn [mtype, id, ts];\n}\n\n\nfunction eval_hash() {\n\tif (!window.hotkeys_attached) {\n\t\twindow.hotkeys_attached = true;\n\t\tdocument.onkeydown = ahotkeys;\n\t\twindow.onpopstate = treectl.onpopfun;\n\t}\n\n\tif (hash0 && window.og_fn) {\n\t\tvar all = msel.getall(), mi;\n\t\tfor (var a = 0; a < all.length; a++)\n\t\t\tif (og_fn == uricom_dec(vsplit(all[a].vp)[1].split('?')[0])) {\n\t\t\t\tmi = all[a];\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\tvar ch = !mi ? '' :\n\t\t\timg_re.exec(og_fn) ? 'g' :\n\t\t\tebi('a' + mi.id) ? 'a' :\n\t\t\t'';\n\n\t\thash0 = ch ? ('#' + ch + mi.id) : '';\n\t}\n\n\tvar v = hash0;\n\thash0 = null;\n\tif (!v)\n\t\treturn;\n\n\tvar media = scan_hash(v);\n\tif (media) {\n\t\tvar mtype = media[0],\n\t\t\tid = media[1],\n\t\t\tts = media[2];\n\n\t\tif (mtype == 'a') {\n\t\t\tif (!ts)\n\t\t\t\treturn play(id, -1);\n\n\t\t\treturn play(id, -1, ts);\n\t\t}\n\n\t\tif (mtype == 'g') {\n\t\t\tif (!thegrid.en)\n\t\t\t\tebi('griden').click();\n\n\t\t\tvar t = setInterval(function () {\n\t\t\t\tif (!thegrid.bbox)\n\t\t\t\t\treturn;\n\n\t\t\t\tclearInterval(t);\n\t\t\t\tbaguetteBox.urltime(ts);\n\t\t\t\tvar im = QS('#ggrid a[ref=\"' + id + '\"]');\n\t\t\t\tif (!im)\n\t\t\t\t\treturn toast.warn(10, L.im_hnf);\n\n\t\t\t\tif (thegrid.sel)\n\t\t\t\t\tsetTimeout(function () {\n\t\t\t\t\t\tthegrid.sel = true;\n\t\t\t\t\t}, 1);\n\n\t\t\t\tthegrid.sel = false;\n\t\t\t\tim.click();\n\t\t\t\tim.scrollIntoView();\n\t\t\t}, 50);\n\t\t}\n\t}\n\n\tif (v.startsWith('#q=')) {\n\t\tgoto('search');\n\t\tvar i = ebi('q_raw');\n\t\ti.value = uricom_dec(v.slice(3));\n\t\treturn i.onkeydown({ 'key': 'Enter' });\n\t}\n\n\tif (v.startsWith('#v=')) {\n\t\tgoto(v.slice(3));\n\t\treturn;\n\t}\n\n\tif (v.startsWith(\"#m3u=\")) {\n\t\tload_m3u(v.slice(5));\n\t\treturn;\n\t}\n}\n\n\n(function () {\n\tvar props = {};\n\n\t// a11y jump-to-content\n\tfor (var a = 0; a < 2; a++)\n\t\t(function (a) {\n\t\t\tvar d = mknod('a');\n\t\t\td.setAttribute('href', '#');\n\t\t\td.setAttribute('class', 'ayjump');\n\t\t\td.innerHTML = a ? L.ay_path : L.ay_files;\n\t\t\tdocument.body.insertBefore(d, ebi('ops'));\n\t\t\td.onclick = function (e) {\n\t\t\t\tev(e);\n\t\t\t\tif (a)\n\t\t\t\t\td = QS(treectl.hidden ? '#path a:nth-last-child(2)' : '#treeul a.hl');\n\t\t\t\telse\n\t\t\t\t\td = QS(thegrid.en ? '#ggrid a' : '#files tbody tr[tabindex]');\n\t\t\t\tif (d)\n\t\t\t\t\td.focus();\n\t\t\t};\n\t\t})(a);\n\n\t// account-info label\n\tvar d = mknod('div', 'acc_info');\n\tdocument.body.insertBefore(d, ebi('ops'));\n\n\t// folder nav\n\tebi('goh').parentElement.appendChild(mknod('span', null,\n\t\t'<a href=\"#\" id=\"gop\" tt=\"' + L.gop + '</a>/<a href=\"#\" id=\"gou\" tt=\"' + L.gou + '</a>/<a href=\"#\" id=\"gon\" tt=\"' + L.gon + '</a>'));\n\tebi('gop').onclick = function () { tree_neigh(-1); }\n\tebi('gon').onclick = function () { tree_neigh(1); }\n\tebi('gou').onclick = function () { tree_up(true); }\n\n\t// show/hide scrollbars\n\tfunction setsb() {\n\t\tclmod(document.documentElement, 'noscroll', !props.sbars);\n\t}\n\tbcfg_bind(props, 'sbars', 'sbars', true, setsb);\n\tsetsb();\n\n\t// compact media player\n\tfunction setacmp() {\n\t\tclmod(ebi('widget'), 'cmp', props.mcmp);\n\t\tpbar.onresize();\n\t\tvbar.onresize();\n\t}\n\tbcfg_bind(props, 'mcmp', 'au_compact', false, setacmp);\n\tsetacmp();\n\n\t// toggle bup checksums\n\tebi('uput').onchange = function() {\n\t\tQS('#op_bup input[name=\"act\"]').value = this.checked ? 'uput' : 'bput';\n\t};\n})();\n\n\nfunction read_dsort(txt) {\n\tdnsort = dnsort ? 1 : 0;\n\tENATSORT = NATSORT && (sread('nsort') || dnsort) == 1;\n\tclmod(ebi('nsort'), 'on', ENATSORT);\n\ttry {\n\t\tvar zt = (('' + txt).trim() || 'href').split(/,+/g);\n\t\tdsort = [];\n\t\tfor (var a = 0; a < zt.length; a++) {\n\t\t\tvar z = zt[a].trim(), n = 1, t = \"\";\n\t\t\tif (z.startsWith(\"-\")) {\n\t\t\t\tz = z.slice(1);\n\t\t\t\tn = -1;\n\t\t\t}\n\t\t\tif (z == \"sz\" || z.indexOf('/.') + 1)\n\t\t\t\tt = \"int\";\n\n\t\t\tdsort.push([z, n, t]);\n\t\t}\n\t}\n\tcatch (ex) {\n\t\ttoast.warn(10, 'failed to apply default sort order [' + esc('' + txt) + ']:\\n' + ex);\n\t\tdsort = [['href', 1, '']];\n\t}\n}\nread_dsort(dsort);\n\n\nfunction getsort() {\n\tvar ret = '',\n\t\tsopts = jread('fsort');\n\n\tsopts = sopts && sopts.length ? sopts : dsort;\n\n\tfor (var a = 0; a < Math.min(hsortn, sopts.length); a++)\n\t\tret += ',sort' + (sopts[a][1] < 0 ? '-' : '') + sopts[a][0];\n\n\treturn ret;\n}\n\n\nfunction sortfiles(nodes) {\n\tif (!nodes.length)\n\t\treturn nodes;\n\n\tvar sopts = jread('fsort'),\n\t\tdir1st = sread('dir1st') !== '0';\n\n\tsopts = sopts && sopts.length ? sopts : jcp(dsort);\n\n\ttry {\n\t\tvar is_srch = false;\n\t\tif (nodes[0]['rp']) {\n\t\t\tis_srch = true;\n\t\t\tfor (var b = 0, bb = nodes.length; b < bb; b++)\n\t\t\t\tnodes[b].ext = nodes[b].rp.split('.').pop();\n\t\t\tfor (var b = 0; b < sopts.length; b++)\n\t\t\t\tif (sopts[b][0] == 'href')\n\t\t\t\t\tsopts[b][0] = 'rp';\n\t\t}\n\t\tfor (var a = sopts.length - 1; a >= 0; a--) {\n\t\t\tvar name = sopts[a][0], rev = sopts[a][1], typ = sopts[a][2];\n\t\t\tif (!name)\n\t\t\t\tcontinue;\n\n\t\t\tname = name.toLowerCase();\n\n\t\t\tif (name == 'ts')\n\t\t\t\ttyp = 'int';\n\n\t\t\tif (name.indexOf('tags/') === 0) {\n\t\t\t\tname = name.slice(5);\n\t\t\t\tfor (var b = 0, bb = nodes.length; b < bb; b++)\n\t\t\t\t\tnodes[b]._sv = nodes[b].tags[name];\n\t\t\t}\n\t\t\telse {\n\t\t\t\tfor (var b = 0, bb = nodes.length; b < bb; b++) {\n\t\t\t\t\tvar v = nodes[b][name];\n\n\t\t\t\t\tif ((v + '').indexOf('<a ') === 0)\n\t\t\t\t\t\tv = v.split('>')[1];\n\t\t\t\t\telse if (name == \"href\" && v)\n\t\t\t\t\t\tv = uricom_dec(v);\n\n\t\t\t\t\tnodes[b]._sv = v\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar onodes = nodes.map(function (x) { return x; });\n\t\t\tnodes.sort(function (n1, n2) {\n\t\t\t\tvar v1 = n1._sv,\n\t\t\t\t\tv2 = n2._sv;\n\n\t\t\t\tif (v1 === undefined) {\n\t\t\t\t\tif (v2 === undefined) {\n\t\t\t\t\t\treturn onodes.indexOf(n1) - onodes.indexOf(n2);\n\t\t\t\t\t}\n\t\t\t\t\treturn -1 * rev;\n\t\t\t\t}\n\t\t\t\tif (v2 === undefined) return 1 * rev;\n\n\t\t\t\tvar ret = rev * (typ == 'int' ? (v1 - v2) :\n\t\t\t\t\tENATSORT ? NATSORT.compare(v1, v2) :\n\t\t\t\t\tv1.localeCompare(v2));\n\n\t\t\t\tif (ret === 0)\n\t\t\t\t\tret = onodes.indexOf(n1) - onodes.indexOf(n2);\n\n\t\t\t\treturn ret;\n\t\t\t});\n\t\t}\n\t\tfor (var b = 0, bb = nodes.length; b < bb; b++) {\n\t\t\tdelete nodes[b]._sv;\n\t\t\tif (is_srch)\n\t\t\t\tdelete nodes[b].ext;\n\t\t}\n\t\tif (dir1st) {\n\t\t\tvar r1 = [], r2 = [];\n\t\t\tfor (var b = 0, bb = nodes.length; b < bb; b++)\n\t\t\t\t(nodes[b].href.split('?')[0].slice(-1) == '/' ? r1 : r2).push(nodes[b]);\n\n\t\t\tnodes = r1.concat(r2);\n\t\t}\n\t}\n\tcatch (ex) {\n\t\tconsole.log(\"failed to apply sort config: \" + ex);\n\t\tconsole.log(\"resetting fsort \" + sread('fsort'));\n\t\tsdrop('fsort');\n\t}\n\treturn nodes;\n}\n\n\nfunction fmt_ren(re, md, fmt) {\n\tvar ptr = 0;\n\tfunction dive(stop_ch) {\n\t\tvar ret = '', ng = 0;\n\t\twhile (ptr < fmt.length) {\n\t\t\tvar dbg = fmt.slice(ptr),\n\t\t\t\tch = fmt[ptr++];\n\n\t\t\tif (ch == '\\\\') {\n\t\t\t\tret += fmt[ptr++];\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (ch == ')' || ch == ']' || ch == stop_ch)\n\t\t\t\treturn [ng, ret];\n\n\t\t\tif (ch == '[') {\n\t\t\t\tvar r2 = dive();\n\t\t\t\tif (r2[0] == 0)\n\t\t\t\t\tret += r2[1];\n\t\t\t}\n\t\t\telse if (ch == '(') {\n\t\t\t\tvar end = fmt.indexOf(')', ptr);\n\t\t\t\tif (end < 0)\n\t\t\t\t\tthrow 'the ( was never closed: ' + fmt.slice(0, ptr);\n\n\t\t\t\tvar arg = fmt.slice(ptr, end), v = null;\n\t\t\t\tptr = end + 1;\n\n\t\t\t\tif (arg != parseInt(arg))\n\t\t\t\t\tv = md[arg];\n\t\t\t\telse {\n\t\t\t\t\targ = parseInt(arg);\n\t\t\t\t\tif (arg >= re.length)\n\t\t\t\t\t\tthrow 'matching group ' + arg + ' exceeds ' + (re.length - 0);\n\n\t\t\t\t\tv = re[arg];\n\t\t\t\t}\n\n\t\t\t\tif (v !== null && v !== undefined)\n\t\t\t\t\tret += v;\n\t\t\t\telse\n\t\t\t\t\tng++;\n\t\t\t}\n\t\t\telse if (ch == '$') {\n\t\t\t\tch = fmt[ptr++];\n\t\t\t\tvar end = fmt.indexOf('(', ptr);\n\t\t\t\tif (end < 0)\n\t\t\t\t\tthrow 'no function name after the $ here: ' + fmt.slice(0, ptr);\n\n\t\t\t\tvar fun = fmt.slice(ptr - 1, end);\n\t\t\t\tptr = end + 1;\n\n\t\t\t\tif (fun == \"lpad\") {\n\t\t\t\t\tvar str = dive(',')[1];\n\t\t\t\t\tvar len = dive(',')[1];\n\t\t\t\t\tvar chr = dive()[1];\n\t\t\t\t\tif (!len || !chr)\n\t\t\t\t\t\tthrow 'invalid arguments to ' + fun;\n\n\t\t\t\t\tif (!str.length)\n\t\t\t\t\t\tng += 1;\n\n\t\t\t\t\twhile (str.length < len)\n\t\t\t\t\t\tstr = chr + str;\n\n\t\t\t\t\tret += str;\n\t\t\t\t}\n\t\t\t\telse if (fun == \"rpad\") {\n\t\t\t\t\tvar str = dive(',')[1];\n\t\t\t\t\tvar len = dive(',')[1];\n\t\t\t\t\tvar chr = dive()[1];\n\t\t\t\t\tif (!len || !chr)\n\t\t\t\t\t\tthrow 'invalid arguments to ' + fun;\n\n\t\t\t\t\tif (!str.length)\n\t\t\t\t\t\tng += 1;\n\n\t\t\t\t\twhile (str.length < len)\n\t\t\t\t\t\tstr += chr;\n\n\t\t\t\t\tret += str;\n\t\t\t\t}\n\t\t\t\telse throw 'function not implemented: \"' + fun + '\"';\n\t\t\t}\n\t\t\telse ret += ch;\n\t\t}\n\t\treturn [ng, ret];\n\t}\n\ttry {\n\t\treturn [true, dive()[1]];\n\t}\n\tcatch (ex) {\n\t\treturn [false, ex];\n\t}\n}\n\n\nfunction enre_rw_edit() {\n\twindow.re_rw_edit = new RegExp('\\.(' + rw_edit.replace(/,/g, '|') + ')$', 'i');\n}\nenre_rw_edit();\n\n\nfunction fs_abrt() {\n\ttoast.inf(30, L.fp_abrt);\n\tfileman.sn++;\n\tfileman.f.length = 0;\n\tvar xhr = new XHR();\n\txhr.open('POST', '/?fs_abrt=' + abrt_key, true);\n\txhr.send();\n}\n\n\nvar fileman = (function () {\n\tvar bren = ebi('fren'),\n\t\tbdel = ebi('fdel'),\n\t\tbcut = ebi('fcut'),\n\t\tbcpy = ebi('fcpy'),\n\t\tbpst = ebi('fpst'),\n\t\tbshr = ebi('fshr'),\n\t\tt_paste,\n\t\tr = {};\n\n\tr.f = [];\n\tr.sn = 1;\n\tr.clip = null;\n\ttry {\n\t\tr.bus = new BroadcastChannel(\"fileman_bus\");\n\t}\n\tcatch (ex) { }\n\n\tr.render = function () {\n\t\tif (r.clip === null) {\n\t\t\tr.clip = jread('fman_clip', []).slice(1);\n\t\t\tr.ccp = r.clip.length && r.clip[0] == '//c';\n\t\t\tif (r.ccp)\n\t\t\t\tr.clip.shift();\n\t\t}\n\n\t\tvar sel = msel.getsel(),\n\t\t\tnsel = sel.length,\n\t\t\tenren = nsel,\n\t\t\tendel = nsel,\n\t\t\tencut = nsel,\n\t\t\tencpy = nsel,\n\t\t\tenpst = r.clip && r.clip.length,\n\t\t\thren = !(have_mv && has(perms, 'write') && has(perms, 'move')),\n\t\t\thdel = !(have_del && has(perms, 'delete')),\n\t\t\thcut = !(have_mv && has(perms, 'move')),\n\t\t\thpst = !(have_mv && has(perms, 'write')),\n\t\t\thshr = !can_shr || !get_evpath().indexOf(have_shr);\n\n\t\tif (!(enren || endel || encut || enpst))\n\t\t\thren = hdel = hcut = hpst = true;\n\n\t\tclmod(bren, 'en', enren);\n\t\tclmod(bdel, 'en', endel);\n\t\tclmod(bcut, 'en', encut);\n\t\tclmod(bcpy, 'en', encpy);\n\t\tclmod(bpst, 'en', enpst);\n\t\tclmod(bshr, 'en', 1);\n\n\t\tclmod(bren, 'hide', hren);\n\t\tclmod(bdel, 'hide', hdel);\n\t\tclmod(bcut, 'hide', hcut);\n\t\tclmod(bpst, 'hide', hpst);\n\t\tclmod(bshr, 'hide', hshr);\n\n\t\tclmod(ebi('wfm'), 'act', QS('#wfm a.en:not(.hide)'));\n\t\tclmod(ebi('wtoggle'), 'm3u', mpl.m3uen && (nsel || (mp && mp.au)));\n\n\t\tvar wfs = ebi('wfs'), h = '';\n\t\ttry {\n\t\t\twfs.innerHTML = h = r.fsi(sel);\n\t\t}\n\t\tcatch (ex) { }\n\t\tclmod(wfs, 'act', h);\n\n\t\tbpst.setAttribute('tt', L.ft_paste.format(r.clip.length));\n\t\tbshr.setAttribute('tt', nsel ? L.fs_ss : L.fs_sc);\n\t};\n\n\tr.fsi = function (sel) {\n\t\tif (!sel.length)\n\t\t\treturn '';\n\n\t\tvar lf = treectl.lsc.files,\n\t\t\tnf = 0,\n\t\t\tsz = 0,\n\t\t\tdur = 0,\n\t\t\tntab = new Set();\n\n\t\tfor (var a = 0; a < sel.length; a++)\n\t\t\tntab.add(sel[a].vp.split('/').pop());\n\n\t\tfor (var a = 0; a < lf.length; a++) {\n\t\t\tif (!ntab.has(lf[a].href.split('?')[0]))\n\t\t\t\tcontinue;\n\n\t\t\tvar f = lf[a];\n\t\t\tnf++;\n\t\t\tsz += f.sz;\n\t\t\tif (f.tags && f.tags['.dur'])\n\t\t\t\tdur += f.tags['.dur']\n\t\t}\n\n\t\tif (!nf)\n\t\t\treturn '';\n\n\t\tvar ret = '{0}<br />{1}<small>F</small>'.format(humansize(sz), nf);\n\n\t\tif (dur)\n\t\t\tret += ' ' + s2ms(dur);\n\n\t\treturn ret;\n\t};\n\n\tr.share = function (e) {\n\t\tev(e);\n\n\t\tvar vp = uricom_dec(get_evpath()),\n\t\t\tsel = msel.getsel(),\n\t\t\tfns = [];\n\n\t\tfor (var a = 0; a < sel.length; a++)\n\t\t\tfns.push(uricom_dec(noq_href(ebi(sel[a].id))));\n\n\t\tif (fns.length == 1 && fns[0].endsWith('/'))\n\t\t\tvp = fns.pop();\n\n\t\tfor (var a = 0; a < fns.length; a++)\n\t\t\tif (fns[a].endsWith('/'))\n\t\t\t\treturn toast.err(10, L.fs_just1d);\n\n\t\tvar shui = ebi('shui');\n\t\tif (!shui) {\n\t\t\tshui = mknod('div', 'shui');\n\t\t\tdocument.body.appendChild(shui);\n\t\t}\n\t\tshui.style.display = 'block';\n\n\t\tvar html = [\n\t\t\t'<div>',\n\t\t\t'<table>',\n\t\t\t'<tr><td colspan=\"2\">',\n\t\t\t'<button id=\"sh_abrt\">' + L.fs_abrt + '</button>',\n\t\t\t'<button id=\"sh_rand\">' + L.fs_rand + '</button>',\n\t\t\t'<button id=\"sh_apply\">' + L.fs_go + '</button>',\n\t\t\t'</td></tr>',\n\t\t\t'<tr><td>' + L.fs_name + '</td><td><input type=\"text\" id=\"sh_k\" ' + NOAC + ' placeholder=\"  ' + L.fs_pname + '\" /></td></tr>',\n\t\t\t'<tr><td>' + L.fs_src + '</td><td><input type=\"text\" id=\"sh_vp\" ' + NOAC + ' readonly tt=\"' + L.fs_tsrc + '\" /></td></tr>',\n\t\t\t'<tr><td>' + L.fs_pwd + '</td><td><input type=\"text\" id=\"sh_pw\" ' + NOAC + ' placeholder=\"  ' + L.fs_ppwd + '\" /></td></tr>',\n\t\t\t'<tr><td>' + L.fs_exp + '</td><td class=\"exs\">',\n\t\t\t'<input type=\"text\" id=\"sh_exm\" ' + NOAC + ' /> ' + L.fs_tmin + ' / ',\n\t\t\t'<input type=\"text\" id=\"sh_exh\" ' + NOAC + ' /> ' + L.fs_thrs + ' / ',\n\t\t\t'<input type=\"text\" id=\"sh_exd\" ' + NOAC + ' /> ' + L.fs_tdays + ' / ',\n\t\t\t'<button id=\"sh_noex\">' + L.fs_never + '</button>',\n\t\t\t'</td></tr>',\n\t\t\t'<tr><td>perms</td><td class=\"sh_axs\">',\n\t\t];\n\t\tfor (var a = 0; a < perms.length; a++)\n\t\t\tif (!has(['admin', 'move', 'delete'], perms[a]))\n\t\t\t\thtml.push('<a href=\"#\" class=\"tgl btn\">' + perms[a] + '</a>');\n\n\t\tif (has(perms, 'write'))\n\t\t\thtml.push('<a href=\"#\" class=\"btn\">write-only</a>');\n\n\t\thtml.push('</td></tr></div');\n\t\tshui.innerHTML = html.join('\\n');\n\n\t\tvar sh_rand = ebi('sh_rand'),\n\t\t\tsh_abrt = ebi('sh_abrt'),\n\t\t\tsh_apply = ebi('sh_apply'),\n\t\t\tsh_noex = ebi('sh_noex'),\n\t\t\texm = ebi('sh_exm'),\n\t\t\texh = ebi('sh_exh'),\n\t\t\texd = ebi('sh_exd'),\n\t\t\tsh_k = ebi('sh_k'),\n\t\t\tsh_vp = ebi('sh_vp'),\n\t\t\tsh_pw = ebi('sh_pw');\n\n\t\tfunction setexp(a, b) {\n\t\t\ta = parseFloat(a);\n\t\t\tif (!isNum(a))\n\t\t\t\treturn;\n\n\t\t\tvar v = a * b;\n\t\t\tswrite('fsh_exp', v);\n\n\t\t\tif (exm.value != v) exm.value = Math.round(v * 10) / 10; v /= 60;\n\t\t\tif (exh.value != v) exh.value = Math.round(v * 10) / 10; v /= 24;\n\t\t\tif (exd.value != v) exd.value = Math.round(v * 10) / 10;\n\t\t}\n\t\tfunction setdef() {\n\t\t\tsetexp(icfg_get('fsh_exp', 60 * 24), 1);\n\t\t}\n\t\tsetdef();\n\n\t\texm.oninput = function () { setexp(this.value, 1); };\n\t\texh.oninput = function () { setexp(this.value, 60); };\n\t\texd.oninput = function () { setexp(this.value, 60 * 24); };\n\t\texm.onfocus = exh.onfocus = exd.onfocus = function () {\n\t\t\tthis.value = '';\n\t\t};\n\t\tsh_noex.onclick = function () {\n\t\t\tsetexp(0, 1);\n\t\t};\n\t\texm.onblur = exh.onblur = exd.onblur = setdef;\n\n\t\texm.onkeydown = exh.onkeydown = exd.onkeydown =\n\t\tsh_k.onkeydown = sh_pw.onkeydown = function (e) {\n\t\t\tvar kc = (e.key || e.code) + '';\n\t\t\tif (kc.endsWith('Enter'))\n\t\t\t\tsh_apply.click();\n\t\t};\n\n\t\tsh_abrt.onclick = function () {\n\t\t\tshui.parentNode.removeChild(shui);\n\t\t};\n\t\tsh_rand.onclick = function () {\n\t\t\tsh_k.value = randstr(12).replace(/l/g, 'n');\n\t\t};\n\t\ttt.att(shui);\n\n\t\tvar pbtns = QSA('#shui .sh_axs a');\n\t\tfor (var a = 0; a < pbtns.length; a++)\n\t\t\tpbtns[a].onclick = shspf;\n\n\t\tfunction shspf() {\n\t\t\tclmod(this, 'on', 't');\n\t\t\tif (this.textContent == 'write-only')\n\t\t\t\tfor (var a = 0; a < pbtns.length; a++)\n\t\t\t\t\tclmod(pbtns[a], 'on', pbtns[a].textContent == 'write');\n\t\t}\n\t\tclmod(pbtns[0], 'on', 1);\n\n\t\tvar vpt = vp;\n\t\tif (fns.length) {\n\t\t\tvpt = fns.length + ' files in ' + vp + '  '\n\t\t\tfor (var a = 0; a < fns.length; a++)\n\t\t\t\tvpt += '「' + fns[a].split('/').pop() + '」';\n\t\t}\n\t\tsh_vp.value = vpt;\n\n\t\tsh_k.oninput = function (e) {\n\t\t\tvar v = this.value,\n\t\t\t\tv2 = v.replace(/[^0-9a-zA-Z-]/g, '_');\n\n\t\t\tif (v != v2)\n\t\t\t\tthis.value = v2;\n\t\t};\n\n\t\tfunction shr_cb() {\n\t\t\ttoast.hide();\n\t\t\tvar surl = this.responseText;\n\t\t\tif (this.status !== 201 || !/^created share:/.exec(surl)) {\n\t\t\t\tshui.style.display = 'block';\n\t\t\t\tvar msg = unpre(surl);\n\t\t\t\ttoast.err(9, msg);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsurl = surl.slice(15).trim();\n\t\t\tvar txt = esc(surl) + '<img class=\"b64\" width=\"100\" height=\"100\" src=\"' + surl + '?qr\" />';\n\t\t\tmodal.confirm(txt + L.fs_ok, function() {\n\t\t\t\tcliptxt(surl, function () {\n\t\t\t\t\ttoast.ok(2, L.clipped);\n\t\t\t\t});\n\t\t\t}, null);\n\t\t}\n\n\t\tsh_apply.onclick = function () {\n\t\t\tif (!sh_k.value)\n\t\t\t\tsh_rand.click();\n\n\t\t\tvar plist = [];\n\t\t\tfor (var a = 0; a < pbtns.length; a++)\n\t\t\t\tif (clgot(pbtns[a], 'on'))\n\t\t\t\t\tplist.push(pbtns[a].textContent);\n\n\t\t\tshui.style.display = 'none';\n\t\t\ttoast.inf(30, L.fs_w8);\n\n\t\t\tvar body = {\n\t\t\t\t\"k\": sh_k.value,\n\t\t\t\t\"vp\": fns.length ? fns : [sh_vp.value],\n\t\t\t\t\"pw\": sh_pw.value,\n\t\t\t\t\"exp\": exm.value,\n\t\t\t\t\"perms\": plist,\n\t\t\t};\n\t\t\tvar xhr = new XHR();\n\t\t\txhr.open('POST', SR + '/?share', true);\n\t\t\txhr.setRequestHeader('Content-Type', 'text/plain');\n\t\t\txhr.onload = xhr.onerror = shr_cb;\n\t\t\txhr.send(JSON.stringify(body));\n\t\t};\n\n\t\tsetTimeout(sh_pw.focus.bind(sh_pw), 1);\n\t};\n\n\tr.rename = function (e) {\n\t\tev(e);\n\t\tvar sel = msel.getsel(),\n\t\t\tall = msel.all;\n\t\tif (!sel.length)\n\t\t\treturn toast.err(3, L.fr_emore);\n\n\t\tif (clgot(bren, 'hide'))\n\t\t\treturn toast.err(3, L.fr_eperm);\n\n\t\tvar f = [],\n\t\t\tsn = ++r.sn,\n\t\t\tbase = vsplit(sel[0].vp)[0],\n\t\t\ts2d = {},\n\t\t\tmkeys;\n\n\t\tr.f = f;\n\t\tr.n_s = 1;\n\t\tr.n_d = 1;\n\n\t\tfor (var a = 0; a < sel.length; a++) {\n\t\t\ts2d[a] = all.indexOf(sel[a]);\n\n\t\t\tvar vp = sel[a].vp;\n\t\t\tif (vp.endsWith('/'))\n\t\t\t\tvp = vp.slice(0, -1);\n\n\t\t\tvar vsp = vsplit(vp);\n\t\t\tif (base != vsp[0])\n\t\t\t\treturn toast.err(0, esc('bug:\\n' + base + '\\n' + vsp[0]));\n\n\t\t\tvar vars = ft2dict(ebi(sel[a].id).closest('tr'));\n\t\t\tmkeys = [\".n.d\", \".n.s\"].concat(vars[1], vars[2]);\n\n\t\t\tvar md = vars[0];\n\t\t\tmd[\".n.s\"] = md[\".n.d\"] = 0;\n\t\t\tfor (var k in md) {\n\t\t\t\tif (!md.hasOwnProperty(k))\n\t\t\t\t\tcontinue;\n\n\t\t\t\tmd[k] = (md[k] + '').replace(/[\\/\\\\]/g, '-');\n\n\t\t\t\tif (k.startsWith('.'))\n\t\t\t\t\tmd[k.slice(1)] = md[k];\n\t\t\t}\n\t\t\tmd.t = md.ext;\n\t\t\tmd.date = md.ts;\n\t\t\tmd.size = md.sz;\n\n\t\t\tf.push({\n\t\t\t\t\"src\": vp,\n\t\t\t\t\"ofn\": uricom_dec(vsp[1]),\n\t\t\t\t\"md\": vars[0],\n\t\t\t\t\"ok\": true\n\t\t\t});\n\t\t}\n\n\t\tvar rui = ebi('rui');\n\t\tif (!rui) {\n\t\t\trui = mknod('div', 'rui');\n\t\t\tdocument.body.appendChild(rui);\n\t\t}\n\n\t\tvar html = sel.length > 1 ? ['<div>'] : [\n\t\t\t'<div>',\n\t\t\t'<button class=\"rn_dec\" id=\"rn_dec_0\" tt=\"' + L.frt_dec + '</button>',\n\t\t\t'//',\n\t\t\t'<button class=\"rn_reset\" id=\"rn_reset_0\" tt=\"' + L.frt_rst + '</button>'\n\t\t];\n\n\t\thtml = html.concat([\n\t\t\t'<button id=\"rn_cancel\" tt=\"' + L.frt_abrt + '</button>',\n\t\t\t'<button id=\"rn_apply\">✅ ' + L.frb_apply + '</button>',\n\t\t\t'<a id=\"rn_adv\" class=\"tgl btn\" href=\"#\" tt=\"' + L.fr_adv + '</a>',\n\t\t\t'<a id=\"rn_case\" class=\"tgl btn\" href=\"#\" tt=\"' + L.fr_case + '</a>',\n\t\t\t'<a id=\"rn_win\" class=\"tgl btn\" href=\"#\" tt=\"' + L.fr_win + '</a>',\n\t\t\t'<a id=\"rn_slash\" class=\"tgl btn\" href=\"#\" tt=\"' + L.fr_slash + '</a>',\n\t\t\t'</div>',\n\t\t\t'<div id=\"rn_vadv\"><table>',\n\t\t\t'<tr><td>regex</td><td><input type=\"text\" id=\"rn_re\" ' + NOAC + ' tt=\"' + L.fr_re + '\" placeholder=\"^[0-9]+[\\\\. ]+(.*) - (.*)\" /></td></tr>',\n\t\t\t'<tr><td>format</td><td><input type=\"text\" id=\"rn_fmt\" ' + NOAC + ' tt=\"' + L.fr_fmt + '\" placeholder=\"[(artist) - ](title).(ext)\" /></td></tr>',\n\t\t\t'<tr><td>preset</td><td><select id=\"rn_pre\"></select>',\n\t\t\t'<tr><td>num0</td><td>',\n\t\t\t'<code>n.d=</code><input type=\"text\" id=\"rn_n_d\" placeholder=\"1\" ' + NOAC + ' /> &nbsp;',\n\t\t\t'<code>n.s=</code><input type=\"text\" id=\"rn_n_s\" placeholder=\"1\" ' + NOAC + ' />',\n\t\t\t'</td></tr>',\n\t\t\t'<button id=\"rn_pdel\">❌ ' + L.fr_pdel + '</button>',\n\t\t\t'<button id=\"rn_pnew\">💾 ' + L.fr_pnew + '</button>',\n\t\t\t'</td></tr>',\n\t\t\t'</table></div>'\n\t\t]);\n\n\t\tvar cheap = f.length > 500,\n\t\t\tt_rst = L.frt_rst.split('>').pop();\n\n\t\tif (sel.length == 1)\n\t\t\thtml.push(\n\t\t\t\t'<div><table id=\"rn_f\">\\n' +\n\t\t\t\t'<tr><td>old:</td><td><input type=\"text\" id=\"rn_old_0\" readonly /></td></tr>\\n' +\n\t\t\t\t'<tr><td>new:</td><td><input type=\"text\" id=\"rn_new_0\" /></td></tr>');\n\t\telse {\n\t\t\thtml.push(\n\t\t\t\t'<div><table id=\"rn_f\" class=\"m\">' +\n\t\t\t\t'<tr><td></td><td>' + L.fr_lnew + '</td><td>' + L.fr_lold + '</td></tr>');\n\t\t\tfor (var a = 0; a < f.length; a++)\n\t\t\t\thtml.push(\n\t\t\t\t\t'<tr><td>' +\n\t\t\t\t\t(cheap ? '</td>' :\n\t\t\t\t\t\t'<button class=\"rn_dec\" id=\"rn_dec_' + a + '\">decode</button>' +\n\t\t\t\t\t\t'<button class=\"rn_reset\" id=\"rn_reset_' + a + '\">' + t_rst + '</button></td>') +\n\t\t\t\t\t'<td><input type=\"text\" id=\"rn_new_' + a + '\" /></td>' +\n\t\t\t\t\t'<td><input type=\"text\" id=\"rn_old_' + a + '\" readonly /></td></tr>');\n\t\t}\n\t\thtml.push('</table></div>');\n\n\t\tif (sel.length == 1) {\n\t\t\thtml.push('<div><p style=\"margin:.6em 0\">' + L.fr_tags + '</p><table>');\n\t\t\tfor (var a = 0; a < mkeys.length; a++)\n\t\t\t\thtml.push('<tr><td>' + esc(mkeys[a]) + '</td><td><input type=\"text\" readonly value=\"' + esc(f[0].md[mkeys[a]]) + '\" /></td></tr>');\n\n\t\t\thtml.push('</table></div>');\n\t\t}\n\n\t\trui.innerHTML = html.join('\\n');\n\t\tfor (var a = 0; a < f.length; a++) {\n\t\t\tf[a].iold = ebi('rn_old_' + a);\n\t\t\tf[a].inew = ebi('rn_new_' + a);\n\t\t\tf[a].inew.value = f[a].iold.value = f[a].ofn;\n\n\t\t\tif (!cheap)\n\t\t\t\t(function (a) {\n\t\t\t\t\tf[a].inew.onkeydown = function (e) {\n\t\t\t\t\t\trn_ok(a, true);\n\t\t\t\t\t\tvar kc = (e.key || e.code) + '';\n\t\t\t\t\t\tif (kc.endsWith('Enter'))\n\t\t\t\t\t\t\treturn rn_apply();\n\t\t\t\t\t};\n\t\t\t\t\tebi('rn_dec_' + a).onclick = function (e) {\n\t\t\t\t\t\tev(e);\n\t\t\t\t\t\tf[a].inew.value = uricom_dec(f[a].inew.value);\n\t\t\t\t\t};\n\t\t\t\t\tebi('rn_reset_' + a).onclick = function (e) {\n\t\t\t\t\t\tev(e);\n\t\t\t\t\t\trn_reset(a);\n\t\t\t\t\t};\n\t\t\t\t})(a);\n\t\t}\n\t\trn_reset(0);\n\t\ttt.att(rui);\n\n\t\tfunction sadv() {\n\t\t\tebi('rn_vadv').style.display = ebi('rn_case').style.display = r.adv ? '' : 'none';\n\t\t}\n\t\tbcfg_bind(r, 'adv', 'rn_adv', false, sadv);\n\t\tbcfg_bind(r, 'cs', 'rn_case', false);\n\t\tbcfg_bind(r, 'win', 'rn_win', true);\n\t\tbcfg_bind(r, 'slash', 'rn_slash', true);\n\t\tsadv();\n\n\t\tfunction rn_ok(n, ok) {\n\t\t\tf[n].ok = ok;\n\t\t\tclmod(f[n].inew.closest('tr'), 'err', !ok);\n\t\t}\n\n\t\tfunction rn_reset(n) {\n\t\t\tf[n].inew.value = f[n].iold.value = f[n].ofn;\n\t\t\tf[n].inew.focus();\n\t\t\tf[n].inew.setSelectionRange(0, f[n].inew.value.lastIndexOf('.'), \"forward\");\n\t\t}\n\t\tfunction rn_cancel(e) {\n\t\t\tev(e);\n\t\t\trui.parentNode.removeChild(rui);\n\t\t}\n\n\t\tebi('rn_cancel').onclick = rn_cancel;\n\t\tebi('rn_apply').onclick = rn_apply;\n\n\t\tvar ire = ebi('rn_re'),\n\t\t\tifmt = ebi('rn_fmt'),\n\t\t\tipre = ebi('rn_pre'),\n\t\t\tidel = ebi('rn_pdel'),\n\t\t\tinew = ebi('rn_pnew'),\n\t\t\tdefp = '$lpad((tn),2,0). [(artist) - ](title).(ext)';\n\n\t\tire.value = sread('cpp_rn_re') || '';\n\t\tifmt.value = sread('cpp_rn_fmt') || '';\n\n\t\tvar presets = {};\n\t\tpresets[defp] = ['', defp];\n\t\tpresets = jread(\"rn_pre\", presets);\n\n\t\tfunction spresets() {\n\t\t\tvar keys = Object.keys(presets), o;\n\t\t\tkeys.sort();\n\t\t\tipre.innerHTML = '<option value=\"\"></option>';\n\t\t\tfor (var a = 0; a < keys.length; a++) {\n\t\t\t\to = mknod('option');\n\t\t\t\to.setAttribute('value', keys[a]);\n\t\t\t\to.textContent = keys[a];\n\t\t\t\tipre.appendChild(o);\n\t\t\t}\n\t\t}\n\t\tinew.onclick = function (e) {\n\t\t\tev(e);\n\t\t\tmodal.prompt(L.fr_pname, ifmt.value, function (name) {\n\t\t\t\tif (!name)\n\t\t\t\t\treturn toast.warn(3, L.fr_aborted);\n\n\t\t\t\tpresets[name] = [ire.value, ifmt.value];\n\t\t\t\tjwrite('rn_pre', presets);\n\t\t\t\tspresets();\n\t\t\t\tipre.value = name;\n\t\t\t});\n\t\t};\n\t\tidel.onclick = function (e) {\n\t\t\tev(e);\n\t\t\tdelete presets[ipre.value];\n\t\t\tjwrite('rn_pre', presets);\n\t\t\tspresets();\n\t\t};\n\t\tipre.oninput = function () {\n\t\t\tvar cfg = presets[ipre.value];\n\t\t\tif (cfg) {\n\t\t\t\tire.value = cfg[0];\n\t\t\t\tifmt.value = cfg[1];\n\t\t\t}\n\t\t\tifmt.oninput();\n\t\t};\n\t\tspresets();\n\n\t\tebi('rn_n_s').oninput = function () {\n\t\t\tr.n_s = parseInt(this.value || '1');\n\t\t\tifmt.oninput();\n\t\t};\n\t\tebi('rn_n_d').oninput = function () {\n\t\t\tr.n_d = parseInt(this.value || '1');\n\t\t\tifmt.oninput();\n\t\t};\n\n\t\tire.onkeydown = ifmt.onkeydown = function (e) {\n\t\t\tvar k = (e.key || e.code) + '';\n\n\t\t\tif (k == 'Escape' || k == 'Esc')\n\t\t\t\treturn rn_cancel();\n\n\t\t\tif (k.endsWith('Enter'))\n\t\t\t\treturn rn_apply();\n\t\t};\n\n\t\tire.oninput = ifmt.oninput = function (e) {\n\t\t\tvar ptn = ire.value,\n\t\t\t\tfmt = ifmt.value,\n\t\t\t\tre = null;\n\n\t\t\tif (!fmt)\n\t\t\t\treturn;\n\n\t\t\ttry {\n\t\t\t\tif (ptn)\n\t\t\t\t\tre = new RegExp(ptn, r.cs ? 'i' : '');\n\t\t\t}\n\t\t\tcatch (ex) {\n\t\t\t\treturn toast.err(5, esc('invalid regex:\\n' + ex));\n\t\t\t}\n\t\t\ttoast.hide();\n\n\t\t\tfor (var a = 0; a < f.length; a++) {\n\t\t\t\tvar m = re ? re.exec(f[a].ofn) : null,\n\t\t\t\t\td = f[a].md,\n\t\t\t\t\tok, txt = '';\n\n\t\t\t\td[\".n.s\"] = d[\"n.s\"] = '' + (r.n_s + a);\n\t\t\t\td[\".n.d\"] = d[\"n.d\"] = '' + (r.n_d + s2d[a]);\n\n\t\t\t\tif (re && !m) {\n\t\t\t\t\ttxt = 'regex did not match';\n\t\t\t\t\tok = false;\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tvar ret = fmt_ren(m, d, fmt);\n\t\t\t\t\tok = ret[0];\n\t\t\t\t\ttxt = ret[1];\n\t\t\t\t}\n\t\t\t\trn_ok(a, ok);\n\t\t\t\tf[a].inew.value = (ok ? '' : 'ERROR: ') + txt;\n\t\t\t}\n\t\t};\n\n\t\tfunction rn_apply(e) {\n\t\t\tev(e);\n\t\t\tswrite('cpp_rn_re', ire.value);\n\t\t\tswrite('cpp_rn_fmt', ifmt.value);\n\t\t\tif (r.win || r.slash) {\n\t\t\t\tvar changed = 0;\n\t\t\t\tfor (var a = 0; a < f.length; a++) {\n\t\t\t\t\tvar ov = f[a].inew.value,\n\t\t\t\t\t\tnv = namesan(ov, r.win, r.slash);\n\n\t\t\t\t\tif (ov != nv) {\n\t\t\t\t\t\tf[a].inew.value = nv;\n\t\t\t\t\t\tchanged++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (changed)\n\t\t\t\t\treturn modal.confirm(L.fr_nchg.format(changed), rn_apply_loop, null);\n\t\t\t}\n\t\t\trn_apply_loop();\n\t\t}\n\n\t\tfunction rn_apply_loop() {\n\t\t\twhile (f.length && (!f[0].ok || f[0].ofn == f[0].inew.value))\n\t\t\t\tf.shift();\n\n\t\t\tif (!f.length) {\n\t\t\t\ttoast.ok(2, 'rename OK');\n\t\t\t\ttreectl.goto();\n\t\t\t\treturn rn_cancel();\n\t\t\t}\n\n\t\t\tvar msg = esc(L.fr_busy.format(f.length, f[0].ofn));\n\t\t\tmsg += '\\n<a id=\"fs_abrt\" class=\"btn\" href=\"#\" onclick=\"fs_abrt()\">' + L.fs_abrt + '</a>';\n\t\t\ttoast.show('inf r', 0, msg);\n\t\t\tvar dst = base + uricom_enc(f[0].inew.value, false);\n\n\t\t\tfunction rename_cb() {\n\t\t\t\tif (this.status !== 201) {\n\t\t\t\t\tvar msg = unpre(this.responseText);\n\t\t\t\t\ttoast.err(9, L.fr_efail + msg);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (r.sn != sn)\n\t\t\t\t\treturn modal.confirm('WARNING: the rename was aborted');\n\n\t\t\t\tf.shift().inew.value = '( OK )';\n\t\t\t\treturn rn_apply_loop();\n\t\t\t}\n\n\t\t\tabrt_key = randstr(9);\n\n\t\t\tvar xhr = new XHR();\n\t\t\txhr.open('POST', f[0].src + '?move=' + dst + '&akey=' + abrt_key, true);\n\t\t\txhr.onload = xhr.onerror = rename_cb;\n\t\t\txhr.send();\n\t\t}\n\t};\n\n\tr.delete = function (e) {\n\t\tvar sel = msel.getsel(),\n\t\t\tsn = ++r.sn,\n\t\t\tvps = [];\n\n\t\tfor (var a = 0; a < sel.length; a++)\n\t\t\tvps.push(sel[a].vp);\n\n\t\tif (!sel.length)\n\t\t\treturn toast.err(3, L.fd_emore);\n\n\t\tev(e);\n\n\t\tif (clgot(bdel, 'hide'))\n\t\t\treturn toast.err(3, L.fd_eperm);\n\n\t\tfunction deleter(err) {\n\t\t\tvar xhr = new XHR(),\n\t\t\t\tvp = vps.shift();\n\n\t\t\tif (!vp) {\n\t\t\t\tif (err !== 'xbd')\n\t\t\t\t\ttoast.ok(2, L.fd_ok);\n\n\t\t\t\ttreectl.goto();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttoast.show('inf r', 0, esc(L.fd_busy.format(vps.length + 1, vp)), 'r');\n\n\t\t\txhr.open('POST', vp + '?delete', true);\n\t\t\txhr.onload = xhr.onerror = delete_cb;\n\t\t\txhr.send();\n\t\t}\n\t\tfunction delete_cb() {\n\t\t\tif (this.status !== 200) {\n\t\t\t\tvar msg = unpre(this.responseText);\n\t\t\t\ttoast.err(9, L.fd_err + msg);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (r.sn != sn)\n\t\t\t\treturn modal.confirm('WARNING: the delete was aborted');\n\n\t\t\tif (this.responseText.indexOf('deleted 0 files (and 0') + 1) {\n\t\t\t\ttoast.err(9, L.fd_none);\n\t\t\t\treturn deleter('xbd');\n\t\t\t}\n\t\t\tdeleter();\n\t\t}\n\n\t\tvar asks = r.qdel ? 1 : 2;\n\t\tif (dqdel === 0)\n\t\t\tasks -= 1;\n\n\t\tif (!asks)\n\t\t\treturn deleter();\n\n\t\tmodal.confirm('<h6 style=\"color:#900\">' + L.danger + '</h6>\\n<b>' + L.fd_warn1.format(vps.length) + '</b><ul>' + uricom_adec(vps, true).join('') + '</ul>', function () {\n\t\t\tif (asks === 1)\n\t\t\t\treturn deleter();\n\t\t\tmodal.confirm(L.fd_warn2, deleter, null);\n\t\t}, null);\n\t};\n\n\tr.cut = function (e) {\n\t\tvar sel = msel.getsel(),\n\t\t\tstamp = Date.now(),\n\t\t\tvps = [stamp];\n\n\t\tif (!sel.length)\n\t\t\treturn toast.err(3, L.fc_emore);\n\n\t\tev(e);\n\n\t\tif (clgot(bcut, 'hide'))\n\t\t\treturn toast.err(3, L.fc_eperm);\n\n\t\tvar els = [], griden = thegrid.en;\n\t\tfor (var a = 0; a < sel.length; a++) {\n\t\t\tvps.push(sel[a].vp);\n\t\t\tif (sel.length < 100)\n\t\t\t\ttry {\n\t\t\t\t\tif (griden)\n\t\t\t\t\t\tels.push(QS('#ggrid>a[ref=\"' + sel[a].id + '\"]'));\n\t\t\t\t\telse\n\t\t\t\t\t\tels.push(ebi(sel[a].id).closest('tr'));\n\n\t\t\t\t\tclmod(els[a], 'fcut');\n\t\t\t\t}\n\t\t\t\tcatch (ex) { }\n\t\t}\n\n\t\tsetTimeout(function () {\n\t\t\ttry {\n\t\t\t\tfor (var a = 0; a < els.length; a++)\n\t\t\t\t\tclmod(els[a], 'fcut', 1);\n\t\t\t}\n\t\t\tcatch (ex) { }\n\t\t}, 1);\n\n\t\tr.ccp = false;\n\t\tr.clip = vps.slice(1);\n\n\t\ttry {\n\t\t\tvps = JSON.stringify(vps);\n\t\t\tif (vps.length > 1024 * 1024)\n\t\t\t\tthrow 'a';\n\n\t\t\tswrite('fman_clip', vps);\n\t\t\tr.tx(stamp);\n\t\t\tif (sel.length)\n\t\t\t\ttoast.inf(1.5, L.fc_ok.format(sel.length));\n\t\t}\n\t\tcatch (ex) {\n\t\t\ttoast.warn(30, L.fc_warn.format(sel.length));\n\t\t}\n\t};\n\n\tr.cpy = function (e) {\n\t\tvar sel = msel.getsel(),\n\t\t\tstamp = Date.now(),\n\t\t\tvps = [stamp, '//c'];\n\n\t\tif (!sel.length)\n\t\t\treturn toast.err(3, L.fcp_emore);\n\n\t\tev(e);\n\n\t\tvar els = [], griden = thegrid.en;\n\t\tfor (var a = 0; a < sel.length; a++) {\n\t\t\tvps.push(sel[a].vp);\n\t\t\tif (sel.length < 100)\n\t\t\t\ttry {\n\t\t\t\t\tif (griden)\n\t\t\t\t\t\tels.push(QS('#ggrid>a[ref=\"' + sel[a].id + '\"]'));\n\t\t\t\t\telse\n\t\t\t\t\t\tels.push(ebi(sel[a].id).closest('tr'));\n\n\t\t\t\t\tclmod(els[a], 'fcut');\n\t\t\t\t}\n\t\t\t\tcatch (ex) { }\n\t\t}\n\n\t\tsetTimeout(function () {\n\t\t\ttry {\n\t\t\t\tfor (var a = 0; a < els.length; a++)\n\t\t\t\t\tclmod(els[a], 'fcut', 1);\n\t\t\t}\n\t\t\tcatch (ex) { }\n\t\t}, 1);\n\n\t\tif (vps.length < 3)\n\t\t\tvps.pop();\n\n\t\tr.ccp = true;\n\t\tr.clip = vps.slice(2);\n\n\t\tvar prefix = sread(\"clip_uo\") || location.origin;\n\t\ttry {\n\t\t\tcliptxt(prefix + r.clip.join('\\n' + prefix));\n\t\t}\n\t\tcatch (ex) {}\n\n\t\ttry {\n\t\t\tvps = JSON.stringify(vps);\n\t\t\tif (vps.length > 1024 * 1024)\n\t\t\t\tthrow 'a';\n\n\t\t\tswrite('fman_clip', vps);\n\t\t\tr.tx(stamp);\n\t\t\tif (sel.length)\n\t\t\t\ttoast.inf(1.5, L.fcc_ok.format(sel.length));\n\t\t}\n\t\tcatch (ex) {\n\t\t\ttoast.warn(30, L.fcc_warn.format(sel.length));\n\t\t}\n\t};\n\n\tdocument.onpaste = function (e) {\n\t\tvar xfer = e.clipboardData || window.clipboardData;\n\t\tif (!xfer || !xfer.files || !xfer.files.length)\n\t\t\treturn;\n\n\t\tvar files = [];\n\t\tfor (var a = 0, aa = xfer.files.length; a < aa; a++)\n\t\t\tfiles.push(xfer.files[a]);\n\n\t\tclearTimeout(t_paste);\n\n\t\tif (!r.clip.length)\n\t\t\treturn r.clip_up(files);\n\n\t\tvar src = r.clip.length == 1 ? r.clip[0] : vsplit(r.clip[0])[0],\n\t\t\tmsg = (r.ccp ? L.fcp_both_m : L.fp_both_m).format(r.clip.length, src, files.length);\n\n\t\tmodal.confirm(msg, r.paste, function () { r.clip_up(files); }, null, (r.ccp ? L.fcp_both_b : L.fp_both_b));\n\t};\n\n\tr.clip_up = function (files) {\n\t\tgoto_up2k();\n\t\tvar good = [], nil = [], bad = [];\n\t\tfor (var a = 0, aa = files.length; a < aa; a++) {\n\t\t\tvar fobj = files[a], dst = good;\n\t\t\ttry {\n\t\t\t\tif (fobj.size < 1)\n\t\t\t\t\tdst = nil;\n\t\t\t}\n\t\t\tcatch (ex) {\n\t\t\t\tdst = bad;\n\t\t\t}\n\t\t\tdst.push([fobj, fobj.name]);\n\t\t}\n\t\tvar doit = function (is_img) {\n\t\t\tjwrite('fman_clip', [Date.now()]);\n\t\t\tr.clip = [];\n\n\t\t\tvar x = up2k.uc.ask_up;\n\t\t\tif (is_img)\n\t\t\t\tup2k.uc.ask_up = false;\n\n\t\t\tup2k.gotallfiles[0](good, nil, bad, up2k.gotallfiles.slice(1));\n\t\t\tup2k.uc.ask_up = x;\n\t\t};\n\t\tif (good.length != 1)\n\t\t\treturn doit();\n\n\t\tvar fn = good[0][1],\n\t\t\tofs = fn.lastIndexOf('.');\n\n\t\t// stop linux-chrome from adding the fs-path into the <input>\n\t\tsetTimeout(function () {\n\t\t\tmodal.prompt(L.fp_name, fn, function (v) {\n\t\t\t\tgood[0][1] = v;\n\t\t\t\tdoit(true);\n\t\t\t}, null, null, 0, ofs > 0 ? ofs : undefined);\n\t\t}, 1);\n\t};\n\n\tr.d_paste = function () {\n\t\t// gets called before onpaste; defer\n\t\tclearTimeout(t_paste);\n\t\tt_paste = setTimeout(r.paste, 50);\n\t};\n\n\tr.paste = function () {\n\t\tif (!r.clip.length)\n\t\t\treturn toast.err(5, L.fp_ecut);\n\n\t\tif (clgot(bpst, 'hide'))\n\t\t\treturn toast.err(3, L.fp_eperm);\n\n\t\tvar html = [\n\t\t\t\t'<div>',\n\t\t\t\t'<button id=\"rn_cancel\" tt=\"' + L.frt_abrt + '</button>',\n\t\t\t\t'<button id=\"rn_skip\">⏭ ' + L.fp_skip + '</button>',\n\t\t\t\t'<button id=\"rn_apply\">✅ ' + L.fp_apply + '</button>',\n\t\t\t\t' &nbsp; src: ' + esc(r.clip[0].replace(/[^/]+$/, '')),\n\t\t\t\t'</div>',\n\t\t\t\t'<p id=\"cnmt\"></p>',\n\t\t\t\t'<div><table id=\"rn_f\" class=\"m\">',\n\t\t\t\t'<tr><td>' + L.fr_lnew + '</td><td>' + L.fr_lold + '</td></tr>',\n\t\t\t],\n\t\t\tsn = ++r.sn,\n\t\t\tui = false,\n\t\t\tf = [],\n\t\t\tindir = [],\n\t\t\tsrcdir = vsplit(r.clip[0])[0],\n\t\t\tlinks = QSA('#files tbody td:nth-child(2) a');\n\n\t\tr.f = f;\n\n\t\tfor (var a = 0, aa = links.length; a < aa; a++)\n\t\t\tindir.push(uricom_dec(vsplit(noq_href(links[a]))[1]));\n\n\t\tfor (var a = 0; a < r.clip.length; a++) {\n\t\t\tvar t = {\n\t\t\t\t'ok': true,\n\t\t\t\t'src': r.clip[a],\n\t\t\t\t'dst': uricom_dec(r.clip[a].split('/').pop()),\n\t\t\t};\n\t\t\tf.push(t);\n\n\t\t\tfor (var b = 0; b < indir.length; b++)\n\t\t\t\tif (t.dst == indir[b]) {\n\t\t\t\t\tt.ok = false;\n\t\t\t\t\tui = true;\n\t\t\t\t}\n\n\t\t\thtml.push('<tr' + (!t.ok ? ' class=\"ng\"' : '') + '><td><input type=\"text\" id=\"rn_new_' + a + '\" value=\"' + esc(t.dst) + '\" /></td><td><input type=\"text\" id=\"rn_old_' + a + '\" value=\"' + esc(t.dst) + '\" readonly /></td></tr>');\n\t\t}\n\n\t\tfunction paster() {\n\t\t\tvar t = f.shift();\n\t\t\tif (!t) {\n\t\t\t\ttoast.ok(2, r.ccp ? L.fcp_ok : L.fp_ok);\n\t\t\t\ttreectl.goto();\n\t\t\t\tr.tx(srcdir);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!t.dst)\n\t\t\t\treturn paster();\n\n\t\t\tvar msg = esc((r.ccp ? L.fcp_busy : L.fp_busy).format(f.length + 1, uricom_dec(t.src)));\n\t\t\tmsg += '\\n<a id=\"fs_abrt\" class=\"btn\" href=\"#\" onclick=\"fs_abrt()\">' + L.fs_abrt + '</a>';\n\t\t\ttoast.show('inf r', 0, msg);\n\n\t\t\tvar xhr = new XHR(),\n\t\t\t\tact = r.ccp ? '?copy=' : '?move=',\n\t\t\t\tdst = get_evpath() + uricom_enc(t.dst);\n\n\t\t\tabrt_key = randstr(9);\n\n\t\t\txhr.open('POST', t.src + act + dst + '&akey=' + abrt_key, true);\n\t\t\txhr.onload = xhr.onerror = paste_cb;\n\t\t\txhr.send();\n\t\t}\n\t\tfunction paste_cb() {\n\t\t\tif (this.status !== 201) {\n\t\t\t\tvar msg = unpre(this.responseText);\n\t\t\t\ttoast.err(9, (r.ccp ? L.fcp_err : L.fp_err) + msg);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (r.sn != sn)\n\t\t\t\treturn modal.confirm('WARNING: the paste was aborted');\n\n\t\t\tpaster();\n\t\t}\n\t\tfunction okgo() {\n\t\t\tpaster();\n\t\t\tjwrite('fman_clip', [Date.now()]);\n\t\t}\n\n\t\tif (!ui) {\n\t\t\tvar src = [];\n\t\t\tfor (var a = 0; a < f.length; a++)\n\t\t\t\tsrc.push(f[a].src);\n\n\t\t\treturn modal.confirm((r.ccp ? L.fcp_confirm : L.fp_confirm).format(f.length) + '<ul>' + uricom_adec(src, true).join('') + '</ul>', okgo, null);\n\t\t}\n\n\t\tvar rui = ebi('rui');\n\t\tif (!rui) {\n\t\t\trui = mknod('div', 'rui');\n\t\t\tdocument.body.appendChild(rui);\n\t\t}\n\t\thtml.push('</table>');\n\t\trui.innerHTML = html.join('\\n');\n\t\ttt.att(rui);\n\n\t\tfunction rn_apply(e) {\n\t\t\tfor (var a = 0; a < f.length; a++)\n\t\t\t\tif (!f[a].ok) {\n\t\t\t\t\ttoast.err(30, L.fp_emore);\n\t\t\t\t\treturn setcnmt(true);\n\t\t\t\t}\n\t\t\trn_cancel(e);\n\t\t\tokgo();\n\t\t}\n\t\tfunction rn_skip(e) {\n\t\t\tvar o = QSA('#rn_f tr.ng');\n\t\t\tfor (var a = o.length - 1; a >= 0; a--) {\n\t\t\t\tvar oo = o[a].querySelector('input');\n\t\t\t\too.value = '';\n\t\t\t\too.oninput.call(oo);\n\t\t\t}\n\t\t\trn_apply();\n\t\t}\n\t\tfunction rn_cancel(e) {\n\t\t\tev(e);\n\t\t\trui.parentNode.removeChild(rui);\n\t\t}\n\t\tebi('rn_cancel').onclick = rn_cancel;\n\t\tebi('rn_skip').onclick = rn_skip;\n\t\tebi('rn_apply').onclick = rn_apply;\n\n\t\tvar first_bad = 0;\n\t\tfunction setcnmt(sel) {\n\t\t\tvar nbad = 0;\n\t\t\tfor (var a = 0; a < f.length; a++) {\n\t\t\t\tif (f[a].ok)\n\t\t\t\t\tcontinue;\n\t\t\t\tif (!nbad)\n\t\t\t\t\tfirst_bad = a;\n\t\t\t\tnbad += 1;\n\t\t\t}\n\t\t\tebi('cnmt').innerHTML = (r.ccp ? L.fcp_ename : L.fp_ename).format(nbad);\n\t\t\tif (sel && nbad) {\n\t\t\t\tvar el = ebi('rn_new_' + first_bad);\n\t\t\t\tel.focus();\n\t\t\t\tel.setSelectionRange(0, el.value.lastIndexOf('.'), \"forward\");\n\t\t\t}\n\t\t}\n\t\tsetcnmt(true);\n\n\t\tfor (var a = 0; a < f.length; a++)\n\t\t\t(function (a) {\n\t\t\t\tvar inew = ebi('rn_new_' + a);\n\t\t\t\tinew.onkeydown = function (e) {\n\t\t\t\t\tif (((e.key || e.code) + '').endsWith('Enter'))\n\t\t\t\t\t\treturn rn_apply();\n\t\t\t\t};\n\t\t\t\tinew.oninput = function (e) {\n\t\t\t\t\tf[a].dst = this.value;\n\t\t\t\t\tf[a].ok = true;\n\t\t\t\t\tif (f[a].dst)\n\t\t\t\t\t\tfor (var b = 0; b < indir.length; b++)\n\t\t\t\t\t\t\tif (indir[b] == this.value)\n\t\t\t\t\t\t\t\tf[a].ok = false;\n\t\t\t\t\tclmod(this.closest('tr'), 'ng', !f[a].ok);\n\t\t\t\t\tsetcnmt();\n\t\t\t\t};\n\t\t\t})(a);\n\t}\n\n\tfunction onmsg(msg) {\n\t\tr.clip = null;\n\t\tvar n = parseInt('' + msg), tries = 0;\n\t\tvar fun = function () {\n\t\t\tif (n == msg && n > 1 && r.clip === null) {\n\t\t\t\tvar fc = jread('fman_clip', []);\n\t\t\t\tif (!fc || !fc.length || fc[0] != n) {\n\t\t\t\t\tif (++tries > 10)\n\t\t\t\t\t\treturn modal.alert(L.fp_etab);\n\n\t\t\t\t\treturn setTimeout(fun, 100);\n\t\t\t\t}\n\t\t\t}\n\t\t\tr.render();\n\t\t\tif (msg == get_evpath())\n\t\t\t\ttreectl.goto(msg);\n\t\t};\n\t\tfun();\n\t}\n\n\tif (r.bus)\n\t\tr.bus.onmessage = function (e) {\n\t\t\tonmsg(e ? e.data : 1)\n\t\t};\n\n\tr.tx = function (msg) {\n\t\tif (!r.bus)\n\t\t\treturn onmsg(msg);\n\n\t\tr.bus.postMessage(msg);\n\t\tr.bus.onmessage();\n\t};\n\n\tbcfg_bind(r, 'qdel', 'qdel', dqdel == 1);\n\n\tbren.onclick = r.rename;\n\tbdel.onclick = r.delete;\n\tbcut.onclick = r.cut;\n\tbcpy.onclick = r.cpy;\n\tbpst.onclick = r.paste;\n\tbshr.onclick = r.share;\n\n\treturn r;\n})();\n\n\nvar showfile = (function () {\n\tvar r = {\n\t\t'nrend': 0,\n\t};\n\tr.map = {\n\t\t'.asm': 'nasm',\n\t\t'.bas': 'basic',\n\t\t'.bat': 'batch',\n\t\t'.cxx': 'cpp',\n\t\t'.diz': 'ans',\n\t\t'.ex': 'elixir',\n\t\t'.exs': 'elixir',\n\t\t'.frag': 'glsl',\n\t\t'.h': 'c',\n\t\t'.hpp': 'cpp',\n\t\t'.htm': 'html',\n\t\t'.hxx': 'cpp',\n\t\t'.log': 'ans',\n\t\t'.m': 'matlab',\n\t\t'.moon': 'moonscript',\n\t\t'.nfo': 'ans',\n\t\t'.patch': 'diff',\n\t\t'.ps1': 'powershell',\n\t\t'.psm1': 'powershell',\n\t\t'.pl': 'perl',\n\t\t'.rs': 'rust',\n\t\t'.sh': 'bash',\n\t\t'.service': 'systemd',\n\t\t'.socket': 'systemd',\n\t\t'.timer': 'systemd',\n\t\t'.txt': 'ans',\n\t\t'.vb': 'vbnet',\n\t\t'.v': 'verilog',\n\t\t'.vert': 'glsl',\n\t\t'.vh': 'verilog',\n\t\t'.yml': 'yaml'\n\t};\n\tr.nmap = {\n\t\t'dockerfile': 'docker'\n\t};\n\tvar 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';\n\tx = x.split(/ +/g);\n\tfor (var a = 0; a < x.length; a++)\n\t\tif (!r.map[\".\" + x[a]])\n\t\t\tr.map[\".\" + x[a]] = x[a];\n\n\tr.sname = function (srch) {\n\t\treturn srch.split(/[?&]doc=/)[1].split('&')[0];\n\t};\n\n\tif (window.og_fn) {\n\t\tvar ext = og_fn.split(/\\./g).pop();\n\t\tif (r.map['.' + ext])\n\t\t\thist_replace(get_evpath() + '?doc=' + og_fn);\n\t}\n\n\twindow.Prism = { 'manual': true };\n\tvar em = QS('#bdoc>pre');\n\tif (em)\n\t\tem = [r.sname(location.search), location.hash, em.textContent];\n\telse {\n\t\tvar m = /[?&]doc=([^&]+)/.exec(location.search);\n\t\tif (m) {\n\t\t\tsetTimeout(function () {\n\t\t\t\tr.show(uricom_dec(m[1]), true);\n\t\t\t}, 1);\n\t\t}\n\t}\n\n\tr.setstyle = function () {\n\t\tif (window.no_prism)\n\t\t\treturn;\n\n\t\tqsr('#prism_css');\n\t\tvar el = mknod('link', 'prism_css');\n\t\tel.rel = 'stylesheet';\n\t\tel.href = SR + '/.cpr/w/deps/prism' + (light ? '' : 'd') + '.css?_=' + TS;\n\t\tdocument.head.appendChild(el);\n\t};\n\n\tr.active = function () {\n\t\treturn !!/[?&]doc=/.exec(location.search);\n\t};\n\n\tr.getlang = function (fn) {\n\t\tfn = fn.toLowerCase();\n\t\tvar ext = fn.slice(fn.lastIndexOf('.'));\n\t\treturn r.map[ext] || r.nmap[fn];\n\t}\n\n\tr.addlinks = function () {\n\t\tr.files = [];\n\t\tvar links = msel.getall();\n\t\tfor (var a = 0; a < links.length; a++) {\n\t\t\tvar link = links[a],\n\t\t\t\tfn = link.vp.split('/').pop(),\n\t\t\t\tlang = r.getlang(fn);\n\n\t\t\tif (!lang)\n\t\t\t\tcontinue;\n\n\t\t\tr.files.push({ 'id': link.id, 'name': uricom_dec(fn) });\n\n\t\t\tvar ah = ebi(link.id),\n\t\t\t\ttd = ah.closest('tr').getElementsByTagName('td')[0];\n\n\t\t\tif (ah.textContent.endsWith('/'))\n\t\t\t\tcontinue;\n\n\t\t\tif (lang == 'ts' || (lang == 'md' && td.textContent != '-'))\n\t\t\t\tcontinue;\n\n\t\t\ttd.innerHTML = '<a href=\"#\" id=\"t' +\n\t\t\t\tlink.id + '\" class=\"doc bri\" hl=\"' +\n\t\t\t\tlink.id + '\" rel=\"nofollow\">-txt-</a>';\n\n\t\t\ttd.getElementsByTagName('a')[0].setAttribute('href', '?doc=' + fn);\n\t\t}\n\t\tr.mktree();\n\t\tif (em) {\n\t\t\tif (r.taildoc || em[2] == '( size of textfile exceeds serverside limit )')\n\t\t\t\tr.show(em[0], true);\n\t\t\telse\n\t\t\t\trender(em);\n\t\t\tem = null;\n\t\t}\n\t};\n\n\tr.tail = function (url, no_push) {\n\t\tr.abrt = new AbortController();\n\t\twidget.setvis();\n\t\trender([url, '', ''], no_push);\n\t\tvar me = r.tail_id = Date.now(),\n\t\t\twfp = ebi('wfp'),\n\t\t\tedoc = ebi('doc'),\n\t\t\ttxt = '';\n\n\t\turl = addq(url, 'tail=-' + r.tailnb);\n\t\tfetch(url, {'signal': r.abrt.signal}).then(function(rsp) {\n\t\t\tvar ro = rsp.body.pipeThrough(\n\t\t\t\tnew TextDecoderStream('utf-8', {'fatal': false}),\n\t\t\t\t{'signal': r.abrt.signal}).getReader();\n\n\t\t\tvar rf = function() {\n\t\t\t\tro.read().then(function(v) {\n\t\t\t\t\tif (r.tail_id != me)\n\t\t\t\t\t\treturn;\n\t\t\t\t\tvar vt = v.done ? '\\n*** lost connection to copyparty ***' : v.value;\n\t\t\t\t\tif (vt == '\\x00')\n\t\t\t\t\t\treturn rf();\n\t\t\t\t\ttxt += vt;\n\t\t\t\t\tvar ofs = txt.length - r.tailnb;\n\t\t\t\t\tif (ofs > 0) {\n\t\t\t\t\t\tvar ofs2 = txt.indexOf('\\n', ofs);\n\t\t\t\t\t\tif (ofs2 >= ofs && ofs - ofs2 < 512)\n\t\t\t\t\t\t\tofs = ofs2;\n\t\t\t\t\t\ttxt = txt.slice(ofs);\n\t\t\t\t\t}\n\t\t\t\t\tvar html = esc(txt);\n\t\t\t\t\tif (r.tailansi)\n\t\t\t\t\t\thtml = r.ansify(html);\n\t\t\t\t\tedoc.innerHTML = html;\n\t\t\t\t\tif (r.tail2end)\n\t\t\t\t\t\twindow.scrollTo(0, wfp.offsetTop - window.innerHeight);\n\t\t\t\t\tif (!v.done)\n\t\t\t\t\t\trf();\n\t\t\t\t});\n\t\t\t};\n\t\t\tif (r.tail_id == me)\n\t\t\t\trf();\n\t\t});\n\t};\n\n\tr.untail = function () {\n\t\tif (!r.abrt)\n\t\t\treturn;\n\t\tr.abrt.abort();\n\t\tr.abrt = null;\n\t\tr.tail_id = -1;\n\t\twidget.setvis();\n\t};\n\n\tr.show = function (url, no_push) {\n\t\tr.untail();\n\t\tvar xhr = new XHR(),\n\t\t\tm = /[?&](k=[^&#]+)/.exec(url);\n\n\t\turl = url.split('?')[0] + (m ? '?' + m[1] : '');\n\t\tassert_vp(url);\n\t\tif (r.taildoc)\n\t\t\treturn r.tail(url, no_push);\n\n\t\txhr.url = url;\n\t\txhr.fname = uricom_dec(url.split('/').pop());\n\t\txhr.no_push = no_push;\n\t\txhr.ts = Date.now();\n\t\txhr.open('GET', url, true);\n\t\txhr.onprogress = loading;\n\t\txhr.onload = xhr.onerror = load_cb;\n\t\txhr.send();\n\t};\n\n\tfunction loading(e) {\n\t\tif (e.total < 1024 * 256)\n\t\t\treturn;\n\n\t\tvar m = L.tv_load.format(\n\t\t\tesc(this.fname),\n\t\t\tf2f(e.loaded * 100 / e.total, 1),\n\t\t\tf2f(e.loaded / 1024 / 1024, 1),\n\t\t\tf2f(e.total / 1024 / 1024, 1))\n\n\t\tif (!this.toasted) {\n\t\t\tthis.toasted = 1;\n\t\t\treturn toast.inf(573, m);\n\t\t}\n\t\tebi('toastb').innerHTML = lf2br(m);\n\t}\n\n\tfunction load_cb(e) {\n\t\tif (this.toasted)\n\t\t\ttoast.hide();\n\n\t\tif (!xhrchk(this, L.tv_xe1, L.tv_xe2))\n\t\t\treturn;\n\n\t\trender([this.url, '', this.responseText], this.no_push);\n\t}\n\n\tfunction render(doc, no_push) {\n\t\tr.q = null;\n\t\tr.nrend++;\n\t\tvar url = r.url = doc[0],\n\t\t\tlnh = doc[1],\n\t\t\ttxt = doc[2],\n\t\t\tname = url.split('?')[0].split('/').pop(),\n\t\t\ttname = uricom_dec(name),\n\t\t\tlang = r.getlang(name),\n\t\t\tis_md = lang == 'md';\n\n\t\tebi('files').style.display = ebi('gfiles').style.display = ebi('lazy').style.display = ebi('pro').style.display = ebi('epi').style.display = 'none';\n\t\tebi('dldoc').setAttribute('href', url);\n\t\tebi('editdoc').setAttribute('href', addq(url, 'edit'));\n\t\tebi('editdoc').style.display = (has(perms, 'write') && (re_rw_edit.test(name) || has(perms, 'delete'))) ? '' : 'none';\n\n\t\tvar wr = ebi('bdoc'),\n\t\t\tnrend = r.nrend,\n\t\t\tdefer = !Prism.highlightElement;\n\n\t\tvar fun = function (el) {\n\t\t\tif (r.nrend != nrend)\n\t\t\t\treturn;\n\n\t\t\ttry {\n\t\t\t\tif (lnh.slice(0, 5) == '#doc.')\n\t\t\t\t\tsethash(lnh.slice(1));\n\n\t\t\t\tel = el || QS('#doc>code');\n\t\t\t\tPrism.highlightElement(el);\n\t\t\t\tif (el.className == 'language-ans' || (!lang && /\\x1b\\[[0-9;]{0,16}m/.exec(txt.slice(0, 4096))))\n\t\t\t\t\tel.innerHTML = r.ansify(el.innerHTML);\n\t\t\t}\n\t\t\tcatch (ex) { }\n\t\t}\n\n\t\tvar skip_prism = !txt || txt.length > 1024 * 256;\n\t\tif (skip_prism) {\n\t\t\tfun = function (el) { };\n\t\t\tis_md = false;\n\t\t}\n\n\t\tqsr('#doc');\n\t\tvar el = mknod('pre', 'doc');\n\t\tel.setAttribute('tabindex', '0');\n\t\tclmod(ebi('wrap'), 'doc', !is_md);\n\t\tif (is_md) {\n\t\t\tshow_md(txt, name, el);\n\t\t}\n\t\telse {\n\t\t\tel.textContent = txt;\n\t\t\tel.innerHTML = '<code>' + el.innerHTML + '</code>';\n\t\t\tif (!window.no_prism && !skip_prism) {\n\t\t\t\tif ((lang == 'conf' || lang == 'cfg') && ('\\n' + txt).indexOf('\\n# -*- mode: yaml -*-') + 1)\n\t\t\t\t\tlang = 'yaml';\n\n\t\t\t\tel.className = 'prism linkable-line-numbers line-numbers language-' + lang;\n\t\t\t\tif (!defer)\n\t\t\t\t\tfun(el.firstChild);\n\t\t\t\telse\n\t\t\t\t\timport_js(SR + '/.cpr/w/deps/prism.js', function () { fun(); });\n\t\t\t}\n\t\t\tif (!txt && r.wrap)\n\t\t\t\tel.className = 'wrap';\n\t\t}\n\n\t\twr.appendChild(el);\n\t\twr.style.display = '';\n\t\tset_tabindex();\n\n\t\twintitle(tname + ' \\u2014 ');\n\t\tdocument.documentElement.scrollTop = 0;\n\t\tvar hfun = no_push ? hist_replace : hist_push;\n\t\thfun(get_evpath() + '?doc=' + name);  // can't dk: server wants dk and js needs fk\n\n\t\tqsr('#docname');\n\t\tel = mknod('span', 'docname');\n\t\tel.textContent = tname;\n\t\tebi('path').appendChild(el);\n\n\t\tr.updtree();\n\t\ttreectl.textmode(true);\n\t\ttree_scrollto();\n\t}\n\n\tr.ansify = function (html) {\n\t\tvar ctab = (light ?\n\t\t\t'bfbfbf d30253 497600 b96900 006fbb a50097 288276 2d2d2d 9f9f9f 943b55 3a5600 7f4f00 00507d 683794 004343 000000' :\n\t\t\t'404040 f03669 b8e346 ffa402 02a2ff f65be3 3da698 d2d2d2 606060 c75b79 c8e37e ffbe4a 71cbff b67fe3 9cf0ed ffffff').split(/ /g),\n\t\t\tsrc = html.split(/\\x1b\\[/g),\n\t\t\tout = ['<span>'], fg = 7, bg = null, bfg = 0, bbg = 0, inv = 0, bold = 0;\n\n\t\tfor (var a = 0; a < src.length; a++) {\n\t\t\tvar m = /^([0-9;]+)m/.exec(src[a]);\n\t\t\tif (!m) {\n\t\t\t\tif (a)\n\t\t\t\t\tout.push('\\x1b[');\n\n\t\t\t\tout.push(src[a]);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tvar cs = m[1].split(/;/g),\n\t\t\t\ttxt = src[a].slice(m[1].length + 1);\n\n\t\t\tfor (var b = 0; b < cs.length; b++) {\n\t\t\t\tvar c = parseInt(cs[b]);\n\t\t\t\tif (c == 0) {\n\t\t\t\t\tfg = 7;\n\t\t\t\t\tbg = null;\n\t\t\t\t\tbfg = bbg = bold = inv = 0;\n\t\t\t\t}\n\t\t\t\tif (c == 1) bfg = bold = 1;\n\t\t\t\tif (c == 7) inv = 1;\n\t\t\t\tif (c == 22) bfg = bold = 0;\n\t\t\t\tif (c == 27) inv = 0;\n\t\t\t\tif (c >= 30 && c <= 37) fg = c - 30;\n\t\t\t\tif (c >= 40 && c <= 47) bg = c - 40;\n\t\t\t\tif (c >= 90 && c <= 97) {\n\t\t\t\t\tfg = c - 90;\n\t\t\t\t\tbfg = 1;\n\t\t\t\t}\n\t\t\t\tif (c >= 100 && c <= 107) {\n\t\t\t\t\tbg = c - 100;\n\t\t\t\t\tbbg = 1;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar cfg = fg, cbg = bg;\n\t\t\tif (inv) {\n\t\t\t\tcbg = fg;\n\t\t\t\tcfg = bg || 0;\n\t\t\t}\n\n\t\t\tvar s = '</span><span style=\"color:#' + ctab[cfg + bfg * 8];\n\t\t\tif (cbg !== null)\n\t\t\t\ts += ';background:#' + ctab[cbg + bbg * 8];\n\t\t\tif (bold)\n\t\t\t\ts += ';font-weight:bold';\n\n\t\t\tout.push(s + '\">' + txt);\n\t\t}\n\t\treturn out.join('');\n\t};\n\n\tr.ppj = function (e) {\n\t\tebi(e);\n\t\ttry {\n\t\t\tr.ppj2();\n\t\t}\n\t\tcatch (ex) {\n\t\t\ttoast.err(10, '' + ex);\n\t\t}\n\t};\n\tr.ppj2 = function () {\n\t\tvar btn = ebi('dldoc'),\n\t\t\tel = ebi('doc'),\n\t\t\tt = el.textContent.trim(),\n\t\t\tjo = JSON.parse(t),\n\t\t\tjt = JSON.stringify(jo, null, t.indexOf('\\n') + 1 ? 0 : 2);\n\t\tel.textContent = jt;\n\t\tel.innerHTML = '<code>' + el.innerHTML + '</code>';\n\t\ttry {\n\t\t\tel = QS('#doc>code');\n\t\t\tel.className = 'language-json';\n\t\t\tPrism.highlightElement(el);\n\t\t}\n\t\tcatch (ex) { }\n        btn.setAttribute('download', ebi('docname').innerHTML);\n        btn.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(jt));\n\t};\n\n\tr.mktree = function () {\n\t\tvar top = get_evpath().slice(SR.length),\n\t\t\tcrumbs = linksplit(top).join('<span>/</span>'),\n\t\t\thtml = ['<li class=\"bn\">' + L.tv_lst + '<br />' + crumbs + '</li>'];\n\t\tfor (var a = 0; a < r.files.length; a++) {\n\t\t\tvar file = r.files[a];\n\t\t\thtml.push('<li><a href=\"?doc=' +\n\t\t\t\turicom_enc(file.name) + '\" hl=\"' + file.id +\n\t\t\t\t'\">' + esc(file.name) + '</a>');\n\t\t}\n\t\tebi('docul').innerHTML = html.join('\\n');\n\t};\n\n\tr.updtree = function () {\n\t\tvar fn = QS('#path span:last-child'),\n\t\t\tlis = QSA('#docul li a'),\n\t\t\tsels = msel.getsel(),\n\t\t\tactsel = false;\n\n\t\tfn = fn ? fn.textContent : '';\n\t\tfor (var a = 0, aa = lis.length; a < aa; a++) {\n\t\t\tvar lin = lis[a].textContent,\n\t\t\t\tsel = false;\n\n\t\t\tfor (var b = 0; b < sels.length; b++)\n\t\t\t\tif (vsplit(sels[b].vp)[1] == lin)\n\t\t\t\t\tsel = true;\n\n\t\t\tclmod(lis[a], 'hl', lin == fn);\n\t\t\tclmod(lis[a], 'sel', sel);\n\t\t\tif (lin == fn && sel)\n\t\t\t\tactsel = true;\n\t\t}\n\t\tclmod(ebi('seldoc'), 'sel', actsel);\n\t};\n\n\tr.tglsel = function () {\n\t\tvar fn = ebi('docname').textContent;\n\t\tfor (var a = 0; a < r.files.length; a++)\n\t\t\tif (r.files[a].name == fn)\n\t\t\t\tclmod(ebi(r.files[a].id).closest('tr'), 'sel', 't');\n\n\t\tmsel.selui();\n\t};\n\n\tr.tgltail = function () {\n\t\tif (!window.TextDecoderStream) {\n\t\t\tbcfg_set('taildoc', r.taildoc = false);\n\t\t\treturn toast.err(10, L.tail_2old);\n\t\t}\n\t\tr.show(r.url, true);\n\t};\n\n\tr.tglwrap = function () {\n\t\tr.show(r.url, true);\n\t};\n\n\tvar bdoc = ebi('bdoc');\n\tbdoc.className = 'line-numbers';\n\tbdoc.innerHTML = (\n\t\t'<div id=\"hdoc\" class=\"ghead\">\\n' +\n\t\t'<a href=\"#\" class=\"btn\" id=\"xdoc\" tt=\"' + L.tvt_close + '</a>\\n' +\n\t\t'<a href=\"#\" class=\"btn\" id=\"dldoc\" tt=\"' + L.tvt_dl + '</a>\\n' +\n\t\t'<a href=\"#\" class=\"btn\" id=\"prevdoc\" tt=\"' + L.tvt_prev + '</a>\\n' +\n\t\t'<a href=\"#\" class=\"btn\" id=\"nextdoc\" tt=\"' + L.tvt_next + '</a>\\n' +\n\t\t'<a href=\"#\" class=\"btn\" id=\"seldoc\" tt=\"' + L.tvt_sel + '</a>\\n' +\n\t\t'<a href=\"#\" class=\"btn\" id=\"ppjdoc\" tt=\"' + L.tvt_j + '</a>\\n' +\n\t\t'<a href=\"#\" class=\"btn\" id=\"editdoc\" tt=\"' + L.tvt_edit + '</a>\\n' +\n\t\t'<a href=\"#\" class=\"btn tgl\" id=\"taildoc\" tt=\"' + L.tvt_tail + '</a>\\n' +\n\t\t'<div id=\"tailbtns\">\\n' +\n\t\t'<a href=\"#\" class=\"btn tgl\" id=\"wrapdoc\" tt=\"' + L.tvt_wrap + '</a>\\n' +\n\t\t'<a href=\"#\" class=\"btn tgl\" id=\"tail2end\" tt=\"' + L.tvt_atail + '</a>\\n' +\n\t\t'<a href=\"#\" class=\"btn tgl\" id=\"tailansi\" tt=\"' + L.tvt_ctail + '</a>\\n' +\n\t\t'<input type=\"text\" id=\"tailnb\" value=\"\" ' + NOAC + ' style=\"width:4em\" tt=\"' + L.tvt_ntail + '\" />' +\n\t\t'</div>\\n' +\n\t\t'</div>'\n\t);\n\tebi('xdoc').onclick = function () {\n\t\tr.untail();\n\t\tthegrid.setvis(true);\n\t\tbcfg_bind(r, 'taildoc', 'taildoc', false, r.tgltail);\n\t};\n\tebi('dldoc').setAttribute('download', '');\n\tebi('prevdoc').onclick = function () { tree_neigh(-1); };\n\tebi('nextdoc').onclick = function () { tree_neigh(1); };\n\tebi('seldoc').onclick = r.tglsel;\n\tebi('ppjdoc').onclick = r.ppj;\n\tbcfg_bind(r, 'wrap', 'wrapdoc', true, r.tglwrap);\n\tbcfg_bind(r, 'taildoc', 'taildoc', false, r.tgltail);\n\tbcfg_bind(r, 'tail2end', 'tail2end', true);\n\tbcfg_bind(r, 'tailansi', 'tailansi', false, r.tgltail);\n\n\tr.tailnb = ebi('tailnb').value = icfg_get('tailnb', 131072);\n\tebi('tailnb').oninput = function (e) {\n\t\tswrite('tailnb', r.tailnb = this.value);\n\t};\n\n\tif (/[?&]tail\\b/.exec(sloc0)) {\n\t\tclmod(ebi('taildoc'), 'on', 1);\n\t\tr.taildoc = true;\n\t}\n\n\treturn r;\n})();\n\n\nvar thegrid = (function () {\n\tvar lfiles = ebi('files'),\n\t\tgfiles = mknod('div', 'gfiles');\n\n\tgfiles.style.display = 'none';\n\tgfiles.innerHTML = (\n\t\t'<div id=\"ghead\" class=\"ghead\">' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"gridvau\" tt=\"' + L.gt_vau + '</a> ' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"gridsel\" tt=\"' + L.gt_msel + '</a> ' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"gridcrop\" tt=\"' + L.gt_crop + '</a> ' +\n\t\t'<a href=\"#\" class=\"tgl btn\" id=\"grid3x\" tt=\"' + L.gt_3x + '</a> ' +\n\t\t'<span>' + L.gt_zoom + ': ' +\n\t\t'<a href=\"#\" class=\"btn\" z=\"-1.1\" tt=\"Hotkey: shift-A\">&ndash;</a> ' +\n\t\t'<a href=\"#\" class=\"btn\" z=\"1.1\" tt=\"Hotkey: shift-D\">+</a></span> <span>' + L.gt_chop + ': ' +\n\t\t'<a href=\"#\" class=\"btn\" l=\"-1\" tt=\"' + L.gt_c1 + '\">&ndash;</a> ' +\n\t\t'<a href=\"#\" class=\"btn\" l=\"1\" tt=\"' + L.gt_c2 + '\">+</a></span> <span>' + L.gt_sort + ': ' +\n\t\t'<a href=\"#\" s=\"href\">' + L.gt_name + '</a> ' +\n\t\t'<a href=\"#\" s=\"sz\">' + L.gt_sz + '</a> ' +\n\t\t'<a href=\"#\" s=\"ts\">' + L.gt_ts + '</a> ' +\n\t\t'<a href=\"#\" s=\"ext\">' + L.gt_ext + '</a>' +\n\t\t'</span></div>' +\n\t\t'<div id=\"ggrid\"></div>'\n\t);\n\tlfiles.parentNode.insertBefore(gfiles, lfiles);\n\tvar ggrid = ebi('ggrid');\n\n\tvar r = {\n\t\t'sz': clamp(fcfg_get('gridsz', 10), 4, 80),\n\t\t'ln': clamp(icfg_get('gridln', 3), 1, 7),\n\t\t'isdirty': true,\n\t\t'bbox': null\n\t};\n\n\tvar btnclick = function (e) {\n\t\tev(e);\n\t\tvar s = this.getAttribute('s'),\n\t\t\tz = this.getAttribute('z'),\n\t\t\tl = this.getAttribute('l');\n\n\t\tif (z)\n\t\t\treturn setsz(z > 0 ? r.sz * z : r.sz / (-z));\n\n\t\tif (l)\n\t\t\treturn setln(parseInt(l));\n\n\t\tvar t = lfiles.tHead.rows[0].cells;\n\t\tfor (var a = 0; a < t.length; a++)\n\t\t\tif (t[a].getAttribute('name') == s) {\n\t\t\t\tt[a].click();\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\tr.setdirty();\n\t};\n\n\tvar links = QSA('#ghead a');\n\tfor (var a = 0; a < links.length; a++)\n\t\tlinks[a].onclick = btnclick;\n\n\tr.setvis = function (force) {\n\t\tif (showfile.active()) {\n\t\t\tif (!force)\n\t\t\t\treturn;\n\n\t\t\thist_push(get_evpath() + (dk ? '?k=' + dk : ''));\n\t\t\twintitle();\n\t\t}\n\n\t\tlfiles = ebi('files');\n\t\tgfiles = ebi('gfiles');\n\t\tggrid = ebi('ggrid');\n\n\t\tvar vis = has(perms, \"read\");\n\t\tgfiles.style.display = vis && r.en ? '' : 'none';\n\t\tlfiles.style.display = vis && !r.en ? '' : 'none';\n\t\tclmod(ggrid, 'crop', r.crop);\n\t\tclmod(ggrid, 'nocrop', !r.crop);\n\t\tebi('pro').style.display = ebi('epi').style.display = ebi('lazy').style.display = ebi('treeul').style.display = ebi('treepar').style.display = '';\n\t\tebi('bdoc').style.display = 'none';\n\t\tclmod(ebi('wrap'), 'doc');\n\t\tqsr('#docname');\n\t\tif (treectl)\n\t\t\ttreectl.textmode(false);\n\n\t\tif (filecols)\n\t\t\tfilecols.uivis();\n\n\t\taligngriditems();\n\t\trestore_scroll();\n\t};\n\n\tr.setdirty = function () {\n\t\tr.dirty = true;\n\t\tif (r.en)\n\t\t\tloadgrid();\n\t\telse\n\t\t\tr.setvis();\n\t};\n\n\tfunction setln(v) {\n\t\tif (v) {\n\t\t\tr.ln += v;\n\t\t\tif (r.ln < 1) r.ln = 1;\n\t\t\tif (r.ln > 7) r.ln = v < 0 ? 7 : 99;\n\t\t\tswrite('gridln', r.ln);\n\t\t\tsetTimeout(r.tippen, 20);\n\t\t}\n\t\tsetcvar('--grid-ln', r.ln);\n\t}\n\tsetln();\n\n\tfunction setsz(v) {\n\t\tif (v !== undefined) {\n\t\t\tr.sz = clamp(v, 4, 80);\n\t\t\tswrite('gridsz', r.sz);\n\t\t\tsetTimeout(r.tippen, 20);\n\t\t}\n\t\tsetcvar('--grid-sz', r.sz + 'em');\n\t\taligngriditems();\n\t}\n\tsetsz();\n\n\tfunction gclick1(e) {\n\t\tif (ctrl(e) && !treectl.csel && !r.sel)\n\t\t\treturn true;\n\n\t\treturn gclick.call(this, e, false);\n\t}\n\n\tfunction gclick2(e) {\n\t\tif (ctrl(e) || !r.sel)\n\t\t\treturn true;\n\n\t\treturn gclick.call(this, e, true);\n\t}\n\n\tfunction gclick(e, dbl) {\n\t\tvar oth = ebi(this.getAttribute('ref')),\n\t\t\tqhref = this.getAttribute('href'),\n\t\t\thref = qhref.split('?')[0],\n\t\t\tfid = oth.getAttribute('id'),\n\t\t\taplay = ebi('a' + fid),\n\t\t\tatext = ebi('t' + fid),\n\t\t\tis_txt = atext && !/\\.ts$/.test(href) && showfile.getlang(href),\n\t\t\tis_img = img_re.test(href),\n\t\t\tis_dir = href.endsWith('/'),\n\t\t\tis_srch = !!ebi('unsearch'),\n\t\t\tin_tree = is_dir && treectl.find(oth.textContent.slice(0, -1)),\n\t\t\thave_sel = QS('#files tr.sel'),\n\t\t\ttd = oth.closest('td').nextSibling,\n\t\t\ttr = td.parentNode;\n\n\t\tif (!is_srch && ((r.sel && !dbl && !ctrl(e)) || (treectl.csel && (e.shiftKey || ctrl(e))))) {\n\t\t\ttd.onclick.call(td, e);\n\t\t\tif (e.shiftKey)\n\t\t\t\treturn r.loadsel();\n\t\t\tclmod(this, 'sel', clgot(tr, 'sel'));\n\t\t}\n\t\telse if (in_tree && !have_sel)\n\t\t\tin_tree.click();\n\n\t\telse if (oth.hasAttribute('download'))\n\t\t\toth.click();\n\n\t\telse if (aplay && (r.vau || !is_img))\n\t\t\taplay.click();\n\n\t\telse if (is_dir && !have_sel)\n\t\t\ttreectl.reqls(qhref, true);\n\n\t\telse if (is_txt && !has(['md', 'htm', 'html'], is_txt))\n\t\t\tatext.click();\n\n\t\telse if (!is_img && have_sel)\n\t\t\twindow.open(qhref, '_blank');\n\n\t\telse {\n\t\t\tif (!dbl)\n\t\t\t\treturn true;\n\n\t\t\tsetTimeout(function () {\n\t\t\t\tr.sel = true;\n\t\t\t}, 1);\n\t\t\tr.sel = false;\n\t\t\tthis.click();\n\t\t}\n\t\tev(e);\n\t}\n\n\tr.imshow = function (url) {\n\t\tvar sel = '#ggrid>a'\n\t\tif (!thegrid.en) {\n\t\t\tthegrid.bagit('#files');\n\t\t\tsel = '#files a[id]';\n\t\t}\n\t\tvar ims = QSA(sel);\n\t\tfor (var a = 0, aa = ims.length; a < aa; a++) {\n\t\t\tvar iu = ims[a].getAttribute('href').split('?')[0].split('/').slice(-1)[0];\n\t\t\tif (iu == url)\n\t\t\t\treturn ims[a].click();\n\t\t}\n\t\tbaguetteBox.hide();\n\t};\n\n\tr.loadsel = function () {\n\t\tif (r.dirty)\n\t\t\treturn;\n\n\t\tvar ths = QSA('#ggrid>a');\n\n\t\tfor (var a = 0, aa = ths.length; a < aa; a++) {\n\t\t\tvar tr = ebi(ths[a].getAttribute('ref')).closest('tr'),\n\t\t\t\tcl = tr.className || '';\n\n\t\t\tif (noq_href(ths[a]).endsWith('/'))\n\t\t\t\tcl += ' dir';\n\n\t\t\tths[a].className = cl;\n\t\t}\n\n\t\tvar sp = ['unsearch', 'moar'];\n\t\tfor (var a = 0; a < sp.length; a++)\n\t\t\t(function (a) {\n\t\t\t\tvar o = QS('#ggrid a[ref=\"' + sp[a] + '\"]');\n\t\t\t\tif (o)\n\t\t\t\t\to.onclick = function (e) {\n\t\t\t\t\t\tev(e);\n\t\t\t\t\t\tebi(sp[a]).click();\n\t\t\t\t\t};\n\t\t\t})(a);\n\t};\n\n\tr.tippen = function () {\n\t\tvar els = QSA('#ggrid>a>span'),\n\t\t\taa = els.length;\n\n\t\tif (!aa)\n\t\t\treturn;\n\n\t\tvar cs = window.getComputedStyle(els[0]),\n\t\t\tfs = parseFloat(cs.lineHeight),\n\t\t\tpad = parseFloat(cs.paddingTop),\n\t\t\tpels = [],\n\t\t\ttodo = [];\n\n\t\tfor (var a = 0; a < aa; a++) {\n\t\t\tvar vis = Math.round((els[a].offsetHeight - pad) / fs),\n\t\t\t\tall = Math.round((els[a].scrollHeight - pad) / fs),\n\t\t\t\tpar = els[a].parentNode;\n\n\t\t\tpels.push(par);\n\t\t\ttodo.push(vis < all ? par.getAttribute('ttt') : null);\n\t\t}\n\n\t\tfor (var a = 0; a < todo.length; a++) {\n\t\t\tif (todo[a])\n\t\t\t\tpels[a].setAttribute('tt', todo[a]);\n\t\t\telse\n\t\t\t\tpels[a].removeAttribute('tt');\n\t\t}\n\n\t\ttt.att(ggrid);\n\t};\n\n\tfunction loadgrid() {\n\t\tif (have_webp === null || have_jxl === null)\n\t\t\treturn setTimeout(loadgrid, 50);\n\n\t\tr.setvis();\n\t\tif (!r.dirty)\n\t\t\treturn r.loadsel();\n\n\t\tif (dcrop.startsWith('f') || !sread('gridcrop'))\n\t\t\tbcfg_upd_ui('gridcrop', r.crop = ('y' == dcrop.slice(-1)));\n\n\t\tif (dth3x.startsWith('f') || !sread('grid3x'))\n\t\t\tbcfg_upd_ui('grid3x', r.x3 = ('y' == dth3x.slice(-1)));\n\n\t\tvar html = [],\n\t\t\tsvgs = new Set(),\n\t\t\tmax_svgs = CHROME ? 500 : 5000,\n\t\t\tneed_ext = !r.thumbs || !!ext_th,\n\t\t\tuse_ext_th = r.thumbs && ext_th,\n\t\t\tfiles = QSA('#files>tbody>tr>td:nth-child(2) a[id]');\n\n\t\tfor (var a = 0, aa = files.length; a < aa; a++) {\n\t\t\tvar ao = files[a],\n\t\t\t\tohref = esc(ao.getAttribute('href')),\n\t\t\t\thref = ohref.split('?')[0],\n\t\t\t\text = '',\n\t\t\t\text0 = '',\n\t\t\t\tname = uricom_dec(vsplit(href)[1]),\n\t\t\t\tref = ao.getAttribute('id'),\n\t\t\t\tisdir = href.endsWith('/'),\n\t\t\t\tac = isdir ? ' class=\"dir\"' : '',\n\t\t\t\tihref = ohref;\n\n\t\t\tif (need_ext && href != \"#\") {\n\t\t\t\tvar ar = href.split('.');\n\t\t\t\tif (ar.length > 1)\n\t\t\t\t\tar.shift();\n\n\t\t\t\tar.reverse();\n\t\t\t\text0 = ar[0];\n\t\t\t\tfor (var b = 0; b < Math.min(2, ar.length); b++) {\n\t\t\t\t\tif (ar[b].length > 7)\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\text = ext ? (ar[b] + '.' + ext) : ar[b];\n\t\t\t\t}\n\t\t\t\tif (!ext)\n\t\t\t\t\text = 'unk';\n\t\t\t}\n\n\t\t\tif (use_ext_th && (ext_th[ext] || ext_th[ext0])) {\n\t\t\t\tihref = ext_th[ext] || ext_th[ext0];\n\t\t\t}\n\t\t\telse if (r.thumbs) {\n\t\t\t\tihref = addq(ihref, 'th=' + (\n\t\t\t\t\thave_jxl  ? 'x' :\n\t\t\t\t\thave_webp ? 'w' :\n\t\t\t\t\t            'j'\n\t\t\t\t));\n\t\t\t\tif (!r.crop)\n\t\t\t\t\tihref += 'f';\n\t\t\t\tif (r.x3)\n\t\t\t\t\tihref += '3';\n\t\t\t\tif (href == \"#\")\n\t\t\t\t\tihref = SR + '/.cpr/ico/' + (ref == 'moar' ? '++' : 'exit');\n\t\t\t}\n\t\t\telse if (isdir) {\n\t\t\t\tihref = SR + '/.cpr/ico/folder';\n\t\t\t}\n\t\t\telse {\n\t\t\t\tif (!svgs.has(ext)) {\n\t\t\t\t\tif (svgs.size < max_svgs)\n\t\t\t\t\t\tsvgs.add(ext);\n\t\t\t\t\telse\n\t\t\t\t\t\text = \"unk\";\n\t\t\t\t}\n\t\t\t\tihref = SR + '/.cpr/ico/' + ext;\n\t\t\t}\n\t\t\tihref = addq(ihref, 'cache=i&_=' + ACB + TS);\n\t\t\tif (CHROME)\n\t\t\t\tihref += \"&raster\";\n\n\t\t\thtml.push('<a href=\"' + ohref + '\" ref=\"' + ref +\n\t\t\t\t'\"' + ac + ' ttt=\"' + esc(name) + '\"><img style=\"height:' +\n\t\t\t\t(r.sz / 1.25) + 'em\" loading=\"lazy\" onload=\"th_onload(this)\" src=\"' +\n\t\t\t\tihref + '\" /><span' + ac + '>' + ao.innerHTML + '</span></a>');\n\t\t}\n\t\tggrid.innerHTML = html.join('\\n');\n\t\tclmod(ggrid, 'crop', r.crop);\n\t\tclmod(ggrid, 'nocrop', !r.crop);\n\n\t\tvar srch = ebi('unsearch'),\n\t\t\tgsel = ebi('gridsel');\n\n\t\tgsel.style.display = srch ? 'none' : '';\n\t\tif (srch && r.sel)\n\t\t\tgsel.click();\n\n\t\tvar ths = QSA('#ggrid>a');\n\t\tfor (var a = 0, aa = ths.length; a < aa; a++) {\n\t\t\tths[a].ondblclick = gclick2;\n\t\t\tths[a].onclick = gclick1;\n\t\t}\n\n\t\tr.dirty = false;\n\t\tr.bagit('#ggrid');\n\t\tr.loadsel();\n\t\taligngriditems();\n\t\tsetTimeout(r.tippen, 20);\n\t}\n\n\tr.bagit = function (isrc) {\n\t\tif (!window.baguetteBox)\n\t\t\treturn;\n\n\t\tif (r.bbox)\n\t\t\tbaguetteBox.destroy();\n\n\t\tvar br = baguetteBox.run(isrc, {\n\t\t\tnoScrollbars: true,\n\t\t\tduringHide: r.onhide,\n\t\t\tafterShow: function () {\n\t\t\t\tr.bbox_opts.refocus = true;\n\t\t\t},\n\t\t\tcaptions: function (g, idx) {\n\t\t\t\tvar h = '' + g;\n\n\t\t\t\treturn '<a download href=\"' + h +\n\t\t\t\t\t'\">' + (idx + 1) + ' / ' + this.length + ' -- ' +\n\t\t\t\t\tesc(uricom_dec(h.split('/').pop())) + '</a>';\n\t\t\t},\n\t\t\tonChange: function (i, maxIdx) {\n\t\t\t\tif (this[i].imageElement) {\n\t\t\t\t\tsethash('g' + this[i].imageElement.getAttribute('ref') + getsort());\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tr.bbox = true;\n\t\tr.bbox_opts = br[1];\n\t};\n\n\tr.onhide = function () {\n\t\tafilt.apply();\n\n\t\tif (!thegrid.ihop)\n\t\t\treturn;\n\n\t\ttry {\n\t\t\tvar el = QS('#ggrid a[ref=\"' + location.hash.slice(2) + '\"]'),\n\t\t\t\tf = function () {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tel.focus();\n\t\t\t\t\t}\n\t\t\t\t\tcatch (ex) { }\n\t\t\t\t};\n\n\t\t\tf();\n\t\t\tsetTimeout(f, 10);\n\t\t\tsetTimeout(f, 100);\n\t\t\tsetTimeout(f, 200);\n\t\t\t// thx fullscreen api\n\n\t\t\tif (ANIM) {\n\t\t\t\tclmod(el, 'glow', 1);\n\t\t\t\tsetTimeout(function () {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tclmod(el, 'glow');\n\t\t\t\t\t}\n\t\t\t\t\tcatch (ex) { }\n\t\t\t\t}, 600);\n\t\t\t}\n\t\t\tr.bbox_opts.refocus = false;\n\t\t}\n\t\tcatch (ex) {\n\t\t\tconsole.log('ihop:', ex);\n\t\t}\n\t};\n\n\tr.set_crop = function (en) {\n\t\tif (!dcrop.startsWith('f'))\n\t\t\treturn r.setdirty();\n\n\t\tr.crop = dcrop.endsWith('y');\n\t\tbcfg_upd_ui('gridcrop', r.crop);\n\t\tif (r.crop != en)\n\t\t\ttoast.warn(10, L.ul_btnlk);\n\t};\n\n\tr.set_x3 = function (en) {\n\t\tif (!dth3x.startsWith('f'))\n\t\t\treturn r.setdirty();\n\n\t\tr.x3 = dth3x.endsWith('y');\n\t\tbcfg_upd_ui('grid3x', r.x3);\n\t\tif (r.x3 != en)\n\t\t\ttoast.warn(10, L.ul_btnlk);\n\t};\n\n\tif (/[?&]grid\\b/.exec(sloc0))\n\t\tswrite('griden', /[?&]grid=0\\b/.exec(sloc0) ? 0 : 1)\n\n\tif (/[?&]thumb\\b/.exec(sloc0))\n\t\tswrite('thumbs', /[?&]thumb=0\\b/.exec(sloc0) ? 0 : 1)\n\n\tif (/[?&]imgs\\b/.exec(sloc0)) {\n\t\tvar n = /[?&]imgs=0\\b/.exec(sloc0) ? 0 : 1;\n\t\tswrite('griden', n);\n\t\tif (n)\n\t\t\tswrite('thumbs', 1);\n\t}\n\n\tbcfg_bind(r, 'thumbs', 'thumbs', true, r.setdirty);\n\tbcfg_bind(r, 'ihop', 'ihop', true);\n\tbcfg_bind(r, 'vau', 'gridvau', false);\n\tbcfg_bind(r, 'crop', 'gridcrop', !dcrop.endsWith('n'), r.set_crop);\n\tbcfg_bind(r, 'x3', 'grid3x', dth3x.endsWith('y'), r.set_x3);\n\tbcfg_bind(r, 'sel', 'gridsel', false, r.loadsel);\n\tbcfg_bind(r, 'en', 'griden', dgrid, function (v) {\n\t\tv ? loadgrid() : r.setvis(true);\n\t\tpbar.onresize();\n\t\tvbar.onresize();\n\t});\n\tebi('wtgrid').onclick = ebi('griden').onclick;\n\n\treturn r;\n})();\n\n\nfunction th_onload(el) {\n\tel.style.height = '';\n}\n\n\nfunction tree_scrollto(e) {\n\tev(e);\n\ttree_scrolltoo('#treeul a.hl');\n\ttree_scrolltoo('#docul a.hl');\n}\n\n\nfunction tree_scrolltoo(q) {\n\tvar act = QS(q),\n\t\tul = act ? act.offsetParent : null;\n\n\tif (!ul)\n\t\treturn;\n\n\tvar ctr = ebi('tree'),\n\t\tem = parseFloat(getComputedStyle(act).fontSize),\n\t\ttop = act.offsetTop + ul.offsetTop,\n\t\tmin = top - 20 * em,\n\t\tmax = top - (ctr.offsetHeight - 16 * em);\n\n\tif (ctr.scrollTop > min)\n\t\tctr.scrollTop = Math.floor(min);\n\telse if (ctr.scrollTop < max)\n\t\tctr.scrollTop = Math.floor(max);\n}\n\n\nfunction tree_neigh(n, ratelimit) {\n\tif (ratelimit && QS('.dumb_loader_thing') && Date.now() - treectl.busied < 5)\n\t\treturn;\n\n\tvar links = QSA(showfile.active() || treectl.texts ? '#docul li>a' : '#treeul li>a+a');\n\tif (!links.length) {\n\t\ttreectl.dir_cb = function () {\n\t\t\ttree_neigh(n);\n\t\t\ttreectl.detree();\n\t\t};\n\t\ttreectl.entree(null, true);\n\t\treturn;\n\t}\n\tvar act = -1;\n\tfor (var a = 0, aa = links.length; a < aa; a++) {\n\t\tif (clgot(links[a], 'hl')) {\n\t\t\tact = a;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (act == -1 && !treectl.texts)\n\t\treturn;\n\n\tact += n;\n\tif (act < 0)\n\t\tact = links.length - 1;\n\tif (act >= links.length)\n\t\tact = 0;\n\n\tif (showfile.active())\n\t\tlinks[act].click();\n\telse\n\t\ttreectl.treego.call(links[act]);\n}\n\n\nfunction tree_up(justgo) {\n\tif (showfile.active())\n\t\treturn thegrid.setvis(true);\n\n\tvar act = QS('#treeul a.hl');\n\tif (!act) {\n\t\ttreectl.dir_cb = function () {\n\t\t\ttree_up(justgo);\n\t\t\ttreectl.detree();\n\t\t};\n\t\ttreectl.entree(null, true);\n\t\treturn;\n\t}\n\tif (act.previousSibling.textContent == '-') {\n\t\tact.previousSibling.click();\n\t\tif (!justgo)\n\t\t\treturn;\n\t}\n\tvar a = act.parentNode.parentNode.parentNode.getElementsByTagName('a')[1];\n\tif (a.parentNode.tagName == 'LI')\n\t\ta.click();\n}\n\n\nfunction hkhelp() {\n\tvar html = [];\n\tfor (var ic = 0; ic < L.hks.length; ic++) {\n\t\tvar c = L.hks[ic];\n\t\thtml.push('<table>');\n\t\tfor (var a = 0; a < c.length; a++)\n\t\t\ttry {\n\t\t\t\tif (!Array.isArray(c[a]))\n\t\t\t\t\thtml.push('<tr><th colspan=\"2\">' + esc(c[a]) + '</th></tr>');\n\t\t\t\telse {\n\t\t\t\t\tvar t1 = c[a][0].replace('⇧', '<b>⇧</b>');\n\t\t\t\t\thtml.push('<tr><td>{0}</td><td>{1}</td></tr>'.format(t1, c[a][1]));\n\t\t\t\t}\n\t\t\t}\n\t\t\tcatch (ex) {\n\t\t\t\thtml.push(\">>> \" + c[a]);\n\t\t\t}\n\n\t\thtml.push('</table>');\n\t}\n\tqsr('#hkhelp');\n\tvar o = mknod('div', 'hkhelp');\n\to.innerHTML = html.join('\\n');\n\tdocument.body.appendChild(o);\n}\n\n\nvar fselgen, fselctr;\nfunction fselfunw(e, ae, d, rem) {\n\tfselctr = 0;\n\tvar gen = fselgen = Date.now();\n\tif (rem)\n\t\trem *= window.innerHeight;\n\n\tvar selfun = function () {\n\t\tvar el = ae[d + 'ElementSibling'];\n\t\tif (!el || gen != fselgen)\n\t\t\treturn;\n\n\t\tel.focus();\n\t\tvar elh = el.offsetHeight;\n\t\tif (ctrl(e))\n\t\t\tdocument.documentElement.scrollTop += (d == 'next' ? 1 : -1) * elh;\n\n\t\tif (e.shiftKey) {\n\t\t\tclmod(el, 'sel', 't');\n\t\t\tmsel.origin_tr(el);\n\t\t\tmsel.selui();\n\t\t}\n\n\t\trem -= elh;\n\t\tif (rem > 0) {\n\t\t\tae = document.activeElement;\n\t\t\tif (++fselctr % 5 && rem > elh * (FIREFOX ? 5 : 2))\n\t\t\t\tselfun();\n\t\t\telse\n\t\t\t\tsetTimeout(selfun, 1);\n\t\t}\n\t}\n\tselfun();\n}\nvar konmai = 0, konmak = (function() {\n\tvar u = \"arrowup\",\n\t\td = \"arrowdown\",\n\t\tl = \"arrowleft\",\n\t\tr = \"arrowright\";\n\treturn [u, u, d, d, l, r, l, r, \"b\", \"a\", \"enter\"];\n})();\nvar ahotkeys = function (e) {\n\tif (e.altKey || e.isComposing)\n\t\treturn;\n\n\tif (QS('#bbox-overlay.visible') || modal.busy)\n\t\treturn;\n\n\tvar k = (e.key || e.code) + '', pos = -1, n,\n\t\tsh = e.shiftKey,\n\t\tae = document.activeElement,\n\t\taet = ae && ae != document.body ? ae.nodeName.toLowerCase() : '';\n\n\tif (k.startsWith('Key'))\n\t\tk = k.slice(3);\n\telse if (k.startsWith('Digit'))\n\t\tk = k.slice(5);\n\n\tvar kl = k.toLowerCase();\n\n\tif (dbg_kbd)\n\t\tconsole.log('KBD', k, kl, e.key, e.code, e.keyCode, e.which);\n\n\tif (konmai < 0)\n\t\tnoop();\n\telse if (konmak[konmai] != kl)\n\t\tkonmai = konmai && kl == konmak[0] ? (konmai<3?konmai:1):0;\n\telse if (++konmai >= konmak.length) {\n\t\tkonmai = -1;\n\t\tdocument.documentElement.scrollTop = 0;\n\t\tsettheme.go(6);\n\t\tstart_actx();\n\t\tsfx_nice();\n\t\ttoast.inf(9, 'omega clearance granted', null, 'top');\n\t\tsetTimeout(function() {\n\t\t\tapply_perms(treectl.lsc);\n\t\t\tfileman.render();\n\t\t}, 573);\n\t\treturn ev(e);\n\t}\n\n\tif (k == 'Escape' || k == 'Esc') {\n\t\tae && ae.blur();\n\t\ttt.hide();\n\n\t\tif (ebi('hkhelp'))\n\t\t\treturn qsr('#hkhelp');\n\n\t\tif (ebi('rcm').style.display)\n\t\t\treturn rcm.hide();\n\n\t\tif (toast.visible)\n\t\t\treturn toast.hide();\n\n\t\tif (ebi('rn_cancel'))\n\t\t\treturn ebi('rn_cancel').click();\n\n\t\tif (ebi('sh_abrt'))\n\t\t\treturn ebi('sh_abrt').click();\n\n\t\tif (QS('.opview.act'))\n\t\t\treturn QS('#ops>a').click();\n\n\t\tif (widget.is_open)\n\t\t\treturn widget.close();\n\n\t\tif (showfile.active())\n\t\t\treturn thegrid.setvis(true);\n\n\t\tif (!treectl.hidden)\n\t\t\treturn treectl.detree();\n\n\t\tif (QS('#unsearch'))\n\t\t\treturn QS('#unsearch').click();\n\n\t\tif (thegrid.en)\n\t\t\treturn ebi('griden').click();\n\t}\n\n\tvar in_ftab = (aet == 'tr' || aet == 'td') && ae.closest('#files');\n\tif (in_ftab) {\n\t\tvar d = '', rem = 0;\n\t\tif (aet == 'td') ae = ae.closest('tr'); //ie11\n\t\tif (k == 'ArrowUp' || k == 'Up') d = 'previous';\n\t\tif (k == 'ArrowDown' || k == 'Down') d = 'next';\n\t\tif (k == 'PageUp') { d = 'previous'; rem = 0.6; }\n\t\tif (k == 'PageDown') { d = 'next'; rem = 0.6; }\n\t\tif (d) {\n\t\t\tfselfunw(e, ae, d, rem);\n\t\t\treturn ev(e);\n\t\t}\n\t\tif (k == 'Space' || k == 'Spacebar' || k == ' ') {\n\t\t\tclmod(ae, 'sel', 't');\n\t\t\tmsel.origin_tr(ae);\n\t\t\tmsel.selui();\n\t\t\treturn ev(e);\n\t\t}\n\t}\n\tif (in_ftab || !aet || (ae && ae.closest('#ggrid'))) {\n\t\tif ((kl == 'a') && ctrl(e)) {\n\t\t\tvar ntot = treectl.lsc.files.length + treectl.lsc.dirs.length,\n\t\t\t\tsel = msel.getsel(),\n\t\t\t\tall = msel.getall();\n\n\t\t\tmsel.evsel(e, sel.length < all.length);\n\t\t\tmsel.origin_id(null);\n\t\t\tif (ntot > all.length)\n\t\t\t\ttoast.warn(10, L.f_anota.format(all.length, ntot), L.f_anota);\n\t\t\telse if (toast.tag == L.f_anota)\n\t\t\t\ttoast.hide();\n\t\t\treturn ev(e);\n\t\t}\n\t}\n\n\tif (ae && ae.closest('pre')) {\n\t\tif ((kl == 'a') && ctrl(e)) {\n\t\t\tvar sel = document.getSelection(),\n\t\t\t\tran = document.createRange();\n\n\t\t\tsel.removeAllRanges();\n\t\t\tran.selectNode(ae.closest('pre'));\n\t\t\tsel.addRange(ran);\n\t\t\treturn ev(e);\n\t\t}\n\t}\n\n\tif (k.endsWith('Enter') && ae && (ae.onclick || ae.hasAttribute('tabIndex')))\n\t\treturn ev(e) && ae.click() || true;\n\n\tif (aet && aet != 'a' && aet != 'tr' && aet != 'td' && aet != 'div' && aet != 'pre')\n\t\treturn;\n\n\tif (k == '?')\n\t\treturn hkhelp();\n\n\tif (!sh && ctrl(e)) {\n\t\tvar sel = window.getSelection && window.getSelection() || {};\n\t\tsel = sel && !sel.isCollapsed && sel.direction != 'none';\n\n\t\tif (kl == 'x')\n\t\t\treturn fileman.cut(e);\n\n\t\tif (kl == 'c' && !sel)\n\t\t\treturn fileman.cpy(e);\n\n\t\tif (kl == 'v')\n\t\t\treturn fileman.d_paste(e);\n\n\t\tif (kl == 'k')\n\t\t\treturn fileman.delete(e);\n\n\t\treturn;\n\t}\n\n\tif (showfile.active()) {\n\t\tif (!sh && kl == 's')\n\t\t\treturn showfile.tglsel() || true;\n\t\tif (!sh && kl == 'e' && ebi('editdoc').style.display != 'none')\n\t\t\treturn ebi('editdoc').click() || true;\n\t\tif (sh && kl == 'j')\n\t\t\treturn showfile.ppj(e) || true;\n\t}\n\n\tif (sh && kl != 'a' && kl != 'd')\n\t\treturn;\n\n\tif (/^[0-9]$/.test(k))\n\t\tpos = parseInt(k) * 0.1;\n\n\tif (pos !== -1)\n\t\treturn seek_au_mul(pos) || true;\n\n\tif (kl == 'j')\n\t\treturn prev_song() || true;\n\n\tif (kl == 'l')\n\t\treturn next_song() || true;\n\n\tif (kl == 'p')\n\t\treturn playpause() || true;\n\n\tn = kl == 'u' ? -10 : kl == 'o' ? 10 : 0;\n\tif (n !== 0)\n\t\treturn seek_au_rel(n) || true;\n\n\tif (kl == 'y')\n\t\treturn msel.getsel().length ? ebi('seldl').click() :\n\t\t\tshowfile.active() ? ebi('dldoc').click() :\n\t\t\t\tdl_song();\n\n\tn = kl == 'i' ? -1 : kl == 'k' ? 1 : 0;\n\tif (n !== 0)\n\t\treturn tree_neigh(n, 1);\n\n\tif (kl == 'm')\n\t\treturn tree_up();\n\n\tif (kl == 'b')\n\t\treturn treectl.hidden ? treectl.entree() : treectl.detree();\n\n\tif (kl == 'g')\n\t\treturn ebi('griden').click();\n\n\tif (kl == 't')\n\t\treturn ebi('thumbs').click();\n\n\tif (kl == 'v')\n\t\treturn ebi('filetree').click();\n\n\tif (k == 'F2')\n\t\treturn fileman.rename();\n\n\tif (!treectl.hidden && (!sh || !thegrid.en)) {\n\t\tif (kl == 'a')\n\t\t\treturn QS('#twig').click();\n\n\t\tif (kl == 'd')\n\t\t\treturn QS('#twobytwo').click();\n\t}\n\n\tif (mp && mp.au && !mp.au.paused) {\n\t\tif (kl == 's')\n\t\t\treturn sel_song();\n\t}\n\n\tif (thegrid.en) {\n\t\tif (kl == 's')\n\t\t\treturn ebi('gridsel').click();\n\n\t\tif (kl == 'a')\n\t\t\treturn QSA('#ghead a[z]')[0].click();\n\n\t\tif (kl == 'd')\n\t\t\treturn QSA('#ghead a[z]')[1].click();\n\t}\n};\n\n\n// search\nvar search_ui = (function () {\n\tvar sconf = [\n\t\t[\n\t\t\tL.s_sz,\n\t\t\t[\"szl\", \"sz_min\", L.s_s1, \"14\", \"\"],\n\t\t\t[\"szu\", \"sz_max\", L.s_s2, \"14\", \"\"]\n\t\t],\n\t\t[\n\t\t\tL.s_dt,\n\t\t\t[\"dtl\", \"dt_min\", L.s_d1, \"14\", \"1997-08-15, 01:00\"],\n\t\t\t[\"dtu\", \"dt_max\", L.s_d2, \"14\", \"2020\"]\n\t\t],\n\t\t[\n\t\t\tL.s_rd,\n\t\t\t[\"path\", \"path\", L.s_r1, \"30\", \"windows  -system32\"]\n\t\t],\n\t\t[\n\t\t\tL.s_fn,\n\t\t\t[\"name\", \"name\", L.s_f1, \"30\", \".exe$\"]\n\t\t],\n\t\t[\n\t\t\tL.s_ta,\n\t\t\t[\"tags\", \"tags\", L.s_t1, \"30\", \"^irui$\"]\n\t\t],\n\t\t[\n\t\t\tL.s_ad,\n\t\t\t[\"adv\", \"adv\", L.s_a1, \"30\", \"key>=1A  key<=2B  .bpm>165\"]\n\t\t],\n\t\t[\n\t\t\tL.s_ua,\n\t\t\t[\"utl\", \"ut_min\", L.s_u1, \"14\", \"2007-04-08\"],\n\t\t\t[\"utu\", \"ut_max\", L.s_u2, \"14\", \"2038-01-19\"]\n\t\t]\n\t];\n\n\tvar r = {},\n\t\ttrs = [],\n\t\torig_url = null,\n\t\torig_html = null,\n\t\tcap = 125;\n\n\tfor (var a = 0; a < sconf.length; a++) {\n\t\tvar html = ['<tr id=\"tsrch_' + sconf[a][1][0] + '\"><td><br />' + sconf[a][0] + '</td>'];\n\t\tfor (var b = 1; b < 3; b++) {\n\t\t\tvar hn = \"srch_\" + sconf[a][b][0],\n\t\t\t\tcsp = (sconf[a].length == 2) ? 2 : 1;\n\n\t\t\thtml.push(\n\t\t\t\t'<td colspan=\"' + csp + '\"><input id=\"' + hn + 'c\" type=\"checkbox\">\\n' +\n\t\t\t\t'<label for=\"' + hn + 'c\">' + sconf[a][b][2] + '</label>\\n' +\n\t\t\t\t'<br /><input id=\"' + hn + 'v\" type=\"text\" style=\"width:' + sconf[a][b][3] +\n\t\t\t\t'em\" name=\"' + sconf[a][b][1] + '\" placeholder=\"' + sconf[a][b][4] + '\" /></td>');\n\t\t\tif (csp == 2)\n\t\t\t\tbreak;\n\t\t}\n\t\thtml.push('</tr>');\n\t\ttrs.push(html);\n\t}\n\tvar html = [];\n\tfor (var a = 0; a < trs.length; a += 2) {\n\t\thtml.push('<table>' + (trs[a].concat(trs[a + 1])).join('\\n') + '</table>');\n\t}\n\thtml.push('<table id=\"tq_raw\"><tr><td>raw</td><td><input id=\"q_raw\" type=\"text\" name=\"q\" ' + NOAC + ' placeholder=\"( tags like *nhato* or tags like *taishi* ) and ( not tags like *nhato* or not tags like *taishi* )\" /></td></tr></table>');\n\tebi('srch_form').innerHTML = html.join('\\n');\n\n\tvar o = QSA('#op_search input');\n\tfor (var a = 0; a < o.length; a++) {\n\t\to[a].oninput = ev_search_input;\n\t\to[a].onkeydown = ev_search_keydown;\n\t}\n\n\tfunction srch_msg(err, txt) {\n\t\tvar o = ebi('srch_q');\n\t\to.textContent = txt;\n\t\tclmod(o, 'err', err);\n\t}\n\n\tvar search_timeout,\n\t\tdefer_timeout,\n\t\tsearch_in_progress = 0;\n\n\tfunction ev_search_input() {\n\t\tvar v = unsmart(this.value),\n\t\t\tid = this.getAttribute('id'),\n\t\t\tis_txt = id.slice(-1) == 'v',\n\t\t\tis_chk = id.slice(-1) == 'c';\n\n\t\tif (is_txt) {\n\t\t\tvar chk = ebi(id.slice(0, -1) + 'c');\n\t\t\tchk.checked = ((v + '').length > 0);\n\t\t}\n\n\t\tif (id != \"q_raw\")\n\t\t\tencode_query();\n\n\t\tset_vq();\n\t\tcap = 125;\n\n\t\tclearTimeout(defer_timeout);\n\t\tif (is_chk)\n\t\t\treturn do_search();\n\n\t\tdefer_timeout = setTimeout(try_search, 2000);\n\t\ttry_search(v);\n\t}\n\n\tfunction ev_search_keydown(e) {\n\t\tif ((e.key + '').endsWith('Enter'))\n\t\t\tdo_search();\n\t}\n\n\tfunction try_search(v) {\n\t\tif (Date.now() - search_in_progress > 30 * 1000) {\n\t\t\tclearTimeout(defer_timeout);\n\t\t\tclearTimeout(search_timeout);\n\t\t\tsearch_timeout = setTimeout(do_search,\n\t\t\t\tv && v.length < (MOBILE ? 4 : 3) ? 1000 : 500);\n\t\t}\n\t}\n\n\tfunction set_vq() {\n\t\tif (search_in_progress)\n\t\t\treturn;\n\n\t\tvar q = unsmart(ebi('q_raw').value),\n\t\t\tvq = ebi('files').getAttribute('q_raw');\n\n\t\tsrch_msg(false, (q == vq) ? '' : L.sm_prev + (vq ? vq : '(*)'));\n\t}\n\n\tfunction encode_query() {\n\t\tvar q = '';\n\t\tfor (var a = 0; a < sconf.length; a++) {\n\t\t\tfor (var b = 1; b < sconf[a].length; b++) {\n\t\t\t\tvar k = sconf[a][b][0],\n\t\t\t\t\tchk = 'srch_' + k + 'c',\n\t\t\t\t\tvs = unsmart(ebi('srch_' + k + 'v').value),\n\t\t\t\t\ttvs = [];\n\n\t\t\t\tif (a == 1)\n\t\t\t\t\tvs = vs.trim().replace(/ +/, 'T');\n\n\t\t\t\twhile (vs) {\n\t\t\t\t\tvs = vs.trim();\n\t\t\t\t\tif (!vs)\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tvar v = '';\n\t\t\t\t\tif (vs.startsWith('\"')) {\n\t\t\t\t\t\tvar vp = vs.slice(1).split(/\"(.*)/);\n\t\t\t\t\t\tv = vp[0];\n\t\t\t\t\t\tvs = vp[1] || '';\n\t\t\t\t\t\twhile (v.endsWith('\\\\')) {\n\t\t\t\t\t\t\tvp = vs.split(/\"(.*)/);\n\t\t\t\t\t\t\tv = v.slice(0, -1) + '\"' + vp[0];\n\t\t\t\t\t\t\tvs = vp[1] || '';\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tvar vp = vs.split(/ +(.*)/);\n\t\t\t\t\t\tv = vp[0].replace(/\\\\\"/g, '\"');\n\t\t\t\t\t\tvs = vp[1] || '';\n\t\t\t\t\t}\n\t\t\t\t\ttvs.push(v);\n\t\t\t\t}\n\n\t\t\t\tif (!ebi(chk).checked)\n\t\t\t\t\tcontinue;\n\n\t\t\t\tfor (var c = 0; c < tvs.length; c++) {\n\t\t\t\t\tvar tv = tvs[c];\n\t\t\t\t\tif (!tv.length)\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tq += ' and ';\n\n\t\t\t\t\tif (k == 'adv') {\n\t\t\t\t\t\tq += tv.replace(/ +/g, \" and \").replace(/([=!><]=?)/, \" $1 \");\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (k.length == 3) {\n\t\t\t\t\t\tq += k.replace(/l$/, ' >= ').replace(/u$/, ' <= ').replace(/^sz/, 'size').replace(/^dt/, 'date').replace(/^ut/, 'up_at') + tv;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (k == 'path' || k == 'name' || k == 'tags') {\n\t\t\t\t\t\tvar not = '';\n\t\t\t\t\t\tif (tv.slice(0, 1) == '-') {\n\t\t\t\t\t\t\ttv = tv.slice(1);\n\t\t\t\t\t\t\tnot = 'not ';\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (tv.slice(0, 1) == '^') {\n\t\t\t\t\t\t\ttv = tv.slice(1);\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\ttv = '*' + tv;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (tv.slice(-1) == '$') {\n\t\t\t\t\t\t\ttv = tv.slice(0, -1);\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\ttv += '*';\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (tv.indexOf(' ') + 1) {\n\t\t\t\t\t\t\ttv = '\"' + tv + '\"';\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tq += not + k + ' like ' + tv;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tebi('q_raw').value = q.slice(5);\n\t}\n\n\tfunction do_search() {\n\t\tsearch_in_progress = Date.now();\n\t\tsrch_msg(false, L.sm_w8);\n\t\tclearTimeout(search_timeout);\n\n\t\tvar xhr = new XHR();\n\t\txhr.open('POST', SR + '/?srch', true);\n\t\txhr.setRequestHeader('Content-Type', 'text/plain');\n\t\txhr.onload = xhr.onerror = xhr_search_results;\n\t\txhr.ts = Date.now();\n\t\txhr.q_raw = unsmart(ebi('q_raw').value);\n\t\txhr.send(JSON.stringify({ \"q\": xhr.q_raw, \"n\": cap }));\n\t}\n\n\tfunction xhr_search_results() {\n\t\tif (this.status !== 200) {\n\t\t\tvar msg = hunpre(this.responseText);\n\t\t\tsrch_msg(true, \"http \" + this.status + \": \" + msg);\n\t\t\tsearch_in_progress = 0;\n\t\t\treturn;\n\t\t}\n\t\tsearch_in_progress = 0;\n\t\tsrch_msg(false, '');\n\n\t\tvar res = JSON.parse(this.responseText);\n\t\tr.render(res, this, true);\n\t}\n\n\tr.render = function (res, xhr, sort) {\n\t\tvar tagord = res.tag_order;\n\n\t\tsrch_msg(false, '');\n\t\tif (sort)\n\t\t\tsortfiles(res.hits);\n\n\t\tvar ofiles = ebi('files');\n\t\tif (xhr && ofiles.getAttribute('ts') > xhr.ts)\n\t\t\treturn;\n\n\t\ttreectl.hide();\n\t\tthegrid.setvis(true);\n\n\t\tvar html = mk_files_header(tagord), seen = {};\n\t\thtml.push('<tbody>');\n\t\thtml.push('<tr class=\"srch_hdr\"><td>-</td><td><a href=\"#\" id=\"unsearch\"><big style=\"font-weight:bold\">[❌] ' + L.sl_close + '</big></a> -- ' + L.sl_hits.format(res.hits.length) + (res.trunc ? ' -- <a href=\"#\" id=\"moar\">' + L.sl_moar + '</a>' : '') + '</td></tr>');\n\n\t\tfor (var a = 0; a < res.hits.length; a++) {\n\t\t\tvar r = res.hits[a],\n\t\t\t\tts = parseInt(r.ts),\n\t\t\t\tsz = parseInt(r.sz),\n\t\t\t\thsz = filesizefun(sz),\n\t\t\t\trp = esc(uricom_dec(r.rp + '')),\n\t\t\t\text = rp.lastIndexOf('.') > 0 ? rp.split('.').pop().split('?')[0] : '%',\n\t\t\t\tid = 'f-' + ('00000000' + crc32(rp)).slice(-8);\n\n\t\t\twhile (seen[id])\n\t\t\t\tid += 'a';\n\t\t\tseen[id] = 1;\n\n\t\t\tif (ext.length > 8)\n\t\t\t\text = '%';\n\n\t\t\tvar links = linksplit(r.rp + '', null, id).join('<span>/</span>'),\n\t\t\t\tnodes = ['<tr><td>-</td><td><div>' + links +\n\t\t\t\t\t'</div></td><td sortv=\"' + sz + '\">' + hsz];\n\n\t\t\tfor (var b = 0; b < tagord.length; b++) {\n\t\t\t\tvar k = esc(tagord[b]),\n\t\t\t\t\tv = r.tags[k] || \"\";\n\n\t\t\t\tif (k == \".dur\") {\n\t\t\t\t\tvar sv = v ? s2ms(v) : \"\";\n\t\t\t\t\tnodes[nodes.length - 1] += '</td><td sortv=\"' + v + '\">' + sv;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tnodes.push(esc('' + v));\n\t\t\t}\n\n\t\t\tnodes = nodes.concat([ext, unix2ui(ts)]);\n\t\t\thtml.push(nodes.join('</td><td>'));\n\t\t\thtml.push('</td></tr>');\n\t\t}\n\n\t\tif (!orig_html || orig_url != get_evpath()) {\n\t\t\torig_html = ebi('files').innerHTML;\n\t\t\torig_url = get_evpath();\n\t\t}\n\n\t\tofiles = set_files_html(html.join('\\n'));\n\t\tofiles.setAttribute(\"ts\", xhr ? xhr.ts : 1);\n\t\tofiles.setAttribute(\"q_raw\", xhr ? xhr.q_raw : 'playlist');\n\t\tset_vq();\n\t\tmukey.render();\n\t\treload_browser();\n\t\tfilecols.set_style(['File Name']);\n\n\t\tif (xhr)\n\t\t\tsethash('q=' + uricom_enc(xhr.q_raw));\n\n\t\tebi('unsearch').onclick = unsearch;\n\t\tvar m = ebi('moar');\n\t\tif (m)\n\t\t\tm.onclick = moar;\n\t};\n\n\tfunction unsearch(e) {\n\t\tev(e);\n\t\ttreectl.show();\n\t\tset_files_html(orig_html);\n\t\tebi('files').removeAttribute('q_raw');\n\t\torig_html = null;\n\t\tsethash('');\n\t\treload_browser();\n\t}\n\n\tfunction moar(e) {\n\t\tev(e);\n\t\tcap *= 2;\n\t\tdo_search();\n\t}\n\n\treturn r;\n})();\n\n\nfunction ev_load_m3u(e) {\n\tev(e);\n\tvar id = this.getAttribute('id').slice(1),\n\t\turl = ebi(id).getAttribute('href').split('?')[0];\n\n\tmodal.confirm(L.mm_m3u,\n\t\tfunction () { load_m3u(url); },\n\t\tfunction () {\n\t\t\tif (has(perms, 'write') && has(perms, 'delete'))\n\t\t\t\tlocation = url + '?edit';\n\t\t\telse\n\t\t\t\tshowfile.show(url);\n\t\t}\n\t);\n\treturn false;\n}\nfunction load_m3u(url) {\n\tassert_vp(url);\n\tvar xhr = new XHR();\n\txhr.open('GET', url, true);\n\txhr.onload = render_m3u;\n\txhr.url = url;\n\txhr.send();\n\treturn false;\n}\nfunction render_m3u() {\n\tif (!xhrchk(this, L.tv_xe1, L.tv_xe2))\n\t\treturn;\n\n\tvar evp = get_evpath(),\n\t\tm3u = this.responseText,\n\t\txtd = m3u.slice(0, 12).indexOf('#EXTM3U') + 1,\n\t\tlines = m3u.replace(/\\r/g, '\\n').split('\\n'),\n\t\tdur = 1,\n\t\tartist = '',\n\t\ttitle = '',\n\t\tret = {'hits': [], 'tag_order': ['artist', 'title', '.dur'], 'trunc': false};\n\n\tfor (var a = 0; a < lines.length; a++) {\n\t\tvar ln = lines[a].trim();\n\t\tif (xtd && ln.startsWith('#')) {\n\t\t\tvar m = /^#EXTINF:([0-9]+)[, ](.*)/.exec(ln);\n\t\t\tif (m) {\n\t\t\t\tdur = m[1];\n\t\t\t\ttitle = m[2];\n\t\t\t\tvar ofs = title.indexOf(' - ');\n\t\t\t\tif (ofs > 0) {\n\t\t\t\t\tartist = title.slice(0, ofs);\n\t\t\t\t\ttitle = title.slice(ofs + 3);\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (ln.indexOf('.') < 0)\n\t\t\tcontinue;\n\n\t\tvar n = ret.hits.length + 1,\n\t\t\turl = ln;\n\n\t\tif (url.indexOf(':\\\\'))  // C:\\\n\t\t\turl = url.split(/\\\\/g).pop();\n\n\t\turl = url.replace(/\\\\/g, '/');\n\t\turl = uricom_enc(url).replace(/%2f/gi, '/')\n\n\t\tif (!url.startsWith('/'))\n\t\t\turl = vjoin(evp, url);\n\n\t\tret.hits.push({\n\t\t\t\"ts\": 946684800 + n,\n\t\t\t\"sz\": 100000 + n,\n\t\t\t\"rp\": url,\n\t\t\t\"tags\": {\".dur\": dur, \"artist\": artist, \"title\": title}\n\t\t});\n\t\tdur = 1;\n\t\tartist = title = '';\n\t}\n\n\tsearch_ui.render(ret, null, false);\n\tsethash('m3u=' + this.url.split('?')[0].split('/').pop());\n\tgoto();\n\n\tvar el = QS('#files>tbody>tr.au>td>a.play');\n\tif (el)\n\t\tel.click();\n}\n\n\nfunction aligngriditems() {\n\tif (!treectl)\n\t\treturn;\n\n\tvar ggrid = ebi('ggrid'),\n\t\tem2px = parseFloat(getComputedStyle(ggrid).fontSize),\n\t\tgridsz = 10;\n\ttry {\n\t\tgridsz = cprop('--grid-sz').slice(0, -2);\n\t}\n\tcatch (ex) { }\n\tvar gridwidth = ggrid.clientWidth,\n\t\tgriditemcount = ggrid.children.length,\n\t\ttotalgapwidth = em2px * griditemcount;\n\n\tif (/b/.test(themen + ''))\n\t\ttotalgapwidth *= 2.8;\n\n\tvar val, st = ggrid.style;\n\n\tif (((griditemcount * em2px) * gridsz) + totalgapwidth < gridwidth) {\n\t\tval = 'left';\n\t} else {\n\t\tval = treectl.hidden ? 'center' : 'space-between';\n\t}\n\tif (st.justifyContent != val)\n\t\tst.justifyContent = val;\n}\nonresize100.add(aligngriditems);\n\n\nvar filecolwidth = (function () {\n\tvar lastwidth = -1;\n\n\treturn function () {\n\t\tvar vw = window.innerWidth / parseFloat(getComputedStyle(document.body)['font-size']),\n\t\t\tw = Math.floor(vw - 2);\n\n\t\tif (w == lastwidth)\n\t\t\treturn;\n\n\t\tlastwidth = w;\n\t\tsetcvar('--file-td-w', w + 'em');\n\t}\n})();\nonresize100.add(filecolwidth, true);\n\n\nvar treectl = (function () {\n\tvar r = {\n\t\t\"hidden\": true,\n\t\t\"sb_msg\": false,\n\t\t\"ls_cb\": null,\n\t\t\"dir_cb\": tree_scrollto,\n\t\t\"pdir\": []\n\t},\n\t\tentreed = false,\n\t\tfixedpos = false,\n\t\tprev_atop = null,\n\t\tprev_winh = null,\n\t\tmentered = null,\n\t\ttreesz = clamp(icfg_get('treesz', 16), 10, 50);\n\n\tif (/[?&]dlni\\b/.exec(sloc0))\n\t\tswrite('dlni', /[?&]dlni=0\\b/.exec(sloc0) ? 0 : 1);\n\n\tvar resort = function () {\n\t\tENATSORT = NATSORT && clgot(ebi('nsort'), 'on');\n\t\ttreectl.gentab(get_evpath(), treectl.lsc);\n\t};\n\tbcfg_bind(r, 'ireadme', 'ireadme', true);\n\tbcfg_bind(r, 'idxh', 'idxh', idxh, setidxh);\n\tbcfg_bind(r, 'dyn', 'dyntree', true, onresize);\n\tbcfg_bind(r, 'csel', 'csel', dgsel);\n\tbcfg_bind(r, 'dsel', 'dsel', !MOBILE);\n\tbcfg_bind(r, 'dlni', 'dlni', dlni, resort);\n\tbcfg_bind(r, 'dots', 'dotfiles', see_dots, function (v) {\n\t\tr.goto();\n\t\tsetck('dots=' + (v ? 'y' : ''));\n\t});\n\tbcfg_bind(r, 'utctid', 'utctid', dutc, function (v) {\n\t\twindow.unix2ui = v ? unix2iso : unix2iso_localtime;\n\t\tresort();\n\t});\n\tbcfg_bind(r, 'nsort', 'nsort', dnsort, resort);\n\tbcfg_bind(r, 'dir1st', 'dir1st', true, resort);\n\tsetwrap(bcfg_bind(r, 'wtree', 'wraptree', true, setwrap));\n\tsetwrap(bcfg_bind(r, 'parpane', 'parpane', true, onscroll));\n\tbcfg_bind(r, 'htree', 'hovertree', false, reload_tree);\n\tbcfg_bind(r, 'ask', 'bd_ask', MOBILE && FIREFOX);\n\tebi('bd_lim').value = r.lim = icfg_get('bd_lim');\n\tebi('bd_lim').oninput = function (e) {\n\t\tvar n = parseInt(this.value);\n\t\tswrite('bd_lim', r.lim = (isNum(n) ? n : 0) || 1000);\n\t};\n\tr.nvis = r.lim;\n\n\tldks = jread('dks', []);\n\tfor (var a = ldks.length - 1; a >= 0; a--) {\n\t\tvar s = ldks[a],\n\t\t\to = s.lastIndexOf('?');\n\n\t\tdks[s.slice(0, o)] = s.slice(o + 1);\n\t}\n\n\tfunction setwrap(v) {\n\t\tclmod(ebi('tree'), 'nowrap', !v);\n\t\treload_tree();\n\t}\n\tsetwrap(r.wtree);\n\n\tfunction setidxh(v) {\n\t\tif (!v == !/\\bidxh=y\\b/.exec('' + document.cookie))\n\t\t\treturn;\n\n\t\tsetck('idxh=' + (v ? 'y' : 'n'));\n\t}\n\tsetidxh(r.idxh);\n\n\tr.entree = function (e, nostore) {\n\t\tev(e);\n\t\tentreed = true;\n\t\tif (!nostore)\n\t\t\tswrite('entreed', 'tree');\n\n\t\tget_tree(\"\", get_evpath(), true);\n\t\tr.show();\n\t}\n\n\tr.show = function () {\n\t\tr.hidden = false;\n\t\tif (!entreed) {\n\t\t\tebi('path').style.display = nonav ? 'none' : 'inline-block';\n\t\t\treturn;\n\t\t}\n\n\t\tebi('path').style.display = 'none';\n\t\tebi('tree').style.display = 'block';\n\t\twindow.addEventListener('scroll', onscroll);\n\t\twindow.addEventListener('resize', onresize);\n\t\tonresize();\n\t\taligngriditems();\n\t};\n\n\tr.detree = function (e, nw) {\n\t\tev(e);\n\t\tentreed = false;\n\t\tif (!nw)\n\t\t\tswrite('entreed', 'na');\n\n\t\tr.hide();\n\t\tif (!nonav)\n\t\t\tebi('path').style.display = '';\n\t};\n\n\tr.hide = function () {\n\t\tr.hidden = true;\n\t\tebi('path').style.display = 'none';\n\t\tebi('tree').style.display = 'none';\n\t\tebi('wrap').style.marginLeft = '';\n\t\twindow.removeEventListener('resize', onresize);\n\t\twindow.removeEventListener('scroll', onscroll);\n\t\taligngriditems();\n\t};\n\n\tfunction unmenter() {\n\t\tif (mentered) {\n\t\t\tmentered.style.position = '';\n\t\t\tmentered = null;\n\t\t}\n\t}\n\n\tr.textmode = function (ya) {\n\t\tvar chg = !r.texts != !ya;\n\t\tr.texts = ya;\n\t\tebi('docul').style.display = ya ? '' : 'none';\n\t\tebi('treeul').style.display = ebi('treepar').style.display = ya ? 'none' : '';\n\t\tclmod(ebi('filetree'), 'on', ya);\n\t\tif (chg)\n\t\t\ttree_scrollto();\n\t};\n\tebi('filetree').onclick = function (e) {\n\t\tev(e);\n\t\tr.textmode(!r.texts);\n\t};\n\tr.textmode(false);\n\n\tfunction onscroll() {\n\t\tunmenter();\n\t\tonscroll2();\n\t}\n\n\tfunction onscroll2() {\n\t\tif (!entreed || r.hidden || document.visibilityState == 'hidden')\n\t\t\treturn;\n\n\t\tvar tree = ebi('tree'),\n\t\t\twrap = ebi('wrap'),\n\t\t\twraptop = null,\n\t\t\tatop = wrap.getBoundingClientRect().top,\n\t\t\twinh = window.innerHeight,\n\t\t\tparp = ebi('treepar'),\n\t\t\ty = tree.scrollTop,\n\t\t\tw = tree.offsetWidth;\n\n\t\tif (atop !== prev_atop || winh !== prev_winh)\n\t\t\twraptop = Math.floor(wrap.offsetTop);\n\n\t\tif (r.parpane && r.pdir.length && w != r.pdirw) {\n\t\t\tr.pdirw = w;\n\t\t\tcompy();\n\t\t}\n\n\t\tif (!r.parpane || !r.pdir.length || y >= r.pdir.slice(-1)[0][0] || y <= r.pdir[0][0]) {\n\t\t\tclmod(parp, 'off', 1);\n\t\t\tr.pdirh = null;\n\t\t}\n\t\telse {\n\t\t\tvar h1 = [], h2 = [], els = [];\n\t\t\tfor (var a = 0; a < r.pdir.length; a++) {\n\t\t\t\tif (r.pdir[a][0] > y)\n\t\t\t\t\tbreak;\n\n\t\t\t\tvar e2 = r.pdir[a][1], e1 = e2.previousSibling;\n\t\t\t\th1.push('<li>' + e1.outerHTML + e2.outerHTML + '<ul>');\n\t\t\t\th2.push('</ul></li>');\n\t\t\t\tels.push([e1, e2]);\n\t\t\t}\n\t\t\th1 = h1.join('\\n') + h2.join('\\n');\n\t\t\tif (h1 != r.pdirh) {\n\t\t\t\tr.pdirh = h1;\n\t\t\t\tparp.innerHTML = h1;\n\t\t\t\tclmod(parp, 'off');\n\t\t\t\tvar els = QSA('#treepar a');\n\t\t\t\tfor (var a = 0, aa = els.length; a < aa; a++)\n\t\t\t\t\tels[a].onclick = bad_proxy;\n\t\t\t}\n\t\t\ty = ebi('treeh').offsetHeight;\n\t\t\tif (!fixedpos)\n\t\t\t\ty += tree.offsetTop - yscroll();\n\n\t\t\ty = (y - 3) + 'px';\n\t\t\tif (parp.style.top != y)\n\t\t\t\tparp.style.top = y;\n\t\t}\n\n\t\tif (wraptop === null)\n\t\t\treturn;\n\n\t\tprev_atop = atop;\n\t\tprev_winh = winh;\n\n\t\tif (fixedpos && atop >= 0) {\n\t\t\ttree.style.position = 'absolute';\n\t\t\ttree.style.bottom = '';\n\t\t\tfixedpos = false;\n\t\t}\n\t\telse if (!fixedpos && atop < 0) {\n\t\t\ttree.style.position = 'fixed';\n\t\t\ttree.style.height = 'auto';\n\t\t\tfixedpos = true;\n\t\t}\n\n\t\tif (fixedpos) {\n\t\t\ttree.style.top = Math.max(0, parseInt(atop)) + 'px';\n\t\t}\n\t\telse {\n\t\t\tvar top = Math.max(0, wraptop),\n\t\t\t\ttreeh = winh - atop;\n\n\t\t\ttree.style.top = top + 'px';\n\t\t\ttree.style.height = treeh < 10 ? '' : Math.floor(treeh) + 'px';\n\t\t}\n\t}\n\ttimer.add(onscroll2, true);\n\n\tfunction onresize(e) {\n\t\tif (!entreed || r.hidden)\n\t\t\treturn;\n\n\t\tvar q = '#tree',\n\t\t\tnq = -3;\n\n\t\twhile (r.dyn) {\n\t\t\tnq++;\n\t\t\tq += '>ul>li';\n\t\t\tif (!QS(q))\n\t\t\t\tbreak;\n\t\t}\n\t\tnq = Math.max(nq, get_evpath().split('/').length - 2);\n\t\tvar iw = (treesz + Math.max(0, nq)),\n\t\t\tw = iw + 'em',\n\t\t\tw2 = (iw + 2) + 'em';\n\n\t\tsetcvar('--nav-sz', w);\n\t\tebi('tree').style.width = w;\n\t\tebi('wrap').style.marginLeft = w2;\n\t\tonscroll();\n\t}\n\n\tr.find = function (txt) {\n\t\tvar ta = QSA('#treeul a.hl+ul>li>a+a');\n\t\tfor (var a = 0, aa = ta.length; a < aa; a++)\n\t\t\tif (ta[a].textContent == txt)\n\t\t\t\treturn ta[a];\n\t};\n\n\tr.goto = function (url, push, back) {\n\t\tif (!url || !url.startsWith('/'))\n\t\t\turl = get_evpath() + (url || '');\n\n\t\tget_tree(\"\", url, true);\n\t\tr.reqls(url, push, back);\n\t};\n\n\tfunction get_tree(top, dst, rst) {\n\t\tvar xhr = new XHR(),\n\t\t\tm = /[?&](k=[^&#]+)/.exec(dst),\n\t\t\tk = m ? '&' + m[1] : dk ? '&k=' + dk : '';\n\n\t\txhr.top = top;\n\t\txhr.dst = dst;\n\t\txhr.rst = rst;\n\t\txhr.ts = r.busied = Date.now();\n\t\txhr.open('GET', addq(dst, 'tree=' + top + (r.dots ? '&dots' : '') + k), true);\n\t\txhr.onload = xhr.onerror = r.recvtree;\n\t\txhr.send();\n\t\tenspin('t');\n\t}\n\n\tr.recvtree = function () {\n\t\tif (!xhrchk(this, L.tl_xe1, L.tl_xe2))\n\t\t\treturn;\n\n\t\ttry {\n\t\t\tvar res = JSON.parse(this.responseText);\n\t\t}\n\t\tcatch (ex) {\n\t\t\treturn toast.err(30, \"bad <code>?tree</code> reply;\\nexpected json, got this:\\n\\n\" + esc(this.responseText + ''));\n\t\t}\n\t\tr.rendertree(res, this.ts, this.top, this.dst, this.rst);\n\n\t\tif (r.lsc && r.lsc.unlist)\n\t\t\tr.prunetree(r.lsc);\n\t};\n\n\tr.prunetree = function (res) {\n\t\tif (r.dots)\n\t\t\treturn;\n\t\tvar ptn = new RegExp(res.unlist);\n\t\tvar els = QSA('#treeul li>a+a');\n\t\tfor (var a = els.length - 1; a >= 0; a--)\n\t\t\tif (ptn.exec(els[a].textContent) && !els[a].className)\n\t\t\t\tels[a].closest('ul').removeChild(els[a].closest('li'));\n\t};\n\n\tr.rendertree = function (res, ts, top0, dst, rst) {\n\t\tvar cur = ebi('treeul').getAttribute('ts');\n\t\tif (cur && parseInt(cur) > ts + 20 && QS('#treeul>li>a+a')) {\n\t\t\tconsole.log(\"reject tree; \" + cur + \" / \" + (ts - cur));\n\t\t\treturn;\n\t\t}\n\t\tebi('treeul').setAttribute('ts', ts);\n\n\t\tif (SR && !top0) {\n\t\t\tvar x = SR.slice(1).split('/');\n\t\t\twhile (x[0]) {\n\t\t\t\tres = res['k' + x.shift()];\n\t\t\t\tif (!res)\n\t\t\t\t\tthrow 'invalid --rp-loc (or bug?)';\n\t\t\t}\n\t\t}\n\n\t\tvar top = (top0 == '.' ? dst : top0).split('?')[0],\n\t\t\tname = uricom_dec(top.split('/').slice(-2)[0]),\n\t\t\trtop = top.replace(/^\\/+/, \"\"),\n\t\t\thtml = parsetree(res, rtop.slice(SR.length));\n\n\t\tif (!top0) {\n\t\t\thtml = '<li><a href=\"#\">-</a><a href=\"' + SR + '/\">[root]</a>\\n<ul>' + html;\n\t\t\tif (rst || !ebi('treeul').getElementsByTagName('li').length)\n\t\t\t\tebi('treeul').innerHTML = html + '</ul></li>';\n\t\t}\n\t\telse {\n\t\t\thtml = '<a href=\"#\">-</a><a href=\"' +\n\t\t\t\tesc(top) + '\">' + esc(name) +\n\t\t\t\t\"</a>\\n<ul>\\n\" + html + \"</ul>\";\n\n\t\t\tvar links = QSA('#treeul a+a');\n\t\t\tfor (var a = 0, aa = links.length; a < aa; a++) {\n\t\t\t\tif (links[a].getAttribute('href').split('?')[0] == top) {\n\t\t\t\t\tvar o = links[a].parentNode;\n\t\t\t\t\tif (!o.getElementsByTagName('li').length)\n\t\t\t\t\t\to.innerHTML = html;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tqsr('#dlt_t');\n\n\t\ttry {\n\t\t\tQS('#treeul>li>a+a').textContent = '[root]';\n\t\t}\n\t\tcatch (ex) {\n\t\t\tconsole.log('got no root yet');\n\t\t\tr.dir_cb = null;\n\t\t\treturn;\n\t\t}\n\n\t\treload_tree();\n\t\tvar fun = r.dir_cb;\n\t\tif (fun) {\n\t\t\tr.dir_cb = null;\n\t\t\ttry {\n\t\t\t\tfun();\n\t\t\t}\n\t\t\tcatch (ex) {\n\t\t\t\tconsole.log(\"dir_cb failed\", ex);\n\t\t\t}\n\t\t}\n\t};\n\n\tfunction reload_tree() {\n\t\tvar cevp = get_evpath(),\n\t\t\tcdir = r.nextdir || uricom_dec(cevp),\n\t\t\tlinks = QSA('#treeul a+a'),\n\t\t\tnowrap = QS('#tree.nowrap') && QS('#hovertree.on'),\n\t\t\tact = null;\n\n\t\tfor (var a = 0, aa = links.length; a < aa; a++) {\n\t\t\tvar qhref = links[a].getAttribute('href'),\n\t\t\t\tehref = qhref.split('?')[0],\n\t\t\t\thref = uricom_dec(ehref),\n\t\t\t\tcl = '';\n\n\t\t\tif (dk && ehref == cevp && !/[?&]k=/.exec(qhref))\n\t\t\t\tlinks[a].setAttribute('href', addq(qhref, 'k=' + dk));\n\n\t\t\tif (href == cdir) {\n\t\t\t\tact = links[a];\n\t\t\t\tcl = 'hl';\n\t\t\t}\n\t\t\telse if (cdir.startsWith(href)) {\n\t\t\t\tcl = 'par';\n\t\t\t}\n\n\t\t\tlinks[a].className = cl;\n\t\t\tlinks[a].onclick = r.treego;\n\t\t\tlinks[a].onmouseenter = nowrap ? menter : null;\n\t\t\tlinks[a].onmouseleave = nowrap ? mleave : null;\n\t\t}\n\t\tlinks = QSA('#treeul li>a:first-child');\n\t\tfor (var a = 0, aa = links.length; a < aa; a++) {\n\t\t\tlinks[a].setAttribute('dst', links[a].nextSibling.getAttribute('href'));\n\t\t\tlinks[a].onclick = treegrow;\n\t\t}\n\t\tebi('tree').onscroll = nowrap ? unmenter : null;\n\t\tr.pdir = [];\n\t\ttry {\n\t\t\twhile (act) {\n\t\t\t\tr.pdir.unshift([-1, act]);\n\t\t\t\tact = act.parentNode.parentNode.closest('li').querySelector('a:first-child+a');\n\t\t\t}\n\t\t}\n\t\tcatch (ex) { }\n\t\tr.pdir.shift();\n\t\tr.pdirw = -1;\n\t\tonresize();\n\t}\n\n\tfunction compy() {\n\t\tfor (var a = 0; a < r.pdir.length; a++)\n\t\t\tr.pdir[a][0] = r.pdir[a][1].offsetTop;\n\n\t\tvar ofs = 0;\n\t\tfor (var a = 0; a < r.pdir.length - 1; a++) {\n\t\t\tofs += r.pdir[a][1].offsetHeight + 1;\n\t\t\tr.pdir[a + 1][0] -= ofs;\n\t\t}\n\t}\n\n\tfunction menter(e) {\n\t\tvar p = this.offsetParent,\n\t\t\tpp = p.offsetParent,\n\t\t\tppy = pp.offsetTop,\n\t\t\ty = this.offsetTop + p.offsetTop + ppy - p.scrollTop - pp.scrollTop - (ppy ? document.documentElement.scrollTop : 0);\n\n\t\tthis.style.top = y + 'px';\n\t\tthis.style.position = 'fixed';\n\t\tmentered = this;\n\t}\n\n\tfunction mleave(e) {\n\t\tthis.style.position = '';\n\t\tmentered = null;\n\t}\n\n\tfunction bad_proxy(e) {\n\t\tif (ctrl(e))\n\t\t\treturn true;\n\n\t\tev(e);\n\t\tvar dst = this.getAttribute('dst'),\n\t\t\tk = dst ? 'dst' : 'href',\n\t\t\tv = dst ? dst : this.getAttribute('href'),\n\t\t\tels = QSA('#treeul a');\n\n\t\tfor (var a = 0, aa = els.length; a < aa; a++)\n\t\t\tif (els[a].getAttribute(k) === v)\n\t\t\t\treturn els[a].click();\n\t}\n\n\tr.treego = function (e) {\n\t\tif (ctrl(e))\n\t\t\treturn true;\n\n\t\tev(e);\n\t\tif (this.className == 'hl' &&\n\t\t\tthis.previousSibling.textContent == '-') {\n\t\t\ttreegrow.call(this.previousSibling, e);\n\t\t\treturn;\n\t\t}\n\t\tvar href = this.getAttribute('href');\n\t\tr.reqls(href, true);\n\t\tr.dir_cb = tree_scrollto;\n\t\tthegrid.setvis(true);\n\t\tclmod(this, 'ld', 1);\n\t}\n\n\tr.reqls = function (url, hpush, back, hydrate) {\n\t\tif (IE && !history.pushState)\n\t\t\treturn location = url;\n\n\t\tvar xhr = new XHR(),\n\t\t\tm = /[?&](k=[^&#]+)/.exec(url),\n\t\t\tk = m ? '&' + m[1] : dk ? '&k=' + dk : '',\n\t\t\tuq = (r.dots ? '&dots' : '') + k;\n\n\t\tif (rtt !== null)\n\t\t\tuq += '&rtt=' + rtt;\n\n\t\txhr.top = url.split('?')[0];\n\t\txhr.back = back\n\t\txhr.hpush = hpush;\n\t\txhr.hydrate = hydrate;\n\t\txhr.ts = r.busied = Date.now();\n\t\txhr.open('GET', xhr.top + '?ls' + uq, true);\n\t\txhr.setRequestHeader('Fnugg', '' + xhr.ts);\n\t\txhr.onload = xhr.onerror = recvls;\n\t\txhr.send();\n\n\t\tr.nvis = r.lim;\n\t\tr.sb_msg = false;\n\t\tr.nextdir = xhr.top;\n\t\tclearTimeout(mpl.t_eplay);\n\t\tenspin('t');\n\t\tenspin('f');\n\t\twindow.removeEventListener('scroll', r.tscroll);\n\t}\n\n\tfunction treegrow(e) {\n\t\tev(e);\n\t\tif (this.textContent == '-') {\n\t\t\twhile (this.nextSibling.nextSibling) {\n\t\t\t\tvar rm = this.nextSibling.nextSibling;\n\t\t\t\trm.parentNode.removeChild(rm);\n\t\t\t}\n\t\t\tthis.textContent = '+';\n\t\t\tonresize();\n\t\t\treturn;\n\t\t}\n\t\tvar dst = this.getAttribute('dst');\n\t\tget_tree('.', dst);\n\t}\n\n\tfunction recvls() {\n\t\tif (!xhrchk(this, L.fl_xe1, L.fl_xe2))\n\t\t\treturn;\n\n\t\trtt = Date.now() - this.ts;\n\n\t\tr.nextdir = null;\n\t\tvar cdir = get_evpath(),\n\t\t\tlfiles = ebi('files'),\n\t\t\tcur = lfiles.getAttribute('ts');\n\n\t\tif (cur && parseInt(cur) > this.ts) {\n\t\t\tconsole.log(\"reject ls\");\n\t\t\treturn;\n\t\t}\n\t\tlfiles.setAttribute('ts', this.ts);\n\n\t\ttry {\n\t\t\tvar res = JSON.parse(this.responseText);\n\t\t\tObject.assign(res, res.cfg);\n\t\t\tres.cfg.k;\n\t\t}\n\t\tcatch (ex) {\n\t\t\tif (r.ls_cb) {\n\t\t\t\tr.ls_cb = null;\n\t\t\t\treturn toast.inf(10, L.mm_nof);\n\t\t\t}\n\n\t\t\tif (!this.hydrate) {\n\t\t\t\tlocation = this.top;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\treturn toast.err(30, \"bad <code>?ls</code> reply;\\nexpected json, got this:\\n\\n\" + esc(this.responseText + ''));\n\t\t}\n\n\t\tif (r.chk_index_html(this.top, res))\n\t\t\treturn;\n\n\t\tif (this.ts != res.fnugg && res.fnugg != 'nei' && sread('no_fnugg') !== '1')\n\t\t\ttoast.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 <code>π</code> and run this: <code>STG.no_fnugg=1</code>\");\n\n\t\tfor (var a = 0; a < res.files.length; a++)\n\t\t\tif (res.files[a].tags === undefined)\n\t\t\t\tres.files[a].tags = {};\n\n\t\tsb_lg = res.sb_lg;\n\t\tsb_md = res.sb_md;\n\t\tdnsort = res.dnsort;\n\t\tread_dsort(res.dsort);\n\t\tdcrop = res.dcrop;\n\t\tdth3x = res.dth3x;\n\t\tdk = res.dk;\n\n\t\tdlni = res.dlni;\n\t\tif (!sread('dlni'))\n\t\t\tclmod(ebi('dlni'), 'on', treectl.dlni = dlni);\n\n\t\tdgrid = res.dgrid;\n\t\tif (!sread('griden'))\n\t\t\tclmod(ebi('griden'), 'on', thegrid.en = dgrid);\n\n\t\tsrvinf = res.srvinf;\n\t\tif (rtt !== null)\n\t\t\tsrvinf += (srvinf ? '</span> // <span>rtt: ' : 'rtt: ') + rtt;\n\n\t\tvar o = ebi('srv_info2');\n\t\tif (o)\n\t\t\to.innerHTML = ebi('srv_info').innerHTML = '<span>' + srvinf + '</span>';\n\n\t\tif (res.ufavico && (!favico.en || !ebi('icot').value)) {\n\t\t\twhile (qsr('head>link[rel~=\"icon\"]')) { }\n\t\t\tdocument.head.insertAdjacentHTML('beforeend', res.ufavico);\n\t\t}\n\n\t\tif (this.hpush && !showfile.active())\n\t\t\thist_push(this.top + (dk ? '?k=' + dk : ''));\n\n\t\tif (!this.back) {\n\t\t\tvar dirs = [];\n\t\t\tfor (var a = 0; a < res.dirs.length; a++) {\n\t\t\t\tvar dh = res.dirs[a].href,\n\t\t\t\t\tdn = dh.split('/')[0].split('?')[0],\n\t\t\t\t\tm = /[?&](k=[^&#]+)/.exec(dh);\n\n\t\t\t\tif (m)\n\t\t\t\t\tdn += '?' + m[1];\n\n\t\t\t\tdirs.push(dn);\n\t\t\t}\n\n\t\t\tr.rendertree({ \"a\": dirs }, this.ts, \".\", get_evpath() + (dk ? '?k=' + dk : ''));\n\t\t\tif (res.unlist)\n\t\t\t\tr.prunetree(res);\n\t\t}\n\n\t\tr.gentab(this.top, res);\n\t\tqsr('#dlt_t');\n\t\tqsr('#dlt_f');\n\n\t\tvar lg0 = res.logues ? res.logues[0] || \"\" : \"\",\n\t\t\tlg1 = res.logues ? res.logues[1] || \"\" : \"\",\n\t\t\tmds = res.readmes && treectl.ireadme,\n\t\t\tmd0 = mds ? res.readmes[0] || \"\" : \"\",\n\t\t\tmd1 = mds ? res.readmes[1] || \"\" : \"\",\n\t\t\tdirchg = get_evpath() != cdir;\n\n\t\tif (lg1 === Ls.eng.f_empty)\n\t\t\tlg1 = L.f_empty;\n\n\t\tsandbox(ebi('pro'), sb_lg, sba_lg,'', lg0);\n\t\tif (dirchg)\n\t\t\tsandbox(ebi('epi'), sb_lg, sba_lg, '', lg1);\n\n\t\tclmod(ebi('pro'), 'mdo');\n\t\tclmod(ebi('epi'), 'mdo');\n\n\t\tif (md0)\n\t\t\tshow_readme(md0, 0);\n\n\t\tif (md1)\n\t\t\tshow_readme(md1, 1);\n\t\telse if (!dirchg)\n\t\t\tsandbox(ebi('epi'), sb_lg, sba_lg, '', lg1);\n\n\t\tif (this.hpush && !this.back) {\n\t\t\tvar ofs = ebi('wrap').offsetTop;\n\t\t\tif (document.documentElement.scrollTop > ofs)\n\t\t\t\tdocument.documentElement.scrollTop = ofs;\n\t\t}\n\n\t\twintitle();\n\t\tvar fun = r.ls_cb;\n\t\tif (fun) {\n\t\t\tr.ls_cb = null;\n\t\t\tfun();\n\t\t}\n\n\t\tif (can_shr && in_shr && QS('#op_unpost.act'))\n\t\t\tgoto('unpost');\n\t}\n\n\tr.chk_index_html = function (top, res) {\n\t\tif (!r.idxh || !res || !res.files || noih)\n\t\t\treturn;\n\n\t\tfor (var a = 0; a < res.files.length; a++)\n\t\t\tif (/^index.html?(\\?|$)/i.exec(res.files[a].href)) {\n\t\t\t\tlocation = vjoin(top, res.files[a].href);\n\t\t\t\treturn true;\n\t\t\t}\n\t};\n\n\tr.gentab = function (top, res) {\n\t\tshowfile.untail();\n\t\tvar nodes = res.dirs.concat(res.files),\n\t\t\thtml = mk_files_header(res.taglist),\n\t\t\tsel = msel.hist[top],\n\t\t\tae = document.activeElement,\n\t\t\tcid = null,\n\t\t\tplain = [],\n\t\t\tseen = {};\n\n\t\tin_shr = have_shr && top.startsWith(SR + have_shr);\n\n\t\tif (ae && /^tr$/i.exec(ae.nodeName))\n\t\t\tif (ae = ae.querySelector('a[id]'))\n\t\t\t\tcid = ae.getAttribute('id');\n\n\t\tvar m = /[?&]k=([^&]+)/.exec(location.search);\n\t\tif (m)\n\t\t\tmemo_dk(top, m[1]);\n\n\t\tr.lsc = res;\n\t\tif (res.unlist && !r.dots) {\n\t\t\tvar ptn = new RegExp(res.unlist);\n\t\t\tfor (var a = nodes.length - 1; a >= 0; a--)\n\t\t\t\tif (ptn.exec(uricom_dec(nodes[a].href.split('?')[0])))\n\t\t\t\t\tnodes.splice(a, 1);\n\t\t}\n\t\tnodes = sortfiles(nodes);\n\t\twindow.removeEventListener('scroll', r.tscroll);\n\t\tr.trunc = nodes.length > r.nvis && location.hash.length < 2;\n\t\tif (r.trunc) {\n\t\t\tfor (var a = r.lim; a < nodes.length; a++) {\n\t\t\t\tvar tn = nodes[a],\n\t\t\t\t\ttns = Object.keys(tn.tags || {});\n\n\t\t\t\tplain.push(uricom_dec(tn.href.split('?')[0]));\n\n\t\t\t\tfor (var b = 0; b < tns.length; b++)\n\t\t\t\t\tif (has(res.taglist, tns[b]))\n\t\t\t\t\t\tplain.push(tn.tags[tns[b]]);\n\t\t\t}\n\t\t\tnodes = nodes.slice(0, r.nvis);\n\t\t}\n\n\t\tshowfile.files = [];\n\t\thtml.push('<tbody>');\n\t\tfor (var a = 0; a < nodes.length; a++) {\n\t\t\tvar tn = nodes[a],\n\t\t\t\tbhref = tn.href.split('?')[0],\n\t\t\t\tfname = uricom_dec(bhref),\n\t\t\t\thname = esc(fname),\n\t\t\t\tid = 'f-' + ('00000000' + crc32(fname)).slice(-8),\n\t\t\t\tlang = showfile.getlang(fname);\n\n\t\t\twhile (seen[id])  // ejyefs ev69gg y9j8sg .opus\n\t\t\t\tid += 'a';\n\t\t\tseen[id] = 1;\n\n\t\t\tif (lang) {\n\t\t\t\tshowfile.files.push({ 'id': id, 'name': fname });\n\t\t\t\tif (lang == 'md')\n\t\t\t\t\ttn.href = addq(tn.href, 'v');\n\t\t\t}\n\n\t\t\tif (tn.lead == '-')\n\t\t\t\ttn.lead = '<a href=\"?doc=' + bhref + '\" id=\"t' + id +\n\t\t\t\t\t'\" rel=\"nofollow\" class=\"doc' + (lang ? ' bri' : '') +\n\t\t\t\t\t'\" hl=\"' + id + '\" name=\"' + hname + '\">-txt-</a>';\n\n\t\t\tvar cl = /\\.PARTIAL$/.exec(fname) ? ' class=\"fade\"' : '',\n\t\t\t\tln = ['<tr' + cl + '><td>' + tn.lead + '</td><td><a href=\"' +\n\t\t\t\t\ttop + tn.href + '\" id=\"' + id + '\">' + hname +\n\t\t\t\t\t'</a></td><td sortv=\"' + tn.sz + '\">' + filesizefun(tn.sz)];\n\n\t\t\tfor (var b = 0; b < res.taglist.length; b++) {\n\t\t\t\tvar k = esc(res.taglist[b]),\n\t\t\t\t\tv = (tn.tags || {})[k] || \"\",\n\t\t\t\t\tsv = null;\n\n\t\t\t\tif (k == \".dur\")\n\t\t\t\t\tsv = v ? s2ms(v) : \"\";\n\t\t\t\telse if (k == \".up_at\")\n\t\t\t\t\tsv = v ? unix2ui(v) : \"\";\n\t\t\t\telse {\n\t\t\t\t\tln.push(esc('' + v));\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tln[ln.length - 1] += '</td><td sortv=\"' + v + '\">' + sv;\n\t\t\t}\n\t\t\tln = ln.concat([tn.ext, unix2ui(tn.ts)]).join('</td><td>');\n\t\t\thtml.push(ln + '</td></tr>');\n\t\t}\n\t\thtml.push('</tbody>');\n\t\thtml = html.join('\\n');\n\t\tset_files_html(html);\n\t\tif (r.dlni) {\n\t\t\tvar o = QSA('#files a[id]');\n\t\t\tfor (var a = 0, aa = o.length; a < aa; a++)\n\t\t\t\to[a].setAttribute('download', '');\n\t\t}\n\t\tif (r.trunc) {\n\t\t\tr.setlazy(plain);\n\t\t\tif (!r.ask) {\n\t\t\t\twindow.addEventListener('scroll', r.tscroll);\n\t\t\t\tsetTimeout(r.tscroll, 100);\n\t\t\t}\n\t\t}\n\t\telse ebi('lazy').innerHTML = '';\n\n\t\tfunction asdf() {\n\t\t\tshowfile.mktree();\n\t\t\tmukey.render();\n\t\t\treload_tree();\n\t\t\treload_browser();\n\t\t\ttree_scrollto();\n\t\t\tif (res.cfg) {\n\t\t\t\tacct = res.acct;\n\t\t\t\thave_up2k_idx = res.idx;\n\t\t\t\thave_tags_idx = res.itag;\n\t\t\t\tlifetime = res.lifetime;\n\t\t\t\tapply_perms(res);\n\t\t\t\tfileman.render();\n\t\t\t}\n\t\t\tmsel.loadsel(top, sel);\n\n\t\t\tif (cid) try {\n\t\t\t\tebi(cid).closest('tr').focus();\n\t\t\t} catch (ex) { }\n\n\t\t\tsetTimeout(eval_hash, 1);\n\t\t}\n\n\t\tvar m = scan_hash(hash0),\n\t\t\turl = null;\n\n\t\tif (m) {\n\t\t\turl = ebi(m[1]);\n\t\t\tif (url) {\n\t\t\t\turl = url.href;\n\t\t\t\tvar mt = m[0] == 'a' ? 'audio' : /\\.(webm|mkv)($|\\?)/i.exec(url) ? 'video' : 'image'\n\t\t\t\tif (mt == 'image') {\n\t\t\t\t\turl = addq(url, 'cache');\n\t\t\t\t\tconsole.log(url);\n\t\t\t\t\tnew Image().src = url;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (url) setTimeout(asdf, 1); else asdf();\n\t}\n\n\tr.hydrate = function () {\n\t\tqsr('#bbsw');\n\t\tsrvinf = ebi('srv_info').innerHTML.slice(6, -7);\n\t\tif (ls0 === null) {\n\t\t\tr.ls_cb = showfile.addlinks;\n\t\t\treturn setck('js=y', function () {\n\t\t\t\tr.reqls(get_evpath(), false, undefined, true);\n\t\t\t});\n\t\t}\n\t\tls0.unlist = unlist0;\n\t\tls0.u2ts = u2ts;\n\n\t\tvar top = get_evpath();\n\t\tif (r.chk_index_html(top, ls0))\n\t\t\treturn;\n\n\t\tr.gentab(top, ls0);\n\t\tpbar.onresize();\n\t\tvbar.onresize();\n\t\tshowfile.addlinks();\n\t\tsetTimeout(eval_hash, 1);\n\t};\n\n\tfunction memo_dk(vp, k) {\n\t\tdks[vp] = k;\n\t\tvar lv = vp + \"?\" + k;\n\t\tif (has(ldks, lv))\n\t\t\treturn;\n\n\t\tldks.unshift(lv);\n\t\tif (ldks.length > 32) {\n\t\t\tvar keep = [], evp = get_evpath();\n\t\t\tfor (var a = 0; a < ldks.length; a++) {\n\t\t\t\tvar s = ldks[a];\n\t\t\t\tif (evp.startsWith(s.replace(/\\?[^?]+$/, '')))\n\t\t\t\t\tkeep.push(s);\n\t\t\t}\n\t\t\tvar lim = 32 - keep.length;\n\t\t\tfor (var a = 0; a < lim; a++) {\n\t\t\t\tif (!has(keep, ldks[a]))\n\t\t\t\t\tkeep.push(ldks[a])\n\t\t\t}\n\t\t\tldks = keep;\n\t\t}\n\t\tjwrite('dks', ldks);\n\t}\n\n\tr.setlazy = function (plain) {\n\t\tvar html = ['<div id=\"plazy\">', esc(plain.join(' ')), '</div>'],\n\t\t\tall = r.lsc.files.length + r.lsc.dirs.length,\n\t\t\tnxt = r.nvis * 4;\n\n\t\tif (r.ask)\n\t\t\thtml.push((nxt >= all ? L.fbd_all : L.fbd_more).format(r.nvis, all, nxt));\n\n\t\tebi('lazy').innerHTML = html.join('\\n');\n\n\t\ttry {\n\t\t\tebi('bd_all').onclick = function (e) {\n\t\t\t\tev(e);\n\t\t\t\tr.showmore(all);\n\t\t\t};\n\t\t\tebi('bd_more').onclick = function (e) {\n\t\t\t\tev(e);\n\t\t\t\tr.showmore(nxt);\n\t\t\t};\n\t\t}\n\t\tcatch (ex) { }\n\t};\n\n\tr.showmore = function (n, cb) {\n\t\twindow.removeEventListener('scroll', r.tscroll);\n\t\tconsole.log('nvis {0} -> {1}'.format(r.nvis, n));\n\t\tr.nvis = n;\n\t\tebi('lazy').innerHTML = '';\n\t\tebi('wrap').style.opacity = 0.4;\n\t\tdocument.documentElement.scrollLeft = 0;\n\t\tsetTimeout(function () {\n\t\t\tr.gentab(get_evpath(), r.lsc);\n\t\t\tebi('wrap').style.opacity = CLOSEST ? 'unset' : 1;\n\t\t\tif (cb)\n\t\t\t\tcb();\n\t\t}, 1);\n\t};\n\n\tr.tscroll = function () {\n\t\tvar el = r.trunc ? ebi('plazy') : null;\n\t\tif (!el || ebi('lazy').style.display || ebi('unsearch'))\n\t\t\treturn;\n\n\t\tvar sy = yscroll() + window.innerHeight,\n\t\t\tty = el.offsetTop;\n\n\t\tif (sy <= ty)\n\t\t\treturn;\n\n\t\twindow.removeEventListener('scroll', r.tscroll);\n\n\t\tvar all = r.lsc.files.length + r.lsc.dirs.length;\n\t\tif (r.nvis * 16 <= all) {\n\t\t\tconsole.log(\"{0} ({1} * 16) <= {2}\".format(r.nvis * 16, r.nvis, all));\n\t\t\tr.showmore(r.nvis * 4);\n\t\t}\n\t\telse {\n\t\t\tconsole.log(\"{0} ({1} * 16) > {2}\".format(r.nvis * 16, r.nvis, all));\n\t\t\tr.showmore(all);\n\t\t}\n\t};\n\n\tfunction parsetree(res, top) {\n\t\tvar ret = '';\n\t\tfor (var a = 0; a < res.a.length; a++) {\n\t\t\tif (res.a[a] !== '')\n\t\t\t\tres['k' + res.a[a]] = 0;\n\t\t}\n\t\tdelete res['a'];\n\t\tvar keys = Object.keys(res);\n\t\tfor (var a = 0; a < keys.length; a++)\n\t\t\tkeys[a] = [uricom_dec(keys[a]), keys[a]];\n\n\t\tif (ENATSORT)\n\t\t\tkeys.sort(function (a, b) { return NATSORT.compare(a[0], b[0]); });\n\t\telse\n\t\t\tkeys.sort(function (a, b) { return a[0].localeCompare(b[0]); });\n\n\t\tfor (var a = 0; a < keys.length; a++) {\n\t\t\tvar kk = keys[a][1],\n\t\t\t\tm = /(\\?k=[^\\n]+)/.exec(kk),\n\t\t\t\tkdk = m ? m[1] : '',\n\t\t\t\tks = kk.replace(kdk, '').slice(1),\n\t\t\t\tded = ks.endsWith('\\n'),\n\t\t\t\tk = uricom_sdec(ded ? ks.replace(/\\n$/, '') : ks),\n\t\t\t\thek = esc(k[0]),\n\t\t\t\tuek = k[1] ? uricom_enc(k[0], true) : k[0],\n\t\t\t\turl = '/' + (top ? top + uek : uek) + '/',\n\t\t\t\tsym = res[kk] ? '-' : '+',\n\t\t\t\tlink = '<a href=\"#\">' + sym + '</a><a href=\"' +\n\t\t\t\t\tSR + url + kdk + '\">' + hek + '</a>';\n\n\t\t\tif (res[kk]) {\n\t\t\t\tvar subtree = parsetree(res[kk], url.slice(1));\n\t\t\t\tret += '<li>' + link + '\\n<ul>\\n' + subtree + '</ul></li>\\n';\n\t\t\t}\n\t\t\telse {\n\t\t\t\tret += (ded ? '<li class=\"offline\">' : '<li>') + link + '</li>\\n';\n\t\t\t}\n\t\t}\n\t\treturn ret;\n\t}\n\n\tfunction scaletree(e) {\n\t\tev(e);\n\t\ttreesz += parseInt(this.getAttribute(\"step\"));\n\t\tif (!isNum(treesz))\n\t\t\ttreesz = 16;\n\n\t\ttreesz = clamp(treesz, 2, 120);\n\t\tswrite('treesz', treesz);\n\t\tonresize();\n\t}\n\n\tebi('entree').onclick = r.entree;\n\tebi('detree').onclick = r.detree;\n\tebi('visdir').onclick = tree_scrollto;\n\tebi('twig').onclick = scaletree;\n\tebi('twobytwo').onclick = scaletree;\n\n\tvar cs = sread('entreed'),\n\t\tvw = window.innerWidth / parseFloat(getComputedStyle(document.body)['font-size']);\n\n\tif (notree) {\n\t\tcs = 'na';\n\t\tr.detree(null, 1);\n\t}\n\n\tif (cs == 'tree' || (cs != 'na' && vw >= 60))\n\t\tr.entree(null, true);\n\n\tr.onpopfun = function (e) {\n\t\tconsole.log(\"h-pop \" + e.state);\n\t\tif (!e.state)\n\t\t\treturn;\n\n\t\tvar url = new URL(e.state, \"https://\" + location.host),\n\t\t\treq = url.pathname,\n\t\t\thbase = req,\n\t\t\tcbase = location.pathname,\n\t\t\tmdoc = /[?&]doc=/.exec('' + url),\n\t\t\tmdk = /[?&](k=[^&#]+)/.exec('' + url);\n\n\t\tif (mdoc && hbase == cbase)\n\t\t\treturn showfile.show(hbase + showfile.sname(url.search), true);\n\n\t\tif (mdk)\n\t\t\treq += '?' + mdk[1];\n\n\t\tr.goto(req, false, true);\n\t};\n\n\tvar evp = get_evpath() + (dk ? '?k=' + dk : '');\n\thist_replace(evp + location.hash);\n\tr.onscroll = onscroll;\n\treturn r;\n})();\n\n\nfunction enspin(i) {\n\ti = 'dlt_' + i;\n\tif (ebi(i))\n\t\treturn;\n\tvar d = mknod('div', i, SPINNER);\n\td.className = 'dumb_loader_thing';\n\tif (SPINNER_CSS)\n\t\td.style.cssText = SPINNER_CSS;\n\tdocument.body.appendChild(d);\n}\n\n\nvar wfp_debounce = (function () {\n\tvar r = { 'n': 0, 't': 0 };\n\n\tr.hide = function () {\n\t\tif (!sb_lg && !sb_md)\n\t\t\treturn;\n\n\t\tif (++r.n <= 1) {\n\t\t\tr.n = 1;\n\t\t\tclearTimeout(r.t);\n\t\t\tr.t = setTimeout(r.reset, 300);\n\t\t\tebi('wfp').style.opacity = 0.1;\n\t\t}\n\t};\n\tr.show = function () {\n\t\tif (!sb_lg && !sb_md)\n\t\t\treturn;\n\n\t\tif (--r.n <= 0) {\n\t\t\tr.n = 0;\n\t\t\tclearTimeout(r.t);\n\t\t\tebi('wfp').style.opacity = CLOSEST ? 'unset' : 1;\n\t\t}\n\t};\n\tr.reset = function () {\n\t\tr.n = 0;\n\t\tr.show();\n\t};\n\treturn r;\n})();\n\n\nfunction apply_perms(res) {\n\tperms = res.perms || [];\n\n\tvar axs = [],\n\t\taclass = '>',\n\t\tchk = ['read', 'write', 'move', 'delete', 'get', 'admin'];\n\n\tif (konmai < 0) {\n\t\tacct = 'Ted Faro';\n\t\tsrvinf = 'FAS Nexus</span> // <span>57.3 EiB free of 127 EiB';\n\t\tres.shr_who = 'auth';\n\t\tperms = res.perms = chk;\n\t\thave_up2k_idx = have_tags_idx = 1;\n\t\thave_mv = have_del = true;\n\t}\n\n\tvar a = QS('#ops a[data-dest=\"up2k\"]');\n\tif (have_up2k_idx) {\n\t\ta.removeAttribute('data-perm');\n\t\ta.setAttribute('tt', L.ot_u2i);\n\t}\n\telse {\n\t\ta.setAttribute('data-perm', 'write');\n\t\ta.setAttribute('tt', L.ot_u2w);\n\t}\n\tclmod(ebi('srch_form'), 'tags', have_tags_idx);\n\n\ta.style.display = '';\n\ttt.att(QS('#ops'));\n\n\tfor (var a = 0; a < chk.length; a++)\n\t\tif (has(perms, chk[a]))\n\t\t\taxs.push(chk[a].slice(0, 1).toUpperCase() + chk[a].slice(1));\n\n\taxs = axs.join('-');\n\tif (perms.length == 1) {\n\t\taclass = ' class=\"warn\">';\n\t\taxs += '-Only';\n\t}\n\n\tvar dst = \"?h\";\n\tif (idp_login && acct == \"*\")\n\t\tdst = idp_login.replace(/\\{dst\\}/g, get_evpath());\n\n\tebi('acc_info').innerHTML = '<span id=\"srv_info2\"><span>' + srvinf +\n\t\t'</span></span><span' + aclass + axs + L.access + '</span>' + (acct != '*' ?\n\t\t\t'<form id=\"flogout\" method=\"post\" enctype=\"multipart/form-data\"><input type=\"hidden\" name=\"act\" value=\"logout\" /><input id=\"blogout\" type=\"submit\" value=\"' + L.logout + acct + '\"></form>' :\n\t\t\t'<a href=\"' + dst + '\">' + L.login + '</a>');\n\n\tvar o = QSA('#ops>a[data-perm]');\n\tfor (var a = 0; a < o.length; a++) {\n\t\tvar display = '';\n\t\tvar needed = o[a].getAttribute('data-perm').split(' ');\n\t\tfor (var b = 0; b < needed.length; b++) {\n\t\t\tif (!has(perms, needed[b])) {\n\t\t\t\tdisplay = 'none';\n\t\t\t}\n\t\t}\n\t\to[a].style.display = display;\n\t}\n\n\tvar o = QSA('#ops>a[data-dep], #u2conf td[data-dep]');\n\tfor (var a = 0; a < o.length; a++)\n\t\to[a].style.display = (\n\t\t\to[a].getAttribute('data-dep') != 'idx' || have_up2k_idx\n\t\t) ? '' : 'none';\n\n\tif (in_shr)\n\t\tebi('opa_srch').style.display = 'none';\n\n\tvar act = QS('#ops>a.act');\n\tif (act && act.style.display === 'none')\n\t\tgoto();\n\n\tdocument.body.setAttribute('perms', perms.join(' '));\n\n\tvar have_write = has(perms, \"write\"),\n\t\thave_read = has(perms, \"read\"),\n\t\tde = document.documentElement,\n\t\ttds = QSA('#u2conf td');\n\n\tshr_who = res.shr_who || shr_who;\n\tcan_shr = acct != '*' && (have_read || have_write) && (\n\t\t(shr_who == 'a' && has(perms, 'admin')) ||\n\t\t(shr_who == 'auth'));\n\n\tclmod(de, \"read\", have_read);\n\tclmod(de, \"write\", have_write);\n\tclmod(de, \"nread\", !have_read);\n\tclmod(de, \"nwrite\", !have_write);\n\n\tfor (var a = 0; a < tds.length; a++) {\n\t\ttds[a].style.display =\n\t\t\t(have_write || tds[a].getAttribute('data-perm') == 'read') ?\n\t\t\t\t'table-cell' : 'none';\n\t}\n\tif (res.frand)\n\t\tebi('u2rand').parentNode.style.display = 'none';\n\n\tu2ts = res.u2ts;\n\tif (up2k)\n\t\tup2k.set_fsearch();\n\n\tif (res.cfg)\n\t\trw_edit = res.rw_edit;\n\tenre_rw_edit();\n\tebi('new_mdi').innerHTML = has(perms, \"delete\") ? L.nmd_i1 : L.nmd_i2.format(rw_edit.replace(/,/g, '/'));\n\n\twidget.setvis();\n\tthegrid.setvis();\n\tif (!have_read && have_write)\n\t\tgoto('up2k');\n}\n\n\nfunction tr2id(tr) {\n\ttry {\n\t\treturn tr.cells[1].querySelector('a[id]').getAttribute('id');\n\t}\n\tcatch (ex) {\n\t\treturn null;\n\t}\n}\n\n\nfunction find_file_col(txt) {\n\tvar i = -1,\n\t\tmin = false,\n\t\ttds = ebi('files').tHead.getElementsByTagName('th');\n\n\tfor (var a = 0; a < tds.length; a++) {\n\t\tvar spans = tds[a].getElementsByTagName('span');\n\t\tif (spans.length && spans[0].textContent == txt) {\n\t\t\tmin = (tds[a].className || '').indexOf('min') !== -1;\n\t\t\ti = a;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tif (i == -1)\n\t\treturn;\n\n\treturn [i, min];\n}\n\n\nfunction mk_files_header(taglist) {\n\tvar html = [\n\t\t'<thead><tr>',\n\t\t'<th name=\"lead\"><span>c</span></th>',\n\t\t'<th name=\"href\"><span>File Name</span></th>',\n\t\t'<th name=\"sz\" sort=\"int\"><span>Size</span></th>'\n\t];\n\tfor (var a = 0; a < taglist.length; a++) {\n\t\tvar tag = taglist[a],\n\t\t\tc1 = tag.slice(0, 1).toUpperCase();\n\n\t\ttag = esc(c1 + tag.slice(1));\n\t\tif (c1 == '.')\n\t\t\ttag = '<th name=\"tags/' + tag + '\" sort=\"int\"><span>' + tag.slice(1);\n\t\telse\n\t\t\ttag = '<th name=\"tags/' + tag + '\"><span>' + tag;\n\n\t\thtml.push(tag + '</span></th>');\n\t}\n\thtml = html.concat([\n\t\t'<th name=\"ext\"><span>T</span></th>',\n\t\t'<th name=\"ts\"><span>Date</span></th>',\n\t\t'</tr></thead>',\n\t]);\n\treturn html;\n}\n\n\nvar filecols = (function () {\n\tvar r = { 'picking': false };\n\tvar hidden = jread('filecols', []);\n\n\tr.add_btns = function () {\n\t\tvar ths = QSA('#files>thead th>span');\n\t\tfor (var a = 0, aa = ths.length; a < aa; a++) {\n\t\t\tvar th = ths[a].parentElement,\n\t\t\t\ttoh = ths[a].outerHTML, // !ff10\n\t\t\t\tttv = L.cols[ths[a].textContent];\n\n\t\t\tttv = (ttv ? ttv + '; ' : '') + 'id=<code>' + th.getAttribute('name') + '</code>';\n\t\t\tif (!MOBILE && toh) {\n\t\t\t\tth.innerHTML = '<div class=\"cfg\"><a href=\"#\">-</a></div>' + toh;\n\t\t\t\tth.getElementsByTagName('a')[0].onclick = ev_row_tgl;\n\t\t\t}\n\t\t\tif (ttv) {\n\t\t\t\tth.setAttribute(\"tt\", ttv);\n\t\t\t\tth.setAttribute(\"ttd\", \"u\");\n\t\t\t\tth.setAttribute(\"ttm\", \"12\");\n\t\t\t}\n\t\t}\n\t};\n\n\tfunction hcols_click(e) {\n\t\tev(e);\n\t\tvar t = e.target;\n\t\tif (t.tagName != 'A')\n\t\t\treturn;\n\n\t\tr.toggle(t.textContent);\n\t}\n\n\tr.uivis = function () {\n\t\tvar hcols = ebi('hcols');\n\t\thcols.previousSibling.style.display = hcols.style.display = ((!thegrid || !thegrid.en) && (hidden.length || MOBILE)) ? 'block' : 'none';\n\t};\n\n\tr.set_style = function (unhide) {\n\t\thidden.sort();\n\n\t\tif (!unhide)\n\t\t\tunhide = [];\n\n\t\tvar html = [],\n\t\t\thcols = ebi('hcols');\n\n\t\tfor (var a = 0; a < hidden.length; a++) {\n\t\t\tvar ttv = L.cols[hidden[a]],\n\t\t\t\ttta = ttv ? ' tt=\"' + ttv + '\">' : '>';\n\n\t\t\thtml.push('<a href=\"#\" class=\"btn\"' + tta + esc(hidden[a]) + '</a>');\n\t\t}\n\t\thcols.innerHTML = html.join('\\n');\n\t\thcols.onclick = hcols_click;\n\t\tr.uivis();\n\t\tr.add_btns();\n\n\t\tvar ohidden = [],\n\t\t\tths = QSA('#files>thead th'),\n\t\t\tncols = ths.length;\n\n\t\tfor (var a = 0; a < ncols; a++) {\n\t\t\tvar span = ths[a].getElementsByTagName('span');\n\t\t\tif (span.length <= 0)\n\t\t\t\tcontinue;\n\n\t\t\tvar name = span[0].textContent,\n\t\t\t\tcls = false;\n\n\t\t\tif (has(hidden, name) && !has(unhide, name)) {\n\t\t\t\tohidden.push(a);\n\t\t\t\tcls = true;\n\t\t\t}\n\t\t\tclmod(ths[a], 'min', cls)\n\t\t}\n\t\tfor (var a = 0; a < ncols; a++) {\n\t\t\tvar cls = has(ohidden, a) ? 'min' : '',\n\t\t\t\ttds = QSA('#files>tbody>tr>td:nth-child(' + (a + 1) + ')');\n\n\t\t\tfor (var b = 0, bb = tds.length; b < bb; b++)\n\t\t\t\ttds[b].className = cls;\n\t\t}\n\t\tif (tt) {\n\t\t\ttt.att(ebi('hcols'));\n\t\t\ttt.att(QS('#files>thead'));\n\t\t}\n\t};\n\n\tr.setvis = function (name, vis) {\n\t\tvar ofs = hidden.indexOf(name);\n\t\tif (ofs !== -1 && vis != 0)\n\t\t\thidden.splice(ofs, 1);\n\t\telse if (vis != 1) {\n\t\t\tif (!sread(\"chide_ok\")) {\n\t\t\t\treturn modal.confirm(L.f_chide.format(name), function () {\n\t\t\t\t\tswrite(\"chide_ok\", 1);\n\t\t\t\t\tr.toggle(name);\n\t\t\t\t}, null);\n\t\t\t}\n\t\t\thidden.push(name);\n\t\t}\n\t\tjwrite(\"filecols\", hidden);\n\t\tr.set_style();\n\t};\n\tr.show = function (name) { r.setvis(name, 1); };\n\tr.hide = function (name) { r.setvis(name, 0); };\n\tr.toggle = function (name) { r.setvis(name, -1); };\n\n\tebi('hcolsr').onclick = function (e) {\n\t\tev(e);\n\t\tr.reset(true);\n\t};\n\n\tif (MOBILE)\n\t\tebi('hcolsh').onclick = function (e) {\n\t\t\tev(e);\n\t\t\tif (r.picking)\n\t\t\t\treturn r.unpick();\n\n\t\t\tvar lbs = QSA('#files>thead th');\n\t\t\tfor (var a = 0; a < lbs.length; a++) {\n\t\t\t\tlbs[a].onclick = function (e) {\n\t\t\t\t\tev(e);\n\t\t\t\t\tif (toast.tag == 'pickhide')\n\t\t\t\t\t\ttoast.hide();\n\n\t\t\t\t\tr.hide(e.target.textContent);\n\t\t\t\t};\n\t\t\t};\n\t\t\tr.picking = true;\n\t\t\tclmod(ebi('files'), 'hhpick', 1);\n\t\t\ttoast.inf(0, L.cl_hpick, 'pickhide');\n\t\t};\n\n\tr.unpick = function () {\n\t\tr.picking = false;\n\t\ttoast.inf(5, L.cl_hcancel);\n\n\t\tclmod(ebi('files'), 'hhpick');\n\n\t\tvar lbs = QSA('#files>thead th');\n\t\tfor (var a = 0; a < lbs.length; a++)\n\t\t\tlbs[a].onclick = null;\n\t};\n\n\tr.reset = function (force) {\n\t\tif (force || JSON.stringify(def_hcols) != sread('hfilecols')) {\n\t\t\tconsole.log(\"applying default hidden-cols\");\n\t\t\thidden = [];\n\t\t\tjwrite('hfilecols', def_hcols);\n\t\t\tfor (var a = 0; a < def_hcols.length; a++) {\n\t\t\t\tvar t = def_hcols[a];\n\t\t\t\tt = t.slice(0, 1).toUpperCase() + t.slice(1);\n\t\t\t\tif (t.startsWith(\".\"))\n\t\t\t\t\tt = t.slice(1);\n\n\t\t\t\tif (hidden.indexOf(t) == -1)\n\t\t\t\t\thidden.push(t);\n\t\t\t}\n\t\t\tjwrite(\"filecols\", hidden);\n\t\t}\n\t\tr.set_style();\n\t}\n\tr.reset();\n\n\ttry {\n\t\tvar ci = find_file_col('dur'),\n\t\t\ti = ci[0],\n\t\t\trows = ebi('files').tBodies[0].rows;\n\n\t\tfor (var a = 0, aa = rows.length; a < aa; a++) {\n\t\t\tvar c = rows[a].cells[i];\n\t\t\tif (c && c.textContent)\n\t\t\t\tc.textContent = s2ms(c.textContent);\n\t\t}\n\t}\n\tcatch (ex) { }\n\n\treturn r;\n})();\n\n\nvar mukey = (function () {\n\tvar maps = {\n\t\t\"rekobo_alnum\": [\n\t\t\t\"1B \", \"2B \", \"3B \", \"4B \", \"5B \", \"6B \", \"7B \", \"8B \", \"9B \", \"10B\", \"11B\", \"12B\",\n\t\t\t\"1A \", \"2A \", \"3A \", \"4A \", \"5A \", \"6A \", \"7A \", \"8A \", \"9A \", \"10A\", \"11A\", \"12A\"\n\t\t],\n\t\t\"rekobo_classic\": [\n\t\t\t\"B  \", \"F# \", \"Db \", \"Ab \", \"Eb \", \"Bb \", \"F  \", \"C  \", \"G  \", \"D  \", \"A  \", \"E  \",\n\t\t\t\"Abm\", \"Ebm\", \"Bbm\", \"Fm \", \"Cm \", \"Gm \", \"Dm \", \"Am \", \"Em \", \"Bm \", \"F#m\", \"Dbm\"\n\t\t],\n\t\t\"traktor_musical\": [\n\t\t\t\"B  \", \"Gb \", \"Db \", \"Ab \", \"Eb \", \"Bb \", \"F  \", \"C  \", \"G  \", \"D  \", \"A  \", \"E  \",\n\t\t\t\"Abm\", \"Ebm\", \"Bbm\", \"Fm \", \"Cm \", \"Gm \", \"Dm \", \"Am \", \"Em \", \"Bm \", \"Gbm\", \"Dbm\"\n\t\t],\n\t\t\"traktor_sharps\": [\n\t\t\t\"B  \", \"F# \", \"C# \", \"G# \", \"D# \", \"A# \", \"F  \", \"C  \", \"G  \", \"D  \", \"A  \", \"E  \",\n\t\t\t\"G#m\", \"D#m\", \"A#m\", \"Fm \", \"Cm \", \"Gm \", \"Dm \", \"Am \", \"Em \", \"Bm \", \"F#m\", \"C#m\"\n\t\t],\n\t\t\"traktor_open\": [\n\t\t\t\"6d \", \"7d \", \"8d \", \"9d \", \"10d\", \"11d\", \"12d\", \"1d \", \"2d \", \"3d \", \"4d \", \"5d \",\n\t\t\t\"6m \", \"7m \", \"8m \", \"9m \", \"10m\", \"11m\", \"12m\", \"1m \", \"2m \", \"3m \", \"4m \", \"5m \"\n\t\t]\n\t},\n\t\tdefnot = 'rekobo_alnum';\n\n\tvar map = {},\n\t\thtml = [],\n\t\tcb = ebi('key_notation');\n\n\tfor (var k in maps) {\n\t\tif (!maps.hasOwnProperty(k))\n\t\t\tcontinue;\n\n\t\thtml.push('<option value=\"{0}\">{0}</option>'.format(k));\n\t\tfor (var a = 0; a < 24; a++)\n\t\t\tmaps[k][a] = maps[k][a].trim();\n\t}\n\tcb.innerHTML = html.join('');\n\n\tfunction set_key_notation() {\n\t\tload_notation(cb.value);\n\t\ttry_render();\n\t}\n\n\tfunction load_notation(notation) {\n\t\tswrite(\"cpp_keynot\", notation);\n\t\tmap = {};\n\t\tvar dst = maps[notation];\n\t\tfor (var k in maps)\n\t\t\tif (k != notation && maps.hasOwnProperty(k))\n\t\t\t\tfor (var a = 0; a < 24; a++)\n\t\t\t\t\tif (maps[k][a] != dst[a])\n\t\t\t\t\t\tmap[maps[k][a]] = dst[a];\n\t}\n\n\tfunction render() {\n\t\tvar ci = find_file_col('Key');\n\t\tif (!ci)\n\t\t\treturn;\n\n\t\tvar i = ci[0],\n\t\t\tmin = ci[1],\n\t\t\trows = ebi('files').tBodies[0].rows;\n\n\t\tif (min)\n\t\t\tfor (var a = 0, aa = rows.length; a < aa; a++) {\n\t\t\t\tvar c = rows[a].cells[i];\n\t\t\t\tif (!c)\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar v = c.getAttribute('html');\n\t\t\t\tc.setAttribute('html', map[v] || v);\n\t\t\t}\n\t\telse\n\t\t\tfor (var a = 0, aa = rows.length; a < aa; a++) {\n\t\t\t\tvar c = rows[a].cells[i];\n\t\t\t\tif (!c)\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar v = c.textContent;\n\t\t\t\tc.textContent = map[v] || v;\n\t\t\t}\n\t}\n\n\tfunction try_render() {\n\t\ttry {\n\t\t\trender();\n\t\t}\n\t\tcatch (ex) {\n\t\t\tconsole.log(\"key notation failed: \" + ex);\n\t\t}\n\t}\n\n\tvar notation = sread(\"cpp_keynot\") || defnot;\n\tif (!maps[notation])\n\t\tnotation = defnot;\n\n\tcb.value = notation;\n\tcb.onchange = set_key_notation;\n\tload_notation(notation);\n\n\treturn {\n\t\t\"render\": try_render\n\t};\n})();\n\n\nvar light, theme, themen;\nvar settheme = (function () {\n\tvar r = {},\n\t\tax = 'abcdefghijklmnopqrstuvwx',\n\t\ttre = '🌲',\n\t\tchldr = !SPINNER_CSS && SPINNER == tre;\n\n\tr.ldr = {\n\t\t'4':['🌴'],\n\t\t'5':['🌭', 'padding:0 0 .7em .7em;filter:saturate(3)'],\n\t\t'6':['📞', 'padding:0;filter:brightness(2) sepia(1) saturate(3) hue-rotate(60deg)'],\n\t\t'7':['▲', 'font-size:3em'], //cp437\n\t};\n\n\ttheme = sread('cpp_thm') || 'a';\n\tif (!/^[a-x][yz]/.exec(theme))\n\t\ttheme = dtheme;\n\n\tthemen = theme.split(/ /)[0];\n\tlight = !!(theme.indexOf('y') + 1);\n\n\tfunction freshen() {\n\t\tvar cl = document.documentElement.className;\n\t\tcl = cl.replace(/\\b(light|dark|[a-z]{1,2})\\b/g, '').replace(/ +/g, ' ');\n\t\tdocument.documentElement.className = cl + ' ' + theme + ' ';\n\n\t\tpbar.drawbuf();\n\t\tpbar.drawpos();\n\t\tvbar.draw();\n\t\tshowfile.setstyle();\n\t\tbchrome();\n\n\t\tvar html = [],\n\t\t\tcb = ebi('themes'),\n\t\t\titheme = ax.indexOf(theme[0]) * 2 + (light ? 1 : 0),\n\t\t\tnames = ['classic dark', 'classic light', 'pm-monokai', 'flat light', 'vice', 'hotdog stand', 'hacker', 'hi-con', 'phi95 dark', 'phi95'];\n\n\t\tfor (var a = 0; a < themes; a++)\n\t\t\thtml.push('<option value=\"{0}\">{0} ┃ {1}</option>'.format(a, names[a] || 'custom'));\n\n\t\tebi('themes').innerHTML = html.join('');\n\t\tcb.value = itheme;\n\t\tcb.onchange = r.onsel;\n\n\t\tif (chldr) {\n\t\t\tvar x = r.ldr[itheme] || [tre];\n\t\t\tSPINNER = x[0];\n\t\t\tSPINNER_CSS = x[1];\n\t\t}\n\n\t\tbcfg_set('light', light);\n\t}\n\n\tr.onsel = function () {\n\t\tr.go(parseInt(ebi('themes').value));\n\t};\n\n\tr.go = function (i) {\n\t\tlight = i % 2 == 1;\n\t\tvar c = ax[Math.floor(i / 2)],\n\t\t\tl = light ? 'y' : 'z';\n\t\ttheme = c + l + ' ' + c + ' ' + l;\n\t\tthemen = c + l;\n\t\tswrite('cpp_thm', theme);\n\t\tfreshen();\n\t};\n\n\tvar m = /[?&]theme=([0-9]+)/.exec(sloc0);\n\tif (m)\n\t\tr.go(parseInt(m[1]));\n\telse\n\t\tfreshen();\n\n\treturn r;\n})();\n\n\nvar setfszf = (function () {\n\tfunction freshen() {\n\t\tvar cb = ebi('fszfmt'),\n\t\t\tfmt = sread(\"fszfmt\", humansize_fmts) || window.dfszf;\n\t\tif (!has(humansize_fmts, fmt))\n\t\t\tfmt = '1';\n\t\twindow.filesizefun = window['humansize_' + fmt];\n\t\tcb.onchange = onch;\n\t\tif (cb.value != fmt)\n\t\t\tcb.value = fmt;\n\t}\n\tfunction onch(e) {\n\t\tev(e);\n\t\tsetfmt(ebi('fszfmt').value)\n\t}\n\tfunction setfmt(fmt) {\n\t\tswrite(\"fszfmt\", fmt);\n\t\tfreshen();\n\t\ttreectl.gentab(get_evpath(), treectl.lsc);\n\t}\n\tfreshen();\n\treturn setfmt;\n})();\n\n\n(function () {\n\tfunction freshen() {\n\t\tvar cb = ebi('langs'), html = [];\n\t\tfor (var a = 0; a < LANGN.length; a++) {\n\t\t\thtml.push('<option value=\"{0}\">{0} ┃ {1}</option>'.format(LANGN[a][0], LANGN[a][1]));\n\t\t}\n\t\tcb.innerHTML = html.join('');\n\t\tcb.onchange = setlang;\n\t\tcb.value = lang;\n\t}\n\n\tfunction setlang(e) {\n\t\tev(e);\n\t\tlang = ebi('langs').value;\n\t\tsetck('cplng=' + lang);\n\t\tfreshen();\n\t\tvar t = L.tt == 'English' ? '' : Ls.eng.lang_set;\n\t\tmodal.confirm(L.lang_set + \"\\n\\n\" + t, location.reload.bind(location), null);\n\t}\n\n\tfreshen();\n})();\n\n\nvar arcfmt = (function () {\n\tif (!ebi('arc_fmt'))\n\t\treturn { \"render\": function () { } };\n\n\tvar html = [],\n\t\tfmts = [\n\t\t\t[\"tar\", \"tar\", L.fz_tar],\n\t\t\t[\"pax\", \"tar=pax\", L.fz_pax],\n\t\t\t[\"tgz\", \"tar=gz\", L.fz_targz],\n\t\t\t[\"txz\", \"tar=xz\", L.fz_tarxz],\n\t\t\t[\"zip\", \"zip\", L.fz_zip8],\n\t\t\t[\"zip_dos\", \"zip=dos\", L.fz_zipd],\n\t\t\t[\"zip_crc\", \"zip=crc\", L.fz_zipc]\n\t\t];\n\n\tfor (var a = 0; a < fmts.length; a++) {\n\t\tvar k = fmts[a][0];\n\t\thtml.push(\n\t\t\t'<span><input type=\"radio\" name=\"arcfmt\" value=\"' + k + '\" id=\"arcfmt_' + k + '\" tt=\"' + fmts[a][2] + '\">' +\n\t\t\t'<label for=\"arcfmt_' + k + '\" tt=\"' + fmts[a][2] + '\">' + k + '</label></span>');\n\t}\n\tebi('arc_fmt').innerHTML = html.join('\\n');\n\n\tvar fmt = sread(\"arc_fmt\");\n\tif (!ebi('arcfmt_' + fmt))\n\t\tfmt = \"zip\";\n\n\tebi('arcfmt_' + fmt).checked = true;\n\n\tfunction render() {\n\t\tvar arg = null,\n\t\t\ttds = QSA('#files tbody td:first-child a');\n\n\t\tfor (var a = 0; a < fmts.length; a++)\n\t\t\tif (fmts[a][0] == fmt)\n\t\t\t\targ = fmts[a][1];\n\n\t\tfor (var a = 0, aa = tds.length; a < aa; a++) {\n\t\t\tvar o = tds[a], txt = o.textContent, href = o.getAttribute('href');\n\t\t\tif (!/^(zip|tar|pax|tgz|txz)$/.exec(txt))\n\t\t\t\tcontinue;\n\n\t\t\tvar m = /(.*[?&])(tar|zip)([^&#]*)(.*)$/.exec(href);\n\t\t\tif (!m)\n\t\t\t\tthrow new Error('missing arg in url');\n\n\t\t\to.setAttribute(\"href\", m[1] + arg + m[4]);\n\t\t\to.textContent = fmt.split('_')[0];\n\t\t}\n\t\tebi('selzip').textContent = fmt.split('_')[0];\n\t\tebi('selzip').setAttribute('fmt', arg);\n\n\t\tQS('#zip1 span').textContent = fmt.split('_')[0];\n\t\tebi('zip1').setAttribute(\"href\",\n\t\t\tget_evpath() + (dk ? '?k=' + dk + '&': '?') + arg);\n\n\t\tif (!have_zip) {\n\t\t\tebi('zip1').style.display = 'none';\n\t\t\tebi('selzip').style.display = 'none';\n\t\t}\n\t}\n\n\tfunction try_render() {\n\t\ttry {\n\t\t\trender();\n\t\t}\n\t\tcatch (ex) {\n\t\t\tconsole.log(\"arcfmt failed: \" + ex);\n\t\t}\n\t}\n\n\tfunction change_fmt(e) {\n\t\tev(e);\n\t\tfmt = this.getAttribute('value');\n\t\tswrite(\"arc_fmt\", fmt);\n\t\ttry_render();\n\t}\n\n\tvar o = QSA('#arc_fmt input');\n\tfor (var a = 0; a < o.length; a++) {\n\t\to[a].onchange = change_fmt;\n\t}\n\n\treturn {\n\t\t\"render\": try_render\n\t};\n})();\n\n\nvar msel = (function () {\n\tvar r = {};\n\tr.sel = null;\n\tr.all = null;\n\tr.hist = {};\n\tr.so = null;  // selection origin\n\tr.pr = null;  // previous range\n\n\tr.load = function (reset) {\n\t\tif (r.sel && !reset)\n\t\t\treturn;\n\n\t\tr.sel = [];\n\t\tif (r.all && r.all.length) {\n\t\t\tfor (var a = 0; a < r.all.length; a++) {\n\t\t\t\tvar ao = r.all[a];\n\t\t\t\tao.sel = clgot(ebi(ao.id).closest('tr'), 'sel');\n\t\t\t\tif (ao.sel)\n\t\t\t\t\tr.sel.push(ao);\n\t\t\t}\n\t\t\tif (!reset)\n\t\t\t\treturn;\n\t\t}\n\n\t\tr.all = [];\n\t\tvar links = QSA('#files tbody td:nth-child(2) a:last-child'),\n\t\t\tis_srch = !!ebi('unsearch'),\n\t\t\tvbase = get_evpath();\n\n\t\tfor (var a = 0, aa = links.length; a < aa; a++) {\n\t\t\tvar qhref = links[a].getAttribute('href'),\n\t\t\t\thref = qhref.split('?')[0],\n\t\t\t\titem = {};\n\n\t\t\tif (href.endsWith('/')) {\n\t\t\t\thref = href.slice(0, -1);\n\t\t\t\titem.isd = true;\n\t\t\t}\n\t\t\titem.id = links[a].getAttribute('id');\n\t\t\titem.sel = clgot(links[a].closest('tr'), 'sel');\n\t\t\titem.vp = href.indexOf('/') !== -1 ? href : vbase + href;\n\n\t\t\tif (dk) {\n\t\t\t\tvar m = /[?&](k=[^&#]+)/.exec(qhref);\n\t\t\t\titem.q = m ? '?' + m[1] : '';\n\t\t\t}\n\t\t\telse item.q = '';\n\n\t\t\tr.all.push(item);\n\t\t\tif (item.sel)\n\t\t\t\tr.sel.push(item);\n\n\t\t\tif (!is_srch)\n\t\t\t\tlinks[a].closest('tr').setAttribute('tabindex', '0');\n\t\t}\n\t};\n\n\tr.loadsel = function (vp, sel) {\n\t\tif (!sel || !r.so || !ebi(r.so))\n\t\t\tr.so = r.pr = null;\n\n\t\tif (!sel)\n\t\t\treturn r.origin_id(null);\n\n\t\tr.hist[vp] = sel;\n\t\tr.sel = [];\n\t\tr.load();\n\n\t\tvar vsel = new Set();\n\t\tfor (var a = 0; a < sel.length; a++)\n\t\t\tvsel.add(sel[a].vp);\n\n\t\tfor (var a = 0; a < r.all.length; a++)\n\t\t\tif (vsel.has(r.all[a].vp))\n\t\t\t\tclmod(ebi(r.all[a].id).closest('tr'), 'sel', 1);\n\n\t\tr.selui();\n\t};\n\n\tr.getsel = function () {\n\t\tr.load();\n\t\treturn r.sel;\n\t};\n\tr.getall = function () {\n\t\tr.load();\n\t\treturn r.all;\n\t};\n\tr.selui = function (reset) {\n\t\tr.sel = null;\n\t\tif (reset)\n\t\t\tr.all = null;\n\n\t\tclmod(ebi('wtoggle'), 'sel', r.getsel().length);\n\t\tthegrid.loadsel();\n\t\tfileman.render();\n\t\tshowfile.updtree();\n\n\t\tif (r.sel.length)\n\t\t\tr.hist[get_evpath()] = r.sel;\n\t\telse\n\t\t\tdelete r.hist[get_evpath()];\n\t};\n\tr.seltgl = function (e) {\n\t\tev(e);\n\t\tvar tr = this.parentNode,\n\t\t\tid = tr2id(tr);\n\n\t\tif ((treectl.csel || !thegrid.en || thegrid.sel) && e.shiftKey && r.so && id && r.so != id) {\n\t\t\tvar o1 = -1, o2 = -1;\n\t\t\tfor (a = 0; a < r.all.length; a++) {\n\t\t\t\tvar ai = r.all[a].id;\n\t\t\t\tif (ai == r.so)\n\t\t\t\t\to1 = a;\n\t\t\t\tif (ai == id)\n\t\t\t\t\to2 = a;\n\t\t\t}\n\t\t\tvar st = r.all[o1].sel;\n\t\t\tif (o1 > o2)\n\t\t\t\to2 = [o1, o1 = o2][0];\n\n\t\t\tif (r.pr) {\n\t\t\t\t// invert previous range, in case it was narrowed\n\t\t\t\tfor (var a = r.pr[0]; a <= r.pr[1]; a++)\n\t\t\t\t\tclmod(ebi(r.all[a].id).closest('tr'), 'sel', !st);\n\n\t\t\t\t// and invert current selection if repeated\n\t\t\t\tif (r.pr[0] === o1 && r.pr[1] === o2)\n\t\t\t\t\tst = !st;\n\t\t\t}\n\n\t\t\tfor (var a = o1; a <= o2; a++)\n\t\t\t\tclmod(ebi(r.all[a].id).closest('tr'), 'sel', st);\n\n\t\t\tr.pr = [o1, o2];\n\n\t\t\tif (window.getSelection)\n\t\t\t\twindow.getSelection().removeAllRanges();\n\t\t}\n\t\telse {\n\t\t\tclmod(tr, 'sel', 't');\n\t\t\tr.origin_tr(tr);\n\t\t}\n\t\tr.selui();\n\t};\n\tr.origin_tr = function (tr) {\n\t\tr.so = tr2id(tr);\n\t\tr.pr = null;\n\t};\n\tr.origin_id = function (id) {\n\t\tr.so = id;\n\t\tr.pr = null;\n\t};\n\tr.evsel = function (e, fun) {\n\t\tev(e);\n\t\tr.so = r.pr = null;\n\t\tvar trs = QSA('#files tbody tr');\n\t\tfor (var a = 0, aa = trs.length; a < aa; a++)\n\t\t\tclmod(trs[a], 'sel', fun);\n\t\tr.selui();\n\t}\n\tebi('selall').onclick = function (e) {\n\t\tr.evsel(e, \"add\");\n\t};\n\tebi('selinv').onclick = function (e) {\n\t\tr.evsel(e, \"t\");\n\t};\n\tebi('selzip').onclick = function (e) {\n\t\tev(e);\n\t\tvar sel = r.getsel(),\n\t\t\targ = ebi('selzip').getAttribute('fmt'),\n\t\t\tfrm = mknod('form'),\n\t\t\ttxt = [];\n\n\t\tif (dk)\n\t\t\targ += '&k=' + dk;\n\n\t\tfor (var a = 0; a < sel.length; a++)\n\t\t\ttxt.push(vsplit(sel[a].vp)[1]);\n\n\t\ttxt = txt.join('\\n');\n\n\t\tfrm.setAttribute('action', '?' + arg);\n\t\tfrm.setAttribute('method', 'post');\n\t\tfrm.setAttribute('target', '_blank');\n\t\tfrm.setAttribute('enctype', 'multipart/form-data');\n\t\tfrm.innerHTML = '<input name=\"act\" value=\"zip\" />' +\n\t\t\t'<textarea name=\"files\" id=\"ziptxt\"></textarea>';\n\t\tfrm.style.display = 'none';\n\n\t\tqsr('#widgeti>form');\n\t\tebi('widgeti').appendChild(frm);\n\t\tvar obj = ebi('ziptxt');\n\t\tobj.value = txt;\n\t\tconsole.log(txt);\n\t\tfrm.submit();\n\t};\n\tebi('seldl').onclick = function (e) {\n\t\tev(e);\n\t\tvar sel = r.getsel();\n\t\tfor (var a = 0; a < sel.length; a++)\n\t\t\tif (sel[a].isd)\n\t\t\t\ttoast.warn(7, L.f_dl_nd + esc(sel[a].vp));\n\t\t\telse\n\t\t\t\tdl_file(sel[a].vp + sel[a].q);\n\t};\n\tr.render = function () {\n\t\tvar tds = QSA('#files tbody td+td+td'),\n\t\t\tis_srch = !!ebi('unsearch');\n\n\t\tif (!is_srch)\n\t\t\tfor (var a = 0, aa = tds.length; a < aa; a++)\n\t\t\t\ttds[a].onclick = r.seltgl;\n\n\t\tr.selui(true);\n\t\tarcfmt.render();\n\t\tfileman.render();\n\n\t\tvar zipvis = (is_srch || !have_zip) ? 'none' : '';\n\t\tebi('selzip').style.display = zipvis;\n\t\tebi('zip1').style.display = zipvis;\n\t}\n\treturn r;\n})();\n\n\n(function () {\n\tif (!FormData)\n\t\treturn;\n\n\tvar form = QS('#op_new_md>form'),\n\t\ttb = QS('#op_new_md input[name=\"name\"]');\n\n\tform.onsubmit = function (e) {\n\t\tif (!has(perms, \"delete\") && !re_rw_edit.test(tb.value)) {\n\t\t\tev(e);\n\t\t\ttoast.err(10, L.nmd_i2.format(rw_edit.replace(/,/g, '/')));\n\t\t\treturn false;\n\t\t}\n\t\tif (tb.value) {\n\t\t\tif (toast.tag == L.mk_noname)\n\t\t\t\ttoast.hide();\n\n\t\t\treturn true;\n\t\t}\n\t\tev(e);\n\t\ttoast.err(10, L.mk_noname, L.mk_noname);\n\t\treturn false;\n\t};\n})();\n\n\n(function () {\n\tif (!FormData)\n\t\treturn;\n\n\tvar form = QS('#op_mkdir>form'),\n\t\ttb = QS('#op_mkdir input[name=\"name\"]'),\n\t\tsf = mknod('div');\n\n\tclmod(sf, 'msg', 1);\n\tform.parentNode.appendChild(sf);\n\n\tform.onsubmit = function (e) {\n\t\tev(e);\n\t\tvar dn = tb.value;\n\t\tif (!dn) {\n\t\t\ttoast.err(10, L.mk_noname, L.mk_noname);\n\t\t\treturn false;\n\t\t}\n\n\t\tif (toast.tag == L.mk_noname || toast.tag == L.fd_xe1)\n\t\t\ttoast.hide();\n\n\t\tclmod(sf, 'vis', 1);\n\t\tsf.textContent = 'creating \"' + dn + '\"...';\n\n\t\tvar fd = new FormData();\n\t\tfd.append(\"act\", \"mkdir\");\n\t\tfd.append(\"name\", dn);\n\n\t\tvar xhr = new XHR();\n\t\txhr.vp = get_evpath();\n\t\txhr.dn = dn;\n\t\txhr.open('POST', dn.startsWith('/') ? (SR || '/') : xhr.vp, true);\n\t\txhr.onload = xhr.onerror = cb;\n\t\txhr.responseType = 'text';\n\t\txhr.send(fd);\n\n\t\treturn false;\n\t};\n\n\tfunction cb() {\n\t\tif (this.vp !== get_evpath()) {\n\t\t\tsf.textContent = 'aborted due to location change';\n\t\t\treturn;\n\t\t}\n\n\t\txhrchk(this, L.fd_xe1, L.fd_xe2);\n\n\t\tif (this.status !== 201) {\n\t\t\tsf.textContent = 'error: ' + hunpre(this.responseText);\n\t\t\treturn;\n\t\t}\n\n\t\ttb.value = '';\n\t\tclmod(sf, 'vis');\n\t\tsf.textContent = '';\n\n\t\tvar dn = this.getResponseHeader('X-New-Dir');\n\t\tdn = dn ? '/' + dn + '/' : uricom_enc(this.dn);\n\t\ttreectl.goto(dn, true);\n\t\ttree_scrollto();\n\t}\n})();\n\n\n(function () {\n\tvar form = QS('#op_msg>form'),\n\t\ttb = QS('#op_msg input[name=\"msg\"]'),\n\t\tsf = mknod('div');\n\n\tclmod(sf, 'msg', 1);\n\tform.parentNode.appendChild(sf);\n\n\tform.onsubmit = function (e) {\n\t\tev(e);\n\t\tclmod(sf, 'vis', 1);\n\t\tsf.textContent = 'sending...';\n\n\t\tvar xhr = new XHR(),\n\t\t\tsel = msel.getsel(),\n\t\t\tmsg = uricom_enc(tb.value),\n\t\t\tct = 'application/x-www-form-urlencoded;charset=UTF-8';\n\n\t\tfor (var a = 0; a < sel.length; a++)\n\t\t\tmsg += \"&sel=\" + sel[a].vp;\n\n\t\txhr.msg = msg;\n\t\txhr.open('POST', get_evpath(), true);\n\t\txhr.responseType = 'text';\n\t\txhr.onload = xhr.onerror = cb;\n\t\txhr.setRequestHeader('Content-Type', ct);\n\t\tif (xhr.overrideMimeType)\n\t\t\txhr.overrideMimeType('Content-Type', ct);\n\n\t\txhr.send('msg=' + xhr.msg);\n\t\treturn false;\n\t};\n\n\tfunction cb() {\n\t\txhrchk(this, L.fsm_xe1, L.fsm_xe2);\n\n\t\tif (this.status < 200 || this.status > 202) {\n\t\t\tsf.textContent = 'error: ' + hunpre(this.responseText);\n\t\t\treturn;\n\t\t}\n\n\t\ttb.value = '';\n\t\tclmod(sf, 'vis');\n\t\tvar txt = 'sent: <code>' + esc(this.msg) + '</code>';\n\t\tif (this.status == 202)\n\t\t\ttxt += '<br />&nbsp; got: <code>' + esc(this.responseText) + '</code>';\n\n\t\tsf.innerHTML = txt;\n\t\tsetTimeout(function () {\n\t\t\ttreectl.goto();\n\t\t}, 100);\n\t}\n})();\n\n\nvar globalcss = (function () {\n\tvar ret = '';\n\treturn function () {\n\t\tif (ret)\n\t\t\treturn ret;\n\n\t\tvar dcs = document.styleSheets;\n\t\tfor (var a = 0; a < dcs.length; a++) {\n\t\t\tvar ds, base = '';\n\t\t\ttry {\n\t\t\t\tbase = dcs[a].href;\n\t\t\t\tif (!base)\n\t\t\t\t\tcontinue;\n\n\t\t\t\tds = dcs[a].cssRules;\n\t\t\t\tbase = base.replace(/[^/]+$/, '');\n\t\t\t\tfor (var b = 0; b < ds.length; b++) {\n\t\t\t\t\tvar css = ds[b].cssText.split(/\\burl\\(/g);\n\t\t\t\t\tret += css[0];\n\t\t\t\t\tfor (var c = 1; c < css.length; c++) {\n\t\t\t\t\t\tvar m = /(^ *[\"']?)(.*)/.exec(css[c]),\n\t\t\t\t\t\t\tdelim = m[1],\n\t\t\t\t\t\t\tctxt = m[2],\n\t\t\t\t\t\t\tis_abs = /^\\/|[^)/:]+:\\/\\//.exec(ctxt);\n\n\t\t\t\t\t\tret += 'url(' + delim + (is_abs ? '' : base) + ctxt;\n\t\t\t\t\t}\n\t\t\t\t\tret += '\\n';\n\t\t\t\t}\n\t\t\t\tif (ret.indexOf('\\n@import') + 1) {\n\t\t\t\t\tvar c0 = ret.split('\\n'),\n\t\t\t\t\t\tc1 = [],\n\t\t\t\t\t\tc2 = [];\n\n\t\t\t\t\tfor (var a = 0; a < c0.length; a++)\n\t\t\t\t\t\t(c0[a].startsWith('@import') ? c1 : c2).push(c0[a]);\n\n\t\t\t\t\tret = c1.concat(c2).join('\\n');\n\t\t\t\t}\n\t\t\t}\n\t\t\tcatch (ex) {\n\t\t\t\tconsole.log('could not read css', a, base);\n\t\t\t}\n\t\t}\n\t\treturn ret;\n\t};\n})();\n\nvar sandboxjs = (function () {\n\tvar ret = '',\n\t\tbusy = false,\n\t\turl = SR + '/.cpr/w/util.js?_=' + TS,\n\t\ttag = '<script src=\"' + url + '\"></script>';\n\n\treturn function () {\n\t\tif (ret || busy)\n\t\t\treturn ret || tag;\n\n\t\tvar xhr = new XHR();\n\t\txhr.open('GET', url, true);\n\t\txhr.onload = function () {\n\t\t\tif (this.status == 200)\n\t\t\t\tret = '<script>' + this.responseText + '</script>';\n\t\t};\n\t\txhr.send();\n\t\tbusy = true;\n\t\treturn tag;\n\t};\n})();\n\n\nfunction show_md(md, name, div, url, depth) {\n\tvar errmsg = L.md_eshow + name + ':\\n\\n',\n\t\tnow = get_evpath();\n\n\turl = url || now;\n\tif (url != now)\n\t\treturn;\n\n\twfp_debounce.hide();\n\tif (!marked) {\n\t\tif (depth) {\n\t\t\tclmod(div, 'raw', 1);\n\t\t\tdiv.textContent = \"--[ \" + name + \" ]---------\\r\\n\" + md;\n\t\t\treturn toast.warn(10, errmsg + (WebAssembly ? 'failed to load marked.js' : 'your browser is too old'));\n\t\t}\n\n\t\twfp_debounce.n--;\n\t\treturn import_js(SR + '/.cpr/w/deps/marked.js', function () {\n\t\t\tshow_md(md, name, div, url, 1);\n\t\t});\n\t}\n\n\tmd_plug = {}\n\tmd = load_md_plug(md, 'pre');\n\tmd = load_md_plug(md, 'post', sb_md);\n\n\tvar marked_opts = {\n\t\theaderPrefix: 'md-',\n\t\tbreaks: !md_no_br,\n\t\tgfm: true\n\t};\n\tvar ext = md_plug.pre;\n\tif (ext)\n\t\tObject.assign(marked_opts, ext[0]);\n\n\ttry {\n\t\tclmod(div, 'mdo', 1);\n\n\t\tvar md_html = marked.parse(md, marked_opts);\n\t\tif (!have_emp)\n\t\t\tmd_html = DOMPurify.sanitize(md_html);\n\n\t\tif (sandbox(div, sb_md, sba_md, 'mdo', md_html))\n\t\t\treturn;\n\n\t\text = md_plug.post;\n\t\text = ext ? [ext[0].render, ext[0].render2] : [];\n\t\tfor (var a = 0; a < ext.length; a++)\n\t\t\tif (ext[a])\n\t\t\t\ttry {\n\t\t\t\t\text[a](div);\n\t\t\t\t}\n\t\t\t\tcatch (ex) {\n\t\t\t\t\tconsole.log(ex);\n\t\t\t\t}\n\n\t\tvar els = QSA('#epi a');\n\t\tfor (var a = 0, aa = els.length; a < aa; a++) {\n\t\t\tvar href = els[a].getAttribute('href');\n\t\t\tif (!href || !href.startsWith('#') || href.startsWith('#md-'))\n\t\t\t\tcontinue;\n\n\t\t\tels[a].setAttribute('href', '#md-' + href.slice(1));\n\t\t}\n\t\tmd_th_set();\n\t\tset_tabindex();\n\t\tvar hash = location.hash;\n\t\tif (hash.startsWith('#md-'))\n\t\t\tsetTimeout(function () {\n\t\t\t\ttry {\n\t\t\t\t\tQS(hash).scrollIntoView();\n\t\t\t\t}\n\t\t\t\tcatch (ex) { }\n\t\t\t}, 1);\n\t}\n\tcatch (ex) {\n\t\ttoast.warn(10, errmsg + ex);\n\t}\n\twfp_debounce.show();\n}\n\n\nfunction set_tabindex() {\n\tvar els = QSA('pre');\n\tfor (var a = 0, aa = els.length; a < aa; a++)\n\t\tels[a].setAttribute('tabindex', '0');\n}\n\n\nfunction show_readme(md, n) {\n\tvar tgt = ebi(n ? 'epi' : 'pro');\n\n\tif (!treectl.ireadme)\n\t\treturn sandbox(tgt, '', '', '', 'a');\n\n\tshow_md(md, n ? 'README.md' : 'PREADME.md', tgt);\n}\nfor (var a = 0; a < readmes.length; a++)\n\tif (readmes[a])\n\t\tshow_readme(readmes[a], a);\n\n\nfunction sandbox(tgt, rules, allow, cls, html) {\n\tif (!treectl.ireadme) {\n\t\ttgt.innerHTML = html ? L.md_off : '';\n\t\treturn;\n\t}\n\tif (!rules || (html || '').indexOf('<') == -1) {\n\t\ttgt.innerHTML = html;\n\t\tclmod(tgt, 'sb');\n\t\treturn false;\n\t}\n\tif (!CLOSEST) {\n\t\ttgt.textContent = html;\n\t\tclmod(tgt, 'sb');\n\t\treturn false;\n\t}\n\tclmod(tgt, 'sb', 1);\n\n\tvar tid = tgt.getAttribute('id'),\n\t\thash = location.hash,\n\t\twant = '';\n\n\tif (!cls)\n\t\twfp_debounce.hide();\n\n\tif (hash.startsWith('#md-'))\n\t\twant = hash.slice(1);\n\n\tvar env = '', tags = QSA('script');\n\tfor (var a = 0; a < tags.length; a++) {\n\t\tvar js = tags[a].innerHTML;\n\t\tif (js && js.indexOf('have_up2k_idx') + 1)\n\t\t\tenv = js.split(/\\blogues *=/)[0] + 'a;';\n\t}\n\n\thtml = '<html class=\"iframe ' + document.documentElement.className +\n\t\t'\"><head><style>html{background:#eee;color:#000}</style><style>' + globalcss() +\n\t\t'</style><base target=\"_parent\"></head><body id=\"b\" class=\"logue ' + cls + '\">' + html +\n\t\t'<script>' + env + '</script>' + sandboxjs() +\n\t\t'<script>var d=document.documentElement,TS=\"' + TS + '\",' +\n\t\t'loc=new URL(\"' + location.href.split('?')[0] + '\");' +\n\t\t'function say(m){window.parent.postMessage(m,\"*\")};' +\n\t\t'setTimeout(function(){var its=0,pih=-1,f=function(){' +\n\t\t'var ih=2+Math.min(parseInt(getComputedStyle(d).height),d.scrollHeight);' +\n\t\t'if(ih!=pih&&!isNaN(ih)){pih=ih;say(\"iheight #' + tid + ' \"+ih,\"*\")}' +\n\t\t'if(++its<20)return setTimeout(f,20);if(its==20)setInterval(f,200)' +\n\t\t'};f();' +\n\t\t'window.onfocus=function(){say(\"igot #' + tid + '\")};' +\n\t\t'window.onblur=function(){say(\"ilost #' + tid + '\")};' +\n\t\t'window.treectl={\"goto\":function(a){say(\"goto #' + tid + ' \"+(a||\"\"))}};' +\n\t\t'var el=\"' + want + '\"&&ebi(\"' + want + '\");' +\n\t\t'if(el)say(\"iscroll #' + tid + ' \"+el.offsetTop);' +\n\t\t'md_th_set();' +\n\t\t(cls == 'mdo' && md_plug.post ?\n\t\t\t'const x={' + md_plug.post + '};' +\n\t\t\t'if(x.render)x.render(ebi(\"b\"));' +\n\t\t\t'if(x.render2)x.render2(ebi(\"b\"));' : '') +\n\t\t'},1)</script></body></html>';\n\n\tvar fr = mknod('iframe');\n\tfr.setAttribute('title', 'folder ' + tid + 'logue');\n\tfr.setAttribute('sandbox', rules ? 'allow-' + rules.replace(/ /g, ' allow-') : '');\n\tfr.setAttribute('allow', allow);\n\tfr.setAttribute('srcdoc', html);\n\ttgt.appendChild(fr);\n\ttreectl.sb_msg = true;\n\treturn true;\n}\nwindow.addEventListener(\"message\", function (e) {\n\tif (!treectl.sb_msg)\n\t\treturn;\n\n\ttry {\n\t\tconsole.log('msg:' + e.data);\n\t\tvar t = e.data.split(/ /g);\n\t\tif (t[0] == 'iheight') {\n\t\t\tvar el = QSA(t[1] + '>iframe');\n\t\t\tel = el[el.length - 1];\n\t\t\tif (wfp_debounce.n)\n\t\t\t\twhile (el.previousSibling)\n\t\t\t\t\tel.parentNode.removeChild(el.previousSibling);\n\n\t\t\tel.style.height = (parseInt(t[2]) + SBH) + 'px';\n\t\t\tel.style.visibility = CLOSEST ? 'unset' : 'block';\n\t\t\twfp_debounce.show();\n\t\t}\n\t\telse if (t[0] == 'iscroll') {\n\t\t\tvar y1 = QS(t[1]).offsetTop,\n\t\t\t\ty2 = parseInt(t[2]);\n\t\t\tconsole.log(y1, y2);\n\t\t\tdocument.documentElement.scrollTop = y1 + y2;\n\t\t}\n\t\telse if (t[0] == 'igot' || t[0] == 'ilost') {\n\t\t\tclmod(QS(t[1] + '>iframe'), 'focus', t[0] == 'igot');\n\t\t}\n\t\telse if (t[0] == 'imshow') {\n\t\t\tthegrid.imshow(e.data.slice(7));\n\t\t}\n\t\telse if (t[0] == 'goto') {\n\t\t\tvar t = e.data.replace(/^[^ ]+ [^ ]+ /, '').split(/[?&]/)[0];\n\t\t\ttreectl.goto(t, !!t);\n\t\t}\n\t} catch (ex) {\n\t\tconsole.log('msg-err: ' + ex);\n\t}\n}, false);\n\n\nif (sb_lg && logues.length) {\n\tif (logues[1] === Ls.eng.f_empty)\n\t\tlogues[1] = L.f_empty;\n\n\tsandbox(ebi('pro'), sb_lg, sba_lg, '', logues[0]);\n\tsandbox(ebi('epi'), sb_lg, sba_lg, '', logues[1]);\n}\n\n\n(function () {\n\ttry {\n\t\tvar tr = ebi('files').tBodies[0].rows;\n\t\tfor (var a = 0; a < tr.length; a++) {\n\t\t\tvar td = tr[a].cells[1],\n\t\t\t\tao = td.firstChild,\n\t\t\t\thref = noq_href(ao),\n\t\t\t\tisdir = href.endsWith('/'),\n\t\t\t\ttxt = ao.textContent;\n\n\t\t\ttd.setAttribute('sortv', (isdir ? '\\t' : '') + txt);\n\t\t}\n\t}\n\tcatch (ex) { }\n})();\n\n\nfunction ev_row_tgl(e) {\n\tev(e);\n\tfilecols.toggle(this.parentElement.parentElement.getElementsByTagName('span')[0].textContent);\n}\n\n\nvar unpost = (function () {\n\tebi('op_unpost').innerHTML = (\n\t\tL.un_m1 + ' &ndash; <a id=\"unpost_refresh\" href=\"#\">' + L.un_upd + '</a>' +\n\t\t'<p>' + L.un_m4 + ' <a id=\"unpost_ulist\" href=\"#\">' + L.un_ulist + '</a> / <a id=\"unpost_ucopy\" href=\"#\">' + L.un_ucopy + '</a>' +\n\t\t'<p>' + L.un_flt + ' <input type=\"text\" id=\"unpost_filt\" size=\"20\" placeholder=\"documents/passwords\" /><a id=\"unpost_nofilt\" href=\"#\">' + L.un_fclr + '</a></p>' +\n\t\t'<div id=\"unpost\"></div>'\n\t);\n\n\tvar r = {},\n\t\tct = ebi('unpost'),\n\t\tfilt = ebi('unpost_filt');\n\n\tr.files = [];\n\tr.me = null;\n\n\tr.load = function () {\n\t\tvar me = Date.now(),\n\t\t\thtml = [];\n\n\t\tfunction unpost_load_cb() {\n\t\t\tif (!xhrchk(this, L.fu_xe1, L.fu_xe2))\n\t\t\t\treturn ebi('op_unpost').innerHTML = L.fu_xe1;\n\n\t\t\ttry {\n\t\t\t\tvar ores = JSON.parse(this.responseText);\n\t\t\t}\n\t\t\tcatch (ex) {\n\t\t\t\treturn ebi('op_unpost').innerHTML = '<p>' + L.badreply + ':</p>' + unpre(this.responseText);\n\t\t\t}\n\n\t\t\tif (ores.nou)\n\t\t\t\thtml.push('<p>' + L.un_nou + '</p>');\n\n\t\t\tif (ores.noc)\n\t\t\t\thtml.push('<p>' + L.un_noc + '</p>');\n\n\t\t\tvar res = ores.f;\n\n\t\t\tif (res.length) {\n\t\t\t\tif (ores.of)\n\t\t\t\t\thtml.push(\"<p>\" + L.un_max);\n\t\t\t\telse\n\t\t\t\t\thtml.push(\"<p>\" + L.un_avail.format(ores.nc, ores.nu));\n\n\t\t\t\thtml.push(\"<br />\" + L.un_m2 + \"</p>\");\n\t\t\t\thtml.push(\"<table><thead><tr><td></td><td>time</td><td>size</td><td>done</td><td>file</td></tr></thead><tbody>\");\n\t\t\t}\n\t\t\telse\n\t\t\t\thtml.push('-- <em>' + (filt.value ? L.un_no2 : L.un_no1) + '</em>');\n\n\t\t\tvar mods = [10, 100, 1000];\n\t\t\tfor (var a = 0; a < res.length; a++) {\n\t\t\t\tfor (var b = 0; b < mods.length; b++)\n\t\t\t\t\tif (a % mods[b] == 0 && res.length > a + mods[b] / 10)\n\t\t\t\t\t\thtml.push(\n\t\t\t\t\t\t\t'<tr><td></td><td colspan=\"3\" style=\"padding:.5em\">' +\n\t\t\t\t\t\t\t'<a me=\"' + me + '\" class=\"n' + a + '\" n2=\"' + (a + mods[b]) +\n\t\t\t\t\t\t\t'\" href=\"#\">' + L.un_next.format(Math.min(mods[b], res.length - a)) + '</a></td></tr>');\n\n\t\t\t\tvar done = res[a].pd === undefined;\n\t\t\t\thtml.push(\n\t\t\t\t\t'<tr><td><a me=\"' + me + '\" class=\"n' + a + '\" href=\"#\">' + (done ? L.un_del : L.un_abrt) + '</a></td>' +\n\t\t\t\t\t'<td>' + unix2ui(res[a].at) + '</td>' +\n\t\t\t\t\t'<td>' + ('' + res[a].sz).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \" \") + '</td>' +\n\t\t\t\t\t(done ? '<td>100%</td>' : '<td>' + res[a].pd + '%</td>') +\n\t\t\t\t\t'<td>' + linksplit(res[a].vp).join('<span> / </span>') + '</td></tr>');\n\t\t\t}\n\n\t\t\thtml.push(\"</tbody></table>\");\n\t\t\tct.innerHTML = html.join('\\n');\n\t\t\tr.files = res;\n\t\t\tr.me = me;\n\t\t}\n\n\t\tvar q = get_evpath() + '?ups';\n\t\tif (filt.value)\n\t\t\tq += '&filter=' + uricom_enc(filt.value, true);\n\n\t\tvar xhr = new XHR();\n\t\txhr.open('GET', q, true);\n\t\txhr.onload = xhr.onerror = unpost_load_cb;\n\t\txhr.send();\n\n\t\tct.innerHTML = \"<p><em>\" + L.un_m3 + \"</em></p>\";\n\t};\n\n\tfunction linklist() {\n\t\tvar ret = [],\n\t\t\tbase = location.origin.replace(/\\/$/, '');\n\n\t\tfor (var a = 0; a < r.files.length; a++)\n\t\t\tret.push(base + r.files[a].vp);\n\n\t\treturn ret.join('\\r\\n');\n\t}\n\n\tfunction unpost_delete_cb() {\n\t\tif (this.status !== 200) {\n\t\t\tvar msg = unpre(this.responseText);\n\t\t\ttoast.err(9, L.un_derr + msg);\n\t\t\treturn;\n\t\t}\n\n\t\tfor (var a = this.n; a < this.n2; a++) {\n\t\t\tvar o = QSA('#op_unpost a.n' + a);\n\t\t\tfor (var b = 0; b < o.length; b++) {\n\t\t\t\tvar o2 = o[b].closest('tr');\n\t\t\t\to2.parentNode.removeChild(o2);\n\t\t\t}\n\t\t}\n\t\ttoast.ok(5, this.responseText);\n\n\t\tif (!QS('#op_unpost a[me]'))\n\t\t\tgoto_unpost();\n\n\t\tvar fi = window.up2k && up2k.st.files;\n\t\tif (fi && fi.length < 9) {\n\t\t\tfor (var a = 0; a < fi.length; a++) {\n\t\t\t\tvar f = fi[a];\n\t\t\t\tif (!f.done && (f.rechecks || f.want_recheck) &&\n\t\t\t\t\t!has(up2k.st.todo.handshake, f) &&\n\t\t\t\t\t!has(up2k.st.busy.handshake, f)\n\t\t\t\t) {\n\t\t\t\t\tup2k.st.todo.handshake.push(f);\n\t\t\t\t\tup2k.ui.seth(f.n, 2, L.u_hashdone);\n\t\t\t\t\tup2k.ui.seth(f.n, 1, '📦 wait');\n\t\t\t\t\tup2k.ui.move(f.n, 'bz');\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tct.onclick = function (e) {\n\t\tvar tgt = e.target.closest('a[me]');\n\t\tif (!tgt)\n\t\t\treturn;\n\n\t\tif (!tgt.getAttribute('href'))\n\t\t\treturn;\n\n\t\tev(e);\n\t\tvar ame = tgt.getAttribute('me');\n\t\tif (ame != r.me)\n\t\t\treturn toast.err(0, L.un_f5);\n\n\t\tvar n = parseInt(tgt.className.slice(1)),\n\t\t\tn2 = parseInt(tgt.getAttribute('n2') || n + 1),\n\t\t\treq = [];\n\n\t\tfor (var a = n; a < n2; a++) {\n\t\t\tvar links = QSA('#op_unpost a.n' + a);\n\t\t\tif (!links.length)\n\t\t\t\tcontinue;\n\n\t\t\tvar f = r.files[a];\n\t\t\tif (f.k == 'u') {\n\t\t\t\tvar vp = vsplit(f.vp.split('?')[0]),\n\t\t\t\t\tdfn = uricom_dec(vp[1]);\n\t\t\t\tfor (var iu = 0; iu < up2k.st.files.length; iu++) {\n\t\t\t\t\tvar uf = up2k.st.files[iu];\n\t\t\t\t\tif (uf.name == dfn && uf.purl == vp[0])\n\t\t\t\t\t\treturn modal.alert(L.un_uf5);\n\t\t\t\t}\n\t\t\t}\n\t\t\treq.push(uricom_dec(f.vp.split('?')[0]));\n\t\t\tfor (var b = 0; b < links.length; b++) {\n\t\t\t\tlinks[b].removeAttribute('href');\n\t\t\t\tlinks[b].innerHTML = '[busy]';\n\t\t\t}\n\t\t}\n\n\t\ttoast.show('inf r', 0, L.un_busy.format(req.length));\n\n\t\tvar xhr = new XHR();\n\t\txhr.n = n;\n\t\txhr.n2 = n2;\n\t\txhr.open('POST', SR + '/?delete&unpost&lim=' + req.length, true);\n\t\txhr.onload = xhr.onerror = unpost_delete_cb;\n\t\txhr.send(JSON.stringify(req));\n\t};\n\n\tvar tfilt = null;\n\tfilt.oninput = function () {\n\t\tclearTimeout(tfilt);\n\t\ttfilt = setTimeout(r.load, 250);\n\t};\n\n\tebi('unpost_nofilt').onclick = function (e) {\n\t\tev(e);\n\t\tfilt.value = '';\n\t\tr.load();\n\t};\n\n\tebi('unpost_refresh').onclick = function (e) {\n\t\tev(e);\n\t\tgoto('unpost');\n\t};\n\n\tebi('unpost_ulist').onclick = function (e) {\n\t\tev(e);\n\t\tmodal.alert(linklist());\n\t};\n\n\tebi('unpost_ucopy').onclick = function (e) {\n\t\tev(e);\n\t\tvar txt = linklist();\n\t\tcliptxt(txt + '\\n', function () {\n\t\t\ttoast.inf(5, L.un_clip.format(txt.split('\\n').length));\n\t\t});\n\t};\n\n\treturn r;\n})();\n\n\nfunction goto_unpost(e) {\n\tunpost.load();\n}\n\n\nfunction wintitle(txt, noname) {\n\tif (txt === undefined)\n\t\ttxt = '';\n\n\tif (s_name && !noname)\n\t\ttxt = s_name + ' ' + txt;\n\n\ttxt += uricom_dec(get_evpath()).slice(1, -1).split('/').pop();\n\n\tdocument.title = txt || \"copyparty\";\n}\n\n\nebi('path').onclick = function (e) {\n\tif (ctrl(e))\n\t\treturn true;\n\n\tvar a = e.target.closest('a[href]');\n\tif (!a || !(a = a.getAttribute('href') + '') || !a.endsWith('/'))\n\t\treturn;\n\n\tthegrid.setvis(true);\n\ttreectl.reqls(a, true);\n\treturn ev(e);\n};\n\n\nvar scroll_y = -1;\nvar scroll_vp = '\\n';\nvar scroll_obj = null;\nfunction persist_scroll() {\n\tvar obj = scroll_obj;\n\tif (!obj) {\n\t\tvar o1 = document.getElementsByTagName('html')[0];\n\t\tvar o2 = document.body;\n\t\tobj = o1.scrollTop > o2.scrollTop ? o1 : o2;\n\t}\n\tvar y = obj.scrollTop;\n\tif (y > 0)\n\t\tscroll_obj = obj;\n\n\tscroll_y = y;\n\tscroll_vp = get_evpath();\n}\nfunction restore_scroll() {\n\tif (get_evpath() == scroll_vp && scroll_obj && scroll_obj.scrollTop < 1)\n\t\tscroll_obj.scrollTop = scroll_y;\n}\n\n\nebi('files').onclick = ebi('docul').onclick = function (e) {\n\tif (!treectl.csel && e && (ctrl(e) || e.shiftKey))\n\t\treturn true;\n\n\tif (!showfile.active())\n\t\tpersist_scroll();\n\n\tvar tgt = e.target.closest('a[id]');\n\tif (tgt && tgt.getAttribute('id').indexOf('f-') === 0 && tgt.textContent.endsWith('/')) {\n\t\tvar el = treectl.find(tgt.textContent.slice(0, -1));\n\t\tif (el) {\n\t\t\tel.click();\n\t\t\treturn ev(e);\n\t\t}\n\t\ttreectl.reqls(tgt.getAttribute('href'), true);\n\t\treturn ev(e);\n\t}\n\tif (tgt && /\\.PARTIAL(\\?|$)/.exec('' + tgt.getAttribute('href')) && !window.partdlok) {\n\t\tev(e);\n\t\tmodal.confirm(L.f_partial, function () {\n\t\t\twindow.partdlok = 1;\n\t\t\ttgt.click();\n\t\t}, null);\n\t}\n\n\ttgt = e.target.closest('a[hl]');\n\tif (tgt) {\n\t\tvar a = ebi(tgt.getAttribute('hl')),\n\t\t\thref = a.getAttribute('href'),\n\t\t\tfun = function () {\n\t\t\t\tshowfile.show(href, tgt.getAttribute('lang'));\n\t\t\t},\n\t\t\ttfun = function () {\n\t\t\t\tbcfg_set('taildoc', showfile.taildoc = true);\n\t\t\t\tfun();\n\t\t\t},\n\t\t\tszs = ft2dict(a.closest('tr'))[0].sz,\n\t\t\tsz = parseInt(szs.replace(/[, ]/g, ''));\n\n\t\tif (sz < 1024 * 1024 || showfile.taildoc)\n\t\t\tfun();\n\t\telse\n\t\t\tmodal.confirm(L.f_bigtxt.format(f2f(sz / 1024 / 1024, 1)), fun, function() {\n\t\t\t\tmodal.confirm(L.f_bigtxt2, tfun, null)});\n\n\t\treturn ev(e);\n\t}\n\n\ttgt = e.target.closest('a');\n\tif (tgt && tgt.closest('li.bn')) {\n\t\tthegrid.setvis(true);\n\t\ttreectl.goto(tgt.getAttribute('href'), true);\n\t\treturn ev(e);\n\t}\n};\n\n\n\nvar rcm = (function () {\n\tif (MOBILE)\n\t\treturn {enabled: false}\n\n\tvar r = {};\n\tbcfg_bind(r, 'enabled', 'rcm_en', drcm.charAt(0)=='y');\n\tbcfg_bind(r, 'double', 'rcm_db', drcm.charAt(1)=='y');\n\n\tvar menu = ebi('rcm');\n\tvar nsFile = {\n\t\telem: null,\n\t\ttype: null,\n\t\tpath: null,\n\t\tdpath: null,\n\t\turl: null,\n\t\tid: null,\n\t\tname: null,\n\t\tno_dsel: false\n\t};\n\tvar selFile = jcp(nsFile);\n\n\tfunction mktemp(is_dir) {\n\t\tqsr('#rcm_tmp');\n\t\tif (!thegrid.en) {\n\t\t\tvar row = mknod('tr', 'rcm_tmp',\n\t\t\t\t'<td>-new-</td><td colspan=\"' + (QSA(\"#files thead th\").length - 1) + '\"><input id=\"tempname\" class=\"i\" type=\"text\" placeholder=\"' + (is_dir ? 'Folder' : 'File') + ' Name\"></td>');\n\t\t\tQS(\"#files tbody\").appendChild(row);\n\t    }\n\t\telse {\n\t\t\tvar row = mknod('a', 'rcm_tmp',\n\t\t\t\t'<span class=\"dir\" style=\"align-self:end\"><input id=\"tempname\" class=\"dir\" type=\"text\" placeholder=\"' + (is_dir ? 'Folder' : 'File') + ' Name\"></span>');\n\t\t\tif (is_dir)\n\t\t\t\trow.className = 'dir';\n\t\t\trow.style.display = 'flex';\n\t\t\tQS(\"#ggrid\").appendChild(row);\n\t\t}\n\n\t\tfunction sendit(name) {\n\t\t\tname = ('' + name).trim();\n\t\t\tif (!name)\n\t\t\t\treturn;\n\t\t\tvar data = new FormData();\n\t\t\tdata.set(\"act\", is_dir ? \"mkdir\" : \"new_md\");\n\t\t\tdata.set(\"name\", name);\n\n\t\t\tvar req = new XHR();\n\t\t\treq.open(\"POST\", get_evpath());\n\t\t\treq.onload = req.onerror = function() {\n\t\t\t\tif (req.status == 405 || req.status == 500)\n\t\t\t\t\treturn toast.err(3, \"a \" + (is_dir ? \"folder\" : \"file\") + \" with that name already exists.\");\n\t\t\t\tif (req.status < 200 || req.status > 399)\n\t\t\t\t\treturn toast.err(3, \"couldn't create \" + (is_dir ? \"folder\" : \"file\") + \": <br><code>\" + esc(req.responseText) + '</code>');\n\t\t\t\ttreectl.goto();\n\t\t\t};\n\t\t\treq.send(data);\n\t\t}\n\n\t\tvar input = ebi(\"tempname\");\n\t\tinput.onblur = function() {\n\t\t\tsendit(input.value);\n\t\t\t// Chrome blurs elements when calling remove for some reason\n\t\t\tinput.onblur = null;\n\t\t\trow.remove();\n\t\t};\n\t\tinput.onkeydown = function(e) {\n\t\t\tif (e.key == \"Enter\")\n\t\t\t\tsendit(input.value);\n\t\t\tif (e.key == \"Enter\" || e.key == \"Escape\") {\n\t\t\t\tinput.onblur = null;\n\t\t\t\trow.remove();\n\t\t\t\tev(e);\n\t\t\t}\n\t\t};\n\t\tinput.focus();\n\t}\n\n\tvar opts = QSA('#rcm a');\n\tfor (var i = 0; i < opts.length; i++) {\n\t\topts[i].onclick = function(e) {\n\t\t\tev(e);\n\t\t\tswitch(e.target.id.slice(1)) {\n\t\t\t\tcase 'opn':\n\t\t\t\t\tvar a = mknod('a');\n\t\t\t\t\ta.href = selFile.url;\n\t\t\t\t\ta.target = selFile.type == \"dir\" ? '' : '_blank';\n\t\t\t\t\ta.click();\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'ply': selFile.type == 'gf' ? thegrid.imshow(selFile.name) : play('f-' + selFile.id); break;\n\t\t\t\tcase 'pla': play('f-' + selFile.id); break;\n\t\t\t\tcase 'txt': showfile.show(selFile.name); break;\n\t\t\t\tcase 'md': location = selFile.path + (has(selFile.path, '?') ? '&v' : '?v'); break;\n\t\t\t\tcase 'cpl': cliptxt(selFile.url, function() {toast.ok(2, L.clipped)}); break;\n\t\t\t\tcase 'dl': ebi('seldl').click(); break;\n\t\t\t\tcase 'zip': ebi('selzip').click(); break;\n\t\t\t\tcase 'del': fileman.delete(); break;\n\t\t\t\tcase 'cut': fileman.cut(); break;\n\t\t\t\tcase 'cpy': fileman.cpy(); break;\n\t\t\t\tcase 'pst':\n\t\t\t\t\tfileman.paste();\n\t\t\t\t\tfileman.clip = [];\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'rnm': fileman.rename(); break;\n\t\t\t\tcase 'nfo': mktemp(true); break;\n\t\t\t\tcase 'nfi': mktemp(); break;\n\t\t\t\tcase 'sal':\n\t\t\t\t\tmsel.evsel(null, true);\n\t\t\t\t\tselFile.no_dsel = true;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'sin': msel.evsel(null, 't'); break;\n\t\t\t\tcase 'shr': fileman.share(); break;\n\t\t\t}\n\t\t\tr.hide(true);\n\t\t};\n\t}\n\n\tfunction show(x, y, target, isGrid) {\n\t\tselFile = jcp(nsFile);\n\t\tif (target) {\n\t\t\tvar file = target.closest(\"#files tbody tr\");\n\t\t\tif (isGrid && target.matches && target.matches('#ggrid > a')) {\n\t\t\t\tvar ref = ebi(target.getAttribute('ref'));\n\t\t\t\tfile = ref && ref.closest('#files tbody tr');\n\t\t\t}\n\t\t\tvar fa = file && file.children[1].querySelector('a[id]');\n\t\t\tif (fa && fa.id != 'unsearch') {\n\t\t\t\tselFile.no_dsel = clgot(file, \"sel\");\n\t\t\t\tclmod(file, \"sel\", true);\n\t\t\t\tselFile.elem = file;\n\t\t\t\tselFile.url = fa.href;\n\t\t\t\tselFile.path = basenames(selFile.url).replace(/(&|\\?)v/, '');\n\t\t\t\tvar url = selFile.url.split(\"?\")[0],\n\t\t\t\t\tvsp = vsplit(url);\n\t\t\t\tselFile.dpath = vsp[0];\n\t\t\t\tselFile.name = vsp[1];\n\t\t\t\tif (url.endsWith(\"/\"))\n\t\t\t\t\tselFile.type = \"dir\";\n\t\t\t\telse {\n\t\t\t\t\tvar lead = file.firstChild.firstChild;\n\t\t\t\t\tif (lead.id === undefined)\n\t\t\t\t\t\tselFile.type = \"tf\";\n\t\t\t\t\telse {\n\t\t\t\t\t\tselFile.id = lead.id.split('-')[1];\n\t\t\t\t\t\tselFile.type = lead.innerHTML[0] == '(' ? 'gf' : lead.id.split('-')[0];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tmsel.selui();\n\n\t\tvar has_sel = msel.getsel().length;\n\t\tvar has_clip = fileman.clip.length;\n\n\t\tclmod(ebi('ropn'), 'hide', !selFile.path);\n\t\tclmod(ebi('rply'), 'hide', selFile.type != 'gf' && selFile.type != 'af');\n\t\tclmod(ebi('rpla'), 'hide', selFile.type != 'gf');\n\t\tclmod(ebi('rtxt'), 'hide', !selFile.id);\n\t\tclmod(ebi('rs1'), 'hide', !selFile.path);\n\t\tclmod(ebi('rmd'), 'hide', !selFile.name || selFile.name.slice(-3) != \".md\");\n\t\tclmod(ebi('rcpl'), 'hide', !selFile.path);\n\t\tclmod(ebi('rdl'), 'hide', !has_sel);\n\t\tclmod(ebi('rzip'), 'hide', !has_sel);\n\t\tclmod(ebi('rs2'), 'hide', !has_sel);\n\t\tclmod(ebi('rcut'), 'hide', !has_sel);\n\t\tclmod(ebi('rdel'), 'hide', !has_sel);\n\t\tclmod(ebi('rcpy'), 'hide', !has_sel);\n\t\tclmod(ebi('rpst'), 'hide', !has_clip);\n\t\tclmod(ebi('rrnm'), 'hide', !has_sel);\n\t\tclmod(ebi('rs3'), 'hide', !has_sel);\n\t\tclmod(ebi('rs4'), 'hide', !has_sel && !has(perms, \"write\"));\n\t\tvar shr = ebi('rshr');\n\t\tclmod(shr, 'hide', !can_shr || !get_evpath().indexOf(have_shr));\n\t\tshr.innerHTML = has_sel ? L.rc_shs : L.rc_shf;\n\n\t\tmenu.style.left = x + 5 + 'px';\n\t\tmenu.style.top = y + 5 + 'px';\n\t\tmenu.style.display = 'block';\n\t\tmenu.focus();\n\t}\n\n\tr.hide = function(force) {\n\t\tif (!menu.style.display || (!force && menu.contains(document.activeElement)))\n\t\t\treturn;\n\t\tif (selFile.elem && !selFile.no_dsel) {\n\t\t\tclmod(selFile.elem, \"sel\", false);\n\t\t\tmsel.selui();\n\t\t}\n\t\tselFile = jcp(nsFile);\n\t\tmenu.style.display = '';\n\t}\n\n\tebi('wrap').oncontextmenu = function(e) {\n\t\tif (!r.enabled || e.shiftKey || (r.double && menu.style.display) || /doc=/.exec(location.search)) {\n\t\t\tr.hide(true);\n\t\t\treturn true;\n\t\t}\n\t\tr.hide(true);\n\t\tif (selFile.elem && !selFile.no_dsel) {\n\t\t\tclmod(selFile.elem, \"sel\", false);\n\t\t\tmsel.selui();\n\t\t}\n\t\tev(e);\n\t\tvar gfile = thegrid.en && e.target && e.target.closest('#ggrid > a');\n\t\tshow(xscroll() + e.clientX, yscroll() + e.clientY, gfile || e.target, gfile);\n\t\treturn false;\n\t};\n\tmenu.onblur = function() {setTimeout(r.hide)};\n\n\treturn r;\n})();\n\n\nfunction reload_mp() {\n\tif (mp && mp.au) {\n\t\tmpo.au = mp.au;\n\t\tmpo.au2 = mp.au2;\n\t\tmpo.acs = mp.acs;\n\t\tmpo.fau = mp.fau;\n\t\tmpl.unbuffer();\n\t}\n\tvar plays = QSA('tr>td:first-child>a.play');\n\tfor (var a = plays.length - 1; a >= 0; a--)\n\t\tplays[a].parentNode.innerHTML = '-';\n\n\tmp = new MPlayer();\n\tif (mp.au && mp.au.tid && mp.au.evp == get_evpath()) {\n\t\tvar el = QS('a#a' + mp.au.tid);\n\t\tif (el)\n\t\t\tclmod(el, 'act', 1);\n\n\t\tel = el && el.closest('tr');\n\t\tif (el)\n\t\t\tclmod(el, 'play', 1);\n\t}\n\n\tsetTimeout(pbar.onresize, 1);\n}\n\n\nfunction reload_browser() {\n\tfilecols.set_style();\n\n\tvar parts = get_evpath().split('/'),\n\t\trm = ebi('entree'),\n\t\tftab = ebi('files'),\n\t\tlink = '', o;\n\n\twhile (rm.nextSibling)\n\t\trm.parentNode.removeChild(rm.nextSibling);\n\n\tfor (var a = 0; a < parts.length - 1; a++) {\n\t\tlink += parts[a] + '/';\n\t\tvar link2 = dks[link] ? addq(link, 'k=' + dks[link]) : link;\n\n\t\to = mknod('a');\n\t\to.setAttribute('href', link2);\n\t\to.textContent = uricom_dec(parts[a]) || '/';\n\t\tebi('path').appendChild(mknod('i'));\n\t\tebi('path').appendChild(o);\n\t}\n\n\treload_mp();\n\ttry { showsort(ftab); } catch (ex) { }\n\tmakeSortable(ftab, function () {\n\t\tmsel.origin_id(null);\n\t\tmsel.load(true);\n\t\tthegrid.setdirty();\n\t\tmp.read_order();\n\t});\n\n\tvar ns = ['pro', 'epi', 'lazy']\n\tfor (var a = 0; a < ns.length; a++)\n\t\tclmod(ebi(ns[a]), 'hidden', ebi('unsearch'));\n\n\tif (up2k)\n\t\tup2k.set_fsearch();\n\n\tthegrid.setdirty();\n\tmsel.render();\n}\n\n(function() {\n\tvar is_selma = false;\n\tvar dragging = false;\n\n\tvar startx, starty;\n\tvar fwrap = null;\n\tvar selbox = null;\n\tvar ttimer = null;\n\n\tvar lpdelay = 250; \n\tvar mvthresh = 44;\n\n\tfunction unbox() {\n\t\tqsr('.selbox');\n\t\tebi('gfiles').style.removeProperty('pointer-events')\n\t\tebi('wrap').style.removeProperty('user-select')\n\t\t\n\t\tif (selbox) {\n\t\t\tconsole.log(selbox)\n\t\t\twindow.getSelection().removeAllRanges();\n\t\t}\n\t\t\n\t\tis_selma = false;\n\t\tdragging = false;\n\t\tfwrap = null;\n\t\tselbox = null;\n\t\tttimer = null;\n\t}\n\n\tfunction getpp(e) {\n\t\tvar touch = (e.touches && e.touches[0]) || e;\n\t\treturn { x: touch.clientX, y: touch.clientY };\n\t}\n\n\tfunction sel_toggle(el, m) {\n\t\tclmod(el, 'sel', m);\n\t\tvar eref = el.getAttribute('ref');\n\t\tif (eref) {\n\t\t\tvar ehidden = ebi(eref);\n\t\t\tif (ehidden) {\n\t\t\t\tvar tr = ehidden.closest('tr');\n\t\t\t\tif (tr) clmod(tr, 'sel', m);\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction bob(b1, b2) {\n\t\treturn !(b1.right < b2.left || b1.left > b2.right ||\n\t\t\t\t b1.bottom < b2.top || b1.top > b2.bottom);\n\t}\n\n\tfunction sel_start(e) {\n\t\tif (e.button !== 0 && e.type !== 'touchstart') return;\n\t\tif (!thegrid.en || !treectl.dsel) return;\n\t\tif (e.target.closest('#widget,#ops,.opview,.doc')) return;\n\n\t\tif (e.target.closest('#gfiles'))\n\t\t\tebi('gfiles').style.userSelect = \"none\"\n\n\t\tvar pos = getpp(e);\n\t\tstartx = pos.x;\n\t\tstarty = pos.y;\n\t\tis_selma = true;\n\t\tttimer = null;\n\t\t\n\t\tif (e.type === 'touchstart') {\n\t\t\tttimer = setTimeout(function() {\n\t\t\t\tttimer = null;\n\t\t\t\tstart_drag();\n\t\t\t}, lpdelay);\n\t\t}\n\t}\n\t\n\tfunction start_drag() {\n\t\tif (dragging) return;\n\n\t\tdragging = true;\n\t\tselbox = document.createElement('div');\n\t\tselbox.className = 'selbox';\n\t\tdocument.body.appendChild(selbox);\n\n\t\tebi('gfiles').style.pointerEvents = 'none';\n\t}\n\t\n\tfunction sel_move(e) {\n\t\tif (!is_selma) return;\n\t\tvar pos = getpp(e);\n\t\tvar dist = Math.sqrt(Math.pow(pos.x - startx, 2) + Math.pow(pos.y - starty, 2));\n\n\t\tif (e.type === 'touchmove' && ttimer) {\n\t\t\tif (dist > mvthresh) {\n\t\t\t\tclearTimeout(ttimer);\n\t\t\t\tttimer = null;\n\t\t\t\tis_selma = false;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (!dragging && dist > mvthresh && !window.getSelection().toString()) {\n\t\t\tif (fwrap = e.target.closest('#wrap')) \n\t\t\t\tfwrap.style.userSelect = 'none';\n\t\t\telse return;\n\t\t\tstart_drag();\n\t\t}\n\n\t\tif (!dragging || !selbox) return;\n\t\tev(e);\n\n\t\tselbox.style.width = Math.abs(pos.x - startx) + 'px';\n\t\tselbox.style.height = Math.abs(pos.y - starty) + 'px';\n\t\tselbox.style.left = Math.min(pos.x, startx) + 'px';\n\t\tselbox.style.top = Math.min(pos.y, starty) + 'px';\n\n\t\tif (IE && window.getSelection)\n\t\t\twindow.getSelection().removeAllRanges();\n\t}\n\n\tfunction sel_end(e) {\n\t\tclearTimeout(ttimer);\n\t\tif (dragging && selbox) {\n\t\t\tvar sbrect = selbox.getBoundingClientRect();\n\t\t\tvar faf = QSA('#ggrid a');\n\t\t\tvar sadmode = e.shiftKey ? true : e.altKey ? false : \"t\";\n\t\t\tfor (var a = 0, aa = faf.length; a < aa; a++)\n\t\t\t\tif (bob(sbrect, faf[a].getBoundingClientRect()))\n\t\t\t\t\tsel_toggle(faf[a], sadmode);\n\t\t\tmsel.selui();\n\t\t\tev(e);\n\t\t}\n\t\tunbox();\n\t}\n\n\tfunction dsel_init() {\n\t\twindow.addEventListener('mousedown', sel_start);\n\t\twindow.addEventListener('mousemove', sel_move);\n\t\twindow.addEventListener('mouseup', sel_end);\n\n\t\twindow.addEventListener('touchstart', sel_start, { passive: true });\n\t\twindow.addEventListener('touchmove', sel_move, { passive: false });\n\t\twindow.addEventListener('touchend', sel_end, { passive: true });\n\n\t\twindow.addEventListener('dragstart', function(e) {\n\t\t\tif (treectl.dsel && (is_selma || dragging)) {\n\t\t\t\te.preventDefault();\n\t\t\t}\n\t\t});\n\t}\n\t\n\tdsel_init();\n})();\n\n\nvar mpss = (function() {\n\tvar r = {}, config, ssint, npaint = 0;\n\n\tr.load = function () {\n\t\tif (!afilt.ssg)\n\t\t\treturn false;\n\n\t\tconfig = {\n\t\t\tvthresh: afilt.sscv[0],\n\t\t\tsthresh: afilt.sscv[1],\n\t\t\tetresh: afilt.sscv[2],\n\t\t\tsspeed: clamp(afilt.sscv[3], 0.15, 8.0),\n\t\t\trspeed: 0.2,\n\t\t\tloopInterval: 25,\n\t\t};\n\t\treturn true;\n\t};\n\n\tr.go = function () {\n\t\tif (!ssint && afilt.ssen && r.load())\n\t\t\tssint = setInterval(detectSilence, config.loopInterval);\n\t};\n\n\tr.stop = function () {\n\t\tclearInterval(ssint);\n\t\tssint = null;\n\t\tif (!mp) return;\n\t\tif (afilt.ssg) afilt.ssg.gain.value = 1.0;\n\t\tif (mp.au && mp.au._ss) mp.au.playbackRate = 1.0;\n\t\tif (mp.au2 && mp.au2._ss) mp.au2.playbackRate = 1.0;\n\t};\n\n\tfunction detectSilence() {\n\t\tvar ae = mp.au;\n\t\tae._ss = true;\n\n\t\tvar gain = afilt.ssg.gain;\n\t\tvar duration = ae.duration || 0;\n\t\n\t\tvar slimit = duration * (config.sthresh / 100);\n\t\tvar elimit = duration * (1 - (config.etresh / 100));\n\t\tvar in_limits = ae.currentTime < slimit || ae.currentTime > elimit;\n\n\t\tvar tspeed = 1.0;\n\t\tvar tvol = 1.0;\n\t\tvar is_silent = false;\n\n\t\tif (in_limits) {\n\t\t\tvar analyser = afilt.ssa;\n\t\t\tvar da = new Uint8Array(analyser.frequencyBinCount);\n\t\t\tanalyser.getByteFrequencyData(da);\n\n\t\t\tvar maxvol = 0;\n\t\t\tfor (var i = 0; i < da.length; i++) {\n\t\t\t\tif (da[i] > maxvol) maxvol = da[i];\n\t\t\t}\n\n\t\t\tif (++npaint > 4) {\n\t\t\t\tnpaint = 0;\n\t\t\t\tebi('au_ss').innerHTML = maxvol;\n\t\t\t}\n\n\t\t\tif (maxvol < config.vthresh) {\n\t\t\t\ttspeed = config.sspeed;\n\t\t\t\ttvol = 0.0;\n\t\t\t\tis_silent = true;\n\t\t\t}\n\t\t}\n\n\t\tif (is_silent) {\n\t\t\tif (Math.abs(ae.playbackRate - tspeed) > 0.01) {\n\t\t\t\tae.playbackRate += (tspeed - ae.playbackRate) * config.rspeed;\n\t\t\t}\n\t\t\tif (Math.abs(gain.value - tvol) > 0.01) {\n\t\t\t\tgain.value += (tvol - gain.value) * config.rspeed;\n\t\t\t}\n\t\t} else {\n\t\t\tae.playbackRate = 1.0;\n\t\t\tgain.value = 1.0;\n\t\t}\n\t}\n\n\treturn r;\n})();\n\ntreectl.hydrate();\n\nJ_BRW = 2;\n"
  },
  {
    "path": "copyparty/web/browser2.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n\t<meta charset=\"utf-8\">\n\t<title>{{ title }}</title>\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=0.8\">\n\t<style>\n\t\thtml{font-family:sans-serif}\n\t\ttd{border:1px solid #999;border-width:1px 1px 0 0;padding:0 5px}\n\t\ta{display:block}\n\t</style>\n</head>\n\n<body>\n\t{%- if srv_info %}\n\t<p><span>{{ srv_info }}</span></p>\n\t{%- endif %}\n\n\t{%- if have_b_u %}\n\t<form method=\"post\" enctype=\"multipart/form-data\" accept-charset=\"utf-8\" action=\"{{ url_suf }}\">\n\t\t<input type=\"hidden\" name=\"act\" value=\"bput\" />\n\t\t<input type=\"file\" name=\"f\" multiple /><br />\n\t\t<input type=\"submit\" value=\"start upload\" />\n\t</form>\n\t<br />\n\t{%- endif %}\n\n\t{%- if logues[0] %}\n\t\t{%- if sb_lg %}\n\t<div>{{ logues[0][:2000]|e }}</div><br />\n\t\t{%- else %}\n\t<div>{{ logues[0][:2000] }}</div><br />\n\t\t{%- endif %}\n\t{%- endif %}\n\n\t<table id=\"files\">\n\t\t<thead>\n\t\t\t<tr>\n\t\t\t\t<th name=\"lead\"><span>c</span></th>\n\t\t\t\t<th name=\"href\"><span>File Name</span></th>\n\t\t\t\t<th name=\"sz\" sort=\"int\"><span>Size</span></th>\n\t\t\t\t<th name=\"ts\"><span>Date</span></th>\n\t\t\t</tr>\n\t\t</thead>\n\t\t<tbody>\n<tr><td></td><td><a href=\"../{{ url_suf }}\">parent folder</a></td><td>-</td><td>-</td></tr>\n\n{%- for f in files %}\n<tr><td>{{ f.lead }}</td><td><a href=\"{{ f.href }}{{\n\t'&' + url_suf[1:] if url_suf[:1] == '?' and '?' in f.href else url_suf\n\t}}\">{{ f.name|e }}</a></td><td>{{ f.sz }}</td><td>{{ f.dt }}</td></tr>\n{%- endfor %}\n\n\t\t</tbody>\n\t</table>\n\n\t{%- if logues[1] %}\n\t\t{%- if sb_lg %}\n\t<div>{{ logues[1][:2000]|e }}</div><br />\n\t\t{%- else %}\n\t<div>{{ logues[1][:2000] }}</div><br />\n\t\t{%- endif %}\n\t{%- endif %}\n\n\t<h2><a href=\"{{ r }}/{{ url_suf }}{{ url_suf and '&amp;' or '?' }}h\">control-panel</a></h2>\n\n</body>\n</html>\n\n"
  },
  {
    "path": "copyparty/web/cf.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n\t<meta charset=\"utf-8\">\n\t<title>{{ s_doctitle }}</title>\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=0.8\">\n</head>\n\n<body>\n\t<div id=\"box\" style=\"opacity: 0; font-family: sans-serif\">\n\t\t<h3>please press F5 to reload the page</h3>\n\t\t<p>sorry for the inconvenience</p>\n\t</div>\n\n\t<script>\n\t\tsetTimeout(function() {\n\t\t\tdocument.getElementById('box').style.opacity = 1;\n\t\t}, 500);\n\n\t\tparent.toast.ok(30, parent.L.cf_ok);\n\t\tparent.qsr('#cf_frame');\n\t</script>\n</body>\n\n</html>\n\n"
  },
  {
    "path": "copyparty/web/dbg-audio.js",
    "content": "var ofun = audio_eq.apply.bind(audio_eq);\naudio_eq.apply = function () {\n    var ac1 = mp.ac;\n    ofun();\n    var ac = mp.ac,\n        w = 2048,\n        h = 256;\n\n    if (!audio_eq.filters.length) {\n        audio_eq.ana = null;\n        return;\n    }\n\n    var can = ebi('fft_can');\n    if (!can) {\n        can = mknod('canvas', 'fft_can');\n        can.style.cssText = 'position:absolute;left:0;bottom:5em;width:' + w + 'px;height:' + h + 'px;z-index:9001';\n        document.body.appendChild(can);\n        can.width = w;\n        can.height = h;\n    }\n    var cc = can.getContext('2d');\n    if (!ac)\n        return;\n\n    var ana = ac.createAnalyser();\n    ana.smoothingTimeConstant = 0;\n    ana.fftSize = 8192;\n\n    audio_eq.filters[0].connect(ana);\n    audio_eq.ana = ana;\n\n    var buf = new Uint8Array(ana.frequencyBinCount),\n        colw = can.width / buf.length;\n\n    cc.fillStyle = '#fc0';\n    function draw() {\n        if (ana == audio_eq.ana)\n            requestAnimationFrame(draw);\n\n        ana.getByteFrequencyData(buf);\n\n        cc.clearRect(0, 0, can.width, can.height);\n\n        /*var x = 0, w = 1;\n        for (var a = 0; a < buf.length; a++) {\n            cc.fillRect(x, h - buf[a], w, h);\n            x += w;\n        }*/\n        var mul = Math.pow(w, 4) / buf.length;\n        for (var x = 0; x < w; x++) {\n            var a = Math.floor(Math.pow(x, 4) / mul),\n                v = buf[a];\n\n            cc.fillRect(x, h - v, 1, v);\n        }\n    }\n    draw();\n};\naudio_eq.apply();\n"
  },
  {
    "path": "copyparty/web/idp.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" id=\"ht_idp\">\n\n<head>\n\t<meta charset=\"utf-8\">\n\t<title>{{ s_doctitle }}</title>\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=0.8\">\n    <meta name=\"robots\" content=\"noindex, nofollow\">\n\t<meta name=\"theme-color\" content=\"#{{ tcolor }}\">\n\t<link rel=\"stylesheet\" media=\"screen\" href=\"{{ r }}/.cpr/w/shares.css?_={{ ts }}\">\n\t<link rel=\"stylesheet\" media=\"screen\" href=\"{{ r }}/.cpr/w/ui.css?_={{ ts }}\">\n{{ html_head }}\n</head>\n\n<body>\n\t<div id=\"wrap\">\n\t\t<a href=\"{{ r }}/?idp\">refresh</a>\n\t\t<a href=\"{{ r }}/?h\">control-panel</a>\n\n        <table id=\"tab\"><thead><tr>\n            <th>forget</th>\n            <th>user</th>\n            <th>groups</th>\n        </tr></thead><tbody>\n        {%- for un, gn in rows %}\n        <tr>\n            <td><a href=\"{{ r }}/?idp=rm={{ un|e }}\">forget</a></td>\n            <td>{{ un|e }}</td>\n            <td>{{ gn|e }}</td>\n        </tr>\n        {%- endfor %}\n        </tbody></table>\n        {%- if not rows %}\n        (there are no IdP users in the cache)\n        {%- endif %}\n    </div>\n\t<a href=\"#\" id=\"repl\">π</a>\n\t<script>\n\nvar SR=\"{{ r }}\",\n\tlang=\"{{ lang }}\",\n\tdfavico=\"{{ favico }}\";\n\nvar STG = window.localStorage;\ndocument.documentElement.className = (STG && STG.cpp_thm) || \"{{ this.args.theme }}\";\n\n</script>\n<script src=\"{{ r }}/.cpr/w/util.js?_={{ ts }}\"></script>\n{%- if js %}\n<script src=\"{{ js }}_={{ ts }}\"></script>\n{%- endif %}\n<script>\n    Date.now();function jsldp(a,b){2!=window[a]&&alert(\"FATAL ERROR: cannot load \"+b+\".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R\")}\n    jsldp(\"J_UTL\",\"util\");\n</script>\n</body>\n</html>\n\n"
  },
  {
    "path": "copyparty/web/md.css",
    "content": "html, body {\n\tcolor: #333;\n\tbackground: #eee;\n\tfont-family: sans-serif;\n\tfont-family: var(--font-main), sans-serif;\n\tline-height: 1.5em;\n}\nhtml.y #helpbox a {\n\tcolor: #079;\n}\nhtml.z #helpbox a {\n\tcolor: #fc5;\n}\n#repl {\n\tposition: absolute;\n\ttop: 0;\n\tright: .5em;\n\tborder: none;\n\tcolor: inherit;\n\tbackground: none;\n}\n#mtw {\n\tdisplay: none;\n}\n#mw {\n\tmargin: 0 auto;\n\tpadding: 0 1.5em;\n}\n#toast {\n\tbottom: auto;\n\ttop: 1.4em;\n}\na {\n\ttext-decoration: none;\n}\n#toc {\n\tmargin: 0 1em;\n\t-ms-scroll-chaining: none;\n\toverscroll-behavior-y: none;\n}\n#toc ul {\n\tpadding-left: 1em;\n}\n#toc>ul {\n\ttext-align: left;\n\tpadding-left: .5em;\n}\n#toc li {\n\tlist-style-type: none;\n\tline-height: 1.2em;\n\tmargin: .5em 0;\n}\n#toc a {\n\tcolor: #057;\n\tborder: none;\n\tbackground: none;\n\tdisplay: block;\n\tmargin-left: -.3em;\n\tpadding: .2em .3em;\n}\n#toc a.act {\n\tcolor: #fff;\n\tbackground: #07a;\n}\n.todo_pend,\n.todo_done {\n\tz-index: 99;\n\tposition: relative;\n\tdisplay: inline-block;\n\tfont-family: 'scp', monospace, monospace;\n\tfont-family: var(--font-mono), 'scp', monospace, monospace;\n\tfont-weight: bold;\n\tfont-size: 1.3em;\n\tline-height: .1em;\n\tmargin: -.5em 0 -.5em -.85em;\n\ttop: .1em;\n\tcolor: #b29;\n}\n.todo_done {\n\tcolor: #6b3;\n\ttext-shadow: .02em 0 0 #6b3;\n}\nblink {\n\tanimation: blinker .7s cubic-bezier(.9, 0, .1, 1) infinite;\n}\n@keyframes blinker {\n\t10% {\n\t\topacity: 0;\n\t}\n\t60% {\n\t\topacity: 1;\n\t}\n}\n\n\n.mdo pre {\n\tcounter-reset: precode;\n}\n.mdo pre code {\n\tcounter-increment: precode;\n\tdisplay: inline-block;\n\tborder: none;\n\tborder-bottom: 1px solid #cdc;\n\tmin-width: calc(100% - .6em);\n}\n.mdo pre code:last-child {\n\tborder-bottom: none;\n}\n.mdo pre code::before {\n\tcontent: counter(precode);\n\t-webkit-user-select: none;\n\t-moz-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none;\n\tdisplay: inline-block;\n\ttext-align: right;\n\tfont-size: .75em;\n\tcolor: #48a;\n\twidth: 4em;\n\tpadding-right: 1.5em;\n\tmargin-left: -5.5em;\n}\n\n\n@media screen {\n\thtml, body {\n\t\tmargin: 0;\n\t\tpadding: 0;\n\t\toutline: 0;\n\t\tborder: none;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t}\n\t#mw {\n\t\tmargin: 0 auto;\n\t\tright: 0;\n\t}\n\t#mp {\n\t\tmax-width: 52em;\n\t\tmargin-bottom: 6em;\n\t}\n\t#mn {\n\t\tpadding: 1.3em 0 .7em 1em;\n\t\tborder-bottom: 1px solid #ccc;\n\t\tbackground: #eee;\n\t\tz-index: 10;\n\t\twidth: calc(100% - 1em);\n\t}\n\t#mn a {\n\t\tcolor: #444;\n\t\tbackground: none;\n\t\tmargin: 0 0 0 -.2em;\n\t\tpadding: .3em 0 .3em .4em;\n\t\ttext-decoration: none;\n\t\tborder: none;\n\t\t/* ie: */\n\t\tborder-bottom: .1em solid #777\\9;\n\t\tmargin-right: 1em\\9;\n\t}\n\t#mn a:first-child {\n\t\tpadding-left: .5em;\n\t}\n\t#mn a:last-child {\n\t\tpadding-right: .5em;\n\t}\n\t#mn a:not(:last-child)::after {\n\t\tcontent: '';\n\t\twidth: 1.05em;\n\t\theight: 1.05em;\n\t\tmargin: -.2em .3em -.2em -.4em;\n\t\tdisplay: inline-block;\n\t\tborder: 1px solid rgba(154,154,154,0.6);\n\t\tborder-width: .2em .2em 0 0;\n\t\ttransform: rotate(45deg);\n\t}\n\t#mn a:hover {\n\t\tcolor: #000;\n\t\ttext-decoration: underline;\n\t}\n\t#mh {\n\t\tpadding: .4em 1em;\n\t\tposition: relative;\n\t\twidth: 100%;\n\t\twidth: calc(100% - 3em);\n\t\tbackground: #eee;\n\t\tz-index: 9;\n\t\ttop: 0;\n\t}\n\t#mh a {\n\t\tcolor: #444;\n\t\tbackground: none;\n\t\ttext-decoration: underline;\n\t\tmargin: 0 .1em;\n\t\tpadding: 0 .3em;\n\t\tborder: none;\n\t}\n\t#mh a:hover {\n\t\tcolor: #000;\n\t\tbackground: #ddd;\n\t}\n\t#toolsbox {\n\t\toverflow: hidden;\n\t\tdisplay: inline-block;\n\t\tbackground: #eee;\n\t\theight: 1.5em;\n\t\tpadding: 0 .2em;\n\t\tmargin: 0 .2em;\n\t\tposition: absolute;\n\t}\n\t#toolsbox.open {\n\t\theight: auto;\n\t\toverflow: visible;\n\t\tbackground: #eee;\n\t\tbox-shadow: 0 .2em .2em #ccc;\n\t\tpadding-bottom: .2em;\n\t}\n\t#toolsbox a {\n\t\tdisplay: block;\n\t}\n\t#toolsbox a+a {\n\t\ttext-decoration: none;\n\t}\n\t#lno {\n\t\tposition: absolute;\n\t\tright: 0;\n\t}\n\n\n\n\thtml.z,\n\thtml.z body {\n\t\tbackground: #222;\n\t\tcolor: #ccc;\n\t}\n\thtml.z #toc a {\n\t\tcolor: #ccc;\n\t\tborder-left: .4em solid #444;\n\t\tborder-bottom: .1em solid #333;\n\t}\n\thtml.z #toc a.act {\n\t\tcolor: #fff;\n\t\tborder-left: .4em solid #3ad;\n\t}\n\thtml.z #toc li {\n\t\tborder-width: 0;\n\t}\n\thtml.z #mn a {\n\t\tcolor: #ccc;\n\t}\n\thtml.z #mn {\n\t\tborder-bottom: 1px solid #333;\n\t}\n\thtml.z #mn,\n\thtml.z #mh {\n\t\tbackground: #222;\n\t}\n\thtml.z #mh a {\n\t\tcolor: #ccc;\n\t\tbackground: none;\n\t}\n\thtml.z #mh a:hover {\n\t\tbackground: #333;\n\t\tcolor: #fff;\n\t}\n\thtml.z #toolsbox {\n\t\tbackground: #222;\n\t}\n\thtml.z #toolsbox.open {\n\t\tbox-shadow: 0 .2em .2em #069;\n\t\tborder-radius: 0 0 .4em .4em;\n\t}\n}\n\n@media screen and (min-width: 66em) {\n\t#mw {\n\t\tposition: fixed;\n\t\toverflow-y: auto;\n\t\tleft: 14em;\n\t\tleft: calc(100% - 55em);\n\t\tmax-width: none;\n\t\tbottom: 0;\n\t\tscrollbar-color: #eb0 #f7f7f7;\n\t}\n\t#toc {\n\t\twidth: 13em;\n\t\twidth: calc(100% - 55.3em);\n\t\tmax-width: 30em;\n\t\tbackground: #eee;\n\t\tposition: fixed;\n\t\toverflow-y: auto;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\tbottom: 0;\n\t\tpadding: 0;\n\t\tmargin: 0;\n\t\tscrollbar-color: #eb0 #f7f7f7;\n\t\tbox-shadow: 0 0 1em rgba(0,0,0,0.1);\n\t\tborder-top: 1px solid #d7d7d7;\n\t}\n\t#toc li {\n\t\tborder-left: .3em solid #ccc;\n\t}\n\t#toc::-webkit-scrollbar-track {\n\t\tbackground: #f7f7f7;\n\t}\n\t#toc::-webkit-scrollbar {\n\t\tbackground: #f7f7f7;\n\t\twidth: .8em;\n\t}\n\t#toc::-webkit-scrollbar-thumb {\n\t\tbackground: #eb0;\n\t}\n\n\n\n\thtml.z #toc {\n\t\tbackground: #282828;\n\t\tborder-top: 1px solid #2c2c2c;\n\t\tbox-shadow: 0 0 1em #181818;\n\t}\n\thtml.z #toc,\n\thtml.z #mw {\n\t\tscrollbar-color: #b80 #282828;\n\t}\n\thtml.z #toc::-webkit-scrollbar-track {\n\t\tbackground: #282828;\n\t}\n\thtml.z #toc::-webkit-scrollbar {\n\t\tbackground: #282828;\n\t\twidth: .8em;\n\t}\n\thtml.z #toc::-webkit-scrollbar-thumb {\n\t\tbackground: #b80;\n\t}\n}\n@media screen and (min-width: 85.5em) {\n\t#toc { width: 30em }\n\t#mw { left: 30.5em }\n}\n@media print {\n\t@page {\n\t\tsize: A4;\n\t\tpadding: 0;\n\t\tmargin: .5in .6in;\n\t\tmso-header-margin: .6in;\n\t\tmso-footer-margin: .6in;\n\t\tmso-paper-source: 0;\n\t}\n\t.mdo a {\n\t\tcolor: #079;\n\t\ttext-decoration: none;\n\t\tborder-bottom: .07em solid #4ac;\n\t\tpadding: 0 .3em;\n\t}\n\t#repl {\n\t\tdisplay: none;\n\t}\n\t#toc>ul {\n\t\tborder-left: .1em solid #84c4dd;\n\t}\n\t#mn, #mh {\n\t\tdisplay: none;\n\t}\n\thtml, body, #toc, #mw {\n\t\tmargin: 0 !important;\n\t\tword-break: break-word;\n\t\twidth: 52em;\n\t}\n\t#toc {\n\t\tmargin-left: 1em !important;\n\t}\n\t#toc a {\n\t\tcolor: #000 !important;\n\t}\n\t#toc a::after {\n\t\t/* hopefully supported by browsers eventually */\n\t\tcontent: leader('.') target-counter(attr(href), page);\n\t}\n\ta[ctr]::before {\n\t\tcontent: attr(ctr) '. ';\n\t}\n\t.mdo h1 {\n\t\tmargin: 2em 0;\n\t}\n\t.mdo h2 {\n\t\tmargin: 2em 0 0 0;\n\t}\n\t.mdo h1,\n\t.mdo h2,\n\t.mdo h3 {\n\t\tpage-break-inside: avoid;\n\t}\n\t.mdo h1::after,\n\t.mdo h2::after,\n\t.mdo h3::after {\n\t\tcontent: 'orz';\n\t\tcolor: transparent;\n\t\tdisplay: block;\n\t\tline-height: 1em;\n\t\tpadding: 4em 0 0 0;\n\t\tmargin: 0 0 -5em 0;\n\t}\n\t.mdo p {\n\t\tpage-break-inside: avoid;\n\t}\n\t.mdo table {\n\t\tpage-break-inside: auto;\n\t}\n\t.mdo tr {\n\t\tpage-break-inside: avoid;\n\t\tpage-break-after: auto;\n\t}\n\t.mdo thead {\n\t\tdisplay: table-header-group;\n\t}\n\t.mdo tfoot {\n\t\tdisplay: table-footer-group;\n\t}\n\t#mp a.vis::after {\n\t\tcontent: ' (' attr(href) ')';\n\t\tborder-bottom: 1px solid #bbb;\n\t\tcolor: #444;\n\t}\n\t.mdo blockquote {\n\t\tborder-color: #555;\n\t}\n\t.mdo code {\n\t\tborder-color: #bbb;\n\t}\n\t.mdo pre,\n\t.mdo pre code {\n\t\tborder-color: #999;\n\t}\n\t.mdo pre code::before {\n\t\tcolor: #058;\n\t}\n\n\n\n\thtml.z .mdo a {\n\t\tcolor: #000;\n\t}\n\thtml.z .mdo pre,\n\thtml.z .mdo code {\n\t\tcolor: #240;\n\t}\n\thtml.z .mdo p>em,\n\thtml.z .mdo li>em,\n\thtml.z .mdo td>em {\n\t\tcolor: #940;\n\t}\n}\n"
  },
  {
    "path": "copyparty/web/md.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" id=\"ht_md\">\n\n<head>\n\t<meta charset=\"utf-8\">\n\t<title>📝 {{ title }}</title>\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=0.7\">\n\t<meta name=\"theme-color\" content=\"#{{ tcolor }}\">\n\t<link rel=\"stylesheet\" href=\"{{ r }}/.cpr/w/ui.css?_={{ ts }}\">\n\t<link rel=\"stylesheet\" href=\"{{ r }}/.cpr/w/md.css?_={{ ts }}\">\n\t{%- if edit %}\n\t<link rel=\"stylesheet\" href=\"{{ r }}/.cpr/w/md2.css?_={{ ts }}\">\n\t{%- endif %}\n{{ html_head }}\n</head>\n\n<body>\n\t<div id=\"mn\"></div>\n\t<div id=\"mh\">\n\t\t<a id=\"lightswitch\" href=\"#\">go dark</a>\n\t\t<a id=\"navtoggle\" href=\"#\">hide nav</a>\n\t\t{%- if edit %}\n\t\t\t<a id=\"save\" href=\"{{ arg_base }}edit\" tt=\"Hotkey: ctrl-s\">save</a>\n\t\t\t<a id=\"sbs\" href=\"#\" tt=\"editor and preview side by side\">sbs</a>\n\t\t\t<a id=\"nsbs\" href=\"#\" tt=\"switch between editor and preview$NHotkey: ctrl-e\">editor</a>\n\t\t\t<div id=\"toolsbox\">\n\t\t\t\t<a id=\"tools\" href=\"#\">tools</a>\n\t\t\t\t<a id=\"fmt_table\" href=\"#\">beautify table (ctrl-k)</a>\n\t\t\t\t<a id=\"fmt_json\" href=\"#\">beautify json (ctrl-j)</a>\n\t\t\t\t<a id=\"iter_uni\" href=\"#\">non-ascii: iterate (ctrl-u)</a>\n\t\t\t\t<a id=\"mark_uni\" href=\"#\">non-ascii: markup</a>\n\t\t\t\t<a id=\"cfg_uni\" href=\"#\">non-ascii: whitelist</a>\n\t\t\t\t<a id=\"help\" href=\"#\">help</a>\n\t\t\t</div>\n\t\t\t<span id=\"lno\">L#</span>\n\t\t{%- else %}\n\t\t\t<a href=\"{{ arg_base }}edit\" tt=\"good: higher performance$Ngood: same document width as viewer$Nbad: assumes you know markdown\">edit (basic)</a>\n\t\t\t<a href=\"{{ arg_base }}edit2\" id=\"edit2\" tt=\"not in-house so probably less buggy\">edit (fancy)</a>\n\t\t\t<a href=\"{{ arg_base }}\">view raw</a>\n\t\t{%- endif %}\n\t</div>\n\t<div id=\"toc\"></div>\n\t<div id=\"mtw\">\n\t\t<textarea id=\"mt\" autocomplete=\"off\">{{ md }}</textarea>\n\t</div>\n\t<div id=\"mw\">\n\t\t<div id=\"ml\">\n\t\t\t<div style=\"text-align:center;margin:5em 0\">\n\t\t\t\t<div style=\"font-size:2em;margin:1em 0\">Loading</div>\n\t\t\t\tif you're still reading this, check that javascript is allowed\n\t\t\t</div>\n\t\t</div>\n\t\t<div id=\"mp\" class=\"mdo\"></div>\n\t</div>\n\t<a href=\"#\" id=\"repl\">π</a>\n\n\t{%- if edit %}\n\t<div id=\"helpbox\">\n\t\t<textarea autocomplete=\"off\">\n\nwrite markdown (most html is 🙆 too)\n\n## hotkey list\n* `Ctrl-S` to save\n* `Ctrl-E` to toggle mode\n* `Ctrl-K` to prettyprint a table\n* `Ctrl-U` to iterate non-ascii chars\n* `Ctrl-H` / `Ctrl-Shift-H` to create a header\n* `TAB` / `Shift-TAB` to indent/dedent a selection\n\n## toolbar\n1. toggle dark mode\n2. show/hide navigation bar\n3. save changes on server\n4. side-by-side editing\n5. toggle editor/preview\n6. this thing :^)\n\n## markdown\n|||\n|--|--|\n|`**bold**`|**bold**|\n|`_italic_`|_italic_|\n|`~~strike~~`|~~strike~~|\n|`` `code` ``|`code`|\n|`[](#hotkey-list)`|[](#hotkey-list)|\n|`[](/foo/bar.md#header)`|[](/foo/bar.md#header)|\n|`<blink>💯</blink>`|<blink>💯</blink>|\n\n## tables\n    |left-aligned|centered|right-aligned\n    | ---------- | :----: | ----------:\n    |one         |two     |three\n\n|left-aligned|centered|right-aligned\n| ---------- | :----: | ----------:\n|one         |two     |three\n\n## lists\n\t* one\n\t* two\n\t1. one\n\t1. two\n* one\n* two\n1. one\n1. two\n\n## headers\n\t# level 1\n\t## level 2\n\t### level 3\n\n## quote\n\t> hello\n> hello\n\n## codeblock\n\t\tfour spaces (no tab pls)\n\n## code in lists\n\t* foo\n\t  bar\n          six spaces total\n* foo\n  bar\n      six spaces total\n.\n\t\t</textarea>\n\t</div>\n\t{%- endif %}\n\n\t<script>\n\nvar SR = \"{{ r }}\",\n\tlast_modified = {{ lastmod }},\n\thave_emp = {{ have_emp }},\n\tmd_no_br = {{ md_no_br }},\n\tdfavico = \"{{ favico }}\";\n\nvar md_opt = {\n\tlink_md_as_html: false,\n\tmodpoll_freq: {{ md_chk_rate }}\n};\n\n(function () {\n\tvar l = window.localStorage,\n\t\tdrk = (l && l.light) != 1,\n\t\tbtn = document.getElementById(\"lightswitch\"),\n\t\tf = function (e) {\nif (e) { e.preventDefault(); drk = !drk; }\ndocument.documentElement.className = drk? \"z\":\"y\";\nbtn.innerHTML = \"go \" + (drk ? \"light\":\"dark\");\ntry { l.light = drk? 0:1; } catch (ex) { }\n\t\t};\n\tbtn.onclick = f;\n\tf();\n})();\n\n\t</script>\n\t<script src=\"{{ r }}/.cpr/w/util.js?_={{ ts }}\"></script>\n\t<script src=\"{{ r }}/.cpr/w/deps/marked.js?_={{ ts }}\"></script>\n\t<script src=\"{{ r }}/.cpr/w/md.js?_={{ ts }}\"></script>\n\t{%- if edit %}\n\t<script src=\"{{ r }}/.cpr/w/md2.js?_={{ ts }}\"></script>\n\t{%- endif %}\n\t<script>\n\t\tDate.now();function jsldp(a,b){2!=window[a]&&alert(\"FATAL ERROR: cannot load \"+b+\".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R\")}\n\t\tjsldp(\"J_UTL\",\"util\");\n\t\tjsldp(\"J_MD\",\"md\");\n\t\t{%- if edit %}\n\t\tjsldp(\"J_MD2\",\"md2\");\n\t\t{%- endif %}\n\t</script>\n\t{%- if js %}\n\t<script src=\"{{ js }}_={{ ts }}\"></script>\n\t{%- endif %}\n</body>\n</html>\n\n"
  },
  {
    "path": "copyparty/web/md.js",
    "content": "\"use strict\";\n\nvar J_MD = 1;\nvar dom_toc = ebi('toc'),\n    dom_wrap = ebi('mw'),\n    dom_hbar = ebi('mh'),\n    dom_nav = ebi('mn'),\n    dom_pre = ebi('mp'),\n    dom_src = ebi('mt'),\n    dom_navtgl = ebi('navtoggle'),\n    hash0 = location.hash;\n\n\n// chrome 49 needs this\nvar chromedbg = function () { console.log(arguments); }\n\n// null-logger\nvar dbg = function () { };\n\n// replace dbg with the real deal here or in the console:\n// dbg = chromedbg;\n// dbg = console.log;\n\n\n// dodge browser issues\n(function () {\n    if (UA.indexOf(') Gecko/') !== -1 && /Linux| Mac /.exec(UA)) {\n        // necessary on ff-68.7 at least\n        var s = mknod('style');\n        s.innerHTML = '@page { margin: .5in .6in .8in .6in; }';\n        console.log(s.innerHTML);\n        document.head.appendChild(s);\n    }\n})();\n\n\n// add navbar\n(function () {\n    var parts = (get_evpath().slice(0, -1).split('?')[0] + '?v').split('/'), link = '', o;\n    for (var a = 0, aa = parts.length - 1; a <= aa; a++) {\n        link += parts[a] + (a < aa ? '/' : '');\n        o = mknod('a');\n        o.setAttribute('href', link);\n        o.textContent = uricom_dec(parts[a].split('?')[0]) || 'top';\n        dom_nav.appendChild(o);\n    }\n})();\n\n\n// image load handler\nvar img_load = (function () {\n    var r = {};\n    r.callbacks = [];\n\n    var fire = function () {\n        for (var a = 0; a < r.callbacks.length; a++)\n            r.callbacks[a]();\n    }\n\n    var timeout = null;\n    r.done = function () {\n        clearTimeout(timeout);\n        timeout = setTimeout(fire, 500);\n    };\n\n    return r;\n})();\n\n\n// faster than replacing the entire html (chrome 1.8x, firefox 1.6x)\nfunction copydom(src, dst, lv) {\n    var sc = src.childNodes,\n        dc = dst.childNodes;\n\n    if (sc.length !== dc.length) {\n        dbg(\"replace L%d (%d/%d) |%d|\",\n            lv, sc.length, dc.length, src.innerHTML.length);\n\n        dst.innerHTML = src.innerHTML;\n        return;\n    }\n\n    var rpl = [];\n    for (var a = sc.length - 1; a >= 0; a--) {\n        var st = sc[a].tagName || sc[a].nodeType,\n            dt = dc[a].tagName || dc[a].nodeType;\n\n        if (st !== dt) {\n            dbg(\"replace L%d (%d/%d) type %s/%s\", lv, a, sc.length, st, dt);\n            dst.innerHTML = src.innerHTML;\n            return;\n        }\n\n        var sa = sc[a].attributes || [],\n            da = dc[a].attributes || [];\n\n        if (sa.length !== da.length) {\n            dbg(\"replace L%d (%d/%d) attr# %d/%d\",\n                lv, a, sc.length, sa.length, da.length);\n\n            rpl.push(a);\n            continue;\n        }\n\n        var dirty = false;\n        for (var b = sa.length - 1; b >= 0; b--) {\n            var name = sa[b].name,\n                sv = sa[b].value,\n                dv = dc[a].getAttribute(name);\n\n            if (name == \"data-ln\" && sv !== dv) {\n                dc[a].setAttribute(name, sv);\n                continue;\n            }\n\n            if (sv !== dv) {\n                dbg(\"replace L%d (%d/%d) attr %s [%s] [%s]\",\n                    lv, a, sc.length, name, sv, dv);\n\n                dirty = true;\n                break;\n            }\n        }\n        if (dirty)\n            rpl.push(a);\n    }\n\n    // TODO pure guessing\n    if (rpl.length > sc.length / 3) {\n        dbg(\"replace L%d fully, %s (%d/%d) |%d|\",\n            lv, rpl.length, sc.length, src.innerHTML.length);\n\n        dst.innerHTML = src.innerHTML;\n        return;\n    }\n\n    // repl is reversed; build top-down\n    var nbytes = 0;\n    for (var a = rpl.length - 1; a >= 0; a--) {\n        var i = rpl[a],\n            prop = sc[i].nodeType == 1 ? 'outerHTML' : 'nodeValue';\n\n        var html = sc[i][prop];\n        dc[i][prop] = html;\n        nbytes += html.length;\n    }\n    if (nbytes > 0)\n        dbg(\"replaced %d bytes L%d\", nbytes, lv);\n\n    for (var a = 0; a < sc.length; a++)\n        copydom(sc[a], dc[a], lv + 1);\n\n    if (src.innerHTML !== dst.innerHTML) {\n        dbg(\"setting %d bytes L%d\", src.innerHTML.length, lv);\n        dst.innerHTML = src.innerHTML;\n    }\n}\n\n\nmd_plug_err = function (ex, js) {\n    qsr('#md_errbox');\n    if (!ex)\n        return;\n\n    var msg = (ex + '').split('\\n')[0];\n    var ln = ex.lineNumber;\n    var o = null;\n    if (ln) {\n        msg = \"Line \" + ln + \", \" + msg;\n        var lns = js.split('\\n');\n        if (ln < lns.length) {\n            o = mknod('span');\n            o.style.cssText = \"color:#ac2;font-size:.9em;font-family:'scp',monospace,monospace;display:block\";\n            o.textContent = lns[ln - 1];\n        }\n    }\n    var errbox = mknod('div', 'md_errbox');\n    errbox.style.cssText = 'position:absolute;top:0;left:0;padding:1em .5em;background:#2b2b2b;color:#fc5'\n    errbox.textContent = msg;\n    errbox.onclick = function () {\n        modal.alert('<pre>' + esc(ex.stack) + '</pre>');\n    };\n    if (o) {\n        errbox.appendChild(o);\n        errbox.style.padding = '.25em .5em';\n    }\n    dom_nav.appendChild(errbox);\n\n    try {\n        console.trace();\n    }\n    catch (ex2) { }\n}\n\n\nfunction convert_markdown(md_text, dest_dom) {\n    md_text = md_text.replace(/\\r/g, '');\n\n    md_plug_err(null);\n    md_text = load_md_plug(md_text, 'pre');\n    md_text = load_md_plug(md_text, 'post');\n\n    var marked_opts = {\n        //headerPrefix: 'h-',\n        breaks: !md_no_br,\n        gfm: true\n    };\n\n    var ext = md_plug.pre;\n    if (ext)\n        Object.assign(marked_opts, ext[0]);\n\n    try {\n        var md_html = marked.parse(md_text, marked_opts);\n        if (!have_emp)\n            md_html = DOMPurify.sanitize(md_html);\n    }\n    catch (ex) {\n        if (IE) {\n            dest_dom.innerHTML = 'IE cannot into markdown ;_;';\n            return false;\n        }\n\n        if (ext)\n            md_plug_err(ex, ext[1]);\n\n        throw ex;\n    }\n    var md_dom = dest_dom;\n    try {\n        md_dom = new DOMParser().parseFromString(md_html, \"text/html\").body;\n    }\n    catch (ex) {\n        md_dom.innerHTML = md_html;\n        window.copydom = noop;\n    }\n\n    var nodes = md_dom.getElementsByTagName('a');\n    for (var a = nodes.length - 1; a >= 0; a--) {\n        var href = nodes[a].getAttribute('href');\n        var txt = nodes[a].innerHTML;\n\n        if (/\\.[Mm][Dd]$/.test(href)) {\n            var o = new URL(href, location.href).origin;\n            if (!o || o == location.origin)\n                nodes[a].href = href + '?v';\n        }\n\n        if (!txt)\n            nodes[a].textContent = href;\n        else if (href !== txt && !nodes[a].className)\n            nodes[a].className = 'vis';\n    }\n\n    // todo-lists (should probably be a marked extension)\n    nodes = md_dom.getElementsByTagName('input');\n    for (var a = nodes.length - 1; a >= 0; a--) {\n        var dom_box = nodes[a];\n        if (dom_box.getAttribute('type') !== 'checkbox')\n            continue;\n\n        var dom_li = dom_box.parentNode;\n        var done = dom_box.getAttribute('checked');\n        done = done !== null;\n        var clas = done ? 'done' : 'pend';\n        var char = done ? 'Y' : 'N';\n\n        dom_li.className = 'task-list-item';\n        dom_li.style.listStyleType = 'none';\n        var html = dom_li.innerHTML;\n        dom_li.innerHTML =\n            '<span class=\"todo_' + clas + '\">' + char + '</span>' +\n            html.slice(html.indexOf('>') + 1);\n    }\n\n    // separate <code> for each line in <pre>\n    nodes = md_dom.getElementsByTagName('pre');\n    for (var a = nodes.length - 1; a >= 0; a--) {\n        var el = nodes[a];\n\n        var is_precode =\n            el.tagName == 'PRE' &&\n            el.childNodes.length === 1 &&\n            el.childNodes[0].tagName == 'CODE';\n\n        if (!is_precode)\n            continue;\n\n        var nline = parseInt(el.getAttribute('data-ln')) + 1;\n        var lines = el.innerHTML.replace(/\\n<\\/code>$/i, '</code>').split(/\\n/g);\n        for (var b = 0; b < lines.length - 1; b++)\n            lines[b] += '</code>\\n<code data-ln=\"' + (nline + b) + '\">';\n\n        el.innerHTML = lines.join('');\n    }\n\n    // self-link headers\n    var id_seen = {},\n        dyn = md_dom.getElementsByTagName('*');\n\n    nodes = [];\n    for (var a = 0, aa = dyn.length; a < aa; a++)\n        if (/^[Hh]([1-6])/.exec(dyn[a].tagName) !== null)\n            nodes.push(dyn[a]);\n\n    for (var a = 0; a < nodes.length; a++) {\n        el = nodes[a];\n        var id = el.getAttribute('id'),\n            orig_id = id;\n\n        if (id_seen[id]) {\n            for (var n = 1; n < 4096; n++) {\n                id = orig_id + '-' + n;\n                if (!id_seen[id])\n                    break;\n            }\n            el.setAttribute('id', id);\n        }\n        id_seen[id] = 1;\n        el.innerHTML = '<a href=\"#' + id + '\">' + el.innerHTML + '</a>';\n    }\n\n    ext = md_plug.post;\n    if (ext && ext[0].render)\n        try {\n            ext[0].render(md_dom);\n        }\n        catch (ex) {\n            md_plug_err(ex, ext[1]);\n        }\n\n    copydom(md_dom, dest_dom, 0);\n\n    var imgs = dest_dom.getElementsByTagName('img');\n    for (var a = 0, aa = imgs.length; a < aa; a++)\n        imgs[a].onload = img_load.done;\n\n    if (ext && ext[0].render2)\n        try {\n            ext[0].render2(dest_dom);\n        }\n        catch (ex) {\n            md_plug_err(ex, ext[1]);\n        }\n\n    if (hash0)\n        setTimeout(function () {\n            try {\n                QS(hash0).scrollIntoView();\n                hash0 = '';\n            }\n            catch (ex) { }\n        }, 1);\n    \n    return true;\n}\n\n\nfunction init_toc() {\n    qsr('#ml');\n\n    var anchors = [];  // list of toc entries, complex objects\n    var anchor = null; // current toc node\n    var html = [];     // generated toc html\n    var lv = 0;        // current indentation level in the toc html\n    var ctr = [0, 0, 0, 0, 0, 0];\n\n    var manip_nodes_dyn = dom_pre.getElementsByTagName('*');\n    var manip_nodes = [];\n    for (var a = 0, aa = manip_nodes_dyn.length; a < aa; a++)\n        manip_nodes.push(manip_nodes_dyn[a]);\n\n    for (var a = 0, aa = manip_nodes.length; a < aa; a++) {\n        var elm = manip_nodes[a];\n        var m = /^[Hh]([1-6])/.exec(elm.tagName);\n        var is_header = m !== null;\n        if (is_header) {\n            var nlv = m[1];\n            while (lv < nlv) {\n                html.push('<ul>');\n                lv++;\n            }\n            while (lv > nlv) {\n                html.push('</ul>');\n                lv--;\n            }\n            ctr[lv - 1]++;\n            for (var b = lv; b < 6; b++)\n                ctr[b] = 0;\n\n            elm.childNodes[0].setAttribute('ctr', ctr.slice(0, lv).join('.'));\n\n            var elm2 = elm.cloneNode(true);\n            elm2.childNodes[0].textContent = elm.textContent;\n            while (elm2.childNodes.length > 1)\n                elm2.removeChild(elm2.childNodes[1]);\n\n            html.push('<li>' + elm2.innerHTML + '</li>');\n\n            if (anchor != null)\n                anchors.push(anchor);\n\n            anchor = {\n                elm: elm,\n                kids: [],\n                y: null\n            };\n        }\n        if (!is_header && anchor)\n            anchor.kids.push(elm);\n    }\n    dom_toc.innerHTML = html.join('\\n');\n    if (anchor != null)\n        anchors.push(anchor);\n\n    // copy toc links into the toc list\n    var atoc = dom_toc.getElementsByTagName('a');\n    for (var a = 0, aa = anchors.length; a < aa; a++)\n        anchors[a].lnk = atoc[a];\n\n    // collect vertical position of all toc items (headers in document)\n    function freshen_offsets() {\n        var top = yscroll();\n        for (var a = anchors.length - 1; a >= 0; a--) {\n            var y = top + anchors[a].elm.getBoundingClientRect().top;\n            y = Math.round(y * 10.0) / 10;\n            if (anchors[a].y === y)\n                break;\n\n            anchors[a].y = y;\n        }\n    }\n\n    // highlight the correct toc items + scroll into view\n    function freshen_toclist() {\n        if (anchors.length == 0)\n            return;\n\n        var ptop = yscroll();\n        var hit = anchors.length - 1;\n        for (var a = 0; a < anchors.length; a++) {\n            if (anchors[a].y >= ptop - 8) {  //???\n                hit = a;\n                break;\n            }\n        }\n\n        var links = dom_toc.getElementsByTagName('a');\n        if (!anchors[hit].active) {\n            for (var a = 0; a < anchors.length; a++) {\n                if (anchors[a].active) {\n                    anchors[a].active = false;\n                    links[a].className = '';\n                }\n            }\n            anchors[hit].active = true;\n            links[hit].className = 'act';\n        }\n\n        var pane_height = parseInt(getComputedStyle(dom_toc).height);\n        var link_bounds = links[hit].getBoundingClientRect();\n        var top = link_bounds.top - (pane_height / 6);\n        var btm = link_bounds.bottom + (pane_height / 6);\n        if (top < 0)\n            dom_toc.scrollTop -= -top;\n        else if (btm > pane_height)\n            dom_toc.scrollTop += btm - pane_height;\n    }\n\n    function refresh() {\n        freshen_offsets();\n        freshen_toclist();\n    }\n\n    return { \"refresh\": refresh }\n}\n\n\n// \"main\" :p\nconvert_markdown(dom_src.value, dom_pre);\nvar toc = init_toc();\nimg_load.callbacks = [toc.refresh];\n\n\n// scroll handler\nvar redraw = (function () {\n    var sbs = true;\n    var onresize = function () {\n        if (window.matchMedia)\n            sbs = window.matchMedia('(min-width: 64em)').matches;\n\n        var y = (dom_hbar.offsetTop + dom_hbar.offsetHeight) + 'px';\n        if (sbs) {\n            dom_toc.style.top = y;\n            dom_wrap.style.top = y;\n            dom_toc.style.marginTop = '0';\n        }\n        onscroll();\n    }\n\n    var onscroll = function () {\n        toc.refresh();\n    }\n\n    window.onresize = onresize;\n    window.onscroll = onscroll;\n    dom_wrap.onscroll = onscroll;\n\n    onresize();\n    return onresize;\n})();\n\n\ndom_navtgl.onclick = function () {\n    var hidden = dom_navtgl.innerHTML == 'hide nav';\n    dom_navtgl.innerHTML = hidden ? 'show nav' : 'hide nav';\n    dom_nav.style.display = hidden ? 'none' : 'block';\n\n    swrite('hidenav', hidden ? 1 : 0);\n    redraw();\n};\n\nif (sread('hidenav') == 1)\n    dom_navtgl.onclick();\n\nif (window.tt && tt.init)\n    tt.init();\n\nJ_MD = 2;\n"
  },
  {
    "path": "copyparty/web/md2.css",
    "content": "#toc {\n\tdisplay: none;\n}\n#mtw {\n\tdisplay: block;\n\tposition: fixed;\n\tleft: .5em;\n\tbottom: 0;\n\twidth: calc(100% - 56em);\n}\n#mw {\n\toverflow-y: auto;\n\tposition: fixed;\n\tbottom: 0;\n\tleft: 0;\n}\n@media (min-width: 55em) {\n\t#mw {left:calc(100% - 55em)}\n}\n\n\n/* single-screen */\n#mtw.preview,\n#mw.editor {\n\topacity: 0;\n\tz-index: 1;\n}\n#mw.preview,\n#mtw.editor {\n\tz-index: 5;\n}\n#mtw.single,\n#mw.single {\n\tmargin: 0;\n\tleft: 1em;\n\tleft: max(1em, calc((100% - 56em) / 2));\n}\n#mtw.single {\n\twidth: 55em;\n\twidth: min(55em, calc(100% - 2em));\n}\n#mtw.single.editor,\n#mw.single.editor {\n\twidth: calc(100% - 1em);\n\tleft: .5em;\n}\n\n\n#mp {\n\tposition: relative;\n}\n#mt, #mtr {\n\twidth: 100%;\n\theight: calc(100% - 1px);\n\tcolor: #444;\n\tbackground: #f7f7f7;\n\tborder: 1px solid #999;\n\toutline: none;\n\tpadding: 0;\n\tmargin: 0;\n\tfont-family: 'scp', monospace, monospace;\n\tfont-family: var(--font-mono), 'scp', monospace, monospace;\n\twhite-space: pre-wrap;\n\tword-break: break-word;\n\toverflow-wrap: break-word;\n\tword-wrap: break-word; /*ie*/\n\toverflow-y: scroll;\n\tline-height: 1.3em;\n\tfont-size: .9em;\n\tposition: relative;\n\tscrollbar-color: #eb0 #f7f7f7;\n}\nhtml.z #mt {\n\tcolor: #eee;\n\tbackground: #222;\n\tborder: 1px solid #777;\n\tscrollbar-color: #b80 #282828;\n}\n#mtr {\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n}\n#save.force-save {\n\tcolor: #400;\n\tbackground: #f97;\n\tborder-radius: .15em;\n}\nhtml.z #save.force-save {\n\tcolor: #fca;\n\tbackground: #720;\n}\n#save.disabled {\n\topacity: .4;\n}\n#helpbox {\n\tbackground: #f7f7f7;\n\tborder-radius: .4em;\n\tz-index: 9001;\n\tdisplay: none;\n\tposition: fixed;\n\tpadding: 2em;\n\ttop: 4em;\n\toverflow-y: auto;\n\tbox-shadow: 0 .5em 2em #777;\n\theight: calc(100% - 12em);\n\tleft: calc(50% - 15em);\n\tright: 0;\n\twidth: 30em;\n}\n#helpclose {\n\tdisplay: block;\n}\nhtml.z #helpbox {\n\tbox-shadow: 0 .5em 2em #444;\n\tbackground: #222;\n\tborder: 1px solid #079;\n\tborder-width: 1px 0;\n}\n"
  },
  {
    "path": "copyparty/web/md2.js",
    "content": "\"use strict\";\n\n\nvar J_MD2 = 1;\nvar sloc0 = '' + location,\n    dbg_kbd = /[?&]dbgkbd\\b/.exec(sloc0);\n\n\n// server state\nvar server_md = dom_src.value;\n\n\n// the non-ascii whitelist\nvar esc_uni_whitelist = '\\\\n\\\\t\\\\x20-\\\\x7eÆØÅæøå';\nvar js_uni_whitelist = eval('\\'' + esc_uni_whitelist + '\\'');\n\n\n// dom nodes\nvar dom_swrap = ebi('mtw');\nvar dom_sbs = ebi('sbs');\nvar dom_nsbs = ebi('nsbs');\nvar dom_tbox = ebi('toolsbox');\nvar dom_ref = (function () {\n    var d = mknod('div', 'mtr');\n    dom_swrap.appendChild(d);\n    d = ebi('mtr');\n    // hide behind the textarea (offsetTop is not computed if display:none)\n    dom_src.style.zIndex = '4';\n    d.style.zIndex = '3';\n    return d;\n})();\n\n\n// line->scrollpos maps\nfunction genmapq(dom, query) {\n    var ret = [];\n    var last_y = -1;\n    var parent_y = 0;\n    var parent_n = null;\n    var nodes = dom.querySelectorAll(query);\n    for (var a = 0; a < nodes.length; a++) {\n        var n = nodes[a];\n        var ln = parseInt(n.getAttribute('data-ln'));\n        if (ln in ret)\n            continue;\n\n        var y = 0;\n        var par = n.offsetParent;\n        if (par && par != parent_n) {\n            while (par && par != dom) {\n                y += par.offsetTop;\n                par = par.offsetParent;\n            }\n            if (par != dom)\n                continue;\n\n            parent_y = y;\n            parent_n = n.offsetParent;\n        }\n        while (ln > ret.length)\n            ret.push(null);\n\n        y = parent_y + n.offsetTop;\n        if (y <= last_y)\n            //console.log('awawa');\n            continue;\n\n        //console.log('%d  %d  (%d+%d)', a, y, parent_y, n.offsetTop);\n        ret.push(y);\n        last_y = y;\n    }\n    return ret;\n}\nvar map_src = [];\nvar map_pre = [];\nfunction genmap(dom, oldmap) {\n    var find = nlines;\n    while (oldmap && find-- > 0) {\n        var tmap = genmapq(dom, '*[data-ln=\"' + find + '\"]');\n        if (!tmap || !tmap.length)\n            continue;\n\n        var cy = tmap[find];\n        var oy = parseInt(oldmap[find]);\n        if (cy + 24 > oy && cy - 24 < oy)\n            return oldmap;\n\n        console.log('map regen', dom.getAttribute('id'), find, oy, cy, oy - cy);\n        break;\n    }\n    return genmapq(dom, '*[data-ln]');\n}\n\n\n// input handler\nvar action_stack = null;\nvar nlines = 0;\nvar draw_md = (function () {\n    var delay = 1;\n    var draw_md = function () {\n        var t0 = Date.now();\n        var src = dom_src.value;\n        convert_markdown(src, dom_pre);\n\n        var lines = esc(src).replace(/\\r/g, \"\").split('\\n');\n        nlines = lines.length;\n        var html = [];\n        for (var a = 0; a < lines.length; a++)\n            html.push('<span data-ln=\"' + (a + 1) + '\">' + lines[a] + \"</span>\");\n\n        dom_ref.innerHTML = html.join('\\n');\n        map_src = genmap(dom_ref, map_src);\n        map_pre = genmap(dom_pre, map_pre);\n\n        clmod(ebi('save'), 'disabled',\n            src.replace(/\\r/g, \"\") == server_md.replace(/\\r/g, \"\"));\n\n        var t1 = Date.now();\n        delay = t1 - t0 > 100 ? 25 : 1;\n    }\n\n    var timeout = null;\n    dom_src.oninput = function (e) {\n        clearTimeout(timeout);\n        timeout = setTimeout(draw_md, delay);\n        if (action_stack)\n            action_stack.push();\n    };\n\n    draw_md();\n    return draw_md;\n})();\n\n\n// discard TOC callback, just regen editor scroll map\nimg_load.callbacks = [function () {\n    map_pre = genmap(dom_pre, map_pre);\n}];\n\n\n// resize handler\nredraw = (function () {\n    var onresize = function () {\n        var y = (dom_hbar.offsetTop + dom_hbar.offsetHeight) + 'px';\n        dom_wrap.style.top = y;\n        dom_swrap.style.top = y;\n        dom_ref.style.width = getComputedStyle(dom_src).offsetWidth + 'px';\n        map_src = genmap(dom_ref, map_src);\n        map_pre = genmap(dom_pre, map_pre);\n    }\n    var setsbs = function () {\n        dom_wrap.className = '';\n        dom_swrap.className = '';\n        onresize();\n    }\n    var modetoggle = function () {\n        var mode = dom_nsbs.innerHTML;\n        dom_nsbs.innerHTML = mode == 'editor' ? 'preview' : 'editor';\n        mode += ' single';\n        dom_wrap.className = mode;\n        dom_swrap.className = mode;\n        onresize();\n    }\n\n    window.onresize = onresize;\n    window.onscroll = null;\n    dom_wrap.onscroll = null;\n    dom_sbs.onclick = setsbs;\n    dom_nsbs.onclick = modetoggle;\n\n    (IE ? modetoggle : onresize)();\n    return onresize;\n})();\n\n\n// scroll handlers\n(function () {\n    var skip_src = false, skip_pre = false;\n\n    var scroll = function (src, srcmap, dst, dstmap) {\n        var y = src.scrollTop;\n        if (y < 8) {\n            dst.scrollTop = 0;\n            return;\n        }\n        if (y + 48 + src.clientHeight > src.scrollHeight) {\n            dst.scrollTop = dst.scrollHeight - dst.clientHeight;\n            return;\n        }\n        y += src.clientHeight / 2;\n        var sy1 = -1, sy2 = -1, dy1 = -1, dy2 = -1;\n        for (var a = 1; a < nlines + 1; a++) {\n            if (srcmap[a] == null || dstmap[a] == null)\n                continue;\n\n            if (srcmap[a] > y) {\n                sy2 = srcmap[a];\n                dy2 = dstmap[a];\n                break;\n            }\n            sy1 = srcmap[a];\n            dy1 = dstmap[a];\n        }\n        if (sy1 == -1)\n            return;\n\n        var dy = dy1;\n        if (sy2 != -1 && dy2 != -1) {\n            var mul = (y - sy1) / (sy2 - sy1);\n            dy = dy1 + (dy2 - dy1) * mul;\n        }\n        dst.scrollTop = dy - dst.clientHeight / 2;\n    }\n\n    dom_src.onscroll = function () {\n        //dbg: dom_ref.scrollTop = dom_src.scrollTop;\n        if (skip_src) {\n            skip_src = false;\n            return;\n        }\n        skip_pre = true;\n        scroll(dom_src, map_src, dom_wrap, map_pre);\n    };\n\n    dom_wrap.onscroll = function () {\n        if (skip_pre) {\n            skip_pre = false;\n            return;\n        }\n        skip_src = true;\n        scroll(dom_wrap, map_pre, dom_src, map_src);\n    };\n})();\n\n\n// modification checker\nfunction Modpoll() {\n    var r = {\n        initial: true,\n        skip_one: false,\n        disabled: false\n    };\n\n    r.periodic = function () {\n        var skip = null;\n\n        if (toast.visible)\n            skip = 'toast';\n\n        else if (r.skip_one)\n            skip = 'saved';\n\n        else if (r.disabled)\n            skip = 'disabled';\n\n        if (skip) {\n            console.log('modpoll skip, ' + skip);\n            r.skip_one = false;\n            return;\n        }\n\n        console.log('modpoll...');\n        var url = (location + '').split('?')[0] + '?_=' + Date.now();\n        var xhr = new XHR();\n        xhr.open('GET', url, true);\n        xhr.responseType = 'text';\n        xhr.onload = xhr.onerror = r.cb;\n        xhr.send();\n    };\n\n    r.cb = function () {\n        if (r.disabled || r.skip_one) {\n            console.log('modpoll abort');\n            return;\n        }\n\n        if (this.status !== 200) {\n            console.log('modpoll err ' + this.status + \": \" + this.responseText);\n            return;\n        }\n\n        if (!this.responseText)\n            return;\n\n        var new_md = this.responseText,\n            new_mt = this.getResponseHeader('X-Lastmod3') || r.lastmod,\n            server_ref = server_md.replace(/\\r/g, ''),\n            server_now = new_md.replace(/\\r/g, '');\n\n        // firefox bug: sometimes get stale text even if copyparty sent a 200\n        if (r.initial && server_ref != server_now)\n            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 () {\n                dom_src.value = server_md = new_md;\n                last_modified = new_mt;\n                draw_md();\n            }, null);\n\n        r.initial = false;\n\n        if (server_ref != server_now) {\n            console.log(\"modpoll diff |\" + server_ref.length + \"|, |\" + server_now.length + \"|\");\n            r.disabled = true;\n            var msg = [\n                \"The document has changed on the server.\",\n                \"The changes will NOT be loaded into your editor automatically.\",\n                \"\",\n                \"Press F5 or CTRL-R to refresh the page,\",\n                \"replacing your document with the server copy.\",\n                \"\",\n                \"You can close this message to ignore and contnue.\"\n            ];\n            return toast.warn(0, msg.join('\\n'));\n        }\n\n        console.log('modpoll eq');\n    };\n\n    setTimeout(r.periodic, 300);\n    if (md_opt.modpoll_freq > 0)\n        setInterval(r.periodic, 1000 * md_opt.modpoll_freq);\n\n    return r;\n}\nvar modpoll = new Modpoll();\n\n\nwindow.onbeforeunload = function (e) {\n    if ((ebi(\"save\").className + '').indexOf('disabled') >= 0)\n        return; //nice (todo)\n\n    e.preventDefault(); //ff\n    e.returnValue = ''; //chrome\n};\n\n\n// save handler\nfunction save(e) {\n    ev(e);\n    var save_btn = ebi(\"save\"),\n        save_cls = save_btn.className + '';\n\n    if (save_cls.indexOf('disabled') >= 0)\n        return toast.inf(2, \"no changes\");\n\n    var force = (save_cls.indexOf('force-save') >= 0);\n    function save2() {\n        var txt = dom_src.value,\n            fd = new FormData();\n\n        fd.append(\"act\", \"tput\");\n        fd.append(\"lastmod\", (force ? -1 : last_modified));\n        fd.append(\"body\", txt);\n\n        var url = (location + '').split('?')[0];\n        var xhr = new XHR();\n        xhr.open('POST', url, true);\n        xhr.responseType = 'text';\n        xhr.onload = xhr.onerror = save_cb;\n        xhr.btn = save_btn;\n        xhr.txt = txt;\n\n        modpoll.skip_one = true;  // skip one iteration while we save\n        xhr.send(fd);\n    }\n\n    if (!force)\n        save2();\n    else\n        modal.confirm('confirm that you wish to lose the changes made on the server since you opened this document', save2, function () {\n            toast.inf(3, 'aborted');\n        });\n}\n\nfunction save_cb() {\n    if (this.status !== 200)\n        return toast.err(0, 'Error!  The file was NOT saved.\\n\\nError ' + this.status + \":\\n\" + unpre(this.responseText));\n\n    var r;\n    try {\n        r = JSON.parse(this.responseText);\n    }\n    catch (ex) {\n        return toast.err(0, 'Error!  The file was likely NOT saved.\\n\\nFailed to parse reply from server:\\n\\n' + unpre(this.responseText));\n    }\n\n    if (!r.ok) {\n        if (!clgot(this.btn, 'force-save')) {\n            clmod(this.btn, 'force-save', 1);\n            var msg = [\n                'This file has been modified since you started editing it!\\n',\n                'if you really want to overwrite, press save again.\\n',\n                'modified ' + ((r.now - r.lastmod) / 1000) + ' seconds ago,',\n                ((r.lastmod - last_modified) / 1000) + ' sec after you opened it\\n',\n                last_modified + ' lastmod when you opened it,',\n                r.lastmod + ' lastmod on the server now,',\n                r.now + ' server time now,\\n',\n            ];\n            return toast.err(0, msg.join('\\n'));\n        }\n        else\n            return toast.err(0, 'Error! Save failed.  Maybe this JSON explains why:\\n\\n' + this.responseText);\n    }\n\n    clmod(this.btn, 'force-save');\n    //alert('save OK -- wrote ' + r.size + ' bytes.\\n\\nsha512: ' + r.sha512);\n\n    run_savechk(r.lastmod, this.txt, this.btn, 0);\n}\n\nfunction run_savechk(lastmod, txt, btn, ntry) {\n    // download the saved doc from the server and compare\n    var url = (location + '').split('?')[0] + '?_=' + Date.now();\n    var xhr = new XHR();\n    xhr.open('GET', url, true);\n    xhr.responseType = 'text';\n    xhr.onload = xhr.onerror = savechk_cb;\n    xhr.lastmod = lastmod;\n    xhr.txt = txt;\n    xhr.btn = btn;\n    xhr.ntry = ntry;\n    xhr.send();\n}\n\nfunction savechk_cb() {\n    if (this.status !== 200)\n        return toast.err(0, 'Error!  The file was NOT saved.\\n\\nError ' + this.status + \":\\n\" + unpre(this.responseText));\n\n    var doc1 = this.txt.replace(/\\r\\n/g, \"\\n\");\n    var doc2 = this.responseText.replace(/\\r\\n/g, \"\\n\");\n    if (doc1 != doc2) {\n        var that = this;\n        if (that.ntry < 10) {\n            // qnap funny, try a few more times\n            setTimeout(function () {\n                run_savechk(that.lastmod, that.txt, that.btn, that.ntry + 1)\n            }, 100);\n            return;\n        }\n        modal.alert(\n            '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' +\n            'Length: yours=' + doc1.length + ', server=' + doc2.length\n        );\n        modal.alert('yours, ' + doc1.length + ' byte:\\n[' + doc1 + ']');\n        modal.alert('server, ' + doc2.length + ' byte:\\n[' + doc2 + ']');\n        return;\n    }\n\n    last_modified = this.lastmod;\n    server_md = this.txt;\n    draw_md();\n    toast.ok(2, 'save OK' + (this.ntry ? '\\nattempt ' + this.ntry : ''));\n    modpoll.disabled = false;\n}\n\n\n// firefox bug: initial selection offset isn't cleared properly through js\nvar ff_clearsel = (function () {\n    if (UA.indexOf(') Gecko/') === -1)\n        return function () { }\n\n    return function () {\n        var txt = dom_src.value;\n        var y = dom_src.scrollTop;\n        dom_src.value = '';\n        dom_src.value = txt;\n        dom_src.scrollTop = y;\n    };\n})();\n\n\n// returns car/cdr (selection bounds) and n1/n2 (grown to full lines)\nfunction linebounds(just_car, greedy_growth) {\n    var car = dom_src.selectionStart,\n        cdr = dom_src.selectionEnd;\n\n    if (just_car)\n        cdr = car;\n\n    var md = dom_src.value,\n        n1 = Math.max(car, 0),\n        n2 = Math.min(cdr, md.length - 1);\n\n    if (greedy_growth !== true) {\n        if (n1 < n2 && md[n1] == '\\n')\n            n1++;\n\n        if (n1 < n2 && md[n2 - 1] == '\\n')\n            n2 -= 2;\n    }\n\n    n1 = md.lastIndexOf('\\n', n1 - 1) + 1;\n    n2 = md.indexOf('\\n', n2);\n    if (n2 < n1)\n        n2 = md.length;\n\n    return {\n        \"car\": car,\n        \"cdr\": cdr,\n        \"n1\": n1,\n        \"n2\": n2,\n        \"md\": md\n    }\n}\n\n\n// linebounds + the three textranges\nfunction getsel() {\n    var s = linebounds(false);\n    s.pre = s.md.substring(0, s.n1);\n    s.sel = s.md.substring(s.n1, s.n2);\n    s.post = s.md.substring(s.n2);\n    return s;\n}\n\n\n// place modified getsel into markdown\nfunction setsel(s) {\n    if (s.car != s.cdr) {\n        s.car = s.pre.length;\n        s.cdr = s.pre.length + s.sel.length;\n    }\n    dom_src.value = [s.pre, s.sel, s.post].join('');\n    dom_src.setSelectionRange(s.car, s.cdr, dom_src.selectionDirection);\n    dom_src.oninput();\n    // support chrome:\n    dom_src.blur();\n    dom_src.focus();\n}\n\n\n// cut/copy current line\nfunction md_cut(cut) {\n    var s = linebounds();\n    if (s.car != s.cdr)\n        return;\n\n    dom_src.setSelectionRange(s.n1, s.n2 + 1, 'forward');\n    setTimeout(function () {\n        var i = cut ? s.n1 : s.car;\n        dom_src.setSelectionRange(i, i, 'forward');\n    }, 1);\n}\n\n\n// indent/dedent\nfunction md_indent(dedent) {\n    var s = getsel(),\n        sel0 = s.sel;\n\n    if (dedent)\n        s.sel = s.sel.replace(/^  /, \"\").replace(/\\n  /g, \"\\n\");\n    else\n        s.sel = '  ' + s.sel.replace(/\\n/g, '\\n  ');\n\n    if (s.car == s.cdr)\n        s.car = s.cdr += s.sel.length - sel0.length;\n\n    setsel(s);\n}\n\n\n// header\nfunction md_header(dedent) {\n    var s = getsel(),\n        sel0 = s.sel;\n\n    if (dedent)\n        s.sel = s.sel.replace(/^#/, \"\").replace(/^ +/, \"\");\n    else\n        s.sel = s.sel.replace(/^(#*) ?/, \"#$1 \");\n\n    if (s.car == s.cdr)\n        s.car = s.cdr += s.sel.length - sel0.length;\n\n    setsel(s);\n}\n\n\n// smart-home\nfunction md_home(shift) {\n    var s = linebounds(false, true),\n        ln = s.md.substring(s.n1, s.n2),\n        dir = dom_src.selectionDirection,\n        rev = dir === 'backward',\n        p1 = rev ? s.car : s.cdr,\n        p2 = rev ? s.cdr : s.car,\n        home = 0,\n        lf = ln.lastIndexOf('\\n') + 1,\n        re = /^[ \\t#>+-]*(\\* )?([0-9]+\\. +)?/;\n\n    if (rev)\n        home = s.n1 + re.exec(ln)[0].length;\n    else\n        home = s.n1 + lf + re.exec(ln.substring(lf))[0].length;\n\n    p1 = (p1 !== home) ? home : (rev ? s.n1 : s.n1 + lf);\n    if (!shift)\n        p2 = p1;\n\n    if (rev !== p1 < p2)\n        dir = rev ? 'forward' : 'backward';\n\n    if (!shift)\n        ff_clearsel();\n\n    dom_src.setSelectionRange(Math.min(p1, p2), Math.max(p1, p2), dir);\n}\n\n\n// autoindent\nfunction md_newline() {\n    var s = linebounds(true),\n        ln = s.md.substring(s.n1, s.n2),\n        m1 = /^( *)([0-9]+)(\\. +)/.exec(ln),\n        m2 = /^[ \\t]*[>+*-]{0,2}[ \\t]/.exec(ln),\n        drop = dom_src.selectionEnd - dom_src.selectionStart;\n\n    var pre = m2 ? m2[0] : '';\n    if (m1 !== null)\n        pre = m1[1] + (parseInt(m1[2]) + 1) + m1[3];\n\n    if (pre.length > s.car - s.n1)\n        // in gutter, do nothing\n        return true;\n\n    s.pre = s.md.substring(0, s.car) + '\\n' + pre;\n    s.sel = '';\n    s.post = s.md.substring(s.car + drop);\n    s.car = s.cdr = s.pre.length;\n    setsel(s);\n    return false;\n}\n\n\n// backspace\nfunction md_backspace() {\n    var s = linebounds(true),\n        o0 = dom_src.selectionStart,\n        left = s.md.slice(s.n1, o0),\n        m = /^[ \\t>+-]*(\\* )?([0-9]+\\. +)?/.exec(left);\n\n    // if car is in whitespace area, do nothing\n    if (/^\\s*$/.test(left))\n        return true;\n\n    // same if selection\n    if (o0 != dom_src.selectionEnd)\n        return true;\n\n    // same if line is all-whitespace or non-markup\n    var v = m[0].replace(/[^ ]/g, \" \");\n    if (v === m[0] || v.length !== left.length)\n        return true;\n\n    s.pre = s.md.substring(0, s.n1) + v;\n    s.sel = '';\n    s.post = s.md.substring(s.car);\n    s.car = s.cdr = s.pre.length;\n    setsel(s);\n    return false;\n}\n\n\n// paragraph jump\nfunction md_p_jump(down) {\n    var txt = dom_src.value,\n        ofs = dom_src.selectionStart;\n\n    if (down) {\n        while (txt[ofs] == '\\n' && --ofs > 0);\n        ofs = txt.indexOf(\"\\n\\n\", ofs);\n        if (ofs < 0)\n            ofs = txt.length - 1;\n\n        while (txt[ofs] == '\\n' && ++ofs < txt.length - 1);\n    }\n    else {\n        txt += '\\n\\n';\n        while (ofs > 1 && txt[ofs - 1] == '\\n') ofs--;\n        ofs = Math.max(0, txt.lastIndexOf(\"\\n\\n\", ofs - 1));\n        while (txt[ofs] == '\\n' && ++ofs < txt.length - 1);\n    }\n\n    dom_src.setSelectionRange(ofs, ofs, \"none\");\n}\n\n\nfunction reLastIndexOf(txt, ptn, end) {\n    var ofs = (typeof end !== 'undefined') ? end : txt.length;\n    end = ofs;\n    while (ofs >= 0) {\n        var sub = txt.slice(ofs, end);\n        if (ptn.test(sub))\n            return ofs;\n\n        ofs--;\n    }\n    return -1;\n}\n\n\n// json formatter\nfunction fmt_json(e) {\n    ev(e);\n    try {\n        fmt_json2();\n    }\n    catch (ex) {\n        return toast.err(7, 'json-format (CTRL-J) failed\\n\\n(hint: select the json you want to beautify/minify first)\\n\\n' + ex);\n    }\n}\nfunction fmt_json2() {\n    var txt = dom_src.value,\n        o0 = txt.lastIndexOf('\\n', dom_src.selectionStart - 1),\n        o1 = txt.indexOf('\\n', dom_src.selectionEnd);\n    o0 = o0 + 1 ? o0 + 1 : 0;\n    if (o1 < 0) o1 = txt.length;\n    for (var a = 0; a < 9; a++) {\n        if (has(['\\r', '\\n', ' '], txt.charAt(o0))) ++o0;\n        if (has(['\\r', '\\n', ' '], txt.charAt(o1))) --o1;\n    }\n    var jt0 = txt.slice(o0, ++o1),\n        jo = JSON.parse(jt0),\n        jt = JSON.stringify(jo, null, jt0.indexOf('\\n') + 1 ? 0 : 2);\n    setsel({\n        \"pre\": txt.slice(0, o0),\n        \"sel\": jt,\n        \"post\": txt.slice(o1),\n        \"car\": o0,\n        \"cdr\": o0,\n    });\n}\n\n\n// table formatter\nfunction fmt_table(e) {\n    ev(e);\n    try {\n        fmt_table2();\n    }\n    catch (ex) {\n        return toast.err(7, 'table-format (CTRL-K) failed:\\n' + ex);\n    }\n}\nfunction fmt_table2() {\n    var txt = dom_src.value,\n        ofs = dom_src.selectionStart,\n        //o0 = txt.lastIndexOf('\\n\\n', ofs),\n        //o1 = txt.indexOf('\\n\\n', ofs);\n        o0 = reLastIndexOf(txt, /\\n\\s*\\n/m, ofs),\n        o1 = txt.slice(ofs).search(/\\n\\s*\\n|\\n\\s*$/m);\n    // note \\s contains \\n but its fine\n\n    if (o0 < 0)\n        o0 = 0;\n    else {\n        // seek past the hit\n        var m = /\\n\\s*\\n/m.exec(txt.slice(o0));\n        o0 += m[0].length;\n    }\n\n    o1 = o1 < 0 ? txt.length : o1 + ofs;\n\n    var err = 'cannot format table due to ',\n        tab = txt.slice(o0, o1).split(/\\s*\\n/),\n        re_ind = /^\\s*/,\n        ind = tab[1].match(re_ind)[0],\n        r0_ind = tab[0].slice(0, ind.length),\n        lpipe = tab[1].indexOf('|') < tab[1].indexOf('-'),\n        rpipe = tab[1].lastIndexOf('|') > tab[1].lastIndexOf('-'),\n        re_lpipe = lpipe ? /^\\s*\\|\\s*/ : /^\\s*/,\n        re_rpipe = rpipe ? /\\s*\\|\\s*$/ : /\\s*$/,\n        ncols;\n\n    // the second row defines the table,\n    // need to process that first\n    var tmp = tab[0];\n    tab[0] = tab[1];\n    tab[1] = tmp;\n\n    for (var a = 0; a < tab.length; a++) {\n        var row_name = (a == 1) ? 'header' : 'row#' + (a + 1);\n\n        var ind2 = tab[a].match(re_ind)[0];\n        if (ind != ind2 && a != 1)  // the table can be a list entry or something, ignore [0]\n            return toast.err(7, err + 'indentation mismatch on row#2 and ' + row_name + ',\\n' + tab[a]);\n\n        var t = tab[a].slice(ind.length);\n        t = t.replace(re_lpipe, \"\");\n        t = t.replace(re_rpipe, \"\");\n        tab[a] = t.split(/\\s*\\|\\s*/g);\n\n        if (a == 0)\n            ncols = tab[a].length;\n        else if (ncols < tab[a].length)\n            return toast.err(7, err + 'num.columns(' + row_name + ') exceeding row#2;  ' + ncols + ' < ' + tab[a].length);\n\n        // if row has less columns than row2, fill them in\n        while (tab[a].length < ncols)\n            tab[a].push('');\n    }\n\n    // aight now swap em back\n    tmp = tab[0];\n    tab[0] = tab[1];\n    tab[1] = tmp;\n\n    var re_align = /^ *(:?)-+(:?) *$/;\n    var align = [];\n    for (var col = 0; col < tab[1].length; col++) {\n        var m = tab[1][col].match(re_align);\n        if (!m)\n            return toast.err(7, err + 'invalid column specification, row#2, col ' + (col + 1) + ', [' + tab[1][col] + ']');\n\n        if (m[2]) {\n            if (m[1])\n                align.push('c');\n            else\n                align.push('r');\n        }\n        else\n            align.push('l');\n    }\n\n    var pad = [];\n    var tmax = 0;\n    for (var col = 0; col < ncols; col++) {\n        var max = 0;\n        for (var row = 0; row < tab.length; row++)\n            if (row != 1)\n                max = Math.max(max, tab[row][col].length);\n\n        var s = '';\n        for (var n = 0; n < max; n++)\n            s += ' ';\n\n        pad.push(s);\n        tmax = Math.max(max, tmax);\n    }\n\n    var dashes = '';\n    for (var a = 0; a < tmax; a++)\n        dashes += '-';\n\n    var ret = [];\n    for (var row = 0; row < tab.length; row++) {\n        var ln = [];\n        for (var col = 0; col < tab[row].length; col++) {\n            var p = pad[col];\n            var s = tab[row][col];\n\n            if (align[col] == 'l') {\n                s = (s + p).slice(0, p.length);\n            }\n            else if (align[col] == 'r') {\n                s = (p + s).slice(-p.length);\n            }\n            else {\n                var pt = p.length - s.length;\n                var pl = p.slice(0, Math.floor(pt / 2));\n                var pr = p.slice(0, pt - pl.length);\n                s = pl + s + pr;\n            }\n\n            if (row == 1) {\n                if (align[col] == 'l')\n                    s = dashes.slice(0, p.length);\n                else if (align[col] == 'r')\n                    s = dashes.slice(0, p.length - 1) + ':';\n                else\n                    s = ':' + dashes.slice(0, p.length - 2) + ':';\n            }\n            ln.push(s);\n        }\n        ret.push(ind + '| ' + ln.join(' | ') + ' |');\n    }\n\n    // restore any markup in the row0 gutter\n    ret[0] = r0_ind + ret[0].slice(ind.length);\n\n    ret = {\n        \"pre\": txt.slice(0, o0),\n        \"sel\": ret.join('\\n'),\n        \"post\": txt.slice(o1),\n        \"car\": o0,\n        \"cdr\": o0\n    };\n    setsel(ret);\n}\n\n\n// show unicode\nfunction mark_uni(e) {\n    ev(e);\n    dom_tbox.className = '';\n\n    var txt = dom_src.value,\n        ptn = new RegExp('([^' + js_uni_whitelist + ']+)', 'g'),\n        mod = txt.replace(/\\r/g, \"\").replace(ptn, \"\\u2588\\u2770$1\\u2771\");\n\n    if (txt == mod)\n        return toast.inf(5, 'no results;  no modifications were made');\n\n    dom_src.value = mod;\n}\n\n\n// iterate unicode\nfunction iter_uni(e) {\n    ev(e);\n\n    var txt = dom_src.value,\n        ofs = dom_src.selectionDirection == \"forward\" ? dom_src.selectionEnd : dom_src.selectionStart,\n        re = new RegExp('([^' + js_uni_whitelist + ']+)'),\n        m = re.exec(txt.slice(ofs));\n\n    if (!m)\n        return toast.inf(5, 'no more hits from cursor onwards');\n\n    ofs += m.index;\n\n    dom_src.setSelectionRange(ofs, ofs + m[0].length, \"forward\");\n    dom_src.oninput();\n    // support chrome:\n    dom_src.blur();\n    dom_src.focus();\n}\n\n\n// configure whitelist\nfunction cfg_uni(e) {\n    ev(e);\n\n    modal.prompt(\"unicode whitelist\", esc_uni_whitelist, function (reply) {\n        esc_uni_whitelist = reply;\n        js_uni_whitelist = eval('\\'' + esc_uni_whitelist + '\\'');\n    }, null);\n}\n\n\nvar set_lno = (function () {\n    var t = null,\n        pi = null,\n        pv = null,\n        lno = ebi('lno');\n\n    var poke = function () {\n        clearTimeout(t);\n        t = setTimeout(fire, 20);\n    }\n\n    var fire = function () {\n        try {\n            clearTimeout(t);\n\n            var i = dom_src.selectionStart;\n            if (i === pi)\n                return;\n\n            var lns = dom_src.value.slice(0, i).split('\\n'),\n                v = lns.length + ' : ' + lns.pop().length;\n\n            if (v != pv)\n                lno.innerHTML = v;\n\n            pi = i;\n            pv = v;\n        }\n        catch (e) { }\n    }\n\n    timer.add(fire);\n    return poke;\n})();\n\n\n// hotkeys / toolbar\n(function () {\n    var keydown = function (e) {\n        if (!e && window.event) {\n            e = window.event;\n            if (dev_fbw == 1) {\n                toast.warn(10, 'hello from fallback code ;_;\\ncheck console trace');\n                console.error('using window.event');\n            }\n        }\n        var k = (e.key || e.code) + '',\n            editing = document.activeElement == dom_src;\n\n        if (k.startsWith('Key'))\n            k = k.slice(3);\n\n        var kl = k.toLowerCase();\n\n        if (dbg_kbd)\n            console.log('KBD', k, kl, e.key, e.code, e.keyCode, e.which);\n\n        if (ctrl(e) && kl == \"s\") {\n            save();\n            return false;\n        }\n        if (k == \"Escape\" || k == \"Esc\") {\n            var d = ebi('helpclose');\n            if (d)\n                d.click();\n        }\n        if (editing)\n            set_lno();\n\n        if (ctrl(e)) {\n            if (kl == \"e\") {\n                dom_nsbs.click();\n                return false;\n            }\n            if (!editing)\n                return true;\n\n            if (kl == \"h\") {\n                md_header(e.shiftKey);\n                return false;\n            }\n            if (kl == \"z\") {\n                if (e.shiftKey)\n                    action_stack.redo();\n                else\n                    action_stack.undo();\n\n                return false;\n            }\n            if (kl == \"y\") {\n                action_stack.redo();\n                return false;\n            }\n            if (kl == \"j\") {\n                fmt_json(e.shiftKey);\n                return false;\n            }\n            if (kl == \"k\") {\n                fmt_table();\n                return false;\n            }\n            if (kl == \"u\") {\n                iter_uni();\n                return false;\n            }\n            if (k == \"ArrowUp\" || k == \"ArrowDown\") {\n                md_p_jump(k == \"ArrowDown\");\n                return false;\n            }\n            if (kl == \"x\" || kl == \"c\") {\n                md_cut(kl == \"x\");\n                return true; //sic\n            }\n        }\n        else {\n            if (!editing)\n                return true;\n\n            if (k == \"Tab\") {\n                md_indent(e.shiftKey);\n                return false;\n            }\n            if (k == \"Home\") {\n                md_home(e.shiftKey);\n                return false;\n            }\n            if (!e.shiftKey && k.endsWith(\"Enter\")) {\n                return md_newline();\n            }\n            if (!e.shiftKey && k == \"Backspace\") {\n                return md_backspace();\n            }\n        }\n    }\n    document.onkeydown = keydown;\n    ebi('save').onclick = save;\n})();\n\n\nebi('tools').onclick = function (e) {\n    ev(e);\n    var is_open = dom_tbox.className != 'open';\n    dom_tbox.className = is_open ? 'open' : '';\n};\n\n\nebi('help').onclick = function (e) {\n    ev(e);\n    dom_tbox.className = '';\n\n    var dom = ebi('helpbox');\n    var dtxt = dom.getElementsByTagName('textarea');\n    if (dtxt.length > 0) {\n        var txt = dtxt[0].value;\n        if (!convert_markdown(txt, dom))\n            dom.innerText = txt.split('## markdown')[0];\n        dom.innerHTML = '<a href=\"#\" id=\"helpclose\">close</a>' + dom.innerHTML;\n    }\n\n    dom.style.display = 'block';\n    ebi('helpclose').onclick = function () {\n        dom.style.display = 'none';\n    };\n};\n\n\nebi('fmt_table').onclick = fmt_table;\nebi('fmt_json').onclick = fmt_json;\nebi('mark_uni').onclick = mark_uni;\nebi('iter_uni').onclick = iter_uni;\nebi('cfg_uni').onclick = cfg_uni;\n\n\n// blame steen\naction_stack = (function () {\n    var hist = {\n        un: [],\n        re: []\n    };\n    var sched_cpos = 0;\n    var sched_timer = null;\n    var ignore = false;\n    var ref = dom_src.value;\n\n    var diff = function (from, to, cpos) {\n        if (from === to)\n            return null;\n\n        var car = 0,\n            max = Math.max(from.length, to.length);\n\n        for (; car < max; car++)\n            if (from[car] != to[car])\n                break;\n\n        var p1 = from.length,\n            p2 = to.length;\n\n        while (p1 --> 0 && p2 --> 0)\n            if (from[p1] != to[p2])\n                break;\n\n        if (car > ++p1)\n            car = p1;\n\n        var txt = from.substring(car, p1)\n        return {\n            car: car,\n            cdr: p2 + (car && 1),\n            txt: txt,\n            cpos: cpos\n        };\n    }\n\n    var undiff = function (from, change) {\n        var t1 = from.substring(0, change.car),\n            t2 = from.substring(change.cdr);\n\n        return {\n            txt: t1 + change.txt + t2,\n            cpos: change.cpos\n        };\n    }\n\n    var apply = function (src, dst) {\n        dbg('undos(%d) redos(%d)', hist.un.length, hist.re.length);\n\n        if (src.length === 0)\n            return false;\n\n        var patch = src.pop(),\n            applied = undiff(ref, patch),\n            cpos = patch.cpos - (patch.cdr - patch.car) + patch.txt.length,\n            reverse = diff(ref, applied.txt, cpos);\n\n        if (reverse === null)\n            return false;\n\n        dst.push(reverse);\n        ref = applied.txt;\n        ignore = true; // just some browsers\n        dom_src.value = ref;\n        dom_src.setSelectionRange(cpos, cpos);\n        ignore = true; // all browsers\n        dom_src.oninput();\n        return true;\n    }\n\n    var schedule_push = function () {\n        if (ignore) {\n            ignore = false;\n            return;\n        }\n        hist.re = [];\n        clearTimeout(sched_timer);\n        sched_cpos = dom_src.selectionEnd;\n        sched_timer = setTimeout(push, 500);\n    }\n\n    var undo = function () {\n        if (hist.re.length == 0) {\n            clearTimeout(sched_timer);\n            push();\n        }\n        return apply(hist.un, hist.re);\n    }\n\n    var redo = function () {\n        return apply(hist.re, hist.un);\n    }\n\n    var push = function () {\n        var newtxt = dom_src.value;\n        var change = diff(ref, newtxt, sched_cpos);\n        if (change !== null)\n            hist.un.push(change);\n\n        ref = newtxt;\n        dbg('undos(%d) redos(%d)', hist.un.length, hist.re.length);\n        if (hist.un.length > 0)\n            dbg(jcp(hist.un.slice(-1)[0]));\n        if (hist.re.length > 0)\n            dbg(jcp(hist.re.slice(-1)[0]));\n    }\n\n    return {\n        undo: undo,\n        redo: redo,\n        push: schedule_push,\n        _hist: hist,\n        _ref: ref\n    }\n})();\n\nJ_MD2 = 2;\n"
  },
  {
    "path": "copyparty/web/mde.css",
    "content": "html .editor-toolbar>button { margin-left: -1px; border: 1px solid rgba(0,0,0,0.1) }\nhtml .editor-toolbar>button+button { border-left: 1px solid rgba(0,0,0,0) }\nhtml .editor-toolbar>button:hover,\nhtml .editor-toolbar>button:active { box-shadow: 0 .1em .3em #999; z-index: 9 }\nhtml .editor-toolbar>button:active,\nhtml .editor-toolbar>button.active { border-color: rgba(0,0,0,0.4); background: #fc0 }\nhtml .editor-toolbar>i.separator { border-left: 1px solid #ccc; }\nhtml .editor-toolbar.disabled-for-preview>button:not(.no-disable) { opacity: .35 }\n\n\n\nhtml {\n\tline-height: 1.5em;\n}\nhtml, body {\n\tmargin: 0;\n\tpadding: 0;\n\tmin-height: 100%;\n\tfont-family: sans-serif;\n\tfont-family: var(--font-main), sans-serif;\n\tbackground: #f7f7f7;\n\tcolor: #333;\n}\n#toast {\n\tbottom: auto;\n\ttop: 1.4em;\n}\n#repl {\n\tposition: absolute;\n\ttop: 0;\n\tright: .5em;\n\tborder: none;\n\tcolor: inherit;\n\tbackground: none;\n\ttext-decoration: none;\n}\n\n\n\n#mn {\n\tfont-weight: normal;\n\tmargin: 1.3em 0 .7em 1em;\n}\n#mn a {\n\tcolor: #444;\n\tmargin: 0 0 0 -.2em;\n\tpadding: 0 0 0 .4em;\n\ttext-decoration: none;\n\t/* ie: */\n\tborder-bottom: .1em solid #777\\9;\n\tmargin-right: 1em\\9;\n}\n#mn a:first-child {\n\tpadding-left: .5em;\n}\n#mn a:last-child {\n\tpadding-right: .5em;\n}\n#mn a:not(:last-child):after {\n\tcontent: '';\n\twidth: 1.05em;\n\theight: 1.05em;\n\tmargin: -.2em .3em -.2em -.4em;\n\tdisplay: inline-block;\n\tborder: 1px solid rgba(0,0,0,0.2);\n\tborder-width: .2em .2em 0 0;\n\ttransform: rotate(45deg);\n}\n#mn a:hover {\n\tcolor: #000;\n\ttext-decoration: underline;\n}\n\nhtml .editor-toolbar>button.disabled {\n\topacity: .35;\n\tpointer-events: none;\n}\nhtml .editor-toolbar>button.save.force-save {\n\tbackground: #f97;\n}\n.CodeMirror {\n\tbackground: #f7f7f7;\n}\n\n\n\n/* darkmode */\nhtml.z .mdo,\nhtml.z .CodeMirror {\n\tborder-color: #222;\n}\nhtml.z,\nhtml.z body,\nhtml.z .CodeMirror {\n\tbackground: #222;\n\tcolor: #ccc;\n}\nhtml.z .CodeMirror-cursor {\n\tborder-color: #fff;\n}\nhtml.z .CodeMirror-selected {\n\tbox-shadow: 0 0 1px #0cf inset;\n}\nhtml.z .CodeMirror-selected,\nhtml.z .CodeMirror-selectedtext {\n\tborder-radius: .1em;\n\tbackground: #246;\n\tcolor: #fff;\n}\n\n\n\nhtml.z #mn a {\n\tcolor: #ccc;\n}\nhtml.z #mn a:not(:last-child):after {\n\tborder-color: rgba(255,255,255,0.3);\n}\nhtml.z .editor-toolbar {\n\tborder-color: #2c2c2c;\n\tbackground: #1c1c1c;\n}\nhtml.z .editor-toolbar>i.separator {\n\tborder-left: 1px solid #444;\n\tborder-right: 1px solid #111;\n}\nhtml.z .editor-toolbar>button {\n\tmargin-left: -1px; border: 1px solid rgba(255,255,255,0.1);\n\tcolor: #aaa;\n}\n\n\n\nhtml.z .editor-toolbar>button:hover {\n\tcolor: #333;\n}\nhtml.z .editor-toolbar>button.active {\n\tcolor: #333;\n\tborder-color: #ec1;\n\tbackground: #c90;\n}\nhtml.z .editor-toolbar::after,\nhtml.z .editor-toolbar::before {\n\tbackground: none;\n}\n\n\n\n/* ui.css overrides */\n.mdo {\n\tpadding: 1em;\n\tbackground: #f7f7f7;\n}\nhtml.z .mdo {\n\tbackground: #1c1c1c;\n}\n"
  },
  {
    "path": "copyparty/web/mde.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" id=\"ht_mde\">\n\n<head>\n\t<meta charset=\"utf-8\">\n\t<title>📝 {{ title }}</title>\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=0.7\">\n\t<meta name=\"theme-color\" content=\"#{{ tcolor }}\">\n\t<link rel=\"stylesheet\" href=\"{{ r }}/.cpr/w/ui.css?_={{ ts }}\">\n\t<link rel=\"stylesheet\" href=\"{{ r }}/.cpr/w/mde.css?_={{ ts }}\">\n\t<link rel=\"stylesheet\" href=\"{{ r }}/.cpr/w/deps/mini-fa.css?_={{ ts }}\">\n\t<link rel=\"stylesheet\" href=\"{{ r }}/.cpr/w/deps/easymde.css?_={{ ts }}\">\n{{ html_head }}\n</head>\n\n<body>\n\t<div id=\"mw\">\n\t\t<div id=\"mn\"></div>\n\t\t<div id=\"ml\">\n\t\t\t<div style=\"text-align:center;margin:5em 0\">\n\t\t\t\t<div style=\"font-size:2em;margin:1em 0\">Loading</div>\n\t\t\t\tif you're still reading this, check that javascript is allowed\n\t\t\t</div>\n\t\t</div>\n\t\t<div id=\"m\">\n\t\t\t<textarea id=\"mt\" style=\"display:none\" autocomplete=\"off\">{{ md }}</textarea>\n\t\t</div>\n\t</div>\n\t<a href=\"#\" id=\"repl\">π</a>\n\t<script>\n\nvar SR = \"{{ r }}\",\n\tlast_modified = {{ lastmod }},\n\thave_emp = {{ have_emp }},\n\tmd_no_br = {{ md_no_br }},\n\tdfavico = \"{{ favico }}\";\n\nvar md_opt = {\n\tlink_md_as_html: false,\n\tmodpoll_freq: {{ md_chk_rate }}\n};\n\nvar lightswitch = (function () {\n\tvar l = window.localStorage,\n\t\tdrk = (l && l.light) != 1,\n\t\tf = function (e) {\nif (e) drk = !drk;\ndocument.documentElement.className = drk? \"z\":\"y\";\ntry { l.light = drk? 0:1; } catch (ex) { }\n\t\t};\n\tf();\n\treturn f;\n})();\n\n\t</script>\n\t<script src=\"{{ r }}/.cpr/w/util.js?_={{ ts }}\"></script>\n\t<script src=\"{{ r }}/.cpr/w/deps/marked.js?_={{ ts }}\"></script>\n\t<script src=\"{{ r }}/.cpr/w/deps/easymde.js?_={{ ts }}\"></script>\n\t<script src=\"{{ r }}/.cpr/w/mde.js?_={{ ts }}\"></script>\n\t{%- if js %}\n\t<script src=\"{{ js }}_={{ ts }}\"></script>\n\t{%- endif %}\n\t<script>\n\t\tDate.now();function jsldp(a,b){2!=window[a]&&alert(\"FATAL ERROR: cannot load \"+b+\".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R\")}\n\t\tjsldp(\"J_UTL\",\"util\");\n\t\tjsldp(\"J_MDE\",\"mde\");\n\t</script>\n</body></html>\n\n"
  },
  {
    "path": "copyparty/web/mde.js",
    "content": "\"use strict\";\n\nvar J_MDE = 1;\nvar dom_wrap = ebi('mw');\nvar dom_nav = ebi('mn');\nvar dom_doc = ebi('m');\nvar dom_md = ebi('mt');\n\n(function () {\n    var n = location + '';\n    n = (n.slice(n.indexOf('//') + 2).split('?')[0] + '?v').split('/');\n    n[0] = 'top';\n    var loc = [];\n    var nav = [];\n    for (var a = 0; a < n.length; a++) {\n        if (a > 0)\n            loc.push(n[a]);\n\n        var dec = uricom_dec(n[a].split('?')[0]).replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n\n        nav.push('<a href=\"/' + loc.join('/') + '\">' + dec + '</a>');\n    }\n    dom_nav.innerHTML = nav.join('');\n})();\n\nvar mde = (function () {\n    var tbar = [\n        {\n            name: \"light\",\n            title: \"light\",\n            className: \"fa fa-lightbulb\",\n            action: lightswitch\n        }, {\n            name: \"save\",\n            title: \"save\",\n            className: \"fa fa-save\",\n            action: save\n        }, '|',\n        'bold', 'italic', 'strikethrough', 'heading', '|',\n        'code', 'quote', 'unordered-list', 'ordered-list', 'clean-block', '|',\n        'link', 'image', 'table', 'horizontal-rule', '|',\n        'preview', 'side-by-side', 'fullscreen', '|',\n        'undo', 'redo'];\n\n    var mde = new EasyMDE({\n        autoDownloadFontAwesome: false,\n        autofocus: true,\n        spellChecker: false,\n        renderingConfig: {\n            markedOptions: {\n                breaks: true,\n                gfm: true\n            }\n        },\n        shortcuts: {\n            \"save\": \"Ctrl-S\"\n        },\n        insertTexts: [\"[](\", \")\"],\n        indentWithTabs: false,\n        tabSize: 2,\n        toolbar: tbar,\n        previewClass: 'mdo',\n        onToggleFullScreen: set_jumpto,\n    });\n    md_changed(mde, true);\n    mde.codemirror.on(\"change\", function () {\n        md_changed(mde);\n    });\n    qsr('#ml');\n    return mde;\n})();\n\nfunction set_jumpto() {\n    QS('.editor-preview-side').onclick = jumpto;\n}\n\nfunction jumpto(ev) {\n    var tgt = ev.target;\n    var ln = null;\n    while (tgt && !ln) {\n        ln = tgt.getAttribute('data-ln');\n        tgt = tgt.parentElement;\n    }\n    var ln = parseInt(ln);\n    console.log(ln);\n    var cm = mde.codemirror;\n    var y = cm.heightAtLine(ln - 1, 'local');\n    var y2 = cm.heightAtLine(ln, 'local');\n    cm.scrollTo(null, y + (y2 - y) - cm.getScrollInfo().clientHeight / 2);\n}\n\nfunction md_changed(mde, on_srv) {\n    if (on_srv)\n        window.md_saved = mde.value();\n\n    var md_now = mde.value();\n    var save_btn = QS('.editor-toolbar button.save');\n\n    clmod(save_btn, 'disabled', md_now == window.md_saved);\n    set_jumpto();\n}\n\nfunction save(mde) {\n    var save_btn = QS('.editor-toolbar button.save');\n    if (clgot(save_btn, 'disabled'))\n        return toast.inf(2, 'no changes');\n\n    var force = clgot(save_btn, 'force-save');\n    function save2() {\n        var txt = mde.value();\n\n        var fd = new FormData();\n        fd.append(\"act\", \"tput\");\n        fd.append(\"lastmod\", (force ? -1 : last_modified));\n        fd.append(\"body\", txt);\n\n        var url = (location + '').split('?')[0];\n        var xhr = new XHR();\n        xhr.open('POST', url, true);\n        xhr.responseType = 'text';\n        xhr.onload = xhr.onerror = save_cb;\n        xhr.btn = save_btn;\n        xhr.mde = mde;\n        xhr.txt = txt;\n        xhr.send(fd);\n    }\n\n    if (!force)\n        save2();\n    else\n        modal.confirm('confirm that you wish to lose the changes made on the server since you opened this document', save2, function () {\n            toast.inf(3, 'aborted');\n        });\n}\n\nfunction save_cb() {\n    if (this.status !== 200)\n        return toast.err(0, 'Error!  The file was NOT saved.\\n\\nError ' + this.status + \":\\n\" + unpre(this.responseText));\n\n    var r;\n    try {\n        r = JSON.parse(this.responseText);\n    }\n    catch (ex) {\n        return toast.err(0, 'Error!  The file was likely NOT saved.\\n\\nFailed to parse reply from server:\\n\\n' + unpre(this.responseText));\n    }\n\n    if (!r.ok) {\n        if (!clgot(this.btn, 'force-save')) {\n            clmod(this.btn, 'force-save', 1);\n            var msg = [\n                'This file has been modified since you started editing it!\\n',\n                'if you really want to overwrite, press save again.\\n',\n                'modified ' + ((r.now - r.lastmod) / 1000) + ' seconds ago,',\n                ((r.lastmod - last_modified) / 1000) + ' sec after you opened it\\n',\n                last_modified + ' lastmod when you opened it,',\n                r.lastmod + ' lastmod on the server now,',\n                r.now + ' server time now,\\n',\n            ];\n            return toast.err(0, msg.join('\\n'));\n        }\n        else\n            return toast.err(0, 'Error! Save failed.  Maybe this JSON explains why:\\n\\n' + this.responseText);\n    }\n\n    clmod(this.btn, 'force-save');\n    //alert('save OK -- wrote ' + r.size + ' bytes.\\n\\nsha512: ' + r.sha512);\n\n    // download the saved doc from the server and compare\n    var url = (location + '').split('?')[0] + '?_=' + Date.now();\n    var xhr = new XHR();\n    xhr.open('GET', url, true);\n    xhr.responseType = 'text';\n    xhr.onload = xhr.onerror = save_chk;\n    xhr.btn = this.save_btn;\n    xhr.mde = this.mde;\n    xhr.txt = this.txt;\n    xhr.lastmod = r.lastmod;\n    xhr.send();\n}\n\nfunction save_chk() {\n    if (this.status !== 200)\n        return toast.err(0, 'Error!  The file was NOT saved.\\n\\nError ' + this.status + \":\\n\" + unpre(this.responseText));\n\n    var doc1 = this.txt.replace(/\\r\\n/g, \"\\n\");\n    var doc2 = this.responseText.replace(/\\r\\n/g, \"\\n\");\n    if (doc1 != doc2) {\n        modal.alert(\n            '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' +\n            'Length: yours=' + doc1.length + ', server=' + doc2.length\n        );\n        modal.alert('yours, ' + doc1.length + ' byte:\\n[' + doc1 + ']');\n        modal.alert('server, ' + doc2.length + ' byte:\\n[' + doc2 + ']');\n        return;\n    }\n\n    last_modified = this.lastmod;\n    md_changed(this.mde, true);\n\n    toast.ok(2, 'save OK' + (this.ntry ? '\\nattempt ' + this.ntry : ''));\n}\n\nJ_MDE = 2;\n"
  },
  {
    "path": "copyparty/web/msg.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" id=\"ht_msg\">\n\n<head>\n\t<meta charset=\"utf-8\">\n\t<title>{{ s_doctitle }}</title>\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=0.8\">\n\t<meta name=\"theme-color\" content=\"#{{ tcolor }}\">\n<style>:root{--font-main:sans-serif;--font-mono:monospace}\nhtml,body,a{margin:0;padding:0;border:none;color:#ccc;background:none;font-family:sans-serif;font-family:var(--font-main),sans-serif}\npre{font-family:monospace,monospace;font-family:var(--font-mono),monospace}\nhtml{touch-action:manipulation;background:#333}\n#box{padding:.5em 1em;background:#2c2c2c}\na{color:#fc5}</style>\n{{ html_head }}\n</head>\n\n<body>\n\t<div id=\"box\">\n\n\t\t{%- if h1 %}\n\t\t<h1>{{ h1 }}</h1>\n\t\t{%- endif %}\n\n\t\t{%- if h2 %}\n\t\t<h2>{{ h2 }}</h2>\n\t\t{%- endif %}\n\n\t\t{%- if p %}\n\t\t<p>{{ p }}</p>\n\t\t{%- endif %}\n\n\t\t{%- if pre %}\n\t\t<pre>{{ pre }}</pre>\n\t\t{%- endif %}\n\n\t\t{%- if html %}\n\t\t{{ html }}\n\t\t{%- endif %}\n\n\t\t{%- if click %}\n\t\t<script>document.getElementsByTagName(\"a\")[0].click()</script>\n\t\t{%- endif %}\n\t</div>\n\n\t{%- if redir %}\n\t<script>\n\t\tsetTimeout(function() {\n\t\t\tlocation.replace(\"{{ redir }}\");\n\t\t}, 800);\n\t</script>\n\t{%- endif %}\n\t{%- if js %}\n\t<script src=\"{{ js }}_={{ ts }}\"></script>\n\t{%- endif %}\n</body>\n\n</html>\n\n"
  },
  {
    "path": "copyparty/web/opds.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<feed xmlns=\"http://www.w3.org/2005/Atom\">\n    <link rel=\"search\"\n    href=\"{{ opds_osd | e }}\"\n    type=\"application/opensearchdescription+xml\"/>\n    {%- for d in dirs %}\n    <entry>\n        <title>{{ d.name | e }}</title>\n        <link rel=\"subsection\"\n        href=\"{{ d.href | e }}\"\n        type=\"application/atom+xml;profile=opds-catalog\"/>\n        <updated>{{ d.iso8601 }}</updated>\n    </entry>\n    {%- endfor %}\n    {%- for f in files %}\n    <entry>\n        <title>{{ f.name | e }}</title>\n        <updated>{{ f.iso8601 }}</updated>\n        <link rel=\"http://opds-spec.org/acquisition\"\n        href=\"{{ f.href | e }}\"\n        type=\"{{ f.mime }}\"/>\n        {%- if f.jpeg_thumb_href != None %}\n        <link rel=\"http://opds-spec.org/image/thumbnail\"\n        href=\"{{ f.jpeg_thumb_href | e }}\"\n        type=\"image/jpeg\"/>\n        {%- endif %}\n        {%- if f.jpeg_thumb_href_hires != None %}\n        <link rel=\"http://opds-spec.org/image\"\n        href=\"{{ f.jpeg_thumb_href_hires | e }}\"\n        type=\"image/jpeg\"/>\n        {%- endif %}\n    </entry>\n    {%- endfor %}\n</feed>\n"
  },
  {
    "path": "copyparty/web/opds_osd.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\">\n  <ShortName>{{ longname | truncate(16) | e }}</ShortName>\n  <Description>{{ longname | e }}</Description>\n  <Url type=\"application/atom+xml;profile=opds-catalog\" template=\"{{ search_url }}?opds&amp;q={searchTerms}\"/>\n</OpenSearchDescription>\n"
  },
  {
    "path": "copyparty/web/rups.css",
    "content": "html {\n\tcolor: #333;\n\tbackground: #f7f7f7;\n\tfont-family: sans-serif;\n\tfont-family: var(--font-main), sans-serif;\n\ttouch-action: manipulation;\n}\n#wrap {\n\tmargin: 2em auto;\n\tpadding: 0 1em 3em 1em;\n\tline-height: 2.3em;\n}\na {\n\tcolor: #047;\n\tbackground: #fff;\n\ttext-decoration: none;\n\tborder-bottom: 1px solid #8ab;\n\tborder-radius: .2em;\n\tpadding: .2em .6em;\n\tmargin: 0 .3em;\n}\n#wrap td a {\n\tmargin: 0;\n\tline-height: 1em;\n\tdisplay: inline-block;\n\twhite-space: initial;\n\tfont-family: var(--font-main), sans-serif;\n}\n#repl {\n\tborder: none;\n\tbackground: none;\n\tcolor: inherit;\n\tpadding: 0;\n\tposition: fixed;\n\tbottom: .25em;\n\tleft: .2em;\n}\n#wrap table {\n\tborder-collapse: collapse;\n\tposition: relative;\n\tmargin-top: 2em;\n}\n#wrap th {\n\ttop: -1px;\n\tposition: sticky;\n\tbackground: #f7f7f7;\n}\n#wrap td {\n\tfont-family: var(--font-mono), monospace, monospace;\n\twhite-space: pre; /*date*/\n\toverflow: hidden; /*ipv6*/\n}\n#wrap th:first-child,\n#wrap td:first-child {\n\ttext-align: right;\n}\n#wrap td,\n#wrap th {\n\ttext-align: left;\n\tpadding: .3em .6em;\n\tmax-width: 30vw;\n}\n#wrap tr:hover td {\n\tbackground: #ddd;\n\tbox-shadow: 0 -1px 0 rgba(128, 128, 128, 0.5) inset;\n}\n#wrap th:first-child,\n#wrap td:first-child {\n\tborder-radius: .5em 0 0 .5em;\n}\n#wrap th:last-child,\n#wrap td:last-child {\n\tborder-radius: 0 .5em .5em 0;\n}\n\n\n\nhtml.z {\n\tbackground: #222;\n\tcolor: #ccc;\n}\nhtml.bz {\n\tbackground: #11121d;\n\tcolor: #bbd;\n}\nhtml.z a {\n\tcolor: #fff;\n\tbackground: #057;\n\tborder-color: #37a;\n}\nhtml.z input[type=text] {\n\tcolor: #ddd;\n\tbackground: #223;\n\tborder: none;\n\tborder-bottom: 1px solid #fc5;\n\tborder-radius: .2em;\n\tpadding: .2em .3em;\n}\nhtml.z #wrap th {\n\tbackground: #222;\n}\nhtml.bz #wrap th {\n\tbackground: #223;\n}\nhtml.z #wrap tr:hover td {\n\tbackground: #000;\n}\n"
  },
  {
    "path": "copyparty/web/rups.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" id=\"ht_rup\">\n\n<head>\n\t<meta charset=\"utf-8\">\n\t<title>{{ s_doctitle }}</title>\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=0.8\">\n\t<meta name=\"robots\" content=\"noindex, nofollow\">\n\t<meta name=\"theme-color\" content=\"#{{ tcolor }}\">\n\t<link rel=\"stylesheet\" media=\"screen\" href=\"{{ r }}/.cpr/w/rups.css?_={{ ts }}\">\n\t<link rel=\"stylesheet\" media=\"screen\" href=\"{{ r }}/.cpr/w/ui.css?_={{ ts }}\">\n{{ html_head }}\n</head>\n\n<body>\n\t<div id=\"wrap\">\n        <a href=\"#\" id=\"re\">refresh</a>\n        <a href=\"{{ r }}/?h\">control-panel</a>\n        &nbsp; Filter: <input type=\"text\" id=\"filter\" size=\"20\" placeholder=\"documents/passwords\" />\n        &nbsp; <span id=\"hits\"></span>\n        <div id=\"tw\"></div>\n    </div>\n\t<a href=\"#\" id=\"repl\">π</a>\n\t<script>\n\nvar SR=\"{{ r }}\",\n\tlang=\"{{ lang }}\",\n    dutc={{ this.args.js_utc }},\n\tdfavico=\"{{ favico }}\";\n\nvar STG = window.localStorage;\ndocument.documentElement.className = (STG && STG.cpp_thm) || \"{{ this.args.theme }}\";\n\n</script>\n<script src=\"{{ r }}/.cpr/w/util.js?_={{ ts }}\"></script>\n<script>var V={{ v }};</script>\n<script src=\"{{ r }}/.cpr/w/rups.js?_={{ ts }}\"></script>\n{%- if js %}\n<script src=\"{{ js }}_={{ ts }}\"></script>\n{%- endif %}\n<script>\n\tDate.now();function jsldp(a,b){2!=window[a]&&alert(\"FATAL ERROR: cannot load \"+b+\".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R\")}\n\tjsldp(\"J_UTL\",\"util\");\n\tjsldp(\"J_RUP\",\"rups\");\n</script>\n</body>\n</html>\n\n"
  },
  {
    "path": "copyparty/web/rups.js",
    "content": "\"use strict\";\n\nvar J_RUP = 1;\n\nfunction render() {\n    var html = ['<table id=\"tab\"><thead><tr><th>size</th><th>who</th><th>ip</th><th>when</th><th>age</th><th>dir</th><th>file</th></tr></thead><tbody>'];\n    var ups = V.ups, now = V.now;\n    ebi('filter').value = V.filter;\n    ebi('hits').innerHTML = 'showing ' + ups.length + ' files';\n\n    for (var a = 0; a < ups.length; a++) {\n        var f = ups[a],\n            vsp = vsplit(f.vp.split('?')[0]),\n            dn = esc(uricom_dec(vsp[0])),\n            fn = esc(uricom_dec(vsp[1])),\n            at = f.at,\n            td = now - f.at,\n            ts = !at ? '(?)' : unix2ui(at),\n            sa = !at ? '(?)' : td > 60 ? shumantime(td) : (td + 's'),\n            sz = ('' + f.sz).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \" \");\n\n        html.push('<tr><td>' + sz +\n            '</td><td>' + (f.un || '') +\n            '</td><td>' + f.ip +\n            '</td><td>' + ts +\n            '</td><td>' + sa +\n            '</td><td><a href=\"' + vsp[0] + '\">' + dn +\n            '</a></td><td><a href=\"' + f.vp + '\">' + fn +\n            '</a></td></tr>');\n    }\n    if (!ups.length) {\n        var t = V.filter ? ' matching the filter' : '';\n        html = ['<tr><td colspan=\"6\">there are no uploads' + t + '</td></tr>'];\n    }\n    html.push('</tbody></table>');\n    ebi('tw').innerHTML = html.join('\\n');\n}\nrender();\n\nvar ti;\nfunction ask(e) {\n    ev(e);\n    clearTimeout(ti);\n    ebi('hits').innerHTML = 'Loading...';\n\n    var xhr = new XHR(),\n        filter = unsmart(ebi('filter').value);\n\n    hist_replace(get_evpath().split('?')[0] + '?ru&filter=' + uricom_enc(filter));\n\n    xhr.onload = xhr.onerror = function () {\n        try {\n            V = JSON.parse(this.responseText)\n        }\n        catch (ex) {\n            ebi('tw').innerHTML = 'failed to decode server response as json: <pre>' + esc(this.responseText) + '</pre>';\n            return;\n        }\n        render();\n    };\n    xhr.open('GET', SR + '/?ru&j&filter=' + uricom_enc(filter), true);\n    xhr.send();\n}\nebi('re').onclick = ask;\nebi('filter').oninput = function () {\n    clearTimeout(ti);\n    ti = setTimeout(ask, 500);\n    ebi('hits').innerHTML = '...';\n};\nebi('filter').onkeydown = function (e) {\n    if (('' + e.key).endsWith('Enter'))\n        ask();\n};\n\nJ_RUP = 2;\n"
  },
  {
    "path": "copyparty/web/shares.css",
    "content": "html {\n\tcolor: #333;\n\tbackground: #f7f7f7;\n\tfont-family: sans-serif;\n\tfont-family: var(--font-main), sans-serif;\n\ttouch-action: manipulation;\n}\n#wrap {\n\tmargin: 2em auto;\n\tpadding: 0 1em 3em 1em;\n\tline-height: 2.3em;\n}\n#wrap>span {\n    margin: 0 0 0 1em;\n    border-bottom: 1px solid #999;\n}\nli {\n\tmargin: 1em 0;\n}\na {\n\tcolor: #047;\n\tbackground: #fff;\n\ttext-decoration: none;\n\twhite-space: nowrap;\n\tborder-bottom: 1px solid #8ab;\n\tborder-radius: .2em;\n\tpadding: .2em .6em;\n\tmargin: 0 .3em;\n}\n#wrap td a {\n\tmargin: 0;\n}\n#w {\n\tcolor: #fff;\n\tbackground: #940;\n\tborder-color: #b70;\n}\n#repl {\n\tborder: none;\n\tbackground: none;\n\tcolor: inherit;\n\tpadding: 0;\n\tposition: fixed;\n\tbottom: .25em;\n\tleft: .2em;\n}\n#wrap table {\n\tborder-collapse: collapse;\n\tposition: relative;\n\tmargin-top: 2em;\n}\nth {\n\ttop: -1px;\n\tposition: sticky;\n    background: #f7f7f7;\n}\n#wrap td,\n#wrap th {\n\tpadding: .3em .6em;\n\ttext-align: left;\n\tvertical-align: top;\n\twhite-space: nowrap;\n}\n#wrap td+td+td+td+td+td+td+td {\n\tfont-family: var(--font-mono), monospace, monospace;\n}\n#wrap th:first-child,\n#wrap td:first-child {\n\tborder-radius: .5em 0 0 .5em;\n}\n#wrap th:last-child,\n#wrap td:last-child {\n\tborder-radius: 0 .5em .5em 0;\n}\n#wrap.terse td div {\n   \theight: 2.3em;\n\toverflow-y: hidden;\n}\n\n\nhtml.z {\n\tbackground: #222;\n\tcolor: #ccc;\n}\nhtml.z a {\n\tcolor: #fff;\n\tbackground: #057;\n\tborder-color: #37a;\n}\nhtml.z th {\n    background: #222;\n}\nhtml.bz {\n\tcolor: #bbd;\n\tbackground: #11121d;\n}\nhtml.bz th {\n\tbackground: #223;\n}\n"
  },
  {
    "path": "copyparty/web/shares.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" id=\"ht_shr\">\n\n<head>\n\t<meta charset=\"utf-8\">\n\t<title>{{ s_doctitle }}</title>\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=0.8\">\n\t<meta name=\"robots\" content=\"noindex, nofollow\">\n\t<meta name=\"theme-color\" content=\"#{{ tcolor }}\">\n\t<link rel=\"stylesheet\" media=\"screen\" href=\"{{ r }}/.cpr/w/shares.css?_={{ ts }}\">\n\t<link rel=\"stylesheet\" media=\"screen\" href=\"{{ r }}/.cpr/w/ui.css?_={{ ts }}\">\n{{ html_head }}\n</head>\n\n<body>\n\t<div id=\"wrap\" class=\"terse\">\n\t\t<a href=\"{{ r }}/?shares\">refresh</a>\n\t\t<a id=\"xpnd\" href=\"#\">files</a>\n\t\t<a href=\"{{ r }}/?h\">control-panel</a>\n\n        <span>axs = perms (read,write,get)</span>\n        <span>nf = numFiles (0=dir)</span>\n        <span>min/hrs = time left</span>\n\n        <table id=\"tab\"><thead><tr>\n            <th>sharekey</th>\n            <th>delete</th>\n            <th>pw</th>\n            <th>source</th>\n            <th>axs</th>\n            <th>nf</th>\n            <th>user</th>\n            <th>created</th>\n            <th>expires</th>\n            <th>min</th>\n            <th>hrs</th>\n            <th>add time</th>\n        </tr></thead><tbody>\n        {%- for k, pw, vp, pr, st, un, t0, t1 in rows %}\n        <tr>\n            <td>\n                <a href=\"{{ site }}{{ shr }}{{ k }}/?qr\">qr</a>\n                <a href=\"{{ r }}{{ shr }}{{ k }}/\">{{ k }}</a>\n            </td>\n            <td><a href=\"#\" k=\"{{ k }}\">delete</a></td>\n            <td>{{ \"yes\" if pw else \"--\" }}</td>\n            <td><a href=\"{{ r }}/{{ vp|e }}\">/{{ vp|e }}</a></td>\n            <td>{{ pr }}</td>\n            <td><div>{{ st }}</div></td>\n            <td>{{ un|e }}</td>\n            <td>{{ t0 }}</td>\n            <td>{{ t1 }}</td>\n            <td>{{ \"inf\" if not t1 else \"dead\" if t1 < now else ((t1 - now) / 60)   | round(1) }}</td>\n            <td>{{ \"inf\" if not t1 else \"dead\" if t1 < now else ((t1 - now) / 3600) | round(1) }}</td>\n            <td></td>\n        </tr>\n        {%- endfor %}\n        </tbody></table>\n        {%- if not rows %}\n        (you don't have any active shares btw)\n        {%- endif %}\n    </div>\n\t<a href=\"#\" id=\"repl\">π</a>\n\t<script>\n\nvar SR=\"{{ r }}\",\n    shr=\"{{ shr }}\",\n\tlang=\"{{ lang }}\",\n    dutc={{ this.args.js_utc }},\n\tdfavico=\"{{ favico }}\";\n\nvar STG = window.localStorage;\ndocument.documentElement.className = (STG && STG.cpp_thm) || \"{{ this.args.theme }}\";\n\n</script>\n<script src=\"{{ r }}/.cpr/w/util.js?_={{ ts }}\"></script>\n<script src=\"{{ r }}/.cpr/w/shares.js?_={{ ts }}\"></script>\n{%- if js %}\n<script src=\"{{ js }}_={{ ts }}\"></script>\n{%- endif %}\n<script>\n    Date.now();function jsldp(a,b){2!=window[a]&&alert(\"FATAL ERROR: cannot load \"+b+\".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R\")}\n    jsldp(\"J_UTL\",\"util\");\n    jsldp(\"J_SHR\",\"shares\");\n</script>\n</body>\n</html>\n\n"
  },
  {
    "path": "copyparty/web/shares.js",
    "content": "\"use strict\";\n\nvar J_SHR = 1;\n\nvar t = QSA('a[k]');\nfor (var a = 0; a < t.length; a++)\n    t[a].onclick = rm;\n\nfunction rm() {\n    var u = SR + '/?eshare=rm&skey=' + uricom_enc(this.getAttribute('k')),\n        xhr = new XHR();\n\n    xhr.open('POST', u, true);\n    xhr.onload = xhr.onerror = cb;\n    xhr.send();\n}\n\nfunction bump() {\n    var k = this.closest('tr').querySelector('a[k]').getAttribute('k'),\n        u = SR + '/?skey=' + uricom_enc(k) + '&eshare=' + this.value,\n        xhr = new XHR();\n\n    xhr.open('POST', u, true);\n    xhr.onload = xhr.onerror = cb;\n    xhr.send();\n}\n\nfunction cb() {\n    if (this.status !== 200)\n        return modal.alert('<h6>server error</h6>' + esc(unpre(this.responseText)));\n\n    location = '?shares';\n}\n\nebi('xpnd').onclick = function (e) {\n\tev(e);\n\tclmod(ebi('wrap'), 'terse', 't');\n};\n\nfunction qr(e) {\n    ev(e);\n    var href = this.href,\n        pw = this.closest('tr').cells[2].textContent;\n\n    if (pw.indexOf('yes') < 0)\n        return showqr(href);\n\n    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) {\n        if (v)\n            href += \"&pw=\" + v;\n        showqr(href);\n    });\n}\n\nfunction showqr(href) {\n    var vhref = href.replace('?qr&', '?').replace('?qr', '');\n    modal.alert(esc(vhref) + '<img class=\"b64\" width=\"100\" height=\"100\" src=\"' + href + '\" />');\n}\n\n(function() {\n    var tab = ebi('tab').tBodies[0],\n        tr = Array.prototype.slice.call(tab.rows, 0);\n\n    var buf = [];\n    for (var a = 0; a < tr.length; a++) {\n        var td = tr[a].cells[0],\n            sa = td.getElementsByTagName('a'),\n            h0 = sa[0].href,\n            h1 = sa[1].href;\n        sa[0].onclick = qr;\n        if (!h0.startsWith(h1)) {\n            var a2 = mknod('a', '', sa[1].innerHTML);\n            a2.href = h0.slice(0, -3);\n            sa[1].innerHTML = 'LAN';\n            td.appendChild(a2);\n        }\n        for (var b = 7; b < 9; b++)\n            buf.push(parseInt(tr[a].cells[b].innerHTML));\n    }\n\n    var ibuf = 0;\n    for (var a = 0; a < tr.length; a++)\n        for (var b = 7; b < 9; b++) {\n            var v = buf[ibuf++];\n            tr[a].cells[b].innerHTML =\n                v ? unix2ui(v).replace(' ', ',&nbsp;') : 'never';\n        }\n\n    for (var a = 0; a < tr.length; a++)\n        tr[a].cells[11].innerHTML =\n            '<button value=\"1\">1min</button> ' +\n            '<button value=\"60\">1h</button>';\n\n    var btns = QSA('td button'), aa = btns.length;\n    for (var a = 0; a < aa; a++)\n        btns[a].onclick = bump;\n})();\n\nJ_SHR = 2;\n"
  },
  {
    "path": "copyparty/web/splash.css",
    "content": "html {\n\tcolor: #333;\n\tbackground: #f7f7f7;\n\tfont-family: sans-serif;\n\tfont-family: var(--font-main), sans-serif;\n\ttouch-action: manipulation;\n}\n#wrap {\n\tmax-width: 40em;\n\tmargin: 2em auto;\n\tpadding: 0 1em 3em 1em;\n\tline-height: 1.3em;\n}\n#wrap.w {\n\tmax-width: 96%;\n}\nh1 {\n\tborder-bottom: 1px solid #ccc;\n\tmargin: 2em 0 .4em 0;\n\tpadding: 0;\n\tline-height: 1em;\n\tfont-weight: normal;\n}\nli {\n\tmargin: 1em 0;\n}\n#lo,\na {\n\tcolor: #047;\n\tbackground: #fff;\n\ttext-decoration: none;\n\twhite-space: nowrap;\n\tborder-bottom: 1px solid #8ab;\n\tborder-radius: .2em;\n\tpadding: .2em .6em;\n\tmargin: 0 .3em;\n}\ntd a {\n\tmargin: 0;\n}\n#wb,\n#w {\n\tcolor: #fff;\n\tbackground: #940;\n\tborder-color: #b70;\n}\n.af,\n.logout {\n\tfloat: right;\n\tmargin: -.2em 0 0 .8em;\n}\n#lo,\n.logout,\na.r {\n\tcolor: #c04;\n\tborder-color: #c7a;\n}\na.g {\n\tcolor: #0a0;\n\tborder-color: #3a0;\n\tbox-shadow: 0 .3em 1em #4c0;\n}\n#repl,\n#pb a {\n\tborder: none;\n\tbackground: none;\n\tcolor: inherit;\n\tpadding: 0;\n}\n#repl {\n\tposition: fixed;\n\tbottom: .25em;\n\tleft: .2em;\n}\n#pb {\n\topacity: .5;\n\tposition: fixed;\n\tbottom: .25em;\n\tright: .3em;\n}\n#pb span {\n\topacity: .6;\n}\n#pb a {\n\tmargin: 0;\n}\ntable {\n\tborder-collapse: collapse;\n}\n.vols td,\n.vols th {\n\tpadding: .3em .6em;\n\ttext-align: left;\n\twhite-space: nowrap;\n}\n.vols td:empty,\n.vols th:empty {\n\tpadding: 0;\n}\n.vols img {\n\tmargin: -4px 0;\n}\n.num {\n\tborder-right: 1px solid #bbb;\n}\n.num td {\n\tpadding: .1em .7em .1em 0;\n}\n.num td:first-child {\n\ttext-align: right;\n}\n.cn {\n\ttext-align: center;\n}\n.btns {\n\tmargin: 1em 0;\n}\n.btns>a:first-child {\n\tmargin-left: 0;\n}\n.agr br {\n\tdisplay: none;\n}\n#lo,\n.agr a,\n.agr form {\n\tmargin: 0 .5em 0 0;\n\tline-height: 4em;\n}\n.agr form,\n.agr input {\n\tdisplay: inline;\n\tpadding: 0;\n\tmargin: 0;\n}\n#lo,\n.agr input {\n\tline-height: 1em;\n\tfont-weight: normal;\n}\n#msg {\n\tmargin: 3em 0;\n}\n#msg h1 {\n\tmargin-bottom: 0;\n}\n#msg h1 + p {\n\tmargin-top: .3em;\n\ttext-align: right;\n}\nblockquote {\n\tmargin: 0 0 1.6em .6em;\n\tpadding: .7em 1em 0 1em;\n\tborder-left: .3em solid rgba(128,128,128,0.5);\n\tborder-radius: 0 0 0 .25em;\n}\npre, code {\n\tcolor: #480;\n\tbackground: #fff;\n\tfont-family: 'scp', monospace, monospace;\n\tfont-family: var(--font-mono), 'scp', monospace, monospace;\n\tborder: 1px solid rgba(128,128,128,0.3);\n\tborder-radius: .2em;\n\tpadding: .15em .2em;\n}\nhtml.z pre,\nhtml.z code {\n\tcolor: #9e0;\n\tbackground: #000;\n\tbackground: rgba(0,16,0,0.2);\n}\n.os {\n\tline-height: 1.5em;\n}\n.sph {\n\tmargin-top: 4em;\n}\n.sph code {\n\tmargin-left: .3em;\n}\npre b,\ncode b {\n\tcolor: #000;\n\tfont-weight: normal;\n\ttext-shadow: 0 0 .2em #3f3;\n\tborder-bottom: 1px solid #090;\n}\nhtml.z pre b,\nhtml.z code b {\n\tcolor: #fff;\n\tborder-bottom: 1px solid #9f9;\n}\n\n\nhtml.z {\n\tbackground: #222;\n\tcolor: #ccc;\n}\nhtml.z h1 {\n\tborder-color: #777;\n}\nhtml.z #lo,\nhtml.z a {\n\tcolor: #fff;\n\tbackground: #057;\n\tborder-color: #37a;\n}\nhtml.z .logout,\nhtml.z #lo,\nhtml.z a.r {\n\tbackground: #804;\n\tborder-color: #c28;\n}\nhtml.z a.g {\n\tbackground: #470;\n\tborder-color: #af4;\n\tbox-shadow: 0 .3em 1em #7d0;\n}\nform {\n\tline-height: 2.5em;\n}\n#x,\ninput {\n\tcolor: #a50;\n\tbackground: #fff;\n\tborder: 1px solid #a50;\n\tborder-radius: .3em;\n\tpadding: .25em .6em;\n\tmargin: 0 .3em 0 0;\n\tfont-size: 1em;\n}\ninput::placeholder {\n\tfont-size: 1.2em;\n\tfont-style: italic;\n\tletter-spacing: .04em;\n\topacity: 0.64;\n\tcolor: #930;\n}\n#x,\nhtml.z input {\n\tcolor: #fff;\n\tbackground: #626;\n\tborder-color: #c2c;\n}\nhtml.z input::placeholder {\n\tcolor: #fff;\n}\nhtml.z .num {\n\tborder-color: #777;\n}\n\n\nhtml.bz {\n\tcolor: #bbd;\n\tbackground: #11121d;\n}\nhtml.bz .vols img {\n\tfilter: sepia(0.8) hue-rotate(180deg);\n}\n"
  },
  {
    "path": "copyparty/web/splash.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" id=\"ht_spl\">\n\n<head>\n\t<meta charset=\"utf-8\">\n\t<title>{{ s_doctitle }}</title>\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=0.8\">\n\t<meta name=\"theme-color\" content=\"#{{ tcolor }}\">\n\t<link rel=\"stylesheet\" media=\"screen\" href=\"{{ r }}/.cpr/w/splash.css?_={{ ts }}\">\n\t<link rel=\"stylesheet\" media=\"screen\" href=\"{{ r }}/.cpr/w/ui.css?_={{ ts }}\">\n{{ html_head }}\n</head>\n\n<body>\n\t<div id=\"wrap\">\n\t\t{%- if not in_shr %}\n\t\t<a id=\"a\" href=\"{{ r }}/?h{{ re }}\" class=\"af\">refresh</a>\n\t\t<a id=\"v\" href=\"{{ r }}/?hc\" class=\"af\">connect</a>\n\n\t\t\t{%- if this.uname == '*' %}\n\t\t<p id=\"b\">howdy stranger &nbsp; <small>(you're not logged in)</small></p>\n\t\t\t{%- else %}\n\t\t\t\t{%- if this.args.idp_logout %}\n\t\t<a id=\"c\" href=\"{{ this.args.idp_logout }}\" class=\"logout\">logout</a>\n\t\t\t\t{%- else %}\n\t\t<a id=\"c\" href=\"{{ r }}/?pw=x\" class=\"logout\">logout</a>\n\t\t\t\t{%- endif %}\n\t\t<p><span id=\"m\">welcome back,</span> <strong id=\"un\">{{ this.uname|e }}</strong></p>\n\t\t\t{%- endif %}\n\t\t{%- endif %}\n\n\t\t{%- if msg %}\n\t\t<div id=\"msg\">\n\t\t\t{{ msg }}\n\t\t</div>\n\t\t{%- endif %}\n\n\t\t{%- if ups %}\n\t\t<h1 id=\"aa\">incoming files:</h1>\n\t\t\t<table class=\"vols\">\n\t\t\t\t<thead><tr><th>%</th><th>speed</th><th>eta</th><th>idle</th><th>dir</th><th>file</th></tr></thead>\n\t\t\t\t<tbody>\n\t\t\t\t\t{%- for u in ups %}\n\t\t\t\t\t<tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td><a href=\"{{ r }}{{ u[4] }}\">{{ u[5]|e }}</a></td><td>{{ u[6]|e }}</td></tr>\n\t\t\t\t\t{%- endfor %}\n\t\t\t\t</tbody>\n\t\t\t</table>\n\t\t{%- endif %}\n\n\t\t{%- if dls %}\n\t\t<h1 id=\"ae\">active downloads:</h1>\n\t\t\t<table class=\"vols\">\n\t\t\t\t<thead><tr><th>%</th><th>sent</th><th>speed</th><th>eta</th><th>idle</th><th></th><th>dir</th><th>file</th></tr></thead>\n\t\t\t\t<tbody>\n\t\t\t\t\t{%- for u in dls %}\n\t\t\t\t\t<tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td>{{ u[4] }}</td><td>{{ u[5] }}</td><td><a href=\"{{ r }}{{ u[6] }}\">{{ u[7]|e }}</a></td><td>{{ u[8] }}</td></tr>\n\t\t\t\t\t{%- endfor %}\n\t\t\t\t</tbody>\n\t\t\t</table>\n\t\t{%- endif %}\n\n\t\t{%- if avol %}\n\t\t<h1>admin panel:</h1>\n\t\t<table><tr><td> <!-- hehehe -->\n\t\t\t<table class=\"num\">\n\t\t\t\t<tr><td>scanning</td><td>{{ scanning }}</td></tr>\n\t\t\t\t<tr><td>hash-q</td><td>{{ hashq }}</td></tr>\n\t\t\t\t<tr><td>tag-q</td><td>{{ tagq }}</td></tr>\n\t\t\t\t<tr><td>mtp-q</td><td>{{ mtpq }}</td></tr>\n\t\t\t\t<tr><td>db-act</td><td id=\"u\">{{ dbwt }}</td></tr>\n\t\t\t</table>\n\t\t</td><td>\n\t\t\t<table class=\"vols\">\n\t\t\t\t<thead><tr><th>vol</th><th id=\"t\">action</th><th>status</th></tr></thead>\n\t\t\t\t<tbody>\n\t\t\t\t\t{%- for mp in avol %}\n\t\t\t\t\t{%- if mp in vstate and vstate[mp] %}\n\t\t\t\t\t<tr><td><a href=\"{{ r }}{{ mp }}{{ url_suf }}\">{{ mp }}</a></td><td><a class=\"s\" href=\"{{ r }}{{ mp }}?scan\">rescan</a></td><td>{{ vstate[mp] }}</td></tr>\n\t\t\t\t\t{%- endif %}\n\t\t\t\t\t{%- endfor %}\n\t\t\t\t</tbody>\n\t\t\t</table>\n\t\t</td></tr></table>\n\t\t<div class=\"btns\">\n\t\t\t<a id=\"d\" href=\"{{ r }}/?stack\">dump stack</a>\n\t\t\t<a id=\"e\" href=\"{{ r }}/?reload=cfg\">reload cfg</a>\n\t\t</div>\n\t\t{%- endif %}\n\n\t\t{%- if rvol %}\n\t\t<h1 id=\"f\">you can browse:</h1>\n\t\t<ul>\n\t\t\t{%- for mp in rvol %}\n\t\t\t<li><a href=\"{{ r }}{{ mp }}{{ url_suf }}\">{{ mp }}</a></li>\n\t\t\t{%- endfor %}\n\t\t</ul>\n\t\t{%- endif %}\n\n\t\t{%- if wvol %}\n\t\t<h1 id=\"g\">you can upload to:</h1>\n\t\t<ul>\n\t\t\t{%- for mp in wvol %}\n\t\t\t<li><a href=\"{{ r }}{{ mp }}{{ url_suf }}\">{{ mp }}</a></li>\n\t\t\t{%- endfor %}\n\t\t</ul>\n\t\t{%- endif %}\n\n\t\t{%- if in_shr %}\n\t\t<h1 id=\"z\">unlock this share:</h1>\n\t\t<div>\n\t\t\t<form id=\"lf\" method=\"post\" enctype=\"multipart/form-data\" action=\"{{ r }}/{{ qvpath }}\">\n\t\t\t\t<input type=\"hidden\" id=\"la\" name=\"act\" value=\"login\" />\n\t\t\t\t<input type=\"password\" id=\"lp\" name=\"cppwd\" placeholder=\" password\" />\n\t\t\t\t<input type=\"hidden\" name=\"uhash\" id=\"uhash\" value=\"x\" />\n\t\t\t\t<input type=\"submit\" id=\"ls\" value=\"Unlock\" />\n\t\t\t\t{%- if ahttps %}\n\t\t\t\t<a id=\"w\" href=\"{{ ahttps }}\">switch to https</a>\n\t\t\t\t{%- endif %}\n\t\t\t\t<div id=\"lm\"></div>\n\t\t\t</form>\n\t\t</div>\n\t\t{%- else %}\n\t\t\t{%- if this.uname == '*' %}\n\t\t<h1 id=\"l\">login for more:</h1>\n\t\t\t{%- else %}\n\t\t<h1 id=\"l\">change account:</h1>\n\t\t\t{%- endif %}\n\t\t<div>\n\t\t\t{%- if this.args.idp_login %}\n\t\t\t<ul><li>\n\t\t\t\t<a href=\"{{ this.args.idp_login | replace(\"{dst}\",r+\"/\"+qvpath) }}\">{{ this.args.idp_login_t }}</a>\n\t\t\t\t{%- if this.args.ao_have_pw %}or alternatively:{%- endif %}\n\t\t\t</li></ul>\n\t\t\t{%- endif %}\n\t\t\t{%- if this.args.ao_have_pw %}\n\t\t\t<form id=\"lf\" method=\"post\" enctype=\"multipart/form-data\" action=\"{{ r }}/{{ qvpath }}\">\n\t\t\t\t<input type=\"hidden\" id=\"la\" name=\"act\" value=\"login\" />\n\t\t\t\t{%- if this.args.usernames %}\n\t\t\t\t<input type=\"text\" id=\"lu\" name=\"uname\" placeholder=\" username\" size=\"12\" />\n\t\t\t\t<input type=\"password\" id=\"lp\" name=\"cppwd\" placeholder=\" password\" size=\"12\" />\n\t\t\t\t{%- else %}\n\t\t\t\t<input type=\"password\" id=\"lp\" name=\"cppwd\" placeholder=\" password\" />\n\t\t\t\t{%- endif %}\n\t\t\t\t<input type=\"hidden\" name=\"uhash\" id=\"uhash\" value=\"x\" />\n\t\t\t\t<input type=\"submit\" id=\"ls\" value=\"login\" />\n\t\t\t\t{%- if chpw %}\n\t\t\t\t<a id=\"x\" href=\"#\">change password</a>\n\t\t\t\t{%- endif %}\n\t\t\t\t{%- if ahttps %}\n\t\t\t\t<a id=\"w\" href=\"{{ ahttps }}\">switch to https</a>\n\t\t\t\t{%- endif %}\n\t\t\t\t<div id=\"lm\"></div>\n\t\t\t</form>\n\t\t\t{%- endif %}\n\t\t</div>\n\t\t{%- endif %}\n\n\t\t<h1 id=\"cc\">other stuff:</h1>\n\t\t<div class=\"agr\">\n\t\t\t{%- if ahttps %}\n\t\t\t<a id=\"wb\" href=\"{{ ahttps }}\">switch to https</a><br />\n\t\t\t{%- endif %}\n\n\t\t\t<a id=\"af\" href=\"{{ r }}/?ru\">show recent uploads</a><br />\n\n\t\t\t{%- if this.uname != '*' and this.args.shr %}\n\t\t\t<a id=\"y\" href=\"{{ r }}/?shares\">edit shares</a><br />\n\t\t\t{%- endif %}\n\n            {%- if this.uname in this.args.idp_adm_set %}\n\t\t\t<a id=\"ag\" href=\"{{ r }}/?idp\">view idp cache</a><br />\n\t\t\t{%- endif %}\n\n\t\t\t<a id=\"k\" href=\"{{ r }}/?reset\" class=\"r\" onclick=\"localStorage.clear();return true\">reset client settings</a><br />\n\n\t\t\t{%- if this.uname != '*' and not in_shr %}\n\t\t\t<form method=\"post\" enctype=\"multipart/form-data\">\n\t\t\t\t<input type=\"hidden\" name=\"act\" value=\"logout\" />\n\t\t\t\t<input type=\"submit\" id=\"lo\" value=\"logout “{{ this.uname|e }}” everywhere\" />\n\t\t\t</form>\n\t\t\t{%- endif %}\n\t\t</div>\n\t\t<ul>\n\t\t\t{%- if k304 or k304vis %}\n\t\t\t{%- if k304 %}\n\t\t\t<li><a id=\"h\" href=\"{{ r }}/?cc&setck=k304=n\">disable k304</a> (currently enabled)\n\t\t\t{%- else %}\n\t\t\t<li><a id=\"i\" href=\"{{ r }}/?cc&setck=k304=y\" class=\"r\">enable k304</a> (currently disabled)\n\t\t\t{%- endif %}\n\t\t\t<blockquote id=\"j\">enabling k304 will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general</blockquote></li>\n\t\t\t{%- endif %}\n\n\t\t\t{%- if no304 or no304vis %}\n\t\t\t{%- if no304 %}\n\t\t\t<li><a id=\"ab\" href=\"{{ r }}/?cc&setck=no304=n\">disable no304</a> (currently enabled)\n\t\t\t{%- else %}\n\t\t\t<li><a id=\"ac\" href=\"{{ r }}/?cc&setck=no304=y\" class=\"r\">enable no304</a> (currently disabled)\n\t\t\t{%- endif %}\n\t\t\t<blockquote id=\"ad\">enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!</blockquote></li>\n\t\t\t{%- endif %}\n\t\t</ul>\n\n\t</div>\n\t<a href=\"#\" id=\"repl\">π</a>\n\t{%- if not this.args.nb %}\n\t<span id=\"pb\"><span>powered by</span> <a href=\"{{ this.args.pb_url }}\">copyparty {{ver}}</a></span>\n\t{%- endif %}\n\t<script>\n\nvar SR=\"{{ r }}\",\n\tlang=\"{{ lang }}\",\n\tdfavico=\"{{ favico }}\";\n\nvar STG = window.localStorage;\ndocument.documentElement.className = (STG && STG.cpp_thm) || \"{{ this.args.theme }}\";\n\n</script>\n<script src=\"{{ r }}/.cpr/w/util.js?_={{ ts }}\"></script>\n{%- if lang != \"eng\" %}\n<script src=\"{{ r }}/.cpr/w/tl/{{ lang }}.js?_={{ ts }}\"></script>\n{%- endif %}\n<script src=\"{{ r }}/.cpr/w/splash.js?_={{ ts }}\"></script>\n{%- if js %}\n<script src=\"{{ js }}_={{ ts }}\"></script>\n{%- endif %}\n<script>\n\tDate.now();function jsldp(a,b){2!=window[a]&&alert(\"FATAL ERROR: cannot load \"+b+\".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R\")}\n\tjsldp(\"J_UTL\",\"util\");\n\tjsldp(\"J_SPL\",\"splash\");\n</script>\n</body>\n</html>\n\n"
  },
  {
    "path": "copyparty/web/splash.js",
    "content": "\"use strict\";\n\nvar J_SPL = 1;\n\nLs.eng = {\n\t\"splash\": {\n\t\t\"d2\": \"shows the state of all active threads\",\n\t\t\"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\",\n\t\t\"lo2\": \"ends the session on all browsers\",\n\t\t\"u2\": \"time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds\",\n\t\t\"v2\": \"use this server as a local HDD\",\n\t\t\"ta1\": \"fill in your new password first\",\n\t\t\"ta2\": \"repeat to confirm new password:\",\n\t\t\"ta3\": \"found a typo; please try again\",\n\t\t\"nop\": \"ERROR: Password cannot be blank\",\n\t\t\"nou\": \"ERROR: Username and/or password cannot be blank\",\n\t}\n};\n\nif (window.langmod)\n\tlangmod();\n\nvar d = (Ls[lang] || Ls.eng).splash;\nif (Ls.eng && d !== Ls.eng.splash)\n\tfor (var k in Ls.eng.splash)\n\t\tif (d[k] === undefined)\n\t\t\td[k] = Ls.eng.splash[k];\n\nd.wb = d.w;\n\nfor (var k in (d || {})) {\n\tvar f = k.slice(-1),\n\t\ti = k.slice(0, -1),\n\t\to = QSA(i.startsWith('.') ? i : '#' + i);\n\n\tfor (var a = 0; a < o.length; a++)\n\t\tif (f == 1)\n\t\t\to[a].innerHTML = d[k];\n\t\telse if (f == 2)\n\t\t\to[a].setAttribute(\"tt\", d[k]);\n\t\telse if (f == 3)\n\t\t\to[a].setAttribute(\"value\", d[k]);\n\t\telse if (f == 4)\n\t\t\to[a].setAttribute(\"placeholder\", \" \" + d[k]);\n}\nvar o1 = ebi('lo'), o2 = ebi('un');\nif (o1 && o2 && d.lo3)\n\to1.setAttribute(\"value\", d.lo3.format(o2.textContent));\n\ntry {\n\tif (is_idp > 1) {\n\t\tvar z = ['#l+div', '#l', '#c'];\n\t\tfor (var a = 0; a < z.length; a++)\n\t\t\tQS(z[a]).style.display = 'none';\n\t}\n}\ncatch (ex) { }\n\ntt.init();\nvar o = QS('input[name=\"uname\"]') || QS('input[name=\"cppwd\"]');\nif (o && !MOBILE && !ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)\n\to.focus();\n\no = ebi('u');\nif (o && /[0-9]+$/.exec(o.innerHTML))\n\to.innerHTML = shumantime(o.innerHTML);\n\no = ebi('uhash')\nif (o)\n\to.value = '' + location.hash;\n\nif (/\\&re=/.test('' + location))\n\tebi('a').className = 'af g';\n\n(function() {\n\tif (!ebi('x'))\n\t\treturn;\n\n\tvar pwi = ebi('lp');\n\n\tfunction redo(msg) {\n\t\tmodal.alert(msg, function() {\n\t\t\tpwi.value = '';\n\t\t\tpwi.focus();\n\t\t});\n\t}\n\tfunction mok(v) {\n\t\tif (v !== pwi.value)\n\t\t\treturn redo(d.ta3);\n\n\t\tpwi.setAttribute('name', 'pw');\n\t\tebi('la').value = 'chpw';\n\t\tebi('lf').submit();\n\t}\n\tfunction stars() {\n\t\tvar m = ebi('modali');\n\t\tfunction enstars(n) {\n\t\t\tsetTimeout(function() { m.value = ''; }, n);\n\t\t}\n\t\tm.setAttribute('type', 'password');\n\t\tenstars(17);\n\t\tenstars(32);\n\t\tenstars(69);\n\t}\n\tebi('x').onclick = function (e) {\n\t\tev(e);\n\t\tif (!pwi.value)\n\t\t\treturn ebi('lm').innerHTML = d.ta1;\n\n\t\tmodal.prompt(d.ta2, \"y\", mok, null, stars);\n\t};\n})();\n\nif (ebi('lf'))\n\tebi('lf').onsubmit = function() {\n\t\tvar un = ebi('lu');\n\t\tif (ebi('lp').value && (!un || un.value))\n\t\t\treturn true;\n\t\tebi('lm').innerHTML = un ? d.nou : d.nop;\n\t\treturn false;\n\t};\n\nif (ebi('lp'))\n\tebi('lp').oninput = function() {\n\t\tebi('lm').innerHTML = this.value.length <= 64 ?\n\t\t\t'' : 'ERROR: Password too long (max=64)';\n\t};\n\nJ_SPL = 2;\n"
  },
  {
    "path": "copyparty/web/svcs.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" id=\"ht_svc\">\n\n<head>\n\t<meta charset=\"utf-8\">\n\t<title>{{ s_doctitle }}</title>\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=0.8\">\n\t<meta name=\"theme-color\" content=\"#{{ tcolor }}\">\n\t<link rel=\"stylesheet\" media=\"screen\" href=\"{{ r }}/.cpr/w/splash.css?_={{ ts }}\">\n\t<link rel=\"stylesheet\" media=\"screen\" href=\"{{ r }}/.cpr/w/ui.css?_={{ ts }}\">\n\t<style>ul{padding-left:1.3em}li{margin:.4em 0}.txa{float:right;margin:0 0 0 1em}</style>\n{{ html_head }}\n</head>\n\n<body>\n\t<div id=\"wrap\" class=\"w\">\n        <div class=\"cn\">\n            <p class=\"btns\"><a href=\"/{{ rvp }}\">browse files</a> // <a href=\"{{ r }}/?h\">control panel</a></p>\n            <p>or choose your OS for cooler alternatives:</p>\n            <div class=\"ossel\">\n                <a id=\"swin\" href=\"#\">Windows</a>\n                <a id=\"slin\" href=\"#\">Linux</a>\n                <a id=\"smac\" href=\"#\">macOS</a>\n            </div>\n        </div>\n\n        <p class=\"sph\">\n            make this server appear on your computer as a regular HDD!<br />\n            pick your favorite below (sorted by performance, best first) and lets 🎉<br />\n            <br />\n            <span class=\"os win lin mac\">placeholders:</span>\n            <span class=\"os win\">\n                {% if accs %}{% if un %}<code><b id=\"un0\">{{ un }}</b></code>=username, <code><b id=\"up0\">{{ unpw }}</b></code>=username:password, {% endif %}<code><b id=\"pw0\">{{ pw }}</b></code>=password, {% endif %}<code><b>W:</b></code>=mountpoint\n            </span>\n            <span class=\"os lin mac\">\n                {% if accs %}{% if un %}<code><b id=\"un0\">{{ un }}</b></code>=username, <code><b id=\"up0\">{{ unpw }}</b></code>=username:password, {% endif %}<code><b id=\"pw0\">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint\n            </span>\n            {% if accs %}<a href=\"#\" id=\"setpw\">use real password</a>{% endif %}\n            <a href=\"#\" id=\"qr\">show qr</a>\n        </p>\n\n\n\n        {% if args.have_idp_hdrs %}\n        <p style=\"line-height:2em\"><b>WARNING:</b> 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 <a href=\"https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients\">the IdP docs</a></p>\n        {%- endif %}\n\n\n\n        {% if not args.no_dav %}\n        <h1>WebDAV</h1>\n\n        <div class=\"os win\">\n            <p>if you can, install <a href=\"https://winfsp.dev/rel/\">winfsp</a>+<a href=\"https://downloads.rclone.org/rclone-current-windows-amd64.zip\">rclone</a> and then paste this in cmd:</p>\n            <pre>\n                rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}\n                rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-dav:{{ rvp }} <b>W:</b>\n            </pre>\n            <ul>\n                {%- if s %}\n                <li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>\n                {%- endif %}\n                <li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>\n            </ul>\n\n            <p>if you want to use the native WebDAV client in windows instead (slow and buggy), first run <a href=\"{{ r }}/.cpr/a/webdav-cfg.txt?dl=webdav-cfg.bat\">webdav-cfg.bat</a> to remove the 47 MiB filesize limit (also fixes latency and password login), then connect:</p>\n            <pre>\n                {%- if un %}\n                net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} <b>{{ pw }}</b> /user:{{ b_un }}{% endif %}\n                {%- else %}\n                net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} k /user:<b>{{ pw }}</b>{% endif %}\n                {%- endif %}\n            </pre>\n        </div>\n\n        <div class=\"os lin\">\n            <p>rclone (v1.63 or later) is recommended:</p>\n            <pre>\n                rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}\n                rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b>\n            </pre>\n            <ul>\n                {%- if s %}\n                <li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>\n                {%- endif %}\n                <li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>\n                <li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>\n            </ul>\n            <p>alternatively use davfs2 (requires root, is slower, forgets lastmodified-timestamp on upload):</p>\n            <pre>\n                yum install davfs2\n                {%- if un %}\n                {% if accs %}printf '%s\\n' {{ b_un }} <b>{{ pw }}</b> | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b>\n                {%- else %}\n                {% if accs %}printf '%s\\n' <b>{{ pw }}</b> k | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b>\n                {%- endif %}\n            </pre>\n            {%- if accs %}\n            <p>make davfs2 automount on boot:</p>\n            <pre>\n                {%- if un %}\n                printf '%s\\n' \"http{{ s }}://{{ ep }}/{{ rvp }} {{ b_un }} <b>{{ pw }}</b>\" >> /etc/davfs2/secrets\n                {%- else %}\n                printf '%s\\n' \"http{{ s }}://{{ ep }}/{{ rvp }} <b>{{ pw }}</b> k\" >> /etc/davfs2/secrets\n                {%- endif %}\n                printf '%s\\n' \"http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b> davfs rw,user,uid=1000,noauto 0 0\" >> /etc/fstab\n            </pre>\n            {%- endif %}\n            <p>or the emergency alternative (gnome/gui-only):</p>\n            <!-- gnome-bug: ignores vp -->\n            <pre>\n                {%- if accs %}\n                echo <b>{{ pw }}</b> | gio mount dav{{ s }}://{{ b_un }}@{{ ep }}/{{ rvp }}\n                {%- else %}\n                gio mount -a dav{{ s }}://{{ ep }}/{{ rvp }}\n                {%- endif %}\n            </pre>\n            <p>on KDE Dolphin, use <code>webdav{{ s }}://{{ ep }}/{{ rvp }}</code></p>\n        </div>\n\n        <div class=\"os mac\">\n            <pre>\n                osascript -e ' mount volume \"http{{ s }}://{{ b_un }}:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}\" '\n            </pre>\n            <p>or you can open up a Finder, press command-K and paste this instead:</p>\n            <pre>\n                http{{ s }}://{{ b_un }}:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}\n            </pre>\n\n            {%- if s %}\n            <p><em>replace <code>https</code> with <code>http</code> if it doesn't work</em></p>\n            {%- endif %}\n        </div>\n        {%- endif %}\n\n\n\n        {% if args.ftp or args.ftps %}\n        <h1>FTP</h1>\n\n        <div class=\"os win\">\n            <p>if you can, install <a href=\"https://winfsp.dev/rel/\">winfsp</a>+<a href=\"https://downloads.rclone.org/rclone-current-windows-amd64.zip\">rclone</a> and then paste this in cmd:</p>\n            {%- if args.ftp %}\n            <p>connect with plaintext FTP:</p>\n            <pre>\n                {%- if un %}\n                rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false\n                {%- else %}\n                rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false\n                {%- endif %}\n                rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-ftp:{{ rvp }} <b>W:</b>\n            </pre>\n            {%- endif %}\n            {%- if args.ftps %}\n            <p>connect with TLS-encrypted FTPS:</p>\n            <pre>\n                {%- if un %}\n                rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true\n                {%- else %}\n                rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true\n                {%- endif %}\n                rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-ftps:{{ rvp }} <b>W:</b>\n            </pre>\n            {%- endif %}\n            <ul>\n                {%- if args.ftps %}\n                <li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>\n                {%- endif %}\n                <li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>\n            </ul>\n            <p>if you want to use the native FTP client in windows instead (please dont), press <code>win+R</code> and run this command:</p>\n            <pre>\n                {%- if un %}\n                explorer {{ \"ftp\" if args.ftp else \"ftps\" }}://{% if accs %}{{ b_un }}:<b>{{ pw }}</b>@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}\n                {%- else %}\n                explorer {{ \"ftp\" if args.ftp else \"ftps\" }}://{% if accs %}<b>{{ pw }}</b>:k@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}\n                {%- endif %}\n            </pre>\n        </div>\n\n        <div class=\"os lin\">\n            {%- if args.ftp %}\n            <p>connect with plaintext FTP:</p>\n            <pre>\n                {%- if un %}\n                rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false\n                {%- else %}\n                rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false\n                {%- endif %}\n                rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>mp</b>\n            </pre>\n            {%- endif %}\n            {%- if args.ftps %}\n            <p>connect with TLS-encrypted FTPS:</p>\n            <pre>\n                {%- if un %}\n                rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true\n                {%- else %}\n                rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true\n                {%- endif %}\n                rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>mp</b>\n            </pre>\n            {%- endif %}\n            <ul>\n                {%- if args.ftps %}\n                <li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>\n                {%- endif %}\n                <li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>\n                <li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>\n            </ul>\n            <p>emergency alternative (gnome/gui-only):</p>\n            <!-- gnome-bug: ignores vp -->\n            <pre>\n                {%- if accs %}\n                echo <b>{{ pw }}</b> | gio mount ftp{{ \"\" if args.ftp else \"s\" }}://{{ b_un }}@{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}\n                {%- else %}\n                gio mount -a ftp{{ \"\" if args.ftp else \"s\" }}://{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}\n                {%- endif %}\n            </pre>\n        </div>\n\n        <div class=\"os mac\">\n            <p>note: FTP is read-only on macos; please use WebDAV instead</p>\n            <pre>\n                open {{ \"ftp\" if args.ftp else \"ftps\" }}://{% if accs %}{{ b_un }}:<b>{{ pw }}</b>@{% else %}anonymous:@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}\n            </pre>\n        </div>\n        {%- endif %}\n\n\n\n        <h1>partyfuse</h1>\n        <p>\n            <a href=\"{{ r }}/.cpr/a/partyfuse.py\">partyfuse.py</a> -- fast, read-only,\n            needs <a href=\"{{ r }}/.cpr/w/deps/fuse.py\">fuse.py</a> in the same folder,\n            <span class=\"os win\">needs <a href=\"https://winfsp.dev/rel/\">winfsp</a></span>\n            <span class=\"os lin\">doesn't need root</span>\n        </p>\n        <pre>\n            partyfuse.py{% if accs %} -a <b>{{ unpw }}</b>{% endif %} http{{ s }}://{{ ep }}/{{ rvp }} <b><span class=\"os win\">W:</span><span class=\"os lin mac\">mp</span></b>\n        </pre>\n        {%- if s %}\n        <ul><li>if you are on LAN (or just dont have valid certificates), add <code>-td</code></li></ul>\n        {%- endif %}\n        <p>\n            you can use <a href=\"{{ r }}/.cpr/a/u2c.py\">u2c.py</a> to upload (sometimes faster than web-browsers)\n        </p>\n\n\n        {% if args.smb %}\n        <h1>SMB / CIFS</h1>\n\n        {%- if un %}\n        <h2>not available on this server because <code>--usernames</code> is enabled in the server config</h2>\n        {%- else %}\n\n        <div class=\"os win\">\n            <pre>\n                net use <b>w:</b> \\\\{{ host }}\\a{% if accs %} k /user:<b>{{ pw }}</b>{% endif %}\n            </pre>\n            <!-- rclone fails due to copyparty-smb bugs -->\n        </div>\n\n        <div class=\"os lin\">\n            <pre>\n                mount -t cifs -o{% if accs %}user=<b>{{ pw }}</b>,pass=k,{% endif %}vers={{ 1 if args.smb1 else 2 }}.0,port={{ args.smb_port }},uid=1000 //{{ host }}/a/ <b>mp</b>\n            </pre>\n            <!-- p>or the emergency alternative (gnome/gui-only):</p nevermind, only works through mdns -->\n        </div>\n\n        <pre class=\"os mac\">\n            open 'smb://<b>{{ pw }}</b>:k@{{ host }}/a'\n        </pre>\n        {%- endif %}\n        {%- endif %}\n\n\n\n        <div class=\"os win\">\n            <h1>ShareX</h1>\n\n            <p>to upload screenshots using ShareX <a href=\"https://getsharex.com/\">v15+</a>, save this as <code>copyparty.sxcu</code> and run it:</p>\n\n            <pre class=\"dl\" name=\"copyparty.sxcu\">\n                { \"Version\": \"15.0.0\", \"Name\": \"copyparty\",\n                \"RequestURL\": \"http{{ s }}://{{ ep }}/{{ rvp }}\",\n                \"Headers\": {\n                    {% if accs %}\"pw\": \"<b>{{ unpw }}</b>\", {% endif %}\"accept\": \"url\"\n                },\n                \"DestinationType\": \"ImageUploader, TextUploader, FileUploader\",\n                \"Body\": \"MultipartFormData\", \"URL\": \"{response}\",\n                \"RequestMethod\": \"POST\", \"FileFormName\": \"f\" }\n            </pre>\n\n            <p>for ShareX <a href=\"https://github.com/ShareX/ShareX/releases/tag/v12.1.1\">v12</a> specifically, save this as <code>copyparty.sxcu</code> and run it:</p>\n\n            <pre class=\"dl\" name=\"copyparty.sxcu\">\n                { \"Name\": \"copyparty\",\n                \"RequestURL\": \"http{{ s }}://{{ ep }}/{{ rvp }}\",\n                \"Headers\": {\n                    {% if accs %}\"pw\": \"<b>{{ unpw }}</b>\", {% endif %}\"accept\": \"url\"\n                },\n                \"DestinationType\": \"ImageUploader, TextUploader, FileUploader\",\n                \"FileFormName\": \"f\" }\n            </pre>\n        </div>\n\n\n\n        <div class=\"os mac\">\n            <h1>ishare</h1>\n\n            <p>to upload screenshots using <a href=\"https://isharemac.app/\">ishare</a>, save this as <code>copyparty.iscu</code> and run it:</p>\n\n            <pre class=\"dl\" name=\"copyparty.iscu\">\n                { \"Name\": \"copyparty\",\n                \"RequestURL\": \"http{{ s }}://{{ ep }}/{{ rvp }}\",\n                \"Headers\": {\n                    {%- if accs %}\n                    \"pw\": \"<b>{{ unpw }}</b>\",\n                    {%- endif %}\n                    \"accept\": \"json\"\n                },\n                \"ResponseURL\": \"{{ '{{fileurl}}' }}\",\n                \"FileFormName\": \"f\" }\n            </pre>\n        </div>\n\n\n\n        <div class=\"os lin\">\n            <h1>flameshot</h1>\n\n            <p>to upload screenshots using <a href=\"https://flameshot.org/\">flameshot</a>, save this as <code>flameshot.sh</code> and run it:</p>\n\n            <pre class=\"dl\" name=\"flameshot.sh\">\n                #!/bin/bash\n                pw=\"<b>{{ unpw }}</b>\"\n                url=\"http{{ s }}://{{ ep }}/{{ rvp }}\"\n                filename=\"$(date +%Y-%m%d-%H%M%S).png\"\n                flameshot gui -s -r | curl -sT- \"$url$filename?want=url&pw=$pw\" | xsel -ib\n            </pre>\n        </div>\n\n\n\n    </div>\n\t<a href=\"#\" id=\"repl\">π</a>\n\t<script>\n\nvar SR=\"{{ r }}\",\n    lang=\"{{ lang }}\",\n\tdfavico=\"{{ favico }}\";\n\nvar STG = window.localStorage;\ndocument.documentElement.className = (STG && STG.cpp_thm) || \"{{ args.theme }}\";\n\n</script>\n<script src=\"{{ r }}/.cpr/w/util.js?_={{ ts }}\"></script>\n<script src=\"{{ r }}/.cpr/w/svcs.js?_={{ ts }}\"></script>\n{%- if js %}\n<script src=\"{{ js }}_={{ ts }}\"></script>\n{%- endif %}\n<script>\n\tDate.now();function jsldp(a,b){2!=window[a]&&alert(\"FATAL ERROR: cannot load \"+b+\".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R\")}\n\tjsldp(\"J_UTL\",\"util\");\n\tjsldp(\"J_SVC\",\"svcs\");\n</script>\n</body>\n</html>\n\n"
  },
  {
    "path": "copyparty/web/svcs.js",
    "content": "\"use strict\";\n\nvar J_SVC = 1;\n\nvar oa = QSA('pre');\nfor (var a = 0; a < oa.length; a++) {\n    var html = oa[a].innerHTML,\n        nd = /^ +/.exec(html)[0].length,\n        rd = new RegExp('(^|\\r?\\n) {' + nd + '}', 'g');\n\n    oa[a].innerHTML = html.replace(rd, '$1').replace(/[ \\r\\n]+$/, '').replace(/\\r?\\n/g, '<br />');\n}\n\nfunction add_dls() {\n    oa = QSA('pre.dl');\n    for (var a = 0; a < oa.length; a++) {\n        var an = 'ta' + a,\n            o = ebi(an) || mknod('a', an, 'download');\n\n        oa[a].setAttribute('id', 'tx' + a);\n        oa[a].parentNode.insertBefore(o, oa[a]);\n        o.setAttribute('download', oa[a].getAttribute('name'));\n        o.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(oa[a].innerText));\n        clmod(o, 'txa', 1);\n    }\n}\nadd_dls();\n\n\noa = QSA('.ossel a');\nfor (var a = 0; a < oa.length; a++)\n    oa[a].onclick = esetos;\n\nfunction esetos(e) {\n    ev(e);\n    setos(((e && e.target) || (window.event && window.event.srcElement)).id.slice(1));\n}\n\nfunction setos(os) {\n    var oa = QSA('.os');\n    for (var a = 0; a < oa.length; a++)\n        oa[a].style.display = 'none';\n\n    var oa = QSA('.' + os);\n    for (var a = 0; a < oa.length; a++)\n        oa[a].style.display = '';\n\n    oa = QSA('.ossel a');\n    for (var a = 0; a < oa.length; a++)\n        clmod(oa[a], 'g', oa[a].id.slice(1) == os);\n}\n\nsetos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : 'idk');\n\n\nvar un, un0, pw, pw0, unpw, up0;\nfunction setpw(e) {\n    ev(e);\n    if (!ebi('un0'))\n        return askpw();\n\n    modal.prompt('username:', '', function (v) {\n        if (!v)\n            return;\n\n        un = v;\n        un0 = ebi('un0').innerHTML;\n        var oa = QSA('b');\n\n        for (var a = 0; a < oa.length; a++)\n            if (oa[a].innerHTML == un0)\n                oa[a].textContent = un;\n        \n        askpw();\n    });\n}\nfunction askpw() {\n    modal.prompt('password:', '', function (v) {\n        if (!v)\n            return;\n\n        pw = v;\n        pw0 = ebi('pw0').innerHTML;\n        var oa = QSA('b');\n\n        for (var a = 0; a < oa.length; a++)\n            if (oa[a].innerHTML == pw0)\n                oa[a].textContent = pw;\n\n        if (un) {\n            unpw = un ? (un+':'+pw) : pw;\n            up0 = ebi('up0').innerHTML;\n            for (var a = 0; a < oa.length; a++)\n                if (oa[a].innerHTML == up0)\n                    oa[a].textContent = unpw;\n        }\n        add_dls();\n    });\n}\nif (ebi('setpw'))\n    ebi('setpw').onclick = setpw;\n\n\nebi('qr').onclick = function () {\n    var url = ('' + location).split('?')[0];\n    if (pw)\n        url += '?pw=' + pw;\n    var txt = esc(url) + '<img class=\"b64\" width=\"100\" height=\"100\" src=\"' + addq(url, 'qr') + '\" />';\n    modal.alert(txt);\n};\n\nJ_SVC = 2;\n"
  },
  {
    "path": "copyparty/web/tl/chi.js",
    "content": "\n// 以 //m 结尾的行是未经验证的机器翻译\n\nLs.chi = {\n\t\"tt\": \"中文\",\n\n\t\"cols\": {\n\t\t\"c\": \"操作按钮\",\n\t\t\"dur\": \"时长\",\n\t\t\"q\": \"质量 / 比特率\",\n\t\t\"Ac\": \"音频编码\",\n\t\t\"Vc\": \"视频编码\",\n\t\t\"Fmt\": \"格式 / 容器\",\n\t\t\"Ahash\": \"音频校验和\",\n\t\t\"Vhash\": \"视频校验和\",\n\t\t\"Res\": \"分辨率\",\n\t\t\"T\": \"文件类型\",\n\t\t\"aq\": \"音频质量 / 比特率\",\n\t\t\"vq\": \"视频质量 / 比特率\",\n\t\t\"pixfmt\": \"子采样 / 像素结构\",\n\t\t\"resw\": \"水平分辨率\",\n\t\t\"resh\": \"垂直分辨率\",\n\t\t\"chs\": \"声道数\",\n\t\t\"hz\": \"采样率\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"杂项\",\n\t\t\t[\"ESC\", \"关闭各种窗口\"],\n\n\t\t\t\"文件管理器\",\n\t\t\t[\"G\", \"切换列表 / 网格视图\"],\n\t\t\t[\"T\", \"切换缩略图 / 图标\"],\n\t\t\t[\"⇧ A/D\", \"缩略图大小\"],\n\t\t\t[\"ctrl-K\", \"删除选中项\"],\n\t\t\t[\"ctrl-X\", \"剪切选中项到剪贴板\"],\n\t\t\t[\"ctrl-C\", \"复制选中项到剪贴板\"],\n\t\t\t[\"ctrl-V\", \"粘贴（移动/复制）到此处\"],\n\t\t\t[\"Y\", \"下载选中项\"],\n\t\t\t[\"F2\", \"重命名选中项\"],\n\n\t\t\t\"文件列表选择\",\n\t\t\t[\"space\", \"切换文件选择\"],\n\t\t\t[\"↑/↓\", \"移动选择光标\"],\n\t\t\t[\"ctrl ↑/↓\", \"移动光标和视图\"],\n\t\t\t[\"⇧ ↑/↓\", \"选择上一个/下一个文件\"],\n\t\t\t[\"ctrl-A\", \"选择所有文件 / 文件夹\"]\n\t\t], [\n\t\t\t\"导航\",\n\t\t\t[\"B\", \"切换面包屑导航 / 导航窗格\"],\n\t\t\t[\"I/K\", \"上一个/下一个文件夹\"],\n\t\t\t[\"M\", \"父文件夹（或折叠当前文件夹）\"],\n\t\t\t[\"V\", \"切换导航窗格显示文件夹 / 文本文件\"],\n\t\t\t[\"A/D\", \"导航窗格大小\"]\n\t\t], [\n\t\t\t\"音频播放器\",\n\t\t\t[\"J/L\", \"上一首/下一首歌曲\"],\n\t\t\t[\"U/O\", \"快退/快进 10 秒\"],\n\t\t\t[\"0..9\", \"跳转到 0%..90%\"],\n\t\t\t[\"P\",  \"播放/暂停（或者启动播放）\"],\n\t\t\t[\"S\", \"选择正在播放的歌曲\"],\n\t\t\t[\"Y\", \"下载歌曲\"]\n\t\t], [\n\t\t\t\"图片查看器\",\n\t\t\t[\"J/L, ←/→\", \"上一张/下一张图片\"],\n\t\t\t[\"Home/End\", \"第一张/最后一张图片\"],\n\t\t\t[\"F\", \"全屏\"],\n\t\t\t[\"R\", \"顺时针旋转\"],\n\t\t\t[\"⇧ R\", \"逆时针旋转\"],\n\t\t\t[\"S\", \"选择图片\"],\n\t\t\t[\"Y\", \"下载图片\"]\n\t\t], [\n\t\t\t\"视频播放器\",\n\t\t\t[\"U/O\", \"快退/快进 10 秒\"],\n\t\t\t[\"P/K/Space\", \"播放/暂停\"],\n\t\t\t[\"C\", \"继续播放下一个视频\"],\n\t\t\t[\"V\", \"循环\"],\n\t\t\t[\"M\", \"静音\"],\n\t\t\t[\"[ 和 ]\", \"设置循环区间\"]\n\t\t], [\n\t\t\t\"文本文件查看器\",\n\t\t\t[\"I/K\", \"上一个/下一个文件\"],\n\t\t\t[\"M\", \"关闭文本文件\"],\n\t\t\t[\"E\", \"编辑文本文件\"],\n\t\t\t[\"S\", \"选择文件（用于剪切/复制/重命名）\"],\n\t\t\t[\"Y\", \"下载文本文件\"],\n\t\t\t[\"⇧ J\", \"美化 json\"],\n\t\t]\n\t],\n\n\t\"m_ok\": \"确定\",\n\t\"m_ng\": \"取消\",\n\n\t\"enable\": \"启用\",\n\t\"danger\": \"危险\",\n\t\"clipped\": \"已复制到剪贴板\",\n\n\t\"ht_s1\": \"秒\",\n\t\"ht_s2\": \"秒\",\n\t\"ht_m1\": \"分钟\",\n\t\"ht_m2\": \"分钟\",\n\t\"ht_h1\": \"小时\",\n\t\"ht_h2\": \"小时\",\n\t\"ht_d1\": \"天\",\n\t\"ht_d2\": \"天\",\n\t\"ht_and\": \"又 \",\n\n\t\"goh\": \"控制面板\",\n\t\"gop\": '上一个同级文件夹\">前',\n\t\"gou\": '上一级文件夹\">上',\n\t\"gon\": '下一个文件夹\">后',\n\t\"logout\": \"登出 \",\n\t\"login\": \"登录\",\n\t\"access\": \" 权限\",\n\t\"ot_close\": \"关闭子菜单\",\n\t\"ot_search\": \"`按属性、路径/文件名、音乐标签或以上条件的任意组合搜索文件$N$N`foo bar` = 必须包含 «foo» 和 «bar»，$N`foo -bar` = 包含 «foo» 而不包含 «bar»，$N`^yana .opus$` = 以 «yama» 为开头的 «opus» 文件$N`&quot;try unite&quot;` = 精确包含 «try unite»$N$N时间格式为 iso-8601，例如：$N`2009-12-31`、`2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"取消发布：删除最近上传的内容，或中止未完成的上传\",\n\t\"ot_bup\": \"bup：基础上传器，连 netscape 4.0 都支持\",\n\t\"ot_mkdir\": \"mkdir：创建新目录\",\n\t\"ot_md\": \"new-file：创建新文本文件\",\n\t\"ot_msg\": \"msg：发送一条会输出到服务器日志中的消息\",\n\t\"ot_mp\": \"媒体播放器选项\",\n\t\"ot_cfg\": \"配置选项\",\n\t\"ot_u2i\": 'up2k：上传文件（如果你有写入权限），或切换到搜索模式来判断文件是否存在于服务器上$N$N支持断点续传，多线程上传，保留文件时间戳，但比 [🎈]&nbsp;（基础上传器）更占 CPU<br /><br />上传过程中，此图标会变成进度条！',\n\t\"ot_u2w\": 'up2k：上传文件，支持断点续传（关闭浏览器后，再次上传同一文件即可续传）$N$N支持多线程上传，保留文件时间戳，但比 [🎈]&nbsp; （基础上传器）更占 CPU<br /><br />上传过程中，此图标会变成进度条！',\n\t\"ot_noie\": '请使用 Chrome / Firefox / Edge',\n\n\t\"ab_mkdir\": \"新建文件夹\",\n\t\"ab_mkdoc\": \"新建文本文件\",\n\t\"ab_msg\": \"发送消息到服务器日志\",\n\n\t\"ay_path\": \"跳转到文件夹\",\n\t\"ay_files\": \"跳转到文件\",\n\n\t\"wt_ren\": \"重命名选中的项目$N快捷键: F2\",\n\t\"wt_del\": \"删除选中的项目$N快捷键: ctrl-K\",\n\t\"wt_cut\": \"剪切选中的项目到剪贴板&lt;small&gt;（然后粘贴到其他地方）&lt;/small&gt;$N快捷键: ctrl-X\",\n\t\"wt_cpy\": \"将选中的项目复制到剪贴板&lt;small&gt;（用于粘贴到其他地方）&lt;/small&gt;$N快捷键: ctrl-C\",\n\t\"wt_pst\": \"粘贴之前剪切/复制的选择$N快捷键: ctrl-V\",\n\t\"wt_selall\": \"选择所有文件$N快捷键: ctrl-A（当焦点在文件上时）\",\n\t\"wt_selinv\": \"反选\",\n\t\"wt_zip1\": \"将此文件夹下载为压缩包\",\n\t\"wt_selzip\": \"将选中项下载为压缩包\",\n\t\"wt_seldl\": \"将选中项下载为单独的文件$N快捷键: Y\",\n\t\"wt_npirc\": \"复制 IRC 格式的曲目信息\",\n\t\"wt_nptxt\": \"复制纯文本格式的曲目信息\",\n\t\"wt_m3ua\": \"添加到 m3u 播放列表（加好后点击 <code>📻copy</code>）\",\n\t\"wt_m3uc\": \"复制 m3u 播放列表到剪贴板\",\n\t\"wt_grid\": \"切换网格/列表视图$N快捷键: G\",\n\t\"wt_prev\": \"上一曲$N快捷键: J\",\n\t\"wt_play\": \"播放/暂停$N快捷键: P\",\n\t\"wt_next\": \"下一曲$N快捷键: L\",\n\n\t\"ul_par\": \"并行上传：\",\n\t\"ut_rand\": \"随机化文件名\",\n\t\"ut_u2ts\": \"将最后修改的时间戳$N从你的文件系统复制到服务器\\\">📅\",\n\t\"ut_ow\": \"覆盖服务器上的现有文件？$N🛡️：不要覆盖（会生成新文件名）$N🕒：如果服务器文件较旧则覆盖$N♻️：只要文件内容不同就覆盖$N⏭️：无条件跳过所有已有文件\",\n\t\"ut_mt\": \"在上传时继续哈希其他文件$N$N如果你的 CPU 或硬盘是瓶颈，可能需要禁用\",\n\t\"ut_ask\": '上传开始前询问确认\">💭',\n\t\"ut_pot\": \"通过简化界面来$N提高慢设备上的上传速度\",\n\t\"ut_srch\": \"不会真的上传，而是检查文件是否$N已经存在于服务器上（将扫描你可以读取的所有文件夹）\",\n\t\"ut_par\": \"设置为 0 可暂停上传$N$N如果你的网络很慢/延迟很高，请增加该值$N$N在局域网内/瓶颈在服务器硬盘时，请保持该值为 1\",\n\t\"ul_btn\": \"将文件/文件夹拖放到这里（或点击我）\",\n\t\"ul_btnu\": \"上 传\",\n\t\"ul_btns\": \"搜 索\",\n\n\t\"ul_hash\": \"哈希\",\n\t\"ul_send\": \"发送\",\n\t\"ul_done\": \"完成\",\n\t\"ul_idle1\": \"没有排队的上传任务\",\n\t\"ut_etah\": \"平均 &lt;em&gt;哈希&lt;/em&gt; 速度和估计完成时间\",\n\t\"ut_etau\": \"平均 &lt;em&gt;上传&lt;/em&gt; 速度和估计完成时间\",\n\t\"ut_etat\": \"平均 &lt;em&gt;总&lt;/em&gt; 速度和估计完成时间\",\n\n\t\"uct_ok\": \"成功完成\",\n\t\"uct_ng\": \"有问题：失败/拒绝/未找到\",\n\t\"uct_done\": \"成功+失败\",\n\t\"uct_bz\": \"正在哈希或上传\",\n\t\"uct_q\": \"空闲，待处理\",\n\n\t\"utl_name\": \"文件名\",\n\t\"utl_ulist\": \"列出\",\n\t\"utl_ucopy\": \"复制\",\n\t\"utl_links\": \"链接\",\n\t\"utl_stat\": \"状态\",\n\t\"utl_prog\": \"进度\",\n\n\t// 保持简短:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"错误\",\n\t\"utl_oserr\": \"OS错误\",\n\t\"utl_found\": \"已找到\",\n\t\"utl_defer\": \"延期\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"完成\",\n\n\t\"ul_flagblk\": \"文件已添加到队列</b><br>但别的标签页里已经有一个 up2k 实例在运行了，<br>所以要先等待那边上传完成\",\n\t\"ul_btnlk\": \"服务器配置已将此开关锁定为当前状态\",\n\n\t\"udt_up\": \"上传\",\n\t\"udt_srch\": \"搜索\",\n\t\"udt_drop\": \"将文件拖放到这里\",\n\n\t\"u_nav_m\": '<h6>好，你要传什么？</h6><code>Enter</code> = 文件（不管有几个）\\n<code>ESC</code> = 一个文件夹（包括子文件夹）',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">文件</a><a href=\"#\" id=\"modal-ng\">一个文件夹</a>',\n\n\t\"cl_opts\": \"开关选项\",\n\t\"cl_hfsz\": \"文件大小格式\",\n\t\"cl_themes\": \"主题\",\n\t\"cl_langs\": \"语言\",\n\t\"cl_ziptype\": \"打包下载\",\n\t\"cl_uopts\": \"up2k 开关\",\n\t\"cl_favico\": \"网站图标\",\n\t\"cl_bigdir\": \"大目录\",\n\t\"cl_hsort\": \"排序依据数\",\n\t\"cl_keytype\": \"调式记法\",\n\t\"cl_hiddenc\": \"隐藏列\",\n\t\"cl_hidec\": \"隐藏\",\n\t\"cl_reset\": \"重置\",\n\t\"cl_hpick\": \"在下方文件列表中点击某列表头即可从表中隐去该列\",\n\t\"cl_hcancel\": \"列隐藏操作已中止\",\n\t\"cl_rcm\": \"右键菜单\",\n\n\t\"ct_grid\": '田 网格',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ 提示',\n\t\"ct_thumb\": '在网格视图中，切换图标或缩略图$N快捷键: T\">🖼️ 缩略',\n\t\"ct_csel\": '在网格视图中，允许使用 CTRL 和 SHIFT 进行文件选择\">选择',\n\t\"ct_dsel\": '在网格视图中，允许拖动选择\">拖选',\n\t\"ct_dl\": '点击文件时强制下载（不要就地显示）\">下载',\n\t\"ct_ihop\": '当图像查看器关闭时，滚动到刚才查看的文件\">回跳⮯',\n\t\"ct_dots\": '显示隐藏文件（如果服务器允许）\">显隐',\n\t\"ct_qdel\": '删除文件时，只需确认一次\">快删',\n\t\"ct_dir1st\": '把文件夹排在文件之前\">📁 在前',\n\t\"ct_nsort\": '按自然数顺序排列含有数字的文件名\">数顺',\n\t\"ct_utc\": '按协调世界时显示所有时间\">UTC',\n\t\"ct_readme\": '在文件夹列表中显示 README.md\">📜 自述',\n\t\"ct_idxh\": '如有 index.html 则显示之，而非显示文件列表\">网页',\n\t\"ct_sbars\": '显示滚动条\">⟊',\n\n\t\"cut_umod\": \"如果文件已存在于服务器上，将服务器的最后修改时间戳更新为你本地的时间戳（需要写入和删除权限）\\\">刷📅\",\n\n\t\"cut_turbo\": \"yolo 按钮，不作死就不会死，谨慎使用，后果自负！$N$N如果你上传大量文件到一半，由于某些原因不得不重新启动，$N然后想立刻开始继续上传，则可开启此“超频”（turbo）选项$N$N开启后，哈希校验将被简单的 <em>“服务器上的文件大小是否相同？”</em> 取代，$N因此内容不同但大小一致的文件将不会被上传$N$N你应该在上传完成后关闭此选项，$N然后重新“上传”同一批文件以供客户端校验\\\">超频\", // turbo button 指 386 时代机箱上的降频按钮。此处 YOLO 选译为“作死”。\n\n\t\"cut_datechk\": \"只在启用超频（turbo）时有效$N$N略微降低 yolo / 作死程度：$N检查服务器上的文件时间戳是否与你的一致$N$N<em>理论上</em> 应该能发现大部分未上传完成/损坏的文件，$N但不能取代之后禁用超频再次校验\\\">戳检\",\n\n\t\"cut_u2sz\": \"每个上传块的大小（以 MiB 为单位）；超远距离（如跨洋）传输用较大的值效果更好。网络非常不稳定时可尝试改小\",\n\n\t\"cut_flag\": \"确保一次只有一个标签页在上传$N——其他标签页也必须启用此选项$N——仅影响同一域名下的标签页\",\n\n\t\"cut_az\": \"按字母顺序上传文件，而不是按最小文件优先$N$N按字母顺序可以更容易地查看服务器上是否出现了问题，但在光纤/局域网上传稍微慢一些\",\n\n\t\"cut_nag\": \"上传完成时弹出操作系统通知$N（仅当浏览器或标签页在后台时）\",\n\t\"cut_sfx\": \"上传完成时发出声音提醒$N（仅当浏览器或标签页在后台时）\",\n\n\t\"cut_mt\": \"使用多线程加速文件哈希$N$N使用 web worker，占更多内存（最多多 512 MiB）$N$N能使上传速度在 https 下快 30%，http 下快 4.5 倍\\\">多线\",\n\n\t\"cut_wasm\": \"用 wasm 代替浏览器内置的哈希功能；可以加快 chrome 系浏览器上的速度，但会增加 CPU 占用，而且许多旧版本的 chrome 存在漏洞，启用此功能会导致浏览器耗光内存然后崩溃\\\">wasm\",\n\n\t\"cft_text\": \"网站图标文本（留空并刷新以禁用）\",\n\t\"cft_fg\": \"前景色\",\n\t\"cft_bg\": \"背景色\",\n\n\t\"cdt_lim\": \"文件夹中显示的最大文件数\",\n\t\"cdt_ask\": \"滚动到底部时，$N不会自动加载更多文件，$N而是询问要执行什么操作\",\n\t\"cdt_hsort\": \"`要包含在媒体 URL 中的排序依据（`,sorthref`）个数。设置为 0 时，也会忽略点击媒体链接时其中包含的排序规则\",\n\t\"cdt_ren\": \"启用自定义右键菜单，按住 shift 键并右键单击仍可打开浏览器菜单\\\">启用\",\n\t\"cdt_rdb\": \"当自定义右键菜单已打开并再次右键点击时显示浏览器右键菜单\\\">双击\",\n\n\t\"tt_entree\": \"显示导航面板（目录树侧边栏）$N快捷键: B\",\n\t\"tt_detree\": \"显示面包屑导航$N快捷键: B\",\n\t\"tt_visdir\": \"滚动到选定的文件夹\",\n\t\"tt_ftree\": \"切换文件夹树 / 文本文件$N快捷键: V\",\n\t\"tt_pdock\": \"用停靠在顶部的窗格显示父文件夹\",\n\t\"tt_dynt\": \"自动随着目录树展开而变宽\",\n\t\"tt_wrap\": \"自动换行\",\n\t\"tt_hover\": \"悬停时完整显示出写不下的文字$N（启用后，鼠标光标只有$N&nbsp; 位于左边线上才滚得动）\",\n\n\t\"ml_pmode\": \"文件夹播完后\",\n\t\"ml_btns\": \"命令\",\n\t\"ml_tcode\": \"转码\",\n\t\"ml_tcode2\": \"转为\",\n\t\"ml_tint\": \"不透明度\",\n\t\"ml_eq\": \"音频均衡器\",\n\t\"ml_drc\": \"动态范围压缩器\",\n\t\"ml_ss\": \"无声段自动快进\",\n\n\t\"mt_loop\": \"单曲循环\\\">🔁\",\n\t\"mt_one\": \"播完一首歌曲后停止\\\">1️⃣\",\n\t\"mt_shuf\": \"随机播放各文件夹中的歌曲\\\">🔀\",\n\t\"mt_aplay\": \"如果链接中有歌曲 ID，则自动播放$N$N禁用此选项将不再在播放音乐时更新页面 URL 中的歌曲 ID，以防止设置丢失但 URL 保留时又自动播放起来\\\">自▶\",\n\t\"mt_preload\": \"在歌曲快结束时开始加载下一首歌，以实现无缝播放\\\">预载\",\n\t\"mt_prescan\": \"在最后一首歌结束之前自动跳转到下一个文件夹$N以防止浏览器换页时停止播放\\\">预扫\",\n\t\"mt_fullpre\": \"尝试预加载整首歌；$N✅ 网络环境<b>不稳定</b>时启用，$N❌ 网络慢时大概应该<b>禁用</b>\\\">全载\",\n\t\"mt_fau\": \"在手机上，即使下一首歌预加载得不够快，也不要停止播放（可能导致标签显示异常）\\\">☕️\",\n\t\"mt_waves\": \"波形进度条：$N在进度条上显示音频振幅\\\">波形\",\n\t\"mt_npclip\": \"显示复制当前播放歌曲信息的按钮（IRC 格式或纯文本格式）\\\">/正播\",\n\t\"mt_m3u_c\": \"显示将所选歌曲复制为$Nm3u8 播放列表的按钮\\\">📻\",\n\t\"mt_octl\": \"启用操作系统集成（媒体快捷键 / osd）\\\">统控\",\n\t\"mt_oseek\": \"允许通过操作系统集成界面快进快退$N$N注意：在某些设备（如 iPhone）上，$N这会替换掉下一首按钮\\\">进退\",\n\t\"mt_oscv\": \"在系统 osd 中显示专辑封面\\\">封面\",\n\t\"mt_follow\": \"切换歌曲时，将正在播放的曲目滚动到视野内\\\">🎯\",\n\t\"mt_compact\": \"紧凑的控制按钮\\\">⟎\",\n\t\"mt_uncache\": \"清除缓存&nbsp;$N（如果你的浏览器因缓存歌曲损坏而无法播放，请尝试此操作）\\\">清缓\",\n\t\"mt_mloop\": \"循环播放当前播放中的文件夹\\\">🔁 循环\",\n\t\"mt_mnext\": \"加载下一个文件夹并继续播放\\\">📂 继续\",\n\t\"mt_mstop\": \"停止播放\\\">⏸ 停止\",\n\t\"mt_cflac\": \"将 flac / wav 转换为 {0}\\\">flac\",\n\t\"mt_caac\": \"将 aac / m4a 转换为 {0}\\\">aac\",\n\t\"mt_coth\": \"将其他（非 mp3）文件转换为 {0}\\\">其他\",\n\t\"mt_c2opus\": \"适合桌面电脑、笔记本电脑和 android 设备的最佳选择\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba（适用于 iOS 17.5 及更新版本）\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf（适用于 iOS 11 到 17）\\\">caf\",\n\t\"mt_c2mp3\": \"适用于非常旧的设备\\\">mp3\",\n\t\"mt_c2flac\": \"音质最好，但很耗流量\\\">flac\",\n\t\"mt_c2wav\": \"无压缩播放（巨大无比）\\\">wav\",\n\t\"mt_c2ok\": \"不错的选择！\",\n\t\"mt_c2nd\": \"这不是你的设备推荐的输出格式，但应该没问题\",\n\t\"mt_c2ng\": \"你的设备似乎不支持此输出格式，不过还是先试试再说\",\n\t\"mt_xowa\": \"iOS 系统仍存在无法后台播放此格式的 bug，请改用 caf 或 mp3 格式\",\n\t\"mt_tint\": \"进度条未缓冲部分的不透明度（0-100）$N用以减轻缓冲造成的视觉干扰\",\n\t\"mt_eq\": \"`启用均衡器和增益控制；$N$Nboost `0` = 标准 100% 音量（无变化）$N$Nwidth `1 &nbsp;` = 标准立体声（无变化）$Nwidth `0.5` = 50% 左右声道交叉馈送（crossfeed）$Nwidth `0 &nbsp;` = 单声道$N$Nboost `-0.8` + width `10` = 消除人声 :^)$N$N启用均衡器可使无缝专辑完全无缝连播，所以如果你不要均衡但想要无缝，请保持均衡器开启，并将所有值设为零（除了 width = 1）\",\n\t\"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（请参考维基百科，比我讲得清楚多了）\",\n\t\"mt_ss\": \"`自动跳过无声部分：当音量低于 `音量`，且播放进度处于曲目前 `开头`% 或后 `结尾`% 时，以 `倍速` 倍速播放\",\n\t\"mt_ssvt\": \"音量阈值（0-255）\\\">音量\",\n\t\"mt_ssts\": \"进度阈值（在歌曲开头百分之多少启用此功能）\\\">开头\",\n\t\"mt_sste\": \"进度阈值（在歌曲结尾百分之多少启用此功能）\\\">结尾\",\n\t\"mt_ssrt\": \"音量/速度渐入/渐出用时\\\">渐变\",\n\t\"mt_sssm\": \"播放速度倍率（范围：0.15 到 8）\\\">倍速\",\n\n\t\"mb_play\": \"播放\",\n\t\"mm_hashplay\": \"播放这个音频文件？\",\n\t\"mm_m3u\": \"按 <code>Enter/确定</code> 播放\\n按 <code>ESC/取消</code> 编辑\",\n\t\"mp_breq\": \"需要 Firefox 82+ 或 Chrome 73+ 或 iOS 15+\",\n\t\"mm_bload\": \"正在加载...\",\n\t\"mm_bconv\": \"正在转换为 {0}，请稍等...\",\n\t\"mm_opusen\": \"你的浏览器无法播放 aac / m4a 文件，\\n已启用转码为 opus\",\n\t\"mm_playerr\": \"播放失败：\",\n\t\"mm_eabrt\": \"播放的尝试已取消\",\n\t\"mm_enet\": \"你的网络连接不稳定\",\n\t\"mm_edec\": \"这个文件好像已损坏？？\",\n\t\"mm_esupp\": \"你的浏览器不支持这个音频格式\",\n\t\"mm_eunk\": \"未知错误\",\n\t\"mm_e404\": \"无法播放音频；错误 404：文件未找到。\",\n\t\"mm_e403\": \"无法播放音频；错误 403：访问被拒绝。\\n\\n尝试按 F5 刷新，也许你已被注销\",\n\t\"mm_e415\": \"无法播放音频；错误 415：文件转码失败，请检查服务器日志。\",\n\t\"mm_e500\": \"无法播放音频；错误 500：请检查服务器日志。\",\n\t\"mm_e5xx\": \"无法播放音频；服务器错误\",\n\t\"mm_nof\": \"附近找不到更多音频文件\",\n\t\"mm_prescan\": \"正在寻找下一首音乐...\",\n\t\"mm_scank\": \"已找到下一首歌：\",\n\t\"mm_uncache\": \"缓存已清除，所有歌曲将在下次播放时重新下载\",\n\t\"mm_hnf\": \"那首歌已经没了\",\n\n\t\"im_hnf\": \"那张图片已经没了\",\n\n\t\"f_empty\": '该文件夹为空',\n\t\"f_chide\": '即将隐藏 «{0}» 一列\\n\\n你可以在设置选项卡中恢复各列的显示',\n\t\"f_bigtxt\": \"这个文件大小为 {0} MiB——真的要以文本形式查看吗？\",\n\t\"f_bigtxt2\": \"是否查看文件的结尾部分？这也将启用实时跟踪（tail）功能，实时显示新加的文本行。\",\n\t\"fbd_more\": '<div id=\"blazy\">已显示 <code>{1}</code> 个文件中的 <code>{0}</code> 个，<a href=\"#\" id=\"bd_more\">显示 {2} 个</a> 或 <a href=\"#\" id=\"bd_all\">显示全部</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\">已显示 <code>{1}</code> 个文件中的 <code>{0}</code> 个，<a href=\"#\" id=\"bd_all\">显示全部</a></div>',\n\t\"f_anota\": \"仅选择了 {1} 个项目中的 {0} 个；\\n要选择整个文件夹，请先滚动到底\",\n\n\t\"f_dls\": '当前文件夹中的文件链接已\\n更改为下载链接',\n\t\"f_dl_nd\": '已跳过文件夹（请改去下载 zip/tar）：\\n',\n\n\t\"f_partial\": \"要安全下载正在上传的文件，请点击没有 <code>.PARTIAL</code> 文件扩展名的同名文件。请按取消或 Escape 执行此操作。\\n\\n按 确定 / Enter 将忽略此警告并继续下载 <code>.PARTIAL</code> 临时文件，这几乎肯定会导致数据损坏。\",\n\n\t\"ft_paste\": \"粘贴 {0} 项$N快捷键: ctrl-V\",\n\t\"fr_eperm\": '无法重命名：\\n你在此文件夹中没有 “移动” 权限',\n\t\"fd_eperm\": '无法删除：\\n你在此文件夹中没有 “删除” 权限',\n\t\"fc_eperm\": '无法剪切：\\n你在此文件夹中没有 “移动” 权限',\n\t\"fp_eperm\": '无法粘贴：\\n你在此文件夹中没有 “写入” 权限',\n\t\"fr_emore\": \"请选择至少一个要重命名的项目\",\n\t\"fd_emore\": \"请选择至少一个要删除的项目\",\n\t\"fc_emore\": \"请选择至少一个要剪切的项目\",\n\t\"fcp_emore\": \"请选择至少一个要复制到剪贴板的项目\",\n\n\t\"fs_sc\": \"分享你所在的文件夹\",\n\t\"fs_ss\": \"分享选定的文件\",\n\t\"fs_just1d\": \"你不能同时选择多个文件夹，\\n也不能同时选择文件夹和文件\",\n\t\"fs_abrt\": \"❌ 取消\",\n\t\"fs_rand\": \"🎲 随机名称\",\n\t\"fs_go\": \"✅ 创建分享链接\",\n\t\"fs_name\": \"名称\",\n\t\"fs_src\": \"源\",\n\t\"fs_pwd\": \"密码\",\n\t\"fs_exp\": \"过期\",\n\t\"fs_tmin\": \"分钟\",\n\t\"fs_thrs\": \"小时\",\n\t\"fs_tdays\": \"天\",\n\t\"fs_never\": \"永久\",\n\t\"fs_pname\": \"链接名称可留空，留空则随机生成\",\n\t\"fs_tsrc\": \"要分享的文件或文件夹\",\n\t\"fs_ppwd\": \"密码可留空\",\n\t\"fs_w8\": \"正在创建分享链接...\",\n\t\"fs_ok\": \"按 <code>Enter/确定</code> 复制到剪贴板\\n按 <code>ESC/取消</code> 关闭\",\n\n\t\"frt_dec\": \"可能可以修复一些文件名损坏的情况\\\">url-decode\",\n\t\"frt_rst\": \"将修改后的文件名重置为原始文件名\\\">↺ 重置\",\n\t\"frt_abrt\": \"中止并关闭此窗口\\\">❌ 取消\",\n\t\"frb_apply\": \"应用重命名\",\n\t\"fr_adv\": \"批量 / 元数据 / 正则表达式重命名\\\">高级\",\n\t\"fr_case\": \"正则表达式区分大小写\\\">大小写\",\n\t\"fr_win\": \"兼容 Windows 文件名，将 <code>&lt;&gt;:&quot;\\\\|?*</code> 替换为中文全角字符\\\">win\",\n\t\"fr_slash\": \"把 <code>/</code> 换掉，以免创建新文件夹\\\">不要 /\",\n\t\"fr_re\": \"`要在原始文件名上匹配的正则表达式；可在下面的 format 字段中按`(1)`、`(2)`的格式使用捕获组\",\n\t\"fr_fmt\": \"`借鉴 foobar2000：$N`(title)` 替换为歌曲名称，$N`[(artist) - ](title)` 当歌曲艺术家为空时省略[方括号]中的内容$N`$lpad((tn),2,0)` 将曲目编号补到 2 位数\",\n\t\"fr_pdel\": \"删除\",\n\t\"fr_pnew\": \"另存为\",\n\t\"fr_pname\": \"为你的新预设命名\",\n\t\"fr_aborted\": \"已中止\",\n\t\"fr_lold\": \"原文件名\",\n\t\"fr_lnew\": \"新文件名\",\n\t\"fr_tags\": \"选定文件的标签（只读，仅供参考）：\",\n\t\"fr_busy\": \"正在重命名 {0} 个项目...\\n\\n{1}\",\n\t\"fr_efail\": \"重命名失败：\\n\",\n\t\"fr_nchg\": \"{0} 个新文件名由于 <code>win</code> 和/或 <code>不要 /</code> 被更改\\n\\n确定继续使用这些有变的新文件名？\",\n\n\t\"fd_ok\": \"删除成功\",\n\t\"fd_err\": \"删除失败：\\n\",\n\t\"fd_none\": \"没有文件被删除；可能被服务器配置（xbd）阻止？\",\n\t\"fd_busy\": \"正在删除 {0} 项...\\n\\n{1}\",\n\t\"fd_warn1\": \"删除这 {0} 项？\",\n\t\"fd_warn2\": \"<b>最后一次确认！</b> 无法撤销。要删除吗？\",\n\n\t\"fc_ok\": \"已剪切 {0} 个项目\",\n\t\"fc_warn\": '已剪切 {0} 个项目\\n\\n但：只有<b>当前</b>浏览器标签页可以粘贴它们\\n（因为选得实在太多了）',\n\n\t\"fcc_ok\": \"已将 {0} 个项目复制到剪贴板\",\n\t\"fcc_warn\": '已将 {0} 个项目复制到剪贴板\\n\\n但：只有<b>当前</b>浏览器标签页可以粘贴它们\\n（因为选得实在太多了）',\n\n\t\"fp_apply\": \"使用新文件名\",\n\t\"fp_skip\": \"跳过冲突项\",\n\t\"fp_ecut\": \"先剪切或复制一些文件/文件夹，然后才能粘贴/移动\\n\\n注：你可以在不同的浏览器标签页之间剪切/粘贴\",\n\t\"fp_ename\": \"{0} 个项目不能移动到这里，因为文件名已被占用。请在下方输入新名称以继续，或将名称留空（“跳过冲突项”）以跳过这些项目：\",\n\t\"fcp_ename\": \"{0} 个项目不能复制到这里，因为文件名已被占用。请在下方输入新名称以继续，或将名称留空（“跳过冲突项”）以跳过这些项目：\",\n\t\"fp_emore\": \"还有一些文件名冲突需要解决\",\n\t\"fp_ok\": \"移动成功\",\n\t\"fcp_ok\": \"复制成功\",\n\t\"fp_busy\": \"正在移动 {0} 项...\\n\\n{1}\",\n\t\"fcp_busy\": \"正在复制 {0} 项...\\n\\n{1}\",\n\t\"fp_abrt\": \"正在中止...\",\n\t\"fp_err\": \"移动失败：\\n\",\n\t\"fcp_err\": \"复制失败：\\n\",\n\t\"fp_confirm\": \"将这 {0} 项移动到这里？\",\n\t\"fcp_confirm\": \"将这 {0} 项复制到这里？\",\n\t\"fp_etab\": '无法从其他浏览器标签页读取剪贴板',\n\t\"fp_name\": \"从你的设备上传一个文件。给它一个名字：\",\n\t\"fp_both_m\": '<h6>选择粘贴内容</h6><code>Enter</code> = 从 «{1}» 移动 {0} 个文件\\n<code>ESC</code> = 从你的设备上传 {2} 个文件',\n\t\"fcp_both_m\": '<h6>选择粘贴内容</h6><code>Enter</code> = 从 «{1}» 复制 {0} 个文件\\n<code>ESC</code> = 从你的设备上传 {2} 个文件',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">移动</a><a href=\"#\" id=\"modal-ng\">上传</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">复制</a><a href=\"#\" id=\"modal-ng\">上传</a>',\n\n\t\"mk_noname\": \"请先在左侧文本框中输入文件名，再点击按钮 :p\",\n\t\"nmd_i1\": \"带上所需的文件扩展名，例如 <code>.md</code>\",\n\t\"nmd_i2\": \"由于你没有删除权限，只能创建 <code>.{0}</code> 文件\",\n\n\t\"tv_load\": \"加载文本文件：\\n\\n{0}\\n\\n{1}%（已加载 {2} MiB，共 {3} MiB）\",\n\t\"tv_xe1\": \"无法加载文本文件：\\n\\n错误 \",\n\t\"tv_xe2\": \"404，找不到文件\",\n\t\"tv_lst\": \"文本文件列表\",\n\t\"tvt_close\": \"返回到文件夹视图$N快捷键: M（或 Esc）\\\">❌ 关闭\",\n\t\"tvt_dl\": \"下载此文件$N快捷键: Y\\\">💾 下载\",\n\t\"tvt_prev\": \"显示上一个文档$N快捷键: i\\\">⬆ 上一个\",\n\t\"tvt_next\": \"显示下一个文档$N快捷键: K\\\">⬇ 下一个\",\n\t\"tvt_sel\": \"选择文件&nbsp;（用于剪切/删除/...）$N快捷键: S\\\">选择\",\n\t\"tvt_j\": \"美化 json$N快捷键: shift-J\\\">j\",\n\t\"tvt_edit\": \"在文本编辑器中打开文件$N快捷键: E\\\">✏️ 编辑\",\n\t\"tvt_tail\": \"监视文件更改，实时显示新行（tail）\\\">📡 实时\",\n\t\"tvt_wrap\": \"自动换行\\\">↵\",\n\t\"tvt_atail\": \"锁定滚动到页面底部\\\">⚓\",\n\t\"tvt_ctail\": \"解析终端颜色（ansi 转义码）\\\">🌈\",\n\t\"tvt_ntail\": \"滚动历史上限（保留多少字节的文本）\",\n\n\t\"m3u_add1\": \"歌曲已添加到 m3u 播放列表\",\n\t\"m3u_addn\": \"已添加 {0} 首歌曲到 m3u 播放列表\",\n\t\"m3u_clip\": \"m3u 播放列表已复制到剪贴板\\n\\n请创建一个名为 ××.m3u 的文本文件，并将播放列表粘贴进去，这样就可以播放了\",\n\n\t\"gt_vau\": \"不显示视频，仅播放音频\\\">🎧\",\n\t\"gt_msel\": \"启用文件选择；按住 ctrl 键可暂时恢复原点击功能$N$N&lt;em&gt;当启用时：双击可打开文件/文件夹&lt;/em&gt;$N$N快捷键：S\\\">多选\",\n\t\"gt_crop\": \"截取缩略图中间一块\\\">裁剪\",\n\t\"gt_3x\": \"高分辨率缩略图\\\">3x\",\n\t\"gt_zoom\": \"缩放\",\n\t\"gt_chop\": \"截断\",\n\t\"gt_sort\": \"排序依据\",\n\t\"gt_name\": \"名称\",\n\t\"gt_sz\": \"大小\",\n\t\"gt_ts\": \"日期\",\n\t\"gt_ext\": \"类型\",\n\t\"gt_c1\": \"截掉更多文件名（显示部分更短）\",\n\t\"gt_c2\": \"截掉更少文件名（显示部分更长）\",\n\n\t\"sm_w8\": \"正在搜索...\",\n\t\"sm_prev\": \"上次查询的搜索结果：\\n  \",\n\t\"sl_close\": \"关闭搜索结果\",\n\t\"sl_hits\": \"显示 {0} 个结果\",\n\t\"sl_moar\": \"加载更多\",\n\n\t\"s_sz\": \"大小\",\n\t\"s_dt\": \"日期\",\n\t\"s_rd\": \"路径\",\n\t\"s_fn\": \"名称\",\n\t\"s_ta\": \"标签\",\n\t\"s_ua\": \"上传于\",\n\t\"s_ad\": \"高级\",\n\t\"s_s1\": \"最小（MiB）\",\n\t\"s_s2\": \"最大（MiB）\",\n\t\"s_d1\": \"最早（iso8601 格式）\",\n\t\"s_d2\": \"最晚（iso8601 格式）\",\n\t\"s_u1\": \"上传时间不早于\",\n\t\"s_u2\": \"且/或不晚于\",\n\t\"s_r1\": \"路径包含 &nbsp;（空格分隔）\",\n\t\"s_f1\": \"名称包含 &nbsp;（用 -nope 反选）\",\n\t\"s_t1\": \"标签包含 &nbsp;（^=开头，结尾=$）\",\n\t\"s_a1\": \"特定元数据属性\",\n\n\t\"md_eshow\": \"无法渲染 \",\n\t\"md_off\": \"[📜<em>自述</em>] 在 [⚙️] 中被禁用——文档已隐藏\",\n\n\t\"badreply\": \"解析服务器回复失败\",\n\n\t\"xhr403\": \"403：访问被拒绝\\n\\n尝试按 F5，也许你已被注销\",\n\t\"xhr0\": \"未知（可能已与服务器断开连接，或者服务器已离线）\",\n\t\"cf_ok\": \"抱歉——DD\" + wah + \"oS 防护启动\\n\\n应该会在大约 30 秒后恢复\\n\\n如果无事发生，按 F5 刷新页面\",\n\t\"tl_xe1\": \"无法列出子文件夹：\\n\\n错误 \",\n\t\"tl_xe2\": \"404：找不到文件夹\",\n\t\"fl_xe1\": \"无法列出文件夹中的文件：\\n\\n错误 \",\n\t\"fl_xe2\": \"404：找不到文件夹\",\n\t\"fd_xe1\": \"无法创建子文件夹：\\n\\n错误 \",\n\t\"fd_xe2\": \"404：找不到父文件夹\",\n\t\"fsm_xe1\": \"无法发送消息：\\n\\n错误 \",\n\t\"fsm_xe2\": \"404：找不到父文件夹\",\n\t\"fu_xe1\": \"无法从服务器加载取消发布列表：\\n\\n错误 \",\n\t\"fu_xe2\": \"404：找不到文件？？\",\n\n\t\"fz_tar\": \"未压缩的 gnu-tar 文件（linux / mac）\",\n\t\"fz_pax\": \"未压缩的 pax 格式 tar（较慢）\",\n\t\"fz_targz\": \"gnu-tar 格式，3 级 gzip 压缩$N$N通常非常慢，所以$N建议使用未压缩的 tar\",\n\t\"fz_tarxz\": \"gnu-tar 格式，1 级 xz 压缩$N$N通常非常慢，所以$N建议使用未压缩的 tar\",\n\t\"fz_zip8\": \"zip 格式，文件名用 utf8 编码（在 windows 7 及更早的系统上可能会出问题）\",\n\t\"fz_zipd\": \"zip 格式，文件名用传统 cp437 编码，适用于非常旧的软件\",\n\t\"fz_zipc\": \"文件名用 cp437 编码，预先计算 crc32，$N适用于 MS-DOS PKZIP v2.04g（1993 年 10 月）$N（下载开始前所需处理时间较长）\",\n\n\t\"un_m1\": \"你可以删除以下近期上传的文件（或中止未上传完的文件）\",\n\t\"un_upd\": \"刷新\",\n\t\"un_m4\": \"或分享下面列出的文件：\",\n\t\"un_ulist\": \"显示\",\n\t\"un_ucopy\": \"复制\",\n\t\"un_flt\": \"筛选：&nbsp; URL 必须包含\",\n\t\"un_fclr\": \"取消筛选\",\n\t\"un_derr\": '删除失败：\\n',\n\t\"un_f5\": '出现问题，请尝试刷新或按 F5',\n\t\"un_uf5\": \"抱歉，你必须先刷新页面（例如按 F5 或 CTRL-R），然后才能中止此上传\",\n\t\"un_nou\": '<b>警告：</b> 服务器太繁忙，无法显示未上传完成的文件；请稍后点击“刷新”链接',\n\t\"un_noc\": '<b>警告：</b> 服务器配置中未启用/不允许取消发布已上传完成的文件',\n\t\"un_max\": \"已显示前 2000 个文件（请使用筛选功能）\",\n\t\"un_avail\": \"{0} 个近期上传可以被删除<br />{1} 个未完成的上传可以被中止\",\n\t\"un_m2\": \"按上传时间排序，最新的在前：\",\n\t\"un_no1\": \"哎呀！没有最近上传的文件\",\n\t\"un_no2\": \"哎呀！没有符合筛选条件的最近上传的文件\",\n\t\"un_next\": \"删除以下 {0} 个文件\",\n\t\"un_abrt\": \"中止\",\n\t\"un_del\": \"删除\",\n\t\"un_m3\": \"正在加载你最近上传的文件...\",\n\t\"un_busy\": \"正在删除 {0} 个文件...\",\n\t\"un_clip\": \"{0} 个链接已复制到剪贴板\",\n\n\t\"u_https1\": \"你应该\",\n\t\"u_https2\": \"切换到 https\",\n\t\"u_https3\": \"以获得更好的性能\",\n\t\"u_ancient\": '你还在用远古浏览器，也许你应该 <a href=\"#\" onclick=\"goto(\\'bup\\')\">改用 bup</a>',\n\t\"u_nowork\": \"需要 Firefox 53+ 或 Chrome 57+ 或 iOS 11+\",\n\t\"tail_2old\": \"需要 Firefox 105+ 或 Chrome 71+ 或 iOS 14.5+\",\n\t\"u_nodrop\": '浏览器版本低，不支持通过拖动文件到窗口来上传文件',\n\t\"u_notdir\": \"这不是文件夹！\\n\\n你的浏览器太老了，\\n请尝试将文件夹拖入窗口\",\n\t\"u_uri\": \"要从其他浏览器窗口拖放图片，\\n请将其拖放到大上传按钮上\",\n\t\"u_enpot\": '切换到 <a href=\"#\">土豆界面</a>（可能提高上传速度）',\n\t\"u_depot\": '切换到 <a href=\"#\">精美界面</a>（可能降低上传速度）',\n\t\"u_gotpot\": '切换到土豆界面以提高上传速度，\\n\\n不满意可以随时切换回去！',\n\t\"u_pott\": \"<p>文件： &nbsp; <b>{0}</b> 个已完成， &nbsp; <b>{1}</b> 个失败， &nbsp; <b>{2}</b> 个正在处理， &nbsp; <b>{3}</b> 个排队中</p>\",\n\t\"u_ever\": \"这是基本的上传工具，up2k 需要至少<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1\",\n\t\"u_su2k\": '这是基本的上传工具，<a href=\"#\" id=\"u2yea\">up2k</a> 更好用',\n\t\"u_uput\": '提高速度（跳过校验和）',\n\t\"u_ewrite\": '你对这个文件夹没有写入权限',\n\t\"u_eread\": '你对这个文件夹没有读取权限',\n\t\"u_enoi\": '文件搜索在服务器配置中未启用',\n\t\"u_enoow\": \"无法覆盖此处的文件；需要删除权限\",\n\t\"u_badf\": '这 {0} 个文件（共 {1} 个）被跳过，可能是由于文件系统权限：\\n\\n',\n\t\"u_blankf\": '这 {0} 个文件（共 {1} 个）是空的，是否仍然上传？\\n\\n',\n\t\"u_applef\": \"这 {0} 个文件（共 {1} 个）可能不是你想上传的。\\n按 <code>确定/Enter</code> <b>跳过</b>以下文件，\\n按 <code>取消/ESC</code> <b>取消</b>排除并<b>上传</b>这些文件：\\n\\n\",\n\t\"u_just1\": '\\n只选择一个文件的话也许能搞得好点',\n\t\"u_ff_many\": \"如果你使用的是 <b>Linux / MacOS / Android</b>，这么多文件 <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\"><em>可能</em> 导致 Firefox 崩溃！</a>\\n如果发生这种情况，请再试一次（或换用 Chrome）。\",\n\t\"u_up_life\": \"这次上传的文件将在 {0}后从服务器删除\",\n\t\"u_asku\": '将这 {0} 个文件上传到 <code>{1}</code>',\n\t\"u_unpt\": \"你可以使用左上角的 🧯 撤销/删除此次上传\",\n\t\"u_bigtab\": '即将显示 {0} 个文件，可能会导致你的浏览器崩溃。你确定吗？',\n\t\"u_scan\": '正在扫描文件...',\n\t\"u_dirstuck\": '目录迭代器尝试访问以下 {0} 个项目时卡住了，它们将被跳过：',\n\t\"u_etadone\": '已完成（{0}，{1} 个文件）',\n\t\"u_etaprep\": '（正在准备上传）',\n\t\"u_hashdone\": '哈希完成',\n\t\"u_hashing\": '哈希',\n\t\"u_hs\": '正在等待服务器...',\n\t\"u_started\": \"文件现在正在上传，见 [🚀]\",\n\t\"u_dupdefer\": \"这是一个重复文件，将在其他所有文件上传完成后处理\",\n\t\"u_actx\": \"单击此文本以防止切换到其他窗口/选项卡时性能下降\",\n\t\"u_fixed\": \"好！&nbsp;已修复 👍\",\n\t\"u_cuerr\": \"上传第 {0} 块（共 {1} 块）时失败，\\n说不定没事，正在继续\\n\\n文件：{2}\",\n\t\"u_cuerr2\": \"服务器拒绝上传（第 {0} 块，共 {1} 块），\\n稍后将重试\\n\\n文件：{2}\\n\\n错误 \",\n\t\"u_ehstmp\": \"将重试，见右下角\",\n\t\"u_ehsfin\": \"服务器拒绝了上传结束的请求，正在重试...\",\n\t\"u_ehssrch\": \"服务器拒绝了执行搜索的请求，正在重试...\",\n\t\"u_ehsinit\": \"服务器拒绝了开始上传的请求，正在重试...\",\n\t\"u_eneths\": \"进行上传握手时发生网络错误，正在重试...\",\n\t\"u_enethd\": \"检测目标是否存在时发生网络错误，正在重试...\",\n\t\"u_cbusy\": \"已从网络问题中恢复，正在等待服务器认证...\",\n\t\"u_ehsdf\": \"服务器磁盘空间不足！\\n\\n仍将持续重试，万一有人\\n清理出了足够空间就能继续\",\n\t\"u_emtleak1\": \"你的网页浏览器看起来可能有内存泄漏，\\n请\",\n\t\"u_emtleak2\": ' <a href=\"{0}\">切换到 https（推荐）</a> 或 ',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": '尝试以下操作：\\n<ul><li>按 <code>F5</code> 刷新页面</li><li>然后在&nbsp;<code>⚙️ 设置</code> 中禁用&nbsp;<code>多线</code>&nbsp;按钮</li><li>然后再次尝试上传</li></ul>上传会稍微慢一些，不过没关系。\\n麻烦你了！\\n\\nPS：chrome v107 <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">已修复</a>此问题',\n\t\"u_emtleakf\": '尝试以下操作：\\n<ul><li>按 <code>F5</code> 刷新页面</li><li>然后在上传界面中启用 <code>🥔</code>（土豆）<li>然后再次尝试上传</li></ul>\\nPS: 希望 firefox 以后能修复<a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">此问题</a>',\n\t\"u_s404\": \"在服务器上未找到\",\n\t\"u_expl\": \"解释\",\n\t\"u_maxconn\": \"大多数浏览器限制为 6，但 Firefox 允许你在 <code>about:config</code> 中配置 <code>connections-per-server</code> 来提高上限\",\n\t\"u_tu\": '<p class=\"warn\">警告：启用了超频（turbo），<span>&nbsp;客户端可能无法检测并恢复不完整的上传；请阅读超频按钮的帮助提示</span></p>',\n\t\"u_ts\": '<p class=\"warn\">警告：启用了超频（turbo），<span>&nbsp;搜索结果可能不正确；请阅读超频按钮的帮助提示</span></p>',\n\t\"u_turbo_c\": \"服务器配置中禁用了超频（turbo）\",\n\t\"u_turbo_g\": \"超频（turbo）已禁用，因为你在此卷中没有\\n列出目录下文件列表的权限\",\n\t\"u_life_cfg\": '在 <input id=\"lifem\" p=\"60\" /> 分钟（或 <input id=\"lifeh\" p=\"3600\" /> 小时）后自动删除',\n\t\"u_life_est\": '上传的文件将于 <span id=\"lifew\" tt=\"本地时间\">---</span> 删除',\n\t\"u_life_max\": '此文件夹要求\\n文件寿命不超过 {0}',\n\t\"u_unp_ok\": '{0}内允许取消发布',\n\t\"u_unp_ng\": '将不能取消发布',\n\t\"ue_ro\": '你对这个文件夹的权限是只读\\n\\n',\n\t\"ue_nl\": '你当前未登录',\n\t\"ue_la\": '你当前以 \"{0}\" 的身份登录',\n\t\"ue_sr\": '你当前处于文件搜索模式\\n\\n通过点击大搜索按钮旁边的放大镜 🔎 切换到上传模式，然后重试上传\\n\\n抱歉',\n\t\"ue_ta\": '尝试再次上传，现在应该可以了',\n\t\"ue_ab\": \"这个文件正在上传到另一个文件夹，必须完成那次上传后，才能将此文件上传到其他地方。\\n\\n你可以通过左上角的 🧯 中止并丢弃那次上传\",\n\t\"ur_1uo\": \"成功：文件上传成功\",\n\t\"ur_auo\": \"成功：{0} 个文件全部上传成功\",\n\t\"ur_1so\": \"成功：文件已在服务器上找到\",\n\t\"ur_aso\": \"成功：{0} 个文件都已在服务器上找到\",\n\t\"ur_1un\": \"上传失败，抱歉\",\n\t\"ur_aun\": \"{0} 个文件全都上传失败了，抱歉\",\n\t\"ur_1sn\": \"文件未在服务器上找到\",\n\t\"ur_asn\": \"这 {0} 个文件未在服务器上找到\",\n\t\"ur_um\": \"完成；\\n{0} 个上传成功，\\n{1} 个上传失败，抱歉\",\n\t\"ur_sm\": \"完成；\\n{0} 个文件在服务器上找到，\\n{1} 个文件未在服务器上找到\",\n\n\t\"rc_opn\": \"打开\",\n\t\"rc_ply\": \"播放\",\n\t\"rc_pla\": \"作为音频播放\",\n\t\"rc_txt\": \"在文本文件查看器中打开\",\n\t\"rc_md\": \"在 markdown 查看器中打开\",\n\t\"rc_dl\": \"下载\",\n\t\"rc_zip\": \"下载为压缩包\",\n\t\"rc_cpl\": \"复制链接\",\n\t\"rc_del\": \"删除\",\n\t\"rc_cut\": \"剪切\",\n\t\"rc_cpy\": \"复制\",\n\t\"rc_pst\": \"粘贴\",\n\t\"rc_rnm\": \"重命名\",\n\t\"rc_nfo\": \"新建文件夹\",\n\t\"rc_nfi\": \"新建文件\",\n\t\"rc_sal\": \"全选\",\n\t\"rc_sin\": \"反选\",\n\t\"rc_shf\": \"共享此文件夹\",\n\t\"rc_shs\": \"共享选中项\",\n\n\t\"lang_set\": \"刷新以使更改生效？\",\n\n\t\"splash\": {\n\t\t\"a1\": \"刷新\",\n\t\t\"b1\": \"你好呀，陌生人 &nbsp; <small>（你尚未登录）</small>\",\n\t\t\"c1\": \"登出\",\n\t\t\"d1\": \"栈转储\",\n\t\t\"d2\": \"显示所有活动线程的状态\",\n\t\t\"e1\": \"重新加载配置\",\n\t\t\"e2\": \"重新加载配置文件（账户/卷/卷标），$N并重新扫描所有 e2ds 卷$N$N注意：任何全局设置的更改$N都需要完全重启才能生效\",\n\t\t\"f1\": \"你可以浏览：\",\n\t\t\"g1\": \"你可以上传到：\",\n\t\t\"cc1\": \"其他：\",\n\t\t\"h1\": \"禁用 k304\",\n\t\t\"i1\": \"启用 k304\",\n\t\t\"j1\": \"启用 k304 后，会在每次 HTTP 304 时断开客户端连接。这可解决某些有 bug 的代理服务器卡住的问题（突然加载不出页面），<em>但</em> 也会降低各方面性能\",\n\t\t\"k1\": \"重置客户端设置\",\n\t\t\"l1\": \"登录看更多：\",\n\t\t\"ls3\": \"登录\",\n\t\t\"lu4\": \"用户名\",\n\t\t\"lp4\": \"密码\",\n\t\t\"lo3\": \"在所有浏览器中注销 “{0}”\",\n\t\t\"lo2\": \"将结束所有浏览器中的会话\",\n\t\t\"m1\": \"欢迎回来，\",\n\t\t\"n1\": \"404 找不到页面 &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": '或者你可能没有权限？尝试输入密码或 <a href=\"' + SR + '/?h\">返回主页</a>',\n\t\t\"p1\": \"403：访问被拒绝 &nbsp;~┻━┻\",\n\t\t\"q1\": '尝试输入密码或 <a href=\"' + SR + '/?h\">返回主页</a>',\n\t\t\"r1\": \"返回主页\",\n\t\t\".s1\": \"重新扫描\",\n\t\t\"t1\": \"操作\",\n\t\t\"u2\": \"自上次服务器写入的时间$N( 上传 / 重命名 / ... )$N$N17d = 17 天$N1h23 = 1 小时 23 分钟$N4m56 = 4 分钟 56 秒\",\n\t\t\"v1\": \"连接\",\n\t\t\"v2\": \"将此服务器用作本地硬盘\",\n\t\t\"w1\": \"切换到 https\",\n\t\t\"x1\": \"更改密码\",\n\t\t\"y1\": \"编辑分享的文件\",\n\t\t\"z1\": \"解锁分享的文件：\",\n\t\t\"ta1\": \"请先输入新密码\",\n\t\t\"ta2\": \"重复以确认新密码：\",\n\t\t\"ta3\": \"打错了，请重试\",\n\t\t\"nop\": \"错误：密码不能为空\",\n\t\t\"nou\": \"错误：用户名和/或密码不能为空\",\n\t\t\"aa1\": \"正在接收的文件：\",\n\t\t\"ab1\": \"禁用 no304\",\n\t\t\"ac1\": \"启用 no304\",\n\t\t\"ad1\": \"启用 no304 将禁用所有缓存；如果 k304 没用，可以尝试此选项。这会浪费大量流量！\",\n\t\t\"ae1\": \"正在下载：\",\n\t\t\"af1\": \"显示最近上传的文件\",\n\t\t\"ag1\": \"查看 idp 缓存\",\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/cze.js",
    "content": "\n// Řádky končící na //m jsou neověřené strojové překlady\n\nLs.cze = {\n\t\"tt\": \"Čeština\",\n\n\t\"cols\": {\n\t\t\"c\": \"tlačítka akcí\",\n\t\t\"dur\": \"doba trvání\",\n\t\t\"q\": \"kvalita / bitrate\",\n\t\t\"Ac\": \"audio kodek\",\n\t\t\"Vc\": \"video kodek\",\n\t\t\"Fmt\": \"formát / kontejner\",\n\t\t\"Ahash\": \"kontrolní součet audia\",\n\t\t\"Vhash\": \"kontrolní součet videa\",\n\t\t\"Res\": \"rozlišení\",\n\t\t\"T\": \"typ souboru\",\n\t\t\"aq\": \"kvalita zvuku / bitrate\",\n\t\t\"vq\": \"kvalita videa / bitrate\",\n\t\t\"pixfmt\": \"podvzorkování / struktura pixelů\",\n\t\t\"resw\": \"horizontální rozlišení\",\n\t\t\"resh\": \"vertikální rozlišení\",\n\t\t\"chs\": \"audio kanály\",\n\t\t\"hz\": \"vzorkovací frekvence\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"různé\",\n\t\t\t[\"ESC\", \"zavřít různé věci\"],\n\n\t\t\t\"správce souborů\",\n\t\t\t[\"G\", \"přepnout seznam / zobrazení mřížky\"],\n\t\t\t[\"T\", \"přepnout náhledy / ikony\"],\n\t\t\t[\"⇧ A/D\", \"velikost náhledů\"],\n\t\t\t[\"ctrl-K\", \"smazat vybrané\"],\n\t\t\t[\"ctrl-X\", \"vyjmout výběr do schránky\"],\n\t\t\t[\"ctrl-C\", \"kopírovat výběr do schránky\"],\n\t\t\t[\"ctrl-V\", \"vložit (přesunout/kopírovat) zde\"],\n\t\t\t[\"Y\", \"stáhnout vybrané\"],\n\t\t\t[\"F2\", \"přejmenovat vybrané\"],\n\n\t\t\t\"výběr souborů\",\n\t\t\t[\"space\", \"přepnout výběr souboru\"],\n\t\t\t[\"↑/↓\", \"posunout kurzor výběru\"],\n\t\t\t[\"ctrl ↑/↓\", \"posunout kurzor a zobrazení\"],\n\t\t\t[\"⇧ ↑/↓\", \"vybrat předchozí/následující soubor\"],\n\t\t\t[\"ctrl-A\", \"vybrat všechny soubory / složky\"],\n\t\t], [\n\t\t\t\"navigace\",\n\t\t\t[\"B\", \"přepnout drobečkovou navigaci / navigační panel\"],\n\t\t\t[\"I/K\", \"předchozí/následující složka\"],\n\t\t\t[\"M\", \"nadřazená složka (nebo sbalit aktuální)\"],\n\t\t\t[\"V\", \"přepnout složky / textové soubory v navigačním panelu\"],\n\t\t\t[\"A/D\", \"velikost navigačního panelu\"],\n\t\t], [\n\t\t\t\"audio přehrávač\",\n\t\t\t[\"J/L\", \"předchozí/následující skladba\"],\n\t\t\t[\"U/O\", \"přeskočit 10 sekund zpět/vpřed\"],\n\t\t\t[\"0..9\", \"přejít na 0%..90%\"],\n\t\t\t[\"P\", \"přehrát/pozastavit (také spustit)\"],\n\t\t\t[\"S\", \"vybrat přehrávanou skladbu\"],\n\t\t\t[\"Y\", \"stáhnout skladbu\"],\n\t\t], [\n\t\t\t\"prohlížeč obrázků\",\n\t\t\t[\"J/L, ←/→\", \"předchozí/následující obrázek\"],\n\t\t\t[\"Home/End\", \"první/poslední obrázek\"],\n\t\t\t[\"F\", \"celá obrazovka\"],\n\t\t\t[\"R\", \"otočit po směru hodinových ručiček\"],\n\t\t\t[\"⇧ R\", \"otočit proti směru hodinových ručiček\"],\n\t\t\t[\"S\", \"vybrat obrázek\"],\n\t\t\t[\"Y\", \"stáhnout obrázek\"],\n\t\t], [\n\t\t\t\"video přehrávač\",\n\t\t\t[\"U/O\", \"přeskočit 10 sekund zpět/vpřed\"],\n\t\t\t[\"P/K/Space\", \"přehrát/pozastavit\"],\n\t\t\t[\"C\", \"pokračovat přehráváním následující\"],\n\t\t\t[\"V\", \"smyčka\"],\n\t\t\t[\"M\", \"ztlumit\"],\n\t\t\t[\"[ and ]\", \"nastavit interval smyčky\"],\n\t\t], [\n\t\t\t\"prohlížeč textových souborů\",\n\t\t\t[\"I/K\", \"předchozí/následující soubor\"],\n\t\t\t[\"M\", \"zavřít textový soubor\"],\n\t\t\t[\"E\", \"upravit textový soubor\"],\n\t\t\t[\"S\", \"vybrat soubor (pro vyjmutí/kopírování/přejmenování)\"],\n\t\t\t[\"Y\", \"stáhnout textový soubor\"], //m\n\t\t\t[\"⇧ J\", \"zkrášlit json\"], //m\n\t\t]\n\t],\n\n\t\"m_ok\": \"OK\",\n\t\"m_ng\": \"Zrušit\",\n\n\t\"enable\": \"Povolit\",\n\t\"danger\": \"NEBEZPEČÍ\",\n\t\"clipped\": \"zkopírováno do schránky\",\n\n\t\"ht_s1\": \"sekunda\",\n\t\"ht_s2\": \"sekundy\",\n\t\"ht_s5\": \"sekund\",\n\t\"ht_m1\": \"minuta\",\n\t\"ht_m2\": \"minuty\",\n\t\"ht_m5\": \"minut\",\n\t\"ht_h1\": \"hodina\",\n\t\"ht_h2\": \"hodiny\",\n\t\"ht_h5\": \"hodin\",\n\t\"ht_d1\": \"den\",\n\t\"ht_d2\": \"dny\",\n\t\"ht_d5\": \"dní\",\n\t\"ht_and\": \" a \",\n\n\t\"goh\": \"ovládací panel\",\n\t\"gop\": 'předchozí sourozenec\">předchozí',\n\t\"gou\": 'nadřazená složka\">nahoru',\n\t\"gon\": 'následující složka\">následující',\n\t\"logout\": \"Odhlásit \",\n\t\"login\": \"Přihlásit se\", //m\n\t\"access\": \" přístup\",\n\t\"ot_close\": \"zavřít podnabídku\",\n\t\"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`&quot;try unite&quot;` = obsahuje přesně «try unite»$N$Nformát data je iso-8601, jako$N`2009-12-31` nebo `2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"unpost: smazat vaše nedávné nahrání nebo zrušit nedokončené\",\n\t\"ot_bup\": \"bup: základní nahrávač, podporuje i netscape 4.0\",\n\t\"ot_mkdir\": \"mkdir: vytvořit nový adresář\",\n\t\"ot_md\": \"new-file: vytvořit nový textový soubor\", //m\n\t\"ot_msg\": \"msg: poslat zprávu do logu serveru\",\n\t\"ot_mp\": \"možnosti přehrávače médií\",\n\t\"ot_cfg\": \"možnosti konfigurace\",\n\t\"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ž [🎈]&nbsp; (základní nahrávač)<br /><br />během nahrávání se tato ikona stává indikátorem průběhu!',\n\t\"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ž [🎈]&nbsp; (základní nahrávač)<br /><br />během nahrávání se tato ikona stává indikátorem průběhu!',\n\t\"ot_noie\": 'Prosím použijte Chrome / Firefox / Edge',\n\n\t\"ab_mkdir\": \"vytvořit adresář\",\n\t\"ab_mkdoc\": \"nový textový soubor\", //m\n\t\"ab_msg\": \"poslat zprávu do logu serveru\",\n\n\t\"ay_path\": \"přejít na složky\",\n\t\"ay_files\": \"přejít na soubory\",\n\n\t\"wt_ren\": \"přejmenovat vybrané položky$NKlávesová zkratka: F2\",\n\t\"wt_del\": \"smazat vybrané položky$NKlávesová zkratka: ctrl-K\",\n\t\"wt_cut\": \"vyjmout vybrané položky &lt;small&gt;(pak je vložit někam jinam)&lt;/small&gt;$NKlávesová zkratka: ctrl-X\",\n\t\"wt_cpy\": \"kopírovat vybrané položky do schránky$N(pro vložení někam jinam)$NKlávesová zkratka: ctrl-C\",\n\t\"wt_pst\": \"vložit dříve vyjmutý / zkopírovaný výběr$NKlávesová zkratka: ctrl-V\",\n\t\"wt_selall\": \"vybrat všechny soubory$NKlávesová zkratka: ctrl-A (když je zaměřen soubor)\",\n\t\"wt_selinv\": \"invertovat výběr\",\n\t\"wt_zip1\": \"stáhnout tuto složku jako archiv\",\n\t\"wt_selzip\": \"stáhnout výběr jako archiv\",\n\t\"wt_seldl\": \"stáhnout výběr jako samostatné soubory$NKlávesová zkratka: Y\",\n\t\"wt_npirc\": \"kopírovat informace o stopě ve formátu irc\",\n\t\"wt_nptxt\": \"kopírovat informace o stopě v prostém textu\",\n\t\"wt_m3ua\": \"přidat do m3u playlistu (klikněte později na <code>📻kopírovat</code>)\",\n\t\"wt_m3uc\": \"kopírovat m3u playlist do schránky\",\n\t\"wt_grid\": \"přepnout zobrazení mřížky / seznamu$NKlávesová zkratka: G\",\n\t\"wt_prev\": \"předchozí stopa$NKlávesová zkratka: J\",\n\t\"wt_play\": \"přehrát / pozastavit$NKlávesová zkratka: P\",\n\t\"wt_next\": \"následující stopa$NKlávesová zkratka: L\",\n\n\t\"ul_par\": \"paralelní nahrávání:\",\n\t\"ut_rand\": \"náhodné názvy souborů\",\n\t\"ut_u2ts\": \"kopírovat časovou značku poslední změny$Nz vašeho souborového systému na server\\\">📅\",\n\t\"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\n\t\"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\",\n\t\"ut_ask\": 'požádat o potvrzení před zahájením nahrávání\">💭',\n\t\"ut_pot\": \"zlepšit rychlost nahrávání na pomalých zařízeních$Nzjednodušením UI\",\n\t\"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)\",\n\t\"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\",\n\t\"ul_btn\": \"přetáhněte soubory / složky<br>sem (nebo sem klikněte)\",\n\t\"ul_btnu\": \"N A H R Á T\",\n\t\"ul_btns\": \"H L E D A T\",\n\n\t\"ul_hash\": \"hash\",\n\t\"ul_send\": \"odeslat\",\n\t\"ul_done\": \"hotovo\",\n\t\"ul_idle1\": \"zatím nejsou zařazena žádná nahrávání\",\n\t\"ut_etah\": \"průměrná rychlost &lt;em&gt;hashování&lt;/em&gt; a odhadovaný čas do dokončení\",\n\t\"ut_etau\": \"průměrná rychlost &lt;em&gt;nahrávání&lt;/em&gt; a odhadovaný čas do dokončení\",\n\t\"ut_etat\": \"průměrná &lt;em&gt;celková&lt;/em&gt; rychlost a odhadovaný čas do dokončení\",\n\n\t\"uct_ok\": \"úspěšně dokončeno\",\n\t\"uct_ng\": \"nedobré: selhalo / odmítnuto / nenalezeno\",\n\t\"uct_done\": \"celkem\",\n\t\"uct_bz\": \"hashování nebo nahrávání\",\n\t\"uct_q\": \"nečinné, čekající\",\n\n\t\"utl_name\": \"název souboru\",\n\t\"utl_ulist\": \"seznam\",\n\t\"utl_ucopy\": \"kopírovat\",\n\t\"utl_links\": \"odkazy\",\n\t\"utl_stat\": \"stav\",\n\t\"utl_prog\": \"průběh\",\n\n\t// keep short:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"CHYBA\",\n\t\"utl_oserr\": \"chyba OS\",\n\t\"utl_found\": \"nalezeno\",\n\t\"utl_defer\": \"odložit\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"hotovo\",\n\n\t\"ul_flagblk\": \"soubory byly přidány do fronty</b><br>nicméně v jiné kartě prohlížeče běží up2k,<br>takže čekáme až skončí\",\n\t\"ul_btnlk\": \"konfigurace serveru uzamkla tento přepínač v tomto stavu\",\n\n\t\"udt_up\": \"Nahrávání\",\n\t\"udt_srch\": \"Hledání\",\n\t\"udt_drop\": \"přetáhněte to sem\",\n\n\t\"u_nav_m\": '<h6>jasnačka, co máte?</h6><code>Enter</code> = Soubory (jeden nebo více)\\n<code>ESC</code> = Jednu složku (včetně podsložek)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Soubory</a><a href=\"#\" id=\"modal-ng\">Jedna složka</a>',\n\n\t\"cl_opts\": \"přepínače\",\n\t\"cl_hfsz\": \"velikost souboru\", //m\n\t\"cl_themes\": \"téma\",\n\t\"cl_langs\": \"jazyk\",\n\t\"cl_ziptype\": \"stahování složky\",\n\t\"cl_uopts\": \"up2k přepínače\",\n\t\"cl_favico\": \"favicon\",\n\t\"cl_bigdir\": \"velké adresáře\",\n\t\"cl_hsort\": \"#řazení\",\n\t\"cl_keytype\": \"notace kláves\",\n\t\"cl_hiddenc\": \"skryté sloupce\",\n\t\"cl_hidec\": \"skrýt\",\n\t\"cl_reset\": \"resetovat\",\n\t\"cl_hpick\": \"klepněte na záhlaví sloupců pro skrytí v tabulce níže\",\n\t\"cl_hcancel\": \"skrývání sloupců zrušeno\",\n\t\"cl_rcm\": \"kontextová nabídka\", //m\n\n\t\"ct_grid\": '田 mřížka',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ nápovědy',\n\t\"ct_thumb\": 'v zobrazení mřížky přepnout ikony nebo náhledy$NKlávesová zkratka: T\">🖼️ náhledy',\n\t\"ct_csel\": 'použít CTRL a SHIFT pro výběr souborů v zobrazení mřížky\">výběr',\n\t\"ct_dsel\": 'použít tažený výběr v zobrazení mřížky\">tažení', //m\n\t\"ct_dl\": 'vynutit stažení (nezobrazovat inline) při kliknutí na soubor\">dl', //m\n\t\"ct_ihop\": 'když se zavře prohlížeč obrázků, posunout dolů k naposledy zobrazenému souboru\">g⮯',\n\t\"ct_dots\": 'zobrazit skryté soubory (pokud to server povoluje)\">dotfiles',\n\t\"ct_qdel\": 'při mazání souborů požádat o potvrzení jen jednou\">rychlé mazání',\n\t\"ct_dir1st\": 'řadit složky před soubory\">📁 první',\n\t\"ct_nsort\": 'přirozené řazení (pro názvy souborů s úvodními číslicemi)\">přirozené řazení',\n\t\"ct_utc\": 'zobrazit všechny časy v UTC\">UTC',\n\t\"ct_readme\": 'zobrazit README.md v seznamech složek\">📜 readme',\n\t\"ct_idxh\": 'zobrazit index.html místo seznamu složky\">htm',\n\t\"ct_sbars\": 'zobrazit posuvníky\">⟊',\n\n\t\"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📅\",\n\n\t\"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 <em>&quot;má to stejnou velikost souboru na serveru?&quot;</em> takže pokud se obsah souborů liší, NEBUDE nahrán$N$Nměli byste to vypnout když nahrávání skončí a pak znovu &quot;nahrát&quot; stejné soubory aby je klient ověřil\\\">turbo\",\n\n\t\"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 <em>teoreticky</em> 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\",\n\n\t\"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\",\n\n\t\"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ě\",\n\n\t\"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\",\n\n\t\"cut_nag\": \"notifikace OS když nahrávání skončí$N(jen pokud prohlížeč nebo karta není aktivní)\",\n\t\"cut_sfx\": \"zvukové upozornění když nahrávání skončí$N(jen pokud prohlížeč nebo karta není aktivní)\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cft_text\": \"text favicon (prázdné a obnovte pro zakázání)\",\n\t\"cft_fg\": \"barva popředí\",\n\t\"cft_bg\": \"barva pozadí\",\n\n\t\"cdt_lim\": \"maximální počet souborů k zobrazení ve složce\",\n\t\"cdt_ask\": \"při posunování na konec,$Nmísto načítání více souborů,$N se zeptat co dělat\",\n\t\"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ě\",\n\t\"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\n\t\"cdt_rdb\": \"zobrazit běžné menu pravého tlačítka, když je vlastní již otevřené a znovu se klikne pravým\\\">x2\", //m\n\n\t\"tt_entree\": \"zobrazit navigační panel (postranní strom adresářů)$NKlávesová zkratka: B\",\n\t\"tt_detree\": \"zobrazit drobečkovou navigaci$NKlávesová zkratka: B\",\n\t\"tt_visdir\": \"posunout k vybrané složce\",\n\t\"tt_ftree\": \"přepnout strom složek / textové soubory$NKlávesová zkratka: V\",\n\t\"tt_pdock\": \"zobrazit nadřazené složky v ukotveném panelu nahoře\",\n\t\"tt_dynt\": \"automaticky rozrůstat jak se strom rozšiřuje\",\n\t\"tt_wrap\": \"zalomení řádků\",\n\t\"tt_hover\": \"odhalit přetékající řádky při najetí$N( ruší posun pokud kurzor myši $N&nbsp; není v levém okraji )\",\n\n\t\"ml_pmode\": \"na konci složky...\",\n\t\"ml_btns\": \"příkazy\",\n\t\"ml_tcode\": \"transkódovat\",\n\t\"ml_tcode2\": \"transkódovat na\",\n\t\"ml_tint\": \"odstín\",\n\t\"ml_eq\": \"audio ekvalizér\",\n\t\"ml_drc\": \"kompresor dynamického rozsahu\",\n\t\"ml_ss\": \"přeskočit ticho\", //m\n\n\t\"mt_loop\": \"smyčka/opakovat jednu skladbu\\\">🔁\",\n\t\"mt_one\": \"zastavit po jedné skladbě\\\">1️⃣\",\n\t\"mt_shuf\": \"zamíchat skladby v každé složce\\\">🔀\",\n\t\"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▶\",\n\t\"mt_preload\": \"začít načítat následující skladbu před koncem pro plynulé přehrávání\\\">přednahrání\",\n\t\"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\",\n\t\"mt_fullpre\": \"zkusit přednahrát celou skladbu;$N✅ povolit na <b>nespolehlivých</b> připojeních,$N❌ <b>zakázat</b> na pomalých připojeních pravděpodobně\\\">úplné\",\n\t\"mt_fau\": \"na telefonech zabránit zastavení hudby, pokud se další píseň nenahraje dostatečně rychle (může způsobit chybné zobrazení tagů)\\\">☕️\",\n\t\"mt_waves\": \"vlnový posuvník:$Nzobrazit amplitudu zvuku v posuvníku\\\">~s\",\n\t\"mt_npclip\": \"zobrazit tlačítka pro kopírování aktuálně přehrávané písně do schránky\\\">/np\",\n\t\"mt_m3u_c\": \"zobrazit tlačítka pro kopírování$Nvybraných písní jako položky m3u8 playlistu\\\">📻\",\n\t\"mt_octl\": \"integrace s OS (mediální klávesy / osd)\\\">os-ctl\",\n\t\"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\",\n\t\"mt_oscv\": \"zobrazit obal alba v osd\\\">art\",\n\t\"mt_follow\": \"udržet přehrávanou stopu v zobrazení\\\">🎯\",\n\t\"mt_compact\": \"kompaktní ovládání\\\">⟎\",\n\t\"mt_uncache\": \"vymazat cache &nbsp;(zkuste to, pokud váš prohlížeč uložil$Nporušenou kopii písně a odmítá ji přehrát)\\\">uncache\",\n\t\"mt_mloop\": \"opakovat otevřenou složku\\\">🔁 loop\",\n\t\"mt_mnext\": \"načíst další složku a pokračovat\\\">📂 next\",\n\t\"mt_mstop\": \"zastavit přehrávání\\\">⏸ stop\",\n\t\"mt_cflac\": \"převést flac / wav na {0}\\\">flac\",\n\t\"mt_caac\": \"převést aac / m4a na {0}\\\">aac\",\n\t\"mt_coth\": \"převést všechny ostatní (ne mp3) na {0}\\\">oth\",\n\t\"mt_c2opus\": \"nejlepší volba pro desktopy, laptopy, android\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, pro iOS 17.5 a novější\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, pro iOS 11 až 17\\\">caf\",\n\t\"mt_c2mp3\": \"použijte na velmi starých zařízeních\\\">mp3\",\n\t\"mt_c2flac\": \"nejlepší kvalita zvuku, ale obrovské stahování\\\">flac\",\n\t\"mt_c2wav\": \"nekomprimované přehrávání (ještě větší)\\\">wav\",\n\t\"mt_c2ok\": \"výborně, dobrá volba\",\n\t\"mt_c2nd\": \"to není doporučený výstupní formát pro vaše zařízení, ale v pořádku\",\n\t\"mt_c2ng\": \"vaše zařízení, zdá se, nepodporuje tento výstupní formát, ale zkusíme to\",\n\t\"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\",\n\t\"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\",\n\t\"mt_eq\": \"`povoluje ekvalizér a ovládání zisku;$N$Nboost `0` = standardní 100% hlasitost (nezměněno)$N$Nwidth `1 &nbsp;` = standardní stereo (nezměněno)$Nwidth `0.5` = 50% levý-pravý crossfeed$Nwidth `0 &nbsp;` = mono$N$Nboost `-0.8` &amp; 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ží\",\n\t\"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)\",\n\t\"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\n\t\"mt_ssvt\": \"prahová hlasitost (0-255)\\\">hl\", //m\n\t\"mt_ssts\": \"aktivní práh (% stopy, začátek)\\\">zač\", //m\n\t\"mt_sste\": \"aktivní práh (% stopy, konec)\\\">kon\", //m\n\t\"mt_sssm\": \"násobič rychlosti přehrávání\\\">rych\", //m\n\n\t\"mb_play\": \"přehrát\",\n\t\"mm_hashplay\": \"přehrát tento audio soubor?\",\n\t\"mm_m3u\": \"stiskněte <code>Enter/OK</code> pro Přehrání\\nstiskněte <code>ESC/Zrušit</code> pro Úpravu\",\n\t\"mp_breq\": \"potřebujete firefox 82+ nebo chrome 73+ nebo iOS 15+\",\n\t\"mm_bload\": \"nyní se načítá...\",\n\t\"mm_bconv\": \"převádí se na {0}, čekejte prosím...\",\n\t\"mm_opusen\": \"váš prohlížeč nemůže přehrát aac / m4a soubory;\\ntranscoding na opus je nyní povolen\",\n\t\"mm_playerr\": \"přehrávání selhalo: \",\n\t\"mm_eabrt\": \"Pokus o přehrávání byl zrušen\",\n\t\"mm_enet\": \"Vaše internetové připojení je nestabilní\",\n\t\"mm_edec\": \"Tento soubor je údajně poškozený??\",\n\t\"mm_esupp\": \"Váš prohlížeč nerozumí tomuto audio formátu\",\n\t\"mm_eunk\": \"Neznámá chyba\",\n\t\"mm_e404\": \"Nelze přehrát audio; chyba 404: Soubor nenalezen.\",\n\t\"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\",\n\t\"mm_e415\": \"Nelze přehrát audio; chyba 415: Převod souboru selhal; zkontrolujte logy serveru.\", //m\n\t\"mm_e500\": \"Nelze přehrát audio; chyba 500: Zkontrolujte logy serveru.\",\n\t\"mm_e5xx\": \"Nelze přehrát audio; chyba serveru \",\n\t\"mm_nof\": \"žádné další audio soubory v okolí nenalezeny\",\n\t\"mm_prescan\": \"Hledám hudbu k dalšímu přehrání...\",\n\t\"mm_scank\": \"Další píseň nalezena:\",\n\t\"mm_uncache\": \"cache vymazána; všechny písně se znovu stáhnou při dalším přehrávání\",\n\t\"mm_hnf\": \"tato píseň již neexistuje\",\n\n\t\"im_hnf\": \"tento obrázek již neexistuje\",\n\n\t\"f_empty\": 'tato složka je prázdná',\n\t\"f_chide\": 'toto skryje sloupec «{0}»\\n\\nmůžete odkrýt sloupce v záložce nastavení',\n\t\"f_bigtxt\": \"tento soubor má {0} MiB -- opravdu zobrazit jako text?\",\n\t\"f_bigtxt2\": \"zobrazit pouze konec souboru? to také povolí sledování/tailing, zobrazí nově přidané řádky textu v reálném čase\",\n\t\"fbd_more\": '<div id=\"blazy\">zobrazuji <code>{0}</code> z <code>{1}</code> souborů; <a href=\"#\" id=\"bd_more\">zobraz {2}</a> nebo <a href=\"#\" id=\"bd_all\">zobraz všechny</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\">zobrazuji <code>{0}</code> z <code>{1}</code> souborů; <a href=\"#\" id=\"bd_all\">zobraz všechny</a></div>',\n\t\"f_anota\": \"pouze {0} z {1} položek bylo vybráno;\\npro výběr celé složky nejprve přejděte na konec\",\n\n\t\"f_dls\": 'odkazy na soubory v aktuální složce byly\\nzměněny na odkazy ke stažení',\n\t\"f_dl_nd\": 'přeskakuje se složka (místo toho použijte stažení zip/tar):\\n', //m\n\n\t\"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 <code>.PARTIAL</code>. Stiskněte prosím Zrušit nebo Escape.\\n\\nStisknutím OK / Enter ignorujete toto varování a pokračujete ve stahování <code>.PARTIAL</code> dočasného souboru, což téměř jistě vyústí jako poškozená data.\",\n\n\t\"ft_paste\": \"vložit {0} položek$NKlávesová zkratka: ctrl-V\",\n\t\"fr_eperm\": 'nelze přejmenovat:\\nnemáte oprávnění “přesunout” v této složce',\n\t\"fd_eperm\": 'nelze smazat:\\nnemáte oprávnění “smazat” v této složce',\n\t\"fc_eperm\": 'nelze vyjmout:\\nnemáte oprávnění “přesunout” v této složce',\n\t\"fp_eperm\": 'nelze vložit:\\nnemáte oprávnění “zapisovat” v této složce',\n\t\"fr_emore\": \"vyberte alespoň jednu položku k přejmenování\",\n\t\"fd_emore\": \"vyberte alespoň jednu položku ke smazání\",\n\t\"fc_emore\": \"vyberte alespoň jednu položku k vyjmutí\",\n\t\"fcp_emore\": \"vyberte alespoň jednu položku k zkopírování do schránky\",\n\n\t\"fs_sc\": \"sdílet složku, ve které se nacházíte\",\n\t\"fs_ss\": \"sdílet vybrané soubory\",\n\t\"fs_just1d\": \"nelze vybrat více než jednu složku,\\nnebo míchat soubory a složky v jednom výběru\",\n\t\"fs_abrt\": \"❌ zrušit\",\n\t\"fs_rand\": \"🎲 náhodný.název\",\n\t\"fs_go\": \"✅ vytvořit sdílení\",\n\t\"fs_name\": \"název\",\n\t\"fs_src\": \"zdroj\",\n\t\"fs_pwd\": \"heslo\",\n\t\"fs_exp\": \"vypršení\",\n\t\"fs_tmin\": \"min\",\n\t\"fs_thrs\": \"hodin\",\n\t\"fs_tdays\": \"dní\",\n\t\"fs_never\": \"navždy\",\n\t\"fs_pname\": \"volitelný název odkazu; bude náhodný, pokud je prázdný\",\n\t\"fs_tsrc\": \"soubor nebo složka ke sdílení\",\n\t\"fs_ppwd\": \"volitelné heslo\",\n\t\"fs_w8\": \"vytváření sdílení...\",\n\t\"fs_ok\": \"stiskněte <code>Enter/OK</code> pro zkopírování do schránky\\nstiskněte <code>ESC/Zrušit</code> pro zavření\",\n\n\t\"frt_dec\": \"může opravit některé případy porušených názvů souborů\\\">url-decode\",\n\t\"frt_rst\": \"resetovat změněné názvy souborů zpět na původní\\\">↺ reset\",\n\t\"frt_abrt\": \"zrušit a zavřít toto okno\\\">❌ cancel\",\n\t\"frb_apply\": \"PŘEJMENOVAT\",\n\t\"fr_adv\": \"dávkové / metadata / přejmenování podle vzoru\\\">pokročilé\",\n\t\"fr_case\": \"regex citlivý na velikost písmen\\\">velikost\",\n\t\"fr_win\": \"názvy bezpečné pro windows; nahradit <code>&lt;&gt;:&quot;\\\\|?*</code> japonskými plnošířkovými znaky\\\">win\",\n\t\"fr_slash\": \"nahradit <code>/</code> znakem který nezpůsobí vytvoření nových složek\\\">žádné /\",\n\t\"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.\",\n\t\"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\",\n\t\"fr_pdel\": \"smazat\",\n\t\"fr_pnew\": \"uložit jako\",\n\t\"fr_pname\": \"zadejte název pro vaše nové přednastavení\",\n\t\"fr_aborted\": \"zrušeno\",\n\t\"fr_lold\": \"starý název\",\n\t\"fr_lnew\": \"nový název\",\n\t\"fr_tags\": \"tagy pro vybrané soubory (pouze pro čtení, jen pro referenci):\",\n\t\"fr_busy\": \"přejmenovávám {0} položek...\\n\\n{1}\",\n\t\"fr_efail\": \"přejmenování selhalo:\\n\",\n\t\"fr_nchg\": \"{0} z nových názvů bylo změněno kvůli <code>win</code> a/nebo <code>žádné /</code>\\n\\nPokračovat s těmito změněnými novými názvy?\",\n\n\t\"fd_ok\": \"mazání OK\",\n\t\"fd_err\": \"mazání selhalo:\\n\",\n\t\"fd_none\": \"nic nebylo smazáno; možná blokováno konfigurací serveru (xbd)?\",\n\t\"fd_busy\": \"mažu {0} položek...\\n\\n{1}\",\n\t\"fd_warn1\": \"SMAZAT těchto {0} položek?\",\n\t\"fd_warn2\": \"<b>Poslední šance!</b> Nelze vrátit zpět. Smazat?\",\n\n\t\"fc_ok\": \"vyjmout {0} položek\",\n\t\"fc_warn\": 'vyjmout {0} položek\\n\\nale: pouze <b>tato</b> karta prohlížeče je může vložit\\n(protože výběr je tak absolutně masivní)',\n\n\t\"fcc_ok\": \"zkopírováno {0} položek do schránky\",\n\t\"fcc_warn\": 'zkopírováno {0} položek do schránky\\n\\nale: pouze <b>tato</b> karta prohlížeče je může vložit\\n(protože výběr je tak absolutně masivní)',\n\n\t\"fp_apply\": \"použít tyto názvy\",\n\t\"fp_skip\": \"přeskočit konflikty\", //m\n\t\"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\",\n\t\"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\n\t\"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\n\t\"fp_emore\": \"stále jsou některé kolize názvů souborů k opravě\",\n\t\"fp_ok\": \"přesun OK\",\n\t\"fcp_ok\": \"kopírování OK\",\n\t\"fp_busy\": \"přesouvám {0} položek...\\n\\n{1}\",\n\t\"fcp_busy\": \"kopíruji {0} položek...\\n\\n{1}\",\n\t\"fp_abrt\": \"přerušuji...\", //m\n\t\"fp_err\": \"přesun selhal:\\n\",\n\t\"fcp_err\": \"kopírování selhalo:\\n\",\n\t\"fp_confirm\": \"přesunout těchto {0} položek sem?\",\n\t\"fcp_confirm\": \"zkopírovat těchto {0} položek sem?\",\n\t\"fp_etab\": 'selhalo čtení schránky z jiné karty prohlížeče',\n\t\"fp_name\": \"nahrávání souboru z vašeho zařízení. Dejte mu název:\",\n\t\"fp_both_m\": '<h6>vyberte co vložit</h6><code>Enter</code> = Přesunout {0} souborů z «{1}»\\n<code>ESC</code> = Nahrát {2} souborů z vašeho zařízení',\n\t\"fcp_both_m\": '<h6>vyberte co vložit</h6><code>Enter</code> = Kopírovat {0} souborů z «{1}»\\n<code>ESC</code> = Nahrát {2} souborů z vašeho zařízení',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Přesunout</a><a href=\"#\" id=\"modal-ng\">Nahrát</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Kopírovat</a><a href=\"#\" id=\"modal-ng\">Nahrát</a>',\n\n\t\"mk_noname\": \"napište název do textového pole vlevo předtím než to uděláte :p\",\n\t\"nmd_i1\": \"můžeš také přidat příponu souboru, například <code>.md</code>\", //m\n\t\"nmd_i2\": \"můžeš vytvářet pouze <code>.{0}</code> soubory, protože nemáš oprávnění mazat\", //m\n\n\t\"tv_load\": \"Načítání textového dokumentu:\\n\\n{0}\\n\\n{1}% ({2} z {3} MiB načteno)\",\n\t\"tv_xe1\": \"nelze načíst textový soubor:\\n\\nchyba \",\n\t\"tv_xe2\": \"404, soubor nenalezen\",\n\t\"tv_lst\": \"seznam textových souborů v\",\n\t\"tvt_close\": \"návrat do zobrazení složky$NKlávesová zkratka: M (nebo Esc)\\\">❌ zavřít\",\n\t\"tvt_dl\": \"stáhnout tento soubor$NKlávesová zkratka: Y\\\">💾 stáhnout\",\n\t\"tvt_prev\": \"zobrazit předchozí dokument$NKlávesová zkratka: i\\\">⬆ předchozí\",\n\t\"tvt_next\": \"zobrazit následující dokument$NKlávesová zkratka: K\\\">⬇ další\",\n\t\"tvt_sel\": \"vybrat soubor &nbsp; ( pro vyjmutí / kopírování / mazání / ... )$NKlávesová zkratka: S\\\">výběr\",\n\t\"tvt_j\": \"zkrášlit json$NKlávesová zkratka: shift-J\\\">j\", //m\n\t\"tvt_edit\": \"otevřít soubor v textovém editoru$NKlávesová zkratka: E\\\">✏️ upravit\",\n\t\"tvt_tail\": \"sledovat soubor pro změny; zobrazit nové řádky v reálném čase\\\">📡 sledovat\",\n\t\"tvt_wrap\": \"zalamování slov\\\">↵\",\n\t\"tvt_atail\": \"zamknout posun na konec stránky\\\">⚓\",\n\t\"tvt_ctail\": \"dekódovat barvy terminálu (ansi escape kódy)\\\">🌈\",\n\t\"tvt_ntail\": \"limit zpětného posouvání (kolik bajtů textu ponechat načtených)\",\n\n\t\"m3u_add1\": \"skladba přidána do m3u playlistu\",\n\t\"m3u_addn\": \"{0} skladeb přidáno do m3u playlistu\",\n\t\"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\",\n\n\t\"gt_vau\": \"nezobrazovat videa, jen přehrát zvuk\\\">🎧\",\n\t\"gt_msel\": \"povolit výběr souborů; ctrl-klik na soubor pro přepsání$N$N&lt;em&gt;když aktivní: dvojklik na soubor / složku pro otevření&lt;/em&gt;$N$NKlávesová zkratka: S\\\">výběr více\",\n\t\"gt_crop\": \"ořez náhledů na střed\\\">ořez\",\n\t\"gt_3x\": \"náhledy s vysokým rozlišením\\\">3x\",\n\t\"gt_zoom\": \"zoom\",\n\t\"gt_chop\": \"rozdělit\",\n\t\"gt_sort\": \"řadit podle\",\n\t\"gt_name\": \"název\",\n\t\"gt_sz\": \"velikost\",\n\t\"gt_ts\": \"datum\",\n\t\"gt_ext\": \"typ\",\n\t\"gt_c1\": \"více zkrátit názvy souborů (zobrazit méně)\",\n\t\"gt_c2\": \"méně zkrátit názvy souborů (zobrazit více)\",\n\n\t\"sm_w8\": \"hledám...\",\n\t\"sm_prev\": \"výsledky hledání níže jsou z předchozího dotazu:\\n  \",\n\t\"sl_close\": \"zavřít výsledky hledání\",\n\t\"sl_hits\": \"zobrazuji {0} zásahů\",\n\t\"sl_moar\": \"načíst více\",\n\n\t\"s_sz\": \"velikost\",\n\t\"s_dt\": \"datum\",\n\t\"s_rd\": \"cesta\",\n\t\"s_fn\": \"název\",\n\t\"s_ta\": \"tagy\",\n\t\"s_ua\": \"nahráno@\",\n\t\"s_ad\": \"pokročilé\",\n\t\"s_s1\": \"minimum MiB\",\n\t\"s_s2\": \"maximum MiB\",\n\t\"s_d1\": \"min. iso8601\",\n\t\"s_d2\": \"max. iso8601\",\n\t\"s_u1\": \"nahráno po\",\n\t\"s_u2\": \"a/nebo před\",\n\t\"s_r1\": \"cesta obsahuje &nbsp; (oddělené mezerami)\",\n\t\"s_f1\": \"název obsahuje &nbsp; (negace s -ne)\",\n\t\"s_t1\": \"tagy obsahují &nbsp; (^=začátek, konec=$)\",\n\t\"s_a1\": \"specifické vlastnosti metadat\",\n\n\t\"md_eshow\": \"nelze vykreslit \",\n\t\"md_off\": \"[📜<em>readme</em>] zakázáno v [⚙️] -- dokument skryt\",\n\n\t\"badreply\": \"Selhalo parsování odpovědi ze serveru\",\n\n\t\"xhr403\": \"403: Přístup odepřen\\n\\nzkuste stisknout F5, možná jste se odhlásili\",\n\t\"xhr0\": \"neznámý (pravděpodobně ztraceno spojení se serverem, nebo server je offline)\",\n\t\"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\",\n\t\"tl_xe1\": \"nelze vypsat podsložky:\\n\\nchyba \",\n\t\"tl_xe2\": \"404: Složka nenalezena\",\n\t\"fl_xe1\": \"nelze vypsat soubory ve složce:\\n\\nchyba \",\n\t\"fl_xe2\": \"404: Složka nenalezena\",\n\t\"fd_xe1\": \"nelze vytvořit podsložku:\\n\\nchyba \",\n\t\"fd_xe2\": \"404: Nadřazená složka nenalezena\",\n\t\"fsm_xe1\": \"nelze odeslat zprávu:\\n\\nchyba \",\n\t\"fsm_xe2\": \"404: Nadřazená složka nenalezena\",\n\t\"fu_xe1\": \"selhalo načtení unpost seznamu ze serveru:\\n\\nchyba \",\n\t\"fu_xe2\": \"404: Soubor nenalezen??\",\n\n\t\"fz_tar\": \"nekomprimovaný gnu-tar soubor (linux / mac)\",\n\t\"fz_pax\": \"nekomprimovaný tar formátu pax (pomalejší)\",\n\t\"fz_targz\": \"gnu-tar s gzip kompresí úrovně 3$N$Nto je obvykle velmi pomalé, takže$Npoužijte místo toho nekomprimovaný tar\",\n\t\"fz_tarxz\": \"gnu-tar s xz kompresí úrovně 1$N$Nto je obvykle velmi pomalé, takže$Npoužijte místo toho nekomprimovaný tar\",\n\t\"fz_zip8\": \"zip s utf8 názvy souborů (možná problematické na windows 7 a starších)\",\n\t\"fz_zipd\": \"zip s tradičními cp437 názvy souborů, pro opravdu starý software\",\n\t\"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í)\",\n\n\t\"un_m1\": \"můžete smazat vaše nedávné nahrání (nebo zrušit nedokončené) níže\",\n\t\"un_upd\": \"obnovit\",\n\t\"un_m4\": \"nebo sdílet soubory viditelné níže:\",\n\t\"un_ulist\": \"zobrazit\",\n\t\"un_ucopy\": \"kopírovat\",\n\t\"un_flt\": \"volitelný filtr:&nbsp; URL musí obsahovat\",\n\t\"un_fclr\": \"vymazat filtr\",\n\t\"un_derr\": 'unpost-delete selhalo:\\n',\n\t\"un_f5\": 'něco se pokazilo, zkuste prosím obnovit, nebo stiskněte F5',\n\t\"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\",\n\t\"un_nou\": '<b>varování:</b> server je příliš zaneprázdněn pro zobrazení nedokončených nahrávání; za chvíli klikněte na odkaz \"obnovit\"',\n\t\"un_noc\": '<b>varování:</b> unpost plně nahraných souborů není povoleno/dovoleno v konfiguraci serveru',\n\t\"un_max\": \"zobrazuji prvních 2000 souborů (použijte filtr)\",\n\t\"un_avail\": \"{0} nedávných nahrávání může být smazáno<br />{1} nedokončených může být zrušeno\",\n\t\"un_m2\": \"řazeno podle času nahrávání; nejnovější první:\",\n\t\"un_no1\": \"počkej! žádná nahrávání nejsou dostatečně nedávná\",\n\t\"un_no2\": \"počkej! žádná nahrávání odpovídající tomuto filtru nejsou dostatečně nedávná\",\n\t\"un_next\": \"smazat dalších {0} souborů níže\",\n\t\"un_abrt\": \"zrušit\",\n\t\"un_del\": \"smazat\",\n\t\"un_m3\": \"načítám vaše nedávné nahrání...\",\n\t\"un_busy\": \"mažu {0} souborů...\",\n\t\"un_clip\": \"{0} odkazů zkopírováno do schránky\",\n\n\t\"u_https1\": \"měli byste\",\n\t\"u_https2\": \"přejít na https\",\n\t\"u_https3\": \"pro lepší výkon\",\n\t\"u_ancient\": \"váš prohlížeč je úctyhodně starý -- možná byste měli <a href=\\\"#\\\" onclick=\\\"goto('bup')\\\">použít bup</a>\",\n\t\"u_nowork\": \"vyžadován firefox 53+ nebo chrome 57+ nebo iOS 11+\",\n\t\"tail_2old\": \"vyžadován firefox 105+ nebo chrome 71+ nebo iOS 14.5+\",\n\t\"u_nodrop\": \"váš prohlížeč je příliš starý pro nahrávání přetažením (drag-and-drop)\",\n\t\"u_notdir\": \"toto není složka!\\n\\nváš prohlížeč je příliš starý,\\nzkuste prosím soubory přetáhnout\",\n\t\"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í\",\n\t\"u_enpot\": \"přepnout na <a href=\\\"#\\\">potato UI</a> (může zrychlit nahrávání)\",\n\t\"u_depot\": \"přepnout na <a href=\\\"#\\\">fancy UI</a> (může zpomalit nahrávání)\",\n\t\"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!\",\n\t\"u_pott\": \"<p>soubory: &nbsp; <b>{0}</b> dokončeno, &nbsp; <b>{1}</b> selhalo, &nbsp; <b>{2}</b> nahrává se, &nbsp; <b>{3}</b> ve frontě</p>\",\n\t\"u_ever\": \"toto je základní nahrávání; up2k vyžaduje alespoň<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1\",\n\t\"u_su2k\": \"toto je základní nahrávání; <a href=\\\"#\\\" id=\\\"u2yea\\\">up2k</a> je lepší\",\n\t\"u_uput\": \"optimalizovat pro rychlost (přeskočit kontrolní součet)\",\n\t\"u_ewrite\": \"nemáte oprávnění k zápisu do této složky\",\n\t\"u_eread\": \"nemáte oprávnění ke čtení této složky\",\n\t\"u_enoi\": \"vyhledávání souborů není povoleno v konfiguraci serveru\",\n\t\"u_enoow\": \"přepsání zde nebude fungovat; je vyžadováno oprávnění k mazání\",\n\t\"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\",\n\t\"u_blankf\": \"Těchto {0} souborů (z celkem {1}) je prázdných; přesto je nahrát?\\n\\n\",\n\t\"u_applef\": \"Těchto {0} souborů (z celkem {1}) je pravděpodobně nežádoucích;\\nStiskněte <code>OK/Enter</code> pro PŘESKOČENÍ následujících souborů,\\nStiskněte <code>Zrušit/ESC</code> pro Zahrnutí a NAHRÁNÍ i těchto souborů:\\n\\n\",\n\t\"u_just1\": \"\\nMožná to bude fungovat lépe, když vyberete pouze jeden soubor\",\n\t\"u_ff_many\": \"pokud používáte <b>Linux / MacOS / Android,</b> takové množství souborů <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\"><em>může</em> shodit Firefox!</a>\\npokud se to stane, zkuste to prosím znovu (nebo použijte Chrome).\",\n\t\"u_up_life\": \"Tento upload bude smazán ze serveru\\n{0} po jeho dokončení\",\n\t\"u_asku\": \"Nahrát {0} souborů do <code>{1}</code>\",\n\t\"u_unpt\": \"toto nahrávání můžete vrátit zpět / smazat pomocí 🧯 vlevo nahoře\",\n\t\"u_bigtab\": \"chystám se zobrazit {0} souborů\\n\\nto může shodit váš prohlížeč, jste si jisti?\",\n\t\"u_scan\": \"Skenuji soubory...\",\n\t\"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:\",\n\t\"u_etadone\": \"Hotovo ({0}, {1} souborů)\",\n\t\"u_etaprep\": \"(příprava na nahrávání)\",\n\t\"u_hashdone\": \"hashování dokončeno\",\n\t\"u_hashing\": \"hashování\",\n\t\"u_hs\": \"navazuji spojení...\",\n\t\"u_started\": \"soubory se nyní nahrávají; viz [🚀]\",\n\t\"u_dupdefer\": \"duplikát; bude zpracován po všech ostatních souborech\",\n\t\"u_actx\": \"klikněte na tento text, abyste zabránili ztrátě<br />výkonu při přepínání do jiných oken/záložek\",\n\t\"u_fixed\": \"OK!&nbsp; Opraveno 👍\",\n\t\"u_cuerr\": \"nepodařilo se nahrát část {0} z {1};\\npatrně neškodné, pokračuji\\n\\nsoubor: {2}\",\n\t\"u_cuerr2\": \"server odmítl nahrání (část {0} z {1});\\nzopakuji později\\n\\nsoubor: {2}\\n\\nchyba \",\n\t\"u_ehstmp\": \"zopakuji pokus; viz vpravo dole\",\n\t\"u_ehsfin\": \"server odmítl požadavek na dokončení nahrávání; opakuji pokus...\",\n\t\"u_ehssrch\": \"server odmítl požadavek na vyhledávání; opakuji pokus...\",\n\t\"u_ehsinit\": \"server odmítl požadavek na zahájení nahrávání; opakuji pokus...\",\n\t\"u_eneths\": \"síťová chyba při navazování spojení pro nahrávání; opakuji pokus...\",\n\t\"u_enethd\": \"síťová chyba při ověřování existence cíle; opakuji pokus...\",\n\t\"u_cbusy\": \"čekám, až nám server po síťovém problému začne znovu důvěřovat...\",\n\t\"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í\",\n\t\"u_emtleak1\": \"vypadá to, že váš webový prohlížeč může mít únik paměti (memory leak);\\nprosím\",\n\t\"u_emtleak2\": \" <a href=\\\"{0}\\\">přejděte na https (doporučeno)</a> nebo \",\n\t\"u_emtleak3\": \" \",\n\t\"u_emtleakc\": \"zkuste následující:\\n<ul><li>stiskněte <code>F5</code> pro obnovení stránky</li><li>poté vypněte tlačítko &nbsp;<code>mt</code>&nbsp; v &nbsp;<code>⚙️ nastavení</code></li><li>a zkuste nahrávání znovu</li></ul>Nahrávání bude o něco pomalejší, ale co se dá dělat.\\nOmlouváme se za potíže!\\n\\nPS: chrome v107 <a href=\\\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\\\" target=\\\"_blank\\\">obsahuje opravu</a> pro tento problém\",\n\t\"u_emtleakf\": \"zkuste následující:\\n<ul><li>stiskněte <code>F5</code> pro obnovení stránky</li><li>poté zapněte <code>🥔</code> (potato) v rozhraní nahrávání<li>a zkuste nahrávání znovu</li></ul>\\nPS: firefox snad <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\">bude mít opravu</a> v některé z příštích verzí\",\n\t\"u_s404\": \"nenalezeno na serveru\",\n\t\"u_expl\": \"vysvětlit\",\n\t\"u_maxconn\": \"většina prohlížečů omezuje počet na 6, ale firefox umožňuje toto navýšit pomocí <code>connections-per-server</code> v <code>about:config</code>\",\n\t\"u_tu\": \"<p class=\\\"warn\\\">VAROVÁNÍ: turbo zapnuto, <span>&nbsp;klient nemusí detekovat a obnovit nedokončené nahrávání; viz nápovědu u tlačítka turbo</span></p>\",\n\t\"u_ts\": \"<p class=\\\"warn\\\">VAROVÁNÍ: turbo zapnuto, <span>&nbsp;výsledky vyhledávání mohou být nesprávné; viz nápovědu u tlačítka turbo</span></p>\",\n\t\"u_turbo_c\": \"turbo je vypnuto v konfiguraci serveru\",\n\t\"u_turbo_g\": \"vypínám turbo, protože nemáte oprávnění\\nk výpisu adresářů na tomto svazku\",\n\t\"u_life_cfg\": 'automatické smazání po <input id=\"lifem\" p=\"60\" /> min (nebo <input id=\"lifeh\" p=\"3600\" /> hodinách)',\n\t\"u_life_est\": 'nahrání bude smazáno <span id=\"lifew\" tt=\"local time\">---</span>',\n\t\"u_life_max\": 'tato složka vynucuje\\nmax. životnost {0}',\n\t\"u_unp_ok\": 'unpost je povoleno pro {0}',\n\t\"u_unp_ng\": 'unpost NEBUDE povoleno',\n\t\"ue_ro\": 'váš přístup k této složce je pouze pro čtení\\n\\n',\n\t\"ue_nl\": 'momentálně nejste přihlášeni',\n\t\"ue_la\": 'momentálně jste přihlášeni jako \"{0}\"',\n\t\"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',\n\t\"ue_ta\": 'zkuste nahrávání znovu, nyní by to mělo fungovat',\n\t\"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 🧯\",\n\t\"ur_1uo\": \"OK: Soubor úspěšně nahrán\",\n\t\"ur_auo\": \"OK: Všech {0} souborů úspěšně nahráno\",\n\t\"ur_1so\": \"OK: Soubor nalezen na serveru\",\n\t\"ur_aso\": \"OK: Všech {0} souborů nalezeno na serveru\",\n\t\"ur_1un\": \"Nahrání selhalo, omlouváme se\",\n\t\"ur_aun\": \"Všech {0} nahrání selhalo, omlouváme se\",\n\t\"ur_1sn\": \"Soubor NEBYL nalezen na serveru\",\n\t\"ur_asn\": \"{0} souborů NEBYLO nalezeno na serveru\",\n\t\"ur_um\": \"Dokončeno;\\n{0} nahrání OK,\\n{1} nahrání selhalo, omlouváme se\",\n\t\"ur_sm\": \"Dokončeno;\\n{0} souborů nalezeno na serveru,\\n{1} souborů NENALEZENO na serveru\",\n\n\t\"rc_opn\": \"otevřít\", //m\n\t\"rc_ply\": \"přehrát\", //m\n\t\"rc_pla\": \"přehrát jako zvuk\", //m\n\t\"rc_txt\": \"otevřít v prohlížeči souborů\", //m\n\t\"rc_md\": \"otevřít v textovém editoru\", //m\n\t\"rc_dl\": \"stáhnout\", //m\n\t\"rc_zip\": \"stáhnout jako archiv\", //m\n\t\"rc_cpl\": \"kopírovat odkaz\", //m\n\t\"rc_del\": \"smazat\", //m\n\t\"rc_cut\": \"vyjmout\", //m\n\t\"rc_cpy\": \"kopírovat\", //m\n\t\"rc_pst\": \"vložit\", //m\n\t\"rc_rnm\": \"přejmenovat\", //m\n\t\"rc_nfo\": \"nová složka\", //m\n\t\"rc_nfi\": \"nový soubor\", //m\n\t\"rc_sal\": \"vybrat vše\", //m\n\t\"rc_sin\": \"invertovat výběr\", //m\n\t\"rc_shf\": \"sdílet tuto složku\", //m\n\t\"rc_shs\": \"sdílet výběr\", //m\n\n\t\"lang_set\": \"obnovit stránku, aby se změna projevila?\",\n\n\t\"splash\": {\n\t\t\"a1\": \"obnovit\",\n\t\t\"b1\": \"ahoj cizinče &nbsp; <small>(nejsi přihlášen)</small>\",\n\t\t\"c1\": \"odhlásit se\",\n\t\t\"d1\": \"vypsat zásobníku\",\n\t\t\"d2\": \"zobrazit stav všech aktivních vláken\",\n\t\t\"e1\": \"znovu načíst konfiguraci\",\n\t\t\"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\",\n\t\t\"f1\": \"můžeš procházet:\",\n\t\t\"g1\": \"můžeš nahrávat do:\",\n\t\t\"cc1\": \"další věci:\",\n\t\t\"h1\": \"zakázat k304\",\n\t\t\"i1\": \"povolit k304\",\n\t\t\"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), <em>ale</em> také to obecně zpomalí věci\",\n\t\t\"k1\": \"resetovat nastavení klienta\",\n\t\t\"l1\": \"přihlaste se pro více:\",\n\t\t\"ls3\": \"přihlásit se\", //m\n\t\t\"lu4\": \"uživatelské jméno\", //m\n\t\t\"lp4\": \"heslo\", //m\n\t\t\"lo3\": \"odhlásit “{0}” všude\", //m\n\t\t\"lo2\": \"tímto ukončíte relaci ve všech prohlížečích\", //m\n\t\t\"m1\": \"vítej zpět,\",\n\t\t\"n1\": \"404 nenalezeno &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": 'nebo možná nemáš přístup -- zkus heslo nebo <a href=\"' + SR + '/?h\">jdi domů</a>',\n\t\t\"p1\": \"403 zakázáno &nbsp;~┻━┻\",\n\t\t\"q1\": 'použij heslo nebo <a href=\"' + SR + '/?h\">jdi domů</a>',\n\t\t\"r1\": \"jdi domů\",\n\t\t\".s1\": \"znovu prohledat\",\n\t\t\"t1\": \"akce\",\n\t\t\"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\",\n\t\t\"v1\": \"připojit\",\n\t\t\"v2\": \"použít tento server jako místní HDD\",\n\t\t\"w1\": \"přepnout na https\",\n\t\t\"x1\": \"změnit heslo\",\n\t\t\"y1\": \"upravit sdílení\",\n\t\t\"z1\": \"odblokovat toto sdílení:\",\n\t\t\"ta1\": \"nejprve vyplňte své nové heslo\",\n\t\t\"ta2\": \"zopakujte pro potvrzení nového hesla:\",\n\t\t\"ta3\": \"nalezen překlep; zkuste to prosím znovu\",\n\t\t\"nop\": \"CHYBA: Heslo nesmí být prázdné\", //m\n\t\t\"nou\": \"CHYBA: Uživatelské jméno a/nebo heslo nesmí být prázdné\", //m\n\t\t\"aa1\": \"příchozí soubory:\",\n\t\t\"ab1\": \"deaktivovat no304\",\n\t\t\"ac1\": \"povolit no304\",\n\t\t\"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!\",\n\t\t\"ae1\": \"aktivní stahování:\",\n\t\t\"af1\": \"zobrazit nedávné nahrávání\",\n\t\t\"ag1\": \"zobrazit známé uživatele IdP\", //m\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/deu.js",
    "content": "\n// Zeilen, die mit //m enden, sind nicht verifizierte maschinelle Übersetzungen\n\nLs.deu = {\n\t\"tt\": \"Deutsch\",\n\n\t\"cols\": {\n\t\t\"c\": \"Aktionen\",\n\t\t\"dur\": \"Dauer\",\n\t\t\"q\": \"Qualität / Bitrate\",\n\t\t\"Ac\": \"Audiocodec\",\n\t\t\"Vc\": \"Videocodec\",\n\t\t\"Fmt\": \"Format / Container\",\n\t\t\"Ahash\": \"Audio Checksumme\",\n\t\t\"Vhash\": \"Video Checksumme\",\n\t\t\"Res\": \"Auflösung\",\n\t\t\"T\": \"Dateityp\",\n\t\t\"aq\": \"Audioqualität / Bitrate\",\n\t\t\"vq\": \"Videoqualität / Bitrate\",\n\t\t\"pixfmt\": \"Subsampling / Pixelstruktur\",\n\t\t\"resw\": \"horizontale Auflösung\",\n\t\t\"resh\": \"vertikale Auflösung\",\n\t\t\"chs\": \"Audiokanäle\",\n\t\t\"hz\": \"Abtastrate\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"misc\",\n\t\t\t[\"ESC\", \"Dinge schliessen\"],\n\n\t\t\t\"file-manager\",\n\t\t\t[\"G\", \"zwischen Liste und Gitter wechseln\"],\n\t\t\t[\"T\", \"zwischen Vorschaubildern und Symbolen wechseln\"],\n\t\t\t[\"⇧ A/D\", \"Vorschaubildergrösse ändern\"],\n\t\t\t[\"STRG-K\", \"Auswahl löschen\"],\n\t\t\t[\"STRG-X\", \"Auswahl ausschneiden\"],\n\t\t\t[\"STRG-C\", \"Auswahl in Zwischenablage kopieren\"],\n\t\t\t[\"STRG-V\", \"Zwischenablage hier einfügen\"],\n\t\t\t[\"Y\", \"Auswahl herunterladen\"],\n\t\t\t[\"F2\", \"Auswahl umbenennen\"],\n\n\t\t\t\"file-list-sel\",\n\t\t\t[\"LEER\", \"Dateiauswahl aktivieren\"],\n\t\t\t[\"↑/↓\", \"Cursor verschieben\"],\n\t\t\t[\"STRG ↑/↓\", \"Cursor und Bildschirm verschieben\"],\n\t\t\t[\"⇧ ↑/↓\", \"Vorherige / nächste Datei auswählen\"],\n\t\t\t[\"STRG-A\", \"Alle Dateien / Ordner auswählen\"],\n\t\t], [\n\t\t\t\"navigation\",\n\t\t\t[\"B\", \"Zwischen Brotkrumen und Navpane wechseln\"],\n\t\t\t[\"I/K\", \"vorheriger / nächster Ordner\"],\n\t\t\t[\"M\", \"übergeordneter Ordner (oder Vorherigen einklappen)\"],\n\t\t\t[\"V\", \"Zwischen Textdateien und Navpane wechseln\"],\n\t\t\t[\"A/D\", \"Grösse der Navpane ändern\"],\n\t\t], [\n\t\t\t\"audio-player\",\n\t\t\t[\"J/L\", \"Vorheriger / nächster Song\"],\n\t\t\t[\"U/O\", \"10 Sek. vor- / zurückspringen\"],\n\t\t\t[\"0..9\", \"zu 0%..90% springen\"],\n\t\t\t[\"P\", \"Wiedergabe / Pause\"],\n\t\t\t[\"S\", \"aktuell abgespielten Song auswählen\"],\n\t\t\t[\"Y\", \"Song herunterladen\"],\n\t\t], [\n\t\t\t\"image-viewer\",\n\t\t\t[\"J/L, ←/→\", \"vorheriges / nächstes Bild\"],\n\t\t\t[\"Pos1/Ende\", \"erstes / letztes Bild\"],\n\t\t\t[\"F\", \"Vollbild\"],\n\t\t\t[\"R\", \"im Uhrzeigersinn drehen\"],\n\t\t\t[\"⇧ R\", \"gegen den Uhrzeigensinn drehen\"],\n\t\t\t[\"S\", \"Bild auswählen\"],\n\t\t\t[\"Y\", \"Bild herunterladen\"],\n\t\t], [\n\t\t\t\"video-player\",\n\t\t\t[\"U/O\", \"10 Sek. vor- / zurückspringen\"],\n\t\t\t[\"P/K/LEER\", \"Wiedergabe / Pause\"],\n\t\t\t[\"C\", \"continue playing next\"],\n\t\t\t[\"V\", \"Wiederholungs-Wiedergabe (Loop)\"],\n\t\t\t[\"M\", \"Stummschalten\"],\n\t\t\t[\"[ und ]\", \"Loop-Interval einstellen\"],\n\t\t], [\n\t\t\t\"textfile-viewer\",\n\t\t\t[\"I/K\", \"vorherige / nächste Datei\"],\n\t\t\t[\"M\", \"Textdatei schliessen\"],\n\t\t\t[\"E\", \"Textdatei bearbeiten\"],\n\t\t\t[\"S\", \"Textdatei auswählen (für Ausschneiden / Kopieren / Umbenennen)\"],\n\t\t\t[\"Y\", \"Textdatei herunterladen\"],\n\t\t\t[\"⇧ J\", \"json verschönern\"],\n\t\t]\n\t],\n\n\t\"m_ok\": \"OK\",\n\t\"m_ng\": \"Abbrechen\",\n\n\t\"enable\": \"Aktivieren\",\n\t\"danger\": \"ACHTUNG\",\n\t\"clipped\": \"in Zwischenablage kopiert\",\n\n\t\"ht_s1\": \"Sekunde\",\n\t\"ht_s2\": \"Sekunden\",\n\t\"ht_m1\": \"Minute\",\n\t\"ht_m2\": \"Minuten\",\n\t\"ht_h1\": \"Stunde\",\n\t\"ht_h2\": \"Stunden\",\n\t\"ht_d1\": \"Tag\",\n\t\"ht_d2\": \"Tage\",\n\t\"ht_and\": \" und \",\n\n\t\"goh\": \"Einstellungen\",\n\t\"gop\": 'zum vorherigen Ordner springen\">vorh.',\n\t\"gou\": 'zum übergeordneter Ordner springen\">hoch',\n\t\"gon\": 'zum nächsten Ordner springen\">nächst.',\n\t\"logout\": \"Abmelden \",\n\t\"login\": \"Anmelden\",\n\t\"access\": \" Zugriff\",\n\t\"ot_close\": \"Submenu schliessen\",\n\t\"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`&quot;try unite&quot;` = genau «try unite» enthalten$N$NDatumsformat ist iso-8601, z.B.$N`2009-12-31` oder `2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"unpost: lösche deine letzten Uploads oder breche unvollständige ab\",\n\t\"ot_bup\": \"bup: Basic Uploader, unterstützt sogar Neuheiten wie Netscape 4.0\",\n\t\"ot_mkdir\": \"mkdir: neuen Ordner erstellen\",\n\t\"ot_md\": \"new-file: neue Textdatei erstellen\",\n\t\"ot_msg\": \"msg: eine Nachricht an das Server-Log schicken\",\n\t\"ot_mp\": \"Media Player-Optionen\",\n\t\"ot_cfg\": \"Konfigurationsoptionen\",\n\t\"ot_u2i\": 'up2k: Dateien hochladen (wenn du Schreibrechte hast) oder in den Suchmodus wechseln, um zu prüfen, ob sie bereits auf dem Server existieren$N$NUploads sind fortsetzbar, multithreaded und behalten Dateizeitstempel, verbrauchen aber mehr CPU als [🎈]&nbsp; (der einfache Uploader)<br /><br />während Uploads wird dieses Symbol zu einem Fortschrittsanzeiger!',\n\t\"ot_u2w\": 'up2k: Dateien mit Wiederaufnahme-Unterstützung hochladen (Browser schließen und später dieselben Dateien erneut hochladen)$N$Nmultithreaded, behält Dateizeitstempel, verbraucht aber mehr CPU als [🎈]&nbsp; (der einfache Uploader)<br /><br />während Uploads wird dieses Symbol zu einem Fortschrittsanzeiger!',\n\t\"ot_noie\": 'Bitte benutze Chrome / Firefox / Edge',\n\n\t\"ab_mkdir\": \"Ordner erstellen\",\n\t\"ab_mkdoc\": \"Textdatei erstellen\",\n\t\"ab_msg\": \"Nachricht an Server Log senden\",\n\n\t\"ay_path\": \"zu Ordnern springen\",\n\t\"ay_files\": \"zu Dateien springen\",\n\n\t\"wt_ren\": \"ausgewählte Elemente umbenennen$NHotkey: F2\",\n\t\"wt_del\": \"ausgewählte Elemente löschen$NHotkey: STRG-K\",\n\t\"wt_cut\": \"ausgewählte Elemente ausschneiden &lt;small&gt;(um sie dann irgendwo anders einzufügen)&lt;/small&gt;$NHotkey: STRG-X\",\n\t\"wt_cpy\": \"ausgewählte Elemente in Zwischenablage kopieren$N(um sie dann irgendwo anders einzufügen)$NHotkey: ctrl-C\",\n\t\"wt_pst\": \"zuvor ausgeschnittenen / kopierte Elemente einfügen$NHotkey: STRG-V\",\n\t\"wt_selall\": \"alle Dateien auswählen$NHotkey: STRG-A (wenn Datei fokusiert)\",\n\t\"wt_selinv\": \"Auswahl invertieren\",\n\t\"wt_zip1\": \"Diesen Ordner als Archiv herunterladen\",\n\t\"wt_selzip\": \"Auswahl als Archiv herunterladen\",\n\t\"wt_seldl\": \"Auswahl als separate Dateien herunterladen$NHotkey: Y\",\n\t\"wt_npirc\": \"kopiere Titelinfo als IRC-formattierten Text\",\n\t\"wt_nptxt\": \"kopiere Titelinfo als Text\",\n\t\"wt_m3ua\": \"Zu M3U-Wiedergabeliste hinzufügen (wähle später <code>📻copy</code>)\",\n\t\"wt_m3uc\": \"M3U-Wiedergabeliste in Zwischenablage kopieren\",\n\t\"wt_grid\": \"Zwischen Gitter und Liste wechseln$NHotkey: G\",\n\t\"wt_prev\": \"Vorheriger Titel$NHotkey: J\",\n\t\"wt_play\": \"Wiedergabe / Pause$NHotkey: P\",\n\t\"wt_next\": \"Nächster Titel$NHotkey: L\",\n\n\t\"ul_par\": \"Parallele Uploads:\",\n\t\"ut_rand\": \"Zufällige Dateinamen\",\n\t\"ut_u2ts\": \"Zuletzt geändert-Zeitstempel von$Ndeinem Dateisystem auf den Server übertragen\\\">📅\",\n\t\"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\",\n\t\"ut_mt\": \"Andere Dateien während des Uploads hashen$N$Nsolltest du deaktivieren, falls deine CPU oder Festplatte zum Flaschenhals werden könnte\",\n\t\"ut_ask\": 'Vor dem Upload nach Bestätigung fragen\">💭',\n\t\"ut_pot\": \"Verbessert Upload-Geschwindigkeit$Nindem das UI weniger komplex gemacht wird\",\n\t\"ut_srch\": \"nicht wirklich hochladen, stattdessen prüfen ob Datei bereits auf dem Server existiert (scannt alle Ordner, die du lesen kannst)\",\n\t\"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\",\n\t\"ul_btn\": \"Dateien / Ordner hier<br>ablegen (oder klick mich)\",\n\t\"ul_btnu\": \"U P L O A D\",\n\t\"ul_btns\": \"S U C H E N\",\n\n\t\"ul_hash\": \"hash\",\n\t\"ul_send\": \"senden\",\n\t\"ul_done\": \"fertig\",\n\t\"ul_idle1\": \"keine Uploads in der Warteschlange\",\n\t\"ut_etah\": \"durchschnittl. &lt;em&gt;hashing&lt;/em&gt; Geschw. &amp; gesch. Restzeit\",\n\t\"ut_etau\": \"durchschnittl. &lt;em&gt;upload&lt;/em&gt; Geschw. &amp; gesch. Restzeit\",\n\t\"ut_etat\": \"durchschnittl. &lt;em&gt;total&lt;/em&gt; Geschw. &amp; gesch. Restzeit\",\n\n\t\"uct_ok\": \"Erfolgreich abgeschlossen\",\n\t\"uct_ng\": \"no-good: fehlgeschlagen / abgelehnt / nicht gefunden\",\n\t\"uct_done\": \"ok and ng zusammen\",\n\t\"uct_bz\": \"wird gehasht oder hochgeladen\",\n\t\"uct_q\": \"ausstehend\",\n\n\t\"utl_name\": \"Dateiname\",\n\t\"utl_ulist\": \"Liste\",\n\t\"utl_ucopy\": \"kopieren\",\n\t\"utl_links\": \"Links\",\n\t\"utl_stat\": \"Status\",\n\t\"utl_prog\": \"Fortschritt\",\n\n\t// keep short:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"Fehler\",\n\t\"utl_oserr\": \"OS-Fehler\",\n\t\"utl_found\": \"gefunden\",\n\t\"utl_defer\": \"zurückstellen\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"fertig\",\n\n\t\"ul_flagblk\": \"Die Dateien wurden zur Warteschlange hinzugefügt</b><br>jedoch ist up2k gerade in einem anderen Browsertab aktiv.<br>Ich warte, bis der Upload abgeschlossen ist.\",\n\t\"ul_btnlk\": \"Die Serverkonfiguration hat diese Einstellung gesperrt\",\n\n\t\"udt_up\": \"Upload\",\n\t\"udt_srch\": \"Suchen\",\n\t\"udt_drop\": \"hier ablegen\",\n\n\t\"u_nav_m\": '<h6>okay, was gibts??</h6><code>Eingabe</code> = Dateien (1 oder mehr)\\n<code>ESC</code> = 1 Ordner (inkl. Unterordner)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Dateien</a><a href=\"#\" id=\"modal-ng\">1 Ordner</a>',\n\n\t\"cl_opts\": \"Schalter\",\n\t\"cl_hfsz\": \"Dateigröße\",\n\t\"cl_themes\": \"Themes\",\n\t\"cl_langs\": \"Sprache\",\n\t\"cl_ziptype\": \"Ordner Download\",\n\t\"cl_uopts\": \"up2k Schalter\",\n\t\"cl_favico\": \"Favicon\",\n\t\"cl_bigdir\": \"grosse Ordner\",\n\t\"cl_hsort\": \"#sort\",\n\t\"cl_keytype\": \"Schlüsselnotation\",\n\t\"cl_hiddenc\": \"Spalten verstecken\",\n\t\"cl_hidec\": \"verstecken\",\n\t\"cl_reset\": \"zurücksetzen\",\n\t\"cl_hpick\": \"zum Verstecken, tippe auf Spaltenüberschriften in der Tabelle unten\",\n\t\"cl_hcancel\": \"Spaltenbearbeitung abgebrochen\",\n\t\"cl_rcm\": \"Rechtsklick-Menü\",\n\n\t\"ct_grid\": '田 Das Raster&trade;',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ Tooltips',\n\t\"ct_thumb\": 'In Raster-Ansicht, zwischen Icons und Vorschau wechseln$NHotkey: T\">🖼️ Vorschaubilder',\n\t\"ct_csel\": 'Benutze STRG und UMSCHALT für Dateiauswahl in Raster-Ansicht\">sel',\n\t\"ct_dsel\": 'Ziehauswahl in Raster-Ansicht verwenden\">ziehen', //m\n\t\"ct_dl\": 'Beim Klick auf Dateien sie immer herunterladen (nicht einbetten)\">dl',\n\t\"ct_ihop\": 'Wenn die Bildanzeige geschlossen ist, scrolle runter zu den zuletzt angesehenen Dateien\">g⮯',\n\t\"ct_dots\": 'Verstecke Dateien anzeigen (wenn durch den Server erlaubt)\">dotfiles',\n\t\"ct_qdel\": 'Nur einmal fragen, wenn mehrere Dateien gelöscht werden\">qdel',\n\t\"ct_dir1st\": 'Ordner vor Dateien sortieren\">📁 zuerst',\n\t\"ct_nsort\": 'Natürliche Sortierung (für Dateinamen mit führenden Ziffern)\">nsort',\n\t\"ct_utc\": 'Für alle Zeitangaben UTC verwenden\">UTC',\n\t\"ct_readme\": 'README.md in Dateiliste anzeigen\">📜 readme',\n\t\"ct_idxh\": 'index.html anstelle von Dateiliste anzeigen\">htm',\n\t\"ct_sbars\": 'Scrollbars zeigen\">⟊',\n\n\t\"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📅\",\n\n\t\"cut_turbo\": \"der YOLO-Knopf, den du wahrscheinlich NICHT aktivieren willst:$N$NBenutze ihn, falls du ne Menge Zeug hochladen wolltest und aus irgendeinem Grund neustarten musstest und du so schnell wie möglich weitermachen willst.$N$Ndies ersetzt den Hash-Check mit einem einfachen <em>&quot;Ist die Datei auf dem Server gleich gross?&quot;</em>, wenn die Datei also anderen Inhalt hat, wird sie NICHT nochmal hochgeladen!$N$NDu solltest dieses Feature ausschalten, sobald der Upload fertig ist und dann die gleichen Dateien nochmal &quot;hochladen&quot;, damit der Client sie verifizieren kann.\\\">turbo\",\n\n\t\"cut_datechk\": \"Funktioniert nur in kombination mit dem Turbo-Knopf$N$NReduziert den YOLO-Faktor ein bisschen; prüft, ob der Zeitstempel deiner Datei mit dem auf dem Server übereinstimmt$N$Nsollte <em>theoretisch</em> die meisten unfertigen / korrupten Uploads erwischen, ist aber nicht zu gebrauchen, um einen Prüfdurchgang nach einem Turbo-Upload zu machen\\\">date-chk\",\n\n\t\"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)\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cut_nag\": \"Benachrichtigung über das Betriebssystem abgeben, wenn Upload fertig ist$N(nur wenn Browser oder Tab nicht im Vordergrund ist)\",\n\t\"cut_sfx\": \"Spielt ein Ton ab, wenn Upload fertig ist$N(nur wenn Browser oder Tab nicht im Vordergrund ist)\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cft_text\": \"Favicon Text (leer lassen und neuladen zum Deaktivieren)\",\n\t\"cft_fg\": \"Vordergrundfarbe\",\n\t\"cft_bg\": \"Hintergrundfarbe\",\n\n\t\"cdt_lim\": \"max. Anz. Dateien, die in einem Ordner gezeigt werden sollen\",\n\t\"cdt_ask\": \"beim Runterscrollen nach $NAktion fragen statt mehr,$NDateien zu laden\",\n\t\"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\",\n\t\"cdt_ren\": \"spezielles Rechtsklick-Menü aktivieren, das Browser-Menü ist weiterhin mit Shift + Rechtsklick erreichbar\\\">aktivieren\",\n\t\"cdt_rdb\": \"normales Rechtsklick-Menü anzeigen, wenn das benutzerdefinierte bereits offen ist und erneut rechts geklickt wird\\\">x2\", //m\n\n\t\"tt_entree\": \"Navpane anzeigen (Ordnerbaum Sidebar)$NHotkey: B\",\n\t\"tt_detree\": \"Breadcrumbs anzeigen$NHotkey: B\",\n\t\"tt_visdir\": \"zu ausgewähltem Ordner scrollen\",\n\t\"tt_ftree\": \"zw. Ordnerbaum / Textdateien wechseln$NHotkey: V\",\n\t\"tt_pdock\": \"übergeordnete Ordner in einem angedockten Fenster oben anzeigen\",\n\t\"tt_dynt\": \"autom. wachsen wenn Baum wächst\",\n\t\"tt_wrap\": \"Zeilenumbruch\",\n\t\"tt_hover\": \"Beim Hovern überlange Zeilen anzeigen$N(Scrollen funktioniert nicht ausser $N&nbsp; Cursor ist im linken Gutter)\",\n\n\t\"ml_pmode\": \"am Ende des Ordners...\",\n\t\"ml_btns\": \"cmds\",\n\t\"ml_tcode\": \"transcodieren\",\n\t\"ml_tcode2\": \"transcodieren zu\",\n\t\"ml_tint\": \"färben\",\n\t\"ml_eq\": \"Audio Equalizer\",\n\t\"ml_drc\": \"Dynamic Range Compressor\",\n\t\"ml_ss\": \"Stille Überspringen\", //m\n\n\t\"mt_loop\": \"Song wiederholen\\\">🔁\",\n\t\"mt_one\": \"Wiedergabe nach diesem Song beenden\\\">1️⃣\",\n\t\"mt_shuf\": \"Zufällige Wiedergabe im Ordner\\\">🔀\",\n\t\"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▶\",\n\t\"mt_preload\": \"nächsten Titel gegen Ende vorladen für nahtlose Wiedergabe\\\">Vorladen\",\n\t\"mt_prescan\": \"vor Ende des letzten Titels zum nächsten Ordner wechseln,$Ndamit der Browser die$NWiedergabe nicht stoppt\\\">Navigation\",\n\t\"mt_fullpre\": \"versuchen, den gesamten Titel vorzuladen;$N✅ bei <b>unzuverlässiger</b> Verbindung aktivieren,$N❌ bei langsamer Verbindung deaktivieren\\\">vollst&auml;ndig\",\n\t\"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)\\\">☕️\",\n\t\"mt_waves\": \"Wellenform-Suchleiste:$NAudio-Amplitude in der Leiste anzeigen\\\">~s\",\n\t\"mt_npclip\": \"Buttons zum Kopieren des aktuellen Titels anzeigen\\\">/np\",\n\t\"mt_m3u_c\": \"Buttons zum Kopieren der$Nausgewählten Titel als m3u8-Wiedergabeliste anzeigen\\\">📻\",\n\t\"mt_octl\": \"OS-Integration (Media-Hotkeys/OSD)\\\">os-ctl\",\n\t\"mt_oseek\": \"Suchen via OS-Integration erlauben$N$NHinweis: auf einigen Geräten (iPhones)$Nersetzt dies den nächsten-Titel-Button\\\">Suchen\",\n\t\"mt_oscv\": \"Albumcover in OSD anzeigen\\\">Cover\",\n\t\"mt_follow\": \"den spielenden Titel im Blick behalten\\\">🎯\",\n\t\"mt_compact\": \"kompakte Steuerelemente\\\">⟎\",\n\t\"mt_uncache\": \"Cache leeren &nbsp;(probier das, wenn dein Browser$Neine defekte Kopie eines Titels zwischenspeichert und sich weigert, ihn abzuspielen)\\\">Cache leeren\",\n\t\"mt_mloop\": \"offenen Ordner wiederholen\\\">🔁 Schleife\",\n\t\"mt_mnext\": \"nächsten Ordner laden und fortfahren\\\">📂 nächster\",\n\t\"mt_mstop\": \"Wiedergabe beenden\\\">⏸ Stop\",\n\t\"mt_cflac\": \"FLAC / WAV zu {0} konvertierebn\\\">flac\",\n\t\"mt_caac\": \"AAC / M4A zu {0} konvertieren\\\">aac\",\n\t\"mt_coth\": \"Convertiere alle Dateien (die nicht MP3 sind) zu {0}\\\">oth\",\n\t\"mt_c2opus\": \"Beste Wahl für Desktops, Laptops, Android\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, für iOS 17.5 und neuer\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, für iOS 11 bis 17\\\">caf\",\n\t\"mt_c2mp3\": \"benutze dieses Format für ältere Geräte\\\">mp3\",\n\t\"mt_c2flac\": \"beste Klangqualität, aber riesige Downloads\\\">flac\",\n\t\"mt_c2wav\": \"unkomprimierte Wiedergabe (noch größer)\\\">wav\",\n\t\"mt_c2ok\": \"Gute Wahl, Chef!\",\n\t\"mt_c2nd\": \"Das ist nicht das empfohlene Ausgabeformat für dein Gerät, aber passt schon\",\n\t\"mt_c2ng\": \"Dein Gerät scheint dieses Ausgabeformat nicht zu unterstützen, aber lass trotzdem mal probieren\",\n\t\"mt_xowa\": \"Es gibt Bugs in iOS, die die Hintergrund-Wiedergabe mit diesem Format verhindern; bitte nutze caf oder mp3 stattdessen\",\n\t\"mt_tint\": \"Hintergrundlevel (0-100) auf der Seekbar$Num Buffern weniger ablenkend zu machen\",\n\t\"mt_eq\": \"`Aktiviert Equalizer und Lautstärkeregelung;$N$Nboost `0` = Standard 100% Lautstärke (unverändert)$N$Nwidth `1 &nbsp;` = Standard Stereo (unverändert)$Nwidth `0.5` = 50% Links-Rechts-Crossfeed$Nwidth `0 &nbsp;` = Mono$N$Nboost `-0.8` &amp; 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\",\n\t\"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)\",\n\t\"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\n\t\"mt_ssvt\": \"Lautstärkeschwelle (0-255)\\\">laut\", //m\n\t\"mt_ssts\": \"Aktiv-Schwelle (% Titel, Anfang)\\\">anf\", //m\n\t\"mt_sste\": \"Aktiv-Schwelle (% Titel, Ende)\\\">end\", //m\n\t\"mt_sssm\": \"Wiedergabegeschwindigkeits-Multiplikator\\\">vor\", //m\n\n\t\"mb_play\": \"Abspielen\",\n\t\"mm_hashplay\": \"Diese Audiodatei abspielen?\",\n\t\"mm_m3u\": \"Drücke <code>Eingabe/OK</code> zum Abspielen\\nDrücke <code>ESC/Abbrechen</code> zum Bearbeiten\",\n\t\"mp_breq\": \"Benötigt Firefox 82+ oder Chrome 73+ oder iOS 15+\",\n\t\"mm_bload\": \"Lädt...\",\n\t\"mm_bconv\": \"Konvertiere zu {0}, bitte warte...\",\n\t\"mm_opusen\": \"Dein Browser kann AAC- / M4A-Dateien nicht abspielen;\\nUmwandlung zu Opus ist jetzt aktiv\",\n\t\"mm_playerr\": \"Wiedergabefehler: \",\n\t\"mm_eabrt\": \"Der Wiedergabeversuch wurde abgebrochen\",\n\t\"mm_enet\": \"Dein Internet läuft auf Edge, wa?\",\n\t\"mm_edec\": \"Die Datei scheint beschädigt zu sein??\",\n\t\"mm_esupp\": \"Dein Browser versteht dieses Audioformat nicht\",\n\t\"mm_eunk\": \"Unbekannter Fehler\",\n\t\"mm_e404\": \"Konnte Datei nicht abspielen; Fehler 404: Datei nicht gefunden.\",\n\t\"mm_e403\": \"Konnte Datei nicht abspielen; Fehler 403: Zugriff verweigert.\\n\\nDrücke F5 zum Neuladen, vielleicht wurdest du abgemeldet\",\n\t\"mm_e415\": \"Konnte Datei nicht abspielen; Fehler 415: Umwandlung der Datei fehlgeschlagen; Serverlogs prüfen.\", //m\n\t\"mm_e500\": \"Konnte Datei nicht abspielen; Fehler 500: Prüfe die Serverlogs.\",\n\t\"mm_e5xx\": \"Konnte Datei nicht abspielen; Server Fehler \",\n\t\"mm_nof\": \"finde keine weiteren Audiodateien in der Nähe\",\n\t\"mm_prescan\": \"Suche nach Musik zum Abspielen...\",\n\t\"mm_scank\": \"Nächster Song gefunden:\",\n\t\"mm_uncache\": \"Cache geleert; Alle Songs werden beim nächsten Abspielversuch neu heruntergeladen\",\n\t\"mm_hnf\": \"dieser Song existiert nicht mehr\",\n\n\t\"im_hnf\": \"dieses Bild existiert nicht mehr\",\n\n\t\"f_empty\": 'Dieser Ordner ist leer',\n\t\"f_chide\": 'Dies blendet die Spalte «{0}» aus\\n\\nDu kannst Spalten in den Einstellungen wieder einblenden.',\n\t\"f_bigtxt\": \"Diese Datei ist {0} MiB gross -- Sicher, dass du sie als Text anzeigen willst?\",\n\t\"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\",\n\t\"fbd_more\": '<div id=\"blazy\">zeige <code>{0}</code> von <code>{1}</code> Dateien; <a href=\"#\" id=\"bd_more\">{2} anzeigen</a> oder <a href=\"#\" id=\"bd_all\">alle anzeigen</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\">zeige <code>{0}</code> von <code>{1}</code> Dateien; <a href=\"#\" id=\"bd_all\">alle anzeigen</a></div>',\n\t\"f_anota\": \"nur {0} der {1} Elemente wurden ausgewählt;\\num den gesamten Ordner auszuwählen, zuerst nach unten scrollen\",\n\n\t\"f_dls\": 'die Dateilinks im aktuellen Ordner wurden\\nin Downloadlinks geändert',\n\t\"f_dl_nd\": 'ordner wird übersprungen (bitte zip/tar-download verwenden):\\n', //m\n\n\t\"f_partial\": \"Um eine Datei sicher herunterzuladen, die gerade hochgeladen wird, klicke bitte die Datei mit dem gleichen Namen, aber ohne die <code>.PARTIAL</code>-Endung. Bitte drücke Abbrechen oder Escape, um dies zu tun.\\n\\nWenn du auf OK / Eingabe drückst, ignorierst du diese Warnung und lädst die <code>.PARTIAL</code>-Datei herunter, die ziemlich sicher beschädigte Daten enthält.\",\n\n\t\"ft_paste\": \"{0} Elemente einfügen$NHotkey: STRG-V\",\n\t\"fr_eperm\": 'Umbenennen fehlgeschlagen:\\nDir fehlt die \"Verschieben\"-Berechtigung in diesem Ordner',\n\t\"fd_eperm\": 'Löschen fehlgeschlagen:\\nDir fehlt die \"Löschen\"-Berechtigung in diesem Ordner',\n\t\"fc_eperm\": 'Ausschneiden fehlgeschlagen:\\nDir fehlt die \"Verschieben\"-Berechtigung in diesem Ordner',\n\t\"fp_eperm\": 'Einfügen fehlgeschlagen:\\nDir fehlt die \"Schreiben\"-Berechtigung in diesem Ordner',\n\t\"fr_emore\": \"Wähle mindestens ein Element zum Umbenennen aus\",\n\t\"fd_emore\": \"Wähle mindestens ein Element zum Löschen aus\",\n\t\"fc_emore\": \"Wähle mindestens ein Element zum Ausschneiden aus\",\n\t\"fcp_emore\": \"Wähle mindestens ein Element aus, um es in die Zwischenablage zu kopieren\",\n\n\t\"fs_sc\": \"Teile diesen Ordner\",\n\t\"fs_ss\": \"Teile die ausgewählten Dateien\",\n\t\"fs_just1d\": \"Du kannst nicht mehrere Ordner auswählen \\noder Dateien und Ordner in der Auswahl mischen.\",\n\t\"fs_abrt\": \"❌ Abbrechen\",\n\t\"fs_rand\": \"🎲 Zufallsname\",\n\t\"fs_go\": \"✅ Share erstellen\",\n\t\"fs_name\": \"Name\",\n\t\"fs_src\": \"Quelle\",\n\t\"fs_pwd\": \"Passwort\",\n\t\"fs_exp\": \"Ablauf\",\n\t\"fs_tmin\": \"Minuten\",\n\t\"fs_thrs\": \"Stunden\",\n\t\"fs_tdays\": \"Tage\",\n\t\"fs_never\": \"nie\",\n\t\"fs_pname\": \"optionaler Linkname; zufällig wenn leer\",\n\t\"fs_tsrc\": \"zu teilende Datei oder Ordner\",\n\t\"fs_ppwd\": \"optionales Passwort\",\n\t\"fs_w8\": \"erstelle Share...\",\n\t\"fs_ok\": \"drücke <code>Eingabe/OK</code> für Zwischenablage\\ndrücke <code>ESC/Abbrechen</code> zum Schliessen\",\n\n\t\"frt_dec\": \"Kann Fälle von beschädigten Dateien beheben\\\">url-decode\",\n\t\"frt_rst\": \"Geänderte Dateinamen auf Orginale zurücksetzen\\\">↺ zurücksetzen\",\n\t\"frt_abrt\": \"Abbrechen und dieses Fenster schliessen\\\">❌ abbrechen\",\n\t\"frb_apply\": \"ÜBERNEHMEN\",\n\t\"fr_adv\": \"Stapel-/Metadaten-/Musterumbenennung\\\">erweitert\",\n\t\"fr_case\": \"Groß-/Kleinschreibung beachten (Regex)\\\">Großschreibung\",\n\t\"fr_win\": \"Windows-kompatible Namen; ersetzt <code>&lt;&gt;:&quot;\\\\|?*</code> durch japanische Fullwidth-Zeichen\\\">win\",\n\t\"fr_slash\": \"Ersetzt <code>/</code> durch ein Zeichen, das keine neuen Ordner erstellt\\\">no /\",\n\t\"fr_re\": \"`Regex-Suchmuster für Originaldateinamen; Erfassungsgruppen können im Formatfeld unten als `(1)` und `(2)` usw. referenziert werden\",\n\t\"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\",\n\t\"fr_pdel\": \"Löschen\",\n\t\"fr_pnew\": \"Speichern als\",\n\t\"fr_pname\": \"Gib der Vorlage einen Namen\",\n\t\"fr_aborted\": \"Abgebrochen\",\n\t\"fr_lold\": \"Alter Name\",\n\t\"fr_lnew\": \"Neuer Name\",\n\t\"fr_tags\": \"Tags für die ausgewählten Dateien (liest nur, als Referenz):\",\n\t\"fr_busy\": \"Benenne {0} Elemente um...\\n\\n{1}\",\n\t\"fr_efail\": \"Umbenennen fehlgeschlagen:\\n\",\n\t\"fr_nchg\": \"{0} der neuen Namen wurden angepasst durch <code>win</code> und/oder <code>no /</code>\\n\\nMöchtest du mit diesen geänderten Namen fortfahren?\",\n\n\t\"fd_ok\": \"Löschen OK\",\n\t\"fd_err\": \"Löschen fehlgeschlagen:\\n\",\n\t\"fd_none\": \"Nichts würde gelöscht; vielleicht durch die Serverkonfiguration blockiert (xbd)?\",\n\t\"fd_busy\": \"Lösche {0} Elemente...\\n\\n{1}\",\n\t\"fd_warn1\": \"Diese {0} Elemente LÖSCHEN?\",\n\t\"fd_warn2\": \"<b>Ich frage das letzte Mal!</b> Was weg ist, ist weg. Keine Chance, das rückgängig zu machen. Löschen?\",\n\n\t\"fc_ok\": \"{0} Elemente ausgeschnitten\",\n\t\"fc_warn\": '{0} Elemente in die Zwischenablage kopiert\\n\\nAber: nur <b>dieses</b> Browsertab kann sie einfügen\\n(da deine Auswahl so abartig riesig war)',\n\n\t\"fcc_ok\": \"{0} Elemente in die Zwischenablage kopiert\",\n\t\"fcc_warn\": '{0} Elemente in die Zwischenablage kopiert\\n\\nAber: nur <b>dieses</b> Browsertab kann sie einfügen\\n(da deine Auswahl so abartig riesig war)',\n\n\t\"fp_apply\": \"Diese Namen verwenden\",\n\t\"fp_skip\": \"Konflikte überspringen\",\n\t\"fp_ecut\": \"Kopiere erst ein paar Dateien / Ordner, um sie einzufügen\\n\\nTipp: Ausschneiden und Kopieren funktioniert über Browsertabs hinweg\",\n\t\"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):',\n\t\"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):',\n\t\"fp_emore\": \"Es gibt noch ein paar Dateinamen, die geändert werden müssen\",\n\t\"fp_ok\": \"Verschieben OK\",\n\t\"fcp_ok\": \"Kopieren OK\",\n\t\"fp_busy\": \"Verschiebe {0} Elemente...\\n\\n{1}\",\n\t\"fcp_busy\": \"Kopiere {0} Elemente...\\n\\n{1}\",\n\t\"fp_abrt\": \"wird abgebrochen...\",\n\t\"fp_err\": \"Verschieben fehlgeschlagen:\\n\",\n\t\"fcp_err\": \"Kopieren fehlgeschlagen:\\n\",\n\t\"fp_confirm\": \"Diese {0} Elemente hierher verschieben?\",\n\t\"fcp_confirm\": \"Diese {0} Elemente hierher kopieren?\",\n\t\"fp_etab\": 'Konnte die Zwischenablage nicht vom anderen Browsertab lesen',\n\t\"fp_name\": \"Lade Datei von deinem Gerät hoch. Gib ihr einen Namen:\",\n\t\"fp_both_m\": '<h6>Wähle, was eingefügt werden soll</h6><code>Eingabe</code> = {0} Dateien von «{1}» verschieben\\n<code>ESC</code> = {2} Dateien von deinem Gerät hochladen',\n\t\"fcp_both_m\": '<h6>Wähle, was eingefügt werden soll</h6><code>Eingabe</code> = {0} Dateien von «{1}» kopieren\\n<code>ESC</code> = {2} Dateien von deinem Gerät hochladen',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Verschieben</a><a href=\"#\" id=\"modal-ng\">Hochladen</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Kopieren</a><a href=\"#\" id=\"modal-ng\">Hochladen</a>',\n\n\t\"mk_noname\": \"Tipp' mal vorher lieber einen Namen in das Textfeld links, bevor du das machst :p\",\n\t\"nmd_i1\": \"Füge auch die Dateiendung hinzu, z.B. <code>.md</code>\",\n\t\"nmd_i2\": \"Du kannst nur <code>.{0}</code>-Dateien erstellen, da dir Lösch-Rechte fehlen\",\n\n\t\"tv_load\": \"Textdatei wird geladen:\\n\\n{0}\\n\\n{1}% ({2} von {3} MiB geladen)\",\n\t\"tv_xe1\": \"Konnte Textdatei nicht laden:\\n\\nFehler \",\n\t\"tv_xe2\": \"404, Datei nicht gefunden\",\n\t\"tv_lst\": \"Liste der Textdateien in\",\n\t\"tvt_close\": \"Zu Ordneransicht zurück$NHotkey: M (oder Esc)\\\">❌ Schliessen\",\n\t\"tvt_dl\": \"Diese Datei herunterladen$NHotkey: Y\\\">💾 Herunterladen\",\n\t\"tvt_prev\": \"Vorheriges Dokument zeigen$NHotkey: i\\\">⬆ vorh.\",\n\t\"tvt_next\": \"Nächstes Dokument zeigen$NHotkey: K\\\">⬇ nächst.\",\n\t\"tvt_sel\": \"Wählt diese Datei aus &nbsp; ( zum Ausschneiden / Kopieren / Löschen / ... )$NHotkey: S\\\">ausw.\",\n\t\"tvt_j\": \"json verschönern$NHotkey: shift-J\\\">j\",\n\t\"tvt_edit\": \"Datei im Texteditor zum Bearbeiten öffnen$NHotkey: E\\\">✏️ bearb.\",\n\t\"tvt_tail\": \"Datei auf Veränderungen überwachen; Neue Zeilen werden in Echtzeit angezeigt\\\">📡 folgen\",\n\t\"tvt_wrap\": \"Zeilenumbruch\\\">↵\",\n\t\"tvt_atail\": \"Automatisch nach unten scrollen\\\">⚓\",\n\t\"tvt_ctail\": \"Terminal-Farben dekodieren (ANSI Escape Codes)\\\">🌈\",\n\t\"tvt_ntail\": \"Scrollback limitieren (Menge an Bytes an Text, die geladen bleiben sollen)\",\n\n\t\"m3u_add1\": \"Song wurde zur M3U-Playlist hinzugefügt\",\n\t\"m3u_addn\": \"{0} Songs zur M3U-Playlist hinzugefügt\",\n\t\"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\",\n\n\t\"gt_vau\": \"nur Ton abspielen, kein Video zeigen\\\">🎧\",\n\t\"gt_msel\": \"Dateiauswahl aktivieren; STRG-klicke eine Datei zum überschreiben$N$N&lt;em&gt;wenn aktiv: Datei / Ordner doppelklicken zum Öffnen&lt;/em&gt;$N$NHotkey: S\\\">multiselect\",\n\t\"gt_crop\": \"Vorschaubilder mittig zuschneiden\\\">crop\",\n\t\"gt_3x\": \"hochauflösende Vorschaubilder\\\">3x\",\n\t\"gt_zoom\": \"zoom\",\n\t\"gt_chop\": \"kürzen\",\n\t\"gt_sort\": \"sortieren nach\",\n\t\"gt_name\": \"Name\",\n\t\"gt_sz\": \"Grösse\",\n\t\"gt_ts\": \"Datum\",\n\t\"gt_ext\": \"Typ\",\n\t\"gt_c1\": \"Dateinamen mehr kürzen (weniger zeigen)\",\n\t\"gt_c2\": \"Dateinamen weniger kürzen (mehr zeigen)\",\n\n\t\"sm_w8\": \"Suche ...\",\n\t\"sm_prev\": \"Die Suchresultate gehören zu einer vorherigen Suchanfrage:\\n  \",\n\t\"sl_close\": \"Suchresultate schliessen\",\n\t\"sl_hits\": \"Zeige {0} Treffer\",\n\t\"sl_moar\": \"Mehr laden\",\n\n\t\"s_sz\": \"Grösse\",\n\t\"s_dt\": \"Datum\",\n\t\"s_rd\": \"Pfad\",\n\t\"s_fn\": \"Name\",\n\t\"s_ta\": \"Tags\",\n\t\"s_ua\": \"up@\",\n\t\"s_ad\": \"adv.\",\n\t\"s_s1\": \"minimum MiB\",\n\t\"s_s2\": \"maximum MiB\",\n\t\"s_d1\": \"min. iso8601\",\n\t\"s_d2\": \"max. iso8601\",\n\t\"s_u1\": \"hochgeladen nach\",\n\t\"s_u2\": \"und/oder vor\",\n\t\"s_r1\": \"Pfad enthält &nbsp; (Leerzeichen-separiert)\",\n\t\"s_f1\": \"Name enthält &nbsp; (negieren mit -nope)\",\n\t\"s_t1\": \"Tags enthält &nbsp; (^=start, end=$)\",\n\t\"s_a1\": \"spezifische Metadaten-Eigenschaften\",\n\n\t\"md_eshow\": \"Kann nicht rendern \",\n\t\"md_off\": \"[📜<em>readme</em>] deaktiviert in [⚙️] -- Dokument versteckt\",\n\n\t\"badreply\": \"Hab die Antwort vom Server nicht verstanden. (badreply)\",\n\n\t\"xhr403\": \"403: Zugriff verweigert\\n\\nVersuche, F5 zu drücken. Vielleicht wurdest du abgemeldet.\",\n\t\"xhr0\": \"Unbekannt (wahrschenlich Verbindung zum Server verloren oder der Server ist offline)\",\n\t\"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\",\n\t\"tl_xe1\": \"Konnte Unterordner nicht auflisten:\\n\\nFehler \",\n\t\"tl_xe2\": \"404: Ordner nicht gefunden\",\n\t\"fl_xe1\": \"Konnte Dateien in Ordner nicht auflisten:\\n\\nFehler \",\n\t\"fl_xe2\": \"404: Ordner nicht gefunden\",\n\t\"fd_xe1\": \"Konnte Unterordner nicht erstellen:\\n\\nFehler \",\n\t\"fd_xe2\": \"404: Übergeordneter Ordner nicht gefunden\",\n\t\"fsm_xe1\": \"Konnte Nachricht nicht senden:\\n\\nFehler \",\n\t\"fsm_xe2\": \"404: Übergeordneter Ordner nicht gefunden\",\n\t\"fu_xe1\": \"Konnte unpost-Liste nicht laden:\\n\\nFehler \",\n\t\"fu_xe2\": \"404: Datei nicht gefunden??\",\n\n\t\"fz_tar\": \"Unkomprimierte GNU TAR-Datei (Linux / Mac)\",\n\t\"fz_pax\": \"Unkomprimierte pax-Format TAR-Datei (langsamer)\",\n\t\"fz_targz\": \"GNU-TAR mit gzip Level 3 Kompression$N$Nüblicherweise recht langsam,$Nbenutze stattdessen ein unkomprimiertes TAR\",\n\t\"fz_tarxz\": \"GNU-TAR mit xz level 1 Kompression$N$Nüblicherweise recht langsam,$Nbenutze stattdessen ein unkomprimiertes TAR\",\n\t\"fz_zip8\": \"ZIP mit UTF8-Dateinamen (könnte kaputt gehen auf Windows 7 oder älter)\",\n\t\"fz_zipd\": \"ZIP mit traditionellen CP437-Dateinamen, für richtig alte Software\",\n\t\"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)\",\n\n\t\"un_m1\": \"Unten kannst du deine neusten Uploads löschen (oder unvollständige abbrechen)\",\n\t\"un_upd\": \"Neu laden\",\n\t\"un_m4\": \"Oder die unten sichtbaren Dateien teilen:\",\n\t\"un_ulist\": \"Anzeigen\",\n\t\"un_ucopy\": \"Kopieren\",\n\t\"un_flt\": \"Optionale Filter:&nbsp; URL muss enthalten\",\n\t\"un_fclr\": \"Filter löschen\",\n\t\"un_derr\": 'unpost-delete fehlgeschlagen:\\n',\n\t\"un_f5\": 'Etwas ist kaputt gegangen, versuche die Seite neuzuladen (drücke dazu F5)',\n\t\"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\",\n\t\"un_nou\": '<b>Warnung:</b> Der Server ist grade zu beschäftigt, um unvollständige Uploads anzuzeigen; Drücke den \"Neu laden\"-Link in ein paar Sekunden',\n\t\"un_noc\": '<b>Warnung:</b> unpost von vollständig hochgeladenen Dateien ist über die Serverkonfiguration gesperrt',\n\t\"un_max\": \"Zeige die ersten 2000 Dateien (benutze Filter, um die gewünschten Dateien zu finden)\",\n\t\"un_avail\": \"{0} zuletzt hochgeladene Dateien können gelöscht werden<br />{1} Unvollständige können abgebrochen werden\",\n\t\"un_m2\": \"Sortiert nach Upload-Zeitpunkt; neuste zuerst:\",\n\t\"un_no1\": \"Hoppala! Es gibt keine ausreichend aktuellen Uploads.\",\n\t\"un_no2\": \"Pech gehabt! Kein Upload, der zu dem Filter passen würde, ist neu genug\",\n\t\"un_next\": \"Lösche die nächsten {0} Dateien\",\n\t\"un_abrt\": \"Abbrechen\",\n\t\"un_del\": \"Löschen\",\n\t\"un_m3\": \"Deine letzten Uploads werden geladen ...\",\n\t\"un_busy\": \"Lösche {0} Dateien ...\",\n\t\"un_clip\": \"{0} Links in die Zwischenablage kopiert\",\n\n\t\"u_https1\": \"für bessere Performance solltest du\",\n\t\"u_https2\": \"auf HTTPS wechseln\",\n\t\"u_https3\": \" \",\n\t\"u_ancient\": 'Dein Browser ist verdammt antik -- vielleicht solltest du <a href=\"#\" onclick=\"goto(\\'bup\\')\">stattdessen bup benutzen</a>',\n\t\"u_nowork\": \"Benötigt Firefox 53+ oder Chrome 57+ oder iOS 11+\",\n\t\"tail_2old\": \"Benötigt Firefox 105+ oder Chrome 71+ oder iOS 14.5+\",\n\t\"u_nodrop\": 'Dein Browser ist zu alt für Drag-and-Drop Uploads',\n\t\"u_notdir\": \"Das ist kein Ordner!\\n\\nDein Browser ist zu alt,\\nversuch stattdessen dragdrop\",\n\t\"u_uri\": \"Um Bilder per Drag-and-Drop aus anderen Browserfenstern hochzuladen,\\nlass' sie bitte über dem grossen Upload-Button fallen\",\n\t\"u_enpot\": 'Zu <a href=\"#\">Potato UI</a> wechseln (kann Upload-Geschw. verbessern)',\n\t\"u_depot\": 'Zu <a href=\"#\">fancy UI</a> wechseln (kann Upload-Geschw. verschlechtern)',\n\t\"u_gotpot\": 'Wechsle zu Potato UI für verbesserte Upload-Geschwindigkeit,\\n\\nwenn du anderer Meinung bist, kannst du gerne zurück wechseln',\n\t\"u_pott\": \"<p>Dateien: &nbsp; <b>{0}</b> fertig, &nbsp; <b>{1}</b> fehlgeschlagen, &nbsp; <b>{2}</b> in Bearbeitung, &nbsp; <b>{3}</b> ausstehend</p>\",\n\t\"u_ever\": \"Dies ist der Basic Uploader; up2k benötigt mind. <br>Chrome 21 // Firefox 13 // Edge 12 // Opera 12 // Safari 5.1\",\n\t\"u_su2k\": 'Dies ist der Basic Uploader; <a href=\"#\" id=\"u2yea\">up2k</a> ist besser',\n\t\"u_uput\": 'Für Geschwindigkeit optimieren (Checksum überspringen)',\n\t\"u_ewrite\": 'Du hast kein Schreibzugriff auf diesen Ordner',\n\t\"u_eread\": 'Du hast kein Lesezugriff auf diesen Ordner',\n\t\"u_enoi\": 'file-search ist in der Serverkonfiguration nicht aktiviert',\n\t\"u_enoow\": \"Überschreiben wird hier nicht funktionieren; benötige Lösch-Berechtigung\",\n\t\"u_badf\": 'Diese {0} Dateien (von insgesammt {1}) wurden übersprungen, wahrscheinlich wegen Dateisystem-Berechtigungen:\\n\\n',\n\t\"u_blankf\": 'Diese {0} Dateien (von insgesammt {1}) sind leer; trotzdem hochladen?\\n\\n',\n\t\"u_applef\": 'Diese {0} Dateien (von insgesammt {1}) sind möglicherweise unerwünscht;\\n<code>OK/Eingabe</code> drücken, um die folgenden Dateien zu überspringen.\\nDrücke <code>Abbrechen/ESC</code> um sie NICHT zu überspringen und diese AUCH HOCHZULADEN:\\n\\n',\n\t\"u_just1\": '\\nFunktioniert vielleicht besser, wenn du nur eine Datei auswählst',\n\t\"u_ff_many\": \"Falls du <b>Linux / MacOS / Android</b> benutzt, <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\"><em>könnte</em> Firefox mit dieser Menge an Dateien crashen!</a>\\nFalls das passiert, probier nochmal (oder benutz Chrome).\",\n\t\"u_up_life\": \"Dieser Upload wird vom Server gelöscht\\n{0} nachdem er abgeschlossen ist\",\n\t\"u_asku\": 'Diese {0} Dateien nach <code>{1}</code> hochladen',\n\t\"u_unpt\": \"Du kannst diesen Upload rückgängig machen mit dem 🧯 oben-links\",\n\t\"u_bigtab\": 'Versuche {0} Dateien anzuzeigen.\\n\\nDas könnte dein Browser crashen, bist du dir wirklich sicher?',\n\t\"u_scan\": 'Scanne Dateien...',\n\t\"u_dirstuck\": 'Ordner-Iterator blieb hängen beim Versuch, diese {0} Einträge zu lesen; überspringe:',\n\t\"u_etadone\": 'Fertig ({0}, {1} Dateien)',\n\t\"u_etaprep\": '(Upload wird vorbereitet)',\n\t\"u_hashdone\": 'Hashing vollständig',\n\t\"u_hashing\": 'Hash',\n\t\"u_hs\": 'Wir schütteln uns die Hände (\"handshaking\")...',\n\t\"u_started\": \"Dateien werden hochgeladen; siehe [🚀]\",\n\t\"u_dupdefer\": \"Duplikat; wird nach allen anderen Dateien verarbeitet\",\n\t\"u_actx\": \"Klicke diesen Text um Performance-<br />Einbusen zu Vermeiden beim Wechsel auf andere Fenster/Tabs\",\n\t\"u_fixed\": \"OK!&nbsp; Habs repariert 👍\",\n\t\"u_cuerr\": \"failed to upload chunk {0} of {1};\\nprobably harmless, continuing\\n\\nfile: {2}\",\n\t\"u_cuerr2\": \"server rejected upload (chunk {0} of {1});\\nwill retry later\\n\\nfile: {2}\\n\\nerror \",\n\t\"u_ehstmp\": \"versuche nochmal; siehe unten-rechts\",\n\t\"u_ehsfin\": \"Der Server hat die Anfrage zum Abschluss des Uploads abgelehnt; versuche nochmal...\",\n\t\"u_ehssrch\": \"Der Server hat die Anfrage zur Suche abgelehnt; versuche nochmal...\",\n\t\"u_ehsinit\": \"Der Server hat die Anfrage zum Start des Uploads abgelehnt; versuche nochmal...\",\n\t\"u_eneths\": \"Netzwerkfehler beim Upload-Handshake; versuche nochmal...\",\n\t\"u_enethd\": \"Netzwerkfehler beim Testen der Existenz des Ziels; versuche nochmal...\",\n\t\"u_cbusy\": \"Der Server mag uns grade nicht mehr nach einem Netzwerkglitch, warte einen Moment...\",\n\t\"u_ehsdf\": \"Server hat kein Speicherplatz mehr!\\n\\nwerde es erneut versuchen, falls jemand\\ngenug Platz schafft um fortzufahren\",\n\t\"u_emtleak1\": \"scheint, als ob dein Browser ein Memory Leak hätte;\\nbitte\",\n\t\"u_emtleak2\": ' <a href=\"{0}\">wechsle auf HTTPS (empfohlen)</a> oder ',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": 'versuche folgendes:\\n<ul><li>drücke <code>F5</code> um die Seite neu zu laden</li><li>deaktivere dann den &nbsp;<code>mt</code>&nbsp; Button in den &nbsp;<code>⚙️ Einstellungen</code></li><li>und versuche den Upload nochmal.</li></ul>Uploads werden etwas langsamer sein, aber man kann ja nicht alles haben.\\nSorry für die Umstände !\\n\\nPS: Chrome v107 <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">hat ein Bugfix</a> dafür',\n\t\"u_emtleakf\": 'versuche folgendes:\\n<ul><li>drücke <code>F5</code> um die Seite neu zu laden</li><li>aktivere dann <code>🥔</code> (potato) im Upload UI<li>und versuche den Upload nochmal</li></ul>\\nPS: Firefox <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">hat hoffentlich irgendwann ein Bugfix</a>',\n\t\"u_s404\": \"nicht auf dem Server gefunden\",\n\t\"u_expl\": \"erklären\",\n\t\"u_maxconn\": \"die meisten Browser limitieren dies auf 6, aber Firefox lässt mehr zu unter <code>connections-per-server</code> in <code>about:config</code>\",\n\t\"u_tu\": '<p class=\"warn\">WARNUNG: Turbo aktiviert, <span>&nbsp;Client könnte unvollständige Uploads verpassen und nicht wiederholen; siehe Turbo-Button Tooltip</span></p>',\n\t\"u_ts\": '<p class=\"warn\">WARNUNG: Turbo aktiviert, <span>&nbsp;Suchresultate können inkorrekt sein; siehe Turbo-Button Tooltip</span></p>',\n\t\"u_turbo_c\": \"Turbo deaktiviert in der Serverkonfiguration\",\n\t\"u_turbo_g\": \"Turbo deaktiviert, da du keine Listen-Berechtigung\\nauf diesem Volume hast\",\n\t\"u_life_cfg\": 'Autodelete nach <input id=\"lifem\" p=\"60\" /> min (or <input id=\"lifeh\" p=\"3600\" /> h)',\n\t\"u_life_est\": 'Upload wird gelöscht <span id=\"lifew\" tt=\"local time\">---</span>',\n\t\"u_life_max\": 'Dieser Ordner erzwingt eine\\nmax Lebensdauer von {0}',\n\t\"u_unp_ok\": 'unpost ist erlaubt für {0}',\n\t\"u_unp_ng\": 'unpost wird NICHT erlaubt',\n\t\"ue_ro\": 'Du hast nur Lese-Zugriff auf diesen Ordner\\n\\n',\n\t\"ue_nl\": 'Du bist nicht angemeldet',\n\t\"ue_la\": 'Du bist angemeldet als \"{0}\"',\n\t\"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',\n\t\"ue_ta\": 'Versuche den Upload nochmal, sollte jetzt klappen',\n\t\"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\",\n\t\"ur_1uo\": \"OK: Datei erfolgreich hochgeladen\",\n\t\"ur_auo\": \"OK: Alle {0} Dateien erfolgreich hochgeladen\",\n\t\"ur_1so\": \"OK: Datei auf dem Server gefunden\",\n\t\"ur_aso\": \"OK: Alle {0} Dateien auf dem Server gefunden\",\n\t\"ur_1un\": \"Upload fehlgeschlagen, sorry\",\n\t\"ur_aun\": \"Alle {0} Uploads fehlgeschlagen, sorry\",\n\t\"ur_1sn\": \"Datei wurde NICHT auf dem Server gefunden\",\n\t\"ur_asn\": \"Die {0} Dateien wurden NICHT auf dem Server gefunden\",\n\t\"ur_um\": \"Fertig;\\n{0} Uploads OK,\\n{1} Uploads fehlgeschlagen, sorry\",\n\t\"ur_sm\": \"Fertig;\\n{0} Uploads gefunden auf dem Server,\\n{1} Dateien NICHT gefunden auf dem Server\",\n\n\t\"rc_opn\": \"öffnen\",\n\t\"rc_ply\": \"abspielen\",\n\t\"rc_pla\": \"als Audio abspielen\",\n\t\"rc_txt\": \"als Text öffnen\",\n\t\"rc_md\": \"im Texteditor öffnen\",\n\t\"rc_dl\": \"herunterladen\",\n\t\"rc_zip\": \"als Archiv herunterladen\",\n\t\"rc_cpl\": \"Link kopieren\", //m\n\t\"rc_del\": \"löschen\",\n\t\"rc_cut\": \"ausschneiden\",\n\t\"rc_cpy\": \"kopieren\",\n\t\"rc_pst\": \"einfügen\",\n\t\"rc_rnm\": \"umbenennen\", //m\n\t\"rc_nfo\": \"neuer Ordner\",\n\t\"rc_nfi\": \"neue Datei\",\n\t\"rc_sal\": \"alles auswählen\",\n\t\"rc_sin\": \"auswahl umkehren\",\n\t\"rc_shf\": \"diesen ordner teilen\", //m\n\t\"rc_shs\": \"auswahl teilen\", //m\n\n\t\"lang_set\": \"Neuladen um Änderungen anzuwenden?\",\n\n\t\"splash\": {\n\t\t\"a1\": \"Neu laden\",\n\t\t\"b1\": \"Tach, wie geht's? &nbsp; <small>(Du bist nicht angemeldet)</small>\",\n\t\t\"c1\": \"Abmelden\",\n\t\t\"d1\": \"Zustand\",\n\t\t\"d2\": \"Zeigt den Zustand aller aktiven Threads\",\n\t\t\"e1\": \"Config neu laden\",\n\t\t\"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\",\n\t\t\"f1\": \"Du kannst lesen:\",\n\t\t\"g1\": \"Du kannst hochladen nach:\",\n\t\t\"cc1\": \"Andere Dinge:\",\n\t\t\"h1\": \"k304 deaktivieren\",\n\t\t\"i1\": \"k304 aktivieren\",\n\t\t\"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\",\n\t\t\"k1\": \"Client-Einstellungen zurücksetzen\",\n\t\t\"l1\": \"Melde dich an für mehr:\",\n\t\t\"ls3\": \"Anmelden\",\n\t\t\"lu4\": \"Benutzername\",\n\t\t\"lp4\": \"Passwort\",\n\t\t\"lo3\": \"“{0}” überall abmelden\",\n\t\t\"lo2\": \"Das beendet die Sitzung in allen Browsern\",\n\t\t\"m1\": \"Willkommen zurück,\",\n\t\t\"n1\": \"404 Nicht gefunden &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": 'or maybe you don\\'t have access -- try a password or <a href=\"' + SR + '/?h\">go home</a>',\n\t\t\"p1\": \"403 Verboten &nbsp;~┻━┻\",\n\t\t\"q1\": 'Benutze ein Passwort oder <a href=\"' + SR + '/?h\">gehe zur Homepage</a>',\n\t\t\"r1\": \"Gehe zur Homepage\",\n\t\t\".s1\": \"Neu scannen\",\n\t\t\"t1\": \"Aktion\",\n\t\t\"u2\": \"time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds\",\n\t\t\"v1\": \"Verbinden\",\n\t\t\"v2\": \"Benutze diesen Server als lokale Festplatte\",\n\t\t\"w1\": \"Zu HTTPS wechseln\",\n\t\t\"x1\": \"Passwort ändern\",\n\t\t\"y1\": \"Shares bearbeiten\",\n\t\t\"z1\": \"Share entsperren:\",\n\t\t\"ta1\": \"Trage zuerst dein Passwort ein\",\n\t\t\"ta2\": \"Wiederhole dein Passwort zur Bestätigung:\",\n\t\t\"ta3\": \"Da stimmt etwas nicht; probier's nochmal\",\n\t\t\"nop\": \"FEHLER: Passwort darf nicht leer sein\",\n\t\t\"nou\": \"FEHLER: Benutzername und/oder Passwort dürfen nicht leer sein\",\n\t\t\"aa1\": \"Eingehende Dateien:\",\n\t\t\"ab1\": \"no304 deaktivieren\",\n\t\t\"ac1\": \"no304 aktivieren\",\n\t\t\"ad1\": \"Das Aktivieren von no304 deaktiviert jegliche Form von Caching; probier dies, wenn k304 nicht genug war. Dies verschwendet eine grosse Menge Netzwerk-Traffic!\",\n\t\t\"ae1\": \"Aktive Downloads:\",\n\t\t\"af1\": \"Zeige neue Uploads\",\n\t\t\"ag1\": \"Bekannte IdP-Benutzer anzeigen\", //m\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/epo.js",
    "content": "\n// Linioj, finiĝantaj per \"//m\", estas nekontrolitaj maŝinaj tradukoj\n\nLs.epo = {\n\t\"tt\": \"Esperanto\",\n\n\t\"cols\": {\n\t\t\"c\": \"ago-butonoj\",\n\t\t\"dur\": \"daŭro\",\n\t\t\"q\": \"kvalito / bitrapido\",\n\t\t\"Ac\": \"sonkodeko\",\n\t\t\"Vc\": \"videokodeko\",\n\t\t\"Fmt\": \"formato / ujo\",\n\t\t\"Ahash\": \"kontrolsumo de aŭdio\",\n\t\t\"Vhash\": \"kontrolsumo de video\",\n\t\t\"Res\": \"distingivo\",\n\t\t\"T\": \"dosiertipo\",\n\t\t\"aq\": \"kvalito / bitrapido de aŭdio\",\n\t\t\"vq\": \"kvalito / bitrapido de video\",\n\t\t\"pixfmt\": \"specimenado / strukturo de bilderoj\",\n\t\t\"resw\": \"horizontala distingivo\",\n\t\t\"resh\": \"vertikala distingivo\",\n\t\t\"chs\": \"nombro de aŭdio-kanaloj\",\n\t\t\"hz\": \"sonpecrapido\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"misc\",\n\t\t\t[\"ESK\", \"malfermi variajn aferojn\"],\n\n\t\t\t\"file-manager\",\n\t\t\t[\"G\", \"baskuli inter lista kaj krada vido\"],\n\t\t\t[\"T\", \"baskuli montradon de bildetoj\"],\n\t\t\t[\"⇧ A/D\", \"grandeco de bildetoj\"],\n\t\t\t[\"stir-K\", \"forigi elektitajn\"],\n\t\t\t[\"stir-X\", \"eltondi elektaĵon al tondujo\"],\n\t\t\t[\"stir-C\", \"kopii elektaĵon al tondujo\"],\n\t\t\t[\"stir-V\", \"alglui (movi/kopii) ĉi tien\"],\n\t\t\t[\"Y\", \"elŝuti elektitajn\"],\n\t\t\t[\"F2\", \"alinomi elektitajn\"],\n\n\t\t\t\"file-list-sel\",\n\t\t\t[\"spacoklavo\", \"baskuli elektadon de dosieroj\"],\n\t\t\t[\"↑/↓\", \"movi elektado-kursoron\"],\n\t\t\t[\"stir ↑/↓\", \"movi kursoron kaj vidujon\"],\n\t\t\t[\"⇧ ↑/↓\", \"elekti (mal)sekvan dosieron\"],\n\t\t\t[\"stir-A\", \"elekti ĉiujn dosier(uj)ojn\"],\n\t\t], [\n\t\t\t\"navigation\",\n\t\t\t[\"B\", \"baskuli inter paĝnivela kaj arbovida navigo\"],\n\t\t\t[\"I/K\", \"(mal-)sekva dosierujo\"],\n\t\t\t[\"M\", \"parent folder (or unexpand current)\"],\n\t\t\t[\"V\", \"baskuli inter montrado de dosierujoj aŭ tekst-dosieroj en toggle folders / textfiles en arbovida navig-panelo\"],\n\t\t\t[\"A/D\", \"grandeco de arbovida navig-panelo\"],\n\t\t], [\n\t\t\t\"audio-player\",\n\t\t\t[\"J/L\", \"(mal-)sekva kanto\"],\n\t\t\t[\"U/O\", \"iri 10 sekundoj (mal)antaŭen\"],\n\t\t\t[\"0..9\", \"iri al 0%..90%\"],\n\t\t\t[\"P\", \"ludi/paŭzigi (ankaŭ komencas)\"],\n\t\t\t[\"S\", \"elekti ludantan kanton\"],\n\t\t\t[\"Y\", \"elŝuti kanton\"],\n\t\t], [\n\t\t\t\"image-viewer\",\n\t\t\t[\"J/L, ←/→\", \"(mal)sekva bildo\"],\n\t\t\t[\"Home/End\", \"unua/lasta bildo\"],\n\t\t\t[\"F\", \"plenekrana vido\"],\n\t\t\t[\"R\", \"turni dekstrumen\"],\n\t\t\t[\"⇧ R\", \"turni maldekstrumen\"],\n\t\t\t[\"S\", \"elekti bildon\"],\n\t\t\t[\"Y\", \"elŝuti bildon\"],\n\t\t], [\n\t\t\t\"video-player\",\n\t\t\t[\"U/O\", \"iri 10 sekundoj (mal)antaŭen\"],\n\t\t\t[\"P/K/Spaco\", \"ludi/paŭzigi\"],\n\t\t\t[\"C\", \"??continue playing next\"],\n\t\t\t[\"V\", \"cikla ludado\"],\n\t\t\t[\"M\", \"silentigi\"],\n\t\t\t[\"[ and ]\", \"agordi intervalon de cikla ludado\"],\n\t\t], [\n\t\t\t\"textfile-viewer\",\n\t\t\t[\"I/K\", \"(mal)sekva dosiero\"],\n\t\t\t[\"M\", \"fermi dosieron\"],\n\t\t\t[\"E\", \"redakti dosieron\"],\n\t\t\t[\"S\", \"elekti dosieron (por eltondado/kopiado/alinomado)\"],\n\t\t\t[\"Y\", \"elŝuti tekstodosieron\"],\n\t\t\t[\"⇧ J\", \"beligi JSONon\"],\n\t\t]\n\t],\n\n\t\"m_ok\": \"OK\",\n\t\"m_ng\": \"Rezigni\",\n\n\t\"enable\": \"Ŝalti\",\n\t\"danger\": \"DANĜERO\",\n\t\"clipped\": \"kopiita al tondujo\",\n\n\t\"ht_s1\": \"sekundo\",\n\t\"ht_s2\": \"sekundoj\",\n\t\"ht_m1\": \"minuto\",\n\t\"ht_m2\": \"minutoj\",\n\t\"ht_h1\": \"horo\",\n\t\"ht_h2\": \"horoj\",\n\t\"ht_d1\": \"tago\",\n\t\"ht_d2\": \"tagoj\",\n\t\"ht_and\": \" kaj \",\n\n\t\"goh\": \"stirpanelo\",\n\t\"gop\": 'malsekva dosierujo\">malsekva',\n\t\"gou\": 'supra dosierujo\">supren',\n\t\"gon\": 'sekva dosierujo\">sekva',\n\t\"logout\": \"Adiaŭi kiel \",\n\t\"login\": \"Ensaluti\",\n\t\"access\": \" atingo\",\n\t\"ot_close\": \"fermi submenuon\",\n\t\"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`&quot;try unite&quot;` = enhavi precipe «try unite»$N$Nformato de datoj estas iso-8601, ekzemple$N`2009-12-31` aŭ `2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"unpost: forigi viaj plej lastaj alŝutoj, aŭ ĉesigi nefinigitajn\",\n\t\"ot_bup\": \"bup: fundamenta alŝutilo, funkias eĉ kun netscape 4.0\",\n\t\"ot_mkdir\": \"mkdir: krei novan dosierujon\",\n\t\"ot_md\": \"new-file: krei novan tekstodosieron\",\n\t\"ot_msg\": \"msg: sendi mesaĝon al servila protokolo\",\n\t\"ot_mp\": \"agordoj de medialudilo\",\n\t\"ot_cfg\": \"aliaj agordoj\",\n\t\"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 [🎈]&nbsp; (la fundamenta alŝutilo)<br /><br />dum alŝutado, ĉi tiu simbolo iĝas plenumindikilo!',\n\t\"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 [🎈]&nbsp; (la fundamenta alŝutilo)<br /><br />dum alŝutado, ĉi tiu simbolo iĝas plenumindikilo!',\n\t\"ot_noie\": 'Bonvolu uzi retumilojn Chrome / Firefox / Edge',\n\n\t\"ab_mkdir\": \"krei dosierujon\",\n\t\"ab_mkdoc\": \"krei tekstodosieron\",\n\t\"ab_msg\": \"sendi mesaĝon al protokolo\",\n\n\t\"ay_path\": \"iri al dosierujoj\",\n\t\"ay_files\": \"iri al dosieroj\",\n\n\t\"wt_ren\": \"alinomi elektitajn aĵojn$NFulmoklavo: F2\",\n\t\"wt_del\": \"forigi elektitajn aĵojn$NFulmoklavo: stir-K\",\n\t\"wt_cut\": \"eltondi elektitajn aĵojn &lt;small&gt;(do alglui ien aliloke)&lt;/small&gt;$NFulmoklavo: stir-X\",\n\t\"wt_cpy\": \"kopii elektitajn aĵojn al tondujo$N(por alglui ien aliloke)$NFulmoklavo: stir-C\",\n\t\"wt_pst\": \"alglui antaŭe eltonditajn / kopiitajn aĵojn$NFulmoklavo: stir-V\",\n\t\"wt_selall\": \"elekti ĉiujn dosierojn$NFulmoklavo: stir-A (se dosiero estas elektita)\",\n\t\"wt_selinv\": \"inversigi elektaĵon\",\n\t\"wt_zip1\": \"elŝuti dosierujon kiel arkivo\",\n\t\"wt_selzip\": \"elŝuti elektaĵon kiel arkivo\",\n\t\"wt_seldl\": \"elŝuti elektaĵon kiel apartaj dosieroj$NFulmoklavo: Y\",\n\t\"wt_npirc\": \"kopii IRC-formatan muzikaĵ-informon\",\n\t\"wt_nptxt\": \"kopii tekstan muzikaĵ-informon\",\n\t\"wt_m3ua\": \"aldoni al m3u-ludliston (klaku butonon <code>📻copy</code> poste)\",\n\t\"wt_m3uc\": \"kopii m3u-ludliston al tondujo\",\n\t\"wt_grid\": \"baskuli kradan / listan vidon$NFulmoklavo: G\",\n\t\"wt_prev\": \"malsekva muzikaĵo$NFulmoklavo: J\",\n\t\"wt_play\": \"ludi / paŭzigi$NFulmoklavo: P\",\n\t\"wt_next\": \"sekva muzikaĵo$NFulmoklavo: L\",\n\n\t\"ul_par\": \"paralelaj alŝutoj:\",\n\t\"ut_rand\": \"hazardigi dosiernomojn\",\n\t\"ut_u2ts\": \"kopii la tempon de lasta modifo$Nel via dosiersistemo al la servilo\\\">📅\",\n\t\"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\",\n\t\"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\",\n\t\"ut_ask\": 'peti konfirmon antaŭ komenco de alŝutado\">💭',\n\t\"ut_pot\": \"plirapidigi alŝutadon por malrapidaj komputiloj$Nper malkomplikado de fasado\",\n\t\"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)\",\n\t\"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\",\n\t\"ul_btn\": \"demeti dosier(uj)ojn<br>ĉi tien (aŭ alklaki ĉi tien)\",\n\t\"ul_btnu\": \"A L Ŝ U T I\",\n\t\"ul_btns\": \"S E R Ĉ I\",\n\n\t\"ul_hash\": \"k-sumado\",\n\t\"ul_send\": \"sendado\",\n\t\"ul_done\": \"finita\",\n\t\"ul_idle1\": \"neniuj alŝutoj envicigitaj\",\n\t\"ut_etah\": \"meza rapido de &lt;em&gt;kontrolsumado&lt;/em&gt;, kaj pritaksita tempo ĝis la fino\",\n\t\"ut_etau\": \"meza rapido de &lt;em&gt;alŝutado&lt;/em&gt;, kaj pritaksita tempo ĝis la fino\",\n\t\"ut_etat\": \"meza &lt;em&gt;tuta&lt;/em&gt; rapido, kaj pritaksita tempo ĝis la fino\",\n\n\t\"uct_ok\": \"sukcese plenumita\",\n\t\"uct_ng\": \"malbona: malsukceso / malakcepto / ne trovita (no good)\",\n\t\"uct_done\": \"ambaŭ ok kaj ng\",\n\t\"uct_bz\": \"kontrolsumado aŭ alŝutado (busy)\",\n\t\"uct_q\": \"envicigita (queue)\",\n\n\t\"utl_name\": \"dosiernomo\",\n\t\"utl_ulist\": \"listigi\",\n\t\"utl_ucopy\": \"kopii\",\n\t\"utl_links\": \"ligilojn\",\n\t\"utl_stat\": \"stato\",\n\t\"utl_prog\": \"progreso\",\n\n\t// keep short:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"ERARO\",\n\t\"utl_oserr\": \"OS-eraro\",\n\t\"utl_found\": \"trovita\",\n\t\"utl_defer\": \"postigita\",\n\t\"utl_yolo\": \"rapidega\",\n\t\"utl_done\": \"finita\",\n\n\t\"ul_flagblk\": \"la dosieroj estis aldonita al la vico,</b><br>sed alia langeto de retumilo jam alŝutas dosierojn per up2k,<br>do tiu alŝutado devas finiĝi unue\",\n\t\"ul_btnlk\": \"la agordado de servilo ne permesas ŝanĝi tiun agordon\",\n\n\t\"udt_up\": \"Alŝuti\",\n\t\"udt_srch\": \"Serĉi\",\n\t\"udt_drop\": \"demetu ĉi tien\",\n\n\t\"u_nav_m\": '<h6>do, kion vi havas ĉi tie?</h6><code>Enter</code> = Dosierojn (unu al multaj)\\n<code>ESK</code> = Unu dosierujon (eble kun subdosierujoj)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Dosierojn</a><a href=\"#\" id=\"modal-ng\">Unu dosierujo</a>',\n\n\t\"cl_opts\": \"ŝaltiloj\",\n\t\"cl_hfsz\": \"dosiergrando\",\n\t\"cl_themes\": \"etoso\",\n\t\"cl_langs\": \"lingvo\",\n\t\"cl_ziptype\": \"elŝutado de dosieroj\",\n\t\"cl_uopts\": \"agordoj de up2k\",\n\t\"cl_favico\": \"retpaĝsimbolo\",\n\t\"cl_bigdir\": \"grandaj ujoj\",\n\t\"cl_hsort\": \"#ordigo\",\n\t\"cl_keytype\": \"skemo de fulmoklavoj\",\n\t\"cl_hiddenc\": \"kaŝitaj kolumnoj\",\n\t\"cl_hidec\": \"kaŝi\",\n\t\"cl_reset\": \"restarigi\",\n\t\"cl_hpick\": \"alklaki la kapojn de kolumnoj por kasi en la suban tabelon\",\n\t\"cl_hcancel\": \"kaŝado de kolumno nuligita\",\n\t\"cl_rcm\": \"dekstra-klaka menuo\",\n\n\t\"ct_grid\": '田 krado',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ ŝpruchelpiloj',\n\t\"ct_thumb\": 'dum krado-vido, baskuli montradon de simboloj aŭ bildetoj$NFulmoklavo: T\">🖼️ bildetoj',\n\t\"ct_csel\": 'uzi STIR kaj MAJ por elekti dosierojn en krado-vido\">elekto',\n\t\"ct_dsel\": 'uzi tren-elekton en krado-vido\">treni',\n\t\"ct_dl\": 'devigi elŝuton (ne montri enkadre), kiam dosiero estas alklakita\">elŝuti',\n\t\"ct_ihop\": 'rulumi al la lasta vidita bildo-dosiero post fermado de bildo-vidilo\">🖼️⮯',\n\t\"ct_dots\": 'montri kaŝitajn dosierojn (se servilo permesas)\">kaŝitaj',\n\t\"ct_qdel\": 'peti konfirmon nur unufoje antaŭ forigado\">rapid-forig.',\n\t\"ct_dir1st\": 'ordigi dosierujojn antaŭ dosieroj\">📁 unue',\n\t\"ct_nsort\": '`numera ordigo de dosiernomoj (ekz. `2` antaŭ `11`)\">№.ord',\n\t\"ct_utc\": 'montri ĉiuj datoj kaj tempoj per UTC\">UTC',\n\t\"ct_readme\": 'montri enhavon de README.md en listaĵo de dosieroj\">📜 readme',\n\t\"ct_idxh\": 'montri paĝon index.html anstataŭ listaĵo de dosieroj\">htm',\n\t\"ct_sbars\": 'montri rulumskalojn\">⟊',\n\n\t\"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\",\n\n\t\"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 &quot;alŝuti&quot; la tiuj samaj dosieroj — la kontrolsumado rekomencos kaj ne realŝutos ion ajn, se la alŝutado vere sukcesis\\\">rapidega\",\n\n\t\"cut_datechk\": \"efektas nur se &quot;rapidega&quot; alŝutado estas ŝaltita$N$Nete plibonigas fidindon de kontrolado, per kontrolado de modifo-tempoj aldone al grandoj$N$N<em>teorie</em> estas sufiĉe por detekti nefinigitajn aŭ difektitajn alŝutojn, sed ne estas kompleta alternativo por sen-&quot;rapidega&quot; kontrolado\\\">dato-kontrolo\",\n\n\t\"cut_u2sz\": \"grando (en MiBoj) de ĉiu alŝutanta ero; grandaj valoroj estas pli bonaj por longdistancaj konektoj, malgrandajn por malalt-kvalitaj konektoj\",\n\n\t\"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\",\n\n\t\"cut_az\": \"alŝuti dosierojn en alfabeta ordigo anstataŭ &quot;plej malgrandaj unue&quot;$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)\",\n\n\t\"cut_nag\": \"sciigi per operaciumo je fino de alŝutado$N(nur se ĉi tiu langeto de retumilo ne estas aktiva)\",\n\t\"cut_sfx\": \"sciigi per sono je fino de alŝutado$N(nur se ĉi tiu langeto de retumilo ne estas aktiva)\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cft_text\": \"teksto de retpaĝsimbolo (blankigu kaj reŝargu la paĝon por malŝalti)\",\n\t\"cft_fg\": \"teksta koloro\",\n\t\"cft_bg\": \"fona koloro\",\n\n\t\"cdt_lim\": \"maks. nombro de dosieroj por montri en dosierujo\",\n\t\"cdt_ask\": \"je malsupro de paĝo, peti por ago$Nanstataŭ ŝarĝi pli da dosieroj\",\n\t\"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\",\n\t\"cdt_ren\": \"ebligi propran dekstra-klakan menuon, la normala menuo restas alirebla per MAJ + dekstra klako\\\">ŝalti\",\n\t\"cdt_rdb\": \"montri la normalan dekstraklakan menuon, kiam la propra jam estas malfermita kaj oni denove dekstre klakas\\\">duobla\",\n\n\t\"tt_entree\": \"montri arbovidan navig-panelon$NFulmoklavo: B\",\n\t\"tt_detree\": \"montri paĝnivelan navig-panelon$NFulmoklavo: B\",\n\t\"tt_visdir\": \"rulumi al elektita dosierujo\",\n\t\"tt_ftree\": \"baskuli dosieruj-arban aŭ teksto-dosieran vidon$NFulmoklavo: V\",\n\t\"tt_pdock\": \"fiksi patrajn dosierojn sur supro de panelo\",\n\t\"tt_dynt\": \"aŭtomate pligrandigi panelon\",\n\t\"tt_wrap\": \"linifaldo\",\n\t\"tt_hover\": \"montri kompletajn nomojn sur musumo$N( paneas rulumadon, se la kursoro de muso $N&nbsp; ne estas en la maldekstra malplenaĵo )\",\n\n\t\"ml_pmode\": \"je la fino de dosierujo...\",\n\t\"ml_btns\": \"komandoj\",\n\t\"ml_tcode\": \"transkodi\",\n\t\"ml_tcode2\": \"transkodi al\",\n\t\"ml_tint\": \"kolorado\",\n\t\"ml_eq\": \"ekvalizilo\",\n\t\"ml_drc\": \"kompresoro\",\n\t\"ml_ss\": \"preterpasi silenton\",\n\n\t\"mt_loop\": \"ripeti unu kanton\\\">🔁\",\n\t\"mt_one\": \"haltigi post unu kanto\\\">1️⃣\",\n\t\"mt_shuf\": \"ludi ĉiu dosierujo en hazarda ordo\\\">🔀\",\n\t\"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▶\",\n\t\"mt_preload\": \"komenci ŝargadon de sekva kanto antaŭ la fino de la nuna, por kontinua ludado\\\">antaŭŝarg.\",\n\t\"mt_prescan\": \"eniri la sekvan dosierujon antaŭ la fino de la lasta kanto, $Npor ke la retumilo ne interrompis la ludadon\\\">nav\",\n\t\"mt_fullpre\": \"antaŭŝargi la tutan kanton;$N✅ ŝalti por <b>malaltkvalitaj</b> konektoj,$N❌ eble <b>malŝalti</b> por malrapidaj konektoj\\\">tute\",\n\t\"mt_fau\": \"por poŝtelefonoj: komenci sekvan kanton, eĉ se ĝi ne estis tute ŝargita (povas difektigi la montradon de muzikaĵ-etikedoj)\\\">☕️\",\n\t\"mt_waves\": \"bildigo:$Nmontri amplitudon de ludanta kanto en ludadbreto\\\">~\",\n\t\"mt_npclip\": \"montri butonojn por kopiado de ludanta kanto\\\">/np\", //ne tradukita: \"/np\" estas referenco al komando, uzata en IRC-babilejoj\n\t\"mt_m3u_c\": \"montri butonojn por kopiado de elektitaj kantoj kiel m3u8-ludlisto\\\">📻\",\n\t\"mt_octl\": \"integrado kun operaciumo (medio-klavoj kaj montriloj)\\\">integr.\",\n\t\"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\",\n\t\"mt_oscv\": \"montri album-bildojn en montriloj\\\">bildo\",\n\t\"mt_follow\": \"rulumi la pagon, por ke la ludanta kanto restas videbla\\\">🎯\",\n\t\"mt_compact\": \"kompaktaj ruliloj\\\">⟎\",\n\t\"mt_uncache\": \"malplenigi kaŝmemoron &nbsp;(uzinda, se via retumilo kaŝmemoris$Ndifektitan kopion de kanto, kaj ne povas ludi ĝin)\\\">🗑️ kaŝmem.\",\n\t\"mt_mloop\": \"ripeti la nunan dosierujon\\\">🔁 ripeti\",\n\t\"mt_mnext\": \"ŝargi la sekvan dosierujon kaj daŭrigi\\\">📂 sekva\",\n\t\"mt_mstop\": \"haltigi ludadon\\\">⏸ haltigi\",\n\t\"mt_cflac\": \"konverti el flac / wav al {0}\\\">flac\",\n\t\"mt_caac\": \"konverti el aac / m4a al {0}\\\">aac\",\n\t\"mt_coth\": \"konverti aliajn (krom mp3) al {0}\\\">aliaj\",\n\t\"mt_c2opus\": \"pli bona elekto por propraj komputiloj kaj Android\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, por iOS 17.5 kaj pli novaj\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, por iOS 11-17\\\">caf\",\n\t\"mt_c2mp3\": \"por tre malnovaj iloj\\\">mp3\",\n\t\"mt_c2flac\": \"plej bona sonkvalito, sed grandegaj elŝutoj\\\">flac\",\n\t\"mt_c2wav\": \"sendensigita ludado (pli bonaj elŝutoj)\\\">wav\",\n\t\"mt_c2ok\": \"bona elekto\",\n\t\"mt_c2nd\": \"ĉi tiu formato ne estas rekomendita por via aparato, sed ĝi ankaŭ funkcios\",\n\t\"mt_c2ng\": \"via aparato ŝajne ne subtenas ĉi tiun formaton, sed ni provu uzi ĝin malgraŭe\",\n\t\"mt_xowa\": \"estas difektoj en iOS, kiuj preventas fonan ludadon per ĉi tiu formato; bonvolu uzi caf aŭ mp3 anstataŭe\",\n\t\"mt_tint\": \"travideblo (0-100) de ludadbreto$Nvi povas ŝanĝi ĝin, se ĝi aspektas tro distre dum ŝargado\",\n\t\"mt_eq\": \"`ŝaltas ekvalizilon kaj stirilon de plifortigado;$N$Nboost (plifortigado) `0` = senmodifa 100%a laŭteco$N$Nwidth (larĝo) `1 &nbsp;` = senmodifa dukanala sono$Nwidth (larĝo) `0.5` = 50% miksado inter maldekstra kaj dekstra kanaloj$Nwidth (larĝo) `0 &nbsp;` = unukanala sono$N$Nboost `-0.8` &amp; 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\",\n\t\"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)\",\n\t\"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%`\",\n\t\"mt_ssvt\": \"maksimuma laŭtnivelo por silentsaltado (0-255)\\\">laŭt\",\n\t\"mt_ssts\": \"komenca intervalo por silentsaltado (% de trakolongo)\\\">ek%\",\n\t\"mt_sste\": \"fina intervalo por silentsaltado (% de trakolongo)\\\">fin%\",\n\t\"mt_sssm\": \"multiplikado de ludrapideco dum silentsaltado\\\">×rapid\",\n\n\t\"mb_play\": \"ludi\",\n\t\"mm_hashplay\": \"ludi ĉi tiun aŭdiodosieron?\",\n\t\"mm_m3u\": \"premu <code>Enter/OK</code> por ludado \\npremu <code>ESK/Rezigni</code> por redaktado\",\n\t\"mp_breq\": \"bezonas Firefox 82+, Chrome 73+ aŭ iOS 15+\",\n\t\"mm_bload\": \"ŝargado...\",\n\t\"mm_bconv\": \"konvertado al formato {0}, bonvolu atendi...\",\n\t\"mm_opusen\": \"via retumilo ne povas ludi dosierojn de formatoj aac / m4a;\\ntranskodado al opus estas ŝaltigita\",\n\t\"mm_playerr\": \"ludado malsukcesis: \",\n\t\"mm_eabrt\": \"Klopodo de ludado estis nuligita\",\n\t\"mm_enet\": \"Via retkonekto estas nestabila\",\n\t\"mm_edec\": \"Ĉi tiu dosiero estas ŝajne difektita??\",\n\t\"mm_esupp\": \"Via retumilo ne komprenas ĉi tiun aŭdio-formaton\",\n\t\"mm_eunk\": \"Nekonata eraro\",\n\t\"mm_e404\": \"Ne povas ludi aŭdiaĵon; eraro 404: Dosiero ne trovita.\",\n\t\"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\",\n\t\"mm_e415\": \"Ne povas ludi aŭdiaĵon; eraro 415: Transkodigo de dosiero malsukcesis; rigardu la protokolojn de la servilo.\",\n\t\"mm_e500\": \"Ne povas ludi aŭdiaĵon; eraro 500: Rigardu la protokolojn de servilo.\",\n\t\"mm_e5xx\": \"Ne povas ludi aŭdiaĵon; servila eraro \",\n\t\"mm_nof\": \"neniuj aŭdio-dosieroj trovitaj proksime\",\n\t\"mm_prescan\": \"Serĉado por sekva aŭdiaĵo...\",\n\t\"mm_scank\": \"Sekva muzikaĵo trovita:\",\n\t\"mm_uncache\": \"kaŝmemoro malplenigita; ĉiuj muzikaĵoj estos reelŝutitaj dum sekva ludado\",\n\t\"mm_hnf\": \"ĉi tiu muzikaĵo ne ekzistas plu\",\n\n\t\"im_hnf\": \"ĉi tiu bildo ne ekzistas plu\",\n\n\t\"f_empty\": 'ĉi tiu dosierujo estas malplena',\n\t\"f_chide\": 'ĉi tiu ago kaŝos kolumnon «{0}»\\n\\nvi povas malkaŝi kolumnojn en agordoj',\n\t\"f_bigtxt\": \"ĉi tiu dosiero estas {0}-MiB-granda -- ĉu vere malfermi kiel teksto?\",\n\t\"f_bigtxt2\": \"ĉu malfermi nur la finon de dosiero? ĉi tiu reĝimo ankaŭ ŝaltos tujan ĝisdatigon, novaj linioj estos tuj montritaj\",\n\t\"fbd_more\": '<div id=\"blazy\"><code>{0}</code> de <code>{1}</code> dosieroj montrataj; <a href=\"#\" id=\"bd_more\">montri {2}</a> aŭ <a href=\"#\" id=\"bd_all\">montri ĉiujn</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\"><code>{0}</code> de <code>{1}</code> dosieroj montrataj; <a href=\"#\" id=\"bd_all\">montri ĉiujn</a></div>',\n\t\"f_anota\": \"nur {0} de {1} eroj estis elektita;\\nrulumi al la malsupro por elekti la tutan dosierujon\",\n\n\t\"f_dls\": 'la ligiloj de dosieroj en ĉi tiu dosierujo estis\\nanstataŭigitaj per elŝuto-ligiloj',\n\t\"f_dl_nd\": 'dosierujo preterlasita (uzu zip/tar-elŝuton anstataŭe):\\n',\n\n\t\"f_partial\": \"Por sendifekta elŝuto de nune-alŝutata dosiero, elektu dosieron kun sama nomo, sed sen etendaĵo <code>.PARTIAL</code>. Bonvolu uzi la butonon \\\"Rezigni\\\" aŭ klavon ESK por fari tion.\\n\\nSe vi uzas OK / Enter, la provizora dosiero <code>.PARTIAL</code> estos elŝutita, kiu tre probable enhavas nekompletajn datumojn.\",\n\n\t\"ft_paste\": \"alglui {0} erojn$NFulmoklavo: stir-V\",\n\t\"fr_eperm\": 'ne povas alinomi:\\nvi ne havas permeson “move” en ĉi tiu dosierujo',\n\t\"fd_eperm\": 'ne povas forigi:\\nvi ne havas permeson “delete” en ĉi tiu dosierujo',\n\t\"fc_eperm\": 'ne povas eltondi:\\nvi ne havas permeson “move” en ĉi tiu dosierujo',\n\t\"fp_eperm\": 'ne povas alglui:\\nvi ne havas permeson “write” en ĉi tiu dosierujo',\n\t\"fr_emore\":  \"elekti almenaŭ unu aĵon por alinomi\",\n\t\"fd_emore\":  \"elekti almenaŭ unu aĵon por forigi\",\n\t\"fc_emore\":  \"elekti almenaŭ unu aĵon por eltondi\",\n\t\"fcp_emore\": \"elekti almenaŭ unu aĵon por kopii al tondujo\",\n\n\t\"fs_sc\": \"kunhavigi la aktualan dosierujon\",\n\t\"fs_ss\": \"kunhavigi la elektitajn dosierojn\",\n\t\"fs_just1d\": \"vi ne povas elekti pli ol unu dosierujon\\naŭ miksi dosierojn kaj dosierujojn en elektaĵo\",\n\t\"fs_abrt\": \"❌ ĉesigi\",\n\t\"fs_rand\": \"🎲 haz. nomo\",\n\t\"fs_go\": \"✅ krei komunaĵon\",\n\t\"fs_name\": \"nomo\",\n\t\"fs_src\": \"indiko\",\n\t\"fs_pwd\": \"pasvorto\",\n\t\"fs_exp\": \"tempolimo\",\n\t\"fs_tmin\": \"min\",\n\t\"fs_thrs\": \"horoj\",\n\t\"fs_tdays\": \"tagoj\",\n\t\"fs_never\": \"eterna\",\n\t\"fs_pname\": \"nomo de ligilo; estos hazarde kreita, se malplena\",\n\t\"fs_tsrc\": \"dosier(uj)o por kunhavigi\",\n\t\"fs_ppwd\": \"pasvorto (nedeviga)\",\n\t\"fs_w8\": \"kreado de komunaĵo...\",\n\t\"fs_ok\": \"premu <code>Enter/OK</code> por kopii al tondujo\\npremu <code>ESK/Rezigni</code> por fermi\",\n\n\t\"frt_dec\": \"povas ripari difektitajn dosiernomojn\\\">url-malkodo\",\n\t\"frt_rst\": \"reagordi modifitajn dosiernomojn al originalaj\\\">↺ malfari\",\n\t\"frt_abrt\": \"ĉesigi operacion kaj fermi ĉi tiun fenestron\\\">❌ rezigni\",\n\t\"frb_apply\": \"ALINOMI\",\n\t\"fr_adv\": \"amasa / metadatuma / ŝablona alinomado\\\">altnivela\",\n\t\"fr_case\": \"uskleciva regula esprimo\\\">uskleco\",\n\t\"fr_win\": \"Windows-taŭgaj nomoj; signoj <code>&lt;&gt;:&quot;\\\\|?*</code> estos anstataŭigitaj per japanaj duobla-larĝaj signoj\\\">win\",\n\t\"fr_slash\": \"anstataŭigi <code>/</code>n per signo, kiu ne devigas kreadon de novaj dosierujoj\\\">sen /\",\n\t\"fr_re\": \"`ŝablono de regula esprimo, kiu estos aplikita al originalaj dosiernomoj; kaptogrupoj povas esti referencita en formatkampo, ekz. `(1)`, `(2)` k.t.p.\",\n\t\"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\",\n\t\"fr_pdel\": \"forigi\",\n\t\"fr_pnew\": \"konservi kiel\",\n\t\"fr_pname\": \"nomu vian novan ŝablonon\",\n\t\"fr_aborted\": \"ĉesigita\",\n\t\"fr_lold\": \"malnova nomo\",\n\t\"fr_lnew\": \"nova nomo\",\n\t\"fr_tags\": \"etikedoj por elektitaj dosieroj (ne redakteblas, nur por referenco):\",\n\t\"fr_busy\": \"alinomado de {0} aĵoj...\\n\\n{1}\",\n\t\"fr_efail\": \"alinomado malsukcesis:\\n\",\n\t\"fr_nchg\": \"{0} da novaj nomoj estis modifita pro reguloj <code>win</code> kaj/aŭ <code>sen /</code>\\n\\nĈu daŭrigi kun modifitaj nomoj?\",\n\n\t\"fd_ok\": \"forigado sukcesis\",\n\t\"fd_err\": \"forigado malsukcesis:\\n\",\n\t\"fd_none\": \"nenio estis forigita; eble servila eraro malpermesis ĝin (xbd)?\",\n\t\"fd_busy\": \"forigado de {0} aĵoj...\\n\\n{1}\",\n\t\"fd_warn1\": \"ĉu FORIGI ĉi tiujn {0} aĵojn?\",\n\t\"fd_warn2\": \"<b>Averto!</b> Ĉi tiu ago ne malfareblas. Ĉu forigi?\",\n\n\t\"fc_ok\": \"{0} aĵoj eltonditaj\",\n\t\"fc_warn\": '{0} aĵoj eltonditaj\\n\\nnur <b>ĉi tiu</b> langeto de retumilo povas alglui ilin\\n(pro la grando de elektaĵo)',\n\n\t\"fcc_ok\": \"{0} aĵoj kopiitaj al tondujo\",\n\t\"fcc_warn\": '{0} aĵoj kopiitaj al tondujo\\n\\nnur <b>ĉi tiu</b> langeto de retumilo povas alglui ilin\\n(pro la grando de elektaĵo)',\n\n\t\"fp_apply\": \"uzi ĉi tiujn nomojn\",\n\t\"fp_skip\": \"preterpasi konfliktojn\",\n\t\"fp_ecut\": \"unue eltondi aŭ kopii dosier(uj)ojn, do alglui ĝin poste\\n\\nnoto: tondujo ankaŭ funkcias inter aliaj langetoj de retumilo\",\n\t\"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:\",\n\t\"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:\",\n\t\"fp_emore\": \"ankoraŭ restas koincidoj de dosiernomoj, kiuj bezonas solvon\",\n\t\"fp_ok\": \"movado sukcesis\",\n\t\"fcp_ok\": \"kopiado sukcesis\",\n\t\"fp_busy\": \"movado de {0} aĵoj...\\n\\n{1}\",\n\t\"fcp_busy\": \"kopiado {0} aĵoj...\\n\\n{1}\",\n\t\"fp_abrt\": \"ĉesigado...\",\n\t\"fp_err\": \"movado malsukcesis:\\n\",\n\t\"fcp_err\": \"kopiado malsukcesis:\\n\",\n\t\"fp_confirm\": \"ĉu movi tiujn {0} aĵojn ĉi tien?\",\n\t\"fcp_confirm\": \"ĉu kopii tiujn {0} aĵojn ĉi tien?\",\n\t\"fp_etab\": 'eraro dum legado de tondujo el alia langeto de retumilo',\n\t\"fp_name\": \"alŝutado de dosiero el via aparato. Nomi ĝin:\",\n\t\"fp_both_m\":  '<h6>elektu, kion alglui</h6><code>Enter</code> = Movi {0} dosierojn al «{1}»\\n<code>ESK</code> = Alŝuti {2} dosierojn el via aparato',\n\t\"fcp_both_m\": '<h6>elektu, kion alglui</h6><code>Enter</code> = Kopii {0} dosierojn al «{1}»\\n<code>ESK</code> = Alŝuti {2} dosierojn el via aparato',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Movi</a><a href=\"#\" id=\"modal-ng\">Alŝuti</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Kopii</a><a href=\"#\" id=\"modal-ng\">Alŝuti</a>',\n\n\t\"mk_noname\": \"tajpu nomon en tekstokampo maldekstre antaŭ vi faras ĉi tion :p\",\n\t\"nmd_i1\": \"vi povas aldoni la deziratan sufikson, ekzemple <code>.md</code>\",\n\t\"nmd_i2\": \"vi povas krei nur <code>.{0}</code>-dosierojn, ĉar vi ne rajtas forigi dosierojn\",\n\n\t\"tv_load\": \"Ŝargado de teksto-dokumento:\\n\\n{0}\\n\\n{1}% ({2} da {3} MiB ŝargita)\",\n\t\"tv_xe1\": \"ne povas ŝargi teksto-dosieron:\\n\\neraro \",\n\t\"tv_xe2\": \"404, dosiero ne trovita\",\n\t\"tv_lst\": \"listo de teksto-dosieroj en\",\n\t\"tvt_close\": \"reveni al vido de dosierujo$NFulmoklavo: M (aŭ Esk)\\\">❌ fermi\",\n\t\"tvt_dl\": \"elŝuti ĉi tiun dosieron$NFulmoklavo: Y\\\">💾 elŝuti\",\n\t\"tvt_prev\": \"montri malsekvan dokumenton$NFulmoklavo: i\\\">⬆ malsekva\",\n\t\"tvt_next\": \"montri sekvan dokumenton$NFulmoklavo: K\\\">⬇ sekva\",\n\t\"tvt_sel\": \"elekti dosieron &nbsp; ( por eltondado / kopiado / forigado / ... )$NFulmoklavo: S\\\">elekti\",\n\t\"tvt_j\": \"beligi JSONon$NFulmoklavo: MAJ-J\\\">j\",\n\t\"tvt_edit\": \"malfermi dosieron en teksto-redaktilo$NFulmoklavo: E\\\">✏️ redakti\",\n\t\"tvt_tail\": \"observi ŝanĝojn en dosiero; novaj linioj estos tuje montritaj\\\">📡 gvati\",\n\t\"tvt_wrap\": \"linifaldo\\\">↵\",\n\t\"tvt_atail\": \"alpingli rulumadon al malsupro de paĝo\\\">⚓\",\n\t\"tvt_ctail\": \"malkodi ANSI-kodojn de terminal-koloroj\\\">🌈\",\n\t\"tvt_ntail\": \"limo de rulumado (kiom da bajtoj de teksto konservi en memoro)\",\n\n\t\"m3u_add1\": \"muzikaĵo aldonita al m3u-ludlisto\",\n\t\"m3u_addn\": \"{0} muzikaĵoj aldonitaj al m3u-ludlisto\",\n\t\"m3u_clip\": \"m3u-ludlisto kopiita al tondujo\\n\\nvi devus krei tekst-dosieron kun etendaĵo <code>.m3u</code> kaj alglui la tekston en ĝi por krei uzeblan ludliston\",\n\n\t\"gt_vau\": \"ne montri videojn, nur ludi muzikaĵojn\\\">🎧\",\n\t\"gt_msel\": \"ŝalti elektado-reĝimon; stir-klaki dosieron por ne-elekta ago$N$N&lt;em&gt;kiam ŝaltita: duoblaklako por malfermi dosier(uj)on&lt;/em&gt;$N$NFulmoklavo: S\\\">elektado\",\n\t\"gt_crop\": \"stuci bildetojn\\\">stuci\",\n\t\"gt_3x\": \"alt-kvalitaj bildetoj\\\">3x\",\n\t\"gt_zoom\": \"grando\",\n\t\"gt_chop\": \"nomlongo\",\n\t\"gt_sort\": \"ordigi per\",\n\t\"gt_name\": \"nomo\",\n\t\"gt_sz\": \"grando\",\n\t\"gt_ts\": \"dato\",\n\t\"gt_ext\": \"tipo\",\n\t\"gt_c1\": \"pli mallongaj dosiernomoj\",\n\t\"gt_c2\": \"pli longaj dosiernomoj\",\n\n\t\"sm_w8\": \"serĉado...\",\n\t\"sm_prev\": \"serĉrezultoj sube venas al la lasta informpeto:\\n  \",\n\t\"sl_close\": \"fermi serĉrezultojn\",\n\t\"sl_hits\": \"montrado de {0} kongruaĵoj\",\n\t\"sl_moar\": \"ŝargi pli\",\n\n\t\"s_sz\": \"grando\",\n\t\"s_dt\": \"dato\",\n\t\"s_rd\": \"vojo\",\n\t\"s_fn\": \"nomo\",\n\t\"s_ta\": \"etikedoj\",\n\t\"s_ua\": \"alŝut📅\",\n\t\"s_ad\": \"aliaj\",\n\t\"s_s1\": \"minimuma MiB\",\n\t\"s_s2\": \"maksimuma MiB\",\n\t\"s_d1\": \"min. iso8601\",\n\t\"s_d2\": \"maks. iso8601\",\n\t\"s_u1\": \"alŝutita post\",\n\t\"s_u2\": \"kaj/aŭ antaŭ\",\n\t\"s_r1\": \"vojo enhavas &nbsp; (apartigi per spacoj)\",\n\t\"s_f1\": \"nomo enhavas &nbsp; (negacii per minuso)\",\n\t\"s_t1\": \"etikedoj enhavas &nbsp; (^=komenco, fino=$)\",\n\t\"s_a1\": \"ecoj de metadatumoj\",\n\n\t\"md_eshow\": \"ne povas montri \",\n\t\"md_off\": \"[📜<em>readme</em>] malŝaltita en [⚙️] -- dokumento kaŝita\",\n\n\t\"badreply\": \"Eraro dum legado de respondo al servilo\",\n\n\t\"xhr403\": \"403: Atingo malpermesita\\n\\nklopodu reŝargi paĝon per klavo F5, eble via seanco senvalidiĝis\",\n\t\"xhr0\": \"nekonata (eble konekto al servilo estis perdita, aŭ servilo ne funkcias)\",\n\t\"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\",\n\t\"tl_xe1\": \"ne povas listigi subdosierujojn:\\n\\neraro \",\n\t\"tl_xe2\": \"404: Dosierujo ne trovita\",\n\t\"fl_xe1\": \"ne povas listigi dosierojn en dosierujo:\\n\\neraro \",\n\t\"fl_xe2\": \"404: Dosierujo ne trovita\",\n\t\"fd_xe1\": \"ne povas krei subdosierujon:\\n\\neraro \",\n\t\"fd_xe2\": \"404: Patra dosierujo ne trovita\",\n\t\"fsm_xe1\": \"ne povas sendi mesaĝon:\\n\\neraro \",\n\t\"fsm_xe2\": \"404: Patra dosierujo ne trovita\",\n\t\"fu_xe1\": \"ne povas ŝargi malsend-liston el servilo:\\n\\neraro \",\n\t\"fu_xe2\": \"404: Dosiero ne trovita??\",\n\n\t\"fz_tar\": \"nedensigita dosiero GNU TAR (linux / mac)\",\n\t\"fz_pax\": \"nedensigita PAX-formata dosiero TAR (malpli rapide)\",\n\t\"fz_targz\": \"GNU TAR, densigita per 3a nivelo de GZip$N$Nkutime tre malrapida, do$Nuzu nedensigitan TARon anstataŭe\",\n\t\"fz_tarxz\": \"GNU TAR, densigita per 1a nivelo de XZ$N$Nkutime tre malrapida, do$Nuzu nedensigitan TARon anstataŭe\",\n\t\"fz_zip8\": \"Zip kun dosiernomoj laŭ UTF-8 (eble difektiĝis je Windows 7 kaj pli malnovaj)\",\n\t\"fz_zipd\": \"Zip kun dosiernomoj laŭ CP437, por tre malnovaj programoj (esperantaj diakritaĵoj ne funkcios)\",\n\t\"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)\",\n\n\t\"un_m1\": \"vi povas forigi viajn lastajn alŝutojn (aŭ ĉesigi nefinigitajn) sube\",\n\t\"un_upd\": \"reŝargi\",\n\t\"un_m4\": \"aŭ kunhavigi la dosierojn sube:\",\n\t\"un_ulist\": \"montri\",\n\t\"un_ucopy\": \"kopii\",\n\t\"un_flt\": \"nedeviga filtrilo:&nbsp; URL devas enhavi\",\n\t\"un_fclr\": \"vakigi filtrilon\",\n\t\"un_derr\": 'malalŝutado malsukcesis:\\n',\n\t\"un_f5\": 'io difektiĝis, bonvolu reŝargi aŭ uzi klavon F5',\n\t\"un_uf5\": \"pardonu, sed vi devas reŝargi la paĝon (F5 aŭ Stir+R) antaŭ ĉesigi ĉi tiun alŝuton\",\n\t\"un_nou\": '<b>averto:</b> servilo estas tro okupara por montri nefinigitajn alŝutojn; klaku la butonon \"reŝargi\" post kelkaj sekundoj',\n\t\"un_noc\": '<b>averto:</b> malalŝutado de tute alŝutitaj dosieroj estas malpermesita laŭ agordoj de servilo',\n\t\"un_max\": \"unuaj 2000 dosieroj montritaj (uzi filtrilon por vidi aliajn)\",\n\t\"un_avail\": \"{0} lastaj alŝutoj forigeblas<br />{1} nefinigitajn ĉesigeblas\",\n\t\"un_m2\": \"ordigita per alŝuto-tempo, plej lastaj unue:\",\n\t\"un_no1\": \"neniuj sufiĉe lastaj alŝutoj\",\n\t\"un_no2\": \"neniuj sufiĉe lastaj alŝutoj, kongruaj laŭ filtrilo\",\n\t\"un_next\": \"forigi la sekvajn {0} dosierojn sube\",\n\t\"un_abrt\": \"ĉesigi\",\n\t\"un_del\": \"forigi\",\n\t\"un_m3\": \"ŝargado de viaj lastaj alŝutoj...\",\n\t\"un_busy\": \"forigado de {0} dosieroj...\",\n\t\"un_clip\": \"{0} ligiloj kopiitaj al tondujo\",\n\n\t\"u_https1\": \"vi devas\",\n\t\"u_https2\": \"ŝalti HTTPS-protokolon\",\n\t\"u_https3\": \"por pli bona rendimento\",\n\t\"u_ancient\": 'via retumilo estas vere antikva -- eble vi devus <a href=\"#\" onclick=\"goto(\\'bup\\')\">uzi alŝutilon bup anstataŭe</a>',\n\t\"u_nowork\": \"Firefox 53+ aŭ Chrome 57+ aŭ iOS 11+ necesas\",\n\t\"tail_2old\": \"Firefox 105+ aŭ Chrome 71+ aŭ iOS 14.5+ necesas\",\n\t\"u_nodrop\": 'via retumilo estas tro malnova por ŝova-kaj-demeta alŝutado',\n\t\"u_notdir\": \"tio ne estas dosierujo!\\n\\nvia retumilo estas tro malnova,\\nbonvolu ŝovu kaj demetu anstataŭe\",\n\t\"u_uri\": \"por ŝovi-kaj-demeti bildon de aliaj fenestroj de retumiloj,\\nbonvolu demeti ĝin sur la grandan alŝut-butonon\",\n\t\"u_enpot\": 'uzi <a href=\"#\">simplan fasadon</a> (povas plirapidigi alŝutojn)',\n\t\"u_depot\": 'uzi <a href=\"#\">elegantan fasadon</a> (povas plimalrapidigi alŝutojn)',\n\t\"u_gotpot\": 'ŝaltado de simpla fasado por pli rapidaj alŝutoj,\\n\\nvi povas malŝalti ĝin, se ĝi ne plaĉas al vi!',\n\t\"u_pott\": \"<p>dosieroj: &nbsp; <b>{0}</b> finitaj, &nbsp; <b>{1}</b> eraroj, &nbsp; <b>{2}</b> alŝutataj, &nbsp; <b>{3}</b> envicigitaj</p>\",\n\t\"u_ever\": \"ĉi tiu estas fundamenta alŝutilo; up2k postulas almenaŭ retumilojn<br>Chrome 21 // Firefox 13 // Edge 12 // Opera 12 // Safari 5.1\",\n\t\"u_su2k\": 'ĉi tiu estas fundamenta alŝutilo; <a href=\"#\" id=\"u2yea\">up2k</a> estas pli bona',\n\t\"u_uput\": 'optimumigi por rapideco (ne kalkuli kontrolsumojn)',\n\t\"u_ewrite\": 'vi ne havas permeson skribi en ĉi tiun dosierujon',\n\t\"u_eread\": 'vi ne havas permeson legi ĉi tiun dosierujon',\n\t\"u_enoi\": 'serĉado de dosieroj estas malŝaltita en servilaj agordoj',\n\t\"u_enoow\": \"anstataŭigo ne funkcios ĉi tie; forigo-permeso necesas\",\n\t\"u_badf\": 'Ĉi tiuj {0} de {1} dosieroj estis preterpasitaj, eble pro permesoj de dosiersistemo:\\n\\n',\n\t\"u_blankf\": 'Ĉi tiuj {0} de {1} dosieroj estas blankaj; ĉu alŝuti malgraŭ tio?\\n\\n',\n\t\"u_applef\": 'Ĉi tiuj {0} de {1} dosieroj estas eble nedezirataj;\\nPremu <code>OK/Enter</code> por PRETERPASI ilin,\\nPremu <code>Rezigni/ESK</code> por ignori ĉi tiun mesaĝon kaj ALŜUTI ilin:\\n\\n',\n\t\"u_just1\": '\\nEble ĝi funkcios pli bone, se vi elektas nur unu dosieron',\n\t\"u_ff_many\": \"se vi uzas operaciumojn <b>Linux / MacOS / Android,</b> ĉi tiu kvanto de dosieroj <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\"><em>povas</em> kraŝi retumilon Firefox!</a>\\nse ĉi tio okazos, klopodu denove (aŭ uzu Chrome-on).\",\n\t\"u_up_life\": \"Ĉi tiu alŝuto estos forigita de servilo\\n{0} post kompletado\",\n\t\"u_asku\": 'alŝuti ĉi tiujn {0} dosierojn al <code>{1}</code>',\n\t\"u_unpt\": \"vi povas malalŝuti / forigi ĉi tiun alŝuton per 🧯 supre-maldesktre\",\n\t\"u_bigtab\": '{0} dosieroj montrotaj\\n\\nĉi tiu povas kraŝi vian retumilon, ĉu daŭrigi?',\n\t\"u_scan\": 'Skanado de dosieroj...',\n\t\"u_dirstuck\": 'skanilo haltis dum atingado de sekvaj {0} dosieroj; ili estos preterpasitaj:',\n\t\"u_etadone\": 'Finita ({0}, {1} dosieroj)',\n\t\"u_etaprep\": '(preparado por alŝutado)',\n\t\"u_hashdone\": 'kontrolsumita',\n\t\"u_hashing\": 'k-sumado',\n\t\"u_hs\": 'kvitanco...',\n\t\"u_started\": \"la dosieroj estas alŝutataj; rigardu [🚀]\",\n\t\"u_dupdefer\": \"duplikatoj; estos traktita post ĉiuj aliaj dosieroj\",\n\t\"u_actx\": \"alklaku ĉi tiun tekston por eviti malrapidigon<br />dum uzado de aliaj fenestroj/langetoj\",\n\t\"u_fixed\": \"Bone!&nbsp; Riparita 👍\",\n\t\"u_cuerr\": \"alŝutado de ero {0} de {1} malsukcesis;\\neble malgravas, alŝutado daŭrigas\\n\\ndosiero: {2}\",\n\t\"u_cuerr2\": \"servilo rifuzis alŝutadon (ero {0} de {1});\\nprovos denove poste\\n\\ndosiero: {2}\\n\\neraro \",\n\t\"u_ehstmp\": \"reprovos poste; rigardu sube-dekstre\",\n\t\"u_ehsfin\": \"servilo rifuzis peton por finigi alŝutadon; reprovado...\",\n\t\"u_ehssrch\": \"servilo rifuzis peton por ŝerco; reprovado...\",\n\t\"u_ehsinit\": \"servilo rifuzis peton por komenco de alŝutado; reprovado...\",\n\t\"u_eneths\": \"reta eraro dum alŝutada kvitanco; reprovado...\",\n\t\"u_enethd\": \"reta eraro dum kontrolo de ekzistado de cela dosiero; reprovado...\",\n\t\"u_cbusy\": \"atendado, por ke la servilo estas atingebla post reta eraro...\",\n\t\"u_ehsdf\": \"servilo ne havas sufiĉe da diskospaco!\\n\\nla alŝutado reprovos daŭre, okaze de iu liberigas\\nsufiĉe da diskospaco\",\n\t\"u_emtleak1\": \"ŝajnas, ke via retumilo likas memoron; \\nbonvolu\",\n\t\"u_emtleak2\": ' <a href=\"{0}\">uzi protokolon HTTPS (rekomendita)</a> aŭ ',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": 'provu fari tiel:\\n<ul><li>reŝargu la paĝon per klavo <code>F5</code></li><li>, do malŝalti la butonon &nbsp;<code>mt</code>&nbsp; en la &nbsp;<code>⚙️ agordoj</code></li><li>kaj reprovu alŝuton</li></ul>Alŝuto estos pli malrapida, sed nenio fareblas.\\nPardonon por la ĝenaĵo!\\n\\nPS: chrome v107 <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">korektis ĉi tion</a>',\n\t\"u_emtleakf\": 'provu fari tiel:\\n<ul><li>reŝargu la paĝon per klavo <code>F5</code></li><li>, do malŝalti la butonon &nbsp;<code>mt</code>&nbsp; en la &nbsp;<code>⚙️ agordoj</code></li><li>kaj reprovu alŝuton</li></ul>\\nPS: firefox <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">espereble korektos ĉi tion</a> baldaŭ',\n\t\"u_s404\": \"ne trovita ĉe la servilo\",\n\t\"u_expl\": \"klarigi\",\n\t\"u_maxconn\": \"plimulto da retumiloj ne permesas uzi pli ol 6, sed Firefox havas agordon <code>connections-per-server</code> en <code>about:config</code>, per kiu la limo ŝanĝeblas\",\n\t\"u_tu\": '<p class=\"warn\">AVERTO: rapidega reĝimo ŝaltita, <span>&nbsp;kliento povas ne detekti kaj daŭrigi nefinigitajn alŝutojn; rigardu la ŝpruchelpilon de rapidega-butono</span></p>',\n\t\"u_ts\": '<p class=\"warn\">AVERTO: rapidega reĝimo ŝaltita, <span>&nbsp;serĉrezultoj povas esti eraraj; rigardu la ŝpruchelpilon de rapidega-butono</span></p>',\n\t\"u_turbo_c\": \"rapidega reĝimo estas malpermesita laŭ servilaj agordoj\",\n\t\"u_turbo_g\": \"rapidega reĝimo estas malpermesita, ĉar vi\\nne rajtas listigi dosierujojn en ĉi tiu datumportilo\",\n\t\"u_life_cfg\": 'aŭtoforigado post <input id=\"lifem\" p=\"60\" /> minutoj (aŭ <input id=\"lifeh\" p=\"3600\" /> horoj)',\n\t\"u_life_est\": 'alŝuto estos forigita je <span id=\"lifew\" tt=\"local time\">---</span>',\n\t\"u_life_max\": 'ĉi tiu dosierujo postulas \\naŭtoforigadon de dosieroj post {0}',\n\t\"u_unp_ok\": 'malalŝuto permesitas por {0}',\n\t\"u_unp_ng\": 'malalŝuto NE estos permesita',\n\t\"ue_ro\": 'via atingo de ĉi tiu dosierujo estas nur-lega\\n\\n',\n\t\"ue_nl\": 'vi ne estas ensalutita',\n\t\"ue_la\": 'vi estas ensalutita kiel \"{0}\"',\n\t\"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',\n\t\"ue_ta\": 'provu alŝuti denove, ĝi devus funkcii nun',\n\t\"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\",\n\t\"ur_1uo\": \"OK: Dosiero sukcese alŝutita\",\n\t\"ur_auo\": \"OK: Ĉiuj {0} dosieroj sukcese alŝutitaj\",\n\t\"ur_1so\": \"OK: Dosiero trovita ĉe la servilo\",\n\t\"ur_aso\": \"OK: Ĉiuj {0} dosieroj trovitaj ĉe la serviloj\",\n\t\"ur_1un\": \"Alŝutado malsukcesis, pardonon\",\n\t\"ur_aun\": \"Ĉiuj {0} alŝutadoj malsukcesis, pardonon\",\n\t\"ur_1sn\": \"Dosiero estis NE trovita ĉe la servilo\",\n\t\"ur_asn\": \"La {0} dosieroj estis NE trovitaj ĉe la servilo\",\n\t\"ur_um\": \"Finita;\\n{0} alŝutoj sukcesis,\\n{1} alŝutoj malsukcesis, pardonon\",\n\t\"ur_sm\": \"Finita;\\n{0} dosieroj trovitaj ĉe la servilo,\\n{1} dosieroj NE trovitaj ĉe la servilo\",\n\n\t\"rc_opn\": \"malfermi\",\n\t\"rc_ply\": \"ludi\",\n\t\"rc_pla\": \"ludi kiel aŭdiaĵo\",\n\t\"rc_txt\": \"malfermi per tekstovidilo\",\n\t\"rc_md\": \"malfermi per tekstoredaktilo\",\n\t\"rc_dl\": \"elŝuti\",\n\t\"rc_zip\": \"elŝuti kiel arkivo\",\n\t\"rc_cpl\": \"kopii ligilon\",\n\t\"rc_del\": \"forigi\",\n\t\"rc_cut\": \"eltondi\",\n\t\"rc_cpy\": \"kopii\",\n\t\"rc_pst\": \"alglui\",\n\t\"rc_rnm\": \"alinomi\",\n\t\"rc_nfo\": \"nova dosierujo\",\n\t\"rc_nfi\": \"nova dosiero\",\n\t\"rc_sal\": \"elekti ĉiujn\",\n\t\"rc_sin\": \"inversigi elekton\",\n\t\"rc_shf\": \"kunhavigi ĉi tiun dosierujon\",\n\t\"rc_shs\": \"kunhavigi elekton\",\n\n\t\"lang_set\": \"ĉu reŝargi paĝon por efektivigi lingvo-ŝanĝon?\",\n\n\t\"splash\": {\n\t\t\"a1\": \"reŝargi\",\n\t\t\"b1\": \"sal, nekonatulo &nbsp; <small>(vi ne estas ensalutita)</small>\",\n\t\t\"c1\": \"elsaluti\",\n\t\t\"d1\": \"montru stakon\",\n\t\t\"d2\": \"montras la staton de ĉiuj aktivaj fadenoj\",\n\t\t\"e1\": \"reŝargi CFGon\",\n\t\t\"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\",\n\t\t\"f1\": \"vi povas vidi:\",\n\t\t\"g1\": \"vi povas alŝuti al:\",\n\t\t\"cc1\": \"aliaĵoj:\",\n\t\t\"h1\": \"malŝalti k304-on\",\n\t\t\"i1\": \"ŝalti k304-on\",\n\t\t\"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), <em>sed</em> ĝi ankaŭ plimalrapidigas ĉion\",\n\t\t\"k1\": \"rekomenci agordojn de kliento\",\n\t\t\"l1\": \"ensaluti por pli da opcioj:\",\n\t\t\"ls3\": \"ensaluti\",\n\t\t\"lu4\": \"uzantnomo\",\n\t\t\"lp4\": \"pasvorto\",\n\t\t\"lo3\": \"ensaluti kiel “{0}” ĉie\",\n\t\t\"lo2\": \"ĉi tiu finigos seancon en ĉiuj retumiloj\",\n\t\t\"m1\": \"bonvenon denove,\",\n\t\t\"n1\": \"404 ne trovita &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": 'aŭ eble vi ne havas rajton -- provu uzi pasvorton aŭ <a href=\"' + SR + '/?h\">iri hejmen</a>',\n\t\t\"p1\": \"403 ne permesita &nbsp;~┻━┻\",\n\t\t\"q1\": 'uzu pasvorton aŭ <a href=\"' + SR + '/?h\">iru hejmen</a>',\n\t\t\"r1\": \"hejmen\",\n\t\t\".s1\": \"reskani\",\n\t\t\"t1\": \"ago\",\n\t\t\"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\",\n\t\t\"v1\": \"konekti\",\n\t\t\"v2\": \"uzi ĉi tiun servilon kiel loka disko\",\n\t\t\"w1\": \"uzi HTTPS-protokolon\",\n\t\t\"x1\": \"ŝanĝi pasvorton\",\n\t\t\"y1\": \"redakti komunaĵojn\",\n\t\t\"z1\": \"malŝlosi ĉi tiun komunaĵon:\",\n\t\t\"ta1\": \"entajpu novan pasvorton unue\",\n\t\t\"ta2\": \"retajpu por konfirmi:\",\n\t\t\"ta3\": \"tajpo-eraro; bonvolu provu denove\",\n\t\t\"nop\": \"ERARO: Pasvorto ne povas esti malplena\",\n\t\t\"nou\": \"ERARO: Uzantnomo kaj/aŭ pasvorto ne povas esti malplena\",\n\t\t\"aa1\": \"aktivaj alŝutoj:\",\n\t\t\"ab1\": \"malŝalti no304-on\",\n\t\t\"ac1\": \"ŝalti no304-on\",\n\t\t\"ad1\": \"no304 malŝaltas ĉiun kaŝmemoradon; provu ĉi tion, se k304 ne riparis la difektojn. Ĉi tiu agordo malŝparas multon da datumtrafiko!\",\n\t\t\"ae1\": \"aktivaj elŝutoj:\",\n\t\t\"af1\": \"montri lastajn alŝutojn\",\n\t\t\"ag1\": \"montri kaŝmemoron de idp\",\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/fin.js",
    "content": "\n// Rivut, jotka päättyvät //m, ovat varmentamattomia konekäännöksiä\n\nLs.fin = {\n\t\"tt\": \"Suomi\",\n\n\t\"cols\": {\n\t\t\"c\": \"toimintopainikkeet\",\n\t\t\"dur\": \"kesto\",\n\t\t\"q\": \"laatu / bittinopeus\",\n\t\t\"Ac\": \"äänikoodekki\",\n\t\t\"Vc\": \"videokoodekki\",\n\t\t\"Fmt\": \"formaatti / säiliö\",\n\t\t\"Ahash\": \"äänen tarkistussumma\",\n\t\t\"Vhash\": \"videon tarkistussumma\",\n\t\t\"Res\": \"resoluutio\",\n\t\t\"T\": \"tiedostotyyppi\",\n\t\t\"aq\": \"äänenlaatu / bittinopeus\",\n\t\t\"vq\": \"kuvalaatu / bittinopeus\",\n\t\t\"pixfmt\": \"alinäytteistys / pikselirakenne\",\n\t\t\"resw\": \"horisontaalinen resoluutio\",\n\t\t\"resh\": \"vertikaalinen resoluutio\",\n\t\t\"chs\": \"äänikanavat\",\n\t\t\"hz\": \"näytteenottotaajuus\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"misc\",\n\t\t\t[\"ESC\", \"sulje asioita\"],\n\n\t\t\t\"file-manager\",\n\t\t\t[\"G\", \"vaihda lista/kuvanäkymään\"],\n\t\t\t[\"T\", \"vaihda pienoiskuviin/kuvakkeisiin\"],\n\t\t\t[\"⇧ A/D\", \"pienoiskuvien koko\"],\n\t\t\t[\"ctrl-K\", \"poista valitut\"],\n\t\t\t[\"ctrl-X\", \"siirrä valitut leikepöydälle\"],\n\t\t\t[\"ctrl-C\", \"kopioi valitut leikepöydälle\"],\n\t\t\t[\"ctrl-V\", \"siirrä tai kopioi tähän\"],\n\t\t\t[\"Y\", \"lataa valitut\"],\n\t\t\t[\"F2\", \"uudelleennimeä valitut\"],\n\n\t\t\t\"file-list-sel\",\n\t\t\t[\"space\", \"vaihda tiedostonvalintatilaan\"],\n\t\t\t[\"↑/↓\", \"siirrä valintaosoitinta\"],\n\t\t\t[\"ctrl ↑/↓\", \"siirrä osoitinta ja näkymää\"],\n\t\t\t[\"⇧ ↑/↓\", \"valitse edellinen/seuraava tiedosto\"],\n\t\t\t[\"ctrl-A\", \"valitse kaikki tiedostot / hakemistot\"],\n\t\t], [\n\t\t\t\"navigation\",\n\t\t\t[\"B\", \"näytä linkkipolku\"],\n\t\t\t[\"I/K\", \"siirry edelliseen/seuraavaan hakemistoon\"],\n\t\t\t[\"M\", \"siirry ylähakemistoon/supista nykyinen hakemisto\"],\n\t\t\t[\"V\", \"näytä hakemistot/tekstitiedostot navigointipaneelissa\"],\n\t\t\t[\"A/D\", \"navigointipaneelin koko\"],\n\t\t], [\n\t\t\t\"audio-player\",\n\t\t\t[\"J/L\", \"edellinen/seuraava kappale\"],\n\t\t\t[\"U/O\", \"kelaa 10s taaksepäin/eteenpäin\"],\n\t\t\t[\"0..9\", \"siirry 0%..90%\"],\n\t\t\t[\"P\", \"toista/pysäytä kappale\"],\n\t\t\t[\"S\", \"valitse toistossa oleva kappale\"],\n\t\t\t[\"Y\", \"lataa kappale\"],\n\t\t], [\n\t\t\t\"image-viewer\",\n\t\t\t[\"J/L, ←/→\", \"edellinen/seuraava kuva\"],\n\t\t\t[\"Home/End\", \"ensimmäinen/viimeinen kuva\"],\n\t\t\t[\"F\", \"siirry koko näytön tilaan\"],\n\t\t\t[\"R\", \"kierrä myötäpäivään\"],\n\t\t\t[\"⇧ R\", \"kierrä vastapäivään\"],\n\t\t\t[\"S\", \"valitse kuva\"],\n\t\t\t[\"Y\", \"lataa kuva\"],\n\t\t], [\n\t\t\t\"video-player\",\n\t\t\t[\"U/O\", \"kelaa 10s taaksepäin/eteenpäin\"],\n\t\t\t[\"P/K/Space\", \"toista/pysäytä video\"],\n\t\t\t[\"C\", \"jatka toistoa seuraavaan videoon\"],\n\t\t\t[\"V\", \"toista uudelleen\"],\n\t\t\t[\"M\", \"vaimenna\"],\n\t\t\t[\"[ ja ]\", \"aseta videon uudelleentoistoväli\"],\n\t\t], [\n\t\t\t\"textfile-viewer\",\n\t\t\t[\"I/K\", \"edellinen/seuraava tiedosto\"],\n\t\t\t[\"M\", \"sulje tekstitiedosto\"],\n\t\t\t[\"E\", \"muokkaa tekstitiedostoa\"],\n\t\t\t[\"S\", \"valitse tiedosto (leikkausta/kopiointia/uudelleennimeämistä varten)\"],\n\t\t\t[\"Y\", \"lataa tekstitiedosto\"],\n\t\t\t[\"⇧ J\", \"muotoile/siisti json\"],\n\t\t]\n\t],\n\n\t\"m_ok\": \"OK\",\n\t\"m_ng\": \"Peruuta\",\n\n\t\"enable\": \"Aktivoi\",\n\t\"danger\": \"HUOMIO!\",\n\t\"clipped\": \"kopioitu leikepöydälle\",\n\n\t\"ht_s1\": \"sekunti\",\n\t\"ht_s2\": \"sekuntia\",\n\t\"ht_m1\": \"minuutti\",\n\t\"ht_m2\": \"minuuttia\",\n\t\"ht_h1\": \"tunti\",\n\t\"ht_h2\": \"tuntia\",\n\t\"ht_d1\": \"päivä\",\n\t\"ht_d2\": \"päivää\",\n\t\"ht_and\": \" ja \",\n\n\t\"goh\": \"hallintapaneeli\",\n\t\"gop\": 'viereinen hakemisto\">edell',\n\t\"gou\": 'ylempi hakemisto\">ylös',\n\t\"gon\": 'seuraava hakemisto\">seur',\n\t\"logout\": \"Kirjaudu ulos \",\n\t\"login\": \"Kirjaudu sisään\",\n\t\"access\": \" -oikeudet\",\n\t\"ot_close\": \"sulje alavalikko\",\n\t\"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`&quot;try unite&quot;` = 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`\",\n\t\"ot_unpost\": \"unpost: poista viimeaikaiset tai keskeytä keskeneräiset lataukset\",\n\t\"ot_bup\": \"bup: tiedostojen 'perus'lähetysohjelma, tukee jopa netscape 4.0\",\n\t\"ot_mkdir\": \"mkdir: luo uusi hakemisto\",\n\t\"ot_md\": \"new-file: luo uusi tekstitiedosto\",\n\t\"ot_msg\": \"msg: lähetä viesti palvelinlokiin\",\n\t\"ot_mp\": \"mediasoittimen asetukset\",\n\t\"ot_cfg\": \"asetukset\",\n\t\"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 [🎈]&nbsp; (peruslatausohjelma)<br /><br />tiedostojen lähetyksen aikana tämä kuvake muuttuu kertoo lähetyksen edistymisestilanteen!',\n\t\"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 [🎈]&nbsp; (peruslatausohjelma)<br /><br />tiedostojen lähetyksen aikana tämä kuvake muuttuu kertoo lähetyksen edistymisestilanteen!',\n\t\"ot_noie\": 'Suosittelemme käyttämään uudempaa selainta.',\n\n\t\"ab_mkdir\": \"luo hakemisto\",\n\t\"ab_mkdoc\": \"luo tekstitiedosto\",\n\t\"ab_msg\": \"lähetä viesti palvelinlokiin\",\n\n\t\"ay_path\": \"siirry hakemistoihin\",\n\t\"ay_files\": \"siirry tiedostoihin\",\n\n\t\"wt_ren\": \"uudelleennimeä valitut kohteet$NPikanäppäin: F2\",\n\t\"wt_del\": \"poista valitut kohteet$NPikanäppäin: ctrl-K\",\n\t\"wt_cut\": \"siirrä valitut kohteet leikepöydälle &lt;small&gt;(siirtääksesi ne muualle)&lt;/small&gt;$NPikanäppäin: ctrl-X\",\n\t\"wt_cpy\": \"kopioi valitut kohteet leikepöydälle$N(liittääksesi ne muualle)$NPikanäppäin: ctrl-C\",\n\t\"wt_pst\": \"liitä aiemmin leikatut / kopioidut valinnat$NPikanäppäin: ctrl-V\",\n\t\"wt_selall\": \"valitse kaikki tiedostot$NPikanäppäin: ctrl-A (kun tiedosto on kohdistettu)\",\n\t\"wt_selinv\": \"valitse vastakkaiset tiedostot\",\n\t\"wt_zip1\": \"lataa tämä hakemisto pakattuna\",\n\t\"wt_selzip\": \"lataa valitut kohteet pakattuna\",\n\t\"wt_seldl\": \"lataa valitut kohteet paketoimatta$NPikanäppäin: Y\",\n\t\"wt_npirc\": \"kopioi kappaletiedot IRC-muotoilulla\",\n\t\"wt_nptxt\": \"kopioi kappaletiedot ilman muotoilua\",\n\t\"wt_m3ua\": \"lisää m3u-soittolistaan (klikkaa <code>📻kopioi</code> myöhemmin)\",\n\t\"wt_m3uc\": \"kopioi m3u-soittolista leikepöydälle\",\n\t\"wt_grid\": \"vaihda kuva- ja listanäkymän välillä$NPikanäppäin: G\",\n\t\"wt_prev\": \"edellinen kappale$NPikanäppäin: J\",\n\t\"wt_play\": \"toista / pysäytä$NPikanäppäin: P\",\n\t\"wt_next\": \"seuraava kappale$NPikanäppäin: L\",\n\n\t\"ul_par\": \"rinnakkaislatausten lkm:\",\n\t\"ut_rand\": \"satunnaisgeneroidut tiedostonimet\",\n\t\"ut_u2ts\": \"kopioi viimeksi muokattu aikaleima$Ntiedostojärjestelmästäsi palvelimelle\\\">📅\",\n\t\"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\",\n\t\"ut_mt\": \"jatka muiden tiedostojen tiivisteiden laskemista latauksen aikana$N$Nkannattanee poistaa käytöstä, mikäli prosessori tai kovalevy on vanhempaa mallia\",\n\t\"ut_ask\": 'kysy vahvistusta ennen latauksen aloittamista\">💭',\n\t\"ut_pot\": \"paranna latausnopeutta hitailla laitteilla$Nvähentämällä käyttöliittymän monimutkaisuutta\",\n\t\"ut_srch\": \"lataamisen sijaan tarkista, ovatko tiedostot jo $N olemassa palvelimella (käy läpi kaikki hakemistot, joihin sinulla on read-oikeudet)\",\n\t\"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\",\n\t\"ul_btn\": \"vedä tiedostoja / hakemistoja tähän<br>(tai klikkaa minua)\",\n\t\"ul_btnu\": \"L Ä H E T Ä\",\n\t\"ul_btns\": \"E T S I\",\n\n\t\"ul_hash\": \"tiiviste\",\n\t\"ul_send\": \"lähetä\",\n\t\"ul_done\": \"valmis\",\n\t\"ul_idle1\": \"ei latauksia jonossa\",\n\t\"ut_etah\": \"keskimääräinen &lt;em&gt;tiivisteiden lasku&lt;/em&gt;nopeus ja arvioitu aika valmistumiseen\",\n\t\"ut_etau\": \"keskimääräinen &lt;em&gt;lataus&lt;/em&gt;nopeus ja arvioitu aika valmistumiseen\",\n\t\"ut_etat\": \"keskimääräinen &lt;em&gt;kokonais&lt;/em&gt;nopeus ja arvioitu aika valmistumiseen\",\n\n\t\"uct_ok\": \"onnistui\",\n\t\"uct_ng\": \"ei-hyvä: epäonnistui / hylätty / ei löydy\",\n\t\"uct_done\": \"ok ja ng yhdistettynä\",\n\t\"uct_bz\": \"laskee tiivisteitä tai lataa\",\n\t\"uct_q\": \"tyhjäkäynnillä, odottaa\",\n\n\t\"utl_name\": \"tiedostonimi\",\n\t\"utl_ulist\": \"lista\",\n\t\"utl_ucopy\": \"kopioi\",\n\t\"utl_links\": \"linkit\",\n\t\"utl_stat\": \"tila\",\n\t\"utl_prog\": \"edistyminen\",\n\n\t// keep short:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"VIRHE\",\n\t\"utl_oserr\": \"Käyttöjärjestelmävirhe\",\n\t\"utl_found\": \"löytyi\",\n\t\"utl_defer\": \"lykkää\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"valmis\",\n\n\t\"ul_flagblk\": \"tiedostot lisättiin jonoon</b><br>mutta toisen selainvälilehden up2k on kiireinen,<br>joten odotetaan sen valmistumista ensin\",\n\t\"ul_btnlk\": \"palvelinkonfiguraatio on lukinnut tämän kytkimen tähän tilaan\",\n\n\t\"udt_up\": \"Lataa\",\n\t\"udt_srch\": \"Etsi\",\n\t\"udt_drop\": \"pudota se tähän\",\n\n\t\"u_nav_m\": '<h6>selvä, mitäs sulla on?</h6><code>Enter</code> = Tiedostoja (yksi tai useampi)\\n<code>ESC</code> = Yksi hakemisto (mukaan lukien alihakemistot)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Tiedostoja</a><a href=\"#\" id=\"modal-ng\">Yksi hakemisto</a>',\n\n\t\"cl_opts\": \"asetukset\",\n\t\"cl_hfsz\": \"tiedostokoko\",\n\t\"cl_themes\": \"teema\",\n\t\"cl_langs\": \"kieli\",\n\t\"cl_ziptype\": \"hakemiston pakkaustyyppi\",\n\t\"cl_uopts\": \"up2k-kytkimet\",\n\t\"cl_favico\": \"favicon\",\n\t\"cl_bigdir\": \"suuret hakemistot\",\n\t\"cl_hsort\": \"#sort\",\n\t\"cl_keytype\": \"sävellajin notaatiotyyppi\",\n\t\"cl_hiddenc\": \"piilotetut sarakkeet\",\n\t\"cl_hidec\": \"piilota\",\n\t\"cl_reset\": \"palauta\",\n\t\"cl_hpick\": \"napauta sarakeotsikoita piilottaaksesi alla olevassa taulukossa\",\n\t\"cl_hcancel\": \"sarakkeiden piilotus peruttu\",\n\t\"cl_rcm\": \"hiiren pikavalikko\",\n\n\t\"ct_grid\": '田 kuvanäkymä',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ vihjelaatikot',\n\t\"ct_thumb\": 'valitse kuvakkeiden / pienoiskuvien välillä kuvanäkymässä $NPikanäppäin: T\">🖼️ pienoiskuvat',\n\t\"ct_csel\": 'käytä CTRL ja SHIFT tiedostojen valintaan kuvanäkymässä\">valitse',\n\t\"ct_dsel\": 'käytä aluevalintaa tiedostojen valintaan kuvanäkymässä\">aluevalinta',\n\t\"ct_dl\": 'pakota lataus (älä näytä upotettuna), kun tiedostoa klikataan\">dl',\n\t\"ct_ihop\": 'kun kuvakatselin suljetaan, vieritä alas viimeksi katsottuun tiedostoon\">g⮯',\n\t\"ct_dots\": 'näytä piilotetut tiedostot (jos palvelin sallii)\">piilotiedostot',\n\t\"ct_qdel\": 'kysy vahvistusta vain kerran tiedostoja poistaessa\">qdel',\n\t\"ct_dir1st\": 'lajittele hakemistot ennen tiedostoja\">📁 ensin',\n\t\"ct_nsort\": 'luonnollinen lajittelu (tiedostonimille jotka ovat numeroalkuisia)\">nsort',\n\t\"ct_utc\": 'näytä kaikki aikaleimat UTC-ajassa\">UTC',\n\t\"ct_readme\": 'näytä README.md hakemistolistauksissa\">📜 readme',\n\t\"ct_idxh\": 'näytä index.html hakemistolistan sijasta\">htm',\n\t\"ct_sbars\": 'näytä vierityspalkit\">⟊',\n\n\t\"cut_umod\": \"jos tiedosto on jo olemassa palvelimella, päivitä palvelimen viimeksi muokattu aikaleima vastaamaan paikallista tiedostoasi (vaatii write- ja delete-oikeudet)\\\">re📅\",\n\n\t\"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 <em>&quot;onko tällä sama tiedostokoko palvelimella?&quot;</em> 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 &quot;ladata&quot; samat tiedostot uudelleen antaaksesi selaimesi varmistaa ne\\\">turbo\",\n\n\t\"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 <em>teoriassa</em> napata useimmat keskeneräiset / vioittuneet lataukset, mutta ei ole korvike varmistuskierrokselle turbo poistettuna käytöstä jälkeenpäin\\\">päiväysvarmistin\",\n\n\t\"cut_u2sz\": \"kunkin lähetyspalan koko (MiB:ssä); suuret arvot lentävät paremmin atlantin yli. kokeile pieniä arvoja erittäin heikoilla yhteyksillä\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cut_nag\": \"käyttöjärjestelmäilmoitus kun lataus valmistuu$N(vain jos selain tai välilehti ei ole aktiivinen)\",\n\t\"cut_sfx\": \"äänivaroitus kun lataus valmistuu$N(vain jos selain tai välilehti ei ole aktiivinen)\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cft_text\": \"favicon-teksti (tyhjennä ja päivitä poistaaksesi käytöstä)\",\n\t\"cft_fg\": \"edustaväri\",\n\t\"cft_bg\": \"taustaväri\",\n\n\t\"cdt_lim\": \"tiedostojen enimmäismäärä näytettäväksi hakemistossa\",\n\t\"cdt_ask\": \"sivun lopussa, sen sijaan että lataa $Nautomaattisesti lisää tiedostoja, kysy mitä tehdä\",\n\t\"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ä\",\n\t\"cdt_ren\": \"ota käyttöön mukautettu pikavalikko, tavallinen valikko on käytettävissä painamalla shift ja napsauttamalla hiiren oikeanpuoleisella painikkeella\\\">aktivoi\",\n\t\"cdt_rdb\": \"näytä tavallinen pikavalikko, kun mukautettu on jo auki ja oikeanpuoleista painiketta painetaan uudelleen\\\">x2\",\n\n\t\"tt_entree\": \"näytä navigointipaneeli$NPikanäppäin: B\",\n\t\"tt_detree\": \"näytä linkkipolku$NPikanäppäin: B\",\n\t\"tt_visdir\": \"näytä valittu hakemisto\",\n\t\"tt_ftree\": \"vaihda linkkipolku- / tekstitiedostonäkymään$NPikanäppäin: V\",\n\t\"tt_pdock\": \"näytä ylähakemistot telakoitussa paneelissa ylhäällä\",\n\t\"tt_dynt\": \"kasvata automaattisesti hakemistosyvyyden kasvaessa\",\n\t\"tt_wrap\": \"rivitys\",\n\t\"tt_hover\": \"paljasta ylivuotavat rivit leijutettaessa$N( rikkoo vierityksen ellei hiiri $N&nbsp; ole vasemmassa marginaalissa )\",\n\n\t\"ml_pmode\": \"hakemiston lopussa...\",\n\t\"ml_btns\": \"komennot\",\n\t\"ml_tcode\": \"muunna nämä\",\n\t\"ml_tcode2\": \"tähän muotoon\",\n\t\"ml_tint\": \"sävy\",\n\t\"ml_eq\": \"taajuuskorjain\",\n\t\"ml_drc\": \"dynaaminen alueen kompressori\",\n\t\"ml_ss\": \"ohita hiljaiset kohdat\",\n\n\t\"mt_loop\": \"toista samaa kappaletta\\\">🔁\",\n\t\"mt_one\": \"lopeta yhden toiston jälkeen\\\">1️⃣\",\n\t\"mt_shuf\": \"aktivoi satunnaistoisto\\\">🔀\",\n\t\"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▶\",\n\t\"mt_preload\": \"aloita seuraavan kappaleen lataaminen lähellä loppua, mahdollistaen saumattoman toiston\\\">esilataus\",\n\t\"mt_prescan\": \"siirry seuraavaan hakemistoon ennen viimeisen kappaleen$Nloppumista, pitäen verkkoselaimen tyytyväisenä$Njotta se ei pysäytä toistoa\\\">nav\",\n\t\"mt_fullpre\": \"yritä esiladata koko kappale;$N✅ ota käyttöön <b>heikoilla</b> yhteyksillä,$N❌ <b>poista käytöstä</b> hitailla yhteyksillä\\\">esi+\",\n\t\"mt_fau\": \"puhelimissa: estä musiikin pysähtyminen jos seuraava kappale ei esilataudu tarpeeksi nopeasti (voi aiheuttaa ongelmia kappaletietojen näyttämisessä)\\\">☕️\",\n\t\"mt_waves\": \"aaltomuoto-hakupalkki:$Nnäytä äänenvahvuus selaimessa\\\">~s\",\n\t\"mt_npclip\": \"näytä painikkeet parhaillaan soivan kappaleen leikepöydälle kopioimiseen\\\">/np\",\n\t\"mt_m3u_c\": \"näytä painikkeet valittujen$Nkappaleiden kopioimiseen m3u8-soittolistana leikepöydälle\\\">📻\",\n\t\"mt_octl\": \"käyttöjärjestelmäintegraatio (medianäppäimet / osd)\\\">os-ctl\",\n\t\"mt_oseek\": \"salli haku käyttöjärjestelmäintegraation kautta$N$Nhuom: joissakin laitteissa (iPhonet),$Ntämä korvaa 'seuraava kappale' -painikkeen\\\">kelaus\",\n\t\"mt_oscv\": \"näytä albumin kansi osd:ssä\\\">kansikuvat\",\n\t\"mt_follow\": \"pidä soiva kappale näkyvissä\\\">🎯\",\n\t\"mt_compact\": \"kompaktit säätimet\\\">⟎\",\n\t\"mt_uncache\": \"tyhjennä välimuisti &nbsp;(kokeile tätä jos selaimesi välimuistissa on$Nrikkinäinen kopio kappaleesta)\\\">uncache\",\n\t\"mt_mloop\": \"toista avoinna olevaa hakemistoa loputtomasti\\\">🔁 alkuun\",\n\t\"mt_mnext\": \"lataa seuraava hakemisto ja jatka\\\">📂 seuraava\",\n\t\"mt_mstop\": \"pysäytä toisto\\\">⏸ pysäytä\",\n\t\"mt_cflac\": \"muunna flac / wav {0}-muotoon\\\">flac\",\n\t\"mt_caac\": \"muunna aac / m4a {0}-muotoon\\\">aac\",\n\t\"mt_coth\": \"muunna kaikki muut paitsi mp3 {0}-muotoon\\\">muut\",\n\t\"mt_c2opus\": \"paras valinta pöytäkoneille, kannettaville, androidille\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, iOS 17.5:lle ja uudemmille\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, iOS 11:lle - 17:lle\\\">caf\",\n\t\"mt_c2mp3\": \"käytä tätä erittäin vanhoissa laitteissa\\\">mp3\",\n\t\"mt_c2flac\": \"paras äänenlaatu, suuremmat latauskoot\\\">flac\",\n\t\"mt_c2wav\": \"pakkaamaton toisto, suurimmat latauskoot\\\">wav\",\n\t\"mt_c2ok\": \"hienoa, hyvä valinta\",\n\t\"mt_c2nd\": \"tuo ei ole suositeltu formaatti laitteellesi, mutta tee miten lystäät\",\n\t\"mt_c2ng\": \"laitteesi ei näytä tukevan tätä formaattia, mutta yritetään nyt silti\",\n\t\"mt_xowa\": \"iOS:ssä on bugeja jotka estävät taustatoiston tällä formaatilla; käytä caf:ia tai mp3:a sen sijaan\",\n\t\"mt_tint\": \"taustan taso (0-100) liukupalkissa$Ntehden puskuroinnista vähemmän häiritsevän\",\n\t\"mt_eq\": \"`aktivoi taajuuskorjaimen ja vahvistussäätimen;$N$Nvahvistus `0` = normaali 100% äänenvoimakkuus (muokkaamaton)$N$Nleveys `1 &nbsp;` = normaali stereo (muokkaamaton)$Nleveys `0.5` = 50% vasen-oikea ristisyöttö$Nleveys `0 &nbsp;` = mono$N$Nvahvistus `-0.8` &amp; 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ä\",\n\t\"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)\",\n\t\"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`%\",\n\t\"mt_ssvt\": \"äänenvoimakkuuden kynnysarvo (0-255)\\\">vol\",\n\t\"mt_ssts\": \"alkuraja (prosenttia kappaleen alusta)\\\">alk\",\n\t\"mt_sste\": \"loppuraja (prosenttia kappaleen lopusta)\\\">lop\",\n\t\"mt_sssm\": \"toistonopeuden kerroin (min. 0.15, max. 8))\\\">nop\",\n\n\t\"mb_play\": \"toista\",\n\t\"mm_hashplay\": \"soita tämä äänitiedosto?\",\n\t\"mm_m3u\": \"paina <code>Enter/OK</code> Toistaaksesi\\npaina <code>ESC/Peruuta</code> Muokataksesi\",\n\t\"mp_breq\": \"tarvitset firefox 82+ tai chrome 73+ tai iOS 15+\",\n\t\"mm_bload\": \"ladataan...\",\n\t\"mm_bconv\": \"muunnetaan muotoon {0}, odota...\",\n\t\"mm_opusen\": \"selaimesi ei voi toistaa aac / m4a -tiedostoja;\\ntranskoodaus opukseen on nyt käytössä\",\n\t\"mm_playerr\": \"toisto epäonnistui: \",\n\t\"mm_eabrt\": \"Toistoyritys peruttiin\",\n\t\"mm_enet\": \"Internet-yhteytesi on epävakaa\",\n\t\"mm_edec\": \"Tämä tiedosto on väitetysti vioittunut??\",\n\t\"mm_esupp\": \"Selaimesi ei ymmärrä tätä äänimuotoa\",\n\t\"mm_eunk\": \"Tuntematon virhe\",\n\t\"mm_e404\": \"Kappaletta ei voitu toistaa; virhe 404: Tiedostoa ei löydy.\",\n\t\"mm_e403\": \"Kappaletta ei voitu toistaa; virhe 403: Pääsy kielletty.\\n\\nKokeile painaa F5 päivittääksesi, ehkä kirjauduit ulos\",\n\t\"mm_e415\": \"Kappaletta ei voitu toistaa; virhe 415: Tiedoston muunnos epäonnistui; tarkista palvelimen lokitiedostot.\",\n\t\"mm_e500\": \"Kappaletta ei voitu toistaa; virhe 500: Tarkista palvelinlokit.\",\n\t\"mm_e5xx\": \"Kappaletta ei voitu toistaa; palvelinvirhe \",\n\t\"mm_nof\": \"ei löydy enempää äänitiedostoja lähistöltä\",\n\t\"mm_prescan\": \"Etsitään musiikkia toistettavaksi seuraavaksi...\",\n\t\"mm_scank\": \"Löytyi seuraava kappale:\",\n\t\"mm_uncache\": \"välimuisti tyhjennetty; kaikki kappaleet ladataan uudelleen seuraavalla toistolla\",\n\t\"mm_hnf\": \"tuota kappaletta ei enää ole olemassa\",\n\n\t\"im_hnf\": \"tuota kuvaa ei enää ole olemassa\",\n\n\t\"f_empty\": 'tämä hakemisto on tyhjä',\n\t\"f_chide\": 'tämä piilottaa sarakkeen «{0}»\\n\\nvoit palauttaa sarakkeet asetuksista',\n\t\"f_bigtxt\": \"tämä tiedosto on {0} Mt kokoinen -- näytetäänkö silti tekstinä?\",\n\t\"f_bigtxt2\": \"näytetäänkö vain tiedoston loppu? tämä myös mahdollistaa seuraamisen/tailing, näyttäen uudet tekstirivit reaaliaikaisesti\",\n\t\"fbd_more\": '<div id=\"blazy\">näytetään <code>{0}</code> / <code>{1}</code> tiedostoa; <a href=\"#\" id=\"bd_more\">näytä {2}</a> tai <a href=\"#\" id=\"bd_all\">näytä kaikki</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\">näytetään <code>{0}</code> / <code>{1}</code> tiedostoa; <a href=\"#\" id=\"bd_all\">näytä kaikki</a></div>',\n\t\"f_anota\": \"vain {0} / {1} kohdetta valittiin;\\nvalitaksesi koko hakemiston, vieritä ensin loppuun\",\n\n\t\"f_dls\": 'nykyisen hakemiston tiedostolinkit on\\nvaihdettu latauslinkeiksi',\n\t\"f_dl_nd\": 'ohitetaan kansio (käytä sen sijaan zip/tar-latausta):\\n',\n\n\t\"f_partial\": \"Ladataksesi turvallisesti tiedoston joka on parhaillaan latautumassa, klikkaa tiedostoa jolla on sama nimi mutta ilman <code>.PARTIAL</code> päätettä. Paina PERUUTA tai Escape tehdäksesi tämän.\\n\\nOK / Enter painaminen sivuuttaa tämän varoituksen ja jatkaa <code>.PARTIAL</code> väliaikaistiedoston lataamista, mikä todennäköisesti antaa sinulle vioittunutta dataa.\",\n\n\t\"ft_paste\": \"liitä {0} kohdetta$NPikanäppäin: ctrl-V\",\n\t\"fr_eperm\": 'ei voida nimetä uudelleen:\\nsinulla ei ole “move”-oikeutta tässä hakemistossa',\n\t\"fd_eperm\": 'ei voida poistaa:\\nsinulla ei ole “delete” oikeutta tässä hakemistossa',\n\t\"fc_eperm\": 'ei voida leikata:\\nsinulla ei ole “move” oikeutta tässä hakemistossa',\n\t\"fp_eperm\": 'ei voida liittää:\\nsinulla ei ole “write” oikeutta tässä hakemistossa',\n\t\"fr_emore\": \"valitse vähintään yksi kohde uudelleennimettäväksi\",\n\t\"fd_emore\": \"valitse vähintään yksi kohde poistettavaksi\",\n\t\"fc_emore\": \"valitse vähintään yksi kohde leikattavaksi\",\n\t\"fcp_emore\": \"valitse vähintään yksi kohde kopioitavaksi leikepöydälle\",\n\n\t\"fs_sc\": \"jaa hakemisto jossa olet\",\n\t\"fs_ss\": \"jaa valitut tiedostot\",\n\t\"fs_just1d\": \"et voi valita useampaa kuin yhtä,\\ntai sekoittaa tiedostoja ja hakemistoja yhdessä valinnassa\",\n\t\"fs_abrt\": \"❌ keskeytä\",\n\t\"fs_rand\": \"🎲 joku.nimi\",\n\t\"fs_go\": \"✅ luo share\",\n\t\"fs_name\": \"nimi\",\n\t\"fs_src\": \"lähde\",\n\t\"fs_pwd\": \"salasana\",\n\t\"fs_exp\": \"vanheneminen\",\n\t\"fs_tmin\": \"min\",\n\t\"fs_thrs\": \"tuntia\",\n\t\"fs_tdays\": \"päivää\",\n\t\"fs_never\": \"ikuinen\",\n\t\"fs_pname\": \"valinnainen linkin nimi; on satunnainen jos tyhjä\",\n\t\"fs_tsrc\": \"jaettava tiedosto tai hakemisto\",\n\t\"fs_ppwd\": \"valinnainen salasana\",\n\t\"fs_w8\": \"luodaan sharea...\",\n\t\"fs_ok\": \"paina <code>Enter/OK</code> lisätäksesi leikepöydälle\\npaina <code>ESC/Peruuta</code> sulkeaksesi\",\n\n\t\"frt_dec\": \"saattaa korjata joitakin rikkinäisiä tiedostonimiä\\\">url-decode\",\n\t\"frt_rst\": \"palauta muokatut tiedostonimet takaisin alkuperäisiksi\\\">↺ palauta\",\n\t\"frt_abrt\": \"keskeytä ja sulje tämä ikkuna\\\">❌ peruuta\",\n\t\"frb_apply\": \"UUDELLEENNIMEÄ\",\n\t\"fr_adv\": \"erä / liitännäistiedot / kaava uudelleennimeäminen\\\">lisäasetukset\",\n\t\"fr_case\": \"isot ja pienet kirjaimet erottava regex\\\">kirjainkoko\",\n\t\"fr_win\": \"windows-yhteensopivat nimet; korvaa <code>&lt;&gt;:&quot;\\\\|?*</code> japanilaisilla leveillä merkeillä\\\">win\",\n\t\"fr_slash\": \"korvaa <code>/</code> merkillä joka ei aiheuta uusien hakemistoiden luomista\\\">ei /\",\n\t\"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\",\n\t\"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)` \",\n\t\"fr_pdel\": \"poista\",\n\t\"fr_pnew\": \"tallenna nimellä\",\n\t\"fr_pname\": \"anna nimi uudelle esiasetuksellesi\",\n\t\"fr_aborted\": \"keskeytetty\",\n\t\"fr_lold\": \"vanha nimi\",\n\t\"fr_lnew\": \"uusi nimi\",\n\t\"fr_tags\": \"valittujen tiedostojen tagit (vain luku, viitetarkoituksiin):\",\n\t\"fr_busy\": \"nimetään uudelleen {0} kohdetta...\\n\\n{1}\",\n\t\"fr_efail\": \"uudelleennimeäminen epäonnistui:\\n\",\n\t\"fr_nchg\": \"{0} uusista nimistä muutettiin <code>win</code> ja/tai <code>ei /</code> vuoksi\\n\\nJatketaanko näillä muutetuilla uusilla nimillä?\",\n\n\t\"fd_ok\": \"poisto OK\",\n\t\"fd_err\": \"poisto epäonnistui:\\n\",\n\t\"fd_none\": \"mitään ei poistettu; ehkä palvelimen asetukset estivät (xbd)?\",\n\t\"fd_busy\": \"poistetaan {0} kohdetta...\\n\\n{1}\",\n\t\"fd_warn1\": \"POISTA nämä {0} kohdetta?\",\n\t\"fd_warn2\": \"<b>Viimeinen varoitus!</b> Haluatko varmasti poistaa?\",\n\n\t\"fc_ok\": \"siirettiin {0} kohdetta leikepöydälle\",\n\t\"fc_warn\": 'siirettiin {0} kohdetta leikepöydälle\\n\\nmutta: vain <b>tämä</b> selain-välilehti voi liittää ne\\n(koska valinta on niin valtavan suuri)',\n\n\t\"fcc_ok\": \"kopioitiin {0} kohdetta leikepöydälle\",\n\t\"fcc_warn\": 'kopioitiin {0} kohdetta leikepöydälle\\n\\nmutta: vain <b>tämä</b> selain-välilehti voi liittää ne\\n(koska valinta on niin valtavan suuri)',\n\n\t\"fp_apply\": \"käytä näitä nimiä\",\n\t\"fp_skip\": \"ohita ristiriitatilanteissa\",\n\t\"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ä\",\n\t\"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:\",\n\t\"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:\",\n\t\"fp_emore\": \"tiedostonimien törmäyksiä on vielä korjaamatta\",\n\t\"fp_ok\": \"siirto OK\",\n\t\"fcp_ok\": \"kopiointi OK\",\n\t\"fp_busy\": \"siirretään {0} kohdetta...\\n\\n{1}\",\n\t\"fcp_busy\": \"kopioidaan {0} kohdetta...\\n\\n{1}\",\n\t\"fp_abrt\": \"keskeytetään...\",\n\t\"fp_err\": \"siirto epäonnistui:\\n\",\n\t\"fcp_err\": \"kopiointi epäonnistui:\\n\",\n\t\"fp_confirm\": \"siirrä nämä {0} kohdetta tänne?\",\n\t\"fcp_confirm\": \"kopioi nämä {0} kohdetta tänne?\",\n\t\"fp_etab\": 'leikepöydän lukeminen toisesta selain-välilehdestä epäonnistui',\n\t\"fp_name\": \"ladataan tiedostoa laitteeltasi. Anna sille nimi:\",\n\t\"fp_both_m\": '<h6>valitse mitä liittää</h6><code>Enter</code> = Siirrä {0} tiedostoa kohteesta «{1}»\\n<code>ESC</code> = Lataa {2} tiedostoa laitteeltasi',\n\t\"fcp_both_m\": '<h6>valitse mitä liittää</h6><code>Enter</code> = Kopioi {0} tiedostoa kohteesta «{1}»\\n<code>ESC</code> = Lataa {2} tiedostoa laitteeltasi',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Siirrä</a><a href=\"#\" id=\"modal-ng\">Lähetä</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Kopioi</a><a href=\"#\" id=\"modal-ng\">Lähetä</a>',\n\n\t\"mk_noname\": \"kirjoita nimi vasemmalla olevaan tekstikenttään ennen kuin teet tuon :p\",\n\t\"nmd_i1\": \"voit myös lisätä haluamasi tiedostopäätteen, esimerkiksi <code>.md</code>\",\n\t\"nmd_i2\": \"voit luoda vain <code>.{0}</code>-tiedostoja, koska sinulla ei ole “delete”-oikeutta\",\n\n\t\"tv_load\": \"Ladataan tekstidokumenttia:\\n\\n{0}\\n\\n{1}% ({2} / {3} Mt ladattu)\",\n\t\"tv_xe1\": \"tekstitiedoston lataaminen epäonnistui:\\n\\nvirhe \",\n\t\"tv_xe2\": \"404, tiedostoa ei löydy\",\n\t\"tv_lst\": \"tekstitiedostojen lista hakemistossa\",\n\t\"tvt_close\": \"palaa hakemistonäkymään$NPikanäppäin: M (tai Esc)\\\">❌ sulje\",\n\t\"tvt_dl\": \"lataa tämä tiedosto$NPikanäppäin: Y\\\">💾 lataa\",\n\t\"tvt_prev\": \"näytä edellinen dokumentti$NPikanäppäin: i\\\">⬆ edell\",\n\t\"tvt_next\": \"näytä seuraava dokumentti$NPikanäppäin: K\\\">⬇ seur\",\n\t\"tvt_sel\": \"valitse tiedosto &nbsp; ( leikkausta / kopiointia / poistoa / ... varten )$NPikanäppäin: S\\\">val\",\n\t\"tvt_j\": \"muotoile/siisti json$NPikanäppäin: shift-J\\\">j\",\n\t\"tvt_edit\": \"avaa tiedosto tekstieditorissa$NPikanäppäin: E\\\">✏️ muokkaa\",\n\t\"tvt_tail\": \"seuraa tiedoston muutoksia; näytä uudet rivit reaaliaikaisesti\\\">📡 seuraa\",\n\t\"tvt_wrap\": \"rivitys\\\">↵\",\n\t\"tvt_atail\": \"lukitse vieritys sivun alaosaan\\\">⚓\",\n\t\"tvt_ctail\": \"dekoodaa terminaalin värit (ansi escape koodit)\\\">🌈\",\n\t\"tvt_ntail\": \"vieritysbufferin raja (kuinka monta tavua tekstiä pidetään ladattuna)\",\n\n\t\"m3u_add1\": \"kappale lisätty m3u soittolistaan\",\n\t\"m3u_addn\": \"{0} kappaletta lisätty m3u soittolistaan\",\n\t\"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\",\n\n\t\"gt_vau\": \"älä näytä videoita, toista vain ääni\\\">🎧\",\n\t\"gt_msel\": \"aktivoi tiedostonvalintatila; ctrl-klikkaa ohittaaksesi valitsemisen väliaikaisesti$N$N&lt;em&gt;tuplaklikkaa tiedostoa / hakemistoa avataksesi sen&lt;/em&gt;$N$NPikanäppäin: S\\\">valitsin\",\n\t\"gt_crop\": \"rajaa pienoiskuvat keskeltä\\\">rajaa\",\n\t\"gt_3x\": \"korkearesoluutioiset pienoiskuvat\\\">3x\",\n\t\"gt_zoom\": \"zoomaa\",\n\t\"gt_chop\": \"pilko\",\n\t\"gt_sort\": \"järjestä\",\n\t\"gt_name\": \"nimi\",\n\t\"gt_sz\": \"koko\",\n\t\"gt_ts\": \"päiväys\",\n\t\"gt_ext\": \"tyyppi\",\n\t\"gt_c1\": \"rajaa tiedostonimiä enemmän (näytä vähemmän)\",\n\t\"gt_c2\": \"rajaa tiedostonimiä vähemmän (näytä enemmän)\",\n\n\t\"sm_w8\": \"haetaan...\",\n\t\"sm_prev\": \"alla olevat hakutulokset ovat edellisestä hausta:\\n  \",\n\t\"sl_close\": \"sulje hakutulokset\",\n\t\"sl_hits\": \"näytetään {0} osumaa\",\n\t\"sl_moar\": \"lataa lisää\",\n\n\t\"s_sz\": \"koko\",\n\t\"s_dt\": \"päiväys\",\n\t\"s_rd\": \"polku\",\n\t\"s_fn\": \"nimi\",\n\t\"s_ta\": \"tagit\",\n\t\"s_ua\": \"ylös@\",\n\t\"s_ad\": \"edist.\",\n\t\"s_s1\": \"minimi Mt\",\n\t\"s_s2\": \"maksimi Mt\",\n\t\"s_d1\": \"min. iso8601\",\n\t\"s_d2\": \"maks. iso8601\",\n\t\"s_u1\": \"ladattu jälkeen\",\n\t\"s_u2\": \"ja/tai ennen\",\n\t\"s_r1\": \"polku sisältää &nbsp; (välilyönnillä erotetuttuina)\",\n\t\"s_f1\": \"nimi sisältää &nbsp; (negatoi käyttämällä -nope)\",\n\t\"s_t1\": \"tagit sisältää &nbsp; (^=alku, loppu=$)\",\n\t\"s_a1\": \"tietyt metadatan ominaisuudet\",\n\n\t\"md_eshow\": \"ei voida renderoida \",\n\t\"md_off\": \"[📜<em>readme</em>] poistettu käytöstä [⚙️] -- dokumentti piilotettu\",\n\n\t\"badreply\": \"Palvelimen vastauksen jäsentäminen epäonnistui.\",\n\n\t\"xhr403\": \"403: Pääsy kielletty\\n\\nkokeile painaa F5, ehkä sinut kirjattiin ulos\",\n\t\"xhr0\": \"tuntematon (todennäköisesti yhteys palvelimeen katosi, tai palvelin on pois päältä)\",\n\t\"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\",\n\t\"tl_xe1\": \"alihakemistojen listaaminen epäonnistui:\\n\\nvirhe \",\n\t\"tl_xe2\": \"404: hakemistoa ei löydy\",\n\t\"fl_xe1\": \"hakemiston tiedostojen listaaminen epäonnistui:\\n\\nvirhe \",\n\t\"fl_xe2\": \"404: hakemistoa ei löydy\",\n\t\"fd_xe1\": \"alihakemiston luominen epäonnistui:\\n\\nvirhe \",\n\t\"fd_xe2\": \"404: Ylähakemistoa ei löydy\",\n\t\"fsm_xe1\": \"viestin lähettäminen epäonnistui:\\n\\nvirhe \",\n\t\"fsm_xe2\": \"404: Ylähakemistoa ei löydy\",\n\t\"fu_xe1\": \"unpost-listan lataaminen palvelimelta epäonnistui:\\n\\nvirhe \",\n\t\"fu_xe2\": \"404: Tiedostoa ei löydy??\",\n\n\t\"fz_tar\": \"pakkaamaton gnu-tar tiedosto (linux / mac)\",\n\t\"fz_pax\": \"pakkaamaton pax-formaatin tar (hitaampi)\",\n\t\"fz_targz\": \"gnu-tar gzip tason 3 pakkauksella$N$Nyleensä hyvin hidas, $Nkäytä pakkamatonta tar:ia tämän sijasta\",\n\t\"fz_tarxz\": \"gnu-tar xz tason 1 pakkauksella$N$Nyleensä hyvin hidas, $Nkäytä pakkamatonta tar:ia tämän sijasta\",\n\t\"fz_zip8\": \"zip utf8-tiedostonimillä (suattaapi olla epävakaa windows 7:ssa ja vanhemmissa)\",\n\t\"fz_zipd\": \"zip perinteisillä cp437 tiedostonimillä esihistoriallisille ohjelmistoille\",\n\t\"fz_zipc\": \"cp437, jossa crc32 laskettu aikaisin,$NMS-DOS PKZIP v2.04g:lle (lokakuu 1993)$N(kestää kauemmin käsitellä ennen latauksen alkua)\",\n\n\t\"un_m1\": \"voit poistaa tuoreet tai keskeyttää keskeneräiset latauksesi alta\",\n\t\"un_upd\": \"päivitä\",\n\t\"un_m4\": \"tai jakaa alla näkyvät tiedostot:\",\n\t\"un_ulist\": \"näytä\",\n\t\"un_ucopy\": \"kopioi\",\n\t\"un_flt\": \"valinnainen suodatin:&nbsp; URL:n täytyy sisältää\",\n\t\"un_fclr\": \"tyhjennä suodatin\",\n\t\"un_derr\": 'unpost-poisto epäonnistui:\\n',\n\t\"un_f5\": 'jotain hajosi, kokeile päivitystä tai paina F5',\n\t\"un_uf5\": \"pahoittelen mutta sinun täytyy päivittää sivu (esimerkiksi painamalla F5 tai CTRL-R) ennen kuin tämä lataus voidaan keskeyttää\",\n\t\"un_nou\": '<b>huom!</b> palvelin liian kiireinen näyttääkseen keskeneräiset lataukset; klikkaa \"päivitä\" linkkiä hetken kuluttua',\n\t\"un_noc\": '<b>huom!</b> täysin ladattujen tiedostojen unpost ei ole käytössä/sallittu palvelimen asetuksissa',\n\t\"un_max\": \"näytetään ensimmäiset 2000 tiedostoa (käytä suodatinta)\",\n\t\"un_avail\": \"{0} viimeaikaista latausta voidaan poistaa<br />{1} keskeneräistä voidaan keskeyttää\",\n\t\"un_m2\": \"järjestetty latausajan mukaan; viimeisimmät ensin:\",\n\t\"un_no1\": \"hupsis! yksikään lataus ei ole riittävän tuore\",\n\t\"un_no2\": \"hupsis! yksikään tuota suodatinta vastaava lataus ei ole riittävän tuore\",\n\t\"un_next\": \"poista seuraavat {0} tiedostoa alla\",\n\t\"un_abrt\": \"keskeytä\",\n\t\"un_del\": \"poista\",\n\t\"un_m3\": \"ladataan viimeaikana lähettämiäsi tiedostoja...\",\n\t\"un_busy\": \"poistetaan {0} tiedostoa...\",\n\t\"un_clip\": \"{0} linkkiä kopioitu leikepöydälle\",\n\n\t\"u_https1\": \"sinun kannattaisi\",\n\t\"u_https2\": \"vaihtaa https:ään\",\n\t\"u_https3\": \"paremman suorituskyvyn vuoksi\",\n\t\"u_ancient\": 'selaimesi on ns. vaikuttavan ikivanha --  kannattais varmaan <a href=\"#\" onclick=\"goto(\\'bup\\')\">käyttää bup:ia tän sijaan</a>',\n\t\"u_nowork\": \"tarvitaan firefox 53+ tai chrome 57+ tai iOS 11+\",\n\t\"tail_2old\": \"tarvitaan firefox 105+ tai chrome 71+ tai iOS 14.5+\",\n\t\"u_nodrop\": 'selaimesi on liian vanha vedä-ja-pudota lataamiseen',\n\t\"u_notdir\": \"tuo ei ole hakemisto!\\n\\nselaimesi on liian vanha,\\nkokeile sen sijaan 'vedä-pudota'-tekniikkaa.\",\n\t\"u_uri\": \"'vedä-pudottaaksesi' kuvia muista selainikkunoista,\\npudota se isoon latausnapppiin\",\n\t\"u_enpot\": 'vaihda <a href=\"#\">peruna UI:hin</a> (voi parantaa latausnopeutta)',\n\t\"u_depot\": 'vaihda <a href=\"#\">ylelliseen UI:hin</a> (voi vähentää latausnopeutta)',\n\t\"u_gotpot\": 'vaihdetaan peruna UI:hin paremman latausnopeuden vuoksi,\\n\\ntee miten lystäät, jos ei kelpaa!',\n\t\"u_pott\": \"<p>tiedostot: &nbsp; <b>{0}</b> valmis, &nbsp; <b>{1}</b> epäonnistui, &nbsp; <b>{2}</b> kiireinen, &nbsp; <b>{3}</b> jonossa</p>\",\n\t\"u_ever\": \"tämä on peruslatain; up2k tarvitsee vähintään<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1\",\n\t\"u_su2k\": 'peruslatain; <a href=\"#\" id=\"u2yea\">up2k</a> on parempi',\n\t\"u_uput\": 'optimoi latausnopeus (älä laske tarkistussummia)',\n\t\"u_ewrite\": 'sinulla ei ole move-oikeutta tähän hakemistoon',\n\t\"u_eread\": 'sinulla ei ole read-oikeutta tähän hakemistoon',\n\t\"u_enoi\": 'tiedostohaku ei ole käytössä palvelimen asetuksissa',\n\t\"u_enoow\": \"ylikirjoitus ei toimi täällä; tarvitaan “Delete”-oikeus\",\n\t\"u_badf\": 'Nämä {0} tiedostoa ({1} yhteensä) ohitettiin, mahdollisesti tiedostojärjestelmän oikeuksien vuoksi:\\n\\n',\n\t\"u_blankf\": 'Nämä {0} tiedostoa ({1} yhteensä) ovat tyhjiä; ladataanko ne silti?\\n\\n',\n\t\"u_applef\": 'Nämä {0} tiedostoa ({1} yhteensä) ovat todennäköisesti ei-toivottuja;\\nPaina <code>OK/Enter</code> OHITTAAKSESI seuraavat tiedostot,\\nPaina <code>Peruuta/ESC</code> jos ET halua sulkea pois, ja LATAA nekin:\\n\\n',\n\t\"u_just1\": '\\nEhkä toimii paremmin jos valitset vain yhden tiedoston',\n\t\"u_ff_many\": \"jos käytät <b>Linux / MacOS / Android,</b> niin tämä määrä tiedostoja <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\"><em>saattaa</em> kaataa Firefoxin!</a>\\njos niin käy, kokeile uudelleen (tai käytä Chromea).\",\n\t\"u_up_life\": \"Tämä lataus poistetaan palvelimelta\\n{0} sen valmistumisen jälkeen\",\n\t\"u_asku\": 'lataa nämä {0} tiedostoa kohteeseen <code>{1}</code>',\n\t\"u_unpt\": \"voit perua / poistaa tämän latauksen käyttämällä vasemmalla ylhäällä olevaa 🧯\",\n\t\"u_bigtab\": 'näytetään {0} tiedostoa\\n\\ntämä voi kaataa selaimesi, oletko varma?',\n\t\"u_scan\": 'Skannataan tiedostoja...',\n\t\"u_dirstuck\": 'hakemistoiteraattori jumittui yrittäessään käyttää seuraavia {0} kohdetta; ohitetaan:',\n\t\"u_etadone\": 'Valmis ({0}, {1} tiedostoa)',\n\t\"u_etaprep\": '(valmistellaan latausta)',\n\t\"u_hashdone\": 'hajautus valmis',\n\t\"u_hashing\": 'hajautus',\n\t\"u_hs\": 'kätellään...',\n\t\"u_started\": \"tiedostoja ladataan nyt; tsekkaa [🚀]\",\n\t\"u_dupdefer\": \"duplikaatti; käsitellään kaikkien muiden tiedostojen jälkeen\",\n\t\"u_actx\": \"klikkaa tätä tekstiä estääksesi suorituskyvyn<br />heikkenemisen vaihtaessasi muihin ikkunoihin/välilehtiin\",\n\t\"u_fixed\": \"OK!&nbsp; Hommat hoidossa 👍\",\n\t\"u_cuerr\": \"chunk {0} / {1} lataus epäonnistui;\\ntuskin haittaa, jatketaan\\n\\ntiedosto: {2}\",\n\t\"u_cuerr2\": \"palvelin hylkäsi latauksen (chunk {0} / {1});\\nyritetään myöhemmin uudelleen\\n\\ntiedosto: {2}\\n\\nvirhe \",\n\t\"u_ehstmp\": \"yritetään uudelleen; katso oikealta alhaalta\",\n\t\"u_ehsfin\": \"palvelin hylkäsi pyynnön viimeistellä lataus; yritetään uudelleen...\",\n\t\"u_ehssrch\": \"palvelin hylkäsi pyynnön suorittaa haku; yritetään uudelleen...\",\n\t\"u_ehsinit\": \"palvelin hylkäsi pyynnön aloittaa lataus; yritetään uudelleen...\",\n\t\"u_eneths\": \"verkkovirhe latauksen kättelyssä; yritetään uudelleen...\",\n\t\"u_enethd\": \"verkkovirhe kohteen olemassaolon testauksessa; yritetään uudelleen...\",\n\t\"u_cbusy\": \"odotetaan palvelimen luottavan meihin taas verkko-ongelman jälkeen...\",\n\t\"u_ehsdf\": \"palvelimen levytila loppui!\\n\\nyritetään jatkuvasti, siinä tapauksessa että joku\\nvapauttaa tarpeeksi tilaa jatkamiseen\",\n\t\"u_emtleak1\": \"näyttää siltä että selaimessasi saattaa olla muistivuoto;\\nole hyvä ja\",\n\t\"u_emtleak2\": ' <a href=\"{0}\">vaihda https:ään (suositeltu)</a> tai ',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": 'kokeile seuraavaa:\\n<ul><li>paina <code>F5</code> päivittääksesi sivun</li><li>sitten poista käytöstä &nbsp;<code>mt</code>&nbsp; nappi &nbsp;<code>⚙️ asetuksissa</code></li><li>ja kokeile latausta uudelleen</li></ul>Lataukset ovat hieman hitaampia, minkäs teet.\\nSori siitä!\\n\\nPS: chrome v107 <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">sisältää bugfixin tätä varten</a>',\n\t\"u_emtleakf\": 'kokeile seuraavaa:\\n<ul><li>paina <code>F5</code> päivittääksesi sivun</li><li>sitten ota käyttöön <code>🥔</code> (peruna) lataus UI:ssa<li>ja kokeile latausta uudelleen</li></ul>\\nPS: firefox <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">toivottavasti saa kerättyä itsensä kasaan</a> jossain vaiheessa',\n\t\"u_s404\": \"ei löydy palvelimelta\",\n\t\"u_expl\": \"selitä\",\n\t\"u_maxconn\": \"useimmat selaimet rajoittavat tämän 6:een, mutta firefox antaa nostaa sitä <code>connections-per-server</code> asetuksella <code>about:config</code>:issa\",\n\t\"u_tu\": '<p class=\"warn\">VAROITUS: turbo päällä, <span>&nbsp;asiakasohjelma ei välttämättä huomaa jatkaa keskeneräisiä latauksia; katso turbo-napin vihje</span></p>',\n\t\"u_ts\": '<p class=\"warn\">VAROITUS: turbo päällä, <span>&nbsp;hakutulokset voivat olla vääriä; katso turbo-napin vihje</span></p>',\n\t\"u_turbo_c\": \"turbo on poistettu käytöstä palvelimen asetuksissa\",\n\t\"u_turbo_g\": \"poistetaan turbo käytöstä koska sinulla ei ole\\nhakemistolistausoikeuksia tässä asemassa\",\n\t\"u_life_cfg\": 'automaattinen poisto <input id=\"lifem\" p=\"60\" /> min kuluttua (tai <input id=\"lifeh\" p=\"3600\" /> tuntia)',\n\t\"u_life_est\": 'lataus poistetaan <span id=\"lifew\" tt=\"paikallinen aika\">---</span>',\n\t\"u_life_max\": 'tämä hakemisto pakottaa\\nmaksimi elinajan {0}',\n\t\"u_unp_ok\": 'unpost on sallittu {0}',\n\t\"u_unp_ng\": 'unpost EI ole sallittu',\n\t\"ue_ro\": 'sinulla on vain read-oikeus tähän hakemistoon\\n\\n',\n\t\"ue_nl\": 'et ole tällä hetkellä kirjautunut sisään',\n\t\"ue_la\": 'olet tällä hetkellä kirjautunut sisään nimellä \"{0}\"',\n\t\"ue_sr\": 'olet tällä hetkellä tiedostohaku-tilassa\\n\\nvaihda lataus-tilaan klikkaamalla suurennuslasia 🔎 (suuren HAKU napin vieressä), ja yritä latausta uudelleen\\n\\npahoittelen',\n\t\"ue_ta\": 'yritä latausta uudelleen, sen pitäisi toimia nyt',\n\t\"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 🧯\",\n\t\"ur_1uo\": \"OK: Tiedosto ladattu onnistuneesti\",\n\t\"ur_auo\": \"OK: Kaikki {0} tiedostoa ladattu onnistuneesti\",\n\t\"ur_1so\": \"OK: Tiedosto löytyi palvelimelta\",\n\t\"ur_aso\": \"OK: Kaikki {0} tiedostoa löytyi palvelimelta\",\n\t\"ur_1un\": \"Lataus epäonnistui, pahoittelen\",\n\t\"ur_aun\": \"Kaikki {0} latausta epäonnistui, pahoittelen\",\n\t\"ur_1sn\": \"Tiedostoa EI löytynyt palvelimelta\",\n\t\"ur_asn\": \"{0} tiedostoa EI löytynyt palvelimelta\",\n\t\"ur_um\": \"Valmis;\\n{0} latausta OK,\\n{1} latausta epäonnistui, pahoittelen\",\n\t\"ur_sm\": \"Valmis;\\n{0} tiedostoa löytyi palvelimelta,\\n{1} tiedostoa EI löytynyt palvelimelta\",\n\n\t\"rc_opn\": \"avaa\",\n\t\"rc_ply\": \"toista\",\n\t\"rc_pla\": \"toista äänitiedostona\",\n\t\"rc_txt\": \"avaa tekstinäkymässä\",\n\t\"rc_md\": \"avaa markdown-näkymässä\",\n\t\"rc_dl\": \"lataa\",\n\t\"rc_zip\": \"lataa arkistona\",\n\t\"rc_cpl\": \"kopioi linkki\",\n\t\"rc_del\": \"poista\",\n\t\"rc_cut\": \"leikkaa\",\n\t\"rc_cpy\": \"kopioi\",\n\t\"rc_pst\": \"liitä\",\n\t\"rc_rnm\": \"nimeä uudelleen\",\n\t\"rc_nfo\": \"uusi kansio\",\n\t\"rc_nfi\": \"uusi tiedosto\",\n\t\"rc_sal\": \"valitse kaikki\",\n\t\"rc_sin\": \"käänteinen valinta\",\n\t\"rc_shf\": \"jaa tämä kansio\",\n\t\"rc_shs\": \"jaa valinta\",\n\n\t\"lang_set\": \"ladataanko sivu uudestaan kielen vaihtamiseksi?\",\n\n\t\"splash\": {\n\t\t\"a1\": \"päivitä\",\n\t\t\"b1\": \"hei sie muukalainen &nbsp; <small>(et ole kirjautunut sisään)</small>\",\n\t\t\"c1\": \"kirjaudu ulos\",\n\t\t\"d1\": \"tulosta pinojälki\",\n\t\t\"d2\": \"näytä kaikkien aktiivisten säikeiden tila\",\n\t\t\"e1\": \"päivitä konffit\",\n\t\t\"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\",\n\t\t\"f1\": \"voit selata näitä:\",\n\t\t\"g1\": \"voit ladata näihin:\",\n\t\t\"cc1\": \"muuta:\",\n\t\t\"h1\": \"poista k304 käytöstä\",\n\t\t\"i1\": \"ota k304 käyttöön\",\n\t\t\"j1\": \"k304 katkaisee yhteytesi jokaisella HTTP 304:llä, mikä voi estää joitain bugisia välityspalvelimia jumittumasta/lopettamasta sivujen lataamista, <em>mutta</em> se myös vähentää suorituskykyä\",\n\t\t\"k1\": \"nollaa asetukset\",\n\t\t\"l1\": \"kirjaudu sisään:\",\n\t\t\"ls3\": \"kirjaudu sisään\",\n\t\t\"lu4\": \"käyttäjätunnus\",\n\t\t\"lp4\": \"salasana\",\n\t\t\"lo3\": \"kirjaa “{0}” ulos kaikkialta\",\n\t\t\"lo2\": \"tämä lopettaa istunnon kaikissa selaimissa\",\n\t\t\"m1\": \"tervetuloa takaisin,\",\n\t\t\"n1\": \"404: ei löytynyt mitään &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": 'tai ehkä sinulla ei vain ole käyttöoikeuksia? kokeile salasanaa tai <a href=\"' + SR + '/?h\">mene kotiin</a>',\n\t\t\"p1\": \"403: pääsy kielletty &nbsp;~┻━┻\",\n\t\t\"q1\": 'kokeile salasanaa tai <a href=\"' + SR + '/?h\">mene kotiin</a>',\n\t\t\"r1\": \"mene kotiin\",\n\t\t\".s1\": \"uudelleenkartoita\",\n\t\t\"t1\": \"toiminto\",\n\t\t\"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\",\n\t\t\"v1\": \"yhdistä\",\n\t\t\"v2\": \"käytä tätä palvelinta paikallisena kiintolevynä\",\n\t\t\"w1\": \"vaihda https:ään\",\n\t\t\"x1\": \"vaihda salasana\",\n\t\t\"y1\": \"muokkaa jakoja\",\n\t\t\"z1\": \"avaa tämä jako:\",\n\t\t\"ta1\": \"täytä ensin uusi salasana\",\n\t\t\"ta2\": \"toista vahvistaaksesi uuden salasanan:\",\n\t\t\"ta3\": \"löytyi kirjoitusvirhe; yritä uudelleen\",\n\t\t\"nop\": \"VIRHE: Salasana ei voi olla tyhjä\",\n\t\t\"nou\": \"VIRHE: Käyttäjänimi ja/tai salasana ei voi olla tyhjä\",\n\t\t\"aa1\": \"saapuvat:\",\n\t\t\"ab1\": \"poista no304 käytöstä\",\n\t\t\"ac1\": \"ota no304 käyttöön\",\n\t\t\"ad1\": \"no304:n lopettaa välimuistin käytön kokonaan; kokeile tätä jos k304 ei riittänyt. Tuhlaa valtavan määrän verkkoliikennettä!\",\n\t\t\"ae1\": \"lähtevät:\",\n\t\t\"af1\": \"näytä viimeaikaiset lataukset\",\n\t\t\"ag1\": \"näytä tunnetut IdP-käyttäjät\",\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/fra.js",
    "content": "\n// Les lignes se terminant par //m sont des traductions automatiques non vérifiées\n\nLs.fra = {\n\t\"tt\": \"français\",\n\n\t\"cols\": {\n\t\t\"c\": \"bouton d'action\",\n\t\t\"dur\": \"durée\",\n\t\t\"q\": \"qualité / débit binaire\",\n\t\t\"Ac\": \"codec audio\",\n\t\t\"Vc\": \"codec vidéo\",\n\t\t\"Fmt\": \"format / conteneur\",\n\t\t\"Ahash\": \"somme de contrôle audio\",\n\t\t\"Vhash\": \"somme de contrôle vidéo\",\n\t\t\"Res\": \"résolution\",\n\t\t\"T\": \"type de fichier\",\n\t\t\"aq\": \"qualité audio / débit binaire\",\n\t\t\"vq\": \"qualité vidéo / débit binaire\",\n\t\t\"pixfmt\": \"sous-échantillonnage / structure de pixel\",\n\t\t\"resw\": \"résolution horizontale\",\n\t\t\"resh\": \"résolution verticale\",\n\t\t\"chs\": \"canaux audio\",\n\t\t\"hz\": \"fréquence\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"misc\",\n\t\t\t[\"Échap\", \"ferme divers menus\"],\n\n\t\t\t\"gestionaire de fichiers\",\n\t\t\t[\"G\", \"activer vue en liste / vue en grille\"],\n\t\t\t[\"T\", \"activer les miniatures / icônes\"],\n\t\t\t[\"⇧ A/D\", \"taille des miniatures\"],\n\t\t\t[\"ctrl-K\", \"suprimer la sélection\"],\n\t\t\t[\"ctrl-X\", \"couper la sélection au presse-papier\"],\n\t\t\t[\"ctrl-C\", \"copier la sélection au presse-papier\"],\n\t\t\t[\"ctrl-V\", \"coller (déplacer/copier) ici\"],\n\t\t\t[\"Y\", \"télécharger la sélection\"],\n\t\t\t[\"F2\", \"renomer la sélection\"],\n\n\t\t\t\"file-list-sel\",\n\t\t\t[\"Espace\", \"activer la sélection de fichiers\"],\n\t\t\t[\"↑/↓\", \"déplacer le selecteur\"],\n\t\t\t[\"ctrl ↑/↓\", \"déplacer le curseur et la zone d'affichage\"],\n\t\t\t[\"⇧ ↑/↓\", \"sélectioner le fichier précédent/suivant\"],\n\t\t\t[\"ctrl-A\", \"sélectionner tout les fichiers / dossiers\"],\n\t\t], [\n\t\t\t\"navigation\",\n\t\t\t[\"B\", \"basculer la vue en fil d'Ariane / panneau de navigation\"],\n\t\t\t[\"I/K\", \"dossier précédent/suivant\"],\n\t\t\t[\"M\", \"dossier parent (ou réduire le dossier actuel)\"],\n\t\t\t[\"V\", \"activer les dossiers / fichiers texte dans le volet de navigation\"],\n\t\t\t[\"A/D\", \"taille du volet de navigation\"],\n\t\t], [\n\t\t\t\"lecteur-audio\",\n\t\t\t[\"J/L\", \"chanson précédente/suivante\"],\n\t\t\t[\"U/O\", \"sauter 10s en arrière/avant\"],\n\t\t\t[\"0..9\", \"sauter à 0%..90%\"],\n\t\t\t[\"P\", \"lecture/pause (démarre également la lecture)\"],\n\t\t\t[\"S\", \"sélectionner la chanson en cours\"],\n\t\t\t[\"Y\", \"télécharger le morceau\"],\n\t\t], [\n\t\t\t\"visionneuse d'image\",\n\t\t\t[\"J/L, ←/→\", \"image précédente/suivante\"],\n\t\t\t[\"Début/Fin, ⭦/Fin\", \"première/dernière image\"],\n\t\t\t[\"F\", \"plein écran\"],\n\t\t\t[\"R\", \"rotation horaire\"],\n\t\t\t[\"⇧ R\", \"rotation antihoraire\"],\n\t\t\t[\"S\", \"sélectionner l'image\"],\n\t\t\t[\"Y\", \"télécharger l'image\"],\n\t\t], [\n\t\t\t\"lecteur vidéo\",\n\t\t\t[\"U/O\", \"sauter 10s en arrière/avant\"],\n\t\t\t[\"P/K/Espace\", \"lecture/pause\"],\n\t\t\t[\"C\", \"continuer de lire la suivante\"],\n\t\t\t[\"V\", \"lire en boucle\"],\n\t\t\t[\"M\", \"couper le son\"],\n\t\t\t[\"[ and ]\", \"définir l'intervalle de boucle\"],\n\t\t], [\n\t\t\t\"visionneuse de texte\",\n\t\t\t[\"I/K\", \"fichier précédent/suivant\"],\n\t\t\t[\"M\", \"fermer le fichier texte\"],\n\t\t\t[\"E\", \"modifier le fichier texte\"],\n\t\t\t[\"S\", \"sélectioner le fichier (pour le couper/copier/renommer)\"],\n\t\t\t[\"Y\", \"télécharger le fichier texte\"], //m\n\t\t\t[\"⇧ J\", \"embellir json\"], //m\n\t\t]\n\t],\n\n\t\"m_ok\": \"OK\",\n\t\"m_ng\": \"Annuler\",\n\n\t\"enable\": \"Activer\",\n\t\"danger\": \"DANGER\",\n\t\"clipped\": \"copié dans le presse-papier\",\n\n\t\"ht_s1\": \"seconde\",\n\t\"ht_s2\": \"secondes\",\n\t\"ht_m1\": \"minute\",\n\t\"ht_m2\": \"minutes\",\n\t\"ht_h1\": \"heure\",\n\t\"ht_h2\": \"heures\",\n\t\"ht_d1\": \"jour\",\n\t\"ht_d2\": \"jours\",\n\t\"ht_and\": \" et \",\n\n\t\"goh\": \"panneau-de-commande\",\n\t\"gop\": 'élément \"frère\" précédent\">précédent',\n\t\"gou\": 'dossier parent\">haut',\n\t\"gon\": 'dossier suivant\">suivant',\n\t\"logout\": \"Déconnexion \",\n\t\"login\": \"Se connecter\", //m\n\t\"access\": \" accès\",\n\t\"ot_close\": \"fermer le sous-menu\",\n\t\"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`&quot;try unite&quot;` = contient exactement «try unite»$N$Nle format de date est iso-8601, comme$N`2009-12-31` ou `2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"unpost: supprimer vos téléchargements récents, ou annuler ceux en cours\",\n\t\"ot_bup\": \"bup: téléverseur de base, prend même en charge netscape 4.0\",\n\t\"ot_mkdir\": \"mkdir: créer un nouveau répertoire\",\n\t\"ot_md\": \"new-file: créer un nouveau fichier texte\", //m\n\t\"ot_msg\": \"msg: envoyer un message au journal du serveur\",\n\t\"ot_mp\": \"options du lecteur multimedia\",\n\t\"ot_cfg\": \"options de configuration\",\n\t\"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 [🎈]&nbsp; (le téléverseur de base)<br /><br />pendant les téléversements, cette icône devient un indicateur de progression!',\n\t\"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 [🎈]&nbsp; (le téléverseur de base)<br /><br />pendant les téléversements, cette icône devient un indicateur de progression!',\n\t\"ot_noie\": 'Utilisez Chrome / Firefox / Edge',\n\n\t\"ab_mkdir\": \"créer un nouveau répertoire\",\n\t\"ab_mkdoc\": \"nouveau fichier texte\", //m\n\t\"ab_msg\": \"envoyer un message au journal du serveur\",\n\n\t\"ay_path\": \"passer aux dossiers\",\n\t\"ay_files\": \"passer aux fichiers\",\n\n\t\"wt_ren\": \"renommer les éléments sélectionnés$NHotkey: F2\",\n\t\"wt_del\": \"supprimer les éléments sélectionnés$NHotkey: ctrl-K\",\n\t\"wt_cut\": \"couper les éléments sélectionnés &lt;small&gt;(puis coller ailleurs)&lt;/small&gt;$NHotkey: ctrl-X\",\n\t\"wt_cpy\": \"copier les éléments sélectionnés dans le presse-papiers$N(pour les coller ailleurs)$NHotkey: ctrl-C\",\n\t\"wt_pst\": \"coller une sélection précédemment coupée / copiée$NHotkey: ctrl-V\",\n\t\"wt_selall\": \"sélectionner tous les fichiers$NHotkey: ctrl-A (lorsque le fichier est sélectionné)\",\n\t\"wt_selinv\": \"inverser la sélection\",\n\t\"wt_zip1\": \"télécharger ce dossier en tant qu'archive\",\n\t\"wt_selzip\": \"télécharger la sélection en tant qu'archive\",\n\t\"wt_seldl\": \"télécharger la sélection en tant que fichiers séparés$NHotkey: Y\",\n\t\"wt_npirc\": \"copier les informations de la musique au format irc\",\n\t\"wt_nptxt\": \"copier les informations de la musique en texte brut\",\n\t\"wt_m3ua\": \"ajouter à la playlist m3u (cliquez sur <code>📻copier</code> plus tard)\",\n\t\"wt_m3uc\": \"copier la playlist m3u dans le presse-papiers\",\n\t\"wt_grid\": \"basculer entre la vue en grille / liste$NHotkey: G\",\n\t\"wt_prev\": \"musique précédente$NHotkey: J\",\n\t\"wt_play\": \"lecture / pause$NHotkey: P\",\n\t\"wt_next\": \"musique suivante$NHotkey: L\",\n\n\t\"ul_par\": \"téléversements parallèles:\",\n\t\"ut_rand\": \"attribution de noms de fichiers aléatoires\",\n\t\"ut_u2ts\": \"copier l'horodatage de dernière modification$Nde votre système de fichiers vers le serveur\\\">📅\",\n\t\"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\n\t\"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\",\n\t\"ut_ask\": 'demander confirmation avant le début du téléversement\">💭',\n\t\"ut_pot\": \"améliorer la vitesse de téléversement sur les appareils lents$Nen simplifiant l'interface utilisateur\",\n\t\"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)\",\n\t\"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\",\n\t\"ul_btn\": \"déposer des fichiers / dossiers<br>ici (ou cliquez sur moi)\",\n\t\"ul_btnu\": \"T É L É V E R S E R\",\n\t\"ul_btns\": \"C H E R C H E R\",\n\n\t\"ul_hash\": \"somme de contrôle\",\n\t\"ul_send\": \"envoyer\",\n\t\"ul_done\": \"terminé\",\n\t\"ul_idle1\": \"aucun téléversement n'est encore dans la file d'attente\",\n\t\"ut_etah\": \"moyenne &lt;em&gt;hashing&lt;/em&gt; vitesse, et temps estimé jusqu'à la fin\",\n\t\"ut_etau\": \"moyenne &lt;em&gt;upload&lt;/em&gt; vitesse et temps estimé jusqu'à la fin\",\n\t\"ut_etat\": \"moyenne &lt;em&gt;total&lt;/em&gt; vitesse et temps estimé jusqu'à la fin\",\n\n\t\"uct_ok\": \"terminé avec succès\",\n\t\"uct_ng\": \"non réussi : échoué / rejeté / non trouvé\",\n\t\"uct_done\": \"terminés et échoué combinés\",\n\t\"uct_bz\": \"hachage ou téléversement\",\n\t\"uct_q\": \"inactif, en attente\",\n\n\t\"utl_name\": \"nom de fichier\",\n\t\"utl_ulist\": \"liste\",\n\t\"utl_ucopy\": \"copie\",\n\t\"utl_links\": \"liens\",\n\t\"utl_stat\": \"état\",\n\t\"utl_prog\": \"progrès\",\n\n\t// keep short:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"ERREUR\",\n\t\"utl_oserr\": \"OS-ERREUR\",\n\t\"utl_found\": \"trouvé\",\n\t\"utl_defer\": \"état\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"terminé\",\n\n\t\"ul_flagblk\": \"les fichiers ont été ajoutés à la file d'attente</b><br>cependant, il y a un processus up2k actif dans un autre onglet du navigateur,<br>en attente qu'il finisse d'abord\",\n\t\"ul_btnlk\": \"la configuration du serveur a verrouillé cette options dans cet état\",\n\n\t\"udt_up\": \"Téléverser\",\n\t\"udt_srch\": \"Chercher\",\n\t\"udt_drop\": \"déposer ici\",\n\n\t\"u_nav_m\": '<h6>aight, ques-que tu à ?</h6><code>Enter</code> = Fichiers (un ou plus)\\n<code>ESC</code> = Un dossier (sous-dossiers inclus)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Fichiers</a><a href=\"#\" id=\"modal-ng\">Un dossier</a>',\n\n\t\"cl_opts\": \"options\",\n\t\"cl_hfsz\": \"taille du fichier\", //m\n\t\"cl_themes\": \"thème\",\n\t\"cl_langs\": \"langue\",\n\t\"cl_ziptype\": \"téléchargement de dossier\",\n\t\"cl_uopts\": \"up2k\",\n\t\"cl_favico\": \"favicon\",\n\t\"cl_bigdir\": \"gros dossiers\",\n\t\"cl_hsort\": \"#sort\",\n\t\"cl_keytype\": \"notation des touches\",\n\t\"cl_hiddenc\": \"colonnes masquées\",\n\t\"cl_hidec\": \"masquer\",\n\t\"cl_reset\": \"réinitialiser\",\n\t\"cl_hpick\": \"cliquez sur les en-têtes de colonnes pour les masquer dans le tableau ci-dessous\",\n\t\"cl_hcancel\": \"masquage des colonnes annulé\",\n\t\"cl_rcm\": \"menu contextuel\", //m\n\n\t\"ct_grid\": '田 grille',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ infobulles',\n\t\"ct_thumb\": 'vue en grille, activer les icônes ou les miniatures$NHotkey: T\">🖼️ minia',\n\t\"ct_csel\": 'utiliser CTRL et MAJ pour selectioner des fichiers en vue en grille\">sel',\n\t\"ct_dsel\": 'utiliser la sélection par glisser en vue en grille\">glisser', //m\n\t\"ct_dl\": 'forcer le téléchargement (ne pas afficher en ligne) lorsqu’un fichier est cliqué\">dl', //m\n\t\"ct_ihop\": 'quand le visionneuse d\\'image est fermé, faire defiller vers le bas jusqu\\'au dernier fichier\">g⮯',\n\t\"ct_dots\": 'voir les fichiers caché (si le serveur le permet)\">dotfiles',\n\t\"ct_qdel\": 'ne demander qu\\'une confirmation lors de la suppression de fichiers>qdel',\n\t\"ct_dir1st\": 'trier les dossiers avant les fichiers\">📁 first',\n\t\"ct_nsort\": 'triage par numérotation (pour les nom de fichiers qui sont numérotés)\">nsort',\n\t\"ct_utc\": 'voir tout les horodatage en format UTC\">UTC',\n\t\"ct_readme\": 'voir le fichier README.md dans le listage des dossiers\">📜 readme',\n\t\"ct_idxh\": 'voir une version html (index.html) au-lieu du listage des dossiers normal\">htm',\n\t\"ct_sbars\": 'montrer la barre de defilement\">⟊',\n\n\t\"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📅\",\n\n\t\"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 <em>&quot;est-ce que cela a la même taille de fichier sur le serveur?&quot;</em> 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 &quot;télécharger&quot; les mêmes fichiers à nouveau pour laisser le client les vérifier\\\">turbo\",\n\n\t\"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 <em>théoriquement</em> 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\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"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)\",\n\t\"cut_sfx\": \"alerte audible quand le téléversement finit$N(seulement si le navigateur ou l'onglet n'est pas actif)\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cft_text\": \"text favicon (laisser vide et rafraîchir pour désactiver)\",\n\t\"cft_fg\": \"couleur de premier plan\",\n\t\"cft_bg\": \"couleur d'arrière-plan\",\n\n\t\"cdt_lim\": \"nombre maximum de fichiers à afficher dans un dossier\",\n\t\"cdt_ask\": \"lorsque vous faites défiler vers le bas,$Nau lieu de charger plus de fichiers,$Ndemander quoi faire\",\n\t\"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.\",\n\t\"cdt_ren\": \"activer le menu contextuel personnalisé, le menu normal reste accessible avec shift + clic droit\\\">activer\", //m\n\t\"cdt_rdb\": \"afficher le menu clic droit normal lorsque le menu personnalisé est déjà ouvert et qu’on clique à nouveau\\\">x2\", //m\n\n\t\"tt_entree\": \"afficher le panneau de navigation (arborescence des dossiers)$NHotkey: B\",\n\t\"tt_detree\": \"afficher le fil d’Ariane$NHotkey: B\",\n\t\"tt_visdir\": \"faire défiler jusqu'au dossier sélectionné\",\n\t\"tt_ftree\": \"basculer l'arborescence des dossiers / fichiers texte$NHotkey: V\",\n\t\"tt_pdock\": \"afficher les dossiers parents dans un panneau ancré en haut\",\n\t\"tt_dynt\": \"croissance automatique à mesure que l'arborescence s'étend\",\n\t\"tt_wrap\": \"retour à la ligne\",\n\t\"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 )\",\n\n\t\"ml_pmode\": \"à la fin du dossier…\",\n\t\"ml_btns\": \"cmds\",\n\t\"ml_tcode\": \"transcoder\",\n\t\"ml_tcode2\": \"transcoder vers\",\n\t\"ml_tint\": \"teinte\",\n\t\"ml_eq\": \"égaliseur audio\",\n\t\"ml_drc\": \"compresseur de plage dynamique\",\n\t\"ml_ss\": \"ignorer les silences\", //m\n\n\t\"mt_loop\": \"répéter en boucle une musique\\\">🔁\",\n\t\"mt_one\": \"stopper après une musique\\\">1️⃣\",\n\t\"mt_shuf\": \"mélanger les musiques dans chaque dossiers\\\">🔀\",\n\t\"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▶\",\n\t\"mt_preload\": \"commencer à charger la prochaine chanson près de la fin pour une lecture sans interruption\\\">preload\",\n\t\"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\",\n\t\"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\",\n\t\"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é)\\\">☕️\",\n\t\"mt_waves\": \"barre de progression en spectrograme:$Nmontrer l'amplitude audio dans la miniature\\\">~s\",\n\t\"mt_npclip\": \"montrer les boutons pour copier le morceau en cours de lecture\\\">/np\",\n\t\"mt_m3u_c\": \"montrer les boutons pour copier les$morceaux sélectionnées en tant qu'entrées de playlist m3u8\\\">📻\",\n\t\"mt_octl\": \"intégration os (touches de raccourci multimédia / osd)\\\">os-ctl\",\n\t\"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\",\n\t\"mt_oscv\": \"montrer la couverture de l'album dans l'osd\\\">art\",\n\t\"mt_follow\": \"garder la piste en cours défilée dans la vue\\\">🎯\",\n\t\"mt_compact\": \"contrôles compacts\\\">⟎\",\n\t\"mt_uncache\": \"effacer le cache &nbsp;(essayez ceci si votre navigateur a mis en cache$Nun copie défectueuse d'une chanson, ce qui empêche sa lecture)\\\">uncache\",\n\t\"mt_mloop\": \"lire en boucle le dossier ouvert\\\">🔁 loop\",\n\t\"mt_mnext\": \"charger le dossier suivant et continuer\\\">📂 next\",\n\t\"mt_mstop\": \"arrêter la lecture\\\">⏸ stop\",\n\t\"mt_cflac\": \"convertir flac / wav en {0}\\\">flac\",\n\t\"mt_caac\": \"convertir aac / m4a en {0}\\\">aac\",\n\t\"mt_coth\": \"convertir tout les autres (pas mp3) en {0}\\\">oth\",\n\t\"mt_c2opus\": \"meilleur choix pour PC fixe, PC portable, android\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, pour iOS 17.5 et supérieur\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, pour iOS 11 à 17\\\">caf\",\n\t\"mt_c2mp3\": \"utilisez ceci sur des appareils très anciens\\\">mp3\",\n\t\"mt_c2flac\": \"meilleure qualité sonore, mais téléchargements énormes\\\">flac\",\n\t\"mt_c2wav\": \"lecture non compressée (encore plus gros)\\\">wav\",\n\t\"mt_c2ok\": \"bien, bon choix\",\n\t\"mt_c2nd\": \"ce n'est pas le format de sortie recommandé pour votre appareil, mais ça devrait aller\",\n\t\"mt_c2ng\": \"votre appareil ne semble pas prendre en charge ce format de sortie, mais essayons quand même\",\n\t\"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\",\n\t\"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\",\n\t\"mt_eq\": \"`active l'égaliseur et le contrôle de gain;$N$Nboost `0` = volume standard 100% (non modifié)$N$Nwidth `1 &nbsp;` = stéréo standard (non modifié)$Nwidth `0.5` = 50% de crossfeed gauche-droite$Nwidth `0 &nbsp;` = mono$N$Nboost `-0.8` &amp; 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\",\n\t\"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)\",\n\t\"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\n\t\"mt_ssvt\": \"seuil de volume (0-255)\\\">vol\", //m\n\t\"mt_ssts\": \"seuil actif (% piste, début)\\\">deb\", //m\n\t\"mt_sste\": \"seuil actif (% piste, fin)\\\">fin\", //m\n\t\"mt_sssm\": \"multiplicateur de vitesse de lecture\\\">av\", //m\n\n\t\"mb_play\": \"lecture\",\n\t\"mm_hashplay\": \"lire ce fichier audio ?\",\n\t\"mm_m3u\": \"appuyez sur <code>Entrée/OK</code> pour lire\\nappuyez sur <code>Échap/Annuler</code> pour modifier\",\n\t\"mp_breq\": \"nécessite firefox 82+ ou chrome 73+ ou iOS 15+\",\n\t\"mm_bload\": \"chargement en cours…\",\n\t\"mm_bconv\": \"conversion en {0}, veuillez patienter…\",\n\t\"mm_opusen\": \"votre navigateur ne peut pas lire les fichiers aac / m4a ;\\nle transcodage en opus est maintenant activé\",\n\t\"mm_playerr\": \"échec de la lecture : \",\n\t\"mm_eabrt\": \"La tentative de lecture a été annulée\",\n\t\"mm_enet\": \"Votre connexion internet est instable ou inexistante\",\n\t\"mm_edec\": \"Ce fichier est supposément corrompu??\",\n\t\"mm_esupp\": \"Votre navigateur ne comprend pas ce format audio\",\n\t\"mm_eunk\": \"Erreur inconnue\",\n\t\"mm_e404\": \"Impossible de lire l'audio ; erreur 404 : fichier introuvable.\",\n\t\"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é\",\n\t\"mm_e415\": \"Impossible de lire l'audio ; erreur 415 : échec de la conversion du fichier ; vérifiez les journaux du serveur.\", //m\n\t\"mm_e500\": \"Impossible de lire l'audio ; erreur 500 : vérifiez les journaux du serveur.\",\n\t\"mm_e5xx\": \"Impossible de lire l'audio ; erreur serveur \",\n\t\"mm_nof\": \"Pas d'autres fichiers audio trouvés par ici\",\n\t\"mm_prescan\": \"En recherche d'une autre musique à lire…\",\n\t\"mm_scank\": \"Prochaine musique trouvée :\",\n\t\"mm_uncache\": \"cache vidé ; toutes les chansons seront retéléchargées lors de la prochaine lecture\",\n\t\"mm_hnf\": \"cette chanson n'existe plus\",\n\n\t\"im_hnf\": \"cette image n'existe plus\",\n\n\t\"f_empty\": 'ce dossier est vide',\n\t\"f_chide\": 'ceci va cacher les colonnes «{0}»\\n\\ntu peut les réafficher dans les options',\n\t\"f_bigtxt\": \"ce fichier fait {0} MiB -- tu veut vraiment le voir en tant que texte ?\",\n\t\"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\",\n\t\"fbd_more\": '<div id=\"blazy\">showing <code>{0}</code> of <code>{1}</code> files; <a href=\"#\" id=\"bd_more\">show {2}</a> or <a href=\"#\" id=\"bd_all\">show all</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\">showing <code>{0}</code> of <code>{1}</code> files; <a href=\"#\" id=\"bd_all\">show all</a></div>',\n\t\"f_anota\": \"seulement {0} des {1} elements sont selectioné;\\npour selectioner le dossier entier, fait défiler jusqu'au fond\",\n\n\t\"f_dls\": 'le lien de fichier dans le répertoire actuel\\nà été changé en lien de téléchargement',\n\t\"f_dl_nd\": 'dossier ignoré (utilisez le téléchargement zip/tar à la place):\\n', //m\n\n\t\"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 <code>.PARTIAL</code>. 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 <code>.PARTIAL</code> à la place, ce qui donnera presque certainement des données corrompues.\",\n\n\t\"ft_paste\": \"coller {0} éléments$NHotkey: ctrl-V\",\n\t\"fr_eperm\": 'impossible de renommer:\\n vous n\\'avez pas la permission “move” dans ce dossier',\n\t\"fd_eperm\": 'impossible de supprimer:\\nvous n\\'avez pas la permission “delete” dans ce dossier',\n\t\"fc_eperm\": 'impossible de couper:\\nvous n\\'avez pas la permission “move” dans ce dossier',\n\t\"fp_eperm\": 'impossible de coller:\\nvous n\\'avez pas la permission “write” dans ce dossier',\n\t\"fr_emore\": \"sélectionnez au moins un élément à renommer\",\n\t\"fd_emore\": \"sélectionnez au moins un élément à supprimer\",\n\t\"fc_emore\": \"sélectionnez au moins un élément à couper\",\n\t\"fcp_emore\": \"sélectionnez au moins un élément à copier dans le presse-papiers\",\n\n\t\"fs_sc\": \"partager le dossier dans lequel vous vous trouvez\",\n\t\"fs_ss\": \"partager les fichiers sélectionnés\",\n\t\"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\",\n\t\"fs_abrt\": \"❌ abandonner\",\n\t\"fs_rand\": \"🎲 nom.aleatoire\",\n\t\"fs_go\": \"✅ créer partage\",\n\t\"fs_name\": \"nom\",\n\t\"fs_src\": \"source\",\n\t\"fs_pwd\": \"mdp\",\n\t\"fs_exp\": \"expiration\",\n\t\"fs_tmin\": \"min\",\n\t\"fs_thrs\": \"heures\",\n\t\"fs_tdays\": \"jours\",\n\t\"fs_never\": \"éternel\",\n\t\"fs_pname\": \"nom de lien optionnel ; sera aléatoire si vide\",\n\t\"fs_tsrc\": \"le fichier ou le dossier à partager\",\n\t\"fs_ppwd\": \"mot de passe optionnel\",\n\t\"fs_w8\": \"création du partage…\",\n\t\"fs_ok\": \"appuyez sur <code>Entrée/OK</code> pour le Presse-papiers\\nappuyez sur <code>Échap/Annuler</code> pour fermer\",\n\n\t\"frt_dec\": \"peut potentiellement réparer certaines instances de noms de fichiers cassés\\\">url-decode\",\n\t\"frt_rst\": \"réinitialiser les noms de fichiers modifiés à leurs originaux\\\">↺ reset\",\n\t\"frt_abrt\": \"abandonner et fermer cette fenêtre\\\">❌ cancel\",\n\t\"frb_apply\": \"APPLIQUER RENOMMER\",\n\t\"fr_adv\": \"renommage par lot / métadonnées / motif\\\">advanced\",\n\t\"fr_case\": \"regex sensible à la casse\\\">case\",\n\t\"fr_win\": \"noms windows-safe; remplacer <code>&lt;&gt;:&quot;\\\\|?*</code> par des caractères japonais en pleine largeur\\\">win\",\n\t\"fr_slash\": \"remplacer <code>/</code> par un caractère qui ne provoque pas la création de nouveaux dossiers\\\">no /\",\n\t\"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\",\n\t\"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\",\n\t\"fr_pdel\": \"supprimer\",\n\t\"fr_pnew\": \"enregistrer sous\",\n\t\"fr_pname\": \"donnez un nom pour le nouveau preset\",\n\t\"fr_aborted\": \"abandonné\",\n\t\"fr_lold\": \"ancien nom\",\n\t\"fr_lnew\": \"nouveau nom\",\n\t\"fr_tags\": \"tags pour les fichier selectioné (lecture-seule, juste pour référence):\",\n\t\"fr_busy\": \"renomage de {0} items…\\n\\n{1}\",\n\t\"fr_efail\": \"renomage a échoué:\\n\",\n\t\"fr_nchg\": \"{0} des nouveaux noms ont été modifiés en raison de <code>win</code> et/ou <code>no /</code>\\n\\nOK pour continuer avec ces nouveaux noms modifiés ?\",\n\n\t\"fd_ok\": \"suppression réussie\",\n\t\"fd_err\": \"impossible de supprimer:\\n\",\n\t\"fd_none\": \"rien n'a été supprimé ; peut-être bloqué par la configuration du serveur (xbd) ?\",\n\t\"fd_busy\": \"suppression de {0} éléments…\\n\\n{1}\",\n\t\"fd_warn1\": \"SUPPRIMER ces {0} éléments ?\",\n\t\"fd_warn2\": \"<b>Dernière chance !</b> Impossible de revenir en arrière. Supprimer ?\",\n\n\t\"fc_ok\": \"couper {0} éléments\",\n\t\"fc_warn\": 'couper {0} éléments\\n\\nmais : seul <b>cet</b> onglets peut les coller\\n(puisque la sélection est si absolument massive)',\n\n\t\"fcc_ok\": \"copié {0} éléments dans le presse-papiers\",\n\t\"fcc_warn\": 'copié {0} éléments dans le presse-papiers\\n\\nmais : seul <b>cet</b> onglet peut les coller\\n(puisque la sélection est si absolument massive)',\n\n\t\"fp_apply\": \"utiliser ces noms\",\n\t\"fp_skip\": \"ignorer les conflits\", //m\n\t\"fp_ecut\": \"en premier, coupez ou copiez quelques fichiers / dossiers à coller / déplacer\\n\\nnote: vous pouvez couper / coller a travers different onglets\",\n\t\"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\n\t\"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\n\t\"fp_emore\": \"il reste encore des collisions de noms de fichiers à corriger\",\n\t\"fp_ok\": \"déplacement OK\",\n\t\"fcp_ok\": \"copie OK\",\n\t\"fp_busy\": \"déplacement de {0} éléments…\\n\\n{1}\",\n\t\"fcp_busy\": \"copie de {0} éléments…\\n\\n{1}\",\n\t\"fp_abrt\": \"abandon en cours...\", //m\n\t\"fp_err\": \"deplacement échoué:\\n\",\n\t\"fcp_err\": \"copie échouée:\\n\",\n\t\"fp_confirm\": \"déplacer ces {0} éléments ici ?\",\n\t\"fcp_confirm\": \"copier ces {0} éléments ici ?\",\n\t\"fp_etab\": 'lecture du presse-papier venant d\\'un autre onglet échoué',\n\t\"fp_name\": \"téléversement d'un fichier de votre apareil. Donnez lui un nom:\",\n\t\"fp_both_m\": '<h6>choisisez ce qu\\'il faut coller</h6><code>Entrer</code> = Déplacer {0} fichiers de «{1}»\\n<code>ESC</code> = Téléverser {2} fichiers de votre appareil',\n\t\"fcp_both_m\": '<h6>choisissez ce qu\\'il faut coller</h6><code>Entrer</code> = Copier {0} fichiers de «{1}»\\n<code>ESC</code> = Téléverser {2} fichiers de votre appareil',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Déplacer</a><a href=\"#\" id=\"modal-ng\">Téléverser</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Copier</a><a href=\"#\" id=\"modal-ng\">Téléverser</a>',\n\n\t\"mk_noname\": \"entrez un nom dans le champ de texte à gauche avant de faire ça :p\",\n\t\"nmd_i1\": \"ajoutez aussi l’extension souhaitée, par exemple <code>.md</code>\", //m\n\t\"nmd_i2\": \"vous ne pouvez créer que des fichiers <code>.{0}</code> car vous n’avez pas la permission d’effacer\", //m\n\n\t\"tv_load\": \"Chargement du document texte:\\n\\n{0}\\n\\n{1}% ({2} de {3} MiB chargés)\",\n\t\"tv_xe1\": \"impossible de charger le fichier texte:\\n\\nerreur\",\n\t\"tv_xe2\": \"404, fichier introuvable\",\n\t\"tv_lst\": \"liste des fichiers texte dans\",\n\t\"tvt_close\": \"retour a la vue de dossier$NHotkey: M (ou Échap)\\\">❌ fermer\",\n\t\"tvt_dl\": \"télécharger ce fichier$NHotkey: Y\\\">💾 télécharger\",\n\t\"tvt_prev\": \"montrer le document précédent$NHotkey: i\\\">⬆ précédent\",\n\t\"tvt_next\": \"montrer le document suivant$NHotkey: K\\\">⬇ suivant\",\n\t\"tvt_sel\": \"sélectionner le fichier &nbsp; ( pour couper / copier / supprimer / … )$NHotkey: S\\\">sel\",\n\t\"tvt_j\": \"embellir json$NHotkey: shift-J\\\">j\", //m\n\t\"tvt_edit\": \"ouvrir le fichier dans l'éditeur de texte$NHotkey: E\\\">✏️ modifier\",\n\t\"tvt_tail\": \"surveiller le fichier pour les changements; montrer les nouvelles lignes en temps réel\\\">📡 suivre\",\n\t\"tvt_wrap\": \"retour à la ligne\\\">↵\",\n\t\"tvt_atail\": \"ancrer le défilement au fond de la page\\\">⚓\",\n\t\"tvt_ctail\": \"décoder les couleurs du terminal (ansi escape codes)\\\">🌈\",\n\t\"tvt_ntail\": \"limite de défilement en arrière (combien d'octets de texte à garder chargé)\",\n\n\t\"m3u_add1\": \"musique ajoutée à la playlist m3u\",\n\t\"m3u_addn\": \"{0} musiques ajoutées à la playlist m3u\",\n\t\"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\",\n\n\t\"gt_vau\": \"ne pas voir les vidéos, juste jouer l'audio\\\">🎧\",\n\t\"gt_msel\": \"activer la séléction de fichiers ; ctrl-clic sur un fichier pour override écraser$N$N<em>quand actif : double-cliquer sur un fichier / dossier pour l'ouvrir</em>$N$NHotkey: S\\\">multiséléction\",\n\t\"gt_crop\": \"rogner les miniatures au centre\\\"&gt;rogner\",\n\t\"gt_3x\": \"miniatures haute résolution\\\">3x\",\n\t\"gt_zoom\": \"zoomer\",\n\t\"gt_chop\": \"rogner\",\n\t\"gt_sort\": \"trier par\",\n\t\"gt_name\": \"nom\",\n\t\"gt_sz\": \"taille\",\n\t\"gt_ts\": \"date\",\n\t\"gt_ext\": \"type\",\n\t\"gt_c1\": \"tronquer les noms de fichiers (montrer moins)\",\n\t\"gt_c2\": \"tronquer les noms de fichiers (montrer plus)\",\n\n\t\"sm_w8\": \"recherche…\",\n\t\"sm_prev\": \"les résultats de recherche ci-dessous proviennent d'une requête précédente:\\n  \",\n\t\"sl_close\": \"fermer les résultats de recherche\",\n\t\"sl_hits\": \"affichage de {0} résultats\",\n\t\"sl_moar\": \"chercher plus\",\n\n\t\"s_sz\": \"taille\",\n\t\"s_dt\": \"date\",\n\t\"s_rd\": \"chemin\",\n\t\"s_fn\": \"nom\",\n\t\"s_ta\": \"tags\",\n\t\"s_ua\": \"up@\",\n\t\"s_ad\": \"adv.\",\n\t\"s_s1\": \"minimum MiB\",\n\t\"s_s2\": \"maximum MiB\",\n\t\"s_d1\": \"min. iso8601\",\n\t\"s_d2\": \"max. iso8601\",\n\t\"s_u1\": \"téléverser après\",\n\t\"s_u2\": \"et/ou avant\",\n\t\"s_r1\": \"le chemin contient &nbsp; (séparé par des espaces)\",\n\t\"s_f1\": \"le nom contient &nbsp; (négation avec -nope)\",\n\t\"s_t1\": \"les tags contiennent &nbsp; (^=début, fin=$)\",\n\t\"s_a1\": \"propriétés de métadonnées spécifiques\",\n\n\t\"md_eshow\": \"impossible d'afficher le rendu \",\n\t\"md_off\": \"[📜<em>readme</em>] disabled in [⚙️] -- document caché\",\n\n\t\"badreply\": \"Échec de l'analyse de la réponse du serveur\",\n\n\t\"xhr403\": \"403: Accès refusé\\n\\nessayez d'appuyer sur F5, peut-être que vous avez été déconnecté\",\n\t\"xhr0\": \"inconnu (vous avez probablement perdu la connexion au serveur, ou le serveur est hors ligne)\",\n\t\"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\",\n\t\"tl_xe1\": \"impossible de lister les sous-dossiers:\\n\\nerreur \",\n\t\"tl_xe2\": \"404: Dossier introuvable\",\n\t\"fl_xe1\": \"impossible de lister les fichiers dans le dossier:\\n\\nerreur \",\n\t\"fl_xe2\": \"404: Dossier introuvable\",\n\t\"fd_xe1\": \"impossible de créer le sous-dossier:\\n\\nerreur \",\n\t\"fd_xe2\": \"404: Dossier parent introuvable\",\n\t\"fsm_xe1\": \"impossible d'envoyer le message:\\n\\nerreur \",\n\t\"fsm_xe2\": \"404: Dossier parent introuvable\",\n\t\"fu_xe1\": \"échec du chargement de la liste des unpost du serveur:\\n\\nerreur \",\n\t\"fu_xe2\": \"404: Fichier introuvable??\",\n\n\t\"fz_tar\": \"fichier gnu-tar non compressé (linux / mac)\",\n\t\"fz_pax\": \"tar au format pax non compressé (plus lent)\",\n\t\"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é\",\n\t\"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é\",\n\t\"fz_zip8\": \"zip avec noms de fichiers utf8 (peut être instable sur windows 7 et versions antérieures)\",\n\t\"fz_zipd\": \"zip avec noms de fichiers cp437 traditionnels, pour les très anciens logiciels\",\n\t\"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)\",\n\n\t\"un_m1\": \"vous pouvez supprimer vos téléchargements récents (ou annuler ceux en cours) ci-dessous\",\n\t\"un_upd\": \"rafraîchir\",\n\t\"un_m4\": \"ou partager les fichiers visibles ci-dessous:\",\n\t\"un_ulist\": \"montrer\",\n\t\"un_ucopy\": \"copier\",\n\t\"un_flt\": \"filtre optionnel:&nbsp; l'URL doit contenir\",\n\t\"un_fclr\": \"effacer le filtre\",\n\t\"un_derr\": 'échec de l\\'unpost-delete:\\n',\n\t\"un_f5\": 'quelque chose a cassé, veuillez essayer de rafraîchir ou d\\'appuyer sur F5',\n\t\"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é\",\n\t\"un_nou\": '<b>warning:</b> serveur trop occupé pour afficher les téléversements non finis; cliquez sur le lien \"rafraîchir\" dans un instant',\n\t\"un_noc\": '<b>warning:</b> unpost des fichiers entièrement téléchargés n\\'est pas activé/permis dans la configuration du serveur',\n\t\"un_max\": \"affichage des 2000 premiers fichiers (utilisez le filtre)\",\n\t\"un_avail\": \"{0} téléchargements récents peuvent être supprimés<br />{1} ceux en cours peuvent être annulés\",\n\t\"un_m2\": \"triés par date de téléchargement; les plus récents en premier:\",\n\t\"un_no1\": \"sike! aucun téléchargement n'est suffisamment récent\",\n\t\"un_no2\": \"sike! aucun téléchargement correspondant à ce filtre n'est suffisamment récent\",\n\t\"un_next\": \"supprimer les {0} fichiers suivants ci-dessous\",\n\t\"un_abrt\": \"abandonner\",\n\t\"un_del\": \"supprimer\",\n\t\"un_m3\": \"chargement de vos téléchargements récents…\",\n\t\"un_busy\": \"suppression de {0} fichiers…\",\n\t\"un_clip\": \"{0} liens copiés dans le presse-papiers\",\n\n\t\"u_https1\": \"vous devriez\",\n\t\"u_https2\": \"passer à https\",\n\t\"u_https3\": \"pour de meilleure performances\",\n\t\"u_ancient\": 'votre navigateur est impressionnamment ancien -- vous devriez peut-être <a href=\"#\" onclick=\"goto(\\'bup\\')\">utiliser bup à la place</a>',\n\t\"u_nowork\": \"nécessite firefox 53+ ou chrome 57+ ou iOS 11+\",\n\t\"tail_2old\": \"nécessite firefox 105+ ou chrome 71+ ou iOS 14.5+\",\n\t\"u_nodrop\": 'votre navigateur est trop ancien pour le téléversement par glisser-déposer',\n\t\"u_notdir\": \"ce n'est pas un dossier!\\n\\nvotre navigateur est trop ancien,\\nveuillez essayer le glisser-déposer à la place\",\n\t\"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\",\n\t\"u_enpot\": 'passer à <a href=\"#\">l\\'interface utilisateur potato</a> (peut améliorer la vitesse de téléversement)',\n\t\"u_depot\": 'passer à <a href=\"#\">l\\'interface utilisateur fancy</a> (peut réduire la vitesse de téléversement)',\n\t\"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 !',\n\t\"u_pott\": \"<p>fichiers: &nbsp; <b>{0}</b> fini, &nbsp; <b>{1}</b> échoué, &nbsp; <b>{2}</b> en cours, &nbsp; <b>{3}</b> en attente</p>\",\n\t\"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\",\n\t\"u_su2k\": 'ceci est le téléverseur de base; <a href=\"#\" id=\"u2yea\">up2k</a> est meilleur',\n\t\"u_uput\": 'optimiser pour la vitesse (ignorer la somme de contrôle)',\n\t\"u_ewrite\": 'vous n\\'avez pas accès en écriture à ce dossier',\n\t\"u_eread\": 'vous n\\'avez pas accès en lecture à ce dossier',\n\t\"u_enoi\": 'la recherche de fichiers n\\'est pas activée dans la configuration du serveur',\n\t\"u_enoow\": \"l'écrasage ne fonctionnera pas ici; besoin de permissions de suppression\",\n\t\"u_badf\": 'Ces {0} fichiers (sur {1} au total) ont été ignorés, probablement en raison de permissions système de fichiers:\\n\\n',\n\t\"u_blankf\": 'Ces {0} fichiers (sur {1} au total) sont vides; les téléverser quand même ?\\n\\n',\n\t\"u_applef\": 'Ces {0} fichiers (sur {1} au total) sont probablement indésirables;\\nAppuyez sur <code>OK/Enter</code> pour IGNORER les fichiers suivants,\\nAppuyez sur <code>Annuler/Échap</code> pour NE PAS exclure, et TÉLÉVERSER ceux-ci également:\\n\\n',\n\t\"u_just1\": '\\nPeut-être que cela fonctionne mieux si vous sélectionnez juste un fichier',\n\t\"u_ff_many\": \"si vous utilisez <b>Linux / MacOS / Android,</b> alors ce nombre de fichiers <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\"><em>peut</em> faire planter Firefox!</a>\\nSi cela se produit, veuillez réessayer (ou utiliser Chrome).\",\n\t\"u_up_life\": \"Ce téléversement va être supprimé du serveur\\n{0} après son achèvement\",\n\t\"u_asku\": 'téléverser ces {0} fichiers vers <code>{1}</code>',\n\t\"u_unpt\": \"vous pouvez défaire / supprimer ce téléversement en utilisant le 🧯 en haut à gauche\",\n\t\"u_bigtab\": 'sur le point d\\'afficher {0} fichiers\\n\\ncela peut faire planter votre navigateur, êtes-vous sûr ?',\n\t\"u_scan\": 'Analyse des fichiers…',\n\t\"u_dirstuck\": 'l\\'itérateur de répertoire est bloqué en essayant d\\'accéder aux {0} éléments suivants ; il sera ignoré :',\n\t\"u_etadone\": 'Terminé ({0}, {1} fichiers)',\n\t\"u_etaprep\": '(préparation au téléversement)',\n\t\"u_hashdone\": 'calcul de la somme de contrôle terminé',\n\t\"u_hashing\": 'calcul de la somme de contrôle',\n\t\"u_hs\": 'établissement d\\'une liaison…',\n\t\"u_started\": \"les fichiers sont maintenant en cours de téléversement ; voir [🚀]\",\n\t\"u_dupdefer\": \"dupliqué ; sera traité après tous les autres fichiers\",\n\t\"u_actx\": \"cliquez sur ce texte pour éviter la perte de<br />performance lors du passage à d'autres fenêtres/onglets\",\n\t\"u_fixed\": \"OK!&nbsp; Résolu 👍\",\n\t\"u_cuerr\": \"echec du téléversement du morceau {0} de {1};\\nprobablement inoffensif, poursuite\\n\\nfichier : {2}\",\n\t\"u_cuerr2\": \"le serveur a rejeté le téléversement (morceau {0} de {1});\\nréessaiera plus tard\\n\\nfichier : {2}\\n\\nerreur \",\n\t\"u_ehstmp\": \"réessaiera ; voir en bas à droite\",\n\t\"u_ehsfin\": \"le serveur a rejeté la demande de finalisation du téléversement ; nouvelle tentative…\",\n\t\"u_ehssrch\": \"le serveur a rejeté la demande d'effectuer une recherche ; nouvelle tentative…\",\n\t\"u_ehsinit\": \"le serveur a rejeté la demande d'initier le téléversement ; nouvelle tentative…\",\n\t\"u_eneths\": \"erreur réseau lors de l'exécution de l'initialisation du téléversement ; nouvelle tentative…\",\n\t\"u_enethd\": \"erreur réseau lors du test de l'existence de la cible ; nouvelle tentative…\",\n\t\"u_cbusy\": \"attente que le serveur nous fasse à nouveau confiance après un problème réseau…\",\n\t\"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\",\n\t\"u_emtleak1\": \"il semble que votre navigateur web ait une fuite de mémoire ;\\nveuillez\",\n\t\"u_emtleak2\": ' <a href=\"{0}\">passer à https (recommandé)</a> ou ',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": 'essayez la solution suivante:\\n<ul><li>appuyez sur <code>F5</code> pour rafraîchir la page</li><li>ensuite désactivez le bouton &nbsp;<code>mt</code>&nbsp; dans les &nbsp;<code>⚙️ paramètres</code></li><li>et réessayez ce téléversement</li></ul>Les téléversements seront un peu plus lents, mais tant pis.\\nDésolé pour le dérangement !\\n\\nPS : chrome v107 <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">a un correctif</a> pour cela',\n\t\"u_emtleakf\": 'essayez la solution suivante:\\n<ul><li>appuyez sur <code>F5</code> pour rafraîchir la page</li><li>ensuite activez <code>🥔</code> (pomme de terre) dans l\\'interface de téléversement</li><li>et réessayez ce téléversement</li></ul>\\nPS : firefox <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">aura probablement un correctif</a> à un moment donné',\n\t\"u_s404\": \"pas trouvé sur le serveur\",\n\t\"u_expl\": \"expliquer\",\n\t\"u_maxconn\": \"la plupart des navigateur limite ceci à 6, mais firefox vous permet de l'augmenter avec <code>connections-per-server</code> dans <code>about:config</code>\",\n\t\"u_tu\": '<p class=\"warn\">WARNING: turbo enclenché, <span>&nbsp;le client peut ne pas détecter et reprendre les téléversements incomplets ; voir l\\'info-bulle du bouton turbo</span></p>',\n\t\"u_ts\": '<p class=\"warn\">WARNING: turbo enclenché, <span>&nbsp;les résultats de recherche peuvent être incorrects ; voir l\\'info-bulle du bouton turbo</span></p>',\n\t\"u_turbo_c\": \"turbo est désactivé dans la configuration du serveur\",\n\t\"u_turbo_g\": \"désactivation de turbo car vous n'avez pas de\\nprivilèges de listing de répertoires dans ce volume\",\n\t\"u_life_cfg\": 'suppression automatique après <input id=\"lifem\" p=\"60\" /> min (ou <input id=\"lifeh\" p=\"3600\" /> heures)',\n\t\"u_life_est\": 'le téléversement sera supprimé <span id=\"lifew\" tt=\"local time\">---</span>',\n\t\"u_life_max\": 'ce dossier impose une\\ndurée de vie maximale de {0}',\n\t\"u_unp_ok\": 'unpost est autorisé pour {0}',\n\t\"u_unp_ng\": 'unpost ne sera PAS autorisé',\n\t\"ue_ro\": 'votre accès à ce dossier est en lecture seule\\n\\n',\n\t\"ue_nl\": 'vous n\\'êtes actuellement pas connecté',\n\t\"ue_la\": 'vous êtes actuellement connecté en tant que \"{0}\"',\n\t\"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é',\n\t\"ue_ta\": 'essayez de téléverser à nouveau, cela devrait fonctionner maintenant',\n\t\"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.\",\n\t\"ur_1uo\": \"OK: Fichier téléversé avec succès\",\n\t\"ur_auo\": \"OK: Tous les {0} fichiers téléversés avec succès\",\n\t\"ur_1so\": \"OK: Fichier trouvé sur le serveur\",\n\t\"ur_aso\": \"OK: Tous les {0} fichiers trouvés sur le serveur\",\n\t\"ur_1un\": \"Échec du téléversement, désolé\",\n\t\"ur_aun\": \"Tous les {0} téléversements ont échoué, désolé\",\n\t\"ur_1sn\": \"Fichier NON trouvé sur le serveur\",\n\t\"ur_asn\": \"Les {0} fichiers n'ont PAS ÉTÉ trouvés sur le serveur\",\n\t\"ur_um\": \"Terminé;\\n{0} téléversements OK,\\n{1} téléversements échoués, désolé\",\n\t\"ur_sm\": \"Terminé;\\n{0} fichiers trouvés sur le serveur,\\n{1} fichiers NON trouvés sur le serveur\",\n\n\t\"rc_opn\": \"ouvrir\", //m\n\t\"rc_ply\": \"Lire\", //m\n\t\"rc_pla\": \"Lire comme audio\", //m\n\t\"rc_txt\": \"ouvrir dans le visionneur de fichiers\", //m\n\t\"rc_md\": \"ouvrir dans l’éditeur de texte\", //m\n\t\"rc_dl\": \"télécharger\", //m\n\t\"rc_zip\": \"télécharger comme archive\", //m\n\t\"rc_cpl\": \"copier le lien\", //m\n\t\"rc_del\": \"supprimer\", //m\n\t\"rc_cut\": \"couper\", //m\n\t\"rc_cpy\": \"copier\", //m\n\t\"rc_pst\": \"coller\", //m\n\t\"rc_rnm\": \"renommer\", //m\n\t\"rc_nfo\": \"nouveau dossier\", //m\n\t\"rc_nfi\": \"nouveau fichier\", //m\n\t\"rc_sal\": \"tout sélectionner\", //m\n\t\"rc_sin\": \"inverser la sélection\", //m\n\t\"rc_shf\": \"partager ce dossier\", //m\n\t\"rc_shs\": \"partager la sélection\", //m\n\n\t\"lang_set\": \"rafraîchir pour que les changements prennent effet ?\",\n\n\t\"splash\": {\n\t\t\"a1\": \"rafraîchir\",\n\t\t\"b1\": \"salut étranger &nbsp; <small>(vous n'êtes pas connecté.)</small>\",\n\t\t\"c1\": \"déconnexion\",\n\t\t\"d1\": \"vidange de la pile\",\n\t\t\"d2\": \"affiche l'état de tous les threads actifs\",\n\t\t\"e1\": \"recharger la configuration\",\n\t\t\"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\",\n\t\t\"f1\": \"vous pouvez naviguer :\",\n\t\t\"g1\": \"vous pouvez télécharger sur :\",\n\t\t\"cc1\": \"autres choses :\",\n\t\t\"h1\": \"désactiver k304\",\n\t\t\"i1\": \"activer k304\",\n\t\t\"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), <em>mais</em> cela ralentira également les choses en général\",\n\t\t\"k1\": \"réinitialiser les paramètres du client\",\n\t\t\"l1\": \"connectez-vous pour en savoir plus :\",\n\t\t\"ls3\": \"se connecter\", //m\n\t\t\"lu4\": \"nom d'utilisateur\", //m\n\t\t\"lp4\": \"mot de passe\", //m\n\t\t\"lo3\": \"déconnecter “{0}” partout\", //m\n\t\t\"lo2\": \"cela mettra fin à la session sur tous les navigateurs\", //m\n\t\t\"m1\": \"heureux de vous revoir,\",\n\t\t\"n1\": \"404 introuvable &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": 'ou peut-être que vous n\\'y avez pas accès -- essayer un mot de passe ou <a href=\"' + SR + '/?h\">aller à la page d\\'accueil</a>',\n\t\t\"p1\": \"403 interdit &nbsp;~┻━┻\",\n\t\t\"q1\": 'utiliser un mot de passe ou <a href=\"' + SR + '/?h\">aller à la page d\\'accueil</a>',\n\t\t\"r1\": \"aller à la page d\\'accueil\",\n\t\t\".s1\": \"rescanner\",\n\t\t\"t1\": \"action\",\n\t\t\"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\",\n\t\t\"v1\": \"connecter\",\n\t\t\"v2\": \"utilisez ce serveur en tant que disque dur local\",\n\t\t\"w1\": \"passer à https\",\n\t\t\"x1\": \"changer mot de passe\",\n\t\t\"y1\": \"modifier les partages\",\n\t\t\"z1\": \"déverrouiller ce partage :\",\n\t\t\"ta1\": \"entrez d'abord votre nouveau mot de passe\",\n\t\t\"ta2\": \"répétez pour confirmer le nouveau mot de passe :\",\n\t\t\"ta3\": \"une faute de frappe a été détectée ; veuillez réessayer.\",\n\t\t\"nop\": \"ERREUR : Le mot de passe ne peut pas être vide\", //m\n\t\t\"nou\": \"ERREUR : Le nom d’utilisateur et/ou le mot de passe ne peut pas être vide\", //m\n\t\t\"aa1\": \"fichiers entrants :\",\n\t\t\"ab1\": \"désactiver no304\",\n\t\t\"ac1\": \"activer no304\",\n\t\t\"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 !\",\n\t\t\"ae1\": \"téléchargements actifs :\",\n\t\t\"af1\": \"afficher les derniers téléchargements\",\n\t\t\"ag1\": \"afficher les utilisateurs IdP connus\", //m\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/grc.js",
    "content": "\n// Οι γραμμές που τελειώνουν με //m είναι μη επαληθευμένες μηχανικές μεταφράσεις\n\nLs.grc = {\n\t\"tt\": \"Ελληνικά\",\n\n\t\"cols\": {\n\t\t\"c\": \"κουμπιά ενεργειών\",\n\t\t\"dur\": \"διάρκεια\",\n\t\t\"q\": \"ποιότητα / bitrate\",\n\t\t\"Ac\": \"κωδικοποιητής ήχου\",\n\t\t\"Vc\": \"κωδικοποιητής βίντεο\",\n\t\t\"Fmt\": \"μορφή / container\",\n\t\t\"Ahash\": \"checksum ήχου\",\n\t\t\"Vhash\": \"checksum βίντεο\",\n\t\t\"Res\": \"ανάλυση\",\n\t\t\"T\": \"τύπος αρχείου\",\n\t\t\"aq\": \"ποιότητα ήχου / bitrate\",\n\t\t\"vq\": \"ποιότητα βίντεο / bitrate\",\n\t\t\"pixfmt\": \"subsampling / δομή εικονοστοιχείων\",\n\t\t\"resw\": \"οριζόντια ανάλυση\",\n\t\t\"resh\": \"κάθετη ανάλυση\",\n\t\t\"chs\": \"κανάλια ήχου\",\n\t\t\"hz\": \"συχνότητα δειγματοληψίας\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"διάφορα\",\n\t\t\t[\"ESC\", \"κλείσιμο διαφόρων λειτουργιών\"],\n\n\t\t\t\"διαχειριστής αρχείων\",\n\t\t\t[\"G\", \"εναλλαγή λίστας / πλέγματος\"],\n\t\t\t[\"T\", \"εναλλαγή μικρογραφιών / εικονιδίων\"],\n\t\t\t[\"⇧ A/D\", \"μέγεθος μικρογραφιών\"],\n\t\t\t[\"ctrl-K\", \"διαγραφή επιλεγμένων\"],\n\t\t\t[\"ctrl-X\", \"αποκοπή επιλογής στο πρόχειρο\"],\n\t\t\t[\"ctrl-C\", \"αντιγραφή επιλογής στο πρόχειρο\"],\n\t\t\t[\"ctrl-V\", \"επικόλληση (μετακίνηση/αντιγραφή) εδώ\"],\n\t\t\t[\"Y\", \"λήψη επιλεγμένων\"],\n\t\t\t[\"F2\", \"μετονομασία επιλεγμένων\"],\n\n\t\t\t\"λίστα αρχείων\",\n\t\t\t[\"space\", \"εναλλαγή επιλογής αρχείου\"],\n\t\t\t[\"↑/↓\", \"μετακίνηση δείκτη επιλογής\"],\n\t\t\t[\"ctrl ↑/↓\", \"μετακίνηση δείκτη και προβολής\"],\n\t\t\t[\"⇧ ↑/↓\", \"επιλογή προηγούμενου/επόμενου αρχείου\"],\n\t\t\t[\"ctrl-A\", \"επιλογή όλων των αρχείων / φακέλων\"]\n\t\t], [\n\t\t\t\"πλοήγηση\",\n\t\t\t[\"B\", \"εναλλαγή σε καρτέλες διαδρομών / δέντρο διαδρομών\"],\n\t\t\t[\"I/K\", \"προηγούμενος/επόμενος φάκελος\"],\n\t\t\t[\"M\", \"γονικός φάκελος (ή σμίκρυνση τρέχοντος)\"],\n\t\t\t[\"V\", \"εναλλαγή φακέλων / δέντρο αρχείων κειμένου\"],\n\t\t\t[\"A/D\", \"μέγεθος πίνακα πλοήγησης\"]\n\t\t], [\n\t\t\t\"μουσική\",\n\t\t\t[\"J/L\", \"προηγούμενο/επόμενο τραγούδι\"],\n\t\t\t[\"U/O\", \"μετάβαση 10δευτ πίσω/μπροστά\"],\n\t\t\t[\"0..9\", \"μετάβαση στο 0%..90%\"],\n\t\t\t[\"P\", \"αναπαραγωγή/παύση (ξεκινάει κιόλας)\"],\n\t\t\t[\"S\", \"επιλογή αναπαραγόμενου τραγουδιού\"],\n\t\t\t[\"Y\", \"λήψη τραγουδιού\"]\n\t\t], [\n\t\t\t\"εικόνες\",\n\t\t\t[\"J/L, ←/→\", \"προηγούμενη/επόμενη εικόνα\"],\n\t\t\t[\"Home/End\", \"πρώτη/τελευταία εικόνα\"],\n\t\t\t[\"F\", \"πλήρης οθόνη\"],\n\t\t\t[\"R\", \"περιστροφή δεξιόστροφα\"],\n\t\t\t[\"⇧ R\", \"περιστροφή αριστερόστροφα\"],\n\t\t\t[\"S\", \"επιλογή εικόνας\"],\n\t\t\t[\"Y\", \"λήψη εικόνας\"]\n\t\t], [\n\t\t\t\"βίντεο\",\n\t\t\t[\"U/O\", \"μετάβαση 10δευτ πίσω/μπροστά\"],\n\t\t\t[\"P/K/Space\", \"αναπαραγωγή/παύση\"],\n\t\t\t[\"C\", \"συνέχεια στο επόμενο\"],\n\t\t\t[\"V\", \"επανάληψη\"],\n\t\t\t[\"M\", \"σίγαση\"],\n\t\t\t[\"[ και ]\", \"ορισμός διαστήματος επανάληψης\"]\n\t\t], [\n\t\t\t\"αρχεία κειμένου\",\n\t\t\t[\"I/K\", \"προηγούμενο/επόμενο αρχείο\"],\n\t\t\t[\"M\", \"κλείσιμο αρχείου\"],\n\t\t\t[\"E\", \"επεξεργασία αρχείου\"],\n\t\t\t[\"S\", \"επιλογή αρχείου (για αποκοπή/αντιγραφή/μετονομασία)\"],\n\t\t\t[\"Y\", \"λήψη αρχείου κειμένου\"], //m\n\t\t\t[\"⇧ J\", \"ομορφοποίηση json\"], //m\n\t\t]\n\t],\n\n\t\"m_ok\": \"Εντάξει\",\n\t\"m_ng\": \"Άκυρο\",\n\n\t\"enable\": \"Ενεργοποίηση\",\n\t\"danger\": \"ΚΙΝΔΥΝΟΣ\",\n\t\"clipped\": \"αντιγράφηκε στο πρόχειρο\",\n\n\t\"ht_s1\": \"δευτερόλεπτο\",\n\t\"ht_s2\": \"δευτερόλεπτα\",\n\t\"ht_m1\": \"λεπτό\",\n\t\"ht_m2\": \"λεπτά\",\n\t\"ht_h1\": \"ώρα\",\n\t\"ht_h2\": \"ώρες\",\n\t\"ht_d1\": \"μέρα\",\n\t\"ht_d2\": \"μέρες\",\n\t\"ht_and\": \" και \",\n\n\t\"goh\": \"πίνακας ελέγχου\",\n\t\"gop\": 'προηγούμενος φάκελος στο ίδιο επίπεδο\">προηγούμενο',\n\t\"gou\": 'γονικός φάκελος\">πάνω',\n\t\"gon\": 'επόμενος φάκελος\">επόμενο',\n\t\"logout\": \"Αποσύνδεση \",\n\t\"login\": \"Σύνδεση\", //m\n\t\"access\": \" πρόσβαση\",\n\t\"ot_close\": \"κλείσιμο υπομενού\",\n\t\"ot_search\": \"`αναζήτηση αρχείων με βάση χαρακτηριστικά, διαδρομή / όνομα, μουσικά tags ή οποιονδήποτε συνδυασμό$N$N`foo bar` = πρέπει να περιέχει και τα «foo» και «bar»,$N`foo -bar` = πρέπει να περιέχει το «foo» αλλά όχι το «bar»,$N`^yana .opus$` = να ξεκινά με «yana» και να είναι αρχείο «opus»$N`&quot;try unite&quot;` = να περιέχει ακριβώς «try unite»$N$Nη μορφή ημερομηνίας είναι iso-8601, όπως$N`2009-12-31` ή `2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"unpost: διαγραφή πρόσφατων μεταφορτώσεων ή ακύρωση ανολοκλήρωτων\",\n\t\"ot_bup\": \"bup: βασικός uploader, υποστηρίζει μέχρι και netscape 4.0\",\n\t\"ot_mkdir\": \"mkdir: δημιουργία νέου φακέλου\",\n\t\"ot_md\": \"new-file: δημιουργία νέου αρχείου κειμένου\", //m\n\t\"ot_msg\": \"msg: αποστολή μηνύματος στο server log\",\n\t\"ot_mp\": \"επιλογές media player\",\n\t\"ot_cfg\": \"επιλογές ρυθμίσεων\",\n\t\"ot_u2i\": 'up2k: ανέβασε αρχεία (αν έχεις δικαίωμα εγγραφής) ή ενεργοποίησε τη λειτουργία αναζήτησης για να δεις αν υπάρχουν ήδη στο server$N$Nοι μεταφορτώσεις συνεχίζονται αν διακοπούν, είναι πολυνηματικές και διατηρούν τις χρονοσφραγίδες, αλλά καταναλώνουν περισσότερο CPU από τον [🎈]&nbsp; (βασικός uploader)<br /><br />κατά τη διάρκεια της μεταφόρτωσης, αυτό το εικονίδιο δείχνει την πρόοδό της!',\n\t\"ot_u2w\": 'up2k: ανέβασε αρχεία με υποστήριξη συνέχισης (κλείσε τον browser και ρίξε τα ίδια αρχεία ξανά μετά)$N$Nπολυνηματικό, διατηρεί τις χρονοσφραγίδες, αλλά καταναλώνει περισσότερο CPU από τον [🎈]&nbsp; (βασικός uploader)<br /><br />κατά τη διάρκεια της μεταφόρτωσης, αυτό το εικονίδιο δείχνει την πρόοδό της!',\n\t\"ot_noie\": 'Χρησιμοποίησε Chrome / Firefox / Edge',\n\n\t\"ab_mkdir\": \"δημιουργία φακέλου\",\n\t\"ab_mkdoc\": \"νέο αρχείο κειμένου\", //m\n\t\"ab_msg\": \"στείλε μήνυμα στο server log\",\n\n\t\"ay_path\": \"πήγαινε σε φακέλους\",\n\t\"ay_files\": \"πήγαινε σε αρχεία\",\n\n\t\"wt_ren\": \"μετονομασία επιλεγμένων$NΣυντόμευση: F2\",\n\t\"wt_del\": \"διαγραφή επιλεγμένων$NΣυντόμευση: ctrl-K\",\n\t\"wt_cut\": \"αποκοπή επιλεγμένων &lt;small&gt;(και επικόλληση αλλού)&lt;/small&gt;$NΣυντόμευση: ctrl-X\",\n\t\"wt_cpy\": \"αντιγραφή επιλεγμένων στο πρόχειρο$N(για επικόλληση αλλού)$NΣυντόμευση: ctrl-C\",\n\t\"wt_pst\": \"επικόλληση αποκομμένων / αντεγραμμένων$NΣυντόμευση: ctrl-V\",\n\t\"wt_selall\": \"επιλογή όλων$NΣυντόμευση: ctrl-A (με το αρχείο επιλεγμένο)\",\n\t\"wt_selinv\": \"αντιστροφή επιλογής\",\n\t\"wt_zip1\": \"κατέβασμα φακέλου ως συμπιεσμένο αρχείο\",\n\t\"wt_selzip\": \"κατέβασμα επιλογής ως συμπιεσμένο αρχείο\",\n\t\"wt_seldl\": \"κατέβασμα επιλογής ως μεμονωμένα αρχεία$NΣυντόμευση: Y\",\n\t\"wt_npirc\": \"αντιγραφή πληροφοριών τραγουδιού σε μορφή irc\",\n\t\"wt_nptxt\": \"αντιγραφή πληροφοριών τραγουδιού ως κείμενο\",\n\t\"wt_m3ua\": \"προσθήκη σε m3u λίστα αναπαραγωγής (μετά πάτησε <code>📻αντιγραφή</code>)\",\n\t\"wt_m3uc\": \"αντιγραφή m3u λίστας αναπαραγωγής στο πρόχειρο\",\n\t\"wt_grid\": \"εναλλαγή πλέγματος / λίστας$NΣυντόμευση: G\",\n\t\"wt_prev\": \"προηγούμενο κομμάτι$NΣυντόμευση: J\",\n\t\"wt_play\": \"αναπαραγωγή / παύση$NΣυντόμευση: P\",\n\t\"wt_next\": \"επόμενο κομμάτι$NΣυντόμευση: L\",\n\n\t\"ul_par\": \"παράλληλες μεταφορτώσεις:\",\n\t\"ut_rand\": \"τυχαιοποίηση ονομάτων αρχείων\",\n\t\"ut_u2ts\": \"αντιγραφή της τελευταίας τροποποιημένης χρονοσφραγίδας αλλαγής$Nαπό το σύστημά σου στον server\\\">📅\",\n\t\"ut_ow\": \"αντικατάσταση σε ήδη υπάρχοντα αρχεία του server?$N🛡️: ποτέ (θα δημιουργηθεί νέο όνομα)$N🕒: αν το αρχείο του server είναι παλαιότερο$N♻️: πάντα να αντικαθίστανται αν διαφέρουν$N⏭️: παράλειψη όλων των υπαρχόντων αρχείων χωρίς όρους\", //m\n\t\"ut_mt\": \"συνέχιση υπολογισμού hash για άλλα αρχεία κατά τη μεταφόρτωση$N$Nαπενεργοποίησέ το αν η CPU ή ο δίσκος σου ζορίζονται\",\n\t\"ut_ask\": 'επιβεβαίωση πριν ξεκινήσει η μεταφόρτωση\">💭',\n\t\"ut_pot\": \"βελτίωση ταχύτητας μεταφόρτωσης σε αργές συσκευές$Nμε απλοποίηση του UI\",\n\t\"ut_srch\": \"μην ανεβάζεις, έλεγξε αν τα αρχεία$Nυπάρχουν ήδη στον server (ψάχνει σε όλους τους φακέλους που έχεις πρόσβαση)\",\n\t\"ut_par\": \"κάνε παύση στις μεταφορτώσεις βάζοντάς το 0$N$Nαύξησε το αν έχεις αργή/μεγάλη καθυστέρηση σύνδεσης$N$Nκράτα το 1 σε LAN ή αν ο server έχει αργό δίσκο\",\n\t\"ul_btn\": \"ρίξε αρχεία / φακέλους<br>εδώ (ή κάνε κλικ σε μένα)\",\n\t\"ul_btnu\": \"Μ Ε Τ Α Φ Ο Ρ Τ Ω Σ Η\",\n\t\"ul_btns\": \"Α Ν Α Ζ Η Τ Η Σ Η\",\n\n\t\"ul_hash\": \"υπολογισμός hash\",\n\t\"ul_send\": \"αποστολή\",\n\t\"ul_done\": \"ολοκληρώθηκε\",\n\t\"ul_idle1\": \"καμία μεταφόρτωση στην ουρά για την ώρα\",\n\t\"ut_etah\": \"μέση ταχύτητα &lt;em&gt;υπολογισμού hash&lt;/em&gt; και εκτίμηση χρόνου μέχρι την ολοκλήρωση\",\n\t\"ut_etau\": \"μέση ταχύτητα &lt;em&gt;μεταφόρτωσης&lt;/em&gt; και εκτίμηση χρόνου μέχρι την ολοκλήρωση\",\n\t\"ut_etat\": \"μέση &lt;em&gt;συνολική&lt;/em&gt; ταχύτητα και εκτίμηση χρόνου μέχρι την ολοκλήρωση\",\n\n\t\"uct_ok\": \"ολοκληρώθηκε επιτυχώς\",\n\t\"uct_ng\": \"no-good: απέτυχε / απορρίφθηκε / δεν βρέθηκε\",\n\t\"uct_done\": \"ολοκληρωμένα και αποτυχημένα\",\n\t\"uct_bz\": \"κάνει hash ή μεταφορτώνει\",\n\t\"uct_q\": \"σε αναμονή, εκκρεμεί\",\n\n\t\"utl_name\": \"όνομα αρχείου\",\n\t\"utl_ulist\": \"λίστα\",\n\t\"utl_ucopy\": \"αντιγραφή\",\n\t\"utl_links\": \"σύνδεσμοι\",\n\t\"utl_stat\": \"κατάσταση\",\n\t\"utl_prog\": \"πρόοδος\",\n\n\t// keep short:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"ΣΦΑΛΜΑ\",\n\t\"utl_oserr\": \"ΣΦ-ΛΕ\",\n\t\"utl_found\": \"βρέθηκε\",\n\t\"utl_defer\": \"αναβολή\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"έγινε\",\n\n\t\"ul_flagblk\": \"τα αρχεία προστέθηκαν στην ουρά</b><br>αλλά υπάρχει άλλη ενεργή μεταφόρτωση σε άλλη καρτέλα,<br>οπότε περίμενε να τελειώσει αυτό πρώτα\",\n\t\"ul_btnlk\": \"ο διακομιστής έχει κλειδώσει αυτήν την επιλογή σε αυτήν την κατάσταση\",\n\n\t\"udt_up\": \"Μεταφόρτωση\",\n\t\"udt_srch\": \"Αναζήτηση\",\n\t\"udt_drop\": \"ρίξ' το εδώ\",\n\n\t\"u_nav_m\": '<h6>οκ, τι έχουμε εδώ;</h6><code>Enter</code> = Αρχεία (ένα ή περισσότερα)\\n<code>ESC</code> = Ένας φάκελος (μαζί με υποφακέλους)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Αρχεία</a><a href=\"#\" id=\"modal-ng\">Ένας φάκελος</a>',\n\n\t\"cl_opts\": \"διακόπτες\",\n\t\"cl_hfsz\": \"μέγεθος αρχείου\", //m\n\t\"cl_themes\": \"θέμα\",\n\t\"cl_langs\": \"γλώσσα\",\n\t\"cl_ziptype\": \"λήψη φακέλου\",\n\t\"cl_uopts\": \"διακόπτες μεταφόρτωσης\",\n\t\"cl_favico\": \"favicon\",\n\t\"cl_bigdir\": \"μεγάλοι φάκελοι\",\n\t\"cl_hsort\": \"#ταξινόμηση\",\n\t\"cl_keytype\": \"σημείωση πλήκτρων\",\n\t\"cl_hiddenc\": \"κρυφές στήλες\",\n\t\"cl_hidec\": \"κρύψε\",\n\t\"cl_reset\": \"επανεκκίνηση\",\n\t\"cl_hpick\": \"πάτησε στις κεφαλίδες στηλών για να τις κρύψεις στον πίνακα παρακάτω\",\n\t\"cl_hcancel\": \"η απόκρυψη στηλών ακυρώθηκε\",\n\t\"cl_rcm\": \"μενού δεξιού κλικ\", //m\n\n\t\"ct_grid\": '田 το πλέγμα',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ συμβουλές εργαλείων',\n\t\"ct_thumb\": 'σε προβολή πλέγματος, εναλλαγή εικονιδίων ή μικρογραφιών$NΠλήκτρο συντόμευσης: T\">🖼️ μικρογραφίες',\n\t\"ct_csel\": 'χρησιμοποίησε CTRL και SHIFT για επιλογή αρχείων σε προβολή πλέγματος\">επιλογή',\n\t\"ct_dsel\": 'χρησιμοποίησε επιλογή με σύρσιμο σε προβολή πλέγματος\">σύρσιμο', //m\n\t\"ct_dl\": 'εξαναγκασμός λήψης (να μην εμφανίζεται ενσωματωμένα) όταν γίνεται κλικ σε ένα αρχείο\">dl', //m\n\t\"ct_ihop\": 'όταν η προβολή εικόνων κλείνει, κάνε scroll στο τελευταίο προβαλλόμενο αρχείο\">g⮯',\n\t\"ct_dots\": 'εμφάνιση κρυφών αρχείων (αν το επιτρέπει ο server)\">dotfiles',\n\t\"ct_qdel\": 'όταν διαγράφεις αρχεία, ζήτα επιβεβαίωση μόνο μία φορά\">γρήγορη διαγραφή',\n\t\"ct_dir1st\": 'ταξινόμηση φακέλων πριν από τα αρχεία\">📁 πρώτα',\n\t\"ct_nsort\": 'φυσική ταξινόμηση (για ονόματα αρχείων με αριθμούς στην αρχή)\">φυσική ταξινόμηση',\n\t\"ct_utc\": 'εμφάνιση όλων των ημερομηνιών σε UTC\">UTC',\n\t\"ct_readme\": 'εμφάνιση README.md στις λίστες φακέλων\">📜 πληροφορίες',\n\t\"ct_idxh\": 'εμφάνιση index.html αντί για λίστα φακέλων\">html',\n\t\"ct_sbars\": 'εμφάνιση μπαρών κύλισης\">⟊',\n\n\t\"cut_umod\": \"αν το αρχείο υπάρχει ήδη στον server, ενημέρωσέ το με την τελευταία χρονοσφραγίδα τροποποίησης για να ταιριάζει με το τοπικό αρχείο (απαιτεί δικαιώματα εγγραφής+διαγραφής)\\\">re📅\",\n\n\t\"cut_turbo\": \"το κουμπί yolo, πιθανόν να ΜΗΝ ΘΕΛΕΙΣ να το ενεργοποιήσεις:$N$Nχρησιμοποίησέ το αν μεταφορτώνεις πολλά αρχεία και χρειάστηκε να ξαναρχίσεις, και θες να συνεχίσεις τη μεταφότρωση όσο το δυνατόν πιο γρήγορα$N$Nαντικαθιστά τον έλεγχο hash με απλό <em>&quot;έχει το ίδιο μέγεθος αρχείου στον server?&quot;</em> οπότε αν το περιεχόμενο είναι διαφορετικό, ΔΕΝ θα ανέβει$N$Nπρέπει να το κλείσεις όταν τελειώσει η μεταφόρτωση και μετά να &quot;μεταφορτώσεις&quot; πάλι τα ίδια αρχεία για να τα επιβεβαιώσει το τοπικό σου πρόγραμμα\\\">turbo\",\n\n\t\"cut_datechk\": \"δεν επηρεάζει τίποτα εκτός αν το turbo είναι ενεργοποιημένο$N$Nμειώνει λίγο τον παράγοντα yolo; ελέγχει αν οι χρονοσφραγίδες στο διακομιστή ταιριάζουν με τα δικά σου$N$Nπιάνει <em>θεωρητικά</em> τις περισσότερες μισοτελειωμένες/κατεστραμμένες μεταφορτώσεις, αλλά δεν αντικαθιστά τον έλεγχο με το turbo απενεργοποιημένο μετέπειτα\\\">έλεγχος ημερομηνίας\",\n\n\t\"cut_u2sz\": \"μέγεθος (σε MiB) κάθε κομματιού μεταφόρτωσης; μεγάλες τιμές λειτουργούν καλύτερα σε μεγαλύτερες αποστάσεις διακομιστή-πελάτη. Δοκίμασε μικρές τιμές σε πολύ άστατες συνδέσεις\",\n\n\t\"cut_flag\": \"εξασφαλίζει ότι μόνο μία καρτέλα μεταφορτώνει κάθε φορά $N -- οι άλλες καρτέλες πρέπει να το έχουν κι αυτές ενεργό $N -- επηρεάζει μόνο τις καρτέλες που βρίσκονται στο ίδιο διεύθυνση\",\n\n\t\"cut_az\": \"μεταφόρτωσε τα αρχεία αλφαβητικά, αντί για το μικρότερο αρχείο, πρώτα$N$Nη αλφαβητική σειρά βοηθά να καταλάβεις αν κάτι χάλασε στο διακομιστή αλλά κάνει το ανέβασμα λίγο πιο αργό σε fiber / LAN\",\n\n\t\"cut_nag\": \"ειδοποίηση λειτουργικού συστήματος όταν τελειώσει η μεταφόρτωση$N(μόνο αν ο browser ή η καρτέλα δεν είναι ενεργά)\",\n\t\"cut_sfx\": \"ηχητική ειδοποίηση όταν τελειώσει η μεταφόρτωση$N(μόνο αν ο browser ή η καρτέλα δεν είναι ενεργά)\",\n\n\t\"cut_mt\": \"χρησιμοποίησε multithreading για να επιταχύνεις το hashing των αρχείων$N$Nχρησιμοποιεί web-workers και χρειάζεται$Nπερισσότερη RAM (μέχρι 512 MiB επιπλέον)$N$Nκάνει το https 30% πιο γρήγορο, το http 4.5x πιο γρήγορο\\\">mt\",\n\n\t\"cut_wasm\": \"χρησιμοποίησε wasm αντί για τον ενσωματωμένο hasher του browser; βελτιώνει την ταχύτητα σε chrome-based browsers αλλά αυξάνει το φορτίο της CPU, και παλιές εκδόσεις chrome έχουν bugs που κάνουν το browser να τρώει όλη τη RAM και να κρασάρει αν ενεργοποιηθεί\\\">wasm\",\n\n\t\"cft_text\": \"κείμενο favicon (κενό και ανανέωση για απενεργοποίηση)\",\n\t\"cft_fg\": \"χρώμα προσκηνίου\",\n\t\"cft_bg\": \"χρώμα παρασκηνίου\",\n\n\t\"cdt_lim\": \"μέγιστος αριθμός αρχείων προς εμφάνιση σε ένα φάκελο\",\n\t\"cdt_ask\": \"όταν φτάνεις στο τέλος,$Nαντί να φορτώσει περισσότερα αρχεία,$Nρωτά τι να κάνει\",\n\t\"cdt_hsort\": \"`πόσους κανόνες ταξινόμησης (`,sorthref`) να συμπεριλάβει σε URLs πολυμέσων. Αν το βάλεις 0 αγνοεί και κανόνες ταξινόμησης στους συνδέσμους πολυμέσων\",\n\t\"cdt_ren\": \"ενεργοποίηση προσαρμοσμένου μενού δεξιού κλικ, το κανονικό μενού είναι προσβάσιμο με shift + δεξί κλικ\\\">ενεργοποίηση\", //m\n\t\"cdt_rdb\": \"εμφάνιση του κανονικού μενού δεξιού κλικ όταν το προσαρμοσμένο είναι ήδη ανοιχτό και γίνεται ξανά δεξί κλικ\\\">x2\", //m\n\n\t\"tt_entree\": \"εμφάνιση navpane (δέντρο διαδρομών)$NΠλήκτρο συντόμευσης: B\",\n\t\"tt_detree\": \"εμφάνιση breadcrumbs (καρτέλες διαδρομών)$NΠλήκτρο συντόμευσης: B\",\n\t\"tt_visdir\": \"κύλιση στον επιλεγμένο φάκελο\",\n\t\"tt_ftree\": \"εναλλαγή δέντρου διαδρομών / αρχείων κειμένου$NΠλήκτρο συντόμευσης: V\",\n\t\"tt_pdock\": \"εμφάνιση γονικών φακέλων σε σταθερή μπάρα επάνω\",\n\t\"tt_dynt\": \"αυτόματη επέκταση καθώς επεκτείνεται το δέντρο διαδρομών\",\n\t\"tt_wrap\": \"αναδίπλωση λέξεων\",\n\t\"tt_hover\": \"αποκάλυψη των γραμμών που ξεπερνούν το πλάτος με το ποντίκι πάνω τους$N( σπάει το scroll εκτός αν το ποντίκι $N&nbsp; είναι στην αριστερή στήλη )\",\n\n\t\"ml_pmode\": \"στο τέλος του φακέλου...\",\n\t\"ml_btns\": \"εντολές\",\n\t\"ml_tcode\": \"μετακωδικοποίηση\",\n\t\"ml_tcode2\": \"μετακωδικοποίηση σε\",\n\t\"ml_tint\": \"φίλτρο χρώματος\",\n\t\"ml_eq\": \"ισοσταθμιστής ήχου\",\n\t\"ml_drc\": \"συμπιεστής δυναμικής εμβέλειας\",\n\t\"ml_ss\": \"παράβλεψη σιωπής\", //m\n\n\t\"mt_loop\": \"επανάληψη ενός τραγουδιού\\\">🔁\",\n\t\"mt_one\": \"σταμάτα μετά από ένα τραγούδι\\\">1️⃣\",\n\t\"mt_shuf\": \"τυχαία σειρά τραγουδιών σε κάθε φάκελο\\\">🔀\",\n\t\"mt_aplay\": \"αυτόματη αναπαραγωγή αν υπάρχει song-ID στη διεύθυνση που μπήκες στο διακομιστή$N$Nη απενεργοποίηση αυτού, σταματά το URL από το να ενημερώνεται με τα song-ID ενώ παίζει η μουσική για να αποτραπεί η αυτόματη αναπαραγωγή αν χαθούν αυτές οι ρυθμίσεις αλλά το URL παραμείνει το ίδιο\\\">a▶\",\n\t\"mt_preload\": \"ξεκίνα  τη φόρτωση του επόμενου τραγουδιού κοντά στο τέλος για συνεχόμενη ακρόαση\\\">προφόρτωση\",\n\t\"mt_prescan\": \"πήγαινε στον επόμενο φάκελο πριν τελειώσει το τελευταίο τραγούδι$Nγια να μη σταματήσει το πρόγραμμα περιήγησης να παίζει μουσική\\\">nav\",\n\t\"mt_fullpre\": \"προσπάθησε να προφορτώσεις ολόκληρο το τραγούδι;$N✅ ενεργό σε <b>αναξιόπιστες</b> συνδέσεις,$N❌ πιθανότατα απενεργοποιημένο σε αργές συνδέσεις\\\">πλήρες\",\n\t\"mt_fau\": \"σε κινητά, πρόλαβε να μην σταματήσει η μουσική αν το επόμενο τραγούδι δεν προφορτώθηκε γρήγορα (μπορεί να προκαλέσει πρόβλημα στην εμφάνιση των ετικετών)\\\">☕️\",\n\t\"mt_waves\": \"γραμμή αναζήτησης κυματομορφής:$Nεμφάνιση έντασης ήχου στην μπάρα αναζήτησης\\\">~s\",\n\t\"mt_npclip\": \"εμφάνισε κουμπιά για αντιγραφή του τρέχοντος τραγουδιού\\\">/np\",\n\t\"mt_m3u_c\": \"εμφάνισε κουμπιά για αντιγραφή των$Nεπιλεγμένων τραγουδιών ως καταχωρήσεις λίστας m3u8\\\">📻\",\n\t\"mt_octl\": \"ενσωμάτωση στο λειτουργικό σύστημα (σηντομεύσεις πλήκτρων πολυμέσων / osd)\\\">έλεγχος-OS\",\n\t\"mt_oseek\": \"επιτρέπει την αναζήτηση μέσω ενσωμάτωσης του λειτουργικού συστήματος$N$Nσημείωση: σε μερικές συσκευές (iPhones),$Nαντικαθιστά το κουμπί επόμενου τραγουδιού\\\">αναζήτηση\",\n\t\"mt_oscv\": \"εμφάνιση εξωφύλλου άλμπουμ σε osd\\\">εξώφυλλο\",\n\t\"mt_follow\": \"κρατά το τρέχον κομμάτι ορατό κατά την κύλιση\\\">🎯\",\n\t\"mt_compact\": \"συμπαγή κουμπιά ελέγχου\\\">⟎\",\n\t\"mt_uncache\": \"καθάρισε την προσωρινή μνήμη &nbsp;(δοκίμασε αυτό αν ο browser έχει αποθηκεύσει$Nχαλασμένο αντίγραφο τραγουδιού και αρνείται να παίξει)\\\">εκκαθάριση\",\n\t\"mt_mloop\": \"τυχαία αναπαραγωγή στον ανοικτό φάκελο\\\">🔁 τυχαία αναπαραγωγή\",\n\t\"mt_mnext\": \"φόρτωση επόμενου φακέλου και συνέχιση\\\">📂 επόμενο\",\n\t\"mt_mstop\": \"σταμάτησε την αναπαραγωγή\\\">⏸ σταμάτημα\",\n\t\"mt_cflac\": \"μετατροπή flac / wav σε {0}\\\">flac\",\n\t\"mt_caac\": \"μετατροπή aac / m4a σε {0}\\\">aac\",\n\t\"mt_coth\": \"μετατροπή όλων των άλλων (εκτός των mp3) σε {0}\\\">άλλο\",\n\t\"mt_c2opus\": \"καλύτερη επιλογή για desktop, laptop, android\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, για iOS 17.5 και νεότερα\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, για iOS 11 έως 17\\\">caf\",\n\t\"mt_c2mp3\": \"χρησιμοποίησε αυτό σε πολύ παλιές συσκευές\\\">mp3\",\n\t\"mt_c2flac\": \"βέλτιστη ποιότητα ήχου αλλά τεράστιο αρχείο για μεταφόρτωση\\\">flac\",\n\t\"mt_c2wav\": \"ασυμπίεστη αναπαραγωγή (ακόμα μεγαλύτερο αρχείο)\\\">wav\",\n\t\"mt_c2ok\": \"μια χαρά, σοφή επιλογή\",\n\t\"mt_c2nd\": \"δεν είναι η προτεινόμενη μορφή εξόδου για τη συσκευή σου, αλλά αυτό είναι ok\",\n\t\"mt_c2ng\": \"η συσκευή σου φαίνεται να μην υποστηρίζει αυτήν τη μορφή εξόδου, αλλά ας το δοκιμάσουμε ούτως ή άλλως\",\n\t\"mt_xowa\": \"υπάρχουν bugs σε iOS που εμποδίζουν την αναπαραγωγή στο παρασκήνιο με αυτήν τη μορφή· χρησιμοποίησε caf ή mp3 αντ’ αυτού\",\n\t\"mt_tint\": \"επίπεδο φόντου (0-100) στην μπάρα αναζήτησης$Nγια να κάνεις το buffering λιγότερο ενοχλητικό\",\n\t\"mt_eq\": \"`ενεργοποιεί τον ισοσταθμιστή και τον έλεγχο ενίσχυσης;$N$Nενίσχυση `0` = στάνταρ 100% ένταση (απαράλλαχτη)$N$Nεύρος `1 &nbsp;` = στάνταρ στερεοφωνικό (απαράλλαχτο)$Nεύρος `0.5` = 50% αριστερά-δεξιά μίξη ήχου$Nεύρος `0 &nbsp;` = μονοφωνικό$N$Nενίσχυση `-0.8` &amp; εύρος `10` = αφαίρεση φωνής :^)$N$Nη ενεργοποίηση του ισοσταθμιστή κάνει τα άλμπουμ χωρίς κενά, να παίζουν χωρίς καθόλου κενά, οπότε άφησέ το ενεργό με όλες τις τιμές στο μηδέν (εκτός από εύρος = 1) αν σε νοιάζει\",\n\t\"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, το εξηγούν καλύτερα)\",\n\t\"mt_ss\": \"`ἐνεργοποιεῖ τὴν παράλειψιν σιγῆς· πολλαπλασιάζει τὴν ταχύτητα διὰ `ταχ` πλησίον ἀρχῆς/τέλους, ὅταν ἡ φωνὴ ὑπὸ `φων` καὶ ἡ θέσις ἐν τοῖς πρώτοις `ἀρχ`% ἢ τελευταίοις `τελ`%\", //m\n\t\"mt_ssvt\": \"ὅριον φωνῆς (0-255)\\\">φων\", //m\n\t\"mt_ssts\": \"ἐνεργὸν ὅριον (% τροχιᾶς, ἀρχή)\\\">ἀρχ\", //m\n\t\"mt_sste\": \"ἐνεργὸν ὅριον (% τροχιᾶς, τέλος)\\\">τελ\", //m\n\t\"mt_sssm\": \"πολλαπλασιαστὴς ταχύτητος\\\">ταχ\", //m\n\n\t\"mb_play\": \"παίξε\",\n\t\"mm_hashplay\": \"να παίξω αυτό το αρχείο ήχου;\",\n\t\"mm_m3u\": \"πάτα <code>Enter/Εντάξει</code> για Αναπαραγωγή\\nπάτα <code>ESC/Άκυρο</code> για Επεξεργασία\",\n\t\"mp_breq\": \"χρειάζεται firefox 82+ ή chrome 73+ ή iOS 15+\",\n\t\"mm_bload\": \"φορτώνει...\",\n\t\"mm_bconv\": \"μετατροπή σε {0}, περίμενε...\",\n\t\"mm_opusen\": \"ο browser σου δεν παίζει αρχεία aac / m4a;\\nη μετατροπή σε opus είναι τώρα ενεργή\",\n\t\"mm_playerr\": \"η αναπαραγωγή, απέτυχε: \",\n\t\"mm_eabrt\": \"Η προσπάθεια αναπαραγωγής ακυρώθηκε\",\n\t\"mm_enet\": \"Η σύνδεση του ίντερνέτ σου είναι χάλια\",\n\t\"mm_edec\": \"Το αρχείο αυτό είναι μάλλον κατεστραμμένο;;\",\n\t\"mm_esupp\": \"Ο browser σου δεν καταλαβαίνει αυτή τη μορφή ήχου\",\n\t\"mm_eunk\": \"Άγνωστο σφάλμα\",\n\t\"mm_e404\": \"Αδύνατη η αναπαραγωγή ήχου; σφάλμα 404: Το αρχείο δεν βρέθηκε.\",\n\t\"mm_e403\": \"Αδύνατη η αναπαραγωγή ήχου; σφάλμα 403: Άρνηση πρόσβασης.\\n\\nΔοκίμασε F5 για επαναφόρτωση, ίσως να έχεις αποσυνδεθεί\",\n\t\"mm_e415\": \"Αδύνατη η αναπαραγωγή ήχου; σφάλμα 415: Απέτυχε η μετατροπή αρχείου; έλεγξε τα logs του διακομιστή.\", //m\n\t\"mm_e500\": \"Αδύνατη η αναπαραγωγή ήχου; σφάλμα 500: Έλεγξε τα logs του διακομιστή.\",\n\t\"mm_e5xx\": \"Αδύνατη η αναπαραγωγή ήχου; σφάλμα διακομιστή\",\n\t\"mm_nof\": \"δεν βρέθηκαν άλλα αρχεία ήχου τριγύρω\",\n\t\"mm_prescan\": \"Αναζήτηση μουσικής για επόμενο τραγούδι...\",\n\t\"mm_scank\": \"Βρέθηκε το επόμενο τραγούδι:\",\n\t\"mm_uncache\": \"κρυφή μνήμη καθαρίστηκε· όλα τα τραγούδια θα ξανακατεβούν στην επόμενη αναπαραγωγή\",\n\t\"mm_hnf\": \"το τραγούδι αυτό πλέον δεν υπάρχει\",\n\n\t\"im_hnf\": \"η εικόνα αυτή πλέον δεν υπάρχει\",\n\n\t\"f_empty\": \"αυτός ο φάκελος είναι άδειος\",\n\t\"f_chide\": \"αυτό θα κρύψει τη στήλη «{0}»\\n\\nμπορείς να εμφανίσεις τις στήλες από τις ρυθμίσεις\",\n\t\"f_bigtxt\": \"αυτό το αρχείο είναι {0} MiB σε μέγεθος — σίγουρα θέλεις να το δεις ως κείμενο;\",\n\t\"f_bigtxt2\": \"να δεις μόνο το τέλος του αρχείου αντί για όλο; αυτό ενεργοποιεί και το following/tailing, που δείχνει νέες γραμμές που προστίθενται ζωντανά\",\n\t\"fbd_more\": '<div id=\"blazy\">εμφανίζονται <code>{0}</code> από <code>{1}</code> αρχεία; <a href=\"#\" id=\"bd_more\">δείξε {2}</a> ή <a href=\"#\" id=\"bd_all\">δείξε τα όλα</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\">εμφανίζονται <code>{0}</code> από <code>{1}</code> αρχεία; <a href=\"#\" id=\"bd_all\">δείξε όλα</a></div>',\n\t\"f_anota\": \"μόνο {0} από τα {1} αντικείμενα επιλέχθηκαν;\\nγια να επιλέξεις ολόκληρο το φάκελο, κύλησε πρώτα μέχρι κάτω\",\n\n\t\"f_dls\": 'οι σύνδεσμοι αρχείων στον τρέχοντα φάκελο έχουν\\nμετατραπεί σε συνδέσμους λήψης',\n\t\"f_dl_nd\": 'παράλειψη φακέλου (χρησιμοποιήστε λήψη zip/tar αντί γι’ αυτό):\\n', //m\n\n\t\"f_partial\": \"Για να κατεβάσεις με ασφάλεια ένα αρχείο που ανεβαίνει, κλίκαρε το αρχείο με το ίδιο όνομα, αλλά χωρίς την κατάληξη <code>.PARTIAL</code>. Πάτα Άκυρο ή Escape για να σταματήσεις.\\n\\nΠάτα Εντάξει / Enter αν αγνοείς την προειδοποίηση και κατέβασε το <code>.PARTIAL</code> αρχείο, που σχεδόν σίγουρα θα είναι κατεστραμμένο.\",\n\n\t\"ft_paste\": \"επικόλλησε {0} αντικείμενα$NΠλήκτρο συντόμευσης: ctrl-V\",\n\t\"fr_eperm\": 'δεν μπορεί να μετονομαστεί:\\nδεν έχεις δικαίωμα “μετακίνησης” σε αυτόν το φάκελο',\n\t\"fd_eperm\": 'δεν μπορεί να διαγραφεί:\\nδεν έχεις δικαίωμα “διαγραφής” σε αυτόν το φάκελο',\n\t\"fc_eperm\": 'δεν μπορεί να κοπεί:\\nδεν έχεις δικαίωμα “μετακίνησης” σε αυτόν το φάκελο',\n\t\"fp_eperm\": 'δεν μπορεί να επικολληθεί:\\nδεν έχεις δικαίωμα “εγγραφής” σε αυτόν το φάκελο',\n\t\"fr_emore\": \"επίλεξε τουλάχιστον ένα αντικείμενο για μετονομασία\",\n\t\"fd_emore\": \"επίλεξε τουλάχιστον ένα αντικείμενο για διαγραφή\",\n\t\"fc_emore\": \"επίλεξε τουλάχιστον ένα αντικείμενο για αποκοπή\",\n\t\"fcp_emore\": \"επίλεξε τουλάχιστον ένα αντικείμενο για αντιγραφή στο πρόχειρο\",\n\n\t\"fs_sc\": \"μοιράσου το φάκελο που βρίσκεσαι\",\n\t\"fs_ss\": \"μοιράσου τα επιλεγμένα αρχεία\",\n\t\"fs_just1d\": \"δεν μπορείς να επιλέξεις περισσότερους από έναν φακέλους,\\nή να αναμείξεις αρχεία και φακέλους στην ίδια επιλογή\",\n\t\"fs_abrt\": \"❌ ακύρωση\",\n\t\"fs_rand\": \"🎲 τυχαίο όνομα\",\n\t\"fs_go\": \"✅ δημιούργησε κοινή χρήση\",\n\t\"fs_name\": \"όνομα\",\n\t\"fs_src\": \"πηγή\",\n\t\"fs_pwd\": \"κωδικός\",\n\t\"fs_exp\": \"λήξη\",\n\t\"fs_tmin\": \"λεπτά\",\n\t\"fs_thrs\": \"ώρες\",\n\t\"fs_tdays\": \"ημέρες\",\n\t\"fs_never\": \"αιώνιο\",\n\t\"fs_pname\": \"προαιρετικό όνομα συνδέσμου; αν είναι κενό, θα είναι τυχαίο\",\n\t\"fs_tsrc\": \"το αρχείο ή ο φάκελος προς κοινή χρήση\",\n\t\"fs_ppwd\": \"προαιρετικός κωδικός\",\n\t\"fs_w8\": \"δημιουργία κοινής χρήσης...\",\n\t\"fs_ok\": \"πάτα <code>Enter/Εντάξει</code> για Πρόχειρο\\nπάτα <code>ESC/Άκυρο</code> για Κλείσιμο\",\n\n\t\"frt_dec\": \"μπορεί να διορθώσει μερικές περιπτώσεις κατεστραμμένων ονομάτων αρχείων\\\">αποκωδικοποίηση url\",\n\t\"frt_rst\": \"επανέφερε τα ονόματα αρχείων στα αρχικά τους\\\">↺ επαναφορά\",\n\t\"frt_abrt\": \"ακύρωσε και κλείσε αυτό το παράθυρο\\\">❌ ακύρωση\",\n\t\"frb_apply\": \"ΕΦΑΡΜΟΓΗ ΜΕΤΟΝΟΜΑΣΙΑΣ\",\n\t\"fr_adv\": \"μαζική / μεταδεδομένα / μετονομασία με πρότυπα\\\">προχωρημένη\",\n\t\"fr_case\": \"regex με διάκριση πεζών/κεφαλαίων\\\">case\",\n\t\"fr_win\": \"ασφαλή ονόματα για windows; αντικαθιστά <code>&lt;&gt;:&quot;\\\\|?*</code> με ιαπωνικούς χαρακτήρες πλήρους πλάτους\\\">win\",\n\t\"fr_slash\": \"αντικαθίσταται <code>/</code> με χαρακτήρα που δεν δημιουργεί νέους φακέλους\\\">όχι /\",\n\t\"fr_re\": \"`μοτίβα αναζήτησης (regex) για αναζήτηση στα αρχικά ονόματα; τα καταγραφόμενα groups μπορούν να χρησιμοποιηθούν στο πεδίο μορφοποίησης παρακάτω όπως `(1)` και `(2)` και ούτω καθεξής\",\n\t\"fr_fmt\": \"`εμπνευσμένο από foobar2000:$N`(title)` αντικαθίσταται από τίτλο τραγουδιού,$N`[(artist) - ](title)` παραλείπει το [this] αν το artist είναι κενό$N`$lpad((tn),2,0)` γεμίζει τον αριθμό κομματιού σε 2 ψηφία\",\n\t\"fr_pdel\": \"διαγραφή\",\n\t\"fr_pnew\": \"αποθήκευση ως\",\n\t\"fr_pname\": \"δώσε όνομα για τη νέα προεπιλογή\",\n\t\"fr_aborted\": \"ακυρώθηκε\",\n\t\"fr_lold\": \"παλιό όνομα\",\n\t\"fr_lnew\": \"νέο όνομα\",\n\t\"fr_tags\": \"ετικέτες για τα επιλεγμένα αρχεία (μόνο για ανάγνωση):\",\n\t\"fr_busy\": \"μετονομασία {0} αντικειμένων...\\n\\n{1}\",\n\t\"fr_efail\": \"αποτυχία μετονομασίας:\\n\",\n\t\"fr_nchg\": \"{0} από τα νέα ονόματα άλλαξαν λόγω <code>win</code> και/ή <code>όχι /</code>\\n\\nΕίναι ΟΚ να συνεχίσουμε με αυτά τα ονόματα;\",\n\n\t\"fd_ok\": \"διαγραφή OK\",\n\t\"fd_err\": \"αποτυχία διαγραφής:\\n\",\n\t\"fd_none\": \"δεν διαγράφηκε τίποτα; ίσως μπλοκαρισμένο από τις ρυθμίσεις του διακομιστή (xbd);\",\n\t\"fd_busy\": \"διαγραφή {0} αντικειμένων...\\n\\n{1}\",\n\t\"fd_warn1\": \"ΔΙΑΓΡΑΦΗ αυτών των {0} αντικειμένων;\",\n\t\"fd_warn2\": \"<b>Τελευταία ευκαιρία!</b> Δεν υπάρχει αναίρεση. Διαγραφή;\",\n\n\t\"fc_ok\": \"αποκοπή {0} αντικειμένων\",\n\t\"fc_warn\": 'αποκοπή {0} αντικειμένων\\n\\nαλλά: μόνο <b>αυτή</b> η καρτέλα browser μπορεί να τα επικολλήσει\\n(λόγω πολύ μεγάλης επιλογής)',\n\n\t\"fcc_ok\": \"αντιγράφηκαν {0} αντικείμενα στο πρόχειρο\",\n\t\"fcc_warn\": \"αντιγράφηκαν {0} αντικείμενα στο πρόχειρο\\n\\nαλλά: μόνο <b>αυτή</b> η καρτέλα browser μπορεί να τα επικολλήσει\\n(λόγω πολύ μεγάλης επιλογής)\",\n\n\t\"fp_apply\": \"χρησιμοποίησε αυτά τα ονόματα\",\n\t\"fp_skip\": \"αγνόησε συγκρούσεις\", //m\n\t\"fp_ecut\": \"πρώτα κάνε αποκοπή ή αντιγραφή κάποιων αρχείων / φακέλων για επικόλληση / μετακίνηση\\n\\nσημείωση: μπορείς να αποκόπτεις / επικολλάς ανάμεσα σε διαφορετικές καρτέλες browser\",\n\t\"fp_ename\": \"τα {0} αντικείμενα δεν μπορούν να μετακινηθούν εδώ γιατί τα ονόματα υπάρχουν ήδη. Δώσε νέα ονόματα παρακάτω για να συνεχίσεις, ή άφησε κενό (\\\"αγνόησε συγκρούσεις\\\") για να τα αγνοήσεις:\", //m\n\t\"fcp_ename\": \"τα {0} αντικείμενα δεν μπορούν να αντιγραφούν εδώ γιατί τα ονόματα υπάρχουν ήδη. Δώσε νέα ονόματα παρακάτω για να συνεχίσεις, ή άφησε κενό (\\\"αγνόησε συγκρούσεις\\\") για να τα αγνοήσεις:\", //m\n\t\"fp_emore\": \"υπάρχουν ακόμα συγκρούσεις ονομάτων που πρέπει να διορθωθούν\",\n\t\"fp_ok\": \"μετακίνηση OK\",\n\t\"fcp_ok\": \"αντιγραφή OK\",\n\t\"fp_busy\": \"μετακίνηση {0} αντικειμένων...\\n\\n{1}\",\n\t\"fcp_busy\": \"αντιγραφή {0} αντικειμένων...\\n\\n{1}\",\n\t\"fp_abrt\": \"γίνεται ακύρωση...\", //m\n\t\"fp_err\": \"αποτυχία μετακίνησης:\\n\",\n\t\"fcp_err\": \"αποτυχία αντιγραφής:\\n\",\n\t\"fp_confirm\": \"να μετακινηθούν αυτά τα {0} αντικείμενα εδώ;\",\n\t\"fcp_confirm\": \"να αντιγραφούν αυτά τα {0} αντικείμενα εδώ;\",\n\t\"fp_etab\": \"αποτυχία ανάγνωσης πρόχειρου από άλλη καρτέλα browser\",\n\t\"fp_name\": \"μεταφόρτωση αρχείου από τη συσκευή σου. Δώσε του όνομα:\",\n\t\"fp_both_m\": '<h6>διάλεξε τι θα επικολλήσεις</h6><code>Enter</code> = Μετακίνηση {0} αρχείων από «{1}»\\n<code>ESC</code> = Μεταφόρτωση {2} αρχείων από τη συσκευή σου',\n\t\"fcp_both_m\": '<h6>διάλεξε τι θα επικολλήσεις</h6><code>Enter</code> = Αντιγραφή {0} αρχείων από «{1}»\\n<code>ESC</code> = Μεταφόρτωση {2} αρχείων από τη συσκευή σου',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Μετακίνηση</a><a href=\"#\" id=\"modal-ng\">Μεταφόρτωση</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Αντιγραφή</a><a href=\"#\" id=\"modal-ng\">Μεταφόρτωση</a>',\n\n\t\"mk_noname\": \"γράψε ένα όνομα στο πεδίο κειμένου αριστερά πριν το κάνεις :p\",\n\t\"nmd_i1\": \"μπορείτε επίσης να προσθέσετε την κατάληξη που θέλετε, όπως <code>.md</code>\", //m\n\t\"nmd_i2\": \"μπορείτε να δημιουργήσετε μόνο αρχεία <code>.{0}</code> επειδή δεν έχετε δικαίωμα διαγραφής\", //m\n\n\t\"tv_load\": \"Φόρτωση αρχείου κειμένου:\\n\\n{0}\\n\\n{1}% ({2} από {3} MiB φορτωμένα)\",\n\t\"tv_xe1\": \"αδυναμία φόρτωσης αρχείου κειμένου:\\n\\nσφάλμα \",\n\t\"tv_xe2\": \"404, αρχείο δεν βρέθηκε\",\n\t\"tv_lst\": \"λίστα αρχείων κειμένου σε\",\n\t\"tvt_close\": \"επιστροφή στην προβολή φακέλου$NΣυντόμευση: M (ή Esc)\\\">❌ κλείσιμο\",\n\t\"tvt_dl\": \"κατέβασε αυτό το αρχείο$NΣυντόμευση: Y\\\">💾 λήψη\",\n\t\"tvt_prev\": \"προβολή προηγούμενου εγγράφου$NΣυντόμευση: i\\\">⬆ προηγούμενο\",\n\t\"tvt_next\": \"προβολή επόμενου εγγράφου$NΣυντόμευση: K\\\">⬇ επόμενο\",\n\t\"tvt_sel\": \"επέλεξε αρχείο &nbsp; (για αποκοπή / αντιγραφή / διαγραφή / ...)$NΣυντόμευση: S\\\">επιλογή\",\n\t\"tvt_j\": \"ομορφοποίηση json$NΣυντόμευση: shift-J\\\">j\", //m\n\t\"tvt_edit\": \"άνοιγμα αρχείου στον επεξεργαστή κειμένου$NΣυντόμευση: E\\\">✏️ επεξεργασία\",\n\t\"tvt_tail\": \"παρακολούθηση αρχείου για αλλαγές; εμφάνιση νέων γραμμών σε πραγματικό χρόνο\\\">📡 παρακολούθηση\",\n\t\"tvt_wrap\": \"αναδίπλωση λέξεων\\\">↵\",\n\t\"tvt_atail\": \"κλείδωμα κύλισης στο κάτω μέρος\\\">⚓\",\n\t\"tvt_ctail\": \"αποκωδικοποίηση χρωμάτων τερματικού (ansi escape codes)\\\">🌈\",\n\t\"tvt_ntail\": \"όριο κύλισης (πόσα bytes κειμένου να κρατούνται φορτωμένα)\",\n\n\t\"m3u_add1\": \"το τραγούδι προστέθηκε στη λίστα m3u\",\n\t\"m3u_addn\": \"προστέθηκαν {0} τραγούδια στη λίστα m3u\",\n\t\"m3u_clip\": \"η λίστα m3u αντιγράφηκε στο πρόχειρο\\n\\nπρέπει να φτιάξεις ένα νέο αρχείο κειμένου με όνομα η_λίστα_μου.m3u και να επικολλήσεις τη λίστα μέσα· αυτό θα το καταστήσει αναπαράξιμο\",\n\n\t\"gt_vau\": \"μην δείχνεις το βίντεο, παίξε μόνο τον ήχο\\\">🎧\",\n\t\"gt_msel\": \"ενεργοποίηση επιλογής αρχείων; ctrl-κλικ σε αρχείο για παράκαμψη$N$N&lt;em&gt;όταν είναι ενεργό: διπλό κλικ σε αρχείο / φάκελο το ανοίγει&lt;/em&gt;$N$NΣυντόμευση: S\\\">πολλαπλή επιλογή\",\n\t\"gt_crop\": \"κεντραρισμένη περικοπή μικρογραφιών\\\">περικοπή\",\n\t\"gt_3x\": \"μικρογραφίες υψηλής ανάλυσης\\\">3x\",\n\t\"gt_zoom\": \"ζουμ\",\n\t\"gt_chop\": \"κόψε\",\n\t\"gt_sort\": \"ταξινόμηση κατά\",\n\t\"gt_name\": \"όνομα\",\n\t\"gt_sz\": \"μέγεθος\",\n\t\"gt_ts\": \"ημερομηνία\",\n\t\"gt_ext\": \"τύπος\",\n\t\"gt_c1\": \"μεγαλύτερη περικοπή ονομάτων αρχείων (δείξε λιγότερα)\",\n\t\"gt_c2\": \"μικρότερη περικοπή ονομάτων αρχείων (δείξε περισσότερα)\",\n\n\t\"sm_w8\": \"αναζήτηση...\",\n\t\"sm_prev\": \"τα παρακάτω αποτελέσματα αναζήτησης προέρχονται από προηγούμενη αναζήτηση:\\n  \",\n\t\"sl_close\": \"κλείσιμο αποτελεσμάτων αναζήτησης\",\n\t\"sl_hits\": \"εμφανίζονται {0} αποτελέσματα\",\n\t\"sl_moar\": \"φόρτωσε περισσότερα\",\n\n\t\"s_sz\": \"μέγεθος\",\n\t\"s_dt\": \"ημερομηνία\",\n\t\"s_rd\": \"μονοπάτι\",\n\t\"s_fn\": \"όνομα\",\n\t\"s_ta\": \"ετικέτες\",\n\t\"s_ua\": \"ανέβηκε@\",\n\t\"s_ad\": \"προχωρ.\",\n\t\"s_s1\": \"ελάχιστο σε MiB\",\n\t\"s_s2\": \"μέγιστο σε MiB\",\n\t\"s_d1\": \"ελάχιστο iso8601\",\n\t\"s_d2\": \"μέγιστο iso8601\",\n\t\"s_u1\": \"μεταφορτώθηκε αργότερα\",\n\t\"s_u2\": \"και/ή πριν\",\n\t\"s_r1\": \"το μονοπάτι περιέχει &nbsp; (χωρισμένα με κενό)\",\n\t\"s_f1\": \"το όνομα περιέχει &nbsp; (άρνηση με -nope)\",\n\t\"s_t1\": \"οι ετικέτες περιέχουν &nbsp; (^=αρχή, τέλος=$)\",\n\t\"s_a1\": \"συγκεκριμένες ιδιότητες μεταδεδομένων\",\n\n\t\"md_eshow\": \"δεν μπορεί να εμφανιστεί \",\n\t\"md_off\": \"[📜<em>readme</em>] απενεργοποιημένο στο [⚙️] -- κρυμμένο έγγραφο\",\n\n\t\"badreply\": \"Αποτυχία ανάλυσης απάντησης από το διακομιστή\",\n\n\t\"xhr403\": \"403: Πρόσβαση αρνήθηκε\\n\\nδοκίμασε το F5, ίσως αποσυνδέθηκες\",\n\t\"xhr0\": \"άγνωστο (πιθανόν αποσύνδεση από το διακομιστή ή ο διακομιστής είναι εκτός σύνδεσης)\",\n\t\"cf_ok\": \"συγγνώμη γι' αυτό -- η προστασία DD\" + wah + \"oS ενεργοποιήθηκε\\n\\nοι διαδικασίες θα συνεχιστούν σε περίπου 30 δευτερόλεπτα\\n\\nαν δεν γίνει τίποτα, πάτα F5 για επαναφόρτωση\",\n\t\"tl_xe1\": \"αδύνατη η λίστα υποφακέλων:\\n\\nσφάλμα \",\n\t\"tl_xe2\": \"404: Ο φάκελος δεν βρέθηκε\",\n\t\"fl_xe1\": \"αδύνατη η λίστα αρχείων σε φάκελο:\\n\\nσφάλμα \",\n\t\"fl_xe2\": \"404: Ο φάκελος δεν βρέθηκε\",\n\t\"fd_xe1\": \"αδύνατη η δημιουργία υποφακέλου:\\n\\nσφάλμα \",\n\t\"fd_xe2\": \"404: Ο γονικός φάκελος δεν βρέθηκε\",\n\t\"fsm_xe1\": \"αδύνατη η αποστολή μηνύματος:\\n\\nσφάλμα \",\n\t\"fsm_xe2\": \"404: Ο γονικός φάκελος δεν βρέθηκε\",\n\t\"fu_xe1\": \"αποτυχία φόρτωσης λίστας unpost από το διακομιστή:\\n\\nσφάλμα \",\n\t\"fu_xe2\": \"404: Το αρχείο δεν βρέθηκε??\",\n\n\t\"fz_tar\": \"μη συμπιεσμένο αρχείο gnu-tar (linux / mac)\",\n\t\"fz_pax\": \"μη συμπιεσμένο pax-format tar (πιο αργό)\",\n\t\"fz_targz\": \"gnu-tar με συμπίεση gzip επίπεδο 3$N$Nσυνήθως πολύ αργό, οπότε$Nχρησιμοποίησε καλύτερα μη συμπιεσμένο tar\",\n\t\"fz_tarxz\": \"gnu-tar με συμπίεση xz επίπεδο 1$N$Nσυνήθως πολύ αργό, οπότε$Nχρησιμοποίησε καλύτερα μη συμπιεσμένο tar\",\n\t\"fz_zip8\": \"zip με ονόματα αρχείων utf8 (ίσως να κολλάει σε windows 7 και παλιότερα)\",\n\t\"fz_zipd\": \"zip με παραδοσιακά ονόματα cp437, για πολύ παλιό λογισμικό\",\n\t\"fz_zipc\": \"cp437 με crc32 υπολογισμένο νωρίτερα,$Nγια MS-DOS PKZIP v2.04g (οκτώβριος 1993)$N(παίρνει παραπάνω χρόνο πριν ξεκινήσει η μεταφόρτωση)\",\n\n\t\"un_m1\": \"μπορείς να διαγράψεις τα πρόσφατα αρχεία που μεταφόρτωσες (ή να ακυρώσεις τα μισοτελειωμένα) παρακάτω\",\n\t\"un_upd\": \"ανανέωση\",\n\t\"un_m4\": \"ή μοιράσου τα αρχεία που βλέπεις παρακάτω:\",\n\t\"un_ulist\": \"εμφάνιση\",\n\t\"un_ucopy\": \"αντιγραφή\",\n\t\"un_flt\": \"προαιρετικό φίλτρο:&nbsp; η διεύθυνση πρέπει να περιέχει\",\n\t\"un_fclr\": \"καθαρισμός φίλτρου\",\n\t\"un_derr\": \"αποτυχία διαγραφής unpost:\\n\",\n\t\"un_f5\": \"κάτι χάλασε, δοκίμασε την ανανέωση ή πάτα F5\",\n\t\"un_uf5\": \"συγγνώμη αλλά πρέπει να ανανεώσεις τη σελίδα (πχ με F5 ή CTRL-R) πριν ακυρώσεις αυτήν την αποστολή\",\n\t\"un_nou\": \"<b>προσοχή:</b> ο διακομιστής είναι πολύ φορτωμένος για να δείξει μισοτελειωμένες αποστολές· πάτα την ανανέωση, σε λίγο\",\n\t\"un_noc\": \"<b>προσοχή:</b> το unpost των ολοκληρωμένων αρχείων δεν επιτρέπεται από τη ρύθμιση του διακομιστή\",\n\t\"un_max\": \"εμφανίζονται τα πρώτα 2000 αρχεία (χρησιμοποίησε φίλτρο)\",\n\t\"un_avail\": \"μπορείς να διαγράψεις {0} πρόσφατα αρχεία<br />μπορείς να ακυρώσεις {1} μισοτελειωμένες αποστολές\",\n\t\"un_m2\": \"ταξινομημένα κατά χρόνο μεταφόρτωσης; τα πιο πρόσφατα πρώτα:\",\n\t\"un_no1\": \"άκυρο! καμία μεταφόρτωση δεν είναι αρκετά πρόσφατη\",\n\t\"un_no2\": \"άκυρο! καμία μεταφόρτωση με αυτό το φίλτρο δεν είναι αρκετά πρόσφατη\",\n\t\"un_next\": \"διάγραψε τα επόμενα {0} αρχεία παρακάτω\",\n\t\"un_abrt\": \"άκυρο\",\n\t\"un_del\": \"διαγραφή\",\n\t\"un_m3\": \"φορτώνω τις πρόσφατες μεταφορτώσεις σου...\",\n\t\"un_busy\": \"διαγράφω {0} αρχεία...\",\n\t\"un_clip\": \"αντιγράφηκαν {0} σύνδεσμοι στο πρόχειρο\",\n\n\t\"u_https1\": \"πρέπει\",\n\t\"u_https2\": \"μετάβαση σε https\",\n\t\"u_https3\": \"για καλύτερη απόδοση\",\n\t\"u_ancient\": 'ο browser σου είναι εντυπωσιακά απαρχαιωμένος — ίσως να <a href=\"#\" onclick=\"goto(\\'bup\\')\">χρησιμοποιήσεις το bup αντί γι\\' αυτό</a>',\n\t\"u_nowork\": \"χρειάζεται firefox 53+ ή chrome 57+ ή iOS 11+\",\n\t\"tail_2old\": \"χρειάζεται firefox 105+ ή chrome 71+ ή iOS 14.5+\",\n\t\"u_nodrop\": \"ο browser σου είναι πολύ παλιός για drag&amp;drop μεταφορτώσεις\",\n\t\"u_notdir\": \"αυτός δεν είναι φάκελος!\\n\\nο browser σου είναι πολύ παλιός,\\nδοκίμασε drag&amp;drop αντ' αυτού\",\n\t\"u_uri\": \"για να κάνεις drag&amp;drop εικόνων από άλλα παράθυρα browser,\\nρίξ' τες πάνω στο μεγάλο κουμπί μεταφόρτωσης\",\n\t\"u_enpot\": 'άλλαξε στο <a href=\"#\">potato UI</a> (ίσως ανεβάζει πιο γρήγορα)',\n\t\"u_depot\": 'άλλαξε στο <a href=\"#\">fancy UI</a> (ίσως ανεβάζει πιο αργά)',\n\t\"u_gotpot\": \"αλλάζω στο potato UI για πιο γρήγορη μεταφόρτωση,\\n\\nμπορείς να το αλλάξεις πάλι αν θες!\",\n\t\"u_pott\": \"<p>αρχεία: &nbsp; <b>{0}</b> ολοκληρωμένα, &nbsp; <b>{1}</b> αποτυχημένα, &nbsp; <b>{2}</b> σε εξέλιξη, &nbsp; <b>{3}</b> σε ουρά</p>\",\n\t\"u_ever\": \"αυτός είναι ο βασικός uploader; το up2k θέλει τουλάχιστον<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1\",\n\t\"u_su2k\": 'αυτός είναι ο βασικός uploader; το <a href=\"#\" id=\"u2yea\">up2k</a> είναι καλύτερο',\n\t\"u_uput\": \"βελτιστοποίηση για ταχύτητα (παράλειψη ελέγχου ακεραιότητας)\",\n\t\"u_ewrite\": \"δεν έχεις δικαίωμα εγγραφής σε αυτόν τον φάκελο\",\n\t\"u_eread\": \"δεν έχεις δικαίωμα ανάγνωσης σε αυτόν τον φάκελο\",\n\t\"u_enoi\": \"η αναζήτηση αρχείων δεν είναι ενεργοποιημένη στο αρχείο ρυθμίσεων του διακομιστή\",\n\t\"u_enoow\": \"δεν μπορείς να κάνεις αντικατάσταση εδώ· χρειάζεται δικαίωμα Διαγραφής\",\n\t\"u_badf\": \"Αυτά τα {0} αρχεία (από {1} συνολικά) παραλείφθηκαν, πιθανώς λόγω δικαιωμάτων συστήματος αρχείων:\\n\\n\",\n\t\"u_blankf\": \"Αυτά τα {0} αρχεία (από {1} συνολικά) είναι άδεια / κενά· να τα μεταφορτώσω έτσι κι αλλιώς;\\n\\n\",\n\t\"u_applef\": \"Αυτά τα {0} αρχεία (από {1} συνολικά) πιθανώς δεν είναι επιθυμητά;\\nΠάτα <code>Εντάξει/Enter</code> για ΝΑ ΑΓΝΟΗΘΟΥΝ τα παρακάτω αρχεία,\\nΠάτα <code>Άκυρο/ESC</code> για ΝΑ ΜΗΝ ΑΠΟΚΛΕΙΣΤΟΥΝ και να ΜΕΤΑΦΟΡΤΩΘΟΎΝ κι αυτά:\\n\\n\",\n\t\"u_just1\": \"\\nΊσως δουλέψει καλύτερα αν επιλέξεις μόνο ένα αρχείο\",\n\t\"u_ff_many\": \"αν χρησιμοποιείς <b>Linux / MacOS / Android,</b> τότε τόσα αρχεία <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\"><em>μπορεί</em> να κατάρρευση του Firefox!</a>\\nαν γίνει αυτό, δοκίμασε ξανά (ή χρησιμοποίησε τον Chrome).\",\n\t\"u_up_life\": \"Αυτή η μεταφόρτωση θα διαγραφεί από το διακομιστή\\n{0} μετά την ολοκλήρωσή της\",\n\t\"u_asku\": \"μεταφόρτωση αυτών των {0} αρχείων στο <code>{1}</code>\",\n\t\"u_unpt\": \"μπορείς να αναιρέσεις / διαγράψεις αυτήν τη μεταφόρτωση χρησιμοποιώντας το 🧯, πάνω αριστερά\",\n\t\"u_bigtab\": \"θα εμφανιστούν {0} αρχεία\\n\\nαυτό μπορεί να κάνει τον browser σου να κολλήσει, είσαι σίγουρος;\",\n\t\"u_scan\": \"Σάρωση αρχείων...\",\n\t\"u_dirstuck\": \"ο επεξεργαστής φακέλων κόλλησε προσπαθώντας να προσπελάσει τα εξής {0} αντικείμενα· θα τα παραλείψει:\",\n\t\"u_etadone\": \"Ολοκληρώθηκε ({0}, {1} αρχεία)\",\n\t\"u_etaprep\": \"(προετοιμασία για μεταφόρτωση)\",\n\t\"u_hashdone\": \"το hashing ολοκληρώθηκε\",\n\t\"u_hashing\": \"υπολογισμός hash\",\n\t\"u_hs\": \"handshaking...\",\n\t\"u_started\": \"τα αρχεία ανεβαίνουν τώρα· δες τα στο [🚀]\",\n\t\"u_dupdefer\": \"διπλότυπο; θα επεξεργαστεί μετά από όλα τα άλλα αρχεία\",\n\t\"u_actx\": \"πάτα αυτό το κείμενο για να μην χάσεις<br />απόδοση όταν αλλάζεις παράθυρα/καρτέλες\",\n\t\"u_fixed\": \"ΟΚ!&nbsp; Το διόρθωσα 👍\",\n\t\"u_cuerr\": \"αποτυχία μεταφόρτωσης τμήματοςς {0} από {1};\\nπιθανώς ακίνδυνο, συνεχίζω\\n\\nαρχείο: {2}\",\n\t\"u_cuerr2\": \"ο διακομιστής απέρριψε τη μεταφόρτωση (τμήμα {0} από {1});\\nθα ξαναδοκιμάσει αργότερα\\n\\nαρχείο: {2}\\n\\nσφάλμα \",\n\t\"u_ehstmp\": \"θα ξαναδοκιμάσει; δες κάτω δεξιά\",\n\t\"u_ehsfin\": \"ο διακομιστής απέρριψε το αίτημα ολοκλήρωσης της μεταφόρτωσης; ξαναδοκιμάζει...\",\n\t\"u_ehssrch\": \"ο διακομιστής απέρριψε το αίτημα αναζήτησης; ξαναδοκιμάζει...\",\n\t\"u_ehsinit\": \"ο διακομιστής απέρριψε το αίτημα για εκκίνηση μεταφόρτωσης; ξαναδοκιμάζει...\",\n\t\"u_eneths\": \"σφάλμα δικτύου κατά το handshake μεταφόρτωσης; ξαναδοκιμάζει...\",\n\t\"u_enethd\": \"σφάλμα δικτύου κατά τον έλεγχο ύπαρξης στόχου; ξαναδοκιμάζει...\",\n\t\"u_cbusy\": \"ο διακομιστής περιμένει να μας εμπιστευτεί ξανά μετά από πρόβλημα δικτύου...\",\n\t\"u_ehsdf\": \"ο διακομιστής έμεινε από χώρο στο δίσκο!\\n\\nθα συνεχίσει να ξαναδοκιμάζει,\\nσε περίπτωση που κάποιος\\nελευθερώσει αρκετό χώρο για συνέχεια\",\n\t\"u_emtleak1\": \"φαίνεται πως ο browser σου έχει διαρροή μνήμης;\\nπαρακαλώ\",\n\t\"u_emtleak2\": ' <a href=\"{0}\">αλλαγή σε https (συνιστάται)</a> ή ',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": 'δοκίμασε τα εξής:\\n<ul><li>πάτα <code>F5</code> για ανανέωση σελίδας</li><li>μετά απενεργοποίησε το &nbsp;<code>mt</code>&nbsp; κουμπί στις &nbsp;<code>⚙️ ρυθμίσεις</code></li><li>και δοκίμασε ξανά τη μεταφόρτωση</li></ul>Οι μεταφορτώσιες θα είναι λίγο πιο αργές, αλλά ok.\\nΣυγγνώμη για την ταλαιπωρία!\\n\\nPS: το chrome v107 <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">έχει διόρθωση γι\\' αυτό</a>',\n\t\"u_emtleakf\": 'δοκίμασε τα εξής:\\n<ul><li>πάτα <code>F5</code> για ανανέωση σελίδας</li><li>μετά άνοιξε το <code>🥔</code> (potato) στο UI μεταφόρτωσης<li>και δοκίμασε ξανά τη μεταφόρτωση</li></ul>\\nPS: ο firefox <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">ελπίζει να φτιάξει αυτό το bug</a> κάποια στιγμή',\n\t\"u_s404\": \"δεν βρέθηκε στο διακομιστή\",\n\t\"u_expl\": \"επεξήγηση\",\n\t\"u_maxconn\": \"οι περισσότεροι browser το περιορίζουν στα 6, αλλά ο firefox σου επιτρέπει να το αυξήσεις με <code>connections-per-server</code> στο <code>about:config</code>\",\n\t\"u_tu\": '<p class=\"warn\">ΠΡΟΕΙΔΟΠΟΙΗΣΗ: το turbo είναι ενεργοποιημένο, <span>&nbsp;το πρόγραμμα πελάτη ίσως να μην ανιχνεύσει και να μην ξαναεκκινήσει μισοτελειωμένες μεταφορτώσεις; δες τα tooltip του κουμπιού turbo</span></p>',\n\t\"u_ts\": '<p class=\"warn\">ΠΡΟΕΙΔΟΠΟΙΗΣΗ: το turbo είναι ενεργοποιημένο, <span>&nbsp;τα αποτελέσματα αναζήτησης μπορεί να είναι λάθος; δες τα tooltip του κουμπιού turbo</span></p>',\n\t\"u_turbo_c\": \"το turbo είναι απενεργοποιημένο στο αρχείο ρυθμίσεων του διακομιστή\",\n\t\"u_turbo_g\": \"απενεργοποιώ το turbo επειδή δεν έχεις δικαίωμα\\nγια τη λίστα φακέλων σε αυτόν τον τόμο\",\n\t\"u_life_cfg\": 'αυτόματη διαγραφή μετά από <input id=\"lifem\" p=\"60\" /> λεπτά (ή <input id=\"lifeh\" p=\"3600\" /> ώρες)',\n\t\"u_life_est\": 'η μεταφόρτωση θα διαγραφεί <span id=\"lifew\" tt=\"τοπική ώρα\">---</span>',\n\t\"u_life_max\": 'αυτός ο φάκελος επιβάλλει\\nμέγιστη διάρκεια ζωής {0}',\n\t\"u_unp_ok\": \"επιτρέπεται το unpost για {0}\",\n\t\"u_unp_ng\": \"δεν επιτρέπεται το unpost\",\n\t\"ue_ro\": \"έχεις μόνο δικαίωμα ανάγνωσης σε αυτόν το φάκελο\\n\\n\",\n\t\"ue_nl\": \"δεν είσαι συνδεδεμένος τώρα\",\n\t\"ue_la\": 'είσαι συνδεδεμένος ως \"{0}\"',\n\t\"ue_sr\": \"είσαι σε λειτουργία αναζήτησης αρχείων\\n\\nπήγαινε σε λειτουργία μεταφόρτωσης πατώντας το 🔎 (δίπλα στο μεγάλο κουμπί ΑΝΑΖΗΤΗΣΗΣ) και δοκίμασε πάλι\\n\\nσυγγνώμη\",\n\t\"ue_ta\": \"δοκίμασε να μεταφορτώσεις εκ νέου, θα πρέπει να δουλέψει τώρα\",\n\t\"ue_ab\": \"αυτό το αρχείο ανεβαίνει σε άλλο φάκελο και η μεταφόρτωση πρέπει να ολοκληρωθεί πριν ανέβει αλλού.\\n\\nΜπορείς να ακυρώσεις και να ξεχάσεις την αρχική μεταφόρτωση με το κουμπί 🧯 πάνω αριστερά\",\n\t\"ur_1uo\": \"ΟΚ: Το αρχείο ανέβηκε επιτυχώς\",\n\t\"ur_auo\": \"ΟΚ: Και τα {0} αρχεία ανέβηκαν επιτυχώς\",\n\t\"ur_1so\": \"ΟΚ: Το αρχείο βρέθηκε στο διακομιστή\",\n\t\"ur_aso\": \"ΟΚ: Και τα {0} αρχεία βρέθηκαν στο διακομιστή\",\n\t\"ur_1un\": \"Η μεταφόρτωση απέτυχε, συγγνώμη\",\n\t\"ur_aun\": \"Και οι {0} μεταφορτώσεις απέτυχαν, συγγνώμη\",\n\t\"ur_1sn\": \"Το αρχείο ΔΕΝ βρέθηκε στο διακομιστή\",\n\t\"ur_asn\": \"Τα {0} αρχεία ΔΕΝ βρέθηκαν στο διακομιστή\",\n\t\"ur_um\": \"Ολοκληρώθηκε;\\n{0} μεταφορτώσεις είναι OK,\\n{1} μεταφορτώσεις απέτυχαν, συγγνώμη\",\n\t\"ur_sm\": \"Ολοκληρώθηκε;\\n{0} αρχεία βρέθηκαν στο διακομιστή,\\n{1} αρχεία ΔΕΝ βρέθηκαν στο διακομιστή\",\n\n\t\"rc_opn\": \"άνοιγμα\", //m\n\t\"rc_ply\": \"αναπαραγωγή\", //m\n\t\"rc_pla\": \"αναπαραγωγή ως ήχος\", //m\n\t\"rc_txt\": \"άνοιγμα στον προβολέα αρχείων\", //m\n\t\"rc_md\": \"άνοιγμα στον επεξεργαστή κειμένου\", //m\n\t\"rc_dl\": \"λήψη\", //m\n\t\"rc_zip\": \"λήψη ως αρχείο\", //m\n\t\"rc_cpl\": \"ἀντίγραφε σύνδεσμον\", //m\n\t\"rc_del\": \"διαγραφή\", //m\n\t\"rc_cut\": \"αποκοπή\", //m\n\t\"rc_cpy\": \"αντιγραφή\", //m\n\t\"rc_pst\": \"επικόλληση\", //m\n\t\"rc_rnm\": \"μετονομασία\", //m\n\t\"rc_nfo\": \"νέος φάκελος\", //m\n\t\"rc_nfi\": \"νέο αρχείο\", //m\n\t\"rc_sal\": \"επιλογή όλων\", //m\n\t\"rc_sin\": \"αντιστροφή επιλογής\", //m\n\t\"rc_shf\": \"κοινή χρήση αυτού του φακέλου\", //m\n\t\"rc_shs\": \"κοινή χρήση επιλογής\", //m\n\n\t\"lang_set\": \"ανανέωση σελίδας για εφαρμογή της αλλαγής;\",\n\n\t\"splash\": {\n\t\t\"a1\": \"ανανέωση\",\n\t\t\"b1\": \"γεια σου ξένε! &nbsp; <small>(δεν είσαι συνδεδεμένος)</small>\",\n\t\t\"c1\": \"αποσύνδεση\",\n\t\t\"d1\": \"λίστα διεργασιών\",\n\t\t\"d2\": \"εμφανίζει την κατάσταση όλων των ενεργών διεργασιών\",\n\t\t\"e1\": \"επαναφόρτωση του cfg\",\n\t\t\"e2\": \"φορτώνει ξανά τα αρχεία ρυθμίσεων (λογαριασμοί/τόμοι/volflags),$Nκαι κάνει επανεξέταση όλων των τόμων e2ds$N$Nσημείωση: οποιαδήποτε αλλαγή στις καθολικές ρυθμίσεις$Nαπαιτεί πλήρη επανεκκίνηση για να εφαρμοστεί\",\n\t\t\"f1\": \"μπορείς να περιηγηθείς:\",\n\t\t\"g1\": \"μπορείς να εκτελέσεις μεταφόρτωση σε:\",\n\t\t\"cc1\": \"άλλα πράγματα:\",\n\t\t\"h1\": \"απενεργοποίση k304\",\n\t\t\"i1\": \"ενεργοποίηση k304\",\n\t\t\"j1\": \"η ενεργοποίηση του k304 θα αποσυνδέσει το πρόγραμμα πελάτη σου σε κάθε HTTP 304, κάτι που μπορεί να αποτρέψει κάποια προβληματικά proxies από το να κολλάνε (να μην φορτώνουν ξαφνικά σελίδες), <em>αλλά</em> θα κάνει τα πράγματα, γενικά πιο αργά\",\n\t\t\"k1\": \"επαναφορά ρυθμίσεων στο πρόγραμμα πελάτη\",\n\t\t\"l1\": \"συνδέσου για περισσότερα:\",\n\t\t\"ls3\": \"σύνδεση\", //m\n\t\t\"lu4\": \"όνομα χρήστη\", //m\n\t\t\"lp4\": \"κωδικός πρόσβασης\", //m\n\t\t\"lo3\": \"αποσύνδεση του “{0}” από παντού\", //m\n\t\t\"lo2\": \"αυτό θα τερματίσει τη συνεδρία σε όλους τους περιηγητές\", //m\n\t\t\"m1\": \"καλώς ήρθες,\",\n\t\t\"n1\": \"404 δεν βρέθηκε &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": '´η μήπως δεν έχεις πρόσβαση -- δοκίμασε έναν κωδικό <a href=\"' + SR + '/?h\">πήγαινε στην αρχική</a>',\n\t\t\"p1\": \"403 απαγορευμένο &nbsp;~┻━┻\",\n\t\t\"q1\": 'δοκίμασε έναν κωδικό <a href=\"' + SR + '/?h\">πήγαινε στην αρχική</a>',\n\t\t\"r1\": \"πίσω στην αρχική\",\n\t\t\".s1\": \"επανάληψη σάρωσης\",\n\t\t\"t1\": \"ενέργεια\",\n\t\t\"u2\": \"χρόνος από την τελευταία εγγραφή του διακομιστή$N( μεταφόρτωση / μετονομασία / ... )$N$N17d = 17 days$N1ω23 = 1 ώρα 23 λεπτά$N4λ56 = 4 λεπτά 56 δευτερόλεπτα\",\n\t\t\"v1\": \"σύνδεση\",\n\t\t\"v2\": \"χρησιμοποίησε αυτόν το διακομιστή σαν τοπικό δίσκο\",\n\t\t\"w1\": \"εναλλαγή σε https\",\n\t\t\"x1\": \"αλλαγή κωδικού\",\n\t\t\"y1\": \"επεξεργασία κοινόχρηστων φακέλων\",\n\t\t\"z1\": \"ξεκλείδωμα αυτού του κοινόχρηστου φακέλου:\",\n\t\t\"ta1\": \"συμπλήρωσε πρώτα το νέο σου κωδικό\",\n\t\t\"ta2\": \"επανέλαβε για να επιβεβαιώσεις το νέο κωδικό:\",\n\t\t\"ta3\": \"βρέθηκε τυπογραφικό λάθος· δοκίμασε ξανά\",\n\t\t\"nop\": \"ΣΦΑΛΜΑ: Ο κωδικός πρόσβασης δεν μπορεί να είναι κενός\", //m\n\t\t\"nou\": \"ΣΦΑΛΜΑ: Το όνομα χρήστη και/ή ο κωδικός πρόσβασης δεν μπορεί να είναι κενό\", //m\n\t\t\"aa1\": \"εισερχόμενα αρχεία:\",\n\t\t\"ab1\": \"απενεργοποίηση no304\",\n\t\t\"ac1\": \"ενεργοποίηση no304\",\n\t\t\"ad1\": \"η ενεργοποίηση του no304 θα απενεργοποιήσει όλη την προσωρινή αποθήκευση· δοκίμασέ το αν το k304 δεν ήταν αρκετό. Προσοχή, θα σπαταλήσει τεράστιο όγκο δικτυακής κίνησης!\",\n\t\t\"ae1\": \"ενεργές μεταφορτώσεις:\",\n\t\t\"af1\": \"προβολή πρόσφατων μεταφορτώσεων\",\n\t\t\"ag1\": \"εμφάνιση γνωστών χρηστών IdP\", //m\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/hun.js",
    "content": "Ls.hun = {\n\t\"tt\": 'Magyar',\n\n\t\"cols\": {\n\t\t\"c\": 'műveletek',\n\t\t\"dur\": 'hossz',\n\t\t\"q\": 'minőség / bitrate',\n\t\t\"Ac\": 'audió kodek',\n\t\t\"Vc\": 'videó kodek',\n\t\t\"Fmt\": 'konténer',\n\t\t\"Ahash\": 'audió hash',\n\t\t\"Vhash\": 'videó hash',\n\t\t\"Res\": 'felbontás',\n\t\t\"T\": 'típus',\n\t\t\"aq\": 'audió minőség / bitrate',\n\t\t\"vq\": 'videó minőség / bitrate',\n\t\t\"pixfmt\": 'színkódolás / pixel',\n\t\t\"resw\": 'szélesség',\n\t\t\"resh\": 'magasság',\n\t\t\"chs\": 'csatornák',\n\t\t\"hz\": 'mintavételezés',\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t'egyéb',\n\t\t\t['ESC', 'minden bezárása'],\n\n\t\t\t'fájlkezelő',\n\t\t\t['G', 'lista / rács nézet'],\n\t\t\t['T', 'ikon / indexkép váltás'],\n\t\t\t['⇧ A/D', 'méret módosítása'],\n\t\t\t['ctrl-K', 'kijelöltek törlése'],\n\t\t\t['ctrl-X', 'kivágás vágólapra'],\n\t\t\t['ctrl-C', 'másolás vágólapra'],\n\t\t\t['ctrl-V', 'beillesztés ide'],\n\t\t\t['Y', 'kijelöltek letöltése'],\n\t\t\t['F2', 'átnevezés'],\n\n\t\t\t'kijelölés',\n\t\t\t['space', 'fájl kijelölése'],\n\t\t\t['↑/↓', 'kurzor mozgatása'],\n\t\t\t['ctrl ↑/↓', 'kurzor + nézet mozgatása'],\n\t\t\t['⇧ ↑/↓', 'fájlok kijelölése'],\n\t\t\t['ctrl-A', 'mindet kijelöli'],\n\t\t],\n\t\t[\n\t\t\t'navigáció',\n\t\t\t['B', 'fa / breadcrumbs váltás'],\n\t\t\t['I/K', 'előző/következő mappa'],\n\t\t\t['M', 'vissza a főmappába'],\n\t\t\t['V', 'fájlok mutatása az oldalsávon'],\n\t\t\t['A/D', 'oldalsáv mérete'],\n\t\t],\n\t\t[\n\t\t\t'zenelejátszó',\n\t\t\t['J/L', 'előző/következő szám'],\n\t\t\t['U/O', '10 mp tekerés (vissza/előre)'],\n\t\t\t['0..9', 'ugrás (0%..90%)'],\n\t\t\t['P', 'play/pause'],\n\t\t\t['S', 'épp szóló szám kijelölése'],\n\t\t\t['Y', 'szám letöltése'],\n\t\t],\n\t\t[\n\t\t\t'képnézegető',\n\t\t\t['J/L, ←/→', 'előző/következő kép'],\n\t\t\t['Home/End', 'első/utolsó kép'],\n\t\t\t['F', 'teljes képernyő'],\n\t\t\t['R', 'forgatás jobbra'],\n\t\t\t['⇧ R', 'forgatás balra'],\n\t\t\t['S', 'kép kijelölése'],\n\t\t\t['Y', 'kép letöltése'],\n\t\t],\n\t\t[\n\t\t\t'videólejátszó',\n\t\t\t['U/O', '10 mp tekerés (vissza/előre)'],\n\t\t\t['P/K/Space', 'play/pause'],\n\t\t\t['C', 'folyamatos lejátszás'],\n\t\t\t['V', 'ismétlés'],\n\t\t\t['M', 'némítás'],\n\t\t\t['[ és ]', 'ismétlési tartomány'],\n\t\t],\n\t\t[\n\t\t\t'szövegszerkesztő',\n\t\t\t['I/K', 'előző/következő fájl'],\n\t\t\t['M', 'bezárás'],\n\t\t\t['E', 'szerkesztés'],\n\t\t\t['S', 'kijelöl fv művelethez'],\n\t\t\t['Y', 'letöltés'],\n\t\t\t['⇧ J', 'json formázás (beautify)'],\n\t\t],\n\t],\n\n\t\"m_ok\": 'ok',\n\t\"m_ng\": 'mégse',\n\n\t\"enable\": 'bekapcsolva',\n\t\"danger\": 'FIGYELEM',\n\t\"clipped\": 'vágólapra másolva',\n\n\t\"ht_s1\": 'másodperce',\n\t\"ht_s2\": 'másodperce',\n\t\"ht_m1\": 'perce',\n\t\"ht_m2\": 'perce',\n\t\"ht_h1\": 'órája',\n\t\"ht_h2\": 'órája',\n\t\"ht_d1\": 'napja',\n\t\"ht_d2\": 'napja',\n\t\"ht_and\": ' és ',\n\n\t\"goh\": 'irányítópult',\n\t\"gop\": 'előző mappába\">előző',\n\t\"gou\": 'szülőmappa\">fel',\n\t\"gon\": 'következő mappába\">következő',\n\t\"logout\": 'Kilépés ',\n\t\"login\": 'Belépés',\n\t\"access\": ' hozzáférés',\n\t\"ot_close\": 'almenü bezárása',\n\t\"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`&quot;pontosan ez&quot;` = pontos egyezés$N$Ndátum formátum iso-8601, pl.$N`2024-12-31` vagy `2024-09-12 23:30:00`',\n\t\"ot_unpost\": 'töröld a nemrég feltöltött fájljaidat, vagy állítsd le a folyamatban lévőket',\n\t\"ot_bup\": 'bup: egyszerű feltöltő (még netscape 4-en is megy)',\n\t\"ot_mkdir\": 'új mappa létrehozása',\n\t\"ot_md\": 'új szöveges fájl',\n\t\"ot_msg\": 'üzenet küldése a szerver logba',\n\t\"ot_mp\": 'lejátszó beállításai',\n\t\"ot_cfg\": 'beállítások',\n\t\"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.<br /><br />feltöltés közben az ikon mutatja a haladást!',\n\t\"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.<br /><br />feltöltés közben az ikon mutatja a haladást!',\n\t\"ot_noie\": 'Kérlek használj Chrome-ot / Firefox-ot / Edge-et',\n\n\t\"ab_mkdir\": 'mappa létrehozása',\n\t\"ab_mkdoc\": 'új szövegfájl',\n\t\"ab_msg\": 'log üzenet küldése',\n\n\t\"ay_path\": 'ugrás a mappákhoz',\n\t\"ay_files\": 'ugrás a fájlokhoz',\n\n\t\"wt_ren\": 'átnevezés$Ngyorsbillentyű: F2',\n\t\"wt_del\": 'törlés$Ngyorsbillentyű: ctrl-K',\n\t\"wt_cut\": 'kivágás &lt;small&gt;(beillesztéshez)&lt;/small&gt;$Ngyorsbillentyű: ctrl-X',\n\t\"wt_cpy\": 'másolás vágólapra$Ngyorsbillentyű: ctrl-C',\n\t\"wt_pst\": 'beillesztés$Ngyorsbillentyű: ctrl-V',\n\t\"wt_selall\": 'összes kijelölése$Ngyorsbillentyű: ctrl-A',\n\t\"wt_selinv\": 'kijelölés megfordítása',\n\t\"wt_zip1\": 'mappa letöltése zip/tar-ként',\n\t\"wt_selzip\": 'kijelöltek letöltése archívumként',\n\t\"wt_seldl\": 'kijelöltek letöltése külön fájlokként$Ngyorsbillentyű: Y',\n\t\"wt_npirc\": 'irc-formátumú infó másolása',\n\t\"wt_nptxt\": 'egyszerű szöveges infó másolása',\n\t\"wt_m3ua\": 'hozzáadás m3u listához (később <code>📻másolás</code>)',\n\t\"wt_m3uc\": 'm3u lista másolása vágólapra',\n\t\"wt_grid\": 'lista / rács nézet váltás$Ngyorsbillentyű: G',\n\t\"wt_prev\": 'előző szám$Ngyorsbillentyű: J',\n\t\"wt_play\": 'lejátszás / szünet$Ngyorsbillentyű: P',\n\t\"wt_next\": 'következő szám$Ngyorsbillentyű: L',\n\n\t\"ul_par\": 'párhuzamos szálak:',\n\t\"ut_rand\": 'véletlen fájlnevek',\n\t\"ut_u2ts\": 'helyi dátumok$Nátvitele a szerverre\">📅',\n\t\"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',\n\t\"ut_mt\": 'háttérben hashelés feltöltés alatt$N$Nkapcsold ki, ha fagy a géped',\n\t\"ut_ask\": 'megerősítés feltöltés előtt\">💭',\n\t\"ut_pot\": 'feltöltés gyorsítása (egyszerűbb UI)',\n\t\"ut_srch\": 'csak létezés ellenőrzése$N(nem tölt fel semmit)',\n\t\"ut_par\": '0 = szünet$N$Nnöveld, ha lassú a net$N$NHDD limit vagy LAN esetén hagyd 1-en',\n\t\"ul_btn\": 'dobd ide a fájlokat/mappákat<br>(vagy kattints)',\n\t\"ul_btnu\": 'F E L T Ö L T É S',\n\t\"ul_btns\": 'K E R E S É S',\n\n\t\"ul_hash\": 'hash',\n\t\"ul_send\": 'küldés',\n\t\"ul_done\": 'kész',\n\t\"ul_idle1\": 'nincs várakozó feltöltés',\n\t\"ut_etah\": 'átlagos &lt;em&gt;hashelési&lt;/em&gt; sebesség és becsült idő',\n\t\"ut_etau\": 'átlagos &lt;em&gt;feltöltési&lt;/em&gt; sebesség és becsült idő',\n\t\"ut_etat\": 'átlagos &lt;em&gt;össz&lt;/em&gt; sebesség és becsült idő',\n\n\t\"uct_ok\": 'sikerült',\n\t\"uct_ng\": 'hiba / elutasítva',\n\t\"uct_done\": 'összesen',\n\t\"uct_bz\": 'hashelés vagy feltöltés...',\n\t\"uct_q\": 'várakozik',\n\n\t\"utl_name\": 'fájlnév',\n\t\"utl_ulist\": 'lista',\n\t\"utl_ucopy\": 'másolás',\n\t\"utl_links\": 'linkek',\n\t\"utl_stat\": 'státusz',\n\t\"utl_prog\": 'haladás',\n\n\t\"utl_404\": '404',\n\t\"utl_err\": 'HIBA',\n\t\"utl_oserr\": 'OS-hiba',\n\t\"utl_found\": 'megvan',\n\t\"utl_defer\": 'halasztva',\n\t\"utl_yolo\": 'YOLO',\n\t\"utl_done\": 'kész',\n\n\t\"ul_flagblk\": 'fájlok a várólistán</b><br>egy másik lapon már fut egy feltöltés,<br>megvárjuk, amíg az végez',\n\t\"ul_btnlk\": 'szerveroldali korlátozás miatt nem módosítható',\n\n\t\"udt_up\": 'Feltöltés',\n\t\"udt_srch\": 'Keresés',\n\t\"udt_drop\": 'dobd ide',\n\n\t\"u_nav_m\": '<h6>mit töltsünk fel?</h6><code>Enter</code> = Fájlok (egy vagy több)\\n<code>ESC</code> = Egy mappa (almappákkal)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Fájlok</a><a href=\"#\" id=\"modal-ng\">Egy mappa</a>',\n\n\t\"cl_opts\": 'beállítások',\n\t\"cl_hfsz\": 'méret',\n\t\"cl_themes\": 'téma',\n\t\"cl_langs\": 'nyelv',\n\t\"cl_ziptype\": 'mappa letöltés',\n\t\"cl_uopts\": 'up2k beállítások',\n\t\"cl_favico\": 'favicon',\n\t\"cl_bigdir\": 'limit feletti mappák',\n\t\"cl_hsort\": '#rendezés',\n\t\"cl_keytype\": 'hangnem-jelölés',\n\t\"cl_hiddenc\": 'rejtett oszlopok',\n\t\"cl_hidec\": 'rejt',\n\t\"cl_reset\": 'alaphelyzet',\n\t\"cl_hpick\": 'kattints az oszlopfejlécre az elrejtéshez',\n\t\"cl_hcancel\": 'elrejtés megszakítva',\n\t\"cl_rcm\": 'jobb-klikkes menü',\n\n\t\"ct_grid\": '田 rács nézet',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ segítő szövegek',\n\t\"ct_thumb\": 'rács nézetben ikonok/indexképek váltása$Ngyorsbillentyű: T\">🖼️ képek',\n\t\"ct_csel\": 'kijelölés CTRL és SHIFT gombokkal rács nézetben\">kijelölés',\n\t\"ct_dsel\": 'kijelölés egérhúzással rács nézetben\">húzás',\n\t\"ct_dl\": 'azonnali letöltés (beágyazás helyett)\">letöltés',\n\t\"ct_ihop\": 'nézegető bezárásakor ugorjon az utolsó képhez\">g⮯',\n\t\"ct_dots\": 'rejtett fájlok mutatása (ha szabad)\">rejtett',\n\t\"ct_qdel\": 'csak egyszer kérdezzen rá a törlésre\">gyorstörlés',\n\t\"ct_dir1st\": 'mappák előre rendezése\">📁 előre',\n\t\"ct_nsort\": 'természetes rendezés (pl. 1, 2, 10)\">rendezés',\n\t\"ct_utc\": 'időpontok UTC-ben\">UTC',\n\t\"ct_readme\": 'README.md mutatása a mappákban\">📜 readme',\n\t\"ct_idxh\": 'index.html mutatása listázás helyett\">htm',\n\t\"ct_sbars\": 'gördítősávok mutatása\">⟊',\n\n\t\"cut_umod\": 'létező fájl dátumának frissítése a helyire (írható+törölhető jog kell)\">re📅',\n\n\t\"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',\n\n\t\"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',\n\n\t\"cut_u2sz\": 'feltöltési egység (chunk) mérete (MiB); nagy érték jó a távoli szerverekhez, kicsi a gyenge nethez',\n\n\t\"cut_flag\": 'egyszerre csak egy fül tölthessen fel (azonos domainen belül hat)',\n\n\t\"cut_az\": 'fájlok feltöltése ABC-sorrendben (nem a legkisebb-először módon)',\n\n\t\"cut_nag\": 'értesítés, ha kész a feltöltés (ha nem ezen a fülön állsz)',\n\t\"cut_sfx\": 'hangjelzés, ha kész a feltöltés',\n\n\t\"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',\n\n\t\"cut_wasm\": 'wasm hashelő; chrome-on gyorsabb, de CPU-igényes és régebbi verzióknál fagyhat\">wasm',\n\n\t\"cft_text\": 'favicon szöveg (üresen kikapcsol)',\n\t\"cft_fg\": 'szöveg színe',\n\t\"cft_bg\": 'háttér színe',\n\n\t\"cdt_lim\": 'lista hossza (max fájl/mappa)',\n\t\"cdt_ask\": 'lista végén ne töltsön be automatikusan, inkább kérdezzen rá',\n\t\"cdt_hsort\": 'rendezési szabályok száma a linkekben. 0 = tiltás',\n\t\"cdt_ren\": 'egyedi jobb-klikkes menü (gyári: SHIFT + jobb-klikk)\">bekapcsolva',\n\t\"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',\n\n\t\"tt_entree\": 'oldalsáv (mappafa) mutatása$Ngyorsbillentyű: B',\n\t\"tt_detree\": 'elérési út mutatása$Ngyorsbillentyű: B',\n\t\"tt_visdir\": 'gördítés a kijelölt mappához',\n\t\"tt_ftree\": 'mappák / szövegfájlok váltása$Ngyorsbillentyű: V',\n\t\"tt_pdock\": 'szülőmappák rögzítése felül',\n\t\"tt_dynt\": 'automatikus méretezés nyitáskor',\n\t\"tt_wrap\": 'sortörés',\n\t\"tt_hover\": 'túl hosszú sorok mutatása rámutatáskor',\n\n\t\"ml_pmode\": 'mappa végén...',\n\t\"ml_btns\": 'gombok',\n\t\"ml_tcode\": 'átkódolás',\n\t\"ml_tcode2\": 'átkódolás ide:',\n\t\"ml_tint\": 'színezés',\n\t\"ml_eq\": 'eq',\n\t\"ml_drc\": 'dinamikatartomány-tömörítő',\n\t\"ml_ss\": 'csönd átugrása',\n\n\t\"mt_loop\": 'egy szám ismétlése\">🔁',\n\t\"mt_one\": 'leállás egy szám után\">1️⃣',\n\t\"mt_shuf\": 'véletlenszerű lejátszás mappánként\">🔀',\n\t\"mt_aplay\": 'automatikus kezdés a link alapján$N$Nha kikapcsolod, a linkek sem fognak szám-azonosítót tartalmazni\">a▶',\n\t\"mt_preload\": 'következő szám betöltése a végén (gapless)\">preload',\n\t\"mt_prescan\": 'mappaváltás az utolsó szám vége előtt, hogy a böngésző ne álljon le\">nav',\n\t\"mt_fullpre\": 'teljes szám előtöltése;$N✅ instabil netre,$N❌ lassú neten inkább kapcsold ki\">full',\n\t\"mt_fau\": 'mobilon megakadályozza a leállást, ha lassú az előtöltés (a tagek kijelzése hibás lehet)\">☕️',\n\t\"mt_waves\": 'hullámforma a tekerősávon:$Nhangerő amplitúdó mutatása a sávban\">~s',\n\t\"mt_npclip\": 'gombok a most szóló szám infójának másolásához\">/np',\n\t\"mt_m3u_c\": 'gombok a kijelöltek m3u8 listába másolásához\">📻',\n\t\"mt_octl\": 'rendszerintegráció (média gombok / osd)\">os-ctl',\n\t\"mt_oseek\": 'tekerés engedése a rendszer szinten$N$Nmegjegyzés: iphonon ez felülírja a következő szám gombot\">seek',\n\t\"mt_oscv\": 'albumkép mutatása az osd-n\">art',\n\t\"mt_follow\": 'fókuszban tartja az aktuális számot\">🎯',\n\t\"mt_compact\": 'kompakt vezérlők\">⟎',\n\t\"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',\n\t\"mt_mloop\": 'mappa ismétlése\">🔁 ismétlés',\n\t\"mt_mnext\": 'következő mappa betöltése és folytatás\">📂 tovább',\n\t\"mt_mstop\": 'stop\">⏸ stop',\n\t\"mt_cflac\": 'flac / wav konvertálása ide: {0}\">flac',\n\t\"mt_caac\": 'aac / m4a konvertálása ide: {0}\">aac',\n\t\"mt_coth\": 'egyéb (nem mp3) konvertálása ide: {0}\">egyéb',\n\t\"mt_c2opus\": 'asztali gép, laptop, android kedvence\">opus',\n\t\"mt_c2owa\": 'opus-weba (iOS 17.5+)\">owa',\n\t\"mt_c2caf\": 'opus-caf (iOS 11-17)\">caf',\n\t\"mt_c2mp3\": 'nagyon régi eszközökhöz\">mp3',\n\t\"mt_c2flac\": 'legjobb minőség, de hatalmas fájlok\">flac',\n\t\"mt_c2wav\": 'tömörítetlen (még nagyobb)\">wav',\n\t\"mt_c2ok\": 'szuper, jó választás',\n\t\"mt_c2nd\": 'nem javasolt ehhez az eszközhöz, de rendben',\n\t\"mt_c2ng\": 'úgy tűnik, az eszközöd nem támogatja, de próbáljuk meg',\n\t\"mt_xowa\": 'iOS hiba akadályozza a háttérben lejátszást ezzel; használd inkább a caf-ot vagy mp3-at',\n\t\"mt_tint\": 'háttérszint (0-100) a tekerősávon, hogy ne zavarjon a betöltés',\n\t\"mt_eq\": '`eq és erősítés;$N$Nboost `0` = alap hangerő (100%)$N$Nszélesség `1 &nbsp;` = standard sztereó$Nszélesség `0.5` = 50% áthallás$Nszélesség `0 &nbsp;` = monó$N$Nboost `-0.8` &amp; 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',\n\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',\n\t\"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',\n\t\"mt_ssvt\": 'hangerő küszöb (0-255)\">vol',\n\t\"mt_ssts\": 'aktív tartomány (%, eleje)\">start',\n\t\"mt_sste\": 'aktív tartomány (%, vége)\">end',\n\t\"mt_sssm\": 'lejátszási sebesség szorzó\">ffwd',\n\n\t\"mb_play\": 'play',\n\t\"mm_hashplay\": 'lejátsszam ezt a fájlt?',\n\t\"mm_m3u\": '<code>Enter/OK</code>: Lejátszás\\n<code>ESC/Cancel</code>: Szerkesztés',\n\t\"mp_breq\": \"firefox 82+, chrome 73+, vagy iOS 15+ szükséges\",\n\t\"mm_bload\": 'betöltés...',\n\t\"mm_bconv\": 'konvertálás ide: {0}, várj légy szíves...',\n\t\"mm_opusen\": 'a böngésződ nem támogatja az aac / m4a fájlokat;\\nátkódolás opus-ba bekapcsolva',\n\t\"mm_playerr\": 'lejátszási hiba: ',\n\t\"mm_eabrt\": 'lejátszás megszakítva',\n\t\"mm_enet\": 'szakadozik a neted',\n\t\"mm_edec\": 'ez a fájl valahogy sérült??',\n\t\"mm_esupp\": 'a böngésződ nem ismeri ezt a formátumot',\n\t\"mm_eunk\": 'ismeretlen hiba',\n\t\"mm_e404\": 'audió hiba 404: fájl nem található.',\n\t\"mm_e403\": 'audió hiba 403: hozzáférés megtagadva.\\n\\npróbáld meg az F5-öt, hátha kiléptetett a rendszer',\n\t\"mm_e415\": 'audió hiba 415: átkódolás sikertelen; nézd meg a szerver logot.',\n\t\"mm_e500\": 'audió hiba 500: nézd meg a szerver logot.',\n\t\"mm_e5xx\": 'audió hiba; szerver hiba ',\n\t\"mm_nof\": 'nem találok több zenét a környéken',\n\t\"mm_prescan\": 'következő szám keresése...',\n\t\"mm_scank\": 'következő megvan:',\n\t\"mm_uncache\": 'gyorsítótár ürítve; minden számot újra letölt az újabb lejátszásnál',\n\t\"mm_hnf\": 'ez a szám már nem létezik',\n\n\t\"im_hnf\": 'ez a kép már nem létezik',\n\n\t\"f_empty\": 'a mappa üres',\n\t\"f_chide\": 'ez elrejti a(z) «{0}» oszlopot\\n\\nvisszakapcsolhatod a beállításoknál',\n\t\"f_bigtxt\": 'ez a fájl {0} MiB -- valóban megnyitod szövegként?',\n\t\"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',\n\t\"fbd_more\": '<div id=\"blazy\"><code>{0}</code> / <code>{1}</code> fájl látszik; <a href=\"#\" id=\"bd_more\">mutass még {2}-t</a> vagy <a href=\"#\" id=\"bd_all\">mutasd mindet</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\"><code>{0}</code> / <code>{1}</code> fájl látszik; <a href=\"#\" id=\"bd_all\">mutasd mindet</a></div>',\n\t\"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',\n\n\t\"f_dls\": 'a mappában lévő linkek letöltési linkekké alakultak',\n\t\"f_dl_nd\": 'mappa kihagyása (használd a zip/tar letöltést):\\n',\n\n\t\"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 <code>.PARTIAL</code> kiterjesztés.\\n\\nha mégis OK-t nyomsz, valószínűleg hibás adatot kapsz.',\n\n\t\"ft_paste\": 'beillesztés: {0} elem$Ngyorsbillentyű: ctrl-V',\n\t\"fr_eperm\": 'nem tudom átnevezni:\\nnincs “mozgatás” jogod ebben a mappában',\n\t\"fd_eperm\": 'nem tudom törölni:\\nnincs “törlés” jogod ebben a mappában',\n\t\"fc_eperm\": 'nem tudom kivágni:\\nnincs “ mozgatás ” jogod ebben a mappában',\n\t\"fp_eperm\": 'nem tudom beilleszteni:\\nnincs “írás” jogod ebben a mappában',\n\t\"fr_emore\": 'jelölj ki legalább egy elemet az átnevezéshez',\n\t\"fd_emore\": 'jelölj ki legalább egy elemet a törléshez',\n\t\"fc_emore\": 'jelölj ki legalább egy elemet a kivágáshoz',\n\t\"fcp_emore\": 'jelölj ki legalább egy elemet a másoláshoz',\n\n\t\"fs_sc\": 'mappa megosztása',\n\t\"fs_ss\": 'kijelöltek megosztása',\n\t\"fs_just1d\": 'nem tudsz egyszerre több mappát,\\nvagy fájlokat és mappákat vegyesen megosztani',\n\t\"fs_abrt\": '❌ mégse',\n\t\"fs_rand\": '🎲 random név',\n\t\"fs_go\": '✅ megosztás létrehozása',\n\t\"fs_name\": 'név',\n\t\"fs_src\": 'forrás',\n\t\"fs_pwd\": 'jelszó',\n\t\"fs_exp\": 'lejárat',\n\t\"fs_tmin\": 'perc',\n\t\"fs_thrs\": 'óra',\n\t\"fs_tdays\": 'nap',\n\t\"fs_never\": 'örök',\n\t\"fs_pname\": 'nem kötelező link név; üresen véletlenszerű lesz',\n\t\"fs_tsrc\": 'a megosztandó fájl vagy mappa',\n\t\"fs_ppwd\": 'nem kötelező jelszó',\n\t\"fs_w8\": 'megosztás létrehozása...',\n\t\"fs_ok\": '<code>Enter/OK</code>: Másolás vágólapra\\n<code>ESC/Cancel</code>: Bezárás',\n\n\t\"frt_dec\": 'helyreállíthatja a hibás fájlneveket\">url-decode',\n\t\"frt_rst\": 'eredeti nevek visszaállítása\">↺ alaphelyzet',\n\t\"frt_abrt\": 'ablak bezárása\">❌ mégse',\n\t\"frb_apply\": 'ÁTNEVEZÉS',\n\t\"fr_adv\": 'tömeges / metaadat / minta alapú átnevezés\">haladó',\n\t\"fr_case\": 'kis/nagybetű érzékeny regex\">case',\n\t\"fr_win\": 'windows-biztos nevek; cseréli a <code>&lt;&gt;:&quot;\\\\|?*</code> karaktereket japán teljes szélességűekre\">win',\n\t\"fr_slash\": '<code>/</code> cseréje olyanra, ami nem hoz létre új mappát\">no /',\n\t\"fr_re\": '`regex keresési minta; a csoportokra így hivatkozhatsz lent: `(1)`, `(2)` stb.',\n\t\"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',\n\t\"fr_pdel\": 'törlés',\n\t\"fr_pnew\": 'mentés mint',\n\t\"fr_pname\": 'adj nevet az új presetnek',\n\t\"fr_aborted\": 'megszakítva',\n\t\"fr_lold\": 'régi név',\n\t\"fr_lnew\": 'új név',\n\t\"fr_tags\": 'tagek a kijelölt fájlokban (csak olvasásra):',\n\t\"fr_busy\": '{0} elem átnevezése...\\n\\n{1}',\n\t\"fr_efail\": 'átnevezés sikertelen:\\n',\n\t\"fr_nchg\": '{0} név módosult a <code>win</code> vagy <code>no /</code> miatt\\n\\nfolytassuk az új nevekkel?',\n\n\t\"fd_ok\": 'törölve',\n\t\"fd_err\": 'törlés sikertelen:\\n',\n\t\"fd_none\": 'semmi sem lett törölve; lehet, hogy a szerver tiltja (xbd)?',\n\t\"fd_busy\": '{0} elem törlése...\\n\\n{1}',\n\t\"fd_warn1\": 'TÖRLÖD ezt a(z) {0} elemet?',\n\t\"fd_warn2\": '<b>Utolsó esély!</b> Nem vonható vissza. Törlöd?',\n\n\t\"fc_ok\": '{0} elem kivágva',\n\t\"fc_warn\": '{0} elem kivágva\\n\\nde: csak <b>ez</b> a fül tudja beilleszteni őket\\n(mivel a kijelölés brutálisan nagy)',\n\n\t\"fcc_ok\": '{0} elem vágólapra másolva',\n\t\"fcc_warn\": '{0} elem vágólapra másolva\\n\\nde: csak <b>ez</b> a fül tudja beilleszteni őket\\n(mivel a kijelölés brutálisan nagy)',\n\n\t\"fp_apply\": 'nevek mentése',\n\t\"fp_skip\": 'ütközések kihagyása',\n\t\"fp_ecut\": 'előbb vágj ki vagy másolj valamit\\n\\nmegjegyzés: néha fülek között is megy',\n\t\"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:',\n\t\"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:',\n\t\"fp_emore\": 'maradt még néhány névütközés',\n\t\"fp_ok\": 'mozgatás kész',\n\t\"fcp_ok\": 'másolás kész',\n\t\"fp_busy\": '{0} elem mozgatása...\\n\\n{1}',\n\t\"fcp_busy\": '{0} elem másolása...\\n\\n{1}',\n\t\"fp_abrt\": 'megszakítás...',\n\t\"fp_err\": 'mozgatás sikertelen:\\n',\n\t\"fcp_err\": 'másolás sikertelen:\\n',\n\t\"fp_confirm\": 'mozgassuk ide ezt a {0} elemet?',\n\t\"fcp_confirm\": 'másoljuk ide ezt a {0} elemet?',\n\t\"fp_etab\": 'nem sikerült a vágólap olvasása másik fülről',\n\t\"fp_name\": 'fájl feltöltése. Adj neki nevet:',\n\t\"fp_both_m\": '<h6>mi legyen?</h6><code>Enter</code> = Mozgatás: {0} elem innen: «{1}»\\n<code>ESC</code> = Feltöltés: {2} fájl a gépedről',\n\t\"fcp_both_m\": '<h6>mi legyen?</h6><code>Enter</code> = Másolás: {0} elem innen: «{1}»\\n<code>ESC</code> = Feltöltés: {2} fájl a gépedről',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Mozgatás</a><a href=\"#\" id=\"modal-ng\">Feltöltés</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Másolás</a><a href=\"#\" id=\"modal-ng\">Feltöltés</a>',\n\n\t\"mk_noname\": 'írj be egy nevet a bal oldali mezőbe :p',\n\t\"nmd_i1\": 'add meg a kiterjesztést is, pl. <code>.md</code>',\n\t\"nmd_i2\": 'csak <code>.{0}</code> fájlt hozhatsz létre, mert nincs törlési jogod',\n\n\t\"tv_load\": 'Szövegfájl betöltése:\\n\\n{0}\\n\\n{1}% ({2} / {3} MiB betöltve)',\n\t\"tv_xe1\": 'nem sikerült betölteni:\\n\\nhiba ',\n\t\"tv_xe2\": '404, fájl nem található',\n\t\"tv_lst\": 'szövegfájlok a mappában:',\n\t\"tvt_close\": 'vissza a mappába$Ngyorsbillentyű: M (vagy Esc)\">❌ bezárás',\n\t\"tvt_dl\": 'fájl letöltése$Ngyorsbillentyű: Y\">💾 letöltés',\n\t\"tvt_prev\": 'előző dokumentum$Ngyorsbillentyű: i\">⬆ előző',\n\t\"tvt_next\": 'következő dokumentum$Ngyorsbillentyű: K\">⬇ következő',\n\t\"tvt_sel\": 'kijelölés (vágáshoz/másoláshoz/törléshez)$Ngyorsbillentyű: S\">kijelöl',\n\t\"tvt_j\": 'json formázás$Ngyorsbillentyű: shift-J\">j',\n\t\"tvt_edit\": 'szerkesztés szövegként$Ngyorsbillentyű: E\">✏️ szerkeszt',\n\t\"tvt_tail\": 'fájl figyelése; új sorok mutatása élőben\">📡 követve',\n\t\"tvt_wrap\": 'sortörés\">↵',\n\t\"tvt_atail\": 'gördítés az aljára rögzítve\">⚓',\n\t\"tvt_ctail\": 'terminál színek (ansi escape) dekódolása\">🌈',\n\t\"tvt_ntail\": 'visszapörgetési limit (hány bájt szöveg maradjon a memóriában)',\n\n\t\"m3u_add1\": 'szám hozzáadva az m3u listához',\n\t\"m3u_addn\": '{0} szám hozzáadva az m3u listához',\n\t\"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',\n\n\t\"gt_vau\": 'videó elrejtése, csak a hang lejátszása\">🎧',\n\t\"gt_msel\": 'kijelölés engedélyezése; ctrl-klikk a felülbíráláshoz$N$N&lt;em&gt;ha aktív: dupla klikk a megnyitáshoz&lt;/em&gt;$N$Ngyorsbillentyű: S\">tömeges',\n\t\"gt_crop\": 'középre vágott indexképek\">crop',\n\t\"gt_3x\": 'nagy felbontású indexképek\">3x',\n\t\"gt_zoom\": 'zoom',\n\t\"gt_chop\": 'vágás',\n\t\"gt_sort\": 'rendezés:',\n\t\"gt_name\": 'név',\n\t\"gt_sz\": 'méret',\n\t\"gt_ts\": 'dátum',\n\t\"gt_ext\": 'típus',\n\t\"gt_c1\": 'rövidebb fájlnevek',\n\t\"gt_c2\": 'hosszabb fájlnevek',\n\n\t\"sm_w8\": 'keresés...',\n\t\"sm_prev\": 'az alábbi eredmények egy korábbi keresésből vannak:\\n  ',\n\t\"sl_close\": 'eredmények bezárása',\n\t\"sl_hits\": '{0} találat',\n\t\"sl_moar\": 'továbbiak betöltése',\n\n\t\"s_sz\": 'méret',\n\t\"s_dt\": 'dátum',\n\t\"s_rd\": 'útvonal',\n\t\"s_fn\": 'név',\n\t\"s_ta\": 'tagek',\n\t\"s_ua\": 'feltöltve',\n\t\"s_ad\": 'haladó',\n\t\"s_s1\": 'minimum MiB',\n\t\"s_s2\": 'maximum MiB',\n\t\"s_d1\": 'min. iso8601',\n\t\"s_d2\": 'max. iso8601',\n\t\"s_u1\": 'ezután töltve',\n\t\"s_u2\": 'vagy ez előtt',\n\t\"s_r1\": 'útvonal tartalmazza (szóközzel elválasztva)',\n\t\"s_f1\": 'név tartalmazza (negálás pl -nemez)',\n\t\"s_t1\": 'tagek tartalmazzák (^=kezdet, vég=$)',\n\t\"s_a1\": 'specifikus metaadatok',\n\n\t\"md_eshow\": 'nem sikerült megjeleníteni ',\n\t\"md_off\": '[📜<em>readme</em>] kikapcsolva a beállításokban -- rejtve',\n\n\t\"badreply\": 'Hibás válasz a szervertől',\n\n\t\"xhr403\": '403: Hozzáférés megtagadva\\n\\npróbáld az F5-öt, hátha kiléptetett a rendszer',\n\t\"xhr0\": 'ismeretlen hiba (valószínűleg megszakadt a kapcsolat)',\n\t\"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',\n\t\"tl_xe1\": 'almappák listázása sikertelen:\\n\\nhiba ',\n\t\"tl_xe2\": '404: mappa nem található',\n\t\"fl_xe1\": 'fájlok listázása sikertelen:\\n\\nhiba ',\n\t\"fl_xe2\": '404: mappa nem található',\n\t\"fd_xe1\": 'almappa létrehozása sikertelen:\\n\\nhiba ',\n\t\"fd_xe2\": '404: szülőmappa nincs meg',\n\t\"fsm_xe1\": 'üzenetküldés sikertelen:\\n\\nhiba ',\n\t\"fsm_xe2\": '404: szülőmappa nincs meg',\n\t\"fu_xe1\": 'unpost lista betöltése sikertelen:\\n\\nhiba ',\n\t\"fu_xe2\": '404: fájl nincs meg??',\n\n\t\"fz_tar\": 'tömörítetlen gnu-tar (linux / mac)',\n\t\"fz_pax\": 'tömörítetlen pax-formátumú tar (lassabb)',\n\t\"fz_targz\": 'gnu-tar gzip 3-as szinten$N$Náltalában lassú, inkább használd a sima tar-t',\n\t\"fz_tarxz\": 'gnu-tar xz 1-es szinten$N$Náltalában lassú, inkább használd a sima tar-t',\n\t\"fz_zip8\": 'zip utf8 nevekkel (régi windows alatt hibás lehet)',\n\t\"fz_zipd\": 'zip cp437 nevekkel (nagyon régi szoftverekhez)',\n\t\"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)',\n\n\t\"un_m1\": 'itt törölheted a nemrég feltöltött fájljaidat (vagy leállíthatod a futókat)',\n\t\"un_upd\": 'frissítés',\n\t\"un_m4\": 'vagy oszd meg az alábbi fájlokat:',\n\t\"un_ulist\": 'mutasd',\n\t\"un_ucopy\": 'másold',\n\t\"un_flt\": 'szűrő:&nbsp; az URL tartalmazza:',\n\t\"un_fclr\": 'szűrő törlése',\n\t\"un_derr\": 'törlés sikertelen:\\n',\n\t\"un_f5\": 'valami elromlott, próbáld meg frissíteni az oldalt',\n\t\"un_uf5\": 'frissítened kell az oldalt (F5 vagy CTRL-R), mielőtt ezt leállíthatnád',\n\t\"un_nou\": '<b>figyelem:</b> a szerver túlterhelt a futó feltöltések listázásához; próbáld később',\n\t\"un_noc\": '<b>figyelem:</b> a kész feltöltések törlése nincs engedélyezve a szerveren',\n\t\"un_max\": 'az első 2000 fájl látszik (használd a szűrőt)',\n\t\"un_avail\": '{0} kész feltöltés törölhető<br />{1} folyamatban lévő leállítható',\n\t\"un_m2\": 'idő szerint rendezve (legfrissebb legelöl):',\n\t\"un_no1\": 'semmi! nincsenek nemrég feltöltött fájljaid',\n\t\"un_no2\": 'semmi! nincs a szűrőnek megfelelő friss feltöltés',\n\t\"un_next\": 'következő {0} fájl törlése',\n\t\"un_abrt\": 'leállítás',\n\t\"un_del\": 'törlés',\n\t\"un_m3\": 'legutóbbi feltöltések betöltése...',\n\t\"un_busy\": '{0} fájl törlése...',\n\t\"un_clip\": '{0} link vágólapra másolva',\n\n\t\"u_https1\": 'érdemesebb lenne',\n\t\"u_https2\": 'https-re váltani',\n\t\"u_https3\": 'a jobb sebességért',\n\t\"u_ancient\": 'lenyűgözően őskori a böngésződ -- talán <a href=\"#\" onclick=\"goto(\\'bup\\')\">használd inkább az egyszerű uploader-t</a>',\n\t\"u_nowork\": 'legalább Firefox 53, Chrome 57 vagy iOS 11 kell',\n\t\"tail_2old\": 'legalább Firefox 105, Chrome 71 vagy iOS 14.5 kell',\n\t\"u_nodrop\": 'a böngésződ túl öreg a drag-and-drop-hoz',\n\t\"u_notdir\": 'ez nem egy mappa!\\n\\na böngésződ túl öreg,\\npróbáld meg inkább behúzni',\n\t\"u_uri\": 'képek behúzásához más ablakból,\\ndobd rá a nagy feltöltés gombra',\n\t\"u_enpot\": 'váltás <a href=\"#\">egyszerű UI-ra</a> (gyorsíthat a feltöltésen)',\n\t\"u_depot\": 'váltás <a href=\"#\">vibráló UI-ra</a> (visszafoghatja a sebességet)',\n\t\"u_gotpot\": 'átváltottunk egyszerű UI-ra a sebesség miatt,\\nde bármikor visszaválthatsz!',\n\t\"u_pott\": '<p>fájlok: &nbsp; <b>{0}</b> kész, &nbsp; <b>{1}</b> hiba, &nbsp; <b>{2}</b> fut, &nbsp; <b>{3}</b> vár</p>',\n\t\"u_ever\": 'ez az egyszerű feltöltő; az up2k-hoz legalább<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1 kell',\n\t\"u_su2k\": 'ez az egyszerű feltöltő; az <a href=\"#\" id=\"u2yea\">up2k</a> sokkal jobb',\n\t\"u_uput\": 'sebességre optimalizál (ellenőrzés nélkül)',\n\t\"u_ewrite\": 'nincs írási jogod ebben a mappában',\n\t\"u_eread\": 'nincs olvasási jogod ebben a mappában',\n\t\"u_enoi\": 'fájlkeresés letiltva a szerveren',\n\t\"u_enoow\": 'felülírás nem fog menni; törlési jog kell hozzá',\n\t\"u_badf\": 'Ez a(z) {0} fájl ({1}-ből) kimaradt, valószínűleg jogok miatt:\\n\\n',\n\t\"u_blankf\": 'Ez a(z) {0} fájl ({1}-ből) teljesen üres; biztos feltöltöd?\\n\\n',\n\t\"u_applef\": 'Ez a(z) {0} fájl ({1}-ből) valószínűleg felesleges;\\nNyomj <code>OK/Enter</code>-t a KIHAGYÁSHOZ,\\nvagy <code>Cancel/ESC</code>-et a FELTÖLTÉSHEZ:\\n\\n',\n\t\"u_just1\": '\\nlehet, hogy jobban működik, ha csak egy fájlt jelölsz ki',\n\t\"u_ff_many\": 'ha <b>Linux / MacOS / Android</b> rendszert használsz, ennyi fájl <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">fagyást okozhat Firefoxban!</a>\\nha ez történik, próbáld újra (vagy használd a Chrome-ot).',\n\t\"u_up_life\": 'ez a feltöltés a befejezés után\\n{0} múlva törlődik a szerverről',\n\t\"u_asku\": 'feltöltsem ezt a {0} fájlt ide: <code>{1}</code>?',\n\t\"u_unpt\": 'visszavonhatod/törölheted a feltöltést a bal felső 🧯 gombbal',\n\t\"u_bigtab\": '{0} fájl listázása következik\\n\\nez lefagyaszthatja a böngészőt, biztos?',\n\t\"u_scan\": 'Fájlok beolvasása...',\n\t\"u_dirstuck\": 'a rendszer elakadt az alábbi {0} elemnél; kihagyjuk őket:',\n\t\"u_etadone\": 'Kész ({0}, {1} fájl)',\n\t\"u_etaprep\": '(előkészítés...)',\n\t\"u_hashdone\": 'hashelés kész',\n\t\"u_hashing\": 'hash',\n\t\"u_hs\": 'kapcsolódás...',\n\t\"u_started\": 'a feltöltés elindult; lásd [🚀]',\n\t\"u_dupdefer\": 'másolat; a végén fogjuk feldolgozni',\n\t\"u_actx\": 'kattints ide, hogy a böngésző ne fogja vissza a sebességet,\\nha más ablakba/fülre mész',\n\t\"u_fixed\": 'Oké! Javítva 👍',\n\t\"u_cuerr\": 'hiba a(z) {0} / {1}. egység feltöltésekor;\\nvalószínűleg nem gond, folytatjuk\\n\\nfájl: {2}',\n\t\"u_cuerr2\": 'szerver elutasította az egységet ({0} / {1});\\nkésőbb újrapróbáljuk\\n\\nfájl: {2}\\n\\nhiba ',\n\t\"u_ehstmp\": 'újrapróbálás; lásd jobb lent',\n\t\"u_ehsfin\": 'szerver elutasította a befejezést; újrapróbáljuk...',\n\t\"u_ehssrch\": 'szerver elutasította a keresést; újrapróbáljuk...',\n\t\"u_ehsinit\": 'szerver elutasította a kezdést; újrapróbáljuk...',\n\t\"u_eneths\": 'hálózati hiba a kapcsolódáskor; újrapróbálás...',\n\t\"u_enethd\": 'hálózati hiba az ellenőrzéskor; újrapróbálás...',\n\t\"u_cbusy\": 'várakozás, hogy a szerver megint bízzon bennünk hiba után...',\n\t\"u_ehsdf\": 'a szerveren elfogyott a hely!\\n\\naddig próbálkozunk, amíg valaki\\nfel nem szabadít egy kis területet',\n\t\"u_emtleak1\": 'úgy tűnik, a böngésződ memóriát szivárogtat;\\nkérlek',\n\t\"u_emtleak2\": ' <a href=\"{0}\">válts https-re (ajánlott)</a> vagy ',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": 'próbáld a következőt:\\n<ul><li>nyomj <code>F5</code>-öt</li><li>kapcsold ki az &nbsp;<code>mt</code>&nbsp; gombot a &nbsp;<code>⚙️ beállításokban</code></li><li>majd próbáld újra</li></ul>Lassabb lesz, de legalább lemegy.\\nBocsi a kellemetlenségért!\\n\\nPS: a chrome v107 már <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">javított ezen</a>',\n\t\"u_emtleakf\": 'próbáld a következőt:\\n<ul><li>nyomj <code>F5</code>-öt</li><li>kapcsold be az <code>egyszerű</code> módot<li>majd próbáld újra megint</li></ul>\\nPS: remélhetőleg a firefox is <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">javítja egyszer</a>',\n\t\"u_s404\": 'nincs a szerveren',\n\t\"u_expl\": 'magyarázat',\n\t\"u_maxconn\": 'a legtöbb böngésző limitje 6, de Firefoxban az <code>about:config</code> alatt a <code>connections-per-server</code> értékkel növelhető',\n\t\"u_tu\": '<p class=\"warn\">FIGYELEM: turbo bekapcsolva, <span>&nbsp;lehet, hogy nem veszi észre a félbeszakadt feltöltéseket; lásd a súgót</span></p>',\n\t\"u_ts\": '<p class=\"warn\">FIGYELEM: turbo bekapcsolva, <span>&nbsp;a keresési eredmények hibásak lehetnek; lásd a súgót</span></p>',\n\t\"u_turbo_c\": 'turbo letiltva a szerveren',\n\t\"u_turbo_g\": 'turbo kikapcsolva, mert nincs jogod mappalistázáshoz ezen a köteten',\n\t\"u_life_cfg\": 'autotörlés <input id=\"lifem\" p=\"60\" /> perc (vagy <input id=\"lifeh\" p=\"3600\" /> óra) múlva',\n\t\"u_life_est\": 'a feltöltés törlődni fog ekkor: <span id=\"lifew\" tt=\"helyi idő\">---</span>',\n\t\"u_life_max\": 'ez a mappa kényszeríti a\\nmax élettartamot: {0}',\n\t\"u_unp_ok\": 'unpost engedélyezve eddig: {0}',\n\t\"u_unp_ng\": 'unpost NEM lesz engedélyezve',\n\t\"ue_ro\": 'ebben a mappában csak olvasási jogod van\\n\\n',\n\t\"ue_nl\": 'nem vagy belépve',\n\t\"ue_la\": 'jelenleg mint “{0}” vagy bejelentkezve',\n\t\"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',\n\t\"ue_ta\": 'próbáld meg újra, most már mennie kell',\n\t\"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',\n\t\"ur_1uo\": 'OK: Fájl sikeresen feltöltve',\n\t\"ur_auo\": 'OK: Mind a(z) {0} fájl feltöltve',\n\t\"ur_1so\": 'OK: Fájl megvan a szerveren',\n\t\"ur_aso\": 'OK: Mind a(z) {0} fájl megvan a szerveren',\n\t\"ur_1un\": 'A feltöltés nem sikerült',\n\t\"ur_aun\": 'Az összes ({0}) feltöltés megszakadt',\n\t\"ur_1sn\": 'Fájl nincs a szerveren',\n\t\"ur_asn\": 'A(z) {0} fájl nincs a szerveren',\n\t\"ur_um\": 'Kész;\\n{0} sikeres,\\n{1} sajnos nem sikerült',\n\t\"ur_sm\": 'Kész;\\n{0} megvan a szerveren,\\n{1} nincs meg',\n\n\t\"rc_opn\": 'megnyitás',\n\t\"rc_ply\": 'lejátszás',\n\t\"rc_pla\": 'lejátszás audióként',\n\t\"rc_txt\": 'megnyitás szövegként',\n\t\"rc_md\": 'megnyitás markdownként',\n\t\"rc_dl\": 'letöltés',\n\t\"rc_zip\": 'letöltés archívumként',\n\t\"rc_cpl\": 'link másolása',\n\t\"rc_del\": 'törlés',\n\t\"rc_cut\": 'kivágás',\n\t\"rc_cpy\": 'másolás',\n\t\"rc_pst\": 'beillesztés',\n\t\"rc_rnm\": 'átnevezés',\n\t\"rc_nfo\": 'új mappa',\n\t\"rc_nfi\": 'új fájl',\n\t\"rc_sal\": 'összes kijelölése',\n\t\"rc_sin\": 'kijelölés megfordítása',\n\t\"rc_shf\": 'mappa megosztása',\n\t\"rc_shs\": 'kijelöltek megosztása',\n\n\t\"lang_set\": 'frissítsünk, hogy átálljon a nyelv?',\n\n\t\"splash\": {\n\t\t\"a1\": 'frissítés',\n\t\t\"b1\": 'üdv idegen &nbsp; <small>(nem vagy bejelentkezve)</small>',\n\t\t\"c1\": 'kilépés',\n\t\t\"d1\": 'dump stack',\n\t\t\"d2\": 'minden aktív szál állapotának mutatása',\n\t\t\"e1\": 'config újratöltés',\n\t\t\"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',\n\t\t\"f1\": 'böngészhető:',\n\t\t\"g1\": 'ide tudsz feltölteni:',\n\t\t\"cc1\": 'egyéb dolgok:',\n\t\t\"h1\": 'k304 kikapcsolása',\n\t\t\"i1\": 'k304 bekapcsolása',\n\t\t\"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',\n\t\t\"k1\": 'beállítások alaphelyzetbe állítása',\n\t\t\"l1\": 'jelentkezz be a többiért:',\n\t\t\"ls3\": 'belépés',\n\t\t\"lu4\": 'felhasználónév',\n\t\t\"lp4\": 'jelszó',\n\t\t\"lo3\": '“{0}” kijelentkeztetése mindenhonnan',\n\t\t\"lo2\": 'minden böngészőben lelövi a munkamenetet',\n\t\t\"m1\": 'üdv újra,',\n\t\t\"n1\": '404 nincs meg &nbsp;┐( ´ -`)┌',\n\t\t\"o1\": 'vagy nincs jogod -- próbálj belépni vagy <a href=\"' + SR + '/?h\">menj a főoldalra</a>',\n\t\t\"p1\": '403 tiltva &nbsp;~┻━┻',\n\t\t\"q1\": 'kell egy jelszó vagy <a href=\"' + SR + '/?h\">menj a főoldalra</a>',\n\t\t\"r1\": 'vissza a főoldalra',\n\t\t\".s1\": 'újraszkennelés',\n\t\t\"t1\": 'művelet',\n\t\t\"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',\n\t\t\"v1\": 'csatlakozás',\n\t\t\"v2\": 'szerver használata helyi meghajtóként',\n\t\t\"w1\": 'váltás https-re',\n\t\t\"x1\": 'jelszó megváltoztatása',\n\t\t\"y1\": 'megosztások szerkesztése',\n\t\t\"z1\": 'megosztás feloldása:',\n\t\t\"ta1\": 'írd be az új jelszavad',\n\t\t\"ta2\": 'új jelszó még egyszer:',\n\t\t\"ta3\": 'elírtad; próbáld újra',\n\t\t\"nop\": 'HIBA: A jelszó nem lehet üres',\n\t\t\"nou\": 'HIBA: A felhasználónév és jelszó nem lehet üres',\n\t\t\"aa1\": 'érkező fájlok:',\n\t\t\"ab1\": 'no304 kikapcsolása',\n\t\t\"ac1\": 'no304 bekapcsolása',\n\t\t\"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!',\n\t\t\"ae1\": 'futó letöltések:',\n\t\t\"af1\": 'utóbbi feltöltések mutatása',\n\t\t\"ag1\": 'idp cache megtekintése',\n\t},\n};\n"
  },
  {
    "path": "copyparty/web/tl/ita.js",
    "content": "\n// Le righe che terminano con //m sono traduzioni automatiche non verificate\n\nLs.ita = {\n\t\"tt\": \"Italiano\",\n\n\t\"cols\": {\n\t\t\"c\": \"pulsanti azione\",\n\t\t\"dur\": \"durata\",\n\t\t\"q\": \"qualità / bitrate\",\n\t\t\"Ac\": \"codec audio\",\n\t\t\"Vc\": \"codec video\",\n\t\t\"Fmt\": \"formato / container\",\n\t\t\"Ahash\": \"checksum audio\",\n\t\t\"Vhash\": \"checksum video\",\n\t\t\"Res\": \"risoluzione\",\n\t\t\"T\": \"tipo file\",\n\t\t\"aq\": \"qualità audio / bitrate\",\n\t\t\"vq\": \"qualità video / bitrate\",\n\t\t\"pixfmt\": \"subsampling / struttura pixel\",\n\t\t\"resw\": \"risoluzione orizzontale\",\n\t\t\"resh\": \"risoluzione verticale\",\n\t\t\"chs\": \"canali audio\",\n\t\t\"hz\": \"frequenza di campionamento\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"varie\",\n\t\t\t[\"ESC\", \"chiudi vari elementi\"],\n\n\t\t\t\"file-manager\",\n\t\t\t[\"G\", \"alterna vista lista / griglia\"],\n\t\t\t[\"T\", \"alterna miniature / icone\"],\n\t\t\t[\"⇧ A/D\", \"dimensione miniature\"],\n\t\t\t[\"ctrl-K\", \"elimina selezionati\"],\n\t\t\t[\"ctrl-X\", \"taglia selezione negli appunti\"],\n\t\t\t[\"ctrl-C\", \"copia selezione negli appunti\"],\n\t\t\t[\"ctrl-V\", \"incolla (sposta/copia) qui\"],\n\t\t\t[\"Y\", \"scarica selezionati\"],\n\t\t\t[\"F2\", \"rinomina selezionati\"],\n\n\t\t\t\"file-list-sel\",\n\t\t\t[\"spazio\", \"alterna selezione file\"],\n\t\t\t[\"↑/↓\", \"sposta cursore selezione\"],\n\t\t\t[\"ctrl ↑/↓\", \"sposta cursore e viewport\"],\n\t\t\t[\"⇧ ↑/↓\", \"seleziona file prec/succ\"],\n\t\t\t[\"ctrl-A\", \"seleziona tutti i file / cartelle\"],\n\t\t], [\n\t\t\t\"navigation\",\n\t\t\t[\"B\", \"alterna breadcrumb / pannello nav\"],\n\t\t\t[\"I/K\", \"cartella prec/succ\"],\n\t\t\t[\"M\", \"cartella genitore (o comprimi corrente)\"],\n\t\t\t[\"V\", \"alterna cartelle / file di testo nel pannello nav\"],\n\t\t\t[\"A/D\", \"dimensione pannello nav\"],\n\t\t], [\n\t\t\t\"audio-player\",\n\t\t\t[\"J/L\", \"brano prec/succ\"],\n\t\t\t[\"U/O\", \"salta 10sec indietro/avanti\"],\n\t\t\t[\"0..9\", \"salta a 0%..90%\"],\n\t\t\t[\"P\", \"play/pausa (avvia anche)\"],\n\t\t\t[\"S\", \"seleziona brano in riproduzione\"],\n\t\t\t[\"Y\", \"scarica brano\"],\n\t\t], [\n\t\t\t\"image-viewer\",\n\t\t\t[\"J/L, ←/→\", \"immagine prec/succ\"],\n\t\t\t[\"Home/End\", \"prima/ultima immagine\"],\n\t\t\t[\"F\", \"schermo intero\"],\n\t\t\t[\"R\", \"ruota in senso orario\"],\n\t\t\t[\"⇧ R\", \"ruota in senso antiorario\"],\n\t\t\t[\"S\", \"seleziona immagine\"],\n\t\t\t[\"Y\", \"scarica immagine\"],\n\t\t], [\n\t\t\t\"video.player\",\n\t\t\t[\"U/O\", \"salta 10sec indietro/avanti\"],\n\t\t\t[\"P/K/Spazio\", \"play/pausa\"],\n\t\t\t[\"C\", \"continua riproduzione successivo\"],\n\t\t\t[\"V\", \"loop\"],\n\t\t\t[\"M\", \"muto\"],\n\t\t\t[\"[ e ]\", \"imposta intervallo loop\"],\n\t\t], [\n\t\t\t\"textfile-viewer\",\n\t\t\t[\"I/K\", \"file prec/succ\"],\n\t\t\t[\"M\", \"chiudi file di testo\"],\n\t\t\t[\"E\", \"modifica file di testo\"],\n\t\t\t[\"S\", \"seleziona file (per taglia/copia/rinomina)\"],\n\t\t\t[\"Y\", \"scarica il file di testo\"], //m\n\t\t\t[\"⇧ J\", \"abbellire json\"], //m\n\t\t]\n\t],\n\n\t\"m_ok\": \"OK\",\n\t\"m_ng\": \"Annulla\",\n\n\t\"enable\": \"Abilita\",\n\t\"danger\": \"PERICOLO\",\n\t\"clipped\": \"copiato negli appunti\",\n\n\t\"ht_s1\": \"secondo\",\n\t\"ht_s2\": \"secondi\",\n\t\"ht_m1\": \"minuto\",\n\t\"ht_m2\": \"minuti\",\n\t\"ht_h1\": \"ora\",\n\t\"ht_h2\": \"ore\",\n\t\"ht_d1\": \"giorno\",\n\t\"ht_d2\": \"giorni\",\n\t\"ht_and\": \" e \",\n\n\t\"goh\": \"control-panel\",\n\t\"gop\": 'cartella sorella precedente\">prec',\n\t\"gou\": 'cartella genitore\">su',\n\t\"gon\": 'prossima cartella\">succ',\n\t\"logout\": \"Logout \",\n\t\"login\": \"Accedi\", //m\n\t\"access\": \" accesso\",\n\t\"ot_close\": \"chiudi sottomenu\",\n\t\"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`&quot;try unite&quot;` = contiene esattamente «try unite»$N$Nil formato data è iso-8601, come$N`2009-12-31` o `2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"unpost: elimina i tuoi caricamenti recenti, o interrompi quelli non completati\",\n\t\"ot_bup\": \"bup: uploader di base, supporta anche netscape 4.0\",\n\t\"ot_mkdir\": \"mkdir: crea una nuova directory\",\n\t\"ot_md\": \"new-file: crea un nuovo file di testo\", //m\n\t\"ot_msg\": \"msg: invia un messaggio al log del server\",\n\t\"ot_mp\": \"opzioni lettore multimediale\",\n\t\"ot_cfg\": \"opzioni di configurazione\",\n\t\"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 [🎈]&nbsp; (l\\'uploader di base)<br /><br />durante i caricamenti, questa icona diventa un indicatore di progresso!',\n\t\"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 [🎈]&nbsp; (l\\'uploader di base)<br /><br />durante i caricamenti, questa icona diventa un indicatore di progresso!',\n\t\"ot_noie\": 'Perfavore usa Chrome / Firefox / Edge',\n\n\t\"ab_mkdir\": \"crea directory\",\n\t\"ab_mkdoc\": \"nuovo file di testo\", //m\n\t\"ab_msg\": \"invia msg al log srv\",\n\n\t\"ay_path\": \"salta alle cartelle\",\n\t\"ay_files\": \"salta ai file\",\n\n\t\"wt_ren\": \"rinomina elementi selezionati$NTasto rapido: F2\",\n\t\"wt_del\": \"elimina elementi selezionati$NTasto rapido: ctrl-K\",\n\t\"wt_cut\": \"taglia elementi selezionati &lt;small&gt;(poi incolla altrove)&lt;/small&gt;$NTasto rapido: ctrl-X\",\n\t\"wt_cpy\": \"copia elementi selezionati negli appunti$N(per incollarli altrove)$NTasto rapido: ctrl-C\",\n\t\"wt_pst\": \"incolla una selezione precedentemente tagliata / copiata$NTasto rapido: ctrl-V\",\n\t\"wt_selall\": \"seleziona tutti i file$NTasto rapido: ctrl-A (quando il file è focalizzato)\",\n\t\"wt_selinv\": \"inverti selezione\",\n\t\"wt_zip1\": \"scarica questa cartella come archivio\",\n\t\"wt_selzip\": \"scarica selezione come archivio\",\n\t\"wt_seldl\": \"scarica selezione come file separati$NTasto rapido: Y\",\n\t\"wt_npirc\": \"copia info traccia formato irc\",\n\t\"wt_nptxt\": \"copia info traccia testo semplice\",\n\t\"wt_m3ua\": \"aggiungi alla playlist m3u (clicca <code>📻copia</code> dopo)\",\n\t\"wt_m3uc\": \"copia playlist m3u negli appunti\",\n\t\"wt_grid\": \"alterna vista griglia / lista$NTasto rapido: G\",\n\t\"wt_prev\": \"traccia precedente$NTasto rapido: J\",\n\t\"wt_play\": \"play / pausa$NTasto rapido: P\",\n\t\"wt_next\": \"traccia successiva$NTasto rapido: L\",\n\n\t\"ul_par\": \"caricamenti paralleli:\",\n\t\"ut_rand\": \"randomizza nomi file\",\n\t\"ut_u2ts\": \"copia il timestamp di ultima modifica$Ndal tuo filesystem al server\\\">📅\",\n\t\"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\n\t\"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\",\n\t\"ut_ask\": 'chiedi conferma prima che inizi il caricamento\">💭',\n\t\"ut_pot\": \"migliora la velocità di caricamento su dispositivi lenti$Nrendendo l'interfaccia meno complessa\",\n\t\"ut_srch\": \"non caricare realmente, invece controlla se i file esistono già $N sul server (scansionerà tutte le cartelle che puoi leggere)\",\n\t\"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\",\n\t\"ul_btn\": \"trascina file / cartelle<br>qui (o cliccami)\",\n\t\"ul_btnu\": \"C A R I C A\",\n\t\"ul_btns\": \"C E R C A\",\n\n\t\"ul_hash\": \"hash\",\n\t\"ul_send\": \"invia\",\n\t\"ul_done\": \"fatto\",\n\t\"ul_idle1\": \"nessun caricamento ancora in coda\",\n\t\"ut_etah\": \"velocità media di &lt;em&gt;hashing&lt;/em&gt;, e tempo stimato al completamento\",\n\t\"ut_etau\": \"velocità media di &lt;em&gt;caricamento&lt;/em&gt; e tempo stimato al completamento\",\n\t\"ut_etat\": \"velocità &lt;em&gt;totale&lt;/em&gt; media e tempo stimato al completamento\",\n\n\t\"uct_ok\": \"completato con successo\",\n\t\"uct_ng\": \"non-valido: fallito / rifiutato / non-trovato\",\n\t\"uct_done\": \"ok e ng combinati\",\n\t\"uct_bz\": \"hashing o caricamento\",\n\t\"uct_q\": \"inattivo, in attesa\",\n\n\t\"utl_name\": \"nome file\",\n\t\"utl_ulist\": \"lista\",\n\t\"utl_ucopy\": \"copia\",\n\t\"utl_links\": \"link\",\n\t\"utl_stat\": \"stato\",\n\t\"utl_prog\": \"progresso\",\n\n\t// keep short:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"ERRORE\",\n\t\"utl_oserr\": \"Errore-SO\",\n\t\"utl_found\": \"trovato\",\n\t\"utl_defer\": \"rinvia\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"finito\",\n\n\t\"ul_flagblk\": \"i file sono stati aggiunti alla coda</b><br>tuttavia c'è un up2k occupato in un'altra scheda del browser,<br>quindi aspetto che quello finisca prima\",\n\t\"ul_btnlk\": \"la configurazione del server ha bloccato questo interruttore in questo stato\",\n\n\t\"udt_up\": \"Carica\",\n\t\"udt_srch\": \"Cerca\",\n\t\"udt_drop\": \"lascialo qui\",\n\n\t\"u_nav_m\": '<h6>ok, cosa hai?</h6><code>Invio</code> = File (uno o più)\\n<code>ESC</code> = Una cartella (incluse sottocartelle)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">File</a><a href=\"#\" id=\"modal-ng\">Una cartella</a>',\n\n\t\"cl_opts\": \"opzioni\",\n\t\"cl_hfsz\": \"dimensione file\", //m\n\t\"cl_themes\": \"tema\",\n\t\"cl_langs\": \"lingua\",\n\t\"cl_ziptype\": \"download cartella\",\n\t\"cl_uopts\": \"opzioni up2k\",\n\t\"cl_favico\": \"favicon\",\n\t\"cl_bigdir\": \"cartelle grandi\",\n\t\"cl_hsort\": \"#ordinamento\",\n\t\"cl_keytype\": \"notazione tasti\",\n\t\"cl_hiddenc\": \"colonne nascoste\",\n\t\"cl_hidec\": \"nascondi\",\n\t\"cl_reset\": \"reset\",\n\t\"cl_hpick\": \"tocca le intestazioni delle colonne per nascondere nella tabella sottostante\",\n\t\"cl_hcancel\": \"nascondere colonne annullato\",\n\t\"cl_rcm\": \"menu contestuale\", //m\n\n\t\"ct_grid\": '田 griglia',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ tooltip',\n\t\"ct_thumb\": 'nella vista griglia, alterna icone o miniature$NTasto rapido: T\">🖼️ miniature',\n\t\"ct_csel\": 'usa CTRL e SHIFT per la selezione file nella vista griglia\">sel',\n\t\"ct_dsel\": 'usa la selezione tramite trascinamento nella vista griglia\">trascina', //m\n\t\"ct_dl\": 'forza il download (non visualizzare inline) quando si clicca su un file\">dl', //m\n\t\"ct_ihop\": 'quando il visualizzatore immagini è chiuso, scorri fino all\\'ultimo file visualizzato\">g⮯',\n\t\"ct_dots\": 'mostra file nascosti (se il server lo permette)\">dotfile',\n\t\"ct_qdel\": 'quando elimini file, chiedi conferma solo una volta\">qdel',\n\t\"ct_dir1st\": 'ordina cartelle prima dei file\">📁 prima',\n\t\"ct_nsort\": 'ordinamento naturale (per nomi file con cifre iniziali)\">nsort',\n\t\"ct_utc\": 'mostra tutte le date/ore in UTC\">UTC',\n\t\"ct_readme\": 'mostra README.md negli elenchi cartelle\">📜 readme',\n\t\"ct_idxh\": 'mostra index.html invece dell\\'elenco cartelle\">htm',\n\t\"ct_sbars\": 'mostra barre di scorrimento\">⟊',\n\n\t\"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📅\",\n\n\t\"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 <em>&quot;questo ha la stessa dimensione file sul server?&quot;</em> quindi se il contenuto del file è diverso NON verrà caricato$N$NDovresti spegnere questo quando il caricamento è finito, e poi &quot;caricare&quot; di nuovo gli stessi file per far verificare al client\\\">turbo\",\n\n\t\"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 <em>teoricamente</em> 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\",\n\n\t\"cut_u2sz\": \"dimensione (in MiB) di ogni chunk di caricamento; valori grandi volano meglio attraverso l'atlantico. Prova valori bassi su connessioni molto inaffidabili\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cut_nag\": \"notifica SO quando il caricamento si completa$N(solo se il browser o la scheda non è attiva)\",\n\t\"cut_sfx\": \"allarme sonoro quando il caricamento si completa$N(solo se il browser o la scheda non è attiva)\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cft_text\": \"testo favicon (vuoto e aggiorna per disabilitare)\",\n\t\"cft_fg\": \"colore primo piano\",\n\t\"cft_bg\": \"colore sfondo\",\n\n\t\"cdt_lim\": \"numero massimo di file da mostrare in una cartella\",\n\t\"cdt_ask\": \"quando scorri verso il fondo,$Ninvece di caricare più file,$Nchiedi cosa fare\",\n\t\"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\",\n\t\"cdt_ren\": \"abilita il menu contestuale personalizzato, il menu normale è accessibile con shift + clic destro\\\">abilita\", //m\n\t\"cdt_rdb\": \"mostra il menu normale con il tasto destro quando quello personalizzato è già aperto e si clicca di nuovo\\\">x2\", //m\n\n\t\"tt_entree\": \"mostra pannello nav (barra laterale albero directory)$NTasto rapido: B\",\n\t\"tt_detree\": \"mostra breadcrumb$NTasto rapido: B\",\n\t\"tt_visdir\": \"scorri alla cartella selezionata\",\n\t\"tt_ftree\": \"alterna albero cartelle / file di testo$NTasto rapido: V\",\n\t\"tt_pdock\": \"mostra cartelle genitore in un pannello ancorato in alto\",\n\t\"tt_dynt\": \"crescita automatica mentre l'albero si espande\",\n\t\"tt_wrap\": \"a capo parola\",\n\t\"tt_hover\": \"rivela righe che traboccano al passaggio del mouse$N( interrompe lo scorrimento a meno che il cursore $N&nbsp; del mouse non sia nella grondaia sinistra )\",\n\n\t\"ml_pmode\": \"alla fine della cartella...\",\n\t\"ml_btns\": \"comandi\",\n\t\"ml_tcode\": \"transcodifica\",\n\t\"ml_tcode2\": \"transcodifica in\",\n\t\"ml_tint\": \"tinta\",\n\t\"ml_eq\": \"equalizzatore audio\",\n\t\"ml_drc\": \"compressore gamma dinamica\",\n\t\"ml_ss\": \"salta i silenzi\", //m\n\n\t\"mt_loop\": \"loop/ripeti una canzone\\\">🔁\",\n\t\"mt_one\": \"fermati dopo una canzone\\\">1️⃣\",\n\t\"mt_shuf\": \"mescola le canzoni in ogni cartella\\\">🔀\",\n\t\"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▶\",\n\t\"mt_preload\": \"inizia a caricare la prossima canzone verso la fine per riproduzione senza interruzioni\\\">preload\",\n\t\"mt_prescan\": \"vai alla prossima cartella prima che finisca l'ultima canzone$Nmantenendo felice il browser web$Ncosì non si ferma la riproduzione\\\">nav\",\n\t\"mt_fullpre\": \"prova a precaricare l'intera canzone;$N✅ abilita su connessioni <b>inaffidabili</b>,$N❌ <b>disabilita</b> su connessioni lente probabilmente\\\">full\",\n\t\"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)\\\">☕️\",\n\t\"mt_waves\": \"barra di ricerca forma d'onda:$Nmostra ampiezza audio nello scrubber\\\">~s\",\n\t\"mt_npclip\": \"mostra pulsanti per copiare negli appunti la canzone attualmente in riproduzione\\\">/np\",\n\t\"mt_m3u_c\": \"mostra pulsanti per copiare negli appunti le$Ncanzoni selezionate come voci playlist m3u8\\\">📻\",\n\t\"mt_octl\": \"integrazione so (tasti multimediali / osd)\\\">os-ctl\",\n\t\"mt_oseek\": \"permetti ricerca attraverso integrazione so$N$Nnota: su alcuni dispositivi (iPhone),$Nquesto sostituisce il pulsante canzone successiva\\\">seek\",\n\t\"mt_oscv\": \"mostra copertina album in osd\\\">art\",\n\t\"mt_follow\": \"mantieni la traccia in riproduzione scorrevole nella vista\\\">🎯\",\n\t\"mt_compact\": \"controlli compatti\\\">⟎\",\n\t\"mt_uncache\": \"pulisci cache &nbsp;(prova ad attivare se il tuo browser ha messo in cache$Nuna copia rotta di una canzone e si rifiuta di riprodurla)\\\">uncache\",\n\t\"mt_mloop\": \"loop della cartella aperta\\\">🔁 loop\",\n\t\"mt_mnext\": \"carica la prossima cartella e continua\\\">📂 succ\",\n\t\"mt_mstop\": \"ferma riproduzione\\\">⏸ stop\",\n\t\"mt_cflac\": \"converti flac / wav in {0}\\\">flac\",\n\t\"mt_caac\": \"converti aac / m4a in {0}\\\">aac\",\n\t\"mt_coth\": \"converti tutti gli altri (non mp3) in {0}\\\">oth\",\n\t\"mt_c2opus\": \"scelta migliore per desktop, laptop, android\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, per iOS 17.5 e più recenti\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, per iOS 11 fino a 17\\\">caf\",\n\t\"mt_c2mp3\": \"usa questo su dispositivi molto vecchi\\\">mp3\",\n\t\"mt_c2flac\": \"qualità audio migliore, ma download pesanti\\\">flac\", //m\n\t\"mt_c2wav\": \"riproduzione non compressa (ancora più grande)\\\">wav\", //m\n\t\"mt_c2ok\": \"bene, buona scelta\",\n\t\"mt_c2nd\": \"quello non è il formato di output raccomandato per il tuo dispositivo, ma va bene\",\n\t\"mt_c2ng\": \"il tuo dispositivo non sembra supportare questo formato di output, ma proviamo comunque\",\n\t\"mt_xowa\": \"ci sono bug in iOS che prevengono la riproduzione in background usando questo formato; usa caf o mp3 invece\",\n\t\"mt_tint\": \"livello sfondo (0-100) sulla barra di ricerca$Nper rendere il buffering meno distraente\",\n\t\"mt_eq\": \"`abilita l'equalizzatore e controllo guadagno;$N$Nboost `0` = volume standard 100% (non modificato)$N$Nwidth `1 &nbsp;` = stereo standard (non modificato)$Nwidth `0.5` = 50% crossfeed sinistra-destra$Nwidth `0 &nbsp;` = mono$N$Nboost `-0.8` &amp; 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\",\n\t\"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)\",\n\t\"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\n\t\"mt_ssvt\": \"soglia volume (0-255)\\\">vol\", //m\n\t\"mt_ssts\": \"soglia attiva (% traccia, inizio)\\\">ini\", //m\n\t\"mt_sste\": \"soglia attiva (% traccia, fine)\\\">fin\", //m\n\t\"mt_sssm\": \"moltiplicatore velocità riproduzione\\\">av\", //m\n\n\t\"mb_play\": \"riproduci\",\n\t\"mm_hashplay\": \"riprodurre questo file audio?\",\n\t\"mm_m3u\": \"premi <code>Invio/OK</code> per Riprodurre\\npremi <code>ESC/Annulla</code> per Modificare\",\n\t\"mp_breq\": \"serve firefox 82+ o chrome 73+ o iOS 15+\",\n\t\"mm_bload\": \"ora caricando...\",\n\t\"mm_bconv\": \"convertendo in {0}, attendi...\",\n\t\"mm_opusen\": \"il tuo browser non può riprodurre file aac / m4a;\\ntranscodifica in opus ora abilitata\",\n\t\"mm_playerr\": \"riproduzione fallita: \",\n\t\"mm_eabrt\": \"Il tentativo di riproduzione è stato cancellato\",\n\t\"mm_enet\": \"La tua connessione internet è instabile\",\n\t\"mm_edec\": \"Questo file è presumibilmente corrotto??\",\n\t\"mm_esupp\": \"Il tuo browser non capisce questo formato audio\",\n\t\"mm_eunk\": \"Errore Sconosciuto\",\n\t\"mm_e404\": \"Non è stato possibile riprodurre audio; errore 404: File non trovato.\",\n\t\"mm_e403\": \"Non è stato possibile riprodurre audio; errore 403: Accesso negato.\\n\\nProva a premere F5 per ricaricare, forse sei stato disconnesso\",\n\t\"mm_e415\": \"Non è stato possibile riprodurre audio; errore 415: Conversione del file non riuscita; controlla i log del server.\", //m\n\t\"mm_e500\": \"Non è stato possibile riprodurre audio; errore 500: Controlla i log del server.\",\n\t\"mm_e5xx\": \"Non è stato possibile riprodurre audio; errore server \",\n\t\"mm_nof\": \"non trovo altri file audio nelle vicinanze\",\n\t\"mm_prescan\": \"Cercando musica da riprodurre dopo...\",\n\t\"mm_scank\": \"Trovata la prossima canzone:\",\n\t\"mm_uncache\": \"cache pulita; tutte le canzoni si riscaricheranno alla prossima riproduzione\",\n\t\"mm_hnf\": \"quella canzone non esiste più\",\n\n\t\"im_hnf\": \"quell'immagine non esiste più\",\n\n\t\"f_empty\": 'questa cartella è vuota',\n\t\"f_chide\": 'questo nasconderà la colonna «{0}»\\n\\npuoi mostrare le colonne nella scheda impostazioni',\n\t\"f_bigtxt\": \"questo file è {0} MiB grande -- visualizzare davvero come testo?\",\n\t\"f_bigtxt2\": \"visualizzare solo la fine del file invece? questo abiliterà anche following/tailing, mostrando righe di testo appena aggiunte in tempo reale\",\n\t\"fbd_more\": '<div id=\"blazy\">mostrando <code>{0}</code> di <code>{1}</code> file; <a href=\"#\" id=\"bd_more\">mostra {2}</a> o <a href=\"#\" id=\"bd_all\">mostra tutti</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\">mostrando <code>{0}</code> di <code>{1}</code> file; <a href=\"#\" id=\"bd_all\">mostra tutti</a></div>',\n\t\"f_anota\": \"solo {0} dei {1} elementi sono stati selezionati;\\nper selezionare l'intera cartella, prima scorri fino in fondo\",\n\n\t\"f_dls\": 'i link dei file nella cartella corrente sono stati\\ncambiati in link di download',\n\t\"f_dl_nd\": 'cartella ignorata (usa invece il download zip/tar):\\n', //m\n\n\t\"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 <code>.PARTIAL</code>. Premi ANNULLA o Escape per farlo.\\n\\nPremendo OK / Invio ignorerai questo avviso e continuerai a scaricare il file <code>.PARTIAL</code> scratch, che quasi sicuramente ti darà dati corrotti.\",\n\n\t\"ft_paste\": \"incolla {0} elementi$NTasto rapido: ctrl-V\",\n\t\"fr_eperm\": 'impossibile rinominare:\\nnon hai il permesso “sposta” in questa cartella',\n\t\"fd_eperm\": 'impossibile eliminare:\\nnon hai il permesso “elimina” in questa cartella',\n\t\"fc_eperm\": 'impossibile tagliare:\\nnon hai il permesso “sposta” in questa cartella',\n\t\"fp_eperm\": 'impossibile incollare:\\nnon hai il permesso “scrivi” in questa cartella',\n\t\"fr_emore\": \"seleziona almeno un elemento da rinominare\",\n\t\"fd_emore\": \"seleziona almeno un elemento da eliminare\",\n\t\"fc_emore\": \"seleziona almeno un elemento da tagliare\",\n\t\"fcp_emore\": \"seleziona almeno un elemento da copiare negli appunti\",\n\n\t\"fs_sc\": \"condividi la cartella in cui ti trovi\",\n\t\"fs_ss\": \"condividi i file selezionati\",\n\t\"fs_just1d\": \"non puoi selezionare più di una cartella,\\no mescolare file e cartelle in una selezione\",\n\t\"fs_abrt\": \"❌ interrompi\",\n\t\"fs_rand\": \"🎲 nome.casuale\",\n\t\"fs_go\": \"✅ crea condivisione\",\n\t\"fs_name\": \"nome\",\n\t\"fs_src\": \"sorgente\",\n\t\"fs_pwd\": \"password\",\n\t\"fs_exp\": \"scadenza\",\n\t\"fs_tmin\": \"min\",\n\t\"fs_thrs\": \"ore\",\n\t\"fs_tdays\": \"giorni\",\n\t\"fs_never\": \"eterno\",\n\t\"fs_pname\": \"nome link opzionale; sarà casuale se vuoto\",\n\t\"fs_tsrc\": \"il file o cartella da condividere\",\n\t\"fs_ppwd\": \"password opzionale\",\n\t\"fs_w8\": \"creando condivisione...\",\n\t\"fs_ok\": \"premi <code>Invio/OK</code> per Appunti\\npremi <code>ESC/Annulla</code> per Chiudere\",\n\n\t\"frt_dec\": \"può risolvere alcuni casi di nomi file corrotti\\\">url-decode\",\n\t\"frt_rst\": \"ripristina nomi file modificati a quelli originali\\\">↺ reset\",\n\t\"frt_abrt\": \"interrompi e chiudi questa finestra\\\">❌ annulla\",\n\t\"frb_apply\": \"APPLICA RINOMINA\",\n\t\"fr_adv\": \"rinomina batch / metadata / pattern\\\">avanzato\",\n\t\"fr_case\": \"regex case-sensitive\\\">maiusc\",\n\t\"fr_win\": \"nomi sicuri per windows; sostituisce <code>&lt;&gt;:&quot;\\\\|?*</code> con caratteri giapponesi fullwidth\\\">win\",\n\t\"fr_slash\": \"sostituisce <code>/</code> con un carattere che non causa la creazione di nuove cartelle\\\">no /\",\n\t\"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\",\n\t\"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\",\n\t\"fr_pdel\": \"elimina\",\n\t\"fr_pnew\": \"salva come\",\n\t\"fr_pname\": \"fornisci un nome per il tuo nuovo preset\",\n\t\"fr_aborted\": \"interrotto\",\n\t\"fr_lold\": \"nome vecchio\",\n\t\"fr_lnew\": \"nome nuovo\",\n\t\"fr_tags\": \"tag per i file selezionati (sola lettura, solo per riferimento):\",\n\t\"fr_busy\": \"rinominando {0} elementi...\\n\\n{1}\",\n\t\"fr_efail\": \"rinomina fallita:\\n\",\n\t\"fr_nchg\": \"{0} dei nuovi nomi sono stati alterati a causa di <code>win</code> e/o <code>no /</code>\\n\\nOK per continuare con questi nuovi nomi alterati?\",\n\n\t\"fd_ok\": \"eliminazione OK\",\n\t\"fd_err\": \"eliminazione fallita:\\n\",\n\t\"fd_none\": \"niente è stato eliminato; forse bloccato dalla configurazione server (xbd)?\",\n\t\"fd_busy\": \"eliminando {0} elementi...\\n\\n{1}\",\n\t\"fd_warn1\": \"ELIMINARE questi {0} elementi?\",\n\t\"fd_warn2\": \"<b>Ultima possibilità!</b> Nessun modo per annullare. Eliminare?\",\n\n\t\"fc_ok\": \"tagliati {0} elementi\",\n\t\"fc_warn\": 'tagliati {0} elementi\\n\\nma: solo <b>questa</b> scheda-browser può incollarli\\n(dato che la selezione è così assolutamente massiva)',\n\n\t\"fcc_ok\": \"copiati {0} elementi negli appunti\",\n\t\"fcc_warn\": 'copiati {0} elementi negli appunti\\n\\nma: solo <b>questa</b> scheda-browser può incollarli\\n(dato che la selezione è così assolutamente massiva)',\n\n\t\"fp_apply\": \"usa questi nomi\",\n\t\"fp_skip\": \"salta conflitti\", //m\n\t\"fp_ecut\": \"prima taglia o copia alcuni file / cartelle da incollare / spostare\\n\\nnota: puoi tagliare / incollare attraverso diverse schede del browser\",\n\t\"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\n\t\"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\n\t\"fp_emore\": \"ci sono ancora alcune collisioni di nomi file rimaste da risolvere\",\n\t\"fp_ok\": \"spostamento OK\",\n\t\"fcp_ok\": \"copia OK\",\n\t\"fp_busy\": \"spostando {0} elementi...\\n\\n{1}\",\n\t\"fcp_busy\": \"copiando {0} elementi...\\n\\n{1}\",\n\t\"fp_abrt\": \"annullamento in corso...\", //m\n\t\"fp_err\": \"spostamento fallito:\\n\",\n\t\"fcp_err\": \"copia fallita:\\n\",\n\t\"fp_confirm\": \"spostare questi {0} elementi qui?\",\n\t\"fcp_confirm\": \"copiare questi {0} elementi qui?\",\n\t\"fp_etab\": 'fallito leggere appunti da altra scheda browser',\n\t\"fp_name\": \"caricando un file dal tuo dispositivo. Dagli un nome:\",\n\t\"fp_both_m\": '<h6>scegli cosa incollare</h6><code>Invio</code> = Sposta {0} file da «{1}»\\n<code>ESC</code> = Carica {2} file dal tuo dispositivo',\n\t\"fcp_both_m\": '<h6>scegli cosa incollare</h6><code>Invio</code> = Copia {0} file da «{1}»\\n<code>ESC</code> = Carica {2} file dal tuo dispositivo',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Sposta</a><a href=\"#\" id=\"modal-ng\">Carica</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Copia</a><a href=\"#\" id=\"modal-ng\">Carica</a>',\n\n\t\"mk_noname\": \"scrivi un nome nel campo di testo a sinistra prima di farlo :p\",\n\t\"nmd_i1\": \"puoi anche aggiungere l’estensione che vuoi, per esempio <code>.md</code>\", //m\n\t\"nmd_i2\": \"puoi creare solo file <code>.{0}</code> perché non hai il permesso di eliminare\", //m\n\n\t\"tv_load\": \"Caricando documento di testo:\\n\\n{0}\\n\\n{1}% ({2} di {3} MiB caricati)\",\n\t\"tv_xe1\": \"impossibile caricare file di testo:\\n\\nerrore \",\n\t\"tv_xe2\": \"404, file non trovato\",\n\t\"tv_lst\": \"lista di file di testo in\",\n\t\"tvt_close\": \"torna alla vista cartella$NTasto rapido: M (o Esc)\\\">❌ chiudi\",\n\t\"tvt_dl\": \"scarica questo file$NTasto rapido: Y\\\">💾 scarica\",\n\t\"tvt_prev\": \"mostra documento precedente$NTasto rapido: i\\\">⬆ prec\",\n\t\"tvt_next\": \"mostra documento successivo$NTasto rapido: K\\\">⬇ succ\",\n\t\"tvt_sel\": \"seleziona file &nbsp; ( per taglia / copia / elimina / ... )$NTasto rapido: S\\\">sel\",\n\t\"tvt_j\": \"abbellire json$NTasto rapido: shift-J\\\">j\", //m\n\t\"tvt_edit\": \"apri file nell'editor di testo$NTasto rapido: E\\\">✏️ modifica\",\n\t\"tvt_tail\": \"monitora file per cambiamenti; mostra nuove righe in tempo reale\\\">📡 segui\",\n\t\"tvt_wrap\": \"a capo parola\\\">↵\",\n\t\"tvt_atail\": \"blocca scorrimento in fondo alla pagina\\\">⚓\",\n\t\"tvt_ctail\": \"decodifica colori terminale (codici escape ansi)\\\">🌈\",\n\t\"tvt_ntail\": \"limite scrollback (quanti byte di testo mantenere caricati)\",\n\n\t\"m3u_add1\": \"canzone aggiunta alla playlist m3u\",\n\t\"m3u_addn\": \"{0} canzoni aggiunte alla playlist m3u\",\n\t\"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\",\n\n\t\"gt_vau\": \"non mostrare video, riproduci solo l'audio\\\">🎧\",\n\t\"gt_msel\": \"abilita selezione file; ctrl-click un file per sovrascrivere$N$N&lt;em&gt;quando attivo: doppio-click un file / cartella per aprirlo&lt;/em&gt;$N$NTasto rapido: S\\\">multiselezione\",\n\t\"gt_crop\": \"ritaglia miniature al centro\\\">ritaglia\",\n\t\"gt_3x\": \"miniature hi-res\\\">3x\",\n\t\"gt_zoom\": \"zoom\",\n\t\"gt_chop\": \"taglia\",\n\t\"gt_sort\": \"ordina per\",\n\t\"gt_name\": \"nome\",\n\t\"gt_sz\": \"dimensione\",\n\t\"gt_ts\": \"data\",\n\t\"gt_ext\": \"tipo\",\n\t\"gt_c1\": \"tronca nomi file di più (mostra meno)\",\n\t\"gt_c2\": \"tronca nomi file di meno (mostra di più)\",\n\n\t\"sm_w8\": \"cercando...\",\n\t\"sm_prev\": \"i risultati di ricerca qui sotto sono da una query precedente:\\n  \",\n\t\"sl_close\": \"chiudi risultati ricerca\",\n\t\"sl_hits\": \"mostrando {0} risultati\",\n\t\"sl_moar\": \"carica altro\",\n\n\t\"s_sz\": \"dimensione\",\n\t\"s_dt\": \"data\",\n\t\"s_rd\": \"percorso\",\n\t\"s_fn\": \"nome\",\n\t\"s_ta\": \"tag\",\n\t\"s_ua\": \"car@\",\n\t\"s_ad\": \"avanz.\",\n\t\"s_s1\": \"MiB minimo\",\n\t\"s_s2\": \"MiB massimo\",\n\t\"s_d1\": \"iso8601 min.\",\n\t\"s_d2\": \"iso8601 max.\",\n\t\"s_u1\": \"caricato dopo\",\n\t\"s_u2\": \"e/o prima\",\n\t\"s_r1\": \"percorso contiene &nbsp; (separato da spazi)\",\n\t\"s_f1\": \"nome contiene &nbsp; (nega con -nope)\",\n\t\"s_t1\": \"tag contiene &nbsp; (^=inizio, fine=$)\",\n\t\"s_a1\": \"proprietà metadata specifiche\",\n\n\t\"md_eshow\": \"impossibile renderizzare \",\n\t\"md_off\": \"[📜<em>readme</em>] disabilitato in [⚙️] -- documento nascosto\",\n\n\t\"badreply\": \"Fallito nel parsare risposta dal server\",\n\n\t\"xhr403\": \"403: Accesso negato\\n\\nprova a premere F5, forse sei stato disconnesso\",\n\t\"xhr0\": \"sconosciuto (probabilmente persa connessione al server, o server offline)\",\n\t\"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\",\n\t\"tl_xe1\": \"impossibile elencare sottocartelle:\\n\\nerrore \",\n\t\"tl_xe2\": \"404: Cartella non trovata\",\n\t\"fl_xe1\": \"impossibile elencare file nella cartella:\\n\\nerrore \",\n\t\"fl_xe2\": \"404: Cartella non trovata\",\n\t\"fd_xe1\": \"impossibile creare sottocartella:\\n\\nerrore \",\n\t\"fd_xe2\": \"404: Cartella genitore non trovata\",\n\t\"fsm_xe1\": \"impossibile inviare messaggio:\\n\\nerrore \",\n\t\"fsm_xe2\": \"404: Cartella genitore non trovata\",\n\t\"fu_xe1\": \"fcaricamento fallito per la lista unpost dal server:\\n\\nerrore \",\n\t\"fu_xe2\": \"404: File non trovato??\",\n\n\t\"fz_tar\": \"file gnu-tar non compresso (linux / mac)\",\n\t\"fz_pax\": \"tar formato pax non compresso (più lento)\",\n\t\"fz_targz\": \"gnu-tar con compressione gzip livello 3$N$NSolitamente è molto lento, quindi$Nusa tar non compresso\",\n\t\"fz_tarxz\": \"gnu-tar con compressione xz livello 1$N$NQuesto è solitamente molto lento, quindi$Nusa tar non compresso\",\n\t\"fz_zip8\": \"zip con nomi file utf8 (forse instabile su windows 7 e precedenti)\",\n\t\"fz_zipd\": \"zip con nomi file cp437 tradizionali, per software molto vecchio\",\n\t\"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)\",\n\n\t\"un_m1\": \"puoi eliminare i tuoi caricamenti recenti (o interrompere quelli non finiti) qui sotto\",\n\t\"un_upd\": \"aggiorna\",\n\t\"un_m4\": \"o condividi i file visibili qui sotto:\",\n\t\"un_ulist\": \"mostra\",\n\t\"un_ucopy\": \"copia\",\n\t\"un_flt\": \"filtro opzionale:&nbsp; URL deve contenere\",\n\t\"un_fclr\": \"resetta filtro\",\n\t\"un_derr\": 'unpost-delete fallito:\\n',\n\t\"un_f5\": 'qualcosa si è rotto, prova un aggiornamento o premi F5',\n\t\"un_uf5\": \"scusa ma devi aggiornare la pagina (per esempio premendo F5 o CTRL-R) prima che questo caricamento possa essere interrotto\",\n\t\"un_nou\": '<b>avviso:</b> server troppo occupato per mostrare caricamenti non finiti; clicca il link \"aggiorna\" tra un po\\'',\n\t\"un_noc\": '<b>avviso:</b> unpost di file completamente caricati non è abilitato/permesso nella configurazione server',\n\t\"un_max\": \"mostrando primi 2000 file (usa il filtro)\",\n\t\"un_avail\": \"{0} caricamenti recenti possono essere eliminati<br />{1} non finiti possono essere interrotti\",\n\t\"un_m2\": \"ordinati per tempo di caricamento; più recenti prima:\",\n\t\"un_no1\": \"scherzo! nessun caricamento è abbastanza recente\",\n\t\"un_no2\": \"scherzo! nessun caricamento che corrisponde a quel filtro è abbastanza recente\",\n\t\"un_next\": \"elimina i prossimi {0} file qui sotto\",\n\t\"un_abrt\": \"interrompi\",\n\t\"un_del\": \"elimina\",\n\t\"un_m3\": \"caricando i tuoi caricamenti recenti...\",\n\t\"un_busy\": \"eliminando {0} file...\",\n\t\"un_clip\": \"{0} link copiati negli appunti\",\n\n\t\"u_https1\": \"dovresti\",\n\t\"u_https2\": \"passare a https\",\n\t\"u_https3\": \"per prestazioni migliori\",\n\t\"u_ancient\": 'il tuo browser è incredibilmente antico -- forse dovresti <a href=\"#\" onclick=\"goto(\\'bup\\')\">usare bup invece</a>',\n\t\"u_nowork\": \"serve firefox 53+ o chrome 57+ o iOS 11+\",\n\t\"tail_2old\": \"serve firefox 105+ o chrome 71+ o iOS 14.5+\",\n\t\"u_nodrop\": 'il tuo browser è troppo vecchio per il caricamento drag-and-drop',\n\t\"u_notdir\": \"quella non è una cartella!\\n\\nil tuo browser è troppo vecchio,\\nprova dragdrop invece\",\n\t\"u_uri\": \"per trascinare immagini da altre finestre del browser,\\nrilasciale sul pulsante upload grande\",\n\t\"u_enpot\": 'passa alla <a href=\"#\">UI patata</a> (può migliorare velocità upload)',\n\t\"u_depot\": 'passa alla <a href=\"#\">UI elegante</a> (può ridurre velocità upload)',\n\t\"u_gotpot\": 'passando alla UI patata per migliorare velocità upload,\\n\\nsentiti libero di non essere d\\'accordo e tornare indietro!',\n\t\"u_pott\": \"<p>file: &nbsp; <b>{0}</b> finiti, &nbsp; <b>{1}</b> falliti, &nbsp; <b>{2}</b> occupati, &nbsp; <b>{3}</b> in coda</p>\",\n\t\"u_ever\": \"questo è l'uploader di base; up2k necessita almeno<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1\",\n\t\"u_su2k\": 'questo è l\\'uploader di base; <a href=\"#\" id=\"u2yea\">up2k</a> è migliore',\n\t\"u_uput\": 'velocizza (salta checksum)',\n\t\"u_ewrite\": 'non hai accesso in scrittura a questa cartella',\n\t\"u_eread\": 'non hai accesso in lettura a questa cartella',\n\t\"u_enoi\": 'file-search non è abilitato nella configurazione server',\n\t\"u_enoow\": \"non puoi sovrascrivere qui; serve permesso Elimina\",\n\t\"u_badf\": 'Questi {0} file (di {1} totali) sono stati saltati, probabilmente a causa di permessi filesystem:\\n\\n',\n\t\"u_blankf\": 'Questi {0} file (di {1} totali) sono vuoti; caricarli comunque?\\n\\n',\n\t\"u_applef\": 'Questi {0} file (di {1} totali) sono probabilmente indesiderabili;\\nPremi <code>OK/Invio</code> per SALTARE i seguenti file,\\nPremi <code>Annulla/ESC</code> per NON escludere, e CARICARE anche quelli:\\n\\n',\n\t\"u_just1\": '\\nForse funziona meglio se selezioni solo un file',\n\t\"u_ff_many\": \"se stai usando <b>Linux / MacOS / Android,</b> allora questa quantità di file <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\"><em>potrebbe</em> far crashare Firefox!</a>\\nse succede, riprova (o usa Chrome).\",\n\t\"u_up_life\": \"Questo caricamento sarà eliminato dal server\\n{0} dopo che si completa\",\n\t\"u_asku\": 'caricare questi {0} file in <code>{1}</code>',\n\t\"u_unpt\": \"puoi annullare / eliminare questo caricamento usando 🧯 in alto a sinistra\",\n\t\"u_bigtab\": 'sto per mostrare {0} file\\n\\nquesto potrebbe far crashare il tuo browser, sei sicuro?',\n\t\"u_scan\": 'Scansionando file...',\n\t\"u_dirstuck\": 'iteratore directory si è bloccato tentando di accedere ai seguenti {0} elementi; salterò:',\n\t\"u_etadone\": 'Fatto ({0}, {1} file)',\n\t\"u_etaprep\": '(preparando per caricare)',\n\t\"u_hashdone\": 'hashing completato',\n\t\"u_hashing\": 'hash',\n\t\"u_hs\": 'handshaking...',\n\t\"u_started\": \"i file ora sono in caricamento; vedi [🚀]\",\n\t\"u_dupdefer\": \"duplicato; sarà processato dopo tutti gli altri file\",\n\t\"u_actx\": \"clicca questo testo per prevenire perdita di<br />prestazioni quando cambi ad altre finestre/schede\",\n\t\"u_fixed\": \"OK!&nbsp; Risolto 👍\",\n\t\"u_cuerr\": \"caricamento fallito del chunk {0} di {1};\\nprobabilmente innocuo, continuo\\n\\nfile: {2}\",\n\t\"u_cuerr2\": \"il server ha rifiutato il caricamento (chunk {0} di {1});\\nriproverò più tardi\\n\\nfile: {2}\\n\\nerrore \",\n\t\"u_ehstmp\": \"riproverò; vedi in basso a destra\",\n\t\"u_ehsfin\": \"il server ha rifiutato la richiesta di finalizzare caricamento; riprovando...\",\n\t\"u_ehssrch\": \"il server ha rifiutato la richiesta di eseguire ricerca; riprovando...\",\n\t\"u_ehsinit\": \"il server ha rifiutato la richiesta di iniziare caricamento; riprovando...\",\n\t\"u_eneths\": \"errore di rete durante handshake per upload; riprovando...\",\n\t\"u_enethd\": \"errore di rete durante test esistenza target; riprovando...\",\n\t\"u_cbusy\": \"aspettando che il server si fidi di noi di nuovo dopo un problema di rete...\",\n\t\"u_ehsdf\": \"il server ha finito lo spazio su disco!\\n\\ncontinuerò a riprovare, nel caso qualcuno\\nliberi abbastanza spazio per continuare\",\n\t\"u_emtleak1\": \"sembra che il tuo browser possa avere un memory leak;\\nper favore\",\n\t\"u_emtleak2\": ' <a href=\"{0}\">passa a https (raccomandato)</a> o ',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": 'prova quanto segue:\\n<ul><li>premi <code>F5</code> per aggiornare la pagina</li><li>poi disabilita il pulsante &nbsp;<code>mt</code>&nbsp; nelle &nbsp;<code>⚙️ impostazioni</code></li><li>e riprova quel caricamento</li></ul>I caricamenti saranno un po\\' più lenti, ma pazienza.\\nScusa per il disturbo !\\n\\nPS: chrome v107 <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">ha un bugfix</a> per questo',\n\t\"u_emtleakf\": 'prova quanto segue:\\n<ul><li>premi <code>F5</code> per aggiornare la pagina</li><li>poi abilita <code>🥔</code> (patata) nell\\'UI caricamento<li>e riprova quel caricamento</li></ul>\\nPS: firefox <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">avrà sperabilmente un bugfix</a> ad un certo punto',\n\t\"u_s404\": \"non trovato sul server\",\n\t\"u_expl\": \"spiega\",\n\t\"u_maxconn\": \"la maggior parte dei browser limita questo a 6, ma firefox ti permette di alzarlo con <code>connections-per-server</code> in <code>about:config</code>\",\n\t\"u_tu\": '<p class=\"warn\">AVVISO: turbo abilitato, <span>&nbsp;client potrebbe non rilevare e riprendere caricamenti incompleti; vedi tooltip pulsante turbo</span></p>',\n\t\"u_ts\": '<p class=\"warn\">AVVISO: turbo abilitato, <span>&nbsp;risultati ricerca possono essere incorretti; vedi tooltip pulsante turbo</span></p>',\n\t\"u_turbo_c\": \"turbo è disabilitato nella configurazione server\",\n\t\"u_turbo_g\": \"disabilitando turbo perché non hai\\nprivilegi di elenco directory all'interno di questo volume\",\n\t\"u_life_cfg\": 'auto-elimina dopo <input id=\"lifem\" p=\"60\" /> min (o <input id=\"lifeh\" p=\"3600\" /> ore)',\n\t\"u_life_est\": 'caricamento sarà eliminato <span id=\"lifew\" tt=\"ora locale\">---</span>',\n\t\"u_life_max\": 'questa cartella impone una\\nvita massima di {0}',\n\t\"u_unp_ok\": 'unpost è permesso per {0}',\n\t\"u_unp_ng\": 'unpost NON sarà permesso',\n\t\"ue_ro\": 'il tuo accesso a questa cartella è solo-Lettura\\n\\n',\n\t\"ue_nl\": 'attualmente non sei loggato',\n\t\"ue_la\": 'attualmente sei loggato come \"{0}\"',\n\t\"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',\n\t\"ue_ta\": 'prova a caricare di nuovo, dovrebbe funzionare ora',\n\t\"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\",\n\t\"ur_1uo\": \"OK: File caricato con successo\",\n\t\"ur_auo\": \"OK: Tutti i {0} file caricati con successo\",\n\t\"ur_1so\": \"OK: File trovato sul server\",\n\t\"ur_aso\": \"OK: Tutti i {0} file trovati sul server\",\n\t\"ur_1un\": \"Caricamento fallito, scusa\",\n\t\"ur_aun\": \"Tutti i {0} caricamenti falliti, scusa\",\n\t\"ur_1sn\": \"File NON trovato sul server\",\n\t\"ur_asn\": \"I {0} file NON sono stati trovati sul server\",\n\t\"ur_um\": \"Finito;\\n{0} caricamenti OK,\\n{1} caricamenti falliti, scusa\",\n\t\"ur_sm\": \"Finito;\\n{0} file trovati sul server,\\n{1} file NON trovati sul server\",\n\n\t\"rc_opn\": \"apri\", //m\n\t\"rc_ply\": \"riproduci\", //m\n\t\"rc_pla\": \"riproduci come audio\", //m\n\t\"rc_txt\": \"apri nel visualizzatore di file\", //m\n\t\"rc_md\": \"apri nell’editor di testo\", //m\n\t\"rc_dl\": \"scarica\", //m\n\t\"rc_zip\": \"scarica come archivio\", //m\n\t\"rc_cpl\": \"copia link\", //m\n\t\"rc_del\": \"elimina\", //m\n\t\"rc_cut\": \"taglia\", //m\n\t\"rc_cpy\": \"copia\", //m\n\t\"rc_pst\": \"incolla\", //m\n\t\"rc_rnm\": \"rinomina\", //m\n\t\"rc_nfo\": \"nuova cartella\", //m\n\t\"rc_nfi\": \"nuovo file\", //m\n\t\"rc_sal\": \"seleziona tutto\", //m\n\t\"rc_sin\": \"inverti selezione\", //m\n\t\"rc_shf\": \"condividi questa cartella\", //m\n\t\"rc_shs\": \"condividi selezione\", //m\n\n\t\"lang_set\": \"aggiornare per rendere effettivo il cambiamento?\",\n\n\t\"splash\": {\n\t\t\"a1\": \"aggiorna\",\n\t\t\"b1\": \"ciao &nbsp; <small>(non sei connesso)</small>\",\n\t\t\"c1\": \"disconnetti\",\n\t\t\"d1\": \"stato\",\n\t\t\"d2\": \"mostra lo stato di tutti i thread attivi\",\n\t\t\"e1\": \"ricarica configurazione\",\n\t\t\"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\",\n\t\t\"f1\": \"puoi visualizzare:\",\n\t\t\"g1\": \"puoi caricare su:\",\n\t\t\"cc1\": \"altro:\",\n\t\t\"h1\": \"disattiva k304\",\n\t\t\"i1\": \"attiva k304\",\n\t\t\"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\",\n\t\t\"k1\": \"resetta impostazioni\",\n\t\t\"l1\": \"accedi:\",\n\t\t\"ls3\": \"accedi\", //m\n\t\t\"lu4\": \"nome utente\", //m\n\t\t\"lp4\": \"password\", //m\n\t\t\"lo3\": \"disconnetti “{0}” ovunque\", //m\n\t\t\"lo2\": \"questo terminerà la sessione su tutti i browser\", //m\n\t\t\"m1\": \"bentornato,\",\n\t\t\"n1\": \"404: file non trovato &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": \"oppure forse non hai accesso? prova una password o <a href=\\\"SR/?h\\\">torna alla home</a>\",\n\t\t\"p1\": \"403: accesso negato &nbsp;~┻━┻\",\n\t\t\"q1\": \"prova una password o <a href=\\\"SR/?h\\\">torna alla home</a>\",\n\t\t\"r1\": \"torna alla home\",\n\t\t\".s1\": \"mappa\",\n\t\t\"t1\": \"azione\",\n\t\t\"u2\": \"tempo dall'ultima scrittura sul server\\n (caricamento / rinomina / ...)\\n\\n17d = 17 giorni\\n1h23 = 1 ora 23 minuti\\n4m56 = 4 minuti 56 secondi\",\n\t\t\"v1\": \"connetti\",\n\t\t\"v2\": \"usa questo server come un disco locale\",\n\t\t\"w1\": \"passa a https\",\n\t\t\"x1\": \"cambia password\",\n\t\t\"y1\": \"le tue condivisioni\",\n\t\t\"z1\": \"sblocca area:\",\n\t\t\"ta1\": \"devi prima inserire una nuova password\",\n\t\t\"ta2\": \"ripeti per confermare la nuova password:\",\n\t\t\"ta3\": \"errore di digitazione; riprova\",\n\t\t\"nop\": \"ERRORE: La password non può essere vuota\", //m\n\t\t\"nou\": \"ERRORE: Il nome utente e/o la password non possono essere vuoti\", //m\n\t\t\"aa1\": \"in arrivo:\",\n\t\t\"ab1\": \"disattiva no304\",\n\t\t\"ac1\": \"attiva no304\",\n\t\t\"ad1\": \"no304 disabilita completamente la cache. Se k304 non è sufficiente, prova questa opzione. Aumenterà notevolmente il consumo di dati!\",\n\t\t\"ae1\": \"in uscita:\",\n\t\t\"af1\": \"mostra i file caricati di recente\",\n\t\t\"ag1\": \"mostra utenti IdP conosciuti\",\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/jpn.js",
    "content": "\n//末尾が //m の行は未検証の機械翻訳です\n\nLs.jpn = {\n\t\"tt\": \"日本語\",\n\n\t\"cols\": {\n\t\t\"c\": \"アクションボタン\",\n\t\t\"dur\": \"間隔\",\n\t\t\"q\": \"品質 / ビットレート\",\n\t\t\"Ac\": \"オーディオコーデック\",\n\t\t\"Vc\": \"ビデオコーデック\",\n\t\t\"Fmt\": \"フォーマット / コンテナ\",\n\t\t\"Ahash\": \"オーディオチェックサム\",\n\t\t\"Vhash\": \"ビデオチェックサム\",\n\t\t\"Res\": \"解像度\",\n\t\t\"T\": \"ファイル形式\",\n\t\t\"aq\": \"オーディオ 品質 / ビットレート\",\n\t\t\"vq\": \"ビデオ 品質 / ビットレート\",\n\t\t\"pixfmt\": \"サブサンプリング / ピクセル構造\",\n\t\t\"resw\": \"水平解像度\",\n\t\t\"resh\": \"垂直解像度\",\n\t\t\"chs\": \"オーディオチャンネル\",\n\t\t\"hz\": \"サンプリングレート\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"misc\",\n\t\t\t[\"ESC\", \"閉じる\"],\n\n\t\t\t\"file-manager\",\n\t\t\t[\"G\", \"リスト / グリッド表示を切り替える\"],\n\t\t\t[\"T\", \"サムネイル / アイコンを切り替える\"],\n\t\t\t[\"⇧ A/D\", \"サムネイルサイズ\"],\n\t\t\t[\"ctrl-K\", \"選択した項目を削除\"],\n\t\t\t[\"ctrl-X\", \"選択範囲をクリップボードに切り取る\"],\n\t\t\t[\"ctrl-C\", \"選択範囲をクリップボードにコピー\"],\n\t\t\t[\"ctrl-V\", \"ここに貼り付け（移動/コピー）\"],\n\t\t\t[\"Y\", \"選択した項目をダウンロード\"],\n\t\t\t[\"F2\", \"選択した項目の名前を変更\"],\n\n\t\t\t\"file-list-sel\",\n\t\t\t[\"space\", \"ファイル選択の切り替え\"],\n\t\t\t[\"↑/↓\", \"選択カーソルを移動\"],\n\t\t\t[\"ctrl ↑/↓\", \"カーソルとビューポートを移動\"],\n\t\t\t[\"⇧ ↑/↓\", \"前/次のファイルを選択\"],\n\t\t\t[\"ctrl-A\", \"すべてのファイル / フォルダを選択\"],\n\t\t], [\n\t\t\t\"navigation\",\n\t\t\t[\"B\", \"パンくずリスト / ナビペインを切り替える\"],\n\t\t\t[\"I/K\", \"前/次のフォルダ\"],\n\t\t\t[\"M\", \"親フォルダ（または現在のフォルダを展開解除）\"],\n\t\t\t[\"V\", \"ナビペインのフォルダ / テキストファイルを切り替える\"],\n\t\t\t[\"A/D\", \"ナビペインサイズ\"],\n\t\t], [\n\t\t\t\"audio-player\",\n\t\t\t[\"J/L\", \"前/次の曲\"],\n\t\t\t[\"U/O\", \"10秒前/後スキップ\"],\n\t\t\t[\"0..9\", \"0%～90%へジャンプ\"],\n\t\t\t[\"P\", \"再生/一時停止（開始も）\"],\n\t\t\t[\"S\", \"再生中の曲を選択\"],\n\t\t\t[\"Y\", \"曲をダウンロード\"],\n\t\t], [\n\t\t\t\"image-viewer\",\n\t\t\t[\"J/L, ←/→\", \"前/次の画像\"],\n\t\t\t[\"Home/End\", \"最初/最後の画像\"],\n\t\t\t[\"F\", \"フルスクリーン\"],\n\t\t\t[\"R\", \"時計回りに回転\"],\n\t\t\t[\"⇧ R\", \"反時計回りに回転\"],\n\t\t\t[\"S\", \"画像を選択\"],\n\t\t\t[\"Y\", \"画像をダウンロード\"],\n\t\t], [\n\t\t\t\"video-player\",\n\t\t\t[\"U/O\", \"10秒前/後へスキップ\"],\n\t\t\t[\"P/K/Space\", \"再生/一時停止\"],\n\t\t\t[\"C\", \"自動再生\"],\n\t\t\t[\"V\", \"ループ\"],\n\t\t\t[\"M\", \"ミュート\"],\n\t\t\t[\"[ and ]\", \"ループ間隔を設定\"],\n\t\t], [\n\t\t\t\"textfile-viewer\",\n\t\t\t[\"I/K\", \"前/次のファイル\"],\n\t\t\t[\"M\", \"テキストファイルを閉じる\"],\n\t\t\t[\"E\", \"テキストファイルの編集\"],\n\t\t\t[\"S\", \"ファイルを選択（切り取り/コピー/名前変更）\"],\n\t\t\t[\"Y\", \"テキストファイルをダウンロード\"],\n\t\t\t[\"⇧ J\", \"JSONを整形する\"],\n\t\t]\n\t],\n\n\t\"m_ok\": \"OK\",\n\t\"m_ng\": \"キャンセル\",\n\n\t\"enable\": \"有効\",\n\t\"danger\": \"危険\",\n\t\"clipped\": \"クリップボードにコピーされました\",\n\n\t\"ht_s1\": \"秒\",\n\t\"ht_s2\": \"秒\",\n\t\"ht_m1\": \"分\",\n\t\"ht_m2\": \"分\",\n\t\"ht_h1\": \"時間\",\n\t\"ht_h2\": \"時間\",\n\t\"ht_d1\": \"日\",\n\t\"ht_d2\": \"日\",\n\t\"ht_and\": \" と \",\n\n\t\"goh\": \"コントロールパネル\",\n\t\"gop\": '前のフォルダ\">prev',\n\t\"gou\": '親フォルダ\">up',\n\t\"gon\": '次のフォルダ\">next',\n\t\"logout\": \"ログアウト \",\n\t\"login\": \"ログイン\",\n\t\"access\": \" アクセス\",\n\t\"ot_close\": \"サブメニューを閉じる\",\n\t\"ot_search\": \"`属性、パス/名前、音楽タグ、またはそれらの組み合わせでファイルを検索$N$N`foo bar` = «foo» と «bar» の両方を含める。$N`foo -bar` = «foo» を含み、«bar» を含まない必要があります。$N`^yana .opus$` = «yana» で始まり、«opus» ファイルである$N`&quot;try unite&quot;` = «try unite» だけを含む$N$N日付形式は iso-8601。たとえば、$N`2009-12-31` や `2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"unpost: 最近アップロードした投稿を削除するか、未完成の投稿を中止\",\n\t\"ot_bup\": \"bup: 基本的なアップローダー。Netscape 4.0 もサポートしています。\",\n\t\"ot_mkdir\": \"mkdir: 新しいディレクトリを作成\",\n\t\"ot_md\": \"new-file: 新しいテキストファイルを作成\",\n\t\"ot_msg\": \"msg: サーバーログにメッセージを送信\",\n\t\"ot_mp\": \"メディアプレーヤー設定\",\n\t\"ot_cfg\": \"設定オプション\",\n\t\"ot_u2i\": 'up2k: ファイルをアップロードする (書き込みアクセス権がある場合)、または検索モードに切り替えてファイルがサーバーのどこかに存在するかどうかを確認$N$Nアップロードは再開可能で、マルチスレッドであり、ファイルのタイムスタンプが保持されますが、[🎈] (基本的なアップローダー) よりも多くの CPU を使用します<br /><br />アップロード中、このアイコンは進行状況インジケーターになります。',\n\t\"ot_u2w\": 'up2k: 再開サポート付きのファイルのアップロード（ブラウザを閉じて、後で同じファイルをドロップします）$N$Nマルチスレッドで、ファイルのタイムスタンプが保持されますが、[🎈]（基本的なアップローダー）よりも多くの CPU を使用します。<br /><br />アップロード中は、このアイコンが進行状況インジケーターになります。',\n\t\"ot_noie\": 'Chrome / Firefox / Edgeを利用してください',\n\n\t\"ab_mkdir\": \"ディレクトリを作成\",\n\t\"ab_mkdoc\": \"新しいテキストファイル\",\n\t\"ab_msg\": \"メッセージをサーバーログに送信\",\n\n\t\"ay_path\": \"フォルダへジャンプ\",\n\t\"ay_files\": \"ファイルへジャンプ\",\n\n\t\"wt_ren\": \"選択した項目の名前を変更する$Nホットキー: F2\",\n\t\"wt_del\": \"選択した項目を削除$Nホットキー: ctrl-K\",\n\t\"wt_cut\": \"選択した項目を切り取る &lt;small&gt;(その後別の場所に貼り付ける)&lt;/small&gt;$Nホットキー: ctrl-X\",\n\t\"wt_cpy\": \"選択した項目をクリップボード$Nにコピー（別の場所に貼り付ける）$Nホットキー: ctrl-C\",\n\t\"wt_pst\": \"以前に切り取った/コピーした選択範囲を貼り付ける$Nホットキー: ctrl-V\",\n\t\"wt_selall\": \"すべてのファイルを選択$Nホットキー: ctrl-A（ファイルにフォーカスがあるとき）\",\n\t\"wt_selinv\": \"選択を反転\",\n\t\"wt_zip1\": \"このフォルダを圧縮してダウンロード\",\n\t\"wt_selzip\": \"選択内容を圧縮してダウンロード\",\n\t\"wt_seldl\": \"選択した項目を個別のファイルとしてダウンロード$Nホットキー: Y\",\n\t\"wt_npirc\": \"IRC形式のトラック情報をコピーする\",\n\t\"wt_nptxt\": \"プレーンテキストのトラック情報をコピー\",\n\t\"wt_m3ua\": \"m3uプレイリストに追加 (後で <code>📻コピー</code> をクリック)\",\n\t\"wt_m3uc\": \"m3uプレイリストをクリップボードにコピー\",\n\t\"wt_grid\": \"グリッド / リスト表示を切り替える$Nホットキー: G\",\n\t\"wt_prev\": \"前のトラック$Nホットキー: J\",\n\t\"wt_play\": \"再生 / 一時停止$Nホットキー: P\",\n\t\"wt_next\": \"次のトラック$Nホットキー: L\",\n\n\t\"ul_par\": \"並列アップロード:\",\n\t\"ut_rand\": \"ファイル名をランダム化する\",\n\t\"ut_u2ts\": \"最終更新日時のタイムスタンプ$Nファイルシステムからサーバーへコピーする\\\">📅\",\n\t\"ut_ow\": \"サーバー上の既存のファイルを上書きする？$N🛡️: しない（代わりに新しいファイル名を生成する）$N🕒: サーバーのファイルが古い場合は上書きする$N♻️: ファイルが異なる場合は常に上書きする$N⏭️: 既存のファイルをすべて無条件にスキップする\",\n\t\"ut_mt\": \"アップロード中に他のファイルのハッシュを継続する$N$NCPUやHDDがボトルネックになっている場合は無効にしてください\",\n\t\"ut_ask\": 'aアップロードを開始する前に確認を求める\">💭',\n\t\"ut_pot\": \"UIをシンプルにすることで$N低速デバイスでのアップロード速度を向上させる\",\n\t\"ut_srch\": \"実際にはアップロードせず、代わりにファイルが既にアップロードされているかどうかを確認 $N すでにサーバー上に存在（読み取り可能なすべてのフォルダをスキャン）\",\n\t\"ut_par\": \"0に設定するとアップロードを一時停止$N$N接続が遅い / 遅延が大きい場合は増やす$N$NLANやサーバーのHDDがボトルネックになっている場合は1にする\",\n\t\"ul_btn\": \"ファイル / フォルダを<br>ここにドロップしてください（またはクリックしてください）\",\n\t\"ul_btnu\": \"ア ッ プ ロ ー ド\",\n\t\"ul_btns\": \"検 索\",\n\n\t\"ul_hash\": \"ハッシュ\",\n\t\"ul_send\": \"送信\",\n\t\"ul_done\": \"完了\",\n\t\"ul_idle1\": \"キューにはまだアップロードは登録されていない\",\n\t\"ut_etah\": \"平均 &lt;em&gt;ハッシュ化&lt;/em&gt; 速度と完了予想時間\",\n\t\"ut_etau\": \"平均 &lt;em&gt;アップロード&lt;/em&gt; 速度と完了予想時間\",\n\t\"ut_etat\": \"平均 &lt;em&gt;合計&lt;/em&gt; 速度と完了予想時間\",\n\n\t\"uct_ok\": \"正常に完了\",\n\t\"uct_ng\": \"NG: 失敗 / 拒否 / 見つからない\",\n\t\"uct_done\": \"OKとNGの組み合わせ\",\n\t\"uct_bz\": \"ハッシュ化またはアップロード\",\n\t\"uct_q\": \"アイドル状態、保留中\",\n\n\t\"utl_name\": \"ファイル名\",\n\t\"utl_ulist\": \"リスト\",\n\t\"utl_ucopy\": \"コピー\",\n\t\"utl_links\": \"リンク\",\n\t\"utl_stat\": \"状態\",\n\t\"utl_prog\": \"進捗\",\n\n\t// keep short:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"エラー\",\n\t\"utl_oserr\": \"OS-エラー\",\n\t\"utl_found\": \"発見\",\n\t\"utl_defer\": \"延期\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"完了\",\n\n\t\"ul_flagblk\": \"ファイルがキューに追加されました</b><br>しかし、別のブラウザタブにはビジー状態のup2kがあり、<br>それが終わるまで待機します\",\n\t\"ul_btnlk\": \"サーバー構成によりこのスイッチはこの状態にロックされています\",\n\n\t\"udt_up\": \"アップロード\",\n\t\"udt_srch\": \"検索\",\n\t\"udt_drop\": \"ここにドロップ\",\n\n\t\"u_nav_m\": '<h6>はい、何かもってますか？</h6><code>Enter</code> = ファイル（1つ以上）\\n<code>ESC</code> = 1つのフォルダ（サブフォルダを含む）',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">ファイル</a><a href=\"#\" id=\"modal-ng\">フォルダ</a>',\n\n\t\"cl_opts\": \"スイッチ\",\n\t\"cl_hfsz\": \"ファイルサイズ\",\n\t\"cl_themes\": \"テーマ\",\n\t\"cl_langs\": \"言語\",\n\t\"cl_ziptype\": \"フォルダダウロード\",\n\t\"cl_uopts\": \"up2kスイッチ\",\n\t\"cl_favico\": \"ファビコン\",\n\t\"cl_bigdir\": \"ディレクトリの最大数\",\n\t\"cl_hsort\": \"#ソート\",\n\t\"cl_keytype\": \"キーの表記タイプ\",\n\t\"cl_hiddenc\": \"非表示の列\",\n\t\"cl_hidec\": \"非表示\",\n\t\"cl_reset\": \"リセット\",\n\t\"cl_hpick\": \"下の表で非表示にするには列ヘッダーをタップします\",\n\t\"cl_hcancel\": \"列の非表示を解除\",\n\t\"cl_rcm\": \"右クリックメニュー\",\n\n\t\"ct_grid\": '田 グリッド',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ ツールチップ',\n\t\"ct_thumb\": 'グリッドビューではアイコンまたはサムネイルを切り替える$Nホットキー: T\">🖼️ サムネイル',\n\t\"ct_csel\": 'グリッドビューでファイルを選択するにはCtrlとShiftを使用する。\">選択',\n\t\"ct_dsel\": 'グリッドビューでドラッグ選択を使用する。\">ドラッグ', //m\n\t\"ct_dl\": 'ファイルをクリックしたときに強制的にダウンロードする（インラインで表示しない）\">dl',\n\t\"ct_ihop\": '画像ビューアを閉じたら最後に表示したファイルまでスクロールする。\">g⮯',\n\t\"ct_dots\": '隠しファイルを表示する（サーバーが許可している場合）\">隠しファイル',\n\t\"ct_qdel\": 'ファイルを削除するときは確認を一度だけ求める\">qdel',\n\t\"ct_dir1st\": 'ファイルの前にフォルダを並べ替える\">📁 優先',\n\t\"ct_nsort\": '自然ソート（先頭に数字があるファイル名の場合）\">nsort',\n\t\"ct_utc\": 'すべての日時をUTCで表示\">UTC',\n\t\"ct_readme\": 'フォルダ一覧にREADME.mdを表示\">📜 readme',\n\t\"ct_idxh\": 'フォルダ一覧の代わりにindex.htmlを表示\">htm',\n\t\"ct_sbars\": 'スクロールバーを表示\">⟊',\n\n\t\"cut_umod\": \"サーバー上にファイルが既に存在する場合はサーバーの最終更新タイムスタンプをローカルファイルと一致するように更新します（書き込み+削除権限が必要です）\\\">re📅\",\n\n\t\"cut_turbo\": \"yoloボタンを使用する場合できればこれを有効にしないでください:$N$N大量のファイルをアップロードしていて何らかの理由で再起動する必要があり、できるだけ早くアップロードを続行したい場合にこれを使用します。$N$Nこれはハッシュチェックを単純な<em>&quot;これはサーバー上で同じファイルサイズですか？&quot;</em>に置き換えます。そのため、ファイルの内容が異なる場合はアップロードされません。$N$Nアップロードが完了したらこれをオフにし、同じファイルを再度 &quot;アップロード&quot; してクライアントに検証させる必要があります。\\\">turbo\",\n\n\t\"cut_datechk\": \"turboボタンが有効になっていなければ効果はありません$N$Nyolo要素をわずかに減らしサーバー上のファイルのタイムスタンプが一致するかどうかを確認します。$N$N<em>理論的</em>にはほとんどの未完了 / 破損したアップロードを把握できるはずですが、その後ターボを無効にして検証パスを実行する代わりにはなりません。\\\">date-chk\",\n\n\t\"cut_u2sz\": \"各アップロードチャンクのサイズ（MiB）; 大西洋を横断する場合は大きな値の方が飛行効率が良いです。接続の信頼性が低い場合は、小さな値を試してください。\",\n\n\t\"cut_flag\": \"一度にアップロードするタブは1つだけにしてください $N -- 他のタブでもこれを有効にする必要があります $N -- 同じドメインのタブにのみ影響します\",\n\n\t\"cut_az\": \"ファイルの最小サイズ順ではなくアルファベット順にファイルをアップロードします。$N$アルファベット順にするとサーバー上で何か問題が発生した場合に簡単に確認できるようになりますが、光ファイバー / LANでのアップロードが若干遅くなります。\",\n\n\t\"cut_nag\": \"アップロード完了時のOS通知$N（ブラウザまたはタブがアクティブでない場合のみ）\",\n\t\"cut_sfx\": \"アップロードが完了すると音声アラートが鳴ります$N（ブラウザまたはタブがアクティブでない場合のみ）\",\n\n\t\"cut_mt\": \"マルチスレッドを使用してファイルハッシュを高速化する$N$NこれはWebワーカーを使用し$N追加のRAM（最大512 MiB）が必要$N$Nこれによりhttpsは30%高速化、httpは4.5倍高速化r\\\">mt\",\n\n\t\"cut_wasm\": \"ブラウザの組込みのハッシュ関数の代わりにwasmを使用します。Chromeベースのブラウザでは速度が向上しますがCPU負荷が増加します。また、Chromeの古いバージョンの多くにはこれを有効にするとブラウザがすべてのRAMを消費してクラッシュするというバグがあります。\\\">wasm\",\n\n\t\"cft_text\": \"ファビコンテキスト（無効にするには空白にして更新してください）\",\n\t\"cft_fg\": \"テキストカラー\",\n\t\"cft_bg\": \"背景色\",\n\n\t\"cdt_lim\": \"フォルダに表示するファイルの最大数\",\n\t\"cdt_ask\": \"一番下までスクロールしたときに$N更にファイルを読み込む代わりに$N何をするか尋ねる\",\n\t\"cdt_hsort\": \"`メディアURLに含めるソートルール (`,sorthref`) の数。0に設定するとメディアリンクをクリックした際にそのリンクに含まれるソートルールも無視されます。\",\n\t\"cdt_ren\": \"カスタム右クリックメニューを有効にしてもShiftキーを押しながら右クリックすることで通常のメニューにアクセスできます。\\\">有効\",\n\t\"cdt_rdb\": \"カスタム右クリックメニューが開いている状態で再度右クリックしたときに通常のメニューを表示する\\\">x2\", //m\n\n\t\"tt_entree\": \"ナビペインを表示（ディレクトリツリーサイドバー）$Nホットキー: B\",\n\t\"tt_detree\": \"パンくずリストを表示$Nホットキー: B\",\n\t\"tt_visdir\": \"選択したフォルダまでスクロール\",\n\t\"tt_ftree\": \"フォルダツリー / テキストファイルの切り替え$Nホットキー: V\",\n\t\"tt_pdock\": \"上部のドッキングされたペインに親フォルダを表示\",\n\t\"tt_dynt\": \"ツリーが拡大するにつれて自動的に増加\",\n\t\"tt_wrap\": \"単語の折り返し\",\n\t\"tt_hover\": \"ホバーすると溢れた線を表示する$N( マウスを押さない限りスクロールが中断されます $N&nbsp; カーソルは左余白です )\",\n\n\t\"ml_pmode\": \"フォルダの末尾...\",\n\t\"ml_btns\": \"コマンド\",\n\t\"ml_tcode\": \"変換\",\n\t\"ml_tcode2\": \"この形式に変換\",\n\t\"ml_tint\": \"色合い\",\n\t\"ml_eq\": \"オーディオイコライザー\",\n\t\"ml_drc\": \"ダイナミックレンジコンプレッサー\",\n\t\"ml_ss\": \"無音をスキップ\", //m\n\n\t\"mt_loop\": \"1曲をループ/リピート再生\\\">🔁\",\n\t\"mt_one\": \"1曲で止める\\\">1️⃣\",\n\t\"mt_shuf\": \"各フォルダ内の曲をシャッフルする\\\">🔀\",\n\t\"mt_aplay\": \"サーバーにアクセスするためにクリックしたリンクに曲IDがある場合は自動再生されます$N$Nこれを無効にすると、音楽を再生するときにページのURLが曲IDで更新されなくなります。これにより設定が失われてもURLが残っている場合の自動再生が防止されます。\\\">a▶\",\n\t\"mt_preload\": \"ギャップレス再生のために曲の終わり近くに次の曲の読み込みを開始する\\\">preload\",\n\t\"mt_prescan\": \"最後の曲が終了する前に次のフォルダへ移動し$Nウェブブラウザが$N再生を停止しないようにする\\\">nav\",\n\t\"mt_fullpre\": \"曲全体を事前ロードしてみる;$N✅ <b>信頼できない</b>接続で有効にする、$N❌ 低速接続では<b>無効</b>にする\\\">full\",\n\t\"mt_fau\": \"携帯電話では、次の曲が十分に早く読み込まれない場合に音楽が停止しないようにする（タグの表示が不安定になる可能性があります）\\\">☕️\",\n\t\"mt_waves\": \"波形シークバー:$Nスクラバーにオーディオ振幅を表示する\\\">~s\",\n\t\"mt_npclip\": \"現在再生中の曲をクリップボードに保存するためのボタンを表示する\\\">/np\",\n\t\"mt_m3u_c\": \"選択した曲をm3u8プレイリストエントリとして$Nクリップボードに保存するためのボタンを表示する\\\">📻\",\n\t\"mt_octl\": \"OS統合（メディアホットキー / OSD）\\\">os-ctl\",\n\t\"mt_oseek\": \"OS統合によるシークを許可する$N$N注: 一部のデバイス（iPhone）では$N次の曲ボタンの代わりになります\\\">シーク\",\n\t\"mt_oscv\": \"OSDでアルバムカバーを表示する\\\">art\",\n\t\"mt_follow\": \"再生中の曲をスクロールして表示したままにする\\\">🎯\",\n\t\"mt_compact\": \"コントローラーを小さく\\\">⟎\",\n\t\"mt_uncache\": \"キャッシュクリア &nbsp;（ブラウザが破損した曲のコピーをキャッシュしているために$N再生できない場合はこれを試してください）\\\">uncache\",\n\t\"mt_mloop\": \"開いているフォルダをループ\\\">🔁 ループ\",\n\t\"mt_mnext\": \"次のフォルダを読み込んで続行\\\">📂 次\",\n\t\"mt_mstop\": \"再生を停止\\\">⏸ 停止\",\n\t\"mt_cflac\": \"flac / wavを{0}に変換\\\">flac\",\n\t\"mt_caac\": \"aac / m4aを{0}に変換\\\">aac\",\n\t\"mt_coth\": \"その他すべて（mp3以外）を{0}に変換\\\">その他\",\n\t\"mt_c2opus\": \"デスクトップ、ノート、Androidに最適\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba（iOS 17.5以降）\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf（iOS 11から17まで）\\\">caf\",\n\t\"mt_c2mp3\": \"非常に古いデバイスでこれを使用\\\">mp3\",\n\t\"mt_c2flac\": \"最高の音質、ダウンロードサイズが大きい\\\">flac\",\n\t\"mt_c2wav\": \"非圧縮再生（さらに大きい）\\\">wav\",\n\t\"mt_c2ok\": \"素晴らしい、良い選択です\",\n\t\"mt_c2nd\": \"これは現在のデバイスに推奨される出力形式ではありませんが、問題ありません\",\n\t\"mt_c2ng\": \"現在のデバイスはこの出力形式をサポートしていないようですが、とにかく試してみましょう\",\n\t\"mt_xowa\": \"iOSにはこのフォーマットを使用したバックグラウンド再生を妨げるバグがあります; 代わりにcafまたはmp3を使用してください\",\n\t\"mt_tint\": \"シークバーの背景レベル（0～100）を調整し$Nバッファリングの邪魔にならにようにする\",\n\t\"mt_eq\": \"`イコライザーとゲイン制御を有効にします;$N$Nブースト `0` = 標準音量100%（変更なし）$N$N幅 `1 &nbsp;` = 標準ステレオ（変更なし）$N幅 `0.5` = 左右のクロスフィード50%$N幅 `0 &nbsp;` = モノラル$N$Nブースト `-0.8` &amp; 幅 `10` = ボーカル除去 :^)$N$Nイコライザーを有効にするとギャップレスアルバムは完全にギャップレスになります。そのため、それを気に場合すべての値をゼロ（幅 = 1を除く）にしてイコライザーをオンにしたままにしてください。\",\n\t\"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 を参照してください。もっとわかりやすく説明されています）\",\n\t\"mt_ss\": \"`無音スキップを有効化；音量が `音` 未満で、再生位置が最初の `始`% または最後の `終`% にある場合、開始/終了付近で再生速度を `速` 倍します\", //m\n\t\"mt_ssvt\": \"音量しきい値 (0-255)\\\">音\", //m\n\t\"mt_ssts\": \"有効しきい値 (トラック%, 開始)\\\">始\", //m\n\t\"mt_sste\": \"有効しきい値 (トラック%, 終了)\\\">終\", //m\n\t\"mt_sssm\": \"再生速度倍率\\\">速\", //m\n\n\t\"mb_play\": \"再生\",\n\t\"mm_hashplay\": \"このオーディオファイルを再生しますか？\",\n\t\"mm_m3u\": \"再生するには<code>Enter/OK</code>を押します\\n編集するには<code>ESC/キャンセル</code>を押してください\",\n\t\"mp_breq\": \"Firefox 82以降、Chrome 73以降、またはiOS 15以降が必要です\",\n\t\"mm_bload\": \"読み込み中...\",\n\t\"mm_bconv\": \"{0}に変換中。お待​​ちください。...\",\n\t\"mm_opusen\": \"現在のブラウザはaac / m4aファイルを再生できません;\\nOpusへのトランスコードが有効になりました\",\n\t\"mm_playerr\": \"再生に失敗: \",\n\t\"mm_eabrt\": \"再生の試行はキャンセルされました\",\n\t\"mm_enet\": \"インターネット接続が不安定です\",\n\t\"mm_edec\": \"このファイルは破損している？？\",\n\t\"mm_esupp\": \"現在のブラウザはこのオーディオ形式に対応していません\",\n\t\"mm_eunk\": \"不明なエラー\",\n\t\"mm_e404\": \"オーディオを再生できませんでした。エラー404: ファイルが見つかりません。\",\n\t\"mm_e403\": \"オーディオを再生できませんでした。エラー403: アクセス拒否。\\n\\nF5キーを押してリロードしてみてください。ログアウトしている可能性があります。\",\n\t\"mm_e415\": \"オーディオを再生できませんでした。エラー415: ファイルの変換に失敗しました。サーバーログを確認してください。\", //m\n\t\"mm_e500\": \"オーディオを再生できませんでした。エラー500: サーバーログを確認してください。\",\n\t\"mm_e5xx\": \"オーディオを再生できませんでした。サーバーエラー \",\n\t\"mm_nof\": \"近くにオーディオファイルが見つかりません\",\n\t\"mm_prescan\": \"次に再生する曲を探しています...\",\n\t\"mm_scank\": \"次の曲を見つけました:\",\n\t\"mm_uncache\": \"キャッシュがクリアされました; 次回の再生時にすべての曲が再ダウンロードされます\",\n\t\"mm_hnf\": \"その曲はもう存在しません\",\n\n\t\"im_hnf\": \"その画像はもう存在しません\",\n\n\t\"f_empty\": 'このフォルダは空です',\n\t\"f_chide\": 'これにより列 «{0}» が非表示になります\\n\\n設定タブで列の非表示を解除できます',\n\t\"f_bigtxt\": \"このファイルは {0} MiB の大きさです -- 本当にテキストとして表示しますか？\",\n\t\"f_bigtxt2\": \"代わりにファイルの末尾だけを表示しませんか？これにより追跡も有効になり、新しく追加されたテキスト行がリアルタイムで表示されます。\",\n\t\"fbd_more\": '<div id=\"blazy\">表示中 <code>{0}</code> / <code>{1}</code> ファイル; <a href=\"#\" id=\"bd_more\">{2}件を表示</a> または <a href=\"#\" id=\"bd_all\">すべて表示</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\">表示中 <code>{0}</code> / <code>{1}</code> ファイル; <a href=\"#\" id=\"bd_all\">すべて表示</a></div>',\n\t\"f_anota\": \"{1}件のアイテムのうち {0}件が選択されました;\\nフォルダ全体を選択するには、まず一番下までスクロールします\",\n\n\t\"f_dls\": '現在のフォルダ内のファイルリンクは\\nダウンロードリンクに変更されました',\n\t\"f_dl_nd\": 'フォルダーをスキップしています（代わりに zip/tar ダウンロードを使用してください）：\\n', //m\n\n\t\"f_partial\": \"現在アップロード中のファイルを安全にダウンロードするには、同じファイル名で<code>.PARTIAL</code>拡張子がないファイルをクリックしてください。これを行うにはキャンセルまたはEscキーを押してください。\\n\\nOK / Enter を押すとこの警告は無視され、代わりに<code>.PARTIAL</code>スクラッチファイルのダウンロードが続行されますが、ほとんどの場合データが破損することになります。\",\n\n\t\"ft_paste\": \"{0}件のアイテムを貼り付け$Nホットキー: ctrl-V\",\n\t\"fr_eperm\": '名前を変更できません:\\nこのフォルダではの\"移動\"の権限がありません',\n\t\"fd_eperm\": '削除できません:\\nこのフォルダではの\"削除\"の権限がありません',\n\t\"fc_eperm\": '切り取りできません:\\nこのフォルダではの\"移動\"の権限がありません',\n\t\"fp_eperm\": '貼付けできません:\\nこのフォルダではの\"書込\"の権限がありません',\n\t\"fr_emore\": \"名前を変更する項目を1つ以上選択してください\",\n\t\"fd_emore\": \"削除する項目を1つ以上選択してください\",\n\t\"fc_emore\": \"切り取る項目を1つ以上選択してください\",\n\t\"fcp_emore\": \"クリップボードにコピーする項目を1つ以上選択してください\",\n\n\t\"fs_sc\": \"現在表示中のフォルダを共有する\",\n\t\"fs_ss\": \"選択したファイルを共有する\",\n\t\"fs_just1d\": \"複数のフォルダを選択することはできません\\nまた、1回の選択でファイルとフォルダを混在させることはできません。\",\n\t\"fs_abrt\": \"❌ 中止\",\n\t\"fs_rand\": \"🎲 ランダム名\",\n\t\"fs_go\": \"✅ 共有を作成\",\n\t\"fs_name\": \"名前\",\n\t\"fs_src\": \"ソース\",\n\t\"fs_pwd\": \"パスワード\",\n\t\"fs_exp\": \"有効期限\",\n\t\"fs_tmin\": \"分\",\n\t\"fs_thrs\": \"時間\",\n\t\"fs_tdays\": \"日\",\n\t\"fs_never\": \"無期限\",\n\t\"fs_pname\": \"任意のリンク名; 空白の場合はランダムになります\",\n\t\"fs_tsrc\": \"共有するファイルまたはフォルダ\",\n\t\"fs_ppwd\": \"任意のパスワード\",\n\t\"fs_w8\": \"共有の作成...\",\n\t\"fs_ok\": \"<code>Enter/OK</code>を押してクリップボードに保存します\\n<code>ESC/キャンセル</code>を押して閉じます\",\n\n\t\"frt_dec\": \"壊れたファイル名を修正するかもしれません\\\">url-decode\",\n\t\"frt_rst\": \"変更したファイル名を元の名前に戻す\\\">↺ リセット\",\n\t\"frt_abrt\": \"中止してこのウィンドウを閉じる\\\">❌ キャンセル\",\n\t\"frb_apply\": \"名前の変更を適用\",\n\t\"fr_adv\": \"バッチ / メタデータ / パターン名変更\\\">詳細設定\",\n\t\"fr_case\": \"大文字と小文字を区別する正規表現\\\">case\",\n\t\"fr_win\": \"Windowsで安全な名前; <code>&lt;&gt;:&quot;\\\\|?*</code>を日本語の全角文字に置き換える\\\">win\",\n\t\"fr_slash\": \"<code>/</code>を新しいフォルダが作成されない文字に置き換える\\\">no /\",\n\t\"fr_re\": \"`元のファイル名に適用する正規表現検索パターン; キャプチャグループは、`(1)` や `(2)` のように、以下のフォーマットフィールドで参照することができるので、\",\n\t\"fr_fmt\": \"`foob​​ar2000 を参考にしています:$N`(title)` は曲名に置き換えられます。$N`[(artist) - ](title)` はアーティストが空白の場合は[この]部分をスキップします。$N`$lpad((tn),2,0)` はトラック番号を2桁にパディングします。\",\n\t\"fr_pdel\": \"削除\",\n\t\"fr_pnew\": \"名前をつけて保存\",\n\t\"fr_pname\": \"新しいプリセットの名前を入力します\",\n\t\"fr_aborted\": \"中止されました\",\n\t\"fr_lold\": \"古い名前\",\n\t\"fr_lnew\": \"新しい名前\",\n\t\"fr_tags\": \"選択したファイルのタグ（読み取り専用、参照のみ）:\",\n\t\"fr_busy\": \"{0}件のアイテムの名前を変更...\\n\\n{1}\",\n\t\"fr_efail\": \"名前の変更に失敗:\\n\",\n\t\"fr_nchg\": \"新しい名前のうち {0}件は、<code>win</code> および/または <code>no /</code> により変更されました。\\n\\nこれらを変更後の新しい名前で続行しますか？\",\n\n\t\"fd_ok\": \"削除成功\",\n\t\"fd_err\": \"削除失敗:\\n\",\n\t\"fd_none\": \"何も削除されませんでした; サーバー構成 (xbd) によってブロックされた可能性があります。\",\n\t\"fd_busy\": \"{0}件のアイテムを削除中...\\n\\n{1}\",\n\t\"fd_warn1\": \"これら {0}件のアイテムを削除しますか？\",\n\t\"fd_warn2\": \"<b>最後の警告！</b> 取り消し不可。削除しますか？\",\n\n\t\"fc_ok\": \"{0}件のアイテムを切り取り\",\n\t\"fc_warn\": '{0}件のアイテムを切り取りました\\n\\nしかし: <b>この</b>ブラウザタブでのみ貼り付けることができます\\n（選択範囲が非常に大きいため）',\n\n\t\"fcc_ok\": \"{0}件のアイテムをクリップボードにコピー\",\n\t\"fcc_warn\": '{0}件のアイテムをクリップボードにコピーしました\\n\\nしかし: <b>この</b>ブラウザタブでのみ貼り付けることができます\\n（選択範囲が非常に大きいため）',\n\n\t\"fp_apply\": \"これらの名前を使用する\",\n\t\"fp_skip\": \"競合をスキップ\",  // トピックノート: \"既存の名前をスキップ\" （対象フォルダ内のファイル名）\n\t\"fp_ecut\": \"最初にファイル / フォルダを切り取りまたはコピーし、貼り付け / 移動します\\n\\n注: 異なるブラウザタブ間で切り取り / 貼り付けが可能です\",\n\t\"fp_ename\": \"名前がすでに使用されているため、{0}件のアイテムはここに移動することはできません。続行するには、以下に新しい名前を入力するか名前を空白（\\\"競合をスキップ\\\"）にしてスキップしてください:\",\n\t\"fcp_ename\": \"名前がすでに使用されているため、{0}件のアイテムはここにコピーすることはできません。続行するには、以下に新しい名前を入力するか名前を空白（\\\"競合をスキップ\\\"）にしてスキップしてください:\",\n\t\"fp_emore\": \"まだ修正すべきファイル名の競合がいくつか残っています\",\n\t\"fp_ok\": \"移動完了\",\n\t\"fcp_ok\": \"コピー完了\",\n\t\"fp_busy\": \"{0}件のアイテムを移動中...\\n\\n{1}\",\n\t\"fcp_busy\": \"{0}件のアイテムをコピー中...\\n\\n{1}\",\n\t\"fp_abrt\": \"中止しています...\",\n\t\"fp_err\": \"移動に失敗:\\n\",\n\t\"fcp_err\": \"コピーに失敗:\\n\",\n\t\"fp_confirm\": \"これらの{0}件のアイテムをここに移動しますか？\",\n\t\"fcp_confirm\": \"これらの{0}件のアイテムをここにコピーしますか？\",\n\t\"fp_etab\": '他のブラウザタブからクリップボードを読み取ることができませんでした',\n\t\"fp_name\": \"デバイスからファイルをアップロードします。ファイル名を入力してください:\",\n\t\"fp_both_m\": '<h6>貼り付けるものを選択</h6><code>Enter</code> = «{1}»から{0}件のファイルを移動\\n<code>ESC</code> = デバイスから{2}件のファイルをアップロード',\n\t\"fcp_both_m\": '<h6>貼り付けるものを選択</h6><code>Enter</code> = «{1}»から{0}件のファイルをコピー\\n<code>ESC</code> = デバイスから{2}件のファイルをアップロード',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">移動</a><a href=\"#\" id=\"modal-ng\">アップロード</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">コピー</a><a href=\"#\" id=\"modal-ng\">アップロード</a>',\n\n\t\"mk_noname\": \"それをする前に左側のテキストフィールドに名前を入力してください :p\",\n\t\"nmd_i1\": \"必要なファイル拡張子も追加します。例: <code>.md</code>\",\n\t\"nmd_i2\": \"削除権限がないため、<code>.{0}</code> ファイルのみを作成できます\",\n\n\t\"tv_load\": \"テキストドキュメントの読み込み中:\\n\\n{0}\\n\\n{1}%（{2} / {3} MiB ロード済み）\",\n\t\"tv_xe1\": \"テキストファイルを読み込めませんでした:\\n\\nエラー \",\n\t\"tv_xe2\": \"404、ファイルが見つかりません\",\n\t\"tv_lst\": \"テキストファイルの一覧\",\n\t\"tvt_close\": \"フォルダビューに戻る$Nホットキー: M（または Esc）\\\">❌ 閉じる\",\n\t\"tvt_dl\": \"このファイルをダウンロード$Nホットキー: Y\\\">💾 ダウンロード\",\n\t\"tvt_prev\": \"前のドキュメントを表示$Nホットキー: i\\\">⬆ 前へ\",\n\t\"tvt_next\": \"次のドキュメントを表示$Nホットキー: K\\\">⬇ 次へ\",\n\t\"tvt_sel\": \"ファイルの選択 &nbsp;（切り取り/コピー/削除/...）$Nホットキー: S\\\">選択\",\n\t\"tvt_j\": \"jsonの整形$Nホットキー: shift-J\\\">j\",\n\t\"tvt_edit\": \"テキストエディタでファイルを開く$Nホットキー: E\\\">✏️ 編集\",\n\t\"tvt_tail\": \"ファイルの変更を監視する; 新しい行をリアルタイムで表示する\\\">📡 監視\",\n\t\"tvt_wrap\": \"ワードラップ\\\">↵\",\n\t\"tvt_atail\": \"ページ最下部までスクロールをロック\\\">⚓\",\n\t\"tvt_ctail\": \"端末カラーをデコード（ANSIエスケープコード）\\\">🌈\",\n\t\"tvt_ntail\": \"スクロールバック制限（読み込むテキストの最大バイト数）\",\n\n\t\"m3u_add1\": \"曲がm3uプレイリストに追加されました\",\n\t\"m3u_addn\": \"{0}曲をm3uプレイリストに追加しました\",\n\t\"m3u_clip\": \"m3uプレイリストがクリップボードにコピーされました\\n\\nsomething.m3uという新しいテキストファイルを作成し、そのドキュメントにプレイリストを貼り付けます; これで再生可能になります\",\n\n\t\"gt_vau\": \"動画は表示せず音声のみを再生する\\\">🎧\",\n\t\"gt_msel\": \"ファイル選択を有効にする; ファイルをCtrlキーを押しながらクリックすると上書き保存します$N$N&lt;em&gt;有効時: ファイル / フォルダをダブルクリックすると開きます&lt;/em&gt;$N$Nホットキー: S\\\">複数選択\",\n\t\"gt_crop\": \"中央切り抜きのサムネイル\\\">切抜き\",\n\t\"gt_3x\": \"高解像度サムネイル\\\">3x\",\n\t\"gt_zoom\": \"拡大\",\n\t\"gt_chop\": \"切断\",\n\t\"gt_sort\": \"並べ替え\",\n\t\"gt_name\": \"名前\",\n\t\"gt_sz\": \"サイズ\",\n\t\"gt_ts\": \"日付\",\n\t\"gt_ext\": \"種類\",\n\t\"gt_c1\": \"ファイル名をさらに短縮する（表示を減らす）\",\n\t\"gt_c2\": \"ファイル名を短縮する（詳細を表示）\",\n\n\t\"sm_w8\": \"検索中...\",\n\t\"sm_prev\": \"以下の検索結果は以前のクエリからのものです:\\n  \",\n\t\"sl_close\": \"検索結果を閉じる\",\n\t\"sl_hits\": \"{0}件の結果を表示中\",\n\t\"sl_moar\": \"さらに読み込む\",\n\n\t\"s_sz\": \"サイズ\",\n\t\"s_dt\": \"日付\",\n\t\"s_rd\": \"パス\",\n\t\"s_fn\": \"名前\",\n\t\"s_ta\": \"タグ\",\n\t\"s_ua\": \"Up@\",\n\t\"s_ad\": \"詳細.\",\n\t\"s_s1\": \"最小 MiB\",\n\t\"s_s2\": \"最大 MiB\",\n\t\"s_d1\": \"古い. iso8601\",\n\t\"s_d2\": \"新しい. iso8601\",\n\t\"s_u1\": \"アップロード後\",\n\t\"s_u2\": \"および/または前\",\n\t\"s_r1\": \"パスに含まれる &nbsp;（スペース区切り）\",\n\t\"s_f1\": \"名前に含まれる &nbsp;（-nopeで否定）\",\n\t\"s_t1\": \"タグに含まれる &nbsp;（^=開始、終了=$）\",\n\t\"s_a1\": \"特定のメタデータプロパティ\",\n\n\t\"md_eshow\": \"レンダリングできません \",\n\t\"md_off\": \"[📜<em>readme</em>] 無効 [⚙️] -- ドキュメントを非表示\",\n\n\t\"badreply\": \"サーバーからの応答を解析できませんでした\",\n\n\t\"xhr403\": \"403: アクセスが拒否されました\\n\\nF5キーを押してみてください。ログアウトしている可能性があります\",\n\t\"xhr0\": \"不明（サーバーへの接続が失われたか、サーバーがオフラインになっている可能性があります）\",\n\t\"cf_ok\": \"申し訳ありません -- DD\" + wah + \"oS 保護が作動し\\n\\n約30秒で再開されます\\n\\n何も起こらない場合はF5キーを押してページを再読み込みしてください\",\n\t\"tl_xe1\": \"サブフォルダを一覧表示できませんでした:\\n\\nエラー \",\n\t\"tl_xe2\": \"404: フォルダーが見つかりません\",\n\t\"fl_xe1\": \"フォルダ内のファイルを一覧表示できませんでした:\\n\\nエラー \",\n\t\"fl_xe2\": \"404: フォルダーが見つかりません\",\n\t\"fd_xe1\": \"サブフォルダを作成できませんでした:\\n\\nエラー \",\n\t\"fd_xe2\": \"404: 親フォルダが見つかりません\",\n\t\"fsm_xe1\": \"メッセージを送信できませんでした:\\n\\nエラー \",\n\t\"fsm_xe2\": \"404: 親フォルダが見つかりません\",\n\t\"fu_xe1\": \"サーバーから投稿取り消しリストを読み込めませんでした:\\n\\nエラー \",\n\t\"fu_xe2\": \"404: ファイルが見つかりません？？\",\n\n\t\"fz_tar\": \"非圧縮gnu-tarファイル（Linux / Mac）\",\n\t\"fz_pax\": \"非圧縮pax形式のtar（遅い）\",\n\t\"fz_targz\": \"gzip圧縮レベル3のgnu-tar$N$Nこれは通常非常に遅いので$N代わりに非圧縮tarを使用してください。\",\n\t\"fz_tarxz\": \"xz圧縮レベル1のgnu-tar$N$Nこれは通常非常に遅いので$N代わりに非圧縮のtarを使用してください。\",\n\t\"fz_zip8\": \"UTF8ファイル名のzip形式（Windows7以前では動作が不安定になる可能性があります）\",\n\t\"fz_zipd\": \"非常に古いソフトウェア用の従来のcp437ファイル名のzip\",\n\t\"fz_zipc\": \"cp437はcrc32を早期に計算します$NMS-DOS PKZIP v2.04g用（1993年10月）$N（ダウンロードを開始するまでの処理に時間がかかります）\",\n\n\t\"un_m1\": \"最近アップロードした動画を削除したり未完成の動画を中止したりできます。\",\n\t\"un_upd\": \"更新\",\n\t\"un_m4\": \"または以下のファイルを共有する:\",\n\t\"un_ulist\": \"表示\",\n\t\"un_ucopy\": \"コピー\",\n\t\"un_flt\": \"任意のフィルター:&nbsp; URLには以下を含める必要があります\",\n\t\"un_fclr\": \"フィルターをクリア\",\n\t\"un_derr\": '未投稿の削除に失敗しました:\\n',\n\t\"un_f5\": '何かが壊れています。リロードするかF5キーを押してみてください。',\n\t\"un_uf5\": \"申し訳ありませんが、このアップロードを中止するにはページを更新する必要があります（例:F5キーまたはCtrl+Rキーを押す）\",\n\t\"un_nou\": '<b>警告:</b> サーバーが混雑しているため未完了のアップロードを表示できません; しばらくしてから「更新」リンクをクリックしてください',\n\t\"un_noc\": '<b>警告:</b> 完全にアップロードされたファイルの未投稿化はサーバー設定で有効化 / 許可されていません',\n\t\"un_max\": \"最初の2000件のファイルを表示中 (フィルターを使用)\",\n\t\"un_avail\": \"{0}件の最近のアップロードを削除できます<br />未完了のものは{1}件中止できます\",\n\t\"un_m2\": \"アップロード日時順に並べ替え; 新しい順:\",\n\t\"un_no1\": \"冗談だよ！アップロードされたものはどれも最新じゃない\",\n\t\"un_no2\": \"冗談だよ！そのフィルターに一致するアップロードに最新のものはありません\",\n\t\"un_next\": \"次の{0}件のファイルを削除します\",\n\t\"un_abrt\": \"中止\",\n\t\"un_del\": \"削除\",\n\t\"un_m3\": \"最近のアップロードを読み込み中...\",\n\t\"un_busy\": \"{0}件のファイルを削除中...\",\n\t\"un_clip\": \"{0}件のリンクをクリップボードにコピーしました\",\n\n\t\"u_https1\": \"あなたはするべき\",\n\t\"u_https2\": \"httpsに切り替える\",\n\t\"u_https3\": \"パフォーマンス向上のため\",\n\t\"u_ancient\": '現在のブラウザは驚くほど古いです -- <a href=\"#\" onclick=\"goto(\\'bup\\')\">代わりにbup</a>を使った方がいいかもしれません',\n\t\"u_nowork\": \"Firefox 53以降、Chrome 57以降、またはiOS 11以降が必要です\",\n\t\"tail_2old\": \"Firefox 105以降、Chrome 71以降、またはiOS 14.5以降が必要です\",\n\t\"u_nodrop\": '現在のブラウザは古すぎてドラッグ＆ドロップによるアップロードに対応していません',\n\t\"u_notdir\": \"それはフォルダではありません！\\n\\nブラウザが古すぎです、\\n代わりにドラッグドロップを試してください\",\n\t\"u_uri\": \"他のブラウザウィンドウから画像をドラッグアンドドロップするには、\\n大きなアップロードボタンにドロップしてください\",\n\t\"u_enpot\": '<a href=\"#\">potato UI</a>に切り替える （アップロード速度が向上する可能性があります）',\n\t\"u_depot\": '<a href=\"#\">fancy UI</a>に切り替える（アップロード速度が低下する可能性があります）',\n\t\"u_gotpot\": 'アップロード速度を向上させるためにポテトUIに切り替えました。\\n\\n同意しない場合は遠慮なく元に戻してください！',\n\t\"u_pott\": \"<p>ファイル: &nbsp; <b>{0}</b> 完了済み, &nbsp; <b>{1}</b> 失敗, &nbsp; <b>{2}</b> ビジー, &nbsp; <b>{3}</b> キュー内</p>\",\n\t\"u_ever\": \"これは基本的なアップローダーです; up2kは少なくとも以下が必要です<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1\",\n\t\"u_su2k\": 'これは基本的なアップローダーです; <a href=\"#\" id=\"u2yea\">up2k</a>の方が優れています',\n\t\"u_uput\": '速度を最適化（チェックサムをスキップ）',\n\t\"u_ewrite\": 'このフォルダへの書き込み権限がありません',\n\t\"u_eread\": 'このフォルダーへの読み取りアクセス権がありません',\n\t\"u_enoi\": 'サーバー設定でファイル検索が有効になっていません',\n\t\"u_enoow\": \"ここでは上書きは機能しません; 削除権限が必要です\",\n\t\"u_badf\": 'これらの {0}件のファイル（合計 {1}件中）はファイルシステムの権限が原因でスキップされた可能性があります:\\n\\n',\n\t\"u_blankf\": 'これらの {0}件のファイル（合計 {1}件中）は空白です; それでもアップロードしますか？\\n\\n',\n\t\"u_applef\": 'これらの {0}件のファイル（合計 {1}件中）はおそらく不要です;\\n次のファイルをスキップするには<code>OK/Enter</code>を押してください、\\n除外しない場合は<code>キャンセル/ESC</code>を押してそれらもアップロードしてください:\\n\\n',\n\t\"u_just1\": '\\nファイルを1つだけ選択した方がうまくいくかもしれません',\n\t\"u_ff_many\": \"<b>Linux / MacOS / Android,</b>を使用している場合この量のファイルにより<a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\">Firefoxがクラッシュする<em>可能性</em></a>があります\\nその場合はもう一度お試しください（または Chrome を使用してください）。\",\n\t\"u_up_life\": \"このアップロードは {0}後に\\nサーバーから削除されます。\",\n\t\"u_asku\": 'これらの{0}件のファイルを<code>{1}</code>にアップロードします',\n\t\"u_unpt\": \"左上のボタン🧯を使ってこのアップロードを取り消し / 削除できます\",\n\t\"u_bigtab\": '{0}件のファイルを表示しようとしています\\n\\nブラウザがクラッシュする可能性がありますが、よろしいですか？',\n\t\"u_scan\": 'ファイルをスキャン中...',\n\t\"u_dirstuck\": 'ディレクトリ走査が次の {0}件のアイテムへのアクセス中に停止しました; スキップします:',\n\t\"u_etadone\": '完了 ({0}, {1}件のファイル)',\n\t\"u_etaprep\": '（アップロード準備中）',\n\t\"u_hashdone\": 'ハッシュ化完了',\n\t\"u_hashing\": 'ハッシュ',\n\t\"u_hs\": 'ハンドシェイク中...',\n\t\"u_started\": \"ファイルをアップロード中です; 参照 [🚀]\",\n\t\"u_dupdefer\": \"複製; 他のすべてのファイルの処理完了後に処理されます\",\n\t\"u_actx\": \"他のウィンドウ/タブに切り替えるときに<br />パフォーマンスの低下を防ぐにはこのテキストをクリックしてください\",\n\t\"u_fixed\": \"OK!&nbsp; 修正しました 👍\",\n\t\"u_cuerr\": \"{0} / {1} のチャンクのアップロードに失敗しました;\\nおそらく無害、継続中\\n\\nファイル: {2}\",\n\t\"u_cuerr2\": \"サーバーがアップロードを拒否しました（{0} / {1}のチャンク）;\\n後で再試行します\\n\\nファイル: {2}\\n\\nエラー \",\n\t\"u_ehstmp\": \"再試行します; 右下を参照\",\n\t\"u_ehsfin\": \"サーバーがアップロードの完了リクエストを拒否しました; 再試行中...\",\n\t\"u_ehssrch\": \"サーバーは検索実行リクエストを拒否しました; 再試行中...\",\n\t\"u_ehsinit\": \"サーバーはアップロード開始リクエストを拒否しました; 再試行中...\",\n\t\"u_eneths\": \"アップロードハンドシェイク中にネットワークエラーが発生しました; 再試行中...\",\n\t\"u_enethd\": \"ターゲットの存在をテスト中にネットワークエラーが発生しました; 再試行中...\",\n\t\"u_cbusy\": \"ネットワークの不具合後サーバーが再び信頼するのを待っています...\",\n\t\"u_ehsdf\": \"サーバーのディスク容量が不足しました！\\n\\n誰かが十分な空き領域を確保して続行できるようになるまで\\n再試行を続けます\",\n\t\"u_emtleak1\": \"お使いのウェブブラウザでメモリリークが発生している可能性があります;\\nお願いします\",\n\t\"u_emtleak2\": ' <a href=\"{0}\">httpsに切り替える（推奨）</a>もしくは',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": '次を試してください:\\n<ul><li><code>F5</code>を押してページを更新してください</li><li>次に&nbsp;<code>mt</code>を無効にします&nbsp; &nbsp;<code>⚙️ settings</code></li>ボタンをクリックし<li>もう一度アップロードしてみてください</li></ul>アップロードは少し遅くなりますが、仕方ないでしょう。\\nご迷惑をおかけして申し訳ありません！\\n\\n追記: chrome v107ではこの問題が<a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">修正されました</a>',\n\t\"u_emtleakf\": '次を試してください:\\n<ul><li><code>F5</code>を押してページを更新してください</li><li>アップロードUIで<code>🥔</code>（ポテト）を有効にして<li>もう一度アップロードを試してください</li></ul>\\n追記: firefoxは<a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">バグ修正が行われることを期待しています</a>',\n\t\"u_s404\": \"サーバー上にファイルが見つかりません\",\n\t\"u_expl\": \"説明\",\n\t\"u_maxconn\": \"ほとんどのブラウザではこの数が6に制限されていますが、Firefoxでは<code>about:config</code>の<code>connections-per-server</code>でこの数を増やすことができます\",\n\t\"u_tu\": '<p class=\"warn\">警告: ターボが有効になっている場合、<span>&nbsp;クライアントは不完全なアップロードを検出して再開できない可能性があります; ターボボタンのツールチップを見る</span></p>',\n\t\"u_ts\": '<p class=\"warn\">警告: ターボが有効になっている場合、<span>&nbsp;検索結果が間違っている可能性があります; ターボボタンのツールチップを見る</span></p>',\n\t\"u_turbo_c\": \"サーバー設定でターボが無効になっています\",\n\t\"u_turbo_g\": \"このボリューム内でディレクトリ一覧表示の権限がないため\\nターボを無効化しています\",\n\t\"u_life_cfg\": '<input id=\"lifem\" p=\"60\" />分後に自動削除（もしくは<input id=\"lifeh\" p=\"3600\" />時間）',\n\t\"u_life_est\": 'アップロードは削除されます <span id=\"lifew\" tt=\"local time\">---</span>',\n\t\"u_life_max\": 'このフォルダーの最大保存期間が\\n{0}に設定されています。',\n\t\"u_unp_ok\": '{0}に対して未投稿が許可されています',\n\t\"u_unp_ng\": '未投稿は許可されません',\n\t\"ue_ro\": 'このフォルダのアクセス権限は読み取り専用です\\n\\n',\n\t\"ue_nl\": '現在ログインしていません',\n\t\"ue_la\": '現在\"{0}\"としてログインしています',\n\t\"ue_sr\": '現在ファイル検索モードです\\n\\n虫眼鏡 🔎（大きな検索ボタンの横）をクリックしてアップロードモードに切り替え、もう一度アップロードしてみてください。\\n\\n申し訳ありません',\n\t\"ue_ta\": 'もう一度アップロードしてみてください。今度は動作するはずです。',\n\t\"ue_ab\": \"このファイルはすでに別のフォルダにアップロード中です。それが完了するまでこのファイルを他の場所にアップロードすることはできません。\\n\\n左上の🧯を使用して最初のアップロードを中止して忘れることができます。\",\n\t\"ur_1uo\": \"OK: ファイルのアップロードに成功しました\",\n\t\"ur_auo\": \"OK: {0}件すべてのファイルが正常にアップロードされました\",\n\t\"ur_1so\": \"OK: ファイルがサーバー上で見つかりました\",\n\t\"ur_aso\": \"OK: {0}件すべてのファイルがサーバー上で見つかりました\",\n\t\"ur_1un\": \"アップロードに失敗しました。申し訳ありません。\",\n\t\"ur_aun\": \"{0}件すべてのファイルのアップロードに失敗しました。申し訳ありません。\",\n\t\"ur_1sn\": \"サーバー上にファイルが見つかりません\",\n\t\"ur_asn\": \"サーバー上に {0}件のファイルが見つかりませんでした\",\n\t\"ur_um\": \"完了;\\n{0}件のアップロードはOKです,\\n{1}件のアップロードに失敗しました。申し訳ありません。\",\n\t\"ur_sm\": \"完了;\\n{0}件のファイルがサーバー上に見つかりました,\\n{1}件のファイルがサーバー上に見つかりませんでした\",\n\n\t\"rc_opn\": \"開く\",\n\t\"rc_ply\": \"再生\",\n\t\"rc_pla\": \"オーディオとして再生\",\n\t\"rc_txt\": \"ファイルビューアで開く\",\n\t\"rc_md\": \"markdownエディターで開く\",\n\t\"rc_dl\": \"ダウンロード\",\n\t\"rc_zip\": \"圧縮してダウンロード\",\n\t\"rc_cpl\": \"リンクをコピー\",\n\t\"rc_del\": \"削除\",\n\t\"rc_cut\": \"切り取り\",\n\t\"rc_cpy\": \"コピー\",\n\t\"rc_pst\": \"貼り付け\",\n\t\"rc_rnm\": \"名前を変更\", //m\n\t\"rc_nfo\": \"新しいフォルダ\",\n\t\"rc_nfi\": \"新しいファイル\",\n\t\"rc_sal\": \"すべて選択\",\n\t\"rc_sin\": \"選択を反転\",\n\t\"rc_shf\": \"このフォルダを共有\", //m\n\t\"rc_shs\": \"選択項目を共有\", //m\n\n\t\"lang_set\": \"変更を反映させるために更新しますか？\",\n\n\t\"splash\": {\n\t\t\"a1\": \"更新\",\n\t\t\"b1\": \"やあ、見知らぬ人 &nbsp; <small>（あなたはログインしていません）</small>\",\n\t\t\"c1\": \"ログアウト\",\n\t\t\"d1\": \"dump stack\",  // トピックノート: \"d2\" はこのボタンのツールチップです\n\t\t\"d2\": \"すべてのアクティブなスレッドの状態を表示します\",\n\t\t\"e1\": \"cfgの再読み込み\",\n\t\t\"e2\": \"設定ファイル（アカウント/ボリューム/ボリュームフラグ）を再読み込みして、$Nすべてのe2dsボリュームを再スキャンします$N$N注: グローバル設定の変更を$N有効にするには完全な再起動が必要です\",\n\t\t\"f1\": \"閲覧可能:\",\n\t\t\"g1\": \"アップロード加納:\",\n\t\t\"cc1\": \"その他:\",\n\t\t\"h1\": \"k304を無効化\",  // トピックノート: \"j1\" はk304が何であるかを説明します\n\t\t\"i1\": \"k304を有効化\",\n\t\t\"j1\": \"k304 を有効にすると、HTTP 304ごとにクライアントが切断されバグのあるプロキシがスタックするのを防ぐことができます（突然ページが読み込まれなくなるなど）<em>ただし</em>全体的に速度が低下します\",\n\t\t\"k1\": \"クライアント設定をリセット\",\n\t\t\"l1\": \"詳しくはログインしてください:\",\n\t\t\"ls3\": \"ログイン\",\n\t\t\"lu4\": \"ユーザー名\",\n\t\t\"lp4\": \"パスワード\",\n\t\t\"lo3\": \"「{0}」をどこからでもログアウトする\",\n\t\t\"lo2\": \"これによりすべてのブラウザでセッションが終了します\",\n\t\t\"m1\": \"おかえりなさい、\",  // トピックノート: \"おかえりなさい、ユーザー名\"\n\t\t\"n1\": \"404 not found &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": 'あるいはアクセスできないかもしれません -- パスワードを試すか <a href=\"' + SR + '/?h\">ホームに戻ってください</a>',\n\t\t\"p1\": \"403 forbiddena &nbsp;~┻━┻\",\n\t\t\"q1\": 'パスワードを使うか <a href=\"' + SR + '/?h\">ホームに戻ってください</a>',\n\t\t\"r1\": \"ホームに戻る\",\n\t\t\".s1\": \"再スキャン\",\n\t\t\"t1\": \"アクション\",  // トピックノート: これは\"再スキャン\"ボタンの上のヘッダーです\n\t\t\"u2\": \"最後のサーバー書き込みからの時間$N（アップロード / 名前変更 / ...）$N$N17d = 17日$N1h23 = 1時間23分$N4m56 = 4分56秒\",\n\t\t\"v1\": \"接続\",\n\t\t\"v2\": \"このサーバーをローカルHDDとして使用する\",\n\t\t\"w1\": \"httpsへ切り替え\",\n\t\t\"x1\": \"パスワードの変更\",\n\t\t\"y1\": \"共有の編集\",  // トピックノート: ユーザーが共有することを決定したフォルダのリストを表示します\n\t\t\"z1\": \"この共有のロックを解除する:\",  // トピックノート: 隠し共有を表示するためのパスワードプロンプト\n\t\t\"ta1\": \"最初に新しいパスワードを入力してください\",\n\t\t\"ta2\": \"確認のため新しいパスワードを再入力してください:\",\n\t\t\"ta3\": \"タイプミスを発見しました; もう一度試してください\",\n\t\t\"nop\": \"エラー: パスワードは空白にできません\",\n\t\t\"nou\": \"エラー: ユーザー名とパスワードは空白にできません\",\n\t\t\"aa1\": \"受信ファイル:\",\n\t\t\"ab1\": \"no304を無効化\",\n\t\t\"ac1\": \"no304を有効化\",\n\t\t\"ad1\": \"no304を有効にするとすべてのキャッシュが無効になります; k304 では不十分な場合はこちらをお試しください。これにより膨大な量のネットワークトラフィックが無駄になります！\",\n\t\t\"ae1\": \"アクティブなダウンロード:\",\n\t\t\"af1\": \"最近のアップロードを表示\",\n\t\t\"ag1\": \"idpキャッシュを表示\",  // トピックノート: IdPユーザーを管理できるページへのリンクです\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/kor.js",
    "content": "\n// //m으로 끝나는 줄은 검증되지 않은 기계 번역입니다\n\nLs.kor = {\n\t\"tt\": \"한국어\",\n\n\t\"cols\": {\n\t\t\"c\": \"작업 버튼\",\n\t\t\"dur\": \"길이\",\n\t\t\"q\": \"품질/비트레이트\",\n\t\t\"Ac\": \"오디오 코덱\",\n\t\t\"Vc\": \"비디오 코덱\",\n\t\t\"Fmt\": \"형식/컨테이너\",\n\t\t\"Ahash\": \"오디오 체크섬\",\n\t\t\"Vhash\": \"비디오 체크섬\",\n\t\t\"Res\": \"해상도\",\n\t\t\"T\": \"파일 유형\",\n\t\t\"aq\": \"오디오 품질/비트레이트\",\n\t\t\"vq\": \"비디오 품질/비트레이트\",\n\t\t\"pixfmt\": \"서브샘플링/픽셀 구조\",\n\t\t\"resw\": \"가로 해상도\",\n\t\t\"resh\": \"세로 해상도\",\n\t\t\"chs\": \"오디오 채널\",\n\t\t\"hz\": \"샘플레이트\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"기타\",\n\t\t\t[\"ESC\", \"다양한 창 닫기\"],\n\n\t\t\t\"파일 관리자\",\n\t\t\t[\"G\", \"목록/그리드 보기 전환\"],\n\t\t\t[\"T\", \"썸네일/아이콘 전환\"],\n\t\t\t[\"⇧ A/D\", \"썸네일 이미지 크기\"],\n\t\t\t[\"ctrl-K\", \"선택 항목 삭제\"],\n\t\t\t[\"ctrl-X\", \"선택 항목 잘라내기\"],\n\t\t\t[\"ctrl-C\", \"선택 항목 복사\"],\n\t\t\t[\"ctrl-V\", \"여기에 붙여넣기 (이동/복사)\"],\n\t\t\t[\"Y\", \"선택 항목 다운로드\"],\n\t\t\t[\"F2\", \"선택 항목 이름 바꾸기\"],\n\n\t\t\t\"파일 목록 선택\",\n\t\t\t[\"space\", \"파일 선택/해제\"],\n\t\t\t[\"↑/↓\", \"선택 커서 이동\"],\n\t\t\t[\"ctrl ↑/↓\", \"커서와 뷰포트 동시 이동\"],\n\t\t\t[\"⇧ ↑/↓\", \"이전/다음 파일 선택\"],\n\t\t\t[\"ctrl-A\", \"모든 파일/폴더 선택\"],\n\t\t], [\n\t\t\t\"탐색\",\n\t\t\t[\"B\", \"브레드크럼/탐색창 전환\"],\n\t\t\t[\"I/K\", \"이전/다음 폴더\"],\n\t\t\t[\"M\", \"상위 폴더 (또는 현재 항목 닫기)\"],\n\t\t\t[\"V\", \"탐색창에 폴더/텍스트 파일 표시 전환\"],\n\t\t\t[\"A/D\", \"탐색창 크기\"],\n\t\t], [\n\t\t\t\"오디오 플레이어\",\n\t\t\t[\"J/L\", \"이전/다음 곡\"],\n\t\t\t[\"U/O\", \"10초 뒤로/앞으로 건너뛰기\"],\n\t\t\t[\"0..9\", \"0%..90% 지점으로 이동\"],\n\t\t\t[\"P\", \"재생/일시정지 (시작 포함)\"],\n\t\t\t[\"S\", \"재생 중인 곡 선택\"],\n\t\t\t[\"Y\", \"곡 다운로드\"],\n\t\t], [\n\t\t\t\"이미지 뷰어\",\n\t\t\t[\"J/L, ←/→\", \"이전/다음 이미지\"],\n\t\t\t[\"Home/End\", \"첫/마지막 이미지\"],\n\t\t\t[\"F\", \"전체 화면\"],\n\t\t\t[\"R\", \"시계 방향으로 회전\"],\n\t\t\t[\"⇧ R\", \"반시계 방향으로 회전\"],\n\t\t\t[\"S\", \"이미지 선택\"],\n\t\t\t[\"Y\", \"이미지 다운로드\"],\n\t\t], [\n\t\t\t\"비디오 플레이어\",\n\t\t\t[\"U/O\", \"10초 뒤로/앞으로 건너뛰기\"],\n\t\t\t[\"P/K/Space\", \"재생/일시정지\"],\n\t\t\t[\"C\", \"다음 파일 계속 재생\"],\n\t\t\t[\"V\", \"반복\"],\n\t\t\t[\"M\", \"음소거\"],\n\t\t\t[\"[ 와 ]\", \"반복 구간 설정\"],\n\t\t], [\n\t\t\t\"텍스트 파일 뷰어\",\n\t\t\t[\"I/K\", \"이전/다음 파일\"],\n\t\t\t[\"M\", \"텍스트 파일 닫기\"],\n\t\t\t[\"E\", \"텍스트 파일 편집\"],\n\t\t\t[\"S\", \"파일 선택 (잘라내기/복사/이름 바꾸기용)\"],\n\t\t\t[\"Y\", \"텍스트 파일 다운로드\"], //m\n\t\t\t[\"⇧ J\", \"json 미화\"], //m\n\t\t]\n\t],\n\n\t\"m_ok\": \"확인\",\n\t\"m_ng\": \"취소\",\n\n\t\"enable\": \"활성화\",\n\t\"danger\": \"위험\",\n\t\"clipped\": \"클립보드에 복사되었습니다\",\n\n\t\"ht_s1\": \"초\",\n\t\"ht_s2\": \"초\",\n\t\"ht_m1\": \"분\",\n\t\"ht_m2\": \"분\",\n\t\"ht_h1\": \"시간\",\n\t\"ht_h2\": \"시간\",\n\t\"ht_d1\": \"일\",\n\t\"ht_d2\": \"일\",\n\t\"ht_and\": \" \",\n\n\t\"goh\": \"제어판\",\n\t\"gop\": '이전 형제 폴더\">이전',\n\t\"gou\": '상위 폴더\">위로',\n\t\"gon\": '다음 폴더\">다음',\n\t\"logout\": \"로그아웃 \",\n\t\"login\": \"로그인\", //m\n\t\"access\": \" 액세스\",\n\t\"ot_close\": \"하위 메뉴 닫기\",\n\t\"ot_search\": \"`속성, 경로/이름, 음악 태그 또는 이들의 조합으로 파일을 검색합니다.$N$N`foo bar` = «foo»와 «bar»를 모두 포함해야 함,$N`foo -bar` = «foo»는 포함하지만 «bar»는 포함하지 않아야 함,$N`^yana .opus$` = «yana»로 시작하고 «opus» 파일이어야 함$N`&quot;try unite&quot;` = 정확히 «try unite»를 포함해야 함$N$N날짜 형식은 ISO-8601입니다. 예:$N`2009-12-31` 또는 `2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"주워담기: 최근 업로드한 항목을 삭제하거나 미완료된 업로드를 중단합니다\",\n\t\"ot_bup\": \"bup: 기본 업로더. 넷스케이프 4.0도 지원합니다\",\n\t\"ot_mkdir\": \"mkdir: 새 디렉터리를 만듭니다\",\n\t\"ot_md\": \"new-file: 새 텍스트 파일을 만듭니다\", //m\n\t\"ot_msg\": \"msg: 서버 로그에 메시지를 보냅니다\",\n\t\"ot_mp\": \"미디어 플레이어 옵션\",\n\t\"ot_cfg\": \"구성 옵션\",\n\t\"ot_u2i\": 'up2k: (쓰기 권한이 있는 경우) 파일을 업로드하거나, 검색 모드로 전환하여 서버 어딘가에 파일이 있는지 확인합니다.$N$N업로드는 재개 가능하고, 멀티스레드로 작동하며, 파일 타임스탬프가 보존되지만, [🎈] (기본 업로더)보다 CPU를 더 많이 사용합니다.<br /><br />업로드 중에는 이 아이콘이 진행률 표시창이 됩니다!',\n\t\"ot_u2w\": 'up2k: 이어올리기 기능을 지원하는 파일 업로더입니다 (브라우저를 닫았다가 나중에 동일한 파일을 끌어다 놓으세요).$N$N멀티스레드로 작동하며, 파일 타임스탬프가 보존되지만, [🎈] (기본 업로더)보다 CPU를 더 많이 사용합니다.<br /><br />업로드 중에는 이 아이콘이 진행률 표시창이 됩니다!',\n\t\"ot_noie\": 'Chrome / Firefox / Edge를 사용해주세요',\n\n\t\"ab_mkdir\": \"디렉터리 만들기\",\n\t\"ab_mkdoc\": \"새 텍스트 파일\", //m\n\t\"ab_msg\": \"서버 로그에 메시지 보내기\",\n\n\t\"ay_path\": \"폴더로 건너뛰기\",\n\t\"ay_files\": \"파일로 건너뛰기\",\n\n\t\"wt_ren\": \"선택한 항목 이름 바꾸기$N단축키: F2\",\n\t\"wt_del\": \"선택한 항목 삭제$N단축키: ctrl-K\",\n\t\"wt_cut\": \"선택한 항목 잘라내기 &lt;small&gt;(다른 곳에 붙여넣기용)&lt;/small&gt;$N단축키: ctrl-X\",\n\t\"wt_cpy\": \"선택한 항목 클립보드에 복사$N(다른 곳에 붙여넣기용)$N단축키: ctrl-C\",\n\t\"wt_pst\": \"이전에 잘라내거나 복사한 항목 붙여넣기$N단축키: ctrl-V\",\n\t\"wt_selall\": \"모든 파일 선택$N단축키: ctrl-A (파일에 포커스된 경우)\",\n\t\"wt_selinv\": \"선택 반전\",\n\t\"wt_zip1\": \"이 폴더를 압축 파일로 다운로드\",\n\t\"wt_selzip\": \"선택 항목을 압축 파일로 다운로드\",\n\t\"wt_seldl\": \"선택 항목을 개별 파일로 다운로드$N단축키: Y\",\n\t\"wt_npirc\": \"IRC 형식 트랙 정보 복사\",\n\t\"wt_nptxt\": \"일반 텍스트 트랙 정보 복사\",\n\t\"wt_m3ua\": \"m3u 재생 목록에 추가 (나중에 <code>📻복사</code> 클릭)\",\n\t\"wt_m3uc\": \"m3u 재생 목록을 클립보드에 복사\",\n\t\"wt_grid\": \"그리드/목록 보기 전환$N단축키: G\",\n\t\"wt_prev\": \"이전 트랙$N단축키: J\",\n\t\"wt_play\": \"재생/일시정지$N단축키: P\",\n\t\"wt_next\": \"다음 트랙$N단축키: L\",\n\n\t\"ul_par\": \"동시 업로드:\",\n\t\"ut_rand\": \"파일명 무작위로 만들기\",\n\t\"ut_u2ts\": \"사용자 파일 시스템의 마지막 수정 타임스탬프를$N서버에 복사\\\">📅\",\n\t\"ut_ow\": \"서버에 있는 기존 파일을 덮어쓸까요?$N🛡️: 안 함 (대신 새 파일 이름 생성)$N🕒: 서버 파일이 더 오래된 경우 덮어쓰기$N♻️: 파일이 다르면 항상 덮어쓰기$N⏭️: 기존 파일을 모두 무조건 건너뜀\", //m\n\t\"ut_mt\": \"업로드 중 다른 파일 해싱 계속하기$N$NCPU 또는 HDD가 병목 현상을 일으키는 경우 비활성화하세요\",\n\t\"ut_ask\": '업로드 시작 전 확인 요청\">💭',\n\t\"ut_pot\": \"느린 기기에서 UI를 단순화하여$N업로드 속도 향상\",\n\t\"ut_srch\": \"실제로 업로드하는 대신, 파일이 이미 서버에 있는지 확인합니다$N(읽을 수 있는 모든 폴더를 스캔합니다)\",\n\t\"ut_par\": \"0으로 설정하여 업로드 일시정지$N$N연결이 느리거나 지연 시간이 길면 늘리세요$N$NLAN 환경이거나 서버 HDD가 병목 현상을 일으키면 1로 유지하세요\",\n\t\"ul_btn\": \"파일/폴더를 여기에<br>끌어다 놓거나 클릭하세요\",\n\t\"ul_btnu\": \"업 로 드\",\n\t\"ul_btns\": \"검 색\",\n\n\t\"ul_hash\": \"해싱\",\n\t\"ul_send\": \"전송\",\n\t\"ul_done\": \"완료\",\n\t\"ul_idle1\": \"대기 중인 업로드가 없습니다\",\n\t\"ut_etah\": \"평균 &lt;em&gt;해싱&lt;/em&gt; 속도 및 예상 완료 시간\",\n\t\"ut_etau\": \"평균 &lt;em&gt;업로드&lt;/em&gt; 속도 및 예상 완료 시간\",\n\t\"ut_etat\": \"평균 &lt;em&gt;총&lt;/em&gt; 속도 및 예상 완료 시간\",\n\n\t\"uct_ok\": \"성공적으로 완료됨\",\n\t\"uct_ng\": \"문제 발생: 실패/거부/찾을 수 없음\",\n\t\"uct_done\": \"완료됨 (성공 및 문제 발생 포함)\",\n\t\"uct_bz\": \"해싱 또는 업로드 중\",\n\t\"uct_q\": \"대기 중, 보류 중\",\n\n\t\"utl_name\": \"파일명\",\n\t\"utl_ulist\": \"목록\",\n\t\"utl_ucopy\": \"복사\",\n\t\"utl_links\": \"링크\",\n\t\"utl_stat\": \"상태\",\n\t\"utl_prog\": \"진행률\",\n\n\t// keep short:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"오류\",\n\t\"utl_oserr\": \"OS 오류\",\n\t\"utl_found\": \"찾음\",\n\t\"utl_defer\": \"보류\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"완료\",\n\n\t\"ul_flagblk\": \"파일이 대기열에 추가되었습니다.</b><br>하지만 다른 브라우저 탭에서 up2k가 실행 중이므로,<br>해당 작업이 끝날 때까지 기다립니다.\",\n\t\"ul_btnlk\": \"서버 구성에서 이 스위치를 현재 상태로 잠갔습니다.\",\n\n\t\"udt_up\": \"업로드\",\n\t\"udt_srch\": \"검색\",\n\t\"udt_drop\": \"여기에 놓으세요\",\n\n\t\"u_nav_m\": '<h6>자, 갖고 있는 게 무엇인가?</h6><code>Enter</code> = 파일 (하나 이상)\\n<code>ESC</code> = 폴더 하나 (하위 폴더 포함)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">파일</a><a href=\"#\" id=\"modal-ng\">폴더 하나</a>',\n\n\t\"cl_opts\": \"스위치\",\n\t\"cl_hfsz\": \"파일 크기\", //m\n\t\"cl_themes\": \"테마\",\n\t\"cl_langs\": \"언어\",\n\t\"cl_ziptype\": \"폴더 다운로드\",\n\t\"cl_uopts\": \"up2k 스위치\",\n\t\"cl_favico\": \"파비콘\",\n\t\"cl_bigdir\": \"큰 디렉터리\",\n\t\"cl_hsort\": \"#sort\",\n\t\"cl_keytype\": \"조성 표기법\",\n\t\"cl_hiddenc\": \"숨겨진 열\",\n\t\"cl_hidec\": \"숨기기\",\n\t\"cl_reset\": \"초기화\",\n\t\"cl_hpick\": \"아래 테이블에서 숨기고 싶은 열의 헤더를 탭하세요\",\n\t\"cl_hcancel\": \"열 숨기기가 중단되었습니다\",\n\t\"cl_rcm\": \"우클릭 메뉴\", //m\n\n\t\"ct_grid\": \"田 그리드\",\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ 도움말',\n\t\"ct_thumb\": '그리드 보기에서 아이콘 또는 미리보기 이미지 전환$N단축키: T\">🖼️ 미리보기',\n\t\"ct_csel\": '그리드 보기에서 CTRL과 SHIFT를 사용하여 파일 선택\">선택',\n\t\"ct_dsel\": '그리드 보기에서 드래그 선택 사용\">드래그', //m\n\t\"ct_dl\": '파일을 클릭하면 다운로드를 강제로 수행 (인라인으로 표시하지 않음)\">dl', //m\n\t\"ct_ihop\": '이미지 뷰어를 닫으면 마지막으로 본 파일로 스크롤\">g⮯',\n\t\"ct_dots\": '숨김 파일 표시 (서버가 허용하는 경우)\">숨김파일',\n\t\"ct_qdel\": '파일 삭제 시 한 번만 확인 요청\">빠른삭제',\n\t\"ct_dir1st\": '폴더를 파일보다 먼저 정렬\">📁 먼저',\n\t\"ct_nsort\": '자연어 정렬 (파일명의 숫자를 인식)\">자연어정렬',\n\t\"ct_utc\": '모든 날짜/시간을 UTC로 표시\">UTC',\n\t\"ct_readme\": '폴더 목록에 README.md 표시\">📜 readme',\n\t\"ct_idxh\": '폴더 목록 대신 index.html 표시\">htm',\n\t\"ct_sbars\": '스크롤바 표시\">⟊',\n\n\t\"cut_umod\": '파일이 서버에 이미 있는 경우, 서버의 마지막 수정 타임스탬프를 로컬 파일과 일치하도록 업데이트합니다 (쓰기+삭제 권한 필요).\\\">re📅',\n\n\t\"cut_turbo\": 'YOLO 버튼. 아마 활성화하고 싶지 않으실 겁니다.$N$N대량의 파일을 업로드하다가 어떤 이유로 재시작해야 할 때, 최대한 빨리 업로드를 계속하고 싶을 때 사용하세요.$N$N이 옵션은 해시 확인을 단순히 <em>&quot;서버에 동일한 파일 크기를 가진 파일이 있는가?&quot;</em>로 대체하므로, 파일 내용만 다를 경우 업로드되지 않습니다.$N$N업로드가 끝나면 이 옵션을 끄고, 동일한 파일을 다시 \\\"업로드\\\"하여 클라이언트가 검증하도록 해야 합니다.\\\">turbo',\n\n\t\"cut_datechk\": '터보 버튼이 활성화되어 있지 않으면 효과가 없습니다.$N$NYOLO의 위험성을 약간 줄여줍니다. 서버의 파일 타임스탬프가 사용자의 것과 일치하는지 확인합니다.$N$N<em>이론적으로는</em> 대부분의 미완료/손상된 업로드를 잡아내지만, 터보를 비활성화하고 검증 과정을 거치는 것을 대체할 수는 없습니다.\\\">날짜확인',\n\n\t\"cut_u2sz\": \"각 업로드 청크의 크기 (MiB)입니다. 큰 값은 태평양을 건너는 데 더 유리합니다. 매우 불안정한 연결에서는 낮은 값을 시도해보세요.\",\n\n\t\"cut_flag\": '한 번에 하나의 탭만 업로드하도록 보장합니다.$N-- 다른 탭도 이 옵션을 활성화해야 합니다.$N-- 동일한 도메인의 탭에만 영향을 미칩니다.',\n\n\t\"cut_az\": '가장 작은 파일 우선이 아닌 알파벳 순서로 파일을 업로드합니다.$N$N알파벳 순서는 서버에서 문제가 발생했는지 눈으로 확인하기 쉽게 해주지만, 광랜/LAN 환경에서는 업로드 속도가 약간 느려집니다.',\n\n\t\"cut_nag\": '업로드 완료 시 OS 알림$N(브라우저나 탭이 활성화되지 않은 경우에만)',\n\t\"cut_sfx\": '업로드 완료 시 소리 알림$N(브라우저나 탭이 활성화되지 않은 경우에만)',\n\n\t\"cut_mt\": '멀티스레딩을 사용하여 파일 해싱 속도를 높입니다.$N$N이 기능은 웹 워커를 사용하며$N더 많은 RAM이 필요합니다 (추가적으로 최대 512 MiB).$N$Nhttps는 30% 더 빠르게, http는 4.5배 더 빠르게 만듭니다.\\\">mt',\n\n\t\"cut_wasm\": '브라우저 내장 해셔 대신 wasm을 사용합니다. 크롬 기반 브라우저에서 속도를 향상시키지만 CPU 부하를 증가시키며, 많은 구버전 크롬에는 이 기능을 활성화하면 모든 RAM을 소모하고 충돌하는 버그가 있습니다.\\\">wasm',\n\n\t\"cft_text\": \"파비콘 텍스트 (비워두고 새로고침하면 비활성화됨)\",\n\t\"cft_fg\": \"전경색\",\n\t\"cft_bg\": \"배경색\",\n\n\t\"cdt_lim\": \"폴더에 표시할 최대 파일 수\",\n\t\"cdt_ask\": \"맨 아래로 스크롤할 때$N더 많은 파일을 불러오는 대신$N무엇을 할지 묻기\",\n\t\"cdt_hsort\": \"`미디어 URL에 포함할 정렬 규칙 (`,sorthref`)의 수. 0으로 설정하면 미디어 링크를 클릭할 때 포함된 정렬 규칙도 무시됩니다.\",\n\t\"cdt_ren\": \"사용자 지정 우클릭 메뉴를 활성화합니다. shift 키를 누른 채 우클릭하면 기본 메뉴를 사용할 수 있습니다\\\">활성화\", //m\n\t\"cdt_rdb\": \"사용자 정의 우클릭 메뉴가 이미 열려 있을 때 다시 우클릭하면 기본 메뉴 표시\\\">x2\", //m\n\n\t\"tt_entree\": \"탐색 창 (디렉터리 트리 사이드바) 표시$N단축키: B\",\n\t\"tt_detree\": \"이동 경로 표시$N단축키: B\",\n\t\"tt_visdir\": \"선택한 폴더로 스크롤하기\",\n\t\"tt_ftree\": \"폴더 트리/텍스트 파일 전환$N단축키: V\",\n\t\"tt_pdock\": \"상위 폴더를 상단에 고정된 창에 표시\",\n\t\"tt_dynt\": \"트리가 확장될 때 자동으로 너비 증가\",\n\t\"tt_wrap\": \"자동 줄 바꿈\",\n\t\"tt_hover\": \"마우스를 올리면 넘어가는 줄 표시$N(마우스 커서가 왼쪽 여백에$N&nbsp; 있지 않으면 스크롤이 깨짐)\",\n\n\t\"ml_pmode\": \"폴더 끝에서...\",\n\t\"ml_btns\": \"명령\",\n\t\"ml_tcode\": \"트랜스코딩\",\n\t\"ml_tcode2\": \"다음으로 트랜스코딩\",\n\t\"ml_tint\": \"틴트\",\n\t\"ml_eq\": \"오디오 이퀄라이저\",\n\t\"ml_drc\": \"다이내믹 레인지 압축기\",\n\t\"ml_ss\": \"무음 건너뛰기\", //m\n\n\t\"mt_loop\": \"한 곡 반복 재생\\\">🔁\",\n\t\"mt_one\": \"한 곡 재생 후 중지\\\">1️⃣\",\n\t\"mt_shuf\": \"각 폴더의 곡을 무작위 재생\\\">🔀\",\n\t\"mt_aplay\": \"서버에 접속한 링크에 곡 ID가 있으면 자동 재생$N$N이것을 비활성화하면 음악 재생 시 페이지 URL이 곡 ID로 업데이트되지 않아, 이 설정이 손실되고 URL이 남아있을 경우 자동 재생되는 것을 방지합니다.\\\">a▶\",\n\t\"mt_preload\": \"끊김 없는 재생을 위해 다음 곡을 미리 불러오기 시작\\\">미리로드\",\n\t\"mt_prescan\": \"마지막 곡이 끝나기 전에 다음 폴더로 이동하여$N웹브라우저가 재생을 멈추지 않도록 합니다.\\\">탐색\",\n\t\"mt_fullpre\": \"전체 곡을 미리 불러오기 시도;$N✅ <b>불안정한</b> 연결에서 활성화,$N❌ <b>느린</b> 연결에서는 아마도 비활성화\\\">전체\",\n\t\"mt_fau\": \"폰에서 다음 곡이 충분히 빨리 미리 불러오지 않아 음악이 멈추는 것을 방지합니다 (태그 표시가 불안정해질 수 있음).\\\">☕️\",\n\t\"mt_waves\": \"파형 탐색 바:$N탐색 바에 오디오 진폭 표시\\\">~s\",\n\t\"mt_npclip\": \"현재 재생 중인 곡을 클립보드에 복사하는 버튼 표시\\\">/np\",\n\t\"mt_m3u_c\": \"선택한 곡을 m3u8 재생 목록 항목으로$N클립보드에 복사하는 버튼 표시\\\">📻\",\n\t\"mt_octl\": \"OS 통합 (미디어 단축키/OSD)\\\">os-ctl\",\n\t\"mt_oseek\": \"OS 통합을 통해 탐색 허용$N$N참고: 일부 기기 (iPhone)에서는$N이것이 다음 곡 버튼을 대체합니다.\\\">탐색\",\n\t\"mt_oscv\": \"OSD에 앨범 커버 표시\\\">아트\",\n\t\"mt_follow\": \"재생 중인 트랙이 보이도록 스크롤 유지\\\">🎯\",\n\t\"mt_compact\": \"컴팩트 컨트롤\\\">⟎\",\n\t\"mt_uncache\": \"캐시 지우기 (브라우저가 곡의 깨진 사본을 캐시하여$N재생이 안되는 경우 시도해보세요)\\\">캐시삭제\",\n\t\"mt_mloop\": \"열린 폴더 반복\\\">🔁 반복\",\n\t\"mt_mnext\": \"다음 폴더 불러오고 계속\\\">📂 다음\",\n\t\"mt_mstop\": \"재생 중지\\\">⏸ 중지\",\n\t\"mt_cflac\": \"flac/wav를 {0}로 변환\\\">flac\",\n\t\"mt_caac\": \"aac/m4a를 {0}로 변환\\\">aac\",\n\t\"mt_coth\": \"다른 모든 것 (mp3 제외)을 {0}로 변환\\\">기타\",\n\t\"mt_c2opus\": \"데스크톱, 노트북, 안드로이드 환경에 최적\\\">opus\",\n\t\"mt_c2owa\": \"iOS 17.5 이상용 opus-weba\\\">owa\",\n\t\"mt_c2caf\": \"iOS 11부터 17까지용 opus-caf\\\">caf\",\n\t\"mt_c2mp3\": \"매우 오래된 기기에서 사용\\\">mp3\",\n\t\"mt_c2flac\": \"최고 음질이지만 다운로드 용량이 큼\\\">flac\",\n\t\"mt_c2wav\": \"비압축 재생 (더 큼)\\\">wav\",\n\t\"mt_c2ok\": \"네, 좋은 선택입니다\",\n\t\"mt_c2nd\": \"기기에 권장되는 출력 형식이 아니지만 괜찮습니다\",\n\t\"mt_c2ng\": \"기기가 이 출력 형식을 지원하지 않는 것 같지만, 시도해 보겠습니다\",\n\t\"mt_xowa\": \"iOS에서 이 형식의 백그라운드 재생이 안되는 버그가 있습니다. 대신 caf나 mp3를 사용해주세요.\",\n\t\"mt_tint\": \"탐색 바의 배경 레벨 (0-100)$N버퍼링이 덜 눈시리게 만듦\",\n\t\"mt_eq\": \"`이퀄라이저 및 게인 제어 활성화;$N$Nboost `0` = 표준 100% 볼륨 (수정 없음)$N$Nwidth `1 &nbsp;` = 표준 스테레오 (수정 없음)$Nwidth `0.5` = 50% 좌우 크로스피드$Nwidth `0 &nbsp;` = 모노$N$Nboost `-0.8` &amp; width `10` = 보컬 제거 :^)$N$N이퀄라이저를 활성화하면 끊김 없는 앨범이 온전히 끊김 없이 재생되므로, 그 점이 중요하다면 모든 값을 0으로 두고 (width=1 제외) 켜두세요.\",\n\t\"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(위키백과를 참조하세요, 훨씬 더 잘 설명되어 있습니다)\",\n\t\"mt_ss\": \"`무음 건너뛰기 활성화; 볼륨이 `음` 미만이고 위치가 처음 `시`% 또는 마지막 `끝`% 이내일 때 시작/끝 근처에서 재생 속도를 `고` 배로 합니다\", //m\n\t\"mt_ssvt\": \"볼륨 임계값 (0-255)\\\">음\", //m\n\t\"mt_ssts\": \"활성 임계값 (% 트랙, 시작)\\\">시\", //m\n\t\"mt_sste\": \"활성 임계값 (% 트랙, 끝)\\\">끝\", //m\n\t\"mt_sssm\": \"재생 속도 배율\\\">고\", //m\n\n\t\"mb_play\": \"재생\",\n\t\"mm_hashplay\": \"이 오디오 파일을 재생할까요?\",\n\t\"mm_m3u\": \"<code>Enter/확인</code>을 눌러 재생\\n<code>ESC/취소</code>를 눌러 편집\",\n\t\"mp_breq\": \"Firefox 82+, Chrome 73+ 또는 iOS 15+ 필요\",\n\t\"mm_bload\": \"불러오는 중...\",\n\t\"mm_bconv\": \"{0}(으)로 변환 중, 잠시만 기다려주세요...\",\n\t\"mm_opusen\": \"브라우저가 aac/m4a 파일을 재생할 수 없습니다.\\nopus로의 트랜스코딩이 활성화되었습니다.\",\n\t\"mm_playerr\": \"재생 실패: \",\n\t\"mm_eabrt\": \"재생 시도가 취소되었습니다\",\n\t\"mm_enet\": \"인터넷 연결이 불안정합니다\",\n\t\"mm_edec\": \"이 파일이 손상된 것 같습니다??\",\n\t\"mm_esupp\": \"브라우저가 이 오디오 형식을 이해하지 못합니다\",\n\t\"mm_eunk\": \"알 수 없는 오류\",\n\t\"mm_e404\": \"오디오를 재생할 수 없습니다; 오류 404: 파일을 찾을 수 없습니다.\",\n\t\"mm_e403\": \"오디오를 재생할 수 없습니다; 오류 403: 접근이 거부되었습니다.\\n\\nF5를 눌러 새로고침 해보세요, 로그아웃되었을 수 있습니다\",\n\t\"mm_e415\": \"오디오를 재생할 수 없습니다; 오류 415: 파일 변환에 실패했습니다; 서버 로그를 확인하세요.\", //m\n\t\"mm_e500\": \"오디오를 재생할 수 없습니다; 오류 500: 서버 로그를 확인하세요.\",\n\t\"mm_e5xx\": \"오디오를 재생할 수 없습니다; 서버 오류 \",\n\t\"mm_nof\": \"주변에서 더 이상 오디오 파일을 찾을 수 없습니다\",\n\t\"mm_prescan\": \"다음에 재생할 음악을 찾는 중...\",\n\t\"mm_scank\": \"다음 곡을 찾았습니다:\",\n\t\"mm_uncache\": \"캐시가 지워졌습니다. 모든 곡은 다음 재생 시 다시 다운로드됩니다.\",\n\t\"mm_hnf\": \"그 곡이 더 이상 존재하지 않습니다\",\n\n\t\"im_hnf\": \"그 이미지가 더 이상 존재하지 않습니다\",\n\n\t\"f_empty\": '이 폴더는 비어 있습니다',\n\t\"f_chide\": '«{0}» 열을 숨깁니다.\\n\\n설정 탭에서 열을 다시 표시할 수 있습니다.',\n\t\"f_bigtxt\": \"이 파일은 {0} MiB입니다 -- 정말 텍스트로 보시겠습니까?\",\n\t\"f_bigtxt2\": \"대신 파일의 끝부분만 보시겠습니까? 이렇게 하면 실시간으로 새로 추가되는 텍스트 줄을 보여주는 팔로잉/테일링 기능도 활성화됩니다.\",\n\t\"fbd_more\": '<div id=\"blazy\"><code>{1}</code>개 파일 중 <code>{0}</code>개 표시 중; <a href=\"#\" id=\"bd_more\">{2}개 더 보기</a> 또는 <a href=\"#\" id=\"bd_all\">모두 보기</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\"><code>{1}</code>개 파일 중 <code>{0}</code>개 표시 중; <a href=\"#\" id=\"bd_all\">모두 보기</a></div>',\n\t\"f_anota\": \"{1}개 항목 중 {0}개만 선택되었습니다.\\n전체 폴더를 선택하려면 먼저 맨 아래로 스크롤하세요.\",\n\n\t\"f_dls\": '현재 폴더의 파일 링크가\\n다운로드 링크로 변경되었습니다',\n\t\"f_dl_nd\": '폴더를 건너뜁니다 (대신 zip/tar 다운로드를 사용하세요):\\n', //m\n\n\t\"f_partial\": \"현재 업로드 중인 파일을 안전하게 다운로드하려면, 파일 이름이 같지만 <code>.PARTIAL</code> 확장자가 없는 파일을 클릭하세요. 이 경고를 무시하려면 \\\"취소\\\" 또는 ESC를 누르세요.\\n\\n\\\"확인\\\"/Enter를 누르면 이 경고를 무시하고 <code>.PARTIAL</code> 임시 파일을 계속 다운로드하며, 이 경우 거의 확실히 손상된 데이터를 받게 됩니다.\",\n\n\t\"ft_paste\": \"{0}개 항목 붙여넣기$N단축키: ctrl-V\",\n\t\"fr_eperm\": \"이름을 바꿀 수 없습니다:\\n이 폴더에 \\\"이동\\\" 권한이 없습니다\",\n\t\"fd_eperm\": \"삭제할 수 없습니다:\\n이 폴더에 \\\"삭제\\\" 권한이 없습니다\",\n\t\"fc_eperm\": \"잘라낼 수 없습니다:\\n이 폴더에 \\\"이동\\\" 권한이 없습니다\",\n\t\"fp_eperm\": \"붙여넣을 수 없습니다\\n이 폴더에 \\\"쓰기\\\" 권한이 없습니다\",\n\t\"fr_emore\": \"이름을 바꿀 항목을 하나 이상 선택하세요\",\n\t\"fd_emore\": \"삭제할 항목을 하나 이상 선택하세요\",\n\t\"fc_emore\": \"잘라낼 항목을 하나 이상 선택하세요\",\n\t\"fcp_emore\": \"클립보드에 복사할 항목을 하나 이상 선택하세요\",\n\n\t\"fs_sc\": \"현재 폴더 공유\",\n\t\"fs_ss\": \"선택한 파일 공유\",\n\t\"fs_just1d\": \"하나 이상의 폴더를 선택하거나,\\n파일과 폴더를 한 번에 섞어 선택할 수 없습니다\",\n\t\"fs_abrt\": \"❌ 중단\",\n\t\"fs_rand\": \"🎲 무작위 이름\",\n\t\"fs_go\": \"✅ 공유 생성\",\n\t\"fs_name\": \"이름\",\n\t\"fs_src\": \"소스\",\n\t\"fs_pwd\": \"비밀번호\",\n\t\"fs_exp\": \"만료\",\n\t\"fs_tmin\": \"분\",\n\t\"fs_thrs\": \"시간\",\n\t\"fs_tdays\": \"일\",\n\t\"fs_never\": \"영원\",\n\t\"fs_pname\": \"선택적 링크 이름; 비워두면 무작위로 생성\",\n\t\"fs_tsrc\": \"공유할 파일 또는 폴더\",\n\t\"fs_ppwd\": \"비밀번호 (선택사항)\",\n\t\"fs_w8\": \"공유 생성 중...\",\n\t\"fs_ok\": \"<code>Enter/OK</code>를 눌러 클립보드에 복사\\n<code>ESC/Cancel</code>를 눌러 닫기\",\n\n\t\"frt_dec\": \"깨진 파일 이름의 일부 경우를 수정할 수 있습니다\\\">url-디코드\",\n\t\"frt_rst\": \"수정된 파일 이름을 원래대로 되돌립니다\\\">↺ 초기화\",\n\t\"frt_abrt\": \"이 창을 중단하고 닫습니다\\\">❌ 취소\",\n\t\"frb_apply\": \"이름 바꾸기 적용\",\n\t\"fr_adv\": \"배치/메타데이터/패턴 이름 바꾸기\\\">고급\",\n\t\"fr_case\": \"대소문자 구분 정규식\\\">대소문자\",\n\t\"fr_win\": \"Windows 안전 이름; <code>&lt;&gt;:&quot;\\\\|?*</code>를 일본어 전각 문자로 바꿉니다\\\">win\",\n\t\"fr_slash\": \"<code>/</code>를 새 폴더를 만들지 않는 문자로 바꿉니다\\\">/ 없음\",\n\t\"fr_re\": \"`원본 파일 이름에 적용할 정규식 검색 패턴; 캡처링 그룹은 아래 형식 필드에서 `(1)`, `(2)` 등으로 참조할 수 있습니다\",\n\t\"fr_fmt\": \"`foobar2000에서 영감을 받음:$N`(title)`은(는) 곡 제목으로 대체됨,$N`[(artist) - ](title)`은(는) 아티스트가 비어 있으면 [이] 부분을 건너뜀$N`$lpad((tn),2,0)`은(는) 트랙 번호를 2자리로 채움\",\n\t\"fr_pdel\": \"삭제\",\n\t\"fr_pnew\": \"다른 이름으로 저장\",\n\t\"fr_pname\": \"새 프리셋의 이름을 입력하세요\",\n\t\"fr_aborted\": \"중단됨\",\n\t\"fr_lold\": \"이전 이름\",\n\t\"fr_lnew\": \"새 이름\",\n\t\"fr_tags\": \"선택한 파일의 태그 (읽기 전용, 참조용):\",\n\t\"fr_busy\": \"{0}개 항목 이름 바꾸는 중...\\n\\n{1}\",\n\t\"fr_efail\": \"이름 바꾸기 실패:\\n\",\n\t\"fr_nchg\": \"<code>win</code> 및/또는 <code>/ 없음</code>으로 인해 새 이름 중 {0}개가 변경되었습니다.\\n\\n이 변경된 새 이름으로 계속하시겠습니까?\",\n\n\t\"fd_ok\": \"삭제 확인\",\n\t\"fd_err\": \"삭제 실패:\\n\",\n\t\"fd_none\": \"아무것도 삭제되지 않았습니다. 서버 구성 (xbd)에 의해 차단되었을 수 있습니다.\",\n\t\"fd_busy\": \"삭제 중 {0}개 항목...\\n\\n{1}\",\n\t\"fd_warn1\": \"이 {0}개 항목을 삭제하시겠습니까?\",\n\t\"fd_warn2\": \"<b>마지막 기회입니다!</b> 되돌릴 수 없습니다. 삭제하시겠습니까?\",\n\n\t\"fc_ok\": \"{0}개 항목 잘라내기 완료\",\n\t\"fc_warn\": \"{0}개 항목 잘라내기 완료\\n\\n하지만: 선택 항목이 너무 커서 <b>이</b> 브라우저 탭에서만 붙여넣을 수 있습니다\",\n\n\t\"fcc_ok\": \"{0}개 항목을 클립보드에 복사했습니다\",\n\t\"fcc_warn\": \"{0}개 항목을 클립보드에 복사했습니다\\n\\n하지만: 선택 항목이 너무 커서 <b>이</b> 브라우저 탭에서만 붙여넣을 수 있습니다\",\n\n\t\"fp_apply\": \"이 이름 사용\",\n\t\"fp_skip\": \"충돌 건너뛰기\", //m\n\t\"fp_ecut\": \"붙여넣거나 이동하려면 먼저 파일/폴더를 잘라내거나 복사하세요\\n\\n참고: 다른 브라우저 탭 간에 잘라내기/붙여넣기를 할 수 있습니다\",\n\t\"fp_ename\": \"이름이 이미 사용 중이므로 {0}개 항목을 여기로 이동할 수 없습니다. 계속하려면 아래에 새 이름을 지정하거나, 이름을 비워두면 (\\\"충돌 건너뛰기\\\") 건너뜁니다:\", //m\n\t\"fcp_ename\": \"이름이 이미 사용 중이므로 {0}개 항목을 여기로 복사할 수 없습니다. 계속하려면 아래에 새 이름을 지정하거나, 이름을 비워두면 (\\\"충돌 건너뛰기\\\") 건너뜁니다:\", //m\n\t\"fp_emore\": \"아직 해결해야 할 파일 이름 충돌이 남아 있습니다\",\n\t\"fp_ok\": \"이동 완료\",\n\t\"fcp_ok\": \"복사 완료\",\n\t\"fp_busy\": \"{0}개 항목 이동 중...\\n\\n{1}\",\n\t\"fcp_busy\": \"{0}개 항목 복사 중...\\n\\n{1}\",\n\t\"fp_abrt\": \"취소 중...\",\n\t\"fp_err\": \"이동 실패:\\n\",\n\t\"fcp_err\": \"복사 실패:\\n\",\n\t\"fp_confirm\": \"이 {0}개 항목을 여기로 이동하시겠습니까?\",\n\t\"fcp_confirm\": \"이 {0}개 항목을 여기로 복사하시겠습니까?\",\n\t\"fp_etab\": '다른 브라우저 탭에서 클립보드를 읽지 못했습니다',\n\t\"fp_name\": \"기기에서 파일을 업로드합니다. 이름을 지정하세요:\",\n\t\"fp_both_m\": '<h6>붙여넣을 항목 선택</h6><code>Enter</code> = «{1}»에서 파일 {0}개 이동\\n<code>ESC</code> = 기기에서 파일 {2}개 업로드',\n\t\"fcp_both_m\": '<h6>붙여넣을 항목 선택</h6><code>Enter</code> = «{1}»에서 파일 {0}개 복사\\n<code>ESC</code> = 기기에서 파일 {2}개 업로드',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">이동</a><a href=\"#\" id=\"modal-ng\">업로드</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">복사</a><a href=\"#\" id=\"modal-ng\">업로드</a>',\n\n\t\"mk_noname\": \"왼쪽 텍스트 필드에 이름을 먼저 입력해주세요 :p\",\n\t\"nmd_i1\": \"원하는 파일 확장자를 추가할 수 있습니다. 예: <code>.md</code>\", //m\n\t\"nmd_i2\": \"삭제 권한이 없어서 <code>.{0}</code> 파일만 만들 수 있습니다\", //m\n\n\t\"tv_load\": \"텍스트 문서 불러오는 중:\\n\\n{0}\\n\\n{1}% ({3} MiB 중 {2} MiB 로드됨)\",\n\t\"tv_xe1\": \"텍스트 파일을 불러올 수 없습니다:\\n\\n오류 \",\n\t\"tv_xe2\": \"404, 파일을 찾을 수 없음\",\n\t\"tv_lst\": \"텍스트 파일 목록\",\n\t\"tvt_close\": \"폴더 보기로 돌아가기$N단축키: M (또는 Esc)\\\">❌ 닫기\",\n\t\"tvt_dl\": \"이 파일 다운로드$N단축키: Y\\\">💾 다운로드\",\n\t\"tvt_prev\": \"이전 문서 보기$N단축키: i\\\">⬆ 이전\",\n\t\"tvt_next\": \"다음 문서 보기$N단축키: K\\\">⬇ 다음\",\n\t\"tvt_sel\": \"파일 선택 &nbsp; (잘라내기/복사/삭제/...용)$N단축키: S\\\">선택\",\n\t\"tvt_j\": \"json 미화$N단축키: shift-J\\\">j\", //m\n\t\"tvt_edit\": \"텍스트 편집기에서 파일 열기$N단축키: E\\\">✏️ 편집\",\n\t\"tvt_tail\": \"파일 변경 사항 모니터링; 실시간으로 새 줄 표시\\\">📡 팔로우\",\n\t\"tvt_wrap\": \"자동 줄 바꿈\\\">↵\",\n\t\"tvt_atail\": \"페이지 하단으로 스크롤 고정\\\">⚓\",\n\t\"tvt_ctail\": \"터미널 색상 디코딩 (ANSI 이스케이프 코드)\\\">🌈\",\n\t\"tvt_ntail\": \"스크롤백 제한 (불러온 상태로 유지할 텍스트 바이트 수)\",\n\n\t\"m3u_add1\": \"m3u 재생 목록에 곡이 추가되었습니다\",\n\t\"m3u_addn\": \"{0}개의 곡이 m3u 재생 목록에 추가되었습니다\",\n\t\"m3u_clip\": \"m3u 재생 목록이 클립보드에 복사되었습니다\\n\\n something.m3u와 같은 이름의 새 텍스트 파일을 만들고 그 문서에 재생 목록을 붙여넣으면 재생할 수 있습니다.\",\n\n\t\"gt_vau\": \"비디오를 표시하지 않고 오디오만 재생\\\">🎧\",\n\t\"gt_msel\": \"파일 선택 활성화; ctrl-클릭하여 파일 재정의$N$N&lt;em&gt;활성 시: 파일/폴더를 두 번 클릭하여 열기&lt;/em&gt;$N$N단축키: S\\\">다중선택\",\n\t\"gt_crop\": \"썸네일 중앙 자르기\\\">자르기\",\n\t\"gt_3x\": \"고해상도 썸네일\\\">3x\",\n\t\"gt_zoom\": \"확대/축소\",\n\t\"gt_chop\": \"자르기\",\n\t\"gt_sort\": \"정렬 기준\",\n\t\"gt_name\": \"이름\",\n\t\"gt_sz\": \"크기\",\n\t\"gt_ts\": \"날짜\",\n\t\"gt_ext\": \"유형\",\n\t\"gt_c1\": \"파일명 더 많이 생략하기 (더 적게 표시)\",\n\t\"gt_c2\": \"파일명 덜 생략하기 (더 많이 표시)\",\n\n\t\"sm_w8\": \"검색 중...\",\n\t\"sm_prev\": \"아래 검색 결과는 이전 검색어에 대한 결과입니다:\\n  \",\n\t\"sl_close\": \"검색 결과 닫기\",\n\t\"sl_hits\": \"{0}개 결과 표시 중\",\n\t\"sl_moar\": \"더 불러오기\",\n\n\t\"s_sz\": \"크기\",\n\t\"s_dt\": \"날짜\",\n\t\"s_rd\": \"경로\",\n\t\"s_fn\": \"이름\",\n\t\"s_ta\": \"태그\",\n\t\"s_ua\": \"업로드 시점\",\n\t\"s_ad\": \"고급\",\n\t\"s_s1\": \"최소 MiB\",\n\t\"s_s2\": \"최대 MiB\",\n\t\"s_d1\": \"최소 ISO-8601\",\n\t\"s_d2\": \"최대 ISO-8601\",\n\t\"s_u1\": \"이후\",\n\t\"s_u2\": \"이전\",\n\t\"s_r1\": \"경로에 포함 &nbsp; (공백으로 구분)\",\n\t\"s_f1\": \"이름에 포함 &nbsp; (-로 제외)\",\n\t\"s_t1\": \"태그에 포함 &nbsp; (^=시작, 끝=$)\",\n\t\"s_a1\": \"특정 메타데이터 속성\",\n\n\t\"md_eshow\": \"렌더링할 수 없음 \",\n\t\"md_off\": \"[📜<em>readme</em>]가 [⚙️]에서 비활성화됨 -- 문서 숨김\",\n\n\t\"badreply\": \"서버로부터의 응답을 구문 분석하지 못했습니다\",\n\n\t\"xhr403\": \"403: 접근 거부됨\\n\\nF5를 눌러보세요, 로그아웃되었을 수 있습니다\",\n\t\"xhr0\": \"알 수 없음 (서버와의 연결이 끊겼거나 서버가 오프라인일 수 있습니다)\",\n\t\"cf_ok\": \"죄송합니다 -- DD\" + wah + \"oS 보호 기능이 작동했습니다\\n\\n약 30초 후에 다시 정상적으로 작동할 것입니다\\n\\n아무 일도 일어나지 않으면 F5를 눌러 페이지를 새로고침하세요\",\n\t\"tl_xe1\": \"하위 폴더를 나열할 수 없습니다:\\n\\n오류 \",\n\t\"tl_xe2\": \"404: 폴더를 찾을 수 없음\",\n\t\"fl_xe1\": \"폴더의 파일을 나열할 수 없습니다:\\n\\n오류 \",\n\t\"fl_xe2\": \"404: 폴더를 찾을 수 없음\",\n\t\"fd_xe1\": \"하위 폴더를 만들 수 없습니다:\\n\\n오류 \",\n\t\"fd_xe2\": \"404: 상위 폴더를 찾을 수 없음\",\n\t\"fsm_xe1\": \"메시지를 보낼 수 없습니다:\\n\\n오류 \",\n\t\"fsm_xe2\": \"404: 상위 폴더를 찾을 수 없음\",\n\t\"fu_xe1\": \"서버에서 주워담기 목록을 불러오지 못했습니다:\\n\\n오류 \",\n\t\"fu_xe2\": \"404: 파일을 찾을 수 없음??\",\n\n\t\"fz_tar\": \"압축되지 않은 gnu-tar 파일 (linux / mac)\",\n\t\"fz_pax\": \"압축되지 않은 pax 형식 tar (느림)\",\n\t\"fz_targz\": \"gzip 레벨 3 압축이 적용된 gnu-tar$N$N이것은 보통 매우 느리므로$N압축되지 않은 tar를 대신 사용하세요\",\n\t\"fz_tarxz\": \"xz 레벨 1 압축이 적용된 gnu-tar$N$N이것은 보통 매우 느리므로$N압축되지 않은 tar를 대신 사용하세요\",\n\t\"fz_zip8\": \"utf8 파일 이름이 포함된 zip (windows 7 및 이전 버전에서 문제가 있을 수 있음)\",\n\t\"fz_zipd\": \"정말 오래된 소프트웨어를 위한 전통적인 cp437 파일 이름이 포함된 zip\",\n\t\"fz_zipc\": \"MS-DOS PKZIP v2.04g (1993년 10월)용으로$Ncrc32가 미리 계산된 cp437$N(다운로드 시작 전 처리 시간이 더 걸림)\",\n\n\t\"un_m1\": \"아래에서 최근 업로드를 삭제하거나 미완료된 업로드를 중단할 수 있습니다\",\n\t\"un_upd\": \"새로고침\",\n\t\"un_m4\": \"또는 아래에 보이는 파일을 공유할 수 있습니다:\",\n\t\"un_ulist\": \"보기\",\n\t\"un_ucopy\": \"복사\",\n\t\"un_flt\": \"선택적 필터:&nbsp; URL에 포함되어야 함\",\n\t\"un_fclr\": \"필터 지우기\",\n\t\"un_derr\": '주워담기-삭제 실패:\\n',\n\t\"un_f5\": '문제가 발생했습니다, 새로고침하거나 F5를 눌러보세요',\n\t\"un_uf5\": \"죄송하지만, 이 업로드를 중단하기 전에 페이지를 새로고침해야 합니다 (예: F5 또는 CTRL-R 누르기).\",\n\t\"un_nou\": '<b>경고:</b> 서버가 너무 바빠서 미완료 업로드를 표시할 수 없습니다; 잠시 후 \"새로고침\" 링크를 클릭하세요',\n\t\"un_noc\": '<b>경고:</b> 완전히 업로드된 파일의 주워담기가 서버 구성에서 활성화/허용되지 않았습니다',\n\t\"un_max\": \"처음 2000개 파일을 표시합니다 (필터를 사용하세요)\",\n\t\"un_avail\": \"{0}개의 최근 업로드를 삭제할 수 있습니다<br />{1}개의 미완료 업로드를 중단할 수 있습니다\",\n\t\"un_m2\": \"업로드 시간순으로 정렬됨. 가장 최근 항목이 먼저 표시:\",\n\t\"un_no1\": \"아쉽다! 충분히 최근인 업로드가 없습니다.\",\n\t\"un_no2\": \"아쉽다! 해당 필터와 일치하는 최근 업로드가 없습니다.\",\n\t\"un_next\": \"아래의 다음 {0}개 파일 삭제\",\n\t\"un_abrt\": \"중단\",\n\t\"un_del\": \"삭제\",\n\t\"un_m3\": \"최근 업로드 로드 중...\",\n\t\"un_busy\": \"{0}개 파일 삭제 중...\",\n\t\"un_clip\": \"{0}개의 링크가 클립보드에 복사되었습니다\",\n\n\t\"u_https1\": \"더 나은 성능을 위해\",\n\t\"u_https2\": \"https로 전환\",\n\t\"u_https3\": \"하는 것이 좋습니다\",\n\t\"u_ancient\": '브라우저가 정말 오래되었네요 -- 아마도 <a href=\"#\" onclick=\"goto(\\'bup\\')\">bup을 대신 사용</a>해야 할 것 같습니다',\n\t\"u_nowork\": \"Firefox 53+, Chrome 57+ 또는 iOS 11+가 필요합니다\",\n\t\"tail_2old\": \"Firefox 105+, Chrome 71+ 또는 iOS 14.5+가 필요합니다\",\n\t\"u_nodrop\": '브라우저가 너무 오래되어 드래그 앤 드롭 업로드를 지원하지 않습니다',\n\t\"u_notdir\": '폴더가 아닙니다!\\n\\n브라우저가 너무 오래되었습니다,\\n대신 드래그드롭을 시도해보세요',\n\t\"u_uri\": '다른 브라우저 창에서 이미지를 드래그드롭하려면,\\n큰 업로드 버튼 위로 떨어뜨려주세요',\n\t\"u_enpot\": '<a href=\"#\">단순 UI로 전환</a> (업로드 속도가 향상될 수 있음)',\n\t\"u_depot\": '<a href=\"#\">화려한 UI로 전환</a> (업로드 속도가 감소할 수 있음)',\n\t\"u_gotpot\": '업로드 속도 향상을 위해 단순 UI로 전환합니다,\\n\\n언제든지 다시 전환하셔도 좋습니다!',\n\t\"u_pott\": \"<p>파일: &nbsp; <b>{0}</b> 완료, &nbsp; <b>{1}</b> 실패, &nbsp; <b>{2}</b> 처리 중, &nbsp; <b>{3}</b> 대기 중</p>\",\n\t\"u_ever\": \"이것은 기본 업로더입니다. up2k는 최소한 다음 버전이 필요합니다:<br>Chrome 21 // Firefox 13 // Edge 12 // Opera 12 // Safari 5.1\",\n\t\"u_su2k\": '이것은 기본 업로더입니다. <a href=\"#\" id=\"u2yea\">up2k</a>가 더 좋습니다',\n\t\"u_uput\": '속도 최적화 (체크섬 건너뛰기)',\n\t\"u_ewrite\": '이 폴더에 쓰기 권한이 없습니다',\n\t\"u_eread\": '이 폴더에 읽기 권한이 없습니다',\n\t\"u_enoi\": '파일 검색이 서버 구성에서 활성화되지 않았습니다',\n\t\"u_enoow\": '여기서는 덮어쓰기가 작동하지 않습니다. 삭제 권한이 필요합니다',\n\t\"u_badf\": '총 {1}개 중 다음 {0}개의 파일은 파일 시스템 권한 문제 등으로 건너뛰었습니다:\\n\\n',\n\t\"u_blankf\": '총 {1}개 중 다음 {0}개의 파일은 비어있습니다. 그래도 업로드하시겠습니까?\\n\\n',\n\t\"u_applef\": '총 {1}개 중 다음 {0}개의 파일은 아마도 불필요한 파일일 것입니다.\\n다음 파일을 건너뛰려면 <code>확인/Enter</code>를 누르세요,\\n해당 파일도 업로드하려면 <code>취소/ESC</code>를 누르세요:\\n\\n',\n\t\"u_just1\": '\\n파일을 하나만 선택하면 더 잘 작동할 수 있습니다',\n\t\"u_ff_many\": '<b>리눅스/macOS/안드로이드</b>를 사용 중이라면, 이 정도의 파일 수는 <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">Firefox를 <em>충돌시킬 수 있습니다!</em></a>\\n만약 그런 일이 발생하면 다시 시도하거나 Chrome을 사용해주세요.',\n\t\"u_up_life\": '이 업로드는 완료 후 {0} 뒤에\\n서버에서 삭제됩니다',\n\t\"u_asku\": '이 {0}개의 파일을 <code>{1}</code>(으)로 업로드하시겠습니까?',\n\t\"u_unpt\": '왼쪽 상단의 🧯를 사용하여 이 업로드를 취소/삭제할 수 있습니다',\n\t\"u_bigtab\": '{0}개의 파일을 표시하려고 합니다\\n\\n브라우저가 충돌할 수 있습니다, 계속 진행합니까?',\n\t\"u_scan\": '파일 스캔 중...',\n\t\"u_dirstuck\": '디렉터리 반복자가 다음 {0}개 항목에 접근하는 데 실패하여 건너뜁니다:',\n\t\"u_etadone\": '완료 ({0}, {1}개 파일)',\n\t\"u_etaprep\": '(업로드 준비 중)',\n\t\"u_hashdone\": '해싱 완료',\n\t\"u_hashing\": '해시',\n\t\"u_hs\": '핸드셰이킹 중...',\n\t\"u_started\": '파일이 현재 업로드 중입니다. [🚀] 참조',\n\t\"u_dupdefer\": '중복됨. 다른 모든 파일 처리 후 처리됩니다',\n\t\"u_actx\": \"다른 창/탭으로 전환 시 성능 저하를<br />방지하려면 이 텍스트를 클릭하세요\",\n\t\"u_fixed\": \"OK!&nbsp; 해결됐습니다 👍\",\n\t\"u_cuerr\": \"{1} 중 청크 {0} 업로드 실패;\\n아마 문제 없을 겁니다. 계속 진행합니다\\n\\n파일: {2}\",\n\t\"u_cuerr2\": \"서버가 업로드를 거부했습니다 (청크 {0}/{1});\\n나중에 다시 시도합니다\\n\\n파일: {2}\\n\\n오류 \",\n\t\"u_ehstmp\": \"다시 시도합니다; 오른쪽 하단 참조\",\n\t\"u_ehsfin\": \"서버가 업로드 완료 요청을 거부했습니다. 재시도 중...\",\n\t\"u_ehssrch\": \"서버가 검색 수행 요청을 거부했습니다. 재시도 중...\",\n\t\"u_ehsinit\": \"서버가 업로드 시작 요청을 거부했습니다. 재시도 중...\",\n\t\"u_eneths\": \"업로드 핸드셰이크 중 네트워크 오류 발생. 재시도 중...\",\n\t\"u_enethd\": \"대상 존재 여부 테스트 중 네트워크 오류 발생; 재시도 중...\",\n\t\"u_cbusy\": '네트워크 문제 후 서버가 다시 우리를 신뢰할 때까지 기다리는 중...',\n\t\"u_ehsdf\": '서버 디스크 공간이 부족합니다!\\n\\n누군가 계속할 수 있을 만큼의 공간을\\n비워줄 경우를 대비해 계속 재시도합니다',\n\t\"u_emtleak1\": \"웹 브라우저에 메모리 누수가 있는 것 같습니다.\\n\",\n\t\"u_emtleak2\": ' <a href=\"{0}\">https로 전환 (권장)</a>하거나 ',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": '다음을 시도해보세요:\\n<ul><li><code>F5</code>를 눌러 페이지를 새로고침하세요</li><li>그런 다음 <code>⚙️ 설정</code>에서 &nbsp;<code>mt</code>&nbsp; 버튼을 비활성화하세요</li><li>그리고 다시 그 업로드를 시도해보세요</li></ul>업로드가 조금 느려지겠지만, 어쩔 수 없죠.\\n불편을 드려 죄송합니다!\\n\\nPS: 이 버그는 Chrome v107에서 <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">수정되었습니다</a>.',\n\t\"u_emtleakf\": '다음을 시도해보세요:\\n<ul><li><code>F5</code>를 눌러 페이지를 새로고침하세요</li><li>그런 다음 업로드 UI에서 <code>🥔</code>(단순 UI)를 활성화하세요<li>그리고 다시 그 업로드를 시도해보세요</li></ul>\\nPS: Firefox에서 <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">언젠가 이 버그가 수정될 거라 믿습니다</a>.',\n\t\"u_s404\": '서버에서 찾을 수 없음',\n\t\"u_expl\": '설명',\n\t\"u_maxconn\": \"대부분의 브라우저는 이를 6으로 제한하지만, Firefox에서는 <code>about:config</code>에서 <code>connections-per-server</code> 설정값으로 높일 수 있습니다.\",\n\t\"u_tu\": '<p class=\"warn\">경고: 터보가 활성화되어 <span>클라이언트가 불완전한 업로드를 감지하고 재개하지 못할 수 있습니다. 터보 버튼의 툴팁을 참조하세요</span></p>',\n\t\"u_ts\": '<p class=\"warn\">경고: 터보가 활성화되어 <span>검색 결과가 부정확할 수 있습니다. 터보 버튼의 툴팁을 참조하세요</span></p>',\n\t\"u_turbo_c\": \"터보가 서버 구성에서 비활성화되었습니다\",\n\t\"u_turbo_g\": '이 볼륨 내에서 디렉터리 목록 권한이 없으므로\\n터보를 비활성화합니다',\n\t\"u_life_cfg\": '자동 삭제 시간 <input id=\"lifem\" p=\"60\" /> 분 (또는 <input id=\"lifeh\" p=\"3600\" /> 시간)',\n\t\"u_life_est\": '업로드가 <span id=\"lifew\" tt=\"현지 시간\">---</span>에 삭제됩니다',\n\t\"u_life_max\": '이 폴더는 최대 수명을\\n{0}(으)로 강제합니다',\n\t\"u_unp_ok\": '주워담기는 {0} 동안 허용됩니다',\n\t\"u_unp_ng\": '주워담기는 허용되지 않습니다',\n\t\"ue_ro\": '이 폴더에 대한 접근은 읽기 전용입니다\\n\\n',\n\t\"ue_nl\": '현재 로그인되어 있지 않습니다',\n\t\"ue_la\": '현재 \\'{0}\\'(으)로 로그인되어 있습니다',\n\t\"ue_sr\": '현재 파일 검색 모드입니다\\n\\n큰 \"검색\" 버튼 옆의 돋보기 🔎를 클릭하여 업로드 모드로 전환한 후 다시 업로드해보세요\\n\\n죄송합니다',\n\t\"ue_ta\": '다시 업로드해보세요, 이제 작동할 겁니다',\n\t\"ue_ab\": '이 파일은 이미 다른 폴더로 업로드 중이며, 파일이 다른 곳에 업로드되기 전에 해당 업로드가 완료되어야 합니다.\\n\\n왼쪽 상단의 🧯를 사용하여 초기 업로드를 중단하고 잊을 수 있습니다.',\n\t\"ur_1uo\": \"OK: 파일이 성공적으로 업로드되었습니다\",\n\t\"ur_auo\": \"OK: 모든 {0}개의 파일이 성공적으로 업로드되었습니다\",\n\t\"ur_1so\": \"OK: 서버에서 파일을 찾았습니다\",\n\t\"ur_aso\": \"OK: 서버에서 모든 {0}개의 파일을 찾았습니다\",\n\t\"ur_1un\": \"업로드에 실패했습니다, 죄송\",\n\t\"ur_aun\": \"모든 {0}개의 업로드에 실패했습니다, 죄송\",\n\t\"ur_1sn\": \"서버에서 파일을 찾지 못했습니다\",\n\t\"ur_asn\": \"서버에서 {0}개의 파일을 찾지 못했습니다\",\n\t\"ur_um\": \"완료;\\n{0}개 업로드 성공,\\n{1}개 업로드 실패, 죄송\",\n\t\"ur_sm\": \"완료;\\n서버에서 {0}개 파일 찾음,\\n서버에서 {1}개 파일 찾지 못함\",\n\n\t\"rc_opn\": \"열기\", //m\n\t\"rc_ply\": \"재생\", //m\n\t\"rc_pla\": \"오디오로 재생\", //m\n\t\"rc_txt\": \"파일 뷰어에서 열기\", //m\n\t\"rc_md\": \"텍스트 편집기에서 열기\", //m\n\t\"rc_dl\": \"다운로드\", //m\n\t\"rc_zip\": \"압축 파일로 다운로드\", //m\n\t\"rc_cpl\": \"링크 복사\", //m\n\t\"rc_del\": \"삭제\", //m\n\t\"rc_cut\": \"잘라내기\", //m\n\t\"rc_cpy\": \"복사\", //m\n\t\"rc_pst\": \"붙여넣기\", //m\n\t\"rc_rnm\": \"이름 변경\", //m\n\t\"rc_nfo\": \"새 폴더\", //m\n\t\"rc_nfi\": \"새 파일\", //m\n\t\"rc_sal\": \"모두 선택\", //m\n\t\"rc_sin\": \"선택 반전\", //m\n\t\"rc_shf\": \"이 폴더 공유\", //m\n\t\"rc_shs\": \"선택 항목 공유\", //m\n\n\t\"lang_set\": '변경 사항을 적용하기 위해 새로고침하시겠습니까?',\n\n\t\"splash\": {\n\t\t\"a1\": \"새로고침\",\n\t\t\"b1\": \"어이 친구! 처음 보는 얼굴인데? &nbsp; <small>(로그인되어 있지 않습니다)</small>\",\n\t\t\"c1\": \"로그아웃\",\n\t\t\"d1\": \"스택 덤프하기\",\n\t\t\"d2\": \"모든 활성 스레드의 상태를 표시합니다\",\n\t\t\"e1\": \"설정 다시 불러오기\",\n\t\t\"e2\": \"설정 파일(계정/볼륨/볼륨 플래그)을 다시 불러오고,$N모든 e2ds 볼륨을 다시 스캔합니다$N$N참고: 전역 설정에 대한 변경 사항은$N적용하려면 전체 재시작이 필요합니다\",\n\t\t\"f1\": \"탐색 가능한 곳:\",\n\t\t\"g1\": \"업로드 가능한 곳:\",\n\t\t\"cc1\": \"기타 항목:\",\n\t\t\"h1\": \"k304 비활성화\",\n\t\t\"i1\": \"k304 활성화\",\n\t\t\"j1\": \"k304를 활성화하면 모든 HTTP 304 응답 시 클라이언트 연결이 끊어집니다. 이는 일부 프록시가 멈추는 현상(갑자기 페이지가 로드되지 않음)을 방지할 수 있지만, <em>대신 전반적인 속도는 느려집니다.</em>\",\n\t\t\"k1\": \"클라이언트 설정 초기화\",\n\t\t\"l1\": \"로그인하기:\",\n\t\t\"ls3\": \"로그인\", //m\n\t\t\"lu4\": \"사용자 이름\", //m\n\t\t\"lp4\": \"비밀번호\", //m\n\t\t\"lo3\": \"{0}을(를) 모든 곳에서 로그아웃\", //m\n\t\t\"lo2\": \"이 작업은 모든 브라우저에서 세션을 종료합니다\", //m\n\t\t\"m1\": \"또 오셨네요,\",\n\t\t\"n1\": \"404 찾을 수 없음 &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": \"또는 접근 권한이 없을 수 있습니다. 비밀번호를 입력하거나 <a href=\\\"' + SR + '/?h\\\">홈으로 이동</a>하세요\",\n\t\t\"p1\": \"403 접근 금지 &nbsp;~┻━┻\",\n\t\t\"q1\": \"비밀번호를 입력하거나 <a href=\\\"' + SR + '/?h\\\">홈으로 이동</a>하세요\",\n\t\t\"r1\": \"홈으로 이동\",\n\t\t\".s1\": \"다시 스캔\",\n\t\t\"t1\": \"작업\",\n\t\t\"u2\": \"서버에 마지막으로 쓰기 작업을 한 후 경과된 시간$N(업로드 / 이름 변경 / 등등...)$N$N17d = 17일$N1h23 = 1시간 23분$N4m56 = 4분 56초\",\n\t\t\"v1\": \"연결\",\n\t\t\"v2\": \"이 서버를 로컬 하드디스크처럼 사용하기\",\n\t\t\"w1\": \"HTTPS로 전환\",\n\t\t\"x1\": \"비밀번호 변경\",\n\t\t\"y1\": \"공유 설정\",\n\t\t\"z1\": \"이 공유 잠금해제:\",\n\t\t\"ta1\": \"새 비밀번호를 먼저 입력하세요\",\n\t\t\"ta2\": \"새 비밀번호 확인을 위해 다시 입력하세요:\",\n\t\t\"ta3\": \"오타가 있습니다. 다시 시도해주세요\",\n\t\t\"nop\": \"오류: 비밀번호를 비워 둘 수 없습니다\", //m\n\t\t\"nou\": \"오류: 사용자 이름 및/또는 비밀번호를 비워 둘 수 없습니다\", //m\n\t\t\"aa1\": \"수신 중인 파일:\",\n\t\t\"ab1\": \"no304 비활성화\",\n\t\t\"ac1\": \"no304 활성화\",\n\t\t\"ad1\": \"no304를 활성화하면 모든 캐싱이 비활성화됩니다. k304로 충분하지 않은 경우 시도해보세요. 네트워크 트래픽이 대량으로 낭비됩니다!\",\n\t\t\"ae1\": \"활성 다운로드:\",\n\t\t\"af1\": \"최근 업로드 보기\",\n\t\t\"ag1\": \"IdP 캐시 보기\",\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/nld.js",
    "content": "\n// Regels die eindigen op //m zijn niet-geverifieerde machinale vertalingen\n\nLs.nld = {\n\t\"tt\": \"Nederlands\",\n\n\t\"cols\": {\n\t\t\"c\": \"Action knoppen\",\n\t\t\"dur\": \"Duratie\",\n\t\t\"q\": \"Kwaliteit / bitrate\",\n\t\t\"Ac\": \"Audio codec\",\n\t\t\"Vc\": \"Video codec\",\n\t\t\"Fmt\": \"Formaat / container\",\n\t\t\"Ahash\": \"Audio checksum\",\n\t\t\"Vhash\": \"Video checksum\",\n\t\t\"Res\": \"Resolution\",\n\t\t\"T\": \"Bestandstype\",\n\t\t\"aq\": \"Audio kwaliteit / bitrate\",\n\t\t\"vq\": \"Video kwaliteit / bitrate\",\n\t\t\"pixfmt\": \"Subsampling / pixel structure\",\n\t\t\"resw\": \"Horizontale resolutie\",\n\t\t\"resh\": \"Verticale resolutie\",\n\t\t\"chs\": \"Audiokanalen\",\n\t\t\"hz\": \"Samplefrequentie\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"diversen\",\n\t\t\t[\"ESC\", \"Sluit verschillende dingen\"],\n\n\t\t\t\"bestand beheer\",\n\t\t\t[\"G\", \"Verwissel tussen list / grid weergave\"],\n\t\t\t[\"T\", \"Verwissel tussen miniaturen / iconen\"],\n\t\t\t[\"⇧ A/D\", \"Thumbnail formaat\"],\n\t\t\t[\"ctrl-K\", \"Verwijder geselecteerde\"],\n\t\t\t[\"ctrl-X\", \"Knip selectie naar klembord\"],\n\t\t\t[\"ctrl-C\", \"Kopieer selectie naar klembord\"],\n\t\t\t[\"ctrl-V\", \"Hier plakken (verplaatsen/kopieëren)\"],\n\t\t\t[\"Y\", \"Download geselecteerde\"],\n\t\t\t[\"F2\", \"Hernoem geselecteerde\"],\n\n\t\t\t\"bestand-lijst-selectie\",\n\t\t\t[\"space\", \"wissel bestand selectie\"],\n\t\t\t[\"↑/↓\", \"verplaats selectie cursor\"],\n\t\t\t[\"ctrl ↑/↓\", \"verplaats cursor en scherm\"],\n\t\t\t[\"⇧ ↑/↓\", \"select vorige/volgende bestand\"],\n\t\t\t[\"ctrl-A\", \"selecteer alle bestanden / mappen\"],\n\t\t], [\n\t\t\t\"navigatie\",\n\t\t\t[\"B\", \"verwissel breadcrumbs / navpane\"],\n\t\t\t[\"I/K\", \"Vorige/volgende map\"],\n\t\t\t[\"M\", \"Bovenliggende map (of huidige uitvouwen)\"],\n\t\t\t[\"V\", \"Berwissel map / tekstbestand in navpane\"],\n\t\t\t[\"A/D\", \"Navpane formaat\"],\n\t\t], [\n\t\t\t\"muziek-speler\",\n\t\t\t[\"J/L\", \"Vorige/volgende song\"],\n\t\t\t[\"U/O\", \"Skip 10sec terug/vooruit\"],\n\t\t\t[\"0..9\", \"Spring naar 0%..90%\"],\n\t\t\t[\"P\", \"Speel/pauzeer (start ook)\"],\n\t\t\t[\"S\", \"Selecteer afspelende song\"],\n\t\t\t[\"Y\", \"Download song\"],\n\t\t], [\n\t\t\t\"afbeelding viewer\",\n\t\t\t[\"J/L, ←/→\", \"Vorige/volgende afbeelding\"],\n\t\t\t[\"Home/End\", \"Eerste/laatste afbeelding\"],\n\t\t\t[\"F\", \"Volledig scherm\"],\n\t\t\t[\"R\", \"Draai rechtsom\"],\n\t\t\t[\"⇧ R\", \"Draai linksom\"],\n\t\t\t[\"S\", \"Selecteer afbeelding\"],\n\t\t\t[\"Y\", \"Download afbeelding\"],\n\t\t], [\n\t\t\t\"video-speler\",\n\t\t\t[\"U/O\", \"Skip 10sec terug/vooruit\"],\n\t\t\t[\"P/K/Space\", \"Speel/pauze\"],\n\t\t\t[\"C\", \"Verder met volgende\"],\n\t\t\t[\"V\", \"herhaal\"],\n\t\t\t[\"M\", \"stil\"],\n\t\t\t[\"[ and ]\", \"zet herhaal interval\"],\n\t\t], [\n\t\t\t\"tekstbestand-viewer\",\n\t\t\t[\"I/K\", \"vorige/volgende bestand\"],\n\t\t\t[\"M\", \"sluit tekst bestand\"],\n\t\t\t[\"E\", \"bewerk tekst bestand\"],\n\t\t\t[\"S\", \"selecteer bestand (voor knip/kopie/hernoem)\"],\n\t\t\t[\"Y\", \"tekst bestand downloaden\"], //m\n\t\t\t[\"⇧ J\", \"json verfraaien\"], //m\n\t\t]\n\t],\n\n\t\"m_ok\": \"OK\",\n\t\"m_ng\": \"Annuleren\",\n\n\t\"enable\": \"Inschakelen\",\n\t\"danger\": \"GEVAARLIJK\",\n\t\"clipped\": \"Gekopieërd naar klembord\",\n\n\t\"ht_s1\": \"seconde\",\n\t\"ht_s2\": \"secondes\",\n\t\"ht_m1\": \"minuut\",\n\t\"ht_m2\": \"minuten\",\n\t\"ht_h1\": \"uur\",\n\t\"ht_h2\": \"uur\",\n\t\"ht_d1\": \"dag\",\n\t\"ht_d2\": \"dagen\",\n\t\"ht_and\": \" en \",\n\n\t\"goh\": \"Beheer-paneel\",\n\t\"gop\": 'Vorige map\">Vorige',\n\t\"gou\": 'Bovenligende map\">Omhoog',\n\t\"gon\": 'Volgende map\">Volgende',\n\t\"logout\": \"Uitloggen \",\n\t\"login\": \"Inloggen\", //m\n\t\"access\": \" Toegang\",\n\t\"ot_close\": \"Sluit onder-menu\",\n\t\"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`&quot;try unite&quot;` = moet precies «try unite» bevatten$N$Nde datum formaat is iso-8601, zoals$N`2009-12-31` of `2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"unpost: verwijder je recente uploads, of onvoltooide uploads afbreken\",\n\t\"ot_bup\": \"bup: Basisuploader, supports zelfs netscape 4.0\",\n\t\"ot_mkdir\": \"mkdir: Maak een nieuwe map\",\n\t\"ot_md\": \"new-file: Maak een nieuw tekstbestand\", //m\n\t\"ot_msg\": \"msg: Verstuur een bericht naar de server logs\",\n\t\"ot_mp\": \"Media speler opties\",\n\t\"ot_cfg\": \"Configuratie opties\",\n\t\"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 [🎈]&nbsp; (de basic uploader)<br /><br />tijdens het uploaden, dit icoon word dan een progress indicatie!',\n\t\"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 [🎈]&nbsp; (de basic uploader)<br /><br />tijdens het uploaden, dit icoon word dan een progress indicatie!',\n\t\"ot_noie\": 'Gebruik alstublieft Chrome / Firefox / Edge',\n\n\t\"ab_mkdir\": \"maak map\",\n\t\"ab_mkdoc\": \"nieuw tekstbestand\", //m\n\t\"ab_msg\": \"verstuur msg naar srv log\",\n\n\t\"ay_path\": \"skip naar mappen\",\n\t\"ay_files\": \"skip naar bestanden\",\n\n\t\"wt_ren\": \"Hernoem geselecteerde items$NHotkey: F2\",\n\t\"wt_del\": \"Berwijder geselecteerde items$NHotkey: ctrl-K\",\n\t\"wt_cut\": \"Knip geselecteerde items &lt;small&gt;(en plak het ergens anders)&lt;/small&gt;$NHotkey: ctrl-X\",\n\t\"wt_cpy\": \"Kopieer geselecteerde items naar klembord$N(om te plakken ergens anders)$NHotkey: ctrl-C\",\n\t\"wt_pst\": \"Plak eeen laatst geknipte / gekopieërde selectie$NHotkey: ctrl-V\",\n\t\"wt_selall\": \"Selecteer alle bestanden$NHotkey: ctrl-A (wanneer bestand gefocused is)\",\n\t\"wt_selinv\": \"Selectie omkeren\",\n\t\"wt_zip1\": \"Download deze map als archief\",\n\t\"wt_selzip\": \"Download selectie als archief\",\n\t\"wt_seldl\": \"Download selectie als losse bestanden$NHotkey: Y\",\n\t\"wt_npirc\": \"Kopieer irc-geformarteerde track info\",\n\t\"wt_nptxt\": \"Kopieer platte tekst track info\",\n\t\"wt_m3ua\": \"Aan m3u afspeellijst toevoegen (klik <code>📻kopieer</code> later)\",\n\t\"wt_m3uc\": \"Kopieer m3u playlist naar klembord\",\n\t\"wt_grid\": \"Verwissel grid / lijst weergave$NHotkey: G\",\n\t\"wt_prev\": \"Vorig nummer$NHotkey: J\",\n\t\"wt_play\": \"Afspelen / pauzeer$NHotkey: P\",\n\t\"wt_next\": \"Volgend nummer$NHotkey: L\",\n\n\t\"ul_par\": \"Parallel uploads:\",\n\t\"ut_rand\": \"Willekeurige bestandsnaam\",\n\t\"ut_u2ts\": \"Kopieer de laatste-gewijzigde tijdstamp$Nvan je bestandsysteem naar de server\\\">📅\",\n\t\"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\n\t\"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\",\n\t\"ut_ask\": 'Vraag voor bevestiging voordat het uploaden start\">💭',\n\t\"ut_pot\": \"Verbeter de uploadsnelheid voor langzame apparaten$Ndoor de interface minder complex te maken\",\n\t\"ut_srch\": \"Niet uploaden, maar check of de bestanden als op de server bestaan$N (checkt alle mappen die waar jij toegang op hebt)\",\n\t\"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\",\n\t\"ul_btn\": \"Drop bestanden / mappen<br>hier (of klik mij)\",\n\t\"ul_btnu\": \"U P L O A D\",\n\t\"ul_btns\": \"Z O E K E N\",\n\n\t\"ul_hash\": \"Hashing\",\n\t\"ul_send\": \"Versturen\",\n\t\"ul_done\": \"Klaar\",\n\t\"ul_idle1\": \"Geen uploads in wachtrij\",\n\t\"ut_etah\": \"Gemiddelde &lt;em&gt;hashing&lt;/em&gt; snelheid en geschatte tijd tot de voltooiing\",\n\t\"ut_etau\": \"Gemiddelde &lt;em&gt;verzend&lt;/em&gt; snelheid en geschatte tijd tot voltooiing\",\n\t\"ut_etat\": \"Gemiddelde &lt;em&gt;totale&lt;/em&gt; snelheid en geschatte tijd tot voltooiing\",\n\n\t\"uct_ok\": \"Succesvol afgerond\",\n\t\"uct_ng\": \"Niet goed: gefaald / geweigerd / niet gevonden\",\n\t\"uct_done\": \"ok en ng gecombineerd\",\n\t\"uct_bz\": \"Hashing van uploads\",\n\t\"uct_q\": \"Inactief, in afwachting\",\n\n\t\"utl_name\": \"Bestandsnaam\",\n\t\"utl_ulist\": \"Lijst\",\n\t\"utl_ucopy\": \"Kopieer\",\n\t\"utl_links\": \"Links\",\n\t\"utl_stat\": \"Status\",\n\t\"utl_prog\": \"Vooruitgang\",\n\n\t// keep short:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"FOUT\",\n\t\"utl_oserr\": \"OS-FOUT\",\n\t\"utl_found\": \"gevonden\",\n\t\"utl_defer\": \"Uitgesteld\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"klaar\",\n\n\t\"ul_flagblk\": \"De bestanden zijn toegevoegd aan de wachtrij</b><br>maar er is een drukke up2k bezig in een andere tabblad,<br>wachten totdat die eerst klaar is\",\n\t\"ul_btnlk\": \"De server configuratie heeft deze schakelaar versleuteld in deze staat\",\n\n\t\"udt_up\": \"Upload\",\n\t\"udt_srch\": \"Zoeken\",\n\t\"udt_drop\": \"Laat hier los\",\n\n\t\"u_nav_m\": '<h6>Hey, wat heb jij daar?</h6><code>Enter</code> = Bestanden (een of meer)\\n<code>ESC</code> = Een map (inclusief submappen)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Bestanden</a><a href=\"#\" id=\"modal-ng\">Een map</a>',\n\n\t\"cl_opts\": \"Switches\",\n\t\"cl_hfsz\": \"Bestandsgrootte\", //m\n\t\"cl_themes\": \"Thema\",\n\t\"cl_langs\": \"Taal\",\n\t\"cl_ziptype\": \"Download map als\",\n\t\"cl_uopts\": \"up2k switches\",\n\t\"cl_favico\": \"Favicon\",\n\t\"cl_bigdir\": \"Item limiet in map\",\n\t\"cl_hsort\": \"#sorteer\",\n\t\"cl_keytype\": \"Key notaties\",\n\t\"cl_hiddenc\": \"Verborgen kolomen\",\n\t\"cl_hidec\": \"Verborgen\",\n\t\"cl_reset\": \"Reset\",\n\t\"cl_hpick\": \"Tik op de kolomkoppen om ze in de onderstaande tabel te verbergen\",\n\t\"cl_hcancel\": \"Kolumn verbergen geannuleerd\",\n\t\"cl_rcm\": \"Rechtermuisknopmenu\", //m\n\n\t\"ct_grid\": '田 grid',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ tooltips',\n\t\"ct_thumb\": 'In grid-overzicht, wissel tussen iconen of thumbnails$NHotkey: T\">🖼️ thumbs',\n\t\"ct_csel\": 'Gebruik CTRL en SHIFT voor de bestand selectie in grid-overzicht>sel',\n\t\"ct_dsel\": 'Gebruik slepen om te selecteren in grid-overzicht>slepen', //m\n\t\"ct_dl\": 'download afdwingen (niet inline weergeven) wanneer op een bestand wordt geklikt\">dl', //m\n\t\"ct_ihop\": 'Als je afbeeldingviewer afsluit, scroll omlaag naar de laatst bekeken bestand\">g⮯',\n\t\"ct_dots\": 'Laat verborgen bestanden zien (als de server dat toestaat)\">dotfiles',\n\t\"ct_qdel\": 'Waneeer je een bestand verwijderd, vraag eenmalig om bevestiging\">qdel',\n\t\"ct_dir1st\": 'Sorteer mappen eerst en dan de bestanden\">📁 first',\n\t\"ct_nsort\": 'Natural sort (voor bestandsnamen dat beginnen met getallen)\">nsort',\n\t\"ct_readme\": 'Laat README.md in mappen lijst zien\">📜 readme',\n\t\"ct_utc\": 'Toon alle datums en tijden in UTC\">UTC',\n\t\"ct_idxh\": 'Laat index.html zien in plaats van de map overzicht\">htm',\n\t\"ct_sbars\": 'Laat scrollbars zien\">⟊',\n\n\t\"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📅\",\n\n\t\"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 <em>&quot;heeft dit dezelfde bestands groote op de server?&quot;</em>, 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 &quot;upload&quot; de zelfde bestanden opnieuw uploaden zo de client het kan verifieren\\\">turbo\",\n\n\t\"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 <em>in theorie</em> de meest onvoltooide/onvoledige uploads, maar dit is geen vervaning voor de verificatie-check met de turbo knop uitgeschakeld daarna\\\">date-chk\",\n\n\t\"cut_u2sz\": \"Grote (in MiB) voor elk geuploade stuk; grote waardes vliegen beter over de Atlantische Oceaan. Probeer lage waardes op zeer onstabiele verbindingen\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cut_nag\": \"Systeem notificatie weergeven als een upload voltooid is$N(alleen als de browser of tabblad niet actief is)\",\n\t\"cut_sfx\": \"Geluid waarschuwing afspelen als een upload voltooid is$N(alleen als de browser of tabblad niet actief is)\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cft_text\": \"Favicon tekst (laat leeg en vernieuw om uit te schakelen)\",\n\t\"cft_fg\": \"Voorgrondkleur\",\n\t\"cft_bg\": \"Achtergrondkleur\",\n\n\t\"cdt_lim\": \"Max aantal bestanden laten zien in een map\",\n\t\"cdt_ask\": \"Als helemaal naar beneden gescrolld bent,$Nin plaats van meer inladen,$Nvraag wat het moet doen\",\n\t\"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.\",\n\t\"cdt_ren\": \"Aangepast rechtermuisknopmenu inschakelen, het normale menu blijft beschikbaar met shift + rechtermuisknop\\\">inschakelen\", //m\n\t\"cdt_rdb\": \"toon het normale rechtermuisknopmenu wanneer het aangepaste al open is en opnieuw wordt geklikt\\\">x2\", //m\n\n\t\"tt_entree\": \"Laat navpane zien (directoryboom zijbalk)$NHotkey: B\",\n\t\"tt_detree\": \"Laat breadcrumbs zien$NHotkey: B\",\n\t\"tt_visdir\": \"Scroll naar geselecteerde map\",\n\t\"tt_ftree\": \"Verwissel tussen directoryboom / tekst bestanden$NHotkey: V\",\n\t\"tt_pdock\": \"Laat bovenliggende mappen zien in een vastgezet deelvenster bovenaan\",\n\t\"tt_dynt\": \"Automatisch groeien naarmate de directoryboom zich uitbreidt\",\n\t\"tt_wrap\": \"Automatische terugloop\",\n\t\"tt_hover\": \"Laat overlopenden lijnen zien bij zweven$N(stopt het scrollen tenzij de muis in de linker gedeelte van het scherm is)\",\n\n\t\"ml_pmode\": \"Aan het einde van de map...\",\n\t\"ml_btns\": \"Cmds\",\n\t\"ml_tcode\": \"Transcode\",\n\t\"ml_tcode2\": \"Transcode naar\",\n\t\"ml_tint\": \"Tint\",\n\t\"ml_eq\": \"Audio-equalizer\",\n\t\"ml_drc\": \"Dynamisch bereikcompressor\",\n\t\"ml_ss\": \"Stiltes overslaan\", //m\n\n\t\"mt_loop\": \"Loop/herhaal een nummer\\\">🔁\",\n\t\"mt_one\": \"Stop na een nummer\\\">1️⃣\",\n\t\"mt_shuf\": \"Shuffle alle muziek in alle mappen\\\">🔀\",\n\t\"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▶\",\n\t\"mt_preload\": \"Begin het laden van de volgende nummer vlak voordat de huidige nummer het einde bereikt voor gapless playback\\\">preload\",\n\t\"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\",\n\t\"mt_fullpre\": \"Probeer het hele nummer vooraf te laden;$N✅ activeer dit op <b>onstabiele</b> verbindingen,$N❌ <b>zet uit</b> als je waarschijnlijk een trage verbinding hebt\\\">full\",\n\t\"mt_fau\": \"Op telefoons, voorkom muziek van stoppen als de volgende nummer niet snel genoeg voorgeladen is (kan de weergave van tags glitchy maken)\\\">☕️\",\n\t\"mt_waves\": \"Waveform zoekbar:$NToon audio-amplitude in de zoekbar\\\">~s\",\n\t\"mt_npclip\": \"Knoppen tonen voor het clipboarden van het nummer dat op dat moment wordt afgespeeld\\\">/np\",\n\t\"mt_m3u_c\": \"Knoppen tonen om de geselecteerde nummers als m3u8-afspeellijstitems te clipboarden\\\">📻\",\n\t\"mt_octl\": \"OS-integratie (media hotkeys / osd)\\\">os-ctl\",\n\t\"mt_oseek\": \"Zoeken via os-integratie mogelijk maken$N$NNotitie: op sommige toestellen (iPhones) dit vervcangt de volgende-nummer knop\\\">seek\",\n\t\"mt_oscv\": \"Albumhoes weergeven in osd\\\">art\",\n\t\"mt_follow\": \"Het afgespeelde nummer in beeld houden\\\">🎯\",\n\t\"mt_compact\": \"Compacte bedieningselementen\\\">⟎\",\n\t\"mt_uncache\": \"Cache wissen &nbsp;(Probeer dit als uw browser een kapotte kopie van een nummer heeft gecached, waardoor het niet afgespeeld kan worden)\\\">uncache\",\n\t\"mt_mloop\": \"De open map herhalen\\\">🔁 loop\",\n\t\"mt_mnext\": \"Laad de volgende map en ga verder\\\">📂 next\",\n\t\"mt_mstop\": \"Stoppen met afspelen\\\">⏸ stop\",\n\t\"mt_cflac\": \"flac / wav omzetten naar {0}\\\">flac\",\n\t\"mt_caac\": \"aac / m4a omzetten naar {0}\\\">aac\",\n\t\"mt_coth\": \"Alle andere bestanden (geen mp3) converteren naar {0}\\\">oth\",\n\t\"mt_c2opus\": \"Beste keuze voor computers, laptops, android\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, voor iOS 17.5 en nieuwer\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, voor iOS 11 tot en met iOS 17\\\">caf\",\n\t\"mt_c2mp3\": \"Gebruik dit hele oude toestellen\\\">mp3\",\n\t\"mt_c2flac\": \"Beste geluidskwaliteit, maar grote downloads\\\">flac\", //m\n\t\"mt_c2wav\": \"Ongemprimeerde weergave (nog groter)\\\">wav\", //m\n\t\"mt_c2ok\": \"Mooi, goede keuze\",\n\t\"mt_c2nd\": \"Dat is niet het aanbevolen uitvoerformaat voor uw apparaat, maar dat is prima\",\n\t\"mt_c2ng\": \"Uw apparaat lijkt dit uitvoerformaat niet te ondersteunen, maar we gaan het toch proberen\",\n\t\"mt_xowa\": \"iOS bevat bugs waardoor dit formaat niet op de achtergrond kan worden afgespeeld; gebruik in plaats daarvan caf of mp3.\",\n\t\"mt_tint\": \"Achtergrond helderheid (0-100) op de zoekbalk om bufferen minder storend te maken\",\n\t\"mt_eq\": \"`Schakelt de equalizer en gain-control in;$N$Nboost `0` = standaard 100% volume (ongeweijzigd)$N$Nwidth `1 &nbsp;` = standaard stereo (ongeweijzigd)$Nwidth `0.5` = 50% links-rechts crossfeed$Nwidth `0 &nbsp;` = mono$N$Nboost `-0.8` &amp; 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.\",\n\t\"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)\",\n\t\"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\n\t\"mt_ssvt\": \"Volumedrempel (0-255)\\\">vol\", //m\n\t\"mt_ssts\": \"Actieve drempel (% track, begin)\\\">beg\", //m\n\t\"mt_sste\": \"Actieve drempel (% track, einde)\\\">eind\", //m\n\t\"mt_sssm\": \"Vermenigvuldiger afspeelsnelheid\\\">vrs\", //m\n\n\t\"mb_play\": \"Afspelen\",\n\t\"mm_hashplay\": \"Deze audio bestand afspelen?\",\n\t\"mm_m3u\": \"Druk op <code>Enter/OK</code> om af te spelen\\nDruk op <code>ESC/Annuleren</code> om te bewerken\",\n\t\"mp_breq\": \"Heeft firefox 82+ of chrome 73+ of iOS 15+\",\n\t\"mm_bload\": \"Aan het laden...\",\n\t\"mm_bconv\": \"Opmzetten naar {0}, even geduld...\",\n\t\"mm_opusen\": \"Uw browser kan geen aac / m4a-bestanden afspelen;\\ntranscodering naar opus is nu ingeschakeld\",\n\t\"mm_playerr\": \"Afspelen mislukt: \",\n\t\"mm_eabrt\": \"De afspeelpoging is geannuleerd\",\n\t\"mm_enet\": \"Je internetverbinding is onstabiel\",\n\t\"mm_edec\": \"Dit bestand is vermoedelijk beschadigd??\",\n\t\"mm_esupp\": \"Uw browser begrijpt deze audio-formaat niet\",\n\t\"mm_eunk\": \"Onbekende fout\",\n\t\"mm_e404\": \"Kan audio niet afspelen; fout 404: Bestand niet gevonden..\",\n\t\"mm_e403\": \"Kan audio niet afspelen; fout 403: Toegang geweigerd.\\n\\nProbeer op F5 te drukken om opnieuw te laden, misschien ben je uitgelogd\",\n\t\"mm_e415\": \"Kan geen audio afspelen; fout 415: Bestandsconversie mislukt; controleer serverlogs.\", //m\n\t\"mm_e500\": \"Kan geen audio afspelen; fout 500: Controleer serverlogs.\",\n\t\"mm_e5xx\": \"Kan geen audio afspelen; serverfout \",\n\t\"mm_nof\": \"Geen audiobestanden meer vinden in de buurt\",\n\t\"mm_prescan\": \"Op zoek naar muziek om als volgende te spelen...\",\n\t\"mm_scank\": \"Het volgende nummer gevonden:\",\n\t\"mm_uncache\": \"Cache gewist; alle nummers worden opnieuw gedownload bij de volgende keer afspelen\",\n\t\"mm_hnf\": \"Dat liedje bestaat niet meer\",\n\n\t\"im_hnf\": \"Deze afbeelding bestaat niet meer\",\n\n\t\"f_empty\": 'Deze map is leeg',\n\t\"f_chide\": 'Dit verbergt kolom «{0}»\\n\\nje kunt kolommen verbergen op de instellingen tabblad',\n\t\"f_bigtxt\": \"Dit bestand is {0} MiB groot -- echt bekijken als tekst?\",\n\t\"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.\",\n\t\"fbd_more\": '<div id=\"blazy\"><code>{0}</code> van de <code>{1}</code> bestanden weergegeven; <a href=\"#\" id=\"bd_more\">Toon {2}</a> of <a href=\"#\" id=\"bd_all\">Laat alles zien</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\"><code>{0}</code> van de <code>{1}</code> bestanden weergegeven; <a href=\"#\" id=\"bd_all\">Laat alles zien</a></div>',\n\t\"f_anota\": \"Alleen {0} van de {1} items zijn geselecteerd;\\nom de volledige map te selecteren, scrol je eerst naar beneden\",\n\n\t\"f_dls\": 'de bestandslinks in de huidige map zijn veranderd in downloadlinks',\n\t\"f_dl_nd\": 'map wordt overgeslagen (gebruik in plaats daarvan zip/tar-download):\\n', //m\n\n\t\"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 <code>.PARTIAL</code>. 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 <code>.PARTIAL</code> scratchbestand, waardoor u vrijwel zeker beschadigde gegevens krijgt.\",\n\n\t\"ft_paste\": \"plakken {0} items$NHotkey: ctrl-V\",\n\t\"fr_eperm\": 'kan de naam niet wijzigen:\\nje hebt geen “move” rechten in deze map',\n\t\"fd_eperm\": 'kan niet verwijderen:\\nje hebt geen “delete” rechten in deze map',\n\t\"fc_eperm\": 'kan niet knippen:\\nje hebt geen “move” rechten in deze map',\n\t\"fp_eperm\": 'kan niet plakken:\\nje hebt geen “schrijf” rechten in deze map',\n\t\"fr_emore\": \"selecteer ten minste één item om te hernoemen\",\n\t\"fd_emore\": \"selecteer minstens één item om te verwijderen\",\n\t\"fc_emore\": \"selecteer ten minste één item om te knippen\",\n\t\"fcp_emore\": \"selecteer ten minste één item om naar het klembord te kopiëren\",\n\n\t\"fs_sc\": \"Deel de map waarin je je bevindt\",\n\t\"fs_ss\": \"De geselecteerde bestand(en) delen\",\n\t\"fs_just1d\": \"U kunt niet meer dan één map selecteren\\nof mix bestanden en mappen in één selectie\",\n\t\"fs_abrt\": \"❌ Afbreken\",\n\t\"fs_rand\": \"🎲 rand.naam\",\n\t\"fs_go\": \"✅ Maak share\",\n\t\"fs_name\": \"Naam\",\n\t\"fs_src\": \"Bron\",\n\t\"fs_pwd\": \"Wachtwoord\",\n\t\"fs_exp\": \"Verloopt\",\n\t\"fs_tmin\": \"min\",\n\t\"fs_thrs\": \"uur\",\n\t\"fs_tdays\": \"dag(en)\",\n\t\"fs_never\": \"eeuwig\",\n\t\"fs_pname\": \"Optionele linknaam; is willekeurig als deze leeg is\",\n\t\"fs_tsrc\": \"Het bestand of de map die u wilt delen\",\n\t\"fs_ppwd\": \"Optioneel wachtwoord\",\n\t\"fs_w8\": \"Delen...\",\n\t\"fs_ok\": \"Druk op <code>Enter/OK</code> naar klembord te zetten\\Druk op <code>ESC/Annuleren</code> om te sluiten\",\n\n\t\"frt_dec\": \"Kan sommige gevallen van gebroken bestandsnamen oplossen\\\">url-decode\",\n\t\"frt_rst\": \"Gewijzigde bestandsnamen terugzetten naar de oorspronkelijke namen\\\">↺ reset\",\n\t\"frt_abrt\": \"Afbreken en dit venster sluiten\\\">❌ Annuleren\",\n\t\"frb_apply\": \"HERNOEMEN TOEPASSEN\",\n\t\"fr_adv\": \"Batch / metadata / patroon hernoemen\\\">Geavanceerd\",\n\t\"fr_case\": \"Hoofdlettergevoelige regex\\\">case\",\n\t\"fr_win\": \"Windows-veilige namen; vervangen <code>&lt;&gt;:&quot;\\\\|?*</code> met japanse tekens over de volledige breedte\\\">win\",\n\t\"fr_slash\": \"Vervang <code>/</code> met een teken waardoor er geen nieuwe mappen worden gemaakt\\\">geen /\",\n\t\"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\",\n\t\"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)\",\n\t\"fr_pdel\": \"Verwijderen\",\n\t\"fr_pnew\": \"Opslaan als\",\n\t\"fr_pname\": \"Geef een naam op voor je nieuwe preset\",\n\t\"fr_aborted\": \"Afgebroken\",\n\t\"fr_lold\": \"Oude naam\",\n\t\"fr_lnew\": \"Nieuwe naam\",\n\t\"fr_tags\": \"Tags voor de geselecteerde bestanden (alleen-lezen, alleen ter referentie):\",\n\t\"fr_busy\": \"Hernoemen van {0} items...\\n\\n{1}\",\n\t\"fr_efail\": \"Hernoemen mislukt:\\n\",\n\t\"fr_nchg\": \"{0} van de nieuwe namen zijn gewijzigd als gevolg van <cod>win</code> en/of <code>geen /</code>\\n\\nOK om door te gaan met deze gewijzigde nieuwe namen?\",\n\n\t\"fd_ok\": \"Verwijderen OK\",\n\t\"fd_err\": \"Verwijderen mislukt:\\n\",\n\t\"fd_none\": \"Er is niets verwijderd; misschien geblokkeerd door serverconfiguratie (xbd)?\",\n\t\"fd_busy\": \"{0} items verwijderen...\\n\\n{1}\",\n\t\"fd_warn1\": \"VERWIJDER deze {0} items?\",\n\t\"fd_warn2\": \"<b>LAATSTE KANS!</b> Geen manier om ongedaan te maken. Verwijderen?\",\n\n\t\"fc_ok\": \"Knip {0} items\",\n\t\"fc_warn\": 'Knip {0} items\\n\\nmaar: alleen <b>deze</b> browser-tabblad kan weer plakken\\n(omdat de selectie zo enorm is)',\n\n\t\"fcc_ok\": \"{0} items naar klembord gekopieerd\",\n\t\"fcc_warn\": '{0} items naar klembord gekopieerd\\n\\maar: alleen <b>deze</b> browser-tabblad kan weer plakken\\n(omdat de selectie zo enorm is)',\n\n\t\"fp_apply\": \"Gebruik deze namen\",\n\t\"fp_skip\": \"Conflicten overslaan\", //m\n\t\"fp_ecut\": \"Knip of kopieer eerst enkele bestanden/mappen om te verplaatsen/plakken\\n\\nnotitie: je kunt knippen/plakken in verschillende browsertabbladen\",\n\t\"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\n\t\"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\n\t\"fp_emore\": \"Er zijn nog enkele bestandsnaambotsingen die moeten worden opgelost\",\n\t\"fp_ok\": \"Verplaatsen OK\",\n\t\"fcp_ok\": \"Kopiëren OK\",\n\t\"fp_busy\": \"{0} items verplaatsen...\\n\\n{1}\",\n\t\"fcp_busy\": \"{0} items kopiëren...\\n\\n{1}\",\n\t\"fp_abrt\": \"afbreken...\", //m\n\t\"fp_err\": \"Verplaatsen mislukt:\\n\",\n\t\"fcp_err\": \"Kopieëren mislukt:\\n\",\n\t\"fp_confirm\": \"Verplaats deze {0} items hierheen?\",\n\t\"fcp_confirm\": \"Kopieer deze {0} items hier?\",\n\t\"fp_etab\": 'Kan klembord van ander browsertabblad niet lezen',\n\t\"fp_name\": \"Een bestand uploaden vanaf uw apparaat. Geef het een naam:\",\n\t\"fp_both_m\": '<h6>Kies wat je wilt plakken</h6><code>Enter</code> = Verplaatsen {0} bestanden van «{1}»\\n<code>ESC</code> = Upload {2} bestanden van je apparaat',\n\t\"fcp_both_m\": '<h6>Kies wat je wilt plakken</h6><code>Enter</code> = Kopieer {0} bestanden van «{1}»\\n<code>ESC</code> = Upload {2} bestanden van je apparaat',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Verplaats</a><a href=\"#\" id=\"modal-ng\">Upload</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Kopieer</a><a href=\"#\" id=\"modal-ng\">Upload</a>',\n\n\t\"mk_noname\": \"Voer een naam in het tekstveld aan de linkerkant voordat je verder gaat :p\",\n\t\"nmd_i1\": \"Voeg ook de gewenste extensie toe, bijvoorbeeld <code>.md</code>\", //m\n\t\"nmd_i2\": \"Je kunt alleen <code>.{0}</code>-bestanden maken omdat je geen verwijderrechten hebt\", //m\n\n\t\"tv_load\": \"Tekstdocument laden:\\n\\n{0}\\n\\n{1}% ({2} van de {3} MiB geladen)\",\n\t\"tv_xe1\": \"Kon tekstbestand niet laden:\\n\\nfout \",\n\t\"tv_xe2\": \"404, bestand niet gevonden\",\n\t\"tv_lst\": \"Lijst met tekstbestanden in\",\n\t\"tvt_close\": \"Terugkeren naar mapweergave$NHotkey: M (of Esc)\\\">❌ Sluiten\",\n\t\"tvt_dl\": \"Download dit bestand$NHotkey: Y\\\">💾 download\",\n\t\"tvt_prev\": \"Vorig document tonen$NHotkey: i\\\">⬆ prev\",\n\t\"tvt_next\": \"Volgende document tonen$NHotkey: K\\\">⬇ next\",\n\t\"tvt_sel\": \"Selecteer bestand &nbsp; ( voor knip / verplaats / verwijder / ... )$NHotkey: S\\\">sel\",\n\t\"tvt_j\": \"json verfraaien$NHotkey: shift-J\\\">j\", //m\n\t\"tvt_edit\": \"Bestand openen in teksteditor$NHotkey: E\\\">✏️ bewerk\",\n\t\"tvt_tail\": \"Bestand controleren op wijzigingen; nieuwe regels in realtime weergeven\\\">📡 volgen\",\n\t\"tvt_wrap\": \"Automatische terugloop\\\">↵\",\n\t\"tvt_atail\": \"Vergrendelen scroll naar onderkant van pagina\\\">⚓\",\n\t\"tvt_ctail\": \"Kleuren van terminals decoderen (ansi escape codes)\\\">🌈\",\n\t\"tvt_ntail\": \"Terugrollimiet (hoeveel tekst geladen moeten blijven)\",\n\n\t\"m3u_add1\": \"Nummer toegevoegd aan m3u afspeellijst\",\n\t\"m3u_addn\": \"{0} nummers toegevoegd aan m3u-afspeellijst\",\n\t\"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\",\n\n\t\"gt_vau\": \"Laat geen video's zien, speel alleen de audio af\\\">🎧\",\n\t\"gt_msel\": \"Schakel bestandsselectie in; ctrl-klik op een bestand om te openen$N$N&lt;em&gt;indien actief: dubbelklik op een bestand / map om het te openen&lt;/em&gt;$N$NHotkey: S\\\">multiselect\",\n\t\"gt_crop\": \"Gecentreerde miniaturen\\\">crop\",\n\t\"gt_3x\": \"Hi-res miniaturen\\\">3x\",\n\t\"gt_zoom\": \"Zoom\",\n\t\"gt_chop\": \"Verkorten\",\n\t\"gt_sort\": \"Sorteer bij\",\n\t\"gt_name\": \"naam\",\n\t\"gt_sz\": \"grootte\",\n\t\"gt_ts\": \"datum\",\n\t\"gt_ext\": \"type\",\n\t\"gt_c1\": \"Bestandsnamen meer inkorten (minder tonen)\",\n\t\"gt_c2\": \"Bestandsnamen minder inkorten (meer tonen)\",\n\n\t\"sm_w8\": \"Zoeken...\",\n\t\"sm_prev\": \"Onderstaande zoekresultaten zijn afkomstig van een eerdere zoekopdracht:\\n  \",\n\t\"sl_close\": \"Zoekresultaten sluiten\",\n\t\"sl_hits\": \"Toont {0} treffers\",\n\t\"sl_moar\": \"Laad meer\",\n\n\t\"s_sz\": \"grootte\",\n\t\"s_dt\": \"datum\",\n\t\"s_rd\": \"pad\",\n\t\"s_fn\": \"naam\",\n\t\"s_ta\": \"tags\",\n\t\"s_ua\": \"op@\",\n\t\"s_ad\": \"adv.\",\n\t\"s_s1\": \"Minimaal MiB\",\n\t\"s_s2\": \"Maximaal MiB\",\n\t\"s_d1\": \"Min. iso8601\",\n\t\"s_d2\": \"Max. iso8601\",\n\t\"s_u1\": \"Uploaded na\",\n\t\"s_u2\": \"en/of voor\",\n\t\"s_r1\": \"Pad bevad &nbsp; (spatie-gescheiden)\",\n\t\"s_f1\": \"Naam bevat &nbsp; (ontkennen met -nope)\",\n\t\"s_t1\": \"Tags bevat &nbsp; (^=start, einde=$)\",\n\t\"s_a1\": \"Specifieke metadata-eigenschappen\",\n\n\t\"md_eshow\": \"Kan niet weergeven \",\n\t\"md_off\": \"[📜<em>readme</em>] uitgeschakeld in [⚙️] -- document verborgen\",\n\n\t\"badreply\": \"Mislukt om antwoord van server te parsen\",\n\n\t\"xhr403\": \"403: Toegang geweigerd\\n\\nprobeer F5 in te drukken, misschien ben je uitgelogd\",\n\t\"xhr0\": \"Onbekend (waarschijnlijk verbinding met server verloren of server is offline)\",\n\t\"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\",\n\t\"tl_xe1\": \"Kon submappen niet weergeven:\\n\\nfout \",\n\t\"tl_xe2\": \"404: Map niet gevonden\",\n\t\"fl_xe1\": \"Kon bestanden in map niet weergeven:\\n\\nfout \",\n\t\"fl_xe2\": \"404: Map niet gevonden\",\n\t\"fd_xe1\": \"Kon submap niet aanmaken:\\n\\nfout \",\n\t\"fd_xe2\": \"404: Bovenliggende map niet gevonden\",\n\t\"fsm_xe1\": \"Kon bericht niet verzenden:\\n\\nfout \",\n\t\"fsm_xe2\": \"404: Bovenliggende map niet gevonden\",\n\t\"fu_xe1\": \"Mislukt om unpost lijst van server te laden:\\n\\nfout \",\n\t\"fu_xe2\": \"404: Bestand niet gevonden??\",\n\n\t\"fz_tar\": \"gnu-tar bestand uitpakken (linux / mac)\",\n\t\"fz_pax\": \"pax-formaat tar uitpakken (trager)\",\n\t\"fz_targz\": \"gnu-tar met gzip niveau 3 compressie$N$Ndit is meestal erg langzaam, dus gebruik in plaats daarvan ongecomprimeerde tar\",\n\t\"fz_tarxz\": \"gnu-tar met xz-niveau 1 compressie$N$Ndit is meestal erg langzaam, dus gebruik in plaats daarvan ongecomprimeerde tar\",\n\t\"fz_zip8\": \"Zip met utf8 bestandsnamen (misschien onhandig op windows 7 en ouder)\",\n\t\"fz_zipd\": \"Zip met traditionele cp437-bestandsnamen, voor echt oude software\",\n\t\"fz_zipc\": \"cp437 met crc32 vroeg berekend$Nvoor MS-DOS PKZIP v2.04g (oktober 1993)$N(het duurt langer voordat het downloaden kan beginnen)\",\n\n\t\"un_m1\": \"Hieronder kunt u uw recente uploads verwijderen (of onvoltooide uploads afbreken)\",\n\t\"un_upd\": \"Vernieuwen\",\n\t\"un_m4\": \"of deel de bestanden die hieronder zichtbaar zijn:\",\n\t\"un_ulist\": \"Toon\",\n\t\"un_ucopy\": \"Kopieer\",\n\t\"un_flt\": \"Optionele filter:&nbsp; URL moet het volgende bevatten\",\n\t\"un_fclr\": \"Reset filter\",\n\t\"un_derr\": 'unpost-verwijderen mislukt:\\n',\n\t\"un_f5\": 'Er is iets kapot, probeer te verversen of druk op F5',\n\t\"un_uf5\": \"Sorry, maar u moet de pagina vernieuwen (bijvoorbeeld door op F5 of CTRL-R te drukken) voordat deze upload kan worden afgebroken.\",\n\t\"un_nou\": '<b>Waarschuwing:</b> server te druk om onvoltooide uploads weer te geven; klik straks op de \"refresh\" link',\n\t\"un_noc\": '<b>Waarschuwing:</b> unpost van volledig geüploade bestanden is niet ingeschakeld/toegestaan in de serverconfiguratie',\n\t\"un_max\": \"Toont de eerste 2000 bestanden (gebruik de filter)\",\n\t\"un_avail\": \"{0} recente uploads kunnen worden verwijderd<br />{1} onvoltooide kunnen worden afgebroken\",\n\t\"un_m2\": \"Gesorteerd op uploadtijd; meest recente eerst:\",\n\t\"un_no1\": \"sike! geen enkele upload is recent genoeg\",\n\t\"un_no2\": \"sike! geen uploads die aan dat filter voldoen zijn voldoende recent\",\n\t\"un_next\": \"Verwijder de volgende {0} bestanden\",\n\t\"un_abrt\": \"Afbreken\",\n\t\"un_del\": \"Verwijderen\",\n\t\"un_m3\": \"Je recente uploads laden...\",\n\t\"un_busy\": \"Verwijderen van {0} bestanden...\",\n\t\"un_clip\": \"{0} links gekopieerd naar klembord\",\n\n\t\"u_https1\": \"Je moet\",\n\t\"u_https2\": \"overschakelen naar https\",\n\t\"u_https3\": \"voor betere prestaties\",\n\t\"u_ancient\": 'Je browser is indrukwekkend oud -- misschien moet je <a href=\"#\" onclick=\"goto(\\'bup\\')\">in plaats daarvan bup gebruiken</a>',\n\t\"u_nowork\": \"Je moet firefox 53+ of chrome 57+ of iOS 11+ hebben\",\n\t\"tail_2old\": \"Je moet firefox 105+ of chrome 71+ of iOS 14.5+ hebben\",\n\t\"u_nodrop\": 'Je browser is te oud voor uploaden via slepen en neerzetten',\n\t\"u_notdir\": \"Dat is geen map!\\n\\nuw browser is te oud,\\nprobeer in plaats daarvan sleep en neerzetten\",\n\t\"u_uri\": \"Om afbeeldingen te slepen vanuit andere browser tabblad,\\nplaats deze dan op de grote uploadknop\",\n\t\"u_enpot\": 'Overschakelen naar <a href=\"#\">potato UI</a> (kan uploadsnelheid verbeteren)',\n\t\"u_depot\": 'Overschakelen naar <a href=\"#\">fancy UI</a> (kan uploadsnelheid verminderen)',\n\t\"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!',\n\t\"u_pott\": \"<p>Bestanden: &nbsp; <b>{0}</b> klaar, &nbsp; <b>{1}</b> mislukt, &nbsp; <b>{2}</b> bezig, &nbsp; <b>{3}</b> in de wachtrij</p>\",\n\t\"u_ever\": \"Dit is de basis uploader; up2k heeft minstens het volgende nodig<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1\",\n\t\"u_su2k\": 'Dit is de basis uploader; <a href=\"#\" id=\"u2yea\">up2k</a> is beter',\n\t\"u_uput\": 'Optimaliseren voor snelheid (checksum overslaan)',\n\t\"u_ewrite\": 'Je hebt geen schrijftoegang tot deze map',\n\t\"u_eread\": 'Je hebt geen leestoegang tot deze map',\n\t\"u_enoi\": 'Zoeken naar bestanden is niet ingeschakeld in de serverconfiguratie',\n\t\"u_enoow\": \"Overschrijven zal hier niet werken; je heb verwijder toestemming nodig\",\n\t\"u_badf\": 'Deze {0} bestanden (van {1} totaal) zijn overgeslagen, mogelijk door bestandssysteemmachtigingen:\\n\\n',\n\t\"u_blankf\": 'Deze {0} bestanden (van {1} totaal) zijn leeg; alsnog uploaden?\\n\\n',\n\t\"u_applef\": 'Deze {0} bestanden (van {1} totaal) zijn waarschijnlijk ongewenst;\\nKlik op <code>OK/Enter</code> om de volgende bestanden over te slaan,\\Klik op <code>Annuleren/ESC</code> niet uit te sluiten en deze ook te uploaden:\\n\\n',\n\t\"u_just1\": '\\nMisschien werkt het beter als je slechts één bestand selecteert',\n\t\"u_ff_many\": \"Als je <b>Linux / MacOS / Android,</b> gebruikt dan <em>kan</em> deze hoeveelheid bestanden <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\">Firefox crashen!</a>\\nals dat gebeurt, probeer het dan opnieuw (of gebruik Chrome).\",\n\t\"u_up_life\": \"Deze upload wordt verwijderd van de server\\n{0} nadat het is voltooid\",\n\t\"u_asku\": 'Upload deze {0} bestanden naar <code>{1}</code>',\n\t\"u_unpt\": \"Je kunt deze upload ongedaan maken / verwijderen met de linkerbovenhoek 🧯\",\n\t\"u_bigtab\": 'We staan op het punt om {0} bestanden te tonen\\n\\nDit kan uw browser laten crashen, weet je het zeker??',\n\t\"u_scan\": 'Bestanden scannen...',\n\t\"u_dirstuck\": 'Directory iterator liep vast bij het benaderen van het volgende {0} items; zal het volgende overslaan:',\n\t\"u_etadone\": 'Klaar ({0}, {1} bestanden)',\n\t\"u_etaprep\": '(klaarmaken om te uploaden)',\n\t\"u_hashdone\": 'hashing klaar',\n\t\"u_hashing\": 'Hash',\n\t\"u_hs\": 'Hallo zeggen...',\n\t\"u_started\": \"De bestanden worden nu geüpload; zie [🚀]\",\n\t\"u_dupdefer\": \"Duplicaat; wordt verwerkt na alle andere bestanden\",\n\t\"u_actx\": \"klik op deze tekst om prestatieverlies</br>bij het overschakelen naar andere vensters/tabbladen te voorkomen\",\n\t\"u_fixed\": \"OK!&nbsp; Fixed it 👍\",\n\t\"u_cuerr\": \"Mislukt bij het uploaden van stuk {0} van {1};\\nwaarschijnlijk ongevaarlijk, doorgaan\\n\\nbestand: {2}\",\n\t\"u_cuerr2\": \"Upload door server geweigerd (stuk {0} van {1});\\nzal later opnieuw proberen\\n\\nbestand: {2}\\n\\nfout \",\n\t\"u_ehstmp\": \"Zal opnieuw proberen; zie rechtsonder\",\n\t\"u_ehsfin\": \"Server heeft het verzoek om de upload te finaliseren afgewezen; opnieuw proberen...\",\n\t\"u_ehssrch\": \"Server heeft de zoekaanvraag afgewezen; opnieuw proberen...\",\n\t\"u_ehsinit\": \"Server heeft het verzoek om het uploaden te starten afgewezen; opnieuw proberen...\",\n\t\"u_eneths\": \"Netwerkfout tijdens het uitvoeren van de uploadhanddruk; opnieuw proberen...\",\n\t\"u_enethd\": \"Netwerkfout tijdens het testen van het bestaan van het doel; opnieuw proberen...\",\n\t\"u_cbusy\": \"Wachten tot de server ons weer vertrouwt na een netwerkstoring...\",\n\t\"u_ehsdf\": \"Server heeft geen schijfruimte meer!\\n\\nzal blijven proberen, voor het geval iemand genoeg ruimte vrijmaakt om door te gaan\",\n\t\"u_emtleak1\": \"Het lijkt erop dat uw webbrowser een geheugenlek heeft;\\nprobeer\",\n\t\"u_emtleak2\": ' <a href=\"{0}\">over te schakel over naar https (aanbevolen)</a> of ',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": 'Probeer het volgende:\\n<ul><li>druk op <code>F5</code> om de pagina te verversen</li><li>dan schakel de &nbsp;<code>mt</code>&nbsp; uit, deze knop staat in &nbsp;<code>⚙️ instellingen</code></li><li>en probeer de upload opnieuw</li></ul>Uploaden zal wat langzamer gaan, maar ja.\\nSorry voor de problemen!\\n\\nPS: chrome v107 <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">heeft een bugfix</a> voor dit',\n\t\"u_emtleakf\": '{robeer het volgende:\\n<ul><li>druk op <code>F5</code> om de pagina te verversen</li><li>dan activeer <code>🥔</code> (aardappel) in de upload scherm<li>en probeer de upload opnieuw</li></ul>\\nPS: firefox <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">heeft mogelijk een fix</a> op een gegeven moment',\n\t\"u_s404\": \"Niet gevonden op server\",\n\t\"u_expl\": \"Leg uit\",\n\t\"u_maxconn\": \"De meeste browsers beperken dit tot 6, maar firefox laat je dit verhogen met <code>network.http.max-persistent-connections-per-server</code> in <code>about:config</code>\",\n\t\"u_tu\": '<p class=\"warn\">WAARSCHUWING: turbo ingeschakeld, <span>&nbsp;webbrowser detecteert en hervat onvolledige uploads mogelijk niet; zie de tooltip van de turboknop</span></p>',\n\t\"u_ts\": '<p class=\"warn\">WAARSCHUWING: turbo ingeschakeld, <span>&nbsp;zoekresultaten kunnen onjuist zijn; zie turbo-knop tooltip</span></p>',\n\t\"u_turbo_c\": \"Turbo is uitgeschakeld in serverconfiguratie\",\n\t\"u_turbo_g\": \"Turbo uitgeschakeld, je geen recht om mappen in deze volume te tonen\",\n\t\"u_life_cfg\": 'Automatisch verwijderen na <input id=\"lifem\" p=\"60\" /> minuten (of <input id=\"lifeh\" p=\"3600\" /> uur)',\n\t\"u_life_est\": 'Upload wordt verwijderd <span id=\"lifew\" tt=\"local time\">---</span>',\n\t\"u_life_max\": 'Deze map dwingt een\\nmaximale levensduur van {0} af',\n\t\"u_unp_ok\": 'unpost is toegestaan voor {0}',\n\t\"u_unp_ng\": 'unpost zijn NIET toegestaan',\n\t\"ue_ro\": 'Je toegang tot deze map is alleen-lezen\\n\\n',\n\t\"ue_nl\": 'Je bent momenteel niet ingelogd',\n\t\"ue_la\": 'Je bent momenteel aangemeld als \"{0}\"',\n\t\"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',\n\t\"ue_ta\": 'Probeer opnieuw te uploaden, het zou nu moeten werken',\n\t\"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 🧯\",\n\t\"ur_1uo\": \"OK: Bestand succesvol geüpload\",\n\t\"ur_auo\": \"OK: Alle {0} bestanden succesvol geüpload\",\n\t\"ur_1so\": \"OK: Bestand gevonden op server\",\n\t\"ur_aso\": \"OK: Alle {0} bestanden gevonden op server\",\n\t\"ur_1un\": \"Uploaden mislukt, sorry\",\n\t\"ur_aun\": \"Alle {0} uploads mislukt, sorry\",\n\t\"ur_1sn\": \"Bestand NIET gevonden op server\",\n\t\"ur_asn\": \"De {0} bestanden zijn NIET gevonden op de server\",\n\t\"ur_um\": \"Voltooid;\\n{0} upload(s) OK,\\n{1} upload(s) mislukt, sorry\",\n\t\"ur_sm\": \"Voltooid;\\n{0} bestand(en) gevonden op de server,\\n{1} bestand(en) NIET gevonden op de server\",\n\n\t\"rc_opn\": \"Openen\", //m\n\t\"rc_ply\": \"Afspelen\", //m\n\t\"rc_pla\": \"Afspelen als audio\", //m\n\t\"rc_txt\": \"Openen in bestandsviewer\", //m\n\t\"rc_md\": \"Openen in teksteditor\", //m\n\t\"rc_dl\": \"Downloaden\", //m\n\t\"rc_zip\": \"Downloaden als archief\", //m\n\t\"rc_cpl\": \"Link kopiëren\", //m\n\t\"rc_del\": \"Verwijderen\", //m\n\t\"rc_cut\": \"Knippen\", //m\n\t\"rc_cpy\": \"Kopiëren\", //m\n\t\"rc_pst\": \"Plakken\", //m\n\t\"rc_rnm\": \"hernoemen\", //m\n\t\"rc_nfo\": \"Nieuwe map\", //m\n\t\"rc_nfi\": \"Nieuw bestand\", //m\n\t\"rc_sal\": \"Alles selecteren\", //m\n\t\"rc_sin\": \"Selectie omkeren\", //m\n\t\"rc_shf\": \"deel deze map\", //m\n\t\"rc_shs\": \"deel selectie\", //m\n\n\t\"lang_set\": \"Vernieuw de pagina om de wijziging door te voeren?\",\n\n\t\"splash\": {\n\t\t\"a1\": \"Update\",\n\t\t\"b1\": \"Hallo, hoe gaat het met jou? &nbsp; <small>(Je bent niet ingelogd)</small>\",\n\t\t\"c1\": \"Uitloggen\",\n\t\t\"d1\": \"Voorwaarde\",\n\t\t\"d2\": \"Toont de status van alle actieve threads\",\n\t\t\"e1\": \"Configuratie opnieuw laden.\",\n\t\t\"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\",\n\t\t\"f1\": \"Je kan het volgende lezen:\",\n\t\t\"g1\": \"Je kan naar het volgende uploaden:\",\n\t\t\"cc1\": \"Schakelaars en dergelijke:\",\n\t\t\"h1\": \"k304 uitschakelen\",\n\t\t\"i1\": \"k304 inschakelen\",\n\t\t\"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\",\n\t\t\"k1\": \"Instellingen resetten\",\n\t\t\"l1\": \"Inloggen:\",\n\t\t\"ls3\": \"inloggen\", //m\n\t\t\"lu4\": \"gebruikersnaam\", //m\n\t\t\"lp4\": \"wachtwoord\", //m\n\t\t\"lo3\": \"“{0}” overal afmelden\", //m\n\t\t\"lo2\": \"dit zal de sessie in alle browsers beëindigen\", //m\n\t\t\"m1\": \"Welkom terug,\",\n\t\t\"n1\": \"404: bestand bestaat niet &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": 'of misschien heb je geen toegang? probeer een wachtwoord of <a href=\"' + SR + '/?h\">ga naar startscherm</a>',\n\t\t\"p1\": \"403: toegang geweigerd &nbsp;~┻━┻\",\n\t\t\"q1\": 'Probeer een wachtwoord of <a href=\"' + SR + '/?h\">ga naar startscherm</a>',\n\t\t\"r1\": \"Ga naar startscherm\",\n\t\t\".s1\": \"Kaart\",\n\t\t\"t1\": \"Actie\",\n\t\t\"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\",\n\t\t\"v1\": \"Verbinden\",\n\t\t\"v2\": \"Gebruik deze server als een lokale harde schijf\",\n\t\t\"w1\": \"Overschakelen naar https\",\n\t\t\"x1\": \"Wachtwoord wijzigen\",\n\t\t\"y1\": \"Jou gedeelde items\",\n\t\t\"z1\": \"Ontgrendel gebied:\",\n\t\t\"ta1\": \"Je moet eerst een nieuw wachtwoord invoeren\",\n\t\t\"ta2\": \"Herhaal om nieuw wachtwoord te bevestigen:\",\n\t\t\"ta3\": \"Typefout gevonden; probeer het opnieuw\",\n\t\t\"nop\": \"FOUT: Wachtwoord mag niet leeg zijn\", //m\n\t\t\"nou\": \"FOUT: Gebruikersnaam en/of wachtwoord mag niet leeg zijn\", //m\n\t\t\"aa1\": \"Inkomend:\",\n\t\t\"ab1\": \"Schakel nr. 304 uit\",\n\t\t\"ac1\": \"Schakel nr. 304 in\",\n\t\t\"ad1\": \"Nr. 304 stopt al het cachegebruik. Als k304 niet voldoende was, probeer dan deze. Vermenigvuldigt het dataverbruik.!\",\n\t\t\"ae1\": \"Uitgaand:\",\n\t\t\"af1\": \"Recent geüploade bestanden weergeven\",\n\t\t\"ag1\": \"Bekende IdP-gebruikers weergeven\",\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/nno.js",
    "content": "Ls.nno = {\n\t\"tt\": \"Nynorsk\",\n\n\t\"cols\": {\n\t\t\"c\": \"handlingsknappar\",\n\t\t\"dur\": \"varigheit\",\n\t\t\"q\": \"kvalitet / bitrate\",\n\t\t\"Ac\": \"lydformat\",\n\t\t\"Vc\": \"videoformat\",\n\t\t\"Fmt\": \"format / innpakning\",\n\t\t\"Ahash\": \"lydkontrollsum\",\n\t\t\"Vhash\": \"videokontrollsum\",\n\t\t\"Res\": \"oppløysing\",\n\t\t\"T\": \"filtype\",\n\t\t\"aq\": \"lydkvalitet / bitrate\",\n\t\t\"vq\": \"videokvalitet / bitrate\",\n\t\t\"pixfmt\": \"fargekoding / detaljnivå\",\n\t\t\"resw\": \"horisontal oppløysing\",\n\t\t\"resh\": \"vertikal oppløysing\",\n\t\t\"chs\": \"lydkanaler\",\n\t\t\"hz\": \"lydoppløsing\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"ymse\",\n\t\t\t[\"ESC\", \"lukk saker og ting\"],\n\n\t\t\t\"filbehandlar\",\n\t\t\t[\"G\", \"listevisning eller ikon\"],\n\t\t\t[\"T\", \"miniatyrbilder på/av\"],\n\t\t\t[\"⇧ A/D\", \"ikonstorleik\"],\n\t\t\t[\"ctrl-K\", \"slett valde\"],\n\t\t\t[\"ctrl-X\", \"klipp ut valde\"],\n\t\t\t[\"ctrl-C\", \"kopiér åt utklippstavle\"],\n\t\t\t[\"ctrl-V\", \"lim inn (flytt/kopiér)\"],\n\t\t\t[\"Y\", \"last ned valde\"],\n\t\t\t[\"F2\", \"endre namn på valde\"],\n\n\t\t\t\"filmarkering\",\n\t\t\t[\"space\", \"markér fil\"],\n\t\t\t[\"↑/↓\", \"flytt markør\"],\n\t\t\t[\"ctrl ↑/↓\", \"flytt markør og scroll\"],\n\t\t\t[\"⇧ ↑/↓\", \"velg forr./neste fil\"],\n\t\t\t[\"ctrl-A\", \"velg alle filer / mapper\"],\n\t\t], [\n\t\t\t\"navigering\",\n\t\t\t[\"B\", \"mappehierarki eller filsti\"],\n\t\t\t[\"I/K\", \"forr./neste mappe\"],\n\t\t\t[\"M\", \"eitt nivå opp (eller lukk)\"],\n\t\t\t[\"V\", \"vis mapper eller tekstfiler\"],\n\t\t\t[\"A/D\", \"panelstorleik\"],\n\t\t], [\n\t\t\t\"musikkspelar\",\n\t\t\t[\"J/L\", \"forr./neste song\"],\n\t\t\t[\"U/O\", \"hopp 10sek bak/fram\"],\n\t\t\t[\"0..9\", \"hopp åt 0%..90%\"],\n\t\t\t[\"P\", \"pause, eller start / fortsett\"],\n\t\t\t[\"S\", \"marker spelande song\"],\n\t\t\t[\"Y\", \"last ned song\"],\n\t\t], [\n\t\t\t\"bildevisar\",\n\t\t\t[\"J/L, ←/→\", \"forr./neste bilde\"],\n\t\t\t[\"Home/End\", \"første/siste bilde\"],\n\t\t\t[\"F\", \"fullskjermvisning\"],\n\t\t\t[\"R\", \"rotér åt høyre\"],\n\t\t\t[\"⇧ R\", \"rotér åt venstre\"],\n\t\t\t[\"S\", \"markér bilde\"],\n\t\t\t[\"Y\", \"last ned bilde\"],\n\t\t], [\n\t\t\t\"videospelar\",\n\t\t\t[\"U/O\", \"hopp 10sek bak/fram\"],\n\t\t\t[\"P/K/Space\", \"pause / fortsett\"],\n\t\t\t[\"C\", \"fortsett åt neste fil\"],\n\t\t\t[\"V\", \"gjenta avspeling\"],\n\t\t\t[\"M\", \"lyd av/på\"],\n\t\t\t[\"[ og ]\", \"gjentaksintervall\"],\n\t\t], [\n\t\t\t\"dokumentvisar\",\n\t\t\t[\"I/K\", \"forr./neste fil\"],\n\t\t\t[\"M\", \"lukk tekstdokument\"],\n\t\t\t[\"E\", \"redigér tekstdokument\"],\n\t\t\t[\"S\", \"markér fil (for F2/ctrl-x/...)\"],\n\t\t\t[\"Y\", \"last ned tekstfil\"],\n\t\t\t[\"⇧ J\", \"formattér json\"],\n\t\t]\n\t],\n\n\t\"m_ok\": \"OK\",\n\t\"m_ng\": \"Avbryt\",\n\n\t\"enable\": \"Aktiv\",\n\t\"danger\": \"VARSKU\",\n\t\"clipped\": \"kopiert åt utklippstavla\",\n\n\t\"ht_s1\": \"sekund\",\n\t\"ht_s2\": \"sekund\",\n\t\"ht_m1\": \"minutt\",\n\t\"ht_m2\": \"minutt\",\n\t\"ht_h1\": \"time\",\n\t\"ht_h2\": \"timar\",\n\t\"ht_d1\": \"dag\",\n\t\"ht_d2\": \"dagar\",\n\t\"ht_and\": \" og \",\n\n\t\"goh\": \"kontrollpanel\",\n\t\"gop\": 'navigér åt mappa før den her\">forr.',\n\t\"gou\": 'navigér eitt nivå opp\">opp',\n\t\"gon\": 'navigér åt mappa etter den her\">neste',\n\t\"logout\": \"Logg ut \",\n\t\"login\": \"Logg inn\",\n\t\"access\": \" åtgang\",\n\t\"ot_close\": \"lukk reiskap\",\n\t\"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`&quot;try unite&quot;` = «try unite» eksakt$N$Ndatoformat er iso-8601, så f.eks.$N`2009-12-31` eller `2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"unpost: slett filer som du nyleg har lastet opp; «angre-knappen»\",\n\t\"ot_bup\": \"bup: tradisjonell / primitiv filopplasting,$N$Nfungerar i om lag samtlege nettlesarar\",\n\t\"ot_mkdir\": \"mkdir: lag ei ny mappe\",\n\t\"ot_md\": \"new-file: lag ein ny tekstfil\",\n\t\"ot_msg\": \"msg: send ein beskjed åt serverloggen\",\n\t\"ot_mp\": \"musikkspelarinstillinger\",\n\t\"ot_cfg\": \"andre innstillinger\",\n\t\"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 [🎈]&nbsp; (den primitive opplastaren \"bup\")<br /><br />mens opplastinger føregår så visast framdrifta her oppe!',\n\t\"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 [🎈]&nbsp; (den primitive opplastaren \"bup\")<br /><br />mens opplastinger føregår så visast framdrifta her oppe!',\n\t\"ot_noie\": 'Fungerer mye betre i Chrome / Firefox / Edge',\n\n\t\"ab_mkdir\": \"lag mappe\",\n\t\"ab_mkdoc\": \"ny tekstfil\",\n\t\"ab_msg\": \"send melding\",\n\n\t\"ay_path\": \"gå videre åt mapper\",\n\t\"ay_files\": \"gå videre åt filer\",\n\n\t\"wt_ren\": \"gje nye namn åt dei valde filene$NSnarvei: F2\",\n\t\"wt_del\": \"slett dei valde filene$NSnarvei: ctrl-K\",\n\t\"wt_cut\": \"klipp ut dei valde filene &lt;small&gt;(for å lime inn ein annan plass)&lt;/small&gt;$NSnarvei: ctrl-X\",\n\t\"wt_cpy\": \"kopiér dei valde filene åt utklippstavla$N(for å lime inn ein annan plass)$NSnarvei: ctrl-C\",\n\t\"wt_pst\": \"lim inn filer (som tidligare blei klipt ut / kopiert ein annan plass)$NSnarvei: ctrl-V\",\n\t\"wt_selall\": \"velg alle filer$NSnarvei: ctrl-A (mens fokus er på ei fil)\",\n\t\"wt_selinv\": \"invertér utval\",\n\t\"wt_zip1\": \"last ned denne mappa som eit arkiv\",\n\t\"wt_selzip\": \"last ned dei valde filene som eit arkiv\",\n\t\"wt_seldl\": \"last ned dei valde filene$NSnarvei: Y\",\n\t\"wt_npirc\": \"kopiér songinfo (irc-formatert)\",\n\t\"wt_nptxt\": \"kopiér songinfo\",\n\t\"wt_m3ua\": \"legg song åt i m3u-speleliste$N(husk å klikk på <code>📻copy</code> senere)\",\n\t\"wt_m3uc\": \"kopiér m3u-spelelista åt utklippstavla\",\n\t\"wt_grid\": \"bytt mellom ikon og listevising$NSnarvei: G\",\n\t\"wt_prev\": \"førre  song$NSnarvei: J\",\n\t\"wt_play\": \"play / pause$NSnarvei: P\",\n\t\"wt_next\": \"neste song$NSnarvei: L\",\n\n\t\"ul_par\": \"samtidige handl.:\",\n\t\"ut_rand\": \"finn opp nye tilfeldige filnamn\",\n\t\"ut_u2ts\": \"gje fila på serveren same$Ntidsstempel som lokalt hos deg\\\">📅\",\n\t\"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\",\n\t\"ut_mt\": \"fortsett å synfare køa mens opplasting føregår$N$Nskru denne av dersom du har ein$Ntreig prosessor eller harddisk\",\n\t\"ut_ask\": 'bekreft filutvalg før opplasting startar\">💭',\n\t\"ut_pot\": \"forbetre ytinga på treige einheiter ved å$Nforenkle brukergrensesnittet\",\n\t\"ut_srch\": \"gjer eit søk i staden for å laste opp --$Nleitar gjennom alle mappane du har lov åt å sjå\",\n\t\"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\",\n\t\"ul_btn\": \"slepp filer / mapper<br>her (eller klikk meg)\",\n\t\"ul_btnu\": \"L A S T &nbsp; O P P\",\n\t\"ul_btns\": \"F I L S Ø K\",\n\n\t\"ul_hash\": \"synfar\",\n\t\"ul_send\": \"&nbsp;send\",\n\t\"ul_done\": \"total\",\n\t\"ul_idle1\": \"ingen handlinger i køen\",\n\t\"ut_etah\": \"snitthastigheit for &lt;em&gt;synfaring&lt;/em&gt; samt gjenståande tid\",\n\t\"ut_etau\": \"snitthastigheit for &lt;em&gt;opplasting&lt;/em&gt; samt gjenståande tid\",\n\t\"ut_etat\": \"&lt;em&gt;total&lt;/em&gt; snitthastigheit og gjenståande tid\",\n\n\t\"uct_ok\": \"fullført uten problem\",\n\t\"uct_ng\": \"fullført under tvil (duplikat, ikkje funne, ...)\",\n\t\"uct_done\": \"fullført (enten &lt;em&gt;ok&lt;/em&gt; eller &lt;em&gt;ng&lt;/em&gt;)\",\n\t\"uct_bz\": \"aktive handlinger (synfaring / opplasting)\",\n\t\"uct_q\": \"køa\",\n\n\t\"utl_name\": \"filnamn\",\n\t\"utl_ulist\": \"vis\",\n\t\"utl_ucopy\": \"kopiér\",\n\t\"utl_links\": \"lenker\",\n\t\"utl_stat\": \"status\",\n\t\"utl_prog\": \"fremdrift\",\n\n\t// må vere korte:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"FEIL!\",\n\t\"utl_oserr\": \"OS-feil\",\n\t\"utl_found\": \"funnet\",\n\t\"utl_defer\": \"seinare\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"ferdig\",\n\n\t\"ul_flagblk\": \"filene har blitt lagd i køa</b><br>men det er ein anna nettlesarfane som held på med synfaring eller opplasting akkurat no,<br>så venter åt den er ferdig først\",\n\t\"ul_btnlk\": \"brytaren har blitt låst åt denne tilstanden i serverens konfigurasjon\",\n\n\t\"udt_up\": \"Last opp\",\n\t\"udt_srch\": \"Søk\",\n\t\"udt_drop\": \"Slepp filene her\",\n\n\t\"u_nav_m\": '<h6>kva har du?</h6><code>Enter</code> = Filer (éin eller fleire)\\n<code>ESC</code> = Éi mappe (inkludert undermapper)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Filer</a><a href=\"#\" id=\"modal-ng\">Éi mappe</a>',\n\n\t\"cl_opts\": \"brytarar\",\n\t\"cl_hfsz\": \"filstorleik\",\n\t\"cl_themes\": \"utsjånad\",\n\t\"cl_langs\": \"språk\",\n\t\"cl_ziptype\": \"nedlasting av mapper\",\n\t\"cl_uopts\": \"up2k-brytarar\",\n\t\"cl_favico\": \"favicon\",\n\t\"cl_bigdir\": \"store mapper\",\n\t\"cl_hsort\": \"#sort\",\n\t\"cl_keytype\": \"notasjon for musikalsk dur\",\n\t\"cl_hiddenc\": \"skjulte kolonner\",\n\t\"cl_hidec\": \"skjul\",\n\t\"cl_reset\": \"nullstill\",\n\t\"cl_hpick\": \"klikk på overskrifta åt kolonnene du ønskjer å skjule i tabellen nedanfor\",\n\t\"cl_hcancel\": \"kolonne-skjuling avbrote\",\n\t\"cl_rcm\": \"høgreklikkmeny\",\n\n\t\"ct_grid\": '田 ikon',\n\t\"ct_ttips\": 'vis hjelpetekst ved å holde musa over ting\">ℹ️ tips',\n\t\"ct_thumb\": 'vis miniatyrbilder i staden for ikon$NSnarvei: T\">🖼️ bilder',\n\t\"ct_csel\": 'bruk tastane CTRL og SHIFT for markering av filer i ikonvising\">merk',\n\t\"ct_dsel\": 'klikk-og-dra for å merke filer i ikonvising\">dra',\n\t\"ct_dl\": 'last ned filer (ikkje vis i nettleseren)\">dl',\n\t\"ct_ihop\": 'bla ned åt sist viste bilde når bildevisaren lukkast\">g⮯',\n\t\"ct_dots\": 'vis skjulte filer (gitt at serveren tillèt det)\">.synlig',\n\t\"ct_qdel\": 'sletteknappen spør berre éin gong om stadfesting\">hurtig🗑️',\n\t\"ct_dir1st\": 'sortér slik at mapper kjem framanfor filer\">📁 først',\n\t\"ct_nsort\": 'naturlig sortering (skjønar tal i filnamn)\">nsort',\n\t\"ct_utc\": 'bruk UTC for alle klokkeslett\">UTC',\n\t\"ct_readme\": 'vis README.md nedanfor filene\">📜 readme',\n\t\"ct_idxh\": 'vis index.html i staden for filliste\">htm',\n\t\"ct_sbars\": 'vis rullgardiner / skrollefelt\">⟊',\n\n\t\"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📅',\n\n\t\"cut_turbo\": \"forenkla synfaring ved opplasting; bør etter alt å døme <em>ikkje</em> 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 <em>filstorleiken</em> stemmer. Så dersom ein korrupt fil vere på serveren allerede, på same plass, med same storleik og namn, så blir det <em>ikkje oppdaga</em>.$N$Ndet anbefalast å kun benytte denne funksjonen for å komme seg raskt gjennom sjølve opplastinga, for så å skru den av, og åt slutt &quot;laste opp&quot; dei same filene éin gong åt -- slik at integriteten kan verifiserast\\\">turbo\",\n\n\t\"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$N<em>burde</em> oppdage og gjenoppta dei fleste ufullstendige opplastinger, men er <em>ikkje</em> ein fullverdig erstatning for å deaktivere turbo og gjere ein skikkeleg sjekk\\\">date-chk\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cut_nag\": \"meldingsvarsel når opplasting er ferdig$N(kun om nettlesarfana ikkje er synlig)\",\n\t\"cut_sfx\": \"lydvarsel når opplasting er ferdig$N(kun om nettlesarfanen ikkje er synlig)\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cft_text\": \"ikontekst (blank ut og last siden på nytt for å deaktivere)\",\n\t\"cft_fg\": \"farge\",\n\t\"cft_bg\": \"bakgrunnsfarge\",\n\n\t\"cdt_lim\": \"maks mengd filer å vise per mappe\",\n\t\"cdt_ask\": \"vis knappar for å laste fleire filer nederst på sida i staden for å gradvis laste meir av mappea når man scroller ned\",\n\t\"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\",\n\t\"cdt_ren\": \"slå på tilpassa høgreklikkmeny (den vanlege menyen er tilgjengeleg med shift + høgreklikk)\\\">aktiv\",\n\t\"cdt_rdb\": \"høgreklikk to gonger for å vise den vanlege høgreklikkmenyen\\\">x2\",\n\n\t\"tt_entree\": \"bytt åt mappehierarki$NSnarvei: B\",\n\t\"tt_detree\": \"bytt åt tradisjonell stivising$NSnarvei: B\",\n\t\"tt_visdir\": \"bla ned åt den åpne mappa\",\n\t\"tt_ftree\": \"bytt mellom filstruktur og tekstfiler$NSnarvei: V\",\n\t\"tt_pdock\": \"vis dei overordna mappane i eit panel\",\n\t\"tt_dynt\": \"øk bredda på panelet ettersom treet utvider seg\",\n\t\"tt_wrap\": \"linjebryting\",\n\t\"tt_hover\": \"vis heile mappenamnet når musepeikaren treff mappa$N( gjer diverre at scrollhjulet fusker dersom musepeikaren ikkje finn seg i grøfta )\",\n\n\t\"ml_pmode\": \"ved enden av mappa\",\n\t\"ml_btns\": \"knapper\",\n\t\"ml_tcode\": \"konvertering\",\n\t\"ml_tcode2\": \"konvertér til\",\n\t\"ml_tint\": \"tint\",\n\t\"ml_eq\": \"audio equalizer (tonejustering)\",\n\t\"ml_drc\": \"compressor (volumutjevning)\",\n\t\"ml_ss\": \"spol forbi stillheit\",\n\n\t\"mt_loop\": \"spel den same songen om og om igjen\\\">🔁\",\n\t\"mt_one\": \"spel kun éin song\\\">1️⃣\",\n\t\"mt_shuf\": \"songane i kvar mappe$Nspelast i tilfeldig rekkefølge\\\">🔀\",\n\t\"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▶\",\n\t\"mt_preload\": \"hent ned litt av neste song i forkant,$Nslik at pausa i overgangen blir mindre\\\">forsyn\",\n\t\"mt_prescan\": \"ved behov, bla åt neste mappe$Nslik at nettlesaren lar oss$Nfortsetja å spele musikk\\\">bla\",\n\t\"mt_fullpre\": \"hent ned heile neste song, ikkje berre litt:$N✅ skru på viss nettet ditt er <b>ustabilt</b>,$N❌ skru av viss nettet ditt er <b>treigt</b>\\\">full\",\n\t\"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\\\">☕️\",\n\t\"mt_waves\": \"waveform seekbar:$Nvis volumkurve i avspelingsfeltet\\\">~s\",\n\t\"mt_npclip\": \"vis knappar for å kopiere info om songen du høyrer på\\\">/np\",\n\t\"mt_m3u_c\": \"vis knapper for å kopiere dei valde$Nsongene som innslag i ei m3u8-speleliste\\\">📻\",\n\t\"mt_octl\": \"integrering med operativsystemet (fjernkontroll, infoskjerm)\\\">os-ctl\",\n\t\"mt_oseek\": \"gje løyve åt spoling med fjernkontroll$N$Nmerk: på nokon eininger (iPhones) så vil$Ndette erstatte knappen for neste song\\\">spoling\",\n\t\"mt_oscv\": \"vis albumcover på infoskjermen\\\">bilde\",\n\t\"mt_follow\": \"bla slik at songen som spelast alltid er synleg\\\">🎯\",\n\t\"mt_compact\": \"tettpakka spelarpanel\\\">⟎\",\n\t\"mt_uncache\": \"prøv denne viss ein song ikkje spelar riktig\\\">oppfrisk\",\n\t\"mt_mloop\": \"repetér heile mappa\\\">🔁 gjenta\",\n\t\"mt_mnext\": \"hopp åt neste mappe og fortsett\\\">📂 neste\",\n\t\"mt_mstop\": \"stopp avspeling\\\">⏸ stopp\",\n\t\"mt_cflac\": \"konvertér flac / wav-filer åt {0}\\\">flac\",\n\t\"mt_caac\": \"konvertér aac / m4a-filer åt to {0}\\\">aac\",\n\t\"mt_coth\": \"konvertér alt anna (men ikkje mp3) åt {0}\\\">andre\",\n\t\"mt_c2opus\": \"det beste valget for alle PCar og Android\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, for iOS 17.5 og nyare\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, for iOS 11 åt og med 17\\\">caf\",\n\t\"mt_c2mp3\": \"bra valg for steinalder-utstyr (slår aldri feil)\\\">mp3\",\n\t\"mt_c2flac\": \"gir best lydkvalitet, men et nettet ditt\\\">flac\",\n\t\"mt_c2wav\": \"heilt rå lydstrøm (bruker enda meir data enn flac)\\\">wav\",\n\t\"mt_c2ok\": \"bra valg!\",\n\t\"mt_c2nd\": \"ikkje det føretrekte valget for din einheit, men funker sikkert greit\",\n\t\"mt_c2ng\": \"ser verkelig ikkje ut som enheiten din taklar dette formatet... men ok, vi prøver\",\n\t\"mt_xowa\": \"iOS har fortsatt problem med avspeling av owa-musikk i bakgrunnen. Bruk caf eller mp3 i staden for\",\n\t\"mt_tint\": \"nivå av bakgrunnsfarge på søkestripa (0-100),$Ngjer oppdateringer mindre distraherande\",\n\t\"mt_eq\": \"`aktivér tonekontroll og forsterker;$N$Nboost `0` = normal volumskala$N$Nwidth `1 &nbsp;` = normal stereo$Nwidth `0.5` = 50% blanding venstre-høgre$Nwidth `0 &nbsp;` = mono$N$Nboost `-0.8` &amp; width `10` = instrumental :^)$N$Nreduserer óg daudtid mellom songfiler\",\n\t\"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\",\n\t\"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\",\n\t\"mt_ssvt\": \"volumterskel (0-255)\\\">volum\",\n\t\"mt_ssts\": \"aktiv innanfor første % av songen\\\">start\",\n\t\"mt_sste\": \"aktiv innanfor siste % av songen\\\">slutt\",\n\t\"mt_sssm\": \"avspelingshastigheitsmultiplikator\\\">ffwd\",\n\n\t\"mb_play\": \"lytt\",\n\t\"mm_hashplay\": \"spel denne songen?\",\n\t\"mm_m3u\": \"trykk <code>Enter/OK</code> for å spele\\ntrykk <code>ESC/Avbryt</code> for å redigere\",\n\t\"mp_breq\": \"krev firefox 82+, chrome 73+, eller iOS 15+\",\n\t\"mm_bload\": \"lastar inn...\",\n\t\"mm_bconv\": \"konverterer åt {0}, vent litt...\",\n\t\"mm_opusen\": \"nettlesaren din skjønar ikkje aac / m4a;\\nkonvertering åt opus er no aktivert\",\n\t\"mm_playerr\": \"avspeling feilet: \",\n\t\"mm_eabrt\": \"Avspelingsforespørselen blei avbroten\",\n\t\"mm_enet\": \"Nettet ditt er ustabilt\",\n\t\"mm_edec\": \"Noko er galt med musikkfila\",\n\t\"mm_esupp\": \"Nettleseren din skjønar ikkje filtypen\",\n\t\"mm_eunk\": \"Ukjent feil\",\n\t\"mm_e404\": \"Avspeling feilet: Fil ikkje funnet.\",\n\t\"mm_e403\": \"Avspeling feilet: Høve nekta.\\n\\nKanskje du blei logget ut?\\nPrøv å trykk F5 for å laste sida på nytt.\",\n\t\"mm_e415\": \"Avspeling feilet: Kunne ikkje konvertere fila, sjekk serverloggen.\",\n\t\"mm_e500\": \"Avspeling feilet: Rusk i maskineriet, sjekk serverloggen.\",\n\t\"mm_e5xx\": \"Avspeling feilet: \",\n\t\"mm_nof\": \"finn ikkje flere songer i nærheita\",\n\t\"mm_prescan\": \"Leitar etter neste song...\",\n\t\"mm_scank\": \"Fann neste song:\",\n\t\"mm_uncache\": \"alle songer vil lastast på nytt ved neste avspeling\",\n\t\"mm_hnf\": \"songen finnast ikkje lenger\",\n\n\t\"im_hnf\": \"bildet finnast ikkje lenger\",\n\n\t\"f_empty\": 'denne mappa er tom',\n\t\"f_chide\": 'dette vil skjule kolonna «{0}»\\n\\nfana for \"andre innstillinger\" let deg vise kolonna igjen',\n\t\"f_bigtxt\": \"denne fila er heeile {0} MiB -- vis som tekst?\",\n\t\"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\",\n\t\"fbd_more\": '<div id=\"blazy\">visar <code>{0}</code> av <code>{1}</code> filer; <a href=\"#\" id=\"bd_more\">vis {2}</a> eller <a href=\"#\" id=\"bd_all\">vis alle</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\">visar <code>{0}</code> av <code>{1}</code> filer; <a href=\"#\" id=\"bd_all\">vis alle</a></div>',\n\t\"f_anota\": \"kun {0} av totalt {1} element blei markert;\\nfor å velje alt må du bla åt bunnen av mappa først\",\n\n\t\"f_dls\": 'lenkane i denne mappa er no\\nomgjort åt nedlastingsknappar',\n\t\"f_dl_nd\": 'hoppar over mappe (bruk zip/tar-nedlasting i staden):\\n',\n\n\t\"f_partial\": \"For å laste ned ei fil som enda ikkje er ferdig opplasta, klikk på filen som har same filnamn som denne, men uten <code>.PARTIAL</code> 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 <code>.PARTIAL</code>-filen på ein ukontrollert måte, trykk OK / Enter for å ignorere denne advarselen. Slik vil du høgst sannsynleg motta korrupt data.\",\n\n\t\"ft_paste\": \"Lim inn {0} filer$NSnarvei: ctrl-V\",\n\t\"fr_eperm\": 'kan ikkje endre namn:\\ndu har ikkje høve åt “move” i denne mappa',\n\t\"fd_eperm\": 'kan ikkje slette:\\ndu har ikkje høve åt “delete” i denne mappa',\n\t\"fc_eperm\": 'kan ikkje klippe ut:\\ndu har ikkje høve åt “move” i denne mappa',\n\t\"fp_eperm\": 'kan ikkje lime inn:\\ndu har ikkje høve åt “write” i denne mappa',\n\t\"fr_emore\": \"vel minst éi fil som skal få nytt namn\",\n\t\"fd_emore\": \"vel minst éi fil som skal slettast\",\n\t\"fc_emore\": \"vel minst éi fil som skal klippast ut\",\n\t\"fcp_emore\": \"vel minst éi fil som skal kopierast åt utklippstavla\",\n\n\t\"fs_sc\": \"del mappa du er i no\",\n\t\"fs_ss\": \"del dei valde filene\",\n\t\"fs_just1d\": \"du kan ikkje markere flere mapper samtidig,\\neller kombinere mapper og filer\",\n\t\"fs_abrt\": \"❌ avbryt\",\n\t\"fs_rand\": \"🎲 tilfeldig namn\",\n\t\"fs_go\": \"✅ opprett deling\",\n\t\"fs_name\": \"namn\",\n\t\"fs_src\": \"kjelde\",\n\t\"fs_pwd\": \"passord\",\n\t\"fs_exp\": \"varigheit\",\n\t\"fs_tmin\": \"min\",\n\t\"fs_thrs\": \"timar\",\n\t\"fs_tdays\": \"dagar\",\n\t\"fs_never\": \"for evig\",\n\t\"fs_pname\": \"valfri namn (blir litt tilfeldig ellers)\",\n\t\"fs_tsrc\": \"fil/mappe som skal delast\",\n\t\"fs_ppwd\": \"valfri passord\",\n\t\"fs_w8\": \"opprettar deling...\",\n\t\"fs_ok\": \"trykk <code>Enter/OK</code> for å kopiere lenka (for CTRL-V)\\ntrykk <code>ESC/Avbryt</code> for å kun bekrefta\",\n\n\t\"frt_dec\": \"kan korrigere visse ødelagte filnamn\\\">url-decode\",\n\t\"frt_rst\": \"nullstillar endringar (tilbake åt dei originale filnamna)\\\">↺ reset\",\n\t\"frt_abrt\": \"avbryt og lukk dette vindauget\\\">❌ avbryt\",\n\t\"frb_apply\": \"IVERKSETT\",\n\t\"fr_adv\": \"automasjon basert på metadata<br>og / eller mønster (regulære uttrykk)\\\">avansert\",\n\t\"fr_case\": \"versalfølsomme uttrykk\\\">Aa\",\n\t\"fr_win\": \"bytt ut bokstavane <code>&lt;&gt;:&quot;\\\\|?*</code> med$Ntilsvarande som windows ikkje får panikk av\\\">win\",\n\t\"fr_slash\": \"bytt ut bokstaven <code>/</code> slik at den ikkje forårsakar at nye mapper opprettes\\\">ikke /\",\n\t\"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.\",\n\t\"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\",\n\t\"fr_pdel\": \"slett\",\n\t\"fr_pnew\": \"lagre som\",\n\t\"fr_pname\": \"gje innstillingane dine eit namn\",\n\t\"fr_aborted\": \"avbrote\",\n\t\"fr_lold\": \"gamalt namn\",\n\t\"fr_lnew\": \"nytt namn\",\n\t\"fr_tags\": \"metadata for dei valde filene (kun for referanse):\",\n\t\"fr_busy\": \"endrar namn på {0} filer...\\n\\n{1}\",\n\t\"fr_efail\": \"endring av namn feila:\\n\",\n\t\"fr_nchg\": \"{0} av namna blei justert pga. <code>win</code> og/eller <code>ikkje /</code>\\n\\nvil du fortsetja med dei nye namna som blei valde?\",\n\n\t\"fd_ok\": \"sletting OK\",\n\t\"fd_err\": \"sletting feila:\\n\",\n\t\"fd_none\": \"ingenting blei sletta; kanskje avvist av serverkonfigurasjon (xbd)?\",\n\t\"fd_busy\": \"slettar {0} filer...\\n\\n{1}\",\n\t\"fd_warn1\": \"SLETT disse {0} filene?\",\n\t\"fd_warn2\": \"<b>Siste sjanse!</b> Dette kan ikkje angrast. Slett?\",\n\n\t\"fc_ok\": \"klipte ut {0} filer\",\n\t\"fc_warn\": 'klipte ut {0} filer\\n\\nmen: kun <b>denne</b> nettlesarfana har muligheit åt å lime dei inn ein annan plass, siden antallet filer er helt hinsides',\n\n\t\"fcc_ok\": \"kopierte {0} filer åt utklippstavla\",\n\t\"fcc_warn\": 'kopierte {0} filer åt utklippstavla\\n\\nmen: kun <b>denne</b> nettlesarfana har muligheit åt å lime dei inn ein annan plass, sidan antallet filer er heilt på hi sida',\n\n\t\"fp_apply\": \"bekreft og lim inn no\",\n\t\"fp_skip\": \"berre ledige\",\n\t\"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\",\n\t\"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:\",\n\t\"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:\",\n\t\"fp_emore\": \"det er fortsatt fleire namn som må endrast\",\n\t\"fp_ok\": \"flytting OK\",\n\t\"fcp_ok\": \"kopiering OK\",\n\t\"fp_busy\": \"flyttar {0} filer...\\n\\n{1}\",\n\t\"fcp_busy\": \"kopierar {0} filer...\\n\\n{1}\",\n\t\"fp_abrt\": \"avbryt...\",\n\t\"fp_err\": \"flytting feila:\\n\",\n\t\"fcp_err\": \"kopiering feila:\\n\",\n\t\"fp_confirm\": \"flytt disse {0} filene hit?\",\n\t\"fcp_confirm\": \"kopiér disse {0} filene hit?\",\n\t\"fp_etab\": 'kunne ikkje lese lista med filer frå den andre nettlesarfana',\n\t\"fp_name\": \"Lastar opp éi fil frå einheita di. Velg filnamn:\",\n\t\"fp_both_m\": '<h6>kva skal limast inn her?</h6><code>Enter</code> = Flytt {0} filer frå «{1}»\\n<code>ESC</code> = Last opp {2} filer frå einheita din',\n\t\"fcp_both_m\": '<h6>kva skal limes inn her?</h6><code>Enter</code> = Kopiér {0} filer frå «{1}»\\n<code>ESC</code> = Last opp {2} filer frå einheita din',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Flytt</a><a href=\"#\" id=\"modal-ng\">Last opp</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Kopiér</a><a href=\"#\" id=\"modal-ng\">Last opp</a>',\n\n\t\"mk_noname\": \"skriv inn eit namn i tekstboksa åt venstre først :p\",\n\t\"nmd_i1\": \"leggja også til filendinga du vil, til dømes <code>.md</code>\",\n\t\"nmd_i2\": \"du kan berre laga <code>.{0}</code>-filer fordi du ikkje har delete-tilgang\",\n\n\t\"tv_load\": \"Lastar inn tekstfil:\\n\\n{0}\\n\\n{1}% ({2} av {3} MiB lasta ned)\",\n\t\"tv_xe1\": \"kunne ikkje laste tekstfil:\\n\\nfeil \",\n\t\"tv_xe2\": \"404, Fil ikkje funne\",\n\t\"tv_lst\": \"tekstfiler i mappa\",\n\t\"tvt_close\": \"gå tilbake åt mappa$NSnarvei: M (eller Esc)\\\">❌ lukk\",\n\t\"tvt_dl\": \"last ned denne fila$NSnarvei: Y\\\">💾 last ned\",\n\t\"tvt_prev\": \"vis førre dokument$NSnarvei: i\\\">⬆ forr.\",\n\t\"tvt_next\": \"vis neste dokument$NSnarvei: K\\\">⬇ neste\",\n\t\"tvt_sel\": \"markér fila &nbsp; ( for utklipp / sletting / ... )$NSnarvei: S\\\">merk\",\n\t\"tvt_j\": \"formattér json$NSnarvei: shift-J\\\">j\",\n\t\"tvt_edit\": \"redigér fila$NSnarvei: E\\\">✏️ endre\",\n\t\"tvt_tail\": \"overvak fila for endringar og vis nye linjer i sanntid\\\">📡 følg\",\n\t\"tvt_wrap\": \"tekstbryting\\\">↵\",\n\t\"tvt_atail\": \"hald dei nyaste linjene synlege (lås åt botnen av sida)\\\">⚓\",\n\t\"tvt_ctail\": \"skjøn og vis terminalfargar (ansi-sekvensar)\\\">🌈\",\n\t\"tvt_ntail\": \"maksgrense for antal bokstavar som skal visast i vindauget\",\n\n\t\"m3u_add1\": \"songen blei lagd åt i m3u-spelelista\",\n\t\"m3u_addn\": \"{0} songer blei lagde åt i m3u-spelelista\",\n\t\"m3u_clip\": \"m3u-spelelista blei kopiert åt utklippstavla\\n\\nneste steg er å oppretta eit tekstdokument med filnamn som sluttar på <code>.m3u</code> og lime inn spelelista der\",\n\n\t\"gt_vau\": \"ikkje vis videofiler, berre spel lyden\\\">🎧\",\n\t\"gt_msel\": \"markér filer i staden for å åpne dei; ctrl-klikk filer for å overstyre$N$N&lt;em&gt;når aktiv: dobbelklikk ei fil / mappe for å åpne&lt;/em&gt;$N$NSnarvei: S\\\">markering\",\n\t\"gt_crop\": \"skjer ikona slik at dei passar betre\\\">✂\",\n\t\"gt_3x\": \"høgare oppløysing på ikon\\\">3x\",\n\t\"gt_zoom\": \"zoom\",\n\t\"gt_chop\": \"trim\",\n\t\"gt_sort\": \"sortér\",\n\t\"gt_name\": \"namn\",\n\t\"gt_sz\": \"størr.\",\n\t\"gt_ts\": \"dato\",\n\t\"gt_ext\": \"type\",\n\t\"gt_c1\": \"redusér makslengde på filnamn\",\n\t\"gt_c2\": \"auk makslengde på filnamn\",\n\n\t\"sm_w8\": \"søker...\",\n\t\"sm_prev\": \"søkeresultata er frå eit tidlegare søk:\\n  \",\n\t\"sl_close\": \"lukk søkeresultat\",\n\t\"sl_hits\": \"visar {0} treff\",\n\t\"sl_moar\": \"hent fleire\",\n\n\t\"s_sz\": \"størr.\",\n\t\"s_dt\": \"dato\",\n\t\"s_rd\": \"sti\",\n\t\"s_fn\": \"namn\",\n\t\"s_ta\": \"meta\",\n\t\"s_ua\": \"up@\",\n\t\"s_ad\": \"avns.\",\n\t\"s_s1\": \"større enn ↓ MiB\",\n\t\"s_s2\": \"mindre enn ↓ MiB\",\n\t\"s_d1\": \"nyare enn &lt;dato&gt;\",\n\t\"s_d2\": \"eldre enn\",\n\t\"s_u1\": \"lasta opp etter\",\n\t\"s_u2\": \"og/eller før\",\n\t\"s_r1\": \"mappaamn inneheld\",\n\t\"s_f1\": \"filnamn inneheld\",\n\t\"s_t1\": \"song-info inneheld\",\n\t\"s_a1\": \"konkrete eigenskapar\",\n\n\t\"md_eshow\": \"visar forenkla \",\n\t\"md_off\": \"[📜<em>readme</em>] er skrudd av i [⚙️] -- dokument skjult\",\n\n\t\"badreply\": \"Ugyldig svar frå serveren\",\n\n\t\"xhr403\": \"403: Høve nekta\\n\\nkanskje du blei logga ut? prøv å trykk F5\",\n\t\"xhr0\": \"ukjend (enten nettverksproblem eller serverkræsj)\",\n\t\"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\",\n\t\"tl_xe1\": \"kunne ikkje hente undermapper:\\n\\nfeil \",\n\t\"tl_xe2\": \"404: Mappa finnast ikkje\",\n\t\"fl_xe1\": \"kunne ikkje hente filer i mappa:\\n\\nfeil \",\n\t\"fl_xe2\": \"404: Mappa finnast ikkje\",\n\t\"fd_xe1\": \"kan ikkje opprette ny mappe:\\n\\nfeil \",\n\t\"fd_xe2\": \"404: Den overordna mappa finnast ikkje\",\n\t\"fsm_xe1\": \"kunne ikkje sende melding:\\n\\nfeil \",\n\t\"fsm_xe2\": \"404: Den overordna mappa finnast ikkje\",\n\t\"fu_xe1\": \"kunne ikkje hente lista med nyleg opplastede filer frå serveren:\\n\\nfeil \",\n\t\"fu_xe2\": \"404: Fila finnast ikkje??\",\n\n\t\"fz_tar\": \"ukomprimert gnu-tar arkiv, for linux og mac\",\n\t\"fz_pax\": \"ukomprimert pax-tar arkiv, litt treigare\",\n\t\"fz_targz\": \"gnu-tar pakket med gzip (nivå 3)$N$NNB: denne er veldig treig;$Nukomprimert tar er betre\",\n\t\"fz_tarxz\": \"gnu-tar pakket med xz (nivå 1)$N$NNB: denne er veldig treig;$Nukomprimert tar er betre\",\n\t\"fz_zip8\": \"zip med filnamn i utf8 (noko problematisk på windows 7 og eldre)\",\n\t\"fz_zipd\": \"zip med filnamn i cp437, for høggamle maskiner\",\n\t\"fz_zipc\": \"cp437 med tidlig crc32,$Nfor MS-DOS PKZIP v2.04g (oktober 1993)$N(øker behandlingstid på server)\",\n\n\t\"un_m1\": \"nedanfor kan du angre / slette filer som du nyleg har lastet opp, eller avbryte ufullstendige opplastinger\",\n\t\"un_upd\": \"oppdater\",\n\t\"un_m4\": \"eller viss du vil dele nedlastings-lenkene:\",\n\t\"un_ulist\": \"vis\",\n\t\"un_ucopy\": \"kopiér\",\n\t\"un_flt\": \"valgfritt filter:&nbsp; filnamn / filsti må inneholde\",\n\t\"un_fclr\": \"nullstill filter\",\n\t\"un_derr\": 'unpost-sletting feilet:\\n',\n\t\"un_f5\": 'noko gjekk galt, prøv å oppdatere lista eller trykk F5',\n\t\"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\",\n\t\"un_nou\": '<b>advarsel:</b> kan ikkje vise ufullstendige opplastingar akkurat no; klikk på oppdater-lenka om litt',\n\t\"un_noc\": '<b>advarsel:</b> angring av fullførte opplastingar er deaktivert i serverkonfigurasjonen',\n\t\"un_max\": \"visar dei første 2000 filene (bruk filteret for å snevre inn)\",\n\t\"un_avail\": \"{0} nyleg opplasta filer kan slettast<br />{1} ufullstendige opplastingar kan avbrytast\",\n\t\"un_m2\": \"sortert etter opplastingstid; nyaste først:\",\n\t\"un_no1\": \"men nei, her var det jaggu ikkje noko som slettast kan\",\n\t\"un_no2\": \"men nei, her var det jaggu ingenting som passa overens med filteret\",\n\t\"un_next\": \"slett dei neste {0} filene nedanfor\",\n\t\"un_abrt\": \"avbryt\",\n\t\"un_del\": \"slett\",\n\t\"un_m3\": \"hentar lista med nyleg opplasta filer...\",\n\t\"un_busy\": \"slettar {0} filer...\",\n\t\"un_clip\": \"{0} lenkar kopiert åt utklippstavla\",\n\n\t\"u_https1\": \"du burde\",\n\t\"u_https2\": \"bytte åt https\",\n\t\"u_https3\": \"for høgare hastigheit\",\n\t\"u_ancient\": 'nettlesaren din er prehistorisk -- mulig du burde <a href=\"#\" onclick=\"goto(\\'bup\\')\">bruke bup i staden for</a>',\n\t\"u_nowork\": \"krev firefox 53+, chrome 57+, eller iOS 11+\",\n\t\"tail_2old\": \"krev firefox 105+, chrome 71+, eller iOS 14.5+\",\n\t\"u_nodrop\": 'nettlesaren din er for gamal åt å laste opp filer ved å drage dei inn i vindauget',\n\t\"u_notdir\": \"mottok ikkje mappa!\\n\\nnettlesaren din er for gamal,\\nprøv å drage mappa inn i vindauget i staden for\",\n\t\"u_uri\": \"for å laste opp bilder frå andre nettlesarvindauge,\\nslipp bildet rett på den store last-opp-knappen\",\n\t\"u_enpot\": 'bytt åt <a href=\"#\">enkelt UI</a> (gir sannsynleg raskere opplasting)',\n\t\"u_depot\": 'bytt åt <a href=\"#\">snæsent UI</a> (gir sannsynleg treigare opplasting)',\n\t\"u_gotpot\": 'bytta åt eit enklare UI for å laste opp raskere,\\n\\ndu kan gjerne bytte tilbake altså!',\n\t\"u_pott\": \"<p>filer: &nbsp; <b>{0}</b> ferdig, &nbsp; <b>{1}</b> feilet, &nbsp; <b>{2}</b> behandlast, &nbsp; <b>{3}</b> i kø</p>\",\n\t\"u_ever\": \"dette er den primitive opplastaren; up2k krev minst:<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1\",\n\t\"u_su2k\": 'dette er den primitive opplastaren; <a href=\"#\" id=\"u2yea\">up2k</a> er betre',\n\t\"u_uput\": 'litt raskare (uten sha512)',\n\t\"u_ewrite\": 'du har ikkje høve til å skrive i denne mappa',\n\t\"u_eread\": 'du har ikkje høve til å lese i denne mappa',\n\t\"u_enoi\": 'filsøk er deaktivert i serverkonfigurasjonen',\n\t\"u_enoow\": \"kan ikkje overskrive filer her (Delete-rettigheiten er nødvendig)\",\n\t\"u_badf\": 'Disse {0} filene (av totalt {1}) kan ikkje leses, kanskje pga rettigheitsproblem i filsystemet på datamaskinen din:\\n\\n',\n\t\"u_blankf\": 'Disse {0} filene (av totalt {1}) er blanke / uten innhald; ønskjer du å laste dei opp uansett?\\n\\n',\n\t\"u_applef\": 'Disse {0} filene (av totalt {1}) er antakeleg uønska;\\nTrykk <code>OK/Enter</code> for å HOPPE OVER disse filene,\\nTrykk <code>Avbryt/ESC</code> for å LASTE OPP disse filene óg:\\n\\n',\n\t\"u_just1\": '\\nFunkar kanskje betre viss du berre tar éi fil om gangen',\n\t\"u_ff_many\": 'Viss du bruker <b>Linux / MacOS / Android,</b> så kan dette antalet filer<br /><a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\"><em>kanskje</em> kræsje Firefox!</a> Viss det skjer, så prøv igjen (eller bruk Chrome).',\n\t\"u_up_life\": \"Filene slettast frå serveren {0}\\netter at opplastingen er fullført\",\n\t\"u_asku\": 'Laste opp disse {0} filene åt <code>{1}</code>',\n\t\"u_unpt\": \"Du kan angre / slette opplastinga med 🧯 oppe åt venstre\",\n\t\"u_bigtab\": 'Vil no vise {0} filer...\\n\\nDette kan kræsje nettlesaren din. Fortsette?',\n\t\"u_scan\": 'Les mappane...',\n\t\"u_dirstuck\": 'Nettleseren din fekk ikkje høve åt å lese følgande {0} filer/mapper, så dei blir hoppa over:',\n\t\"u_etadone\": 'Ferdig ({0}, {1} filer)',\n\t\"u_etaprep\": '(forberedar opplasting)',\n\t\"u_hashdone\": 'synfaring ferdig',\n\t\"u_hashing\": 'les',\n\t\"u_hs\": 'serveren tenkjer...',\n\t\"u_started\": \"filene blir no lasta opp 🚀\",\n\t\"u_dupdefer\": \"duplikat; vil bli håndtert åt slutt\",\n\t\"u_actx\": \"klikk her for å forhindre tap av<br />yting ved bytte åt andre vindauge/faner\",\n\t\"u_fixed\": \"OK!&nbsp; Løyste seg 👍\",\n\t\"u_cuerr\": \"kunne ikkje laste opp del {0} av {1};\\nsikkert greit, fortsetjar\\n\\nfil: {2}\",\n\t\"u_cuerr2\": \"server nekta opplastinga (del {0} av {1});\\nprøver igjen senere\\n\\nfil: {2}\\n\\nerror \",\n\t\"u_ehstmp\": \"prøver igjen; se mld nederst\",\n\t\"u_ehsfin\": \"server nekta forespørselen om å ferdigstille filen; prøver igjen...\",\n\t\"u_ehssrch\": \"server nekta forespørselen om å utføre søk; prøver igjen...\",\n\t\"u_ehsinit\": \"server nekta forespørselen om å begynne ei ny opplasting; prøver igjen...\",\n\t\"u_eneths\": \"eit problem med nettverket gjorde at avtale om opplasting ikkje kunne inngås; prøver igjen...\",\n\t\"u_enethd\": \"eit problem med nettverket gjorde at filsjekk ikkje kunne utførast; prøver igjen...\",\n\t\"u_cbusy\": \"ventar på klarering frå server etter eit lite nettverksglipp...\",\n\t\"u_ehsdf\": \"serveren er full!\\n\\nprøver igjen regelmessig,\\ni tilfelle nokon ryddar litt...\",\n\t\"u_emtleak1\": \"uff, det er mulig at nettlesaren din har ei minnelekkasje...\\nForeslår\",\n\t\"u_emtleak2\": ' helst at du <a href=\"{0}\">byttar åt https</a>, eller ',\n\t\"u_emtleak3\": ' at du ',\n\t\"u_emtleakc\": 'prøver følgande:\\n<ul><li>trykk F5 for å laste sida på nytt</li><li>så skru av &nbsp;<code>mt</code>&nbsp; brytaren under &nbsp;<code>⚙️ innstillinger</code></li><li>og prøv den same opplastinga igjen</li></ul>Opplasting vil gå litt treigare, men det får så vere.\\nBeklager bryderiet!\\n\\nPS: feilen <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">skal vere fikset</a> i chrome v107',\n\t\"u_emtleakf\": 'prøver følgende:\\n<ul><li>trykk F5 for å laste sida på nytt</li><li>så skru på <code>🥔</code> (\"enkelt UI\") i opplastaren</li><li>og prøv den same opplastingen igjen</li></ul>\\nPS: Firefox <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">fiksar forhåpentligvis feilen</a> ein eller annen gong',\n\t\"u_s404\": \"ikkje funne på serveren\",\n\t\"u_expl\": \"forklar\",\n\t\"u_maxconn\": \"dei fleste nettlesarar tillet ikkje meir enn 6, men firefox lar deg øke grensen med <code>connections-per-server</code> i <code>about:config</code>\",\n\t\"u_tu\": '<p class=\"warn\">ADVARSEL: turbo er på, <span>&nbsp;avbrotne opplastingar vil muligens ikkje oppdagast og gjenopptakast; hald musepeikaren over turbo-knappen for meir info</span></p>',\n\t\"u_ts\": '<p class=\"warn\">ADVARSEL: turbo er på, <span>&nbsp;søkeresultat kan vere feil; hold musepeikaren over turbo-knappen for meir info</span></p>',\n\t\"u_turbo_c\": \"turbo er deaktivert i serverkonfigurasjonen\",\n\t\"u_turbo_g\": 'turbo blei deaktivert fordi du ikkje har\\nhøve åt å sjå mappeinnhold i dette volumet',\n\t\"u_life_cfg\": 'slett opplasting etter <input id=\"lifem\" p=\"60\" /> min (eller <input id=\"lifeh\" p=\"3600\" /> timar)',\n\t\"u_life_est\": 'opplastingen slettast <span id=\"lifew\" tt=\"lokal tid\">---</span>',\n\t\"u_life_max\": 'denne mappa tillet ikkje å \\noppbevare filer i meir enn {0}',\n\t\"u_unp_ok\": 'opplasting kan angrast i {0}',\n\t\"u_unp_ng\": 'opplasting kan IKKE angrast',\n\t\"ue_ro\": 'du har ikkje høve åt skriving i denne mappa\\n\\n',\n\t\"ue_nl\": 'du er ikkje logga inn',\n\t\"ue_la\": 'du er logga inn som \"{0}\"',\n\t\"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',\n\t\"ue_ta\": 'prøv å last opp igjen, det burde fungere no',\n\t\"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\",\n\t\"ur_1uo\": \"OK: Fila blei lastet opp\",\n\t\"ur_auo\": \"OK: Alle {0} filene blei lastet opp\",\n\t\"ur_1so\": \"OK: Fila blei funne på serveren\",\n\t\"ur_aso\": \"OK: Alle {0} filene blei funne på serveren\",\n\t\"ur_1un\": \"Opplasting feila!\",\n\t\"ur_aun\": \"Alle {0} opplastingene gjekk feil!\",\n\t\"ur_1sn\": \"Fila finnast IKKE på serveren\",\n\t\"ur_asn\": \"Fann INGEN av dei {0} filene på serveren\",\n\t\"ur_um\": \"Ferdig;\\n{0} opplastingar gjekk bra,\\n{1} opplastingar gjekk feil\",\n\t\"ur_sm\": \"Ferdig;\\n{0} filer blei funne,\\n{1} filer finnast IKKJE på serveren\",\n\n\t\"rc_opn\": \"opne\",\n\t\"rc_ply\": \"spel av\",\n\t\"rc_pla\": \"spel av som lyd\",\n\t\"rc_txt\": \"opne i filvisar\",\n\t\"rc_md\": \"opne i tekstredigerar\",\n\t\"rc_dl\": \"Last ned\",\n\t\"rc_zip\": \"Last ned som arkiv\",\n\t\"rc_cpl\": \"kopier lenke\",\n\t\"rc_del\": \"slett\",\n\t\"rc_cut\": \"klipp ut\",\n\t\"rc_cpy\": \"kopier\",\n\t\"rc_pst\": \"Lim inn\",\n\t\"rc_rnm\": \"endre namn\",\n\t\"rc_nfo\": \"ny mappe\",\n\t\"rc_nfi\": \"ny fil\",\n\t\"rc_sal\": \"vel alle\",\n\t\"rc_sin\": \"inverter val\",\n\t\"rc_shf\": \"del denne mappa\",\n\t\"rc_shs\": \"del markering\",\n\n\t\"lang_set\": \"passar det å laste sida på nytt?\",\n\n\t\"splash\": {\n\t\t\"a1\": \"oppdatér\",\n\t\t\"b1\": \"heisann &nbsp; <small>(du er ikkje logga inn)</small>\",\n\t\t\"c1\": \"logg ut\",\n\t\t\"d1\": \"tilstand\",\n\t\t\"d2\": \"vis tilstanden åt alle trådar\",\n\t\t\"e1\": \"last innst.\",\n\t\t\"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\",\n\t\t\"f1\": \"du kan sjå på:\",\n\t\t\"g1\": \"du kan laste opp åt:\",\n\t\t\"cc1\": \"brytarar og slikt:\",\n\t\t\"h1\": \"skru av k304\",\n\t\t\"i1\": \"skru på k304\",\n\t\t\"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\",\n\t\t\"k1\": \"nullstill innstillinger\",\n\t\t\"l1\": \"logg inn:\",\n\t\t\"ls3\": \"logg inn\",\n\t\t\"lu4\": \"brukarnamn\",\n\t\t\"lp4\": \"passord\",\n\t\t\"lo3\": \"logg ut “{0}” overalt\",\n\t\t\"lo2\": \"avslutt økta på alle nettlesarar\",\n\t\t\"m1\": \"velkomen attende,\",\n\t\t\"n1\": \"404: filen finnast ikkje &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": 'eller kanskje du ikkje har høve? prøv eit passord eller <a href=\"' + SR + '/?h\">gå heim</a>',\n\t\t\"p1\": \"403: tilgang nektet &nbsp;~┻━┻\",\n\t\t\"q1\": 'prøv eit passord eller <a href=\"' + SR + '/?h\">gå heim</a>',\n\t\t\"r1\": \"gå heim\",\n\t\t\".s1\": \"kartlegg\",\n\t\t\"t1\": \"handling\",\n\t\t\"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\",\n\t\t\"v1\": \"kople åt\",\n\t\t\"v2\": \"bruk denne serveren som ein lokal harddisk\",\n\t\t\"w1\": \"bytt åt https\",\n\t\t\"x1\": \"bytt passord\",\n\t\t\"y1\": \"dine delinger\",\n\t\t\"z1\": \"lås opp område:\",\n\t\t\"ta1\": \"du må skrive eit nytt passord først\",\n\t\t\"ta2\": \"gjenta for å stadfeste nytt passord:\",\n\t\t\"ta3\": \"fant ein skrivefeil; vennligst prøv igjen\",\n\t\t\"nop\": \"FEIL: Passord kan ikkje vere tomt\",\n\t\t\"nou\": \"FEIL: Brukarnamn og passord må fyllast ut\",\n\t\t\"aa1\": \"innkommande:\",\n\t\t\"ab1\": \"skru av no304\",\n\t\t\"ac1\": \"skru på no304\",\n\t\t\"ad1\": \"no304 stoppar all bruk av cache. Hvis ikkje k304 var nok, prøv denne. Vil mangedoble dataforbruk!\",\n\t\t\"ae1\": \"utgående:\",\n\t\t\"af1\": \"vis nylig opplasta filer\",\n\t\t\"ag1\": \"vis kjente IdP-brukarar\",\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/nor.js",
    "content": "Ls.nor = {\n\t\"tt\": \"Norsk\",\n\n\t\"cols\": {\n\t\t\"c\": \"handlingsknapper\",\n\t\t\"dur\": \"varighet\",\n\t\t\"q\": \"kvalitet / bitrate\",\n\t\t\"Ac\": \"lyd-format\",\n\t\t\"Vc\": \"video-format\",\n\t\t\"Fmt\": \"format / innpakning\",\n\t\t\"Ahash\": \"lyd-kontrollsum\",\n\t\t\"Vhash\": \"video-kontrollsum\",\n\t\t\"Res\": \"oppløsning\",\n\t\t\"T\": \"filtype\",\n\t\t\"aq\": \"lydkvalitet / bitrate\",\n\t\t\"vq\": \"videokvalitet / bitrate\",\n\t\t\"pixfmt\": \"fargekoding / detaljenivå\",\n\t\t\"resw\": \"horisontal oppløsning\",\n\t\t\"resh\": \"vertikal oppløsning\",\n\t\t\"chs\": \"lydkanaler\",\n\t\t\"hz\": \"lyd-oppløsning\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"ymse\",\n\t\t\t[\"ESC\", \"lukk saker og ting\"],\n\n\t\t\t\"filbehandler\",\n\t\t\t[\"G\", \"listevisning eller ikoner\"],\n\t\t\t[\"T\", \"miniatyrbilder på/av\"],\n\t\t\t[\"⇧ A/D\", \"ikonstørrelse\"],\n\t\t\t[\"ctrl-K\", \"slett valgte\"],\n\t\t\t[\"ctrl-X\", \"klipp ut valgte\"],\n\t\t\t[\"ctrl-C\", \"kopiér til utklippstavle\"],\n\t\t\t[\"ctrl-V\", \"lim inn (flytt/kopiér)\"],\n\t\t\t[\"Y\", \"last ned valgte\"],\n\t\t\t[\"F2\", \"endre navn på valgte\"],\n\n\t\t\t\"filmarkering\",\n\t\t\t[\"space\", \"marker fil\"],\n\t\t\t[\"↑/↓\", \"flytt markør\"],\n\t\t\t[\"ctrl ↑/↓\", \"flytt markør og scroll\"],\n\t\t\t[\"⇧ ↑/↓\", \"velg forr./neste fil\"],\n\t\t\t[\"ctrl-A\", \"velg alle filer / mapper\"],\n\t\t], [\n\t\t\t\"navigering\",\n\t\t\t[\"B\", \"mappehierarki eller filsti\"],\n\t\t\t[\"I/K\", \"forr./neste mappe\"],\n\t\t\t[\"M\", \"ett nivå opp (eller lukk)\"],\n\t\t\t[\"V\", \"vis mapper eller tekstfiler\"],\n\t\t\t[\"A/D\", \"panelstørrelse\"],\n\t\t], [\n\t\t\t\"musikkspiller\",\n\t\t\t[\"J/L\", \"forr./neste sang\"],\n\t\t\t[\"U/O\", \"hopp 10sek bak/frem\"],\n\t\t\t[\"0..9\", \"hopp til 0%..90%\"],\n\t\t\t[\"P\", \"pause, eller start / fortsett\"],\n\t\t\t[\"S\", \"marker spillende sang\"],\n\t\t\t[\"Y\", \"last ned sang\"],\n\t\t], [\n\t\t\t\"bildeviser\",\n\t\t\t[\"J/L, ←/→\", \"forr./neste bilde\"],\n\t\t\t[\"Home/End\", \"første/siste bilde\"],\n\t\t\t[\"F\", \"fullskjermvisning\"],\n\t\t\t[\"R\", \"rotere mot høyre\"],\n\t\t\t[\"⇧ R\", \"rotere mot venstre\"],\n\t\t\t[\"S\", \"marker bilde\"],\n\t\t\t[\"Y\", \"last ned bilde\"],\n\t\t], [\n\t\t\t\"videospiller\",\n\t\t\t[\"U/O\", \"hopp 10sek bak/frem\"],\n\t\t\t[\"P/K/Space\", \"pause / fortsett\"],\n\t\t\t[\"C\", \"fortsett til neste fil\"],\n\t\t\t[\"V\", \"gjenta avspilling\"],\n\t\t\t[\"M\", \"lyd av/på\"],\n\t\t\t[\"[ og ]\", \"gjentaksintervall\"],\n\t\t], [\n\t\t\t\"dokumentviser\",\n\t\t\t[\"I/K\", \"forr./neste fil\"],\n\t\t\t[\"M\", \"lukk tekstdokument\"],\n\t\t\t[\"E\", \"rediger tekstdokument\"],\n\t\t\t[\"S\", \"marker fil (for F2/ctrl-x/...)\"],\n\t\t\t[\"Y\", \"last ned tekstfil\"],\n\t\t\t[\"⇧ J\", \"formattér json\"],\n\t\t]\n\t],\n\n\t\"m_ok\": \"OK\",\n\t\"m_ng\": \"Avbryt\",\n\n\t\"enable\": \"Aktiv\",\n\t\"danger\": \"VARSKU\",\n\t\"clipped\": \"kopiert til utklippstavlen\",\n\n\t\"ht_s1\": \"sekund\",\n\t\"ht_s2\": \"sekunder\",\n\t\"ht_m1\": \"minutt\",\n\t\"ht_m2\": \"minutter\",\n\t\"ht_h1\": \"time\",\n\t\"ht_h2\": \"timer\",\n\t\"ht_d1\": \"dag\",\n\t\"ht_d2\": \"dager\",\n\t\"ht_and\": \" og \",\n\n\t\"goh\": \"kontrollpanel\",\n\t\"gop\": 'naviger til mappen før denne\">forr.',\n\t\"gou\": 'naviger ett nivå opp\">opp',\n\t\"gon\": 'naviger til mappen etter denne\">neste',\n\t\"logout\": \"Logg ut \",\n\t\"login\": \"Logg inn\",\n\t\"access\": \" tilgang\",\n\t\"ot_close\": \"lukk verktøy\",\n\t\"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`&quot;try unite&quot;` = «try unite» eksakt$N$Ndatoformat er iso-8601, så f.eks.$N`2009-12-31` eller `2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"unpost: slett filer som du nylig har lastet opp; «angre-knappen»\",\n\t\"ot_bup\": \"bup: tradisjonell / primitiv filopplastning,$N$Nfungerer i omtrent samtlige nettlesere\",\n\t\"ot_mkdir\": \"mkdir: lag en ny mappe\",\n\t\"ot_md\": \"new-file: lag en ny tekstfil\",\n\t\"ot_msg\": \"msg: send en beskjed til serverloggen\",\n\t\"ot_mp\": \"musikkspiller-instillinger\",\n\t\"ot_cfg\": \"andre innstillinger\",\n\t\"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 [🎈]&nbsp; (den primitive opplasteren \"bup\")<br /><br />mens opplastninger foregår så vises fremdriften her oppe!',\n\t\"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 [🎈]&nbsp; (den primitive opplasteren \"bup\")<br /><br />mens opplastninger foregår så vises fremdriften her oppe!',\n\t\"ot_noie\": 'Fungerer mye bedre i Chrome / Firefox / Edge',\n\n\t\"ab_mkdir\": \"lag mappe\",\n\t\"ab_mkdoc\": \"ny tekstfil\",\n\t\"ab_msg\": \"send melding\",\n\n\t\"ay_path\": \"gå videre til mapper\",\n\t\"ay_files\": \"gå videre til filer\",\n\n\t\"wt_ren\": \"gi nye navn til de valgte filene$NSnarvei: F2\",\n\t\"wt_del\": \"slett de valgte filene$NSnarvei: ctrl-K\",\n\t\"wt_cut\": \"klipp ut de valgte filene &lt;small&gt;(for å lime inn et annet sted)&lt;/small&gt;$NSnarvei: ctrl-X\",\n\t\"wt_cpy\": \"kopiér de valgte filene til utklippstavlen$N(for å lime inn et annet sted)$NSnarvei: ctrl-C\",\n\t\"wt_pst\": \"lim inn filer (som tidligere ble klippet ut / kopiert et annet sted)$NSnarvei: ctrl-V\",\n\t\"wt_selall\": \"velg alle filer$NSnarvei: ctrl-A (mens fokus er på en fil)\",\n\t\"wt_selinv\": \"inverter utvalg\",\n\t\"wt_zip1\": \"last ned denne mappen som et arkiv\",\n\t\"wt_selzip\": \"last ned de valgte filene som et arkiv\",\n\t\"wt_seldl\": \"last ned de valgte filene$NSnarvei: Y\",\n\t\"wt_npirc\": \"kopiér sang-info (irc-formatert)\",\n\t\"wt_nptxt\": \"kopiér sang-info\",\n\t\"wt_m3ua\": \"legg til sang i m3u-spilleliste$N(husk å klikke på <code>📻copy</code> senere)\",\n\t\"wt_m3uc\": \"kopiér m3u-spillelisten til utklippstavlen\",\n\t\"wt_grid\": \"bytt mellom ikoner og listevisning$NSnarvei: G\",\n\t\"wt_prev\": \"forrige sang$NSnarvei: J\",\n\t\"wt_play\": \"play / pause$NSnarvei: P\",\n\t\"wt_next\": \"neste sang$NSnarvei: L\",\n\n\t\"ul_par\": \"samtidige handl.:\",\n\t\"ut_rand\": \"finn opp nye tilfeldige filnavn\",\n\t\"ut_u2ts\": \"gi filen på serveren samme$Ntidsstempel som lokalt hos deg\\\">📅\",\n\t\"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\",\n\t\"ut_mt\": \"fortsett å befare køen mens opplastning foregår$N$Nskru denne av dersom du har en$Ntreg prosessor eller harddisk\",\n\t\"ut_ask\": 'bekreft filutvalg før opplastning starter\">💭',\n\t\"ut_pot\": \"forbedre ytelsen på trege enheter ved å$Nforenkle brukergrensesnittet\",\n\t\"ut_srch\": \"utfør søk istedenfor å laste opp --$Nleter igjennom alle mappene du har lov til å se\",\n\t\"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\",\n\t\"ul_btn\": \"slipp filer / mapper<br>her (eller klikk meg)\",\n\t\"ul_btnu\": \"L A S T &nbsp; O P P\",\n\t\"ul_btns\": \"F I L S Ø K\",\n\n\t\"ul_hash\": \"befar\",\n\t\"ul_send\": \"&nbsp;send\",\n\t\"ul_done\": \"total\",\n\t\"ul_idle1\": \"ingen handlinger i køen\",\n\t\"ut_etah\": \"snitthastighet for &lt;em&gt;befaring&lt;/em&gt; samt gjenstående tid\",\n\t\"ut_etau\": \"snitthastighet for &lt;em&gt;opplastning&lt;/em&gt; samt gjenstående tid\",\n\t\"ut_etat\": \"&lt;em&gt;total&lt;/em&gt; snitthastighet og gjenstående tid\",\n\n\t\"uct_ok\": \"fullført uten problemer\",\n\t\"uct_ng\": \"fullført under tvil (duplikat, ikke funnet, ...)\",\n\t\"uct_done\": \"fullført (enten &lt;em&gt;ok&lt;/em&gt; eller &lt;em&gt;ng&lt;/em&gt;)\",\n\t\"uct_bz\": \"aktive handlinger (befaring / opplastning)\",\n\t\"uct_q\": \"køen\",\n\n\t\"utl_name\": \"filnavn\",\n\t\"utl_ulist\": \"vis\",\n\t\"utl_ucopy\": \"kopiér\",\n\t\"utl_links\": \"lenker\",\n\t\"utl_stat\": \"status\",\n\t\"utl_prog\": \"fremdrift\",\n\n\t// må være korte:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"FEIL!\",\n\t\"utl_oserr\": \"OS-feil\",\n\t\"utl_found\": \"funnet\",\n\t\"utl_defer\": \"senere\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"ferdig\",\n\n\t\"ul_flagblk\": \"filene har blitt lagt i køen</b><br>men det er en annen nettleserfane som holder på med befaring eller opplastning akkurat nå,<br>så venter til den er ferdig først\",\n\t\"ul_btnlk\": \"bryteren har blitt låst til denne tilstanden i serverens konfigurasjon\",\n\n\t\"udt_up\": \"Last opp\",\n\t\"udt_srch\": \"Søk\",\n\t\"udt_drop\": \"Slipp filene her\",\n\n\t\"u_nav_m\": '<h6>hva har du?</h6><code>Enter</code> = Filer (én eller flere)\\n<code>ESC</code> = Én mappe (inkludert undermapper)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Filer</a><a href=\"#\" id=\"modal-ng\">Én mappe</a>',\n\n\t\"cl_opts\": \"brytere\",\n\t\"cl_hfsz\": \"filstørrelse\",\n\t\"cl_themes\": \"utseende\",\n\t\"cl_langs\": \"språk\",\n\t\"cl_ziptype\": \"nedlastning av mapper\",\n\t\"cl_uopts\": \"up2k-brytere\",\n\t\"cl_favico\": \"favicon\",\n\t\"cl_bigdir\": \"store mapper\",\n\t\"cl_hsort\": \"#sort\",\n\t\"cl_keytype\": \"notasjon for musikalsk dur\",\n\t\"cl_hiddenc\": \"skjulte kolonner\",\n\t\"cl_hidec\": \"skjul\",\n\t\"cl_reset\": \"nullstill\",\n\t\"cl_hpick\": \"klikk på overskriften til kolonnene du ønsker å skjule i tabellen nedenfor\",\n\t\"cl_hcancel\": \"kolonne-skjuling avbrutt\",\n\t\"cl_rcm\": \"høyreklikkmeny\",\n\n\t\"ct_grid\": '田 ikoner',\n\t\"ct_ttips\": 'vis hjelpetekst ved å holde musen over ting\">ℹ️ tips',\n\t\"ct_thumb\": 'vis miniatyrbilder istedenfor ikoner$NSnarvei: T\">🖼️ bilder',\n\t\"ct_csel\": 'bruk tastene CTRL og SHIFT for markering av filer i ikonvisning\">merk',\n\t\"ct_dsel\": 'marker filer med klikk-og-dra i ikonvisning\">dsel',\n\t\"ct_dl\": 'last ned filer (ikke vis i nettleseren)\">dl',\n\t\"ct_ihop\": 'bla ned til sist viste bilde når bildeviseren lukkes\">g⮯',\n\t\"ct_dots\": 'vis skjulte filer (gitt at serveren tillater det)\">.synlig',\n\t\"ct_qdel\": 'sletteknappen spør bare én gang om bekreftelse\">hurtig🗑️',\n\t\"ct_dir1st\": 'sorter slik at mapper kommer foran filer\">📁 først',\n\t\"ct_nsort\": 'naturlig sortering (forstår tall i filnavn)\">nsort',\n\t\"ct_utc\": 'bruk UTC for alle klokkeslett\">UTC',\n\t\"ct_readme\": 'vis README.md nedenfor filene\">📜 readme',\n\t\"ct_idxh\": 'vis index.html istedenfor fil-liste\">htm',\n\t\"ct_sbars\": 'vis rullgardiner / skrollefelt\">⟊',\n\n\t\"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📅',\n\n\t\"cut_turbo\": \"forenklet befaring ved opplastning; bør sannsynlig <em>ikke</em> 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 <em>filstørrelsen</em> stemmer. Så dersom en korrupt fil skulle befinne seg på serveren allerede, på samme sted med samme størrelse og navn, så blir det <em>ikke oppdaget</em>.$N$Ndet anbefales å kun benytte denne funksjonen for å komme seg raskt igjennom selve opplastningen, for så å skru den av, og til slutt &quot;laste opp&quot; de samme filene én gang til -- slik at integriteten kan verifiseres\\\">turbo\",\n\n\t\"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$N<em>burde</em> oppdage og gjenoppta de fleste ufullstendige opplastninger, men er <em>ikke</em> en fullverdig erstatning for å deaktivere turbo og gjøre en skikkelig sjekk\\\">date-chk\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cut_nag\": \"meldingsvarsel når opplastning er ferdig$N(kun om nettleserfanen ikke er synlig)\",\n\t\"cut_sfx\": \"lydvarsel når opplastning er ferdig$N(kun om nettleserfanen ikke er synlig)\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cft_text\": \"ikontekst (blank ut og last siden på nytt for å deaktivere)\",\n\t\"cft_fg\": \"farge\",\n\t\"cft_bg\": \"bakgrunnsfarge\",\n\n\t\"cdt_lim\": \"maks antall filer å vise per mappe\",\n\t\"cdt_ask\": \"vis knapper for å laste flere filer nederst på siden istedenfor å gradvis laste mer av mappen når man scroller ned\",\n\t\"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\",\n\t\"cdt_ren\": \"bruk egendefinert høyreklikkmeny (den vanlige menyen er tilgjengelig med shift + høyreklikk)\\\">aktiv\",\n\t\"cdt_rdb\": \"høyreklikk to ganger for å vise den vanlige høyreklikkmenyen\\\">x2\",\n\n\t\"tt_entree\": \"bytt til mappehierarki$NSnarvei: B\",\n\t\"tt_detree\": \"bytt til tradisjonell sti-visning$NSnarvei: B\",\n\t\"tt_visdir\": \"bla ned til den åpne mappen\",\n\t\"tt_ftree\": \"bytt mellom filstruktur og tekstfiler$NSnarvei: V\",\n\t\"tt_pdock\": \"vis de overordnede mappene i et panel\",\n\t\"tt_dynt\": \"øk bredden på panelet ettersom treet utvider seg\",\n\t\"tt_wrap\": \"linjebryting\",\n\t\"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 )\",\n\n\t\"ml_pmode\": \"ved enden av mappen\",\n\t\"ml_btns\": \"knapper\",\n\t\"ml_tcode\": \"konvertering\",\n\t\"ml_tcode2\": \"konverter til\",\n\t\"ml_tint\": \"tint\",\n\t\"ml_eq\": \"audio equalizer (tonejustering)\",\n\t\"ml_drc\": \"compressor (volum-utjevning)\",\n\t\"ml_ss\": \"spol forbi stillhet\",\n\n\t\"mt_loop\": \"spill den samme sangen om og om igjen\\\">🔁\",\n\t\"mt_one\": \"spill kun én sang\\\">1️⃣\",\n\t\"mt_shuf\": \"sangene i hver mappe$Nspilles i tilfeldig rekkefølge\\\">🔀\",\n\t\"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▶\",\n\t\"mt_preload\": \"hent ned litt av neste sang i forkant,$Nslik at pausen i overgangen blir mindre\\\">forles\",\n\t\"mt_prescan\": \"ved behov, bla til neste mappe$Nslik at nettleseren lar oss$Nfortsette å spille musikk\\\">bla\",\n\t\"mt_fullpre\": \"hent ned hele neste sang, ikke bare litt:$N✅ skru på hvis nettet ditt er <b>ustabilt</b>,$N❌ skru av hvis nettet ditt er <b>tregt</b>\\\">full\",\n\t\"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\\\">☕️\",\n\t\"mt_waves\": \"waveform seekbar:$Nvis volumkurve i avspillingsfeltet\\\">~s\",\n\t\"mt_npclip\": \"vis knapper for å kopiere info om sangen du hører på\\\">/np\",\n\t\"mt_m3u_c\": \"vis knapper for å kopiere de valgte$Nsangene som innslag i en m3u8 spilleliste\\\">📻\",\n\t\"mt_octl\": \"integrering med operativsystemet (fjernkontroll, info-skjerm)\\\">os-ctl\",\n\t\"mt_oseek\": \"tillat spoling med fjernkontroll$N$Nmerk: på noen enheter (iPhones) så vil$Ndette erstatte knappen for neste sang\\\">spoling\",\n\t\"mt_oscv\": \"vis album-cover på infoskjermen\\\">bilde\",\n\t\"mt_follow\": \"bla slik at sangen som spilles alltid er synlig\\\">🎯\",\n\t\"mt_compact\": \"tettpakket avspillerpanel\\\">⟎\",\n\t\"mt_uncache\": \"prøv denne hvis en sang ikke spiller riktig\\\">oppfrisk\",\n\t\"mt_mloop\": \"repeter hele mappen\\\">🔁 gjenta\",\n\t\"mt_mnext\": \"hopp til neste mappe og fortsett\\\">📂 neste\",\n\t\"mt_mstop\": \"stopp avspilling\\\">⏸ stopp\",\n\t\"mt_cflac\": \"konverter flac / wav-filer til {0}\\\">flac\",\n\t\"mt_caac\": \"konverter aac / m4a-filer til to {0}\\\">aac\",\n\t\"mt_coth\": \"konverter alt annet (men ikke mp3) til {0}\\\">andre\",\n\t\"mt_c2opus\": \"det beste valget for alle PCer og Android\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, for iOS 17.5 og nyere\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, for iOS 11 tilogmed 17\\\">caf\",\n\t\"mt_c2mp3\": \"bra valg for steinalder-utstyr (slår aldri feil)\\\">mp3\",\n\t\"mt_c2flac\": \"gir best lydkvalitet, men eter nettet ditt\\\">flac\",\n\t\"mt_c2wav\": \"helt rå lydstrøm (bruker enda mere data enn flac)\\\">wav\",\n\t\"mt_c2ok\": \"bra valg!\",\n\t\"mt_c2nd\": \"ikke det foretrukne valget for din enhet, men funker sikkert greit\",\n\t\"mt_c2ng\": \"ser virkelig ikke ut som enheten din takler dette formatet... men ok, vi prøver\",\n\t\"mt_xowa\": \"iOS har fortsatt problemer med avspilling av owa-musikk i bakgrunnen. Bruk caf eller mp3 istedenfor\",\n\t\"mt_tint\": \"nivå av bakgrunnsfarge på søkestripa (0-100),$Ngjør oppdateringer mindre distraherende\",\n\t\"mt_eq\": \"`aktiver tonekontroll og forsterker;$N$Nboost `0` = normal volumskala$N$Nwidth `1 &nbsp;` = normal stereo$Nwidth `0.5` = 50% blanding venstre-høyre$Nwidth `0 &nbsp;` = mono$N$Nboost `-0.8` &amp; width `10` = instrumental :^)$N$Nreduserer også dødtid imellom sangfiler\",\n\t\"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\",\n\t\"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\",\n\t\"mt_ssvt\": \"volumterskel (0-255)\\\">volum\",\n\t\"mt_ssts\": \"aktiv innenfor første % av sangen\\\">start\",\n\t\"mt_sste\": \"aktiv innenfor siste % av sangen\\\">slutt\",\n\t\"mt_sssm\": \"avspillingshastighetsmultiplikator\\\">ffwd\",\n\n\t\"mb_play\": \"lytt\",\n\t\"mm_hashplay\": \"spill denne sangen?\",\n\t\"mm_m3u\": \"trykk <code>Enter/OK</code> for å spille\\ntrykk <code>ESC/Avbryt</code> for å redigere\",\n\t\"mp_breq\": \"krever firefox 82+, chrome 73+, eller iOS 15+\",\n\t\"mm_bload\": \"laster inn...\",\n\t\"mm_bconv\": \"konverterer til {0}, vent litt...\",\n\t\"mm_opusen\": \"nettleseren din forstår ikke aac / m4a;\\nkonvertering til opus er nå aktivert\",\n\t\"mm_playerr\": \"avspilling feilet: \",\n\t\"mm_eabrt\": \"Avspillingsforespørselen ble avbrutt\",\n\t\"mm_enet\": \"Nettet ditt er ustabilt\",\n\t\"mm_edec\": \"Noe er galt med musikkfilen\",\n\t\"mm_esupp\": \"Nettleseren din forstår ikke filtypen\",\n\t\"mm_eunk\": \"Ukjent feil\",\n\t\"mm_e404\": \"Avspilling feilet: Fil ikke funnet.\",\n\t\"mm_e403\": \"Avspilling feilet: Tilgang nektet.\\n\\nKanskje du ble logget ut?\\nPrøv å trykk F5 for å laste siden på nytt.\",\n\t\"mm_e415\": \"Avspilling feilet: Kunne ikke konvertere filen, sjekk serverloggen.\",\n\t\"mm_e500\": \"Avspilling feilet: Rusk i maskineriet, sjekk serverloggen.\",\n\t\"mm_e5xx\": \"Avspilling feilet: \",\n\t\"mm_nof\": \"finner ikke flere sanger i nærheten\",\n\t\"mm_prescan\": \"Leter etter neste sang...\",\n\t\"mm_scank\": \"Fant neste sang:\",\n\t\"mm_uncache\": \"alle sanger vil lastes på nytt ved neste avspilling\",\n\t\"mm_hnf\": \"sangen finnes ikke lenger\",\n\n\t\"im_hnf\": \"bildet finnes ikke lenger\",\n\n\t\"f_empty\": 'denne mappen er tom',\n\t\"f_chide\": 'dette vil skjule kolonnen «{0}»\\n\\nfanen for \"andre innstillinger\" lar deg vise kolonnen igjen',\n\t\"f_bigtxt\": \"denne filen er hele {0} MiB -- vis som tekst?\",\n\t\"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\",\n\t\"fbd_more\": '<div id=\"blazy\">viser <code>{0}</code> av <code>{1}</code> filer; <a href=\"#\" id=\"bd_more\">vis {2}</a> eller <a href=\"#\" id=\"bd_all\">vis alle</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\">viser <code>{0}</code> av <code>{1}</code> filer; <a href=\"#\" id=\"bd_all\">vis alle</a></div>',\n\t\"f_anota\": \"kun {0} av totalt {1} elementer ble markert;\\nfor å velge alt må du bla til bunnen av mappen først\",\n\n\t\"f_dls\": 'linkene i denne mappen er nå\\nomgjort til nedlastningsknapper',\n\t\"f_dl_nd\": 'hopper over mappe (bruk zip/tar-nedlasting i stedet):\\n',\n\n\t\"f_partial\": \"For å laste ned en fil som enda ikke er ferdig opplastet, klikk på filen som har samme filnavn som denne, men uten <code>.PARTIAL</code> 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 <code>.PARTIAL</code>-filen på en ukontrollert måte, trykk OK / Enter for å ignorere denne advarselen. Slik vil du høyst sannsynlig motta korrupt data.\",\n\n\t\"ft_paste\": \"Lim inn {0} filer$NSnarvei: ctrl-V\",\n\t\"fr_eperm\": 'kan ikke endre navn:\\ndu har ikke “move”-rettigheten i denne mappen',\n\t\"fd_eperm\": 'kan ikke slette:\\ndu har ikke “delete”-rettigheten i denne mappen',\n\t\"fc_eperm\": 'kan ikke klippe ut:\\ndu har ikke “move”-rettigheten i denne mappen',\n\t\"fp_eperm\": 'kan ikke lime inn:\\ndu har ikke “write”-rettigheten i denne mappen',\n\t\"fr_emore\": \"velg minst én fil som skal få nytt navn\",\n\t\"fd_emore\": \"velg minst én fil som skal slettes\",\n\t\"fc_emore\": \"velg minst én fil som skal klippes ut\",\n\t\"fcp_emore\": \"velg minst én fil som skal kopieres til utklippstavlen\",\n\n\t\"fs_sc\": \"del mappen du er i nå\",\n\t\"fs_ss\": \"del de valgte filene\",\n\t\"fs_just1d\": \"du kan ikke markere flere mapper samtidig,\\neller kombinere mapper og filer\",\n\t\"fs_abrt\": \"❌ avbryt\",\n\t\"fs_rand\": \"🎲 tilfeldig navn\",\n\t\"fs_go\": \"✅ opprett deling\",\n\t\"fs_name\": \"navn\",\n\t\"fs_src\": \"kilde\",\n\t\"fs_pwd\": \"passord\",\n\t\"fs_exp\": \"varighet\",\n\t\"fs_tmin\": \"min\",\n\t\"fs_thrs\": \"timer\",\n\t\"fs_tdays\": \"dager\",\n\t\"fs_never\": \"for evig\",\n\t\"fs_pname\": \"frivillig navn (blir noe tilfeldig ellers)\",\n\t\"fs_tsrc\": \"fil/mappe som skal deles\",\n\t\"fs_ppwd\": \"frivillig passord\",\n\t\"fs_w8\": \"oppretter deling...\",\n\t\"fs_ok\": \"trykk <code>Enter/OK</code> for å kopiere linken (for CTRL-V)\\ntrykk <code>ESC/Avbryt</code> for å bare bekrefte\",\n\n\t\"frt_dec\": \"kan korrigere visse ødelagte filnavn\\\">url-decode\",\n\t\"frt_rst\": \"nullstiller endringer (tilbake til de originale filnavnene)\\\">↺ reset\",\n\t\"frt_abrt\": \"avbryt og lukk dette vinduet\\\">❌ avbryt\",\n\t\"frb_apply\": \"IVERKSETT\",\n\t\"fr_adv\": \"automasjon basert på metadata<br>og / eller mønster (regulære uttrykk)\\\">avansert\",\n\t\"fr_case\": \"versalfølsomme uttrykk\\\">Aa\",\n\t\"fr_win\": \"bytt ut bokstavene <code>&lt;&gt;:&quot;\\\\|?*</code> med$Ntilsvarende som windows ikke får panikk av\\\">win\",\n\t\"fr_slash\": \"bytt ut bokstaven <code>/</code> slik at den ikke forårsaker at nye mapper opprettes\\\">ikke /\",\n\t\"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.\",\n\t\"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\",\n\t\"fr_pdel\": \"slett\",\n\t\"fr_pnew\": \"lagre som\",\n\t\"fr_pname\": \"gi innstillingene dine et navn\",\n\t\"fr_aborted\": \"avbrutt\",\n\t\"fr_lold\": \"gammelt navn\",\n\t\"fr_lnew\": \"nytt navn\",\n\t\"fr_tags\": \"metadata for de valgte filene (kun for referanse):\",\n\t\"fr_busy\": \"endrer navn på {0} filer...\\n\\n{1}\",\n\t\"fr_efail\": \"endring av navn feilet:\\n\",\n\t\"fr_nchg\": \"{0} av navnene ble justert pga. <code>win</code> og/eller <code>ikke /</code>\\n\\nvil du fortsette med de nye navnene som ble valgt?\",\n\n\t\"fd_ok\": \"sletting OK\",\n\t\"fd_err\": \"sletting feilet:\\n\",\n\t\"fd_none\": \"ingenting ble slettet; kanskje avvist av serverkonfigurasjon (xbd)?\",\n\t\"fd_busy\": \"sletter {0} filer...\\n\\n{1}\",\n\t\"fd_warn1\": \"SLETT disse {0} filene?\",\n\t\"fd_warn2\": \"<b>Siste sjanse!</b> Dette kan ikke angres. Slett?\",\n\n\t\"fc_ok\": \"klippet ut {0} filer\",\n\t\"fc_warn\": 'klippet ut {0} filer\\n\\nmen: kun <b>denne</b> nettleserfanen har mulighet til å lime dem inn et annet sted, siden antallet filer er helt hinsides',\n\n\t\"fcc_ok\": \"kopierte {0} filer til utklippstavlen\",\n\t\"fcc_warn\": 'kopierte {0} filer til utklippstavlen\\n\\nmen: kun <b>denne</b> nettleserfanen har mulighet til å lime dem inn et annet sted, siden antallet filer er helt hinsides',\n\n\t\"fp_apply\": \"bekreft og lim inn nå\",\n\t\"fp_skip\": \"kun ledige\",\n\t\"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\",\n\t\"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:\",\n\t\"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:\",\n\t\"fp_emore\": \"det er fortsatt flere navn som må endres\",\n\t\"fp_ok\": \"flytting OK\",\n\t\"fcp_ok\": \"kopiering OK\",\n\t\"fp_busy\": \"flytter {0} filer...\\n\\n{1}\",\n\t\"fcp_busy\": \"kopierer {0} filer...\\n\\n{1}\",\n\t\"fp_abrt\": \"avbryter...\",\n\t\"fp_err\": \"flytting feilet:\\n\",\n\t\"fcp_err\": \"kopiering feilet:\\n\",\n\t\"fp_confirm\": \"flytt disse {0} filene hit?\",\n\t\"fcp_confirm\": \"kopiér disse {0} filene hit?\",\n\t\"fp_etab\": 'kunne ikke lese listen med filer ifra den andre nettleserfanen',\n\t\"fp_name\": \"Laster opp én fil fra enheten din. Velg filnavn:\",\n\t\"fp_both_m\": '<h6>hva skal limes inn her?</h6><code>Enter</code> = Flytt {0} filer fra «{1}»\\n<code>ESC</code> = Last opp {2} filer fra enheten din',\n\t\"fcp_both_m\": '<h6>hva skal limes inn her?</h6><code>Enter</code> = Kopiér {0} filer fra «{1}»\\n<code>ESC</code> = Last opp {2} filer fra enheten din',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Flytt</a><a href=\"#\" id=\"modal-ng\">Last opp</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Kopiér</a><a href=\"#\" id=\"modal-ng\">Last opp</a>',\n\n\t\"mk_noname\": \"skriv inn et navn i tekstboksen til venstre først :p\",\n\t\"nmd_i1\": \"legg også til ønsket filtype, for eksempel <code>.md</code>\",\n\t\"nmd_i2\": \"du kan bare lage <code>.{0}</code>-filer fordi du ikke har delete-tilgang\",\n\n\t\"tv_load\": \"Laster inn tekstfil:\\n\\n{0}\\n\\n{1}% ({2} av {3} MiB lastet ned)\",\n\t\"tv_xe1\": \"kunne ikke laste tekstfil:\\n\\nfeil \",\n\t\"tv_xe2\": \"404, Fil ikke funnet\",\n\t\"tv_lst\": \"tekstfiler i mappen\",\n\t\"tvt_close\": \"gå tilbake til mappen$NSnarvei: M (eller Esc)\\\">❌ lukk\",\n\t\"tvt_dl\": \"last ned denne filen$NSnarvei: Y\\\">💾 last ned\",\n\t\"tvt_prev\": \"vis forrige dokument$NSnarvei: i\\\">⬆ forr.\",\n\t\"tvt_next\": \"vis neste dokument$NSnarvei: K\\\">⬇ neste\",\n\t\"tvt_sel\": \"markér filen &nbsp; ( for utklipp / sletting / ... )$NSnarvei: S\\\">merk\",\n\t\"tvt_j\": \"formattér json$NSnarvei: shift-J\\\">j\",\n\t\"tvt_edit\": \"redigér filen$NSnarvei: E\\\">✏️ endre\",\n\t\"tvt_tail\": \"overvåk filen for endringer og vis nye linjer i sanntid\\\">📡 følg\",\n\t\"tvt_wrap\": \"tekstbryting\\\">↵\",\n\t\"tvt_atail\": \"hold de nyeste linjene synlig (lås til bunnen av siden)\\\">⚓\",\n\t\"tvt_ctail\": \"forstå og vis terminalfarger (ansi-sekvenser)\\\">🌈\",\n\t\"tvt_ntail\": \"maks-grense for antall bokstaver som skal vises i vinduet\",\n\n\t\"m3u_add1\": \"sangen ble lagt til i m3u-spillelisten\",\n\t\"m3u_addn\": \"{0} sanger ble lagt til i m3u-spillelisten\",\n\t\"m3u_clip\": \"m3u-spillelisten ble kopiert til utklippstavlen\\n\\nneste steg er å opprette et tekstdokument med filnavn som slutter på <code>.m3u</code> og lime inn spillelisten der\",\n\n\t\"gt_vau\": \"ikke vis videofiler, bare spill lyden\\\">🎧\",\n\t\"gt_msel\": \"markér filer istedenfor å åpne dem; ctrl-klikk filer for å overstyre$N$N&lt;em&gt;når aktiv: dobbelklikk en fil / mappe for å åpne&lt;/em&gt;$N$NSnarvei: S\\\">markering\",\n\t\"gt_crop\": \"beskjær ikonene så de passer bedre\\\">✂\",\n\t\"gt_3x\": \"høyere oppløsning på ikoner\\\">3x\",\n\t\"gt_zoom\": \"zoom\",\n\t\"gt_chop\": \"trim\",\n\t\"gt_sort\": \"sorter\",\n\t\"gt_name\": \"navn\",\n\t\"gt_sz\": \"størr.\",\n\t\"gt_ts\": \"dato\",\n\t\"gt_ext\": \"type\",\n\t\"gt_c1\": \"reduser maks-lengde på filnavn\",\n\t\"gt_c2\": \"øk maks-lengde på filnavn\",\n\n\t\"sm_w8\": \"søker...\",\n\t\"sm_prev\": \"søkeresultatene er fra et tidligere søk:\\n  \",\n\t\"sl_close\": \"lukk søkeresultater\",\n\t\"sl_hits\": \"viser {0} treff\",\n\t\"sl_moar\": \"hent flere\",\n\n\t\"s_sz\": \"størr.\",\n\t\"s_dt\": \"dato\",\n\t\"s_rd\": \"sti\",\n\t\"s_fn\": \"navn\",\n\t\"s_ta\": \"meta\",\n\t\"s_ua\": \"up@\",\n\t\"s_ad\": \"avns.\",\n\t\"s_s1\": \"større enn ↓ MiB\",\n\t\"s_s2\": \"mindre enn ↓ MiB\",\n\t\"s_d1\": \"nyere enn &lt;dato&gt;\",\n\t\"s_d2\": \"eldre enn\",\n\t\"s_u1\": \"lastet opp etter\",\n\t\"s_u2\": \"og/eller før\",\n\t\"s_r1\": \"mappenavn inneholder\",\n\t\"s_f1\": \"filnavn inneholder\",\n\t\"s_t1\": \"sang-info inneholder\",\n\t\"s_a1\": \"konkrete egenskaper\",\n\n\t\"md_eshow\": \"viser forenklet \",\n\t\"md_off\": \"[📜<em>readme</em>] er avskrudd i [⚙️] -- dokument skjult\",\n\n\t\"badreply\": \"Ugyldig svar ifra serveren\",\n\n\t\"xhr403\": \"403: Tilgang nektet\\n\\nkanskje du ble logget ut? prøv å trykk F5\",\n\t\"xhr0\": \"ukjent (enten nettverksproblemer eller serverkrasj)\",\n\t\"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\",\n\t\"tl_xe1\": \"kunne ikke hente undermapper:\\n\\nfeil \",\n\t\"tl_xe2\": \"404: Mappen finnes ikke\",\n\t\"fl_xe1\": \"kunne ikke hente filer i mappen:\\n\\nfeil \",\n\t\"fl_xe2\": \"404: Mappen finnes ikke\",\n\t\"fd_xe1\": \"kan ikke opprette ny mappe:\\n\\nfeil \",\n\t\"fd_xe2\": \"404: Den overordnede mappen finnes ikke\",\n\t\"fsm_xe1\": \"kunne ikke sende melding:\\n\\nfeil \",\n\t\"fsm_xe2\": \"404: Den overordnede mappen finnes ikke\",\n\t\"fu_xe1\": \"kunne ikke hente listen med nylig opplastede filer ifra serveren:\\n\\nfeil \",\n\t\"fu_xe2\": \"404: Filen finnes ikke??\",\n\n\t\"fz_tar\": \"ukomprimert gnu-tar arkiv, for linux og mac\",\n\t\"fz_pax\": \"ukomprimert pax-tar arkiv, litt tregere\",\n\t\"fz_targz\": \"gnu-tar pakket med gzip (nivå 3)$N$NNB: denne er veldig treg;$Nukomprimert tar er bedre\",\n\t\"fz_tarxz\": \"gnu-tar pakket med xz (nivå 1)$N$NNB: denne er veldig treg;$Nukomprimert tar er bedre\",\n\t\"fz_zip8\": \"zip med filnavn i utf8 (noe problematisk på windows 7 og eldre)\",\n\t\"fz_zipd\": \"zip med filnavn i cp437, for høggamle maskiner\",\n\t\"fz_zipc\": \"cp437 med tidlig crc32,$Nfor MS-DOS PKZIP v2.04g (oktober 1993)$N(øker behandlingstid på server)\",\n\n\t\"un_m1\": \"nedenfor kan du angre / slette filer som du nylig har lastet opp, eller avbryte ufullstendige opplastninger\",\n\t\"un_upd\": \"oppdater\",\n\t\"un_m4\": \"eller hvis du vil dele nedlastnings-lenkene:\",\n\t\"un_ulist\": \"vis\",\n\t\"un_ucopy\": \"kopiér\",\n\t\"un_flt\": \"valgfritt filter:&nbsp; filnavn / filsti må inneholde\",\n\t\"un_fclr\": \"nullstill filter\",\n\t\"un_derr\": 'unpost-sletting feilet:\\n',\n\t\"un_f5\": 'noe gikk galt, prøv å oppdatere listen eller trykk F5',\n\t\"un_uf5\": \"beklager, men du må laste siden på nytt (f.eks. ved å trykke F5 eller CTRL-R) før denne opplastningen kan avbrytes\",\n\t\"un_nou\": '<b>advarsel:</b> kan ikke vise ufullstendige opplastninger akkurat nå; klikk på oppdater-linken om litt',\n\t\"un_noc\": '<b>advarsel:</b> angring av fullførte opplastninger er deaktivert i serverkonfigurasjonen',\n\t\"un_max\": \"viser de første 2000 filene (bruk filteret for å innsnevre)\",\n\t\"un_avail\": \"{0} nylig opplastede filer kan slettes<br />{1} ufullstendige opplastninger kan avbrytes\",\n\t\"un_m2\": \"sortert etter opplastningstid; nyeste først:\",\n\t\"un_no1\": \"men nei, her var det jaggu ikkeno som slettes kan\",\n\t\"un_no2\": \"men nei, her var det jaggu ingenting som passet overens med filteret\",\n\t\"un_next\": \"slett de neste {0} filene nedenfor\",\n\t\"un_abrt\": \"avbryt\",\n\t\"un_del\": \"slett\",\n\t\"un_m3\": \"henter listen med nylig opplastede filer...\",\n\t\"un_busy\": \"sletter {0} filer...\",\n\t\"un_clip\": \"{0} lenker kopiert til utklippstavlen\",\n\n\t\"u_https1\": \"du burde\",\n\t\"u_https2\": \"bytte til https\",\n\t\"u_https3\": \"for høyere hastighet\",\n\t\"u_ancient\": 'nettleseren din er prehistorisk -- mulig du burde <a href=\"#\" onclick=\"goto(\\'bup\\')\">bruke bup istedenfor</a>',\n\t\"u_nowork\": \"krever firefox 53+, chrome 57+, eller iOS 11+\",\n\t\"tail_2old\": \"krever firefox 105+, chrome 71+, eller iOS 14.5+\",\n\t\"u_nodrop\": 'nettleseren din er for gammel til å laste opp filer ved å dra dem inn i vinduet',\n\t\"u_notdir\": \"mottok ikke mappen!\\n\\nnettleseren din er for gammel,\\nprøv å dra mappen inn i vinduet istedenfor\",\n\t\"u_uri\": \"for å laste opp bilder ifra andre nettleservinduer,\\nslipp bildet rett på den store last-opp-knappen\",\n\t\"u_enpot\": 'bytt til <a href=\"#\">enkelt UI</a> (gir sannsynlig raskere opplastning)',\n\t\"u_depot\": 'bytt til <a href=\"#\">snæsent UI</a> (gir sannsynlig tregere opplastning)',\n\t\"u_gotpot\": 'byttet til et enklere UI for å laste opp raskere,\\n\\ndu kan gjerne bytte tilbake altså!',\n\t\"u_pott\": \"<p>filer: &nbsp; <b>{0}</b> ferdig, &nbsp; <b>{1}</b> feilet, &nbsp; <b>{2}</b> behandles, &nbsp; <b>{3}</b> i kø</p>\",\n\t\"u_ever\": \"dette er den primitive opplasteren; up2k krever minst:<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1\",\n\t\"u_su2k\": 'dette er den primitive opplasteren; <a href=\"#\" id=\"u2yea\">up2k</a> er bedre',\n\t\"u_uput\": 'litt raskere (uten sha512)',\n\t\"u_ewrite\": 'du har ikke skrivetilgang i denne mappen',\n\t\"u_eread\": 'du har ikke lesetilgang i denne mappen',\n\t\"u_enoi\": 'filsøk er deaktivert i serverkonfigurasjonen',\n\t\"u_enoow\": \"kan ikke overskrive filer her (Delete-rettigheten er nødvendig)\",\n\t\"u_badf\": 'Disse {0} filene (av totalt {1}) kan ikke leses, kanskje pga rettighetsproblemer i filsystemet på datamaskinen din:\\n\\n',\n\t\"u_blankf\": 'Disse {0} filene (av totalt {1}) er blanke / uten innhold; ønsker du å laste dem opp uansett?\\n\\n',\n\t\"u_applef\": 'Disse {0} filene (av totalt {1}) er antagelig uønskede;\\nTrykk <code>OK/Enter</code> for å HOPPE OVER disse filene,\\nTrykk <code>Avbryt/ESC</code> for å LASTE OPP disse filene også:\\n\\n',\n\t\"u_just1\": '\\nFunker kanskje bedre hvis du bare tar én fil om gangen',\n\t\"u_ff_many\": 'Hvis du bruker <b>Linux / MacOS / Android,</b> så kan dette antallet filer<br /><a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\"><em>kanskje</em> krasje Firefox!</a> Hvis det skjer, så prøv igjen (eller bruk Chrome).',\n\t\"u_up_life\": \"Filene slettes fra serveren {0}\\netter at opplastningen er fullført\",\n\t\"u_asku\": 'Laste opp disse {0} filene til <code>{1}</code>',\n\t\"u_unpt\": \"Du kan angre / slette opplastningen med 🧯 oppe til venstre\",\n\t\"u_bigtab\": 'Vil nå vise {0} filer...\\n\\nDette kan krasje nettleseren din. Fortsette?',\n\t\"u_scan\": 'Leser mappene...',\n\t\"u_dirstuck\": 'Nettleseren din fikk ikke tilgang til å lese følgende {0} filer/mapper, så de blir hoppet over:',\n\t\"u_etadone\": 'Ferdig ({0}, {1} filer)',\n\t\"u_etaprep\": '(forbereder opplastning)',\n\t\"u_hashdone\": 'befaring ferdig',\n\t\"u_hashing\": 'les',\n\t\"u_hs\": 'serveren tenker...',\n\t\"u_started\": \"filene blir nå lastet opp 🚀\",\n\t\"u_dupdefer\": \"duplikat; vil bli håndtert til slutt\",\n\t\"u_actx\": \"klikk her for å forhindre tap av<br />ytelse ved bytte til andre vinduer/faner\",\n\t\"u_fixed\": \"OK!&nbsp; Løste seg 👍\",\n\t\"u_cuerr\": \"kunne ikke laste opp del {0} av {1};\\nsikkert greit, fortsetter\\n\\nfil: {2}\",\n\t\"u_cuerr2\": \"server nektet opplastningen (del {0} av {1});\\nprøver igjen senere\\n\\nfil: {2}\\n\\nerror \",\n\t\"u_ehstmp\": \"prøver igjen; se mld nederst\",\n\t\"u_ehsfin\": \"server nektet forespørselen om å ferdigstille filen; prøver igjen...\",\n\t\"u_ehssrch\": \"server nektet forespørselen om å utføre søk; prøver igjen...\",\n\t\"u_ehsinit\": \"server nektet forespørselen om å begynne en ny opplastning; prøver igjen...\",\n\t\"u_eneths\": \"et problem med nettverket gjorde at avtale om opplastning ikke kunne inngås; prøver igjen...\",\n\t\"u_enethd\": \"et problem med nettverket gjorde at filsjekk ikke kunne utføres; prøver igjen...\",\n\t\"u_cbusy\": \"venter på klarering ifra server etter et lite nettverksglipp...\",\n\t\"u_ehsdf\": \"serveren er full!\\n\\nprøver igjen regelmessig,\\ni tilfelle noen rydder litt...\",\n\t\"u_emtleak1\": \"uff, det er mulig at nettleseren din har en minnelekkasje...\\nForeslår\",\n\t\"u_emtleak2\": ' helst at du <a href=\"{0}\">bytter til https</a>, eller ',\n\t\"u_emtleak3\": ' at du ',\n\t\"u_emtleakc\": 'prøver følgende:\\n<ul><li>trykk F5 for å laste siden på nytt</li><li>så skru av &nbsp;<code>mt</code>&nbsp; bryteren under &nbsp;<code>⚙️ innstillinger</code></li><li>og forsøk den samme opplastningen igjen</li></ul>Opplastning vil gå litt tregere, men det får så være.\\nBeklager bryderiet !\\n\\nPS: feilen <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">skal være fikset</a> i chrome v107',\n\t\"u_emtleakf\": 'prøver følgende:\\n<ul><li>trykk F5 for å laste siden på nytt</li><li>så skru på <code>🥔</code> (\"enkelt UI\") i opplasteren</li><li>og forsøk den samme opplastningen igjen</li></ul>\\nPS: Firefox <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">fikser forhåpentligvis feilen</a> en eller annen gang',\n\t\"u_s404\": \"ikke funnet på serveren\",\n\t\"u_expl\": \"forklar\",\n\t\"u_maxconn\": \"de fleste nettlesere tillater ikke mer enn 6, men firefox lar deg øke grensen med <code>connections-per-server</code> i <code>about:config</code>\",\n\t\"u_tu\": '<p class=\"warn\">ADVARSEL: turbo er på, <span>&nbsp;avbrutte opplastninger vil muligens ikke oppdages og gjenopptas; hold musepekeren over turbo-knappen for mer info</span></p>',\n\t\"u_ts\": '<p class=\"warn\">ADVARSEL: turbo er på, <span>&nbsp;søkeresultater kan være feil; hold musepekeren over turbo-knappen for mer info</span></p>',\n\t\"u_turbo_c\": \"turbo er deaktivert i serverkonfigurasjonen\",\n\t\"u_turbo_g\": 'turbo ble deaktivert fordi du ikke har\\ntilgang til å se mappeinnhold i dette volumet',\n\t\"u_life_cfg\": 'slett opplastning etter <input id=\"lifem\" p=\"60\" /> min (eller <input id=\"lifeh\" p=\"3600\" /> timer)',\n\t\"u_life_est\": 'opplastningen slettes <span id=\"lifew\" tt=\"lokal tid\">---</span>',\n\t\"u_life_max\": 'denne mappen tillater ikke å \\noppbevare filer i mer enn {0}',\n\t\"u_unp_ok\": 'opplastning kan angres i {0}',\n\t\"u_unp_ng\": 'opplastning kan IKKE angres',\n\t\"ue_ro\": 'du har ikke skrivetilgang i denne mappen\\n\\n',\n\t\"ue_nl\": 'du er ikke logget inn',\n\t\"ue_la\": 'du er logget inn som \"{0}\"',\n\t\"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',\n\t\"ue_ta\": 'prøv å laste opp igjen, det burde funke nå',\n\t\"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\",\n\t\"ur_1uo\": \"OK: Filen ble lastet opp\",\n\t\"ur_auo\": \"OK: Alle {0} filene ble lastet opp\",\n\t\"ur_1so\": \"OK: Filen ble funnet på serveren\",\n\t\"ur_aso\": \"OK: Alle {0} filene ble funnet på serveren\",\n\t\"ur_1un\": \"Opplastning feilet!\",\n\t\"ur_aun\": \"Alle {0} opplastningene gikk feil!\",\n\t\"ur_1sn\": \"Filen finnes IKKE på serveren\",\n\t\"ur_asn\": \"Fant INGEN av de {0} filene på serveren\",\n\t\"ur_um\": \"Ferdig;\\n{0} opplastninger gikk bra,\\n{1} opplastninger gikk feil\",\n\t\"ur_sm\": \"Ferdig;\\n{0} filer ble funnet,\\n{1} filer finnes IKKE på serveren\",\n\n\t\"rc_opn\": \"åpne\",\n\t\"rc_ply\": \"spill av\",\n\t\"rc_pla\": \"spill av som lyd\",\n\t\"rc_txt\": \"åpne i filviser\",\n\t\"rc_md\": \"åpne i teksteditor\",\n\t\"rc_dl\": \"Last ned\",\n\t\"rc_zip\": \"Last ned som arkiv\",\n\t\"rc_cpl\": \"kopier lenke\",\n\t\"rc_del\": \"slett\",\n\t\"rc_cut\": \"klipp ut\",\n\t\"rc_cpy\": \"kopier\",\n\t\"rc_pst\": \"Lim inn\",\n\t\"rc_rnm\": \"gi nytt navn\",\n\t\"rc_nfo\": \"ny mappe\",\n\t\"rc_nfi\": \"ny fil\",\n\t\"rc_sal\": \"velg alle\",\n\t\"rc_sin\": \"inverter utvalg\",\n\t\"rc_shf\": \"del denne mappen\",\n\t\"rc_shs\": \"del markering\",\n\n\t\"lang_set\": \"passer det å laste siden på nytt?\",\n\n\t\"splash\": {\n\t\t\"a1\": \"oppdater\",\n\t\t\"b1\": \"halloien &nbsp; <small>(du er ikke logget inn)</small>\",\n\t\t\"c1\": \"logg ut\",\n\t\t\"d1\": \"tilstand\",\n\t\t\"d2\": \"vis tilstanden til alle tråder\",\n\t\t\"e1\": \"last innst.\",\n\t\t\"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\",\n\t\t\"f1\": \"du kan betrakte:\",\n\t\t\"g1\": \"du kan laste opp til:\",\n\t\t\"cc1\": \"brytere og sånt:\",\n\t\t\"h1\": \"skru av k304\",\n\t\t\"i1\": \"skru på k304\",\n\t\t\"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\",\n\t\t\"k1\": \"nullstill innstillinger\",\n\t\t\"l1\": \"logg inn:\",\n\t\t\"ls3\": \"logg inn\",\n\t\t\"lu4\": \"brukernavn\",\n\t\t\"lp4\": \"passord\",\n\t\t\"lo3\": \"logg ut “{0}” overalt\",\n\t\t\"lo2\": \"avslutter økten på alle nettlesere\",\n\t\t\"m1\": \"velkommen tilbake,\",\n\t\t\"n1\": \"404: filen finnes ikke &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": 'eller kanskje du ikke har tilgang? prøv et passord eller <a href=\"' + SR + '/?h\">gå hjem</a>',\n\t\t\"p1\": \"403: tilgang nektet &nbsp;~┻━┻\",\n\t\t\"q1\": 'prøv et passord eller <a href=\"' + SR + '/?h\">gå hjem</a>',\n\t\t\"r1\": \"gå hjem\",\n\t\t\".s1\": \"kartlegg\",\n\t\t\"t1\": \"handling\",\n\t\t\"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\",\n\t\t\"v1\": \"koble til\",\n\t\t\"v2\": \"bruk denne serveren som en lokal harddisk\",\n\t\t\"w1\": \"bytt til https\",\n\t\t\"x1\": \"bytt passord\",\n\t\t\"y1\": \"dine delinger\",\n\t\t\"z1\": \"lås opp område:\",\n\t\t\"ta1\": \"du må skrive et nytt passord først\",\n\t\t\"ta2\": \"gjenta for å bekrefte nytt passord:\",\n\t\t\"ta3\": \"fant en skrivefeil; vennligst prøv igjen\",\n\t\t\"nop\": \"FEIL: Passord kan ikke være blankt\",\n\t\t\"nou\": \"FEIL: Både brukernavn og passord må angis\",\n\t\t\"aa1\": \"innkommende:\",\n\t\t\"ab1\": \"skru av no304\",\n\t\t\"ac1\": \"skru på no304\",\n\t\t\"ad1\": \"no304 stopper all bruk av cache. Hvis ikke k304 var nok, prøv denne. Vil mangedoble dataforbruk!\",\n\t\t\"ae1\": \"utgående:\",\n\t\t\"af1\": \"vis nylig opplastede filer\",\n\t\t\"ag1\": \"vis kjente IdP-brukere\",\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/pol.js",
    "content": "\n// Wiersze kończące się na //m to niezweryfikowane tłumaczenia maszynowe\n\nLs.pol = {\n\t\"tt\": \"Polski\",\n\n\t\"cols\": {\n\t\t\"c\": \"przyciski akcji\",\n\t\t\"dur\": \"czas trwania\",\n\t\t\"q\": \"jakość / bitrate\",\n\t\t\"Ac\": \"kodek audio\",\n\t\t\"Vc\": \"kodek wideo\",\n\t\t\"Fmt\": \"format / kontener\",\n\t\t\"Ahash\": \"suma kontrolna audio\",\n\t\t\"Vhash\": \"suma kontrolna wideo\",\n\t\t\"Res\": \"rozdzielczość\",\n\t\t\"T\": \"rodzaj pliku\",\n\t\t\"aq\": \"jakość / bitrate audio\",\n\t\t\"vq\": \"jakość / bitrate wideo\",\n\t\t\"pixfmt\": \"podpróbkowanie / struktura pikseli\",\n\t\t\"resw\": \"rozdzielczość pozioma\",\n\t\t\"resh\": \"rozdzielczość pionowa\",\n\t\t\"chs\": \"kanały audio\",\n\t\t\"hz\": \"częstotliwość próbkowania\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"misc\",\n\t\t\t[\"ESC\", \"zamknij różne rzeczy\"],\n\n\t\t\t\"file-manager\",\n\t\t\t[\"G\", \"przełącz widok lista / siatka\"],\n\t\t\t[\"T\", \"przełącz miniaturki / ikony\"],\n\t\t\t[\"⇧ A/D\", \"wielkość miniaturki\"],\n\t\t\t[\"ctrl-K\", \"usuń zaznaczone\"],\n\t\t\t[\"ctrl-X\", \"wytnij zaznaczone do schowka\"],\n\t\t\t[\"ctrl-C\", \"skopiuj zaznaczone do schowka\"],\n\t\t\t[\"ctrl-V\", \"wklej (przenieś/skopiuj) tutaj\"],\n\t\t\t[\"Y\", \"pobierz zaznaczone\"],\n\t\t\t[\"F2\", \"zmień nazwę zaznaczonych\"],\n\n\t\t\t\"file-list-sel\",\n\t\t\t[\"spacja\", \"przełącz zaznaczanie plików\"],\n\t\t\t[\"↑/↓\", \"przenieś kursor zaznaczenia\"],\n\t\t\t[\"ctrl ↑/↓\", \"przenieś kursor i widok\"],\n\t\t\t[\"⇧ ↑/↓\", \"wybierz poprzedni/następny plik\"],\n\t\t\t[\"ctrl-A\", \"wybierz wszystkie pliki/foldery\"],\n\t\t], [\n\t\t\t\"navigation\",\n\t\t\t[\"B\", \"przełącz ścieżkę nawigacyjną / panel nawigacyjny\"],\n\t\t\t[\"I/K\", \"poprzedni/następny folder\"],\n\t\t\t[\"M\", \"folder nadrzędny (lub zwiń aktualny)\"],\n\t\t\t[\"V\", \"przełącz foldery / pliki tekstowe w panelu nawigacyjnym\"],\n\t\t\t[\"A/D\", \"rozmiar panelu nawigacyjnego\"],\n\t\t], [\n\t\t\t\"audio-player\",\n\t\t\t[\"J/L\", \"poprzedni/następny utwór\"],\n\t\t\t[\"U/O\", \"przejdź 10 sek. do tyłu/przodu\"],\n\t\t\t[\"0..9\", \"przeskocz do 0%..90%\"],\n\t\t\t[\"P\", \"odtwórz/pauza (również rozpoczyna)\"],\n\t\t\t[\"S\", \"wybierz odtwarzany utwór\"],\n\t\t\t[\"Y\", \"pobierz utwór\"],\n\t\t], [\n\t\t\t\"image-viewer\",\n\t\t\t[\"J/L, ←/→\", \"poprzednie/następne zdjęcie\"],\n\t\t\t[\"Home/End\", \"pierwsze/ostatnie zdjęcie\"],\n\t\t\t[\"F\", \"pełny ekran\"],\n\t\t\t[\"R\", \"obróć zgodnie ze wskaz. zegara\"],\n\t\t\t[\"⇧ R\", \"obróć przeciwnie do ruchu wskaz. zegara\"],\n\t\t\t[\"S\", \"wybierz zdjęcie\"],\n\t\t\t[\"Y\", \"pobierz zdjęcie\"],\n\t\t], [\n\t\t\t\"video-player\",\n\t\t\t[\"U/O\", \"przejdź 10 sek. do tyłu/przodu\"],\n\t\t\t[\"P/K/Spacja\", \"odtwórz/pauza\"],\n\t\t\t[\"C\", \"odtwarzaj następne po zakończeniu\"],\n\t\t\t[\"V\", \"odtwarzaj w pętli\"],\n\t\t\t[\"M\", \"wycisz\"],\n\t\t\t[\"[ i ]\", \"ustaw opóźnienie pętli\"],\n\t\t], [\n\t\t\t\"textfile-viewer\",\n\t\t\t[\"I/K\", \"poprzedni/następny plik\"],\n\t\t\t[\"M\", \"zamknij plik\"],\n\t\t\t[\"E\", \"edytuj plik\"],\n\t\t\t[\"S\", \"wybierz plik (do wycięcia/skopiowania/zmiany nazwy)\"],\n\t\t\t[\"Y\", \"pobierz plik tekstowy\"], //m\n\t\t\t[\"⇧ J\", \"upiększ json\"], //m\n\t\t]\n\t],\n\n\t\"m_ok\": \"OK\",\n\t\"m_ng\": \"Anuluj\",\n\n\t\"enable\": \"Włącz\",\n\t\"danger\": \"NIEBEZPIECZEŃSTWO\",\n\t\"clipped\": \"skopiowano do schowka\",\n\n\t\"ht_s1\": \"sekunda\",\n\t\"ht_s2\": \"sekundy\",\n\t\"ht_s5\": \"sekund\",\n\t\"ht_m1\": \"minuta\",\n\t\"ht_m2\": \"minuty\",\n\t\"ht_m5\": \"minut\",\n\t\"ht_h1\": \"godzina\",\n\t\"ht_h2\": \"godziny\",\n\t\"ht_h5\": \"godzin\",\n\t\"ht_d1\": \"dzień\",\n\t\"ht_d2\": \"dni\",\n\t\"ht_and\": \" i \",\n\n\t\"goh\": \"panel sterowania\",\n\t\"gop\": 'poprzedni plik/folder\">poprzedni',\n\t\"gou\": 'nadrzędny folder\">w górę',\n\t\"gon\": 'następny folder\">następny',\n\t\"logout\": \"Wyloguj \",\n\t\"login\": \"Zaloguj się\", //m\n\t\"access\": \" dostęp\",\n\t\"ot_close\": \"zamknij pod-menu\",\n\t\"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`&quot;try unite&quot;` = zawierać dokładnie «try unite»$N$Nformatem daty jest iso-8601, czyli$N`2009-12-31` lub `2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"unpost: usuń ostatnio przesłane pliki lub przerwij przesyłanie\",\n\t\"ot_bup\": \"bup: podstawowe przesyłanie danych, wspiera nawet netscape 4.0\",\n\t\"ot_mkdir\": \"mkdir: tworzy nowy folder\",\n\t\"ot_md\": \"new-file: tworzy nowy plik tekstowy\", //m\n\t\"ot_msg\": \"msg: wysyła wiadomość do loga serwera\",\n\t\"ot_mp\": \"opcje odtwarzacza multimediów\",\n\t\"ot_cfg\": \"opcje konfiguracji\",\n\t\"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ż [🎈]&nbsp; (podstawowe przesyłanie)<br /><br />podczas przesyłania ta ikona zamienia się w wskaźnik postępu!',\n\t\"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 [🎈]&nbsp; (podstawowego przesyłania)<br /><br />podczas przesyłania ta ikona zamienia się w wskaźnik postępu!',\n\t\"ot_noie\": 'Użyj przeglądarki Chrome / Firefox / Edge',\n\n\t\"ab_mkdir\": \"stwórz folder\",\n\t\"ab_mkdoc\": \"nowy plik tekstowy\", //m\n\t\"ab_msg\": \"wyślij wiad. do logów serwera\",\n\n\t\"ay_path\": \"przejdź do folderów\",\n\t\"ay_files\": \"przejdź do plików\",\n\n\t\"wt_ren\": \"zmień nazwę zaznaczonych elementów$NSkrót: F2\",\n\t\"wt_del\": \"usuń zaznaczone elementy$NSkrót: ctrl-K\",\n\t\"wt_cut\": \"wytnij zaznaczone elementy &lt;small&gt;(aby wkleić gdzie indziej)&lt;/small&gt;$NSkrót: ctrl-X\",\n\t\"wt_cpy\": \"skopiuj zaznaczone pliki do schowka$N(aby wkleić gdzie indziej)$NSkrót: ctrl-C\",\n\t\"wt_pst\": \"wklej wcześniej wycięte/skopiowane zaznaczenie$NSkrót: ctrl-V\",\n\t\"wt_selall\": \"zaznacz wszystko$NHotkey: ctrl-A (when file focused)\",\n\t\"wt_selinv\": \"odwróć zaznaczenie\",\n\t\"wt_zip1\": \"pobierz folder jako archiwum\",\n\t\"wt_selzip\": \"pobierz zaznaczone jako archiwum\",\n\t\"wt_seldl\": \"pobierz zaznaczenie jako oddzielne pliki$NSkrót: Y\",\n\t\"wt_npirc\": \"skopiuj informacje o utworze w formacie irc\",\n\t\"wt_nptxt\": \"skopiuj informacje o utworze jako zwykły tekst\",\n\t\"wt_m3ua\": \"dodaj to playlisty m3u (kliknij <code>📻copy</code> kliknij)\",\n\t\"wt_m3uc\": \"skopiuj playlistę m3u do schowka\",\n\t\"wt_grid\": \"przełącz widok siatki / listy$NSkrót: G\",\n\t\"wt_prev\": \"poprzeni utwór$NSkrót: J\",\n\t\"wt_play\": \"odtwórz / pauza$NSkrót: P\",\n\t\"wt_next\": \"następny utwór$NSkrót: L\",\n\n\t\"ul_par\": \"przesyłane równolegle:\",\n\t\"ut_rand\": \"losuj nazwy plików\",\n\t\"ut_u2ts\": \"kopiuj znacznik ostatniej modyfikacji$Nz twojego systemu plików na serwer\\\">📅\",\n\t\"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\n\t\"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\",\n\t\"ut_ask\": 'pytaj o potwierdzenie rozpoczęcia przesyłania\">💭',\n\t\"ut_pot\": \"przyspiesz przesyłanie na słabszych urządzeniach,$Nupraszczając interfejs\",\n\t\"ut_srch\": \"nie przesyłaj plików, jedynie sprawdź czy istnieją$Njuż na serwerze (przeskanuje wszystkie foldery dostępne do odczytu)\",\n\t\"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\",\n\t\"ul_btn\": \"upuść pliki / foldery<br>tutaj (lub kliknij mnie)\",\n\t\"ul_btnu\": \"P R Z E Ś L I J\",\n\t\"ul_btns\": \"S Z U K A J\",\n\n\t\"ul_hash\": \"hashowanie\",\n\t\"ul_send\": \"przesyłanie\",\n\t\"ul_done\": \"gotowe\",\n\t\"ul_idle1\": \"nic się jeszcze nie przesyła\",\n\t\"ut_etah\": \"średnia prędkość &lt;em&gt;hashowania&lt;/em&gt i przewidywany czas do końca\",\n\t\"ut_etau\": \"średnia prędkość &lt;em&gt;przesyłania&lt;/em&gt i przewidywany czas do końca\",\n\t\"ut_etat\": \"średnia prędkość &lt;em&gt;ogólna&lt;/em&gt i przewidywany czas do końca\",\n\n\t\"uct_ok\": \"zakończone pomyślnie\",\n\t\"uct_ng\": \"zakończono niepowodzeniem (odrzucono, nie znaleziono, itp.)\",\n\t\"uct_done\": \"zakończono z błędami\",\n\t\"uct_bz\": \"w trakcie (oblicznie sumy kontrolnej, przesyłanie)\",\n\t\"uct_q\": \"oczekujące\",\n\n\t\"utl_name\": \"nazwa pliku\",\n\t\"utl_ulist\": \"lista\",\n\t\"utl_ucopy\": \"kopia\",\n\t\"utl_links\": \"linki\",\n\t\"utl_stat\": \"status\",\n\t\"utl_prog\": \"postęp\",\n\n\t// keep short:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"BŁĄD\",\n\t\"utl_oserr\": \"błąd OS\",\n\t\"utl_found\": \"znaleziono\",\n\t\"utl_defer\": \"opóźnij\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"gotowe\",\n\n\t\"ul_flagblk\": \"pliki zostały zakolejkowane,</b><br>lecz przesyłanie up2k już trwa (w innej zakładce),<br>oczekuję na zakończenie\",\n\t\"ul_btnlk\": \"przełącznik zablokowany przez konfigurację serwera\",\n\n\t\"udt_up\": \"Prześlij\",\n\t\"udt_srch\": \"Szukaj\",\n\t\"udt_drop\": \"upuść tutaj\",\n\n\t\"u_nav_m\": '<h6>co my tu mamy?</h6><code>Enter</code> = Pliki (jeden lub wiecej)\\n<code>ESC</code> = Jeden folder (włącznie z podfolderami)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Pliki</a><a href=\"#\" id=\"modal-ng\">Jeden folder</a>',\n\n\t\"cl_opts\": \"przełączniki\",\n\t\"cl_hfsz\": \"rozmiar pliku\", //m\n\t\"cl_themes\": \"motyw\",\n\t\"cl_langs\": \"język\",\n\t\"cl_ziptype\": \"pobieranie folderów\",\n\t\"cl_uopts\": \"przełączniki przesyłania (up2k)\",\n\t\"cl_favico\": \"favicon (ikona w przeglądarce)\",\n\t\"cl_bigdir\": \"duże foldery\",\n\t\"cl_hsort\": \"#sortowanie\",\n\t\"cl_keytype\": \"notacja klucza\",  // not sure\n\t\"cl_hiddenc\": \"ukryte kolumny\",\n\t\"cl_hidec\": \"ukryj\",\n\t\"cl_reset\": \"zresetuj\",\n\t\"cl_hpick\": \"kliknij nagłówki kolumn, aby ukryć je w tabeli niżej\",\n\t\"cl_hcancel\": \"ukrywanie kolumn przerwane\",\n\t\"cl_rcm\": \"menu kontekstowe\", //m\n\n\t\"ct_grid\": '田 siatka',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ podpowiedzi',\n\t\"ct_thumb\": 'w widoku siatki, przełącz ikony i miniaturki$NSkrót: T\">🖼️ miniaturki',\n\t\"ct_csel\": 'użyj CTRL i SHIFT do wybierania plików w widoku siatki\">wybierz',\n\t\"ct_dsel\": 'użyj zaznaczania przez przeciąganie w widoku siatki\">przeciągnij', //m\n\t\"ct_dl\": 'wymuś pobieranie (nie wyświetlaj inline) po kliknięciu pliku\">dl', //m\n\t\"ct_ihop\": 'przejdź do ostatniego pliku po zamknięciu przeglądarki obrazów\">g⮯',\n\t\"ct_dots\": 'pokaż ukryte pliki (jeśli pozwala serwer)\">ukryte',\n\t\"ct_qdel\": 'pytaj o potwierdzenie przy usuwaniu tylko raz\">pyt. us.',\n\t\"ct_dir1st\": 'pokazuj foldery na początku\">📁 najpierw',\n\t\"ct_nsort\": 'naturalne sortowanie (dla numerowanych plików)\">nsort',\n\t\"ct_utc\": 'pokaż wszystkie daty/czas w UTC\">UTC',\n\t\"ct_readme\": 'pokazuj README.md w folderach\">📜 readme',\n\t\"ct_idxh\": 'pokazuj plik index.html zamiast zawartości folderu\">htm',\n\t\"ct_sbars\": 'pokazuj paski przewijania\">⟊',\n\n\t\"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\",\n\n\t\"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 <em>&quot;czy ten plik jest tego samego rozmiaru jak ten na serwerze?&quot;</em> 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 &quot;przesłać&quot; te same pliki ponownie w celu weryfikacji\\\">turbo\",\n\n\t\"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$N<em>teorytycznie</em> 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\",\n\n\t\"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?)\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cut_nag\": \"powiadomienie systemowe po zakończeniu przesyłania$N(tylko jeśli przeglądarka lub karta nie jest aktywna)\",\n\t\"cut_sfx\": \"sygnał dźwiękowy po zakończeniu przesyłania$N(tylko jeśli przeklądarka lub karta nie jest aktywna)\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cft_text\": \"tekst favicon (aby wyłączyć, usuń zawartość i przeładuj stronę)\",\n\t\"cft_fg\": \"kolor tekstu\",\n\t\"cft_bg\": \"kolor tła\",\n\n\t\"cdt_lim\": \"maksymalna liczba plików do pokazania na raz w folderze\",\n\t\"cdt_ask\": \"przy przewijaniu w dół,$Nzapytaj co robić,$Nzamiast wczytywać kolejne pliki\",\n\t\"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\",\n\t\"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\n\t\"cdt_rdb\": \"pokaż standardowe menu prawego przycisku, gdy niestandardowe jest już otwarte i nastąpi ponowne kliknięcie\\\">x2\", //m\n\n\t\"tt_entree\": \"pokaż panel nawigacyjny (panel boczny z drzewem folderów)$NSkrót: B\",\n\t\"tt_detree\": \"pokaż ślad nawigacyjny$NSkrót: B\",\n\t\"tt_visdir\": \"przewiń do wybranego folderu\",\n\t\"tt_ftree\": \"przełącz drzewo folderów / pliki tekstowe$NSkrót: V\",\n\t\"tt_pdock\": \"pokaż foldery nadrzędne w przypiętym u góry panelu\",\n\t\"tt_dynt\": \"rozszerzaj panel wraz z drzewem\",\n\t\"tt_wrap\": \"zawijaj tekst\",\n\t\"tt_hover\": \"pokazuj za długie linie po najechaniu kursorem$N( psuje przewijanie gdy $N&nbsp; kursor nie jest w lewym marginesie )\",\n\n\t\"ml_pmode\": \"na końcu folderu...\",\n\t\"ml_btns\": \"komendy\",\n\t\"ml_tcode\": \"transkoduj\",\n\t\"ml_tcode2\": \"transkoduj do\",\n\t\"ml_tint\": \"odcień\",\n\t\"ml_eq\": \"korektor dźwięku (equalizer)\",\n\t\"ml_drc\": \"kompresor zasięgu dynamiki\",\n\t\"ml_ss\": \"pomijaj ciszę\", //m\n\n\t\"mt_loop\": \"pętla/powtarzaj jeden utwór\\\">🔁\",\n\t\"mt_one\": \"zatrzymaj po jednym utworze\\\">1️⃣\",\n\t\"mt_shuf\": \"odtwarzaj losowo w każdym folderze\\\">🔀\",\n\t\"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▶\",\n\t\"mt_preload\": \"rozpocznij ładowanie kolejnego utworu blisko końca aktualnego w celu uzyskania odtwarzania bez przerw\\\">preload\",\n\t\"mt_prescan\": \"przechodzi do następnego folderu przed zakończeniem ostatniego utworu,$Naby udobruchać przeglądarkę,$Nżeby nie zatrzymała odtwarzania\\\">naw\",\n\t\"mt_fullpre\": \"próbuj zbuforować cały utwór;$N✅ włącz na <b>niestabilnych</b> połączeniach,$N❌ <b>wyłącz</b> na wolnych połączeniach\\\">pełnebuf\",\n\t\"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ć)\\\">☕️\",\n\t\"mt_waves\": \"falisty pasek:$Npokazuj amplitudę dźwięku w pasku utworu\\\">~s\",\n\t\"mt_npclip\": \"pokaż przyciski kopiowania aktualnie odtwarzanego utworu\\\">/np\",\n\t\"mt_m3u_c\": \"pokaż przyciski kopiowania$Nwybranych piosenek jako playlista m3u8\\\">📻\",\n\t\"mt_octl\": \"integracja z systemem operacyjnym (przyciski multimedialne / informacje o utworze)\\\">os-int\",\n\t\"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\",\n\t\"mt_oscv\": \"pokaż okładkę albumu w widoku systemu\\\">okładka\",\n\t\"mt_follow\": \"podążaj za odtwarzanym utworem przewijając widok\\\">🎯\",\n\t\"mt_compact\": \"kompaktowe sterowanie\\\">⟎\",\n\t\"mt_uncache\": \"wyczyść pamięć podręczną &nbsp;(spróbuj jeśli przeglądarka$Nzachowała zepsutą kopię utworu, przez co nie odtwarza się ona)\\\">uncache\",\n\t\"mt_mloop\": \"odtwarzaj utwory w folderze w pętli\\\">🔁 loop\",\n\t\"mt_mnext\": \"wczytaj następny folder i kontynuuj\\\">📂 next\",\n\t\"mt_mstop\": \"zatrzymaj odtwarzanie\\\">⏸ stop\",\n\t\"mt_cflac\": \"przekonwertuj format flac / wav na {0}\\\">flac\",\n\t\"mt_caac\": \"przekonwertuj format aac / m4a na {0}\\\">aac\",\n\t\"mt_coth\": \"przekonwertuj wszystkie inne formaty (nie będące mp3) na {0}\\\">oth\",\n\t\"mt_c2opus\": \"najlepszy wybór dla komputerów, laptopów i urządzeń z androidem\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, dla iOS 17.5 i nowszych\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, dla iOS 11 do 17\\\">caf\",\n\t\"mt_c2mp3\": \"używaj na bardzo starych urządzeniach\\\">mp3\",\n\t\"mt_c2flac\": \"najlepsza jakość dźwięku, ale ogromne pliki do pobrania\\\">flac\", //m\n\t\"mt_c2wav\": \"nieskompresowane odtwarzanie (jeszcze większe)\\\">wav\", //m\n\t\"mt_c2ok\": \"cudownie, dobry wybór\",\n\t\"mt_c2nd\": \"ten format nie jest rekomendowany dla twojego urządzenia, ale nadal jest w porządku\",\n\t\"mt_c2ng\": \"wygląda na to, że to urządzenie nie wspiera tego formatu, lecz spróbujmy i tak\",\n\t\"mt_xowa\": \"iOS zawiera błędy uniemożliwiające odtwarzanie w tle używając tego formatu; wybierz caf lub mp3\",\n\t\"mt_tint\": \"jasność tła (0-100) paska,$Naby zmniejszyć widoczność buforowania\",\n\t\"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 &nbsp;` = standardowe stereo (niezmodyfikowane)$Nwidth `0.5` = 50% crossfeed lewo-prawo$Nwidth `0 &nbsp;` = mono$N$Nboost `-0.8` &amp; 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)\",\n\t\"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)\",\n\t\"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\n\t\"mt_ssvt\": \"próg głośności (0-255)\\\">gł\", //m\n\t\"mt_ssts\": \"aktywny próg (% utworu, początek)\\\">pocz\", //m\n\t\"mt_sste\": \"aktywny próg (% utworu, koniec)\\\">kon\", //m\n\t\"mt_sssm\": \"mnożnik prędkości odtwarzania\\\">szyb\", //m\n\n\t\"mb_play\": \"odtwórz\",\n\t\"mm_hashplay\": \"odtworzyć ten plik audio?\",\n\t\"mm_m3u\": \"naciśnij <code>Enter/OK</code>, aby odtworzyć\\nnaciśnij <code>ESC/Anuluj</code>, aby edytować\",\n\t\"mp_breq\": \"wymagany jest Firefox 82+, Chrome 73+ lub iOS 15+\",\n\t\"mm_bload\": \"wczytywanie...\",\n\t\"mm_bconv\": \"konwertowanie do {0}, proszę czekać...\",\n\t\"mm_opusen\": \"ta przeglądarka nie może odtwarzać plików aac / m4a;\\ntranskodowanie do formatu opus włączone\",\n\t\"mm_playerr\": \"odtwarzanie nie powiodło się: \",\n\t\"mm_eabrt\": \"Odtwarzanie zostało przerwane\",\n\t\"mm_enet\": \"Połączenie z internetem jest słabe\",\n\t\"mm_edec\": \"Ten plik wydaje się uszkodzony??\",\n\t\"mm_esupp\": \"Twoja przeglądarka nie rozumie tego formatu audio\",\n\t\"mm_eunk\": \"Nieznany błąd\",\n\t\"mm_e404\": \"Nie można odtworzyć; błąd 404: Nie znaleziono pliku.\",\n\t\"mm_e403\": \"Nie można odtworzyć; błąd 403: Odmowa dostępu.\\n\\nSpróbuj przeładować stronę (F5), może cię wylogowało\",\n\t\"mm_e415\": \"Nie można odtworzyć; błąd 415: Konwersja pliku nie powiodła się; sprawdź logi serwera.\", //m\n\t\"mm_e500\": \"Nie można odtworzyć; błąd 500: Sprawdź logi serwera.\",\n\t\"mm_e5xx\": \"Nie można odtworzyć; błąd serwera\",\n\t\"mm_nof\": \"nie znaleziono więcej plików audio\",\n\t\"mm_prescan\": \"Szukanie kolejnego utworu...\",\n\t\"mm_scank\": \"Znaleziono następną piosenkę:\",\n\t\"mm_uncache\": \"wyczyszczono pamięć podręczną; wszystkie utwory zostaną pobrane ponownie przy następnym odtworzeniu\",\n\t\"mm_hnf\": \"ten utwór już nie istnieje\",\n\n\t\"im_hnf\": \"ten obraz już nie istnieje\",\n\n\t\"f_empty\": 'ten folder jest pusty',\n\t\"f_chide\": 'schowa kolumnę «{0}»\\n\\nkolumny można ponownie pokazać w zakładce ustwaień',\n\t\"f_bigtxt\": \"ten plik waży {0} MiB -- na pewno pokazać jako tekst?\",\n\t\"f_bigtxt2\": \"odczytać jedynie koniec pliku? włączy również śledzenie, pokazując nowo-dodane linie tekstu w czasie rzeczywistym\",\n\t\"fbd_more\": '<div id=\"blazy\">pokazuję <code>{0}</code> z <code>{1}</code> plików; <a href=\"#\" id=\"bd_more\">pokaż {2}</a> lub <a href=\"#\" id=\"bd_all\">pokaż wszystko</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\">pokazuję <code>{0}</code> z <code>{1}</code> files; <a href=\"#\" id=\"bd_all\">pokaż wszystko</a></div>',\n\t\"f_anota\": \"{0} z {1} elementów zostało wybranych;\\naby pokazać cały folder, zjedź na dół\",\n\n\t\"f_dls\": 'linki do plików w aktualnym folderze\\nzostały zmienione w linki pobierania',\n\t\"f_dl_nd\": 'pomijanie folderu (użyj zamiast tego pobierania zip/tar):\\n', //m\n\n\t\"f_partial\": \"Aby bezpiecznie pobrać plik, który aktualnie jest przesyłany, wybierz plik o tej samej nazwie, lecz bez rozszerzenia <code>.PARTIAL</code>. Żeby to zrobić, naciśnij ANULUJ lub klawisz ESC.\\n\\nWciśnięcie OK / Enter zignoruje to ostrzeżenie i pobierze plik tymczasowy <code>.PARTIAL</code>, który prawie z pewnością będzie zepsuty\",\n\n\t\"ft_paste\": \"wklej {0} elementów$NSkrót: ctrl-V\",\n\t\"fr_eperm\": 'nie można zmienić nazwy:\\nnie posiadasz uprawnienia „move” w tym folderze',\n\t\"fd_eperm\": 'nie można usunąć:\\nnie posiadasz uprawnienia „delete” w tym folderze',\n\t\"fc_eperm\": 'nie można wyciąć:\\nnie posiadasz uprawnienia „move” w tym folderze',\n\t\"fp_eperm\": 'nie można wkleić:\\nnie posiadasz uprawnienia „write” w tym folderze',\n\t\"fr_emore\": \"wybierz przynajmniej jeden element do zmiany nazwy\",\n\t\"fd_emore\": \"wybierz przynajmniej jeden element do usunięcia\",\n\t\"fc_emore\": \"wybierz przynajmniej jeden element do wycięcia\",\n\t\"fcp_emore\": \"wybierz przynajmniej jeden element do skopiowania\",\n\n\t\"fs_sc\": \"udostępnij ten folder\",\n\t\"fs_ss\": \"udostępnij zaznaczone pliki\",\n\t\"fs_just1d\": \"nie można wybrać więcej niż jednego folderu,\\nani mieszać plików i folderów w jednym zaznaczeniu\",\n\t\"fs_abrt\": \"❌ przerwij\",\n\t\"fs_rand\": \"🎲 losuj nazwę\",\n\t\"fs_go\": \"✅ stwórz udostępnienie\",\n\t\"fs_name\": \"nazwa\",\n\t\"fs_src\": \"źródło\",\n\t\"fs_pwd\": \"hasło\",\n\t\"fs_exp\": \"wygaśnięcie\",\n\t\"fs_tmin\": \"min\",\n\t\"fs_thrs\": \"godz.\",\n\t\"fs_tdays\": \"dni\",\n\t\"fs_never\": \"na zawsze\",\n\t\"fs_pname\": \"opcjonalna nazwa linku; zostanie wylosowana jeśli pusta\",\n\t\"fs_tsrc\": \"plik lub folder do udostępnienia\",\n\t\"fs_ppwd\": \"hasło (opcjonalnie)\",\n\t\"fs_w8\": \"udostępnianie...\",\n\t\"fs_ok\": \"naciśnij <code>Enter/OK</code>, aby skopiować do schowka\\nnaciśnij <code>ESC/Anuluj</code>, aby zamknąć\",\n\n\t\"frt_dec\": \"może naprawić niektóre zepsute nazwy plików\\\">dekoduj-url\",\n\t\"frt_rst\": \"zresetuj zmodyfikowane nazwy plików do oryginalnych\\\">↺ zresetuj\",\n\t\"frt_abrt\": \"przerwij i zamknij to okno\\\">❌ anuluj\",\n\t\"frb_apply\": \"ZASTOSUJ ZMIANĘ NAZWY\",\n\t\"fr_adv\": \"zmiana nazwy hurtowa / metadanych / wzorcem\\\">zaawansowane\",\n\t\"fr_case\": \"rozróżnianie wielkości liter w regex\\\">wlit\",\n\t\"fr_win\": \"nazwy bezpieczne dla systemu Windows; zamienia symbole <code>&lt;&gt;:&quot;\\\\|?*</code> na japońskie odpowiedniki\\\">win\",\n\t\"fr_slash\": \"zamienia <code>/</code> symbolem, który nie tworzy nowych folderów\\\">brak /\",\n\t\"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.\",\n\t\"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)\",\n\t\"fr_pdel\": \"usuń\",\n\t\"fr_pnew\": \"zapisz jako\",\n\t\"fr_pname\": \"podaj nazwę nowego szablonu\",\n\t\"fr_aborted\": \"anulowano\",\n\t\"fr_lold\": \"poprzednia nazwa\",\n\t\"fr_lnew\": \"nowa nazwa\",\n\t\"fr_tags\": \"znaczniki dla wybranych plików (tylko do odczytu, w celach informacyjnych):\",\n\t\"fr_busy\": \"zmienianie nazwy {0} plików...\\n\\n{1}\",\n\t\"fr_efail\": \"zmiana nazwy zakończona niepowodzeniem:\\n\",\n\t\"fr_nchg\": \"{0} nowych nazw zostało zmienionych przez opcje <code>win</code> i/lub <code>brak /</code>\\n\\nKontynuować ze zmienionymi nazwami?\",\n\n\t\"fd_ok\": \"usunięto\",\n\t\"fd_err\": \"usuwanie zakończone niepowodzeniem:\\n\",\n\t\"fd_none\": \"nie usunięto nic; usunięcie mogło zostać zablokowane przez konfigurację serwera (xbd)?\",\n\t\"fd_busy\": \"usuwanie {0} elementów...\\n\\n{1}\",\n\t\"fd_warn1\": \"USUNĄĆ {0} elementów?\",\n\t\"fd_warn2\": \"<b>OSTATNIA SZANSA!</b> Tej operacji nie da się cofnąć. Usunąć?\",\n\n\t\"fc_ok\": \"wycięto {0} elementów\",\n\t\"fc_warn\": 'wycięto {0} elementów,\\n\\nlecz można je wkleić tylko w <b>tej</b> karcie\\n(ze względu na ogromną ilość wybranych elementów)',\n\n\t\"fcc_ok\": \"skopiowano {0} elementów do schowka\",\n\t\"fcc_warn\": 'skopiowano {0} elementów,\\n\\nlecz można je wkleić tylko w <b>tej</b> karcie\\n(ze względu na ogromną ilość wybranych elementów)',\n\n\t\"fp_apply\": \"zastosuj te nazwy\",\n\t\"fp_skip\": \"pomiń konflikty\", //m\n\t\"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\",\n\t\"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\n\t\"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\n\t\"fp_emore\": \"pozostało jeszcze kilka kolizji nazw plików do poprawy\",\n\t\"fp_ok\": \"przeniesiono\",\n\t\"fcp_ok\": \"przekopiowano\",\n\t\"fp_busy\": \"przenoszenie {0} elementów...\\n\\n{1}\",\n\t\"fcp_busy\": \"kopiowanie {0} elementów...\\n\\n{1}\",\n\t\"fp_abrt\": \"przerywanie...\", //m\n\t\"fp_err\": \"nie udało się przenieść:\\n\",\n\t\"fcp_err\": \"nie udało się skopiować:\\n\",\n\t\"fp_confirm\": \"przenieść tutaj {0} elementy(ów)?\",\n\t\"fcp_confirm\": \"skopiować tutaj {0} elementy(ów)?\",\n\t\"fp_etab\": 'nie udało się odczytać schowka z innej karty przeglądarki',\n\t\"fp_name\": \"przesyłanie pliku z twojego urządzenia. Nadaj nazwę:\",\n\t\"fp_both_m\": '<h6>wybierz metodę wklejenia</h6><code>Enter</code> = Przenieś {0} pliki(ów) z «{1}»\\n<code>ESC</code> = Prześlij {2} pliki(ów) z twojego urządzenia',\n\t\"fcp_both_m\": '<h6>wybierz metodę wklejenia</h6><code>Enter</code> = Skopiuj {0} pliki(ów) z «{1}»\\n<code>ESC</code> = Prześlij {2} pliki(ów) z twojego urządzenia',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Przenieś</a><a href=\"#\" id=\"modal-ng\">Prześlij</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Kopiuj</a><a href=\"#\" id=\"modal-ng\">Prześlij</a>',\n\n\t\"mk_noname\": \"wpisz nazwę do pola po lewej zanim to zrobisz :p\",\n\t\"nmd_i1\": \"możesz też dodać wybrane rozszerzenie, np. <code>.md</code>\", //m\n\t\"nmd_i2\": \"możesz tworzyć tylko pliki <code>.{0}</code>, ponieważ nie masz uprawnień do usuwania\", //m\n\n\t\"tv_load\": \"Wczytywanie pliku tekstowego:\\n\\n{0}\\n\\n{1}% (wczytano {2} z {3} MiB)\",\n\t\"tv_xe1\": \"nie udało się wczytać pliku:\\n\\nbłąd \",\n\t\"tv_xe2\": \"404, nie znaleziono pliku\",\n\t\"tv_lst\": \"lista plików tekstowych w\",\n\t\"tvt_close\": \"powróć do widoku folderów$NSkrót: M (lub Esc)\\\">❌ zamknij\",\n\t\"tvt_dl\": \"pobierz ten plik$NHotkey: Y\\\">💾 pobierz\",\n\t\"tvt_prev\": \"pokaż poprzedni dokument$NSkrót: i\\\">⬆ poprzedni\",\n\t\"tvt_next\": \"pokaż następny dokument$NSkrót: K\\\">⬇ następny\",\n\t\"tvt_sel\": \"wybierz plik &nbsp; ( do wycięcia / skopiowania / usunięcia / itp. )$NSkrót: S\\\">wyb\",\n\t\"tvt_j\": \"upiększ json$NSkrót: shift-J\\\">j\", //m\n\t\"tvt_edit\": \"otwórz plik w edytorze tekstu$NSkrót: E\\\">✏️ edytuj\",\n\t\"tvt_tail\": \"śledź zmiany w pliku; pokazuj nowe linie w czasie rzeczywistym\\\">📡 śledź\",\n\t\"tvt_wrap\": \"zawijaj tekst\\\">↵\",\n\t\"tvt_atail\": \"utrzymuj widok na dole strony\\\">⚓\",\n\t\"tvt_ctail\": \"dekoduj kolory terminala (sekwencje sterujące ANSI)\\\">🌈\",\n\t\"tvt_ntail\": \"limit przewijania (ile bajtów tekstu przechowywać w pamięci)\",\n\n\t\"m3u_add1\": \"dodano utwór do playlisty m3u\",\n\t\"m3u_addn\": \"dodano {0} utwory(ów) do playlisty m3u\",\n\t\"m3u_clip\": \"skopiowano playlistę m3u do schowka\\n\\nutwórz\",\n\n\t\"gt_vau\": \"nie pokazuj obrazu, odtwarzaj tylko dźwięk\\\">🎧\",\n\t\"gt_msel\": \"wybierz pliki; kliknij plik z wciśniętym klawiszem CTRL, aby zastąpić$N$N&lt;em&gt;gdy tryb jest aktywny, kliknij dwukrotnie na plik / folder, żeby go otworzyć&lt;/em&gt;$N$NSkrót: S\\\">wybierz wiele\",\n\t\"gt_crop\": \"kadruj miniaturki do środka\\\">kadruj\",\n\t\"gt_3x\": \"miniaturki w wysokiej rozdzielczości\\\">3x\",\n\t\"gt_zoom\": \"przybliż\",\n\t\"gt_chop\": \"przytnij\",\n\t\"gt_sort\": \"sortuj według\",\n\t\"gt_name\": \"nazwa\",\n\t\"gt_sz\": \"rozmiar\",\n\t\"gt_ts\": \"data\",\n\t\"gt_ext\": \"typ\",\n\t\"gt_c1\": \"przycinaj większą część nazw plików (pokazuj mniej)\",\n\t\"gt_c2\": \"przycinaj mniejszą część nazw plików (pokazuj więcej)\",\n\n\t\"sm_w8\": \"wyszukiwanie...\",\n\t\"sm_prev\": \"wyniki wyszukiwania poniżej pochodzą z poprzedniego zapytania:\\n  \",\n\t\"sl_close\": \"zamknij wyniki wyszukiwania\",\n\t\"sl_hits\": \"pokazuję {0} wyniki(ów)\",\n\t\"sl_moar\": \"pokaż więcej\",\n\n\t\"s_sz\": \"rozmiar\",\n\t\"s_dt\": \"data\",\n\t\"s_rd\": \"ścieżka\",\n\t\"s_fn\": \"nazwa\",\n\t\"s_ta\": \"znaczniki\",\n\t\"s_ua\": \"data przesłania\",\n\t\"s_ad\": \"zaawansowane\",\n\t\"s_s1\": \"min. rozmiar (MiB)\",\n\t\"s_s2\": \"maks. rozmiar (MiB)\",\n\t\"s_d1\": \"min. data iso8601\",\n\t\"s_d2\": \"maks. data iso8601\",\n\t\"s_u1\": \"przesłane po\",\n\t\"s_u2\": \"i/lub przed\",\n\t\"s_r1\": \"ścieżka zawiera &nbsp; (oddzielone spacją)\",\n\t\"s_f1\": \"nazwa zawiera &nbsp; (odwróć za pomocą -nope)\",\n\t\"s_t1\": \"znaczniki zawierają &nbsp; (^=start, koniec=$)\",\n\t\"s_a1\": \"dokładne właściwości metadanych\",\n\n\t\"md_eshow\": \"nie można wyświetlić \",\n\t\"md_off\": \"[📜<em>readme</em>] wyłączone w [⚙️] -- dokument ukryty\",\n\n\t\"badreply\": \"Nie udało się przeanalizować odpowiedzi serwera\",\n\n\t\"xhr403\": \"403: Odmowa dostępu.\\n\\nSpróbuj przeładować stronę (F5), możliwe, że cię wylogowano\",\n\t\"xhr0\": \"nieznany (być może utracono połączenie z serwerem, lub jest on nieaktywny)\",\n\t\"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ę\",\n\t\"tl_xe1\": \"nie można wyświetlić podfolderów:\\n\\nbłąd \",\n\t\"tl_xe2\": \"404: Nie znaleziono folderu\",\n\t\"fl_xe1\": \"nie można wyświetlić plików w folderze:\\n\\nbłąd \",\n\t\"fl_xe2\": \"404: Nie znaleziono folderu\",\n\t\"fd_xe1\": \"nie można stworzyć podfolderu:\\n\\nbłąd \",\n\t\"fd_xe2\": \"404: Nie znaleziono folderu nadrzędnego\",\n\t\"fsm_xe1\": \"nie można wysłać wiadomości:\\n\\nbłąd \",\n\t\"fsm_xe2\": \"404: Nie znaleziono folderu nadrzędnego\",\n\t\"fu_xe1\": \"nie udało się wczytać listy unpost z serwera:\\n\\nbłąd \",\n\t\"fu_xe2\": \"404: Nie znaleziono pliku??\",\n\n\t\"fz_tar\": \"nieskompresowane archiwum gnu-tar (linux / mac)\",\n\t\"fz_pax\": \"nieskompresowane archiwum tar w formacie pax (wolniejsze)\",\n\t\"fz_targz\": \"gnu-tar z kompresją gzip poziomu 3.,$N$Nzazwyczaj bardzo wolne, używaj nieskompresowanego tar\",\n\t\"fz_tarxz\": \"gnu-tar z kompresją xz poziomu 3.$N$Nzazwyczaj bardzo wolne, używaj nieksompresowanego tar\",\n\t\"fz_zip8\": \"zip z nazwami plików UTF-8 (może działać nieprawidłowo na systemie Windows 7 i starszych)\",\n\t\"fz_zipd\": \"zip z nazwami plików cp437, dobre dla bardzo starego oprogramowania\",\n\t\"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)\",\n\n\t\"un_m1\": \"można usunąć ostatnio przesłane pliki (lub przerwać trwające) poniżej\",\n\t\"un_upd\": \"odśwież\",\n\t\"un_m4\": \"lub udostępnij pliki widoczne poniżej:\",\n\t\"un_ulist\": \"pokaż\",\n\t\"un_ucopy\": \"kopiuj\",\n\t\"un_flt\": \"filtruj (opcjonalnie):&nbsp; URL musi zawierać\",\n\t\"un_fclr\": \"wyczyść kryteria filtrowania\",\n\t\"un_derr\": 'nie udało się usunąć unpost:\\n',\n\t\"un_f5\": 'coś poszło nie tak, spróbuj odświeżyć lub wciśnij F5',\n\t\"un_uf5\": \"przed przerwaniem przesyłania trzeba odświeżyć stronę (za pomocą CTRL-R lub F5)\",\n\t\"un_nou\": '<b>ostrzeżenie:</b> serwer jest aktualnie zbyt obciążony, żeby pokazać niedokończone przesłania; kliknij link \"odśwież\" za chwilę',\n\t\"un_noc\": '<b>ostrzeżenie:</b> unpost w pełni przesłanych plików jest wyłączone/zabronione w konfiguracji serwera',\n\t\"un_max\": \"pokazuję pierwsze 2000 plików (użyj filtrowania)\",\n\t\"un_avail\": \"{0} ostatnio przesłanych elementów może zostać usunięte<br />{1} niedokończonych można przerwać\",\n\t\"un_m2\": \"przesortowano po czasie przesłania; najnowsze elementy pierwsze: \",\n\t\"un_no1\": \"cholibka! żaden przesłany element nie jest wystarczająco niedawny\",\n\t\"un_no2\": \"cholibka! żaden przesłany element pasujący do filtra nie jest wystarczająco niedawny\",\n\t\"un_next\": \"usuń następne {0} pliki(ów) poniżej\",\n\t\"un_abrt\": \"przerwij\",\n\t\"un_del\": \"usuń\",\n\t\"un_m3\": \"wczytywanie ostatnio przesłanych elementów...\",\n\t\"un_busy\": \"usuwanie {0} plików...\",\n\t\"un_clip\": \"skopiowano {0} linków do schowka\",\n\n\t\"u_https1\": \"powinieneś przejść\",\n\t\"u_https2\": \"na HTTPS w celu\",\n\t\"u_https3\": \"uzyskania lepszej wydajności\",\n\t\"u_ancient\": 'twoja przeglądarka jest niezwykle zabytkowa -- powinieneś zamiast tego <a href=\"#\" onclick=\"goto(\\'bup\\')\">użyć bup</a>',\n\t\"u_nowork\": \"wymaga Firefox 53+, Chrome 57+ lub iOS 11+\",\n\t\"tail_2old\": \"wymaga Firefox 105+, Chrome 71+ lub iOS 14.5+\",\n\t\"u_nodrop\": 'ta przeglądarka jest za stara, nie wspiera przesyłania \"przeciągnij i upuść\"',\n\t\"u_notdir\": \"to nie jest folder!\\n\\nta przeglądarka jest za stara\\nspróbuj przeciągnąć i upuścić\",\n\t\"u_uri\": \"aby przeciągnąć i upuścić obrazy z innych okien przeglądarki,\\nupuść je na duży przycisk przesyłania\",\n\t\"u_enpot\": 'przełącz na <a href=\"#\">lekki interfejs</a> (może zwiększyć prędkość przesyłania)',\n\t\"u_depot\": 'przełącz na <a href=\"#\">ładny interfejs</a> (może zmniejszyć prędkośc przesyłania)',\n\t\"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!',\n\t\"u_pott\": \"<p>pliki: &nbsp; <b>{0}</b> ukończonych, &nbsp; <b>{1}</b> nie powiodło się, &nbsp; <b>{2}</b> w trakcie, &nbsp; <b>{3}</b> oczekujących</p>\",\n\t\"u_ever\": \"podstawowe przesyłanie; up2k wymaga minimalnie przeglądarek:<br>Chrome 21 // Firefox 13 // Edge 12 // Opera 12 // Safari 5.1\",\n\t\"u_su2k\": 'podstawowe przesyłanie; <a href=\"#\" id=\"u2yea\">up2k</a> jest lepszy',\n\t\"u_uput\": 'optymalizuj dla prędkości (pomijając spr. sum kontrolnych)',\n\t\"u_ewrite\": 'nie masz dostępu do zapisu (write) w tym folderze',\n\t\"u_eread\": 'nie masz dostępu do odczytu (read) tego folderu',\n\t\"u_enoi\": 'wyszukiwanie plików jest wyłączone w konfiguracji serwera',\n\t\"u_enoow\": \"nadpisanie nie zadziała, wymagany dostęp do usuwania (delete)\",\n\t\"u_badf\": '{0} (z {1}) plików zostało pominiętych, prawdopodobnie przez opcje dostępu systemu plików:\\n\\n',\n\t\"u_blankf\": '{0} (z {1}) plików jest pustych; przesłać mimo to?\\n\\n',\n\t\"u_applef\": '{0} (z {1}) plików może być niepożądane;\\nNaciśnij <code>OK/Enter</code>, aby pominąć je (wypisane poniżej);\\nNaciśnij <code>Anuluj/ESC</code>, by je przesłać mimo to:\\n\\n',\n\t\"u_just1\": '\\nTa funkcja może działać lepiej z wybranym jednym plikiem',\n\t\"u_ff_many\": \"na systemach <b>Linux / MacOS / Android,</b> ta ilośc plików <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\"><em>może</em> spowodować przymusowe zamknięcie przeglądarki Firefox</a>\\nw takim przypadku, spróbuj ponownie (lub użyj Chrome'a).\",\n\t\"u_up_life\": \"Ten przesyłany plik zostanie usunięty z serwera\\n{0} po zakończeniu przesyłania\",\n\t\"u_asku\": 'prześlij {0} pliki(ów) do <code>{1}</code>',\n\t\"u_unpt\": \"można cofnąć / usunąć ten przesłany plik za pomocą 🧯 w lewym górnym rogu\",\n\t\"u_bigtab\": 'zaraz pokażę {0} plików\\n\\nta operacja może zawiesić twoją przeglądarkę, na pewno kontynuować?',\n\t\"u_scan\": 'Skanowanie plików...',\n\t\"u_dirstuck\": 'iterator katalogów utknął podczas próby dostępu poniższych {0} elementów, pominięto:',\n\t\"u_etadone\": 'Ukończono ({0}, {1} plików)',\n\t\"u_etaprep\": '(przygotowywanie do przesłania)',\n\t\"u_hashdone\": 'obliczono sumę kontrolną',\n\t\"u_hashing\": 'obliczanie sumy kontrolnej',\n\t\"u_hs\": 'nawiązywanie połączenia...',\n\t\"u_started\": \"rozpoczęto przesyłanie; zobacz w [🚀]\",\n\t\"u_dupdefer\": \"duplikat; zostanie przetworzony na końcu\",\n\t\"u_actx\": \"kliknij ten napis, aby zapobiec spadkowi <br />wydajności po zmianie aktywnego okna/karty przeglądarki\",\n\t\"u_fixed\": \"OK!&nbsp; Naprawiono 👍\",\n\t\"u_cuerr\": \"nie udało się przesłać fragmentu {0} z {1};\\nprawdopodobnie niegroźne, kontynuowanie\\n\\nplik: {2}\",\n\t\"u_cuerr2\": \"serwer odrzucił przesyłanie (kawałek {0} z {1});\\nspróbuję ponownie później\\n\\nplik: {2}\\n\\nbłąd \",\n\t\"u_ehstmp\": \"spróbuję ponownie; więcej informacji w prawym dolnym rogu\",\n\t\"u_ehsfin\": \"serwer odrzucił prośbę o zakończenie przesyłania; próbuję ponownie...\",\n\t\"u_ehssrch\": \"serwer odrzucił prośbę o wykonanie wyszukania; próbuję ponownie...\",\n\t\"u_ehsinit\": \"serwer odrzucił prośbę o rozpoczęcie przesyłania; próbuję ponownie...\",\n\t\"u_eneths\": \"błąd sieci podczas negocjacji warunków przesyłania; próbuję ponownie...\",\n\t\"u_enethd\": \"błąd sieci podczas sprawdzania istnienia celu; próbuję ponownie...\",\n\t\"u_cbusy\": \"oczekiwanie na ponowne zaufanie serwera po błędzie sieci...\",\n\t\"u_ehsdf\": \"brak miejsca na dysku serwera!\\n\\npróby będą ponawiane na wypadek\\nzwolnienia wystarczająco dużo miejsca aby kontynuować\",\n\t\"u_emtleak1\": \"wygląda na to, że twoja przeglądarka może mieć wyciek pamięci;\\n\",\n\t\"u_emtleak2\": ' <a href=\"{0}\">przejdź na HTTPS (zalecane)</a> lub ',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": 'spróbuj:\\n<ul><li>wciśnij <code>F5</code>, aby odświeżyć stronę</li><li>wyłącz przycisk &nbsp;<code>ww</code>&nbsp; w menu <code>⚙️ ustawienia</code></li><li>i spróbuj przesłać ponownie</li></ul>Prędkość przesyłania będzie niższa, ale cóż zrobisz.\\nPrzepraszamy za problemu!\\n\\nPS: Chrome v107 <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">ma poprawkę tego błędu</a>.',\n\t\"u_emtleakf\": 'spróbuj:\\n<ul><li>wciśnij <code>F5</code>, aby odświeżyć stronę</li><li>włącz tryb <code>🥔</code> (lekkiego interfejsu) w interfejsie przesyłania<li>i spróbuj przesłać ponownie</li></ul>\\nPS: Firefox może kiedyś mieć <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">poprawkę tego błędu</a>',\n\t\"u_s404\": \"nie znaleziono na serwerze\",\n\t\"u_expl\": \"wytłumacz\",\n\t\"u_maxconn\": \"większość przeglądarek ogranicza to do 6, ale Firefox pozwala zwiększyć tą wartość, ustawiając <code>connections-per-server</code> w <code>about:config</code>\",\n\t\"u_tu\": '<p class=\"warn\">UWAGA: tryb turbo włączony, <span>&nbsp;klient może nie wykryć i nie kontynuować niedokończonych przesłań; patrz wskazówka przycisku turbo</span></p>',\n\t\"u_ts\": '<p class=\"warn\">UWAGA: tryb turbo włączony, <span>&nbsp;wyniki wyszukiwania mogą być niepoprawne; patrz wskazówka przycisku turbo</span></p>',\n\t\"u_turbo_c\": \"tryb turbo jest wyłączony w konfiguracji serwera\",\n\t\"u_turbo_g\": \"wyłączanie trybu turbo, nie posiadasz dostępu\\ndo listy katalogu w tym wolumenie\",\n\t\"u_life_cfg\": 'autousuwanie po <input id=\"lifem\" p=\"60\" /> min (lub <input id=\"lifeh\" p=\"3600\" /> godz.)',\n\t\"u_life_est\": 'przesłany plik zostanie usunięty <span id=\"lifew\" tt=\"local time\">---</span>',\n\t\"u_life_max\": 'ten folder wymaga\\nmaks. czasu do usunięcia równego {0}',\n\t\"u_unp_ok\": 'unpost jest dozwolony przez {0}',\n\t\"u_unp_ng\": 'unpost NIE jest dozwolony',\n\t\"ue_ro\": 'dostęp tylko-do-odczytu\\n\\n',\n\t\"ue_nl\": 'nie jesteś zalogowany',\n\t\"ue_la\": 'zalogowano jako \"{0}\"',\n\t\"ue_sr\": 'jesteś w trybie wyszukiwania\\n\\nprzełącz się na tryb przesyłania, klikając lupę 🔎 (obok przycisku Szukaj), i spróbuj ponownie',\n\t\"ue_ta\": 'spróbuj przesłać ponownie, wszystko powinno być w porządku',\n\t\"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\",\n\t\"ur_1uo\": \"OK: Plik przesłany pomyślnie\",\n\t\"ur_auo\": \"OK: Wszystkie ({0}) pliki zostały przesłane pomyślnie\",\n\t\"ur_1so\": \"OK: Znaleziono plik na serwerze\",\n\t\"ur_aso\": \"OK: Znaleziono wszystkie ({0}) pliki na serwerze\",\n\t\"ur_1un\": \"Przesyłanie nie powiodło się\",\n\t\"ur_aun\": \"Wszystkie ({0}) przesłania nie powiodły się\",\n\t\"ur_1sn\": \"NIE znaleziono pliku na serwerze\",\n\t\"ur_asn\": \"NIE znaleziono {0} plików na serwerze\",\n\t\"ur_um\": \"Zakończono;\\n{0} przesłań OK,\\n{1} przesłań nie powiodło się\",\n\t\"ur_sm\": \"Zakończono;\\nznaleziono {0} pliki(ów),\\nnie znaleziono {1} pliki(ów) na serwerze\",\n\n\t\"rc_opn\": \"otwórz\", //m\n\t\"rc_ply\": \"odtwórz\", //m\n\t\"rc_pla\": \"odtwórz jako dźwięk\", //m\n\t\"rc_txt\": \"otwórz w przeglądarce plików\", //m\n\t\"rc_md\": \"otwórz w edytorze tekstu\", //m\n\t\"rc_dl\": \"pobierz\", //m\n\t\"rc_zip\": \"pobierz jako archiwum\", //m\n\t\"rc_cpl\": \"kopiuj link\", //m\n\t\"rc_del\": \"usuń\", //m\n\t\"rc_cut\": \"wytnij\", //m\n\t\"rc_cpy\": \"kopiuj\", //m\n\t\"rc_pst\": \"wklej\", //m\n\t\"rc_rnm\": \"zmień nazwę\", //m\n\t\"rc_nfo\": \"nowy folder\", //m\n\t\"rc_nfi\": \"nowy plik\", //m\n\t\"rc_sal\": \"zaznacz wszystko\", //m\n\t\"rc_sin\": \"odwróć zaznaczenie\", //m\n\t\"rc_shf\": \"udostępnij ten folder\", //m\n\t\"rc_shs\": \"udostępnij zaznaczenie\", //m\n\n\t\"lang_set\": \"odśwież stronę (F5), aby zastosować zmianę.\",\n\n\t\"splash\": {\n\t\t\"a1\": \"odśwież\",\n\t\t\"b1\": \"witaj, nieznajomy &nbsp; <small>(nie jesteś zalogowany)</small>\",\n\t\t\"c1\": \"wyloguj się\",\n\t\t\"d1\": \"zrzut stosu\",\n\t\t\"d2\": \"pokazuje status wszystkich aktywnych wątków\",\n\t\t\"e1\": \"przeładuj konfigurację\",\n\t\t\"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ć\",\n\t\t\"f1\": \"możesz przeglądać:\",\n\t\t\"g1\": \"możesz przesyłać do:\",\n\t\t\"cc1\": \"inne:\",\n\t\t\"h1\": \"wyłącz k304\",\n\t\t\"i1\": \"włącz k304\",\n\t\t\"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, <em>ale</em> spowolni ogólne działanie\",\n\t\t\"k1\": \"zresetuj ustawienia klienta\",\n\t\t\"l1\": \"zaloguj się po więcej:\",\n\t\t\"ls3\": \"zaloguj się\", //m\n\t\t\"lu4\": \"nazwa użytkownika\", //m\n\t\t\"lp4\": \"hasło\", //m\n\t\t\"lo3\": \"wyloguj “{0}” wszędzie\", //m\n\t\t\"lo2\": \"spowoduje to zakończenie sesji we wszystkich przeglądarkach\", //m\n\t\t\"m1\": \"Witaj,\",\n\t\t\"n1\": \"404 nie znaleziono &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": 'lub możesz nie mieć dostępu -- spróbuj wprowadzić hasło lub <a href=\"' + SR + '/?h\">przejdź do strony głównej</a>',\n\t\t\"p1\": \"403 odmowa dostępu &nbsp;~┻━┻\",\n\t\t\"q1\": 'użyj hasła lub <a href=\"' + SR + '/?h\">przejdź do strony głównej</a>',\n\t\t\"r1\": \"idź do strony głównej\",\n\t\t\".s1\": \"przeskanuj ponownie\",\n\t\t\"t1\": \"akcje\",\n\t\t\"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\",\n\t\t\"v1\": \"połącz\",\n\t\t\"v2\": \"używaj tego serwera jako dysku lokalnego\",\n\t\t\"w1\": \"przejdź na HTTPS\",\n\t\t\"x1\": \"zmień hasło\",\n\t\t\"y1\": \"edytuj udostępnione\",\n\t\t\"z1\": \"odblokuj udostępnienie:\",\n\t\t\"ta1\": \"najpierw wprowadź nowe hasło\",\n\t\t\"ta2\": \"powtórz hasło dla potwierdzenia:\",\n\t\t\"ta3\": \"znaleziono literówkę, spróbuj ponownie\",\n\t\t\"nop\": \"BŁĄD: Hasło nie może być puste\", //m\n\t\t\"nou\": \"BŁĄD: Nazwa użytkownika i/lub hasło nie może być puste\", //m\n\t\t\"aa1\": \"pliki przychodzące:\",\n\t\t\"ab1\": \"wyłącz no304\",\n\t\t\"ac1\": \"włącz no304\",\n\t\t\"ad1\": \"włączenie no304 wyłączy przechowywanie jakiejkolwiek pamięci podręcznej. Zmarnuje to olbrzymią ilość ruchu sieciowego!\",\n\t\t\"ae1\": \"trwające pobierania:\",\n\t\t\"af1\": \"pokaż ostatnio przesłane pliki\",\n\t\t\"ag1\": \"pokaż znanych użytkowników IdP\",\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/por.js",
    "content": "\n// Linhas que terminam com //m são traduções automáticas não verificadas\n\nLs.por = {\n\t\"tt\": \"Português\",\n\n\t\"cols\": {\n\t\t\"c\": \"botões de ação\",\n\t\t\"dur\": \"duração\",\n\t\t\"q\": \"qualidade / bitrate\",\n\t\t\"Ac\": \"codec de áudio\",\n\t\t\"Vc\": \"codec de vídeo\",\n\t\t\"Fmt\": \"formato / contêiner\",\n\t\t\"Ahash\": \"checksum de áudio\",\n\t\t\"Vhash\": \"checksum de vídeo\",\n\t\t\"Res\": \"resolução\",\n\t\t\"T\": \"tipo de arquivo\",\n\t\t\"aq\": \"qualidade / bitrate de áudio\",\n\t\t\"vq\": \"qualidade / bitrate de vídeo\",\n\t\t\"pixfmt\": \"subamostragem / estrutura de pixel\",\n\t\t\"resw\": \"resolução horizontal\",\n\t\t\"resh\": \"resolução vertical\",\n\t\t\"chs\": \"canais de áudio\",\n\t\t\"hz\": \"taxa de amostragem\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"diversos\",\n\t\t\t[\"ESC\", \"fechar várias coisas\"],\n\n\t\t\t\"gerenciador de arquivos\",\n\t\t\t[\"G\", \"alternar entre visualização de lista / grade\"],\n\t\t\t[\"T\", \"alternar entre miniaturas / ícones\"],\n\t\t\t[\"⇧ A/D\", \"tamanho da miniatura\"],\n\t\t\t[\"ctrl-K\", \"excluir selecionados\"],\n\t\t\t[\"ctrl-X\", \"recortar seleção para a área de transferência\"],\n\t\t\t[\"ctrl-C\", \"copiar seleção para a área de transferência\"],\n\t\t\t[\"ctrl-V\", \"colar (mover/copiar) aqui\"],\n\t\t\t[\"Y\", \"baixar selecionado\"],\n\t\t\t[\"F2\", \"renomear selecionado\"],\n\n\t\t\t\"seleção de lista de arquivos\",\n\t\t\t[\"espaço\", \"alternar seleção de arquivo\"],\n\t\t\t[\"↑/↓\", \"mover cursor de seleção\"],\n\t\t\t[\"ctrl ↑/↓\", \"mover cursor e visualização\"],\n\t\t\t[\"⇧ ↑/↓\", \"selecionar arquivo anterior/próximo\"],\n\t\t\t[\"ctrl-A\", \"selecionar todos os arquivos / pastas\"],\n\t\t], [\n\t\t\t\"navegação\",\n\t\t\t[\"B\", \"alternar entre breadcrumbs / painel de navegação\"],\n\t\t\t[\"I/K\", \"pasta anterior/próxima\"],\n\t\t\t[\"M\", \"pasta pai (ou desexpandir a atual)\"],\n\t\t\t[\"V\", \"alternar entre pastas / arquivos de texto no painel de navegação\"],\n\t\t\t[\"A/D\", \"tamanho do painel de navegação\"],\n\t\t], [\n\t\t\t\"reprodutor de áudio\",\n\t\t\t[\"J/L\", \"música anterior/próxima\"],\n\t\t\t[\"U/O\", \"pular 10 segundos para trás/frente\"],\n\t\t\t[\"0..9\", \"pular para 0%..90%\"],\n\t\t\t[\"P\", \"reproduzir/pausar (também inicia)\"],\n\t\t\t[\"S\", \"selecionar a música que está tocando\"],\n\t\t\t[\"Y\", \"baixar música\"],\n\t\t], [\n\t\t\t\"visualizador de imagens\",\n\t\t\t[\"J/L, ←/→\", \"imagem anterior/próxima\"],\n\t\t\t[\"Home/End\", \"primeira/última imagem\"],\n\t\t\t[\"F\", \"tela cheia\"],\n\t\t\t[\"R\", \"girar no sentido horário\"],\n\t\t\t[\"⇧ R\", \"girar no sentido anti-horário\"],\n\t\t\t[\"S\", \"selecionar imagem\"],\n\t\t\t[\"Y\", \"baixar imagem\"],\n\t\t], [\n\t\t\t\"reprodutor de vídeo\",\n\t\t\t[\"U/O\", \"pular 10 segundos para trás/frente\"],\n\t\t\t[\"P/K/Espaço\", \"reproduzir/pausar\"],\n\t\t\t[\"C\", \"continuar reproduzindo o próximo\"],\n\t\t\t[\"V\", \"loop\"],\n\t\t\t[\"M\", \"mudo\"],\n\t\t\t[\"[ e ]\", \"definir intervalo de loop\"],\n\t\t], [\n\t\t\t\"visualizador de arquivos de texto\",\n\t\t\t[\"I/K\", \"arquivo anterior/próximo\"],\n\t\t\t[\"M\", \"fechar arquivo de texto\"],\n\t\t\t[\"E\", \"editar arquivo de texto\"],\n\t\t\t[\"S\", \"selecionar arquivo (para recortar/copiar/renomear)\"],\n\t\t\t[\"Y\", \"baixar arquivo de texto\"],\n\t\t\t[\"⇧ J\", \"embelezar json\"],\n\t\t]\n\t],\n\n\t\"m_ok\": \"OK\",\n\t\"m_ng\": \"Cancelar\",\n\n\t\"enable\": \"Ativar\",\n\t\"danger\": \"PERIGO\",\n\t\"clipped\": \"copiado para a área de transferência\",\n\n\t\"ht_s1\": \"segundo\",\n\t\"ht_s2\": \"segundos\",\n\t\"ht_m1\": \"minuto\",\n\t\"ht_m2\": \"minutos\",\n\t\"ht_h1\": \"hora\",\n\t\"ht_h2\": \"horas\",\n\t\"ht_d1\": \"dia\",\n\t\"ht_d2\": \"dias\",\n\t\"ht_and\": \" e \",\n\n\t\"goh\": \"painel de controle\",\n\t\"gop\": 'pai anterior\">anterior',\n\t\"gou\": 'pasta pai\">acima',\n\t\"gon\": 'próxima pasta\">próximo',\n\t\"logout\": \"Sair \",\n\t\"login\": \"Fazer login\",\n\t\"access\": \" acesso\",\n\t\"ot_close\": \"fechar submenu\",\n\t\"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`&quot;try unite&quot;` = conter exatamente «try unite»$N$No formato de data é iso-8601, como$N`2009-12-31` ou `2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"despublicar: excluir seus uploads recentes, ou abortar os que não foram concluídos\",\n\t\"ot_bup\": \"bup: uploader básico, até suporta netscape 4.0\",\n\t\"ot_mkdir\": \"mkdir: criar um novo diretório\",\n\t\"ot_md\": \"new-file: criar um novo arquivo de texto\",\n\t\"ot_msg\": \"msg: enviar uma mensagem para o log do servidor\",\n\t\"ot_mp\": \"opções do reprodutor de mídia\",\n\t\"ot_cfg\": \"opções de configuração\",\n\t\"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 [🎈]&nbsp; (o uploader básico)<br /><br />durante os uploads, este ícone se torna um indicador de progresso!',\n\t\"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 [🎈]&nbsp; (o uploader básico)<br /><br />durante os uploads, este ícone se torna um indicador de progresso!',\n\t\"ot_noie\": 'Por favor, use Chrome / Firefox / Edge',\n\n\t\"ab_mkdir\": \"criar diretório\",\n\t\"ab_mkdoc\": \"novo arquivo de texto\",\n\t\"ab_msg\": \"enviar msg para o log do srv\",\n\n\t\"ay_path\": \"pular para pastas\",\n\t\"ay_files\": \"pular para arquivos\",\n\n\t\"wt_ren\": \"renomear itens selecionados$NHotkey: F2\",\n\t\"wt_del\": \"excluir itens selecionados$NHotkey: ctrl-K\",\n\t\"wt_cut\": \"recortar itens selecionados &lt;small&gt;(depois colar em outro lugar)&lt;/small&gt;$NHotkey: ctrl-X\",\n\t\"wt_cpy\": \"copiar itens selecionados para a área de transferência$N(para colá-los em outro lugar)$NHotkey: ctrl-C\",\n\t\"wt_pst\": \"colar uma seleção previamente recortada / copiada$NHotkey: ctrl-V\",\n\t\"wt_selall\": \"selecionar todos os arquivos$NHotkey: ctrl-A (quando o arquivo estiver em foco)\",\n\t\"wt_selinv\": \"inverter seleção\",\n\t\"wt_zip1\": \"baixar esta pasta como um arquivo compactado\",\n\t\"wt_selzip\": \"baixar seleção como um arquivo compactado\",\n\t\"wt_seldl\": \"baixar seleção como arquivos separados$NHotkey: Y\",\n\t\"wt_npirc\": \"copiar informações da faixa em formato irc\",\n\t\"wt_nptxt\": \"copiar informações da faixa em texto simples\",\n\t\"wt_m3ua\": \"adicionar à playlist m3u (clique em <code>📻copiar</code> depois)\",\n\t\"wt_m3uc\": \"copiar playlist m3u para a área de transferência\",\n\t\"wt_grid\": \"alternar entre visualização de grade / lista$NHotkey: G\",\n\t\"wt_prev\": \"faixa anterior$NHotkey: J\",\n\t\"wt_play\": \"reproduzir / pausar$NHotkey: P\",\n\t\"wt_next\": \"próxima faixa$NHotkey: L\",\n\n\t\"ul_par\": \"uploads paralelos:\",\n\t\"ut_rand\": \"randomizar nomes de arquivos\",\n\t\"ut_u2ts\": \"copiar o carimbo de data/hora de última modificação$Ndo seu sistema de arquivos para o servidor\\\">📅\",\n\t\"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\",\n\t\"ut_mt\": \"continuar a fazer o hash de outros arquivos enquanto faz upload$N$Ntalvez desativar se sua CPU ou HDD for um gargalo\",\n\t\"ut_ask\": 'pedir confirmação antes do upload começar\">💭',\n\t\"ut_pot\": \"melhorar a velocidade de upload em dispositivos lentos$Ntornando a UI menos complexa\",\n\t\"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)\",\n\t\"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\",\n\t\"ul_btn\": \"soltar arquivos / pastas<br>aqui (ou clique em mim)\",\n\t\"ul_btnu\": \"U P L O A D\",\n\t\"ul_btns\": \"B U S C A R\",\n\n\t\"ul_hash\": \"hash\",\n\t\"ul_send\": \"enviar\",\n\t\"ul_done\": \"feito\",\n\t\"ul_idle1\": \"nenhum upload está na fila ainda\",\n\t\"ut_etah\": \"velocidade média de &lt;em&gt;hash&lt;/em&gt;, e tempo estimado até o fim\",\n\t\"ut_etau\": \"velocidade média de &lt;em&gt;upload&lt;/em&gt; e tempo estimado até o fim\",\n\t\"ut_etat\": \"velocidade média &lt;em&gt;total&lt;/em&gt; e tempo estimado até o fim\",\n\n\t\"uct_ok\": \"concluído com sucesso\",\n\t\"uct_ng\": \"ruim: falhou / rejeitado / não encontrado\",\n\t\"uct_done\": \"ok e ruim combinados\",\n\t\"uct_bz\": \"fazendo hash ou upload\",\n\t\"uct_q\": \"ocioso, pendente\",\n\n\t\"utl_name\": \"nome do arquivo\",\n\t\"utl_ulist\": \"lista\",\n\t\"utl_ucopy\": \"copiar\",\n\t\"utl_links\": \"links\",\n\t\"utl_stat\": \"status\",\n\t\"utl_prog\": \"progresso\",\n\n\t// mantenha curto:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"ERRO\",\n\t\"utl_oserr\": \"Erro-SO\",\n\t\"utl_found\": \"encontrado\",\n\t\"utl_defer\": \"adiar\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"feito\",\n\n\t\"ul_flagblk\": \"os arquivos foram adicionados à fila</b><br>no entanto, há um up2k ocupado em outra aba do navegador,<br>então esperando que ele termine primeiro\",\n\t\"ul_btnlk\": \"a configuração do servidor bloqueou este interruptor neste estado\",\n\n\t\"udt_up\": \"Upload\",\n\t\"udt_srch\": \"Buscar\",\n\t\"udt_drop\": \"solte aqui\",\n\n\t\"u_nav_m\": '<h6>certo, o que você tem?</h6><code>Enter</code> = Arquivos (um ou mais)\\n<code>ESC</code> = Uma pasta (incluindo subpastas)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Arquivos</a><a href=\"#\" id=\"modal-ng\">Uma pasta</a>',\n\n\t\"cl_opts\": \"interruptores\",\n\t\"cl_hfsz\": \"tamanho do arquivo\", \n\t\"cl_themes\": \"tema\",\n\t\"cl_langs\": \"idioma\",\n\t\"cl_ziptype\": \"download de pasta\",\n\t\"cl_uopts\": \"interruptores up2k\",\n\t\"cl_favico\": \"favicon\",\n\t\"cl_bigdir\": \"grandes dirs\",\n\t\"cl_hsort\": \"#sort\",\n\t\"cl_keytype\": \"notação de tecla\",\n\t\"cl_hiddenc\": \"colunas ocultas\",\n\t\"cl_hidec\": \"ocultar\",\n\t\"cl_reset\": \"resetar\",\n\t\"cl_hpick\": \"toque nos cabeçalhos das colunas para ocultá-los na tabela abaixo\",\n\t\"cl_hcancel\": \"ocultar coluna abortado\",\n\t\"cl_rcm\": \"menu de clique direito\",\n\n\t\"ct_grid\": '田 a grade',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ dicas de ferramentas',\n\t\"ct_thumb\": 'na visualização de grade, alternar entre ícones ou miniaturas$NHotkey: T\">🖼️ miniaturas',\n\t\"ct_csel\": 'usar CTRL e SHIFT para seleção de arquivo na visualização de grade\">sel',\n\t\"ct_dsel\": 'usar seleção por arrasto na visualização de grade\">arrastar',\n\t\"ct_dl\": 'forçar download (não exibir na página) ao clicar em um arquivo\">dl',\n\t\"ct_ihop\": 'quando o visualizador de imagens for fechado, rolar para o último arquivo visualizado\">g⮯',\n\t\"ct_dots\": 'mostrar arquivos ocultos (se o servidor permitir)\">dotfiles',\n\t\"ct_qdel\": 'ao excluir arquivos, pedir confirmação apenas uma vez\">qdel',\n\t\"ct_dir1st\": 'ordenar pastas antes de arquivos\">📁 primeiro',\n\t\"ct_nsort\": 'ordem natural (para nomes de arquivos com dígitos iniciais)\">nsort',\n\t\"ct_utc\": 'mostrar todas as datas/horas em UTC\">UTC',\n\t\"ct_readme\": 'mostrar README.md nas listas de pastas\">📜 readme',\n\t\"ct_idxh\": 'mostrar index.html em vez de lista de pastas\">htm',\n\t\"ct_sbars\": 'mostrar barras de rolagem\">⟊',\n\n\t\"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📅\",\n\n\t\"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 <em>&quot;este arquivo tem o mesmo tamanho no servidor?&quot;</em> 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 &quot;enviar&quot; os mesmos arquivos novamente para permitir que o cliente os verifique\\\">turbo\",\n\n\t\"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 <em>teoricamente</em> pegar a maioria dos uploads incompletos / corrompidos, mas não é um substituto para fazer uma verificação com o turbo desativado depois\\\">date-chk\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cut_nag\": \"notificação do SO quando o upload for concluído$N(somente se o navegador ou aba não estiver ativo)\",\n\t\"cut_sfx\": \"alerta audível quando o upload for concluído$N(somente se o navegador ou aba não estiver ativo)\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cft_text\": \"texto do favicon (deixe em branco e atualize para desativar)\",\n\t\"cft_fg\": \"cor do primeiro plano\",\n\t\"cft_bg\": \"cor do fundo\",\n\n\t\"cdt_lim\": \"número máximo de arquivos para mostrar em uma pasta\",\n\t\"cdt_ask\": \"ao rolar para o final,$Nem vez de carregar mais arquivos,$Nperguntar o que fazer\",\n\t\"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\",\n\t\"cdt_ren\": \"ativar menu de clique direito personalizado, o menu normal permanece acessível com shift + clique direito\\\">ativar\",\n\t\"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\n\n\t\"tt_entree\": \"mostrar painel de navegação (árvore de diretórios)$NHotkey: B\",\n\t\"tt_detree\": \"mostrar breadcrumbs$NHotkey: B\",\n\t\"tt_visdir\": \"rolar para a pasta selecionada\",\n\t\"tt_ftree\": \"alternar entre árvore de pastas / arquivos de texto$NHotkey: V\",\n\t\"tt_pdock\": \"mostrar pastas pai em um painel acoplado no topo\",\n\t\"tt_dynt\": \"crescer automaticamente à medida que a árvore se expande\",\n\t\"tt_wrap\": \"quebra de linha\",\n\t\"tt_hover\": \"revelar linhas transbordando ao passar o mouse$N( quebra a rolagem a menos que o cursor do mouse $N&nbsp; esteja na margem esquerda )\",\n\n\t\"ml_pmode\": \"ao final da pasta...\",\n\t\"ml_btns\": \"comandos\",\n\t\"ml_tcode\": \"transcodificar\",\n\t\"ml_tcode2\": \"transcodificar para\",\n\t\"ml_tint\": \"matiz\",\n\t\"ml_eq\": \"equalizador de áudio\",\n\t\"ml_drc\": \"compressor de faixa dinâmica\",\n\t\"ml_ss\": \"ignorar silêncio\", //m\n\n\t\"mt_loop\": \"loop/repetir uma música\\\">🔁\",\n\t\"mt_one\": \"parar depois de uma música\\\">1️⃣\",\n\t\"mt_shuf\": \"embaralhar as músicas em cada pasta\\\">🔀\",\n\t\"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▶\",\n\t\"mt_preload\": \"começar a carregar a próxima música perto do final para uma reprodução sem interrupções\\\">preload\",\n\t\"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\",\n\t\"mt_fullpre\": \"tentar pré-carregar a música inteira;$N✅ ativar em conexões <b>não confiáveis</b>,$N❌ <b>desativar</b> em conexões lentas provavelmente\\\">full\",\n\t\"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)\\\">☕️\",\n\t\"mt_waves\": \"barra de busca de forma de onda:$Nmostrar amplitude de áudio no scrubber\\\">~s\",\n\t\"mt_npclip\": \"mostrar botões para copiar a música que está tocando para a área de transferência\\\">/np\",\n\t\"mt_m3u_c\": \"mostrar botões para copiar as$Nmúsicas selecionadas como entradas de playlist m3u8 para a área de transferência\\\">📻\",\n\t\"mt_octl\": \"integração com o SO (atalhos de mídia / osd)\\\">os-ctl\",\n\t\"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\",\n\t\"mt_oscv\": \"mostrar capa do álbum no osd\\\">art\",\n\t\"mt_follow\": \"manter a faixa que está tocando rolando à vista\\\">🎯\",\n\t\"mt_compact\": \"controles compactos\\\">⟎\",\n\t\"mt_uncache\": \"limpar cache &nbsp;(tente isso se seu navegador armazenou em cache$Numa cópia quebrada de uma música e se recusa a tocar)\\\">uncache\",\n\t\"mt_mloop\": \"loop na pasta aberta\\\">🔁 loop\",\n\t\"mt_mnext\": \"carregar a próxima pasta e continuar\\\">📂 próximo\",\n\t\"mt_mstop\": \"parar reprodução\\\">⏸ parar\",\n\t\"mt_cflac\": \"converter flac / wav para {0}\\\">flac\",\n\t\"mt_caac\": \"converter aac / m4a para {0}\\\">aac\",\n\t\"mt_coth\": \"converter todos os outros (não mp3) para {0}\\\">oth\",\n\t\"mt_c2opus\": \"melhor escolha para desktops, laptops, android\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, para iOS 17.5 e mais recentes\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, para iOS 11 a 17\\\">caf\",\n\t\"mt_c2mp3\": \"use isso em dispositivos muito antigos\\\">mp3\",\n\t\"mt_c2flac\": \"melhor qualidade de som, mas downloads enormes\\\">flac\",\n\t\"mt_c2wav\": \"reprodução não comprimida (ainda maior)\\\">wav\",\n\t\"mt_c2ok\": \"legal, boa escolha\",\n\t\"mt_c2nd\": \"esse não é o formato de saída recomendado para o seu dispositivo, mas tudo bem\",\n\t\"mt_c2ng\": \"seu dispositivo não parece suportar este formato de saída, mas vamos tentar mesmo assim\",\n\t\"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\",\n\t\"mt_tint\": \"nível de fundo (0-100) na barra de busca$Npara tornar o buffer menos distrativo\",\n\t\"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 &nbsp;` = estéreo padrão (não modificado)$Nlargura `0.5` = 50% de crossfeed esquerda-direita$Nlargura `0 &nbsp;` = 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\",\n\t\"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)\",\n\t\"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\n\t\"mt_ssvt\": \"limiar de volume (0-255)\\\">vol\", //m\n\t\"mt_ssts\": \"limiar ativo (% faixa, início)\\\">ini\", //m\n\t\"mt_sste\": \"limiar ativo (% faixa, fim)\\\">fim\", //m\n\t\"mt_sssm\": \"multiplicador de velocidade de reprodução\\\">av\", //m\n\n\t\"mb_play\": \"reproduzir\",\n\t\"mm_hashplay\": \"reproduzir este arquivo de áudio?\",\n\t\"mm_m3u\": \"pressione <code>Enter/OK</code> para Reproduzir\\npressione <code>ESC/Cancelar</code> para Editar\",\n\t\"mp_breq\": \"precisa do firefox 82+ ou chrome 73+ ou iOS 15+\",\n\t\"mm_bload\": \"carregando...\",\n\t\"mm_bconv\": \"convertendo para {0}, por favor, espere...\",\n\t\"mm_opusen\": \"seu navegador não pode reproduzir arquivos aac / m4a;\\na transcodificação para opus agora está ativada\",\n\t\"mm_playerr\": \"reprodução falhou: \",\n\t\"mm_eabrt\": \"A tentativa de reprodução foi cancelada\",\n\t\"mm_enet\": \"Sua conexão de internet está instável\",\n\t\"mm_edec\": \"Este arquivo está supostamente corrompido??\",\n\t\"mm_esupp\": \"Seu navegador não entende este formato de áudio\",\n\t\"mm_eunk\": \"Erro Desconhecido\",\n\t\"mm_e404\": \"Não foi possível reproduzir áudio; erro 404: Arquivo não encontrado.\",\n\t\"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\",\n\t\"mm_e415\": \"Não foi possível reproduzir áudio; erro 415: Falha na conversão do arquivo; verifique os logs do servidor.\",\n\t\"mm_e500\": \"Não foi possível reproduzir áudio; erro 500: Verifique os logs do servidor.\",\n\t\"mm_e5xx\": \"Não foi possível reproduzir áudio; erro do servidor \",\n\t\"mm_nof\": \"não encontrando mais arquivos de áudio por perto\",\n\t\"mm_prescan\": \"Procurando música para tocar a seguir...\",\n\t\"mm_scank\": \"Encontrei a próxima música:\",\n\t\"mm_uncache\": \"cache limpo; todas as músicas serão baixadas novamente na próxima reprodução\",\n\t\"mm_hnf\": \"essa música não existe mais\",\n\n\t\"im_hnf\": \"essa imagem não existe mais\",\n\n\t\"f_empty\": 'esta pasta está vazia',\n\t\"f_chide\": 'isso irá ocultar a coluna «{0}»\\n\\nvocê pode reexibir as colunas na aba de configurações',\n\t\"f_bigtxt\": \"este arquivo tem {0} MiB de tamanho -- realmente ver como texto?\",\n\t\"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\",\n\t\"fbd_more\": '<div id=\"blazy\">mostrando <code>{0}</code> de <code>{1}</code> arquivos; <a href=\"#\" id=\"bd_more\">mostrar {2}</a> ou <a href=\"#\" id=\"bd_all\">mostrar todos</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\">mostrando <code>{0}</code> de <code>{1}</code> arquivos; <a href=\"#\" id=\"bd_all\">mostrar todos</a></div>',\n\t\"f_anota\": \"apenas {0} dos {1} itens foram selecionados;\\npara selecionar a pasta inteira, primeiro role para o final\",\n\n\t\"f_dls\": 'os links de arquivo na pasta atual foram\\nalterados para links de download',\n\t\"f_dl_nd\": 'pulando pasta (use o download zip/tar em vez disso):\\n',\n\n\t\"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 <code>.PARTIAL</code>. Por favor, pressione CANCELAR ou Escape para fazer isso.\\n\\nPressionar OK / Enter irá ignorar este aviso e continuar baixando o arquivo temporário <code>.PARTIAL</code>, o que quase certamente lhe dará dados corrompidos.\",\n\n\t\"ft_paste\": \"colar {0} itens$NHotkey: ctrl-V\",\n\t\"fr_eperm\": 'não é possível renomear:\\nvocê não tem permissão de “mover” nesta pasta',\n\t\"fd_eperm\": 'não é possível excluir:\\nvocê não tem permissão de “excluir” nesta pasta',\n\t\"fc_eperm\": 'não é possível recortar:\\nvocê não tem permissão de “mover” nesta pasta',\n\t\"fp_eperm\": 'não é possível colar:\\nvocê não tem permissão de “escrever” nesta pasta',\n\t\"fr_emore\": \"selecione pelo menos um item para renomear\",\n\t\"fd_emore\": \"selecione pelo menos um item para excluir\",\n\t\"fc_emore\": \"selecione pelo menos um item para recortar\",\n\t\"fcp_emore\": \"selecione pelo menos um item para copiar para a área de transferência\",\n\n\t\"fs_sc\": \"compartilhar a pasta em que você está\",\n\t\"fs_ss\": \"compartilhar os arquivos selecionados\",\n\t\"fs_just1d\": \"você não pode selecionar mais de uma pasta,\\nou misturar arquivos e pastas em uma seleção\",\n\t\"fs_abrt\": \"❌ abortar\",\n\t\"fs_rand\": \"🎲 nome aleatório\",\n\t\"fs_go\": \"✅ criar compartilhamento\",\n\t\"fs_name\": \"nome\",\n\t\"fs_src\": \"fonte\",\n\t\"fs_pwd\": \"senha\",\n\t\"fs_exp\": \"expira\",\n\t\"fs_tmin\": \"min\",\n\t\"fs_thrs\": \"horas\",\n\t\"fs_tdays\": \"dias\",\n\t\"fs_never\": \"eterno\",\n\t\"fs_pname\": \"nome do link opcional; será aleatório se em branco\",\n\t\"fs_tsrc\": \"o arquivo ou pasta a ser compartilhado\",\n\t\"fs_ppwd\": \"senha opcional\",\n\t\"fs_w8\": \"criando compartilhamento...\",\n\t\"fs_ok\": \"pressione <code>Enter/OK</code> para Copiar para a Área de Transferência\\npressione <code>ESC/Cancelar</code> para Fechar\",\n\n\t\"frt_dec\": \"pode consertar alguns casos de nomes de arquivos quebrados\\\">url-decode\",\n\t\"frt_rst\": \"resetar nomes de arquivos modificados de volta para os originais\\\">↺ resetar\",\n\t\"frt_abrt\": \"abortar e fechar esta janela\\\">❌ cancelar\",\n\t\"frb_apply\": \"APLICAR RENOMEAÇÃO\",\n\t\"fr_adv\": \"renomeação em lote / metadados / padrão\\\">avançado\",\n\t\"fr_case\": \"regex sensível a maiúsculas e minúsculas\\\">case\",\n\t\"fr_win\": \"nomes seguros para windows; substituir <code>&lt;&gt;:&quot;\\\\|?*</code> por caracteres japoneses de largura total\\\">win\",\n\t\"fr_slash\": \"substituir <code>/</code> por um caractere que não cause a criação de novas pastas\\\">no /\",\n\t\"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\",\n\t\"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\",\n\t\"fr_pdel\": \"excluir\",\n\t\"fr_pnew\": \"salvar como\",\n\t\"fr_pname\": \"forneça um nome para seu novo preset\",\n\t\"fr_aborted\": \"abortado\",\n\t\"fr_lold\": \"nome antigo\",\n\t\"fr_lnew\": \"novo nome\",\n\t\"fr_tags\": \"tags para os arquivos selecionados (somente leitura, apenas para referência):\",\n\t\"fr_busy\": \"renomeando {0} itens...\\n\\n{1}\",\n\t\"fr_efail\": \"renomeação falhou:\\n\",\n\t\"fr_nchg\": \"{0} dos novos nomes foram alterados devido a <code>win</code> e/ou <code>no /</code>\\n\\nOK para continuar com estes novos nomes alterados?\",\n\n\t\"fd_ok\": \"exclusão OK\",\n\t\"fd_err\": \"exclusão falhou:\\n\",\n\t\"fd_none\": \"nada foi excluído; talvez bloqueado pela configuração do servidor (xbd)?\",\n\t\"fd_busy\": \"excluindo {0} itens...\\n\\n{1}\",\n\t\"fd_warn1\": \"EXCLUIR estes {0} itens?\",\n\t\"fd_warn2\": \"<b>Última chance!</b> Não há como desfazer. Excluir?\",\n\n\t\"fc_ok\": \"recortar {0} itens\",\n\t\"fc_warn\": 'recortar {0} itens\\n\\nmas: apenas <b>esta</b> aba do navegador pode colá-los\\n(já que a seleção é tão absolutamente massiva)',\n\n\t\"fcc_ok\": \"copiado {0} itens para a área de transferência\",\n\t\"fcc_warn\": 'copiado {0} itens para a área de transferência\\n\\nmas: apenas <b>esta</b> aba do navegador pode colá-los\\n(já que a seleção é tão absolutamente massiva)',\n\n\t\"fp_apply\": \"usar estes nomes\",\n\t\"fp_skip\": \"pular conflitos\",\n\t\"fp_ecut\": \"primeiro recorte ou copie alguns arquivos / pastas para colar / mover\\n\\nnota: você pode recortar / colar entre abas diferentes do navegador\",\n\t\"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:\",\n\t\"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:\",\n\t\"fp_emore\": \"ainda há algumas colisões de nome de arquivo para consertar\",\n\t\"fp_ok\": \"movimento OK\",\n\t\"fcp_ok\": \"cópia OK\",\n\t\"fp_busy\": \"movendo {0} itens...\\n\\n{1}\",\n\t\"fcp_busy\": \"copiando {0} itens...\\n\\n{1}\",\n\t\"fp_abrt\": \"abortando...\",\n\t\"fp_err\": \"movimento falhou:\\n\",\n\t\"fcp_err\": \"cópia falhou:\\n\",\n\t\"fp_confirm\": \"mover estes {0} itens para cá?\",\n\t\"fcp_confirm\": \"copiar estes {0} itens para cá?\",\n\t\"fp_etab\": 'falha ao ler a área de transferência de outra aba do navegador',\n\t\"fp_name\": \"enviando um arquivo do seu dispositivo. Dê-lhe um nome:\",\n\t\"fp_both_m\": '<h6>escolha o que colar</h6><code>Enter</code> = Mover {0} arquivos de «{1}»\\n<code>ESC</code> = Enviar {2} arquivos do seu dispositivo',\n\t\"fcp_both_m\": '<h6>escolha o que colar</h6><code>Enter</code> = Copiar {0} arquivos de «{1}»\\n<code>ESC</code> = Enviar {2} arquivos do seu dispositivo',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Mover</a><a href=\"#\" id=\"modal-ng\">Enviar</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Copiar</a><a href=\"#\" id=\"modal-ng\">Enviar</a>',\n\n\t\"mk_noname\": \"digite um nome no campo de texto à esquerda antes de fazer isso :p\",\n\t\"nmd_i1\": \"também adicione a extensão desejada, por exemplo <code>.md</code>\",\n\t\"nmd_i2\": \"só pode criar arquivos <code>.{0}</code> porque não tem permissão para apagar\",\n\n\t\"tv_load\": \"Carregando documento de texto:\\n\\n{0}\\n\\n{1}% ({2} de {3} MiB carregados)\",\n\t\"tv_xe1\": \"não foi possível carregar o arquivo de texto:\\n\\nerro \",\n\t\"tv_xe2\": \"404, arquivo não encontrado\",\n\t\"tv_lst\": \"lista de arquivos de texto em\",\n\t\"tvt_close\": \"voltar para a visualização da pasta$NHotkey: M (ou Esc)\\\">❌ fechar\",\n\t\"tvt_dl\": \"baixar este arquivo$NHotkey: Y\\\">💾 baixar\",\n\t\"tvt_prev\": \"mostrar documento anterior$NHotkey: i\\\">⬆ anterior\",\n\t\"tvt_next\": \"mostrar próximo documento$NHotkey: K\\\">⬇ próximo\",\n\t\"tvt_sel\": \"selecionar arquivo &nbsp; ( para recortar / copiar / excluir / ... )$NHotkey: S\\\">sel\",\n\t\"tvt_j\": \"embelezar json$NHotkey: shift-J\\\">j\",\n\t\"tvt_edit\": \"abrir arquivo no editor de texto$NHotkey: E\\\">✏️ editar\",\n\t\"tvt_tail\": \"monitorar arquivo para alterações; mostrar novas linhas em tempo real\\\">📡 seguir\",\n\t\"tvt_wrap\": \"quebra de linha\\\">↵\",\n\t\"tvt_atail\": \"fixar rolagem no final da página\\\">⚓\",\n\t\"tvt_ctail\": \"decodificar cores do terminal (códigos de escape ansi)\\\">🌈\",\n\t\"tvt_ntail\": \"limite de rolagem para trás (quantos bytes de texto manter carregados)\",\n\n\t\"m3u_add1\": \"música adicionada à playlist m3u\",\n\t\"m3u_addn\": \"{0} músicas adicionadas à playlist m3u\",\n\t\"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\",\n\n\t\"gt_vau\": \"não mostrar vídeos, apenas tocar o áudio\\\">🎧\",\n\t\"gt_msel\": \"ativar seleção de arquivo; ctrl-clique em um arquivo para substituir$N$N&lt;em&gt;quando ativo: clique duas vezes em um arquivo / pasta para abri-lo&&gt;t;/em&gt;$N$NHotkey: S\\\">multisseleção\",\n\t\"gt_crop\": \"cortar miniaturas ao centro\\\">cortar\",\n\t\"gt_3x\": \"miniaturas de alta resolução\\\">3x\",\n\t\"gt_zoom\": \"zoom\",\n\t\"gt_chop\": \"picar\",\n\t\"gt_sort\": \"ordenar por\",\n\t\"gt_name\": \"nome\",\n\t\"gt_sz\": \"tamanho\",\n\t\"gt_ts\": \"data\",\n\t\"gt_ext\": \"tipo\",\n\t\"gt_c1\": \"truncar nomes de arquivos mais (mostrar menos)\",\n\t\"gt_c2\": \"truncar nomes de arquivos menos (mostrar mais)\",\n\n\t\"sm_w8\": \"buscando...\",\n\t\"sm_prev\": \"os resultados da busca abaixo são de uma consulta anterior:\\n  \",\n\t\"sl_close\": \"fechar resultados da busca\",\n\t\"sl_hits\": \"mostrando {0} resultados\",\n\t\"sl_moar\": \"carregar mais\",\n\n\t\"s_sz\": \"tamanho\",\n\t\"s_dt\": \"data\",\n\t\"s_rd\": \"caminho\",\n\t\"s_fn\": \"nome\",\n\t\"s_ta\": \"tags\",\n\t\"s_ua\": \"up@\",\n\t\"s_ad\": \"adv.\",\n\t\"s_s1\": \"MiB mínimo\",\n\t\"s_s2\": \"MiB máximo\",\n\t\"s_d1\": \"iso8601 min.\",\n\t\"s_d2\": \"iso8601 max.\",\n\t\"s_u1\": \"enviado depois de\",\n\t\"s_u2\": \"e/ou antes de\",\n\t\"s_r1\": \"caminho contém &nbsp; (separado por espaço)\",\n\t\"s_f1\": \"nome contém &nbsp; (negar com -nope)\",\n\t\"s_t1\": \"tags contém &nbsp; (^=início, fim=$)\",\n\t\"s_a1\": \"propriedades de metadados específicas\",\n\n\t\"md_eshow\": \"não é possível renderizar \",\n\t\"md_off\": \"[📜<em>readme</em>] desativado em [⚙️] -- documento oculto\",\n\n\t\"badreply\": \"Falha ao analisar a resposta do servidor\",\n\n\t\"xhr403\": \"403: Acesso negado\\n\\ntente pressionar F5, talvez você tenha saído da conta\",\n\t\"xhr0\": \"desconhecido (provavelmente perdeu a conexão com o servidor, ou o servidor está offline)\",\n\t\"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\",\n\t\"tl_xe1\": \"não foi possível listar as subpastas:\\n\\nerro \",\n\t\"tl_xe2\": \"404: Pasta não encontrada\",\n\t\"fl_xe1\": \"não foi possível listar os arquivos na pasta:\\n\\nerro \",\n\t\"fl_xe2\": \"404: Pasta não encontrada\",\n\t\"fd_xe1\": \"não foi possível criar a subpasta:\\n\\nerro \",\n\t\"fd_xe2\": \"404: Pasta pai não encontrada\",\n\t\"fsm_xe1\": \"não foi possível enviar a mensagem:\\n\\nerro \",\n\t\"fsm_xe2\": \"404: Pasta pai não encontrada\",\n\t\"fu_xe1\": \"falha ao carregar a lista de despublicação do servidor:\\n\\nerro \",\n\t\"fu_xe2\": \"404: Arquivo não encontrado??\",\n\n\t\"fz_tar\": \"arquivo gnu-tar não comprimido (linux / mac)\",\n\t\"fz_pax\": \"tar de formato pax não comprimido (mais lento)\",\n\t\"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\",\n\t\"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\",\n\t\"fz_zip8\": \"zip com nomes de arquivos utf8 (pode ser instável no windows 7 e mais antigos)\",\n\t\"fz_zipd\": \"zip com nomes de arquivos cp437 tradicionais, para software realmente antigo\",\n\t\"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)\",\n\n\t\"un_m1\": \"você pode excluir seus uploads recentes (ou abortar os que não foram concluídos) abaixo\",\n\t\"un_upd\": \"atualizar\",\n\t\"un_m4\": \"ou compartilhar os arquivos visíveis abaixo:\",\n\t\"un_ulist\": \"mostrar\",\n\t\"un_ucopy\": \"copiar\",\n\t\"un_flt\": \"filtro opcional:&nbsp; a URL deve conter\",\n\t\"un_fclr\": \"limpar filtro\",\n\t\"un_derr\": 'a exclusão da despublicação falhou:\\n',\n\t\"un_f5\": 'algo quebrou, por favor, tente uma atualização ou pressione F5',\n\t\"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\",\n\t\"un_nou\": \"<b>aviso:</b> o servidor está muito ocupado para mostrar uploads incompletos; clique no link \\\"atualizar\\\" em um momento\",\n\t\"un_noc\": \"<b>aviso:</b> a despublicação de arquivos totalmente enviados não está ativada/permitida na configuração do servidor\",\n\t\"un_max\": \"mostrando os primeiros 2000 arquivos (use o filtro)\",\n\t\"un_avail\": \"{0} uploads recentes podem ser excluídos<br />{1} incompletos podem ser abortados\",\n\t\"un_m2\": \"ordenado por tempo de upload; o mais recente primeiro:\",\n\t\"un_no1\": \"sike! nenhum upload é suficientemente recente\",\n\t\"un_no2\": \"sike! nenhum upload que corresponda a esse filtro é suficientemente recente\",\n\t\"un_next\": \"excluir os próximos {0} arquivos abaixo\",\n\t\"un_abrt\": \"abortar\",\n\t\"un_del\": \"excluir\",\n\t\"un_m3\": \"carregando seus uploads recentes...\",\n\t\"un_busy\": \"excluindo {0} arquivos...\",\n\t\"un_clip\": \"{0} links copiados para a área de transferência\",\n\n\t\"u_https1\": \"você deveria\",\n\t\"u_https2\": \"mudar para https\",\n\t\"u_https3\": \"para um melhor desempenho\",\n\t\"u_ancient\": 'seu navegador é impressionantemente antigo -- talvez você devesse <a href=\"#\" onclick=\"goto(\\'bup\\')\">usar o bup em vez disso</a>',\n\t\"u_nowork\": \"precisa do firefox 53+ ou chrome 57+ ou iOS 11+\",\n\t\"tail_2old\": \"precisa do firefox 105+ ou chrome 71+ ou iOS 14.5+\",\n\t\"u_nodrop\": 'seu navegador é muito antigo para upload de arrastar e soltar',\n\t\"u_notdir\": \"isso não é uma pasta!\\n\\nseu navegador é muito antigo,\\npor favor, tente arrastar e soltar em vez disso\",\n\t\"u_uri\": \"para arrastar e soltar imagens de outras janelas do navegador,\\npor favor, solte-as no grande botão de upload\",\n\t\"u_enpot\": 'mudar para <a href=\"#\">UI batata</a> (pode melhorar a velocidade de upload)',\n\t\"u_depot\": 'mudar para <a href=\"#\">UI chique</a> (pode reduzir a velocidade de upload)',\n\t\"u_gotpot\": 'mudando para a UI batata para uma velocidade de upload melhorada,\\n\\nsinta-se à vontade para discordar e voltar!',\n\t\"u_pott\": \"<p>arquivos: &nbsp; <b>{0}</b> concluídos, &nbsp; <b>{1}</b> falhados, &nbsp; <b>{2}</b> ocupados, &nbsp; <b>{3}</b> na fila</p>\",\n\t\"u_ever\": \"este é o uploader básico; up2k precisa de pelo menos<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1\",\n\t\"u_su2k\": 'este é o uploader básico; <a href=\"#\" id=\"u2yea\">up2k</a> é melhor',\n\t\"u_uput\": 'otimizar para velocidade (pular checksum)',\n\t\"u_ewrite\": 'você não tem acesso de escrita a esta pasta',\n\t\"u_eread\": 'você não tem acesso de leitura a esta pasta',\n\t\"u_enoi\": 'a busca de arquivos não está ativada na configuração do servidor',\n\t\"u_enoow\": \"substituir não funcionará aqui; precisa de permissão de Excluir\",\n\t\"u_badf\": 'Estes {0} arquivos (de um total de {1}) foram ignorados, possivelmente devido a permissões do sistema de arquivos:\\n\\n',\n\t\"u_blankf\": 'Estes {0} arquivos (de um total de {1}) estão em branco / vazios; enviá-los de qualquer maneira?\\n\\n',\n\t\"u_applef\": 'Estes {0} arquivos (de um total de {1}) são provavelmente indesejáveis;\\nPressione <code>OK/Enter</code> para PULAR os seguintes arquivos,\\nPressione <code>Cancelar/ESC</code> para NÃO excluir, e ENVIAR esses também:\\n\\n',\n\t\"u_just1\": '\\nTalvez funcione melhor se você selecionar apenas um arquivo',\n\t\"u_ff_many\": \"se você estiver usando <b>Linux / MacOS / Android,</b> então essa quantidade de arquivos <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\"><em>pode</em> travar o Firefox!</a>\\nse isso acontecer, por favor, tente novamente (ou use o Chrome).\",\n\t\"u_up_life\": \"Este upload será excluído do servidor\\n{0} após ser concluído\",\n\t\"u_asku\": 'enviar estes {0} arquivos para <code>{1}</code>',\n\t\"u_unpt\": \"você pode desfazer / excluir este upload usando o 🧯 no canto superior esquerdo\",\n\t\"u_bigtab\": 'prestes a mostrar {0} arquivos\\n\\nisto pode travar seu navegador, tem certeza?',\n\t\"u_scan\": 'Escaneando arquivos...',\n\t\"u_dirstuck\": 'o iterador de diretório travou ao tentar acessar os seguintes {0} itens; irá pular:',\n\t\"u_etadone\": 'Concluído ({0}, {1} arquivos)',\n\t\"u_etaprep\": '(preparando para enviar)',\n\t\"u_hashdone\": 'hash concluído',\n\t\"u_hashing\": 'hash',\n\t\"u_hs\": 'handshaking...',\n\t\"u_started\": \"os arquivos estão sendo enviados agora; veja [🚀]\",\n\t\"u_dupdefer\": \"duplicado; será processado após todos os outros arquivos\",\n\t\"u_actx\": \"clique neste texto para evitar perda de<br />desempenho ao mudar para outras janelas/abas\",\n\t\"u_fixed\": \"OK!&nbsp; Consertado 👍\",\n\t\"u_cuerr\": \"falha ao enviar o bloco {0} de {1};\\nprovavelmente inofensivo, continuando\\n\\narquivo: {2}\",\n\t\"u_cuerr2\": \"o servidor rejeitou o upload (bloco {0} de {1});\\ntentará novamente mais tarde\\n\\narquivo: {2}\\n\\nerro \",\n\t\"u_ehstmp\": \"tentará novamente; veja no canto inferior direito\",\n\t\"u_ehsfin\": \"o servidor rejeitou a solicitação para finalizar o upload; tentando novamente...\",\n\t\"u_ehssrch\": \"o servidor rejeitou a solicitação para realizar a busca; tentando novamente...\",\n\t\"u_ehsinit\": \"o servidor rejeitou a solicitação para iniciar o upload; tentando novamente...\",\n\t\"u_eneths\": \"erro de rede ao realizar o handshake de upload; tentando novamente...\",\n\t\"u_enethd\": \"erro de rede ao testar a existência do alvo; tentando novamente...\",\n\t\"u_cbusy\": \"esperando o servidor confiar em nós novamente após uma falha de rede...\",\n\t\"u_ehsdf\": \"o servidor ficou sem espaço em disco!\\n\\ncontinuará tentando novamente, caso alguém\\nlibere espaço suficiente para continuar\",\n\t\"u_emtleak1\": \"parece que seu navegador pode ter um vazamento de memória;\\npor favor,\",\n\t\"u_emtleak2\": ' <a href=\"{0}\">mude para https (recomendado)</a> ou ',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": 'tente o seguinte:\\n<ul><li>pressione <code>F5</code> para atualizar a página</li><li>depois desative o botão &nbsp;<code>mt</code>&nbsp; nas &nbsp;<code>⚙️ configurações</code></li><li>e tente o upload novamente</li></ul>Os uploads serão um pouco mais lentos, mas tudo bem.\\nDesculpe pelo problema !\\n\\nPS: chrome v107 <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">tem uma correção de bug</a> para isso',\n\t\"u_emtleakf\": 'tente o seguinte:\\n<ul><li>pressione <code>F5</code> para atualizar a página</li><li>depois ative <code>🥔</code> (batata) na UI de upload<li>e tente o upload novamente</li></ul>\\nPS: o firefox <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">esperançosamente terá uma correção de bug</a> em algum momento',\n\t\"u_s404\": \"não encontrado no servidor\",\n\t\"u_expl\": \"explicar\",\n\t\"u_maxconn\": \"a maioria dos navegadores limita isso a 6, mas o firefox permite que você aumente com <code>connections-per-server</code> em <code>about:config</code>\",\n\t\"u_tu\": '<p class=\"warn\">AVISO: turbo ativado, <span>&nbsp;o cliente pode não detectar e retomar uploads incompletos; veja a dica de ferramenta do botão turbo</span></p>',\n\t\"u_ts\": '<p class=\"warn\">AVISO: turbo ativado, <span>&nbsp;os resultados da busca podem estar incorretos; veja a dica de ferramenta do botão turbo</span></p>',\n\t\"u_turbo_c\": \"o turbo está desativado na configuração do servidor\",\n\t\"u_turbo_g\": \"desativando o turbo porque você não tem\\n privilégios de listagem de diretório neste volume\",\n\t\"u_life_cfg\": 'excluir automaticamente depois de <input id=\"lifem\" p=\"60\" /> min (ou <input id=\"lifeh\" p=\"3600\" /> horas)',\n\t\"u_life_est\": 'o upload será excluído <span id=\"lifew\" tt=\"local time\">---</span>',\n\t\"u_life_max\": 'esta pasta impõe um\\n tempo de vida máximo de {0}',\n\t\"u_unp_ok\": 'a despublicação é permitida para {0}',\n\t\"u_unp_ng\": 'a despublicação NÃO será permitida',\n\t\"ue_ro\": 'seu acesso a esta pasta é Somente Leitura\\n\\n',\n\t\"ue_nl\": 'você não está logado no momento',\n\t\"ue_la\": 'você está logado no momento como \"{0}\"',\n\t\"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',\n\t\"ue_ta\": 'tente enviar novamente, deve funcionar agora',\n\t\"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\",\n\t\"ur_1uo\": \"OK: Arquivo enviado com sucesso\",\n\t\"ur_auo\": \"OK: Todos os {0} arquivos enviados com sucesso\",\n\t\"ur_1so\": \"OK: Arquivo encontrado no servidor\",\n\t\"ur_aso\": \"OK: Todos os {0} arquivos encontrados no servidor\",\n\t\"ur_1un\": \"O upload falhou, desculpe\",\n\t\"ur_aun\": \"Todos os {0} uploads falharam, desculpe\",\n\t\"ur_1sn\": \"O arquivo NÃO foi encontrado no servidor\",\n\t\"ur_asn\": \"Os {0} arquivos NÃO foram encontrados no servidor\",\n\t\"ur_um\": \"Concluído;\\n{0} uploads OK,\\n{1} uploads falharam, desculpe\",\n\t\"ur_sm\": \"Concluído;\\n{0} arquivos encontrados no servidor,\\n{1} arquivos NÃO encontrados no servidor\",\n\n\t\"rc_opn\": \"abrir\",\n\t\"rc_ply\": \"reproduzir\",\n\t\"rc_pla\": \"reproduzir como áudio\",\n\t\"rc_txt\": \"abrir no leitor de texto\",\n\t\"rc_md\": \"abrir no leitor markdown\",\n\t\"rc_dl\": \"baixar\",\n\t\"rc_zip\": \"baixar compactado\",\n\t\"rc_cpl\": \"copiar link\",\n\t\"rc_del\": \"excluir\",\n\t\"rc_cut\": \"recortar\",\n\t\"rc_cpy\": \"copiar\",\n\t\"rc_pst\": \"colar\",\n\t\"rc_rnm\": \"renomear\",\n\t\"rc_nfo\": \"nova pasta\",\n\t\"rc_nfi\": \"novo arquivo\",\n\t\"rc_sal\": \"selecionar tudo\",\n\t\"rc_sin\": \"inverter seleção\",\n\t\"rc_shf\": \"compartilhar esta pasta\",\n\t\"rc_shs\": \"compartilhar seleção\",\n\n\t\"lang_set\": \"atualizar para a mudança ter efeito?\",\n\n\t\"splash\": {\n\t\t\"a1\": \"atualizar\",\n\t\t\"b1\": \"olá &nbsp; <small>(você não está logado)</small>\",\n\t\t\"c1\": \"encerrar sessão\",\n\t\t\"d1\": \"despejar o estado da pilha\",\n\t\t\"d2\": \"mostra o estado de todos os threads ativos\",\n\t\t\"e1\": \"recarregar configuração\",\n\t\t\"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\",\n\t\t\"f1\": \"você pode navegar:\",\n\t\t\"g1\": \"você pode fazer upload para:\",\n\t\t\"cc1\": \"outras coisas:\",\n\t\t\"h1\": \"desativar k304\",\n\t\t\"i1\": \"ativar k304\",\n\t\t\"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), <em>mas</em> também irá desacelerar as coisas em geral\",\n\t\t\"k1\": \"redefinir config. de cliente\",\n\t\t\"l1\": \"faça login para mais:\",\n\t\t\"ls3\": \"fazer login\",\n\t\t\"lu4\": \"nome de usuário\",\n\t\t\"lp4\": \"senha\",\n\t\t\"lo3\": \"encerrar sessão de \\\"{0}\\\" em todos os lugares\",\n\t\t\"lo2\": \"isso irá encerrar a sessão em todos os navegadores\",\n\t\t\"m1\": \"bem-vindo de volta,\",\n\t\t\"n1\": \"404 não encontrado &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": \"ou talvez você não tenha acesso? -- tente com uma senha ou volte para o início\",\n\t\t\"p1\": \"403 proibido &nbsp;~┻━┻\",\n\t\t\"q1\": \"use uma senha ou volte para o início\",\n\t\t\"r1\": \"ir para o início\",\n\t\t\".s1\": \"reescanear\",\n\t\t\"t1\": \"ação\",\n\t\t\"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\",\n\t\t\"v1\": \"conectar\",\n\t\t\"v2\": \"usar este servidor como um disco rígido local\",\n\t\t\"w1\": \"mudar para https\",\n\t\t\"x1\": \"mudar senha\",\n\t\t\"y1\": \"editar recursos compartilhados\",\n\t\t\"z1\": \"desbloquear este recurso compartilhado:\",\n\t\t\"ta1\": \"primeiro digite sua nova senha\",\n\t\t\"ta2\": \"repita para confirmar a nova senha:\",\n\t\t\"ta3\": \"há um erro; por favor, tente novamente\",\n\t\t\"nop\": \"ERRO: A senha não pode estar em branco\",\n\t\t\"nou\": \"ERRO: O nome de usuário e/ou a senha não podem estar em branco\",\n\t\t\"aa1\": \"arquivos de entrada:\",\n\t\t\"ab1\": \"desativar no304\",\n\t\t\"ac1\": \"ativar no304\",\n\t\t\"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!\",\n\t\t\"ae1\": \"downloads ativos:\",\n\t\t\"af1\": \"mostrar uploads recentes\",\n\t\t\"ag1\": \"mostrar usuários IdP conhecidos\",\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/rus.js",
    "content": "\n// Строки, оканчивающиеся на //m, являются непроверенными машинными переводами\n\nLs.rus = {\n\t\"tt\": \"Русский\",\n\n\t\"cols\": {\n\t\t\"c\": \"кнопки действий\",\n\t\t\"dur\": \"длительность\",\n\t\t\"q\": \"качество / битрейт\",\n\t\t\"Ac\": \"аудио кодек\",\n\t\t\"Vc\": \"видео кодек\",\n\t\t\"Fmt\": \"формат / контейнер\",\n\t\t\"Ahash\": \"контрольная сумма аудио\",\n\t\t\"Vhash\": \"контрольная сумма видео\",\n\t\t\"Res\": \"разрешение\",\n\t\t\"T\": \"тип файла\",\n\t\t\"aq\": \"качество аудио / битрейт\",\n\t\t\"vq\": \"качество видео / битрейт\",\n\t\t\"pixfmt\": \"сабсемплинг / пиксельный формат\",\n\t\t\"resw\": \"горизонтальное разрешение\",\n\t\t\"resh\": \"вертикальное разрешение\",\n\t\t\"chs\": \"аудио каналы\",\n\t\t\"hz\": \"частота дискретизации\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"разное\",\n\t\t\t[\"ESC\", \"закрыть всякие штуки\"],\n\n\t\t\t\"файловый менеджер\",\n\t\t\t[\"G\", \"переключиться между списком / плиткой\"],\n\t\t\t[\"T\", \"переключиться между миниатюрами / иконками\"],\n\t\t\t[\"⇧ A/D\", \"размер миниатюры\"],\n\t\t\t[\"ctrl-K\", \"удалить выделенное\"],\n\t\t\t[\"ctrl-X\", \"вырезать выделенное в буфер\"],\n\t\t\t[\"ctrl-C\", \"копировать выделенное в буфер\"],\n\t\t\t[\"ctrl-V\", \"вставить (переместить/копировать) сюда\"],\n\t\t\t[\"Y\", \"скачать выделенное\"],\n\t\t\t[\"F2\", \"переименовать выделенное\"],\n\n\t\t\t\"выделение файлов\",\n\t\t\t[\"пробел\", \"выделить/снять выделение с текущего файла\"],\n\t\t\t[\"↑/↓\", \"двигать курсор\"],\n\t\t\t[\"ctrl ↑/↓\", \"двигать курсор и скроллить\"],\n\t\t\t[\"⇧ ↑/↓\", \"выделить предыдущий/следующий файл\"],\n\t\t\t[\"ctrl-A\", \"выделить все файлы и папки\"],\n\t\t], [\n\t\t\t\"навигация\",\n\t\t\t[\"B\", \"показать/скрыть панель навигации\"],\n\t\t\t[\"I/K\", \"предыдущая/следующая папка\"],\n\t\t\t[\"M\", \"перейти на уровень выше (или свернуть текущую папку)\"],\n\t\t\t[\"V\", \"переключиться между папками / текстовыми файлами в панели навигации\"],\n\t\t\t[\"A/D\", \"уменьшить/увеличить панель навигации\"],\n\t\t], [\n\t\t\t\"аудиоплеер\",\n\t\t\t[\"J/L\", \"предыдущий/следующий трек\"],\n\t\t\t[\"U/O\", \"перемотать на 10 секунд назад/вперёд\"],\n\t\t\t[\"0..9\", \"перемотать на 0%..90%\"],\n\t\t\t[\"P\", \"играть/пауза (или подгрузить трек)\"],\n\t\t\t[\"S\", \"выделить текущий трек\"],\n\t\t\t[\"Y\", \"скачать трек\"],\n\t\t], [\n\t\t\t\"просмотрщик изображений\",\n\t\t\t[\"J/L, ←/→\", \"предыдущее/следующее изображение\"],\n\t\t\t[\"Home/End\", \"первое/последнее изображение\"],\n\t\t\t[\"F\", \"развернуть на полный экран\"],\n\t\t\t[\"R\", \"повернуть по часовой стрелке\"],\n\t\t\t[\"⇧ R\", \"повернуть против часовой стрелки\"],\n\t\t\t[\"S\", \"выделить изображение\"],\n\t\t\t[\"Y\", \"скачать изображение\"],\n\t\t], [\n\t\t\t\"видеоплеер\",\n\t\t\t[\"U/O\", \"перемотать на 10 секунд назад/вперёд\"],\n\t\t\t[\"P/K/Space\", \"играть/пауза\"],\n\t\t\t[\"C\", \"продолжить проигрывать следующее\"],\n\t\t\t[\"V\", \"повтор\"],\n\t\t\t[\"M\", \"выключить звук\"],\n\t\t\t[\"[ and ]\", \"задать интервал повтора\"],\n\t\t], [\n\t\t\t\"просмотрщик текстовых файлов\",\n\t\t\t[\"I/K\", \"предыдущий/следующий файл\"],\n\t\t\t[\"M\", \"закрыть файл\"],\n\t\t\t[\"E\", \"отредактировать файл\"],\n\t\t\t[\"S\", \"выделить файл\"],\n\t\t\t[\"Y\", \"скачать текстовый файл\"], //m\n\t\t\t[\"⇧ J\", \"приукрасить json\"], //m\n\t\t]\n\t],\n\n\t\"m_ok\": \"OK\",\n\t\"m_ng\": \"Отмена\",\n\n\t\"enable\": \"Включить\",\n\t\"danger\": \"ВНИМАНИЕ\",\n\t\"clipped\": \"скопировано в буфер обмена\",\n\n\t\"ht_s1\": \"секунда\",\n\t\"ht_s2\": \"секунды\",\n\t\"ht_m1\": \"минута\",\n\t\"ht_m2\": \"минуты\",\n\t\"ht_h1\": \"час\",\n\t\"ht_h2\": \"часы\",\n\t\"ht_d1\": \"день\",\n\t\"ht_d2\": \"дни\",\n\t\"ht_and\": \" и \",\n\n\t\"goh\": \"панель управления\",\n\t\"gop\": 'предыдущая папка\">пред',\n\t\"gou\": 'родительская папка\">вверх',\n\t\"gon\": 'следующая папка\">след',\n\t\"logout\": \"Выйти \",\n\t\"login\": \"Войти\", //m\n\t\"access\": \" доступ\",\n\t\"ot_close\": \"закрыть подменю\",\n\t\"ot_search\": \"`искать файлы по атрибутам, пути / имени, музыкальным тегам или любой другой комбинации из следующих конструкций$N$N`foo bar` = обязано содержать «foo» И «bar»,$N`foo -bar` = обязано содержать «foo», но не «bar»,$N`^yana .opus$` = начинается с «yana» и имеет расширение «opus»$N`&quot;try unite&quot;` = содержит именно «try unite»$N$Nформат времени задаётся по стандарту iso-8601, например$N`2009-12-31` или `2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"unpost: удалить ваши недавние загрузки и отменить незавершённые\",\n\t\"ot_bup\": \"bup: легковесный загрузчик файлов, поддерживает даже netscape 4.0\",\n\t\"ot_mkdir\": \"mkdir: создать новую папку\",\n\t\"ot_md\": \"new-file: создать новый текстовый файл\", //m\n\t\"ot_msg\": \"msg: отправить сообщение в лог сервера\",\n\t\"ot_mp\": \"настройка медиаплеера\",\n\t\"ot_cfg\": \"остальные настройки\",\n\t\"ot_u2i\": 'up2k: загрузить файлы (если имеется доступ к записи) или переключиться в режим поиска$N$Nзагрузки являются возобновляемыми и многопоточными, а даты изменения файлов сохраняются в процессе, но этот метод использует больше ресурсов процессора, чем [🎈]&nbsp; (легковесный загрузчик)<br /><br />во время загрузки эта иконка превращается в индикатор!',\n\t\"ot_u2w\": 'up2k: загрузить файлы с поддержкой возобновления (закиньте те же файлы после перезапуска браузера)$N$Nподдерживается многопоточность, даты изменения файлов сохраняются в процессе, но этот метод использует больше ресурсов процессора, чем [🎈]&nbsp; (легковесный загрузчик)<br /><br />во время загрузки эта иконка превращается в индикатор!',\n\t\"ot_noie\": 'Пожалуйста, используйте Chrome / Firefox / Edge',\n\n\t\"ab_mkdir\": \"создать папку\",\n\t\"ab_mkdoc\": \"создать текстовый файл\", //m\n\t\"ab_msg\": \"отправить сообщение в лог сервера\",\n\n\t\"ay_path\": \"перейти к папкам\",\n\t\"ay_files\": \"перейти к файлам\",\n\n\t\"wt_ren\": \"переименовать выделенные файлы$NГорячая клавиша: F2\",\n\t\"wt_del\": \"удалить выделенные файлы$NГорячая клавиша: ctrl-K\",\n\t\"wt_cut\": \"вырезать выделенные файлы &lt;small&gt;(затем вставить куда-то в другое место)&lt;/small&gt;$NГорячая клавиша: ctrl-X\",\n\t\"wt_cpy\": \"копировать выделенные файлы в буфер$N(чтобы вставить их куда-то ещё)$NГорячая клавиша: ctrl-C\",\n\t\"wt_pst\": \"вставить ранее вырезанный / скопированный файл$NГорячая клавиша: ctrl-V\",\n\t\"wt_selall\": \"выделить все файлы$NГорячая клавиша: ctrl-A (когда выделен хотя бы один файл)\",\n\t\"wt_selinv\": \"инвертировать выделение\",\n\t\"wt_zip1\": \"скачать эту папку как архив\",\n\t\"wt_selzip\": \"скачать выделенные файлы как архив\",\n\t\"wt_seldl\": \"скачать выделенные файлы по-отдельности$NГорячая клавиша: Y\",\n\t\"wt_npirc\": \"копировать информацию о треке в формате irc\",\n\t\"wt_nptxt\": \"копировать информацию о треке обычным текстом\",\n\t\"wt_m3ua\": \"добавить в плейлист m3u (нажмите <code>📻коп.</code> в конце)\",\n\t\"wt_m3uc\": \"копировать плейлист m3u в буфер обмена\",\n\t\"wt_grid\": \"переключить между сеткой / списком$NГорячая клавиша: G\",\n\t\"wt_prev\": \"предыдущий трек$NГорячая клавиша: J\",\n\t\"wt_play\": \"играть / пауза$NГорячая клавиша: P\",\n\t\"wt_next\": \"следующий трек$NГорячая клавиша: L\",\n\n\t\"ul_par\": \"параллельные загрузки:\",\n\t\"ut_rand\": \"случайные имена файлов\",\n\t\"ut_u2ts\": \"копировать время последнего изменения$Nиз вашей файловой системы на сервер\\\">📅\",\n\t\"ut_ow\": \"перезаписывать существующие файлы на сервере?$N🛡️: нет (для повторяющихся файлов будут создаваться новые имена)$N🕒: перезаписать файлы с датой изменения старее, чем у загружаемых$N♻️: всегда перезаписывать (если файлы различаются по содержанию)$N⏭️: безусловно пропускать все существующие файлы\", //m\n\t\"ut_mt\": \"продолжать хешировать другие файлы во время загрузки$N$Nесть смысл отключить при медленном диске или процессоре\",\n\t\"ut_ask\": 'требовать подтверждения перед началом загрузки\">💭',\n\t\"ut_pot\": \"улучшить скорость загрузки на слабых устройства$Nс помощью упрощения интерфейса\",\n\t\"ut_srch\": \"не загружать, а проверять, существуют ли данные файлы $N на сервере (проверка всех доступных вам папок)\",\n\t\"ut_par\": \"при 0 загрузка встанет на паузу$N$Nследует повысить, если ваше подключение медленное$N$Nоставьте 1, если используется локальная сеть или диск сервера медленный\",\n\t\"ul_btn\": \"отпустите файлы / папки<br>здесь (или нажмите)\",\n\t\"ul_btnu\": \"З А Г Р У З И Т Ь\",\n\t\"ul_btns\": \"И С К А Т Ь\",\n\n\t\"ul_hash\": \"хеш\",\n\t\"ul_send\": \"отправка\",\n\t\"ul_done\": \"готово\",\n\t\"ul_idle1\": \"нет загрузок в очереди\",\n\t\"ut_etah\": \"средняя скорость &lt;em&gt;хеширования&lt;/em&gt; и примерное время до завершения\",\n\t\"ut_etau\": \"средняя скорость &lt;em&gt;загрузки&lt;/em&gt; и примерное время до завершения\",\n\t\"ut_etat\": \"средняя &lt;em&gt;общая&lt;/em&gt; скорость и примерное время до завершения\",\n\n\t\"uct_ok\": \"успешно завершены\",\n\t\"uct_ng\": \"ошибки / отказы / не найдены\",\n\t\"uct_done\": \"готово (ok и ng вместе)\",\n\t\"uct_bz\": \"хешируются или загружаются\",\n\t\"uct_q\": \"в очереди\",\n\n\t\"utl_name\": \"имя файла\",\n\t\"utl_ulist\": \"список\",\n\t\"utl_ucopy\": \"копировать\",\n\t\"utl_links\": \"ссылки\",\n\t\"utl_stat\": \"статус\",\n\t\"utl_prog\": \"прогресс\",\n\n\t// keep short:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"ОШИБКА\",\n\t\"utl_oserr\": \"ошибка ОС\",\n\t\"utl_found\": \"найдено\",\n\t\"utl_defer\": \"отложить\",\n\t\"utl_yolo\": \"турбо\",\n\t\"utl_done\": \"готово\",\n\n\t\"ul_flagblk\": \"файлы были добавлены в очередь</b>,<br>однако в другой вкладке уже есть активная загрузка через up2k,<br>поэтому ожидаем её завершения\",\n\t\"ul_btnlk\": \"настройки сервера запрещают изменение состояния этой опции\",\n\n\t\"udt_up\": \"Загрузить\",\n\t\"udt_srch\": \"Поиск\",\n\t\"udt_drop\": \"отпустите здесь\",\n\n\t\"u_nav_m\": '<h6>лады, что там у вас?</h6><code>Enter</code> = Файлы (один или больше)\\n<code>ESC</code> = Одна папка (с учётом подпапок)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Файлы</a><a href=\"#\" id=\"modal-ng\">Одна папка</a>',\n\n\t\"cl_opts\": \"переключатели\",\n\t\"cl_hfsz\": \"размер файла\", //m\n\t\"cl_themes\": \"тема\",\n\t\"cl_langs\": \"язык\",\n\t\"cl_ziptype\": \"архивация папок\",\n\t\"cl_uopts\": \"опции up2k\",\n\t\"cl_favico\": \"иконка\",\n\t\"cl_bigdir\": \"бол. папки\",\n\t\"cl_hsort\": \"#сорт.\",\n\t\"cl_keytype\": \"схема горячих клавиш\",\n\t\"cl_hiddenc\": \"скрытые столбцы\",\n\t\"cl_hidec\": \"скрыть\",\n\t\"cl_reset\": \"сбросить\",\n\t\"cl_hpick\": \"нажмите на заголовки столбцов, чтобы скрыть их в таблице ниже\",\n\t\"cl_hcancel\": \"скрытие столбца отменено\",\n\t\"cl_rcm\": \"контекстное меню\", //m\n\n\t\"ct_grid\": '田 сетка',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ подсказки',\n\t\"ct_thumb\": 'переключение между иконками и миниатюрами в режиме сетки$NГорячая клавиша: T\">🖼️ миниат.',\n\t\"ct_csel\": 'держите CTRL или SHIFT для выделения файлов в режиме сетки\">выбор',\n\t\"ct_dsel\": 'использовать выделение перетаскиванием в режиме сетки\">перетащить', //m\n\t\"ct_dl\": 'принудительная загрузка (не показывать встроенно) при щелчке по файлу\">dl', //m\n\t\"ct_ihop\": 'показывать последний открытый файл после закрытия просмотрщика изображений\">g⮯',\n\t\"ct_dots\": 'показывать скрытые файлы (если есть доступ)\">скрыт.',\n\t\"ct_qdel\": 'спрашивать подтверждение только один раз перед удалением файлов\">быстр. удал.',\n\t\"ct_dir1st\": 'разместить папки над файлами\">📁 сверху',\n\t\"ct_nsort\": 'сортировка по числам$N(например, файл с &gt;code&lt;2&gt;/code&lt; в начале названия идёт перед &gt;code&lt;11&gt;/code&lt;)\">нат. сорт.',\n\t\"ct_utc\": 'используйте UTC для всех временных меток\">UTC', //m\n\t\"ct_readme\": 'показывать содержимое README.md в описании папки\">📜 ридми',\n\t\"ct_idxh\": 'показывать страницу index.html в текущей папке вместо интерфейса\">htm',\n\t\"ct_sbars\": 'показывать полосы прокрутки\">⟊',\n\n\t\"cut_umod\": \"если файл уже существует на сервере, обновить время последнего изменения на сервере в соответствии с локальным файлом (требуются права write+delete)\\\">перенос 📅\",\n\n\t\"cut_turbo\": \"используйте эту функцию С ОСТОРОЖНОСТЬЮ:$N$Nпригодится в случае, если была прервана загрузка большого количества файлов, и вы хотите возобновить её как можно быстрее$N$Nпроверка файлов по хешу заменяется на простой алгоритм: <em>&quot;если размер файла отличается - тогда загрузить&quot;</em>, но содержание файлов не сравнивается$N$Nследует отключить эту функцию после окончания загрузки, а затем &quot;загрузить&quot; те же файлы заново, чтобы валидировать их\\\">турбо\",\n\n\t\"cut_datechk\": \"работает только при включённой кнопке &quot;турбо&quot;$N$Nчуть-чуть повышает надёжность турбо-загрузок с помощью сверки дат изменений между файлами на сервере и вашими$N$N<em>в теории</em> достаточно для проверки большинства незавершённых / повреждённых загрузок, но не является альтернативой валидации файлов после турбо-загрузки\\\">провер. дат\",\n\n\t\"cut_u2sz\": \"размер (в МиБ) каждой загружаемой части; большие значения показывают лучшие результаты для дальних соединений. Если подключение нестабильное, попробуйте значение пониже\",\n\n\t\"cut_flag\": \"разрешить одновременную загрузку только из одной вкладки за раз $N -- обязательно включить эту опцию в остальных вкладках $N -- работает только в пределах одного домена\",\n\n\t\"cut_az\": \"загружать файлы в алфавитном порядке вместо &quot;от меньшего к большему&quot;$N$Nэто позволит проще отследить проблемы во время загрузки, но скорость слегка ниже на очень быстрых соединениях (например, в локальной сети)\",\n\n\t\"cut_nag\": \"системное уведомление по завершении загрузки$N(только при неактивной вкладке браузера)\",\n\t\"cut_sfx\": \"звуковое уведомление по завершении загрузки$N(только при неактивной вкладке браузера)\",\n\n\t\"cut_mt\": \"использовать многопоточность для ускорения хеширования$N$Nиспользует Web Worker'ы и требует больше памяти (до 512 МиБ)$N$Nускоряет https на 30%, http - в 4,5 раз\\\">мп\",\n\n\t\"cut_wasm\": \"использовать модуль WASM вместо встроенной в браузер функции хеширования; ускоряет процесс в браузерах на основе Chromium, но увеличивает нагрузку на процессор. Старые версии Chrome содержат баги, которые заполняют всю оперативную память и крашат браузер, когда включена эта опция\\\">wasm\",\n\n\t\"cft_text\": \"текст для иконки (очистите поле и перезагрузите страницу для применения)\",\n\t\"cft_fg\": \"цвет текста\",\n\t\"cft_bg\": \"цвет фона\",\n\n\t\"cdt_lim\": \"максимальное количество файлов для показа в папке\",\n\t\"cdt_ask\": \"внизу страницы спрашивать о действии вместо автоматической загрузки следующих файлов\",\n\t\"cdt_hsort\": \"`сколько правил сортировки (`,sorthref`) включать в адрес страницы. Если значение равно 0, по нажатии на ссылки будут игнорироваться правила, включённые в них\",\n\t\"cdt_ren\": \"включить настраиваемое контекстное меню, обычное меню доступно при нажатии shift и правой кнопки мыши\\\">вкл.\",\n\t\"cdt_rdb\": \"показывать обычное меню правого клика, если пользовательское уже открыто и выполняется повторный клик\\\">двойное\",\n\n\t\"tt_entree\": \"показать панель навигации$NГорячая клавиша: B\",\n\t\"tt_detree\": \"скрыть панель навигации$NГорячая клавиша: B\",\n\t\"tt_visdir\": \"прокрутить до выделенной папки\",\n\t\"tt_ftree\": \"переключить между иерархией и списком текстовых файлов$NГорячая клавиша: V\",\n\t\"tt_pdock\": \"закрепить родительские папки сверху панели\",\n\t\"tt_dynt\": \"автоматическое расширение панели\",\n\t\"tt_wrap\": \"перенос слов\",\n\t\"tt_hover\": \"раскрывать обрезанные строки при наведении$N( ломает скроллинг, если $N&nbsp; курсор не в пустоте слева )\",\n\n\t\"ml_pmode\": \"в конце папки...\",\n\t\"ml_btns\": \"команды\",\n\t\"ml_tcode\": \"транскодировать\",\n\t\"ml_tcode2\": \"транскод. в\",\n\t\"ml_tint\": \"затемн.\",\n\t\"ml_eq\": \"эквалайзер\",\n\t\"ml_drc\": \"компрессор\",\n\t\"ml_ss\": \"пропускать тишину\", //m\n\n\t\"mt_loop\": \"повторять один трек\\\">🔁\",\n\t\"mt_one\": \"остановить после этого трека\\\">1️⃣\",\n\t\"mt_shuf\": \"перемешать треки во всех папках\\\">🔀\",\n\t\"mt_aplay\": \"автоматически играть треки по нажатии на ссылки с их ID$N$Nпри отключении адрес сайта также перестанет обновляться в соответствии с текущим треком\\\">a▶\",\n\t\"mt_preload\": \"подгружать следующий трек перед концом текущего для бесшовного переключения\\\">предзагр.\",\n\t\"mt_prescan\": \"переходить в следующую папку перед окончанием последнего трека$Nне даёт браузеру прервать следующий плейлист\\\">нав.\",\n\t\"mt_fullpre\": \"подгружать следующий трек целиком;$N✅ полезно при <b>нестабильном</b> подключении,$N❌ при медленной скорости лучше <b>выключить</b>\\\">цел.\",\n\t\"mt_fau\": \"для телефонов: начинать следующий трек сразу, даже если он не успел подгрузиться целиком (может сломать отображение тегов)\\\">☕️\",\n\t\"mt_waves\": \"визуализация:$Nпоказывать волну громкости на полосе воспроизведения\\\">~\",\n\t\"mt_npclip\": \"показать кнопки копирования для текущего трека\\\">/np\",\n\t\"mt_m3u_c\": \"показать кнопки копирования для выделенных треков$Nв формате плейлистов m3u8\\\">📻\",\n\t\"mt_octl\": \"интеграция с ОС (поддержка медиа-клавиш и музыкальных виджетов)\\\">интегр.\",\n\t\"mt_oseek\": \"позволить перематывать треки через системные виджеты$N$Nвнимание: на некоторых устройствах (iPhone)$Nэто заменит кнопку следующего трека\\\">перемотка\",\n\t\"mt_oscv\": \"показывать картинки альбомов в виджетах\\\">арт\",\n\t\"mt_follow\": \"держать фокус на играющем треке\\\">🎯\",\n\t\"mt_compact\": \"компактный плеер\\\">⟎\",\n\t\"mt_uncache\": \"очистить кеш &nbsp;(если браузер кешировал повреждённый$Nтрек и отказывается его запускать)\\\">уд. кеш\",\n\t\"mt_mloop\": \"повторять треки в папке\\\">🔁 цикл\",\n\t\"mt_mnext\": \"загрузить следующую папку и продолжить в ней\\\">📂 след.\",\n\t\"mt_mstop\": \"приостановить воспроизведение\\\">⏸ стоп\",\n\t\"mt_cflac\": \"конвертировать flac / wav в {0}\\\">flac\",\n\t\"mt_caac\": \"конвертировать aac / m4a в {0}\\\">aac\",\n\t\"mt_coth\": \"конвертировать всё остальное (кроме mp3) в {0}\\\">др.\",\n\t\"mt_c2opus\": \"лучший вариант для компьютеров и устройств на Android\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, для iOS 17.5 и выше\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, для iOS 11-17\\\">caf\",\n\t\"mt_c2mp3\": \"для очень старых устройств\\\">mp3\",\n\t\"mt_c2flac\": \"лучшее качество звука, но большие файлы\\\">flac\", //m\n\t\"mt_c2wav\": \"не сжатое воспроизведение (ещё больше)\\\">wav\", //m\n\t\"mt_c2ok\": \"хороший выбор\",\n\t\"mt_c2nd\": \"это не рекомендованный вариант формата для вашего устройства, но сойдёт\",\n\t\"mt_c2ng\": \"не похоже, что ваше устройство поддерживает этот формат, но давайте попробуем и узнаем наверняка\",\n\t\"mt_xowa\": \"в iOS есть баги, препятствующие фоновому воспроизведению этого формата. Пожалуйста, используйте caf или mp3\",\n\t\"mt_tint\": \"непрозрачность фона (0-100) на полосе воспроизведения$N$Nделает буферизацию менее отвлекающей\",\n\t\"mt_eq\": \"`включить эквалайзер$N$Nboost `0` = стандартная громкость$N$Nwidth `1 &nbsp;` = обычное стерео$Nwidth `0.5` = микширование левого и правого каналов на 50%$Nwidth `0 &nbsp;` = моно$N$Nboost `-0.8` и width `10` = удаление голоса :^)$N$Nвключённый эквалайзер полностью убирает задержку между треками, поэтому следует его включить со всеми значениями на 0 (кроме width = 1), если вам нужно бесшовное воспроизведение\",\n\t\"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(загляните в википедию, там всё объяснено подробнее)\",\n\t\"mt_ss\": \"`включает пропуск тишины; умножает скорость на `уск` возле начала/конца, когда громкость ниже `гром` и позиция в первых `нач`% или последних `кон`%\", //m\n\t\"mt_ssvt\": \"порог громкости (0-255)\\\">гром\", //m\n\t\"mt_ssts\": \"активный порог (% трека, начало)\\\">нач\", //m\n\t\"mt_sste\": \"активный порог (% трека, конец)\\\">кон\", //m\n\t\"mt_sssm\": \"множитель скорости воспроизведения\\\">уск\", //m\n\n\t\"mb_play\": \"играть\",\n\t\"mm_hashplay\": \"воспроизвести этот музыкальный файл?\",\n\t\"mm_m3u\": \"нажмите <code>Enter/OK</code>, чтобы играть\\nнажмите <code>ESC/Отмена</code>, чтобы редактировать\",\n\t\"mp_breq\": \"требуется Firefox 82+, Chrome 73+ или iOS 15+\",\n\t\"mm_bload\": \"загружаю...\",\n\t\"mm_bconv\": \"конвертирую в {0}, подождите...\",\n\t\"mm_opusen\": \"ваш браузер не может воспроизводить файлы aac / m4a;\\nвключено транскодирование в opus\",\n\t\"mm_playerr\": \"ошибка воспроизведения: \",\n\t\"mm_eabrt\": \"Попытка воспроизведения была отменена\",\n\t\"mm_enet\": \"Ваше подключение нестабильно\",\n\t\"mm_edec\": \"Этот файл, возможно, повреждён??\",\n\t\"mm_esupp\": \"Ваш браузер не распознаёт этот аудио-формат\",\n\t\"mm_eunk\": \"Неопознанная ошибка\",\n\t\"mm_e404\": \"Не удалось воспроизвести аудио; ошибка 404: Файл не найден.\",\n\t\"mm_e403\": \"Не удалось воспроизвести аудио; ошибка 403: Доступ запрещён.\\n\\nПопробуйте перезагрузить страницу, возможно, ваша сессия истекла\",\n\t\"mm_e415\": \"Не удалось воспроизвести аудио; ошибка 415: Сбой преобразования файла; проверьте логи сервера.\", //m\n\t\"mm_e500\": \"Не удалось воспроизвести аудио; ошибка 500: Проверьте логи сервера.\",\n\t\"mm_e5xx\": \"Не удалось воспроизвести аудио; ошибка сервера \",\n\t\"mm_nof\": \"больше аудио-файлов не найдено\",\n\t\"mm_prescan\": \"Поиск музыки для воспроизведения дальше...\",\n\t\"mm_scank\": \"Найден следующий трек:\",\n\t\"mm_uncache\": \"кеш очищен; все треки будут загружены заново при воспроизведении\",\n\t\"mm_hnf\": \"это трек больше не существует\",\n\n\t\"im_hnf\": \"это изображение больше не существует\",\n\n\t\"f_empty\": 'эта папка пуста',\n\t\"f_chide\": 'это скроет столбец «{0}»\\n\\nвы можете показать скрытые столбцы в настройках',\n\t\"f_bigtxt\": \"объём данного файла - {0} МиБ. точно открыть как текст?\",\n\t\"f_bigtxt2\": \"просмотреть только конец файла? это также включит обновление в реальном времени, показывая новые строки сразу после их добавления\",\n\t\"fbd_more\": '<div id=\"blazy\">показано <code>{0}</code> из <code>{1}</code> файлов; <a href=\"#\" id=\"bd_more\">показать {2}</a> или <a href=\"#\" id=\"bd_all\">показать всё</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\">показано <code>{0}</code> из <code>{1}</code> файлов; <a href=\"#\" id=\"bd_all\">показать всё</a></div>',\n\t\"f_anota\": \"только {0} из {1} файлов было выделено;\\nчтобы выделить всё папку, отмотайте до низа\",\n\n\t\"f_dls\": 'ссылки на файлы в данной папке были\\nзаменены ссылками на скачивание',\n\t\"f_dl_nd\": 'пропуск папки (используйте загрузку zip/tar вместо этого):\\n', //m\n\n\t\"f_partial\": \"Чтобы безопасно скачать файл, который в текущий момент загружается, нажмите на файл с таким же названием, но без расширения <code>.PARTIAL</code>. Пожалуйста, нажмите Отмена или ESC, чтобы сделать это.\\n\\nПри нажатии OK / Enter, вы скачаете этот временный файл, который с огромной вероятностью содержит лишь неполные данные.\",\n\n\t\"ft_paste\": \"вставить {0} файлов$NГорячая клавиша: ctrl-V\",\n\t\"fr_eperm\": 'не удалось переименовать:\\nу вас нет разрешения “move” в этой папке',\n\t\"fd_eperm\": 'не удалось удалить:\\nу вас нет разрешения “delete” в этой папке',\n\t\"fc_eperm\": 'не удалось вырезать:\\nу вас нет разрешения “move” в этой папке',\n\t\"fp_eperm\": 'не удалось вставить:\\nу вас нет разрешения “write” в этой папке',\n\t\"fr_emore\": \"выделите хотя бы один файл, чтобы переименовать\",\n\t\"fd_emore\": \"выделите хотя бы один файл, чтобы удалить\",\n\t\"fc_emore\": \"выделите хотя бы один файл, чтобы вырезать\",\n\t\"fcp_emore\": \"выделите хотя бы один файл, чтобы скопировать в буфер\",\n\n\t\"fs_sc\": \"поделиться текущей папкой\",\n\t\"fs_ss\": \"поделиться выделенными файлами\",\n\t\"fs_just1d\": \"вы не можете выбрать больше одной папки\\nили смешивать файлы с папками при выделении\",\n\t\"fs_abrt\": \"❌ отменить\",\n\t\"fs_rand\": \"🎲 случ. имя\",\n\t\"fs_go\": \"✅ создать доступ\",\n\t\"fs_name\": \"имя\",\n\t\"fs_src\": \"путь\",\n\t\"fs_pwd\": \"пароль\",\n\t\"fs_exp\": \"срок\",\n\t\"fs_tmin\": \"мин\",\n\t\"fs_thrs\": \"часов\",\n\t\"fs_tdays\": \"дней\",\n\t\"fs_never\": \"вечно\",\n\t\"fs_pname\": \"имя ссылки; генерируется случайно если не указано\",\n\t\"fs_tsrc\": \"путь к файлу или папке, которыми нужно поделиться\",\n\t\"fs_ppwd\": \"пароль (необязательно)\",\n\t\"fs_w8\": \"создаю доступ...\",\n\t\"fs_ok\": \"нажмите <code>Enter/OK</code>, чтобы скопировать\\nнажмите <code>ESC/Отмена</code>, чтобы закрыть\",\n\n\t\"frt_dec\": \"может исправить некоторые случаи с некорректными именами файлов\\\">декодировать url\",\n\t\"frt_rst\": \"сбросить изменённые имена обратно к оригинальным\\\">↺ сброс\",\n\t\"frt_abrt\": \"отменить операцию и закрыть это окно\\\">❌ отмена\",\n\t\"frb_apply\": \"ПЕРЕИМЕНОВАТЬ\",\n\t\"fr_adv\": \"переименование массовое / метаданных / по шаблону\\\">эксперт\",\n\t\"fr_case\": \"чувствительный к регистру regex\\\">РеГиСтР\",\n\t\"fr_win\": \"совместимые с windows имена; заменяет <code>&lt;&gt;:&quot;\\\\|?*</code> японскими полноширинными символами\\\">win\",\n\t\"fr_slash\": \"заменяет <code>/</code> символом, который не создаёт новые папки\\\">без /\",\n\t\"fr_re\": \"`поиск по шаблону regex, применяемый к оригинальным именам; группы захвата могут применяться в поле форматирования с помощью `(1)` и `(2)` и так далее\",\n\t\"fr_fmt\": \"`вдохновлено foobar2000:$N`(title)` заменяется названием трека,$N`[(artist) - ](title)` пропускает [эту] часть, если композитор не указан$N`$lpad((tn),2,0)` добавляет ведущие нули до двух цифр\",\n\t\"fr_pdel\": \"удалить\",\n\t\"fr_pnew\": \"сохранить как\",\n\t\"fr_pname\": \"предоставьте название для нового шаблона\",\n\t\"fr_aborted\": \"прервано\",\n\t\"fr_lold\": \"старое имя\",\n\t\"fr_lnew\": \"новое имя\",\n\t\"fr_tags\": \"теги для выделенных файлов (не редактируется, это для инструкции):\",\n\t\"fr_busy\": \"переименовываю {0} файлов...\\n\\n{1}\",\n\t\"fr_efail\": \"ошибка переименования:\\n\",\n\t\"fr_nchg\": \"{0} новых имён были модифицированы для соответствия опциям <code>win</code> и/или <code>без /</code>\\n\\nХотите использовать эти имена?\",\n\n\t\"fd_ok\": \"успешно удалено\",\n\t\"fd_err\": \"ошибка удаления:\\n\",\n\t\"fd_none\": \"ничего не удалено; возможно, не позволяет конфигурация сервера (xbd)?\",\n\t\"fd_busy\": \"удалено {0} файлов...\\n\\n{1}\",\n\t\"fd_warn1\": \"УДАЛИТЬ эти {0} файлов?\",\n\t\"fd_warn2\": \"<b>Внимание!</b> Это необратимый процесс. Удалить?\",\n\n\t\"fc_ok\": \"вырезано {0} файлов\",\n\t\"fc_warn\": 'вырезано {0} файлов\\n\\nно только <b>эта</b> вкладка браузера может их вставить\\n(поскольку выделение оказалось настолько огромным)',\n\n\t\"fcc_ok\": \"скопировано {0} файлов в буфер\",\n\t\"fcc_warn\": 'скопировано {0} файлов в буфер\\n\\nно только <b>эта</b> вкладка браузера может их вставить\\n(поскольку выделение оказалось настолько огромным)',\n\n\t\"fp_apply\": \"использовать эти имена\",\n\t\"fp_skip\": \"пропустить конфликты\", //m\n\t\"fp_ecut\": \"сначала вырезать или скопировать только некоторые файлы / папки\\n\\nучтите: вы можете вырезать / вставлять файлы между вкладками\",\n\t\"fp_ename\": \"{0} файлов невозможно перенести сюда, потому что их имена уже заняты. Введите имена ниже, чтобы продолжить, или оставьте поля пустыми (\\\"пропустить конфликты\\\"), чтобы пропустить:\", //m\n\t\"fcp_ename\": \"{0} файлов невозможно скопировать сюда, потому что их имена уже заняты. Введите имена ниже, чтобы продолжить, или оставьте поля пустыми (\\\"пропустить конфликты\\\"), чтобы пропустить:\", //m\n\t\"fp_emore\": \"есть ещё коллизии имён, которые требуется исправить\",\n\t\"fp_ok\": \"успешно перенесено\",\n\t\"fcp_ok\": \"успешно скопировано\",\n\t\"fp_busy\": \"перемещаю {0} файлов...\\n\\n{1}\",\n\t\"fcp_busy\": \"копирую {0} файлов...\\n\\n{1}\",\n\t\"fp_abrt\": \"прерывание...\", //m\n\t\"fp_err\": \"ошибка перемещения:\\n\",\n\t\"fcp_err\": \"ошибка копирования:\\n\",\n\t\"fp_confirm\": \"переместить эти {0} файлов сюда?\",\n\t\"fcp_confirm\": \"скопировать эти {0} файлов сюда?\",\n\t\"fp_etab\": 'ошибка чтения буфера обмена из другой вкладки браузера',\n\t\"fp_name\": \"загружаю файл с вашего устройства. Назовите его:\",\n\t\"fp_both_m\": '<h6>выберите, что вставить</h6><code>Enter</code> = Перенести {0} файлов из «{1}»\\n<code>ESC</code> = Загрузить {2} файлов с вашего устройства',\n\t\"fcp_both_m\": '<h6>выберите, что вставить</h6><code>Enter</code> = Скопировать {0} файлов из «{1}»\\n<code>ESC</code> = Загрузить {2} файлов с вашего устройства',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Переместить</a><a href=\"#\" id=\"modal-ng\">Загрузить</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Скопировать</a><a href=\"#\" id=\"modal-ng\">Загрузить</a>',\n\n\t\"mk_noname\": \"введите имя в текстовое поле слева перед тем, как это делать :p\",\n\t\"nmd_i1\": \"вы также можете указать нужное расширение, например <code>.md</code>\", //m\n\t\"nmd_i2\": \"вы можете создавать только файлы <code>.{0}</code>, так как у вас нет разрешения на удаление\", //m\n\n\t\"tv_load\": \"Загружаю текстовый документ:\\n\\n{0}\\n\\n{1}% ({2} из {3} МиБ загружено)\",\n\t\"tv_xe1\": \"не удалось загрузить текстовый файл:\\n\\nошибка \",\n\t\"tv_xe2\": \"404, файл не найден\",\n\t\"tv_lst\": \"список текстовых файлов в\",\n\t\"tvt_close\": \"вернуться в обзор папки$NГорячая клавиша: M (или Esc)\\\">❌ закрыть\",\n\t\"tvt_dl\": \"скачать этот файл$NГорячая клавиша: Y\\\">💾 скачать\",\n\t\"tvt_prev\": \"показать предыдущий документ$NГорячая клавиша: i\\\">⬆ пред\",\n\t\"tvt_next\": \"показать следующий документ$NГорячая клавиша: K\\\">⬇ след\",\n\t\"tvt_sel\": \"выбрать документ &nbsp; ( для вырезания / копирования / удаления / ... )$NГорячая клавиша: S\\\">выд\",\n\t\"tvt_j\": \"приукрасить json$NГорячая клавиша: shift-J\\\">j\", //m\n\t\"tvt_edit\": \"открыть документ в текстовом редакторе$NГорячая клавиша: E\\\">✏️ изменить\",\n\t\"tvt_tail\": \"проверять файл на изменения; показывать новые строки в реальном времени\\\">📡 обновлять\",\n\t\"tvt_wrap\": \"перенос слов\\\">↵\",\n\t\"tvt_atail\": \"прикрепить вид к низу страницы\\\">⚓\",\n\t\"tvt_ctail\": \"декодировать цвета терминала (ansi escape codes)\\\">🌈\",\n\t\"tvt_ntail\": \"лимит прокрутки (как много байт текста держать в памяти)\",\n\n\t\"m3u_add1\": \"трек добавлен в плейлист m3u\",\n\t\"m3u_addn\": \"{0} треков добавлено в плейлист m3u\",\n\t\"m3u_clip\": \"плейлист m3u скопирован в буфер\\n\\nсоздайте файл с расширением <code>.m3u</code> и вставьте текст туда, чтобы сделать из него плейлист\",\n\n\t\"gt_vau\": \"не показывать видео, только воспроизводить аудио\\\">🎧\",\n\t\"gt_msel\": \"включить режим выделения; держите ctrl при нажатии для инвертации действия$N$N&lt;em&gt;когда активно: дважды кликните на файле / папке, чтобы открыть их&lt;/em&gt;$N$NГорячая клавиша: S\\\">выделение\",\n\t\"gt_crop\": \"обрезать миниатюры\\\">обрезка\",\n\t\"gt_3x\": \"миниатюры высокого разрешения\\\">3x\",\n\t\"gt_zoom\": \"размер\",\n\t\"gt_chop\": \"длина имён\",\n\t\"gt_sort\": \"сортировать по\",\n\t\"gt_name\": \"имени\",\n\t\"gt_sz\": \"размеру\",\n\t\"gt_ts\": \"дате\",\n\t\"gt_ext\": \"типу\",\n\t\"gt_c1\": \"укоротить названия файлов\",\n\t\"gt_c2\": \"удлинить названия файлов\",\n\n\t\"sm_w8\": \"ищем...\",\n\t\"sm_prev\": \"результаты поиска ниже - из предыдущего запроса:\\n  \",\n\t\"sl_close\": \"закрыть результаты поиска\",\n\t\"sl_hits\": \"показ {0} совпадений\",\n\t\"sl_moar\": \"загрузить больше\",\n\n\t\"s_sz\": \"размер\",\n\t\"s_dt\": \"дата\",\n\t\"s_rd\": \"путь\",\n\t\"s_fn\": \"имя\",\n\t\"s_ta\": \"теги\",\n\t\"s_ua\": \"дата⬆️\",\n\t\"s_ad\": \"другое\",\n\t\"s_s1\": \"минимум МиБ\",\n\t\"s_s2\": \"максимум МиБ\",\n\t\"s_d1\": \"мин. iso8601\",\n\t\"s_d2\": \"макс. iso8601\",\n\t\"s_u1\": \"загружено после\",\n\t\"s_u2\": \"и/или до\",\n\t\"s_r1\": \"путь содержит &nbsp; (разделить пробелами)\",\n\t\"s_f1\": \"имя содержит &nbsp; (для исключения писать -nope)\",\n\t\"s_t1\": \"теги содержат &nbsp; (^=начало, конец=$)\",\n\t\"s_a1\": \"свойства метаданных\",\n\n\t\"md_eshow\": \"не удалось показать \",\n\t\"md_off\": \"[📜<em>ридми</em>] отключён в [⚙️] -- документ скрыт\",\n\n\t\"badreply\": \"Ошибка обработки ответа сервера\",\n\n\t\"xhr403\": \"403: Доступ запрещён\\n\\nпопробуйте перезагрузить страницу, возможно, ваша сессия истекла\",\n\t\"xhr0\": \"неизвестно (возможно, потеряно соединение с сервером, либо он отключён)\",\n\t\"cf_ok\": \"просим прощения -- сработала защита от DD\" + wah + \"oS\\n\\nвсё должно вернуться в норму через 30 сек\\n\\nесли ничего не происходит - перезагрузите страницу\",\n\t\"tl_xe1\": \"не удалось показать подпапки:\\n\\nошибка \",\n\t\"tl_xe2\": \"404: Папка не найдена\",\n\t\"fl_xe1\": \"не удалось показать файлы:\\n\\nошибка \",\n\t\"fl_xe2\": \"404: Папка не найдена\",\n\t\"fd_xe1\": \"не удалось создать подпапку:\\n\\nошибка \",\n\t\"fd_xe2\": \"404: Родительская папка не найдена\",\n\t\"fsm_xe1\": \"не удалось отправить сообщение:\\n\\nошибка \",\n\t\"fsm_xe2\": \"404: Родительская папка не найдена\",\n\t\"fu_xe1\": \"не удалось удалить список с сервера:\\n\\nошибка \",\n\t\"fu_xe2\": \"404: Файл не найден??\",\n\n\t\"fz_tar\": \"несжатый файл gnu-tar (linux / mac)\",\n\t\"fz_pax\": \"несжатый pax-форматированный tar (медленнее)\",\n\t\"fz_targz\": \"gnu-tar с 3 уровнем сжатия gzip$N$Nобычно это очень медленно,$Nлучше использовать несжатый tar\",\n\t\"fz_tarxz\": \"gnu-tar с 1 уровнем сжатия xz$N$Nобычно это очень медленно,$Nлучше использовать несжатый tar\",\n\t\"fz_zip8\": \"zip с именами по utf8 (может работать криво на windows 7 и ниже)\",\n\t\"fz_zipd\": \"zip с именами по cp437, для очень старого софта\",\n\t\"fz_zipc\": \"cp437 с предварительным вычислением crc32,$N для MS-DOS PKZIP v2.04g (октябрь 1993)$N(требует больше времени для обработки перед скачиванием)\",\n\n\t\"un_m1\": \"вы можете удалить ваши недавние загрузки (или отменить незавершённые) ниже\",\n\t\"un_upd\": \"обновить\",\n\t\"un_m4\": \"или поделиться файлами снизу:\",\n\t\"un_ulist\": \"показать\",\n\t\"un_ucopy\": \"копировать\",\n\t\"un_flt\": \"опциональный фильтр:&nbsp; адрес должен содержать\",\n\t\"un_fclr\": \"очистить фильтр\",\n\t\"un_derr\": 'ошибка удаления:\\n',\n\t\"un_f5\": 'что-то сломалось, пожалуйста перезагрузите страницу',\n\t\"un_uf5\": \"извините, но вам нужно перезагрузить страницу (F5 или Ctrl+R) перед тем, как отменить эту загрузку\",\n\t\"un_nou\": '<b>внимание:</b> сервер слишком нагружен, чтобы показать незавершённые загрузки; нажмите на ссылку \"обновления\" через пару секунд',\n\t\"un_noc\": '<b>внимание:</b> удаление уже загруженных файлов запрещено конфигурацией сервера',\n\t\"un_max\": \"показаны первые 2000 файлов (используйте фильтр)\",\n\t\"un_avail\": \"{0} недавних загрузок может быть удалено<br />{1} незавершённых может быть отменено\",\n\t\"un_m2\": \"отсортировано по времени загрузки; сначала самые последние:\",\n\t\"un_no1\": \"ха, поверил! достаточно свежих загрузок ещё нет\",\n\t\"un_no2\": \"ха, поверил! достаточно свежих загрузок, соответствующих фильтру, ещё нет\",\n\t\"un_next\": \"удалить следующие {0} файлов ниже\",\n\t\"un_abrt\": \"отменить\",\n\t\"un_del\": \"удалить\",\n\t\"un_m3\": \"загружаю ваши недавние загрузки...\",\n\t\"un_busy\": \"удаляю {0} файлов...\",\n\t\"un_clip\": \"{0} ссылок скопировано в буфер\",\n\n\t\"u_https1\": \"вам стоит\",\n\t\"u_https2\": \"включить https\",\n\t\"u_https3\": \"для лучшей производительности\",\n\t\"u_ancient\": 'у вас действительно антикварный браузер -- возможно, стоит <a href=\"#\" onclick=\"goto(\\'bup\\')\">использовать bup</a>',\n\t\"u_nowork\": \"требуется firefox 53+, chrome 57+ или iOS 11+\",\n\t\"tail_2old\": \"требуется firefox 105+, chrome 71+ или iOS 14.5+\",\n\t\"u_nodrop\": 'ваш браузер слишком старый для загрузки через перетаскивание',\n\t\"u_notdir\": \"это не папка!\\n\\nваш браузер слишком старый,\\nиспользуйте перетаскивание\",\n\t\"u_uri\": \"чтобы перетащить картинку из других окон браузера,\\nотпустите её на большую кнопку загрузки\",\n\t\"u_enpot\": 'переключиться на <a href=\"#\">простой интерфейс</a> (может ускорить загрузку)',\n\t\"u_depot\": 'переключиться на <a href=\"#\">модный интерфейс</a> (может замедлить загрузку)',\n\t\"u_gotpot\": 'переключаюсь на простой интерфес для ускорения загрузки,\\n\\nможете переключиться обратно, если хотите!',\n\t\"u_pott\": \"<p>файлы: &nbsp; <b>{0}</b> завершено, &nbsp; <b>{1}</b> ошибок, &nbsp; <b>{2}</b> загружаются, &nbsp; <b>{3}</b> в очереди</p>\",\n\t\"u_ever\": \"это упрощённый загрузчик; up2k требует хотя бы<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1\",\n\t\"u_su2k\": 'это упрощённый загрузчик; <a href=\"#\" id=\"u2yea\">up2k</a> лучше',\n\t\"u_uput\": 'увеличить скорость (пропуск подсчёта контрольных сумм)',\n\t\"u_ewrite\": 'у вас нет прав на запись в эту папку',\n\t\"u_eread\": 'у вас нет прав на чтение из этой папки',\n\t\"u_enoi\": 'поиск файлов выключен настройками сервера',\n\t\"u_enoow\": \"перезапись здесь не работает; требуются права на удаление\",\n\t\"u_badf\": 'Эти {0} из {1} файлов были пропущены, вероятно, из-за настроек доступа в файловой системе:\\n\\n',\n\t\"u_blankf\": 'Эти {0} из {1} файлов пустые; всё равно загрузить?\\n\\n',\n\t\"u_applef\": 'Эти {0} из {1} файлов, вероятно, нежелательны;\\nНажмите <code>OK/Enter</code>, чтобы ПРОПУСТИТЬ их,\\nНажмите <code>Отмена/ESC</code>, чтобы проигнорировать сообщение и ЗАГРУЗИТЬ их:\\n\\n',\n\t\"u_just1\": '\\nВозможно, будет лучше, если вы выберете только один файл',\n\t\"u_ff_many\": \"если вы используете <b>Linux / MacOS / Android,</b> тогда такое количество файлов <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\"><em>может</em> крашнуть Firefox!</a>\\nв таком случае попробуйте снова (или используйте Chrome).\",\n\t\"u_up_life\": \"Эта загрузка будет удалена с сервера\\nчерез {0} после её завершения\",\n\t\"u_asku\": 'загрузить эти {0} файлов в <code>{1}</code>',\n\t\"u_unpt\": \"вы можете отменить / удалить эту загрузку с помощью 🧯 сверху слева\",\n\t\"u_bigtab\": 'будет показано {0} файлов\\n\\nэто может крашнуть браузер, вы уверены?',\n\t\"u_scan\": 'Сканирую файлы...',\n\t\"u_dirstuck\": 'сканер папки завис при попытке получить доступ к следующим {0} файлам; будет пропущено:',\n\t\"u_etadone\": 'Готово ({0}, {1} файлов)',\n\t\"u_etaprep\": '(подготавливаю загрузку)',\n\t\"u_hashdone\": 'хеширование выполнено',\n\t\"u_hashing\": 'хеш',\n\t\"u_hs\": 'подготовка к хешированию...',\n\t\"u_started\": \"файлы загружаются; подробнее в [🚀]\",\n\t\"u_dupdefer\": \"дубликат; будет обработан после всех остальных файлов\",\n\t\"u_actx\": \"нажмите на этот текст, чтобы предотвратить<br />падение производительности при просмотре других вкладок / окон\",\n\t\"u_fixed\": \"Окей!&nbsp; Исправлено 👍\",\n\t\"u_cuerr\": \"не удалось загрузить фрагмент {0} из {1};\\nвероятно, не критично - продолжаю\\n\\nфайл: {2}\",\n\t\"u_cuerr2\": \"отказ в загрузке (фрагмент {0} из {1});\\nпопытаюсь повторить позже\\n\\nфайл: {2}\\n\\nошибка \",\n\t\"u_ehstmp\": \"попытаюсь повторить позже; подробнее снизу справа\",\n\t\"u_ehsfin\": \"отказ в запросе завершения загрузки; повторяю...\",\n\t\"u_ehssrch\": \"отказ в запросе поиска; повторяю...\",\n\t\"u_ehsinit\": \"отказ в запросе начала загрузки; повторяю...\",\n\t\"u_eneths\": \"ошибка подключения во время подготовки загрузки; повторяю...\",\n\t\"u_enethd\": \"ошибка подключения во время проверки существования целевого файла; повторяю...\",\n\t\"u_cbusy\": \"ожидаю возвращения доступа к серверу после ошибки подключения...\",\n\t\"u_ehsdf\": \"на сервере закончилось место!\\n\\nбуду пытаться повторить, на случай если\\nместо будет освобождено\",\n\t\"u_emtleak1\": \"кажется, у вашего браузера может быть утечка памяти;\\nпожалуйста,\",\n\t\"u_emtleak2\": ' <a href=\"{0}\">перейдите на https (рекомендовано)</a> или ',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": 'попробуйте сделать так:\\n<ul><li>нажмите <code>F5</code> для обновления страницы</li><li>затем отключите опцию &nbsp;<code>мп</code>&nbsp; в &nbsp;<code>⚙️ настройках</code></li><li>и повторите попытку загрузки</li></ul>Она будет чуть медленнее, но что поделать.\\nИзвините за неудобства!\\n\\nPS: в chrome v107 <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">это исправили</a>',\n\t\"u_emtleakf\": 'попробуйте сделать так:\\n<ul><li>нажмите <code>F5</code> для обновления страницы</li><li>затем включите опцию <code>🥔</code> (простой интерфейс) во вкладке загрузок<li>и повторите попытку</li></ul>\\nPS: firefox <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">скоро должны починить</a> в этом аспекте',\n\t\"u_s404\": \"не найдено на сервере\",\n\t\"u_expl\": \"объяснить\",\n\t\"u_maxconn\": \"в большинстве браузеров это нельзя поднять выше 6, но firefox позволяет увеличить лимит с помощью <code>connections-per-server</code> в <code>about:config</code>\",\n\t\"u_tu\": '<p class=\"warn\">ВНИМАНИЕ: активен режим турбо, <span>&nbsp;клиент может игнорировать незавершённые загрузки; подробнее при наведении на кнопку турбо</span></p>',\n\t\"u_ts\": '<p class=\"warn\">ВНИМАНИЕ: активен режим турбо, <span>&nbsp;результаты поиска могут быть некорректными; подробнее при наведении на кнопку турбо</span></p>',\n\t\"u_turbo_c\": \"режим турбо отключён сервером\",\n\t\"u_turbo_g\": \"отключаю турбо, поскольку у вас нет прав\\nна просмотр папок в этом хранилище\",\n\t\"u_life_cfg\": 'автоудаление через <input id=\"lifem\" p=\"60\" /> мин (или <input id=\"lifeh\" p=\"3600\" /> часов)',\n\t\"u_life_est\": 'загрузка будет удалена <span id=\"lifew\" tt=\"local time\">---</span>',\n\t\"u_life_max\": 'эта папка требует\\nавтоудаления файлов через {0}',\n\t\"u_unp_ok\": 'удаление разрешено для {0}',\n\t\"u_unp_ng\": 'удаление НЕ будет разрешено',\n\t\"ue_ro\": 'ваш доступ к данной папке - только чтение\\n\\n',\n\t\"ue_nl\": 'на данный момент вы не авторизованы',\n\t\"ue_la\": 'вы авторизованы как \"{0}\"',\n\t\"ue_sr\": 'сейчас вы в режиме поиска файлов\\n\\nперейдите в режим загрузки, нажав на лупу 🔎 (рядом с огромной кнопкой ИСКАТЬ), и попробуйте снова\\n\\nизвините',\n\t\"ue_ta\": 'попробуйте загрузить снова, теперь должно сработать',\n\t\"ue_ab\": \"этот файл уже загружают в другую папку, та загрузка должна быть завершена перед тем, как загрузить этот же файл в другое место.\\n\\nВы можете отменить свою загрузку через 🧯 сверху слева\",\n\t\"ur_1uo\": \"OK: Файл успешно загружен\",\n\t\"ur_auo\": \"OK: Все {0} файлов успешно загружены\",\n\t\"ur_1so\": \"OK: Файл найден на сервере\",\n\t\"ur_aso\": \"OK: Все {0} файлов найдены на сервере\",\n\t\"ur_1un\": \"Загрузка не удалась, извините\",\n\t\"ur_aun\": \"Все {0} загрузок не удались, извините\",\n\t\"ur_1sn\": \"Файл НЕ был найден на сервере\",\n\t\"ur_asn\": \"Все {0} файлов НЕ было найдено на сервере\",\n\t\"ur_um\": \"Завершено;\\n{0} успешно,\\n{1} ошибок, извините\",\n\t\"ur_sm\": \"Завершено;\\n{0} файлов найдено на сервере,\\n{1} файлов НЕ найдено на сервере\",\n\n\t\"rc_opn\": \"открыть\", //m\n\t\"rc_ply\": \"воспроизвести\", //m\n\t\"rc_pla\": \"воспроизвести как аудио\", //m\n\t\"rc_txt\": \"открыть в просмотрщике файлов\", //m\n\t\"rc_md\": \"открыть в текстовом редакторе\", //m\n\t\"rc_dl\": \"скачать\", //m\n\t\"rc_zip\": \"скачать как архив\", //m\n\t\"rc_cpl\": \"копировать ссылку\", //m\n\t\"rc_del\": \"удалить\", //m\n\t\"rc_cut\": \"вырезать\", //m\n\t\"rc_cpy\": \"копировать\", //m\n\t\"rc_pst\": \"вставить\", //m\n\t\"rc_rnm\": \"переименовать\", //m\n\t\"rc_nfo\": \"новая папка\", //m\n\t\"rc_nfi\": \"новый файл\", //m\n\t\"rc_sal\": \"выбрать всё\", //m\n\t\"rc_sin\": \"инвертировать выделение\", //m\n\t\"rc_shf\": \"поделиться этой папкой\", //m\n\t\"rc_shs\": \"поделиться выделенным\", //m\n\n\t\"lang_set\": \"перезагрузить страницу, чтобы применить изменения?\",\n\n\t\"splash\": {\n\t\t\"a1\": \"обновить\",\n\t\t\"b1\": \"приветик, незнакомец &nbsp; <small>(вы не авторизованы)</small>\",\n\t\t\"c1\": \"выйти\",\n\t\t\"d1\": \"трассировка стека\",\n\t\t\"d2\": \"показывает состояние всех активных потоков\",\n\t\t\"e1\": \"перезагрузить конфиг\",\n\t\t\"e2\": \"перезагрузить файлы конфига (аккаунты/хранилища/флаги),$Nи пересканировать все хранилища с флагом e2ds$N$Nвнимание: изменения глобальных настроек$Nтребуют полного перезапуска сервера\",\n\t\t\"f1\": \"вы можете видеть:\",\n\t\t\"g1\": \"вы можете загружать файлы в:\",\n\t\t\"cc1\": \"всякая всячина:\",\n\t\t\"h1\": \"отключить k304\",\n\t\t\"i1\": \"включить k304\",\n\t\t\"j1\": \"включённый k304 будет отключать вас при получении HTTP 304, что может помочь при работе с некоторыми глючными прокси (перестают загружаться страницы), <em>но</em> это также сделает работу клиента медленнее\",\n\t\t\"k1\": \"сбросить локальные настройки\",\n\t\t\"l1\": \"авторизуйтесь для других опций:\",\n\t\t\"ls3\": \"войти\", //m\n\t\t\"lu4\": \"имя пользователя\", //m\n\t\t\"lp4\": \"пароль\", //m\n\t\t\"lo3\": \"выйти из “{0}” везде\", //m\n\t\t\"lo2\": \"это завершит сеанс во всех браузерах\", //m\n\t\t\"m1\": \"с возвращением,\",\n\t\t\"n1\": \"404 не найдено &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": 'или у вас нет доступа -- попробуйте авторизоваться или <a href=\"' + SR + '/?h\">вернуться на главную</a>',\n\t\t\"p1\": \"403 доступ запрещён &nbsp;~┻━┻\",\n\t\t\"q1\": 'авторизуйтесь или <a href=\"' + SR + '/?h\">вернитесь на главную</a>',\n\t\t\"r1\": \"вернуться на главную\",\n\t\t\".s1\": \"пересканировать\",\n\t\t\"t1\": \"действия\",\n\t\t\"u2\": \"время с последней записи на сервер$N( загрузка / переименование / ... )$N$N17d = 17 дней$N1h23 = 1 час 23 минут$N4m56 = 4 минут 56 секунд\",\n\t\t\"v1\": \"подключить\",\n\t\t\"v2\": \"использовать сервер как локальный диск\",\n\t\t\"w1\": \"перейти на https\",\n\t\t\"x1\": \"поменять пароль\",\n\t\t\"y1\": \"управление доступом\",\n\t\t\"z1\": \"разблокировать:\",\n\t\t\"ta1\": \"сначала введите свой новый пароль\",\n\t\t\"ta2\": \"повторите новый пароль:\",\n\t\t\"ta3\": \"опечатка; попробуйте снова\",\n\t\t\"nop\": \"ОШИБКА: Пароль не может быть пустым\", //m\n\t\t\"nou\": \"ОШИБКА: Имя пользователя и/или пароль не могут быть пустыми\", //m\n\t\t\"aa1\": \"входящие файлы:\",\n\t\t\"ab1\": \"отключить no304\",\n\t\t\"ac1\": \"включить no304\",\n\t\t\"ad1\": \"включённый no304 полностью отключит хеширование; используйте, если k304 не помог. Сильно увеличит объём трафика!\",\n\t\t\"ae1\": \"активные скачивания:\",\n\t\t\"af1\": \"показать недавние загрузки\",\n\t\t\"ag1\": \"показать известных IdP-пользователей\",\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/spa.js",
    "content": "\n// Las líneas que terminan con //m son traducciones automáticas no verificadas\n\nLs.spa = {\n\t\"tt\": \"Español\",\n\n\t\"cols\": {\n\t\t\"c\": \"acciones\",\n\t\t\"dur\": \"duración\",\n\t\t\"q\": \"calidad / bitrate\",\n\t\t\"Ac\": \"códec de audio\",\n\t\t\"Vc\": \"códec de vídeo\",\n\t\t\"Fmt\": \"formato / contenedor\",\n\t\t\"Ahash\": \"checksum de audio\",\n\t\t\"Vhash\": \"checksum de vídeo\",\n\t\t\"Res\": \"resolución\",\n\t\t\"T\": \"tipo de archivo\",\n\t\t\"aq\": \"calidad de audio / bitrate\",\n\t\t\"vq\": \"calidad de vídeo / bitrate\",\n\t\t\"pixfmt\": \"submuestreo / estructura de píxel\",\n\t\t\"resw\": \"resolución horizontal\",\n\t\t\"resh\": \"resolución vertical\",\n\t\t\"chs\": \"canales de audio\",\n\t\t\"hz\": \"frecuencia de muestreo\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"varios\",\n\t\t\t[\"ESC\", \"cerrar varias cosas\"],\n\n\t\t\t\"gestor de archivos\",\n\t\t\t[\"G\", \"alternar vista de lista / cuadrícula\"],\n\t\t\t[\"T\", \"alternar miniaturas / iconos\"],\n\t\t\t[\"⇧ A/D\", \"tamaño de miniatura\"],\n\t\t\t[\"ctrl-K\", \"eliminar seleccionados\"],\n\t\t\t[\"ctrl-X\", \"cortar selección al portapapeles\"],\n\t\t\t[\"ctrl-C\", \"copiar selección al portapapeles\"],\n\t\t\t[\"ctrl-V\", \"pegar (mover/copiar) aquí\"],\n\t\t\t[\"Y\", \"descargar seleccionados\"],\n\t\t\t[\"F2\", \"renombrar seleccionados\"],\n\n\t\t\t\"selección en lista de archivos\",\n\t\t\t[\"space\", \"alternar selección de archivo\"],\n\t\t\t[\"↑/↓\", \"mover cursor de selección\"],\n\t\t\t[\"ctrl ↑/↓\", \"mover cursor y vista\"],\n\t\t\t[\"⇧ ↑/↓\", \"seleccionar anterior/siguiente archivo\"],\n\t\t\t[\"ctrl-A\", \"seleccionar todos los archivos / carpetas\"]\n\t\t], [\n\t\t\t\"navegación\",\n\t\t\t[\"B\", \"alternar breadcrumbs / panel de navegación\"],\n\t\t\t[\"I/K\", \"anterior/siguiente carpeta\"],\n\t\t\t[\"M\", \"carpeta de nivel superior (o contraer actual)\"],\n\t\t\t[\"V\", \"alternar carpetas / archivos en panel de navegación\"],\n\t\t\t[\"A/D\", \"tamaño del panel de navegación\"]\n\t\t], [\n\t\t\t\"reproductor de audio\",\n\t\t\t[\"J/L\", \"anterior/siguiente canción\"],\n\t\t\t[\"U/O\", \"saltar 10s atrás/adelante\"],\n\t\t\t[\"0..9\", \"saltar a 0%..90%\"],\n\t\t\t[\"P\", \"reproducir/pausar (también inicia)\"],\n\t\t\t[\"S\", \"seleccionar canción en reproducción\"],\n\t\t\t[\"Y\", \"descargar canción\"]\n\t\t], [\n\t\t\t\"visor de imágenes\",\n\t\t\t[\"J/L, ←/→\", \"anterior/siguiente imagen\"],\n\t\t\t[\"Home/End\", \"primera/última imagen\"],\n\t\t\t[\"F\", \"pantalla completa\"],\n\t\t\t[\"R\", \"rotar en sentido horario\"],\n\t\t\t[\"⇧ R\", \"rotar en sentido antihorario\"],\n\t\t\t[\"S\", \"seleccionar imagen\"],\n\t\t\t[\"Y\", \"descargar imagen\"]\n\t\t], [\n\t\t\t\"reproductor de vídeo\",\n\t\t\t[\"U/O\", \"saltar 10s atrás/adelante\"],\n\t\t\t[\"P/K/Space\", \"reproducir/pausar\"],\n\t\t\t[\"C\", \"continuar con el siguiente\"],\n\t\t\t[\"V\", \"bucle\"],\n\t\t\t[\"M\", \"silenciar\"],\n\t\t\t[\"[ y ]\", \"establecer intervalo de bucle\"]\n\t\t], [\n\t\t\t\"visor de texto\",\n\t\t\t[\"I/K\", \"anterior/siguiente archivo\"],\n\t\t\t[\"M\", \"cerrar archivo\"],\n\t\t\t[\"E\", \"editar archivo\"],\n\t\t\t[\"S\", \"seleccionar archivo (para cortar/copiar/renombrar)\"],\n\t\t\t[\"Y\", \"descargar archivo de texto\"], //m\n\t\t\t[\"⇧ J\", \"embellecer json\"], //m\n\t\t]\n\t],\n\n\t\"m_ok\": \"Aceptar\",\n\t\"m_ng\": \"Cancelar\",\n\n\t\"enable\": \"Activar\",\n\t\"danger\": \"PELIGRO\",\n\t\"clipped\": \"copiado al portapapeles\",\n\n\t\"ht_s1\": \"segundo\",\n\t\"ht_s2\": \"segundos\",\n\t\"ht_m1\": \"minuto\",\n\t\"ht_m2\": \"minutos\",\n\t\"ht_h1\": \"hora\",\n\t\"ht_h2\": \"horas\",\n\t\"ht_d1\": \"día\",\n\t\"ht_d2\": \"días\",\n\t\"ht_and\": \" y \",\n\n\t\"goh\": \"panel de control\",\n\t\"gop\": 'hermano anterior\">anterior',\n\t\"gou\": 'carpeta de nivel superior\">subir',\n\t\"gon\": 'siguiente carpeta\">siguiente',\n\t\"logout\": \"Cerrar sesión \",\n\t\"login\": \"Iniciar sesión\", //m\n\t\"access\": \" acceso\",\n\t\"ot_close\": \"cerrar submenú\",\n\t\"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`&quot;try unite&quot;` = contiene exactamente «try unite»$N$Nel formato de fecha es iso-8601, como$N`2009-12-31` o `2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"dessubir: elimina tus subidas recientes, o aborta las inacabadas\",\n\t\"ot_bup\": \"bup: uploader básico, soporta hasta netscape 4.0\",\n\t\"ot_mkdir\": \"mkdir: crear un nuevo directorio\",\n\t\"ot_md\": \"new-file: crear un nuevo archivo de texto\", //m\n\t\"ot_msg\": \"msg: enviar un mensaje al registro del servidor\",\n\t\"ot_mp\": \"opciones del reproductor multimedia\",\n\t\"ot_cfg\": \"opciones de configuración\",\n\t\"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 [🎈]&nbsp; (el uploader básico)<br /><br />¡Durante las subidas, este icono se convierte en un indicador de progreso!\",\n\t\"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 [🎈]&nbsp; (el uploader básico)<br /><br />¡Durante las subidas, este icono se convierte en un indicador de progreso!\",\n\t\"ot_noie\": \"Por favor, usa Chrome / Firefox / Edge\",\n\n\t\"ab_mkdir\": \"crear directorio\",\n\t\"ab_mkdoc\": \"nuevo archivo de texto\", //m\n\t\"ab_msg\": \"enviar msg al registro del servidor\",\n\n\t\"ay_path\": \"saltar a carpetas\",\n\t\"ay_files\": \"saltar a archivos\",\n\n\t\"wt_ren\": \"renombrar elementos seleccionados$NAtajo: F2\",\n\t\"wt_del\": \"eliminar elementos seleccionados$NAtajo: ctrl-K\",\n\t\"wt_cut\": \"cortar elementos seleccionados &lt;small&gt;(luego pegar en otro lugar)&lt;/small&gt;$NAtajo: ctrl-X\",\n\t\"wt_cpy\": \"copiar elementos seleccionados al portapapeles$N(para pegarlos en otro lugar)$NAtajo: ctrl-C\",\n\t\"wt_pst\": \"pegar una selección previamente cortada / copiada$NAtajo: ctrl-V\",\n\t\"wt_selall\": \"seleccionar todos los archivos$NAtajo: ctrl-A (con un archivo con foco)\",\n\t\"wt_selinv\": \"invertir selección\",\n\t\"wt_zip1\": \"descargar esta carpeta como un archivo comprimido\",\n\t\"wt_selzip\": \"descargar selección como archivo comprimido\",\n\t\"wt_seldl\": \"descargar selección como archivos separados$NAtajo: Y\",\n\t\"wt_npirc\": \"copiar información de pista en formato IRC\",\n\t\"wt_nptxt\": \"copiar información de pista en texto plano\",\n\t\"wt_m3ua\": \"añadir a lista m3u (haz clic en <code>📻copiar</code> después)\",\n\t\"wt_m3uc\": \"copiar lista m3u al portapapeles\",\n\t\"wt_grid\": \"alternar vista de cuadrícula / lista$NAtajo: G\",\n\t\"wt_prev\": \"pista anterior$NAtajo: J\",\n\t\"wt_play\": \"reproducir / pausar$NAtajo: P\",\n\t\"wt_next\": \"siguiente pista$NAtajo: L\",\n\n\t\"ul_par\": \"subidas paralelas:\",\n\t\"ut_rand\": \"aleatorizar nombres de archivo\",\n\t\"ut_u2ts\": 'copiar la fecha de última modificación$Nde tu sistema de archivos al servidor\">📅',\n\t\"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\n\t\"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\",\n\t\"ut_ask\": 'pedir confirmación antes de iniciar la subida\">💭',\n\t\"ut_pot\": \"mejorar la velocidad de subida en dispositivos lentos$Nsimplificando la interfaz de usuario\",\n\t\"ut_srch\": \"no subir, en su lugar comprobar si los archivos ya $N existen en el servidor (escaneará todas las carpetas que puedas leer)\",\n\t\"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\",\n\t\"ul_btn\": \"arrastra archivos / carpetas<br>aquí (o haz clic)\",\n\t\"ul_btnu\": \"S U B I R\",\n\t\"ul_btns\": \"B U S C A R\",\n\n\t\"ul_hash\": \"hash\",\n\t\"ul_send\": \"envio\",\n\t\"ul_done\": \"hecho\",\n\t\"ul_idle1\": \"aún no hay subidas en cola\",\n\t\"ut_etah\": \"velocidad media de &lt;em&gt;hashing&lt;/em&gt;, y tiempo estimado para finalizar\",\n\t\"ut_etau\": \"velocidad media de &lt;em&gt;subida&lt;/em&gt; y tiempo estimado para finalizar\",\n\t\"ut_etat\": \"velocidad media &lt;em&gt;total&lt;/em&gt; y tiempo estimado para finalizar\",\n\n\t\"uct_ok\": \"completado con éxito\",\n\t\"uct_ng\": \"fallido: error / rechazado / no encontrado\",\n\t\"uct_done\": \"éxitos y fallos combinados\",\n\t\"uct_bz\": \"generando hash o subiendo\",\n\t\"uct_q\": \"inactivo, pendiente\",\n\n\t\"utl_name\": \"nombre de archivo\",\n\t\"utl_ulist\": \"lista\",\n\t\"utl_ucopy\": \"copiar\",\n\t\"utl_links\": \"enlaces\",\n\t\"utl_stat\": \"estado\",\n\t\"utl_prog\": \"progreso\",\n\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"ERROR\",\n\t\"utl_oserr\": \"Error-SO\",\n\t\"utl_found\": \"encontrado\",\n\t\"utl_defer\": \"posponer\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"hecho\",\n\n\t\"ul_flagblk\": \"los archivos se añadieron a la cola</b><br>sin embargo, hay un up2k ocupado en otra pestaña del navegador,<br>esperando a que termine primero\",\n\t\"ul_btnlk\": \"la configuración del servidor ha bloqueado esta opción en este estado\",\n\n\t\"udt_up\": \"Subir\",\n\t\"udt_srch\": \"Buscar\",\n\t\"udt_drop\": \"suéltalo aquí\",\n\n\t\"u_nav_m\": \"<h6>vale, ¿qué tienes?</h6><code>Intro</code> = Archivos (uno o más)\\n<code>ESC</code> = Una carpeta (incluyendo subcarpetas)\",\n\t\"u_nav_b\": \"<a href=\\\"#\\\" id=\\\"modal-ok\\\">Archivos</a><a href=\\\"#\\\" id=\\\"modal-ng\\\">Una carpeta</a>\",\n\n\t\"cl_opts\": \"opciones\",\n\t\"cl_hfsz\": \"tamaño del archivo\", //m\n\t\"cl_themes\": \"tema\",\n\t\"cl_langs\": \"idioma\",\n\t\"cl_ziptype\": \"descarga de carpeta\",\n\t\"cl_uopts\": \"opciones up2k\",\n\t\"cl_favico\": \"favicon\",\n\t\"cl_bigdir\": \"directorios grandes\",\n\t\"cl_hsort\": \"#ordenar\",\n\t\"cl_keytype\": \"notación musical\",\n\t\"cl_hiddenc\": \"columnas ocultas\",\n\t\"cl_hidec\": \"ocultar\",\n\t\"cl_reset\": \"restablecer\",\n\t\"cl_hpick\": \"toca en las cabeceras de columna para ocultarlas en la tabla de abajo\",\n\t\"cl_hcancel\": \"ocultación de columna cancelada\",\n\t\"cl_rcm\": \"menú contextual\", //m\n\n\t\"ct_grid\": '田 cuadrícula',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ tooltips',\n\t\"ct_thumb\": 'en vista de cuadrícula, alternar iconos o miniaturas$NAtajo: T\">🖼️ miniaturas',\n\t\"ct_csel\": 'usa CTRL y SHIFT para seleccionar archivos en la vista de cuadrícula\">sel',\n\t\"ct_dsel\": 'usa la selección por arrastre en la vista de cuadrícula\">arrastrar', //m\n\t\"ct_dl\": 'forzar descarga (no mostrar en línea) al hacer clic en un archivo\">dl', //m\n\t\"ct_ihop\": 'al cerrar el visor de imágenes, desplazarse hasta el último archivo visto\">g⮯',\n\t\"ct_dots\": 'mostrar archivos ocultos (si el servidor lo permite)\">archivos ocultos',\n\t\"ct_qdel\": 'al eliminar archivos, pedir confirmación solo una vez\">elim. rápida',\n\t\"ct_dir1st\": 'ordenar carpetas antes que archivos\">📁 primero',\n\t\"ct_nsort\": 'orden natural (para nombres de archivo con dígitos iniciales)\">ord. natural',\n\t\"ct_utc\": 'use UTC para todas las horas\">UTC', //m\n\t\"ct_readme\": 'mostrar README.md en los listados de carpetas\">📜 léeme',\n\t\"ct_idxh\": 'mostrar index.html en lugar del listado de carpetas\">htm',\n\t\"ct_sbars\": 'mostrar barra lateral\">⟊',\n\n\t\"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📅',\n\n\t\"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 <em>&quot;¿tiene este el mismo tamaño de archivo en el servidor?&quot;</em> así que si el contenido del archivo es diferente, NO se subirá$N$Ndeberías desactivar esto cuando la subida termine, y luego &quot;subir&quot; los mismos archivos de nuevo para que el cliente los verifique\">turbo',\n\n\t\"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$N<em>teóricamente</em> 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',\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cut_nag\": \"notificación del SO cuando la subida se complete$N(solo si el navegador o la pestaña no están activos)\",\n\t\"cut_sfx\": \"alerta sonora cuando la subida se complete$N(solo si el navegador o la pestaña no están activos)\",\n\n\t\"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',\n\n\t\"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',\n\n\t\"cft_text\": \"texto del favicon (dejar en blanco y refrescar para desactivar)\",\n\t\"cft_fg\": \"color de primer plano\",\n\t\"cft_bg\": \"color de fondo\",\n\n\t\"cdt_lim\": \"número máximo de archivos a mostrar en una carpeta\",\n\t\"cdt_ask\": \"al llegar al final,$Nen lugar de cargar más archivos,$Npreguntar qué hacer\",\n\t\"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\",\n\t\"cdt_ren\": \"habilitar menú contextual personalizado, el menú normal sigue siendo accesible con shift + clic derecho\\\">activar\", //m\n\t\"cdt_rdb\": \"mostrar el menú normal de clic derecho cuando el personalizado ya está abierto y se vuelve a hacer clic\\\">x2\", //m\n\n\t\"tt_entree\": \"mostrar panel de navegación (barra lateral con árbol de directorios)$NAtajo: B\",\n\t\"tt_detree\": \"mostrar breadcrumbs$NAtajo: B\",\n\t\"tt_visdir\": \"desplazarse a la carpeta seleccionada\",\n\t\"tt_ftree\": \"alternar árbol de carpetas / archivos de texto$NAtajo: V\",\n\t\"tt_pdock\": \"mostrar carpetas de niveles superiores en un panel acoplado en la parte superior\",\n\t\"tt_dynt\": \"crecimiento automático a medida que el árbol se expande\",\n\t\"tt_wrap\": \"ajuste de línea\",\n\t\"tt_hover\": \"revelar líneas que se desbordan al pasar el ratón$N( rompe el desplazamiento a menos que el $N&nbsp; cursor esté en el margen izquierdo )\",\n\n\t\"ml_pmode\": \"al final de la carpeta...\",\n\t\"ml_btns\": \"acciones\",\n\t\"ml_tcode\": \"transcodificar\",\n\t\"ml_tcode2\": \"transcodificar a\",\n\t\"ml_tint\": \"tinte\",\n\t\"ml_eq\": \"ecualizador de audio\",\n\t\"ml_drc\": \"compresor de rango dinámico\",\n\t\"ml_ss\": \"saltar silencios\", //m\n\n\t\"mt_loop\": 'poner en bucle/repetir una canción\">🔁',\n\t\"mt_one\": 'parar después de una canción\">1️⃣',\n\t\"mt_shuf\": 'reproducir aleatoriamente las canciones en cada carpeta\">🔀',\n\t\"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▶',\n\t\"mt_preload\": 'empezar a cargar la siguiente canción cerca del final para una reproducción sin pausas\">precarga',\n\t\"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',\n\t\"mt_fullpre\": 'intentar precargar la canción entera;$N✅ activar en conexiones <b>inestables</b>,$N❌ <b>desactivar</b> probablemente en conexiones lentas\">completa',\n\t\"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)\">☕️',\n\t\"mt_waves\": 'barra de búsqueda con forma de onda:$Nmostrar la amplitud del audio en la barra de progreso\">~s',\n\t\"mt_npclip\": 'mostrar botones para copiar al portapapeles la canción actual\">/np',\n\t\"mt_m3u_c\": 'mostrar botones para copiar al portapapeles las$Ncanciones seleccionadas como entradas de lista m3u8\">📻',\n\t\"mt_octl\": 'integración con SO (teclas multimedia / OSD)\">ctl-so',\n\t\"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',\n\t\"mt_oscv\": 'mostrar carátula del álbum en OSD\">arte',\n\t\"mt_follow\": 'mantener la pista en reproducción visible en pantalla\">🎯',\n\t\"mt_compact\": 'controles compactos\">⟎',\n\t\"mt_uncache\": 'limpiar caché &nbsp;(prueba esto si tu navegador guardó en caché$Nuna copia rota de una canción que se niega a reproducir)\">limpiar caché',\n\t\"mt_mloop\": 'repetir la carpeta actual\">🔁 bucle',\n\t\"mt_mnext\": 'cargar la siguiente carpeta y continuar\">📂 sig',\n\t\"mt_mstop\": 'detener reproducción\">⏸ parar',\n\t\"mt_cflac\": 'convertir flac / wav a {0}\">flac',\n\t\"mt_caac\": 'convertir aac / m4a a {0}\">aac',\n\t\"mt_coth\": 'convertir todos los demás (no mp3) a {0}\">oth',\n\t\"mt_c2opus\": 'la mejor opción para ordenadores, portátiles, android\">opus',\n\t\"mt_c2owa\": 'opus-weba, para iOS 17.5 y superior\">owa',\n\t\"mt_c2caf\": 'opus-caf, para iOS 11 a 17\">caf',\n\t\"mt_c2mp3\": 'usar en dispositivos muy antiguos\">mp3',\n\t\"mt_c2flac\": \"la mejor calidad de sonido,$Npero descargas muy grandes\\\">flac\", //m\n\t\"mt_c2wav\": \"reproducción sin comprimir (aún más grande)\\\">wav\", //m\n\t\"mt_c2ok\": \"bien, buena elección\",\n\t\"mt_c2nd\": \"ese no es el formato de salida recomendado para tu dispositivo, pero está bien\",\n\t\"mt_c2ng\": \"tu dispositivo no parece soportar este formato de salida, pero intentémoslo de todas formas\",\n\t\"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\",\n\t\"mt_tint\": \"nivel de fondo (0-100) en la barra de búsqueda$Npara hacer el buffering menos molesto\",\n\t\"mt_eq\": \"`activa el ecualizador y el control de ganancia;$N$Nganancia `0` = volumen estándar 100% (sin modificar)$N$Nancho `1 &nbsp;` = estéreo estándar (sin modificar)$Nancho `0.5` = 50% de crossfeed izq-der$Nancho `0 &nbsp;` = 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\",\n\t\"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)\",\n\t\"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\n\t\"mt_ssvt\": \"umbral de volumen (0-255)\\\">vol\", //m\n\t\"mt_ssts\": \"umbral activo (% pista, inicio)\\\">ini\", //m\n\t\"mt_sste\": \"umbral activo (% pista, fin)\\\">fin\", //m\n\t\"mt_sssm\": \"multiplicador de velocidad de reproducción\\\">av\", //m\n\n\t\"mb_play\": \"reproducir\",\n\t\"mm_hashplay\": \"¿reproducir este archivo de audio?\",\n\t\"mm_m3u\": \"pulsa <code>Intro/Aceptar</code> para Reproducir\\npulsa <code>ESC/Cancelar</code> para Editar\",\n\t\"mp_breq\": \"se necesita firefox 82+ o chrome 73+ o iOS 15+\",\n\t\"mm_bload\": \"cargando...\",\n\t\"mm_bconv\": \"convirtiendo a {0}, por favor espera...\",\n\t\"mm_opusen\": \"tu navegador no puede reproducir archivos aac / m4a;\\nse ha activado la transcodificación a opus\",\n\t\"mm_playerr\": \"fallo de reproducción: \",\n\t\"mm_eabrt\": \"El intento de reproducción fue cancelado\",\n\t\"mm_enet\": \"Tu conexión a internet es inestable\",\n\t\"mm_edec\": \"¿Este archivo está supuestamente corrupto?\",\n\t\"mm_esupp\": \"Tu navegador no entiende este formato de audio\",\n\t\"mm_eunk\": \"Error desconocido\",\n\t\"mm_e404\": \"No se pudo reproducir el audio; error 404: Archivo no encontrado.\",\n\t\"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\",\n\t\"mm_e415\": \"No se pudo reproducir el audio; error 415: Falló la conversión del archivo; revisa los registros del servidor.\", //m\n\t\"mm_e500\": \"No se pudo reproducir el audio; error 500: Revisa los registros del servidor.\",\n\t\"mm_e5xx\": \"No se pudo reproducir el audio; error del servidor \",\n\t\"mm_nof\": \"no se encuentran más archivos de audio cerca\",\n\t\"mm_prescan\": \"Buscando música para reproducir a continuación...\",\n\t\"mm_scank\": \"Encontrada la siguiente canción:\",\n\t\"mm_uncache\": \"caché limpiada; todas las canciones se volverán a descargar en la próxima reproducción\",\n\t\"mm_hnf\": \"esa canción ya no existe\",\n\n\t\"im_hnf\": \"esa imagen ya no existe\",\n\n\t\"f_empty\": \"esta carpeta está vacía\",\n\t\"f_chide\": \"esto ocultará la columna «{0}»\\n\\npuedes volver a mostrar las columnas en la pestaña de configuración\",\n\t\"f_bigtxt\": \"este archivo pesa {0} MiB -- ¿realmente verlo como texto?\",\n\t\"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\",\n\t\"fbd_more\": '<div id=\"blazy\">mostrando <code>{0}</code> de <code>{1}</code> archivos; <a href=\"#\" id=\"bd_more\">mostrar {2}</a> o <a href=\"#\" id=\"bd_all\">mostrar todos</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\">mostrando <code>{0}</code> de <code>{1}</code> archivos; <a href=\"#\" id=\"bd_all\">mostrar todos</a></div>',\n\t\"f_anota\": \"solo {0} de los {1} elementos fueron seleccionados;\\npara seleccionar la carpeta completa, primero desplázate hasta el final\",\n\n\t\"f_dls\": \"los enlaces a archivos en la carpeta actual se han\\nconvertido en enlaces de descarga\",\n\t\"f_dl_nd\": 'omitiendo carpeta (use la descarga zip/tar en su lugar):\\n', //m\n\n\t\"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 <code>.PARTIAL</code>. Por favor, pulsa CANCELAR o Escape para hacer esto.\\n\\nPulsar ACEPTAR o Intro ignorará esta advertencia y continuará descargando el archivo temporal <code>.PARTIAL</code>, lo que casi con toda seguridad te dará datos corruptos.\",\n\n\t\"ft_paste\": \"pegar {0} elementos$NAtajo: ctrl-V\",\n\t\"fr_eperm\": \"no se puede renombrar:\\nno tienes permiso de “mover” en esta carpeta\",\n\t\"fd_eperm\": \"no se puede eliminar:\\nno tienes permiso de “eliminar” en esta carpeta\",\n\t\"fc_eperm\": \"no se puede cortar:\\nno tienes permiso de “mover” en esta carpeta\",\n\t\"fp_eperm\": \"no se puede pegar:\\nno tienes permiso de “escribir” en esta carpeta\",\n\t\"fr_emore\": \"selecciona al menos un elemento para renombrar\",\n\t\"fd_emore\": \"selecciona al menos un elemento para eliminar\",\n\t\"fc_emore\": \"selecciona al menos un elemento para cortar\",\n\t\"fcp_emore\": \"selecciona al menos un elemento para copiar al portapapeles\",\n\n\t\"fs_sc\": \"compartir la carpeta en la que estás\",\n\t\"fs_ss\": \"compartir los archivos seleccionados\",\n\t\"fs_just1d\": \"no puedes seleccionar más de una carpeta,\\no mezclar archivos y carpetas en una selección\",\n\t\"fs_abrt\": \"❌ abortar\",\n\t\"fs_rand\": \"🎲 nombre aleatorio\",\n\t\"fs_go\": \"✅ crear enlace\",\n\t\"fs_name\": \"nombre\",\n\t\"fs_src\": \"origen\",\n\t\"fs_pwd\": \"contraseña\",\n\t\"fs_exp\": \"caducidad\",\n\t\"fs_tmin\": \"minutos\",\n\t\"fs_thrs\": \"horas\",\n\t\"fs_tdays\": \"días\",\n\t\"fs_never\": \"eterno\",\n\t\"fs_pname\": \"nombre opcional del enlace; será aleatorio si se deja en blanco\",\n\t\"fs_tsrc\": \"el archivo o carpeta a compartir\",\n\t\"fs_ppwd\": \"contraseña opcional\",\n\t\"fs_w8\": \"creando enlace...\",\n\t\"fs_ok\": \"pulsa <code>Intro/Aceptar</code> para Copiar al Portapapeles\\npulsa <code>ESC/Cancelar</code> para Cerrar\",\n\n\t\"frt_dec\": \"puede arreglar algunos casos de nombres de archivo rotos\\\">url-decode\",\n\t\"frt_rst\": \"restaurar los nombres de archivo modificados a los originales\\\">↺ restablecer\",\n\t\"frt_abrt\": \"abortar y cerrar esta ventana\\\">❌ cancelar\",\n\t\"frb_apply\": \"APLICAR RENOMBRADO\",\n\t\"fr_adv\": \"renombrado por lotes / metadatos / patrones\\\">avanzado\",\n\t\"fr_case\": \"regex sensible a mayúsculas\\\">mayús\",\n\t\"fr_win\": \"nombres seguros para windows; reemplaza <code>&lt;&gt;:&quot;\\\\|?*</code> con caracteres japoneses de ancho completo\\\">win\",\n\t\"fr_slash\": \"reemplaza <code>/</code> con un carácter que no cree nuevas carpetas\\\">sin /\",\n\t\"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\",\n\t\"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\",\n\t\"fr_pdel\": \"eliminar\",\n\t\"fr_pnew\": \"guardar como\",\n\t\"fr_pname\": \"proporciona un nombre para tu nuevo preajuste\",\n\t\"fr_aborted\": \"abortado\",\n\t\"fr_lold\": \"nombre antiguo\",\n\t\"fr_lnew\": \"nombre nuevo\",\n\t\"fr_tags\": \"etiquetas para los archivos seleccionados (solo lectura, como referencia):\",\n\t\"fr_busy\": \"renombrando {0} elementos...\\n\\n{1}\",\n\t\"fr_efail\": \"fallo al renombrar:\\n\",\n\t\"fr_nchg\": \"{0} de los nuevos nombres fueron alterados debido a <code>win</code> y/o <code>sin /</code>\\n\\n¿Aceptar para continuar con estos nuevos nombres alterados?\",\n\n\t\"fd_ok\": \"eliminación correcta\",\n\t\"fd_err\": \"fallo al eliminar:\\n\",\n\t\"fd_none\": \"no se eliminó nada; quizás bloqueado por la configuración del servidor (xbd)?\",\n\t\"fd_busy\": \"eliminando {0} elementos...\\n\\n{1}\",\n\t\"fd_warn1\": \"¿ELIMINAR estos {0} elementos?\",\n\t\"fd_warn2\": \"<b>¡Última oportunidad!</b> No se puede deshacer. ¿Eliminar?\",\n\n\t\"fc_ok\": \"cortados {0} elementos\",\n\t\"fc_warn\": \"cortados {0} elementos\\n\\npero: solo <b>esta</b> pestaña del navegador puede pegarlos\\n(dado que la selección es absolutamente masiva)\",\n\n\t\"fcc_ok\": \"copiados {0} elementos al portapapeles\",\n\t\"fcc_warn\": \"copiados {0} elementos al portapapeles\\n\\npero: solo <b>esta</b> pestaña del navegador puede pegarlos\\n(dado que la selección es absolutamente masiva)\",\n\n\t\"fp_apply\": \"usar estos nombres\",\n\t\"fp_skip\": \"omitir conflictos\", //m\n\t\"fp_ecut\": \"primero corta o copia algunos archivos / carpetas para pegar / mover\\n\\nnota: puedes cortar / pegar entre diferentes pestañas del navegador\",\n\t\"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\n\t\"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\n\t\"fp_emore\": \"todavía quedan algunas colisiones de nombres por resolver\",\n\t\"fp_ok\": \"movimiento correcto\",\n\t\"fcp_ok\": \"copia correcta\",\n\t\"fp_busy\": \"moviendo {0} elementos...\\n\\n{1}\",\n\t\"fcp_busy\": \"copiando {0} elementos...\\n\\n{1}\",\n\t\"fp_abrt\": \"cancelando...\", //m\n\t\"fp_err\": \"fallo al mover:\\n\",\n\t\"fcp_err\": \"fallo al copiar:\\n\",\n\t\"fp_confirm\": \"¿mover estos {0} elementos aquí?\",\n\t\"fcp_confirm\": \"¿copiar estos {0} elementos aquí?\",\n\t\"fp_etab\": \"fallo al leer el portapapeles de otra pestaña del navegador\",\n\t\"fp_name\": \"subiendo un archivo desde tu dispositivo. Dale un nombre:\",\n\t\"fp_both_m\": \"<h6>elige qué pegar</h6><code>Intro</code> = Mover {0} archivos desde «{1}»\\n<code>ESC</code> = Subir {2} archivos desde tu dispositivo\",\n\t\"fcp_both_m\": \"<h6>elige qué pegar</h6><code>Intro</code> = Copiar {0} archivos desde «{1}»\\n<code>ESC</code> = Subir {2} archivos desde tu dispositivo\",\n\t\"fp_both_b\": \"<a href=\\\"#\\\" id=\\\"modal-ok\\\">Mover</a><a href=\\\"#\\\" id=\\\"modal-ng\\\">Subir</a>\",\n\t\"fcp_both_b\": \"<a href=\\\"#\\\" id=\\\"modal-ok\\\">Copiar</a><a href=\\\"#\\\" id=\\\"modal-ng\\\">Subir</a>\",\n\n\t\"mk_noname\": \"escribe un nombre en el campo de texto de la izquierda antes de hacer eso :p\",\n\t\"nmd_i1\": \"también puedes añadir la extensión que quieras, por ejemplo <code>.md</code>\", //m\n\t\"nmd_i2\": \"solo puedes crear archivos <code>.{0}</code> porque no tienes permiso para borrar\", //m\n\n\t\"tv_load\": \"Cargando documento de texto:\\n\\n{0}\\n\\n{1}% ({2} de {3} MiB cargados)\",\n\t\"tv_xe1\": \"no se pudo cargar el archivo de texto:\\n\\nerror \",\n\t\"tv_xe2\": \"404, archivo no encontrado\",\n\t\"tv_lst\": \"lista de archivos de texto en\",\n\t\"tvt_close\": \"volver a la vista de carpetas$NAtajo: M (o Esc)\\\">❌ cerrar\",\n\t\"tvt_dl\": \"descargar este archivo$NAtajo: Y\\\">💾 descargar\",\n\t\"tvt_prev\": \"mostrar documento anterior$NAtajo: i\\\">⬆ ant\",\n\t\"tvt_next\": \"mostrar siguiente documento$NAtajo: K\\\">⬇ sig\",\n\t\"tvt_sel\": \"seleccionar archivo &nbsp; ( para cortar / copiar / eliminar / ... )$NAtajo: S\\\">sel\",\n\t\"tvt_j\": \"embellecer json$NAtajo: shift-J\\\">j\", //m\n\t\"tvt_edit\": \"abrir archivo en editor de texto$NAtajo: E\\\">✏️ editar\",\n\t\"tvt_tail\": \"monitorizar cambios en el archivo; mostrar nuevas líneas en tiempo real\\\">📡 seguir\",\n\t\"tvt_wrap\": \"ajuste de línea\\\">↵\",\n\t\"tvt_atail\": \"bloquear el desplazamiento al final de la página\\\">⚓\",\n\t\"tvt_ctail\": \"decodificar colores de terminal (códigos de escape ansi)\\\">🌈\",\n\t\"tvt_ntail\": \"límite de historial (cuántos bytes de texto mantener cargados)\",\n\n\t\"m3u_add1\": \"canción añadida a la lista m3u\",\n\t\"m3u_addn\": \"{0} canciones añadidas a la lista m3u\",\n\t\"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\",\n\n\t\"gt_vau\": \"no mostrar vídeos, solo reproducir el audio\\\">🎧\",\n\t\"gt_msel\": \"activar selección de archivos; ctrl-clic en un archivo para anular$N$N&lt;em&gt;cuando está activo: doble clic en un archivo / carpeta para abrirlo&lt;/em&gt;$N$NAtajo: S\\\">multiselección\",\n\t\"gt_crop\": \"recortar miniaturas\\\">recortar\",\n\t\"gt_3x\": \"miniaturas de alta resolución\\\">3x\",\n\t\"gt_zoom\": \"zoom\",\n\t\"gt_chop\": \"recortar\",\n\t\"gt_sort\": \"ordenar por\",\n\t\"gt_name\": \"nombre\",\n\t\"gt_sz\": \"tamaño\",\n\t\"gt_ts\": \"fecha\",\n\t\"gt_ext\": \"tipo\",\n\t\"gt_c1\": \"truncar más los nombres de archivo (mostrar menos)\",\n\t\"gt_c2\": \"truncar menos los nombres de archivo (mostrar más)\",\n\n\t\"sm_w8\": \"buscando...\",\n\t\"sm_prev\": \"los resultados de búsqueda a continuación son de una consulta anterior:\\n  \",\n\t\"sl_close\": \"cerrar resultados de búsqueda\",\n\t\"sl_hits\": \"mostrando {0} resultados\",\n\t\"sl_moar\": \"cargar más\",\n\n\t\"s_sz\": \"tamaño\",\n\t\"s_dt\": \"fecha\",\n\t\"s_rd\": \"ruta\",\n\t\"s_fn\": \"nombre\",\n\t\"s_ta\": \"etiquetas\",\n\t\"s_ua\": \"subido@\",\n\t\"s_ad\": \"avanzado\",\n\t\"s_s1\": \"MiB mínimo\",\n\t\"s_s2\": \"MiB máximo\",\n\t\"s_d1\": \"mín. iso8601\",\n\t\"s_d2\": \"máx. iso8601\",\n\t\"s_u1\": \"subido después de\",\n\t\"s_u2\": \"y/o antes de\",\n\t\"s_r1\": \"la ruta contiene &nbsp; (separado por espacios)\",\n\t\"s_f1\": \"el nombre contiene &nbsp; (negar con -no)\",\n\t\"s_t1\": \"las etiquetas contienen &nbsp; (^=inicio, fin=$)\",\n\t\"s_a1\": \"propiedades de metadatos específicas\",\n\n\t\"md_eshow\": \"no se puede renderizar \",\n\t\"md_off\": \"[📜<em>léeme</em>] desactivado en [⚙️] -- documento oculto\",\n\n\t\"badreply\": \"Fallo al procesar la respuesta del servidor\",\n\n\t\"xhr403\": \"403: Acceso denegado\\n\\nintenta pulsar F5, quizás se cerró tu sesión\",\n\t\"xhr0\": \"desconocido (probablemente se perdió la conexión con el servidor, o el servidor está desconectado)\",\n\t\"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\",\n\t\"tl_xe1\": \"no se pudieron listar las subcarpetas:\\n\\nerror \",\n\t\"tl_xe2\": \"404: Carpeta no encontrada\",\n\t\"fl_xe1\": \"no se pudieron listar los archivos en la carpeta:\\n\\nerror \",\n\t\"fl_xe2\": \"404: Carpeta no encontrada\",\n\t\"fd_xe1\": \"no se pudo crear la subcarpeta:\\n\\nerror \",\n\t\"fd_xe2\": \"404: Carpeta de nivel superior no encontrada\",\n\t\"fsm_xe1\": \"no se pudo enviar el mensaje:\\n\\nerror \",\n\t\"fsm_xe2\": \"404: Carpeta de nivel superior no encontrada\",\n\t\"fu_xe1\": \"fallo al cargar la lista de deshacer del servidor:\\n\\nerror \",\n\t\"fu_xe2\": \"404: ¿Archivo no encontrado?\",\n\n\t\"fz_tar\": \"archivo gnu-tar sin comprimir (linux / mac)\",\n\t\"fz_pax\": \"tar formato pax sin comprimir (más lento)\",\n\t\"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\",\n\t\"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\",\n\t\"fz_zip8\": \"zip con nombres de archivo utf8 (puede dar problemas en windows 7 y anteriores)\",\n\t\"fz_zipd\": \"zip con nombres de archivo cp437 tradicionales, para software muy antiguo\",\n\t\"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)\",\n\n\t\"un_m1\": \"puedes eliminar tus subidas recientes (o abortar las inacabadas) a continuación\",\n\t\"un_upd\": \"actualizar\",\n\t\"un_m4\": \"o compartir los archivos visibles a continuación:\",\n\t\"un_ulist\": \"mostrar\",\n\t\"un_ucopy\": \"copiar\",\n\t\"un_flt\": \"filtro opcional:&nbsp; la URL debe contener\",\n\t\"un_fclr\": \"limpiar filtro\",\n\t\"un_derr\": \"fallo al deshacer-eliminar:\\n\",\n\t\"un_f5\": \"algo se rompió, por favor intenta actualizar o pulsa F5\",\n\t\"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\",\n\t\"un_nou\": \"<b>aviso:</b> servidor demasiado ocupado para mostrar subidas inacabadas; haz clic en el enlace \\\"actualizar\\\" en un momento\",\n\t\"un_noc\": \"<b>aviso:</b> la opción de deshacer subidas completadas no está activada/permitida en la configuración del servidor\",\n\t\"un_max\": \"mostrando los primeros 2000 archivos (usa el filtro)\",\n\t\"un_avail\": \"{0} subidas recientes se pueden eliminar<br />{1} inacabadas se pueden abortar\",\n\t\"un_m2\": \"ordenado por tiempo de subida; más recientes primero:\",\n\t\"un_no1\": \"¡pues no! ninguna subida es suficientemente reciente\",\n\t\"un_no2\": \"¡pues no! ninguna subida que coincida con ese filtro es suficientemente reciente\",\n\t\"un_next\": \"eliminar los siguientes {0} archivos a continuación\",\n\t\"un_abrt\": \"abortar\",\n\t\"un_del\": \"eliminar\",\n\t\"un_m3\": \"cargando tus subidas recientes...\",\n\t\"un_busy\": \"eliminando {0} archivos...\",\n\t\"un_clip\": \"{0} enlaces copiados al portapapeles\",\n\n\t\"u_https1\": \"deberías\",\n\t\"u_https2\": \"cambiar a https\",\n\t\"u_https3\": \"para un mejor rendimiento\",\n\t\"u_ancient\": \"tu navegador es impresionantemente antiguo -- quizás deberías <a href=\\\"#\\\" onclick=\\\"goto('bup')\\\">usar bup en su lugar</a>\",\n\t\"u_nowork\": \"se necesita firefox 53+ o chrome 57+ o iOS 11+\",\n\t\"tail_2old\": \"se necesita firefox 105+ o chrome 71+ o iOS 14.5+\",\n\t\"u_nodrop\": \"tu navegador es demasiado antiguo para subir arrastrando y soltando\",\n\t\"u_notdir\": \"¡eso no es una carpeta!\\n\\ntu navegador es demasiado antiguo,\\npor favor intenta arrastrar y soltar en su lugar\",\n\t\"u_uri\": \"para arrastrar y soltar imágenes desde otras ventanas del navegador,\\npor favor suéltalas sobre el gran botón de subida\",\n\t\"u_enpot\": \"cambiar a <a href=\\\"#\\\">UI ligera</a> (puede mejorar la velocidad de subida)\",\n\t\"u_depot\": \"cambiar a <a href=\\\"#\\\">UI elegante</a> (puede reducir la velocidad de subida)\",\n\t\"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!\",\n\t\"u_pott\": \"<p>archivos: &nbsp; <b>{0}</b> finalizados, &nbsp; <b>{1}</b> fallidos, &nbsp; <b>{2}</b> ocupados, &nbsp; <b>{3}</b> en cola</p>\",\n\t\"u_ever\": \"este es el uploader básico; up2k necesita al menos<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1\",\n\t\"u_su2k\": \"este es el uploader básico; <a href=\\\"#\\\" id=\\\"u2yea\\\">up2k</a> es mejor\",\n\t\"u_uput\": \"optimizar para velocidad (omitir checksum)\",\n\t\"u_ewrite\": \"no tienes acceso de escritura a esta carpeta\",\n\t\"u_eread\": \"no tienes acceso de lectura a esta carpeta\",\n\t\"u_enoi\": \"la búsqueda de archivos no está activada en la configuración del servidor\",\n\t\"u_enoow\": \"sobrescribir no funcionará aquí; se necesita permiso de eliminación\",\n\t\"u_badf\": \"Estos {0} archivos (de un total de {1}) se omitieron, posiblemente debido a permisos del sistema de archivos:\\n\\n\",\n\t\"u_blankf\": \"Estos {0} archivos (de un total de {1}) están en blanco / vacíos; ¿subirlos de todos modos?\\n\\n\",\n\t\"u_applef\": \"Estos {0} archivos (de un total de {1}) probablemente no son deseables;\\nPulsa <code>Aceptar/Intro</code> para OMITIR los siguientes archivos,\\nPulsa <code>Cancelar/ESC</code> para NO excluir, y SUBIR esos también:\\n\\n\",\n\t\"u_just1\": \"\\nQuizás funcione mejor si seleccionas solo un archivo\",\n\t\"u_ff_many\": \"si usas <b>Linux / MacOS / Android,</b> esta cantidad de archivos <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\"><em>podría</em> bloquear Firefox!</a>\\nsi eso ocurre, por favor inténtalo de nuevo (o usa Chrome).\",\n\t\"u_up_life\": \"Esta subida será eliminada del servidor\\n{0} después de que se complete\",\n\t\"u_asku\": \"subir estos {0} archivos a <code>{1}</code>\",\n\t\"u_unpt\": \"puedes deshacer / eliminar esta subida usando el 🧯 de arriba a la izquierda\",\n\t\"u_bigtab\": \"a punto de mostrar {0} archivos\\n\\nesto podría bloquear tu navegador, ¿estás seguro?\",\n\t\"u_scan\": \"Escaneando archivos...\",\n\t\"u_dirstuck\": \"el iterador de directorios se atascó intentando acceder a los siguientes {0} elementos; se omitirán:\",\n\t\"u_etadone\": \"Hecho ({0}, {1} archivos)\",\n\t\"u_etaprep\": \"(preparando para subir)\",\n\t\"u_hashdone\": \"hashing completado\",\n\t\"u_hashing\": \"hash\",\n\t\"u_hs\": \"negociando...\",\n\t\"u_started\": \"los archivos se están subiendo ahora; mira en [🚀]\",\n\t\"u_dupdefer\": \"duplicado; se procesará después de todos los demás archivos\",\n\t\"u_actx\": \"haz clic en este texto para evitar la pérdida de<br />rendimiento al cambiar a otras ventanas/pestañas\",\n\t\"u_fixed\": \"¡OK!&nbsp; Arreglado 👍\",\n\t\"u_cuerr\": \"fallo al subir el trozo {0} de {1};\\nprobablemente inofensivo, continuando\\n\\narchivo: {2}\",\n\t\"u_cuerr2\": \"el servidor rechazó la subida (trozo {0} de {1});\\nse reintentará más tarde\\n\\narchivo: {2}\\n\\nerror \",\n\t\"u_ehstmp\": \"se reintentará; mira abajo a la derecha\",\n\t\"u_ehsfin\": \"el servidor rechazó la solicitud para finalizar la subida; reintentando...\",\n\t\"u_ehssrch\": \"el servidor rechazó la solicitud para realizar la búsqueda; reintentando...\",\n\t\"u_ehsinit\": \"el servidor rechazó la solicitud para iniciar la subida; reintentando...\",\n\t\"u_eneths\": \"error de red al realizar la negociación de subida; reintentando...\",\n\t\"u_enethd\": \"error de red al comprobar la existencia del destino; reintentando...\",\n\t\"u_cbusy\": \"esperando a que el servidor vuelva a confiar en nosotros después de un fallo de red...\",\n\t\"u_ehsdf\": \"¡el servidor se quedó sin espacio en disco!\\n\\nse seguirá reintentando, por si alguien\\nlibera suficiente espacio para continuar\",\n\t\"u_emtleak1\": \"parece que tu navegador podría tener una fuga de memoria;\\npor favor\",\n\t\"u_emtleak2\": \" <a href=\\\"{0}\\\">cambia a https (recomendado)</a> o \",\n\t\"u_emtleak3\": \" \",\n\t\"u_emtleakc\": \"prueba lo siguiente:\\n<ul><li>pulsa <code>F5</code> para refrescar la página</li><li>luego desactiva el botón &nbsp;<code>mt</code>&nbsp; en los &nbsp;<code>⚙️ ajustes</code></li><li>e intenta esa subida de nuevo</li></ul>Las subidas serán un poco más lentas, pero bueno.\\n¡Perdón por las molestias!\\n\\nPD: chrome v107 <a href=\\\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\\\" target=\\\"_blank\\\">tiene una solución</a> para esto\",\n\t\"u_emtleakf\": \"prueba lo siguiente:\\n<ul><li>pulsa <code>F5</code> para refrescar la página</li><li>luego activa <code>🥔</code> (ligera) en la interfaz de subida</li><li>e intenta esa subida de nuevo</li></ul>\\nPD: firefox <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\">con suerte tendrá una solución</a> en algún momento\",\n\t\"u_s404\": \"no encontrado en el servidor\",\n\t\"u_expl\": \"explicar\",\n\t\"u_maxconn\": \"la mayoría de los navegadores limitan esto a 6, pero firefox te permite aumentarlo con <code>connections-per-server</code> en <code>about:config</code>\",\n\t\"u_tu\": '<p class=\"warn\">AVISO: turbo activado, <span>&nbsp;el cliente puede no detectar y reanudar subidas incompletas; ver tooltip del botón turbo</span></p>',\n\t\"u_ts\": '<p class=\"warn\">AVISO: turbo activado, <span>&nbsp;los resultados de búsqueda pueden ser incorrectos; ver tooltip del botón turbo</span></p>',\n\t\"u_turbo_c\": \"turbo está desactivado en la configuración del servidor\",\n\t\"u_turbo_g\": \"desactivando turbo porque no tienes\\nprivilegios para listar directorios en este volumen\",\n\t\"u_life_cfg\": 'autoeliminar después de <input id=\"lifem\" p=\"60\" /> min (o <input id=\"lifeh\" p=\"3600\" /> horas)',\n\t\"u_life_est\": 'la subida se eliminará <span id=\"lifew\" tt=\"hora local\">---</span>',\n\t\"u_life_max\": \"esta carpeta impone una\\nvida máxima de {0}\",\n\t\"u_unp_ok\": \"se permite deshacer la subida durante {0}\",\n\t\"u_unp_ng\": \"NO se permitirá deshacer la subida\",\n\t\"ue_ro\": \"tu acceso a esta carpeta es de solo lectura\\n\\n\",\n\t\"ue_nl\": \"actualmente no has iniciado sesión\",\n\t\"ue_la\": \"actualmente has iniciado sesión como \\\"{0}\\\"\",\n\t\"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\",\n\t\"ue_ta\": \"intenta subir de nuevo, ahora debería funcionar\",\n\t\"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\",\n\t\"ur_1uo\": \"OK: Archivo subido con éxito\",\n\t\"ur_auo\": \"OK: Todos los {0} archivos subidos con éxito\",\n\t\"ur_1so\": \"OK: Archivo encontrado en el servidor\",\n\t\"ur_aso\": \"OK: Todos los {0} archivos encontrados en el servidor\",\n\t\"ur_1un\": \"Subida fallida, lo siento\",\n\t\"ur_aun\": \"Todas las {0} subidas fallaron, lo siento\",\n\t\"ur_1sn\": \"El archivo NO se encontró en el servidor\",\n\t\"ur_asn\": \"Los {0} archivos NO se encontraron en el servidor\",\n\t\"ur_um\": \"Finalizado;\\n{0} subidas OK,\\n{1} subidas fallidas, lo siento\",\n\t\"ur_sm\": \"Finalizado;\\n{0} archivos encontrados en el servidor,\\n{1} archivos NO encontrados en el servidor\",\n\n\t\"rc_opn\": \"abrir\", //m\n\t\"rc_ply\": \"reproducir\", //m\n\t\"rc_pla\": \"reproducir como audio\", //m\n\t\"rc_txt\": \"abrir en el visor de archivos\", //m\n\t\"rc_md\": \"abrir en el editor de texto\", //m\n\t\"rc_dl\": \"descargar\", //m\n\t\"rc_zip\": \"descargar como archivo\", //m\n\t\"rc_cpl\": \"copiar enlace\", //m\n\t\"rc_del\": \"eliminar\", //m\n\t\"rc_cut\": \"cortar\", //m\n\t\"rc_cpy\": \"copiar\", //m\n\t\"rc_pst\": \"pegar\", //m\n\t\"rc_rnm\": \"renombrar\", //m\n\t\"rc_nfo\": \"nueva carpeta\", //m\n\t\"rc_nfi\": \"nuevo archivo\", //m\n\t\"rc_sal\": \"seleccionar todo\", //m\n\t\"rc_sin\": \"invertir selección\", //m\n\t\"rc_shf\": \"compartir esta carpeta\", //m\n\t\"rc_shs\": \"compartir selección\", //m\n\n\t\"lang_set\": \"¿refrescar para que el cambio surta efecto?\",\n\n\t\"splash\": {\n\t\t\"a1\": \"actualizar\",\n\t\t\"b1\": \"hola &nbsp; <small>(no has iniciado sesión)</small>\",\n\t\t\"c1\": \"cerrar sesión\",\n\t\t\"d1\": \"volcar estado de la pila\",\n\t\t\"d2\": \"muestra el estado de todos los hilos activos\",\n\t\t\"e1\": \"recargar configuración\",\n\t\t\"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\",\n\t\t\"f1\": \"puedes explorar:\",\n\t\t\"g1\": \"puedes subir a:\",\n\t\t\"cc1\": \"otras cosas:\",\n\t\t\"h1\": \"desactivar k304\",\n\t\t\"i1\": \"activar k304\",\n\t\t\"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), <em>pero</em> también ralentizará las cosas en general\",\n\t\t\"k1\": \"restablecer config. de cliente\",\n\t\t\"l1\": \"inicia sesión para más:\",\n\t\t\"ls3\": \"iniciar sesión\", //m\n\t\t\"lu4\": \"nombre de usuario\", //m\n\t\t\"lp4\": \"contraseña\", //m\n\t\t\"lo3\": \"cerrar sesión de “{0}” en todas partes\", //m\n\t\t\"lo2\": \"esto finalizará la sesión en todos los navegadores\", //m\n\t\t\"m1\": \"bienvenido de nuevo,\",\n\t\t\"n1\": \"404 no encontrado &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": '¿o quizás no tienes acceso? -- prueba con una contraseña o <a href=\\\"' + SR + '/?h\\\">vuelve al inicio</a>',\n\t\t\"p1\": \"403 prohibido &nbsp;~┻━┻\",\n\t\t\"q1\": 'usa una contraseña o <a href=\\\"' + SR + '/?h\\\">vuelve al inicio</a>',\n\t\t\"r1\": \"ir al inicio\",\n\t\t\".s1\": \"reescanear\",\n\t\t\"t1\": \"acción\",\n\t\t\"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\",\n\t\t\"v1\": \"conectar\",\n\t\t\"v2\": \"usar este servidor como un disco duro local\",\n\t\t\"w1\": \"cambiar a https\",\n\t\t\"x1\": \"cambiar contraseña\",\n\t\t\"y1\": \"editar recursos compartidos\",\n\t\t\"z1\": \"desbloquear este recurso compartido:\",\n\t\t\"ta1\": \"primero escribe tu nueva contraseña\",\n\t\t\"ta2\": \"repite para confirmar la nueva contraseña:\",\n\t\t\"ta3\": \"hay un error; por favor, inténtalo de nuevo\",\n\t\t\"nop\": \"ERROR: La contraseña no puede estar vacía\", //m\n\t\t\"nou\": \"ERROR: El nombre de usuario y/o la contraseña no pueden estar vacíos\", //m\n\t\t\"aa1\": \"archivos entrantes:\",\n\t\t\"ab1\": \"desactivar no304\",\n\t\t\"ac1\": \"activar no304\",\n\t\t\"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!\",\n\t\t\"ae1\": \"descargas activas:\",\n\t\t\"af1\": \"mostrar subidas recientes\",\n\t\t\"ag1\": \"mostrar usuarios IdP conocidos\",\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/swe.js",
    "content": "\n// Rader som slutar med //m är overifierade maskinöversättningar\n\nLs.swe = {\n\t\"tt\": \"Svenska\",\n\n\t\"cols\": {\n\t\t\"c\": \"aktion\",\n\t\t\"dur\": \"längd\",\n\t\t\"q\": \"kvalitet / bitrate\",\n\t\t\"Ac\": \"ljudkodek\",\n\t\t\"Vc\": \"videokodek\",\n\t\t\"Fmt\": \"format / container\",\n\t\t\"Ahash\": \"ljudchecksumma\",\n\t\t\"Vhash\": \"videochecksumma\",\n\t\t\"Res\": \"upplösning\",\n\t\t\"T\": \"filtyp\",\n\t\t\"aq\": \"ljudkvalitet / bitrate\",\n\t\t\"vq\": \"videokvalitet / bitrate\",\n\t\t\"pixfmt\": \"subsampling / pixelstruktur\",\n\t\t\"resw\": \"horisontell upplösning\",\n\t\t\"resh\": \"vertikal upplösning\",\n\t\t\"chs\": \"antal ljudkanaler\",\n\t\t\"hz\": \"samplingsfrekvens\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"övrigt\",\n\t\t\t[\"ESC\", \"stäng diverse paneler\"],\n\n\t\t\t\"filhanterare\",\n\t\t\t[\"G\", \"växla mellan listvy / rutnät\"],\n\t\t\t[\"T\", \"växla mellan miniatyrer / ikoner\"],\n\t\t\t[\"⇧ A/D\", \"miniatyrstorlek\"],\n\t\t\t[\"ctrl-K\", \"radera urval\"],\n\t\t\t[\"ctrl-X\", \"klipp urval till urklipp\"],\n\t\t\t[\"ctrl-C\", \"kopiera urval till urklipp\"],\n\t\t\t[\"ctrl-V\", \"klistra in (kopiera/flytta) hit\"],\n\t\t\t[\"Y\", \"ladda ner urval\"],\n\t\t\t[\"F2\", \"byt namn på urval\"],\n\n\t\t\t\"välja filer\",\n\t\t\t[\"Blanksteg\", \"växla val av fil\"],\n\t\t\t[\"↑/↓\", \"flytta filvalsmarkör\"],\n\t\t\t[\"ctrl ↑/↓\", \"flytta markör och vy\"],\n\t\t\t[\"⇧ ↑/↓\", \"välj föregående/nästa fil\"],\n\t\t\t[\"ctrl-A\", \"välj alla filer / mappar\"],\n\t\t], [\n\t\t\t\"navigation\",\n\t\t\t[\"B\", \"växla mellan brödsmulor / trädvy\"],\n\t\t\t[\"I/K\", \"föregående/nästa mapp\"],\n\t\t\t[\"M\", \"hoppa till överordnad mapp (eller kollapsa närvarande)\"],\n\t\t\t[\"V\", \"växla mellan mappar / textfiler i trädvy\"],\n\t\t\t[\"A/D\", \"trädvystorlek\"],\n\t\t], [\n\t\t\t\"ljudspelare\",\n\t\t\t[\"J/L\", \"föregående/nästa låt\"],\n\t\t\t[\"U/O\", \"hoppa 10sek bakåt/framåt\"],\n\t\t\t[\"0..9\", \"hoppa till 0%..90%\"],\n\t\t\t[\"P\", \"play/paus (startar även uppspelning)\"],\n\t\t\t[\"S\", \"välj låten som spelas\"],\n\t\t\t[\"Y\", \"ladda ner låt\"],\n\t\t], [\n\t\t\t\"bildvisare\",\n\t\t\t[\"J/L, ←/→\", \"föregående/nästa bild\"],\n\t\t\t[\"Hem/End\", \"första/sista bilden\"],\n\t\t\t[\"F\", \"helskärm\"],\n\t\t\t[\"R\", \"rotera medsols\"],\n\t\t\t[\"⇧ R\", \"rotera motsols\"],\n\t\t\t[\"S\", \"välj bild\"],\n\t\t\t[\"Y\", \"ladda ner bild\"],\n\t\t], [\n\t\t\t\"videospelare\",\n\t\t\t[\"U/O\", \"hoppa 10sek bakåt/framåt\"],\n\t\t\t[\"P/K/Blanksteg\", \"play/paus\"],\n\t\t\t[\"C\", \"fortsätt med nästa\"],\n\t\t\t[\"V\", \"loopa\"],\n\t\t\t[\"M\", \"stäng av ljud\"],\n\t\t\t[\"[ och ]\", \"ställ in loopintervall\"],\n\t\t], [\n\t\t\t\"textfilsvisare\",\n\t\t\t[\"I/K\", \"föregående/nästa fil\"],\n\t\t\t[\"M\", \"stäng textfil\"],\n\t\t\t[\"E\", \"redigera textfil\"],\n\t\t\t[\"S\", \"välj fil\"],\n\t\t\t[\"Y\", \"ladda ner textfil\"], //m\n\t\t\t[\"⇧ J\", \"försköna json\"], //m\n\t\t]\n\t],\n\n\t\"m_ok\": \"OK\",\n\t\"m_ng\": \"Avbryt\",\n\n\t\"enable\": \"Aktivera\",\n\t\"danger\": \"VARNING\",\n\t\"clipped\": \"kopierat till urklipp\",\n\n\t\"ht_s1\": \"sekund\",\n\t\"ht_s2\": \"sekunder\",\n\t\"ht_m1\": \"minut\",\n\t\"ht_m2\": \"minuter\",\n\t\"ht_h1\": \"timme\",\n\t\"ht_h2\": \"timmar\",\n\t\"ht_d1\": \"dag\",\n\t\"ht_d2\": \"dagar\",\n\t\"ht_and\": \" och \",\n\n\t\"goh\": \"kontrollpanel\",\n\t\"gop\": 'föregående mapp\">föreg.',\n\t\"gou\": 'överordnad mapp\">upp',\n\t\"gon\": 'nästa mapp\">nästa',\n\t\"logout\": \"Logga ut \",\n\t\"login\": \"Logga in\", //m\n\t\"access\": \"-rättighet\",\n\t\"ot_close\": \"stäng undermeny\",\n\t\"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`&quot;try unite&quot;` = 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`\",\n\t\"ot_unpost\": \"unpost: radera dina senaste uppladdningar, eller avbryt pågående sådana\",\n\t\"ot_bup\": \"bup: enkel uppladdare, stödjer t o m netscape 4.0\",\n\t\"ot_mkdir\": \"mkdir: skapa en ny mapp\",\n\t\"ot_md\": \"new-file: skapa en ny textfil\",\n\t\"ot_msg\": \"msg: skicka ett meddelande till serverloggen\",\n\t\"ot_mp\": \"mediaspelarinställningar\",\n\t\"ot_cfg\": \"konfigurationsinställningar\",\n\t\"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 [🎈]&nbsp; (den enkla uppladdaren)<br /><br />under uppladdningens gång blir denna ikon en förloppsindikator!',\n\t\"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 [🎈]&nbsp; (den enkla uppladdaren)<br /><br />under uppladdningens gång blir denna ikon en förloppsindikator!',\n\t\"ot_noie\": 'Var vänlig använd Chrome / Firefox / Edge',\n\n\t\"ab_mkdir\": \"skapa mapp\",\n\t\"ab_mkdoc\": \"ny textfil\",\n\t\"ab_msg\": \"skicka medd. till serverlogg\",\n\n\t\"ay_path\": \"hoppa till mappar\",\n\t\"ay_files\": \"hoppa till filer\",\n\n\t\"wt_ren\": \"byt namn på urval$NSnabbtangent: F2\",\n\t\"wt_del\": \"radera urval$NSnabbtangent: ctrl-K\",\n\t\"wt_cut\": \"klipp urval&lt;small&gt;(för att klistra in någonstans)&lt;/small&gt;$NSnabbtangent: ctrl-X\",\n\t\"wt_cpy\": \"kopiera urval till urklipp$N(för att klistra in någonstans)$NSnabbtangent: ctrl-C\",\n\t\"wt_pst\": \"klistra in tidigare urval$NSnabbtangent: ctrl-V\",\n\t\"wt_selall\": \"välj alla filer$NSnabbtangent: ctrl-A (när en fil har fokus)\",\n\t\"wt_selinv\": \"invertera urval\",\n\t\"wt_zip1\": \"ladda ner denna mapp som ett arkiv\",\n\t\"wt_selzip\": \"ladda ner urval som ett arkiv\",\n\t\"wt_seldl\": \"ladda ner urval som separata filer$NSnabbtangent: Y\",\n\t\"wt_npirc\": \"kopiera IRC-formatterad låtinfo\",\n\t\"wt_nptxt\": \"kopiera låtinfo i klartext\",\n\t\"wt_m3ua\": \"lägg till i m3u-spellista (klicka på <code>📻copy</code> senare)\",\n\t\"wt_m3uc\": \"kopiera m3u-spellista till urklipp\",\n\t\"wt_grid\": \"växla mellan rutnät och listvy$NSnabbtangent: G\",\n\t\"wt_prev\": \"föregående låt$NSnabbtangent: J\",\n\t\"wt_play\": \"play / paus$NSnabbtangent: P\",\n\t\"wt_next\": \"nästa låt$NSnabbtangent: L\",\n\n\t\"ul_par\": \"samtidiga uppladdningar:\",\n\t\"ut_rand\": \"slumpa filnamn\",\n\t\"ut_u2ts\": \"bevara tidsstämpeln för senaste ändring$Nfrån ditt filsystem till servern\\\">📅\",\n\t\"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\n\t\"ut_mt\": \"fortsätt hasha filer under uppladdningens gång$N$Nstäng av om din CPU eller disk är en flaskhals\",\n\t\"ut_ask\": 'bekräfta innan uppladdningar påbörjas\">💭',\n\t\"ut_pot\": \"förbättra uppladdningshastigheten på långsamma enheter$Ngenom att förenkla användargränssnittet\",\n\t\"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)\",\n\t\"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\",\n\t\"ul_btn\": \"släpp filer / mappar<br>här (eller klicka)\",\n\t\"ul_btnu\": \"L A D D A  U P P\",\n\t\"ul_btns\": \"S Ö K\",\n\n\t\"ul_hash\": \"hashar\",\n\t\"ul_send\": \"skickar\",\n\t\"ul_done\": \"klar\",\n\t\"ul_idle1\": \"inga uppladdningar har köats\",\n\t\"ut_etah\": \"medelhastighet för &lt;em&gt;hashning&lt;/em&gt;, och uppskattad återstående tid\",\n\t\"ut_etau\": \"medelhastighet för &lt;em&gt;överföring&lt;/em&gt;, och uppskattad återstående tid\",\n\t\"ut_etat\": \"&lt;em&gt;total&lt;/em&gt; medelhastighet, och uppskattad återstående tid\",\n\n\t\"uct_ok\": \"lyckade\",\n\t\"uct_ng\": \"no-good: misslyckade / avvisade / ej funna\",\n\t\"uct_done\": \"ok och ng kombinerat\",\n\t\"uct_bz\": \"pågående\",\n\t\"uct_q\": \"köade\",\n\n\t\"utl_name\": \"filnamn\",\n\t\"utl_ulist\": \"visa\",\n\t\"utl_ucopy\": \"kopiera\",\n\t\"utl_links\": \"länkar\",\n\t\"utl_stat\": \"status\",\n\t\"utl_prog\": \"förlopp\",\n\n\t// keep short:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"FEL\",\n\t\"utl_oserr\": \"OS-fel\",\n\t\"utl_found\": \"hittad\",\n\t\"utl_defer\": \"väntar\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"klar\",\n\n\t\"ul_flagblk\": \"filerna lades till i kön,</b><br>men det finns en upptagen up2k i en annan webbläsarflik,<br>så vi väntar på den först\",\n\t\"ul_btnlk\": \"serverkonfigurationen har låst denna inställning\",\n\n\t\"udt_up\": \"Ladda upp\",\n\t\"udt_srch\": \"Sök\",\n\t\"udt_drop\": \"släpp här\",\n\n\t\"u_nav_m\": '<h6>jaha, vad har du då?</h6><code>Enter</code> = Filer (en eller flera)\\n<code>ESC</code> = En mapp (inklusive undermappar)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Filer</a><a href=\"#\" id=\"modal-ng\">En mapp</a>',\n\n\t\"cl_opts\": \"växlar\",\n\t\"cl_hfsz\": \"filstorlek\", //m\n\t\"cl_themes\": \"tema\",\n\t\"cl_langs\": \"språk\",\n\t\"cl_ziptype\": \"mappnedladdning\",\n\t\"cl_uopts\": \"up2k-inställningar\",\n\t\"cl_favico\": \"favikon\",\n\t\"cl_bigdir\": \"stora mappar\",\n\t\"cl_hsort\": \"#sort.\",\n\t\"cl_keytype\": \"tonartsnotering\",\n\t\"cl_hiddenc\": \"dolda kolumner\",\n\t\"cl_hidec\": \"dölj\",\n\t\"cl_reset\": \"återställ\",\n\t\"cl_hpick\": \"tryck på en kolumntitel för att dölja den i filvyn\",\n\t\"cl_hcancel\": \"kolumndöljning avbruten\",\n\t\"cl_rcm\": \"högerklicksmeny\", //m\n\n\t\"ct_grid\": '田 rutnätet',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ tips',\n\t\"ct_thumb\": 'växla mellan miniatyrer och ikoner i rutnätsvyn$NSnabbtangent: T\">🖼️ miniatyrer',\n\t\"ct_csel\": 'använd CTRL och SKIFT för urval av filer i rutnätsvyn\">val',\n\t\"ct_dsel\": 'använd dra-urval i rutnätsvyn\">dra', //m\n\t\"ct_dl\": 'tvinga nedladdning (visa inte inline) när en fil klickas\">dl', //m\n\t\"ct_ihop\": 'skrolla till den senast visade filen när bildvisaren stängs\">g⮯',\n\t\"ct_dots\": 'visa dolda filer (om servern tillåter detta)\">dolda',\n\t\"ct_qdel\": 'bekräfta endast en gång när filer raderas\">srad',\n\t\"ct_dir1st\": 'sortera mappar före filer\">📁 först',\n\t\"ct_nsort\": 'naturlig sortering (för filnamn med ledande siffror)\">nsort',\n\t\"ct_utc\": 'visa alla datum och tider i UTC\">UTC',\n\t\"ct_readme\": 'visa README.md i listvyn\">📜 läsmig',\n\t\"ct_idxh\": 'visa index.html istället för listvyn\">htm',\n\t\"ct_sbars\": 'visa rullningslister\">⟊',\n\n\t\"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📅\",\n\n\t\"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 <em>&quot;har denna fil samma filstorlek på servern?&quot;</em>, 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 &quot;ladda upp&quot; samma filer igen för att låta klienten verifiera dem\\\">turbo\",\n\n\t\"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 <em>bör</em> fånga de flesta ofärdiga / korrumperade uppladdningarna, men kan inte ersätta ett fullständigt verifieringspass med turbo avstängt\\\">date-chk\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cut_nag\": \"skicka en systemnotifikation när uppladdningar blir klara$N(endast om webbläsaren eller fliken inte är fokuserade)\",\n\t\"cut_sfx\": \"ljudnotifikation när uppladdningar blir klara$N(endast om webbläsaren eller fliken inte är fokuserade)\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cft_text\": \"favikon-text (låt stå tom och uppdatera sidan för att stänga av)\",\n\t\"cft_fg\": \"förgrundsfärg\",\n\t\"cft_bg\": \"bakgrundsfärg\",\n\n\t\"cdt_lim\": \"högsta antal filer att visa in en mapp\",\n\t\"cdt_ask\": \"när du når botten av vyn,$Nbe om en åtgärd istället för att ladda fler filer\",\n\t\"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\",\n\t\"cdt_ren\": \"aktivera anpassad högerklicksmeny, den vanliga menyn är tillgänglig med shift + högerklick\\\">aktivera\", //m\n\t\"cdt_rdb\": \"visa den vanliga högerklicksmenyn när den anpassade redan är öppen och man högerklickar igen\\\">x2\", //m\n\n\t\"tt_entree\": \"visa trädvy$NSnabbtangent: B\",\n\t\"tt_detree\": \"visa brödsmulor$NSnabbtangent: B\",\n\t\"tt_visdir\": \"skrolla till öppnad mapp\",\n\t\"tt_ftree\": \"växla mellan trädvy och textfiler$NHotkey: V\",\n\t\"tt_pdock\": \"visa överordnade mappar i en panel längst upp i vyn\",\n\t\"tt_dynt\": \"väx vyn när trädet expanderar\",\n\t\"tt_wrap\": \"automatisk radbrytning\",\n\t\"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 )\",\n\n\t\"ml_pmode\": \"vid mappens slut...\",\n\t\"ml_btns\": \"komm.\",\n\t\"ml_tcode\": \"konvertera\",\n\t\"ml_tcode2\": \"konvertera till\",\n\t\"ml_tint\": \"hy\",\n\t\"ml_eq\": \"ljudutjämnare\",\n\t\"ml_drc\": \"dynamikkompressor\",\n\t\"ml_ss\": \"hoppa över tystnad\", //m\n\n\t\"mt_loop\": \"upprepa en låt\\\">🔁\",\n\t\"mt_one\": \"stoppa uppspelningen efter en låt\\\">1️⃣\",\n\t\"mt_shuf\": \"blanda låtarna i varje mapp\\\">🔀\",\n\t\"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▶\",\n\t\"mt_preload\": \"påbörja nedladdning av nästa låt i förväg för gapfri uppspelning\\\">ladda\",\n\t\"mt_prescan\": \"hoppa till nästa mapp i förväg så att webbläsaren$Nförblir glad och inte avbryter uppspelningen\\\">nav\",\n\t\"mt_fullpre\": \"försök att ladda ner hela låten i förväg;$N✅ aktivera på <b>opålitliga</b> uppkopplingar,$N❌ <b>avaktivera</b> kanske på långsamma uppkopplingar\\\">full\",\n\t\"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)\\\">☕️\",\n\t\"mt_waves\": \"vågformsreglage:$Nvisa ljudstyrkan i uppspelningsreglaget\\\">~s\",\n\t\"mt_npclip\": \"visa knappar för att kopiera låtinfo till urklippet\\\">/np\",\n\t\"mt_m3u_c\": \"visa knappar för att kopiera de valda$Nlåtarna som en m3u8-spellista\\\">📻\",\n\t\"mt_octl\": \"systemintegration (mediaknappar / skärmdisplay)\\\">os-ctl\",\n\t\"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\",\n\t\"mt_oscv\": \"visa skivomslag i skärmdisplayen\\\">omslag\",\n\t\"mt_follow\": \"skrolla vyn till den spelande låten\\\">🎯\",\n\t\"mt_compact\": \"kompakt kontrollpanel\\\">⟎\",\n\t\"mt_uncache\": \"rensa cachen &nbsp;(prova detta om din webbläsare har cachat$Nen trasig kopia av en låt och den vägrar spela upp den)\\\">rensa\",\n\t\"mt_mloop\": \"upprepa den öppna mappen\\\">🔁 upprepa\",\n\t\"mt_mnext\": \"ladda nästa mapp och fortsätt\\\">📂 nästa\",\n\t\"mt_mstop\": \"stoppa uppspelningen\\\">⏸ stopp\",\n\t\"mt_cflac\": \"konvertera flac / wav till {0}\\\">flac\",\n\t\"mt_caac\": \"konvertera aac / m4a till {0}\\\">aac\",\n\t\"mt_coth\": \"konvertera allt annat (förutom mp3) till {0}\\\">annat\",\n\t\"mt_c2opus\": \"bäst val för pc, laptop, android\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, för iOS 17.5 och senare\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, för iOS 11 till 17\\\">caf\",\n\t\"mt_c2mp3\": \"använd detta på mycket gamla enheter\\\">mp3\",\n\t\"mt_c2flac\": \"bäst ljudkvalitet, men enorma nedladdningar\\\">flac\",\n\t\"mt_c2wav\": \"okomprimerad uppspelning (ännu större)\\\">wav\",\n\t\"mt_c2ok\": \"snyggt, bra val\",\n\t\"mt_c2nd\": \"det är inte det rekommenderade formatet för din enhet, men det är lungt\",\n\t\"mt_c2ng\": \"din enhet verkar inte stödja det här formatet, men vi provar ändå\",\n\t\"mt_xowa\": \"det finns buggar i iOS som hindrar uppspelning i bakgrunden med detta format; vänligen använd caf eller mp3 istället\",\n\t\"mt_tint\": \"nivå på bakgrundsfärg (0-100) på uppspelningsreglaget;$Ngör buffring mindre distraherande\",\n\t\"mt_eq\": \"`aktiverar utjämning och förstärkning;$N$Nboost `0` = standard 100%-volym (omodifierad)$N$Nwidth `1 &nbsp;` = standard stereo (omodifierad)$Nwidth `0.5` = 50% vänster-höger crossfeed$Nwidth `0 &nbsp;` = mono$N$Nboost `-0.8` &amp; 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\",\n\t\"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)\",\n\t\"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\n\t\"mt_ssvt\": \"volymtröskel (0-255)\\\">vol\", //m\n\t\"mt_ssts\": \"aktiv tröskel (% spår, start)\\\">sta\", //m\n\t\"mt_sste\": \"aktiv tröskel (% spår, slut)\\\">slt\", //m\n\t\"mt_sssm\": \"uppspelningshastighetsmultiplikator\\\">sn\", //m\n\n\t\"mb_play\": \"play\",\n\t\"mm_hashplay\": \"spela upp den här ljudfilen?\",\n\t\"mm_m3u\": \"tryck <code>Enter/OK</code> för att spela\\ntryck <code>ESC/Avbryt</code> to Edit\",\n\t\"mp_breq\": \"firefox 82+ eller chrome 73+ eller iOS 15+ krävs\",\n\t\"mm_bload\": \"laddar...\",\n\t\"mm_bconv\": \"konverterar till {0}, vänligen vänta...\",\n\t\"mm_opusen\": \"din webbläsare kan inte spela upp aac- eller m4a-filer;\\nkonvertering till opus är nu påslaget\",\n\t\"mm_playerr\": \"uppspelning misslyckades: \",\n\t\"mm_eabrt\": \"Uppspelningen avbröts\",\n\t\"mm_enet\": \"Din uppkoppling är skum\",\n\t\"mm_edec\": \"Filen är korrumperad??\",\n\t\"mm_esupp\": \"Din webbläsare förstår inte detta format\",\n\t\"mm_eunk\": \"Okänt Fel\",\n\t\"mm_e404\": \"Kunde inte spela upp ljudfil; fel 404: Filen hittades inte.\",\n\t\"mm_e403\": \"Kunde inte spela upp ljudfil; fel 403: Åtkomst nekad.\\n\\nProva att ladda om sidan med F5, du kanske blev utloggad\",\n\t\"mm_e415\": \"Kunde inte spela upp ljudfil; fel 415: Filkonvertering misslyckades; kolla serverloggen.\", //m\n\t\"mm_e500\": \"Kunde inte spela upp ljudfil; fel 500: Kolla serverloggen.\",\n\t\"mm_e5xx\": \"Kunde inte spela upp ljudfil; serverfel \",\n\t\"mm_nof\": \"hittade inga fler låtar i närheten\",\n\t\"mm_prescan\": \"Letar efter fler låtar...\",\n\t\"mm_scank\": \"Hittade nästa låt:\",\n\t\"mm_uncache\": \"cachen rensad; alla låtar kommer att laddas ner igen vid uppspelning\",\n\t\"mm_hnf\": \"den låten finns inte längre\",\n\n\t\"im_hnf\": \"den bilden finns inte längre\",\n\n\t\"f_empty\": 'mappen är tom',\n\t\"f_chide\": 'detta kommer att dölja kolumnen «{0}»\\n\\ndu kan visa kolumner igen i inställningarna',\n\t\"f_bigtxt\": \"den här filen är {0} MiB stor -- vill du verkligen visa den som text?\",\n\t\"f_bigtxt2\": \"visa endast slutet på filen? detta aktiverar även övervakning, vilket visar nya rader i filen i realtid\",\n\t\"fbd_more\": '<div id=\"blazy\"><code>{0}</code> av <code>{1}</code> filer visas; <a href=\"#\" id=\"bd_more\">visa {2}</a> eller <a href=\"#\" id=\"bd_all\">visa alla</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\"><code>{0}</code> av <code>{1}</code> filer visas; <a href=\"#\" id=\"bd_all\">visa alla</a></div>',\n\t\"f_anota\": \"endast {0} av {1} objekt valdes;\\nför att välja hela mappen, skrolla först till botten av vyn\",\n\n\t\"f_dls\": 'fillänkarna i den öppna mappen har\\nbytts till nedladdningslänkar',\n\t\"f_dl_nd\": 'hoppar över mapp (använd zip/tar-nedladdning istället):\\n', //m\n\n\t\"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 <code>.PARTIAL</code>-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 <code>.PARTIAL</code>-filen istället att laddas ner, vilket är nästan garanterat att ge dig korrumperad data.\",\n\n\t\"ft_paste\": \"klistra in {0} objekt$NSnabbtangent: ctrl-V\",\n\t\"fr_eperm\": 'kan ej byta namn:\\ndu har inte flytträttighet i denna mapp',\n\t\"fd_eperm\": 'kan ej radera:\\ndu har inte raderingsrättighet i denna mapp',\n\t\"fc_eperm\": 'kan ej klippa:\\ndu har inte flytträttighet i denna mapp',\n\t\"fp_eperm\": 'kan ej klistra in:\\ndu har inte skrivrättighet i denna mapp',\n\t\"fr_emore\": \"välj minst en fil att byta namn på\",\n\t\"fd_emore\": \"välj minst en fil att radera\",\n\t\"fc_emore\": \"välj minst en fil att klippa\",\n\t\"fcp_emore\": \"välj minst en fil att kopiera till urklippet\",\n\n\t\"fs_sc\": \"dela den öppna mappen\",\n\t\"fs_ss\": \"dela de urvalda filerna\",\n\t\"fs_just1d\": \"du kan inte välja mer än en mapp\\neller blanda filer och mappar i samma urval\",\n\t\"fs_abrt\": \"❌ avbryt\",\n\t\"fs_rand\": \"🎲 slump.namn\",\n\t\"fs_go\": \"✅ skapa utdelning\",\n\t\"fs_name\": \"namn\",\n\t\"fs_src\": \"källa\",\n\t\"fs_pwd\": \"lösen\",\n\t\"fs_exp\": \"utgång\",\n\t\"fs_tmin\": \"min\",\n\t\"fs_thrs\": \"timmar\",\n\t\"fs_tdays\": \"dagar\",\n\t\"fs_never\": \"oändlig\",\n\t\"fs_pname\": \"valfritt länknamn; slumpas fram om detta står tomt\",\n\t\"fs_tsrc\": \"filen eller mappen att dela\",\n\t\"fs_ppwd\": \"valfritt lösenord\",\n\t\"fs_w8\": \"skapar utdelning...\",\n\t\"fs_ok\": \"tryck <code>Enter/OK</code> för att kopiera länken till urklipp\\ntryck <code>ESC/Avbryt</code> för att stänga\",\n\n\t\"frt_dec\": \"kan laga vissa typer av trasiga filnamn\\\">avkoda-url\",\n\t\"frt_rst\": \"återställ modifierade filnamn till de ursprungliga\\\">↺ återställ\",\n\t\"frt_abrt\": \"avbryt och stäng denna panel\\\">❌ avbryt\",\n\t\"frb_apply\": \"BYT NAMN\",\n\t\"fr_adv\": \"batch-, metadata- och mönsteromskrivning\\\">avancerat\",\n\t\"fr_case\": \"skiftlägeskänsligt reguljärt uttryck\\\">skift\",\n\t\"fr_win\": \"windows-säkra namn; ersätt <code>&lt;&gt;:&quot;\\\\|?*</code> med japanska fullbreddtecken\\\">win\",\n\t\"fr_slash\": \"ersätt <code>/</code> med ett tecken som inte skapar nya mappar\\\">ingen /\",\n\t\"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.\",\n\t\"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\",\n\t\"fr_pdel\": \"ta bort\",\n\t\"fr_pnew\": \"spara som\",\n\t\"fr_pname\": \"ge ett nytt namn på din inställning\",\n\t\"fr_aborted\": \"avbrutet\",\n\t\"fr_lold\": \"gammalt namn\",\n\t\"fr_lnew\": \"nytt namn\",\n\t\"fr_tags\": \"taggar för de valda filerna (skrivskyddat, endast som referens):\",\n\t\"fr_busy\": \"byter namn på {0} objekt...\\n\\n{1}\",\n\t\"fr_efail\": \"namnbyte misslyckades:\\n\",\n\t\"fr_nchg\": \"{0} av de nya namnen ändrades p g a <code>win</code> och/eller <code>ingen /</code>\\n\\nÄr det okej att fortsätta med de nya namnen?\",\n\n\t\"fd_ok\": \"radering lyckades\",\n\t\"fd_err\": \"radering misslyckades:\\n\",\n\t\"fd_none\": \"inget raderades; kanske blockerat av serverkonfigurationen (xbd)?\",\n\t\"fd_busy\": \"raderar {0} objekt...\\n\\n{1}\",\n\t\"fd_warn1\": \"RADERA dessa {0} objekt?\",\n\t\"fd_warn2\": \"<b>Sista chansen!</b> Det finns inget sätt att ångra detta. Radera?\",\n\n\t\"fc_ok\": \"klippte {0} objekt\",\n\t\"fc_warn\": 'klippte {0} objekt, men:\\n\\nendast <b>denna</b> webbläsarflik kan klistra in dem\\n(eftersom urvalet är så enormt stort)',\n\n\t\"fcc_ok\": \"kopierade {0} objekt till urklippet\",\n\t\"fcc_warn\": 'kopierade {0} objekt till urklippet, men:\\n\\nendast <b>denna</b> webbläsarflik kan klistra in dem\\n(eftersom urvalet är så enormt stort)',\n\n\t\"fp_apply\": \"använd dessa namn\",\n\t\"fp_skip\": \"skippa upptagna\", //m\n\t\"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\",\n\t\"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\n\t\"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\n\t\"fp_emore\": \"det finns fortfarande filnamnskrockar att fixa\",\n\t\"fp_ok\": \"flytt lyckades\",\n\t\"fcp_ok\": \"kopiering lyckades\",\n\t\"fp_busy\": \"flyttar {0} objekt...\\n\\n{1}\",\n\t\"fcp_busy\": \"kopierar {0} objekt...\\n\\n{1}\",\n\t\"fp_abrt\": \"avbryter...\",\n\t\"fp_err\": \"flytt misslyckades:\\n\",\n\t\"fcp_err\": \"kopiering misslyckades:\\n\",\n\t\"fp_confirm\": \"flytta dessa {0} objekt hit?\",\n\t\"fcp_confirm\": \"kopiera dessa {0} objekt hit?\",\n\t\"fp_etab\": 'lyckades ej läsa urklippet från en annan webbläsarflik',\n\t\"fp_name\": \"laddar upp en fil från din enhet. Ge den ett namn:\",\n\t\"fp_both_m\": '<h6>välj vad som ska klistras in</h6><code>Enter</code> = Flytta {0} objekt från «{1}»\\n<code>ESC</code> = Ladda upp {2} filer från din enhet',\n\t\"fcp_both_m\": '<h6>välj vad som ska klistras in</h6><code>Enter</code> = Kopiera {0} objekt från «{1}»\\n<code>ESC</code> = Ladda upp {2} filer från din enhet',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Flytta</a><a href=\"#\" id=\"modal-ng\">Ladda upp</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Kopiera</a><a href=\"#\" id=\"modal-ng\">Ladda upp</a>',\n\n\t\"mk_noname\": \"skriv ett namn i fältet till vänster först :p\",\n\t\"nmd_i1\": \"lägg också till filändelsen du vill ha, till exempel <code>.md</code>\", //m\n\t\"nmd_i2\": \"du kan bara skapa <code>.{0}</code>-filer eftersom du inte har borttagningsbehörighet\", //m\n\n\t\"tv_load\": \"Laddar textfil:\\n\\n{0}\\n\\n{1}% ({2} av {3} MiB laddat)\",\n\t\"tv_xe1\": \"kunde ej ladda textfil:\\n\\nfel \",\n\t\"tv_xe2\": \"404, filen hittades inte\",\n\t\"tv_lst\": \"lista av textfiler i\",\n\t\"tvt_close\": \"återvänd till mapp$NSnabbtangent: M (eller Esc)\\\">❌ stäng\",\n\t\"tvt_dl\": \"ladda ner denna fil$NSnabbtangent: Y\\\">💾 ladda ner\",\n\t\"tvt_prev\": \"visa föregående fil$NSnabbtangent: i\\\">⬆ föreg.\",\n\t\"tvt_next\": \"visa nästa fil$NSnabbtangent: K\\\">⬇ nästa\",\n\t\"tvt_sel\": \"välj fil &nbsp; ( för klipp / kopiera / radera / ... )$NSnabbtangent: S\\\">välj\",\n\t\"tvt_j\": \"försköna json$NSnabbtangent: shift-J\\\">j\", //m\n\t\"tvt_edit\": \"öppna fil i textredigerare$NSnabbtangent: E\\\">✏️ redigera\",\n\t\"tvt_tail\": \"övervaka filen; visa nya rader i realtid\\\">📡 övervaka\",\n\t\"tvt_wrap\": \"automatisk radbrytning\\\">↵\",\n\t\"tvt_atail\": \"lås vyn till sidans botten\\\">⚓\",\n\t\"tvt_ctail\": \"avkoda terminalfärger (ansi-escapesekvenser)\\\">🌈\",\n\t\"tvt_ntail\": \"gräns för scrollback (hur många byte ska behållas laddade)\",\n\n\t\"m3u_add1\": \"låt tillagd till m3u-spellista\",\n\t\"m3u_addn\": \"{0} låtar tillagda till m3u-spellista\",\n\t\"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\",\n\n\t\"gt_vau\": \"visa inte videor, spela endast ljudet\\\">🎧\",\n\t\"gt_msel\": \"urval av filer; ctrl-klicka en fil för standardbeteende$N$N&lt;em&gt;när detta är aktiverat: dubbelklicka en fil / mapp för att öppna den&lt;/em&gt;$N$NSnabbtangent: S\\\">urval\",\n\t\"gt_crop\": \"centrera och beskär miniatyrbilder\\\">beskär\",\n\t\"gt_3x\": \"högupplösta miniatyrbilder\\\">3x\",\n\t\"gt_zoom\": \"zoom\",\n\t\"gt_chop\": \"klipp\",\n\t\"gt_sort\": \"sortera efter\",\n\t\"gt_name\": \"namn\",\n\t\"gt_sz\": \"storlek\",\n\t\"gt_ts\": \"datum\",\n\t\"gt_ext\": \"typ\",\n\t\"gt_c1\": \"förkorta filnamn (visa mindre)\",\n\t\"gt_c2\": \"förläng filnamn (visa mer)\",\n\n\t\"sm_w8\": \"söker...\",\n\t\"sm_prev\": \"sökresultaten nedan är från en tidigare sökning:\\n  \",\n\t\"sl_close\": \"stäng sökresultaten\",\n\t\"sl_hits\": \"visar {0} träffar\",\n\t\"sl_moar\": \"ladda fler\",\n\n\t\"s_sz\": \"storlek\",\n\t\"s_dt\": \"datum\",\n\t\"s_rd\": \"sökväg\",\n\t\"s_fn\": \"namn\",\n\t\"s_ta\": \"taggar\",\n\t\"s_ua\": \"uppl.\",\n\t\"s_ad\": \"avanc.\",\n\t\"s_s1\": \"minimum MiB\",\n\t\"s_s2\": \"maximum MiB\",\n\t\"s_d1\": \"min. iso8601\",\n\t\"s_d2\": \"max. iso8601\",\n\t\"s_u1\": \"uppladdad efter\",\n\t\"s_u2\": \"och/eller före\",\n\t\"s_r1\": \"sökvägen innehåller &nbsp; (blankstegsseparerat)\",\n\t\"s_f1\": \"filnamnet innehåller &nbsp; (invertera med -intedetta)\",\n\t\"s_t1\": \"taggar innehåller &nbsp; (^=start, slut=$)\",\n\t\"s_a1\": \"specifika metadataegenskaper\",\n\n\t\"md_eshow\": \"kan ej visa \",\n\t\"md_off\": \"[📜<em>läsmig</em>] avstängt i [⚙️] -- dokumentet är dolt\",\n\n\t\"badreply\": \"Kunde ej tolka svaret från servern\",\n\n\t\"xhr403\": \"403: Åtkomst nekad\\n\\nProva att ladda om sidan med F5, du kanske blev utloggad\",\n\t\"xhr0\": \"okänt (tappade förmodligen kontakt med servern, eller så är den nere)\",\n\t\"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\",\n\t\"tl_xe1\": \"kunde inte visa undermappar:\\n\\nfel \",\n\t\"tl_xe2\": \"404: Mappen hittades inte\",\n\t\"fl_xe1\": \"kunde inte visa filer i mapp:\\n\\nfel \",\n\t\"fl_xe2\": \"404: Mappen hittades inte\",\n\t\"fd_xe1\": \"kunde inte skapa mapp:\\n\\nfel \",\n\t\"fd_xe2\": \"404: Överordnad mapp hittades inte\",\n\t\"fsm_xe1\": \"kunde inte skicka meddelande:\\n\\ndel \",\n\t\"fsm_xe2\": \"404: Överordnad mapp hittades inte\",\n\t\"fu_xe1\": \"kunde inte ladda unpost-listan från servern:\\n\\nfel \",\n\t\"fu_xe2\": \"404: Filen hittades inte??\",\n\n\t\"fz_tar\": \"okomprimerad tar-fil i gnu-format (linux / mac)\",\n\t\"fz_pax\": \"okomprimerad tar-fil i pax-format (långsammare)\",\n\t\"fz_targz\": \"gnu-tar komprimerad med gzip-nivå 3$N$Ndetta är vanligtvis mycket långsamt,$Nanvänd okomprimerad tar istället\",\n\t\"fz_tarxz\": \"gnu-tar komprimerad med xz-nivå 1$N$Ndetta är vanligtvis mycket långsamt,$Nanvänd okomprimerad tar istället\",\n\t\"fz_zip8\": \"zip-fil med utf8-filnman (kan vara skum i windows 7 och äldre)\",\n\t\"fz_zipd\": \"zip-fil med standard cp437-filnamn, för riktigt gammal mjukvara\",\n\t\"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)\",\n\n\t\"un_m1\": \"du kan radera dina senaste uppladdningar (eller avbryta pågående sådana) nedan\",\n\t\"un_upd\": \"uppdatera\",\n\t\"un_m4\": \"eller, dela filerna som syns nedan:\",\n\t\"un_ulist\": \"visa\",\n\t\"un_ucopy\": \"kopiera\",\n\t\"un_flt\": \"filter:&nbsp; sökvägen måste innehålla\",\n\t\"un_fclr\": \"rensa filtret\",\n\t\"un_derr\": 'unpost-radering misslyckades:\\n',\n\t\"un_f5\": 'något gick sönder, prova att uppdatera eller tryck på F5',\n\t\"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\",\n\t\"un_nou\": '<b>varning:</b> servern är för upptagen för att visa pågående uppladdningar; klicka på \"uppdatera\" om en stund',\n\t\"un_noc\": '<b>varning:</b> serverkonfigurationen tillåter inte unpost:ning av uppladdade filer',\n\t\"un_max\": \"visar de första 2000 filerna (använd filtret)\",\n\t\"un_avail\": \"{0} av de senaste uppladdningarna kan raderas<br />{1} pågående uppladdningar kan avbrytas\",\n\t\"un_m2\": \"sorterat efter uppladdningstid; senast uppladdad först:\",\n\t\"un_no1\": \"tjosan! inga uppladdningar är tillräckligt nya\",\n\t\"un_no2\": \"tjosan! inga uppladdningar som matchar filtret är tillräckligt nya\",\n\t\"un_next\": \"radera de {0} nästkommande filerna\",\n\t\"un_abrt\": \"avbryt\",\n\t\"un_del\": \"radera\",\n\t\"un_m3\": \"laddar dina senaste uppladdningar...\",\n\t\"un_busy\": \"raderar {0} filer...\",\n\t\"un_clip\": \"{0} länkar kopierade till urklippet\",\n\n\t\"u_https1\": \"du bör\",\n\t\"u_https2\": \"byta till https\",\n\t\"u_https3\": \"för bättre prestanda\",\n\t\"u_ancient\": 'din webbläsare är imponerande uråldrig -- du kanske borde <a href=\"#\" onclick=\"goto(\\'bup\\')\">använda bup istället</a>',\n\t\"u_nowork\": \"firefox 53+ eller chrome 57+ eller iOS 11+ krävs\",\n\t\"tail_2old\": \"firefox 105+ eller chrome 71+ eller iOS 14.5+ krävs\",\n\t\"u_nodrop\": 'din webbläsare är för gammal för dra-och-släpp-uppladdning',\n\t\"u_notdir\": \"det där är ingen mapp!\\n\\ndin webbläsare är för gammal,\\nprova dra-och-släpp istället\",\n\t\"u_uri\": \"släpp bilder från andra webbläsarfönster på den stora\\nuppladdningsknappen för att ladda upp dem\",\n\t\"u_enpot\": 'byt till <a href=\"#\">potatisgränssnittet</a> (kan förbättra uppladdningshastigheten)',\n\t\"u_depot\": 'byt till <a href=\"#\">det snygga gränssnittet</a> (kan försämra uppladdningshastigheten)',\n\t\"u_gotpot\": 'byter till potatisgränssnittet för förbättrad uppladdningshastighet,\\n\\nbyt gärna tillbaka om du vill!',\n\t\"u_pott\": \"<p>filer: &nbsp; <b>{0}</b> färdiga, &nbsp; <b>{1}</b> misslyckade, &nbsp; <b>{2}</b> pågående, &nbsp; <b>{3}</b> köade</p>\",\n\t\"u_ever\": \"detta är den enkla uppladdaren; up2k kräver minst<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1\",\n\t\"u_su2k\": 'detta är den enkla uppladdaren; <a href=\"#\" id=\"u2yea\">up2k</a> är bättre',\n\t\"u_uput\": 'optimera hastigheten (skippa checksumman)',\n\t\"u_ewrite\": 'du har inte skrivrättighet i denna mapp',\n\t\"u_eread\": 'du har inte läsrättighet i denna mapp',\n\t\"u_enoi\": 'serverkonfigurationen har inte slagit på sökning',\n\t\"u_enoow\": \"du kan inte skriva över här; raderingsrättighet krävs\",\n\t\"u_badf\": 'Dessa {0} filer (av totalt {1}) skippades, möjligtvis p.g.a. filsystemsrättigheter:\\n\\n',\n\t\"u_blankf\": 'Dessa {0} filer (av totalt {1}) är tomma; ladda upp dem ändå?\\n\\n',\n\t\"u_applef\": 'Dessa {0} filer (av totalt {1}) är förmodligen oönskade;\\nTryck <code>OK/Enter</code> för att SKIPPA de följande filerna,\\nTryck <code>Avbryt/ESC</code> för att INKLUDERA och LADDA UPP dem:\\n\\n',\n\t\"u_just1\": '\\nDet kanske fungerar om du endast väljer en fil',\n\t\"u_ff_many\": \"om du använder <b>Linux / MacOS / Android,</b> så <em>kan</em> denna mängd filer <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\">krascha Firefox!</a>\\nom detta händer, vänligen försök igen (eller använd Chrome).\",\n\t\"u_up_life\": \"Denna uppladdning kommer att raderas från servern om\\n{0} efter att den har blivit uppladdad\",\n\t\"u_asku\": 'ladda upp dessa {0} filer till <code>{1}</code>',\n\t\"u_unpt\": \"du kan ångra / radera denna uppladdning med 🧯 uppe till vänster\",\n\t\"u_bigtab\": 'försöker att visa {0} filer\\n\\ndetta kan krascha din webbläsare, är du säker?',\n\t\"u_scan\": 'Scannar filer...',\n\t\"u_dirstuck\": 'katalogskannern fastnade när den försökte komma åt de följande {0} objekten; dessa kommer att skippas:',\n\t\"u_etadone\": 'Klar ({0}, {1} filer)',\n\t\"u_etaprep\": '(förbereder uppladdning)',\n\t\"u_hashdone\": 'hashning klar',\n\t\"u_hashing\": 'hashar',\n\t\"u_hs\": 'skakar hand...',\n\t\"u_started\": \"filerna laddas nu upp; se [🚀]\",\n\t\"u_dupdefer\": \"duplikat; kommer att behandlas efter alla andra filer\",\n\t\"u_actx\": \"klicka här för att undvika prestandaförlust<br />när du byter till andra fönster/flikar\",\n\t\"u_fixed\": \"Okej!&nbsp; Fixat 👍\",\n\t\"u_cuerr\": \"misslyckades att ladda upp chunk {0} av {1};\\nförmodligen harmlöst, fortsätter\\n\\nfil: {2}\",\n\t\"u_cuerr2\": \"servern avvisade uppladdningen (chunk {0} av {1});\\nprovar igen senare\\n\\nfil: {2}\\n\\nfel \",\n\t\"u_ehstmp\": \"provar igen; see nedåt till höger\",\n\t\"u_ehsfin\": \"servern avvisade förfrågan att färdigställa uppladdningen; provar igen...\",\n\t\"u_ehssrch\": \"servern avvisade förfrågan att söka; provar igen...\",\n\t\"u_ehsinit\": \"servern avvisade förfrågan att påbörja uppladdningen; provar igen...\",\n\t\"u_eneths\": \"nätverksfel vid handskakning; provar igen...\",\n\t\"u_enethd\": \"nätverksfel när destinationens existens testades; provar igen...\",\n\t\"u_cbusy\": \"väntar på att servern ska lita på oss igen efter nätverksfel...\",\n\t\"u_ehsdf\": \"servern fick slut på diskutrymme!\\n\\nprovar igen, ifall någon rensar upp\\ntillräckligt med utrymme för att fortsätta\",\n\t\"u_emtleak1\": \"det verkar som att din webbläsare kanske har en minnesläcka;\\nvänligen\",\n\t\"u_emtleak2\": ' <a href=\"{0}\">byt till https (rekommenderat)</a> eller ',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": 'prova följande:\\n<ul><li>tryck <code>F5</code> för att uppdatera sidan</li><li>avaktivera sedan &nbsp;<code>mt</code>&nbsp;-växeln i &nbsp;<code>⚙️-inställningarna</code></li><li>och prova att ladda upp igen</li></ul>Uppladdningar kommer att vara lite långsammare, men aja.\\nBeklagar problemet!\\n\\nPS: chrome v107 <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">har en buggfix</a> för detta',\n\t\"u_emtleakf\": 'prova följande:\\n<ul><li>tryck <code>F5</code> för att uppdatera sidan</li><li>aktivera sedan <code>🥔</code> (potatis) i uppladdningsgränssnittet<li>och prova att ladda upp igen</li></ul>\\nPS: firefox <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">kommer förhoppningsvis få en buggfix</a> vid något tillfälle',\n\t\"u_s404\": \"hittades ej på servern\",\n\t\"u_expl\": \"förklara\",\n\t\"u_maxconn\": \"de flesta webbläsare begränsar detta till 6, men firefox låter dig höja gränsen med <code>connections-per-server</code> i <code>about:config</code>\",\n\t\"u_tu\": '<p class=\"warn\">VARNING: turbo är aktiverat, <span>&nbsp;det är möjligt att klienten inte upptäcker och återupptar ofärdiga uppladdningar; se tipset för turbo-växeln</span></p>',\n\t\"u_ts\": '<p class=\"warn\">VARNING: turbo är aktiverat, <span>&nbsp;sökresultat kan vara felaktiga; se tipset för turbo-växeln</span></p>',\n\t\"u_turbo_c\": \"serverkonfigurationen har avaktiverat turbo\",\n\t\"u_turbo_g\": \"avaktiverar turbo eftersom du inte har rättigheten\\natt se mappars innehåll i den här volymen\",\n\t\"u_life_cfg\": 'radera automatiskt efter <input id=\"lifem\" p=\"60\" /> min (eller <input id=\"lifeh\" p=\"3600\" /> timmar)',\n\t\"u_life_est\": 'uppladdningen kommer att raderas vid <span id=\"lifew\" tt=\"local time\">---</span>',\n\t\"u_life_max\": 'denna mapp tvingar en\\nhögsta livstid på {0}',\n\t\"u_unp_ok\": 'unpost är tillåten för {0}',\n\t\"u_unp_ng\": 'unpost är INTE tillåten',\n\t\"ue_ro\": 'du har endast läsrättighet till denna mapp\\n\\n',\n\t\"ue_nl\": 'du är inte inloggad',\n\t\"ue_la\": 'du är inloggad som \"{0}\"',\n\t\"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',\n\t\"ue_ta\": 'prova att ladda upp igen nu, det bör fungera',\n\t\"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\",\n\t\"ur_1uo\": \"Okej: Filen laddades upp med framgång\",\n\t\"ur_auo\": \"Okej: Alla {0} filer laddades upp med framgång\",\n\t\"ur_1so\": \"Okej: Filen fanns på servern\",\n\t\"ur_aso\": \"Okej: Alla {0} filer fanns på servern\",\n\t\"ur_1un\": \"Uppladdningen misslyckades, ledsen\",\n\t\"ur_aun\": \"Alla {0} uppladdningar misslyckades, ledsen\",\n\t\"ur_1sn\": \"Filen hittades INTE på servern\",\n\t\"ur_asn\": \"De {0} filerna hittades INTE på servern\",\n\t\"ur_um\": \"Klar;\\n{0} uppladdningar gick okej,\\n{1} uppladdningar misslyckades, ledsen\",\n\t\"ur_sm\": \"Klar;\\n{0} filer hittades på servern,\\n{1} filer hittades INTE på servern\",\n\n\t\"rc_opn\": \"öppna\", //m\n\t\"rc_ply\": \"spela upp\", //m\n\t\"rc_pla\": \"spela upp som ljud\", //m\n\t\"rc_txt\": \"öppna i filvisare\", //m\n\t\"rc_md\": \"öppna i textredigerare\", //m\n\t\"rc_dl\": \"Ladda ner\", //m\n\t\"rc_zip\": \"Ladda ner som arkiv\", //m\n\t\"rc_cpl\": \"kopiera länk\", //m\n\t\"rc_del\": \"radera\", //m\n\t\"rc_cut\": \"klipp ut\", //m\n\t\"rc_cpy\": \"kopiera\", //m\n\t\"rc_pst\": \"klistra in\", //m\n\t\"rc_rnm\": \"byt namn\", //m\n\t\"rc_nfo\": \"ny mapp\", //m\n\t\"rc_nfi\": \"ny fil\", //m\n\t\"rc_sal\": \"markera alla\", //m\n\t\"rc_sin\": \"invertera markering\", //m\n\t\"rc_shf\": \"dela denna mapp\", //m\n\t\"rc_shs\": \"dela urval\", //m\n\n\t\"lang_set\": \"uppdatera för att ändringen ska ta effekt?\",\n\n\t\"splash\": {\n\t\t\"a1\": \"uppdatera\",\n\t\t\"b1\": \"tjena främling &nbsp; <small>(du är inte inloggad)</small>\",\n\t\t\"c1\": \"logga ut\",\n\t\t\"d1\": \"dumpa stacken\",\n\t\t\"d2\": \"visar tillståndet på alla aktiva trådar\",\n\t\t\"e1\": \"ladda om konfig.\",\n\t\t\"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\",\n\t\t\"f1\": \"du kan bläddra:\",\n\t\t\"g1\": \"du kan ladda upp till:\",\n\t\t\"cc1\": \"annat:\",\n\t\t\"h1\": \"avaktivera k304\",\n\t\t\"i1\": \"aktivera k304\",\n\t\t\"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), <em>men</em> saker kommer också att bli långsammare i allmänhet\",\n\t\t\"k1\": \"återställ klientinställningar\",\n\t\t\"l1\": \"logga in för att se mer:\",\n\t\t\"ls3\": \"logga in\", //m\n\t\t\"lu4\": \"användarnamn\", //m\n\t\t\"lp4\": \"lösenord\", //m\n\t\t\"lo3\": \"logga ut “{0}” överallt\", //m\n\t\t\"lo2\": \"avsluta sessionen i alla webbläsare\", //m\n\t\t\"m1\": \"välkommen tillbaka,\",\n\t\t\"n1\": \"404 hittades inte &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": 'eller så har du kanske inte tillgång -- prova ett lösenord eller <a href=\"' + SR + '/?h\">åk hem</a>',\n\t\t\"p1\": \"403 nekat &nbsp;~┻━┻\",\n\t\t\"q1\": 'använd ett lösenord eller <a href=\"' + SR + '/?h\">åk hem</a>',\n\t\t\"r1\": \"åk hem\",\n\t\t\".s1\": \"skanna om\",\n\t\t\"t1\": \"åtgärd\",\n\t\t\"u2\": \"tid sedan senaste serverskrivning$N( uppladdning / namnbyte / ... )$N$N17d = 17 dagar$N1h23 = 1 timme 23 minuter$N4m56 = 4 minuter 56 sekunder\",\n\t\t\"v1\": \"koppla upp\",\n\t\t\"v2\": \"använd denna server som en lokal disk\",\n\t\t\"w1\": \"byt till https\",\n\t\t\"x1\": \"byt lösenord\",\n\t\t\"y1\": \"redigera utdelningar\",\n\t\t\"z1\": \"lås upp denna utdelning:\",\n\t\t\"ta1\": \"fyll i ditt nya lösenord\",\n\t\t\"ta2\": \"upprepa det nya lösenordet:\",\n\t\t\"ta3\": \"det blev fel; vänligen försök igen\",\n\t\t\"nop\": \"FEL: Lösenordet får inte vara tomt\", //m\n\t\t\"nou\": \"FEL: Användarnamn och/eller lösenord får inte vara tomt\", //m\n\t\t\"aa1\": \"inkommande filer:\",\n\t\t\"ab1\": \"avaktivera no304\",\n\t\t\"ac1\": \"aktivera no304\",\n\t\t\"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!\",\n\t\t\"ae1\": \"aktiva nedladdningar:\",\n\t\t\"af1\": \"visa senaste uppladdningar\",\n\t\t\"ag1\": \"visa idp-cache\",\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/tur.js",
    "content": "\n// \"//m\" ile biten satırlar doğrulanmamış makine çevirileridir\n\nLs.tur = {\n\t\"tt\": \"Türkçe\",\n\n\t\"cols\": {\n\t\t\"c\": \"işlem butonları\",\n\t\t\"dur\": \"süre\",\n\t\t\"q\": \"kalite / bitrate\",\n\t\t\"Ac\": \"ses kodlaması\",\n\t\t\"Vc\": \"video kodlaması\",\n\t\t\"Fmt\": \"format / yapı\",\n\t\t\"Ahash\": \"ses denetim toplamı\",\n\t\t\"Vhash\": \"video denetim toplamı\",\n\t\t\"Res\": \"çözünürlük\",\n\t\t\"T\": \"dosya türü\",\n\t\t\"aq\": \"ses kalitesi / bitrate\",\n\t\t\"vq\": \"video kalitesi / bitrate\",\n\t\t\"pixfmt\": \"subsampling / pixel yapısı\",\n\t\t\"resw\": \"yatay çözünürlük\",\n\t\t\"resh\": \"dikey çözünürlük\",\n\t\t\"chs\": \"ses kanalları\",\n\t\t\"hz\": \"örnekleme hızı\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"diğer\",\n\t\t\t[\"ESC\", \"kapat\"],\n\n\t\t\t\"dosya yönetimi\",\n\t\t\t[\"G\", \"liste / ızgara görünümü arasında geçiş yap\"],\n\t\t\t[\"T\", \"küçük resimler / simgeler arasında geçiş yap\"],\n\t\t\t[\"⇧ A/D\", \"küçük resim boyutu\"],\n\t\t\t[\"ctrl-K\", \"seçileni sil\"],\n\t\t\t[\"ctrl-X\", \"seçimi panoya kes\"],\n\t\t\t[\"ctrl-C\", \"seçimi panoya kopyala\"],\n\t\t\t[\"ctrl-V\", \"buraya yapıştır (taşı/kopyala)\"],\n\t\t\t[\"Y\", \"seçileni indir\"],\n\t\t\t[\"F2\", \"seçileni yeniden adlandır\"],\n\n\t\t\t\"dosya yönetimi seçimleri\",\n\t\t\t[\"boşluk\", \"seçimi değiştir\"],\n\t\t\t[\"↑/↓\", \"seçim imlecini hareket ettir\"],\n\t\t\t[\"ctrl ↑/↓\", \"imleci ve görünümü hareket ettir\"],\n\t\t\t[\"⇧ ↑/↓\", \"önceki/sonraki dosyayı seç\"],\n\t\t\t[\"ctrl-A\", \"tüm dosyaları / klasörleri seç\"],\n\t\t], [\n\t\t\t\"navigasyon\",\n\t\t\t[\"B\", \"içerik haritası / navigasyon paneli arasında geçiş yap\"],\n\t\t\t[\"I/K\", \"önceki/sonraki klasör\"],\n\t\t\t[\"M\", \"üst klasör (veya mevcut olanı daralt)\"],\n\t\t\t[\"V\", \"navigasyon panelinde klasörler / metin dosyaları arasında geçiş yap\"],\n\t\t\t[\"A/D\", \"navigasyon paneli boyutu\"],\n\t\t], [\n\t\t\t\"ses oynatıcı\",\n\t\t\t[\"J/L\", \"önceki/sonraki şarkı\"],\n\t\t\t[\"U/O\", \"10sn geri/ileri atla\"],\n\t\t\t[\"0..9\", \"%0..%90 atla\"],\n\t\t\t[\"P\", \"oynat/duraklat (aynı zamanda oynatıcıyı aç)\"],\n\t\t\t[\"S\", \"şarkıyı seç\"],\n\t\t\t[\"Y\", \"şarkıyı indir\"],\n\t\t], [\n\t\t\t\"resim görüntüleyici\",\n\t\t\t[\"J/L, ←/→\", \"önceki/sonraki resim\"],\n\t\t\t[\"Home/End\", \"ilk/son resim\"],\n\t\t\t[\"F\", \"tam ekran\"],\n\t\t\t[\"R\", \"sağa döndür\"],\n\t\t\t[\"⇧ R\", \"sola döndür\"],\n\t\t\t[\"S\", \"resmi seç\"],\n\t\t\t[\"Y\", \"resmi indir\"],\n\t\t], [\n\t\t\t\"video oynatıcı\",\n\t\t\t[\"U/O\", \"10sn geri/ileri atla\"],\n\t\t\t[\"P/K/Space\", \"oynat/duraklat\"],\n\t\t\t[\"C\", \"sıradakinden devam et\"],\n\t\t\t[\"V\", \"döngü\"],\n\t\t\t[\"M\", \"sessiz\"],\n\t\t\t[\"[ ve ]\", \"döngü aralığını ayarla\"],\n\t\t], [\n\t\t\t\"metin dosyası görüntüleyici\",\n\t\t\t[\"I/K\", \"önceki/sonraki dosya\"],\n\t\t\t[\"M\", \"metin dosyasını kapat\"],\n\t\t\t[\"E\", \"metin dosyasını düzenle\"],\n\t\t\t[\"S\", \"dosyayı seç (kes/kopyala/yeniden adlandır)\"],\n\t\t\t[\"Y\", \"metin dosyasını indir\"], //m\n\t\t\t[\"⇧ J\", \"json güzelleştir\"], //m\n\t\t]\n\t],\n\n\t\"m_ok\": \"Tamam\",\n\t\"m_ng\": \"İptal\",\n\n\t\"enable\": \"Etkinleştir\",\n\t\"danger\": \"TEHLİKE\",\n\t\"clipped\": \"panoya kopyalandı\",\n\n\t\"ht_s1\": \"saniye\",\n\t\"ht_s2\": \"saniye\",\n\t\"ht_m1\": \"dakika\",\n\t\"ht_m2\": \"dakika\",\n\t\"ht_h1\": \"saat\",\n\t\"ht_h2\": \"saat\",\n\t\"ht_d1\": \"gün\",\n\t\"ht_d2\": \"gün\",\n\t\"ht_and\": \" ve \",\n\n\t\"goh\": \"kontrol paneli\",\n\t\"gop\": 'önceki kardeş\">önceki',\n\t\"gou\": 'üst klasör\">üst',\n\t\"gon\": 'sonraki klasör\">sonraki',\n\t\"logout\": \"Çıkış \",\n\t\"login\": \"Giriş\",\n\t\"access\": \" erişim\",\n\t\"ot_close\": \"alt menüyü kapat\",\n\t\"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`&quot;try unite&quot;` = 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`\",\n\t\"ot_unpost\": \"unpost: son yüklemelerinizi silin veya tamamlanmamış olanları iptal edin\",\n\t\"ot_bup\": \"bup: temel yükleyici, hatta netscape 4.0'ı destekler\",\n\t\"ot_mkdir\": \"mkdir: yeni bir dizin oluştur\",\n\t\"ot_md\": \"new-file: yeni bir metin dosyası oluştur\", //m\n\t\"ot_msg\": \"msg: sunucu günlüğüne bir mesaj gönder\",\n\t\"ot_mp\": \"medya oynatıcı seçenekleri\",\n\t\"ot_cfg\": \"konfigürasyon seçenekleri\",\n\t\"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 [🎈]&nbsp; (temel yükleyici) ile karşılaştırıldığında daha fazla CPU kullanır<br /><br />yükleme sırasında, bu simge bir ilerleme göstergesi haline gelir!',\n\t\"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 [🎈]&nbsp; (temel yükleyici) ile karşılaştırıldığında daha fazla CPU kullanır<br /><br />yükleme sırasında, bu simge bir ilerleme göstergesi haline gelir!',\n\t\"ot_noie\": 'Lütfen Chrome / Firefox / Edge kullanın',\n\n\t\"ab_mkdir\": \"dizin oluştur\",\n\t\"ab_mkdoc\": \"yeni metin dosyası\", //m\n\t\"ab_msg\": \"sunucu günlüğüne mesaj gönder\",\n\n\t\"ay_path\": \"klasörlere atla\",\n\t\"ay_files\": \"dosyalara atla\",\n\n\t\"wt_ren\": \"seçilenleri yeniden adlandır$NKısa yol: F2\",\n\t\"wt_del\": \"Seçilen ögeleri sil$Kısa yol: ctrl-K\",\n\t\"wt_cut\": \"seçilen ögeleri kes &lt;small&gt;&lt;/small&gt;$NKısa yol: ctrl-X\",\n\t\"wt_cpy\": \"seçilen ögeleri panoya kopyala$N$NKısa yol: ctrl-C\",\n\t\"wt_pst\": \"daha önce kesilmiş / kopyalanmış bir seçimi yapıştır$NKısa yol: ctrl-V\",\n\t\"wt_selall\": \"tüm dosyaları seç$NKısa yol: ctrl-A (dosya odaklandığında)\",\n\t\"wt_selinv\": \"seçimi tersine çevir\",\n\t\"wt_zip1\": \"seçilenleri sıkıştırılmış bir arşiv olarak indir\",\n\t\"wt_selzip\": \"seçimi arşiv olarak indir\",\n\t\"wt_seldl\": \"seçimi ayrı dosyalar olarak indir$NHotkey: Y\",\n\t\"wt_npirc\": \"irc biçiminde parça bilgilerini kopyala\",\n\t\"wt_nptxt\": \"düz metin parça bilgilerini kopyala\",\n\t\"wt_m3ua\": \"m3u çalma listesine ekle (daha sonra <code>📻kopyala</code>ya tıklayın)\",\n\t\"wt_m3uc\": \"m3u çalma listesini panoya kopyala\",\n\t\"wt_grid\": \"ızgara / liste görünümünü değiştir$NHotkey: G\",\n\t\"wt_prev\": \"önceki parça$NHotkey: J\",\n\t\"wt_play\": \"oynat / duraklat$NHotkey: P\",\n\t\"wt_next\": \"sonraki parça$NHotkey: L\",\n\n\t\"ul_par\": \"paralel yüklemeler:\",\n\t\"ut_rand\": \"dosya adlarını rastgeleleştir\",\n\t\"ut_u2ts\": \"kendi dosyalarınızdan sunucuya$Nzaman damgasını kopyala\\\">📅\",\n\t\"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\n\t\"ut_mt\": \"yükleme yaparken diğer dosyaların hash'lenmesini durdur$N$kötü bir CPU veya HDD'ye sahipseniz kullanabilirsiniz.\",\n\t\"ut_ask\": 'yüklemeye başlamadan önce doğrulama mesajı göster\">💭',\n\t\"ut_pot\": \"arayüzü daha az karmaşık hale getirerek$Nyükleme hızını yavaş cihazlarda artır\",\n\t\"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)\",\n\t\"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\",\n\t\"ul_btn\": \"dosyaları / klasörleri <br>buraya sürükleyin (veya bana tıklayın)\",\n\t\"ul_btnu\": \"Y Ü K L E\",\n\t\"ul_btns\": \"A R A\",\n\n\t\"ul_hash\": \"hash\",\n\t\"ul_send\": \"gönder\",\n\t\"ul_done\": \"tamamlandı\",\n\t\"ul_idle1\": \"henüz bekleyen yükleme yok\",\n\t\"ut_etah\": \"ortalama &lt;em&gt;hash&lt;/em&gt; hızı ve bitişe kadar tahmini süre\",\n\t\"ut_etau\": \"ortalama &lt;em&gt;yükleme&lt;/em&gt; hızı ve bitişe kadar tahmini süre\",\n\t\"ut_etat\": \"ortalama &lt;em&gt;toplam&lt;/em&gt; hızı ve bitişe kadar tahmini süre\",\n\n\t\"uct_ok\": \"başarıyla tamamlandı\",\n\t\"uct_ng\": \"başarısız: başarısız / reddedildi / bulunamadı\",\n\t\"uct_done\": \"hem başarılı hem de başarısız\",\n\t\"uct_bz\": \"hash'liyor veya yüklüyor\",\n\t\"uct_q\": \"beklemede, gönderiliyor\",\n\n\t\"utl_name\": \"dosya adı\",\n\t\"utl_ulist\": \"liste\",\n\t\"utl_ucopy\": \"kopyala\",\n\t\"utl_links\": \"bağlantılar\",\n\t\"utl_stat\": \"durum\",\n\t\"utl_prog\": \"ilerleme\",\n\n\t// kısa tutun:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"HATA\",\n\t\"utl_oserr\": \"OS-hatası\",\n\t\"utl_found\": \"bulundu\",\n\t\"utl_defer\": \"ertele\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"tamamlandı\",\n\n\t\"ul_flagblk\": \"dosyalar sıraya alındı</b><br>ancak başka bir tarayıcı sekmesinde meşgul bir up2k var,<br>bu nedenle önce onun bitmesini bekliyor\",\n\t\"ul_btnlk\": \"sunucu yapılandırması bu anahtarı bu duruma kilitledi\",\n\n\t\"udt_up\": \"Yükle\",\n\t\"udt_srch\": \"Ara\",\n\t\"udt_drop\": \"buraya bırakın\",\n\n\t\"u_nav_m\": '<h6>Pekâlâ, bakalım neyin  varmnış?</h6><code>Enter</code> = Dosyalar (bir veya birden fazla)\\n<code>ESC</code> = Bir klasör (alt klasörler dahil)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Dosyalar</a><a href=\"#\" id=\"modal-ng\">Tek klasör</a>',\n\t\n\t\"cl_opts\": \"aç / kapat\",\n\t\"cl_hfsz\": \"dosya boyutu\", //m\n\t\"cl_themes\": \"tema\",\n\t\"cl_langs\": \"dil\",\n\t\"cl_ziptype\": \"klasör indirme\",\n\t\"cl_uopts\": \"up2k aç / kapat\",\n\t\"cl_favico\": \"favicon\",\n\t\"cl_bigdir\": \"big dirs\",\n\t\"cl_hsort\": \"#sort\",\n\t\"cl_keytype\": \"anahtar notasyonu\",\n\t\"cl_hiddenc\": \"gizli sütunlar\",\n\t\"cl_hidec\": \"gizle\",\n\t\"cl_reset\": \"sıfırla\",\n\t\"cl_hpick\": \"aşağıdaki tabloda gizlemek için sütun başlıklarına dokunun\",\n\t\"cl_hcancel\": \"sütun gizleme iptal edildi\",\n\t\"cl_rcm\": \"sağ tık menüsü\", //m\n\n\t\"ct_grid\": '田 ızgara',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ ipuçları',\n\t\"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',\n\t\"ct_csel\": 'ızgara görünümünde dosya seçimi için CTRL ve SHIFT tuşlarını kullanın\">seç',\n\t\"ct_dsel\": 'ızgara görünümünde sürükleyerek seçimi kullanın\">sürükle', //m\n\t\"ct_dl\": 'dosyaya tıklandığında indirmeyi zorla (satır içinde görüntüleme)\">dl', //m\n\t\"ct_ihop\": 'resim görüntüleyici kapatıldığında, en son görüntülenen dosyaya kaydırın\">g⮯',\n\t\"ct_dots\": 'gizli dosyaları göster (sunucu izin veriyorsa)\">nokta dosyaları',\n\t\"ct_qdel\": 'dosyaları silerken yalnız bir kez onay isteyin\">qdel',\n\t\"ct_dir1st\": 'sıralamada klasörleri dosyalardan önceye koy\">ilk 📁',\n\t\"ct_nsort\": 'doğal sıralama (başında sayı bulunan isimler için)\">dsort',\n\t\"ct_utc\": 'tüm zaman damgalarını UTC diliminde göster\">UTC',\n\t\"ct_readme\": 'README.md\\'yi klasör listelemelerinde göster\">📜 readme',\n\t\"ct_idxh\": 'index.html\\'i klasör listelemeleri yerine göster\">htm',\n\t\"ct_sbars\": 'kaydırma çubuklarını göster\">⟊',\n\n\t\"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📅\",\n\n\t\"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 <em> ile değiştirir&quot;sunucudaki boyutu da aynı mı?&quot;</em> dosya içerikleri farklıysa yükleme devam ETMEYECEKTİR!$N$Nyükleme tamamlandığında bu ayarı kapatın ve sonra aynı dosyaları yeniden &quot;yüklemeyi&quot; deneyin ki sunucu bu dosyaları doğrulayabilsin\\\">turbo\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cut_nag\": \"yükleme tamamlandığında işletim sistemi bildirimi$N(sadece tarayıcı veya serkme aktif değilse)\",\n\t\"cut_sfx\": \"yükleme tamamlandığında sesli bildirim$N(sadece tarayıcı veya serkme aktif değilse)\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cft_text\": \"favicon yazısı (devre dışı bırakmak için boş bırakıp yenileyin)\",\n\t\"cft_fg\": \"ön plan rengi\",\n\t\"cft_bg\": \"arka plan rengi\",\n\n\t\"cdt_lim\": \"bir klasörde gösterilecek maksimum dosya sayısı\",\n\t\"cdt_ask\": \"aşağı kaydırırken,$Ndaha fazla dosya yüklemek yerine,$Nne yapılacağını sor\",\n\t\"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\",\n\t\"cdt_ren\": \"özel sağ tık menüsünü etkinleştir, normal menü shift + sağ tık ile erişilebilir\\\">etkinleştir\", //m\n\t\"cdt_rdb\": \"özel menü zaten açıkken tekrar sağ tıklanınca normal sağ tık menüsünü göster\\\">x2\", //m\n\n\t\"tt_entree\": \"navigasyon panosunu göster (yan dizin panosu)$NHotkey: B\",\n\t\"tt_detree\": \"içerik haritasını göster$Kısayol: B\",\n\t\"tt_visdir\": \"seçili klasöre kaydır\",\n\t\"tt_ftree\": \"klasör ağacını / metin dosyalarını aç/kapat$Kısayol: V\",\n\t\"tt_pdock\": \"üstteki bir pencerede üst klasörleri göster\",\n\t\"tt_dynt\": \"ağaç genişledikçe otomatik büyüt\",\n\t\"tt_wrap\": \"kelime sarma\",\n\t\"tt_hover\": \"fare ile üzerine gelindiğinde taşan satırları göster$N( fare imleci sol kenarda değilse kaydırmayı bozar )\",\n\n\t\"ml_pmode\": \"klasör sonunda...\",\n\t\"ml_btns\": \"komutlar\",\n\t\"ml_tcode\": \"dönüştür\",\n\t\"ml_tcode2\": \"dönüştür\",\n\t\"ml_tint\": \"tonlama\",\n\t\"ml_eq\": \"ses eşitleyici\",\n\t\"ml_drc\": \"dinamik aralık sıkıştırıcı\",\n\t\"ml_ss\": \"sessizliği atla\", //m\n\n\t\"mt_loop\": \"bir şarkıyı döngüye al / tekrar et\\\">🔁\",\n\t\"mt_one\": \"bir şarkıdan sonra dur\\\">1️⃣\",\n\t\"mt_shuf\": \"klasörlerdeki şarkıları karıştır\\\">🔀\",\n\t\"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▶\",\n\t\"mt_preload\": \"aralıksız oynatma için sıradaki şarkıyı önceden yüklemeye başla\\\">ön yükleme\",\n\t\"mt_prescan\": \"son şarkı bitmeden önce bir sonraki klasöre git$Nweb tarayıcısını mutlu tutar$Nbu nedenle oynatmayı durdurmaz\\\">nav\",\n\t\"mt_fullpre\": \"tüm şarkıyı ön yüklemeye çalış;$N✅ <b>güvenilmez</b> bağlantılarda etkinleştir,$N❌ <b>yavaş</b> bağlantılarda devre dışı bırak\\\">full\",\n\t\"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)\\\">☕️\",\n\t\"mt_waves\": \"dalga formu kaydırıcı:$Nses genliğini kaydırıcıda göster\\\">~s\",\n\t\"mt_npclip\": \"şu anda çalan şarkıyı kopyalamak için düğmeleri göster\\\">/np\",\n\t\"mt_m3u_c\": \"seçilen şarkıları m3u8 çalma listesi girişleri olarak kopyalamak için düğmeleri göster\\\">📻\",\n\t\"mt_octl\": \"os entegrasyonu (medya kısayolları / osd)\\\">os-ctl\",\n\t\"mt_oseek\": \"OS entegrasyonuyla şarkı ilerletmeye izin ver$N$Nnot: bazı cihazlarda (iPhone'lar)$Nbu, sıradaki şarkı düğmesini değiştirir\\\">ilerletme\",\n\t\"mt_oscv\": \"ekranda albüm kapaklarını göster\\\">görsel\",\n\t\"mt_follow\": \"oynatılan müzik ibaresini görünümde tut\\\">🎯\",\n\t\"mt_compact\": \"kompakt kontroller\\\">⟎\",\n\t\"mt_uncache\": \"önbelleği temizle &nbsp;(bunu, tarayıcınızın bozuk bir şarkı kopyasını önbelleğe alması nedeniyle çalmayı reddettiğinde deneyin)\\\">önbelleği temizle\",\n\t\"mt_mloop\": \"açık klasörü döngüye al\\\">🔁 döngü\",\n\t\"mt_mnext\": \"bir sonraki klasörü yükle ve devam et\\\">📂 sonraki\",\n\t\"mt_mstop\": \"oynatmayı durdur\\\">⏸ durdur\",\n\t\"mt_cflac\": \"flac / wav'ı {0}'a dönüştür\\\">flac\",\n\t\"mt_caac\": \"aac / m4a'yı {0}'a dönüştür\\\">aac\",\n\t\"mt_coth\": \"diğer tüm formatları (mp3 hariç) {0}'a dönüştür\\\">oth\",\n\t\"mt_c2opus\": \"masaüstü, dizüstü bilgisayarlar, android için en iyi seçim\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, iOS 17.5 ve üzeri için\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, iOS 11 ile 17 arasında için\\\">caf\",\n\t\"mt_c2mp3\": \"çok eski cihazlarda bunu kullanın\\\">mp3\",\n\t\"mt_c2flac\": \"en iyi ses kalitesi, ancak büyük indirmeler\\\">flac\",\n\t\"mt_c2wav\": \"sıkıştırılmamış oynatma (daha da büyük)\\\">wav\",\n\t\"mt_c2ok\": \"güzel, iyi seçim\",\n\t\"mt_c2nd\": \"bu, cihazınız için önerilen çıkış formatı değil, ama sorun değil\",\n\t\"mt_c2ng\": \"cihazınız bu çıkış formatını desteklemiyor gibi görünüyor, ama yine de deneyelim\",\n\t\"mt_xowa\": \"iOS'ta bu formatta arka plan oynatımını engelleyen hatalar var; lütfen bunun yerine caf veya mp3 kullanın\",\n\t\"mt_tint\": \"seekbar'da arka plan seviyesi (0-100)$ön belleğin daha az dikkat dağıtıcı olmasını sağlar\",\n\t\"mt_eq\": \"`ekolayzer ve kazanç kontrolünü aktifleştirir;$N$Nboost `0` = standart %100 ses (varsayılan)$N$Ngenişlik `1 &nbsp;` = standart çift kanal (varsayılan)$Ngenişlik `0.5` = %50 sol-sağ crossfeed$Ngenişlik `0 &nbsp;` = mono$N$Nartış `-0.8` &amp; 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\",\n\t\"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)\",\n\t\"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\n\t\"mt_ssvt\": \"ses eşiği (0-255)\\\">ses\", //m\n\t\"mt_ssts\": \"etkin eşik (% parça, başlangıç)\\\">bas\", //m\n\t\"mt_sste\": \"etkin eşik (% parça, bitiş)\\\">son\", //m\n\t\"mt_sssm\": \"oynatma hızı çarpanı\\\">ileri\", //m\n\n\t\"mb_play\": \"oynat\",\n\t\"mm_hashplay\": \"bu ses dosyası oynatılsın mı?\",\n\t\"mm_m3u\": \"Oynatmak için <code>Enter/Tamam</code> tuşuna basın\\nDüzenlemek için <code>ESC/İptal</code> tuşuna basın\",\n\t\"mp_breq\": \"firefox 82+ veya chrome 73+ veya iOS 15+ gerektirir\",\n\t\"mm_bload\": \"şu anda yükleniyor...\",\n\t\"mm_bconv\": \"{0} formatına dönüştürülüyor, lütfen bekleyin...\",\n\t\"mm_opusen\": \"tarayıcınız aac / m4a dosyalarını oynatamıyor;\\nopus'a dönüştürme etkinleştirildi\",\n\t\"mm_playerr\": \"oynatma hatası: \",\n\t\"mm_eabrt\": \"Oynatma denemesi iptal edildi\",\n\t\"mm_enet\": \"İnternet bağlantınızda bir bokluk var\",\n\t\"mm_edec\": \"Bu dosya muhtemelen bozuk!\",\n\t\"mm_esupp\": \"Tarayıcınız salak, bu ses formatını anlamıyor\",\n\t\"mm_eunk\": \"Bilinmeyen Hata\",\n\t\"mm_e404\": \"Ses oynatılamadı; hata 404: Dosya bulunamadı.\",\n\t\"mm_e403\": \"Ses oynatılamadı; hata 403: Erişim reddedildi.\\n\\nYeniden yüklemek için F5 tuşuna basın, oturumunuz kapanmış olabilir.\",\n\t\"mm_e415\": \"Ses oynatılamadı; hata 415: Dosya dönüştürme başarısız oldu; sunucu günlüklerini kontrol edin.\", //m\n\t\"mm_e500\": \"Ses oynatılamadı; hata 500: Sunucu günlüklerini kontrol edin.\",\n\t\"mm_e5xx\": \"Ses oynatılamadı; sunucu hatası \",\n\t\"mm_nof\": \"yakınlarda başka ses dosyası bulunamadı\",\n\t\"mm_prescan\": \"Sonraki şarkı aranıyor...\",\n\t\"mm_scank\": \"Sonraki şarkı bulundu:\",\n\t\"mm_uncache\": \"önbellek temizlendi; tüm şarkılar bir sonraki çalınmada yeniden indirilecek\",\n\t\"mm_hnf\": \"bu şarkı artık mevcut değil\",\n\n\t\"im_hnf\": \"bu resim artık mevcut değil\",\n\n\t\"f_empty\": 'bu klasör boş',\n\t\"f_chide\": 'bu, «{0}» sütununu gizleyecektir\\n\\nsütunları ayarlar sekmesinden yeniden açabilirsiniz',\n\t\"f_bigtxt\": \"bu dosya {0} MiB boyutunda -- Cidden saf yazı olarak mı görüntülemek istiyo'n?\",\n\t\"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.\",\n\t\"fbd_more\": '<div id=\"blazy\"><code>{1}</code> dosyadan <code>{0}</code> tanesi gösteriliyor; <a href=\"#\" id=\"bd_more\">{2}</a> tanesini ya da <a href=\"#\" id=\"bd_all\">tümünü göster</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\"><code>{1}</code> dosyadan <code>{0}</code> tanesi gösteriliyor; <a href=\"#\" id=\"bd_all\">tümünü göster</a></div>',\n\t\"f_anota\": \"{1} dosyadan sadece {0} tanesi seçildi;\\nTüm klasörü seçmek için önce en alta kaydırın.\",\n\n\t\"f_dls\": 'bu klasördeki dosya linkleri\\nindirme linklerine dönüştürüldü',\n\t\"f_dl_nd\": 'klasör atlanıyor (bunun yerine zip/tar indirmesini kullanın):\\n', //m\n\n\t\"f_partial\": \"Mevcutta yüklenen bir dosyayı güvenli bir şekilde indirmek için lütfen aynı adlı ama <code>.PARTIAL</code> 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 <code>.PARTIAL</code> geçici dosyasını indirmeye devam edecektir ki bu da elinize bozuk veriler sunacaktır.\",\n\n\t\"ft_paste\": \"{0} ögeyi yapıştır$Kısayol: ctrl-V\",\n\t\"fr_eperm\": 'yeniden adlandırılamıyor:\\nbu klasörü “taşıma” izniniz yok',\n\t\"fd_eperm\": 'silinemiyor:\\nbu klasörde “silme” izniniz yok',\n\t\"fc_eperm\": 'kesilemiyor:\\nbu klasörde “taşıma“ izniniz yok',\n\t\"fp_eperm\": 'yapıştırılamıyor:\\nbu klasörde “yazma“ izniniz yok',\n\t\"fr_emore\": \"yeniden adlandırmak için en az bir öge seçin\",\n\t\"fd_emore\": \"silmek için en az bir öge seçin\",\n\t\"fc_emore\": \"kesmek için en az bir öge seçin\",\n\t\"fcp_emore\": \"panoya kopyalamak için en az bir öge seçin\",\n\n\t\"fs_sc\": \"bulunduğunuz klasörü paylaşın\",\n\t\"fs_ss\": \"seçilen dosyaları paylaşın\",\n\t\"fs_just1d\": \"birden fazla klasör seçemezsiniz\\nveya bir seçimde dosyaları ve klasörleri karıştıramazsınız\",\n\t\"fs_abrt\": \"❌ iptal\",\n\t\"fs_rand\": \"🎲 rastgele.ad\",\n\t\"fs_go\": \"✅ paylaşımı oluştur\",\n\t\"fs_name\": \"isim\",\n\t\"fs_src\": \"kaynak\",\n\t\"fs_pwd\": \"şifre\",\n\t\"fs_exp\": \"son kullanma\",\n\t\"fs_tmin\": \"dakika\",\n\t\"fs_thrs\": \"saat\",\n\t\"fs_tdays\": \"gün\",\n\t\"fs_never\": \"sonsuz\",\n\t\"fs_pname\": \"isteğe bağlı bağlantı adı; boşsa rastgele olacaktır\",\n\t\"fs_tsrc\": \"paylaşılacak dosya veya klasör\",\n\t\"fs_ppwd\": \"isteğe bağlı şifre\",\n\t\"fs_w8\": \"paylaşım oluşturuluyor...\",\n\t\"fs_ok\": \"panoya kopyalamak için <code>Enter/Tamam</code> tuşuna basın\\nkapatmak için <code>ESC/İptal</code> tuşuna basın\",\n\n\t\"frt_dec\": \"bazı bozuk dosya adlarını düzeltebilir\\\">url-decode\",\n\t\"frt_rst\": \"değiştirilen dosya adlarını orijinal haline döndür\\\">↺ sıfırla\",\n\t\"frt_abrt\": \"iptal et ve bu pencereyi kapat\\\">❌ iptal\",\n\t\"frb_apply\": \"YENİ ADI UYGULA\",\n\t\"fr_adv\": \"toplu / meta veri / desen yeniden adlandırma\\\">gelişmiş\",\n\t\"fr_case\": \"büyük/küçük harf duyarlı regex\\\">büyük/küçük harf\",\n\t\"fr_win\": \"windows'a uygun adlar; <code>&lt;&gt;:&quot;\\\\|?*</code> karakterlerini Japonca tam genişlik karakterleriyle değiştir\\\">win\",\n\t\"fr_slash\": \"<code>/</code> karakterini yeni klasörlerin oluşturulmasına neden olmayan bir karakterle değiştir\\\">/ yok\",\n\t\"fr_re\": \"`orijinal dosya adlarına uygulanacak regex arama deseni; yakalama grupları aşağıdaki format alanında `(1)` ve `(2)` gibi belirtilebilir\",\n\t\"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\",\n\t\"fr_pdel\": \"sil\",\n\t\"fr_pnew\": \"farklı kaydet\",\n\t\"fr_pname\": \"yeni ayarınızı adlandırın\",\n\t\"fr_aborted\": \"iptal edildi\",\n\t\"fr_lold\": \"eski ad\",\n\t\"fr_lnew\": \"yeni ad\",\n\t\"fr_tags\": \"seçilen dosyalar için etiketler (salt okunur, sadece referans için):\",\n\t\"fr_busy\": \"{0} öğe yeniden adlandırılıyor...\\n\\n{1}\",\n\t\"fr_efail\": \"yeniden adlandırma başarısız:\\n\",\n\t\"fr_nchg\": \"{0} yeni adlardan bazıları <code>win</code> ve/veya <code>/ yok</code> nedeniyle değiştirildi\\n\\nBu değiştirilmiş yeni adlarla devam etmek için Tamam'a basın.\",\n\n\t\"fd_ok\": \"silme tamam\",\n\t\"fd_err\": \"silme başarısız:\\n\",\n\t\"fd_none\": \"hiçbir şey silinmedi; belki sunucu yapılandırması (xbd) tarafından engellenmiştir?\",\n\t\"fd_busy\": \"{0} öğe siliniyor...\\n\\n{1}\",\n\t\"fd_warn1\": \"Bu {0} öge silinsin mi?\",\n\t\"fd_warn2\": \"<b>Son şans!</b> Geri alma imkanın yok. Silinsin mi?\",\n\n\t\"fc_ok\": \"{0} öge kesildi\",\n\t\"fc_warn\": '{0} öge kesildi\\n\\namma velakin: sadece <b>bu</b> tarayıcı penceresine yapıştırılabilirler\\n(çünkü seçiminin boyutu hayvan gibi)',\n\n\t\"fcc_ok\": \"{0} öge panoya kopyalandı\",\n\t\"fcc_warn\": '{0} öge panoya kopyalandı\\n\\namma velakin: sadece <b>bu</b> tarayıcı penceresine yapıştırılabilirler\\n(çünkü seçiminin boyutu hayvan gibi)',\n\n\t\"fp_apply\": \"bu adları kullan\",\n\t\"fp_skip\": \"çakışmaları atla\", //m\n\t\"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\",\n\t\"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\n\t\"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\n\t\"fp_emore\": \"hâlâ düzeltilmesi gereken bazı dosya adı çakışmaları var\",\n\t\"fp_ok\": \"taşıma tamam\",\n\t\"fcp_ok\": \"kopyalama tamam\",\n\t\"fp_busy\": \"{0} öğe taşınıyor...\\n\\n{1}\",\n\t\"fcp_busy\": \"{0} öğe kopyalanıyor...\\n\\n{1}\",\n\t\"fp_abrt\": \"İptal ediliyor...\",\n\t\"fp_err\": \"taşıma başarısız:\\n\",\n\t\"fcp_err\": \"kopyalama başarısız:\\n\",\n\t\"fp_confirm\": \"bu {0} öğeyi buraya taşımak istiyor musunuz?\",\n\t\"fcp_confirm\": \"bu {0} öğeyi buraya kopyalamak istiyor musunuz?\",\n\t\"fp_etab\": 'diğer tarayıcı sekmesinden pano okunamadı',\n\t\"fp_name\": \"cihazınızdan bir dosya yüklüyorsunuz. Bir ad verin:\",\n\t\"fp_both_m\": '<h6>yapıştırılacak şeyi seçin</h6><code>Enter</code> = «{1}» içinden {0} dosyayı taşı\\n<code>ESC</code> = cihazınızdan {2} dosyayı yükle',\n\t\"fcp_both_m\": '<h6>yapıştırılacak şeyi seçin</h6><code>Enter</code> = «{1}» içinden {0} dosyayı kopyala\\n<code>ESC</code> = cihazınızdan {2} dosyayı yükle',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Taşı</a><a href=\"#\" id=\"modal-ng\">Yükle</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Kopyala</a><a href=\"#\" id=\"modal-ng\">Yükle</a>',\n\n\t\"mk_noname\": \"bunu yapmadan önce soldaki boşluğa bir şeyler yazsana :p\",\n\t\"nmd_i1\": \"ayrıca istediğin dosya uzantısını ekleyebilirsin, örneğin <code>.md</code>\", //m\n\t\"nmd_i2\": \"silme iznin olmadığı için yalnızca <code>.{0}</code> dosyaları oluşturabilirsin\", //m\n\n\t\"tv_load\": \"Metin belgesi yükleniyor:\\n\\n{0}\\n\\n{1}% ({2} of {3} MiB yüklendi)\",\n\t\"tv_xe1\": \"metin dosyası yüklenemedi:\\n\\nhata \",\n\t\"tv_xe2\": \"404, dosya bulunamadı\",\n\t\"tv_lst\": \"metin dosyalarının listesi\",\n\t\"tvt_close\": \"klasör görünümüne dön$NKısayol: M (veya Esc)\\\">❌ kapat\",\n\t\"tvt_dl\": \"bu dosyayı indir$NKısayol: Y\\\">💾 indir\",\n\t\"tvt_prev\": \"önceki belgeyi göster$NKısayol: i\\\">⬆ önceki\",\n\t\"tvt_next\": \"sonraki belgeyi göster$NKısayol: K\\\">⬇ sonraki\",\n\t\"tvt_sel\": \"dosyayı seç$NKısayol: S\\\">seç\",\n\t\"tvt_j\": \"json güzelleştir$NKısayol: shift-J\\\">j\", //m\n\t\"tvt_edit\": \"dosyayı metin düzenleyicisinde aç$NKısayol: E\\\">✏️ düzenle\",\n\t\"tvt_tail\": \"dosyalardaki değişiklikleri izle; yeni satırları gerçek zamanlı göster\\\">📡 takip\",\n\t\"tvt_wrap\": \"kelime sarma\\\">↵\",\n\t\"tvt_atail\": \"sayfanın altına sabitle\\\">⚓\",\n\t\"tvt_ctail\": \"terminal renklerini çöz (ansi kaçış kodları)\\\">🌈\",\n\t\"tvt_ntail\": \"önbellek limiti (kaç byte yüklenmiş metin tutulacağı)\",\n\n\t\"m3u_add1\": \"şarkı m3u çalma listesine eklendi\",\n\t\"m3u_addn\": \"{0} şarkı m3u çalma listesine eklendi\",\n\t\"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\",\n\n\t\"gt_vau\": \"videoları gösterme, sadece sesi oynat\\\">🎧\",\n\t\"gt_msel\": \"dosya seçimlerini etkinleştir; bir dosyayı geçersiz kılmak için ctrl-tıkla$N$N&lt;em&gt;etkin olduğunda: bir dosyayı / klasörü açmak için çift tıkla&lt;/em&gt;$N$NKısayol: S\\\">çoklu seçim\",\n\t\"gt_crop\": \"küçültülmüş önizlemeleri ortala\\\">kırp\",\n\t\"gt_3x\": \"yüksek çözünürlüklü önizlemeler\\\">3x\",\n\t\"gt_zoom\": \"yakınlaştır\",\n\t\"gt_chop\": \"kes\",\n\t\"gt_sort\": \"sırala\",\n\t\"gt_name\": \"isim\",\n\t\"gt_sz\": \"boyut\",\n\t\"gt_ts\": \"tarih\",\n\t\"gt_ext\": \"tip\",\n\t\"gt_c1\": \"dosya adlarını daha fazla kısalt (daha az göster)\",\n\t\"gt_c2\": \"dosya adlarını daha az kısalt (daha fazla göster)\",\n\n\t\"sm_w8\": \"aranıyor...\",\n\t\"sm_prev\": \"aşağıdaki arama sonuçları önceki bir sorgudan alınmıştır:\\n  \",\n\t\"sl_close\": \"arama sonuçlarını kapat\",\n\t\"sl_hits\": \"gösterilen {0} sonuç\",\n\t\"sl_moar\": \"daha fazla yükle\",\n\n\t\"s_sz\": \"boyut\",\n\t\"s_dt\": \"tarih\",\n\t\"s_rd\": \"yol\",\n\t\"s_fn\": \"isim\",\n\t\"s_ta\": \"etiketler\",\n\t\"s_ua\": \"yükleme@\",\n\t\"s_ad\": \"gel.\",\n\t\"s_s1\": \"en az MiB\",\n\t\"s_s2\": \"en fazla MiB\",\n\t\"s_d1\": \"en az iso8601\",\n\t\"s_d2\": \"en fazla. iso8601\",\n\t\"s_u1\": \"yüklendi\",\n\t\"s_u2\": \"veya önce\",\n\t\"s_r1\": \"dosya yolu &nbsp içerir; (boşlukla ayrılmış)\",\n\t\"s_f1\": \"isim &nbsp içerir; (-nope kullan)\",\n\t\"s_t1\": \"etiketler &nbsp içerir; (^=baş, son=$)\",\n\t\"s_a1\": \"özel metadata özellikleri\",\n\n\t\"md_eshow\": \"görüntülenemiyor:  \",\n\t\"md_off\": \"[📜<em>beni-oku</em>], [⚙️] içinde kapalıdır -- belge gizli\",\n\n\t\"badreply\": \"Sunucudan gelen yanıt çözümlenemedi\",\n\n\t\"xhr403\": \"403: Erişim reddedildi\\n\\nF5'e basmayı deneyin, oturumunuz kapanmış olabilir\",\n\t\"xhr0\": \"bilinmiyor (muhtemelen sunucuya bağlantı kaybedildi veya sunucu çevrimdışı)\",\n\t\"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\",\n\t\"tl_xe1\": \"alt klasörler listelenemiyor:\\n\\nhata \",\n\t\"tl_xe2\": \"404: klasör bulunamadı\",\n\t\"fl_xe1\": \"dosyalar listelenemiyor:\\n\\nhata \",\n\t\"fl_xe2\": \"404: klasör bulunamadı\",\n\t\"fd_xe1\": \"alt klasör oluşturulamadı:\\n\\nhata \",\n\t\"fd_xe2\": \"404: Üst klasör bulunamadı\",\n\t\"fsm_xe1\": \"mesaj gönderilemedi:\\n\\nhata \",\n\t\"fsm_xe2\": \"404: Üst klasör bulunamadı\",\n\t\"fu_xe1\": \"sunucudan unpost(?) listesini yüklemek başarısız oldu:\\n\\nhata \",\n\t\"fu_xe2\": \"404: Dosya bulunamadı!!\",\n\n\t\"fz_tar\": \"sıkıştırılmamış gnu-tar dosyası (linux / mac)\",\n\t\"fz_pax\": \"sıkıştırılmamış pax-format tar (daha yavaş)\",\n\t\"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\",\n\t\"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\",\n\t\"fz_zip8\": \"utf8 dosya adları ile zip (windows 7 ve daha eski sürümlerde sorunlu olabilir)\",\n\t\"fz_zipd\": \"gerçekten eski yazılımlar için geleneksel cp437 dosya adları ile zip\",\n\t\"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)\",\n\n\t\"un_m1\": \"aşağıdan son yüklemelerinizi (veya tamamlanmamış olanları) silebilirsiniz\",\n\t\"un_upd\": \"yenile\",\n\t\"un_m4\": \"veya aşağıda görünen dosyaları paylaşın:\",\n\t\"un_ulist\": \"göster\",\n\t\"un_ucopy\": \"kopyala\",\n\t\"un_flt\": \"isteğe bağlı filtre:&nbsp; URL şunu içermelidir:\",\n\t\"un_fclr\": \"filtreyi temizle\",\n\t\"un_derr\": 'unpost-delete failed:\\n',\n\t\"un_f5\": 'bir şeyler bozuldu, lütfen yenilemeyi deneyin veya F5 tuşuna basın',\n\t\"un_uf5\": \"üzgünüm ama bu yükleme iptal edilmeden önce sayfayı yenilemeniz gerekiyor (F5 veya CTRL-R tuşlarına basarak)\",\n\t\"un_nou\": '<b>uyarı:</b> sunucu tamamlanmamış yüklemeleri göstermek için çok meşgul; birazdan \"yenile\" bağlantısına tıklayın',\n\t\"un_noc\": '<b>uyarı:</b> tamamen yüklenmiş dosyaların unpost edilmesi sunucu yapılandırmasında etkinleştirilmemiştir/izin verilmemiştir',\n\t\"un_max\": \"ilk 2000 dosya gösteriliyor (filtreyi kullan)\",\n\t\"un_avail\": \"son yüklemelerin {0} tanesi silinebilir<br />tamamlanmayanların {1} tanesi iptal edilebilir\",\n\t\"un_m2\": \"yükleme zamanına göre sıralandı; en son önce:\",\n\t\"un_no1\": \"Hayda! yeterince güncel yükleme yok\",\n\t\"un_no2\": \"Hayda! o filtreye uyan yeterince güncel yükleme yok\",\n\t\"un_next\": \"aşağıdaki {0} dosyayı sil\",\n\t\"un_abrt\": \"iptal\",\n\t\"un_del\": \"sil\",\n\t\"un_m3\": \"son yüklemelerinizi hazırlanıyor...\",\n\t\"un_busy\": \"{0} dosya siliniyor...\",\n\t\"un_clip\": \"{0} bağlantı panoya kopyalandı\",\n\n\t\"u_https1\": \"daha iyi performans\",\n\t\"u_https2\": \"için https'i\",\n\t\"u_https3\": \"kullanın\",\n\t\"u_ancient\": 'tarayıcınız resmen fosilleşmiş -- belki de <a href=\"#\" onclick=\"goto(\\'bup\\')\">bup kullanmalısınız</a>',\n\t\"u_nowork\": \"firefox 53+ veya chrome 57+ veya iOS 11+ gerekiyor\",\n\t\"tail_2old\": \"firefox 105+ veya chrome 71+ veya iOS 14.5+ gerekiyor\",\n\t\"u_nodrop\": 'tarayıcınız sürükleyip bırakmak için çok eski',\n\t\"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\",\n\t\"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\",\n\t\"u_enpot\": '<a href=\"#\">Patates arayüze</a> geçiş yap (yükleme hızını artırabilir)',\n\t\"u_depot\": '<a href=\"#\">Havalı arayüze</a> geçiş yap (yükleme hızını azaltabilir)',\n\t\"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!',\n\t\"u_pott\": \"<p>dosyalar: &nbsp; <b>{0}</b> tamamlandı, &nbsp; <b>{1}</b> başarısız, &nbsp; <b>{2}</b> meşgul, &nbsp; <b>{3}</b> sırada bekliyor</p>\",\n\t\"u_ever\": \"bu temel yükleyici; up2k en az<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1 gerektirir\",\n\t\"u_su2k\": 'bu temel yükleyici; <a href=\"#\" id=\"u2yea\">up2k</a> daha iyidir',\n\t\"u_uput\": 'hız için optimize et (kontrol toplamını atla)',\n\t\"u_ewrite\": 'bu klasöre yazma izniniz yok',\n\t\"u_eread\": 'bu klasörü okuma izniniz yok',\n\t\"u_enoi\": 'dosya arama sunucu yapılandırmasında etkin değil',\n\t\"u_enoow\": \"üstüne yazma burada çalışmayacak; Silme izni gerekiyor\",\n\t\"u_badf\": 'Bu {0} dosya (toplam {1} arasında) atlandı, muhtemelen dosya sistemi izinleri nedeniyle:\\n\\n',\n\t\"u_blankf\": 'Bu {0} dosya (toplam {1} arasında) boş / boş; yine de yüklemek ister misiniz?\\n\\n',\n\t\"u_applef\": 'Bu {0} dosya (toplam {1} arasında) muhtemelen istenmiyor;\\nAşağıdaki dosyaları ATLAMAK için <code>TAMAM/Enter</code> tuşuna basın,\\nyüklemek için <code>İptal/ESC</code> tuşuna basın\\n\\n',\n\t\"u_just1\": '\\nBelki sadece bir dosya seçerseniz daha iyi çalışır',\n\t\"u_ff_many\": \"eğer <b>Linux / MacOS / Android</b> kullanıyorsanız bu kadar çok dosya yüklemenin <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\"><em>Firefox'u</em> çökertme ihtimali var!</a>\\nEğer çökerse lütfen tekrar deneyin (veya Chrome kullanın).\",\n\t\"u_up_life\": \"Bu yükleme, tamamlandıktan {0} süre sonra sunucudan silinecek\\n\",\n\t\"u_asku\": 'bu {0} dosya <code>{1}</code> içerisine yükleniyor',\n\t\"u_unpt\": \"bu yüklemeyi sol üstteki 🧯 ile geri alabilir / silebilirsiniz\",\n\t\"u_bigtab\": '{0} dosya görüntülenmek üzere\\n\\nBu kadar fazlası tarayıcınızı çökertebilir, emin misiniz?',\n\t\"u_scan\": 'Dsyalar tarıyor...',\n\t\"u_dirstuck\": 'dizin yineleyicisi aşağıdaki {0} öğeye erişmeye çalışırken takıldı; atlanacak:',\n\t\"u_etadone\": 'Tamamlandı ({0}, {1} dosya)',\n\t\"u_etaprep\": '(yüklemeye hazırlanıyor)',\n\t\"u_hashdone\": 'hash\\'leme tamamlandı',\n\t\"u_hashing\": 'hash\\'leniyor',\n\t\"u_hs\": 'el sıkılıyor...',\n\t\"u_started\": \"dosyalar şu anda yükleniyor; [🚀]'i gör\",\n\t\"u_dupdefer\": \"başka bir dosyanın kopyası; diğer tüm dosyalardan sonra işlenecek\",\n\t\"u_actx\": \"diğer pencereler/sekmeler arasında geçiş yaparken <br /> performansın düşmemesi için bu yazıya tıklayın\",\n\t\"u_fixed\": \"Timam!&nbsp; Hallettim 👍\",\n\t\"u_cuerr\": \"parça {0} / {1} yüklenemedi;\\nmuhtemelen zararsız, devam ediliyor\\n\\ndosya: {2}\",\n\t\"u_cuerr2\": \"sunucu yüklemeyi reddetti (parça {0} / {1});\\nsonra tekrar denenecek\\n\\ndosya: {2}\\n\\nhata \",\n\t\"u_ehstmp\": \"tekrar denenecek; sağ alt köşeye mak\",\n\t\"u_ehsfin\": \"sunucu yüklemeyi tamamlama isteğini reddetti; tekrar deniyor...\",\n\t\"u_ehssrch\": \"sunucu arama yapma isteğini reddetti; tekrar deniyor...\",\n\t\"u_ehsinit\": \"sunucu yüklemeyi başlatma isteğini reddetti; tekrar deniyor...\",\n\t\"u_eneths\": \"yükleme el sıkışması sırasında ağ hatası; tekrar deniyor...\",\n\t\"u_enethd\": \"hedef varlığını test ederken ağ hatası; tekrar deniyor...\",\n\t\"u_cbusy\": \"ağ kesintisinden sonra sunucunun bize tekrar güvenmesini bekliyoruz...\",\n\t\"u_ehsdf\": \"sunucuda disk alanı kalmadı!\\n\\ndevam edebilmek için birinin\\nyeterli alan açmasını bekleyeceğiz\",\n\t\"u_emtleak1\": \"görünüşe göre web tarayıcınızda bir bellek sızıntısı var;\\nlütfen\",\n\t\"u_emtleak2\": ' <a href=\"{0}\">https\\'ye geçin (önerilir)</a> veya ',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": 'sırayla şunları deneyin:\\n<ul><li>sayfayı yenilemek için <code>F5</code>\\'e basın</li><li>sonra &nbsp;<code>mt</code>&nbsp; düğmesini &nbsp;<code>⚙️ ayarlar</code> menüsünde devre dışı bırakın</li><li>ve o yüklemeyi tekrar deneyin</li></ul>Yüklemeler biraz daha yavaş olacaktır, ama salla gitsin.\\nSorun için özür dileriz!\\n\\nDipnot: chrome v107 <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">için bir hata düzeltmesi</a>',\n\t\"u_emtleakf\": 'şunları deneyin:\\n<ul><li>sayfayı yenilemek için <code>F5</code>\\'e basın</li><li>sonra yükleme arayüzünde <code>🥔</code> (patates) seçeneğini etkinleştirin<li>ve yüklemeyi tekrar deneyin</li></ul>\\nDipnot: firefox <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">için muhtemel bir hata düzeltmesi</a>',\n\t\"u_s404\": \"sunucuda bulunamadı\",\n\t\"u_expl\": \"açıklama\",\n\t\"u_maxconn\": \"çoğu tarayıcı bunu 6 ile sınırlar, ancak firefox bunu <code>about:config</code> içinde <code>connections-per-server</code> ile artırmanıza izin verir\",\n\t\"u_tu\": '<p class=\"warn\">UYARI: turbo etkin, <span>&nbsp;istemci tamamlanmamış yüklemeleri algılayamayabilir ve devam ettiremeyebilir; turbo düğmesinden ipuçlarına bakabilirsiniz</span></p>',\n\t\"u_ts\": '<p class=\"warn\">UYARI: turbo etkin, <span>&nbsp;arama sonuçları yanlış olabilir; turbo düğmesi ipuçlarına bakın</span></p>',\n\t\"u_turbo_c\": \"turbo sunucu yapılandırmasında devre dışı bırakıldı\",\n\t\"u_turbo_g\": \"turbo devre dışı bırakılıyor çünkü bu alanda\\ndizin listeleme ayrıcalıklarınız yok\",\n\t\"u_life_cfg\": 'yüklemeleri <input id=\"lifem\" p=\"60\" /> dk (veya <input id=\"lifeh\" p=\"3600\" /> saat) sonra otomatik olarak sil',\n\t\"u_life_est\": 'yükleme <span id=\"lifew\" tt=\"local time\">---</span> sonra silinecek',\n\t\"u_life_max\": 'bu klasör bir\\nmaksimum ömür {0} uygular',\n\t\"u_unp_ok\": 'unpost {0} için izin verildi',\n\t\"u_unp_ng\": 'unpost izin verilmeyecek',\n\t\"ue_ro\": 'bu klasöre erişiminiz Salt Okuma\\n\\n',\n\t\"ue_nl\": 'şu anda oturum açmamışsınız',\n\t\"ue_la\": 'şu anda \"{0}\" olarak oturum açmışsınız',\n\t\"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',\n\t\"ue_ta\": 'tekrar yüklemeyi deneyin, artık çalışsana be',\n\t\"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.\",\n\t\"ur_1uo\": \"OK: Dosya başarıyla yüklendi\",\n\t\"ur_auo\": \"OK: {0} dosyanın tamamı başarıyla yüklendi\",\n\t\"ur_1so\": \"OK: Dosya sunucuda bulundu\",\n\t\"ur_aso\": \"OK: {0} dosyanın tamamı sunucuda bulundu\",\n\t\"ur_1un\": \"Yükleme başarısız oldu, üzgünüm\",\n\t\"ur_aun\": \"{0} yüklemenin tamamı başarısız oldu, üzgünüm\",\n\t\"ur_1sn\": \"Dosya sunucuda bulunamadı\",\n\t\"ur_asn\": \"{0} dosyas sunucuda bulunamadı\",\n\t\"ur_um\": \"Tamamlandı;\\n{0} yükleme başarılı,\\n{1} yükleme başarısız oldu, üzgünüm\",\n\t\"ur_sm\": \"Tamamlandı;\\n{0} dosya sunucuda bulundu,\\n{1} dosya sunucuda bulunamadı\",\n\n\t\"rc_opn\": \"aç\", //m\n\t\"rc_ply\": \"oynat\", //m\n\t\"rc_pla\": \"ses olarak oynat\", //m\n\t\"rc_txt\": \"dosya görüntüleyicide aç\", //m\n\t\"rc_md\": \"metin düzenleyicide aç\", //m\n\t\"rc_dl\": \"i̇ndir\", //m\n\t\"rc_zip\": \"arşiv olarak indir\", //m\n\t\"rc_cpl\": \"bağlantıyı kopyala\", //m\n\t\"rc_del\": \"sil\", //m\n\t\"rc_cut\": \"kes\", //m\n\t\"rc_cpy\": \"kopyala\", //m\n\t\"rc_pst\": \"yapıştır\", //m\n\t\"rc_rnm\": \"yeniden adlandır\", //m\n\t\"rc_nfo\": \"yeni klasör\", //m\n\t\"rc_nfi\": \"yeni dosya\", //m\n\t\"rc_sal\": \"tümünü seç\", //m\n\t\"rc_sin\": \"seçimi tersine çevir\", //m\n\t\"rc_shf\": \"bu klasörü paylaş\", //m\n\t\"rc_shs\": \"seçimi paylaş\", //m\n\n\t\"lang_set\": \"Değişikliklerin etki göstermesi için sayfa yenilensin mi?\",\n\n\t\"splash\": {\n\t\t\"a1\": \"yenile\",\n\t\t\"b1\": \"N'aber aga &nbsp; <small>(giriş yapmamışsın)</small>\",\n\t\t\"c1\": \"çıkış yap\",\n\t\t\"d1\": \"yığını yolla\",\n\t\t\"d2\": \"tüm aktif iş parçacıklarının durumunu gösterir\",\n\t\t\"e1\": \"cfg'yi yenile\",\n\t\t\"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\",\n\t\t\"f1\": \"göz atabilirsiniz:\",\n\t\t\"g1\": \"yükleyebilirsiniz:\",\n\t\t\"cc1\": \"diğer şeyler:\",\n\t\t\"h1\": \"k304'ü devre dışı bırak\",\n\t\t\"i1\": \"k304'ü etkinleştir\",\n\t\t\"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); <em>ama</em> bu, aynı zamanda genel olarak işleyişi yavaşlatır\",\n\t\t\"k1\": \"istemci ayarlarını sıfırla\",\n\t\t\"l1\": \"daha fazlası için giriş yap:\",\n\t\t\"m1\": \"hoş geldin,\",\n\t\t\"n1\": \"404 bulunamadı &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": 'ya da erişim iznin yok -- bir şifre dene veya <a href=\"' + SR + '/?h\">ana sayfaya dön</a>',\n\t\t\"p1\": \"403 yasaklandı &nbsp;~┻━┻\",\n\t\t\"q1\": 'bir şifre kullan veya <a href=\"' + SR + '/?h\">ana sayfaya dön</a>',\n\t\t\"r1\": \"ana sayfaya dön\",\n\t\t\".s1\": \"yeniden tara\",\n\t\t\"t1\": \"işlem\",\n\t\t\"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\",\n\t\t\"v1\": \"bağlan\",\n\t\t\"v2\": \"bu sunucuyu yerel HDD olarak kullan\",\n\t\t\"w1\": \"https'ye geç\",\n\t\t\"x1\": \"şifreyi değiştir\",\n\t\t\"y1\": \"paylaşılanları düzenle\",\n\t\t\"z1\": \"gizli paylaşımın kilidini aç:\",\n\t\t\"ta1\": \"ilk önce yeni şifreyi doldur\",\n\t\t\"ta2\": \"yeni şifreyi onaylamak için tekrar girin:\",\n\t\t\"ta3\": \"bir yazım hatası bulundu; lütfen tekrar deneyin\",\n\t\t\"nop\": \"HATA: Parola boş olamaz\", //m\n\t\t\"nou\": \"HATA: Kullanıcı adı ve/veya parola boş olamaz\", //m\n\t\t\"aa1\": \"gelen dosyalar:\",\n\t\t\"ab1\": \"no304'ü devre dışı bırak\",\n\t\t\"ac1\": \"no304'ü etkinleştir\",\n\t\t\"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!\",\n\t\t\"ae1\": \"aktif indirmeler:\",\n\t\t\"af1\": \"son yüklemeleri göster\",\n\t\t\"ag1\": \"bilinen IdP kullanıcılarını göster\", //m\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/ukr.js",
    "content": "\n// Рядки, що закінчуються на //m, є неперевіреними машинними перекладами\n\nLs.ukr = {\n\t\"tt\": \"Українська\",\n\n\t\"cols\": {\n\t\t\"c\": \"кнопки дій\",\n\t\t\"dur\": \"тривалість\",\n\t\t\"q\": \"якість / бітрейт\",\n\t\t\"Ac\": \"аудіо кодек\",\n\t\t\"Vc\": \"відео кодек\",\n\t\t\"Fmt\": \"формат / контейнер\",\n\t\t\"Ahash\": \"контрольна сума аудіо\",\n\t\t\"Vhash\": \"контрольна сума відео\",\n\t\t\"Res\": \"роздільність\",\n\t\t\"T\": \"тип файлу\",\n\t\t\"aq\": \"якість аудіо / бітрейт\",\n\t\t\"vq\": \"якість відео / бітрейт\",\n\t\t\"pixfmt\": \"підвибірка / структура пікселів\",\n\t\t\"resw\": \"горизонтальна роздільність\",\n\t\t\"resh\": \"вертикальна роздільність\",\n\t\t\"chs\": \"аудіо канали\",\n\t\t\"hz\": \"частота дискретизації\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"різне\",\n\t\t\t[\"ESC\", \"закрити різні речі\"],\n\n\t\t\t\"файловий менеджер\",\n\t\t\t[\"G\", \"перемкнути список / сітку\"],\n\t\t\t[\"T\", \"перемкнути мініатюри / іконки\"],\n\t\t\t[\"⇧ A/D\", \"розмір мініатюр\"],\n\t\t\t[\"ctrl-K\", \"видалити вибране\"],\n\t\t\t[\"ctrl-X\", \"вирізати до буфера\"],\n\t\t\t[\"ctrl-C\", \"копіювати до буфера\"],\n\t\t\t[\"ctrl-V\", \"вставити (перемістити/копіювати) сюди\"],\n\t\t\t[\"Y\", \"завантажити вибране\"],\n\t\t\t[\"F2\", \"перейменувати вибране\"],\n\n\t\t\t\"вибір файлів у списку\",\n\t\t\t[\"space\", \"перемкнути вибір файлу\"],\n\t\t\t[\"↑/↓\", \"перемістити курсор вибору\"],\n\t\t\t[\"ctrl ↑/↓\", \"перемістити курсор і вікно\"],\n\t\t\t[\"⇧ ↑/↓\", \"вибрати попередній/наступний файл\"],\n\t\t\t[\"ctrl-A\", \"вибрати всі файли / папки\"],\n\t\t], [\n\t\t\t\"навігація\",\n\t\t\t[\"B\", \"перемкнути хлібні крихти / панель навігації\"],\n\t\t\t[\"I/K\", \"попередня/наступна папка\"],\n\t\t\t[\"M\", \"батьківська папка (або згорнути поточну)\"],\n\t\t\t[\"V\", \"перемкнути папки / текстові файли в панелі навігації\"],\n\t\t\t[\"A/D\", \"розмір панелі навігації\"],\n\t\t], [\n\t\t\t\"аудіо плеєр\",\n\t\t\t[\"J/L\", \"попередня/наступна пісня\"],\n\t\t\t[\"U/O\", \"перемотати на 10сек назад/вперед\"],\n\t\t\t[\"0..9\", \"перейти до 0%..90%\"],\n\t\t\t[\"P\", \"відтворити/пауза (також запускає)\"],\n\t\t\t[\"S\", \"вибрати поточну пісню\"],\n\t\t\t[\"Y\", \"завантажити пісню\"],\n\t\t], [\n\t\t\t\"переглядач зображень\",\n\t\t\t[\"J/L, ←/→\", \"попереднє/наступне зображення\"],\n\t\t\t[\"Home/End\", \"перше/останнє зображення\"],\n\t\t\t[\"F\", \"повний екран\"],\n\t\t\t[\"R\", \"повернути за годинниковою стрілкою\"],\n\t\t\t[\"⇧ R\", \"повернути проти годинникової стрілки\"],\n\t\t\t[\"S\", \"вибрати зображення\"],\n\t\t\t[\"Y\", \"завантажити зображення\"],\n\t\t], [\n\t\t\t\"відео плеєр\",\n\t\t\t[\"U/O\", \"перемотати на 10сек назад/вперед\"],\n\t\t\t[\"P/K/Space\", \"відтворити/пауза\"],\n\t\t\t[\"C\", \"продовжити відтворення наступного\"],\n\t\t\t[\"V\", \"повтор\"],\n\t\t\t[\"M\", \"вимкнути звук\"],\n\t\t\t[\"[ and ]\", \"встановити інтервал повтору\"],\n\t\t], [\n\t\t\t\"переглядач текстових файлів\",\n\t\t\t[\"I/K\", \"попередній/наступний файл\"],\n\t\t\t[\"M\", \"закрити текстовий файл\"],\n\t\t\t[\"E\", \"редагувати текстовий файл\"],\n\t\t\t[\"S\", \"вибрати файл (для вирізання/копіювання/перейменування)\"],\n\t\t\t[\"Y\", \"завантажити текстовий файл\"], //m\n\t\t\t[\"⇧ J\", \"прикрасити json\"], //m\n\t\t]\n\t],\n\n\t\"m_ok\": \"Гаразд\",\n\t\"m_ng\": \"Скасувати\",\n\n\t\"enable\": \"Увімкнути\",\n\t\"danger\": \"НЕБЕЗПЕКА\",\n\t\"clipped\": \"скопійовано до буфера обміну\",\n\n\t\"ht_s1\": \"секунда\",\n\t\"ht_s2\": \"секунд\",\n\t\"ht_m1\": \"хвилина\",\n\t\"ht_m2\": \"хвилин\",\n\t\"ht_h1\": \"година\",\n\t\"ht_h2\": \"годин\",\n\t\"ht_d1\": \"день\",\n\t\"ht_d2\": \"днів\",\n\t\"ht_and\": \" і \",\n\n\t\"goh\": \"панель керування\",\n\t\"gop\": 'попередній сусід\">назад',\n\t\"gou\": 'батьківська папка\">вгору',\n\t\"gon\": 'наступна папка\">далі',\n\t\"logout\": \"Вийти \",\n\t\"login\": \"увійти\", //m\n\t\"access\": \" доступ\",\n\t\"ot_close\": \"закрити підменю\",\n\t\"ot_search\": \"`пошук файлів за атрибутами, шляхом / іменем, музичними тегами, або будь-якою комбінацією$N$N`foo bar` = має містити «foo» і «bar»,$N`foo -bar` = має містити «foo», але не «bar»,$N`^yana .opus$` = починатися з «yana» і бути файлом «opus»$N`&quot;try unite&quot;` = містити точно «try unite»$N$Nформат дати - iso-8601, наприклад$N`2009-12-31` або `2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"скасувати: видалити недавні завантаження або перервати незавершені\",\n\t\"ot_bup\": \"bup: основний завантажувач, підтримує навіть netscape 4.0\",\n\t\"ot_mkdir\": \"mkdir: створити нову папку\",\n\t\"ot_md\": \"new-file: створити новий текстовий файл\", //m\n\t\"ot_msg\": \"msg: надіслати повідомлення в лог сервера\",\n\t\"ot_mp\": \"налаштування медіаплеєра\",\n\t\"ot_cfg\": \"параметри конфігурації\",\n\t\"ot_u2i\": 'up2k: завантажити файли (якщо у вас є доступ для запису) або переключитися на режим пошуку, щоб побачити, чи існують вони десь на сервері$N$Nзавантаження можна поновлювати, багатопотокові, і часові мітки файлів зберігаються, але використовує більше CPU ніж [🎈]&nbsp; (основний завантажувач)<br /><br />під час завантаження ця іконка стає індикатором прогресу!',\n\t\"ot_u2w\": 'up2k: завантажити файли з підтримкою поновлення (закрийте браузер і перетягніть ті самі файли пізніше)$N$Nбагатопотокові, і часові мітки файлів зберігаються, але використовує більше CPU ніж [🎈]&nbsp; (основний завантажувач)<br /><br />під час завантаження ця іконка стає індикатором прогресу!',\n\t\"ot_noie\": 'Будь ласка, використовуйте Chrome / Firefox / Edge',\n\n\t\"ab_mkdir\": \"створити папку\",\n\t\"ab_mkdoc\": \"новий текстовий файл\", //m\n\t\"ab_msg\": \"надіслати повідомлення в лог сервера\",\n\n\t\"ay_path\": \"перейти до папок\",\n\t\"ay_files\": \"перейти до файлів\",\n\n\t\"wt_ren\": \"перейменувати вибрані елементи$NГаряча клавіша: F2\",\n\t\"wt_del\": \"видалити вибрані елементи$NГаряча клавіша: ctrl-K\",\n\t\"wt_cut\": \"вирізати вибрані елементи &lt;small&gt;(потім вставити в іншому місці)&lt;/small&gt;$NГаряча клавіша: ctrl-X\",\n\t\"wt_cpy\": \"копіювати вибрані елементи до буфера$N(щоб вставити їх в іншому місці)$NГаряча клавіша: ctrl-C\",\n\t\"wt_pst\": \"вставити раніше вирізане / скопійоване$NГаряча клавіша: ctrl-V\",\n\t\"wt_selall\": \"вибрати всі файли$NГаряча клавіша: ctrl-A (коли фокус на файлі)\",\n\t\"wt_selinv\": \"інвертувати вибір\",\n\t\"wt_zip1\": \"завантажити цю папку як архів\",\n\t\"wt_selzip\": \"завантажити вибір як архів\",\n\t\"wt_seldl\": \"завантажити вибір як окремі файли$NГаряча клавіша: Y\",\n\t\"wt_npirc\": \"копіювати інформацію треку у форматі irc\",\n\t\"wt_nptxt\": \"копіювати інформацію треку у текстовому форматі\",\n\t\"wt_m3ua\": \"додати до m3u плейлисту (потім клацніть <code>📻копіювати</code>)\",\n\t\"wt_m3uc\": \"копіювати m3u плейлист до буфера\",\n\t\"wt_grid\": \"перемкнути сітку / список$NГаряча клавіша: G\",\n\t\"wt_prev\": \"попередній трек$NГаряча клавіша: J\",\n\t\"wt_play\": \"відтворити / пауза$NГаряча клавіша: P\",\n\t\"wt_next\": \"наступний трек$NГаряча клавіша: L\",\n\n\t\"ul_par\": \"паралельні завантаження:\",\n\t\"ut_rand\": \"випадкові імена файлів\",\n\t\"ut_u2ts\": \"копіювати часову мітку останньої зміни$Nз вашої файлової системи на сервер\\\">📅\",\n\t\"ut_ow\": \"перезаписати існуючі файли на сервері?$N🛡️: ніколи (замість цього створить нове ім'я файлу)$N🕒: перезаписати, якщо файл на сервері старіший за ваш$N♻️: завжди перезаписувати, якщо файли відрізняються$N⏭️: безумовно пропускати всі наявні файли\", //m\n\t\"ut_mt\": \"продовжувати хешування інших файлів під час завантаження$N$Nможливо, вимкніть, якщо ваш CPU або HDD є вузьким місцем\",\n\t\"ut_ask\": 'запитати підтвердження перед початком завантаження\">💭',\n\t\"ut_pot\": \"покращити швидкість завантаження на повільних пристроях$Nроблячи інтерфейс менш складним\",\n\t\"ut_srch\": \"не завантажувати, а перевірити, чи файли вже $N існують на сервері (сканує всі папки, які ви можете читати)\",\n\t\"ut_par\": \"призупинити завантаження, встановивши 0$N$Nзбільшіть, якщо ваше з'єднання повільне / висока затримка$N$Nзалишіть 1 в локальній мережі або якщо HDD сервера є вузьким місцем\",\n\t\"ul_btn\": \"перетягніть файли / папки<br> (або клацніть сюди)\",\n\t\"ul_btnu\": \"ЗАВАНТАЖИТИ\",\n\t\"ul_btns\": \"П О Ш У К\",\n\n\t\"ul_hash\": \"хеш\",\n\t\"ul_send\": \"надіслати\",\n\t\"ul_done\": \"готово\",\n\t\"ul_idle1\": \"завантаження ще не поставлені в чергу\",\n\t\"ut_etah\": \"середня швидкість &lt;em&gt;хешування&lt;/em&gt; і орієнтовний час до завершення\",\n\t\"ut_etau\": \"середня швидкість &lt;em&gt;завантаження&lt;/em&gt; і орієнтовний час до завершення\",\n\t\"ut_etat\": \"середня &lt;em&gt;загальна&lt;/em&gt; швидкість і орієнтовний час до завершення\",\n\n\t\"uct_ok\": \"успішно завершено\",\n\t\"uct_ng\": \"невдало: помилка / відхилено / не знайдено\",\n\t\"uct_done\": \"ok і ng разом\",\n\t\"uct_bz\": \"хешування або завантаження\",\n\t\"uct_q\": \"очікує, в черзі\",\n\n\t\"utl_name\": \"ім'я файлу\",\n\t\"utl_ulist\": \"список\",\n\t\"utl_ucopy\": \"копіювати\",\n\t\"utl_links\": \"посилання\",\n\t\"utl_stat\": \"статус\",\n\t\"utl_prog\": \"прогрес\",\n\n\t// keep short:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"ПОМИЛКА\",\n\t\"utl_oserr\": \"помилка ОС\",\n\t\"utl_found\": \"знайдено\",\n\t\"utl_defer\": \"відкласти\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"готово\",\n\n\t\"ul_flagblk\": \"файли були додані до черги</b><br>однак є зайнятий up2k в іншій вкладці браузера,<br>тому чекаємо, поки він завершиться спочатку\",\n\t\"ul_btnlk\": \"конфігурація сервера заблокувала цей перемикач у цьому стані\",\n\n\t\"udt_up\": \"Завантаження\",\n\t\"udt_srch\": \"Пошук\",\n\t\"udt_drop\": \"перетягніть сюди\",\n\n\t\"u_nav_m\": '<h6>гаразд, що у вас є?</h6><code>Enter</code> = Файли (один або більше)\\n<code>ESC</code> = Одна папка (включаючи підпапки)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Файли</a><a href=\"#\" id=\"modal-ng\">Одна папка</a>',\n\n\t\"cl_opts\": \"перемикачі\",\n\t\"cl_hfsz\": \"розмір файлу\", //m\n\t\"cl_themes\": \"тема\",\n\t\"cl_langs\": \"мова\",\n\t\"cl_ziptype\": \"завантаження папки\",\n\t\"cl_uopts\": \"перемикачі up2k\",\n\t\"cl_favico\": \"favicon\",\n\t\"cl_bigdir\": \"великі папки\",\n\t\"cl_hsort\": \"#сортування\",\n\t\"cl_keytype\": \"позначення клавіш\",\n\t\"cl_hiddenc\": \"приховані стовпці\",\n\t\"cl_hidec\": \"приховати\",\n\t\"cl_reset\": \"скинути\",\n\t\"cl_hpick\": \"натисніть на заголовки стовпців, щоб приховати їх у таблиці нижче\",\n\t\"cl_hcancel\": \"приховання стовпців скасовано\",\n\t\"cl_rcm\": \"контекстне меню\", //m\n\n\t\"ct_grid\": '田 сітка',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ підказки',\n\t\"ct_thumb\": 'у режимі сітки, перемкнути іконки або мініатюри$NГаряча клавіша: T\">🖼️ мініатюри',\n\t\"ct_csel\": 'використовувати CTRL і SHIFT для вибору файлів у режимі сітки\">вибір',\n\t\"ct_dsel\": 'використовувати вибір перетягуванням у режимі сітки\">перетягнути', //m\n\t\"ct_dl\": 'примусове завантаження (не показувати вбудовано) під час натискання на файл\">dl', //m\n\t\"ct_ihop\": 'коли переглядач зображень закрито, прокрутити вниз до останнього переглянутого файлу\">g⮯',\n\t\"ct_dots\": 'показати приховані файли (якщо сервер дозволяє)\">приховані файли',\n\t\"ct_qdel\": 'при видаленні файлів, запитати підтвердження лише один раз\">швидке видалення',\n\t\"ct_dir1st\": 'сортувати папки перед файлами\">спочатку 📁',\n\t\"ct_nsort\": 'природне сортування (для імен файлів з початковими цифрами)\">природне сортування',\n\t\"ct_utc\": 'використовуйте UTC для всіх часових позначень\">UTC', //m\n\t\"ct_readme\": 'показати README.md у списках папок\">📜 readme',\n\t\"ct_idxh\": 'показати index.html замість списку папки\">htm',\n\t\"ct_sbars\": 'показати смуги прокрутки\">⟊',\n\n\t\"cut_umod\": \"якщо файл вже існує на сервері, оновити часову мітку останньої зміни сервера відповідно до вашого локального файлу (потребує дозволів на запис+видалення)\\\">re📅\",\n\n\t\"cut_turbo\": \"кнопка yolo, ви, ймовірно, НЕ хочете її вмикати:$N$Nвикористовуйте це, якщо ви завантажували величезну кількість файлів і змушені були перезапустити з якоїсь причини, і хочете продовжити завантаження якнайшвидше$N$Nце замінює перевірку хешу простою <em>&quot;чи має цей файл той самий розмір на сервері?&quot;</em>, тому якщо вміст файлу відрізняється, він НЕ буде завантажений$N$Nви повинні вимкнути це, коли завантаження буде завершено, а потім &quot;завантажити&quot; ті самі файли знову, щоб дозволити клієнту перевірити їх\\\">turbo\",\n\n\t\"cut_datechk\": \"не має ефекту, якщо кнопка turbo не ввімкнена$N$Nзменшує yolo фактор на крихту; перевіряє, чи відповідають часові мітки файлів на сервері вашим$N$N<em>теоретично</em>, повинно зловити більшість незавершених / пошкоджених завантажень, але не є заміною виконання перевірки з вимкненим turbo потім\\\">date-chk\",\n\n\t\"cut_u2sz\": \"розмір (у MiB) кожного фрагмента завантаження; великі значення краще летять через атлантичний океан. Спробуйте низькі значення на дуже ненадійних з'єднаннях\",\n\n\t\"cut_flag\": \"переконатися, що лише одна вкладка завантажує одночасно $N -- інші вкладки також повинні мати це ввімкнене $N -- впливає лише на вкладки на тому самому домені\",\n\n\t\"cut_az\": \"завантажувати файли в алфавітному порядку, а не від найменшого файлу$N$Nалфавітний порядок може полегшити огляд, якщо щось пішло не так на сервері, але робить завантаження трохи повільнішим на fiber / LAN\",\n\n\t\"cut_nag\": \"сповіщення ОС після завершення завантаження$N(тільки якщо браузер або вкладка не активні)\",\n\t\"cut_sfx\": \"звуковий сигнал після завершення завантаження$N(тільки якщо браузер або вкладка не активні)\",\n\n\t\"cut_mt\": \"використовувати багатопотоковість для прискорення хешування файлів$N$Nце використовує веб-воркери і потребує$Nбільше пам'яті (до 512 MiB додатково)$N$Nробить https на 30% швидше, http у 4.5 рази швидше\\\">mt\",\n\n\t\"cut_wasm\": \"використовувати wasm замість вбудованого хешера браузера; покращує швидкість у браузерах на базі chrome, але збільшує навантаження CPU, плюс багато старих версій chrome мають баги, які змушують браузер споживати всю пам'ять і вилітати, якщо ця опція ввімкненаа\\\">wasm\",\n\n\t\"cft_text\": \"текст favicon (порожній і оновити для відключення)\",\n\t\"cft_fg\": \"колір переднього плану\",\n\t\"cft_bg\": \"колір фону\",\n\n\t\"cdt_lim\": \"максимальна кількість файлів для показу в папці\",\n\t\"cdt_ask\": \"при прокрутці до низу,$Nзамість завантаження більше файлів,$Nзапитати, що робити\",\n\t\"cdt_hsort\": \"`скільки правил сортування (`,sorthref`) включати в медіа-URL. Встановлення цього в 0 також буде ігнорувати правила сортування, включені в медіа посилання при їх натисканні\",\n\t\"cdt_ren\": \"увімкнути користувацьке контекстне меню, звичайне меню доступне при натисканні shift і правої кнопки миші\\\">увімкнути\", //m\n\t\"cdt_rdb\": \"показувати звичайне меню правої кнопки, якщо власне меню вже відкрите і відбувається повторне клацання\\\">x2\", //m\n\n\t\"tt_entree\": \"показати панель навігації (бічна панель дерева каталогів)$NГаряча клавіша: B\",\n\t\"tt_detree\": \"показати хлібні крихти$NГаряча клавіша: B\",\n\t\"tt_visdir\": \"прокрутити до вибраної папки\",\n\t\"tt_ftree\": \"перемкнути дерево папок / текстові файли$NГаряча клавіша: V\",\n\t\"tt_pdock\": \"показати батьківські папки в закріпленій панелі зверху\",\n\t\"tt_dynt\": \"автоматично збільшуватися при розширенні дерева\",\n\t\"tt_wrap\": \"перенесення слів\",\n\t\"tt_hover\": \"показувати переповнені рядки при наведенні$N( порушує прокрутку, якщо курсор $N&nbsp; миші не знаходиться в лівому відступі )\",\n\n\t\"ml_pmode\": \"в кінці папки...\",\n\t\"ml_btns\": \"команди\",\n\t\"ml_tcode\": \"транскодувати\",\n\t\"ml_tcode2\": \"транскодувати в\",\n\t\"ml_tint\": \"відтінок\",\n\t\"ml_eq\": \"аудіо еквалайзер\",\n\t\"ml_drc\": \"компресор динамічного діапазону\",\n\t\"ml_ss\": \"пропускати тишу\", //m\n\n\t\"mt_loop\": \"зациклити/повторити одну пісню\\\">🔁\",\n\t\"mt_one\": \"зупинити після однієї пісні\\\">1️⃣\",\n\t\"mt_shuf\": \"перемішати пісні в кожній папці\\\">🔀\",\n\t\"mt_aplay\": \"автовідтворення, якщо є ID пісні в посиланні, по якому ви клацнули для доступу до сервера$N$Nвідключення цього також зупинить оновлення URL сторінки з ID пісень під час відтворення музики, щоб запобігти автовідтворенню, якщо ці налаштування втрачені, але URL залишається\\\">a▶\",\n\t\"mt_preload\": \"почати завантаження наступної пісні ближче до кінця для безперервного відтворення\\\">preload\",\n\t\"mt_prescan\": \"перейти до наступної папки перед тим, як остання пісня$Nзакінчиться, підтримуючи веб-браузер у робочому стані$Nщоб він не зупинив відтворення\\\">nav\",\n\t\"mt_fullpre\": \"спробувати попередньо завантажити всю пісню;$N✅ увімкніть на <b>ненадійних</b> з'єднаннях,$N❌ <b>вимкніть</b> на повільних з'єднаннях, ймовірно\\\">full\",\n\t\"mt_fau\": \"на телефонах, запобігти зупинці музики, якщо наступна пісня не завантажується достатньо швидко (може зробити відображення тегів глючним)\\\">☕️\",\n\t\"mt_waves\": \"смуга хвильової форми:$Nпоказати амплітуду аудіо в повзунку\\\">~s\",\n\t\"mt_npclip\": \"показати кнопки для копіювання до буфера поточної пісні, що відтворюється\\\">/np\",\n\t\"mt_m3u_c\": \"показати кнопки для копіювання до буфера$Nвибраних пісень як записи плейлисту m3u8\\\">📻\",\n\t\"mt_octl\": \"інтеграція з ОС (медіа гарячі клавіші / osd)\\\">os-ctl\",\n\t\"mt_oseek\": \"дозволити перемотування через інтеграцію з ОС$N$Nзауваження: на деяких пристроях (iPhone),$Nце замінює кнопку наступної пісні\\\">seek\",\n\t\"mt_oscv\": \"показати обкладинку альбому в osd\\\">art\",\n\t\"mt_follow\": \"тримати трек, що відтворюється, у полі зору\\\">🎯\",\n\t\"mt_compact\": \"компактні елементи керування\\\">⟎\",\n\t\"mt_uncache\": \"очистити кеш &nbsp;(спробуйте це, якщо ваш браузер закешував$Nпошкоджену копію пісні, тому відмовляється її відтворювати)\\\">uncache\",\n\t\"mt_mloop\": \"зациклити відкриту папку\\\">🔁 loop\",\n\t\"mt_mnext\": \"завантажити наступну папку і продовжити\\\">📂 next\",\n\t\"mt_mstop\": \"зупинити відтворення\\\">⏸ stop\",\n\t\"mt_cflac\": \"конвертувати flac / wav в {0}\\\">flac\",\n\t\"mt_caac\": \"конвертувати aac / m4a в {0}\\\">aac\",\n\t\"mt_coth\": \"конвертувати всі інші (не mp3) в {0}\\\">oth\",\n\t\"mt_c2opus\": \"найкращий вибір для робочих столів, ноутбуків, android\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, для iOS 17.5 і новіших\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, для iOS 11 до 17\\\">caf\",\n\t\"mt_c2mp3\": \"використовуйте це на дуже старих пристроях\\\">mp3\",\n\t\"mt_c2flac\": \"найкраща якість звуку, але великі завантаження\\\">flac\", //m\n\t\"mt_c2wav\": \"відтворення без стиснення (ще більше)\\\">wav\", //m\n\t\"mt_c2ok\": \"гарно, хороший вибір\",\n\t\"mt_c2nd\": \"це не рекомендований вихідний формат для вашого пристрою, але це нормально\",\n\t\"mt_c2ng\": \"ваш пристрій, здається, не підтримує цей вихідний формат, але давайте все одно спробуємо\",\n\t\"mt_xowa\": \"є баги в iOS, які запобігають фоновому відтворенню з використанням цього формату; будь ласка, використовуйте caf або mp3 замість цього\",\n\t\"mt_tint\": \"рівень фону (0-100) на смузі перемотування$Nщоб зробити буферизацію менш відвертаючою\",\n\t\"mt_eq\": \"`вмикає еквалайзер і контроль посилення;$N$Nпосилення `0` = стандартна 100% гучність (немодифікована)$N$Nширина `1 &nbsp;` = стандартне стерео (немодифіковане)$Nширина `0.5` = 50% перехресне живлення ліво-право$Nширина `0 &nbsp;` = моно$N$Nпосилення `-0.8` &amp; ширина `10` = видалення вокалу :^)$N$Nвключення еквалайзера робить безшовні альбоми повністю безшовними, тому залишайте його увімкненим з усіма значеннями в нулі (окрім ширини = 1), якщо вам це важливо\",\n\t\"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(дивіться вікіпедію, вони пояснюють це набагато краще)\",\n\t\"mt_ss\": \"`вмикає пропуск тиші; множить швидкість на `шв` біля початку/кінця, коли гучність нижче `гуч` і позиція в перших `поч`% або останніх `кін`%\", //m\n\t\"mt_ssvt\": \"поріг гучності (0-255)\\\">гуч\", //m\n\t\"mt_ssts\": \"активний поріг (% треку, початок)\\\">поч\", //m\n\t\"mt_sste\": \"активний поріг (% треку, кінець)\\\">кін\", //m\n\t\"mt_sssm\": \"множник швидкості відтворення\\\">шв\", //m\n\n\t\"mb_play\": \"відтворити\",\n\t\"mm_hashplay\": \"відтворити цей аудіо файл?\",\n\t\"mm_m3u\": \"натисніть <code>Enter/Гаразд</code> для відтворення\\nнатисніть <code>ESC/Скасувати</code> для редагування\",\n\t\"mp_breq\": \"потрібен firefox 82+ або chrome 73+ або iOS 15+\",\n\t\"mm_bload\": \"зараз завантажується...\",\n\t\"mm_bconv\": \"конвертується в {0}, будь ласка, зачекайте...\",\n\t\"mm_opusen\": \"ваш браузер не може відтворювати aac / m4a файли;\\nтранскодування в opus тепер увімкнено\",\n\t\"mm_playerr\": \"відтворення невдале: \",\n\t\"mm_eabrt\": \"Спроба відтворення була скасована\",\n\t\"mm_enet\": \"Ваше інтернет-з'єднання нестабільне\",\n\t\"mm_edec\": \"Цей файл нібито пошкоджений??\",\n\t\"mm_esupp\": \"Ваш браузер не розуміє цей аудіо формат\",\n\t\"mm_eunk\": \"Невідома помилка\",\n\t\"mm_e404\": \"Не вдалося відтворити аудіо; помилка 404: Файл не знайдено.\",\n\t\"mm_e403\": \"Не вдалося відтворити аудіо; помилка 403: Доступ заборонено.\\n\\nСпробуйте натиснути F5 для перезавантаження, можливо, ви вийшли з системи\",\n\t\"mm_e415\": \"Не вдалося відтворити аудіо; помилка 415: Не вдалося перетворити файл; перевірте логи сервера.\", //m\n\t\"mm_e500\": \"Не вдалося відтворити аудіо; помилка 500: Перевірте логи сервера.\",\n\t\"mm_e5xx\": \"Не вдалося відтворити аудіо; помилка сервера \",\n\t\"mm_nof\": \"не знаходжу більше аудіо файлів поблизу\",\n\t\"mm_prescan\": \"Шукаю музику для наступного відтворення...\",\n\t\"mm_scank\": \"Знайшов наступну пісню:\",\n\t\"mm_uncache\": \"кеш очищено; всі пісні будуть перезавантажені при наступному відтворенні\",\n\t\"mm_hnf\": \"ця пісня більше не існує\",\n\n\t\"im_hnf\": \"це зображення більше не існує\",\n\n\t\"f_empty\": 'ця папка порожня',\n\t\"f_chide\": 'це приховає стовпець «{0}»\\n\\nви можете показати стовпці в вкладці налаштувань',\n\t\"f_bigtxt\": \"цей файл розміром {0} MiB -- дійсно переглядати як текст?\",\n\t\"f_bigtxt2\": \"переглянути лише кінець файлу замість цього? це також увімкне відслідковування/tailing, показуючи новододані рядки тексту в реальному часі\",\n\t\"fbd_more\": '<div id=\"blazy\">показано <code>{0}</code> з <code>{1}</code> файлів; <a href=\"#\" id=\"bd_more\">показати {2}</a> або <a href=\"#\" id=\"bd_all\">показати всі</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\">показано <code>{0}</code> з <code>{1}</code> файлів; <a href=\"#\" id=\"bd_all\">показати всі</a></div>',\n\t\"f_anota\": \"лише {0} з {1} елементів було вибрано;\\nщоб вибрати всю папку, спочатку прокрутіть до низу\",\n\n\t\"f_dls\": 'посилання на файли в поточній папці були\\nзмінені на посилання для завантаження',\n\t\"f_dl_nd\": 'пропуск папки (скористайтеся завантаженням zip/tar замість цього):\\n', //m\n\n\t\"f_partial\": \"Щоб безпечно завантажити файл, який зараз завантажується, будь ласка, клацніть на файл, який має таке саме ім'я, але без розширення <code>.PARTIAL</code>. Будь ласка, натисніть Скасувати або Escape, щоб зробити це.\\n\\nНатиснення Гаразд / Enter проігнорує це попередження і продовжить завантаження <code>.PARTIAL</code> робочого файлу замість цього, що майже напевно дасть вам пошкоджені дані.\",\n\n\t\"ft_paste\": \"вставити {0} елементів$NГаряча клавіша: ctrl-V\",\n\t\"fr_eperm\": 'не можу перейменувати:\\nу вас немає дозволу “переміщення“ в цій папці',\n\t\"fd_eperm\": 'не можу видалити:\\nу вас немає дозволу “видалення“ в цій папці',\n\t\"fc_eperm\": 'не можу вирізати:\\nу вас немає дозволу “переміщення“ в цій папці',\n\t\"fp_eperm\": 'не можу вставити:\\nу вас немає дозволу “запису“ в цій папці',\n\t\"fr_emore\": \"виберіть принаймні один елемент для перейменування\",\n\t\"fd_emore\": \"виберіть принаймні один елемент для видалення\",\n\t\"fc_emore\": \"виберіть принаймні один елемент для вирізання\",\n\t\"fcp_emore\": \"виберіть принаймні один елемент для копіювання до буфера\",\n\n\t\"fs_sc\": \"поділитися папкою, в якій ви знаходитесь\",\n\t\"fs_ss\": \"поділитися вибраними файлами\",\n\t\"fs_just1d\": \"ви не можете вибрати більше однієї папки,\\nабо змішувати файли і папки в одному виборі\",\n\t\"fs_abrt\": \"❌ скасувати\",\n\t\"fs_rand\": \"🎲 випадк.ім'я\",\n\t\"fs_go\": \"✅ створити спільний доступ\",\n\t\"fs_name\": \"ім'я\",\n\t\"fs_src\": \"джерело\",\n\t\"fs_pwd\": \"пароль\",\n\t\"fs_exp\": \"термін дії\",\n\t\"fs_tmin\": \"хв\",\n\t\"fs_thrs\": \"годин\",\n\t\"fs_tdays\": \"днів\",\n\t\"fs_never\": \"вічний\",\n\t\"fs_pname\": \"необов'язкове ім'я посилання; буде випадковим, якщо порожнє\",\n\t\"fs_tsrc\": \"файл або папка для спільного доступу\",\n\t\"fs_ppwd\": \"необов'язковий пароль\",\n\t\"fs_w8\": \"створення спільного доступу...\",\n\t\"fs_ok\": \"натисніть <code>Enter/Гаразд</code> для копіювання до буфера\\nнатисніть <code>ESC/Скасувати</code> для закриття\",\n\n\t\"frt_dec\": \"може виправити деякі випадки пошкоджених імен файлів\\\">url-decode\",\n\t\"frt_rst\": \"скинути змінені імена файлів назад до оригінальних\\\">↺ reset\",\n\t\"frt_abrt\": \"перервати і закрити це вікно\\\">❌ cancel\",\n\t\"frb_apply\": \"ЗАСТОСУВАТИ ПЕРЕЙМЕНУВАННЯ\",\n\t\"fr_adv\": \"пакетне / метадані / шаблонне перейменування\\\">розширене\",\n\t\"fr_case\": \"регулярний вираз з урахуванням регістру\\\">регістр\",\n\t\"fr_win\": \"безпечні для windows імена; замінити <code>&lt;&gt;:&quot;\\\\|?*</code> на японські повноширинні символи\\\">win\",\n\t\"fr_slash\": \"замінити <code>/</code> на символ, який не призводить до створення нових папок\\\">без /\",\n\t\"fr_re\": \"`шаблон пошуку регулярного виразу для застосування до оригінальних імен файлів; групи захоплення можна посилатися в полі формату нижче як `(1)` і `(2)` і так далі\",\n\t\"fr_fmt\": \"`натхненний foobar2000:$N`(title)` замінюється назвою пісні,$N`[(artist) - ](title)` пропускає [цю] частину, якщо виконавець порожній$N`$lpad((tn),2,0)` доповнює номер треку до 2 цифр\",\n\t\"fr_pdel\": \"видалити\",\n\t\"fr_pnew\": \"зберегти як\",\n\t\"fr_pname\": \"надайте ім'я для вашого нового пресету\",\n\t\"fr_aborted\": \"перервано\",\n\t\"fr_lold\": \"старе ім'я\",\n\t\"fr_lnew\": \"нове ім'я\",\n\t\"fr_tags\": \"теги для вибраних файлів (тільки для читання, лише для довідки):\",\n\t\"fr_busy\": \"перейменування {0} елементів...\\n\\n{1}\",\n\t\"fr_efail\": \"перейменування невдале:\\n\",\n\t\"fr_nchg\": \"{0} з нових імен були змінені через <code>win</code> та/або <code>no /</code>\\n\\nOK продовжити з цими зміненими новими іменами?\",\n\n\t\"fd_ok\": \"видалення OK\",\n\t\"fd_err\": \"видалення невдале:\\n\",\n\t\"fd_none\": \"нічого не було видалено; можливо, заблоковано конфігурацією сервера (xbd)?\",\n\t\"fd_busy\": \"видалення {0} елементів...\\n\\n{1}\",\n\t\"fd_warn1\": \"ВИДАЛИТИ ці {0} елементи?\",\n\t\"fd_warn2\": \"<b>Останній шанс!</b> Неможливо скасувати. Видалити?\",\n\n\t\"fc_ok\": \"вирізано {0} елементів\",\n\t\"fc_warn\": 'вирізано {0} елементів\\n\\nале: тільки <b>ця</b> вкладка браузера може їх вставити\\n(оскільки вибір настільки величезний)',\n\n\t\"fcc_ok\": \"скопійовано {0} елементів до буфера\",\n\t\"fcc_warn\": 'скопійовано {0} елементів до буфера\\n\\nале: тільки <b>ця</b> вкладка браузера може їх вставити\\n(оскільки вибір настільки величезний)',\n\n\t\"fp_apply\": \"використовувати ці імена\",\n\t\"fp_skip\": \"пропустити конфлікти\", //m\n\t\"fp_ecut\": \"спочатку вирізати або скопіювати деякі файли / папки для вставки / переміщення\\n\\nзауваження: ви можете вирізати / вставляти через різні вкладки браузера\",\n\t\"fp_ename\": \"{0} елементів не можуть бути переміщені сюди, тому що імена вже зайняті. Дайте їм нові імена нижче для продовження, або залиште ім'я порожнім (\\\"пропустити конфлікти\\\"), щоб пропустити їх:\", //m\n\t\"fcp_ename\": \"{0} елементів не можуть бути скопійовані сюди, тому що імена вже зайняті. Дайте їм нові імена нижче для продовження, або залиште ім'я порожнім (\\\"пропустити конфлікти\\\"), щоб пропустити їх:\", //m\n\t\"fp_emore\": \"є ще деякі конфлікти імен файлів, які потрібно виправити\",\n\t\"fp_ok\": \"переміщення OK\",\n\t\"fcp_ok\": \"копіювання OK\",\n\t\"fp_busy\": \"переміщення {0} елементів...\\n\\n{1}\",\n\t\"fcp_busy\": \"копіювання {0} елементів...\\n\\n{1}\",\n\t\"fp_abrt\": \"переривання...\", //m\n\t\"fp_err\": \"переміщення невдале:\\n\",\n\t\"fcp_err\": \"копіювання невдале:\\n\",\n\t\"fp_confirm\": \"перемістити ці {0} елементи сюди?\",\n\t\"fcp_confirm\": \"скопіювати ці {0} елементи сюди?\",\n\t\"fp_etab\": 'не вдалося прочитати буфер з іншої вкладки браузера',\n\t\"fp_name\": \"завантаження файлу з вашого пристрою. Дайте йому ім'я:\",\n\t\"fp_both_m\": '<h6>виберіть, що вставити</h6><code>Enter</code> = Перемістити {0} файлів з «{1}»\\n<code>ESC</code> = Завантажити {2} файлів з вашого пристрою',\n\t\"fcp_both_m\": '<h6>виберіть, що вставити</h6><code>Enter</code> = Скопіювати {0} файлів з «{1}»\\n<code>ESC</code> = Завантажити {2} файлів з вашого пристрою',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Перемістити</a><a href=\"#\" id=\"modal-ng\">Завантажити</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Скопіювати</a><a href=\"#\" id=\"modal-ng\">Завантажити</a>',\n\n\t\"mk_noname\": \"введіть ім'я в текстове поле зліва перед тим, як робити це :p\",\n\t\"nmd_i1\": \"ви також можете додати потрібне розширення, наприклад <code>.md</code>\", //m\n\t\"nmd_i2\": \"ви можете створювати тільки файли <code>.{0}</code>, оскільки не маєте дозволу на видалення\", //m\n\n\t\"tv_load\": \"Завантаження текстового документа:\\n\\n{0}\\n\\n{1}% ({2} з {3} MiB завантажено)\",\n\t\"tv_xe1\": \"не вдалося завантажити текстовий файл:\\n\\nпомилка \",\n\t\"tv_xe2\": \"404, файл не знайдено\",\n\t\"tv_lst\": \"список текстових файлів в\",\n\t\"tvt_close\": \"повернутися до перегляду папки$NГаряча клавіша: M (або Esc)\\\">❌ закрити\",\n\t\"tvt_dl\": \"завантажити цей файл$NГаряча клавіша: Y\\\">💾 завантажити\",\n\t\"tvt_prev\": \"показати попередній документ$NГаряча клавіша: i\\\">⬆ попер\",\n\t\"tvt_next\": \"показати наступний документ$NГаряча клавіша: K\\\">⬇ наст\",\n\t\"tvt_sel\": \"вибрати файл &nbsp; ( для вирізання / копіювання / видалення / ... )$NГаряча клавіша: S\\\">вибр\",\n\t\"tvt_j\": \"прикрасити json$NГаряча клавіша: shift-J\\\">j\", //m\n\t\"tvt_edit\": \"відкрити файл в текстовому редакторі$NГаряча клавіша: E\\\">✏️ редагувати\",\n\t\"tvt_tail\": \"моніторити файл на зміни; показувати нові рядки в реальному часі\\\">📡 слідкувати\",\n\t\"tvt_wrap\": \"перенесення слів\\\">↵\",\n\t\"tvt_atail\": \"заблокувати прокрутку до низу сторінки\\\">⚓\",\n\t\"tvt_ctail\": \"декодувати кольори терміналу (ansi escape коди)\\\">🌈\",\n\t\"tvt_ntail\": \"ліміт історії прокрутки (скільки байтів тексту тримати завантаженими)\",\n\n\t\"m3u_add1\": \"пісня додана до m3u плейлисту\",\n\t\"m3u_addn\": \"{0} пісень додано до m3u плейлисту\",\n\t\"m3u_clip\": \"m3u плейлист тепер скопійований до буфера\\n\\nви повинні створити новий текстовий файл з назвою щось.m3u і вставити плейлист в цей документ; це зробить його відтворюваним\",\n\n\t\"gt_vau\": \"не показувати відео, лише відтворювати аудіо\\\">🎧\",\n\t\"gt_msel\": \"увімкнути вибір файлів; ctrl-клік по файлу для перевизначення$N$N&lt;em&gt;коли активний: подвійний клік по файлу / папці щоб відкрити&lt;/em&gt;$N$NГаряча клавіша: S\\\">мультивибір\",\n\t\"gt_crop\": \"обрізати мініатюри по центру\\\">обрізка\",\n\t\"gt_3x\": \"мініатюри високої роздільності\\\">3x\",\n\t\"gt_zoom\": \"масштаб\",\n\t\"gt_chop\": \"обрізати\",\n\t\"gt_sort\": \"сортувати за\",\n\t\"gt_name\": \"ім'ям\",\n\t\"gt_sz\": \"розміром\",\n\t\"gt_ts\": \"датою\",\n\t\"gt_ext\": \"типом\",\n\t\"gt_c1\": \"обрізати імена файлів більше (показувати менше)\",\n\t\"gt_c2\": \"обрізати імена файлів менше (показувати більше)\",\n\n\t\"sm_w8\": \"пошук...\",\n\t\"sm_prev\": \"результати пошуку нижче з попереднього запиту:\\n  \",\n\t\"sl_close\": \"закрити результати пошуку\",\n\t\"sl_hits\": \"показано {0} результатів\",\n\t\"sl_moar\": \"завантажити більше\",\n\n\t\"s_sz\": \"розмір\",\n\t\"s_dt\": \"дата\",\n\t\"s_rd\": \"шлях\",\n\t\"s_fn\": \"ім'я\",\n\t\"s_ta\": \"теги\",\n\t\"s_ua\": \"up@\",\n\t\"s_ad\": \"розш.\",\n\t\"s_s1\": \"мінімум MiB\",\n\t\"s_s2\": \"максимум MiB\",\n\t\"s_d1\": \"мін. iso8601\",\n\t\"s_d2\": \"макс. iso8601\",\n\t\"s_u1\": \"завантажено після\",\n\t\"s_u2\": \"та/або до\",\n\t\"s_r1\": \"шлях містить &nbsp; (розділені пробілами)\",\n\t\"s_f1\": \"ім'я містить &nbsp; (заперечення з -ні)\",\n\t\"s_t1\": \"теги містять &nbsp; (^=початок, кінець=$)\",\n\t\"s_a1\": \"специфічні властивості метаданих\",\n\n\t\"md_eshow\": \"не можу відобразити \",\n\t\"md_off\": \"[📜<em>readme</em>] відключено в [⚙️] -- документ прихований\",\n\n\t\"badreply\": \"Не вдалося обробити відповідь сервера\",\n\n\t\"xhr403\": \"403: Доступ заборонено\\n\\nспробуйте натиснути F5, можливо ви вийшли з системи\",\n\t\"xhr0\": \"невідома (ймовірно втрачено з'єднання з сервером, або сервер офлайн)\",\n\t\"cf_ok\": \"вибачте за це -- захист від DD\" + wah + \"oS спрацював\\n\\nречі повинні відновитися приблизно через 30 сек\\n\\nякщо нічого не відбувається, натисніть F5 для перезавантаження сторінки\",\n\t\"tl_xe1\": \"не вдалося перелічити підпапки:\\n\\nпомилка \",\n\t\"tl_xe2\": \"404: Папка не знайдена\",\n\t\"fl_xe1\": \"не вдалося перелічити файли в папці:\\n\\nпомилка \",\n\t\"fl_xe2\": \"404: Папка не знайдена\",\n\t\"fd_xe1\": \"не вдалося створити підпапку:\\n\\nпомилка \",\n\t\"fd_xe2\": \"404: Батьківська папка не знайдена\",\n\t\"fsm_xe1\": \"не вдалося надіслати повідомлення:\\n\\nпомилка \",\n\t\"fsm_xe2\": \"404: Батьківська папка не знайдена\",\n\t\"fu_xe1\": \"не вдалося завантажити список unpost з сервера:\\n\\nпомилка \",\n\t\"fu_xe2\": \"404: Файл не знайдено??\",\n\n\t\"fz_tar\": \"нестиснутий gnu-tar файл (linux / mac)\",\n\t\"fz_pax\": \"нестиснутий tar в pax-форматі (повільніше)\",\n\t\"fz_targz\": \"gnu-tar зі стисненням gzip рівня 3$N$Nце зазвичай дуже повільно, тому$Nвикористовуйте нестиснутий tar замість цього\",\n\t\"fz_tarxz\": \"gnu-tar зі стисненням xz рівня 1$N$Nце зазвичай дуже повільно, тому$Nвикористовуйте нестиснутий tar замість цього\",\n\t\"fz_zip8\": \"zip з utf8 іменами файлів (можливо нестабільний на windows 7 і старіших)\",\n\t\"fz_zipd\": \"zip з традиційними cp437 іменами файлів, для дуже старого ПЗ\",\n\t\"fz_zipc\": \"cp437 з crc32, обчисленим заздалегідь,$Nдля MS-DOS PKZIP v2.04g (жовтень 1993)$N(потребує більше часу для обробки перед початком завантаження)\",\n\n\t\"un_m1\": \"ви можете видалити ваші недавні завантаження (або перервати незавершені) нижче\",\n\t\"un_upd\": \"оновити\",\n\t\"un_m4\": \"або поділитися файлами, видимими нижче:\",\n\t\"un_ulist\": \"показати\",\n\t\"un_ucopy\": \"копіювати\",\n\t\"un_flt\": \"необов'язковий фільтр:&nbsp; URL повинен містити\",\n\t\"un_fclr\": \"очистити фільтр\",\n\t\"un_derr\": 'unpost-видалення невдале:\\n',\n\t\"un_f5\": 'щось зламалося, будь ласка, спробуйте оновити або натисніть F5',\n\t\"un_uf5\": \"вибачте, але ви повинні оновити сторінку (наприклад, натиснувши F5 або CTRL-R) перед тим, як це завантаження можна буде перервати\",\n\t\"un_nou\": '<b>попередження:</b> сервер занадто зайнятий, щоб показати незавершені завантаження; клацніть посилання \"оновити\" трохи пізніше',\n\t\"un_noc\": '<b>попередження:</b> unpost повністю завантажених файлів не увімкнено/дозволено в конфігурації сервера',\n\t\"un_max\": \"показано перші 2000 файлів (використовуйте фільтр)\",\n\t\"un_avail\": \"{0} недавніх завантажень можуть бути видалені<br />{1} незавершених можуть бути перервані\",\n\t\"un_m2\": \"відсортовано за часом завантаження; найновіші спочатку:\",\n\t\"un_no1\": \"ха! немає завантажень, достатньо недавніх\",\n\t\"un_no2\": \"ха! немає завантажень, що відповідають цьому фільтру, достатньо недавніх\",\n\t\"un_next\": \"видалити наступні {0} файлів нижче\",\n\t\"un_abrt\": \"перервати\",\n\t\"un_del\": \"видалити\",\n\t\"un_m3\": \"завантаження ваших недавніх завантажень...\",\n\t\"un_busy\": \"видалення {0} файлів...\",\n\t\"un_clip\": \"{0} посилань скопійовано до буфера\",\n\n\t\"u_https1\": \"вам слід\",\n\t\"u_https2\": \"переключитися на https\",\n\t\"u_https3\": \"для кращої продуктивності\",\n\t\"u_ancient\": 'ваш браузер вражаюче старий -- можливо, вам слід <a href=\"#\" onclick=\"goto(\\'bup\\')\">використовувати bup замість цього</a>',\n\t\"u_nowork\": \"потрібен firefox 53+ або chrome 57+ або iOS 11+\",\n\t\"tail_2old\": \"потрібен firefox 105+ або chrome 71+ або iOS 14.5+\",\n\t\"u_nodrop\": 'ваш браузер занадто старий для перетягування завантажень',\n\t\"u_notdir\": \"це не папка!\\n\\nваш браузер занадто старий,\\nбудь ласка, спробуйте перетягування замість цього\",\n\t\"u_uri\": \"щоб перетягнути зображення з інших вікон браузера,\\nбудь ласка, перетягніть його на велику кнопку завантаження\",\n\t\"u_enpot\": 'переключитися на <a href=\"#\">картоплинний UI</a> (може покращити швидкість завантаження)',\n\t\"u_depot\": 'переключитися на <a href=\"#\">вишуканий UI</a> (може зменшити швидкість завантаження)',\n\t\"u_gotpot\": 'переключення на картоплинний UI для покращення швидкості завантаження,\\n\\nне соромтеся не погодитися і переключитися назад!',\n\t\"u_pott\": \"<p>файли: &nbsp; <b>{0}</b> завершено, &nbsp; <b>{1}</b> невдало, &nbsp; <b>{2}</b> зайнято, &nbsp; <b>{3}</b> в черзі</p>\",\n\t\"u_ever\": \"це базовий завантажувач; up2k потребує принаймні<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1\",\n\t\"u_su2k\": 'це базовий завантажувач; <a href=\"#\" id=\"u2yea\">up2k</a> кращий',\n\t\"u_uput\": 'оптимізувати для швидкості (пропустити контрольну суму)',\n\t\"u_ewrite\": 'у вас немає доступу для запису в цю папку',\n\t\"u_eread\": 'у вас немає доступу для читання цієї папки',\n\t\"u_enoi\": 'пошук файлів не увімкнено в конфігурації сервера',\n\t\"u_enoow\": \"перезапис не працюватиме тут; потрібен дозвіл на видалення\",\n\t\"u_badf\": 'Ці {0} файли (з {1} загальних) були пропущені, можливо, через дозволи файлової системи:\\n\\n',\n\t\"u_blankf\": 'Ці {0} файли (з {1} загальних) порожні; все одно завантажити їх?\\n\\n',\n\t\"u_applef\": 'Ці {0} файли (з {1} загальних), ймовірно, небажані;\\nНатисніть <code>Гаразд/Enter</code> щоб ПРОПУСТИТИ наступні файли,\\nНатисніть <code>Скасувати/ESC</code> щоб НЕ виключати, і ЗАВАНТАЖИТИ їх також:\\n\\n',\n\t\"u_just1\": '\\nМожливо, це спрацює краще, якщо ви виберете лише один файл',\n\t\"u_ff_many\": \"якщо ви використовуєте <b>Linux / MacOS / Android,</b> то така кількість файлів <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\"><em>може</em> завісити Firefox!</a>\\nякщо це станеться, будь ласка, спробуйте знову (або використовуйте Chrome).\",\n\t\"u_up_life\": \"Це завантаження буде видалено з сервера\\n{0} після його завершення\",\n\t\"u_asku\": 'завантажити ці {0} файлів до <code>{1}</code>',\n\t\"u_unpt\": \"ви можете скасувати / видалити це завантаження, використовуючи 🧯 зверху зліва\",\n\t\"u_bigtab\": 'збираюся показати {0} файлів\\n\\nце може завісити ваш браузер, ви впевнені?',\n\t\"u_scan\": 'Сканування файлів...',\n\t\"u_dirstuck\": 'ітератор каталогу застряг, намагаючись отримати доступ до наступних {0} елементів; пропущу:',\n\t\"u_etadone\": 'Готово ({0}, {1} файлів)',\n\t\"u_etaprep\": '(підготовка до завантаження)',\n\t\"u_hashdone\": 'хешування завершено',\n\t\"u_hashing\": 'хешування',\n\t\"u_hs\": 'рукостискання...',\n\t\"u_started\": \"файли тепер завантажуються; дивіться [🚀]\",\n\t\"u_dupdefer\": \"дублікат; буде оброблено після всіх інших файлів\",\n\t\"u_actx\": \"клацніть цей текст, щоб запобігти втраті<br />продуктивності при переключенні на інші вікна/вкладки\",\n\t\"u_fixed\": \"OK!&nbsp; Виправлено 👍\",\n\t\"u_cuerr\": \"не вдалося завантажити фрагмент {0} з {1};\\nймовірно, нешкідливо, продовжую\\n\\nфайл: {2}\",\n\t\"u_cuerr2\": \"сервер відхилив завантаження (фрагмент {0} з {1});\\nспробую пізніше\\n\\nфайл: {2}\\n\\nпомилка \",\n\t\"u_ehstmp\": \"спробую знову; дивіться внизу справа\",\n\t\"u_ehsfin\": \"сервер відхилив запит на завершення завантаження; повторюю...\",\n\t\"u_ehssrch\": \"сервер відхилив запит на виконання пошуку; повторюю...\",\n\t\"u_ehsinit\": \"сервер відхилив запит на ініціацію завантаження; повторюю...\",\n\t\"u_eneths\": \"мережева помилка під час виконання рукостискання завантаження; повторюю...\",\n\t\"u_enethd\": \"мережева помилка під час тестування існування цілі; повторюю...\",\n\t\"u_cbusy\": \"чекаємо, поки сервер знову нам довірятиме після мережевого збою...\",\n\t\"u_ehsdf\": \"на сервері закінчилося місце на диску!\\n\\nбуду продовжувати спроби, на випадок, якщо хтось\\nзвільнить достатньо місця для продовження\",\n\t\"u_emtleak1\": \"схоже, ваш веб-браузер може мати витік пам'яті;\\nбудь ласка,\",\n\t\"u_emtleak2\": ' <a href=\"{0}\">переключіться на https (рекомендується)</a> або ',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": 'спробуйте наступне:\\n<ul><li>натисніть <code>F5</code> для оновлення сторінки</li><li>потім відключіть кнопку &nbsp;<code>mt</code>&nbsp; в &nbsp;<code>⚙️ налаштуваннях</code></li><li>і спробуйте це завантаження знову</li></ul>Завантаження будуть трохи повільнішими, але що поробиш.\\nВибачте за незручності !\\n\\nPS: chrome v107 <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">має виправлення</a> для цього',\n\t\"u_emtleakf\": 'спробуйте наступне:\\n<ul><li>натисніть <code>F5</code> для оновлення сторінки</li><li>потім увімкніть <code>🥔</code> (картопля) в UI завантаження<li>і спробуйте це завантаження знову</li></ul>\\nPS: firefox <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">сподіваємося, матиме виправлення</a> в якийсь момент',\n\t\"u_s404\": \"не знайдено на сервері\",\n\t\"u_expl\": \"пояснити\",\n\t\"u_maxconn\": \"більшість браузерів обмежують це до 6, але firefox дозволяє підвищити це з <code>connections-per-server</code> в <code>about:config</code>\",\n\t\"u_tu\": '<p class=\"warn\">ПОПЕРЕДЖЕННЯ: turbo увімкнено, <span>&nbsp;клієнт може не виявити і поновити неповні завантаження; дивіться підказку turbo-кнопки</span></p>',\n\t\"u_ts\": '<p class=\"warn\">ПОПЕРЕДЖЕННЯ: turbo увімкнено, <span>&nbsp;результати пошуку можуть бути неправильними; дивіться підказку turbo-кнопки</span></p>',\n\t\"u_turbo_c\": \"turbo відключено в конфігурації сервера\",\n\t\"u_turbo_g\": \"відключаю turbo, тому що у вас немає\\nпривілеїв перегляду каталогів в цьому томі\",\n\t\"u_life_cfg\": 'автовидалення через <input id=\"lifem\" p=\"60\" /> хв (або <input id=\"lifeh\" p=\"3600\" /> годин)',\n\t\"u_life_est\": 'завантаження буде видалено <span id=\"lifew\" tt=\"місцевий час\">---</span>',\n\t\"u_life_max\": 'ця папка забезпечує\\nмакс. термін життя {0}',\n\t\"u_unp_ok\": 'unpost дозволено для {0}',\n\t\"u_unp_ng\": 'unpost НЕ буде дозволено',\n\t\"ue_ro\": 'ваш доступ до цієї папки тільки для читання\\n\\n',\n\t\"ue_nl\": 'ви зараз не увійшли в систему',\n\t\"ue_la\": 'ви зараз увійшли як \"{0}\"',\n\t\"ue_sr\": 'ви зараз в режимі пошуку файлів\\n\\nпереключіться на режим завантаження, клацнувши лупу 🔎 (поруч з великою кнопкою ПОШУК), і спробуйте завантажити знову\\n\\nвибачте',\n\t\"ue_ta\": 'спробуйте завантажити знову, це повинно спрацювати зараз',\n\t\"ue_ab\": \"цей файл вже завантажується в іншу папку, і це завантаження повинно бути завершено перед тим, як файл можна буде завантажити в інше місце.\\n\\nВи можете перервати і забути початкове завантаження, використовуючи 🧯 зверху зліва\",\n\t\"ur_1uo\": \"OK: Файл успішно завантажено\",\n\t\"ur_auo\": \"OK: Всі {0} файлів успішно завантажено\",\n\t\"ur_1so\": \"OK: Файл знайдено на сервері\",\n\t\"ur_aso\": \"OK: Всі {0} файлів знайдено на сервері\",\n\t\"ur_1un\": \"Завантаження невдале, вибачте\",\n\t\"ur_aun\": \"Всі {0} завантажень невдалі, вибачте\",\n\t\"ur_1sn\": \"Файл НЕ знайдено на сервері\",\n\t\"ur_asn\": \"{0} файлів НЕ знайдено на сервері\",\n\t\"ur_um\": \"Завершено;\\n{0} завантажень OK,\\n{1} завантажень невдалих, вибачте\",\n\t\"ur_sm\": \"Завершено;\\n{0} файлів знайдено на сервері,\\n{1} файлів НЕ знайдено на сервері\",\n\n\t\"rc_opn\": \"відкрити\", //m\n\t\"rc_ply\": \"відтворити\", //m\n\t\"rc_pla\": \"відтворити як аудіо\", //m\n\t\"rc_txt\": \"відкрити у переглядачі файлів\", //m\n\t\"rc_md\": \"відкрити в текстовому редакторі\", //m\n\t\"rc_dl\": \"завантажити\", //m\n\t\"rc_zip\": \"завантажити як архів\", //m\n\t\"rc_cpl\": \"копіювати посилання\", //m\n\t\"rc_del\": \"видалити\", //m\n\t\"rc_cut\": \"вирізати\", //m\n\t\"rc_cpy\": \"копіювати\", //m\n\t\"rc_pst\": \"вставити\", //m\n\t\"rc_rnm\": \"перейменувати\", //m\n\t\"rc_nfo\": \"нова папка\", //m\n\t\"rc_nfi\": \"новий файл\", //m\n\t\"rc_sal\": \"вибрати все\", //m\n\t\"rc_sin\": \"інвертувати вибір\", //m\n\t\"rc_shf\": \"поділитися цією папкою\", //m\n\t\"rc_shs\": \"поділитися вибраним\", //m\n\n\t\"lang_set\": \"оновити сторінку, щоб зміни набули чинності?\",\n\n\t\"splash\": {\n\t\t\"a1\": \"оновити\",\n\t\t\"b1\": \"привітик, незнайомцю &nbsp; <small>(ви не авторизовані)</small>\",\n\t\t\"c1\": \"вийти\",\n\t\t\"d1\": \"трасування стека\",\n\t\t\"d2\": \"показує стан усіх активних потоків\",\n\t\t\"e1\": \"перезавантажити конфіг\",\n\t\t\"e2\": \"перезавантажити файли конфігурації (облікові записи/томи/прапорці),$Nта пересканувати всі томи e2ds$N$Nувага: будь-які зміни глобальних налаштувань$Nвимагають повного перезапуску\",\n\t\t\"f1\": \"ви можете бачити:\",\n\t\t\"g1\": \"ви можете завантажувати файли в:\",\n\t\t\"cc1\": \"всяка всячина:\",\n\t\t\"h1\": \"вимкнути k304\",\n\t\t\"i1\": \"увімкнути k304\",\n\t\t\"j1\": \"увімкнення k304 буде відключати ваш клієнт при кожному HTTP 304, що може запобігти зависанню деяких глючних проксі (раптово перестають завантажувати сторінки), <em>але</em> це також зробить усе повільнішим загалом\",\n\t\t\"k1\": \"скинути налаштування клієнта\",\n\t\t\"l1\": \"авторизуйтесь для інших опцій:\",\n\t\t\"ls3\": \"увійти\", //m\n\t\t\"lu4\": \"ім'я користувача\", //m\n\t\t\"lp4\": \"пароль\", //m\n\t\t\"lo3\": \"вийти з облікового запису “{0}” всюди\", //m\n\t\t\"lo2\": \"це завершить сеанс у всіх браузерах\", //m\n\t\t\"m1\": \"з поверненням,\",\n\t\t\"n1\": \"404 не знайдено &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": 'або у вас немає доступу -- спробуйте авторизуватися або <a href=\"' + SR + '/?h\">повернутися на головну</a>',\n\t\t\"p1\": \"403 доступ заборонений &nbsp;~┻━┻\",\n\t\t\"q1\": 'авторизуйтесь або <a href=\"' + SR + '/?h\">поверніться на головну</a>',\n\t\t\"r1\": \"повернутися на головну\",\n\t\t\".s1\": \"пересканувати\",\n\t\t\"t1\": \"дія\",\n\t\t\"u2\": \"час з останнього запису сервера$N( завантаження / перейменування / ... )$N$N17d = 17 днів$N1h23 = 1 година 23 хвилини$N4m56 = 4 хвилини 56 секунд\",\n\t\t\"v1\": \"підключити\",\n\t\t\"v2\": \"використовувати цей сервер як локальний HDD\",\n\t\t\"w1\": \"перейти на https\",\n\t\t\"x1\": \"змінити пароль\",\n\t\t\"y1\": \"керування доступом\",\n\t\t\"z1\": \"розблокувати:\",\n\t\t\"ta1\": \"спочатку заповніть ваш новий пароль\",\n\t\t\"ta2\": \"повторіть для підтвердження нового пароля:\",\n\t\t\"ta3\": \"описка; спробуйте знову\",\n\t\t\"nop\": \"ПОМИЛКА: Пароль не може бути порожнім\", //m\n\t\t\"nou\": \"ПОМИЛКА: Ім’я користувача та/або пароль не можуть бути порожніми\", //m\n\t\t\"aa1\": \"вхідні файли:\",\n\t\t\"ab1\": \"вимкнути no304\",\n\t\t\"ac1\": \"увімкнути no304\",\n\t\t\"ad1\": \"увімкнення no304 вимкне все кешування; спробуйте це, якщо k304 було недостатньо. Це витратить величезну кількість мережевого трафіку!\",\n\t\t\"ae1\": \"активні завантаження:\",\n\t\t\"af1\": \"показати нещодавні завантаження\",\n\t\t\"ag1\": \"показати відомих IdP-користувачів\",\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/tl/vie.js",
    "content": "Ls.vie = {\n\t\"tt\": \"Tiếng Việt\",\n\n\t\"cols\": {\n\t\t\"c\": \"nút hành động\",\n\t\t\"dur\": \"thời lượng\",\n\t\t\"q\": \"chất lượng / bitrate\",\n\t\t\"Ac\": \"codec âm thanh\",\n\t\t\"Vc\": \"codec video\",\n\t\t\"Fmt\": \"định dạng / container\",\n\t\t\"Ahash\": \"checksum âm thanh\",\n\t\t\"Vhash\": \"checksum video\",\n\t\t\"Res\": \"độ phân giải\",\n\t\t\"T\": \"loại tệp\",\n\t\t\"aq\": \"chất lượng âm thanh / bitrate\",\n\t\t\"vq\": \"chất lượng video / bitrate\",\n\t\t\"pixfmt\": \"subsampling / pixel structure\",\n\t\t\"resw\": \"độ phân giải ngang\",\n\t\t\"resh\": \"độ phân giải dọc\",\n\t\t\"chs\": \"kênh âm thanh\",\n\t\t\"hz\": \"tốc độ lấy mẫu\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"misc\",\n\t\t\t[\"ESC\", \"đóng nhiều mục\"],\n\n\t\t\t\"file-manager\",\n\t\t\t[\"G\", \"chuyển đổi chế độ xem danh sách / lưới\"],\n\t\t\t[\"T\", \"chuyển đổi ảnh thu nhỏ / biểu tượng\"],\n\t\t\t[\"⇧ A/D\", \"kích thước ảnh thu nhỏ\"],\n\t\t\t[\"ctrl-K\", \"xoá mục đã chọn\"],\n\t\t\t[\"ctrl-X\", \"cắt mục đã chọn vào bảng nhớ tạm\"],\n\t\t\t[\"ctrl-C\", \"sao chép mục đã chọn vào bảng nhớ tạm\"],\n\t\t\t[\"ctrl-V\", \"dán (di chuyển/sao chép) tại đây\"],\n\t\t\t[\"Y\", \"tải xuống mục đã chọn\"],\n\t\t\t[\"F2\", \"đổi tên mục đã chọn\"],\n\n\t\t\t\"file-list-sel\",\n\t\t\t[\"space\", \"chuyển đổi chọn tệp\"],\n\t\t\t[\"↑/↓\", \"di chuyển con trỏ chọn\"],\n\t\t\t[\"ctrl ↑/↓\", \"di chuyển con trỏ và khung nhìn\"],\n\t\t\t[\"⇧ ↑/↓\", \"chọn tệp trước / sau\"],\n\t\t\t[\"ctrl-A\", \"chọn tất cả tệp / thư mục\"],\n\t\t], [\n\t\t\t\"navigation\",\n\t\t\t[\"B\", \"chuyển đổi đường dẫn / thanh điều hướng\"],\n\t\t\t[\"I/K\", \"thư mục trước / sau\"],\n\t\t\t[\"M\", \"thư mục cha (hoặc thu gọn hiện tại)\"],\n\t\t\t[\"V\", \"chuyển đổi thư mục / tệp văn bản trong thanh điều hướng\"],\n\t\t\t[\"A/D\", \"kích thước thanh điều hướng\"],\n\t\t], [\n\t\t\t\"audio-player\",\n\t\t\t[\"J/L\", \"bài trước / sau\"],\n\t\t\t[\"U/O\", \"lùi / tiến 10 giây\"],\n\t\t\t[\"0..9\", \"nhảy đến 0%..90%\"],\n\t\t\t[\"P\", \"phát/tạm dừng (cũng khởi động)\"],\n\t\t\t[\"S\", \"chọn bài đang phát\"],\n\t\t\t[\"Y\", \"tải xuống bài hát\"],\n\t\t], [\n\t\t\t\"image-viewer\",\n\t\t\t[\"J/L, ←/→\", \"ảnh trước / sau\"],\n\t\t\t[\"Home/End\", \"ảnh đầu / cuối\"],\n\t\t\t[\"F\", \"toàn màn hình\"],\n\t\t\t[\"R\", \"xoay theo chiều kim đồng hồ\"],\n\t\t\t[\"⇧ R\", \"xoay ngược chiều kim đồng hồ\"],\n\t\t\t[\"S\", \"chọn ảnh\"],\n\t\t\t[\"Y\", \"tải xuống ảnh\"],\n\t\t], [\n\t\t\t\"video-player\",\n\t\t\t[\"U/O\", \"lùi / tiến 10 giây\"],\n\t\t\t[\"P/K/Space\", \"phát/tạm dừng\"],\n\t\t\t[\"C\", \"tiếp tục phát bài tiếp theo\"],\n\t\t\t[\"V\", \"vòng lặp\"],\n\t\t\t[\"M\", \"tắt tiếng\"],\n\t\t\t[\"[ and ]\", \"đặt khoảng lặp\"],\n\t\t], [\n\t\t\t\"textfile-viewer\",\n\t\t\t[\"I/K\", \"tệp trước / sau\"],\n\t\t\t[\"M\", \"đóng tệp văn bản\"],\n\t\t\t[\"E\", \"chỉnh sửa tệp văn bản\"],\n\t\t\t[\"S\", \"chọn tệp (để cắt/sao chép/đổi tên)\"],\n\t\t]\n\t],\n\n\t\"m_ok\": \"OK\",\n\t\"m_ng\": \"Hủy\",\n\n\t\"enable\": \"Bật\",\n\t\"danger\": \"NGUY HIỂM\",\n\t\"clipped\": \"đã sao chép vào bảng nhớ tạm\",\n\n\t\"ht_s1\": \"giây\",\n\t\"ht_s2\": \"giây\",\n\t\"ht_m1\": \"phút\",\n\t\"ht_m2\": \"phút\",\n\t\"ht_h1\": \"giờ\",\n\t\"ht_h2\": \"giờ\",\n\t\"ht_d1\": \"ngày\",\n\t\"ht_d2\": \"ngày\",\n\t\"ht_and\": \" và \",\n\n\t\"goh\": \"bảng điều khiển\",\n\t\"gop\": 'thư mục trước\">trước',\n\t\"gou\": 'thư mục cha\">lên',\n\t\"gon\": 'thư mục sau\">tiếp',\n\t\"logout\": \"Đăng xuất \",\n\t\"login\": \"Đăng nhập\",\n\t\"access\": \"quyền truy cập\",\n\t\"ot_close\": \"đóng menu con\",\n\n\t\"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`&quot;try unite&quot;` = 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`\",\n\n\t\"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ở\",\n\t\"ot_bup\": \"bup: trình tải lên cơ bản, hỗ trợ cả Netscape 4.0\",\n\t\"ot_mkdir\": \"mkdir: tạo thư mục mới\",\n\t\"ot_md\": \"new-file: tạo tệp văn bản mới\",\n\t\"ot_msg\": \"msg: gửi tin nhắn đến nhật ký máy chủ\",\n\t\"ot_mp\": \"tuỳ chọn trình phát phương tiện\",\n\t\"ot_cfg\": \"tuỳ chọn cấu hình\",\n\t\"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 [🎈]&nbsp; (trình tải lên cơ bản)<br /><br />trong quá trình tải, biểu tượng này sẽ trở thành chỉ thị tiến trình!',\n\t\"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 [🎈]&nbsp; (trình tải lên cơ bản)<br /><br />trong quá trình tải, biểu tượng này sẽ trở thành chỉ thị tiến trình!',\n\t\"ot_noie\": 'Vui lòng sử dụng Chrome / Firefox / Edge',\n\n\t\"ab_mkdir\": \"tạo thư mục\",\n\t\"ab_mkdoc\": \"tạo tệp văn bản\",\n\t\"ab_msg\": \"gửi tin nhắn đến nhật ký máy chủ\",\n\n\t\"ay_path\": \"bỏ qua đến thư mục\",\n\t\"ay_files\": \"bỏ qua đến tệp\",\n\n\t\"wt_ren\": \"đổi tên các mục đã chọn$NPhím tắt: F2\",\n\t\"wt_del\": \"xóa các mục đã chọn$NPhím tắt: ctrl-K\",\n\t\"wt_cut\": \"cắt các mục đã chọn &lt;small&gt;(sau đó dán ở nơi khác)&lt;/small&gt;$NPhím tắt: ctrl-X\",\n\t\"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\",\n\t\"wt_pst\": \"dán một lựa chọn đã cắt / sao chép trước đó$NPhím tắt: ctrl-V\",\n\t\"wt_selall\": \"chọn tất cả các tệp$NPhím tắt: ctrl-A (khi tệp được chọn)\",\n\t\"wt_selinv\": \"đảo ngược lựa chọn\",\n\t\"wt_zip1\": \"tải thư mục này dưới định dạng nén\",\n\t\"wt_selzip\": \"tải lựa chọn dưới định dạng nén\",\n\t\"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\",\n\t\"wt_npirc\": \"sao chép thông tin bản nhạc theo định dạng irc\",\n\t\"wt_nptxt\": \"sao chép thông tin bản nhạc dưới dạng văn bản thuần túy\",\n\t\"wt_m3ua\": \"thêm vào danh sách phát m3u (bấm <code>📻copy</code> sau)\",\n\t\"wt_m3uc\": \"sao chép danh sách phát m3u vào bảng nhớ tạm\",\n\t\"wt_grid\": \"chuyển đổi chế độ xem danh sách / lưới $NPhím tắt: G\",\n\t\"wt_prev\": \"bài trước$NPhím tắt: J\",\n\t\"wt_play\": \"phát / tạm dừng$NPhím tắt: P\",\n\t\"wt_next\": \"bài sau$NPhím tắt: L\",\n\n\t\"ul_par\": \"tải lên song song:\",\n\t\"ut_rand\": \"ngẫu nhiên hoá tên tệp\",\n\t\"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ủ\\\">📅\",\n\t\"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\n\t\"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\",\n\t\"ut_ask\": 'yêu cầu xác nhận trước khi bắt đầu tải lên\">💭',\n\t\"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\",\n\t\"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)\",\n\t\"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\",\n\t\"ul_btn\": \"thả tệp / thư mục<br>ở đây (hoặc nhấn vào tôi)\",\n\t\"ul_btnu\": \"T Ả I  L Ê N\",\n\t\"ul_btns\": \"T Ì M  K I Ế M\",\n\n\t\"ul_hash\": \"hash\",\n\t\"ul_send\": \"gửi\",\n\t\"ul_done\": \"hoàn tất\",\n\t\"ul_idle1\": \"chưa có mục nào trong hàng chờ tải lên\",\n\t\"ut_etah\": \"tốc độ &lt;em&gt;hash&lt;/em&gt; trung bình và thời gian dự kiến để hoàn tất\",\n\t\"ut_etau\": \"tốc độ &lt;em&gt;tải lên&lt;/em&gt; trung bình và thời gian dự kiến để hoàn tất\",\n\t\"ut_etat\": \"tốc độ &lt;em&gt;tổng&lt;/em&gt; trung bình và thời gian dự kiến để hoàn tất\",\n\n\t\"uct_ok\": \"hoàn tất thành công\",\n\t\"uct_ng\": \"không hợp lệ: lỗi / bị từ chối / không tìm thấy\",\n\t\"uct_done\": \"đã xử lý: gồm cả thành công và không hợp lệ\",\n\t\"uct_bz\": \"đang hash hoặc tải lên\",\n\t\"uct_q\": \"nhàn rỗi, đang chờ\",\n\n\t\"utl_name\": \"tên tệp\",\n\t\"utl_ulist\": \"danh sách\",\n\t\"utl_ucopy\": \"sao chép\",\n\t\"utl_links\": \"đường dẫn\",\n\t\"utl_stat\": \"trạng thái\",\n\t\"utl_prog\": \"tiến trình\",\n\n\t// keep short:\n\n\t// phần up2k\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"LỖI\",\n\t\"utl_oserr\": \"Lỗi hệ thống\",\n\t\"utl_found\": \"tìm thấy\",\n\t\"utl_defer\": \"hoãn\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"hoàn tất\",\n\n\t\"ul_flagblk\": \"tệp đã được thêm vào hàng chờ</b><br>tuy vậy đang có một tiến trình up2k đang chạy ở một tab khác<br>vui lòng đợi cho đến khi tiến trình đó hoàn tất hoặc bị hủy\",\n\t\"ul_btnlk\": \"cài đặt của máy chủ đã khóa tùy chọn ở trạng thái này\",\n\n\t\"udt_up\": \"Tải lên\",\n\t\"udt_srch\": \"Tìm kiếm\",\n\t\"udt_drop\": \"thả vào đây\",\n\n\t\"u_nav_m\": '<h6>chọn phương thức tải lên</h6><code>Enter</code> = Tệp (một hoặc nhiều)\\n<code>ESC</code> = Một thư mục (kèm thư mục con)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Tệp</a><a href=\"#\" id=\"modal-ng\">Một thư mục</a>',\n\n\t// settings / config:\n\t\"cl_opts\": \"tuỳ chọn\",\n\t\"cl_hfsz\": \"kích thước tệp\",\n\t\"cl_themes\": \"giao diện\",\n\t\"cl_langs\": \"ngôn ngữ\",\n\t\"cl_ziptype\": \"định dạng nén\",\n\t\"cl_uopts\": \"tuỳ chọn up2k\",\n\t\"cl_favico\": \"favicon\",\n\t\"cl_bigdir\": \"thư mục lớn\",\n\t\"cl_hsort\": \"#sắp xếp\",\n\t\"cl_keytype\": \"ghi chú bàn phím\",\n\t\"cl_hiddenc\": \"cột đã ẩn\",\n\t\"cl_hidec\": \"ẩn\",\n\t\"cl_reset\": \"đặt lại\",\n\t\"cl_hpick\": \"chạm vào tiêu đề cột để ẩn trong bảng bên dưới\",\n\t\"cl_hcancel\": \"đã hủy việc ẩn cột\",\n\t\"cl_rcm\": \"menu chuột phải\", //m\n\n\t// settings / tuỳ chọn\n\t\"ct_grid\": '田 chế độ lưới',\n\t\"ct_ttips\": '༼ ◕_◕ ༽\">ℹ️ tooltips',\n\t\"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ỏ',\n\t\"ct_csel\": 'dùng CTRL và SHIFT để chọn tệp trong chế độ lưới\">sel',\n\t\"ct_dsel\": 'dùng chọn bằng cách kéo trong chế độ lưới\">kéo', //m\n\t\"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',\n\t\"ct_ihop\": 'khi đóng trình xem ảnh, cuộn xuống tệp đã xem gần nhất\">g⮯',\n\t\"ct_dots\": 'hiển thị tệp ẩn (nếu máy chủ cho phép)\">dotfiles',\n\t\"ct_qdel\": 'khi xóa tệp, chỉ hỏi xác nhận một lần\">qdel',\n\t\"ct_dir1st\": 'sắp xếp thư mục trước tệp\">📁 first',\n\t\"ct_nsort\": 'sắp xếp tự nhiên (cho tên tệp có số ở đầu)\">nsort',\n\t\"ct_utc\": 'hiển thị mọi thời gian theo UTC\">UTC',\n\t\"ct_readme\": 'hiển thị README.md trong danh sách thư mục\">📜 readme',\n\t\"ct_idxh\": 'hiển thị index.html thay cho danh sách thư mục\">htm',\n\t\"ct_sbars\": 'hiển thị thanh cuộn\">⟊',\n\n\t// tuỳ chọn up2k\n\t\"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📅\",\n\n\t\"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 <em>&quot;kích thước tệp ở trên máy chủ có giống nhau không&quot;</em> 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à &quot;tải lên&quot; lại các tệp đó để xác minh\\\">turbo\",\n\n\t\"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 <em>về lý thuyết</em> 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\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"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)\",\n\t\"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)\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t// favicon\n\t\"cft_text\": \"chuỗi favicon (để trống và làm mới trang để tắt)\",\n\t\"cft_fg\": \"màu chữ\",\n\t\"cft_bg\": \"màu nền\",\n\n\t// big dirs\n\t\"cdt_lim\": \"số tệp tối đa hiển thị trong thư mục\",\n\t\"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ì\",\n\t\"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\",\n\t\"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\n\t\"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\n\n\t\"tt_entree\": \"hiển thị thanh điều hướng (cây thư mục)$NPhím tắt: B\",\n\t\"tt_detree\": \"hiển thị đường dẫn$NPhím tắt: B\",\n\t\"tt_visdir\": \"cuộn đến thư mục đã chọn\",\n\t\"tt_ftree\": \"chuyển đổi cây thư mục / tệp văn bản$NPhím tắt: V\",\n\t\"tt_pdock\": \"hiển thị thư mục cha trong thanh ghim trên cùng\",\n\t\"tt_dynt\": \"tự mở rộng khi cây mở rộng\",\n\t\"tt_wrap\": \"ngắt dòng\",\n\t\"tt_hover\": \"hiện thị dòng tràn khi rê chuột$N( không cuộn được nếu $N&nbsp; con trỏ chuột nằm ngoài cột trái )\",\n\n\t\"ml_pmode\": \"ở cuối thư mục...\",\n\t\"ml_btns\": \"lệnh\",\n\t\"ml_tcode\": \"mã hoá lại\",\n\t\"ml_tcode2\": \"mã hoá lại thành\",\n\t\"ml_tint\": \"tô màu\",\n\t\"ml_eq\": \"bộ cân bằng âm thanh\",\n\t\"ml_drc\": \"bộ nén dải động\",\n\t\"ml_ss\": \"bỏ qua khoảng lặng\", //m\n\n\t\"mt_loop\": \"lặp lại một bài\\\">🔁\",\n\t\"mt_one\": \"dừng sau một bài\\\">1️⃣\",\n\t\"mt_shuf\": \"trộn các bài trong thư mụcr\\\">🔀\",\n\t\"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▶\",\n\t\"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\",\n\t\"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\",\n\t\"mt_fullpre\": \"cố gắng tải trước toàn bộ bài;$N✅ bật với kết nối <b>không ổn định</b>,$N❌ tắt <b>với kết nối chậm</b>\\\">full\",\n\t\"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)\\\">☕️\",\n\t\"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\",\n\t\"mt_npclip\": \"hiển thị nút để sao chép bài đang phát\\\">/np\",\n\t\"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\\\">📻\",\n\t\"mt_octl\": \"tích hợp hệ điều hành (phím tắt media / OSD)\\\">os-ctl\",\n\t\"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\",\n\t\"mt_oscv\": \"hiển thị bài album trên OSD\\\">art\",\n\t\"mt_follow\": \"giữ bài đang phát trong tầm nhìn\\\">🎯\",\n\t\"mt_compact\": \"giao diện điều khiển thu gọn\\\">⟎\",\n\t\"mt_uncache\": \"xoá bộ nhớ đệm &nbsp;(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\",\n\t\"mt_mloop\": \"lặp trong thư mục đang mở\\\">🔁 loop\",\n\t\"mt_mnext\": \"tải thư mục tiếp theo và tiếp tục\\\">📂 next\",\n\t\"mt_mstop\": \"dừng phát\\\">⏸ stop\",\n\t\"mt_cflac\": \"chuyển flac / wav sang {0}\\\">flac\",\n\t\"mt_caac\": \"chuyển aac / m4a sang {0}\\\">aac\",\n\t\"mt_coth\": \"chuyển mọi loại khác (trừ mp3) thành {0}\\\">oth\",\n\t\"mt_c2opus\": \"lựa chọn tốt nhất cho máy tính, laptop, android\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, cho iOS 17.5 trở lên\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, cho iOS 11 đến 17\\\">caf\",\n\t\"mt_c2mp3\": \"dùng trên thiết bị cũ\\\">mp3\",\n\t\"mt_c2flac\": \"chất lượng âm thanh tốt nhất, nhưng tệp tải xuống lớn\\\">flac\",\n\t\"mt_c2wav\": \"phát không nén (tệp còn lớn hơn nữa)\\\">wav\",\n\t\"mt_c2ok\": \"lựa chọn hợp lý\",\n\t\"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\",\n\t\"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\",\n\t\"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\",\n\t\"mt_tint\": \"mức nền (0-100) trên thanh tiến trình\",\n\t\n\t\"mt_eq\": \"`bật bộ cân bằng âm thanh và bộ tăng ích;$N$Nboost `0 &nbsp;` = âm lượng chuẩn 100% (không chỉnh)$N$Nwidth `1 &nbsp;` = stereo chuẩn (không chỉnh)$Nwidth `0.5` = 50% pha trái-phải$Nwidth `0 &nbsp;` = mono$N$Nboost `-0.8` &amp; 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\",\n\n\t\"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\",\n\n\t\"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\n\t\"mt_ssvt\": \"ngưỡng âm lượng (0-255)\\\">am\", //m\n\t\"mt_ssts\": \"ngưỡng hoạt động (% bài, đầu)\\\">dau\", //m\n\t\"mt_sste\": \"ngưỡng hoạt động (% bài, cuối)\\\">cuoi\", //m\n\t\"mt_sssm\": \"hệ số tốc độ phát\\\">nh\", //m\n\n\t\"mb_play\": \"phát\",\n\t\"mm_hashplay\": \"phát bản nhạc này?\",\n\t\"mm_m3u\": \"bấm <code>Enter/OK</code> để phát\\nbấm <code>ESC/Cancel</code> để chỉnh sửa\",\n\t\"mp_breq\": \"cần firefox 82+ hoặc chrome 73+ hoặc iOS 15+\",\n\t\"mm_bload\": \"đang tải...\",\n\t\"mm_bconv\": \"đang chuyển đổi sang {0}, vui lòng chờ...\",\n\t\"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\",\n\t\"mm_playerr\": \"phát lỗi: \",\n\t\"mm_eabrt\": \"Việc phát nhạc đã bị huỷ\",\n\t\"mm_enet\": \"Kết nối Internet không ổn định\",\n\t\"mm_edec\": \"Tệp này dường như đã bị hỏng??\",\n\t\"mm_esupp\": \"Trình duyệt của bạn không nhận dạng được định dạng tệp này.\",\n\t\"mm_eunk\": \"Lỗi không xác định\",\n\t\"mm_e404\": \"Không thể phát âm thanh; lỗi 404: Không tìm thấy tệp.\",\n\t\"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\",\n\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\n\t\"mm_e500\": \"Không thể phát âm thanh; lỗi 500: Kiểm tra nhật ký máy chủ.\",\n\t\"mm_e5xx\": \"Không thể phát âm thanh; lỗi máy chủ \",\n\t\"mm_nof\": \"không tìm thấy thêm tệp âm thanh nào gần đó\",\n\t\"mm_prescan\": \"Đang tìm bài nhạc tiếp theo để phát...\",\n\t\"mm_scank\": \"Đã tìm thấy bài nhạc tiếp theo:\",\n\t\"mm_uncache\": \"đã xoá bộ nhớ đệm; tất cả bài nhạc sẽ được tải lại khi phát tiếp\",\n\t\"mm_hnf\": \"bài nhạc này không còn tồn tại nữa\",\n\n\t\"im_hnf\": \"hình ảnh này không còn tồn tại nữa\",\n\n\t\"f_empty\": 'thư mục này trống',\n\t\"f_chide\": 'ẩn cột «{0}»\\n\\bạn có thế hiện lại nó trong tuỳ chọn cấu hình',\n\t\"f_bigtxt\": \"tệp này nặng {0} MiB  -- xác nhận xem dưới dạng văn bản?\",\n\t\"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\",\n\t\"fbd_more\": '<div id=\"blazy\">hiện <code>{0}</code> của <code>{1}</code> tệp; <a href=\"#\" id=\"bd_more\">hiện {2}</a> hoặc <a href=\"#\" id=\"bd_all\">hiện tất cả</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\">đang hiện <code>{0}</code> của <code>{1}</code> tệp; <a href=\"#\" id=\"bd_all\">hiện tất cả</a></div>',\n\t\"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\",\n\n\t\"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',\n\t\"f_dl_nd\": 'bỏ qua thư mục (hãy dùng tải zip/tar thay thế):\\n', //m\n\n\t\"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 <code>.PARTIAL</code>. 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 <code>.PARTIAL</code> thay vào đó, gần như chắc chắn dẫn đến dữ liệu bị hỏng.\",\n\n\t\"ft_paste\": \"dán {0} mục$NPhím tắt: ctrl-V\",\n\t\"fr_eperm\": \"không thể đổi tên:\\nbạn không có quyền “move” trong thư mục này\",\n\t\"fd_eperm\": \"không thể xóa:\\nbạn không có quyền “delete” trong thư mục này\",\n\t\"fc_eperm\": \"không thể cắt:\\nbạn không có quyền “move” trong thư mục này\",\n\t\"fp_eperm\": \"không thể dán:\\nbạn không có quyền “write” trong thư mục này\",\n\t\"fr_emore\": \"hãy chọn ít nhất một mục để đổi tên\",\n\t\"fd_emore\": \"hãy chọn ít nhất một mục để xóa\",\n\t\"fc_emore\": \"hãy chọn ít nhất một mục để cắt\",\n\t\"fcp_emore\": \"hãy chọn ít nhất một mục để sao chép vào bảng nhớ tạm\",\n\n\t\"fs_sc\": \"chia sẻ thư mục hiện tại\",\n\t\"fs_ss\": \"chia sẻ các tệp đã chọn\",\n\t\"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\",\n\t\"fs_abrt\": \"❌ hủy\",\n\t\"fs_rand\": \"🎲 tên ngẫu nhiên\",\n\t\"fs_go\": \"✅ tạo liên kết chia sẻ\",\n\t\"fs_name\": \"tên\",\n\t\"fs_src\": \"nguồn\",\n\t\"fs_pwd\": \"mật khẩu\",\n\t\"fs_exp\": \"hết hạn\",\n\t\"fs_tmin\": \"phút\",\n\t\"fs_thrs\": \"giờ\",\n\t\"fs_tdays\": \"ngày\",\n\t\"fs_never\": \"vĩnh viễn\",\n\t\"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\",\n\t\"fs_tsrc\": \"tệp hoặc thư mục cần chia sẻ\",\n\t\"fs_ppwd\": \"mật khẩu tùy chọn\",\n\t\"fs_w8\": \"đang tạo liên kết chia sẻ...\",\n\t\"fs_ok\": \"nhấn <code>Enter/OK</code> để chép vào bảng nhớ tạm\\nnhấn <code>ESC/Cancel</code> để đóng\",\n\n\t\"frt_dec\": \"có thể sửa một số trường hợp tên tệp bị lỗi\\\">url-decode\",\n\t\"frt_rst\": \"khôi phục tên gốc\\\">↺ reset\",\n\t\"frt_abrt\": \"hủy và đóng cửa sổ này\\\">❌ cancel\",\n\t\"frb_apply\": \"ÁP DỤNG ĐỔI TÊN\",\n\t\"fr_adv\": \"đổi tên theo lô / metadata / pattern\\\">advanced\",\n\t\"fr_case\": \"regex phân biệt hoa thường\\\">case\",\n\t\"fr_win\": \"tên tương thích Windows; thay <code>&lt;&gt;:&quot;\\\\|?*</code> bằng ký tự fullwidth tiếng Nhật\\\">win\",\n\t\"fr_slash\": \"thay <code>/</code> bằng ký tự khác để tránh tạo thư mục mới\\\">no /\",\n\t\"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)` ...\",\n\t\"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ố\",\n\t\"fr_pdel\": \"xóa\",\n\t\"fr_pnew\": \"lưu dưới tên mới\",\n\t\"fr_pname\": \"nhập tên cho preset mới\",\n\t\"fr_aborted\": \"đã hủy\",\n\t\"fr_lold\": \"tên cũ\",\n\t\"fr_lnew\": \"tên mới\",\n\t\"fr_tags\": \"tag của các tệp đã chọn (chỉ xem, không chỉnh sửa):\",\n\t\"fr_busy\": \"đang đổi tên {0} mục...\\n\\n{1}\",\n\t\"fr_efail\": \"đổi tên thất bại:\\n\",\n\t\"fr_nchg\": \"{0} tên mới đã bị chỉnh sửa do <code>win</code> và/hoặc <code>no /</code>\\n\\nTiếp tục với các tên đã chỉnh sửa?\",\n\n\t\"fd_ok\": \"hoàn tất xoá\",\n\t\"fd_err\": \"xoá gặp lỗi:\\n\",\n\t\"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)?\",\n\t\"fd_busy\": \"đang xóa {0} mục...\\n\\n{1}\",\n\t\"fd_warn1\": \"XÓA {0} mục này?\",\n\t\"fd_warn2\": \"<b>Cảnh báo cuối!</b> Không thể hoàn tác. Xóa?\",\n\n\t\"fc_ok\": \"đã cắt {0} mục\",\n\t\"fc_warn\": \"đã cắt {0} mục\\n\\nnhưng: chỉ <b>tab trình duyệt này</b> có thể dán\\n(vì lựa chọn quá lớn)\",\n\n\t\"fcc_ok\": \"đã sao chép {0} mục vào bảng nhớ tạm\",\n\t\"fcc_warn\": \"đã sao chép {0} mục vào bảng nhớ tạm\\n\\nnhưng: chỉ <b>tab trình duyệt này</b> có thể dán\\n(vì lựa chọn quá lớn)\",\n\n\t\"fp_apply\": \"dùng các tên này\",\n\t\"fp_skip\": \"bỏ qua xung đột\", //m\n\t\"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\",\n\t\"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\n\t\"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\n\t\"fp_emore\": \"vẫn còn xung đột tên tệp cần xử lý\",\n\t\"fp_ok\": \"di chuyển OK\",\n\t\"fcp_ok\": \"sao chép OK\",\n\t\"fp_busy\": \"đang di chuyển {0} mục...\\n\\n{1}\",\n\t\"fcp_busy\": \"đang sao chép {0} mục...\\n\\n{1}\",\n\t\"fp_abrt\": \"đang hủy...\",\n\t\"fp_err\": \"di chuyển thất bại:\\n\",\n\t\"fcp_err\": \"sao chép thất bại:\\n\",\n\t\"fp_confirm\": \"di chuyển {0} mục này vào đây?\",\n\t\"fcp_confirm\": \"sao chép {0} mục này vào đây?\",\n\t\"fp_etab\": \"không đọc được bảng nhớ tạm từ tab trình duyệt khác\",\n\t\"fp_name\": \"đang tải lên tệp từ thiết bị. Hãy đặt tên cho tệp:\",\n\t\"fp_both_m\": \"<h6>chọn thao tác để dán</h6><code>Enter</code> = Di chuyển {0} tệp từ «{1}»\\n<code>ESC</code> = Tải lên {2} tệp từ thiết bị\",\n\t\"fcp_both_m\": \"<h6>chọn thao tác để dán</h6><code>Enter</code> = Sao chép {0} tệp từ «{1}»\\n<code>ESC</code> = Tải lên {2} tệp từ thiết bị\",\n\t\"fp_both_b\": \"<a href=\\\"#\\\" id=\\\"modal-ok\\\">Di chuyển</a><a href=\\\"#\\\" id=\\\"modal-ng\\\">Tải lên</a>\",\n\t\"fcp_both_b\": \"<a href=\\\"#\\\" id=\\\"modal-ok\\\">Sao chép</a><a href=\\\"#\\\" id=\\\"modal-ng\\\">Tải lên</a>\",\n\n\t\"mk_noname\": \"hãy nhập tên vào ô bên trái trước khi thực hiện :p\",\n\t\"nmd_i1\": \"hãy thêm cả phần mở rộng tệp bạn muốn, ví dụ <code>.md</code>\",\n\t\"nmd_i2\": \"bạn chỉ có thể tạo tệp <code>.{0}</code> vì bạn không có quyền xóa\",\n\n\t\"tv_load\": \"Đang tải tài liệu văn bản:\\n\\n{0}\\n\\n{1}% ({2} / {3} MiB)\",\n\t\"tv_xe1\": \"không thể tải tệp văn bản:\\n\\nlỗi \",\n\t\"tv_xe2\": \"404, không tìm thấy tệp\",\n\t\"tv_lst\": \"danh sách các tệp văn bản trong\",\n\t\"tvt_close\": \"quay lại chế độ xem thư mục$NPhím tắt: M (hoặc Esc)\\\">❌ close\",\n\t\"tvt_dl\": \"tải xuống tệp này$NPhím tắt: Y\\\">💾 download\",\n\t\"tvt_prev\": \"hiển thị tài liệu trước đó$NPhím tắt: I\\\">⬆ prev\",\n\t\"tvt_next\": \"hiển thị tài liệu kế tiếp$NPhím tắt: K\\\">⬇ next\",\n\t\"tvt_sel\": \"chọn tệp &nbsp; (để cắt / sao chép / xóa / ...)$NPhím tắt: S\\\">sel\",\n\t\"tvt_j\": \"chuẩn hóa json$NPhím tắt: shift-J\\\">j\",\n\t\"tvt_edit\": \"mở tệp trong trình soạn thảo văn bản$NPhím tắt: E\\\">✏️ edit\",\n\t\"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\",\n\t\"tvt_wrap\": \"ngắt dòng\\\">↵\",\n\t\"tvt_atail\": \"khóa cuộn ở cuối trang\\\">⚓\",\n\t\"tvt_ctail\": \"giải mã màu terminal (ansi escape codes)\\\">🌈\",\n\t\"tvt_ntail\": \"giới hạn scrollback (số byte văn bản được giữ trong bộ nhớ)\",\n\n\t\"m3u_add1\": \"đã thêm 1 bài vào danh sách phát m3u\",\n\t\"m3u_addn\": \"đã thêm {0} bài vào danh sách phát m3u\",\n\t\"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\",\n\n\t\"gt_vau\": \"không hiện video, chỉ phát âm thanh\\\">🎧\",\n\t\"gt_msel\": \"bật chọn nhiều; ctrl-click để ghi đè$N$N&lt;em&gt;khi bật: nhấn đúp để mở tệp / thư mục&lt;/em&gt;$N$NPhím tắt: S\\\">multiselect\",\n\t\"gt_crop\": \"cắt chính giữa ảnh\\\">crop\",\n\n\t\"gt_3x\": \"ảnh độ nét cao\\\">3x\",\n\t\"gt_zoom\": \"zoom\",\n\t\"gt_chop\": \"chop\",\n\t\"gt_sort\": \"sắp xếp theo\",\n\t\"gt_name\": \"tên\",\n\t\"gt_sz\": \"dung lượng\",\n\t\"gt_ts\": \"ngày\",\n\t\"gt_ext\": \"loại\",\n\t\"gt_c1\": \"rút ngắn tên tệp hơn (hiện ít hơn)\",\n\t\"gt_c2\": \"rút ngắn tên tệp ít hơn (hiện nhiều hơn)\",\n\n\t\"sm_w8\": \"đang tìm...\",\n\t\"sm_prev\": \"kết quả bên dưới là từ lần tìm trước:\\n  \",\n\t\"sl_close\": \"đóng kết quả tìm kiếm\",\n\t\"sl_hits\": \"hiển thị {0} kết quả\",\n\t\"sl_moar\": \"tải thêm\",\n\n\t\"s_sz\": \"dung lượng\",\n\t\"s_dt\": \"ngày\",\n\t\"s_rd\": \"đường dẫn\",\n\t\"s_fn\": \"tên\",\n\t\"s_ta\": \"tag\",\n\t\"s_ua\": \"up@\",\n\t\"s_ad\": \"nâng cao\",\n\t\"s_s1\": \"tối thiểu MiB\",\n\t\"s_s2\": \"tối đa MiB\",\n\t\"s_d1\": \"ngày tối thiểu (iso8601)\",\n\t\"s_d2\": \"ngày tối đa (iso8601)\",\n\t\"s_u1\": \"tải lên sau\",\n\t\"s_u2\": \"và/hoặc trước\",\n\t\"s_r1\": \"đường dẫn chứa &nbsp; (cách nhau bằng dấu cách)\",\n\t\"s_f1\": \"tên chứa &nbsp; (thêm -nope để phủ định)\",\n\t\"s_t1\": \"tag chứa &nbsp; (^=bắt đầu, kết thúc=$)\",\n\t\"s_a1\": \"thuộc tính metadata cụ thể\",\n\n\t\"md_eshow\": \"không thể tải\",\n\t\"md_off\": \"[📜<em>readme</em>] đã tắt trong [⚙️] -- tài liệu bị ẩn\",\n\n\t\"badreply\": \"Không thể phân tích phản hồi từ máy chủ\",\n\n\t\"xhr403\": \"403: Access denied\\n\\nhãy thử nhấn F5, có thể bạn đã bị đăng xuất\",\n\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)\",\n\t\"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\",\n\t\"tl_xe1\": \"không thể liệt kê thư mục con:\\n\\nlỗi \",\n\t\"tl_xe2\": \"404: Không tìm thấy thư mục\",\n\t\"fl_xe1\": \"không thể liệt kê tệp trong thư mục:\\n\\nlỗi \",\n\t\"fl_xe2\": \"404: Không tìm thấy thư mục\",\n\t\"fd_xe1\": \"không thể tạo thư mục con:\\n\\nlỗi \",\n\t\"fd_xe2\": \"404: Không tìm thấy thư mục cha\",\n\t\"fsm_xe1\": \"không thể gửi tin nhắn:\\n\\nlỗi \",\n\t\"fsm_xe2\": \"404: Không tìm thấy thư mục cha\",\n\t\"fu_xe1\": \"không tải được danh sách unpost từ máy chủ:\\n\\nlỗi \",\n\t\"fu_xe2\": \"404: Không tìm thấy tệp??\",\n\n\t\"fz_tar\": \"file gnu-tar không nén (linux / mac)\",\n\t\"fz_pax\": \"file tar định dạng pax không nén (chậm hơn)\",\n\t\"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\",\n\t\"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\",\n\t\"fz_zip8\": \"zip với tên tệp utf8 (có thể lỗi trên windows 7 và cũ hơn)\",\n\t\"fz_zipd\": \"zip với tên tệp cp437 truyền thống, dành cho phần mềm rất cũ\",\n\t\"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)\",\n\n\t\"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\",\n\t\"un_upd\": \"làm mới\",\n\t\"un_m4\": \"hoặc chia sẻ các tệp hiển thị bên dưới:\",\n\t\"un_ulist\": \"hiện\",\n\t\"un_ucopy\": \"chép\",\n\t\"un_flt\": \"bộ lọc tùy chọn:&nbsp; đường dẫn phải chứa\",\n\t\"un_fclr\": \"xóa bộ lọc\",\n\t\"un_derr\": \"xóa unpost thất bại:\\n\",\n\t\"un_f5\": \"có gì đó lỗi rồi, hãy thử tải lại trang hoặc nhấn F5\",\n\t\"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\",\n\t\"un_nou\": \"<b>cảnh báo:</b> 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\",\n\t\"un_noc\": \"<b>cảnh báo:</b> 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ủ\",\n\t\"un_max\": \"đang hiển thị 2000 tệp đầu tiên (hãy dùng bộ lọc)\",\n\t\"un_avail\": \"có thể xóa {0} tải lên gần đây<br />{1} mục chưa hoàn tất có thể hủy\",\n\t\"un_m2\": \"sắp xếp theo thời gian tải lên; mới nhất ở trên:\",\n\t\"un_no1\": \"không có tải lên nào đủ mới\",\n\t\"un_no2\": \"không có tải lên nào đủ mới khớp với bộ lọc đó\",\n\t\"un_next\": \"xóa {0} tệp kế bên dưới\",\n\t\"un_abrt\": \"hủy\",\n\t\"un_del\": \"xóa\",\n\t\"un_m3\": \"đang tải danh sách tải lên gần đây...\",\n\t\"un_busy\": \"đang xóa {0} tệp...\",\n\t\"un_clip\": \"{0} liên kết đã chép vào bảng nhớ tạm\",\n\n\t\"u_https1\": \"bạn nên\",\n\t\"u_https2\": \"chuyển sang https\",\n\t\"u_https3\": \"để có hiệu suất tốt hơn\",\n\n\t\"u_ancient\": \"trình duyệt của bạn quá cũ; bạn có thể <a href=\\\"#\\\" onclick=\\\"goto('bup')\\\">dùng bup</a> thay thế\",\n\t\"u_nowork\": \"cần Firefox 53+, Chrome 57+ hoặc iOS 11+\",\n\t\"tail_2old\": \"cần Firefox 105+, Chrome 71+ hoặc iOS 14.5+\",\n\t\"u_nodrop\": \"trình duyệt của bạn quá cũ để dùng kéo thả khi tải lên\",\n\t\"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\",\n\n\t\"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\",\n\n\t\"u_enpot\": \"chuyển sang <a href=\\\"#\\\">giao diện đơn giản</a> (có thể tăng tốc độ tải lên)\",\n\t\"u_depot\": \"chuyển sang <a href=\\\"#\\\">giao diện đầy đủ</a> (có thể giảm tốc độ tải lên)\",\n\t\"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\",\n\n\t\"u_pott\": \"<p>tệp: &nbsp; <b>{0}</b> hoàn tất, &nbsp; <b>{1}</b> lỗi, &nbsp; <b>{2}</b> đang chạy, &nbsp; <b>{3}</b> chờ</p>\",\n\n\t\"u_ever\": \"đây là trình tải lên cơ bản; up2k yêu cầu<br>Chrome 21 // Firefox 13 // Edge 12 // Opera 12 // Safari 5.1 trở lên\",\n\t\"u_su2k\": \"đây là trình tải lên cơ bản; <a href=\\\"#\\\" id=\\\"u2yea\\\">up2k</a> tốt hơn\",\n\n\t\"u_uput\": \"tối ưu tốc độ (bỏ qua checksum)\",\n\t\"u_ewrite\": \"bạn không có quyền ghi vào thư mục này\",\n\t\"u_eread\": \"bạn không có quyền đọc thư mục này\",\n\t\"u_enoi\": \"tìm kiếm tệp chưa được bật trong cấu hình máy chủ\",\n\t\"u_enoow\": \"ghi đè không khả dụng; cần quyền Xóa\",\n\n\t\"u_badf\": \"Đã bỏ qua {0} tệp (trong tổng {1}) có thể do quyền hệ thống tệp:\\n\\n\",\n\t\"u_blankf\": \"{0} tệp (trong tổng {1}) trống; vẫn tải lên?\\n\\n\",\n\t\"u_applef\": \"{0} tệp (trong tổng {1}) có thể không cần thiết;\\nNhấn <code>OK/Enter</code> để BỎ QUA,\\nNhấn <code>Cancel/ESC</code> để giữ lại và tải lên:\\n\\n\",\n\n\t\"u_just1\": \"\\nCó thể sẽ hoạt động tốt hơn nếu bạn chỉ chọn một tệp\",\n\t\"u_ff_many\": \"nếu bạn dùng <b>Linux / macOS / Android</b> thì số lượng tệp này <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\"><em>có thể</em> làm Firefox crash</a>\\nnếu xảy ra, vui lòng thử lại hoặc dùng Chrome\",\n\n\t\"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\",\n\t\"u_asku\": \"tải {0} tệp này lên <code>{1}</code>\",\n\t\"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 🧯\",\n\n\t\"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?\",\n\t\"u_scan\": \"Đang quét tệp...\",\n\t\"u_dirstuck\": \"bộ lặp thư mục bị treo khi truy cập {0} mục sau; sẽ bỏ qua:\",\n\t\"u_etadone\": \"Hoàn tất ({0}, {1} tệp)\",\n\t\"u_etaprep\": \"(đang chuẩn bị tải lên)\",\n\t\"u_hashdone\": \"hash hoàn tất\",\n\t\"u_hashing\": \"hash\",\n\t\"u_hs\": \"đang bắt tay...\",\n\t\"u_started\": \"tệp đang được tải lên; xem biểu tượng [🚀]\",\n\t\"u_dupdefer\": \"bị trùng; sẽ xử lý sau khi hoàn tất các tệp khác\",\n\n\t\"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\",\n\t\"u_fixed\": \"OK;&nbsp; đã sửa 👍\",\n\n\t\"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}\",\n\t\"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 \",\n\t\"u_ehstmp\": \"sẽ thử lại; xem góc dưới bên phải\",\n\t\"u_ehsfin\": \"máy chủ từ chối yêu cầu hoàn tất tải lên; đang thử lại...\",\n\t\"u_ehssrch\": \"máy chủ từ chối yêu cầu tìm kiếm; đang thử lại...\",\n\t\"u_ehsinit\": \"máy chủ từ chối yêu cầu bắt đầu tải lên; đang thử lại...\",\n\t\"u_eneths\": \"lỗi mạng khi thực hiện bắt tay; đang thử lại...\",\n\t\"u_enethd\": \"lỗi mạng khi kiểm tra tồn tại mục tiêu; đang thử lại...\",\n\t\"u_cbusy\": \"chờ máy chủ cho phép tiếp tục sau sự cố mạng...\",\n\t\"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\",\n\n\t\"u_emtleak1\": \"có dấu hiệu cho thấy trình duyệt có thể bị rò rỉ bộ nhớ;\\nvui lòng\",\n\t\"u_emtleak2\": \" <a href=\\\"{0}\\\">chuyển sang https (khuyến nghị)</a> hoặc \",\n\t\"u_emtleak3\": \" \",\n\t\"u_emtleakc\": \"thử cách sau:\\n<ul><li>nhấn <code>F5</code> để tải lại trang</li><li>sau đó tắt nút &nbsp;<code>mt</code>&nbsp; trong &nbsp;<code>⚙️ cài đặt</code></li><li>và thử tải lại</li></ul>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 <a href=\\\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\\\" target=\\\"_blank\\\">đã có bản sửa</a>\",\n\t\"u_emtleakf\": \"thử cách sau:\\n<ul><li>nhấn <code>F5</code> để tải lại trang</li><li>sau đó bật <code>🥔</code> (potato) trong giao diện tải lên<li>và thử lại</li></ul>\\nPS: firefox <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\">hy vọng sẽ có bản sửa</a>\",\n\n\t\"u_s404\": \"không tìm thấy trên máy chủ\",\n\t\"u_expl\": \"giải thích\",\n\t\"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 <code>connections-per-server</code> trong <code>about:config</code>\",\n\n\t\"u_tu\": \"<p class=\\\"warn\\\">CẢNH BÁO: turbo đang bật <span>&nbsp;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</span></p>\",\n\t\"u_ts\": \"<p class=\\\"warn\\\">CẢNH BÁO: turbo đang bật <span>&nbsp;kết quả tìm kiếm có thể không chính xác; xem tooltip nút turbo</span></p>\",\n\n\t\"u_turbo_c\": \"turbo bị tắt trong cấu hình máy chủ\",\n\t\"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\",\n\n\t\"u_life_cfg\": \"tự xóa sau <input id=\\\"lifem\\\" p=\\\"60\\\" /> phút (hoặc <input id=\\\"lifeh\\\" p=\\\"3600\\\" /> giờ)\",\n\t\"u_life_est\": \"tệp sẽ bị xóa <span id=\\\"lifew\\\" tt=\\\"local time\\\">---</span>\",\n\t\"u_life_max\": \"thư mục này áp dụng\\nthời gian tồn tại tối đa {0}\",\n\n\t\"u_unp_ok\": \"cho phép unpost trong {0}\",\n\t\"u_unp_ng\": \"không cho phép unpost\",\n\n\t\"ue_ro\": \"bạn chỉ có quyền đọc thư mục này\\n\\n\",\n\t\"ue_nl\": \"bạn chưa đăng nhập\",\n\t\"ue_la\": \"bạn đang đăng nhập với tài khoản \\\"{0}\\\"\",\n\t\"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\",\n\t\"ue_ta\": \"hãy thử tải lên lại; giờ có thể được\",\n\t\"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 🧯\",\n\n\t\"ur_1uo\": \"OK: tệp đã tải lên thành công\",\n\t\"ur_auo\": \"OK: toàn bộ {0} tệp đã tải lên thành công\",\n\t\"ur_1so\": \"OK: đã tìm thấy tệp trên máy chủ\",\n\t\"ur_aso\": \"OK: đã tìm thấy toàn bộ {0} tệp trên máy chủ\",\n\n\t\"ur_1un\": \"Tải lên thất bại\",\n\t\"ur_aun\": \"{0} lần tải lên đều thất bại\",\n\n\t\"ur_1sn\": \"KHÔNG tìm thấy tệp trên máy chủ\",\n\t\"ur_asn\": \"{0} tệp KHÔNG tìm thấy trên máy chủ\",\n\n\t\"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\",\n\t\"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\",\n\n\t\"rc_opn\": \"mở\", //m\n\t\"rc_ply\": \"phát\", //m\n\t\"rc_pla\": \"phát dưới dạng âm thanh\", //m\n\t\"rc_txt\": \"mở trong trình xem tệp\", //m\n\t\"rc_md\": \"mở trong trình soạn thảo văn bản\", //m\n\t\"rc_dl\": \"tải xuống\", //m\n\t\"rc_zip\": \"tải xuống dưới dạng gói nén\", //m\n\t\"rc_cpl\": \"sao chép liên kết\", //m\n\t\"rc_del\": \"xóa\", //m\n\t\"rc_cut\": \"cắt\", //m\n\t\"rc_cpy\": \"sao chép\", //m\n\t\"rc_pst\": \"dán\", //m\n\t\"rc_rnm\": \"đổi tên\", //m\n\t\"rc_nfo\": \"thư mục mới\", //m\n\t\"rc_nfi\": \"tệp mới\", //m\n\t\"rc_sal\": \"chọn tất cả\", //m\n\t\"rc_sin\": \"đảo ngược lựa chọn\", //m\n\t\"rc_shf\": \"chia sẻ thư mục này\", //m\n\t\"rc_shs\": \"chia sẻ lựa chọn\", //m\n\n\t\"lang_set\": \"tải lại trang để áp dụng thay đổi ngôn ngữ\",\n\n\t\"splash\": {\n\t\t\"a1\": \"tải lại\",\n\t\t\"b1\": \"xin chào khách &nbsp; <small>(bạn chưa đăng nhập)</small>\",\n\t\t\"c1\": \"đăng xuất\",\n\t\t\"d1\": \"ghi lại ngăn xếp\",\n\t\t\"d2\": \"hiển thị trạng thái của tất cả các luồng đang hoạt động\",\n\t\t\"e1\": \"tải lại cấu hình\",\n\t\t\"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\",\n\t\t\"f1\": \"bạn có thể duyệt:\",\n\t\t\"g1\": \"bạn có thể tải lên:\",\n\t\t\"cc1\": \"thứ khác:\",\n\t\t\"h1\": \"vô hiệu hoá k304\",\n\t\t\"i1\": \"bật k304\",\n\t\t\"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), <em>nhưng</em> nó cũng sẽ làm mọi thứ chậm hơn\",\n\t\t\"k1\": \"đặt lại cài đặt client\",\n\t\t\"l1\": \"đăng nhập để có thêm:\",\n\t\t\"m1\": \"chào mừng trở lại,\",\n\t\t\"n1\": \"404 không tìm thấy &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": 'hoặc có thể bạn không có quyền truy cập -- thử mật khẩu hoặc <a href=\"' + SR + '/?h\">về trang chủ</a>',\n\t\t\"p1\": \"403 bị cấm &nbsp;~┻━┻\",\n\t\t\"q1\": 'sử dụng mật khẩu hoặc <a href=\"' + SR + '/?h\">về trang chủ</a>',\n\t\t\"r1\": \"về trang chủ\",\n\t\t\".s1\": \"quét lại\",\n\t\t\"t1\": \"hành động\",\n\t\t\"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\",\n\t\t\"v1\": \"kết nối\",\n\t\t\"v2\": \"sử dụng máy chủ này như một ổ cứng cục bộ\",\n\t\t\"w1\": \"chuyển sang https\",\n\t\t\"x1\": \"đổi mật khẩu\",\n\t\t\"y1\": \"chỉnh sửa chia sẻ\",\n\t\t\"z1\": \"mở khóa chia sẻ này:\",\n\t\t\"ta1\": \"điền mật khẩu mới:\",\n\t\t\"ta2\": \"nhập lại mật khẩu mới:\",\n\t\t\"ta3\": \"mật khẩu không khớp, xin hãy thử lại\",\n\t\t\"nop\": \"LỖI: Mật khẩu không được để trống\", //m\n\t\t\"nou\": \"LỖI: Tên người dùng và/hoặc mật khẩu không được để trống\", //m\n\t\t\"aa1\": \"tệp đến\",\n\t\t\"ab1\": \"vô hiệu hóa no304\",\n\t\t\"ac1\": \"bật no304\",\n\t\t\"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!\",\n\t\t\"ae1\": \"tải xuống đang hoạt động:\",\n\t\t\"af1\": \"hiển thị các tệp đã tải lên gần đây\",\n\t\t\"ag1\": \"hiển thị người dùng IdP đã biết\", //m\n\t}\n};\n"
  },
  {
    "path": "copyparty/web/ui.css",
    "content": ":root {\n\t--font-main: sans-serif;\n\t--font-serif: serif;\n\t--font-mono: 'scp';\n\n\t--fg: #ccc;\n\t--fg-max: #fff;\n\t--bg-u2: #2b2b2b;\n\t--bg-u5: #444;\n}\nhtml.y {\n\t--fg: #222;\n\t--fg-max: #000;\n\t--bg-u2: #f7f7f7;\n\t--bg-u5: #ccc;\n}\nhtml.bz {\n\t--bg-u2: #202231;\n}\n@font-face {\n\tfont-family: 'scp';\n\tfont-display: swap;\n\tsrc: local('Source Code Pro Regular'), local('SourceCodePro-Regular'), url(deps/scp.woff2) format('woff2');\n}\nhtml {\n\ttext-size-adjust: 100%;\n\t-webkit-text-size-adjust: 100%;\n\ttouch-action: manipulation;\n}\n#tt, #toast {\n\tposition: fixed;\n\tmax-width: 34em;\n\tmax-width: min(34em, 90%);\n\tmax-width: min(34em, calc(100% - 7em));\n\tcolor: #ddd;\n\tcolor: var(--fg);\n\tbackground: #333;\n\tbackground: var(--bg-u2);\n\tborder: 0 solid #777;\n\tbox-shadow: 0 .2em .5em #111;\n\tborder-radius: .4em;\n\tz-index: 9001;\n}\n#tt {\n\tmax-width: min(34em, calc(100% - 3.3em));\n\toverflow: hidden;\n\tmargin: .7em 0;\n\tpadding: 0 1.3em;\n\theight: 0;\n\topacity: .1;\n\ttransition: opacity 0.14s, height 0.14s, padding 0.14s;\n}\n#toast {\n\tbottom: 5em;\n\tright: -1em;\n\tline-height: 1.5em;\n\tpadding: 1em 1.3em;\n\tmargin-left: 3em;\n\tborder-width: .4em 0;\n\toverflow-wrap: break-word;\n\ttransform: translateX(100%);\n\ttransition:\n\t\ttransform .4s cubic-bezier(.2, 1.2, .5, 1),\n\t\tright .4s cubic-bezier(.2, 1.2, .5, 1);\n\ttext-shadow: 1px 1px 0 #000;\n\tcolor: #fff;\n}\n#toast.top {\n\ttop: 2em;\n\tbottom: unset;\n}\n#toastt {\n\tposition: absolute;\n\theight: 1px;\n\ttop: 1px;\n\tright: 1px;\n\tleft: 1px;\n\tanimation: toastt var(--tmtime) 0.07s steps(var(--tmstep)) forwards;\n\ttransform-origin: right;\n}\n@keyframes toastt {\n\tto {transform: scaleX(0)}\n}\n#toast a {\n\tcolor: inherit;\n\ttext-shadow: inherit;\n\tbackground: rgba(0, 0, 0, 0.4);\n\tborder-radius: .3em;\n\tpadding: .2em .3em;\n}\n#toast a#toastc {\n\tdisplay: inline-block;\n\tposition: absolute;\n\toverflow: hidden;\n\tleft: 0;\n\twidth: 0;\n\topacity: 0;\n\tpadding: .3em 0;\n\tmargin: -.3em 0 0 0;\n\tline-height: 1.3em;\n\tcolor: #000;\n\tborder: none;\n\toutline: none;\n\ttext-shadow: none;\n\tborder-radius: .5em 0 0 .5em;\n\ttransition: left .3s, width .3s, padding .3s, opacity .3s;\n}\n#toastb {\n\tmax-height: 70vh;\n\toverflow-y: auto;\n\tpadding: .1em;\n}\n#toast.scroll #toastb {\n\toverflow-y: scroll;\n\tmargin-right: -1.2em;\n\tpadding-right: .7em;\n}\n#toast.r #toastb {\n\ttext-align: right;\n}\n#toast pre {\n\tmargin: 0;\n}\n#toast.hide {\n\tdisplay: none;\n}\n#toast.vis {\n\tright: 1.3em;\n\ttransform: inherit;\n\ttransform: initial;\n}\n#toast.vis #toastc {\n\tleft: -2em;\n\twidth: .4em;\n\tpadding: .3em .8em;\n\topacity: 1;\n}\n#toast.inf {\n\tbackground: #07a;\n\tborder-color: #0be;\n}\n#toast.inf #toastc {\n\tbackground: #0be;\n}\n#toast.inf #toastt {\n\tbackground: #8ef;\n}\n#toast.ok {\n\tbackground: #380;\n\tborder-color: #8e4;\n}\n#toast.ok #toastc {\n\tbackground: #8e4;\n}\n#toast.ok #toastt {\n\tbackground: #cf9;\n}\n#toast.warn {\n\tbackground: #960;\n\tborder-color: #fc0;\n}\n#toast.warn #toastc {\n\tbackground: #fc0;\n}\n#toast.warn #toastt {\n\tbackground: #fe9;\n}\n#toast.err {\n\tbackground: #900;\n\tborder-color: #d06;\n}\n#toast.err #toastc {\n\tbackground: #d06;\n}\n#toast.err #toastt {\n\tbackground: #f9c;\n}\n#toast code {\n\tpadding: 0 .2em;\n\tbackground: rgba(0,0,0,0.2);\n}\n#tth {\n\tcolor: #fff;\n\tbackground: #111;\n\tfont-size: .9em;\n\tpadding: 0 .26em;\n\tline-height: .97em;\n\tborder-radius: 1em;\n\tposition: absolute;\n\tdisplay: none;\n}\n#tth.act {\n\tdisplay: block;\n\tz-index: 9001;\n}\n#tt.b {\n\tpadding: 0 2em;\n\tborder-radius: .5em;\n\tbox-shadow: 0 .2em 1em #000;\n}\n#tt.show {\n\tpadding: 1em 1.3em;\n\tborder-width: .4em 0;\n\theight: auto;\n\topacity: 1;\n}\n#tt.show.b {\n\tpadding: 1.5em 2em;\n\tborder-width: .5em 0;\n}\n.logue code,\n#modalc code,\n#tt code {\n\tcolor: #eee;\n\tcolor: var(--fg-max);\n\tbackground: #444;\n\tbackground: var(--bg-u5);\n\tpadding: .1em .3em;\n\tborder-radius: .3em;\n\tline-height: 1.7em;\n}\n#tt em {\n\tcolor: #f6a;\n}\nhtml.y #tt {\n\tborder-color: #888 #000 #777 #000;\n}\nhtml.bz #tt {\n\tborder-color: #3b3f58;\n}\nhtml.y #tt,\nhtml.y #toast {\n\tbox-shadow: 0 .3em 1em rgba(0,0,0,0.4);\n}\n#modalc code {\n\tcolor: #060;\n\tbackground: transparent;\n\tborder: 1px solid #ccc;\n}\nhtml.y #tt em {\n\tcolor: #d38;\n}\nhtml.y #tth {\n\tcolor: #000;\n\tbackground: #fff;\n}\n#cf_frame {\n\tposition: fixed;\n\tz-index: 573;\n\ttop: 3em;\n\tleft: 50%;\n\twidth: 40em;\n\theight: 30em;\n\tmargin-left: -20.2em;\n\tborder-radius: .4em;\n\tborder: .4em solid var(--fg);\n\tbox-shadow: 0 2em 4em 1em var(--bg-max);\n}\n#hkhelp,\n#modal {\n\tposition: fixed;\n    overflow: auto;\n\ttop: 0;\n\tleft: 0;\n\tright: 0;\n\tbottom: 0;\n\twidth: 100%;\n\theight: 100%;\n\tz-index: 9001;\n\tbackground: rgba(64,64,64,0.6);\n}\n#modal>table {\n    width: 100%;\n    height: 100%;\n}\n#modal td {\n\ttext-align: center;\n}\n#modalc {\n\tposition: relative;\n\tdisplay: inline-block;\n\tbackground: #f7f7f7;\n\tcolor: #333;\n\ttext-shadow: none;\n\ttext-align: left;\n\tmargin: 3em;\n\tpadding: 1em 1.1em;\n\tborder-radius: .6em;\n    box-shadow: 0 .3em 3em rgba(0,0,0,0.5);\n\tmax-width: 50em;\n\tmax-height: 30em;\n\toverflow-x: auto;\n\toverflow-y: scroll;\n}\n#modalc.yk {\n\toverflow-y: auto;\n}\n#modalc td {\n\ttext-align: unset;\n\tpadding: .2em;\n}\n@media (min-width: 40em) {\n    #modalc {\n        min-width: 30em;\n    }\n}\n#modalc li {\n\tmargin: 1em 0;\n}\n#modalc h6 {\n\tfont-size: 1.3em;\n\tborder-bottom: 1px solid #999;\n\tmargin: 0;\n\tpadding: .3em;\n\ttext-align: center;\n}\n#modalc a {\n\tcolor: #07b;\n}\n#modalc .b64 {\n\tdisplay: block;\n\tmargin: .1em auto;\n\twidth: 60%;\n\theight: 60%;\n\tbackground: #999;\n\tbackground: rgba(128,128,128,0.2);\n}\n#modalb {\n\tposition: sticky;\n\ttext-align: right;\n\tpadding-top: 1em;\n\tbottom: 0;\n\tright: 0;\n}\n#modalb a {\n\tcolor: #000;\n\tbackground: #ccc;\n\tdisplay: inline-block;\n\tborder-radius: .3em;\n\tpadding: .5em 1em;\n\toutline: none;\n\tborder: none;\n}\n#modalb a:focus,\n#modalb a:hover {\n\tbackground: #06d;\n\tcolor: #fff;\n}\n#modalb a+a {\n\tmargin-left: .5em;\n}\n#modali {\n\tdisplay: block;\n\tbackground: #fff;\n\tcolor: #000;\n\twidth: calc(100% - 1.25em);\n    margin: 1em -.1em 0 -.1em;\n\tpadding: .5em;\n\toutline: none;\n\tborder: .25em solid #ccc;\n\tborder-radius: .4em;\n}\n#modali:focus {\n\tborder-color: #06d;\n}\n#repl_pre {\n\tmax-width: 24em;\n}\n*:focus,\n*:focus+label,\n#pctl *:focus,\n.btn:focus {\n\tbox-shadow: 0 .1em .2em #fc0 inset;\n\toutline: #fc0 solid .1em;\n\tborder-radius: .2em;\n}\nhtml.y *:focus,\nhtml.y *:focus+label,\nhtml.y #pctl *:focus,\nhtml.y .btn:focus {\n\tbox-shadow: 0 .1em .2em #037 inset;\n\toutline: #037 solid .1em;\n}\ninput, button {\n\tfont-family: var(--font-main), sans-serif;\n}\ninput[type=\"submit\"] {\n\tcursor: pointer;\n}\ninput[type=\"text\"]:focus,\ninput:not([type]):focus,\ntextarea:focus {\n\tbox-shadow: 0 .1em .3em #fc0, 0 -.1em .3em #fc0;\n}\nhtml.y input[type=\"text\"]:focus,\nhtml.y input:not([type]):focus,\nhtml.y textarea:focus {\n\tbox-shadow: 0 .1em .3em #037, 0 -.1em .3em #037;\n}\n\n\n\n\n\n\n\n\n\n\n\n.mdo pre,\n.mdo code,\n.mdo a {\n\tcolor: #480;\n\tbackground: #f7f7f7;\n\tborder: .07em solid #ddd;\n\tborder-radius: .2em;\n\tpadding: .1em .3em;\n\tmargin: 0 .1em;\n}\n.mdo pre,\n.mdo code,\n.mdo code[class*=\"language-\"],\n.mdo tt {\n\tfont-family: 'scp', monospace, monospace;\n\tfont-family: var(--font-mono), 'scp', monospace, monospace;\n\twhite-space: pre-wrap;\n\tword-break: break-all;\n}\n.mdo code {\n\tfont-size: .96em;\n}\nhtml.z .mdo a>code,\nhtml.y .mdo a>code {\n\tcolor: inherit;\n\tbackground: inherit;\n\tbackground: rgba(0, 0, 0, 0.2);\n\tpadding-top: 0;\n\tpadding-bottom: 0;\n\tborder: none;\n}\n.mdo h1,\n.mdo h2 {\n\tline-height: 1.5em;\n}\n.mdo h1 {\n\tfont-size: 1.7em;\n\ttext-align: center;\n\tborder: 1em solid #777;\n\tborder-width: .05em 0;\n\tmargin: 3em 0;\n}\n.mdo h2 {\n\tfont-size: 1.5em;\n\tfont-weight: normal;\n\tbackground: #f7f7f7;\n\tborder-top: .07em solid #fff;\n\tborder-bottom: .07em solid #bbb;\n\tborder-radius: .5em .5em 0 0;\n\tpadding-left: .4em;\n\tmargin-top: 3em;\n}\n.mdo h3 {\n\tborder-bottom: .1em solid #999;\n}\n.mdo h1 a, .mdo h3 a, .mdo h5 a,\n.mdo h2 a, .mdo h4 a, .mdo h6 a {\n\tcolor: inherit;\n\tdisplay: block;\n\tbackground: none;\n\tborder: none;\n\tpadding: 0;\n\tmargin: 0;\n}\n.mdo ul,\n.mdo ol {\n\tpadding-left: 1em;\n}\n.mdo ul ul,\n.mdo ul ol,\n.mdo ol ul,\n.mdo ol ol {\n\tpadding-left: 2em;\n\tborder-left: .3em solid #ddd;\n}\n.mdo ul>li {\n\tmargin: .7em 0;\n\tlist-style-type: disc;\n}\n.mdo ol>li {\n\tmargin: .7em 0 .7em 2em;\n}\n.mdo strong {\n\tcolor: #000;\n}\n.mdo p>em,\n.mdo li>em,\n.mdo td>em {\n\tcolor: #c50;\n\tpadding: .1em;\n\tborder-bottom: .1em solid #bbb;\n}\n.mdo blockquote {\n\tfont-family: serif;\n\tfont-family: var(--font-serif), serif;\n\tbackground: #f7f7f7;\n\tborder: .07em dashed #ccc;\n\tpadding: 0 2em;\n\tmargin: 1em 0;\n}\n.mdo small {\n\topacity: .8;\n}\n.mdo pre code {\n\tdisplay: block;\n\tmargin: 0 -.3em;\n\tpadding: .4em .5em;\n\tline-height: 1.1em;\n}\n.mdo pre code:hover {\n\tbackground: #fec;\n\tcolor: #360;\n}\n.mdo table {\n\tborder-collapse: collapse;\n\tmargin: 1em 0;\n}\n.mdo th,\n.mdo td {\n\tpadding: .2em .5em;\n\tborder: .12em solid #aaa;\n}\n.mdo .mdth,\n.mdo .mdthl,\n.mdo .mdthr {\n\tmargin: .5em .5em .5em 0;\n}\n.mdthl {\n\tfloat: left;\n}\n.mdthr {\n\tfloat: right;\n}\nhr {\n\tclear: both;\n}\n\n@media screen {\n\t.mdo {\n\t\tword-break: break-word;\n\t\toverflow-wrap: break-word;\n\t\tword-wrap: break-word; /*ie*/\n\t}\n\thtml.y .mdo a,\n\t.mdo a {\n\t\tcolor: #fff;\n\t\tbackground: #39b;\n\t\ttext-decoration: none;\n\t\tpadding: 0 .3em;\n\t\tborder: none;\n\t\tborder-bottom: .07em solid #079;\n\t}\n\t.mdo h1 {\n\t\tcolor: #fff;\n\t\tbackground: #444;\n\t\tfont-weight: normal;\n\t\tborder-top: .4em solid #fb0;\n\t\tborder-bottom: .4em solid #777;\n\t\tborder-radius: 0 1em 0 1em;\n\t\tmargin: 3em 0 1em 0;\n\t\tpadding: .5em 0;\n\t}\n\t.mdo h2 {\n\t\tcolor: #fff;\n\t\tbackground: #555;\n\t\tmargin-top: 2em;\n\t\tborder-bottom: .22em solid #999;\n\t\tborder-top: none;\n\t}\n\n\n\n\thtml.z .mdo a {\n\t\tbackground: #057;\n\t}\n\thtml.z .mdo h1 a, html.z .mdo h4 a,\n\thtml.z .mdo h2 a, html.z .mdo h5 a,\n\thtml.z .mdo h3 a, html.z .mdo h6 a {\n\t\tcolor: inherit;\n\t\tbackground: none;\n\t}\n\thtml.z .mdo pre,\n\thtml.z .mdo code {\n\t\tcolor: #8c0;\n\t\tbackground: #1a1a1a;\n\t\tborder: .07em solid #333;\n\t}\n\thtml.z .mdo ul,\n\thtml.z .mdo ol {\n\t\tborder-color: #444;\n\t}\n\thtml.z .mdo strong {\n\t\tcolor: #fff;\n\t}\n\thtml.z .mdo p>em,\n\thtml.z .mdo li>em,\n\thtml.z .mdo td>em {\n\t\tcolor: #f94;\n\t\tborder-color: #666;\n\t}\n\thtml.z .mdo h1 {\n\t\tbackground: #383838;\n\t\tborder-top: .4em solid #b80;\n\t\tborder-bottom: .4em solid #4c4c4c;\n\t}\n\thtml.bz .mdo h1 {\n\t\tbackground: #202231;\n\t\tborder: 1px solid #2d2f45;\n\t\tborder-width: 0 0 .4em 0;\n\t}\n\thtml.z .mdo h2 {\n\t\tbackground: #444;\n\t\tborder-bottom: .22em solid #555;\n\t}\n\thtml.bz .mdo h2,\n\thtml.bz .mdo h3 {\n\t\tbackground: transparent;\n\t\tborder-color: #3b3f58;\n\t}\n\thtml.z .mdo td,\n\thtml.z .mdo th {\n\t\tborder-color: #444;\n\t}\n\thtml.z .mdo blockquote {\n\t\tbackground: #282828;\n\t\tborder: .07em dashed #444;\n\t}\n}\n\n@media (prefers-reduced-motion) {\n\t#toast,\n\t#toast a#toastc,\n\t#tt {\n\t\ttransition: none;\n\t}\n}"
  },
  {
    "path": "copyparty/web/up2k.js",
    "content": "\"use strict\";\n\nvar J_U2K = 1;\n\n(function () {\n    var x = sread('nosubtle');\n    if (x === '0' || x === '1')\n        nosubtle = parseInt(x);\n    if ((nosubtle > 1 && !CHROME && !FIREFOX) ||\n        (nosubtle > 2 && !CHROME) ||\n        (CHROME && nosubtle > VCHROME) ||\n        !WebAssembly)\n        nosubtle = 0;\n})();\n\n\nfunction goto_up2k() {\n    if (up2k === false)\n        return goto('bup');\n\n    if (!up2k)\n        return setTimeout(goto_up2k, 100);\n\n    up2k.init_deps();\n}\n\n\n// chrome requires https to use crypto.subtle,\n// usually it's undefined but some chromes throw on invoke\nvar up2k = null,\n    up2k_hooks = [],\n    hws = [],\n    hws_ok = 0,\n    hws_ng = false,\n    sha_js = WebAssembly ? 'hw' : 'ac',  // ff53,c57,sa11\n    m = 'will use ' + sha_js + ' instead of native sha512 due to';\n\ntry {\n    if (nosubtle)\n        throw 'chickenbit';\n    var cf = crypto.subtle || crypto.webkitSubtle;\n    cf.digest('SHA-512', new Uint8Array(1)).then(\n        function (x) { console.log('sha-ok'); up2k = up2k_init(cf); },\n        function (x) { console.log(m, x); up2k = up2k_init(false); }\n    );\n}\ncatch (ex) {\n    console.log(m, ex);\n    try {\n        up2k = up2k_init(false);\n    }\n    catch (ex) {\n        console.log('up2k init failed:', ex);\n        toast.err(10, 'could not initialize up2k\\n\\n' + basenames(ex));\n    }\n}\ntreectl.onscroll();\n\n\nfunction up2k_flagbus() {\n    var flag = {\n        \"id\": Math.floor(Math.random() * 1024 * 1024 * 1023 * 2),\n        \"ch\": new BroadcastChannel(\"up2k_flagbus\"),\n        \"ours\": false,\n        \"owner\": null,\n        \"wants\": null,\n        \"act\": false,\n        \"last_tx\": [\"x\", null]\n    };\n    var dbg = function (who, msg) {\n        console.log('flagbus(' + flag.id + '): [' + who + '] ' + msg);\n    };\n    flag.ch.onmessage = function (e) {\n        var who = e.data[0],\n            what = e.data[1];\n\n        if (who == flag.id) {\n            dbg(who, 'hi me (??)');\n            return;\n        }\n        flag.act = Date.now();\n        if (what == \"want\") {\n            // lowest id wins, don't care if that's us\n            if (who < flag.id) {\n                dbg(who, 'wants (ack)');\n                flag.wants = [who, flag.act];\n            }\n            else {\n                dbg(who, 'wants (ign)');\n            }\n        }\n        else if (what == \"have\") {\n            dbg(who, 'have');\n            flag.owner = [who, flag.act];\n        }\n        else if (what == \"give\") {\n            if (flag.owner && flag.owner[0] == who) {\n                flag.owner = null;\n                dbg(who, 'give (ok)');\n            }\n            else {\n                dbg(who, 'give, INVALID, ' + flag.owner);\n            }\n        }\n        else if (what == \"hi\") {\n            dbg(who, 'hi');\n            flag.ch.postMessage([flag.id, \"hey\"]);\n        }\n        else {\n            dbg('?', e.data);\n        }\n    };\n    var tx = function (now, msg) {\n        var td = now - flag.last_tx[1];\n        if (td > 500 || flag.last_tx[0] != msg) {\n            dbg('*', 'tx ' + msg);\n            flag.ch.postMessage([flag.id, msg]);\n            flag.last_tx = [msg, now];\n        }\n    };\n    var do_take = function (now) {\n        tx(now, \"have\");\n        flag.owner = [flag.id, now];\n        flag.ours = true;\n    };\n    var do_want = function (now) {\n        tx(now, \"want\");\n    };\n    flag.take = function (now) {\n        if (flag.ours) {\n            do_take(now);\n            return;\n        }\n        if (flag.owner && now - flag.owner[1] > 12000) {\n            flag.owner = null;\n        }\n        if (flag.wants && now - flag.wants[1] > 12000) {\n            flag.wants = null;\n        }\n        if (!flag.owner && !flag.wants) {\n            do_take(now);\n            return;\n        }\n        do_want(now);\n    };\n    flag.give = function () {\n        dbg('#', 'put give');\n        flag.ch.postMessage([flag.id, \"give\"]);\n        flag.owner = null;\n        flag.ours = false;\n    };\n    flag.ch.postMessage([flag.id, 'hi']);\n    return flag;\n}\n\n\nfunction U2pvis(act, btns, uc, st) {\n    var r = this;\n    r.act = act;\n    r.ctr = { \"ok\": 0, \"ng\": 0, \"bz\": 0, \"q\": 0 };\n    r.tab = [];\n    r.hq = {};\n    r.head = 0;\n    r.tail = -1;\n    r.wsz = 3;\n    r.npotato = 99;\n    r.modn = 0;\n    r.modv = 0;\n    r.mod0 = null;\n\n    var markup = {\n        '404': '<span class=\"err\">' + L.utl_404 + '</span>',\n        'ERROR': '<span class=\"err\">' + L.utl_err + '</span>',\n        'OS-error': '<span class=\"err\">' + L.utl_oserr + '</span>',\n        'found': '<span class=\"inf\">' + L.utl_found + '</span>',\n        'defer': '<span class=\"inf\">' + L.utl_defer + '</span>',\n        'YOLO': '<span class=\"inf\">' + L.utl_yolo + '</span>',\n        'done': '<span class=\"ok\">' + L.utl_done + '</span>',\n    };\n\n    r.addfile = function (entry, sz, draw) {\n        r.tab.push({\n            \"hn\": entry[0],\n            \"ht\": entry[1],\n            \"hp\": entry[2],\n            \"in\": 'q',\n            \"nh\": 0, //hashed\n            \"nd\": 0, //done\n            \"cb\": [], // bytes done in chunk\n            \"bt\": sz, // bytes total\n            \"bd\": 0,  // bytes done\n            \"bd0\": 0  // upload start\n        });\n        r.ctr[\"q\"]++;\n        if (!draw)\n            return;\n\n        r.drawcard(\"q\");\n        if (r.act == \"q\") {\n            r.addrow(r.tab.length - 1);\n        }\n        if (r.act == \"bz\") {\n            r.bzw();\n        }\n    };\n\n    r.is_act = function (card) {\n        if (uc.potato && !uc.fsearch)\n            return false;\n\n        if (r.act == \"done\")\n            return card == \"ok\" || card == \"ng\";\n\n        return r.act == card;\n    }\n\n    r.seth = function (nfile, field, html) {\n        var fo = r.tab[nfile];\n        field = ['hn', 'ht', 'hp'][field];\n        if (fo[field] === html)\n            return;\n\n        fo[field] = html;\n        if (!r.is_act(fo.in))\n            return;\n\n        var k = 'f' + nfile + '' + field.slice(1),\n            obj = ebi(k);\n\n        obj.innerHTML = field == 'ht' ? (markup[html] || html) : html;\n        if (field == 'hp') {\n            obj.style.color = '';\n            obj.style.background = '';\n            delete r.hq[nfile];\n        }\n    };\n\n    r.setab = function (nfile, nblocks) {\n        var t = [];\n        for (var a = 0; a < nblocks; a++)\n            t.push(0);\n\n        r.tab[nfile].cb = t;\n    };\n\n    r.setat = function (nfile, blocktab) {\n        var fo = r.tab[nfile], bd = 0;\n\n        for (var a = 0; a < blocktab.length; a++)\n            bd += blocktab[a];\n\n        fo.bd = bd;\n        fo.bd0 = bd;\n        fo.cb = blocktab;\n    };\n\n    r.perc = function (bd, bd0, sz, t0) {\n        var td = Date.now() - t0,\n            p = bd * 100.0 / sz,\n            nb = bd - bd0,\n            spd = nb / (td / 1000),\n            eta = spd ? (sz - bd) / spd : 3599;\n\n        return [p, s2ms(eta), spd / (1024 * 1024)];\n    };\n\n    r.hashed = function (fobj) {\n        var fo = r.tab[fobj.n],\n            nb = fo.bt * (++fo.nh / fo.cb.length),\n            p = r.perc(nb, 0, fobj.size, fobj.t_hashing);\n\n        fo.hp = f2f(p[0], 2) + '%, ' + p[1] + ', ' + f2f(p[2], 2) + ' MB/s';\n        if (!r.is_act(fo.in))\n            return;\n\n        var o1 = p[0] - 2, o2 = p[0] - 0.1, o3 = p[0];\n\n        r.hq[fobj.n] = [fo.hp, '#fff', 'linear-gradient(90deg, #025, #06a ' + o1 + '%, #09d ' + o2 + '%, #222 ' + o3 + '%, #222 99%, #555)'];\n    };\n\n    r.prog = function (fobj, nchunk, cbd) {\n        var fo = r.tab[fobj.n],\n            delta = cbd - fo.cb[nchunk];\n\n        fo.cb[nchunk] = cbd;\n        fo.bd += delta;\n\n        if (!fo.bd)\n            return;\n\n        var p = r.perc(fo.bd, fo.bd0, fo.bt, fobj.t_uploading);\n        fo.hp = f2f(p[0], 2) + '%, ' + p[1] + ', ' + f2f(p[2], 2) + ' MB/s';\n\n        if (!r.is_act(fo.in))\n            return;\n\n        var obj = ebi('f' + fobj.n + 'p'),\n            o1 = p[0] - 2, o2 = p[0] - 0.1, o3 = p[0];\n\n        if (!obj) {\n            var msg = [\n                \"act\", r.act,\n                \"in\", fo.in,\n                \"is_act\", r.is_act(fo.in),\n                \"head\", r.head,\n                \"tail\", r.tail,\n                \"nfile\", fobj.n,\n                \"name\", fobj.name,\n                \"sz\", fobj.size,\n                \"bytesDelta\", delta,\n                \"bytesDone\", fo.bd,\n            ],\n                m2 = '',\n                ds = QSA(\"#u2tab>tbody>tr>td:first-child>a:last-child\");\n\n            for (var a = 0; a < msg.length; a += 2)\n                m2 += msg[a] + '=' + msg[a + 1] + ', ';\n\n            console.log(m2);\n\n            for (var a = 0, aa = ds.length; a < aa; a++) {\n                var id = ds[a].parentNode.getAttribute('id').slice(1, -1);\n                console.log(\"dom %d/%d = [%s] in(%s) is_act(%s) %s\",\n                    a, aa, id, r.tab[id].in, r.is_act(fo.in), ds[a].textContent);\n            }\n\n            for (var a = 0, aa = r.tab.length; a < aa; a++)\n                if (r.is_act(r.tab[a].in))\n                    console.log(\"tab %d/%d = sz %s\", a, aa, r.tab[a].bt);\n\n            throw new Error('see console');\n        }\n\n        r.hq[fobj.n] = [fo.hp, '#fff', 'linear-gradient(90deg, #050, #270 ' + o1 + '%, #4b0 ' + o2 + '%, #222 ' + o3 + '%, #222 99%, #555)'];\n    };\n\n    r.move = function (nfile, newcat) {\n        var fo = r.tab[nfile],\n            oldcat = fo.in,\n            bz_act = r.act == \"bz\";\n\n        if (oldcat == newcat)\n            return;\n\n        fo.in = newcat;\n        r.ctr[oldcat]--;\n        r.ctr[newcat]++;\n\n        while (st.car < r.tab.length && has(['ok', 'ng'], r.tab[st.car].in))\n            st.car++;\n\n        r.drawcard(oldcat);\n        r.drawcard(newcat);\n        if (r.is_act(newcat)) {\n            r.tail = Math.max(r.tail, nfile + 1);\n            if (!ebi('f' + nfile))\n                r.addrow(nfile);\n        }\n        else if (r.is_act(oldcat)) {\n            while (r.head < Math.min(r.tab.length, r.tail) && r.precard[r.tab[r.head].in])\n                r.head++;\n\n            if (!bz_act) {\n                qsr(\"#f\" + nfile);\n            }\n        }\n        else return;\n\n        if (bz_act)\n            r.bzw();\n    };\n\n    r.bzw = function () {\n        var mod = 0,\n            t0 = Date.now(),\n            first = QS('#u2tab>tbody>tr:first-child');\n\n        if (!first)\n            return;\n\n        var last = QS('#u2tab>tbody>tr:last-child');\n        first = parseInt(first.getAttribute('id').slice(1));\n        last = parseInt(last.getAttribute('id').slice(1));\n\n        while (r.head - first > r.wsz) {\n            qsr('#f' + (first++));\n            mod++;\n        }\n        while (last - r.tail < r.wsz && last < r.tab.length - 1) {\n            var obj = ebi('f' + (++last));\n            if (!obj) {\n                r.addrow(last);\n                mod++;\n            }\n        }\n        if (mod && r.modn < 200 && ebi('repl').offsetTop) {\n            if (++r.modn >= 10) {\n                if (r.modn == 10)\n                    r.mod0 = Date.now();\n\n                r.modv += Date.now() - t0;\n            }\n\n            if (r.modn >= 200) {\n                var n = r.modn - 10,\n                    ipu = r.modv / n,\n                    spu = (Date.now() - r.mod0) / n,\n                    ir = spu / ipu;\n\n                console.log('bzw:', f2f(ipu, 2), ' spu:', f2f(spu, 2), ' ir:', f2f(ir, 2), ' tab:', r.tab.length);\n                // efficiency estimates;\n                // ir: 5=16% 4=50%,30% 27=100%\n                // ipu: 2.7=16% 2=30% 1.6=50% 1.8=100% (ng for big files)\n                if (ipu >= 1.5 && ir <= 9 && r.tab.length >= 1000 && r.tab[Math.floor(r.tab.length / 3)].bt <= 1024 * 1024 * 4)\n                    r.go_potato();\n            }\n        }\n    };\n\n    r.potatolabels = function () {\n        var ode = ebi('u2depotato'),\n            oen = ebi('u2enpotato');\n\n        if (!ode)\n            return;\n\n        ode.style.display = uc.potato ? '' : 'none';\n        oen.style.display = uc.potato ? 'none' : '';\n    }\n\n    r.potato = function () {\n        ebi('u2tabw').style.minHeight = '';\n        QS('#u2cards a[act=\"bz\"]').click();\n        timer[uc.potato ? \"add\" : \"rm\"](draw_potato);\n        timer[uc.potato ? \"rm\" : \"add\"](apply_html);\n        r.potatolabels();\n    };\n\n    r.go_potato = function () {\n        r.go_potato = noop;\n        var ode = mknod('div', 'u2depotato'),\n            oen = mknod('div', 'u2enpotato'),\n            u2f = ebi('u2foot'),\n            btn = ebi('potato');\n\n        ode.innerHTML = L.u_depot;\n        oen.innerHTML = L.u_enpot;\n\n        if (sread('potato') === null) {\n            btn.click();\n            toast.inf(30, L.u_gotpot);\n            sdrop('potato');\n        }\n\n        u2f.appendChild(ode);\n        u2f.appendChild(oen);\n        ode.onclick = oen.onclick = btn.onclick;\n        r.potatolabels();\n    };\n\n    function draw_potato() {\n        if (++r.npotato < 2)\n            return;\n\n        r.npotato = 0;\n        var html = [L.u_pott.format(r.ctr.ok, r.ctr.ng, r.ctr.bz, r.ctr.q)];\n\n        while (r.head < r.tab.length && has([\"ok\", \"ng\"], r.tab[r.head].in))\n            r.head++;\n\n        var act = null;\n        if (r.head < r.tab.length)\n            act = r.tab[r.head];\n\n        if (act)\n            html.push(\"<p>file {0} of {1} : &nbsp; {2} &nbsp; <code>{3}</code></p>\\n<div>{4}</div>\".format(\n                r.head + 1, r.tab.length, act.ht, act.hp, act.hn));\n\n        html = html.join('\\n');\n        if (r.hpotato == html)\n            return;\n\n        r.hpotato = html;\n        ebi('u2mu').innerHTML = html;\n    }\n\n    function apply_html() {\n        var oq = {}, n = 0;\n        for (var k in r.hq) {\n            var o = ebi('f' + k + 'p');\n            if (!o)\n                continue;\n\n            oq[k] = o;\n            n++;\n        }\n        if (!n)\n            return;\n\n        for (var k in oq) {\n            var o = oq[k],\n                v = r.hq[k];\n\n            o.innerHTML = v[0];\n            o.style.color = v[1];\n            o.style.background = v[2];\n        }\n        r.hq = {};\n    }\n\n    r.drawcard = function (cat) {\n        var cards = QSA('#u2cards>a>span');\n\n        if (cat == \"q\") {\n            cards[4].innerHTML = r.ctr[cat];\n            return;\n        }\n        if (cat == \"bz\") {\n            cards[3].innerHTML = r.ctr[cat];\n            return;\n        }\n\n        cards[2].innerHTML = r.ctr[\"ok\"] + r.ctr[\"ng\"];\n\n        if (cat == \"ng\") {\n            cards[1].innerHTML = r.ctr[cat];\n        }\n        if (cat == \"ok\") {\n            cards[0].innerHTML = r.ctr[cat];\n        }\n    };\n\n    r.changecard = function (card) {\n        r.act = card;\n        r.precard = has([\"ok\", \"ng\", \"done\"], r.act) ? {} : r.act == \"bz\" ? { \"ok\": 1, \"ng\": 1 } : { \"ok\": 1, \"ng\": 1, \"bz\": 1 };\n        r.postcard = has([\"ok\", \"ng\", \"done\"], r.act) ? { \"bz\": 1, \"q\": 1 } : r.act == \"bz\" ? { \"q\": 1 } : {};\n        r.head = -1;\n        r.tail = -1;\n        var html = [];\n        for (var a = 0; a < r.tab.length; a++) {\n            var rt = r.tab[a].in;\n            if (r.is_act(rt)) {\n                html.push(r.genrow(a, true));\n\n                r.tail = a;\n                if (r.head == -1)\n                    r.head = a;\n            }\n        }\n        if (r.head == -1) {\n            for (var a = 0; a < r.tab.length; a++) {\n                var rt = r.tab[a].in;\n                if (r.precard[rt]) {\n                    r.head = a + 1;\n                    r.tail = a;\n                }\n                else if (r.postcard[rt]) {\n                    r.head = a;\n                    r.tail = a - 1;\n                    break;\n                }\n            }\n        }\n\n        if (r.head < 0)\n            r.head = 0;\n\n        if (card == \"bz\") {\n            for (var a = r.head - 1; a >= r.head - r.wsz && a >= 0; a--) {\n                html.unshift(r.genrow(a, true).replace(/><td>/, \"><td>a \"));\n            }\n            for (var a = r.tail + 1; a <= r.tail + r.wsz && a < r.tab.length; a++) {\n                html.push(r.genrow(a, true).replace(/><td>/, \"><td>b \"));\n            }\n        }\n        var el = ebi('u2tab');\n        el.tBodies[0].innerHTML = html.join('\\n');\n        el.className = (uc.fsearch ? 'srch ' : 'up ') + r.act;\n    };\n\n    r.genrow = function (nfile, as_html) {\n        var row = r.tab[nfile],\n            td1 = '<td id=\"f' + nfile,\n            td = '</td>' + td1,\n            ret = td1 + 'n\">' + row.hn +\n                td + 't\">' + (markup[row.ht] || row.ht) +\n                td + 'p\" class=\"prog\">' + row.hp + '</td>';\n\n        if (as_html)\n            return '<tr id=\"f' + nfile + '\">' + ret + '</tr>';\n\n        var obj = mknod('tr', 'f' + nfile);\n        obj.innerHTML = ret;\n        return obj;\n    };\n\n    r.addrow = function (nfile) {\n        var tr = r.genrow(nfile);\n        ebi('u2tab').tBodies[0].appendChild(tr);\n    };\n\n    btns = QSA(btns + '>a[act]');\n    for (var a = 0; a < btns.length; a++) {\n        btns[a].onclick = function (e) {\n            ev(e);\n            var newtab = this.getAttribute('act');\n            var go = function () {\n                for (var b = 0; b < btns.length; b++) {\n                    btns[b].className = (\n                        btns[b].getAttribute('act') == newtab) ? 'act' : '';\n                }\n                r.changecard(newtab);\n            }\n            var nf = r.ctr[newtab];\n            if (nf === undefined)\n                nf = r.ctr[\"ok\"] + r.ctr[\"ng\"];\n\n            if (nf < 9000)\n                return go();\n\n            modal.confirm(L.u_bigtab.format(nf), go, null);\n        };\n    }\n\n    r.changecard(r.act);\n    r.potato();\n}\n\n\nfunction Donut(uc, st) {\n    var r = this,\n        el = null,\n        psvg = null,\n        tenstrobe = null,\n        tstrober = null,\n        strobes = [],\n        o = 20 * 2 * Math.PI,\n        optab = QS('#ops a[data-dest=\"up2k\"]');\n\n    optab.setAttribute('ico', optab.textContent);\n\n    function svg(v) {\n        var ico = v !== undefined,\n            bg = ico ? '#333' : 'transparent',\n            fg = '#fff',\n            fsz = 52,\n            rc = 32;\n\n        if (r.eta && (r.eta > 99 || (uc.fsearch ? st.time.hashing : st.time.uploading) < 20))\n            r.eta = null;\n\n        if (r.eta) {\n            if (r.eta < 10) {\n                fg = '#fa0';\n                fsz = 72;\n            }\n            rc = 8;\n        }\n\n        return (\n            '<svg version=\"1.1\" viewBox=\"0 0 64 64\" xmlns=\"http://www.w3.org/2000/svg\">\\n' +\n            (ico ? '<rect width=\"100%\" height=\"100%\" rx=\"' + rc + '\" fill=\"#333\" />\\n' :\n                '<circle stroke=\"white\" stroke-width=\"6\" r=\"3\" cx=\"32\" cy=\"32\" />\\n') +\n            (r.eta ? (\n                '<text x=\"55%\" y=\"58%\" dominant-baseline=\"middle\" text-anchor=\"middle\"' +\n                ' font-family=\"sans-serif\" font-weight=\"bold\" font-size=\"' + fsz + 'px\"' +\n                ' fill=\"' + fg + '\">' + r.eta + '</text></svg>'\n            ) : (\n                '<circle class=\"donut\" stroke=\"white\" fill=\"' + bg +\n                '\" stroke-dashoffset=\"' + (ico ? v : o) + '\" stroke-dasharray=\"' + o + ' ' + o +\n                '\" transform=\"rotate(270 32 32)\" stroke-width=\"12\" r=\"20\" cx=\"32\" cy=\"32\" /></svg>'\n            ))\n        );\n    }\n\n    function pos() {\n        return uc.fsearch ?\n            Math.max(st.bytes.hashed, st.bytes.finished) :\n            st.bytes.inflight + st.bytes.finished;\n    }\n\n    r.on = function (ya) {\n        r.fc = r.tc = r.dc = 99;\n        r.eta = null;\n        r.base = pos();\n        optab.innerHTML = ya ? svg() : optab.getAttribute('ico');\n        el = QS('#ops a .donut');\n        clearTimeout(tenstrobe);\n        if (!ya) {\n            favico.upd();\n            wintitle();\n            if (document.visibilityState == 'hidden')\n                tenstrobe = setTimeout(r.enstrobe, 500); //debounce\n        }\n    };\n\n    r.do = function () {\n        if (!el)\n            return;\n\n        var t = st.bytes.total - r.base,\n            v = pos() - r.base,\n            ofs = o - o * v / t;\n\n        if (!uc.potato || ++r.dc >= 4) {\n            el.style.strokeDashoffset = ofs;\n            r.dc = 0;\n        }\n\n        if (++r.tc >= 10) {\n            var s = r.eta === null ? 'paused' : r.eta > 60 ? shumantime(r.eta) : (r.eta + 's');\n            wintitle(\"{0}%, {1}, #{2}, \".format(\n                f2f(v * 100 / t, 1), s, st.files.length - st.nfile.upload), true);\n            r.tc = 0;\n        }\n\n        if (favico.txt) {\n            if (++r.fc < 10 && r.eta && r.eta > 99)\n                return;\n\n            var s = svg(ofs);\n            if (s == psvg || (r.eta === null && r.fc < 10))\n                return;\n\n            favico.upd('', s);\n            psvg = s;\n            r.fc = 0;\n        }\n    };\n\n    r.enstrobe = function () {\n        strobes = ['████████████████', '________________', '████████████████'];\n        tstrober = setInterval(strobe, 300);\n\n        if (uc.upsfx && actx && actx.state != 'suspended')\n            sfx_nice();\n\n        // firefox may forget that filedrops are user-gestures so it can skip this:\n        if (uc.upnag && Notification && Notification.permission == 'granted')\n            new Notification(uc.nagtxt);\n    }\n\n    function strobe() {\n        var txt = strobes.pop();\n        wintitle(txt, false);\n        if (!txt)\n            clearInterval(tstrober);\n    }\n}\n\nfunction sfx_nice() {\n    if (true) {\n        var osc = actx.createOscillator(),\n            gain = actx.createGain(),\n            gg = gain.gain,\n            ft = [660, 880, 440, 660, 880],\n            ofs = 0;\n\n        osc.connect(gain);\n        gain.connect(actx.destination);\n        var ct = actx.currentTime + 0.03;\n\n        osc.type = 'triangle';\n        while (ft.length)\n            osc.frequency.setTargetAtTime(\n                ft.shift(), ct + (ofs += 0.05), 0.001);\n\n        gg.value = 0.15;\n        gg.setTargetAtTime(0.8, ct, 0.01);\n        gg.setTargetAtTime(0.3, ct + 0.13, 0.01);\n        gg.setTargetAtTime(0, ct + ofs + 0.05, 0.02);\n\n        osc.start();\n        setTimeout(function () {\n            osc.stop();\n            osc.disconnect();\n            gain.disconnect();\n        }, 500);\n    }\n}\n\n\nfunction fsearch_explain(n) {\n    if (n)\n        return toast.inf(60, L.ue_ro + (acct == '*' ? L.ue_nl : L.ue_la).format(acct));\n\n    if (bcfg_get('fsearch', false))\n        return toast.inf(60, L.ue_sr);\n\n    return toast.inf(60, L.ue_ta);\n}\n\n\nfunction up2k_init(subtle) {\n    var r = {\n        \"tact\": Date.now(),\n        \"init_deps\": init_deps,\n        \"set_fsearch\": set_fsearch,\n        \"gotallfiles\": [gotallfiles]  // hooks\n    };\n\n    setTimeout(function () {\n        if (WebAssembly && !hws.length)\n            fetch(SR + '/.cpr/w/w.hash.js?_=' + TS);\n    }, 1000);\n\n    function showmodal(msg) {\n        ebi('u2notbtn').innerHTML = msg;\n        ebi('u2btn').style.display = 'none';\n        ebi('u2notbtn').style.display = 'block';\n        ebi('u2conf').style.opacity = '0.5';\n    }\n\n    function unmodal() {\n        ebi('u2notbtn').style.display = 'none';\n        ebi('u2conf').style.opacity = '1';\n        ebi('u2btn').style.display = '';\n        ebi('u2notbtn').innerHTML = '';\n    }\n\n    var suggest_up2k = L.u_su2k;\n\n    function got_deps() {\n        return subtle || window.asmCrypto || window.hashwasm;\n    }\n\n    var loading_deps = false;\n    function init_deps() {\n        if (!loading_deps && !got_deps()) {\n            var fn = 'sha512.' + sha_js + '.js',\n                m = L.u_https1 + ' <a href=\"' + (location + '').replace(':', 's:') + '\">' + L.u_https2 + '</a> ' + L.u_https3;\n\n            showmodal('<h1>loading ' + fn + '</h1>');\n            import_js(SR + '/.cpr/w/deps/' + fn, unmodal);\n\n            if (HTTPS) {\n                // chrome<37 firefox<34 edge<12 opera<24 safari<7\n                m = L.u_ancient;\n                setmsg('');\n            }\n            qsr('#u2depmsg');\n            var o = mknod('div', 'u2depmsg');\n            o.innerHTML = nosubtle ? '' : m;\n            ebi('u2foot').appendChild(o);\n        }\n        loading_deps = true;\n    }\n\n    if (perms.length && !has(perms, 'read') && has(perms, 'write'))\n        goto('up2k');\n\n    function setmsg(msg, type) {\n        if (msg !== undefined) {\n            ebi('u2err').className = type;\n            ebi('u2err').innerHTML = msg;\n        }\n        else {\n            ebi('u2err').className = '';\n            ebi('u2err').innerHTML = '';\n        }\n        if (msg == suggest_up2k) {\n            ebi('u2yea').onclick = function (e) {\n                ev(e);\n                goto('up2k');\n            };\n        }\n    }\n\n    function un2k(msg) {\n        setmsg(msg, 'err');\n        return false;\n    }\n\n    setmsg(suggest_up2k, 'msg');\n\n    var u2szs = u2sz.split(','),\n        u2sz_min = parseInt(u2szs[0]),\n        u2sz_tgt = parseInt(u2szs[1]),\n        u2sz_max = parseInt(u2szs[2]);\n\n    var parallel_uploads = ebi('nthread').value = icfg_get('nthread', u2j),\n        stitch_tgt = ebi('u2szg').value = icfg_get('u2sz', u2sz_tgt),\n        uc = {},\n        fdom_ctr = 0,\n        biggest_file = 0;\n\n    bcfg_bind(uc, 'rand', 'u2rand', false, null, false);\n    bcfg_bind(uc, 'multitask', 'multitask', true, null, false);\n    bcfg_bind(uc, 'potato', 'potato', false, set_potato, false);\n    bcfg_bind(uc, 'ask_up', 'ask_up', true, null, false);\n    bcfg_bind(uc, 'umod', 'umod', false, null, false);\n    bcfg_bind(uc, 'u2ts', 'u2ts', !u2ts.endsWith('u'), set_u2ts, false);\n    bcfg_bind(uc, 'fsearch', 'fsearch', false, set_fsearch, false);\n\n    bcfg_bind(uc, 'flag_en', 'flag_en', false, apply_flag_cfg);\n    bcfg_bind(uc, 'turbo', 'u2turbo', turbolvl > 1, draw_turbo);\n    bcfg_bind(uc, 'datechk', 'u2tdate', turbolvl < 3, null);\n    bcfg_bind(uc, 'az', 'u2sort', u2sort.indexOf('n') + 1, set_u2sort);\n    bcfg_bind(uc, 'hashw', 'hashw', !!WebAssembly && !(CHROME && MOBILE) && (!subtle || !CHROME || VCHROME > 136), set_hashw);\n    bcfg_bind(uc, 'hwasm', 'nosubtle', nosubtle, set_nosubtle);\n    bcfg_bind(uc, 'upnag', 'upnag', false, set_upnag);\n    bcfg_bind(uc, 'upsfx', 'upsfx', false, set_upsfx);\n\n    uc.ow = parseInt(sread('u2ow', ['0', '1', '2', '3']) || u2ow);\n    uc.owt = ['🛡️', '🕒', '♻️', '⏭️'];\n    function set_ow() {\n        QS('label[for=\"u2ow\"]').innerHTML = uc.owt[uc.ow];\n        ebi('u2ow').checked  = true; //cosmetic\n    }\n    ebi('u2ow').onclick = function (e) {\n        ev(e);\n        if (++uc.ow > 3)\n            uc.ow = 0;\n        swrite('u2ow', uc.ow);\n        set_ow();\n        if (uc.ow && uc.ow !== 3 &&  !has(perms, 'delete'))\n            toast.warn(10, L.u_enoow, 'noow');\n        else if (toast.tag == 'noow')\n            toast.hide();\n    };\n    set_ow();\n\n    var st = {\n        \"files\": [],\n        \"nfile\": {\n            \"hash\": 0,\n            \"upload\": 0\n        },\n        \"seen\": {},\n        \"todo\": {\n            \"head\": [],\n            \"hash\": [],\n            \"handshake\": [],\n            \"upload\": []\n        },\n        \"busy\": {\n            \"head\": [],\n            \"hash\": [],\n            \"handshake\": [],\n            \"upload\": []\n        },\n        \"bytes\": {\n            \"total\": 0,\n            \"hashed\": 0,\n            \"inflight\": 0,\n            \"uploaded\": 0,\n            \"finished\": 0\n        },\n        \"time\": {\n            \"hashing\": 0.01,\n            \"uploading\": 0.01,\n            \"busy\": 0.01\n        },\n        \"eta\": {\n            \"h\": \"\",\n            \"u\": \"\",\n            \"t\": \"\"\n        },\n        \"etaw\": {\n            \"h\": [['', 0, 0, 0]],\n            \"u\": [['', 0, 0, 0]],\n            \"t\": [['', 0, 0, 0]]\n        },\n        \"etac\": {\n            \"h\": 0,\n            \"u\": 0,\n            \"t\": 0\n        },\n        \"car\": 0,\n        \"nre\": 0,\n        \"slow_io\": null,\n        \"oserr\": false,\n        \"modn\": 0,\n        \"modv\": 0,\n        \"mod0\": null\n    };\n\n    function push_t(arr, t) {\n        var sort = arr.length && arr[arr.length - 1].n > t.n;\n        arr.push(t);\n        if (sort)\n            arr.sort(function (a, b) {\n                return a.n < b.n ? -1 : 1;\n            });\n    }\n\n    var pvis = new U2pvis(\"bz\", '#u2cards', uc, st),\n        donut = new Donut(uc, st);\n\n    r.ui = pvis;\n    r.st = st;\n    r.uc = uc;\n\n    if (!window.File || !window.FileReader || !window.FileList || !File.prototype || !File.prototype.slice)\n        return un2k(L.u_ever);\n\n    var flag = false;\n    apply_flag_cfg();\n    set_fsearch();\n\n    function nav() {\n        start_actx();\n\n        var uf = function () { ebi('file' + fdom_ctr).click(); },\n            ud = function () { ebi('dir' + fdom_ctr).click(); };\n\n        // too buggy on chrome <= 72\n        var m = / Chrome\\/([0-9]+)\\./.exec(UA);\n        if (m && parseInt(m[1]) < 73)\n            return uf();\n\n        // phones dont support folder upload\n        if (MOBILE)\n            return uf();\n\n        modal.confirm(L.u_nav_m, uf, ud, null, L.u_nav_b);\n    }\n    ebi('u2btn').onclick = nav;\n\n    var nenters = 0;\n    function ondrag(e) {\n        if (++nenters <= 0)\n            nenters = 1;\n\n        if (onover.call(this, e))\n            return true;\n\n        var mup, up = QS('#up_zd');\n        var msr, sr = QS('#srch_zd');\n        if (!has(perms, 'write'))\n            mup = L.u_ewrite;\n        if (!has(perms, 'read'))\n            msr = L.u_eread;\n        if (!have_up2k_idx)\n            msr = L.u_enoi;\n\n        up.querySelector('span').textContent = mup || L.udt_drop;\n        sr.querySelector('span').textContent = msr || L.udt_drop;\n        clmod(up, 'err', mup);\n        clmod(sr, 'err', msr);\n        clmod(up, 'ok', !mup);\n        clmod(sr, 'ok', !msr);\n        ebi('up_dz').setAttribute('err', mup || '');\n        ebi('srch_dz').setAttribute('err', msr || '');\n    }\n    function onoverb(e) {\n        // zones are alive; disable cuo2duo branch\n        document.body.ondragover = document.body.ondrop = null;\n        return onover.call(this, e);\n    }\n    function onover(e) {\n        return onovercmn(this, e, false);\n    }\n    function onoverbtn(e) {\n        return onovercmn(this, e, true);\n    }\n    function onovercmn(self, e, btn) {\n        try {\n            var ok = false, dt = e.dataTransfer.types;\n            for (var a = 0; a < dt.length; a++)\n                if (dt[a] == 'Files')\n                    ok = true;\n                else if (dt[a] == 'text/uri-list') {\n                    if (btn) {\n                        ok = true;\n                        if (toast.txt == L.u_uri)\n                            toast.hide();\n                    }\n                    else\n                        return toast.inf(10, L.u_uri) || true;\n                }\n\n            if (!ok)\n                return true;\n        }\n        catch (ex) { }\n\n        ev(e);\n        try {\n            e.dataTransfer.dropEffect = 'copy';\n            e.dataTransfer.effectAllowed = 'copy';\n        }\n        catch (ex) {\n            document.body.ondragenter = document.body.ondragleave = document.body.ondragover = null;\n            return modal.alert(L.u_nodrop);\n        }\n        if (btn)\n            return;\n\n        clmod(ebi('drops'), 'vis', 1);\n        var v = self.getAttribute('v');\n        if (v)\n            clmod(ebi(v), 'hl', 1);\n    }\n    function offdrag(e) {\n        noope(e);\n\n        var v = this.getAttribute('v');\n        if (v)\n            clmod(ebi(v), 'hl');\n\n        if (--nenters <= 0) {\n            clmod(ebi('drops'), 'vis');\n            clmod(ebi('up_dz'), 'hl');\n            clmod(ebi('srch_dz'), 'hl');\n            // cuo2duo:\n            document.body.ondragover = onover;\n            document.body.ondrop = gotfile;\n        }\n    }\n    document.body.ondragenter = ondrag;\n    document.body.ondragleave = offdrag;\n    document.body.ondragover = onover;\n    document.body.ondrop = gotfile;\n    ebi('u2btn').ondrop = gotfile;\n    ebi('u2btn').ondragover = onoverbtn;\n\n    var drops = [ebi('up_dz'), ebi('srch_dz')];\n    for (var a = 0; a < 2; a++) {\n        drops[a].ondragenter = ondrag;\n        drops[a].ondragover = onoverb;\n        drops[a].ondragleave = offdrag;\n        drops[a].ondrop = gotfile;\n    }\n    ebi('drops').onclick = offdrag;  // old ff\n\n    function gotdir(e) {\n        ev(e);\n        var good_files = [],\n            nil_files = [],\n            bad_files = [];\n\n        for (var a = 0, aa = e.target.files.length; a < aa; a++) {\n            var fobj = e.target.files[a],\n                name = fobj.webkitRelativePath,\n                dst = good_files;\n\n            try {\n                if (!name)\n                    throw 1;\n\n                if (fobj.size < 1)\n                    dst = nil_files;\n            }\n            catch (ex) {\n                dst = bad_files;\n            }\n            dst.push([fobj, name]);\n        }\n\n        if (!good_files.length && bad_files.length)\n            return toast.err(30, L.u_notdir);\n\n        return read_dirs(null, [], [], good_files, nil_files, bad_files);\n    }\n\n    function gotfile(e) {\n        ev(e);\n        nenters = 0;\n        offdrag.call(this);\n        var dz = this && this.getAttribute('id');\n        if (!dz && e && e.clientY)\n            // cuo2duo fallback\n            dz = e.clientY < window.innerHeight / 2 ? 'up_dz' : 'srch_dz';\n\n        var err = this.getAttribute('err');\n        if (err)\n            return modal.alert('sorry, ' + err);\n\n        toast.inf(0, L.u_scan);\n\n        if ((dz == 'up_dz' && uc.fsearch) || (dz == 'srch_dz' && !uc.fsearch))\n            tgl_fsearch();\n\n        if (!QS('#op_up2k.act'))\n            goto('up2k');\n\n        var files,\n            is_itemlist = false;\n\n        if (e.dataTransfer) {\n            if (e.dataTransfer.items) {\n                files = e.dataTransfer.items; // DataTransferItemList\n                is_itemlist = true;\n            }\n            else files = e.dataTransfer.files; // FileList\n        }\n        else files = e.target.files;\n\n        if (!files || !files.length)\n            return toast.err(0, 'no files selected??');\n\n        more_one_file();\n        var bad_files = [],\n            nil_files = [],\n            good_files = [],\n            dirs = [];\n\n        for (var a = 0; a < files.length; a++) {\n            var fobj = files[a],\n                dst = good_files;\n\n            if (is_itemlist) {\n                if (fobj.kind !== 'file' && fobj.type !== 'text/uri-list')\n                    continue;\n\n                try {\n                    var wi = fobj.getAsEntry ? fobj.getAsEntry() : fobj.webkitGetAsEntry();\n                    if (wi.isDirectory) {\n                        dirs.push(wi);\n                        continue;\n                    }\n                }\n                catch (ex) { }\n                fobj = fobj.getAsFile();\n                if (!fobj)\n                    continue;\n            }\n            try {\n                if (fobj.size < 1)\n                    dst = nil_files;\n            }\n            catch (ex) {\n                dst = bad_files;\n            }\n            dst.push([fobj, fobj.name]);\n        }\n        start_actx();  // good enough for chrome; not firefox\n        return read_dirs(null, [], dirs, good_files, nil_files, bad_files);\n    }\n\n    function rd_flatten(pf, dirs) {\n        var ret = jcp(pf);\n        for (var a = 0; a < dirs.length; a++)\n            ret.push(dirs.fullPath || '');\n\n        ret.sort();\n        return ret;\n    }\n\n    var rd_missing_ref = [];\n    function read_dirs(rd, pf, dirs, good, nil, bad, spins) {\n        spins = spins || 0;\n        if (++spins == 5)\n            rd_missing_ref = rd_flatten(pf, dirs);\n\n        if (spins == 200) {\n            var missing = rd_flatten(pf, dirs),\n                match = rd_missing_ref.length == missing.length,\n                aa = match ? missing.length : 0;\n\n            missing.sort();\n            for (var a = 0; a < aa; a++)\n                if (rd_missing_ref[a] != missing[a])\n                    match = false;\n\n            if (match) {\n                var msg = [L.u_dirstuck.format(missing.length) + '<ul>'];\n                for (var a = 0; a < Math.min(20, missing.length); a++)\n                    msg.push('<li>' + esc(missing[a]) + '</li>');\n\n                return modal.alert(msg.join('') + '</ul>', function () {\n                    read_dirs(rd, [], [], good, nil, bad, spins);\n                });\n            }\n            spins = 0;\n        }\n\n        if (!dirs.length) {\n            if (!pf.length)\n                // call first hook, pass list of remaining hooks to call\n                return r.gotallfiles[0](good, nil, bad, r.gotallfiles.slice(1));\n\n            console.log(\"retry pf, \" + pf.length);\n            setTimeout(function () {\n                read_dirs(rd, pf, dirs, good, nil, bad, spins);\n            }, 50);\n            return;\n        }\n\n        if (!rd)\n            rd = dirs[0].createReader();\n\n        rd.readEntries(function (ents) {\n            var ngot = 0;\n            ents.forEach(function (dn) {\n                if (dn.isDirectory) {\n                    dirs.push(dn);\n                }\n                else {\n                    var name = dn.fullPath;\n                    if (name.startsWith('/'))\n                        name = name.slice(1);\n\n                    pf.push(name);\n                    dn.file(function (fobj) {\n                        apop(pf, name);\n                        var dst = good;\n                        try {\n                            if (fobj.size < 1)\n                                dst = nil;\n                        }\n                        catch (ex) {\n                            dst = bad;\n                        }\n                        dst.push([fobj, name]);\n                    });\n                }\n                ngot += 1;\n            });\n            if (!ngot) {\n                dirs.shift();\n                rd = null;\n            }\n            read_dirs(rd, pf, dirs, good, nil, bad, spins);\n        }, function () {\n            var dn = dirs[0],\n                name = dn.fullPath;\n\n            if (name.startsWith('/'))\n                name = name.slice(1);\n\n            bad.push([dn, name + '/']);\n            read_dirs(null, pf, dirs.slice(1), good, nil, bad, spins);\n        });\n    }\n\n    function gotallfiles(good_files, nil_files, bad_files) {\n        if (toast.txt == L.u_scan)\n            toast.hide();\n\n        if (uc.fsearch && !uc.turbo)\n            nil_files = [];\n\n        var ntot = good_files.concat(nil_files, bad_files).length;\n        if (bad_files.length) {\n            var msg = L.u_badf.format(bad_files.length, ntot);\n            for (var a = 0, aa = Math.min(20, bad_files.length); a < aa; a++)\n                msg += '-- ' + esc(bad_files[a][1]) + '\\n';\n\n            msg += L.u_just1;\n            return modal.alert(msg, function () {\n                start_actx();\n                gotallfiles(good_files, nil_files, []);\n            });\n        }\n\n        if (nil_files.length) {\n            var msg = L.u_blankf.format(nil_files.length, ntot);\n            for (var a = 0, aa = Math.min(20, nil_files.length); a < aa; a++)\n                msg += '-- ' + esc(nil_files[a][1]) + '\\n';\n\n            msg += L.u_just1;\n            return modal.confirm(msg, function () {\n                start_actx();\n                gotallfiles(good_files.concat(nil_files), [], []);\n            }, function () {\n                start_actx();\n                gotallfiles(good_files, [], []);\n            });\n        }\n\n        var fps = new Set(), pdp = '';\n        for (var a = 0; a < good_files.length; a++) {\n            var fp = good_files[a][1],\n                dp = vsplit(fp)[0];\n            fps.add(fp);\n            if (pdp != dp) {\n                pdp = dp;\n                dp = dp.slice(0, -1);\n                while (dp) {\n                    fps.add(dp);\n                    dp = vsplit(dp)[0].slice(0, -1);\n                }\n            }\n        }\n\n        var junk = [], rmi = [];\n        for (var a = 0; a < good_files.length; a++) {\n            var fn = good_files[a][1];\n            if (fn.indexOf(\"/.\") < 0 && fn.indexOf(\"/__MACOS\") < 0)\n                continue;\n\n            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)) {\n                junk.push(good_files[a]);\n                rmi.push(a);\n                continue;\n            }\n\n            if (fn.indexOf(\"/._\") + 1 &&\n                fps.has(fn.replace(\"/._\", \"/\")) &&\n                fn.split(\"/\").pop().startsWith(\"._\") &&\n                !has(rmi, a)\n            ) {\n                junk.push(good_files[a]);\n                rmi.push(a);\n            }\n        }\n\n        if (!junk.length)\n            return gotallfiles2(good_files);\n\n        junk.sort();\n        rmi.sort(function (a, b) { return a - b; });\n\n        var msg = L.u_applef.format(junk.length, good_files.length);\n        for (var a = 0, aa = Math.min(1000, junk.length); a < aa; a++)\n            msg += '-- ' + esc(junk[a][1]) + '\\n';\n\n        return modal.confirm(msg, function () {\n            for (var a = rmi.length - 1; a >= 0; a--)\n                good_files.splice(rmi[a], 1);\n\n            start_actx();\n            gotallfiles2(good_files);\n        }, function () {\n            start_actx();\n            gotallfiles2(good_files);\n        });\n    }\n\n    function gotallfiles2(good_files) {\n        good_files.sort(function (a, b) {\n            return a[1] < b[1] ? -1 : 1;\n        });\n\n        var msg = [];\n\n        if (lifetime)\n            msg.push('<b>' + L.u_up_life.format(lhumantime(st.lifetime || lifetime)) + '</b>\\n\\n');\n\n        if (FIREFOX && good_files.length > 3000)\n            msg.push(L.u_ff_many + \"\\n\\n\");\n\n        msg.push(L.u_asku.format(good_files.length, esc(uricom_dec(get_evpath()))) + '<ul>');\n        for (var a = 0, aa = Math.min(20, good_files.length); a < aa; a++)\n            msg.push('<li>' + esc(good_files[a][1]) + '</li>');\n\n        if (uc.ask_up && !uc.fsearch)\n            return modal.confirm(msg.join('') + '</ul>', function () {\n                start_actx();\n                up_them(good_files);\n                if (have_up2k_idx)\n                    toast.inf(15, L.u_unpt, L.u_unpt);\n            }, null);\n\n        up_them(good_files);\n    }\n\n    function up_them(good_files) {\n        start_actx();\n        draw_turbo();\n        var evpath = get_evpath(),\n            draw_each = good_files.length < 50;\n\n        if (WebAssembly && !hws.length) {\n            var nw = Math.min(navigator.hardwareConcurrency || 4, 16);\n\n            if (CHROME) {\n                // chrome-bug 383568268 // #124\n                nw = Math.max(1, (nw > 4 ? 4 : (nw - 1)));\n                if (VCHROME < 137)\n                nw = (subtle && !MOBILE && nw > 2) ? 2 : nw;\n            }\n\n            var x = sread('u2hashers') || window.u2hashers;\n            if (x) {\n                console.log('u2hashers is overriding default-value ' + nw);\n                nw = parseInt(x);\n            }\n\n            for (var a = 0; a < nw; a++)\n                hws.push(new Worker(SR + '/.cpr/w/w.hash.js?_=' + TS));\n\n            if (!subtle)\n                for (var a = 0; a < hws.length; a++)\n                    hws[a].postMessage('nosubtle');\n\n            console.log(hws.length + \" hashers\");\n        }\n\n        if (!uc.az)\n            good_files.sort(function (a, b) {\n                return a[0].size - b[0].size;\n            });\n\n        for (var a = 0; a < good_files.length; a++) {\n            var fobj = good_files[a][0],\n                name = good_files[a][1],\n                fdir = evpath,\n                now = Date.now(),\n                lmod = (uc.u2ts && fobj.lastModified) || 0,\n                ofs = name.lastIndexOf('/') + 1;\n\n            if (ofs) {\n                fdir += url_enc(name.slice(0, ofs));\n                name = name.slice(ofs);\n            }\n\n            var entry = {\n                \"n\": st.files.length,\n                \"t0\": now,\n                \"fobj\": fobj,\n                \"name\": name,\n                \"size\": fobj.size || 0,\n                \"lmod\": lmod / 1000,\n                \"purl\": fdir,\n                \"done\": false,\n                \"bytes_uploaded\": 0,\n                \"hash\": []\n            },\n                key = name + '\\n' + entry.size + '\\n' + lmod + '\\n' + uc.fsearch;\n\n            if (uc.fsearch)\n                entry.srch = 1;\n            else if (uc.rand) {\n                entry.rand = true;\n                entry.name = 'a\\n' + entry.name;\n            }\n            else if (uc.umod)\n                entry.umod = true;\n\n            if (biggest_file < entry.size)\n                biggest_file = entry.size;\n\n            try {\n                if (st.seen[fdir][key])\n                    continue;\n            }\n            catch (ex) {\n                st.seen[fdir] = {};\n            }\n\n            st.seen[fdir][key] = 1;\n\n            pvis.addfile([\n                uc.fsearch ? esc(entry.name) : linksplit(\n                    entry.purl + uricom_enc(entry.name), window.up_site).join(' / '),\n                '📐 ' + L.u_hashing,\n                ''\n            ], entry.size, draw_each);\n\n            st.bytes.total += entry.size;\n            st.files.push(entry);\n            if (uc.turbo)\n                push_t(st.todo.head, entry);\n            else\n                push_t(st.todo.hash, entry);\n        }\n        if (!draw_each) {\n            pvis.drawcard(\"q\");\n            pvis.changecard(pvis.act);\n        }\n        ebi('u2tabw').className = 'ye';\n\n        setTimeout(function () {\n            if (!actx || actx.state != 'suspended' || toast.visible)\n                return;\n\n            toast.warn(30, \"<div onclick=\\\"start_actx();toast.inf(3,'thanks!')\\\">\" + L.u_actx + \"</div>\");\n        }, 500);\n    }\n\n    function more_one_file() {\n        fdom_ctr++;\n        var elm = mknod('div');\n        elm.innerHTML = (\n            '<input id=\"file{0}\" type=\"file\" name=\"file{0}[]\" multiple=\"multiple\" tabindex=\"-1\" />' +\n            '<input id=\"dir{0}\" type=\"file\" name=\"dir{0}[]\" multiple=\"multiple\" tabindex=\"-1\" webkitdirectory />'\n        ).format(fdom_ctr);\n        ebi('u2form').appendChild(elm);\n        ebi('file' + fdom_ctr).onchange = gotfile;\n        ebi('dir' + fdom_ctr).onchange = gotdir;\n    }\n    more_one_file();\n\n    function linklist() {\n\t\tvar ret = [],\n            base = (window.up_site || location.origin).replace(/\\/$/, '');\n\n        for (var a = 0; a < st.files.length; a++) {\n            var t = st.files[a],\n                url = t.purl + uricom_enc(t.name);\n\n            if (t.fk)\n                url += '?k=' + t.fk;\n\n            ret.push(base + url);\n        }\n        return ret.join('\\r\\n');\n    }\n\n    ebi('luplinks').onclick = function (e) {\n        ev(e);\n        modal.alert(linklist());\n    };\n\n    ebi('cuplinks').onclick = function (e) {\n        ev(e);\n        var txt = linklist();\n        cliptxt(txt + '\\n', function () {\n            toast.inf(5, L.un_clip.format(txt.split('\\n').length));\n        });\n    };\n\n    var etaref = 0, etaskip = 0, utw_minh = 0, utw_read = 0, utw_card = 0;\n    function etafun() {\n        var nhash = st.busy.head.length + st.busy.hash.length + st.todo.head.length + st.todo.hash.length,\n            nsend = st.busy.upload.length + st.todo.upload.length,\n            now = Date.now(),\n            td = (now - (etaref || now)) / 1000.0;\n\n        etaref = now;\n        if (td > 1.2)\n            td = 0.05;\n\n        //ebi('acc_info').innerHTML = humantime(st.time.busy) + ' ' + f2f(now / 1000, 1);\n\n        if (utw_card != pvis.act) {\n            utw_card = pvis.act;\n            utw_read = 9001;\n            ebi('u2tabw').style.minHeight = '0px';\n        }\n\n        if (++utw_read >= 20) {\n            utw_read = 0;\n            utw_minh = parseInt(ebi('u2tabw').style.minHeight || '0');\n        }\n\n        var minh = QS('#op_up2k.act') && st.is_busy ? Math.max(utw_minh, ebi('u2tab').offsetHeight + 32) : 0;\n        if (utw_minh < minh || !utw_minh) {\n            utw_minh = minh;\n            ebi('u2tabw').style.minHeight = utw_minh + 'px';\n        }\n\n        if (!nhash) {\n            var h = L.u_etadone.format(humansize(st.bytes.hashed), pvis.ctr.ok + pvis.ctr.ng);\n            if (st.eta.h !== h) {\n                st.eta.h = ebi('u2etah').innerHTML = h;\n                console.log('{0} hash, {1} up, {2} busy'.format(\n                    f2f(st.time.hashing, 1),\n                    f2f(st.time.uploading, 1),\n                    f2f(st.time.busy, 1)));\n            }\n        }\n\n        if (!nsend && !nhash) {\n            var h = L.u_etadone.format(humansize(st.bytes.uploaded), pvis.ctr.ok + pvis.ctr.ng);\n\n            if (st.eta.u !== h)\n                st.eta.u = ebi('u2etau').innerHTML = h;\n\n            if (st.eta.t !== h)\n                st.eta.t = ebi('u2etat').innerHTML = h;\n        }\n\n        if (!st.busy.hash.length && !hashing_permitted())\n            nhash = 0;\n\n        if (!parallel_uploads || !(nhash + nsend) || (flag && flag.owner && !flag.ours))\n            return;\n\n        var t = [];\n        if (nhash) {\n            st.time.hashing += td;\n            t.push(['u2etah', st.bytes.hashed, st.bytes.hashed, st.time.hashing]);\n            if (uc.fsearch) {\n                st.time.busy += td;\n                t.push(['u2etat', st.bytes.hashed, st.bytes.hashed, st.time.hashing]);\n            }\n        }\n\n        var b_up = st.bytes.inflight + st.bytes.uploaded,\n            b_fin = st.bytes.inflight + st.bytes.finished;\n\n        if (nsend) {\n            st.time.uploading += td;\n            t.push(['u2etau', b_up, b_fin, st.time.uploading]);\n        }\n        if ((nhash || nsend) && !uc.fsearch) {\n            if (!b_fin) {\n                ebi('u2etat').innerHTML = L.u_etaprep;\n            }\n            else {\n                st.time.busy += td;\n                t.push(['u2etat', b_fin, b_fin, st.time.busy]);\n            }\n        }\n        for (var a = 0; a < t.length; a++) {\n            var hid = t[a][0],\n                eid = hid.slice(-1),\n                etaw = st.etaw[eid];\n\n            if (st.etac[eid] > 100) {  // num chunks\n                st.etac[eid] = 0;\n                etaw.push(jcp(t[a]));\n                if (etaw.length > 5)\n                    etaw.shift();\n            }\n\n            var h = etaw[0],\n                rem = st.bytes.total - t[a][2],\n                bps = (t[a][1] - h[1]) / Math.max(0.1, t[a][3] - h[3]),\n                eta = Math.floor(rem / bps);\n\n            if (t[a][1] < 1024 || t[a][3] < 0.1) {\n                ebi(hid).innerHTML = L.u_etaprep;\n                continue;\n            }\n\n            donut.eta = eta;\n            st.eta[eid] = '{0}, {1}/s, {2}'.format(\n                humansize(rem), humansize(bps, 1), humantime(eta));\n\n            if (!etaskip)\n                ebi(hid).innerHTML = st.eta[eid];\n        }\n        if (++etaskip > 2)\n            etaskip = 0;\n    }\n\n    function got_oserr() {\n        if (!hws.length || !uc.hashw || st.oserr)\n            return;\n\n        st.oserr = true;\n        var msg = HTTPS ? L.u_emtleak3 : L.u_emtleak2.format((location + '').replace(':', 's:'));\n        modal.alert(L.u_emtleak1 + msg + (CHROME ? L.u_emtleakc : FIREFOX ? L.u_emtleakf : ''));\n    }\n\n    /////\n    ////\n    ///   actuator\n    //\n\n    function handshakes_permitted() {\n        if (!st.todo.handshake.length)\n            return true;\n\n        var t = st.todo.handshake[0],\n            cd = t.cooldown;\n\n        if (cd && cd > Date.now())\n            return false;\n\n        // keepalive or verify\n        if (t.keepalive ||\n            t.t_uploaded)\n            return true;\n\n        if (parallel_uploads <\n            st.busy.handshake.length)\n            return false;\n\n        if (t.n - st.car > Math.max(8, parallel_uploads))\n            // prevent runahead from a stuck upload (slow server hdd)\n            return false;\n\n        if ((uc.multitask ? parallel_uploads : 0) <\n            st.todo.upload.length +\n            st.busy.upload.length)\n            return false;\n\n        return true;\n    }\n\n    function hashing_permitted() {\n        if (!parallel_uploads)\n            return false;\n\n        var nhs = st.todo.handshake.length + st.busy.handshake.length,\n            nup = st.todo.upload.length + st.busy.upload.length;\n\n        if (uc.multitask) {\n            if (nhs + nup < parallel_uploads)\n                return true;\n\n            if (!uc.az)\n                return nhs < 2;\n\n            var ahead = st.bytes.hashed - st.bytes.finished,\n                nmax = ahead < biggest_file / 8 ? 32 : 16;\n\n            return ahead < biggest_file && nhs < nmax;\n        }\n        return handshakes_permitted() && 0 == nhs + nup;\n    }\n\n    var tasker = (function () {\n        var running = false;\n\n        var defer = function () {\n            running = false;\n        }\n\n        var taskerd = function () {\n            if (running)\n                return;\n\n            if (crashed || !got_deps())\n                return defer();\n\n            running = true;\n            while (true) {\n                var now = Date.now(),\n                    blocktime = now - r.tact,\n                    was_busy = !!st.is_busy,\n                    is_busy = !!(  // gzip take the wheel\n                        st.car < st.files.length ||\n                        st.busy.hash.length ||\n                        st.todo.hash.length ||\n                        st.busy.handshake.length ||\n                        st.todo.handshake.length ||\n                        st.busy.upload.length ||\n                        st.todo.upload.length ||\n                        st.busy.head.length ||\n                        st.todo.head.length);\n\n                if (blocktime > 2500)\n                    console.log('main thread blocked for ' + blocktime);\n\n                r.tact = now;\n\n                if (was_busy && !is_busy) {\n                    var nre = 0, nf = 0;\n                    for (var a = 0; a < st.files.length; a++)\n                        if (st.files[a].want_recheck)\n                            nre++;\n                    console.log('nre', nre, 'st', st.nre);\n                    if (st.nre != nre) {\n                        st.nre = nre;\n                        nf = st.files.length;\n                    }\n                    for (var a = 0; a < nf; a++) {\n                        var t = st.files[a];\n                        if (t.want_recheck) {\n                            t.rechecks++;\n                            t.want_recheck = false;\n                            push_t(st.todo.handshake, t);\n                        }\n                    }\n                    is_busy = !!st.todo.handshake.length;\n                    try {\n                        if (!is_busy && !uc.fsearch && !msel.getsel().length && (!mp.au || mp.au.paused))\n                            treectl.goto();\n                    }\n                    catch (ex) { }\n                }\n\n                if (was_busy != is_busy) {\n                    st.is_busy = is_busy;\n\n                    window[(is_busy ? \"add\" : \"remove\") +\n                        \"EventListener\"](\"beforeunload\", warn_uploader_busy);\n\n                    donut.on(is_busy);\n\n                    if (!is_busy) {\n                        uptoast();\n                        //throw console.hist.join('\\n');\n                    }\n                    else {\n                        timer.add(donut.do);\n                        timer.add(etafun, false);\n                        ebi('u2etas').style.textAlign = 'left';\n                    }\n                    etafun();\n                }\n\n                if (flag) {\n                    if (is_busy) {\n                        flag.take(now);\n                        ebi('u2flagblock').style.display = flag.ours ? '' : 'block';\n                        if (!flag.ours)\n                            return defer();\n                    }\n                    else if (flag.ours) {\n                        flag.give();\n                    }\n                }\n\n                if (st.bytes.inflight && (st.bytes.inflight < 0 || !st.busy.upload.length)) {\n                    console.log('insane inflight ' + st.bytes.inflight);\n                    st.bytes.inflight = 0;\n                }\n\n                var mou_ikkai = false;\n\n                var nprev = -1;\n                for (var a = 0; a < st.todo.upload.length; a++) {\n                    var nf = st.todo.upload[a].nfile;\n                    if (nprev == nf)\n                        continue;\n\n                    nprev = nf;\n                    var t = st.files[nf];\n                    if (now - t.t_busied > 1000 * 30 &&\n                        now - t.t_handshake > 1000 * (21600 - 1800)\n                    ) {\n                        apop(st.todo.handshake, t);\n                        st.todo.handshake.unshift(t);\n                        t.keepalive = true;\n                    }\n                }\n\n                if (st.todo.head.length &&\n                    st.busy.head.length < parallel_uploads &&\n                    (!is_busy || st.todo.head[0].n - st.car < parallel_uploads * 2)) {\n                    exec_head();\n                    mou_ikkai = true;\n                }\n\n                if (handshakes_permitted() &&\n                    st.todo.handshake.length) {\n                    exec_handshake();\n                    mou_ikkai = true;\n                }\n\n                if (st.todo.upload.length &&\n                    st.busy.upload.length < parallel_uploads &&\n                    can_upload_next()) {\n                    exec_upload();\n                    mou_ikkai = true;\n                }\n\n                if (hashing_permitted() &&\n                    st.todo.hash.length &&\n                    !st.busy.hash.length) {\n                    exec_hash();\n                    mou_ikkai = true;\n                }\n\n                if (is_busy && st.modn < 100) {\n                    var t0 = Date.now() + (ebi('repl').offsetTop ? 0 : 0);\n\n                    if (++st.modn >= 10) {\n                        if (st.modn == 10)\n                            st.mod0 = Date.now();\n\n                        st.modv += Date.now() - t0;\n                    }\n\n                    if (st.modn >= 100) {\n                        var n = st.modn - 10,\n                            ipu = st.modv / n,\n                            spu = (Date.now() - st.mod0) / n,\n                            ir = spu / ipu;\n\n                        console.log('tsk:', f2f(ipu, 2), ' spu:', f2f(spu, 2), ' ir:', f2f(ir, 2));\n                        // efficiency estimates;\n                        // ir: 8=16% 11=60% 16=90% 24=100%\n                        // ipu: 1=40% .8=60% .3=100%\n                        if (ipu >= 0.5 && ir <= 15)\n                            pvis.go_potato();\n                    }\n                }\n\n                if (!mou_ikkai || crashed)\n                    return defer();\n            }\n        }\n        timer.add(taskerd, true);\n        return taskerd;\n    })();\n\n    function uptoast() {\n        if (st.busy.handshake.length)\n            return;\n\n        for (var a = 0; a < st.files.length; a++) {\n            var t = st.files[a];\n            if (t.want_recheck && t.rechecks < 999)\n                return;\n        }\n\n        var sr = uc.fsearch,\n            ok = pvis.ctr.ok,\n            ng = pvis.ctr.ng,\n            spd = Math.floor(st.bytes.finished / st.time.busy),\n            suf = '\\n\\n{0} @ {1}/s'.format(shumantime(st.time.busy), humansize(spd)),\n            t = uc.ask_up ? 0 : 10;\n\n        console.log('toast', ok, ng);\n\n        if (ok && ng)\n            toast.warn(t, uc.nagtxt = (sr ? L.ur_sm : L.ur_um).format(ok, ng) + suf);\n        else if (ok > 1)\n            toast.ok(t, uc.nagtxt = (sr ? L.ur_aso : L.ur_auo).format(ok) + suf);\n        else if (ok)\n            toast.ok(t, uc.nagtxt = (sr ? L.ur_1so : L.ur_1uo) + suf);\n        else if (ng > 1)\n            toast.err(t, uc.nagtxt = (sr ? L.ur_asn : L.ur_aun).format(ng) + suf);\n        else if (ng)\n            toast.err(t, uc.nagtxt = (sr ? L.ur_1sn : L.ur_1un) + suf);\n\n        timer.rm(etafun);\n        timer.rm(donut.do);\n        ebi('u2tabw').style.minHeight = '0px';\n        utw_minh = 0;\n\n        if (pvis.act == 'bz')\n            pvis.changecard('bz');\n\n        var n = st.files.length - 1,\n            f = n >= 0 && st.files[n];\n        if (f && !f.srch) {\n            var xhr = new XHR(),\n                ct = 'application/x-www-form-urlencoded;charset=UTF-8';\n            xhr.open('POST', f.purl, true);\n            xhr.setRequestHeader('Content-Type', ct);\n            if (xhr.overrideMimeType)\n                xhr.overrideMimeType('Content-Type', ct);\n            xhr.send('msg=upload-queue-empty;' + uricom_enc(f.name));\n        }\n    }\n\n    function chill(t) {\n        var now = Date.now();\n        if ((t.coolmul || 0) < 5 || now - t.cooldown < t.coolmul * 700)\n            t.coolmul = Math.min((t.coolmul || 0.5) * 2, 32);\n\n        var cd = now + 1000 * (t.coolmul + Math.random() * 4 + 2);\n        t.cooldown = Math.floor(Math.max(cd, t.cooldown || 1));\n        return t;\n    }\n\n    /////\n    ////\n    ///   hashing\n    //\n\n    // https://gist.github.com/jonleighton/958841\n    function buf2b64(arrayBuffer) {\n        var base64 = '',\n            cset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',\n            src = new Uint8Array(arrayBuffer),\n            nbytes = src.byteLength,\n            byteRem = nbytes % 3,\n            mainLen = nbytes - byteRem,\n            a, b, c, d, chunk;\n\n        for (var i = 0; i < mainLen; i = i + 3) {\n            chunk = (src[i] << 16) | (src[i + 1] << 8) | src[i + 2];\n            // create 8*3=24bit segment then split into 6bit segments\n            a = (chunk & 16515072) >> 18; // (2^6 - 1) << 18\n            b = (chunk & 258048) >> 12; // (2^6 - 1) << 12\n            c = (chunk & 4032) >> 6; // (2^6 - 1) << 6\n            d = chunk & 63; // 2^6 - 1\n\n            // Convert the raw binary segments to the appropriate ASCII encoding\n            base64 += cset[a] + cset[b] + cset[c] + cset[d];\n        }\n\n        if (byteRem == 1) {\n            chunk = src[mainLen];\n            a = (chunk & 252) >> 2; // (2^6 - 1) << 2\n            b = (chunk & 3) << 4; // 2^2 - 1  (zero 4 LSB)\n            base64 += cset[a] + cset[b];//+ '==';\n        }\n        else if (byteRem == 2) {\n            chunk = (src[mainLen] << 8) | src[mainLen + 1];\n            a = (chunk & 64512) >> 10; // (2^6 - 1) << 10\n            b = (chunk & 1008) >> 4; // (2^6 - 1) << 4\n            c = (chunk & 15) << 2; // 2^4 - 1  (zero 2 LSB)\n            base64 += cset[a] + cset[b] + cset[c];//+ '=';\n        }\n\n        return base64;\n    }\n\n    function hex2u8(txt) {\n        return new Uint8Array(txt.match(/.{2}/g).map(function (b) { return parseInt(b, 16); }));\n    }\n\n    function get_chunksize(filesize) {\n        var chunksize = 1024 * 1024,\n            stepsize = 512 * 1024;\n\n        while (true) {\n            for (var mul = 1; mul <= 2; mul++) {\n                var nchunks = Math.ceil(filesize / chunksize);\n                if (nchunks <= 256 || (chunksize >= 32 * 1024 * 1024 && nchunks <= 4096))\n                    return chunksize;\n\n                chunksize += stepsize;\n                stepsize *= mul;\n            }\n        }\n    }\n\n    function exec_hash() {\n        var t = st.todo.hash.shift();\n        if (!t.size)\n            return st.todo.handshake.push(t);\n\n        st.busy.hash.push(t);\n        st.nfile.hash = t.n;\n        t.t_hashing = Date.now();\n\n        var bpend = 0,\n            nchunk = 0,\n            chunksize = get_chunksize(t.size),\n            nchunks = Math.ceil(t.size / chunksize),\n            csz_mib = chunksize / 1048576,\n            tread = t.t_hashing,\n            cache_buf = null,\n            cache_car = 0,\n            cache_cdr = 0,\n            hashers = 0,\n            hashtab = {};\n\n        // resolving subtle.digest w/o worker takes 1sec on blur if the actx hack breaks\n        var use_workers = hws.length && !hws_ng && uc.hashw && (nchunks > 1 || document.visibilityState == 'hidden'),\n            hash_par = (!subtle && !use_workers) ? 0 : csz_mib < 48 ? 2 : csz_mib < 96 ? 1 : 0;\n\n        pvis.setab(t.n, nchunks);\n        pvis.move(t.n, 'bz');\n\n        if (use_workers)\n            return wexec_hash(t, chunksize, nchunks);\n\n        var segm_next = function () {\n            if (nchunk >= nchunks || bpend)\n                return false;\n\n            var nch = nchunk++,\n                car = nch * chunksize,\n                cdr = Math.min(chunksize + car, t.size);\n\n            st.bytes.hashed += cdr - car;\n            st.etac.h++;\n\n            if (MOBILE && CHROME && st.slow_io === null && nch == 1 && cdr - car >= 1024 * 512) {\n                var spd = Math.floor((cdr - car) / (Date.now() + 1 - tread));\n                st.slow_io = spd < 40 * 1024;\n                console.log('spd {0}, slow: {1}'.format(spd, st.slow_io));\n            }\n\n            if (cdr <= cache_cdr && car >= cache_car) {\n                try {\n                    var ofs = car - cache_car,\n                        ofs2 = ofs + (cdr - car),\n                        buf = cache_buf.subarray(ofs, ofs2);\n\n                    hash_calc(nch, buf);\n                }\n                catch (ex) {\n                    vis_exh(ex + '', 'up2k.js', '', '', ex);\n                }\n                return;\n            }\n\n            var reader = new FileReader(),\n                fr_cdr = cdr;\n\n            if (st.slow_io) {\n                var step = cdr - car,\n                    tgt = 48 * 1048576;\n\n                while (step && fr_cdr - car < tgt)\n                    fr_cdr += step;\n                if (fr_cdr - car > tgt && fr_cdr > cdr)\n                    fr_cdr -= step;\n                if (fr_cdr > t.size)\n                    fr_cdr = t.size;\n            }\n\n            var orz = function (e) {\n                bpend = 0;\n                var buf = e.target.result;\n                if (fr_cdr > cdr) {\n                    cache_buf = new Uint8Array(buf);\n                    cache_car = car;\n                    cache_cdr = fr_cdr;\n                    buf = cache_buf.subarray(0, cdr - car);\n                }\n                if (hashers < hash_par)\n                    segm_next();\n\n                hash_calc(nch, buf);\n            };\n            reader.onload = function (e) {\n                try { orz(e); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); }\n            };\n            reader.onerror = function () {\n                var err = esc('' + reader.error),\n                    handled = false;\n\n                if (err.indexOf('NotReadableError') !== -1 || // win10-chrome defender\n                    err.indexOf('NotFoundError') !== -1  // macos-firefox permissions\n                ) {\n                    pvis.seth(t.n, 1, 'OS-error');\n                    pvis.seth(t.n, 2, err + ' @ ' + car);\n                    console.log('OS-error', reader.error, '@', car);\n                    handled = true;\n                    got_oserr();\n                }\n\n                if (handled) {\n                    pvis.move(t.n, 'ng');\n                    apop(st.busy.hash, t);\n                    st.bytes.finished += t.size;\n                    return;\n                }\n\n                toast.err(0, 'y o u   b r o k e    i t\\nfile: ' + esc(t.name + '') + '\\nerror: ' + err);\n            };\n            bpend = 1;\n            tread = Date.now();\n            reader.readAsArrayBuffer(t.fobj.slice(car, fr_cdr));\n\n            return true;\n        };\n\n        var hash_calc = function (nch, buf) {\n            hashers++;\n            var orz = function (hashbuf) {\n                var hslice = new Uint8Array(hashbuf).subarray(0, 33),\n                    b64str = buf2b64(hslice);\n\n                hashers--;\n                hashtab[nch] = b64str;\n                t.hash.push(nch);\n                pvis.hashed(t);\n                if (t.hash.length < nchunks)\n                    return segm_next();\n\n                t.hash = [];\n                for (var a = 0; a < nchunks; a++)\n                    t.hash.push(hashtab[a]);\n\n                t.t_hashed = Date.now();\n\n                pvis.seth(t.n, 2, L.u_hashdone);\n                pvis.seth(t.n, 1, '📦 wait');\n                apop(st.busy.hash, t);\n                st.todo.handshake.push(t);\n                tasker();\n            };\n\n            var hash_done = function (hashbuf) {\n                try { orz(hashbuf); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); }\n            };\n\n            if (subtle)\n                subtle.digest('SHA-512', buf).then(hash_done);\n            else setTimeout(function () {\n                var u8buf = new Uint8Array(buf);\n                if (sha_js == 'hw') {\n                    hashwasm.sha512(u8buf).then(function (v) {\n                        hash_done(hex2u8(v))\n                    });\n                }\n                else {\n                    var hasher = new asmCrypto.Sha512();\n                    hasher.process(u8buf);\n                    hasher.finish();\n                    hash_done(hasher.result);\n                }\n            }, 1);\n        };\n        segm_next();\n    }\n\n    function wexec_hash(t, chunksize, nchunks) {\n        var nchunk = 0,\n            reading = 0,\n            max_readers = 1,\n            opt_readers = 2,\n            failed = false,\n            free = [],\n            busy = {},\n            nbusy = 0,\n            init = 0,\n            ninit = 0,\n            hashtab = {},\n            mem = (MOBILE ? 128 : 256) * 1024 * 1024;\n\n        if (!hws_ok)\n            init = setInterval(function() {\n                if (ninit < hws_ok) {\n                    ninit = hws_ok;\n                    return toast.inf(10, 'initializing webworkers ({0}/{1})'.format(hws_ok, hws.length), \"iwwt\");\n                }\n                clearInterval(init);\n                hws_ng = true;\n                toast.warn(30, 'webworkers failed to start\\n\\nwill be a bit slower due to\\nhashing on main-thread');\n                apop(st.busy.hash, t);\n                st.todo.hash.unshift(t);\n                exec_hash();\n            }, 5000);\n\n        for (var a = 0; a < hws.length; a++) {\n            var w = hws[a];\n            w.onmessage = onmsg;\n            if (init)\n                w.postMessage('ping');\n            if (mem > 0)\n                free.push(w);\n            mem -= chunksize;\n        }\n\n        function go_next() {\n            if (st.slow_io && uc.multitask)\n                // android-chrome filereader latency is ridiculous but scales linearly\n                // (unlike every other platform which instead suffers on parallel reads...)\n                max_readers = opt_readers = free.length;\n\n            if (reading >= max_readers || !free.length || nchunk >= nchunks)\n                return;\n\n            var w = free.pop(),\n                car = nchunk * chunksize,\n                cdr = Math.min(chunksize + car, t.size);\n\n            //console.log('[P ] %d read bgin (%d reading, %d busy)', nchunk, reading + 1, nbusy + 1);\n            w.postMessage([nchunk, t.fobj, car, cdr]);\n            busy[nchunk] = w;\n            nbusy++;\n            reading++;\n            nchunk++;\n            if (Date.now() - up2k.tact > 1500)\n                tasker();\n        }\n\n        function go_fail() {\n            failed = true;\n            if (nbusy)\n                return;\n            apop(st.busy.hash, t);\n            st.bytes.finished += t.size;\n        }\n\n        function onmsg(d) {\n            if (hws_ng)\n                return;\n\n            d = d.data;\n            var k = d[0];\n\n            if (k == \"pong\")\n                if (++hws_ok == hws.length) {\n                    clearInterval(init);\n                    if (toast.tag == 'iwwt')\n                        toast.hide();\n                    go_next();\n                }\n\n            if (k == \"panic\")\n                return vis_exh(d[1], 'up2k.js', '', '', d[1]);\n\n            if (k == \"fail\") {\n                var nchunk = d[1];\n                free.push(busy[nchunk]);\n                delete busy[nchunk];\n                nbusy--;\n                reading--;\n\n                pvis.seth(t.n, 1, d[1]);\n                pvis.seth(t.n, 2, d[2]);\n                console.log(d[1], d[2]);\n                if (d[1] == 'OS-error')\n                    got_oserr();\n\n                pvis.move(t.n, 'ng');\n                return go_fail();\n            }\n\n            if (k == \"ferr\")\n                return toast.err(0, 'y o u   b r o k e    i t\\nfile: ' + esc(t.name + '') + '\\nerror: ' + d[1]);\n\n            if (k == \"read\") {\n                reading--;\n                if (MOBILE && CHROME && st.slow_io === null && d[1] == 1 && d[2] > 1024 * 512) {\n                    var spd = Math.floor(d[2] / d[3]);\n                    st.slow_io = spd < 40 * 1024;\n                    console.log('spd {0}, slow: {1}'.format(spd, st.slow_io));\n                }\n                //console.log('[P ] %d read DONE (%d reading, %d busy)', d[1], reading, nbusy);\n                return go_next();\n            }\n\n            if (k == \"done\") {\n                var nchunk = d[1],\n                    hslice = d[2],\n                    sz = d[3];\n\n                free.push(busy[nchunk]);\n                delete busy[nchunk];\n                nbusy--;\n\n                //console.log('[P ] %d HASH DONE (%d reading, %d busy)', nchunk, reading, nbusy);\n\n                hashtab[nchunk] = buf2b64(hslice);\n                st.bytes.hashed += sz;\n                t.hash.push(nchunk);\n                pvis.hashed(t);\n\n                if (failed)\n                    return go_fail();\n\n                if (t.hash.length < nchunks)\n                    return nbusy < opt_readers && go_next();\n\n                t.hash = [];\n                for (var a = 0; a < nchunks; a++)\n                    t.hash.push(hashtab[a]);\n\n                t.t_hashed = Date.now();\n\n                pvis.seth(t.n, 2, L.u_hashdone);\n                pvis.seth(t.n, 1, '📦 wait');\n                apop(st.busy.hash, t);\n                st.todo.handshake.push(t);\n                tasker();\n            }\n        }\n        if (!init)\n            go_next();\n    }\n\n    /////\n    ////\n    ///   head\n    //\n\n    function exec_head() {\n        var t = st.todo.head.shift();\n        if (t.done)\n            return console.log('done; skip head1', t.name, t);\n\n        st.busy.head.push(t);\n\n        var xhr = new XMLHttpRequest();\n        xhr.onerror = xhr.ontimeout = function () {\n            console.log('head onerror, retrying', t.name, t);\n            if (!toast.visible)\n                toast.warn(9.98, L.u_enethd + \"\\n\\nfile: \" + esc(t.name), t);\n\n            apop(st.busy.head, t);\n            st.todo.head.unshift(t);\n        };\n        function orz(e) {\n            if (t.done)\n                return console.log('done; skip head2', t.name, t);\n\n            var ok = false;\n            if (xhr.status == 200) {\n                var srv_sz = xhr.getResponseHeader('Content-Length'),\n                    srv_ts = xhr.getResponseHeader('Last-Modified');\n\n                ok = t.size == srv_sz;\n                if (ok && uc.datechk) {\n                    srv_ts = new Date(srv_ts) / 1000;\n                    ok = Math.abs(srv_ts - t.lmod) < 2;\n                }\n            }\n            apop(st.busy.head, t);\n            if (!ok && !t.srch) {\n                push_t(st.todo.hash, t);\n                tasker();\n                return;\n            }\n\n            t.done = true;\n            t.fobj = null;\n            st.bytes.hashed += t.size;\n            st.bytes.finished += t.size;\n            pvis.move(t.n, 'bz');\n            pvis.seth(t.n, 1, ok ? 'YOLO' : '404');\n            pvis.seth(t.n, 2, \"turbo'd\");\n            pvis.move(t.n, ok ? 'ok' : 'ng');\n            tasker();\n        };\n        xhr.onload = function (e) {\n            try { orz(e); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); }\n        };\n\n        xhr.open('HEAD', t.purl + uricom_enc(t.name), true);\n        xhr.timeout = 34000;\n        xhr.send();\n    }\n\n    /////\n    ////\n    ///   handshake\n    //\n\n    function exec_handshake() {\n        var t = st.todo.handshake.shift(),\n            keepalive = t.keepalive,\n            me = Date.now();\n\n        if (t.done)\n            return console.log('done; skip hs', t.name, t);\n\n        st.busy.handshake.push(t);\n        t.keepalive = undefined;\n        t.t_busied = me;\n\n        if (keepalive)\n            console.log(\"sending keepalive handshake\", t.name, t);\n\n        if (!t.srch && !t.t_handshake)\n            pvis.seth(t.n, 2, L.u_hs);\n\n        var xhr = new XMLHttpRequest();\n        xhr.onerror = xhr.ontimeout = function () {\n            if (t.t_busied != me)  // t.done ok\n                return console.log('zombie handshake onerror', t.name, t);\n\n            if (!toast.visible)\n                toast.warn(9.98, L.u_eneths + \"\\n\\nfile: \" + esc(t.name), t);\n\n            console.log('handshake onerror, retrying', t.name, t);\n            apop(st.busy.handshake, t);\n            st.todo.handshake.unshift(chill(t));\n            t.keepalive = keepalive;\n        };\n        var orz = function (e) {\n            if (t.t_busied != me || t.done)\n                return console.log('zombie handshake onload', t.name, t);\n\n            if (xhr.status == 200) {\n                try {\n                    var response = JSON.parse(xhr.responseText);\n                }\n                catch (ex) {\n                    apop(st.busy.handshake, t);\n                    st.todo.handshake.unshift(chill(t));\n                    var txt = t.t_uploading ? L.u_ehsfin : t.srch ? L.u_ehssrch : L.u_ehsinit;\n                    return toast.err(0, txt + '\\n\\n' + L.badreply + ':\\n\\n' + unpre(xhr.responseText));\n                }\n\n                t.t_handshake = Date.now();\n                if (keepalive) {\n                    apop(st.busy.handshake, t);\n                    tasker();\n                    return;\n                }\n\n                if (toast.tag === t)\n                    toast.ok(5, L.u_fixed);\n\n                if (!response.name) {\n                    var msg = '',\n                        smsg = '';\n\n                    if (!response || !response.hits || !response.hits.length) {\n                        smsg = '404';\n                        msg = (L.u_s404 + ' <a href=\"#\" onclick=\"fsearch_explain(' +\n                            (has(perms, 'write') ? '0' : '1') + ')\" class=\"fsearch_explain\">(' + L.u_expl + ')</a>');\n                    }\n                    else {\n                        smsg = 'found';\n                        var msg = [];\n                        for (var a = 0, aa = Math.min(20, response.hits.length); a < aa; a++) {\n                            var hit = response.hits[a],\n                                tr = unix2ui(hit.ts),\n                                tu = unix2ui(t.lmod),\n                                diff = parseInt(t.lmod) - parseInt(hit.ts),\n                                cdiff = (Math.abs(diff) <= 2) ? '3c0' : 'f0b',\n                                sdiff = '<span style=\"color:#' + cdiff + '\">diff ' + diff;\n\n                            msg.push(linksplit(hit.rp, window.up_site).join(' / ') + '<br /><small>' + tr + ' (srv), ' + tu + ' (You), ' + sdiff + '</small></span>');\n                        }\n                        msg = msg.join('<br />\\n');\n                    }\n                    pvis.seth(t.n, 2, msg);\n                    pvis.seth(t.n, 1, smsg);\n                    pvis.move(t.n, smsg == '404' ? 'ng' : 'ok');\n                    apop(st.busy.handshake, t);\n                    st.bytes.finished += t.size;\n                    t.done = true;\n                    t.fobj = null;\n                    tasker();\n                    return;\n                }\n\n                t.sprs = response.sprs;\n\n                var fk = response.fk,\n                    rsp_purl = url_enc(response.purl),\n                    rename = rsp_purl !== t.purl || response.name !== t.name;\n\n                if (rename || fk) {\n                    if (rename)\n                        console.log(\"server-rename [\" + t.purl + \"] [\" + t.name + \"] to [\" + rsp_purl + \"] [\" + response.name + \"]\");\n\n                    t.purl = rsp_purl;\n                    t.name = response.name;\n\n                    var url = t.purl + uricom_enc(t.name);\n                    if (fk) {\n                        t.fk = fk;\n                        url += '?k=' + fk;\n                    }\n\n                    pvis.seth(t.n, 0, linksplit(url, window.up_site).join(' / '));\n                }\n\n                var chunksize = get_chunksize(t.size),\n                    cdr_idx = Math.ceil(t.size / chunksize) - 1,\n                    cdr_sz = (t.size % chunksize) || chunksize,\n                    cbd = [];\n\n                for (var a = 0; a <= cdr_idx; a++) {\n                    cbd.push(a == cdr_idx ? cdr_sz : chunksize);\n                }\n\n                t.postlist = [];\n                t.wark = response.wark;\n                var missing = response.hash;\n                for (var a = 0; a < missing.length; a++) {\n                    var idx = t.hash.indexOf(missing[a]);\n                    if (idx < 0)\n                        return modal.alert('wtf negative index for hash \"{0}\" in task:\\n{1}'.format(\n                            missing[a], esc(JSON.stringify(t))));\n\n                    t.postlist.push(idx);\n                    cbd[idx] = 0;\n                }\n\n                pvis.setat(t.n, cbd);\n                pvis.prog(t, 0, cbd[0]);\n\n                var done = true,\n                    msg = 'done';\n\n                if (t.postlist.length) {\n                    if (t.rechecks && QS('#opa_del.act'))\n                        toast.inf(30, L.u_started, L.u_unpt);\n\n                    var arr = st.todo.upload,\n                        sort = arr.length && arr[arr.length - 1].nfile > t.n;\n\n                    if (!t.stitch_sz) {\n                        // keep all connections busy\n                        var bpc = (st.bytes.total - st.bytes.finished) / (parallel_uploads || 1),\n                            ocs = 1024 * 1024,\n                            stp = 1024 * 512,\n                            ccs = ocs;\n                        while (ccs < bpc) {\n                            ocs = ccs;\n                            ccs += stp; if (ccs < bpc) ocs = ccs;\n                            ccs += stp; stp *= 2;\n                        }\n                        ocs = Math.floor(ocs / 1024 / 1024);\n                        t.stitch_sz = Math.min(ocs, stitch_tgt);\n                    }\n\n                    for (var a = 0; a < t.postlist.length; a++) {\n                        var nparts = [], tbytes = 0, stitch = t.stitch_sz;\n                        if (t.nojoin && t.nojoin - t.postlist.length < 6)\n                            stitch = 1;\n\n                        --a;\n                        for (var b = 0; b < stitch; b++) {\n                            nparts.push(t.postlist[++a]);\n                            tbytes += chunksize;\n                            if (tbytes + chunksize > stitch * 1024 * 1024 || t.postlist[a + 1] - t.postlist[a] !== 1)\n                                break;\n                        }\n                        arr.push({\n                            'nfile': t.n,\n                            'nparts': nparts\n                        });\n                    }\n                    t.nojoin = 0;\n\n                    msg = null;\n                    done = false;\n\n                    if (sort)\n                        arr.sort(function (a, b) {\n                            return a.nfile < b.nfile ? -1 :\n                            /*  */ a.nfile > b.nfile ? 1 :\n                            /*  */ a.nparts[0] < b.nparts[0] ? -1 : 1;\n                        });\n                }\n\n                if (msg)\n                    pvis.seth(t.n, 1, msg);\n\n                apop(st.busy.handshake, t);\n\n                if (done) {\n                    t.done = true;\n                    t.fobj = null;\n                    st.bytes.finished += t.size - t.bytes_uploaded;\n                    var spd1 = (t.size / ((t.t_hashed - t.t_hashing) / 1000.)) / (1024 * 1024.),\n                        spd2 = (t.size / ((t.t_uploaded - t.t_uploading) / 1000.)) / (1024 * 1024.);\n\n                    pvis.seth(t.n, 2, 'hash {0}, up {1} MB/s'.format(\n                        f2f(spd1, 2), !isNum(spd2) ? '--' : f2f(spd2, 2)));\n\n                    pvis.move(t.n, 'ok');\n                }\n                else {\n                    if (t.t_uploaded)\n                        chill(t);\n\n                    t.t_uploaded = undefined;\n                }\n                tasker();\n            }\n            else {\n                pvis.seth(t.n, 1, \"ERROR\");\n                pvis.seth(t.n, 2, L.u_ehstmp, t);\n                apop(st.busy.handshake, t);\n\n                var err = \"\",\n                    cls = \"ERROR\",\n                    rsp = unpre(xhr.responseText),\n                    ofs = rsp.lastIndexOf('\\nURL: ');\n\n                if (ofs !== -1)\n                    rsp = rsp.slice(0, ofs);\n\n                if (rsp.indexOf('rate-limit ') !== -1) {\n                    var penalty = rsp.replace(/.*rate-limit /, \"\").split(' ')[0];\n                    console.log(\"rate-limit: \" + penalty);\n                    t.cooldown = Date.now() + parseFloat(penalty) * 1000;\n                    st.todo.handshake.unshift(t);\n                    return;\n                }\n\n                var err_pend = rsp.indexOf('partial upload exists at a different') + 1,\n                    err_srcb = rsp.indexOf('source file busy; please try again') + 1,\n                    err_plug = rsp.indexOf('upload blocked by x') + 1,\n                    err_dupe = rsp.indexOf('upload rejected, file already exists') + 1,\n                    err_exists = rsp.indexOf('upload rejected, a file with that name already exists') + 1;\n\n                if (err_pend || err_srcb || err_plug || err_dupe || err_exists) {\n                    err = rsp;\n                    ofs = err.indexOf('\\n/');\n                    if (ofs !== -1) {\n                        err = err.slice(0, ofs + 1) + linksplit(err.slice(ofs + 2).trimEnd()).join(' / ');\n                    }\n                    if (!t.rechecks)\n                        t.rechecks = 0;\n                    if (t.rechecks < 999 && (err_pend || err_srcb)) {\n                        t.want_recheck = true;\n                        if (st.busy.upload.length || st.busy.handshake.length || st.bytes.uploaded) {\n                            err = L.u_dupdefer;\n                            cls = 'defer';\n                        }\n                    }\n                    if (err_pend) {\n                        err += ' <a href=\"#\" onclick=\"toast.inf(60, L.ue_ab);\" class=\"fsearch_explain\">(' + L.u_expl + ')</a>';\n                    }\n                }\n\n                if (err != \"\") {\n                    if (!t.t_uploading)\n                        st.bytes.finished += t.size;\n\n                    pvis.seth(t.n, 1, cls);\n                    pvis.seth(t.n, 2, err);\n                    pvis.move(t.n, 'ng');\n\n                    tasker();\n                    return;\n                }\n\n                st.todo.handshake.unshift(chill(t));\n\n                if (rsp.indexOf('server HDD is full') + 1)\n                    return toast.err(0, L.u_ehsdf + \"\\n\\n\" + rsp.replace(/.*; /, ''));\n\n                err = t.t_uploading ? L.u_ehsfin : t.srch ? L.u_ehssrch : L.u_ehsinit;\n                xhrchk(xhr, err + \"\\n\\nfile: \" + esc(t.name) + \"\\n\\nerror \", \"404, target folder not found\", \"warn\", t);\n            }\n        }\n        xhr.onload = function (e) {\n            try { orz(e); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); }\n        };\n\n        var req = {\n            \"name\": t.name,\n            \"size\": t.size,\n            \"lmod\": t.lmod,\n            \"life\": st.lifetime,\n            \"hash\": t.hash\n        };\n        if (t.srch)\n            req.srch = 1;\n        else if (t.rand)\n            req.rand = true;\n        else if (t.umod)\n            req.umod = true;\n\n        if (!t.srch) {\n            if (uc.ow == 1)\n                req.replace = 'mt';\n            if (uc.ow == 2)\n                req.replace = true;\n            if (uc.ow == 3)\n                req.replace = 'skip';\n        }\n\n        xhr.open('POST', t.purl, true);\n        xhr.responseType = 'text';\n        xhr.timeout = 42000 + (t.srch || t.t_uploaded ? 0 :\n            (t.size / (1048 * 20))); // safededup 20M/s hdd\n        xhr.send(JSON.stringify(req));\n    }\n\n    /////\n    ////\n    ///   upload\n    //\n\n    function can_upload_next() {\n        var upt = st.todo.upload[0],\n            upf = st.files[upt.nfile],\n            nhs = st.busy.handshake.length,\n            hs = nhs && st.busy.handshake[0],\n            now = Date.now();\n\n        if (nhs >= 16)\n            return false;\n\n        if (hs && hs.t_uploaded && Date.now() - hs.t_busied > 10000)\n            // verification HS possibly held back by uploads\n            return false;\n\n        for (var a = 0, aa = st.busy.handshake.length; a < aa; a++) {\n            var hs = st.busy.handshake[a];\n            if (hs.n < upt.nfile && hs.t_busied > now - 10 * 1000 && !st.files[hs.n].bytes_uploaded)\n                return false;  // handshake race; wait for lexically first\n        }\n\n        if (upf.sprs)\n            return true;\n\n        for (var a = 0, aa = st.busy.upload.length; a < aa; a++)\n            if (st.busy.upload[a].nfile == upt.nfile)\n                return false;\n\n        return true;\n    }\n\n    function exec_upload() {\n        var upt = st.todo.upload.shift(),\n            t = st.files[upt.nfile],\n            nparts = upt.nparts,\n            pcar = nparts[0],\n            pcdr = nparts[nparts.length - 1],\n            maxsz = (u2sz_max > 1 ? u2sz_max : 2040) * 1024 * 1024;\n\n        if (t.done)\n            return console.log('done; skip chunk', t.name, t);\n\n        st.busy.upload.push(upt);\n        st.nfile.upload = upt.nfile;\n\n        if (!t.t_uploading)\n            t.t_uploading = Date.now();\n\n        pvis.seth(t.n, 1, \"🚀 \" + L.ul_send);\n\n        var chunksize = get_chunksize(t.size),\n            car = pcar * chunksize,\n            cdr = (pcdr + 1) * chunksize;\n\n        if (cdr >= t.size)\n            cdr = t.size;\n\n        if (cdr - car <= maxsz)\n            return upload_sub(t, upt, pcar, pcdr, car, cdr, chunksize, car, []);\n\n        var car0 = car, subs = [];\n        while (car < cdr) {\n            subs.push([car, Math.min(cdr, car + maxsz)]);\n            car += maxsz;\n        }\n        upload_sub(t, upt, pcar, pcdr, 0, 0, chunksize, car0, subs);\n    }\n\n    function upload_sub(t, upt, pcar, pcdr, car, cdr, chunksize, car0, subs) {\n        var nparts = upt.nparts,\n            is_sub = subs.length;\n\n        if (is_sub) {\n            var x = subs.shift();\n            car = x[0];\n            cdr = x[1];\n        }\n\n        var snpart = is_sub ? ('' + pcar + '(' + (car-car0) +'+'+ (cdr-car)) :\n                pcar == pcdr ? pcar : ('' + pcar + '~' + pcdr);\n\n        var orz = function (xhr) {\n            st.bytes.inflight -= xhr.bsent;\n            var txt = unpre((xhr.response && xhr.response.err) || xhr.responseText);\n            if (txt.indexOf('upload blocked by x') + 1) {\n                apop(st.busy.upload, upt);\n                for (var a = pcar; a <= pcdr; a++)\n                    apop(t.postlist, a);\n                pvis.seth(t.n, 1, \"ERROR\");\n                pvis.seth(t.n, 2, txt.split(/\\n/)[0]);\n                pvis.move(t.n, 'ng');\n                return;\n            }\n            if (xhr.status == 200) {\n                car = car0;\n                if (subs.length)\n                    return upload_sub(t, upt, pcar, pcdr, 0, 0, chunksize, car0, subs);\n\n                var bdone = cdr - car;\n                for (var a = pcar; a <= pcdr; a++) {\n                    pvis.prog(t, a, Math.min(bdone, chunksize));\n                    bdone -= chunksize;\n                }\n                st.bytes.finished += cdr - car;\n                st.bytes.uploaded += cdr - car;\n                t.bytes_uploaded += cdr - car;\n                t.cooldown = t.coolmul = 0;\n                st.etac.u++;\n                st.etac.t++;\n            }\n            else if (txt.indexOf('already got that') + 1 ||\n                txt.indexOf('already being written') + 1) {\n                t.nojoin = t.nojoin || t.postlist.length;\n                console.log(\"ignoring dupe-segment with backoff\", t.nojoin, t.name, t);\n                if (!toast.visible && st.todo.upload.length < 4)\n                    toast.inf(10, L.u_cbusy);\n            }\n            else {\n                xhrchk(xhr, L.u_cuerr2.format(snpart, Math.ceil(t.size / chunksize), esc(t.name)), \"404, target folder not found (???)\", \"warn\", t);\n                chill(t);\n            }\n            orz2(xhr);\n        }\n        var orz2 = function (xhr) {\n            apop(st.busy.upload, upt);\n            for (var a = pcar; a <= pcdr; a++)\n                apop(t.postlist, a);\n            if (!t.postlist.length) {\n                t.t_uploaded = Date.now();\n                pvis.seth(t.n, 1, 'verifying');\n                st.todo.handshake.unshift(t);\n            }\n            tasker();\n        }\n        function do_send() {\n            var xhr = new XMLHttpRequest(),\n                bfin = Math.floor(st.bytes.finished / 1024 / 1024),\n                btot = Math.floor(st.bytes.total / 1024 / 1024);\n\n            xhr.upload.onprogress = function (xev) {\n                var nb = xev.loaded,\n                    db = nb - xhr.bsent;\n\n                if (!db)\n                    return;\n\n                st.bytes.inflight += db;\n                xhr.bsent = nb;\n                if (!IE)\n                    xhr.timeout = 64000 + Date.now() - xhr.t0;\n                pvis.prog(t, pcar, nb);\n            };\n            xhr.onload = function (xev) {\n                try { orz(xhr); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); }\n            };\n            xhr.onerror = xhr.ontimeout = function (xev) {\n                if (crashed)\n                    return;\n\n                st.bytes.inflight -= (xhr.bsent || 0);\n                xhr.bsent = 0;\n\n                if (!toast.visible)\n                    toast.warn(9.98, L.u_cuerr.format(snpart, Math.ceil(t.size / chunksize), esc(t.name)), t);\n\n                t.nojoin = t.nojoin || t.postlist.length;  // maybe rproxy postsize limit\n                console.log('chunkpit onerror,', t.name, t);\n                orz2(xhr);\n            };\n\n            var chashes = [],\n                ctxt = t.hash[pcar],\n                plen = Math.floor(192 / nparts.length);\n\n            plen = plen > 9 ? 9 : plen < 2 ? 2 : plen;\n            for (var a = pcar + 1; a <= pcdr; a++)\n                chashes.push(t.hash[a].slice(0, plen));\n\n            if (chashes.length)\n                ctxt += ',' + plen + ',' + chashes.join('');\n\n            xhr.open('POST', t.purl, true);\n            xhr.setRequestHeader(\"X-Up2k-Hash\", ctxt);\n            xhr.setRequestHeader(\"X-Up2k-Wark\", t.wark);\n            if (is_sub)\n                xhr.setRequestHeader(\"X-Up2k-Subc\", car - car0);\n\n            xhr.setRequestHeader(\"X-Up2k-Stat\", \"{0}/{1}/{2}/{3} {4}/{5} {6}\".format(\n                pvis.ctr.ok, pvis.ctr.ng, pvis.ctr.bz, pvis.ctr.q, btot, btot - bfin,\n                st.eta.t.indexOf('/s, ')+1 ? st.eta.t.split(' ').pop() : 'x'));\n\n            xhr.setRequestHeader('Content-Type', 'application/octet-stream');\n            if (xhr.overrideMimeType)\n                xhr.overrideMimeType('Content-Type', 'application/octet-stream');\n\n            xhr.bsent = 0;\n            xhr.t0 = Date.now();\n            xhr.timeout = 1000 * (IE ? 1234 : 42);\n            xhr.responseType = 'text';\n            xhr.send(t.fobj.slice(car, cdr));\n        }\n        do_send();\n    }\n\n    /////\n    ////\n    ///   config ui\n    //\n\n    function onresize(e) {\n        // 10x faster than matchMedia('(min-width\n        var bar = ebi('ops'),\n            wpx = window.innerWidth,\n            fpx = parseInt(getComputedStyle(bar)['font-size']),\n            wem = wpx * 1.0 / fpx,\n            wide = wem > 57 ? 'w' : '',\n            parent = ebi(wide ? 'u2btn_cw' : 'u2btn_ct'),\n            btn = ebi('u2btn');\n\n        if (btn.parentNode !== parent) {\n            parent.appendChild(btn);\n            ebi('u2conf').className = ebi('u2cards').className = ebi('u2etaw').className = wide;\n        }\n\n        wide = wem > 86 ? 'ww' : wide;\n        parent = ebi(wide == 'ww' ? 'u2c3w' : 'u2c3t');\n        var its = [ebi('u2etaw'), ebi('u2cards')];\n        if (its[0].parentNode !== parent) {\n            ebi('u2conf').className = wide;\n            for (var a = 0; a < 2; a++) {\n                parent.appendChild(its[a]);\n                its[a].className = wide;\n            }\n        }\n    }\n    onresize100.add(onresize, true);\n\n    if (MOBILE) {\n        // android-chrome wobbles for a bit; firefox / iOS-safari are OK\n        setTimeout(onresize, 20);\n        setTimeout(onresize, 100);\n        setTimeout(onresize, 500);\n    }\n\n    var o = QSA('#u2conf .c *[tt]');\n    for (var a = o.length - 1; a >= 0; a--) {\n        o[a].parentNode.getElementsByTagName('input')[0].setAttribute('tt', o[a].getAttribute('tt'));\n    }\n    tt.att(QS('#u2conf'));\n\n    function bumpthread2(e) {\n        if (anymod(e))\n            return;\n\n        var k = e.key || e.code;\n\n        if (k == 'ArrowUp')\n            bumpthread(1);\n\n        if (k == 'ArrowDown')\n            bumpthread(-1);\n    }\n\n    function bumpthread(dir) {\n        try {\n            dir.stopPropagation();\n            dir.preventDefault();\n        } catch (ex) { }\n\n        var obj = ebi('nthread');\n        if (dir.target) {\n            clmod(obj, 'err', 1);\n            var v = Math.floor(parseInt(obj.value));\n            if (v < 0 || v !== v)\n                return;\n\n            if (v > 64) {\n                var p = obj.selectionStart;\n                v = obj.value = 64;\n                obj.selectionStart = obj.selectionEnd = p;\n            }\n\n            parallel_uploads = v;\n            if (v == u2j)\n                sdrop('nthread');\n            else\n                swrite('nthread', v);\n\n            clmod(obj, 'err');\n            return;\n        }\n\n        parallel_uploads += dir;\n\n        if (parallel_uploads < 0)\n            parallel_uploads = 0;\n\n        if (parallel_uploads > 16)\n            parallel_uploads = 16;\n\n        if (parallel_uploads > 6)\n            toast.warn(10, L.u_maxconn);\n        else if (toast.txt == L.u_maxconn)\n            toast.hide();\n\n        obj.value = parallel_uploads;\n        bumpthread({ \"target\": 1 });\n    }\n\n    var read_u2sz = function () {\n        var el = ebi('u2szg'), n = parseInt(el.value);\n        stitch_tgt = n = (\n            isNaN(n) ? u2sz_tgt :\n            n < u2sz_min ? u2sz_min :\n            n > u2sz_max ? u2sz_max : n\n        );\n        if (n == u2sz_tgt) sdrop('u2sz'); else swrite('u2sz', n);\n        if (el.value != n) el.value = n;\n    };\n    ebi('u2szg').addEventListener('blur', read_u2sz);\n    ebi('u2szg').onkeydown = function (e) {\n        if (anymod(e)) return;\n        var k = e.key || e.code,\n            n = k == 'ArrowUp' ? 1 : k == 'ArrowDown' ? -1 : 0;\n        if (!n) return;\n        this.value = parseInt(this.value) + n;\n        read_u2sz();\n    }\n\n    function tgl_fsearch() {\n        set_fsearch(!uc.fsearch);\n    }\n\n    function draw_turbo() {\n        if (turbolvl < 0 && uc.turbo) {\n            bcfg_set('u2turbo', uc.turbo = false);\n            toast.err(10, L.u_turbo_c);\n        }\n\n        if (uc.turbo && !has(perms, 'read')) {\n            bcfg_set('u2turbo', uc.turbo = false);\n            toast.warn(30, L.u_turbo_g);\n        }\n\n        var msg = (turbolvl || !uc.turbo) ? null : uc.fsearch ? L.u_ts : L.u_tu,\n            html = ebi('u2foot').innerHTML;\n\n        if (msg && html.indexOf(msg) + 1)\n            return;\n\n        qsr('#u2turbomsg');\n        if (!msg)\n            return;\n\n        var o = mknod('div', 'u2turbomsg');\n        o.innerHTML = msg;\n        ebi('u2foot').appendChild(o);\n    }\n    draw_turbo();\n\n    function draw_life() {\n        var el = ebi('u2life');\n        if (!lifetime) {\n            el.style.display = 'none';\n            el.innerHTML = '';\n            st.lifetime = 0;\n            return;\n        }\n        el.style.display = uc.fsearch ? 'none' : '';\n        el.innerHTML = '<div>' + L.u_life_cfg + '</div><div>' + L.u_life_est + '</div><div id=\"undor\"></div>';\n        set_life(Math.min(lifetime, icfg_get('lifetime', lifetime)));\n        ebi('lifem').oninput = ebi('lifeh').oninput = mod_life;\n        ebi('lifem').onkeydown = ebi('lifeh').onkeydown = kd_life;\n        tt.att(ebi('u2life'));\n    }\n    draw_life();\n\n    function mod_life(e) {\n        var el = e.target,\n            pow = parseInt(el.getAttribute('p')),\n            v = parseInt(el.value);\n\n        if (!isNum(v))\n            return;\n\n        if (toast.tag == mod_life)\n            toast.hide();\n\n        v *= pow;\n        if (v > lifetime) {\n            v = lifetime;\n            toast.warn(20, L.u_life_max.format(lhumantime(lifetime)), mod_life);\n        }\n\n        swrite('lifetime', v);\n        set_life(v);\n    }\n\n    function kd_life(e) {\n        var el = e.target,\n            k = e.key || e.code,\n            d = k == 'ArrowUp' ? 1 : k == 'ArrowDown' ? -1 : 0;\n\n        if (anymod(e) || !d)\n            return;\n\n        el.value = parseInt(el.value) + d;\n        mod_life(e);\n    }\n\n    function set_life(v) {\n        //ebi('lifes').value = v;\n        ebi('lifem').value = parseInt(v / 60);\n        ebi('lifeh').value = parseInt(v / 3600);\n\n        var undo = have_unpost - (v ? lifetime - v : 0);\n        ebi('undor').innerHTML = undo <= 0 ?\n            L.u_unp_ng : L.u_unp_ok.format(lhumantime(undo));\n\n        st.lifetime = v;\n        rel_life();\n    }\n\n    function rel_life() {\n        if (!lifetime)\n            return;\n\n        try {\n            ebi('lifew').innerHTML = unix2ui((st.lifetime || lifetime) +\n                Date.now() / 1000 - new Date().getTimezoneOffset() * 60\n            ).replace(' ', ', ').slice(0, -3);\n        }\n        catch (ex) { }\n    }\n    setInterval(rel_life, 9000);\n\n    function set_potato() {\n        pvis.potato();\n        set_fsearch();\n    }\n\n    function set_fsearch(new_state) {\n        var fixed = false,\n            persist = new_state !== undefined,\n            preferred = bcfg_get('fsearch', undefined),\n            can_write = false;\n\n        if (!ebi('fsearch')) {\n            new_state = false;\n        }\n        else if (perms.length) {\n            if (!(can_write = has(perms, 'write'))) {\n                new_state = true;\n                fixed = true;\n            }\n            if (!has(perms, 'read') || !have_up2k_idx) {\n                new_state = false;\n                fixed = true;\n            }\n            if (new_state === undefined && preferred === undefined)\n                new_state = can_write ? false : have_up2k_idx ? true : undefined;\n        }\n\n        if (new_state === undefined)\n            new_state = preferred;\n\n        if (new_state !== undefined)\n            if (persist)\n                bcfg_set('fsearch', uc.fsearch = new_state);\n            else\n                bcfg_upd_ui('fsearch', uc.fsearch = new_state);\n\n        try {\n            clmod(ebi('u2c3w'), 's', !can_write);\n            QS('label[for=\"fsearch\"]').style.display = QS('#fsearch').style.display = fixed ? 'none' : '';\n        }\n        catch (ex) { }\n\n        try {\n            var ico = uc.fsearch ? '🔎' : '🚀',\n                desc = uc.fsearch ? L.ul_btns : L.ul_btnu;\n\n            clmod(ebi('op_up2k'), 'srch', uc.fsearch);\n            ebi('u2bm').innerHTML = ico + '&nbsp; <sup>' + desc + '</sup>';\n        }\n        catch (ex) { }\n\n        ebi('u2tab').className = (uc.fsearch ? 'srch ' : 'up ') + pvis.act;\n\n        var potato = uc.potato && !uc.fsearch;\n        ebi('u2cards').style.display = ebi('u2tab').style.display = potato ? 'none' : '';\n        ebi('u2mu').style.display = potato ? '' : 'none';\n\n        if (u2ts.startsWith('f') || !sread('u2ts'))\n            uc.u2ts = bcfg_upd_ui('u2ts', !u2ts.endsWith('u'));\n\n        draw_turbo();\n        draw_life();\n        onresize();\n    }\n\n    function apply_flag_cfg() {\n        if (uc.flag_en && !flag) {\n            try {\n                flag = up2k_flagbus();\n            }\n            catch (ex) {\n                toast.err(5, \"not supported on your browser:\\n\" + esc(basenames(ex)));\n                bcfg_set('flag_en', uc.flag_en = false);\n            }\n        }\n        else if (!uc.flag_en && flag) {\n            if (flag.ours)\n                flag.give();\n\n            flag.ch.close();\n            flag = false;\n        }\n    }\n\n    function set_u2sort(en) {\n        if (u2sort.indexOf('f') < 0)\n            return;\n\n        var fen = uc.az = u2sort.indexOf('n') + 1;\n        bcfg_upd_ui('u2sort', fen);\n        if (en != fen)\n            toast.warn(10, L.ul_btnlk);\n    }\n\n    function set_u2ts(en) {\n        if (u2ts.indexOf('f') < 0)\n            return;\n\n        var fen = !u2ts.endsWith('u');\n        bcfg_upd_ui('u2ts', fen);\n        if (en != fen)\n            toast.warn(10, L.ul_btnlk);\n    }\n\n    function set_hashw() {\n        if (!WebAssembly) {\n            bcfg_set('hashw', uc.hashw = false);\n            toast.err(10, L.u_nowork);\n        }\n    }\n\n    function set_nosubtle(v) {\n        if (!WebAssembly)\n            return toast.err(10, L.u_nowork);\n        modal.confirm(L.lang_set, location.reload.bind(location), null);\n    }\n\n    function set_upnag(en) {\n        function nopenag() {\n            bcfg_set('upnag', uc.upnag = false);\n            toast.err(10, \"https only\");\n        }\n\n        function chknag() {\n            if (Notification.permission != 'granted')\n                nopenag();\n        }\n\n        if (!Notification || !HTTPS)\n            return nopenag();\n\n        if (en && Notification.permission == 'default')\n            Notification.requestPermission().then(chknag, chknag);\n\n        set_upsfx(en);\n    }\n\n    function set_upsfx(en) {\n        if (!en)\n            return;\n\n        toast.inf(10, 'OK -- <a href=\"#\" id=\"nagtest\">test it!</a>')\n\n        ebi('nagtest').onclick = function () {\n            start_actx();\n            uc.nagtxt = ':^)';\n            setTimeout(donut.enstrobe, 200);\n        };\n    }\n\n    if (uc.upnag && (!Notification || Notification.permission != 'granted'))\n        bcfg_set('upnag', uc.upnag = false);\n\n    ebi('nthread_add').onclick = function (e) {\n        ev(e);\n        bumpthread(1);\n    };\n    ebi('nthread_sub').onclick = function (e) {\n        ev(e);\n        bumpthread(-1);\n    };\n\n    ebi('nthread').onkeydown = bumpthread2;\n    ebi('nthread').oninput = bumpthread;\n\n    ebi('u2etas').onclick = function (e) {\n        ev(e);\n        clmod(ebi('u2etas'), 'o', 't');\n    };\n\n    set_fsearch();\n    bumpthread({ \"target\": 1 });\n    if (parallel_uploads < 1)\n        bumpthread(1);\n\n    setTimeout(function () {\n        for (var a = 0; a < up2k_hooks.length; a++)\n            up2k_hooks[a]();\n    }, 1);\n\n    return r;\n}\n\n\nfunction warn_uploader_busy(e) {\n    e.preventDefault();\n    e.returnValue = '';\n    return \"upload in progress, click abort and use the file-tree to navigate instead\";\n}\n\n\ntt.init();\nfavico.init();\nebi('ico1').onclick = function () {\n    var a = favico.txt == this.textContent;\n    swrite('icot', a ? 'c' : this.textContent);\n    swrite('icof', a ? 'fc5' : '000');\n    swrite('icob', a ? '222' : '');\n    favico.init();\n};\n\n\nif (QS('#op_up2k.act'))\n    goto_up2k();\n\napply_perms({ \"perms\": perms, \"frand\": frand, \"u2ts\": u2ts });\nif (ls0)\n    fileman.render();\n\n\n(function () {\n    goto();\n    var op = sread('opmode');\n    if (op !== null && op !== '.')\n        try {\n            goto(op);\n        }\n        catch (ex) { }\n})();\n\nJ_U2K = 2;\n"
  },
  {
    "path": "copyparty/web/util.js",
    "content": "\"use strict\";\n\nvar J_UTL = 1;\n\nif (!window.console || !console.log)\n    window.console = {\n        \"log\": function (msg) { }\n    };\n\nif (!Object.assign)\n    Object.assign = function (a, b) {\n        for (var k in b)\n            a[k] = b[k];\n    };\n\nif (window.CGV1)\n    Object.assign(window, window.CGV1);\n\nif (window.CGV)\n    Object.assign(window, window.CGV);\n\n\nvar wah = '',\n    STG = null,\n    NOAC = 'autocorrect=\"off\" autocapitalize=\"off\"',\n    L, tt, treectl, thegrid, up2k, asmCrypto, hashwasm, vbar, marked,\n    T0 = Date.now(),\n    R = SR.slice(1),\n    RS = R ? \"/\" + R : \"\",\n    HALFMAX = 8192 * 8192 * 8192 * 8192,\n    HTTPS = ('' + location).indexOf('https:') === 0,\n    TOUCH = 'ontouchstart' in window,\n    MOBILE = TOUCH,\n    CHROME = !!window.chrome,  // safari=false\n    VCHROME = CHROME ? 1 : 0,\n    UA = '' + navigator.userAgent,\n    IE = !!document.documentMode,\n    FIREFOX = ('netscape' in window) && / rv:/.test(UA),\n    IPHONE = TOUCH && /iPhone|iPad|iPod/i.test(UA),\n    LINUX = /Linux/.test(UA),\n    MACOS = /Macintosh/.test(UA),\n    WINDOWS = /Windows/.test(UA),\n    APPLE = IPHONE || MACOS,\n    APPLEM = TOUCH && APPLE;\n\nif (!window.WebAssembly || !WebAssembly.Memory)\n    window.WebAssembly = false;\n\nif (!window.Notification || !Notification.permission)\n    window.Notification = false;\n\nif (!window.FormData)\n    window.FormData = false;\n\ntry {\n    STG = window.localStorage;\n    STG.STG;\n}\ncatch (ex) {\n    STG = null;\n    if ((ex + '').indexOf('sandbox') < 0)\n        console.log('no localStorage: ' + ex);\n}\n\ntry {\n    if (navigator.userAgentData.mobile)\n        MOBILE = true;\n\n    if (navigator.userAgentData.platform == 'Windows')\n        WINDOWS = true;\n\n    CHROME = navigator.userAgentData.brands.find(function (d) { return d.brand == 'Chromium' });\n    if (CHROME)\n        VCHROME = parseInt(CHROME.version);\n    else\n        VCHROME = 0;\n\n    CHROME = !!CHROME;\n}\ncatch (ex) { }\n\n\nvar ebi = document.getElementById.bind(document),\n    QS = document.querySelector.bind(document),\n    QSA = document.querySelectorAll.bind(document),\n    XHR = XMLHttpRequest;\n\n\nfunction mknod(et, eid, html) {\n    var ret = document.createElement(et);\n\n    if (eid)\n        ret.id = eid;\n\n    if (html)\n        ret.innerHTML = html;\n\n    return ret;\n}\n\n\nfunction qsr(sel) {\n    var el = QS(sel);\n    if (el)\n        el.parentNode.removeChild(el);\n\n    return el;\n}\n\n\n// error handler for mobile devices\nfunction esc(txt) {\n    return txt.replace(/[&\"<>]/g, function (c) {\n        return {\n            '&': '&amp;',\n            '\"': '&quot;',\n            '<': '&lt;',\n            '>': '&gt;'\n        }[c];\n    });\n}\nfunction basenames(txt) {\n    return (txt + '').replace(/https?:\\/\\/[^ \\/]+\\//g, '/').replace(/js\\?_=[a-zA-Z]{4}/g, 'js');\n}\nif ((location + '').indexOf(',rej,') + 1)\n    window.onunhandledrejection = function (e) {\n        var err = e.reason;\n        try {\n            err += '\\n' + e.reason.stack;\n        }\n        catch (e) { }\n        err = basenames(err);\n        console.log(\"REJ: \" + err);\n        try {\n            toast.warn(30, err);\n        }\n        catch (e) { }\n    };\n\ntry {\n    console.hist = [];\n    var CMAXHIST = MOBILE ? 9000 : 44000;\n    var hook = function (t) {\n        var orig = console[t].bind(console),\n            cfun = function () {\n                console.hist.push(Date.now() + ' ' + t + ': ' + Array.from(arguments).join(', '));\n                if (console.hist.length > CMAXHIST)\n                    console.hist = console.hist.slice(CMAXHIST / 4);\n\n                orig.apply(console, arguments);\n            };\n\n        console['std' + t] = orig;\n        console[t] = cfun;\n    };\n    hook('log');\n    console.log('log-capture ok');\n    hook('debug');\n    hook('warn');\n    hook('error');\n}\ncatch (ex) {\n    if (console.stdlog)\n        console.log = console.stdlog;\n    console.log('console capture failed', ex);\n}\nvar crashed = false, ignexd = {}, evalex_fatal = false;\nfunction vis_exh(msg, url, lineNo, columnNo, error) {\n    var ekey = url + '\\n' + lineNo + '\\n' + msg;\n    if (ignexd[ekey] || crashed)\n        return;\n\n    msg = String(msg);\n    url = String(url);\n\n    if (msg.indexOf('ResizeObserver') + 1)\n        return;  // chrome issue 809574 (benign, from <video>)\n\n    if (msg.indexOf('l2d.js') + 1)\n        return;  // `t` undefined in tapEvent -> hitTestSimpleCustom\n\n    if (!/\\.js($|\\?)/.exec(url))\n        return;  // chrome debugger\n\n    if (url.indexOf('extension://') + 1)\n        return;\n\n    if (url.indexOf(' > eval') + 1 && !evalex_fatal)\n        return;  // md timer\n\n    if (url.indexOf('prism.js') + 1)\n        return;\n\n    if (url.indexOf('easymde.js') + 1)\n        return;  // clicking the preview pane\n\n    if (url.indexOf('deps/marked.js') + 1 && !WebAssembly)\n        return; // ff<52\n\n    crashed = true;\n    window.onerror = undefined;\n    var html = [\n        '<h1>you hit a bug!</h1>',\n        '<p style=\"font-size:1.3em;margin:0;line-height:2em\">try to <a href=\"#\" onclick=\"localStorage.clear();location.reload();\">reset copyparty settings</a> if you are stuck here, or <a href=\"#\" onclick=\"ignex();\">ignore this</a> / <a href=\"#\" onclick=\"ignex(true);\">ignore all</a> / <a href=\"?b=u\">basic</a></p>',\n        '<p style=\"color:#fff\">please send me a screenshot arigathanks gozaimuch: <a href=\"<ghi>\" target=\"_blank\">new github issue</a></p>',\n        '<p class=\"b\">' + esc(url + ' @' + lineNo + ':' + columnNo), '<br />' + esc(msg).replace(/\\n/g, '<br />') + '</p>',\n        '<p><b>UA:</b> ' + esc(UA)\n    ];\n\n    try {\n        var ua = '',\n            ad = navigator.userAgentData,\n            adb = ad.brands;\n\n        for (var a = 0; a < adb.length; a++)\n            if (!/Not.*A.*Brand/.exec(adb[a].brand))\n                ua += adb[a].brand + '/' + adb[a].version + ', ';\n        ua += ad.platform;\n\n        html.push('<br /><b>UAD:</b> ' + esc(ua.slice(0, 100)));\n    }\n    catch (e) { }\n    html.push('</p>');\n\n    try {\n        if (error) {\n            var find = ['desc', 'stack', 'trace'];\n            for (var a = 0; a < find.length; a++)\n                if (String(error[find[a]]) !== 'undefined')\n                    html.push('<p class=\"b\"><b>' + find[a] + ':</b><br />' +\n                        esc(String(error[find[a]])).replace(/\\n/g, '<br />\\n') + '</p>');\n        }\n        ignexd[ekey] = true;\n\n        var ls = {},\n            lsk = Object.keys(localStorage),\n            nka = lsk.length,\n            nk = Math.min(200, nka);\n\n        for (var a = 0; a < nk; a++) {\n            var k = lsk[a],\n                v = localStorage.getItem(k);\n\n            ls[k] = v.length > 256 ? v.slice(0, 32) + '[...' + v.length + 'b]' : v;\n        }\n\n        lsk = Object.keys(ls);\n        lsk.sort();\n        html.push('<p class=\"b\"><b>' + nka + ':&nbsp;</b>');\n        for (var a = 0; a < nk; a++)\n            html.push(' <b>' + esc(lsk[a]) + '</b> <code>' + esc(ls[lsk[a]]) + '</code> ');\n\n        html.push('</p>');\n    }\n    catch (e) { }\n\n    if (console.hist.length) {\n        html.push('<p class=\"b\"><b>console:</b><ul><li>' + Date.now() + ' @</li>');\n        for (var a = console.hist.length - 1, aa = Math.max(0, console.hist.length - 20); a >= aa; a--)\n            html.push('<li>' + esc(console.hist[a]) + '</li>');\n        html.push('</ul>')\n    }\n\n    try {\n        var exbox = ebi('exbox');\n        if (!exbox) {\n            exbox = mknod('div', 'exbox');\n            document.body.appendChild(exbox);\n\n            var s = mknod('style');\n            s.innerHTML = (\n                '#exbox{background:#222;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em;z-index:80386;position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;overflow:auto;width:calc(100% - 2em)} ' +\n                '#exbox,#exbox *{line-height:1.5em;overflow-wrap:break-word} ' +\n                '#exbox code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} ' +\n                '#exbox a{text-decoration:underline;color:#fc0;background:#222;border:none} ' +\n                '#exbox h1{margin:.5em 1em 0 0;padding:0} ' +\n                '#exbox p.b{border-top:1px solid #999;margin:1em 0 0 0;font-size:1em} ' +\n                '#exbox ul, #exbox li {margin:0 0 0 .5em;padding:0} ' +\n                '#exbox b{color:#fff}'\n            );\n            document.head.appendChild(s);\n        }\n        exbox.innerHTML = basenames(html.join('\\n')).replace(/<ghi>/, 'https://github.com/9001/copyparty/issues/new?labels=bug&template=bug_report.md');\n        exbox.style.display = 'block';\n    }\n    catch (e) {\n        document.body.innerHTML = html.join('\\n');\n    }\n}\nfunction ignex(all) {\n    var o = ebi('exbox');\n    o.style.display = 'none';\n    o.innerHTML = '';\n    crashed = false;\n    if (!all)\n        window.onerror = vis_exh;\n}\nwindow.onerror = vis_exh;\n\n\nif (!window.Ls || !window.langmod)\n    var Ls = {};\n\n\nfunction noop() { }\n\n\nfunction ctrl(e) {\n    return e && (e.ctrlKey || e.metaKey);\n}\n\n\nfunction anymod(e, shift_ok) {\n    return e && (e.ctrlKey || e.altKey || e.metaKey || e.isComposing || (!shift_ok && e.shiftKey));\n}\n\n\nvar dev_fbw = sread('dev_fbw');\nfunction ev(e) {\n    if (!e && window.event) {\n        e = window.event;\n        if (dev_fbw == 1) {\n            toast.warn(10, 'hello from fallback code ;_;\\ncheck console trace');\n            console.error('using window.event');\n        }\n    }\n    if (!e)\n        return;\n\n    if (e.preventDefault)\n        e.preventDefault();\n\n    if (e.stopPropagation)\n        e.stopPropagation();\n\n    if (e.stopImmediatePropagation)\n        e.stopImmediatePropagation();\n\n    e.returnValue = false;\n    return e;\n}\n\n\nfunction noope(e) {\n    try { ev(e); } catch (ex) { }\n}\n\n\n// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith\nif (!String.prototype.endsWith)\n    String.prototype.endsWith = function (search, this_len) {\n        if (this_len === undefined || this_len > this.length) {\n            this_len = this.length;\n        }\n        return this.substring(this_len - search.length, this_len) === search;\n    };\n\nif (!String.prototype.startsWith)\n    String.prototype.startsWith = function (s, i) {\n        i = i > 0 ? i | 0 : 0;\n        return this.substring(i, i + s.length) === s;\n    };\n\nif (!String.prototype.trimEnd)\n    String.prototype.trimEnd = String.prototype.trimRight = function () {\n        return this.replace(/[ \\t\\r\\n]+$/, '');\n    };\n\nif (!Element.prototype.matches)\n    Element.prototype.matches =\n        Element.prototype.oMatchesSelector ||\n        Element.prototype.msMatchesSelector ||\n        Element.prototype.mozMatchesSelector ||\n        Element.prototype.webkitMatchesSelector;\n\nvar CLOSEST = !!Element.prototype.closest;\nif (!CLOSEST)\n    Element.prototype.closest = function (s) {\n        var el = this;\n        do {\n            if (el.matches(s)) return el;\n            el = el.parentElement || el.parentNode;\n        } while (el !== null && el.nodeType === 1);\n    };\n\nif (!String.prototype.format)\n    String.prototype.format = function () {\n        var args = arguments;\n        return this.replace(/{(\\d+)}/g, function (match, number) {\n            return typeof args[number] != 'undefined' ?\n                args[number] : match;\n        });\n    };\n\nvar have_URL = false;\ntry {\n    new URL('/a/', 'https://a.com/');\n    have_URL = true;\n}\ncatch (ex) {\n    console.log('ie11 shim URL()');\n    window.URL = function (url, base) {\n        if (url.indexOf('//') < 0)\n            url = base + '/' + url.replace(/^\\/?/, '');\n        else if (url.indexOf('//') == 0)\n            url = 'https:' + url;\n\n        var x = url.split('?');\n        return {\n            \"pathname\": '/' + x[0].split('://')[1].replace(/[^/]+\\//, ''),\n            \"search\": x.length > 1 ? x[1] : ''\n        };\n    }\n}\n\nif (!window.Set)\n    window.Set = function () {\n        var r = this;\n        r.size = 0;\n        r.d = {};\n        r.add = function (k) {\n            if (!r.d[k]) {\n                r.d[k] = 1;\n                r.size++;\n            }\n        };\n        r.has = function (k) {\n            return r.d[k];\n        };\n    };\n\n// https://stackoverflow.com/a/950146\nfunction import_js(url, cb, ecb) {\n    var head = document.head || document.getElementsByTagName('head')[0];\n    var script = mknod('script');\n    script.type = 'text/javascript';\n    script.src = url + '?_=' + (window.TS || 'a');\n    script.onload = cb;\n    script.onerror = ecb || function () {\n        var m = 'Failed to load module:\\n' + url;\n        console.log(m);\n        toast.err(0, m);\n    };\n    head.appendChild(script);\n}\n\n\nfunction unsmart(txt) {\n    return !APPLEM ? txt : (txt.\n        replace(/[\\u2014]/g, \"--\").\n        replace(/[\\u2022]/g, \"*\").\n        replace(/[\\u2018\\u2019]/g, \"'\").\n        replace(/[\\u201c\\u201d]/g, '\"'));\n}\n\n\nfunction namesan(txt, win, fslash) {\n    if (win)\n        txt = (txt.\n            replace(/</g, \"＜\").\n            replace(/>/g, \"＞\").\n            replace(/:/g, \"：\").\n            replace(/\"/g, \"＂\").\n            replace(/\\\\/g, \"＼\").\n            replace(/\\|/g, \"｜\").\n            replace(/\\?/g, \"？\").\n            replace(/\\*/g, \"＊\"));\n\n    if (fslash)\n        txt = txt.replace(/\\//g, \"／\");\n\n    return txt;\n}\n\n\nvar NATSORT, ENATSORT;\ntry {\n    NATSORT = new Intl.Collator([], {numeric: true});\n}\ncatch (ex) { }\n\n\nvar crctab = (function () {\n    var c, tab = [];\n    for (var n = 0; n < 256; n++) {\n        c = n;\n        for (var k = 0; k < 8; k++) {\n            c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));\n        }\n        tab[n] = c;\n    }\n    return tab;\n})();\n\n\nfunction crc32(str) {\n    var crc = 0 ^ (-1);\n    for (var i = 0; i < str.length; i++) {\n        crc = (crc >>> 8) ^ crctab[(crc ^ str.charCodeAt(i)) & 0xFF];\n    }\n    return ((crc ^ (-1)) >>> 0).toString(16);\n}\n\n\nfunction randstr(len) {\n    var ret = '';\n    try {\n        var ar = new Uint32Array(Math.floor((len + 3) / 4));\n        crypto.getRandomValues(ar);\n        for (var a = 0; a < ar.length; a++)\n            ret += ('000' + ar[a].toString(36)).slice(-4);\n        return ret.slice(0, len);\n    }\n    catch (ex) {\n        console.log('using unsafe randstr because ' + ex);\n        while (ret.length < len)\n            ret += ('000' + Math.floor(Math.random() * 1679616).toString(36)).slice(-4);\n        return ret.slice(0, len);\n    }\n}\n\n\nfunction clmod(el, cls, add) {\n    if (!el)\n        return false;\n\n    if (el.classList) {\n        var have = el.classList.contains(cls);\n        if (add == 't')\n            add = !have;\n\n        if (!add == !have)\n            return false;\n\n        el.classList[add ? 'add' : 'remove'](cls);\n        return true;\n    }\n\n    var re = new RegExp('\\\\s*\\\\b' + cls + '\\\\s*\\\\b', 'g'),\n        n1 = el.className;\n\n    if (add == 't')\n        add = !re.test(n1);\n\n    var n2 = n1.replace(re, ' ') + (add ? ' ' + cls : '');\n\n    if (n1 == n2)\n        return false;\n\n    el.className = n2;\n    return true;\n}\n\n\nfunction clgot(el, cls) {\n    if (!el)\n        return;\n\n    if (el.classList)\n        return el.classList.contains(cls);\n\n    var lst = (el.className + '').split(/ /g);\n    return has(lst, cls);\n}\n\n\nfunction setcvar(k, v) {\n    try {\n        document.documentElement.style.setProperty(k, v);\n    }\n    catch (e) { }\n}\n\n\nvar ANIM = true;\ntry {\n    var mq = window.matchMedia('(prefers-reduced-motion: reduce)');\n    mq.onchange = function () {\n        ANIM = !mq.matches;\n    };\n    ANIM = !mq.matches;\n}\ncatch (ex) { }\n\n\nfunction yscroll() {\n    if (document.documentElement.scrollTop) {\n        return (window.yscroll = function () {\n            return document.documentElement.scrollTop;\n        })();\n    }\n    if (window.pageYOffset) {\n        return (window.yscroll = function () {\n            return window.pageYOffset;\n        })();\n    }\n    return 0;\n}\nfunction xscroll() {\n    if (document.documentElement.scrollLeft) {\n        return (window.xscroll = function () {\n            return document.documentElement.scrollLeft;\n        })();\n    }\n    if (window.pageXOffset) {\n        return (window.xscroll = function () {\n            return window.pageXOffset;\n        })();\n    }\n    return 0;\n}\n\n\nfunction showsort(tab) {\n    var v, vn, v1, v2, th = tab.tHead,\n        sopts = jread('fsort');\n\n    sopts = sopts && sopts.length ? sopts : dsort;\n\n    th && (th = th.rows[0]) && (th = th.cells);\n\n    for (var a = sopts.length - 1; a >= 0; a--) {\n        if (!sopts[a][0])\n            continue;\n\n        v2 = v1;\n        v1 = sopts[a];\n    }\n\n    v = [v1, v2];\n    vn = [v1 ? v1[0] : '', v2 ? v2[0] : ''];\n\n    var ga = QSA('#ghead a[s]');\n    for (var a = 0; a < ga.length; a++)\n        ga[a].className = '';\n\n    for (var a = 0; a < th.length; a++) {\n        var n = vn.indexOf(th[a].getAttribute('name')),\n            cl = n < 0 ? ' ' : ' s' + n + (v[n][1] > 0 ? ' ' : 'r ');\n\n        th[a].className = th[a].className.replace(/ *s[01]r? */, ' ') + cl;\n        if (n + 1) {\n            ga = QS('#ghead a[s=\"' + vn[n] + '\"]');\n            if (ga)\n                ga.className = cl;\n        }\n    }\n}\nfunction st_cmp_num(a, b) {\n    a = a[0];\n    b = b[0];\n    return (\n        a === null ? -1 :\n        b === null ? 1 :\n        (a - b)\n    );\n}\nfunction st_cmp_nat(a, b) {\n    a = a[0];\n    b = b[0];\n    return (\n        a === null ? -1 :\n        b === null ? 1 :\n        NATSORT.compare(a, b)\n    );\n}\nfunction st_cmp_gen(a, b) {\n    a = a[0];\n    b = b[0];\n    return (\n        a === null ? -1 :\n        b === null ? 1 :\n        a.localeCompare(b)\n    );\n}\nfunction sortTable(table, col, cb) {\n    var tb = table.tBodies[0],\n        th = table.tHead.rows[0].cells,\n        tr = Array.prototype.slice.call(tb.rows, 0),\n        i, reverse = /s0[^r]/.exec(th[col].className + ' ') ? -1 : 1;\n\n    var kname = th[col].getAttribute('name'),\n        stype = th[col].getAttribute('sort');\n    try {\n        var nrules = [],\n            rules = kname == 'href' ? [] : jread(\"fsort\", []);\n\n        rules.unshift([kname, reverse, stype || '']);\n        for (var a = 0; a < rules.length; a++) {\n            var add = true;\n            for (var b = 0; b < a; b++)\n                if (rules[a][0] == rules[b][0])\n                    add = false;\n\n            if (add)\n                nrules.push(rules[a]);\n\n            if (nrules.length >= 10)\n                break;\n        }\n        jwrite(\"fsort\", nrules);\n        try { showsort(table); } catch (ex) { }\n    }\n    catch (ex) {\n        console.log(\"failed to persist sort rules, resetting: \" + ex);\n        jwrite(\"fsort\", null);\n    }\n    var vl = [];\n    for (var a = 0; a < tr.length; a++) {\n        var cell = tr[a].cells[col];\n        if (!cell) {\n            vl.push([null, a]);\n            continue;\n        }\n        var v = cell.getAttribute('sortv') || cell.textContent.trim();\n        if (stype == 'int') {\n            v = parseInt(v.replace(/[, ]/g, '')) || 0;\n        }\n        vl.push([v, a]);\n    }\n\n    if (stype == 'int')\n        vl.sort(st_cmp_num);\n    else if (ENATSORT)\n        vl.sort(st_cmp_nat);\n    else\n        vl.sort(st_cmp_gen);\n\n    if (reverse < 0)\n        vl.reverse();\n\n    if (sread('dir1st') !== '0') {\n        var r1 = [], r2 = [];\n        for (var i = 0; i < tr.length; i++) {\n            var cell = tr[vl[i][1]].cells[1],\n                href = cell.getAttribute('sortv') || cell.textContent.trim();\n\n            (href.split('?')[0].slice(-1) == '/' ? r1 : r2).push(vl[i]);\n        }\n        vl = r1.concat(r2);\n    }\n    for (i = 0; i < tr.length; ++i) tb.appendChild(tr[vl[i][1]]);\n    if (cb) cb();\n}\nfunction makeSortable(table, cb) {\n    var th = table.tHead, i;\n    th && (th = th.rows[0]) && (th = th.cells);\n    if (th) i = th.length;\n    else return; // if no `<thead>` then do nothing\n    while (--i >= 0) (function (i) {\n        th[i].onclick = function (e) {\n            ev(e);\n            sortTable(table, i, cb);\n        };\n    }(i));\n}\n\n\nfunction assert_vp(path) {\n    if (path.indexOf('//') + 1)\n        throw 'nonlocal1: ' + path;\n\n    var o = location.origin;\n    if (have_URL && (new URL(path, o)).origin != o)\n        throw 'nonlocal2: ' + path;\n}\n\n\nfunction linksplit(rp, base, id) {\n    var ret = [],\n        apath = base || '/',\n        q = null;\n\n    if (rp && rp.indexOf('?') + 1) {\n        q = rp.split('?', 2);\n        rp = q[0];\n        q = '?' + q[1];\n    }\n\n    if (rp && rp[0] == '/')\n        rp = rp.slice(1);\n\n    while (rp) {\n        var link = rp;\n        var ofs = rp.indexOf('/');\n        if (ofs === -1) {\n            rp = null;\n        }\n        else {\n            link = rp.slice(0, ofs + 1);\n            rp = rp.slice(ofs + 1);\n        }\n        var vlink = esc(uricom_dec(link));\n\n        if (link.indexOf('/') !== -1)\n            vlink = vlink.slice(0, -1);\n\n        if (!rp) {\n            if (q)\n                link += q;\n\n            if (id)\n                link += '\" id=\"' + id;\n        }\n\n        ret.push('<a href=\"' + apath + link + '\">' + vlink + '</a>');\n        apath += link;\n    }\n    return ret;\n}\n\n\nfunction vsplit(vp) {\n    if (vp.endsWith('/'))\n        vp = vp.slice(0, -1);\n\n    var ofs = vp.lastIndexOf('/') + 1,\n        base = vp.slice(0, ofs),\n        fn = vp.slice(ofs);\n\n    return [base, fn];\n}\n\n\nfunction vjoin(p1, p2) {\n    if (!p1)\n        p1 = '';\n\n    if (!p2)\n        p2 = '';\n\n    if (p1.endsWith('/'))\n        p1 = p1.slice(0, -1);\n\n    if (p2.startsWith('/'))\n        p2 = p2.slice(1);\n\n    if (!p1)\n        return p2;\n\n    if (!p2)\n        return p1;\n\n    return p1 + '/' + p2;\n}\n\n\nfunction addq(url, q) {\n    var uh = url.split('#', 1),\n        u = uh[0],\n        h = uh.length == 1 ? '' : '#' + uh[1];\n\n    return u + (u.indexOf('?') < 0 ? '?' : '&') + (q === undefined ? '' : q) + h;\n}\n\n\nfunction uricom_enc(txt, do_fb_enc) {\n    try {\n        return encodeURIComponent(txt);\n    }\n    catch (ex) {\n        console.log(\"uce-err [\" + txt + \"]\");\n        if (do_fb_enc)\n            return esc(txt);\n\n        return txt;\n    }\n}\n\nfunction url_enc(txt) {\n    var parts = txt.split('/'),\n        ret = [];\n\n    for (var a = 0; a < parts.length; a++)\n        ret.push(uricom_enc(parts[a]));\n\n    return ret.join('/');\n}\n\n\nfunction uricom_dec(txt) {\n    try {\n        return decodeURIComponent(txt);\n    }\n    catch (ex) {\n        console.log(\"ucd-err [\" + txt + \"]\");\n        return txt;\n    }\n}\n\n\nfunction uricom_sdec(txt) {\n    try {\n        return [decodeURIComponent(txt), true];\n    }\n    catch (ex) {\n        console.log(\"ucd-err [\" + txt + \"]\");\n        return [txt, false];\n    }\n}\n\n\nfunction uricom_adec(arr, li) {\n    var ret = [];\n    for (var a = 0; a < arr.length; a++) {\n        var txt = uricom_dec(arr[a]);\n        ret.push(li ? '<li>' + esc(txt) + '</li>' : txt);\n    }\n\n    return ret;\n}\n\n\nfunction get_evpath() {\n    var ret = location.pathname;\n\n    if (ret.indexOf('/') !== 0)\n        ret = '/' + ret;\n\n    if (ret.lastIndexOf('/') !== ret.length - 1)\n        ret += '/';\n\n    return ret;\n}\n\n\nfunction noq_href(el) {\n    return el.getAttribute('href').split('?')[0];\n}\n\n\nfunction pad2(v) {\n    return ('0' + v).slice(-2);\n}\n\n\nfunction unix2iso(ts) {\n    return new Date(ts * 1000).toISOString().replace(\"T\", \" \").slice(0, -5);\n}\n\n\nfunction unix2iso_localtime(ts) {\n    var o = new Date(ts * 1000),\n        p = pad2;\n    return \"{0}-{1}-{2} {3}:{4}:{5}\".format(\n        o.getFullYear(),\n        p(o.getMonth() + 1),\n        p(o.getDate()),\n        p(o.getHours()),\n        p(o.getMinutes()),\n        p(o.getSeconds()));\n}\n\n\nfunction s2ms(s) {\n    s = Math.floor(s);\n    var m = Math.floor(s / 60);\n    return m + \":\" + (\"0\" + (s - m * 60)).slice(-2);\n}\n\n\nvar isNum = function (v) {\n    var n = parseFloat(v);\n    return !isNaN(v - n) && n === v;\n};\nif (window.Number && Number.isFinite)\n    isNum = Number.isFinite;\n\n\nfunction f2f(val, nd) {\n    // 10.toFixed(1) returns 10.00 for certain values of 10\n    if (!isNum(val)) {\n        val = parseFloat(val);\n        if (!isNum(val))\n            val = 999;\n    }\n    val = (val * Math.pow(10, nd)).toFixed(0).split('.')[0];\n    return nd ? (val.slice(0, -nd) || '0') + '.' + val.slice(-nd) : val;\n}\n\n\nvar HSZ_U = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];\nfunction humansize(b, terse) {\n    var i = 0;\n    while (b >= 1000 && i < 5) { b /= 1024; i += 1; }\n    return (f2f(b, b >= 100 ? 0 : b >= 10 ? 1 : 2) +\n        ' ' + (terse ? HSZ_U[i].charAt(0) : HSZ_U[i]));\n}\nfunction humansize_su(b) {\n    var i = 0;\n    while (b >= 1000 && i < 5) { b /= 1024; i += 1; }\n    return [b, HSZ_U[i]];\n}\nfunction humansize_0(b) {\n    return '' + b;\n}\nfunction humansize_1(b) {\n    return ('' + b).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \" \");\n}\nfunction humansize_2g(b) {\n    var z = humansize_su(b), u = z[1].charAt(0); b = z[0];\n    return [f2f(b, b >= 100 ? 0 : b >= 10 ? 1 : 2) + ' ' + u, u];\n}\nfunction humansize_3g(b) {\n    var z = humansize_su(b), u = z[1].charAt(0); b = z[0];\n    return [f2f(b, b >= 10 ? 0 : 1) + ' ' + u, u];\n}\nfunction humansize_4g(b) {\n    var z = humansize_su(b), u = z[1]; b = z[0];\n    return [parseFloat(b.toFixed(b >= 100 ? 0 : b >= 10 ? 1 : 2)) + ' ' + u, u.charAt(0)];\n}\nfunction humansize_5g(b) {\n    var z = humansize_su(b), u = z[1]; b = z[0];\n    return [parseFloat(b.toFixed(b >= 10 ? 0 : 1)) + ' ' + u, u.charAt(0)];\n}\nfunction humansize_2(b) {\n    return humansize_2g(b)[0];\n}\nfunction humansize_3(b) {\n    return humansize_3g(b)[0];\n}\nfunction humansize_4(b) {\n    return humansize_4g(b)[0];\n}\nfunction humansize_5(b) {\n    return humansize_5g(b)[0];\n}\nfunction humansize_2c(b) {\n    var v = humansize_2g(b);\n    return '<span class=\"fsz_' + v[1].charAt(0) + '\">' + v[0] + '</span>';\n}\nfunction humansize_3c(b) {\n    var v = humansize_3g(b);\n    return '<span class=\"fsz_' + v[1].charAt(0) + '\">' + v[0] + '</span>';\n}\nfunction humansize_4c(b) {\n    var v = humansize_4g(b);\n    return '<span class=\"fsz_' + v[1].charAt(0) + '\">' + v[0] + '</span>';\n}\nfunction humansize_5c(b) {\n    var v = humansize_5g(b);\n    return '<span class=\"fsz_' + v[1].charAt(0) + '\">' + v[0] + '</span>';\n}\nfunction humansize_fuzzy(b) {\n    if (b <= 0) return \"yes\";\n\tif (b <= 80) return \"hullkort\";\n\tif (b <= 368640) return \"5¼ DD\";\n\tif (b <= 1474560) return \"save icon\";\n\tif (b <= 2880000) return \"3½ Extended\";\n\tif (b <= 13107200) return \"C90 Tape\";\n\tif (b <= 21000000) return \"Floptical\";\n\tif (b <= 33554432) return \"MPMan F10\";\n\tif (b <= 50000000) return \"creditcardCD\";\n\tif (b <= 100663296) return \"Zipdisk\";\n\tif (b <= 170000000) return \"MD\";\n\tif (b <= 220200960) return \"8cm CD\";\n\tif (b <= 737280000) return \"CD-R\";\n\tif (b <= 900000000) return \"UMD\";\n\tif (b <= 1300000000) return \"GD-ROM\";\n\tif (b <= 4700000000) return \"DVD\";\n\tif (b <= 9400000000) return \"DVD-DL\";\n\tif (b <= 25025000000) return \"BluRei\";\n\tif (b <= 50050000000) return \"BD-DL\";\n\treturn \"LTO\";\n}\nvar humansize_fmts = ['0', '1', '2', '2c', '3', '3c', '4', '4c', '5', '5c', 'fuzzy'];\nwindow.filesizefun = (function () {\n    var v = sread('fszfmt', humansize_fmts);\n    return window['humansize_' + (v || window.dfszf)] || humansize_1;\n})();\n\n\nfunction humantime(v) {\n    if (v >= 60 * 60 * 24)\n        return shumantime(v);\n\n    try {\n        return /.*(..:..:..).*/.exec(new Date(v * 1000).toUTCString())[1];\n    }\n    catch (ex) {\n        return v;\n    }\n}\n\n\nfunction shumantime(v, long) {\n    if (v < 10)\n        return f2f(v, 2) + 's';\n    if (v < 60)\n        return f2f(v, 1) + 's';\n\n    v = parseInt(v);\n    var st = [[60 * 60 * 24, 60 * 60, 'd'], [60 * 60, 60, 'h'], [60, 1, 'm']];\n\n    for (var a = 0; a < st.length; a++) {\n        var m1 = st[a][0],\n            m2 = st[a][1],\n            ch = st[a][2];\n\n        if (v < m1)\n            continue;\n\n        var v1 = parseInt(v / m1),\n            v2 = ('0' + parseInt((v % m1) / m2)).slice(-2);\n\n        return v1 + ch + (v1 >= 10 || v2 == '00' ? '' : v2 + (\n            long && a < st.length - 1 ? st[a + 1][2] : ''));\n    }\n}\n\n\nfunction lhumantime(v) {\n    var t = shumantime(v, 1);\n    if (/[0-9]$/.exec(t))\n        t += 's';\n\n    var tp = t.replace(/([a-z])/g, \" $1 \").split(/ /g).slice(0, -1);\n\n    if (!L || tp.length < 2 || tp[1].indexOf('$') + 1)\n        return t;\n\n    var u, n, ret = '';\n    for (var a = 0; a < tp.length; a += 2) {\n        n = tp[a];\n        u = L.ht_h5 ? (n==1 ? 1 : (n>1&&n<5) ? 2 : 5) :\n            (n==1 ? 1 : 2);\n        ret += tp[a] + ' ' + L['ht_' + tp[a + 1] + u] + L.ht_and;\n    }\n\n    return ret.slice(0, -L.ht_and.length);\n}\n\n\nfunction clamp(v, a, b) {\n    return Math.min(Math.max(v, a), b);\n}\n\n\nfunction has(haystack, needle) {\n    try { return haystack.includes(needle); } catch (ex) { }\n\n    for (var a = 0; a < haystack.length; a++)\n        if (haystack[a] == needle)\n            return true;\n\n    return false;\n}\n\n\nfunction apop(arr, v) {\n    var ofs = arr.indexOf(v);\n    if (ofs !== -1)\n        arr.splice(ofs, 1);\n}\n\n\nfunction jcp1(obj) {\n    return JSON.parse(JSON.stringify(obj));\n}\n\n\nfunction jcp2(src) {\n    if (Array.isArray(src)) {\n        var ret = [];\n        for (var a = 0; a < src.length; ++a) {\n            var sub = src[a];\n            ret.push((sub === null) ? sub : (sub instanceof Date) ? new Date(sub.valueOf()) : (typeof sub === 'object') ? jcp2(sub) : sub);\n        }\n    } else {\n        var ret = {};\n        for (var key in src) {\n            var sub = src[key];\n            ret[key] = sub === null ? sub : (sub instanceof Date) ? new Date(sub.valueOf()) : (typeof sub === 'object') ? jcp2(sub) : sub;\n        }\n    }\n    return ret;\n};\n\n\n// jcp1 50% faster on android-chrome, jcp2 7x everywhere else\nvar jcp = MOBILE && CHROME ? jcp1 : jcp2;\n\n\nfunction sdrop(key) {\n    try {\n        STG.removeItem(key);\n    }\n    catch (ex) { }\n}\n\nfunction sread(key, al) {\n    try {\n        var ret = STG.getItem(key);\n        return (!al || has(al, ret)) ? ret : null;\n    }\n    catch (e) {\n        return null;\n    }\n}\n\nfunction swrite(key, val) {\n    try {\n        if (val === undefined || val === null)\n            STG.removeItem(key);\n        else\n            STG.setItem(key, val);\n    }\n    catch (e) { }\n}\n\nfunction jread(key, fb) {\n    var str = sread(key);\n    if (!str)\n        return fb;\n\n    try {\n        // '' throws, null is ok, sasuga\n        return JSON.parse(str);\n    }\n    catch (e) {\n        return fb;\n    }\n}\n\nfunction jwrite(key, val) {\n    if (!val)\n        swrite(key);\n    else\n        swrite(key, JSON.stringify(val));\n}\n\nfunction icfg_get(name, defval) {\n    return parseInt(fcfg_get(name, defval));\n}\n\nfunction fcfg_get(name, defval) {\n    var o = ebi(name),\n        val = parseFloat(sread(name));\n\n    if (!isNum(val))\n        return parseFloat(o && o.value !== '' ? o.value : defval);\n\n    if (o)\n        o.value = val;\n\n    return val;\n}\n\nfunction scfg_get(name, defval) {\n    var o = ebi(name),\n        val = sread(name);\n\n    if (val === null)\n        val = defval;\n\n    if (o)\n        o.value = val;\n\n    return val;\n}\n\nfunction bcfg_get(name, defval) {\n    var o = ebi(name);\n    if (!o)\n        return defval;\n\n    var val = sread(name);\n    if (val === null)\n        val = defval;\n    else\n        val = (val == '1');\n\n    bcfg_upd_ui(name, val);\n    return val;\n}\n\nfunction bcfg_set(name, val) {\n    swrite(name, val ? '1' : '0');\n    bcfg_upd_ui(name, val);\n    return val;\n}\n\nfunction bcfg_upd_ui(name, val) {\n    var o = ebi(name);\n    if (!o)\n        return val;\n\n    if (o.getAttribute('type') == 'checkbox')\n        o.checked = val;\n    else if (o) {\n        clmod(o, 'on', val);\n    }\n    return val;\n}\n\nfunction bcfg_bind(obj, oname, cname, defval, cb, un_ev) {\n    var v = bcfg_get(cname, defval),\n        el = ebi(cname);\n\n    obj[oname] = v;\n    if (el)\n        el.onclick = function (e) {\n            if (un_ev !== false)\n                ev(e);\n\n            obj[oname] = bcfg_set(cname, !obj[oname]);\n            if (cb)\n                cb(obj[oname]);\n        };\n\n    return v;\n}\n\nfunction scfg_bind(obj, oname, cname, defval, cb) {\n    var v = scfg_get(cname, defval),\n        el = ebi(cname);\n\n    obj[oname] = v;\n    if (el)\n        el.oninput = function (e) {\n            swrite(cname, obj[oname] = this.value);\n            if (cb)\n                cb(obj[oname]);\n        };\n\n    return v;\n}\n\nfunction setck(v, cb) {\n    var xhr = new XHR();\n    xhr.open('GET', SR + '/?setck=' + v, true);\n    xhr.onload = cb;\n    xhr.send();\n}\n\nwindow.unix2ui = (function () {\n    var v = sread('utctid');\n    v = v ? (v === '0') : (window.dutc === false);\n    return v ? unix2iso_localtime : unix2iso;\n})();\n\n\nfunction hist_push(url) {\n    console.log(\"h-push \" + url);\n    try {\n        history.pushState(url, url, url);\n    }\n    catch (ex) { }\n}\n\nfunction hist_replace(url) {\n    console.log(\"h-repl \" + url);\n    try {\n        history.replaceState(url, url, url);\n    }\n    catch (ex) { }  // ff \"The operation is insecure.\" on rapid switches\n}\n\nfunction sethash(hv) {\n    if (window.history && history.replaceState) {\n        hist_replace(location.pathname + location.search + '#' + hv);\n    }\n    else {\n        location.hash = hv;\n    }\n}\n\n\nfunction dl_file(url) {\n    console.log('DL [%s]', url);\n    qsr('#dlfth');\n    var o = mknod('a', 'dlfth');\n    o.setAttribute('href', url);\n    o.setAttribute('download', '');\n    document.body.appendChild(o);\n    ebi('dlfth').click();\n    qsr('#dlfth');\n}\n\n\nfunction cliptxt(txt, ok) {\n    var fb = function () {\n        console.log('clip-fb');\n        var o = mknod('textarea');\n        o.value = txt;\n        document.body.appendChild(o);\n        o.focus();\n        o.select();\n        document.execCommand(\"copy\");\n        document.body.removeChild(o);\n        ok();\n    };\n    try {\n        if (!window.isSecureContext)\n            throw 1;\n        navigator.clipboard.writeText(txt).then(ok, fb);\n    }\n    catch (ex) { fb(); }\n}\n\n\nfunction Debounce(delay) {\n    var r = this;\n    r.delay = delay;\n    r.timer = 0;\n    r.t_hit = 0;\n    r.t_run = 0;\n    r.q = [];\n\n    r.add = function (fun, run) {\n        r.rm(fun);\n        r.q.push(fun);\n\n        if (run)\n            fun();\n    };\n\n    r.rm = function (fun) {\n        apop(r.q, fun);\n    };\n\n    r.run = function () {\n        if (crashed)\n            return;\n\n        r.t_run = Date.now();\n\n        var q = r.q.slice(0);\n        for (var a = 0; a < q.length; a++)\n            q[a]();\n    };\n\n    r.hit = function () {\n        if (crashed)\n            return;\n\n        var now = Date.now(),\n            td_hit = now - r.t_hit,\n            td_run = now - r.t_run;\n\n        if (td_run >= r.delay * 2)\n            r.t_run = now;\n\n        if (td_run >= r.delay && td_run <= r.delay * 2) {\n            // r.delay is also deadline\n            clearTimeout(r.timer);\n            return r.run();\n        }\n\n        if (td_hit < r.delay / 5)\n            return;\n\n        clearTimeout(r.timer);\n        r.timer = setTimeout(r.run, r.delay);\n        r.t_hit = now;\n    };\n};\n\nvar onresize100 = new Debounce(100);\nwindow.addEventListener('resize', onresize100.hit);\n\n\nvar timer = (function () {\n    var r = {};\n    r.q = [];\n    r.last = 0;\n    r.fs = 0;\n    r.fc = 0;\n\n    r.add = function (fun, run) {\n        r.rm(fun);\n        r.q.push(fun);\n\n        if (run)\n            fun();\n    };\n\n    r.rm = function (fun) {\n        apop(r.q, fun);\n    };\n\n    var doevents = function () {\n        if (crashed)\n            return;\n\n        if (Date.now() - r.last < 69)\n            return;\n\n        var q = r.q.slice(0);\n        for (var a = 0; a < q.length; a++)\n            q[a]();\n\n        r.last = Date.now();\n        //r.fc++; if (r.last - r.fs >= 2000) { console.log(r.last - r.fs, r.fc); r.fs = r.last; r.fc = 0; }\n    }\n    setInterval(doevents, 100);\n\n    return r;\n})();\n\n\nvar tt = (function () {\n    var r = {\n        \"tt\": mknod(\"div\", 'tt'),\n        \"th\": mknod(\"div\", 'tth'),\n        \"en\": !window.notooltips,\n        \"el\": null,\n        \"skip\": false,\n        \"lvis\": 0\n    };\n\n    r.th.innerHTML = '?';\n    document.body.appendChild(r.tt);\n    document.body.appendChild(r.th);\n\n    var prev = null;\n    r.cshow = function () {\n        if (this !== prev)\n            r.show.call(this);\n\n        prev = this;\n    };\n\n    var tev, vh;\n    r.dshow = function (e) {\n        clearTimeout(tev);\n        if (!r.getmsg(this))\n            return;\n\n        if (Date.now() - r.lvis < 400)\n            return r.show.call(this);\n\n        tev = setTimeout(r.show.bind(this), 800);\n        if (TOUCH)\n            return;\n\n        vh = window.innerHeight;\n        this.addEventListener('mousemove', r.move);\n        clmod(r.th, 'act', 1);\n        r.move(e);\n    };\n\n    r.getmsg = function (el) {\n        if (APPLEM && QS('body.bbox-open'))\n            return;\n\n        var cfg = sread('tooltips');\n        if (cfg !== null && cfg != '1')\n            return;\n\n        return el.getAttribute('tt');\n    };\n\n    r.show = function () {\n        clearTimeout(tev);\n        if (r.skip) {\n            r.skip = false;\n            return;\n        }\n        var msg = r.getmsg(this);\n        if (!msg)\n            return;\n\n        if (msg.startsWith('`')) {\n            var x = false;\n            msg = msg.slice(1);\n            while (msg.indexOf('`') + 1)\n                msg = msg.replace('`', (x = !x) ? '<code>' : '</code>')\n        }\n\n        r.el = this;\n        var pos = this.getBoundingClientRect(),\n            dir = this.getAttribute('ttd') || '',\n            margin = parseFloat(this.getAttribute('ttm') || 0),\n            top = pos.top < window.innerHeight / 2,\n            big = this.className.indexOf(' ttb') !== -1;\n\n        if (dir.indexOf('u') + 1) top = false;\n        if (dir.indexOf('d') + 1) top = true;\n\n        clmod(r.th, 'act');\n        clmod(r.tt, 'b', big);\n        r.tt.style.left = '0';\n        r.tt.style.top = '0';\n\n        r.tt.innerHTML = msg.replace(/\\$N/g, \"<br />\");\n        r.el.addEventListener('mouseleave', r.hide);\n        window.addEventListener('scroll', r.hide);\n        clmod(r.tt, 'show', 1);\n\n        var tw = r.tt.offsetWidth,\n            x = pos.left + (pos.right - pos.left) / 2 - tw / 2;\n\n        if (x + tw >= window.innerWidth - 24)\n            x = window.innerWidth - tw - 24;\n\n        if (x < 0)\n            x = 12;\n\n        r.tt.style.left = x + 'px';\n        r.tt.style.top = top ? (margin + pos.bottom) + 'px' : 'auto';\n        r.tt.style.bottom = top ? 'auto' : (margin + window.innerHeight - pos.top) + 'px';\n    };\n\n    r.hide = function (e) {\n        //ev(e);  // eats checkbox-label clicks\n        clearTimeout(tev);\n        window.removeEventListener('scroll', r.hide);\n\n        clmod(r.tt, 'b');\n        clmod(r.th, 'act');\n        if (clmod(r.tt, 'show'))\n            r.lvis = Date.now();\n\n        if (r.el)\n            r.el.removeEventListener('mouseleave', r.hide);\n\n        if (e && e.target)\n            e.target.removeEventListener('mousemove', r.move);\n    };\n\n    r.move = function (e) {\n        var sy = e.clientY + 128 > vh ? -1 : 1;\n        r.th.style.left = (e.pageX + 12) + 'px';\n        r.th.style.top = (e.pageY + 12 * sy) + 'px';\n    };\n\n    if (TOUCH) {\n        var f1 = r.show,\n            f2 = r.hide,\n            q = [];\n\n        // if an onclick-handler creates a new timer,\n        // webkits delay the entire handler by up to 401ms,\n        // win by using a shared timer instead\n\n        timer.add(function () {\n            while (q.length && Date.now() >= q[0][0])\n                q.shift()[1]();\n        });\n\n        r.show = function () {\n            q.push([Date.now() + 100, f1.bind(this)]);\n        };\n        r.hide = function () {\n            q.push([Date.now() + 100, f2.bind(this)]);\n        };\n    }\n\n    r.tt.onclick = r.hide;\n\n    r.att = function (ctr) {\n        var _cshow = r.en ? r.cshow : null,\n            _dshow = r.en ? r.dshow : null,\n            _hide = r.en ? r.hide : null,\n            o = ctr.querySelectorAll('*[tt]');\n\n        for (var a = o.length - 1; a >= 0; a--) {\n            o[a].addEventListener('focus', _cshow);\n            o[a].addEventListener('blur', _hide);\n            o[a].addEventListener('mouseenter', _dshow);\n            o[a].addEventListener('mouseleave', _hide);\n        }\n        r.hide();\n    }\n\n    r.init = function () {\n        bcfg_bind(r, 'en', 'tooltips', r.en, r.init);\n        r.att(document);\n    };\n\n    return r;\n})();\n\n\nfunction lf2br(txt) {\n    var html = '', hp = txt.split(/(?=<.?pre>)/i);\n    for (var a = 0; a < hp.length; a++)\n        html += hp[a].startsWith('<pre>') ? hp[a] :\n            hp[a].replace(/<br ?.?>\\n/g, '\\n').replace(/\\n<br ?.?>/g, '\\n').replace(/\\n/g, '<br />\\n');\n\n    return html;\n}\n\n\nfunction hunpre(txt) {\n    return ('' + txt).replace(/^<pre>/, '');\n}\nfunction unpre(txt) {\n    return esc(hunpre(txt));\n}\n\n\nvar toast = (function () {\n    var r = {},\n        te = null,\n        scrolling = false,\n        obj = mknod('div', 'toast');\n\n    document.body.appendChild(obj);\n    r.visible = false;\n    r.txt = null;\n    r.tag = obj;  // filler value (null is scary)\n    r.p_txt = '';\n    r.p_sec = 0;\n    r.p_t = 0;\n\n    var scrollchk = function () {\n        if (scrolling)\n            return;\n\n        var tb = ebi('toastb'),\n            vis = tb.offsetHeight,\n            all = tb.scrollHeight;\n\n        if (8 + vis >= all)\n            return;\n\n        clmod(obj, 'scroll', 1);\n        scrolling = true;\n    }\n\n    var unscroll = function () {\n        timer.rm(scrollchk);\n        clmod(obj, 'scroll');\n        scrolling = false;\n    }\n\n    r.hide = function (e) {\n        if (this === ebi('toastc'))\n            ev(e);\n\n        unscroll();\n        clearTimeout(te);\n        clmod(obj, 'vis');\n        r.visible = false;\n        r.tag = obj;\n        if (!WebAssembly)\n            te = setTimeout(function () {\n                obj.className = 'hide';\n            }, 500);\n    };\n\n    r.show = function (cl, sec, txt, tag) {\n        txt = (txt + '').slice(0, 16384);\n\n        var same = r.visible && txt == r.p_txt && r.p_sec == sec,\n            delta = Date.now() - r.p_t;\n\n        if (same && delta < 100)\n            return;\n\n        r.p_txt = txt;\n        r.p_sec = sec;\n        r.p_t = Date.now();\n\n        clearTimeout(te);\n        if (sec)\n            te = setTimeout(r.hide, sec * 1000);\n\n        if (same && delta < 1000) {\n            var tb = ebi('toastt');\n            if (tb) {\n                tb.style.animation = 'none';\n                tb.offsetHeight;\n                tb.style.animation = null;\n            }\n            return;\n        }\n\n        if (txt.indexOf('<body>') + 1)\n            txt = txt.slice(0, txt.indexOf('<')) + ' [...]';\n\n        var html = '';\n        if (sec) {\n            setcvar('--tmtime', (sec - 0.15) + 's');\n            setcvar('--tmstep', Math.floor(sec * 20));\n            html += '<div id=\"toastt\"></div>';\n        }\n        obj.innerHTML = html + '<a href=\"#\" id=\"toastc\">x</a><div id=\"toastb\">' + lf2br(txt) + '</div>';\n        obj.className = cl;\n        sec += obj.offsetWidth;\n        obj.className += ' vis';\n        ebi('toastc').onclick = r.hide;\n        timer.add(scrollchk);\n        r.visible = true;\n        r.txt = txt;\n        r.tag = tag;\n    };\n\n    r.ok = function (sec, txt, tag, cls) {\n        r.show('ok ' + (cls || ''), sec, txt, tag);\n    };\n    r.inf = function (sec, txt, tag, cls) {\n        r.show('inf ' + (cls || ''), sec, txt, tag);\n    };\n    r.warn = function (sec, txt, tag, cls) {\n        r.show('warn ' + (cls || ''), sec, txt, tag);\n    };\n    r.err = function (sec, txt, tag, cls) {\n        r.show('err ' + (cls || ''), sec, txt, tag);\n    };\n\n    return r;\n})();\n\n\nvar modal = (function () {\n    var r = {},\n        q = [],\n        o = null,\n        scrolling = null,\n        cb_up = null,\n        cb_ok = null,\n        cb_ng = null,\n        sel_0 = 0,\n        sel_1 = 0,\n        tok, tng, prim, sec, ok_cancel;\n\n    r.load = function () {\n        tok = (L && L.m_ok) || 'OK';\n        tng = (L && L.m_ng) || 'Cancel';\n        prim = '<a href=\"#\" id=\"modal-ok\">' + tok + '</a>';\n        sec = '<a href=\"#\" id=\"modal-ng\">' + tng + '</a>';\n        ok_cancel = WINDOWS ? prim + sec : sec + prim;\n    };\n    r.load();\n\n    r.busy = false;\n    r.nofocus = 0;\n\n    r.show = function (html) {\n        tt.hide();\n        o = mknod('div', 'modal');\n        o.innerHTML = '<table><tr><td><div id=\"modalc\">' + html + '</div></td></tr></table>';\n        document.body.appendChild(o);\n        document.addEventListener('keydown', onkey);\n        r.busy = true;\n\n        var a = ebi('modal-ng');\n        if (a)\n            a.onclick = ng;\n\n        a = ebi('modal-ok');\n        a.addEventListener('blur', onblur);\n        a.onclick = ok;\n\n        var inp = ebi('modali');\n        (inp || a).focus();\n        if (inp)\n            setTimeout(function () {\n                inp.setSelectionRange(sel_0, sel_1, \"forward\");\n            }, 0);\n\n        document.addEventListener('focus', onfocus);\n        document.addEventListener('selectionchange', onselch);\n        timer.add(scrollchk, 1);\n        timer.add(onfocus);\n        if (cb_up)\n            setTimeout(cb_up, 1);\n    };\n\n    r.hide = function () {\n        timer.rm(onfocus);\n        timer.rm(scrollchk);\n        scrolling = null;\n        try {\n            ebi('modal-ok').removeEventListener('blur', onblur);\n        }\n        catch (ex) { }\n        document.removeEventListener('selectionchange', onselch);\n        document.removeEventListener('focus', onfocus);\n        document.removeEventListener('keydown', onkey);\n        try {\n            o.parentNode.removeChild(o);\n        }\n        catch (ex) {\n            if (r.busy)\n                alert('WARNING: you have a browser-extension which is causing problems');\n        }\n        r.busy = false;\n        setTimeout(next, 50);\n    };\n    var ok = function (e) {\n        ev(e);\n        var v = ebi('modali');\n        v = v ? v.value : true;\n        r.hide();\n        if (cb_ok)\n            cb_ok(v);\n    };\n    var ng = function (e) {\n        ev(e);\n        r.hide();\n        if (cb_ng)\n            cb_ng(null);\n    };\n\n    var scrollchk = function () {\n        if (scrolling === true)\n            return;\n\n        var o = ebi('modalc'),\n            vis = o.offsetHeight,\n            all = o.scrollHeight,\n            nsc = 8 + vis < all;\n\n        if (scrolling !== nsc)\n            clmod(o, 'yk', !nsc);\n\n        scrolling = nsc;\n    };\n\n    var onselch = function () {\n        try {\n            if (window.getSelection() + '')\n                r.nofocus = 15;\n        }\n        catch (ex) { }\n    };\n\n    var onblur = function () {\n        r.nofocus = 3;\n    };\n\n    var onfocus = function (e) {\n        if (MOBILE)\n            return;\n\n        var ctr = ebi('modalc');\n        if (!ctr || !ctr.contains || !document.activeElement || ctr.contains(document.activeElement))\n            return;\n\n        setTimeout(function () {\n            if (--r.nofocus >= 0)\n                return;\n\n            if (ctr = ebi('modal-ok'))\n                ctr.focus();\n        }, 20);\n        ev(e);\n    };\n\n    var onkey = function (e) {\n        var k = (e.key || e.code) + '',\n            eok = ebi('modal-ok'),\n            eng = ebi('modal-ng'),\n            ae = document.activeElement;\n\n        if ((k == 'Space' || k == 'Spacebar' || k == ' ') && ae && (ae === eok || ae === eng))\n            k = 'Enter';\n\n        if (k.endsWith('Enter')) {\n            if (ae && ae == eng)\n                return ng(e);\n\n            return ok(e);\n        }\n\n        if ((k == 'ArrowLeft' || k == 'ArrowRight' || k == 'Left' || k == 'Right') && eng && (ae == eok || ae == eng))\n            return (ae == eok ? eng : eok).focus() || ev(e);\n\n        if (k == 'Escape' || k == 'Esc')\n            return ng(e);\n    }\n\n    var next = function () {\n        if (!r.busy && q.length)\n            q.shift()();\n    }\n\n    r.alert = function (html, cb, fun) {\n        q.push(function () {\n            _alert(lf2br(html), cb, fun);\n        });\n        next();\n    };\n    var _alert = function (html, cb, fun) {\n        cb_ok = cb_ng = cb;\n        cb_up = fun;\n        html += '<div id=\"modalb\"><a href=\"#\" id=\"modal-ok\">OK</a></div>';\n        r.show(html);\n    }\n\n    r.confirm = function (html, cok, cng, fun, btns) {\n        q.push(function () {\n            _confirm(lf2br(html), cok, cng, fun, btns);\n        });\n        next();\n    }\n    var _confirm = function (html, cok, cng, fun, btns) {\n        cb_ok = cok;\n        cb_ng = cng === undefined ? cok : cng;\n        cb_up = fun;\n        html += '<div id=\"modalb\">' + (btns || ok_cancel) + '</div>';\n        r.show(html);\n    }\n\n    r.prompt = function (html, v, cok, cng, fun, so0, so1) {\n        q.push(function () {\n            _prompt(lf2br(html), v, cok, cng, fun, so0, so1);\n        });\n        next();\n    }\n    var _prompt = function (html, v, cok, cng, fun, so0, so1) {\n        cb_ok = cok;\n        cb_ng = cng === undefined ? cok : null;\n        cb_up = fun;\n        sel_0 = so0 || 0;\n        sel_1 = so1 === undefined ? v.length : so1;\n        html += '<input id=\"modali\" type=\"text\" ' + NOAC + ' /><div id=\"modalb\">' + ok_cancel + '</div>';\n        r.show(html);\n\n        ebi('modali').value = v || '';\n    }\n\n    return r;\n})();\n\n\nfunction winpopup(txt) {\n    fetch(get_evpath(), {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'\n        },\n        body: 'msg=' + uricom_enc(Date.now() + ', ' + txt)\n    });\n}\n\n\nvar last_repl = null;\nfunction repl_load() {\n    var ipre = ebi('repl_pre'),\n        tb = ebi('modali');\n\n    function getpres() {\n        var o, ret = jread(\"repl_pre\", []);\n        if (!ret.length)\n            ret = [\n                'var v=Object.keys(localStorage); v.sort(); JSON.stringify(v)',\n                \"for (var a of QSA('#files a[id]')) a.setAttribute('download','')\",\n                'console.hist.slice(-50).join(\"\\\\n\")'\n            ];\n\n        ipre.innerHTML = '<option value=\"\"></option>';\n        for (var a = 0; a < ret.length; a++) {\n            o = mknod('option');\n            o.setAttribute('value', ret[a]);\n            o.textContent = ret[a];\n            ipre.appendChild(o);\n        }\n        last_repl = ipre.value = (last_repl || (ret.length ? ret.slice(-1)[0] : ''));\n        return ret;\n    }\n    ebi('repl_pdel').onclick = function (e) {\n        var val = ipre.value,\n            pres = getpres();\n\n        apop(pres, val);\n        jwrite('repl_pre', pres);\n        getpres();\n    };\n    ebi('repl_pnew').onclick = function (e) {\n        var val = tb.value,\n            pres = getpres();\n\n        apop(pres, ipre.value);\n        pres.push(val);\n        jwrite('repl_pre', pres);\n        getpres();\n        ipre.value = val;\n    };\n    ipre.oninput = ipre.onchange = function () {\n        tb.value = last_repl = ipre.value;\n    };\n    tb.oninput = function () {\n        last_repl = this.value;\n    };\n    getpres();\n    tb.value = last_repl;\n    setTimeout(function () {\n        tb.setSelectionRange(0, tb.value.length, \"forward\");\n    }, 10);\n}\nfunction repl(e) {\n    ev(e);\n    var html = [\n        '<p>js repl (prefix with <code>,</code> to allow raise)</p>',\n        '<p><select id=\"repl_pre\"></select>',\n        ' &nbsp; <button id=\"repl_pdel\">❌ del</button>',\n        ' &nbsp; <button id=\"repl_pnew\">💾 SAVE</button></p>'\n    ];\n\n    modal.prompt(html.join(''), '', function (cmd) {\n        if (!cmd)\n            return toast.inf(3, 'eval aborted');\n\n        cmd = unsmart(cmd);\n\n        if (cmd.startsWith(',')) {\n            evalex_fatal = true;\n            return modal.alert(esc(eval(cmd.slice(1)) + ''));\n        }\n\n        try {\n            modal.alert(esc(eval(cmd) + ''));\n        }\n        catch (ex) {\n            modal.alert('<h6>exception</h6>' + esc(ex + ''));\n        }\n    }, undefined, repl_load);\n}\nif (ebi('repl'))\n    ebi('repl').onclick = repl;\n\n\nvar md_plug = {};\nvar md_plug_err = function (ex, js) {\n    if (ex)\n        console.log(ex, js);\n};\nfunction load_md_plug(md_text, plug_type, defer) {\n    if (defer)\n        md_plug[plug_type] = null;\n\n    if (plug_type == 'pre')\n        try {\n            md_text = md_thumbs(md_text);\n        }\n        catch (ex) {\n            toast.warn(30, '' + ex);\n        }\n\n    if (!have_emp)\n        return md_text;\n\n    var find = '\\n```copyparty_' + plug_type + '\\n',\n        md = '\\n' + md_text.replace(/\\r/g, '') + '\\n',\n        ofs = md.indexOf(find),\n        ofs2 = md.indexOf('\\n```', ofs + 1);\n\n    if (ofs < 0 || ofs2 < 0)\n        return md_text;\n\n    var js = md.slice(ofs + find.length, ofs2 + 1);\n    md = md.slice(0, ofs + 1) + md.slice(ofs2 + 4);\n    md = md.replace(/$/g, '\\r');\n\n    if (defer) { // insert into sandbox\n        md_plug[plug_type] = js;\n        return md;\n    }\n\n    var old_plug = md_plug[plug_type];\n    if (!old_plug || old_plug[1] != js) {\n        js = 'const loc = new URL(\"' + location.href + '\"), x = { ' + js + ' }; x;';\n        try {\n            var x = eval(js);\n            if (x['ctor']) {\n                x['ctor']();\n                delete x['ctor'];\n            }\n        }\n        catch (ex) {\n            md_plug[plug_type] = null;\n            md_plug_err(ex, js);\n            return md;\n        }\n        md_plug[plug_type] = [x, js];\n    }\n\n    return md;\n}\nfunction md_thumbs(md) {\n    if (!/(^|\\n)<!-- th -->/.exec(md))\n        return md;\n\n    // `!th[flags](some.jpg)`\n    // flags: nothing or \"l\" or \"r\"\n\n    md = md.split(/!th\\[/g);\n    for (var a = 1; a < md.length; a++) {\n        if (!/^[^\\]!()]*\\]\\([^\\][!()]+\\)/.exec(md[a])) {\n            md[a] = '!th[' + md[a];\n            continue;\n        }\n\n        var o1 = md[a].indexOf(']('),\n            o2 = md[a].indexOf(')', o1),\n            alt = md[a].slice(0, o1),\n            flags = alt.split(','),\n            url = md[a].slice(o1 + 2, o2),\n            float = has(flags, 'l') ? 'left' : has(flags, 'r') ? 'right' : '';\n\n        if (!/[?&]cache/.exec(url))\n            url = addq(url, 'cache=i');\n\n        md[a] = '<a href=\"' + url + '\" class=\"mdth mdth' + float.slice(0, 1) + '\"><img src=\"' + url + '&th=w\" alt=\"' + alt + '\" /></a>' + md[a].slice(o2 + 1);\n    }\n    return md.join('');\n}\nfunction md_th_set() {\n    var els = QSA('.mdth');\n    for (var a = 0, aa = els.length; a < aa; a++)\n        els[a].onclick = md_th_click;\n}\nfunction md_th_click(e) {\n    ev(e);\n    var url = this.getAttribute('href').split('?')[0];\n    if (window.sb_md)\n        window.parent.postMessage(\"imshow \" + url, \"*\");\n    else\n        thegrid.imshow(url);\n}\n\n\nvar svg_decl = '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n';\n\n\nvar favico = (function () {\n    var r = {};\n    r.en = true;\n    r.tag = null;\n\n    var gx = function (txt) {\n        return (svg_decl +\n            '<svg version=\"1.1\" viewBox=\"0 0 64 64\" xmlns=\"http://www.w3.org/2000/svg\">\\n' +\n            (r.bg ? '<rect width=\"100%\" height=\"100%\" rx=\"16\" fill=\"#' + r.bg + '\" />\\n' : '') +\n            '<text x=\"50%\" y=\"55%\" dominant-baseline=\"middle\" text-anchor=\"middle\"' +\n            ' font-family=\"sans-serif\" font-weight=\"bold\" font-size=\"64px\"' +\n            ' fill=\"#' + r.fg + '\">' + txt + '</text></svg>'\n        );\n    }\n\n    r.upd = function (txt, svg) {\n        if (!r.txt)\n            return;\n\n        var b64;\n        try {\n            b64 = btoa(svg ? svg_decl + svg : gx(r.txt));\n        }\n        catch (e1) {\n            try {\n                b64 = btoa(gx(encodeURIComponent(r.txt).replace(/%([0-9A-F]{2})/g,\n                    function x(m, v) { return String.fromCharCode('0x' + v); })));\n            }\n            catch (e2) {\n                try {\n                    b64 = btoa(gx(unescape(encodeURIComponent(r.txt))));\n                }\n                catch (e3) {\n                    return;\n                }\n            }\n        }\n\n        if (!r.tag) {\n            r.tag = mknod('link');\n            r.tag.rel = 'icon';\n            document.head.appendChild(r.tag);\n        }\n        r.tag.href = 'data:image/svg+xml;base64,' + b64;\n    };\n\n    r.init = function () {\n        clearTimeout(r.to);\n        var dv = (window.dfavico || '').trim().split(/ +/),\n            fg = dv.length < 2 ? 'fc5' : dv[1].toLowerCase() == 'none' ? '' : dv[1],\n            bg = dv.length < 3 ? '222' : dv[2].toLowerCase() == 'none' ? '' : dv[2];\n\n        scfg_bind(r, 'txt', 'icot', dv[0], r.upd);\n        scfg_bind(r, 'fg', 'icof', fg, r.upd);\n        scfg_bind(r, 'bg', 'icob', bg, r.upd);\n        r.upd();\n    };\n\n    r.to = setTimeout(r.init, 100);\n    return r;\n})();\n\n\nfunction cprop(name) {\n    return getComputedStyle(document.documentElement).getPropertyValue(name);\n}\n\n\nfunction bchrome() {\n    var v, o = QS('meta[name=theme-color]');\n    if (!o)\n        return;\n\n    try {\n        v = cprop('--bg-u3');\n    }\n    catch (ex) { }\n    o.setAttribute('content', v ? v : document.documentElement.className.indexOf('y') + 1 ? '#eee' : '#333');\n}\nbchrome();\n\nvar XC_CMSG = {\n    502: \"bad gateway (server offline)\",\n    503: \"server offline\",\n    504: \"gateway timeout (server busy)\",\n    529: \"gateway timeout (server busy)\",\n    520: \"unknown error from server\",\n    521: \"server offline\",\n    523: \"server offline\",\n    522: \"proxy timeout (server busy)\",\n    524: \"proxy timeout (server busy)\",\n    598: \"proxy timeout (server busy)\",\n    599: \"proxy timeout (server busy)\",\n};\nvar cf_cha_t = 0;\nfunction xhrchk(xhr, prefix, e404, lvl, tag) {\n    if (xhr.status < 400 && xhr.status >= 200)\n        return true;\n\n    if (tag === undefined)\n        tag = prefix;\n\n    var errtxt = ((xhr.response && xhr.response.err) || xhr.responseText) || '',\n        suf = '',\n        fun = toast[lvl || 'err'],\n        is_cf = /[Cc]loud[f]lare|>Just a mo[m]ent|#cf-b[u]bbles|Chec[k]ing your br[o]wser|\\/chall[e]nge-platform|\"chall[e]nge-error|nable Ja[v]aScript and cook/.test(errtxt);\n\n    if (errtxt.startsWith('<pre>'))\n        suf = '\\n\\nerror-details: «' + unpre(errtxt).split('\\n')[0].trim() + '»';\n    else\n        errtxt = esc(errtxt).slice(0, 32768);\n\n    if (xhr.status == 403 && !is_cf)\n        return toast.err(0, prefix + (L && L.xhr403 || \"403: access denied\\n\\ntry pressing F5, maybe you got logged out\") + suf, tag);\n\n    if (xhr.status == 404)\n        return toast.err(0, prefix + e404 + suf, tag);\n\n    if (!xhr.status && !errtxt)\n        return toast.err(0, prefix + L.xhr0);\n\n    if (is_cf && (xhr.status == 403 || xhr.status == 503)) {\n        var now = Date.now(), td = now - cf_cha_t;\n        if (td < 15000)\n            return;\n\n        cf_cha_t = now;\n        errtxt = 'Clou' + wah + 'dflare protection kicked in\\n\\n<strong>trying to fix it...</strong>';\n        fun = toast.warn;\n\n        qsr('#cf_frame');\n        var fr = mknod('iframe', 'cf_frame');\n        fr.src = SR + '/?cf_challenge';\n        document.body.appendChild(fr);\n    }\n\n    if (XC_CMSG[xhr.status] && (errtxt.indexOf('<html') + 1))\n        errtxt = XC_CMSG[xhr.status];\n\n    return fun(0, prefix + xhr.status + \": \" + errtxt, tag);\n}\n\nJ_UTL = 2;\n"
  },
  {
    "path": "copyparty/web/w.hash.js",
    "content": "\"use strict\";\n\n\nfunction hex2u8(txt) {\n    return new Uint8Array(txt.match(/.{2}/g).map(function (b) { return parseInt(b, 16); }));\n}\nfunction esc(txt) {\n    return txt.replace(/[&\"<>]/g, function (c) {\n        return {\n            '&': '&amp;',\n            '\"': '&quot;',\n            '<': '&lt;',\n            '>': '&gt;'\n        }[c];\n    });\n}\n\n\nvar subtle = null;\ntry {\n    subtle = crypto.subtle;\n    subtle.digest('SHA-512', new Uint8Array(1)).then(\n        function (x) { },\n        function (x) { load_fb(); }\n    );\n}\ncatch (ex) {\n    load_fb();\n}\nfunction load_fb() {\n    subtle = null;\n    if (self.hashwasm)\n        return;\n    importScripts('deps/sha512.hw.js');\n    console.log('using fallback hasher');\n}\n\n\nvar reader = null,\n    gc1, gc2, gc3,\n    busy = false;\n\n\nonmessage = (d) => {\n    if (d.data == 'nosubtle')\n        return load_fb();\n\n    if (d.data == 'ping')\n        return postMessage(['pong']);\n\n    if (busy)\n        return postMessage([\"panic\", 'worker got another task while busy']);\n\n    if (!reader)\n        reader = new FileReader();\n\n    var [nchunk, fobj, car, cdr] = d.data,\n        t0 = Date.now();\n\n    reader.onload = function (e) {\n        try {\n            // chrome gc forgets the filereader output; remind it\n            // (for some chromes, also necessary for subtle)\n            gc1 = e.target.result;\n            gc2 = new Uint8Array(gc1, 0, 1);\n            gc3 = new Uint8Array(gc1, gc1.byteLength - 1);\n\n            //console.log('[ w] %d HASH bgin', nchunk);\n            postMessage([\"read\", nchunk, cdr - car, Date.now() - t0]);\n            hash_calc(gc1);\n        }\n        catch (ex) {\n            busy = false;\n            postMessage([\"panic\", ex + '']);\n        }\n    };\n    reader.onerror = function () {\n        busy = false;\n        var err = esc('' + reader.error);\n\n        if (err.indexOf('NotReadableError') !== -1 || // win10-chrome defender\n            err.indexOf('NotFoundError') !== -1  // macos-firefox permissions\n        )\n            return postMessage([\"fail\", 'OS-error', err + ' @ ' + car]);\n\n        postMessage([\"ferr\", err]);\n    };\n    //console.log('[ w] %d read bgin', nchunk);\n    busy = true;\n    reader.readAsArrayBuffer(fobj.slice(car, cdr));\n\n\n    var hash_calc = function (buf) {\n        var hash_done = function (hashbuf) {\n            // stop gc from attempting to free early\n            if (!gc1 || !gc2 || !gc3)\n                return console.log('torch went out');\n\n            gc1 = gc2 = gc3 = null;\n            busy = false;\n            try {\n                var hslice = new Uint8Array(hashbuf).subarray(0, 33);\n                //console.log('[ w] %d HASH DONE', nchunk);\n                postMessage([\"done\", nchunk, hslice, cdr - car]);\n            }\n            catch (ex) {\n                postMessage([\"panic\", ex + '']);\n            }\n        };\n\n        // stop gc from attempting to free early\n        if (!gc1 || !gc2 || !gc3)\n            console.log('torch went out');\n\n        if (subtle)\n            subtle.digest('SHA-512', buf).then(hash_done);\n        else {\n            // note: lifting u8buf counterproductive for the chrome gc bug\n            var u8buf = new Uint8Array(buf);\n            hashwasm.sha512(u8buf).then(function (v) {\n                hash_done(hex2u8(v))\n            });\n        }\n    };\n}\n"
  },
  {
    "path": "docs/README.md",
    "content": "**NOTE:** there's more stuff (sharex config, service scripts, nginx configs, ...) in [`/contrib/`](/contrib/)\n\n\n\n# utilities\n\n## [`multisearch.html`](multisearch.html)\n* takes a list of filenames of youtube rips, grabs the youtube-id of each file, and does a search on the server for those\n* use it by putting it somewhere on the server and opening it as an html page\n* also serves as an extendable template for other specific search behaviors\n\n\n\n# other stuff\n\n## [`TODO.md`](TODO.md)\n* planned features / fixes / changes\n\n## [`example.conf`](example.conf)\n* example config file for `-c`\n\n## [`versus.md`](versus.md)\n* similar software / alternatives (with pros/cons)\n\n## [`changelog.md`](changelog.md)\n* occasionally grabbed from github release notes\n\n## [`synology-dsm.md`](synology-dsm.md)\n* running copyparty on a synology nas\n\n## [`devnotes.md`](devnotes.md)\n* technical stuff\n\n## [`rclone.md`](rclone.md)\n* notes on using rclone as a fuse client/server\n\n\n\n# junk\n\nalphabetical list of the remaining files\n\n| what | why |\n| -- | -- |\n| [biquad.html](biquad.html) | bruteforce calibrator for the audio equalizer since im not that good at maths |\n| [design.txt](design.txt) | initial brainstorming of the copyparty design, unmaintained, incorrect, sentimental value only |\n| [hls.html](hls.html) | experimenting with hls playback using `hls.js`, works p well, almost became a thing |\n| [music-analysis.sh](music-analysis.sh) | testing various bpm/key detection libraries before settling on the ones used in [`/bin/mtag/`](/bin/mtag/) |\n| [notes.sh](notes.sh) | notepad, just scraps really |\n| [nuitka.txt](nuitka.txt) | how to build a copyparty exe using nuitka (not maintained) |\n| [pretend-youre-qnap.patch](pretend-youre-qnap.patch) | simulate a NAS which keeps returning old cached data even though you just modified the file yourself |\n| [tcp-debug.sh](tcp-debug.sh) | looks like this was to debug stuck tcp connections? |\n| [unirange.py](unirange.py) | uhh |\n| [up2k.txt](up2k.txt) | initial ideas for how up2k should work, another unmaintained sentimental-value-only thing |\n"
  },
  {
    "path": "docs/TODO.md",
    "content": "a living list of upcoming features / fixes / changes, very roughly in order of priority\n\n* download accelerator\n  * definitely download chunks in parallel\n  * maybe resumable downloads (chrome-only, jank api)\n  * maybe checksum validation (return sha512 of requested range in responses, and probably also warks)\n\n* [github issue #37](https://github.com/9001/copyparty/issues/37) - upload PWA\n  * or [maybe not](https://arstechnica.com/tech-policy/2024/02/apple-under-fire-for-disabling-iphone-web-apps-eu-asks-developers-to-weigh-in/), or [maybe](https://arstechnica.com/gadgets/2024/03/apple-changes-course-will-keep-iphone-eu-web-apps-how-they-are-in-ios-17-4/)\n\n* [github issue #57](https://github.com/9001/copyparty/issues/57) - config GUI\n  * configs given to -c can be ordered with numerical prefix\n  * autorevert settings if it fails to apply\n  * countdown until session invalidates in settings gui, with refresh-button\n\n"
  },
  {
    "path": "docs/bad-codecs.md",
    "content": "due to legal reasons, the copyparty [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 decode and display images and videos which were made using certain codecs\n\n* specifically, photos and videos taken with iphones will not work, and perhaps some samsung phones, idk\n* this also includes thumbnails thereof\n\nI suggest you stop reading at this point, unless you want to share my frustration, in which case please do continue\n\n\n## why hevc is not included\n\nthe following is my understanding, which is probably wrong because [I anal](https://en.wikipedia.org/wiki/IANAL)\n\n* the h265 codec, also known as h.265 and hevc, is patent-encumbered and legally problematic to distribute;\n  * there are several patent-pools of patent-holders with conflicting and unclear requirements\n  * even FOSS is not exempt from demands of payment; https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding#Provision_for_costless_software\n  * I have no idea how \"number of sales\" maps to FOSS, but copyparty doesn't have telemetry so it would be impossible to satisfy the requirements either way\n* due to this, both chrome and firefox refuse to add a built-in decoder for hevc; https://caniuse.com/hevc\n  * and while I haven't discussed this with a lawyer, I presume the reason is they did\n* most heif/heic images are hevc, meaning they are equally troublesome\n* safari is the only webbrowser willing to decode and display heif/heic photos, for self-inflicted reasons https://caniuse.com/heif\n\nand anyways there's no reason to use hevc in the first place because [av1](https://en.wikipedia.org/wiki/AV1) gives higher quality at a smaller filesize, is entirely free, and avif (its heif counterpart) is widely supported across all browsers: https://caniuse.com/avif\n\n\n## why only the docker and flashdrive images are affected\n\nsupposedly, royalties is like a jigsaw puzzle, where whoever lays down the last piece wins the responsibility of dealing with that mess -- and because the docker-image has everything bundled as one big ball of software, that might(?) be a problem...idk, i anal\n\nso because ffmpeg is the component that handles everything regarding hevc, only the packages which include ffmpeg are affected, which means the docker-image and bootable-flashdrive-image\n\nif you use or install copyparty in any other way, then you are in charge of obtaining and providing an ffmpeg for copyparty to use, and thus nothing has changed\n\n\n## how hevc support was removed\n\nthe regular ffmpeg package from the alpinelinux repos was replaced with a [custom ffmpeg build](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker/base) where the hevc decoder was physically stripped out, meaning hevc is not just \"disabled\", but entirely removed from all official copyparty distributions\n\noh and the aac support has also been tampered with; now only AAC-LC can be decoded, which is fine because that's like 99% of all aac files (nobody uses HE-AAC or AAC-LD), and LC-AAC has become royalty-free in all relevant parts of the world at this point\n\nand any traces of vvc was also stripped out because that codec was dead on arrival, unable to compete with av1 (and soon av2)\n\nthe silver lining is that this has made the docker images *much* smaller; the `ac` image is now half the size -- it went from 67 to 35 MiB gzipped, from 195 to 99 MiB installed, which is nice\n\n\n## how to enable hevc support\n\nall I can say is good luck; I legally cannot help you with that\n\nsee, here's the fun part -- apparently I'm not allowed to assist with a technical explanation on how it could be done, because that would \"facilitate access\" as they call it?? but all copyparty does is call `ffmpeg` to generate the thumbnail; copyparty doesn't even know or care what \"hevc\" is; this is all purely on the ffmpeg side of things -- so technically none of this is even related to copyparty itself in the first place... ah whatever\n\nman I just wanna write software, I hate this\n\npain peko\n"
  },
  {
    "path": "docs/biquad.html",
    "content": "<!DOCTYPE html><html><head></head><body><script>\n\nsetTimeout(location.reload.bind(location), 700);\ndocument.documentElement.scrollLeft = 0;\n\nvar cali = (function() {\n    var ac = new AudioContext(),\n        fi = ac.createBiquadFilter(),\n        freqs = new Float32Array(1),\n        mag = new Float32Array(1),\n        phase = new Float32Array(1);\n\n    freqs[0] = 14000;\n    fi.type = 'peaking';\n    fi.frequency.value = 18000;\n    fi.Q.value = 0.8;\n    fi.gain.value = 1;\n    fi.getFrequencyResponse(freqs, mag, phase);\n\n    return mag[0];  // 1.0407 good, 1.0563 bad\n})(),\n    mp = cali < 1.05;\n\nvar can = document.createElement('canvas'),\n    cc = can.getContext('2d'),\n    w = 2048,\n    h = 1024;\n\nw = 2048;\n\ncan.width = w;\ncan.height = h;\ndocument.body.appendChild(can);\ncan.style.cssText = 'width:' + w + 'px;height:' + h + 'px';\n\ncc.fillStyle = '#000';\ncc.fillRect(0, 0, w, h);\n\nvar cfg = [ // hz, q, g\n    [31.25 * 0.88, 0, 1.4],  // shelf\n    [31.25 * 1.04, 0.7, 0.96],  // peak\n    [62.5, 0.7, 1],\n    [125, 0.8, 1],\n    [250, 0.9, 1.03],\n    [500, 0.9, 1.1],\n    [1000, 0.9, 1.1],\n    [2000, 0.9, 1.105],\n    [4000, 0.88, 1.05],\n    [8000 * 1.006, 0.73, mp ? 1.24 : 1.2],\n    //[16000 * 1.00, 0.5, 1.75],  // peak.v1\n    //[16000 * 1.19, 0, 1.8]  // shelf.v1\n    [16000 * 0.89, 0.7, mp ? 1.26 : 1.2],  // peak\n    [16000 * 1.13, 0.82, mp ? 1.09 : 0.75],  // peak\n    [16000 * 1.205, 0, mp ? 1.9 : 1.85]  // shelf\n];\n\nvar freqs = new Float32Array(22000),\n    sum = new Float32Array(freqs.length),\n    ac = new AudioContext(),\n    step = w / freqs.length,\n    colors = [\n        'rgba(255, 0, 0, 0.7)',\n        'rgba(0, 224, 0, 0.7)',\n        'rgba(0, 64, 255, 0.7)'\n    ];\n\nvar order = [];\n\nfor (var a = 0; a < cfg.length; a += 2)\n    order.push(a);\n\nfor (var a = 1; a < cfg.length; a += 2)\n    order.push(a);\n\nfor (var ia = 0; ia < order.length; ia++) {\n    var a = order[ia],\n        fi = ac.createBiquadFilter(),\n        mag = new Float32Array(freqs.length),\n        phase = new Float32Array(freqs.length);\n\n    for (var b = 0; b < freqs.length; b++)\n        freqs[b] = b;\n\n    fi.type = a == 0 ? 'lowshelf' : a == cfg.length - 1 ? 'highshelf' : 'peaking';\n    fi.frequency.value = cfg[a][0];\n    fi.Q.value = cfg[a][1];\n    fi.gain.value = 1;\n\n    fi.getFrequencyResponse(freqs, mag, phase);\n    cc.fillStyle = colors[a % colors.length];\n    for (var b = 0; b < sum.length; b++) {\n        mag[b] -= 1;\n        sum[b] += mag[b] * cfg[a][2];\n        var y = h - (mag[b] * h * 3);\n        cc.fillRect(b * step, y, step, h - y);\n        cc.fillRect(b * step - 1, y - 1, 3, 3);\n    }\n}\n\nvar min = 999999, max = 0;\nfor (var a = 0; a < sum.length; a++) {\n    min = Math.min(min, sum[a]);\n    max = Math.max(max, sum[a]);\n}\ncc.fillStyle = 'rgba(255,255,255,1)';\nfor (var a = 0; a < sum.length; a++) {\n    var v = (sum[a] - min) / (max - min);\n    cc.fillRect(a * step, 0, step, v * h / 2);\n}\n\ncc.fillRect(0, 460, w, 1);\n\n</script></body></html>"
  },
  {
    "path": "docs/bufsize.txt",
    "content": "notes from testing various buffer sizes of files and sockets\n\nsummary:\n\ndownload-folder-as-tar:   would be 7% faster with --iobuf 65536 (but got 20% faster in v1.11.2)\n\ndownload-folder-as-zip:   optimal with default --iobuf 262144\n\ndownload-file-over-https: optimal with default --iobuf 262144\n\nput-large-file:           optimal with default --iobuf 262144, --s-rd-sz 262144 (and got 14% faster in v1.11.2)\n\npost-large-file:          optimal with default --iobuf 262144, --s-rd-sz 262144 (and got 18% faster in v1.11.2)\n\n----\n\noha -z10s -c1 --ipv4 --insecure http://127.0.0.1:3923/bigs/?tar\n 3.3            req/s 1.11.1\n 4.3  4.0  3.3  req/s 1.12.2\n  64  256  512  --iobuf        256 (prefer smaller)\n  32   32   32  --s-rd-sz\n\noha -z10s -c1 --ipv4 --insecure http://127.0.0.1:3923/bigs/?zip\n 2.9            req/s 1.11.1\n 2.5  2.9  2.9  req/s 1.12.2\n  64  256  512  --iobuf        256 (prefer bigger)\n  32   32   32  --s-rd-sz\n\noha -z10s -c1 --ipv4 --insecure http://127.0.0.1:3923/pairdupes/?tar\n 8.3            req/s 1.11.1\n 8.4  8.4  8.5  req/s 1.12.2\n  64  256  512  --iobuf        256 (prefer bigger)\n  32   32   32  --s-rd-sz\n\noha -z10s -c1 --ipv4 --insecure http://127.0.0.1:3923/pairdupes/?zip\n 13.9              req/s 1.11.1\n 14.1  14.0  13.8  req/s 1.12.2\n   64   256   512  --iobuf     256 (prefer smaller)\n   32    32    32  --s-rd-sz\n\noha -z10s -c1 --ipv4 --insecure http://127.0.0.1:3923/pairdupes/987a\n5260                    req/s 1.11.1\n5246  5246  5280  5268  req/s 1.12.2\n  64   256   512   256  --iobuf      dontcare\n  32    32    32   512  --s-rd-sz    dontcare\n\noha -z10s -c1 --ipv4 --insecure https://127.0.0.1:3923/pairdupes/987a\n4445              req/s 1.11.1\n4462  4494  4444  req/s 1.12.2\n  64   256   512  --iobuf      dontcare\n  32    32    32  --s-rd-sz\n\noha -z10s -c1 --ipv4 --insecure http://127.0.0.1:3923/bigs/gssc-02-cannonball-skydrift/track10.cdda.flac\n 95       req/s 1.11.1\n 95   97  req/s 1.12.2\n 64  512  --iobuf              dontcare\n 32   32  --s-rd-sz\n\noha -z10s -c1 --ipv4 --insecure https://127.0.0.1:3923/bigs/gssc-02-cannonball-skydrift/track10.cdda.flac\n 15.4                    req/s 1.11.1\n 15.4  15.3  14.9  15.4  req/s 1.12.2\n   64   256   512   512  --iobuf        256 (prefer smaller, and smaller than s-wr-sz)\n   32    32    32    32  --s-rd-sz\n  256   256   256   512  --s-wr-sz\n\n----\n\npython3 ~/dev/old/copyparty\\ v1.11.1\\ dont\\ ban\\ the\\ pipes.py -q -i 127.0.0.1 -v .::A --daw \npython3 ~/dev/copyparty/dist/copyparty-sfx.py -q -i 127.0.0.1 -v .::A --daw --iobuf $((1024*512)) \n\noha -z10s -c1 --ipv4 --insecure -mPUT -r0 -D ~/Music/gssc-02-cannonball-skydrift/track10.cdda.flac http://127.0.0.1:3923/a.bin \n10.8                                req/s 1.11.1\n10.8  11.5  11.8  12.1  12.2  12.3  req/s new\n 512   512   512   512   512   256  --iobuf         256\n  32    64   128   256   512   256  --s-rd-sz       256 (prefer bigger)\n\n----\n\nbuildpost() {\nb=--jeg-er-grensestaven;\nprintf -- \"$b\\r\\nContent-Disposition: form-data; name=\\\"act\\\"\\r\\n\\r\\nbput\\r\\n$b\\r\\nContent-Disposition: form-data; name=\\\"f\\\"; filename=\\\"a.bin\\\"\\r\\nContent-Type: audio/mpeg\\r\\n\\r\\n\"\ncat \"$1\"\nprintf -- \"\\r\\n${b}--\\r\\n\"\n}\nbuildpost ~/Music/gssc-02-cannonball-skydrift/track10.cdda.flac >big.post\nbuildpost ~/Music/bottomtext.txt >smol.post\n\noha -z10s -c1 --ipv4 --insecure -mPOST -r0 -T 'multipart/form-data; boundary=jeg-er-grensestaven' -D big.post http://127.0.0.1:3923/?replace\n9.6  11.2  11.3  11.1  10.9  req/s v1.11.2\n512   512   256   128   256  --iobuf         256\n 32   512   256   128   128  --s-rd-sz       256\n\noha -z10s -c1 --ipv4 --insecure -mPOST -r0 -T 'multipart/form-data; boundary=jeg-er-grensestaven' -D smol.post http://127.0.0.1:3923/?replace\n2445  2414  2401  2437  \n 256   128   256   256  --iobuf           256\n 128   128   256    64  --s-rd-sz         128 (but use 256 since big posts are more important)\n"
  },
  {
    "path": "docs/changelog.md",
    "content": "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2026-0308-2106  `v1.20.11`  what? nohtml is evolving!\n\n## ⚠️ ATTN: this release fixes a vulnerability\n\n[GHSA-m6hv-x64c-27mm](https://github.com/9001/copyparty/security/advisories/GHSA-m6hv-x64c-27mm) the `nohtml` volflag did not prevent javascript inside SVG images from executing -- a malicious user with write-access could upload an SVG file which would execute as javascript when someone opens it 1c9f894e\n\n## 🧪 new features\n\n* version-checker (thx @icxes!) c6965f06\n  * default-disabled; you must [choose a URL](https://github.com/9001/copyparty/#version-checker) to grab security advisories from to enable it\n  * periodically checks the security advisories and shows a warning in the controlpanel if you're running a vulnerable version\n  * can optionally panic and shutdown the server if you prefer that\n  * man, the timing on this though... absolute cinema\n\n## 🩹 bugfixes\n\n* fix `nohtml` not being aware that SVG images can execute javascript 1c9f894e\n  * a new volflag [noscript](https://github.com/9001/copyparty/#security) was also added; `nohtml` will automatically enable `noscript`, but `noscript` can also be useful on its own; see readme\n* various [upload rules](https://github.com/9001/copyparty/#upload-rules) fixes:\n  * #1335 `rotf` couldn't handle trailing slash (thx @NecRaul!) 8e20506d\n  * #1337 `rotn` didn't always count correctly (thx @NecRaul!) 23d4a62e\n  * `rotn` didn't apply to dupes 00e821db\n* combining [rp-loc](https://copyparty.eu/cli/#g-rp-loc) and [site](https://copyparty.eu/cli/#g-site) was a bit jank (thx @new-sashok724!) 31b23843\n* global-option [idp-store: 2](https://copyparty.eu/cli/#g-idp-store) would result in excessive config reloading 1272de9d\n* fix fd-leak when indexing certain compressed files, including epub books 8b5ac23e\n* [forget-ip](https://copyparty.eu/cli/#g-forget-ip): fix sqlite cursor-locking 37123e33\n\n## 🔧 other changes\n\n* #1316 Chinese translation got a huge makeover (thx @satgo1546 and @lxdlam!) b0152741\n* #1324 better rclone advice on the connect-page 8941701a\n* static website resources, previously served from `/.cpr/` have moved to `/.cpr/w/` for easier configuration of allowlists in reverseproxies and authentication middlewares 753ff548 \n\n## 🌠 fun facts\n\n* according to [the SVG spec](https://www.w3.org/TR/SVG11/script.html), images being able to execute javascript is a feature and intentional behavior... what a concept!\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2026-0225-1533  `v1.20.10`  fix login (ﾉ ﾟヮﾟ)ﾉ ~┻━┻\n\n## recent important news\n\n* [v1.20.9 (2025-02-25)](https://github.com/9001/copyparty/releases/tag/v1.20.9) fixed [CVE-2026-27948](https://github.com/9001/copyparty/security/advisories/GHSA-62cr-6wp5-q43h) (XSS)\n\n## 🩹 bugfixes\n\n* #1311 fix login (broke in v1.20.9) ecdfd2d1\n\n## 🔧 other changes\n\n* warn that config-reload doesn't do global-options a29037a0\n\n## 🌠 fun facts\n\n* rushing out a cve-fix in the wee hours of the morning before the 9-5 is a great idea that never goes wrong\n  * 10/10 will probably do again\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2026-0225-0834  `v1.20.9`  SECURITY: XSS fix\n\n## ⚠️ ATTN: this release fixes an XSS vulnerability\n\n[GHSA-62cr-6wp5-q43h](https://github.com/9001/copyparty/security/advisories/GHSA-62cr-6wp5-q43h) could let an attacker execute arbitrary JS by tricking you into clicking a malicious link 31b2801f\n\n## 🔧 other changes\n\n* webdav: [dav-port](https://copyparty.eu/cli/#g-dav-port) can be used as an alternative to [daw](https://copyparty.eu/cli/#g-daw) d21242fc\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2026-0222-1507  `v1.20.8`  no265\n\n## 🧪 new features\n\n* #1298 add Hungarian translation (thx @sonacl!) eefb181b f37c3b96\n* #1299 chown now accepts 4-digit values (thx @new-sashok724!) 5a7504fd\n\n## 🩹 bugfixes\n\n* audioplayer skip-silence:\n  * #1303 clamp ffwd to safe values (thx @icxes!) f5e70c7f\n  * fix crash on folderchange f1a433a6\n\n## 🔧 other changes\n\n* due to [legal reasons](https://github.com/9001/copyparty/blob/hovudstraum/docs/bad-codecs.md), the [docker-images](https://github.com/9001/copyparty/blob/hovudstraum/scripts/docker) and [bootable flashdrive](https://a.ocv.me/pub/stuff/edcd001/enterprise-edition/) are now unable to create thumbnails of HEVC/h265 videos and heif/heic images 1bec91d1\n  * this primarily means photos/videos taken with iphones (and maybe some samsung phones)\n  * on the bright side, this has made the docker-images much smaller; `ac` is now half the size it used to be, and `iv` / `dj` are each 97 MiB smaller\n\n## 🌠 fun facts\n\n* if you wanna see your car doing its best impression of a frictionless spherical cow, I can warmly (heh) recommend the icy snowcoated countryroads of viken this weekend\n  * goes oddly well with [sakuraburst - deconstructing nature](https://www.youtube.com/watch?v=MJjO-pwYpJg)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2026-0214-2315  `v1.20.7`  fika\n\n## 🧪 new features\n\n* now possible to upload/delete files while the filesystem-indexer is still busy d44ea245 0ca4c1bd\n  * global-option [fika](https://copyparty.eu/cli/#g-fika) decides which actions to allow while still indexing; default is upload+copy+delete\n  * full deduplication is only guaranteed if this option is set blank, as dupes are allowed while indexing\n* #1266 browsers can request thumbnails as jxl images, and view jxl files in the gallery (thx @intelfx!) b2711e05 720c83b2 93ffc65c a65a30b1 a7a25deb 59de5e2c 16403d8c 48c10178 0e8913c2\n  * only works in browsers which support jxl, which is FINALLY happening ([sure took a while](https://issues.chromium.org/issues/40168998))\n  * some notes on memory/RAM usage though -- it is fine on Alpine Linux, so docker is also fine, just don't enable mimalloc\n    * jxl can be disabled with global-option [th-no-jxl](https://copyparty.eu/cli/#g-th-no-jxl) if necessary on baremetal deployments until libvips fixes this\n* #1265 audioplayer can \"skip silence\" now (thx @icxes!) 66949989\n* #1287 opensearch support for opds (thx @philips!) 84e687a0\n* #1276 option [rw-edit](https://copyparty.eu/cli/#g-rw-edit) is the list of file-extensions that can be edited as textfiles with only permissions read+write (default is `md` like before); all other files still require read+write+delete 312f48e1 d6928380\n* #1288 option to customize the links copied when selecting files and pressing ctrl-c (thx @icxes!) e5d0a057\n* docker: add env-var [DI_PREPARTY](https://github.com/9001/copyparty/blob/hovudstraum/scripts/docker/devnotes.md#modding-on-the-fly) to run an arbitrary script during startup, for customizations and such bf01ca48\n\n## 🩹 bugfixes\n\n* #1279 the textfile-viewer would refuse to load huge documents when hotlinked f02e9cf6\n* #1280 the custom rightclick-menu was enabled in the textfile viewer fc8a4b8e\n* #1262 logtail now works on windows; would previously take an exclusive-lock on the monitored file, as windows does by default a368fc66\n\n## 🔧 other changes\n\n* volumes are hidden from the treeview if the name starts with a dot 76041fdb\n* #1277 `descript.ion` files no longer require the `e2d` and `e2t` options to be enabled 4cb4e820\n* chunked PUT-uploads are now terminated if they exceed a configured size limit dfadb5a7\n* #1282 improved compatibility with GraalPy (thx @vgskye!) e8609b87\n* #1292 #1296 updated Esperanto translation (thx @slashdevslashurandom!) 418bf2f9 914f84ce\n* thumbnails: use libvips as fallback for rawpy 27ae2e1e\n  * libvips doesn't support .arw files (sony) yet, so still need rawpy\n* make server config slightly easier:\n  * improve xff warnings 96aeb898\n  * warn if config-values are quoted 598df44e\n  * lowercase headernames in configs fd096385\n\n## 🌠 fun facts\n\n* the `fika` option sends the filesystem-indexer on [a coffee break](https://en.wikipedia.org/wiki/Coffee_in_Sweden#Fika)\n* exci wants me to mention aoi yuuki here for some reason :^) so here's [gekisou gungnir](https://www.youtube.com/watch?v=feeFscLH6QE)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2026-0131-2001  `v1.20.6`  one safeguard too many\n\n## 🧪 new features\n\n* #1264 now possible to grant the `get` permission when creating a share 95b827f1\n  * the button was already there, but until now it did nothing\n\n## 🩹 bugfixes\n\n* a safeguard (24141b49) added in [v1.20.5](https://github.com/9001/copyparty/releases/tag/v1.20.5) was too strict and would block requests from certain reverseproxies, specifically anything that adds `X-Forwarded-HTTP-Version` 72224d29\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2026-0130-2145  `v1.20.5`  fast again\n\n<img src=\"https://github.com/9001/copyparty/raw/hovudstraum/docs/logo.svg\" width=\"250\" align=\"right\"/>\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) ╱ [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) ╱ [client testbed](https://cd.ocv.me/b/)\n\nthere is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone` in case of future important updates, such as [vulnerabilities](https://github.com/9001/copyparty/security) (most recently 2025-09-07)\n\n## 🧪 new features\n\n* #1240 webdav clients can now set fractional last-modified timestamps (thx @jcwillox!) 296362fc\n* #1260 add support for running the server with GraalPy (thx @vgskye!) 73d06eaf\n* #1182 pressing CTRL-C will copy links of selected files to clipboard 9c14972d\n\n## 🩹 bugfixes\n\n* #1248 shares: fix the buttons for extending expiration time b6bf6d5f\n* #1242 webdav: fix «MacOS Finder» taking forever to connect (thx @freddyheppell!) 8e046fb6\n* ie11 would spinlock in write-only folders 5c4ba376\n\n## 🔧 other changes\n\n* fast again! ed6a8d5a\n  * replaced the `connection:close` band-aid added in [v1.20.4](https://github.com/9001/copyparty/releases/tag/v1.20.4) with a proper fix that doesn't make things slower behind reverseproxies\n  * I've tried everything I can think of (with nginx as reverseproxy) and can't notice any difference in behavior, but please let me know if this breaks anything for you 🙏\n* #1245 updated Portuguese translation (thx @000yesnt!) 69fa1d10\n* #1259 OpenRC: add command to test config (thx @lotsospaghetti!) 79273a7d\n* #1257 removed the `nth` global-option because it was never implemented (thx @stackxp!) 22cdc0f8\n* syntax highlighter: added languages `nasm` + `nix`, removed `autohotkey` + `cmake` b20d3259\n\n## 🌠 fun facts\n\n* http/1.1 still tends to be faster than http/2 and http/3 for large transfers which is the main reason copyparty hasn't made the change\n  * eh, not really a *fun* fact I suppose ┐( ´ w `)┌\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2026-0123-0055  `v1.20.4`  a fresh pair of sock(et)s\n\n## 🩹 bugfixes\n\n* #1235 rightclick-menu: fix creating new files/folders in gridview (thx @SpaceXCheeseWheel!) ffca67f2\n* #1231 fix http desync if the [`urlform`](https://copyparty.eu/cli/#urlform-help-page) global-option was changed to `get`\n  * this initial fix only applies when reverse-proxied, in which case copyparty will now always `connection:close` (don't reuse tcp/uds connections), as giving each client a fresh socket helps avoid all such issues e1eff216 b4fddbc3\n  * the expected performance impact from this change is near-zero for real use, even if benchmarks show a 40% reduction in requests/sec in the absolute-worst-case (burst of cheap requests)\n  * a future version will also fix this issue for non-proxied clients\n\n## 🔧 other changes\n\n* #1229 updated the Esperanto translation (thx @slashdevslashurandom!) 1142ac25\n* #1232 shares: if an external domain is configured, then show both the LAN and external link for each share 81e5eb7b\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2026-0121-0505  `v1.20.3`  dillo approves\n\n## 🧪 new features\n\n* send-message-to-serverlog now also available as url-parameter `?smsg=foo` 6dcb1efb\n  * option `smsg` configures which HTTP-methods to allow; can be set to `GET,POST` but default is only `POST` because `GET` is dangerous (CSRF)\n\n## 🩹 bugfixes\n\n* #1227 [dillo](https://dillo-browser.github.io/) was not able to login because dillo is more standards-compliant than every other browser (nice) b4df8fa2\n* a web-scraper which got banned for making malicious requests could remain banned for one request longer than intended (wait why did I fix this) ba67b279\n* `?ls` was still a bit jank 0a3a8072\n\n## 🌠 fun facts\n\n* this 6AM release was [powered by void/mournfinale](https://www.youtube.com/watch?v=lFEEXloqk9Q&list=PLlEk36g9RI8Ppjr3HhaO3wjjmA6HnSo2U)\n* was going to name the release \"dilla på dillo\" but somehow google-translate thinks that means \"fuck on fuck\" which would have been inappropriate\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2026-0119-0126  `v1.20.2`  xattrs + range-select\n\n## 🧪 new features\n\n* #1212, #1214 range-select in the grid-view by click-and-drag (thx @icxes!) 3e3228e0 72c59405\n* #134 xattrs (linux extended file attributes) can now be indexed and searchable 8240ef61\n* rightclick-menu:\n  * #1184 add rename option (thx @stackxp!) 25a8b96f\n  * #1216 add sharing options (thx @stackxp!) ffb25603\n  * #1198, #1206 also works in the search-results view (thx @hackysphere!) 04f612ff d32704ed\n* option to override the domain in certain links, so copyparty returns an external URL even if you're accessing it by a LAN address:\n  * #1211 newly created shares 41d3bae9\n  * #255 newly uploaded files d9255538\n* new option `vol-nospawn` (volflag `nospawn`) to *not* automatically create the volume's folder on the server's HDD if it doesn't exist\n* new option `vol-or-crash` (volflag `assert_root`) to intentionally crash on startup if a volume's folder doesn't already exist on the server HDD\n* new option `--flo` to tweak the log-format used by the `-lo` option for logging to a file 826e84c8\n* #1197 u2c ([commandline uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy)): give up and crash if server is offline for longer than 3 minutes (configurable) 67c5d8da\n\n## 🩹 bugfixes\n\n* #1203 configured chmod/chown rules were not applied when a file was being deduped bef07720\n* the `unlistc*` volflags could not be specified for single-file volumes 26648911\n* the defensive renaming of uploaded readmes/logues would assume the default filenames, not considering the recently added option to customize these names c17c3be0\n* #1191 the `ipu` option can once again be used to reject connections from certain IP-ranges caf831fc\n  * this was a regression in v1.19.21 causing the server to crash on startup if such a config was attempted\n* some empty folders could be created during startup in certain server-configs with nested volumes 4e67b467\n* api: trying to `?ls` nested virtual folders could return an error 66750391\n* ui/ux:\n  * #1179 improve errormessage if audio transcoding fails 7357d46f\n  * ensure a trailing slash when viewing a folder with the `h` permission; good for relative links in html-files\n\n## 🔧 other changes\n\n* #1193, #1194: NixOS improvements (thx @toast003!) 9d223d6c d5a8a34b\n* truncate huge errormessages from ffmpeg so the log doesn't get flooded 3aebfabd\n* ui/ux:\n  * the `dl` button (to download selected files individually) now skips folders, since that never worked bc24604a\n  * #1200 add html classes to make custom styling easier c46cd7f5\n  * rephrase errormessages from `see serverlog` to `see fileserver log`\n* docs:\n  * mention in the readme that uploading files from a deeply nested folder using a webbrowser on Windows can fail because browsers don't handle the max-pathlen limitation of Windows optimally (not a copyparty-specific issue, but still hits us)\n\n## 🌠 fun facts\n\n* n/a; no fun has been had since [v1.20.0](https://github.com/9001/copyparty/releases/tag/v1.20.0)\n  * (that's a lie btw, [sniffing the airwaves](https://a.ocv.me/pub/g/2026/01/PXL_20260117_192619830.jpg?cache) *is* pretty darn fun 😁)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2026-0109-0052  `v1.20.1`  sftp fixes\n\n## 🧪 new features\n\n* #1174 add Japanese translation (thx @tkymmm!) b918b592\n* #1164 rightclick-menu now works in the gridview too (thx @Foox-dev!) feabbf3e\n* #1176 IP to bind can be specified per protocol 87a5c22a\n\n## 🩹 bugfixes\n\n* various SFTP fixes (i blame the single [tschunk](https://germanfoods.org/tschunk/) on day 3):\n  * #1170 be more lenient regarding `stat` permissions 90308284\n  * #1170 deletes could return EPERM when ENOENT was more appropriate 8c9e1016\n  * #1170 files would be created with an extremely restrictive chmod 2f4a30b6\n    * certified octal moment\n  * write-only folders could return ENOENT 6c41bac6\n* #1177 disk-usage quotas became incompatible with shares in v1.20.0 038af507\n* appending to existing files with `?apnd` was possible in volumes with non-reflink dedup, where it could propagate to deduped copies of the file 738a419b\n  * (was only possible for users with write+delete perms, so at least it couldn't be used for nefarious purposes)\n* rightclick-menu: \"copy link\" would strip filekeys 3a16d346\n\n## 🔧 other changes\n\n* copyparty.exe: updated pillow to 12.1.0 a9ae6d51\n\n## 🌠 fun facts\n\n* [tschunk](https://germanfoods.org/tschunk/) is the sound a hacker makes as they faceplant onto the table after having one too many\n  * also see the funfacts in [the previous release](https://github.com/9001/copyparty/releases/tag/v1.20.0) for more CCC hijinks :p\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2026-0102-0007  `v1.20.0`  sftp is fine too\n\n## 🧪 new features\n\n* sftp server 4714c2fa ec7ea309\n  * included in [docker-images](https://hub.docker.com/u/copyparty) `im`, `iv`, `ac`, `dj`\n  * not using docker? install the optional dependency [paramiko](https://pypi.org/project/paramiko/)\n* #1135 right-click menu (thx @stackxp and @Scotsguy!) 82c49609 05a44720\n* PUT can now append to existing files 63d8e5a0\n  * new option [apnd-who](https://copyparty.eu/cli/#g-apnd-who) to configure who is allowed to do that\n* #1128 added option to skip uploading a file if the filename is already taken on the server (thx @Scotsguy!) fa32e159\n* #1127 descript.ion now also works for folders 2c26aecd\n* in file listings, `up_by` and `up_ip` (uploader info) can now be displayed for non-admin users by adding them to the [mte](https://copyparty.eu/cli/#g-mte) option 7bfd370b\n* #1120 display the disk-space quota instead of the underlying HDD size (thx @rabid-dev!) 511dc016 e0845b23\n* option to skip dotfiles/dotfolders when using download-as-zip 7d7a1510\n* #1124 button to skip files with a filename collision when copying/moving files 85639ad2\n* #1151 u2c ([commandline uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy)): option to use basic-auth instead of the `PW` header (thx @Le0Developer!) 120fdfb2\n* the name of the `pw` url-param and http-header can be changed f81d80bc\n  * mainly to force basic-auth, but perhaps also for other purposes\n  * changing these will break support for many clients, so you probably want to keep the default\n\n## 🩹 bugfixes\n\n* image-viewer:\n  * images now scale properly when rotated while the zoom feature is enabled c0e167fd\n  * the current image rotation will now be applied to the next image as well 485c60cf c82a3cb2\n* groups announced by an IdP will now also apply for native (copyparty-config) users f08cb25c\n* windows: download-as-zip would flatten everything to a single folder 2d1d295a\n* #1157 [dirkeys](https://github.com/9001/copyparty/#dirkeys) did not work in grid-view d1ddcb19\n* #1123 the [ui-notree](https://copyparty.eu/cli/#g-ui-notree) option to simplify the UI would simplify a bit too much 4c73704c\n* don't add the trailing slash to a volume in the controlpanel when the volume is a file 80a37492\n  * the link couldn't be clicked on Windows CE 4.20 using Internet Explorer 4.01\n\n## 🔧 other changes\n\n* #1158 updated german translation (thx @Scotsguy!) 3bf80c81\n* the `dotfiles` button now also toggles showing [unlisted](https://copyparty.eu/cli/#g-unlist) files e55e5a45\n* `/?h&ls` (the api to list volumes) now includes the user's permissions for each volume 1f6e8116\n* #1142 new option [dav-port](https://copyparty.eu/cli/#g-dav-port) to open a dedicated port for webdav clients 4642d323\n  * workaround for certain clients which pretend to be webbrowsers\n* #1147 workaround for a buggy browser-extension 8551472b\n* detect (and panic) when a webbrowser has failed to load one of the javascript files af3f777e\n  * replaces the confusing errormessage resulting from half of the code missing\n\n## 🌠 fun facts\n\n* it [turns out](https://github.com/9001/copyparty/#server-hall-of-fame) that copyparty runs just fine on [SGI IRIX](https://en.wikipedia.org/wiki/IRIX)! 39c3ccc2\n  * there's a [photo of the server](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/sgi-o2.jpg?cache) and a [screenshot](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/sgi-o2.png?cache) as proof 😁\n  * the [feature comparison](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md#general) has been updated accordingly\n* this release was mostly coded at 39c3 (see photo above) and the release was [made at revspace](https://a.ocv.me/pub/g/2026/01/PXL_20260102_235328552.jpg)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-1217-0014  `v1.19.23`  bad apple x2\n\n## 🧪 new features\n\n* #1080 new translation: Vietnamese (thx @thatfrozenfrog and @khoidauminh!) b60eb3f0 d4a9787c\n* #1110 add option `xf-proto-fb` to support reverseproxies which do not provide an `x-forwarded-proto` header 9c64788d\n  * and improve the rproxy config guidance message in the serverlog c8f3b4ef\n  * and show a warning in the controlpanel if misconfiguration is detected c8f3b4ef\n* #1109 add option `--ipar`, reverseproxy-aware alternative to `--ipa` 33684219\n  * its purpose is rejecting connections from unexpected/unwanted IPs/subnets\n* option `idp-chsub` can be used to replace spaces in IdP usernames/groupnames 5e1d9a58 \n* #1029 indicate password max-length in ui 8d46cf18\n  * thx to @grantbacon for the initial take!\n\n## 🩹 bugfixes\n\n* #1111 apple gave us coal for xmas this year 0b6d2d24\n  * workaround for a new bug in safari (iOS and Macos) where it would randomly show a login-popup\n* #1113 the `@acct` group was unavailable in groupless IdP setups b6c2ec15\n\n## 🌠 fun facts\n\n* speaking of current events, @stackxp made copyparty [bad-apple-certified](https://copyparty.eu/bad-apple.mp4) a little while back :grin:\n  * the plugin is [available here](https://github.com/stackxp/copyparty-badapple)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-1214-2304  `v1.19.22`  merikuri\n\n## 🧪 new features\n\n* #1068 #1089 add [options](https://copyparty.eu/cli#g-prologues) to customize which textfiles (readme/prologue/epilogue) to embed above/below directory listings 14bef85b\n  * `prologues`, `epilogues`, `readmes`, `preadmes` (global-options and/or volflags) accept a comma-separated list of filenames to look for\n* #1092 add option [th-qv](https://copyparty.eu/cli#g-th-qv) to change the thumbnail quality a1cbac02\n  * also found and enabled a size-optimization for libvips, so:\n* #1092 automatically delete and rebuild thumbnails if thumbnailer-config is changed ca6c4dea\n* #1049 add option [log-date](https://copyparty.eu/cli#g-log-date) to display dates in logs 965a4a69\n* #1047 rss-feed: title/description of each entry is now a [template-string](https://copyparty.eu/cli#g-rss-fmt-t) which can reference arbitrary metadata properties (thx @djjeane!) 5e85e3d6\n* extend the ramdisk safeguard to also prevent moving files into ephemeral storage fa918228\n  * would previously prevent creating new files, but this was another potential source for confusion (thx coworker!)\n* now possible to [customize](https://copyparty.eu/cli#g-banmsg) the `thank you for playing` ban-message ce2eeba2\n* #964 option to change the default value of the [`Cache-Control`](https://github.com/9001/copyparty/#other-flags) response-header 3bc0bf19\n  * [don't let this be you](https://a.ocv.me/pub/g/2025/12/f7mc7gbkvkdd1.jpeg) :^)\n\n## 🩹 bugfixes\n\n* #1010 correctly replace illegal characters in filenames according to underlying filesystem ba017f7b\n  * for example, uploading a folder named [COMPLE:X](https://www.youtube.com/watch?v=U9QKCUufpDc&list=OLAK5uy_lMgzSTDg0XcZcqYSVAlfZ4O3rlGckolW4) into an exFAT flashdrive on linux is now possible\n  * and, to make that possible, filesystem-detection now sees the true filesystem behind FUSE (for example ntfs-3g) 3bbed1bc\n* audio-playback would skip into the next folder rather than play the rest of the current one if the folder was sufficiently massive 8e2fb05a\n* #1094 fix `ipu` with idp users 594ec394\n* [commandline uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy): fix termsize detection on windows 7d526eab\n* #1104 the rss feature now complains loudly if e2d is not enabled (because that was always necessary but not obvious) 92195403\n* ui/ux:\n  * #1102 the option to cosmetically hide server info did not apply for all themes e440578c\n  * the metadata-property `date` (default-disabled) was renamed to `tdate` to avoid colliding with the last-modified timestamp if enabled fecc3fd5\n* docs:\n  * #1070 how to use the bundled [archlinux](https://github.com/9001/copyparty/#arch-package) systemd scripts 7f82189d\n  * [podman-systemd](https://github.com/9001/copyparty/tree/hovudstraum/contrib/podman-systemd): fix paths in guide (thx @emiliatheworst!) a8698392\n  * [synology](https://github.com/9001/copyparty/blob/hovudstraum/docs/synology-dsm.md): better way to hide `@eaDir` 1b0eb450\n\n## 🔧 other changes\n\n* add a loud warning in logs if `X-Forwarded-Proto` is not added by the reverseproxy ad45de94 1b222fb5\n  * almost did the same for `X-Forwarded-Host` too before realizing that's generally not a thing\n* #1038 creating a blank `chpw.json` before starting copyparty is now supported and no longer crashes on startup efc6a09d\n* #1105 better feedback in the login ui (thx @stackxp!) 08474dbe\n* [mtag/audio-key.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/audio-key.py): replaced the [melodic key detector](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker#detecting-bpm-and-musical-key) since ffmpeg-8 / alpine-3.23 broke it 67ddc641\n* updated deps:\n  * webdeps: dompurify-3.3.1 e0b04d9c\n  * copyparty.exe: python-3.13.11 9e64fe02\n\n## 🌠 fun facts\n\n* 39c3 has a LOT of awesome [self-organized sessions](https://events.ccc.de/congress/2025/hub/en/event/list/so)\n  * didn't have anything copyparty-related this time but CCC HYPE!\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-1202-2047  `v1.19.21`  tadaimback\n\n## 🧪 new features\n\n* [hooks](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks#readme) now behave more usefully/predictably; 889bd324\n  * hooks returning `0` will run the next hook (if any), and let the initiating action proceed if no other hooks object\n  * hooks returning `100` will stop processing successive hooks, but return success, letting the initiating action proceed\n  * hooks returning anything else will stop processing successive hooks (like the documentation always said) and also fail the initiating action (if hook is checked)\n  * zmq hooks can now respond with json, doing relocations and all that stuff\n* new mtag plugin, [geotag.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/geotag.py): read image geotags with exiftool ([demo](https://a.ocv.me/pub/blog/j8/11/)) 1c15c0d5 ac085b81\n* #972 markdown-links are rewritten to open in the markdown-viewer 278a0d85\n* #794 add json beautifier / minifier\n  * ...in the textfile-editor fd8c5bfc\n  * ...in the textfile-viewer 89cab5b5\n* #1058 ui-option and server-config to force download instead of showing files inline a9174e5d\n* option `stats-u` to grant access to prometheus-metrics based on username, not just permissions b427d780\n\n## 🩹 bugfixes\n\n* #1003 u2c.py (commandline uploader) did not install correctly on archlinux and/or pypi 9385daea\n* #1035 uploader could fail to initialize if: 98701b78\n  * the `mt` button (webworkers) was enabled in the settings tab\n  * **and** the network was severely strained during intial page load \n* possible deadlock on shutdown if thumbnailer queue was hella busy fb9f0441\n* #971 windows: fix deadlock on startup if trying to use a nonexistant driveletter as a volume 945b2276\n* #1022 js-panic if audio playback is set to stay-in-folder a28503e8\n* links to ongoing file transfers in the controlpanel could 404 (thx @Habetdin!) 77f74ddb f4d67ff0\n* video scrubbing on iOS dba7c5d4\n* #1054 audio volume slider could skip one percent (thx @shermanhlc!) ca6d3a5c\n* detect invalid config:\n  * #959 panic if `ipu` user doesn't exist 79e10786\n  * panic if share config overlaps with a volume cedfc444\n* #943\n\n## 🔧 other changes\n\n* the \"new-markdown\" feature was repurposed into \"new-file\", accepting any file extension 7d62335c\n* #1023 the option to grant delete-access when creating a share was removed due to never having been implemented in the backend 04ac7fbd\n* #1012 rephrased the controlpanel login-text when logged in to avoid confusion 7a291403\n* add hints that the serverlog is a good place to look in some situations c424a55d\n* all thumbnail types and combinations can now be pregenerated a359b89e\n* #1030 add debug if cfssl is misbehaving ec00dc18\n* #871 `grid` volflag is applied during navigation if user has not set a preference a9378a8e\n* cosmetic:\n  * show column number in markdown editor b9aacba1\n  * reduced grid margins in theme2 e469bc94\n  * reduced redirect delay after logging in f7e7b03f\n  * controlpanel greeting in some fail-early responses acde21d4\n  * update hooks to ignore the new upload-queue-empty message 3f4b79ff\n* docs:\n  * #1032 fix typo in example docker idp config (thx @tuetenk0pp!) 867237d0\n  * warn that using/changing `-j` is usually a bad idea cad15fbf\n  * add hotlink anchors to https://copyparty.eu/cli/ 7f9c139e \n* nixos:\n  * #868 option to install from git-head (thx @shelvacu!) c7345308\n  * #962 support idp volumes (thx @nicomem!) 904c984b\n  * #963 use configured chmod-d when creating volumes (thx @nicomem!) 3242145e\n* copyparty.exe: update to python 3.13.10, pillow 12.0 cdffde78\n\n## 🌠 fun facts\n\n* copyparty has been observed running [on a wristwatch](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/clockyparty.jpg) and on an [android tv-box](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/aallwinner.jpg) running in big-endian mode, so copyparty is [BE-certified](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/be-ready.png)\n* also... **it's december!** [you know what that means](https://a.ocv.me/pub/demo/music/.bonus/#af-55d4554d) :^)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-1102-0109  `v1.19.20`  november\n\n## 🧪 new features\n\n* #961 the `/?shares` listing now shows the list of filenames for each share 2cc53ea15181f750b4367e6cd20dfebd0bcb3bee\n\n## 🩹 bugfixes\n\n* #967 per-volume md/lg sandbox rules are now applied during navigation db60951d9fa5b17c8190e8b3ab4ceb422a9d2701\n  * if a volume has `no-sb-lg` or `no-sb-md` set then it'll apply when navigating into that volume, and vice-versa\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-1025-1918  `v1.19.19`  copyparty.eu マークII\n\n## 🩹 bugfixes\n\n* fix building the archlinux package e3524d85\n  * otherwise identical to [v1.19.18](https://github.com/9001/copyparty/releases/tag/v1.19.18)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-1025-1434  `v1.19.18`  copyparty.eu\n\n## 🧪 new features\n\n* #949 when all uploads have finished, the client (both the browser and u2c) sends a message to the server saying it's done db87ea5c\n* #941 [copyparty-en.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty-en.pyz), yet another copyparty variant, with enterprise-friendly tweaks:\n  * does not include the smb-server, so antivirus doesn't think it's malware 7f5810f1\n  * english-only, because antivirus apparently hates certain translations too 7f5810f1\n  * renamed the webdav-config `.bat` to `.txt` because clearly only one of those are \"dangerous\" b624a387\n* show volumes with permssion `h` in the navpane fff7291d \n* #937 global-option `--notooltips` to default-disable tooltips a325353b\n\n## 🩹 bugfixes\n\n* #948 fix the u2c `--dr` option when the server is running on windows d3dd3456\n* fix crash on startup when using volflags `unlistc*` and the parent folder is not a volume cdd5e78a\n* `og` / opengraph / discord-embed fixes:\n  * using the `h` permission could result in unexpected 404 c9e45c12\n  * a single-file volume could make filenames in its parent volume unintentionally visible 36ab77e0\n    * this would only happen when combined with `--og`\n* fix some harmless warnings from single-file volumes b1efc006\n* fix filesize-colors in selected rows 1c17b63b\n\n## 🔧 other changes\n\n* releases can now also be downloaded from https://copyparty.eu/ 547a7ab1\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-1017-2313  `v1.19.17`  read:cbz + re:ftp\n\n## 🧪 new features\n\n* #916 view cbz manga/comics in the browser (thx @Scotsguy!) 8ef6dda7\n* #845 users/groups can be subtracted from a broader access grant b4fda5f1\n  * for example `*,-@acct` hides a volume from everyone who's logged in\n* [reflink dedup](https://github.com/9001/copyparty/#file-deduplication) is now available in most python versions, not just 3.14 and newer f2caab61\n  * much better and safer than symlink/hardlink-based dedup, but only works with a few filesystems\n* #905 option to magnify images/videos to fill the screen 66dc8b5c\n* #921 #685 `xm` hooks can see the selected files (thx @carson-coder!) 6c024dbf 33644488\n* #927 textfiles can now be viewed with the `?doc=` suffix with just the `g` permission dbb78705\n* #742 new volflag `nodupem` to prevent dupes from being moved into a volume; the stronger alternative to `nodupe` which only prevents uploads f55d8341\n* audioplayer: show embedded coverart as fallback for cover.jpg in OS widgets 9746b4e2\n* #928 option to [hide certain ui-elements](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#hide-ui-elements), either with volflags or url-params 98da5cc5\n* #911 users can now avoid autoban according to permissions 6f02812a\n* verbosity and permssion options for `?stack` 677fd8ee\n  * default is now admin-only; previously it was \"admin or read+write\"\n\n## 🩹 bugfixes\n\n* #914 ftp-server: resuming interrupted uploads (thx @Audionut!) 33b0cd5a\n* race-the-beam didn't work in non-toplevel shares d9cd7ec3\n\n## 🔧 other changes\n\n* #904 new example hook [wget-i.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/wget-i.py); import-safe fork of [wget.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/wget.py) dbd8f837\n* hide the search-ui while viewing a share because searching in shares is not possible cca1f9b2\n* config-parser now prevents invalid values for the lifetime volflag 5d96862c\n* translations are now [separate files](https://github.com/9001/copyparty/tree/hovudstraum/copyparty/web/tl) instead of all chilling inside browser.js d099e5e8 d6433b78 a7840beb a7cdc5de 98086948 a85ad201 c2e03bf6 b9d7ede3 5a29df6b 52446bb5 bb166c98 0fa862e1 6de6aa4b 748aaa95 07ace416 b61b910e 28b93238 14bd4cf5 50109f76 3b009d97 f5425a88 5232ce6a 02ba9ea7 ff01723c d099e5e8\n\n## 🌠 fun facts\n\n* looks like i'll be in Japan november 7～26 and then at CCC for newyears!\n  * wait, I never made stickers... orz\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-1005-2259  `v1.19.16`  FULLBURST\n\n## 🧪 new features\n\n* [hooks](https://github.com/9001/copyparty#event-hooks) got some nice upgrades\n  * add flag `I` to run in-process for a **140x** speed boost 41ed559f\n    * bring your own safeguards (if an `I` hook has a bug then it can deadlock copyparty)\n    * (this is where the releasetitle came from btw)\n  * add flag `s` to send info on stdin instead of argv 4542ad3c\n  * new hook: [reject uploads into ramdisks](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reject-ramdisk.py) (granular alternative to `wram`) efd19af7\n    * will be default-enabled in the [bootable copyparty flashdrive image](https://a.ocv.me/pub/stuff/edcd001/enterprise-edition/)\n* show media-tags inside shares 50276c0c\n* #881 manga-mode (RTL) for the image viewer (thx @Scotsguy!) dacc64dd \n* #872 combining `chpw` and IdP-auth is now supported 3f597102\n* #854 auto-incrementing counters for batch-rename d05a88d2 76e9f23a\n* #882 change to volume-specific favicon on navigation 2ce32e4f\n* #884 option to turn the servername into a link (thx @Morganamilo!) 38cc8098 9b7f933b\n* rss: add option to not embed pw into feed 73ec2d29\n* cbz and epub files can become folder-thumbnails eb173be4\n\n## 🩹 bugfixes\n\n* web-ui: only show generic http errors if nothing better is available 0453b7ac\n* #860 epub-thumbnailer errors are less noisy now 4177c1d9\n* the `ui-filesz` option can have a trailing hyphen now 2248705e\n* hide \"create share\" button while inside a share c5f12296\n\n## 🔧 other changes\n\n* #460 example config for running the podman images as a systemd service (thx @danloveg!) 7fc379ab\n* #886 nixos: option to specify unix-user/group to run as (thx @2Kaleb!) 31f1b535\n* #895 mention the `?v` suffix to open mediafiles in the mediaplayer f8e19815\n* ignore 403s from `/favicon.png` (samsung-android)\n* docker: shrink the `min` image from 45 to 33 MiB a8f53d5e\n* #887 add missing entries in `--licenses` 805a7054\n* #887 various vendored python libraries can now be ripped out and replaced with system-libs:\n  * `ifaddr` 656f0a6c\n  * `dnslib` 39bd4e5b\n  * `qrcodegen` 08ebb0b4\n  * `surrogateescape` aace711e\n\n## 🌠 fun facts\n\n* today's genre is Techcore (a subgenre of J-core (a subgenre of UKHC))\n  * [FULLBURST](https://www.youtube.com/watch?v=mYqFHZdbawE) by ぱらどっと is an excellent example\n  * omake: [speedrun through 18 other genres](https://www.youtube.com/watch?v=_YnwnxSE2UA) (pick your favorite)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0929-2310  `v1.19.15`  merry christmas\n\n## 🧪 new features\n\n* #184 add various human-readable formats for filesizes 234eddec \n* search for files by their identifier (\"wark\"/checksum) 4e38e408\n  * and those are displayed in file-listings now too 456addf2\n* PUT-upload with header `Replace` will overwrite any existing files 397ed565\n* xbu/xau hooks can reject uploads with a custom message df0fa9d1\n* #855 mDNS options to change the announced http/https port a3d95067\n* #473 #383 custom favicons per-volume (.ico/png/gif/svg) 470b5048 \n  * doesn't seem to work in internet explorer... ah whatever, go next\n\n## 🩹 bugfixes\n\n* #849 create IdP-db for `--idp-store` when necessary 80ca7851\n* #859 cbz-thumbnailing had an accidental dependency on FFmpeg 983865d9 \n* docs: misleading markdown-expansion example e187df28\n\n## 🔧 other changes\n\n* #851 show a huge warning when copyparty accidentally detects a failing HDD and/or filesystem-corruption during indexing 6912e867 eb5d767b\n* #870 improved discord video embeds (thx @tsuza!) f0ecb083\n* #858 prefer reflinks (not hardlinks) in the `-ss` security option 57650a21\n* improved controlpanel action-buttons layout 9f46e4db\n\n## 🌠 fun facts\n\n* includes (a tiny bit of) code written at [koie ramen](https://a.ocv.me/pub/g/2025/09/PXL_20250925_151716836.jpg)\n* [according to Biltema](https://a.ocv.me/pub/g/2025/09/PXL_20250927_160446367~2.jpg), september is an excellent time to start decorating for xmas\n\n<img src=\"https://a.ocv.me/pub/stuff/padoru.gif\" alt=\"padoru\" /> <img src=\"https://a.ocv.me/pub/stuff/padoru.gif\" alt=\"padoru\" /> <img src=\"https://a.ocv.me/pub/stuff/padoru.gif\" alt=\"padoru\" />\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0923-2247  `v1.19.14`  Voile, the Magic Library\n\n## 🧪 new features\n\n* #779 add [OPDS](https://opds.io/) support (thx @Scotsguy!) 6dbd9901\n  * copyparty can now serve books for [KOReader](https://koreader.rocks/)\n  * [the mandatory soundtrack](https://www.youtube.com/watch?v=F8Aex6tzH-s)\n* #786 add Turkish translation (thx @NandeMD!) 549fe33f\n* #808 support reading config-files in UTF8-BOM 5e4ff90b\n* make more http-errors return a friendly errortext rather than the scary wall of html 9d066414\n\n## 🩹 bugfixes\n\n* #842 could not navpane into webroot if webroot is unmapped 0941fd4e\n* upload-resume becomes funky when the OS/network is overloaded to the point where it starts dropping connections left and right -- the issue was reported on discord and I don't have a good way to reproduce it, but these changes may help and/or fix it: \n  * b136a5b0 panic and drop chunk reservations if client or connection glitches out\n  * 38df223b also drop reservations if subchunk logic hits an edgecase\n\n## 🔧 other changes\n\n* [versus.md](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) tweaks:\n  * #840 tooltips in the table headers (thx @guano!) e9ca36fa\n  * #839 sftpgo updates (thx @augustanational!) a053a663\n\n## 🌠 fun facts\n\n* this release is identical to v1.19.13 except [the pypi package isn't messed up](https://github.com/9001/copyparty/issues/847) 👉😎👉\n  * as if the 13 wasn't foreshadowing enough\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0921-2211  `v1.19.12`  conlangparty\n\n## 🧪 new features\n\n* #787 add Esperanto translation (thx @slashdevslashurandom!) 15d3c2fb\n* #802 timezone can be specified for the rotf upload rule (thx @Lehmustus!) 1460fe97\n\n## 🩹 bugfixes\n\n* #837 sharing an entire HDD on Windows ([v1.19.9](https://github.com/9001/copyparty/releases/tag/v1.19.9) regression) 6a244320\n  * sharing your whole [【Dドライブ】](https://www.youtube.com/watch?v=BFfYrxm2t58) is once again possible\n    * TLNote: `Dドライブ` means \"D:\\ drive\"\n    * if you can't upgrade, a workaround is global-option `casechk: n`\n* `/?ls` on an unmapped root didn't give a sensible response; now it should be okay except it won't have a `cfg` field 8f6194fe\n\n## 🔧 other changes\n\n* #836 hide the unpost tab in folders where user has no write-access ca872c40\n* #835 fix webdep buildscript to avoid an edgecase on some platforms (thx @25huizengek1!) 260da2f4\n\n## 🌠 fun facts\n\n* the esperanto translation was the final straw; `copyparty-sfx.py` is now 1 MiB large\n  * `copyparty-en.py` is still a comfy 759 KiB\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0920-1011  `v1.19.11`  ftp fix\n\n## 🩹 bugfixes\n\n* #827 ftp on servers with unmapped root broke in v1.19.9 280815f1\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0919-2244  `v1.19.10`  ramdisk kinshi\n\n## 🧪 new features\n\n* prevent uploading into ramdisks by default 59a01221 538a205c\n  * safeguard against misconfigured docker containers, where certain parts of the vfs has not been mapped to actual storage, for example `/w/music` is but `/w/` itself isn't\n  * can be disabled with `wram` (global-option and/or volflag), mainly for ephemeral servers\n* #799 nixos: groups can be specified (thx @AnyTimeTraveler!) ee5f3190\n* the logspam from the filesystem indexer can be reduced/disabled 478f1c76\n  * new options `scan-st-r`, `scan-pr-r`, `scan-pr-s`\n\n## 🩹 bugfixes\n\n* #809 medialinks (`#af-badf00d`) would fail on the very first pageload from a new browser 5996a58b\n* #806 instructions for running on iOS was bad (thx @GhelloZ!) 35326a6f\n\n## 🔧 other changes\n\n* copyparty32.exe is now english-only, to save space 669b1075\n* version info on startup indicates free-threading or not 65591528\n* docs: explain the `daw` option better a043d7cf\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0915-0019  `v1.19.9`  case-sensitivity, give or take\n\n## 🧪 new features\n\n* #781 case-sensitive behavior is now simulated on Windows/Macos/Fat32/NTFS 8b66874b \n  * avoids some of the scary issues associated with case-insensitive filesystems\n  * unfortunately this is expensive and may be **noticeably slower in large folders;** disable the safeguard with `casechk: n` if you know you don't need it\n* #789 case-insensitive search for unicode filenames/paths (thx @km-clay!) e2aa8fc1 ecd18adc\n  * default-disabled because it is somewhat expensive; enable with global-option `srch-icase`\n* [CB-1](https://codeberg.org/9001/copyparty/issues/1) add `--qr-stdout` and `--qr-stderr` to show qr-code even with `-q` d7887f3d\n\n## 🩹 bugfixes\n\n* #775 the basic-uploader didn't accept empty files 25749b4b\n* opt-out from index.html with `?v` did not work as documented 3d09bec1 \n* Windows: dedup could get rejected by the filesystem if the origin file had a timestamp from the cambrian era e09f3c9e\n* webdav would incorrectly return an error for Depth:0 on an unmapped root 3a2381ff\n* markdown-editor would waste another http roundtrip on certain documents 14b7e514\n* `--help` didn't render if terminal was non-UTF8 3f454927\n\n## 🔧 other changes\n\n* #788 fixed a hotkey typo in the imageviewer (thx @tkroo!) 5c1a43c7\n* #778 improved polish translation (thx @daimond113!) 52438bcc\n* #798 debian: fixed an issue in the systemd script (thx @Beethoven-n, and congrats on commit number 4000!) dfd9e007\n* media-tag `conductor` is no longer mapped to `circle` (album-artist) 9c9e4057\n* \"download-selection-as-zip\" now produces a better filename, `sel-FOLDERNAME.zip` instead of `FIRSTFILE.zip` 8f587627\n* detect and warn if IdP volumes are misconfigured in a particular way 83bd1974\n\n## 🌠 fun facts\n\n* the themesong of this release is [KO3 - Give it up?](https://www.youtube.com/watch?v=8w_na7HAppU) because that's what the car mechanic got to enjoy when i forgot to unplug the flashdrive before handing in the shitbox for service\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0907-2300  `v1.19.8`  SECURITY: fix single-file shares\n\n## ⚠️ ATTN: this release fixes [CVE-2025-58753](https://github.com/9001/copyparty/security/advisories/GHSA-pxvw-4w88-6x95), an issue with shares\n\n* when a share is created for just one or more files inside a folder, it was possible to access the other files inside that folder by guessing the filenames\n* it was not possible to descend into subdirectories in this manner; only the sibling files were accessible\n* NOTE: this does NOT affect filekeys; this is specifically regarding the `shr` global-option\n\n## recent important news\n\n* [v1.19.8 (2025-09-07)](https://github.com/9001/copyparty/releases/tag/v1.19.8) fixed [CVE-2025-58753](https://github.com/9001/copyparty/security/advisories/GHSA-pxvw-4w88-6x95) (a missing permission-check inside single-file shares)\n* [v1.15.0 (2024-09-08)](https://github.com/9001/copyparty/releases/tag/v1.15.0) changed upload deduplication to be default-disabled\n* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details\n\n## 🧪 new features\n\n* #761 IdP: option to replace the login/logout links and buttons with redirects into an IdP UI 09f22993\n* #726 disk-usage and server-version can be selectively hidden according to user permissions 19a4c453\n* option `--shr-who` / volflag `shr_who` decides who is able to create a share of that volume edafa158\n* #751 nixos: add globalExtraConfig to specify repeatable config parameters (thx @xvrqt!) 09e3018b\n* some very small speedups (mainly u2c and ancient python versions) 74821a38\n* #759 #393 total folder size now decreases when files inside are deleted 96b109b0\n  * would previously require a reindex to get back on track\n\n## 🩹 bugfixes\n\n* fix [GHSA-pxvw-4w88-6x95](https://github.com/9001/copyparty/security/advisories/GHSA-pxvw-4w88-6x95) by fencing fileshares to just the shared files e0a92ba7\n* #397 prevent hinting at valid passwords, even if they cannot be used to authenticate with 7a4ee4db\n* #747 disable some features if `/tmp` must be used for runtime config e6755aa8\n  * the config-folder will now also be created with chmod 700 (accessible by owner only)\n* #733 #298 fix hotkeys on non-qwerty keyboard layouts (dvorak etc.) e798a9a5\n* #539 ftp-server: support clients which never does a CWD b0496311\n* ignore the plaintext session-cookie on https; fixes some confusing behavior when switching from https to http c71128fd\n* `og-ua` would prevent clients matching the pattern from accessing fullsize files\n* `og-ua` was only possible to set globally; the `og_ua` volflag was ignored 422f8f62\n* uds / unix-domain-sockets got wrong permissions when `rm-sck` was used e270fe60\n* #727 macos: support running from config-files 230a1462\n* #539 avoid issues if someone uploads a file with a last-modified timestamp from year -9999999999999 eeb7738b\n* using the spacebar to pause a video was jank on chrome bfcb6eac\n* block the next-song hotkey while a folder is loading f7e08ed0\n* #748 fix rare js-panic when an action is aborted aaeec11f\n* #738 bubbleparty: use /bin/bash (thx @ckastner!) 0469b5a2\n\n## 🔧 other changes\n\n* partyfuse: nice speedup by caching `readdir` too 06d2654b\n* partyfuse: explain usage with usernames 1cdb3880\n* connect-page: better examples when usernames enabled 3bdef75e\n* docker: fix image annotations ab562382\n\n## 🌠 fun facts\n\n* konami's biggest legacy lives on f0caf881 bd6d1f96\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0828-2014  `v1.19.7`  chdir\n\n## 🧪 new features\n\n* new option `chdir` to change the PWD (process working-directory) before volumes are mapped 14555d58\n\n## 🩹 bugfixes\n\n* fix using empty folders as statefile storage ([v1.19.6](https://github.com/9001/copyparty/releases/tag/v1.19.6) made this a bit too strict) 0d96786e\n* holding I/K to scroll through folders quickly now works better 914686ec\n\n## 🔧 other changes\n\n* #717 docker: fix the image repo metadata (thx @EmilyxFox!) 6f087117\n* docker: change `$HOME` to `/state` 01cf20a0 d1f75229\n  * and use the new `chdir` option to preserve old config-file semantics 14555d58\n  * helps avoid statefiles accidentally landing in `/w` as a consequence of misconfiguration\n\n## 🌠 fun facts\n\n* this release was made at [RevSpace NL](https://a.ocv.me/pub/g/nerd-stuff/PXL_20250828_202820075.jpg?cache) \n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0827-2038  `v1.19.6`  auth-precedence\n\n## 🧪 new features\n\n* #673 add Portuguese translation (thx anonymous!) 4b8c2215\n  * ...and enable the Polish translation (whoops) 8f235be6\n* #689 add option to control authentication priority/precedence 543b7ea9\n* url-parameter `?dl` forces file download instead of displaying in-browser 48d6224e\n* #533 more ways to make the QR-code always-visible in the console 2848941e\n* #695 option to log invalid xml from clients 28b93d79\n* #552 configurable markdown newline behavior 0491123b\n  * and tweak the styling of monospace in links 68503444\n\n## 🩹 bugfixes\n\n* #628 FTP-server now accepts connections from IPv6 link-local addresses 978801d0\n* incorrect assumption that all IPv6 link-local addresses start with `fe80` d39c74c1\n* ftp: fix file rename d40f061a\n* u2c: couldn't upload files located at the very top of the unix file hierarchy 599e82f2\n* #699 markdown-editor: fix panic if the table-formatter is executed on something that isn't a table 4c042b3c\n\n## 🔧 other changes\n\n* #696 a volume can be one single file, not just folders aa1c9213\n* #442 strongly prefer XDG_CONFIG_HOME as config location 35472557\n* #691 album-art collected from audio-files can now become folder thumbnails 0b50fde3\n* allow spaces in more of the comma-separated options d30240b4\n* docs:\n  * mention config requirements for [syncing folders](https://github.com/9001/copyparty/#folder-sync) with u2c 6cd0a396 59f142cd\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0821-2319  `v1.19.5`  it runs on iOS\n\n## 🧪 new features\n\n* #328 run copyparty on iPhones; see [install on iOS](https://github.com/9001/copyparty#install-on-iOS) in the readme ca98d54f\n  * cannot run in the background, doesn't have full access to your files, and is slightly buggy, but it *works*\n  * [running on android](https://github.com/9001/copyparty#install-on-android) gives you a much better experience\n* save the qr-code to a file (txt/svg/png) 202ddeac\n\n## 🩹 bugfixes\n\n* #661 fix incorrect `rproxy` hint in the logs 6c76614e\n* #649 fix js-crash when tapping in the exactly correct place (thx @hahaslav for debugging!) 0de07d8e\n* #628 ftpd: fix banning IPv6 clients 6d76254c\n\n## 🔧 other changes\n\n* #296 nixos: support non-flake setups (thx @Sorixelle!) 20ef74cd 32593670\n* config-parser catches and explains a few more common mistakes cc65b1b5\n* docs:\n  * #490, #199: readme: confirm that combining copyparty and syncthing is safe c51371c7\n  * #377 improved authelia docker example (thx @xFuture603!) cd8771fa \n  * mention the homebrew formulae f9cb2c15\n  * #651 versus.md: fix hfs3 comparison (thx @rejetto!) 7a4973fa\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0817-1556  `v1.19.4`  take two (fix cfg vols)\n\n## ℹ️ this upgrade is a one-way ticket\n\n* your up2k database (`.hist/up2k.db`), used by the `e2d` filesystem indexing feature, **will be upgraded to a new format** which older copyparty versions cannot read. A backup of each database will be created automatically, named `up2k.db.bak.SOMETHING.v5`. If you need to downgrade to a previous version: Shutdown copyparty, delete these files: `up2k.db up2k.db-shm up2k.db-wal` and then copy `up2k.db.bak.*.v5` to `up2k.db`\n\n## 🧪 new features\n\n* new translations:\n  * #551 Swedish (thx @Bevinsky!) d676a86f\n  * #551 Korean (thx @nyqui!) 4e878d2f\n* #581 new theme: phi95 (thx @varphi-online!) d8662aeb\n* #567 .raw image thumbnails (thx @ar-nelson!) 0177a9b4\n  * available in docker-images `iv` and `dj`\n* #561 epub thumbnails (thx @Scotsguy!) 9435e6b2\n* #252 music thumbnails use embdded coverart if available 98d117b8\n  * thumbnails folder `.hist/th` must be deleted to take effect\n* #530 show username of uploaders in file listings; requires `a` (admin) permission 4df033ec\n* #604 a new group `@acct` which automatically contains all known usernames 68907eaf\n* controlpanel has a dedicated \"logout all sessions\" button, similar to the logout-link in the browser f4a3fba2\n* #397 accounts can be restricted to certian IPs 62e072a2\n* #504 automatic login through tailscale auth a4649d1e\n* #533 sticky qr-code with `--qr-pin 1` 1ebe06f5\n* #572 button to abort copy/move 715d374e\n* #618 \"download selected files\" didn't work on firefox 52 (winxp) dcc6b1b4\n* max number of cookies to allow can be configured 6303effe\n  * good if you have too many selfhosted services on one domain (but will beware of the spec-mandataed max length of the cookie field!)\n\n## 🩹 bugfixes\n\n* fix xvol/xdev edgecases:\n  * #603 rootless vfs 554cc2f3 \n  * false-positive with overlapping volumes d9046f7e\n* #573 ftp: attempting an upload into read-only folder no longer kills the connection 3aa8b7aa\n* #306 adjust navpane for `--rp-loc` (location-based proxying)\n* #556 more sensible config expansion order f4727f8e\n  * #624 ...which broke things bf1fdcab\n* the video player now stays fullscreen between videos 782e2f1d\n* heif thumbnailing with libvips\n\n## 🔧 other changes\n\n* #253 build nix-packages from source (thx @toast003, @chinponya!) 187cae25\n* #616 logfiles will have a plaintext severity column if `--no-ansi` d4cf42e7\n* #598 separate option `--ac-convt` for audio transcoding timeout d5623057\n* #596 users with a blank password gets a strong random-generated one 7f448750\n* copyparty.exe: upgrade to python 3.13.7\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0810-1226  `v1.19.1`  archlinux fix\n\n## 🧪 new features\n\n* new translations:\n  * #486 French (thx @Tr3yWay996, @Packingdustry, @Alee14, @jakubiakfr, @Equinoxs!) e9ddfccf 7aa21483 b87f8f1b\n  * #463 Polish (thx @pufereq and @daimond113!) 392a4db5\n  * #537 Nynorsk (thx @chinatsu!) 3931bc27\n* #549 custom mdns domain 3c78c6a8\n\n## 🩹 bugfixes\n\n* #539 FTP glitches when running on windows 8ba98877\n* #555 global-config didn't load through PRTY_CONFIG (thx @icxes!) 074e106e\n* macos: could take a while to establish webdav connection from finder a01870b7\n* ux:\n  * dropdown colors 347cf6a5\n  * case-sensitivity in filters e5e82295\n  * iOS being too enthusiastic about using saved passwords 03acd65e\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0807-2213  `v1.19.0`  usernames\n\n## 🧪 new features\n\n* #511 login with username and password (not just password) can now optionally be enabled with `--usernames` 346515cc\n  * if you have enabled password hashing (`ah-alg: argon2` or similar) then you will need to hash your passwords again after enabling usernames, hashing them as `username:password:`\n* #468 add Greek translation (thx @chamdim!) 50f46187 392abd06\n* #471 add Czech translation (thx @kubakubakuba!) c9556583\n* #515 support systemd socket acivation (thx @mati1210!) 9b9d2a92\n* #523 add QR-code to the connectpage bcc3b156\n* #513 optional EOL-conversion for texteditor 8b31ed88\n* controlpanel refresh-button now toggles automatic refresh 7ae84dea\n\n## 🩹 bugfixes\n\n* fix stuck uploads when the up2k database (`e2d`) is not enabled 4a043568\n  * if more than 60'000 files were uploaded and there were several dupes of some files, they could get stuck and never upload\n  * upload performance is improved remarkably by enabling `e2d` so such huge uploads non-e2d had not been tested in a long time \n* #467 #470 fix ui-crash when exporting links of all uploaded files to clipboard (thx @geekalaa!) 0df1901f\n* #487 fix ui-crash when the location url-part is `//` 0f55a1ae\n* fix viewing `.MD` files (8a0746c6)\n\n## 🔧 other changes\n\n* when a reverse-proxy is detected, force explicit configuration of `--rproxy` to obtain correct client IP 3f8cb7e8\n  * a bit inconvenient, but helps prevent potentially-dangerous misconfiguration\n  * the necessary configuration changes are explained in the serverlog (you can't miss it)\n  * thanks to @person4268 for pointing out that there was room for improvements!\n* failed login attempts now only log a sha512 hash of the provided password\n  * to see login-attempts with incorrect passwords as plaintext like before, `log-badpwd: 1`\n* #502 add systemd user services and templated services (thx @icxes!) 34d98e99\n* #475 improve helptext for multivalue global-options c2ac57a2\n* #475 add [chungus.conf](https://github.com/9001/copyparty/blob/hovudstraum/docs/chungus.conf), massive extensive nonsensical demo config b664ebb0\n* try to detect proxies with incorrect caching behavior 9e980bb5\n* recent-uploads now support ie9 a57f7cc2\n* languages and themes are now dropdowns a9ee4f24\n* copyparty.exe: upgrade python to 3.13.6 a98360f2\n* introduce [copyparty-en.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-en.py), english-only edition of copyparty-sfx.py to save space 33497e6b\n\n## 🗿 known issues \n\n* the `copyparty.pyz` in this release is english-only, and does not include the translations -- they got lost in transit while adjusting the buildscripts to make `copyparty-en.py`\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0804-0013  `v1.18.10`  idp speedboost\n\n## 🧪 new features\n\n* #426 add Dutch translation (thx @DeStilleGast!) 3798e19a\n* #458 add Italian translation (thx @AOTREVAI!) a38e6e65\n* #456 transcode to flac/wav (thx @missaustraliana!) b469db3c b2d48c64 0d09fb68\n* #439 config-file can be provided through `PRTY_CONFIG` (thx @icxes!) 971360e9\n* #459 videos can become folder thumbnails 16bbcce5\n* add `--idp-cookie`, session-tickets for IdP auth (performance boost) f9502c3d\n  * useful when the IdP-server becomes a bottleneck\n\n## 🩹 bugfixes\n\n* #412 fix PUT-uploads into volumes with `nosub` volflag 47fa4a92\n* #435 ignore spurious exceptions from browser extensions 39e55824\n* #449 IPv6 QR-Code didn't include port 66a5bf36\n* #295 do not force `d2d` in blank vfs (introduced in v1.18.3) 848315c0\n\n## 🔧 other changes\n\n* #440 improved finnish translation (thx @icxes!) a68d5b03\n* point to the `-nc` option in the \"at max connections\" warning 153d240d\n* the play-button now indicates \"play-as-audio\" for video-files 40d56bb3\n* docs:\n  * #411 improve password-hashing instructions (thx @chinponya!) c69c7c8a\n  * #429 improve `--cert` helptext (thx @kzshantonu!) 7e3825f8\n  * #413 copyparty is Wii Internet Channel compatible! (thx @techflashYT!) 50f16293\n  * #461 how to use groups without IdP e85a7107\n  * mention that WebDAV and OpenGraph are incompatible by default (and how to fix that) 0bc1b8f7\n  * #345 short explanation about the sfx in quickstart ae5eefc5\n* #398 pypi-package now has extra-group `all` 6eaf8af1\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0801-2056  `v1.18.9`  fix Denial-of-Service\n\n## ⚠️ ATTN: this release fixes a Denial-of-Service vuln\n\n[CVE-2025-54796](https://github.com/9001/copyparty/security/advisories/GHSA-5662-2rj7-f2v6): an unauthenticated user could make the server grind to a halt by accessing a particular URL\n\n## recent important news\n\n* [v1.18.9 (2025-08-01)](https://github.com/9001/copyparty/releases/tag/v1.18.9) fixed [CVE-2025-54796](https://github.com/9001/copyparty/security/advisories/GHSA-5662-2rj7-f2v6) (Denial-of-Service)\n* [v1.15.0 (2024-09-08)](https://github.com/9001/copyparty/releases/tag/v1.15.0) changed upload deduplication to be default-disabled\n* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details\n\n## 🧪 new features\n\n* #310 translated to Spanish (thx @herruzo99!) a1dfd0be\n* #350 translated to Ukrainian (thx @MrMebelMan!) fea45e45\n* #321 translated to Russian (thx @A1Asriel!) 0b05c726\n* #381 translated to Finnish (thx @icxes and @Permik!) 7ecedb2c\n  * haha it says surf\n* #312 add option to use localtime in the UI ad23b253\n* #386 initial packaging for debian (thx @Beethoven-n!) 3c6f0b17\n\n## 🩹 bugfixes\n\n* CVE-2025-54796 / GHSA-5662-2rj7-f2v6 09910ba8\n* #347 fix upload-abort when uploading to a share 6d6d79fc\n* fix xiu backlog dropping on restart 3222ba3a\n* #375 fix crash on really old versions of python2.7 (thx @bb!) b69d5901\n* #388 another python2.7 fix: improve unicode support in u2c (thx @KevinXuxuxu!) 9c197535\n* log creator of new/blank markdown docs d0d2f206\n* #400 config didn't support indenting with tabs c1604288\n\n## 🔧 other changes\n\n* `ack` was changed to `continue` 4fa7be2a\n\n## 🌠 fun facts\n\n* the translations have made the sfx size balloon from 766 to 845 KiB in under a week... nice! keep em coming :tada: \n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0731-0833  `v1.18.8`  sfx hotfix\n\n## 🩹 bugfixes\n\n* #354 fix `copyparty-sfx.py` failing to start on certain versions of python c17ce4892ecdb4e11437bc2785d132bd8100eaec\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0730-2131  `v1.18.7`  SECURITY: fix another XSS\n\n## ⚠️ ATTN: this release fixes an XSS vulnerability\n\n[GHSA-8mx2-rjh8-q3jq](https://github.com/9001/copyparty/security/advisories/GHSA-8mx2-rjh8-q3jq), could let an attacker execute arbitrary JS by tricking you into clicking a malicious URL\n\nSoon there won't be many of these left, surely. Huge thanks to @Ju0x for finding and reporting this.\n\n## 🧪 new features\n\n* #265 uid/gid for new files can be configured per-volume f1959988\n  * has preconditions; [see readme](https://github.com/9001/copyparty#chmod-and-chown)\n* #212 add German translation (thx @rGunti, @Scotsguy, @chocolateimage) 9d32564c\n\n## 🩹 bugfixes\n\n* GHSA-8mx2-rjh8-q3jq a8705e61\n* #276 windows: fix segfault (thx @kernel1994 for debugging!) a9d07c63\n* #272 webdav: send disk-size and disk-free to clients 4988a55e\n* #285 use disk-free sans root-reserve on linux (thx @Arklaum!) c3cc2dde \n* cors-check was funky on IPv6 e9684d40\n* #325 upgrade sharex example for newer versions 6016ec93\n* #300 restore support for old versions of python 2.7 b7ca6f4a\n\n## 🔧 other changes\n\n* shares: the config POST-target is now always the webroot (for ease of IdP configuration) fb7cbc42 \n* unlist: now applies to the navpane too fbf17be2\n* windows: show disk-usage as well, not just disk-free 5c6341e9\n* #228 nix-pkg improvements (thx @dtomvan!) 4915b14b\n* docker-compose: ensure logs appear in realtime 3cde1f3b\n* mention that IdP-volumes and users [can now be persisted](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#but-you-can-enable-idp-volume-persistence) 6069bc9b\n* #316 explain a scary-looking thing in the code 053de619\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0728-2320  `v1.18.6`  reflink-dedup\n\n## 🧪 new features\n\n* #201 add support for reflink-based dedup on cow filesystems df9feabc\n  * combine `--dedup` with `--reflink` to enable, or volflags with same name\n  * a better and safer alternative to the other dedup approaches (symlink/hardlink), but only possible to use in some cases:\n    * needs linux 5.3 or newer, python 3.14 or newer, btrfs/xfs/zfs\n    * not available in the docker images yet; needs a new version of python, so maybe next alpine release (november/december 2025)\n* ratelimit password changes to impede bruteforcing a2601fd6\n  * limit is set by `--ban-pwc` (default is 5 changes in 60min)\n\n## 🩹 bugfixes\n\n* #240 nixos: fix unixgroups issue (thx @chinponya!) 7c9c962b\n* #246 cbz: use correct page for thumbnail (thx @Scotsguy!) 542a1de1\n\n## 🔧 other changes\n\n* volflag `nosub` now also prevents mkdir 0f2c6235\n* improve documentation:\n  * #229 use the same example UDS path everywhere cb019afe\n  * [example nginx config](https://github.com/9001/copyparty/blob/hovudstraum/contrib/nginx/copyparty.conf) had misleading cloudflare comment (thx @jmi2k!) 674fc1fe\n  * more readable `--help-chmod` 03d23dae\n  * #244 fix typo in `--help` 4f013f64\n* #242 hide \"use real pw\" on connectpage if no accounts (thx @toast003!) 025942a7\n* #211 docker: remove deprecated attribute (thx @ptweezy!) 5b98e104\n* #190 add the feature-showcase video to the readme (thx @RustoMCSpit!) 43e6da34\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0727-2305  `v1.18.5`  SECURITY: fix XSS in media tags\n\n## ⚠️ ATTN: this release fixes an XSS vulnerability\n\n[GHSA-9q4r-x2hj-jmvr](https://github.com/9001/copyparty/security/advisories/GHSA-9q4r-x2hj-jmvr), exploitable in two different ways, could let an attacker execute arbitrary javascript on other users:\n* either: tricking someone into clicking a malicious URL to load and execute javascript\n* or: uploading a malicious audio file to the server, affecting any successive visitors\n\nso, with new and curious eyes on the project, we are starting off with a bang. Huge thanks to @altperfect for finding and reporting this earlier today.\n\n## recent important news\n\n* [v1.18.5 (2025-07-28)](https://github.com/9001/copyparty/releases/tag/v1.18.5) fixed XSS in display of media tags\n* [v1.15.0 (2024-09-08)](https://github.com/9001/copyparty/releases/tag/v1.15.0) changed upload deduplication to be default-disabled\n* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details\n\n## 🧪 new features\n\n* #214 option to stop playback after one song, and/or at end of folder 6bb27e60\n\n## 🩹 bugfixes\n\n* GHSA-9q4r-x2hj-jmvr 895880ae \n* block external m3u files 2228f81f\n* #202 the connect-page could show IP-address when it should have used hostnames/domains b0dec83a\n* scrolling locked after tailing a file and closing it creatively d197e754\n\n## 🔧 other changes\n\n* #189 the `SameSite` cookie parameter now defaults to `Strict`, increasing CSRF protection ca6d0b8d\n  * new option `--cookie-lax` reverts to previous value `Lax`\n* docker: add FTPS support b4199847\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0725-1841  `v1.18.4`  Landmarks\n\n## 🧪 new features\n\n* #182 [Landmarks](https://github.com/9001/copyparty#database-location) edba7fff\n  * detects that a storage backend is glitching out and disengage the up2k-database as a precaution\n* #183 quickdelete 21a96bcf \n  * new togglebutton `qdel` in the UI which reduces the number of deletion confirmations by one\n  * global-option `--qdel=0` which can bring it all the way to zero (good luck)\n\n## 🩹 bugfixes\n\n* fix unpost in recently created shares 2d322dd4\n* fix filekeys on windows df6d4df4\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0721-2307  `v1.18.3`  drop the umask\n\n## 🧪 new features\n\n* #181 the default chmod (unix-permissions) of new files and folders can now be changed 9921c43e\n  * `--chmod-d` or volflag `chmod_d` sets directory permissions; default is 755\n  * `--chmod-f` or volflag `chmod_f` sets file permissions; default is usually 644 (OS-defined)\n  * see `--help-chmod` which explains the numbers\n\n## 🩹 bugfixes\n\n* #179 couldn't combine `--shr` (shares) and `--xvol` (symlink-guard) 0f0f8d90\n* #180 gallery buttons could still be clicked when faded-out 8c32b0e7\n* rss-feeds were slightly busted when combined with rp-loc (location-based proxying) 56d3bcf5\n* music-playback within search-results no longer jumps into the next folder at end-of-list 9bc4c5d2\n* video-playback on iOS now behaves like on all other platforms 78605d9a\n  * (it would force-switch into fullscreen because that's their default)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0707-1419  `v1.18.2`  idp-vol persistence\n\n## 🧪 new features\n\n* IdP-volumes can optionally be persisted across restarts d162502c\n  * there is a UI to manage the cached users/groups 4f264a0a\n    * only available to users listed in the new option `--idp-adm`\n* api for manually rescanning several volumes at once 42c199e7\n  * `/some/path/?scan` does that one volume like before\n  * `/any/path/?scan=/vol1,/another/vol2` rescans `/vol1` and `/another/vol2`\n* volflag to hide volume from listing in controlpanel fd7c71d6\n\n## 🩹 bugfixes\n\n* macos: fix confusing crash when blocked by [Little Snitch](https://www.obdev.at/products/littlesnitch/) bf11b2a4\n* unpost could break in some hairy reverseproxy setups 1b2d3985\n* copyparty32.exe: fix segfault on win7 c9fafb20\n* ui: fix navpane overlapping the scrollbar (still a bit jank but eh) 7ef6fd13\n* usb-eject: support all volume names ed908b98\n* docker: ensure clean slate deb6711b\n* fix up2k on ie11 d2714434\n\n## 🔧 other changes\n\n* update buildscript for keyfinder to support llvm 65c4e035\n* #175 add `python-magic` into the `iv` and `dj` docker flavors (thx @Morganamilo) 77274e9d\n* properly killed the experimental docker flavors to avoid confusion 8306e3d9\n* copyparty.exe: updated pillow 299cff3f f6be3905\n  * avif support was removed to save 2 MiB\n\n## 🌠 fun facts\n\n* this release was slightly delayed due to a [norwegian traffic jam](https://a.ocv.me/pub/g/2025/07/PXL_20250706_143558381.jpg)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0622-0020  `v1.18.0`  Logtail\n\n## 🧪 new features\n\n* textfile-viewer can now livestream logfiles (and other growing files) 17fa4906 77df17d1 a1c7a095 6ecf4fdc\n  * see [readme](https://github.com/9001/copyparty/#textfile-viewer) and the [live demo](https://a.ocv.me/pub/demo/logtail/)\n* IdP-volumes: extend syntax for excluding certain users/groups 2e53f797\n  * the commit-message explains it well enough\n* new option `--see-dots` to show dotfiles in the web-ui by default c599e2aa\n* #171 automatic mimetype detection for files without extensions (thx @Morganamilo!) ec05f8cc 9dd5dec0\n  * default-disabled since it has a performance impact on webdav\n    * there are plans to fix this by using the db instead\n* #170 improve custom filetype icons\n  * be less strict; if a thumbnail is set for `.gz` files, use it for `.tar.gz` too c75b0c25\n  * improve config docs fa5845ff\n\n## 🩹 bugfixes\n\n* cosmetic: get rid of some noise along the bottom of some cards in the gridview 8cae7a71\n* cosmetic: satisfy a new syntax warning in cpython-3.14 5ac38648\n\n## 🔧 other changes\n\n* properly document how to [build from source](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#build-from-scratch) / build from scratch f61511d8\n* update deps\n  * copyparty.exe: python 3.13 1eff87c3\n  * webdeps: dompurify 7eca90cc\n\n## 🌠 fun facts\n\n* this release was cooked up in a [swedish forest cabin](https://a.ocv.me/pub/g/nerd-stuff/forestparty.jpg)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0527-1939  `v1.17.2`  pushing chrome to the limits (and then some)\n\n## 🧪 new features\n\n* not this time\n\n## 🩹 bugfixes\n\n* up2k: improve file-hashing speed on recent versions of google chrome e3e51fb8\n  * speed increased from 319 to 513 MiB/s by default (but older chrome versions did 748...)\n  * read the commit message for the full story, but basically chrome has gotten gradually slower over the past couple versions (starting from v133) and this makes it slightly less bad again\n  * hashing speed can be further improved from `0.5` to `1.1` GiB/s by enabling the `[wasm]` option in the `[⚙️] settings` tab\n    * this option can be made default-enabled with `--nosubtle 137` but beware that this increases the chances of running into browser-bugs (foreshadowing...)\n* up2k: fix errorhandler for browser-bugs (oom and such) 49c71247\n  * because [chrome-bug 383568268](https://issues.chromium.org/issues/383568268) is about to make a [surprise return?!](https://issues.chromium.org/issues/383568268#comment14)\n* #168 fix uploading into shares if path-based proxying is used 9cb93ae1\n* #165 unconditionally heed `--rp-loc` 84f5f417\n  * the config-option for [path-based proxying](https://github.com/9001/copyparty/#reverse-proxy) was ignored if the reverse-proxy was untrusted; this was confusing and not strictly necessary\n\n## 🔧 other changes\n\n* #166 the nixos module was improved once more (thx @msfjarvis!) 48470f6b 60fb1207\n* added usage instructions to [minimal-up2k.js](https://github.com/9001/copyparty/tree/hovudstraum/contrib/plugins#example-browser-js), the up2k-ui [simplifier](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png) 1d308eeb\n* docker: improve feedback if config is bad or missing 28b63e58\n\n## 🌠 fun facts\n\n* this release was tested using an [unreliable rdp connection](https://a.ocv.me/pub/g/nerd-stuff/PXL_20250526_021207825.jpg) through two ssh-jumphosts to a qemu win10 vm back home from the bergen-oslo night train wifi\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0518-2234  `v1.17.1`  as seen on archlinux\n\n## 🧪 new features\n\n* new toolbar button to zip/tar the currently open folder 256dad8c\n* new options to specify the default checksum algorithm for PUT/bup/WebDAV uploads 0de09860\n* #164 new option `--put-name` to specify the filename of nameless uploads 5dcd88a6\n  * the default is still `put-TIMESTAMP-IPADDRESS.bin`\n\n## 🩹 bugfixes\n\n* #162 password-protected shares was incompatible with password-hashing c3ef3fdc\n* #161 m3u playlist creation was only possible over https 94352f27\n* when relocating/redirecting an upload from an xbu hook (execute-before-upload), could miss an already existing file at the destination and create another copy 0a9a8077\n* some edgecases when moving files between filesystems f425ff51\n* improve tagscan-resume after a server restart (primarily for dupes) 41fa6b25\n* support prehistoric timestamps in fat16 vhd-drives on windows 261236e3\n\n## 🔧 other changes\n\n* #159 the nixos module was improved (thx @gabevenberg and @chinponya!) d1bca1f5\n* an archlinux maintainer adopted the aur package; copyparty is now [officially in arch](https://archlinux.org/packages/extra/any/copyparty/) b9ba783c\n* #162 add KDE Dolphin instructions to the conect-page d4a8071d\n* audioplayer now knows that `.oga` means `.ogg`\n\n## 🌠 fun facts\n\n* this release contains code [pair-programmed during an anime rave](https://a.ocv.me/pub/g/nerd-stuff/PXL_20250503_222654610.jpg)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0426-2149  `v1.17.0`  mixtape.m3u\n\n## 🧪 new features\n\n* [m3u playlists](https://github.com/9001/copyparty/#playlists) 897f9d32 ad200f2b 4195762d fff45552\n  * create and play m3u / m3u8 files\n\n## 🩹 bugfixes\n\n* improve support for ie11 (yes, internet explorer 11) 3090c748 95157d02\n* now possible to launch the password-hasher cli while another instance is running dbfc899d\n  * in preparation of #157 / #159\n\n## 🔧 other changes\n\n* make better decisions when running in a VM with less than 1 GiB RAM dc3b7a27\n\n## 🌠 fun facts\n\n* this release contains code written [less than 1masl](https://a.ocv.me/pub/g/nerd-stuff/PXL_20250425_170037812.jpg) and was gonna be named [hash again](https://www.youtube.com/watch?v=twUFbqyul_M) since it was originally just the password-hasher fix, but then kipun suggested adding playlist support (thx kipun)\n* [donations](https://github.com/9001/) are now also possible through github -- good alternative to paypal (y)\n  * and thanks a lot for the support (and kind words therein) so far, appreciate it :>\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0420-1836  `v1.16.21`  unzip-compat\n\na couple guys have been asking if I accept donations -- thanks a lot!! added a few options on [my github page](https://github.com/9001/) :>\n\n## 🧪 new features\n\n* #156 add button to loop/repeat music 71c55659\n\n## 🩹 bugfixes\n\n* #155 download-as-zip: increase compatibility with the unix `unzip` command db33d68d\n  * this unfortunately reduces support for huge zipfiles on old software (WinXP and such)\n  * and makes it less safe to stream zips into unzippers, so use tar.gz instead\n  * and is perhaps not even a copyparty bug; see commit-message for the full story\n\n## 🔧 other changes\n\n* show warning on Ctrl-A in lazy-loaded folders 5b3a5fe7\n* docker: hide keepalive pings from logs d5a9bd80\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0413-2151  `v1.16.20`  all sorted\n\n## 🧪 new features\n\n* when enabled, natural-sort will now also apply to tags, not just filenames 7b2bd6da\n\n## 🩹 bugfixes\n\n* some sorting-related stuff 7b2bd6da\n  * folders with non-ascii names would sort incorrectly in the navpane/sidebar\n  * natural-sort didn't apply correctly after changing the sort order\n* workaround [ffmpeg-bug 10797](https://trac.ffmpeg.org/ticket/10797) 98dcaee2\n  * reduces ram usage from 1534 to 230 MiB when generating spectrograms of s3xmodit songs (amiga chiptunes)\n* disable mdns if only listening on uds (unix-sockets) ffc16109 361aebf8\n\n## 🔧 other changes\n\n* hotkey CTRL-A will now select all files in gridview 233075ae\n  * and it toggles (just like in list-view) so try pressing it again\n* copyparty.exe: upgrade to pillow v11.2.1 c7aa1a35\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0408-2132  `v1.16.19`  GHOST\n\ndid you know that every song named `GHOST` is a banger? it's true! [ghost](https://www.youtube.com/watch?v=NoUAwC4yiAw) // [ghost](https://www.youtube.com/watch?v=IKKar5SS29E) // [ghost](https://www.youtube.com/watch?v=tFSFlgm_tsw)\n\n## 🧪 new features\n\n* option to store markdown backups out-of-volume fc883418\n  * the default is still a subfolder named `.hist` next to the markdown file\n  * `--md-hist v` puts them in the volume's hist-folder instead\n  * `--md-hist n` disables markdown-backups entirely\n* #149 option to store the volume sqlite databases at a custom locations outside the hist-folder e1b9ac63\n  * new option `--dbpath` works like `--hist` but it only moves the database file, not the thumbnails\n  * they can be combined, in which case `--hist` is applied to thumbnails, `--dbpath` to the db\n  * useful when you're squeezing every last drop of performance out of your filesystem (see the issue)\n* actively prevent sharing certain databases (sessions/shares) between multiple copyparty instances acfaacbd\n  * an errormessage was added to explain some different alternatives for doing this safely\n    * for example by setting `XDG_CONFIG_HOME` which now works on all platforms b17ccc38\n\n## 🩹 bugfixes\n\n* #151 mkdir did not work in locations outside the volume root (via symlinks) 2b50fc20\n* improve the ui feedback when trying to play an audio file which failed to transcode f9954bc4\n  * also helps with server-filesystem issues, including image-thumbs\n\n## 🔧 other changes\n\n* #152 custom fonts are also applied to textboxes and buttons (thx @thaddeuskkr) d450f615\n* be more careful with the shares-db 8e0364ef\n* be less careful with the sessions-db 8e0364ef\n* update deps c0becc64\n  * web: dompurify\n  * copyparty.exe: python 3.12.10\n* rephrase `-j0` warning on windows to also mention that Microsoft Defender will freak out c0becc64\n* #149 add [a script](https://github.com/9001/copyparty/tree/hovudstraum/contrib#zfs-tunepy) to optimize the sqlite databases for storage on zfs 4f397b9b\n* block `GoogleOther` (another recalcitrant bot) from zip-downloads c2034f7b\n* rephrase `-j0` warning on windows to also mention that Microsoft Defender will freak out c0becc64\n* update [contributing.md](https://github.com/9001/copyparty/blob/hovudstraum/CONTRIBUTING.md) with a section regarding LLM/AI-written code cec3bee0\n* the [helptext](https://ocv.me/copyparty/helptext.html) will also be uploaded to each github release from now on, [permalink](https://github.com/9001/copyparty/releases/latest/download/helptext.html)\n* add review from ixbt forums b383c08c\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0323-2216  `v1.16.18`  zlib-ng\n\n## 🧪 new features\n\n* prefer zlib-ng when available 57a56073\n  * download-as-tar-gz becomes 2.5x faster\n  * default-enabled in docker-images\n  * not enabled in copyparty.exe yet; coming in a future python version\n* docker: add mimalloc (optional, default-disabled) de2c9788\n  * gives twice the speed, and twice the ram usage\n\n## 🩹 bugfixes\n\n* small up2k glitch 3c90cec0\n\n## 🔧 other changes\n\n* rename logues/readmes when uploaded with write-only access 2525d594\n  * since they are used as helptext when viewing the page\n* try to block google and other bad bots from `?doc` and `?zip` 99f63adf\n  * apparently `rel=\"nofollow\"` means nothing these days\n\n### the docker images for this release were built from e1dea7ef\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0316-2002  `v1.16.17`  boot2party\n\n## NEW: make it a bootable usb flashdrive\n\nget the party going anywhere, anytime, no OS required! [download flashdrive image](https://a.ocv.me/pub/stuff/edcd001/enterprise-edition/) or watch the [low-effort demo video](https://a.ocv.me/pub/stuff/edcd001/enterprise-edition/hub-demo-hq.webm) which eventually gets to the copyparty part after showing off a bunch of other stuff on there\n\n* there is [source code](https://github.com/9001/asm/tree/hovudstraum/p/hub) and [build instructions](https://github.com/9001/asm/tree/hovudstraum/p/hub/sm/how2build) too\n* please don't take this too seriously\n\n## 🧪 new features\n\n* option to specify max-size for download-as-zip/tar 494179bd 0a33336d\n  * either the total download size (`--zipmaxs 500M`), and/or max number of files (`--zipmaxn 9k`)\n  * applies to all uesrs by default; can also ignore limits for authorized users (`--zipmaxu`)\n  * errormessage can be customized with `--zipmaxt \"winter is coming... but this download isn't\"`\n* [appledoubles](https://a.ocv.me/pub/stuff/?doc=appledoubles-and-friends.txt) are detected and skipped when uploading with the browser-UI 78208405\n* IdP-volumes can be filtered by group 9c2c4237\n  * `[/users/${u}]` in a config-file creates the volume for all users like before\n  * `[/users/${u%+canwrite}]` only if the user is in the `canwrite` group\n  * `[/users/${u%-admins}]` only if the user is NOT in the `admins` group\n\n## 🩹 bugfixes\n\n* when moving a folder with symlinks, don't expand them into full files 5ab09769\n  * absolute symlinks are moved as-is; relative symlinks are rewritten so they still point to the same file when possible (if both source and destination are indexed in the db)\n  * the previous behavior was good for un-deduplicating files after changing the server-settings, but was too inconvenient for all other usecases\n* #146 fix downloading from shares when `-j0` enabled 8417098c\n* only show the download-as-zip link when the user is actually allowed to 14bb2999\n* the suggestions in the serverlog regarding how to fix incorrect X-Forwarded-For settings would be incorrect if the reverse-proxy used IPv6 to communicate with copyparty 16462ee5\n* set nofollow on `?doc` links so crawlers don't download binary files as text 6a2644fe\n\n## 🔧 other changes\n\n* #147 IdP: fix the warning about dangerous misconfigurations to be more accurate 29a17ae2\n* #143 print a warning on incorrect character-encoding in textfiles (config-files, logues, readmes etc.) 25974d66\n* copyparty.exe: update to jinja 3.1.6 (copyparty was *not affected* by the jinja-3.1.5 vuln)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0228-1846  `v1.16.16`  lemon melon cookie\n\n<img src=\"https://github.com/9001/copyparty/raw/hovudstraum/docs/logo.svg\" width=\"250\" align=\"right\"/>\n\nwebdev is [like a lemon](https://youtu.be/HPURbfKb7to) sometimes\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) ╱ [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) ╱ [client testbed](https://cd.ocv.me/b/)\n\nthere is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone` in case of future important updates, such as [vulnerabilities](https://github.com/9001/copyparty/security) (most recently 2025-02-25)\n\n## recent important news\n\n* [v1.16.15 (2025-02-25)](https://github.com/9001/copyparty/releases/tag/v1.16.15) fixed low-severity xss when uploading maliciously-named files\n* [v1.15.0 (2024-09-08)](https://github.com/9001/copyparty/releases/tag/v1.15.0) changed upload deduplication to be default-disabled\n* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details\n\n## 🧪 new features\n\n* #142 workaround android-chrome timestamp bug 5e12abbb\n  * all files were uploaded with last-modified year 1601 in specific recent versions of chrome\n  * https://issues.chromium.org/issues/393149335 has the actual fix; will be out soon\n\n## 🩹 bugfixes\n\n* add helptext for volflags `dk`, `dks`, `dky` 65a7706f\n* fix false-positive warning when disabling a global option per-volume by unsetting the volflag\n\n## 🔧 other changes\n\n* #140 nixos: @daimond113 fixed a warning in the nixpkg (thx!) e0fe2b97\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0225-0017  `v1.16.15`  fix low-severity vuln\n\n<img src=\"https://github.com/9001/copyparty/raw/hovudstraum/docs/logo.svg\" width=\"250\" align=\"right\"/>\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) ╱ [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) ╱ [client testbed](https://cd.ocv.me/b/)\n\n## ⚠️ this fixes a minor vulnerability; CVE-score `3.6`/`10`\n\n[GHSA-m2jw-cj8v-937r](https://github.com/9001/copyparty/security/advisories/GHSA-m2jw-cj8v-937r) aka [CVE-2025-27145](https://www.cve.org/CVERecord?id=CVE-2025-27145) could let an attacker run arbitrary javascript by tricking an authenticated user into uploading files with malicious filenames\n\n* ...but it required some clever social engineering, and is **not likely** to be a cause for concern... ah, better safe than sorry\n\nthere is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone` in case of future important updates, such as [vulnerabilities](https://github.com/9001/copyparty/security) (most recently 2025-02-25)\n\n## recent important news\n\n* [v1.15.0 (2024-09-08)](https://github.com/9001/copyparty/releases/tag/v1.15.0) changed upload deduplication to be default-disabled\n* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details\n\n## 🧪 new features\n\n* nothing this time\n\n## 🩹 bugfixes\n\n* fix [GHSA-m2jw-cj8v-937r](https://github.com/9001/copyparty/security/advisories/GHSA-m2jw-cj8v-937r) / [CVE-2025-27145](https://www.cve.org/CVERecord?id=CVE-2025-27145) in 438ea6cc\n  * when trying to upload an empty files by dragging it into the browser, the filename would be rendered as HTML, allowing javascript injection if the filename was malicious\n  * issue discovered and reported by @JayPatel48 (thx!)\n* related issues in errorhandling of uploads 499ae1c7 36866f1d\n  * these all had the same consequences as the GHSA above, but a network outage was necessary to trigger them\n    * which would probably have the lucky side-effect of blocking the javascript download, nice\n* paranoid fixing of probably-not-even-issues 3adbb2ff\n* fix some markdown / texteditor bugs 407531bc\n  * only indicate file-versions for markdown files in listings, since it's tricky to edit non-textfiles otherwise\n  * CTRL-C followed by CTRL-V and CTRL-Z in a single-line file would make a character fall off\n  * ensure safety of extensions\n\n## 🔧 other changes\n\n* readme:\n  * mention support for running the server on risc-v 6d102fc8\n  * mention that the [sony psp](https://github.com/user-attachments/assets/9d21f020-1110-4652-abeb-6fc09c533d4f) can browse and upload 598a29a7\n\n----\n\n# 💾 what to download?\n| download link | is it good? | description |\n| -- | -- | -- |\n| **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** | ✅ the best 👍 | runs anywhere! only needs python |\n| [a docker image](https://github.com/9001/copyparty/blob/hovudstraum/scripts/docker/README.md) | it's ok | good if you prefer docker 🐋 |\n| [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) |  ⚠️ [acceptable](https://github.com/9001/copyparty#copypartyexe) | for [win8](https://user-images.githubusercontent.com/241032/221445946-1e328e56-8c5b-44a9-8b9f-dee84d942535.png) or later; built-in thumbnailer |\n| [u2c.exe](https://github.com/9001/copyparty/releases/download/v1.16.14/u2c.exe) | ⚠️ acceptable | [CLI uploader](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) as a win7+ exe ([video](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm)) |\n| [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) | ⚠️ acceptable | similar to the regular sfx, [mostly worse](https://github.com/9001/copyparty#zipapp) |\n| [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) | ⛔️ [dangerous](https://github.com/9001/copyparty#copypartyexe) | for [win7](https://user-images.githubusercontent.com/241032/221445944-ae85d1f4-d351-4837-b130-82cab57d6cca.png) -- never expose to the internet! |\n| [cpp-winpe64.exe](https://github.com/9001/copyparty/releases/download/v1.16.5/copyparty-winpe64.exe) | ⛔️ dangerous | runs on [64bit WinPE](https://user-images.githubusercontent.com/241032/205454984-e6b550df-3c49-486d-9267-1614078dd0dd.png), otherwise useless |\n\n* except for [u2c.exe](https://github.com/9001/copyparty/releases/download/v1.16.14/u2c.exe), all of the options above are mostly equivalent\n* the zip and tar.gz files below are just source code\n* python packages are available at [PyPI](https://pypi.org/project/copyparty/#files)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0219-2309  `v1.16.14`  overwrite by upload\n\n## 🧪 new features\n\n* #139 overwrite existing files by uploading over them e9f78ea7\n  * default-disabled; a new togglebutton in the upload-UI configures it\n  * can optionally compare last-modified-time and only overwrite older files\n* [GDPR compliance](https://github.com/9001/copyparty#GDPR-compliance) (maybe/probably) 4be0d426\n\n## 🩹 bugfixes\n\n* some cosmetic volflag stuff, all harmless b190e676\n  * disabling a volflag `foo` with `-foo` shows a warning that `-foo` was not a recognized volflag, but it still does the right thing\n  * some volflags give the *\"unrecognized volflag, will ignore\"* warning, but not to worry, they still work just fine:\n    * `xz` to allow serverside xz-compression of uploaded files \n* the option to customize the loader-spinner would glitch out during the initial page load 7d7d5d6c\n\n## 🔧 other changes\n\n* [randpic.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/handlers/randpic.py), new 404-handler example, returns a random pic from a folder 60d5f271\n* readme: [howto permanent cloudflare tunnel](https://github.com/9001/copyparty#permanent-cloudflare-tunnel) for easy hosting from home 2beb2acc\n* [synology-dsm](https://github.com/9001/copyparty/blob/hovudstraum/docs/synology-dsm.md): mention how to update the docker image 56ce5919\n* spinner improvements 6858cb06\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0213-2057  `v1.16.13`  configure with confidence\n\n## 🧪 new features\n\n* make the config-parser more helpful regarding volflags a255db70\n  * if an unrecognized volflag is specified, print a warning instead of silently ignoring it\n  * understand volflag-names with Uppercase and/or kebab-case (dashes), and not just snake_case (underscores)\n  * improve `--help-flags` to mention and explain all available flags\n* #136 WebDAV: support COPY 62ee7f69\n  * also support overwrite of existing target files (default-enabled according to the spec)\n    * the user must have the delete-permission to actually replace files\n* option to specify custom icons for certain file extensions 7e4702cf\n  * see `--ext-th` mentioned briefly in the [thumbnails section](https://github.com/9001/copyparty/#thumbnails)\n* option to replace the loading-spinner animation 685f0869\n  * including how to [make it exceptionally normal-looking](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#boring-loader-spinner)\n\n## 🩹 bugfixes\n\n* #136 WebDAV fixes 62ee7f69\n  * COPY/MOVE/MKCOL: challenge clients to provide the password as necessary\n    * most clients only need this in PROPFIND, but KDE-Dolphin is more picky\n  * MOVE: support `webdav://` Destination prefix as used by Dolphin, probably others\n* #136 WebDAV: improve support for KDE-Dolphin as client 9d769027\n  * it masquerades as a graphical browser yet still expects 401, so special-case it with a useragent scan\n\n## 🔧 other changes\n\n* Docker-only: quick hacky fix for the [musl CVE](https://www.openwall.com/lists/musl/2025/02/13/1) until the official fix is out 4d6626b0\n  * the docker images will be rebuilt when `musl-1.2.5-r9.apk` is released, in 6~24h or so\n  * until then, there is no support for reading korean XML files when running in docker\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0209-2331  `v1.16.12`  RTT\n\n## 🧪 new features\n\n* show rtt (network latency to server, including request processing time) in the top status text d27f1104 \n  * and log the client-reported RTT to serverlog 20ddeb6e\n* remember file selection when changing folders c7db08ed\n  * good for when you accidentally navigate elsewhere\n* option to restrict download-as-zip/tar to admins-only c87af9e8\n* #135 add [bubbleparty](https://github.com/9001/copyparty/blob/hovudstraum/bin/README.md#bubblepartysh), thx @coderofsalvation! 3582a100\n  * runs copyparty in a [sandbox](https://github.com/containers/bubblewrap), making it harder to gain unintended access through bugs in python or copyparty\n  * better alternative to [prisonparty](https://github.com/9001/copyparty/tree/hovudstraum/bin#prisonpartysh), more similar to [the sandboxing in the nixos package](https://github.com/9001/copyparty/blob/7dda77dcb/contrib/nixos/modules/copyparty.nix#L232-L272)\n* new plugin: [quickmove](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/quickmove.js) 46f9e9ef\n  * adds hotkey `W` to quickly move selected files into a subfolder\n* #133 new plugin: [graft-thumbs.js](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/graft-thumbs.js) 6c202eff\n  * in folders with foobar.mp3 and foobar.png, can copy the thumbnail from the png to the jpg (and then hide the png)\n* handlers: add [http-redirect example](https://github.com/9001/copyparty/blob/hovudstraum/bin/handlers/redirect.py) 22cbd2db\n* add [ping.html](https://github.com/9001/copyparty/blob/hovudstraum/srv/ping.html) 7de9d15a 910797cc\n\n## 🩹 bugfixes\n\n* improve iPad detection so they get opus instead of mp3 12dcea4f\n\n## 🔧 other changes\n\n* safeguard against accidental config loss cd71b505\n  * while no copyparty servers have ended up in this unfortunate situation yet (afaik), be proactive and borrow some experience from other docker-based services\n* readme: improve config examples 32e90859\n* improve serverlog entries regarding 403s b020fd4a\n* #132 mention fuse permissions in readme d9d2a092\n* traefik-example: fix disconnect during big uploads 6a9ffe7e\n* try to show an appropriate warning for media that the browser doesn't support playing 4ef35263\n  * was an attempt at detecting iphones failing to play high-color-precision webm files, but safari doesn't seem to realize itself that playback has failed, ah well \n* copyparty.exe: update to python 3.12.9\n* update deps: dompurify 3.2.4\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0127-0140  `v1.16.11`  fix no-acode\n\n## 🧪 new features\n\n* u2c (commandline uploader): print download-links for uploaded files 1fe30363\n  * `-u` prints a list after all uploads finished\n  * `-ud` print during upload, after each file\n  * `-uf a.txt` writes them to `a.txt`\n\n## 🩹 bugfixes\n\n* [previous ver](https://github.com/9001/copyparty/releases/tag/v1.16.10) broke `--no-acode` (disable audio transcoding) by showing javascript errors 54a7256c\n  * reported on discord (thx)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0125-1809  `v1.16.10`  iOS9 is fine too\n\n## 🧪 new features\n\n* support audio playback on *really old* apple devices c9eba39e\n  * will now transcode to mp3 when necessary, since iOS didn't support opus-in-caf before iOS 11\n* support audio playback on *future* apple devices 28c9de3f 95390b65\n  * iOS 17.5 introduced support for opus-in-weba (like webp just audio instead) and, unlike caf, this intentionally supports vbr-opus (awesome)\n  * ...but the current code in iOS is too buggy, so this new format is default-disabled and we'll stick to caf for now fff38f48\n* ZeroMQ event-hooks can reject uploads 3a5c1d9f\n  * see [the example zmq listener](https://github.com/9001/copyparty/blob/1dace720/bin/zmq-recv.py#L26-L28)\n* chat with ZeroMQ event-hooks from javascript cdd3b67a\n  * replies from ZMQ REP servers are included in the msg-to-log responses\n  * which makes [this joke](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/usb-eject.py) possible f38c7543\n\n## 🩹 bugfixes\n\n* nope\n\n## 🔧 other changes\n\n* option to restrict the recent-uploads listing to admins-only b8b5214f\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0122-2326  `v1.16.9`  ZeroMQ says hello\n\n## 🧪 new features\n\n* event-hooks can send zeromq / zmq / 0mq messages; see [readme](https://github.com/9001/copyparty#zeromq) or `--help-hooks` for examples d9db1534\n* new volflags to specify the [allow-tag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy#iframes) of the markdown/logue sandbox, to allow fullscreen and such (see `--help-flags`) 6a0aaaf0\n* new volflag `nosparse` for possibly-better performance in very rare and specific scenarios 917380dd\n  * only enable this if you're uploading to s3 or something like that, and do plenty of benchmarking to make sure that it actually improved performance instead of making it worse\n\n## 🩹 bugfixes\n\n* restrict max-length of filekeys to 72 characters e0cac6fd\n* the hash-calculator mode of the commandline uploader produced incorrect whole-file hashes 4c04798a\n  * each chunk (`--chs`) was okay, but the final sum was not\n\n## 🔧 other changes\n\n* selftest the xml-parser on startup with malicious xml b2e8bf6e\n  * just in case a future python-version suddenly makes it unsafe somehow\n* disable some features if a dangerously misconfigured reverseproxy is detected 3f84b0a0\n* the download-as-zip feature now defaults to utf8 filenames 1231ce19\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2025-0111-1611  `v1.16.8`  android boost\n\n## 🧪 new features\n\n* 10x faster file hashing in android-chrome ec507889\n  * on a recent pixel, speed went from 13 to 139 MiB/s\n  * android's sandboxing makes small reads expensive, so do bigger reads instead\n    * so the browser-tab will use more RAM on android now, maybe around 200 MiB\n    * this only affects chrome-based browsers on android, not firefox\n* PUT/multipart uploads: request-header `Accept: json` makes it return json instead of html, just like `?j` ce0e5be4\n* add config examples for [ishare](https://isharemac.app/), a MacOS screenshot utility inspired by ShareX 0c0d6b2b\n  * also includes a bug-workaround for [ishare#107](https://github.com/castdrian/ishare/issues/107) - copyparty will now include a toplevel json property `fileurl` in the response if exactly one file was uploaded\n  * the [connect-page](https://a.ocv.me/?hc) generates an appropriate `copyparty.iscu` for ishare; [it looks like this](https://github.com/user-attachments/assets/820730ad-2319-4912-8eb2-733755a4cf54)\n\n## 🩹 bugfixes\n\n* fix a potential upload deadlock when...\n  * ...the database (`-e2d`) is **not** enabled for any volume, and...\n  * ...either the shares feature, or user-changeable passwords, is enabled 9e542cf8\n* when loading the partial-uploads registry on startup, a cosmetic desync could occur 467acb47\n\n## 🔧 other changes\n\n* remove some deprecated properties in partial-upload metadata aa2a8fa2\n  * v1.15.7 is now the oldest version which still has any chance of reading a modern up2k.snap\n* #129 added howto: [using webdav when copyparty is behind IdP](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients) -- thanks @wuast94 !\n* added howto: [install copyparty on a synology nas](https://github.com/9001/copyparty/blob/hovudstraum/docs/synology-dsm.md) 21f93042\n* more examples in the connect-page: 278258ee fb139697\n  * config-file for sharex on windows\n  * config-file for ishare on macos\n  * script for flameshot on linux\n* #75 add recommendation to use the [kamelåså project](https://github.com/steinuil/kameloso) instead of copyparty's [very-bad-idea.py](https://github.com/9001/copyparty/tree/hovudstraum/bin/mtag#dangerous-plugins) 9f84dc42\n* more reverse-proxy examples (haproxy, lighttpd, traefik, caddy) and improved nginx performance ac0a2da3\n  * readme has a [performance comparison](https://github.com/9001/copyparty?tab=readme-ov-file#reverse-proxy-performance) -- `haproxy > caddy > traefik > nginx > apache > lighttpd`\n* copyparty.exe: updated pillow 244e952f\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-1223-0005  `v1.16.7`  an idp fix for xmas\n\n# ☃️🎄 **there is still time** 🎅🎁\n\n❄️❄️❄️ please [enjoy some appropriate music](https://a.ocv.me/pub/demo/music/.bonus/#af-55d4554d) -- you'll probably like this more than the idp thing honestly ❄️❄️❄️\n\n## 🧪 new features\n\n* more improvements to the recent-uploads feature 87598dcd\n  * move html rendering to clientside\n    * any changes to the filter-text applies in real-time\n    * loads 50% faster, reduces server-load by 30%\n    * inhibits search engines from indexing it\n\n## 🩹 bugfixes\n\n* using idp without e2d could mess with uploads dd6e9ea7\n* u2c (commandline uploader): fix window title 946a8c5b\n* mDNS/SSDP: fix incorrect log colors when multiple primary IPs are lost 552897ab\n\n## 🔧 other changes\n\n* ui: make it more obvious that the volume-control is a volume-control 7f044372\n* copyparty.exe: update deps (jinja2, markupsafe, pyinstaller) c0dacbc4\n* improve safety of custom plugins 988a7223\n  * if you've made your own plugins which expect certain values (host-header, filekeys) to be html-safe, then you'll want to upgrade\n  * also fixes rss-feed xml if password contains special characters\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-1219-0037  `v1.16.6`  merry \\x58mas\n\n# ☃️🎄 **it is time** 🎅🎁\n\n❄️❄️❄️ please [enjoy some appropriate music](https://a.ocv.me/pub/demo/music/.bonus/#af-55d4554d) (trust me on this one, you won't regret it) ❄️❄️❄️\n\n## 🧪 new features\n\n* [list of recent uploads](https://a.ocv.me/?ru) eaa4b04a\n  * new button in the controlpanel; can be disabled with `--no-ups-page`\n  * only users with the dot-permission can see dotfiles\n  * only admins can see uploader-ip and upload-times\n    * enable `--ups-when` to let all users see upload-times\n* #125 log decoded request-URLs 73f7249c\n  * non-ascii filenames would make the accesslog a wall of `%E5%B9%BB%E6%83%B3%E9%83%B7` so print [the decoded URL](https://github.com/user-attachments/assets/9d411183-30f3-4cb2-a880-84cf18011183) in addition to the original one, which is left as-is for debugging purposes\n\n## 🩹 bugfixes\n\n* #126 improve dotfile handling 4c4e48ba\n  * was impossible to delete a folder which contained hidden files if the user did not have the permission to see hidden files\n  * would also affect moving, renaming, copying folders, in which case the dotfiles would not be carried over to the new location\n  * now, dotfiles are always deleted, and always moved/copied into a new destination, on the condition that this is safe -- if the user has the dotfile permission in the target loocation but not in the source location, the dotfiles will be left behind to avoid accidentally making then browsable\n* ux: cosmetic eta/idle-timer fixes 01a3eb29\n\n## 🔧 other changes\n\n* warn on ambiguous comments in config files da5ad2ab\n* avoid writing mojibake to the log 3051b131\n  * use `\\x`-encoding for unprintable text\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-1211-2236  `v1.16.5`  4chrome\n\n## 🧪 new features\n\n* #124 add workaround for a chrome bug (crash during upload) 24ce46b3\n  * chrome and chromium-based browsers could OOM\n  * https://issues.chromium.org/issues/383568268\n\n* #122 \"hybrid IdP\", regular users can still auth while [IdP](https://github.com/9001/copyparty#identity-providers) is enabled 64501fd7\n  * previously, enabling IdP would entirely disable password-based login\n  * now, password-auth is attempted for requests without a valid IdP header\n\n## 🩹 bugfixes\n\n* the terminal window title would only change if `--no-ansi` was specified, which is exactly the opposite of what it should be (and now is) doing db3c0b09\n\n## 🔧 other changes\n\n* mDNS: better log messages when several IPs are added/removed a49bf81f\n* webdeps: update dompurify 06868606\n\n----\n\nthis release includes a build of [copyparty-winpe64.exe](https://github.com/9001/copyparty/releases/download/v1.16.5/copyparty-winpe64.exe) since the last one was [almost a year ago](https://github.com/9001/copyparty/releases/tag/v1.10.1)\n\n* winpe64.exe is only for *very* specific usecases, you almost definitely *do not* want to download it, please just grab the regular [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) instead (works on all 64bit machines running win8 or newer)\n\n* the only difference between winpe64.exe and [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) is that winpe64.exe works in the win7x64 PE (rescue-env), which makes it *almost* entirely useless, and every bit as dangerous to use as copyparty32.exe\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-1207-0024  `v1.16.4`  ux is hard\n\n## 🧪 new features\n\n* improve the upload ui so it explains how to abort an unfinished upload when someone uploads to the wrong folder by accident be6afe2d\n  * also reduces serverload slightly when cloning an incoming file to multiple destinations\n* u2c (commandline uploader): windows improvements 91637800\n  * now supports globbing (filename wildcards) on windows\n  * progressbar in the windows taskbar (requires conemu or the \"new windows terminal\")\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-1204-0003  `v1.16.3`  120%\n\n## 🧪 new features\n\n* #120 add option `--srch-excl` and volflag `srch_excl` for excluding certain paths from search results 697a4fa8\n* mDNS: add workaround for https://github.com/avahi/avahi/issues/379 6c1cf68b 94d1924f\n  * Avahi mDNS Reflection, sometimes used in intricate LAN setups, doesn't understand NSEC records and corrupts them\n  * the workaround makes copyparty able to read the corrupted packets, but clients without a similar workaround will require either `--zm4` or `--zm6` so copyparty doesn't include the usual NSEC records\n    * this is mentioned in a very loud warning in the logs when necessary\n* mDNS: option to silently ignore buggy devices instead of spamming the log with parser errors 395af051\n* webdav: support listing unmapped root with infinite recursion (Depth:0) 21a3f369\n* embed current sort config into media URLs (gallery/music) 0f257c93 4cfdc4c5 01670827\n  * ensures that anyone clicking your link will see the files in the same order as you\n  * can be confgured serverside (`--hsortn`, volflag `hsortn`) and clientside (`#sort` in settings)\n* URL and UI options to disable checksum calculation of PUT, bup, basic uploads c5a000d2\n  * also allows [choosing either md5, sha1, sha256, or blake2](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#write) instead of the default sha512\n  * can give uploads a nice speed boost when copyparty is running on a potato\n\n## 🩹 bugfixes\n\n* webdav: more correct login challenge 2ce82339\n  * the previous behavior could make some clients reluctant to send the password\n* #120 forget metadata of all files (including uploads) when shadowed d168b2ac\n  * thanks to @Gremious for all the debugging to narrow this down!\n* #120 drop volume caches if relevant config is changed (mainly indexing filters) 2f83c6c7\n* #121 couldn't access arbitrary toplevel files from accounts with `h` permission 1f5f42f2\n\n## 🔧 other changes\n\n* exclude thumbnails from accesslog by default 9082c470\n* filesearch: show a final summary of time-elapsed and average hashing speed 8a631f04\n* improve phrasing of debug messages during indexing at startup 127f414e\n* `--license` no longer depends on opensource.org at build time 33c4ccff\n* update deps 6cedcfbf\n  * copyparty.exe: python 3.12.7 => 3.12.8\n  * webdeps: hashwasm, dompurify\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-1123-2336  `v1.16.2`  webdav upload fix\n\n## 🧪 new features\n\n* add `--nsort` and volflag `nsort` to default-enable natural sort of filenames with leading digits 8f7ffcf3\n* video-player: support `.mov` files which contain browser-native codecs 2d0cbdf1\n\n## 🩹 bugfixes\n\n* #119 v1.16.0 broke webdav uploads from rclone and possibly other clients 7dfbfc72\n  * a collection of webdav unittests will be added soon to prevent similar issues in the future\n* #118 ip-ranges can be mixed with `lan` when specifying the list of trusted proxies for `x-forwarded-for` with `--xff-src`\n  * found and fixed by @codemicro (thx!) 0e31cfa7\n* ux:\n  * in the grid-view, markdown files would open in the generic text viewer 520ac8f4\n  * qr-codes (create-share, view-share) didn't render on chrome db069c3d\n  * qr-codes could cause layout-shifting 5afb562a\n  * fix layout-shifting for ongoing downloads in controlpanel 9c8507a0\n  * cosmetic eta jank b10843d0\n\n## 🔧 other changes\n\n* upto 7% faster folder listings due to refactoring for more ux knobs 0c43b592 \n* fix resource leaks (only affected tests/debug) 2ab8924e\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-1115-2218  `v1.16.1`  cbz thumbnails\n\n## 🧪 new features\n\n* thumbnails of .cbz manga archives 4d15dd6e\n\n## 🩹 bugfixes\n\n* when running with `-j0`, download-ETA could break in complex volume layouts 10fc4768\n* linking to the image gallery didn't quite work if multiselect was enabled 56a04996\n* password-hashing parameters (cpu/ram cost) could not be customized 1f177528\n  * the defaults must be perfect considering nobody ever tried changing them ¯\\\\_(ツ)_/¯\n\n## 🔧 other changes\n\n* add intentional crash on startup if two volumes are configured to use the same histpath 2b63d7d1\n  * prevents funky deadlocks and an eventual database loss in case of a no-thoughts-head-empty moment, purely hypothetical of course 🗿\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-1110-1932  `v1.16.0`  COPYparty\n\n## 🧪 new features\n\n* #46 #115 copy/paste files and folders cacec9c1\n  * cut/paste still exists, but now you can copy too\n  * with a UI to rename files in case of filename collisions 56317b00\n  * files are created according to the dedup settings in the target volume (either full copies or symlinks/hardlinks)\n* show currently active downloads in the controlpanel 8aba5aed\n  * can be made admin-only with `--dl-list=1` or disabled with `--dl-list=0`\n  * hides filenames of hidden files, and files from volumes where the viewer doesn't have access\n* #114 async reinit on new [IdP users](https://github.com/9001/copyparty#identity-providers) 44ee07f0\n  * new IdP users can now always auth, even while a filesystem reindex is running\n* ux:\n  * remember batch-rename settings from last time 6a8d5e17\n  * URL parameters to force grid/thumbs on/off 5718caa9\n\n## 🩹 bugfixes\n\n* folders that fail to list due to a corrupt HDD/filesystem will now return a 404 instead of an empty listing 119e88d8\n  * also fixes similar issues in u2c and partyfuse\n* u2c (commandline uploader): detect and adapt to proxies with short connection keepalives c784e528\n* ui/ux:\n  * show the \"switch-to-https\" button in 404-messages too efd8a32e\n  * the folder-loading indicator could steal keyboard focus d9962f65\n  * hotkey-help was very trigger-happy 71d9e010\n\n## 🔧 other changes\n\n* choose more conservative defaults when server has less than 1 GiB RAM 2bf9055c\n  * runs okay down to 128 MiB, but thumbnails die below 256 MiB\n* update the [comparison to similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) after years of optimizations on both sides 0ce7cf5e\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-1027-0751  `v1.15.10`  temporary upload links\n\n## 🧪 new features\n\n* [shares](https://github.com/9001/copyparty#shares) can now be uploaded into, and unpost works too 4bdcbc1c\n  * useful to create temporary URLs for other people to upload to\n  * shares can be write-only, so visitors can't browse or see any files\n* #110 HTTP 304 (caching):\n  * support `If-Range` for HTTP 206 159f51b1\n  * add server-side and client-side options to force-disable cache dd6dbdd9\n    * `--no304=1` shows a button in the controlpanel to disable caching\n    * `--no304=2` makes that button auto-enabled\n    * even when `--no304` is not specified, accessing the URL `/?setck=no304=y` force-disables cache\n    * when cache is force-disabled, browsers will waste a lot of network traffic / data usage\n    * might help to avoid bugs in browsers or proxies, for example if media files suddenly stop loading\n      * but such bugs should be exceedingly rare, so do not enable this unless actually necessary \n\n## 🩹 bugfixes\n\n* #110 HTTP 304 (caching):\n  * remove `Content-Length` and `Content-Type` response headers from 304 replies 91240236\n    * browsers don't need these, and some middlewares might get confused if they're present\n* #113 fix crash on startup if `-j0` was combined with `--ipa` or `--ipu` 3a0d882c\n* #111 fix javascript crash if `--u2sz` was set to an invalid value b13899c6\n\n## 🔧 other changes\n\n* #110 HTTP 304 (caching):\n  * never automatically enable k304 because the `Vary` header killed support for caching in msie anyways 63013cc5\n  * change time comparison for `If-Modified-Since` to require an exact timestamp match, instead of the intended \"modified since\". This technically violates the http-spec, but should be safer for backdating file mtimes 159f51b1\n* new option `--ohead` to log response headers 7678a91b\n* added [nintendo 3ds](https://github.com/user-attachments/assets/88deab3d-6cad-4017-8841-2f041472b853) to the [list of supported browsers](https://github.com/9001/copyparty#browser-support) cb81f0ad\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-1018-2342  `v1.15.9`  rss server\n\n## 🧪 new features\n\n* #109 [rss feed generator](https://github.com/9001/copyparty#rss-feeds) 7ffd805a\n  * monitor folders recursively with RSS readers\n\n## 🩹 bugfixes\n\n* #107 `--df` diskspace limits was incompatible with webdav 2a570bb4\n* #108 up2k javascript crash (only affected the Chinese translation) a7e2a0c9\n\n## 🔧 other changes\n\n* up2k: detect buggy webworkers 5ca8f070\n* up2k: improve upload retry/timeout logic a9b4436c\n  * js: make handshake retries more aggressive\n  * u2c: reduce chunks timeout + ^\n  * main: reduce tcp timeout to 128sec (js is 42s)\n  * httpcli: less confusing log messages\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-1016-2153  `v1.15.8`  the sky is the limit\n\n## 🧪 new features\n\n* subchunks; avoid the Cloudflare filesize limit entirely fc8298c4 48147c07\n  * the previous max filesize was `383.9 GiB`, now only the sky is the limit\n  * if you're using another proxy with a more restrictive limit than Cloudflare's 100 MiB, for example 64 MiB, then `--u2sz 1,64,64`\n* m4v videos can be played in the gallery ff0a71f2\n\n## 🩹 bugfixes\n\n* up2k: uploading duplicate files could initially fail (but would succeed after a few automatic retries) due to a toctou 114b71b7\n* [u2c](https://github.com/9001/copyparty/blob/hovudstraum/bin/README.md#u2cpy) / commandline uploader:\n  * directory scanner got stuck if it found a FIFO cba1878b\n  * excessive number of FDs when uploading large files 65a2b6a2\n  * chunksize calculation; only affected files exactly 128 GiB large a2e037d6\n  * support filenames with newlines and invalid utf-8 b2770a20\n    * invalid utf-8 is replaced by `?` when they hit the server\n\n## 🔧 other changes\n\n* don't show the toast countdown bar if duration is infinite 22dfc6ec\n* chickenbit to disable the browser's built-in sha512 implementation and force the bundled wasm instead d715479e\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-1013-2244  `v1.15.7`  the 'a' in \"ip address\" stands for authentication\n\n## 🧪 new features\n\n* [cidr-based autologin](https://github.com/9001/copyparty#ip-auth) b7f9bf5a\n  * map a cidr ip-range to a username; anyone connecting from that ip-range will autologin as that user\n  * thx to @byteturtle for the idea!\n* [u2c](https://github.com/9001/copyparty/blob/hovudstraum/bin/README.md#u2cpy) / commandline uploader:\n  * option `--chs` to list individual chunk hashes cf1b7562\n  * fix progress indicator when resuming an upload 53ffd245\n* up2k: verbose logging of detected/corrected bitflips ee628363\n  * *foreshadowing intensifies* (story still developing)\n\n## 🩹 bugfixes\n\n* up2k with database disabled / running without `-e2d` 705f598b\n  * respect `noforget` when loading snaps\n  * ...but actually forget deleted files otherwise\n  * snap-loader adds empty need/hash entries as necessary\n\n## 🔧 other changes\n\n* authed users can now unpost recent uploads of unauthed users from the same IP 22b58e31\n  * would have become problematic now that cidr-based autologin is a thing\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-1011-2256  `v1.15.6`  preadme\n\n## 🧪 new features\n\n* #105 files named `preadme.md` appear at the top of directory listings 1d68acf8\n* entirely disable dedup with `--no-clone` / volflag `noclone` 3d7facd7 6b7ebdb7\n  * even if a file exists for sure on the server HDD, let the client continue uploading instead of reusing the existing data\n  * using this option \"never\" makes sense, unless you're using something like S3 Glacier storage where reading is really expensive but writing is cheap\n\n## 🩹 bugfixes\n\n* up2k jank after detecting a bitflip or network glitch 4a4ec88d\n  * instead of resuming the interrupted upload like it should, the upload client could get stuck or start over\n* #104 support viewing dotfile documents when dotfiles are hidden 9ccd8bb3\n* fix a buttload of typos 6adc778d 1e7697b5\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-1005-1803  `v1.15.5`  pyz all the cores\n\n## 🩹 bugfixes\n\n* the pkgres / pyz changes in 1.15.4 broke multiprocessing c3985537\n\n## 🔧 other changes\n\n* pyz: drop easymde to save some bytes + make it a tiny bit faster\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-1004-2319  `v1.15.4`  hermetic\n\n## 🧪 new features\n\n* [u2c](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy) (commandline uploader):\n  * remove all dependencies; now entirely self-contained 9daeed92\n    * made it 3x faster for small files, 2x faster in general\n  * improve `-x` behavior to not traverse into excluded folders b9c5c7bb\n* [partyfuse](https://github.com/9001/copyparty/tree/hovudstraum/bin#partyfusepy) (fuse client; mount a copyparty server as a local filesystem):\n  * 9x faster directory listings 03f0f994\n  * 4x faster downloads on high-latency connections 847a2bdc\n  * embed `fuse.py` (its only dependency) -- can be downloaded from the connect-page 44f2b63e\n  * support mounting nginx and iis servers too, not just copyparty c81e8984\n* reduce ram usage down to 10% when running without `-e2d` 88a1c5ca\n  * does not affect servers with `-e2d` enabled (was already optimal)\n* share folders as qr-codes e4542064\n  * when creating a share, you get a qr-code for quick access\n  * buttons in the shares controlpanel to reshow it, optionally with the password embedded into the qr-code\n* #98 read embedded webdeps and templates with `pkg_resources`; thx @shizmob! a462a644 d866841c\n  * [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) now runs straight from the source file without unpacking anything to disk\n    * ...and is now much slower at returning resource GETs, but that is fine\n* og / opengraph / discord embeds: support filekeys ae982006\n* add option for natural sorting; thx @oshiteku! 9804f25d\n* eyecandy timer bar on toasts 0dfe1d5b\n* smb-server: impacket 0.12 is out! dc4d0d8e\n  * now *possible* to list folders with more than 400 files (it's REALLY slow)\n\n## 🩹 bugfixes\n\n* webdav:\n  * support `<allprop/>` in propfind dc157fa2\n  * list volumes when root is unmapped 480ac254\n    * previously, clients couldn't connect to the root of a copyparty server unless a volume existed at `/`\n* #101 show `.prologue.html` and `.epilogue.html` in directory listings even if user cannot see hidden files 21be82ef\n* #100 confusing toast when pressing F2 without selecting anything 2715ee6c\n* fix prometheus metrics 678675a9\n\n## 🔧 other changes\n\n* #100 allow uploading `.prologue.html` and `.epilogue.html` 19a5985f\n* #102 make translation easier when running in docker\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0916-0107  `v1.15.3`  incoming eta\n\n## 🧪 new features\n\n![cpanel-upload-eta2-or8](https://github.com/user-attachments/assets/eb003bd4-da3c-4995-bf6e-3a8c1c1b26dd)\n\n* incoming uploads (and their ETA) are shown in the controlpanel 609c5921 844194ee\n* list total directory sizes 427597b6\n  * show the total size and number of files of each directory in listings\n  * makes browsing a bit slower (up to 30%) so can be disabled with `--no-dirsz`\n  * sizes are calculated during startup, so it requires `-e2dsa`\n    * file-uploads will recalculate the sizes immediately, but a full rescan is necessary to see changes caused by moves/deletes\n* optimizations;\n  * reduce broker overhead when multiprocessing is disabled 4e75534e\n    * should reduce cpu usage by uploads, thumbnails, prometheus metrics\n  * reduce cpu usage from downloading thumbnails 7d64879b\n\n## 🩹 bugfixes\n\n* fix sqlite indexes d67e9cc5\n  * upload handshakes would get exponentially slow if a volume has more than 200'000 files\n  * reindex on startup can be 150x faster in some rare cases (same filename in MANY folders)\n  * the database is now around 10% larger (likely worst-case)\n* misc ux: 58835b2b\n  * shares: show media tags\n  * html hydrator assumed a folder named `foo.txt` was a doc\n  * due to sessions, use `pwd` as password placeholder on services\n\n## 🔧 other changes\n\n* add [example](https://github.com/9001/copyparty/tree/hovudstraum/contrib#flameshotsh) for uploading screenshots from linux with flameshot 1c2acdc9\n* [nginx example](https://github.com/9001/copyparty/blob/hovudstraum/contrib/nginx/copyparty.conf): use unix-sockets for higher performance a5ce1032\n* #97 chinese translation was improved, thx again @ultwcz 7a573caf\n\n## 🗿 known issues \n\n* prometheus metrics are busted\n  * **workaround:** disable monitoring of volume status with `--nos-vst`\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0909-2343  `v1.15.1`  session\n\n<img src=\"https://github.com/9001/copyparty/raw/hovudstraum/docs/logo.svg\" width=\"250\" align=\"right\"/>\n\nblessed by ⑨, this release is [certified strong](https://github.com/user-attachments/assets/05459032-736c-4b9a-9ade-a0044461194a) ([artist](https://x.com/hcnone))\n\n## new features\n\n* login sessions b5405174\n  * a random session cookie is generated for each known user, replacing the previous plaintext login cookie\n  * the logout button will nuke the session on all clients where that user is logged in\n  * the sessions are stored in the database at `--ses-db`, default `~/.config/copyparty/sessions.db` (docker uses `/cfg/sessions.db` similar to the other runtime configs)\n    * if you run multiple copyparty instances, much like [shares](https://github.com/9001/copyparty#shares) and [user-changeable passwords](https://github.com/9001/copyparty#user-changeable-passwords) you'll want to keep a separate db for each instance\n  * can be mostly disabled with `--no-ses` when it turns out to be buggy\n\n## bugfixes\n\n* v1.13.8 broke the u2c `--ow` option to replace/overwrite files on the server during upload 6eee6015\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0908-1925  `v1.15.0`  fill the drives\n\n## recent important news\n\n* [v1.15.0 (2024-09-08)](https://github.com/9001/copyparty/releases/tag/v1.15.0) changed upload deduplication to be default-disabled\n* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details\n\n# upload deduplication now disabled by default\n\nbecause many people found the behavior surprising. This also makes it easier to use copyparty together with other software, since there is no risk of damage to symlinks if there are no symlinks to damage\n\nto enable deduplication, use either `--dedup` (old-default, symlink-based), or `--hardlink` (will use hardlinks when possible), or `--hardlink-only` (disallow symlinks). To choose the approach that fits your usecase, see [file deduplication](https://github.com/9001/copyparty#file-deduplication) in the readme\n\nverification of local file consistency was also added; this happens when someone uploads a dupe, to ensure that no other software has modified the local file since last reindex. This unfortunately makes uploading of duplicate files much slower, and can be disabled with `--safe-dedup 1` if you know that only copyparty will be modifying the filesystem\n\n## new features\n\n* dedup improvements:\n  * verify consistency of local files before using them as dedup source 6e671c52\n    * if a local file has been altered by other software since the last reindexing, then this will now be detected\n* u2c (commandline uploader): add mode to print hashes of local files 08848be7\n  * if you've lost a file but you know its `wark` (file identifier), you can now use u2c.exe to scan your whole filesystem for it: `u2c - .`\n* #96 use local timezone in log messages b599fbae\n\n## bugfixes\n\n* dedup fixes:\n  * symlinks could break if moved/renamed inside a volume where deduplication was disabled after some files within had already been deduplicated 4401de04\n  * when moving/renaming, only consider symlinks between volumes if `xlink` volflag is set b5ad9369\n* database consistency verifier (`-e2vp`):\n  * support filenames with newlines, and warn about missing files b0de84cb\n* opengraph/`--og`: fix viewing textfiles e5a836cb\n* up2k.js: fix confusing message when uploading many copies of the same file f1130db1\n\n## other changes\n\n* disable upload deduplication by default a2e0f986\n* up2k.js: increase handshake timeout to several minutes because of the dedup changes c5988a04\n* copyparty.exe: update to python 3.12.6\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0902-0108  `v1.14.4`  another\n\n## recent important news\n\n* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details\n\n## bugfixes\n\n* a network glitch could cause the uploader UI to panic d9e95262\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0830-2311  `v1.14.3`  important dedup fix\n\n<img src=\"https://github.com/9001/copyparty/raw/hovudstraum/docs/logo.svg\" width=\"250\" align=\"right\"/>\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) ╱ [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) ╱ [client testbed](https://cd.ocv.me/b/)\n\nthere is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone` in case of future important updates, such as [vulnerabilities](https://github.com/9001/copyparty/security) (most recently 2023-07-23)\n\n# important bugfix ☢️\n\nthis version fixes a file deduplication bug which was introduced in [v1.13.8](https://github.com/9001/copyparty/releases/tag/v1.13.8), released 2024-08-13\n\nits worst-case outcome is **loss of data** in the following scenario:\n* someone uploads a file into a folder where that filename is already taken, but the file contents are different, and the server already has a copy of that new file elsewhere under a different name\n\nspecific example:\n* the server has two existing files, `logo.png` and `logo-v2.png`, in the same volume but not necessarily in the same folder, and those files contain different data\n* you have a local copy of `logo-v2.png` on your laptop, but your local filename is `logo.png`\n* you upload your local `logo.png` onto the server, into the same folder as the server's `logo.png`\n* because the files contain different data, the server accidentally replaces the contents of `logo.png` with your version\n\nif you have been using the database feature (globally with `-e2dsa` or volflag `e2ds`), and you suspect you may have hit this bug, then it is a good idea to make a backup of the up2k databases for all your volumes (the files with names starting with `up2k.db`) before restarting copyparty and before you do anything else, especially if you do not have serverlogs from far back in time -- if you have either the databases and/or the serverlogs, then it is possible to identify replaced files with some manual work\n\nyou can check if you hit the bug using one of the following two approaches:\n* if your OS has the [gnu find](https://linux.die.net/man/1/find) command, do a search for empty files with `find -type f -size 0`\n* using copyparty (any OS), do the following steps:\n  * make sure that reindex-on-startup is enabled; either globally with `-e2dsa` or volflag `e2ds`\n  * then install this new copyparty version\n  * click the search tab `[🔎]` and type the number `0` into the `maximum MiB` textbox\n\nif you find any empty files with a filename that indicates it was autogenerated to avoid a name collision, for example `logo.png-1725040569.239207-kbt0xteO.png`, and the value of the number after `logo.png` is larger than `1723507200` (unixtime for 2024-08-13), then this indicates that `logo.png` may have been replaced by another upload\n\nif you have the serverlogs from when the original upload of `logo.png` was made, then this can be used to identify the original contents of the file that was replaced, and to look for other copies. Please get in touch on the discord for assistance if necessary\n\n----\n\n## new features\n\n* shares: add revival and expiration extension ad2371f8\n  * share-owners can revive expired shares for `--shr-rt` minutes (default 1 day)\n  * ...and extend expiration time by adding 1 minute or 1 hour to the timer\n* [sfx customizer](https://github.com/9001/copyparty/blob/hovudstraum/scripts/make-sfx.sh) improvements 03b13e8a\n  * improved translations stripper\n  * add more examples\n\n## bugfixes\n\n* the dedup bug 3da62ec2\n* tftp: support unmapped root 01233991\n\n## other changes\n\n* copyparty.exe: update to pyinstaller 6.10.0 \n* textviewer wordwrapping c4e2b0f9\n* add logo 7037e736 ee359742\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0823-2307  `v1.14.2`  bing chilling\n\n## new features\n\n* #94 @ultwcz translated the UI to Chinese (thx!)  92edea1d\n* #84 improvements to [shares](https://github.com/9001/copyparty#shares): 8122dded\n  * if one or more files are selected for sharing, they are placed into a virtual folder\n  * more appropriate password UI for accessing protected shares\n  * human-readable timestamps in shares listing\n* u2c (commandline uploader): support multiple exclusion patterns f356faa2\n\n## bugfixes\n\n* remove confusing logmessage when downloading a zerobyte file 9f034d9c\n* shares: 7ff46966\n  * fix crash if the root volume is unmapped\n  * log-spam on config reload\n  * password coalescing\n  * add chrome support\n\n## other changes\n\n* #93 add html IDs to the tabstrip 461f3158\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0819-0014  `v1.14.1`  one step forward\n\n[if i turn back now, then this will always follow... one step forward, forward](https://youtu.be/xe3Wkzc0O3k?t=27)\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) ╱ [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) ╱ [client testbed](https://cd.ocv.me/b/)\n\nthere is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone` in case of future important updates, such as [vulnerabilities](https://github.com/9001/copyparty/security) (most recently 2023-07-23)\n\n## new features\n\n* #92 users can change their own passwords 83fb569d 00da7440\n  * this feature is default-disabled; see [readme](https://github.com/9001/copyparty#user-changeable-passwords)\n* #84 share files/folders by creating a temporary url 7c2beba5\n  * inspired by other file servers; click the share-button to create a link like `example.com/share/enkz8g374o8g`\n    * primary usecase is to sneak past authentication services (see issue description)\n  * the create-share UI has options to accept uploads into the share, and/or set expiration time\n  * this feature is default-disabled; see [readme](https://github.com/9001/copyparty#shares)\n\n## bugfixes\n\n* #93 fixes for vproxy / location-based / not-vhost-based reverse-proxying 0b46b1a6\n  * using `--rp-loc` to reverse-proxy from a subfolder made some UI stuff break\n* listening on unix-sockets: 687df2fa\n  * fix `x-forwarded-for` support, and avoid a possible container-specific collision\n  * new syntax which allows setting unix-permissions and unix-group\n    * `-i unix:770:www:/tmp/party.sock` (see `--help-bind` for more examples)\n* using relocation hooks (introduced in previous ver) could cause dedup issues c8f4aeae b0af4b37\n* custom fonts using `@import` css statements 5a62cb48\n* invert volume scrollwheel 7d8d9438\n\n## other changes\n\n* changed the button colors in theme 2 (pm-monokai) from red to yellow 5153db6b\n  * the red buttons look better, but are too confusing because usually red means off\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0813-0008  `v1.13.8`  hook into place\n\n## new features\n\n* #86 intentional side-effects from hooks 6c94a63f\n  * use hooks (plugins) to conditionally move uploads into another folder depending on filename, extension, uploader ip/name, file contents, ...\n  * hooks can create additional files and tell copyparty to index them immediately, or delete an existing file based on some condition\n  * only one example so far though, [reloc-by-ext](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks#before-upload) which was a feature-request to dodge [sharex#3992](https://github.com/ShareX/ShareX/issues/3992)\n* listen on unix-sockets ee9aad82\n  * `-i unix:/tmp/party.sock` stops listening on TCP ports entirely, and only listens on that unix-socket\n  * can be combined with regular sockets, `-i 127.0.0.1,unix:/tmp/a.sock`\n  * kinda buggy for now (need to `--xff-src=any` and doesn't let you set socket-perms yet), will be fixed in next ver\n  * makes it 10% faster, but more importantly offers tighter access control behind reverse-proxies\n    * inspired by https://www.oligo.security/blog/0-0-0-0-day-exploiting-localhost-apis-from-the-browser\n* up2k stitching:\n  * more optimal stitch sizes for max throughput across connections c862ec1b\n  * improve fat32 compatibility 373194c3\n* new option `--js-other` to load custom javascript dbd42bc6\n  * `--js-browser` affects the filebrowser page, `--js-other` does all the others\n  * endless possibilities, such as [adding a login-banner](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/banner.js) which [looks like this](https://github.com/user-attachments/assets/8ae8e087-b209-449c-b08d-74e040f0284b)\n* list detected optional dependencies on startup 3db117d8\n  * hopefully reduces the guesswork / jank factor by a tiny bit\n\n## bugfixes\n\n* up2k stitching:\n  * put the request headers on a diet so they fit through more reverse-proxies 0da719f4\n* fix deadlock on s390x (IBM mainframes) 250c8c56\n\n## other changes\n\n* add flags to disengage [features](https://github.com/9001/copyparty/tree/hovudstraum#feature-chickenbits) and [dependencies](https://github.com/9001/copyparty/tree/hovudstraum#dependency-chickenbits) in case they cause trouble 72361c99\n* optimizations\n  * 6% faster on average d5c9c8eb\n  * docker: reduce ram usage 98ffaadf\n  * python2: reduce ram usage ebb19818\n* docker: add [portainer howto](https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/docker/portainer.md) e136231c\n* update deps ca001c85\n  * pyftpdlib 1.5.10\n  * copyparty.exe: python 3.12.5\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0729-2028  `v1.13.6`  not that big\n\n## new features\n\n* up2k.js: set clientside timeouts on http connections during upload 85e54980\n  * some reverse-proxy setups could cause uploads to hang indefinitely by eating requests; should recover nicely now\n* audio-player shows statustext while loading 662541c6\n* [bsod theme](https://github.com/9001/copyparty/tree/hovudstraum/contrib/themes) [(live demo)](https://cd.ocv.me/c/) 15ddcf53\n\n## bugfixes\n\n* fix bugs in the [long-distance upload optimizations](https://github.com/9001/copyparty/releases/tag/v1.13.5) in the previous version:\n  * up2k.js didn't necessarily use the expected chunksize when stitching 225bd80e\n  * u2c (commandline uploader): 8916bce3\n    * use the correct chunksize instead of overshooting like crazy\n    * could crash on exit if `-z` was enabled (so basically harmless)\n    * the \"time spent uploading\" statustext that was printed on exit could multiply by `-j` and exceed walltime\n* misc ux 9bb6e0dc\n  * don't accept hotkeys until it's safe to do so\n  * improve messages regarding the [firefox crash](https://bugzilla.mozilla.org/show_bug.cgi?id=1790500)\n  * keep more console logs in memory (easier to debug)\n  * fix wordwrap in messageboxes on firefox a19a0fa9\n\n## other changes\n\n* changed the `xm` / \"on message\" [hook examples](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks#on-message) to reject users without write-access 99edba4f\n* docker images were rebuilt on 2024-08-02, 23:30 UTC with new optimizations: 98ffaadf\n  * 😃 RAM usage decreased by `5-6 MiB` for most flavors; `10 MiB` for dj/iv\n  * 😕 image size grew by `4 MiB` (min), `6 MiB` (ac/im/iv), `9 MiB` (dj)\n  * 😃 startup time reduced to about half\n  * and avoids a deadlock on IBM mainframes\n* updated comparison to other software 6b54972e\n  * `hfs2` is dead, `hfs3` and `filebrowser` improved\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0722-2323  `v1.13.5`  american sized\n\n## new features\n\n* long-distance uploads are now **twice as fast** on average 132a8350\n  * boost tcp windowsize scaling by stitching together smaller chunks into bigger chonks so they fly better across the atlantic\n  * i'm not kidding, on the two routes we've tested this on we gained 1.6x / 160% (from US-West to Finland) and **2.6x / 260%** (Norway to US-East)\n    * files that are between 4 MiB and 256 MiB see the biggest improvement; 70% faster <= 768 MiB, 40% <= 1.5 GiB, 10% <= 6G\n  * if this turns out to be buggy, disable it serverside with `--u2sz 1,1,1` or clientside in the browser-ui: `[⚙️]` -> `up2k switches` -> change `64` to `1`\n* u2c.py (CLI uploader): support stitching (☝️) + print a summary with hashing and upload speeds  987bce21\n* video files can play as audio 53f1e3c9\n  * audio is extracted serverside to avoid wasting bandwidth\n  * extraction is lossy (converted to opus or mp3 depending on browser)\n  * togglebutton `🎧` in the gridview toolbar to enable/disable\n* new hook: [into-the-cache-it-goes.py](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks#after-upload) d26a944d\n  * avoids a cloudflare bug (race condition?) where it will send truncated files to visitors on the very first load if several people simultaneously access a file that hasn't been viewed before\n\n## bugfixes\n\n* inline markdown/logues rendered black-on-black in firefox 54 and some other browsers from 2017 and older eeef8091\n* unintuitive folder thumbnail selection if folder contains both `Cover.jpg` and `cover.jpg` f955d2bd\n* the gridview toolbar got undocked after viewing a pic/vid dc449bf8\n\n## other changes\n\n* #90 recommend rclone in favor of davfs2 ef0ecf87\n* improved some error messages e565ad5f\n* added helptext exporters to generate the online [html](https://ocv.me/copyparty/helptext.html) and [txt](https://ocv.me/copyparty/helptext.txt) editions 59533990\n* mention that cloudflare is incompatible with uploading files larger than 383.9 GiB \n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0716-0457  `v1.13.4`  descript.ion\n\n## new features\n\n* \"medialinks\"; instead of the usual hotlink, the basic-uploader (as used by sharex and such) can return a link that opens the file in the media viewer c9281f89\n  * enable for all uploads with volflag `medialinks`, or just for one upload by adding `?media` to the post url\n* thumbnails are now fully compatible with dirkeys/filekeys 52e06226\n* `--th-covers` will respect filename order, selecting the first matching filename as the folder thumbnail 1cdb1702\n* new hook: [bittorrent downloader](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks#on-message) bd3b3863 803e1565\n* hooks: d749683d\n  * can be restricted to only run when user has specific permissions\n  * user permissions are also included in the json message to the hook\n  * new syntax to prepend args to the hook's command\n  * (all this will be better documented after some additional upcoming hook-related features, see `--help-hooks` for now)\n* support `descript.ion` usenet metadata; will parse and render into directory listings when possible 927c3bce\n  * directory listings are now 2% slower, eh who's keeping count anyways\n* tftp-server: 45259251\n  * improved support for buggy clients\n  * improved ipv6 support, especially on macos\n  * improved robustness on unreliable networks\n* #85 new option `--gsel` to default-enable the client setting to select files by ctrl-clicking them in the grid 9a87ee2f\n* music player: set audio volume by scrollwheel 36d6d29a\n\n## bugfixes\n\n* race-the-beam (downloading an unfinished upload) could get interrupted near the end, requiring a manual resume in the browser's download manager to finish f37187a0\n* ftp-server: when accessing the root folder of servers without a root folder, it could mention inaccessible folders 84e8e1dd\n* ftp-server: uploads will automatically replace existing files if user has delete perms 0a9f4c60\n  * windows 2000 expects this behavior, otherwise it'll freak out and delete stuff and then not actually upload it, nice\n  * new option `--ftp-no-ow` restores old default behavior of rejecting upload if target filename exists\n* music player:\n  * stop trying to recover from a corrupted file if the user already fixed it manually 55a011b9\n  * support downloading the currently playing song regardless of current folder c06aa683\n* music player preloader: db6059e1\n  * stop searching after 5 folders of nothing\n  * don't crash playback by walking into error-pages\n* `--og` (rich discord embeds) was incompatible with viewing markdown docs d75a2c77\n* `--cgen` (configfile generator) much less jank d5de3f2f\n\n## other changes\n\n* mention that HTTP/2 is still usually slower than HTTP/1.1 dfe7f1d9\n* give up much sooner if a client is supposed to send a request body but isn't c549f367\n* support running copyparty as a server on windows 2000 and winXP 8c73e0cb 2fd12a83\n* updated deps 6e58514b\n  * copyparty.exe: python 3.12, pillow 10.4, pyinstaller 6.9\n  * dompurify 3.1.6\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0601-2324  `v1.13.3`  700+\n\n## new features\n\n* keep tags when transcoding music to opus/mp3 07ea629c\n  * useful for batch-downloading folders with [on-the-fly transcoding](https://github.com/9001/copyparty#zip-downloads)\n  * excessively large tags will be individually dropped (traktor beatmaps, cover-art, xmp)\n\n## bugfixes\n\n* optimization for large amounts (700+) of tcp connections / clients 07b2bf11\n  * `select()` was used for non-https downloads and mdns/ssdp initialization, which would start spinning at more than 1024 FDs, so now they `poll()` when possible (so not on windows)\n  * default max number of connections on windows was lowered to 486 since windows maxes out at 512 FDs\n* the markdown editor autoindent would duplicate `<hr>` 692175f5\n\n## other changes\n\n* #83: more intuitive behavior for `--df` and the `df` volflag 5ad65450\n* print helpful warning if OS restrictions make it impossible to persist config b629d18d\n* censor filesystem paths in the download-as-zip error summary 5919607a\n* `u2c.exe`: explain that https is disabled bef96176\n* ux: 60c96f99\n  * hide lightbox buttons when a video is playing\n  * move audio seekbar text down a bit so it hides less of the waveform and minute-markers\n* updated dompurify to 3.1.5 f00b9394\n* updated docker images to alpine 3.20\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0510-1431  `v1.13.2`  s3xmodit.zip\n\n## new features\n\n* play [compressed](https://a.ocv.me/pub/demo/music/chiptunes/compressed/#af-99f0c0e4) s3xmodit chiptunes/modules c0466279\n  * can now read gz/xz/zip-compressed s3m/xm/mod/it songs\n  * new filetypes supported: mdz, mdgz, mdxz, s3z, s3gz, s3xz, xmz, xmgz, xmxz, itz, itgz, itxz\n  * and if you need to fit even more tracks on the mixtape, [try mo3](https://a.ocv.me/pub/demo/music/chiptunes/compressed/#af-0bc9b877)\n* option to batch-convert audio waveforms 38e4fdfe\n* volflag to improve audio waveform compression with pngquant 82ce6862\n* option to add or change mappings from file-extensions to mimetypes 560d7b66\n* export and publish the `--help` text for online viewing 560d7b66\n  * now available [as html](https://ocv.me/copyparty/helptext.html) and as [plaintext](https://ocv.me/copyparty/helptext.txt), includes many features not documented in the readme\n* another way to add your own UI translations 19d156ff\n\n## bugfixes\n\n* ensure OS signals are immediately received and processed 87c60a1e\n  * things like reload and shutdown signals from systemd could get lost/stuck\n* fix mimetype detection for uppercase file extensions 565daee9\n* when clicking a `.ts` file in the gridview, don't open it as text 925c7f0a\n  * ...as it's probably an mpeg transport-stream, not a typescript file\n* be less aggressive in dropping volume caches e396c5c2\n  * very minor performance gain, only really relevant if you're doing something like burning a copyparty volume onto a CD\n  * previously, adding or removing any volume at all was enough to drop covers cache for all volumes; now this only happens if an intersecting volume is added/removed\n\n## other changes\n\n* updated dompurify to 3.1.2 566cbb65\n* opengraph: add the full filename as url suffix 5c1e2390\n  * so discord picks a good filename when saving an image\n\n----\n\n# 💾 what to download?\n| download link | is it good? | description |\n| -- | -- | -- |\n| **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** | ✅ the best 👍 | runs anywhere! only needs python |\n| [a docker image](https://github.com/9001/copyparty/blob/hovudstraum/scripts/docker/README.md) | it's ok | good if you prefer docker 🐋 |\n| [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) |  ⚠️ [acceptable](https://github.com/9001/copyparty#copypartyexe) | for [win8](https://user-images.githubusercontent.com/241032/221445946-1e328e56-8c5b-44a9-8b9f-dee84d942535.png) or later; built-in thumbnailer |\n| [u2c.exe](https://github.com/9001/copyparty/releases/download/v1.13.0/u2c.exe) | ⚠️ acceptable | [CLI uploader](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) as a win7+ exe ([video](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm)) |\n| [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) | ⚠️ acceptable | similar to the regular sfx, [mostly worse](https://github.com/9001/copyparty#zipapp) |\n| [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) | ⛔️ [dangerous](https://github.com/9001/copyparty#copypartyexe) | for [win7](https://user-images.githubusercontent.com/241032/221445944-ae85d1f4-d351-4837-b130-82cab57d6cca.png) -- never expose to the internet! |\n| [cpp-winpe64.exe](https://github.com/9001/copyparty/releases/download/v1.10.1/copyparty-winpe64.exe) | ⛔️ dangerous | runs on [64bit WinPE](https://user-images.githubusercontent.com/241032/205454984-e6b550df-3c49-486d-9267-1614078dd0dd.png), otherwise useless |\n\n* except for [u2c.exe](https://github.com/9001/copyparty/releases/download/v1.13.0/u2c.exe), all of the options above are mostly equivalent\n* the zip and tar.gz files below are just source code\n* python packages are available at [PyPI](https://pypi.org/project/copyparty/#files)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0506-0029  `v1.13.1`  ctrl-v\n\n## new features\n\n* upload files by `ctrl-c` from OS and `ctrl-v` into browser c5f7cfc3\n  * from just about any file manager (windows explorer, thunar on linux, etc.) into the copyparty web-ui\n  * only files, not folders, so drag-drop is still the recommended way\n* empty folders show an \"empty folder\" banner fdda567f\n* opengraph / discord embeds ea270ab9 36f2c446 48a6789d b15a4ef7\n  * embeds  [audio with covers](https://cd.ocv.me/c/d2/d22/snowy.mp3) , [images](https://cd.ocv.me/c/d2/d22/cover.jpg) , [videos](https://cd.ocv.me/c/d2/d21/no-effect.webm) , [audio without coverart](https://cd.ocv.me/c/d2/bitconnect.mp3) (links to one of the copyparty demoservers where the feature is enabled; link those in discord to test)\n  * images are currently not rendering correctly once clicked on android-discord (works on ios and in browser)\n  * default-disabled because opengraph disables hotlinking by design\n    * enable with `--og` and [see readme](https://github.com/9001/copyparty#opengraph) and [the --help](https://github.com/9001/copyparty/assets/241032/2dabf21e-2470-4e20-8ef0-3821b24be1b6)\n* add option to support base64-encoded url queries parceled into the url location 69517e46\n  * because android-specific discord bugs prevent the use of queries in opengraph tags\n* improve server performance when downloading unfinished uploads, especially on slow storage 70a3cf36\n* add dynamic content into `<head>` using `--html-head` which now takes files and/or jinja templates as input b6cf2d30\n* `--au-vol` (default 50, same as before) sets default audio volume in percent da091aec\n* add **[copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** buildscript 27485a4c\n* support ie4 and the [version of winzip](https://a.ocv.me/pub/g/nerd-stuff/cpp/win311zip.png) you'd find on an average windows 3.11 pc 603d0ed7\n\n## bugfixes\n\n* when logging in from the 403 page, remember and apply the original url hash f8491970\n* the config-reset button in the control-panel didn't clear the dotfiles preference bc2c1e42\n* the search feature could discover and use stale indexes in volumes where indexing was since disabled 95d9e693\n* when in doubt, periodically recheck if filesystems support sparse files f6e693f0\n  * reduces opportunities for confusion on servers with removable media (usb flashdrives)\n\n----\n\nthis release introduces **[copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz)**, yet another way to bring copyparty where it's needed -- very limited and with many drawbacks (see [readme](https://github.com/9001/copyparty#zipapp)) but may work when the others don't\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0420-2232  `v1.13.0`  race the beam\n\n## new features\n\n* files can be downloaded before the upload has completed (\"almost like peer-to-peer\")\n  * watch the [release trailer](http://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm) 👌\n  * if the downloader catches up with the upload, the speed is gradually slowed down so it never runs ahead\n  * can be disabled with `--no-pipe`\n* option `--no-db-ip` disables storing the uploader IP in the database bf585078\n* u2c (cli uploader): option `--ow` to overwrite existing files on the server 439cb7f8\n\n## bugfixes\n\n* when running on windows, using the web-UI to abort an upload could fail 8c552f1a\n* rapidly PUT-uploading and then deleting files could crash the file hasher feecb3e0\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0412-2110  `v1.12.2`  ie11 fix\n\n## new features\n\n* new option `--bauth-last` for when you're hosting other [basic-auth](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) services on the same domain 7b94e4ed\n  * makes it possible to log into copyparty as intended, but it still sees the passwords from the other service until you do\n  * alternatively, the other new option `--no-bauth` entirely disables basic-auth support, but that also kills [the android app](https://github.com/9001/party-up)\n\n## bugfixes\n\n* internet explorer isn't working?! FIX IT!!! 9e5253ef\n* audio transcoding was buggy with filekeys enabled b8733653\n* on windows, theoretical chance that antivirus could interrupt renaming files, so preemptively guard against that c8e3ed3a\n\n## other changes\n\n* add a \"password\" placeholder on the login page since you might think it's asking for a username da26ec36\n* config buttons were jank on iOS b772a4f8\n* readme: [making your homeserver accessible from the internet](https://github.com/9001/copyparty#at-home)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0409-2334  `v1.12.1`  scrolling stuff\n\n## new features\n\n* while viewing pictures/videos, the scrollwheel can be used to view the prev/next file 844d16b9\n\n## bugfixes\n\n* #81 (scrolling suddenly getting disabled) properly fixed after @icxes found another way to reproduce it (thx) 4f0cad54\n* and fixed at least one javascript glitch introduced in v1.12.0 while adding dirkeys 989cc613\n  * directory tree sidebar could fail to render when popping browser history into the lightbox\n\n## other changes\n\n* music preloader is slightly less hyper f89de6b3\n* u2c.exe: updated TLS-certs and deps ab18893c\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0406-2011  `v1.12.0`  locksmith\n\n## new features\n\n* #64 dirkeys; option to auto-generate passwords for folders, so you can give someone a link to a specific folder inside a volume without sharing the rest of the volume 10bc2d92 32c912bb ef52e2c0 0ae12868\n  * enabled by volflag `dk` (exact folder only) and/or volflag `dks` (also subfolders); see [readme](https://github.com/9001/copyparty#dirkeys)\n* audio transcoding to mp3 if browser doesn't support opus a080759a\n  * recursively transcode and download a folder using `?tar&mp3`\n  * accidentally adds support for playing just about any audio format in ie11\n* audio equalizer also applies to videos 7744226b\n\n## bugfixes\n\n* #81 scrolling could break after viewing an image in the lightbox 9c42cbec\n* on phones, audio playback could stop if network is slow/unreliable 59f815ff b88cc7b5 59a53ba9\n  * fixes the issue on android, but ios/safari appears to be [impossible](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#music-playback-halting-on-phones) d94b5b3f\n\n## other changes\n\n* updated dompurify to 3.0.11\n* copyparty.exe: updated to python 3.11.9\n* support for building with pyoxidizer was removed 5ab54763\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0323-1724  `v1.11.2`  public idp volumes\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) ╱ [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) ╱ [client testbed](https://cd.ocv.me/b/)\n\nthere is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone` in case of future important updates, such as [vulnerabilities](https://github.com/9001/copyparty/security) (most recently 2023-07-23)\n\n## new features\n\n* global-option `--iobuf` to set a custom I/O buffersize 2b24c50e\n  * changes the default buffersize to 256 KiB everywhere (was a mix of 64 and 512)\n  * may improve performance of networked volumes (s3 etc.) if increased\n  * on gbit networks: download-as-tar is now up to 20% faster\n  * slightly faster FTP and TFTP too\n\n* global-option `--s-rd-sz` to set a custom read-size for sockets c6acd3a9\n  * changes the default from 32 to 256 KiB\n  * may improve performance of networked volumes (s3 etc.) if increased\n  * on 10gbit networks: uploading large files is now up to 17% faster\n\n* add url parameter `?replace` to overwrite any existing files with a multipart-post c6acd3a9\n\n## bugfixes\n\n* #79 idp volumes (introduced in [v1.11.0](https://github.com/9001/copyparty/releases/tag/v1.11.0)) would only accept permissions for the user that owned the volume; was impossible to grant read/write-access to other users d30ae845\n\n## other changes\n\n* mention the [lack of persistence for idp volumes](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#important-notes) in the IdP docs 2f20d29e\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0318-1709  `v1.11.1`  dont ban the pipes\n\nthe [previous release](https://github.com/9001/copyparty/releases/tag/v1.11.0) had all the fun new features... this one's just bugfixes\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) ╱ [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) ╱ [client testbed](https://cd.ocv.me/b/)\n\n### no vulnerabilities since 2023-07-23\n* there is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone` in case of future important updates\n* [v1.8.7](https://github.com/9001/copyparty/releases/tag/v1.8.7) (2023-07-23) - [CVE-2023-38501](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-38501) - reflected XSS\n* [v1.8.2](https://github.com/9001/copyparty/releases/tag/v1.8.2) (2023-07-14) - [CVE-2023-37474](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-37474) - path traversal (first CVE)\n\n## bugfixes\n\n* less aggressive rejection of requests from banned IPs 51d31588\n  * clients would get kicked before the header was parsed (which contains the xff header), meaning the server could become inaccessible to everyone if the reverse-proxy itself were to \"somehow\" get banned\n    * ...which can happen if a server behind cloudflare also accepts non-cloudflare connections, meaning the client IP would not be resolved, and it'll ban the LAN IP instead heh\n      * that part still happens, but now it won't affect legit clients through the intended route\n  * the old behavior can be restored with `--early-ban` to save some cycles, and/or avoid slowloris somewhat\n* the unpost feature could appear to be disabled on servers where no volume was mapped to `/` 0287c7ba\n* python 3.12 support for [compiling the dependencies](https://github.com/9001/copyparty/tree/hovudstraum/bin/mtag#dependencies) necessary to detect bpm/key in audio files 32553e45\n\n## other changes\n\n* mention [real-ip configuration](https://github.com/9001/copyparty?tab=readme-ov-file#real-ip) in the readme ee80cdb9\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0315-2047  `v1.11.0`  You Can (Not) Proceed\n\nthis release was made possible by [stoltzekleiven, kvikklunsj, and tako](https://a.ocv.me/pub/g/nerd-stuff/2024-0310-stoltzekleiven.jpg)\n\n## new features\n\n* #62 support for [identity providers](https://github.com/9001/copyparty#identity-providers) and automatically creating volumes for each user/group (\"home folders\")\n  * login with passkeys / fido2 / webauthn / yubikey / ldap / active directory / oauth / many other single-sign-on contraptions\n  * [documentation](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md) and [examples](https://github.com/9001/copyparty/tree/hovudstraum/docs/examples/docker/idp-authelia-traefik) could still use some help (I did my best)\n* #77 UI to cancel unfinished uploads (available in the 🧯 unpost tab) 3f05b665\n  * the user's IP and username must match the upload by default; can be changed with global-option / volflag `u2abort`\n* new volflag `sparse` to pretend sparse files are supported even if the filesystem doesn't 8785d2f9\n  * gives drastically better performance when writing to s3 buckets through juicefs/geesefs\n  * only for when you know the filesystem can deal with it (so juicefs/geesefs is OK, but **definitely not** fat32)\n* `--xff-src` and `--ipa` now support CIDR notation (but the old syntax still works) b377791b\n* ux:\n  * #74 option to use [custom fonts](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice) 263adec7 6cc7101d 8016e671\n  * option to disable autoplay when page url contains a song hash 8413ed6d\n    * good if you're using copyparty to listen to music at the office and the office policy is to have the webbrowser automatically restart to install updates, meaning your coworkers are suddenly and involuntarily enjoying some loud af jcore while you're asleep at home\n\n## bugfixes\n\n* don't panic if cloudflare (or another reverse-proxy) decides to hijack json responses and replace them with html 7741870d\n* #73 the fancy markdown editor was incompatible with caddy (a reverse-proxy) ac96fd9c\n* media player could get confused if neighboring folders had songs with the same filenames 206af8f1\n* benign race condition in the config reloader (could only be triggered by admins and/or SIGUSR1) 096de508\n* running tftp with optimizations enabled would cause issues for `--ipa` b377791b\n* cosmetic tftp bugs 115020ba\n* ux:\n  * up2k rendering glitch if the last couple uploads were dupes 547a4863\n  * up2k rendering glitch when switching between readonly/writeonly folders 51a83b04\n  * markdown editor preview was glitchy on tiny screens e5582605\n\n## other changes\n\n* add a [sharex v12.1](https://github.com/9001/copyparty/tree/hovudstraum/contrib#sharexsxcu) config example 2527e903\n* make it easier to discover/diagnose issues with docker and/or reverse-proxy config d744f3ff\n* stop recommending the use of `--xff-src=any` in the log messages 7f08f10c\n* ux:\n  * remove the `k304` togglebutton in the controlpanel by default 1c011ff0\n  * mention that a full restart is required for `[global]` config changes to take effect 0c039219\n* docs e78af022\n  * [how to use copyparty with amazon aws s3](https://github.com/9001/copyparty#using-the-cloud-as-storage)\n  * faq: http/https confusion caused by incorrectly configured cloudflare\n  * #76 docker: ftp-server howto\n* copyparty.exe: updated pyinstaller to 6.5.0 bdbcbbb0\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0221-2132  `v1.10.2`  tall thumbs\n\n## new features\n\n* thumbnails can be way taller when centercrop is disabled in the browser UI 5026b212\n  * good for folders with lots of portrait pics (no more letterboxing)\n* more thumbnail stuff:\n  * zoom levels are twice as granular 5026b212\n  * write-only folders get an \"upload-only\" icon 89c6c2e0\n  * inaccessible files/folders get a 403/404 icon 8a38101e\n\n## bugfixes\n\n* tftp fixes d07859e8\n  * server could crash if a nic disappeared / got restarted mid-transfer\n  * tiny resource leak if dualstack causes ipv4 bind to fail\n* thumbnails:\n  * when behind a caching proxy (cloudflare), icons in folders would be a random mix of png and svg 43ee6b9f\n  * produce valid folder icons when thumbnails are disabled 14af136f\n* trailing newline in html responses d39a99c9\n\n## other changes\n\n* webdeps: update dompurify 13e77777\n* copyparty.exe: update jinja2, markupsafe, pyinstaller, upx 13e77777\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0218-1554  `v1.10.1`  big thumbs\n\n## new features\n\n* button to enable hi-res thumbnails 33f41f3e 58ae38c6\n  * enable with the `3x` button in the gridview\n  * can be force-enabled/disabled serverside with `--th-x3` or volflag `th3x`\n* tftp: IPv6 support and UTF-8 filenames + optimizations 0504b010\n* ux:\n  * when closing the image viewer, scroll to the last viewed pic bbc37990\n  * respect `prefers-reduced-motion` some more places fbfdd833\n\n## bugfixes\n\n* #72 impossible to delete recently uploaded zerobyte files if database was disabled 6bd087dd\n* tftp now works in `copyparty.exe`, `copyparty32.exe`, `copyparty-winpe64.exe`\n* the [sharex config example](https://github.com/9001/copyparty/tree/hovudstraum/contrib#sharexsxcu) was still using cookie-auth 8ff7094e\n* ux:\n  * prevent scrolling while a pic is open 7f1c9926\n  * fix gridview in older firefox versions 7f1c9926\n\n## other changes\n\n* thumbnail center-cropping can be force-enabled/disabled serverside with `--th-crop` or volflag `crop`\n  * replaces `--th-no-crop` which is now deprecated (but will continue to work)\n\n----\n\nthis release contains a build of `copyparty-winpe64.exe` which is almost **entirely useless,** except for in *extremely specific scenarios*, namely the kind where a TFTP server could also be useful -- the [previous build](https://github.com/9001/copyparty/releases/download/v1.8.7/copyparty-winpe64.exe) was from [version 1.8.7](https://github.com/9001/copyparty/releases/tag/v1.8.7) (2023-07-23)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0215-0000  `v1.10.0`  tftp\n\n## new features\n\n* TFTP server d636316a 8796c09f acbb8267 02879713\n  * based on [partftpy](https://github.com/9001/partftpy), has most essential features EXCEPT for [rfc7440](https://datatracker.ietf.org/doc/html/rfc7440) so WAN will be slow\n  * is already doing real work out in the wild! see the fantastic quote in the [readme](https://github.com/9001/copyparty?tab=readme-ov-file#tftp-server)\n* detect some (un)common configuration mistakes\n  * buggy reverse-proxy which strips away all URL parameters 136c0fdc\n    * could cause the browser to get stuck in a refresh-loop\n  * a volume on an sqlite-incompatible filesystem (a remote cifs server or such) and an up2k volume inside d4da3861\n    * sqlite could deadlock or randomly throw exceptions; serverlog will now explain how to fix it\n* ie11: file selection with shift-up/down 64ad5853\n\n## bugfixes\n\n* prevent music playback from stopping at the end of a folder f262aee8\n  * preloader will now proactively hunt for the next file to play as the last song is ending\n* in very specific scenarios, clients could be told their upload had finished processing a tiny bit too early, while the HDD was still busy taking in the last couple bytes 6f8a588c\n  * so if you expected to find the complete file on the server HDD immediately as the final chunk got confirmed, that was not necessarily the case if your server HDD was severely overloaded to the point where closing a file takes half a minute\n    * huge thx to friend with said overloaded server for finding all the crazy edge cases\n* ignore harmless javascript errors from easymde 879e83e2\n\n## other changes\n\n* the \"copy currently playing song info to clipboard\" button now excludes the uploader IP ed524d84\n* mention that enabling `-j0` can improve HDD load during uploads 5d92f4df\n* mention a debian-specific docker bug which prevents starting most containers (not just copyparty) 4e797a71\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0203-1533  `v1.9.31`  eject\n\n## new features\n\n* disable mkdir / new-doc buttons until a name is provided d3db6d29\n* warning about browsers limiting the number of connections c354a38b\n\n## bugfixes\n\n* #71 stop videos from buffering in the background a17c267d\n* improve up2k ETA on slow networks / many connections c1180d6f\n* u2c: exclude-filter didn't apply to file deletions b2e23340\n* `--touch` / `re📅` didn't apply to zerobyte files 945170e2\n\n## other changes\n\n* notes on [hardlink/symlink conversion](https://github.com/9001/copyparty/blob/6c2c6090/docs/notes.sh#L35-L46) 6c2c6090\n* [lore](https://github.com/9001/copyparty/blob/hovudstraum/docs/notes.md#trivia--lore) b1cf5884\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0125-2252  `v1.9.30`  retime\n\nprobably last release before v1.10 (IdP), please watch warmly\n\n## new features\n\n* option to replace serverside last-modified timestamps to match uploader's local files 55eb6921\n  * requires uploader to have write+delete permissions because it tampers with existing files\n  * in the browser-UI, enable with the `re📅` button in the settings tab `⚙️`\n  * u2c (commandline uploader): `--touch`\n* media player can shuffle songs now 01c82b54\n  * click `🔀` in the media-player settings tab `🎺` to enable\n* windows: retry deleting busy files 3313503e aa3a9719\n  * to support webdav-clients that upload and then immediately delete files (clonezilla)\n* options in batch-rename UI to ensure filenames are windows-safe b4e0a341\n* more support for older browsers 4ef31060\n  * ie9: gridview, navpane, text-viewer, text-editor\n  * ie9, firefox10: make sure toasts are properly closed\n\n## bugfixes\n\n* older chromes (and current iPhones) could randomly panic in incognito mode b32d6520\n* errormessage filepath sanitizer didn't catch histpaths in non-default locations 0f386c4b\n* now possible to mount the entire filesystem as a volume (please don't) 14bccbe4\n* on 32bit machines, disable sendfile when necessary to avoid python bug b9d0c853\n* `-q` would still print filesystem-indexing progress to STDOUT 6dbfcddc\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2024-0114-0629  `v1.9.29`  RAM friendly\n\n## new features\n\n* try to keep track of RAM usage in the thumbnailer 95a59996\n  * very inaccurate, just wild guessing really, but probably good enough:\n  * an attempt to stop FFmpeg from eating all the RAM when generating spectrograms\n  * `--th-ram-max` specifies how much RAM it's allowed to use (default 6 GB), crank it up if thumbnailing is too slow now\n* much faster startup on devices with slow filesystems and lots of files in the volume root (especially android phones) f1358dba\n* `uncache` button (in mediaplayer settings) a55e0d6e\n  * rotates all audio URLs, in case the browser has a cached copy of a broken mp3 or whatnot\n* now possible to POST files without having to set the `act: bput` multipart field 9bc09ce9\n  * mainly to support [igloo irc](https://github.com/9001/copyparty#client-examples) and other simplistic upload clients\n* try to point the linux oom-killer at FFmpeg so it doesn't kill innocent processes instead dc8e621d\n  * only works if copyparty has acces to /proc, so not in prisonparty, and maybe not in docker (todo)\n* UX:\n  * do another search immediately if a search-filter gets unchecked a4239a46\n  * several ie11 fixes (keyboard hotkeys and a working text editor) 2fd2c6b9\n\n## bugfixes\n\n* POSTing files could block for a really long time if the database is busy (filesystem reindexing), now it schedules the indexing for later instead e8a653ca\n* less confusing behavior when reindexing a file (keep uploader-ip/time if file contents turn out to be unmodified, and drop both otherwise) 226c7c30\n\n## other changes\n\n* better log messages when clients decide to disconnect in the middle of a POST 02430359\n* add a warning if copyparty is started with an account definition (`-a`) which isn't used in any volumes e01ba855\n* when running on macos, don't index apple metadata files (`.DS_Store` and such) d0eb014c\n  * they are still downloadable by anyone with read-access, and still appear in directory listings for users with access to see dotfiles\n* added a [log repacker](https://github.com/9001/copyparty/blob/hovudstraum/scripts/logpack.sh) to shrink/optimize old logs dee0950f\n* and a [contextlet](https://github.com/9001/copyparty/blob/hovudstraum/contrib/README.md#send-to-cppcontextletjson) example\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1231-1849  `v1.9.28`  eo2023\n\nwas hoping to finish the IdP stuff during 37c3 but that fell through, so here's all the other recent fixes instead -- happy newyears\n\n## new features\n\n* #66 new permission `.` to grant access to see dotfiles (hidden files) to specific users\n  * and new volflag `dots` to grant access to all users with `r`ead\n  * `-ed` still behaves like before (anyone with `r` can see dotfiles in all volumes)\n* #70 new permission `A` (alias of `rwmda.`) grants read/write/move/delete/admin/dotfiles\n* #67 folder thumbnails can be dotfiles (`.cover.jpg`, `.folder.png`) if the database is enabled (`-e2dsa`)\n* new option `--u2j` to specify default number of parallel file uploads in the up2k browser client\n  * default (2) is good on average; 16 can be good when most uploaders are overseas\n* curl gets plaintext 404/403 messages\n\n## bugfixes\n\n* cors-checking is disabled if the `PW` header is provided, just like the [readme](https://github.com/9001/copyparty#cors) always claimed\n* server would return `200 OK` while trying to return a file that is unreadable due to filesystem permissions\n* `--xdev` still doesn't work on windows, but at least now it doesn't entirely break filesystem indexing\n* fix tiny resource leak due to funky dualstack on macos\n\n## other changes\n\n* logfiles are padded to align messages when `-q` is specified, similar to current/previous behavior without `-q`\n* `--hdr-au-usr` was renamed to `--idp-h-usr` in preparation for other `--idp` things\n  * any mentions of `--hdr-au-usr` are translated to the new name on startup\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1208-2133  `v1.9.27`  another dedup bug\n\nso [v1.9.26](https://github.com/9001/copyparty/releases/tag/v1.9.26) fixed how moving a symlink could break other related symlinks, and then it turns out symlinks themselves could also die when moving them to another location, and somehow nobody encountered any of these until now... surely there are no more deduplication-related issues left at this point, yeah?\n\n## bugfixes\n\n* #65 moving deduplicated copies of files (symlinks) from one location to another could make them disappear (break the symlinks)\n\n  * don't worry, we are **not** talking about data loss! but see the [release notes for v1.9.26](https://github.com/9001/copyparty/releases/tag/v1.9.26) which explain how to deal with this issue (how to find, diagnose, and repair broken symlinks)\n\n----\n\n## regarding fedora packages\n\n[copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/) (fedora's build service) is not building at the moment; ***if you installed copyparty from copr-pypi,*** you can upgrade to this release by running one of the following:\n\n```bash\ndnf install https://ocv.me/copyparty/fedora/37/python3-copyparty.fc37.noarch.rpm\ndnf install https://ocv.me/copyparty/fedora/38/python3-copyparty.fc38.noarch.rpm\ndnf install https://ocv.me/copyparty/fedora/39/python3-copyparty.fc39.noarch.rpm\n```\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1208-0136  `v1.9.26`  dont break symlinks\n\n## new features\n* *tumbleweed*\n\n## bugfixes\n\n* deleting files from the server could make some duplicates of that file unavailable (by breaking nested symlinks)\n\n  * don't worry, we are **not** talking about data loss! but such broken links would disappear from the directory listing and would need to be remedied by replacing the broken links manually, either by using a file explorer or commandline\n\n  * **only** affected linux/macos, did **not** affect servers with `--hardlink` or `--never-symlink` or `--no-dedup`, and **mainly** affected servers with lots of duplicate files (with some dupes in the same folder and some elsewhere)\n\n  * if you want to check for such broken symlinks, the following unix command will find all of them: `find -L -type l`\n\n  * to repair a broken link, first remove it and then replace it: `rm thelink.opus; ln -s /mnt/music/realfile.opus thelink.opus`\n\n  * if you are left with a mystery file and want to know where its duplicates are, you can grep for the filename in the logs and you'll find something like the following line, where the `wark` is the file identifier; grep for that to find all the other copies of that file -- `purl` is the folder/URL which that copy of the file was uploaded to:\n    ```json\n    {\"name\": \"04. GHOST.opus\", \"purl\": \"/mu/vt/suisei/still-still-stellar/\", \"size\": 4520986, \"lmod\": 1697091772, \"sprs\": true, \"hash\": [], \"wark\": \"SJMASMtWOa0UZnc002nn5unO5iCBMa-krt2CDcq8eJe9\"}\n    ```\n\n* the server would throw an error if you tried to delete a broken symlink\n* prevent warnings about duplicate file entries in the database by preventing that from happening in the first place\n* `u2c.py` (commandline uploader) would fail to delete files from the server if there's more than ~10'000 files to be deleted\n  * and forgot to bump the version number... `1.11 (2nd season)`\n\n## other changes\n* `--help` was slightly improved\n* docker images are now based on alpine v3.19\n* `copyparty.exe` is now based on python v3.11.7\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1201-2326  `v1.9.25`  focus\n\n## new features\n* remember and restore scroll position when leaving the textfile viewer\n\n## bugfixes\n* the request-smuggling detetcor was too strict, blocking access to textfiles with newlines / control-codes in the filename\n* focus and text selection in messageboxes was still jank, mainly in firefox and especially phones\n\n## other changes\n* the banhammer now applies on attempts at request-smuggling and path traversals\n  * these were merely detected and rejected before, might as well bonk them\n* reject bad requests with a terse 500 instead of abruptly disconnecting in some cases\n  * stops firefox from rapidly spamming additional attempts\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1201-0210  `v1.9.24`  header auth\n\n## new features\n* initial work on #62 (support identity providers, oauth/SSO/...); see [readme](https://github.com/9001/copyparty#identity-providers)\n  * only authentication so far; no authorization yet, and users must exist in the copyparty config with bogus passwords\n* new option `--ipa` rejects connections from clients outside of a given allowlist of IP prefixes\n* environment variables can be used almost everywhere that takes a filesystem path; should make it way more comfy to write configs for docker / systemd\n* #59 added a basic [docker-compose yaml](https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/docker/basic-docker-compose) and an example config\n  * probably much room for improvement on everything docker still\n\n## bugfixes\n* the nftables-based port-forwarding in the [systemd example](https://github.com/9001/copyparty/tree/hovudstraum/contrib/systemd) was buggy; replaced with CAP_NET_BIND_SERVICE\n* palemoon-specific js crash if a text selection was dragged\n* text selection in messageboxes was jank\n\n## other changes\n* improved [systemd example](https://github.com/9001/copyparty/tree/hovudstraum/contrib/systemd) with hardening and a better example config\n* logfiles are flushed for every line written; can be disabled with `--no-logflush` for ~3% more performance best-case\n* iphones probably won't broadcast cover-art to car stereos over bluetooth anymore since the thingamajig in iOS that's in charge of that doesn't have cookie-access, and strapping in the auth is too funky so let's stop doing that b7723ac2\n  * can be remedied by enabling filekeys and granting unauthenticated people access that way, but that's too much effort for anyone to bother with I'm sure\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1125-1417  `v1.9.21`  in a bind\n\n## new features\n* #63 the grid-view will open textfiles in the textfile viewer\n* [prisonparty](https://github.com/9001/copyparty/blob/hovudstraum/bin/prisonparty.sh) now accepts user/group names (in addition to IDs)\n\n## bugfixes\n* the `Y` hotkey (which turns all links into download links) didn't affect the grid-view\n* on some servers with unusual filesystem layouts (especially ubuntu-zfs), [prisonparty](https://github.com/9001/copyparty/blob/hovudstraum/bin/prisonparty.sh) would make an unholy mess of recursive bind-mounts, quickly running out of inodes and requiring a server reboot\n  * added several safeguards to avoid anything like this in the future\n    * mutex around jail setup/teardown to prevent racing other instances\n    * verify jail status by inspecting /proc/mounts between each folder to bind\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1121-2325  `v1.9.20`  nice\n\n## new features\n* expensive subprocesses (ffmpeg, parsers, hooks) will run with `nice` to reduce cpu priority\n  * ...so listening to flacs won't grind everything else to a halt\n\n## bugfixes\n* the \"load more\" search results button didn't disappear if you hit the serverside limit\n* the \"show all\" button for huge folders didn't disappear when navigating into a smaller folder\n* trying to play the previous track when you're already playing the first track in a folder would send you on a wild adventure\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1119-1229  `v1.9.19`  shadow filter\n\n## bugfixes\n* #61 Mk.II: filter search results to also handle this issue in volumes where reindexing is disabled, or (spoiler warning:) a bug in the directory indexer prevents shadowed files from being forgotten\n* filekeys didn't always get included in the up2k UI for world-readable folders\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1118-2106  `v1.9.18`  cache invalidation\n\n## bugfixes\n* #61 search results could contain stale records from overlapping volumes:\n  * if volume `/foo` is indexed and then volume `/foo/bar` is later created, any files inside the `bar` subfolder would not become forgotten in `/foo`'s database until something in `/foo` changes, which could be never\n  * as a result, search results could show stale metadata from `/foo`'s database regarding files in `/foo/bar`\n  * fix this by dropping caches and reindexing if copyparty is started with a different list of volumes than last time\n* #60 client error when ctrl-clicking search results\n* icons for the close/more buttons in search results are now pillow-10.x compatible\n\n## other changes\n* `u2c.exe`: upgraded certifi to version `2023.11.17`\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1111-1738  `v1.9.17`  11-11\n\n## new features\n* `u2c.py` / `u2c.exe` (the commandline uploader):\n  * `-x` is now case-insensitive\n  * if a file fails to upload after 30 attempts, give up (bitflips)\n  * add 5 sec delay before reattempts (configurable with `--cd`)\n\n## bugfixes\n* clients could crash the file indexer by uploading and then instantly deleting files (as some webdav clients tend to do)\n* and fix some upload errorhandling which broke during a refactoring in v1.9.16\n\n## other changes\n* upgraded pyftpdlib to v1.5.9\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1104-2158  `v1.9.16`  windedup\n\n## breaking changes\n* two of the prometheus metrics have changed slightly; see the [breaking changes readme section](https://github.com/9001/copyparty#breaking-changes)\n  * (i'm not familiar with prometheus so i'm not sure if this is a big deal)\n\n## new features\n* #58 versioned docker images! no longer just `latest`\n* browser: the mkdir feature now accepts `foo/bar/qux` and `../foo` and `/bar`\n* add 14 more prometheus metrics; see [readme](https://github.com/9001/copyparty#prometheus) for details\n  * connections, requests, malicious requests, volume state, file hashing/analyzation queues\n* catch some more malicious requests in the autoban filters\n  * some malicious requests are now answered with HTTP 422, so that they count against `--ban-422`\n\n## bugfixes\n* windows: fix symlink-based upload deduplication\n  * MS decided to make symlinks relative to working-directory rather than destination-path...\n* `--stats` would produce invalid metrics if a volume was offline\n* minor improvements to password hashing ux:\n  * properly warn if `--ah-cli` or `--ah-gen` is used without `--ah-alg`\n  * support `^D` during `--ah-cli`\n* browser-ux / cosmetics:\n  * fix toast/tooltip colors on splashpage\n  * easier to do partial text selection inside links (search results, breadcrumbs, uploads)\n  * more rclone-related hints on the connect-page\n\n## other changes\n* malformed http headers from clients are no longer included in the client error-message\n  * just in case there are deployments with a reverse-proxy inserting interesting stuff on the way in\n  * the serverlog still contains all the necessary info to debug your own clients\n* updated [example nginx config](https://github.com/9001/copyparty/blob/hovudstraum/contrib/nginx/copyparty.conf) to recover faster from brief server outages\n  * the default value of `fail_timeout` (10sec) makes nginx cache the outage for longer than necessary\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1024-1643  `v1.9.15`  expand placeholder\n\n[made it just in time!](https://a.ocv.me/pub/g/nerd-stuff/PXL_20231024_170348367.jpg) (EDIT: nevermind, three of the containers didn't finish uploading to ghcr before takeoff ;_; all up now)\n\n## new features\n* #56 placeholder variables in markdown documents and prologue/epilogue html files\n  * default-disabled; must be enabled globally with `--exp` or per-volume with volflag `exp`\n  * `{{self.ip}}` becomes the client IP; see [/srv/expand/README.md](https://github.com/9001/copyparty/blob/hovudstraum/srv/expand/README.md) for more examples\n* dynamic-range-compressor: reduced volume jumps between songs when enabled\n\n## bugfixes\n* v1.9.14 broke the `scan` volflag, causing volume rescans to happen every 10sec if enabled\n  * its global counterpart `--re-maxage` was not affected\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1021-1443  `v1.9.14`  uptime\n\n## new features\n* search for files by upload time\n* option to display upload time in directory listings\n  * enable globally with `-e2d -mte +.up_at` or per-volume with volflags `e2d,mte=+.up_at`\n  * has a ~17% performance impact on directory listings\n* [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression) in the audioplayer settings\n* `--ban-404` is now default-enabled\n  * the turbo-uploader will now un-turbo when necessary to avoid banning itself\n  * this only affects accounts with permissions `g`, `G`, or `h`\n    * accounts with read-access (which are able to see directory listings anyways) and accounts with write-only access are no longer affected by `--ban-404` or `--ban-url`\n\n## bugfixes\n* #55 clients could hit the `--url-ban` filter when uploading over webdav\n  * fixed by limiting `--ban-404` and `--ban-url` to accounts with permission `g`, `G`, or `h`\n* fixed 20% performance drop in python 3.12 due to utcfromtimestamp deprecation\n  * but 3.12.0 is still 5% slower than 3.11.6 for some reason\n* volume listing on startup would display some redundant info\n\n## other changes\n* timeout for unfinished uploads increased from 6 to 24 hours\n  * and is now configurable with `--snap-drop`\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1015-2006  `v1.9.12`  more buttons\n\njust adding requested features, nothing important\n\n## new features\n* button `📅` in the uploader (default-enabled) sends your local last-modified timestamps to the server\n  * when deselected, the files on the server will have the upload time as their timestamps instead\n  * `--u2ts` specifies the default setting, `c` client-last-modified or `u` upload-time, or `fc` and `fu` to force\n* button `full` in the gridview decides if thumbnails should be center-cropped or not\n  * `--no-crop` and the `nocrop` volflag now sets the default value of this instead of forcing the setting\n  * thumbnail cleanup is now more granular, cleaning full-jpg separately from cropped-webp for example\n* set default sort order with `--sort` or volflag `sort`\n  * one or more comma-separated values; `tags/Cirle,tags/.tn,tags/Artist,tags/Title,href`\n    * see the column header tooltips in the browser to know what names (`id`) to use\n  * prefix a column name with `-` for descending sort\n  * specifying a sort order in the client will override all server-defined ones\n* when visiting a read-only folder, the upload-or-filesearch toggle will remember its previous state and restore it when leaving the folder\n  * much more intuitive, if anything about this UI can be called that...\n\n## bugfixes\n* iPhone: rare javascript panic when switching between safari and another app\n* ie9: file-rename ui was borked\n\n## other changes\n* copyparty.exe: upgrade to pillow 10.1 (which adds a new font for thumbnails in chrome)\n  * still based on python 3.11.6 because 3.12 is currently slower than 3.11\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1009-0036  `v1.9.11`  bustin'\n\nokay, i swear this is the last version for weeks! probably\n\n## bugfixes\n* cachebuster didn't apply to dynamically loaded javascript files\n  * READMEs could fail to render with `ReferenceError: DOMPurify is not defined` after upgrading from a copyparty older than v1.9.2\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1008-2051  `v1.9.10`  badpwd\n\n## new features\n* argument `--log-badpwd` specifies how to log invalid login attempts;\n  * `0` = just a warning with no further information\n  * `1` = log incorrect password in plaintext (default)\n  * `2` = log sha512 hash of the incorrect password\n  * `1` and `2` are convenient for stuff like setting up autoban triggers for common passwords using fail2ban or similar\n\n## bugfixes\n* none!\n  * the formerly mentioned caching-directives bug turned out to be unreachable... oh well, better safe than sorry\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1007-2229  `v1.9.9`  fix cross-volume dedup moves\n\n## bugfixes\n* v1.6.2 introduced a bug which, when moving files between volumes, could cause the move operation to abort when it encounters a deduplicated file\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-1006-1750  `v1.9.8`  static filekeys\n\n## new features\n* #52 add alternative filekey generator:\n  * volflag `fka` changes the calculation to ignore filesize and inode-number, only caring about the absolute-path on the filesystem and the `--fk-salt`\n  * good for linking to markdown files which might be edited, but reduces security a tiny bit\n* add warning on startup if `--fk-salt` is too weak (for example when it was upgraded from before [v1.7.6](https://github.com/9001/copyparty/releases/tag/v1.7.6))\n  * removed the filekey upgrade feaure to ensure a weak fk-salt is not selected; a new filekey will be generated from scratch on startup if necessary\n\n## other changes\n* pyftpdlib upgraded to 1.5.8\n* copyparty.exe built on python 3.11.6\n  * the exe in this release will be replaced with an 3.12.0 exe as soon as [pillow adds 3.12 support](https://github.com/python-pillow/Pillow/issues/6941)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0930-2332  `v1.9.7`  better column hider\n\n## new features\n* column hiding on phones is much more intuitive\n  * since you usually want to hide multiple columns, the hiding mode must now be manually disengaged\n  * click-handler now covers the entire header cell, preventing a misclick from accidentally sorting the table instead\n\n## bugfixes\n* #51 running copyparty with an invalid value for `--lang` made it crash with a confusing error message\n  * also makes it more compatible with other localStorage-using webservices running on the same domain\n\n## other changes\n* CVE-2023-5217, a vulnerability in libvpx, was fixed by alpine recently and no longer present in the docker images\n  * unlike the fix in v1.9.6, this is irrelevant since it was impossible to reach in all conceivable setups, but still nice\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0923-1215  `v1.9.6`  configurable x-forwarded-for\n\n## new features\n* rudimentary support for jython and graalpy, and directory tree sidebar in internet explorer 9 through 11, and firefox 10\n  * all older browsers (ie4, ie6, ie8, Netscape) get basic html instead\n* #35 adds a [hook](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/msg-log.py) which extends the message-to-serverlog feature so it writes the message to a textfile on the server\n  * could theoretically be extended into a [full instant-messaging feature](https://github.com/9001/copyparty/blob/hovudstraum/srv/chat.md) but that's silly, [nobody would do that](https://ocv.me/stuff/cchat.webm)\n    * [r0c is much better](https://github.com/9001/r0c) than this joke\n\n## bugfixes\n* 163e3fce the `x-forwarded-for` header was ignored if the nearest reverse-proxy is not asking from 127.0.0.1, which broke client IPs in containerized deployments\n  * the serverlog will now explain how to trust the reverse-proxy to provide client IPs, but basically,\n  * `--xff-hdr` specifies which header to read the client's real ip from\n  * `--xff-src` is an allowlist of IP-addresses to trust that header from\n* a62f744a if copyparty was started while an external HDD was not connected, and that volume's index was stored elsewhere, then the index would get wiped (since all the files are gone)\n* 3b8f66c0 javascript could crash while uploading from a very unreliable internet connection\n\n## other changes\n* copyparty.exe: updated pillow to 10.0.1 which fixes the webp cve\n* alpine, which the docker images are based on, turns out to be fairly slow -- currently working on a new docker image (probably fedora-based) which will be 30% faster at analyzing multimedia files and in general 20% faster on average\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0909-1336  `v1.9.5`  webhotell\n\n[happy 9/9!](https://safebooru.org/index.php?page=post&s=view&id=4027419)\n\n## new features\n* new permission `h` disables directory listing (so works like `g`) except it redirects to the folder's index.html instead of 404\n  * index.html is accessible by anyone with `h` even if filekeys are enabled\n  * well suited for running a shared-webhosting gig (thx kipu) especially now that the...\n* markdown editor can now be used on non-markdown files if account has `w`rite and `d`elete\n  * hotkey `e` to edit a textfile while it's open in the textfile viewer\n* SMB: account permissions now work fully as intended, thanks to impacket 0.11\n  * but enabling `--smb` is still strongly discouraged as it's a massive security hazard\n* download-as-zip can be 2.5x faster on tiny files, at least 15% faster in general\n* download folders as pax-format tarfiles with `?tar=pax` or `?tar=pax,xz:9`\n\n## bugfixes\n* 422-autoban accidentally triggered when uploading lots of duplicate files (thx hiem!)\n* `--css-browser` and `--js-browser` now accepts URLs with cache directives\n  * `--css-browser=/the.css?cache=600` (seconds) or `--js-browser=/.res/the.js?cache=i` (7 days)\n* SMB: avoid windows freaking out and disconnecting if it hits an offline volume\n* hotkey shift-r to rotate pictures counter-clockwise didn't do anything\n* hacker theme wasn't hacker enough (everything is monospace now)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0902-0018  `v1.9.4`  yes symlink times\n\nhello! it's been a while, an entire day even...\n\n## new features\n* download folder as tar.gz, tar.bz2, tar.xz\n  * single-threaded, so extremely slow, but nice for easily compressed data or challenged networks\n  * append `?tar=gz`, `?tar=bz2` or `?tar=xz` to a folder URL to do it\n  * default compression levels are gz:3, bz2:2, xz:1; override with `?tar=gz:9`\n\n# bugfixes\n* c1efd227 symlink-deduplicated files got indexed with the wrong last-modified timestamp\n  * mostly inconsequential; would cause the dupe's uploader-ip to be forgotten on the next server restart since it would reindex to \"fix\" the timestamp\n* when linking [a search query](https://a.ocv.me/pub/#q=tags%20like%20soundsho*) it loads the results faster\n\n# other changes\n* update readme to mention that iPhones and iPads dislike the preload feature and respond by glitching the audio a bit when a song is exactly 20 seconds away from ending and yet how it's probably a bad idea to disable preloading since i bet it's load-bearing against other iOS bugs\n  * speaking of iPhones and iPads, the [previous version](https://github.com/9001/copyparty/releases/tag/v1.9.3) should have fixed album playback on those\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0831-2211  `v1.9.3`  iOS and http fixes\n\n## new features\n* iPhones and iPads are now able to...\n  * 9986136d play entire albums while the screen is off without the music randomly stopping\n    * apple keeps breaking AudioContext in new and interesting ways; time to give up (no more equalizer)\n  * 1c0d9789 perform search queries and execude js code\n    * by translating [smart-quotes](https://stackoverflow.com/questions/48678359/ios-11-safari-html-disable-smart-punctuation) into regular `'` and `\"` characters\n* python 3.12 support\n  * technically a bugfix since it was added [a year ago](https://github.com/9001/copyparty/commit/32e22dfe) way before the first py3.12 alpha was released but turns out i botched it, oh well\n* filter error messages so they never include the filesystem path where copyparty's python files reside\n* print more context in server logs if someone hits an unexpected permission-denied\n\n# bugfixes\nfound some iffy stuff combing over the code but, as far as I can tell, luckily none of these were dangerous:\n* URL normalization was a bit funky, but it appears everything access-control-related was unaffected\n* some url parameters were double-decoded, causing the unpost filtering and file renaming to fail if the values contained `%`\n* clients could cause the server to return an invalid cache-control header, but newlines and control-characters got rejected correctly\n* minor cosmetics / qol fixes:\n  * reduced flickering on page load in chrome\n  * fixed some console spam in search results\n  * markdown documents now have the same line-height in directory listings and the editor\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0826-2116  `v1.9.2`  bigger hammer\n\n## new features\n* more ways to automatically ban users! three new sensors, all default-enabled, giving a 1 day ban after 9 hits in 2 minutes:\n  * `--ban-403`: trying to access volumes that dont exist or require authentication\n  * `--ban-422`: invalid POST messages (from brutefocing POST parameters and such)\n  * `--ban-url`: URLs which 404 and also match `--sus-urls` (scanners/crawlers)\n  * if you want to run a vulnerability scan on copyparty, please just [download the server](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) and do it locally! takes less than 30 seconds to set up, you get lower latency, and you won't be filling up the logfiles on the demo server with junk, thank you 🙏\n* more ban-related stuff,\n  * new global option `--nonsus-urls` specifies regex of URLs which are OK to 404 and shouldn't ban people\n  * `--turbo` now accepts the value `-1` which makes it impossible for clients to enable it, making `--ban-404` safe to use\n* range-selecting files in the list-view by shift-pgup/pgdn\n* volumes which are currently unavailable (dead nfs share, external HDD which is off, ...) are marked with a ❌ in the directory tree sidebar\n* the toggle-button to see dotfiles is now persisted as a cookie so it also applies on the initial page load\n* more effort is made to prevent `<script>`s inside markdown documents from running in the markdown editor and the fullpage viewer\n  * anyone who wanted to use markdown files for malicious stuff can still just upload an html file instead, so this doesn't make anything more secure, just less confusing\n  * the safest approach is still the `nohtml` volflag which disables markdown rendering outside sandboxes entirely, or only giving out write-access to trustworthy people\n  * enabling markdown plugins with `-emp` now has the side-effect of cancelling this band-aid too\n\n## bugfixes\n* textfile navigation hotkeys broke in the previous version\n\n## other changes\n* example [nginx config](https://github.com/9001/copyparty/blob/hovudstraum/contrib/nginx/copyparty.conf) was not compatible with cloudflare (suggest `$http_cf_connecting_ip` instead of `$proxy_add_x_forwarded_for`)\n* `copyparty.exe` is now built with python 3.11.5 which fixes [CVE-2023-40217](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-40217)\n  * `copyparty32.exe` is not, because python understandably ended win7 support \n* [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md):\n  * copyparty appears to be 30x faster than nextcloud and seafile at receiving uploads of many small files\n  * seafile has a size limit when zip-downloading folders\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0820-2338  `v1.9.1`  prometheable\n\n## new features\n* #49 prometheus / grafana / openmetrics integration ([see readme](https://github.com/9001/copyparty#prometheus))\n  * read metrics from http://127.0.0.1:3923/.cpr/metrics after enabling with `--stats`\n* download a folder with all music transcoded to opus by adding `?tar=opus` or `?zip&opus` to the URL\n  * can also be used to download thumbnails instead of full images; `?tar=w` for webp, `?tar=j` for jpg\n    * so i guess the long-time requested feature of pre-generating thumbnails kind of happened after all, if you schedule a `curl http://127.0.0.1:3923/?tar=w >/dev/null` after server startup\n* u2c (commandline uploader): argument `-x` to exclude files by regex (compares absolute filesystem paths)\n* `--zm-spam 30` can be used to improve zeroconf / mDNS reliability on crazy networks\n  * only necessary if there are clients with multiple IPs and some of the IPs are outside the subnets that copyparty are in -- not spec-compliant, not really recommended, but shouldn't cause any issues either\n  * and `--mc-hop` wasn't actually implemented until now\n* dragging an image from another browser window onto the upload button is now possible\n  * only works on chrome, and only on windows or linux (not macos)\n* server hostname is prefixed in all window titles\n  * can be adjusted with `--bname` (the file explorer) and `--doctitle` (all other documents)\n  * can be disabled with `--nth` (just window title) or `--nih` (title + header)\n\n## bugfixes\n* docker: the autogenerated seeds for filekeys and account passwords now get persisted to the config volume (thx noktuas)\n* uploading files with fancy filenames could fail if the copyparty server is running on android\n* improve workarounds for some apple/iphone/ios jank (thx noktuas and spiky)\n  * some ui elements had their font-size selected by fair dice roll\n  * the volume control does nothing because [apple disabled it](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), so add a warning\n  * the image gallery cannot be fullscreened [as apple intended](https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen#browser_compatibility) so add a warning\n\n## other changes\n* file table columns are now limited to browser window width\n* readme: mention that nginx-QUIC is currently very slow (thx noktuas)\n* #50 add a safeguard to the wget plugin in case wget at some point adds support for `file://` or similar\n* show a suggestion on startup to enable the database\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0725-1550  `v1.8.8`  just boring bugfixes\n\nfinal release until late august unless something bad happens and i end up building this thing on a shinkansen\n\n## recent security / vulnerability fixes\n* there is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone` in case of future important updates\n* [v1.8.7](https://github.com/9001/copyparty/releases/tag/v1.8.7) (2023-07-23) - [CVE-2023-38501](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-38501) - reflected XSS\n* [v1.8.2](https://github.com/9001/copyparty/releases/tag/v1.8.2) (2023-07-14) - [CVE-2023-37474](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-37474) - path traversal (first CVE)\n  * all serverlogs reviewed so far (5 public servers) showed no signs of exploitation\n\n## bugfixes\n* range-select with shiftclick:\n  * don't crash when entering another folder and shift-clicking some more\n  * remember selection origin when lazy-loading more stuff into the viewport\n* markdown editor:\n  * fix confusing warnings when the browser cache decides it *really* wants to cache\n  * and when a document starts with a newline\n* remember intended actions such as `?edit` on login prompts\n* Windows: TLS-cert generation (triggered by network changes) could occasionally fail\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0723-1543  `v1.8.7`  XSS for days\n\nat the lack of better ideas, there is now a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone` for all future important updates such as this one\n\n## bugfixes\n* reflected XSS through `/?k304` and `/?setck`\n  * if someone tricked you into clicking a URL containing a chain of `%0d` and `%0a` they could potentially have moved/deleted existing files on the server, or uploaded new files, using your account\n  * if you use a reverse proxy, you can check if you have been exploited like so:\n    * nginx: grep your logs for URLs containing `%0d%0a%0d%0a`, for example using the following command:\n      ```bash\n      (gzip -dc access.log*.gz; cat access.log) | sed -r 's/\" [0-9]+ .*//' | grep -iE '%0[da]%0[da]%0[da]%0[da]'\n      ```\n  * if you find any traces of exploitation (or just want to be on the safe side) it's recommended to change the passwords of your copyparty accounts\n  * huge thanks *again* to @TheHackyDog !\n* the original fix for CVE-2023-37474 broke the download links for u2c.py and partyfuse.py\n* fix mediaplayer spinlock if the server only has a single audio file\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0721-0036  `v1.8.6`  fix reflected XSS\n\n## bugfixes\n* reflected XSS through `/?hc` (the optional subfolder parameter to the [connect](https://a.ocv.me/?hc) page)\n  * if someone tricked you into clicking `http://127.0.0.1:3923/?hc=<script>alert(1)</script>` they could potentially have moved/deleted existing files on the server, or uploaded new files, using your account\n  * if you use a reverse proxy, you can check if you have been exploited like so:\n    * nginx: grep your logs for URLs containing `?hc=` with `<` somewhere in its value, for example using the following command:\n      ```bash\n      (gzip -dc access.log*.gz; cat access.log) | sed -r 's/\" [0-9]+ .*//' | grep -E '[?&](hc|pw)=.*[<>]'\n      ```\n  * if you find any traces of exploitation (or just want to be on the safe side) it's recommended to change the passwords of your copyparty accounts\n  * thanks again to @TheHackyDog !\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0718-0746  `v1.8.4`  range-select v2\n\n**IMPORTANT:** `v1.8.2` (previous release) fixed [CVE-2023-37474](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-37474) ; please see the [1.8.2 release notes](https://github.com/9001/copyparty/releases/tag/v1.8.2) (all serverlogs reviewed so far showed no signs of exploitation)\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) ╱ [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) ╱ [client testbed](https://cd.ocv.me/b/)\n\n## new features\n* #47 file selection by shift-clicking\n  * in list-view: click a table row to select it, then shift-click another to select all files in-between\n  * in grid-view: either enable the `multiselect` button (mainly for phones/tablets), or the new `sel` button in the `[⚙️] settings` tab (better for mouse+keyboard), then shift-click two files\n* volflag `fat32` avoids a bug in android's sdcardfs causing excessive reindexing on startup if any files were modified on the sdcard since last reboot\n\n## bugfixes\n* minor corrections to the new features from #45\n  * uploader IPs are now visible for `a`dmin accounts in `d2t` volumes as well\n\n## other changes\n* the admin-panel is only accessible for accounts which have the `a` (admin) permission-level in one or more volumes; so instead of giving your user `rwmd` access, you'll want `rwmda` instead:\n  ```bash\n  python3 copyparty-sfx.py -a joe:hunter2 -v /mnt/nas/pub:pub:rwmda,joe\n  ```\n  or in a settings file,\n  ```yaml\n  [/pub]\n    /mnt/nas/pub\n    accs:\n      rwmda: joe\n  ```\n  * until now, `rw` was enough, however most readwrite users don't need access to those features\n  * grabbing a stacktrace with `?stack` is permitted for both `rw` and `a`\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0714-1558  `v1.8.2`  URGENT: fix path traversal vulnerability\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) ╱ [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) ╱ [client testbed](https://cd.ocv.me/b/)\n\nStarting with the bad and important news; this release fixes https://github.com/9001/copyparty/security/advisories/GHSA-pxfv-7rr3-2qjg / [CVE-2023-37474](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-37474) -- so please upgrade!\n\nEvery version until now had a [path traversal vulnerability](https://owasp.org/www-community/attacks/Path_Traversal) which allowed read-access to any file on the server's filesystem. To summarize,\n* Every file that the copyparty process had the OS-level permissions to read, could be retrieved over HTTP without password authentication\n* However, an attacker would need to know the full (or copyparty-module-relative) path to the file; it was luckily impossible to list directory contents to discover files on the server\n* You may have been running copyparty with some mitigations against this:\n  * [prisonparty](https://github.com/9001/copyparty/tree/hovudstraum/bin#prisonpartysh) limited the scope of access to files which were intentionally given to copyparty for sharing; meaning all volumes, as well as the following read-only filesystem locations: `/bin`, `/lib`, `/lib32`, `/lib64`, `/sbin`, `/usr`, `/etc/alternatives`\n  * the [nix package](https://github.com/9001/copyparty#nix-package) has a similar mitigation implemented using systemd concepts\n  * [docker containers](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) would only expose the files which were intentionally mounted into the container, so even better\n* More conventional setups, such as just running the sfx (python or exe editions), would unfortunately expose all files readable by the current user\n* The following configurations would have made the impact much worse:\n  * running copyparty as root\n\nSo, three years, and finally a CVE -- which has been there since day one... Not great huh. There is a list of all the copyparty alternatives that I know of in the `similar software` link above.\n\nThanks for flying copyparty! And especially if you decide to continue doing so :-)\n\n## new features\n* #43 volflags to specify thumbnailer behavior per-volume;\n  * `--th-no-crop` / volflag `nocrop` to specify whether autocrop should be disabled\n  * `--th-size` / volflag `thsize` to set a custom thumbnail resolution\n  * `--th-convt` / volflag `convt` to specify conversion timeout\n* #45 resulted in a handful of opportunities to tighten security in intentionally-dangerous setups (public folders with anonymous uploads enabled):\n  * a new permission, `a` (in addition to the existing `rwmdgG`), to show the uploader-IP and upload-time for each file in the file listing\n    * accidentally incompatible with the `d2t` volflag (will be fixed in the next ver)\n  * volflag `nohtml` is a good defense against (un)intentional XSS; it returns HTML-files and markdown-files as plaintext instead of rendering them, meaning any malicious `<script>` won't run -- bad idea for regular use since it breaks fundamental functionality, but good when you really need it\n    * the README-previews below the file-listing still renders as usual, as this is fine thanks to the sandbox\n  * a new eventhook `--xban` to run a plugin when copyparty decides to ban someone (for password bruteforcing or excessive 404's), for example to blackhole the IP using fail2ban or similar\n\n## bugfixes\n* **fixes a path traversal vulnerability,** https://github.com/9001/copyparty/security/advisories/GHSA-pxfv-7rr3-2qjg / [CVE-2023-37474](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-37474)\n  * HUGE thanks to @TheHackyDog for reporting this !!\n  * if you use a reverse proxy, you can check if you have been exploited like so:\n    * nginx: grep your logs for URLs containing both `.cpr/` and `%2[^0]`, for example using the following command:\n      ```bash\n      (gzip -dc access.log.*.gz; cat access.log) | sed -r 's/\" [0-9]+ .*//' | grep -E 'cpr/.*%2[^0]' | grep -vF data:image/svg\n      ```\n* 77f1e514 fixes an extremely unlikely race-condition (see the commit for details)\n* 8f59afb1 fixes another race-condition which is a bit worse:\n  * the unpost feature could collide with other database activity, with the worst-case outcome being aborted batch operations, for example a directory move or a batch-rename which stops halfways\n\n----\n\n# 💾 what to download?\n| download link | is it good? | description |\n| -- | -- | -- |\n| **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** | ✅ the best 👍 | runs anywhere! only needs python |\n| [a docker image](https://github.com/9001/copyparty/blob/hovudstraum/scripts/docker/README.md) | it's ok | good if you prefer docker 🐋 |\n| [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) |  ⚠️ [acceptable](https://github.com/9001/copyparty#copypartyexe) | for [win8](https://user-images.githubusercontent.com/241032/221445946-1e328e56-8c5b-44a9-8b9f-dee84d942535.png) or later; built-in thumbnailer |\n| [u2c.exe](https://github.com/9001/copyparty/releases/download/v1.7.1/u2c.exe) | ⚠️ acceptable | [CLI uploader](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) as a win7+ exe ([video](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm)) |\n| [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) | ⛔️ [dangerous](https://github.com/9001/copyparty#copypartyexe) | for [win7](https://user-images.githubusercontent.com/241032/221445944-ae85d1f4-d351-4837-b130-82cab57d6cca.png) -- never expose to the internet! |\n| [cpp-winpe64.exe](https://github.com/9001/copyparty/releases/download/v1.8.2/copyparty-winpe64.exe) | ⛔️ dangerous | runs on [64bit WinPE](https://user-images.githubusercontent.com/241032/205454984-e6b550df-3c49-486d-9267-1614078dd0dd.png), otherwise useless |\n\n* except for [u2c.exe](https://github.com/9001/copyparty/releases/download/v1.7.1/u2c.exe), all of the options above are equivalent\n* the zip and tar.gz files below are just source code\n* python packages are available at [PyPI](https://pypi.org/project/copyparty/#files)\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0707-2220  `v1.8.1`  in case of 404\n\n## new features\n* [handlers](https://github.com/9001/copyparty/tree/hovudstraum/bin/handlers); change the behavior of 404 / 403 with plugins\n  * makes it possible to use copyparty as a [caching proxy](https://github.com/9001/copyparty/blob/hovudstraum/bin/handlers/caching-proxy.py)\n* #42 add mpv + streamlink support to [very-bad-idea](https://github.com/9001/copyparty/tree/hovudstraum/bin/mtag#dangerous-plugins)\n* add support for Pillow 10\n  * also improved text rendering in icons\n* mention the [fedora package](https://github.com/9001/copyparty#fedora-package) in the readme\n\n## bugfixes\n* theme 6 (hacker) didn't show the state of some toggle-switches\n* windows: keep quickedit enabled when hashing passwords interactively\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0626-0005  `v1.8.0`  argon\n\nNews: if you use rclone as a copyparty webdav client, upgrading to [rclone v1.63](https://github.com/rclone/rclone/releases/tag/v1.63.0) (just released) will give you [a huge speed boost](https://github.com/rclone/rclone/pull/6897) for small files\n\n## new features\n* #39 hashed passwords\n  * instead of keeping plaintext account passwords in config files, you can now store hashed ones instead\n  * `--ah-alg` specifies algorithm; best to worst: `argon2`, `scrypt`, `sha2`, or the default `none`\n  * the default settings of each algorithm takes `0.4 sec` to hash a password, and argon2 eats `256 MiB` RAM\n    * can be adjusted with optional comma-separated args after the algorithm name; see `--help-pwhash`\n  * `--ah-salt` is the [static salt](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hashed-passwords) for all passwords, and is autogenerated-and-persisted if not specified\n  * `--ah-cli` switches copyparty into a shell where you can hash passwords interactively\n    * but copyparty will also autoconvert any unhashed passwords on startup and give you the values to insert into the config anyways\n* #40 volume size limit\n  * volflag `vmaxb` specifies max size of a volume\n  * volflag `vmaxn` specifies max number of files in a volume\n  * example: `-v [...]:c,vmaxb=900g:c,vmaxn=20k` blocks uploads if the volume reaches 900 GiB or a total of 20480 files\n  * good alternative to `--df` since it works per-volume\n\n## bugfixes\n* autogenerated TLS certs didn't include the mDNS name\n\n## other changes\n* improved cloudflare challenge detection\n* markdown edits will now trigger upload hooks\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0611-0814  `v1.7.6`  NO_COLOR\n\n## new features\n* #31 `--grid` shows thumbnails instead of file-list by default\n* #28 `--unlist` regex-exclude files from browser listings\n  * for example `--unlist '\\.(js|css)$'` hides all `.js` and `.css` files\n  * **purely cosmetic!** the files are still fully accessible, and still appear in API calls\n* auto-generate TLS certificates on startup / network-change\n  * mostly good for LAN, requires [cfssl](https://github.com/cloudflare/cfssl/releases/latest), can be disabled with `--no-crt`\n  * creates a self-signed CA and certs with SANs of all detected server IPs\n    * so it's still recommended to use a reverse-proxy / letsencrypt for WAN servers\n* the default `--fk-salt` is now much stronger\n  * all existing installations will keep the previously selected seed -- you can choose to upgrade by deleting `~/.config/copyparty/cert.pem` but this will change all filekeys / per-file passwords\n* the `NO_COLOR` environment-variable is now supported, removing colors from stdout\n  * see https://no-color.org/ and more importantly https://youtu.be/biW5UVGkPMA?t=150\n  * `--ansi` and `--no-ansi` can also be used to force-enable/disable colored output\n* #33 disable colors when stdout is redirected to a pipe/file -- by @clach04 \n* #32 simplify building sfx from source\n* upgraded [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) to [python 3.11.4](https://pythoninsider.blogspot.com/2023/06/python-3114-31012-3917-3817-3717-and.html)\n\n## bugfixes\n* #30 `--ftps` didn't work without `--ftp`\n* tiny css bug in light themes (opaque thumbnail controls)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0513-0000  `v1.7.2`  hard resolve\n\n## new features\n* print a warning if `c:\\`, `c:\\windows*`, or all of `/` are shared\n* upgraded the docker image to v3.18 which enables the [chiptune player](https://a.ocv.me/pub/demo/music/chiptunes/#af-f6fb2e5f)\n* in config files, allow trailing `:` in section headers\n\n## bugfixes\n* when `--hardlink` (or the volflag) is set, resolve symlinks before hardlinking\n  * uploads could fail due to relative symlinks\n* really minor ux fixes\n  * left-align `GET` in access logs\n  * the upload panel didn't always shrink back down after uploads completed\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0507-1834  `v1.7.1`  CräzY;PWDs\n\n## new features\n* webdav:\n  * support write-only folders\n  * option `--dav-auth` / volflag `davauth` forces clients to always auth\n    * helps clients such as `davfs2` see all folders if the root is anon-readable but some subfolders are not\n    * alternatively you could configure your client to always send the password in the `PW` header\n* include usernames in http request logs\n* audio player:\n  * consumes less power on phones when the screen is off\n  * smoother playback cursor on short songs\n\n## bugfixes\n* the characters `;` and `%` can now be used in passwords\n  * but non-ascii characters (such as the ä in the release title) can, in fact, not\n* verify that all accounts have unique passwords on startup (#25)\n\n## other changes\n* ftpd: log incorrect passwords only, not correct ones\n* `up2k.py` (the upload, folder-sync, and file-search client) has been renamed to [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy)\n  * `u2c` as in `up2k client`, or `up2k CLI`, or `upload-to-copyparty` -- good name\n  * now the only things named \"up2k\" are the web-ui and the server backend which is way less confusing\n* upgrade packaging from [setup.py](https://github.com/9001/copyparty/blob/hovudstraum/setup.py) to [pyproject.toml](https://github.com/9001/copyparty/blob/hovudstraum/pyproject.toml)\n  * no practical consequences aside from a warm fuzzy feeling of being in the future\n* the docker images ~~will be~~ got rebuilt 2023-05-11 ~~in a few days (when [alpine](https://alpinelinux.org/) 3.18 is released)~~ enabling [the chiptune player](https://a.ocv.me/pub/demo/music/chiptunes/#af-f6fb2e5f)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0429-2114  `v1.7.0`  unlinked\n\ndon't get excited! nothing new and revolutionary, but `xvol` and `xdev` changed behavior so there's an above-average chance of fresh bugs\n\n## new features\n* (#24): `xvol` and `xdev`, previously just hints to the filesystem indexer, now actively block access as well:\n  * `xvol` stops users following symlinks leaving the volumes they have access to\n    * so if you symlink `/home/ed/music` into `/srv/www/music` it'll get blocked\n    * ...unless both folders are accessible through volumes, and the user has read-access to both\n  * `xdev` stops users crossing the filesystem boundary of the volumes they have access to\n    * so if you symlink another HDD into a volume it'll get blocked, but you can still symlink from other places on the same FS\n  * enabling these will add a slight performance hit; the unlikely worst-case is `14%` slower directory listings, `35%` slower download-as-tar\n* file selection summary (num files, size, audio duration) in the bottom right\n* [u2cli](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py): more aggressive resolving with `--rh`\n* [add a warning](https://github.com/9001/copyparty#fix-unreliable-playback-on-android) that the default powersave settings in android may stop playing music during album changes\n  * also appears [in the media player](https://user-images.githubusercontent.com/241032/235327191-7aaefff9-5d41-4e42-b71f-042a8247f29d.png) if the issue is detected at runtime (playback halts for 30sec while screen is off)\n\n## bugfixes\n* (#23): stop autodeleting empty folders when moving or deleting files\n  * but files which expire / [self-destruct](https://github.com/9001/copyparty#self-destruct) still clean up parent directories like before\n* ftp-server: some clients could fail to `mkdir` at first attempt (and also complain during rmdir)\n\n## other changes\n* new version of [cpp-winpe64.exe](https://github.com/9001/copyparty/releases/download/v1.7.0/copyparty-winpe64.exe) since the ftp-server fix might be relevant\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0426-2300  `v1.6.15`  unexpected boost\n\n## new features\n* 30% faster folder listings due to [the very last thing](https://github.com/9001/copyparty/commit/55c74ad1) i'd ever expect to be a bottleneck, [thx perf](https://docs.python.org/3.12/howto/perf_profiling.html)\n* option to see the lastmod timestamps of symlinks instead of the target files\n  * makes the turbo mode of [u2cli, the commandline uploader and folder-sync tool](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) more turbo since copyparty dedupes uploads by symlinking to an existing copy and the symlink is stamped with the deduped file's lastmod\n  * **webdav:** enabled by default (because rclone will want this), can be disabled with arg `--dav-rt` or volflag `davrt`\n  * **http:** disabled by default, can be enabled per-request with urlparam `lt`\n* [u2cli](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py): option `--rh` to resolve server hostname only once at start of upload\n  * fantastic for buggy networks, but it'll break TLS\n\n## bugfixes\n* new arg `--s-tbody` specifies the network timeout before a dead connection gets dropped (default 3min)\n  * before there was no timeout at all, which could hang uploads or possibly consume all server resources\n  * ...but this is only relevant if your copyparty is directly exposed to the internet with no reverse proxy\n    * with nginx/caddy/etc you can disable the timeout with `--s-tbody 0` for a 3% performance boost (*wow!*)\n* iPhone audio transcoder could turn bad and stop transcoding\n* ~~maybe android phones no longer pause playback at the end of an album~~\n  * nope, that was due to [android's powersaver](https://github.com/9001/copyparty#fix-unreliable-playback-on-android), oh well\n  * ***bonus unintended feature:*** navigate into other folders while a song is plaing\n* [installing from the source tarball](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#build-from-release-tarball) should be ok now\n  * good base for making distro packages probably\n\n## other changes\n* since the network timeout fix is relevant for the single usecase that [cpp-winpe64.exe](https://github.com/9001/copyparty/releases/download/v1.6.15/copyparty-winpe64.exe) covers, there is now a new version of that\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0424-0609  `v1.6.14`  unsettable flags\n\n## new features\n* unset a volflag (override a global option) by negating it (setting volflag `-flagname`)\n* new argument `--cert` to specify TLS certificate location\n  * defaults to `~/.config/copyparty/cert.pem` like before\n\n## bugfixes\n* in zip/tar downloads, always use the parent-folder name as the archive root\n* more reliable ftp authentication when providing password as username\n* connect-page: fix rclone ftps example\n\n## other changes\n* stop suggesting `--http-only` and `--https-only` for performance since the difference is negligible\n* mention how some antivirus (avast, avg, mcafee) thinks that pillow's webp encoder is a virus, affecting `copyparty.exe`\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0420-2141  `v1.6.12`  as seen on nixos\n\n## new features\n* @chinponya [made](https://github.com/9001/copyparty/pull/22) a copyparty [Nix package](https://github.com/9001/copyparty#nix-package) and a [NixOS module](https://github.com/9001/copyparty#nixos-module)! nice 🎉\n  * with [systemd-based hardening](https://github.com/9001/copyparty/blob/hovudstraum/contrib/nixos/modules/copyparty.nix#L230-L270) instead of [prisonparty](https://github.com/9001/copyparty/blob/hovudstraum/bin/prisonparty.sh)\n  * complements the [arch package](https://github.com/9001/copyparty/tree/hovudstraum/contrib/package/arch) very well w\n\n## bugfixes\n* fix an sqlite fd leak\n  * with enough simultaneous traffic, copyparty could run out of file descriptors since it relied on the gc to close sqlite cursors\n  * now there's a pool of cursors shared between the tcp connections instead, limited to the number of CPU cores\n  * performance mostly unaffected (or slightly improved) compared to before, except for a 20% reduction only during max server load caused by directory-listings or searches\n  * ~~somehow explicitly closing the cursors didn't always work... maybe this was actually a python bug :\\\\/~~\n    * yes, it does incomplete cleanup if opening a WAL database fails\n* multirange requests would fail with an error; now they get a 200 as expected (since they're kinda useless and not worth the overhead)\n  * [the only software i've ever seen do that](https://apps.kde.org/discover/) now works as intended\n* expand `~/` filesystem paths in all remaining args: `-c`, `-lo`, `--hist`, `--ssl-log`, and the `hist` volflag\n* never use IPv6-format IPv4 (`::ffff:127.0.0.1`) in responses\n* [u2cli](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py): don't enter delete stage if some of the uploads failed\n* audio player in safari on touchbar macbooks\n  * songs would play backwards because the touchbar keeps spamming play/pause\n  * playback would stop when the preloader kicks in because safari sees the new audio object and freaks out\n\n## other changes\n* added [windows quickstart / service example](https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/windows.md)\n* updated pyinstaller (it makes smaller exe files now)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0401-2112  `v1.6.11`  not joke\n\n## new features\n* new event-hook: [exif stripper](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/image-noexif.py)\n* [markdown thumbnails](https://a.ocv.me/pub/demo/pics-vids/README.md?v) -- see [readme](https://github.com/9001/copyparty#markdown-viewer)\n* soon: support for [web-scrobbler](https://github.com/web-scrobbler/web-scrobbler/) - the [Last.fm](https://www.last.fm/user/tripflag) browser extension\n  * will update here + readme with more info when [the v3](https://github.com/web-scrobbler/web-scrobbler/projects/5) is out\n\n## bugfixes\n* more sqlite query-planner twiddling\n  * deleting files is MUCH faster now, and uploads / bootup might be a bit better too\n* webdav optimizations / compliance\n  * should make some webdav clients run faster than before\n  * in very related news, the webdav-client in [rclone](https://github.com/rclone/rclone/) v1.63 ([currently beta](https://beta.rclone.org/?filter=latest)) will be ***FAST!***\n    * does cool stuff such as [bidirectional sync](https://github.com/9001/copyparty#folder-sync) between copyparty and a local folder\n* [bpm detector](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/audio-bpm.py) is a bit more accurate\n* [u2cli](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) / commandline uploader: better error messages if something goes wrong\n* readme rendering could fail in firefox if certain addons were installed (not sure which)\n* event-hooks: more accurate usage examples\n\n## other changes\n* @chinponya automated the prismjs build step (thx!)\n* updated some js deps (markedjs, codemirror)\n* copyparty.exe: updated Pillow to 9.5.0\n* and finally [the joke](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/rave.js) (looks [like this](https://cd.ocv.me/b/d2/d21/#af-9b927c42))\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0320-2156  `v1.6.10`  rclone sync\n\n## new features\n* [iPhone \"app\"](https://github.com/9001/copyparty#ios-shortcuts) (upload shortcut) -- thanks @Daedren !\n  * can strip exif, upload files, pics, vids, links, clipboard\n  * can download links and rehost the target file on your server\n* support `rclone sync` to [sync folders](https://github.com/9001/copyparty#folder-sync) to/from copyparty\n  * let webdav clients set lastmodified times during upload\n  * let webdav clients replace files during upload\n\n## bugfixes\n* [prisonparty](https://github.com/9001/copyparty/blob/hovudstraum/bin/prisonparty.sh): FFmpeg transcoding was slow because there was no `/dev/urandom`\n* iphones would fail to play *some* songs (low-bitrate and/or shorter than ~7 seconds)\n  * due to either an iOS bug or an FFmpeg bug in the caf remuxing idk\n  * fixed by mixing in white noise into songs if an iPhone asks for them\n* small correction in the docker readme regarding rootless podman\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0316-2106  `v1.6.9`  index.html\n\n## new features\n* option to show `index.html` instead of the folder listing\n  * arg `--ih` makes it default-enabled\n  * clients can enable/disable it in the `[⚙️]` settings tab\n  * url-param `?v` skips it for a particular folder\n* faster folder-thumbnail validation on startup (mostly on conventional HDDs) \n\n## bugfixes\n* \"load more\" button didn't always show up when search results got truncated\n* ux: tooltips could block buttons on android\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0312-1610  `v1.6.8`  folder thumbs\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) ╱ [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) ╱ [client testbed](https://cd.ocv.me/b/)\n\n## new features\n* folder thumbnails are indexed in the db\n  * now supports non-lowercase names (`Cover.jpg`, `Folder.JPG`)\n  * folders without a specific cover/folder image will show the first pic inside\n* when audio playback continues into an empty folder, keep trying for a bit\n* add no-index hints (google etc) in basic-browser HTML (`?b`, `?b=u`)\n* [commandline uploader](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) supports long filenames on win7\n\n## bugfixes\n* rotated logfiles didn't get xz compressed\n* image-gallery links pointing to a deleted image shows an error instead of a crashpage\n\n## other changes\n* folder thumbnails have purple text to differentiate from files\n* `copyparty32.exe` starts 30% faster (but is 6% larger)\n\n----\n\n# what to download?\n| download link | is it good? | description |\n| -- | -- | -- |\n| **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** | ✅ the best 👍 | runs anywhere! only needs python |\n| [a docker image](https://github.com/9001/copyparty/blob/hovudstraum/scripts/docker/README.md) | it's ok | good if you prefer docker 🐋 |\n| [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) |  ⚠️ [acceptable](https://github.com/9001/copyparty#copypartyexe) | for [win8](https://user-images.githubusercontent.com/241032/221445946-1e328e56-8c5b-44a9-8b9f-dee84d942535.png) or later; built-in thumbnailer |\n| [up2k.exe](https://github.com/9001/copyparty/releases/latest/download/up2k.exe) | ⚠️ acceptable | [CLI uploader](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) as a win7+ exe ([video](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm)) |\n| [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) | ⛔️ [dangerous](https://github.com/9001/copyparty#copypartyexe) | for [win7](https://user-images.githubusercontent.com/241032/221445944-ae85d1f4-d351-4837-b130-82cab57d6cca.png) -- never expose to the internet! |\n| [cpp-winpe64.exe](https://github.com/9001/copyparty/releases/download/v1.6.8/copyparty-winpe64.exe) | ⛔️ dangerous | runs on [64bit WinPE](https://user-images.githubusercontent.com/241032/205454984-e6b550df-3c49-486d-9267-1614078dd0dd.png), otherwise useless |\n\n* except for [up2k.exe](https://github.com/9001/copyparty/releases/latest/download/up2k.exe), all of the options above are equivalent\n* the zip and tar.gz files below are just source code\n* python packages are available at [PyPI](https://pypi.org/project/copyparty/#files)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0305-2018  `v1.6.7`  fix no-dedup + add up2k.exe\n\n## new features\n* controlpanel-connect: add example for webdav automount\n\n## bugfixes\n* fix a race which, in worst case (but unlikely on linux), **could cause data loss**\n  * could only happen if `--no-dedup` or volflag `copydupes` was set (**not** default)\n  * if two identical files were uploaded at the same time, there was a small chance that one of the files would become empty\n  * check if you were affected by doing a search for zero-byte files using either of the following:\n    * https://127.0.0.1:3923/#q=size%20%3D%200\n    * `find -type f -size 0`\n  * let me know if you lost something important and had logging enabled!\n* ftp: mkdir can do multiple levels at once (support filezilla)\n* fix flickering toast on upload finish\n* `[💤]` (upload-baton) could disengage if chrome decides to pause the background tab for 10sec (which it sometimes does)\n\n----\n\n## introducing [up2k.exe](https://github.com/9001/copyparty/releases/latest/download/up2k.exe)\n\nthe commandline up2k upload / filesearch client, now as a standalone windows exe\n* based on python 3.7 so it runs on 32bit windows7 or anything newer\n* *no https support* (saves space + the python3.7 openssl is getting old)\n* built from b39ff92f so it can do long filepaths and mojibake\n\n----\n\n⭐️ **you probably want [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) below;**\nthe exe is [not recommended](https://github.com/9001/copyparty#copypartyexe) for longterm use\nand the zip and tar.gz files are source code\n(python packages are available at [PyPI](https://pypi.org/project/copyparty/#files))\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0226-2030  `v1.6.6`  r 2 0 0\n\ntwo hundred releases wow\n* read-only demo server at https://a.ocv.me/pub/demo/\n* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) ╱ [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) ╱ [client testbed](https://cd.ocv.me/b/)\n* currently fighting a ground fault so the demo server will be unreliable for a while\n\n## new features\n* more docker containers! now runs on x64, x32, aarch64, armhf, ppc64, s390x\n  * pls let me know if you actually run copyparty on an IBM mainframe 👍\n* new [event hook](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks) type `xiu` runs just once for all recent uploads\n  * example hook [xiu-sha.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/xiu-sha.py) generates sha512 checksum files\n* new arg `--rsp-jtr` simulates connection jitter\n* copyparty.exe integrity selftest\n* ux:\n  * return to previous page after logging in\n  * show a warning on the login page if you're not using https\n  * freebsd: detect `fetch` and return the [colorful sortable plaintext](https://user-images.githubusercontent.com/241032/215322619-ea5fd606-3654-40ad-94ee-2bc058647bb2.png) listing\n\n## bugfixes\n* permit replacing empty files only during a `--blank-wt` grace period\n* lifetimes: keep upload-time when a size/mtime change triggers a reindex\n* during cleanup after an unlink, never rmdir the entire volume\n* rescan button in the controlpanel required volumes to be e2ds\n* dupes could get indexed with the wrong mtime\n  * only affected the search index; the filesystem got the right one\n* ux: search results could include the same hit twice in case of overlapping volumes\n* ux: upload UI would remain expanded permanently after visiting a huge tab\n* ftp: return proper error messages when client does something illegal\n* ie11: support the back button\n\n## other changes\n* [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) replaces copyparty64.exe -- now built for 64-bit windows 10\n  * **on win10 it just works** -- on win8 it needs [vc redist 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48145) -- no win7 support\n  * has the latest security patches, but sfx.py is still better for long-term use\n  * has pillow and mutagen; can make thumbnails and parse/index media\n* [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) is the old win7-compatible, dangerously-insecure edition\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0212-1411  `v1.6.5`  windows smb fix + win10.exe\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) ╱ [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) ╱ [client testbed](https://cd.ocv.me/b/)\n\n## bugfixes\n* **windows-only:** smb locations (network drives) could not be accessed\n  * appeared in [v1.6.4](https://github.com/9001/copyparty/releases/tag/v1.6.4) while adding support for long filepaths (260chars+)\n\n## other changes\n* removed tentative support for compressed chiptunes (xmgz, xmz, xmj, ...) since FFmpeg usually doesn't\n\n----\n\n# introducing [copyparty640.exe](https://github.com/9001/copyparty/releases/download/v1.6.5/copyparty640.exe)\n* built for win10, comes with the latest python and deps (supports win8 with [vc redist 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48145))\n* __*much* safer__ than the old win7-compatible `copyparty.exe` and `copyparty64.exe`\n  * but only `copyparty-sfx.py` takes advantage of the operating system security patches\n* includes pillow for thumbnails and mutagen for media indexing\n* around 10% slower (trying to figure out what's up with that)\n\nstarting from the next release,\n* `copyparty.exe` (win7 x32) will become `copyparty32.exe`\n* `copyparty640.exe` (win10) will be the new `copyparty.exe`\n* `copyparty64.exe` (win7 x64) will graduate\n\nso the [copyparty64.exe](https://github.com/9001/copyparty/releases/download/v1.6.5/copyparty64.exe) in this release will be the \"final\" version able to run inside a [64bit Win7-era winPE](https://user-images.githubusercontent.com/241032/205454984-e6b550df-3c49-486d-9267-1614078dd0dd.png) (all regular 32/64-bit win7 editions can just use `copyparty32.exe` instead)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0211-1802  `v1.6.4`  🔧🎲🔗🐳🇦🎶\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* [1.6 theme song](https://a.ocv.me/pub/demo/music/.bonus/#af-134e597c) // [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md)\n\n## new features\n* 🔧 new [config syntax](https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf) (#20)\n  * the new syntax is still kinda esoteric and funky but it's an improvement\n  * old config files are still supported\n    * `--vc` prints the autoconverted config which you can copy back into the config file to upgrade\n  * `--vc` will also [annotate and explain](https://user-images.githubusercontent.com/241032/217356028-eb3e141f-80a6-4bc6-8d04-d8d1d874c3e9.png) the config files\n  * new argument `--cgen` to generate config from commandline arguments\n    * kinda buggy, especially the `[global]` section, so give it a lookover before saving it\n* 🎲 randomize filenames on upload\n  * either optionally, using the 🎲 button in the up2k ui\n  * or force-enabled; globally with `--rand` or per-volume with volflag `rand`\n  * specify filename length with `nrand` (globally or volflag), default 9\n* 🔗 export a list of links to your recent uploads\n  * `copy links` in the up2k tab (🚀) will copy links to all uploads since last page refresh,\n  * `copy` in the unpost tab (🧯) will copy links to all your recent uploads (max 2000 files / 12 hours by default)\n  * filekeys are included if that's enabled and you have access to view those (permissions `G` or `r`)\n* 🇦 [arch package](https://github.com/9001/copyparty/tree/hovudstraum/contrib/package/arch) -- added in #18, thx @icxes \n  * maybe in aur soon!\n* 🐳 [docker containers](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) -- 5 editions,\n  * [min](https://hub.docker.com/r/copyparty/min) (57 MiB), just copyparty without thumbnails or audio transcoding\n  * [im](https://hub.docker.com/r/copyparty/im) (70 MiB), thumbnails of popular image formats + media tags with mutagen\n  * [ac (163 MiB)](https://hub.docker.com/r/copyparty/ac) 🥇 adds audio/video thumbnails + audio transcoding + better tags\n  * [iv](https://hub.docker.com/r/copyparty/iv) (211 MiB), makes heif/avic/jxl faster to thumbnail\n  * [dj](https://hub.docker.com/r/copyparty/dj) (309 MiB), adds optional detection of musical key / bpm\n* 🎶 [chiptune player](https://a.ocv.me/pub/demo/music/chiptunes/#af-f6fb2e5f)\n  * transcodes mod/xm/s3m/it/mo3/mptm/mt2/okt to opus\n  * uses FFmpeg (libopenmpt) so the accuracy is not perfect, but most files play OK enough\n  * not **yet** supported in the docker container since Alpine's FFmpeg was built without libopenmpt\n* windows: support long filepaths (over 260 chars)\n  * uses the `//?/` winapi syntax to also support windows 7\n* `--ver` shows the server version on the control panel\n\n## bugfixes\n* markdown files didn't scale properly in the document browser\n* detect and refuse multiple volume definitions sharing the same filesystem path\n* don't return incomplete transcodes if multiple clients try to play the same flac file\n* [prisonparty](https://github.com/9001/copyparty/blob/hovudstraum/bin/prisonparty.sh): more reliable chroot cleanup, sigusr1 for config reload\n* pypi packaging: compress web resources, include webdav.bat\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0131-2103  `v1.6.3`  sandbox k\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* and since [1.6.0](https://github.com/9001/copyparty/releases/tag/v1.6.2) only got 2 days of prime time,\n  * [1.6 theme song](https://a.ocv.me/pub/demo/music/.bonus/#af-134e597c) (hosted on the demo server)\n  * [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) / feature comparison\n\n## new features\n* dotfiles are hidden from search results by default\n  * use `--dotsrch` or volflags `dotsrch` / `nodotsrch` to specify otherwise\n  * they were already being excluded from tar/zip-files if `-ed` is not set, so this makes more sense -- dotfiles *should* now be undiscoverable unless `-ed` or `--smb` is set, but please use [volumes](https://github.com/9001/copyparty#accounts-and-volumes) for isolation / access-control instead, much safer\n\n## bugfixes\n* lots of cosmetic fixes for the new readme/prologue/epilogue sandbox\n  * rushed it into the previous release when someone suggested it, bad idea\n  * still flickers a bit (especially prologues), and hotkeys are blocked while the sandboxed document has focus\n  * can be disabled with `--no-sb-md --no-sb-lg` (not recommended)\n* support webdav uploads from davfs2 (fix LOCK response)\n* always unlink files before overwriting them, in case they are hardlinks\n  * was primarily an issue with `--daw` and webdav clients\n* on windows, replace characters in PUT filenames as necessary\n* [prisonparty](https://github.com/9001/copyparty/blob/hovudstraum/bin/prisonparty.sh): support opus transcoding on debian\n  * `rm -rf .hist/ac` to clear the transcode cache if the old version broke some songs\n\n## other changes\n* add `rel=\"nofollow\"` to zip download links, basic-browser link\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0129-1842  `v1.6.2`  cors k\n\n[Ellie Goulding - Stay Awake (kors k Hardcore Bootleg).mp3](https://a.ocv.me/pub/demo/music/.bonus/#af-134e597c)\n* 👆 the read-only demo server at https://a.ocv.me/pub/demo/\n\n## breaking changes\nbut nothing is affected (that i know of):\n* all requests must pass [cors validation](https://github.com/9001/copyparty#cors)\n  * but they almost definitely did already\n  * sharex and others are OK since they don't supply an `Origin` header\n* [API calls](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#http-api) `?delete` and `?move` are now POST instead of GET\n  * not aware of any clients using these\n\n## known issues\n* the document sandbox is a bit laggy and sometimes eats hotkeys\n  * disable it with `--no-sb-md --no-sb-lg` if you trust everyone who has write and/or move access\n\n## new features\n* [event hooks](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks) -- run programs on new [uploads](https://user-images.githubusercontent.com/241032/215304439-1c1cb3c8-ec6f-4c17-9f27-81f969b1811a.png), renames, deletes\n* [configurable cors](https://github.com/9001/copyparty#cors) (cross-origin resource sharing) behavior; defaults are mostly same as before\n  * `--allow-csrf` disables all csrf protections and makes it intentionally trivial to send authenticated requests from other domains\n* sandboxed readme.md / prologues / epilogues\n  * documents can still run scripts like before, but can no longer tamper with the web-ui / read the login session, so the old advice of `--no-readme` and `--no-logues` is mostly deprecated\n  * unfortunately disables hotkeys while the text has focus + blocks dragdropping files onto that area, oh well\n* password can be provided through http header `PW:` (instead of cookie `cppwd` or or url-param `?pw`)\n* detect network changes (new NICs, IPs) and reconfigure / reannoucne zeroconf\n  * fixes mdns when running as a systemd service and copyparty is started before networking is up\n* add `--freebind` to start listening on IPs before the NIC is up yet (linux-only)\n* per-volume deduplication-control with volflags `hardlink`, `neversymlink`, `copydupes`\n* detect curl and return a [colorful, sortable plaintext](https://user-images.githubusercontent.com/241032/215322619-ea5fd606-3654-40ad-94ee-2bc058647bb2.png) directory listing instead\n* add optional [powered-by-copyparty](https://user-images.githubusercontent.com/241032/215322626-11d1f02b-25f4-45df-a3d9-f8c51354a8eb.png) footnode on the controlpanel\n  * can be disabled with `-nb` or redirected with `--pb-url`\n\n## bugfixes\n* change some API calls (`?delete`, `?move`) from `GET` to `POST`\n  * don't panic! this was safe against authenticated csrf thanks to [SameSite=Lax](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#lax)\n  * `--getmod` restores the GETs if you need the convenience and accept the risks\n* [u2cli](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) (command-line uploader):\n  * recover from network hiccups\n  * add `-ns` for slow uefi TTYs\n* separate login cookies for http / https\n  * avoids an https login from getting accidentally sent over plaintext\n  * sadly no longer possible to login with internet explorer 4.0 / windows 3.11\n* tar/zip-download of hidden folders\n* unpost filtering was buggy for non-ascii characters\n* moving a deduplicated file on a volume where deduplication was since disabled\n* improved the [linux 6.0.16](https://utcc.utoronto.ca/~cks/space/blog/linux/KernelBindBugIn6016) kernel bug [workaround](https://github.com/9001/copyparty/commit/9065226c) because there is similar funk in 5.x\n* add custom text selection colors because chrome is currently broken on fedora\n* blockdevs (`/dev/nvme0n1`) couldn't be downloaded as files\n* misc fixes for location-based reverse-proxying\n* macos dualstack thing\n\n## other changes\n* added a collection of [cursed usecases](https://github.com/9001/copyparty/tree/hovudstraum/docs/cursed-usecases)\n* and [comparisons to similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) in case you ever wanna jump ship\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2023-0112-0515  `v1.5.6`  many hands\n\nhello from warsaw airport (goodbye japan ;_;)\n* read-only demo server at https://a.ocv.me/pub/demo/\n\n## new features\n* multiple upload handshakes in parallel\n  * around **5x faster** when uploading small files\n  * or **50x faster** if the server is on the other side of the planet\n    * just crank up the `parallel uploads` like crazy (max is 64)\n* upload ui: total time and average speed is shown on completion\n\n## bugfixes\n* browser ui didn't allow specifying number of threads for file search\n* dont panic if a digit key is pressed while viewing an image\n* workaround [linux kernel bug](https://utcc.utoronto.ca/~cks/space/blog/linux/KernelBindBugIn6016) causing log spam on dualstack\n  * ~~related issue (also mostly harmless) will be fixed next relese 01077068~~ \n    * they fixed it in linux 6.1 so these workarounds will be gone too\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-1230-0754  `v1.5.5`  made in japan\n\nhello from tokyo\n* read-only demo server at https://a.ocv.me/pub/demo/\n\n## new features\n* image viewer now supports heif, avif, apng, svg\n* [partyfuse and up2k.py](https://github.com/9001/copyparty/tree/hovudstraum/bin): option to read password from textfile\n\n## bugfixes\n* thumbnailing could fail if a primitive build of libvips is installed\n* ssdp was wonky on dualstack ipv6\n* mdns could crash on networks with invalid routes\n* support fat32 timestamp precisions\n  * fixes spurious file reindexing in volumes located on SD cards on android tablets which lie about timestamps until the next device reboot or filesystem remount\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-1213-1956  `v1.5.3`  folder-sync + turbo-rust\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n\n## new features\n* one-way folder sync (client to server) using [up2k.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/README.md#up2kpy) `-z --dr`\n  * great rsync alternative when combined with `-e2ds --hardlink` deduplication on the server\n* **50x faster** when uploading small files to HDD, especially SMR\n  * by switching sqlite to WAL which carries a small chance of temporarily forgetting the ~200 most recent uploads if you have a power outage or your OS crashes; see `--help-dbd` if you have `-mtp` plugins which produces metadata you can't afford to lose\n* location-based [reverse-proxying](https://github.com/9001/copyparty/#reverse-proxy) (but it's still recommended to use a dedicated domain/subdomain instead)\n* IPv6 link-local automatically enabled for TCP and zeroconf on NICs without a routable IPv6\n* zeroconf network filters now accept subnets too, for example `--z-on 192.168.0.0/16`\n* `.hist` folders are hidden on windows\n* ux:\n  * more accurate total ETA on upload\n  * sorting of batch-unpost links was unintuitive / dangerous\n  * hotkey `Y` turns files into download links if nothing's selected\n  * option to replace or disable the mediaplayer-toggle mouse cursor with `--mpmc`\n\n## bugfixes\n* WAL probably/hopefully fixes #10 (we'll know in 6 months roughly)\n* repair db inconsistencies (which can happen if terminated during startup)\n* [davfs2](https://wiki.archlinux.org/title/Davfs2) did not approve of the authentication prompt\n* the `connect` button on the control-panel didn't work on phones\n* couldn't specify windows NICs in arguments `--z-on` / `--z-off` and friends\n* ssdp xml escaping for `--zsl` URL\n* no longer possible to accidentally launch multiple copyparty instances on the same port on windows\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-1203-2048  `v1.5.1`  babel\n\nnamed after [that other thing](https://en.wikipedia.org/wiki/Tower_of_Babel), not [the song](https://soundcloud.com/kanaze/babel-dimension-0-remix)\n* read-only demo server at https://a.ocv.me/pub/demo/\n\n## new features\n* new protocols!\n  * native IPv6 support, no longer requiring a reverse-proxy for that\n  * [webdav server](https://github.com/9001/copyparty#webdav-server) -- read/write-access to copyparty straight from windows explorer, macos finder, kde/gnome\n  * [smb/cifs server](https://github.com/9001/copyparty#smb-server) -- extremely buggy and unsafe, for when there is no other choice\n  * [zeroconf](https://github.com/9001/copyparty#zeroconf) -- copyparty announces itself on the LAN, showing up in various file managers\n    * [mdns](https://github.com/9001/copyparty#mdns) -- macos/kde/gnome + makes copyparty available at http://hostname.local/\n    * [ssdp](https://github.com/9001/copyparty#ssdp) -- windows\n  * commands to mount copyparty as a local disk are in the web-UI at control-panel --> `connect`\n* detect buggy / malicious clients spamming the server with idle connections\n  * first tries to be nice with `Connection: close` (enough to fix windows-webdav)\n  * eventually bans the IP for `--loris` minutes (default: 1 hour)\n* new arg `--xlink` for cross-volume detection of duplicate files on upload\n* new arg `--no-snap` to disable upload tracking on restart\n  * will not create `.hist` folders unless required for thumbnails or markdown backups\n* [config includes](https://github.com/9001/copyparty/blob/hovudstraum/docs/example2.conf) -- split your config across multiple config files\n* ux improvements\n  * hotkey `?` shows a summary of all the hotkeys\n  * hotkey `Y` to download selected files\n  * position indicator when hovering over the audio scrubber\n  * textlabel on the volume slider\n  * placeholder values in textboxes\n  * options to hide scrollbars, compact media player, follow playing song\n  * phone-specific\n    * buttons for prev/next folder\n    * much better ui for hiding folder columns\n\n## bugfixes\n* now possible to upload files larger than 697 GiB\n  * technically a [breaking change](https://github.com/9001/copyparty#breaking-changes) if you wrote your own up2k client\n    * please let me know if you did because that's awesome\n* several macos issues due to hardcoded syscall numbers\n* sfx: fix python 3.12 support (forbids nullbytes in source code)\n* use ctypes to discover network config -- fixes grapheneos, non-english windows\n* detect firefox showing stale markdown documents in the editor\n* detect+ban password bruteforcing on ftp too\n* http 206 failing on empty files\n* incorrect header timestamps on non-english locales\n* remind ftp clients that you cannot cd into an image file -- fixes kde dolphin\n* ux fixes\n  * uploader survives running into inaccessible folders\n  * middleclick documents in the textviewer sidebar to open in a new tab\n  * playing really long audio files (1 week or more) would spinlock the browser\n\n## other changes\n* autodetect max number of clients based on OS limits\n  * `-nc` is probably no longer necessary when running behind a reverse-proxy\n* allow/try playing mkv files in chrome\n* markdown documents returned as plaintext unless `?v`\n* only compress `-lo` logfiles if filename ends with `.xz`\n* changed sfx compression from bz2 to gz\n  * startup is slightly faster\n  * better compatibility with embedded linux\n* copyparty64.exe -- 64bit edition for [running inside WinPE](https://user-images.githubusercontent.com/241032/205454984-e6b550df-3c49-486d-9267-1614078dd0dd.png)\n  * which was an actual feature request, believe it or not!\n* more attempts at avoiding the [firefox fd leak](https://bugzilla.mozilla.org/show_bug.cgi?id=1790500)\n  * if you are uploading many small files and the browser keeps crashing, use chrome instead\n    * or the commandline client, which is now available for download straight from copyparty\n      * control-panel --> `connect` --> `up2k.py`\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-1013-1937  `v1.4.6`  wav2opus\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* latest gzip edition of the sfx: *This version*\n\n## bugfixes\n* the option to transcode flac to opus while playing audio in the browser was supposed to transcode wav-files as well, instead of being extremely hazardous to mobile data plans (sorry)\n* `--license` didn't work if copyparty was installed from `pip`\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-1009-0919  `v1.4.5`  qr-code\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## new features\n* display a server [qr-code](https://github.com/9001/copyparty#qr-code) [(screenshot)](https://user-images.githubusercontent.com/241032/194728533-6f00849b-c6ac-43c6-9359-83e454d11e00.png) on startup\n  * primarily for running copyparty on a phone and accessing it from another\n  * optionally specify a path or password with `--qrl lootbox/?pw=hunter2`\n  * uses the server's exteral ip (default route) unless `--qri` specifies a domain / ip-prefix\n  * classic cp437 `▄` `▀` for space efficiency; some misbehaving terminals / fonts need `--qrz 2`\n* new permission `G` returns the filekey of uploaded files for users without read-access\n  * when combined with permission `w` and volflag `fk`, uploaded files will not be accessible unless the filekey is provided in the url, and `G` provides the filekey to the uploader unlike `g`\n* filekeys are added to the unpost listing\n\n## bugfixes\n* renaming / moving folders is now **at least 120x faster**\n  * and that's on nvme drives, so probably like 2000x on HDDs\n* uploads to volumes with lifetimes could get instapurged depending on browser and browser settings\n* ux fixes\n  * FINALLY fixed messageboxes appearing offscreen on phones (and some other layout issues)\n  * stop asking about folder-uploads on phones because they dont support it\n  * on android-firefox, default to truncating huge folders with the load-more button due to ff onscroll being buggy\n  * audioplayer looking funky if ffmpeg unavailable\n* waveform-seekbar cache expiration (the thumbcleaner complaining about png files)\n* ie11 panic when opening a folder which contains a file named `up2k`\n  * turns out `<a name=foo>` becomes `window.foo` unless that's already declared somewhere in js -- luckily other browsers \"only\" do that with IDs\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0926-2037  `v1.4.3`  signal in the noise\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## new features\n* `--bak-flips` saves a copy of corrupted / bitflipped up2k uploads\n  * comparing against a good copy can help pinpoint the culprit\n  * also see [tracking bitflips](https://github.com/9001/copyparty/blob/hovudstraum/docs/notes.sh#:~:text=tracking%20bitflips)\n\n## bugfixes\n* some edgecases where deleted files didn't get dropped from the db\n  * can reduce performance over time, hitting the filesystem more than necessary\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0925-1236  `v1.4.2`  fuhgeddaboudit\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## new features\n* forget incoming uploads by deleting the name-reservation\n  * (the zerobyte file with the actual filename, not the .PARTIAL)\n  * can take 5min to kick in\n\n## bugfixes\n* zfs on ubuntu 20.04 would reject files with big unicode names such as `148. Профессор Лебединский, Виктор Бондарюк, Дмитрий Нагиев - Я её хой (Я танцую пьяный на столе) (feat. Виктор Бондарюк & Дмитрий Нагиев).mp3`\n  * usually not a problem since copyparty truncates names to fit filesystem limits, except zfs uses a nonstandard errorcode\n* in the \"print-message-to-serverlog\" feature, a unicode message larger than one tcp-frame could decode incorrectly\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0924-1245  `v1.4.1`  fix api compat\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# bugfixes\n* [v1.4.0](https://github.com/9001/copyparty/releases/tag/v1.4.0) accidentally required all clients to use the new up2k.js to continue uploading; support the old js too\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0923-2053  `v1.4.0`  mostly reliable\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## new features\n* huge folders are lazily rendered for a massive speedup, #11\n  * also reduces the number of `?tree` requests; helps a tiny bit on server load\n* [selfdestruct timer](https://github.com/9001/copyparty#self-destruct) on uploaded files -- see link for howto and side-effects\n* ban clients trying to bruteforce passwords\n  * arg `--ban-pw`, default `9,60,1440`, bans for 1440min after 9 wrong passwords in 60min\n  * clients repeatedly trying the same password (due to a bug or whatever) are not counted\n  * does a `/64` range-ban for IPv6 offenders\n  * arg `--ban-404`, disabled by default, bans for excessive 404s / directory-scanning\n    * but that breaks up2k turbo-mode and probably some other eccentric usecases\n* waveform seekbar [(screenshot)](https://user-images.githubusercontent.com/241032/192042695-522b3ec7-6845-494a-abdb-d1c0d0e23801.png)\n* the up2k upload button can do folders recursively now\n  * but only a single folder can be selected at a time, making drag-drop the obvious choice still\n* gridview is now less jank, #12\n* togglebuttons for desktop-notifications and audio-jingle when upload completes\n* stop exposing uploader IPs when avoiding filename collisions\n  * IPs are now HMAC'ed with urandom stored at `~/.config/copyparty/iphash`\n* stop crashing chrome; generate PNGs rather than SVGs for filetype icons\n* terminate connections with SHUT_WR and flush with siocoutq\n  * makes buggy enterprise proxies behave less buggy\n  * do a read-spin on windows for almost the same effect\n* improved upload scheduling\n  * unfortunately removes the `0.0%, NaN:aN, N.aN MB/s` easteregg\n* arg `--magic` enables filetype detection on nameless uploads based on libmagic\n* mtp modifiers to let tagparsers keep their stdout/stderr instead of capturing\n  * `c0` disables all capturing, `c1` captures stdout only, `c2` only stderr, and `c3` (default) captures both\n* arg `--write-uplog` enables the old default of writing upload reports on POSTs\n  * kinda pointless and was causing issues in prisonparty\n* [upload modifiers](https://github.com/9001/copyparty#write) for terse replies and to randomize filenames\n* other optimizations\n  * 30% faster tag collection on directory listings\n  * 8x faster rendering of huge tagsets\n* new mtps [guestbook](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/guestbook.py) and [guestbook-read](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/guestbook-read.py), for example for comment-fields on uploads\n* arg `--stackmon` now takes dateformat filenames to produce multiple files\n* arg `--mtag-vv` to debug tagparser configs\n* arg `--version` shows copyparty version and exits\n* arg `--license` shows a list of embedded dependencies + their licenses\n* arg `--no-forget` and volflag `:c,noforget` keeps deleted files in the up2k db/index\n  * useful if you're shuffling uploads to s3/gdrive/etc and still want deduplication\n\n## bugfixes\n* upload deduplication using symlinks on windows\n* increase timeouts to run better on servers with extremely overloaded HDDs\n  * arg `--mtag-to` (default 60 sec, was 10) can be reduced for faster tag scanning\n* incorrect filekeys for files symlinked into another volume\n* playback could start mid-song if skipping back and forth between songs\n* use affinity mask to determine how many CPU cores are available\n* restore .bin-suffix for nameless PUT/POSTs (disappeared in v1.0.11)\n* fix glitch in uploader-UI when upload queue is bigger than 1 TiB\n* avoid a firefox race-condition accessing the navigation history\n* sfx tmpdir keepalive when flipflopping between unix users\n* reject anon ftp if anon has no read/write\n* improved autocorrect for poor ffmpeg builds\n* patch popen on older pythons so collecting tags on windows is always possible\n* misc ui/ux fixes\n  * filesearch layout in read-only folders\n  * more comfy fadein/fadeout on play/pause\n  * total-ETA going crazy when an overloaded server drops requests\n  * stop trying to play into the next folder while in search results\n  * improve warnings/errors in the uploader ui\n    * some errors which should have been warnings are now warnings\n    * autohide warnings/errors when they are remedied\n  * delay starting the audiocontext until necessary\n    * reduces cpu-load by 0.2% and fixes chrome claiming the tab is playing audio\n\n# copyparty.exe\n\nnow introducing [copyparty.exe](https://github.com/9001/copyparty/releases/download/v1.4.0/copyparty.exe)!   only suitable for the rainiest of days ™\n\n[first thing you'll see](https://user-images.githubusercontent.com/241032/192070274-bfe0bfef-2293-40fc-8852-fcf4f7a90043.png) when you run it is a warning to **«please use the [python-sfx](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) instead»**,\n* `copyparty.exe` was compiled using 32bit python3.7 to support windows7, meaning it won't receive any security patches\n* `copyparty-sfx.py` uses your system libraries instead so it'll stay safe for much longer while also having better performance\n\nso the exe might be super useful in a pinch on a secluded LAN but otherwise *Absolutely Not Recommended*\n\nyou can download [ffmpeg](https://ocv.me/stuff/bin/ffmpeg.exe) and [ffprobe](https://ocv.me/stuff/bin/ffprobe.exe) into the same folder if you want multimedia-info, audio-transcoding or thumbnails/spectrograms/waveforms -- those binaries were [built](https://github.com/9001/copyparty/tree/hovudstraum/scripts/pyinstaller#ffmpeg) with just enough features to cover what copyparty wants, but much like copyparty.exe itself (so due to security reasons) it is strongly recommended to instead grab a [recent official build](https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip) every once in a while\n\n## and finally some good news\n\n* the chrome memory leak will be [fixed in v107](https://bugs.chromium.org/p/chromium/issues/detail?id=1354816)\n* and firefox may fix the crash in [v106 or so](https://bugzilla.mozilla.org/show_bug.cgi?id=1790500)\n* and the release title / this season's codename stems from a cpp instance recently being slammed with terabytes of uploads running on a struggling server mostly without breaking a sweat 👍\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0818-1724  `v1.3.16`  gc kiting\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## bugfixes\n* found a janky workaround for [the remaining chrome wasm gc bug](https://bugs.chromium.org/p/chromium/issues/detail?id=1354816)\n  * worker-global typedarray holding on to the first and last byte of the filereader output while wasm chews on it\n  * overhead is small enough, slows down firefox by 2~3%\n  * seems to work on many chrome versions but no guarantees\n    * still OOM's some 93 and 97 betas, probably way more \n\n## other changes\n* disable `mt` by default on https-desktop-chrome\n  * avoids the gc bug entirely (except for plaintext-http and phones)\n  * chrome [doesn't parallelize](https://bugs.chromium.org/p/chromium/issues/detail?id=1352210) `crypto.subtle.digest` anyways\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0817-2302  `v1.3.15`  pls let me stop finding chrome bugs\n\ntwo browser-bugs in two hours, man i just wanna play horizon\n* read-only demo server at https://a.ocv.me/pub/demo/\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## bugfixes\n* chrome randomly running out of memory while hashing files and `mt` is enabled\n  * the gc suddenly gives up collecting the filereaders\n  * fixed by reusing a pool of readers instead\n* chrome failing to gc Any Buffers At All while hashing files and `mt` is enabled on plaintext http\n  * this one's funkier, they've repeatedly fixed and broke it like 6 times between chrome 84 and 106\n  * looks like it just forgets about everything that's passed into wasm\n  * no way around it, just show a popup explaining how to disable multithreaded hashing\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0815-1825  `v1.3.14`  fix windows db\n\nafter two exciting releases, time for something boring\n* read-only demo server at https://a.ocv.me/pub/demo/\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## new features\n* upload-info (ip and timestamp) is provided to `mtp` tagparser plugins as json\n* tagscanner will index `fmt` (file-format / container type) by default\n  * and `description` can be enabled in `-mte`\n\n## bugfixes\n* [v1.3.12](https://github.com/9001/copyparty/releases/tag/v1.3.12) broke file-indexing on windows if an entire HDD was mounted as a volume\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0812-2258  `v1.3.12`  quickboot\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## new features\n*but wait, there's more!*   not only do you get the [multithreaded file hashing](https://github.com/9001/copyparty/releases/tag/v1.3.11) but also --\n* faster bootup and volume reindexing when `-e2ds` (file indexing) is enabled\n  * `3x` faster is probably the average on most instances; more files per folder = faster\n  * `9x` faster on a 36 TiB zfs music/media nas with `-e2ts` (metadata indexing), dropping from 46sec to 5sec\n  * and `34x` on another zfs box, 63sec -> 1.8sec\n  * new arg `--no-dhash` disables the speedhax in case it's buggy (skipping files or audio tags)\n* add option `--exit idx` to abort and shutdown after volume indexing has finished\n\n## bugfixes\n* [u2cli](https://github.com/9001/copyparty/tree/hovudstraum/bin#up2kpy): detect and skip uploading from recursive symlinks\n* stop reindexing empty files on startup\n* support fips-compliant cpython builds\n  * replaces md5 with sha1, changing the filetype-associated colors in the gallery view\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0810-2135  `v1.3.11`  webworkers\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## new features\n* multithreaded file hashing! **300%** average speed increase\n  * when uploading files through the browser client, based on web-workers\n    * `4.5x` faster on http from a laptop -- `146` -> `670` MiB/s\n    * ` 30%` faster on https from a laptop -- `552` -> `716` MiB/s\n    * `4.2x` faster on http from android -- `13.5` -> `57.1` MiB/s\n    * `5.3x` faster on https from android -- `13.8` -> `73.3` MiB/s\n    * can be disabled using the `mt` togglebtn in the settings pane, for example if your phone runs out of memory (it eats ~250 MiB extra RAM)\n  * `2.3x` faster [u2cli](https://github.com/9001/copyparty/tree/hovudstraum/bin#up2kpy) (cmd-line client) -- `398` -> `930` MiB/s\n  * `2.4x` faster filesystem indexing on the server\n  * thx to @kipukun for the webworker suggestion!\n\n## bugfixes\n* ux: reset scroll when navigating into a new folder\n* u2cli: better errormsg if the server's tls certificate got rejected\n* js: more futureproof cloudflare-challenge detection (they got a new one recently)\n\n## other changes\n* print warning if the python interpreter was built with an unsafe sqlite\n* u2cli: add helpful messages on how to make it run on python 2.6\n\n**trivia:** due to a [chrome bug](https://bugs.chromium.org/p/chromium/issues/detail?id=1352210), http can sometimes be faster than https now ¯\\\\\\_(ツ)\\_/¯\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0803-2340  `v1.3.10`  folders first\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## new features\n* faster\n  * tag scanner\n  * on windows: uploading to fat32 or smb\n* toggle-button to sort folders before files (default-on)\n  * almost the same as before, but now also when sorting by size / date\n* repeatedly hit `ctrl-c` to force-quit if everything dies\n* new file-indexing guards\n  * `--xdev` / volflag `:c,xdev` stops if it hits another filesystem (bindmount/symlink)\n  * `--xvol` / volflag `:c,xvol` does not follow symlinks pointing outside the volume\n  * only affects file indexing -- does NOT prevent access!\n\n## bugfixes\n* forget uploads that failed to initialize (allows retry in another folder)\n* wrong filekeys in upload response if volume path contained a symlink\n* faster shutdown on `ctrl-c` while hashing huge files\n* ux: fix navpane covering files on horizontal scroll\n\n## other changes\n* include version info in the base64 crash-message\n* ux: make upload errors more visible on mobile\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0727-1407  `v1.3.8`  more async\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## new features\n* new arg `--df 4` and volflag `:c,df=4g` to guarantee 4 GiB free disk space by rejecting uploads\n* some features no longer block new uploads while they're processing\n  * `-e2v` file integrity checker\n  * `-e2ts` initial tag scanner\n  * hopefully fixes a [deadlock](https://www.youtube.com/watch?v=DkKoMveT_jo&t=3s) someone ran into (but probably doesn't)\n    * (the \"deadlock\" link is an addictive demoscene banger -- the actual issue is #10)\n* reduced the impact of some features which still do\n  * defer `--re-maxage` reindexing if there was a write (upload/rename/...) recently\n    * `--db-act` sets minimum idle period before reindex can start (default 10sec)\n* bbox / image-viewer: add video hotkeys 0..9 to seek 0%..90%\n* audio-player: add audio crossfeed (left-right channel mixer / vocal isolation)\n* splashpage (`/?h`) shows time since the most recent write\n\n## bugfixes\n* a11y:\n  * enter-key should always trigger onclick\n  * only focus password box if in-bounds\n  * improve skip-to-files\n* prisonparty: volume labeling in root folders\n* other minor stuff\n  * forget deleted shadowed files from the db\n  * be less noisy if a client disconnects mid-reply\n  * up2k.js less eager to thrash slow server HDDs\n\n## other changes\n* show client's upload ETA in server log\n* dump stacks and issue `lsof` on the db if a transaction is stuck\n  * will hopefully help if there's any more deadlocks\n* [up2k-hook-ytid](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/up2k-hook-ytid.js) (the overengineered up2k.js plugin example) now has an mp4/webm/mkv metadata parser\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0716-1848  `v1.3.7`  faster\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## new features\n* `up2k.js`: **improved upload speeds!**\n  * **...when there's many small files** (or the browser is slow)\n    * add [potato mode](https://user-images.githubusercontent.com/241032/179336639-8ecc01ea-2662-4cb6-8048-5be3ad599f33.png) -- lightweight UI for faster uploads from slow boxes\n    * enables automatically if it detects a cpu bottleneck (not very accurate)\n  * **...on really fast connections (LAN / fiber)**\n    * batch progress updates to reduce repaints\n  * **...when there is a mix of big and small files**\n    * sort the uploads by size, smallest first, for optimal cpu/network usage\n      * can be overridden to alphabetical order in the settings tab\n      * new arg `--u2sort` changes the default + overrides the override button\n    * improve upload pacing when alphabetical order is enabled\n      * mainly affecting single files that are 300 GiB + \n* `up2k.js`: add [up2k hooks](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/up2k-hooks.js)\n  * specify *client-side* rules to reject files as they are dropped into the browser\n  * not a hard-reject since people can use [up2k.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) and whatnot, more like a hint\n* `up2k.py`: add file integrity checker\n  * new arg `-e2v` to scan volumes and verify file checksums on startup\n  * `-e2vu` updates the db on mismatch, `-e2vp` panics\n  * uploads are blocked while the scan is running -- might get fixed at some point\n    * for now it prints a warning\n* bbox / image-viewer: doubletap a picture to enter fullscreen mode\n* md-editor: `ctrl-c/x` affects current line if no selection, and `ctrl-e` is fullscreen\n* tag-parser plugins:\n  * add support for passing metadata from one mtp to another (parser dependencies)\n    * the `p` flag in [vidchk](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/vidchk.py) usage makes it run after the base parser, eating its output\n  * add [rclone uploader](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/rclone-upload.py) which optionally and by default depends on vidchk\n\n## bugfixes\n* sfx would crash if it got the same PID as recently (for example across two reboots)\n* audio equalizer on recent chromes\n  * still can't figure out why chrome sometimes drops the mediasession\n* bbox: don't attach click events to videos\n* up2k.py:\n  * more sensible behavior w/ blank files\n  * avoid some extra directory scans when deleting files\n  * faster shutdown on `ctrl-c` during volume indexing\n* warning from the thumbnail cleaner if the volume has no thumbnails\n* `>fixing py2 support` `>2022`\n\n## other changes\n* up2k.js:\n  * sends a summary of the upload queue to [the server log](https://github.com/9001/copyparty#up2k)\n  * shows a toast while loading huge filedrops to indicate it's still alive\n* sfx: disable guru meditation unless running on windows\n  * avoids hanging systemd on certain crashes\n* logs the state of all threads if sqlite hits a timeout\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0706-0029  `v1.3.5`  sup cloudflare\n\n* read-only demo server at https://a.ocv.me/pub/demo/\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## new features\n* detect + recover from cloudflare ddos-protection memes during upload\n  * while carefully avoiding any mention of \"DDoS\" in the JS because enterprise firewalls do not enjoy that\n* new option `--favico` to specify a default favicon\n  * set to `🎉` by default, which also enables the fancy upload progress donut 👌\n* baguettebox (image/video viewer):\n  * toolbar button `⛶` to enter fullscreen mode (same as hotkey `F`)\n  * tap middle of screen to show/hide toolbar\n  * tap left/right-side of pics to navigate prev/next\n  * hotkeys `[` and `]` to set A-B loop in videos\n    * and [URL parameters](https://a.ocv.me/pub/demo/pics-vids/#gf-e2e482ae&t=4.2-6) for that + [initial seekpoint](https://a.ocv.me/pub/demo/pics-vids/#gf-c04bb0f6&t=26s) (same as the audio player)\n\n## bugfixes\n* when a tag-parser hits the timeout, `pkill` all its descendants too\n  * and a [new mtp flag](https://github.com/9001/copyparty/#file-parser-plugins) to override that; `kt` (kill tree, default), `km` (kill main, old default), `kn` (kill none)\n* cpu-wasting spin while waiting for the final handful of files to finish tag-scraping\n* detection of sparse-files support inside [prisonparty](https://github.com/9001/copyparty/tree/hovudstraum/bin#prisonpartysh) and other strict jails\n* baguettebox (image/video viewer):\n  * crash on swipe during close\n* didn't reset terminal color at the end of `?ls=v`\n* don't try to thumbnail empty files (harmless but dumb)\n\n## other changes\n* ux improvements\n  * hide the uploads table until something happens\n* bump codemirror to 5.65.6\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0627-2057  `v1.3.3`  sdcardfs\n\n* **new:** read-only demo server at https://a.ocv.me/pub/demo/\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## bugfixes\n* **upload:** downgrade filenames to ascii if the server filesystem requires it\n  * **android fix:** external sdcard seems to be UCS-2 which can't into emojis\n* **upload:** accurate detection of support for sparse files\n  * now based on filesystem behavior rather than a list of known filesystems\n    * **android fix:** all storage is `sdcardfs` so the list wasn't good enough\n* **ux:** custom css/js did not apply to write-only folders\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0619-2331  `v1.3.2`  think im out of titles\n\n* **new:** read-only demo server at https://a.ocv.me/pub/demo/\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## new features\n* new option `--thickfs` to modify the list of filesystems that dont support sparse files\n  * default should catch most usual cases but I probably missed some\n* detect and warn if filesystem was expected to support sparse files yet doesn't\n\n## bugfixes\n* nonsparse: ensure chunks are flushed on linux as well\n* switching between documents\n* ctrl-clicking a breadcrumb entry didn't open a new tab as expected\n* renaming files based on artist/title/etc tags would create subdirectories if tags contained `/`\n  * not dangerous -- the server correctly prevented any path traversals -- just unexpected\n* markdown stuff\n  * numbered lists appeared as bullet-lists\n  * don't crash if a plugin sets a buggy timer\n  * plugins didn't run when viewing `README.md` inline\n\n## other changes\n* in the `-ss` safety preset, replace `no-dot-mv, no-dot-ren` with `no-logues, no-readme`\n* audio player continues into the next folder by default\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0616-1956  `v1.3.1`  types\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## new features\n* improved support for filesystems without sparse files (fat32, exfat, hpfs)\n  * the server no longer preallocates the whole file with zeroes before upload can start\n  * so you can now finally run copyparty on your android phone or tablet and upload to the sd-card instead of the internal storage\n  * however upload speed will suffer a bit (limited to a single tcp connection doing one chunk at a time)\n* safety profiles; arguments `-s`, `-ss`, and `-sss` are aliases/presets for other safety-related arguments\n  * `-s` reduces attack surface from potentially dangerous software by disabling thumbnails, audio transcoding, ffmpeg, pillow, vips\n  * `-ss` also prevents js-injection, accidental move/deletes, broken symlinks, and enables enterprise-grade security (return 404 on 403)\n  * `-sss` also enables logging to disk and does a scan for dangerous symlinks at startup (possibly expensive)\n* ux improvements\n  * a11y jumpers -- hit tab + enter to jump straight to files/folders\n  * hotkey `Y` to download currently playing song / vid / pic\n  * button to reset the hidden columns\n  * new themes \"hacker\" and \"hi-con\"\n\n## bugfixes\n* spinlock if a client disconnects in the middle of an up2k handshake\n* ftp server couldn't persist metadata when multiprocessing was enabled (`-j 0`)\n* cut/paste (move) files between filesystems\n* allow `Connection: keep-alive` on HTTP/1.0\n* stray `[` appeared at the start of logfiles in the textviewer\n* misleading log message when a completed upload expires from registry and `-e2d` was not set\n\n## other changes\n* the basic uploader adds the `.PARTIAL` suffix while uploading (like up2k)\n* added type hints / mypy checking\n* upgrade deps (markedjs, codemirror)\n* ux improvements\n  * delay spinners a bit\n  * instant feedback when switching folders\n  * a11y outlines in up2k ui\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0522-1502  `v1.3.0`  god dag\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## new features\n* i18n! multilingual client\n  * new option `--lang nor` to set default language\n  * english and norwegian\n    * add your own language at the top of [browser.js](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/web/browser.js) and [splash.js](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/web/splash.js) and send a pr :^)\n  * build an english-only sfx with `./scripts/make-sfx.sh lang eng` (or `eng|nor` for english and norwegian)\n  * translation is incomplete but covers the most important / common stuff\n* show download progress while opening huge textfiles\n* add unix-extrafield to zipfiles for utc timestamps\n  * zip spec says the regular timestamp is supposed to be localtime :||||\n  * only helps on linux and will rollaround in 2038 but should be OK because the msdos field doesn't until 2100\n  * couldn't get ntfs-extrafields to work (supposed to be utc but idgi), would have been better, oh well\n* ux tweaks\n  * remember videoplayer preferences\n  * confirmation messages\n    * hiding a column for the first time\n    * opening a huge textfile\n    * destination in upload msg\n\n## bugfixes\n* dont switch to treeview when playback continues into the next folder\n\n## other changes\n* updated deps (markedjs, codemirror, prismjs)\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0513-1524  `v1.2.11`  big docs\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## bugfixes\n\nthis release fixes #9 (denial-of-service), thx to @chinponya for the report!\n\n* large files no longer embed if you `?doc=some.mkv`\n  * stops copyparty from eating all your RAM\n  * js will stream the file afterwards instead\n* disable selection of search results\n  * didn't serve a purpose, was just confusing\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0512-2344  `v1.2.10`  in addition\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## bugfixes\n* huge speed boost on huge databases (4'000'000+ files)\n  * improves initial tag scans when indexing new files\n  * should also improve directory listings, search results\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0512-2110  `v1.2.9`  monokai\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## new features\n* automatic logout after `--logout` minutes of inactivity\n* show originating path to dangerous symlinks during `--ls` validation\n\n## bugfixes\n* dont try to index nonregular files when scanning filesystem\n* start filesystem indexing even if no interfaces could bind\n* fix minor issues when using a symlink as webroot\n* fix filekeys in the basic-html browser\n* support login on ie4 / win3.11\n* restore minimal support for browsers without css-variables [(makes ie11 look surprisingly dope)](https://user-images.githubusercontent.com/241032/166340135-c59b9ced-5dbe-45d9-9025-285f0ffb5a49.png)\n\n## other changes\n* redirect to webroot after login instead of the controlpanel\n* improve readability of the upload dropzone for smaller screens\n* complain loudly if FFmpeg segfaults on a file\n  * grep your logs for `<Signals.SIG` to investigate\n* safer systemd service example\n* other minor ux fixes\n  * change focus in modals between ok/cancel with left/right keys\n  * removed the option to disable spa (nobody's mentioned any issues)\n  * compensate for play/pause fades by rewinding a bit\n  * focus the password field if not logged in\n  * [theme 2 is now monokai](https://user-images.githubusercontent.com/241032/168170566-bf71c3e0-d068-43cd-a277-f797184a702e.png) (the protonmail edition)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0430-0016  `v1.2.8`  windows++\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## new features\n* new themes `vice` and the windows 3.1 masterpiece `hotdog stand`\n\n<table><tr><td width=\"33%\" align=\"center\"><a href=\"https://user-images.githubusercontent.com/241032/165864907-17e2ac7d-319d-4f25-8718-2f376f614b51.png\"><img src=\"https://user-images.githubusercontent.com/241032/165867551-fceb35dd-38f0-42bb-bef3-25ba651ca69b.png\"></a>\n0. classic dark</td><td width=\"33%\" align=\"center\"><a href=\"https://user-images.githubusercontent.com/241032/165864904-c5b67ddd-f383-4b9e-9f5a-a3bde183d256.png\"><img src=\"https://user-images.githubusercontent.com/241032/165867556-077b6068-2488-4fae-bf88-1fce40e719bc.png\"></a>\n2. flat dark</td><td width=\"33%\" align=\"center\"><a href=\"https://user-images.githubusercontent.com/241032/165864901-db13a429-a5da-496d-8bc6-ce838547f69d.png\"><img src=\"https://user-images.githubusercontent.com/241032/165867560-aa834aef-58dc-4abe-baef-7e562b647945.png\"></a>\n4. vice</td></tr><tr><td align=\"center\"><a href=\"https://user-images.githubusercontent.com/241032/165864905-692682eb-6fb4-4d40-b6fe-27d2c7d3e2a7.png\"><img src=\"https://user-images.githubusercontent.com/241032/165867555-080b73b6-6d85-41bb-a7c6-ad277c608365.png\"></a>\n1. classic light</td><td align=\"center\"><a href=\"https://user-images.githubusercontent.com/241032/165864903-7fba1cb9-036b-4f11-90d5-28b7c0724353.png\"><img src=\"https://user-images.githubusercontent.com/241032/165867557-b5cc0010-d880-48b1-8156-9c84f7bbc521.png\"></a>\n3. flat light\n</td><td align=\"center\"><a href=\"https://user-images.githubusercontent.com/241032/165864898-10ce7052-a117-4fcf-845b-b56c91687908.png\"><img src=\"https://user-images.githubusercontent.com/241032/165867562-f3003d45-dd2a-4564-8aae-fed44c1ae064.png\"></a>\n5. <a href=\"https://blog.codinghorror.com/a-tribute-to-the-windows-31-hot-dog-stand-color-scheme/\">hotdog stand</a></td></tr></table>\n\n* `search:` button to load more search results, starting at 125 instead of 1000, now much better on slow PCs\n* `search:` immediately perform a search when the enter key is pressed\n* `uploader:` optimal column sizing in the uploader depending on which tab is selected (done/busy/queued)\n* `uploader:` new option `--turbo` to change the default settings of the turbo-mode in the uploader\n  * `0` (default) is the old behavior, `1` disables the warning when enabling turbo, `2` enables turbo, `3` also disables the datecheck\n  * see the tooltip in the settings tab for more info; basically it skips the file contents verification and instead relies on filesize and timestamp to guess if a file was uploaded already, useful for massive upload batches that got interrupted\n\n## bugfixes\n* `httpd:` a theoretical XSS opening -- copyparty would echo bad requests as html\n  * it still does that, but now with plaintext content-type\n  * was mostly-harmless -- can't really think of a way to exploit it since it'd only happen on invalid HTTP requests\n* `httpd:` better errorhandling on invalid requests in general\n* **windows-only:** `httpd:` deadlocks when trying to access files with illegal filenames on windows\n  * files containing characters `:*<|>\"/?\\` or names starting with `con.`, `prn.`, `aux.`, `nul.`\n  * for example `aux.c` when unpacking the linux source code on a flashdrive and plugging it into a windows rig\n* **windows-only:** `database:` deadlock if a search was done during the initial filesystem scan\n* `database:` deadlock if an upload was done during a filesystem scan (either initial or periodic rescan)\n* `client:` javascript crash when linking someone an audio URL and they'd never visited before\n* `client:` ignore bugs in the developer console (in future versions of chrome)\n* `uploader:` timestamps of zero-byte uploads were not set\n* `database:` skip busy files during a filesystem rescan\n* `media player:` sending artist / title info to the OS broke at some point\n\n## other changes\n* changed the themes to use css variables for colors, making it way easier (hopefully) to make your own themes\n* mention [chrome issue 1317069](https://bugs.chromium.org/p/chromium/issues/detail?id=1317069) in the readme\n* improved the `--help` text\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0416-2144  `v1.2.7`  write-only unpost\n\nfixed another dumdum, sorry for the spam\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# bugfixes\n* allow unpost with write-only permissions\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0415-1809  `v1.2.6`  hardlink\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# new features\n* new arg `--hardlink` tries to hardlink instead of symlink when receiving a duplicate file through up2k\n* new arg `--never-symlink` disables the fallback to symlink if hardlink fails, making a full dupe\n  * `--no-symlink` was renamed to `--no-dedup`\n\n# bugfixes\n* some css color issues introduced in v1.2.4, mainly in markdown documents\n* setting mtimes / last-modified on up2k uploads when running on windows\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0414-1945  `v1.2.4`  the thumbs and themes update\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# new features\n* gallery URLS -- copy the URL while viewing an image/video in the gallery\n* option to change/disable the gallery animations in the UI\n  * default from OS preferences through `prefers-reduced-motion`\n* decode terminal colors when viewing `diz`, `ans`, `log` textfiles\n* thumbnails:\n  * option to use `pyvips` instead of (or in addition to) `pillow`, 3x faster than pillow\n  * add `ffmpeg` as fallback for creating thumbnails of pictures too, 3x slower than pillow\n    * so now it can read jpeg-xl files + a bunch more\n      * including pdf which is disabled by default because scary\n  * new args to specify which file formats to read using which backend\n    * `--th-r-pil`, `--th-r-vips`, `--th-r-ffi`, `--th-r-ffv`, `--th-r-ffa`\n  * new arg `--th-dec` specifies backend preference, default `pyvips` > `pillow` > `ffmpeg`\n  * volflags to disallow thumbnails inside specific volumes\n    * `dvthumb` for video, `dathumb` for audio, `dithumb` for pics, `dthumb` to disable all\n  * try to detect and adjust for missing ffmpeg features\n    * adds `--th-ff-jpg` and `--th-ff-swr` when necessary but it breaks the first few thumbs\n* flat theme, selectable in the settings tab\n  * new arg `--theme` sets default theme, default 0 = old dark theme\n  * new arg `--themes` adds more theme buttons to the UI if you've included your own theme through `--css-browser`\n\n# bugfixes\n* more aggressively prevent systemd from deleting the sfx from `/tmp` while copyparty is running\n* javascript crash if media player settings were changed without music playing\n\n# other changes\n* add `mpc`/musepack to known audio formats (for streaming and spectrogram thumbnails)\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0324-0135  `v1.2.3`  the ancient ones\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# new features\n* browser-client: never give up on a failed upload -- keep retrying every 30sec\n\n# bugfixes\n* files with last-modified older than 1980-01-01 didn't make it into zip downloads\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0320-0515  `v1.2.2`  dont crawl me bro\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# new features\n* options to tell crawlers / search engines you dont wanna be indexed\n  * either globally with `--no-robots` or single volumes using volflag `norobots`\n  * allow crawlers inside a volume with volflag `robots`\n  * or just use [robots.txt](https://www.robotstxt.org/robotstxt.html) like usual ( ´ w `)\n* `--force-js` disables plain HTML folder listings, making things harder for crawlers who ignore the hints\n  * internet explorer 9 is the oldest surviving browser\n* `--html-head` to append additional HTML to the `<head>` section of all pages\n\n# bugfixes\n* inaccurate server URLs displayed on startup\n  * correct protocol based on port / `--http-only` / `--https-only`\n  * Windows: ignore interfaces with no ethernet cable connected\n  * Windows: show URLs for all IPs on each interface\n  * Linux: show link state next to URLs\n* reset console color on exit\n\n# other changes\n* show name of open document in page title\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0303-0026  `v1.2.1`  ikke den men denja\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# new features\n* plaintext volume listings at http://127.0.0.1:3923/?h&ls=v\n\n# bugfixes\n* search: support negative queries / subtracting tags from searches\n  * you can put stuff like `gura -kagura` in the tags field\n  * also the `raw` field supports `and/or/not` for more complex stuff such as\n    ```\n    ( tags like *nhato* or tags like *taishi* ) and ( not tags like *nhato* or not tags like *taishi* )\n    ```\n* [prisonparty](https://github.com/9001/copyparty/blob/hovudstraum/bin/prisonparty.sh): clean shutdown when used as a service\n* ftp server now runs on python2 as well\n  * ftps does not\n\n# other changes\n* higher debounce for searches\n* slightly more padding in the files table\n* added asyncore/asynchat into the sfx to (hopefully) support running the ftp server in python 3.12 when that releases late 2023\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0213-1558  `v1.2.0`  ftp btw\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# new features\n* ftp server\n  * built on [pyftpdlib](https://pypi.org/project/pyftpdlib/)\n    * plaintext (`--ftp`), and/or...\n    * FTPES / explicit-TLS (`--ftps`)\n    * active or passive, as client prefers\n  * upload, download, accounts (read / write / move / rename / delete)\n  * does NOT have resumable uploads -- delete and reupload as necessary\n  * integrated with up2k\n    * uploaded files are indexed into the database\n    * unpost is available (delete your own recent uploads)\n* `--s-wr-slp` now rate-limits file uploads as well, in addition to downloads\n* `--srch-hits` sets the max number of search results, defaults to 1000 (same as before)\n* ctrl-click `-txt-` links to open the document viewer in a new tab\n* log terse checksum of uploaded files\n\n# bugfixes\n* file-search: path queries didn't include the volume prefix/mountpoint\n* ie11 could throw exceptions on keystrokes\n\n# other changes\n* finally deprecated `copyparty-sfx.sh`\n* update some dependencies\n  * marked `4.0.10` -> `4.0.12` fixes minor table formatting issues\n  * easymde `2.15.0` -> `2.16.1`\n  * codemirror `5.64.0` -> `5.65.1`\n\n# notes\n* the ftp server is not compatible with python 3.12 (releasing october 2023)\n  * will be fixed in a [future version of pyftpdlib](https://github.com/giampaolo/pyftpdlib/issues/560)\n\nthe sfx was built from https://github.com/9001/copyparty/commit/39e7a7a2\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0118-2128  `v1.1.12`  i should stop adding bugs\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# bugfixes\n* fix PUT response in write-only folders (broke in v1.1.11)\n\n# other changes\n* [prisonparty](https://github.com/9001/copyparty/blob/hovudstraum/bin/prisonparty.sh):\n  * fix examples \n  * support running from source\n* [mtag-install-deps](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/install-deps.sh):\n  * fix downloading tarballs from github (they stopped returning content-dispositions)\n  * build vamp-sdk from source if unavailable\n* forgot to mention [partyjournal](https://github.com/9001/copyparty/blob/hovudstraum/bin/partyjournal.py):\n  * was a new feature in v1.1.11\n  * shows a history of all uploads within a volume by reading the up2k db\n  * can replace IPs with nicknames if provided as arguments\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2022-0114-2125  `v1.1.11`  chromecast?\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# new features\n* include file-url in PUT responses\n  * to support the [android app](https://github.com/9001/party-up/)\n* main-tabs have links and are linkable which would have been a great help [before the android app existed](https://user-images.githubusercontent.com/241032/147699835-16101690-aab1-49da-a3cc-d16759808af5.jpg)\n\n# new plugins (disabled by default)\n* [very-bad-idea.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/very-bad-idea.py) and [meadup.js](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js) which together turns a raspberry pi into a janky yet extremely flexible chromecast clone\n  * anything uploaded through the app (files or links) are executed on the server\n  * adds a virtual keyboard by @steinuil to the basic-upload tab\n  * dedicated to extremely particular occasions where randomly evaluating code is A-OK\n    * sweden-approved software\n\n# bugfixes\n* return own external ip as `Host:` if `Host:` is not provided by client\n* correct clipboard actions available when jumping between permission levels\n* markdown converter accidentally using a broken ie11 shim on all browsers\n* changing the sort-order in the file listing didn't affect the thumbnail view\n\n# other changes\n* upgrade marked.js to 4.0.10\n  * fixes misc rendering bugs\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-1216-2305  `v1.1.10`  chill\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# bugfixes\n* patiently wait when clients stop consuming data\n  * fixes connections going bad when streaming movies or music\n  * only affects sendfile, meaning reverse-proxied and non-https connections\n* try FFmpeg when mutagen partially fails to parse a file (not just when it throws)\n\n# other changes\n* add [multisearch.html](https://github.com/9001/copyparty/blob/hovudstraum/docs/multisearch.html), applying a search template to a list of filenames\n  * the currently only example grabs youtube-IDs and finds all related files for that ID\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-1210-0144  `v1.1.8`  merry xmas\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# new features\n* folders are colored blue when using `?ls=v` to list stuff in a terminal\n* add folder breadcrumbs inside the textfile navpane\n\n# bugfixes\n* folder breadcrumbs (the non-navpane ones) glitching out while viewing textfiles\n* give 404 instead of 500 when accessing `/.cpr`\n\n# other changes\n* expose some more state from the up2k client to ease debugging\n  * for example to find out that firefox94 cannot read files bigger than 2 GiB when compiled with musl\n* updated the [alternative fuse client](https://github.com/9001/copyparty/blob/hovudstraum/bin/copyparty-fuseb.py) so it kinda works again\n  * still no reason to use that instead of the [main client](https://github.com/9001/copyparty/blob/hovudstraum/bin/copyparty-fuse.py)\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-1207-1819  `v1.1.7`  two wrongs\n\n[v1.1.5](https://github.com/9001/copyparty/releases/tag/v1.1.5) and [v1.1.6](https://github.com/9001/copyparty/releases/tag/v1.1.6) were pretty busted, sorry bout that\n(so much for stable eh)\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# known problems / todo\nso far just mild annoyances, nothing bad\n* clicking breadcrumbs with the textviewer open will navigate correctly but messes up the breadcrumbs\n* server throws an exception when accessing `/.cpr`\n* up2k should expose `st` for easier debugging\n\n# bugfixes\n* search-results ui\n  * selecting / playing audio results broke in v1.1.5\n  * and playing audio tracks in search results would clobber the search URL but that has always been a thing\n* only show unique IPs in the window-title\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-1207-0017  `v1.1.6`  not copyparty\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# new features\n* option `--doctitle` changes the titles in the web-ui from \"copyparty\" to something else\n* option `--wintitle` sets the console window-title, defaults to the primary/external IP\n* volume-flags [`d2ds` and `d2ts`](https://github.com/9001/copyparty#file-indexing) to selectively disable on-boot indexing for some volumes\n* support funky linux distros (with no `~/.config` and read-only `/tmp` such as recent Termux builds)\n\n# bugfixes\n* last release broke folder listings if you left off the trailing slash in the url\n  * also fix the markdown-editor breadcrumbs which made that very obvious\n* when running without `-e2d`, don't proactively create symlinks for dupe uploads\n  * prevents the client from accidentally pushing superflous links\n* ui didn't update correctly when navigating into a folder with indexing disabled\n\n# other changes\n* less indentation of outermost lists in the markdown viewer\n* update some dependencies\n  * marked `3.0.4` -> `4.0.6` fixes a performance regression in huge documents\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-1204-0233  `v1.1.5`  certified spa\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# new features\n* much faster navigation when the navpane is closed (no more full reloads)\n* sort-order preference also applies to the initial listing now, #8 \n* sort-order indicators in the grid and list views\n* symlinks (duplicate uploads) now keep the uploader's timestamps\n* panic-button in the control panel to reset all browser settings\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-1128-0322  `v1.1.4`  enter the lab\n\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# new features\n* quoted searches, for stuff like \"[more more more](https://www.youtube.com/watch?v=bgVRGmOK4SM)\"\n* upload ETA in the browser window title\n* audio-player stays open on navigation\n* thumbnails indicating whether clicking an audio file will start playing it (when the audio-player is open) or not\n* mtp plugin [image-noexif](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/image-noexif.py) removes EXIF from uploaded images\n* when running on windows; disable quickedit so cmd.exe doesn't pause the server if you accidentally click the console window\n  * option `--keep-qem` disables disabling it\n\n# bugfixes\n* forcing specific compression levels using volume-flag `pk`\n* mtp plugins [audio-bpm](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/audio-bpm.py) and [audio-key](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/audio-key.py) couldn't open files with mojibake / corrupt filenames\n\n# other changes\n* uploading files by dragging them into the browser using a computer from before 2009 should have zero delay now\n* workaround for a chrome bug (appeared in chrome 96, fixed in 98) where dragging a link would activate the uploader\n* mention in the readme that enabling the audio equalizer, with all values at zero, makes gapless albums fully gapless\n* better error messages in the [standalone up2k upload client](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py)\n* mirror at [gitlab](https://gitlab.com/9001/copyparty/-/releases) since github has been down a lot lately\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-1120-0127  `v1.1.3`  CoreAudioFormat aight yeah okay\n\nnot super important but recommended\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# known problems\n* **streaming compression of uploads:** optional arguments to volume-flag `pk` don't work, so you can only force-enable compression without specifying an exact algorithm (gz/xz) and level (0-9), instead letting the client choose a preference -- default is `gz,9`\n\n# new features\n* automatically enable transcoding for unsupported audio codecs (aac/m4a in some chromium builds)\n* audio-player: gapless albums are even closer to gapless now\n  * especially on iOS devices as they generally ignore preload hints\n  * on all other browsers, opus appears to perform better than other codecs (noice)\n* added a tooltip delay, and a hint next to the mouse-cursor for instant feedback\n* new button in the control-panel, `enable k304` which kills the http connection on every `304 Not Modified` response\n  * avoids a bug in some browsers (ie11) and webproxies (squid maybe?) which *sometimes* get stuck, expecting data after the header\n* enable up2k-registry serialization when running without `-e2d` / sqlite, so incomplete uploads can be resumed after a server restart\n* include both the hex and base64 sha512 representations in upload responses\n* [standalone up2k upload client](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py): option `--ok` ignores any inaccessible files/folders and starts the upload anyways\n* option `--rsp-slp` adds a synthetic delay to client responses\n\n# bugfixes\n* up2k-webclient: could crash if two browser-tabs uploaded the same chunk simultaneously\n  * mostly harmless but you'd have to reload the tab to fix it + manually resume the upload\n* buggy behavior when python was compiled without sqlite3 (default on freebsd)\n  * memory usage would grow infinitely as more files were uploaded\n  * exceptions sent to the client when trying to search\n* add timeouts to FFmpeg operations, preventing invalid files from eating the `--th-mt` threads\n  * 10 seconds for filetype / metadata parsing\n  * 60 seconds (`--th-convt`) for thumbnails and audio transcoding\n* up2k-webclient: fix an inconvenient priority inversion when turbo/yolo was enabled\n\n# other changes\n<table><tr><td><a alt=\"screenshot of an iPod displaying the lockscreen controls for the copyparty audio player\" href=\"https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png\"><img src=\"https://user-images.githubusercontent.com/241032/142711927-3e554cc3-01d0-4b46-adb1-a3e82a0870ef.png\" /></a></td><td>\n<p>replaced <code>ogv.js</code> with serverside rewrapping of opus files into the appropriate apple-proprietary container</p>\n<ul>\n<li>sfx size is now 190 KiB smaller</li>\n<li>feature-wise, this <b>only affects iOS devices</b> (iPhones, iPads, iPods)</li>\n<li>no more opus decoding in javascript! now uses the native opus decoder instead</li>\n<li>enables OS media controls since apple finally added <code>mediaSession</code> in iOS 15\n\t<ul><li>play/pause doesn't work too well, probably fixed in a future iOS version</li>\n\t<li>artist/title tags can suddenly become the filename (another iOS bug)</li></ul>\n</li>\n<li><b>disables</b> the in-browser volume control because <a href=\"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\">apple demands it</a>, can't be helped</li>\n<li><b>disables</b> support for <code>ogg/vorbis</code>, only opus is playable without transcoding\n\t<ul><li>vorbis is transcoded to opus automatically, but this causes a quality loss</li></ul>\n</li>\n<li>audio-equalizer is broken for opus and all other 48khz audio files because apple made <code>AudioContext</code> hardcoded to 44100 hz\n\t<ul><li>makes the iPhone X buffer-overflow, all audio dies after ~2 minutes</li>\n\t<li>also ruins the common workaround for apple disabling volume controls</li></ul>\n</li>\n<li>gets rid of the silly sinewave generator which tricked iOS into letting the tab continue playing in the background</li>\n</ul></td></tr></table>\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-1112-2208  `v1.1.2`  mind the gap\n\n* latest important update: **this one**? kind of\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n# new features\n* navigate into textfiles using hotkeys (`v, k`)\n* close various UI elements by repeatedly hitting the Escape key\n* doubleclick files/folders to open them (in the grid view, when multiselect is enabled)\n* `--s-wr-slp` sets a delay between socket writes, simulates a slow network during downloads\n* `--s-wr-sz` sets socket write size, default 256 KiB (was hardcoded 32 KiB until now)\n  * this increase download speed by ~50% (to around 3 GiB/s) when running on windows / where sendfile is unavailable\n\n# bugfixes\n* when uploading two files with the same name and size, only the first file got uploaded\n  * so now it's also possible to upload the same files you just searched for without the refresh jank\n  * discovered thanks to rockylinux serving the same package in multiple pools, nice\n* when full-preload is enabled, also do regular preloading so the decoder has a chance to prepare (fixes gapless playback)\n  * and kill the preloaders if they don't finish in time so free up network\n* additional preloading fixes for ogv.js, only affecting **apple devices** when playing ogg/vorbis/opus audio:\n  * disable full-preload since ogv skips the browser cache somehow\n  * swap between the ogv instances to preserve cached audio\n  * still a bit of silence left between tracks as the decoder boots up but that is the price you have to pay for using proprietary garbage\n* `ctrl-a` now only selects the text within the focused codeblock in text documents\n* minor correctness fix regarding chunked uploads\n* avoid crc32 collisions in filenames\n  * affected the media player and file selection, but [was unlikely to happen](https://i.stack.imgur.com/u4DeG.png)\n\n# other changes\n* prefer fpool on linux as well, since btrfs and zfs (and probably others) perform better with it\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-1108-2139  `v1.1.1`  firefox v92 broke the clipboard\n\n* latest important update: [v1.0.8](https://github.com/9001/copyparty/releases/tag/v1.0.8)\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## upgrade notes\n* clipboard protocol changed -- `F5` your browser-tabs before moving any files with `ctrl-x` + `ctrl-v`\n\n# new features\n* option to preload the entire next song when approaching end-of-track\n  * new button in the audioplayer options panel\n  * should help with spotty but fast connections\n  * *(probably does more harm than good on slow ones)*\n\n# bugfixes\n* [firefox v92 broke clipboard sync](https://bugzilla.mozilla.org/show_bug.cgi?id=1740144), so moving files between browser-tabs didn't work too well\n\n# other changes\n* adjusted the fallback spectrogram generator to better match the preferred one\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-1106-2227  `v1.1.0`  opus\n\n* latest important update: [v1.0.8](https://github.com/9001/copyparty/releases/tag/v1.0.8)\n* latest gzip edition of the sfx: [v1.0.14](https://github.com/9001/copyparty/releases/tag/v1.0.14#:~:text=release-specific%20notes)\n\n## upgrade notes\n* you can use `--no-reload`, `--no-acode`, `--no-athumb` to disable the new features described below\n\n# new features\n* **audio transcoder**\n  * hipster audio formats are transcoded to opus on-demand\n    * `aac` `m4a` `flac` `alac` `mp2` `ac3` `dts` `wma` `ra` `wav` `aif` `aiff` `au` `alaw` `ulaw` `mulaw` `amr` `gsm` `ape` `tak` `tta` `wv`\n  * because kipu wanted to play his `.au` bangers from 1993\n  * needs FFmpeg and FFprobe, can be disabled with `--no-acode`\n* **audio spectrograms**\n  * are shown as thumbnails for audio files\n  * supported formats: same as transcoder + `mp3` `ogg` `opus`\n  * needs FFmpeg and FFprobe, can be disabled with `--no-athumb`\n* **textfile viewer**\n  * with syntax hilighting\n    * can be disabled by deleting `web/deps/prism.js.gz` or building the sfx with `no-hl`\n  * and list of textfiles in the navpane; toggle with hotkey `v`\n* **navpane context dock**\n  * snap parent folders into a panel to keep track in huge folders\n  * toggle-button to disable it in the navpane toolbar\n* **config reload**\n  * SIGUSR1 reloads the config files\n    * the [systemd example](https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.service) has been updated with `ExecReload`\n  * only does accounts, volumes, and volflags -- so any changes to args still require a full restart\n  * also available as a button in the control panel\n    * can be disabled with `--no-reload`\n* option to specify args (command-line arguments) in the config file\n* url parameter `?txt` to return file as utf-8 text\n  * or `?txt=iso-8859-1` to set a specific encoding\n* url parameter `?mime=text/html;charset=shift_jis` to request a specific response mimetype\n* [service script for freebsd](https://github.com/9001/copyparty/blob/hovudstraum/contrib/rc/copyparty), thx @kipukun \n\n# bugfixes\n* [standalone up2k upload client](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) was showing https warnings with `-td`\n* trailing newline missing in `?ls=t` and `?ls=v`\n* add a bunch of known mimetypes to help ms-windows a bit\n* lowercase all content-type charsets (firefox became case-sensitive at some point)\n* example for giving multiple users the same permission-set using config files did not actually work\n\n# other changes\n* navpane is enabled by default on sufficiently large displays\n* audio-player preload increased from 10 to 20 sec, giving the opus transcoder some time\n* finally removed the deprecated `-e2s` option after 9 months (replaced by `-e2ds`)\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-1029-2237  `v1.0.14`  party donuts\n\n* latest important update: [v1.0.8](https://github.com/9001/copyparty/releases/tag/v1.0.8)\n* latest gzip edition of the sfx: **this ver!**\n\n## argv changes\n* `--th-mt 0` no longer means «*use all CPU cores*», however using all cores is (and was) the default when leaving it unset\n* `--re-int` no longer serves a purpose and was removed (it is automatically inferred)\n* `--no-mtag-mt` was replaced by `--mtag-mt 1` to allow setting exact core counts\n\n## new features\n![copyparty-donut](https://user-images.githubusercontent.com/241032/139513444-c22fc17a-6f44-4308-9cb0-ab191e40660b.png)\n* up2k tab (and favicon) become a donut / progress-ring while uploading / searching\n  * favicon becomes ETA when less than 99sec remains and ETA is sufficiently stable\n* tag scanning is now multithreaded for recent uploads as well, like the initial scan is/was\n* url parameter `?ls=t` returns a plaintext directory listing, and `?ls=v` adds terminal colors\n* less cpu wakeups! *conserve electricity and be power smart :^)*\n* add refresh and logout buttons to the control-panel\n* try to catch and warn about some common config mistakes\n* when launched without arguments: try to use port 80 and 443 by default on windows (and when running as root)\n\n## bugfixes\n* couldn't delete empty folders\n* spacebar now triggers the OK/Cancel buttons in modal popups\n* navpane didn't have locale-aware sorting like the file listing does\n* uploading a blank file would glitch the browser tab until the next page refresh\n* the [standalone up2k client](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) tried to mimic rsync behavior for source folder selection but had it the other way around\n* if files were deleted while scanning for tags, the file hash was permanently marked as not having tags\n* if some endpoints fail to bind, don't print them as \"available\" during startup\n* navpane scroll glitch when loading new folders\n* toast-positioning in ie11\n\n## other changes\n* truncate file \"extensions\" longer than 16 characters\n* remove the multiprocessing warning on startup since it's mostly confusing\n* mention selinux (fedora/centos/rhel-specific) setup steps in the systemd example\n* new cheatcode in the javascript repl (bottom-left pi symbol) which turns all file links into download links\n\n## release-specific notes\nthis release includes two additional sfx builds:\n* [copyparty-enterprise.py](https://github.com/9001/copyparty/releases/download/v1.0.14/copyparty-enterprise.py) was built with `./scripts/make-sfx.sh re no-sh no-dd no-ogv`, removing `ogv` (the iOS ogg/opus/vorbis audio decoder) and `dd` (the audio-tray mouse cursor) to save some space\n* [copyparty-sfx-gz.py](https://github.com/9001/copyparty/releases/download/v1.0.14/copyparty-sfx-gz.py) was built with `./scripts/make-sfx.sh re no-sh no-dd no-ogv no-cm gz`, also removing `cm` (the codemirror-based markdown editor), but more importantly using gzip compression rather than the usual bzip2, mostly useful for smoketests on feature-reduced python builds and embedded platforms\n\nfor future releases, you can use a script to automatically grab the latest sfx and create the two additional builds:\n* download and run [copyparty-repack.sh](https://github.com/9001/copyparty/blob/hovudstraum/scripts/copyparty-repack.sh) on either linux, macos, or windows-msys2\n* the two additional builds in this release are `sfx-ent/copyparty-sfx.py` and `sfx-lite/copyparty-sfx-gz.py` -- see [sfx-repack](https://github.com/9001/copyparty#sfx-repack) for more info\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-1024-1906  `v1.0.13`  css fix\n\n* latest important update: [v1.0.8](https://github.com/9001/copyparty/releases/tag/v1.0.8)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## bugfixes\n* currently-playing song didn't hilight correctly\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-1024-0112  `v1.0.12`  some polish\n\n* latest important update: [v1.0.8](https://github.com/9001/copyparty/releases/tag/v1.0.8)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## upgrade notes\n* [minimal-up2k.html](https://github.com/9001/copyparty/blob/hovudstraum/docs/minimal-up2k.html) has changed slightly, [diff](https://github.com/9001/copyparty/commit/d77ec2200781cc1d381a074831c0bffc749e835d#diff-8b665a140ab1a0dde9b487df3b60ba38718253dddc3c7e3513eec5116ab6c11e)\n\n## new features\n* better thumbnail caching\n  * 1 week expiration time\n  * persist the webp-support test results for faster init\n* add `--js-browser` to add custom javascript\n* hop into subfolders from the file-list without doing full reloads\n  * still does a full reload if navigating up to the parent folder, so use the navpane for that\n* support searching on ie9\n\n## bugfixes\n* thumbnail toggle didn't take effect until the next navigation\n* file indexing when mounting an entire disk on windows\n\n## other changes\n* general ux improvements\n  * reflow the up2k panel for superwide screens\n  * make the \"close search results\"  button more obvious\n  * banner over inlined readme files\n* some cleanup of the dark theme\n  * visible panels (for the navpane etc)\n  * thumbnail alignment\n\nthx to @Bevinsky and @icxes for the ux suggestions\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-1018-2310  `v1.0.11`  jeg fant jeg fant\n\n* latest important update: [v1.0.8](https://github.com/9001/copyparty/releases/tag/v1.0.8)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* search results are now shareable URLs\n* optionally provide a filename when uploading with PUT or `?raw` POST\n  * add a trailing slash to the URL to autogenerate a filename like before\n  * and `?raw` POST without content-type is now allowed\n* file-listing is refreshed when all up2k uploads complete\n* new option `--ign-ebind` to continue startup even if one of the IPs / ports couldn't be listened on\n* new option `--ign-ebind-all` to run even if copyparty can't receieve any connections at all\n  * maybe useful for monitoring folders and hashing new files on a timer or something\n\n## bugfixes\n* unpost in jumpvols (inside `/foo/bar/` if `/foo/` and `/foo/bar/qux/` are volumes)\n* u2cli: aggressive flushing to show uploaded files in realtime\n\n## other changes\n* replaced the \"press button to play music\" splashpage with a regular modal\n* replace `:` with `.` in filenames from ipv6 clients\n* volume listing on the frontpage is sorted alphabetically\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-1011-2343  `v1.0.10`  favicon\n\n* latest important update: [v1.0.8](https://github.com/9001/copyparty/releases/tag/v1.0.8)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## breaking changes\n* the argument `--no-hash` and volume-flags `dhash`, `ehash` (booleans) have been replaced with regex patterns; continue reading below\n\n## new features\n* optional favicon! configurable client-side in the `[⚙️]` config tab\n  * the selected favicon is remembered per-server (good for keeping track of tabs)\n* new argument `--no-idx '\\.iso$'`, also available as volume-flag `[...]:c,noidx=\\.iso$`\n  * every filepath matching the given regex (`iso$`) will be ignored/skipped during indexing\n  * uses OS-defined separators, so use `\\\\` as path-separator on windows\n* \"new\" argument `--no-hash foo` and volume-flag `[...]:c,nohash=foo`\n  * like `--no-idx`, but it only skips the file-contents indexing, so filename/path/size is still searchable\n  * this replaces the boolean `--no-hash` and volume-flags `dhash`, `ehash`\n\n## bugfixes\n* fix ui race-condition (mkdir with navpane closed)\n* mkdir was broken on python 2.7 since [v0.12.1 (july 28)](https://github.com/9001/copyparty/releases/tag/v0.12.1)\n* try to support some buggy python builds (invalid ffi symbols)\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-1009-2029  `v1.0.9`  cirno reference\n\n* latest important update: [v1.0.8](https://github.com/9001/copyparty/releases/tag/v1.0.8)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* readme: [run a program when a file is uploaded](https://github.com/9001/copyparty#upload-events)\n  * add `-mtp` support for non-python programs\n* better performance in the `-e2ds` filesystem indexer, particularly for samba/nfs shares\n* support clients with read-only `localStorage` (private-browsing on certain iOS versions according to MDN)\n\n## bugfixes\n* a case of symlink-loops not being detected during `-e2ds` filesystem indexing\n* #4 fixes incorrect protocol in the basic-upload response, thx Daedren\n* flickering when refreshing the browser in lightmode\n* sfx-repack: fix `no-dd` also disabling the loader animation by producing a bit of css with invalid syntax\n\n## other news\n* the [standalone up2k upload client](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) can detect and skip existing files much faster than the regular web client if you give it `-z`\n  * (not part of this release, grab it from the link)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-1004-2050  `v1.0.8`  1.0.8 sketches\n\n* latest important update: **this ver** (if you have non-https users)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## new features\n* [portable / standalone up2k upload client](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) now included in the pypi package, [readme](https://github.com/9001/copyparty/tree/hovudstraum/bin#up2kpy) / [webm](https://ocv.me/stuff/u2cli.webm)\n* empty / zero-byte files can now be uploaded\n* up to 20 results are listed for filesearches, rather than just 1\n* audio player progressbar now has textlabels next to the minute markers\n* new argument `--vague-403` makes copyparty reply with 404 (not found) when it's actually a 403 (permission denied), which was the entirely-too-confusing default behavior for versions `1.0.3` through `1.0.7`\n* new mtp plugin [cksum.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/cksum.py) generates various checksums\n\n## bugfixes\n* race-condition initializing the up2k-client when dropping files into the browser and you're not using https\n* hilight active folder in the navpane even when the browser and copyparty disagrees on how to urlencode\n* hide prologue/epilogue while search results are open\n* toasts could redefine css\n\n## other changes\n* better focus outlines\n* less verbose debug toasts\n* dropzones more obvious at a glance / in a rush\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0926-1815  `v1.0.7`  pool party\n\n* latest important update: [v1.0.3](https://github.com/9001/copyparty/releases/tag/v1.0.3)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## new features\n* [portable / standalone up2k upload client](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py): early beta, apparently faster than browsers, [readme](https://github.com/9001/copyparty/tree/hovudstraum/bin#up2kpy) / [webm](https://ocv.me/stuff/u2cli.webm)\n* up2k: fully parallelized handshakes and uploads\n  * uploading smol files is way faster now\n  * some files may temporarily display as \"failed\" until all uploads complete\n* browser: `mkdir` and `msg` can be used during uploads (no longer does a full page reload)\n* up2k: option to keep destination files open during uploads (fd pool)\n  * on windows: default-ON, due to Microsoft Defender \"real-time protection\" being hella expensive\n  * on linux/macos: default-OFF, but can be enabled with `--use-fpool` for things like nfs\n* up2k: new option `--no-symlink` to fully dupe files instead of adding symlinks\n* add minimal support for some more eccentric browsers (including Hv3)\n\n## bugfixes\n* up2k: check all dupes for a matching filesystem path\n  * prevents duplicate symlinks if the same dupe is repeatedly uploaded to the same place\n* don't crash the tag collector thread if there are invalid tags\n* up2k-client: don't DDoS the server if the http response is invalid\n* when running without `-e2d`, recently uploaded files could not be deleted\n* on windows, absolute filesystem-paths could appear in exceptions sent to the client\n* misc url escaping fixes, mostly regarding files/folders where name contains `?`\n* sort-order being reset if you visit an empty folder\n\n## other changes\n* moved the up2k fence-toggle into the settings pane since probably nobody uses it\n* readme: add a section on recovering from [client crashes](https://github.com/9001/copyparty#client-crashes)\n  * firefox (the whole browser and all its tabs) can crash during upload\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0919-1311  `v1.0.5`  one more\n\n* latest important update: [v1.0.3](https://github.com/9001/copyparty/releases/tag/v1.0.3)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## bugfixes\n* basic-upload into `fk` (accesskey-enabled) folders\n  * affected sharex, scripts, old browsers\n  * files were uploaded correctly but the reply from copyparty was garbage\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0918-2241  `v1.0.4`  early bird gets the bugs\n\n* latest important update: [v1.0.3](https://github.com/9001/copyparty/releases/tag/v1.0.3)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## bugfixes\n* folders and volumes being out-of-order in the initial listing\n* it was possible to shrink the navpane so much that the shrink/grow buttons disappeared\n* a bunch of features stopped working in folders where `fk` (per-file accesskeys) was enabled\n\n## other changes\n* increased cache timeout for static resources\n* can no longer open the markdown editor without write-access\n* the argument parser can handle multiple volume flags in one group now, so `c,e2ds,dupe` instead of `c,e2ds:c,dupe`\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0918-1550  `v1.0.3`  unlisted\n\n* latest important update: **this one**\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## known bugs\n* on phones, it is *possible* to make the navpane so small that the resize buttons disappear\n  * happens if you navigate into a folder 7+ levels deep, reduce the navpane size so the `a` button is barely visible, then disable `a`\n  * **fix:** open the js prompt (click the bottom-left `π`) then execute `,.` (comma dot) and click `reset settings`\n\n## new features\n* new permission `g`: read-access only if you know the full URL to a file; folder contents are hidden, cannot download zip/tar\n* new volume flag `fk`: generate per-file accesskeys, which are then required by `g` users to access files, making it harder to bruteforce URLs\n  * users with full read-access can see the accesskeys appended to the URLs when browsing folders\n* [wget.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/wget.py): download files to the copyparty server by POSTing file URLs in the web-UI\n* show a login prompt on 404/403 pages\n* option to disable wordwrap in the navpane\n\n## bugfixes\n* loss of access to anon-read/write folders after logging in\n  * affected filesearch, regular searching, and volume listings\n* more aggressively `no-cache`, preventing cloudflare from eating api calls\n* after deleteing all files inside a folder, don't delete the folder itself\n  * was intended behavior but fairly confusing\n* don't reshow tooltips when alt-tabbing\n* accessibility: always hilight focused things\n* markdown-editor modification poller doesn't cause performance issues after having a document open for several months\n* mtp plugins [audio-bpm.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/audio-bpm.py) and [audio-key.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/audio-key.py) explicitly asks for just the first audio stream, which prevents ffmpeg from transcoding video (nice)\n\n## other changes\n* updated some web-deps\n  * marked: `v1.1.0` -> `v3.0.4` (with modifications)\n  * easymde: `v2.14.0` -> `v2.15.0` (with modifications)\n  * codemirror: `v5.59.3` -> `v5.62.3` (with modifications)\n  * hashwasm: `v4.7.0` -> `v4.9.0`\n* easymde uses the external `marked.js` to save some space\n* README.md has the same maxwidth as in the viewer/editor\n* show a toast if there's an unhandled promise reject\n* markdown-editor shows the current line number\n* cfssl.sh (certificate generator) asks for fqdn instead of inventing something\n* sfx binaries try to use python3 explicitly since a lot of distros don't have a /usr/bin/python at all\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0909-0721  `v1.0.2`  it is still 9/9\n\nblessed by the strongest, *this will surely be the final version*\n* latest important update: [v1.0.1](https://github.com/9001/copyparty/releases/tag/v1.0.1)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## bugfixes\n* audio equalizer (broke in v1.0.1)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0908-2259  `v1.0.1`  happy 9/9\n\nblessed by the strongest, this will surely be the final version\n* latest important update: **this one**\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## new features\n* click an open tab to close it (thx daniiooo)\n\n## bugfixes\n* multipart POSTs could get incorrectly rejected with `protocol error after field value`\n  * had a `0.14%` chance of happening (worst-case; 1400 mtu, 2 offsets)\n  * affected stuff like saving markdown documents, renaming files, ...\n  * did **not** affect file uploads, and reverseproxy probably helped prevent it\n* filedrop UI could let you try to upload/search without the necessary permissions\n  * purely cosmetic, would immediately fail with a slightly cryptic error message\n* apply a different equalizer tuning for some browsers\n  * some permutations of chrome and win10, and also some phones, have incorrect Q scaling at higher frequencies, causing treble to be massively boosted\n  * now tries to detect this by sampling the frequency response at 15khz and setting different gains (less dangerous than touching Q)\n\n## other changes\n* search ui does not initiate searches as eagerly if the textbox has a very short value\n  * helps prevent overloading slow browsers with accidental wildcard searches\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0907-2118  `v1.0.0`  sufficient\n\nwe did it reddit 👉😎👉\n* latest important update: [v0.13.5](https://github.com/9001/copyparty/releases/tag/v0.13.5)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## known bugs (all harmless)\n* the website may let you attempt to upload stuff without write-access\n  * fails gracefully with an error-message so it's all good\n\n## new features\n* separate dropzones for uploading and searching! no more confusing modeswitching\n  * and the dropzone is global, so just drop files into the browser to upload / search 🚀🚀🚀\n* add 10-minute indicators to the audio player seekbar\n* make-sfx: argument `fast` reduces compression level\n\n![2021-0908-010348-firefox-fs8](https://user-images.githubusercontent.com/241032/132421531-efdc1165-785b-422d-bb5f-8b551c335c39.png)\n\n## bugfixes\n* moving/deleting files when running without `-e2d` (thx ixces)\n* zip/tar downloads: single folders are now the root element of the archive (not their contents)\n  * not really a bug but sufficiently unexpected\n* tiny lightmode fix + minor errormessage cleanups\n\n## other changes\n* crashpage: replace irc handle with new-github-issue link (i'm `+G` anyways heh)\n* meta/github stuff\n  * renamed `master` branch to `hovudstraum` (\"primary river\" in nynorsk)\n  * add [CONTRIBUTING](https://github.com/9001/copyparty/blob/hovudstraum/CONTRIBUTING.md), [code of conduct](https://github.com/9001/copyparty/blob/hovudstraum/CODE_OF_CONDUCT.md), and [issue templates](https://github.com/9001/copyparty/tree/hovudstraum/.github/ISSUE_TEMPLATE)\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0905-2306  `v0.13.14`  inline readme.md\n\n* latest important update: [v0.13.5](https://github.com/9001/copyparty/releases/tag/v0.13.5)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## new features\n* `README.md` is shown below the directory listing\n  * can be disabled with `--no-readme`\n* new option `--no-logues` disables prologue/epilogues in directory listings\n* new option `--no-dot-mv` disallows moving dotfiles (or folders containing them)\n* new option `--no-dot-ren` disallows renaming dotfiles (or making something a dotfile)\n\n## bugfixes\n* fix upload ETA if there is some idle time between batches\n* upload/filesearch with turbo enabled should be even faster now\n* markdown-editor scroll desync if document contains offsite images\n* better fix for the upload status list pushing the rest of the page around\n\n## other changes\n* sfx repacks with `no-fnt` will use `Consolas` instead which does not look terrible on windows\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0903-1921  `v0.13.13`  basic-auth\n\n* latest important update: [v0.13.5](https://github.com/9001/copyparty/releases/tag/v0.13.5)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\nnote: `copyparty-sfx.py` is https://github.com/9001/copyparty/commit/5955940b which fixes upload eta\n\n## new features\n* provide password using basic-authentication\n  * useful for clients which don't support cookies or appending queries to the URL\n  * order of precedence: `?pw=foo query` > `cppwd cookie` > `basic-auth`\n* show OK/Cancel buttons in OS-defined order\n  * Windows does OK/Cancel, everything else is Cancel/OK\n* crashpage: include recent console messages\n* js-repl: command history / presets\n\n## bugfixes\n* \"fix\" the file-list jumping around during uploads\n  * ...by adding a massive padding to the uploads list\n* make-sfx: set correct version-info on repack\n* make-sfx: fix no-dd css modifier\n\n## other changes\n* move column-hider buttons above the header so they're not as easy to hit by accident\n* jpeg thumbnails are slightly smaller\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0901-2148  `v0.13.12`  september\n\n* latest important update: [v0.13.5](https://github.com/9001/copyparty/releases/tag/v0.13.5)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## new features\n* show useragent on the crashpage (plus some ui cleanup)\n\n## bugfixes\n* thumbnail-zoom hotkeys\n* add vertical scrollbar to toasts if necessary\n* cut/paste of more than roughly 30'000 files at once\n\n## other changes\n* replaced the video icon with a play button in the [browser-icons.css](https://github.com/9001/copyparty/tree/master/docs#example-browser-css) example:\n\n![2021-0902-002101-firefox-fs8](https://user-images.githubusercontent.com/241032/131753177-6741d2af-6220-4f42-aaef-8439171cc0be.png)\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0830-2032  `v0.13.11`  selective listening\n\n* latest important update: [v0.13.5](https://github.com/9001/copyparty/releases/tag/v0.13.5)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## bugfixes\n* bind specific interfaces which are not `127.0.0.1`\n\n## other changes\n* sfx should be a tiny bit smaller\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0830-0102  `v0.13.10`  The Net reference\n\n* latest important update: [v0.13.5](https://github.com/9001/copyparty/releases/tag/v0.13.5)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## new features\n* click the bottom-left `π` for a js eval prompt\n  * good for debugging on phones (and a nice meme)\n\n## bugfixes\n* file uploads now happen in alphabetical order\n* the default text is selected in prompts (text-input messageboxes)\n* crash-page was slightly out-of-bounds on phones\n* cheap performance fix when renaming >500 files\n* minor ux fixes for old browsers / iOS ~10\n\n## other changes\n* return to volume listing after logging in\n* fully drop support for playing ogg/vorbis/opus on iOS older than 14\n  * final version where this *somewhat* worked was [v0.13.9](https://github.com/9001/copyparty/releases/tag/v0.13.9)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0829-0024  `v0.13.9`  the iOS update\n\n* latest important update: [v0.13.5](https://github.com/9001/copyparty/releases/tag/v0.13.5)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## new features\n* iOS: play ogg/vorbis/opus files in the background and when the screen is off\n  * but please don't touch the lockscreen play/pause button unless `os-ctl` is enabled in the `🎺 media player options` tab\n    * safari 15 is rumored to support `MediaSession` so it should *magically work* when that is out\n\n## bugfixes\n* iOS: browsers no longer randomly crash when playing an ogg file\n\n## other changes\n* tray drawer is a bit smaller (the bottom right burger thing)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0828-0255  `v0.13.7`  dot-dot-dot\n\n(throw more dots, more dots)\n\n* latest important update: [v0.13.5](https://github.com/9001/copyparty/releases/tag/v0.13.5)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## new features\n* grid-view: filenames longer than 3 lines are truncated with `...`\n  * the full filename appears as a tooltip on hover\n  * use the `chop` buttons to adjust the limit\n\n## bugfixes\n* the 300 msec delay when tapping just about *anything* on phones\n  * iphones got slightly better too (still needs the tooltip workaround)\n* center tooltips horizontally + close on scroll + fix vertical margin\n\n## other changes\n* folder icons are now displayed top-left on thumbnails since it crashed with the ellipsis stuff\n  * which also simplifies the [browser-icons.css](https://github.com/9001/copyparty/blob/master/docs/browser-icons.css) example\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0826-2209  `v0.13.6`  the final countdown\n\n* latest important update: [v0.13.5](https://github.com/9001/copyparty/releases/tag/v0.13.5)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## new features\n* total ETA until all the queued upload/searches are finished\n* shows a toast notification with a summary after all uploads finish\n* colored status indication for uploads/searches\n* shows a warning if uploads/searches are blocked by the up2k flag/mutex\n* replaced most monospace text with SourceCodePro\n  * looks SO MUCH BETTER on windows\n\n![nu_2-fs8](https://user-images.githubusercontent.com/241032/131047767-8844c829-d336-438e-b7db-c28f084c3397.png)\n\n## bugfixes\n* lock navigation focus inside popup messages, we proper modals now\n* hashing didn't pause when `parallel uploads` was 0 (arguably a bug)\n* navpane could scroll horizontally\n* toggling file-search in the middle of an upload queue would affect the remainder of the queue\n  * now the files are tagged with search/upload labels as they're added which makes much more sense\n* top-level folder thumbnails could 404\n* fix up2k-turbo for markdown documents\n* fix files skipping the busy-list entirely with turbo enabled\n* more predictable(?) file-search behavior when turbo is enabled\n* the up2k flag/mutex could get stuck in limbo between two browser tabs if disabled while that tab holds it\n* add missing hotkey hint (thumbnail toggle, bottom right)\n* minor rice and html-escape fixes for modals and toasts\n* avoid android-firefox bug where `number.toFixed(1)` returns `10.00` instead of `10.0` for certain values of 10\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0816-0640  `v0.13.5`  time-travelers friend\n\n* latest important update: **this version**\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## new features\n* button to scroll navpane to the open folder\n  * also automatically does this on page load\n\n## bugfixes\n* unpost only worked for the `/` volume\n* up2k-client could break on interesting folder-names\n* moving more than 100 files at once across browser tabs\n* basic-upload into folders with upload rules didn't really work\n* ui indicated that renaming multiple files was impossible (but you still could tho)\n\n## other changes\n* tiny js optimizations\n* even more ancient browsers (including opera 11, hipp hipp) can now use the thumbnail-view and image viewer\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0814-2046  `v0.13.3`  this side up\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to this version)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## new features\n* image-viewer: rotate images and videos (hotkeys `R` and `shift-R`)\n* video-thumbnails: apply rotation hints from container\n* image-thumbnails: apply rotation hints from exif\n* image-thumbnails: higher quality AND slightly smaller\n  * fix loss of detail on resize\n* argument `--th-mt` specifices number of cores to use for thumbnailing\n  * default is 0 which means all cores\n\n## bugfixes\n* image-viewer: fix pinch-zoom (broke in 0.11.19)\n  * on the bright side: zoom is now less buggy than ever\n\n## other changes\n* (probably extremely minor) performance tweaks in the image-viewer\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0812-2042  `v0.13.2`  jet engine removal\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to this version)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## new features\n* toggle file-selection in the image viewer with hotkey `s` or using the `sel` button\n\n## bugfixes\n* chrome would max a cpu core (and consume even more ram than usual) after sitting idle in the browser for a few weeks due to recursive setTimeouts\n  * just the `setTimeout` call itself took like 67 msec seriously\n  * (firefox was completely fine)\n* button placement in huge modals\n* play videos in the gallery when clicked\n* cut/paste files on ancient chrome versions\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0809-2028  `v0.13.1`  ephemeral\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to this version)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## new features\n* ephemeral uploads - set the volume flag `:c,lifetime=600` to delete files 10 minutes after upload\n  * feature can be disabled with `--no-lifetime`\n* volume flag `:c,rescan=60` to rescan a volume for new/modified files every 60 seconds \n  * same as the old `--re-maxage` except per-volume\n* [prisonparty.sh](https://github.com/9001/copyparty/blob/master/bin/prisonparty.sh) - run copyparty in a chroot if you don't trust the volumes\n\n## bugfixes\n* handle more exceptions\n* dont crash on startup if `XDG_CONFIG_HOME` is invalid\n* up2k-ui: toggle button to continue hashing while uploading did nothing\n* replace filesystem paths with vfs paths in exceptions returned to the user\n* sfx.py: return 1 on exceptions\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0808-0214  `v0.13.0`  future-proof\n\n* **latest stable release:** [v0.12.12](https://github.com/9001/copyparty/releases/tag/v0.12.12)\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to this version)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n\n## new features\n* reinvented `alert`/`confirm`/`prompt` because [google/whatwg is getting rid of them](https://github.com/whatwg/html/issues/6897#issuecomment-885773622)\n* upload quotas (num.files, total bytes) and rotation, see [readme#upload-rules](https://github.com/9001/copyparty#upload-rules)\n* streaming compression of uploads to gz or xz, see [readme#compress-uploads](https://github.com/9001/copyparty#compress-uploads)\n  * not compatible with up2k and breaks file checksums (dupe-detection, file-search)\n* another mtp example ([youtube manifest parser](https://github.com/9001/copyparty/blob/master/bin/mtag/yt-ipr.py))\n\n## bugfixes\nnone! just new bugs this time\n\n## other changes\n* more accurate advice from the up2k searchmode explainer\n* warning prompt if you try to open a massive transfer log in the up2k ui\n* additional --help sections and early vt100 stripper\n* chrome performance fixes in file selection\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0806-0910  `v0.12.12`  lock your doors\n\n[terribly stable](https://www.youtube.com/watch?v=FAVR-FnWGjo)\n* if upgrading from v0.11.x or before, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to this version)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## bugfixes\n* forgot a mutex on renames/moves\n* file metadata could persist after delete\n* relative moves of relative symlinks could break/unlink\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0805-2253  `v0.12.11`  batch-rename\n\n\"stable\"\n* if upgrading from v0.11.x, see [v0.12.4](https://github.com/9001/copyparty/releases/tag/v0.12.4)\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to this version)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## known bugs\n* mtp indexing can halt if files are renamed/moved in the middle of a rebuild\n  * restart copyparty and it'll resume just fine\n\n## new features\n* batch-rename! inspired by foobar2000\n  * rename multiple files based on regex and/or media tags\n* [`media-hash.py`](https://github.com/9001/copyparty/tree/master/bin/mtag), new mtp module\n  * generates `vhash` and `ahash` -- video and audio checksums which can help in spotting dupes\n  * usage: `-mtp ahash,vhash=f,media-hash.py` or per-volume `:c,mtp=ahash,vhash=f,media-hash.py`\n\n![batch-rename-fs8](https://user-images.githubusercontent.com/241032/128434204-eb136680-3c07-4ec7-92e0-ae86af20c241.png)\n\n## bugfixes\n* renaming single symlinks\n* upgrading v0.11 volume arguments on windows\n* thumbnails of files with multiple video tracks (theoretically)\n* race in the httpd threadpool which could cause a tiny performance drop\n* sfx-repack with `no-fnt` / `no-dd`\n* funky padding in some browsers\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0801-2249  `v0.12.10`  mth\n\n* **latest stable release:** [v0.11.47](https://github.com/9001/copyparty/releases/tag/v0.11.47) (v0.12.x is almost there)\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to v0.11.47 or this version)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* `-mth`: list of tags to hide by default in the browser\n\n## bugfixes\n* better codec detection when using mutagen for tag parsing\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0731-2240  `v0.12.9`  ah yes lightmode\n\n* **latest stable release:** [v0.11.47](https://github.com/9001/copyparty/releases/tag/v0.11.47) (v0.12.x is almost there)\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to v0.11.47 or this version)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## bugfixes\n* lightmode rename ui\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0731-2217  `v0.12.8`  better rename ui\n\n* **latest stable release:** [v0.11.47](https://github.com/9001/copyparty/releases/tag/v0.11.47) (v0.12.x is almost there)\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to v0.11.47 or this version)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* better rename ui\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0731-1121  `v0.12.7`  preserve tags\n\n* **latest stable release:** [v0.11.47](https://github.com/9001/copyparty/releases/tag/v0.11.47) (v0.12.x is almost there)\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to v0.11.47 or this version)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## bugfixes\n* loss of tags when renaming / moving files within a volume, and when deleting dupes\n  * restart copyparty (or rescan in the admin panel) to fix the missing tags\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0731-1038  `v0.12.6`  it keeps happening\n\n* **latest stable release:** [v0.11.47](https://github.com/9001/copyparty/releases/tag/v0.11.47) (v0.12.x is almost there)\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to v0.11.47 or this version)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* toggle-button to show dotfiles (hidden files)\n\n## bugfixes\n* renaming files which contain url-escaped characters\n* access display (top-right) didn't include move permissions\n* thumbnails aren't thumbnailed\n\n## other changes\n* move toasts bottom-right (next to the edit buttons) due to phones\n* make-sfx is faster and better\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0730-1728  `v0.12.5`  rfc3986 2nd season\n\nthis release was made from all-natural, free-range code [PXL_20210730_160240244.jpg](https://ocv.me/i/PXL_20210730_160240244.jpg) (☞ﾟ∀ﾟ)☞ [PXL_20210730_174219083.jpg](https://ocv.me/i/PXL_20210730_174219083.jpg)\n\n* **latest stable release:** [v0.11.47](https://github.com/9001/copyparty/releases/tag/v0.11.47) (v0.12.x is almost there)\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to v0.11.47 or this version)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## upgrade notes\nnothing new since v0.12.3 -- short summary of [v0.12.3](https://github.com/9001/copyparty/releases/tag/v0.12.3) and [v0.12.1](https://github.com/9001/copyparty/releases/tag/v0.12.1):\n* `--no-mv` disables file/folder move ops\n* `--no-del` disables file/folder delete and unpost\n* `--unpost 0` disables unpost\n* databases upgrade to v5; incompatible with v0.12.1 and older\n\n## bugfixes\n* multiselect zip download (broke in v0.12.1)\n* filenames of multiselect zip downloads when first item contains \" or % (was always broken)\n* renaming files inside folders with url-escaped characters\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0730-0652  `v0.12.4`  fix permission groups\n\n* **latest stable release:** [v0.11.47](https://github.com/9001/copyparty/releases/tag/v0.11.47) (v0.12.x is almost there)\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to v0.11.47 or this version)\n* latest gzip edition of the sfx: [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## upgrade notes\nshort summary of [v0.12.3](https://github.com/9001/copyparty/releases/tag/v0.12.3) and [v0.12.1](https://github.com/9001/copyparty/releases/tag/v0.12.1):\n* `--no-mv` disables file/folder move ops\n* `--no-del` disables file/folder delete and unpost\n* `--unpost 0` disables just unpost\n* databases upgrade to v5; incompatible with v0.12.1 and older\n\n## bugfixes\n* fix listing multiple users for the same permission-set\n  * `-v .::rw,u1,u2,u3` now works, the workaround was `-v .::rw,u1:rw,u2:rw,u3`\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0729-2232  `v0.12.3`  unpost\n\n1001GET (;_;)\n* **latest stable release:** [v0.11.47](https://github.com/9001/copyparty/releases/tag/v0.11.47)\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to v0.11.47 or this version)\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\nsee [v0.12.1](https://github.com/9001/copyparty/releases/tag/v0.12.1) upgrade-notes regarding new opt-out features\n\n## upgrade notes\n* new argument `--unpost 0` (and/or `--no-del`) disables the new unpost feature\n* your up2k databases will upgrade from v4 to v5; backups are made automatically\n  * v5 DBs require copyparty v0.12.3 or newer, so use the backups for older versions\n\n## new features\n* unpost! uploaders can delete their uploads within `--unpost` seconds (default is 12 hours)\n  * can be disabled by setting `--unpost 0` or with `--no-del`\n\n## bugfixes\n* deleting single files (metadata could persist in db)\n* `--ls` broke in v0.12.1\n* toasts with `<pre>` tags had massive margins\n* hopefully fix a bug where malicious POSTs through an nginx reverse-proxy could put the connection in a bad state, causing the next legit request to fail with bad headers\n\n## other changes\n* uploader-ip and upload-time is stored in the database\n  * but only viewable through an sqlite3 shell;\n    `sqlite3 .hist/up2k.db 'select ip, rd, fn from up where ip'`\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0727-2355  `v0.12.1`  filed\n\n```\n <&ed> copyparty became a file manager, trying to think of a release name\n <&ed> \"far out\", pun on far manager, there we go\n<+des> ed: filed\n<+des> fil-ed\n```\n\n* **latest stable release:** [v0.11.47](https://github.com/9001/copyparty/releases/tag/v0.11.47)\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to v0.11.47 or this version)\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## upgrade notes\n* permission `a` no longer exists; is automatically translated to `r` + `w`\n* new argument `--no-del` disables all delete operations\n* new argument `--no-mv` disables all move/rename operations\n* new argument `--no-voldump` disables the volume/permission summary on startup\n\n## new features\n* file manager! cut/paste, rename, delete files\n  * new permission `m` (move) allows renaming files in (and moving files *out of*) that volume\n  * new permission `d` (delete) allows deleting things in that volume\n  * hotkeys `ctrl-X`, `ctrl-V` to cut/paste, `F2` to rename, `ctrl-K` to delete\n  * tags follow the files when moved; thumbnails just regenerate\n* select files/folders in the browser using the keyboard\n  * click a file row and use cursor-keys to navigate\n  * ctrl-cursor to also scroll the viewport\n  * shift-cursors to expand selection\n  * spacebar and `ctrl-A` toggles selection\n* periodic volume rescan\n  * detect and index files coming into volumes from the outside (sftp, rsync, ...)\n  * will probably get an inotify alternative at some point but this is more reliable\n* list all volumes and permissions on startup\n* print server IPs on macos and windows too\n\n## bugfixes\n* tags are displayed for symlinked/dupe files\n* mkdir defaults to 755, used to be the python-default 777, sorry\n* ensure that the multiprocessing workers start correctly (and crash otherwise)\n* more reliable db backups on upgrade, using the native sqlite3 backup feature\n* signal handler; macos could get stuck on shutdown\n* other minor stuff\n  * centos7 support fixes\n  * missing mojibake support (centralized most of it)\n  * better support for buggy windows smb drives\n  * edgecases with relative symlinks\n\n## other changes\n* replaced the md-editor toasts with the new general-purpose ones\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0722-0809  `v0.11.47`  On Error Resume Next\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to this version)\n* recent maybe-important updates:\n  * [v0.11.45](https://github.com/9001/copyparty/releases/tag/v0.11.45) if clients use up2k over plaintext http\n  * [v0.11.43](https://github.com/9001/copyparty/releases/tag/v0.11.43) fixes stability in the uploader client\n  * [v0.11.41](https://github.com/9001/copyparty/releases/tag/v0.11.41) if running as a service with `-lo`\n  * [v0.11.32](https://github.com/9001/copyparty/releases/tag/v0.11.32) fixes a case of filesystem paths being unmasked\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) fixes crash if no accounts are defined\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* crashpage: add option to ignore exceptions and continue\n  * but please do report them so they can be fixed properly w\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0722-0642  `v0.11.46`  chrome friendly\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to this version)\n* recent maybe-important updates:\n  * [v0.11.45](https://github.com/9001/copyparty/releases/tag/v0.11.45) if clients use up2k over plaintext http\n  * [v0.11.43](https://github.com/9001/copyparty/releases/tag/v0.11.43) fixes stability in the uploader client\n  * [v0.11.41](https://github.com/9001/copyparty/releases/tag/v0.11.41) if running as a service with `-lo`\n  * [v0.11.32](https://github.com/9001/copyparty/releases/tag/v0.11.32) fixes a case of filesystem paths being unmasked\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) fixes crash if no accounts are defined\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## bugfixes\n* ignored `ResizeObserver loop limit exceeded` in the exception handler\n  * chrome [randomly throws this](https://bugs.chromium.org/p/chromium/issues/detail?id=809574) from the `<video>` UI, nice\n* logout link could 404\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0720-2123  `v0.11.45`  user friendly\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to this version)\n* recent maybe-important updates:\n  * [v0.11.45](https://github.com/9001/copyparty/releases/tag/v0.11.45) (this ver) if clients use up2k over plaintext http\n  * [v0.11.43](https://github.com/9001/copyparty/releases/tag/v0.11.43) fixes stability in the uploader client\n  * [v0.11.41](https://github.com/9001/copyparty/releases/tag/v0.11.41) if running as a service with `-lo`\n  * [v0.11.32](https://github.com/9001/copyparty/releases/tag/v0.11.32) fixes a case of filesystem paths being unmasked\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) fixes crash if no accounts are defined\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* login/logout link in the top-right corner\n  * also shows account name + current access level (per-folder)\n\n## bugfixes\n* avoid loading the wasm hasher multiple times\n  * it would reload every time the up2k tab was selected, probably dangerous\n  * only affects clients using up2k with plaintext http (not https)\n* tooltips on iphones, again\n\n## other changes\n* crashpage now includes localstore contents\n* the up2k filesearch \"explain\" link now mentions lack of write permissions, if that is the case\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0719-2303  `v0.11.44`  smol fix\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to this version)\n* recent maybe-important updates:\n  * [v0.11.43](https://github.com/9001/copyparty/releases/tag/v0.11.43) fixes stability in the uploader client\n  * [v0.11.41](https://github.com/9001/copyparty/releases/tag/v0.11.41) if running as a service with `-lo`\n  * [v0.11.32](https://github.com/9001/copyparty/releases/tag/v0.11.32) fixes a case of filesystem paths being unmasked\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) fixes crash if no accounts are defined\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## bugfixes\n* browser crash if the audio player runs into the next folder while the folder sidebar is closed (introduced in 0.11.42)\n\n## other changes\n* make-sfx.sh: `no-fnt` and `no-dd` shaves another ~10kB\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0718-2356  `v0.11.43`  ux is my passion\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to this version)\n* recent maybe-important updates:\n  * [v0.11.43](https://github.com/9001/copyparty/releases/tag/v0.11.43) (this ver) fixes stability in the uploader client\n  * [v0.11.41](https://github.com/9001/copyparty/releases/tag/v0.11.41) if running as a service with `-lo`\n  * [v0.11.32](https://github.com/9001/copyparty/releases/tag/v0.11.32) fixes a case of filesystem paths being unmasked\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) fixes crash if no accounts are defined\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* explain the up2k modeswitch in filesearch results\n\n## bugfixes\n* up2k-ui coherence check was a bit too picky\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0718-2122  `v0.11.42`  in case of tags\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to this version)\n* recent maybe-important updates:\n  * [v0.11.42](https://github.com/9001/copyparty/releases/tag/v0.11.42) (this ver) if you have `-mtp` parsers *and* use Mutagen to read tags\n  * [v0.11.41](https://github.com/9001/copyparty/releases/tag/v0.11.41) if running as a service with `-lo`\n  * [v0.11.33](https://github.com/9001/copyparty/releases/tag/v0.11.33) fixes stability in the uploader client\n  * [v0.11.32](https://github.com/9001/copyparty/releases/tag/v0.11.32) fixes a case of filesystem paths being unmasked\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) fixes crash if no accounts are defined\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* if Mutagen fails to read a file, it retries with FFprobe\n* `--no-mtag-ff` bans all use of FFprobe to read tags\n* hotkeys `i/k` now ensure the active folder stays in-view\n\n## bugfixes\n* tag search was case-sensitive in some cases (most importantly `key>=1a` did not work as intended)\n* advanced-search would break if search terms were double-space separated\n* the preferred key-notation did not apply to search results (did rekobo-alnum instead)\n* tooltips for column headers didn't work for newly-hidden columns\n* no more surprise tooltips when switching tabs\n\nall these changelogs are sorted by importance btw so here's the least important bugfix (since it doesn't affect anyone i know)\n* codec/format info was not collected from Mutagen when scanning audio files\n  * this broke `mtp` (external metadata parsers)\n  * you avoided this issue by not having Mutagen installed, and/or by using `--no-mutagen`\n  * if you *were* using Mutagen to collect tags, you can do a single run with `-e2tsr` for a full rescan if you care\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0717-1553  `v0.11.41`  caas\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to this version)\n* recent maybe-important updates:\n  * [v0.11.41](https://github.com/9001/copyparty/releases/tag/v0.11.41) (this ver) if running as a service with `-lo`\n  * [v0.11.33](https://github.com/9001/copyparty/releases/tag/v0.11.33) fixes stability in the uploader client\n  * [v0.11.32](https://github.com/9001/copyparty/releases/tag/v0.11.32) fixes a case of filesystem paths being unmasked\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) fixes crash if no accounts are defined\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* add shortcut to toggle list/grid-view in the audio drawer\n* combine the mkdir/newdoc/msg tabs on narrow screens\n* sd-notify support to properly use copyparty as a systemd service\n  * other units which `After=copyparty` will be delayed until copyparty is ready to accept connections\n  * updated the [unit example](https://github.com/9001/copyparty/blob/master/contrib/systemd/copyparty.service) with the changes (`Type=notify` and `SyslogIdentifier=copyparty`)\n* markdown editor hotkeys now work properly on dvorak keyboards\n  * the hotkeys use the qwerty layout which seems to be preferred according to stackoverflow\n\n## bugfixes\n* clean shutdown on SIGINT and SIGTERM\n  * previously, when running as a sysv/systemd service, a `service stop` would lose:\n    * lots of log messages when using `-lo`\n    * information about incomplete uploads for the past 30 seconds\n\n## other changes\n* lots of new tooltips with hotkeys info\n  * also explains the cryptic codec/bitrate columns\n  * and iphones can now hide tooltips by tapping them since safari is safari\n* increased up2k snapshot interval from 30sec to 5min now that SIGTERM is a clean shutdown\n* finally found something the `zip_crc` mode is good for: supporting PKZIP v2.04g from october 1993 (absolutely worth)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0714-2313  `v0.11.40`  video player tweaks\n\n(commit #900, checkem)\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) (but skip right to this version)\n* recent maybe-important updates:\n  * [v0.11.33](https://github.com/9001/copyparty/releases/tag/v0.11.33) fixes stability in the uploader client\n  * [v0.11.32](https://github.com/9001/copyparty/releases/tag/v0.11.32) fixes a case of filesystem paths being unmasked\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) fixes crash if no accounts are defined\n* see steps for [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) if upgrading from something before that\n* see steps for [v0.11.24](https://github.com/9001/copyparty/releases/tag/v0.11.24) if you ever used versions v0.11.20 through v0.11.23\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* allow ctrl-clicking the main tabs to open other views in new tabs\n* gallery (the image viewer / video player, accessible from the grid view):\n  * when playing a video, the audio player will pause and autoresume\n  * hotkey `r` to toggle video loop\n  * hotkey `c` to toggle continue-playing-next-video\n    * and added a toggle button for those two ^\n  * remember the mute settings for the next videos\n  * encourage browser to cache aggressively\n  * dispose videos to stop them from buffering in the background\n\n## bugfixes\n* gallery: some keyboard hotkeys were buggy depending on focus\n\n## other changes\n* adjust the sfx text-editor warning to show it's OK to use hex editors\n* minor ux tweaks\n  * settings-reset link on the crashpage (underline, brightmode color)\n  * brightmode: gallery filename / download link\n  * main tabs unselectable\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0712-2254  `v0.11.39`  mob psycho\n\n(get it? cause its the 100th release, at commit 888 even)\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20)\n* recent maybe-important updates:\n  * [v0.11.33](https://github.com/9001/copyparty/releases/tag/v0.11.33) fixes stability in the uploader client\n  * [v0.11.32](https://github.com/9001/copyparty/releases/tag/v0.11.32) fixes a case of filesystem paths being unmasked\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) fixes crash if no accounts are defined\n* additional steps for [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) apply to this version if upgrading from something before that\n* additional steps for [v0.11.24](https://github.com/9001/copyparty/releases/tag/v0.11.24) apply to this version if you ever used versions v0.11.20 through v0.11.23\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* add `--log-thrs` which periodically logs a summary of active threads\n* `--stackmon` also runs inside the worker forks when using `-j`\n* video player: hotkeys `f` for fullscreen and `m` for mute\n* add a link which clears the settings on the js crash page, in case someone gets stuck by enabling grid mode on ie11 for example\n\n## bugfixes\n* the `?stack` link in the controlpanel required `/` to be a volume\n* image gallery: shrink the image a bit so the link doesn't overlap\n* cheap race \"fix\" for pypy\n\n## other changes\n* better thread names\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0711-2251  `v0.11.37`  just 2b safe\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20)\n* recent maybe-important updates:\n  * [v0.11.33](https://github.com/9001/copyparty/releases/tag/v0.11.33) fixes stability in the uploader client\n  * [v0.11.32](https://github.com/9001/copyparty/releases/tag/v0.11.32) fixes a case of filesystem paths being unmasked\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) fixes crash if no accounts are defined\n* additional steps for [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) apply to this version if upgrading from something before that\n* additional steps for [v0.11.24](https://github.com/9001/copyparty/releases/tag/v0.11.24) apply to this version if you ever used versions v0.11.20 through v0.11.23\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* log the list of files that couldn't be included in a tar/zip download\n\n## bugfixes\n* any potential cases of [surprising values in default arguments](https://user-images.githubusercontent.com/241032/125212304-bdb5e980-e2ac-11eb-962f-e1ee5cce510d.png), couldn't see anything bad luckily\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0711-0439  `v0.11.36`  foreshadowing\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20)\n* recent maybe-important updates:\n  * [v0.11.33](https://github.com/9001/copyparty/releases/tag/v0.11.33) fixes stability in the uploader client\n  * [v0.11.32](https://github.com/9001/copyparty/releases/tag/v0.11.32) fixes a case of filesystem paths being unmasked\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) fixes crash if no accounts are defined\n* additional steps for [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) apply to this version if upgrading from something before that\n* additional steps for [v0.11.24](https://github.com/9001/copyparty/releases/tag/v0.11.24) apply to this version if you ever used versions v0.11.20 through v0.11.23\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* video player -- click a webm/mp4 in the grid-view to play it\n  * only does formats/codecs supported by your browser for now (thats the foreshadowing part)\n* `--th-clean 0` disables periodic cleanup of the thumbnail cache \n\n## bugfixes\n* image viewer trying to display folders named `something.jpg`\n* py2 could not list/access files with unicode filenames when using volumes\n  * when is centos7 eol again\n\n## other changes\n* some more context in exceptions\n* thumbnail-generator: `mts` added to list of video file extensions\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0709-1512  `v0.11.34`  multi-process drifting (at low latency)\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20)\n* recent maybe-important updates:\n  * [v0.11.33](https://github.com/9001/copyparty/releases/tag/v0.11.33) fixes stability in the uploader client\n  * [v0.11.32](https://github.com/9001/copyparty/releases/tag/v0.11.32) fixes a case of filesystem paths being unmasked\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) fixes crash if no accounts are defined\n* additional steps for [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) apply to this version if upgrading from something before that\n* additional steps for [v0.11.24](https://github.com/9001/copyparty/releases/tag/v0.11.24) apply to this version if you ever used versions v0.11.20 through v0.11.23\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* faster http replies! reduced the time to establish new connections, so:\n  * up to 25% faster round-trip time on short http requests with `-j1` (server-default), but more importantly...\n  * up to 3.3x faster with `-j4` (now almost equal to `-j1`) and a single client doing stuff, but wait it gets better:\n  * up to 6.6x faster with `-j4` and multiple clients hammering the server\n  * but note that higher `-j` values adds more connection latency in exchange for processing power, https://en.wikipedia.org/wiki/Thundering_herd_problem\n* discard log messages early when `-q` is set without `-lo`, giving better multiprocessing performance\n\n## bugfixes\n* fix general loss of centos7 support (TLnote: early 2.7 versions) introduced in [v0.11.30](https://github.com/9001/copyparty/releases/tag/v0.11.30)\n  * also fixed downloading folders as zip-files which centos7 never could\n\n## other changes\n* `-j1` will be forced for python 2.7 because it cannot pickle tcp servers\n* accessing `?stack` works on any url as long as you're admin *somewhere*\n* the `-j` loadbalancer messages are gone because the loadbalancer is gone\n  * should give a teeny-tiny performance boost to multiprocessing on uploads\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0707-0845  `v0.11.33`  moms spaghetti\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20)\n* recent maybe-important updates:\n  * [v0.11.33](https://github.com/9001/copyparty/releases/tag/v0.11.33) (this ver) fixes stability in the uploader client\n  * [v0.11.32](https://github.com/9001/copyparty/releases/tag/v0.11.32) fixes a case of filesystem paths being unmasked\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) fixes crash if no accounts are defined\n* additional steps for [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) apply to this version if upgrading from something before that\n* additional steps for [v0.11.24](https://github.com/9001/copyparty/releases/tag/v0.11.24) apply to this version if you ever used versions v0.11.20 through v0.11.23\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## bugfixes\n* *another* crash in the up2k UI\n* separate turbo-warning for search mode\n* stop running ahead with handshakes if something uploaded recently\n  * reduces the odds of skipping an upload which should have become a symlink\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0706-1958  `v0.11.32`  turbo button\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20)\n* recent maybe-important updates:\n  * [v0.11.32](https://github.com/9001/copyparty/releases/tag/v0.11.32) (this ver) fixes stability in the uploader client + a case of filesystem paths being unmasked\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) fixes crash if no accounts are defined\n* additional steps for [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) apply to this version if upgrading from something before that\n* additional steps for [v0.11.24](https://github.com/9001/copyparty/releases/tag/v0.11.24) apply to this version if you ever used versions v0.11.20 through v0.11.23\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* `turbo button` in the settings panel for superfast resume of massive uploads \n  * good for when you were in the middle of uploading 100'000 files and had to restart for some reason\n  * comes at a serious cost: files will be skipped as long as they exist on the server with the right filesize, even if they could be incomplete uploads or are otherwise different from your local files, so you should do a \"verification pass\" by disabling turbo + refreshing + redoing the upload once you make it through\n  * when combined with the new `date-chk` button it *should* notice and resume incomplete uploads but please do the verification pass anyways\n  * all of this is explained in the tooltip for the button so idk why im putting it here too\n* `-lo` enables xz-compressed logging to file in addition to printing to the console\n  * with logrotate if the filename contains date-format-strings (like `%Y-%m-%d`)\n  * when combined with `-q` it disables console-logging and only logs to file, gives a tiny speed boost depending on OS\n  * also cleans up a few places with plain prints instead of the threadsafe pretty ones\n* the volume-flags summary on startup now also print *which* volume they're talking about\n\n## bugfixes\n* `dir.txt` inside the thumbnails folder could be downloaded; possibly bad since it contains absolute-paths from the host filesystem\n* [v0.11.31](https://github.com/9001/copyparty/releases/tag/v0.11.31) added parallel handshakes which could cause files to checksum and upload out-of-order, fixed\n  * this also uncovered another UI-crash in the up2k client (nice) which is now also fixed separately\n* a few more cases of recursive symlinks are detected and defused\n  * symlink pointing to its own folder when creating a tar/zip\n  * initial directory scanning (`-e2ds`)\n    * initial directory scanning is now a tiny bit slower, sorry\n* `-nw` didn't apply to PUT uploads\n* more invalid requests get a sensible-ish reply stating what the client did wrong\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0704-1444  `v0.11.31`  an extra pair of hands\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20)\n* recent maybe-important updates:\n  * [v0.11.30](https://github.com/9001/copyparty/releases/tag/v0.11.30) fixes stability in the uploader client\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) fixes crash if no accounts are defined\n* additional steps for [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) apply to this version if upgrading from something before that\n* additional steps for [v0.11.24](https://github.com/9001/copyparty/releases/tag/v0.11.24) apply to this version if you ever used versions v0.11.20 through v0.11.23\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* parallel handshakes\n  * faster uploads and file-search, especially on tiny files / high-latency connections\n\n## bugfixes\n* send keepalive handshakes when an upload has been paused / idle for 5h 45min so it doesn't expire\n  * fixes one of the v0.11.30 known-bugs but still no idea what that other thing was, something about \"bad file descriptor\" right before a power outage so the logs are lost, shoganai\n* race conditions in the up2k-server which couldn't be hit before parallel handshakes was added\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0701-2027  `v0.11.30`  the up2k-client update\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20)\n* recent maybe-important updates:\n  * [v0.11.30](https://github.com/9001/copyparty/releases/tag/v0.11.30) (this ver) fixes stability in the uploader client\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) fixes crash if no accounts are defined\n* additional steps for [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) apply to this version if upgrading from something before that\n* additional steps for [v0.11.24](https://github.com/9001/copyparty/releases/tag/v0.11.24) apply to this version if you ever used versions v0.11.20 through v0.11.23\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## known bugs\n* if an upload is paused by setting `parallel uploads` to `0` in the UI...\n  * ...for \"about an hour\", it *might* be unable to resume\n  * ...for 6 hours or more, it is **definitely** unable to resume\n\nunless the upload was paused for 6 hours or more, it can probably be resumed by refreshing the website and restarting the upload (\"probably\" because haven't been able to reproduce)\n\n## new features\n* up2k-client: 100x faster initialization when adding lots of files\n* cachebuster to force chrome to use the correct js/css files since it ignores the no-cache header\n* make `-nw` apply to more stuff (up2k skips creating files)\n\n## bugfixes\n* up2k-client:\n  * fix crash caused by parallel uploads running far ahead, ui trying to update stuff it already purged\n    * mostly problematic when uploading lots of small files mixed with slightly-larger files\n  * general robustness\n    * recover from tcp/dns issues during chunk-uploads\n    * recover from antivirus yanking files mid-read\n    * ignore server complaining about duplicate chunks, it's fine\n  * help chrome not get stuck when it sees a file named `aux.h` on windows\n  * notice and panic on more errors\n    * and stop trying to do things after something died to an unhandled exception\n  * less confusing debug messages regarding sha512 library selection\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0629-2351  `v0.11.29`  thx kip\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20)\n* recent maybe-important updates:\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) fixes crash if no accounts are defined\n* additional steps for [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) apply to this version if upgrading from something before that\n* additional steps for [v0.11.24](https://github.com/9001/copyparty/releases/tag/v0.11.24) apply to this version if you ever used versions v0.11.20 through v0.11.23\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features suggested by kipu\n* pause uploads by setting `parallel uploads` to `0`\n* increase max `parallel uploads` to 16 (using +/- buttons) and 64 (by manual text entry) to accomodate sad american internet connections\n* also look for `cover.jpg` and `cover.png` as folder thumbnails by default, adjustable with `--th-covers`\n* change the description in the sfx so the corruption warning is the first plaintext you see\n\n## other new features\n* search ui could be visibly confusing if the final text entry event happened in the middle of a search\n* adjustable `tint` on the audio-player progressbar to make buffering updates less visually distracting\n* per-http-connection request counter appended to the transfer speed summary\n\n## bugfixes\n* ctrl-clicking folders in the directory tree didn't open them in a new tab\n* javascript panic-screen could display the wrong stack in some cases\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0628-1336  `v0.11.28`  fix no-accounts crash\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20)\n* recent maybe-important updates:\n  * [v0.11.28](https://github.com/9001/copyparty/releases/tag/v0.11.28) (this version) fixes crash if no accounts are defined\n* additional steps for [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) apply to this version if upgrading from something before that\n* additional steps for [v0.11.24](https://github.com/9001/copyparty/releases/tag/v0.11.24) apply to this version if you ever used versions v0.11.20 through v0.11.23\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* adjust tree width with hotkeys `a/d`\n  * thumbnail zoom is now shift+`a/d`\n* control-panel link always points to the webroot (mostly cosmetic)\n\n## bugfixes\n* lost replies (http handler crash) if you're running without any accounts\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0625-2023  `v0.11.27`  audiogrid\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20)\n* additional steps for [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) apply to this version if upgrading from something before that\n* additional steps for [v0.11.24](https://github.com/9001/copyparty/releases/tag/v0.11.24) apply to this version if you ever used versions v0.11.20 through v0.11.23\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* seek in songs by scrollwheeling the seekbar (very popular request)\n* in the gridview...\n  * play audio files when the audio panel is open (press P to open it)\n  * navigate into subfolders without doing a full-page reload\n* when password is given in the URL (`?pw=wark`), copy into cookie for persistence\n\n## bugfixes\n* icon for the button to leave search results in grid-view\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0625-0110  `v0.11.26`  smooth\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20)\n* see [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) description if upgrading from something before that\n* see [v0.11.24](https://github.com/9001/copyparty/releases/tag/v0.11.24) description if you ever used versions v0.11.20 through v0.11.23\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* play/pause makes audio volume fade in/out\n* jump to start of song if previous-track button is pressed more than 3sec into it\n* media-controls are now default-enabled\n* censor user passwords in the server log\n\n## bugfixes\n* panic if pressing play/pause in a folder without music\n* send utf-8 header for all css/js files (fixes unicode/emotes in custom css)\n* when switching folders,\n  * clear the mediasession (currently playing track info in the OS)\n  * blank the audio seekbar \n* unlikely-to-encounter bugs:\n  * retry filesearch if client hits a ratelimit\n  * extremely-unlikely:\n    * fix autoplay of audio in some buggy chrome installs (not any specific version; depends on win10 settings or something)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0622-1528  `v0.11.24`  no cover\n\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20)\n* if upgrading from [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) or later and you use `-e2ts` to index audio tags:\n  * do a single run with `-e2tsr` to wipe and reindex audio tags to fix songs with bad titles\n  * if you have expensive `-mtp` parsers (bpm/key) and a huge database (or a slow server), then make a backup of the db before `-e2tsr` and use https://github.com/9001/copyparty/tree/master/bin#dbtoolpy to transfer your tags to the new db\n* however if upgrading from something before that, then your database will be wiped anyways so forget the `-e2tsr` stuff above, check the [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) notes instead\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## bugfixes\n* don't pollute audio tags with metadata about embedded album covers (and other similar crosstalk)\n* icon-generator: realize it's not a file extension when a whitespace appears\n* discard and regenerate corrupted databases instead of giving up\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0621-1915  `v0.11.23`  in control, mk.II\n\n* drop-in upgrade; no additional steps to consider since [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1)\n  * but see the description in [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) if upgrading from something before that\n* latest important update: [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20)\n* latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n![copyparty-osctl-fs8](https://user-images.githubusercontent.com/241032/122821375-0e08df80-d2dd-11eb-9fd9-184e8aacf1d0.png)\n* OS integration for the audio player\n  * show media controls on the OS lock-screen\n  * listen to media-hotkeys globally\n    * play/pause, next/prev track, seek fwd/back\n  * disabled by default; enable in the `🎺` tab\n\n## bugfixes\n* append current user's password to the cover URL so windows can actually display it\n* disable scandir for python 3.5 and older (no contextmgr)\n* disable u2idx (searching) if sqlite3 is not available\n* skip blank tags on np-clip\n\n## notes\nwhen you get tired of seeing the OSD popup which Windows doesn't let you disable: https://ocv.me/dev/?media-osd-bgone.ps1\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0620-1925  `v0.11.21`  database.avi\n\n* see [v0.11.20](https://github.com/9001/copyparty/releases/tag/v0.11.20) if upgrading from an older version\n  * aside from that, no additional steps to consider since [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1)\n* the latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## bugfixes\n* more responsive browser during db rebuilds\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0620-1732  `v0.11.20`  database.rmvb\n\n**this release will discard and rebuild your database** (`.hist/up2k.db`)\n* no actions necessary, it just takes a while\n* no additional steps to consider since [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1) actually\n* the old database will be backed up automatically just in case\n* if you have expensive `-mtp` parsers (bpm/key) and a huge database (or a slow server), you can transfer your tags to the new db using https://github.com/9001/copyparty/tree/master/bin#dbtoolpy\n\nreason: [v0.11.12](https://github.com/9001/copyparty/releases/tag/v0.11.12) changed the file checksum algorithm slightly, causing a mismatch between the server and client, and as a result:\n* upload deduplication has been unpredictable\n* filesearch could return false-negatives\n\n## new features\n* much faster filesearch in chrome\n* skip hidden colums in the /np text\n* support cygpaths when pointing to mtag tools\n\n## bugfixes\n* uploading folders through the up2k client would fail if the folder already existed on the server; now they merge\n* change up2k hashlen to 33 bytes / 44 chars (mod24 bits) to fit base64 better, avoiding any padding bugs\n* prefer client IP rater than proxy IP as fallback value when `--rproxy` is configured out of bounds\n* correct indexing of files with names containing backslash on linux/macos\n\n## other notes\n* the latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0618-2332  `v0.11.19`  purely cosmetic\n\n* drop-in upgrade; no additional steps to consider since [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1)\n* nothing big, no server fixes, just client tweaks\n* recent important updates:\n  * [v0.11.14](https://github.com/9001/copyparty/releases/tag/v0.11.14) fixed a deadlock in the thumbnails feature which was added in [v0.11.0](https://github.com/9001/copyparty/releases/tag/v0.11.0)\n* summary of other recent updates:\n  * [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18): audio preloading for near-gapless playback\n  * [v0.11.17](https://github.com/9001/copyparty/releases/tag/v0.11.17): fix thumbnail cache eviction (*finally* something that broke in [v0.11.12](https://github.com/9001/copyparty/releases/tag/v0.11.12))\n  * [v0.11.16](https://github.com/9001/copyparty/releases/tag/v0.11.16): more accurate audio equalizer\n* the latest gzip edition of the sfx is [v0.11.18](https://github.com/9001/copyparty/releases/tag/v0.11.18)\n\n## new features\n* audio player: add some shadow to the timestamps in the progressbar\n* audio player: silently stop playback if playing into a folder without music\n* general ui: make radio selections more visible by using another text color\n* general: smaller html responses by moving some stuff into the js (zopfli-compressed)\n\n## bugfixes\n* mobile devices: disable scrolling while viewing pictures in the lightbox\n* mobile devices: tooltips in the toolbar\n* android-chrome: text distortion in canvases when chrome decides to resize the viewport without invoking onresize like it should\n* android-chrome: initial layout in up2k due to the viewport size taking some time to settle down\n  * [totally appropriate fix](https://github.com/9001/copyparty/commit/57579b2fe5b86eaed062c050fb3c97d539db938f#diff-de02679bd0d9cdc88e772227cb23512033593913c128843e0bdf24810a786afaR1279-R1286)\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0617-2230  `v0.11.18`  seamless\n\n* drop-in upgrade; no additional steps to consider since [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1)\n* no important fixes, mostly new features\n* recent important updates:\n  * [v0.11.14](https://github.com/9001/copyparty/releases/tag/v0.11.14) fixed a deadlock in the thumbnails feature which was added in [v0.11.0](https://github.com/9001/copyparty/releases/tag/v0.11.0)\n\n## notes\nthis release includes [copyparty-sfx-gz.py](https://github.com/9001/copyparty/releases/download/v0.11.18/copyparty-sfx-gz.py), an additional sfx build which uses gzip compression rather than the usual bzip2; only useful for smoketests on minimal python builds. Note that both past and future releases can be converted from bzip2 to gzip by running [copyparty-repack.sh](https://github.com/9001/copyparty/blob/master/scripts/copyparty-repack.sh) on linux/macos/windows-msys2; this will produce the additional sfx in this release, `copyparty-extras/sfx-full/copyparty-sfx-gz.py` (see [sfx-repack](https://github.com/9001/copyparty#sfx-repack) for more info)\n\n## new features\n* **(almost) gapless audio playback!** partially powered by:\n  * url suffix `?cache` to get a response without any `Cache-Control` directives\n  * and using events for end-of-track instead of polling\n* hotkey `b` to toggle breadcrumbs / directory tree sidebar\n  * hotkey `p` is now play/pause\n  * hotkey `m` is now parent-directory\n* hilight the playing track in gallery mode too\n* toggle to disable the now-playing clipboard buttons\n* added lots of tooltips\n  * threw aray the competing tooltip implementations and did a single ok one\n* more accurate error-messages on upload failures due to filesystem permissions\n* add another output to the sfx repacker (gzip-compressed python sfx)\n\n## bugfixes\n* file selection after switching from grid to list\n* playback into next folder if the tree sidebar is closed\n* show the link to exit search results even if columns are hidden\n* make an effort to terminate clients cleanly on shutdown\n* py2 volume listing with `-e2d`\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0616-2231  `v0.11.17`  another media update\n\n* drop-in upgrade; no additional steps to consider since [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1)\n* recent important updates:\n  * [v0.11.14](https://github.com/9001/copyparty/releases/tag/v0.11.14) fixed a deadlock in the thumbnails feature which was added in [v0.11.0](https://github.com/9001/copyparty/releases/tag/v0.11.0)\n\n## new features\n* hotkey `m` for play/pause\n* make audio gain adjustable\n  * cranking it way up behaves differently depending on browser; firefox adds a compressor, chrome just ***goes***\n  * funfact, the base gain is `0.94` to avoid clipping due to imperfections in the equalizer curve\n* responsive settings layout\n* other minor ux tweaks\n  * brightmode contrast and player widget\n  * add gridlines to the files table\n* print summary when thumbcache cleanup finishes\n\n## bugfixes\n* the audio-eq ui didn't handle leading/trailing decimals too well\n* thumbcache-eviction mostly broke in [v0.11.12](https://github.com/9001/copyparty/releases/tag/v0.11.12) (and somehow nothing else so far)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0615-2351  `v0.11.16`  better eq\n\n* drop-in upgrade; no additional steps to consider since [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1)\n* recent important updates:\n  * [v0.11.14](https://github.com/9001/copyparty/releases/tag/v0.11.14) fixed a deadlock in the thumbnails feature which was added in [v0.11.0](https://github.com/9001/copyparty/releases/tag/v0.11.0)\n\n## new features\n* media player can now continue into the next folder\n* eq curve supports both positive and negative values (and scales down to avoid clipping)\n* browser columns now fully hide when hidden; reenable them in the settings tab\n* other ux tweaks\n  * add some icons\n  * tree control buttons remain visible when scrolling\n\n## bugfixes\n* calibrated the eq for more correct frequency response\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0614-2201  `v0.11.15`  v for victory\n\n* drop-in upgrade; no additional steps to consider since [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1)\n* recent important updates:\n  * [v0.11.14](https://github.com/9001/copyparty/releases/tag/v0.11.14) fixed a deadlock in the thumbnails feature which was added in [v0.11.0](https://github.com/9001/copyparty/releases/tag/v0.11.0)\n\n## new features\n* audio equalizer (with a v-shaped default)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0614-0105  `v0.11.14`  frozen\n\n* this release fixes a deadlock in the thumbnails feature introduced in [v0.11.0](https://github.com/9001/copyparty/releases/tag/v0.11.0)\n  * if you cannot upgrade for some reason, use `--no-thumb` to avoid it\n* drop-in upgrade; no additional steps to consider since [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1)\n\n## new features:\n* `--rproxy` specifies which IP to display in logs when reverse-proxied\n  * defaults to `1` which is the origin / actual client\n* `--stackmon` periodically dumps a stacktrace to a file for debugging\n\n## bugfixes:\n* deadlock when converting thumbnails\n* up2k-cli: recover from network errors during handshakes\n  * have to fix chunks too eventually\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0612-1837  `v0.11.13`  image gallery\n\n[v0.11.11](https://github.com/9001/copyparty/releases/tag/v0.11.11) is the latest well-tested version (\"stable\"), maybe keep that as a fallback\n* otherwise a drop-in upgrade; no additional steps to consider since [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1)\n\n## recent updates\nnothing really important happened since [v0.11.6](https://github.com/9001/copyparty/releases/tag/v0.11.6); quick summary:\n* [v0.11.9](https://github.com/9001/copyparty/releases/tag/v0.11.9): fix zip/tar of recursive symlinks\n* [v0.11.10](https://github.com/9001/copyparty/releases/tag/v0.11.10): fix direct tls connections\n* [v0.11.11](https://github.com/9001/copyparty/releases/tag/v0.11.11): fix live-rescan without a root folder\n* this ver only adds new features\n\n## new features:\n* image gallery / lightbox\n\n## notes\nif you want filetype icons on the thumbnails then check out [browser-icons.css](https://github.com/9001/copyparty/tree/master/docs#example-browser-css)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0612-0228  `v0.11.12`  excuse the mess\n\nbig changes, **bugs likely**, keep [v0.11.11](https://github.com/9001/copyparty/releases/tag/v0.11.11) as a fallback and go whine in the irc\n* otherwise a drop-in upgrade; no additional steps to consider since [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1)\n\nnothing really important happened since [v0.11.6](https://github.com/9001/copyparty/releases/tag/v0.11.6); quick summary:\n* [v0.11.9](https://github.com/9001/copyparty/releases/tag/v0.11.9): fix zip/tar of recursive symlinks\n* [v0.11.10](https://github.com/9001/copyparty/releases/tag/v0.11.10): fix direct tls connections\n* [v0.11.11](https://github.com/9001/copyparty/releases/tag/v0.11.11): fix live-rescan without a root folder\n* this ver only fixes unlikely edge-cases\n\n## new features:\n* folder thumbnails if they contain `folder.jpg` or `folder.png`, good for music servers\n* `--hist` stores the per-volume databases and thumbnails all in one place, instead of the `.hist` subfolders in each volume\n* `--no-hash` disables file hashing, good for a simple searchable index, but keep in mind it disables file-search and dupe detection\n  * both this and `--hist` can be adjusted per-volume with volflags, see readme\n* thumbnails keep transparency\n* `--th-ff-jpg` fixes video thumbnails if your FFmpeg is bad (macos)\n* more info in the [admin panel](https://user-images.githubusercontent.com/241032/121763646-15422780-cb3e-11eb-8932-130af39acb48.png) (num.files queued for hashing or tags)\n* `--css-browser` to set [custom CSS](https://user-images.githubusercontent.com/241032/121763647-15dabe00-cb3e-11eb-90a4-1628a072545c.png)\n  * use `.prologue.html` or `.epilogue.html` to do this per-folder; that allows for javascript too\n* cygpaths for windows, `-v c:\\users::r` and `-v /c/users::r` both work now\n* extremely minor (i think) performance improvements which probably drown in the new bloat\n\n## bugfixes:\n* mounting a volume deep inside another volume will no longer create additional databases, avoiding rescan of files in intermediate folders\n  * backwards-compat so it will continue to use any intermediate databases made by v0.11.11 or older\n* better error message on basic-upload into a folder that doesn't exist / without permission\n* minor race introduced in 0.11.1 which could be triggered by an upload really early after starting the server\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0608-2143  `v0.11.11`  re:live\n\n* drop-in upgrade, no additional steps since [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1)\n* nothing really important since [v0.11.6](https://github.com/9001/copyparty/releases/tag/v0.11.6); quick summary:\n  * [v0.11.9](https://github.com/9001/copyparty/releases/tag/v0.11.9): fix zip/tar of recursive symlinks\n  * [v0.11.10](https://github.com/9001/copyparty/releases/tag/v0.11.10): fix direct tls connections\n  * this ver: fix live-rescan without a root folder\n\n## new features\n* threadnames in the stackdump\n  * also truncate/censor filepaths\n  * most of the idle threads are indented + appear last\n* up2k scans folders alphabetically (easier to eyeball progress)\n* slightly better performance when sending files\n  * and other minor performance tweaks\n* sfx: all js/css files are zopfli-compressed\n  * makes sfx bigger but resources are now 1/3 the size in transit\n\n## bugfixes\n* another live-rescan fix (for configs without a root-folder)\n* fix janky load-balancing with `-jN`\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0608-0741  `v0.11.10`  dont leave me hangin\n\n* drop-in upgrade, no additional steps since [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1)\n* nothing really important since [v0.11.6](https://github.com/9001/copyparty/releases/tag/v0.11.6)\n  * [v0.11.9](https://github.com/9001/copyparty/releases/tag/v0.11.9): fix zip/tar of recursive symlinks\n  * this ver: fix direct tls connections\n\n## bugfixes\n* actually close tls connections\n  * only affects direct https connections (no reverse-proxy between)\n  * mainly problematic for zip/tar downloads\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0607-1822  `v0.11.9`  caught in a loop\n\n* nothing too important (unless you have recursive symlinks somewhere)\n* drop-in upgrade, no additional steps since [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1)\n\n## new features\n* `--ls` prints empty directories as well\n  * so now links like that will be detected too\n\n## bugfixes\n* detect recursive symlinks when creating zip/tar files\n  * the first iteration will be archived, then it bails\n* support python 3.5 on windows by autosetting `--no-scandir`\n* `sfx.sh` correctly disables bundled jinja2 when found on system\n\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0606-1709  `v0.11.8`  sharex\n\n* nothing important\n* drop-in upgrade, no additional steps since [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1)\n\n## new features\n* json replies from `bput` (basic uploader) by adding url parameter `j`\n  * better sharex support, especially for interesting filenames\n* append the filename extension when renaming uploads to avoid filename collisions\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0605-0133  `v0.11.7`  additional vtec\n\n* nothing too important\n* drop-in upgrade, no additional steps since [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1)\n\n## new features\n* add [hash-wasm](https://github.com/Daninet/hash-wasm) as preferred fallback up2k hasher, does 250 MiB/s so like 7x faster\n  * still keeping `asmCrypto` for older browsers but minified a bit\n  * technically this allows for a single sha512 over the whole file rather than chunks...\n* in gallery mode, open files in a new tab if there's a selection active\n* `--ls`, which can be used to look for dangerous symlinks\n  * `--ls '**,*,ln,p,r'` does a full scan of all volumes (as all users) and refuses to start if there are links leaving the vols (see `--help`)\n* other minor optimizations\n\n## bugfixes\n* metadata indexing with single-threaded backends\n* loader animation appears over thumbnails too\n* restore support for firefox 12\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0601-0625  `v0.11.6`  vtec\n\n### things to know when upgrading:\n* see release-notes for [v0.11.0](https://github.com/9001/copyparty/releases/tag/v0.11.0) and [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1) as they introduced new features you may wish to disable\n\n### new features:\n* searching for audio tags is now literally 1000x faster\n  (almost as fast as the version numbers recently)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0601-0155  `v0.11.4`  please upgrade\n\n## important news:\n* this release fixes a missing permission check which could allow users to download write-only folders\n  * this bug was introduced 19 days ago, in `v0.10.17`\n  * the requirement to be affected is write-only folders mounted within readable folders\n  * and the worst part is there was a unit-test exactly for this, https://github.com/9001/copyparty/commit/273ca0c8 way overdue\n* also fixes minor bugs introduced in `v0.11.1`\n* this version is the same as `v0.11.5` on pypi\n\n----\n\n### things to know when upgrading:\n* see [v0.11.0](https://github.com/9001/copyparty/releases/tag/v0.11.0) and [v0.11.1](https://github.com/9001/copyparty/releases/tag/v0.11.1) as they introduce new features you may wish to disable\n  * especially the `dbtool` part if your database is huge\n\n### new features:\n* filesearch now powered by a boolean query syntax\n  * the regular search interface generates example queries\n  * `size >= 2048 and ( name like *.mp4 or name like *.mkv )`\n\n### bugfixes:\n* scan files on upload (broke in 0.11.1)\n* restore the loud \"folder does not exist\" warning (another 0.11.1)\n* fix thumbnails in search results (never worked)\n\n#### really minor stuff:\n* increased default thumbnail clean interval from 30min to 12h\n* admin panel also links to the volumes\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0529-2139  `v0.11.1`  do it live\n\nno important bugfixes, just new features\n\n### things to know when upgrading:\n* `--no-rescan` disables `?scan`, a new feature which lets users initiate a recursive scan for new files to hash and read tags from\n  * this is enabled per-volume for users with read+write access\n* `--no-stack` disables `?stack`, a new feature which shows a dump of all the stacks\n  * this is enabled if a user has read+write on at least one folder\n* if you wish to wipe the DB and rebuild it to get the new metadata collected as of v0.11.0, and you have expensive `-mtp` parsers (bpm/key) and a huge database (or a slow server), consider https://github.com/9001/copyparty/tree/master/bin#dbtoolpy\n\n### new features:\n* **live rescan!** no more rebooting if you add/move files outside of copyparty and want to update the database, just hit the rescan button in the new...\n* **admin panel!** access `/?h` (the old control-panel link) to see it\n* **fast startup!** added 40TB of music? no need to wait for the initial scan, it runs in the background now\n  * when this turns out to be buggy you can `--no-fastboot`\n  * uploading is not possible until the initial file hashing has finished and it has started doing tags\n    * you can follow the progress in the new admin panel\n\n### bugfixes:\n* windows: avoid drifting into subvolumes and doublehashing files\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0529-1303  `v0.11.0`  welcome to the grid\n\nno important bugfixes, just new features\n\n### things to know when upgrading:\n* `Pillow` and `FFmpeg` is now used to generate thumbnails\n  * `--no-thumb` disables both\n  * `--no-vthumb` disables just `FFmpeg`\n* new optional dependencies:\n  * `Pillow` to enable thumbnails\n    * `pyheif-pillow-opener` to enable reading HEIF images\n    * `pillow-avif-plugin` to enable reading AVIF images\n    * `ffmpeg` and `ffprobe` to enable video thumbnails\n* if you wish to wipe the DB and rebuild it to get the new metadata collected as of this version, and you have expensive `-mtp` parsers (bpm/key) and a huge database (or a slow server), consider https://github.com/9001/copyparty/tree/master/bin#dbtoolpy\n\n### new features:\n* thumbnails! of both static images and video files\n  * served as webp or jpg depending on browser support\n  * new hotkeys: G, T, S, A/D\n* additional metadata collection with `-e2ts`\n  * audio/video codecs, video/image resolution, fps, ...\n  * if you wanna reindex, do a single run with `-e2tsr` to wipe the DB\n* mtp can collect multiple tags at once\n  * expects json like `{ \"tag\": \"value\" }`, see end of https://github.com/9001/copyparty/blob/master/bin/mtag/exe.py\n\n### bugfixes:\n* when sorting by name, show folders first\n* mimetypes for webp and opus on GET\n* mojibake support\n  * up2k into mb folder\n  * indexing files in mb folders\n  * editing markdown in mb folders\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0518-0210  `v0.10.22`  this is a no drift zone\n\n* browser: fix off-by-one which made the page slowly shrink back down when navigating away from a large folder\n* browser/mediaplayer: handle unsupported audio codecs better in some (older?) browsers\n* readme/requirements: firefox 34 and chrome 41 were the first browsers with native sha512 / full speed in up2k\n* and the feature nobody asked for:\n![2021-0518-041625-hexchat-fs8](https://user-images.githubusercontent.com/241032/118581146-67876700-b791-11eb-99c0-f1f5ace50797.png)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0516-1822  `v0.10.21`  fix tagger crash\n\na\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0516-0551  `v0.10.20`  inspect\n\nnothing important this time, just new bling and some fixes to support old browsers  \n(well except for the basic-uploader summary autoclosing immediately on completion, that was kinda user-confusing)\n\n* add `ad`/`an` flags to `-mtp`; collect and display metadata from any file, not just audio-files\n* up2k speedboost on older iPhones (native hashing on safari 7 through 10)\n* add `--lf-url`, URL regex to exclude from log, defaults to `^/\\.cpr/` (static files)\n* add `--ihead` to print specific request headers, `*` prints all\n* ux fixes\n  * include links to the uploaded files in bup summaries\n  * ...also make the bup summary not auto-close\n  * don't link to bup from up2k if read-only access\n  * toggle-switch for tooltips also affects the up2k ui\n  * stop flipping back to up2k on older browsers\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0513-2200  `v0.10.19`  imagine running servers on windows\n\n* fix: uploads when running copyparty on windows (broke in 0.10.18)\n* fix: bup uploads would not get PARTIAL-suffixed if the filename length hits filesystem-max and the client disconnects mid-upload \n* add `--dotpart` which hides uploads as dotfiles until completed\n* very careful styling of the basic-browser\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0513-1542  `v0.10.18`  just 302 it my dude\n\n* stop trying to be smart, do full redirects instead\n* allow switching to basic-browser using cookie `b=u`\n* fix mode-toggling (upload/search) depending on folder permissions\n* persist/clear the password cookie with expiration\n* slight optimizations for rclone clients\n* other minor ui tweaks\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0512-2139  `v0.10.17`  denoise\n\n* allow navigating to write-only folders using the tree sidebar\n* show logues (prologue/epilogue) in write-only folders as well\n  * rename `.prologue.html` / `.epilogue.html` when uploaded so people can't embed javascript\n* support pyinstaller\n* hide more of the UI while in write-only folders\n  * hide [even more](https://a.ocv.me/pub/g/nerd-stuff/cpp/2021-0513-ui-mod.png) using [lovely hacks](https://github.com/9001/copyparty/blob/master/docs/minimal-up2k.html)\n* add a notice in bup that up2k is generally better\n\nalternative title: [Petit Up2k's - No Gui!](https://www.youtube.com/watch?v=IreeUoI6Kqc)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0502-0718  `v0.10.16`  somebody used -c\n\n* cfg-file: fix shorthand for assigning permissions to anonymous users\n* sfx: `-j` works on python3 (pickle did not enjoy the binary comments)\n* sfx: higher cooldown before it starts deleting tempfiles from old instances\n* sfx: should be a bit smaller (put compressed blobs at the end of the tar)\n* misc minor ui tweaks, mostly the bright-mode theme\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0424-0205  `v0.10.15`  write-only volumes are write-only\n\ngood thing it was so obviously broken and/or that nobody ever tried to use it\n* regression test added to keep it fixed\n* can now make a hidden/inaccessible folder (optionally inside a public folder) like `-v /mnt/nas/music:/music:r -v /mnt/nas/music/inc:/music/inc:w`\n\nin other news, minor ui tweaks:\n* clickdrag in the media player sliders doesn't select text any more\n* a few lightmode adjustments\n* less cpu usage? should be\n\n`copyparty-sfx.py` (latest) made from c5db7c1a (v0.10.15-15) fixes `-j` (multiprocessing/high-performance)\n`copyparty-sfx-5a579db.py` (old) made from 5a579dba (v0.10.15-5) reduced the size\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0421-2004  `v0.10.14`  sparse4win\n\n# great stuff\n* firefox no longer leaking memory like crazy during large uploads\n    * not fixed intentionally (the firefox bug still exists i think)\n        * one of the v0.10.x changes are accidentally avoiding it w\n\n# good stuff\n* up2k-cli: conditional readahead based on filereader latency (firefox was not happy)\n* up2k-srv: make sparse files on windows if larger than `--sparse` MiB\n    * files will unsparse when upload completes if win10 or newer\n    * performance gain starts around 32 and up but default is 4 to save the SSDs\n* up2k-cli: fix high cpu usage after returning to idle\n* up2k-cli: ui tweaks\n* browser: give 404 instead of redirecting home when folder is 404 or 403\n* md-srv: stream documents rather than load into memory\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0420-2319  `v0.10.13`  moon\n\n600 MiB/s for both hashing and uploading on a ryzen 3700\n\n* up2k: hashing 2x faster than before\n    * except on android-chrome where it is now slightly slower because the android file api is a meme\n        * ...but android-firefox gained 4x and is now 3x faster than chrome, google pls\n\nthis concludes the optimization arc\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0419-1958  `v0.10.12`  rocket\n\nup2k [way faster on large files](https://a.ocv.me/pub/g/nerd-stuff/cpp/2021-0419-up2k.webm) this time\n* js: removed a cpu bottleneck in the up2k client\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0419-1517  `v0.10.10`  blinded by the light\n\n* up2k: fix progress bars\n* up2k: more specific error messages (for example when trying to up a rangelocked file)\n* browser: link to timestamps in media files (media fragment urls)\n* fix crash when trying to -e2ts without the necessary dependencies available\n* since there wasn't enough pointless features that nobody will ever use already: added lightmode\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0416-2329  `v0.10.9`  fasten your seatbelts\n\n* up2k: [way faster](https://a.ocv.me/pub/g/nerd-stuff/cpp/2021-0416-up2k.webm) when uploading a large number of files\n  * 2x faster at 500 files, 3x faster at 1000, **8x at 3000**\n* up2k: show ETA and upload/hashing speeds in realtime\n* browser: hide search tab when database disabled\n* avoid crash on startup when mounting the root of a restricted smb share on windows, [cpython bug](https://bugs.python.org/issue43847)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0411-1926  `v0.10.8`  misc\n\nnothing massive, just a bunch of small things\n\n* browser: fix zip download on iphone/android\n* sfx: prevent StorageSense from deleting copyparty while it's running\n* browser: less tree jitter when scrolling\n* browser: only capture hotkeys without modifiers\n* up2k: add some missing presentational uridecodes\n* browser: add `?b` for an extremely minimal browser\n    * `?b=u` includes the uploader\n* browser: somewhat support `?pw=hunter2` in addition to the cppwd cookie\n* make-sfx: optional argument `gz` to build non-bz2 sfx\n* stop crashing argparse on pythons <= june 2018\n* support http/1.0\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0402-2235  `v0.10.7`  thx exci\n\nup2k-client fixes:\n* uploads getting stuck if more than 128 MiB was rejected as dupes\n* displayed links on rejected uploads\n* displayed upload speed was way off\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0402-0111  `v0.10.6`  enterprise ready\n\nminimal-effort support for really old browsers\n* internet explorer 6 can browse, upload files, mkdir\n* internet explorer 9 can also play mp3s, zip selected files\n* internet explorer 10 and newer has near-full support\n* the final version of chrome and firefox on xp have full support\n* netscape 4.5 works well enough, text is yellow on white\n* [netscape 4.0 segfaults](https://a.ocv.me/pub/g/nerd-stuff/cpp/2021-0402-netscape.png) (rip)\n\non a more serious note,\n* fix multiselect zip diving into unselected subfolders\n* decode urlform messages to plaintext\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0330-2328  `v0.10.5`  search fix\n\n* fix audio playback in search results (broke in v0.9.9)\n* sort search results according to userdefined order\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0329-1853  `v0.10.4`  stablesort\n\nrunning out of things to fix so here are nitpicks\n* stable sort when sorting multiple columns\n* default to filenames with directories first (column 2 + 1)\n* remove some console spam\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0329-0247  `v0.10.3`  not slow as tar\n\nnothing too big this time,\n* tar 6x faster (does 1.8 GiB/s now)\n* fix selective archiving of subfolders\n* mute the loadbalancer when `-q`\n* don't show 0:00 as duration for non-audio files\n\nknown inconvenience since 0.9.13 that won't ever be fixed:\nif you use the subfolder hiding thing (`-v :foo/bar:cd2d`) it creates intermediate volumes between the actual volume and the hidden subfolder which kinda messes with existing indexes (it will reindex stuff inside the intermediate volumes) but everything still works so it's just a pain\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0327-1703  `v0.10.2`  do i have to think of a name\n\n* select multiple files/folders to download as tar/zip\n* recover from read-errors when zipping things, adding a textfile in the zip explaining what went wrong\n* fix permissions in zip files for linux/macos unpacking\n* make the first browser column sortable\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0327-0144  `v0.10.1`  zip it\n\n* download folders as .zip or .tar files\n* upload entire folders by dropping them in\n* 4x faster response on the first request on each new connection\n\nforgot to explain the zip formats\n\n| name | url-suffix | description |\n|--|--|--|\n| `tar` | `?tar` | a plain gnutar, works great with `curl \\| tar -xv` |\n| `zip` | `?zip=utf8` | works everywhere, glitchy filenames on win7 and older |\n| `zip_dos` | `?zip` | traditional cp437 (no unicode) to fix glitchy filenames |\n| `zip_crc` | `?zip=crc` | cp437 with crc32 computed early for truly ancient software |\n\n`zip_crc` will take longer to download since the server has to read each file twice, please let me know if you find a program old enough to actually need it btw, curious\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0323-0113  `v0.9.13`  micromanage\n\nyou can skip this version unless your volume setup is crazy advanced\n\n* support hiding specific subfolders with `-v :/foo/bar:cd2d`\n* properly disable db/tags/etc when `cd2d` or `cd2t` volflags are set\n* volume info on startup is prettier\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0321-2105  `v0.9.10`  nurupo\n\nnot so strong anymore\n\n* fixes a nullpointer when sorting a folder that contains markdown revisions\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0321-1615  `v0.9.9`  the strongest\n\n## big ones\n* add support for external analysis tools to provide arbitrary tags for the index\n* add example tools for detecting bpm and melodic key\n  * https://github.com/9001/copyparty/tree/master/bin/mtag\n* add range-search (duration/bpm/key/... between min/max values)\n* hotkeys for changing songs + skipping\n  * `0..9`=jump, `J/L`=file, `U/O`=10sec, `K/I`=folder, `P`=parent\n\n## the rest\n* add search timeouts and rate-control on both server/client-side\n* add time markers in the audio player\n* remember the file browser sort order\n  * the initial html retains server order, so use the tree to navigate\n* fix a race in the tag parser when using the multithreaded FFprobe backend\n* fix minor stuff related to volume flags and tag-display options\n* repacker should no longer break the bundled jinja2\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0315-0013  `v0.9.8`  the strongest for a while\n\nnothing more to add or fix for now (barely avoided adding bpm/tempo detection using keyfinder and vamp+qm since thats just too ridiculous)\n\n* browser: correct music playback order after sorting\n* browser: no more glitching on resize in non-tree-mode\n* fuse-client: read password from `some.txt` with `-a $some.txt`\n* sfx: reduce startup time by 20% or so (import rather than shell out)\n* sfx: support pypy, jython, and ironpy\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0308-0251  `v0.9.7`  the strongest hotfix 2nd season\n\n* actually fix it so it doesn't truncate in the first place\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0307-2044  `v0.9.6`  the strongest hotfix\n\n* don't crash the file browser on truncated table rows\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0307-1825  `v0.9.5`  the strongest potions\n\n* better support for mojibake filenames\n* separate scrollbar for the directory tree\n* stop persisting page data in the browser, reload on each navigation\n  * firefox disapproves of storing >= 4 MB of json in sessionStorage\n* normalization of musical keys collected from tags\n* recover from dying tag parsers\n* be nice to rhelics\n  * add support for the 2013 edition of sqlite3 in rhel 7\n  * and fix some py2 issues with `-e2d`, again thx to ^\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0305-0106  `v0.9.4`  the strongest orz\n\nmarkdown editor works\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0304-2300  `v0.9.3`  the strongest performance\n\ngotta go fast\n\n| | windows | linux | macos |\n|--|--|--|--|\n| file browser / directory listing | *15 times* faster | 2% slower sorry | 15% faster |\n| startup / `-e2ds` verification | 10% faster | even | 10% faster |\n| reading tags with ffprobe | 5 times faster | 4 times faster | 2 times faster |\n\n## new features\n* async scan incoming files for tags (from up2k, basic-upper, PUT)\n* resizable file browser tree\n\n## bugfixes\n* floor mtime so `-e2ds` doesn't keep rescanning\n* use localStorage for pushState data since firefox couldn't handle big folders\n* minor directory rescan semantics\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0303-0028  `v0.9.1`  the strongest bugs\n\nimagine downloading a .0\n* fix file search / search by contents\n* stop spamming responses with `{\"tags\":[\"x\"]}`\n* recover from missing writable volumes during startup\n* redo search when filter-checkboxes are toggled\n* 1.5x faster client-side sorting\n* 1.02x faster server-side\n\nand i just realized i never added runtime tag scanning so copyparty will have to be restarted to see tags of new uploads, TODO for next ver\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0301-2312  `v0.9.0`  the strongest music server\n\n* grab tags from music files and make them searchable\n* and show the tags in the file browser\n* make all the browser columns minimizable\n* shrink the media player widget thing on big screens\n\nuse `-e2dsa` and `-e2ts` to enable the media tag features globally, or enable/disable them per-volume (see readme)\n\n**NOTE:** older fuse clients (from before 5e3775c1  / older than v0.8.0) must be upgraded for this copyparty release, however the new client still supports connecting to old servers\n\nother changes include\n* support chunked PUT requests from curl\n* fix a pypy memleak which broke sqlite3\n* fix directory tree sidebar breaking when nothing is mounted on `/`\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0222-2058  `v0.8.3`  discovery\n\nforgot to update the release name for 0.8 (which introduced searching and directory trees), good opportunity to name it after a dope album with some absolute bangers\n\naside from the release name this version is entirely unrevolutionary\n\n* fixed debug prints on xp / win7 / win8 / early win10 versions\n* load prologues/epilogues when switching between folders\n* fix up2k modeswitching between read/write folders\n* additional minor ux tweaks\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0222-0254  `v0.8.1`  the ux update\n\n* search by name/path/size/date\n* search by file contents\n* directory tree sidebar thing\n  * navigate between folders while uploading\n\nNOTE: this will upgrade your `up2k.db` to `v2` but it will leave a backup of the old version in case you need to downgrade or whatever\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0214-0113  `v0.7.7`  trafikklys\n\n* new checkbox in up2k which coordinates uploading from multiple tabs\n  * if one tab is uploading, others will wait\n* fix up2k handshakes so uploads complete faster\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0212-1953  `v0.7.6`  nothing big\n\n* up2k: resume hashing when <= 128 MiB left to upload\n* stop showing `up2k.db/snap` in the file list\n* fix `--ciphers help`\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0212-0706  `v0.7.5`  you can https if you want to\n\n* fix https on python3 after breaking it in v0.6.3\n  * workaround for older versions: `--no-sendfile`\n  * don't use the native https anyways (pls reverse-proxy)\n* that said, added a bunch of ssl/tls/https options\n  * choice to only accept http or https\n  * specify ssl/tls versions and ciphers to allow\n  * log master-secrets to file\n  * print cipher overlap on connect\n* up2k indexer flushes to disk every minute\n* up2k indexer mentions the filepath on errors\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0204-0001  `v0.7.4`  a\n\n* sfx: save 43kB by replacing all docstrings with \"a\"\n* sfx: upgrade the bundled jinja2 and markupsafe\n    * zero dependencies on python3 as well now\n* do something useful with url-encoded POSTs\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0202-2357  `v0.7.3`  Hey! Listen!\n\n* bind multiple IP's / port ranges\n* dim the connection tracking messages a bit\n* stop gz/br unpacker from being too helpful\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0128-2352  `v0.7.2`  QUALITY\n\n* make up2k confirmations optional\n* let pending uploads stay for 6 hours\n* fix the 0.7.1 regression we won't talk about\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0123-1855  `v0.7.1`  checking it twice\n\n* up2k-client shows an OK/Cancel box before upload starts\n* up2k-client hashes the next pending file at most\n  * previously, all pending uploads were announced immediately\n* fix edgecase when the registry snapshot contained deleted files\n* delete all related files after 1h if an up2k upload was initiated but never started\n  * previously, the `.PARTIAL` (upload data) was kept, even when blank\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0110-1649  `v0.7.0`  keeping track\n\n## remember all uploads using `-e2d` to avoid duplicates\n* `-e2d` stores the up2k registry in a per-volume sqlite3 database at `$VOL/.hist/up2k.db`\n* unfinished uploads are indexed in `$VOL/.hist/up2k.snap` every 30 seconds\n* unfinished uploads which are idle for over 1 hour are forgotten\n* duplicate uploads will be symlinked to the new name (by default) or rejected\n\n## build an index of all existing files at startup using `-e2s`\n* ...so copyparty also knows about files from older versions / other sources\n* this detects deleted/renamed files and updates the database\n\n## reject duplicate uploads instead of symlinking\n* this is a per-volume config option, see the `cnodupe` example in `-h`\n* the uploader gets an error message with the path to the existing file\n\n## other changes\n* uploads temporarily have the extension `.PARTIAL` until the upload is completed\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2021-0107-0009  `v0.6.3`  no nagles beyond this point\n\n* reduce latency of final packet by ~0.2 sec\n* use sendfile(2) when possible (linux and macos)\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2020-1214-0328  `v0.6.2`  happy end of 2020\n\n* support uploads with massive filenames\n* list world-readable volumes when logged in\n* up2k-client: ignore rejected dupe uploads\n* sfx-repack: support wget\n* dodge python-bug #7980 \n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2020-1201-0158  `v0.6.0`  CHRISTMAAAAAS\n\nhttps://www.youtube.com/watch?v=rWc9XuqwoLI\n* md cleanup/fixes (thx eslint)\n* fix the sfx repacker\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2020-1130-0201  `v0.5.7`  multiuser notepad\n\nnot in the etherpad sense but rather\n* md: poll for changes every `-mcr` sec and warn if doc changed\n* md: prevent closing the tab on unsaved changes\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2020-1129-1849  `v0.5.6`  the extra mile\n\n* use git tag/commit as version when creating sfx\n* md: table prettyprinter compacting properly\n* md/plug: add error handling to the plugins\n* md/plug: new feature to modify the final dom tree\n* md/plug: actually replace the plugin instances rather than keep adding new ones tehe\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2020-1127-0225  `v0.5.5`  far beyond\n\nvalvrave-stop.jpg\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2020-1117-2258  `v0.5.4`  edovprim\n\n(get it? becasue reverse proxy haha)\n\n* reverse-proxy support\n* filetype column in the browser\n* md-edit: table formatter more chill\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2020-1113-0231  `v0.5.3`  improved\n\n* show per-connection and per-transfer speeds\n* restore macos support in sfx.sh\n* http correctness fixes\n  * SameSite=Lax\n  * support multiple cookies in parser\n  * `+` no longer decodes to ` `, goodbye netscape 3.04\n* fuse stuff\n  * python client: mojibake support on windows\n  * python client: https and password support\n  * support rclone as client (windows/linux)\n* new markdown-editor features\n  * table formatter\n  * mojibake/unicode hunter\n  * more predictable behavior\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2020-0818-1822  `v0.5.2`  da setter vi punktum\n\nfull disclaimer: `copyparty-sfx.py` was built using `sfx.py` from ~~82e568d4~~ f550a817\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2020-0817-2155  `v0.5.1`  insert soho joke\n\n* add info-banner with hostname and disk-free\n* make older firefox versions cache less aggressively\n* expect less correctness from cots nas\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2020-0816-2304  `v0.5.0`  fuse jelly\n\n* change default port from `1234` to `3923`\n* fuse 10x faster + add windows support\n* minimal CORS support added\n* PUT stuff from a browser-console or wherever\n* markdown editor improvements again\n  * paragraph-jump with ctrl-cursors\n  * fix firefox not showing the latest ver on F5\n* fix systemd killing the sfx binaries (ﾉ ﾟヮﾟ)ﾉ ~┻━┻\n* not actually related to the tegra exploit\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2020-0517-1446  `v0.4.3`  🌲🪓🎉\n\n* print your documents! kill the trees!\n* drop support for opus/vorbis audio playback on iOS 11 *and older*\n* chrome's now twice as fast in the markdown editor\n  * firefox still wins\n* upgrade to marked.js v1.1.0\n* minor fuse + ux fixes\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2020-0514-2302  `v0.4.2`  still not quite emacs (the editor is too good)\n\n* better editor cursor behavior\n* better editor autoindent\n* less broken fuse client\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2020-0513-2308  `v0.4.1`  Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience\n\n* better editor performance in massive documents\n* better undo/redo cursor positioning\n* better ux on safari\n* better ux on phones\n* better\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2020-0512-2244  `v0.4.0`  NIH\n\n* new \"basic\" markdown editor\n  * textarea-based, way less buggy on phones\n  * better autoindent + undo/redo\n* smaller sfx (~170k)\n* osx fixes\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2020-0506-2220  `v0.3.1`  v0.3.1\n\n* indicate version history for files in the browser\n* (also move old versions into .hist subfolders)\n* handle uploads with illegal filenames on windows\n* sortable file list\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2020-0505-2302  `v0.3.0`  docuparty\n\n\"why does a file server have a markdown editor\"\n\n\n\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \n# 2020-0119-1512  `v0.2.3`  hello world\n\n\n"
  },
  {
    "path": "docs/chungus.conf",
    "content": "# not actually YAML but lets pretend:\n# -*- mode: yaml -*-\n# vim: ft=yaml:\n\n\n## this config-file does not make any sense at all, and will not work anywhere\n##\n## it is a nearly-complete example of every config option and how each can be used,\n## but will NOT be maintained, it's just to get a feel for how the config works\n##\n## this file was initially generated by and based on:\n## cat copyparty/__main__.py | awk -F\\\" -vp1=$(printf %090d 0) '/arse.SUPPRE/{next}/add_argument_group/{printf\"\\n  ###%s\\\\\\n  ###// %s \\\\\\\\%s\\n\",p1,$2,p1};{m=\"\"}/metavar=/{m=\": \"$4}/add_argument\\(/{h=$0;sub(/.*, help=\"/,\"\",h);sub(/\"\\)$/,\"\",h);k=$2;sub(/^-+/,\"\",k);printf\"\\n  # %s\\n  %s%s\\n\",h,k,m}' | sed -r 's/^(  ###\\/\\/ .{88})0+/\\1\\\\/;s/([^#]{40})   +/\\1/;s/\\\\033\\[[^m]*[0-9]m//g;s/ \\(volflag=([^)]+)\\)$/\\n  # 📂 also available as volflag \"\\1\"/' | xsel -ib\n## grep -A9001 ^flagcats copyparty/cfg.py | grep -B9001 ^flagdescs | awk -F\\\" -vp1=$(printf %090d 0) '/^    \"/{printf\"\\n    ###%s\\\\\\n    ###// %s \\\\\\\\%s\\n\",p1,$2,p1};/^        [^ ]/{k=$0;h=$0;sub(/^ +./,\"\",k);sub(/.: .*/,\"\",k);sub(/[^:]+: ./,\"\",h);sub(/.,$/,\"\",h);sub(/=/,\": \",k);printf\"\\n    # %s\\n    %s\\n\",h,k}' | sed -r 's/^(    ###\\/\\/ .{88})0+/\\1\\\\/;s/([^#]{40})   +/\\1/' | xsel -ib\n\n\n[global]\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// general options \\\\0000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # REPEATABLE: add a config file; add multiple by repeating the option\n  c: /etc/some.conf\n  c: /etc/another.conf\n\n  # max num clients; will not accept more http/https connections at this point\n  nc: 1024\n\n  # max num cpu cores, 0=all, 1=default=recommended\n  j: 4\n\n  # enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files\n  # 📂 also available as volflag \"dots\"\n  ed\n\n  # how to handle url-form POSTs; see --help-urlform\n  urlform: save,get\n\n  # server terminal title, for example [$ip-10.1.2.] or [$ip-]\n  wintitle: $ip-10.1.2.\n\n  # server name (displayed topleft in browser and in mDNS)\n  name: mogra\n\n  # REPEATABLE: map file EXTension to MIMEtype, for example [jpg=image/jpeg]\n  mime: qoi=image/x-qoi\n  mime: adf=application/x-amiga-disk-format\n\n  # list default mimetype mapping and exit\n  mimes\n\n  # do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower\n  # 📂 also available as volflag \"rmagic\"\n  rmagic\n\n  # show licenses and exit\n  license\n\n  # show versions and exit\n  version\n \n  # url to check against for vulnerable versions (default disabled); setting this will enable automatic \n  # vulnerability checks. the notification, in case you are running a vulnerable version, is shown on the \n  # admin panel (/?h) and only for users with admin permissions. you can choose between the value given here, \n  # or alternatively use https://api.copyparty.eu/security-advisories, or your own custom endpoint\n  vc-url: https://api.github.com/repos/9001/copyparty/security-advisories\n\n  # how many seconds to wait between vulnerability checks; default is 86400 (= 1 day).\n  vc-interval: 86400\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// qr options \\\\000000000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # show http:// QR-code on startup\n  qr\n\n  # show https:// QR-code on startup\n  qrs\n\n  # location to include in the url, for example [priv/?pw=hunter2]\n  qrl: uploads/?pw=okletsgo  # hint\n\n  # select IP which starts with PREFIX; [.] to force default IP when mDNS URL would have been used instead\n  qri: 192.168.  # hint\n\n  # foreground; try [0] if the qr-code is unreadable\n  qr-fg: 46  # hint; default=0=black\n\n  # background (white=255)\n  qr-bg: 92  # hint; default=229=parchment\n\n  # padding (spec says 4 or more, but 1 is usually fine)\n  qrp: 2  # hint\n\n  # [1]=1x, [2]=2x, [0]=auto (try [2] on broken fonts)\n  qrz: 2  # hint\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// filesystem options \\\\0000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # if a file cannot be deleted because it is busy, continue trying for T seconds, retry every R seconds; disable with 0/0\n  # 📂 also available as volflag \"rm_retry\"\n  rm-retry: 15/0.1  # default on windows; 0 on unix\n\n  # if a file cannot be renamed because it is busy, continue trying for T seconds, retry every R seconds; disable with 0/0\n  # 📂 also available as volflag \"mv_retry\"\n  mv-retry: 15/0.1  # default on windows; 0 on unix\n\n  # file I/O buffer-size; if your volumes are on a network drive, try increasing to 524288 or even 4194304 (and let me know if that improves your performance)\n  iobuf: 262144  # default\n\n  # rebuild mountpoint cache every SEC to keep track of sparse-files support; keep low on servers with removable media\n  mtab-age: 60  # default\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// share-url options \\\\00000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # toplevel virtual folder for shared files/folders, for example [/share]\n  shr: /shares  # hint; default is unset\n\n  # database to store shares in\n  shr-db: $XDG_CONFIG_PATH/.copyparty/shares.db  # default (and yes, $ENV will expand in configs)\n\n  # comma-separated list of users allowed to view/delete any share\n  shr-adm: ame,same  # hint\n\n  # shares can be revived by their owner if they expired less than MIN minutes ago; [60]=hour, [1440]=day, [10080]=week\n  shr-rt: 1440  # default\n\n  # debug\n  shr-v\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// upload options \\\\00000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # dotfile incomplete uploads, hiding them from clients unless -ed\n  dotpart\n\n  # when avoiding filename collisions by appending the uploader's ip to the filename: append the plaintext ip instead of salting and hashing the ip\n  plain-ip\n\n  # filename for nameless uploads (when uploader doesn't provide a name); default is [put-UNIXTIME-IP.bin] (the .6f means six decimal places)\n  # 📂 also available as volflag \"put_name\"\n  put-name: put-{now.6f}-{cip}.bin  # default\n\n  # default checksum-hasher for PUT/WebDAV uploads: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s\n  # 📂 also available as volflag \"put_ck\"\n  put-ck: sha512  # default\n\n  # default checksum-hasher for bup/basic-uploader: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s\n  # 📂 also available as volflag \"bup_ck\"\n  bup-ck: sha512  # default\n\n  # grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h\n  unpost: 43200  # default (12h)\n\n  # clients can abort incomplete uploads by using the unpost tab (requires -e2d). [0] = never allowed (disable feature), [1] = allow if client has the same IP as the upload AND is using the same account, [2] = just check the IP, [3] = just check account-name\n  # 📂 also available as volflag \"u2abort\"\n  u2abort: 1  # default\n\n  # file write grace period (any client can write to a blank file last-modified more recently than SEC seconds ago)\n  blank-wt: 300  # default\n\n  # max number of uploads to keep in memory when running without -e2d; roughly 1 MiB RAM per 600\n  reg-cap: 38400  # default\n\n  # disable file-handle pooling -- instead, repeatedly close and reopen files during upload (bad idea to enable this on windows and/or cow filesystems)\n  no-fpool\n\n  # force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)\n  use-fpool\n\n  # unix file permissions to use when creating files; default is probably 644 (OS-decided), see --help-chmod. Examples: [644] = owner-RW + all-R, [755] = owner-RWX + all-RX, [777] = full-yolo\n  # 📂 also available as volflag \"chmod_f\"\n  chmod-f: 644  # hint; default is unset\n\n  # unix file permissions to use when creating directories; see --help-chmod. Examples: [755] = owner-RW + all-R, [2750] = owner-RW + group-R + setgid, [777] = full-yolo\n  # 📂 also available as volflag \"chmod_d\"\n  chmod-d: 755  # default\n\n  # unix user-id to chown new files/folders to; default = -1 = do-not-change\n  # 📂 also available as volflag \"uid\"\n  uid: 1000  # hint\n\n  # unix group-id to chown new files/folders to; default = -1 = do-not-change\n  # 📂 also available as volflag \"gid\"\n  gid: 573  # hint\n\n  # enable symlink-based upload deduplication\n  # 📂 also available as volflag \"dedup\"\n  dedup\n\n  # how careful to be when deduplicating files; [1] = just verify the filesize, [50] = verify file contents have not been altered\n  # 📂 also available as volflag \"safededup\"\n  safe-dedup: 50  # default\n\n  # enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems)\n  # 📂 also available as volflag \"hardlink\"\n  hardlink\n\n  # do not fallback to symlinks when a hardlink cannot be made\n  # 📂 also available as volflag \"hardlinkonly\"\n  hardlink-only\n\n  # enable reflink-based dedup; will fallback on full copies when that is impossible (non-CoW filesystem)\n  # 📂 also available as volflag \"reflink\"\n  reflink\n\n  # reject duplicate files during upload; only matches within the same volume\n  # 📂 also available as volflag \"nodupe\"\n  no-dupe\n\n  # do not use existing data on disk to satisfy dupe uploads; reduces server HDD reads in exchange for much more network load\n  # 📂 also available as volflag \"noclone\"\n  no-clone\n\n  # disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually\n  no-snap\n\n  # write upload state to ./hist/up2k.snap every SEC seconds; allows resuming incomplete uploads after a server crash\n  snap-wri: 300  # default\n\n  # forget unfinished uploads after MIN minutes; impossible to resume them after that (360=6h, 1440=24h)\n  snap-drop: 1440  # default\n\n  # how to timestamp uploaded files; [c]=client-last-modified, [u]=upload-time, [fc]=force-c, [fu]=force-u\n  # 📂 also available as volflag \"u2ts\"\n  u2ts: c  # default\n\n  # force randomized filenames, --nrand chars long\n  # 📂 also available as volflag \"rand\"\n  rand\n\n  # randomized filenames length\n  # 📂 also available as volflag \"nrand\"\n  nrand: 9  # default\n\n  # enable filetype detection on nameless uploads\n  # 📂 also available as volflag \"magic\"\n  magic\n\n  # ensure GiB free disk space by rejecting upload requests; assumes gigabytes unless a unit suffix is given: [256m], [4], [2T]\n  # 📂 also available as volflag \"df\"\n  df: 4  # hint; default=0\n\n  # windows-only: minimum size of incoming uploads through up2k before they are made into sparse files\n  sparse: 4  # default\n\n  # configure turbo-mode in up2k client; [-1] = forbidden/always-off, [0] = default-off and warn if enabled, [1] = default-off, [2] = on, [3] = on and disable datecheck\n  turbo: 0  # default\n\n  # when to use a wasm-hasher instead of the browser's builtin; faster on chrome, but buggy in older chrome versions. [0] = only when necessary (non-https), [1] = always (all browsers), [2] = always on chrome/firefox, [3] = always on chrome, [N] = chrome-version N and newer (recommendation: 137)\n  nosubtle: 0  # default\n\n  # 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\n  u2j: 2  # default\n\n  # web-client: default upload chunksize (MiB); sets min,default,max in the settings gui. Each HTTP POST will aim for default, and never exceed max. Cloudflare max is 96. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [1,1,1]\n  u2sz: 1,64,96  # default\n\n  # web-client: default setting for when to replace/overwrite existing files; [0]=never, [1]=if-client-newer, [2]=always\n  # 📂 also available as volflag \"u2ow\"\n  u2ow: 0  # default\n\n  # upload order; [s]=smallest-first, [n]=alphabetical, [fs]=force-s, [fn]=force-n -- alphabetical is a bit slower on fiber/LAN but makes it easier to eyeball if everything went fine\n  u2sort: s  # default\n\n  # write POST reports to textfiles in working-directory\n  write-uplog\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// network options \\\\0000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # IPs and/or unix-sockets to listen on, COMMA-SEPARATED LIST (see --help-bind). Default: all IPv4 and IPv6\n  i: 192.168.0.1,::1,unix:770:www:/dev/shm/party.sock  # hint; default=::\n\n  # ports to listen on (comma/range); ignored for unix-sockets\n  p: 3923,4001-4005  # hint; default=3923\n\n  # include link-local IPv4/IPv6 in mDNS replies, even if the NIC has routable IPs (breaks some mDNS clients)\n  ll\n\n  # which ip to associate clients with; [0]=tcp, [1]=origin (first x-fwd, unsafe), [2]=outermost-proxy, [3]=second-proxy, [-1]=closest-proxy\n  rproxy: 1  # default\n\n  # if reverse-proxied, which http header to read the client's real ip from\n  xff-hdr: x-forwarded-for  # default\n\n  # list of trusted reverse-proxy CIDRs (comma-separated); only accept the real-ip header (--xff-hdr) and IdP headers if the incoming connection is from an IP within either of these subnets. Specify [lan] to allow all LAN / private / non-internet IPs. Can be disabled with [any] if you are behind cloudflare (or similar) and are using --xff-hdr=cf-connecting-ip (or similar)\n  xff-src: 127.0.0.0/8, ::1/128  # default\n\n  # only accept connections from IP-addresses inside CIDR (comma-separated); examples: [lan] or [10.89.0.0/16, 192.168.33.0/24]\n  ipa: 10.89.0.0/16, 192.168.33.0/24  # hint; default is unset\n\n  # if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here; example: [/foo/bar]\n  rp-loc: /files  # hint; default is unset\n\n  # set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances\n  reuseaddr\n\n  # 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)\n  freebind\n\n  # write list of listening-on ip:port to textfile at PATH when http-servers have started\n  wr-h-eps: /dev/shm/listening-on.txt  # hint; default is unset\n\n  # write list of accessible-on ip:port to textfile at PATH when http-servers have started\n  wr-h-aon: /dev/shm/accessible-on.txt  # hint; default is unset\n\n  # socket timeout (read request header)\n  s-thead: 120  # default\n\n  # 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\n  s-tbody: 128  # default\n\n  # socket read size in bytes (indirectly affects filesystem writes; recommendation: keep equal-to or lower-than --iobuf)\n  s-rd-sz: 262144  # default\n\n  # socket write size in bytes\n  s-wr-sz: 262144  # default\n\n  # debug: socket write delay in seconds\n  s-wr-slp: 0  # default\n\n  # debug: response delay in seconds\n  rsp-slp: 0  # default\n\n  # debug: response delay, random duration 0..SEC\n  rsp-jtr: 0  # default\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// SSL/TLS options \\\\0000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # disable ssl/tls -- force plaintext\n  http-only\n\n  # disable plaintext -- force tls\n  https-only\n\n  # path to file containing a concatenation of TLS key and certificate chain\n  cert: $XDG_CONFIG_PATH/.copyparty/cert.pem  # default\n\n  # set allowed ssl/tls versions; [help] shows available versions; default is what your python version considers safe\n  ssl-ver: ssl3,tls10  # hint; default is unset\n\n  # set allowed ssl/tls ciphers; [help] shows available ciphers\n  ciphers: xtea,rot13  # hint; default is unset\n\n  # dump some tls info\n  ssl-dbg\n\n  # log master secrets for later decryption in wireshark\n  ssl-log: /mnt/stash/gotem.log  # hint; default is unset\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// TLS certificate generator options \\\\0000000000000000000000000000000000000000000000000000\\\n\n  # disable automatic certificate creation\n  no-crt\n\n  # comma-separated list of FQDNs (domains) to add into the certificate\n  crt-ns: example.com,fileshare.nasa.gov\n\n  # do not add wildcard entries for each --crt-ns\n  crt-exact\n\n  # do not add autodetected IP addresses into cert\n  crt-noip\n\n  # do not add 127.0.0.1 / localhost into cert\n  crt-nolo\n\n  # do not add mDNS names / hostname into cert\n  crt-nohn\n\n  # where to save the CA cert\n  crt-dir: $XDG_CONFIG_PATH/.copyparty/  # default\n\n  # ca-certificate expiration time in days\n  crt-cdays: 3650  # default\n\n  # server-cert expiration time in days\n  crt-sdays: 365  # default\n\n  # CA/server-cert common-name\n  crt-cn: partyco  # default\n\n  # override CA name\n  crt-cnc: --crt-cn  # default = copy the above\n\n  # override server-cert name\n  crt-cns: --crt-cnc  # default = copy the above\n\n  # backdate in hours\n  crt-back: 72  # default\n\n  # algorithm and keysize; one of these: ecdsa-256 rsa-4096 rsa-2048\n  crt-alg: ecdsa-256  # default\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// IdP / identity provider / user authentication options \\\\00000000000000000000000000000000\\\n\n  # bypass the copyparty authentication checks if the request-header HN contains a username to associate the request with (for use with authentik/oauth/...)\n  # └─WARNING: if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy\n  idp-h-usr: idp-username  # hint; default is unset\n\n  # assume the request-header HN contains the groupname of the requesting user; can be referenced in config files for group-based access control\n  idp-h-grp: idp-groups  # hint; default is unset\n\n  # optional but recommended safeguard; your reverse-proxy will insert a secret header named HN into all requests, and the other IdP headers will be ignored if this header is not present\n  idp-h-key: supersecretmagicword  # hint; default is unset\n\n  # if there are multiple groups in --idp-h-grp, they are separated by one of the characters in RE\n  idp-gsep: |:;+,  # default\n\n  # where to store the known IdP users/groups (if you run multiple copyparty instances, make sure they use different DBs)\n  idp-db: $XDG_CONFIG_PATH/.copyparty/idp.db  # default\n\n  # how to use --idp-db; [0] = entirely disable, [1] = write-only (effectively disabled), [2] = remember users, [3] = remember users and groups.\n  # └─NOTE: 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\n  idp-store: 1  # default\n\n  # comma-separated list of users allowed to use /?idp (the cache management UI)\n  idp-adm: ben,jerry  # hint; default is unset\n\n  # generate a session-token for IdP users which is written to cookie cppws (or cppwd if plaintext), to reduce the load on the IdP server, lifetime S seconds.\n  # └─note: The expiration time is a client hint only; the actual lifetime of the session-token is infinite (until next restart with --ses-db wiped)\n  idp-cookie: 600  # hint; default=0=disabled\n\n  # disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app\n  no-bauth\n\n  # keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins\n  bauth-last\n\n  # where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)\n  ses-db: $XDG_CONFIG_PATH/.copyparty/sessions.db  # default\n\n  # session key length; default is 120 bits ((20//4)*4*6)\n  ses-len: 20\n\n  # disable sessions; use plaintext passwords in cookies\n  no-ses\n\n  # REPEATABLE: users with IP matching CIDR are auto-authenticated as username USR; example: [172.16.24.0/24=dave]\n  ipu: CIDR=USR  # placeholder\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// user-changeable passwords options \\\\0000000000000000000000000000000000000000000000000000\\\n\n  # allow users to change their own passwords\n  chpw\n\n  # REPEATABLE: do not allow password-changes for this comma-separated list of usernames\n  chpw-no: ole,dole,doffen  # hint; default is unset\n\n  # where to store the passwords database (if you run multiple copyparty instances, make sure they use different DBs)\n  chpw-db: $XDG_CONFIG_PATH/.copyparty/chpw.json\n\n  # minimum password length\n  chpw-len: 8  # default\n\n  # verbosity of summary on config load [0] = nothing at all, [1] = number of users, [2] = list users with default-pw, [3] = list all users\n  chpw-v: 2  # default\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// Zeroconf options \\\\000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # enable all zeroconf backends (mdns, ssdp)\n  z\n\n  # enable zeroconf ONLY on the comma-separated list of subnets and/or interface names/indexes\n  # └─example: eth0, wlo1, virhost0, 192.168.123.0/24, fd00:fda::/96\n  z-on: NETS  # placeholder; default is unset\n\n  # disable zeroconf on the comma-separated list of subnets and/or interface names/indexes\n  z-off: NETS  # placeholder; default is unset\n\n  # check for network changes every SEC seconds (0=disable)\n  z-chk: 10  # default\n\n  # verbose all zeroconf backends\n  zv\n\n  # rejoin multicast groups every SEC seconds (workaround for some switches/routers which cause mDNS to suddenly stop working after some time); try [300] or [180]\n  # └─note: can be due to firewalls; make sure UDP port 5353 is open in both directions (on clients too)\n  mc-hop: 0  # default\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// Zeroconf-mDNS options; also see --help-zm \\\\00000000000000000000000000000000000000000000\\\n\n  # announce the enabled protocols over mDNS (multicast DNS-SD) -- compatible with KDE, gnome, macOS, ...\n  zm\n\n  # enable mDNS ONLY on the comma-separated list of subnets and/or interface names/indexes\n  zm-on: NETS  # placeholder; default is unset\n\n  # disable mDNS on the comma-separated list of subnets and/or interface names/indexes\n  zm-off: NETS  # placeholder; default is unset\n\n  # IPv4 only -- try this if some clients can't connect\n  zm4\n\n  # IPv6 only\n  zm6\n\n  # verbose mdns\n  zmv\n\n  # verboser mdns\n  zmvv\n\n  # mute parser errors (invalid incoming MDNS packets)\n  zm-no-pe\n\n  # disable workaround for avahi-bug #379 (corruption in Avahi's mDNS reflection feature)\n  zm-nwa-1\n\n  # list of services to announce -- d=webdav h=http f=ftp s=smb -- lowercase=plaintext uppercase=TLS -- default: all enabled services except http/https (Ddfs if --ftp and --smb is set, Dd otherwise)\n  zms: dhf  # probably default\n\n  # link a specific folder for webdav shares\n  zm-ld: /public/stuff  # hint; default is unset (webroot)\n\n  # link a specific folder for http shares\n  zm-lh: /public/stuff  # hint; default is unset (webroot)\n\n  # link a specific folder for ftp shares\n  zm-lf: /public/stuff  # hint; default is unset (webroot)\n\n  # link a specific folder for smb shares\n  zm-ls: /public/stuff  # hint; default is unset (webroot)\n\n  # merge NICs which share subnets; assume that same subnet means same network\n  zm-mnic\n\n  # 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\n  zm-msub\n\n  # disable NSEC replies -- try this if some clients don't see copyparty\n  zm-noneg\n\n  # send unsolicited announce every SEC; useful if clients have IPs in a subnet which doesn't overlap with the server, or to avoid some firewall issues\n  zm-spam: 0  # default\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// Zeroconf-SSDP options \\\\0000000000000000000000000000000000000000000000000000000000000000\\\n\n  # announce the enabled protocols over SSDP -- compatible with Windows\n  zs\n\n  # enable SSDP ONLY on the comma-separated list of subnets and/or interface names/indexes\n  zs-on: NETS  # placeholder; default is unset\n\n  # disable SSDP on the comma-separated list of subnets and/or interface names/indexes\n  zs-off: NETS  # placeholder; default is unset\n\n  # verbose SSDP\n  zsv\n\n  # location to include in the url (or a complete external URL), for example [priv/?pw=hunter2] (goes directly to /priv/ with password hunter2) or [?hc=priv&pw=hunter2] (shows mounting options for /priv/ with password)\n  zsl: /?hc  # default\n\n  # USN (device identifier) to announce\n  zsid: UUID  # placeholder; default is an autogenerated UUID\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// FTP options (TCP only) \\\\000000000000000000000000000000000000000000000000000000000000000\\\n\n  # enable FTP server on PORT, for example 3921\n  ftp: 3921  # hint; default is unset\n\n  # enable FTPS server on PORT, for example 3990\n  ftps: 3990  # hint; default is unset\n\n  # verbose\n  ftpv\n\n  # only listen on IPv4\n  ftp4\n\n  # only accept connections from IP-addresses inside CIDR (comma-separated); specify [any] to disable inheriting --ipa. Examples: [lan] or [10.89.0.0/16, 192.168.33.0/24]\n  ftp-ipa: CIDR  # placeholder\n\n  # if target file exists, reject upload instead of overwrite\n  ftp-no-ow\n\n  # grace period for resuming interrupted uploads (any client can write to any file last-modified more recently than SEC seconds ago)\n  ftp-wt: 7  # default\n\n  # the NAT address to use for passive connections\n  ftp-nat: 192.168.1.13  # hint; default is unset\n\n  # the range of TCP ports to use for passive connections, for example 12000-13000\n  ftp-pr: 12000-12099  # hint; default is unset\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// WebDAV options \\\\00000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # enable full write support, even if client may not be webdav. WARNING: This has side-effects -- PUT-operations will now OVERWRITE 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)\n  daw\n\n  # allow depth:infinite requests (recursive file listing); extremely server-heavy but required for spec compliance -- luckily few clients rely on this\n  dav-inf\n\n  # disable apple-garbage filter -- allow macos to create junk files (._* and .DS_Store, .Spotlight-*, .fseventsd, .Trashes, .AppleDouble, __MACOS)\n  dav-mac\n\n  # show symlink-destination's lastmodified instead of the link itself; always enabled for recursive listings\n  # 📂 also available as volflag \"davrt\"\n  dav-rt\n\n  # force auth for all folders (required by davfs2 when only some folders are world-readable)\n  # 📂 also available as volflag \"davauth\"\n  dav-auth\n\n  # regex of tricky user-agents which expect 401 from GET requests; disable with [no] or blank\n  dav-ua1: kioworker/  # default (KDE)\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// TFTP options (UDP only) \\\\00000000000000000000000000000000000000000000000000000000000000\\\n\n  # enable TFTP server on PORT, for example 69 or 3969\n  tftp: 3969  # hint; default is unset\n\n  # only listen on IPv4\n  tftp4\n\n  # verbose\n  tftpv\n\n  # verboser\n  tftpvv\n\n  # debug: disable optimizations\n  tftp-no-fast\n\n  # 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, ...\n  tftp-lsf: \\.?(dir|ls)(\\.txt)?  # default\n\n  # if someone tries to download a directory, return an error instead of showing its directory listing\n  tftp-nols\n\n  # only accept connections from IP-addresses inside CIDR (comma-separated); specify [any] to disable inheriting --ipa. Examples: [lan] or [10.89.0.0/16, 192.168.33.0/24]\n  tftp-ipa: CIDR  # placeholder\n\n  # the range of UDP ports to use for data transfer, for example 12000-13000\n  tftp-pr: 12100-12199  # hint; default is unset\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// SMB/CIFS options \\\\000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # enable smb (read-only) -- this requires running copyparty as root on linux and macos unless --smb-port is set above 1024 and your OS does port-forwarding from 445 to that.\n  # └─WARNING: this protocol is DANGEROUS and buggy! Never expose to the internet!\n  smb\n\n  # enable write support (please dont)\n  smbw\n\n  # disable SMBv2, only enable SMBv1 (CIFS)\n  smb1\n\n  # port to listen on -- if you change this value, you must NAT from TCP:445 to this port using iptables or similar\n  smb-port: 445  # default\n\n  # truncate directory listings to 64kB (~400 files); avoids impacket-0.11 bug, fixes impacket-0.12 performance\n  smb-nwa-1\n\n  # disable impacket workaround for filecopy globs\n  smb-nwa-2\n\n  # small performance boost: disable per-account permissions, enables account coalescing instead (if one user has write/delete-access, then everyone does)\n  smba\n\n  # verbose\n  smbv\n\n  # verboser\n  smbvv\n\n  # verbosest\n  smbvvv\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// handlers (see --help-handlers) \\\\0000000000000000000000000000000000000000000000000000000\\\n\n  # REPEATABLE: handle 404s by executing PY file\n  on404: ~/bin/on404.py  # hint; default is unset\n  on404: ~/bin/on404season2.py\n\n  # REPEATABLE: handle 403s by executing PY file\n  on403: ~/bin/on403.py  # hint; default is unset\n\n  # recompile handlers on each request -- expensive but convenient when hacking on stuff\n  hot-handlers\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// event hooks (see --help-hooks) \\\\0000000000000000000000000000000000000000000000000000000\\\n\n  # REPEATABLE: execute CMD before a file upload starts\n  xbu: ~/bin/execute-before-upload.py  # hint; default is unset\n\n  # REPEATABLE: execute CMD after  a file upload finishes\n  xau: ~/bin/execute-after-upload.py  # hint; default is unset\n\n  # REPEATABLE: execute CMD after  all uploads finish and volume is idle\n  xiu: ~/bin/execute-idle-upload.py  # hint; default is unset\n\n  # REPEATABLE: execute CMD before a file copy\n  xbc: ~/bin/execute-before-copy.py  # hint; default is unset\n\n  # REPEATABLE: execute CMD after  a file copy\n  xac: ~/bin/execute-after-copy.py  # hint; default is unset\n\n  # REPEATABLE: execute CMD before a file move/rename\n  xbr: ~/bin/execute-before-rename.py  # hint; default is unset\n\n  # REPEATABLE: execute CMD after  a file move/rename\n  xar: ~/bin/execute-after-rename.py  # hint; default is unset\n\n  # REPEATABLE: execute CMD before a file delete\n  xbd: ~/bin/execute-before-delete.py  # hint; default is unset\n\n  # REPEATABLE: execute CMD after  a file delete\n  xad: ~/bin/execute-after-delete.py  # hint; default is unset\n\n  # REPEATABLE: execute CMD on message\n  xm: ~/bin/execute-on-message.py  # hint; default is unset\n\n  # REPEATABLE: execute CMD if someone gets banned (pw/404/403/url)\n  xban: ~/bin/execute-on-ban.py  # hint; default is unset\n\n  # verbose hooks\n  hook-v\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// grafana/prometheus metrics endpoint \\\\00000000000000000000000000000000000000000000000000\\\n\n  # enable openmetrics at /.cpr/metrics for admin accounts\n  stats\n\n  # disable disk-space metrics (used/free space)\n  nos-hdd\n\n  # disable volume size metrics (num files, total bytes, vmaxb/vmaxn)\n  nos-vol\n\n  # disable volume state metrics (indexing, analyzing, activity)\n  nos-vst\n\n  # disable dupe-files metrics (good idea; very slow)\n  nos-dup\n\n  # disable unfinished-uploads metrics\n  nos-unf\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// yolo options \\\\0000000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # disable csrf protections; let other domains/sites impersonate you through cross-site requests\n  allow-csrf\n\n  # 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\n  cookie-lax\n\n  # permit ?move=[...] and ?delete as GET\n  getmod\n\n  # allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix\n  # 📂 also available as volflag \"wo_up_readme\"\n  wo-up-readme\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// opt-outs \\\\00000000000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # never write anything to disk (debug/benchmark)\n  nw\n\n  # 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)\n  keep-qem\n\n  # disable webdav support\n  no-dav\n\n  # disable delete operations\n  no-del\n\n  # disable move/rename operations\n  no-mv\n\n  # disable copy operations\n  no-cp\n\n  # no title hostname; don't show --name in <title>\n  nth\n\n  # no info hostname -- don't show in UI\n  nih\n\n  # no info disk-usage -- don't show in UI\n  nid\n\n  # no powered-by-copyparty branding in UI\n  nb\n\n  # reject download-as-zip if more than N files in total; optionally takes a unit suffix: [256], [9K], [4G]\n  # 📂 also available as volflag \"zipmaxn\"\n  zipmaxn: N  # placeholder; default is unset\n\n  # reject download-as-zip if total download size exceeds SZ bytes; optionally takes a unit suffix: [256M], [4G], [2T]\n  # 📂 also available as volflag \"zipmaxs\"\n  zipmaxs: SZ  # placeholder; default is unset\n\n  # custom errormessage when download size exceeds max\n  # 📂 also available as volflag \"zipmaxt\"\n  zipmaxt: TXT  # placeholder; default is unset\n\n  # authenticated users bypass the zip size limit\n  # 📂 also available as volflag \"zipmaxu\"\n  zipmaxu\n\n  # who can download as zip/tar? [0]=nobody, [1]=admins, [2]=authenticated-with-read-access, [3]=everyone-with-read-access (volflag=zip_who)\n  # └─WARNING: if a nested volume has a more restrictive value than a parent volume, then this will be ignored if the download is initiated from the parent, more lenient volume\n  zip-who: 3  # default\n\n  # regex of user-agents to reject from download-as-zip/tar; disable with [no] or blank\n  ua-nozip: Barkrowler|bingbot|BLEXBot|Googlebot|GoogleOther|GPTBot|PetalBot|SeekportBot|SemrushBot|YandexBot  # default\n\n  # disable download as zip/tar; same as --zip-who=0\n  no-zip\n\n  # disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)\n  no-tarcmp\n\n  # do not allow clients (or server config) to schedule an upload to be deleted after a given time\n  no-lifetime\n\n  # disable race-the-beam (lockstep download of files which are currently being uploaded)\n  # 📂 also available as volflag \"nopipe\"\n  no-pipe\n\n  # disable streaming a growing files with ?tail\n  # 📂 also available as volflag \"notail\"\n  no-tail\n\n  # do not write uploader-IP into the database; will also disable unpost, you may want --forget-ip instead\n  # 📂 also available as volflag \"no_db_ip\"\n  no-db-ip\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// safety options \\\\00000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n  # └─Alias of --dotpart --no-thumb --no-mtag-ff --no-robots --force-js\n  s\n\n  # further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav, 404 on 403, ban on excessive 404s.\n  # └─Alias of -s --unpost=0 --no-del --no-mv --hardlink --vague-403 -nih\n  ss\n\n  # further increase safety: Enable logging to disk, scan for dangerous symlinks.\n  # └─Alias of -ss --no-dav --no-logues --no-readme -lo=cpp-%Y-%m%d-%H%M%S.txt.xz --ls=**,*,ln,p,r\n  sss\n\n  # do a sanity/safety check of all volumes on startup; arguments USER,VOL,FLAGS (see --help-ls); example [**,*,ln,p,r]\n  ls: **,*,ln,p,r  # hint; default is unset\n\n  # never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access\n  # 📂 also available as volflag \"xvol\"\n  xvol\n\n  # stay within the filesystem of the volume root; do not descend into other devices (symlink or bind-mount to another HDD, ...)\n  # 📂 also available as volflag \"xdev\"\n  xdev\n\n  # disallow moving dotfiles; makes it impossible to move folders containing dotfiles\n  no-dot-mv\n\n  # disallow renaming dotfiles; makes it impossible to turn something into a dotfile\n  no-dot-ren\n\n  # disable rendering .prologue/.epilogue.html into directory listings\n  no-logues\n\n  # disable rendering readme/preadme.md into directory listings\n  no-readme\n\n  # send 404 instead of 403 (security through ambiguity, very enterprise)\n  vague-403\n\n  # don't send folder listings as HTML, force clients to use the embedded json instead -- slight protection against misbehaving search engines which ignore --no-robots\n  force-js\n\n  # adds http and html headers asking search engines to not index anything\n  # 📂 also available as volflag \"norobots\"\n  no-robots\n\n  # logout clients after H hours of inactivity; [0.0028]=10sec, [0.1]=6min, [24]=day, [168]=week, [720]=month, [8760]=year)\n  logout: 8086  # default\n\n  # more than N wrong passwords in W minutes = ban for B minutes; disable with [no]\n  ban-pw: 9,60,1440  # default\n\n  # more than N password-changes in W minutes = ban for B minutes; disable with [no]\n  ban-pwc: 5,60,1440  # default\n\n  # hitting more than N 404's in W minutes = ban for B minutes; only affects users who cannot see directory listings because their access is either g/G/h\n  ban-404: 50,60,1440  # default\n\n  # hitting more than N 403's in W minutes = ban for B minutes; [1440]=day, [10080]=week, [43200]=month\n  ban-403: 9,2,1440  # default\n\n  # hitting more than N 422's in W minutes = ban for B minutes (invalid requests, attempted exploits ++)\n  ban-422: 9,2,1440  # default\n\n  # hitting more than N sus URL's in W minutes = ban for B minutes; applies only to permissions g/G/h (decent replacement for --ban-404 if that can't be used)\n  ban-url: 9,2,1440  # default\n\n  # URLs which are considered sus / eligible for banning; disable with blank or [no]\n  sus-urls: \\.php$|(^|/)wp-(admin|content|includes)/  # default\n\n  # harmless URLs ignored from 404-bans; disable with blank or [no]\n  nonsus-urls: ^(favicon\\.ico|robots\\.txt)$|^apple-touch-icon|^\\.well-known  # default\n\n  # 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\n  early-ban\n\n  # if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for MIN minutes (and also kill its active connections) -- disable with 0\n  aclose: 10  # default\n\n  # if a client maxes out the server connection limit without sending headers, ban it for B minutes; disable with [0]\n  loris: 60  # default\n\n  # Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [https://1.2.3.4]. Default [*] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives\n  acao: *  # default\n\n  # Access-Control-Allow-Methods; list of methods to accept from offsite ('*' behaves like --acao's description)\n  acam: GET,HEAD  # default\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// salting options \\\\0000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # account-pw hashing algorithm; one of these, best to worst: argon2 scrypt sha2 none (each optionally followed by alg-specific comma-sep. config)\n  ah-alg: argon2  # hint; default is unset\n\n  # account-pw salt; ignored if --ah-alg is none (default)\n  ah-salt: shangalabangala  # hint; default is autogenerated and stored in $XDG_CONFIG_PATH/.copyparty/\n\n  # generate hashed password for PW, or read passwords from STDIN if PW is [-]\n  ah-gen: PW  # placeholder\n\n  # launch an interactive shell which hashes passwords without ever storing or displaying the original passwords\n  ah-cli\n\n  # per-file accesskey salt; used to generate unpredictable URLs for hidden files\n  fk-salt: shangalabangala  # hint; default is autogenerated and stored in $XDG_CONFIG_PATH/.copyparty/\n\n  # per-directory accesskey salt; used to generate unpredictable URLs to share folders with users who only have the 'get' permission\n  dk-salt: shangalabangala  # hint; default is autogenerated and stored in $XDG_CONFIG_PATH/.copyparty/\n\n  # up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)\n  warksalt: hunter2  # default\n\n  # on startup, print the effective value of --ah-salt (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)\n  show-ah-salt\n\n  # on startup, print the effective value of --fk-salt (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)\n  show-fk-salt\n\n  # on startup, print the effective value of --dk-salt (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)\n  show-dk-salt\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// shutdown options \\\\000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # continue running even if it's impossible to listen on some of the requested endpoints\n  ign-ebind\n\n  # continue running even if it's impossible to receive connections at all\n  ign-ebind-all\n\n  # shutdown after WHEN has finished; [cfg] config parsing, [idx] volscan + multimedia indexing\n  exit: WHEN  # placeholder; default is unset\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// logging options \\\\0000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # quiet; disable most STDOUT messages\n  q\n\n  # logfile, example: cpp-%Y-%m%d-%H%M%S.txt.xz (NB: some errors may appear on STDOUT only)\n  lo: PATH  # placeholder\n\n  # disable colors; same as environment-variable NO_COLOR\n  no-ansi\n\n  # force colors; overrides environment-variable NO_COLOR\n  ansi\n\n  # don't flush the logfile after each write; tiny bit faster\n  no-logflush\n\n  # do not list volumes and permissions on startup\n  no-voldump\n\n  # do not use local timezone; assume the TZ env-var is UTC (tiny bit faster)\n  log-utc\n\n  # timestamp resolution / number of timestamp decimals\n  log-tdec: 3  # default\n\n  # log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed\n  log-badpwd: 1  # default\n\n  # debug: print tcp-server msgs\n  log-conn\n\n  # debug: print http-server threadpool scaling\n  log-htp\n\n  # print request HEADER; [*]=all\n  ihead: user-agent  # hint; default is unset\n\n  # print response HEADER; [*]=all\n  ohead: set-cooke  # hint; default is unset\n\n  # dont log URLs matching regex RE\n  lf-url: ^/\\.cpr/|[?&]th=[xwjp]|/\\.(_|ql_|DS_Store$|localized$)  # default\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// admin panel options \\\\000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # disable ?reload=cfg (reload users/volumes/volflags from config file)\n  no-reload\n\n  # disable ?scan (volume reindexing)\n  no-rescan\n\n  # disable ?stack (list all stacks)\n  no-stack\n\n  # disable ?ru (list of recent uploads)\n  no-ups-page\n\n  # don't show list of incoming files in controlpanel\n  no-up-list\n\n  # who can see active downloads in the controlpanel? [0]=nobody, [1]=admins, [2]=everyone\n  dl-list: 2  # default\n\n  # who can see recent uploads on the ?ru page? [0]=nobody, [1]=admins, [2]=everyone\n  # 📂 also available as volflag \"ups_who\"\n  ups-who: 2  # default\n\n  # let everyone see upload timestamps on the ?ru page, not just admins\n  ups-when\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// thumbnail options \\\\00000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # disable all thumbnails\n  # 📂 also available as volflag \"dthumb\"\n  no-thumb\n\n  # disable video thumbnails\n  # 📂 also available as volflag \"dvthumb\"\n  no-vthumb\n\n  # disable audio thumbnails (spectrograms)\n  # 📂 also available as volflag \"dathumb\"\n  no-athumb\n\n  # thumbnail res\n  # 📂 also available as volflag \"thsize\"\n  th-size: 320x256  # default\n\n  # num cpu cores to use for generating thumbnails\n  th-mt: 4  # hint; default is autodetect\n\n  # conversion timeout in seconds\n  # 📂 also available as volflag \"convt\"\n  th-convt: 60  # default\n\n  # max memory usage (GiB) permitted by thumbnailer; not very accurate\n  th-ram-max: 3  # hint; default is 60% of free ram at startup (some conditions apply)\n\n  # crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [y]=crop, [n]=nocrop, [fy]=force-y, [fn]=force-n\n  # 📂 also available as volflag \"crop\"\n  th-crop: y  # default\n\n  # show thumbs at 3x resolution; client can override in UI unless force. [y]=yes, [n]=no, [fy]=force-yes, [fn]=force-no\n  # 📂 also available as volflag \"th3x\"\n  th-x3: n  # default\n\n  # image decoders, in order of preference\n  th-dec: vips,pil,raw,ff  # default\n\n  # disable jpg output\n  th-no-jpg\n\n  # disable webp output\n  th-no-webp\n\n  # disable jpeg-xl output\n  th-no-jxl\n\n  # force jpg output for video thumbs (avoids issues on some FFmpeg builds)\n  th-ff-jpg\n\n  # use swresample instead of soxr for audio thumbs (faster, lower accuracy, avoids issues on some FFmpeg builds)\n  th-ff-swr\n\n  # activity labeling cooldown -- avoids doing keepalive pokes (updating the mtime) on thumbnail folders more often than SEC seconds\n  th-poke: 300  # default\n\n  # cleanup interval; 0=disabled\n  th-clean: 43200  # default\n\n  # max folder age -- folders which haven't been poked for longer than --th-poke seconds will get deleted every --th-clean seconds\n  th-maxage: 604800  # default\n\n  # folder thumbnails to stat/look for; enabling -e2d 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\n  th-covers: folder.png,folder.jpg,cover.png,cover.jpg  # default\n\n  # image formats to decode using pillow\n  th-r-pil: a,very,long,list,of,file,extensions  # hint\n\n  # image formats to decode using pyvips\n  th-r-vips: a,very,long,list,of,file,extensions  # hint\n\n  # image formats to decode using rawpy\n  th-r-raw: a,very,long,list,of,file,extensions  # hint\n\n  # image formats to decode using ffmpeg\n  th-r-ffi: a,very,long,list,of,file,extensions  # hint\n\n  # video formats to decode using ffmpeg\n  th-r-ffv: a,very,long,list,of,file,extensions  # hint\n\n  # audio formats to decode using ffmpeg\n  th-r-ffa: a,very,long,list,of,file,extensions  # hint\n\n  # audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)\n  th-spec-cnv: a,very,long,list,of,file,extensions  # hint\n\n  # audio/image formats to decompress before passing to ffmpeg\n  au-unpk: mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, and so on\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// transcoding options \\\\000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # target bitrate for transcoding to opus; set 0 to disable\n  q-opus: 128  # default\n\n  # target quality for transcoding to mp3, for example [192k] (CBR) or [q0] (CQ/CRF, q0=maxquality, q9=smallest); set 0 to disable\n  q-mp3: q2  # default\n\n  # allow transcoding to wav (lossless, uncompressed)\n  allow-wav\n\n  # allow transcoding to flac (lossless, compressed)\n  allow-flac\n\n  # disable transcoding to caf-opus (affects iOS v12~v17), will use mp3 instead\n  no-caf\n\n  # disable transcoding to webm-opus (iOS v18 and later), will use mp3 instead\n  no-owa\n\n  # disable audio transcoding\n  no-acode\n\n  # disable batch audio transcoding by folder download (zip/tar)\n  no-bacode\n\n  # delete cached transcode output after SEC seconds\n  ac-maxage: 86400  # default\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// tailing options (realtime streaming of a growing file) \\\\0000000000000000000000000000000\\\n\n  # who can tail? [0]=nobody, [1]=admins, [2]=authenticated-with-read-access, [3]=everyone-with-read-access\n  # 📂 also available as volflag \"tail_who\"\n  tail-who: 2  # default\n\n  # do not allow starting a new tail if more than N active downloads\n  tail-cmax: 64  # default\n\n  # terminate connection after SEC seconds; [0]=never\n  # 📂 also available as volflag \"tail_tmax\"\n  tail-tmax: 0  # default\n\n  # check for new data every SEC seconds\n  # 📂 also available as volflag \"tail_rate\"\n  tail-rate: 0.2  # default\n\n  # send a zerobyte if connection is idle for SEC seconds to prevent disconnect\n  tail-ka: 3  # default\n\n  # check if file was replaced (new fd) if idle for SEC seconds\n  # 📂 also available as volflag \"tail_fd\"\n  tail-fd: 1  # default\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// RSS options \\\\00000000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # enable RSS output (experimental)\n  # 📂 also available as volflag \"rss\"\n  rss\n\n  # default number of files to return (url-param 'nf')\n  rss-nf: 250  # default\n\n  # default list of file extensions to include (url-param 'fext'); blank=all\n  rss-fext: mp3,opus  # hint; default is unset\n\n  # default sort order (url-param 'sort'); [m]=last-modified [u]=upload-time [n]=filename [s]=filesize; Uppercase=oldest-first. Note that upload-time is 0 for non-uploaded files\n  rss-sort: m  # default\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// general db options \\\\0000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # enable up2k database; this enables file search, upload-undo, improves deduplication\n  e2d\n\n  # scan writable folders for new files on startup; sets -e2d\n  e2ds\n\n  # scans all folders on startup; sets -e2ds\n  e2dsa\n\n  # verify file integrity; rehash all files and compare with db\n  e2v\n\n  # on hash mismatch: update the database with the new hash\n  e2vu\n\n  # on hash mismatch: panic and quit copyparty\n  e2vp\n\n  # where to store volume data (db, thumbs); default is a folder named \".hist\" inside each volume\n  # 📂 also available as volflag \"hist\"\n  hist: PATH  # placeholder; default is unset\n\n  # override where the volume databases are to be placed; default is the same as --hist\n  # 📂 also available as volflag \"dbpath\"\n  dbpath: PATH  # placeholder; default is unset\n\n  # regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans\n  # 📂 also available as volflag \"nohash\"\n  no-hash: ^/mnt/nas/linux-isos/knoppix/\n\n  # regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scans\n  # 📂 also available as volflag \"noidx\"\n  no-idx: ^/mnt/nas/logs/\n\n  # do not show total recursive size of folders in listings, show inode size instead; slightly faster\n  # 📂 also available as volflag \"nodirsz\"\n  no-dirsz\n\n  # if the directory-sizes in the UI are bonkers, use this along with -e2dsa to rebuild the index from scratch\n  re-dirsz\n\n  # disable rescan acceleration; do full database integrity check -- makes the db ~5% smaller and bootup/rescans 3~10x slower\n  no-dhash\n\n  # force a cache rebuild on startup; enable this once if it gets out of sync (should never be necessary)\n  re-dhash\n\n  # 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\n  # 📂 also available as volflag \"noforget\"\n  no-forget\n\n  # remove uploader-IP from database (and make unpost impossible) MIN minutes after upload, for GDPR reasons. Default [0] is never-forget. [1440]=day, [10080]=week, [43200]=month.\n  # 📂 also available as volflag \"forget_ip\"\n  forget-ip: 0  # default (disabled)\n\n  # database durability profile; sets the tradeoff between robustness and speed, see --help-dbd\n  # 📂 also available as volflag \"dbd\"\n  dbd: wal  # default\n\n  # on upload: check all volumes for dupes, not just the target volume (probably buggy, not recommended)\n  # 📂 also available as volflag \"xlink\"\n  xlink\n\n  # num cpu cores to use for file hashing; set 0 or 1 for single-core hashing\n  hash-mt: 4  # default is autodetect but max 5\n\n  # rescan filesystem for changes every SEC seconds; 0=off\n  # 📂 also available as volflag \"scan\"\n  re-maxage: 0  # default\n\n  # defer any scheduled volume reindexing until SEC seconds after last db write (uploads, renames, ...)\n  db-act: 10  # default\n\n  # search deadline -- terminate searches running for more than SEC seconds\n  srch-time: 45  # default\n\n  # max search results to allow clients to fetch; 125 results will be shown initially\n  srch-hits: 7999  # default\n\n  # regex: exclude files from search results if the file-URL matches PTN (case-sensitive)\n  # 📂 also available as volflag \"srch_excl\"\n  srch-excl: password|logs/[0-9]  # hint (any URL containing 'password' or 'logs/DIGIT'), default is unset\n\n  # show dotfiles in search results (volflags: dotsrch | nodotsrch)\n  dotsrch\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// metadata db options \\\\000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # enable metadata indexing; makes it possible to search for artist/title/codec/resolution/...\n  e2t\n\n  # scan newly discovered files for metadata on startup; sets -e2t\n  e2ts\n\n  # delete all metadata from DB and do a full rescan; sets -e2ts\n  e2tsr\n\n  # use FFprobe for tags instead; will detect more tags\n  no-mutagen\n\n  # never use FFprobe as tag reader; is probably safer\n  no-mtag-ff\n\n  # timeout for FFprobe tag-scan\n  mtag-to: 60  # default\n\n  # num cpu cores to use for tag scanning\n  mtag-mt: 4  # hint; default is autodetect\n\n  # verbose tag scanning; print errors from mtp subprocesses and such\n  mtag-v\n\n  # debug mtp settings and mutagen/FFprobe parsers\n  mtag-vv\n\n  # REPEATABLE: add/replace metadata mapping\n  mtm: qux=foo,bar  # hint (clone metadata-key foo to qux with bar as fallback); default is unset\n\n  # tags to index/display (comma-sep.); either an entire replacement list, or add/remove stuff on the default-list with +foo or /bar\"\n  mte: .files,circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash  # default\n\n  # tags to hide by default (comma-sep.); assign/add/remove same as -mte\"\n  mth: .vq,.aq,vc,ac,fmt,res,.fps  # default\n\n  # REPEATABLE: read tag M using program BIN to parse the file\n  mtp: .bpm=~/bin/audio-bpm.py  # hint; default is unset\n  mtp: key=f,t5,~/bin/audio-key.py\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// textfile options \\\\000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # where to store old version of markdown files; [s]=subfolder, [v]=volume-histpath, [n]=nope/disabled\n  # 📂 also available as volflag \"md_hist\"\n  md-hist: s  # default\n\n  # the textfile editor will check for serverside changes every SEC seconds\n  mcr: 60  # default\n\n  # enable markdown plugins -- neat but dangerous, big XSS risk\n  emp\n\n  # enable textfile expansion -- replace {{self.ip}} and such; see --help-exp\n  # 📂 also available as volflag \"exp\"\n  exp\n\n  # comma/space-separated list of placeholders to expand in markdown files; add/remove stuff on the default list with +hdr_foo or /vf.scan\n  # 📂 also available as volflag \"exp_md\"\n  exp-md: self.ip self.ua self.uname self.host cfg.name cfg.logout vf.scan vf.thsize hdr.cf_ipcountry srv.itime srv.htime  # default\n\n  # comma/space-separated list of placeholders to expand in prologue/epilogue files\n  # 📂 also available as volflag \"exp_lg\"\n  exp-lg: self.ip self.ua self.uname self.host cfg.name cfg.logout vf.scan vf.thsize hdr.cf_ipcountry srv.itime srv.htime  # default\n\n  # regex of user-agents to reject from viewing documents through ?doc=[...]; disable with [no] or blank\n  ua-nodoc: Barkrowler|bingbot|BLEXBot|Googlebot|GoogleOther|GPTBot|PetalBot|SeekportBot|SemrushBot|YandexBot  # default\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// og / open graph / discord-embed options \\\\0000000000000000000000000000000000000000000000\\\n\n  # disable hotlinking and return an html document instead; this is required by open-graph, but can also be useful on its own\n  # 📂 also available as volflag \"og\"\n  og\n\n  # only disable hotlinking / engage OG behavior if the useragent matches regex RE\n  # 📂 also available as volflag \"og_ua\"\n  og-ua: (Discord|Twitter|Slack)bot  # hint; default is unset\n\n  # do not return the regular copyparty html, but instead load the jinja2 template at PATH (if path contains 'EXT' then EXT will be replaced with the requested file's extension)\n  # 📂 also available as volflag \"og_tpl\"\n  og-tpl: /var/copyparty/opengraph-EXT.j2  # hint; default is unset\n\n  # do not automatically add OG entries into <head> (useful if you're doing this yourself in a template or such)\n  # 📂 also available as volflag \"og_no_head\"\n  og-no-head\n\n  # thumbnail format; j=jpeg, jf=jpeg-uncropped, jf3=jpeg-uncropped-large, w=webm, ...\n  # 📂 also available as volflag \"og_th\"\n  og-th: jf3  # default\n\n  # fallback title if there is nothing in the -e2t database\n  # 📂 also available as volflag \"og_title\"\n  og-title: HEY CHECK THIS OUT  # hint; default is blank\n\n  # audio title format; takes any metadata key\n  # 📂 also available as volflag \"og_title_a\"\n  og-title-a: 🎵 {{ artist }} - {{ title }}  # default\n\n  # video title format; takes any metadata key\n  # 📂 also available as volflag \"og_title_v\"\n  og-title-v: {{ title }}  # default\n\n  # image title format; takes any metadata key\n  # 📂 also available as volflag \"og_title_i\"\n  og-title-i: {{ title }}  # default\n\n  # force default title; do not read from tags\n  # 📂 also available as volflag \"og_s_title\"\n  og-s-title\n\n  # description text; same for all files, disable with [-]\n  # 📂 also available as volflag \"og_desc\"\n  og-desc: bottom text  # hint; default is blank\n\n  # sitename; defaults to --name, disable with [-]\n  # 📂 also available as volflag \"og_site\"\n  og-site: dank memes  # hint; default is blank which means it'll copy the `name` global-option\n\n  # accent color (3 or 6 hex digits); may also affect safari and/or android-chrome\n  # 📂 also available as volflag \"tcolor\"\n  tcolor: 333  # default (copyparty gray)\n\n  # query-string parceling; translate a request for /foo/.uqe/BASE64 into /foo?TEXT, or /foo/?TEXT if the first character in TEXT is a slash. Automatically enabled for --og\n  uqe\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// ui options \\\\000000000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # show grid/thumbnails by default\n  # 📂 also available as volflag \"grid\"\n  grid\n\n  # select files in grid by ctrl-click\n  # 📂 also available as volflag \"gsel\"\n  gsel\n\n  # default to local timezone instead of UTC\n  localtime\n\n  # language; one of the following: eng nor chi\n  lang: eng  # default\n\n  # default theme to use (0..7)\n  theme: 0  # default\n\n  # number of themes installed\n  themes: 8  # default\n\n  # default audio/video volume percent\n  au-vol: 50  # default; anything 0-100\n\n  # default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: href -href ext sz ts tags/Album tags/.tn\n  # 📂 also available as volflag \"sort\"\n  sort: href  # default\n\n  # default-enable natural sort of filenames with leading numbers\n  # 📂 also available as volflag \"nsort\"\n  nsort\n\n  # number of sorting rules to include in media URLs by default\n  # 📂 also available as volflag \"hsortn\"\n  hsortn: 2  # default\n\n  # default-enable seeing dotfiles; only takes effect if user has the necessary permissions\n  see-dots\n\n  # number of confirmations to show when deleting files (2/1/0)\n  qdel: 2  # default\n\n  # don't show files/folders matching REGEX in file list. WARNING: Purely cosmetic! Does not affect API calls, just the browser. Example: [\\.(js|css)$]\n  # 📂 also available as volflag \"unlist\"\n  unlist: REGEX  # placeholder\n\n  # favicon-text [ foreground [ background ] ], set blank to disable\n  favico: c 000 none  # default\n\n  # REPEATABLE: use thumbnail-image VP for file-extension E, example: [exe=/.res/exe.png]\n  # 📂 also available as volflag \"ext_th\"\n  ext-th: 7z=/.icons/archive.png  # hint; default is unset\n  ext-th: exe=/.icons/glass-of-red-liquid.png\n\n  # emoji or emoji,css Example: [🥖,padding:0]\n  spinner: 🌲  # default\n\n  # URL to additional CSS to include in the filebrowser html\n  css-browser: /.res/rice.css  # hint; default is unset\n\n  # URL to additional JS to include in the filebrowser html\n  js-browser: /.res/mousetrail.js  # hint; default is unset\n\n  # URL to additional JS to include in all other pages\n  js-other: /.res/snowflakes.jpg  # hint; default is unset\n\n  # text to append to the <head> 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\n  # 📂 also available as volflag \"html_head\"\n  html-head: <script>alert(1);</script>  # hint; default is unset\n\n  # 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)\n  ih\n\n  # file extensions to present as plaintext\n  textfiles: txt,nfo,diz,cue,readme  # default\n\n  # max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)\n  txt-max: 64  # default\n\n  # title / service-name to show in html documents\n  doctitle: copyparty @ --name  # default (--name will copy from global-option `name`)\n\n  # server name (displayed in filebrowser document title)\n  bname: --name  # default (copy global-option `name`)\n\n  # powered-by link; disable with -nb\n  pb-url: https://github.com/9001/copyparty  # default\n\n  # show version on the control panel (incompatible with -nb)\n  ver\n\n  # configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [0] = hidden and default-off, [1] = visible and default-off, [2] = visible and default-on\n  k304: 0  # default\n\n  # configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [0] = hidden and default-off, [1] = visible and default-off, [2] = visible and default-on\n  no304: 0  # default\n\n  # 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\n  md-sbf: downloads forms popups scripts top-navigation-by-user-activation  # default\n\n  # list of capabilities to allow in the iframe 'sandbox' attribute for prologue/epilogue docs\n  # 📂 also available as volflag \"lg_sbf\"\n  lg-sbf: downloads forms popups scripts top-navigation-by-user-activation  # default\n\n  # the value of the iframe 'allow' attribute for README.md docs, for example [fullscreen]\n  # 📂 also available as volflag \"md_sba\"\n  md-sba: fullscreen  # hint; default is blank\n\n  # 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\n  lg-sba: TXT  # placeholder\n\n  # don't sandbox README/PREADME.md documents (volflags: no_sb_md | sb_md)\n  no-sb-md\n\n  # don't sandbox prologue/epilogue docs (volflags: no_sb_lg | sb_lg); enables non-js support\n  no-sb-lg\n\n  ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n  ###// debug options \\\\000000000000000000000000000000000000000000000000000000000000000000000000\\\n\n  # verbose config file parser (explain config)\n  vc\n\n  # generate config file from current config (best-effort; probably buggy)\n  cgen\n\n  # list information about detected optional dependencies\n  deps\n\n  # kernel-bug workaround: disable poll; use select instead (limits max num clients to ~700)\n  no-poll\n\n  # kernel-bug workaround: disable sendfile; do a safe and slow read-send-loop instead\n  no-sendfile\n\n  # kernel-bug workaround: disable scandir; do a listdir + stat on each file instead\n  no-scandir\n\n  # wait for initial filesystem indexing before accepting client requests\n  no-fastboot\n\n  # disable httpserver threadpool, create threads as-needed instead\n  no-htp\n\n  # when listening on unix-sockets, do a basic delete+bind instead of the default atomic bind\n  rm-sck\n\n  # explain search processing, and do some extra expensive sanity checks\n  srch-dbg\n\n  # use mdns-domain instead of server-ip on /?hc\n  rclone-mdns\n\n  # write stacktrace to Path every S second, for example --stackmon=./st/%Y-%m/%d/%H%M.xz,60\n  stackmon: P,S  # placeholder\n\n  # list active threads every SEC\n  log-thrs: 0  # default\n\n  # log filekey params for files where path matches REGEX; [.] (a single dot) = all files\n  log-fk: /mnt/a-problematic-fuse/this-folder-breaks-filekeys/  # hint; default is unset\n\n  # [up2k] if a client uploads a bitflipped/corrupted chunk, store a copy according to --bf-nc and --bf-dir\n  bak-flips\n\n  # bak-flips: stop if there's more than NUM files at --kf-dir already; default: 6.3 GiB max (200*32M)\n  bf-nc: 200  # default\n\n  # bak-flips: store corrupted chunks at PATH; default: folder named 'bf' wherever copyparty was started\n  bf-dir: /srv/bitflips/  # hint; default = bf\n\n  # bak-flips: log corruption info to a textfile at PATH\n  bf-log: /srv/bitflips/the-history.txt  # hint; default is unset\n\n  ###############################################################################################\n  ###############################################################################################\n  ###############################################################################################\n  ###############################################################################################\n  ###############################################################################################\n  ###############################################################################################\n  #####/\n  #####    This is the end of the [global] config section\n  ####/\n\n[accounts]\n  foo: bar  # username foo, password bar\n\n[groups]\n  g1: u1, u2, u3  # group \"g1\" with users u1, u2, and u3\n\n[/the/url/to/share/this/volume/on/]\n  /the/actual/filesystem/path/\n  accs:\n    r: username_who_gets_Read_access\n    w: username_who_gets_Write_access\n    m: username_who_gets_Move_access\n    d: username_who_gets_Delete_access\n    .: username_who_can_see_Dotfiles\n    g: username_who_gets_Get_access\n    G: username_who_gets_upGet_access\n    h: username_who_gets_html_access\n    a: username_who_gets_admin_access\n    A: username_who_gets_ReadWriteMoveDeleteDotfileAdmin_access\n    rwm: @g1  # the group \"g1\" gets Read+Write+Move\n  flags:\n\n    ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n    ###// uploads, general \\\\000000000000000000000000000000000000000000000000000000000000000000000\\\n\n    # enable symlink-based file deduplication\n    dedup\n\n    # enable hardlink-based file deduplication, with fallback on symlinks when that is impossible\n    hardlink\n\n    # dedup with hardlink only, never symlink; make a full copy if hardlink is impossible\n    hardlinkonly\n\n    # enable reflink-based file deduplication, with fallback on full copy when that is impossible\n    reflink\n\n    # verify on-disk data before using it for dedup\n    safededup\n\n    # take dupe data from clients, even if available on HDD\n    noclone\n\n    # rejects existing files (instead of linking/cloning them)\n    nodupe\n\n    # unix-permission for new dirs/folders\n    chmod_d: 755\n\n    # unix-permission for new files\n    chmod_f: 644\n\n    # change owner of new files/folders to unix-user 573\n    uid: 573\n\n    # change owner of new files/folders to unix-group 999\n    gid: 999\n\n    # force use of sparse files, mainly for s3-backed storage\n    sparse\n\n    # deny use of sparse files, mainly for slow storage\n    nosparse\n\n    # enable full WebDAV write support (dangerous); PUT-operations will now OVERWRITE existing files\n    daw\n\n    # forces all uploads into the top folder of the vfs\n    nosub\n\n    # enables filetype detection for nameless uploads\n    magic\n\n    # fallback filename for nameless uploads\n    put_name\n\n    # default checksum-hasher for PUT/WebDAV uploads\n    put_ck\n\n    # default checksum-hasher for bup/basic uploads\n    bup_ck\n\n    # allows server-side gzip compression of uploads with ?gz\n    gz\n\n    # allows server-side lzma compression of uploads with ?xz\n    xz\n\n    # forces server-side compression, optional arg: xz,9\n    pk\n\n    ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n    ###// upload rules \\\\0000000000000000000000000000000000000000000000000000000000000000000000000\\\n\n    # max 250 uploads over 15min\n    maxn: 250,600\n\n    # max 1 GiB over 5min (suffixes: b, k, m, g, t)\n    maxb: 1g,300\n\n    # total volume size max 1 GiB (suffixes: b, k, m, g, t)\n    vmaxb: 1g\n\n    # max 4096 files in volume (suffixes: b, k, m, g, t)\n    vmaxn: 4k\n\n    # return medialinks for non-up2k uploads (not hotlinks)\n    medialinks\n\n    # write-only users can upload logues without getting renamed\n    wo_up_readme\n\n    # force randomized filenames, 9 chars long by default\n    rand\n\n    # randomized filenames are N chars long\n    nrand: N\n\n    # overwrite existing files? 0=no 1=if-older 2=always\n    u2ow: N\n\n    # [f]orce [c]lient-last-modified or [u]pload-time\n    u2ts: fc\n\n    # allow aborting unfinished uploads? 0=no 1=strict 2=ip-chk 3=acct-chk\n    u2abort: 1\n\n    # allow filesizes between 1 KiB and 3MiB\n    sz: 1k-3m\n\n    # ensure 1 GiB free disk space\n    df: 1g\n\n    ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n    ###// upload rotation -- (moves all uploads into the specified folder structure) \\\\00000000000\\\n\n    # 3 levels of subfolders with 100 entries in each\n    rotn: 100,3\n\n    # date-formatted organizing\n    rotf: %Y-%m/%d-%H\n\n    # uploads are deleted after 1 hour\n    lifetime: 3600\n\n    ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n    ###// database, general \\\\00000000000000000000000000000000000000000000000000000000000000000000\\\n\n    # enable database; makes files searchable + enables upload-undo\n    e2d\n\n    # scan writable folders for new files on startup; also sets -e2d\n    e2ds\n\n    # scans all folders for new files on startup; also sets -e2d\n    e2dsa\n\n    # enable multimedia indexing; makes it possible to search for tags\n    e2t\n\n    # scan existing files for tags on startup; also sets -e2t\n    e2ts\n\n    # delete all metadata from DB (full rescan); also sets -e2ts\n    e2tsr\n\n    # disables metadata collection for existing files\n    d2ts\n\n    # verify integrity on startup by hashing files and comparing to db\n    e2v\n\n    # when e2v fails, update the db (assume on-disk files are good)\n    e2vu\n\n    # when e2v fails, panic and quit copyparty\n    e2vp\n\n    # disables onboot indexing, overrides -e2ds*\n    d2ds\n\n    # disables metadata collection, overrides -e2t*\n    d2t\n\n    # disables file verification, overrides -e2v*\n    d2v\n\n    # disables all database stuff, overrides -e2*\n    d2d\n\n    # puts thumbnails and indexes at that location\n    hist: /tmp/cdb\n\n    # puts indexes at that location\n    dbpath: /tmp/cdb\n\n    # disable db if file foo doesn't exist\n    landmark: foo\n\n    # scan for new files every 60sec, same as --re-maxage\n    scan: 60\n\n    # skips hashing file contents if path matches *.iso\n    nohash: \\.iso$\n\n    # fully ignores the contents at paths matching *.iso\n    noidx: \\.iso$\n\n    # don't forget files when deleted from disk\n    noforget\n\n    # forget uploader-IP after 30 days (GDPR)\n    forget_ip: 43200\n\n    # never store uploader-IP in the db; disables unpost\n    no_db_ip\n\n    # avoid excessive reindexing on android sdcardfs\n    fat32\n\n    # database speed-durability tradeoff\n    dbd: [acid|swal|wal|yolo]\n\n    # cross-volume dupe detection / linking (dangerous)\n    xlink\n\n    # do not descend into other filesystems\n    xdev\n\n    # do not follow symlinks leaving the volume root\n    xvol\n\n    # show dotfiles in search results\n    dotsrch\n\n    # hide dotfiles in search results (default)\n    nodotsrch\n\n    # exclude search results with URL matching this regex\n    srch_excl\n\n    # media-tags to index/display\n    mte: artist,title\n\n    # media-tags to hide by default\n    mth: fmt,res,ac\n\n    # uses the \"audio-bpm.py\" program to generate \".bpm\" tags from uploads (f = overwrite tags)\n    mtp: .bpm=f,audio-bpm.py\n\n    # collects two tags at once\n    mtp: ahash,vhash=media-hash.py\n\n    ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n    ###// thumbnails \\\\000000000000000000000000000000000000000000000000000000000000000000000000000\\\n\n    # disables all thumbnails\n    dthumb\n\n    # disables video thumbnails\n    dvthumb\n\n    # disables audio thumbnails (spectrograms)\n    dathumb\n\n    # disables image thumbnails\n    dithumb\n\n    # compress audio waveforms 33% better\n    pngquant\n\n    # thumbnail res; WxH\n    thsize\n\n    # center-cropping (y/n/fy/fn)\n    crop\n\n    # 3x resolution (y/n/fy/fn)\n    th3x\n\n    # conversion timeout in seconds\n    convt\n\n    # use /b.png as thumbnail for file-extension s\n    ext_th: s=/b.png\n\n    ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n    ###// handlers -- (better explained in --help-handlers) \\\\000000000000000000000000000000000000\\\n\n    # handle 404s by executing PY file\n    on404: ~/bin/hook.py\n\n    # handle 403s by executing PY file\n    on403: ~/bin/hook.py\n\n    ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n    ###// event hooks -- (better explained in --help-hooks) \\\\000000000000000000000000000000000000\\\n\n    # execute CMD before a file upload starts\n    xbu: ~/bin/hook.py\n\n    # execute CMD after  a file upload finishes\n    xau: ~/bin/hook.py\n\n    # execute CMD after  all uploads finish and volume is idle\n    xiu: ~/bin/hook.py\n\n    # execute CMD before a file copy\n    xbc: ~/bin/hook.py\n\n    # execute CMD after  a file copy\n    xac: ~/bin/hook.py\n\n    # execute CMD before a file rename/move\n    xbr: ~/bin/hook.py\n\n    # execute CMD after  a file rename/move\n    xar: ~/bin/hook.py\n\n    # execute CMD before a file delete\n    xbd: ~/bin/hook.py\n\n    # execute CMD after  a file delete\n    xad: ~/bin/hook.py\n\n    # execute CMD on message\n    xm: ~/bin/hook.py\n\n    # execute CMD if someone gets banned\n    xban: ~/bin/hook.py\n\n    ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n    ###// client and ux \\\\000000000000000000000000000000000000000000000000000000000000000000000000\\\n\n    # show grid/thumbnails by default\n    grid\n\n    # select files in grid by ctrl-click\n    gsel\n\n    # default sort order\n    sort\n\n    # natural-sort of leading digits in filenames\n    nsort\n\n    # number of sort-rules to add to media URLs\n    hsortn\n\n    # dont list files matching REGEX\n    unlist\n\n    # includes TXT in the <head>, or @PATH for file at PATH\n    html_head: <script>alert(1);</script>\n\n    # theme color (a hint for webbrowsers, discord, etc.)\n    tcolor: #fc0\n\n    # don't show total folder size\n    nodirsz\n\n    # allows indexing by search engines (default)\n    robots\n\n    # kindly asks search engines to leave\n    norobots\n\n    # don't list read-access in controlpanel\n    unlistcr\n\n    # don't list write-access in controlpanel\n    unlistcw\n\n    # disable js sandbox for markdown files\n    no_sb_md\n\n    # disable js sandbox for prologue/epilogue\n    no_sb_lg\n\n    # enable js sandbox for markdown files (default)\n    sb_md\n\n    # enable js sandbox for prologue/epilogue (default)\n    sb_lg\n\n    # list of markdown-sandbox safeguards to disable\n    md_sbf\n\n    # list of *logue-sandbox safeguards to disable\n    lg_sbf\n\n    # value of iframe allow-prop for markdown-sandbox\n    md_sba\n\n    # value of iframe allow-prop for *logue-sandbox\n    lg_sba\n\n    # return html and markdown as text/html\n    nohtml\n\n    ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n    ###// opengraph (discord embeds) \\\\00000000000000000000000000000000000000000000000000000000000\\\n\n    # enable OG (disables hotlinking)\n    og\n\n    # sitename; defaults to --name, disable with '-'\n    og_site\n\n    # description text for all files; disable with '-'\n    og_desc\n\n    # thumbnail format; j / jf / jf3 / w / w3 / ...\n    og_th: jf\n\n    # audio title format; default: {{ artist }} - {{ title }}\n    og_title_a\n\n    # video title format; default: {{ title }}\n    og_title_v\n\n    # image title format; default: {{ title }}\n    og_title_i\n\n    # fallback title if there's nothing in the db\n    og_title: foo\n\n    # force default title; do not read from tags\n    og_s_title\n\n    # custom html; see --og-tpl in --help\n    og_tpl\n\n    # you want to add tags manually with og_tpl\n    og_no_head\n\n    # if defined: only send OG html if useragent matches this regex\n    og_ua\n\n    ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n    ###// textfiles \\\\0000000000000000000000000000000000000000000000000000000000000000000000000000\\\n\n    # where to put markdown backups; s=subfolder, v=volHist, n=nope\n    md_hist\n\n    # enable textfile expansion; see --help-exp\n    exp\n\n    # placeholders to expand in markdown files; see --help\n    exp_md\n\n    # placeholders to expand in prologue/epilogue; see --help\n    exp_lg\n\n    ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n    ###// tailing \\\\000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n\n    # disable ?tail (download a growing file continuously)\n    notail\n\n    # check if file was replaced (new fd) every 1 sec\n    tail_fd: 1\n\n    # check for new data every 0.2 sec\n    tail_rate: 0.2\n\n    # kill connection after 30 sec\n    tail_tmax: 30\n\n    # restrict ?tail access (1=admins,2=authed,3=everyone)\n    tail_who: 2\n\n    ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n    ###// others \\\\0000000000000000000000000000000000000000000000000000000000000000000000000000000\\\n\n    # allow all users with read-access to enable the option to show dotfiles in listings\n    dots\n\n    # generates per-file accesskeys, which are then required at the \"g\" permission; keys are invalidated if filesize or inode changes\n    fk: 8\n\n    # generates slightly weaker per-file accesskeys, which are then required at the \"g\" permission; not affected by filesize or inode numbers\n    fka: 8\n\n    # generates per-directory accesskeys, which are then required at the \"g\" permission; keys are invalidated if filesize or inode changes\n    dk: 8\n\n    # per-directory accesskeys allow browsing into subdirs\n    dks\n\n    # allow seeing files (not folders) inside a specific folder with \"g\" perm, and does not require a valid dirkey to do so\n    dky\n\n    # allow '?rss' URL suffix (experimental)\n    rss\n\n    # expensive analysis for mimetype accuracy\n    rmagic\n\n    # restrict viewing the list of recent uploads\n    ups_who: 2\n\n    # restrict access to download-as-zip/tar\n    zip_who: 2\n\n    # reject download-as-zip if more than 9000 files\n    zipmaxn: 9k\n\n    # reject download-as-zip if size over 2 GiB\n    zipmaxs: 2g\n\n    # reply with 'no' if download-as-zip exceeds max\n    zipmaxt: no\n\n    # zip-size-limit does not apply to authenticated users\n    zipmaxu\n\n    # disable race-the-beam (download unfinished uploads)\n    nopipe\n\n    # ms-windows: timeout for renaming busy files\n    mv_retry\n\n    # ms-windows: timeout for deleting busy files\n    rm_retry\n\n    # ask webdav clients to login for all folders\n    davauth\n\n    # show lastmod time of symlink destination, not the link itself (note: this option is always enabled for recursive listings)\n    davrt\n"
  },
  {
    "path": "docs/chunksizes.py",
    "content": "#!/usr/bin/env python3\n\n# there's far better ways to do this but its 4am and i dont wanna think\n\n# just pypy it my dude\n\nimport math\n\ndef humansize(sz, terse=False):\n    for unit in [\"B\", \"KiB\", \"MiB\", \"GiB\", \"TiB\"]:\n        if sz < 1024:\n            break\n\n        sz /= 1024.0\n\n    ret = \" \".join([str(sz)[:4].rstrip(\".\"), unit])\n\n    if not terse:\n        return ret\n\n    return ret.replace(\"iB\", \"\").replace(\" \", \"\")\n\n\ndef up2k_chunksize(filesize):\n    chunksize = 1024 * 1024\n    stepsize = 512 * 1024\n    while True:\n        for mul in [1, 2]:\n            nchunks = math.ceil(filesize * 1.0 / chunksize)\n            if nchunks <= 256 or (chunksize >= 32 * 1024 * 1024 and nchunks <= 4096):\n                return chunksize\n\n            chunksize += stepsize\n            stepsize *= mul\n\n\ndef main():\n    prev = 1048576\n    n = n0 = 524288\n    while True:\n        csz = up2k_chunksize(n)\n        if csz > prev:\n            print(f\"| {n-n0:>18_} | {humansize(n-n0):>8} | {prev:>13_} | {humansize(prev):>8} |\".replace(\"_\", \" \"))\n            prev = csz\n        n += n0\n\n\nmain()\n"
  },
  {
    "path": "docs/copyparty.d/foo/another.conf",
    "content": "# this file gets included twice from ../some.conf,\n# setting user permissions for a volume\naccs:\n  rw: usr1\n  r: usr2\n  % sibling.conf\n"
  },
  {
    "path": "docs/copyparty.d/foo/sibling.conf",
    "content": "# and this config file gets included from ./another.conf,\n# adding a final permission for each of the two volumes in ../some.conf\nm: usr1, usr2\n"
  },
  {
    "path": "docs/copyparty.d/more-users/david.conf",
    "content": "[accounts]\n  david: letmein\n\n[groups]\n  friends: david\n\n# the \"friends\" group was already created in ../some.conf,\n# so we are just adding david into it\n"
  },
  {
    "path": "docs/copyparty.d/more-users/james.conf",
    "content": "[accounts]\n  james: metooplease\n\n[groups]\n  friends: james\n\n# the \"friends\" group was already created in ../some.conf,\n# so we are just adding james into it\n"
  },
  {
    "path": "docs/copyparty.d/some.conf",
    "content": "# not actually YAML but lets pretend:\n# -*- mode: yaml -*-\n# vim: ft=yaml:\n\n# lets make two volumes with the same accounts/permissions for both;\n# first declare the accounts just once:\n[accounts]\n  usr1: passw0rd\n  usr2: touhou\n\n[global]\n  i: 127.0.0.1  # listen on 127.0.0.1 only,\n  p: 2434       # port 2434\n  e2ds  # enable file indexing+scanning\n  e2ts  # and multimedia indexing+scanning\n  # (inline comments are OK if there is 2 spaces before the #)\n\n# share /usr/share/games from the server filesystem\n[/vidya]\n  /usr/share/games\n  % foo/another.conf  # include config file with volume permissions\n\n# and share your ~/Music folder too\n[/bangers]\n  ~/Music\n  % foo/another.conf\n\n# which should result in each of the volumes getting the following permissions:\n# usr1 read/write/move\n# usr2 read/move\n#\n# because another.conf sets the read/write permissions before it\n# includes sibling.conf which adds the move permission\n\n# ----------------------------------------------------------------------\n\n# we can also create a group here;\n[groups]\n  friends: usr1, usr2\n\n# and a volume which the group \"friends\" can read/write;\n[/friends]\n  /srv/pub/friends\n  accs:\n    rw: @friends\n\n# then we include all the config-files in the folder \"more-users\",\n# which can define more users, and maybe even add them to the \"friends\" group\n# (spoiler: the users \"usr1\", \"usr2\", \"david\", and \"james\" will have access)\n\n% more-users/\n"
  },
  {
    "path": "docs/cursed-usecases/README.md",
    "content": "insane ways to use copyparty\n\n\n## wireless keyboard\n\nproblem: you wanna control mpv or whatever software from the couch but you don't have a wireless keyboard\n\n\"solution\": load some custom javascript which renders a virtual keyboard on the upload UI and each keystroke is actually an upload which gets picked up by a dummy metadata parser which forwards the keystrokes into xdotool\n\n[no joke, this actually exists and it wasn't even my idea or handiwork (thx steen)](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js)\n\n\n## appxsvc tarpit\n\nproblem: `svchost.exe` is using 100% of a cpu core, and upon further inspection (`procmon`) it is `wsappx` desperately trying to install something, repeatedly reading a file named `AppxManifest.xml` and messing with an sqlite3 database\n\n\"solution\": create a virtual filesystem which is intentionally slow and trick windows into reading it from there instead\n\n* create a file called `AppxManifest.xml` and put something dumb in it\n* serve the file from a copyparty instance with `--rsp-slp=1` so every request will hang for 1 sec\n* `net use m: http://127.0.0.1:3993/`  (mount copyparty using the windows-native webdav client)\n* `mklink /d c:\\windows\\systemapps\\microsoftwindows.client.cbs_cw5n1h2txyewy\\AppxManifest.xml m:\\AppxManifest.xml`\n"
  },
  {
    "path": "docs/design.txt",
    "content": "##\n## thumbnails\n\ntwo components:\n  thumbcache is directly accessed by httpd to return pregenerated thumbnails\n  thumbsrv is accessed through a broker on cache miss to generate and return\n\n##\n## initial ideas\n\nneed log interface\n  tcpsrv creates it\n  httpsrv must use interface\n\nmsgsvc\n  simulates a multiprocessing queue\n  takes events from httpsrv\n    logging\n  mpsrv pops queue and forwards to this\n\ntcpsrv\n  tcp listener\n  pass tcp clients to worker\n  api to get status messages from workers\n\nmpsrv\n  uses multiprocessing to handle incoming clients\n\nhttpsrv\n  takes client sockets, starts threads\n  takes argv acc/vol through init args\n  loads acc/vol from config file\n"
  },
  {
    "path": "docs/devnotes.md",
    "content": "## devnotes toc\n\n* top\n* [future ideas](#future-ideas) - list of dreams which will probably never happen\n* [design](#design)\n    * [up2k](#up2k) - quick outline of the up2k protocol\n        * [why not tus](#why-not-tus) - I didn't know about [tus](https://tus.io/)\n        * [why chunk-hashes](#why-chunk-hashes) - a single sha512 would be better, right?\n        * [list of chunk-sizes](#list-of-chunk-sizes) - specific chunksizes are enforced\n* [hashed passwords](#hashed-passwords) - regarding the curious decisions\n* [http api](#http-api)\n    * [read](#read)\n    * [write](#write)\n    * [admin](#admin)\n    * [general](#general)\n* [event hooks](#event-hooks) - on writing your own [hooks](../README.md#event-hooks)\n    * [hook effects](#hook-effects) - hooks can cause intentional side-effects\n    * [hook import](#hook-import) - the `I` flag runs the hook inside copyparty\n* [assumptions](#assumptions)\n    * [mdns](#mdns)\n* [sfx repack](#sfx-repack) - reduce the size of an sfx by removing features\n* [building](#building)\n    * [dev env setup](#dev-env-setup)\n    * [just the sfx](#just-the-sfx)\n    * [build from release tarball](#build-from-release-tarball) - uses the included prebuilt webdeps\n    * [build from scratch](#build-from-scratch) - how the sausage is made\n    * [complete release](#complete-release)\n* [debugging](#debugging)\n    * [music playback halting on phones](#music-playback-halting-on-phones) - mostly fine on android\n    * [discarded ideas](#discarded-ideas)\n\n\n# future ideas\n\nlist of dreams which will probably never happen\n\n* the JS is a mess -- a ~~preact~~ rewrite would be nice\n  * preferably without build dependencies like webpack/babel/node.js, maybe a python thing to assemble js files into main.js\n  * good excuse to look at using virtual lists (browsers start to struggle when folders contain over 5000 files)\n  * maybe preact / vdom isn't the best choice, could just wait for the Next Big Thing\n* the UX is a mess -- a proper design would be nice\n  * very organic (much like the python/js), everything was an afterthought\n  * true for both the layout and the visual flair\n  * something like the tron board-room ui (or most other hollywood ones, like ironman) would be :100:\n    * would preferably keep the information density, just more organized yet [not too boring](https://blog.rachelbinx.com/2023/02/unbearable-sameness/)\n* some of the python files are way too big\n  * `up2k.py` ended up doing all the file indexing / db management\n  * `httpcli.py` should be separated into modules in general\n\n\n# design\n\n## up2k\n\nquick outline of the up2k protocol,  see [uploading](https://github.com/9001/copyparty#uploading) for the web-client\n* the up2k client splits a file into an \"optimal\" number of chunks\n  * 1 MiB each, unless that becomes more than 256 chunks\n  * tries 1.5M, 2M, 3, 4, 6, ... until <= 256 chunks or size >= 32M\n* client posts the list of hashes, filename, size, last-modified\n* server creates the `wark`, an identifier for this upload\n  * `sha512( salt + filesize + chunk_hashes )`\n  * and a sparse file is created for the chunks to drop into\n* client sends a series of POSTs, with one or more consecutive chunks in each\n  * header entries for the chunk-hashes (comma-separated) and wark\n  * server writes chunks into place based on the hash\n* client does another handshake with the hashlist; server replies with OK or a list of chunks to reupload\n\nup2k has saved a few uploads from becoming corrupted in-transfer already;\n* caught an android phone on wifi redhanded in wireshark with a bitflip, however bup with https would *probably* have noticed as well (thanks to tls also functioning as an integrity check)\n* also stopped someone from uploading because their ram was bad\n\nregarding the frequent server log message during uploads;  \n`6.0M 106M/s 2.77G 102.9M/s n948 thank 4/0/3/1 10042/7198 00:01:09`\n* this chunk was `6 MiB`, uploaded at `106 MiB/s`\n* on this http connection, `2.77 GiB` transferred, `102.9 MiB/s` average, `948` chunks handled\n* client says `4` uploads OK, `0` failed, `3` busy, `1` queued, `10042 MiB` total size, `7198 MiB` and `00:01:09` left\n\n### why not tus\n\nI didn't know about [tus](https://tus.io/)  when I made this, but:\n* up2k has the advantage that it supports parallel uploading of non-contiguous chunks straight into the final file -- [tus does a merge at the end](https://tus.io/protocols/resumable-upload.html#concatenation) which is slow and taxing on the server HDD / filesystem (unless i'm misunderstanding)\n* up2k has the slight disadvantage of requiring the client to hash the entire file before an upload can begin, but this has the benefit of immediately skipping duplicate files\n  * and the hashing happens in a separate thread anyways so it's usually not a bottleneck\n\n### why chunk-hashes\n\na single sha512 would be better, right?\n\nthis was due to `crypto.subtle` [not yet](https://github.com/w3c/webcrypto/issues/73) providing a streaming api (or the option to seed the sha512 hasher with a starting hash)\n\nas a result, the hashes are much less useful than they could have been (search the server by sha512, provide the sha512 in the response http headers, ...)\n\nhowever it allows for hashing multiple chunks in parallel, greatly increasing upload speed from fast storage (NVMe, raid-0 and such)\n\n* both the [browser uploader](https://github.com/9001/copyparty#uploading) and the [commandline one](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy) does this now, allowing for fast uploading even from plaintext http\n\nhashwasm would solve the streaming issue but reduces hashing speed for sha512 (xxh128 does 6 GiB/s), and it would make old browsers and [iphones](https://bugs.webkit.org/show_bug.cgi?id=228552) unsupported\n\n* blake2 might be a better choice since xxh is non-cryptographic, but that gets ~15 MiB/s on slower androids\n\n### list of chunk-sizes\n\nspecific chunksizes are enforced  depending on total filesize\n\neach pair of filesize/chunksize is the largest filesize which will use its listed chunksize; a 512 MiB file will use chunksize 2 MiB, but if the file is one byte larger than 512 MiB then it becomes 3 MiB\n\nfor the purpose of performance (or dodging arbitrary proxy limitations), it is possible to upload combined and/or partial chunks using stitching and/or subchunks respectively\n\n| filesize           | filesize | chunksize     | chunksz |\n| -----------------: | -------: | ------------: | ------: |\n|        268 435 456 |  256 MiB |     1 048 576 | 1.0 MiB |\n|        402 653 184 |  384 MiB |     1 572 864 | 1.5 MiB |\n|        536 870 912 |  512 MiB |     2 097 152 | 2.0 MiB |\n|        805 306 368 |  768 MiB |     3 145 728 | 3.0 MiB |\n|      1 073 741 824 |  1.0 GiB |     4 194 304 | 4.0 MiB |\n|      1 610 612 736 |  1.5 GiB |     6 291 456 | 6.0 MiB |\n|      2 147 483 648 |  2.0 GiB |     8 388 608 | 8.0 MiB |\n|      3 221 225 472 |  3.0 GiB |    12 582 912 |  12 MiB |\n|      4 294 967 296 |  4.0 GiB |    16 777 216 |  16 MiB |\n|      6 442 450 944 |  6.0 GiB |    25 165 824 |  24 MiB |\n|    137 438 953 472 |  128 GiB |    33 554 432 |  32 MiB |\n|    206 158 430 208 |  192 GiB |    50 331 648 |  48 MiB |\n|    274 877 906 944 |  256 GiB |    67 108 864 |  64 MiB |\n|    412 316 860 416 |  384 GiB |   100 663 296 |  96 MiB |\n|    549 755 813 888 |  512 GiB |   134 217 728 | 128 MiB |\n|    824 633 720 832 |  768 GiB |   201 326 592 | 192 MiB |\n|  1 099 511 627 776 |  1.0 TiB |   268 435 456 | 256 MiB |\n|  1 649 267 441 664 |  1.5 TiB |   402 653 184 | 384 MiB |\n|  2 199 023 255 552 |  2.0 TiB |   536 870 912 | 512 MiB |\n|  3 298 534 883 328 |  3.0 TiB |   805 306 368 | 768 MiB |\n|  4 398 046 511 104 |  4.0 TiB | 1 073 741 824 | 1.0 GiB |\n|  6 597 069 766 656 |  6.0 TiB | 1 610 612 736 | 1.5 GiB |\n|  8 796 093 022 208 |  8.0 TiB | 2 147 483 648 | 2.0 GiB |\n| 13 194 139 533 312 | 12.0 TiB | 3 221 225 472 | 3.0 GiB |\n| 17 592 186 044 416 | 16.0 TiB | 4 294 967 296 | 4.0 GiB |\n| 26 388 279 066 624 | 24.0 TiB | 6 442 450 944 | 6.0 GiB |\n| 35 184 372 088 832 | 32.0 TiB | 8 589 934 592 | 8.0 GiB |\n\n\n# hashed passwords\n\nregarding the curious decisions\n\nthere is a static salt for all passwords;\n* because most copyparty APIs allow users to authenticate using only their password, making the username unknown, so impossible to do per-account salts\n* the drawback of this is that an attacker can bruteforce all accounts in parallel, however most copyparty instances only have a handful of accounts in the first place, and it can be compensated by increasing the hashing cost anyways\n\n\n# http api\n\n* table-column `params` = URL parameters; `?foo=bar&qux=...`\n* table-column `body` = POST payload\n* method `jPOST` = json post\n* method `mPOST` = multipart post\n* method `uPOST` = url-encoded post\n* `FILE` = conventional HTTP file upload entry (rfc1867 et al, filename in `Content-Disposition`)\n\nclients can authenticate in the following ways; the first of these which is not blank will be used:\n* url-param `&pw=foo` -- can be disabled with `--pw-urlp=A` (or renamed, if provided value is lowercase)\n* then, header `PW: foo` -- can be disabled with `--pw-hdr=A` (or renamed, if provided value is lowercase)\n* then, basic-auth -- can be disabled with `--no-bauth`\n* then, depending on protocol, header `Cookie: cppwd=foo` on plaintext http, or header `Cookie: cppws=foo` on https\n\n## read\n\n| method | params | result |\n|--|--|--|\n| GET | `?dl` | download file (don't show in-browser) |\n| GET | `?ls` | list files/folders at URL as JSON |\n| GET | `?ls&dots` | list files/folders at URL as JSON, including dotfiles |\n| GET | `?ls=t` | list files/folders at URL as plaintext |\n| GET | `?ls=v` | list files/folders at URL, terminal-formatted |\n| GET | `?opds` | list files/folders at URL as opds feed, for e-readers |\n| GET | `?lt` | in listings, use symlink timestamps rather than targets |\n| GET | `?b` | list files/folders at URL as simplified HTML |\n| GET | `?tree=.` | list one level of subdirectories inside URL |\n| GET | `?tree` | list one level of subdirectories for each level until URL |\n| GET | `?tar` | download everything below URL as a gnu-tar file |\n| GET | `?tar=gz:9` | ...as a gzip-level-9 gnu-tar file |\n| GET | `?tar=xz:9` | ...as an xz-level-9 gnu-tar file |\n| GET | `?tar=pax` | ...as a pax-tar file |\n| GET | `?tar=pax,xz` | ...as an xz-level-1 pax-tar file |\n| GET | `?zip` | ...as a zip file |\n| GET | `?zip=dos` | ...as a WinXP-compatible zip file |\n| GET | `?zip=crc` | ...as an MSDOS-compatible zip file |\n| GET | `?tar&w` | pregenerate webp thumbnails |\n| GET | `?tar&j` | pregenerate jpg thumbnails |\n| GET | `?tar&p` | pregenerate audio waveforms |\n| GET | `?shares` | list your shared files/folders |\n| GET | `?dls` | show active downloads (do this as admin) |\n| GET | `?ups` | show recent uploads from your IP |\n| GET | `?ups&filter=f` | ...where URL contains `f` |\n| GET | `?ru` | show all recent uploads |\n| GET | `?ru&filter=f` | ...where URL contains `f` |\n| GET | `?ru&j` | ...as json |\n| GET | `?mime=foo` | specify return mimetype `foo` |\n| GET | `?v` | render markdown file at URL |\n| GET | `?v` | open image/video/audio in mediaplayer |\n| GET | `?txt` | get file at URL as plaintext |\n| GET | `?txt=iso-8859-1` | ...with specific charset |\n| GET | `?tail` | continuously stream a growing file |\n| GET | `?tail=1024` | ...starting from byte 1024 |\n| GET | `?tail=-128` | ...starting 128 bytes from the end |\n| GET | `?th` | get image/video at URL as thumbnail |\n| GET | `?th=opus` | convert audio file to 128kbps opus |\n| GET | `?th=caf` | ...in the iOS-proprietary container |\n| GET | `?zls` | get listing of filepaths in zip file at URL |\n| GET | `?zget=path` | get specific file from inside a zip file at URL |\n\n| method | body | result |\n|--|--|--|\n| jPOST | `{\"q\":\"foo\"}` | do a server-wide search; see the `[🔎]` search tab `raw` field for syntax |\n\n| method | params | body | result |\n|--|--|--|--|\n| jPOST | `?tar` | `[\"foo\",\"bar\"]` | download folders `foo` and `bar` inside URL as a tar file |\n\n## write\n\n| method | params | result |\n|--|--|--|\n| POST | `?copy=/foo/bar` | copy the file/folder at URL to /foo/bar |\n| POST | `?move=/foo/bar` | move/rename the file/folder at URL to /foo/bar |\n\n| method | params | body | result |\n|--|--|--|--|\n| PUT | | (binary data) | upload into file at URL |\n| PUT | `?j` | (binary data) | ...and reply with json |\n| PUT | `?ck` | (binary data) | upload without checksum gen (faster) |\n| PUT | `?ck=md5` | (binary data) | return md5 instead of sha512 |\n| PUT | `?gz` | (binary data) | compress with gzip and write into file at URL |\n| PUT | `?xz` | (binary data) | compress with xz and write into file at URL |\n| PUT | `?apnd` | (binary data) | append to existing file |\n| mPOST | | `f=FILE` | upload `FILE` into the folder at URL |\n| mPOST | `?j` | `f=FILE` | ...and reply with json |\n| mPOST | `?ck` | `f=FILE` | ...and disable checksum gen (faster) |\n| mPOST | `?ck=md5` | `f=FILE` | ...and return md5 instead of sha512 |\n| mPOST | `?replace` | `f=FILE` | ...and overwrite existing files |\n| mPOST | `?apnd` | `f=FILE` | ...and append to existing files |\n| mPOST | `?media` | `f=FILE` | ...and return medialink (not hotlink) |\n| mPOST | | `act=mkdir`, `name=foo` | create directory `foo` at URL |\n| POST | `?delete` | | delete URL recursively |\n| POST | `?eshare=rm` | | stop sharing a file/folder |\n| POST | `?eshare=3` | | set expiration to 3 minutes |\n| jPOST | `?share` | (complicated) | create temp URL for file/folder |\n| jPOST | `?delete` | `[\"/foo\",\"/bar\"]` | delete `/foo` and `/bar` recursively |\n| uPOST | | `msg=foo` | send message `foo` into server log |\n| mPOST | | `act=tput`, `body=TEXT` | overwrite markdown document at URL |\n\nupload modifiers:\n\n| http-header | url-param | effect |\n|--|--|--|\n| `Accept: url` | `want=url` | return just the file URL |\n| `Accept: json` | `want=json` | return upload info as json; same as `?j` |\n| `Rand: 4` | `rand=4` | generate random filename with 4 characters |\n| `Life: 30` | `life=30` | delete file after 30 seconds |\n| `Replace: 1` | `replace` | overwrite file if exists |\n| `CK: no` | `ck` | disable serverside checksum (maybe faster) |\n| `CK: md5` | `ck=md5` | return md5 checksum instead of sha512 |\n| `CK: sha1` | `ck=sha1` | return sha1 checksum |\n| `CK: sha256` | `ck=sha256` | return sha256 checksum |\n| `CK: b2` | `ck=b2` | return blake2b checksum |\n| `CK: b2s` | `ck=b2s` | return blake2s checksum |\n\n* `life` only has an effect if the volume has a lifetime, and the volume lifetime must be greater than the file's\n* `replace` upload-modifier:\n  * the header `replace: 1` works for both PUT and multipart-post\n  * the url-param `replace` only works for multipart-post\n* server behavior of `msg` can be reconfigured with `--urlform`\n\n## admin\n\n| method | params | result |\n|--|--|--|\n| GET | `?reload=cfg` | reload config files and rescan volumes |\n| GET | `?scan` | initiate a rescan of the volume which provides URL |\n| GET | `?scan=/a,/b` | initiate a rescan of volumes `/a` and `/b` |\n| GET | `?stack` | show a stacktrace of all threads |\n\n## general\n\n| method | params | result |\n|--|--|--|\n| GET | `?pw=x` | logout |\n| GET | `?grid` | ui: show grid-view |\n| GET | `?imgs` | ui: show grid-view with thumbnails |\n| GET | `?grid=0` | ui: show list-view |\n| GET | `?imgs=0` | ui: show list-view |\n| GET | `?thumb` | ui, grid-mode: show thumbnails |\n| GET | `?thumb=0` | ui, grid-mode: show icons |\n| POST | `?smsg=foo` | send-msg-to-serverlog / run xm hook |\n\n\n# event hooks\n\non writing your own [hooks](../README.md#event-hooks)\n\n## hook effects\n\nhooks can cause intentional side-effects,  such as redirecting an upload into another location, or creating+indexing additional files, or deleting existing files, by returning json on stdout\n\n* `reloc` can redirect uploads before/after uploading has finished, based on filename, extension, file contents, uploader ip/name etc.\n  * example: [reloc-by-ext](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reloc-by-ext.py)\n* `idx` informs copyparty about a new file to index as a consequence of this upload\n  * example: [podcast-normalizer.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/podcast-normalizer.py)\n* `del` tells copyparty to delete an unrelated file by vpath\n  * example: (　´・ω・) nyoro~n\n\nfor these to take effect, the hook must be defined with the `c1` flag; see example [reloc-by-ext](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reloc-by-ext.py)\n\na subset of effect types are available for a subset of hook types,\n\n* most hook types (xbu/xau/xbr/xar/xbd/xad/xm) support `idx` and `del` for all http protocols (up2k / basic-uploader / webdav), but not ftp/tftp/smb\n* most hook types will abort/reject the action if the hook returns nonzero, assuming flag `c` is given, see examples [reject-extension](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reject-extension.py) and [reject-mimetype](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reject-mimetype.py)\n* `xbu` supports `reloc` for all http protocols (up2k / basic-uploader / webdav), but not ftp/tftp/smb\n* `xau` supports `reloc` for basic-uploader / webdav only, not up2k or ftp/tftp/smb\n  * so clients like sharex are supported, but not dragdrop into browser\n\nto trigger indexing of files `/foo/1.txt` and `/foo/bar/2.txt`, a hook can `print(json.dumps({\"idx\":{\"vp\":[\"/foo/1.txt\",\"/foo/bar/2.txt\"]}}))` (and replace \"idx\" with \"del\" to delete instead)\n* note: paths starting with `/` are absolute URLs, but you can also do `../3.txt` relative to the destination folder of each uploaded file\n\n## hook import\n\nthe `I` flag runs the hook inside copyparty,  which can be very useful and dangerous:\n\n* around 140x faster because it doesn't need to launch a new subprocess\n* the hook can intentionally (or accidentally) mess with copyparty's internals\n  * very easy to crash things if not careful\n\n\n# assumptions\n\n## mdns\n\n* outgoing replies will always fit in one packet\n* if a client mentions any of our services, assume it's not missing any\n* always answer with all services, even if the client only asked for a few\n* not-impl: probe tiebreaking (too complicated)\n* not-impl: unicast listen (assume avahi took it)\n\n\n# sfx repack\n\nreduce the size of an sfx by removing features\n\nif you don't need all the features, you can repack the sfx and save a bunch of space; all you need is an sfx and a copy of this repo (nothing else to download or build, except if you're on windows then you need msys2 or WSL)\n* `393k` size of original sfx.py as of v1.1.3\n* `310k` after `./scripts/make-sfx.sh re no-cm`\n* `269k` after `./scripts/make-sfx.sh re no-cm no-hl`\n\nthe features you can opt to drop are\n* `cm`/easymde, the \"fancy\" markdown editor, saves ~89k\n* `hl`, prism, the syntax highlighter, saves ~41k\n* `fnt`, source-code-pro, the monospace font, saves ~9k\n\nfor the `re`pack to work, first run one of the sfx'es once to unpack it\n\n**note:** you can also just download and run [/scripts/copyparty-repack.sh](https://github.com/9001/copyparty/blob/hovudstraum/scripts/copyparty-repack.sh) -- this will grab the latest copyparty release from github and do a few repacks; works on linux/macos (and windows with msys2 or WSL)\n\n\n# building\n\n## dev env setup\n\nyou need python 3.9 or newer due to type hints\n\nsetting up a venv with the below packages is only necessary if you want it for vscode or similar\n\n```sh\npython3 -m venv .venv\n. .venv/bin/activate\npip install jinja2 strip_hints  # MANDATORY\npip install argon2-cffi  # password hashing\npip install pyzmq  # send 0mq from hooks\npip install mutagen  # audio metadata\npip install paramiko  # sftp server\npip install pyftpdlib  # ftp server\npip install partftpy  # tftp server\npip install impacket  # smb server -- disable Windows Defender if you REALLY need this on windows\npip install Pillow pillow-heif  # thumbnails\npip install pyvips  # faster thumbnails\npip install psutil  # better cleanup of stuck metadata parsers on windows\npip install black==21.12b0 click==8.0.2 bandit pylint flake8 isort mypy  # vscode tooling\n```\n\n* on archlinux you can do this:\n  * `sudo pacman -Sy --needed python-{pip,isort,jinja,argon2-cffi,pyzmq,mutagen,paramiko,pyftpdlib,pillow}`\n  * then, as user: `python3 -m pip install --user --break-system-packages -U strip_hints black==21.12b0 click==8.0.2`\n  * for building docker images: `sudo pacman -Sy --needed qemu-user-static{,-binfmt} podman{,-docker} jq`\n\n* and if you want to run the python 2.7 tests:\n  * `git clone https://github.com/pyenv/pyenv .pyenv ; cd .pyenv/bin ; env PYTHON_CONFIGURE_OPTS='--enable-optimizations' PYTHON_CFLAGS='-march=native -mtune=native -std=c17' ./pyenv install 2.7.18 -v ; ln -s $HOME/.pyenv/versions/2.7.18/bin/python2 $HOME/bin/`\n\n\n## just the sfx\n\nif you just want to modify the copyparty source code (py/html/css/js) then this is the easiest approach\n\nbuild the sfx using any of the following examples:\n\n```sh\n./scripts/make-sfx.sh           # regular edition\n./scripts/make-sfx.sh fast      # build faster (worse js/css compression)\n./scripts/make-sfx.sh gz no-cm  # gzip-compressed + no fancy markdown editor\n```\n\n\n## build from release tarball\n\nuses the included prebuilt webdeps\n\nif you downloaded a [release](https://github.com/9001/copyparty/releases) source tarball from github (for example [copyparty-1.6.15.tar.gz](https://github.com/9001/copyparty/releases/download/v1.6.15/copyparty-1.6.15.tar.gz) so not the autogenerated one) you can build it like so,\n\n```bash\npython3 -m pip install --user -U build setuptools wheel jinja2 strip_hints\nbash scripts/run-tests.sh python3  # optional\npython3 -m build\n```\n\nif you are unable to use `build`, you can use the old setuptools approach instead,\n\n```bash\npython3 setup.py install --user setuptools wheel jinja2\npython3 setup.py build\npython3 setup.py bdist_wheel\n# you now have a wheel which you can install. or extract and repackage:\npython3 setup.py install --skip-build --prefix=/usr --root=$HOME/pe/copyparty\n```\n\n\n## build from scratch\n\nhow the sausage is made:\n\nto get started, first `cd` into the `scripts` folder\n\n* the first step is the webdeps; they end up in `../copyparty/web/deps/` for example `../copyparty/web/deps/marked.js.gz` -- if you need to build the webdeps, run `make -C deps-docker`\n  * this needs rootless podman and the `podman-docker` compat-layer to pretend it's docker, although it *should* be possible to use rootful/rootless docker too\n  * if you don't have rootless podman/docker then `sudo make -C deps-docker` is fine too\n  * alternatively, you can entirely skip building the webdeps and instead extract the compiled webdeps from the latest github release with `./make-sfx.sh fast dl-wd`\n\n* next, build `copyparty-sfx.py` by running `./make-sfx.sh gz fast`\n  * this is a dependency for most of the remaining steps, since they take the sfx as input\n  * removing `fast` makes it compress better\n  * removing `gz` too compresses even better, but startup gets slower\n\n* if you want to build the `.pyz` standalone \"binary\", now run `./make-pyz.sh`\n\n* if you want to build the `tar.gz` for use in a linux-distro package, now run `./make-tgz-release.sh theVersionNumber`\n\n* if you want to build a pypi package, now run `./make-pypi-release.sh d`\n\n* if you want to build a docker-image, you have two options:\n  * if you want to use podman to build all docker-images for all supported architectures, now run `(cd docker; ./make.sh hclean; ./make.sh hclean pull img)`\n  * if you want to use docker to build all docker-images for your native architecture, now run `sudo make -C docker`\n  * if you want to do something else, please take a look at `docker/make.sh` or `docker/Makefile` for inspiration\n\n* if you want to build the windows exe, first grab some snacks and a beer, [you'll need it](https://github.com/9001/copyparty/tree/hovudstraum/scripts/pyinstaller)\n\nthe complete list of buildtime dependencies to do a build from scratch is as follows:\n\n* on ubuntu-server, install podman or [docker](https://get.docker.com/), and then `sudo apt install make zip bzip2`\n  * because ubuntu is specifically what someone asked about :-p\n\n\n## complete release\n\nalso builds the sfx so skip the sfx section above\n\n*WARNING: `rls.sh` has not yet been updated with the docker-images and arch/nix packaging*\n\ndoes everything completely from scratch, straight from your local repo\n\nin the `scripts` folder:\n\n* run `make -C deps-docker` to build all dependencies\n* run `./rls.sh 1.2.3` which uploads to pypi + creates github release + sfx\n\n\n# debugging\n\n## music playback halting on phones\n\nmostly fine on android,  but still haven't find a way to massage iphones into behaving well\n\n* conditionally starting/stopping mp.fau according to mp.au.readyState <3 or <4 doesn't help\n* loop=true doesn't work, and manually looping mp.fau from an onended also doesn't work (it does nothing)\n* assigning fau.currentTime in a timer doesn't work, as safari merely pretends to assign it\n* on ios 16.7.7, mp.fau can sometimes make everything visibly work correctly, but no audio is actually hitting the speakers\n\ncan be reproduced with `--no-sendfile --s-wr-sz 8192 --s-wr-slp 0.3 --rsp-slp 6` and then play a collection of small audio files with the screen off, `ffmpeg -i track01.cdda.flac -c:a libopus -b:a 128k -segment_time 12 -f segment smol-%02d.opus`\n\n\n## discarded ideas\n\n* optimization attempts which didn't improve performance\n  * remove brokers / multiprocessing stuff; https://github.com/9001/copyparty/tree/no-broker\n  * reduce the nesting / indirections in `HttpCli` / `httpcli.py`\n    * nearly zero benefit from stuff like replacing all the `self.conn.hsrv` with a local `hsrv` variable\n* single sha512 across all up2k chunks?\n  * crypto.subtle cannot into streaming, would have to use hashwasm, expensive\n* separate sqlite table per tag\n  * performance fixed by skipping some indexes (`+mt.k`)\n* audio fingerprinting\n  * only makes sense if there can be a wasm client and that doesn't exist yet (except for olaf which is agpl hence counts as not existing)\n* `os.copy_file_range` for up2k cloning\n  * almost never hit this path anyways\n* up2k partials ui\n  * feels like there isn't much point\n* cache sha512 chunks on client\n  * too dangerous -- overtaken by turbo mode\n* comment field\n  * nah\n* look into android thumbnail cache file format\n  * absolutely not\n* indexedDB for hashes, cfg enable/clear/sz, 2gb avail, ~9k for 1g, ~4k for 100m, 500k items before autoeviction\n  * blank hashlist when up-ok to skip handshake\n    * too many confusing side-effects\n* hls framework for Someone Else to drop code into :^)\n  * probably not, too much stuff to consider -- seeking, start at offset, task stitching (probably np-hard), conditional passthru, rate-control (especially multi-consumer), session keepalive, cache mgmt...\n"
  },
  {
    "path": "docs/example.conf",
    "content": "# not actually YAML but lets pretend:\n# -*- mode: yaml -*-\n# vim: ft=yaml:\n\n# append some arguments to the commandline;\n# accepts anything listed in --help (leading dashes are optional)\n# and inline comments are OK if there is 2 spaces before the '#'\n[global]\n  p: 8086, 3939  # listen on ports 8086 and 3939\n  e2dsa  # enable file indexing and filesystem scanning\n  e2ts   # and enable multimedia indexing\n  z, qr  # and zeroconf and qrcode (you can comma-separate arguments)\n\n# create users:\n[accounts]\n  ed: 123   # username: password\n  k: k\n\n# create volumes:\n[/]         # create a volume at \"/\" (the webroot), which will\n  .         # share the contents of \".\" (the current directory)\n  accs:\n    r: *    # everyone gets read-access, but\n    rw: ed  # the user \"ed\" gets read-write\n\n# let's specify different permissions for the \"priv\" subfolder\n# by creating another volume at that location:\n[/priv]\n  ./priv\n  accs:\n    r: k    # the user \"k\" can see the contents,\n    rw: ed  # while \"ed\" gets read-write\n\n# share /home/ed/Music/ as /music and let anyone read it\n# (this will replace any folder called \"music\" in the webroot)\n[/music]\n  /home/ed/Music\n  accs:\n    r: *\n\n# and a folder where anyone can upload, but nobody can see the contents\n[/dump]\n  /home/ed/inc\n  accs:\n    w: *\n  flags:\n    e2d     # the e2d volflag enables the uploads database\n    nodupe  # the nodupe volflag rejects duplicate uploads\n    # (see --help-flags for all available volflags to use)\n\n# and a folder where anyone can upload\n# and anyone can access their own uploads, but nothing else\n[/sharex]\n  /home/ed/inc/sharex\n  accs:\n    wG: *        # wG = write-upget = see your own uploads only\n    rwmd: ed, k  # read-write-modify-delete for users \"ed\" and \"k\"\n  flags:\n    e2d, d2t, fk: 4\n    # volflag \"e2d\" enables the uploads database,\n    # \"d2t\" disables multimedia parsers (in case the uploads are malicious),\n    # \"dthumb\" disables thumbnails (same reason),\n    # \"fk\" enables filekeys (necessary for upget permission) (4 chars long)\n    # -- note that its fine to combine all the volflags on\n    #    one line because only the last volflag has an argument\n\n# this entire config file can be replaced with these arguments:\n# -u ed:123 -u k:k -v .::r:a,ed -v priv:priv:r,k:rw,ed -v /home/ed/Music:music:r -v /home/ed/inc:dump:w:c,e2d,nodupe -v /home/ed/inc/sharex:sharex:wG:c,e2d,d2t,fk=4\n# but note that the config file always wins in case of conflicts\n"
  },
  {
    "path": "docs/example2.conf",
    "content": "# you can include additional config like this\n# (the space after the % is important)\n#\n# since copyparty.d is a folder, it'll include all *.conf\n# files inside (not recursively) in alphabetical order\n# (not necessarily same as numerical/natural order)\n#\n# paths are relative from the location of each included file\n# unless the path is absolute, for example % /etc/copyparty.d\n#\n# max include depth is 64\n\n% copyparty.d\n"
  },
  {
    "path": "docs/examples/README.md",
    "content": "copyparty server config examples\n\n[windows.md](windows.md) -- running copyparty as a service on windows\n\n"
  },
  {
    "path": "docs/examples/docker/basic-docker-compose/copyparty.conf",
    "content": "# not actually YAML but lets pretend:\n# -*- mode: yaml -*-\n# vim: ft=yaml:\n\n\n[global]\n  e2dsa  # enable file indexing and filesystem scanning\n  e2ts   # enable multimedia indexing\n  ansi   # enable colors in log messages (both in logfiles and stdout)\n\n  # q, lo: /cfg/log/%Y-%m%d.log   # log to file instead of docker\n\n  # p: 3939          # listen on another port\n  # ipa: 10.89.      # only allow connections from 10.89.*\n  # ipa: 172.16.4.0/23  # ...or only 172.16.4.* and 172.16.5.*\n  # ipa: lan         # ...or allow LAN only; reject internet IPs\n  # df: 16           # stop accepting uploads if less than 16 GB free disk space\n  # ver              # show copyparty version in the controlpanel\n  # grid             # show thumbnails/grid-view by default\n  # theme: 2         # monokai\n  # name: datasaver  # change the server-name that's displayed in the browser\n  # stats, nos-dup   # enable the prometheus endpoint, but disable the dupes counter (too slow)\n  # no-robots, force-js  # make it harder for search engines to read your server\n\n  # enable version-checking by uncommenting one of the vc-url lines below;\n  # shows a warning-banner in the controlpanel if your version has a known vulnerability\n  #vc-url: https://api.github.com/repos/9001/copyparty/security-advisories?per_page=9\n  #vc-url: https://api.copyparty.eu/advisories\n  vc-exit  # panic and shutdown instead of just showing the warning\n\n\n[accounts]\n  ed: wark  # username: password\n\n\n[/]            # create a volume at \"/\" (the webroot), which will\n  /w           # share /w (the docker data volume)\n  accs:\n    rw: *      # everyone gets read-write access, but\n    rwmda: ed  # the user \"ed\" gets read-write-move-delete-admin\n"
  },
  {
    "path": "docs/examples/docker/basic-docker-compose/docker-compose.yml",
    "content": "---\n\nservices:\n  copyparty:\n    image: copyparty/ac:latest\n    container_name: copyparty\n    user: \"1000:1000\"\n    ports:\n      - 3923:3923\n    volumes:\n      - ./:/cfg:z\n      - /path/to/your/fileshare/top/folder:/w:z\n\n    environment:\n      LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE\n      # enable mimalloc by replacing \"NOPE\" with \"2\" for a nice speed-boost (will use twice as much ram)\n\n      PYTHONUNBUFFERED: 1\n      # ensures log-messages are not delayed (but can reduce speed a tiny bit)\n\n    stop_grace_period: 15s  # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal\n    healthcheck:\n      # hide it from logs with \"/._\" so it matches the default --lf-url filter \n      test: [\"CMD-SHELL\", \"wget --spider -q 127.0.0.1:3923/?reset=/._\"]\n      interval: 1m\n      timeout: 2s\n      retries: 5\n      start_period: 15s\n"
  },
  {
    "path": "docs/examples/docker/idp/copyparty.conf",
    "content": "# not actually YAML but lets pretend:\n# -*- mode: yaml -*-\n# vim: ft=yaml:\n\n\n# example config for how copyparty can be used with an identity\n# provider, replacing the built-in authentication/authorization\n# mechanism, and instead expecting the reverse-proxy to provide\n# the requester's username (and possibly a group-name, for\n# optional group-based access control)\n#\n# the filesystem-path `/w` is used as the storage location\n# because that is the data-volume in the docker containers,\n# because a deployment like this (with an IdP) is more commonly\n# seen in containerized environments -- but this is not required\n#\n# the example group \"su\" (super-user) is the admins group\n\n\n[global]\n  e2dsa  # enable file indexing and filesystem scanning\n  e2ts   # enable multimedia indexing\n  ansi   # enable colors in log messages\n\n  # enable IdP support by expecting username/groupname in\n  # http-headers provided by the reverse-proxy; header \"X-IdP-User\"\n  # will contain the username, \"X-IdP-Group\" the groupname\n  idp-h-usr: x-idp-user\n  idp-h-grp: x-idp-group\n\n  # but copyparty will refuse to accept those headers unless you\n  # tell it the LAN IP of the reverse-proxy to expect them from,\n  # preventing malicious users from pretending to be the proxy;\n  # pay attention to the warning message in the logs and then\n  # adjust the following config option accordingly:\n  xff-src: 192.168.0.0/16\n\n  # or just allow all LAN / private IPs (probably good enough):\n  xff-src: lan\n\n  # an additional, optional security measure is to expect a\n  # secret header name from the reverse-proxy; you can enable\n  # this feature by setting the header-name to expect here:\n  #idp-h-key: shangala-bangala\n\n  # convenient debug option:\n  # log all incoming request headers from the proxy\n  #ihead: *\n\n[/]      # create a volume at \"/\" (the webroot), which will\n  /w     # share /w (the docker data volume)\n  accs:\n    r: *       # everyone gets read-access, but\n    rwmda: @su  # the group \"su\" gets read-write-move-delete-admin\n\n\n[/u/${u}]    # each user gets their own home-folder at /u/username\n  /w/u/${u}  # which will be \"u/username\" in the docker data volume\n  accs:\n    r: *              # read-access for anyone, and\n    rwmda: ${u}, @su  # read-write-move-delete-admin for that username + the \"su\" group\n\n\n[/u/${u}/priv]    # each user also gets a private area at /u/username/priv\n  /w/u/${u}/priv  # stored at DATAVOLUME/u/username/priv\n  accs:\n    rwmda: ${u}, @su  # read-write-move-delete-admin for that username + the \"su\" group\n\n\n[/lounge/${g}]    # each group gets their own shared volume\n  /w/lounge/${g}  # stored at DATAVOLUME/lounge/groupname\n  accs:\n    r: *               # read-access for anyone, and\n    rwmda: @${g}, @su  # read-write-move-delete-admin for that group + the \"su\" group\n\n\n[/lounge/${g}/priv]    # and a private area for each group too\n  /w/lounge/${g}/priv  # stored at DATAVOLUME/lounge/groupname/priv\n  accs:\n    rwmda: @${g}, @su  # read-write-move-delete-admin for that group + the \"su\" group\n\n\n[/sus/${u%+su}]  # users which ARE members of group \"su\" gets /sus/username\n  /w/tank1/${u}  # which will be \"tank1/username\" in the docker data volume\n  accs:\n    rwmda: ${u}  # read-write-move-delete-admin for that username\n\n\n[/m8s/${u%-su}]  # users which are NOT members of group \"su\" gets /m8s/username\n  /w/tank2/${u}  # which will be \"tank2/username\" in the docker data volume\n  accs:\n    rwmda: ${u}  # read-write-move-delete-admin for that username\n\n\n# and create some strategic volumes to prevent anyone from gaining\n# unintended access to priv folders if the users/groups db is lost\n[/u]\n  /w/u\n  accs:\n    rwmda: @su\n[/lounge]\n  /w/lounge\n  accs:\n    rwmda: @su\n[/sus]\n  /w/tank1\n[/m8s]\n  /w/tank2\n\n\n# some other things you can do:\n#  [/demo/${u%-su,%-fds}]   # users which are NOT members of \"su\" or \"fds\"\n#  [/demo/${u%+su,%+fds}]   # users which ARE members of BOTH \"su\" and \"fds\"\n#  [/demo/${g%-su}]         # all groups except su\n#  [/demo/${g%-su,%-fds}]   # all groups except su and fds\n"
  },
  {
    "path": "docs/examples/docker/idp-authelia-traefik/README.md",
    "content": "> [!WARNING]  \n> I am unable to guarantee the quality, safety, and security of anything in this folder; it is a combination of examples I found online. Please submit corrections or improvements 🙏\n\nto try this out with minimal adjustments:\n* specify what filesystem-path to share with copyparty, replacing the default/example value `/srv/pub` in `docker-compose.yml`\n* add `127.0.0.1 fs.example.com traefik.example.com authelia.example.com` to your `/etc/hosts`\n* `sudo docker-compose up`\n* login to https://fs.example.com/ with username `authelia` password `authelia`\n\nto use this in a safe and secure manner:\n* follow a guide on setting up [authelia](https://www.authelia.com/integration/proxies/traefik/#docker-compose) properly and use the copyparty-specific parts of this folder as inspiration for your own config; namely the `cpp` subfolder and the `copyparty` service in `docker-compose.yml`\n\nthis folder is based on:\n* https://github.com/authelia/authelia/tree/39763aaed24c4abdecd884b47357a052b235942d/examples/compose/lite\n\nincomplete list of modifications made:\n* support for running with podman as root on fedora (`:z` volumes, `label:disable`)\n* explicitly using authelia `v4.38.0-beta3` because config syntax changed since last stable release\n* reduced logging from debug to info\n* implemented a docker socket-proxy to not bind the docker.socket directly to traefik\n* using valkey instead of redis for caching\n\n\n# security\n\nthere is probably/definitely room for improvement in this example setup. Some ideas taken from [github issue #62](https://github.com/9001/copyparty/issues/62):\n\n* Move valkey to a private network shared with just authelia\n* Add `watchtower` to manage your image version updates\n* Drop bridge networking for just exposing traefik's public ports\n\nif you manage to improve on any of this, especially in a way that might be useful for other people, consider sending a PR :>\n\n\n# performance\n\ncurrently **not optimal,** at least when compared to running the python sfx outside of docker... some numbers from my laptop (ryzen4500u/fedora39):\n\n| req/s |  https D/L | http D/L | approach |\n| -----:| ----------:|:--------:| -------- |\n|  5200 | 1294 MiB/s | 5+ GiB/s | [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) running on host |\n|  4370 |  725 MiB/s | 4+ GiB/s | `docker run copyparty/ac` |\n|  2420 |  694 MiB/s | n/a      | `copyparty/ac` behind traefik |\n|    75 |  694 MiB/s | n/a      | traefik and authelia **(you are here)** |\n\nauthelia is behaving strangely, handling 340 requests per second for a while, but then it suddenly drops to 75 and stays there...\n\nI'm assuming all of the performance issues is due to a misconfiguration of authelia/traefik/docker on my end, but I don't really know where to start\n"
  },
  {
    "path": "docs/examples/docker/idp-authelia-traefik/authelia/configuration.yml",
    "content": "# Authelia configuration\n\nidentity_validation:\n  reset_password:\n    jwt_secret: 'a_very_important_secret_so_please_change_this'\n\nserver:\n  address: 'tcp://:9091'\n\nlog:\n  level: info\n\ntotp:\n  issuer: authelia.com\n\nauthentication_backend:\n  file:\n    path: /config/users_database.yml\n\naccess_control:\n  default_policy: deny\n  rules:\n    - domain: auth.example.com\n      policy: bypass  # Allow access to the login UI\n    - domain: fs.example.com\n      policy: one_factor\n\nsession:\n  secret: unsecure_session_secret\n  cookies:\n    - name: authelia_session\n      domain: example.com  # this should match whatever your root protected domain is\n      default_redirection_url: https://fs.example.com\n      authelia_url: https://authelia.example.com/\n      expiration: 3600  # 1 hour\n      inactivity: 300  # 5 minutes\n\n  redis:\n    host: valkey\n    port: 6379\n    password: your_secure_password_here\n\n\nregulation:\n  max_retries: 3\n  find_time: 120\n  ban_time: 300\n\nstorage:\n  encryption_key: you_must_generate_a_random_string_of_more_than_twenty_chars_and_configure_this\n  local:\n    path: /config/db.sqlite3\n\nnotifier:\n  disable_startup_check: true\n  smtp:\n    address: 'smtp://127.0.0.1:25'\n    username: 'test'\n    password: 'password'\n    sender: \"Authelia <admin@example.com>\"\n"
  },
  {
    "path": "docs/examples/docker/idp-authelia-traefik/authelia/users_database.yml",
    "content": "# based on https://github.com/authelia/authelia/blob/39763aaed24c4abdecd884b47357a052b235942d/examples/compose/lite/authelia/users_database.yml\n\n# Users Database\n\n# This file can be used if you do not have an LDAP set up.\n\n# List of users\nusers:\n  authelia:\n    disabled: false\n    displayname: \"Authelia User\"\n    # Password is authelia\n    password: \"$6$rounds=50000$BpLnfgDsc2WD8F2q$Zis.ixdg9s/UOJYrs56b5QEZFiZECu0qZVNsIYxBaNJ7ucIL.nlxVCT5tqh8KHG8X4tlwCFm5r6NTOZZ5qRFN/\"\n    email: authelia@authelia.com\n    groups:\n      - admins\n      - dev\n      - su\n"
  },
  {
    "path": "docs/examples/docker/idp-authelia-traefik/cpp/copyparty.conf",
    "content": "# not actually YAML but lets pretend:\n# -*- mode: yaml -*-\n# vim: ft=yaml:\n\n\n# example config for how authelia can be used to replace\n# copyparty's built-in authentication/authorization mechanism,\n# providing copyparty with HTTP headers through traefik to\n# signify who the user is, and what groups they belong to\n#\n# the filesystem-path that will be shared with copyparty is\n# specified in the docker-compose in the parent folder, where\n# a real filesystem-path is mapped onto this container's path `/w`,\n# meaning `/w` in this config-file is actually `/srv/pub` in the\n# outside world (assuming you didn't modify that value)\n\n\n[global]\n  e2dsa  # enable file indexing and filesystem scanning\n  e2ts   # enable multimedia indexing\n  ansi   # enable colors in log messages\n  #q      # disable logging for more performance\n\n  # if we are confident that we got the docker-network config correct\n  # (meaning copyparty is only accessible through traefik, and\n  #  traefik makes sure that all requests go through authelia),\n  # then accept X-Forwarded-For and IdP headers from any private IP:\n  xff-src: lan\n\n  # enable IdP support by expecting username/groupname in\n  # http-headers provided by the reverse-proxy; header \"X-IdP-User\"\n  # will contain the username, \"X-IdP-Group\" the groupname\n  idp-h-usr: remote-user\n  idp-h-grp: remote-groups\n\n  # DEBUG: show all incoming request headers from traefik/authelia\n  #ihead: *\n\n\n[/]      # create a volume at \"/\" (the webroot), which will\n  /w     # share /w (the docker data volume, which is mapped to /srv/pub on the host in docker-compose.yml)\n  accs:\n    rw: *       # everyone gets read-access, but\n    rwmda: @su  # the group \"su\" gets read-write-move-delete-admin\n\n\n[/u/${u}]    # each user gets their own home-folder at /u/username\n  /w/u/${u}  # which will be \"u/username\" in the docker data volume\n  accs:\n    r: *              # read-access for anyone, and\n    rwmda: ${u}, @su  # read-write-move-delete-admin for that username + the \"su\" group\n\n\n[/u/${u}/priv]    # each user also gets a private area at /u/username/priv\n  /w/u/${u}/priv  # stored at DATAVOLUME/u/username/priv\n  accs:\n    rwmda: ${u}, @su  # read-write-move-delete-admin for that username + the \"su\" group\n\n\n[/lounge/${g}]    # each group gets their own shared volume\n  /w/lounge/${g}  # stored at DATAVOLUME/lounge/groupname\n  accs:\n    r: *               # read-access for anyone, and\n    rwmda: @${g}, @su  # read-write-move-delete-admin for that group + the \"su\" group\n\n\n[/lounge/${g}/priv]    # and a private area for each group too\n  /w/lounge/${g}/priv  # stored at DATAVOLUME/lounge/groupname/priv\n  accs:\n    rwmda: @${g}, @su  # read-write-move-delete-admin for that group + the \"su\" group\n\n\n# and create some strategic volumes to prevent anyone from gaining\n# unintended access to priv folders if the users/groups db is lost\n[/u]\n  /w/u\n  accs:\n    rwmda: @su\n[/lounge]\n  /w/lounge\n  accs:\n    rwmda: @su\n"
  },
  {
    "path": "docs/examples/docker/idp-authelia-traefik/docker-compose.yml",
    "content": "---\n\nnetworks:\n  net:\n    driver: bridge\n\nservices:\n  copyparty:\n    image: copyparty/ac:latest\n    container_name: idp_copyparty\n    user: \"1000:1000\"  # should match the user/group of your fileshare volumes\n    volumes:\n      - ./cpp/:/cfg:z  # the copyparty config folder\n      - /srv/pub:/w:z  # this is where we declare that \"/srv/pub\" is the filesystem-path on the server that shall be shared online\n    networks:\n      - net\n    expose:\n      - 3923\n    labels:\n      - 'traefik.enable=true'\n      - 'traefik.http.routers.copyparty.rule=Host(`fs.example.com`)'\n      - 'traefik.http.routers.copyparty.entrypoints=websecure'\n      - 'traefik.http.routers.copyparty.tls=true'\n      - 'traefik.http.routers.copyparty.tls.certresolver=letsencrypt'  # ← THIS IS CRUCIAL\n      - 'traefik.http.routers.copyparty.middlewares=authelia@docker'\n    stop_grace_period: 15s  # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal\n    environment:\n      LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE\n      # enable mimalloc by replacing \"NOPE\" with \"2\" for a nice speed-boost (will use twice as much ram)\n      PYTHONUNBUFFERED: 1\n      # ensures log-messages are not delayed (but can reduce speed a tiny bit)\n\n  authelia:\n    image: authelia/authelia:4.39.5@sha256:023e02e5203dfa0ebaee7a48b5bae34f393d1f9cada4a9df7fbf87eb1759c671\n    container_name: idp_authelia\n    volumes:\n      - ./authelia:/config:z\n    networks:\n      - net\n    labels:\n      - 'traefik.enable=true'\n      - 'traefik.http.routers.authelia.rule=Host(`authelia.example.com`)'\n      - 'traefik.http.routers.authelia.entrypoints=websecure'\n      - 'traefik.http.routers.authelia.tls=true'\n      - 'traefik.http.routers.authelia.tls.certresolver=letsencrypt'\n      - 'traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/authz/forward-auth?authelia_url=https://authelia.example.com'\n      - 'traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true'\n      - 'traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email'\n    expose:\n      - 9091\n    restart: unless-stopped\n    environment:\n      - TZ=Etc/UTC\n\n  valkey:\n    image: valkey/valkey:8.1.3-alpine3.22@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4\n    container_name: idp_valkey\n    volumes:\n      - ./valkey:/data:z\n    networks:\n      - net\n    expose:\n      - 6379\n    restart: unless-stopped\n    environment:\n      - TZ=Etc/UTC\n      - VALKEY_EXTRA_FLAGS=--requirepass your_secure_password_here\n\n  socket-proxy:\n    image: lscr.io/linuxserver/socket-proxy:3.2.3@sha256:63d2e0ce6bb0d12dfdbde5c3af31d08fee343ec3801a050c8197a3f5ffae8bed\n    container_name: idp_socket_proxy\n    environment:\n      - CONTAINERS=1\n      - NETWORKS=1\n      - EVENTS=1\n      - PING=1\n      - VERSION=1\n      - LOG_LEVEL=warning\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock:ro\n    security_opt:\n      - no-new-privileges:true\n    read_only: true\n    tmpfs:\n      - /run\n    networks:\n      - net\n    restart: unless-stopped\n    expose:\n      - 2375\n\n  traefik:\n    image: traefik:3.5.0@sha256:4e7175cfe19be83c6b928cae49dde2f2788fb307189a4dc9550b67acf30c11a5\n    container_name: idp_traefik\n    volumes:\n      - ./traefik:/etc/traefik:z\n    networks:\n      - net\n    labels:\n      - 'traefik.enable=true'\n      - 'traefik.http.routers.api.middlewares=authelia@docker'\n    ports:\n      - '80:80'\n      - '443:443'\n    command:\n      - '--global.sendAnonymousUsage=false'\n      - '--providers.docker.endpoint=tcp://socket-proxy:2375'\n      - '--providers.docker.exposedByDefault=false'\n      - '--entrypoints.web.address=:80'\n      - '--entrypoints.web.http.redirections.entrypoint.to=websecure'\n      - '--entrypoints.web.http.redirections.entrypoint.scheme=https'\n      - '--entrypoints.websecure.address=:443'\n      - '--certificatesResolvers.letsencrypt.acme.email=your-email@your-domain.com'\n      - '--certificatesResolvers.letsencrypt.acme.storage=/etc/traefik/acme.json'\n      - '--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=web'\n      - '--log.level=INFO'\n    depends_on:\n      - socket-proxy\n"
  },
  {
    "path": "docs/examples/docker/idp-authentik-traefik/README.md",
    "content": "> [!WARNING]  \n> I am unable to guarantee the quality, safety, and security of anything in this folder; it is a combination of examples I found online. Please submit corrections or improvements 🙏\n\n> [!WARNING]  \n> does not work yet... if you are able to fix this, please do!\n\nthis is based on:\n* https://goauthentik.io/docker-compose.yml\n* https://goauthentik.io/docs/providers/proxy/server_traefik\n\nincomplete list of modifications made:\n* support for running with podman as root on fedora (`:z` volumes, `label:disable`)\n"
  },
  {
    "path": "docs/examples/docker/idp-authentik-traefik/based-on/docker-compose-authentik.yml",
    "content": "# https://goauthentik.io/docker-compose.yml\n---\nversion: \"3.4\"\n\nservices:\n  postgresql:\n    image: docker.io/library/postgres:12-alpine\n    restart: unless-stopped\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}\"]\n      start_period: 20s\n      interval: 30s\n      retries: 5\n      timeout: 5s\n    volumes:\n      - database:/var/lib/postgresql/data\n    environment:\n      POSTGRES_PASSWORD: ${PG_PASS:?database password required}\n      POSTGRES_USER: ${PG_USER:-authentik}\n      POSTGRES_DB: ${PG_DB:-authentik}\n    env_file:\n      - .env\n  redis:\n    image: docker.io/library/redis:alpine\n    command: --save 60 1 --loglevel warning\n    restart: unless-stopped\n    healthcheck:\n      test: [\"CMD-SHELL\", \"redis-cli ping | grep PONG\"]\n      start_period: 20s\n      interval: 30s\n      retries: 5\n      timeout: 3s\n    volumes:\n      - redis:/data\n  server:\n    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.2.1}\n    restart: unless-stopped\n    command: server\n    environment:\n      AUTHENTIK_REDIS__HOST: redis\n      AUTHENTIK_POSTGRESQL__HOST: postgresql\n      AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}\n      AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}\n      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}\n    volumes:\n      - ./media:/media\n      - ./custom-templates:/templates\n    env_file:\n      - .env\n    ports:\n      - \"${COMPOSE_PORT_HTTP:-9000}:9000\"\n      - \"${COMPOSE_PORT_HTTPS:-9443}:9443\"\n    depends_on:\n      - postgresql\n      - redis\n  worker:\n    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.2.1}\n    restart: unless-stopped\n    command: worker\n    environment:\n      AUTHENTIK_REDIS__HOST: redis\n      AUTHENTIK_POSTGRESQL__HOST: postgresql\n      AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}\n      AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}\n      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}\n    # `user: root` and the docker socket volume are optional.\n    # See more for the docker socket integration here:\n    # https://goauthentik.io/docs/outposts/integrations/docker\n    # Removing `user: root` also prevents the worker from fixing the permissions\n    # on the mounted folders, so when removing this make sure the folders have the correct UID/GID\n    # (1000:1000 by default)\n    user: root\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock\n      - ./media:/media\n      - ./certs:/certs\n      - ./custom-templates:/templates\n    env_file:\n      - .env\n    depends_on:\n      - postgresql\n      - redis\n\nvolumes:\n  database:\n    driver: local\n  redis:\n    driver: local\n"
  },
  {
    "path": "docs/examples/docker/idp-authentik-traefik/based-on/docker-compose-traefik.yml",
    "content": "# https://goauthentik.io/docs/providers/proxy/server_traefik\n---\nversion: \"3.7\"\nservices:\n  traefik:\n    image: traefik:v2.2\n    container_name: traefik\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock\n    ports:\n      - 80:80\n    command:\n      - \"--api\"\n      - \"--providers.docker=true\"\n      - \"--providers.docker.exposedByDefault=false\"\n      - \"--entrypoints.web.address=:80\"\n\n  authentik-proxy:\n    image: ghcr.io/goauthentik/proxy\n    ports:\n      - 9000:9000\n      - 9443:9443\n    environment:\n      AUTHENTIK_HOST: https://your-authentik.tld\n      AUTHENTIK_INSECURE: \"false\"\n      AUTHENTIK_TOKEN: token-generated-by-authentik\n      # Starting with 2021.9, you can optionally set this too\n      # when authentik_host for internal communication doesn't match the public URL\n      # AUTHENTIK_HOST_BROWSER: https://external-domain.tld\n    labels:\n      traefik.enable: true\n      traefik.port: 9000\n      traefik.http.routers.authentik.rule: Host(`app.company`) && PathPrefix(`/outpost.goauthentik.io/`)\n      # `authentik-proxy` refers to the service name in the compose file.\n      traefik.http.middlewares.authentik.forwardauth.address: http://authentik-proxy:9000/outpost.goauthentik.io/auth/traefik\n      traefik.http.middlewares.authentik.forwardauth.trustForwardHeader: true\n      traefik.http.middlewares.authentik.forwardauth.authResponseHeaders: X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-version\n    restart: unless-stopped\n\n  whoami:\n    image: containous/whoami\n    labels:\n      traefik.enable: true\n      traefik.http.routers.whoami.rule: Host(`app.company`)\n      traefik.http.routers.whoami.middlewares: authentik@docker\n    restart: unless-stopped\n"
  },
  {
    "path": "docs/examples/docker/idp-authentik-traefik/cpp/copyparty.conf",
    "content": "# not actually YAML but lets pretend:\n# -*- mode: yaml -*-\n# vim: ft=yaml:\n\n\n# example config for how copyparty can be used with an identity\n# provider, replacing the built-in authentication/authorization\n# mechanism, and instead expecting the reverse-proxy to provide\n# the requester's username (and possibly a group-name, for\n# optional group-based access control)\n#\n# the filesystem-path `/w` is used as the storage location\n# because that is the data-volume in the docker containers,\n# because a deployment like this (with an IdP) is more commonly\n# seen in containerized environments -- but this is not required\n\n\n[global]\n  e2dsa  # enable file indexing and filesystem scanning\n  e2ts   # enable multimedia indexing\n  ansi   # enable colors in log messages\n\n  # enable IdP support by expecting username/groupname in\n  # http-headers provided by the reverse-proxy; header \"X-IdP-User\"\n  # will contain the username, \"X-IdP-Group\" the groupname\n  idp-h-usr: x-idp-user\n  idp-h-grp: x-idp-group\n\n\n[/]      # create a volume at \"/\" (the webroot), which will\n  /w     # share /w (the docker data volume, which is mapped to /srv/pub on the host in docker-compose.yml)\n  accs:\n    rw: *       # everyone gets read-access, but\n    rwmda: @su  # the group \"su\" gets read-write-move-delete-admin\n\n\n[/u/${u}]    # each user gets their own home-folder at /u/username\n  /w/u/${u}  # which will be \"u/username\" in the docker data volume\n  accs:\n    r: *              # read-access for anyone, and\n    rwmda: ${u}, @su  # read-write-move-delete-admin for that username + the \"su\" group\n\n\n[/u/${u}/priv]    # each user also gets a private area at /u/username/priv\n  /w/u/${u}/priv  # stored at DATAVOLUME/u/username/priv\n  accs:\n    rwmda: ${u}, @su  # read-write-move-delete-admin for that username + the \"su\" group\n\n\n[/lounge/${g}]    # each group gets their own shared volume\n  /w/lounge/${g}  # stored at DATAVOLUME/lounge/groupname\n  accs:\n    r: *               # read-access for anyone, and\n    rwmda: @${g}, @su  # read-write-move-delete-admin for that group + the \"su\" group\n\n\n[/lounge/${g}/priv]    # and a private area for each group too\n  /w/lounge/${g}/priv  # stored at DATAVOLUME/lounge/groupname/priv\n  accs:\n    rwmda: @${g}, @su  # read-write-move-delete-admin for that group + the \"su\" group\n\n\n# and create some strategic volumes to prevent anyone from gaining\n# unintended access to priv folders if the users/groups db is lost\n[/u]\n  /w/u\n  accs:\n    rwmda: @su\n[/lounge]\n  /w/lounge\n  accs:\n    rwmda: @su\n"
  },
  {
    "path": "docs/examples/docker/idp-authentik-traefik/docker-compose.yml",
    "content": "version: \"3.4\"\n\nvolumes:\n  database:\n    driver: local\n  redis:\n    driver: local\n\nservices:\n  copyparty:\n    image: copyparty/ac\n    container_name: idp_copyparty\n    restart: unless-stopped\n    user: \"1000:1000\"  # should match the user/group of your fileshare volumes\n    volumes:\n      - ./cpp/:/cfg:z  # the copyparty config folder\n      - /srv/pub:/w:z  # this is where we declare that \"/srv/pub\" is the filesystem-path on the server that shall be shared online\n    ports:\n      - 3923\n    labels:\n      - 'traefik.enable=true'\n      - 'traefik.http.routers.fs.rule=Host(`fs.example.com`)'\n      - 'traefik.http.routers.fs.entrypoints=http'\n      #- 'traefik.http.routers.fs.middlewares=authelia@docker'  # TODO: ???\n    stop_grace_period: 15s  # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal\n    environment:\n      LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE\n      # enable mimalloc by replacing \"NOPE\" with \"2\" for a nice speed-boost (will use twice as much ram)\n\n      PYTHONUNBUFFERED: 1\n      # ensures log-messages are not delayed (but can reduce speed a tiny bit)\n\n  traefik:\n    image: traefik:v2.11\n    container_name: traefik\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock  # WARNING: this gives traefik full root-access to the host OS, but is recommended/required(?) by traefik\n    security_opt:\n      - label:disable  # disable selinux because it (rightly) blocks access to docker.sock\n    ports:\n      - 80:80\n    command:\n      - '--api'\n      - '--providers.docker=true'\n      - '--providers.docker.exposedByDefault=false'\n      - '--entrypoints.web.address=:80'\n\n  postgresql:\n    image: docker.io/library/postgres:12-alpine\n    container_name: idp_postgresql\n    restart: unless-stopped\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}\"]\n      start_period: 20s\n      interval: 30s\n      retries: 5\n      timeout: 5s\n    volumes:\n      - database:/var/lib/postgresql/data:z\n    environment:\n      POSTGRES_PASSWORD: postgrass\n      POSTGRES_USER: authentik\n      POSTGRES_DB: authentik\n    env_file:\n      - .env\n\n  redis:\n    image: docker.io/library/redis:alpine\n    command: --save 60 1 --loglevel warning\n    container_name: idp_redis\n    restart: unless-stopped\n    healthcheck:\n      test: [\"CMD-SHELL\", \"redis-cli ping | grep PONG\"]\n      start_period: 20s\n      interval: 30s\n      retries: 5\n      timeout: 3s\n    volumes:\n      - redis:/data:z\n\n  authentik_server:\n    image: ghcr.io/goauthentik/server:2024.2.1\n    container_name: idp_authentik_server\n    restart: unless-stopped\n    command: server\n    environment:\n      AUTHENTIK_REDIS__HOST: redis\n      AUTHENTIK_POSTGRESQL__HOST: postgresql\n      AUTHENTIK_POSTGRESQL__USER: authentik\n      AUTHENTIK_POSTGRESQL__NAME: authentik\n      AUTHENTIK_POSTGRESQL__PASSWORD: postgrass\n    volumes:\n      - ./media:/media:z\n      - ./custom-templates:/templates:z\n    env_file:\n      - .env\n    ports:\n      - 9000\n      - 9443\n    depends_on:\n      - postgresql\n      - redis\n\n  authentik_worker:\n    image: ghcr.io/goauthentik/server:2024.2.1\n    container_name: idp_authentik_worker\n    restart: unless-stopped\n    command: worker\n    environment:\n      AUTHENTIK_REDIS__HOST: redis\n      AUTHENTIK_POSTGRESQL__HOST: postgresql\n      AUTHENTIK_POSTGRESQL__USER: authentik\n      AUTHENTIK_POSTGRESQL__NAME: authentik\n      AUTHENTIK_POSTGRESQL__PASSWORD: postgrass\n    # `user: root` and the docker socket volume are optional.\n    # See more for the docker socket integration here:\n    # https://goauthentik.io/docs/outposts/integrations/docker\n    # Removing `user: root` also prevents the worker from fixing the permissions\n    # on the mounted folders, so when removing this make sure the folders have the correct UID/GID\n    # (1000:1000 by default)\n    user: root\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock\n      - ./media:/media:z\n      - ./certs:/certs:z\n      - ./custom-templates:/templates:z\n    env_file:\n      - .env\n    depends_on:\n      - postgresql\n      - redis\n"
  },
  {
    "path": "docs/examples/docker/portainer.md",
    "content": "the following setup appears to work (copyparty starts, accepts uploads, is able to persist config)\n\ntested on debian 12 using [portainer-ce](https://docs.portainer.io/start/install-ce/server/docker/linux) with [docker-ce](https://docs.docker.com/engine/install/debian/) as root (not rootless)\n\nbefore making the container, first `mkdir /etc/copyparty /srv/pub` which will be bind-mounts into the container\n\n> both `/etc/copyparty` and `/srv/pub` are examples; you can change them if you'd like\n\nput your copyparty config files directly into `/etc/copyparty` and the files to share inside `/srv/pub`\n\non first startup, copyparty will create a subfolder inside `/etc/copyparty` called `copyparty` where it puts some runtime state; for example replacing `/etc/copyparty/copyparty/cert.pem` with another TLS certificate is a quick and dirty way to get valid HTTPS (if you really want copyparty to handle that and not a reverse-proxy)\n\n\n## in portainer:\n\n```\nenvironments -> local -> containers -> add container:\n\n       name = copyparty-ac\n   registry = docker hub\n      image = copyparty/ac\nalways pull = no\n\nmanual network port publishing:\n  3923 to 3923 [TCP]\n\nadvanced -> command & logging:\n  console = interactive & tty\n\nadvanced -> volumes -> map additional volume:\n  container = /cfg  [Bind]\n  host = /etc/copyparty  [Writable]\n\nadvanced -> volumes -> map additional volume:\n  container = /w  [Bind]\n  host = /srv/pub  [Writable]\n```\n\nnotes:\n\n* `/cfg` is where copyparty expects to find its config files; `/etc/copyparty` is just an example mapping to that\n\n* `/w` is where copyparty expects to find the folder to share; `/srv/pub` is just an example mapping to that\n\n* the volumes must be bind-mounts to avoid permission issues (or so the theory goes)\n"
  },
  {
    "path": "docs/examples/windows.md",
    "content": "# running copyparty on windows\n\nthis is a complete example / quickstart for running copyparty on windows, optionally as a service (autostart on boot)\n\nyou will definitely need either [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) (comfy, portable, more features) or [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) (smaller, safer)\n\n* if you decided to grab `copyparty-sfx.py` instead of the exe you will also need to install the [\"Latest Python 3 Release\"](https://www.python.org/downloads/windows/)\n\nthen you probably want to download [FFmpeg](https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z) and put `ffmpeg.exe` and `ffprobe.exe` in your PATH (so for example `C:\\Windows\\System32\\`) -- this enables thumbnails, audio transcoding, and making music metadata searchable\n\n\n## the config file\n\nopen up notepad and save the following as `c:\\users\\you\\documents\\party.conf` (for example)\n\n```yaml\n[global]\n  lo: ~/logs/cpp-%Y-%m%d.xz  # log to c:\\users\\you\\logs\\\n  e2dsa, e2ts, z    # sets 3 flags; see explanation\n  p: 80, 443  # listen on ports 80 and 443, not 3923\n  theme: 2    # default theme: protonmail-monokai\n  lang: nor   # default language: viking\n\n[accounts]                  # usernames and passwords\n  kevin: shangalabangala    # kevin's password\n\n[/]               # create a volume available at /\n  c:\\pub          # sharing this filesystem location\n  accs:           # and set permissions:\n    r: *          # everyone can read/download files,\n    rwmd: kevin   # kevin can read/write/move/delete\n\n[/inc]            # create another volume at /inc\n  c:\\pub\\inc      # sharing this filesystem location\n  accs:           # permissions:\n    w: *          # everyone can upload, but not browse\n    rwmd: kevin   # kevin is admin here too\n\n[/music]          # and a third volume at /music\n  ~/music         # which shares c:\\users\\you\\music\n  accs:\n    r: *\n    rwmd: kevin\n```\n\n\n### config explained: [global]\n\nthe `[global]` section accepts any config parameters [listed here](https://ocv.me/copyparty/helptext.html), also viewable by running copyparty (either the exe or the sfx.py) with `--help`, so this is the same as running copyparty with arguments `--lo c:\\users\\you\\logs\\copyparty-%Y-%m%d.xz -e2dsa -e2ts -z -p 80,443 --theme 2 --lang nor`\n* `lo: ~/logs/cpp-%Y-%m%d.xz` writes compressed logs (the compression will make them delayed)\n* `e2dsa` enables the file indexer, which enables searching and upload-undo\n* `e2ts` enables music metadata indexing, making albums / titles etc. searchable too\n  * but the improved upload speed from `e2dsa` is not affected\n* `z` enables zeroconf, making the server available at `http://HOSTNAME.local/` from any other machine in the LAN\n* `p: 80,443` listens on the ports `80` and `443` instead of the default `3923`\n* `lang: nor` sets default language to viking\n\n\n### config explained: [accounts]\n\nthe `[accounts]` section defines all the user accounts, which can then be referenced when granting people access to the different volumes\n\n\n### config explained: volumes\n\nthen we create three volumes, one at `/`, one at `/inc`, and one at `/music`\n* `/` and `/music` are readable without requiring people to login (`r: *`) but you need to login as kevin to write/move/delete files (`rwmd: kevin`)\n* anyone can upload to `/inc` but you must be logged in as kevin to see the files inside\n\n\n## run copyparty\n\nto test your config it's best to just run copyparty in a console to watch the output:\n\n```batch\ncopyparty.exe -c party.conf\n```\n\nor if you wanna use `copyparty-sfx.py` instead of the exe (understandable),\n\n```batch\n%localappdata%\\programs\\python\\python311\\python.exe copyparty-sfx.py -c party.conf\n```\n\n(please adjust `python311` to match the python version you installed, i'm not good enough at windows to make that bit generic)\n\n\n## run it as a service\n\nto run this as a service you need [NSSM](https://nssm.cc/ci/nssm-2.24-101-g897c7ad.zip), so put the exe somewhere in your PATH\n\nthen either do this for `copyparty.exe`:\n```batch\nnssm install cpp %homedrive%%homepath%\\downloads\\copyparty.exe -c %homedrive%%homepath%\\documents\\party.conf\n```\n\nor do this for `copyparty-sfx.py`:\n```batch\nnssm install cpp %localappdata%\\programs\\python\\python311\\python.exe %homedrive%%homepath%\\downloads\\copyparty-sfx.py -c %homedrive%%homepath%\\documents\\party.conf\n```\n\nthen after creating the service, modify it so it runs with your own windows account (so file permissions don't get wonky and paths expand as expected):\n```batch\nnssm set cpp ObjectName .\\yourAccoutName yourWindowsPassword\nnssm start cpp\n```\n\nand that's it, all good\n\nif it doesn't start, enable stderr logging so you can see what went wrong:\n```batch\nnssm set cpp AppStderr %homedrive%%homepath%\\logs\\cppsvc.err\nnssm set cpp AppStderrCreationDisposition 2\n```\n"
  },
  {
    "path": "docs/hls.html",
    "content": "<!DOCTYPE html><html lang=\"en\"><head>\n\t<meta charset=\"utf-8\">\n\t<title>hls-test</title>\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n</head><body>\n\n<video id=\"vid\" controls></video>\n<script src=\"hls.light.js\"></script>\n<script>\n\nvar video = document.getElementById('vid');\nvar hls = new Hls({\n\tdebug: true,\n\tautoStartLoad: false\n});\nhls.loadSource('live/v.m3u8');\nhls.attachMedia(video);\nhls.on(Hls.Events.MANIFEST_PARSED, function() {\n\thls.startLoad(0);\n});\nhls.on(Hls.Events.MEDIA_ATTACHED, function() {\n\tvideo.muted = true;\n\tvideo.play();\n});\n\n/*\ngeneral good news:\n- doesn't need fixed-length segments; ok to let x264 pick optimal keyframes and slice on those\n- hls.js polls the m3u8 for new segments, scales the duration accordingly, seeking works great\n- the sfx will grow by 66 KiB since that's how small hls.js can get, wait thats not good\n\n# vod, creates m3u8 at the end, fixed keyframes, v bad\nffmpeg -hide_banner -threads 0 -flags -global_header -i ..\\CowboyBebopMovie-OP1.webm -vf scale=1280:-4,format=yuv420p -ac 2 -c:a libopus -b:a 128k -c:v libx264 -preset slow -crf 24 -maxrate:v 5M -bufsize:v 10M -g 120 -keyint_min 120 -sc_threshold 0 -hls_time 4 -hls_playlist_type vod -hls_segment_filename v%05d.ts v.m3u8\n\n# live, updates m3u8 as it goes, dynamic keyframes, streamable with hls.js\nffmpeg -hide_banner -threads 0 -flags -global_header -i ..\\..\\CowboyBebopMovie-OP1.webm -vf scale=1280:-4,format=yuv420p -ac 2 -c:a libopus -b:a 128k -c:v libx264 -preset slow -crf 24 -maxrate:v 5M -bufsize:v 10M -f segment -segment_list v.m3u8 -segment_format mpegts -segment_list_flags live v%05d.ts\n\n# fmp4 (fragmented mp4), doesn't work with hls.js, gets duratoin 149:07:51 (536871s), probably the tkhd/mdhd 0xffffffff (timebase 8000? ok)\nffmpeg -re -hide_banner -threads 0 -flags +cgop -i ..\\..\\CowboyBebopMovie-OP1.webm -vf scale=1280:-4,format=yuv420p -ac 2 -c:a libopus -b:a 128k -c:v libx264 -preset slow -crf 24 -maxrate:v 5M -bufsize:v 10M -f segment -segment_list v.m3u8 -segment_format fmp4 -segment_list_flags live v%05d.mp4\n\n# try 2, works, uses tempfiles for m3u8 updates, good, 6% smaller\nffmpeg -re -hide_banner -threads 0 -flags +cgop -i ..\\..\\CowboyBebopMovie-OP1.webm -vf scale=1280:-4,format=yuv420p -ac 2 -c:a libopus -b:a 128k -c:v libx264 -preset slow -crf 24 -maxrate:v 5M -bufsize:v 10M -f hls -hls_segment_type fmp4 -hls_list_size 0 -hls_segment_filename v%05d.mp4 v.m3u8\n\nmore notes\n- adding -hls_flags single_file makes duration wack during playback (for both fmp4 and ts), ok once finalized and refreshed, gives no size reduction anyways\n- bebop op has good keyframe spacing for testing hls.js, in particular it hops one seg back and immediately resumes if it hits eof with the explicit hls.startLoad(0); otherwise it jumps into the middle of a seg and becomes art\n- can probably -c:v copy most of the time, is there a way to check for cgop? todo\n\n*/\n</script>\n</body></html>\n"
  },
  {
    "path": "docs/idp.md",
    "content": "there is a [docker-compose example](./examples/docker/idp-authelia-traefik) which is hopefully a good starting point (meaning you can skip the steps below) -- but if you want to set this up from scratch yourself (or learn about how it works), keep reading:\n\nto configure IdP from scratch, you must place copyparty behind a reverse-proxy which sends all requests through a middleware (the IdP / identity-provider service) which will inject a set of headers into the requests, telling copyparty who the user is\n\nin the copyparty `[global]` config, specify which headers to read client info from; username is required (`idp-h-usr: X-Authooley-User`), group(s) are optional (`idp-h-grp: X-Authooley-Groups`)\n\n* it is also required to specify the subnet that legit requests will be coming from, for example `--xff-src=10.88.0.0/24` to allow 10.88.x.x (or `--xff-src=lan` for all private IPs), and it is recommended to configure the reverseproxy to include a secret header as proof that the other headers are also legit (and not smuggled in by a malicious client), telling copyparty the headername to expect with `idp-h-key: shangala-bangala`\n\n\n# important notes\n\n## by default, IdP volumes are forgotten on shutdown\n\nIdP volumes, meaning dynamically-created volumes, meaning volumes that contain `${u}` or `${g}` in their URL, will (by default) be forgotten during a server restart and then \"revived\" when the volume's owner sends their first request after the restart\n\nuntil each IdP volume is revived, it will inherit the permissions of its parent volume (if any)\n\nthis means that, if an IdP volume is located inside a folder that is readable by anyone, then each of those IdP volumes will **also become readable by anyone** until the volume is revived\n\nand likewise -- if the IdP volume is inside a folder that is only accessible by certain users, but the IdP volume is configured to allow access from unauthenticated users, then the contents of the volume will NOT be accessible until it is revived\n\nit is recommended to place IdP volumes inside an appropriate parent volume, so they can inherit acceptable permissions until their revival; see the \"strategic volumes\" at the bottom of [./examples/docker/idp/copyparty.conf](./examples/docker/idp/copyparty.conf)\n\n## but you can enable IdP volume persistence\n\nglobal-option `idp-store` can enable user/group persistence across restarts;\n\n* `idp-store: 1` (default) will log users into a database, but not actually \"remember\" them (the knowledge is ignored)\n* `idp-store: 2` remembers usernames only\n* `idp-store: 3` remembers usernames and their groups\n\nthe reason why this is default-disabled, is because you may expect copyparty to forget about a user when you delete them from the IdP-server; this will not be the case any longer, you will need to click `view idp cache` in the controlpanel and manually remove the users you want gone\n\n\n## Connecting webdav clients\n\nIf you use only idp and want to connect via rclone you have to adapt a few things.\nThe following steps are for Authelia, but should be easy adaptable to other IdPs and clients. There may be better/smarter ways to do this, but this is a known solution.\n\n1. Add a rule for your domain and set it to one factor\n```\n  rules:\n    - domain: 'sub.domain.tld'\n      policy: one_factor\n```\n2. After you created your rclone config find its location with `rclone config file` and add the headers option to it, change the string to `username:password` base64 encoded. Make sure to set the right url location, otherwise you will get a 401 from copyparty.\n```\n[servername-dav]\ntype = webdav\nurl = https://sub.domain.tld/u/user/priv/\nvendor = owncloud\npacer_min_sleep = 0.01ms\nheaders = Proxy-Authorization,basic base64encodedstring==\n```"
  },
  {
    "path": "docs/lics.txt",
    "content": "--- server-side --- software ---\n\nhttps://github.com/9001/copyparty/\nC: 2019 ed\nL: MIT\n\nhttps://github.com/pallets/jinja/\nC: 2007 Pallets\nL: BSD-3-Clause\n\nhttps://github.com/pallets/markupsafe/\nC: 2010 Pallets\nL: BSD-3-Clause\n\nhttps://github.com/paulc/dnslib/\nC: 2010-2017 Paul Chakravarti\nL: BSD-2-Clause\n\nhttps://github.com/pydron/ifaddr/\nC: 2014 Stefan C. Mueller\nL: BSD-2-Clause\n\nhttps://github.com/giampaolo/pyftpdlib/\nC: 2007 Giampaolo Rodola\nL: MIT\n\nhttps://github.com/9001/partftpy\nC: 2010-2021 Michael P. Soulier\nL: MIT\n\nhttps://github.com/nayuki/QR-Code-generator/\nC: Project Nayuki\nL: MIT\n\nhttps://github.com/ahupp/python-magic/\nC: 2001-2014 Adam Hupp\nL: MIT\n\nhttps://github.com/fusepy/fusepy\nC: 2012 Giorgos Verigakis\nL: ISC\n\n--- client-side --- software ---\n\nhttps://github.com/Daninet/hash-wasm/\nC: 2020 Dani Biró\nL: MIT\n\nhttps://github.com/asmcrypto/asmcrypto.js/\nC: 2013 Artem S Vybornov\nL: MIT\n\nhttps://github.com/feimosi/baguetteBox.js/\nC: 2017 Marek Grzybek\nL: MIT\n\nhttps://github.com/cure53/DOMPurify/\nC: 2025 Cure53 / Mario Heiderich\nL: Apache-2.0 or MPL-2.0\n\nhttps://github.com/markedjs/marked/\nC: 2018+, MarkedJS\nC: 2011-2018, Christopher Jeffrey (https://github.com/chjj/)\nL: MIT\n\nhttps://github.com/codemirror/codemirror5/\nC: 2017 Marijn Haverbeke <marijnh@gmail.com> and others\nL: MIT\n\nhttps://github.com/Ionaru/easy-markdown-editor/\nC: 2015 Sparksuite, Inc.\nC: 2017 Jeroen Akkerman.\nL: MIT\n\n--- client-side --- fonts ---\n\nhttps://github.com/adobe-fonts/source-code-pro/\nC: 2010-2019 Adobe\nL: OFL-1.1\n\nhttps://github.com/FortAwesome/Font-Awesome/\nC: 2022 Fonticons, Inc.\nL: OFL-1.1\n"
  },
  {
    "path": "docs/multisearch.html",
    "content": "<!DOCTYPE html><html lang=\"en\"><head>\n\t<meta charset=\"utf-8\">\n\t<title>multisearch</title>\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <style>\n\nhtml, body {\n    margin: 0;\n    padding: 0;\n    color: #ddd;\n    background: #222;\n    font-family: sans-serif;\n}\nbody {\n    padding: 1em;\n}\na {\n    color: #fc5;\n}\nul {\n    line-height: 1.5em;\n}\ncode {\n    color: #fc5;\n    border: 1px solid #444;\n    padding: .1em .2em;\n    font-family: sans-serif, sans-serif;\n}\n#src {\n    display: block;\n    width: calc(100% - 1em);\n    padding: .5em;\n    margin: 0;\n}\ntd {\n    padding-left: 1em;\n}\n.hit,\n.miss {\n    font-weight: bold;\n    padding-left: 0;\n    padding-top: 1em;\n}\n.hit {color: #af0;}\n.miss {color: #f0c;}\n.hit:before {content: '✅';}\n.miss:before {content: '❌';}\n\n</style></head><body>\n    <ul>\n        <li>paste a list of filenames (youtube rips) below and hit search</li>\n        <li>it will grab the youtube-id from the filenames and search for each id</li>\n        <li>filenames must be like <code>-YTID.webm</code> (youtube-dl style) or <code>[YTID].webm</code> (ytdlp style)</li>\n    </ul>\n    <textarea id=\"src\"></textarea>\n    <button id=\"go\">search</button>\n    <div id=\"res\"></div>\n    <script>\n\nvar ebi = document.getElementById.bind(document);\nfunction esc(txt) {\n    return txt.replace(/[&\"<>]/g, function (c) {\n        return {\n            '&': '&amp;',\n            '\"': '&quot;',\n            '<': '&lt;',\n            '>': '&gt;'\n        }[c];\n    });\n}\n\nebi('go').onclick = async function() {\n    var queries = [];\n    for (var ln of ebi('src').value.split(/\\n/g)) {\n        // filter the list of input files,\n        // only keeping youtube videos,\n        // meaning the filename ends with either\n        //   [YOUTUBEID].EXTENSION or\n        //   -YOUTUBEID.EXTENSION\n        var m = /[[-]([0-9a-zA-Z_-]{11})\\]?\\.(mp4|webm|mkv)$/.exec(ln);\n        if (!m || !(m = m[1]))\n            continue;\n\n        // create a search query for each line: name like *youtubeid*\n        queries.push([ln, `name like *${m}*`]);\n    }\n\n    var a = 0, html = ['<table>'], hits = [], misses = [];\n    for (var [fn, q] of queries) {\n        var r = await fetch('/?srch', {\n            method: 'POST',\n            body: JSON.stringify({'q': q})\n        });\n        r = await r.json();\n        \n        var cl, tab2;\n        if (r.hits.length) {\n            tab2 = hits;\n            cl = 'hit';\n        }\n        else {\n            tab2 = misses;\n            cl = 'miss';\n        }\n        var h = `<tr><td class=\"${cl}\" colspan=\"9\">${esc(fn)}</td></tr>`;\n        tab2.push(h);\n        html.push(h);\n        for (var h of r.hits) {\n            var link = `<a href=\"/${h.rp}\">${esc(decodeURIComponent(h.rp))}</a>`;\n            html.push(`<tr><td>${h.sz}</td><td>${link}</td></tr>`);\n        }\n        ebi('res').innerHTML = `searching, ${++a} / ${queries.length} done, ${hits.length} hits, ${misses.length} miss`;\n    }\n    html.push('<tr><td><h1>hits:</h1></td></tr>');\n    html = html.concat(hits);\n\n    html.push('<tr><td><h1>miss:</h1></td></tr>');\n    html = html.concat(misses);\n\n    html.push('</table>');\n    ebi('res').innerHTML = html.join('\\n');\n};\n\n</script></body></html>\n"
  },
  {
    "path": "docs/music-analysis.sh",
    "content": "#!/bin/bash\necho please dont actually run this as a scriopt\nexit 1\n\n\n# dependency-heavy, not particularly good fit\npacman -S llvm10\npython3 -m pip install --user librosa\ngit clone https://github.com/librosa/librosa.git\n\n\n# correct bpm for tracks with bad tags\nbr='\n/Trip Trip Trip\\(Hardcore Edit\\).mp3/ {v=176}\n/World!!.BIG_SOS/ {v=175}\n/\\/08\\..*\\(BIG_SOS Bootleg\\)\\.mp3/ {v=175}\n/もってけ！セーラ服.Asterisk DnB/ {v=175}\n/Rondo\\(Asterisk DnB Re.mp3/ {v=175}\n/Ray Nautica 175 Edit/ {v=175;x=\"thunk\"}\n/TOKIMEKI Language.Jauz/ {v=174}\n/YUPPUN Hardcore Remix\\).mp3/ {v=174;x=\"keeps drifting\"}\n/(èâAâï.î╧ûδ|バーチャリアル.狐耶)J-Core Remix\\).mp3/ {v=172;x=\"hard\"}\n/lucky train..Freezer/ {v=170}\n/Alf zero Bootleg ReMix/ {v=170}\n/Prisoner of Love.Kacky/ {v=170}\n/火炎 .Qota/ {v=170}\n/\\(hu-zin Bootleg\\)\\.mp3/ {v=170}\n/15. STRAIGHT BET\\(Milynn Bootleg\\)\\.mp3/ {v=170}\n/\\/13.*\\(Milynn Bootleg\\)\\.mp3/ {v=167;x=\"way hard\"}\n/COLOR PLANET .10SAI . nijikon Remix\\)\\.mp3/ {v=165}\n/11\\. (朝はご飯派|Æ⌐é═é▓ö╤öh)\\.mp3/ {v=162}\n/09\\. Where.s the core/ {v=160}\n/PLANET\\(Koushif Jersey Club Bootleg\\)remaster.mp3/ {v=160;x=\"starts ez turns bs\"}\n/kened Soul - Madeon x Angel Beats!.mp3/ {v=160}\n/Dear Moments\\(Mother Harlot Bootleg\\)\\.mp3/ {v=150}\n/POWER.Ringos UKG/ {v=140}\n/ブルー・フィールド\\(Ringos UKG Remix\\).mp3/ {v=135}\n/プラチナジェット.Ringo Remix..mp3/ {v=131.2}\n/Mirrorball Love \\(TKM Bootleg Mix\\).mp3/ {v=130}\n/Photon Melodies \\(TKM Bootleg Mix\\).mp3/ {v=128}\n/Trap of Love \\(TKM Bootleg Mix\\).mp3/ {v=128}\n/One Step \\(TKM Bootleg Mix\\)\\.mp3/ {v=126}\n/04 (トリカムイ岩|âgâèâJâÇâCèΓ).mp3/ {v=125}\n/Get your Wish \\(NAWN REMIX\\)\\.mp3/ {v=95}\n/Flicker .Nitro Fun/ {v=92}\n/\\/14\\..*suicat Remix/ {v=85.5;x=\"tricky\"}\n/Yanagi Nagi - Harumodoki \\(EO Remix\\)\\.mp3/ {v=150}\n/Azure - Nicology\\.mp3/ {v=128;x=\"off by 5 how\"}\n'\n\n\n# afun host, collects/grades the results\nrunfun() { cores=8; touch run; rm -f /dev/shm/mres.*; t00=$(date +%s); tbc() { bc | sed -r 's/(\\.[0-9]{2}).*/\\1/'; }; for ((core=0; core<$cores; core++)); do sqlite3 /mnt/Users/ed/Music/.hist/up2k.db 'select dur.w, dur.v, bpm.v from mt bpm join mt dur on bpm.w = dur.w where bpm.k = \".bpm\" and dur.k = \".dur\" order by dur.w' | uniq -w16 | while IFS=\\| read w dur bpm; do sqlite3 /mnt/Users/ed/Music/.hist/up2k.db \"select rd, fn from up where substr(w,1,16) = '$w'\" | sed -r \"s/^/$bpm /\"; done | grep mir/cr | tr \\| / | awk '{v=$1;sub(/[^ ]+ /,\"\")} '\"$br\"' {printf \"%s %s\\n\",v,$0}' | while read bpm fn; do [ -e run ] || break; n=$((n+1)); ncore=$((n%cores)); [ $ncore -eq $core ] || continue; t0=$(date +%s.%N); (afun || exit 1; t=$(date +%s.%N); td=$(echo \"scale=3; $t - $t0\" | tbc); bd=$(echo \"scale=3; $bpm / $py\" | tbc); printf '%4s sec, %4s orig, %6s py, %4s div, %s\\n' $td $bpm $py $bd \"$fn\") | tee -a /dev/shm/mres.$ncore; rv=${PIPESTATUS[0]}; [ $rv -eq 0 ] || { echo \"FAULT($rv): $fn\"; }; done & done; wait 2>/dev/null; cat /dev/shm/mres.* | awk 'function prt(c) {printf \"\\033[3%sm%s\\033[0m\\n\",c,$0} $8!=\"div,\"{next} $5!~/^[0-9\\.]+/{next} {meta=$3;det=$5;div=meta/det} div<0.7{det/=2} div>1.3{det*=2} {idet=sprintf(\"%.0f\",det)} {idiff=idet-meta} meta>idet{idiff=meta-idet} idiff==0{n0++;prt(6);next} idiff==1{n1++;prt(3);next} idiff>10{nx++;prt(1);next} {n10++;prt(5)} END {printf \"ok: %d   1off: %2s   (%3s)   10off: %2s   (%3s)   fail: %2s\\n\",n0,n1,n0+n1,n10,n0+n1+n10,nx}'; te=$(date +%s); echo $((te-t00)) sec spent; }\n\n\n# ok:   8   1off: 62   ( 70)   10off: 86   (156)   fail: 25   # 105 sec,  librosa @ 8c archvm on 3700x w10\n# ok:   4   1off: 59   ( 63)   10off: 65   (128)   fail: 53   # using original tags (bad)\nafun() { ffmpeg -hide_banner -v fatal -nostdin -ss $((dur/3)) -y -i /mnt/Users/ed/Music/\"$fn\" -t 60 /dev/shm/$core.wav || return 1; py=\"$(/home/ed/src/librosa/examples/beat_tracker.py /dev/shm/$core.wav x 2>&1 | awk 'BEGIN {v=1} /^Estimated tempo: /{v=$3} END {print v}')\"; } runfun\n\n\n# ok: 119   1off:  5   (124)   10off:  8   (132)   fail: 49   # 51 sec,  vamp-example-fixedtempo\n# ok: 109   1off:  4   (113)   10off:  9   (122)   fail: 59   # bad-tags\nafun() { ffmpeg -hide_banner -v fatal -nostdin -ss $((dur/3)) -y -i /mnt/Users/ed/Music/\"$fn\" -ac 1 -ar 22050 -f f32le /dev/shm/$core.pcm || return 1; py=\"$(python3 -c 'import vamp; import numpy as np; f = open(\"/dev/shm/'$core'.pcm\", \"rb\"); d = np.fromfile(f, dtype=np.float32); c = vamp.collect(d, 22050, \"vamp-example-plugins:fixedtempo\", parameters={\"maxdflen\":40}); print(c[\"list\"][0][\"label\"].split(\" \")[0])')\"; }; runfun\n\n\n# ok: 102   1off: 61   (163)   10off: 12   (175)   fail:  6   # 61 sec,  vamp-qm-tempotracker\n# ok:  80   1off: 48   (128)   10off: 11   (139)   fail: 42   # bad-tags\nafun() { ffmpeg -hide_banner -v fatal -nostdin -ss $((dur/3)) -y -i /mnt/Users/ed/Music/\"$fn\" -ac 1 -ar 22050 -f f32le /dev/shm/$core.pcm || return 1; py=\"$(python3 -c 'import vamp; import numpy as np; f = open(\"/dev/shm/'$core'.pcm\", \"rb\"); d = np.fromfile(f, dtype=np.float32); c = vamp.collect(d, 22050, \"qm-vamp-plugins:qm-tempotracker\", parameters={\"inputtempo\":150}); v = [float(x[\"label\"].split(\" \")[0]) for x in c[\"list\"] if x[\"label\"]]; v = list(sorted(v))[len(v)//4:-len(v)//4]; print(round(sum(v) / len(v), 1))')\"; }; runfun\n\n\n# ok: 133   1off: 32   (165)   10off: 12   (177)   fail:  3   # 51 sec,  vamp-beatroot\n# ok: 101   1off: 22   (123)   10off: 16   (139)   fail: 39   # bad-tags\n# note: some tracks fully fail to analyze (unlike the others which always provide a guess)\nafun() { ffmpeg -hide_banner -v fatal -nostdin -ss $((dur/3)) -y -i /mnt/Users/ed/Music/\"$fn\" -ac 1 -ar 22050 -f f32le /dev/shm/$core.pcm || return 1; py=\"$(python3 -c 'import vamp; import numpy as np; f = open(\"/dev/shm/'$core'.pcm\", \"rb\"); d = np.fromfile(f, dtype=np.float32); c = vamp.collect(d, 22050, \"beatroot-vamp:beatroot\"); cl=c[\"list\"]; print(round(60*((len(cl)-1)/(float(cl[-1][\"timestamp\"]-cl[1][\"timestamp\"]))), 2))')\"; }; runfun\n\n\n# ok: 124   1off:  9   (133)   10off: 40   (173)   fail:  8   # 231 sec,  essentia/full\n# ok: 109   1off:  8   (117)   10off: 22   (139)   fail: 42   # bad-tags\nafun() { ffmpeg -hide_banner -v fatal -nostdin -ss $((dur/3)) -y -i /mnt/Users/ed/Music/\"$fn\" -ac 1 -ar 44100 /dev/shm/$core.wav || return 1; py=\"$(python3 -c 'import essentia; import essentia.standard as es; fe, fef = es.MusicExtractor(lowlevelStats=[\"mean\", \"stdev\"], rhythmStats=[\"mean\", \"stdev\"], tonalStats=[\"mean\", \"stdev\"])(\"/dev/shm/'$core'.wav\"); print(\"{:.2f}\".format(fe[\"rhythm.bpm\"]))')\"; }; runfun\n\n\n# ok: 113   1off: 18   (131)   10off: 46   (177)   fail:  4   # 134 sec,  essentia/re2013\n# ok: 101   1off: 15   (116)   10off: 26   (142)   fail: 39   # bad-tags\nafun() { ffmpeg -hide_banner -v fatal -nostdin -ss $((dur/3)) -y -i /mnt/Users/ed/Music/\"$fn\" -ac 1 -ar 44100 /dev/shm/$core.wav || return 1; py=\"$(python3 -c 'from essentia.standard import *; a=MonoLoader(filename=\"/dev/shm/'$core'.wav\")(); bpm,beats,confidence,_,intervals=RhythmExtractor2013(method=\"multifeature\")(a); print(\"{:.2f}\".format(bpm))')\"; }; runfun\n\n\n\n########################################################################\n##\n##  key detectyion\n##\n########################################################################\n\n\n\n# console scriptlet reusing keytabs from browser.js\nvar m=''; for (var a=0; a<24; a++) m += 's/\\\\|(' + maps[\"traktor_sharps\"][a].trim() + \"|\" + maps[\"rekobo_classic\"][a].trim() + \"|\" + maps[\"traktor_musical\"][a].trim() + \"|\" + maps[\"traktor_open\"][a].trim() + ')$/|' + maps[\"rekobo_alnum\"][a].trim() + '/;'; console.log(m);\n\n\n# translate to camelot\nre='s/\\|(B|B|B|6d)$/|1B/;s/\\|(F#|F#|Gb|7d)$/|2B/;s/\\|(C#|Db|Db|8d)$/|3B/;s/\\|(G#|Ab|Ab|9d)$/|4B/;s/\\|(D#|Eb|Eb|10d)$/|5B/;s/\\|(A#|Bb|Bb|11d)$/|6B/;s/\\|(F|F|F|12d)$/|7B/;s/\\|(C|C|C|1d)$/|8B/;s/\\|(G|G|G|2d)$/|9B/;s/\\|(D|D|D|3d)$/|10B/;s/\\|(A|A|A|4d)$/|11B/;s/\\|(E|E|E|5d)$/|12B/;s/\\|(G#m|Abm|Abm|6m)$/|1A/;s/\\|(D#m|Ebm|Ebm|7m)$/|2A/;s/\\|(A#m|Bbm|Bbm|8m)$/|3A/;s/\\|(Fm|Fm|Fm|9m)$/|4A/;s/\\|(Cm|Cm|Cm|10m)$/|5A/;s/\\|(Gm|Gm|Gm|11m)$/|6A/;s/\\|(Dm|Dm|Dm|12m)$/|7A/;s/\\|(Am|Am|Am|1m)$/|8A/;s/\\|(Em|Em|Em|2m)$/|9A/;s/\\|(Bm|Bm|Bm|3m)$/|10A/;s/\\|(F#m|F#m|Gbm|4m)$/|11A/;s/\\|(C#m|Dbm|Dbm|5m)$/|12A/;'\n\n\n# runner/wrapper\nrunfun() { cores=8; touch run; tbc() { bc | sed -r 's/(\\.[0-9]{2}).*/\\1/'; }; for ((core=0; core<$cores; core++)); do sqlite3 /mnt/Users/ed/Music/.hist/up2k.db 'select dur.w, dur.v, key.v from mt key join mt dur on key.w = dur.w where key.k = \"key\" and dur.k = \".dur\" order by dur.w' | uniq -w16 | grep -vE '(Off-Key|None)$' | sed -r \"s/ //g;$re\" | uniq -w16 | while IFS=\\| read w dur bpm; do sqlite3 /mnt/Users/ed/Music/.hist/up2k.db \"select rd, fn from up where substr(w,1,16) = '$w'\" | sed -r \"s/^/$bpm /\"; done| grep mir/cr | tr \\| / | while read key fn; do [ -e run ] || break; n=$((n+1)); ncore=$((n%cores)); [ $ncore -eq $core ] || continue; t0=$(date +%s.%N); (afun || exit 1; t=$(date +%s.%N); td=$(echo \"scale=3; $t - $t0\" | tbc); [ \"$key\" = \"$py\" ] && c=2 || c=5; printf '%4s sec, %4s orig, \\033[3%dm%4s py,\\033[0m %s\\n' $td \"$key\" $c \"$py\" \"$fn\") || break; done & done; time wait 2>/dev/null; }\n\n\n# ok: 26   1off: 10   2off: 1   fail: 3   #  15 sec, keyfinder\nafun() { ffmpeg -hide_banner -v fatal -nostdin -ss $((dur/3)) -y -i /mnt/Users/ed/Music/\"$fn\" -ac 1 -ar 44100 -t 60 /dev/shm/$core.wav || break; py=\"$(python3 -c 'import sys; import keyfinder; print(keyfinder.key(sys.argv[1]).camelot())' \"/dev/shm/$core.wav\")\"; }; runfun\n\n\n# https://github.com/MTG/essentia/raw/master/src/examples/tutorial/example_key_by_steps_streaming.py\n# https://essentia.upf.edu/reference/std_Key.html  # edma edmm braw bgate\nsed -ri 's/^(key = Key\\().*/\\1profileType=\"bgate\")/' example_key_by_steps_streaming.py\nafun() { ffmpeg -hide_banner -v fatal -nostdin -ss $((dur/3)) -y -i /mnt/Users/ed/Music/\"$fn\" -ac 1 -ar 44100 -t 60 /dev/shm/$core.wav || break; py=\"$(python3 example_key_by_steps_streaming.py /dev/shm/$core.{wav,yml} 2>/dev/null | sed -r \"s/ major//;s/ minor/m/;s/^/|/;$re;s/.//\")\"; }; runfun\n\n\n\n########################################################################\n##\n##  misc\n##\n########################################################################\n\n\n\npython3 -m pip install --user vamp\n\nimport librosa\nd, r = librosa.load('/dev/shm/0.wav')\nd.dtype\n# dtype('float32')\nd.shape\n# (1323000,)\nd\n# array([-1.9614939e-08,  1.8037968e-08, -1.4106059e-08, ...,\n#         1.2024145e-01,  2.7462116e-01,  1.6202132e-01], dtype=float32)\n\n\n\nimport vamp\nc = vamp.collect(d, r, \"vamp-example-plugins:fixedtempo\")\nc\n# {'list': [{'timestamp':  0.005804988, 'duration':  9.999092971, 'label': '110.0 bpm', 'values': array([109.98116], dtype=float32)}]}\n\n\n\nffmpeg -ss 48 -i /mnt/Users/ed/Music/mir/cr-a/'I Beg You(ths Bootleg).wav' -ac 1 -ar 22050 -f f32le -t 60 /dev/shm/f32.pcm\n\nimport numpy as np\nf = open('/dev/shm/f32.pcm', 'rb')\nd = np.fromfile(f, dtype=np.float32)\nd\narray([-0.17803933, -0.27206388, -0.41586545, ..., -0.04940119,\n       -0.0267825 , -0.03564296], dtype=float32)\n\nd = np.reshape(d, [1, -1])\nd\narray([[-0.17803933, -0.27206388, -0.41586545, ..., -0.04940119,\n        -0.0267825 , -0.03564296]], dtype=float32)\n\n\n\nimport vampyhost\nprint(\"\\n\".join(vampyhost.list_plugins()))\n\nmvamp:marsyas_bextract_centroid\nmvamp:marsyas_bextract_lpcc\nmvamp:marsyas_bextract_lsp\nmvamp:marsyas_bextract_mfcc\nmvamp:marsyas_bextract_rolloff\nmvamp:marsyas_bextract_scf\nmvamp:marsyas_bextract_sfm\nmvamp:marsyas_bextract_zero_crossings\nmvamp:marsyas_ibt\nmvamp:zerocrossing\nqm-vamp-plugins:qm-adaptivespectrogram\nqm-vamp-plugins:qm-barbeattracker\nqm-vamp-plugins:qm-chromagram\nqm-vamp-plugins:qm-constantq\nqm-vamp-plugins:qm-dwt\nqm-vamp-plugins:qm-keydetector\nqm-vamp-plugins:qm-mfcc\nqm-vamp-plugins:qm-onsetdetector\nqm-vamp-plugins:qm-segmenter\nqm-vamp-plugins:qm-similarity\nqm-vamp-plugins:qm-tempotracker\nqm-vamp-plugins:qm-tonalchange\nqm-vamp-plugins:qm-transcription\nvamp-aubio:aubiomelenergy\nvamp-aubio:aubiomfcc\nvamp-aubio:aubionotes\nvamp-aubio:aubioonset\nvamp-aubio:aubiopitch\nvamp-aubio:aubiosilence\nvamp-aubio:aubiospecdesc\nvamp-aubio:aubiotempo\nvamp-example-plugins:amplitudefollower\nvamp-example-plugins:fixedtempo\nvamp-example-plugins:percussiononsets\nvamp-example-plugins:powerspectrum\nvamp-example-plugins:spectralcentroid\nvamp-example-plugins:zerocrossing\nvamp-rubberband:rubberband\n\n\n\nplug = vampyhost.load_plugin(\"vamp-example-plugins:fixedtempo\", 22050, 0)\nplug.info\n{'apiVersion': 2, 'pluginVersion': 1, 'identifier': 'fixedtempo', 'name': 'Simple Fixed Tempo Estimator', 'description': 'Study a short section of audio and estimate its tempo, assuming the tempo is constant', 'maker': 'Vamp SDK Example Plugins', 'copyright': 'Code copyright 2008 Queen Mary, University of London.  Freely redistributable (BSD license)'}\nplug = vampyhost.load_plugin(\"qm-vamp-plugins:qm-tempotracker\", 22050, 0)\nfrom pprint import pprint; pprint(plug.parameters)\n\n\n\nfor c in plug.parameters: print(\"{} \\033[36m{}  [\\033[33m{}\\033[36m] = {}\\033[0m\".format(c[\"identifier\"], c[\"name\"], \"\\033[36m, \\033[33m\".join(c[\"valueNames\"]), c[\"valueNames\"][int(c[\"defaultValue\"])])) if \"valueNames\" in c else print(\"{} \\033[36m{}  [\\033[33m{}..{}\\033[36m] = {}\\033[0m\".format(c[\"identifier\"], c[\"name\"], c[\"minValue\"], c[\"maxValue\"], c[\"defaultValue\"]))\n\n\n\nbeatroot-vamp:beatroot\ncl=c[\"list\"]; 60*((len(cl)-1)/(float(cl[-1][\"timestamp\"]-cl[1][\"timestamp\"])))\n\n\n\nffmpeg -ss 48 -i /mnt/Users/ed/Music/mir/cr-a/'I Beg You(ths Bootleg).wav' -ac 1 -ar 22050 -f f32le -t 60 /dev/shm/f32.pcm\n# 128 bpm, key 5A Cm\n\nimport vamp\nimport numpy as np\nf = open('/dev/shm/f32.pcm', 'rb')\nd = np.fromfile(f, dtype=np.float32)\nc = vamp.collect(d, 22050, \"vamp-example-plugins:fixedtempo\", parameters={\"maxdflen\":40})\nc[\"list\"][0][\"label\"]\n# 127.6 bpm\n\nc = vamp.collect(d, 22050, \"qm-vamp-plugins:qm-tempotracker\", parameters={\"inputtempo\":150})\nprint(\"\\n\".join([v[\"label\"] for v in c[\"list\"] if v[\"label\"]]))\nv = [float(x[\"label\"].split(' ')[0]) for x in c[\"list\"] if x[\"label\"]]\nv = list(sorted(v))[len(v)//4:-len(v)//4]\nv = sum(v) / len(v)\n# 128.1 bpm\n\n"
  },
  {
    "path": "docs/notes.bat",
    "content": "rem appending a static ip to a dhcp nic on windows 10-1703 or later\nnetsh interface ipv4 show interface\nnetsh interface ipv4 set interface interface=\"Ethernet 2\" dhcpstaticipcoexistence=enabled\nnetsh interface ipv4 add address \"Ethernet 2\" 10.1.2.4 255.255.255.0\n"
  },
  {
    "path": "docs/notes.md",
    "content": "this file accidentally got committed at some point, so let's put it to use\n\n# trivia / lore\n\ncopyparty started as [three separate php projects](https://a.ocv.me/pub/stuff/old-php-projects/); an nginx custom directory listing (which became a php script), and a php music/picture viewer, and an additional php project for resumable uploads:\n\n* findex -- directory browser / gallery with thumbnails and a music player which sometime back in 2009 had a canvas visualizer grabbing fft data from a flash audio player\n* findex.mini -- plain-listing fork of findex with streaming zip-download of folders (the js and design should look familiar)\n* upper and up2k -- up2k being the star of the show and where copyparty's chunked resumable uploads came from\n\nthe first link has screenshots but if that doesn't work there's also a [tar here](https://ocv.me/dev/old-php-projects.tgz)\n\n----\n\nbelow this point is misc useless scribbles\n\n# up2k.js\n\n## potato detection\n\n* tsk 0.25/8.4/31.5 bzw 1.27/22.9/18 = 77% (38.4s, 49.7s)\n  * 4c locale #1313, ff-102,deb-11 @ ryzen4500u wifi -> win10\n  * profiling shows 2sec heavy gc every 2sec\n\n* tsk 0.41/4.1/10 bzw 1.41/9.9/7 = 73% (13.3s, 18.2s)\n  * 4c locale #1313, ch-103,deb-11 @ ryzen4500u wifi -> win10\n"
  },
  {
    "path": "docs/notes.sh",
    "content": "#!/bin/bash\necho not a script\nexit 1\n\n\n##\n## add index.html banners\n\nfind -name index.html | sed -r 's/index.html$//' | while IFS= read -r dir; do f=\"$dir/.prologue.html\"; [ -e \"$f\" ] || echo '<h1><a href=\"index.html\">open index.html</a></h1>' >\"$f\"; done\n\n\n##\n## delete all partial uploads\n##  (supports linux/macos, probably windows+msys2)\n\ngzip -d < .hist/up2k.snap | jq -r '.[].tnam' | while IFS= read -r f; do rm -f -- \"$f\"; done\ngzip -d < .hist/up2k.snap | jq -r '.[].name' | while IFS= read -r f; do wc -c -- \"$f\" | grep -qiE '^[^0-9a-z]*0' && rm -f -- \"$f\"; done\n\n\n##\n## detect partial uploads based on file contents\n##  (in case of context loss or old copyparties)\n\necho; find -type f | while IFS= read -r x; do printf '\\033[A\\033[36m%s\\033[K\\033[0m\\n' \"$x\"; tail -c$((1024*1024)) <\"$x\" | xxd -a | awk 'NR==1&&/^[0: ]+.{16}$/{next} NR==2&&/^\\*$/{next} NR==3&&/^[0f]+: [0 ]+65 +.{16}$/{next} {e=1} END {exit e}' || continue; printf '\\033[A\\033[31msus:\\033[33m %s \\033[0m\\n\\n' \"$x\"; done\n\n\n##\n## sync pics/vids from phone\n##  (takes all files named (IMG|PXL|PANORAMA|Screenshot)_20231224_*)\n\ncd /storage/emulated/0/DCIM/Camera\nfind -mindepth 1 -maxdepth 1 | sort | cut -c3- > ls\nurl=https://192.168.1.3:3923/rw/pics/Camera/$d/; awk -F_ '!/^[A-Z][A-Za-z]{1,16}_[0-9]{8}[_-]/{next} {d=substr($2,1,6)} !t[d]++{print d}' ls | while read d; do grep -E \"^[A-Z][A-Za-z]{1,16}_$d\" ls | tr '\\n' '\\0' | xargs -0 python3 ~/dev/copyparty/bin/u2c.py -td $url --; done\n\n\n##\n## convert symlinks to hardlinks (probably safe, no guarantees)\n\nfind -type l | while IFS= read -r lnk; do [ -h \"$lnk\" ] || { printf 'nonlink: %s\\n' \"$lnk\"; continue; }; dst=\"$(readlink -f -- \"$lnk\")\"; [ -e \"$dst\" ] || { printf '???\\n%s\\n%s\\n' \"$lnk\" \"$dst\"; continue; }; printf 'relinking:\\n  %s\\n  %s\\n' \"$lnk\" \"$dst\"; rm -- \"$lnk\"; ln -- \"$dst\" \"$lnk\"; done \n\n\n##\n## convert hardlinks to symlinks (maybe not as safe? use with caution)\n\ne=; p=; find -printf '%i %p\\n' | awk '{i=$1;sub(/[^ ]+ /,\"\")} !n[i]++{p[i]=$0;next} {printf \"real %s\\nlink %s\\n\",p[i],$0}' | while read cls p; do [ -e \"$p\" ] || e=1; p=\"$(realpath -- \"$p\")\" || e=1; [ -e \"$p\" ] || e=1; [ $cls = real ] && { real=\"$p\"; continue; }; [ $cls = link ] || e=1; [ \"$p\" ] || e=1; [ $e ] && { echo \"ERROR $p\"; break; }; printf '\\033[36m%s \\033[0m -> \\033[35m%s\\033[0m\\n' \"$p\" \"$real\"; rm \"$p\"; ln -s \"$real\" \"$p\" || { echo LINK FAILED; break; }; done\n\n\n##\n## create a test payload\n\nhead -c $((2*1024*1024*1024)) /dev/zero | openssl enc -aes-256-ctr -pass pass:hunter2 -nosalt > garbage.file\n\n\n##\n## testing multiple parallel uploads\n## usage:  para | tee log\n\npara() { for s in 1 2 3 4 5 6 7 8 12 16 24 32 48 64; do echo $s; for r in {1..4}; do for ((n=0;n<s;n++)); do curl -sF \"act=bput\" -F \"f=@garbage.file\" http://127.0.0.1:3923/ 2>&1 & done; wait; echo; done; done; }\n\n\n##\n## display average speed\n## usage: avg logfile\n\navg() { awk 'function pr(ncsz) {if (nsmp>0) {printf \"%3s %s\\n\", csz, sum/nsmp} csz=$1;sum=0;nsmp=0} {sub(/\\r$/,\"\")} /^[0-9]+$/ {pr($1);next} / MiB/ {sub(/ MiB.*/,\"\");sub(/.* /,\"\");sum+=$1;nsmp++} END {pr(0)}' \"$1\"; }\n\n\n##\n## time between first and last upload\n\npython3 -um copyparty -nw -v srv::rw -i 127.0.0.1 2>&1 | tee log \ncat log | awk '!/\"purl\"/{next} {s=$1;sub(/[^m]+m/,\"\");gsub(/:/,\" \");t=60*(60*$1+$2)+$3} t<p{t+=86400} !a{a=t;sa=s} {b=t;sb=s} END {print b-a,sa,sb}'\n\n# or if the client you're measuring dies for ~15sec every once ina while and you wanna filter those out,\ncat log | awk '!/\"purl\"/{next} {s=$1;sub(/[^m]+m/,\"\");gsub(/:/,\" \");t=60*(60*$1+$2)+$3} t<p{t+=86400} !p{a=t;p=t;r=0;next} t-p>1{printf \"%.3f += %.3f - %.3f  (%.3f)  # %.3f -> %.3f\\n\",r,p,a,p-a,p,t;r+=p-a;a=t} {p=t} END {print r+p-a}'\n\n\n##\n## find uploads blocked by slow i/o or maybe deadlocks\nawk '/^.\\+. opened logfile/{print;next} {sub(/.$/,\"\")} !/^..36m[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3} /{next} !/0m(POST|writing) /{next} {c=0;p=$3} /0mPOST/{c=1} {s=$1;sub(/[^m]+m/,\"\");gsub(/:/,\" \");s=60*(60*$1+$2)+$3} c{t[p]=s;next} {d=s-t[p]} d>10{print $0 \"  # \" d}'\n\n\n##\n## bad filenames\n\ndirs=(\"./ほげ\" \"./ほげ/ぴよ\" \"./$(printf \\\\xed\\\\x91)\" \"./$(printf \\\\xed\\\\x91/\\\\xed\\\\x92)\" './qw,er;ty%20as df?gh+jkl%zxc&vbn <qwe>\"rty'\"'\"'uio&asd&nbsp;fgh')\nmkdir -p \"${dirs[@]}\"\nfor dir in \"${dirs[@]}\"; do for fn in ふが \"$(printf \\\\xed\\\\x93)\" 'qw,er;ty%20as df?gh+jkl%zxc&vbn <qwe>\"rty'\"'\"'uio&asd&nbsp;fgh'; do echo \"$dir\" > \"$dir/$fn.html\"; done; done\n# qw er+ty%20ui%%20op<as>df&gh&amp;jk#zx'cv\"bn`m=qw*er^ty?ui@op,as.df-gh_jk\n\n\n##\n## upload mojibake\n\nfn=$(printf '\\xba\\xdc\\xab.cab')\necho asdf > \"$fn\"\ncurl --cookie cppwd=wark -sF \"act=bput\" -F \"f=@$fn\" http://127.0.0.1:3923/moji/%ED%91/\n\n\n##\n## test compression\n\nwget -S --header='Accept-Encoding: gzip' -U 'MSIE 6.0; SV1' http://127.0.0.1:3923/.cpr/deps/ogv.js -O- | md5sum; p=~ed/dev/copyparty/copyparty/web/deps/ogv.js.gz; md5sum $p; gzip -d < $p | md5sum\n\n\n##\n## sha512(file) | base64\n## usage:  shab64 chunksize_mb filepath\n\nshab64() { sp=$1; f=\"$2\"; v=0; sz=$(stat -c%s \"$f\"); while true; do w=$((v+sp*1024*1024)); printf $(tail -c +$((v+1)) \"$f\" | head -c $((w-v)) | sha512sum | cut -c-64 | sed -r 's/ .*//;s/(..)/\\\\x\\1/g') | base64 -w0 | cut -c-43 | tr '+/' '-_'; v=$w; [ $v -lt $sz ] || break; done; }\n\n\n##\n## poll url for performance issues\n\ncommand -v gdate && date() { gdate \"$@\"; }; while true; do t=$(date +%s.%N); (time wget http://127.0.0.1:3923/?ls -qO- | jq -C '.files[]|{sz:.sz,ta:.tags.artist,tb:.tags.\".bpm\"}|del(.[]|select(.==null))' | awk -F\\\" '/\"/{t[$2]++} END {for (k in t){v=t[k];p=sprintf(\"%\" (v+1) \"s\",v);gsub(/ /,\"#\",p);printf \"\\033[36m%s\\033[33m%s   \",k,p}}') 2>&1 | awk -v ts=$t 'NR==1{t1=$0} NR==2{sub(/.*0m/,\"\");sub(/s$/,\"\");t2=$0;c=2; if(t2>0.3){c=3} if(t2>0.8){c=1} } END{sub(/[0-9]{6}$/,\"\",ts);printf \"%s   \\033[3%dm%s   %s\\033[0m\\n\",ts,c,t2,t1}'; sleep 0.1 || break; done\n\n\n##\n## track an up2k upload and print all chunks in file-order\n\ngrep '\"name\": \"2021-07-18 02-17-59.mkv\"' fug.log | head -n 1 | sed -r 's/.*\"hash\": \\[//; s/\\].*//' | tr '\"' '\\n' | grep -E '^[a-zA-Z0-9_-]{44}$' | while IFS= read -r cid; do cat -n fug.log | grep -vF '\"purl\": \"' | grep -- \"$cid\"; echo; done | stdbuf -oL tr '\\t' ' ' | while IFS=' ' read -r ln _ _ _ _ _ ts ip port msg; do [ -z \"$msg\" ] && echo && continue; printf '%6s [%s] [%s] %s\\n' $ln \"$ts\" \"$ip $port\" \"$msg\"; read -r ln _ _ _ _ _ ts ip port msg < <(cat -n fug.log | tail -n +$((ln+1)) | grep -F \"$ip $port\" | head -n 1); printf '%6s [%s] [%s] %s\\n' $ln \"$ts\" \"$ip $port\" \"$msg\"; done\n\n\n##\n## js oneliners\n\n# get all up2k search result URLs\nvar t=[]; var b=document.location.href.split('#')[0].slice(0, -1); document.querySelectorAll('#u2tab .prog a').forEach((x) => {t.push(b+encodeURI(x.getAttribute(\"href\")))}); console.log(t.join(\"\\n\"));\n\n# debug md-editor line tracking\nvar s=mknod('style');s.innerHTML='*[data-ln]:before {content:attr(data-ln)!important;color:#f0c;background:#000;position:absolute;left:-1.5em;font-size:1rem}';document.head.appendChild(s);\n\n\n##\n## bash oneliners\n\n# get the size and video-id of all youtube vids in folder, assuming filename ends with -id.ext, and create a copyparty search query\nfind -maxdepth 1 -printf '%s %p\\n' | sort -n | awk '!/-([0-9a-zA-Z_-]{11})\\.(mkv|mp4|webm)$/{next} {sub(/\\.[^\\.]+$/,\"\");n=length($0);v=substr($0,n-10);print $1, v}' | tee /dev/stderr | awk 'BEGIN {p=\"(\"} {printf(\"%s name like *-%s.* \",p,$2);p=\"or\"} END {print \")\\n\"}' | cat >&2\n\n# unique stacks in a stackdump\nf=a; rm -rf stacks; mkdir stacks; grep -E '^#' $f | while IFS= read -r n; do awk -v n=\"$n\" '!$0{o=0} o; $0==n{o=1}' <$f >stacks/f; h=$(sha1sum <stacks/f | cut -c-16); mv stacks/f stacks/$h-\"$n\"; done ; find stacks/ | sort | uniq -cw24\n\n# find unused css variables\ncat browser.css | sed -r 's/(var\\()/\\n\\1/g' | awk '{sub(/:/,\" \")} $1~/^--/{d[$1]=1} /var\\(/{sub(/.*var\\(/,\"\");sub(/\\).*/,\"\");u[$1]=1} END{for (x in u) delete d[x]; for (x in d) print x}' | tr '\\n' '|'\n\n\n##\n## sqlite3 stuff\n\n# find dupe metadata keys\nsqlite3 up2k.db 'select mt1.w, mt1.k, mt1.v, mt2.v from mt mt1 inner join mt mt2 on mt1.w = mt2.w where mt1.k = mt2.k and mt1.rowid != mt2.rowid'\n\n# partial reindex by deleting all tags for a list of files\ntime sqlite3 up2k.db 'select mt1.w from mt mt1 inner join mt mt2 on mt1.w = mt2.w where mt1.k = +mt2.k and mt1.rowid != mt2.rowid'  > warks\ncat warks | while IFS= read -r x; do sqlite3 up2k.db \"delete from mt where w = '$x'\"; done\n\n# dump all dbs\nfind -iname up2k.db | while IFS= read -r x; do sqlite3 \"$x\" 'select substr(w,1,12), rd, fn from up' | sed -r 's/\\|/ \\| /g' | while IFS= read -r y; do printf '%s | %s\\n' \"$x\" \"$y\"; done; done\n\n# unschedule mtp scan for all files somewhere under \"enc/\"\nsqlite3 -readonly up2k.db 'select substr(up.w,1,16) from up inner join mt on mt.w = substr(up.w,1,16) where rd like \"enc/%\" and +mt.k = \"t:mtp\"' > keys; awk '{printf \"delete from mt where w = \\\"%s\\\" and +k = \\\"t:mtp\\\";\\n\", $0}' <keys | tee /dev/stderr | sqlite3 up2k.db\n\n# compare metadata key \"key\" between two databases\nsqlite3 -readonly up2k.db.key-full 'select w, v from mt where k = \"key\" order by w' > k1; sqlite3 -readonly up2k.db 'select w, v from mt where k = \"key\" order by w' > k2; ok=0; ng=0; while IFS='|' read w k2; do k1=\"$(grep -E \"^$w\" k1 | sed -r 's/.*\\|//')\"; [ \"$k1\" = \"$k2\" ] && ok=$((ok+1)) || { ng=$((ng+1)); printf '%3s %3s  %s\\n' \"$k1\" \"$k2\" \"$(sqlite3 -readonly up2k.db.key-full \"select * from up where substr(w,1,16) = '$w'\" | sed -r 's/\\|/ | /g')\"; }; done < <(cat k2); echo \"match $ok   diff $ng\"\n\n# actually this is much better\nsqlite3 -readonly up2k.db.key-full 'select w, v from mt where k = \"key\" order by w' > k1; sqlite3 -readonly up2k.db 'select mt.w, mt.v, up.rd, up.fn from mt inner join up on mt.w = substr(up.w,1,16) where mt.k = \"key\" order by up.rd, up.fn' > k2; ok=0; ng=0; while IFS='|' read w k2 path; do k1=\"$(grep -E \"^$w\" k1 | sed -r 's/.*\\|//')\"; [ \"$k1\" = \"$k2\" ] && ok=$((ok+1)) || { ng=$((ng+1)); printf '%3s %3s  %s\\n' \"$k1\" \"$k2\" \"$path\"; }; done < <(cat k2); echo \"match $ok   diff $ng\"\n\n\n##\n## scanning for exceptions\n\ncd /dev/shm\njournalctl -aS '720 hour ago' -t python3 -o with-unit --utc | cut -d\\  -f2,6- > cpp.log\ntac cpp.log | awk '/RuntimeError: generator ignored GeneratorExit/{n=1} n{n--;if(n==0)print} 1' | grep 'generator ignored GeneratorExit' -C7 | head -n 100\nawk '/Exception ignored in: <generator object StreamZip.gen/{s=1;next} /could not create thumbnail/{s=3;next} s{s--;next} 1' <cpp.log | less -R\nless-search:\n  >: |Exception|Traceback\n\n\n##\n## tracking bitflips\n\nl=log.tmux-1662316902  # your logfile (tmux-capture or decompressed -lo)\n\n# grab handshakes to a smaller logfile\ntr -d '\\r' <$l | awk '/^.\\[36m....-..-...\\[0m.?$/{d=substr($0,6,10)} !d{next} /\"purl\": \"/{t=substr($1,6);sub(/[^ ]+ /,\"\");sub(/ .\\[34m[0-9]+ /,\" \");printf(\"%s %s %s %s\\n\",d,t,ip,$0)}' | while read d t ip f; do u=$(date +%s --date=\"${d}T${t}Z\"); printf '%s\\n' \"$u $ip $f\"; done > handshakes\n\n# quick list of affected files\ngrep 'your chunk got corrupted somehow' -A1 $l | tr -d '\\r' | grep -E '^[a-zA-Z0-9_-]{44}$' | sort | uniq | while IFS= read -r x; do grep -F \"$x\" handshakes | head -c 200; echo; done | sed -r 's/.*\"name\": \"//' | sort | uniq -cw20\n\n# find all cases of corrupt chunks and print their respective handshakes (if any),\n# timestamps are when the corrupted chunk was received (and also the order they are displayed),\n# first checksum is the expected value from the handshake, second is what got uploaded\nawk <$l '/^.\\[36m....-..-...\\[0m.?$/{d=substr($0,6,10)} /your chunk got corrupted somehow/{n=2;t=substr($1,6);next} !n{next} {n--;sub(/\\r$/,\"\")} n{a=$0;next} {sub(/.\\[0m,.*/,\"\");printf \"%s %s %s %s\\n\",d,t,a,$0}' |\nwhile read d t h1 h2; do printf '%s %s\\n' $d $t; (\nprintf '  %s [%s]\\n' $h1 \"$(grep -F $h1 <handshakes | head -n 1)\"\nprintf '  %s [%s]\\n' $h2 \"$(grep -F $h2 <handshakes | head -n 1)\"\n) | sed 's/, \"sprs\":.*//'; done | less -R\n\n# notes; TODO clean up and put in the readme maybe --\n# quickest way to drop the bad files (if a client generated bad hashes for the initial handshake) is shutting down copyparty and moving aside the unfinished file (both the .PARTIAL and the empty placeholder)\n# BUT the clients will immediately re-handshake the upload with the same bitflipped hashes, so the uploaders have to refresh their browsers before you do that,\n# so maybe just ask them to refresh and do nothing for 6 hours so the timeout kicks in, which deletes the placeholders/name-reservations and you can then manually delete the .PARTIALs at some point later\n\n\n##\n## media\n\n# split track into test files\ne=6; s=10; d=~/dev/copyparty/srv/aus; n=1; p=0; e=$((e*60)); rm -rf $d; mkdir $d; while true; do ffmpeg -hide_banner -ss $p -i 'nervous_testpilot - office.mp3' -c copy -t $s $d/$(printf %04d $n).mp3; n=$((n+1)); p=$((p+s)); [ $p -gt $e ] && break; done\n\n-v srv/aus:aus:r:ce2dsa:ce2ts:cmtp=fgsfds=bin/mtag/sleep.py\nsqlite3 .hist/up2k.db 'select * from mt where k=\"fgsfds\" or k=\"t:mtp\"' | tee /dev/stderr | wc -l\n\n# generate the sine meme\nfor ((f=420;f<1200;f++)); do sz=$(ffmpeg -y -f lavfi -i sine=frequency=$f:duration=2 -vf volume=0.1 -ac 1 -ar 44100 -f s16le /dev/shm/a.wav 2>/dev/null; base64 -w0 </dev/shm/a.wav | gzip -c | wc -c); printf '%d %d\\n' $f $sz; done | tee /dev/stderr | sort -nrk2,2\nffmpeg -y -f lavfi -i sine=frequency=1050:duration=2 -vf volume=0.1 -ac 1 -ar 44100 /dev/shm/a.wav\n\n# better sine\nsox -DnV -r8000 -b8 -c1 /dev/shm/a.wav synth 1.1 sin 400 vol 0.02\n\n# play icon calibration pics\nfor w in 150 170 190 210 230 250; do for h in 130 150 170 190 210; do /c/Program\\ Files/ImageMagick-7.0.11-Q16-HDRI/magick.exe convert -size ${w}x${h} xc:brown -fill orange -draw \"circle $((w/2)),$((h/2)) $((w/2)),$((h/3))\" $w-$h.png; done; done\n\n# compress chiptune modules\nmkdir gz; for f in *.*; do pigz -c11 -I100 <\"$f\" >gz/\"$f\"gz; touch -r \"$f\" gz/\"$f\"gz; done\nmkdir xz; for f in *.*; do xz -cz9 <\"$f\" >xz/\"$f\"xz; touch -r \"$f\" xz/\"$f\"xz; done\nmkdir z; for f in *.*; do 7z a -tzip -mx=9 -mm=lzma \"z/${f}z\" \"$f\" && touch -r \"$f\" z/\"$f\"z; done \n\n\n##\n## vscode\n\n# replace variable name\n# (^|[^\\w])oldname([^\\w]|$) => $1newname$2\n\n# monitor linter progress\nhtop -d 2 -p $(ps ax | awk '/electron[ ]/ {printf \"%s%s\", v, $1;v=\",\"}')\n\n# prep debug env (vscode embedded terminal)\nrenice 20 -p $$\n\n# cleanup after a busted shutdown\nps ax | awk '/python[23]? -m copyparty|python[ ]-c from multiproc/ {print $1}' | tee /dev/stderr | xargs kill\n\n# last line of each function in a file\ncat copyparty/httpcli.py | awk '/^[^a-zA-Z0-9]+def / {printf \"%s\\n%s\\n\\n\", f, pl; f=$2} /[a-zA-Z0-9]/ {pl=$0}'\n\n\n##\n## meta\n\n# create a folder with symlinks to big files\nfor d in /usr /var; do find $d -type f -size +30M 2>/dev/null; done | while IFS= read -r x; do ln -s \"$x\" big/; done\n\n# up2k worst-case testfiles: create 64 GiB (256 x 256 MiB) of sparse files; each file takes 1 MiB disk space; each 1 MiB chunk is globally unique\nfor f in {0..255}; do echo $f; truncate -s 256M $f; b1=$(printf '%02x' $f); for o in {0..255}; do b2=$(printf '%02x' $o); printf \"\\x$b1\\x$b2\" | dd of=$f bs=2 seek=$((o*1024*1024)) conv=notrunc 2>/dev/null; done; done\n# create 6.06G file with 16 bytes of unique data at start+end of each 32M chunk\nsz=6509559808; truncate -s $sz f; csz=33554432; sz=$((sz/16)); step=$((csz/16)); ofs=0; while [ $ofs -lt $sz ]; do dd if=/dev/urandom of=f bs=16 count=2 seek=$ofs conv=notrunc iflag=fullblock; [ $ofs = 0 ] && ofs=$((ofs+step-1)) || ofs=$((ofs+step)); done\n# same but for chunksizes 16M (3.1G), 24M (4.1G), 48M (128.1G)\nsz=3321225472;   csz=16777216;\nsz=4394967296;   csz=25165824;\nsz=6509559808;   csz=33554432;\nsz=138438953472; csz=50331648;\nf=csz-$csz; truncate -s $sz $f; sz=$((sz/16)); step=$((csz/16)); ofs=0; while [ $ofs -lt $sz ]; do dd if=/dev/urandom of=$f bs=16 count=2 seek=$ofs conv=notrunc iflag=fullblock; [ $ofs = 0 ] && ofs=$((ofs+step-1)) || ofs=$((ofs+step)); done\n\n# py2 on osx\nbrew install python@2\npip install virtualenv\n\n# fix firefox phantom breakpoints,\n# suggestions from bugtracker, doesnt work (debugger is not attachable)\ndevtools settings >> advanced >> enable browser chrome debugging + enable remote debugging\nburger > developer >> browser toolbox  (ctrl-alt-shift-i)\niframe btn topright >> chrome://devtools/content/debugger/index.html\ndbg.asyncStore.pendingBreakpoints = {}\n\n# fix firefox phantom breakpoints\nabout:config >> devtools.debugger.prefs-schema-version = -1\n\n# determine server version\ngit pull; git reset --hard origin/HEAD && git log --format=format:\"%H %ai %d\" --decorate=full > ../revs && cat ../{util,browser,up2k}.js >../vr && cat ../revs | while read -r rev extra; do (git reset --hard $rev >/dev/null 2>/dev/null && dsz=$(cat copyparty/web/{util,browser,up2k}.js >../vg 2>/dev/null && diff -wNarU0 ../{vg,vr} | wc -c) && printf '%s %6s %s\\n' \"$rev\" $dsz \"$extra\") </dev/null; done                \n\n# download all sfx versions\ncurl https://api.github.com/repos/9001/copyparty/releases?per_page=100 | jq -r '.[] | .tag_name + \" \" + .name' | tr -d '\\r' | while read v t; do fn=\"$(printf '%s\\n' \"copyparty $v $t.py\" | tr / -)\"; [ -e \"$fn\" ] || curl https://github.com/9001/copyparty/releases/download/$v/copyparty-sfx.py -Lo \"$fn\"; done\n\n# convert releasenotes to changelog\ncurl https://api.github.com/repos/9001/copyparty/releases?per_page=100 | jq -r '.[] | \"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀  \\n# \\(.created_at)  `\\(.tag_name)`  \\(.name)\\n\\n\\(.body)\\n\\n\\n\"' | sed -r 's/^# ([0-9]{4}-)([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})Z /# \\1\\2\\3-\\4\\5 /' > changelog.md\n\n# push to multiple git remotes\ngit config -l | grep '^remote'\ngit remote add all git@github.com:9001/copyparty.git\ngit remote set-url --add --push all git@gitlab.com:9001/copyparty.git\ngit remote set-url --add --push all git@github.com:9001/copyparty.git\n\n\n##\n## http 206\n\n# az = abcdefghijklmnopqrstuvwxyz\n\nprintf '%s\\r\\n' 'GET /az HTTP/1.1' 'Host: ocv.me' 'Range: bytes=5-10' '' | ncat ocv.me 80 \n# Content-Range: bytes 5-10/26\n# Content-Length: 6\n# fghijk\n\nRange: bytes=0-1    \"ab\" Content-Range: bytes 0-1/26\nRange: bytes=24-24  \"y\"  Content-Range: bytes 24-24/26\nRange: bytes=24-25  \"yz\" Content-Range: bytes 24-25/26\nRange: bytes=24-    \"yz\" Content-Range: bytes 24-25/26\nRange: bytes=25-29  \"z\"  Content-Range: bytes 25-25/26\nRange: bytes=26-         Content-Range: bytes */26\n  HTTP/1.1 416 Requested Range Not Satisfiable\n\n\n##\n## md perf\n\nvar tsh = [];\nfunction convert_markdown(md_text, dest_dom) {\n    tsh.push(Date.now());\n    while (tsh.length > 10)\n        tsh.shift();\n    if (tsh.length > 1) {\n        var end = tsh.slice(-2);\n        console.log(\"render\", end.pop() - end.pop(), (tsh[tsh.length - 1] - tsh[0]) / (tsh.length - 1));\n    }\n\n\n##\n## tmpfiles.d meme\n\nmk() { rm -rf /tmp/foo; sudo -u ed bash -c 'mkdir /tmp/foo; echo hi > /tmp/foo/bar'; }\nmk && t0=\"$(date)\" && while true; do date -s \"$(date '+ 1 hour')\"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo \"$t0\"\nmk && sudo -u ed flock /tmp/foo sleep 40 & sleep 1; ps aux | grep -E 'sleep 40$' && t0=\"$(date)\" && for n in {1..40}; do date -s \"$(date '+ 1 day')\"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo \"$t0\"\nmk && t0=\"$(date)\" && for n in {1..40}; do date -s \"$(date '+ 1 day')\"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; tar -cf/dev/null /tmp/foo; done; echo \"$t0\"\n\n# number of megabytes downloaded since some date\nawk </var/log/wjaycore.out '/^..36m2025-05-20/{o=1} !o{next} !/ plain 20[06](,| \\[[^,]+\\],) +[0-9.]+.\\[33m[KM] .* n[0-9]+$/{next} {v=$0;sub(/.* plain 20[06](,| \\[[^,]+\\],) +/,\"\",v);sub(/ .*/,\"\",v);u=v;sub(/.\\[.*/,\"\",v);sub(/.*m/,\"\",u);$0=u} /[KMG]/{v*=1024} /[MG]/{v*=1024} /G/{v*=1024} {t+=v} END{printf \"%d\\n\",t/(1024*1024)}'\n\n# find format/% mixups\n%[sdfr].*format| % [^,]+\\)|\\{!?r?\\}.*%[sdfr]|%[sdfr].*\\{!?r?\\}|runhook\n"
  },
  {
    "path": "docs/nuitka.txt",
    "content": "﻿# recipe for building an exe with nuitka (extreme jank edition)\n\nNOTE: copyparty runs SLOWER when compiled with nuitka;\n  just use copyparty-sfx.py and/or pyinstaller instead\n\n  ( the sfx and the pyinstaller EXEs are equally fast if you\n  have the latest jinja2 installed, but the older jinja that\n  comes bundled with the sfx is slightly faster yet )\n\n  roughly, copyparty-sfx.py is 6% faster than copyparty.exe\n  (win10-pyinstaller), and copyparty.exe is 10% faster than\n  nuitka, making copyparty-sfx.py 17% faster than nuitka\n\nNOTE: every time a nuitka-compiled copyparty.exe is launched,\n  it will show the windows firewall prompt since nuitka will\n  pick a new unique location in %TEMP% to unpack an exe into,\n  unlike pyinstaller which doesn't fork itself on startup...\n  might be fixable by configuring nuitka differently, idk\n\nNOTE: nuitka EXEs are larger than pyinstaller ones;\n  a minimal nuitka build of just the sfx (with its bundled\n  dependencies) was already the same size as the pyinstaller\n  copyparty.exe which also includes Mutagen and Pillow\n\nNOTE: nuitka takes a lot longer to build than pyinstaller\n  (due to actual compilation of course, but still)\n\nNOTE: binaries built with nuitka cannot run on windows7,\n  even when compiled with python 3.6 on windows 7 itself\n\nNOTE: `--python-flags=-m` is the magic sauce to\n  correctly compile `from .util import Daemon`\n  (which otherwise only explodes at runtime)\n\nNOTE: `--deployment` doesn't seem to affect performance\n\n########################################################################\n# copypaste the rest of this file into cmd\n\n\n\npython -m pip install --user -U nuitka\n\ncd %homedrive%\ncd %homepath%\\downloads\n\nrd /s /q copypuitka\nmkdir copypuitka\ncd copypuitka\n\nrd /s /q %temp%\\pe-copyparty\npython ..\\copyparty-sfx.py --version\n\nmove %temp%\\pe-copyparty\\copyparty .\\\nmove %temp%\\pe-copyparty\\partftpy .\\\nmove %temp%\\pe-copyparty\\ftp\\pyftpdlib .\\\nmove %temp%\\pe-copyparty\\j2\\jinja2 .\\\nmove %temp%\\pe-copyparty\\j2\\markupsafe .\\\n\nrd /s /q %temp%\\pe-copyparty\n\npython -m nuitka ^\n  --onefile --deployment --python-flag=-m ^\n  --include-package=markupsafe ^\n  --include-package=jinja2 ^\n  --include-package=partftpy ^\n  --include-package=pyftpdlib ^\n  --include-data-dir=copyparty\\web=copyparty\\web ^\n  --include-data-dir=copyparty\\res=copyparty\\res ^\n  --run copyparty\n\n"
  },
  {
    "path": "docs/pretend-youre-qnap.patch",
    "content": "diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py\nindex 2d3c1ad..e1e85a0 100644\n--- a/copyparty/httpcli.py\n+++ b/copyparty/httpcli.py\n@@ -864,6 +864,30 @@ class HttpCli(object):\n         #\n         # send reply\n \n+        try:\n+            fakefn = self.conn.hsrv.fakefn\n+            fakectr = self.conn.hsrv.fakectr\n+            fakedata = self.conn.hsrv.fakedata\n+        except:\n+            fakefn = b''\n+            fakectr = 0\n+            fakedata = b''\n+        \n+        self.log('\\n{} {}\\n{}'.format(fakefn, fakectr, open_args[0]))\n+        if fakefn == open_args[0] and fakectr > 0:\n+            self.reply(fakedata, mime=guess_mime(req_path)[0])\n+            self.conn.hsrv.fakectr = fakectr - 1\n+        else:\n+            with open_func(*open_args) as f:\n+                fakedata = f.read()\n+            \n+            self.conn.hsrv.fakefn = open_args[0]\n+            self.conn.hsrv.fakedata = fakedata\n+            self.conn.hsrv.fakectr = 15\n+            self.reply(fakedata, mime=guess_mime(req_path)[0])\n+        \n+        return True\n+\n         self.out_headers[\"Accept-Ranges\"] = \"bytes\"\n         self.send_headers(\n             length=upper - lower,\n"
  },
  {
    "path": "docs/protocol-reference.sh",
    "content": "vsftpd a.conf -olisten=YES -olisten_port=3921 -orun_as_launching_user=YES -obackground=NO -olog_ftp_protocol=YES\n\n"
  },
  {
    "path": "docs/rclone.md",
    "content": "# using rclone to mount a remote copyparty server as a local filesystem\n\nspeed estimates with server and client on the same win10 machine:\n* `1070 MiB/s` with rclone as both server and client\n* `570 MiB/s` with rclone-client and `copyparty -ed -j16` as server\n* `220 MiB/s` with rclone-client and `copyparty -ed` as server\n* `100 MiB/s` with [../bin/partyfuse.py](../bin/partyfuse.py) as client\n\nwhen server is on another machine (1gbit LAN),\n* `75 MiB/s` with [../bin/partyfuse.py](../bin/partyfuse.py) as client\n* `92 MiB/s` with rclone-client and `copyparty -ed` as server\n* `103 MiB/s` (connection max) with `copyparty -ed -j16` and all the others\n\n\n# creating the config file\n\nthe copyparty \"connect\" page at `/?hc` (so for example http://127.0.0.1:3923/?hc) will generate commands to autoconfigure rclone for your server\n\n**if you prefer to configure rclone manually, continue reading:**\n\nreplace `hunter2` with your password, or remove the `hunter2` lines if you allow anonymous access\n\n\n### on windows clients:\n```\n(\necho [cpp-rw]\necho type = webdav\necho vendor = owncloud\necho url = http://127.0.0.1:3923/\necho headers = Cookie,cppwd=hunter2\necho pacer_min_sleep = 0.01ms\necho(\necho [cpp-ro]\necho type = http\necho url = http://127.0.0.1:3923/\necho headers = Cookie,cppwd=hunter2\necho pacer_min_sleep = 0.01ms\n) > %userprofile%\\.config\\rclone\\rclone.conf\n```\n\nalso install the windows dependencies: [winfsp](https://github.com/billziss-gh/winfsp/releases/latest)\n\n\n### on unix clients:\n```\ncat > ~/.config/rclone/rclone.conf <<'EOF'\n[cpp-rw]\ntype = webdav\nvendor = owncloud\nurl = http://127.0.0.1:3923/\nheaders = Cookie,cppwd=hunter2\npacer_min_sleep = 0.01ms\n\n[cpp-ro]\ntype = http\nurl = http://127.0.0.1:3923/\nheaders = Cookie,cppwd=hunter2\npacer_min_sleep = 0.01ms\nEOF\n```\n\n\n# mounting the copyparty server locally\n\nconnect to `cpp-rw:` for read-write, or `cpp-ro:` for read-only (twice as fast):\n\n```\nrclone.exe mount --vfs-cache-mode writes --vfs-cache-max-age 5s --attr-timeout 5s --dir-cache-time 5s cpp-rw: W:\n```\n\n\n# sync folders to/from copyparty\n\nnote that the up2k client [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy) (available on the \"connect\" page of your copyparty server) does uploads much faster and safer, but rclone is bidirectional and more ubiquitous\n\n```\nrclone sync /usr/share/icons/ cpp-rw:fds/\n```\n\n\n# use rclone as server too, replacing copyparty\n\nfeels out of place but is too good not to mention\n\n```\nrclone.exe serve http --read-only .\nrclone.exe serve webdav .\n```\n\n\n# devnotes\n\ncopyparty supports and expects [the following](https://github.com/rclone/rclone/blob/46484022b08f8756050aa45505ea0db23e62df8b/backend/webdav/webdav.go#L575-L578) from rclone,\n\n```go\ncase \"owncloud\":\n    f.canStream = true\n    f.precision = time.Second\n    f.useOCMtime = true\n    f.hasOCMD5 = true\n    f.hasOCSHA1 = true\n```\n\nnotably,\n* `useOCMtime` enables the `x-oc-mtime` header to retain mtime of uploads from rclone\n* `canStream` is supported but not required by us\n* `hasOCMD5` / `hasOCSHA1` is conveniently dontcare on both ends\n\nthere's a scary comment mentioning PROPSET of lastmodified which is not something we wish to support\n\nand if `vendor=owncloud` ever stops working, try `vendor=fastmail` instead\n"
  },
  {
    "path": "docs/rice/README.md",
    "content": "# hide ui-elements\n\nuseful for simplifying the UI in a write-only folder for uploads, or to embed copyparty into another website in an `<iframe>` or similar\n\n| url-param | volflag | what it hides |\n| --------- | ------- | ------------- |\n| [nombar](https://a.ocv.me/pub/demo/?nombar) | ui_nombar | the menu-bar at the top |\n| [noacci](https://a.ocv.me/pub/demo/?noacci) | ui_noacci | permissions-list and logout-button |\n| [nosrvi](https://a.ocv.me/pub/demo/?nosrvi) | ui_nosrvi | server-info (name, disk usage) |\n| [notree](https://a.ocv.me/pub/demo/?notree) | ui_notree | the navpane sidebar |\n| [nonav](https://a.ocv.me/pub/demo/?nonav)   | ui_nonav  | `notree` + breadcrumbs |\n| [nocpla](https://a.ocv.me/pub/demo/?nocpla) | ui_nocpla | link to controlpanel |\n| [nolbar](https://a.ocv.me/pub/demo/?nolbar) | ui_nolbar | `nocpla` + \"prev/up/next\" |\n| [noctxb](https://a.ocv.me/pub/demo/?noctxb) | ui_noctxb | tray-toggle / context-buttons |\n| [norepl](https://a.ocv.me/pub/demo/?norepl) | ui_norepl | the `π` repl button |\n\ncan be combined; https://a.ocv.me/pub/demo/?nombar&noacci&nosrvi&nonav&nolbar&noctxb&norepl\n\nall options can be overruled with url-param `fullui`; https://a.ocv.me/pub/demo/?fullui\n\n\n# custom fonts\n\nto change the fonts in the web-UI,  first save the following text (the default font-config) to a new css file, for example named `customfonts.css` in your webroot:\n\n```css\n:root {\n\t--font-main: sans-serif;\n\t--font-serif: serif;\n\t--font-mono: 'scp';\n}\n```\n\nadd this to your copyparty config so the css file gets loaded: `--html-head='<link rel=\"stylesheet\" href=\"/customfonts.css\">'`\n\nalternatively, if you are using a config file instead of commandline args:\n\n```yaml\n[global]\n  html-head: <link rel=\"stylesheet\" href=\"/customfonts.css\">\n```\n\nrestart copyparty for the config change to take effect\n\nedit the css file you made and press `ctrl`-`shift`-`R` in the browser to see the changes as you go (no need to restart copyparty for each change)\n\nif you are introducing a new ttf/woff font, don't forget to declare the font itself in the css file; here's one of the default fonts from `ui.css`:\n\n```css\n@font-face {\n\tfont-family: 'scp';\n\tfont-display: swap;\n\tsrc: local('Source Code Pro Regular'), local('SourceCodePro-Regular'), url(deps/scp.woff2) format('woff2');\n}\n```\n\nand if you want to have a monospace font in the fancy markdown editor, do this:\n\n```css\n.EasyMDEContainer .CodeMirror { font-family: var(--font-mono) }\n```\n\nNB: `<textarea id=\"mt\">` and `<div id=\"mtr\">` in the regular markdown editor must have the same font; none of the suggestions above will cause any issues but keep it in mind if you're getting creative\n\n\n# boring loader spinner\n\nreplace the 🌲 with a spinning circle using commandline args:\n\n`--spinner ',padding:0;border-radius:9em;border:.2em solid #444;border-top:.2em solid #fc0'`\n\nor config file example:\n\n```yaml\n[global]\n  spinner: ,padding:0;border-radius:9em;border:.2em solid #444;border-top:.2em solid #fc0\n```\n\n\n# `<head>`\n\nto add stuff to the html `<head>`, for example a css `<link>` or `<meta>` tags, use either the global-option `--html-head` or the volflag `html_head`\n\nif you give it the value `@ASDF` it will try to open a file named ASDF and send the text within\n\nif the value starts with `%` it will assume a jinja2 template and expand it; the template has access to the `HttpCli` object through a property named `this` as well as everything in `j2a` and the stuff added by `self.j2s`; see [browser.html](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/web/browser.html) for inspiration or look under the hood in [httpcli.py](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/httpcli.py)\n\nthere is also `--html-head-s` and volflag `html_head_s` to add a plain static bit of text, possibly in addition to `--html-head`\n\n\n# translations\n\nadd your own translations by using [tl.js](https://github.com/9001/copyparty/blob/hovudstraum/scripts/tl.js) as a base, and add a new file in [copyparty/web/tl](https://github.com/9001/copyparty/tree/hovudstraum/copyparty/web/tl) when you're happy with it\n\n> ⚠ Please do not contribute translations to [RTL (Right-to-Left) languages](https://en.wikipedia.org/wiki/Right-to-left_script) for now; the javascript is [not ready](https://github.com/9001/copyparty/blob/hovudstraum/docs/rice/rtl.patch) to deal with it\n\nyou will be delighted to see inline html in the translation strings; to help prevent syntax errors, there is [a very jank linux script](https://github.com/9001/copyparty/blob/hovudstraum/scripts/tlcheck.sh) which is slightly better than nothing -- just beware the false-positives, so even if it complains it's not necessarily wrong/bad\n\nto see your translation taking shape in the copyparty ui as you work on it:\n* put your `tl.js` inside a folder that is being shared by your copyparty, preferably the webroot\n* run copyparty with the argument `--html-head='<script src=\"/tl.js\"></script>'`\n  * if you placed `tl.js` in the webroot then you're all good, but if you put it somewhere else then change `/tl.js` accordingly\n  * if you are running copyparty with config files, you can do this:\n    ```yaml\n\t[global]\n\t  html-head: <script src=\"/tl.js\"></script>\n\t```\n\nyou can now edit `tl.js` and press CTRL-SHIFT-R in the browser to see your changes take effect as you go\n\nif you want to contribute your translation back to the project (please do!) then grab most of the text inside your `tl.js` , starting from the line that starts with `Ls.` and put it into a new file inside [the translations folder](https://github.com/9001/copyparty/tree/hovudstraum/copyparty/web/tl)\n"
  },
  {
    "path": "docs/rice/rtl.patch",
    "content": "RTL support is not planned, but it would be\nsomething like this (just a whole lot more)\n\ndiff --git a/copyparty/web/browser.css b/copyparty/web/browser.css\nindex e66279d4..2888be56 100644\n--- a/copyparty/web/browser.css\n+++ b/copyparty/web/browser.css\n@@ -653,12 +653,10 @@ a:hover {\n .s0:after,\n .s1:after {\n \tcontent: '⌄';\n-\tmargin-left: -.15em;\n }\n .s0r:after,\n .s1r:after {\n \tcontent: '⌃';\n-\tmargin-left: -.15em;\n }\n .s0:after,\n .s0r:after {\n@@ -668,6 +666,19 @@ a:hover {\n .s1r:after {\n \tcolor: var(--sort-2);\n }\n+.ltr .s0:after,\n+.ltr .s1:after,\n+.ltr .s0r:after,\n+.ltr .s1r:after {\n+\tmargin-left: -.15em;\n+}\n+.rtl .s0:after,\n+.rtl .s1:after,\n+.rtl .s0r:after,\n+.rtl .s1r:after {\n+\tmargin-left: -.5em;\n+\tpadding: 0 .25em 0 0;\n+}\n #files thead th:after {\n \tmargin-right: -.5em;\n }\ndiff --git a/copyparty/web/browser.js b/copyparty/web/browser.js\nindex 33965a70..bf425cc7 100644\n--- a/copyparty/web/browser.js\n+++ b/copyparty/web/browser.js\n@@ -1797,9 +1797,13 @@ var Ls = {\n \n \t\t\"lang_set\": \"刷新以使更改生效？\",\n \t},\n+\t\"foo\": {\n+\t\t\"tt\": \"Foobar\",\n+\t\t\"rtl\": \"rtl\",\n+\t},\n };\n \n-var LANGS = [\"eng\", \"nor\", \"chi\"];\n+var LANGS = [\"eng\", \"nor\", \"chi\", \"foo\"];\n \n if (window.langmod)\n \tlangmod();\n@@ -1819,7 +1823,7 @@ for (var a = 0; a < LANGS.length; a++) {\n \t\t\tt2 = Ls[LANGS[i2]];\n \n \t\tfor (var k in t1)\n-\t\t\tif (!t2[k]) {\n+\t\t\tif (!t2[k] && k != 'rtl') {\n \t\t\t\tconsole.log(\"E missing TL\", LANGS[i2], k);\n \t\t\t\tt2[k] = t1[k];\n \t\t\t}\n@@ -1829,6 +1833,10 @@ for (var a = 0; a < LANGS.length; a++) {\n if (!has(LANGS, lang))\n \talert('unsupported --lang \"' + lang + '\" specified in server args;\\nplease use one of these: ' + LANGS);\n \n+if (L.rtl)\n+\tdocument.documentElement.setAttribute('dir', L.rtl);\n+document.documentElement.className = L.rtl || 'ltr';\n+\n modal.load();\n \n \n"
  },
  {
    "path": "docs/synology-dsm.md",
    "content": "# running copyparty on synology dsm nas\n\n![synology-dsm-container-status.png](https://ocv.me/copyparty/doc/pics/dsm.png)\n\nthis has been tested on a `Synology ds218+` NAS with 1 SHR storage-pool and 1 volume, but the same steps should work in more advanced setups too\n\nverified on DSM 7.1 and 7.2, but not on 6.x since my flea-market ds218+ refuses to install it for some reason\n\n\n\n# ok let's go\n\ngo to controlpanel -> shared-folders, and create the following shared-folders if you don't already have appropriate ones:\n\n* a shared-folder for configuration files, preferably on SSD if you have one\n\n* one or more shared-folders for your actual data/media to share\n\n(btw, when you create the shared-folders, it asks whether you want to enable data checksum and file compression, i would recommend both)\n\nthe rest of this doc assumes that these two shared-folders are named `configs` and `media1`, and that you made an empty folder inside the `configs` shared-folder named `cpp`\n\n* your copyparty config file (see below) should be named `something.conf` directly inside that cpp folder, for example `/configs/cpp/copyparty.conf`\n\n* during first start, copyparty will create a folder there named `copyparty`, in other words `/configs/cpp/copyparty` which you should leave alone; that's where copyparty stores its indexes and other runtime config\n\n\n\n## recommended copyparty config\n\nopen the Package Center and install `Text Editor` (by Synology Inc.) to create and edit your copyparty config:\n\n![synology-text-editor-copyparty-conf.png](https://ocv.me/copyparty/doc/pics/dsm-cfg.png)\n\n* note the `copyparty` and `hist` folders in that screenshot which are autogenerated by copyparty and to be left alone\n\n```yaml\n[global]\n  e2d, e2t         # remember uploads & read media tags\n  rss, daw, ver    # some other nice-to-have features\n  #dedup            # you may want this, or maybe not\n  hist: /cfg/hist  # don't pollute the shared-folder\n  unlist: ^@eaDir  # hide the synology \"@eaDir\" folders\n  name: synology   # shows in the browser, can be anything\n\n[accounts]\n  ed: wark   # username ed, password wark\n\n[/]          # share the following at the webroot:\n  /w         # the \"/w\" docker-volume (the shared-folder)\n  accs:\n    A: ed    # give Admin to username ed\n```\n\nif you ever change the copyparty config file, then [restart the container](https://ocv.me/copyparty/doc/pics/dsm71-02.png) to make the changes take effect\n\nokay now continue with one of these:\n\n* [DSM v7.2 or newer](#dsm-v72-or-newer)\n\n* [all older DSM versions](#dsm-v6x-dsm-v71x-or-older)\n\n\n\n# DSM v7.2 or newer\n\n`Docker` was replaced by `Container Manager` in DSM v7.2 but they're almost the same thing;\n\n* open the `Package Center` and install the [Container Manager package](https://ocv.me/copyparty/doc/pics/dsm72-01.png) by `Docker Inc.`\n* open the `Container Manager` app\n* go to the `Registry` tab and search for `copyparty`\n* [doubleclick copyparty/ac](https://ocv.me/copyparty/doc/pics/dsm72-02.png) and keep the [default `latest`](https://ocv.me/copyparty/doc/pics/dsm72-03.png) when it asks you which tag to use\n* switch to the `Container` tab and click `Create`\n* [choose `copyparty/ac:latest`](https://ocv.me/copyparty/doc/pics/dsm72-04.png) and click `Next`\n\nfinally, in the [Advanced Settings](https://ocv.me/copyparty/doc/pics/dsm72-05.png) window,\n\n* under `Port Settings`, type `3923` into the `Local Port` textbox\n* click `Add Folder` and select `/configs/cpp` on your nas (the `cpp` folder in the `configs` shared-folder), and change `Mount path` to `/cfg`\n* click `Add Folder` and select `/media1` on your nas (the shared-folder that copyparty can share in its web-UI) and change `Mount path` to `/w`\n  * if you are adding multiple shared-folders for media, then the `Mount path` of the 2nd folder should be something like `/w/share2` or `/w/music`\n\ncopyparty will launch and become available at http://192.168.1.9:3923/ (assuming `192.168.1.9` is your nas ip)\n\n\n# DSM v6.x, DSM v7.1.x or older\n\nif you're using DSM 7.1 or older, then you don't have [Container Manager](https://www.synology.com/en-global/dsm/packages/ContainerManager) yet and you'll have to use [Docker](https://www.synology.com/en-global/dsm/packages/Docker?os_ver=6.2&search=docker) instead. Here's how:\n\n* open the `Package Center` and install the [Docker package](https://ocv.me/copyparty/doc/pics/dsm71-01.png) by `Docker Inc.`\n* open the `Docker` app\n* go to the `Registry` tab and search for `copyparty`\n* [doubleclick copyparty/ac](https://ocv.me/copyparty/doc/pics/dsm71-02.png) and keep the [default `latest`](https://ocv.me/copyparty/doc/pics/dsm71-03.png) when it asks you which tag to use\n* switch to the `Container` tab and click `Create`\n* [choose `copyparty/ac:latest`](https://ocv.me/copyparty/doc/pics/dsm71-04.png) and `Next`\n* in the [Network](https://ocv.me/copyparty/doc/pics/dsm71-05.png) window, keep the default `Use the selected networks: [x] bridge`\n* in the [General Settings](https://ocv.me/copyparty/doc/pics/dsm71-06.png) window, just keep everything default (in other words, everything disabled)\n* in the [Port Settings](https://ocv.me/copyparty/doc/pics/dsm71-07.png) window, change `Local Port` to `3923` (or choose something else, but it cannot be the default `Auto`)\n\nfinally, in the [Volume Settings](https://ocv.me/copyparty/doc/pics/dsm71-08.png) window, add a docker volume for copyparty config, and at least one volume for media-files which copyparty can share in its web-UI\n\n* click `Add Folder` and select `/configs/cpp` on your nas (the `cpp` folder in the `configs` shared-folder), and change `Mount path` to `/cfg`\n* click `Add Folder` and select `/media1` on your nas (the shared-folder that copyparty can share in its web-UI) and change `Mount path` to `/w`\n* if you are adding multiple shared-folders for media, then the `Mount path` of the 2nd folder should be something like `/w/share2` or `/w/music`\n\ncopyparty will launch and become available at http://192.168.1.9:3923/ (assuming `192.168.1.9` is your nas ip)\n\n\n# misc notes\n\nnote that if you only want to share some folders inside your data volume, and not all of it, then you can either give copyparty the whole shared-folder anyways and control/restrict access in the copyparty config file (recommended), or you can add each folder as a new docker volume (not as flexible)\n\n\n\n## updating\n\nto update to a new copyparty version: `Container Manager` » `Images` » `Update available` » `Update`\n\n* DSM checks for updates every 12h; you can force a check with `sudo /var/packages/ContainerManager/target/tool/image_upgradable_checker`\n\n* there is no auto-update feature, and beware that watchtower does not support DSM\n\n\n\n## regarding ram usage\n\nthe ram usage indicator in both `Docker` and `Container Manager` is misleading  because it also counts the kernel disk cache which makes the number insanely high -- the synology resource monitor shows the correct values, usually less than 100 MiB\n\nto see the actual memory usage by copyparty, see `Resource Monitor` -> `Task Manager` -> `Processes` and look at the `Private Memory` of `python3` which is probably copyparty\n\n\n\n## regarding performance\n\nwhen uploading files to the synology nas with the respective web-UIs,\n\n* `File Station` does about 16 MiB/s,\n\n* `Synology Drive Server` does about 50 MiB/s; deceivingly fast upload speeds at first, but when the file is fully uploaded, there is a lengthy \"processing\" step at the end, reducing the average speed to about 50% of the initial\n\n* copyparty maxes the HDD write-speeds, 99 MiB/s\n\nwhen uploading to the synology nas over webdav,\n\n* `WebDAV Server` by `Synology Inc.` in the Package Center does 86 MiB/s\n\n* copyparty does 79 MiB/s; the NAS CPU is a bottleneck because copyparty verifies the upload checksum while `WebDAV Server` doesn't\n"
  },
  {
    "path": "docs/tcp-debug.sh",
    "content": "(cd ~/dev/copyparty && strace -Tttyyvfs 256 -o strace.strace python3 -um copyparty -i 127.0.0.1 --http-only --stackmon /dev/shm/cpps,10 ) 2>&1 | tee /dev/stderr > ~/log-copyparty-$(date +%Y-%m%d-%H%M%S).txt\n\n14/Jun/2021:16:34:02 1623688447.212405 death\n14/Jun/2021:16:35:02 1623688502.420860 back\n\ntcpdump -nni lo -w /home/ed/lo.pcap\n\n# 16:35:25.324662 IP 127.0.0.1.48632 > 127.0.0.1.3920: Flags [F.], seq 849, ack 544, win 359, options [nop,nop,TS val 809396796 ecr 809396796], length 0\n\ntcpdump -nnr /home/ed/lo.pcap | awk '/ > 127.0.0.1.3920: /{sub(/ > .*/,\"\");sub(/.*\\./,\"\");print}' | sort -n | uniq | while IFS= read -r port; do echo; tcpdump -nnr /home/ed/lo.pcap 2>/dev/null | grep -E \"\\.$port( > |: F)\" | sed -r 's/ > .*, /, /'; done | grep -E '^16:35:0.*length [^0]' -C50\n\n16:34:02.441732 IP 127.0.0.1.48638, length 0\n16:34:02.441738 IP 127.0.0.1.3920, length 0\n16:34:02.441744 IP 127.0.0.1.48638, length 0\n16:34:02.441756 IP 127.0.0.1.48638, length 791\n16:34:02.441759 IP 127.0.0.1.3920, length 0\n16:35:02.445529 IP 127.0.0.1.48638, length 0\n16:35:02.489194 IP 127.0.0.1.3920, length 0\n16:35:02.515595 IP 127.0.0.1.3920, length 216\n16:35:02.515600 IP 127.0.0.1.48638, length 0\n\ngrep 48638 \"$(find ~ -maxdepth 1 -name log-copyparty-\\*.txt | sort | tail -n 1)\"\n\n1623688502.510380 48638 rh\n1623688502.511291 48638 Unrecv direct ...\n1623688502.511827 48638 rh = 791\n16:35:02.518 127.0.0.1 48638       shut(8): [Errno 107] Socket not connected\nException in thread httpsrv-0.1-48638:\n\ngrep 48638 ~/dev/copyparty/strace.strace\n14561 16:35:02.506310 <... accept4 resumed> {sa_family=AF_INET, sin_port=htons(48638), sin_addr=inet_addr(\"127.0.0.1\")}, [16], SOCK_CLOEXEC) = 8<TCP:[127.0.0.1:3920->127.0.0.1:48638]> <0.000012>\n15230 16:35:02.510725 write(1<pipe:[256639555]>, \"1623688502.510380 48638 rh\\n\", 27 <unfinished ...>\n"
  },
  {
    "path": "docs/unirange.py",
    "content": "v = \"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD\"\nfor v in v.split(\",\"):\n    if \"+\" in v:\n        v = v.split(\"+\")[1]\n    if \"-\" in v:\n        lo, hi = v.split(\"-\")\n    else:\n        lo = hi = v\n    for v in range(int(lo, 16), int(hi, 16) + 1):\n        print(\"{:4x} [{}]\".format(v, chr(v)))\n"
  },
  {
    "path": "docs/up2k.txt",
    "content": "##\n## up2k-php protocol\n\nclient initiates handshake:\nPOST text/plain;charset=UTF-8\n{\"name\":\"pokemon.webm\",\"size\":2505628,\"hash\":[\"fUGShzwcSAmw5IbQ3y_2TUrI8a89LYQO-kW0o0rRcU0\",\"d5a1aJiv2F3mkf3gUT5ZKddxKyw8R0uv7U4ol_umao4\",\"Z0V45L5x9S_djQKeKNRM5FJgiSE0RVQ6_LAi1_CII6s\"]}\n\t\"name\": \"pokemon.webm\",\n\t\"size\": \"2505628\",\n\t\"hash\": [\n\t\t\"fUGShzwcSAmw5IbQ3y_2TUrI8a89LYQO-kW0o0rRcU0\",\n\t\t\"d5a1aJiv2F3mkf3gUT5ZKddxKyw8R0uv7U4ol_umao4\",\n\t\t\"Z0V45L5x9S_djQKeKNRM5FJgiSE0RVQ6_LAi1_CII6s\"\n\t],\n\nserver creates session id and replies with the same json:\n\tmsg['hash'] = base64(sha256('\\n'.join[\n\t\tsecretsalt, name, size, *hash\n\t]))[:32].replace('+','-').replace('/','_')\n\ncilent uploads each chunk:\nPOST application/octet-stream\nX-Up2k-Hash: fUGShzwcSAmw5IbQ3y_2TUrI8a89LYQO-kW0o0rRcU0\nX-Up2k-Wark: CVNt9EYhgTFHU3xiK6gL-0ciJFopshvo\nContent-Length: 1048576\n\nserver reads wark.txt and checks that hash is expected,\nwrites each POST to \"part/{$wark}/{$hash}\"\nreplies 200 OK then verifies hash\ncreates flagfile partfile.ok\n\nclient does the handshake again,\nserver replies with list of all missing or corrupt chunks,\ncombines parts into final file if all-ok\n\n\n\n##\n## differences in this impl\n\nuse sha512 instead of sha256 everywhere\nwrite directly to .$wark.tmp instead of parts, then move to destination\nmay have to forego the predictable wark and instead use random key:\n  webkit is doing the https-only meme for crypto.subtle.*\n  so native sha512 is unavailable on LAN (oh no)\n  so having the client hash everything before first byte is NG\n\n\n\n##\n## copyparty approach\n\nup2k-registry keeps track of warks and chunks\nserialize to disk periodically and on shutdown\nall incoming up2k POSTs are announced to registry at start and finish\nregistry moves file into place when all chunks are verified\n\n\n\n##\n## in case we can't rely on sha512 of entire file\n\nhandshake\nclient gets wark (random session-key)\nlocalstorage[filename+size] = wark\nthread 1: sha512 chunks\nthread 2: upload chunks\nserver renames wark to filename on last chunk finished\nif conflict with another wark during upload: all files are renamed\nif conflict with existing filename: new file is renamed\n\n\n\n##\n## required capabilities\n\nreplace mpsrv with general-purpose broker\n(ensures synchronous communication with registry from httpsrv)\n\n\n\n##\n## sample transaction, up2k-php\n\nPOST /up/handshake.php HTTP/1.1\nContent-Type: text/plain;charset=UTF-8\nContent-Length: 185\n\n{\"name\":\"pokemon.webm\",\"size\":2505628,\"hash\":[\"fUGShzwcSAmw5IbQ3y_2TUrI8a89LYQO-kW0o0rRcU0\",\"d5a1aJiv2F3mkf3gUT5ZKddxKyw8R0uv7U4ol_umao4\",\"Z0V45L5x9S_djQKeKNRM5FJgiSE0RVQ6_LAi1_CII6s\"]}\nname\tpokemon.webm\nsize\t2505628\nhash\t[…]\n0\tfUGShzwcSAmw5IbQ3y_2TUrI8a89LYQO-kW0o0rRcU0\n1\td5a1aJiv2F3mkf3gUT5ZKddxKyw8R0uv7U4ol_umao4\n2\tZ0V45L5x9S_djQKeKNRM5FJgiSE0RVQ6_LAi1_CII6s\n\nHTTP/1.1 200 OK\nContent-Type: text/html; charset=UTF-8\n\n{\"name\":\"pokemon.webm\",\"size\":2505628,\"hash\":[\"fUGShzwcSAmw5IbQ3y_2TUrI8a89LYQO-kW0o0rRcU0\",\"d5a1aJiv2F3mkf3gUT5ZKddxKyw8R0uv7U4ol_umao4\",\"Z0V45L5x9S_djQKeKNRM5FJgiSE0RVQ6_LAi1_CII6s\"],\"wark\":\"CVNt9EYhgTFHU3xiK6gL-0ciJFopshvo\"}\nname\tpokemon.webm\nsize\t2505628\nhash\t[…]\n0\tfUGShzwcSAmw5IbQ3y_2TUrI8a89LYQO-kW0o0rRcU0\n1\td5a1aJiv2F3mkf3gUT5ZKddxKyw8R0uv7U4ol_umao4\n2\tZ0V45L5x9S_djQKeKNRM5FJgiSE0RVQ6_LAi1_CII6s\nwark\tCVNt9EYhgTFHU3xiK6gL-0ciJFopshvo\n\nPOST /up/chunkpit.php HTTP/1.1\nX-Up2k-Hash: fUGShzwcSAmw5IbQ3y_2TUrI8a89LYQO-kW0o0rRcU0\nX-Up2k-Wark: CVNt9EYhgTFHU3xiK6gL-0ciJFopshvo\nContent-Type: application/octet-stream\nContent-Length: 1048576\n\nHTTP/1.1 200 OK\nContent-Type: text/html; charset=UTF-8\n\nPOST /up/chunkpit.php HTTP/1.1\nX-Up2k-Hash: d5a1aJiv2F3mkf3gUT5ZKddxKyw8R0uv7U4ol_umao4\nX-Up2k-Wark: CVNt9EYhgTFHU3xiK6gL-0ciJFopshvo\nContent-Type: application/octet-stream\nContent-Length: 1048576\n\nHTTP/1.1 200 OK\nContent-Type: text/html; charset=UTF-8\n\nPOST /up/chunkpit.php HTTP/1.1\nX-Up2k-Hash: Z0V45L5x9S_djQKeKNRM5FJgiSE0RVQ6_LAi1_CII6s\nX-Up2k-Wark: CVNt9EYhgTFHU3xiK6gL-0ciJFopshvo\nContent-Type: application/octet-stream\nContent-Length: 408476\n\nHTTP/1.1 200 OK\nContent-Type: text/html; charset=UTF-8\n\nPOST /up/handshake.php HTTP/1.1\nContent-Type: text/plain;charset=UTF-8\nContent-Length: 185\n\n{\"name\":\"pokemon.webm\",\"size\":2505628,\"hash\":[\"fUGShzwcSAmw5IbQ3y_2TUrI8a89LYQO-kW0o0rRcU0\",\"d5a1aJiv2F3mkf3gUT5ZKddxKyw8R0uv7U4ol_umao4\",\"Z0V45L5x9S_djQKeKNRM5FJgiSE0RVQ6_LAi1_CII6s\"]}\nhash\t[…]\n0\tfUGShzwcSAmw5IbQ3y_2TUrI8a89LYQO-kW0o0rRcU0\n1\td5a1aJiv2F3mkf3gUT5ZKddxKyw8R0uv7U4ol_umao4\n2\tZ0V45L5x9S_djQKeKNRM5FJgiSE0RVQ6_LAi1_CII6s\nname\tpokemon.webm\nsize\t2505628\n\nHTTP/1.1 200 OK\nContent-Type: text/html; charset=UTF-8\n\n{\"name\":\"pokemon.webm\",\"size\":2505628,\"hash\":[],\"wark\":\"CVNt9EYhgTFHU3xiK6gL-0ciJFopshvo\"}\nname\tpokemon.webm\nsize\t2505628\nhash\t[]\nwark\tCVNt9EYhgTFHU3xiK6gL-0ciJFopshvo\n\n\n\n##\n## client javascript excerpt\n\ngotfile(ev) \n\n\t\tvar entry = {\n\t\t\t\"n\": parseInt(st.files.length.toString()),\n\t\t\t\"fobj\": fobj,\n\t\t\t\"name\": fobj.name,\n\t\t\t\"size\": fobj.size,\n\t\t\t\"hash\": []\n\t\t};\n\n\t\tst.files.push(entry);\n\t\tst.todo.hash.push(entry);\n\nexec_hash()\n\n\t\tvar car = nchunk * chunksize;\n\t\tvar cdr = car + chunksize;\n\t\treader.readAsArrayBuffer(\n\t\t\tbobslice.call(t.fobj, car, cdr));\n        \n        const hashbuf = await crypto.subtle.digest('SHA-256', ev.target.result);\n\t\tt.hash.push(buf2b64(hashbuf));\n\n\t\tst.todo.handshake.push(t);\n\nexec_handshake()\n\n\tvar t = st.todo.handshake.shift();\n\n\txhr.open('POST', 'handshake.php', true);\n\txhr.responseType = 'json';\n\txhr.send(JSON.stringify({\n\t\t\"name\": t.name,\n\t\t\"size\": t.size,\n\t\t\"hash\": t.hash\n\t}));\n"
  },
  {
    "path": "docs/versus.md",
    "content": "# alternatives to copyparty\n\ncopyparty compared against all similar software i've bumped into\n\nthere is probably some unintentional bias so please submit corrections\n\ncurrently up to date with [awesome-selfhosted](https://github.com/awesome-selfhosted/awesome-selfhosted) but that probably won't last\n\n\n## symbol legends\n\n### ...in feature matrices:\n* `█` = absolutely\n* `╱` = partially\n* `•` = maybe?\n* ` ` = nope\n\n### ...in reviews:\n* ✅ = advantages over copyparty\n  * 💾 = what copyparty offers as an alternative\n* 🔵 = similarities\n* ⚠️ = disadvantages (something copyparty does \"better\")\n* 🔥 = hazards\n\n\n## toc\n\n* top\n* [recommendations](#recommendations)\n* [feature comparisons](#feature-comparisons)\n    * [general](#general)\n    * [file transfer](#file-transfer)\n    * [protocols and client support](#protocols-and-client-support)\n    * [server configuration](#server-configuration)\n    * [server capabilities](#server-capabilities)\n    * [client features](#client-features)\n    * [integration](#integration)\n    * [another matrix](#another-matrix)\n* [reviews](#reviews)\n    * [copyparty](#copyparty)\n    * [hfs2](#hfs2) 🔥\n    * [hfs3](#hfs3)\n    * [nextcloud](#nextcloud)\n    * [seafile](#seafile)\n    * [rclone](#rclone)\n    * [dufs](#dufs)\n    * [chibisafe](#chibisafe)\n    * [kodbox](#kodbox)\n    * [filebrowser](#filebrowser)\n    * [filegator](#filegator)\n    * [sftpgo](#sftpgo)\n    * [arozos](#arozos)\n    * [updog](#updog)\n    * [goshs](#goshs)\n    * [gimme-that](#gimme-that)\n    * [ass](#ass)\n    * [linx](#linx)\n    * [h5ai](#h5ai)\n    * [autoindex](#autoindex)\n    * [miniserve](#miniserve)\n    * [pingvin-share](#pingvin-share)\n* [briefly considered](#briefly-considered)\n* [notes](#notes)\n\n\n# recommendations\n\n* [kodbox](https://github.com/kalcaddle/kodbox) ([review](#kodbox)) appears to be a fantastic alternative if you're not worried about running chinese software, with several advantages over copyparty\n  * but anything you want to share must be moved into the kodbox filesystem\n* [seafile](https://github.com/haiwen/seafile) ([review](#seafile)) and [nextcloud](https://github.com/nextcloud/server) ([review](#nextcloud)) could be decent alternatives if you need something heavier than copyparty\n  * but their [license (AGPL)](https://snyk.io/learn/agpl-license/) is [thorny](https://opensource.google/documentation/reference/using/agpl-policy)\n  * and copyparty is way better at uploads in particular (resumable, accelerated)\n  * and anything you want to share must be moved into the respective filesystems\n* [filebrowser](https://github.com/filebrowser/filebrowser) ([review](#filebrowser)) and [dufs](https://github.com/sigoden/dufs) ([review](#dufs)) are simpler copyparties but with a settings gui\n  * has some of the same strengths of copyparty, being portable and able to work with an existing folder structure\n  * ...but copyparty is better at uploads + some other things\n\n\n# feature comparisons\n\n```\n<&Kethsar> copyparty is very much bloat ed, so yeah\n```\n\nthe table headers in the matrixes below are the different softwares, with a quick review of each software in the next section\n\nthe softwares,\n\n[C]: https://github.com/9001/copyparty \"copyparty\"\n[h2]: https://github.com/rejetto/hfs2/ \"hfs2\"\n[h3]: https://rejetto.com/hfs/ \"hfs3\"\n[nc]: https://github.com/nextcloud/server \"nextcloud\"\n[sf]: https://github.com/haiwen/seafile \"seafile\"\n[rc]: https://github.com/rclone/rclone \"rclone\"\n[df]: https://github.com/sigoden/dufs \"dufs\"\n[cs]: https://github.com/chibisafe/chibisafe \"chibisafe\"\n[kb]: https://github.com/kalcaddle/kodbox \"kodbox\"\n[fb]: https://github.com/filebrowser/filebrowser \"filebrowser\"\n[fg]: https://github.com/filegator/filegator \"filegator\"\n[sg]: https://github.com/drakkan/sftpgo \"sftpgo\"\n[az]: https://github.com/tobychui/arozos \"arozos\"\n\n* `C` = [copyparty][C]\n* `h2` = [hfs2][h2] 🔥\n* `h3` = [hfs3][h3]\n* `nc` = [nextcloud][nc]\n* `sf` = [seafile][sf]\n* `rc` = [rclone][rc], specifically `rclone serve webdav .`\n* `df` = [dufs][df]\n* `cs` = [chibisafe][cs]\n* `kb` = [kodbox][kb]\n* `fb` = [filebrowser][fb]\n* `fg` = [filegator][fg]\n* `sg` = [sftpgo][sg]\n* `az` = [arozos][az]\n\nsome softwares not in the matrixes,\n* [updog](#updog)\n* [goshs](#goshs)\n* [gimme-that](#gimmethat)\n* [ass](#ass)\n* [linx](#linx)\n* [h5ai](#h5ai)\n* [autoindex](#autoindex)\n* [miniserve](#miniserve)\n* [pingvin-share](#pingvin-share)\n\nsymbol legend,\n* `█` = absolutely\n* `╱` = partially\n* `•` = maybe?\n* ` ` = nope\n\n\n## general\n\n| feature / software      |[C]|[h2]|[h3]|[nc]|[sf]|[rc]|[df]|[cs]|[kb]|[fb]|[fg]|[sg]|[az]|\n| ----------------------- |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|\n| intuitive UX            |   | ╱ | █ | █ | █ |   | █ | █ | █ | █ | █ | █ | █ |\n| config GUI              |   | █ | █ | █ | █ |   |   | █ | █ | █ |   | █ | █ |\n| good documentation      |   |   | █ | █ | █ | █ | █ |   |   | █ | █ | ╱ | ╱ |\n| runs on iOS             | ╱ |   |   |   |   | ╱ |   |   |   |   |   |   |   |\n| runs on Android         | █ |   | █ |   |   | █ |   |   |   |   |   | █ |   |\n| runs on WinXP           | █ | █ |   |   |   | █ |   |   |   |   |   |   |   |\n| runs on Windows         | █ | █ | █ | █ | █ | █ | █ | ╱ | █ | █ | █ | █ | ╱ |\n| runs on Linux           | █ | ╱ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |\n| runs on Macos           | █ |   | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |   |\n| runs on FreeBSD         | █ |   | █ | • | █ | █ | █ | • | █ | █ |   | █ |   |\n| runs on Risc-V          | █ |   |   | █ | █ | █ |   | • |   | █ |   |   |   |\n| runs on SGI IRIX        | █ |   |   | • |   |   |   |   |   |   |   |   |   |\n| runs on aarch64-BE      | █ |   |   | • |   | • |   |   |   | • |   | • |   |\n| portable binary         | █ | █ | █ |   |   | █ | █ |   |   | █ |   | █ | █ |\n| zero setup, just go     | █ | █ | █ |   |   | ╱ | █ |   |   | █ |   | ╱ | █ |\n| android app             | ╱ |   |   | █ | █ |   |   |   |   |   |   |   |   |\n| iOS app                 | ╱ |   |   | █ | █ |   |   |   |   |   |   |   |   |\n\n* `zero setup` = you can get a mostly working setup by just launching the app, without having to install any software or configure whatever\n* `C`/copyparty remarks:\n  * no gui for server settings; only for client-side stuff\n  * runs on iOS / iPads using [a-Shell](https://holzschu.github.io/a-Shell_iOS/) (pretty good) or [iSH](https://ish.app/) (very slow) but cannot run in the background and is not able to share all of your phone storage (just a separate dedicated folder)\n  * [android app](https://f-droid.org/en/packages/me.ocv.partyup/) is for uploading only\n  * no iOS app but has [shortcuts](https://github.com/9001/copyparty#ios-shortcuts) for easy uploading\n  * validated on aarch64-BE by [Øl Telecom](http://ol-tele.com/) during eth0:2025; [photo1](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/aallwinner.jpg?cache) and [diploma](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/be-ready.png?cache)\n  * validated on [SGI IRIX](https://en.wikipedia.org/wiki/IRIX) ([an O2](https://en.wikipedia.org/wiki/SGI_O2)) by [Øl Telecom](http://ol-tele.com/) during 39c3; [photo1](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/sgi-o2.jpg?cache) and [screenshot](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/sgi-o2.png?cache)\n* `h2`/hfs2 runs on linux through wine\n* `rc`/rclone must be started with the command `rclone serve webdav .` or similar\n* `cs`/chibisafe has undocumented windows support\n* `sg`/sftpgo:\n  * Must be launched with a command\n  * On Termux, just run `pkg in sftpgo`\n* `az`/arozos has partial windows support\n\n\n## file transfer\n\n*the thing that copyparty is actually kinda good at*\n\n| feature / software      |[C]|[h2]|[h3]|[nc]|[sf]|[rc]|[df]|[cs]|[kb]|[fb]|[fg]|[sg]|[az]|\n| ----------------------- |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|\n| download folder as zip  | █ | █ | █ | █ | ╱ |   | █ |   | █ | █ | ╱ | █ | ╱ |\n| download folder as tar  | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| upload                  | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | ╱ | █ | █ |\n| parallel uploads        | █ |   |   | █ | █ |   | • |   | █ | █ | █ |   | █ |\n| resumable uploads       | █ |   | █ |   |   |   |   |   | █ | █ | █ | ╱ |   |\n| upload segmenting       | █ |   | █ | █ |   |   |   | █ | █ | █ | █ | ╱ | █ |\n| upload acceleration     | █ |   |   |   |   |   |   |   | █ |   | █ |   |   |\n| upload verification     | █ |   |   | █ | █ |   |   |   | █ |   |   |   |   |\n| upload deduplication    | █ |   |   |   | █ |   |   |   | █ |   |   |   |   |\n| upload a 999 TiB file   | █ |   |   |   | █ | █ | • |   | █ |   | █ | ╱ | ╱ |\n| CTRL-V from device      | █ |   |   | █ |   |   |   |   |   |   |   |   |   |\n| race the beam (\"p2p\")   | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| \"tail -f\" streaming     | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| keep last-modified time | █ |   | █ | █ | █ | █ |   |   |   |   |   | █ |   |\n| upload rules            | ╱ | ╱ | ╱ | ╱ | ╱ |   |   | ╱ | ╱ |   | ╱ | ╱ | ╱ |\n| ┗ max disk usage        | █ | █ | █ |   | █ |   |   |   | █ |   |   | █ | █ |\n| ┗ max filesize          | █ |   |   |   |   |   |   | █ |   |   | █ | █ | █ |\n| ┗ max items in folder   | █ |   |   |   |   |   |   |   |   |   |   | ╱ |   |\n| ┗ max file age          | █ |   |   |   |   |   |   |   | █ |   |   |   |   |\n| ┗ max uploads over time | █ |   |   |   |   |   |   |   |   |   |   | ╱ |   |\n| ┗ compress before write | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| ┗ randomize filename    | █ |   |   |   |   |   |   | █ | █ |   |   |   |   |\n| ┗ mimetype reject-list  | ╱ |   |   |   |   |   |   |   | • | ╱ |   | ╱ | • |\n| ┗ extension reject-list | ╱ |   |   |   |   |   |   | █ | • | ╱ |   | ╱ | • |\n| ┗ upload routing        | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| checksums provided      |   |   |   | █ | █ |   |   |   | █ | ╱ |   |   |   |\n| cloud storage backend   | ╱ | ╱ | ╱ | █ | █ | █ | ╱ |   |   | ╱ | █ | █ | ╱ |\n\n* `upload segmenting` = files are sliced into chunks, making it possible to upload files larger than 100 MiB on cloudflare for example\n\n* `upload acceleration` = each file can be uploaded using several TCP connections, which can offer a huge speed boost over huge distances / on flaky connections -- like the good old [download accelerators](https://en.wikipedia.org/wiki/GetRight) except in reverse\n\n* `upload verification` = uploads are checksummed or otherwise confirmed to have been transferred correctly\n\n* `CTRL-V from device` = press CTRL-C in Windows Explorer (or whatever) and paste into the webbrowser to upload it\n\n* `race the beam` = files can be downloaded while they're still uploading; downloaders are slowed down such that the uploader is always ahead\n\n* `tail -f` = when viewing or downloading a logfile, the connection can remain open to keep showing new lines as they are added in real time\n\n* `upload routing` = depending on filetype / contents / uploader etc., the file can be redirected to another location or otherwise transformed; mitigates limitations such as [sharex#3992](https://github.com/ShareX/ShareX/issues/3992)\n  * copyparty example: [reloc-by-ext](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks#before-upload)\n\n* `checksums provided` = when downloading a file from the server, the file's checksum is provided for verification client-side\n\n* `cloud storage backend` = able to serve files from (and write to) s3 or similar cloud services; `╱` means the software can do this with some help from `rclone mount` as a bridge\n\n* `C`/copyparty can reject uploaded files (based on complex conditions), for example [by extension](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reject-extension.py) or [mimetype](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reject-mimetype.py)\n* `sf`/seafile download-as-zip is not streaming; it creates the full zipfile before download can start, and fails on big folders\n* `fg`/filebrowser remarks:\n  * can provide checksums for single files on request\n  * can probably do extension/mimetype rejection similar to copyparty\n* `fg`/filegator download-as-zip is not streaming; it creates the full zipfile before download can start\n* `sg`/sftpgo:\n  * resumable/segmented uploads only over SFTP, not over HTTP\n  * upload rules are totals only, not over time\n  * can probably do extension/mimetype rejection similar to copyparty\n* `az`/arozos download-as-zip is not streaming; it creates the full zipfile before download can start, and fails on big folders\n\n\n## protocols and client support\n\n| feature / software      |[C]|[h2]|[h3]|[nc]|[sf]|[rc]|[df]|[cs]|[kb]|[fb]|[fg]|[sg]|[az]|\n| ----------------------- |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|\n| serve https             | █ |   | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |\n| serve webdav            | █ |   |   | █ | █ | █ | █ |   | █ |   |   | █ | █ |\n| serve ftp  (tcp)        | █ |   |   |   |   | █ |   |   |   |   |   | █ | █ |\n| serve ftps (tls)        | █ |   |   |   |   | █ |   |   |   |   |   | █ |   |\n| serve tftp (udp)        | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| serve sftp (ssh)        | █ |   |   |   |   | █ |   |   |   |   |   | █ | █ |\n| serve smb/cifs          | ╱ |   |   |   |   | █ |   |   |   |   |   |   |   |\n| serve dlna              |   |   |   |   |   | █ |   |   |   |   |   |   |   |\n| listen on unix-socket   | █ |   |   | █ | █ |   | █ | █ | █ | █ | █ | █ |   |\n| zeroconf                | █ |   |   |   |   |   |   |   |   |   |   |   | █ |\n| supports netscape 4     | ╱ |   |   |   |   | █ |   |   |   |   | • |   | ╱ |\n| ...internet explorer 6  | ╱ | █ |   | █ |   | █ |   |   |   |   | • |   | ╱ |\n| mojibake filenames      | █ |   |   | • | • | █ | █ | • | █ | • |   | ╱ |   |\n| undecodable filenames   | █ |   |   | • | • | █ |   | • |   |   |   | ╱ |   |\n\n* `webdav` = protocol convenient for mounting a remote server as a local filesystem; see zeroconf:\n* `zeroconf` = the server announces itself on the LAN, [automatically appearing](https://user-images.githubusercontent.com/241032/215344737-0eae8d98-9496-4256-9aa8-cd2f6971810d.png) on other zeroconf-capable devices\n* `mojibake filenames` = filenames decoded with the wrong codec and then reencoded (usually to utf-8), so `宇多田ヒカル` might look like `ëFæ╜ôcâqâJâï`\n* `undecodable filenames` = pure binary garbage which cannot be parsed as utf-8\n  * you can successfully play `$'\\355\\221'` with mpv through mounting a remote copyparty server with rclone, pog\n* `C`/copyparty remarks:\n  * extremely minimal samba/cifs server\n  * netscape 4 / ie6 support is mostly listed as a joke altho some people have actually found it useful ([ie4 tho](https://user-images.githubusercontent.com/241032/118192791-fb31fe00-b446-11eb-9647-898ea8efc1f7.png))\n* `sg`/sftpgo translates mojibake filenames into valid utf-8 (information loss)\n* `az`/arozos has readonly-support for older browsers; no uploading\n\n\n## server configuration\n\n| feature / software      |[C]|[h2]|[h3]|[nc]|[sf]|[rc]|[df]|[cs]|[kb]|[fb]|[fg]|[sg]|[az]|\n| ----------------------- |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|\n| config from cmd args    | █ |   | █ |   |   | █ | █ |   |   | █ |   | ╱ | ╱ |\n| config files            | █ | █ | █ | ╱ | ╱ | █ |   | █ |   | █ | • | ╱ | ╱ |\n| runtime config reload   | █ | █ | █ |   |   |   |   | █ | █ | █ | █ |   | █ |\n| same-port http / https  | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| listen multiple ports   | █ |   |   |   |   |   |   |   |   |   |   | █ |   |\n| virtual file system     | █ | █ | █ |   |   |   | █ |   |   |   |   | █ |   |\n| reverse-proxy ok        | █ |   | █ | █ | █ | █ | █ | █ | • | • | • | █ | ╱ |\n| folder-rproxy ok        | █ |   | █ |   | █ | █ |   | • | • | █ | • |   | • |\n\n* `folder-rproxy` = reverse-proxying without dedicating an entire (sub)domain, using a subfolder instead\n* `sg`/sftpgo:\n  * config: user can be added by cmd command in [Portable mode](https://docs.sftpgo.com/2.6/cli/#portable-mode); if not in  Portable mode users must be added through gui / api calls\n* `az`/arozos:\n  * configuration is primarily through GUI\n  * reverse-proxy is not guaranteed to see the correct client IP\n\n\n## server capabilities\n\n| feature / software      |[C]|[h2]|[h3]|[nc]|[sf]|[rc]|[df]|[cs]|[kb]|[fb]|[fg]|[sg]|[az]|\n| ----------------------- |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|\n| accounts                | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |\n| per-account chroot      |   |   |   |   |   |   |   |   |   |   |   | █ |   |\n| single-sign-on          | ╱ |   |   | █ | █ |   |   |   | • |   |   |   |   |\n| token auth              | ╱ |   |   | █ | █ |   |   | █ |   |   |   |   | █ |\n| 2fa                     | ╱ |   | / | █ | █ |   |   |   |   |   |   | █ | ╱ |\n| per-volume permissions  | █ | █ | █ | █ | █ | █ | █ |   | █ | █ | ╱ | █ | █ |\n| per-folder permissions  | ╱ |   | █ | █ | █ |   | █ |   | █ | █ | ╱ | █ | █ |\n| per-file permissions    |   |   | █ | █ | █ |   | █ |   | █ |   |   |   | █ |\n| per-file passwords      | █ |   |   | █ | █ |   | █ |   | █ |   |   | █ | █ |\n| unmap subfolders        | █ |   | █ |   |   |   | █ |   |   | █ | ╱ | • |   |\n| index.html blocks list  | ╱ |   |   |   |   |   | █ |   |   | • |   |   |   |\n| write-only folders      | █ |   | █ |   | █ |   |   |   |   |   | █ | █ |   |\n| files stored as-is      | █ | █ | █ | █ |   | █ | █ |   |   | █ | █ | █ | █ |\n| file versioning         |   |   |   | █ | █ |   |   |   |   |   |   |   |   |\n| file encryption         |   |   |   | █ | █ | █ |   |   |   |   |   | █ |   |\n| file indexing           | █ |   | █ | █ | █ |   |   | █ | █ | █ |   |   |   |\n| ┗ per-volume db         | █ |   | • | • | • |   |   | • | • |   |   |   |   |\n| ┗ db stored in folder   | █ |   |   |   |   |   |   | • | • | █ |   |   |   |\n| ┗ db stored out-of-tree | █ |   | █ | █ | █ |   |   | • | • | █ |   |   |   |\n| ┗ existing file tree    | █ |   | █ |   |   |   |   |   |   | █ |   |   |   |\n| file action event hooks | █ |   |   |   |   |   |   |   |   | █ |   | █ | • |\n| one-way folder sync     | █ |   |   | █ | █ | █ |   |   |   |   |   |   |   |\n| full sync               |   |   |   | █ | █ |   |   |   |   |   |   |   |   |\n| speed throttle          |   | █ | █ |   |   | █ |   |   | █ |   |   | █ |   |\n| anti-bruteforce         | █ | █ | █ | █ | █ |   |   |   | • |   |   | █ | • |\n| dyndns updater          |   | █ | █ |   |   |   |   |   |   |   |   |   |   |\n| self-updater            | ╱ |   | █ |   |   |   |   |   |   |   |   |   | █ |\n| log rotation            | █ |   | █ | █ | █ |   |   | • | █ |   |   | █ | • |\n| upload tracking / log   | █ | █ | • | █ | █ |   |   | █ | █ |   |   | ╱ | █ |\n| prometheus metrics      | █ |   |   | █ |   |   |   |   |   |   |   | █ |   |\n| curl-friendly ls        | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| curl-friendly upload    | █ |   | █ |   |   | █ | █ | • |   |   |   |   |   |\n\n* `unmap subfolders` = \"shadowing\"; mounting a local folder in the middle of an existing filesystem tree in order to disable access below that path\n* `files stored as-is` = uploaded files are trivially readable from the server HDD, not sliced into chunks or in weird folder structures or anything like that\n* `db stored in folder` = filesystem index can be written to a database file inside the folder itself\n* `db stored out-of-tree` = filesystem index can be stored some place else, not necessarily inside the shared folders\n* `existing file tree` = will index any existing files it finds\n* `file action event hooks` = run script before/after upload, move, rename, ...\n* `one-way folder sync` = like rsync, optionally deleting unexpected files at target\n* `full sync` = stateful, dropbox-like sync\n* `speed throttle` = rate limiting (per ip, per user, per connection, anything like that)\n* `curl-friendly ls` = returns a [sortable plaintext folder listing](https://user-images.githubusercontent.com/241032/215322619-ea5fd606-3654-40ad-94ee-2bc058647bb2.png) when curled\n* `curl-friendly upload` = uploading with curl is just `curl -T some.bin http://.../`\n* `C`/copyparty remarks:\n  * single-sign-on, token-auth, and 2fa is *possible* through authelia/authentik or similar, but nobody's made an example yet\n  * one-way folder sync from local to server can be done efficiently with [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy), or with webdav and conventional rsync\n  * can hot-reload config files (with just a few exceptions)\n  * can set per-folder permissions if that folder is made into a separate volume, so there is configuration overhead\n  * `index.html` on its own does not prevent directory listing, but permission `h` (instead of `r`) enforces index.html to be returned instead of folder contents\n  * [version-checker](https://github.com/9001/copyparty/#version-checker) can check if the current version has a known vulnerability and immediately exit/shutdown, but automatic self-updating is **not** available\n  * [event hooks](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks) ([discord](https://user-images.githubusercontent.com/241032/215304439-1c1cb3c8-ec6f-4c17-9f27-81f969b1811a.png), [desktop](https://user-images.githubusercontent.com/241032/215335767-9c91ed24-d36e-4b6b-9766-fb95d12d163f.png)) inspired by filebrowser, as well as the more complex [media parser](https://github.com/9001/copyparty/tree/hovudstraum/bin/mtag) alternative\n  * upload history can be visualized using [partyjournal](https://github.com/9001/copyparty/blob/hovudstraum/bin/partyjournal.py)\n* `fg`/filegator remarks:\n  * `per-* permissions` -- can limit a user to one folder and its subfolders\n  * `unmap subfolders` -- can globally filter a list of paths\n* `sg`/sftpgo:\n  * `file action event hooks` also include on-download triggers\n  * `upload tracking / log` in main logfile\n* `az`/arozos:\n  * `2fa` maybe possible through LDAP/Oauth\n* `h3`/hfs3\n  * `2fa` available by installing a plugin\n\n## client features\n\n| feature / software      |[C]|[h2]|[h3]|[nc]|[sf]|[rc]|[df]|[cs]|[kb]|[fb]|[fg]|[sg]|[az]|\n| ----------------------  |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|\n| single-page app         | █ |   | █ | █ | █ |   |   | █ | █ | █ | █ |   | █ |\n| themes                  | █ | █ | █ | █ |   |   |   |   | █ |   |   |   |   |\n| directory tree nav      | █ | ╱ |   |   | █ |   |   |   | █ |   | ╱ |   |   |\n| multi-column sorting    | █ |   |   |   |   |   |   |   |   |   |   | █ |   |\n| thumbnails              | █ |   | / | ╱ | ╱ |   |   | █ | █ | ╱ |   |   | █ |\n| ┗ image thumbnails      | █ |   | / | █ | █ |   |   | █ | █ | █ |   |   | █ |\n| ┗ video thumbnails      | █ |   |   | █ | █ |   |   |   | █ |   |   |   | █ |\n| ┗ audio spectrograms    | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| audio player            | █ |   | ╱ | █ | █ |   |   |   | █ | ╱ |   | ╱ | █ |\n| ┗ gapless playback      | █ |   |   |   |   |   |   |   | • |   |   |   |   |\n| ┗ audio equalizer       | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| ┗ waveform seekbar      | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| ┗ OS integration        | █ |   | █ |   |   |   |   |   |   |   |   |   |   |\n| ┗ transcode to lossy    | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| video player            | █ |   | █ | █ | █ |   |   |   | █ | █ |   | ╱ | █ |\n| ┗ video transcoding     |   |   | / |   |   |   |   |   | █ |   |   |   |   |\n| audio BPM detector      | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| audio key detector      | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| search by path / name   | █ | █ | █ | █ | █ |   | █ |   | █ | █ | ╱ |   |   |\n| search by date / size   | █ |   |   |   | █ |   |   | █ | █ |   |   |   |   |\n| search by bpm / key     | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| search by custom tags   |   |   |   |   |   |   |   | █ | █ |   |   |   |   |\n| search in file contents |   |   |   | █ | █ |   |   |   | █ |   |   |   |   |\n| search by custom parser | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| find local file         | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| undo recent uploads     | █ |   |   |   |   |   |   |   |   |   |   |   |   |\n| create directories      | █ |   | █ | █ | █ | ╱ | █ | █ | █ | █ | █ | █ | █ |\n| image viewer            | █ |   | █ | █ | █ |   |   |   | █ | █ | █ | █ | █ |\n| markdown viewer         | █ |   | / |   | █ |   |   |   | █ | ╱ | ╱ |   | █ |\n| markdown editor         | █ |   |   |   | █ |   |   |   | █ | ╱ | ╱ | ╱ | █ |\n| readme.md in listing    | █ |   | / | █ |   |   |   |   |   |   |   |   |   |\n| rename files            | █ | █ | █ | █ | █ | ╱ | █ |   | █ | █ | █ | █ | █ |\n| batch rename            | █ |   |   |   |   |   |   |   | █ |   |   |   |   |\n| cut / paste files       | █ | █ | █ | █ | █ |   |   |   | █ |   |   |   | █ |\n| move files              | █ | █ | █ | █ | █ |   | █ |   | █ | █ | █ | █ | █ |\n| delete files            | █ | █ | █ | █ | █ | ╱ | █ | █ | █ | █ | █ | █ | █ |\n| copy files              |   |   | / |   | █ |   |   |   | █ | █ | █ | █ | █ |\n\n* `single-page app` = multitasking; possible to continue navigating while uploading\n* `audio player » os-integration` = use the [lockscreen](https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png) or [media hotkeys](https://user-images.githubusercontent.com/241032/215347492-b4250797-6c90-4e09-9a4c-721edf2fb15c.png) to play/pause, prev/next song\n* `search by custom tags` = ability to tag files through the UI and search by those\n* `find local file` = drop a file into the browser to see if it exists on the server\n* `undo recent uploads` = accounts without delete permissions have a time window where they can undo their own uploads\n* `C`/copyparty has teeny-tiny skips playing gapless albums depending on audio codec (opus best)\n* `h2`/hfs2 has a very basic directory tree view, not showing sibling folders\n* `rc`/rclone can do some file management (mkdir, rename, delete) when hosting througn webdav\n* `fg`/filebrowser remarks:\n  * audio playback does not continue into next song\n  * plaintext viewer/editor\n* `fg`/filegator directory tree is a modal window\n* `sg`/sftpgo remarks:\n  * audio/video playback does not continue into next song/video\n  * plaintext viewer/editor\n\n\n## integration\n\n| feature / software      |[C]|[h2]|[h3]|[nc]|[sf]|[rc]|[df]|[cs]|[kb]|[fb]|[fg]|[sg]|[az]|\n| ----------------------- |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|\n| OS alert on upload      | ╱ |   |   |   |   |   |   |   |   | ╱ |   | ╱ |   |\n| discord                 | ╱ |   |   |   |   |   |   |   |   | ╱ |   | ╱ |   |\n| ┗ announce uploads      | ╱ |   |   |   |   |   |   |   |   |   |   | ╱ |   |\n| ┗ custom embeds         |   |   |   |   |   |   |   |   |   |   |   | ╱ |   |\n| sharex                  | █ |   |   | █ |   | █ | ╱ | █ |   |   |   |   |   |\n| flameshot               |   |   |   |   |   | █ |   |   |   |   |   |   |   |\n\n* sharex `╱` = yes, but does not provide example sharex config\n* `C`/copyparty remarks:\n  * `OS alert on upload` available as [a plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/notify.py)\n  * `discord » announce uploads` available as [a plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/discord-announce.py)\n* `fg`/filebrowser can probably pull those off with command runners similar to copyparty\n* `sg`/sftpgo has nothing built-in but is very extensible\n\n\n## another matrix\n\n| software / feature | lang   | lic    | size   |\n| ------------------ | ------ | ------ | ------ |\n| copyparty          | python | █ mit  | 0.6 MB |\n| hfs2               | delphi | ░ gpl3 |   2 MB |\n| hfs3               | ts     | ░ gpl3 |  36 MB |\n| nextcloud          | php    | ‼ agpl |    •   |\n| seafile            | c      | ‼ agpl |    •   |\n| rclone             | c      | █ mit  |  45 MB |\n| dufs               | rust   | █ apl2 | 2.5 MB |\n| chibisafe          | ts     | █ mit  |    •   |\n| kodbox             | php    | ░ gpl3 |  92 MB |\n| filebrowser        | go     | █ apl2 |  20 MB |\n| filegator          | php    | █ mit  |    •   |\n| sftpgo             | go     | ‼ agpl |  44 MB |\n| arozos             | go     | ░ gpl3 | 531 MB |\n| updog              | python | █ mit  |  17 MB |\n| goshs              | go     | █ mit  |  11 MB |\n| gimme-that         | python | █ mit  | 4.8 MB |\n| ass                | ts     | █ isc  |    •   |\n| linx               | go     | ░ gpl3 |  20 MB |\n| h5ai               | php    | █ mit  |    •   |\n| autoindex          | go     | █ mpl2 |  11 MB |\n| miniserve          | rust   | █ mit  |   2 MB |\n| pingvin-share      | go     | █ bsd2 | 487 MB |\n\n* `size` = binary (if available) or installed size of program and its dependencies\n  * copyparty size is for the [standalone python](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) file; the [windows exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) is **6 MiB**\n\n\n# reviews\n\n* ✅ are advantages over copyparty\n  * 💾 are what copyparty offers as an alternative\n* 🔵 are similarities\n* ⚠️ are disadvantages (something copyparty does \"better\")\n* 🔥 are hazards\n\n## [copyparty](https://github.com/9001/copyparty)\n* resumable uploads which are verified server-side\n* upload segmenting allows for potentially much faster uploads on some connections, and terabyte-sized files even on cloudflare\n  * both of the above are surprisingly uncommon features\n* very cross-platform (python, no dependencies)\n\n## [hfs2](https://github.com/rejetto/hfs2/)\n* the OG, the legend (now replaced by [hfs3](#hfs3))\n* 🔥 hfs2 is dead and dangerous! unfixed RCE: [info](https://github.com/rejetto/hfs2/issues/44), [info](https://github.com/drapid/hfs/issues/3), [info](https://asec.ahnlab.com/en/67650/)\n* ⚠️ uploads not resumable / accelerated / integrity-checked\n  * ⚠️ on cloudflare: max upload size 100 MiB\n* ⚠️ windows-only\n* ✅ config GUI\n* vfs with gui config, per-volume permissions\n* starting to show its age, hence the rewrite:\n\n## [hfs3](https://rejetto.com/hfs/)\n* nodejs; cross-platform\n* vfs with gui config, per-volume permissions\n* tested locally, v0.53.2 on archlinux\n* 🔵 uploads are resumable\n* ⚠️ uploads are not accelerated (copyparty is 3x faster across the atlantic)\n* ⚠️ uploads are not integrity-checked\n* ⚠️ uploading small files is decent; `107` files per sec (copyparty does `670`/sec, 6x faster)\n* ⚠️ doesn't support crazy filenames\n* ✅ config GUI\n* ✅ download counter\n* ✅ watch active connections\n* ✅ plugins\n\n## [nextcloud](https://github.com/nextcloud/server)\n* php, mariadb\n* tested locally, [linuxserver/nextcloud](https://hub.docker.com/r/linuxserver/nextcloud) v30.0.2 (sqlite)\n* ⚠️ [isolated on-disk file hierarchy] in per-user folders\n  * not that bad, can probably be remedied with bindmounts or maybe symlinks\n* ⚠️ uploads not resumable / accelerated / integrity-checked\n  * 🔵 uploads are segmented; no filesize limit, even on cloudflare\n* ⚠️ uploading small files is slow; `4` files per sec (copyparty does `670`/sec, 160x faster)\n* ⚠️ no write-only / upload-only folders\n* ⚠️ http/webdav only; no ftp, zeroconf\n* ⚠️ less awesome music player\n* ⚠️ doesn't run on android or ipads\n* ⚠️ AGPL licensed\n* ✅ great ui/ux\n* ✅ config gui\n* ✅ apps (android / iphone)\n  * 💾 android upload-only app + iPhone upload shortcut\n* ✅ more granular permissions (per-file)\n* ✅ search: fulltext indexing of file contents\n* ✅ webauthn passwordless authentication\n\n## [seafile](https://github.com/haiwen/seafile)\n* c, mariadb\n* tested locally, [official container](https://manual.seafile.com/latest/docker/deploy_seafile_with_docker/) v11.0.13\n* ⚠️ [isolated on-disk file hierarchy](https://manual.seafile.com/maintain/seafile_fsck/), incompatible with other software\n  * *much worse than nextcloud* in that regard\n* ⚠️ uploads not resumable / accelerated / integrity-checked\n  * ⚠️ on cloudflare: max upload size 100 MiB\n* ⚠️ uploading small files is slow; `4.7` files per sec (copyparty does `670`/sec, 140x faster)\n* ⚠️ big folders cannot be zip-downloaded\n* ⚠️ http/webdav only; no ftp, zeroconf\n* ⚠️ less awesome music player\n* ⚠️ doesn't run on android or ipads\n* ⚠️ AGPL licensed\n* ✅ great ui/ux\n* ✅ config gui\n* ✅ apps (android / iphone)\n  * 💾 android upload-only app + iPhone upload shortcut\n* ✅ more granular permissions (per-file)\n* ✅ search: fulltext indexing of file contents\n\n## [rclone](https://github.com/rclone/rclone)\n* nice standalone c program\n* ⚠️ uploads not resumable / accelerated / integrity-checked\n  * ⚠️ on cloudflare: max upload size 100 MiB\n* ⚠️ no web-ui, just a server / downloader / uploader utility\n* ✅ works with almost any protocol, cloud provider\n  * ⚠️ copyparty's webdav server is slightly faster\n\n## [dufs](https://github.com/sigoden/dufs)\n* rust; cross-platform (windows, linux, macos)\n* tested locally, v0.43.0 on archlinux (plain binary)\n* ⚠️ uploads not resumable / accelerated / integrity-checked\n  * ⚠️ on cloudflare: max upload size 100 MiB\n  * ⚠️ across the atlantic, copyparty is 3x faster\n* ⚠️ uploading small files is decent; `97` files per sec (copyparty does `670`/sec, 7x faster)\n* ⚠️ doesn't support crazy filenames\n* ✅ per-url access control (copyparty is per-volume)\n* 🔵 basic but really snappy ui\n* 🔵 upload, rename, delete, ... see feature matrix\n\n## [chibisafe](https://github.com/chibisafe/chibisafe)\n* nodejs; recommends docker\n* 🔵 *it has upload segmenting!*\n  * ⚠️ but uploads are still not resumable / accelerated / integrity-checked\n* ⚠️ not portable\n* ⚠️ isolated on-disk file hierarchy, incompatible with other software\n* ⚠️ http/webdav only; no ftp or zeroconf\n* ✅ pretty ui\n* ✅ control panel for server settings and user management\n* ✅ user registration\n* ✅ searchable image tags; delete by tag\n* ✅ browser extension to upload files to the server\n* ✅ reject uploads by file extension\n  * 💾 can reject uploads [by extension](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reject-extension.py) or [mimetype](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reject-mimetype.py) using plugins\n* ✅ token auth (api keys)\n\n## [kodbox](https://github.com/kalcaddle/kodbox)\n* this thing is insane (but is getting competition from [arozos](#arozos))\n* php; [docker](https://hub.docker.com/r/kodcloud/kodbox)\n* 🔵 *upload segmenting, acceleration, and integrity checking!*\n  * ⚠️ but uploads are not resumable(?)\n* ⚠️ not portable\n* ⚠️ isolated on-disk file hierarchy, incompatible with other software\n* ⚠️ uploading small files to copyparty is 16x faster\n* ⚠️ uploading large files to copyparty is 3x faster\n* ⚠️ http/webdav only; no ftp or zeroconf\n* ⚠️ some parts of the GUI are in chinese\n* ✅ fantastic ui/ux\n* ✅ control panel for server settings and user management\n* ✅ file tags; file discussions!?\n* ✅ video transcoding\n* ✅ unzip uploaded archives\n* ✅ IDE with syntax highlighting\n* ✅ wysiwyg editor for openoffice files\n\n## [filebrowser](https://github.com/filebrowser/filebrowser)\n* go; cross-platform (windows, linux, mac)\n* tested locally, v2.31.2 on archlinux (plain binary)\n* 🔵 uploads are resumable and segmented\n* 🔵 multiple files are uploaded in parallel, but...\n  * ⚠️ big files are not accelerated (copyparty is 5x faster across the atlantic)\n* ⚠️ uploads are not integrity-checked\n* ⚠️ uploading small files is decent; `69` files per sec (copyparty does `670`/sec, 9x faster)\n* ⚠️ http only; no webdav / ftp / zeroconf\n* ⚠️ doesn't support crazy filenames\n* ⚠️ no directory tree nav\n* ⚠️ limited file search\n* ✅ settings gui\n* ✅ good ui/ux\n  * ⚠️ but no directory tree for navigation\n* ✅ user signup\n* ✅ command runner / remote shell\n* ✅ more efficient; can handle around twice as much simultaneous traffic\n* note: keep an eye on [gtsteffaniak's fork](https://github.com/gtsteffaniak/filebrowser)\n\n## [filegator](https://github.com/filegator/filegator)\n* php; cross-platform (windows, linux, mac)\n* 🔵 *it has upload segmenting and acceleration*\n  * ⚠️ but uploads are still not integrity-checked\n  * ⚠️ on copyparty, uploads are 40x faster\n    * compared to the official filegator docker example which might be bad\n* ⚠️ http only; no webdav / ftp / zeroconf\n* ⚠️ does not support symlinks\n* ⚠️ expensive download-as-zip feature\n* ⚠️ doesn't support crazy filenames\n* ⚠️ limited file search\n\n## [sftpgo](https://github.com/drakkan/sftpgo)\n* go; cross-platform (windows, linux, mac)\n* ⚠️ http uploads not resumable / accelerated / integrity-checked\n  * ⚠️ on cloudflare: max upload size 100 MiB\n  * ⚠️ across the atlantic, copyparty is 2.5x faster\n  * 🔵 sftp uploads are resumable\n* ⚠️ web UI is very minimal + a bit slow\n  * ⚠️ no thumbnails\n* ⚠️ no filesystem indexing / search\n* ⚠️ no zeroconf (mdns/ssdp)\n* ⚠️ impractical directory URLs\n* ⚠️ AGPL licensed\n* 🔵 uploading small files is fast; `340` files per sec (copyparty does `670`/sec)\n* 🔵 sftp, ftp, ftps, webdav\n* ✅ settings gui\n* ✅ acme (automatic tls certs)\n  * 💾 relies on caddy/certbot/acme.sh\n* ✅ at-rest encryption\n  * 💾 relies on LUKS/BitLocker\n* ✅ can use S3/GCS as storage backend\n  * 💾 relies on rclone-mount\n* ✅ on-download event hook (otherwise same as copyparty)\n* ✅ more extensive permissions control\n\n## [arozos](https://github.com/tobychui/arozos)\n* big suite of applications similar to [kodbox](#kodbox), copyparty is better at downloading/uploading/music/indexing but arozos has other advantages\n* go; primarily linux (limited support for windows)\n* ⚠️ needs root\n* ⚠️ uploads not resumable / integrity-checked\n* ⚠️ uploading small files to copyparty is 2.7x faster\n* ⚠️ uploading large files to copyparty is at least 10% faster\n  * arozos is websocket-based, 512 KiB chunks; writes each chunk to separate files and then merges\n  * copyparty splices directly into the final file; faster and better for the HDD and filesystem\n* ⚠️ across the atlantic, uploading to copyparty is 6x faster\n* ⚠️ no directory tree navpane; not as easy to navigate\n* ⚠️ download-as-zip is not streaming; creates a temp.file on the server\n* ⚠️ not self-contained (pulls from jsdelivr)\n* ⚠️ has an audio player, but supports less filetypes\n* ⚠️ limited support for configuring real-ip detection\n* ✅ settings gui\n* ✅ good-looking gui\n* ✅ an IDE, msoffice viewer, rich host integration, much more\n\n## [updog](https://github.com/sc0tfree/updog)\n* python; cross-platform\n* basic directory listing with upload feature\n* ⚠️ less portable\n* ⚠️ uploads not resumable / accelerated / integrity-checked\n  * ⚠️ on cloudflare: max upload size 100 MiB\n* ⚠️ no vfs; single folder, single account\n\n## [goshs](https://github.com/patrickhener/goshs)\n* go; cross-platform (windows, linux, mac)\n* ⚠️ no vfs; single folder, single account\n* ⚠️ uploads not resumable / accelerated / integrity-checked\n  * ⚠️ on cloudflare: max upload size 100 MiB\n* ✅ cool clipboard widget\n  * 💾 the markdown editor is an ok substitute\n* 🔵 read-only and upload-only modes (same as copyparty's write-only)\n* 🔵 https, webdav, but no ftp\n\n## [gimme-that](https://github.com/nejdetckenobi/gimme-that)\n* python, but with c dependencies\n* ⚠️ no vfs; single folder, multiple accounts\n* ⚠️ uploads not resumable / accelerated / integrity-checked\n  * ⚠️ on cloudflare: max upload size 100 MiB\n* ⚠️ weird folder structure for uploads\n* ✅ clamav antivirus check on upload! neat\n* 🔵 optional max-filesize, os-notification on uploads\n  * 💾 os-notification available as [a plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/notify.py)\n\n## [ass](https://github.com/tycrek/ass)\n* nodejs; recommends docker\n* ⚠️ not portable\n* ⚠️ upload only; no browser\n* ⚠️ upload through sharex only; no web-ui\n* ⚠️ uploads not resumable / accelerated / integrity-checked\n  * ⚠️ on cloudflare: max upload size 100 MiB\n* ✅ token auth\n* ✅ gps metadata stripping\n  * 💾 possible with [a plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/image-noexif.py)\n* ✅ discord integration (custom embeds, upload webhook)\n  * 💾 [upload webhook plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/discord-announce.py)\n* ✅ reject uploads by mimetype\n  * 💾 can reject uploads [by extension](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reject-extension.py) or [mimetype](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reject-mimetype.py) using plugins\n* ✅ can use S3 as storage backend\n  * 💾 relies on rclone-mount\n* ✅ custom 404 pages\n\n## [linx](https://github.com/ZizzyDizzyMC/linx-server/)\n* originally [andreimarcu/linx-server](https://github.com/andreimarcu/linx-server) but development has ended\n* ⚠️ uploads not resumable / accelerated / integrity-checked\n  * ⚠️ on cloudflare: max upload size 100 MiB\n* 🔵 some of its unique features have been added to copyparty as former linx users have migrated\n  * file expiration timers, filename randomization\n* ✅ password-protected files\n  * 💾 password-protected folders + filekeys to skip the folder password seem to cover most usecases\n* ✅ file deletion keys\n* ✅ download files as torrents\n* ✅ remote uploads (send a link to the server and it downloads it)\n  * 💾 available as [a plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/wget.py)\n* ✅ can use S3 as storage backend\n  * 💾 relies on rclone-mount\n\n## [h5ai](https://larsjung.de/h5ai/)\n* ⚠️ read only; no upload/move/delete\n* ⚠️ search hits the filesystem directly; not indexed/cached\n* ✅ slick ui\n* ✅ in-browser qr generator to share URLs\n* 🔵 directory tree, image viewer, thumbnails, download-as-tar\n\n## [autoindex](https://github.com/nielsAD/autoindex)\n* ⚠️ read only; no upload/move/delete\n* ✅ directory cache for faster browsing of cloud storage\n  * 💾 local index/cache for recursive search (names/attrs/tags), but not for browsing\n\n## [miniserve](https://github.com/svenstaro/miniserve)\n* rust; cross-platform (windows, linux, mac)\n* ⚠️ uploads not resumable / accelerated / integrity-checked\n  * ⚠️ on cloudflare: max upload size 100 MiB\n* ⚠️ no thumbnails / image viewer / audio player / file manager\n* ⚠️ no filesystem indexing / search\n* 🔵 upload, tar/zip download, qr-code\n* ✅ faster at loading huge folders\n\n## [pingvin-share](https://github.com/stonith404/pingvin-share)\n* node; linux (docker)\n* mainly for uploads, not a general file server\n* 🔵 uploads are segmented (avoids cloudflare size limit)\n* 🔵 segments are written directly to target file (HDD-friendly)\n* ⚠️ uploads not resumable after a browser or laptop crash\n* ⚠️ uploads are not accelerated / integrity-checked\n  * ⚠️ across the atlantic, copyparty is 3x faster\n    * measured with chunksize 96 MiB; pingvin's default 10 MiB is much slower\n* ⚠️ can't upload folders with subfolders\n* ⚠️ no upload ETA\n* 🔵 expiration times, shares, upload-undo\n* ✅ config + user-registration gui\n* ✅ built-in OpenID and LDAP support\n  * 💾 [IdP middleware](https://github.com/9001/copyparty#identity-providers) and config-files\n* ✅ probably more than one person who understands the code\n\n\n\n# briefly considered\n* [pydio](https://github.com/pydio/cells): python/agpl3, looks great, fantastic ux -- but needs mariadb, systemwide install\n* [gossa](https://github.com/pldubouilh/gossa): go/mit, minimalistic, basic file upload, text editor, mkdir and rename (no delete/move)\n\n\n\n# notes\n\n* high-latency connections (cross-atlantic uploads) can be accurately simulated with `tc qdisc add dev eth0 root netem delay 100ms`\n"
  },
  {
    "path": "docs/xff.md",
    "content": "when running behind a reverse-proxy, or a WAF, or another protection service such as cloudflare:\n\nif 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\n\nknowing the correct IP is also crucial for some other features, such as the unpost feature which lets you delete your own recent uploads -- but if everybody has the same IP, well...\n\n----\n\nfor most common setups, there should be a helpful message in the server-log explaining what to do, something like `--xff-src=10.88.0.0/16` or `--xff-src=lan` to accept the `X-Forwarded-For` header from your reverse-proxy with a LAN IP of `10.88.x.y`\n\nif you are behind cloudflare, it is recommended to also set `--xff-hdr=cf-connecting-ip` to use a more trustworthy source of info, but then it's also very important to ensure your reverse-proxy does not accept connections from anything BUT cloudflare; you can do this by generating an ip-address allowlist and reject all other connections\n\n* if you are using nginx as your reverse-proxy, see the [example nginx config](https://github.com/9001/copyparty/blob/hovudstraum/contrib/nginx/copyparty.conf) on how the cloudflare allowlist can be done\n\n----\n\nthe server-log will give recommendations in the form of commandline arguments;\n\nto do the same thing using config files, take the options that are suggested in the serverlog and put them into the `[global]` section in your `copyparty.conf` like so:\n\n```yaml\n[global]\n  xff-src: lan\n  xff-hdr: cf-connecting-ip\n```\n\n----\n\n# but if you just want to get it working:\n\n...and don't care about security, you can optionally disable the bot-detectors, either by specifying commandline-args `--ban-404=no --ban-403=no --ban-422=no --ban-url=no --ban-pw=no`\n\nor by adding these lines inside the `[global]` section in your `copyparty.conf`:\n\n```yaml\n[global]\n  ban-404: no\n  ban-403: no\n  ban-422: no\n  ban-url: no\n  ban-pw: no\n```\n\nbut remember that this will make other features insecure as well, such as unpost\n\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  inputs = {\n    nixpkgs.url = \"nixpkgs/nixos-25.05\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n  outputs =\n    {\n      self,\n      nixpkgs,\n      flake-utils,\n    }:\n    {\n      nixosModules.default = ./contrib/nixos/modules/copyparty.nix;\n      overlays.default = final: prev:\n        (import ./contrib/package/nix/overlay.nix final prev) // { copypartyFlake = self; };\n    }\n    // flake-utils.lib.eachDefaultSystem (\n      system:\n      let\n        pkgs = import nixpkgs {\n          inherit system;\n          config = {\n            allowAliases = false;\n          };\n          overlays = [\n            self.overlays.default\n          ];\n        };\n      in\n      {\n        # check that copyparty builds with all optionals turned on\n        checks = {\n          inherit (pkgs)\n            copyparty-full\n            copyparty-unstable-full\n            ;\n        };\n\n        packages = {\n          inherit (pkgs)\n            copyparty\n            copyparty-full\n            copyparty-unstable\n            copyparty-unstable-full\n            ;\n          default = self.packages.${system}.copyparty;\n        };\n\n        formatter = pkgs.nixfmt-tree;\n      }\n    );\n}\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"copyparty\"\ndescription = \"\"\"\n  Portable file server with accelerated resumable uploads, \\\n  deduplication, WebDAV, SFTP, FTP, zeroconf, media indexer, \\\n  video thumbnails, audio transcoding, and write-only folders\"\"\"\nreadme = \"README.md\"\nauthors = [{ name = \"ed\", email = \"copyparty@ocv.me\" }]\nlicense = { text = \"MIT\" }\nrequires-python = \">=3.3\"\ndependencies = [\"Jinja2\"]\ndynamic = [\"version\"]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.3\",\n    \"Programming Language :: Python :: 3.4\",\n    \"Programming Language :: Python :: 3.5\",\n    \"Programming Language :: Python :: 3.6\",\n    \"Programming Language :: Python :: 3.7\",\n    \"Programming Language :: Python :: 3.8\",\n    \"Programming Language :: Python :: 3.9\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Programming Language :: Python :: Implementation :: CPython\",\n    \"Programming Language :: Python :: Implementation :: Jython\",\n    \"Programming Language :: Python :: Implementation :: PyPy\",\n    \"Operating System :: OS Independent\",\n    \"Environment :: Console\",\n    \"Environment :: No Input/Output (Daemon)\",\n    \"Intended Audience :: End Users/Desktop\",\n    \"Intended Audience :: System Administrators\",\n    \"Topic :: Communications :: File Sharing\",\n    \"Topic :: Internet :: File Transfer Protocol (FTP)\",\n    \"Topic :: Internet :: WWW/HTTP :: HTTP Servers\",\n]\n\n[project.urls]\n\"Source Code\" = \"https://github.com/9001/copyparty\"\n\"Bug Tracker\" = \"https://github.com/9001/copyparty/issues\"\n\"Demo Server\" = \"https://a.ocv.me/pub/demo/\"\n\n[project.optional-dependencies]\nall = [\n    \"argon2-cffi\",\n    \"paramiko\",\n    \"partftpy>=0.4.0\",\n    \"Pillow\",\n    \"pyftpdlib\",\n    \"pyopenssl\",\n    \"pyzmq\",\n]\nthumbnails = [\"Pillow\"]\nthumbnails2 = [\"pyvips\"]\naudiotags = [\"mutagen\"]\nsftp = [\"paramiko\"]\nftpd = [\"pyftpdlib\"]\nftps = [\"pyftpdlib\", \"pyopenssl\"]\ntftpd = [\"partftpy>=0.4.0\"]\npwhash = [\"argon2-cffi\"]\nzeromq = [\"pyzmq\"]\n\n[project.scripts]\ncopyparty = \"copyparty.__main__:main\"\n\"u2c\" = \"copyparty.web.a.u2c:main\"\n\"partyfuse\" = \"copyparty.web.a.partyfuse:main\"\n\n# =====================================================================\n\n[build-system]\nrequires = [\"setuptools\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n# requires = [\"hatchling\"]\n# build-backend = \"hatchling.build\"\n\n[tool.hatch.version]\nsource = \"code\"\npath = \"copyparty/__version__.py\"\n\n[tool.setuptools.dynamic]\nversion = { attr = \"copyparty.__version__.__version__\" }\n\n[tool.setuptools.packages.find]\ninclude = [\"copyparty*\"]\n\n[tool.setuptools.package-data]\ncopyparty = [\n    \"res/COPYING.txt\",\n    \"res/insecure.pem\",\n    \"web/*.gz\",\n    \"web/*.js\",\n    \"web/*.css\",\n    \"web/*.html\",\n    \"web/*.xml\",\n    \"web/tl/*.js\",\n    \"web/tl/*.gz\",\n    \"web/a/*.txt\",\n    \"web/a/*.gz\",\n    \"web/deps/*.gz\",\n    \"web/deps/*.woff*\",\n]\n\n# =====================================================================\n\n[tool.black]\nrequired-version = '21.12b0'\ntarget-version = ['py27']\n\n[tool.isort]\nprofile = \"black\"\ninclude_trailing_comma = true\n\n[tool.bandit]\nskips = [\"B104\", \"B110\", \"B112\"]\n\n[tool.ruff]\nline-length = 120\nignore = [\"E402\", \"E722\"]\n\n# =====================================================================\n\n[tool.pylint.MAIN]\npy-version = \"3.11\"\njobs = 2\n\n[tool.pylint.\"MESSAGES CONTROL\"]\ndisable = [\n    \"missing-module-docstring\",\n    \"missing-class-docstring\",\n    \"missing-function-docstring\",\n    \"import-outside-toplevel\",\n    \"wrong-import-position\",\n    \"raise-missing-from\",\n    \"bare-except\",\n    \"broad-exception-raised\",\n    \"broad-exception-caught\",\n    \"invalid-name\",\n    \"line-too-long\",\n    \"too-many-lines\",\n    \"consider-using-f-string\",\n    \"pointless-string-statement\",\n]\n\n[tool.pylint.FORMAT]\nexpected-line-ending-format = \"LF\"\n\n# =====================================================================\n\n[tool.mypy]\npython_version = \"3.11\"\nfiles = [\"copyparty\"]\nshow_error_codes = true\nshow_column_numbers = true\npretty = true\nstrict = true\nlocal_partial_types = true\nstrict_equality = true\nwarn_unreachable = true\nignore_missing_imports = true\nfollow_imports = \"silent\"\n\n[[tool.mypy.overrides]]\nno_implicit_reexport = false\n"
  },
  {
    "path": "scripts/bench/filehash.sh",
    "content": "#!/bin/bash\nset -euo pipefail\n\n# check how fast copyparty is able to hash files during indexing\n# assuming an infinitely fast HDD to read from (alternatively,\n# checks whether you will be bottlenecked by CPU or HDD)\n#\n# uses copyparty's default config of using, well, it's complicated:\n# * if you have more than 8 cores, then 5 threads,\n# * if you have between 4 and 8, then 4 threads,\n# * anything less and it takes your number of cores\n#\n# can be adjusted with --hash-mt (but alpine caps out at 5)\n\nfsize=256\nnfiles=128\npybin=$(command -v python3 || command -v python)\n#pybin=~/.pyenv/versions/nogil-3.9.10-2/bin/python3\n\n[ $# -ge 1 ] || {\n\techo 'need arg 1: path to copyparty-sfx.py'\n\techo ' (remaining args will be passed on to copyparty,'\n\techo '  for example to tweak the hasher settings)'\n\texit 1\n}\nsfx=\"$1\"\nshift\nsfx=\"$(realpath \"$sfx\" || readlink -e \"$sfx\" || echo \"$sfx\")\"\nawk=$(command -v gawk || command -v awk)\nuname -s | grep -E MSYS && win=1 || win=\ntotalsize=$((fsize*nfiles))\n\n# try to use /dev/shm to avoid hitting filesystems at all,\n# otherwise fallback to mktemp which probably uses /tmp\ntd=/dev/shm/cppbenchtmp\nmkdir $td || td=$(mktemp -d)\ntrap \"rm -rf $td\" INT TERM EXIT\ncd $td\n\necho creating $fsize MiB testfile in $td\nsz=$((1024*1024*fsize))\nhead -c $sz /dev/zero | openssl enc -aes-256-ctr -iter 1 -pass pass:k -nosalt 2>/dev/null >1 || true\nwc -c 1 | awk '$1=='$sz'{r=1}END{exit 1-r}' || head -c $sz /dev/urandom >1\n\necho creating $((nfiles-1)) symlinks to it\nfor n in $(seq 2 $nfiles); do MSYS=winsymlinks:nativestrict ln -s 1 $n; done\n\necho warming up cache\ncat 1 >/dev/null\n\necho ok lets go\n$pybin \"$sfx\" -p39204 -e2dsa --dbd=yolo --exit=idx -lo=t -q \"$@\" && err= || err=$?\n[ $win ] && [ $err = 15 ] && err=  # sigterm doesn't hook on windows, ah whatever\n[ $err ] && echo ERROR $err && exit $err\n\necho and the results are...\nLC_ALL=C $awk '/1 volumes in / {s=$(NF-1); printf \"speed: %.1f MiB/s  (time=%.2fs)\\n\", '$totalsize'/s, s}' <t\n\necho deleting $td and exiting\n\n##\n## some results:\n\n# MiB/s @ cpu or device  (copyparty, pythonver, distro/os)  // comment\n\n#  3887 @ Ryzen 5 4500U  (cpp 1.9.5, nogil 3.9, fedora 39)  // --hash-mt=6; laptop\n#  3732 @ Ryzen 5 4500U  (cpp 1.9.5, py 3.12.1, fedora 39)  // --hash-mt=6; laptop\n#  3608 @ Ryzen 5 4500U  (cpp 1.9.5, py 3.11.5, fedora 38)  // --hash-mt=6; laptop\n#  2726 @ Ryzen 5 4500U  (cpp 1.9.5, py 3.11.5, fedora 38)  // --hash-mt=4 (old-default)\n#  2202 @ Ryzen 5 4500U  (cpp 1.9.5, py 3.11.5, docker-alpine 3.18.3) ??? alpine slow\n#  2719 @ Ryzen 5 4500U  (cpp 1.9.5, py 3.11.2, docker-debian 12.1)\n\n#  7746 @ mbp 2023 m3pro (cpp 1.9.5, py 3.11.7, macos 14.1)  // --hash-mt=6\n#  6687 @ mbp 2023 m3pro (cpp 1.9.5, py 3.11.7, macos 14.1)  // --hash-mt=5 (default)\n#  5544 @ Intel i5-12500 (cpp 1.9.5, py 3.11.2, debian 12.0)  // --hash-mt=12; desktop\n#  5197 @ Ryzen 7 3700X  (cpp 1.9.5, py 3.9.18, freebsd 13.2)  // --hash-mt=8; 2u server\n#  4551 @ mbp 2020 m1    (cpp 1.9.5, py 3.11.7, macos 14.2.1)\n#  4190 @ Ryzen 7 5800X  (cpp 1.9.5, py 3.11.6, fedora 37)  // --hash-mt=8 (vbox-VM on win10-17763.4974)\n#  3028 @ Ryzen 7 5800X  (cpp 1.9.5, py 3.11.6, fedora 37)  // --hash-mt=5 (vbox-VM on win10-17763.4974)\n#  2629 @ Ryzen 7 5800X  (cpp 1.9.5, py 3.11.7, win10-ltsc-1809-17763.4974)  // --hash-mt=5 (default)\n#  2576 @ Ryzen 7 5800X  (cpp 1.9.5, py 3.11.7, win10-ltsc-1809-17763.4974)  // --hash-mt=8 (hello??)\n#  2606 @ Ryzen 7 3700X  (cpp 1.9.5, py 3.9.18, freebsd 13.2)  // --hash-mt=4 (old-default)\n#  1436 @ Ryzen 5 5500U  (cpp 1.9.5, py 3.11.4, alpine 3.18.3)  // nuc\n#  1065 @ Pixel 7        (cpp 1.9.5, py 3.11.5, termux 2023-09)\n#   945 @ Pi 5B v1.0     (cpp 1.9.5, py 3.11.6, alpine 3.19.0)\n#   548 @ Pi 4B v1.5     (cpp 1.9.5, py 3.11.6, debian 11)\n#   435 @ Pi 4B v1.5     (cpp 1.9.5, py 3.11.6, alpine 3.19.0)\n#   212 @ Pi Zero2W v1.0 (cpp 1.9.5, py 3.11.6, alpine 3.19.0)\n#  10.0 @ Pi Zero W v1.1 (cpp 1.9.5, py 3.11.6, alpine 3.19.0)\n\n# notes,\n# podman run --rm -it --shm-size 512m --entrypoint /bin/ash localhost/copyparty-min\n# podman <filehash.sh run --rm -i --shm-size 512m --entrypoint /bin/ash localhost/copyparty-min -s - /z/copyparty-sfx.py\n"
  },
  {
    "path": "scripts/copyparty-android.sh",
    "content": "#!/bin/bash\nset -e\n\n_msg() { printf \"$2\"'\\033[1;30m>\\033[0;33m>\\033[1m>\\033[0m %s\\n' \"$1\" >&2; }\nimsg() { _msg \"$1\" ''; }\nmsg() { _msg \"$1\" \\\\n; }\n\n##\n## helper which installs termux packages\n\ntermux_upd=y\naddpkg() {\n\tt0=$(date +%s -r ~/../usr/var/cache/apt/pkgcache.bin 2>/dev/null || echo 0)\n\tt1=$(date +%s)\n\n\t[ $((t1-t0)) -gt 600 ] && {\n\t\tmsg \"upgrading termux packages\"\n\t\tapt update\n\t\tapt full-upgrade -y\n\t}\n\tmsg \"installing $1 from termux repos\"\n\tapt install -y $1\n}\n\n##\n## ensure python is available\n\ncommand -v python3 >/dev/null ||\n\taddpkg python\n\n##\n## ensure virtualenv and dependencies are available\n\nve=$HOME/ve.copyparty\n\n[ -e $ve/.ok ] || (\n\trm -rf $ve\n\n\tmsg \"creating python3 virtualenv\"\n\tpython3 -m venv $ve\n\n\tmsg \"installing copyparty\"\n\t. $ve/bin/activate\n\tpip install copyparty\n\n\tdeactivate\n\ttouch $ve/.ok\n)\n\n##\n## add copyparty alias to bashrc\n\ngrep -qE '^alias copyparty=' ~/.bashrc 2>/dev/null || {\n\tmsg \"adding alias to bashrc\"\n\techo \"alias copyparty='$HOME/copyparty-android.sh'\" >> ~/.bashrc\n}\n\n##\n## start copyparty\n\nimsg \"starting copyparty\"\n$ve/bin/python -m copyparty \"$@\"\n"
  },
  {
    "path": "scripts/copyparty-repack.sh",
    "content": "#!/bin/bash\nrepacker=1\nset -e\n\n# -- download latest copyparty (source.tgz and sfx),\n# -- build minimal sfx versions,\n# -- create a .tar.gz bundle\n#\n# convenient for deploying updates to inconvenient locations\n#  (and those are usually linux so bash is good inaff)\n#   (but that said this even has macos support)\n#\n# output summary (filesizes and contents):\n#\n# 550760  copyparty-extras/sfx-full/copyparty-sfx.py\n#           `- original unmodified sfx from github\n#\n# 572923  copyparty-extras/sfx-full/copyparty-sfx-gz.py\n#           `- unmodified but recompressed from bzip2 to gzip\n#\n# 353975  copyparty-extras/sfx-ent/copyparty-sfx.py\n# 376934  copyparty-extras/sfx-ent/copyparty-sfx-gz.py\n#           `- removed iOS ogg/opus/vorbis audio decoder,\n#              removed the audio tray mouse cursor,\n#              \"enterprise edition\"\n#\n# 270004  copyparty-extras/sfx-lite/copyparty-sfx.py\n# 293159  copyparty-extras/sfx-lite/copyparty-sfx-gz.py\n#           `- also removed the codemirror markdown editor\n#              and the text-viewer syntax hilighting,\n#              only essential features remaining\n#\n# 646297  copyparty-extras/copyparty-1.0.14.tar.gz\n#   4823  copyparty-extras/copyparty-repack.sh\n#           `- source files from github\n#\n#  23663  copyparty-extras/u2c.py\n#           `- standalone utility to upload or search for files\n#\n#  32280  copyparty-extras/partyfuse.py\n#           `- standalone to mount a URL as a local read-only filesystem\n#\n# 270004  copyparty\n#           `- minimal binary, same as sfx-lite/copyparty-sfx.py\n\n\ncommand -v gnutar && tar() { gnutar \"$@\"; }\ncommand -v gtar && tar() { gtar \"$@\"; }\ncommand -v gsed && sed() { gsed \"$@\"; }\ntd=\"$(mktemp -d)\"\nod=\"$(pwd)\"\ncd \"$td\"\npwd\n\n\ndl_text() {\n\tcommand -v curl >/dev/null && exec curl \"$@\"\n\texec wget -O- \"$@\"\n}\ndl_files() {\n\tcommand -v curl >/dev/null && exec curl -L --remote-name-all \"$@\"\n\texec wget \"$@\"\n}\nexport -f dl_files\n\n\n# if cache exists, use that instead of bothering github\ncache=\"$od/.copyparty-repack.cache\"\n[ -e \"$cache\" ] &&\n\ttar -xf \"$cache\" ||\n{\n\t# get download links from github\n\tdl_text https://api.github.com/repos/9001/copyparty/releases/latest |\n\t(\n\t\t# prefer jq if available\n\t\tjq -r '.assets[]|select(.name|test(\"-sfx|tar.gz\")).browser_download_url' ||\n\n\t\t# fallback to awk (sorry)\n\t\tawk -F\\\" '/\"browser_download_url\".*(\\.tar\\.gz|-sfx\\.)/ {print$4}'\n\t) |\n\tgrep -E '(sfx\\.py|tar\\.gz)$' |\n\ttee /dev/stderr |\n\ttr -d '\\r' | tr '\\n' '\\0' |\n\txargs -0 bash -c 'dl_files \"$@\"' _\n\n\ttar -czf \"$cache\" *\n}\n\n\n# move src into copyparty-extras/,\n# move sfx into copyparty-extras/sfx-full/\nmkdir -p copyparty-extras/sfx-{full,ent,lite}\nmv copyparty-sfx.* copyparty-extras/sfx-full/\nmv copyparty-*.tar.gz copyparty-extras/\n\n\n# unpack the source code\n( cd copyparty-extras/\ntar -xf *.tar.gz\n)\n\n\n# use repacker from release if that is newer\np_other=copyparty-extras/copyparty-*/scripts/copyparty-repack.sh\nother=$(awk -F= 'BEGIN{v=-1} NR<10&&/^repacker=/{v=$NF} END{print v}' <$p_other) \n[ $repacker -lt $other ] &&\n  cat $p_other >\"$od/$0\" && cd \"$od\" && rm -rf \"$td\" && exec \"$0\" \"$@\"\n\n\n# now drop the cache\nrm -f \"$cache\"\n\n\n# fix permissions\nchmod 755 \\\n  copyparty-extras/sfx-full/* \\\n  copyparty-extras/copyparty-*/{scripts,bin}/*\n\n\n# extract the sfx\n( cd copyparty-extras/sfx-full/\n./copyparty-sfx.py --version\n)\n\n\nrepack() {\n\n\t# do the repack\n\t(cd copyparty-extras/copyparty-*/\n\t./scripts/make-sfx.sh $2\n\t)\n\n\t# put new sfx into copyparty-extras/$name/,\n\t( cd copyparty-extras/\n\tmv copyparty-*/dist/* $1/\n\t)\n}\n\nrepack sfx-full \"re gz\"\nrepack sfx-ent  \"re\"\nrepack sfx-ent  \"re gz\"\nrepack sfx-lite \"re no-cm no-hl\"\nrepack sfx-lite \"re no-cm no-hl gz\"\n\n\n# move fuse and up2k clients into copyparty-extras/,\n# copy lite-sfx.py to ./copyparty,\n# delete extracted source code\n( cd copyparty-extras/\nmv copyparty-*/bin/u2c.py .\nmv copyparty-*/bin/partyfuse.py .\ncp -pv sfx-lite/copyparty-sfx.py ../copyparty\nrm -rf copyparty-{0..9}*.*.*{0..9}\n)\n\n\n# and include the repacker itself too\ncp -av \"$od/$0\" copyparty-extras/ ||\ncp -av \"$0\" copyparty-extras/ ||\ntrue\n\n\n# create the bundle\nprintf '\\n\\n'\nfn=copyparty-$(date +%Y-%m%d-%H%M%S).tgz\ntar -czvf \"$od/$fn\" *\ncd \"$od\"\nrm -rf \"$td\"\n\n\necho\necho \"done, here's your bundle:\"\nls -al \"$fn\"\n"
  },
  {
    "path": "scripts/deps-docker/Dockerfile",
    "content": "FROM    alpine:3.23\nWORKDIR /z\nENV     ver_hashwasm=4.12.0 \\\n        ver_marked=4.3.0 \\\n        ver_dompf=3.3.1 \\\n        ver_mde=2.18.0 \\\n        ver_codemirror=5.65.18 \\\n        ver_fontawesome=5.13.0 \\\n        ver_prism=1.30.0 \\\n        ver_zopfli=1.0.3\n\n# versioncheck:\n# https://github.com/markedjs/marked/releases\n# https://github.com/Ionaru/easy-markdown-editor/tags  # ignore 2.20.0\n# https://github.com/codemirror/codemirror5/releases\n# https://github.com/cure53/DOMPurify/releases\n# https://github.com/Daninet/hash-wasm/releases\n# https://github.com/google/zopfli/tags\n\n\n# download;\n# the scp url is regular latin from https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap\nRUN     mkdir -p /z/dist/no-pk \\\n        && wget https://fonts.gstatic.com/s/sourcecodepro/v11/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2 -O scp.woff2 \\\n        && apk add \\\n                bash brotli cmake make g++ git gzip lame npm patch pigz \\\n                python3 python3-dev py3-brotli sox tar unzip wget \\\n        && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \\\n        && wget https://github.com/markedjs/marked/archive/v$ver_marked.tar.gz -O marked.tgz \\\n        && wget https://github.com/Ionaru/easy-markdown-editor/archive/$ver_mde.tar.gz -O mde.tgz \\\n        && wget https://github.com/codemirror/codemirror5/archive/$ver_codemirror.tar.gz -O codemirror.tgz \\\n        && wget https://github.com/cure53/DOMPurify/archive/refs/tags/$ver_dompf.tar.gz -O dompurify.tgz \\\n        && wget https://github.com/FortAwesome/Font-Awesome/releases/download/$ver_fontawesome/fontawesome-free-$ver_fontawesome-web.zip -O fontawesome.zip \\\n        && wget https://github.com/google/zopfli/archive/zopfli-$ver_zopfli.tar.gz -O zopfli.tgz \\\n        && wget https://github.com/Daninet/hash-wasm/releases/download/v$ver_hashwasm/hash-wasm@$ver_hashwasm.zip -O hash-wasm.zip \\\n        && wget https://github.com/PrismJS/prism/archive/refs/tags/v$ver_prism.tar.gz -O prism.tgz \\\n        && wget https://files.pythonhosted.org/packages/04/0b/4506cb2e831cea4b0214d3625430e921faaa05a7fb520458c75a2dbd2152/fusepy-3.0.1.tar.gz -O fusepy.tgz \\\n        && (mkdir hash-wasm \\\n            && cd hash-wasm \\\n            && unzip ../hash-wasm.zip) \\\n        && (tar --no-same-owner -xf marked.tgz \\\n            && cd marked-$ver_marked \\\n            && npm install \\\n            && npm i grunt uglify-js -g ) \\\n        && (tar --no-same-owner -xf codemirror.tgz \\\n            && cd codemirror5-$ver_codemirror \\\n            && npm install ) \\\n        && (tar --no-same-owner -xf mde.tgz \\\n            && cd easy-markdown-editor* \\\n            && npm install \\\n            && npm i gulp-cli -g ) \\\n        && tar --no-same-owner -xf dompurify.tgz \\\n        && tar --no-same-owner -xf prism.tgz \\\n        && tar --no-same-owner -xf fusepy.tgz \\\n        && unzip fontawesome.zip \\\n        && tar --no-same-owner -xf zopfli.tgz\n\n\n#COPY    busy-mp3.sh /z/\n#RUN     /z/busy-mp3.sh \\\n#        && mv -v /dev/shm/busy.mp3.gz /z/dist\n\n\n# build fonttools (which needs zopfli)\nRUN     tar --no-same-owner -xf zopfli.tgz \\\n        && cd zopfli* \\\n        && cmake \\\n            -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \\\n            -DCMAKE_INSTALL_PREFIX=/usr \\\n            -DZOPFLI_BUILD_SHARED=ON \\\n            -B build \\\n            -S . \\\n        && make -C build \\\n        && make -C build install \\\n        && python3 -m ensurepip \\\n        && python3 -m pip install fonttools zopfli\n\n\n# build hash-wasm\nRUN     cd hash-wasm/dist \\\n        && mv sha512.umd.min.js /z/dist/sha512.hw.js\n\n\n# build marked\nCOPY    marked.patch /z/\nCOPY    marked-ln.patch /z/\nRUN     cd marked-$ver_marked \\\n        && patch -p1 < /z/marked-ln.patch \\\n        && patch -p1 < /z/marked.patch \\\n        && npm run build \\\n        && cp -pv marked.min.js /z/dist/marked.js \\\n        && mkdir -p /z/nodepkgs \\\n        && ln -s $(pwd) /z/nodepkgs/marked\n#        && npm run test \\\n\n\n# build codemirror\nCOPY    codemirror.patch /z/\nRUN     cd codemirror5-$ver_codemirror \\\n        && patch -p1 < /z/codemirror.patch \\\n        && sed -ri '/^var urlRE = /d' mode/gfm/gfm.js \\\n        && npm run build \\\n        && ln -s $(pwd) /z/nodepkgs/codemirror\n\n\n# build easymde\nCOPY    easymde.patch /z/\nRUN     cd easy-markdown-editor-$ver_mde \\\n        && patch -p1 < /z/easymde.patch \\\n        && sed -ri 's`https://registry.npmjs.org/marked/-/marked-[0-9\\.]+.tgz`file:/z/nodepkgs/marked`' package-lock.json \\\n        && sed -ri 's`https://registry.npmjs.org/codemirror/-/codemirror-[0-9\\.]+.tgz`file:/z/nodepkgs/codemirror`' package-lock.json \\\n        && sed -ri 's`(\"marked\": \")[^\"]+`\\1file:/z/nodepkgs/marked`' ./package.json \\\n        && sed -ri 's`(\"codemirror\": \")[^\"]+`\\1file:/z/nodepkgs/codemirror`' ./package.json \\\n        && sed -ri 's`^var marked = require\\(.marked.\\).marked;$`var marked = window.marked;`' src/js/easymde.js \\\n        && npm install\n\nCOPY    easymde-ln.patch /z/\nRUN     cd easy-markdown-editor-$ver_mde \\\n        && patch -p1 < /z/easymde-ln.patch \\\n        && gulp \\\n        && cp -pv dist/easymde.min.css /z/dist/easymde.css \\\n        && cp -pv dist/easymde.min.js /z/dist/easymde.js\n\n\n# build dompurify\nRUN     (echo; cat DOMPurify-$ver_dompf/dist/purify.min.js) >> /z/dist/marked.js\n\n\n# build fontawesome and scp\nCOPY    mini-fa.sh /z\nCOPY    mini-fa.css /z\nCOPY    shiftbase.py /z\nRUN     /bin/ash /z/mini-fa.sh\n\n\n# build prismjs\nCOPY    genprism.py /z\nCOPY    genprism.sh /z\nRUN     ./genprism.sh $ver_prism\n\n\n# compress\nCOPY    zopfli.makefile /z/dist/\nRUN     cd /z/dist \\\n        && make -j$(nproc) -f zopfli.makefile \\\n        && rm *.makefile \\\n        && mv no-pk/* . \\\n        && rmdir no-pk\n\n\n# build fusepy\nCOPY    uncomment.py /z\nRUN     mv /z/fusepy-3.0.1/fuse.py /z/dist/f1 \\\n        && cd /z/dist \\\n        && python3 /z/uncomment.py f1 \\\n        && sed -ri '/self.__critical_exception = e/d' f1 \\\n        && awk '/^log =/{s=0} !s; /^from traceback im/{s=1;print\"from functools import partial\";print\"basestring = str\"}' <f1 >f2 \\\n        && awk '/LoggingMixIn:/{exit} --s<0;/self.use_ns = getattr/{s=7}' <f2 >f1 \\\n        && awk \"/if _machine =/{s=0} /'(mips|ppc|ppc64)'/{s=1} !s\" <f1 >f2 \\\n        && rm f1 && mv f2 fuse.py\n\n\n# git diff -U2 --no-index marked-1.1.0-orig/ marked-1.1.0-edit/ -U2 | sed -r '/^index /d;s`^(diff --git a/)[^/]+/(.* b/)[^/]+/`\\1\\2`; s`^(---|\\+\\+\\+) ([ab]/)[^/]+/`\\1 \\2`' > ../dev/copyparty/scripts/deps-docker/marked-ln.patch\n# d=/home/ed/dev/copyparty/scripts/deps-docker/; tar -cf ../x . && ssh root@$bip \"cd $d && tar -xv >&2 && make >&2 && tar -cC ../../copyparty/web deps\" <../x | (cd ../../copyparty/web/; cat > the.tgz; tar -xvf the.tgz; rm the.tgz)\n# gzip -dkf ../dev/copyparty/copyparty/web/deps/deps/marked.full.js.gz && diff -NarU2 ../dev/copyparty/copyparty/web/deps/{,deps/}marked.full.js\n"
  },
  {
    "path": "scripts/deps-docker/Makefile",
    "content": "self := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))\nvend := $(self)/../../copyparty/web/deps\n\n# prefers podman-docker (optionally rootless) over actual docker/moby\n\nall:\n\tcp -pv ../uncomment.py .\n\n\tdocker build -t build-copyparty-deps .\n\n\trm -rf $(vend)\n\tmkdir $(vend)\n\n\techo \"tar -cC /z dist\" | \\\n\tdocker run --rm -i build-copyparty-deps:latest | \\\n\ttar -xvC $(vend) --strip-components=1\n\n\ttouch $(vend)/__init__.py\n\tchown -R `stat $(self) -c %u:%g` $(vend)\n\npurge:\n\t-docker kill `docker ps -q`\n\t-docker rm   `docker ps -qa`\n\t-docker rmi  `docker images -qa`\n\nsh:\n\t@printf \"\\n\\033[1;31mopening a shell in the most recently created docker image\\033[0m\\n\"\n\tdocker run --rm -it `docker images -aq | head -n 1` /bin/ash\n"
  },
  {
    "path": "scripts/deps-docker/busy-mp3.sh",
    "content": "#!/bin/bash\nset -e\n\ncat >/dev/null <<EOF\na frame is 1152 samples\n1sec @ 48000 = 41.66 frames\n11 frames = 12672 samples = 0.264 sec\n22 frames = 25344 samples = 0.528 sec\nEOF\n\nfast=1\nfast=\n\necho\nmkdir -p /dev/shm/$1\ncd /dev/shm/$1\nfind -maxdepth 1 -type f -iname 'a.*.mp3*' -delete\nmin=99999999\n\nfor freq in 425; do  # {400..500}\nfor vol in 0; do  # {10..30}\nfor kbps in 32; do\nfor fdur in 1124; do  # {800..1200}\nfor fdu2 in 1042; do  # {800..1200}\nfor ftyp in h; do  # q h t l p\nfor ofs1 in 9214; do  # {0..32768}\nfor ofs2 in 0; do  # {0..4096}\nfor ofs3 in 0; do  # {0..4096}\nfor nores in --nores; do  # '' --nores\n\nf=a.b$kbps$nores-f$freq-v$vol-$ftyp$fdur-$fdu2-o$ofs1-$ofs2-$ofs3\nsox -r48000 -Dn -r48000 -b16 -c2 -t raw s1.pcm synth 25344s sin $freq vol 0.$vol fade $ftyp ${fdur}s 25344s ${fdu2}s\nsox -r48000 -Dn -r48000 -b16 -c2 -t raw s0.pcm synth 12672s sin $freq vol 0\ntail -c +$ofs1 s0.pcm >s0a.pcm\ntail -c +$ofs2 s0.pcm >s0b.pcm\ntail -c +$ofs3 s0.pcm >s0c.pcm\ncat s{0a,1,0,0b,1,0c}.pcm > a.pcm\nlame --silent -r -s 48 --bitwidth 16 --signed a.pcm -m j --resample 48 -b $kbps -q 0 $nores $f.mp3\nif [ $fast ]\nthen gzip -c9 <$f.mp3 >$f.mp3.gz\nelse pigz -c11 -I1 <$f.mp3 >$f.mp3.gz\nfi\nsz=$(wc -c <$f.mp3.gz)\nprintf '\\033[A%d %s\\033[K\\n' $sz $f\n[ $sz -le $((min+10)) ] && echo\n[ $sz -le $min ] && echo && min=$sz\n\ndone;done;done;done;done;done;done;done;done;done\ntrue\n\nf=a.b32--nores-f425-v0-h1124-1042-o9214-0-0.mp3\n[ $fast ] &&\n    pigz -c11 -I1 <$f >busy.mp3.gz ||\n    mv $f.gz busy.mp3.gz\n\nsz=$(wc -c <busy.mp3.gz)\n[ \"$sz\" -eq 106 ] &&\n    echo busy.mp3 built successfully ||\n    echo WARNING: unexpected size of busy.mp3\n\nfind -maxdepth 1 -type f -iname 'a.*.mp3*' -delete\n"
  },
  {
    "path": "scripts/deps-docker/codemirror.patch",
    "content": "diff -wNarU2 codemirror-5.65.1-orig/mode/gfm/gfm.js codemirror-5.65.1/mode/gfm/gfm.js\n--- codemirror-5.65.1-orig/mode/gfm/gfm.js\t2022-01-20 13:06:23.000000000 +0100\n+++ codemirror-5.65.1/mode/gfm/gfm.js\t2022-02-09 22:50:18.145862052 +0100\n@@ -97,5 +97,5 @@\n         }\n       }\n-      if (stream.match(urlRE) &&\n+      /*if (stream.match(urlRE) &&\n           stream.string.slice(stream.start - 2, stream.start) != \"](\" &&\n           (stream.start == 0 || /\\W/.test(stream.string.charAt(stream.start - 1)))) {\n@@ -106,5 +106,5 @@\n         state.combineTokens = true;\n         return \"link\";\n-      }\n+      }*/\n       stream.next();\n       return null;\ndiff -wNarU2 codemirror-5.65.1-orig/mode/meta.js codemirror-5.65.1/mode/meta.js\n--- codemirror-5.65.1-orig/mode/meta.js\t2022-01-20 13:06:23.000000000 +0100\n+++ codemirror-5.65.1/mode/meta.js\t2022-02-09 22:50:18.145862052 +0100\n@@ -13,4 +13,5 @@\n \n   CodeMirror.modeInfo = [\n+    /*\n     {name: \"APL\", mime: \"text/apl\", mode: \"apl\", ext: [\"dyalog\", \"apl\"]},\n     {name: \"PGP\", mimes: [\"application/pgp\", \"application/pgp-encrypted\", \"application/pgp-keys\", \"application/pgp-signature\"], mode: \"asciiarmor\", ext: [\"asc\", \"pgp\", \"sig\"]},\n@@ -56,5 +57,7 @@\n     {name: \"Gas\", mime: \"text/x-gas\", mode: \"gas\", ext: [\"s\"]},\n     {name: \"Gherkin\", mime: \"text/x-feature\", mode: \"gherkin\", ext: [\"feature\"]},\n+    */\n     {name: \"GitHub Flavored Markdown\", mime: \"text/x-gfm\", mode: \"gfm\", file: /^(readme|contributing|history)\\.md$/i},\n+    /*\n     {name: \"Go\", mime: \"text/x-go\", mode: \"go\", ext: [\"go\"]},\n     {name: \"Groovy\", mime: \"text/x-groovy\", mode: \"groovy\", ext: [\"groovy\", \"gradle\"], file: /^Jenkinsfile$/},\n@@ -65,5 +68,7 @@\n     {name: \"HXML\", mime: \"text/x-hxml\", mode: \"haxe\", ext: [\"hxml\"]},\n     {name: \"ASP.NET\", mime: \"application/x-aspx\", mode: \"htmlembedded\", ext: [\"aspx\"], alias: [\"asp\", \"aspx\"]},\n+    */\n     {name: \"HTML\", mime: \"text/html\", mode: \"htmlmixed\", ext: [\"html\", \"htm\", \"handlebars\", \"hbs\"], alias: [\"xhtml\"]},\n+    /*\n     {name: \"HTTP\", mime: \"message/http\", mode: \"http\"},\n     {name: \"IDL\", mime: \"text/x-idl\", mode: \"idl\", ext: [\"pro\"]},\n@@ -82,5 +87,7 @@\n     {name: \"LiveScript\", mime: \"text/x-livescript\", mode: \"livescript\", ext: [\"ls\"], alias: [\"ls\"]},\n     {name: \"Lua\", mime: \"text/x-lua\", mode: \"lua\", ext: [\"lua\"]},\n+    */\n     {name: \"Markdown\", mime: \"text/x-markdown\", mode: \"markdown\", ext: [\"markdown\", \"md\", \"mkd\"]},\n+    /*\n     {name: \"mIRC\", mime: \"text/mirc\", mode: \"mirc\"},\n     {name: \"MariaDB SQL\", mime: \"text/x-mariadb\", mode: \"sql\"},\n@@ -163,5 +170,7 @@\n     {name: \"VHDL\", mime: \"text/x-vhdl\", mode: \"vhdl\", ext: [\"vhd\", \"vhdl\"]},\n     {name: \"Vue.js Component\", mimes: [\"script/x-vue\", \"text/x-vue\"], mode: \"vue\", ext: [\"vue\"]},\n+    */\n     {name: \"XML\", mimes: [\"application/xml\", \"text/xml\"], mode: \"xml\", ext: [\"xml\", \"xsl\", \"xsd\", \"svg\"], alias: [\"rss\", \"wsdl\", \"xsd\"]},\n+    /*\n     {name: \"XQuery\", mime: \"application/xquery\", mode: \"xquery\", ext: [\"xy\", \"xquery\"]},\n     {name: \"Yacas\", mime: \"text/x-yacas\", mode: \"yacas\", ext: [\"ys\"]},\n@@ -172,4 +181,5 @@\n     {name: \"msgenny\", mime: \"text/x-msgenny\", mode: \"mscgen\", ext: [\"msgenny\"]},\n     {name: \"WebAssembly\", mime: \"text/webassembly\", mode: \"wast\", ext: [\"wat\", \"wast\"]},\n+    */\n   ];\n   // Ensure all modes have a mime property for backwards compatibility\ndiff -wNarU2 codemirror-5.65.1-orig/src/display/selection.js codemirror-5.65.1/src/display/selection.js\n--- codemirror-5.65.1-orig/src/display/selection.js\t2022-01-20 13:06:23.000000000 +0100\n+++ codemirror-5.65.1/src/display/selection.js\t2022-02-09 22:50:18.145862052 +0100\n@@ -96,29 +96,21 @@\n     let order = getOrder(lineObj, doc.direction)\n     iterateBidiSections(order, fromArg || 0, toArg == null ? lineLen : toArg, (from, to, dir, i) => {\n-      let ltr = dir == \"ltr\"\n-      let fromPos = coords(from, ltr ? \"left\" : \"right\")\n-      let toPos = coords(to - 1, ltr ? \"right\" : \"left\")\n+      let fromPos = coords(from, \"left\")\n+      let toPos = coords(to - 1, \"right\")\n \n       let openStart = fromArg == null && from == 0, openEnd = toArg == null && to == lineLen\n       let first = i == 0, last = !order || i == order.length - 1\n       if (toPos.top - fromPos.top <= 3) { // Single line\n-        let openLeft = (docLTR ? openStart : openEnd) && first\n-        let openRight = (docLTR ? openEnd : openStart) && last\n-        let left = openLeft ? leftSide : (ltr ? fromPos : toPos).left\n-        let right = openRight ? rightSide : (ltr ? toPos : fromPos).right\n+        let openLeft = openStart && first\n+        let openRight =  openEnd && last\n+        let left = openLeft ? leftSide : fromPos.left\n+        let right = openRight ? rightSide : toPos.right\n         add(left, fromPos.top, right - left, fromPos.bottom)\n       } else { // Multiple lines\n         let topLeft, topRight, botLeft, botRight\n-        if (ltr) {\n-          topLeft = docLTR && openStart && first ? leftSide : fromPos.left\n-          topRight = docLTR ? rightSide : wrapX(from, dir, \"before\")\n-          botLeft = docLTR ? leftSide : wrapX(to, dir, \"after\")\n-          botRight = docLTR && openEnd && last ? rightSide : toPos.right\n-        } else {\n-          topLeft = !docLTR ? leftSide : wrapX(from, dir, \"before\")\n-          topRight = !docLTR && openStart && first ? rightSide : fromPos.right\n-          botLeft = !docLTR && openEnd && last ? leftSide : toPos.left\n-          botRight = !docLTR ? rightSide : wrapX(to, dir, \"after\")\n-        }\n+          topLeft = openStart && first ? leftSide : fromPos.left\n+          topRight = rightSide\n+          botLeft = leftSide\n+          botRight = openEnd && last ? rightSide : toPos.right\n         add(topLeft, fromPos.top, topRight - topLeft, fromPos.bottom)\n         if (fromPos.bottom < toPos.top) add(leftSide, fromPos.bottom, null, toPos.top)\ndiff -wNarU2 codemirror-5.65.1-orig/src/input/ContentEditableInput.js codemirror-5.65.1/src/input/ContentEditableInput.js\n--- codemirror-5.65.1-orig/src/input/ContentEditableInput.js\t2022-01-20 13:06:23.000000000 +0100\n+++ codemirror-5.65.1/src/input/ContentEditableInput.js\t2022-02-09 22:50:18.145862052 +0100\n@@ -400,4 +400,5 @@\n   let info = mapFromLineView(view, line, pos.line)\n \n+  /*\n   let order = getOrder(line, cm.doc.direction), side = \"left\"\n   if (order) {\n@@ -405,4 +406,5 @@\n     side = partPos % 2 ? \"right\" : \"left\"\n   }\n+  */\n   let result = nodeAndOffsetInLineMap(info.map, pos.ch, side)\n   result.offset = result.collapse == \"right\" ? result.end : result.start\ndiff -wNarU2 codemirror-5.65.1-orig/src/input/movement.js codemirror-5.65.1/src/input/movement.js\n--- codemirror-5.65.1-orig/src/input/movement.js\t2022-01-20 13:06:23.000000000 +0100\n+++ codemirror-5.65.1/src/input/movement.js\t2022-02-09 22:50:18.145862052 +0100\n@@ -15,4 +15,5 @@\n \n export function endOfLine(visually, cm, lineObj, lineNo, dir) {\n+  /*\n   if (visually) {\n     if (cm.doc.direction == \"rtl\") dir = -dir\n@@ -39,8 +40,11 @@\n     }\n   }\n+  */\n   return new Pos(lineNo, dir < 0 ? lineObj.text.length : 0, dir < 0 ? \"before\" : \"after\")\n }\n \n export function moveVisually(cm, line, start, dir) {\n+  return moveLogically(line, start, dir)\n+  /*\n   let bidi = getOrder(line, cm.doc.direction)\n   if (!bidi) return moveLogically(line, start, dir)\n@@ -109,3 +113,4 @@\n   // Case 4: Nowhere to move\n   return null\n+  */\n }\ndiff -wNarU2 codemirror-5.65.1-orig/src/line/line_data.js codemirror-5.65.1/src/line/line_data.js\n--- codemirror-5.65.1-orig/src/line/line_data.js\t2022-01-20 13:06:23.000000000 +0100\n+++ codemirror-5.65.1/src/line/line_data.js\t2022-02-09 22:54:11.542722046 +0100\n@@ -3,5 +3,5 @@\n import { elt, eltP, joinClasses } from \"../util/dom.js\"\n import { eventMixin, signal } from \"../util/event.js\"\n-import { hasBadBidiRects, zeroWidthElement } from \"../util/feature_detection.js\"\n+import { zeroWidthElement } from \"../util/feature_detection.js\"\n import { lst, spaceStr } from \"../util/misc.js\"\n \n@@ -79,6 +79,6 @@\n     // Optionally wire in some hacks into the token-rendering\n     // algorithm, to deal with browser quirks.\n-    if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line, cm.doc.direction)))\n-      builder.addToken = buildTokenBadBidi(builder.addToken, order)\n+    //if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line, cm.doc.direction)))\n+    //  builder.addToken = buildTokenBadBidi(builder.addToken, order)\n     builder.map = []\n     let allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line)\ndiff -wNarU2 codemirror-5.65.1-orig/src/measurement/position_measurement.js codemirror-5.65.1/src/measurement/position_measurement.js\n--- codemirror-5.65.1-orig/src/measurement/position_measurement.js\t2022-01-20 13:06:23.000000000 +0100\n+++ codemirror-5.65.1/src/measurement/position_measurement.js\t2022-02-09 22:50:18.145862052 +0100\n@@ -382,5 +382,6 @@\n     sticky = \"after\"\n   }\n-  if (!order) return get(sticky == \"before\" ? ch - 1 : ch, sticky == \"before\")\n+  /*if (!order)*/ return get(sticky == \"before\" ? ch - 1 : ch, sticky == \"before\")\n+  /*\n \n   function getBidi(ch, partPos, invert) {\n@@ -393,4 +394,5 @@\n   if (other != null) val.other = getBidi(ch, other, sticky != \"before\")\n   return val\n+  */\n }\n \n@@ -470,4 +472,5 @@\n   let begin = 0, end = lineObj.text.length, ltr = true\n \n+  /*\n   let order = getOrder(lineObj, cm.doc.direction)\n   // If the line isn't plain left-to-right text, first figure out\n@@ -484,4 +487,5 @@\n     end = ltr ? part.to : part.from - 1\n   }\n+  */\n \n   // A binary search to find the first character whose bounding box\n@@ -528,4 +532,5 @@\n }\n \n+/*\n function coordsBidiPart(cm, lineObj, lineNo, preparedMeasure, order, x, y) {\n   // Bidi parts are sorted left-to-right, and in a non-line-wrapping\n@@ -582,4 +587,5 @@\n   return part\n }\n+*/\n \n let measureText\ndiff -wNarU2 codemirror-5.65.1-orig/src/util/bidi.js codemirror-5.65.1/src/util/bidi.js\n--- codemirror-5.65.1-orig/src/util/bidi.js\t2022-01-20 13:06:23.000000000 +0100\n+++ codemirror-5.65.1/src/util/bidi.js\t2022-02-09 22:50:18.145862052 +0100\n@@ -4,5 +4,5 @@\n \n export function iterateBidiSections(order, from, to, f) {\n-  if (!order) return f(from, to, \"ltr\", 0)\n+  /*if (!order)*/ return f(from, to, \"ltr\", 0) /*\n   let found = false\n   for (let i = 0; i < order.length; ++i) {\n@@ -14,4 +14,5 @@\n   }\n   if (!found) f(from, to, \"ltr\")\n+*/\n }\n \n@@ -32,5 +33,7 @@\n     }\n   }\n-  return found != null ? found : bidiOther\n+  var ret = found != null ? found : bidiOther\n+  console.log(\"getBidiPartAt(%s,%s,%s) => [%s]\", order, ch, sticky, ret)\n+  return ret\n }\n \n@@ -55,4 +58,7 @@\n // N (ON):  Other Neutrals\n \n+let bidiOrdering = (function() { return function(str, direction) { return false; }})();\n+/*\n+\n // Returns null if characters are ordered as they appear\n // (left-to-right), or an array of sections ({from, to, level}\n@@ -81,5 +87,5 @@\n   }\n \n-  return function(str, direction) {\n+  var fun = function(str, direction) {\n     let outerType = direction == \"ltr\" ? \"L\" : \"R\"\n \n@@ -204,5 +210,11 @@\n     return direction == \"rtl\" ? order.reverse() : order\n   }\n+  return function(str, direction) {\n+    var ret = fun(str, direction);\n+    console.log(\"bidiOrdering inner ([%s], %s) => [%s]\", str, direction, ret);\n+    return ret;\n+  }\n })()\n+*/\n \n // Get the bidi ordering for the given line (and cache it). Returns\n@@ -210,6 +222,4 @@\n // BidiSpan objects otherwise.\n export function getOrder(line, direction) {\n-  let order = line.order\n-  if (order == null) order = line.order = bidiOrdering(line.text, direction)\n-  return order\n+  return false;\n }\ndiff -wNarU2 codemirror-5.65.1-orig/src/util/feature_detection.js codemirror-5.65.1/src/util/feature_detection.js\n--- codemirror-5.65.1-orig/src/util/feature_detection.js\t2022-01-20 13:06:23.000000000 +0100\n+++ codemirror-5.65.1/src/util/feature_detection.js\t2022-02-09 22:50:18.145862052 +0100\n@@ -25,4 +25,5 @@\n }\n \n+/*\n // Feature-detect IE's crummy client rect reporting for bidi text\n let badBidiRects\n@@ -36,4 +37,5 @@\n   return badBidiRects = (r1.right - r0.right < 3)\n }\n+*/\n \n // See if \"\".split is the broken IE version, if so, provide an\n"
  },
  {
    "path": "scripts/deps-docker/easymde-ln.patch",
    "content": "diff -NarU2 easymde-mod1/src/js/easymde.js easymde-edit/src/js/easymde.js\n--- easymde-mod1/src/js/easymde.js\t2020-05-01 14:34:19.878774400 +0200\n+++ easymde-edit/src/js/easymde.js\t2020-05-01 21:24:44.142611200 +0200\n@@ -2189,4 +2189,5 @@\n };\n \n+\n EasyMDE.prototype.createSideBySide = function () {\n     var cm = this.codemirror;\n@@ -2223,12 +2224,80 @@\n         }\n         pScroll = true;\n-        var height = v.getScrollInfo().height - v.getScrollInfo().clientHeight;\n-        var ratio = parseFloat(v.getScrollInfo().top) / height;\n-        var move = (preview.scrollHeight - preview.clientHeight) * ratio;\n-        preview.scrollTop = move;\n+        var md_vp = v.getScrollInfo();\n+        // viewport top: top\n+        // viewport size: clientHeight\n+        // document size: height\n+        var md_scroll_y = md_vp.top + md_vp.clientHeight / 2;\n+        var md_center_n = cm.lineAtHeight(md_scroll_y, 'local') + 1;\n+        var md_next_n = md_center_n;\n+        var md_top = cm.heightAtLine(md_center_n - 1, 'local');\n+        while (md_next_n < cm.lineCount())\n+            if (cm.getLine(md_next_n++).replace(/\\s+/g, '').length > 0)\n+                break;\n+        var md_end = cm.heightAtLine(md_next_n - 1, 'local');\n+        var md_frac = (md_scroll_y - md_top) * 1.0 / (md_end - md_top);\n+        var get_pre_line = function(line_n, increase) {\n+            var end = 0;\n+            var step = -1;\n+            if (increase) {\n+                step = 1;\n+                end = line_n + 1000;\n+            }\n+            // there might be multiple elements in the marked.js output,\n+            // take the element with the biggest height\n+            var biggest = -1;\n+            var line_dom = null;\n+            for (; line_n != end; line_n += step) {\n+                var hits = document.querySelectorAll('.editor-preview-side *[data-ln=\\'' + line_n + '\\']');\n+                for (var i = 0; i < hits.length; i++) {\n+                    var hit_size = hits[i].offsetHeight;\n+                    if (biggest < hit_size) {\n+                        biggest = hit_size;\n+                        line_dom = hits[i];\n+                    }\n+                }\n+                if (line_dom) {\n+                    var ret_y = 0;\n+                    var el = line_dom;\n+                    while (el && (el.getAttribute('class') + '').indexOf('editor-preview-side') < 0) {\n+                        ret_y += el.offsetTop;\n+                        el = el.offsetParent;\n+                    }\n+                    return [line_n, line_dom, ret_y];\n+                }\n+            }\n+            return null;\n+        };\n+        var pre1 = get_pre_line(md_center_n, false);\n+        var pre2 = get_pre_line(pre1[0] + 1, true) ||\n+            [cm.lineCount(), null, preview.scrollHeight];\n+        \n+        //console.log('code-center %d, frac %.2f, pre [%d,%d] [%d,%d]',\n+        //    md_center_n, md_frac, pre1[0], pre1[2], pre2[0], pre2[2]);\n+        \n+        // [0] is the markdown line which matches that preview y-pos\n+        // and since not all preview lines are tagged with a line-number\n+        // take the lineno diff and divide it by the distance\n+        var pre_frac = md_frac / ((pre2[0] - pre1[0]) / (md_next_n - md_center_n));\n+\n+        // then use that fraction for the scroll offset\n+        var pre_y = pre1[2] + (pre2[2] - pre1[2]) * pre_frac;\n+\n+        // unless we couldn't match the markdown line exactly to any preview line\n+        if (md_center_n > pre1[0] && md_center_n < pre2[0])\n+            pre_y = pre2[2];\n+\n+        // except jump to the top or bottom if we're close enough\n+        if (md_vp.top < 32)\n+            pre_y = 0;\n+        else if (md_vp.top + 32 >= md_vp.height - md_vp.clientHeight)\n+            pre_y = preview.scrollHeight;\n+\n+        preview.scrollTop = pre_y - preview.clientHeight / 2;\n     });\n \n     // Syncs scroll  preview -> editor\n-    preview.onscroll = function () {\n+    // disabled since it should be possible to intentionally unsync\n+    preview.onscroll_fgsfds = function () {\n         if (pScroll) {\n             pScroll = false;\n"
  },
  {
    "path": "scripts/deps-docker/easymde-marked6.patch",
    "content": "diff --git a/src/js/easymde.js b/src/js/easymde.js\n--- a/src/js/easymde.js\n+++ b/src/js/easymde.js\n@@ -1962,7 +1962,7 @@ EasyMDE.prototype.markdown = function (text) {\n         marked.setOptions(markedOptions);\n \n         // Convert the markdown to HTML\n-        var htmlText = marked(text);\n+        var htmlText = marked.parse(text);\n \n         // Sanitize HTML\n         if (this.options.renderingConfig && typeof this.options.renderingConfig.sanitizerFunction === 'function') {\n"
  },
  {
    "path": "scripts/deps-docker/easymde.patch",
    "content": "diff -wNarU2 easy-markdown-editor-2.16.1-orig/gulpfile.js easy-markdown-editor-2.16.1/gulpfile.js\n--- easy-markdown-editor-2.16.1-orig/gulpfile.js\t2022-01-14 23:27:44.000000000 +0100\n+++ easy-markdown-editor-2.16.1/gulpfile.js\t2022-02-09 23:06:01.694592535 +0100\n@@ -25,5 +25,4 @@\n     './node_modules/codemirror/lib/codemirror.css',\n     './src/css/*.css',\n-    './node_modules/codemirror-spell-checker/src/css/spell-checker.css',\n ];\n \ndiff -wNarU2 easy-markdown-editor-2.16.1-orig/package.json easy-markdown-editor-2.16.1/package.json\n--- easy-markdown-editor-2.16.1-orig/package.json\t2022-01-14 23:27:44.000000000 +0100\n+++ easy-markdown-editor-2.16.1/package.json\t2022-02-09 23:06:24.778501888 +0100\n@@ -23,5 +23,4 @@\n         \"@types/marked\": \"^4.0.1\",\n         \"codemirror\": \"^5.63.1\",\n-        \"codemirror-spell-checker\": \"1.1.2\",\n         \"marked\": \"^4.0.10\"\n     },\ndiff -wNarU2 easy-markdown-editor-2.16.1-orig/src/js/easymde.js easy-markdown-editor-2.16.1/src/js/easymde.js\n--- easy-markdown-editor-2.16.1-orig/src/js/easymde.js\t2022-01-14 23:27:44.000000000 +0100\n+++ easy-markdown-editor-2.16.1/src/js/easymde.js\t2022-02-09 23:07:21.203131415 +0100\n@@ -12,5 +12,4 @@\n require('codemirror/mode/gfm/gfm.js');\n require('codemirror/mode/xml/xml.js');\n-var CodeMirrorSpellChecker = require('codemirror-spell-checker');\n var marked = require('marked').marked;\n \n@@ -1816,9 +1815,4 @@\n         options.autosave.uniqueId = options.autosave.unique_id;\n \n-    // If overlay mode is specified and combine is not provided, default it to true\n-    if (options.overlayMode && options.overlayMode.combine === undefined) {\n-        options.overlayMode.combine = true;\n-    }\n-\n     // Update this options\n     this.options = options;\n@@ -2057,34 +2051,7 @@\n     var mode, backdrop;\n \n-    // CodeMirror overlay mode\n-    if (options.overlayMode) {\n-        CodeMirror.defineMode('overlay-mode', function (config) {\n-            return CodeMirror.overlayMode(CodeMirror.getMode(config, options.spellChecker !== false ? 'spell-checker' : 'gfm'), options.overlayMode.mode, options.overlayMode.combine);\n-        });\n-\n-        mode = 'overlay-mode';\n-        backdrop = options.parsingConfig;\n-        backdrop.gitHubSpice = false;\n-    } else {\n         mode = options.parsingConfig;\n         mode.name = 'gfm';\n         mode.gitHubSpice = false;\n-    }\n-    if (options.spellChecker !== false) {\n-        mode = 'spell-checker';\n-        backdrop = options.parsingConfig;\n-        backdrop.name = 'gfm';\n-        backdrop.gitHubSpice = false;\n-\n-        if (typeof options.spellChecker === 'function') {\n-            options.spellChecker({\n-                codeMirrorInstance: CodeMirror,\n-            });\n-        } else {\n-            CodeMirrorSpellChecker({\n-                codeMirrorInstance: CodeMirror,\n-            });\n-        }\n-    }\n \n     // eslint-disable-next-line no-unused-vars\ndiff -wNarU2 easy-markdown-editor-2.16.1-orig/types/easymde.d.ts easy-markdown-editor-2.16.1/types/easymde.d.ts\n--- easy-markdown-editor-2.16.1-orig/types/easymde.d.ts\t2022-01-14 23:27:44.000000000 +0100\n+++ easy-markdown-editor-2.16.1/types/easymde.d.ts\t2022-02-09 23:07:55.427605243 +0100\n@@ -167,9 +167,4 @@\n     }\n \n-    interface OverlayModeOptions {\n-        mode: CodeMirror.Mode<any>;\n-        combine?: boolean;\n-    }\n-\n     interface SpellCheckerOptions {\n         codeMirrorInstance: CodeMirror.Editor;\n@@ -229,6 +224,4 @@\n         syncSideBySidePreviewScroll?: boolean;\n \n-        overlayMode?: OverlayModeOptions;\n-\n         direction?: 'ltr' | 'rtl';\n     }\n"
  },
  {
    "path": "scripts/deps-docker/genprism.py",
    "content": "#!/usr/bin/env python3\n\n# author: @chinponya\n\n\nimport argparse\nimport json\nfrom pathlib import Path\nfrom urllib.parse import urlparse, parse_qsl\n\n\ndef read_json(path):\n    return json.loads(path.read_text())\n\n\ndef get_prism_version(prism_path):\n    package_json_path = prism_path / \"package.json\"\n    package_json = read_json(package_json_path)\n    return package_json[\"version\"]\n\n\ndef get_prism_components(prism_path):\n    components_json_path = prism_path / \"components.json\"\n    components_json = read_json(components_json_path)\n    return components_json\n\n\ndef parse_prism_configuration(url_str):\n    url = urlparse(url_str)\n    # prism.com uses a non-standard query string-like encoding\n    query = {k: v.split(\" \") for k, v in parse_qsl(url.fragment)}\n    return query\n\n\ndef paths_of_component(prism_path, kind, components, name, minified):\n    component = components[kind][name]\n    meta = components[kind][\"meta\"]\n    path_format = meta[\"path\"]\n    path_base = prism_path / path_format.replace(\"{id}\", name)\n\n    if isinstance(component, str):\n        # 'core' component has a different shape, so we convert it to be consistent\n        component = {\"title\": component}\n\n    if meta.get(\"noCSS\") or component.get(\"noCSS\"):\n        extensions = [\"js\"]\n    elif kind == \"themes\":\n        extensions = [\"css\"]\n    else:\n        extensions = [\"js\", \"css\"]\n\n    if path_base.is_dir():\n        result = {ext: path_base / f\"{name}.{ext}\" for ext in extensions}\n    elif path_base.suffix:\n        ext = path_base.suffix.replace(\".\", \"\")\n        result = {ext: path_base}\n    else:\n        result = {ext: path_base.with_suffix(f\".{ext}\") for ext in extensions}\n\n    if minified:\n        result = {\n            ext: path.with_suffix(\".min\" + path.suffix) for ext, path in result.items()\n        }\n\n    return result\n\n\ndef read_component_contents(kv_paths):\n    return {k: path.read_text() for k, path in kv_paths.items()}\n\n\ndef get_language_dependencies(components, name):\n    dependencies = components[\"languages\"][name].get(\"require\")\n\n    if isinstance(dependencies, list):\n        return dependencies\n    elif isinstance(dependencies, str):\n        return [dependencies]\n    else:\n        return []\n\n\ndef make_header(prism_path, url):\n    version = get_prism_version(prism_path)\n    header = f\"/* PrismJS {version}\\n{url} */\"\n    return {\"js\": header, \"css\": header}\n\n\ndef make_core(prism_path, components, minified):\n    kv_paths = paths_of_component(prism_path, \"core\", components, \"core\", minified)\n    return read_component_contents(kv_paths)\n\n\ndef make_theme(prism_path, components, name, minified):\n    kv_paths = paths_of_component(prism_path, \"themes\", components, name, minified)\n    return read_component_contents(kv_paths)\n\n\ndef make_language(prism_path, components, name, minified):\n    kv_paths = paths_of_component(prism_path, \"languages\", components, name, minified)\n    return read_component_contents(kv_paths)\n\n\ndef make_languages(prism_path, components, names, minified):\n    names_with_dependencies = sum(\n        ([*get_language_dependencies(components, name), name] for name in names), []\n    )\n\n    seen = set()\n    names_with_dependencies = [\n        x for x in names_with_dependencies if not (x in seen or seen.add(x))\n    ]\n\n    kv_code = [\n        make_language(prism_path, components, name, minified)\n        for name in names_with_dependencies\n    ]\n\n    return kv_code\n\n\ndef make_plugin(prism_path, components, name, minified):\n    kv_paths = paths_of_component(prism_path, \"plugins\", components, name, minified)\n    return read_component_contents(kv_paths)\n\n\ndef make_plugins(prism_path, components, names, minified):\n    kv_code = [make_plugin(prism_path, components, name, minified) for name in names]\n    return kv_code\n\n\ndef make_code(prism_path, url, minified):\n    components = get_prism_components(prism_path)\n    configuration = parse_prism_configuration(url)\n    theme_name = configuration[\"themes\"][0]\n    code = [\n        make_header(prism_path, url),\n        make_core(prism_path, components, minified),\n        make_theme(prism_path, components, theme_name, minified),\n    ]\n\n    if configuration.get(\"languages\"):\n        code.extend(\n            make_languages(prism_path, components, configuration[\"languages\"], minified)\n        )\n\n    if configuration.get(\"plugins\"):\n        code.extend(\n            make_plugins(prism_path, components, configuration[\"plugins\"], minified)\n        )\n\n    return code\n\n\ndef join_code(kv_code):\n    result = {\"js\": \"\", \"css\": \"\"}\n\n    for row in kv_code:\n        for key, code in row.items():\n            result[key] += code\n            result[key] += \"\\n\"\n\n    return result\n\n\ndef write_code(kv_code, js_out, css_out):\n    code = join_code(kv_code)\n\n    with js_out.open(\"w\") as f:\n        f.write(code[\"js\"])\n        print(f\"written {js_out}\")\n\n    with css_out.open(\"w\") as f:\n        f.write(code[\"css\"])\n        print(f\"written {css_out}\")\n\n\ndef parse_args():\n    # fmt: off\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"url\", help=\"configured prism download url\")\n    parser.add_argument(\"--dir\", type=Path, default=Path(\".\"), help=\"prism repo directory\")\n    parser.add_argument(\"--minify\", default=True, action=argparse.BooleanOptionalAction, help=\"use minified files\",)\n    parser.add_argument(\"--js-out\", type=Path, default=Path(\"prism.js\"), help=\"JS output file path\")\n    parser.add_argument(\"--css-out\", type=Path, default=Path(\"prism.css\"), help=\"CSS output file path\")\n    # fmt: on\n    args = parser.parse_args()\n    return args\n\n\ndef main():\n    args = parse_args()\n    code = make_code(args.dir, args.url, args.minify)\n    write_code(code, args.js_out, args.css_out)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/deps-docker/genprism.sh",
    "content": "#!/bin/bash\nset -e\n\nplugins=(\n    line-highlight\n    line-numbers\n    autolinker\n)\n\nlangs=(\n    markup\n    css\n    clike\n    javascript\n    # abap\n    # abnf\n    # actionscript\n    # ada\n    # agda\n    # al\n    # antlr4\n    # apacheconf\n    # apex\n    # apl\n    # applescript\n    # aql\n    # arduino\n    # arff\n    # armasm\n    # arturo\n    # asciidoc\n    # aspnet\n    # asm6502\n    # asmatmel\n    # autohotkey\n    # autoit\n    # avisynth\n    # avro-idl\n    # awk\n    bash\n    basic\n    batch\n    # bbcode\n    # bbj\n    # bicep\n    # birb\n    # bison\n    # bnf\n    # bqn\n    # brainfuck\n    # brightscript\n    # bro\n    # bsl\n    c\n    csharp\n    cpp\n    # cfscript\n    # chaiscript\n    # cil\n    # cilkc\n    # cilkcpp\n    # clojure\n    # cmake\n    # cobol\n    # coffeescript\n    # concurnas\n    # csp\n    # cooklang\n    # coq\n    # crystal\n    # css-extras\n    # csv\n    # cue\n    # cypher\n    # d\n    # dart\n    # dataweave\n    # dax\n    # dhall\n    diff\n    # django\n    # dns-zone-file\n    docker\n    # dot\n    # ebnf\n    # editorconfig\n    # eiffel\n    # ejs\n    elixir\n    # elm\n    # etlua\n    # erb\n    # erlang\n    # excel-formula\n    # fsharp\n    # factor\n    # false\n    # firestore-security-rules\n    # flow\n    # fortran\n    # ftl\n    # gml\n    # gap\n    # gcode\n    # gdscript\n    # gedcom\n    # gettext\n    # gherkin\n    # git\n    glsl\n    # gn\n    # linker-script\n    go\n    # go-module\n    # gradle\n    # graphql\n    # groovy\n    # haml\n    # handlebars\n    # haskell\n    # haxe\n    # hcl\n    # hlsl\n    # hoon\n    # http\n    # hpkp\n    # hsts\n    # ichigojam\n    # icon\n    # icu-message-format\n    # idris\n    # ignore\n    # inform7\n    ini\n    # io\n    # j\n    java\n    # javadoc\n    # javadoclike\n    # javastacktrace\n    # jexl\n    # jolie\n    # jq\n    # jsdoc\n    # js-extras\n    json\n    # json5\n    # jsonp\n    # jsstacktrace\n    # js-templates\n    # julia\n    # keepalived\n    # keyman\n    kotlin\n    # kumir\n    # kusto\n    latex\n    # latte\n    less\n    # lilypond\n    # liquid\n    lisp\n    # livescript\n    # llvm\n    # log\n    # lolcode\n    lua\n    # magma\n    makefile\n    # markdown\n    # markup-templating\n    # mata\n    matlab\n    # maxscript\n    # mel\n    # mermaid\n    # metafont\n    # mizar\n    # mongodb\n    # monkey\n    moonscript\n    # n1ql\n    # n4js\n    # nand2tetris-hdl\n    # naniscript\n    nasm\n    # neon\n    # nevod\n    # nginx\n    nim\n    nix\n    # nsis\n    objectivec\n    # ocaml\n    # odin\n    # opencl\n    # openqasm\n    # oz\n    # parigp\n    # parser\n    # pascal\n    # pascaligo\n    # psl\n    # pcaxis\n    # peoplecode\n    perl\n    # php\n    # phpdoc\n    # php-extras\n    # plant-uml\n    # plsql\n    # powerquery\n    powershell\n    # processing\n    # prolog\n    # promql\n    # properties\n    # protobuf\n    # pug\n    # puppet\n    # pure\n    # purebasic\n    # purescript\n    python\n    # qsharp\n    # q\n    # qml\n    # qore\n    r\n    # racket\n    # cshtml\n    jsx\n    # tsx\n    # reason\n    # regex\n    # rego\n    # renpy\n    # rescript\n    # rest\n    # rip\n    # roboconf\n    # robotframework\n    ruby\n    rust\n    # sas\n    sass\n    scss\n    # scala\n    # scheme\n    # shell-session\n    # smali\n    # smalltalk\n    # smarty\n    # sml\n    # solidity\n    # solution-file\n    # soy\n    # sparql\n    # splunk-spl\n    # sqf\n    sql\n    # squirrel\n    # stan\n    # stata\n    # iecst\n    # stylus\n    # supercollider\n    swift\n    systemd\n    # t4-templating\n    # t4-cs\n    # t4-vb\n    # tap\n    # tcl\n    # tt2\n    # textile\n    toml\n    # tremor\n    # turtle\n    # twig\n    typescript\n    # typoscript\n    # unrealscript\n    # uorazor\n    # uri\n    # v\n    # vala\n    vbnet\n    # velocity\n    verilog\n    vhdl\n    # vim\n    # visual-basic\n    # warpscript\n    # wasm\n    # web-idl\n    # wgsl\n    # wiki\n    # wolfram\n    # wren\n    # xeora\n    # xml-doc\n    # xojo\n    # xquery\n    yaml\n    # yang\n    zig\n)\n\nslangs=\"${langs[*]}\"\nslangs=\"${slangs// /+}\"\n\nsplugins=\"${plugins[*]}\"\nsplugins=\"${splugins// /+}\"\n\nfor theme in prism-funky prism ; do\n    u=\"https://prismjs.com/download.html#themes=$theme&languages=$slangs&plugins=$splugins\"\n    echo \"$u\"\n    ./genprism.py --dir prism-$1 --js-out prism.js --css-out $theme.css \"$u\"\ndone\n\nmv prism-funky.css prismd.css\nmv prismd.css prism.css prism.js /z/dist/\n"
  },
  {
    "path": "scripts/deps-docker/markdown-it.patch",
    "content": "diff -NarU1 markdown-it-10.0.0-orig/lib/common/entities.js markdown-it-10.0.0-edit/lib/common/entities.js\n--- markdown-it-10.0.0-orig/lib/common/entities.js\t2019-09-10 21:39:58.000000000 +0000\n+++ markdown-it-10.0.0-edit/lib/common/entities.js\t2020-04-26 10:24:33.043023331 +0000\n@@ -5,2 +5,5 @@\n /*eslint quotes:0*/\n-module.exports = require('entities/lib/maps/entities.json');\n+//module.exports = require('entities/lib/maps/entities.json');\n+module.exports = {\n+    \"amp\": \"&\", \"quot\": \"\\\"\", \"gt\": \">\", \"lt\": \"<\"\n+}\n"
  },
  {
    "path": "scripts/deps-docker/marked-ln.patch",
    "content": "diff --git a/src/Lexer.js b/src/Lexer.js\nadds linetracking to marked.js v4.3.0;\nadd data-ln=\"%d\" to most tags, %d is the source markdown line\n--- a/src/Lexer.js\n+++ b/src/Lexer.js\n@@ -52,4 +52,5 @@ function mangle(text) {\n export class Lexer {\n   constructor(options) {\n+    this.ln = 1;  // like most editors, start couting from 1\n     this.tokens = [];\n     this.tokens.links = Object.create(null);\n@@ -128,4 +129,15 @@ export class Lexer {\n   }\n \n+  set_ln(token, ln = this.ln) {\n+    // assigns ln (the current line numer) to the token,\n+    // then bump this.ln by the number of newlines in the contents\n+    //\n+    // if ln is set, also assigns the line counter to a new value\n+    // (usually a backup value from before a call into a subparser\n+    //  which bumped the linecounter by a subset of the newlines)\n+    token.ln = ln;\n+    this.ln = ln + (token.raw.match(/\\n/g) || []).length;\n+  }\n+\n   /**\n    * Lexing\n@@ -140,7 +152,11 @@ export class Lexer {\n     }\n \n-    let token, lastToken, cutSrc, lastParagraphClipped;\n+    let token, lastToken, cutSrc, lastParagraphClipped, ln;\n \n     while (src) {\n+      // this.ln will be bumped by recursive calls into this func;\n+      // reset the count and rely on the outermost token's raw only\n+      ln = this.ln;\n+\n       if (this.options.extensions\n         && this.options.extensions.block\n@@ -148,4 +164,5 @@ export class Lexer {\n           if (token = extTokenizer.call({ lexer: this }, src, tokens)) {\n             src = src.substring(token.raw.length);\n+            this.set_ln(token, ln);\n             tokens.push(token);\n             return true;\n@@ -159,4 +176,5 @@ export class Lexer {\n       if (token = this.tokenizer.space(src)) {\n         src = src.substring(token.raw.length);\n+        this.set_ln(token, ln); // is \\n if not type\n         if (token.raw.length === 1 && tokens.length > 0) {\n           // if there's a single \\n as a spacer, it's terminating the last line,\n@@ -172,4 +190,5 @@ export class Lexer {\n       if (token = this.tokenizer.code(src)) {\n         src = src.substring(token.raw.length);\n+        this.set_ln(token, ln);\n         lastToken = tokens[tokens.length - 1];\n         // An indented code block cannot interrupt a paragraph.\n@@ -187,4 +206,5 @@ export class Lexer {\n       if (token = this.tokenizer.fences(src)) {\n         src = src.substring(token.raw.length);\n+        this.set_ln(token, ln);\n         tokens.push(token);\n         continue;\n@@ -194,4 +214,5 @@ export class Lexer {\n       if (token = this.tokenizer.heading(src)) {\n         src = src.substring(token.raw.length);\n+        this.set_ln(token, ln);\n         tokens.push(token);\n         continue;\n@@ -201,4 +222,5 @@ export class Lexer {\n       if (token = this.tokenizer.hr(src)) {\n         src = src.substring(token.raw.length);\n+        this.set_ln(token, ln);\n         tokens.push(token);\n         continue;\n@@ -208,4 +230,5 @@ export class Lexer {\n       if (token = this.tokenizer.blockquote(src)) {\n         src = src.substring(token.raw.length);\n+        this.set_ln(token, ln);\n         tokens.push(token);\n         continue;\n@@ -215,4 +238,5 @@ export class Lexer {\n       if (token = this.tokenizer.list(src)) {\n         src = src.substring(token.raw.length);\n+        this.set_ln(token, ln);\n         tokens.push(token);\n         continue;\n@@ -222,4 +246,5 @@ export class Lexer {\n       if (token = this.tokenizer.html(src)) {\n         src = src.substring(token.raw.length);\n+        this.set_ln(token, ln);\n         tokens.push(token);\n         continue;\n@@ -229,4 +254,5 @@ export class Lexer {\n       if (token = this.tokenizer.def(src)) {\n         src = src.substring(token.raw.length);\n+        this.set_ln(token, ln);\n         lastToken = tokens[tokens.length - 1];\n         if (lastToken && (lastToken.type === 'paragraph' || lastToken.type === 'text')) {\n@@ -246,4 +272,5 @@ export class Lexer {\n       if (token = this.tokenizer.table(src)) {\n         src = src.substring(token.raw.length);\n+        this.set_ln(token, ln);\n         tokens.push(token);\n         continue;\n@@ -253,4 +280,5 @@ export class Lexer {\n       if (token = this.tokenizer.lheading(src)) {\n         src = src.substring(token.raw.length);\n+        this.set_ln(token, ln);\n         tokens.push(token);\n         continue;\n@@ -273,4 +301,5 @@ export class Lexer {\n       }\n       if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) {\n+        this.set_ln(token, ln);\n         lastToken = tokens[tokens.length - 1];\n         if (lastParagraphClipped && lastToken.type === 'paragraph') {\n@@ -290,4 +319,6 @@ export class Lexer {\n       if (token = this.tokenizer.text(src)) {\n         src = src.substring(token.raw.length);\n+        this.set_ln(token, ln);\n+        this.ln++;\n         lastToken = tokens[tokens.length - 1];\n         if (lastToken && lastToken.type === 'text') {\n@@ -367,4 +398,5 @@ export class Lexer {\n           if (token = extTokenizer.call({ lexer: this }, src, tokens)) {\n             src = src.substring(token.raw.length);\n+            this.ln = token.ln || this.ln;\n             tokens.push(token);\n             return true;\n@@ -432,4 +464,6 @@ export class Lexer {\n       if (token = this.tokenizer.br(src)) {\n         src = src.substring(token.raw.length);\n+        // no need to reset (no more blockTokens anyways)\n+        token.ln = this.ln++;\n         tokens.push(token);\n         continue;\n@@ -474,4 +508,5 @@ export class Lexer {\n       if (token = this.tokenizer.inlineText(cutSrc, smartypants)) {\n         src = src.substring(token.raw.length);\n+        this.ln = token.ln || this.ln;\n         if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started\n           prevChar = token.raw.slice(-1);\ndiff --git a/src/Parser.js b/src/Parser.js\nindex a22a2bc..884ad66 100644\n--- a/src/Parser.js\n+++ b/src/Parser.js\n@@ -18,4 +18,5 @@ export class Parser {\n     this.textRenderer = new TextRenderer();\n     this.slugger = new Slugger();\n+    this.ln = 0; // error indicator; should always be set >=1 from tokens\n   }\n \n@@ -64,4 +65,8 @@ export class Parser {\n     for (i = 0; i < l; i++) {\n       token = tokens[i];\n+      // take line-numbers from tokens whenever possible\n+      // and update the renderer's html attribute with the new value\n+      this.ln = token.ln || this.ln;\n+      this.renderer.tag_ln(this.ln);\n \n       // Run any renderer extensions\n@@ -124,7 +129,10 @@ export class Parser {\n             }\n \n-            body += this.renderer.tablerow(cell);\n+            // the +2 is to skip the table header\n+            body += this.renderer.tag_ln(token.ln + j + 2).tablerow(cell);\n           }\n-          out += this.renderer.table(header, body);\n+          // the html attribute is now at the end of the table,\n+          // reset it before writing the <table> tag now\n+          out += this.renderer.tag_ln(token.ln).table(header, body);\n           continue;\n         }\n@@ -167,8 +175,12 @@ export class Parser {\n \n             itemBody += this.parse(item.tokens, loose);\n-            body += this.renderer.listitem(itemBody, task, checked);\n+            // similar to tables, writing contents before the <ul> tag\n+            // so update the tag attribute as we go\n+            // (assuming all list entries got tagged with a source-line, probably safe w)\n+            body += this.renderer.tag_ln((item.tokens[0] || token).ln).listitem(itemBody, task, checked);\n           }\n \n-          out += this.renderer.list(body, ordered, start);\n+          // then reset to the <ul>'s correct line number and write it\n+          out += this.renderer.tag_ln(token.ln).list(body, ordered, start);\n           continue;\n         }\n@@ -179,5 +191,6 @@ export class Parser {\n         }\n         case 'paragraph': {\n-          out += this.renderer.paragraph(this.parseInline(token.tokens));\n+          let t = this.parseInline(token.tokens);\n+          out += this.renderer.tag_ln(token.ln).paragraph(t);\n           continue;\n         }\n@@ -221,4 +234,7 @@ export class Parser {\n       token = tokens[i];\n \n+      // another thing that only affects <br/> and other inlines\n+      this.ln = token.ln || this.ln;\n+\n       // Run any renderer extensions\n       if (this.options.extensions && this.options.extensions.renderers && this.options.extensions.renderers[token.type]) {\ndiff --git a/src/Renderer.js b/src/Renderer.js\n--- a/src/Renderer.js\n+++ b/src/Renderer.js\n@@ -11,6 +11,12 @@ export class Renderer {\n   constructor(options) {\n     this.options = options || defaults;\n+    this.ln = \"\";\n   }\n \n+  tag_ln(n) {\n+    this.ln = ` data-ln=\"${n}\"`;\n+    return this;\n+  };\n+\n   code(code, infostring, escaped) {\n     const lang = (infostring || '').match(/\\S*/)[0];\n@@ -26,10 +32,10 @@ export class Renderer {\n \n     if (!lang) {\n-      return '<pre><code>'\n+      return '<pre' + this.ln + '><code>'\n         + (escaped ? code : escape(code, true))\n         + '</code></pre>\\n';\n     }\n \n-    return '<pre><code class=\"'\n+    return '<pre' + this.ln + '><code class=\"'\n       + this.options.langPrefix\n       + escape(lang)\n@@ -43,5 +49,5 @@ export class Renderer {\n    */\n   blockquote(quote) {\n-    return `<blockquote>\\n${quote}</blockquote>\\n`;\n+    return `<blockquote${this.ln}>\\n${quote}</blockquote>\\n`;\n   }\n \n@@ -59,9 +65,9 @@ export class Renderer {\n     if (this.options.headerIds) {\n       const id = this.options.headerPrefix + slugger.slug(raw);\n-      return `<h${level} id=\"${id}\">${text}</h${level}>\\n`;\n+      return `<h${level}${this.ln} id=\"${id}\">${text}</h${level}>\\n`;\n     }\n \n     // ignore IDs\n-    return `<h${level}>${text}</h${level}>\\n`;\n+    return `<h${level}${this.ln}>${text}</h${level}>\\n`;\n   }\n \n@@ -80,5 +86,5 @@ export class Renderer {\n    */\n   listitem(text) {\n-    return `<li>${text}</li>\\n`;\n+    return `<li${this.ln}>${text}</li>\\n`;\n   }\n \n@@ -95,5 +101,5 @@ export class Renderer {\n    */\n   paragraph(text) {\n-    return `<p>${text}</p>\\n`;\n+    return `<p${this.ln}>${text}</p>\\n`;\n   }\n \n@@ -117,5 +123,5 @@ export class Renderer {\n    */\n   tablerow(content) {\n-    return `<tr>\\n${content}</tr>\\n`;\n+    return `<tr${this.ln}>\\n${content}</tr>\\n`;\n   }\n \n@@ -151,5 +157,5 @@ export class Renderer {\n \n   br() {\n-    return this.options.xhtml ? '<br/>' : '<br>';\n+    return this.options.xhtml ? `<br${this.ln}/>` : `<br${this.ln}>`;\n   }\n \n@@ -190,5 +196,5 @@ export class Renderer {\n     }\n \n-    let out = `<img src=\"${href}\" alt=\"${text}\"`;\n+    let out = `<img${this.ln} src=\"${href}\" alt=\"${text}\"`;\n     if (title) {\n       out += ` title=\"${title}\"`;\ndiff --git a/src/Tokenizer.js b/src/Tokenizer.js\n--- a/src/Tokenizer.js\n+++ b/src/Tokenizer.js\n@@ -333,4 +333,7 @@ export class Tokenizer {\n       const l = list.items.length;\n \n+      // each nested list gets +1 ahead; this hack makes every listgroup -1 but atleast it doesn't get infinitely bad\n+      this.lexer.ln--;\n+\n       // Item child tokens handled here at end because we needed to have the final item to trim it first\n       for (i = 0; i < l; i++) {\n"
  },
  {
    "path": "scripts/deps-docker/marked.patch",
    "content": "diff --git a/src/Lexer.js b/src/Lexer.js\nstrip some features\n--- a/src/Lexer.js\n+++ b/src/Lexer.js\n@@ -7,5 +7,5 @@ import { repeatString } from './helpers.js';\n  * smartypants text replacement\n  * @param {string} text\n- */\n+ *\n function smartypants(text) {\n   return text\n@@ -29,5 +29,5 @@ function smartypants(text) {\n  * mangle email addresses\n  * @param {string} text\n- */\n+ *\n function mangle(text) {\n   let out = '',\n@@ -478,5 +478,5 @@ export class Lexer {\n \n       // autolink\n-      if (token = this.tokenizer.autolink(src, mangle)) {\n+      if (token = this.tokenizer.autolink(src)) {\n         src = src.substring(token.raw.length);\n         tokens.push(token);\n@@ -485,5 +485,5 @@ export class Lexer {\n \n       // url (gfm)\n-      if (!this.state.inLink && (token = this.tokenizer.url(src, mangle))) {\n+      if (!this.state.inLink && (token = this.tokenizer.url(src))) {\n         src = src.substring(token.raw.length);\n         tokens.push(token);\n@@ -506,5 +506,5 @@ export class Lexer {\n         }\n       }\n-      if (token = this.tokenizer.inlineText(cutSrc, smartypants)) {\n+      if (token = this.tokenizer.inlineText(cutSrc)) {\n         src = src.substring(token.raw.length);\n         this.ln = token.ln || this.ln;\ndiff --git a/src/Renderer.js b/src/Renderer.js\n--- a/src/Renderer.js\n+++ b/src/Renderer.js\n@@ -173,5 +173,5 @@ export class Renderer {\n    */\n   link(href, title, text) {\n-    href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);\n+    href = cleanUrl(this.options.baseUrl, href);\n     if (href === null) {\n       return text;\n@@ -191,5 +191,5 @@ export class Renderer {\n    */\n   image(href, title, text) {\n-    href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);\n+    href = cleanUrl(this.options.baseUrl, href);\n     if (href === null) {\n       return text;\ndiff --git a/src/Tokenizer.js b/src/Tokenizer.js\n--- a/src/Tokenizer.js\n+++ b/src/Tokenizer.js\n@@ -367,14 +367,7 @@ export class Tokenizer {\n         type: 'html',\n         raw: cap[0],\n-        pre: !this.options.sanitizer\n-          && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),\n+        pre: (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),\n         text: cap[0]\n       };\n-      if (this.options.sanitize) {\n-        const text = this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0]);\n-        token.type = 'paragraph';\n-        token.text = text;\n-        token.tokens = this.lexer.inline(text);\n-      }\n       return token;\n     }\n@@ -517,15 +510,9 @@ export class Tokenizer {\n \n       return {\n-        type: this.options.sanitize\n-          ? 'text'\n-          : 'html',\n+        type: 'html',\n         raw: cap[0],\n         inLink: this.lexer.state.inLink,\n         inRawBlock: this.lexer.state.inRawBlock,\n-        text: this.options.sanitize\n-          ? (this.options.sanitizer\n-            ? this.options.sanitizer(cap[0])\n-            : escape(cap[0]))\n-          : cap[0]\n+        text: cap[0]\n       };\n     }\n@@ -714,10 +701,10 @@ export class Tokenizer {\n   }\n \n-  autolink(src, mangle) {\n+  autolink(src) {\n     const cap = this.rules.inline.autolink.exec(src);\n     if (cap) {\n       let text, href;\n       if (cap[2] === '@') {\n-        text = escape(this.options.mangle ? mangle(cap[1]) : cap[1]);\n+        text = escape(cap[1]);\n         href = 'mailto:' + text;\n       } else {\n@@ -742,10 +729,10 @@ export class Tokenizer {\n   }\n \n-  url(src, mangle) {\n+  url(src) {\n     let cap;\n     if (cap = this.rules.inline.url.exec(src)) {\n       let text, href;\n       if (cap[2] === '@') {\n-        text = escape(this.options.mangle ? mangle(cap[0]) : cap[0]);\n+        text = escape(cap[0]);\n         href = 'mailto:' + text;\n       } else {\n@@ -779,12 +766,12 @@ export class Tokenizer {\n   }\n \n-  inlineText(src, smartypants) {\n+  inlineText(src) {\n     const cap = this.rules.inline.text.exec(src);\n     if (cap) {\n       let text;\n       if (this.lexer.state.inRawBlock) {\n-        text = this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0];\n+        text = cap[0];\n       } else {\n-        text = escape(this.options.smartypants ? smartypants(cap[0]) : cap[0]);\n+        text = escape(cap[0]);\n       }\n       return {\ndiff --git a/src/defaults.js b/src/defaults.js\n--- a/src/defaults.js\n+++ b/src/defaults.js\n@@ -11,11 +11,7 @@ export function getDefaults() {\n     hooks: null,\n     langPrefix: 'language-',\n-    mangle: true,\n     pedantic: false,\n     renderer: null,\n-    sanitize: false,\n-    sanitizer: null,\n     silent: false,\n-    smartypants: false,\n     tokenizer: null,\n     walkTokens: null,\ndiff --git a/src/helpers.js b/src/helpers.js\n--- a/src/helpers.js\n+++ b/src/helpers.js\n@@ -78,18 +78,5 @@ const originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;\n  * @param {string} href\n  */\n-export function cleanUrl(sanitize, base, href) {\n-  if (sanitize) {\n-    let prot;\n-    try {\n-      prot = decodeURIComponent(unescape(href))\n-        .replace(nonWordAndColonTest, '')\n-        .toLowerCase();\n-    } catch (e) {\n-      return null;\n-    }\n-    if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {\n-      return null;\n-    }\n-  }\n+export function cleanUrl(base, href) {\n   if (base && !originIndependentUrl.test(href)) {\n     href = resolveUrl(base, href);\n@@ -233,10 +220,4 @@ export function findClosingBracket(str, b) {\n }\n \n-export function checkSanitizeDeprecation(opt) {\n-  if (opt && opt.sanitize && !opt.silent) {\n-    console.warn('marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options');\n-  }\n-}\n-\n // copied from https://stackoverflow.com/a/5450113/806777\n /**\ndiff --git a/src/marked.js b/src/marked.js\n--- a/src/marked.js\n+++ b/src/marked.js\n@@ -7,5 +7,4 @@ import { Slugger } from './Slugger.js';\n import { Hooks } from './Hooks.js';\n import {\n-  checkSanitizeDeprecation,\n   escape\n } from './helpers.js';\n@@ -18,5 +17,5 @@ import {\n function onError(silent, async, callback) {\n   return (e) => {\n-    e.message += '\\nPlease report this to https://github.com/markedjs/marked.';\n+    e.message += '\\nmake issue @ https://github.com/9001/copyparty';\n \n     if (silent) {\n@@ -65,6 +64,4 @@ function parseMarkdown(lexer, parser) {\n     }\n \n-    checkSanitizeDeprecation(opt);\n-\n     if (opt.hooks) {\n       opt.hooks.options = opt;\ndiff --git a/test/bench.js b/test/bench.js\n--- a/test/bench.js\n+++ b/test/bench.js\n@@ -39,5 +39,4 @@ export async function runBench(options) {\n     breaks: false,\n     pedantic: false,\n-    sanitize: false\n   });\n   if (options.marked) {\n@@ -50,5 +49,4 @@ export async function runBench(options) {\n     breaks: false,\n     pedantic: false,\n-    sanitize: false\n   });\n   if (options.marked) {\n@@ -61,5 +59,4 @@ export async function runBench(options) {\n   //   breaks: false,\n   //   pedantic: false,\n-  //   sanitize: false\n   // });\n   // if (options.marked) {\ndiff --git a/test/specs/run-spec.js b/test/specs/run-spec.js\n--- a/test/specs/run-spec.js\n+++ b/test/specs/run-spec.js\n@@ -25,9 +25,4 @@ function runSpecs(title, dir, showCompletionTable, options) {\n           }\n \n-          if (spec.options.sanitizer) {\n-            // eslint-disable-next-line no-eval\n-            spec.options.sanitizer = eval(spec.options.sanitizer);\n-          }\n-\n           (spec.only ? fit : (spec.skip ? xit : it))('should ' + passFail + example, async() => {\n             const before = process.hrtime();\n@@ -56,3 +51,2 @@ runSpecs('Original', './original', false, { gfm: false, pedantic: true });\n runSpecs('New', './new');\n runSpecs('ReDOS', './redos');\n-runSpecs('Security', './security', false, { silent: true }); // silent - do not show deprecation warning\ndiff --git a/test/unit/Lexer-spec.js b/test/unit/Lexer-spec.js\n--- a/test/unit/Lexer-spec.js\n+++ b/test/unit/Lexer-spec.js\n@@ -794,5 +794,5 @@ paragraph\n     });\n \n-    it('sanitize', () => {\n+    /*it('sanitize', () => {\n       expectTokens({\n         md: '<div>html</div>',\n@@ -812,5 +812,5 @@ paragraph\n         ]\n       });\n-    });\n+    });*/\n   });\n \n@@ -892,5 +892,5 @@ paragraph\n       });\n \n-      it('html sanitize', () => {\n+      /*it('html sanitize', () => {\n         expectInlineTokens({\n           md: '<div>html</div>',\n@@ -900,5 +900,5 @@ paragraph\n           ]\n         });\n-      });\n+      });*/\n \n       it('link', () => {\n@@ -1211,5 +1211,5 @@ paragraph\n         });\n \n-        it('autolink mangle email', () => {\n+        /*it('autolink mangle email', () => {\n           expectInlineTokens({\n             md: '<test@example.com>',\n@@ -1231,5 +1231,5 @@ paragraph\n             ]\n           });\n-        });\n+        });*/\n \n         it('url', () => {\n@@ -1268,5 +1268,5 @@ paragraph\n         });\n \n-        it('url mangle email', () => {\n+        /*it('url mangle email', () => {\n           expectInlineTokens({\n             md: 'test@example.com',\n@@ -1288,5 +1288,5 @@ paragraph\n             ]\n           });\n-        });\n+        });*/\n       });\n \n@@ -1304,5 +1304,5 @@ paragraph\n       });\n \n-      describe('smartypants', () => {\n+      /*describe('smartypants', () => {\n         it('single quotes', () => {\n           expectInlineTokens({\n@@ -1374,5 +1374,5 @@ paragraph\n           });\n         });\n-      });\n+      });*/\n     });\n   });\n"
  },
  {
    "path": "scripts/deps-docker/mini-fa.css",
    "content": "\n/*\nthat was the original copyright ^\nnow here's a tiny subset of fontawesome\n*/\n\n@font-face {\nfont-family: 'fa';\nfont-style: normal;\nfont-weight: 400;\nfont-display: block;\nsrc: url(\"mini-fa.woff\") format(\"woff\");\n}\n\n.fa,\n.fas,\n.far,\n.fal,\n.fad,\n.fab {\n-moz-osx-font-smoothing: grayscale;\n-webkit-font-smoothing: antialiased;\ndisplay: inline-block;\nfont-style: normal;\nfont-variant: normal;\ntext-rendering: auto;\nline-height: 1;\nfont-family: 'fa';\nfont-weight: 400;\n}\n\n:add\narrows-alt\nbold\ncode\ncolumns\neraser\neye\nheading\nimage\nitalic\nlightbulb\nlink\nlist-ol\nlist-ul\nminus\nquestion-circle\nquote-left\nredo\nsave\nstrikethrough\ntable\nundo"
  },
  {
    "path": "scripts/deps-docker/mini-fa.sh",
    "content": "#!/bin/ash\nset -e\n\norig_css=\"$(find /z/fontawesome-fre* -name fontawesome.css | head -n 1)\"\norig_woff=\"$(find /z/fontawesome-fre* -name fa-solid-900.woff | head -n 1)\"\n\n# first grab the copyright meme\nawk '1; / *\\*\\// {exit}' <\"$orig_css\" >/z/dist/mini-fa.css\n\n# then add the static part of our css template\nawk '/^:add/ {exit} 1' </z/mini-fa.css >>/z/dist/mini-fa.css\n\n# then take the list of icons to include\nawk 'o; /^:add/ {o=1}' </z/mini-fa.css |\nwhile IFS= read -r g; do\n    # and grab them from the upstream css\n    awk 'o{gsub(/[ ;]+/,\"\");print;exit} /^\\.fa-'$g':before/ {o=1;printf \"%s\",$0}' <\"$orig_css\"\ndone >>/z/dist/mini-fa.css\n\n# expecting this input btw:\n# .fa-python:before {\n#  content: \"\\f3e2\"; }\n\n# get the codepoints (should produce lines like \"f3e2\")\nawk '/:before .content:\"\\\\/ {sub(/[^\"]+\"./,\"\"); sub(/\".*/,\"\"); print}' </z/dist/mini-fa.css >/z/icon.list\n\n# and finally create a woff with just our icons\npyftsubset \"$orig_woff\" --unicodes-file=/z/icon.list --no-ignore-missing-unicodes --flavor=woff --with-zopfli --output-file=/z/dist/no-pk/mini-fa.woff --verbose\n\n# scp is easier, just want basic latin\npyftsubset /z/scp.woff2 --unicodes=\"20-7e,ab,b7,bb,2022\" --no-ignore-missing-unicodes --flavor=woff2 --output-file=/z/dist/no-pk/scp.woff2 --verbose\n\nexit 0\n\n# kinda works but ruins hinting on windows, just use the old version of the font which has correct baseline\npython3 shiftbase.py /z/dist/no-pk/scp.woff2\ncd /z/dist/no-pk/\nmv scp.woff2.woff2 scp.woff2\n"
  },
  {
    "path": "scripts/deps-docker/shiftbase.py",
    "content": "#!/usr/bin/env python3\n\nimport sys\nfrom fontTools.ttLib import TTFont, newTable\n\n\ndef main():\n    woff = sys.argv[1]\n    font = TTFont(woff)\n    print(repr(font[\"hhea\"].__dict__))\n    print(repr(font[\"OS/2\"].__dict__))\n    # font[\"hhea\"].ascent = round(base_asc * mul)\n    # font[\"hhea\"].descent = round(base_desc * mul)\n    # font[\"OS/2\"].usWinAscent = round(base_asc * mul)\n    font[\"OS/2\"].usWinDescent = round(font[\"OS/2\"].usWinDescent * 1.1)\n    font[\"OS/2\"].sTypoDescender = round(font[\"OS/2\"].sTypoDescender * 1.1)\n\n    try:\n        del font[\"post\"].mapping[\"Delta#1\"]\n    except:\n        pass\n\n    font.save(woff + \".woff2\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/deps-docker/showdown.patch",
    "content": "diff -NarU1 showdown-orig/Gruntfile.js showdown-mod/Gruntfile.js\n--- showdown-orig/Gruntfile.js\t2020-04-23 06:22:01.486676149 +0000\n+++ showdown-mod/Gruntfile.js\t2020-04-23 08:03:56.700219788 +0000\n@@ -27,3 +27,2 @@\n           'src/subParsers/*.js',\n-          'src/subParsers/makeMarkdown/*.js',\n           'src/loader.js'\ndiff -NarU1 showdown-orig/src/converter.js showdown-mod/src/converter.js\n--- showdown-orig/src/converter.js\t2020-04-23 06:22:01.496676150 +0000\n+++ showdown-mod/src/converter.js\t2020-04-23 08:20:11.056920123 +0000\n@@ -84,5 +84,5 @@\n \n-    if (options.extensions) {\n+    /*if (options.extensions) {\n       showdown.helper.forEach(options.extensions, _parseExtension);\n-    }\n+    }*/\n   }\n@@ -95,3 +95,3 @@\n    */\n-  function _parseExtension (ext, name) {\n+  /*function _parseExtension (ext, name) {\n \n@@ -159,3 +159,3 @@\n    */\n-  function legacyExtensionLoading (ext, name) {\n+  /*function legacyExtensionLoading (ext, name) {\n     if (typeof ext === 'function') {\n@@ -351,3 +351,3 @@\n    */\n-  this.makeMarkdown = this.makeMd = function (src, HTMLParser) {\n+  /*this.makeMarkdown = this.makeMd = function (src, HTMLParser) {\n \n@@ -482,3 +482,3 @@\n    */\n-  this.addExtension = function (extension, name) {\n+  /*this.addExtension = function (extension, name) {\n     name = name || null;\n@@ -491,3 +491,3 @@\n    */\n-  this.useExtension = function (extensionName) {\n+  /*this.useExtension = function (extensionName) {\n     _parseExtension(extensionName);\n@@ -526,3 +526,3 @@\n    */\n-  this.removeExtension = function (extension) {\n+  /*this.removeExtension = function (extension) {\n     if (!showdown.helper.isArray(extension)) {\n@@ -549,3 +549,3 @@\n    */\n-  this.getAllExtensions = function () {\n+  /*this.getAllExtensions = function () {\n     return {\ndiff -NarU1 showdown-orig/src/options.js showdown-mod/src/options.js\n--- showdown-orig/src/options.js\t2020-04-23 06:22:01.496676150 +0000\n+++ showdown-mod/src/options.js\t2020-04-23 08:24:29.176929018 +0000\n@@ -118,3 +118,3 @@\n     },\n-    ghMentions: {\n+    /*ghMentions: {\n       defaultValue: false,\n@@ -127,3 +127,3 @@\n       type: 'string'\n-    },\n+    },*/\n     encodeEmails: {\ndiff -NarU1 showdown-orig/src/showdown.js showdown-mod/src/showdown.js\n--- showdown-orig/src/showdown.js\t2020-04-23 06:22:01.496676150 +0000\n+++ showdown-mod/src/showdown.js\t2020-04-23 08:25:01.976930148 +0000\n@@ -7,3 +7,2 @@\n     parsers = {},\n-    extensions = {},\n     globalOptions = getDefaultOpts(true),\n@@ -25,5 +24,4 @@\n         ghCompatibleHeaderId:                 true,\n-        ghMentions:                           true,\n+        //ghMentions:                           true,\n         backslashEscapesHTMLTags:             true,\n-        emoji:                                true,\n         splitAdjacentBlockquotes:             true\n@@ -48,3 +46,3 @@\n         requireSpaceBeforeHeadingText:        true,\n-        ghMentions:                           false,\n+        //ghMentions:                           false,\n         encodeEmails:                         true\n@@ -65,3 +63,2 @@\n  */\n-showdown.extensions = {};\n \n@@ -193,3 +190,3 @@\n  */\n-showdown.extension = function (name, ext) {\n+/*showdown.extension = function (name, ext) {\n   'use strict';\n@@ -235,3 +232,3 @@\n  */\n-showdown.getAllExtensions = function () {\n+/*showdown.getAllExtensions = function () {\n   'use strict';\n@@ -244,3 +241,3 @@\n  */\n-showdown.removeExtension = function (name) {\n+/*showdown.removeExtension = function (name) {\n   'use strict';\n@@ -252,3 +249,3 @@\n  */\n-showdown.resetExtensions = function () {\n+/*showdown.resetExtensions = function () {\n   'use strict';\n@@ -263,3 +260,3 @@\n  */\n-function validate (extension, name) {\n+/*function validate (extension, name) {\n   'use strict';\n@@ -370,3 +367,3 @@\n  */\n-showdown.validateExtension = function (ext) {\n+/*showdown.validateExtension = function (ext) {\n   'use strict';\n@@ -380 +377,2 @@\n };\n+*/\ndiff -NarU1 showdown-orig/src/subParsers/anchors.js showdown-mod/src/subParsers/anchors.js\n--- showdown-orig/src/subParsers/anchors.js\t2020-04-23 06:22:01.496676150 +0000\n+++ showdown-mod/src/subParsers/anchors.js\t2020-04-23 08:25:26.880264347 +0000\n@@ -76,3 +76,3 @@\n   // Lastly handle GithubMentions if option is enabled\n-  if (options.ghMentions) {\n+  /*if (options.ghMentions) {\n     text = text.replace(/(^|\\s)(\\\\)?(@([a-z\\d]+(?:[a-z\\d.-]+?[a-z\\d]+)*))/gmi, function (wm, st, escape, mentions, username) {\n@@ -93,3 +93,3 @@\n     });\n-  }\n+  }*/\n \ndiff -NarU1 showdown-orig/src/subParsers/spanGamut.js showdown-mod/src/subParsers/spanGamut.js\n--- showdown-orig/src/subParsers/spanGamut.js\t2020-04-23 06:22:01.496676150 +0000\n+++ showdown-mod/src/subParsers/spanGamut.js\t2020-04-23 08:07:50.460227880 +0000\n@@ -22,3 +22,2 @@\n   text = showdown.subParser('simplifiedAutoLinks')(text, options, globals);\n-  text = showdown.subParser('emoji')(text, options, globals);\n   text = showdown.subParser('underline')(text, options, globals);\n@@ -26,3 +25,2 @@\n   text = showdown.subParser('strikethrough')(text, options, globals);\n-  text = showdown.subParser('ellipsis')(text, options, globals);\n \ndiff -NarU1 showdown-orig/test/node/showdown.Converter.js showdown-mod/test/node/showdown.Converter.js\n--- showdown-orig/test/node/showdown.Converter.js\t2020-04-23 06:22:01.520009484 +0000\n+++ showdown-mod/test/node/showdown.Converter.js\t2020-04-23 08:14:58.086909318 +0000\n@@ -29,3 +29,3 @@\n \n-  describe('Converter.options extensions', function () {\n+  /*describe('Converter.options extensions', function () {\n     var runCount;\n@@ -48,3 +48,3 @@\n     });\n-  });\n+  });*/\n \n@@ -115,3 +115,3 @@\n \n-  describe('extension methods', function () {\n+  /*describe('extension methods', function () {\n     var extObjMock = {\n@@ -145,3 +145,3 @@\n     });\n-  });\n+  });*/\n \ndiff -NarU1 showdown-orig/test/node/showdown.js showdown-mod/test/node/showdown.js\n--- showdown-orig/test/node/showdown.js\t2020-04-23 06:22:01.523342816 +0000\n+++ showdown-mod/test/node/showdown.js\t2020-04-23 08:14:31.733575073 +0000\n@@ -25,3 +25,3 @@\n \n-describe('showdown.extension()', function () {\n+/*describe('showdown.extension()', function () {\n   'use strict';\n@@ -110,3 +110,3 @@\n   });\n-});\n+});*/\n \ndiff -NarU1 showdown-orig/test/node/testsuite.features.js showdown-mod/test/node/testsuite.features.js\n--- showdown-orig/test/node/testsuite.features.js\t2020-04-23 06:22:01.523342816 +0000\n+++ showdown-mod/test/node/testsuite.features.js\t2020-04-23 08:25:48.880265106 +0000\n@@ -13,3 +13,2 @@\n     rawPrefixHeaderIdSuite = bootstrap.getTestSuite('test/features/rawPrefixHeaderId/'),\n-    emojisSuite = bootstrap.getTestSuite('test/features/emojis/'),\n     underlineSuite = bootstrap.getTestSuite('test/features/underline/'),\n@@ -69,4 +68,4 @@\n         converter = new showdown.Converter({ghCompatibleHeaderId: true});\n-      } else if (testsuite[i].name === 'ghMentions') {\n-        converter = new showdown.Converter({ghMentions: true});\n+      //} else if (testsuite[i].name === 'ghMentions') {\n+      //  converter = new showdown.Converter({ghMentions: true});\n       } else if (testsuite[i].name === 'disable-email-encoding') {\n@@ -185,17 +184,2 @@\n       it(suite[i].name.replace(/-/g, ' '), assertion(suite[i], converter));\n-    }\n-  });\n-\n-  /** test emojis support **/\n-  describe('emojis support', function () {\n-    var converter,\n-        suite = emojisSuite;\n-    for (var i = 0; i < suite.length; ++i) {\n-      if (suite[i].name === 'simplifiedautolinks') {\n-        converter = new showdown.Converter({emoji: true, simplifiedAutoLink: true});\n-      } else {\n-        converter = new showdown.Converter({emoji: true});\n-      }\n-\n-      it(suite[i].name.replace(/-/g, ' '), assertion(suite[i], converter));\n     }\n"
  },
  {
    "path": "scripts/deps-docker/zopfli.makefile",
    "content": "all: $(addsuffix .gz, $(wildcard *.js *.css))\n\n%.gz: %\n\tpigz -11 -I 2048 $<\n\n# pigz -11 -J 34 -I 100 -F < $< > $@.first\n"
  },
  {
    "path": "scripts/docker/Dockerfile.ac",
    "content": "FROM    alpine:latest\nWORKDIR /z\nLABEL   org.opencontainers.image.url=\"https://github.com/9001/copyparty\" \\\n        org.opencontainers.image.source=\"https://github.com/9001/copyparty\" \\\n        org.opencontainers.image.licenses=\"MIT\" \\\n        org.opencontainers.image.title=\"copyparty-ac\" \\\n        org.opencontainers.image.description=\"copyparty with Pillow and FFmpeg (image/audio/video thumbnails, audio transcoding, media tags)\"\nENV     XDG_CONFIG_HOME=/cfg\n\nRUN     apk --no-cache add !pyc \\\n            tzdata wget mimalloc2 mimalloc2-insecure \\\n            py3-jinja2 py3-argon2-cffi py3-pyzmq \\\n            py3-openssl py3-paramiko py3-pillow\n\nCOPY    i innvikler.sh ./\nRUN     ash innvikler.sh ac\n\nWORKDIR /state\nEXPOSE  3923\nENTRYPOINT [\"/bin/ash\", \"/z/cpp.sh\", \"-c\", \"/z/initcfg\"]\n"
  },
  {
    "path": "scripts/docker/Dockerfile.dj",
    "content": "FROM    alpine:latest\nWORKDIR /z\nLABEL   org.opencontainers.image.url=\"https://github.com/9001/copyparty\" \\\n        org.opencontainers.image.source=\"https://github.com/9001/copyparty\" \\\n        org.opencontainers.image.licenses=\"MIT\" \\\n        org.opencontainers.image.title=\"copyparty-dj\" \\\n        org.opencontainers.image.description=\"copyparty with all optional dependencies, including musical key / bpm detection\"\nENV     XDG_CONFIG_HOME=/cfg\n\nCOPY    i/bin/mtag/install-deps.sh ./\nCOPY    i/bin/mtag/audio-bpm.py /mtag/\nCOPY    i/bin/mtag/audio-key.py /mtag/\nRUN     apk add -U !pyc \\\n            tzdata wget mimalloc2 mimalloc2-insecure \\\n            py3-jinja2 py3-argon2-cffi py3-pyzmq \\\n            py3-openssl py3-paramiko py3-pillow \\\n            py3-pip py3-cffi \\\n            py3-magic \\\n            vips-jxl vips-poppler vips-magick \\\n            py3-numpy fftw libsndfile \\\n            vamp-sdk vamp-sdk-libs keyfinder-cli \\\n            libraw py3-numpy \\\n        && apk add -t .bd \\\n            bash wget gcc g++ make cmake patchelf \\\n            ffmpeg ffmpeg-dev \\\n            python3-dev fftw-dev libsndfile-dev \\\n            py3-wheel py3-numpy-dev libffi-dev \\\n            vamp-sdk-dev \\\n            libraw-dev py3-numpy-dev cython \\\n        && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \\\n        && python3 -m pip install pyvips \\\n        && python3 -m pip install \"$(wget -O- https://api.github.com/repos/letmaik/rawpy/releases/latest | awk -F\\\" '$2==\"tarball_url\"{print$4}')\" \\\n        && bash install-deps.sh \\\n        && apk del py3-pip .bd \\\n        && chmod 777 /root \\\n        && ln -s /root/vamp /root/.local /\n\nCOPY    i innvikler.sh ./\nRUN     ash innvikler.sh dj\n\nWORKDIR /state\nEXPOSE  3923\nENTRYPOINT [\"/bin/ash\", \"/z/cpp.sh\", \"-c\", \"/z/initcfg\"]\n"
  },
  {
    "path": "scripts/docker/Dockerfile.djd",
    "content": "FROM    DO_NOT_USE_THIS_DOCKER_IMAGE\n# this image is an unmaintained experiment to see whether alpine was the correct choice (it was)\n\n#FROM    debian:12-slim\nWORKDIR /z\nLABEL   org.opencontainers.image.url=\"https://github.com/9001/copyparty\" \\\n        org.opencontainers.image.source=\"https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker\" \\\n        org.opencontainers.image.licenses=\"MIT\" \\\n        org.opencontainers.image.title=\"copyparty-djd\" \\\n        org.opencontainers.image.description=\"copyparty with all optional dependencies, including musical key / bpm detection, and higher performance than the other editions\"\nENV     PYTHONPYCACHEPREFIX=/tmp/pyc \\\n        XDG_CONFIG_HOME=/cfg\n\nCOPY    i/bin/mtag/install-deps.sh ./\nCOPY    i/bin/mtag/audio-bpm.py /mtag/\nCOPY    i/bin/mtag/audio-key.py /mtag/\n\n# Suites: bookworm bookworm-updates\n# Components: main\nRUN     sed -ri 's/( main)$/\\1 contrib non-free non-free-firmware/' /etc/apt/sources.list.d/debian.sources \\\n        && apt update \\\n        && DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \\\n            wget \\\n            python3-argon2 python3-pillow python3-pip \\\n            ffmpeg libvips42 vamp-plugin-sdk \\\n            python3-numpy libfftw3-double3 libsndfile1 \\\n            gcc g++ make cmake patchelf jq \\\n            libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev libavutil-dev \\\n            libfftw3-dev python3-dev libsndfile1-dev python3-pip \\\n            patchelf cmake \\\n        && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \\\n        && python3 -m pip install --user pyvips \\\n        && bash install-deps.sh \\\n        && DEBIAN_FRONTEND=noninteractive apt purge -y \\\n            gcc g++ make cmake patchelf jq \\\n            libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev libavutil-dev \\\n            libfftw3-dev python3-dev libsndfile1-dev python3-pip \\\n            patchelf cmake \\\n        && DEBIAN_FRONTEND=noninteractive apt-get autoremove -y \\\n        && dpkg -r --force-depends libraqm0 libgdbm-compat4 libgdbm6 libperl5.36 perl-modules-5.36 mailcap mime-support \\\n        && DEBIAN_FRONTEND=noninteractive apt-get clean -y \\\n        && find /usr/ -name __pycache__ | xargs rm -rf \\\n        && find /usr/ -type d -name tests | grep packages/numpy | xargs rm -rf \\\n        && rm -rf \\\n            /var/lib/apt/lists/* \\\n            /tmp/pyc \\\n            /usr/lib/python*/dist-packages/pip \\\n            /usr/lib/python*/dist-packages/setuptools \\\n            /usr/lib/*/dri \\\n            /usr/lib/*/mfx \\\n            /usr/share/doc \\\n            /usr/share/X11 \\\n            /usr/share/fonts \\\n            /usr/share/libmysofa \\\n            /usr/share/libthai \\\n            /usr/share/alsa \\\n            /usr/share/bash-completion \\\n        && chmod 777 /root \\\n        && ln -s /root/vamp /root/.local / \\\n        && mkdir /cfg /w \\\n        && chmod 777 /cfg /w \\\n        && echo % /cfg > initcfg\n\nCOPY    i/dist/copyparty-sfx.py ./\nWORKDIR /w\nEXPOSE  3923\nENTRYPOINT [\"python3\", \"/z/copyparty-sfx.py\", \"--no-crt\", \"-c\", \"/z/initcfg\"]\n\n# size: 598 MB\n# bpm/key: 485 sec\n# idx-bench: 2751 MB/s\n\n# notes:\n# libraqm0 (pillow dep) pulls in the other packages mentioned on the dpkg line; saves 50m\n\n# advantage: official packages only\n# advantage: ffmpeg with gme, codec2, radiance-hdr\n# drawback: ffmpeg bloat; dc1394, flite, mfx, xorg\n# drawback: python packaging is a bit jank\n# drawback: they apply exciting patches due to old deps\n# drawback: dropping perl the hard way might cause issues\n"
  },
  {
    "path": "scripts/docker/Dockerfile.djf",
    "content": "FROM    DO_NOT_USE_THIS_DOCKER_IMAGE\n# this image is an unmaintained experiment to see whether alpine was the correct choice (it was)\n\n#FROM    fedora:39\nWORKDIR /z\nLABEL   org.opencontainers.image.url=\"https://github.com/9001/copyparty\" \\\n        org.opencontainers.image.source=\"https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker\" \\\n        org.opencontainers.image.licenses=\"MIT\" \\\n        org.opencontainers.image.title=\"copyparty-djf\" \\\n        org.opencontainers.image.description=\"copyparty with all optional dependencies, including musical key / bpm detection, and higher performance than the other editions\"\nENV     PYTHONPYCACHEPREFIX=/tmp/pyc \\\n        XDG_CONFIG_HOME=/cfg\n\nCOPY    i/bin/mtag/install-deps.sh ./\nCOPY    i/bin/mtag/audio-bpm.py /mtag/\nCOPY    i/bin/mtag/audio-key.py /mtag/\nRUN     dnf install -y \\\n            https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm \\\n            https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm \\\n        && dnf install -y --setopt=install_weak_deps=False \\\n            wget \\\n            python3-argon2-cffi python3-pillow python3-pip python3-cffi \\\n            ffmpeg \\\n            vips vips-jxl vips-poppler vips-magick \\\n            python3-numpy fftw libsndfile \\\n            gcc gcc-c++ make cmake patchelf jq \\\n            python3-devel ffmpeg-devel fftw-devel libsndfile-devel python3-setuptools python3-wheel \\\n            vamp-plugin-sdk qm-vamp-plugins \\\n            vamp-plugin-sdk-devel vamp-plugin-sdk-static \\\n        && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \\\n        && python3 -m pip install --user pyvips \\\n        && bash install-deps.sh \\\n        && dnf erase -y \\\n            gcc gcc-c++ make cmake patchelf jq \\\n            python3-devel ffmpeg-devel fftw-devel libsndfile-devel python3-setuptools python3-wheel \\\n            vamp-plugin-sdk-devel vamp-plugin-sdk-static \\\n        && dnf clean all \\\n        && find /usr/ -name __pycache__ | xargs rm -rf \\\n        && find /usr/ -type d -name tests | grep packages/numpy | xargs rm -rf \\\n        && rm -rf \\\n            /usr/share/adobe \\\n            /usr/share/fonts \\\n            /usr/share/graphviz \\\n            /usr/share/poppler/cMap \\\n            /usr/share/licenses \\\n            /usr/share/ghostscript \\\n            /usr/share/tesseract \\\n            /usr/share/X11 \\\n            /usr/share/hwdata \\\n            /usr/share/python-wheels \\\n            /usr/bin/cyrusbdb2current \\\n        && rm -rf /tmp/pyc \\\n        && chmod 777 /root \\\n        && ln -s /root/vamp /root/.local / \\\n        && mkdir /cfg /w \\\n        && chmod 777 /cfg /w \\\n        && echo % /cfg > initcfg\n\nCOPY    i/dist/copyparty-sfx.py ./\nWORKDIR /w\nEXPOSE  3923\nENTRYPOINT [\"python3\", \"/z/copyparty-sfx.py\", \"--no-crt\", \"-c\", \"/z/initcfg\"]\n\n# size: 648 MB\n# bpm/key: 410 sec\n# idx-bench: 2744 MB/s\n\n# advantage: fairly recent and sane ffmpeg build\n# drawback: ffmpeg without gme, codec2, radiance-hdr\n# drawback: ffmpeg from rpmfusion, which is both better and smaller than ffmpeg-free, can occasionally fail to install due to repo desync / conflicts\n# drawback: ffmpeg bloat; samba, modplug, v4l2\n# drawback: manual purging (graphviz/poppler/cmap) can break stuff\n"
  },
  {
    "path": "scripts/docker/Dockerfile.djff",
    "content": "FROM    DO_NOT_USE_THIS_DOCKER_IMAGE\n# this image is an unmaintained experiment to see whether alpine was the correct choice (it was)\n\n#FROM    fedora:38\nWORKDIR /z\nLABEL   org.opencontainers.image.url=\"https://github.com/9001/copyparty\" \\\n        org.opencontainers.image.source=\"https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker\" \\\n        org.opencontainers.image.licenses=\"MIT\" \\\n        org.opencontainers.image.title=\"copyparty-djff\" \\\n        org.opencontainers.image.description=\"copyparty with all optional dependencies, including musical key / bpm detection, and higher performance than the other editions\"\nENV     PYTHONPYCACHEPREFIX=/tmp/pyc \\\n        XDG_CONFIG_HOME=/cfg\n\nCOPY    i/bin/mtag/install-deps.sh ./\nCOPY    i/bin/mtag/audio-bpm.py /mtag/\nCOPY    i/bin/mtag/audio-key.py /mtag/\nRUN     dnf install -y --setopt=install_weak_deps=False \\\n            wget \\\n            python3-argon2-cffi python3-pillow python3-pip python3-cffi \\\n            ffmpeg-free \\\n            vips vips-jxl vips-poppler vips-magick \\\n            python3-numpy fftw libsndfile \\\n            gcc gcc-c++ make cmake patchelf jq \\\n            python3-devel ffmpeg-free-devel fftw-devel libsndfile-devel python3-setuptools \\\n            vamp-plugin-sdk qm-vamp-plugins \\\n            vamp-plugin-sdk-devel vamp-plugin-sdk-static \\\n        && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \\\n        && python3 -m pip install --user pyvips \\\n        && bash install-deps.sh \\\n        && dnf erase -y \\\n            gcc gcc-c++ make cmake patchelf jq \\\n            python3-devel ffmpeg-free-devel fftw-devel libsndfile-devel python3-setuptools \\\n            vamp-plugin-sdk-devel vamp-plugin-sdk-static \\\n        && dnf clean all \\\n        && find /usr/ -name __pycache__ | xargs rm -rf \\\n        && find /usr/ -type d -name tests | grep packages/numpy | xargs rm -rf \\\n        && rm -rf \\\n            /usr/share/adobe \\\n            /usr/share/fonts \\\n            /usr/share/graphviz \\\n            /usr/share/poppler/cMap \\\n            /usr/share/licenses \\\n            /usr/share/ghostscript \\\n            /usr/share/tesseract \\\n            /usr/share/X11 \\\n            /usr/share/hwdata \\\n            /usr/share/python-wheels \\\n            /usr/bin/cyrusbdb2current \\\n        && rm -rf /tmp/pyc \\\n        && chmod 777 /root \\\n        && ln -s /root/vamp /root/.local / \\\n        && mkdir /cfg /w \\\n        && chmod 777 /cfg /w \\\n        && echo % /cfg > initcfg\n\nCOPY    i/dist/copyparty-sfx.py ./\nWORKDIR /w\nEXPOSE  3923\nENTRYPOINT [\"python3\", \"/z/copyparty-sfx.py\", \"--no-crt\", \"-c\", \"/z/initcfg\"]\n\n# size: 673 MB\n# bpm/key: 408 sec\n# idx-bench: 2744 MB/s\n\n# drawback: less ffmpeg features we want, more features we don't (compared to rpmfusion)\n"
  },
  {
    "path": "scripts/docker/Dockerfile.dju",
    "content": "FROM    DO_NOT_USE_THIS_DOCKER_IMAGE\n# this image is an unmaintained experiment to see whether alpine was the correct choice (it was)\n\n#FROM    ubuntu:23.04\nWORKDIR /z\nLABEL   org.opencontainers.image.url=\"https://github.com/9001/copyparty\" \\\n        org.opencontainers.image.source=\"https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker\" \\\n        org.opencontainers.image.licenses=\"MIT\" \\\n        org.opencontainers.image.title=\"copyparty-dju\" \\\n        org.opencontainers.image.description=\"copyparty with all optional dependencies, including musical key / bpm detection, and higher performance than the other editions\"\nENV     PYTHONPYCACHEPREFIX=/tmp/pyc \\\n        XDG_CONFIG_HOME=/cfg\n\nCOPY    i/bin/mtag/install-deps.sh ./\nCOPY    i/bin/mtag/audio-bpm.py /mtag/\nCOPY    i/bin/mtag/audio-key.py /mtag/\n\nRUN     apt update \\\n        && DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \\\n            wget \\\n            python3-argon2 python3-pillow python3-pip \\\n            ffmpeg libvips42 vamp-plugin-sdk \\\n            python3-numpy libfftw3-double3 libsndfile1 \\\n            gcc g++ make cmake patchelf jq \\\n            libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev libavutil-dev \\\n            libfftw3-dev python3-dev libsndfile1-dev python3-pip \\\n            patchelf cmake \\\n        && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED\n\nRUN     python3 -m pip install --user pyvips \\\n        && bash install-deps.sh \\\n        && DEBIAN_FRONTEND=noninteractive apt purge -y \\\n            gcc g++ make cmake patchelf jq \\\n            libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev libavutil-dev \\\n            libfftw3-dev python3-dev libsndfile1-dev python3-pip \\\n            patchelf cmake \\\n        && DEBIAN_FRONTEND=noninteractive apt-get clean -y \\\n        && DEBIAN_FRONTEND=noninteractive apt-get autoremove -y \\\n        && find /usr/ -name __pycache__ | xargs rm -rf \\\n        && find /usr/ -type d -name tests | grep site-packages/numpy | xargs rm -rf \\\n        && rm -rf \\\n            /var/lib/apt/lists/* \\\n            /tmp/pyc \\\n            /usr/lib/python*/dist-packages/pip \\\n            /usr/lib/python*/dist-packages/setuptools \\\n            /usr/lib/*/dri \\\n            /usr/lib/*/mfx \\\n            /usr/share/doc \\\n            /usr/share/X11 \\\n            /usr/share/fonts \\\n            /usr/share/libmysofa \\\n            /usr/share/libthai \\\n            /usr/share/alsa \\\n            /usr/share/bash-completion \\\n        && chmod 777 /root \\\n        && ln -s /root/vamp /root/.local / \\\n        && mkdir /cfg /w \\\n        && chmod 777 /cfg /w \\\n        && echo % /cfg > initcfg\n\nCOPY    i/dist/copyparty-sfx.py ./\nWORKDIR /w\nEXPOSE  3923\nENTRYPOINT [\"python3\", \"/z/copyparty-sfx.py\", \"--no-crt\", \"-c\", \"/z/initcfg\"]\n\n# size: 1198 MB (wowee)\n# bpm/key: 516 sec\n# idx-bench: 2751 MB/s\n\n# advantage: official packages only\n# advantage: ffmpeg with gme, codec2, radiance-hdr\n# drawback: python packaging is a bit jank\n"
  },
  {
    "path": "scripts/docker/Dockerfile.im",
    "content": "FROM    alpine:latest\nWORKDIR /z\nLABEL   org.opencontainers.image.url=\"https://github.com/9001/copyparty\" \\\n        org.opencontainers.image.source=\"https://github.com/9001/copyparty\" \\\n        org.opencontainers.image.licenses=\"MIT\" \\\n        org.opencontainers.image.title=\"copyparty-im\" \\\n        org.opencontainers.image.description=\"copyparty with Pillow and Mutagen (image thumbnails, media tags)\"\nENV     XDG_CONFIG_HOME=/cfg\n\nRUN     apk --no-cache add !pyc \\\n            tzdata wget mimalloc2 mimalloc2-insecure \\\n            py3-jinja2 py3-argon2-cffi \\\n            py3-openssl py3-paramiko py3-pillow py3-mutagen\n\nCOPY    i innvikler.sh ./\nRUN     ash innvikler.sh im\n\nWORKDIR /state\nEXPOSE  3923\nENTRYPOINT [\"/bin/ash\", \"/z/cpp.sh\", \"-c\", \"/z/initcfg\"]\n"
  },
  {
    "path": "scripts/docker/Dockerfile.iv",
    "content": "FROM    alpine:latest\nWORKDIR /z\nLABEL   org.opencontainers.image.url=\"https://github.com/9001/copyparty\" \\\n        org.opencontainers.image.source=\"https://github.com/9001/copyparty\" \\\n        org.opencontainers.image.licenses=\"MIT\" \\\n        org.opencontainers.image.title=\"copyparty-iv\" \\\n        org.opencontainers.image.description=\"copyparty with Pillow, FFmpeg, libvips (image/audio/video thumbnails, audio transcoding, media tags)\"\nENV     XDG_CONFIG_HOME=/cfg\n\nRUN     apk add -U !pyc \\\n            tzdata wget mimalloc2 mimalloc2-insecure \\\n            py3-jinja2 py3-argon2-cffi py3-pyzmq \\\n            py3-openssl py3-paramiko py3-pillow \\\n            py3-pip py3-cffi \\\n            py3-magic \\\n            vips-jxl vips-poppler vips-magick \\\n            libraw py3-numpy \\\n        && apk add -t .bd \\\n            bash wget gcc g++ make cmake patchelf \\\n            python3-dev py3-wheel libffi-dev \\\n            libraw-dev py3-numpy-dev cython \\\n        && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \\\n        && python3 -m pip install pyvips \\\n        && python3 -m pip install \"$(wget -O- https://api.github.com/repos/letmaik/rawpy/releases/latest | awk -F\\\" '$2==\"tarball_url\"{print$4}')\" \\\n        && apk del py3-pip .bd\n\nCOPY    i innvikler.sh ./\nRUN     ash innvikler.sh iv\n\nWORKDIR /state\nEXPOSE  3923\nENTRYPOINT [\"/bin/ash\", \"/z/cpp.sh\", \"-c\", \"/z/initcfg\"]\n"
  },
  {
    "path": "scripts/docker/Dockerfile.min",
    "content": "FROM    alpine:latest\nWORKDIR /z\nLABEL   org.opencontainers.image.url=\"https://github.com/9001/copyparty\" \\\n        org.opencontainers.image.source=\"https://github.com/9001/copyparty\" \\\n        org.opencontainers.image.licenses=\"MIT\" \\\n        org.opencontainers.image.title=\"copyparty-min\" \\\n        org.opencontainers.image.description=\"just copyparty, no thumbnails / media tags / audio transcoding\"\nENV     XDG_CONFIG_HOME=/cfg\n\nRUN     apk --no-cache add !pyc \\\n            py3-jinja2\n\nCOPY    i innvikler.sh ./\nRUN     ash innvikler.sh min\n\nWORKDIR /state\nEXPOSE  3923\nENTRYPOINT [\"/bin/ash\", \"/z/cpp.sh\", \"--no-thumb\", \"-c\", \"/z/initcfg\"]\n"
  },
  {
    "path": "scripts/docker/Dockerfile.min.pip",
    "content": "FROM    alpine:latest\nWORKDIR /z\nLABEL   org.opencontainers.image.url=\"https://github.com/9001/copyparty\" \\\n        org.opencontainers.image.source=\"https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker\" \\\n        org.opencontainers.image.licenses=\"MIT\" \\\n        org.opencontainers.image.title=\"copyparty-min-pip\" \\\n        org.opencontainers.image.description=\"just copyparty, no thumbnails, no media tags, no audio transcoding\"\nENV     PYTHONPYCACHEPREFIX=/tmp/pyc \\\n        XDG_CONFIG_HOME=/cfg\n\nRUN     apk --no-cache add python3 py3-pip !pyc \\\n        && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \\\n        && python3 -m pip install copyparty \\\n        && apk del py3-pip \\\n        && rm -rf /tmp/pyc \\\n        && mkdir /cfg /w \\\n        && chmod 777 /cfg /w \\\n        && echo % /cfg > initcfg\n\nWORKDIR /w\nEXPOSE  3923\nENTRYPOINT [\"python3\", \"-m\", \"copyparty\", \"--no-crt\", \"--no-thumb\", \"-c\", \"/z/initcfg\"]\n"
  },
  {
    "path": "scripts/docker/Makefile",
    "content": "self := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))\n\nall:\n\t-service docker start\n\t-systemctl start docker\n\t\n\trm -rf i\n\tmkdir i\n\ttar -cC../.. dist/copyparty-sfx.py bin/mtag | tar -xvCi\n\t\n\tdocker build -t copyparty/min:latest -f Dockerfile.min .\n\techo 'scale=1;'`docker save copyparty/min:latest | pigz -c | wc -c`/1024/1024 | bc\n\n#\tdocker build -t copyparty/min-pip:latest -f Dockerfile.min.pip .\n#\techo 'scale=1;'`docker save copyparty/min-pip:latest | pigz -c | wc -c`/1024/1024 | bc\n\n\tdocker build -t copyparty/im:latest -f Dockerfile.im .\n\techo 'scale=1;'`docker save copyparty/im:latest | pigz -c | wc -c`/1024/1024 | bc\n\n\tdocker build -t copyparty/iv:latest -f Dockerfile.iv .\n\techo 'scale=1;'`docker save copyparty/iv:latest | pigz -c | wc -c`/1024/1024 | bc\n\n\tdocker build -t copyparty/ac:latest -f Dockerfile.ac .\n\techo 'scale=1;'`docker save copyparty/ac:latest | pigz -c | wc -c`/1024/1024 | bc\n\n\tdocker build -t copyparty/dj:latest -f Dockerfile.dj .\n\techo 'scale=1;'`docker save copyparty/dj:latest | pigz -c | wc -c`/1024/1024 | bc\n\n\tdocker image ls\n\nmin:\n\trm -rf i\n\tmkdir i\n\ttar -cC../.. dist/copyparty-sfx.py bin/mtag | tar -xvCi\n\n\tpodman build --squash --pull=always -t copyparty/min:latest -f Dockerfile.min .\n\techo 'scale=1;'`podman save copyparty/min:latest | pigz -c | wc -c`/1024/1024 | bc\n\npush:\n\tdocker push copyparty/min\n\tdocker push copyparty/im\n\tdocker push copyparty/iv\n\tdocker push copyparty/ac\n\tdocker push copyparty/dj\n\tdocker image tag copyparty/min:latest ghcr.io/9001/copyparty-min:latest\n\tdocker image tag copyparty/im:latest ghcr.io/9001/copyparty-im:latest\n\tdocker image tag copyparty/iv:latest ghcr.io/9001/copyparty-iv:latest\n\tdocker image tag copyparty/ac:latest ghcr.io/9001/copyparty-ac:latest\n\tdocker image tag copyparty/dj:latest ghcr.io/9001/copyparty-dj:latest\n\tdocker push ghcr.io/9001/copyparty-min:latest\n\tdocker push ghcr.io/9001/copyparty-im:latest\n\tdocker push ghcr.io/9001/copyparty-iv:latest\n\tdocker push ghcr.io/9001/copyparty-ac:latest\n\tdocker push ghcr.io/9001/copyparty-dj:latest\n\nclean:\n\t-docker kill `docker ps -q`\n\t-docker rm   `docker ps -qa`\n\t-docker rmi -f `docker images -a | awk '/<none>/{print$$3}'`\n\nhclean:\n\t-docker kill `docker ps -q`\n\t-docker rm   `docker ps -qa`\n\t-docker rmi  `docker images -a | awk '!/^alpine/&&NR>1{print$$3}'`\n\npurge:\n\t-docker kill `docker ps -q`\n\t-docker rm   `docker ps -qa`\n\t-docker rmi  `docker images -qa`\n\nsh:\n\t@printf \"\\n\\033[1;31mopening a shell in the most recently created docker image\\033[0m\\n\"\n\tdocker run --rm -it --entrypoint /bin/ash `docker images -aq | head -n 1`\n"
  },
  {
    "path": "scripts/docker/README.md",
    "content": "copyparty is availabe in these repos:\n* https://hub.docker.com/u/copyparty\n* https://github.com/9001?tab=packages&repo_name=copyparty\n\n\n# getting started\n\nrun this command to grab the latest copyparty image and start it:\n```bash\ndocker run --rm -it -u 1000 -p 3923:3923 -v /mnt/nas:/w -v $PWD/cfgdir:/cfg copyparty/ac\n```\n\n* `/w` is the path inside the container that gets shared by default, so mount one or more folders to share below there\n* `/cfg` is an optional folder with zero or more config files (*.conf) to load\n* `copyparty/ac` is the recommended [image edition](#editions)\n* you can download the image from github instead by replacing `copyparty/ac` with `ghcr.io/9001/copyparty-ac`\n* if you are using rootless podman, remove `-u 1000`\n* if you have selinux, append `:z` to all `-v` args (for example `-v /mnt/nas:/w:z`)\n\nthis example is also available as a podman-compatible [docker-compose yaml](https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/docker/basic-docker-compose); example usage: `docker-compose up` (you may need to `systemctl enable --now podman.socket` or similar)\n\ni'm not very familiar with containers, so let me know if this section could be better 🙏\n\n\n## portainer\n\n* there is a [portainer howto](https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/docker/portainer.md) which is mostly untested\n\n\n## configuration\n\n> this section basically explains how the [docker-compose yaml](https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/docker/basic-docker-compose) works, so you may look there instead\n\nthe container has the same default config as the sfx and the pypi module, meaning it will listen on port 3923 and share the \"current folder\" (`/w` inside the container) as read-write for anyone\n\nthe recommended way to configure copyparty inside a container is to mount a folder which has one or more [config files](https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/docker/basic-docker-compose/copyparty.conf) inside; `-v /your/config/folder:/cfg`\n\n* but you can also provide arguments to the docker command if you prefer that\n* config files must be named `something.conf` to get picked up\n* there are [more extensive config examples](https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf) but those are not made for docker so the paths are wrong (`/home/ed/Music` should be `/w/something` and so on)\n\nalso see [docker-specific recommendations](#docker-specific-recommendations)\n\n\n## editions\n\nwith image size after installation and when gzipped\n\n* [`min`](https://hub.docker.com/r/copyparty/min) (57 MiB, 20 gz) is just copyparty itself\n* [`im`](https://hub.docker.com/r/copyparty/im) (70 MiB, 25 gz) can thumbnail images with pillow, parse media files with mutagen\n* [`ac` (163 MiB, 56 gz)](https://hub.docker.com/r/copyparty/ac) is `im` plus ffmpeg for video/audio thumbs + audio transcoding + better tags\n* [`iv`](https://hub.docker.com/r/copyparty/iv) (211 MiB, 73 gz) is `ac` plus vips for faster heif / avic / jxl thumbnails\n* [`dj`](https://hub.docker.com/r/copyparty/dj) (309 MiB, 104 gz) is `iv` plus beatroot/keyfinder to detect musical keys and bpm\n\n[`ac` is recommended](https://hub.docker.com/r/copyparty/ac) since the additional features available in `iv` and `dj` are rarely useful\n\nmost editions support `x86`, `x86_64`, `armhf`, `aarch64`, `ppc64le`, `s390x`\n* `dj` doesn't run on `ppc64le`, `s390x`, `armhf`\n* `iv` doesn't run on `ppc64le`, `s390x`\n\n> NOTE: the following editions are unfinished experiments, and not published anywhere: djd djf djff dju\n\n\n### aftermarket editions\n\nsome notable alternative copyparty distributions, made and maintained by other people, unaffiliated unless mentioned otherwise -- please use your own judgement whether to trust these\n\n* https://github.com/0x464e/patentparty has more multimedia codecs than the official images\n  * as of writing: a modification of the official images, automatically maintained by github actions; looks clean and legit\n\n\n## detecting bpm and musical key\n\nthe `dj` edition comes with `keyfinder` and `beatroot` which can be used to detect music bpm and musical keys\n\nenable them globally in a config file:\n```yaml\n[global]\ne2dsa, e2ts  # enable filesystem indexing and multimedia indexing\nmtp: .bpm=f,t30,/mtag/audio-bpm.py  # should take ~10sec\nmtp: key=f,t190,/mtag/audio-key.py  # should take ~50sec\n```\n\nor enable them for just one volume,\n```yaml\n[/music]  # share name / URL\n  music   # filesystem path inside the docker volume `/w`\n  flags:\n    e2dsa, e2ts\n    mtp: .bpm=f,t30,/mtag/audio-bpm.py\n    mtp: key=f,t190,/mtag/audio-key.py\n```\n\nor using commandline arguments,\n```\n-e2dsa -e2ts -mtp .bpm=f,t30,/mtag/audio-bpm.py -mtp key=f,t190,/mtag/audio-key.py\n```\n\n\n# faq\n\nthe following advice is best-effort and not guaranteed to be entirely correct\n\n* q: starting a rootless container on debian 12 fails with `failed to register layer: lsetxattr user.overlay.impure /etc: operation not supported`\n  * a: docker's default rootless configuration on debian is to use the overlay2 storage driver; this does not work. Your options are to replace docker with podman (good choice), or to configure docker to use the `fuse-overlayfs` storage driver\n\n\n\n# docker-specific recommendations\n\n* copyparty will generally create a `.hist` folder at the top of each volume, which contains the filesystem index, thumbnails and such. For performance reasons, but also just to keep things tidy, it might be convenient to store these inside the config folder instead. Add the line `hist: /cfg/hists/` inside the `[global]` section of your `copyparty.conf` to do this\n\n* if you want more performance, and you're OK with doubling the RAM usage, then consider enabling mimalloc **(maybe buggy)** with one of these:\n\n  * `-e LD_PRELOAD=/usr/lib/libmimalloc-secure.so.2` makes download-as-zip **3x** as fast, filesystem-indexing **1.5x** as fast, etc.\n\n  * `-e LD_PRELOAD=/usr/lib/libmimalloc-insecure.so.2` adds another 10% speed but makes it easier to exploit future vulnerabilities\n\n  * complete example: `podman run --rm -it -p 3923:3923 -v \"$PWD:/w:z\" -e LD_PRELOAD=/usr/lib/libmimalloc-secure.so.2 copyparty/ac -v /w::r`\n\n\n## enabling the ftp server\n\n...is tricky because ftp is a weird protocol and docker is making it worse 🎉\n\nadd the following three config entries into the `[global]` section of your `copyparty.conf`:\n\n* `ftp: 3921` to enable the service, listening for connections on port 3921\n\n* `ftp-nat: 127.0.0.1` but replace `127.0.0.1` with the actual external IP of your server; the clients will only be able to connect to this IP, even if the server has multiple IPs\n  * do not add `ftp-nat` if you are running the container with host networking\n\n* `ftp-pr: 12000-12099` to restrict the [passive-mode](http://slacksite.com/other/ftp.html#passive) port selection range; this allows up to 100 simultaneous file transfers\n\nthen finally update your docker config so that the port-range you specified (12000-12099) is exposed to the internet\n\n\n# build the images yourself\n\nbasically `./make.sh hclean pull img push` but see [devnotes.md](./devnotes.md)\n\n\n## adding new packages to the image\n\nin case you wish to make a small modification to the official image, for example [add a python package you need](https://github.com/9001/copyparty/issues/479#issuecomment-3152026483), then you don't *really* need to build the whole image from scratch -- see [modifying an image](./devnotes.md#modifying-an-image) in devnotes\n"
  },
  {
    "path": "scripts/docker/base/Dockerfile.zlibng",
    "content": "FROM    alpine:latest\nWORKDIR /z\n\nRUN     apk add py3-pip make gcc musl-dev python3-dev\n\nRUN     pip wheel https://files.pythonhosted.org/packages/46/7d/901c6e333fb031b5bfbd1532099200cf859f12aa83689be494eade6685ec/zlib_ng-1.0.0.tar.gz \\\n        && mkdir whl \\\n        && mv *.whl whl\n"
  },
  {
    "path": "scripts/docker/base/Makefile",
    "content": "self := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))\n\n\nall:\n# build all outdated\n\tbash verchk.sh\n\n\nff:\n# legally comfy\n\t/usr/bin/time ./build-no265.sh img\n\n\nzlib:\n# \tbuild zlib-ng from source so we know how the sausage was made\n#\t(still only doing the archs which are officially supported/tested)\n\n\tpodman build --arch amd64 -t localhost/cpp-zlibng-amd64:latest -f Dockerfile.zlibng .\n\tpodman run --arch amd64 --rm --log-driver=none -i localhost/cpp-zlibng-amd64:latest tar -cC/z whl | tar -xv\n\n\tpodman build --arch arm64 -t localhost/cpp-zlibng-amd64:latest -f Dockerfile.zlibng .\n\tpodman run --arch arm64 --rm --log-driver=none -i localhost/cpp-zlibng-amd64:latest tar -cC/z whl | tar -xv\n\n\nsh:\n\t@printf \"\\n\\033[1;31mopening a shell in the most recently created docker image\\033[0m\\n\"\n\tdocker run --rm -it --entrypoint /bin/ash `docker images -aq | head -n 1`\n"
  },
  {
    "path": "scripts/docker/base/arbeidspakke.sh",
    "content": "#!/bin/ash\nset -e\n\n[ $1 = 1 ] && hub=1\n\nuname -a\napk add alpine-sdk doas wget\necho permit nopass root > /etc/doas.d/u.conf\ncp -pv /root/.abuild/*.pub /etc/apk/keys/ || abuild-keygen -ina\n\n##\n## yeet h265\n\nmkdir /ffmpeg\ncd /ffmpeg\nbase=https://github.com/alpinelinux/aports/raw/refs/heads/3.23-stable/community/ffmpeg/\nwget ${base}APKBUILD\nawk <APKBUILD -vb=\"$base\" '/\"/{o=0}/^source=/{o=1;next}o{print b $1}' | wget -i-\ncp -pv APKBUILD /root/\n\n# grep -E '^extern const.* FF[^ ]+ +ff_(hevc|vvc)_' libavcodec/allcodecs.c libavcodec/hwaccels.h libavcodec/bitstream_filters.c libavcodec/parsers.c libavformat/allformats.c | sed -r 's/.* ff_([^/;]*)_([^/;]*);.*/--disable-\\2=\\1/' | tr '\\n' ' '\nsed -ri 's/--enable-libx265/--disable-decoder=hevc --disable-decoder=hevc_qsv --disable-decoder=hevc_rkmpp --disable-encoder=hevc_rkmpp --disable-decoder=hevc_v4l2m2m --disable-decoder=vvc --disable-encoder=hevc_amf --disable-decoder=hevc_amf --disable-decoder=hevc_cuvid --disable-encoder=hevc_d3d12va --disable-decoder=hevc_mediacodec --disable-encoder=hevc_mediacodec --disable-encoder=hevc_mf --disable-encoder=hevc_nvenc --disable-decoder=hevc_oh --disable-encoder=hevc_oh --disable-encoder=hevc_qsv --disable-encoder=hevc_v4l2m2m --disable-encoder=hevc_vaapi --disable-encoder=hevc_videotoolbox --disable-encoder=hevc_vulkan --disable-decoder=vvc_qsv --disable-hwaccel=hevc_d3d11va --disable-hwaccel=hevc_d3d11va2 --disable-hwaccel=hevc_d3d12va --disable-hwaccel=hevc_dxva2 --disable-hwaccel=hevc_nvdec --disable-hwaccel=hevc_vaapi --disable-hwaccel=hevc_vdpau --disable-hwaccel=hevc_videotoolbox --disable-hwaccel=hevc_vulkan --disable-hwaccel=vvc_vaapi --disable-bsf=hevc_metadata --disable-bsf=hevc_mp4toannexb --disable-bsf=vvc_metadata --disable-bsf=vvc_mp4toannexb --disable-parser=hevc --disable-parser=vvc --disable-demuxer=hevc --disable-muxer=hevc --disable-demuxer=vvc --disable-muxer=vvc /;s/\\bx265-dev\\b//' APKBUILD\n\n##\n## yeet aac he/he+/ld (sbr/ps); keep lc only\n\ncat >>APKBUILD <<'EOF'\nprepare() {\n    default_prepare\n    tar -cC/opt/patch/ffmpeg . | tar -x\n    patch -p1 <aac-lc-only.patch\n}\nEOF\n\n##\n## shrink-ray\n\nsed -ri 's/--enable-lib(bluray|placebo|rav1e|shaderc)/--disable-lib\\1/; s/--enable-(vdpau)/--disable-\\1/; s/\\b(rav1e|shaderc)-dev//; s/\\blib(bluray|placebo|vdpau|xfixes)-dev\\b//' APKBUILD\n# `- rm placebo+shaderc to drop spirv-tools (1.7 MiB apk)\n\nsed -ri 's/--enable-libxcb/--disable-libxcb --disable-indev=xcbgrab --disable-ffplay --disable-encoder=opus /' APKBUILD\nsed -ri 's/\\bffplay$//; s/\\bsdl2-dev\\b//' APKBUILD\n\n##\n## golflympics; decode-only, super-specific for copyparty only\n\n[ $hub ] || {\nsed -ri 's/--enable-(ladspa|lv2|vaapi|vulkan)/--disable-\\1/' APKBUILD\nsed -ri 's/--enable-lib(aom|ass|drm|fontconfig|freetype|fribidi|harfbuzz|pulse|rist|srt|ssh|v4l2|vidstab|x264|xvid|zimg|vpl)/--disable-lib\\1/' APKBUILD\nsed -ri 's/\\b(v4l-utils|libvpx)-dev\\b//' APKBUILD  # (try to) drop v4l2_m2m, and use builtin vp8/vp9 instead of libvpx for decode\nsed -ri 's/(--disable-vulkan)/\\1 --disable-devices --disable-hwaccels --disable-encoders --enable-encoder=flac --enable-encoder=libjxl --enable-encoder=libmp3lame --enable-encoder=libopus --enable-encoder=libwebp --enable-encoder=mjpeg --enable-encoder=pcm_s16le --enable-encoder=pcm_s16le_planar --enable-encoder=png --enable-encoder=rawvideo --enable-encoder=vnull --enable-encoder=wrapped_avframe --disable-muxers --enable-muxer=aiff --enable-muxer=apng --enable-muxer=caf --enable-muxer=ffmetadata --enable-muxer=fifo --enable-muxer=flac --enable-muxer=image2 --enable-muxer=image2pipe --enable-muxer=matroska --enable-muxer=matroska_audio --enable-muxer=mjpeg --enable-muxer=mp3 --enable-muxer=null --enable-muxer=opus --enable-muxer=pcm_s16le --enable-muxer=wav --enable-muxer=webm --enable-muxer=webp --enable-muxer=yuv4mpegpipe --disable-filters --enable-filter=anoisesrc --enable-filter=asplit --enable-filter=amerge --enable-filter=amix --enable-filter=aresample --enable-filter=crop --enable-filter=showspectrumpic --enable-filter=showwavespic --enable-filter=convolution --enable-filter=volume --enable-filter=compand --enable-filter=setsar --enable-filter=scale       --disable-decoder=av1 --disable-hwaccel=v4l2_m2m --disable-decoder=h263_v4l2m2m --disable-decoder=h264_v4l2m2m --disable-decoder=mpeg1_v4l2m2m --disable-decoder=mpeg2_v4l2m2m --disable-decoder=mpeg4_v4l2m2m --disable-decoder=vc1_v4l2m2m --disable-decoder=vp8_v4l2m2m --disable-decoder=vp9_v4l2m2m --disable-decoder=subrip --disable-decoder=srt --disable-decoder=pgssub --disable-decoder=cc_dec --disable-decoder=dvdsub --disable-decoder=dvbsub --disable-decoder=ssa --disable-decoder=ass --disable-decoder=opus /' APKBUILD\n# `- s/av1/libdav1d/; s/libvorbis/vorbis/; s/opus/libopus/; libvorbis and mpg123 gets pulled in by openmpt \n}\n\np=/root/packages/$(abuild -A)\nrm -rf $p\nabuild -FrcK\n\nmkdir $p/ex\nmv $p/ffmpeg-d* $p/ex  # dbg,dev,doc\ncp -pv src/ffmpeg-*/ffbuild/config.log $p/\n\n[ $hub ] && rm -rf $p.hub && mv $p $p.hub\n\ncp -pv /root/.abuild/*.pub ~/packages/\n"
  },
  {
    "path": "scripts/docker/base/build-no265.sh",
    "content": "#!/bin/bash\nset -e\n\n[ $(id -u) -eq 0 ] && {\n\techo dont root\n\texit 1\n}\nself=$(cd -- \"$(dirname \"$BASH_SOURCE\")\"; pwd -P)\ncd \"$self\"\n\nsarchs=\"386 amd64 arm/v7 arm64/v8 ppc64le s390x\"\narchs=\"amd64 amd64 386 arm64 arm s390x ppc64le\"\n\nerr=\nfor x in awk jq podman python3 tar wget ; do\n\tcommand -v $x >/dev/null && continue\n\terr=1; echo ERROR: missing dependency: $x\ndone\n[ $err ] && exit 1\n\nfor v in \"$@\"; do\n\t[ \"$v\" = pull ] && pull=1\n\t[ \"$v\" = img  ] && img=1\ndone\n\n[ $# -gt 0 ] || {\n\techo \"need list of commands, for example: pull img\"\n\texit 1\n}\n\nwt() {\n\tprintf '\\033]0;%s\\033\\\\' \"$*\"\n\t[ -z \"$TMUX\" ] || tmux renamew \"$*\"\n}\n\n[ $pull ] && {\n\tfor a in $sarchs; do  # arm/v6\n\t\tpodman pull --arch=$a alpine:latest\n\tdone\n\n\tpodman images --format \"{{.ID}} {{.History}}\" |\n\tawk '/library\\/alpine/{print$1}' |\n\twhile read id; do\n\t\ttag=alpine-$(podman inspect $id | jq -r '.[]|.Architecture' | tr / -)\n\t\t[ -e .tag-$tag ] && continue\n\t\ttouch .tag-$tag\n\t\techo tagging $tag\n\t\tpodman untag $id\n\t\tpodman tag $id $tag\n\tdone\n\trm .tag-*\n}\n\n[ $img ] && {\n\tmkdir -p \"$self/b\"\n\n\t# enable arm32 crossbuild from aarch64 (macbook or whatever)\n\t[ $(uname -m) = aarch64 ] && [ ! -e /proc/sys/fs/binfmt_misc/qemu-arm ] &&\n\t\techo \":qemu-arm:M:0:\\x7f\\x45\\x4c\\x46\\x01\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x28\\x00:\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xfe\\xff\\xff\\xff:/usr/bin/qemu-arm-static:F\" |\n\t\tsudo tee >/dev/null /proc/sys/fs/binfmt_misc/register\n\n\t# kill abandoned builders\n\tps aux | awk '/bin\\/qemu-[^-]+-static/{print$2}' | xargs -r kill -9\n\n\tn=0; set -x\n\tfor a in $archs; do\n\t\tn=$((n+1)); wt \"$n/$a\"\n\t\t#[ $n -le 3 ] || continue\n\t\ttouch b/t.$a.1.$(date +%s)\n\n\t\ttar -c arbeidspakke.sh patch/ffmpeg |\n\t\ttime nice podman run \\\n\t\t\t--rm -i --pull=never -v \"$self/b:/root:z\" localhost/alpine-$a \\\n\t\t\t/bin/ash -c \"cd /opt;tar -x;/bin/ash ./arbeidspakke.sh $n $a\" 2>&1 |\n\t\ttee b/log.$a\n\n\t\ttouch b/t.$a.2.$(date +%s)\n\tdone\n\twt -;wt \"\"\n}\n\necho ok\n\n# just-no265\n#  4m18.77 x64\n#  4m22.81 386\n# 45m36.44 arm64 \n# 34m31.22 ppc64le\n# 50m01.04 s390x\n\n# golflympics\n#    4:09 x86_64-hub\n#    2:57 x86_64\n#    2:54 x86\n#   31:13 aarch64\n#   22:38 armv7\n#   32:17 s390x\n#   24:27 ppc64le\n# 2:00:35 summa summarum\n"
  },
  {
    "path": "scripts/docker/base/patch/ffmpeg/aac-lc-only.patch",
    "content": "remove all advanced aac features, leaving only aac-lc which is\nno longer patent-encumbered in any relevant parts of the world\n( and 99% of all aac files are lc-aac anyways so that's fine )\n\ndiff --git a/libavcodec/aac/aacdec.c b/libavcodec/aac/aacdec.c\nindex b8d53036d4..054c46f84e 100644\n--- a/libavcodec/aac/aacdec.c\n+++ b/libavcodec/aac/aacdec.c\n@@ -880,9 +880,7 @@ static int decode_pce(AVCodecContext *avctx, MPEG4AudioConfig *m4ac,\n  */\n static int decode_ga_specific_config(AACDecContext *ac, AVCodecContext *avctx,\n-                                     GetBitContext *gb,\n-                                     int get_bit_alignment,\n-                                     MPEG4AudioConfig *m4ac,\n-                                     int channel_config)\n+    GetBitContext *gb, int get_bit_alignment, MPEG4AudioConfig *m4ac, int channel_config)\n {\n+    if (m4ac->sbr > 0) return AVERROR_DECODER_NOT_FOUND;\n     int extension_flag, ret, ep_config, res_flags;\n     uint8_t layout_map[MAX_ELEM_ID*4][3];\n@@ -961,8 +959,7 @@ static int decode_ga_specific_config(AACDecContext *ac, AVCodecContext *avctx,\n \n static int decode_eld_specific_config(AACDecContext *ac, AVCodecContext *avctx,\n-                                     GetBitContext *gb,\n-                                     MPEG4AudioConfig *m4ac,\n-                                     int channel_config)\n+    GetBitContext *gb, MPEG4AudioConfig *m4ac, int channel_config)\n {\n+    return AVERROR_DECODER_NOT_FOUND;  // kill ELD support\n     int ret, ep_config, res_flags;\n     uint8_t layout_map[MAX_ELEM_ID*4][3];\n@@ -1070,5 +1067,4 @@ static int decode_audio_specific_config_gb(AACDecContext *ac,\n     case AOT_AAC_LTP:\n     case AOT_ER_AAC_LC:\n-    case AOT_ER_AAC_LD:\n         if ((ret = decode_ga_specific_config(ac, avctx, gb, get_bit_alignment,\n                                              &oc->m4ac, m4ac->chan_config)) < 0)\n@@ -1948,4 +1944,5 @@ static int decode_extension_payload(AACDecContext *ac, GetBitContext *gb, int cn\n         crc_flag++;\n     case EXT_SBR_DATA:\n+        return res;  // kill HE/SBR support\n         if (!che) {\n             av_log(ac->avctx, AV_LOG_ERROR, \"SBR was found before the first channel element.\\n\");\n@@ -2087,4 +2084,5 @@ static void spectral_to_sample(AACDecContext *ac, int samples)\n                     }\n                     if (ac->oc[1].m4ac.sbr > 0) {\n+                        exit(1);  // kill HE/SBR support\n                         ac->proc.sbr_apply(ac, che, type,\n                                            che->ch[0].output,\ndiff --git a/libavcodec/aacsbr_template.c b/libavcodec/aacsbr_template.c\nindex 31d2d844c4..b55f93752a 100644\n--- a/libavcodec/aacsbr_template.c\n+++ b/libavcodec/aacsbr_template.c\n@@ -639,4 +639,5 @@ static int read_sbr_grid(AACDecContext *ac, SpectralBandReplication *sbr,\n                          GetBitContext *gb, SBRData *ch_data)\n {\n+    exit(1);  // kill SBR support\n     int i;\n     int bs_pointer = 0;\n"
  },
  {
    "path": "scripts/docker/base/patch/ffmpeg/libavcodec/aacps.c",
    "content": "// just the signatures from the original file; all bodies/logic removed\n\n#include <stdint.h>\n#include \"libavutil/common.h\"\n#include \"libavutil/mathematics.h\"\n#include \"libavutil/mem_internal.h\"\n#include \"aacps.h\"\n#if USE_FIXED\n#include \"aacps_fixed_tablegen.h\"\n#else\n#include \"libavutil/internal.h\"\n#include \"aacps_tablegen.h\"\n#endif /* USE_FIXED */\n\nstatic void hybrid_analysis(PSDSPContext *dsp, INTFLOAT out[91][32][2],\n    INTFLOAT in[5][44][2], INTFLOAT L[2][38][64],\n    int is34, int len) {}\n\nstatic void hybrid_synthesis(PSDSPContext *dsp, INTFLOAT out[2][38][64],\n    INTFLOAT in[91][32][2], int is34, int len) {}\n\nstatic void decorrelation(PSContext *ps, INTFLOAT (*out)[32][2], const INTFLOAT (*s)[32][2], int is34) {}\n\nint AAC_RENAME(ff_ps_apply)(PSContext *ps, INTFLOAT L[2][38][64], INTFLOAT R[2][38][64], int top) { return 0; }\n\nav_cold void AAC_RENAME(ff_ps_init)(void) {}\n"
  },
  {
    "path": "scripts/docker/base/patch/ffmpeg/libavcodec/aacsbr.c",
    "content": "// just the signatures from the original file; all bodies/logic removed\n\n#define USE_FIXED 0\n\n#include \"aac.h\"\n#include \"sbr.h\"\n#include \"aacsbr.h\"\n#include \"aacsbrdata.h\"\n#include \"aacps.h\"\n#include \"sbrdsp.h\"\n#include \"libavutil/internal.h\"\n#include \"libavutil/intfloat.h\"\n#include \"libavutil/libm.h\"\n#include \"libavutil/avassert.h\"\n#include \"libavutil/mem_internal.h\"\n\n#include <stdint.h>\n#include <float.h>\n#include <math.h>\n\nstatic void aacsbr_func_ptr_init(AACSBRContext *c);\n\nstatic void make_bands(int16_t* bands, int start, int stop, int num_bands) {}\n\nstatic void sbr_dequant(SpectralBandReplication *sbr, int id_aac) {}\n\nstatic void sbr_hf_inverse_filter(SBRDSPContext *dsp,\n    float (*alpha0)[2], float (*alpha1)[2],\n    const float X_low[32][40][2], int k0) {}\n\nstatic void sbr_chirp(SpectralBandReplication *sbr, SBRData *ch_data) {}\n\nstatic void sbr_gain_calc(SpectralBandReplication *sbr,\n    SBRData *ch_data, const int e_a[2]) {}\n\nstatic void sbr_hf_assemble(float Y1[38][64][2],\n    const float X_high[64][40][2],\n    SpectralBandReplication *sbr, SBRData *ch_data,\n    const int e_a[2]) {}\n\n#include \"aacsbr_template.c\"\n"
  },
  {
    "path": "scripts/docker/base/patch/ffmpeg/libavcodec/aacsbr_fixed.c",
    "content": "// just the signatures from the original file; all bodies/logic removed\n\n#define USE_FIXED 1\n\n#include \"aac.h\"\n#include \"sbr.h\"\n#include \"aacsbr.h\"\n#include \"aacsbrdata.h\"\n#include \"aacps.h\"\n#include \"sbrdsp.h\"\n#include \"libavutil/internal.h\"\n#include \"libavutil/libm.h\"\n#include \"libavutil/avassert.h\"\n\n#include <stdint.h>\n#include <float.h>\n#include <math.h>\n\nstatic void aacsbr_func_ptr_init(AACSBRContext *c);\nstatic const int CONST_RECIP_LN2 = Q31(0.7);\nstatic const int CONST_076923    = Q31(0.7);\n\nstatic const int fixed_log_table[] = {Q31(0)};\n\nstatic int fixed_log(int x) {return 1;}\n\nstatic void make_bands(int16_t* bands, int start, int stop, int num_bands) {}\n\nstatic void sbr_dequant(SpectralBandReplication *sbr, int id_aac) {}\n\nstatic void sbr_hf_inverse_filter(SBRDSPContext *dsp,\n    int (*alpha0)[2], int (*alpha1)[2],\n    const int X_low[32][40][2], int k0) {}\n\nstatic void sbr_chirp(SpectralBandReplication *sbr, SBRData *ch_data) {}\n\nstatic void sbr_gain_calc(SpectralBandReplication *sbr,\n    SBRData *ch_data, const int e_a[2]) {}\n\nstatic void sbr_hf_assemble(int Y1[38][64][2],\n    const int X_high[64][40][2],\n    SpectralBandReplication *sbr, SBRData *ch_data,\n    const int e_a[2]) {}\n\n#include \"aacsbr_template.c\"\n"
  },
  {
    "path": "scripts/docker/base/patch/ffmpeg/libavcodec/aacsbrdata.h",
    "content": "// just the signatures from the original file; all bodies/logic removed\n\n#ifndef AVCODEC_AACSBRDATA_H\n#define AVCODEC_AACSBRDATA_H\n\n#include <stdint.h>\n#include \"libavutil/mem_internal.h\"\n#include \"aac_defines.h\"\n\nstatic const int8_t sbr_offset[6][16] = {};\n\nstatic const DECLARE_ALIGNED(32, INTFLOAT, sbr_qmf_window_ds)[320] = {};\n\nstatic const DECLARE_ALIGNED(32, INTFLOAT, sbr_qmf_window_us)[640] = {};\n\n#endif /* AVCODEC_AACSBRDATA_H */\n"
  },
  {
    "path": "scripts/docker/base/verchk.sh",
    "content": "#!/bin/bash\nset -e\n\nv=3.23\n\nmkdir -p cver\nrm -rf cver2\nmkdir cver2\ncd cver2\ncurl \\\n    -Lo1 https://raw.githubusercontent.com/alpinelinux/aports/refs/heads/$v-stable/main/musl/APKBUILD \\\n    -Lo2 https://raw.githubusercontent.com/alpinelinux/aports/refs/heads/$v-stable/main/python3/APKBUILD \\\n    -Lo3 https://raw.githubusercontent.com/alpinelinux/aports/refs/heads/$v-stable/community/ffmpeg/APKBUILD \\\n    ;\n\nzlib= ff=\ncmp 1 ../cver/1 || zlib=1\ncmp 2 ../cver/2 || zlib=1\ncmp 3 ../cver/3 || ff=1\necho zlib=$zlib ff=$ff\n\n[ \"$1\" ] && exit\n\n[ $zlib ] && { make zlib; cp -pv 1 2 ../cver/; }\n[ $ff ] &&   { make ff;   cp -pv 3 ../cver/; }\nrm -rf cver2\n"
  },
  {
    "path": "scripts/docker/devnotes.md",
    "content": "# building the images yourself\n\nif you want to build the image from scratch then you've come to the right place -- however if you only want to make small modifications to the official image then jump down to [modifying an image](#modifying-an-image) or possibly [modding on the fly](#modding-on-the-fly)\n\n```bash\n./make.sh hclean pull img push\n```\nwill download the latest copyparty-sfx.py from github unless you have [built it from scratch](../../docs/devnotes.md#just-the-sfx) and then build all the images based on that\n\ndeprecated alternative: run `make` to use the makefile however that uses docker instead of podman and only builds x86_64\n\n`make.sh` is necessarily(?) overengineered because:\n* podman keeps burning dockerhub pulls by not using the cached images (`--pull=never` does not apply to manifests)\n* podman cannot build from a local manifest, only local images or remote manifests\n\nbut I don't really know what i'm doing here 💩\n\n* auth for pushing images to repos;  \n  `podman login docker.io`  \n  `podman login ghcr.io -u 9001`  \n  [about gchq](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry) (takes a classic token as password)\n\n\n## building on alpine\n\n```bash\napk add podman{,-docker}\nrc-update add cgroups\nservice cgroups start\nvim /etc/containers/storage.conf  # driver = \"btrfs\"\nmodprobe tun\necho ed:100000:65536 >/etc/subuid\necho ed:100000:65536 >/etc/subgid\napk add qemu-openrc qemu-tools qemu-{arm,armeb,aarch64,s390x,ppc64le}\nrc-update add qemu-binfmt\nservice qemu-binfmt start\n```\n\n\n# modifying an image\n\nif you want to make a small change to the official image, for example [install a python package you need](https://github.com/9001/copyparty/issues/479), then the best approach is to build a new image based on the official one. There's a quick summary below, but check the internets if you want a better walkthrough.\n\n**NOTE:** the `min` image is **not** a good idea to modify (brittle from shoehorning); use any of the other variants instead (`im`, `ac`, `iv`, `dj`)\n\nfirst, create a new folder, and then create a new blank textfile named `Dockerfile` inside that folder:\n\n```bash\nmkdir customparty\ncd customparty\nnano Dockerfile\n```\n\nassuming you want to install the python-package \"[requests](https://pypi.org/project/requests/)\", which in [alpine](https://alpinelinux.org/) is called [py3-requests](https://pkgs.alpinelinux.org/package/edge/main/x86_64/py3-requests), then put the following contents into your `Dockerfile`:\n\n```docker\nFROM docker.io/copyparty/ac:latest\nRUN apk add --no-cache py3-requests\n```\n\nbuild the new docker-image with the additional package you added:\n\n```bash\ndocker pull docker.io/copyparty/ac:latest\ndocker build -t customparty .\n```\n\nnow you can run the image `localhost/customparty:latest` which is `copyparty/ac:latest` with your changes\n\n**one important thing to remember:** Whenever you want to update your copyparty version, you must rebuild that image by running those two docker commands (`pull` and `build`) in that folder\n\n\n## modding on the fly\n\nif you are not able to build your own image, then it is also possible to apply *some* changes as the image is starting up, before copyparty itself is launched. This is hacky but mostly-safe if done correctly\n\n**NOTE:** the `min` image is **not** a good idea to modify (brittle from shoehorning); use any of the other variants instead (`im`, `ac`, `iv`, `dj`)\n\nyou should have a docker-volume which is mapped to `/cfg` in the container; in that volume, create a shellscript named (for example) `strikk-og-binders.sh` and then ensure that the docker-container is always executed with the environment-variable `DI_PREPARTY` set to `strikk-og-binders.sh`\n\n* if you use docker-compose then see the [LD_PRELOAD](https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/docker/basic-docker-compose/docker-compose.yml) example\n\n**NOTE:** if you are doing this to [install a package you need](https://github.com/9001/copyparty/issues/479), then please make sure you **do not** download the package every time you restart copyparty, because that would result in excessive strain on Alpine's poor servers. You should cache the package locally. Here's an example `strikk-og-binders.sh` with caching, using [exiftool](https://pkgs.alpinelinux.org/package/edge/community/x86_64/exiftool) and [py3-requests](https://pkgs.alpinelinux.org/package/edge/main/x86_64/py3-requests) as the example packages to install\n\n```bash\nset -e  # if something below goes wrong, then panic and crash\n#set -x  # uncomment this to debug (enable command-logging)\n\n# packages to install\npkgs=\"exiftool py3-requests\"\n\n# if the docker-image is newer than the cache, then delete the cache\n[ /z/initcfg -nt /cfg/apks/t ] && rm -rf /cfg/apks\n\n# if the cache has the wrong packages, then delete the cache\ngrep -qF \"$pkgs\" /cfg/apks/t || rm -rf /cfg/apks\n\n# if there is a cache, then just install those packages (remove -q to debug)\n[ -e /cfg/apks ] && exec apk add -q --progress=no /cfg/apks/*.apk\n\n# there is no cache; need to download+cache+install\nmkdir /cfg/apks\napk add --cache-predownload --cache-dir /cfg/apks --progress=no $pkgs\necho \"$pkgs\" >/cfg/apks/t\ntouch -r /z/initcfg /cfg/apks/t\n```\n\nsecurity-wise this is safe because apk-files are signed and thus tamper-proof\n"
  },
  {
    "path": "scripts/docker/innvikler.sh",
    "content": "#!/bin/ash\nset -ex\n\ntmv() {\n\ttouch -r \"$1\" t\n\tmv t \"$1\"\n}\niawk() {\n\tawk \"$1\" <\"$2\" >t\n\ttmv \"$2\"\n}\nised() {\n\tsed -r \"$1\" <\"$2\" >t\n\ttmv \"$2\"\n}\n\n# use custom ffmpeg if relevant\necho $1 | grep -qE 'ac|iv|dj' && (\n  cp -pv /z/packages/*.pub /etc/apk/keys/\n  cd /z/packages/$(cat /etc/apk/arch)\n  apk add ./ffmpeg-*.apk\n  cd /z/test-aac\n  for f in *.m4a; do ffmpeg -v 0 -i $f ${f%.*}.flac || true; done\n  ls -1 *.flac | tee /dev/stderr | tr '\\n' ' ' | grep -qE '^(lc.flac *)?$' || {\n    echo ERROR: incorrect aac decoder subset\n    exit 1\n  }\n)\nrm -rf /z/packages /z/test-aac\n\n# use zlib-ng if available\nf=/z/whl/zlib_ng-0.5.1-cp312-cp312-linux_$(cat /etc/apk/arch).whl\n[ \"$1\" != min ] && [ -e $f ] && {\n  apk add -t .bd !pyc py3-pip\n  rm -f /usr/lib/python3*/EXTERNALLY-MANAGED\n  pip install $f\n  apk del .bd\n}\nrm -rf /z/whl\n\n# cleanup for flavors with python build steps (dj/iv)\nrm -rf /var/cache/apk/* /root/.cache\n\n# initial config; common for all flavors\nmkdir /state /cfg /w\nchmod 777 /state /cfg /w\ncat >initcfg <<'EOF'\n[global]\n  chdir: /w\n  no-crt\n\n% /cfg\nEOF\n\n# the bootstrap\ncat >cpp.sh <<'EOF'\n#!/bin/ash\nset -e\n[ \"$DI_PREPARTY\" ] && {\n  p=\"$DI_PREPARTY\"\n  [ \"$p\" = \"${p##*/}\" ] || {\n    echo \"ERROR: DI_PREPARTY must be filename only\"\n    exit 1\n  }\n  echo \"running DI_PREPARTY (/cfg/$p)\"\n  /bin/ash \"/cfg/$p\" || { e=$?\n    echo \"ERROR: DI_PREPARTY returned error $e\"\n    exit $e\n  }\n}\nexec /usr/bin/python3 -m copyparty \"$@\"\nEOF\n\n# unpack sfx and dive in\npython3 copyparty-sfx.py --version\ncd /tmp/pe-copyparty.0\n\n# steal the stuff we need\nmv copyparty partftpy ftp/* /usr/lib/python3.*/site-packages/\n\n# golf\ncd /usr/lib/python3.*/\nrm -rf \\\n  /tmp/pe-* /z/copyparty-sfx.py \\\n  ensurepip pydoc_data turtle.py turtledemo lib2to3\n\ncd /usr/lib/python3.*/site-packages\nrm -rf \\\n  numpy/*/tests \\\n  /usr/share/mime/packages/freedesktop.org.xml\n\ncd /usr/lib/python3.*/site-packages/copyparty/\nrm stolen/surrogateescape.py\niawk '/^[^ ]/{s=0}/^if not VENDORED:/{s=1}!s' qrkode.py\niawk '/^[^ ]/{s=0}/^    DNS_VND = False/{s=1;print\"    raise\"}!s' mdns.py\n\n# speedhack\nised 's/os.environ.get\\(\"PRTY_NO_IMPRESO\"\\)/\"1\"/' util.py\n\n# drop bytecode\nfind / -xdev -name __pycache__ -print0 | xargs -0 rm -rf\n\n# build the stuff we want\npython3 -m compileall -qj4 site-packages sqlite3 xml\n\n# drop the stuff we dont\nfind -name __pycache__ |\n  grep -E 'ty/web/|/pycpar' |\n  tr '\\n' '\\0' | xargs -0 rm -rf\n\n\n\n\n\nsmoketest() {\n\n# two-for-one:\n# 1) smoketest copyparty even starts\n# 2) build any bytecode we missed\n# this tends to race other builders (alle gode ting er tre)\ncd /z\npython3 -m copyparty \\\n  --ign-ebind -p$((1024+RANDOM)),$((1024+RANDOM)),$((1024+RANDOM)) \\\n  -v .::r --no-crt -qi127.1 --exit=idx -e2dsa -e2ts\n\n########################################################################\n# test download-as-tar.gz\n\nt=$(mktemp)\npython3 -m copyparty \\\n  --ign-ebind -p$((1024+RANDOM)),$((1024+RANDOM)),$((1024+RANDOM)) \\\n  -v .::r --no-crt -qi127.1 --wr-h-eps $t & pid=$!\n\nfor n in $(seq 1 900); do sleep 0.2\n  v=$(awk '/^127/{print;n=1;exit}END{exit n-1}' $t) && break\ndone\n[ -z \"$v\" ] && echo SNAAAAAKE && exit 1\nrm $t\n\nfor n in $(seq 1 900); do sleep 0.2\n  wget -O- http://${v/ /:}/?tar=gz:1 >tf && break\ndone\ntar -xzO top/innvikler.sh <tf | cmp innvikler.sh\nrm tf\n\nkill $pid; wait $pid\n\n########################################################################\n\n# output from -e2d\nrm -rf .hist /cfg/copyparty\n\n}\n\nsmoketest\n\n\n\n\n\n[ \"$1\" == min ] && {\n  # shrink amd64 from 45.5 to 33.2 MiB\n\n  # libstdc++ is pulled in by libmpdec++ in libmpdec; keep libmpdec.so\n  cd /usr/lib ; rm -rf \\\n  libmpdec++.so* \\\n  libncurses* \\\n  libpanelw* \\\n  libreadline* \\\n  libstdc++.so* \\\n  --\n\n  cd /usr/lib/python3.*/lib-dynload/ ; rm -rf \\\n  *audioop.* \\\n  _asyncio.* \\\n  _ctypes_test.* \\\n  _curses* \\\n  _test* \\\n  _xx* \\\n  ossaudio.* \\\n  readline.* \\\n  xx* \\\n  --\n\n  # keep http/client for u2c\n  cd /usr/lib/python3.*/ ; rm -rf \\\n  site-packages/*.dist-info \\\n  aifc.py \\\n  asyncio \\\n  bdb.py \\\n  cgi.py \\\n  config-3.*/Makefile \\\n  ctypes/macholib \\\n  dbm \\\n  difflib.py \\\n  doctest.py \\\n  email/_header_value_parser.py \\\n  html \\\n  http/cookiejar.* \\\n  http/server.* \\\n  imaplib.py \\\n  importlib/resources \\\n  mailbox.py \\\n  nntplib.py \\\n  pickletools.py \\\n  pydoc.py \\\n  smtplib.py \\\n  statistics.py \\\n  tomllib \\\n  unittest \\\n  venv \\\n  wsgiref \\\n  xml/dom \\\n  xml/sax \\\n  xmlrpc \\\n  --\n\n  set +x\n  find -iname '*.pyc' |\n  grep -viE 'tftpy' |\n  while IFS= read -r x; do\n    y=\"$(printf '%s\\n' \"$x\" | sed -r 's`/__pycache__/([^/]+)\\.cpython-312\\.pyc$`/\\1.py`')\"\n    [ -e \"$y\" ] || continue\n    [ \"$y\" = \"$x\" ] && continue\n    rm \"$y\"\n    mv \"$x\" \"${y}c\"\n  done\n  find -iname __pycache__ -print0 | xargs -0 rm -rf --\n  rm -rf /a\n  set -x\n\n  smoketest\n\n  # printf '%s\\n' 'FROM localhost/copyparty-min-amd64' 'COPY a /' 'RUN /bin/ash /a' >Dockerfile\n  # podman rmi localhost/m2 ; podman build --squash-all -t m2 . && podman images && podman run --rm -it localhost/m2 --exit=idx && podman images\n}\n\n\n\n\n\n# goodbye\nexec rm innvikler.sh\n"
  },
  {
    "path": "scripts/docker/make.sh",
    "content": "#!/bin/bash\nset -e\nself=$(cd -- \"$(dirname \"$BASH_SOURCE\")\"; pwd -P)\ncd \"$self\"\n\n[ $(id -u) -eq 0 ] && {\n    echo dont root\n    exit 1\n}\n\nsuf=-b1\nsuf=\nsarchs=\"386 amd64 arm/v7 arm64/v8 ppc64le s390x\"\narchs=\"amd64 arm s390x 386 arm64 ppc64le\"\nimgs=\"dj iv min im ac\"\ndhub_order=\"iv dj min im ac\"\nghcr_order=\"ac im min dj iv\"\nngs=(\n    iv-{ppc64le,s390x}\n    dj-{ppc64le,s390x,arm}\n)\n\nerr=\nfor x in awk jq podman python3 tar wget ; do\n    command -v $x >/dev/null && continue\n    err=1; echo ERROR: missing dependency: $x\ndone\n[ $err ] && exit 1\n\nfor v in \"$@\"; do\n    [ \"$v\" = clean  ] && clean=1\n    [ \"$v\" = hclean ] && hclean=1\n    [ \"$v\" = purge  ] && purge=1\n    [ \"$v\" = pull   ] && pull=1\n    [ \"$v\" = img    ] && img=1\n    [ \"$v\" = push   ] && push=1\n    [ \"$v\" = sh     ] && sh=1\ndone\n\n[ $# -gt 0 ] || {\n    echo \"need list of commands, for example: hclean pull img push\"\n    exit 1\n}\n\n[ $sh ] && {\n    printf \"\\n\\033[1;31mopening a shell in the most recently created docker image\\033[0m\\n\"\n    podman run --rm -it --entrypoint /bin/ash $(podman images -aq | head -n 1)\n    exit $?\n}\n\nfilt=\n[ $clean  ] && filt='/<none>/{print$$3}'\n[ $hclean ] && filt='/localhost\\/(copyparty|alpine)-/{print$3}'\n[ $purge  ] && filt='NR>1{print$3}'\n[ $filt ] && {\n    [ $purge ] && {\n        podman kill $(podman ps -q)  || true\n        podman rm   $(podman ps -qa) || true\n    }\n\tpodman rmi -f $(podman images -a --history | awk \"$filt\") || true\n    podman rmi $(podman images -a --history | awk '/^<none>.*<none>.*-tmp:/{print$3}') || true\n}\n\n[ $pull ] && {\n    for a in $sarchs; do  # arm/v6\n        podman pull --arch=$a alpine:latest\n    done\n\n    podman images --format \"{{.ID}} {{.History}}\" |\n    awk '/library\\/alpine/{print$1}' |\n    while read id; do\n        tag=alpine-$(podman inspect $id | jq -r '.[]|.Architecture' | tr / -)\n        [ -e .tag-$tag ] && continue\n        touch .tag-$tag\n        echo tagging $tag\n        podman untag $id\n        podman tag $id $tag\n    done\n    rm .tag-*\n}\n\n[ $img ] && {\n    [ -e base/test-aac/lc.m4a ] || (\n        echo building aac smoketest\n        mkdir -p base/test-aac\n        cd base/test-aac\n        ffmpeg -nostdin -y -f lavfi -i sine -ac 2 -t 1 a.wav &&\n        fdkaac -m 3 -o lc.m4a a.wav &&\n        fdkaac -m 2 -p 5 -o he.m4a a.wav &&\n        fdkaac -m 1 -p 29 -o he2.m4a a.wav &&\n        fdkaac -m 3 -p 23 -o ld.m4a a.wav &&\n        fdkaac -m 3 -p 39 -o eld.m4a a.wav ||\n        echo \"nevermind, failed to build test files, cannot verify aac decoding\"\n        rm -f a.wav\n    )\n\n    fp=../../dist/copyparty-sfx.py\n    [ -e $fp ] || {\n        echo downloading copyparty-sfx.py ...\n        mkdir -p ../../dist\n        wget https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py -O $fp\n    }\n\n    # enable arm32 crossbuild from aarch64 (macbook or whatever)\n    [ $(uname -m) = aarch64 ] && [ ! -e /proc/sys/fs/binfmt_misc/qemu-arm ] &&\n        echo \":qemu-arm:M:0:\\x7f\\x45\\x4c\\x46\\x01\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x28\\x00:\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xfe\\xff\\xff\\xff:/usr/bin/qemu-arm-static:F\" |\n        sudo tee >/dev/null /proc/sys/fs/binfmt_misc/register\n\n    # kill abandoned builders\n    ps aux | awk '/bin\\/qemu-[^-]+-static/{print$2}' | xargs -r kill -9\n\n    # grab deps\n    rm -rf i err\n    mkdir i\n    tar -cC \"$self/base\" whl test-aac \\\n        -C \"$self/base/b\" packages \\\n        -C \"$self/../..\"  bin/mtag \\\n        -C dist copyparty-sfx.py \\\n        | tar -xvCi\n\n    for i in $imgs; do\n        podman rm copyparty-$i || true  # old manifest\n        for a in $archs; do\n            [[ \" ${ngs[*]} \" =~ \" $i-$a \" ]] && continue  # known incompat\n\n            # wait for a free slot\n            while true; do\n                touch .blk\n                [ $(jobs -p | wc -l) -lt $(nproc) ] && break\n                while [ -e .blk ]; do sleep 0.2; done\n            done\n            aa=\"$(printf '%11s' $a-$i)\"\n\n            # arm takes forever so make it top priority\n            [ ${a::3} == arm ] && nice= || nice=-n20\n\n            # not sure if this is necessary or if inherit-annotations=false was enough, but won't hurt\n            readarray -t annot < <(awk <Dockerfile.$i '/org.opencontainers.image/{sub(/[^\\.]+/,\"\");sub(/[\" \\\\]+$/,\"\");sub(/\"/,\"\");print\"--annotation\";print\"org\"$0}')\n            annot+=( --annotation \"org.opencontainers.image.created=$( date -u +%Y-%m-%dT%H:%M:%SZ )\" )\n\n            # --pull=never does nothing at all btw\n            (set -x\n            nice $nice podman build \\\n                --squash \\\n                --pull=never \\\n                --from localhost/alpine-$a \\\n                --inherit-annotations=false \\\n                \"${annot[@]}\" \\\n                -t copyparty-$i-$a$suf \\\n                -f Dockerfile.$i . ||\n                    (echo $? $i-$a >> err; printf '%096d\\n' $(seq 1 42))\n            rm -f .blk\n            ) 2> >(tee $a.err | sed \"s/^/$aa:/\" >&2) > >(tee $a.out | sed \"s/^/$aa:/\") &\n        done\n        [ -e err ] && {\n            echo somethign died,\n            cat err\n            pkill -P $$\n            exit 1\n        }\n        for a in $archs; do\n            rm -f $a.{out,err}\n        done\n    done\n    wait\n    [ -e err ] && {\n        echo somethign died,\n        cat err\n        pkill -P $$\n        exit 1\n    }\n    # avoid podman race-condition by creating manifest manually --\n    # Error: creating image to hold manifest list: image name \"localhost/copyparty-dj:latest\" is already associated with image \"[0-9a-f]{64}\": that name is already in use\n    for i in $imgs; do\n        variants=\n        for a in $archs; do\n            [[ \" ${ngs[*]} \" =~ \" $i-$a \" ]] && continue\n            variants=\"$variants containers-storage:localhost/copyparty-$i-$a$suf\"\n        done\n        podman manifest rm copyparty-$i$suf || echo \"(that's fine btw)\"\n        podman manifest create copyparty-$i$suf $variants\n    done\n}\n\n[ $push ] && {\n    ver=$(\n        python3 ../../dist/copyparty-sfx.py --version 2>/dev/null |\n        awk '/^copyparty v/{sub(/-.*/,\"\");sub(/v/,\"\");print$2;exit}'\n    )\n    echo $ver | grep -E '[0-9]\\.[0-9]' || {\n        echo no ver\n        exit 1\n    }\n    for i in $dhub_order; do\n        printf '\\ndockerhub %s\\n' $i\n        podman manifest push --all copyparty-$i copyparty/$i:$ver\n        podman manifest push --all copyparty-$i copyparty/$i:beta\n        podman manifest push --all copyparty-$i copyparty/$i:latest\n    done &\n    for i in $ghcr_order; do\n        printf '\\nghcr %s\\n' $i\n        podman manifest push --all copyparty-$i ghcr.io/9001/copyparty-$i:$ver\n        podman manifest push --all copyparty-$i ghcr.io/9001/copyparty-$i:latest\n    done &\n    wait\n}\n\necho ok\n"
  },
  {
    "path": "scripts/fusefuzz.py",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport time\n\n\"\"\"\ntd=/dev/shm/; [ -e $td ] || td=$HOME; mkdir -p $td/fusefuzz/{r,v}\nPYTHONPATH=.. python3 -m copyparty -v $td/fusefuzz/r::r -i 127.0.0.1\n../bin/partyfuse.py http://127.0.0.1:3923/ $td/fusefuzz/v -cf 2 -cd 0.5\n(d=\"$PWD\"; cd $td/fusefuzz && \"$d\"/fusefuzz.py)\n\"\"\"\n\n\ndef chk(fsz, rsz, ofs0, shift, ofs, rf, vf):\n    if ofs != rf.tell():\n        rf.seek(ofs)\n        vf.seek(ofs)\n\n    rb = rf.read(rsz)\n    vb = vf.read(rsz)\n\n    print(f\"fsz {fsz} rsz {rsz} ofs {ofs0} shift {shift} ofs {ofs} = {len(rb)}\")\n\n    if rb != vb:\n        for n, buf in enumerate([rb, vb]):\n            with open(\"buf.\" + str(n), \"wb\") as f:\n                f.write(buf)\n\n        raise Exception(f\"{len(rb)} != {len(vb)}\")\n\n    return rb, vb\n\n\ndef main():\n    v = \"v\"\n    for n in range(5):\n        with open(f\"r/{n}\", \"wb\") as f:\n            f.write(b\"h\" * n)\n\n    rand = os.urandom(7919)  # prime\n    for fsz in range(1024 * 1024 * 2 - 3, 1024 * 1024 * 2 + 3):\n        with open(\"r/f\", \"wb\", fsz) as f:\n            f.write((rand * int(fsz / len(rand) + 1))[:fsz])\n\n        for rsz in range(64 * 1024 - 2, 64 * 1024 + 2):\n            ofslist = [0, 1, 2]\n            for n in range(3):\n                ofslist.append(fsz - n)\n                ofslist.append(fsz - (rsz * 1 + n))\n                ofslist.append(fsz - (rsz * 2 + n))\n\n            for ofs0 in ofslist:\n                for shift in range(-3, 3):\n                    print(f\"fsz {fsz} rsz {rsz} ofs {ofs0} shift {shift}\")\n                    ofs = ofs0\n                    if ofs < 0 or ofs >= fsz:\n                        continue\n\n                    for n in range(1, 3):\n                        with open(f\"{v}/{n}\", \"rb\") as f:\n                            f.read()\n\n                    prev_ofs = -99\n                    with open(\"r/f\", \"rb\", rsz) as rf:\n                        with open(f\"{v}/f\", \"rb\", rsz) as vf:\n                            while True:\n                                ofs += shift\n                                if ofs < 0 or ofs > fsz or ofs == prev_ofs:\n                                    break\n\n                                prev_ofs = ofs\n\n                                rb, vb = chk(fsz, rsz, ofs0, shift, ofs, rf, vf)\n\n                                if not rb:\n                                    break\n\n                                ofs += len(rb)\n\n                    for n in range(1, 3):\n                        with open(f\"{v}/{n}\", \"rb\") as f:\n                            f.read()\n\n                    with open(\"r/f\", \"rb\", rsz) as rf:\n                        with open(f\"{v}/f\", \"rb\", rsz) as vf:\n                            for n in range(2):\n                                ofs += shift\n                                if ofs < 0 or ofs > fsz:\n                                    break\n\n                                rb, vb = chk(fsz, rsz, ofs0, shift, ofs, rf, vf)\n\n                                ofs -= rsz\n\n        # bumping fsz, sleep away the dentry cache in cppf\n        time.sleep(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/genhelp.sh",
    "content": "#!/bin/bash\nset -e\n\ncommand -v gfind >/dev/null &&\ncommand -v gsed  >/dev/null &&\ncommand -v gsort >/dev/null && {\n    sed()  { gsed \"$@\"; }\n}\n\n[ -e make-sfx.sh ] || cd scripts\n[ -e make-sfx.sh ] && [ -e deps-docker ] || {\n    echo cd into the scripts folder first\n    exit 1\n}\n\ncd ../dist\n\nkwds='-bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -ls -dbd -chmod -pwhash -zm'\n\nhtml() {\n    for a in '' $kwds; do\n        echo \"html$a\" >&2\n        COLUMNS=140 ./copyparty-sfx.py --ansi --help$a 2>/dev/null\n        printf '\\n\\n\\n%0139d\\n\\n'\n    done | aha -b --no-xml | sed -r '\n        s/color:black/color:#222/g;\n        s/color:dimgray\\b/color:#606060/g;\n        s/color:red\\b/color:#c75b79/g;\n        s/color:lime\\b/color:#b8e346/g;\n        s/color:yellow\\b/color:#ffa402/g;\n        s/color:#3333[Ff]{2}\\b/color:#02a2ff/g;\n        s/color:fuchsia\\b/color:#f65be3/g;\n        s/color:aqua\\b/color:#3da698/g;\n        s/color:white\\b/color:#fff/g;\n        s/style=\"filter:[^;]+/style=\"/g;\n    ' |\n    HLPTXT=CAT python3 ../scripts/help2html.py\n}\n\ntxt() {\n    (for a in '' $kwds; do\n        echo \"txt$a\" >&2\n        COLUMNS=9001 ./copyparty-sfx.py --help$a 2>/dev/null\n        printf '\\n\\n\\n%0255d\\n\\n\\n'\n    done;printf '\\n\\n\\n') |\n    HLPTXT=CAT ../scripts/help2txt.sh\n}\n\nhtml\ntxt\n"
  },
  {
    "path": "scripts/genlic.py",
    "content": "#!/usr/bin/env python3\n\nimport re, os, sys, codecs\n\noutfile = os.path.realpath(sys.argv[1])\n\nos.chdir(os.path.dirname(__file__))\n\nwith open(\"../docs/lics.txt\", \"rb\") as f:\n    s = f.read().decode(\"utf-8\").rstrip(\"\\n\") + \"\\n\\n\\n\\n\"\n    s = re.sub(\"\\nC: \", \"\\nCopyright (c) \", s)\n    s = re.sub(\"\\nL: \", \"\\nLicense: \", s)\n    ret = s.split(\"\\n\")\n\nlics = [\n    \"MIT License\",\n    \"BSD 2-Clause License\",\n    \"BSD 3-Clause License\",\n    \"ISC License\",\n    \"Apache License v2.0\",\n    \"SIL Open Font License v1.1\",\n]\n\nfor n, lic in enumerate(lics, 1):\n    with open(\"lics/%d.r13\" % (n,), \"rb\") as f:\n        s = f.read().decode(\"utf-8\")\n        s = codecs.decode(s, \"rot_13\")\n        s = \"\\n--- %s ---\\n\\n%s\" % (lic, s)\n        ret.extend(s.split(\"\\n\"))\n\nfor n, ln in enumerate(ret):\n    if not ln.startswith(\"--- \"):\n        continue\n    pad = \" \" * ((80 - len(ln)) // 2)\n    ln = \"%s\\033[07m%s\\033[0m\" % (pad, ln)\n    ret[n] = ln\n\nret.append(\"\")\nret.append(\"\")\n\nwith open(outfile, \"wb\") as f:\n    f.write((\"\\n\".join(ret)).encode(\"utf-8\"))\n"
  },
  {
    "path": "scripts/help2html.py",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport re\nimport socket\nimport subprocess as sp\n\n\n# to convert the copyparty --help to html, run this in xfce4-terminal @ 140x43:\n_ = r\"\"\"\"\necho; for a in '' -bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -ls -dbd -chmod -pwhash -zm; do\n./copyparty-sfx.py --help$a 2>/dev/null; printf '\\n\\n\\n%0139d\\n\\n\\n'; done  # xfce4-terminal @ 140x43\n\"\"\"\n# click [edit] => [select all]\n# click [edit] => [copy as html]\n# and then run this script\n\n\ndef readclip():\n    cmds = [\n        \"xsel -ob\",\n        \"xclip -selection CLIPBOARD -o\",\n        \"pbpaste\",\n    ]\n    if os.getenv(\"HLPTXT\") == \"CAT\":\n        cmds = [\"cat\"]\n\n    for cmd in cmds:\n        try:\n            return sp.check_output(cmd.split()).decode(\"utf-8\")\n        except:\n            pass\n    raise Exception(\"need one of these: xsel xclip pbpaste\")\n\n\ndef cnv(src):\n    hostname = str(socket.gethostname()).split(\".\")[0]\n\n    yield '<!DOCTYPE html>'\n    yield '<html><body><style>'\n    yield 'html{background:#222;color:#fff;line-height:1.25em}'\n    yield 'h3{margin:0;padding:0}'\n    yield 'a{color:#fc5;text-decoration:none;scroll-margin-top:3em}'\n    yield 'a:active,a:target{background:#fc5;color:#000;box-shadow:0 0 0 .12em #fc5}'\n    yield '</style>'\n    skip_sfx = False\n    in_sfx = 0\n    in_salt = 0\n    in_name = 0\n    in_cores = 0\n    in_hash_mt = False\n    in_th_ram_max = 0\n\n    while True:\n        ln = next(src)\n        if \"<font\" in ln or \"<span\" in ln:\n            if not ln.startswith(\"<pre>\"):\n                ln = \"<pre>\" + ln\n            yield ln\n            break\n\n    for ln in src:\n        ln = ln.rstrip()\n        t = ln\n        if re.search(r\"^<(font|span)[^>]+>copyparty v[0-9]\", ln):\n            in_sfx = 3\n        if in_sfx:\n            in_sfx -= 1\n            if not skip_sfx:\n                yield ln\n            elif not in_sfx:\n                yield \"<span>\"\n            continue\n        if '\">uuid:' in ln:\n            ln = re.sub(r\">uuid:[0-9a-f-]{36}<\", \">autogenerated<\", ln)\n        if \"-salt SALT\" in ln:\n            in_salt = 3\n        if in_salt:\n            in_salt -= 1\n            ln = re.sub(r\">[0-9a-zA-Z/+]{24}<\", \">24-character-autogenerated<\", ln)\n            ln = re.sub(r\">[0-9a-zA-Z/+]{40}<\", \">40-character-autogenerated<\", ln)\n            if t != ln:\n                in_salt = 0\n        if \"--name TXT\" in ln:\n            in_name = 3\n        if in_name:\n            in_name -= 1\n            ln = ln.replace(\">\" + hostname + \"<\", \">hostname<\")\n            if t != ln:\n                in_name = 0\n        if \"--hash-mt CORES\" in ln:\n            in_cores = 3\n            in_hash_mt = True\n        if \"--mtag-mt CORES\" in ln or \"--th-mt CORES\" in ln:\n            in_cores = 3\n        if in_cores:\n            in_cores -= 1\n            zs = \">numCores\"\n            if in_hash_mt:\n                zs += \" if 5 or less\"\n            ln = re.sub(r\">[0-9]{1,2}<\", zs + \"<\", ln)\n            if t != ln:\n                in_cores = 0\n                in_hash_mt = False\n        if \"--th-ram-max GB\" in ln:\n            in_th_ram_max = 3\n        if in_th_ram_max:\n            in_th_ram_max -= 1\n            ln = re.sub(r\">[0-9]{1,2}\\.[0-9]<\", \">dynamic<\", ln)\n            if t != ln:\n                in_th_ram_max = 0\n        m = re.search(r\"^# (.* help page)(.*)\", ln)\n        if m:\n            zs1, zs2 = m.groups()\n            zs3 = zs1.replace(\" \", \"-\")\n            ln = '<h3># <a id=\"%s\" href=\"#%s\">%s</a>%s</h3></a>' % (zs3, zs3, zs1, zs2)\n        m = re.search(r\"^  (-{1,2})([^ ,]+)(.*)\", ln)\n        if m:\n            zs1, zs2, zs3 = m.groups()\n            ln = '  <a href=\"#g-%s\" id=\"g-%s\">%s%s</a>%s' % (zs2, zs2, zs1, zs2, zs3)\n\n        ln = ln.replace(\">/home/ed/\", \">~/\")\n        if ln.startswith(\"0\" * 20):\n            skip_sfx = True\n        yield ln\n\n    yield \"</pre>eof</body></html>\"\n\n\ndef main():\n    src = readclip()\n    src = re.split(\"0{100,200}\", src[::-1], maxsplit=1)[1][::-1]\n    with open(\"helptext.html\", \"wb\") as fo:\n        for ln in cnv(iter(src.split(\"\\n\")[:-3])):\n            fo.write(ln.encode(\"utf-8\") + b\"\\r\\n\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/help2txt.sh",
    "content": "#!/bin/bash\nset -e\n\ncommand -v gfind >/dev/null &&\ncommand -v gsed  >/dev/null &&\ncommand -v gsort >/dev/null && {\n    head() { ghead \"$@\"; }\n}\n\n( ( HLPTXT=CAT && cat || xsel -ob ) | sed -r '\ns`/home/ed/`~/`;\ns/uuid:[0-9a-f-]{36}/autogenerated/;\ns/(-salt SALT.*default: )[0-9a-zA-Z/+]{24}\\)/\\124-character-autogenerated)/;\ns/(-salt SALT.*default: )[0-9a-zA-Z/+]{40}\\)/\\140-character-autogenerated)/;\ns/(--name TXT.*default: )[^)]+/\\1hostname/;\ns/(--hash-mt CORES.*default: )[0-9]+/\\1numCores if 5 or less/;\ns/(--mtag-mt|th-mt)( CORES.*default: )[0-9]+/\\1\\2numCores/;\ns/(--th-ram-max GB.*default: )[0-9\\.]+/\\1dynamic/;\n' | awk '\n/^copyparty/{a=1} !a{next}\n/^0{20}/{b=1} b&&/^copyparty v[0-9]+\\./{s=3}\ns{s-=1;next} 1' |\nhead -n-6; echo eof ) >helptext.txt\nexit 0\n\n\n# =====================================================================\n# end of script;  below is the explanation how to use this:\n\n\n# first open an infinitely wide console (this is why you own an ultrawide) and copypaste this into it:\nfor a in '' -bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -ls -dbd -chmod -pwhash -zm; do\n./copyparty-sfx.py --help$a 2>/dev/null; printf '\\n\\n\\n%0255d\\n\\n\\n'; done\n\n# then copypaste all of the output by pressing ctrl-shift-a, ctrl-shift-c\n# and finally actually run this script which should produce helptext.txt\n"
  },
  {
    "path": "scripts/install-githooks.sh",
    "content": "#!/bin/bash\nset -ex\n\n[ -e setup.py ] || ..\n[ -e setup.py ] || {\n    echo u wot\n    exit 1\n}\n\ncd .git/hooks\nrm -f pre-commit\nln -s ../../scripts/run-tests.sh pre-commit\n"
  },
  {
    "path": "scripts/lics/1.r13",
    "content": "Crezvffvba vf urerol tenagrq, serr bs punetr, gb nal crefba bognvavat n pbcl bs guvf fbsgjner naq nffbpvngrq qbphzragngvba svyrf (gur \"Fbsgjner\"), gb qrny va gur Fbsgjner jvgubhg erfgevpgvba, vapyhqvat jvgubhg yvzvgngvba gur evtugf gb hfr, pbcl, zbqvsl, zretr, choyvfu, qvfgevohgr, fhoyvprafr, naq/be fryy pbcvrf bs gur Fbsgjner, naq gb crezvg crefbaf gb jubz gur Fbsgjner vf sheavfurq gb qb fb, fhowrpg gb gur sbyybjvat pbaqvgvbaf:\n\nGur nobir pbclevtug abgvpr naq guvf crezvffvba abgvpr funyy or vapyhqrq va nyy pbcvrf be fhofgnagvny cbegvbaf bs gur Fbsgjner.\n\nGUR FBSGJNER VF CEBIVQRQ \"NF VF\", JVGUBHG JNEENAGL BS NAL XVAQ, RKCERFF BE VZCYVRQ, VAPYHQVAT OHG ABG YVZVGRQ GB GUR JNEENAGVRF BS ZREPUNAGNOVYVGL, SVGARFF SBE N CNEGVPHYNE CHECBFR NAQ ABAVASEVATRZRAG. VA AB RIRAG FUNYY GUR NHGUBEF BE PBCLEVTUG UBYQREF OR YVNOYR SBE NAL PYNVZ, QNZNTRF BE BGURE YVNOVYVGL, JURGURE VA NA NPGVBA BS PBAGENPG, GBEG BE BGUREJVFR, NEVFVAT SEBZ, BHG BS BE VA PBAARPGVBA JVGU GUR FBSGJNER BE GUR HFR BE BGURE QRNYVATF VA GUR FBSGJNER."
  },
  {
    "path": "scripts/lics/2.r13",
    "content": "Erqvfgevohgvba naq hfr va fbhepr naq ovanel sbezf, jvgu be jvgubhg zbqvsvpngvba, ner crezvggrq cebivqrq gung gur sbyybjvat pbaqvgvbaf ner zrg:\n\n1. Erqvfgevohgvbaf bs fbhepr pbqr zhfg ergnva gur nobir pbclevtug abgvpr, guvf yvfg bs pbaqvgvbaf naq gur sbyybjvat qvfpynvzre.\n\n2. Erqvfgevohgvbaf va ovanel sbez zhfg ercebqhpr gur nobir pbclevtug abgvpr, guvf yvfg bs pbaqvgvbaf naq gur sbyybjvat qvfpynvzre va gur qbphzragngvba naq/be bgure zngrevnyf cebivqrq jvgu gur qvfgevohgvba.\n\nGUVF FBSGJNER VF CEBIVQRQ OL GUR PBCLEVTUG UBYQREF NAQ PBAGEVOHGBEF \"NF VF\" NAQ NAL RKCERFF BE VZCYVRQ JNEENAGVRF, VAPYHQVAT, OHG ABG YVZVGRQ GB, GUR VZCYVRQ JNEENAGVRF BS ZREPUNAGNOVYVGL NAQ SVGARFF SBE N CNEGVPHYNE CHECBFR NER QVFPYNVZRQ. VA AB RIRAG FUNYY GUR PBCLEVTUG UBYQRE BE PBAGEVOHGBEF OR YVNOYR SBE NAL QVERPG, VAQVERPG, VAPVQRAGNY, FCRPVNY, RKRZCYNEL, BE PBAFRDHRAGVNY QNZNTRF (VAPYHQVAT, OHG ABG YVZVGRQ GB, CEBPHERZRAG BS FHOFGVGHGR TBBQF BE FREIVPRF; YBFF BS HFR, QNGN, BE CEBSVGF; BE OHFVARFF VAGREEHCGVBA) UBJRIRE PNHFRQ NAQ BA NAL GURBEL BS YVNOVYVGL, JURGURE VA PBAGENPG, FGEVPG YVNOVYVGL, BE GBEG (VAPYHQVAT ARTYVTRAPR BE BGUREJVFR) NEVFVAT VA NAL JNL BHG BS GUR HFR BS GUVF FBSGJNER, RIRA VS NQIVFRQ BS GUR CBFFVOVYVGL BS FHPU QNZNTR."
  },
  {
    "path": "scripts/lics/3.r13",
    "content": "Erqvfgevohgvba naq hfr va fbhepr naq ovanel sbezf, jvgu be jvgubhg zbqvsvpngvba, ner crezvggrq cebivqrq gung gur sbyybjvat pbaqvgvbaf ner zrg:\n\n1. Erqvfgevohgvbaf bs fbhepr pbqr zhfg ergnva gur nobir pbclevtug abgvpr, guvf yvfg bs pbaqvgvbaf naq gur sbyybjvat qvfpynvzre.\n\n2. Erqvfgevohgvbaf va ovanel sbez zhfg ercebqhpr gur nobir pbclevtug abgvpr, guvf yvfg bs pbaqvgvbaf naq gur sbyybjvat qvfpynvzre va gur qbphzragngvba naq/be bgure zngrevnyf cebivqrq jvgu gur qvfgevohgvba.\n\n3. Arvgure gur anzr bs gur pbclevtug ubyqre abe gur anzrf bs vgf pbagevohgbef znl or hfrq gb raqbefr be cebzbgr cebqhpgf qrevirq sebz guvf fbsgjner jvgubhg fcrpvsvp cevbe jevggra crezvffvba.\n\nGUVF FBSGJNER VF CEBIVQRQ OL GUR PBCLEVTUG UBYQREF NAQ PBAGEVOHGBEF \"NF VF\" NAQ NAL RKCERFF BE VZCYVRQ JNEENAGVRF, VAPYHQVAT, OHG ABG YVZVGRQ GB, GUR VZCYVRQ JNEENAGVRF BS ZREPUNAGNOVYVGL NAQ SVGARFF SBE N CNEGVPHYNE CHECBFR NER QVFPYNVZRQ. VA AB RIRAG FUNYY GUR PBCLEVTUG UBYQRE BE PBAGEVOHGBEF OR YVNOYR SBE NAL QVERPG, VAQVERPG, VAPVQRAGNY, FCRPVNY, RKRZCYNEL, BE PBAFRDHRAGVNY QNZNTRF (VAPYHQVAT, OHG ABG YVZVGRQ GB, CEBPHERZRAG BS FHOFGVGHGR TBBQF BE FREIVPRF; YBFF BS HFR, QNGN, BE CEBSVGF; BE OHFVARFF VAGREEHCGVBA) UBJRIRE PNHFRQ NAQ BA NAL GURBEL BS YVNOVYVGL, JURGURE VA PBAGENPG, FGEVPG YVNOVYVGL, BE GBEG (VAPYHQVAT ARTYVTRAPR BE BGUREJVFR) NEVFVAT VA NAL JNL BHG BS GUR HFR BS GUVF FBSGJNER, RIRA VS NQIVFRQ BS GUR CBFFVOVYVGL BS FHPU QNZNTR."
  },
  {
    "path": "scripts/lics/4.r13",
    "content": "Crezvffvba gb hfr, pbcl, zbqvsl, naq/be qvfgevohgr guvf fbsgjner sbe nal checbfr jvgu be jvgubhg srr vf urerol tenagrq, cebivqrq gung gur nobir pbclevtug abgvpr naq guvf crezvffvba abgvpr nccrne va nyy pbcvrf.\n\nGUR FBSGJNER VF CEBIVQRQ \"NF VF\" NAQ VFP QVFPYNVZF NYY JNEENAGVRF JVGU ERTNEQ GB GUVF FBSGJNER VAPYHQVAT NYY VZCYVRQ JNEENAGVRF BS ZREPUNAGNOVYVGL NAQ SVGARFF. VA AB RIRAG FUNYY VFP OR YVNOYR SBE NAL FCRPVNY, QVERPG, VAQVERPG, BE PBAFRDHRAGVNY QNZNTRF BE NAL QNZNTRF JUNGFBRIRE ERFHYGVAT SEBZ YBFF BS HFR, QNGN BE CEBSVGF, JURGURE VA NA NPGVBA BS PBAGENPG, ARTYVTRAPR BE BGURE GBEGVBHF NPGVBA, NEVFVAT BHG BS BE VA PBAARPGVBA JVGU GUR HFR BE CRESBEZNAPR BS GUVF FBSGJNER."
  },
  {
    "path": "scripts/lics/5.r13",
    "content": "Ncnpur Yvprafr\nIrefvba 2.0, Wnahnel 2004\nuggc://jjj.ncnpur.bet/yvprafrf/\n\nGREZF NAQ PBAQVGVBAF SBE HFR, ERCEBQHPGVBA, NAQ QVFGEVOHGVBA\n\n1. Qrsvavgvbaf.\n\n\"Yvprafr\" funyy zrna gur grezf naq pbaqvgvbaf sbe hfr, ercebqhpgvba, naq qvfgevohgvba nf qrsvarq ol Frpgvbaf 1 guebhtu 9 bs guvf qbphzrag.\n\n\"Yvprafbe\" funyy zrna gur pbclevtug bjare be ragvgl nhgubevmrq ol gur pbclevtug bjare gung vf tenagvat gur Yvprafr.\n\n\"Yrtny Ragvgl\" funyy zrna gur havba bs gur npgvat ragvgl naq nyy bgure ragvgvrf gung pbageby, ner pbagebyyrq ol, be ner haqre pbzzba pbageby jvgu gung ragvgl. Sbe gur checbfrf bs guvf qrsvavgvba, \"pbageby\" zrnaf (v) gur cbjre, qverpg be vaqverpg, gb pnhfr gur qverpgvba be znantrzrag bs fhpu ragvgl, jurgure ol pbagenpg be bgurejvfr, be (vv) bjarefuvc bs svsgl creprag (50%) be zber bs gur bhgfgnaqvat funerf, be (vvv) orarsvpvny bjarefuvc bs fhpu ragvgl.\n\n\"Lbh\" (be \"Lbhe\") funyy zrna na vaqvivqhny be Yrtny Ragvgl rkrepvfvat crezvffvbaf tenagrq ol guvf Yvprafr.\n\n\"Fbhepr\" sbez funyy zrna gur cersreerq sbez sbe znxvat zbqvsvpngvbaf, vapyhqvat ohg abg yvzvgrq gb fbsgjner fbhepr pbqr, qbphzragngvba fbhepr, naq pbasvthengvba svyrf.\n\n\"Bowrpg\" sbez funyy zrna nal sbez erfhygvat sebz zrpunavpny genafsbezngvba be genafyngvba bs n Fbhepr sbez, vapyhqvat ohg abg yvzvgrq gb pbzcvyrq bowrpg pbqr, trarengrq qbphzragngvba, naq pbairefvbaf gb bgure zrqvn glcrf.\n\n\"Jbex\" funyy zrna gur jbex bs nhgubefuvc, jurgure va Fbhepr be Bowrpg sbez, znqr ninvynoyr haqre gur Yvprafr, nf vaqvpngrq ol n pbclevtug abgvpr gung vf vapyhqrq va be nggnpurq gb gur jbex (na rknzcyr vf cebivqrq va gur Nccraqvk orybj).\n\n\"Qrevingvir Jbexf\" funyy zrna nal jbex, jurgure va Fbhepr be Bowrpg sbez, gung vf onfrq ba (be qrevirq sebz) gur Jbex naq sbe juvpu gur rqvgbevny erivfvbaf, naabgngvbaf, rynobengvbaf, be bgure zbqvsvpngvbaf ercerfrag, nf n jubyr, na bevtvany jbex bs nhgubefuvc. Sbe gur checbfrf bs guvf Yvprafr, Qrevingvir Jbexf funyy abg vapyhqr jbexf gung erznva frcnenoyr sebz, be zreryl yvax (be ovaq ol anzr) gb gur vagresnprf bs, gur Jbex naq Qrevingvir Jbexf gurerbs.\n\n\"Pbagevohgvba\" funyy zrna nal jbex bs nhgubefuvc, vapyhqvat gur bevtvany irefvba bs gur Jbex naq nal zbqvsvpngvbaf be nqqvgvbaf gb gung Jbex be Qrevingvir Jbexf gurerbs, gung vf vagragvbanyyl fhozvggrq gb Yvprafbe sbe vapyhfvba va gur Jbex ol gur pbclevtug bjare be ol na vaqvivqhny be Yrtny Ragvgl nhgubevmrq gb fhozvg ba orunys bs gur pbclevtug bjare. Sbe gur checbfrf bs guvf qrsvavgvba, \"fhozvggrq\" zrnaf nal sbez bs ryrpgebavp, ireony, be jevggra pbzzhavpngvba frag gb gur Yvprafbe be vgf ercerfragngvirf, vapyhqvat ohg abg yvzvgrq gb pbzzhavpngvba ba ryrpgebavp znvyvat yvfgf, fbhepr pbqr pbageby flfgrzf, naq vffhr genpxvat flfgrzf gung ner znantrq ol, be ba orunys bs, gur Yvprafbe sbe gur checbfr bs qvfphffvat naq vzcebivat gur Jbex, ohg rkpyhqvat pbzzhavpngvba gung vf pbafcvphbhfyl znexrq be bgurejvfr qrfvtangrq va jevgvat ol gur pbclevtug bjare nf \"Abg n Pbagevohgvba.\"\n\n\"Pbagevohgbe\" funyy zrna Yvprafbe naq nal vaqvivqhny be Yrtny Ragvgl ba orunys bs jubz n Pbagevohgvba unf orra erprvirq ol Yvprafbe naq fhofrdhragyl vapbecbengrq jvguva gur Jbex.\n\n2. Tenag bs Pbclevtug Yvprafr. Fhowrpg gb gur grezf naq pbaqvgvbaf bs guvf Yvprafr, rnpu Pbagevohgbe urerol tenagf gb Lbh n crecrghny, jbeyqjvqr, aba-rkpyhfvir, ab-punetr, eblnygl-serr, veeribpnoyr pbclevtug yvprafr gb ercebqhpr, cercner Qrevingvir Jbexf bs, choyvpyl qvfcynl, choyvpyl cresbez, fhoyvprafr, naq qvfgevohgr gur Jbex naq fhpu Qrevingvir Jbexf va Fbhepr be Bowrpg sbez.\n\n3. Tenag bs Cngrag Yvprafr. Fhowrpg gb gur grezf naq pbaqvgvbaf bs guvf Yvprafr, rnpu Pbagevohgbe urerol tenagf gb Lbh n crecrghny, jbeyqjvqr, aba-rkpyhfvir, ab-punetr, eblnygl-serr, veeribpnoyr (rkprcg nf fgngrq va guvf frpgvba) cngrag yvprafr gb znxr, unir znqr, hfr, bssre gb fryy, fryy, vzcbeg, naq bgurejvfr genafsre gur Jbex, jurer fhpu yvprafr nccyvrf bayl gb gubfr cngrag pynvzf yvprafnoyr ol fhpu Pbagevohgbe gung ner arprffnevyl vasevatrq ol gurve Pbagevohgvba(f) nybar be ol pbzovangvba bs gurve Pbagevohgvba(f) jvgu gur Jbex gb juvpu fhpu Pbagevohgvba(f) jnf fhozvggrq. Vs Lbh vafgvghgr cngrag yvgvtngvba ntnvafg nal ragvgl (vapyhqvat n pebff-pynvz be pbhagrepynvz va n ynjfhvg) nyyrtvat gung gur Jbex be n Pbagevohgvba vapbecbengrq jvguva gur Jbex pbafgvghgrf qverpg be pbagevohgbel cngrag vasevatrzrag, gura nal cngrag yvprafrf tenagrq gb Lbh haqre guvf Yvprafr sbe gung Jbex funyy grezvangr nf bs gur qngr fhpu yvgvtngvba vf svyrq.\n\n4. Erqvfgevohgvba. Lbh znl ercebqhpr naq qvfgevohgr pbcvrf bs gur Jbex be Qrevingvir Jbexf gurerbs va nal zrqvhz, jvgu be jvgubhg zbqvsvpngvbaf, naq va Fbhepr be Bowrpg sbez, cebivqrq gung Lbh zrrg gur sbyybjvat pbaqvgvbaf:\n\n(n) Lbh zhfg tvir nal bgure erpvcvragf bs gur Jbex be Qrevingvir Jbexf n pbcl bs guvf Yvprafr; naq\n\n(o) Lbh zhfg pnhfr nal zbqvsvrq svyrf gb pneel cebzvarag abgvprf fgngvat gung Lbh punatrq gur svyrf; naq\n\n(p) Lbh zhfg ergnva, va gur Fbhepr sbez bs nal Qrevingvir Jbexf gung Lbh qvfgevohgr, nyy pbclevtug, cngrag, genqrznex, naq nggevohgvba abgvprf sebz gur Fbhepr sbez bs gur Jbex, rkpyhqvat gubfr abgvprf gung qb abg cregnva gb nal cneg bs gur Qrevingvir Jbexf; naq\n\n(q) Vs gur Jbex vapyhqrf n \"ABGVPR\" grkg svyr nf cneg bs vgf qvfgevohgvba, gura nal Qrevingvir Jbexf gung Lbh qvfgevohgr zhfg vapyhqr n ernqnoyr pbcl bs gur nggevohgvba abgvprf pbagnvarq jvguva fhpu ABGVPR svyr, rkpyhqvat gubfr abgvprf gung qb abg cregnva gb nal cneg bs gur Qrevingvir Jbexf, va ng yrnfg bar bs gur sbyybjvat cynprf: jvguva n ABGVPR grkg svyr qvfgevohgrq nf cneg bs gur Qrevingvir Jbexf; jvguva gur Fbhepr sbez be qbphzragngvba, vs cebivqrq nybat jvgu gur Qrevingvir Jbexf; be, jvguva n qvfcynl trarengrq ol gur Qrevingvir Jbexf, vs naq jurerire fhpu guveq-cnegl abgvprf abeznyyl nccrne. Gur pbagragf bs gur ABGVPR svyr ner sbe vasbezngvbany checbfrf bayl naq qb abg zbqvsl gur Yvprafr. Lbh znl nqq Lbhe bja nggevohgvba abgvprf jvguva Qrevingvir Jbexf gung Lbh qvfgevohgr, nybatfvqr be nf na nqqraqhz gb gur ABGVPR grkg sebz gur Jbex, cebivqrq gung fhpu nqqvgvbany nggevohgvba abgvprf pnaabg or pbafgehrq nf zbqvslvat gur Yvprafr.\n\nLbh znl nqq Lbhe bja pbclevtug fgngrzrag gb Lbhe zbqvsvpngvbaf naq znl cebivqr nqqvgvbany be qvssrerag yvprafr grezf naq pbaqvgvbaf sbe hfr, ercebqhpgvba, be qvfgevohgvba bs Lbhe zbqvsvpngvbaf, be sbe nal fhpu Qrevingvir Jbexf nf n jubyr, cebivqrq Lbhe hfr, ercebqhpgvba, naq qvfgevohgvba bs gur Jbex bgurejvfr pbzcyvrf jvgu gur pbaqvgvbaf fgngrq va guvf Yvprafr.\n\n5. Fhozvffvba bs Pbagevohgvbaf. Hayrff Lbh rkcyvpvgyl fgngr bgurejvfr, nal Pbagevohgvba vagragvbanyyl fhozvggrq sbe vapyhfvba va gur Jbex ol Lbh gb gur Yvprafbe funyy or haqre gur grezf naq pbaqvgvbaf bs guvf Yvprafr, jvgubhg nal nqqvgvbany grezf be pbaqvgvbaf. Abgjvgufgnaqvat gur nobir, abguvat urerva funyy fhcrefrqr be zbqvsl gur grezf bs nal frcnengr yvprafr nterrzrag lbh znl unir rkrphgrq jvgu Yvprafbe ertneqvat fhpu Pbagevohgvbaf.\n\n6. Genqrznexf. Guvf Yvprafr qbrf abg tenag crezvffvba gb hfr gur genqr anzrf, genqrznexf, freivpr znexf, be cebqhpg anzrf bs gur Yvprafbe, rkprcg nf erdhverq sbe ernfbanoyr naq phfgbznel hfr va qrfpevovat gur bevtva bs gur Jbex naq ercebqhpvat gur pbagrag bs gur ABGVPR svyr.\n\n7. Qvfpynvzre bs Jneenagl. Hayrff erdhverq ol nccyvpnoyr ynj be nterrq gb va jevgvat, Yvprafbe cebivqrf gur Jbex (naq rnpu Pbagevohgbe cebivqrf vgf Pbagevohgvbaf) ba na \"NF VF\" ONFVF, JVGUBHG JNEENAGVRF BE PBAQVGVBAF BS NAL XVAQ, rvgure rkcerff be vzcyvrq, vapyhqvat, jvgubhg yvzvgngvba, nal jneenagvrf be pbaqvgvbaf bs GVGYR, ABA-VASEVATRZRAG, ZREPUNAGNOVYVGL, be SVGARFF SBE N CNEGVPHYNE CHECBFR. Lbh ner fbyryl erfcbafvoyr sbe qrgrezvavat gur nccebcevngrarff bs hfvat be erqvfgevohgvat gur Jbex naq nffhzr nal evfxf nffbpvngrq jvgu Lbhe rkrepvfr bs crezvffvbaf haqre guvf Yvprafr.\n\n8. Yvzvgngvba bs Yvnovyvgl. Va ab rirag naq haqre ab yrtny gurbel, jurgure va gbeg (vapyhqvat artyvtrapr), pbagenpg, be bgurejvfr, hayrff erdhverq ol nccyvpnoyr ynj (fhpu nf qryvorengr naq tebffyl artyvtrag npgf) be nterrq gb va jevgvat, funyy nal Pbagevohgbe or yvnoyr gb Lbh sbe qnzntrf, vapyhqvat nal qverpg, vaqverpg, fcrpvny, vapvqragny, be pbafrdhragvny qnzntrf bs nal punenpgre nevfvat nf n erfhyg bs guvf Yvprafr be bhg bs gur hfr be vanovyvgl gb hfr gur Jbex (vapyhqvat ohg abg yvzvgrq gb qnzntrf sbe ybff bs tbbqjvyy, jbex fgbccntr, pbzchgre snvyher be znyshapgvba, be nal naq nyy bgure pbzzrepvny qnzntrf be ybffrf), rira vs fhpu Pbagevohgbe unf orra nqivfrq bs gur cbffvovyvgl bs fhpu qnzntrf.\n\n9. Npprcgvat Jneenagl be Nqqvgvbany Yvnovyvgl. Juvyr erqvfgevohgvat gur Jbex be Qrevingvir Jbexf gurerbs, Lbh znl pubbfr gb bssre, naq punetr n srr sbe, npprcgnapr bs fhccbeg, jneenagl, vaqrzavgl, be bgure yvnovyvgl boyvtngvbaf naq/be evtugf pbafvfgrag jvgu guvf Yvprafr. Ubjrire, va npprcgvat fhpu boyvtngvbaf, Lbh znl npg bayl ba Lbhe bja orunys naq ba Lbhe fbyr erfcbafvovyvgl, abg ba orunys bs nal bgure Pbagevohgbe, naq bayl vs Lbh nterr gb vaqrzavsl, qrsraq, naq ubyq rnpu Pbagevohgbe unezyrff sbe nal yvnovyvgl vapheerq ol, be pynvzf nffregrq ntnvafg, fhpu Pbagevohgbe ol ernfba bs lbhe npprcgvat nal fhpu jneenagl be nqqvgvbany yvnovyvgl.\n"
  },
  {
    "path": "scripts/lics/6.r13",
    "content": "CERNZOYR\n\nGur tbnyf bs gur Bcra Sbag Yvprafr (BSY) ner gb fgvzhyngr jbeyqjvqr qrirybczrag bs pbyynobengvir sbag cebwrpgf, gb fhccbeg gur sbag perngvba rssbegf bs npnqrzvp naq yvathvfgvp pbzzhavgvrf, naq gb cebivqr n serr naq bcra senzrjbex va juvpu sbagf znl or funerq naq vzcebirq va cnegarefuvc jvgu bguref.\n\nGur BSY nyybjf gur yvprafrq sbagf gb or hfrq, fghqvrq, zbqvsvrq naq erqvfgevohgrq serryl nf ybat nf gurl ner abg fbyq ol gurzfryirf. Gur sbagf, vapyhqvat nal qrevingvir jbexf, pna or ohaqyrq, rzorqqrq, erqvfgevohgrq naq/be fbyq jvgu nal fbsgjner cebivqrq gung nal erfreirq anzrf ner abg hfrq ol qrevingvir jbexf. Gur sbagf naq qrevingvirf, ubjrire, pnaabg or eryrnfrq haqre nal bgure glcr bs yvprafr. Gur erdhverzrag sbe sbagf gb erznva haqre guvf yvprafr qbrf abg nccyl gb nal qbphzrag perngrq hfvat gur sbagf be gurve qrevingvirf.\n\nQRSVAVGVBAF\n\n\"Sbag Fbsgjner\" ersref gb gur frg bs svyrf eryrnfrq ol gur Pbclevtug Ubyqre(f) haqre guvf yvprafr naq pyrneyl znexrq nf fhpu. Guvf znl vapyhqr fbhepr svyrf, ohvyq fpevcgf naq qbphzragngvba.\n\n\"Erfreirq Sbag Anzr\" ersref gb nal anzrf fcrpvsvrq nf fhpu nsgre gur pbclevtug fgngrzrag(f).\n\n\"Bevtvany Irefvba\" ersref gb gur pbyyrpgvba bs Sbag Fbsgjner pbzcbaragf nf qvfgevohgrq ol gur Pbclevtug Ubyqre(f).\n\n\"Zbqvsvrq Irefvba\" ersref gb nal qrevingvir znqr ol nqqvat gb, qryrgvat, be fhofgvghgvat - va cneg be va jubyr - nal bs gur pbzcbaragf bs gur Bevtvany Irefvba, ol punatvat sbezngf be ol cbegvat gur Sbag Fbsgjner gb n arj raivebazrag.\n\n\"Nhgube\" ersref gb nal qrfvtare, ratvarre, cebtenzzre, grpuavpny jevgre be bgure crefba jub pbagevohgrq gb gur Sbag Fbsgjner.\n\nCREZVFFVBA & PBAQVGVBAF\n\nCrezvffvba vf urerol tenagrq, serr bs punetr, gb nal crefba bognvavat n pbcl bs gur Sbag Fbsgjner, gb hfr, fghql, pbcl, zretr, rzorq, zbqvsl, erqvfgevohgr, naq fryy zbqvsvrq naq hazbqvsvrq pbcvrf bs gur Sbag Fbsgjner, fhowrpg gb gur sbyybjvat pbaqvgvbaf:\n\n1) Arvgure gur Sbag Fbsgjner abe nal bs vgf vaqvivqhny pbzcbaragf, va Bevtvany be Zbqvsvrq Irefvbaf, znl or fbyq ol vgfrys.\n\n2) Bevtvany be Zbqvsvrq Irefvbaf bs gur Sbag Fbsgjner znl or ohaqyrq, erqvfgevohgrq naq/be fbyq jvgu nal fbsgjner, cebivqrq gung rnpu pbcl pbagnvaf gur nobir pbclevtug abgvpr naq guvf yvprafr. Gurfr pna or vapyhqrq rvgure nf fgnaq-nybar grkg svyrf, uhzna-ernqnoyr urnqref be va gur nccebcevngr znpuvar-ernqnoyr zrgnqngn svryqf jvguva grkg be ovanel svyrf nf ybat nf gubfr svryqf pna or rnfvyl ivrjrq ol gur hfre.\n\n3) Ab Zbqvsvrq Irefvba bs gur Sbag Fbsgjner znl hfr gur Erfreirq Sbag Anzr(f) hayrff rkcyvpvg jevggra crezvffvba vf tenagrq ol gur pbeerfcbaqvat Pbclevtug Ubyqre. Guvf erfgevpgvba bayl nccyvrf gb gur cevznel sbag anzr nf cerfragrq gb gur hfref.\n\n4) Gur anzr(f) bs gur Pbclevtug Ubyqre(f) be gur Nhgube(f) bs gur Sbag Fbsgjner funyy abg or hfrq gb cebzbgr, raqbefr be nqiregvfr nal Zbqvsvrq Irefvba, rkprcg gb npxabjyrqtr gur pbagevohgvba(f) bs gur Pbclevtug Ubyqre(f) naq gur Nhgube(f) be jvgu gurve rkcyvpvg jevggra crezvffvba.\n\n5) Gur Sbag Fbsgjner, zbqvsvrq be hazbqvsvrq, va cneg be va jubyr, zhfg or qvfgevohgrq ragveryl haqre guvf yvprafr, naq zhfg abg or qvfgevohgrq haqre nal bgure yvprafr. Gur erdhverzrag sbe sbagf gb erznva haqre guvf yvprafr qbrf abg nccyl gb nal qbphzrag perngrq hfvat gur Sbag Fbsgjner.\n\nGREZVANGVBA\n\nGuvf yvprafr orpbzrf ahyy naq ibvq vs nal bs gur nobir pbaqvgvbaf ner abg zrg.\n\nQVFPYNVZRE\n\nGUR SBAG FBSGJNER VF CEBIVQRQ \"NF VF\", JVGUBHG JNEENAGL BS NAL XVAQ, RKCERFF BE VZCYVRQ, VAPYHQVAT OHG ABG YVZVGRQ GB NAL JNEENAGVRF BS ZREPUNAGNOVYVGL, SVGARFF SBE N CNEGVPHYNE CHECBFR NAQ ABAVASEVATRZRAG BS PBCLEVTUG, CNGRAG, GENQRZNEX, BE BGURE EVTUG. VA AB RIRAG FUNYY GUR PBCLEVTUG UBYQRE OR YVNOYR SBE NAL PYNVZ, QNZNTRF BE BGURE YVNOVYVGL, VAPYHQVAT NAL TRARENY, FCRPVNY, VAQVERPG, VAPVQRAGNY, BE PBAFRDHRAGVNY QNZNTRF, JURGURE VA NA NPGVBA BS PBAGENPG, GBEG BE BGUREJVFR, NEVFVAT SEBZ, BHG BS GUR HFR BE VANOVYVGL GB HFR GUR SBAG FBSGJNER BE SEBZ BGURE QRNYVATF VA GUR SBAG FBSGJNER."
  },
  {
    "path": "scripts/lics/README.md",
    "content": "these are foss licenses in rot13 so scanners don't think copyparty isn't mit\n\n1=mit 2=2bsd 3=3bsd 4=isc 5=apache2 6=ofl\n"
  },
  {
    "path": "scripts/lics/rot.py",
    "content": "#!/usr/bin/env python3\n\nimport os, codecs\n\nfor fn in os.listdir(\".\"):\n    if not fn.endswith(\".txt\"):\n        continue\n    with open(fn, \"rb\") as f:\n        s = f.read().decode(\"utf-8\")\n    b = codecs.encode(s, \"rot_13\").encode(\"utf-8\")\n    with open(fn.replace(\"txt\", \"r13\"), \"wb\") as f:\n        f.write(b)\n"
  },
  {
    "path": "scripts/logpack.sh",
    "content": "#!/bin/bash\nset -e\n\n# recompress logs so they decompress faster + save some space;\n# * will not recurse into subfolders\n# * each file in current folder gets recompressed to zstd; input file is DELETED\n# * any xz-compressed logfiles are decompressed before converting to zstd\n# * SHOULD ignore and skip files which are currently open; SHOULD be safe to run while copyparty is running\n\n\n# for files larger than $cutoff, compress with `zstd -T0`\n# (otherwise do several files in parallel (scales better))\ncutoff=400M\n\n\n# osx support:\n# port install findutils gsed coreutils\ncommand -v gfind >/dev/null &&\ncommand -v gsed  >/dev/null &&\ncommand -v gsort >/dev/null && {\n    find() { gfind \"$@\"; }\n    sed()  { gsed  \"$@\"; }\n    sort() { gsort \"$@\"; }\n}\n\npackfun() {\n    local jobs=$1 fn=\"$2\"\n    printf '%s\\n' \"$fn\" | grep -qF .zst && return\n\n    local of=\"$(printf '%s\\n' \"$fn\" | sed -r 's/\\.(xz|txt)/.zst/')\"\n    [ \"$fn\" = \"$of\" ] &&\n        of=\"$of.zst\"\n\n    [ -e \"$of\" ] &&\n        echo \"SKIP: output file exists: $of\" &&\n        return\n\n    lsof -- \"$fn\" 2>/dev/null | grep -E .. &&\n        printf \"SKIP: file in use: %s\\n\\n\" $fn &&\n        return\n\n    # determine by header; old copyparty versions would produce xz output without .xz names\n    head -c3 \"$fn\" | grep -qF 7z &&\n        cmd=\"xz -dkc\" || cmd=\"cat\"\n\n    printf '<%s> T%d: %s\\n' \"$cmd\" $jobs \"$of\"\n\n    $cmd <\"$fn\" >/dev/null || {\n        echo \"ERROR: uncompress failed: $fn\"\n        return\n    }\n\n    $cmd <\"$fn\" | zstd --long -19 -T$jobs >\"$of\"\n    touch -r \"$fn\" -- \"$of\"\n\n    cmp <($cmd <\"$fn\") <(zstd -d <\"$of\") || {\n        echo \"ERROR: data mismatch: $of\"\n        mv \"$of\"{,.BAD}\n        return\n    }\n    rm -- \"$fn\"\n}\n\n# do small files in parallel first (in descending size);\n# each file can use 4 threads in case the cutoff is poor\nexport -f packfun\nexport -f sed 2>/dev/null || true\nfind -maxdepth 1 -type f -size -$cutoff -printf '%s %p\\n' |\nsort -nr | sed -r 's`[^ ]+ ``; s`^\\./``' | tr '\\n' '\\0' |\nxargs \"$@\" -0i -P$(nproc) bash -c 'packfun 4 \"$@\"' _ {}\n\n# then the big ones, letting each file use the whole cpu\nfor f in *; do packfun 0 \"$f\"; done\n"
  },
  {
    "path": "scripts/make-pypi-release.sh",
    "content": "#!/bin/bash\nset -e\necho\n\n# osx support\n# port install gnutar findutils gsed coreutils\ngtar=$(command -v gtar || command -v gnutar) || true\n[ ! -z \"$gtar\" ] && command -v gfind >/dev/null && {\n\ttar()  { $gtar \"$@\"; }\n\tsed()  { gsed  \"$@\"; }\n\tfind() { gfind \"$@\"; }\n\tsort() { gsort \"$@\"; }\n\tnproc() { gnproc; }\n\tcommand -v grealpath >/dev/null &&\n\t\trealpath() { grealpath \"$@\"; }\n}\n\nmode=\"$1\"\nfast=\"$2\"\n\n[ -z \"$mode\" ] &&\n{\n\techo \"need argument 1:  (D)ry, (T)est, (U)pload\"\n\techo \" optional arg 2:  fast\"\n\techo\n\texit 1\n}\n\n[ -e copyparty/__main__.py ] || cd ..\n[ -e copyparty/__main__.py ] ||\n{\n\techo \"run me from within the copyparty folder\"\n\techo\n\texit 1\n}\n\n\n# one-time stuff, do this manually through copy/paste\ntrue ||\n{\n\tcat > ~/.pypirc <<EOF\n[distutils]\nindex-servers =\n  pypi\n  pypitest\n\n[pypi]\nrepository: https://upload.pypi.org/legacy/\nusername: qwer\npassword: asdf\n\n[pypitest]\nrepository: https://test.pypi.org/legacy/\nusername: qwer\npassword: asdf\nEOF\n\n\t# set pypi password\n\tchmod 600 ~/.pypirc\n\tsed -ri 's/qwer/username/;s/asdf/password/' ~/.pypirc\n}\n\n\n\npydir=\"$(\n\twhich python |\n\tsed -r 's@[^/]*$@@'\n)\"\n\n[ -e \"$pydir/activate\" ] &&\n{\n\techo '`deactivate` your virtualenv'\n\texit 1\n}\n\nfunction have() {\n\tpython -c \"import $1; $1; getattr($1,'__version__',0)\"\n}\n\nfunction load_env() {\n\t. buildenv/bin/activate || return 1\n\thave setuptools &&\n\thave wheel &&\n\thave build &&\n\thave twine &&\n\thave jinja2 &&\n\thave strip_hints &&\n\treturn 0 || return 1\n}\n\nload_env || {\n\techo creating buildenv\n\tdeactivate || true\n\trm -rf buildenv\n\tpython3 -m venv buildenv\n\t(. buildenv/bin/activate && pip install \\\n\t\tsetuptools wheel build twine jinja2 strip_hints )\n\tload_env\n}\n\n# cleanup\nrm -rf unt build/pypi\n\n# generate license list\nscripts/genlic.py copyparty/res/COPYING.txt\n\n# clean-ish packaging env\nrm -rf build/pypi\nmkdir -p build/pypi\ncp -pR pyproject.toml README.md LICENSE copyparty contrib bin scripts/strip_hints build/pypi/\ncd build/pypi\n\n# delete junk\nfind -name '*.pyc' -delete\nfind -name __pycache__ -delete\nfind -name py.typed -delete\nfind -type f \\( -name .DS_Store -or -name ._.DS_Store \\) -delete\nfind -type f -name ._\\* | while IFS= read -r f; do cmp <(printf '\\x00\\x05\\x16') <(head -c 3 -- \"$f\") && rm -f -- \"$f\"; done\n\n# remove type hints to support python < 3.9\nf=../strip-hints-0.1.10.tar.gz\n[ -e $f ] || \n\t(url=https://files.pythonhosted.org/packages/9c/d4/312ddce71ee10f7e0ab762afc027e07a918f1c0e1be5b0069db5b0e7542d/strip-hints-0.1.10.tar.gz;\n\twget -O$f \"$url\" || curl -L \"$url\" >$f)\ntar --strip-components=2 -xf $f strip-hints-0.1.10/src/strip_hints\npython3 -c 'from strip_hints.a import uh; uh(\"copyparty\")'\n\n# resolve symlinks\nfind -type l |\nwhile IFS= read -r f1; do (\n\tcd \"${f1%/*}\"\n\tf1=\"./${f1##*/}\"\n\tf2=\"$(readlink \"$f1\")\"\n\t[ -e \"$f2\" ] || f2=\"../$f2\"\n\t[ -e \"$f2\" ] || {\n\t\techo could not resolve \"$f1\"\n\t\texit 1\n\t}\n\trm \"$f1\"\n\tcp -p \"$f2\" \"$f1\"\n); done\n\n# resolve symlinks on windows\n[ \"$OSTYPE\" = msys ] &&\n(cd ../..; git ls-files -s | awk '/^120000/{print$4}') |\nwhile IFS= read -r x; do\n\t[ $(wc -l <\"$x\") -gt 1 ] && continue\n\t(cd \"${x%/*}\"; cp -p \"../$(cat \"${x##*/}\")\" ${x##*/})\ndone\n\nrm -rf contrib\n[ $fast ] && sed -ri s/573/10/ copyparty/web/Makefile\n(cd copyparty/web && make -j$(nproc) && rm Makefile*)\nrm -f copyparty/web/deps/README.md\n\n# build\npython3 -m build\n\n[ \"$mode\" == t ] && twine upload -r pypitest dist/*\n[ \"$mode\" == u ] && twine upload -r pypi dist/*\n\ntrue\n"
  },
  {
    "path": "scripts/make-pyz.sh",
    "content": "#!/bin/bash\nset -e\necho\n\n# port install gnutar gsed coreutils\ngtar=$(command -v gtar || command -v gnutar) || true\n[ ! -z \"$gtar\" ] && command -v gsed >/dev/null && {\n\ttar()  { $gtar \"$@\"; }\n\tsed()  { gsed  \"$@\"; }\n\tcommand -v grealpath >/dev/null &&\n\t\trealpath() { grealpath \"$@\"; }\n}\n\ntmv() {\n\ttouch -r \"$1\" t\n\tmv t \"$1\"\n}\nised() {\n\tsed -r \"$1\" <\"$2\" >t\n\ttmv \"$2\"\n}\n\ntargs=(--owner=1000 --group=1000)\n[ \"$OSTYPE\" = msys ] &&\n\ttargs=()\n\n[ -e copyparty/__main__.py ] || cd ..\n[ -e copyparty/__main__.py ] || {\n\techo \"run me from within the project root folder\"\n\techo\n\texit 1\n}\n\n[ -e sfx/copyparty/__main__.py ] || {\n    echo \"run ./scripts/make-sfx.py first\"\n    echo\n    exit 1\n}\n\nrm -rf pyz\nmkdir -p pyz\ncd pyz\n\ncp -pR ../sfx/copyparty .\ncp -pR ../sfx/j2/* .\n[ -e ../sfx/partftpy ] && cp -pR ../sfx/partftpy .\n[ -e ../sfx/ftp ] && cp -pR ../sfx/ftp/* .\n\ntrue && {\n\trm -rf copyparty/web/mde.* copyparty/web/deps/easymde*\n\techo h > copyparty/web/mde.html\n\tised '/edit2\">edit \\(fancy/d' copyparty/web/md.html\n}\n\nts=$(date -u +%s)\nhts=$(date -u +%Y-%m%d-%H%M%S)\nver=\"$(cat ../sfx/ver)\"\n\nmkdir -p ../dist\npyz_out=../dist/copyparty.pyz\n\necho creating loader\nsed -r 's/^(VER = ).*/\\1\"'\"$ver\"'\"/; s/^(STAMP = ).*/\\1'$(date +%s)/ \\\n    <../scripts/ziploader.py \\\n    >__main__.py\n\necho creating pyz\nrm -f $pyz_out\nzip -9 -q -r $pyz_out *\n\necho done:\necho \"  $(realpath $pyz_out)\"\n"
  },
  {
    "path": "scripts/make-rpm.sh",
    "content": "#!/bin/bash\nset -e\n\n#--localbuild to build webdeps and tar locally; otherwise just download prebuilt\n#--pm change packagemanager; otherwise default to dnf\n\nwhile [ ! -z \"$1\" ]; do\n\tcase $1 in\n\t\tlocal-build)  local_build=1  ; ;;\n\t\tpm)           shift;packagemanager=\"$1\";  ;;\n\tesac\n\tshift\ndone\n\n[ -e copyparty/__main__.py ] || cd ..\n[ -e copyparty/__main__.py ] ||\n{\n\techo \"run me from within the project root folder\"\n\techo\n\texit 1\n}\n\n\npackagemanager=${packagemanager:-dnf}\nver=$(awk '/^VERSION/{gsub(/[^0-9]/,\" \");printf \"%d.%d.%d\\n\",$1,$2,$3}' copyparty/__version__.py)\nreleasedir=\"dist/temp_copyparty_$ver\"\nsourcepkg=\"copyparty-$ver.tar.gz\"\n\n#make temporary directory to build rpm in\nmkdir -p $releasedir/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}\ntrap \"rm -rf $releasedir\" EXIT\n\n# make/get tarball\nif [ $local_build ]; then \n    if [ ! -f \"copyparty/web/deps/mini-fa.woff\" ]; then\n        sudo $packagemanager update\n        sudo $packagemanager install podman-docker docker\n        make -C deps-docker\n    fi\n    if [ ! -f \"dist/$sourcepkg\" ]; then\n        ./$cppdir/scripts/make-sfx.sh gz fast  # pulls some build-deps + good smoketest\n        ./$cppdir/scripts/make-tgz-release.sh \"$ver\"\n    fi\nelse\n    if [ ! -f \"dist/$sourcepkg\" ]; then\n        curl -OL https://github.com/9001/copyparty/releases/download/v$ver/$sourcepkg --output-dir dist\n    fi\nfi\n\ncp dist/$sourcepkg \"$releasedir/SOURCES/$sourcepkg\"\n\ncp \"contrib/package/rpm/copyparty.spec\" \"$releasedir/SPECS/\"\nsed -i \"s/\\$pkgver/$ver/g\" \"$releasedir/SPECS/copyparty.spec\"\nsed -i \"s/\\$pkgrel/1/g\"  \"$releasedir/SPECS/copyparty.spec\"\n\nsudo $packagemanager update\nsudo $packagemanager install \\\n\trpmdevtools python-devel pyproject-rpm-macros \\\n\tpython-wheel python-setuptools python-jinja2 \\\n    make pigz\ncd \"$releasedir/\"\nrpmbuild --define \"_topdir `pwd`\" -bb SPECS/copyparty.spec\ncd -\n\nrpm=\"copyparty-$ver-1.noarch.rpm\"\nmv \"$releasedir/RPMS/noarch/$rpm\" dist/$rpm\n"
  },
  {
    "path": "scripts/make-sfx.sh",
    "content": "#!/bin/bash\nset -e\necho\n\nberr() { p=$(head -c 72 </dev/zero | tr '\\0' =); printf '\\n%s\\n\\n' $p; cat; printf '\\n%s\\n\\n' $p; }\naerr() { printf '%s\\n' \"$*\" | berr; }\n\nhelp() { exec cat <<'EOF'\n\n# optional args:\n#\n# `fast` builds faster, with cheaper js/css compression\n#\n# `clean` uses files from git (everything except web/deps),\n#   so local changes won't affect the produced sfx\n#\n# `re` does a repack of an sfx which you already executed once\n#   (grabs files from the sfx-created tempdir), overrides `clean`\n#\n# `lang` limits which languages/translations to include,\n#   for example `lang eng` or `lang eng|nor`\n#\n# _____________________________________________________________________\n# compression tweaks:\n#\n# `gz` creates a gzip-compressed python sfx instead of bzip2\n#   (improves compat with minimal and/or ancient pythons)\n#\n# `gzz 50` uses zopfli to create a gzip-compressed python sfx\n#   (better compression than regular gz without affecting compat)\n#\n# `xz` creates an xz-compressed python sfx instead of bzip2\n#   (tiny bit smaller, but needs modern python to run)\n#\n# `nopk` disables js/css compression; builds faster, and\n#   the sfx becomes smaller, but reduces runtime performance\n#\n# `udep` unpacks compressed js/css (use with `nopk` and `xz`);\n#   even smaller sfx, much worse RAM/network waste at runtime\n#   (only useful for jokes such as putting the sfx on a floppy)\n#\n# _____________________________________________________________________\n# core features:\n#\n# `no-ftp` saves ~30k by removing the ftp server, disabling --ftp\n#\n# `no-pf` saves ~12k by removing the option to download partyfuse\n#\n# `no-tfp` saves ~10k by removing the tftp server, disabling --tftp\n#\n# `no-sfp` saves ~?k by removing the sftp server, disabling --sftp\n#\n# `no-zm` saves ~7k by removing the zeroconf mDNS server\n#\n# `no-z` saves ~7k by removing all zeroconf (mDNS, SSDP)\n#\n# `no-smb` saves ~3.5k by removing the smb / cifs server\n#\n# _____________________________________________________________________\n# web features:\n#\n# `no-cm` saves ~89k by removing easymde/codemirror\n#   (the fancy markdown editor)\n#\n# `no-hl` saves ~41k by removing syntax highlighting in the text viewer\n#\n# `no-fnt` saves ~9k by removing the source-code-pro font\n#   (browsers will try to use 'Consolas' instead)\n#\n# _____________________________________________________________________\n# build behavior:\n#\n# `dl-wd` automatically downloads webdeps if necessary\n#\n# `ign-wd` allows building an sfx without webdeps\n#\n# _____________________________________________________________________\n# if you are on windows, you can use msys2:\n#   PATH=/c/Users/$USER/AppData/Local/Programs/Python/Python310:\"$PATH\" ./make-sfx.sh fast\n#\n# _____________________________________________________________________\n# some usage examples:\n#   ./scripts/make-sfx.sh lang eng no-cm no-hl no-fnt no-smb no-pf\n#   ./scripts/rls.sh sfx  lang eng no-cm no-hl no-fnt no-smb no-pf\n#   (reduces v1.14.2 from 700k to 495k)\n\nEOF\n}\n\n# port install gnutar findutils gsed gawk coreutils\ngtar=$(command -v gtar || command -v gnutar) || true\n[ ! -z \"$gtar\" ] && command -v gfind >/dev/null && {\n\ttar()  { $gtar \"$@\"; }\n\ttr()   { gtr   \"$@\"; }\n\tsed()  { gsed  \"$@\"; }\n\tfind() { gfind \"$@\"; }\n\tsort() { gsort \"$@\"; }\n\tnproc() { gnproc; }\n\tsha1sum() { shasum \"$@\"; }\n\tunexpand() { gunexpand \"$@\"; }\n\tcommand -v grealpath >/dev/null &&\n\t\trealpath() { grealpath \"$@\"; }\n\n\t[ -e /opt/local/bin/bzip2 ] &&\n\t\tbzip2() { /opt/local/bin/bzip2 \"$@\"; }\n}\n\ngawk=$(command -v gawk || command -v gnuawk || command -v awk)\nawk() { $gawk \"$@\"; }\n\ntargs=(--owner=1000 --group=1000)\n[ \"$OSTYPE\" = msys ] &&\n\ttargs=()\n\npybin=$(command -v python3 || command -v python) || {\n\techo need python\n\texit 1\n}\n\n[ -e copyparty/__main__.py ] || cd ..\n[ -e copyparty/__main__.py ] ||\n{\n\techo \"run me from within the project root folder\"\n\techo\n\texit 1\n}\n\nlangs=\nuse_gz=\nuse_xz=\nzopf=2000\nudep=\nwhile [ ! -z \"$1\" ]; do\n\tcase $1 in\n\t\tclean)  clean=1  ; ;;\n\t\tre)     repack=1 ; ;;\n\t\txz)     use_xz=1 ; ;;\n\t\tgz)     use_gz=1 ; ;;\n\t\tgzz)    shift;use_gzz=$1;use_gz=1; ;;\n\t\tno-sfp) no_sfp=1 ; ;;\n\t\tno-ftp) no_ftp=1 ; ;;\n\t\tno-tfp) no_tfp=1 ; ;;\n\t\tno-smb) no_smb=1 ; ;;\n\t\tno-zm)  no_zm=1  ; ;;\n\t\tno-z)   no_zm=1;no_z=1; ;;\n\t\tno-pf)  no_pf=1  ; ;;\n\t\tno-fnt) no_fnt=1 ; ;;\n\t\tno-hl)  no_hl=1  ; ;;\n\t\tno-cm)  no_cm=1  ; ;;\n\t\tdl-wd)  dl_wd=1  ; ;;\n\t\tign-wd) ign_wd=1 ; ;;\n\t\tfast)   zopf=    ; ;;\n\t\tnopk)   zopf=no  ; ;;\n\t\tudep)   udep=1   ; ;;\n\t\tlang)   shift;langs=\"$1\"; ;;\n\t\t*)      help     ; ;;\n\tesac\n\tshift\ndone\n\ntmv() {\n\ttouch -r \"$1\" t\n\tmv t \"$1\"\n}\niawk() {\n\tawk \"$1\" <\"$2\" >t\n\ttmv \"$2\"\n}\nised() {\n\tsed -r \"$1\" <\"$2\" >t\n\ttmv \"$2\"\n}\ndlf() {\n\t[ -s \"$f\" ] && return 0\n\twget -O \"$f\" \"$1\" && return 0\n\tcurl -L \"$1\" >\"$f\" && return 0\n\trm -f \"$f\"\n\texit 1\n}\n\nstamp=$(\n\tfor d in copyparty scripts; do\n\t\tfind $d -type f -printf '%TY-%Tm-%Td %TH:%TM:%TS %p\\n'\n\tdone | sort | tail -n 1 | sha1sum | cut -c-16\n)\n\nrm -rf sfx/*\nmkdir -p sfx build\ncd sfx\n\ntmpdir=\"$(\n\tprintf '%s\\n' \"$TMPDIR\" /tmp |\n\tawk '/./ {print; exit}'\n)\"\n\nnecho() {\n\tprintf '\\033[G%s ... \\033[K' \"$*\"\n}\n\n[ $repack ] && {\n\told=\"$tmpdir/pe-copyparty.$(id -u)\"\n\techo \"repack of files in $old\"\n\tcp -pR \"$old/\"*{py2,py37,magic,j2,copyparty} .\n\tcp -pR \"$old/\"*partftpy . || true\n\tcp -pR \"$old/\"*ftp . || true\n}\n\n[ $repack ] || {\n\t(cd ../scripts; ./genlic.py ../copyparty/res/COPYING.txt)\n\n\tnecho collecting ipaddress\n\tf=\"../build/ipaddress-1.0.23.tar.gz\"\n\t[ -e \"$f\" ] ||\n\t\tdlf https://files.pythonhosted.org/packages/b9/9a/3e9da40ea28b8210dd6504d3fe9fe7e013b62bf45902b458d1cdc3c34ed9/ipaddress-1.0.23.tar.gz\n\n\ttar -zxf $f\n\tmkdir py37\n\tmv ipaddress-*/ipaddress.py py37/\n\trm -rf ipaddress-*\n\n\tnecho collecting jinja2\n\tf=\"../build/Jinja2-2.11.3.tar.gz\"\n\t[ -e \"$f\" ] ||\n\t\tdlf https://files.pythonhosted.org/packages/4f/e7/65300e6b32e69768ded990494809106f87da1d436418d5f1367ed3966fd7/Jinja2-2.11.3.tar.gz\n\n\ttar -zxf $f\n\tmv Jinja2-*/src/jinja2 .\n\trm -rf Jinja2-*\n\t\n\tnecho collecting markupsafe\n\tf=\"../build/MarkupSafe-1.1.1.tar.gz\"\n\t[ -e \"$f\" ] ||\n\t\tdlf https://files.pythonhosted.org/packages/b9/2e/64db92e53b86efccfaea71321f597fa2e1b2bd3853d8ce658568f7a13094/MarkupSafe-1.1.1.tar.gz\n\n\ttar -zxf $f\n\tmv MarkupSafe-*/src/markupsafe .\n\trm -rf MarkupSafe-* markupsafe/_speedups.c\n\n\tmkdir j2/\n\tmv {markupsafe,jinja2} j2/\n\n\tnecho collecting pyftpdlib\n\tf=\"../build/pyftpdlib-1.5.10.tar.gz\"\n\t[ -e \"$f\" ] ||\n\t\tdlf https://files.pythonhosted.org/packages/cf/31/8d910cf40317dd0db74ba0b8558d0dee23c8b002468c14d3a5dec0e6e9fd/pyftpdlib-1.5.10.tar.gz\n\n\ttar -zxf $f\n\tmv pyftpdlib-*/pyftpdlib .\n\trm -rf pyftpdlib-* pyftpdlib/test\n\tpatch -s -p1 <../scripts/patches/pyftpdlib-win313.patch\n\tpatch -s -p1 <../scripts/patches/pyftpdlib-fe80.patch\n\tfor f in pyftpdlib/_async{hat,ore}.py; do\n\t\t[ -e \"$f\" ] || continue;\n\t\tiawk 'NR<4||NR>27||!/^#/;NR==4{print\"# license: https://opensource.org/licenses/ISC\\n\"}' $f\n\tdone\n\n\tmkdir ftp/\n\tmv pyftpdlib ftp/\n\n\tnecho collecting partftpy\n\tf=\"../build/partftpy-0.4.0.tar.gz\"\n\t[ -e \"$f\" ] ||\n\t\tdlf https://files.pythonhosted.org/packages/8c/96/642bb3ddcb07a2c6764eb29aa562d1cf56877ad6c330c3c8921a5f05606d/partftpy-0.4.0.tar.gz\n\n\ttar -zxf $f\n\tmv partftpy-*/partftpy .\n\trm -rf partftpy-* partftpy/bin\n\t#(cd partftpy && \"$pybin\" ../../scripts/strip_hints/a.py; rm uh)  # dont need the full thing, just this:\n\tsed -ri 's/from typing import TYPE_CHECKING$/TYPE_CHECKING = False/' partftpy/TftpShared.py\n\n\tnecho collecting python-magic\n\tv=0.4.27\n\tf=\"../build/python-magic-$v.tar.gz\"\n\t[ -e \"$f\" ] ||\n\t\tdlf https://files.pythonhosted.org/packages/da/db/0b3e28ac047452d079d375ec6798bf76a036a08182dbb39ed38116a49130/python-magic-0.4.27.tar.gz\n\n\ttar -zxf $f\n\tmkdir magic\n\tmv python-magic-*/magic .\n\trm -rf python-magic-*\n\trm magic/compat.py\n\tiawk '/^def _add_compat/{o=1} !o; /^_add_compat/{o=0}' magic/__init__.py\n\n\t# enable this to dynamically remove type hints at startup,\n\t# in case a future python version can use them for performance\n\ttrue && (\n\t\tnecho collecting strip-hints\n\t\tf=../build/strip-hints-0.1.10.tar.gz\n\t\t[ -e $f ] ||\n\t\t\tdlf https://files.pythonhosted.org/packages/9c/d4/312ddce71ee10f7e0ab762afc027e07a918f1c0e1be5b0069db5b0e7542d/strip-hints-0.1.10.tar.gz\n\n\t\ttar -zxf $f\n\t\tmv strip-hints-0.1.10/src/strip_hints .\n\t\trm -rf strip-hints-* strip_hints/import_hooks*\n\t\tsed -ri 's/[a-z].* as import_hooks$/\"\"\"a\"\"\"/' strip_hints/*.py\n\n\t\tcp -pR ../scripts/strip_hints/ .\n\t)\n\tcp -pR ../scripts/py2 .\n\n\t# msys2 tar is bad, make the best of it\n\tnecho collecting source\n\techo\n\t[ $clean ] && {\n\t\t(cd .. && git archive hovudstraum >tar) && tar -xf ../tar copyparty\n\t\t(cd .. && tar -cf tar copyparty/web/deps) && tar -xf ../tar\n\t}\n\t[ $clean ] || {\n\t\t(cd .. && tar -cf tar copyparty) && tar -xf ../tar\n\t}\n\trm -f ../tar\n\n\t# resolve symlinks\n\tfind -type l |\n\twhile IFS= read -r f1; do (\n\t\tcd \"${f1%/*}\"\n\t\tf1=\"./${f1##*/}\"\n\t\tf2=\"$(readlink \"$f1\")\"\n\t\t[ -e \"$f2\" ] || f2=\"../$f2\"\n\t\t[ -e \"$f2\" ] || {\n\t\t\techo could not resolve \"$f1\"\n\t\t\texit 1\n\t\t}\n\t\trm \"$f1\"\n\t\tcp -p \"$f2\" \"$f1\"\n\t); done\n\n\t# resolve symlinks on windows\n\t[ \"$OSTYPE\" = msys ] &&\n\t(cd ..; git ls-files -s | awk '/^120000/{print$4}') |\n\twhile IFS= read -r x; do\n\t\t[ $(wc -l <\"$x\") -gt 1 ] && continue\n\t\t(cd \"${x%/*}\"; cp -p \"../$(cat \"${x##*/}\")\" ${x##*/})\n\tdone\n\n\trm -f copyparty/stolen/*/README.md\n\n\t# remove type hints before build instead\n\t(cd copyparty; PYTHONPATH=\"..:$PYTHONPATH\" \"$pybin\" ../../scripts/strip_hints/a.py; rm uh)\n}\n\n[ ! -e copyparty/web/deps/mini-fa.woff ] && [ $dl_wd ] && {\n\techo \"could not find webdeps; downloading...\"\n\turl=https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py\n\tf=x.py; rm -f $f; dlf $url\n\n\techo \"extracting webdeps...\"\n\twdsrc=\"$(\"$pybin\" x.py --version 2>&1 | tee /dev/stderr | awk '/sfxdir:/{sub(/.*: /,\"\");print;exit}')\"\n\t[ \"$wdsrc\" ] || {\n\t\techo failed to discover tempdir of reference copyparty-sfx.py\n\t\texit 1\n\t}\n\trm -rf copyparty/web/deps\n\tcp -pvR \"$wdsrc/copyparty/web/deps\" copyparty/web/\n\n\t# also copy it out into the source-tree for next time\n\trm -rf ../copyparty/web/deps\n\tcp -pR copyparty/web/deps ../copyparty/web\n\n\trm x.py\n}\n\n[ -e copyparty/web/deps/mini-fa.woff ] || [ $ign_wd ] || { berr <<'EOF'\nERROR:\n  could not find webdeps; the front-end will not be fully functional\n\nplease choose one of the following:\n\nA) add the argument \"dl-wd\" to fix it automatically; this will\n    download copyparty-sfx.py and extract the webdeps from there\n\nB) build the webdeps from source:  make -C scripts/deps-docker\n\nC) add the argument \"ign-wd\" to continue building the sfx without webdeps\n\nalternative A is a good choice if you are only intending to\nmodify the copyparty source code (py/html/css/js) and do not\nplan to make any changes to the mostly-third-party webdeps\n\nthere may be additional hints in the devnotes:\nhttps://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#building\nEOF\n\texit 1\n}\n\nver=\n[ -z \"$repack\" ] &&\ngit describe --tags >/dev/null 2>/dev/null && {\n\tgit_ver=\"$(git describe --tags)\";  # v0.5.5-2-gb164aa0\n\tver=\"$(printf '%s\\n' \"$git_ver\" | sed -r 's/^v//')\";\n\tt_ver=\n\n\tprintf '%s\\n' \"$git_ver\" | grep -qE '^v[0-9\\.]+$' && {\n\t\t# short format (exact version number)\n\t\tt_ver=\"$(printf '%s\\n' \"$ver\" | sed -r 's/\\./, /g')\";\n\t}\n\n\tprintf '%s\\n' \"$git_ver\" | grep -qE '^v[0-9\\.]+-[0-9]+-g[0-9a-f]+$' && {\n\t\t# long format (unreleased commit)\n\t\tt_ver=\"$(printf '%s\\n' \"$ver\" | sed -r 's/[-.]/, /g; s/(.*) (.*)/\\1 \"\\2\"/')\"\n\t}\n\n\t[ -z \"$t_ver\" ] && {\n\t\tprintf 'unexpected git version format: [%s]\\n' \"$git_ver\"\n\t\texit 1\n\t}\n\n\tdt=\"$(git log -1 --format=%cd --date=short | sed -E 's/-0?/, /g')\"\n\tprintf 'git %3s: \\033[36m%s\\033[0m\\n' ver \"$ver\" dt \"$dt\"\n\tsed -ri '\n\t\ts/^(VERSION =)(.*)/#\\1\\2\\n\\1 ('\"$t_ver\"')/;\n\t\ts/^(S_VERSION =)(.*)/#\\1\\2\\n\\1 \"'\"$ver\"'\"/;\n\t\ts/^(BUILD_DT =)(.*)/#\\1\\2\\n\\1 ('\"$dt\"')/;\n\t' copyparty/__version__.py\n}\n\n[ -z \"$ver\" ] && \n\tver=\"$(awk '/^VERSION *= \\(/ {\n\t\tgsub(/[^0-9,a-g-]/,\"\"); gsub(/,/,\".\"); print; exit}' < copyparty/__version__.py)\"\n\necho \"$ver\" >ver  # pyz\n\nts=$(date -u +%s)\nhts=$(date -u +%Y-%m%d-%H%M%S) # --date=@$ts (thx osx)\n\nmkdir -p ../dist\nsfx_out=../dist/copyparty-sfx\n\necho cleanup\nfind -name '*.pyc' -delete\nfind -name __pycache__ -delete\nfind -name py.typed -delete\n\n# especially prevent macos/osx from leaking your lan ip (wtf apple)\nfind -type f \\( -name .DS_Store -or -name ._.DS_Store \\) -delete\nfind -type f -name ._\\* | while IFS= read -r f; do cmp <(printf '\\x00\\x05\\x16') <(head -c 3 -- \"$f\") && rm -fv -- \"$f\"; done\n\nrm -f \\\n\tcopyparty/web/deps/*.full.* \\\n\tcopyparty/web/deps/README.md \\\n\tcopyparty/web/dbg-* \\\n\tcopyparty/web/Makefile*\n\nfind copyparty | LC_ALL=C sort | sed -r 's/\\.gz$//;s/$/,/' > have\ncat have | while IFS= read -r x; do\n\tgrep -qF -- \"$x\" ../scripts/sfx.ls || {\n\t\techo \"unexpected file: $x\"\n\t\texit 1\n\t}\ndone\nrm have\n\nised /fork_process/d ftp/pyftpdlib/servers.py\niawk '/^class _Base/{s=1}!s' ftp/pyftpdlib/authorizers.py\niawk '/^ {0,4}[a-zA-Z]/{s=0}/^ {4}def (serve_forever|_loop)/{s=1}!s' ftp/pyftpdlib/servers.py\nrm -f ftp/pyftpdlib/{__main__,prefork}.py\n\nunhelp() {\n\tiawk '!/add_argument\\(\"--'$1'/{print;next}\n\t\t/ent\\(\"--'$1'\"/{print gensub(/(help=\")[^\"]+/,\"\\\\1not available in this build\",\"1\");next}\n\t\t{sub(/help=.*/,\"help=argparse.SUPPRESS)\")}1' copyparty/__main__.py\n}\n\nunhelpg() {\n    iawk '/^def/{m=0}\n        /^def add_'$1'/{m=1}\n        m>1{sub(/, help=\".*\"\\)$/, \", help=argparse.SUPPRESS)\")}\n        m==1&&/, help=\"/{m++;sub(/, help=\".*\"\\)$/, \", help=\\\"not available in this build\\\")\")}\n        1' copyparty/__main__.py\n}\n\n[ $no_ftp ] && {\n\tunhelp ftp\n\trm -rf copyparty/ftpd.py ftp\n}\n\n[ $no_tfp ] && {\n\tunhelp tftp\n\trm -rf copyparty/tftpd.py partftpy\n}\n\n[ $no_sfp ] && {\n\tunhelp sftp\n\trm -rf copyparty/sftpd.py\n}\n\n[ $no_smb ] && {\n\tunhelp smb\n\trm -f copyparty/smbd.py\n\tised 's/^( {8}elif )record\\.name.*\"impacket\".*/\\10:/' copyparty/util.py\n}\n\n[ $no_zm ] &&\n    iawk '$1==\"],\"{s=0}/\"mDNS debugging\"/{s=1;sub(/\".*/,\"\\\"not available in this build\\\",\\\"\\\"\");print};!s' copyparty/__main__.py &&\n    unhelpg zc_mdns &&\n\trm -rf copyparty/mdns.py copyparty/stolen/dnslib\n\n[ $no_z ] &&\n    unhelpg '(zeroconf|zc_ssdp)' &&\n\trm -rf copyparty/ssdp.py copyparty/multicast.py\n\n[ $no_pf ] &&\n\trm -rf copyparty/web/a/partyfuse.py copyparty/web/deps/fuse.py\n\n[ $no_cm ] && {\n\trm -rf copyparty/web/mde.* copyparty/web/deps/easymde*\n\techo h > copyparty/web/mde.html\n\tised '/edit2\">edit \\(fancy/d' copyparty/web/md.html\n}\n\n[ $no_hl ] &&\n\trm -rf copyparty/web/deps/prism*\n\n[ $no_fnt ] && {\n\trm -f copyparty/web/deps/scp.woff2\n\tf=copyparty/web/ui.css\n\tgzip -d \"$f.gz\" || true\n\tised \"s/src:.*scp.*\\)/src:local('Consolas')/\" $f\n}\n\n[ $langs ] && {\n\techo $langs | grep -q eng || {\n\t\tlangs=\"eng|$langs\"\n\t\taerr \"ERROR: removing english is not supported; will do this instead: $langs\"\n\t}\n\tf=copyparty/web/browser.js\n\tgzip -d \"$f.gz\" || true\n\tiawk '/^\\]/{s=0} !s; /^var LANGN /{s=1;next} !s{next} /\"'\"$langs\"'\"/' $f\n\tls -1 copyparty/web/tl/* >t\n\tgrep -vE \"/($langs)\\.\" <t | xargs -- rm\n\trm t\n}\n\n[ ! $repack ] && {\n\t# uncomment\n\tfind | grep -E '\\.py$' |\n\t\tgrep -vE '__version__' |\n\t\ttr '\\n' '\\0' |\n\t\txargs -0 \"$pybin\" ../scripts/uncomment.py\n\n\t# py2-compat\n\t#find | grep -E '\\.py$' | while IFS= read -r x; do\n\t#\tsed -ri '/: TypeAlias = /d' \"$x\"; done\n}\n\nrm -f j2/jinja2/constants.py\niawk '/^ {4}def /{s=0}/^ {4}def compile_templates\\(/{s=1}!s' j2/jinja2/environment.py\nised '/generate_lorem_ipsum/d' j2/jinja2/defaults.py\niawk '/^def /{s=0}/^def generate_lorem_ipsum/{s=1}!s' j2/jinja2/utils.py\niawk '/^(class|def) /{s=0}/^(class InternationalizationExtension|def _make_new_n?gettext)/{s=1}!s' j2/jinja2/ext.py\niawk '/^[^ ]/{s=0}/^def babel_extract/{s=1}!s' j2/jinja2/ext.py\nised '/InternationalizationExtension/d' j2/jinja2/ext.py\niawk '/^class/{s=0}/^class (Package|Dict|Prefix|Choice|Module)Loader/{s=1}!s' j2/jinja2/loaders.py\nsed -ri '/^from .bccache | (Package|Dict|Prefix|Choice|Module)Loader$/d' j2/jinja2/__init__.py\nrm -f j2/jinja2/async* j2/jinja2/{bccache,sandbox}.py\ncat > j2/jinja2/_identifier.py <<'EOF'\nimport re\npattern = re.compile(r\"\\w+\")\nEOF\n\ngrep -rLE '^#[^a-z]*coding: utf-8' j2 |\nwhile IFS= read -r f; do\n\t(echo \"# coding: utf-8\"; cat \"$f\") >t\n\ttmv \"$f\"\ndone\n\ngrep -rlE '^class [^(]+:' |\nwhile IFS= read -r f; do\n\tised 's/(^class [^(:]+):/\\1(object):/' \"$f\"\ndone\n\n# up2k goes from 28k to 22k laff\nawk 'BEGIN{gensub(//,\"\",1)}' </dev/null 2>/dev/null &&\necho entabbening &&\nfind | grep -E '\\.css$' | while IFS= read -r f; do\n\tawk '{\n\t\tsub(/^[ \\t]+/,\"\");\n\t\tsub(/[ \\t]+$/,\"\");\n\t\t$0=gensub(/^([a-z-]+) *: *(.*[^ ]) *;$/,\"\\\\1:\\\\2;\",\"1\");\n\t\tsub(/ +\\{$/,\"{\");\n\t\tgsub(/, /,\",\")\n\t}\n\t!/\\}$/ {printf \"%s\",$0;next}\n\t1\n\t' <$f | sed -r 's/;\\}$/}/; /\\{\\}$/d' >t\n\ttmv \"$f\"\ndone ||\n\techo \"WARNING: your awk does not have gensub, so the sfx will not have optimal compression\"\n\nunexpand -h 2>/dev/null &&\nfind | grep -E '\\.(js|html)$' | while IFS= read -r f; do\n\tunexpand -t 4 --first-only <\"$f\" >t\n\ttmv \"$f\"\ndone\n\ngzres() {\n\tlocal pk=\n\t[ \"$zopf\" = no ] && return\n\t[ $zopf ] && command -v zopfli && pk=\"zopfli --i$zopf\"\n\t[ $zopf ] && command -v pigz && pk=\"pigz -11 -I $zopf\"\n\t[ -z \"$pk\" ] && pk='gzip'\n\n\tnp=$(nproc)\n\techo \"$pk #$np\"\n\n\tfind copyparty/web/tl | grep '\\.js$' | while IFS= read -r f; do\n\t\t/bin/sh ../copyparty/web/Makefile.s1 <\"$f\" >t; tmv \"$f\"\n\tdone\n\n\twhile IFS=' ' read -r _ f; do\n\t\twhile true; do\n\t\t\tna=$(ps auxwww | grep -F \"$pk\" | wc -l)\n\t\t\t[ $na -le $np ] && break\n\t\t\tsleep 0.2\n\t\tdone\n\t\techo -n .\n\t\t$pk \"$f\" &\n\tdone < <(\n\t\tfind -printf '%s %p\\n' |\n\t\tgrep -E '\\.(js|css)$|/web/a/.*\\.txt$' |\n\t\tgrep -vF /deps/ |\n\t\tsort -nr\n\t)\n\twait\n\techo\n}\ngzres\n\n[ $udep ] &&\n    find -iname '*.gz' | while IFS= read -r x; do gzip -d \"$x\"; done\n\necho gen tarlist\nfor d in copyparty partftpy magic j2 py2 py37 ftp; do find $d -type f || true; done |  # strip_hints\nsed -r 's/(.*)\\.(.*)/\\2 \\1/' | LC_ALL=C sort |\nsed -r 's/([^ ]*) (.*)/\\2.\\1/' | grep -vE '/list1?$' > list1\n(grep -vE '\\.gz$' list1; grep -E '\\.gz$' list1) >list || true\n\necho creating tar\ntar -cf tar \"${targs[@]}\" --numeric-owner -T list\n\npc=\"bzip2 -\"; pe=bz2; pl=$(echo {2..9})\n[ $use_gz ] && pc=\"gzip -\" && pe=gz\n[ $use_gzz ] && pc=\"pigz -11 -I$use_gzz\" && pe=gz && pl=0\n[ $use_xz ] && pc=\"xz -zeT0 -\" && pe=xz\n\necho compressing tar\nfor n in $pl; do cp tar t.$n; nice -n20 $pc$n t.$n & done; wait\nminf=$(for f in t.*.$pe; do\n\ts1=$(wc -c <$f)\n\ts2=$(tr -d '\\r\\n\\0' <$f | wc -c)\n\techo \"$(( s2+(s1-s2)*3 )) $f\"\ndone | sort -n | awk '{print$2;exit}')\nmv -v $minf tar.bz2\nrm t.* 2>/dev/null || true\nexts=()\n\n\necho creating sfx\n\npy=../scripts/sfx.py\nsuf=\n[ $use_gz ] && {\n\tsed -r 's/\"r:bz2\"/\"r:gz\"/' <$py >$py.t\n\tpy=$py.t\n}\n[ $use_xz ] && {\n\tsed -r 's/\"r:bz2\"/\"r:xz\"/' <$py >$py.t\n\tpy=$py.t\n}\n\n\"$pybin\" $py --sfx-make tar.bz2 $ver $ts\nmv sfx.out $sfx_out$suf.py\n\nexts+=($suf.py)\n[ $use_gz ] && rm $py\n[ $use_xz ] && rm $py\n\n\nchmod 755 $sfx_out*\n\nprintf \"done:\\n\"\nfor ext in ${exts[@]}; do\n\tprintf \"  %s\\n\" \"$(realpath $sfx_out)\"$ext\ndone\n\n# apk add bash python3 tar xz bzip2\n# while true; do ./make-sfx.sh; f=../dist/copyparty-sfx.py; mv $f $f.$(wc -c <$f | awk '{print$1}'); done\n"
  },
  {
    "path": "scripts/make-tgz-release.sh",
    "content": "#!/bin/bash\nset -e\necho\n\n# osx support\n# port install gnutar findutils gsed coreutils\ngtar=$(command -v gtar || command -v gnutar) || true\n[ ! -z \"$gtar\" ] && command -v gfind >/dev/null && {\n\ttar()  { $gtar \"$@\"; }\n\tsed()  { gsed  \"$@\"; }\n\tfind() { gfind \"$@\"; }\n\tsort() { gsort \"$@\"; }\n\tcommand -v grealpath >/dev/null &&\n\t\trealpath() { grealpath \"$@\"; }\n}\n\nwhich md5sum 2>/dev/null >/dev/null &&\n\tmd5sum=md5sum ||\n\tmd5sum=\"md5 -r\"\n\nver=\"$1\"\n\n[ \"x$ver\" = x ] &&\n{\n\techo \"need argument 1:  version\"\n\techo\n\texit 1\n}\n\n[ -e copyparty/__main__.py ] || cd ..\n[ -e copyparty/__main__.py ] ||\n{\n\techo \"run me from within the project root folder\"\n\techo\n\texit 1\n}\n\nmkdir -p dist\nzip_path=\"$(pwd)/dist/copyparty-$ver.zip\"\ntgz_path=\"$(pwd)/dist/copyparty-$ver.tar.gz\"\n\n[ -e \"$zip_path\" ] ||\n[ -e \"$tgz_path\" ] &&\n{\n\techo \"found existing archives for this version\"\n\techo \"  $zip_path\"\n\techo \"  $tgz_path\"\n\techo\n\techo \"continue?\"\n\tread -u1\n}\nrm \"$zip_path\" 2>/dev/null || true\nrm \"$tgz_path\" 2>/dev/null || true\n\n#sed -ri \"s/^(ADMIN_PWD *= *u).*/\\1'hunter2'/\" copyparty/config.py\n\ntmp=\"$(mktemp -d)\"\nrls_dir=\"$tmp/copyparty-$ver\"\nmkdir \"$rls_dir\"\n\necho \">>> export from git\"\ngit archive hovudstraum | tar -xC \"$rls_dir\"\n\necho \">>> export untracked deps\"\ntar -c copyparty/web/deps | tar -xC \"$rls_dir\"\n\nscripts/genlic.py \"$rls_dir/copyparty/res/COPYING.txt\"\n\ncd \"$rls_dir\"\nfind -type d -exec chmod 755 '{}' \\+\nfind -type f -exec chmod 644 '{}' \\+\n\ncommaver=\"$(\n\tprintf '%s\\n' \"$ver\" |\n\tsed -r 's/\\./, /g'\n)\"\n\ngrep -qE \"^VERSION *= \\(${commaver}\\)$\" copyparty/__version__.py ||\n{\n\techo \"$tmp\"\n\techo \"bad version\"\n\techo\n\techo \" arg: $commaver\"\n\techo \"code: $(\n\t\tcat copyparty/__version__.py |\n\t\tgrep -E '^VERSION'\n\t)\"\n\techo\n\techo \"continue?\"\n\tread -u1\n}\n\nrm -rf .vscode\nrm -f \\\n  copyparty/web/deps/README.md \\\n  .gitattributes \\\n  .gitignore\n\ncp -pv LICENSE LICENSE.txt\nmv setup.py{,.disabled}\n\n# the regular cleanup memes\nfind -name '*.pyc' -delete\nfind -name __pycache__ -delete\nfind -type f \\( -name .DS_Store -or -name ._.DS_Store \\) -delete\nfind -type f -name ._\\* | while IFS= read -r f; do cmp <(printf '\\x00\\x05\\x16') <(head -c 3 -- \"$f\") && rm -f -- \"$f\"; done\n\n# also messy because osx support\nfind -type f -exec $md5sum '{}' \\+ |\nsed -r 's/(.{32})(.*)/\\2\\1/' | LC_COLLATE=c sort |\nsed -r 's/(.*)(.{32})/\\2\\1/' |\nsed -r 's/^(.{32}) \\./\\1  ./' > ../.sums.md5\nmv ../.sums.md5 .\n\ncd ..\npwd\necho \">>> tar\"; tar -czf \"$tgz_path\" --owner=1000 --group=1000 --numeric-owner \"copyparty-$ver\"\necho \">>> zip\"; zip  -qr \"$zip_path\" \"copyparty-$ver\"\n\nrm -rf \"$tmp\"\necho\necho \"done:\"\necho \"  $zip_path\"\necho \"  $tgz_path\"\necho\n\n# function alr() { ls -alR copyparty-$1 | sed -r \"s/copyparty-$1/copyparty/\" | sed -r 's/[A-Z][a-z]{2} [0-9 ]{2} [0-9]{2}:[0-9]{2}//' > $1; }; for x in hovudstraum rls src ; do alr $x; done\n\n"
  },
  {
    "path": "scripts/patches/pyftpdlib-fe80.patch",
    "content": "accept connections from IPv6 link-local addresses\n\ndiff -NarU1 a/pyftpdlib/handlers.py b/pyftpdlib2/handlers.py\n--- a/pyftpdlib/handlers.py\t2024-06-23 14:03:38\n+++ b/pyftpdlib/handlers.py\t2025-08-22 21:59:40\n@@ -451,3 +451,4 @@\n \n-        local_ip = self.cmd_channel.socket.getsockname()[0]\n+        sockname = list(self.cmd_channel.socket.getsockname())\n+        local_ip = sockname[0]\n         if local_ip in self.cmd_channel.masquerade_address_map:\n@@ -459,3 +460,5 @@\n \n-        if self.cmd_channel.server.socket.family != socket.AF_INET:\n+        if local_ip.startswith('fe') and local_ip[2:3] in \"89ab\":\n+            af = socket.AF_INET6  # link-local\n+        elif self.cmd_channel.server.socket.family != socket.AF_INET:\n             # dual stack IPv4/IPv6 support\n@@ -472,3 +475,4 @@\n             # free unprivileged random port.\n-            self.bind((local_ip, 0))\n+            sockname[1] = 0\n+            self.bind(tuple(sockname))\n         else:\n@@ -478,4 +482,5 @@\n                 self.set_reuse_addr()\n+                sockname[1] = port\n                 try:\n-                    self.bind((local_ip, port))\n+                    self.bind(tuple(sockname))\n                 except PermissionError:\n@@ -495,3 +500,4 @@\n                         else:\n-                            self.bind((local_ip, 0))\n+                            sockname[1] = 0\n+                            self.bind(tuple(sockname))\n                             self.cmd_channel.log(\n"
  },
  {
    "path": "scripts/patches/pyftpdlib-win313.patch",
    "content": "Date: Tue, 22 Oct 2024 12:47:30 +0200\nSubject: Workaround for isabs() on Windows + Python 3.13 (#652)\n\nStarting from Python 3.13, `os.path.isabs(\"/foo\")` on Windows return `False`\n\ndiff --git a/pyftpdlib/filesystems.py b/pyftpdlib/filesystems.py\nindex 9b9326bf..320ffe40 100644\n--- a/pyftpdlib/filesystems.py\n+++ b/pyftpdlib/filesystems.py\n@@ -132,6 +132,16 @@ def cwd(self, path):\n \n     # --- Pathname / conversion utilities\n \n+    @staticmethod\n+    def _isabs(path, _windows=os.name == \"nt\"):\n+        # Windows + Python 3.13: isabs() changed so that a path\n+        # starting with \"/\" is no longer considered absolute.\n+        # https://github.com/python/cpython/issues/44626\n+        # https://github.com/python/cpython/pull/113829/\n+        if _windows and path.startswith(\"/\"):\n+            return True\n+        return os.path.isabs(path)\n+\n     def ftpnorm(self, ftppath):\n         \"\"\"Normalize a \"virtual\" ftp pathname (typically the raw string\n         coming from client) depending on the current working directory.\n@@ -146,3 +156,3 @@\n         assert isinstance(ftppath, unicode), ftppath\n-        if os.path.isabs(ftppath):\n+        if self._isabs(ftppath):\n             p = os.path.normpath(ftppath)\n@@ -162,3 +172,3 @@\n         # This is for extra protection, maybe not really necessary.\n-        if not os.path.isabs(p):\n+        if not self._isabs(p):\n             p = u(\"/\")\n@@ -201,3 +211,3 @@\n         assert isinstance(fspath, unicode), fspath\n-        if os.path.isabs(fspath):\n+        if self._isabs(fspath):\n             p = os.path.normpath(fspath)\n"
  },
  {
    "path": "scripts/prep.sh",
    "content": "#!/bin/bash\nset -e\n\n# general housekeeping before a release\n\nself=$(cd -- \"$(dirname \"$BASH_SOURCE\")\"; pwd -P)\nver=$(awk '/^VERSION/{gsub(/[^0-9]/,\" \");printf \"%d.%d.%d\\n\",$1,$2,$3}' $self/../copyparty/__version__.py)\n\nupdate_arch_pkgbuild() {\n    cd \"$self/../contrib/package/arch\"\n    rm -rf x\n    mkdir x\n\n    sha=$(sha256sum \"$self/../dist/copyparty-$ver.tar.gz\" | awk '{print$1}')\n\n    awk -v ver=$ver -v sha=$sha '\n        /^pkgver=/{sub(/[0-9\\.]+/,ver)};\n        /^sha256sums=/{sub(/[0-9a-f]{64}/,sha)};\n        1' PKGBUILD >a\n    mv a PKGBUILD\n\n    rm -rf x\n}\n\nupdate_mpr_pkgbuild() {\n    cd \"$self/../contrib/package/makedeb-mpr\"\n    rm -rf x\n    mkdir x\n\n    sha=$(sha256sum \"$self/../dist/copyparty-$ver.tar.gz\" | awk '{print$1}')\n\n    awk -v ver=$ver -v sha=$sha '\n        /^pkgver=/{sub(/[0-9\\.]+/,ver)};\n        /^sha256sums=/{sub(/[0-9a-f]{64}/,sha)};\n        1' PKGBUILD >a\n    mv a PKGBUILD\n\n    rm -rf x\n}\n\nupdate_nixos_pin() {\n    ( cd $self/../contrib/package/nix/copyparty;\n      ./update.py $self/../dist/copyparty-$ver.tar.gz )\n}\n\nupdate_arch_pkgbuild\nupdate_mpr_pkgbuild\nupdate_nixos_pin\n"
  },
  {
    "path": "scripts/profile.py",
    "content": "#!/usr/bin/env python3\n\nimport sys\n\nsys.path.insert(0, \".\")\ncmd = sys.argv[1]\n\nif cmd == \"cpp\":\n    from copyparty.__main__ import main\n\n    argv = [\"__main__\", \"-vsrv::r:c,e2ds,e2ts\"]\n    main(argv=argv)\n\nelif cmd == \"test\":\n    from unittest import main\n\n    argv = [\"__main__\", \"discover\", \"-s\", \"tests\"]\n    main(module=None, argv=argv)\n\nelse:\n    raise Exception()\n\n# import dis; print(dis.dis(main))\n\n\n# macos:\n#   option1) python3.9 -m pip install --user -U vmprof==0.4.9\n#   option2) python3.9 -m pip install --user -U https://github.com/vmprof/vmprof-python/archive/refs/heads/master.zip\n#\n# python -m vmprof -o prof --lines ./scripts/profile.py test\n\n# linux: ~/.local/bin/vmprofshow prof tree | awk '$2>1{n=5} !n{next} 1;{n--} !n{print\"\"}'\n# macos: ~/Library/Python/3.9/bin/vmprofshow prof tree\n#   win: %appdata%\\..\\Roaming\\Python\\Python39\\Scripts\\vmprofshow.exe prof tree\n"
  },
  {
    "path": "scripts/py2/queue/__init__.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nfrom Queue import Queue, LifoQueue, PriorityQueue, Empty, Full\n"
  },
  {
    "path": "scripts/pyinstaller/README.md",
    "content": "builds copyparty32.exe, fully standalone, compatible with 32bit win7-sp1 and later\n\nrequires a win7 vm which has never been connected to the internet and a host-only network with the linux host at 192.168.123.1\n\ncopyparty.exe is built by a win10-ltsc-2021 vm with similar setup\n\nfirst-time setup steps in notes.txt\n\nrun build.sh in the vm to fetch src + compile + push a new exe to the linux host for manual publishing\n\n\n## ffmpeg\n\nbuilt with [ffmpeg-windows-build-helpers](https://github.com/rdp/ffmpeg-windows-build-helpers) and [this patch](./ffmpeg.patch) using [these steps](./ffmpeg.txt)\n"
  },
  {
    "path": "scripts/pyinstaller/build.sh",
    "content": "#!/bin/bash\nset -e\n\ncurl -k https://192.168.123.1:3923/cpp/scripts/pyinstaller/build.sh |\ntee build2.sh | cmp build.sh && rm build2.sh || {\n    [ -s build2.sh ] || exit 1\n    echo \"new build script; upgrade y/n:\"\n    while true; do read -u1 -n1 -r r; [[ $r =~ [yYnN] ]] && break; done\n    [[ $r =~ [yY] ]] && mv build{2,}.sh && exec ./build.sh\n}\n\nclean=--clean\n[ \"$1\" = f ] && clean= && shift\n\nuname -s | grep WOW64 && m=64 || m=32\nuname -s | grep NT-10 && w10=1 || w7=1\n[ $w7 ] && [ -e up2k.sh ] && [ ! \"$1\" ] && ./up2k.sh\n\n[ $w7 ] && pyv=37 || pyv=313\n[ $w7 ] && sfx=en || sfx=sfx\nesuf=\n[ $w7 ] && [ $m = 32 ] && esuf=32\n[ $w7 ] && [ $m = 64 ] && esuf=-winpe64\n\nappd=$(cygpath.exe \"$APPDATA\")\nspkgs=$appd/Python/Python$pyv/site-packages\n\ndl() { curl -fkLO \"$1\"; }\n\ncd ~/Downloads\n\ndl https://192.168.123.1:3923/cpp/dist/copyparty-sfx.py\ndl https://192.168.123.1:3923/cpp/scripts/pyinstaller/loader.ico\ndl https://192.168.123.1:3923/cpp/scripts/pyinstaller/loader.py\ndl https://192.168.123.1:3923/cpp/scripts/pyinstaller/loader.rc\n\n[ $sfx = en ] && {\n    dl https://192.168.123.1:3923/cpp/dist/copyparty-en.py\n\n    st_en=$(cat copyparty-en.py | awk '/^STAMP = [0-9]+/{print$3;exit}') 2>/dev/null\n    st_sfx=$(cat copyparty-sfx.py | awk '/^STAMP = [0-9]+/{print$3;exit}') 2>/dev/null\n    [ $st_en ] && [ $st_en -ge $st_sfx ] || sfx=sfx\n}\n\nrm -rf $TEMP/pe-copyparty*\npython copyparty-$sfx.py --version\n\nrm -rf mods; mkdir mods\ncp -pR $TEMP/pe-copyparty/{copyparty,partftpy}/ $TEMP/pe-copyparty/{ftp,j2}/* mods/\n[ $w10 ] && rm -rf mods/{jinja2,markupsafe}\n\naf() { awk \"$1\" <$2 >tf; mv tf \"$2\"; }\n\nrm -rf mods/magic/\n\n[ $w7 ] && {\n    sed -ri /pickle/d mods/jinja2/_compat.py\n    sed -ri '/(bccache|PackageLoader)/d' mods/jinja2/__init__.py\n    af '/^class/{s=0}/^class PackageLoader/{s=1}!s' mods/jinja2/loaders.py\n    sed -ri 's/from url.*Request, urlopen.*/pass/' mods/copyparty/svchub.py\n    sed -ri 's/(.*\"--vc-.*, help=).*/\\1argparse.SUPPRESS)/' mods/copyparty/__main__.py\n}\n[ $w10 ] && {\n    sed -ri '/(bccache|PackageLoader)/d' $spkgs/jinja2/__init__.py\n    for f in nodes async_utils; do\n        sed -ri 's/\\binspect\\b/os/' $spkgs/jinja2/$f.py\n    done\n}\n\nsed -ri /fork_process/d mods/pyftpdlib/servers.py\naf '/^class _Base/{s=1}!s' mods/pyftpdlib/authorizers.py\n\nread a b c d _ < <(\n    grep -E '^VERSION =' mods/copyparty/__version__.py |\n    tail -n 1 |\n    sed -r 's/[^0-9]+//;s/[\" )]//g;s/[-,]/ /g;s/$/ 0/'\n)\nsed -r 's/1,2,3,0/'$a,$b,$c,$d'/;s/1\\.2\\.3/'$a.$b.$c/ <loader.rc >loader.rc2\nsed -ri s/copyparty.exe/copyparty$esuf.exe/ loader.rc2\n\nexcl=(\n    asyncio\n    copyparty.broker_mp\n    copyparty.broker_mpw\n    copyparty.smbd\n    ctypes.macholib\n    curses\n    email._header_value_parser\n    importlib.resources\n    importlib_resources\n    multiprocessing\n    packaging\n    pdb\n    pickle\n    pkg_resources\n    PIL.EpsImagePlugin\n    pyftpdlib.prefork\n    urllib.robotparser\n)\n[ $w10 ] && excl+=(\n    _pyrepl\n    distutils\n    setuptools\n    PIL._avif\n    PIL.ImageQt\n    PIL.ImageShow\n    PIL.ImageTk\n    PIL.ImageWin\n    PIL.PdfParser\n    zipimport\n) || excl+=(\n    email.header\n    email.parser\n    inspect\n    PIL\n    PIL.ExifTags\n    PIL.Image\n    PIL.ImageDraw\n    PIL.ImageOps\n    urllib.request\n    urllib.response\n    zipfile\n)\nexcl=( \"${excl[@]/#/--exclude-module }\" )\n\n$APPDATA/python/python$pyv/scripts/pyinstaller \\\n    -y $clean -p mods --upx-dir=. \\\n    ${excl[*]} \\\n    --version-file loader.rc2 -i loader.ico -n copyparty -c -F loader.py \\\n    --add-data 'mods/copyparty/res;copyparty/res' \\\n    --add-data 'mods/copyparty/web;copyparty/web'\n\n# ./upx.exe --best --ultra-brute --lzma -k dist/copyparty.exe\n\nprintf $(sha512sum ~/Downloads/dist/copyparty.exe | head -c 18 | sed -r 's/(..)/\\\\x\\1/g') |\nbase64 | head -c12 >> dist/copyparty.exe\n\ndist/copyparty.exe --version\n\ncsum=$(sha512sum <dist/copyparty.exe | cut -c-56)\n\ncurl -fkT dist/copyparty.exe -HPW:wark https://192.168.123.1:3923/copyparty$esuf.exe >uplod.log\ncat uplod.log\n\ngrep -q $csum uplod.log && echo upload OK || {\n    echo UPLOAD FAILED\n    exit 1\n}\n\necho; read -u1 -n1 -p 'shutdown? y/n: '\n[ \"$REPLY\" = y ] && shutdown -s -t 1\n"
  },
  {
    "path": "scripts/pyinstaller/depchk.sh",
    "content": "#!/bin/bash\nset -e\ne=0\n\ncd ~/dev/pyi\n\nckpypi() {\n\tdeps=(\n\t\taltgraph\n\t\tpefile\n\t\tpyinstaller\n\t\tpyinstaller-hooks-contrib\n\t\tpywin32-ctypes\n\t\tJinja2\n\t\tMarkupSafe\n\t\tmutagen\n\t\tPillow\n\t)\n\tfor dep in \"${deps[@]}\"; do\n\t\tk=\n\t\techo -n .\n\t\tcurl -s https://pypi.org/pypi/$dep/json >h\n\t\tver=$(jq <h -r '.releases|keys|.[]' | sort -V | tail -n 1)\n\t\twhile IFS= read -r fn; do\n\t\t\t[ -e \"$fn\" ] && k=\"$fn\" && break\n\t\tdone < <(\n\t\t\tjq -r '.releases[\"'\"$ver\"'\"]|.[]|.filename' <h\n\t\t)\n\t\t[ -z \"$k\" ] && echo \"outdated: $dep\" && cp h \"ng-$dep\" && e=1\n\tdone\n\ttrue\n}\n\nckgh() {\n\tdeps=(\n\t\tupx/upx\n\t)\n\tfor dep in \"${deps[@]}\"; do\n\t\tk=\n\t\techo -n .\n\t\twhile IFS= read -r fn; do\n\t\t\t[ -e \"$fn\" ] && k=\"$fn\" && break\n\t\tdone < <(\n\t\t\tcurl -s https://api.github.com/repos/$dep/releases | tee h |\n\t\t\tjq -r 'first|.assets|.[]|.name'\n\t\t)\n\t\t[ -z \"$k\" ] && echo \"outdated: $dep\" && cp h \"ng-$dep\" && e=1\n\tdone\n\ttrue\n}\n\nckpypi\nckgh\n\nrm h\nexit $e\n"
  },
  {
    "path": "scripts/pyinstaller/deps.sha512",
    "content": "f117016b1e6a7d7e745db30d3e67f1acf7957c443a0dd301b6c5e10b8368f2aa4db6be9782d2d3f84beadd139bfeef4982e40f21ca5d9065cb794eeb0e473e82  altgraph-0.17.4-py2.py3-none-any.whl\n17ce52ba50692a9d964f57a23ac163fb74c77fdeb2ca988a6d439ae1fe91955ff43730c073af97a7b3223093ffea3479a996b9b50ee7fba0869247a56f74baa6  pefile-2023.2.7-py3-none-any.whl\nb297ff66ec50cf5a1abcf07d6ac949644c5150ba094ffac974c5d27c81574c3e97ed814a47547f4b03a4c83ea0fb8f026433fca06a3f08e32742dc5c024f3d07  pywin32_ctypes-0.2.3-py3-none-any.whl\nfd88d00c7231f9fcbe0fb9037217b6ae5dcb5b06a98be56243400542671a32f003d594b2e1852f77d316e9327ad4c643321e1f9f3ea20cc8500ac5bba8ae170e  upx-5.0.2-win32.zip\n# win7\nd9d30ff960365307833425e340bf0a642fd54e35155f18ff1d9746bc1ecf8ecb864aad24948735e8fd994503b659dc8968e6ba337eeb4568217ebdf97dd292dd  Git-2.46.2-32-bit.exe\n3253e86471e6f9fa85bfdb7684cd2f964ed6e35c6a4db87f81cca157c049bef43e66dfcae1e037b2fb904567b1e028aaeefe8983ba3255105df787406d2aa71e  en_windows_7_professional_with_sp1_x86_dvd_u_677056.iso\nab0db0283f61a5bbe44797d74546786bf41685175764a448d2e3bd629f292f1e7d829757b26be346b5044d78c9c1891736d93237cee4b1b6f5996a902c86d15f  en_windows_7_professional_with_sp1_x64_dvd_u_676939.iso\nd130bfa136bd171b9972b5c281c578545f2a84a909fdf18a6d2d71dd12fb3d512a7a1fa5cf7300433adece1d306eb2f22d7278f4c90e744e04dc67ba627a82c0  future-1.0.0-py3-none-any.whl\n0b4d07434bf8d314f42893d90bce005545b44a509e7353a73cad26dc9360b44e2824218a1a74f8174d02eba87fba91baffa82c8901279a32ebc6b8386b1b4275  importlib_metadata-6.7.0-py3-none-any.whl\n085d39ef4426aa5f097fbc484595becc16e61ca23fc7da4d2a8bba540a3b82e789e390b176c7151bdc67d01735cce22b1562cdb2e31273225a2d3e275851a4ad  setuptools-70.3.0-py3-none-any.whl\n9d2c31701a4d3fef553928c00528a48f9e1854ab5333528b50e358a214eba90029d687f039bcda5760b6fdf9f2de3bcf3784ae21a6374cf2a97a845d33b636c6  packaging-24.0-py3-none-any.whl\n5d7462a584105bccaa9cf376f5a8c5827ead099c813c8af7392d478a4398f373d9e8cac7bbad2db51b335411ab966b21e119b1b1234c9a7ab70c6ddfc9306da6  pip-24.0-py3-none-any.whl\nf298e34356b5590dde7477d7b3a88ad39c622a2bcf3fcd7c53870ce8384dd510f690af81b8f42e121a22d3968a767d2e07595036b2ed7049c8ef4d112bcf3a61  pyinstaller-5.13.2-py3-none-win32.whl\nea73aa54cc6d5db20dfb127e54562dabf890e4cd6171a91b10a51af2bcfc76e1d64cbdce4546df2dcfe42b624724c85b1cd05934be2413425b1f880222727b4f  pyinstaller-5.13.2-py3-none-win_amd64.whl\n2f4e3927a38cf7757bc9a1c06370d79209669a285a80f1b09cf9917137825c7022a50a56b351807e6e687e2c3a7bd7b2c5cc6daeb4d90e11920284c1a04a1cc3  pyinstaller_hooks_contrib-2023.8-py2.py3-none-any.whl\n6bb73cc2db795c59c92f2115727f5c173cacc9465af7710db9ff2f2aec2d73130d0992d0f16dcb3fac222dc15c0916562d0813b2337401022020673a4461df3d  python-3.7.9-amd64.exe\n500747651c87f59f2436c5ab91207b5b657856e43d10083f3ce27efb196a2580fadd199a4209519b409920c562aaaa7dcbdfb83ed2072a43eaccae6e2d056f31  python-3.7.9.exe\n03e50aecc85914567c114e38a1777e32628ee098756f37177bc23220eab33ac7d3ff591fd162db3b4d4e34d55cee93ef0dc67af68a69c38bb1435e0768dee57e  typing_extensions-4.7.1-py3-none-any.whl\n68e1b618d988be56aaae4e2eb92bc0093627a00441c1074ebe680c41aa98a6161e52733ad0c59888c643a33fe56884e4f935178b2557fbbdd105e92e0d993df6  windows6.1-kb2533623-x64.msu\n479a63e14586ab2f2228208116fc149ed8ee7b1e4ff360754f5bda4bf765c61af2e04b5ef123976623d04df4976b7886e0445647269da81436bd0a7b5671d361  windows6.1-kb2533623-x86.msu\nac96786e5d35882e0c5b724794329c9125c2b86ae7847f17acfc49f0d294312c6afc1c3f248655de3f0ccb4ca426d7957d02ba702f4a15e9fcd7e2c314e72c19  zipp-3.15.0-py3-none-any.whl\n# win10\nf4b4e330995ebe96c0bd06e16e5b26062ece9473f06d369775aa68eab261dedcf32dfdd159acaa227502bbf9fa2cd8bbe57cddb89fc6f2196fef7a9ed5a80ae9  Git-2.51.0-64-bit.exe\n0a2cd4cadf0395f0374974cd2bc2407e5cc65c111275acdffb6ecc5a2026eee9e1bb3da528b35c7f0ff4b64563a74857d5c2149051e281cc09ebd0d1968be9aa  en-us_windows_10_enterprise_ltsc_2021_x64_dvd_d289cf96.iso\n16cc0c58b5df6c7040893089f3eb29c074aed61d76dae6cd628d8a89a05f6223ac5d7f3f709a12417c147594a87a94cc808d1e04a6f1e407cc41f7c9f47790d1  virtio-win-0.1.285.iso\n9a7f40edc6f9209a2acd23793f3cbd6213c94f36064048cb8bf6eb04f1bdb2c2fe991cb09f77fe8b13e5cd85c618ef23573e79813b2fef899ab2f290cd129779  jinja2-3.1.6-py3-none-any.whl\n00731cfdd9d5c12efef04a7161c90c1e5ed1dc4677aa88a1d4054aff836f3430df4da5262ed4289c21637358a9e10e5df16f76743cbf5a29bb3a44b146c19cf3  MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl\n8a6e2b13a2ec4ef914a5d62aad3db6464d45e525a82e07f6051ed10474eae959069e165dba011aefb8207cdfd55391d73d6f06362c7eb247b08763106709526e  mutagen-1.47.0-py3-none-any.whl\na726fb46cce24f781fc8b55a3e6dea0a884ebc3b2b400ea74aa02333699f4955a5dc1e2ec5927ac72f35a624401f3f3b442882ba1cc4cadaf9c88558b5b8bdae  packaging-25.0-py3-none-any.whl\nefc712162da7fb005c8869a7612d2f4983d2d073ec79e16a58e7bf1fcd01c88b1cc26656f0893c68edd2294be7c3990db2f6bd77e7e3f2613539d57994b6a033  pillow-12.1.1-cp313-cp313-win_amd64.whl\nb9b98714dfca6fa80b0b3f222965724d63be9c54d19435d1fe768e07016913d6db8d6e043fcb185b55a9bd6fe370a80cf961814fc096046a5f4640d99ed575ef  pyinstaller-6.15.0-py3-none-win_amd64.whl\ncad0f7cf39de691813b1d4abc7d33f8bda99a87d9c5886039b814752e8690364150da26fb61b3e28d5698ff57a90e6dcd619ed2b64b04f72b5aadb75e201bdb0  pyinstaller_hooks_contrib-2025.8-py3-none-any.whl\n50dba4a63957220247be2985bd4ed6928679d9f6dc8cb7cee36394dda4e69cdee910fb39b01965d1358133855ace535eb1e08774fed7090feb8618dfc5fd2441  python-3.13.12-amd64.exe\n2a0420f7faaa33d2132b82895a8282688030e939db0225ad8abb95a47bdb87b45318f10985fc3cee271a9121441c1526caa363d7f2e4a4b18b1a674068766e87  setuptools-80.9.0-py3-none-any.whl\n"
  },
  {
    "path": "scripts/pyinstaller/deps.txt",
    "content": "# links to download pages for each dep\nhttps://pypi.org/project/altgraph/#files\nhttps://pypi.org/project/pefile/#files\nhttps://pypi.org/project/pyinstaller/#files\nhttps://pypi.org/project/pyinstaller-hooks-contrib/#files\nhttps://pypi.org/project/pywin32-ctypes/#files\nhttps://github.com/git-for-windows/git/releases/latest\nhttps://github.com/upx/upx/releases/latest\n\n# win10 additionals\nhttps://pypi.org/project/Jinja2/#files\nhttps://pypi.org/project/MarkupSafe/#files\nhttps://pypi.org/project/mutagen/#files\nhttps://pypi.org/project/Pillow/#files\n\n# win7 additionals\nhttps://pypi.org/project/future/#files\nhttps://pypi.org/project/importlib-metadata/#files\nhttps://pypi.org/project/pip/#files\nhttps://pypi.org/project/typing-extensions/#files\nhttps://pypi.org/project/zipp/#files\nhttps://support.microsoft.com/en-us/topic/microsoft-security-advisory-insecure-library-loading-could-allow-remote-code-execution-486ea436-2d47-27e5-6cb9-26ab7230c704\n  http://www.microsoft.com/download/details.aspx?familyid=c79c41b0-fbfb-4d61-b5d8-cadbe184b9fc\n  http://www.microsoft.com/download/details.aspx?familyid=146ed6f7-b605-4270-8ec4-b9f0f284bb9e\n    https://www.microsoft.com/en-us/download/details.aspx?id=26767\n    https://www.microsoft.com/en-us/download/details.aspx?id=26764\n      see web.archive.org links below\n\n# direct links to version-frozen deps\nhttps://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.285-1/virtio-win-0.1.285.iso\nhttps://www.python.org/ftp/python/3.7.9/python-3.7.9-amd64.exe\nhttps://www.python.org/ftp/python/3.7.9/python-3.7.9.exe\nhttps://web.archive.org/web/20200412130846if_/https://download.microsoft.com/download/2/D/7/2D78D0DD-2802-41F5-88D6-DC1D559F206D/Windows6.1-KB2533623-x86.msu\nhttps://web.archive.org/web/20200203192654if_/https://download.microsoft.com/download/f/1/0/f106e158-89a1-41e3-a9b5-32feb2a99a0b/windows6.1-kb2533623-x64.msu\n"
  },
  {
    "path": "scripts/pyinstaller/ffmpeg.patch",
    "content": "diff --git a/cross_compile_ffmpeg.sh b/cross_compile_ffmpeg.sh\nindex 45c4ef8..f9bc83a 100755\n--- a/cross_compile_ffmpeg.sh\n+++ b/cross_compile_ffmpeg.sh\n@@ -2287,15 +2287,8 @@ build_ffmpeg() {\n   else\n     local output_dir=$3\n   fi\n-  if [[ \"$non_free\" = \"y\" ]]; then\n-    output_dir+=\"_with_fdk_aac\"\n-  fi\n-  if [[ $build_intel_qsv == \"n\" ]]; then\n-    output_dir+=\"_xp_compat\"\n-  fi\n-  if [[ $enable_gpl == 'n' ]]; then\n-    output_dir+=\"_lgpl\"\n-  fi\n+  output_dir+=\"_xp_compat\"\n+  output_dir+=\"_lgpl\"\n \n   if [[ ! -z $ffmpeg_git_checkout_version ]]; then\n     local output_branch_sanitized=$(echo ${ffmpeg_git_checkout_version} | sed \"s/\\//_/g\") # release/4.3 to release_4.3\n@@ -2354,9 +2347,9 @@ build_ffmpeg() {\n       init_options+=\" --disable-schannel\"\n       # Fix WinXP incompatibility by disabling Microsoft's Secure Channel, because Windows XP doesn't support TLS 1.1 and 1.2, but with GnuTLS or OpenSSL it does.  XP compat!\n     fi\n-    config_options=\"$init_options --enable-libcaca --enable-gray --enable-libtesseract --enable-fontconfig --enable-gmp --enable-gnutls --enable-libass --enable-libbluray --enable-libbs2b --enable-libflite --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libilbc --enable-libmodplug --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopus --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libtheora --enable-libtwolame --enable-libvo-amrwbenc --enable-libvorbis --enable-libwebp --enable-libzimg --enable-libzvbi --enable-libmysofa --enable-libopenjpeg  --enable-libopenh264  --enable-libvmaf --enable-libsrt --enable-libxml2 --enable-opengl --enable-libdav1d --enable-cuda-llvm\"\n+    config_options=\"$init_options --enable-gray --enable-libopus --enable-libvorbis --enable-libwebp --enable-libopenjpeg\"\n \n-    if [[ $build_svt = y ]]; then\n+    if [[ '' ]]; then\n       if [ \"$bits_target\" != \"32\" ]; then\n \n         # SVT-VP9 see comments below\n@@ -2379,40 +2372,13 @@ build_ffmpeg() {\n         config_options+=\" --enable-libvpx\"\n       fi # else doesn't work/matter with 32 bit\n     fi\n-    config_options+=\" --enable-libaom\"\n-\n-    if [[ $compiler_flavors != \"native\" ]]; then\n-      config_options+=\" --enable-nvenc --enable-nvdec\" # don't work OS X\n-    fi\n \n-    config_options+=\" --extra-libs=-lharfbuzz\" #  grr...needed for pre x264 build???\n     config_options+=\" --extra-libs=-lm\" # libflite seemed to need this linux native...and have no .pc file huh?\n     config_options+=\" --extra-libs=-lshlwapi\" # lame needed this, no .pc file?\n-    config_options+=\" --extra-libs=-lmpg123\" # ditto\n     config_options+=\" --extra-libs=-lpthread\" # for some reason various and sundry needed this linux native\n \n-    config_options+=\" --extra-cflags=-DLIBTWOLAME_STATIC --extra-cflags=-DMODPLUG_STATIC --extra-cflags=-DCACA_STATIC\" # if we ever do a git pull then it nukes changes, which overrides manual changes to configure, so just use these for now :|\n-    if [[ $build_amd_amf = n ]]; then\n-      config_options+=\" --disable-amf\" # Since its autodetected we have to disable it if we do not want it. #unless we define no autodetection but.. we don't.\n-    else\n-      config_options+=\" --enable-amf\" # This is actually autodetected but for consistency.. we might as well set it.\n-    fi\n-\n-    if [[ $build_intel_qsv = y && $compiler_flavors != \"native\" ]]; then # Broken for native builds right now: https://github.com/lu-zero/mfx_dispatch/issues/71\n-      config_options+=\" --enable-libmfx\"\n-    else\n-      config_options+=\" --disable-libmfx\"\n-    fi\n-    if [[ $enable_gpl == 'y' ]]; then\n-      config_options+=\" --enable-gpl --enable-frei0r --enable-librubberband --enable-libvidstab --enable-libx264 --enable-libx265 --enable-avisynth --enable-libaribb24\"\n-      config_options+=\" --enable-libxvid --enable-libdavs2\"\n-      if [[ $host_target != 'i686-w64-mingw32' ]]; then\n-        config_options+=\" --enable-libxavs2\"\n-      fi\n-      if [[ $compiler_flavors != \"native\" ]]; then\n-        config_options+=\" --enable-libxavs\" # don't compile OS X\n-      fi\n-    fi\n+    config_options+=\" --disable-amf\" # Since its autodetected we have to disable it if we do not want it. #unless we define no autodetection but.. we don't.\n+    config_options+=\" --disable-libmfx\"\n     local licensed_gpl=n # lgpl build with libx264 included for those with \"commercial\" license :)\n     if [[ $licensed_gpl == 'y' ]]; then\n       apply_patch file://$patch_dir/x264_non_gpl.diff -p1\n@@ -2427,7 +2393,7 @@ build_ffmpeg() {\n \n     config_options+=\" $postpend_configure_opts\"\n \n-    if [[ \"$non_free\" = \"y\" ]]; then\n+    if [[ '' ]]; then\n       config_options+=\" --enable-nonfree --enable-libfdk-aac\"\n \n       if [[ $compiler_flavors != \"native\" ]]; then\n@@ -2436,6 +2402,17 @@ build_ffmpeg() {\n       # other possible options: --enable-openssl [unneeded since we already use gnutls]\n     fi\n \n+    config_options+=\" --disable-indevs --disable-outdevs --disable-protocols --disable-hwaccels --disable-schannel --disable-mediafoundation\"  # 8032256\n+    config_options+=\" --disable-muxers --enable-muxer=image2 --enable-muxer=mjpeg --enable-muxer=opus --enable-muxer=webp\"  # 7927296\n+    config_options+=\" --disable-encoders --enable-encoder=libopus --enable-encoder=libopenjpeg --enable-encoder=libwebp --enable-encoder=ljpeg --enable-encoder=png\"  # 6776320\n+    config_options+=\" --enable-small\"  # 5409792\n+    #config_options+=\" --disable-runtime-cpudetect\"  # 5416448\n+    config_options+=\" --disable-bsfs --disable-filters --enable-filter=scale --enable-filter=compand --enable-filter=volume --enable-filter=showwavespic --enable-filter=convolution --enable-filter=aresample --enable-filter=showspectrumpic --enable-filter=crop\"  # 4647424\n+    config_options+=\" --disable-network\"  # 4585984\n+    #config_options+=\" --disable-pthreads --disable-w32threads\"  # kills ffmpeg\n+    config_options+=\" --enable-protocol=cache --enable-protocol=file --enable-protocol=pipe\"\n+\n+\n     do_debug_build=n # if you need one for backtraces/examining segfaults using gdb.exe ... change this to y :) XXXX make it affect x264 too...and make it real param :)\n     if [[ \"$do_debug_build\" = \"y\" ]]; then\n       # not sure how many of these are actually needed/useful...possibly none LOL\n@@ -2561,36 +2538,16 @@ build_ffmpeg_dependencies() {\n   build_meson_cross\n   build_mingw_std_threads\n   build_zlib # Zlib in FFmpeg is autodetected.\n-  build_libcaca # Uses zlib and dlfcn (on windows).\n   build_bzip2 # Bzlib (bzip2) in FFmpeg is autodetected.\n   build_liblzma # Lzma in FFmpeg is autodetected. Uses dlfcn.\n   build_iconv # Iconv in FFmpeg is autodetected. Uses dlfcn.\n-  build_sdl2 # Sdl2 in FFmpeg is autodetected. Needed to build FFPlay. Uses iconv and dlfcn.\n-  if [[ $build_amd_amf = y ]]; then\n-    build_amd_amf_headers\n-  fi\n-  if [[ $build_intel_qsv = y && $compiler_flavors != \"native\" ]]; then # Broken for native builds right now: https://github.com/lu-zero/mfx_dispatch/issues/71\n-    build_intel_quicksync_mfx\n-  fi\n-  build_nv_headers\n   build_libzimg # Uses dlfcn.\n   build_libopenjpeg\n-  build_glew\n-  build_glfw\n   #build_libjpeg_turbo # mplayer can use this, VLC qt might need it? [replaces libjpeg] (ffmpeg seems to not need it so commented out here)\n   build_libpng # Needs zlib >= 1.0.4. Uses dlfcn.\n   build_libwebp # Uses dlfcn.\n-  build_harfbuzz\n   # harf does now include build_freetype # Uses zlib, bzip2, and libpng.\n-  build_libxml2 # Uses zlib, liblzma, iconv and dlfcn.\n-  build_libvmaf\n-  build_fontconfig # Needs freetype and libxml >= 2.6. Uses iconv and dlfcn.\n-  build_gmp # For rtmp support configure FFmpeg with '--enable-gmp'. Uses dlfcn.\n   #build_librtmfp # mainline ffmpeg doesn't use it yet\n-  build_libnettle # Needs gmp >= 3.0. Uses dlfcn.\n-  build_unistring\n-  build_libidn2 # needs iconv and unistring\n-  build_gnutls # Needs nettle >= 3.1, hogweed (nettle) >= 3.1. Uses libidn2, unistring, zlib, and dlfcn.\n   #if [[ \"$non_free\" = \"y\" ]]; then\n   #  build_openssl-1.0.2 # Nonfree alternative to GnuTLS. 'build_openssl-1.0.2 \"dllonly\"' to build shared libraries only.\n   #  build_openssl-1.1.1 # Nonfree alternative to GnuTLS. Can't be used with LibRTMP. 'build_openssl-1.1.1 \"dllonly\"' to build shared libraries only.\n@@ -2598,86 +2555,13 @@ build_ffmpeg_dependencies() {\n   build_libogg # Uses dlfcn.\n   build_libvorbis # Needs libogg >= 1.0. Uses dlfcn.\n   build_libopus # Uses dlfcn.\n-  build_libspeexdsp # Needs libogg for examples. Uses dlfcn.\n-  build_libspeex # Uses libspeexdsp and dlfcn.\n-  build_libtheora # Needs libogg >= 1.1. Needs libvorbis >= 1.0.1, sdl and libpng for test, programs and examples [disabled]. Uses dlfcn.\n-  build_libsndfile \"install-libgsm\" # Needs libogg >= 1.1.3 and libvorbis >= 1.2.3 for external support [disabled]. Uses dlfcn. 'build_libsndfile \"install-libgsm\"' to install the included LibGSM 6.10.\n-  build_mpg123\n-  build_lame # Uses dlfcn, mpg123\n-  build_twolame # Uses libsndfile >= 1.0.0 and dlfcn.\n-  build_libopencore # Uses dlfcn.\n-  build_libilbc # Uses dlfcn.\n-  build_libmodplug # Uses dlfcn.\n-  build_libgme\n-  build_libbluray # Needs libxml >= 2.6, freetype, fontconfig. Uses dlfcn.\n-  build_libbs2b # Needs libsndfile. Uses dlfcn.\n-  build_libsoxr\n-  build_libflite\n-  build_libsnappy # Uses zlib (only for unittests [disabled]) and dlfcn.\n-  build_vamp_plugin # Needs libsndfile for 'vamp-simple-host.exe' [disabled].\n   build_fftw # Uses dlfcn.\n-  build_libsamplerate # Needs libsndfile >= 1.0.6 and fftw >= 0.15.0 for tests. Uses dlfcn.\n-  build_librubberband # Needs libsamplerate, libsndfile, fftw and vamp_plugin. 'configure' will fail otherwise. Eventhough librubberband doesn't necessarily need them (libsndfile only for 'rubberband.exe' and vamp_plugin only for \"Vamp audio analysis plugin\"). How to use the bundled libraries '-DUSE_SPEEX' and '-DUSE_KISSFFT'?\n-  build_frei0r # Needs dlfcn. could use opencv...\n-  if [[ \"$bits_target\" != \"32\" && $build_svt = \"y\" ]]; then\n-    build_svt-hevc\n-    build_svt-av1\n-    build_svt-vp9\n-  fi\n-  build_vidstab\n-  #build_facebooktransform360 # needs modified ffmpeg to use it so not typically useful\n-  build_libmysofa # Needed for FFmpeg's SOFAlizer filter (https://ffmpeg.org/ffmpeg-filters.html#sofalizer). Uses dlfcn.\n-  if [[ \"$non_free\" = \"y\" ]]; then\n-    build_fdk-aac # Uses dlfcn.\n-    if [[ $compiler_flavors != \"native\" ]]; then\n-      build_libdecklink # Error finding rpc.h in native builds even if it's available\n-    fi\n-  fi\n-  build_zvbi # Uses iconv, libpng and dlfcn.\n-  build_fribidi # Uses dlfcn.\n-  build_libass # Needs freetype >= 9.10.3 (see https://bugs.launchpad.net/ubuntu/+source/freetype1/+bug/78573 o_O) and fribidi >= 0.19.0. Uses fontconfig >= 2.10.92, iconv and dlfcn.\n-\n-  build_libxvid # FFmpeg now has native support, but libxvid still provides a better image.\n-  build_libsrt # requires gnutls, mingw-std-threads\n-  build_libaribb24\n-  build_libtesseract\n-  build_lensfun  # requires png, zlib, iconv\n-  # build_libtensorflow # broken\n-  build_libvpx\n-  build_libx265\n-  build_libopenh264\n-  build_libaom\n-  build_dav1d\n-  build_avisynth\n-  build_libx264 # at bottom as it might internally build a copy of ffmpeg (which needs all the above deps...\n  }\n \n build_apps() {\n-  if [[ $build_dvbtee = \"y\" ]]; then\n-    build_dvbtee_app\n-  fi\n-  # now the things that use the dependencies...\n-  if [[ $build_libmxf = \"y\" ]]; then\n-    build_libMXF\n-  fi\n-  if [[ $build_mp4box = \"y\" ]]; then\n-    build_mp4box\n-  fi\n-  if [[ $build_mplayer = \"y\" ]]; then\n-    build_mplayer\n-  fi\n   if [[ $build_ffmpeg_static = \"y\" ]]; then\n     build_ffmpeg static\n   fi\n-  if [[ $build_ffmpeg_shared = \"y\" ]]; then\n-    build_ffmpeg shared\n-  fi\n-  if [[ $build_vlc = \"y\" ]]; then\n-    build_vlc\n-  fi\n-  if [[ $build_lsw = \"y\" ]]; then\n-    build_lsw\n-  fi\n }\n \n # set some parameters initial values\n"
  },
  {
    "path": "scripts/pyinstaller/ffmpeg.txt",
    "content": "apt install subversion ragel curl texinfo ed bison flex cvs yasm automake libtool cmake git make pkg-config pax nasm gperf autogen bzip2 autoconf-archive p7zip-full meson clang libtool-bin ed python-is-python3\n\ngit clone https://github.com/rdp/ffmpeg-windows-build-helpers\n# commit 3d88e2b6aedfbb5b8fed19dd24621e5dd7fc5519 (HEAD -> master, origin/master, origin/HEAD)\n# Merge: b0bd70c 9905dd7\n# Author: Roger Pack <rogerpack2005@gmail.com>\n# Date:   Fri Aug 19 23:36:35 2022 -0600\n\ncd ffmpeg-windows-build-helpers/\nvim cross_compile_ffmpeg.sh\n(cd ./sandbox/win32/ffmpeg_git_xp_compat_lgpl/ ; git reset --hard ; git clean -fx )\n./cross_compile_ffmpeg.sh\nfor f in sandbox/win32/ffmpeg_git_xp_compat_lgpl/ff{mpeg,probe}.exe; do upx --best --ultra-brute -k $f; mv $f ~/dev; done \n"
  },
  {
    "path": "scripts/pyinstaller/icon.sh",
    "content": "#!/bin/bash\nset -e\n\ngenico() {\n\n# imagemagick png compression is broken, use pillow instead\nconvert $1 a.bmp\n\n#convert a.bmp -trim -resize '48x48!' -strip a.png\npython3 <<'EOF'\nfrom PIL import Image\ni = Image.open('a.bmp')\ni = i.crop(i.getbbox())\ni = i.resize((48,48), Image.BICUBIC)\ni = Image.alpha_composite(i,i)\ni.save('a.png')\nEOF\n\npngquant --strip --quality 30 a.png\nmv a-*.png a.png\n\npython3 <<EOF\nfrom PIL import Image\nImage.open('a.png').save('$2',sizes=[(48,48)])\nEOF\n\nrm a.{bmp,png}\n}\n\n\ngenico ~/AndroidStudioProjects/PartyUP/metadata/en-US/images/icon.png loader.ico\ngenico https://raw.githubusercontent.com/googlefonts/noto-emoji/main/png/512/emoji_u1f680.png up2k.ico\nls -al\n"
  },
  {
    "path": "scripts/pyinstaller/loader.py",
    "content": "# coding: utf-8\n\nimport base64\nimport hashlib\nimport os\nimport re\nimport shutil\nimport subprocess as sp\nimport sys\nimport traceback\n\nv = r\"\"\"\n\nthis 32-bit copyparty.exe is compatible with Windows7-SP1 and later.\nTo make this possible, the EXE was compiled with Python 3.7.9,\nwhich is EOL and does not receive security patches anymore.\n\nif possible, for performance and security reasons, please use this instead:\nhttps://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py\n\"\"\"\n\nif sys.version_info > (3, 10):\n    v = r\"\"\"\n\nthis 64-bit copyparty.exe is compatible with Windows 8 and later.\nNo security issues were known to affect this EXE at build time,\nhowever that may have changed since then.\n\nif possible, for performance and security reasons, please use this instead:\nhttps://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py\n\"\"\"\n\nif sys.maxsize > 2 ** 32:\n    v = v.replace(\"32-bit\", \"64-bit\")\n\ntry:\n    print(v.replace(\"\\n\", \"\\n▒▌ \")[1:] + \"\\n\")\nexcept:\n    print(v.replace(\"\\n\", \"\\n|| \")[1:] + \"\\n\")\n\n\ndef confirm(rv):\n    print()\n    print(\"retcode\", rv if rv else traceback.format_exc())\n    print(\"*** hit enter to exit ***\")\n    try:\n        input()\n    except:\n        pass\n\n    sys.exit(rv or 1)\n\n\ndef ckck():\n    hs = hashlib.sha512()\n    with open(sys.executable, \"rb\") as f:\n        f.seek(-12, 2)\n        rem = f.tell()\n        esum = f.read().decode(\"ascii\", \"replace\")\n        f.seek(0)\n        while rem:\n            buf = f.read(min(rem, 64 * 1024))\n            rem -= len(buf)\n            hs.update(buf)\n            if not buf:\n                t = \"unexpected eof @ {} with {} left\"\n                raise Exception(t.format(f.tell(), rem))\n\n        fsum = base64.b64encode(hs.digest()[:9]).decode(\"utf-8\")\n        if fsum != esum:\n            t = \"exe integrity check error; [{}] != [{}]\"\n            raise Exception(t.format(esum, fsum))\n\n\nckck()\n\n\ndef meicln(mod):\n    pdir, mine = os.path.split(mod)\n    dirs = os.listdir(pdir)\n    dirs = [x for x in dirs if x.startswith(\"_MEI\") and x != mine]\n    dirs = [os.path.join(pdir, x) for x in dirs]\n    rm = []\n    for d in dirs:\n        if os.path.isdir(os.path.join(d, \"copyparty\", \"web\")):\n            rm.append(d)\n\n    if not rm:\n        return\n\n    print(\"deleting abandoned SFX dirs:\")\n    for d in rm:\n        print(d)\n        for _ in range(9):\n            try:\n                shutil.rmtree(d)\n                break\n            except:\n                pass\n\n    print()\n\n\ndef meichk():\n    filt = \"copyparty\"\n    if filt not in sys.executable:\n        filt = os.path.basename(sys.executable)\n\n    hits = []\n    try:\n        cmd = \"tasklist /fo csv\".split(\" \")\n        procs = sp.check_output(cmd).decode(\"utf-8\", \"replace\")\n    except:\n        procs = \"\"  # winpe\n\n    for ln in procs.split(\"\\n\"):\n        if filt in ln.split('\"')[:2][-1]:\n            hits.append(ln)\n\n    mod = os.path.dirname(os.path.realpath(__file__))\n    if os.path.basename(mod).startswith(\"_MEI\") and len(hits) == 2:\n        meicln(mod)\n\n\nmeichk()\n\n\nfrom copyparty.__main__ import main\n\ntry:\n    main()\nexcept SystemExit as ex:\n    c = ex.code\n    if c not in [0, -15]:\n        confirm(ex.code)\nexcept KeyboardInterrupt:\n    pass\nexcept:\n    confirm(0)\n"
  },
  {
    "path": "scripts/pyinstaller/loader.rc",
    "content": "# UTF-8\nVSVersionInfo(\n  ffi=FixedFileInfo(\n    filevers=(1,2,3,0),\n    prodvers=(1,2,3,0),\n    mask=0x3f,\n    flags=0x0,\n    OS=0x4,\n    fileType=0x1,\n    subtype=0x0,\n    date=(0, 0)\n  ),\n  kids=[\n    StringFileInfo(\n      [\n      StringTable(\n        '000004b0',\n        [StringStruct('CompanyName', 'ocv.me'),\n        StringStruct('FileDescription', 'copyparty file server'),\n        StringStruct('FileVersion', '1.2.3'),\n        StringStruct('InternalName', 'copyparty'),\n        StringStruct('LegalCopyright', '2019, ed'),\n        StringStruct('OriginalFilename', 'copyparty.exe'),\n        StringStruct('ProductName', 'copyparty'),\n        StringStruct('ProductVersion', '1.2.3')])\n      ]), \n    VarFileInfo([VarStruct('Translation', [0, 1200])])\n  ]\n)\n"
  },
  {
    "path": "scripts/pyinstaller/notes.txt",
    "content": "after performing all the initial setup in this file,\nrun ./build.sh in git-bash to build + upload the exe\n\nto obtain the files referenced below, see ./deps.txt\n\nand if you don't yet have a windows vm to build in, then the\nfirst step will be \"creating the windows VM templates\" below\n\ncommands to start the VMs after initial setup:\nqemu-system-x86_64 -m 4096 -enable-kvm --machine q35 -cpu host -smp 4 -usb -device usb-tablet -net bridge,br=virhost0 -net nic,model=e1000e -drive file=win7-x32.qcow2,discard=unmap,detect-zeroes=unmap\nqemu-system-x86_64 -m 4096 -enable-kvm --machine q35 -cpu host -smp 4 -usb -device usb-tablet -net bridge,br=virhost0 -net nic,model=virtio -drive file=win10-e2021.qcow2,if=virtio,discard=unmap\n\n\n\n## ============================================================\n## first-time setup in a stock win7x32sp1 and/or win10x64 vm:\n##\n\ngrab & install from ftp2host:\n  win7:  Git-2.46.2-32-bit.exe\n  win10: Git-*-64-bit.exe\n\n...and do this on the host so you can grab these notes too:\nunix2dos <~/dev/copyparty/scripts/pyinstaller/notes.txt >~/dev/pyi/notes.txt\n\n\n===[ copy-paste into git-bash ]================================\nuname -s | grep NT-10 && w10=1 || {\n  w7=1; uname -s | grep WOW64 && w7x64=1 || w7x32=1\n}\nfns=(\n  altgraph-0.17.4-py2.py3-none-any.whl\n  pefile-2023.2.7-py3-none-any.whl\n  pywin32_ctypes-0.2.3-py3-none-any.whl\n  upx-5.0.2-win32.zip\n)\n[ $w10 ] && fns+=(\n  jinja2-3.1.6-py3-none-any.whl\n  MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl\n  mutagen-1.47.0-py3-none-any.whl\n  packaging-25.0-py3-none-any.whl\n  pillow-12.1.1-cp313-cp313-win_amd64.whl\n  pyinstaller-6.15.0-py3-none-win_amd64.whl\n  pyinstaller_hooks_contrib-2025.8-py3-none-any.whl\n  python-3.13.12-amd64.exe\n  setuptools-80.9.0-py3-none-any.whl\n)\n[ $w7 ] && fns+=(\n  future-1.0.0-py3-none-any.whl\n  importlib_metadata-6.7.0-py3-none-any.whl\n  packaging-24.0-py3-none-any.whl\n  pip-24.0-py3-none-any.whl\n  pyinstaller_hooks_contrib-2023.8-py2.py3-none-any.whl\n  setuptools-70.3.0-py3-none-any.whl\n  typing_extensions-4.7.1-py3-none-any.whl\n  zipp-3.15.0-py3-none-any.whl\n)\n[ $w7x64 ] && fns+=(\n  windows6.1-kb2533623-x64.msu\n  pyinstaller-5.13.2-py3-none-win64.whl\n  python-3.7.9-amd64.exe\n)\n[ $w7x32 ] && fns+=(\n  windows6.1-kb2533623-x86.msu\n  pyinstaller-5.13.2-py3-none-win32.whl\n  python-3.7.9.exe\n)\ndl() { curl -fkLOC- \"$1\" && return 0; echo \"$1\"; return 1; }\ncd ~/Downloads && rm -f Git-*.exe &&\nfor fn in \"${fns[@]}\"; do\n  dl \"https://192.168.123.1:3923/ro/pyi/$fn\" || {\n    echo ERROR; ok=; break\n  }\ndone\n\n\nWIN7-ONLY: manually install windows6.1-kb2533623 and reboot\n\nmanually install python-3.99.99.exe and then delete it\n\nclose and reopen git-bash so python is in PATH\n\n\n===[ copy-paste into git-bash ]================================\nuname -s | grep NT-10 && w10=1 || w7=1\n[ $w7 ] && pyv=37 || pyv=313\nappd=$(cygpath.exe \"$APPDATA\")\ncd ~/Downloads &&\nyes | unzip upx-*-win32.zip &&\nmv upx-*/upx.exe . &&\npython -m ensurepip &&\n{ [ $w10 ] || python -m pip install --user -U pip-*.whl; } &&\npython -m pip install --user -U packaging-*.whl &&\n{ [ $w7 ] || python -m pip install --user -U {setuptools,mutagen,pillow,jinja2,MarkupSafe}-*.whl; } &&\n{ [ $w10 ] || python -m pip install --user -U future-*.whl importlib_metadata-*.whl typing_extensions-*.whl zipp-*.whl; } &&\npython -m pip install --user -U pyinstaller-*.whl pefile-*.whl pywin32_ctypes-*.whl pyinstaller_hooks_contrib-*.whl altgraph-*.whl &&\nsed -ri 's/--lzma/--best/' $appd/Python/Python$pyv/site-packages/pyinstaller/building/utils.py &&\ncurl -fkLO https://192.168.123.1:3923/cpp/scripts/uncomment.py &&\npython uncomment.py 1 $(for d in $appd/Python/Python$pyv/site-packages/{mutagen,PIL,jinja2,markupsafe}; do find $d -name \\*.py; done) &&\ncd &&\nrm -f build.sh &&\ncurl -fkLO https://192.168.123.1:3923/cpp/scripts/pyinstaller/build.sh &&\n{ [ $w10 ] || curl -fkLO https://192.168.123.1:3923/cpp/scripts/pyinstaller/up2k.sh; } &&\necho ok\n\n\nnow is an excellent time to take another snapshot, but:\n* on win7: first do the 4g.nul thing again\n* on win10: first do a reboot so fstrim kicks in\nthen shutdown and:  vmsnap the.qcow2 snap2\n\n\n\n\n\n## ============================================================\n## creating the windows VM templates\n##\n\nbash ~/dev/asm/doc/setup-virhost.sh  # github:9001/asm\ntruncate -s 4G ~/dev/pyi/4g.nul  # win7 \"trim\"\n\nnote: if you keep accidentally killing the vm with alt-f4 then remove \"-device usb-tablet\" in the qemu commands below\n\n# win7: don't bother with virtio stuff since win7 doesn't fstrim properly anyways (4g.nul takes care of that)\nrm -f win7-x32.qcow2\nqemu-img create -f qcow2 win7-x32.qcow2 64g\nqemu-system-x86_64 -m 4096 -enable-kvm --machine q35 -cpu host -smp 4 -usb -device usb-tablet -net bridge,br=virhost0 -net nic,model=e1000e -drive file=win7-x32.qcow2,discard=unmap,detect-zeroes=unmap \\\n  -cdrom ~/iso/~/iso/en_windows_7_professional_with_sp1_x86_dvd_u_677056.iso\n\n# win10: use virtio hdd and net (viostor+netkvm), but do NOT use qxl graphics (kills mouse cursor)\nrm -f win10-e2021.qcow2\nqemu-img create -f qcow2 win10-e2021.qcow2 64g\nqemu-system-x86_64 -m 4096 -enable-kvm --machine q35 -cpu host -smp 4 -usb -device usb-tablet -net bridge,br=virhost0 -net nic,model=virtio -drive file=win10-e2021.qcow2,if=virtio,discard=unmap \\\n  -drive file=$HOME/iso/virtio-win-0.1.285.iso,media=cdrom -cdrom $HOME/iso/en-us_windows_10_enterprise_ltsc_2021_x64_dvd_d289cf96.iso\n\ntweak stuff to your preference, but also do these steps in order:\n* press ctrl-alt-g so you don't accidentally alt-f4 the vm\n* startmenu, type \"sysdm.cpl\" and hit Enter,\n  * system protection -> configure -> disable\n  * advanced > performance > advanced > virtual memory > no paging file\n* startmenu, type \"cmd\" and hit Ctrl-Shift-Enter, run command:  powercfg /h off\n* reboot\n* make screen resolution something comfy (1440x900 is always a winner)\n* cmd.exe window-width 176 (assuming 1440x900) and buffer-height 8191\n* fix explorer settings (show hidden files and file extensions)\n* WIN10-ONLY: startmenu, device manager, install netkvm driver for ethernet\n* create ftp2host.bat on desktop with following contents:\n    start explorer ftp://wark:k@192.168.123.1:3921/ro/pyi/\n* WIN7-ONLY: connect to ftp, download 4g.nul to desktop, then delete it (poor man's fstrim...)\n\nand finally take snapshots of the VMs by copypasting this stuff into your shell:\nvmsnap() { zstd --long=31 -vT0 -19 -o $1.$2 $1; };\nvmsnap win7-x32.qcow2 snap1\nvmsnap win10-e2021.qcow2 snap1\n\nnote: vmsnap could have defragged the qcow2 as well, but\n  that makes it hard to do xdelta3 memes so it's not worth it --\n  but you can add this before \"zstd\" if you still want to:\n  qemu-img convert -f qcow2 -O qcow2 $1 a.qcow2 && mv a.qcow2 $1 &&\n\n\n\n\n\n## ============================================================\n## notes\n##\n\nsize   t-unpack  virustotal                     cmnt\n8059k  0m0.375s  5/70 generic-only, sandbox-ok  no-upx\n7095k  0m0.563s  4/70 generic-only, sandbox-ok  standard-upx\n6958k  0m0.578s  7/70 generic-only, sandbox-ok  upx+upx\n\nuse python 3.7 since 3.8 onwards requires KB2533623 on target\n\ngenerate loader.rc template:\n%appdata%\\python\\python37\\scripts\\pyi-grab_version C:\\Users\\ed\\AppData\\Local\\Programs\\Python\\Python37\\python.exe\n\n"
  },
  {
    "path": "scripts/pyinstaller/up2k.rc",
    "content": "# UTF-8\nVSVersionInfo(\n  ffi=FixedFileInfo(\n    filevers=(1,2,3,0),\n    prodvers=(1,2,3,0),\n    mask=0x3f,\n    flags=0x0,\n    OS=0x4,\n    fileType=0x1,\n    subtype=0x0,\n    date=(0, 0)\n  ),\n  kids=[\n    StringFileInfo(\n      [\n      StringTable(\n        '000004b0',\n        [StringStruct('CompanyName', 'ocv.me'),\n        StringStruct('FileDescription', 'copyparty uploader / filesearch command'),\n        StringStruct('FileVersion', '1.2.3'),\n        StringStruct('InternalName', 'u2c'),\n        StringStruct('LegalCopyright', '2019, ed'),\n        StringStruct('OriginalFilename', 'u2c.exe'),\n        StringStruct('ProductName', 'copyparty up2k client'),\n        StringStruct('ProductVersion', '1.2.3')])\n      ]), \n    VarFileInfo([VarStruct('Translation', [0, 1200])])\n  ]\n)\n"
  },
  {
    "path": "scripts/pyinstaller/up2k.sh",
    "content": "#!/bin/bash\nset -e\n\ncurl -k https://192.168.123.1:3923/cpp/scripts/pyinstaller/up2k.sh |\ntee up2k2.sh | cmp up2k.sh && rm up2k2.sh || {\n    [ -s up2k2.sh ] || exit 1\n    echo \"new up2k script; upgrade y/n:\"\n    while true; do read -u1 -n1 -r r; [[ $r =~ [yYnN] ]] && break; done\n    [[ $r =~ [yY] ]] && mv up2k{2,}.sh && exec ./up2k.sh\n}\n\nuname -s | grep -E 'WOW64|NT-10' && echo need win7-32 && exit 1\n\ndl() { curl -fkLO \"$1\"; }\ncd ~/Downloads\n\ndl https://192.168.123.1:3923/cpp/bin/u2c.py\ndl https://192.168.123.1:3923/cpp/scripts/pyinstaller/up2k.ico\ndl https://192.168.123.1:3923/cpp/scripts/pyinstaller/up2k.rc\ndl https://192.168.123.1:3923/cpp/scripts/pyinstaller/up2k.spec\n\n# $LOCALAPPDATA/programs/python/python37-32/python -m pip install --user -U pyinstaller\n\nsed -ri 's/^(import .*), selectors$/\\1\\ntry: import selectors\\nexcept: pass/' $LOCALAPPDATA/programs/python/python37-32/Lib/socket.py\n\nsed -ri 's/(add_argument.\"-t[de]\",.*help=\")[^\"]+/\\1not applicable; HTTPS is disabled in this exe/; s/for some reason/in this exe for safety reasons/' u2c.py\nsed -ri '/^import platform/d;s/^(VT100 = )pla.*/\\1False/' u2c.py\n\nread a b _ < <(awk -F\\\" '/^S_VERSION =/{$0=$2;sub(/\\./,\" \");print}' < u2c.py)\nsed -r 's/1,2,3,0/'$a,$b,0,0'/;s/1\\.2\\.3/'$a.$b.0/ <up2k.rc >up2k.rc2\n\n#python uncomment.py u2c.py\n$APPDATA/python/python37/scripts/pyinstaller -y --clean --upx-dir=. up2k.spec\n\n./dist/u2c.exe --version\n\ncsum=$(sha512sum <dist/u2c.exe | cut -c-56)\n\ncurl -fkT dist/u2c.exe -HPW:wark https://192.168.123.1:3923/ >uplod.log\ncat uplod.log\n\ngrep -q $csum uplod.log && echo upload OK || {\n    echo UPLOAD FAILED\n    exit 1\n}\n"
  },
  {
    "path": "scripts/pyinstaller/up2k.spec",
    "content": "# -*- mode: python ; coding: utf-8 -*-\n\n\nblock_cipher = None\n\n\na = Analysis(\n    ['u2c.py'],\n    pathex=[],\n    binaries=[],\n    datas=[],\n    hiddenimports=[],\n    hookspath=[],\n    hooksconfig={},\n    runtime_hooks=[],\n    excludes=[\n        'bz2',\n        'ftplib',\n        'getpass',\n        'lzma',\n        'pickle',\n        'platform',\n        'selectors',\n        'ssl',\n        'subprocess',\n        'tarfile',\n        'tempfile',\n        'tracemalloc',\n        'typing',\n        'zipfile',\n        'zlib',\n        'email.contentmanager',\n        'email.policy',\n        'encodings.zlib_codec',\n        'encodings.base64_codec',\n        'encodings.bz2_codec',\n        'encodings.charmap',\n        'encodings.hex_codec',\n        'encodings.palmos',\n        'encodings.punycode',\n        'encodings.rot_13',\n        'urllib.response',\n        'urllib.robotparser',\n    ],\n    win_no_prefer_redirects=False,\n    win_private_assemblies=False,\n    cipher=block_cipher,\n    noarchive=False,\n)\n\n# this is the only change to the autogenerated specfile:\nxdll = [\"libcrypto-1_1.dll\"]\na.binaries = [x for x in a.binaries if x[0] not in xdll]\n\npyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)\n\nexe = EXE(\n    pyz,\n    a.scripts,\n    a.binaries,\n    a.zipfiles,\n    a.datas,\n    [],\n    name='u2c',\n    debug=False,\n    bootloader_ignore_signals=False,\n    strip=False,\n    upx=True,\n    upx_exclude=[],\n    runtime_tmpdir=None,\n    console=True,\n    disable_windowed_traceback=False,\n    argv_emulation=False,\n    target_arch=None,\n    codesign_identity=None,\n    entitlements_file=None,\n    version='up2k.rc2',\n    icon=['up2k.ico'],\n)\n"
  },
  {
    "path": "scripts/pyinstaller/up2k.spec.sh",
    "content": "#!/bin/bash\nset -e\n\n# grep '\">encodings.cp' C:/Users/ed/dev/copyparty/bin/dist/xref-up2k.html | sed -r 's/.*encodings.cp//;s/<.*//' | sort -n | uniq | tr '\\n' ,\n# grep -i encodings -A1 build/up2k/xref-up2k.html | sed -r 's/.*(Missing|Excluded)Module.*//' | grep moduletype -B1 | grep -v moduletype\n\nex=(\n  ftplib lzma pickle ssl tarfile bz2 zipfile tracemalloc zlib\n  email.contentmanager email.policy\n  encodings.{zlib_codec,base64_codec,bz2_codec,charmap,hex_codec,palmos,punycode,rot_13}\n);\ncex=(); for a in \"${ex[@]}\"; do cex+=(--exclude \"$a\"); done\n$APPDATA/python/python37/scripts/pyi-makespec --version-file up2k.rc2 -i up2k.ico -n u2c -c -F u2c.py \"${cex[@]}\"\n"
  },
  {
    "path": "scripts/rls.sh",
    "content": "#!/bin/bash\nset -e\n\n# usage: ./scripts/rls.sh 1.9.11 gzz 50  # create full release\n# usage: ./scripts/rls.sh sfx gzz 10     # just create sfx.py + en.py + helptext\n#\n# if specified, keep the following sfx-args last:  gz gzz xz nopk udep fast\n#\n# WARNING: when creating full release, will DELETE all of ../dist/,\n#   and all docker-images matching 'localhost/(copyparty|alpine)-'\n\n[ -e make-sfx.sh ] || cd scripts\n[ -e make-sfx.sh ] && [ -e deps-docker ] || {\n    echo cd into the scripts folder first\n    exit 1\n}\n\nv=$1; shift\n[ \"$v\" = sfx ] &&\n    rls= || rls=1\n\n[ $rls ] && {\n    printf '%s\\n' \"$v\" | grep -qE '^[0-9\\.]+$' || exit 1\n    grep -E \"(${v//./, })\" ../copyparty/__version__.py || exit 1\n\n    make -C docker/base\n    ./make-sfx.sh nopk gz\n    ../dist/copyparty-sfx.py --version >/dev/null\n\n    git tag v$v\n    rm -rf ../dist\n    ./make-pypi-release.sh u\n    ./make-tgz-release.sh $v\n}\n\nrm -rf /tmp/pe-copyparty* ../sfx ../dist/copyparty-sfx*\n./make-sfx.sh \"$@\"\n../dist/copyparty-sfx.py --version >/dev/null\nmv ../dist/copyparty-{sfx,int}.py\n\nwhile [ \"$1\" ]; do\n    case \"$1\" in\n        gz*) break;;\n        xz) break;;\n        nopk) break;;\n        udep) break;;\n        fast) break;;\n    esac\n    shift\ndone\n\n./make-pyz.sh \nmv ../dist/copyparty{,-int}.pyz\n\n./make-sfx.sh re lang eng \"$@\" \nmv ../dist/copyparty-{sfx,en}.py\n\nrm -rf /tmp/pe-copyparty* ../sfx\n../dist/copyparty-en.py --version >/dev/null 2>&1\n\n./make-sfx.sh re no-smb \"$@\"\n./make-pyz.sh\nmv ../dist/copyparty{,-en}.pyz\nmv ../dist/copyparty{-int,}.pyz\nmv ../dist/copyparty-{int,sfx}.py\n\n./genhelp.sh\n\n[ $rls ] || exit 0  # ----------------------------------------------------\n\n./prep.sh\ngit add ../contrib/package/arch/PKGBUILD ../contrib/package/makedeb-mpr/PKGBUILD ../contrib/package/nix/copyparty/pin.json\ngit commit -m \"update pkgs to $v\"\ngit log | head\n\n( cd docker\n    #./make.sh purge\n    ./make.sh hclean\n    ./make.sh hclean\n    ./make.sh hclean pull img push\n)\n\ngit push\ngit push --all\ngit push --tags\ngit push all\ngit push all --all\ngit push all --tags\n# git tag -d v$v; git push --delete origin v$v\n"
  },
  {
    "path": "scripts/run-tests.sh",
    "content": "#!/bin/bash\nset -ex\n\nif uname | grep -iE '^(msys|mingw)'; then\n    pids=()\n\n    python -m unittest discover -s tests >/dev/null &\n    pids+=($!)\n\n    python scripts/test/smoketest.py &\n    pids+=($!)\n\n    for pid in ${pids[@]}; do\n        wait $pid\n    done\n    exit $?\nfi\n\n# osx support\ngtar=$(command -v gtar || command -v gnutar) || true\n[ ! -z \"$gtar\" ] && command -v gfind >/dev/null && {\n\ttar()  { $gtar \"$@\"; }\n\tsed()  { gsed  \"$@\"; }\n\tfind() { gfind \"$@\"; }\n\tsort() { gsort \"$@\"; }\n\tcommand -v grealpath >/dev/null &&\n\t\trealpath() { grealpath \"$@\"; }\n}\n\nrm -rf unt\nmkdir -p unt/srv\ncp -pR copyparty tests unt/\ncd unt\n\n# resolve symlinks\nset +x\nfind -type l |\nwhile IFS= read -r f1; do (\n    cd \"${f1%/*}\"\n    f1=\"./${f1##*/}\"\n    f2=\"$(readlink \"$f1\")\"\n    [ -e \"$f2\" ] || f2=\"../$f2\"\n    [ -e \"$f2\" ] || {\n        echo could not resolve \"$f1\"\n        exit 1\n    }\n    rm \"$f1\"\n    cp -p \"$f2\" \"$f1\"\n); done\nset -x\n\npython3 ../scripts/strip_hints/a.py\n\npids=()\nfor py in python{2,3}; do\n    [ \"${1:0:6}\" = python ] && [ \"$1\" != $py ] && continue\n\n    PYTHONPATH=\n    [ $py = python2 ] && PYTHONPATH=../scripts/py2:../sfx/py37:../sfx/j2\n    export PYTHONPATH\n\n    [ $py = python2 ] && py=$(command -v python2.7 || echo $py)\n\n    nice $py -m unittest discover -s tests >/dev/null &\n    pids+=($!)\ndone\n\n[ \"$1\" ] || {\n    python3 ../scripts/test/smoketest.py &\n    pids+=($!)\n}\n\nfor pid in ${pids[@]}; do\n    wait $pid\ndone\n"
  },
  {
    "path": "scripts/sfx.ls",
    "content": "copyparty,\ncopyparty/__init__.py,\ncopyparty/__main__.py,\ncopyparty/__version__.py,\ncopyparty/authsrv.py,\ncopyparty/bos,\ncopyparty/bos/__init__.py,\ncopyparty/bos/bos.py,\ncopyparty/bos/path.py,\ncopyparty/broker_mp.py,\ncopyparty/broker_mpw.py,\ncopyparty/broker_thr.py,\ncopyparty/broker_util.py,\ncopyparty/cert.py,\ncopyparty/cfg.py,\ncopyparty/dxml.py,\ncopyparty/fsutil.py,\ncopyparty/ftpd.py,\ncopyparty/httpcli.py,\ncopyparty/httpconn.py,\ncopyparty/httpsrv.py,\ncopyparty/ico.py,\ncopyparty/mdns.py,\ncopyparty/metrics.py,\ncopyparty/mtag.py,\ncopyparty/multicast.py,\ncopyparty/pwhash.py,\ncopyparty/qrkode.py,\ncopyparty/res,\ncopyparty/res/__init__.py,\ncopyparty/res/COPYING.txt,\ncopyparty/res/insecure.pem,\ncopyparty/sftpd.py,\ncopyparty/smbd.py,\ncopyparty/ssdp.py,\ncopyparty/star.py,\ncopyparty/stolen,\ncopyparty/stolen/__init__.py,\ncopyparty/stolen/dnslib,\ncopyparty/stolen/dnslib/__init__.py,\ncopyparty/stolen/dnslib/bimap.py,\ncopyparty/stolen/dnslib/bit.py,\ncopyparty/stolen/dnslib/buffer.py,\ncopyparty/stolen/dnslib/dns.py,\ncopyparty/stolen/dnslib/label.py,\ncopyparty/stolen/dnslib/lex.py,\ncopyparty/stolen/dnslib/ranges.py,\ncopyparty/stolen/ifaddr,\ncopyparty/stolen/ifaddr/__init__.py,\ncopyparty/stolen/ifaddr/_posix.py,\ncopyparty/stolen/ifaddr/_shared.py,\ncopyparty/stolen/ifaddr/_win32.py,\ncopyparty/stolen/qrcodegen.py,\ncopyparty/stolen/surrogateescape.py,\ncopyparty/sutil.py,\ncopyparty/svchub.py,\ncopyparty/szip.py,\ncopyparty/tcpsrv.py,\ncopyparty/tftpd.py,\ncopyparty/th_cli.py,\ncopyparty/th_srv.py,\ncopyparty/u2idx.py,\ncopyparty/up2k.py,\ncopyparty/util.py,\ncopyparty/web,\ncopyparty/web/a,\ncopyparty/web/a/__init__.py,\ncopyparty/web/a/partyfuse.py,\ncopyparty/web/a/u2c.py,\ncopyparty/web/a/webdav-cfg.txt,\ncopyparty/web/baguettebox.js,\ncopyparty/web/browser.css,\ncopyparty/web/browser.html,\ncopyparty/web/browser.js,\ncopyparty/web/browser2.html,\ncopyparty/web/cf.html,\ncopyparty/web/copyparty.gif,\ncopyparty/web/deps,\ncopyparty/web/deps/__init__.py,\ncopyparty/web/deps/busy.mp3,\ncopyparty/web/deps/easymde.css,\ncopyparty/web/deps/easymde.js,\ncopyparty/web/deps/fuse.py,\ncopyparty/web/deps/marked.js,\ncopyparty/web/deps/mini-fa.css,\ncopyparty/web/deps/mini-fa.woff,\ncopyparty/web/deps/prism.css,\ncopyparty/web/deps/prism.js,\ncopyparty/web/deps/prismd.css,\ncopyparty/web/deps/scp.woff2,\ncopyparty/web/deps/sha512.ac.js,\ncopyparty/web/deps/sha512.hw.js,\ncopyparty/web/idp.html,\ncopyparty/web/iiam.gif,\ncopyparty/web/md.css,\ncopyparty/web/md.html,\ncopyparty/web/md.js,\ncopyparty/web/md2.css,\ncopyparty/web/md2.js,\ncopyparty/web/mde.css,\ncopyparty/web/mde.html,\ncopyparty/web/mde.js,\ncopyparty/web/msg.html,\ncopyparty/web/opds.xml,\ncopyparty/web/opds_osd.xml,\ncopyparty/web/rups.css,\ncopyparty/web/rups.html,\ncopyparty/web/rups.js,\ncopyparty/web/shares.css,\ncopyparty/web/shares.html,\ncopyparty/web/shares.js,\ncopyparty/web/splash.css,\ncopyparty/web/splash.html,\ncopyparty/web/splash.js,\ncopyparty/web/svcs.html,\ncopyparty/web/svcs.js,\ncopyparty/web/tl,\ncopyparty/web/tl/chi.js,\ncopyparty/web/tl/cze.js,\ncopyparty/web/tl/deu.js,\ncopyparty/web/tl/epo.js,\ncopyparty/web/tl/fin.js,\ncopyparty/web/tl/fra.js,\ncopyparty/web/tl/grc.js,\ncopyparty/web/tl/hun.js,\ncopyparty/web/tl/ita.js,\ncopyparty/web/tl/jpn.js,\ncopyparty/web/tl/kor.js,\ncopyparty/web/tl/nld.js,\ncopyparty/web/tl/nno.js,\ncopyparty/web/tl/nor.js,\ncopyparty/web/tl/pol.js,\ncopyparty/web/tl/por.js,\ncopyparty/web/tl/rus.js,\ncopyparty/web/tl/spa.js,\ncopyparty/web/tl/swe.js,\ncopyparty/web/tl/tur.js,\ncopyparty/web/tl/ukr.js,\ncopyparty/web/tl/vie.js,\ncopyparty/web/ui.css,\ncopyparty/web/up2k.js,\ncopyparty/web/util.js,\ncopyparty/web/w.hash.js,\n"
  },
  {
    "path": "scripts/sfx.py",
    "content": "#!/usr/bin/env python3\n# coding: latin-1\nfrom __future__ import print_function, unicode_literals\nimport re, os, sys, time, shutil, signal, tarfile, hashlib, platform, tempfile, traceback\nimport subprocess as sp\n\n\n# skip 1\n#\n# py2sfx (sfx.py) - bundle python-modules into an executable sfx.py\n# (c)2020, ed <oss@ocv.me>, MIT-licensed, originally from copyparty:\n# https://github.com/9001/copyparty/blob/hovudstraum/scripts/sfx.py\n#\n#  ( no need to include the remaining lines of this comment-block\n#     for attribution; the rest is just for context )\n#\n# to create an sfx, use this:\n# https://github.com/9001/copyparty/blob/hovudstraum/scripts/make-sfx.sh\n#\n# wanna steal this for your own project? then maybe\n# see the r0c version, since that's slightly simpler:\n# https://github.com/9001/r0c/blob/master/scripts/sfx.py\n#\n# skip 0\n\n\n\"\"\"\nto edit this file, use HxD or \"vim -b\"\n  (there is compressed stuff at the end)\n\nrun me with python 2.7 or 3.3+ to unpack and run copyparty\n\nthere's zero binaries! just plaintext python scripts all the way down\n  so you can easily unpack the archive and inspect it for shady stuff\n\nthe archive data is attached after the b\"\\n# eof\\n\" archive marker,\n  b\"?0\" decodes to b\"\\x00\"\n  b\"?n\" decodes to b\"\\n\"\n  b\"?r\" decodes to b\"\\r\"\n  b\"??\" decodes to b\"?\"\n\"\"\"\n\n\n# set by make-sfx.sh\nVER = None\nSIZE = None\nCKSUM = None\nSTAMP = None\n\nPY2 = sys.version_info < (3,)\nPY37 = sys.version_info > (3, 7)\nWINDOWS = sys.platform in [\"win32\", \"msys\"]\nsys.dont_write_bytecode = True\nme = os.path.abspath(os.path.realpath(__file__))\n\n\ndef eprint(*a, **ka):\n    ka[\"file\"] = sys.stderr\n    print(*a, **ka)\n\n\ndef msg(*a, **ka):\n    if a:\n        a = [\"[SFX]\", a[0]] + list(a[1:])\n\n    eprint(*a, **ka)\n\n\n# skip 1\n\n\ndef testptn1():\n    \"\"\"test: creates a test-pattern for encode()\"\"\"\n    import struct\n\n    buf = b\"\"\n    for c in range(256):\n        buf += struct.pack(\"B\", c)\n\n    yield buf\n\n\ndef testptn2():\n    import struct\n\n    for a in range(256):\n        if a % 16 == 0:\n            msg(a)\n\n        for b in range(256):\n            buf = b\"\"\n            for c in range(256):\n                buf += struct.pack(\"BBBB\", a, b, c, b)\n            yield buf\n\n\ndef testptn3():\n    with open(\"C:/Users/ed/Downloads/python-3.8.1-amd64.exe\", \"rb\", 512 * 1024) as f:\n        while True:\n            buf = f.read(512 * 1024)\n            if not buf:\n                break\n\n            yield buf\n\n\ntestptn = testptn2\n\n\ndef testchk(cdata):\n    \"\"\"test: verifies that `data` yields testptn\"\"\"\n    import struct\n\n    cbuf = b\"\"\n    mbuf = b\"\"\n    checked = 0\n    t0 = time.time()\n    mdata = testptn()\n    while True:\n        if not mbuf:\n            try:\n                mbuf += next(mdata)\n            except:\n                break\n\n        if not cbuf:\n            try:\n                cbuf += next(cdata)\n            except:\n                expect = mbuf[:8]\n                expect = \"\".join(\n                    \" {:02x}\".format(x)\n                    for x in struct.unpack(\"B\" * len(expect), expect)\n                )\n                raise Exception(\n                    \"truncated at {}, expected{}\".format(checked + len(cbuf), expect)\n                )\n\n        ncmp = min(len(cbuf), len(mbuf))\n        # msg(\"checking {:x}H bytes, {:x}H ok so far\".format(ncmp, checked))\n        for n in range(ncmp):\n            checked += 1\n            if cbuf[n] != mbuf[n]:\n                expect = mbuf[n : n + 8]\n                expect = \"\".join(\n                    \" {:02x}\".format(x)\n                    for x in struct.unpack(\"B\" * len(expect), expect)\n                )\n                cc = struct.unpack(b\"B\", cbuf[n : n + 1])[0]\n                raise Exception(\n                    \"byte {:x}H bad, got {:02x}, expected{}\".format(checked, cc, expect)\n                )\n\n        cbuf = cbuf[ncmp:]\n        mbuf = mbuf[ncmp:]\n\n    td = time.time() - t0\n    txt = \"all {}d bytes OK in {:.3f} sec, {:.3f} MB/s\".format(\n        checked, td, (checked / (1024 * 1024.0)) / td\n    )\n    msg(txt)\n\n\ndef encode(data, size, cksum, ver, ts):\n    \"\"\"creates a new sfx; `data` should yield bufs to attach\"\"\"\n    nb = 0\n    nin = 0\n    nout = 0\n    skip = False\n    with open(me, \"rb\") as fi:\n        unpk = \"\"\n        src = fi.read().replace(b\"\\r\", b\"\").rstrip(b\"\\n\").decode(\"utf-8\")\n        for ln in src.split(\"\\n\"):\n            if ln.endswith(\"# skip 0\"):\n                skip = False\n                nb = 9\n                continue\n\n            if ln.endswith(\"# skip 1\") or skip:\n                skip = True\n                continue\n\n            if ln.strip().startswith(\"# fmt: \"):\n                continue\n\n            if ln:\n                nb = 0\n            else:\n                nb += 1\n                if nb > 2:\n                    continue\n\n            unpk += ln + \"\\n\"\n\n        for k, v in [\n            [\"VER\", '\"' + ver + '\"'],\n            [\"SIZE\", size],\n            [\"CKSUM\", '\"' + cksum + '\"'],\n            [\"STAMP\", ts],\n        ]:\n            v1 = \"\\n{} = None\\n\".format(k)\n            v2 = \"\\n{} = {}\\n\".format(k, v)\n            unpk = unpk.replace(v1, v2)\n\n        unpk = unpk.replace(\"\\n    \", \"\\n\\t\")\n        for _ in range(16):\n            unpk = unpk.replace(\"\\t    \", \"\\t\\t\")\n\n    with open(\"sfx.out\", \"wb\") as f:\n        f.write(unpk.encode(\"utf-8\").rstrip(b\"\\n\") + b\"\\n\\n\\n# eof\")\n        for buf in data:\n            ebuf = (\n                buf.replace(b\"?\", b\"??\")\n                .replace(b\"\\x00\", b\"?0\")\n                .replace(b\"\\r\", b\"?r\")\n                .replace(b\"\\n\", b\"?n\")\n            )\n            nin += len(buf)\n            nout += len(ebuf)\n            while ebuf:\n                ep = 4090\n                while True:\n                    a = ebuf.rfind(b\"?\", 0, ep)\n                    if a < 0 or ep - a > 2:\n                        break\n                    ep = a\n                buf = ebuf[:ep]\n                ebuf = ebuf[ep:]\n                f.write(b\"\\n#\" + buf)\n\n        f.write(b\"\\n\\n\")\n\n    msg(\"wrote {:x}H bytes ({:x}H after encode)\".format(nin, nout))\n\n\ndef makesfx(tar_src, ver, ts):\n    sz = os.path.getsize(tar_src)\n    cksum = hashfile(tar_src)\n    encode(yieldfile(tar_src), sz, cksum, ver, ts)\n\n\n# skip 0\n\n\ndef u8(gen):\n    try:\n        for s in gen:\n            yield s.decode(\"utf-8\", \"ignore\")\n    except:\n        yield s\n        for s in gen:\n            yield s\n\n\ndef yieldfile(fn):\n    s = 64 * 1024\n    with open(fn, \"rb\", s * 4) as f:\n        for block in iter(lambda: f.read(s), b\"\"):\n            yield block\n\n\ndef hashfile(fn):\n    h = hashlib.sha1()\n    for block in yieldfile(fn):\n        h.update(block)\n\n    return h.hexdigest()[:24]\n\n\ndef unpack():\n    \"\"\"unpacks the tar yielded by `data`\"\"\"\n    name = \"pe-copyparty\"\n    try:\n        name += \".\" + str(os.geteuid())\n    except:\n        pass\n\n    tag = \"v\" + str(STAMP)\n    top = tempfile.gettempdir()\n    opj = os.path.join\n    ofe = os.path.exists\n    final = opj(top, name)\n    san = opj(final, \"copyparty/up2k.py\")\n    for suf in range(0, 9001):\n        withpid = \"%s.%d.%s\" % (name, os.getpid(), suf)\n        mine = opj(top, withpid)\n        if not ofe(mine):\n            break\n\n    tar = opj(mine, \"tar\")\n\n    try:\n        if tag in os.listdir(final) and ofe(san):\n            msg(\"found early\")\n            return final\n    except:\n        pass\n\n    sz = 0\n    os.mkdir(mine)\n    with open(tar, \"wb\") as f:\n        for buf in get_payload():\n            sz += len(buf)\n            f.write(buf)\n\n    ck = hashfile(tar)\n    if ck != CKSUM:\n        t = \"\\n\\nexpected %s (%d byte)\\nobtained %s (%d byte)\\nsfx corrupt\"\n        raise Exception(t % (CKSUM, SIZE, ck, sz))\n\n    with tarfile.open(tar, \"r:bz2\") as tf:\n        # this is safe against traversal\n        # skip 1\n        # since it will never process user-provided data;\n        # the only possible input is a single tar.bz2\n        # which gets hardcoded into this script at build stage\n        # skip 0\n        try:\n            tf.extractall(mine, filter=\"tar\")\n        except TypeError:\n            tf.extractall(mine)\n\n    os.remove(tar)\n\n    with open(opj(mine, tag), \"wb\") as f:\n        f.write(b\"h\\n\")\n\n    try:\n        if tag in os.listdir(final) and ofe(san):\n            msg(\"found late\")\n            return final\n    except:\n        pass\n\n    try:\n        if os.path.islink(final):\n            os.remove(final)\n        else:\n            shutil.rmtree(final)\n    except:\n        pass\n\n    for fn in u8(os.listdir(top)):\n        if fn.startswith(name) and fn != withpid:\n            try:\n                old = opj(top, fn)\n                if time.time() - os.path.getmtime(old) > 86400:\n                    shutil.rmtree(old)\n            except:\n                pass\n\n    try:\n        os.symlink(mine, final)\n    except:\n        try:\n            os.rename(mine, final)\n            return final\n        except:\n            msg(\"reloc fail,\", mine)\n\n    return mine\n\n\ndef get_payload():\n    \"\"\"yields the binary data attached to script\"\"\"\n    with open(me, \"rb\") as f:\n        buf = f.read().rstrip(b\"\\r\\n\")\n\n    ptn = b\"\\n# eof\\n#\"\n    a = buf.find(ptn)\n    if a < 0:\n        raise Exception(\"could not find archive marker\")\n\n    esc = {b\"??\": b\"?\", b\"?r\": b\"\\r\", b\"?n\": b\"\\n\", b\"?0\": b\"\\x00\"}\n    buf = buf[a + len(ptn) :].replace(b\"\\n#\", b\"\")\n    p = 0\n    while buf:\n        a = buf.find(b\"?\", p)\n        if a < 0:\n            yield buf[p:]\n            break\n        elif a == p:\n            yield esc[buf[p : p + 2]]\n            p += 2\n        else:\n            yield buf[p:a]\n            p = a\n\n\ndef confirm(rv):\n    msg()\n    msg(\"retcode\", rv if rv else traceback.format_exc())\n    if WINDOWS:\n        msg(\"*** hit enter to exit ***\")\n        try:\n            raw_input() if PY2 else input()\n        except:\n            pass\n\n    sys.exit(rv or 1)\n\n\ndef run(tmp, j2, ftp):\n    msg(\"jinja2:\", j2 or \"bundled\")\n    msg(\"pyftpd:\", ftp or \"bundled\")\n    msg(\"sfxdir:\", tmp)\n    msg()\n\n    sys.argv.append(\"--sfx-tpoke=\" + tmp)\n\n    ld = ((\"\", \"\"), (j2, \"j2\"), (ftp, \"ftp\"), (not PY2, \"py2\"), (PY37, \"py37\"))\n    ld = [os.path.join(tmp, b) for a, b in ld if not a]\n\n    # skip 1\n    # enable this to dynamically remove type hints at startup,\n    # in case a future python version can use them for performance\n    if sys.version_info < (3, 10) and False:\n        sys.path.insert(0, ld[0])\n\n        from strip_hints.a import uh\n\n        uh(tmp + \"/copyparty\")\n    # skip 0\n\n    if any([re.match(r\"^-.*j[0-9]\", x) for x in sys.argv]):\n        run_s(ld)\n    else:\n        run_i(ld)\n\n\ndef run_i(ld):\n    for x in ld:\n        sys.path.insert(0, x)\n\n    e = os.environ\n    e[\"PRTY_NO_IMPRESO\"] = \"1\"\n\n    from copyparty.__main__ import main as p\n\n    p()\n\n\ndef run_s(ld):\n    # fmt: off\n    c = \"import sys,runpy;\" + \"\".join(['sys.path.insert(0,r\"' + x.replace(\"\\\\\", \"/\") + '\");' for x in ld]) + 'runpy.run_module(\"copyparty\",run_name=\"__main__\")'\n    c = [str(x) for x in [sys.executable, \"-c\", c] + list(sys.argv[1:])]\n    # fmt: on\n    msg(\"\\n\", c, \"\\n\")\n    p = sp.Popen(c)\n\n    def bye(*a):\n        p.send_signal(signal.SIGINT)\n\n    signal.signal(signal.SIGTERM, bye)\n    p.wait()\n\n    raise SystemExit(p.returncode)\n\n\ndef main():\n    if \"--versionb\" in sys.argv:\n        return print(VER)\n    sysver = str(sys.version).replace(\"\\n\", \"\\n\" + \" \" * 18)\n    pktime = time.strftime(\"%Y-%m-%d, %H:%M:%S\", time.gmtime(STAMP))\n    msg()\n    msg(\"   this is: copyparty\", VER)\n    msg(\" packed at:\", pktime, \"UTC,\", STAMP)\n    msg(\"archive is:\", me)\n    msg(\"python bin:\", sys.executable)\n    msg(\"python ver:\", platform.python_implementation(), sysver)\n    msg()\n\n    arg = \"\"\n    try:\n        arg = sys.argv[1]\n    except:\n        pass\n\n    # skip 1\n\n    if arg == \"--sfx-testgen\":\n        return encode(testptn(), 1, \"x\", \"x\", 1)\n\n    if arg == \"--sfx-testchk\":\n        return testchk(get_payload())\n\n    if arg == \"--sfx-make\":\n        tar, ver, ts = sys.argv[2:]\n        return makesfx(tar, ver, ts)\n\n    # skip 0\n\n    tmp = os.path.realpath(unpack())\n\n    try:\n        from jinja2 import __version__ as j2\n    except:\n        j2 = None\n\n    try:\n        from pyftpdlib.__init__ import __ver__ as ftp\n    except:\n        ftp = None\n\n    try:\n        run(tmp, j2, ftp)\n    except SystemExit as ex:\n        c = ex.code\n        if c not in [0, -15]:\n            confirm(ex.code)\n    except KeyboardInterrupt:\n        pass\n    except:\n        confirm(0)\n\n\nif __name__ == \"__main__\":\n    main()\n\n\n# skip 1\n# python sfx.py --sfx-testgen && python test.py --sfx-testchk\n# c:\\Python27\\python.exe sfx.py --sfx-testgen && c:\\Python27\\python.exe test.py --sfx-testchk\n"
  },
  {
    "path": "scripts/sfx.sh",
    "content": "# use current/default shell\nset -e\n\ndir=\"$(\n\tprintf '%s\\n' \"$TMPDIR\" /tmp |\n\tawk '/./ {print; exit}'\n)/pe-copyparty\"\n\n[ -e \"$dir/vPACK_TS\" ] || (\n\tprintf '\\033[36munpacking copyparty vCPP_VER (sfx-PACK_HTS)\\033[1;30m\\n\\n'\n\tmkdir -p \"$dir.$$\"\n\tofs=$(awk '$0==\"sfx_eof\" {print NR+1; exit}' < \"$0\")\n\t\n\t[ -z \"$ofs\" ] && {\n\t\tprintf '\\033[31mabort: could not find SFX boundary\\033[0m\\n'\n\t\texit 1\n\t}\n\ttail -n +$ofs \"$0\" | tar -JxC \"$dir.$$\"\n\tln -nsf \"$dir.$$\" \"$dir\"\n\tprintf '\\033[0m'\n\t\n\tnow=$(date -u +%s)\n\tfor d in \"$dir\".*; do\n\t\tts=$(stat -c%Y -- \"$d\" 2>/dev/null) ||\n\t\tts=$(stat -f %m%n -- \"$d\" 2>/dev/null)\n\n\t\t[ $((now-ts)) -gt 300 ] &&\n\t\t\trm -rf \"$d\"\n\tdone\n\techo h > \"$dir/vPACK_TS\"\n) >&2 || exit 1\n\n# detect available pythons\n(IFS=:; for d in $PATH; do\n\tprintf '%s\\n' \"$d\"/python* \"$d\"/pypy*;\ndone) |\n(sed -E 's/(.*\\/[^/0-9]+)([0-9]?[^/]*)$/\\2 \\1/' || cat) |\n(sort -nr || cat) |\n(sed -E 's/([^ ]*) (.*)/\\2\\1/' || cat) |\ngrep -E '/(python|pypy)[0-9\\.-]*$' >$dir/pys || true\n\n# see if we made a choice before\n[ -z \"$pybin\" ] && pybin=\"$(cat $dir/py 2>/dev/null || true)\"\n\n# otherwise find a python with jinja2\n[ -z \"$pybin\" ] && pybin=\"$(cat $dir/pys | while IFS= read -r _py; do\n\tprintf '\\033[1;30mlooking for jinja2 in [%s]\\033[0m\\n' \"$_py\" >&2\n\t$_py -c 'import jinja2' 2>/dev/null || continue\n\tprintf '%s\\n' \"$_py\"\n\tmv $dir/{,x.}j2\n\tbreak\ndone)\"\n\n# otherwise find python2 (bundled jinja2 is way old)\n[ -z \"$pybin\" ] && {\n\tprintf '\\033[0;33mcould not find jinja2; will use py2 + the bundled version\\033[0m\\n' >&2\n\tpybin=\"$(cat $dir/pys | while IFS= read -r _py; do\n\t\tprintf '\\033[1;30mtesting if py2 [%s]\\033[0m\\n' \"$_py\" >&2\n\t\t_ver=$($_py -c 'import sys; sys.stdout.write(str(sys.version_info[0]))' 2>/dev/null) || continue\n\t\t[ $_ver = 2 ] || continue\n\t\tprintf '%s\\n' \"$_py\"\n\t\tbreak\n\tdone)\"\n}\n\n[ -z \"$pybin\" ] && {\n\tprintf '\\033[1;31m\\ncould not find a python with jinja2 installed; please do one of these:\\n\\n  pip install --user jinja2\\n\\n  install python2\\033[0m\\n\\n' >&2\n\texit 1\n}\n\nprintf '\\033[1;30musing [%s]. you can reset with this:\\n  rm -rf %s*\\033[0m\\n\\n' \"$pybin\" \"$dir\"\nprintf '%s\\n' \"$pybin\" > $dir/py\n\nPYTHONPATH=$dir exec \"$pybin\" -m copyparty \"$@\"\n\nsfx_eof\n"
  },
  {
    "path": "scripts/speedtest-fs.py",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport sys\nimport stat\nimport time\nimport signal\nimport traceback\nimport threading\nfrom queue import Queue\n\n\n\"\"\"speedtest-fs: filesystem performance estimate\"\"\"\n__author__ = \"ed <copyparty@ocv.me>\"\n__copyright__ = 2020\n__license__ = \"MIT\"\n__url__ = \"https://github.com/9001/copyparty/\"\n\n\ndef get_spd(nbyte, nfiles, nsec):\n    if not nsec:\n        return \"0.000 MB   0 files   0.000 sec   0.000 MB/s   0.000 f/s\"\n\n    mb = nbyte / (1024 * 1024.0)\n    spd = mb / nsec\n    nspd = nfiles / nsec\n\n    return f\"{mb:.3f} MB   {nfiles} files   {nsec:.3f} sec   {spd:.3f} MB/s   {nspd:.3f} f/s\"\n\n\nclass Inf(object):\n    def __init__(self, t0):\n        self.msgs = []\n        self.errors = []\n        self.reports = []\n        self.mtx_msgs = threading.Lock()\n        self.mtx_reports = threading.Lock()\n\n        self.n_byte = 0\n        self.n_file = 0\n        self.n_sec = 0\n        self.n_done = 0\n        self.t0 = t0\n\n        thr = threading.Thread(target=self.print_msgs)\n        thr.daemon = True\n        thr.start()\n\n    def msg(self, fn, n_read):\n        with self.mtx_msgs:\n            self.msgs.append(f\"{fn} {n_read}\")\n\n    def err(self, fn):\n        with self.mtx_reports:\n            self.errors.append(f\"{fn}\\n{traceback.format_exc()}\")\n\n    def print_msgs(self):\n        while True:\n            time.sleep(0.02)\n            with self.mtx_msgs:\n                msgs = self.msgs\n                self.msgs = []\n\n            if not msgs:\n                continue\n\n            msgs = msgs[-64:]\n            spd = get_spd(self.n_byte, len(self.reports), self.n_sec)\n            msgs = [f\"{spd}   {x}\" for x in msgs]\n            print(\"\\n\".join(msgs))\n\n    def report(self, fn, n_byte, n_sec):\n        with self.mtx_reports:\n            self.reports.append([n_byte, n_sec, fn])\n            self.n_byte += n_byte\n            self.n_sec += n_sec\n\n    def done(self):\n        with self.mtx_reports:\n            self.n_done += 1\n\n\ndef get_files(dir_path):\n    for fn in os.listdir(dir_path):\n        fn = os.path.join(dir_path, fn)\n        st = os.stat(fn).st_mode\n\n        if stat.S_ISDIR(st):\n            yield from get_files(fn)\n\n        if stat.S_ISREG(st):\n            yield fn\n\n\ndef worker(q, inf, read_sz):\n    while True:\n        fn = q.get()\n        if not fn:\n            break\n\n        n_read = 0\n        try:\n            t0 = time.time()\n            with open(fn, \"rb\") as f:\n                while True:\n                    buf = f.read(read_sz)\n                    if not buf:\n                        break\n\n                    n_read += len(buf)\n                    inf.msg(fn, n_read)\n\n            inf.report(fn, n_read, time.time() - t0)\n        except:\n            inf.err(fn)\n\n    inf.done()\n\n\ndef sighandler(signo, frame):\n    os._exit(0)\n\n\ndef main():\n    signal.signal(signal.SIGINT, sighandler)\n\n    root = \".\"\n    if len(sys.argv) > 1:\n        root = sys.argv[1]\n\n    t0 = time.time()\n    q = Queue(256)\n    inf = Inf(t0)\n\n    num_threads = 8\n    read_sz = 32 * 1024\n    targs = (q, inf, read_sz)\n    for _ in range(num_threads):\n        thr = threading.Thread(target=worker, args=targs)\n        thr.daemon = True\n        thr.start()\n\n    for fn in get_files(root):\n        q.put(fn)\n\n    for _ in range(num_threads):\n        q.put(None)\n\n    while inf.n_done < num_threads:\n        time.sleep(0.1)\n\n    t2 = time.time()\n    print(\"\\n\")\n\n    log = inf.reports\n    log.sort()\n    for nbyte, nsec, fn in log[-64:]:\n        spd = get_spd(nbyte, len(log), nsec)\n        print(f\"{spd}   {fn}\")\n\n    print()\n    print(\"\\n\".join(inf.errors))\n\n    print(get_spd(inf.n_byte, len(log), t2 - t0))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/strip_hints/a.py",
    "content": "# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport os\nimport sys\nfrom strip_hints import strip_file_to_string\n\n\n# list unique types used in hints:\n# rm -rf unt && cp -pR copyparty unt && (cd unt && python3 ../scripts/strip_hints/a.py)\n# diff -wNarU1 copyparty unt | grep -E '^\\-' | sed -r 's/[^][, ]+://g; s/[^][, ]+[[(]//g; s/[],()<>{} -]/\\n/g' | grep -E .. | sort | uniq -c | sort -n\n\n\ndef pr(m):\n    sys.stderr.write(m)\n    sys.stderr.flush()\n\n\ndef uh(top):\n    if os.path.exists(top + \"/uh\"):\n        return\n\n    # pr(\"building support for your python ver\")\n    pr(\"unhinting\")\n    files = []\n    for (dp, _, fns) in os.walk(top):\n        for fn in fns:\n            if not fn.endswith(\".py\"):\n                continue\n\n            fp = os.path.join(dp, fn)\n            files.append(fp)\n\n    try:\n        import multiprocessing as mp\n\n        with mp.Pool(os.cpu_count()) as pool:\n            pool.map(uh1, files)\n    except Exception as ex:\n        print(\"\\nnon-mp fallback due to {}\\n\".format(ex))\n        for fp in files:\n            uh1(fp)\n\n    pr(\"k\\n\")\n    with open(top + \"/uh\", \"wb\") as f:\n        f.write(b\"a\")\n\n\ndef uh1(fp):\n    try:\n        uh2(fp)\n    except:\n        print(\"failed to process\", fp)\n        raise\n\n\ndef uh2(fp):\n    pr(\".\")\n    cs = strip_file_to_string(fp, no_ast=True, to_empty=True)\n\n    # remove expensive imports too\n    lns = []\n    on = True\n    on2 = True\n    for ln in cs.split(\"\\n\"):\n        if ln.startswith(\"if True:\"):\n            on = False\n            continue\n\n        if ln.endswith(\"# !rm.yes>\"):\n            on2 = False\n            continue\n\n        if not on2:\n            if ln.endswith(\"# !rm.no>\"):\n                on2 = True\n            continue\n\n        if not on and (not ln.strip() or ln.startswith(\" \")):\n            continue\n\n        on = True\n\n        if \"  # !rm\" in ln:\n            continue\n\n        if ln.endswith(\"TYPE_CHECKING\"):\n            ln = ln.replace(\"from typing import TYPE_CHECKING\", \"TYPE_CHECKING = False\")\n\n        lns.append(ln)\n\n    cs = \"\\n\".join(lns)\n    with open(fp, \"wb\") as f:\n        f.write(cs.encode(\"utf-8\"))\n\n\nif __name__ == \"__main__\":\n    uh(\".\")\n"
  },
  {
    "path": "scripts/test/ptrav.py",
    "content": "#!/usr/bin/env python3\n\nimport re\nimport sys\nimport time\nimport itertools\nimport requests\n\natlas = [\"%\", \"25\", \"2e\", \"2f\", \".\", \"/\"]\n\n\ndef genlen(ubase, port, ntot, nth, wlen):\n    n = 0\n    t0 = time.time()\n    print(\"genlen %s nth %s port %s\" % (wlen, nth, port))\n    rsession = requests.Session()\n    ptn = re.compile(r\"2.2.2.2|\\.\\.\\.|///|%%%|\\.2|/2./|%\\.|/%/\")\n    for path in itertools.product(atlas, repeat=wlen):\n        if \"%\" not in path:\n            continue\n        path = \"\".join(path)\n        if ptn.search(path):\n            continue\n        n += 1\n        if n % ntot != nth:\n            continue\n        url = ubase % (port, path)\n        if n % 500 == nth:\n            spd = n / (time.time() - t0)\n            print(wlen, n, int(spd), url)\n\n        try:\n            r = rsession.get(url)\n        except KeyboardInterrupt:\n            raise\n        except:\n            print(\"\\n[=== RETRY ===]\", url)\n            try:\n                r = rsession.get(url)\n            except:\n                r = rsession.get(url)\n\n        if \"fgsfds\" in r.text:\n            with open(\"hit-%s.txt\" % (time.time()), \"w\", encoding=\"utf-8\") as f:\n                f.write(url)\n            raise Exception(\"HIT! {}\".format(url))\n\n\ndef main():\n    ubase = sys.argv[1]\n    port = int(sys.argv[2])\n    ntot = int(sys.argv[3])\n    nth = int(sys.argv[4])\n    for wlen in range(20):\n        genlen(ubase, port, ntot, nth, wlen)\n\n\nif __name__ == \"__main__\":\n    try:\n        main()\n    except KeyboardInterrupt:\n        pass\n\n\"\"\"\npython3 -m copyparty -v srv::r -p 3931 -q -j4\nnice python3 ./ptrav.py \"http://127.0.0.1:%s/%sfa\" 3931 3 0\nnice python3 ./ptrav.py \"http://127.0.0.1:%s/%sfa\" 3931 3 1\nnice python3 ./ptrav.py \"http://127.0.0.1:%s/%sfa\" 3931 3 2\nnice python3 ./ptrav2.py \"http://127.0.0.1:%s/.cpr/%sfa\" 3931 3 0\nnice python3 ./ptrav2.py \"http://127.0.0.1:%s/.cpr/%sfa\" 3931 3 1\nnice python3 ./ptrav2.py \"http://127.0.0.1:%s/.cpr/%sfa\" 3931 3 2\n(13x slower than /tests/ptrav.py)\n\"\"\"\n"
  },
  {
    "path": "scripts/test/race.py",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport sys\nimport time\nimport json\nimport threading\nimport http.client\n\n\nclass Conn(object):\n    def __init__(self, ip, port):\n        self.s = http.client.HTTPConnection(ip, port, timeout=260)\n        self.st = []\n\n    def get(self, vpath):\n        self.st = [time.time()]\n\n        self.s.request(\"GET\", vpath)\n        self.st.append(time.time())\n\n        ret = self.s.getresponse()\n        self.st.append(time.time())\n\n        if ret.status < 200 or ret.status >= 400:\n            raise Exception(ret.status)\n\n        ret = ret.read()\n        self.st.append(time.time())\n\n        return ret\n\n    def get_json(self, vpath):\n        ret = self.get(vpath)\n        return json.loads(ret)\n\n\nclass CState(threading.Thread):\n    def __init__(self, cs):\n        threading.Thread.__init__(self)\n        self.daemon = True\n        self.cs = cs\n        self.start()\n\n    def run(self):\n        colors = [5, 1, 3, 2, 7]\n        remotes = []\n        remotes_ok = False\n        while True:\n            time.sleep(0.001)\n            if not remotes_ok:\n                remotes = []\n                remotes_ok = True\n                for conn in self.cs:\n                    try:\n                        remotes.append(conn.s.sock.getsockname()[1])\n                    except:\n                        remotes.append(\"?\")\n                        remotes_ok = False\n\n            ta = []\n            for conn, remote in zip(self.cs, remotes):\n                stage = len(conn.st)\n                ta.append(f\"\\033[3{colors[stage]}m{remote}\")\n\n            t = \" \".join(ta)\n            print(f\"{t}\\033[0m\\n\\033[A\", end=\"\")\n\n\ndef allget(cs, urls):\n    thrs = []\n    for c, url in zip(cs, urls):\n        t = threading.Thread(target=c.get, args=(url,))\n        t.start()\n        thrs.append(t)\n\n    for t in thrs:\n        t.join()\n\n\ndef main():\n    os.system(\"\")\n\n    ip, port = sys.argv[1].split(\":\")\n    port = int(port)\n\n    cs = []\n    for _ in range(64):\n        cs.append(Conn(ip, 3923))\n\n    CState(cs)\n\n    urlbase = \"/doujin/c95\"\n    j = cs[0].get_json(f\"{urlbase}?ls\")\n    urls = []\n    for d in j[\"dirs\"]:\n        urls.append(f\"{urlbase}/{d['href']}?th=w\")\n\n    for n in range(100):\n        print(n)\n        allget(cs, urls)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/test/smoketest.py",
    "content": "import os\nimport sys\nimport time\nimport shlex\nimport shutil\nimport signal\nimport tempfile\nimport requests\nimport threading\nimport subprocess as sp\n\n\nCPP = []\n\n\nclass Cpp(object):\n    def __init__(self, args):\n        args = [sys.executable, \"-m\", \"copyparty\"] + args\n        print(\" \".join([shlex.quote(x) for x in args]))\n\n        self.ls_pre = set(list(os.listdir()))\n        self.p = sp.Popen(args)\n        # , stdout=sp.PIPE, stderr=sp.PIPE)\n\n        self.t = threading.Thread(target=self._run)\n        self.t.daemon = True\n        self.t.start()\n\n    def _run(self):\n        self.so, self.se = self.p.communicate()\n\n    def stop(self, wait):\n        if wait:\n            os.kill(self.p.pid, signal.SIGINT)\n            self.t.join(timeout=2)\n        else:\n            self.p.kill()  # macos py3.8\n\n    def clean(self):\n        t = os.listdir()\n        for f in t:\n            if f not in self.ls_pre and f.startswith(\"up.\"):\n                os.unlink(f)\n\n    def await_idle(self, ub, timeout):\n        req = [\"scanning</td><td>False\", \"hash-q</td><td>0\", \"tag-q</td><td>0\"]\n        lim = int(timeout * 10)\n        u = ub + \"?h\"\n        for n in range(lim):\n            try:\n                time.sleep(0.1)\n                r = requests.get(u, timeout=0.1)\n                for x in req:\n                    if x not in r.text:\n                        print(\"ST: {}/{} miss {}\".format(n, lim, x))\n                        raise Exception()\n                print(\"ST: idle\")\n                return\n            except:\n                pass\n\n\ndef tc1(vflags):\n    ub = \"http://127.0.0.1:4321/\"\n    try:\n        if not os.path.exists(\"/dev/shm\"):\n            raise Exception()\n\n        td = \"/dev/shm/cppsmoketst\"\n        ntd = 4\n    except:\n        td = os.path.join(\"srv\", \"smoketest\")\n        ntd = 2\n\n    try:\n        shutil.rmtree(td)\n    except:\n        if os.path.exists(td):\n            raise\n\n    for _ in range(10):\n        try:\n            os.mkdir(td)\n            if os.path.exists(td):\n                break\n        except:\n            time.sleep(0.1)  # win10\n\n    assert os.path.exists(td)\n\n    vidp = os.path.join(tempfile.gettempdir(), \"smoketest.h264\")\n    if not os.path.exists(vidp):\n        cmd = \"ffmpeg -f lavfi -i testsrc=48x32:3 -t 1 -c:v libx264 -tune animation -preset veryslow -crf 69\"\n        sp.check_call(cmd.split(\" \") + [vidp])\n\n    with open(vidp, \"rb\") as f:\n        ovid = f.read()\n\n    args = [\n        \"-q\",\n        \"-j0\",\n        \"-p4321\",\n        \"-e2dsa\",\n        \"-e2tsr\",\n        \"--wram\",\n        \"--ban-403=no\",\n        \"--dbd=yolo\",\n        \"--no-mutagen\",\n        \"--th-ff-jpg\",\n        \"--hist\",\n        os.path.join(td, \"dbm\"),\n    ]\n    pdirs = []\n    hpaths = {}\n\n    for d1 in [\"r\", \"w\", \"rw\"]:\n        pdirs.append(\"{}/{}\".format(td, d1))\n        pdirs.append(\"{}/{}/j\".format(td, d1))\n        for d2 in [\"r\", \"w\", \"rw\", \"c\"]:\n            d = os.path.join(td, d1, \"j\", d2)\n            pdirs.append(d)\n            os.makedirs(d)\n\n    pdirs = [x.replace(\"\\\\\", \"/\") for x in pdirs]\n    udirs = [x.split(\"/\", ntd)[ntd] for x in pdirs]\n    perms = [x.rstrip(\"cj/\")[-1] for x in pdirs]\n    for pd, ud, p in zip(pdirs, udirs, perms):\n        if ud[-1] == \"j\" or ud[-1] == \"c\":\n            continue\n\n        hp = None\n        if pd.endswith(\"st/rw\"):\n            hp = hpaths[ud] = os.path.join(td, \"db1\")\n        elif pd[:-1].endswith(\"rw/j/\"):\n            hpaths[ud] = os.path.join(td, \"dbm\")\n            hp = None\n        else:\n            hp = \"-\"\n            hpaths[ud] = os.path.join(pd, \".hist\")\n\n        arg = \"{}:{}:a{}\".format(pd, ud, p)\n        if hp:\n            arg += \":c,hist=\" + hp\n\n        args += [\"-v\", arg + vflags]\n\n    # print(\"\\n\".join(args))\n    # return\n    cpp = Cpp(args)\n    CPP.append(cpp)\n    cpp.await_idle(ub, 3)\n\n    for d, p in zip(udirs, perms):\n        vid = ovid + \"\\n{}\".format(d).encode(\"utf-8\")\n        r = requests.post(\n            ub + d,\n            data={\"act\": \"bput\"},\n            files={\"f\": (d.replace(\"/\", \"\") + \".h264\", vid)},\n        )\n        c = r.status_code\n        if c == 201 and p not in [\"w\", \"rw\"]:\n            raise Exception(\"post {} with perm {} at {}\".format(c, p, d))\n        elif c == 403 and p not in [\"r\"]:\n            raise Exception(\"post {} with perm {} at {}\".format(c, p, d))\n        elif c not in [201, 403]:\n            raise Exception(\"post {} with perm {} at {}\".format(c, p, d))\n\n    cpp.clean()\n\n    # GET permission\n    for d, p in zip(udirs, perms):\n        u = \"{}{}/{}.h264\".format(ub, d, d.replace(\"/\", \"\"))\n        r = requests.get(u)\n        ok = bool(r)\n        if ok != (p in [\"rw\"]):\n            raise Exception(\"get {} with perm {} at {}\".format(ok, p, u))\n\n    # stat filesystem\n    for d, p in zip(pdirs, perms):\n        u = \"{}/{}.h264\".format(d, d[len(td) :].replace(\"/\", \"\"))\n        ok = os.path.exists(u)\n        if ok != (p in [\"rw\", \"w\"]):\n            raise Exception(\"stat {} with perm {} at {}\".format(ok, p, u))\n\n    # GET thumbnail, vreify contents\n    for d, p in zip(udirs, perms):\n        u = \"{}{}/{}.h264?th=j\".format(ub, d, d.replace(\"/\", \"\"))\n        r = requests.get(u)\n        ok = bool(r and r.content[:3] == b\"\\xff\\xd8\\xff\")\n        if ok != (p in [\"rw\"]):\n            raise Exception(\"thumb {} with perm {} at {}\".format(ok, p, u))\n\n    # check tags\n    cpp.await_idle(ub, 5)\n    for d, p in zip(udirs, perms):\n        u = \"{}{}?ls\".format(ub, d)\n        r = requests.get(u)\n        j = r.json() if r else False\n        tag = None\n        if j:\n            for f in j[\"files\"]:\n                tag = tag or f[\"tags\"].get(\"res\")\n\n        r_ok = bool(j)\n        w_ok = bool(r_ok and j.get(\"files\"))\n\n        if not r_ok or w_ok != (p in [\"rw\"]):\n            raise Exception(\"ls {} with perm {} at {}\".format(ok, p, u))\n\n        if (tag and p != \"rw\") or (not tag and p == \"rw\"):\n            raise Exception(\"tag {} with perm {} at {}\".format(tag, p, u))\n\n        if tag is not None and tag != \"48x32\":\n            raise Exception(\"tag [{}] at {}\".format(tag, u))\n\n    cpp.stop(True)\n\n\ndef run(tc, *a):\n    try:\n        tc(*a)\n    finally:\n        try:\n            CPP[0].stop(False)\n        except:\n            pass\n\n\ndef main():\n    run(tc1, \"\")\n    run(tc1, \":c,fk\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/test/tftp.sh",
    "content": "#!/bin/bash\nset -ex\n\n# PYTHONPATH=.:~/dev/partftpy/ taskset -c 0 python3 -m copyparty -v srv::r -v srv/junk:junk:A --tftp 3969 \n\nget_src=~/dev/copyparty/srv/ro/palette.flac  # tftpwa...\nget_fp=ro/${get_src##*/}  # server url\nget_fn=${get_fp##*/}  # just filename\n\nput_src=~/Downloads/102.zip\nput_dst=~/dev/copyparty/srv/junk/102.zip\n\nexport PATH=\"$PATH:$HOME/src/atftp-0.8.0\"\n\ncd /dev/shm\n\necho curl get 1428 v4; curl --tftp-blksize 1428 tftp://127.0.0.1:3969/$get_fp | cmp $get_src || exit 1\necho curl get 1428 v6; curl --tftp-blksize 1428 tftp://[::1]:3969/$get_fp | cmp $get_src || exit 1\n\necho curl put 1428 v4; rm -f $put_dst && curl --tftp-blksize 1428 -T $put_src tftp://127.0.0.1:3969/junk/ && cmp $put_src $put_dst || exit 1\necho curl put 1428 v6; rm -f $put_dst && curl --tftp-blksize 1428 -T $put_src tftp://[::1]:3969/junk/ && cmp $put_src $put_dst || exit 1\n\necho atftp get 1428; rm -f $get_fn && atftp --option \"blksize 1428\" -g -r $get_fp -l $get_fn 127.0.0.1 3969 && cmp $get_fn $get_src || exit 1\n\necho atftp put 1428; rm -f $put_dst && atftp --option \"blksize 1428\" 127.0.0.1 3969 -p -l $put_src -r junk/102.zip && cmp $put_src $put_dst || exit 1\n\necho tftp-hpa get; rm -f $get_fn && tftp -v -m binary 127.0.0.1 3969 -c get $get_fp && cmp $get_src $get_fn || exit 1\n\necho tftp-hpa put; rm -f $put_dst && tftp -v -m binary 127.0.0.1 3969 -c put $put_src junk/102.zip && cmp $put_src $put_dst || exit 1\n\necho curl get 512; curl tftp://127.0.0.1:3969/$get_fp | cmp $get_src || exit 1\n\necho curl put 512; rm -f $put_dst && curl -T $put_src tftp://127.0.0.1:3969/junk/ && cmp $put_src $put_dst || exit 1\n\necho atftp get 512; rm -f $get_fn && atftp -g -r $get_fp -l $get_fn 127.0.0.1 3969 && cmp $get_fn $get_src || exit 1\n\necho atftp put 512; rm -f $put_dst && atftp 127.0.0.1 3969 -p -l $put_src -r junk/102.zip && cmp $put_src $put_dst || exit 1\n\necho nice\n\nrm -f $get_fn\n"
  },
  {
    "path": "scripts/tl/1.sh",
    "content": "# place new key cl_rcm below cl_hcancel, and cdt_ren below cdt_hsort, and so on\n# (existing keys (ut_ow) are NOT updated; todo)\ngawk <tl.txt '/^var /{lang=$2;k=\"xxxx\"} {p=k} /\"cl_rcm\"/{p=\"cl_hcancel\"} /\"cdt_ren\"/{p=\"cdt_hsort\"} /\"rc_opn\"/{p=\"ur_sm\"} /\"/{k=$1;gsub(/[\" ]/,\"\",k);sub(/:.*/,\"\",k);print lang \" \" p \" \" $0}' | while read lang after ln; do gawk -v p=\"\\\"$after\\\":\" -v v=\"$ln\" <$lang.js '1;$0~p{print \"\\t\" v}' >t; mv t $lang.js; done \n"
  },
  {
    "path": "scripts/tl/1.txt",
    "content": "var chi = {\n\t\"ut_ow\": \"aa\", //m\n\t\"cl_rcm\": \"aa\", //m\n\t\"cdt_ren\": \"aa\", //m\n\t\"rc_opn\": \"aa\", //m\n\t\"rc_ply\": \"aa\", //m\n}\n\nvar cze = {\n\t\"ut_ow\": \"aa\", //m\n\t\"cl_rcm\": \"aa\", //m\n\t\"cdt_ren\": \"aa\", //m\n\t\"rc_opn\": \"aa\", //m\n\t\"rc_ply\": \"aa\", //m\n}\n"
  },
  {
    "path": "scripts/tl.js",
    "content": "\"use strict\";\n\n\n// the three-letter name of the language you're translating to;\n// please adjust this (and the \"Ls.hmn\" further down)\nvar my_lang = \"hmn\";\n\n\n////////////////////////////////////////////////////////////////////////\n// please ignore the next 5 lines:\nvar Ls={}, SR='', wah='';\nfunction langmod() {\n\tif (window.LANGN)\n\t\tLANGN.push([my_lang, Ls[my_lang].tt]);\n}\n\n\n////////////////////////////////////////////////////////////////////////\n// alright,\n// below this point is where the actual translation happens;\n// here is the pairs of \"text-identifier\": \"text-to-translate\"\n//\n// the three-letter language-code \"hmn\" and language-name \"Hymmnos\"\n// is used as an example; please replace these with your language\n//\n// you do not need to translate the TLNotes, those are just for you :-)\n//\n// note: if a line starts with ` then that is to enable\n// turning `foo` into <code>foo</code> (only possible for tooltips)\n//\n// when you are happy with this translation and want to submit it,\n// copy the text below into a new file in the translations folder;\n// https://github.com/9001/copyparty/tree/hovudstraum/copyparty/web/tl\n\n\nLs.hmn = {\n\t\"tt\": \"Hymmnos\",\n\n\t\"cols\": {\n\t\t\"c\": \"action buttons\",\n\t\t\"dur\": \"duration\",\n\t\t\"q\": \"quality / bitrate\",\n\t\t\"Ac\": \"audio codec\",\n\t\t\"Vc\": \"video codec\",\n\t\t\"Fmt\": \"format / container\",\n\t\t\"Ahash\": \"audio checksum\",\n\t\t\"Vhash\": \"video checksum\",\n\t\t\"Res\": \"resolution\",\n\t\t\"T\": \"filetype\",\n\t\t\"aq\": \"audio quality / bitrate\",\n\t\t\"vq\": \"video quality / bitrate\",\n\t\t\"pixfmt\": \"subsampling / pixel structure\",\n\t\t\"resw\": \"horizontal resolution\",\n\t\t\"resh\": \"vertical resolution\",\n\t\t\"chs\": \"audio channels\",\n\t\t\"hz\": \"sample rate\",\n\t},\n\n\t\"hks\": [\n\t\t[\n\t\t\t\"misc\",\n\t\t\t[\"ESC\", \"close various things\"],\n\n\t\t\t\"file-manager\",\n\t\t\t[\"G\", \"toggle list / grid view\"],\n\t\t\t[\"T\", \"toggle thumbnails / icons\"],\n\t\t\t[\"⇧ A/D\", \"thumbnail size\"],\n\t\t\t[\"ctrl-K\", \"delete selected\"],\n\t\t\t[\"ctrl-X\", \"cut selection to clipboard\"],\n\t\t\t[\"ctrl-C\", \"copy selection to clipboard\"],\n\t\t\t[\"ctrl-V\", \"paste (move/copy) here\"],\n\t\t\t[\"Y\", \"download selected\"],\n\t\t\t[\"F2\", \"rename selected\"],\n\n\t\t\t\"file-list-sel\",\n\t\t\t[\"space\", \"toggle file selection\"],\n\t\t\t[\"↑/↓\", \"move selection cursor\"],\n\t\t\t[\"ctrl ↑/↓\", \"move cursor and viewport\"],\n\t\t\t[\"⇧ ↑/↓\", \"select prev/next file\"],\n\t\t\t[\"ctrl-A\", \"select all files / folders\"],\n\t\t], [\n\t\t\t\"navigation\",\n\t\t\t[\"B\", \"toggle breadcrumbs / navpane\"],\n\t\t\t[\"I/K\", \"prev/next folder\"],\n\t\t\t[\"M\", \"parent folder (or unexpand current)\"],\n\t\t\t[\"V\", \"toggle folders / textfiles in navpane\"],\n\t\t\t[\"A/D\", \"navpane size\"],\n\t\t], [\n\t\t\t\"audio-player\",\n\t\t\t[\"J/L\", \"prev/next song\"],\n\t\t\t[\"U/O\", \"skip 10sec back/fwd\"],\n\t\t\t[\"0..9\", \"jump to 0%..90%\"],\n\t\t\t[\"P\", \"play/pause (also initiates)\"],\n\t\t\t[\"S\", \"select playing song\"],\n\t\t\t[\"Y\", \"download song\"],\n\t\t], [\n\t\t\t\"image-viewer\",\n\t\t\t[\"J/L, ←/→\", \"prev/next pic\"],\n\t\t\t[\"Home/End\", \"first/last pic\"],\n\t\t\t[\"F\", \"fullscreen\"],\n\t\t\t[\"R\", \"rotate clockwise\"],\n\t\t\t[\"⇧ R\", \"rotate ccw\"],\n\t\t\t[\"S\", \"select pic\"],\n\t\t\t[\"Y\", \"download pic\"],\n\t\t], [\n\t\t\t\"video-player\",\n\t\t\t[\"U/O\", \"skip 10sec back/fwd\"],\n\t\t\t[\"P/K/Space\", \"play/pause\"],\n\t\t\t[\"C\", \"continue playing next\"],\n\t\t\t[\"V\", \"loop\"],\n\t\t\t[\"M\", \"mute\"],\n\t\t\t[\"[ and ]\", \"set loop interval\"],\n\t\t], [\n\t\t\t\"textfile-viewer\",\n\t\t\t[\"I/K\", \"prev/next file\"],\n\t\t\t[\"M\", \"close textfile\"],\n\t\t\t[\"E\", \"edit textfile\"],\n\t\t\t[\"S\", \"select file (for cut/copy/rename)\"],\n\t\t\t[\"Y\", \"download textfile\"],\n\t\t\t[\"⇧ J\", \"beautify json\"],\n\t\t]\n\t],\n\n\t\"m_ok\": \"OK\",\n\t\"m_ng\": \"Cancel\",\n\n\t\"enable\": \"Enable\",\n\t\"danger\": \"DANGER\",\n\t\"clipped\": \"copied to clipboard\",\n\n\t\"ht_s1\": \"second\",\n\t\"ht_s2\": \"seconds\",\n\t\"ht_m1\": \"minute\",\n\t\"ht_m2\": \"minutes\",\n\t\"ht_h1\": \"hour\",\n\t\"ht_h2\": \"hours\",\n\t\"ht_d1\": \"day\",\n\t\"ht_d2\": \"days\",\n\t\"ht_and\": \" and \",\n\n\t\"goh\": \"control-panel\",\n\t\"gop\": 'previous sibling\">prev',\n\t\"gou\": 'parent folder\">up',\n\t\"gon\": 'next folder\">next',\n\t\"logout\": \"Logout \",\n\t\"login\": \"Login\",\n\t\"access\": \" access\",\n\t\"ot_close\": \"close submenu\",\n\t\"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`&quot;try unite&quot;` = contain exactly «try unite»$N$Nthe date format is iso-8601, like$N`2009-12-31` or `2020-09-12 23:30:00`\",\n\t\"ot_unpost\": \"unpost: delete your recent uploads, or abort unfinished ones\",\n\t\"ot_bup\": \"bup: basic uploader, even supports netscape 4.0\",\n\t\"ot_mkdir\": \"mkdir: create a new directory\",\n\t\"ot_md\": \"new-file: create a new textfile\",\n\t\"ot_msg\": \"msg: send a message to the server log\",\n\t\"ot_mp\": \"media player options\",\n\t\"ot_cfg\": \"configuration options\",\n\t\"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 [🎈]&nbsp; (the basic uploader)<br /><br />during uploads, this icon becomes a progress indicator!',\n\t\"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 [🎈]&nbsp; (the basic uploader)<br /><br />during uploads, this icon becomes a progress indicator!',\n\t\"ot_noie\": 'Please use Chrome / Firefox / Edge',\n\n\t\"ab_mkdir\": \"make directory\",\n\t\"ab_mkdoc\": \"new textfile\",\n\t\"ab_msg\": \"send msg to srv log\",\n\n\t\"ay_path\": \"skip to folders\",\n\t\"ay_files\": \"skip to files\",\n\n\t\"wt_ren\": \"rename selected items$NHotkey: F2\",\n\t\"wt_del\": \"delete selected items$NHotkey: ctrl-K\",\n\t\"wt_cut\": \"cut selected items &lt;small&gt;(then paste somewhere else)&lt;/small&gt;$NHotkey: ctrl-X\",\n\t\"wt_cpy\": \"copy selected items to clipboard$N(to paste them somewhere else)$NHotkey: ctrl-C\",\n\t\"wt_pst\": \"paste a previously cut / copied selection$NHotkey: ctrl-V\",\n\t\"wt_selall\": \"select all files$NHotkey: ctrl-A (when file focused)\",\n\t\"wt_selinv\": \"invert selection\",\n\t\"wt_zip1\": \"download this folder as archive\",\n\t\"wt_selzip\": \"download selection as archive\",\n\t\"wt_seldl\": \"download selection as separate files$NHotkey: Y\",\n\t\"wt_npirc\": \"copy irc-formatted track info\",\n\t\"wt_nptxt\": \"copy plaintext track info\",\n\t\"wt_m3ua\": \"add to m3u playlist (click <code>📻copy</code> later)\",\n\t\"wt_m3uc\": \"copy m3u playlist to clipboard\",\n\t\"wt_grid\": \"toggle grid / list view$NHotkey: G\",\n\t\"wt_prev\": \"previous track$NHotkey: J\",\n\t\"wt_play\": \"play / pause$NHotkey: P\",\n\t\"wt_next\": \"next track$NHotkey: L\",\n\n\t\"ul_par\": \"parallel uploads:\",\n\t\"ut_rand\": \"randomize filenames\",\n\t\"ut_u2ts\": \"copy the last-modified timestamp$Nfrom your filesystem to the server\\\">📅\",\n\t\"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\",\n\t\"ut_mt\": \"continue hashing other files while uploading$N$Nmaybe disable if your CPU or HDD is a bottleneck\",\n\t\"ut_ask\": 'ask for confirmation before upload starts\">💭',\n\t\"ut_pot\": \"improve upload speed on slow devices$Nby making the UI less complex\",\n\t\"ut_srch\": \"don't actually upload, instead check if the files already $N exist on the server (will scan all folders you can read)\",\n\t\"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\",\n\t\"ul_btn\": \"drop files / folders<br>here (or click me)\",\n\t\"ul_btnu\": \"U P L O A D\",\n\t\"ul_btns\": \"S E A R C H\",\n\n\t\"ul_hash\": \"hash\",\n\t\"ul_send\": \"send\",\n\t\"ul_done\": \"done\",\n\t\"ul_idle1\": \"no uploads are queued yet\",\n\t\"ut_etah\": \"average &lt;em&gt;hashing&lt;/em&gt; speed, and estimated time until finish\",\n\t\"ut_etau\": \"average &lt;em&gt;upload&lt;/em&gt; speed and estimated time until finish\",\n\t\"ut_etat\": \"average &lt;em&gt;total&lt;/em&gt; speed and estimated time until finish\",\n\n\t\"uct_ok\": \"completed successfully\",\n\t\"uct_ng\": \"no-good: failed / rejected / not-found\",\n\t\"uct_done\": \"ok and ng combined\",\n\t\"uct_bz\": \"hashing or uploading\",\n\t\"uct_q\": \"idle, pending\",\n\n\t\"utl_name\": \"filename\",\n\t\"utl_ulist\": \"list\",\n\t\"utl_ucopy\": \"copy\",\n\t\"utl_links\": \"links\",\n\t\"utl_stat\": \"status\",\n\t\"utl_prog\": \"progress\",\n\n\t// keep short:\n\t\"utl_404\": \"404\",\n\t\"utl_err\": \"ERROR\",\n\t\"utl_oserr\": \"OS-error\",\n\t\"utl_found\": \"found\",\n\t\"utl_defer\": \"defer\",\n\t\"utl_yolo\": \"YOLO\",\n\t\"utl_done\": \"done\",\n\n\t\"ul_flagblk\": \"the files were added to the queue</b><br>however there is a busy up2k in another browser tab,<br>so waiting for that to finish first\",\n\t\"ul_btnlk\": \"the server configuration has locked this switch into this state\",\n\n\t\"udt_up\": \"Upload\",\n\t\"udt_srch\": \"Search\",\n\t\"udt_drop\": \"drop it here\",\n\n\t\"u_nav_m\": '<h6>aight, what do you have?</h6><code>Enter</code> = Files (one or more)\\n<code>ESC</code> = One folder (including subfolders)',\n\t\"u_nav_b\": '<a href=\"#\" id=\"modal-ok\">Files</a><a href=\"#\" id=\"modal-ng\">One folder</a>',\n\n\t\"cl_opts\": \"switches\",\n\t\"cl_hfsz\": \"filesize\",\n\t\"cl_themes\": \"theme\",\n\t\"cl_langs\": \"language\",\n\t\"cl_ziptype\": \"folder download\",\n\t\"cl_uopts\": \"up2k switches\",\n\t\"cl_favico\": \"favicon\",\n\t\"cl_bigdir\": \"big dirs\",\n\t\"cl_hsort\": \"#sort\",\n\t\"cl_keytype\": \"key notation\",\n\t\"cl_hiddenc\": \"hidden columns\",\n\t\"cl_hidec\": \"hide\",\n\t\"cl_reset\": \"reset\",\n\t\"cl_hpick\": \"tap on column headers to hide in the table below\",\n\t\"cl_hcancel\": \"column hiding aborted\",\n\t\"cl_rcm\": \"right-click menu\",\n\n\t\"ct_grid\": '田 the grid',\n\t\"ct_ttips\": '◔ ◡ ◔\">ℹ️ tooltips',\n\t\"ct_thumb\": 'in grid-view, toggle icons or thumbnails$NHotkey: T\">🖼️ thumbs',\n\t\"ct_csel\": 'use CTRL and SHIFT for file selection in grid-view\">sel',\n\t\"ct_dsel\": 'use drag-selection in grid-view\">dsel',\n\t\"ct_dl\": 'force download (don\\'t display inline) when a file is clicked\">dl',\n\t\"ct_ihop\": 'when the image viewer is closed, scroll down to the last viewed file\">g⮯',\n\t\"ct_dots\": 'show hidden files (if server permits)\">dotfiles',\n\t\"ct_qdel\": 'when deleting files, only ask for confirmation once\">qdel',\n\t\"ct_dir1st\": 'sort folders before files\">📁 first',\n\t\"ct_nsort\": 'natural sort (for filenames with leading digits)\">nsort',\n\t\"ct_utc\": 'show all datetimes in UTC\">UTC',\n\t\"ct_readme\": 'show README.md in folder listings\">📜 readme',\n\t\"ct_idxh\": 'show index.html instead of folder listing\">htm',\n\t\"ct_sbars\": 'show scrollbars\">⟊',\n\n\t\"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📅\",\n\n\t\"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 <em>&quot;does this have the same filesize on the server?&quot;</em> 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 &quot;upload&quot; the same files again to let the client verify them\\\">turbo\",\n\n\t\"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 <em>theoretically</em> catch most unfinished / corrupted uploads, but is not a substitute for doing a verification pass with turbo disabled afterwards\\\">date-chk\",\n\n\t\"cut_u2sz\": \"size (in MiB) of each upload chunk; big values fly better across the atlantic. Try low values on very unreliable connections\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cut_nag\": \"OS notification when upload completes$N(only if the browser or tab is not active)\",\n\t\"cut_sfx\": \"audible alert when upload completes$N(only if the browser or tab is not active)\",\n\n\t\"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\",\n\n\t\"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\",\n\n\t\"cft_text\": \"favicon text (blank and refresh to disable)\",\n\t\"cft_fg\": \"foreground color\",\n\t\"cft_bg\": \"background color\",\n\n\t\"cdt_lim\": \"max number of files to show in a folder\",\n\t\"cdt_ask\": \"when scrolling to the bottom,$Ninstead of loading more files,$Nask what to do\",\n\t\"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\",\n\t\"cdt_ren\": \"enable custom right-click menu, you can still access the regular menu by pressing the shift key and right-clicking\\\">enable\",\n\t\"cdt_rdb\": \"show the regular right-click menu when the custom one is already open and right-clicking again\\\">double\",\n\n\t\"tt_entree\": \"show navpane (directory tree sidebar)$NHotkey: B\",\n\t\"tt_detree\": \"show breadcrumbs$NHotkey: B\",\n\t\"tt_visdir\": \"scroll to selected folder\",\n\t\"tt_ftree\": \"toggle folder-tree / textfiles$NHotkey: V\",\n\t\"tt_pdock\": \"show parent folders in a docked pane at the top\",\n\t\"tt_dynt\": \"autogrow as tree expands\",\n\t\"tt_wrap\": \"word wrap\",\n\t\"tt_hover\": \"reveal overflowing lines on hover$N( breaks scrolling unless mouse $N&nbsp; cursor is in the left gutter )\",\n\n\t\"ml_pmode\": \"at end of folder...\",\n\t\"ml_btns\": \"cmds\",\n\t\"ml_tcode\": \"transcode\",\n\t\"ml_tcode2\": \"transcode to\",\n\t\"ml_tint\": \"tint\",\n\t\"ml_eq\": \"audio equalizer\",\n\t\"ml_drc\": \"dynamic range compressor\",\n\t\"ml_ss\": \"skip silence\",\n\n\t\"mt_loop\": \"loop/repeat one song\\\">🔁\",\n\t\"mt_one\": \"stop after one song\\\">1️⃣\",\n\t\"mt_shuf\": \"shuffle the songs in each folder\\\">🔀\",\n\t\"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▶\",\n\t\"mt_preload\": \"start loading the next song near the end for gapless playback\\\">preload\",\n\t\"mt_prescan\": \"go to the next folder before the last song$Nends, keeping the webbrowser happy$Nso it doesn't stop the playback\\\">nav\",\n\t\"mt_fullpre\": \"try to preload the entire song;$N✅ enable on <b>unreliable</b> connections,$N❌ <b>disable</b> on slow connections probably\\\">full\",\n\t\"mt_fau\": \"on phones, prevent music from stopping if the next song doesn't preload fast enough (can make tags display glitchy)\\\">☕️\",\n\t\"mt_waves\": \"waveform seekbar:$Nshow audio amplitude in the scrubber\\\">~s\",\n\t\"mt_npclip\": \"show buttons for clipboarding the currently playing song\\\">/np\",\n\t\"mt_m3u_c\": \"show buttons for clipboarding the$Nselected songs as m3u8 playlist entries\\\">📻\",\n\t\"mt_octl\": \"os integration (media hotkeys / osd)\\\">os-ctl\",\n\t\"mt_oseek\": \"allow seeking through os integration$N$Nnote: on some devices (iPhones),$Nthis replaces the next-song button\\\">seek\",\n\t\"mt_oscv\": \"show album cover in osd\\\">art\",\n\t\"mt_follow\": \"keep the playing track scrolled into view\\\">🎯\",\n\t\"mt_compact\": \"compact controls\\\">⟎\",\n\t\"mt_uncache\": \"clear cache &nbsp;(try this if your browser cached$Na broken copy of a song so it refuses to play)\\\">uncache\",\n\t\"mt_mloop\": \"loop the open folder\\\">🔁 loop\",\n\t\"mt_mnext\": \"load the next folder and continue\\\">📂 next\",\n\t\"mt_mstop\": \"stop playback\\\">⏸ stop\",\n\t\"mt_cflac\": \"convert flac / wav to {0}\\\">flac\",\n\t\"mt_caac\": \"convert aac / m4a to {0}\\\">aac\",\n\t\"mt_coth\": \"convert all others (not mp3) to {0}\\\">oth\",\n\t\"mt_c2opus\": \"best choice for desktops, laptops, android\\\">opus\",\n\t\"mt_c2owa\": \"opus-weba, for iOS 17.5 and newer\\\">owa\",\n\t\"mt_c2caf\": \"opus-caf, for iOS 11 through 17\\\">caf\",\n\t\"mt_c2mp3\": \"use this on very old devices\\\">mp3\",\n\t\"mt_c2flac\": \"best sound quality, but huge downloads\\\">flac\",\n\t\"mt_c2wav\": \"uncompressed playback (even bigger)\\\">wav\",\n\t\"mt_c2ok\": \"nice, good choice\",\n\t\"mt_c2nd\": \"that's not the recommended output format for your device, but that's fine\",\n\t\"mt_c2ng\": \"your device does not seem to support this output format, but let's try anyways\",\n\t\"mt_xowa\": \"there are bugs in iOS preventing background playback using this format; please use caf or mp3 instead\",\n\t\"mt_tint\": \"background level (0-100) on the seekbar$Nto make buffering less distracting\",\n\t\"mt_eq\": \"`enables the equalizer and gain control;$N$Nboost `0` = standard 100% volume (unmodified)$N$Nwidth `1 &nbsp;` = standard stereo (unmodified)$Nwidth `0.5` = 50% left-right crossfeed$Nwidth `0 &nbsp;` = mono$N$Nboost `-0.8` &amp; 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\",\n\t\"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)\",\n\t\"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\",\n\t\"mt_ssvt\": \"volume threshold (0-255)\\\">vol\",\n\t\"mt_ssts\": \"active threshold (% of track, start)\\\">start\",\n\t\"mt_sste\": \"active threshold (% of track, end)\\\">end\",\n\t\"mt_ssrt\": \"volume/speed ramp up/down time\\\">fade\",\n\t\"mt_sssm\": \"playback speed multiplier\\\">ffwd\",\n\n\t\"mb_play\": \"play\",\n\t\"mm_hashplay\": \"play this audio file?\",\n\t\"mm_m3u\": \"press <code>Enter/OK</code> to Play\\npress <code>ESC/Cancel</code> to Edit\",\n\t\"mp_breq\": \"need firefox 82+ or chrome 73+ or iOS 15+\",\n\t\"mm_bload\": \"now loading...\",\n\t\"mm_bconv\": \"converting to {0}, please wait...\",\n\t\"mm_opusen\": \"your browser cannot play aac / m4a files;\\ntranscoding to opus is now enabled\",\n\t\"mm_playerr\": \"playback failed: \",\n\t\"mm_eabrt\": \"The playback attempt was cancelled\",\n\t\"mm_enet\": \"Your internet connection is wonky\",\n\t\"mm_edec\": \"This file is supposedly corrupted??\",\n\t\"mm_esupp\": \"Your browser does not understand this audio format\",\n\t\"mm_eunk\": \"Unknown Errol\",\n\t\"mm_e404\": \"Could not play audio; error 404: File not found.\",\n\t\"mm_e403\": \"Could not play audio; error 403: Access denied.\\n\\nTry pressing F5 to reload, maybe you got logged out\",\n\t\"mm_e415\": \"Could not play audio; error 415: File transcoding failed; check server logs.\",\n\t\"mm_e500\": \"Could not play audio; error 500: Check server logs.\",\n\t\"mm_e5xx\": \"Could not play audio; server error \",\n\t\"mm_nof\": \"not finding any more audio files nearby\",\n\t\"mm_prescan\": \"Looking for music to play next...\",\n\t\"mm_scank\": \"Found the next song:\",\n\t\"mm_uncache\": \"cache cleared; all songs will redownload on next playback\",\n\t\"mm_hnf\": \"that song no longer exists\",\n\n\t\"im_hnf\": \"that image no longer exists\",\n\n\t\"f_empty\": 'this folder is empty',\n\t\"f_chide\": 'this will hide the column «{0}»\\n\\nyou can unhide columns in the settings tab',\n\t\"f_bigtxt\": \"this file is {0} MiB large -- really view as text?\",\n\t\"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\",\n\t\"fbd_more\": '<div id=\"blazy\">showing <code>{0}</code> of <code>{1}</code> files; <a href=\"#\" id=\"bd_more\">show {2}</a> or <a href=\"#\" id=\"bd_all\">show all</a></div>',\n\t\"fbd_all\": '<div id=\"blazy\">showing <code>{0}</code> of <code>{1}</code> files; <a href=\"#\" id=\"bd_all\">show all</a></div>',\n\t\"f_anota\": \"only {0} of the {1} items were selected;\\nto select the full folder, first scroll to the bottom\",\n\n\t\"f_dls\": 'the file links in the current folder have\\nbeen changed into download links',\n\t\"f_dl_nd\": 'skipping folder (use zip/tar download instead):\\n',\n\n\t\"f_partial\": \"To safely download a file which is currently being uploaded, please click the file which has the same filename, but without the <code>.PARTIAL</code> file extension. Please press CANCEL or Escape to do this.\\n\\nPressing OK / Enter will ignore this warning and continue downloading the <code>.PARTIAL</code> scratchfile instead, which will almost definitely give you corrupted data.\",\n\n\t\"ft_paste\": \"paste {0} items$NHotkey: ctrl-V\",\n\t\"fr_eperm\": 'cannot rename:\\nyou do not have “move” permission in this folder',\n\t\"fd_eperm\": 'cannot delete:\\nyou do not have “delete” permission in this folder',\n\t\"fc_eperm\": 'cannot cut:\\nyou do not have “move” permission in this folder',\n\t\"fp_eperm\": 'cannot paste:\\nyou do not have “write” permission in this folder',\n\t\"fr_emore\": \"select at least one item to rename\",\n\t\"fd_emore\": \"select at least one item to delete\",\n\t\"fc_emore\": \"select at least one item to cut\",\n\t\"fcp_emore\": \"select at least one item to copy to clipboard\",\n\n\t\"fs_sc\": \"share the folder you're in\",\n\t\"fs_ss\": \"share the selected files\",\n\t\"fs_just1d\": \"you cannot select more than one folder,\\nor mix files and folders in one selection\",\n\t\"fs_abrt\": \"❌ abort\",\n\t\"fs_rand\": \"🎲 rand.name\",\n\t\"fs_go\": \"✅ create share\",\n\t\"fs_name\": \"name\",\n\t\"fs_src\": \"source\",\n\t\"fs_pwd\": \"passwd\",\n\t\"fs_exp\": \"expiry\",\n\t\"fs_tmin\": \"min\",\n\t\"fs_thrs\": \"hours\",\n\t\"fs_tdays\": \"days\",\n\t\"fs_never\": \"eternal\",\n\t\"fs_pname\": \"optional link name; will be random if blank\",\n\t\"fs_tsrc\": \"the file or folder to share\",\n\t\"fs_ppwd\": \"optional password\",\n\t\"fs_w8\": \"creating share...\",\n\t\"fs_ok\": \"press <code>Enter/OK</code> to Clipboard\\npress <code>ESC/Cancel</code> to Close\",\n\n\t\"frt_dec\": \"may fix some cases of broken filenames\\\">url-decode\",\n\t\"frt_rst\": \"reset modified filenames back to the original ones\\\">↺ reset\",\n\t\"frt_abrt\": \"abort and close this window\\\">❌ cancel\",\n\t\"frb_apply\": \"APPLY RENAME\",\n\t\"fr_adv\": \"batch / metadata / pattern renaming\\\">advanced\",\n\t\"fr_case\": \"case-sensitive regex\\\">case\",\n\t\"fr_win\": \"windows-safe names; replace <code>&lt;&gt;:&quot;\\\\|?*</code> with japanese fullwidth characters\\\">win\",\n\t\"fr_slash\": \"replace <code>/</code> with a character that doesn't cause new folders to be created\\\">no /\",\n\t\"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\",\n\t\"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\",\n\t\"fr_pdel\": \"delete\",\n\t\"fr_pnew\": \"save as\",\n\t\"fr_pname\": \"provide a name for your new preset\",\n\t\"fr_aborted\": \"aborted\",\n\t\"fr_lold\": \"old name\",\n\t\"fr_lnew\": \"new name\",\n\t\"fr_tags\": \"tags for the selected files (read-only, just for reference):\",\n\t\"fr_busy\": \"renaming {0} items...\\n\\n{1}\",\n\t\"fr_efail\": \"rename failed:\\n\",\n\t\"fr_nchg\": \"{0} of the new names were altered due to <code>win</code> and/or <code>no /</code>\\n\\nOK to continue with these altered new names?\",\n\n\t\"fd_ok\": \"delete OK\",\n\t\"fd_err\": \"delete failed:\\n\",\n\t\"fd_none\": \"nothing was deleted; maybe blocked by server config (xbd)?\",\n\t\"fd_busy\": \"deleting {0} items...\\n\\n{1}\",\n\t\"fd_warn1\": \"DELETE these {0} items?\",\n\t\"fd_warn2\": \"<b>Last chance!</b> No way to undo. Delete?\",\n\n\t\"fc_ok\": \"cut {0} items\",\n\t\"fc_warn\": 'cut {0} items\\n\\nbut: only <b>this</b> browser-tab can paste them\\n(since the selection is so absolutely massive)',\n\n\t\"fcc_ok\": \"copied {0} items to clipboard\",\n\t\"fcc_warn\": 'copied {0} items to clipboard\\n\\nbut: only <b>this</b> browser-tab can paste them\\n(since the selection is so absolutely massive)',\n\n\t\"fp_apply\": \"use these names\",\n\t\"fp_skip\": \"skip conflicts\",  // TLNote: \"skip existing names\" (filenames taken in target folder)\n\t\"fp_ecut\": \"first cut or copy some files / folders to paste / move\\n\\nnote: you can cut / paste across different browser tabs\",\n\t\"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:\",\n\t\"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:\",\n\t\"fp_emore\": \"there are still some filename collisions left to fix\",\n\t\"fp_ok\": \"move OK\",\n\t\"fcp_ok\": \"copy OK\",\n\t\"fp_busy\": \"moving {0} items...\\n\\n{1}\",\n\t\"fcp_busy\": \"copying {0} items...\\n\\n{1}\",\n\t\"fp_abrt\": \"aborting...\",\n\t\"fp_err\": \"move failed:\\n\",\n\t\"fcp_err\": \"copy failed:\\n\",\n\t\"fp_confirm\": \"move these {0} items here?\",\n\t\"fcp_confirm\": \"copy these {0} items here?\",\n\t\"fp_etab\": 'failed to read clipboard from other browser tab',\n\t\"fp_name\": \"uploading a file from your device. Give it a name:\",\n\t\"fp_both_m\": '<h6>choose what to paste</h6><code>Enter</code> = Move {0} files from «{1}»\\n<code>ESC</code> = Upload {2} files from your device',\n\t\"fcp_both_m\": '<h6>choose what to paste</h6><code>Enter</code> = Copy {0} files from «{1}»\\n<code>ESC</code> = Upload {2} files from your device',\n\t\"fp_both_b\": '<a href=\"#\" id=\"modal-ok\">Move</a><a href=\"#\" id=\"modal-ng\">Upload</a>',\n\t\"fcp_both_b\": '<a href=\"#\" id=\"modal-ok\">Copy</a><a href=\"#\" id=\"modal-ng\">Upload</a>',\n\n\t\"mk_noname\": \"type a name into the text field on the left before you do that :p\",\n\t\"nmd_i1\": \"also add the file extension you want, for example <code>.md</code>\",\n\t\"nmd_i2\": \"you can only create <code>.{0}</code> files because you don't have the delete-permission\",\n\n\t\"tv_load\": \"Loading text document:\\n\\n{0}\\n\\n{1}% ({2} of {3} MiB loaded)\",\n\t\"tv_xe1\": \"could not load textfile:\\n\\nerror \",\n\t\"tv_xe2\": \"404, file not found\",\n\t\"tv_lst\": \"list of textfiles in\",\n\t\"tvt_close\": \"return to folder view$NHotkey: M (or Esc)\\\">❌ close\",\n\t\"tvt_dl\": \"download this file$NHotkey: Y\\\">💾 download\",\n\t\"tvt_prev\": \"show previous document$NHotkey: i\\\">⬆ prev\",\n\t\"tvt_next\": \"show next document$NHotkey: K\\\">⬇ next\",\n\t\"tvt_sel\": \"select file &nbsp; ( for cut / copy / delete / ... )$NHotkey: S\\\">sel\",\n\t\"tvt_j\": \"beautify json$NHotkey: shift-J\\\">j\",\n\t\"tvt_edit\": \"open file in text editor$NHotkey: E\\\">✏️ edit\",\n\t\"tvt_tail\": \"monitor file for changes; show new lines in real time\\\">📡 follow\",\n\t\"tvt_wrap\": \"word-wrap\\\">↵\",\n\t\"tvt_atail\": \"lock scroll to bottom of page\\\">⚓\",\n\t\"tvt_ctail\": \"decode terminal colors (ansi escape codes)\\\">🌈\",\n\t\"tvt_ntail\": \"scrollback limit (how many bytes of text to keep loaded)\",\n\n\t\"m3u_add1\": \"song added to m3u playlist\",\n\t\"m3u_addn\": \"{0} songs added to m3u playlist\",\n\t\"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\",\n\n\t\"gt_vau\": \"don't show videos, just play the audio\\\">🎧\",\n\t\"gt_msel\": \"enable file selection; ctrl-click a file to override$N$N&lt;em&gt;when active: doubleclick a file / folder to open it&lt;/em&gt;$N$NHotkey: S\\\">multiselect\",\n\t\"gt_crop\": \"center-crop thumbnails\\\">crop\",\n\t\"gt_3x\": \"hi-res thumbnails\\\">3x\",\n\t\"gt_zoom\": \"zoom\",\n\t\"gt_chop\": \"chop\",\n\t\"gt_sort\": \"sort by\",\n\t\"gt_name\": \"name\",\n\t\"gt_sz\": \"size\",\n\t\"gt_ts\": \"date\",\n\t\"gt_ext\": \"type\",\n\t\"gt_c1\": \"truncate filenames more (show less)\",\n\t\"gt_c2\": \"truncate filenames less (show more)\",\n\n\t\"sm_w8\": \"searching...\",\n\t\"sm_prev\": \"search results below are from a previous query:\\n  \",\n\t\"sl_close\": \"close search results\",\n\t\"sl_hits\": \"showing {0} hits\",\n\t\"sl_moar\": \"load more\",\n\n\t\"s_sz\": \"size\",\n\t\"s_dt\": \"date\",\n\t\"s_rd\": \"path\",\n\t\"s_fn\": \"name\",\n\t\"s_ta\": \"tags\",\n\t\"s_ua\": \"up@\",\n\t\"s_ad\": \"adv.\",\n\t\"s_s1\": \"minimum MiB\",\n\t\"s_s2\": \"maximum MiB\",\n\t\"s_d1\": \"min. iso8601\",\n\t\"s_d2\": \"max. iso8601\",\n\t\"s_u1\": \"uploaded after\",\n\t\"s_u2\": \"and/or before\",\n\t\"s_r1\": \"path contains &nbsp; (space-separated)\",\n\t\"s_f1\": \"name contains &nbsp; (negate with -nope)\",\n\t\"s_t1\": \"tags contains &nbsp; (^=start, end=$)\",\n\t\"s_a1\": \"specific metadata properties\",\n\n\t\"md_eshow\": \"cannot render \",\n\t\"md_off\": \"[📜<em>readme</em>] disabled in [⚙️] -- document hidden\",\n\n\t\"badreply\": \"Failed to parse reply from server\",\n\n\t\"xhr403\": \"403: Access denied\\n\\ntry pressing F5, maybe you got logged out\",\n\t\"xhr0\": \"unknown (probably lost connection to server, or server is offline)\",\n\t\"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\",\n\t\"tl_xe1\": \"could not list subfolders:\\n\\nerror \",\n\t\"tl_xe2\": \"404: Folder not found\",\n\t\"fl_xe1\": \"could not list files in folder:\\n\\nerror \",\n\t\"fl_xe2\": \"404: Folder not found\",\n\t\"fd_xe1\": \"could not create subfolder:\\n\\nerror \",\n\t\"fd_xe2\": \"404: Parent folder not found\",\n\t\"fsm_xe1\": \"could not send message:\\n\\nerror \",\n\t\"fsm_xe2\": \"404: Parent folder not found\",\n\t\"fu_xe1\": \"failed to load unpost list from server:\\n\\nerror \",\n\t\"fu_xe2\": \"404: File not found??\",\n\n\t\"fz_tar\": \"uncompressed gnu-tar file (linux / mac)\",\n\t\"fz_pax\": \"uncompressed pax-format tar (slower)\",\n\t\"fz_targz\": \"gnu-tar with gzip level 3 compression$N$Nthis is usually very slow, so$Nuse uncompressed tar instead\",\n\t\"fz_tarxz\": \"gnu-tar with xz level 1 compression$N$Nthis is usually very slow, so$Nuse uncompressed tar instead\",\n\t\"fz_zip8\": \"zip with utf8 filenames (maybe wonky on windows 7 and older)\",\n\t\"fz_zipd\": \"zip with traditional cp437 filenames, for really old software\",\n\t\"fz_zipc\": \"cp437 with crc32 computed early,$Nfor MS-DOS PKZIP v2.04g (october 1993)$N(takes longer to process before download can start)\",\n\n\t\"un_m1\": \"you can delete your recent uploads (or abort unfinished ones) below\",\n\t\"un_upd\": \"refresh\",\n\t\"un_m4\": \"or share the files visible below:\",\n\t\"un_ulist\": \"show\",\n\t\"un_ucopy\": \"copy\",\n\t\"un_flt\": \"optional filter:&nbsp; URL must contain\",\n\t\"un_fclr\": \"clear filter\",\n\t\"un_derr\": 'unpost-delete failed:\\n',\n\t\"un_f5\": 'something broke, please try a refresh or hit F5',\n\t\"un_uf5\": \"sorry but you have to refresh the page (for example by pressing F5 or CTRL-R) before this upload can be aborted\",\n\t\"un_nou\": '<b>warning:</b> server too busy to show unfinished uploads; click the \"refresh\" link in a bit',\n\t\"un_noc\": '<b>warning:</b> unpost of fully uploaded files is not enabled/permitted in server config',\n\t\"un_max\": \"showing first 2000 files (use the filter)\",\n\t\"un_avail\": \"{0} recent uploads can be deleted<br />{1} unfinished ones can be aborted\",\n\t\"un_m2\": \"sorted by upload time; most recent first:\",\n\t\"un_no1\": \"sike! no uploads are sufficiently recent\",\n\t\"un_no2\": \"sike! no uploads matching that filter are sufficiently recent\",\n\t\"un_next\": \"delete the next {0} files below\",\n\t\"un_abrt\": \"abort\",\n\t\"un_del\": \"delete\",\n\t\"un_m3\": \"loading your recent uploads...\",\n\t\"un_busy\": \"deleting {0} files...\",\n\t\"un_clip\": \"{0} links copied to clipboard\",\n\n\t\"u_https1\": \"you should\",\n\t\"u_https2\": \"switch to https\",\n\t\"u_https3\": \"for better performance\",\n\t\"u_ancient\": 'your browser is impressively ancient -- maybe you should <a href=\"#\" onclick=\"goto(\\'bup\\')\">use bup instead</a>',\n\t\"u_nowork\": \"need firefox 53+ or chrome 57+ or iOS 11+\",\n\t\"tail_2old\": \"need firefox 105+ or chrome 71+ or iOS 14.5+\",\n\t\"u_nodrop\": 'your browser is too old for drag-and-drop uploading',\n\t\"u_notdir\": \"that's not a folder!\\n\\nyour browser is too old,\\nplease try dragdrop instead\",\n\t\"u_uri\": \"to dragdrop images from other browser windows,\\nplease drop it onto the big upload button\",\n\t\"u_enpot\": 'switch to <a href=\"#\">potato UI</a> (may improve upload speed)',\n\t\"u_depot\": 'switch to <a href=\"#\">fancy UI</a> (may reduce upload speed)',\n\t\"u_gotpot\": 'switching to the potato UI for improved upload speed,\\n\\nfeel free to disagree and switch back!',\n\t\"u_pott\": \"<p>files: &nbsp; <b>{0}</b> finished, &nbsp; <b>{1}</b> failed, &nbsp; <b>{2}</b> busy, &nbsp; <b>{3}</b> queued</p>\",\n\t\"u_ever\": \"this is the basic uploader; up2k needs at least<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1\",\n\t\"u_su2k\": 'this is the basic uploader; <a href=\"#\" id=\"u2yea\">up2k</a> is better',\n\t\"u_uput\": 'optimize for speed (skip checksum)',\n\t\"u_ewrite\": 'you do not have write-access to this folder',\n\t\"u_eread\": 'you do not have read-access to this folder',\n\t\"u_enoi\": 'file-search is not enabled in server config',\n\t\"u_enoow\": \"overwrite will not work here; need Delete-permission\",\n\t\"u_badf\": 'These {0} files (of {1} total) were skipped, possibly due to filesystem permissions:\\n\\n',\n\t\"u_blankf\": 'These {0} files (of {1} total) are blank / empty; upload them anyways?\\n\\n',\n\t\"u_applef\": 'These {0} files (of {1} total) are probably undesirable;\\nPress <code>OK/Enter</code> to SKIP the following files,\\nPress <code>Cancel/ESC</code> to NOT exclude, and UPLOAD those as well:\\n\\n',\n\t\"u_just1\": '\\nMaybe it works better if you select just one file',\n\t\"u_ff_many\": \"if you're using <b>Linux / MacOS / Android,</b> then this amount of files <a href=\\\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\\\" target=\\\"_blank\\\"><em>may</em> crash Firefox!</a>\\nif that happens, please try again (or use Chrome).\",\n\t\"u_up_life\": \"This upload will be deleted from the server\\n{0} after it completes\",\n\t\"u_asku\": 'upload these {0} files to <code>{1}</code>',\n\t\"u_unpt\": \"you can undo / delete this upload using the top-left 🧯\",\n\t\"u_bigtab\": 'about to show {0} files\\n\\nthis may crash your browser, are you sure?',\n\t\"u_scan\": 'Scanning files...',\n\t\"u_dirstuck\": 'directory iterator got stuck trying to access the following {0} items; will skip:',\n\t\"u_etadone\": 'Done ({0}, {1} files)',\n\t\"u_etaprep\": '(preparing to upload)',\n\t\"u_hashdone\": 'hashing done',\n\t\"u_hashing\": 'hash',\n\t\"u_hs\": 'handshaking...',\n\t\"u_started\": \"the files are now being uploaded; see [🚀]\",\n\t\"u_dupdefer\": \"duplicate; will be processed after all other files\",\n\t\"u_actx\": \"click this text to prevent loss of<br />performance when switching to other windows/tabs\",\n\t\"u_fixed\": \"OK!&nbsp; Fixed it 👍\",\n\t\"u_cuerr\": \"failed to upload chunk {0} of {1};\\nprobably harmless, continuing\\n\\nfile: {2}\",\n\t\"u_cuerr2\": \"server rejected upload (chunk {0} of {1});\\nwill retry later\\n\\nfile: {2}\\n\\nerror \",\n\t\"u_ehstmp\": \"will retry; see bottom-right\",\n\t\"u_ehsfin\": \"server rejected the request to finalize upload; retrying...\",\n\t\"u_ehssrch\": \"server rejected the request to perform search; retrying...\",\n\t\"u_ehsinit\": \"server rejected the request to initiate upload; retrying...\",\n\t\"u_eneths\": \"network error while performing upload handshake; retrying...\",\n\t\"u_enethd\": \"network error while testing target existence; retrying...\",\n\t\"u_cbusy\": \"waiting for server to trust us again after a network glitch...\",\n\t\"u_ehsdf\": \"server ran out of disk space!\\n\\nwill keep retrying, in case someone\\nfrees up enough space to continue\",\n\t\"u_emtleak1\": \"it looks like your webbrowser may have a memory leak;\\nplease\",\n\t\"u_emtleak2\": ' <a href=\"{0}\">switch to https (recommended)</a> or ',\n\t\"u_emtleak3\": ' ',\n\t\"u_emtleakc\": 'try the following:\\n<ul><li>hit <code>F5</code> to refresh the page</li><li>then disable the &nbsp;<code>mt</code>&nbsp; button in the &nbsp;<code>⚙️ settings</code></li><li>and try that upload again</li></ul>Uploads will be a bit slower, but oh well.\\nSorry for the trouble !\\n\\nPS: chrome v107 <a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=1354816\" target=\"_blank\">has a bugfix</a> for this',\n\t\"u_emtleakf\": 'try the following:\\n<ul><li>hit <code>F5</code> to refresh the page</li><li>then enable <code>🥔</code> (potato) in the upload UI<li>and try that upload again</li></ul>\\nPS: firefox <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\">will hopefully have a bugfix</a> at some point',\n\t\"u_s404\": \"not found on server\",\n\t\"u_expl\": \"explain\",\n\t\"u_maxconn\": \"most browsers limit this to 6, but firefox lets you raise it with <code>connections-per-server</code> in <code>about:config</code>\",\n\t\"u_tu\": '<p class=\"warn\">WARNING: turbo enabled, <span>&nbsp;client may not detect and resume incomplete uploads; see turbo-button tooltip</span></p>',\n\t\"u_ts\": '<p class=\"warn\">WARNING: turbo enabled, <span>&nbsp;search results can be incorrect; see turbo-button tooltip</span></p>',\n\t\"u_turbo_c\": \"turbo is disabled in server config\",\n\t\"u_turbo_g\": \"disabling turbo because you don't have\\ndirectory listing privileges within this volume\",\n\t\"u_life_cfg\": 'autodelete after <input id=\"lifem\" p=\"60\" /> min (or <input id=\"lifeh\" p=\"3600\" /> hours)',\n\t\"u_life_est\": 'upload will be deleted <span id=\"lifew\" tt=\"local time\">---</span>',\n\t\"u_life_max\": 'this folder enforces a\\nmax lifetime of {0}',\n\t\"u_unp_ok\": 'unpost is allowed for {0}',\n\t\"u_unp_ng\": 'unpost will NOT be allowed',\n\t\"ue_ro\": 'your access to this folder is Read-Only\\n\\n',\n\t\"ue_nl\": 'you are currently not logged in',\n\t\"ue_la\": 'you are currently logged in as \"{0}\"',\n\t\"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',\n\t\"ue_ta\": 'try uploading again, it should work now',\n\t\"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 🧯\",\n\t\"ur_1uo\": \"OK: File uploaded successfully\",\n\t\"ur_auo\": \"OK: All {0} files uploaded successfully\",\n\t\"ur_1so\": \"OK: File found on server\",\n\t\"ur_aso\": \"OK: All {0} files found on server\",\n\t\"ur_1un\": \"Upload failed, sorry\",\n\t\"ur_aun\": \"All {0} uploads failed, sorry\",\n\t\"ur_1sn\": \"File was NOT found on server\",\n\t\"ur_asn\": \"The {0} files were NOT found on server\",\n\t\"ur_um\": \"Finished;\\n{0} uploads OK,\\n{1} uploads failed, sorry\",\n\t\"ur_sm\": \"Finished;\\n{0} files found on server,\\n{1} files NOT found on server\",\n\n\t\"rc_opn\": \"open\",\n\t\"rc_ply\": \"play\",\n\t\"rc_pla\": \"play as audio\",\n\t\"rc_txt\": \"open in textfile viewer\",\n\t\"rc_md\": \"open in markdown viewer\",\n\t\"rc_dl\": \"download\",\n\t\"rc_zip\": \"download as archive\",\n\t\"rc_cpl\": \"copy link\",\n\t\"rc_del\": \"delete\",\n\t\"rc_cut\": \"cut\",\n\t\"rc_cpy\": \"copy\",\n\t\"rc_pst\": \"paste\",\n\t\"rc_rnm\": \"rename\",\n\t\"rc_nfo\": \"new folder\",\n\t\"rc_nfi\": \"new file\",\n\t\"rc_sal\": \"select all\",\n\t\"rc_sin\": \"invert selection\",\n\t\"rc_shf\": \"share this folder\",\n\t\"rc_shs\": \"share selection\",\n\n\t\"lang_set\": \"refresh to make the change take effect?\",\n\n\t\"splash\": {\n\t\t\"a1\": \"refresh\",\n\t\t\"b1\": \"howdy stranger &nbsp; <small>(you're not logged in)</small>\",\n\t\t\"c1\": \"logout\",\n\t\t\"d1\": \"dump stack\",  // TLNote: \"d2\" is the tooltip for this button\n\t\t\"d2\": \"shows the state of all active threads\",\n\t\t\"e1\": \"reload cfg\",\n\t\t\"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\",\n\t\t\"f1\": \"you can browse:\",\n\t\t\"g1\": \"you can upload to:\",\n\t\t\"cc1\": \"other stuff:\",\n\t\t\"h1\": \"disable k304\",  // TLNote: \"j1\" explains what k304 is\n\t\t\"i1\": \"enable k304\",\n\t\t\"j1\": \"enabling k304 will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general\",\n\t\t\"k1\": \"reset client settings\",\n\t\t\"l1\": \"login for more:\",\n\t\t\"ls3\": \"login\",\n\t\t\"lu4\": \"username\",\n\t\t\"lp4\": \"password\",\n\t\t\"lo3\": \"logout “{0}” everywhere\",\n\t\t\"lo2\": \"ends the session on all browsers\",\n\t\t\"m1\": \"welcome back,\",  // TLNote: \"welcome back, USERNAME\"\n\t\t\"n1\": \"404 not found &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": 'or maybe you don\\'t have access -- try a password or <a href=\"' + SR + '/?h\">go home</a>',\n\t\t\"p1\": \"403 forbiddena &nbsp;~┻━┻\",\n\t\t\"q1\": 'use a password or <a href=\"' + SR + '/?h\">go home</a>',\n\t\t\"r1\": \"go home\",\n\t\t\".s1\": \"rescan\",\n\t\t\"t1\": \"action\",  // TLNote: this is the header above the \"rescan\" buttons\n\t\t\"u2\": \"time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds\",\n\t\t\"v1\": \"connect\",\n\t\t\"v2\": \"use this server as a local HDD\",\n\t\t\"w1\": \"switch to https\",\n\t\t\"x1\": \"change password\",\n\t\t\"y1\": \"edit shares\",  // TLNote: shows the list of folders that the user has decided to share\n\t\t\"z1\": \"unlock this share:\",  // TLNote: the password prompt to see a hidden share\n\t\t\"ta1\": \"fill in your new password first\",\n\t\t\"ta2\": \"repeat to confirm new password:\",\n\t\t\"ta3\": \"found a typo; please try again\",\n\t\t\"nop\": \"ERROR: Password cannot be blank\",\n\t\t\"nou\": \"ERROR: Username and/or password cannot be blank\",\n\t\t\"aa1\": \"incoming files:\",\n\t\t\"ab1\": \"disable no304\",\n\t\t\"ac1\": \"enable no304\",\n\t\t\"ad1\": \"enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!\",\n\t\t\"ae1\": \"active downloads:\",\n\t\t\"af1\": \"show recent uploads\",\n\t\t\"ag1\": \"view idp cache\",  // TLNote: is a link to a page where IdP users can be managed\n\t}\n};\n"
  },
  {
    "path": "scripts/tl.py",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport sys\n\n\"\"\"\ngenerates a \"tl.js\" which can be used to prototype a new translation\nwithout having to run from source, or tamper with the files that are\nextracted from the sfx\n\nto generate a tl.js for the language you are translating to,\nrun the following command:  python tl.py fra Français\n(\"fra\" being the three-letter language code of your language, and\n \"Français\" the native name of the language)\n\nthen copy tl.js into the webroot and run copyparty with the following:\n  on Linux:    --html-head='<script src=\"/tl.js\"></script>'\n  on Windows:  \"--html-head=<script src='/tl.js'></script>\"\n\nor in a copyparty config file:\n[global]\n  html-head: <script src=\"/tl.js\"></script>\n\nafter editing tl.js, reload your webbrowser by pressing ctrl-shift-r\n\"\"\"\n\n\n#######################################################################\n#######################################################################\n\n\ndef generate_javascript(lang3, native_name, tl_browser):\n    note1 = \"\"\n    note2 = \"\"\n    if lang3 == \"hmn\":\n        note1 = ';\\n// please adjust this (and the \"Ls.hmn\" further down)'\n        note2 = \"\"\"\n// the three-letter language-code \"hmn\" and language-name \"Hymmnos\"\n// is used as an example; please replace these with your language\n//\"\"\"\n\n    return f\"\"\"\"use strict\";\n\n\n// the three-letter name of the language you're translating to{note1}\nvar my_lang = \"{lang3}\";\n\n\n////////////////////////////////////////////////////////////////////////\n// please ignore the next 5 lines:\nvar Ls={{}}, SR='', wah='';\nfunction langmod() {{\n\tif (window.LANGN)\n\t\tLANGN.push([my_lang, Ls[my_lang].tt]);\n}}\n\n\n////////////////////////////////////////////////////////////////////////\n// alright,\n// below this point is where the actual translation happens;\n// here is the pairs of \"text-identifier\": \"text-to-translate\"\n//{note2}\n// you do not need to translate the TLNotes, those are just for you :-)\n//\n// note: if a line starts with ` then that is to enable\n// turning `foo` into <code>foo</code> (only possible for tooltips)\n//\n// when you are happy with this translation and want to submit it,\n// copy the text below into a new file in the translations folder;\n// https://github.com/9001/copyparty/tree/hovudstraum/copyparty/web/tl\n\n\nLs.{lang3} = {{\n\t\"tt\": \"{native_name}\",\n{tl_browser}\n\n\t\"splash\": {{\n\t\t\"a1\": \"refresh\",\n\t\t\"b1\": \"howdy stranger &nbsp; <small>(you're not logged in)</small>\",\n\t\t\"c1\": \"logout\",\n\t\t\"d1\": \"dump stack\",  // TLNote: \"d2\" is the tooltip for this button\n\t\t\"d2\": \"shows the state of all active threads\",\n\t\t\"e1\": \"reload cfg\",\n\t\t\"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\",\n\t\t\"f1\": \"you can browse:\",\n\t\t\"g1\": \"you can upload to:\",\n\t\t\"cc1\": \"other stuff:\",\n\t\t\"h1\": \"disable k304\",  // TLNote: \"j1\" explains what k304 is\n\t\t\"i1\": \"enable k304\",\n\t\t\"j1\": \"enabling k304 will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general\",\n\t\t\"k1\": \"reset client settings\",\n\t\t\"l1\": \"login for more:\",\n\t\t\"ls3\": \"login\",\n\t\t\"lu4\": \"username\",\n\t\t\"lp4\": \"password\",\n\t\t\"lo3\": \"logout “{{0}}” everywhere\",\n\t\t\"lo2\": \"ends the session on all browsers\",\n\t\t\"m1\": \"welcome back,\",  // TLNote: \"welcome back, USERNAME\"\n\t\t\"n1\": \"404 not found &nbsp;┐( ´ -`)┌\",\n\t\t\"o1\": 'or maybe you don\\\\'t have access -- try a password or <a href=\"' + SR + '/?h\">go home</a>',\n\t\t\"p1\": \"403 forbiddena &nbsp;~┻━┻\",\n\t\t\"q1\": 'use a password or <a href=\"' + SR + '/?h\">go home</a>',\n\t\t\"r1\": \"go home\",\n\t\t\".s1\": \"rescan\",\n\t\t\"t1\": \"action\",  // TLNote: this is the header above the \"rescan\" buttons\n\t\t\"u2\": \"time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds\",\n\t\t\"v1\": \"connect\",\n\t\t\"v2\": \"use this server as a local HDD\",\n\t\t\"w1\": \"switch to https\",\n\t\t\"x1\": \"change password\",\n\t\t\"y1\": \"edit shares\",  // TLNote: shows the list of folders that the user has decided to share\n\t\t\"z1\": \"unlock this share:\",  // TLNote: the password prompt to see a hidden share\n\t\t\"ta1\": \"fill in your new password first\",\n\t\t\"ta2\": \"repeat to confirm new password:\",\n\t\t\"ta3\": \"found a typo; please try again\",\n\t\t\"nop\": \"ERROR: Password cannot be blank\",\n\t\t\"nou\": \"ERROR: Username and/or password cannot be blank\",\n\t\t\"aa1\": \"incoming files:\",\n\t\t\"ab1\": \"disable no304\",\n\t\t\"ac1\": \"enable no304\",\n\t\t\"ad1\": \"enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!\",\n\t\t\"ae1\": \"active downloads:\",\n\t\t\"af1\": \"show recent uploads\",\n\t\t\"ag1\": \"view idp cache\",  // TLNote: is a link to a page where IdP users can be managed\n\t}}\n}};\n\"\"\"\n\n\n#######################################################################\n#######################################################################\n\n\ndef die(*a):\n    print(*a)\n    sys.exit(1)\n\n\ndef main():\n    webdir = \"../copyparty/web/splash.js\"\n\n    while webdir and not os.path.exists(webdir):\n        webdir = webdir.split(\"/\", 1)[1]\n\n    if not webdir:\n        t = \"could not find the copyparty/web/*.js files!\\nplease cd into the copyparty repo before running this script\"\n        die(t)\n\n    webdir = webdir.rsplit(\"/\", 1)[0] if \"/\" in webdir else \".\"\n\n    with open(os.path.join(webdir, \"browser.js\"), \"rb\") as f:\n        browserjs = f.read().decode(\"utf-8\")\n\n    _, browserjs = browserjs.split('\\n\\t\\t\"tt\": \"English\",\\n', 1)\n    browserjs, _ = browserjs.split(\"\\n}\", 1)\n    browserjs = browserjs.replace(\"\\n\\t\", \"\\n\")\n\n    try:\n        lang3 = sys.argv[1]\n    except:\n        t = \"you need to provide one more argument: the three-letter language code for the language you are translating to, for example: ger\"\n        die(t)\n\n    try:\n        native_name = sys.argv[2]\n    except:\n        t = \"you need to provide one more argument: the native name of the language you are translating to, for example: Deutsch\"\n        die(t)\n\n    ret = generate_javascript(lang3, native_name, browserjs)\n\n    outpath = os.path.abspath(\"tl.js\")\n    if os.path.exists(outpath):\n        t = \"the output file already exists! if you really want to overwrite it, then delete the following file and try again:\"\n        die(t, outpath)\n\n    with open(outpath, \"wb\") as f:\n        f.write(ret.encode(\"utf-8\"))\n\n    print(\"successfully created\", outpath)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/tlcheck.sh",
    "content": "#!/bin/bash\nset -e\n\n[ -f \"$1\" ] && [ -f \"$2\" ] && [ $# = 2 ] || {\n    echo usage: ./scripts/tlcheck.sh scripts/tl.js copyparty/web/tl/nor.js \n    exit 1\n}\n\ncat \"$1\" \"$2\" | awk '\n    /^\\}/{fa=0;fb=0}\n    /^Ls\\./{if(nln++){fb=1}else{fa=1}}\n    !/\":/{next}\n    fa{a[ia++]=$0}\n    fb{b[ib++]=$0}\n    END{for (i=0;i<ia;i++) printf \"%s\\n%s\\n\\n\",a[i],b[i]}\n' |\nawk -v apos=\\' -v quot=\\\" '\n    # count special chars and prefix to line\n    function c(ch) {\n        m=$0;\n        gsub(ch,\"\",m);\n        t=t sprintf(\"%s%d \", ch, length($0)-length(m))\n    }\n    !$0 && t!=tp {\n        print \"\\n\\033[1;37;41m====DIFF====\"\n    }\n    !$0 && s==sp {\n        print \"\\n\\033[1;37;44m====IDENTICAL====\"\n    }\n    !$0 { print; next; }\n    {\n        sp=s; s=$0;\n        tp=t; t=\"\";\n        c(quot);\n        c(apos);\n        c(\"<\");\n        c(\">\");\n        c(\"{\");\n        c(\"}\");\n        c(\"&\");\n        c(\"\\\\\\$\");\n        c(\"\\\\\\\\\");\n        print t $0;\n    }\n' |\nsed -r $'\n    s/\\\\\\\\/\\033[1;37;41m\\\\\\\\\\033[0m/g;\n    s/\\$N/\\033[1;37;45m$N\\033[0m/g;\n    s/([{}])/\\033[34m\\\\1\\033[0m/g;\n    s/\"/\\033[44m\"\\033[0m/g;\n    s/\\'/\\033[45m\\'\\033[0m/g;\n    s/&/\\033[1;43;30m&\\033[0m/g;\n    s/([<>])/\\033[30;47m\\\\1\\033[0m/g\n' |\nsed -r 's/\\t+//' |\nless -R\n"
  },
  {
    "path": "scripts/toc.sh",
    "content": "#!/bin/bash\nset -e\n\nfor f in README.md docs/devnotes.md docs/versus.md; do\n\ncat $f | awk '\n    function pr() {\n        if (!h) {return};\n        if (/^ *[*!#|]/||!s) {\n            printf \"%s\\n\",h;\n            h=0;\n            return\n        };\n        if (/.../) {\n            printf \"%s - %s\\n\",h,$0;\n            h=0\n        };\n    };\n    /```/{o=!o}\n    o{next}\n    /^#/{s=1;rs=0;pr()}\n    /^#* *(nix package)/{rs=1}\n    /^#* *(themes|install on android|install on iOS|dev env setup|just the sfx|complete release|optional gpl stuff|nixos module|reverse-proxy perf)|```/{s=rs}\n    /^#/{\n        lv=length($1);\n        sub(/[^ ]+ /,\"\");\n        sub(/\\[/,\"\");\n        sub(/\\]\\([^)]+\\)/,\"\");\n        bab=$0;\n        gsub(/ /,\"-\",bab);\n        gsub(/\\./,\"\",bab);\n        h=sprintf(\"%\" ((lv-1)*4+1) \"s [%s](#%s)\", \"*\",$0,bab);\n        next\n    }\n    !h{next}\n    {sub(/  .*/,\"\");sub(/[:;,]$/,\"\")}\n    {pr()}\n' > toc\n\ngrep -E '^#+ *[^ ]+ toc$' -B1000 -A2 <$f >p1\n\nh2=\"$(awk '/^#+ *[^ ]+ toc$/{o=1;next} o&&/^#/{print;exit}' <$f)\"\n\ngrep -F \"$h2\" -B2 -A999999 <$f >p2\n\n(cat p1; grep -F \"${h2#* }\" -A1000 <toc; cat p2) >$f\n\nrm p1 p2 toc\n\ndone\n"
  },
  {
    "path": "scripts/uncomment.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport io\nimport os\nimport sys\nimport tokenize\n\n\ntry:\n    FSTRING_MIDDLE = tokenize.FSTRING_MIDDLE\nexcept:\n    FSTRING_MIDDLE = -9001\n\n\ndef uncomment(fpath):\n    \"\"\"modified https://stackoverflow.com/a/62074206\"\"\"\n\n    print(\".\", end=\"\", flush=True)\n    with open(fpath, \"rb\") as f:\n        orig = f.read().decode(\"utf-8\")\n\n    out = \"\"\n    io_obj = io.StringIO(orig)\n    prev_toktype = tokenize.INDENT\n    last_lineno = -1\n    last_col = 0\n    code = False\n    for tok in tokenize.generate_tokens(io_obj.readline):\n        # print(repr(tok))\n        token_type = tok[0]\n        token_string = tok[1]\n        start_line, start_col = tok[2]\n        end_line, end_col = tok[3]\n\n        if start_line > last_lineno:\n            last_col = 0\n\n        if start_col > last_col and prev_toktype != FSTRING_MIDDLE:\n            out += \" \" * (start_col - last_col)\n\n        is_legalese = (\n            \"copyright\" in token_string.lower() or \"license\" in token_string.lower()\n        )\n\n        if token_type == tokenize.STRING:\n            if (\n                prev_toktype != tokenize.INDENT\n                and prev_toktype != tokenize.NEWLINE\n                and start_col > 0\n                or is_legalese\n            ):\n                out += token_string\n            else:\n                out += '\"a\"'\n        elif token_type == FSTRING_MIDDLE:\n            out += token_string.replace(r\"{\", r\"{{\").replace(r\"}\", r\"}}\")\n            if not code and token_string.strip():\n                code = True\n        elif token_type != tokenize.COMMENT:\n            out += token_string\n            if not code and token_string.strip():\n                code = True\n        elif is_legalese or (not start_col and not code):\n            out += token_string\n        else:\n            if out.rstrip(\" \").endswith(\"\\n\"):\n                out = out.rstrip() + \"\\n\"\n            else:\n                out = out.rstrip()\n\n        prev_toktype = token_type\n        last_lineno = end_line\n        last_col = end_col\n\n    # out = \"\\n\".join(x for x in out.splitlines() if x.strip())\n\n    with open(fpath, \"wb\") as f:\n        f.write(out.encode(\"utf-8\"))\n\n\ndef main():\n    print(\"uncommenting\", end=\"\", flush=True)\n    try:\n        if sys.argv[1] == \"1\":\n            sys.argv.remove(\"1\")\n            raise Exception(\"disabled\")\n\n        import multiprocessing as mp\n\n        mp.set_start_method(\"spawn\", True)\n        with mp.Pool(os.cpu_count()) as pool:\n            pool.map(uncomment, sys.argv[1:])\n    except Exception as ex:\n        print(\"\\nnon-mp fallback due to {}\\n\".format(ex))\n        for f in sys.argv[1:]:\n            try:\n                uncomment(f)\n            except Exception as ex:\n                print(\"uncomment failed: [%s] %s\" % (f, repr(ex)))\n\n    print(\"k\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/ziploader.py",
    "content": "#!/usr/bin/env python3\n\nimport sys\nimport time\nimport traceback\n\n\nVER = None\nSTAMP = None\nWINDOWS = sys.platform in [\"win32\", \"msys\"]\n\n\ndef msg(*a, **ka):\n    if a:\n        a = [\"[ZIP]\", a[0]] + list(a[1:])\n\n    ka[\"file\"] = sys.stderr\n    print(*a, **ka)\n\n\ndef confirm(rv):\n    msg()\n    msg(\"retcode\", rv if rv else traceback.format_exc())\n    if WINDOWS:\n        msg(\"*** hit enter to exit ***\")\n        try:\n            input()\n        except:\n            pass\n\n    sys.exit(rv or 1)\n\n\ndef run():\n    from copyparty.__main__ import main as cm\n\n    cm()\n\n\ndef main():\n    if \"--versionb\" in sys.argv:\n        return print(VER)\n    pktime = time.strftime(\"%Y-%m-%d, %H:%M:%S\", time.gmtime(STAMP))\n    msg()\n    msg(\"build-time:\", pktime, \"UTC,\", STAMP)\n    msg(\"python-bin:\", sys.executable)\n    msg()\n\n    try:\n        run()\n    except SystemExit as ex:\n        c = ex.code\n        if c not in [0, -15]:\n            confirm(ex.code)\n    except KeyboardInterrupt:\n        pass\n    except:\n        confirm(0)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\nfrom __future__ import print_function\n\nimport os\nimport sys\nfrom shutil import rmtree\nfrom setuptools import setup, Command\n\n_ = \"\"\"\nthis probably still works but is no longer in use;\npyproject.toml and scripts/make-pypi-release.sh\nare in charge of packaging wheels now\n\"\"\"\n\nNAME = \"copyparty\"\nVERSION = None\ndata_files = [(\"share/doc/copyparty\", [\"README.md\", \"LICENSE\"])]\nmanifest = \"\"\nfor dontcare, files in data_files:\n    for fn in files:\n        manifest += \"include {0}\\n\".format(fn)\n\nmanifest += \"recursive-include copyparty/res *\\n\"\nmanifest += \"recursive-include copyparty/web *\\n\"\n\nhere = os.path.abspath(os.path.dirname(__file__))\n\nwith open(here + \"/MANIFEST.in\", \"wb\") as f:\n    f.write(manifest.encode(\"utf-8\"))\n\nwith open(here + \"/README.md\", \"rb\") as f:\n    txt = f.read().decode(\"utf-8\")\n    long_description = txt\n\nabout = {}\nif not VERSION:\n    with open(os.path.join(here, NAME, \"__version__.py\"), \"rb\") as f:\n        exec (f.read().decode(\"utf-8\").split(\"\\n\\n\", 1)[1], about)\nelse:\n    about[\"__version__\"] = VERSION\n\n\nclass clean2(Command):\n    description = \"Cleans the source tree\"\n    user_options = []\n\n    def initialize_options(self):\n        pass\n\n    def finalize_options(self):\n        pass\n\n    def run(self):\n        os.system(\"{0} setup.py clean --all\".format(sys.executable))\n\n        try:\n            rmtree(\"./dist\")\n        except:\n            pass\n\n        try:\n            rmtree(\"./copyparty.egg-info\")\n        except:\n            pass\n\n        nuke = []\n        for (dirpath, _, filenames) in os.walk(\".\"):\n            for fn in filenames:\n                if (\n                    fn.startswith(\"MANIFEST\")\n                    or fn.endswith(\".pyc\")\n                    or fn.endswith(\".pyo\")\n                    or fn.endswith(\".pyd\")\n                ):\n                    nuke.append(dirpath + \"/\" + fn)\n\n        for fn in nuke:\n            os.unlink(fn)\n\n\nargs = {\n    \"name\": NAME,\n    \"version\": about[\"__version__\"],\n    \"description\": (\n        \"Portable file server with accelerated resumable uploads, \"\n        + \"deduplication, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, \"\n        + \"video thumbnails, audio transcoding, and write-only folders\"\n    ),\n    \"long_description\": long_description,\n    \"long_description_content_type\": \"text/markdown\",\n    \"author\": \"ed\",\n    \"author_email\": \"copyparty@ocv.me\",\n    \"url\": \"https://github.com/9001/copyparty\",\n    \"license\": \"MIT\",\n    \"classifiers\": [\n        \"Development Status :: 5 - Production/Stable\",\n        \"License :: OSI Approved :: MIT License\",\n        \"Programming Language :: Python\",\n        \"Programming Language :: Python :: 3\",\n        \"Programming Language :: Python :: 3.3\",\n        \"Programming Language :: Python :: 3.4\",\n        \"Programming Language :: Python :: 3.5\",\n        \"Programming Language :: Python :: 3.6\",\n        \"Programming Language :: Python :: 3.7\",\n        \"Programming Language :: Python :: 3.8\",\n        \"Programming Language :: Python :: 3.9\",\n        \"Programming Language :: Python :: 3.10\",\n        \"Programming Language :: Python :: 3.11\",\n        \"Programming Language :: Python :: 3.12\",\n        \"Programming Language :: Python :: 3.13\",\n        \"Programming Language :: Python :: 3.14\",\n        \"Programming Language :: Python :: Implementation :: CPython\",\n        \"Programming Language :: Python :: Implementation :: Jython\",\n        \"Programming Language :: Python :: Implementation :: PyPy\",\n        \"Operating System :: OS Independent\",\n        \"Environment :: Console\",\n        \"Environment :: No Input/Output (Daemon)\",\n        \"Intended Audience :: End Users/Desktop\",\n        \"Intended Audience :: System Administrators\",\n        \"Topic :: Communications :: File Sharing\",\n        \"Topic :: Internet :: File Transfer Protocol (FTP)\",\n        \"Topic :: Internet :: WWW/HTTP :: HTTP Servers\",\n    ],\n    \"include_package_data\": True,\n    \"data_files\": data_files,\n    \"packages\": [\n        \"copyparty\",\n        \"copyparty.bos\",\n        \"copyparty.res\",\n        \"copyparty.stolen\",\n        \"copyparty.stolen.dnslib\",\n        \"copyparty.stolen.ifaddr\",\n        \"copyparty.web\",\n        \"copyparty.web.tl\",\n        \"copyparty.web.a\",\n        \"copyparty.web.deps\",\n    ],\n    \"install_requires\": [\"jinja2\"],\n    \"extras_require\": {\n        \"all\": [\"argon2-cffi\", \"paramiko\", \"partftpy>=0.4.0\", \"Pillow\", \"pyftpdlib\", \"pyopenssl\", \"pyzmq\"],\n        \"thumbnails\": [\"Pillow\"],\n        \"thumbnails2\": [\"pyvips\"],\n        \"audiotags\": [\"mutagen\"],\n        \"sftp\": [\"paramiko\"],\n        \"ftpd\": [\"pyftpdlib\"],\n        \"ftps\": [\"pyftpdlib\", \"pyopenssl\"],\n        \"tftpd\": [\"partftpy>=0.4.0\"],\n        \"pwhash\": [\"argon2-cffi\"],\n        \"zeromq\": [\"pyzmq\"],\n    },\n    \"entry_points\": {\"console_scripts\": [\"copyparty = copyparty.__main__:main\"]},\n    \"scripts\": [\"bin/partyfuse.py\", \"bin/u2c.py\"],\n    \"cmdclass\": {\"clean2\": clean2},\n}\n\nif sys.version_info < (3, 8):\n    args[\"install_requires\"].append(\"ipaddress\")\n\nsetup(**args)\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/ptrav.py",
    "content": "#!/usr/bin/env python3\n\nimport itertools\nimport re\nimport sys\nimport time\n\nfrom copyparty.authsrv import AuthSrv\nfrom copyparty.httpcli import HttpCli\n\nfrom . import util as tu\nfrom .util import Cfg\n\natlas = [\"%\", \"25\", \"2e\", \"2f\", \".\", \"/\"]\n\n\ndef nolog(*a, **ka):\n    pass\n\n\ndef hdr(query):\n    h = \"GET /{} HTTP/1.1\\r\\nCookie: cppwd=o\\r\\nConnection: close\\r\\n\\r\\n\"\n    return h.format(query).encode(\"utf-8\")\n\n\ndef curl(args, asrv, url, binary=False):\n    conn = tu.VHttpConn(args, asrv, nolog, hdr(url))\n    HttpCli(conn).run()\n    if binary:\n        h, b = conn.s._reply.split(b\"\\r\\n\\r\\n\", 1)\n        return [h.decode(\"utf-8\"), b]\n\n    return conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n\n\ndef genlen(ubase, ntot, nth, wlen):\n    args = Cfg(v=[\"s2::r\"], a=[\"o:o\", \"x:x\"])\n    asrv = AuthSrv(args, print)\n    # h, ret = curl(args, asrv, \"hey\")\n\n    n = 0\n    t0 = time.time()\n    print(\"genlen %s nth %s\" % (wlen, nth))\n    ptn = re.compile(r\"2.2.2.2|\\.\\.\\.|///|%%%|\\.2|/2./|%\\.|/%/\")\n    for path in itertools.product(atlas, repeat=wlen):\n        if \"%\" not in path:\n            continue\n        path = \"\".join(path)\n        if ptn.search(path):\n            continue\n        n += 1\n        if n % ntot != nth:\n            continue\n        url = ubase + path + \"fa\"\n        if n % 500 == nth:\n            spd = n / (time.time() - t0)\n            print(wlen, n, int(spd), url)\n\n        hdr, r = curl(args, asrv, url)\n        if \"fgsfds\" in r:\n            with open(\"hit-%s.txt\" % (time.time()), \"w\", encoding=\"utf-8\") as f:\n                f.write(url)\n            raise Exception(\"HIT! {}\".format(url))\n\n\ndef main():\n    ubase = sys.argv[1]\n    ntot = int(sys.argv[2])\n    nth = int(sys.argv[3])\n    for wlen in range(20):\n        genlen(ubase, ntot, nth, wlen)\n\n\nif __name__ == \"__main__\":\n    try:\n        main()\n    except KeyboardInterrupt:\n        pass\n\n\n\"\"\"\nnice pypy3 -m tests.ptrav \"\" 2 0\nnice pypy3 -m tests.ptrav \"\" 2 1\nnice pypy3 -m tests.ptrav .cpr 2 0\nnice pypy3 -m tests.ptrav .cpr 2 1\n(13x faster than /scripts/test/ptrav.py)\n\"\"\"\n"
  },
  {
    "path": "tests/res/idp/1.conf",
    "content": "# -*- mode: yaml -*-\n# vim: ft=yaml:\n\n[global]\n  idp-h-usr: x-idp-user\n  idp-h-grp: x-idp-group\n\n[accounts]\n  ua: pa\n\n[/]\n  /\n  accs:\n    r: ua\n\n[/vb]\n  /b\n"
  },
  {
    "path": "tests/res/idp/2.conf",
    "content": "# -*- mode: yaml -*-\n# vim: ft=yaml:\n\n[global]\n  idp-h-usr: x-idp-user\n  idp-h-grp: x-idp-group\n\n[accounts]\n  ua: pa\n  ub: pb\n  uc: pc\n\n[groups]\n  ga: ua, ub\n\n[/]\n  /\n  accs:\n    r: @ga\n\n[/vb]\n  /b\n  accs:\n    r: @ga, ua\n\n[/vc]\n  /c\n  accs:\n    r: @ga, uc\n"
  },
  {
    "path": "tests/res/idp/3.conf",
    "content": "# -*- mode: yaml -*-\n# vim: ft=yaml:\n\n[global]\n  idp-h-usr: x-idp-user\n  idp-h-grp: x-idp-group\n\n[/vu/${u}]\n  /u${u}\n  accs:\n    r: ${u}\n\n[/vg/${g}]\n  /g${g}\n  accs:\n    r: @${g}\n"
  },
  {
    "path": "tests/res/idp/4.conf",
    "content": "# -*- mode: yaml -*-\n# vim: ft=yaml:\n\n[global]\n  idp-h-usr: x-idp-user\n  idp-h-grp: x-idp-group\n\n[accounts]\n  ua: pa\n  ub: pb\n\n[/vu/${u}]\n  /u-${u}\n  accs:\n    r: ${u}\n\n[/vg/${g}1]\n  /g1-${g}\n  accs:\n    r: @${g}\n\n[/vg/${g}2]\n  /g2-${g}\n  accs:\n    r: @${g}, ua\n"
  },
  {
    "path": "tests/res/idp/5.conf",
    "content": "# -*- mode: yaml -*-\n# vim: ft=yaml:\n\n[global]\n  idp-h-usr: x-idp-user\n  idp-h-grp: x-idp-group\n\n[/ga]\n  /ga\n  accs:\n    r: @ga\n\n[/gb]\n  /gb\n  accs:\n    r: @gb\n\n[/g]\n  /g\n  accs:\n    r: @ga, @gb\n"
  },
  {
    "path": "tests/res/idp/6.conf",
    "content": "# -*- mode: yaml -*-\n# vim: ft=yaml:\n\n[global]\n  idp-h-usr: x-idp-user\n  idp-h-grp: x-idp-group\n\n[/get/${u}]\n  /get/${u}\n  accs:\n    g: *\n    r: ${u}, @su\n    m: @su\n\n[/priv/${u}]\n  /priv/${u}\n  accs:\n    r: ${u}, @su\n    m: @su\n\n[/team/${g}/${u}]\n  /team/${g}/${u}\n  accs:\n    r: @${g}\n"
  },
  {
    "path": "tests/res/idp/7.conf",
    "content": "# -*- mode: yaml -*-\n# vim: ft=yaml:\n\n[global]\n  idp-h-usr: x-idp-user\n  idp-h-grp: x-idp-group\n\n[/u/${u}]\n  /u/${u}\n  accs:\n    r: *\n\n[/uya/${u%+ga}]\n  /uya/${u}\n  accs:\n    r: *\n\n[/uyab/${u%+ga,%+gb}]\n  /uyab/${u}\n  accs:\n    r: *\n\n[/una/${u%-ga}]\n  /una/${u}\n  accs:\n    r: *\n\n[/unab/${u%-ga,%-gb}]\n  /unab/${u}\n  accs:\n    r: *\n\n[/gya/${g%+ga}]\n  /gya/${g}\n  accs:\n    r: *\n\n[/gna/${g%-ga}]\n  /gna/${g}\n  accs:\n    r: *\n\n[/gnab/${g%-ga,%-gb}]\n  /gnab/${g}\n  accs:\n    r: *\n"
  },
  {
    "path": "tests/res/idp/8.conf",
    "content": "# -*- mode: yaml -*-\n# vim: ft=yaml:\n\n[groups]\n  ga: iua, iuab, iuabc\n  gb: iuab, iuabc, iub, iubc\n  gc: iuabc, iubc, iuc\n\n[/u/${u}]\n  /u/${u}\n  accs:\n    r: *\n\n[/uya/${u%+ga}]\n  /uya/${u}\n  accs:\n    r: *\n\n[/uyab/${u%+ga,%+gb}]\n  /uyab/${u}\n  accs:\n    r: *\n\n[/una/${u%-ga}]\n  /una/${u}\n  accs:\n    r: *\n\n[/unab/${u%-ga,%-gb}]\n  /unab/${u}\n  accs:\n    r: *\n\n[/gya/${g%+ga}]\n  /gya/${g}\n  accs:\n    r: *\n\n[/gna/${g%-ga}]\n  /gna/${g}\n  accs:\n    r: *\n\n[/gnab/${g%-ga,%-gb}]\n  /gnab/${g}\n  accs:\n    r: *\n"
  },
  {
    "path": "tests/run.py",
    "content": "#!/usr/bin/env python3\n\nimport runpy\nimport sys\n\nhost = sys.argv[1]\nsys.argv = sys.argv[:1] + sys.argv[2:]\nsys.path.insert(0, \".\")\n\n\ndef rp():\n    runpy.run_module(\"unittest\", run_name=\"__main__\")\n\n\nif host == \"vmprof\":\n    rp()\n\nelif host == \"cprofile\":\n    import cProfile\n    import pstats\n\n    log_fn = \"cprofile.log\"\n    cProfile.run(\"rp()\", log_fn)\n    p = pstats.Stats(log_fn)\n    p.sort_stats(pstats.SortKey.CUMULATIVE).print_stats(64)\n\n\n\"\"\"\npython3.9 tests/run.py cprofile -v tests/test_httpcli.py\n\npython3.9 -m pip install --user vmprof\npython3.9 -m vmprof --lines -o vmprof.log tests/run.py vmprof -v tests/test_httpcli.py\n\"\"\"\n"
  },
  {
    "path": "tests/test_cp.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport os\nimport shutil\nimport tempfile\nimport unittest\nfrom itertools import product\n\nfrom copyparty.authsrv import AuthSrv\nfrom copyparty.httpcli import HttpCli\nfrom tests import util as tu\nfrom tests.util import Cfg\n\n\nclass TestDedup(tu.TC):\n    def setUp(self):\n        self.td = tu.get_ramdisk()\n\n    def tearDown(self):\n        if self.conn:\n            self.conn.shutdown()\n        os.chdir(tempfile.gettempdir())\n        shutil.rmtree(self.td)\n\n    def reset(self):\n        td = os.path.join(self.td, \"vfs\")\n        if os.path.exists(td):\n            shutil.rmtree(td)\n        os.mkdir(td)\n        os.chdir(td)\n        for a in \"abc\":\n            os.mkdir(a)\n            for b in \"fg\":\n                d = \"%s/%s%s\" % (a, a, b)\n                os.mkdir(d)\n                for fn in \"x\":\n                    fp = \"%s/%s%s%s\" % (d, a, b, fn)\n                    with open(fp, \"wb\") as f:\n                        f.write(fp.encode(\"utf-8\"))\n        return td\n\n    def cinit(self):\n        if self.conn:\n            self.fstab = self.conn.hsrv.hub.up2k.fstab\n            self.conn.hsrv.hub.up2k.shutdown()\n        self.asrv = AuthSrv(self.args, self.log)\n        self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b\"\", True)\n        if self.fstab:\n            self.conn.hsrv.hub.up2k.fstab = self.fstab\n\n    def test(self):\n        tc_dedup = [\"sym\", \"no\"]\n        vols = [\".::A\", \"a/af:a/af:r\", \"b:a/b:r\"]\n        tcs = [\n            \"/a?copy=/c/a /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /b/bf/bfx /b/bg/bgx /c/a/af/afx /c/a/ag/agx /c/a/b/bf/bfx /c/a/b/bg/bgx /c/cf/cfx /c/cg/cgx\",\n            \"/b?copy=/d /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /b/bf/bfx /b/bg/bgx /c/cf/cfx /c/cg/cgx /d/bf/bfx /d/bg/bgx\",\n            \"/b/bf?copy=/d /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /b/bf/bfx /b/bg/bgx /c/cf/cfx /c/cg/cgx /d/bfx\",\n            \"/a/af?copy=/d /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /b/bf/bfx /b/bg/bgx /c/cf/cfx /c/cg/cgx /d/afx\",\n            \"/a/af?copy=/ /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /afx /b/bf/bfx /b/bg/bgx /c/cf/cfx /c/cg/cgx\",\n            \"/a/af/afx?copy=/afx /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /afx /b/bf/bfx /b/bg/bgx /c/cf/cfx /c/cg/cgx\",\n        ]\n\n        self.conn = None\n        self.fstab = None\n        for dedup, act_exp in product(tc_dedup, tcs):\n            action, expect = act_exp.split(\" \", 1)\n            t = \"dedup:%s  action:%s\" % (dedup, action)\n            print(\"\\n\\n\\033[0;7m# \", t, \"\\033[0m\")\n\n            ka = {\"dav_inf\": True}\n            if dedup == \"hard\":\n                ka[\"hardlink\"] = True\n            elif dedup == \"no\":\n                ka[\"no_dedup\"] = True\n\n            self.args = Cfg(v=vols, a=[], **ka)\n            self.reset()\n            self.cinit()\n\n            self.do_cp(action)\n            zs = self.propfind()\n\n            fns = \" \".join(zs[1])\n            self.assertEqual(expect, fns)\n\n    def do_cp(self, action):\n        hdr = \"POST %s HTTP/1.1\\r\\nConnection: close\\r\\nContent-Length: 0\\r\\n\\r\\n\"\n        buf = (hdr % (action,)).encode(\"utf-8\")\n        print(\"CP [%s]\" % (action,))\n        HttpCli(self.conn.setbuf(buf)).run()\n        ret = self.conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n        print(\"CP <-- \", ret)\n        self.assertStart(\"HTTP/1.1 201 Created\\r\", ret[0])\n        self.assertEqual(\"k\\r\\n\", ret[1])\n        return ret\n\n    def propfind(self):\n        h = \"PROPFIND / HTTP/1.1\\r\\nConnection: close\\r\\n\\r\\n\"\n        HttpCli(self.conn.setbuf(h.encode(\"utf-8\"))).run()\n        h, t = self.conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n        fns = t.split(\"<D:response><D:href>\")[1:]\n        fns = [x.split(\"</D\", 1)[0] for x in fns]\n        fns = [x for x in fns if not x.endswith(\"/\")]\n        fns.sort()\n        return h, fns\n\n    def log(self, src, msg, c=0):\n        print(msg)\n"
  },
  {
    "path": "tests/test_dedup.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport json\nimport os\nimport shutil\nimport tempfile\nimport unittest\nfrom itertools import product\n\nfrom copyparty.authsrv import AuthSrv\nfrom copyparty.httpcli import HttpCli\nfrom tests import util as tu\nfrom tests.util import Cfg\n\n\nclass TestDedup(tu.TC):\n    def setUp(self):\n        self.td = tu.get_ramdisk()\n\n        # (data, chash, wark)\n        self.files = [\n            (\n                \"one\",\n                \"BfcDQQeKz2oG1CPSFyD5ZD1flTYm2IoCY23DqeeVgq6w\",\n                \"XMbpLRqVdtGmgggqjUI6uSoNMTqZVX4K6zr74XA1BRKc\",\n            ),\n            (\n                \"two\",\n                \"ko1Q0eJNq3zKYs_oT83Pn8aVFgonj5G1wK8itwnYL4qj\",\n                \"fxvihWlnQIbVbUPr--TxyV41913kPLhXPD1ngXYxDfou\",\n            ),\n        ]\n\n    def tearDown(self):\n        if self.conn:\n            self.conn.shutdown()\n        os.chdir(tempfile.gettempdir())\n        shutil.rmtree(self.td)\n\n    def reset(self):\n        td = os.path.join(self.td, \"vfs\")\n        if os.path.exists(td):\n            shutil.rmtree(td)\n        os.mkdir(td)\n        os.chdir(td)\n        return td\n\n    def cinit(self):\n        if self.conn:\n            self.fstab = self.conn.hsrv.hub.up2k.fstab\n            self.conn.hsrv.hub.up2k.shutdown()\n        self.asrv = AuthSrv(self.args, self.log)\n        self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b\"\", True)\n        if self.fstab:\n            self.conn.hsrv.hub.up2k.fstab = self.fstab\n\n    def test_a(self):\n        file404 = \"\\nJ2EOT\"\n        f1, f2 = self.files\n        fns = (\"f1\", \"f2\", \"f3\")\n        dn = \"d\"\n\n        self.conn = None\n        self.fstab = None\n        for e2d in [True, False]:\n            self.args = Cfg(v=[\".::A\"], a=[], e2d=e2d)\n            self.reset()\n            self.cinit()\n\n            # dupes in parallel\n            sfn, hs = self.do_post_hs(dn, fns[0], f1, True)\n            for fn in fns[1:]:\n                h, b = self.handshake(dn, fn, f1)\n                self.assertStart(\"HTTP/1.1 422 Unpro\", h)\n                self.assertIn(\"a different location;\", b)\n            self.do_post_data(dn, fns[0], f1, True, sfn, hs)\n            if not e2d:\n                # dupesched is e2d only; hs into existence\n                for fn, data in zip(fns, (f1[0], file404, file404)):\n                    h, b = self.curl(\"%s/%s\" % (\"d\", fn))\n                    self.assertEqual(b, data)\n                for fn in fns[1:]:\n                    h, b = self.do_post_hs(dn, fn, f1, False)\n            for fn in fns:\n                h, b = self.curl(\"%s/%s\" % (\"d\", fn))\n                self.assertEqual(b, f1[0])\n\n            if not e2d:\n                continue\n\n            # overwrite file\n            sfn, hs = self.do_post_hs(dn, fns[0], f2, True, replace=True)\n            self.do_post_data(dn, fns[0], f2, True, sfn, hs)\n            for fn, f in zip(fns, (f2, f1, f1)):\n                h, b = self.curl(\"%s/%s\" % (\"d\", fn))\n                self.assertEqual(b, f[0])\n\n    def test(self):\n        quick = True  # sufficient for regular smoketests\n        # quick = False\n\n        dirnames = [\"d1\", \"d2\"]\n        filenames = [\"f1\", \"f2\"]\n        files = self.files\n\n        self.ctr = 336 if quick else 2016  # estimated total num uploads\n        self.conn = None\n        self.fstab = None\n        for e2d in [True, False]:\n            self.args = Cfg(v=[\".::A\"], a=[], e2d=e2d)\n            for cm1 in product(dirnames, filenames, files):\n                for cm2 in product(dirnames, filenames, files):\n                    if cm1 == cm2:\n                        continue\n                    for cm3 in product(dirnames, filenames, files):\n                        if cm3 in (cm1, cm2):\n                            continue\n\n                        f1 = cm1[2]\n                        f2 = cm2[2]\n                        f3 = cm3[2]\n                        if not e2d:\n                            rms = [-1]\n                        elif f1 == f2:\n                            if f1 == f3:\n                                rms = [0, 1, 2]\n                            else:\n                                rms = [0, 1]\n                        elif f1 == f3:\n                            rms = [0, 2]\n                        else:\n                            rms = [1, 2]\n\n                        for rm in rms:\n                            self.do_tc(cm1, cm2, cm3, rm)\n\n                        if quick:\n                            break\n\n    def do_tc(self, cm1, cm2, cm3, irm):\n        dn1, fn1, f1 = cm1\n        dn2, fn2, f2 = cm2\n        dn3, fn3, f3 = cm3\n\n        self.reset()\n        self.cinit()\n\n        fn1 = self.do_post(dn1, fn1, f1, True)\n        fn2 = self.do_post(dn2, fn2, f2, False)\n        fn3 = self.do_post(dn3, fn3, f3, False)\n\n        if irm < 0:\n            return\n\n        cms = [(dn1, fn1, f1), (dn2, fn2, f2), (dn3, fn3, f3)]\n        rm = cms[irm]\n        dn, fn, _ = rm\n        h, b = self.curl(\"%s/%s?delete\" % (dn, fn), meth=\"POST\")\n        self.assertStart(\"HTTP/1.1 200 OK\", h)\n        self.assertIn(\"deleted 1 files\", b)\n        h, b = self.curl(\"%s/%s\" % (dn, fn))\n        self.assertStart(\"HTTP/1.1 404 Not Fo\", h)\n        for cm in cms:\n            if cm == rm:\n                continue\n            dn, fn, f = cm\n            h, b = self.curl(\"%s/%s\" % (dn, fn))\n            self.assertEqual(b, f[0])\n\n    def do_post(self, dn, fn, fi, first):\n        print(\"\\n\\n# do_post\", self.ctr, repr((dn, fn, fi, first)))\n        self.ctr -= 1\n        sfn, hs = self.do_post_hs(dn, fn, fi, first)\n        return self.do_post_data(dn, fn, fi, first, sfn, hs)\n\n    def do_post_hs(self, dn, fn, fi, first, replace=False):\n        h, b = self.handshake(dn, fn, fi, replace=replace)\n        hs = json.loads(b)\n        self.assertEqual(hs[\"wark\"], fi[2])\n\n        sfn = hs[\"name\"]\n        if sfn == fn:\n            print(\"using original name \" + fn)\n        else:\n            print(fn + \" got renamed to \" + sfn)\n            if first:\n                raise Exception(\"wait what\")\n\n        return sfn, hs\n\n    def do_post_data(self, dn, fn, fi, first, sfn, hs):\n        data, chash, wark = fi\n        if hs[\"hash\"]:\n            self.assertEqual(hs[\"hash\"][0], chash)\n            self.put_chunk(dn, wark, chash, data)\n        elif first:\n            raise Exception(\"found first; %r, %r\" % ((dn, fn, fi), hs))\n\n        h, b = self.curl(\"%s/%s\" % (dn, sfn))\n        self.assertEqual(b, data)\n        return sfn\n\n    def handshake(self, dn, fn, fi, replace=False):\n        hdr = \"POST /%s/ HTTP/1.1\\r\\nConnection: close\\r\\nContent-Type: text/plain\\r\\nContent-Length: %d\\r\\n\\r\\n\"\n        msg = {\"name\": fn, \"size\": 3, \"lmod\": 1234567890, \"life\": 0, \"hash\": [fi[1]]}\n        if replace:\n            msg[\"replace\"] = True\n        buf = json.dumps(msg).encode(\"utf-8\")\n        buf = (hdr % (dn, len(buf))).encode(\"utf-8\") + buf\n        # print(\"HS -->\", buf)\n        HttpCli(self.conn.setbuf(buf)).run()\n        ret = self.conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n        # print(\"HS <--\", ret)\n        return ret\n\n    def put_chunk(self, dn, wark, chash, data):\n        msg = [\n            \"POST /%s/ HTTP/1.1\" % (dn,),\n            \"Connection: close\",\n            \"Content-Type: application/octet-stream\",\n            \"Content-Length: 3\",\n            \"X-Up2k-Hash: \" + chash,\n            \"X-Up2k-Wark: \" + wark,\n            \"\",\n            data,\n        ]\n        buf = \"\\r\\n\".join(msg).encode(\"utf-8\")\n        # print(\"PUT -->\", buf)\n        HttpCli(self.conn.setbuf(buf)).run()\n        ret = self.conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n        self.assertEqual(ret[1], \"thank\")\n\n    def curl(self, url, binary=False, meth=None):\n        h = \"%s /%s HTTP/1.1\\r\\nConnection: close\\r\\n\\r\\n\"\n        h = h % (meth or \"GET\", url)\n        # print(\"CURL -->\", url)\n        HttpCli(self.conn.setbuf(h.encode(\"utf-8\"))).run()\n        if binary:\n            h, b = self.conn.s._reply.split(b\"\\r\\n\\r\\n\", 1)\n            return [h.decode(\"utf-8\"), b]\n\n        return self.conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n\n    def log(self, src, msg, c=0):\n        print(msg)\n"
  },
  {
    "path": "tests/test_dots.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport io\nimport json\nimport os\nimport shutil\nimport tarfile\nimport tempfile\nimport unittest\n\nfrom copyparty.authsrv import AuthSrv\nfrom copyparty.httpcli import HttpCli\nfrom copyparty.u2idx import U2idx\nfrom copyparty.up2k import Up2k\nfrom tests import util as tu\nfrom tests.util import J2_ENV, Cfg\n\ntry:\n    from typing import Optional\nexcept:\n    pass\n\n\ndef hdr(query, uname, extra=\"\"):\n    h = \"GET /%s HTTP/1.1\\r\\nPW: %s\\r\\nConnection: close\\r\\n%s\\r\\n\"\n    return (h % (query, uname, extra)).encode(\"utf-8\")\n\n\nclass TestDots(unittest.TestCase):\n    def __init__(self, *a, **ka):\n        super(TestDots, self).__init__(*a, **ka)\n        self.is_dut = True\n        self.j2_brw = None\n\n    def setUp(self):\n        self.conn: Optional[tu.VHttpConn] = None\n        self.td = tu.get_ramdisk()\n\n    def tearDown(self):\n        if self.conn:\n            self.conn.shutdown()\n        os.chdir(tempfile.gettempdir())\n        shutil.rmtree(self.td)\n\n    def cinit(self):\n        if self.conn:\n            self.conn.shutdown()\n            self.conn = None\n        self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b\"\")\n\n        if not self.j2_brw:\n            zs = os.path.dirname(__file__)\n            with open(\"%s/../copyparty/web/browser.html\" % (zs,), \"rb\") as f:\n                zs = f.read().decode(\"utf-8\")\n            self.j2_brw = J2_ENV.from_string(zs)\n        self.conn.hsrv.j2[\"browser\"] = self.j2_brw\n\n    def test_dots(self):\n        td = os.path.join(self.td, \"vfs\")\n        os.mkdir(td)\n        os.chdir(td)\n\n        # topDir  volA  volA/*dirA  .volB  .volB/*dirB\n        spaths = \" t .t a a/da a/.da .b .b/db .b/.db\"\n        for n, dirpath in enumerate(spaths.split(\" \")):\n            if dirpath:\n                os.makedirs(dirpath)\n\n            for pfx in \"f\", \".f\":\n                filepath = pfx + str(n)\n                if dirpath:\n                    filepath = os.path.join(dirpath, filepath)\n\n                with open(filepath, \"wb\") as f:\n                    f.write(filepath.encode(\"utf-8\"))\n\n        vcfg = [\".::r,u1:r.,u2\", \"a:a:r,u1:r,u2\", \".b:.b:r.,u1:r,u2\"]\n        self.args = Cfg(v=vcfg, a=[\"u1:u1\", \"u2:u2\"], e2dsa=True)\n        self.asrv = AuthSrv(self.args, self.log)\n        self.cinit()\n\n        self.assertEqual(self.tardir(\"\", \"u1\"), \"f0 t/f1 a/f3 a/da/f4\")\n        self.assertEqual(self.tardir(\".t\", \"u1\"), \"f2\")\n        self.assertEqual(self.tardir(\".b\", \"u1\"), \".f6 f6 .db/.f8 .db/f8 db/.f7 db/f7\")\n\n        zs = \".f0 f0 .t/.f2 .t/f2 t/.f1 t/f1 .b/f6 .b/db/f7 a/f3 a/da/f4\"\n        self.assertEqual(self.tardir(\"\", \"u2\"), zs)\n\n        self.assertEqual(self.curl(\"?tar\", \"x\")[1][:17], \"\\nJ2EOT\")\n\n        ##\n        ## search\n\n        up2k = Up2k(self)\n        u2idx = U2idx(self)\n        allvols = list(self.asrv.vfs.all_vols.values())\n\n        x = u2idx.search(\"u1\", allvols, \"\", 999)\n        x = \" \".join(sorted([x[\"rp\"] for x in x[0]]))\n        # u1 can see dotfiles in volB so they should be included\n        xe = \".b/.db/.f8 .b/.db/f8 .b/.f6 .b/db/.f7 .b/db/f7 .b/f6 a/da/f4 a/f3 f0 t/f1\"\n        self.assertEqual(x, xe)\n\n        x = u2idx.search(\"u2\", allvols, \"\", 999)\n        x = \" \".join(sorted([x[\"rp\"] for x in x[0]]))\n        self.assertEqual(x, \".f0 .t/.f2 .t/f2 a/da/f4 a/f3 f0 t/.f1 t/f1\")\n\n        u2idx.shutdown()\n        self.args = Cfg(v=vcfg, a=[\"u1:u1\", \"u2:u2\"], dotsrch=False, e2d=True)\n        self.asrv = AuthSrv(self.args, self.log)\n        u2idx = U2idx(self)\n        self.cinit()\n\n        x = u2idx.search(\"u1\", self.asrv.vfs.all_vols.values(), \"\", 999)\n        x = \" \".join(sorted([x[\"rp\"] for x in x[0]]))\n        # u1 can see dotfiles in volB so they should be included\n        xe = \"a/da/f4 a/f3 f0 t/f1\"\n        self.assertEqual(x, xe)\n        u2idx.shutdown()\n        up2k.shutdown()\n\n        ##\n        ## dirkeys\n\n        os.mkdir(\"v\")\n        with open(\"v/f1.txt\", \"wb\") as f:\n            f.write(b\"a\")\n        os.rename(\"a\", \"v/a\")\n        os.rename(\".b\", \"v/.b\")\n\n        vcfg = [\n            \".::r.,u1:g,u2:c,dks\",\n            \"v/a:v/a:r.,u1:g,u2:c,dks\",\n            \"v/.b:v/.b:r.,u1:g,u2:c,dks\",\n        ]\n        self.args = Cfg(v=vcfg, a=[\"u1:u1\", \"u2:u2\"])\n        self.asrv = AuthSrv(self.args, self.log)\n        self.cinit()\n\n        zj = json.loads(self.curl(\"?ls\", \"u1\")[1])\n        url = \"?k=\" + zj[\"dk\"]\n        # should descend into folders, but not other volumes:\n        self.assertEqual(self.tardir(url, \"u2\"), \"f0 t/f1 v/f1.txt\")\n\n        zj = json.loads(self.curl(\"v?ls\", \"u1\")[1])\n        url = \"v?k=\" + zj[\"dk\"]\n        self.assertEqual(self.tarsel(url, \"u2\", [\"f1.txt\", \"a\", \".b\"]), \"f1.txt\")\n\n        shutil.rmtree(\"v\")\n\n    def test_dk_fk(self):\n        # python3 -m unittest tests.test_dots.TestDots.test_dk_fk\n\n        td = os.path.join(self.td, \"vfs\")\n        os.mkdir(td)\n        os.chdir(td)\n\n        vcfg = []\n        zs = \"dk dks=12 dky fk fka dk,fk dks=12:c,fk\"\n        for k2 in zs.split():\n            k = k2.replace(\"=12\", \"\").replace(\":c,\", \",\")\n            vcfg += [\"{0}:{0}:r.,u1:g,u2:c,{1}\".format(k, k2)]\n            zs = \"%s/s1/s2\" % (k,)\n            os.makedirs(zs)\n\n            with open(\"%s/f.t1\" % (k,), \"wb\") as f:\n                f.write(b\"f1\")\n\n            with open(\"%s/s1/f.t2\" % (k,), \"wb\") as f:\n                f.write(b\"f2\")\n\n            with open(\"%s/s1/s2/f.t3\" % (k,), \"wb\") as f:\n                f.write(b\"f3\")\n\n        self.args = Cfg(v=vcfg, a=[\"u1:u1\", \"u2:u2\"])\n        self.asrv = AuthSrv(self.args, self.log)\n        self.cinit()\n\n        dk = {}\n        for d in \"dk dks dk,fk dks,fk\".split():\n            zj = json.loads(self.curl(\"%s?ls\" % (d,), \"u1\")[1])\n            dk[d] = zj[\"dk\"]\n\n        ##\n        ## dk\n\n        # should not be able to access dk with wrong dirkey,\n        zs = self.curl(\"dk?ls&k=%s\" % (dk[\"dks\"]), \"u2\")[1]\n        self.assertEqual(zs, \"\\nJ2EOT\")\n        # so use the right key\n        zs = self.curl(\"dk?ls&k=%s\" % (dk[\"dk\"]), \"u2\")[1]\n        zj = json.loads(zs)\n        self.assertEqual(len(zj[\"dirs\"]), 0)\n        self.assertEqual(len(zj[\"files\"]), 1)\n        self.assertEqual(zj[\"files\"][0][\"href\"], \"f.t1\")\n\n        ##\n        ## dk thumbs\n\n        self.assertIn('\">folder</text>', self.curl(\"dk?th=x\", \"u1\")[1])\n        self.assertIn('\">e403</text>', self.curl(\"dk?th=x\", \"u2\")[1])\n\n        zs = \"dk?th=x&k=%s\" % (dk[\"dks\"])\n        self.assertIn('\">e403</text>', self.curl(zs, \"u2\")[1])\n\n        zs = \"dk?th=x&k=%s\" % (dk[\"dk\"])\n        self.assertIn('\">folder</text>', self.curl(zs, \"u2\")[1])\n\n        # fk not enabled, so this should work\n        self.assertIn('\">t1</text>', self.curl(\"dk/f.t1?th=x\", \"u2\")[1])\n        self.assertIn('\">t2</text>', self.curl(\"dk/s1/f.t2?th=x\", \"u2\")[1])\n\n        ##\n        ## dks\n\n        # should not be able to access dks with wrong dirkey,\n        zs = self.curl(\"dks?ls&k=%s\" % (dk[\"dk\"]), \"u2\")[1]\n        self.assertEqual(zs, \"\\nJ2EOT\")\n        # so use the right key\n        zs = self.curl(\"dks?ls&k=%s\" % (dk[\"dks\"]), \"u2\")[1]\n        zj = json.loads(zs)\n        self.assertEqual(len(zj[\"dirs\"]), 1)\n        self.assertEqual(len(zj[\"files\"]), 1)\n        self.assertEqual(zj[\"files\"][0][\"href\"], \"f.t1\")\n        # dks should return correct dirkey of subfolders;\n        s1 = zj[\"dirs\"][0][\"href\"]\n        self.assertEqual(s1.split(\"/\")[0], \"s1\")\n        zs = self.curl(\"dks/%s&ls\" % (s1), \"u2\")[1]\n        self.assertIn('\"s2/?k=', zs)\n\n        # parent key should not exist in response\n        self.assertNotIn(dk[\"dks\"], zs)\n        # and not in html either\n        for ck in (\"\", \"Cookie: js=y\\r\\n\"):\n            zb = hdr(\"dks/%s\" % (s1), \"u2\", ck)\n            zs = self.curl(\"\", \"\", False, zb)[1]\n            self.assertNotIn(dk[\"dks\"], zs)\n            self.assertIn('\"s2/?k=', zs)\n\n        ##\n        ## dks thumbs\n\n        self.assertIn('\">folder</text>', self.curl(\"dks?th=x\", \"u1\")[1])\n        self.assertIn('\">e403</text>', self.curl(\"dks?th=x\", \"u2\")[1])\n\n        zs = \"dks?th=x&k=%s\" % (dk[\"dk\"])\n        self.assertIn('\">e403</text>', self.curl(zs, \"u2\")[1])\n\n        zs = \"dks?th=x&k=%s\" % (dk[\"dks\"])\n        self.assertIn('\">folder</text>', self.curl(zs, \"u2\")[1])\n\n        # fk not enabled, so this should work\n        self.assertIn('\">t1</text>', self.curl(\"dks/f.t1?th=x\", \"u2\")[1])\n        self.assertIn('\">t2</text>', self.curl(\"dks/s1/f.t2?th=x\", \"u2\")[1])\n\n        ##\n        ## dky\n\n        # doesn't care about keys\n        zs = self.curl(\"dky?ls&k=ok\", \"u2\")[1]\n        self.assertEqual(zs, self.curl(\"dky?ls\", \"u2\")[1])\n        zj = json.loads(zs)\n        self.assertEqual(len(zj[\"dirs\"]), 0)\n        self.assertEqual(len(zj[\"files\"]), 1)\n        self.assertEqual(zj[\"files\"][0][\"href\"], \"f.t1\")\n\n        ##\n        ## dky thumbs\n\n        self.assertIn('\">folder</text>', self.curl(\"dky?th=x\", \"u1\")[1])\n        self.assertIn('\">folder</text>', self.curl(\"dky?th=x\", \"u2\")[1])\n\n        zs = \"dky?th=x&k=%s\" % (dk[\"dk\"])\n        self.assertIn('\">folder</text>', self.curl(zs, \"u2\")[1])\n\n        # fk not enabled, so this should work\n        self.assertIn('\">t1</text>', self.curl(\"dky/f.t1?th=x\", \"u2\")[1])\n        self.assertIn('\">t2</text>', self.curl(\"dky/s1/f.t2?th=x\", \"u2\")[1])\n\n        ##\n        ## dk+fk\n\n        # should not be able to access dk with wrong dirkey,\n        zs = self.curl(\"dk,fk?ls&k=%s\" % (dk[\"dk\"]), \"u2\")[1]\n        self.assertEqual(zs, \"\\nJ2EOT\")\n        # so use the right key\n        zs = self.curl(\"dk,fk?ls&k=%s\" % (dk[\"dk,fk\"]), \"u2\")[1]\n        zj = json.loads(zs)\n        self.assertEqual(len(zj[\"dirs\"]), 0)\n        self.assertEqual(len(zj[\"files\"]), 1)\n        self.assertEqual(zj[\"files\"][0][\"href\"][:7], \"f.t1?k=\")\n\n        ##\n        ## dk+fk thumbs\n\n        self.assertIn('\">folder</text>', self.curl(\"dk,fk?th=x\", \"u1\")[1])\n        self.assertIn('\">e403</text>', self.curl(\"dk,fk?th=x\", \"u2\")[1])\n\n        zs = \"dk,fk?th=x&k=%s\" % (dk[\"dk\"])\n        self.assertIn('\">e403</text>', self.curl(zs, \"u2\")[1])\n\n        zs = \"dk,fk?th=x&k=%s\" % (dk[\"dk,fk\"])\n        self.assertIn('\">folder</text>', self.curl(zs, \"u2\")[1])\n\n        # fk enabled, so this should fail\n        self.assertIn('\">e403</text>', self.curl(\"dk,fk/f.t1?th=x\", \"u2\")[1])\n        self.assertIn('\">e403</text>', self.curl(\"dk,fk/s1/f.t2?th=x\", \"u2\")[1])\n\n        # but dk should return correct filekeys, so try that\n        zs = \"dk,fk/%s&th=x\" % (zj[\"files\"][0][\"href\"])\n        self.assertIn('\">t1</text>', self.curl(zs, \"u2\")[1])\n\n        ##\n        ## dks+fk\n\n        # should not be able to access dk with wrong dirkey,\n        zs = self.curl(\"dks,fk?ls&k=%s\" % (dk[\"dk\"]), \"u2\")[1]\n        self.assertEqual(zs, \"\\nJ2EOT\")\n        # so use the right key\n        zs = self.curl(\"dks,fk?ls&k=%s\" % (dk[\"dks,fk\"]), \"u2\")[1]\n        zj = json.loads(zs)\n        self.assertEqual(len(zj[\"dirs\"]), 1)\n        self.assertEqual(len(zj[\"files\"]), 1)\n        self.assertEqual(zj[\"dirs\"][0][\"href\"][:6], \"s1/?k=\")\n        self.assertEqual(zj[\"files\"][0][\"href\"][:7], \"f.t1?k=\")\n\n        ##\n        ## dks+fk thumbs\n\n        self.assertIn('\">folder</text>', self.curl(\"dks,fk?th=x\", \"u1\")[1])\n        self.assertIn('\">e403</text>', self.curl(\"dks,fk?th=x\", \"u2\")[1])\n\n        zs = \"dks,fk?th=x&k=%s\" % (dk[\"dk\"])\n        self.assertIn('\">e403</text>', self.curl(zs, \"u2\")[1])\n\n        zs = \"dks,fk?th=x&k=%s\" % (dk[\"dks,fk\"])\n        self.assertIn('\">folder</text>', self.curl(zs, \"u2\")[1])\n\n        # subdir s1 without key\n        zs = \"dks,fk/s1/?th=x\"\n        self.assertIn('\">e403</text>', self.curl(zs, \"u2\")[1])\n\n        # subdir s1 with bad key\n        zs = \"dks,fk/s1/?th=x&k=no\"\n        self.assertIn('\">e403</text>', self.curl(zs, \"u2\")[1])\n\n        # subdir s1 with correct key\n        zs = \"dks,fk/%s&th=x\" % (zj[\"dirs\"][0][\"href\"])\n        self.assertIn('\">folder</text>', self.curl(zs, \"u2\")[1])\n\n        # fk enabled, so this should fail\n        self.assertIn('\">e403</text>', self.curl(\"dks,fk/f.t1?th=x\", \"u2\")[1])\n        self.assertIn('\">e403</text>', self.curl(\"dks,fk/s1/f.t2?th=x\", \"u2\")[1])\n\n        # but dk should return correct filekeys, so try that\n        zs = \"dks,fk/%s&th=x\" % (zj[\"files\"][0][\"href\"])\n        self.assertIn('\">t1</text>', self.curl(zs, \"u2\")[1])\n\n        # subdir\n        self.assertIn('\">e403</text>', self.curl(\"dks,fk/s1/?th=x\", \"u2\")[1])\n        self.assertEqual(\"\\nJ2EOT\", self.curl(\"dks,fk/s1/?ls\", \"u2\")[1])\n        zs = \"dks,fk/s1%s&th=x\" % (zj[\"files\"][0][\"href\"])\n        zs = self.curl(\"dks,fk?ls&k=%s\" % (dk[\"dks,fk\"]), \"u2\")[1]\n        zj = json.loads(zs)\n        url = \"dks,fk/%s\" % zj[\"dirs\"][0][\"href\"]\n        self.assertIn('\"files\"', self.curl(url + \"&ls\", \"u2\")[1])\n        self.assertEqual(\"\\nJ2EOT\", self.curl(url + \"x&ls\", \"u2\")[1])\n\n    def tardir(self, url, uname):\n        top = url.split(\"?\")[0]\n        top = (\"top\" if not top else top.lstrip(\".\").split(\"/\")[0]) + \"/\"\n        url += (\"&\" if \"?\" in url else \"?\") + \"tar\"\n        h, b = self.curl(url, uname, True)\n        tar = tarfile.open(fileobj=io.BytesIO(b), mode=\"r|\").getnames()\n        if len(tar) != len([x for x in tar if x.startswith(top)]):\n            raise Exception(\"bad-prefix:\", tar)\n        return \" \".join([x[len(top) :] for x in tar])\n\n    def tarsel(self, url, uname, sel):\n        url += (\"&\" if \"?\" in url else \"?\") + \"tar\"\n        zs = '--XD\\r\\nContent-Disposition: form-data; name=\"act\"\\r\\n\\r\\nzip\\r\\n--XD\\r\\nContent-Disposition: form-data; name=\"files\"\\r\\n\\r\\n'\n        zs += \"\\r\\n\".join(sel) + \"\\r\\n--XD--\\r\\n\"\n        zb = zs.encode(\"utf-8\")\n        hdr = \"POST /%s HTTP/1.1\\r\\nPW: %s\\r\\nConnection: close\\r\\nContent-Type: multipart/form-data; boundary=XD\\r\\nContent-Length: %d\\r\\n\\r\\n\"\n        req = (hdr % (url, uname, len(zb))).encode(\"utf-8\") + zb\n        h, b = self.curl(\"\", \"\", True, req)\n        tar = tarfile.open(fileobj=io.BytesIO(b), mode=\"r|\").getnames()\n        return \" \".join(tar)\n\n    def curl(self, url, uname, binary=False, req=b\"\"):\n        req = req or hdr(url.replace(\"th=x\", \"th=j\"), uname)\n        conn = self.conn.setbuf(req)\n        HttpCli(conn).run()\n        if binary:\n            h, b = conn.s._reply.split(b\"\\r\\n\\r\\n\", 1)\n            return [h.decode(\"utf-8\"), b]\n\n        return conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n\n    def log(self, src, msg, c=0):\n        print(msg, \"\\033[0m\")\n"
  },
  {
    "path": "tests/test_dxml.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport re\nimport unittest\nfrom xml.etree import ElementTree as ET\n\nfrom copyparty.dxml import BadXML, mkenod, mktnod, parse_xml\n\nET.register_namespace(\"D\", \"DAV:\")\n\n\ndef _parse(txt):\n    try:\n        parse_xml(txt)\n        raise Exception(\"unsafe\")\n    except BadXML:\n        pass\n\n\nclass TestDXML(unittest.TestCase):\n    def test_qbe(self):\n        # allowed by default; verify that we stopped it\n        txt = r\"\"\"<!DOCTYPE qbe [\n<!ENTITY a \"nice_bakuretsu\">\n]>\n<l>&a;&a;&a;&a;&a;&a;&a;&a;&a;</l>\"\"\"\n        _parse(txt)\n        ET.fromstring(txt)\n\n    def test_ent_file(self):\n        # NOT allowed by default; should still be blocked\n        txt = r\"\"\"<!DOCTYPE ext [\n<!ENTITY ee SYSTEM \"file:///bin/bash\">\n]>\n<root>&ee;</root>\"\"\"\n        _parse(txt)\n        try:\n            ET.fromstring(txt)\n            raise Exception(\"unsafe2\")\n        except ET.ParseError:\n            pass\n\n    def test_ent_ext(self):\n        # NOT allowed by default; should still be blocked\n        txt = r\"\"\"<!DOCTYPE ext [\n<!ENTITY ee SYSTEM \"http://example.com/a.xml\">\n]>\n<root>&ee;</root>\"\"\"\n        _parse(txt)\n\n    def test_dtd(self):\n        # allowed by default; verify that we stopped it\n        txt = r\"\"\"<!DOCTYPE d SYSTEM \"a.dtd\">\n<root>a</root>\"\"\"\n        _parse(txt)\n        ET.fromstring(txt)\n\n    ##\n    ## end of negative/security tests; the rest is functional\n    ##\n\n    def test3(self):\n        txt = r\"\"\"<?xml version=\"1.0\" ?>\n<propfind xmlns=\"DAV:\">\n    <prop>\n        <name/>\n        <href/>\n    </prop>\n</propfind>\n\"\"\"\n        txt = txt.replace(\"\\n\", \"\\r\\n\")\n        ET.fromstring(txt)\n        el = parse_xml(txt)\n        self.assertListEqual(\n            [y.tag for y in el.findall(r\"./{DAV:}prop/*\")],\n            [r\"{DAV:}name\", r\"{DAV:}href\"],\n        )\n\n    def test4(self):\n        txt = r\"\"\"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propertyupdate xmlns:D=\"DAV:\" xmlns:Z=\"urn:schemas-microsoft-com:\">\n    <D:set>\n        <D:prop>\n            <Z:Win32CreationTime>Thu, 20 Oct 2022 02:16:33 GMT</Z:Win32CreationTime>\n            <Z:Win32LastAccessTime>Thu, 20 Oct 2022 02:16:35 GMT</Z:Win32LastAccessTime>\n            <Z:Win32LastModifiedTime>Thu, 20 Oct 2022 02:16:33 GMT</Z:Win32LastModifiedTime>\n            <Z:Win32FileAttributes>00000000</Z:Win32FileAttributes>\n        </D:prop>\n    </D:set>\n</D:propertyupdate>\"\"\"\n\n        ref = r\"\"\"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<D:multistatus xmlns:D=\"DAV:\">\n    <D:response>\n        <D:href>/d1/foo.txt</D:href>\n        <D:propstat>\n            <D:prop>\n                <Win32CreationTime xmlns=\"urn:schemas-microsoft-com:\"></Win32CreationTime>\n                <Win32LastAccessTime xmlns=\"urn:schemas-microsoft-com:\"></Win32LastAccessTime>\n                <Win32LastModifiedTime xmlns=\"urn:schemas-microsoft-com:\"></Win32LastModifiedTime>\n                <Win32FileAttributes xmlns=\"urn:schemas-microsoft-com:\"></Win32FileAttributes>\n            </D:prop>\n            <D:status>HTTP/1.1 403 Forbidden</D:status>\n        </D:propstat>\n    </D:response>\n</D:multistatus>\"\"\"\n\n        txt = re.sub(\"\\n +\", \"\\n\", txt)\n        root = mkenod(\"a\")\n        root.insert(0, parse_xml(txt))\n        prop = root.find(r\"./{DAV:}propertyupdate/{DAV:}set/{DAV:}prop\")\n        assert prop is not None\n        assert len(prop)\n        for el in prop:\n            el.clear()\n\n        res = ET.tostring(prop).decode(\"utf-8\")\n        want = \"\"\"<D:prop xmlns:D=\"DAV:\" xmlns:ns1=\"urn:schemas-microsoft-com:\">\n<ns1:Win32CreationTime /><ns1:Win32LastAccessTime /><ns1:Win32LastModifiedTime /><ns1:Win32FileAttributes /></D:prop>\n\"\"\"\n        self.assertEqual(res, want)\n\n    def test5(self):\n        txt = r\"\"\"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:lockinfo xmlns:D=\"DAV:\">\n    <D:lockscope><D:exclusive/></D:lockscope>\n    <D:locktype><D:write/></D:locktype>\n    <D:owner><D:href>DESKTOP-FRS9AO2\\ed</D:href></D:owner>\n</D:lockinfo>\"\"\"\n\n        ref = r\"\"\"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:prop xmlns:D=\"DAV:\"><D:lockdiscovery><D:activelock>\n    <D:locktype><D:write/></D:locktype>\n    <D:lockscope><D:exclusive/></D:lockscope>\n    <D:depth>infinity</D:depth>\n    <D:owner><D:href>DESKTOP-FRS9AO2\\ed</D:href></D:owner>\n    <D:timeout>Second-3600</D:timeout>\n    <D:locktoken><D:href>1666199679</D:href></D:locktoken>\n    <D:lockroot><D:href>/d1/foo.txt</D:href></D:lockroot>\n</D:activelock></D:lockdiscovery></D:prop>\"\"\"\n\n        txt = re.sub(\"\\n +\", \"\\n\", txt)\n        lk = parse_xml(txt)\n        self.assertEqual(lk.tag, \"{DAV:}lockinfo\")\n\n        if lk.find(r\"./{DAV:}depth\") is None:\n            lk.append(mktnod(\"D:depth\", \"infinity\"))\n\n        lk.append(mkenod(\"D:timeout\", mktnod(\"D:href\", \"Second-3600\")))\n        lk.append(mkenod(\"D:locktoken\", mktnod(\"D:href\", \"56709\")))\n        lk.append(mkenod(\"D:lockroot\", mktnod(\"D:href\", \"/foo/bar.txt\")))\n\n        lk2 = mkenod(\"D:activelock\")\n        root = mkenod(\"D:prop\", mkenod(\"D:lockdiscovery\", lk2))\n        for a in lk:\n            lk2.append(a)\n\n        print(ET.tostring(root).decode(\"utf-8\"))\n"
  },
  {
    "path": "tests/test_hooks.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport os\nimport shutil\nimport tempfile\nimport unittest\n\nfrom copyparty.authsrv import AuthSrv\nfrom copyparty.httpcli import HttpCli\nfrom tests import util as tu\nfrom tests.util import Cfg\n\ntry:\n    from typing import Optional\nexcept:\n    pass\n\n\ndef hdr(query):\n    h = \"GET /{} HTTP/1.1\\r\\nPW: o\\r\\nConnection: close\\r\\n\\r\\n\"\n    return h.format(query).encode(\"utf-8\")\n\n\nclass TestHooks(tu.TC):\n    def setUp(self):\n        self.conn: Optional[tu.VHttpConn] = None\n        self.td = tu.get_ramdisk()\n\n    def tearDown(self):\n        os.chdir(tempfile.gettempdir())\n        shutil.rmtree(self.td)\n\n    def reset(self):\n        td = os.path.join(self.td, \"vfs\")\n        if os.path.exists(td):\n            shutil.rmtree(td)\n        os.mkdir(td)\n        os.chdir(td)\n        return td\n\n    def cinit(self):\n        if self.conn:\n            self.conn.shutdown()\n            self.conn = None\n        self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b\"\")\n\n    def test(self):\n        vcfg = [\"a/b/c/d:c/d:A\", \"a:a:r\"]\n\n        scenarios = (\n            ('{\"vp\":\"x/y\"}', \"c/d/a.png\", \"c/d/x/y/a.png\"),\n            ('{\"vp\":\"x/y\"}', \"c/d/e/a.png\", \"c/d/e/x/y/a.png\"),\n            ('{\"vp\":\"../x/y\"}', \"c/d/e/a.png\", \"c/d/x/y/a.png\"),\n            ('{\"ap\":\"x/y\"}', \"c/d/a.png\", \"c/d/x/y/a.png\"),\n            ('{\"ap\":\"x/y\"}', \"c/d/e/a.png\", \"c/d/e/x/y/a.png\"),\n            ('{\"ap\":\"../x/y\"}', \"c/d/e/a.png\", \"c/d/x/y/a.png\"),\n            ('{\"ap\":\"../x/y\"}', \"c/d/a.png\", \"a/b/c/x/y/a.png\"),\n            ('{\"fn\":\"b.png\"}', \"c/d/a.png\", \"c/d/b.png\"),\n            ('{\"vp\":\"x\",\"fn\":\"b.png\"}', \"c/d/a.png\", \"c/d/x/b.png\"),\n        )\n\n        for x in scenarios:\n            print(\"\\n\\n\\n\", x)\n            hooktxt, url_up, url_dl = x\n            for hooktype in (\"xbu\", \"xau\"):\n                for upfun in (self.put, self.bup):\n                    self.reset()\n                    self.makehook(\"\"\"print('{\"reloc\":%s}')\"\"\" % (hooktxt,))\n                    ka = {hooktype: [\"j,c1,h.py\"]}\n                    self.args = Cfg(v=vcfg, a=[\"o:o\"], e2d=True, **ka)\n                    self.asrv = AuthSrv(self.args, self.log)\n                    self.cinit()\n\n                    h, b = upfun(url_up)\n                    self.assertStart(\"HTTP/1.1 201 Created\\r\", h)\n                    h, b = self.curl(url_dl)\n                    self.assertEqual(b, \"ok %s\\n\" % (url_up))\n\n    def test2(self):\n        hooktxt = \"import sys\\nopen('h%d','wb').close()\\nsys.exit(%d)\\n\"\n        for hooktype in (\"xbu\", \"xau\"):\n            for upfun in (self.put, self.bup):\n                self.reset()\n                for n in [0, 1, 100]:\n                    with open(\"h%d.py\" % (n,), \"wb\") as f:\n                        f.write((hooktxt % (n, n)).encode(\"utf-8\"))\n                vcfg = [\n                    \"012:012:A:c,H=c,h0.py:c,H=c,h1.py:c,H=c,h100.py\",\n                    \"021:021:A:c,H=c,h0.py:c,H=c,h100.py:c,H=c,h1.py\",\n                    \"120:120:A:c,H=c,h1.py:c,H=c,h100.py:c,H=c,h0.py\",\n                    \"30:30:A:c,H=c,enoent.py:c,H=c,h100.py\",  # not-exist\n                ]\n                vcfg = [x.replace(\"H\", hooktype) for x in vcfg]\n                self.args = Cfg(v=vcfg, a=[\"o:o\"], e2d=True)\n                self.asrv = AuthSrv(self.args, self.log)\n                self.cinit()\n                scenarios = (\n                    (\"012\", False, True, True, False),\n                    (\"021\", True, True, False, True),\n                    (\"120\", False, False, True, False),\n                    (\"30\", False, False, False, False),\n                )\n                for (vp, ok, h0, h1, h2) in scenarios:\n                    for zs in (\"h0\", \"h1\", \"h100\"):\n                        if os.path.exists(zs):\n                            os.unlink(zs)\n                    vp = \"%s/f\" % (vp,)\n                    h, b = upfun(vp)\n                    self.assertEqual(ok, os.path.exists(vp))\n                    self.assertEqual(h0, os.path.exists(\"h0\"))\n                    self.assertEqual(h1, os.path.exists(\"h1\"))\n                    self.assertEqual(h2, os.path.exists(\"h100\"))\n\n    def makehook(self, hs):\n        with open(\"h.py\", \"wb\") as f:\n            f.write(hs.encode(\"utf-8\"))\n\n    def put(self, url):\n        buf = \"PUT /{0} HTTP/1.1\\r\\nPW: o\\r\\nConnection: close\\r\\nContent-Length: {1}\\r\\n\\r\\nok {0}\\n\"\n        buf = buf.format(url, len(url) + 4).encode(\"utf-8\")\n        print(\"PUT -->\", buf)\n        conn = self.conn.setbuf(buf)\n        HttpCli(conn).run()\n        ret = conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n        print(\"PUT <--\", ret)\n        return ret\n\n    def bup(self, url):\n        hdr = \"POST /%s HTTP/1.1\\r\\nPW: o\\r\\nConnection: close\\r\\nContent-Type: multipart/form-data; boundary=XD\\r\\nContent-Length: %d\\r\\n\\r\\n\"\n        bdy = '--XD\\r\\nContent-Disposition: form-data; name=\"act\"\\r\\n\\r\\nbput\\r\\n--XD\\r\\nContent-Disposition: form-data; name=\"f\"; filename=\"%s\"\\r\\n\\r\\n'\n        ftr = \"\\r\\n--XD--\\r\\n\"\n        try:\n            url, fn = url.rsplit(\"/\", 1)\n        except:\n            fn = url\n            url = \"\"\n\n        buf = (bdy % (fn,) + \"ok %s/%s\\n\" % (url, fn) + ftr).encode(\"utf-8\")\n        buf = (hdr % (url, len(buf))).encode(\"utf-8\") + buf\n        print(\"PoST -->\", buf)\n        conn = self.conn.setbuf(buf)\n        HttpCli(conn).run()\n        ret = conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n        print(\"POST <--\", ret)\n        return ret\n\n    def curl(self, url, binary=False):\n        conn = self.conn.setbuf(hdr(url))\n        HttpCli(conn).run()\n        if binary:\n            h, b = conn.s._reply.split(b\"\\r\\n\\r\\n\", 1)\n            return [h.decode(\"utf-8\"), b]\n\n        return conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n\n    def log(self, src, msg, c=0):\n        print(msg)\n"
  },
  {
    "path": "tests/test_httpcli.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport io\nimport os\nimport pprint\nimport shutil\nimport tarfile\nimport tempfile\nimport time\nimport unittest\nimport zipfile\n\nfrom copyparty.authsrv import AuthSrv\nfrom copyparty.httpcli import HttpCli\nfrom tests import util as tu\nfrom tests.util import Cfg, eprint, pfind2ls\n\n\ndef hdr(query):\n    h = \"GET /{} HTTP/1.1\\r\\nCookie: cppwd=o\\r\\nConnection: close\\r\\n\\r\\n\"\n    return h.format(query).encode(\"utf-8\")\n\n\nclass TestHttpCli(unittest.TestCase):\n    def setUp(self):\n        self.td = tu.get_ramdisk()\n        self.maxDiff = 99999\n\n    def tearDown(self):\n        os.chdir(tempfile.gettempdir())\n        shutil.rmtree(self.td)\n\n    def test(self):\n        test_tar = True\n        test_zip = True\n\n        td = os.path.join(self.td, \"vfs\")\n        os.mkdir(td)\n        os.chdir(td)\n\n        # \"perm+user\"; r/w/a (a=rw) for user a/o/x (a=all)\n        self.dtypes = [\"ra\", \"ro\", \"rx\", \"wa\", \"wo\", \"wx\", \"aa\", \"ao\", \"ax\"]\n        self.can_read = [\"ra\", \"ro\", \"aa\", \"ao\"]\n        self.can_write = [\"wa\", \"wo\", \"aa\", \"ao\"]\n        self.fn = \"g{:x}g\".format(int(time.time() * 3))\n\n        tctr = 0\n        allfiles = []\n        allvols = []\n        for top in self.dtypes:\n            allvols.append(top)\n            allfiles.append(\"/\".join([top, self.fn]))\n            for s1 in self.dtypes:\n                p = \"/\".join([top, s1])\n                allvols.append(p)\n                allfiles.append(p + \"/\" + self.fn)\n                allfiles.append(p + \"/n/\" + self.fn)\n                for s2 in self.dtypes:\n                    p = \"/\".join([top, s1, \"n\", s2])\n                    os.makedirs(p)\n                    allvols.append(p)\n                    allfiles.append(p + \"/\" + self.fn)\n\n        for fp in allfiles:\n            with open(fp, \"w\") as f:\n                f.write(\"ok {}\\n\".format(fp))\n\n        for top in self.dtypes:\n            vcfg = []\n            for vol in allvols:\n                if not vol.startswith(top):\n                    continue\n\n                mode = vol[-2].replace(\"a\", \"rw\")\n                usr = vol[-1]\n                if usr == \"a\":\n                    usr = \"\"\n\n                if \"/\" not in vol:\n                    vol += \"/\"\n\n                top, sub = vol.split(\"/\", 1)\n                vcfg.append(\"{0}/{1}:{1}:{2},{3}\".format(top, sub, mode, usr))\n\n            pprint.pprint(vcfg)\n\n            self.args = Cfg(v=vcfg, a=[\"o:o\", \"x:x\"])\n            self.asrv = AuthSrv(self.args, self.log)\n            self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b\"\")\n            vfiles = [x for x in allfiles if x.startswith(top)]\n            for fp in vfiles:\n                tctr += 1\n                rok, wok = self.can_rw(fp)\n                furl = fp.split(\"/\", 1)[1]\n                durl = furl.rsplit(\"/\", 1)[0] if \"/\" in furl else \"\"\n\n                # file download\n                h, ret = self.curl(furl)\n                res = \"ok \" + fp in ret\n                print(\"[{}] {} {} = {}\".format(fp, rok, wok, res))\n                if rok != res:\n                    eprint(\"\\033[33m{}\\n# {}\\033[0m\".format(ret, furl))\n                    self.fail()\n\n                # file browser: html\n                h, ret = self.curl(durl)\n                res = \"'{}'\".format(self.fn) in ret\n                print(res)\n                if rok != res:\n                    eprint(\"\\033[33m{}\\n# {}\\033[0m\".format(ret, durl))\n                    self.fail()\n\n                # file browser: json\n                url = durl + \"?ls\"\n                h, ret = self.curl(url)\n                res = '\"{}\"'.format(self.fn) in ret\n                print(res)\n                if rok != res:\n                    eprint(\"\\033[33m{}\\n# {}\\033[0m\".format(ret, url))\n                    self.fail()\n\n                # expected files in archives\n                if rok:\n                    zs = top + \"/\" + durl\n                    ref = [x for x in vfiles if self.in_dive(zs, x)]\n                    ref.sort()\n                else:\n                    ref = []\n\n                h, b = self.propfind(durl, 1)\n                fns = [x for x in pfind2ls(b) if not x.endswith(\"/\")]\n                if ref:\n                    self.assertIn(\"<D:propstat>\", b)\n                elif not rok and not wok:\n                    self.assertListEqual([], fns)\n                else:\n                    self.assertIn(\"<D:multistatus\", b)\n\n                h, b = self.propfind(durl, 0)\n                fns = [x for x in pfind2ls(b) if not x.endswith(\"/\")]\n                if ref:\n                    self.assertIn(\"<D:propstat>\", b)\n                elif not rok:\n                    self.assertListEqual([], fns)\n                else:\n                    self.assertIn(\"<D:multistatus\", b)\n\n                if test_tar:\n                    url = durl + \"?tar\"\n                    h, b = self.curl(url, True)\n                    try:\n                        tar = tarfile.open(fileobj=io.BytesIO(b), mode=\"r|\").getnames()\n                    except:\n                        if \"HTTP/1.1 403 Forbidden\" not in h and b != b\"\\nJ2EOT\":\n                            eprint(\"bad tar?\", url, h, b)\n                            raise\n                        tar = []\n                    tar = [x.split(\"/\", 1)[1] for x in tar]\n                    tar = [\"/\".join([y for y in [top, durl, x] if y]) for x in tar]\n                    tar = [[x] + self.can_rw(x) for x in tar]\n                    tar_ok = [x[0] for x in tar if x[1]]\n                    tar_ng = [x[0] for x in tar if not x[1]]\n                    tar_ok.sort()\n                    self.assertEqual(ref, tar_ok)\n                    self.assertEqual([], tar_ng)\n\n                if test_zip:\n                    url = durl + \"?zip\"\n                    h, b = self.curl(url, True)\n                    try:\n                        with zipfile.ZipFile(io.BytesIO(b), \"r\") as zf:\n                            zfi = zf.infolist()\n                    except:\n                        if \"HTTP/1.1 403 Forbidden\" not in h and b != b\"\\nJ2EOT\":\n                            eprint(\"bad zip?\", url, h, b)\n                            raise\n                        zfi = []\n                    zfn = [x.filename.split(\"/\", 1)[1] for x in zfi]\n                    zfn = [\"/\".join([y for y in [top, durl, x] if y]) for x in zfn]\n                    zfn = [[x] + self.can_rw(x) for x in zfn]\n                    zf_ok = [x[0] for x in zfn if x[1]]\n                    zf_ng = [x[0] for x in zfn if not x[1]]\n                    zf_ok.sort()\n                    self.assertEqual(ref, zf_ok)\n                    self.assertEqual([], zf_ng)\n\n                # stash\n                h, ret = self.put(durl)\n                res = h.startswith(\"HTTP/1.1 201 \")\n                self.assertEqual(res, wok)\n                if wok:\n                    vp = h.split(\"\\nLocation: http://a:1/\")[1].split(\"\\r\")[0]\n                    vn, rem = self.asrv.vfs.get(vp, \"*\", False, False)\n                    ap = os.path.join(vn.realpath, rem)\n                    os.unlink(ap)\n\n            self.conn.shutdown()\n\n    def can_rw(self, fp):\n        # lowest non-neutral folder declares permissions\n        expect = fp.split(\"/\")[:-1]\n        for x in reversed(expect):\n            if x != \"n\":\n                expect = x\n                break\n\n        return [expect in self.can_read, expect in self.can_write]\n\n    def in_dive(self, top, fp):\n        # archiver bails at first inaccessible subvolume\n        top = top.strip(\"/\").split(\"/\")\n        fp = fp.split(\"/\")\n        for f1, f2 in zip(top, fp):\n            if f1 != f2:\n                return False\n\n        for f in fp[len(top) :]:\n            if f == self.fn:\n                return True\n            if f not in self.can_read and f != \"n\":\n                return False\n\n        return True\n\n    def put(self, url):\n        buf = \"PUT /{0} HTTP/1.1\\r\\nCookie: cppwd=o\\r\\nConnection: close\\r\\nContent-Length: {1}\\r\\n\\r\\nok {0}\\n\"\n        buf = buf.format(url, len(url) + 4).encode(\"utf-8\")\n        print(\"PUT -->\", buf)\n        conn = self.conn.setbuf(buf)\n        HttpCli(conn).run()\n        ret = conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n        print(\"PUT <--\", ret)\n        return ret\n\n    def curl(self, url, binary=False):\n        conn = self.conn.setbuf(hdr(url))\n        HttpCli(conn).run()\n        if binary:\n            h, b = conn.s._reply.split(b\"\\r\\n\\r\\n\", 1)\n            return [h.decode(\"utf-8\"), b]\n\n        return conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n\n    def propfind(self, url, depth=1):\n        zs = \"PROPFIND /%s HTTP/1.1\\r\\nDepth: %d\\r\\nPW: o\\r\\nConnection: close\\r\\n\\r\\n\"\n        buf = zs % (url, depth)\n        conn = self.conn.setbuf(buf.encode(\"utf-8\"))\n        HttpCli(conn).run()\n        return conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n\n    def log(self, src, msg, c=0):\n        print(msg)\n"
  },
  {
    "path": "tests/test_idp.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport json\nimport os\nimport unittest\n\nfrom copyparty.__init__ import ANYWIN\nfrom copyparty.authsrv import AuthSrv\nfrom tests.util import Cfg\n\n\nclass TestVFS(unittest.TestCase):\n    def dump(self, vfs):\n        print(json.dumps(vfs, indent=4, sort_keys=True, default=lambda o: o.__dict__))\n\n    def log(self, src, msg, c=0):\n        m = \"%s\" % (msg,)\n        if (\n            \"warning: filesystem-path does not exist:\" in m\n            or \"you are sharing a system directory:\" in m\n            or \"reinitializing due to new user from IdP:\" in m\n            or m.startswith(\"hint: argument\")\n            or (m.startswith(\"loaded \") and \" config files:\" in m)\n        ):\n            return\n\n        print((\"[%s] %s\" % (src, msg)).encode(\"ascii\", \"replace\").decode(\"ascii\"))\n\n    def nav(self, au, vp):\n        return au.vfs.get(vp, \"\", False, False)[0]\n\n    def assertAxs(self, axs, expected):\n        unpacked = []\n        zs = \"uread uwrite umove udel uget upget uhtml uadmin udot\"\n        for k in zs.split():\n            unpacked.append(list(sorted(getattr(axs, k))))\n\n        pad = len(unpacked) - len(expected)\n        self.assertEqual(unpacked, expected + [[]] * pad)\n\n    def assertAxsAt(self, au, vp, expected):\n        vn = self.nav(au, vp)\n        self.assertAxs(vn.axs, expected)\n\n    def assertNodes(self, vfs, expected):\n        got = list(sorted(vfs.nodes.keys()))\n        self.assertEqual(got, expected)\n\n    def assertNodesAt(self, au, vp, expected):\n        vn = self.nav(au, vp)\n        self.assertNodes(vn, expected)\n\n    def assertApEq(self, ap, rhs):\n        if ANYWIN and len(ap) > 2 and ap[1] == \":\":\n            ap = ap[2:].replace(\"\\\\\", \"/\")\n\n        return self.assertEqual(ap, rhs)\n\n    def prep(self):\n        here = os.path.abspath(os.path.dirname(__file__))\n        cfgdir = os.path.join(here, \"res\", \"idp\")\n\n        # globals are applied by main so need to cheat a little\n        xcfg = {\"idp_h_usr\": [\"x-idp-user\"], \"idp_h_grp\": \"x-idp-group\"}\n\n        return here, cfgdir, xcfg\n\n    # buckle up...\n\n    def test_1(self):\n        \"\"\"\n        trivial; volumes [/] and [/vb] with one user in [/] only\n        \"\"\"\n        _, cfgdir, xcfg = self.prep()\n        au = AuthSrv(Cfg(c=[cfgdir + \"/1.conf\"], **xcfg), self.log)\n\n        self.assertEqual(au.vfs.vpath, \"\")\n        self.assertApEq(au.vfs.realpath, \"/\")\n        self.assertNodes(au.vfs, [\"vb\"])\n        self.assertNodes(au.vfs.nodes[\"vb\"], [])\n\n        self.assertAxs(au.vfs.axs, [[\"ua\"]])\n        self.assertAxs(au.vfs.nodes[\"vb\"].axs, [])\n\n    def test_2(self):\n        \"\"\"\n        users ua/ub/uc, group ga (ua+ub) in basic combinations\n        \"\"\"\n        _, cfgdir, xcfg = self.prep()\n        au = AuthSrv(Cfg(c=[cfgdir + \"/2.conf\"], **xcfg), self.log)\n\n        self.assertEqual(au.vfs.vpath, \"\")\n        self.assertApEq(au.vfs.realpath, \"/\")\n        self.assertNodes(au.vfs, [\"vb\", \"vc\"])\n        self.assertNodes(au.vfs.nodes[\"vb\"], [])\n        self.assertNodes(au.vfs.nodes[\"vc\"], [])\n\n        self.assertAxs(au.vfs.axs, [[\"ua\", \"ub\"]])\n        self.assertAxsAt(au, \"vb\", [[\"ua\", \"ub\"]])  # same as:\n        self.assertAxs(au.vfs.nodes[\"vb\"].axs, [[\"ua\", \"ub\"]])\n        self.assertAxs(au.vfs.nodes[\"vc\"].axs, [[\"ua\", \"ub\", \"uc\"]])\n\n    def test_3(self):\n        \"\"\"\n        IdP-only; dynamically created volumes for users/groups\n        \"\"\"\n        _, cfgdir, xcfg = self.prep()\n        au = AuthSrv(Cfg(c=[cfgdir + \"/3.conf\"], **xcfg), self.log)\n\n        self.assertEqual(au.vfs.vpath, \"\")\n        self.assertApEq(au.vfs.realpath, \"\")\n        self.assertNodes(au.vfs, [])\n        self.assertAxs(au.vfs.axs, [])\n\n        au.idp_checkin(None, \"iua\", \"iga\")\n        self.assertNodes(au.vfs, [\"vg\", \"vu\"])\n        self.assertNodesAt(au, \"vu\", [\"iua\"])  # same as:\n        self.assertNodes(au.vfs.nodes[\"vu\"], [\"iua\"])\n        self.assertNodes(au.vfs.nodes[\"vg\"], [\"iga\"])\n        self.assertApEq(au.vfs.nodes[\"vu\"].realpath, \"\")\n        self.assertApEq(au.vfs.nodes[\"vg\"].realpath, \"\")\n        self.assertApEq(au.vfs.nodes[\"vu\"].nodes[\"iua\"].realpath, \"/uiua\")\n        self.assertApEq(au.vfs.nodes[\"vg\"].nodes[\"iga\"].realpath, \"/giga\")\n        self.assertAxs(au.vfs.axs, [])\n        self.assertAxsAt(au, \"vu/iua\", [[\"iua\"]])  # same as:\n        self.assertAxs(self.nav(au, \"vu/iua\").axs, [[\"iua\"]])\n        self.assertAxs(self.nav(au, \"vg/iga\").axs, [[\"iua\"]])  # axs is unames\n\n    def test_4(self):\n        \"\"\"\n        IdP mixed with regular users\n        \"\"\"\n        _, cfgdir, xcfg = self.prep()\n        au = AuthSrv(Cfg(c=[cfgdir + \"/4.conf\"], **xcfg), self.log)\n\n        self.assertEqual(au.vfs.vpath, \"\")\n        self.assertApEq(au.vfs.realpath, \"\")\n        self.assertNodes(au.vfs, [\"vu\"])\n        self.assertNodesAt(au, \"vu\", [\"ua\", \"ub\"])\n        self.assertAxs(au.vfs.axs, [])\n        self.assertAxsAt(au, \"vu\", [])\n        self.assertAxsAt(au, \"vu/ua\", [[\"ua\"]])\n        self.assertAxsAt(au, \"vu/ub\", [[\"ub\"]])\n\n        au.idp_checkin(None, \"iua\", \"iga\")\n        self.assertNodes(au.vfs, [\"vg\", \"vu\"])\n        self.assertNodesAt(au, \"vu\", [\"iua\", \"ua\", \"ub\"])\n        self.assertNodesAt(au, \"vg\", [\"iga1\", \"iga2\"])\n        self.assertAxs(au.vfs.axs, [])\n        self.assertAxsAt(au, \"vu\", [])\n        self.assertAxsAt(au, \"vu/iua\", [[\"iua\"]])\n        self.assertAxsAt(au, \"vu/ua\", [[\"ua\"]])\n        self.assertAxsAt(au, \"vu/ub\", [[\"ub\"]])\n        self.assertAxsAt(au, \"vg\", [])\n        self.assertAxsAt(au, \"vg/iga1\", [[\"iua\"]])\n        self.assertAxsAt(au, \"vg/iga2\", [[\"iua\", \"ua\"]])\n        self.assertApEq(self.nav(au, \"vu/ua\").realpath, \"/u-ua\")\n        self.assertApEq(self.nav(au, \"vu/iua\").realpath, \"/u-iua\")\n        self.assertApEq(self.nav(au, \"vg/iga1\").realpath, \"/g1-iga\")\n        self.assertApEq(self.nav(au, \"vg/iga2\").realpath, \"/g2-iga\")\n\n        au.idp_checkin(None, \"iub\", \"iga\")\n        self.assertAxsAt(au, \"vu/iua\", [[\"iua\"]])\n        self.assertAxsAt(au, \"vg/iga1\", [[\"iua\", \"iub\"]])\n        self.assertAxsAt(au, \"vg/iga2\", [[\"iua\", \"iub\", \"ua\"]])\n\n    def test_5(self):\n        \"\"\"\n        one IdP user in multiple groups\n        \"\"\"\n        _, cfgdir, xcfg = self.prep()\n        au = AuthSrv(Cfg(c=[cfgdir + \"/5.conf\"], **xcfg), self.log)\n\n        self.assertEqual(au.vfs.vpath, \"\")\n        self.assertApEq(au.vfs.realpath, \"\")\n        self.assertNodes(au.vfs, [\"g\", \"ga\", \"gb\"])\n        self.assertAxs(au.vfs.axs, [])\n\n        au.idp_checkin(None, \"iua\", \"ga\")\n        self.assertNodes(au.vfs, [\"g\", \"ga\", \"gb\"])\n        self.assertAxsAt(au, \"g\", [[\"iua\"]])\n        self.assertAxsAt(au, \"ga\", [[\"iua\"]])\n        self.assertAxsAt(au, \"gb\", [])\n\n        au.idp_checkin(None, \"iua\", \"gb\")\n        self.assertNodes(au.vfs, [\"g\", \"ga\", \"gb\"])\n        self.assertAxsAt(au, \"g\", [[\"iua\"]])\n        self.assertAxsAt(au, \"ga\", [])\n        self.assertAxsAt(au, \"gb\", [[\"iua\"]])\n\n        au.idp_checkin(None, \"iua\", \"ga|gb\")\n        self.assertNodes(au.vfs, [\"g\", \"ga\", \"gb\"])\n        self.assertAxsAt(au, \"g\", [[\"iua\"]])\n        self.assertAxsAt(au, \"ga\", [[\"iua\"]])\n        self.assertAxsAt(au, \"gb\", [[\"iua\"]])\n\n    def test_6(self):\n        \"\"\"\n        IdP volumes with anon-get and other users/groups (github#79)\n        \"\"\"\n        _, cfgdir, xcfg = self.prep()\n        au = AuthSrv(Cfg(c=[cfgdir + \"/6.conf\"], **xcfg), self.log)\n\n        self.assertAxs(au.vfs.axs, [])\n        self.assertEqual(au.vfs.vpath, \"\")\n        self.assertApEq(au.vfs.realpath, \"\")\n        self.assertNodes(au.vfs, [])\n\n        au.idp_checkin(None, \"iua\", \"\")\n        star = [\"*\", \"iua\"]\n        self.assertNodes(au.vfs, [\"get\", \"priv\"])\n        self.assertAxsAt(au, \"get/iua\", [[\"iua\"], [], [], [], star])\n        self.assertAxsAt(au, \"priv/iua\", [[\"iua\"], [], []])\n\n        au.idp_checkin(None, \"iub\", \"\")\n        star = [\"*\", \"iua\", \"iub\"]\n        self.assertNodes(au.vfs, [\"get\", \"priv\"])\n        self.assertAxsAt(au, \"get/iua\", [[\"iua\"], [], [], [], star])\n        self.assertAxsAt(au, \"get/iub\", [[\"iub\"], [], [], [], star])\n        self.assertAxsAt(au, \"priv/iua\", [[\"iua\"], [], []])\n        self.assertAxsAt(au, \"priv/iub\", [[\"iub\"], [], []])\n\n        au.idp_checkin(None, \"iuc\", \"su\")\n        star = [\"*\", \"iua\", \"iub\", \"iuc\"]\n        self.assertNodes(au.vfs, [\"get\", \"priv\", \"team\"])\n        self.assertAxsAt(au, \"get/iua\", [[\"iua\", \"iuc\"], [], [\"iuc\"], [], star])\n        self.assertAxsAt(au, \"get/iub\", [[\"iub\", \"iuc\"], [], [\"iuc\"], [], star])\n        self.assertAxsAt(au, \"get/iuc\", [[\"iuc\"], [], [\"iuc\"], [], star])\n        self.assertAxsAt(au, \"priv/iua\", [[\"iua\", \"iuc\"], [], [\"iuc\"]])\n        self.assertAxsAt(au, \"priv/iub\", [[\"iub\", \"iuc\"], [], [\"iuc\"]])\n        self.assertAxsAt(au, \"priv/iuc\", [[\"iuc\"], [], [\"iuc\"]])\n        self.assertAxsAt(au, \"team/su/iuc\", [[\"iuc\"]])\n\n        au.idp_checkin(None, \"iud\", \"su\")\n        self.assertAxsAt(au, \"team/su/iuc\", [[\"iuc\", \"iud\"]])\n        self.assertAxsAt(au, \"team/su/iud\", [[\"iuc\", \"iud\"]])\n\n    def test_7(self):\n        \"\"\"\n        conditional idp-vols\n        \"\"\"\n        _, cfgdir, xcfg = self.prep()\n        au = AuthSrv(Cfg(c=[cfgdir + \"/7.conf\"], **xcfg), self.log)\n        au.idp_checkin(None, \"iua\", \"ga\")\n        au.idp_checkin(None, \"iuab\", \"ga,gb\")\n        au.idp_checkin(None, \"iuabc\", \"ga,gb,gc\")\n        au.idp_checkin(None, \"iub\", \"gb\")\n        au.idp_checkin(None, \"iubc\", \"gb,gc\")\n        au.idp_checkin(None, \"iuc\", \"gc\")\n        zs = \"\"\"\nu/iua\nu/iuab\nu/iuabc\nu/iub\nu/iubc\nu/iuc\nuya/iua\nuya/iuab\nuya/iuabc\nuyab/iuab\nuyab/iuabc\nuna/iub\nuna/iubc\nuna/iuc\nunab/iuc\ngya/ga\ngna/gb\ngna/gc\ngnab/gc\n\"\"\"\n        zl1 = sorted(zs.strip().split(\"\\n\"))[:]\n        zl2 = sorted(list(au.vfs.all_vols))[:]\n        # print(\" \".join(zl1))\n        # print(\" \".join(zl2))\n        self.assertListEqual(zl1, zl2)\n\n    def test_8(self):\n        \"\"\"\n        conditional non-idp vols\n        \"\"\"\n        _, cfgdir, xcfg = self.prep()\n        xcfg = {\"vc\": True}\n        au = AuthSrv(Cfg(c=[cfgdir + \"/8.conf\"], **xcfg), self.log)\n        zs = \"\"\"\nu/iua\nu/iuab\nu/iuabc\nu/iub\nu/iubc\nu/iuc\nuya/iua\nuya/iuab\nuya/iuabc\nuyab/iuab\nuyab/iuabc\nuna/iub\nuna/iubc\nuna/iuc\nunab/iuc\ngya/ga\ngna/gb\ngna/gc\ngnab/gc\n\"\"\"\n        zl1 = sorted(zs.strip().split(\"\\n\"))[:]\n        zl2 = sorted(list(au.vfs.all_vols))[:]\n        self.assertListEqual(zl1, zl2)\n"
  },
  {
    "path": "tests/test_metrics.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport os\nimport re\nimport shutil\nimport tempfile\nimport unittest\n\nfrom copyparty.__init__ import PY2\nfrom copyparty.authsrv import AuthSrv\nfrom copyparty.httpcli import HttpCli\nfrom copyparty.metrics import Metrics\nfrom tests import util as tu\nfrom tests.util import Cfg\n\n\ndef hdr(query):\n    h = \"GET /{} HTTP/1.1\\r\\nCookie: cppwd=o\\r\\nConnection: close\\r\\n\\r\\n\"\n    return h.format(query).encode(\"utf-8\")\n\n\nclass TestMetrics(tu.TC):\n    def setUp(self):\n        self.td = tu.get_ramdisk()\n        os.chdir(self.td)\n\n    def tearDown(self):\n        if self.conn:\n            self.conn.shutdown()\n        os.chdir(tempfile.gettempdir())\n        shutil.rmtree(self.td)\n\n    def cinit(self):\n        if self.conn:\n            self.fstab = self.conn.hsrv.hub.up2k.fstab\n            self.conn.hsrv.hub.up2k.shutdown()\n        self.asrv = AuthSrv(self.args, self.log)\n        self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b\"\", True)\n        if self.fstab:\n            self.conn.hsrv.hub.up2k.fstab = self.fstab\n\n        if not self.metrics:\n            self.metrics = Metrics(self.conn)\n        self.conn.broker = self.conn.hsrv.broker\n        self.conn.hsrv.metrics = self.metrics\n        for k in \"ncli nsus nban\".split():\n            setattr(self.conn, k, 9)\n\n    def test(self):\n        zs = \"nos_dup nos_hdd nos_unf nos_vol nos_vst\"\n        opts = {x: False for x in zs.split()}\n        self.args = Cfg(v=[\".::A\"], a=[], stats=True, **opts)\n        self.conn = self.fstab = self.metrics = None\n        self.cinit()\n        h, b = self.curl(\".cpr/metrics\")\n        self.assertStart(\"HTTP/1.1 200 OK\\r\", h)\n        ptns = r\"\"\"\ncpp_uptime_seconds [0-9]\\.[0-9]{3}$\ncpp_boot_unixtime_seconds [0-9]{7,10}\\.[0-9]{3}$\ncpp_active_dl 0$\ncpp_http_reqs_created [0-9]{7,10}$\ncpp_http_reqs_total -1$\ncpp_http_conns 9$\ncpp_total_bans 9$\ncpp_sus_reqs_total 9$\ncpp_active_bans 0$\ncpp_idle_vols 0$\ncpp_busy_vols 0$\ncpp_offline_vols 0$\ncpp_db_idle_seconds 86399999\\.00$\ncpp_db_act_seconds 0\\.00$\ncpp_hashing_files 0$\ncpp_tagq_files 0$\ncpp_disk_size_bytes\\{vol=\"/\"\\} [0-9]+$\ncpp_disk_free_bytes\\{vol=\"/\"\\} [0-9]+$\ncpp_vol_bytes\\{vol=\"/\"\\} 0$\ncpp_vol_files\\{vol=\"/\"\\} 0$\ncpp_vol_bytes\\{vol=\"total\"\\} 0$\ncpp_vol_files\\{vol=\"total\"\\} 0$\ncpp_dupe_bytes\\{vol=\"total\"\\} 0$\ncpp_dupe_files\\{vol=\"total\"\\} 0$\n\"\"\"\n        if not PY2:\n            ptns += r\"\"\"\ncpp_mtpq_files 0$\ncpp_unf_bytes\\{vol=\"/\"\\} 0$\ncpp_unf_files\\{vol=\"/\"\\} 0$\ncpp_unf_bytes\\{vol=\"total\"\\} 0$\ncpp_unf_files\\{vol=\"total\"\\} 0$\n\"\"\"\n        for want in [x for x in ptns.split(\"\\n\") if x]:\n            if not re.search(\"^\" + want, b.replace(\"\\r\", \"\"), re.MULTILINE):\n                raise Exception(\"server response:\\n%s\\n\\nmissing item: %s\" % (b, want))\n\n    def curl(self, url, binary=False):\n        conn = self.conn.setbuf(hdr(url))\n        HttpCli(conn).run()\n        if binary:\n            h, b = conn.s._reply.split(b\"\\r\\n\\r\\n\", 1)\n            return [h.decode(\"utf-8\"), b]\n\n        return conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n\n    def log(self, src, msg, c=0):\n        print(msg)\n"
  },
  {
    "path": "tests/test_mv.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport json\nimport os\nimport shutil\nimport tempfile\nimport unittest\nfrom itertools import product\n\nfrom copyparty.__init__ import PY2\nfrom copyparty.authsrv import AuthSrv\nfrom copyparty.httpcli import HttpCli\nfrom tests import util as tu\nfrom tests.util import Cfg\n\n\"\"\"\nTODO inject tags into db and verify ls\n\"\"\"\n\n\nclass TestDedup(tu.TC):\n    def setUp(self):\n        self.td = tu.get_ramdisk()\n\n    def tearDown(self):\n        if not PY2 and self.conn:\n            self.conn.shutdown()\n        os.chdir(tempfile.gettempdir())\n        shutil.rmtree(self.td)\n\n    def reset(self):\n        td = os.path.join(self.td, \"vfs\")\n        if os.path.exists(td):\n            shutil.rmtree(td)\n        os.mkdir(td)\n        os.chdir(td)\n        return td\n\n    def cinit(self):\n        if self.conn:\n            self.fstab = self.conn.hsrv.hub.up2k.fstab\n            self.conn.hsrv.hub.up2k.shutdown()\n        self.asrv = AuthSrv(self.args, self.log)\n        self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b\"\", True)\n        if self.fstab:\n            self.conn.hsrv.hub.up2k.fstab = self.fstab\n\n    def test(self):\n        if PY2:\n            raise unittest.SkipTest()\n\n        # tc_e2d = [True, False]  # maybe-TODO only known symlinks are translated\n        tc_e2d = [True]\n        tc_dedup = [\"sym\", \"no\", \"sym-no\"]\n        tc_vols = [[\"::A\"], [\"::A\", \"d1:d1:A\"]]\n        dirs = [\"d1\", \"d1/d2\", \"d1/d2/d3\", \"d1/d4\"]\n        files = [\n            (\n                \"one\",\n                \"BfcDQQeKz2oG1CPSFyD5ZD1flTYm2IoCY23DqeeVgq6w\",\n                \"XMbpLRqVdtGmgggqjUI6uSoNMTqZVX4K6zr74XA1BRKc\",\n            )\n        ]\n        # (data, chash, wark)\n\n        self.conn = None\n        self.fstab = None\n        self.ctr = 0  # 2304\n        tcgen = product(tc_e2d, tc_dedup, tc_vols, dirs, [\"d9\", \"../d9\"])\n        for e2d, dedup, vols, mv_from, dst in tcgen:\n            if \"/\" not in mv_from and dst.startswith(\"..\"):\n                continue  # would move past top of fs\n            if len(vols) > 1 and mv_from == \"d1\":\n                continue  # cannot move a vol\n\n            # print(e2d, dedup, vols, mv_from, dst)\n            ka = {\"e2d\": e2d}\n            if dedup == \"hard\":\n                ka[\"hardlink\"] = True\n            elif dedup == \"no\":\n                ka[\"no_dedup\"] = True\n            self.args = Cfg(v=vols[:], a=[], **ka)\n\n            for u1, u2, u3, u4 in product(dirs, dirs, dirs, dirs):\n                ups = (u1, u2, u3, u4)\n                if len(set(ups)) < 4:\n                    continue  # not unique\n\n                t = \"e2d:%s dedup:%s vols:%d from:%s to:%s\"\n                t = t % (e2d, dedup, len(vols), mv_from, dst)\n                print(\"\\n\\n\\033[0;7m# files:\", ups, t, \"\\033[0m\")\n\n                self.reset()\n                self.cinit()\n\n                for up in [u1, u2, u3, u4]:\n                    self.do_post(up, \"fn\", files[0], up == u1)\n\n                restore_args = None\n                if dedup == \"sym-no\":\n                    restore_args = self.args\n                    ka = {\"e2d\": e2d, \"no_dedup\": True}\n                    self.args = Cfg(v=vols[:], a=[], **ka)\n                    self.cinit()\n\n                mv_to = mv_from\n                for _ in range(2 if dst.startswith(\"../\") else 1):\n                    mv_to = mv_from.rsplit(\"/\", 1)[0] if \"/\" in mv_from else \"\"\n                mv_to += \"/\" + dst.lstrip(\"./\")\n\n                self.do_mv(mv_from, mv_to)\n\n                for dirpath in [u1, u2, u3, u4]:\n                    if dirpath == mv_from:\n                        dirpath = mv_to\n                    elif dirpath.startswith(mv_from):\n                        dirpath = mv_to + dirpath[len(mv_from) :]\n                    h, b = self.curl(dirpath + \"/fn\")\n                    self.assertEqual(b, \"one\")\n\n                if restore_args:\n                    self.args = restore_args\n\n    def do_mv(self, src, dst):\n        hdr = \"POST /%s?move=/%s HTTP/1.1\\r\\nConnection: close\\r\\nContent-Length: 0\\r\\n\\r\\n\"\n        buf = (hdr % (src, dst)).encode(\"utf-8\")\n        print(\"MV [%s] => [%s]\" % (src, dst))\n        HttpCli(self.conn.setbuf(buf)).run()\n        ret = self.conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n        print(\"MV <-- \", ret)\n        self.assertStart(\"HTTP/1.1 201 Created\\r\", ret[0])\n        self.assertEqual(\"k\\r\\n\", ret[1])\n        return ret\n\n    def do_post(self, dn, fn, fi, first):\n        print(\"\\n# do_post\", self.ctr, repr((dn, fn, fi, first)))\n        self.ctr -= 1\n\n        data, chash, wark = fi\n        hs = self.handshake(dn, fn, fi)\n        self.assertEqual(hs[\"wark\"], wark)\n\n        sfn = hs[\"name\"]\n        if sfn == fn:\n            print(\"using original name \" + fn)\n        else:\n            print(fn + \" got renamed to \" + sfn)\n            if first:\n                raise Exception(\"wait what\")\n\n        if hs[\"hash\"]:\n            self.assertEqual(hs[\"hash\"][0], chash)\n            self.put_chunk(dn, wark, chash, data)\n        elif first:\n            raise Exception(\"found first; %r, %r\" % ((dn, fn, fi), hs))\n\n        h, b = self.curl(\"%s/%s\" % (dn, sfn))\n        self.assertEqual(b, data)\n\n    def handshake(self, dn, fn, fi):\n        hdr = \"POST /%s/ HTTP/1.1\\r\\nConnection: close\\r\\nContent-Type: text/plain\\r\\nContent-Length: %d\\r\\n\\r\\n\"\n        msg = {\"name\": fn, \"size\": 3, \"lmod\": 1234567890, \"life\": 0, \"hash\": [fi[1]]}\n        buf = json.dumps(msg).encode(\"utf-8\")\n        buf = (hdr % (dn, len(buf))).encode(\"utf-8\") + buf\n        print(\"HS -->\", buf)\n        HttpCli(self.conn.setbuf(buf)).run()\n        ret = self.conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n        print(\"HS <--\", ret)\n        return json.loads(ret[1])\n\n    def put_chunk(self, dn, wark, chash, data):\n        msg = [\n            \"POST /%s/ HTTP/1.1\" % (dn,),\n            \"Connection: close\",\n            \"Content-Type: application/octet-stream\",\n            \"Content-Length: 3\",\n            \"X-Up2k-Hash: \" + chash,\n            \"X-Up2k-Wark: \" + wark,\n            \"\",\n            data,\n        ]\n        buf = \"\\r\\n\".join(msg).encode(\"utf-8\")\n        print(\"PUT -->\", buf)\n        HttpCli(self.conn.setbuf(buf)).run()\n        ret = self.conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n        self.assertEqual(ret[1], \"thank\")\n\n    def curl(self, url, binary=False):\n        h = \"GET /%s HTTP/1.1\\r\\nConnection: close\\r\\n\\r\\n\"\n        HttpCli(self.conn.setbuf((h % (url,)).encode(\"utf-8\"))).run()\n        if binary:\n            h, b = self.conn.s._reply.split(b\"\\r\\n\\r\\n\", 1)\n            return [h.decode(\"utf-8\"), b]\n\n        return self.conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n\n    def log(self, src, msg, c=0):\n        print(msg)\n"
  },
  {
    "path": "tests/test_shr.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport json\nimport os\nimport shutil\nimport sqlite3\nimport tempfile\nimport unittest\n\nfrom copyparty.__init__ import ANYWIN\nfrom copyparty.authsrv import AuthSrv\nfrom copyparty.httpcli import HttpCli\nfrom copyparty.util import absreal\nfrom tests import util as tu\nfrom tests.util import Cfg\n\n\nclass TestShr(unittest.TestCase):\n    def log(self, src, msg, c=0):\n        m = \"%s\" % (msg,)\n        if (\n            \"warning: filesystem-path does not exist:\" in m\n            or \"you are sharing a system directory:\" in m\n            or \"symlink-based deduplication is enabled\" in m\n            or m.startswith(\"hint: argument\")\n        ):\n            return\n\n        print((\"[%s] %s\" % (src, msg)).encode(\"ascii\", \"replace\").decode(\"ascii\"))\n\n    def assertLD(self, url, auth, els, edl):\n        ls = self.ls(url, auth)\n        self.assertEqual(ls[0], len(els) == 2)\n        if not ls[0]:\n            return\n        a = [list(sorted(els[0])), list(sorted(els[1]))]\n        b = [list(sorted(ls[1])), list(sorted(ls[2]))]\n        self.assertEqual(a, b)\n\n        if edl is None:\n            edl = els[1]\n        can_dl = []\n        for fn in b[1]:\n            if fn == \"a.db\":\n                continue\n            furl = url + \"/\" + fn\n            if auth:\n                furl += \"?pw=p1\"\n            h, zb = self.curl(furl, True)\n            if h.startswith(\"HTTP/1.1 200 \"):\n                can_dl.append(fn)\n        self.assertEqual(edl, can_dl)\n\n    def setUp(self):\n        self.td = tu.get_ramdisk()\n        td = os.path.join(self.td, \"vfs\")\n        os.mkdir(td)\n        os.chdir(td)\n        os.mkdir(\"d1\")\n        os.mkdir(\"d2\")\n        os.mkdir(\"d2/d3\")\n        for zs in (\"d1/f1\", \"d2/f2\", \"d2/d3/f3\"):\n            with open(zs, \"wb\") as f:\n                f.write(zs.encode(\"utf-8\"))\n            for dst in (\"d1\", \"d2\", \"d2/d3\"):\n                src, fn = zs.rsplit(\"/\", 1)\n                os.symlink(absreal(zs), dst + \"/l\" + fn[-1:])\n\n        db = sqlite3.connect(\"a.db\")\n        with db:\n            zs = r\"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)\"\n            db.execute(zs)\n        db.close()\n\n    def tearDown(self):\n        os.chdir(tempfile.gettempdir())\n        shutil.rmtree(self.td)\n\n    def cinit(self):\n        self.asrv = AuthSrv(self.args, self.log)\n        self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b\"\", True)\n\n    def test1(self):\n        self.args = Cfg(\n            a=[\"u1:p1\"],\n            v=[\"::A,u1\", \"d1:v1:A,u1\", \"d2/d3:d2/d3:A,u1\"],\n            shr=\"/shr/\",\n            shr1=\"shr/\",\n            shr_db=\"a.db\",\n            shr_v=False,\n        )\n        self.cinit()\n\n        self.assertLD(\"\", True, [[\"d1\", \"d2\", \"v1\"], [\"a.db\"]], [])\n        self.assertLD(\"d1\", True, [[], [\"f1\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"v1\", True, [[], [\"f1\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"d2\", True, [[\"d3\"], [\"f2\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"d2/d3\", True, [[], [\"f3\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"d3\", True, [], [])\n\n        jt = {\n            \"k\": \"r\",\n            \"vp\": [\"/\"],\n            \"pw\": \"\",\n            \"exp\": \"99\",\n            \"perms\": [\"read\"],\n        }\n        print(self.post_json(\"?pw=p1&share\", jt)[1])\n        jt = {\n            \"k\": \"d2\",\n            \"vp\": [\"/d2/\"],\n            \"pw\": \"\",\n            \"exp\": \"99\",\n            \"perms\": [\"read\"],\n        }\n        print(self.post_json(\"?pw=p1&share\", jt)[1])\n        self.conn.shutdown()\n        self.cinit()\n\n        self.assertLD(\"\", True, [[\"d1\", \"d2\", \"v1\"], [\"a.db\"]], [])\n        self.assertLD(\"d1\", True, [[], [\"f1\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"v1\", True, [[], [\"f1\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"d2\", True, [[\"d3\"], [\"f2\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"d2/d3\", True, [[], [\"f3\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"d3\", True, [], [])\n\n        self.assertLD(\"shr/d2\", False, [[], [\"f2\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"shr/d2/d3\", False, [], None)\n\n        self.assertLD(\"shr/r\", False, [[\"d1\"], [\"a.db\"]], [])\n        self.assertLD(\"shr/r/d1\", False, [[], [\"f1\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"shr/r/d2\", False, [], None)  # unfortunate\n        self.assertLD(\"shr/r/d2/d3\", False, [], None)\n\n        self.conn.shutdown()\n\n    def test2(self):\n        self.args = Cfg(\n            a=[\"u1:p1\"],\n            v=[\"::A,u1\", \"d1:v1:A,u1\", \"d2/d3:d2/d3:A,u1\"],\n            shr=\"/shr/\",\n            shr1=\"shr/\",\n            shr_db=\"a.db\",\n            shr_v=False,\n            xvol=True,\n        )\n        self.cinit()\n\n        self.assertLD(\"\", True, [[\"d1\", \"d2\", \"v1\"], [\"a.db\"]], [])\n        self.assertLD(\"d1\", True, [[], [\"f1\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"v1\", True, [[], [\"f1\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"d2\", True, [[\"d3\"], [\"f2\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"d2/d3\", True, [[], [\"f3\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"d3\", True, [], [])\n\n        jt = {\n            \"k\": \"r\",\n            \"vp\": [\"/\"],\n            \"pw\": \"\",\n            \"exp\": \"99\",\n            \"perms\": [\"read\"],\n        }\n        print(self.post_json(\"?pw=p1&share\", jt)[1])\n        jt = {\n            \"k\": \"d2\",\n            \"vp\": [\"/d2/\"],\n            \"pw\": \"\",\n            \"exp\": \"99\",\n            \"perms\": [\"read\"],\n        }\n        print(self.post_json(\"?pw=p1&share\", jt)[1])\n        self.conn.shutdown()\n        self.cinit()\n\n        self.assertLD(\"\", True, [[\"d1\", \"d2\", \"v1\"], [\"a.db\"]], [])\n        self.assertLD(\"d1\", True, [[], [\"f1\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"v1\", True, [[], [\"f1\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"d2\", True, [[\"d3\"], [\"f2\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"d2/d3\", True, [[], [\"f3\", \"l1\", \"l2\", \"l3\"]], None)\n        self.assertLD(\"d3\", True, [], [])\n\n        self.assertLD(\"shr/d2\", False, [[], [\"f2\", \"l1\", \"l2\", \"l3\"]], [\"f2\", \"l2\"])\n        self.assertLD(\"shr/d2/d3\", False, [], [])\n\n        self.assertLD(\"shr/r\", False, [[\"d1\"], [\"a.db\"]], [])\n        self.assertLD(\n            \"shr/r/d1\", False, [[], [\"f1\", \"l1\", \"l2\", \"l3\"]], [\"f1\", \"l1\", \"l2\"]\n        )\n        self.assertLD(\"shr/r/d2\", False, [], [])  # unfortunate\n        self.assertLD(\"shr/r/d2/d3\", False, [], [])\n\n        self.conn.shutdown()\n\n    def ls(self, url: str, auth: bool):\n        zs = url + \"?ls\" + (\"&pw=p1\" if auth else \"\")\n        h, b = self.curl(zs)\n        if not h.startswith(\"HTTP/1.1 200 \"):\n            return (False, [], [])\n        jo = json.loads(b)\n        return (\n            True,\n            [x[\"href\"].rstrip(\"/\") for x in jo.get(\"dirs\") or {}],\n            [x[\"href\"] for x in jo.get(\"files\") or {}],\n        )\n\n    def curl(self, url: str, binary=False):\n        h = \"GET /%s HTTP/1.1\\r\\nConnection: close\\r\\n\\r\\n\"\n        HttpCli(self.conn.setbuf((h % (url,)).encode(\"utf-8\"))).run()\n        if binary:\n            h, b = self.conn.s._reply.split(b\"\\r\\n\\r\\n\", 1)\n            return [h.decode(\"utf-8\"), b]\n\n        return self.conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n\n    def post_json(self, url: str, data):\n        buf = json.dumps(data).encode(\"utf-8\")\n        msg = [\n            \"POST /%s HTTP/1.1\" % (url,),\n            \"Connection: close\",\n            \"Content-Type: application/json\",\n            \"Content-Length: %d\" % (len(buf),),\n            \"\\r\\n\",\n        ]\n        buf = \"\\r\\n\".join(msg).encode(\"utf-8\") + buf\n        print(\"PUT -->\", buf)\n        HttpCli(self.conn.setbuf(buf)).run()\n        return self.conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport unittest\n\nfrom copyparty.__main__ import PY2\nfrom copyparty.util import w8enc\nfrom tests import util as tu\n\n\nclass TestUtils(unittest.TestCase):\n    def cmp(self, orig, t1, t2):\n        if t1 != t2:\n            raise Exception(\"\\n%r\\n%r\\n%r\\n\" % (w8enc(orig), t1, t2))\n\n    def test_quotep(self):\n        if PY2:\n            raise unittest.SkipTest()\n\n        from copyparty.util import _quotep3, _quotep3b, w8dec\n\n        txt = w8dec(tu.randbytes(8192))\n        self.cmp(txt, _quotep3(txt), _quotep3b(txt))\n\n    def test_unquote(self):\n        if PY2:\n            raise unittest.SkipTest()\n\n        from urllib.parse import unquote_to_bytes as u2b\n\n        from copyparty.util import unquote\n\n        for btxt in (\n            tu.randbytes(8192),\n            br\"%ed%91qw,er;ty%20as df?gh+jkl%zxc&vbn <qwe>\\\"rty'uio&asd&nbsp;fgh\",\n        ):\n            self.cmp(btxt, unquote(btxt), u2b(btxt))\n"
  },
  {
    "path": "tests/test_vfs.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport json\nimport os\nimport shutil\nimport tempfile\nimport unittest\n\nfrom copyparty import util\nfrom copyparty.authsrv import VFS, AuthSrv\nfrom tests import util as tu\nfrom tests.util import Cfg\n\n\nclass TestVFS(unittest.TestCase):\n    def setUp(self):\n        self.td = tu.get_ramdisk()\n\n    def tearDown(self):\n        os.chdir(tempfile.gettempdir())\n        shutil.rmtree(self.td)\n\n    def dump(self, vfs):\n        print(json.dumps(vfs, indent=4, sort_keys=True, default=lambda o: o.__dict__))\n\n    def unfoo(self, foo):\n        for k, v in {\"foo\": \"a\", \"bar\": \"b\", \"baz\": \"c\", \"qux\": \"d\"}.items():\n            foo = foo.replace(k, v)\n\n        return foo\n\n    def undot(self, vfs, query, response):\n        self.assertEqual(util.undot(query), response)\n        query = self.unfoo(query)\n        response = self.unfoo(response)\n        self.assertEqual(util.undot(query), response)\n\n    def ls(self, vfs, vpath, uname):\n        # type: (VFS, str, str) -> tuple[str, str, str]\n        \"\"\"helper for resolving and listing a folder\"\"\"\n        vn, rem = vfs.get(vpath, uname, True, False)\n        r1 = vn.ls(rem, uname, False, [[True]])\n        r2 = vn.ls(rem, uname, False, [[True]])\n        self.assertEqual(r1, r2)\n\n        fsdir, real, virt = r1\n        real = [x[0] for x in real]\n        return fsdir, real, virt\n\n    def log(self, src, msg, c=0):\n        pass\n\n    def assertAxs(self, dct, lst):\n        t1 = list(sorted(dct))\n        t2 = list(sorted(lst))\n        self.assertEqual(t1, t2)\n\n    def wipe_vfs(self, td):\n        os.chdir(\"..\")\n        if os.path.exists(td):\n            shutil.rmtree(td)\n        os.mkdir(td)\n        os.chdir(td)\n\n        for a in \"abc\":\n            for b in \"abc\":\n                for c in \"abc\":\n                    folder = \"{0}/{0}{1}/{0}{1}{2}\".format(a, b, c)\n                    os.makedirs(folder)\n                    for d in \"abc\":\n                        fn = \"{}/{}{}{}{}\".format(folder, a, b, c, d)\n                        with open(fn, \"w\") as f:\n                            f.write(fn)\n\n    def test(self):\n        td = os.path.join(self.td, \"vfs\")\n        self.wipe_vfs(td)\n\n        # defaults\n        self.wipe_vfs(td)\n        vfs = AuthSrv(Cfg(), self.log).vfs\n        self.assertEqual(vfs.nodes, {})\n        self.assertEqual(vfs.vpath, \"\")\n        self.assertEqual(vfs.realpath, td)\n        self.assertAxs(vfs.axs.uread, [\"*\"])\n        self.assertAxs(vfs.axs.uwrite, [\"*\"])\n\n        # single read-only rootfs (relative path)\n        self.wipe_vfs(td)\n        vfs = AuthSrv(Cfg(v=[\"a/ab/::r\"]), self.log).vfs\n        self.assertEqual(vfs.nodes, {})\n        self.assertEqual(vfs.vpath, \"\")\n        self.assertEqual(vfs.realpath, os.path.join(td, \"a\", \"ab\"))\n        self.assertAxs(vfs.axs.uread, [\"*\"])\n        self.assertAxs(vfs.axs.uwrite, [])\n\n        # single read-only rootfs (absolute path)\n        self.wipe_vfs(td)\n        vfs = AuthSrv(Cfg(v=[td + \"//a/ac/../aa//::r\"]), self.log).vfs\n        self.assertEqual(vfs.nodes, {})\n        self.assertEqual(vfs.vpath, \"\")\n        self.assertEqual(vfs.realpath, os.path.join(td, \"a\", \"aa\"))\n        self.assertAxs(vfs.axs.uread, [\"*\"])\n        self.assertAxs(vfs.axs.uwrite, [])\n\n        # read-only rootfs with write-only subdirectory (read-write for k)\n        self.wipe_vfs(td)\n        vfs = AuthSrv(\n            Cfg(a=[\"k:k\"], v=[\".::r:rw,k\", \"a/ac/acb:a/ac/acb:w:rw,k\"]),\n            self.log,\n        ).vfs\n        self.assertEqual(len(vfs.nodes), 1)\n        self.assertEqual(vfs.vpath, \"\")\n        self.assertEqual(vfs.realpath, td)\n        self.assertAxs(vfs.axs.uread, [\"*\", \"k\"])\n        self.assertAxs(vfs.axs.uwrite, [\"k\"])\n        n = vfs.nodes[\"a\"]\n        self.assertEqual(len(vfs.nodes), 1)\n        self.assertEqual(n.vpath, \"a\")\n        self.assertEqual(n.realpath, os.path.join(td, \"a\"))\n        self.assertAxs(n.axs.uread, [\"*\", \"k\"])\n        self.assertAxs(n.axs.uwrite, [\"k\"])\n        n = n.nodes[\"ac\"]\n        self.assertEqual(len(vfs.nodes), 1)\n        self.assertEqual(n.vpath, \"a/ac\")\n        self.assertEqual(n.realpath, os.path.join(td, \"a\", \"ac\"))\n        self.assertAxs(n.axs.uread, [\"*\", \"k\"])\n        self.assertAxs(n.axs.uwrite, [\"k\"])\n        n = n.nodes[\"acb\"]\n        self.assertEqual(n.nodes, {})\n        self.assertEqual(n.vpath, \"a/ac/acb\")\n        self.assertEqual(n.realpath, os.path.join(td, \"a\", \"ac\", \"acb\"))\n        self.assertAxs(n.axs.uread, [\"k\"])\n        self.assertAxs(n.axs.uwrite, [\"*\", \"k\"])\n\n        # something funky about the windows path normalization,\n        # doesn't really matter but makes the test messy, TODO?\n\n        fsdir, real, virt = self.ls(vfs, \"/\", \"*\")\n        self.assertEqual(fsdir, td)\n        self.assertEqual(real, [\"b\", \"c\"])\n        self.assertEqual(list(virt), [\"a\"])\n\n        fsdir, real, virt = self.ls(vfs, \"a\", \"*\")\n        self.assertEqual(fsdir, os.path.join(td, \"a\"))\n        self.assertEqual(real, [\"aa\", \"ab\"])\n        self.assertEqual(list(virt), [\"ac\"])\n\n        fsdir, real, virt = self.ls(vfs, \"a/ab\", \"*\")\n        self.assertEqual(fsdir, os.path.join(td, \"a\", \"ab\"))\n        self.assertEqual(real, [\"aba\", \"abb\", \"abc\"])\n        self.assertEqual(list(virt), [])\n\n        fsdir, real, virt = self.ls(vfs, \"a/ac\", \"*\")\n        self.assertEqual(fsdir, os.path.join(td, \"a\", \"ac\"))\n        self.assertEqual(real, [\"aca\", \"acc\"])\n        self.assertEqual(list(virt), [])\n\n        fsdir, real, virt = self.ls(vfs, \"a/ac\", \"k\")\n        self.assertEqual(fsdir, os.path.join(td, \"a\", \"ac\"))\n        self.assertEqual(real, [\"aca\", \"acc\"])\n        self.assertEqual(list(virt), [\"acb\"])\n\n        self.assertRaises(util.Pebkac, vfs.get, \"a/ac/acb\", \"*\", True, False)\n\n        fsdir, real, virt = self.ls(vfs, \"a/ac/acb\", \"k\")\n        self.assertEqual(fsdir, os.path.join(td, \"a\", \"ac\", \"acb\"))\n        self.assertEqual(real, [\"acba\", \"acbb\", \"acbc\"])\n        self.assertEqual(list(virt), [])\n\n        # admin-only rootfs with all-read-only subfolder\n        self.wipe_vfs(td)\n        vfs = AuthSrv(\n            Cfg(a=[\"k:k\"], v=[\".::rw,k\", \"a:a:r\"]),\n            self.log,\n        ).vfs\n        self.assertEqual(len(vfs.nodes), 1)\n        self.assertEqual(vfs.vpath, \"\")\n        self.assertEqual(vfs.realpath, td)\n        self.assertAxs(vfs.axs.uread, [\"k\"])\n        self.assertAxs(vfs.axs.uwrite, [\"k\"])\n        n = vfs.nodes[\"a\"]\n        self.assertEqual(len(vfs.nodes), 1)\n        self.assertEqual(n.vpath, \"a\")\n        self.assertEqual(n.realpath, os.path.join(td, \"a\"))\n        self.assertAxs(n.axs.uread, [\"*\", \"k\"])\n        self.assertAxs(n.axs.uwrite, [])\n        perm_na = (False, False, False, False, False, False, False, False, False)\n        perm_rw = (True, True, False, False, False, False, False, False, False)\n        perm_ro = (True, False, False, False, False, False, False, False, False)\n        self.assertEqual(vfs.can_access(\"/\", \"*\"), perm_na)\n        self.assertEqual(vfs.can_access(\"/\", \"k\"), perm_rw)\n        self.assertEqual(vfs.can_access(\"/a\", \"*\"), perm_ro)\n        self.assertEqual(vfs.can_access(\"/a\", \"k\"), perm_ro)\n\n        # breadth-first construction\n        self.wipe_vfs(td)\n        vfs = AuthSrv(\n            Cfg(\n                v=[\n                    \"a/ac/acb:a/ac/acb:w\",\n                    \"a:a:w\",\n                    \".::r\",\n                    \"abacdfasdq:abacdfasdq:w\",\n                    \"a/ac:a/ac:w\",\n                ],\n            ),\n            self.log,\n        ).vfs\n\n        # sanitizing relative paths\n        self.undot(vfs, \"foo/bar/../baz/qux\", \"foo/baz/qux\")\n        self.undot(vfs, \"foo/../bar\", \"bar\")\n        self.undot(vfs, \"foo/../../bar\", \"bar\")\n        self.undot(vfs, \"foo/../../\", \"\")\n        self.undot(vfs, \"./.././foo/\", \"foo\")\n        self.undot(vfs, \"./.././foo/..\", \"\")\n\n        # shadowing\n        self.wipe_vfs(td)\n        vfs = AuthSrv(Cfg(v=[\".::r\", \"b:a/ac:r\"]), self.log).vfs\n\n        fsp, r1, v1 = self.ls(vfs, \"\", \"*\")\n        self.assertEqual(fsp, td)\n        self.assertEqual(r1, [\"b\", \"c\"])\n        self.assertEqual(list(v1), [\"a\"])\n\n        fsp, r1, v1 = self.ls(vfs, \"a\", \"*\")\n        self.assertEqual(fsp, os.path.join(td, \"a\"))\n        self.assertEqual(r1, [\"aa\", \"ab\"])\n        self.assertEqual(list(v1), [\"ac\"])\n\n        fsp1, r1, v1 = self.ls(vfs, \"a/ac\", \"*\")\n        fsp2, r2, v2 = self.ls(vfs, \"b\", \"*\")\n        self.assertEqual(fsp1, os.path.join(td, \"b\"))\n        self.assertEqual(fsp2, os.path.join(td, \"b\"))\n        self.assertEqual(r1, [\"ba\", \"bb\", \"bc\"])\n        self.assertEqual(r1, r2)\n        self.assertEqual(list(v1), list(v2))\n\n        # config file parser\n        cfg_path = os.path.join(self.td, \"test.cfg\")\n        with open(cfg_path, \"wb\") as f:\n            f.write(\n                util.dedent(\n                    \"\"\"\n                    u a:123\n                    u asd:fgh:jkl\n\n                    ./src\n                    /dst\n                    r a\n                    rw asd\n                    \"\"\"\n                ).encode(\"utf-8\")\n            )\n\n        self.wipe_vfs(td)\n        au = AuthSrv(Cfg(c=[cfg_path]), self.log)\n        self.assertEqual(au.acct[\"a\"], \"123\")\n        self.assertEqual(au.acct[\"asd\"], \"fgh:jkl\")\n        n = au.vfs\n        # root was not defined, so PWD with no access to anyone\n        self.assertEqual(n.vpath, \"\")\n        self.assertEqual(n.realpath, \"\")\n        self.assertAxs(n.axs.uread, [])\n        self.assertAxs(n.axs.uwrite, [])\n        self.assertEqual(len(n.nodes), 1)\n        n = n.nodes[\"dst\"]\n        self.assertEqual(n.vpath, \"dst\")\n        self.assertEqual(n.realpath, os.path.join(td, \"src\"))\n        self.assertAxs(n.axs.uread, [\"a\", \"asd\"])\n        self.assertAxs(n.axs.uwrite, [\"asd\"])\n        self.assertEqual(len(n.nodes), 0)\n\n        os.unlink(cfg_path)\n"
  },
  {
    "path": "tests/test_webdav.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport os\nimport shutil\nimport tempfile\nimport time\nimport unittest\n\nfrom copyparty.authsrv import AuthSrv\nfrom copyparty.httpcli import HttpCli\nfrom tests import util as tu\nfrom tests.util import TC, Cfg, pfind2ls\n\n# tcpdump of `rclone ls dav:`\nRCLONE_PROPFIND = \"\"\"PROPFIND /%s HTTP/1.1\nHost: 127.0.0.1:3923\nUser-Agent: rclone/v1.67.0\nContent-Length: 308\nAuthorization: Basic azp1\nDepth: 1\nReferer: http://127.0.0.1:3923/\nAccept-Encoding: gzip\n\n<?xml version=\"1.0\"?>\n<d:propfind  xmlns:d=\"DAV:\" xmlns:oc=\"http://owncloud.org/ns\" xmlns:nc=\"http://nextcloud.org/ns\">\n <d:prop>\n  <d:displayname />\n  <d:getlastmodified />\n  <d:getcontentlength />\n  <d:resourcetype />\n  <d:getcontenttype />\n  <oc:checksums />\n  <oc:permissions />\n </d:prop>\n</d:propfind>\n\"\"\"\n\n\n# tcpdump of `rclone copy fa dav:/a/`  (it does a mkcol first)\nRCLONE_MKCOL = \"\"\"MKCOL /%s HTTP/1.1\nHost: 127.0.0.1:3923\nUser-Agent: rclone/v1.67.0\nAuthorization: Basic azp1\nReferer: http://127.0.0.1:3923/\nAccept-Encoding: gzip\n\\n\"\"\"\n\n\n# tcpdump of `rclone copy fa dav:/a/`  (the actual upload)\nRCLONE_PUT = \"\"\"PUT /%s HTTP/1.1\nHost: 127.0.0.1:3923\nUser-Agent: rclone/v1.67.0\nContent-Length: 6\nAuthorization: Basic azp1\nContent-Type: application/octet-stream\nOc-Checksum: SHA1:f5e3dc3fb27af53cd0005a1184e2df06481199e8\nReferer: http://127.0.0.1:3923/\nX-Oc-Mtime: 1689453578\nAccept-Encoding: gzip\n\nfgsfds\"\"\"\n\n\nRCLONE_PUT_FLOAT = \"\"\"PUT /%s HTTP/1.1\nHost: 127.0.0.1:3923\nUser-Agent: rclone/v1.67.0\nContent-Length: 6\nAuthorization: Basic azp1\nContent-Type: application/octet-stream\nOc-Checksum: SHA1:f5e3dc3fb27af53cd0005a1184e2df06481199e8\nReferer: http://127.0.0.1:3923/\nX-Oc-Mtime: 1689453578.123\nAccept-Encoding: gzip\n\nfgsfds\"\"\"\n\n\n# tcpdump of `rclone delete dav:/a/d1/`  (it does propfind recursively and then this on each file)\n# (note: `rclone rmdirs dav:/a/d1/` does the same thing but just each folder after asserting they're empty)\nRCLONE_DELETE = \"\"\"DELETE /%s HTTP/1.1\nHost: 127.0.0.1:3923\nUser-Agent: rclone/v1.67.0\nAuthorization: Basic azp1\nReferer: http://127.0.0.1:3923/\nAccept-Encoding: gzip\n\\n\"\"\"\n\n\n# tcpdump of `rclone move dav:/a/d1/d2 /a/d1/d3`  (it does a lot of boilerplate propfinds/mkcols before)\nRCLONE_MOVE = \"\"\"MOVE /%s HTTP/1.1\nHost: 127.0.0.1:3923\nUser-Agent: rclone/v1.67.0\nAuthorization: Basic azp1\nDestination: http://127.0.0.1:3923/%s\nOverwrite: T\nReferer: http://127.0.0.1:3923/\nAccept-Encoding: gzip\n\\n\"\"\"\n\n\nclass TestHttpCli(TC):\n    def setUp(self):\n        self.td = tu.get_ramdisk()\n        self.maxDiff = 99999\n\n    def tearDown(self):\n        self.conn.shutdown()\n        os.chdir(tempfile.gettempdir())\n        shutil.rmtree(self.td)\n\n    def test(self):\n        td = os.path.join(self.td, \"vfs\")\n        os.mkdir(td)\n        os.chdir(td)\n\n        self.fn = \"g{:x}g\".format(int(time.time() * 3))\n        vcfg = [\n            \"r:r:r,u\",\n            \"w:w:w,u\",\n            \"a:a:A,u\",\n            \"x:x:r,u2\",\n            \"x/r:x/r:r,u\",\n            \"x/x:x/x:r,u2\",\n        ]\n        self.args = Cfg(v=vcfg, a=[\"u:u\", \"u2:u2\"])\n        self.asrv = AuthSrv(self.args, self.log)\n        self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b\"\", True)\n\n        self.fns = [\"%s/%s\" % (zs.split(\":\")[0], self.fn) for zs in vcfg]\n        for fp in self.fns:\n            try:\n                os.makedirs(os.path.dirname(fp))\n            except:\n                pass\n            with open(fp, \"wb\") as f:\n                f.write((\"ok %s\\n\" % (fp,)).encode(\"utf-8\"))\n\n        ##\n        ## depth:1 (regular listing)\n\n        # unmapped root; should return list of volumes\n        h, b = self.req(RCLONE_PROPFIND % (\"\",))\n        fns = pfind2ls(b)\n        self.assertStart(\"HTTP/1.1 207 Multi-Status\\r\", h)\n        self.assertListEqual(fns, [\"/\", \"/a/\", \"/r/\"])\n\n        # toplevel of a volume; has one file\n        h, b = self.req(RCLONE_PROPFIND % (\"a\",))\n        fns = pfind2ls(b)\n        self.assertStart(\"HTTP/1.1 207 Multi-Status\\r\", h)\n        self.assertListEqual(fns, [\"/a/\", \"/a/\" + self.fn])\n\n        # toplevel of a volume; has one file\n        h, b = self.req(RCLONE_PROPFIND % (\"r\",))\n        fns = pfind2ls(b)\n        self.assertStart(\"HTTP/1.1 207 Multi-Status\\r\", h)\n        self.assertListEqual(fns, [\"/r/\", \"/r/\" + self.fn])\n\n        # toplevel of write-only volume; has one file, will not list\n        h, b = self.req(RCLONE_PROPFIND % (\"w\",))\n        fns = pfind2ls(b)\n        self.assertStart(\"HTTP/1.1 207 Multi-Status\\r\", h)\n        self.assertListEqual(fns, [\"/w/\"])\n\n        ##\n        ## auth challenge\n\n        bad_pfind = RCLONE_PROPFIND.replace(\"Authorization: Basic azp1\\n\", \"\")\n        bad_put = RCLONE_PUT.replace(\"Authorization: Basic azp1\\n\", \"\")\n        urls = [\"\", \"r\", \"w\", \"a\"]\n        urls += [x + \"/\" + self.fn for x in urls[1:]]\n        for url in urls:\n            for q in (bad_pfind, bad_put):\n                h, b = self.req(q % (url,))\n                self.assertStart(\"HTTP/1.1 401 Unauthorized\\r\", h)\n                self.assertIn('\\nWWW-Authenticate: Basic realm=\"a\"\\r', h)\n\n        ##\n        ## depth:0 (recursion)\n\n        # depth:0 from unmapped root should work;\n        # will NOT list contents of /x/r/ due to current limitations\n        # (stops descending at first non-accessible volume)\n        recursive = RCLONE_PROPFIND.replace(\"Depth: 1\\n\", \"\")\n        h, b = self.req(recursive % (\"\",))\n        fns = pfind2ls(b)\n        expect = [\"/\", \"/a/\", \"/r/\"]\n        expect += [x + self.fn for x in expect[1:]]\n        self.assertListEqual(fns, expect)\n\n        # same thing here...\n        h, b = self.req(recursive % (\"/x\",))\n        fns = pfind2ls(b)\n        self.assertListEqual(fns, [])\n\n        # but this obviously works\n        h, b = self.req(recursive % (\"/x/r\",))\n        fns = pfind2ls(b)\n        self.assertListEqual(fns, [\"/x/r/\", \"/x/r/\" + self.fn])\n\n        ##\n        ## uploading\n\n        # rclone does a propfind on the target file first; expects 404\n        h, b = self.req(RCLONE_PROPFIND % (\"a/fa\",))\n        self.assertStart(\"HTTP/1.1 404 Not Found\\r\", h)\n\n        # then it does a mkcol (mkdir), expecting 405 (exists)\n        h, b = self.req(RCLONE_MKCOL % (\"a\",))\n        self.assertStart(\"HTTP/1.1 405 Method Not Allowed\\r\", h)\n\n        # then it uploads the file\n        h, b = self.req(RCLONE_PUT % (\"a/fa\",))\n        self.assertStart(\"HTTP/1.1 201 Created\\r\", h)\n\n        # float x-oc-mtime should be accepted\n        h, b = self.req(RCLONE_PUT_FLOAT % (\"a/fb\",))\n        self.assertStart(\"HTTP/1.1 201 Created\\r\", h)\n        self.assertAlmostEqual(os.path.getmtime(\"a/fb\"), 1689453578.123, places=3)\n\n        # then it does a propfind to confirm\n        h, b = self.req(RCLONE_PROPFIND % (\"a/fa\",))\n        fns = pfind2ls(b)\n        self.assertStart(\"HTTP/1.1 207 Multi-Status\\r\", h)\n        self.assertListEqual(fns, [\"/a/fa\"])\n\n        ##\n        ## upload into set of subfolders that don't exist yet\n\n        # rclone does this:\n        # propfind /a/d1/d2/fa => 404\n        # mkcol /a/d1/d2/ => 409\n        # propfind /a/d1/d2/ => 404\n        # mkcol /a/d1/ => 201\n        # mkcol /a/d1/d2/ => 201\n        # put /a/d1/d2/fa => 201\n        # propfind /a/d1/d2/fa => 207\n        # ...some of which already tested above;\n\n        h, b = self.req(RCLONE_PROPFIND % (\"/a/d1/d2/\",))\n        self.assertStart(\"HTTP/1.1 404 Not Found\\r\", h)\n\n        h, b = self.req(RCLONE_PROPFIND % (\"/a/d1/\",))\n        self.assertStart(\"HTTP/1.1 404 Not Found\\r\", h)\n\n        h, b = self.req(RCLONE_MKCOL % (\"/a/d1/d2/\",))\n        self.assertStart(\"HTTP/1.1 409 Conflict\\r\", h)\n\n        h, b = self.req(RCLONE_MKCOL % (\"/a/d1/\",))\n        self.assertStart(\"HTTP/1.1 201 Created\\r\", h)\n\n        h, b = self.req(RCLONE_MKCOL % (\"/a/d1/d2/\",))\n        self.assertStart(\"HTTP/1.1 201 Created\\r\", h)\n\n        h, b = self.req(RCLONE_PUT % (\"a/d1/d2/fa\",))\n        self.assertStart(\"HTTP/1.1 201 Created\\r\", h)\n\n        ##\n        ## rename\n\n        h, b = self.req(RCLONE_MOVE % (\"a/d1/d2/\", \"a/d1/d3/\"))\n        self.assertStart(\"HTTP/1.1 201 Created\\r\", h)\n        self.assertListEqual(os.listdir(\"a/d1\"), [\"d3\"])\n\n        ##\n        ## delete\n\n        h, b = self.req(RCLONE_DELETE % (\"a/d1\",))\n        self.assertStart(\"HTTP/1.1 200 OK\\r\", h)\n        if os.path.exists(\"a/d1\"):\n            self.fail(\"a/d1 still exists\")\n\n    def req(self, q):\n        h, b = q.split(\"\\n\\n\", 1)\n        q = h.replace(\"\\n\", \"\\r\\n\") + \"\\r\\n\\r\\n\" + b\n        conn = self.conn.setbuf(q.encode(\"utf-8\"))\n        HttpCli(conn).run()\n        return conn.s._reply.decode(\"utf-8\").split(\"\\r\\n\\r\\n\", 1)\n\n    def log(self, src, msg, c=0):\n        print(msg)\n"
  },
  {
    "path": "tests/util.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\nfrom __future__ import print_function, unicode_literals\n\nimport os\nimport random\nimport re\nimport shutil\nimport socket\nimport subprocess as sp\nimport sys\nimport tempfile\nimport threading\nimport time\nimport unittest\nfrom argparse import Namespace\n\nimport jinja2\n\nfrom copyparty.__init__ import ANYWIN, MACOS, WINDOWS, E\n\nJ2_ENV = jinja2.Environment(loader=jinja2.BaseLoader)  # type: ignore\nJ2_FILES = J2_ENV.from_string(\"{{ files|join('\\n') }}\\nJ2EOT\")\n\n\ndef nah(*a, **ka):\n    return False\n\n\ndef eprint(*a, **ka):\n    ka[\"file\"] = sys.stderr\n    print(*a, **ka)\n    sys.stderr.flush()\n\n\nfrom copyparty.__main__ import init_E\nfrom copyparty.broker_thr import BrokerThr\nfrom copyparty.ico import Ico\nfrom copyparty.u2idx import U2idx\nfrom copyparty.up2k import Up2k\nfrom copyparty.util import FHC, CachedDict, Garda, Unrecv\n\ninit_E(E)\n\n\ndef randbytes(n):\n    return random.getrandbits(n * 8).to_bytes(n, \"little\")\n\n\ndef runcmd(argv):\n    p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE)\n    stdout, stderr = p.communicate()\n    stdout = stdout.decode(\"utf-8\")\n    stderr = stderr.decode(\"utf-8\")\n    return [p.returncode, stdout, stderr]\n\n\ndef chkcmd(argv):\n    ok, sout, serr = runcmd(argv)\n    if ok != 0:\n        raise Exception(serr)\n\n    return sout, serr\n\n\ndef get_ramdisk():\n    def subdir(top):\n        for d in os.listdir(top):\n            if not d.startswith(\"cptd-\"):\n                continue\n            p = os.path.join(top, d)\n            st = os.stat(p)\n            if time.time() - st.st_mtime > 300:\n                shutil.rmtree(p)\n        ret = os.path.join(top, \"cptd-{}\".format(os.getpid()))\n        shutil.rmtree(ret, True)\n        os.mkdir(ret)\n        return ret\n\n    for vol in [\"/dev/shm\", \"/Volumes/cptd\"]:  # nosec (singleton test)\n        if os.path.exists(vol):\n            return subdir(vol)\n\n    if os.path.exists(\"/Volumes\"):\n        sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n        while True:\n            try:\n                sck.bind((\"127.0.0.1\", 2775))\n                break\n            except:\n                print(\"waiting for 2775\")\n                time.sleep(0.5)\n\n        v = \"/Volumes/cptd\"\n        if os.path.exists(v):\n            return subdir(v)\n\n        # hdiutil eject /Volumes/cptd/\n        devname, _ = chkcmd(\"hdiutil attach -nomount ram://131072\".split())\n        devname = devname.strip()\n        print(\"devname: [{}]\".format(devname))\n        for _ in range(10):\n            try:\n                _, _ = chkcmd([\"diskutil\", \"eraseVolume\", \"HFS+\", \"cptd\", devname])\n                with open(\"/Volumes/cptd/.metadata_never_index\", \"wb\") as f:\n                    f.write(b\"orz\")\n\n                try:\n                    shutil.rmtree(\"/Volumes/cptd/.fseventsd\")\n                except:\n                    pass\n\n                sck.close()\n                return subdir(\"/Volumes/cptd\")\n            except Exception as ex:\n                print(repr(ex))\n                time.sleep(0.25)\n\n        raise Exception(\"ramdisk creation failed\")\n\n    ret = os.path.join(tempfile.gettempdir(), \"copyparty-test\")\n    if not os.path.isdir(ret):\n        os.mkdir(ret)\n\n    return subdir(ret)\n\n\ndef pfind2ls(xml):\n    return [x.split(\"<\", 1)[0] for x in xml.split(\"<D:href>\")[1:]]\n\n\nclass TC(unittest.TestCase):\n    def __init__(self, *a, **ka):\n        super(TC, self).__init__(*a, **ka)\n\n    def assertStart(self, member, container, msg=None):\n        if not container.startswith(member):\n            standardMsg = \"%s not found in %s\" % (member, container)\n            self.fail(self._formatMessage(msg, standardMsg))\n\n\nclass Cfg(Namespace):\n    def __init__(self, a=None, v=None, c=None, **ka0):\n        ka = {}\n\n        ex = \"allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt dlni e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only http_no_tcp ih ihead localtime log_badxml magic md_no_br nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_dupe_m no_fnugg no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip no_zls nrand nsort nw og og_no_head og_s_title ohead opds q rand re_dirsz reflink rm_partial rmagic rss smb srch_dbg srch_excl srch_icase stats ui_noacci ui_nocpla ui_noctxb ui_nolbar ui_nombar ui_nonav ui_notree ui_norepl ui_nosrvi uqe usernames vague_403 vc ver vol_nospawn vol_or_crash wo_up_readme write_uplog xdev xlink xvol zipmaxu zs\"\n        ka.update(**{k: False for k in ex.split()})\n\n        ex = \"dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump wram re_dhash see_dots plain_ip\"\n        ka.update(**{k: True for k in ex.split()})\n\n        ex = \"ah_cli ah_gen css_browser dbpath hist ipu js_browser js_other mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua ua_nodoc ua_nozip\"\n        ka.update(**{k: None for k in ex.split()})\n\n        ex = \"gid uid\"\n        ka.update(**{k: -1 for k in ex.split()})\n\n        ex = \"hash_mt hsortn qdel safe_dedup scan_pr_r scan_pr_s scan_st_r srch_time tail_fd tail_rate th_spec_p u2abort u2j u2sz unp_who\"\n        ka.update(**{k: 1 for k in ex.split()})\n\n        ex = \"ac_convt au_vol dl_list du_iwho mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt th_qv th_qvx ups_who ver_iwho zip_who\"\n        ka.update(**{k: 9 for k in ex.split()})\n\n        ex = \"ctl_re db_act forget_ip idp_cookie idp_store k304 loris no304 nosubtle qr_pin qr_wait re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs\"\n        ka.update(**{k: 0 for k in ex.split()})\n\n        ex = \"ah_alg bname chdir chmod_f chpw_db db_xattr doctitle df epilogues exit favico fika ipa ipar html_head html_head_d html_head_s idp_login idp_logout lg_sba lg_sbf log_date log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i opds_exts preadmes prologues readmes shr shr1 shr_site site smsg tcolor textfiles txt_eol ufavico ufavico_h unlist up_site vc_url vname xff_src zipmaxt R RS SR\"\n        ka.update(**{k: \"\" for k in ex.split()})\n\n        ex = \"apnd_who ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban cachectl http_vary rcm rss_fmt_d rss_fmt_t spinner\"\n        ka.update(**{k: \"no\" for k in ex.split()})\n\n        ex = \"ext_th grp idp_h_usr idp_hm_usr ipr on403 on404 qr_file xac xad xar xau xban xbc xbd xbr xbu xiu xm\"\n        ka.update(**{k: [] for k in ex.split()})\n\n        ex = \"exp_lg exp_md\"\n        ka.update(**{k: {} for k in ex.split()})\n\n        ka.update(ka0)\n\n        super(Cfg, self).__init__(\n            a=a or [],\n            v=v or [],\n            c=c,\n            E=E,\n            auth_ord=\"idp,ipu\",\n            bup_ck=\"sha512\",\n            casechk=\"a\" if ANYWIN or MACOS else \"n\",\n            chmod_d=\"755\",\n            cookie_cmax=8192,\n            cookie_nmax=50,\n            dbd=\"wal\",\n            du_who=\"all\",\n            dk_salt=\"b\" * 16,\n            fk_salt=\"a\" * 16,\n            fsnt=\"lin\",\n            grp_all=\"acct\",\n            idp_gsep=re.compile(\"[|:;+,]\"),\n            iobuf=256 * 1024,\n            lang=\"eng\",\n            log_badpwd=1,\n            logout=573,\n            md_hist=\"s\",\n            mte={\"a\": True},\n            mth={},\n            mtp=[],\n            put_ck=\"sha512\",\n            put_name=\"put-{now.6f}-{cip}.bin\",\n            pw_hdr=\"pw\",\n            pw_urlp=\"pw\",\n            mv_retry=\"0/0\",\n            rm_retry=\"0/0\",\n            rotf_tz=\"UTC\",\n            rss_sort=\"m\",\n            rw_edit=\"md\",\n            s_rd_sz=256 * 1024,\n            s_wr_sz=256 * 1024,\n            shr_who=\"auth\",\n            sort=\"href\",\n            srch_hits=99999,\n            SRS=\"/\",\n            th_covers=[\"folder.png\"],\n            th_coversd=[\"folder.png\"],\n            th_covers_set=set([\"folder.png\"]),\n            th_coversd_set=set([\"folder.png\"]),\n            th_crop=\"y\",\n            th_size=\"320x256\",\n            th_x3=\"n\",\n            u2sort=\"s\",\n            u2ts=\"c\",\n            ui_filesz=\"1\",\n            unpost=600,\n            ver_who=\"all\",\n            warksalt=\"hunter2\",\n            **ka\n        )\n\n\nclass NullBroker(object):\n    def __init__(self, args, asrv):\n        self.args = args\n        self.asrv = asrv\n\n    def say(self, *args):\n        pass\n\n    def ask(self, *args):\n        pass\n\n\nclass VSock(object):\n    def __init__(self, buf):\n        self._query = buf\n        self._reply = b\"\"\n        self.family = socket.AF_INET\n        self.sendall = self.send\n\n    def recv(self, sz):\n        ret = self._query[:sz]\n        self._query = self._query[sz:]\n        return ret\n\n    def send(self, buf):\n        self._reply += buf\n        return len(buf)\n\n    def getsockname(self):\n        return (\"a\", 1)\n\n    def settimeout(self, a):\n        pass\n\n\nclass VHub(object):\n    def __init__(self, args, asrv, log):\n        self.args = args\n        self.asrv = asrv\n        self.log = log\n        self.is_dut = True\n        self.up2k = Up2k(self)\n\n    def reload(self, a, b):\n        pass\n\n\nclass VBrokerThr(BrokerThr):\n    def __init__(self, hub):\n        self.hub = hub\n        self.log = hub.log\n        self.args = hub.args\n        self.asrv = hub.asrv\n\n\nclass VHttpSrv(object):\n    def __init__(self, args, asrv, log):\n        self.args = args\n        self.asrv = asrv\n        self.log = log\n        self.hub = None\n\n        self.broker = NullBroker(args, asrv)\n        self.bad_ver = False\n        self.prism = None\n        self.ipr = None\n        self.bans = {}\n        self.tdls = self.dls = {}\n        self.tdli = self.dli = {}\n        self.nreq = 0\n        self.nsus = 0\n\n        aliases = [\"splash\", \"shares\", \"browser\", \"browser2\", \"msg\", \"md\", \"mde\"]\n        self.j2 = {x: J2_FILES for x in aliases}\n\n        self.gpwd = Garda(\"\")\n        self.g404 = Garda(\"\")\n        self.g403 = Garda(\"\")\n        self.gurl = Garda(\"\")\n\n        self.u2idx = None\n\n    def cachebuster(self):\n        return \"a\"\n\n    def get_u2idx(self):\n        self.u2idx = self.u2idx or U2idx(self)\n        return self.u2idx\n\n    def shutdown(self):\n        if self.u2idx:\n            self.u2idx.shutdown()\n\n\nclass VHttpSrvUp2k(VHttpSrv):\n    def __init__(self, args, asrv, log):\n        super(VHttpSrvUp2k, self).__init__(args, asrv, log)\n        self.hub = VHub(args, asrv, log)\n        self.broker = VBrokerThr(self.hub)\n\n    def shutdown(self):\n        self.hub.up2k.shutdown()\n        if self.u2idx:\n            self.u2idx.shutdown()\n\n\nclass VHttpConn(object):\n    def __init__(self, args, asrv, log, buf, use_up2k=False):\n        self.t0 = time.time()\n        self.aclose = {}\n        self.addr = (\"127.0.0.1\", \"42069\")\n        self.args = args\n        self.asrv = asrv\n        self.bans = {}\n        self.tdls = self.dls = {}\n        self.tdli = self.dli = {}\n        self.freshen_pwd = 0.0\n\n        Ctor = VHttpSrvUp2k if use_up2k else VHttpSrv\n        self.hsrv = Ctor(args, asrv, log)\n        self.ico = Ico(args)\n        self.ipr = None\n        self.ipa_nm = None\n        self.ipar_nm = None\n        self.lf_url = None\n        self.log_func = log\n        self.log_src = \"a\"\n        self.mutex = threading.Lock()\n        self.pipes = CachedDict(1)\n        self.u2mutex = threading.Lock()\n        self.nbyte = 0\n        self.nid = None\n        self.nreq = -1\n        self.thumbcli = None\n        self.u2fh = FHC()\n\n        self.get_u2idx = self.hsrv.get_u2idx\n        self.setbuf(buf)\n\n    def setbuf(self, buf):\n        self.s = VSock(buf)\n        self.sr = Unrecv(self.s, None)  # type: ignore\n        return self\n\n    def shutdown(self):\n        self.hsrv.shutdown()\n\n\nif WINDOWS:\n    os.system(\"rem\")\n"
  }
]