Compare commits
260 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a00ff2b086 | ||
|
|
e4acddc23b | ||
|
|
2b2d8e4e02 | ||
|
|
5501d49032 | ||
|
|
fa54b2eec4 | ||
|
|
cb0160021f | ||
|
|
93a723d588 | ||
|
|
8ebe1fb5e8 | ||
|
|
2acdf685b1 | ||
|
|
9f122ccd16 | ||
|
|
03be26fafc | ||
|
|
df5d309d6e | ||
|
|
c355f9bd91 | ||
|
|
9c28ba417e | ||
|
|
705b58c741 | ||
|
|
510302d667 | ||
|
|
025a537413 | ||
|
|
60a1ff0fc0 | ||
|
|
f94a0b1bff | ||
|
|
4ccfeeb2cd | ||
|
|
2646f6a4f2 | ||
|
|
b286ab539e | ||
|
|
2cca6e0922 | ||
|
|
db51f1b063 | ||
|
|
d979c47f50 | ||
|
|
e64b87b99b | ||
|
|
b985011a00 | ||
|
|
c2ed2314c8 | ||
|
|
cd496658c3 | ||
|
|
deca082623 | ||
|
|
0ea8bb7c83 | ||
|
|
1fb251a4c2 | ||
|
|
4295923b76 | ||
|
|
572aa4b26c | ||
|
|
b1359f039f | ||
|
|
867d8ee49e | ||
|
|
04c86e8a89 | ||
|
|
bc0cb43ef9 | ||
|
|
769454fdce | ||
|
|
4ee81af8f6 | ||
|
|
8b0e66122f | ||
|
|
8a98efb929 | ||
|
|
b6fd555038 | ||
|
|
7eb413ad51 | ||
|
|
4421d509eb | ||
|
|
793ffd7b01 | ||
|
|
1e22222c60 | ||
|
|
544e0549bc | ||
|
|
83178d0836 | ||
|
|
c44f5f5701 | ||
|
|
138f5bc989 | ||
|
|
e4759f86ef | ||
|
|
d71416437a | ||
|
|
a84c583b2c | ||
|
|
cdacdccdb8 | ||
|
|
d3ccd3f174 | ||
|
|
cb6de0387d | ||
|
|
abff40519d | ||
|
|
55c74ad164 | ||
|
|
673b4f7e23 | ||
|
|
d11e02da49 | ||
|
|
8790f89e08 | ||
|
|
33442026b8 | ||
|
|
03193de6d0 | ||
|
|
8675ff40f3 | ||
|
|
d88889d3fc | ||
|
|
6f244d4335 | ||
|
|
cacca663b3 | ||
|
|
d5109be559 | ||
|
|
d999f06bb9 | ||
|
|
a1a8a8c7b5 | ||
|
|
fdd6f3b4a6 | ||
|
|
f5191973df | ||
|
|
ddbaebe779 | ||
|
|
42099baeff | ||
|
|
2459965ca8 | ||
|
|
6acf436573 | ||
|
|
f217e1ce71 | ||
|
|
418000aee3 | ||
|
|
dbbba9625b | ||
|
|
397bc92fbc | ||
|
|
6e615dcd03 | ||
|
|
9ac5908b33 | ||
|
|
50912480b9 | ||
|
|
24b9b8319d | ||
|
|
b0f4f0b653 | ||
|
|
05bbd41c4b | ||
|
|
8f5f8a3cda | ||
|
|
c8938fc033 | ||
|
|
1550350e05 | ||
|
|
5cc190c026 | ||
|
|
d6a0a738ce | ||
|
|
f5fe3678ee | ||
|
|
f2a7925387 | ||
|
|
fa953ced52 | ||
|
|
f0000d9861 | ||
|
|
4e67516719 | ||
|
|
29db7a6270 | ||
|
|
852499e296 | ||
|
|
f1775fd51c | ||
|
|
4bb306932a | ||
|
|
2a37e81bd8 | ||
|
|
6a312ca856 | ||
|
|
e7f3e475a2 | ||
|
|
854ba0ec06 | ||
|
|
209b49d771 | ||
|
|
949baae539 | ||
|
|
5f4ea27586 | ||
|
|
099cc97247 | ||
|
|
592b7d6315 | ||
|
|
0880bf55a1 | ||
|
|
4cbffec0ec | ||
|
|
cc355417d4 | ||
|
|
e2bc573e61 | ||
|
|
41c0376177 | ||
|
|
c01cad091e | ||
|
|
eb349f339c | ||
|
|
24d8caaf3e | ||
|
|
5ac2c20959 | ||
|
|
bb72e6bf30 | ||
|
|
d8142e866a | ||
|
|
7b7979fd61 | ||
|
|
749616d09d | ||
|
|
5485c6d7ca | ||
|
|
b7aea38d77 | ||
|
|
0ecd9f99e6 | ||
|
|
ca04a00662 | ||
|
|
8a09601be8 | ||
|
|
1fe0d4693e | ||
|
|
bba8a3c6bc | ||
|
|
e3d7f0c7d5 | ||
|
|
be7bb71bbc | ||
|
|
e0c4829ec6 | ||
|
|
5af1575329 | ||
|
|
884f966b86 | ||
|
|
f6c6fbc223 | ||
|
|
b0cc396bca | ||
|
|
ae463518f6 | ||
|
|
2be2e9a0d8 | ||
|
|
e405fddf74 | ||
|
|
c269b0dd91 | ||
|
|
8c3211263a | ||
|
|
bf04e7c089 | ||
|
|
c7c6e48b1a | ||
|
|
974ca773be | ||
|
|
9270c2df19 | ||
|
|
b39ff92f34 | ||
|
|
7454167f78 | ||
|
|
5ceb3a962f | ||
|
|
52bd5642da | ||
|
|
c39c93725f | ||
|
|
d00f0b9fa7 | ||
|
|
01cfc70982 | ||
|
|
e6aec189bd | ||
|
|
c98fff1647 | ||
|
|
0009e31bd3 | ||
|
|
db95e880b2 | ||
|
|
e69fea4a59 | ||
|
|
4360800a6e | ||
|
|
b179e2b031 | ||
|
|
ecdec75b4e | ||
|
|
5cb2e33353 | ||
|
|
43ff2e531a | ||
|
|
1c2c9db8f0 | ||
|
|
7ea183baef | ||
|
|
ab87fac6d8 | ||
|
|
1e3b7eee3b | ||
|
|
4de028fc3b | ||
|
|
604e5dfaaf | ||
|
|
05e0c2ec9e | ||
|
|
76bd005bdc | ||
|
|
5effaed352 | ||
|
|
cedaf4809f | ||
|
|
6deaf5c268 | ||
|
|
9dc6a26472 | ||
|
|
14ad5916fc | ||
|
|
1a46738649 | ||
|
|
9e5e3b099a | ||
|
|
292ce75cc2 | ||
|
|
ce7df7afd4 | ||
|
|
e28e793f81 | ||
|
|
3e561976db | ||
|
|
273a4eb7d0 | ||
|
|
6175f85bb6 | ||
|
|
a80579f63a | ||
|
|
96d6bcf26e | ||
|
|
49e8df25ac | ||
|
|
6a05850f21 | ||
|
|
5e7c3defe3 | ||
|
|
6c0987d4d0 | ||
|
|
6eba9feffe | ||
|
|
8adfcf5950 | ||
|
|
36d6fa512a | ||
|
|
79b6e9b393 | ||
|
|
dc2e2cbd4b | ||
|
|
5c12dac30f | ||
|
|
641929191e | ||
|
|
617321631a | ||
|
|
ddc0c899f8 | ||
|
|
cdec42c1ae | ||
|
|
c48f469e39 | ||
|
|
44909cc7b8 | ||
|
|
8f61e1568c | ||
|
|
b7be7a0fd8 | ||
|
|
1526a4e084 | ||
|
|
dbdb9574b1 | ||
|
|
853ae6386c | ||
|
|
a4b56c74c7 | ||
|
|
d7f1951e44 | ||
|
|
7e2ff9825e | ||
|
|
9b423396ec | ||
|
|
781146b2fb | ||
|
|
84937d1ce0 | ||
|
|
98cce66aa4 | ||
|
|
043c2d4858 | ||
|
|
99cc434779 | ||
|
|
5095d17e81 | ||
|
|
87d835ae37 | ||
|
|
6939ca768b | ||
|
|
e3957e8239 | ||
|
|
4ad6e45216 | ||
|
|
76e5eeea3f | ||
|
|
eb17f57761 | ||
|
|
b0db14d8b0 | ||
|
|
2b644fa81b | ||
|
|
190ccee820 | ||
|
|
4e7dd32e78 | ||
|
|
5817fb66ae | ||
|
|
9cb04eef93 | ||
|
|
0019fe7f04 | ||
|
|
852c6f2de1 | ||
|
|
c4191de2e7 | ||
|
|
4de61defc9 | ||
|
|
0aa88590d0 | ||
|
|
405f3ee5fe | ||
|
|
bc339f774a | ||
|
|
e67b695b23 | ||
|
|
4a7633ab99 | ||
|
|
c58f2ef61f | ||
|
|
3866e6a3f2 | ||
|
|
381686fc66 | ||
|
|
a918c285bf | ||
|
|
1e20eafbe0 | ||
|
|
39399934ee | ||
|
|
b47635150a | ||
|
|
78d2f69ed5 | ||
|
|
7a98dc669e | ||
|
|
2f15bb5085 | ||
|
|
712a578e6c | ||
|
|
d8dfc4ccb2 | ||
|
|
e413007eb0 | ||
|
|
6d1d3e48d8 | ||
|
|
04966164ce | ||
|
|
8b62aa7cc7 | ||
|
|
1088e8c6a5 | ||
|
|
8c54c2226f | ||
|
|
f74ac1f18b | ||
|
|
25931e62fd | ||
|
|
707a940399 | ||
|
|
87ef50d384 |
2
.github/pull_request_template.md
vendored
Normal file
2
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
To show that your contribution is compatible with the MIT License, please include the following text somewhere in this PR description:
|
||||
This PR complies with the DCO; https://developercertificate.org/
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -21,11 +21,23 @@ copyparty.egg-info/
|
||||
# winmerge
|
||||
*.bak
|
||||
|
||||
# apple pls
|
||||
.DS_Store
|
||||
|
||||
# derived
|
||||
copyparty/res/COPYING.txt
|
||||
copyparty/web/deps/
|
||||
srv/
|
||||
scripts/docker/i/
|
||||
contrib/package/arch/pkg/
|
||||
contrib/package/arch/src/
|
||||
|
||||
# state/logs
|
||||
up.*.txt
|
||||
.hist/
|
||||
.hist/
|
||||
scripts/docker/*.out
|
||||
scripts/docker/*.err
|
||||
/perf.*
|
||||
|
||||
# nix build output link
|
||||
result
|
||||
|
||||
10
.vscode/launch.py
vendored
10
.vscode/launch.py
vendored
@@ -30,9 +30,17 @@ except:
|
||||
|
||||
argv = [os.path.expanduser(x) if x.startswith("~") else x for x in argv]
|
||||
|
||||
sfx = ""
|
||||
if len(sys.argv) > 1 and os.path.isfile(sys.argv[1]):
|
||||
sfx = sys.argv[1]
|
||||
sys.argv = [sys.argv[0]] + sys.argv[2:]
|
||||
|
||||
argv += sys.argv[1:]
|
||||
|
||||
if re.search(" -j ?[0-9]", " ".join(argv)):
|
||||
if sfx:
|
||||
argv = [sys.executable, sfx] + argv
|
||||
sp.check_call(argv)
|
||||
elif re.search(" -j ?[0-9]", " ".join(argv)):
|
||||
argv = [sys.executable, "-m", "copyparty"] + argv
|
||||
sp.check_call(argv)
|
||||
else:
|
||||
|
||||
32
.vscode/settings.json
vendored
32
.vscode/settings.json
vendored
@@ -35,34 +35,18 @@
|
||||
"python.linting.flake8Enabled": true,
|
||||
"python.linting.banditEnabled": true,
|
||||
"python.linting.mypyEnabled": true,
|
||||
"python.linting.mypyArgs": [
|
||||
"--ignore-missing-imports",
|
||||
"--follow-imports=silent",
|
||||
"--show-column-numbers",
|
||||
"--strict"
|
||||
],
|
||||
"python.linting.flake8Args": [
|
||||
"--max-line-length=120",
|
||||
"--ignore=E722,F405,E203,W503,W293,E402,E501,E128",
|
||||
"--ignore=E722,F405,E203,W503,W293,E402,E501,E128,E226",
|
||||
],
|
||||
"python.linting.banditArgs": [
|
||||
"--ignore=B104"
|
||||
],
|
||||
"python.linting.pylintArgs": [
|
||||
"--disable=missing-module-docstring",
|
||||
"--disable=missing-class-docstring",
|
||||
"--disable=missing-function-docstring",
|
||||
"--disable=import-outside-toplevel",
|
||||
"--disable=wrong-import-position",
|
||||
"--disable=raise-missing-from",
|
||||
"--disable=bare-except",
|
||||
"--disable=broad-except",
|
||||
"--disable=invalid-name",
|
||||
"--disable=line-too-long",
|
||||
"--disable=consider-using-f-string"
|
||||
"--ignore=B104,B110,B112"
|
||||
],
|
||||
// python3 -m isort --py=27 --profile=black copyparty/
|
||||
"python.formatting.provider": "black",
|
||||
"python.formatting.provider": "none",
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"[html]": {
|
||||
"editor.formatOnSave": false,
|
||||
@@ -74,10 +58,6 @@
|
||||
"files.associations": {
|
||||
"*.makefile": "makefile"
|
||||
},
|
||||
"python.formatting.blackArgs": [
|
||||
"-t",
|
||||
"py27"
|
||||
],
|
||||
"python.linting.enabled": true,
|
||||
"python.pythonPath": "/usr/bin/python3"
|
||||
}
|
||||
445
README.md
445
README.md
@@ -1,35 +1,21 @@
|
||||
# ⇆🎉 copyparty
|
||||
# 💾🎉 copyparty
|
||||
|
||||
* portable file sharing hub (py2/py3) [(on PyPI)](https://pypi.org/project/copyparty/)
|
||||
* MIT-Licensed, 2019-05-26, ed @ irc.rizon.net
|
||||
turn almost any device into a file server with resumable uploads/downloads using [*any*](#browser-support) web browser
|
||||
|
||||
* server only needs Python (2 or 3), all dependencies optional
|
||||
* 🔌 protocols: [http](#the-browser) // [ftp](#ftp-server) // [webdav](#webdav-server) // [smb/cifs](#smb-server)
|
||||
* 📱 [android app](#android-app) // [iPhone shortcuts](#ios-shortcuts)
|
||||
|
||||
## summary
|
||||
|
||||
turn your phone or raspi into a portable file server with resumable uploads/downloads using *any* web browser
|
||||
|
||||
* server only needs Python (`2.7` or `3.3+`), all dependencies optional
|
||||
* browse/upload with [IE4](#browser-support) / netscape4.0 on win3.11 (heh)
|
||||
* protocols: [http](#the-browser) // [ftp](#ftp-server) // [webdav](#webdav-server) // [smb/cifs](#smb-server)
|
||||
|
||||
try the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running from a basement in finland
|
||||
👉 **[Get started](#quickstart)!** or visit the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running from a basement in finland
|
||||
|
||||
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer)
|
||||
|
||||
|
||||
## get the app
|
||||
|
||||
<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>
|
||||
|
||||
(the app is **NOT** the full copyparty server! just a basic upload client, nothing fancy yet)
|
||||
|
||||
|
||||
## readme toc
|
||||
|
||||
* top
|
||||
* [quickstart](#quickstart) - download **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** and you're all set!
|
||||
* [quickstart](#quickstart) - just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** -- that's it! 🎉
|
||||
* [on servers](#on-servers) - you may also want these, especially on servers
|
||||
* [on debian](#on-debian) - recommended additional steps on debian
|
||||
* [features](#features)
|
||||
* [testimonials](#testimonials) - small collection of user feedback
|
||||
* [motivations](#motivations) - project goals / philosophy
|
||||
@@ -53,11 +39,14 @@ try the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running fro
|
||||
* [self-destruct](#self-destruct) - uploads can be given a lifetime
|
||||
* [file manager](#file-manager) - cut/paste, rename, and delete files/folders (if you have permission)
|
||||
* [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI
|
||||
* [media player](#media-player) - plays almost every audio format there is
|
||||
* [audio equalizer](#audio-equalizer) - bass boosted
|
||||
* [fix unreliable playback on android](#fix-unreliable-playback-on-android) - due to phone / app settings
|
||||
* [markdown viewer](#markdown-viewer) - and there are *two* editors
|
||||
* [other tricks](#other-tricks)
|
||||
* [searching](#searching) - search by size, date, path/name, mp3-tags, ...
|
||||
* [server config](#server-config) - using arguments or config files, or a mix of both
|
||||
* [zeroconf](#zeroconf) - announce enabled services on the LAN
|
||||
* [zeroconf](#zeroconf) - announce enabled services on the LAN ([pic](https://user-images.githubusercontent.com/241032/215344737-0eae8d98-9496-4256-9aa8-cd2f6971810d.png))
|
||||
* [mdns](#mdns) - LAN domain-name and feature announcer
|
||||
* [ssdp](#ssdp) - windows-explorer announcer
|
||||
* [qr-code](#qr-code) - print a qr-code [(screenshot)](https://user-images.githubusercontent.com/241032/194728533-6f00849b-c6ac-43c6-9359-83e454d11e00.png) for quick access
|
||||
@@ -81,24 +70,31 @@ try the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running fro
|
||||
* [themes](#themes)
|
||||
* [complete examples](#complete-examples)
|
||||
* [reverse-proxy](#reverse-proxy) - running copyparty next to other websites
|
||||
* [packages](#packages) - the party might be closer than you think
|
||||
* [arch package](#arch-package) - now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
|
||||
* [nix package](#nix-package) - `nix profile install github:9001/copyparty`
|
||||
* [nixos module](#nixos-module)
|
||||
* [browser support](#browser-support) - TLDR: yes
|
||||
* [client examples](#client-examples) - interact with copyparty using non-browser clients
|
||||
* [folder sync](#folder-sync) - sync folders to/from copyparty
|
||||
* [mount as drive](#mount-as-drive) - a remote copyparty server as a local filesystem
|
||||
* [android app](#android-app) - upload to copyparty with one tap
|
||||
* [iOS shortcuts](#iOS-shortcuts) - there is no iPhone app, but
|
||||
* [performance](#performance) - defaults are usually fine - expect `8 GiB/s` download, `1 GiB/s` upload
|
||||
* [client-side](#client-side) - when uploading files
|
||||
* [security](#security) - some notes on hardening
|
||||
* [gotchas](#gotchas) - behavior that might be unexpected
|
||||
* [cors](#cors) - cross-site request config
|
||||
* [https](#https) - both HTTP and HTTPS are accepted
|
||||
* [recovering from crashes](#recovering-from-crashes)
|
||||
* [client crashes](#client-crashes)
|
||||
* [frefox wsod](#frefox-wsod) - firefox 87 can crash during uploads
|
||||
* [HTTP API](#HTTP-API) - see [devnotes](#./docs/devnotes.md#http-api)
|
||||
* [HTTP API](#HTTP-API) - see [devnotes](./docs/devnotes.md#http-api)
|
||||
* [dependencies](#dependencies) - mandatory deps
|
||||
* [optional dependencies](#optional-dependencies) - install these to enable bonus features
|
||||
* [install recommended deps](#install-recommended-deps)
|
||||
* [optional gpl stuff](#optional-gpl-stuff)
|
||||
* [sfx](#sfx) - the self-contained "binary"
|
||||
* [copyparty.exe](#copypartyexe) - download [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) or [copyparty64.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty64.exe)
|
||||
* [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+)
|
||||
* [install on android](#install-on-android)
|
||||
* [reporting bugs](#reporting-bugs) - ideas for context to include in bug reports
|
||||
* [devnotes](#devnotes) - for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md)
|
||||
@@ -106,27 +102,49 @@ try the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running fro
|
||||
|
||||
## quickstart
|
||||
|
||||
download **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** and you're all set!
|
||||
just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** -- that's it! 🎉
|
||||
|
||||
if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead
|
||||
* or install through pypi: `python3 -m pip install --user -U copyparty`
|
||||
* or if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead
|
||||
* or install [on arch](#arch-package) ╱ [on NixOS](#nixos-module) ╱ [through nix](#nix-package)
|
||||
* or if you are on android, [install copyparty in termux](#install-on-android)
|
||||
* or if you prefer to [use docker](./scripts/docker/) 🐋 you can do that too
|
||||
* docker has all deps built-in, so skip this step:
|
||||
|
||||
running the sfx 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)
|
||||
enable thumbnails (images/audio/video), media indexing, and audio transcoding by installing some recommended deps:
|
||||
|
||||
* **Alpine:** `apk add py3-pillow ffmpeg`
|
||||
* **Debian:** `apt install python3-pil ffmpeg`
|
||||
* **Fedora:** `dnf install python3-pillow ffmpeg`
|
||||
* **FreeBSD:** `pkg install py39-sqlite3 py39-pillow ffmpeg`
|
||||
* **MacOS:** `port install py-Pillow ffmpeg`
|
||||
* **MacOS** (alternative): `brew install pillow ffmpeg`
|
||||
* **Windows:** `python -m pip install --user -U Pillow`
|
||||
* install python and ffmpeg manually; do not use `winget` or `Microsoft Store` (it breaks $PATH)
|
||||
* copyparty.exe comes with `Pillow` and only needs `ffmpeg`
|
||||
* see [optional dependencies](#optional-dependencies) to enable even more features
|
||||
|
||||
running copyparty without arguments (for example doubleclicking it on Windows) will give everyone read/write access to the current folder; you may want [accounts and volumes](#accounts-and-volumes)
|
||||
|
||||
or see [some usage examples](#complete-examples) for inspiration, or the [complete windows example](./docs/examples/windows.md)
|
||||
|
||||
some recommended options:
|
||||
* `-e2dsa` enables general [file indexing](#file-indexing)
|
||||
* `-e2ts` enables audio metadata indexing (needs either FFprobe or Mutagen), see [optional dependencies](#optional-dependencies) to enable thumbnails and more
|
||||
* `-e2ts` enables audio metadata indexing (needs either FFprobe or Mutagen)
|
||||
* `-v /mnt/music:/music:r:rw,foo -a foo:bar` shares `/mnt/music` as `/music`, `r`eadable by anyone, and read-write for user `foo`, password `bar`
|
||||
* replace `:r:rw,foo` with `:r,foo` to only make the folder readable by `foo` and nobody else
|
||||
* see [accounts and volumes](#accounts-and-volumes) for the syntax and other permissions (`r`ead, `w`rite, `m`ove, `d`elete, `g`et, up`G`et)
|
||||
* see [accounts and volumes](#accounts-and-volumes) (or `--help-accounts`) for the syntax and other permissions
|
||||
|
||||
|
||||
### on servers
|
||||
|
||||
you may also want these, especially on servers:
|
||||
|
||||
* [contrib/systemd/copyparty.service](contrib/systemd/copyparty.service) to run copyparty as a systemd service
|
||||
* [contrib/systemd/copyparty.service](contrib/systemd/copyparty.service) to run copyparty as a systemd service (see guide inside)
|
||||
* [contrib/systemd/prisonparty.service](contrib/systemd/prisonparty.service) to run it in a chroot (for extra security)
|
||||
* [contrib/rc/copyparty](contrib/rc/copyparty) to run copyparty on FreeBSD
|
||||
* [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to [reverse-proxy](#reverse-proxy) behind nginx (for better https)
|
||||
* [nixos module](#nixos-module) to run copyparty on NixOS hosts
|
||||
|
||||
and remember to open the ports you want; here's a complete example including every feature copyparty has to offer:
|
||||
```
|
||||
@@ -137,18 +155,6 @@ firewall-cmd --reload
|
||||
```
|
||||
(1900:ssdp, 3921:ftp, 3923:http/https, 3945:smb, 3990:ftps, 5353:mdns, 12000:passive-ftp)
|
||||
|
||||
### on debian
|
||||
|
||||
recommended additional steps on debian which enable audio metadata and thumbnails (from images and videos):
|
||||
|
||||
* as root, run the following:
|
||||
`apt install python3 python3-pip python3-dev ffmpeg`
|
||||
|
||||
* then, as the user which will be running copyparty (so hopefully not root), run this:
|
||||
`python3 -m pip install --user -U Pillow pillow-avif-plugin`
|
||||
|
||||
(skipped `pyheif-pillow-opener` because apparently debian is too old to build it)
|
||||
|
||||
|
||||
## features
|
||||
|
||||
@@ -162,14 +168,18 @@ recommended additional steps on debian which enable audio metadata and thumbnai
|
||||
* ☑ [smb/cifs server](#smb-server)
|
||||
* ☑ [qr-code](#qr-code) for quick access
|
||||
* ☑ [upnp / zeroconf / mdns / ssdp](#zeroconf)
|
||||
* ☑ [event hooks](#event-hooks) / script runner
|
||||
* ☑ [reverse-proxy support](https://github.com/9001/copyparty#reverse-proxy)
|
||||
* upload
|
||||
* ☑ basic: plain multipart, ie6 support
|
||||
* ☑ [up2k](#uploading): js, resumable, multithreaded
|
||||
* unaffected by cloudflare's max-upload-size (100 MiB)
|
||||
* ☑ stash: simple PUT filedropper
|
||||
* ☑ filename randomizer
|
||||
* ☑ write-only folders
|
||||
* ☑ [unpost](#unpost): undo/delete accidental uploads
|
||||
* ☑ [self-destruct](#self-destruct) (specified server-side or client-side)
|
||||
* ☑ symlink/discard existing files (content-matching)
|
||||
* ☑ symlink/discard duplicates (content-matching)
|
||||
* download
|
||||
* ☑ single files in browser
|
||||
* ☑ [folders as zip / tar files](#zip-downloads)
|
||||
@@ -190,16 +200,21 @@ recommended additional steps on debian which enable audio metadata and thumbnai
|
||||
* ☑ [locate files by contents](#file-search)
|
||||
* ☑ search by name/path/date/size
|
||||
* ☑ [search by ID3-tags etc.](#searching)
|
||||
* client support
|
||||
* ☑ [folder sync](#folder-sync)
|
||||
* ☑ [curl-friendly](https://user-images.githubusercontent.com/241032/215322619-ea5fd606-3654-40ad-94ee-2bc058647bb2.png)
|
||||
* markdown
|
||||
* ☑ [viewer](#markdown-viewer)
|
||||
* ☑ editor (sure why not)
|
||||
|
||||
PS: something missing? post any crazy ideas you've got as a [feature request](https://github.com/9001/copyparty/issues/new?assignees=9001&labels=enhancement&template=feature_request.md) or [discussion](https://github.com/9001/copyparty/discussions/new?category=ideas) 🤙
|
||||
|
||||
|
||||
## testimonials
|
||||
|
||||
small collection of user feedback
|
||||
|
||||
`good enough`, `surprisingly correct`, `certified good software`, `just works`, `why`
|
||||
`good enough`, `surprisingly correct`, `certified good software`, `just works`, `why`, `wow this is better than nextcloud`
|
||||
|
||||
|
||||
# motivations
|
||||
@@ -208,7 +223,7 @@ project goals / philosophy
|
||||
|
||||
* inverse linux philosophy -- do all the things, and do an *okay* job
|
||||
* quick drop-in service to get a lot of features in a pinch
|
||||
* check [the alternatives](./docs/versus.md)
|
||||
* some of [the alternatives](./docs/versus.md) might be a better fit for you
|
||||
* run anywhere, support everything
|
||||
* as many web-browsers and python versions as possible
|
||||
* every browser should at least be able to browse, download, upload files
|
||||
@@ -237,6 +252,9 @@ browser-specific:
|
||||
server-os-specific:
|
||||
* RHEL8 / Rocky8: you can run copyparty using `/usr/libexec/platform-python`
|
||||
|
||||
server notes:
|
||||
* pypy is supported but regular cpython is faster if you enable the database
|
||||
|
||||
|
||||
# bugs
|
||||
|
||||
@@ -260,9 +278,11 @@ server-os-specific:
|
||||
|
||||
* [Firefox issue 1790500](https://bugzilla.mozilla.org/show_bug.cgi?id=1790500) -- entire browser can crash after uploading ~4000 small files
|
||||
|
||||
* Android: music playback randomly stops due to [battery usage settings](#fix-unreliable-playback-on-android)
|
||||
|
||||
* iPhones: the volume control doesn't work because [apple doesn't want it to](https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html#//apple_ref/doc/uid/TP40009523-CH5-SW11)
|
||||
* *future workaround:* enable the equalizer, make it all-zero, and set a negative boost to reduce the volume
|
||||
* "future" because `AudioContext` is broken in the current iOS version (15.1), maybe one day...
|
||||
* "future" because `AudioContext` can't maintain a stable playback speed in the current iOS version (15.7), maybe one day...
|
||||
|
||||
* Windows: folders cannot be accessed if the name ends with `.`
|
||||
* python or windows bug
|
||||
@@ -285,7 +305,7 @@ upgrade notes
|
||||
* http-api: delete/move is now `POST` instead of `GET`
|
||||
* everything other than `GET` and `HEAD` must pass [cors validation](#cors)
|
||||
* `1.5.0` (2022-12-03): [new chunksize formula](https://github.com/9001/copyparty/commit/54e1c8d261df) for files larger than 128 GiB
|
||||
* **users:** upgrade to the latest [cli uploader](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) if you use that
|
||||
* **users:** upgrade to the latest [cli uploader](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) if you use that
|
||||
* **devs:** update third-party up2k clients (if those even exist)
|
||||
|
||||
|
||||
@@ -305,6 +325,7 @@ upgrade notes
|
||||
|
||||
per-folder, per-user permissions - if your setup is getting complex, consider making a [config file](./docs/example.conf) instead of using arguments
|
||||
* much easier to manage, and you can modify the config at runtime with `systemctl reload copyparty` or more conveniently using the `[reload cfg]` button in the control-panel (if logged in as admin)
|
||||
* changes to the `[global]` config section requires a restart to take effect
|
||||
|
||||
a quick summary can be seen using `--help-accounts`
|
||||
|
||||
@@ -450,6 +471,7 @@ click the `🌲` or pressing the `B` hotkey to toggle between breadcrumbs path (
|
||||
## thumbnails
|
||||
|
||||
press `g` or `田` to toggle grid-view instead of the file listing and `t` toggles icons / thumbnails
|
||||
* can be made default globally with `--grid` or per-volume with volflag `grid`
|
||||
|
||||

|
||||
|
||||
@@ -460,6 +482,7 @@ it does static images with Pillow / pyvips / FFmpeg, and uses FFmpeg for video f
|
||||
audio files are covnerted into spectrograms using FFmpeg unless you `--no-athumb` (and some FFmpeg builds may need `--th-ff-swr`)
|
||||
|
||||
images with the following names (see `--th-covers`) become the thumbnail of the folder they're in: `folder.png`, `folder.jpg`, `cover.png`, `cover.jpg`
|
||||
* and, if you enable [file indexing](#file-indexing), all remaining folders will also get thumbnails (as long as they contain any pics at all)
|
||||
|
||||
in the grid/thumbnail view, if the audio player panel is open, songs will start playing when clicked
|
||||
* indicated by the audio files having the ▶ icon instead of 💾
|
||||
@@ -491,7 +514,7 @@ you can also zip a selection of files or folders by clicking them in the browser
|
||||
|
||||
## uploading
|
||||
|
||||
drag files/folders into the web-browser to upload (or use the [command-line uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#up2kpy))
|
||||
drag files/folders into the web-browser to upload (or use the [command-line uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy))
|
||||
|
||||
this initiates an upload using `up2k`; there are two uploaders available:
|
||||
* `[🎈] bup`, the basic uploader, supports almost every browser since netscape 4.0
|
||||
@@ -509,11 +532,14 @@ up2k has several advantages:
|
||||
* much higher speeds than ftp/scp/tarpipe on some internet connections (mainly american ones) thanks to parallel connections
|
||||
* the last-modified timestamp of the file is preserved
|
||||
|
||||
> it is perfectly safe to restart / upgrade copyparty while someone is uploading to it!
|
||||
> all known up2k clients will resume just fine 💪
|
||||
|
||||
see [up2k](#up2k) for details on how it works, or watch a [demo video](https://a.ocv.me/pub/demo/pics-vids/#gf-0f6f5c0d)
|
||||
|
||||

|
||||
|
||||
**protip:** you can avoid scaring away users with [contrib/plugins/minimal-up2k.html](contrib/plugins/minimal-up2k.html) which makes it look [much simpler](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png)
|
||||
**protip:** you can avoid scaring away users with [contrib/plugins/minimal-up2k.js](contrib/plugins/minimal-up2k.js) which makes it look [much simpler](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png)
|
||||
|
||||
**protip:** if you enable `favicon` in the `[⚙️] settings` tab (by typing something into the textbox), the icon in the browser tab will indicate upload progress -- also, the `[🔔]` and/or `[🔊]` switches enable visible and/or audible notifications on upload completion
|
||||
|
||||
@@ -638,12 +664,68 @@ or a mix of both:
|
||||
the metadata keys you can use in the format field are the ones in the file-browser table header (whatever is collected with `-mte` and `-mtp`)
|
||||
|
||||
|
||||
## media player
|
||||
|
||||
plays almost every audio format there is (if the server has FFmpeg installed for on-demand transcoding)
|
||||
|
||||
the following audio formats are usually always playable, even without FFmpeg: `aac|flac|m4a|mp3|ogg|opus|wav`
|
||||
|
||||
some hilights:
|
||||
* OS integration; control playback from your phone's lockscreen ([windows](https://user-images.githubusercontent.com/241032/233213022-298a98ba-721a-4cf1-a3d4-f62634bc53d5.png) // [iOS](https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png) // [android](https://user-images.githubusercontent.com/241032/233212311-a7368590-08c7-4f9f-a1af-48ccf3f36fad.png))
|
||||
* shows the audio waveform in the seekbar
|
||||
* not perfectly gapless but can get really close (see settings + eq below); good enough to enjoy gapless albums as intended
|
||||
|
||||
click the `play` link next to an audio file, or copy the link target to [share it](https://a.ocv.me/pub/demo/music/Ubiktune%20-%20SOUNDSHOCK%202%20-%20FM%20FUNK%20TERRROR!!/#af-1fbfba61&t=18) (optionally with a timestamp to start playing from, like that example does)
|
||||
|
||||
open the `[🎺]` media-player-settings tab to configure it,
|
||||
* switches:
|
||||
* `[preload]` starts loading the next track when it's about to end, reduces the silence between songs
|
||||
* `[full]` does a full preload by downloading the entire next file; good for unreliable connections, bad for slow connections
|
||||
* `[~s]` toggles the seekbar waveform display
|
||||
* `[/np]` enables buttons to copy the now-playing info as an irc message
|
||||
* `[os-ctl]` makes it possible to control audio playback from the lockscreen of your device (enables [mediasession](https://developer.mozilla.org/en-US/docs/Web/API/MediaSession))
|
||||
* `[seek]` allows seeking with lockscreen controls (buggy on some devices)
|
||||
* `[art]` shows album art on the lockscreen
|
||||
* `[🎯]` keeps the playing song scrolled into view (good when using the player as a taskbar dock)
|
||||
* `[⟎]` shrinks the playback controls
|
||||
* playback mode:
|
||||
* `[loop]` keeps looping the folder
|
||||
* `[next]` plays into the next folder
|
||||
* transcode:
|
||||
* `[flac]` convers `flac` and `wav` files into opus
|
||||
* `[aac]` converts `aac` and `m4a` files into opus
|
||||
* `[oth]` converts all other known formats into opus
|
||||
* `aac|ac3|aif|aiff|alac|alaw|amr|ape|au|dfpwm|dts|flac|gsm|it|m4a|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|ogg|okt|opus|ra|s3m|tak|tta|ulaw|wav|wma|wv|xm|xpk`
|
||||
* "tint" reduces the contrast of the playback bar
|
||||
|
||||
|
||||
### audio equalizer
|
||||
|
||||
bass boosted
|
||||
|
||||
can also boost the volume in general, or increase/decrease stereo width (like [crossfeed](https://www.foobar2000.org/components/view/foo_dsp_meiercf) just worse)
|
||||
|
||||
has the convenient side-effect of reducing the pause between songs, so gapless albums play better with the eq enabled (just make it flat)
|
||||
|
||||
|
||||
### fix unreliable playback on android
|
||||
|
||||
due to phone / app settings, android phones may randomly stop playing music when the power saver kicks in, especially at the end of an album -- you can fix it by [disabling power saving](https://user-images.githubusercontent.com/241032/235262123-c328cca9-3930-4948-bd18-3949b9fd3fcf.png) in the [app settings](https://user-images.githubusercontent.com/241032/235262121-2ffc51ae-7821-4310-a322-c3b7a507890c.png) of the browser you use for music streaming (preferably a dedicated one)
|
||||
|
||||
|
||||
## markdown viewer
|
||||
|
||||
and there are *two* editors
|
||||
|
||||

|
||||
|
||||
there is a built-in extension for inline clickable thumbnails;
|
||||
* enable it by adding `<!-- th -->` somewhere in the doc
|
||||
* add thumbnails with `!th[l](your.jpg)` where `l` means left-align (`r` = right-align)
|
||||
* a single line with `---` clears the float / inlining
|
||||
* in the case of README.md being displayed below a file listing, thumbnails will open in the gallery viewer
|
||||
|
||||
other notes,
|
||||
* the document preview has a max-width which is the same as an A4 paper when printed
|
||||
|
||||
|
||||
@@ -689,6 +771,7 @@ using arguments or config files, or a mix of both:
|
||||
* config files (`-c some.conf`) can set additional commandline arguments; see [./docs/example.conf](docs/example.conf) and [./docs/example2.conf](docs/example2.conf)
|
||||
* `kill -s USR1` (same as `systemctl reload copyparty`) to reload accounts and volumes from config files without restarting
|
||||
* or click the `[reload cfg]` button in the control-panel when logged in as admin
|
||||
* changes to the `[global]` config section requires a restart to take effect
|
||||
|
||||
|
||||
## zeroconf
|
||||
@@ -747,6 +830,13 @@ an FTP server can be started using `--ftp 3921`, and/or `--ftps` for explicit T
|
||||
* some older software (filezilla on debian-stable) cannot passive-mode with TLS
|
||||
* login with any username + your password, or put your password in the username field
|
||||
|
||||
some recommended FTP / FTPS clients; `wark` = example password:
|
||||
* https://winscp.net/eng/download.php
|
||||
* https://filezilla-project.org/ struggles a bit with ftps in active-mode, but is fine otherwise
|
||||
* https://rclone.org/ does FTPS with `tls=false explicit_tls=true`
|
||||
* `lftp -u k,wark -p 3921 127.0.0.1 -e ls`
|
||||
* `lftp -u k,wark -p 3990 127.0.0.1 -e 'set ssl:verify-certificate no; ls'`
|
||||
|
||||
|
||||
## webdav server
|
||||
|
||||
@@ -760,6 +850,8 @@ general usage:
|
||||
on macos, connect from finder:
|
||||
* [Go] -> [Connect to Server...] -> http://192.168.123.1:3923/
|
||||
|
||||
in order to grant full write-access to webdav clients, the volflag `daw` must be set and the account must also have delete-access (otherwise the client won't be allowed to replace the contents of existing files, which is how webdav works)
|
||||
|
||||
|
||||
### connecting to webdav from windows
|
||||
|
||||
@@ -799,7 +891,7 @@ some **BIG WARNINGS** specific to SMB/CIFS, in decreasing importance:
|
||||
|
||||
and some minor issues,
|
||||
* clients only see the first ~400 files in big folders; [impacket#1433](https://github.com/SecureAuthCorp/impacket/issues/1433)
|
||||
* hot-reload of server config (`/?reload=cfg`) only works for volumes, not account passwords
|
||||
* hot-reload of server config (`/?reload=cfg`) does not include the `[global]` section (commandline args)
|
||||
* listens on the first IPv4 `-i` interface only (default = :: = 0.0.0.0 = all)
|
||||
* login doesn't work on winxp, but anonymous access is ok -- remove all accounts from copyparty config for that to work
|
||||
* win10 onwards does not allow connecting anonymously / without accounts
|
||||
@@ -839,14 +931,13 @@ through arguments:
|
||||
* `--xlink` enables deduplication across volumes
|
||||
|
||||
the same arguments can be set as volflags, in addition to `d2d`, `d2ds`, `d2t`, `d2ts`, `d2v` for disabling:
|
||||
* `-v ~/music::r:c,e2dsa,e2tsr` does a full reindex of everything on startup
|
||||
* `-v ~/music::r:c,e2ds,e2tsr` does a full reindex of everything on startup
|
||||
* `-v ~/music::r:c,d2d` disables **all** indexing, even if any `-e2*` are on
|
||||
* `-v ~/music::r:c,d2t` disables all `-e2t*` (tags), does not affect `-e2d*`
|
||||
* `-v ~/music::r:c,d2ds` disables on-boot scans; only index new uploads
|
||||
* `-v ~/music::r:c,d2ts` same except only affecting tags
|
||||
|
||||
note:
|
||||
* the parser can finally handle `c,e2dsa,e2tsr` so you no longer have to `c,e2dsa:c,e2tsr`
|
||||
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise
|
||||
* the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher
|
||||
* deduplication is possible on windows if you run copyparty as administrator (not saying you should!)
|
||||
@@ -868,7 +959,11 @@ avoid traversing into other filesystems using `--xdev` / volflag `:c,xdev`, ski
|
||||
|
||||
and/or you can `--xvol` / `:c,xvol` to ignore all symlinks leaving the volume's top directory, but still allow bind-mounts pointing elsewhere
|
||||
|
||||
**NB: only affects the indexer** -- users can still access anything inside a volume, unless shadowed by another volume
|
||||
* symlinks are permitted with `xvol` if they point into another volume where the user has the same level of access
|
||||
|
||||
these options will reduce performance; unlikely worst-case estimates are 14% reduction for directory listings, 35% for download-as-tar
|
||||
|
||||
as of copyparty v1.7.0 these options also prevent file access at runtime -- in previous versions it was just hints for the indexer
|
||||
|
||||
### periodic rescan
|
||||
|
||||
@@ -1064,7 +1159,33 @@ see the top of [./copyparty/web/browser.css](./copyparty/web/browser.css) where
|
||||
|
||||
## complete examples
|
||||
|
||||
* read-only music server
|
||||
* see [running on windows](./docs/examples/windows.md) for a fancy windows setup
|
||||
|
||||
* or use any of the examples below, just replace `python copyparty-sfx.py` with `copyparty.exe` if you're using the exe edition
|
||||
|
||||
* allow anyone to download or upload files into the current folder:
|
||||
`python copyparty-sfx.py`
|
||||
|
||||
* enable searching and music indexing with `-e2dsa -e2ts`
|
||||
|
||||
* start an FTP server on port 3921 with `--ftp 3921`
|
||||
|
||||
* announce it on your LAN with `-z` so it appears in windows/Linux file managers
|
||||
|
||||
* anyone can upload, but nobody can see any files (even the uploader):
|
||||
`python copyparty-sfx.py -e2dsa -v .::w`
|
||||
|
||||
* block uploads if there's less than 4 GiB free disk space with `--df 4`
|
||||
|
||||
* show a popup on new uploads with `--xau bin/hooks/notify.py`
|
||||
|
||||
* anyone can upload, and receive "secret" links for each upload they do:
|
||||
`python copyparty-sfx.py -e2dsa -v .::wG:c,fk=8`
|
||||
|
||||
* anyone can browse, only `kevin` (password `okgo`) can upload/move/delete files:
|
||||
`python copyparty-sfx.py -e2dsa -a kevin:okgo -v .::r:rwmd,kevin`
|
||||
|
||||
* read-only music server:
|
||||
`python copyparty-sfx.py -v /mnt/nas/music:/music:r -e2dsa -e2ts --no-robots --force-js --theme 2`
|
||||
|
||||
* ...with bpm and key scanning
|
||||
@@ -1079,19 +1200,135 @@ see the top of [./copyparty/web/browser.css](./copyparty/web/browser.css) where
|
||||
|
||||
## reverse-proxy
|
||||
|
||||
running copyparty next to other websites hosted on an existing webserver such as nginx or apache
|
||||
running copyparty next to other websites hosted on an existing webserver such as nginx, caddy, or apache
|
||||
|
||||
you can either:
|
||||
* give copyparty its own domain or subdomain (recommended)
|
||||
* or do location-based proxying, using `--rp-loc=/stuff` to tell copyparty where it is mounted -- has a slight performance cost and higher chance of bugs
|
||||
* if copyparty says `incorrect --rp-loc or webserver config; expected vpath starting with [...]` it's likely because the webserver is stripping away the proxy location from the request URLs -- see the `ProxyPass` in the apache example below
|
||||
|
||||
some reverse proxies (such as [Caddy](https://caddyserver.com/)) can automatically obtain a valid https/tls certificate for you, and some support HTTP/2 and QUIC which could be a nice speed boost
|
||||
|
||||
example webserver configs:
|
||||
|
||||
* [nginx config](contrib/nginx/copyparty.conf) -- entire domain/subdomain
|
||||
* [apache2 config](contrib/apache/copyparty.conf) -- location-based
|
||||
|
||||
|
||||
# packages
|
||||
|
||||
the party might be closer than you think
|
||||
|
||||
|
||||
## arch package
|
||||
|
||||
now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
|
||||
|
||||
|
||||
## nix package
|
||||
|
||||
`nix profile install github:9001/copyparty`
|
||||
|
||||
requires a [flake-enabled](https://nixos.wiki/wiki/Flakes) installation of nix
|
||||
|
||||
some recommended dependencies are enabled by default; [override the package](https://github.com/9001/copyparty/blob/hovudstraum/contrib/package/nix/copyparty/default.nix#L3-L22) if you want to add/remove some features/deps
|
||||
|
||||
`ffmpeg-full` was chosen over `ffmpeg-headless` mainly because we need `withWebp` (and `withOpenmpt` is also nice) and being able to use a cached build felt more important than optimizing for size at the time -- PRs welcome if you disagree 👍
|
||||
|
||||
|
||||
## nixos module
|
||||
|
||||
for this setup, you will need a [flake-enabled](https://nixos.wiki/wiki/Flakes) installation of NixOS.
|
||||
|
||||
```nix
|
||||
{
|
||||
# add copyparty flake to your inputs
|
||||
inputs.copyparty.url = "github:9001/copyparty";
|
||||
|
||||
# ensure that copyparty is an allowed argument to the outputs function
|
||||
outputs = { self, nixpkgs, copyparty }: {
|
||||
nixosConfigurations.yourHostName = nixpkgs.lib.nixosSystem {
|
||||
modules = [
|
||||
# load the copyparty NixOS module
|
||||
copyparty.nixosModules.default
|
||||
({ pkgs, ... }: {
|
||||
# add the copyparty overlay to expose the package to the module
|
||||
nixpkgs.overlays = [ copyparty.overlays.default ];
|
||||
# (optional) install the package globally
|
||||
environment.systemPackages = [ pkgs.copyparty ];
|
||||
# configure the copyparty module
|
||||
services.copyparty.enable = true;
|
||||
})
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
copyparty on NixOS is configured via `services.copyparty` options, for example:
|
||||
```nix
|
||||
services.copyparty = {
|
||||
enable = true;
|
||||
# directly maps to values in the [global] section of the copyparty config.
|
||||
# see `copyparty --help` for available options
|
||||
settings = {
|
||||
i = "0.0.0.0";
|
||||
# use lists to set multiple values
|
||||
p = [ 3210 3211 ];
|
||||
# use booleans to set binary flags
|
||||
no-reload = true;
|
||||
# using 'false' will do nothing and omit the value when generating a config
|
||||
ignored-flag = false;
|
||||
};
|
||||
|
||||
# create users
|
||||
accounts = {
|
||||
# specify the account name as the key
|
||||
ed = {
|
||||
# provide the path to a file containing the password, keeping it out of /nix/store
|
||||
# must be readable by the copyparty service user
|
||||
passwordFile = "/run/keys/copyparty/ed_password";
|
||||
};
|
||||
# or do both in one go
|
||||
k.passwordFile = "/run/keys/copyparty/k_password";
|
||||
};
|
||||
|
||||
# create a volume
|
||||
volumes = {
|
||||
# create a volume at "/" (the webroot), which will
|
||||
"/" = {
|
||||
# share the contents of "/srv/copyparty"
|
||||
path = "/srv/copyparty";
|
||||
# see `copyparty --help-accounts` for available options
|
||||
access = {
|
||||
# everyone gets read-access, but
|
||||
r = "*";
|
||||
# users "ed" and "k" get read-write
|
||||
rw = [ "ed" "k" ];
|
||||
};
|
||||
# see `copyparty --help-flags` for available options
|
||||
flags = {
|
||||
# "fk" enables filekeys (necessary for upget permission) (4 chars long)
|
||||
fk = 4;
|
||||
# scan for new files every 60sec
|
||||
scan = 60;
|
||||
# volflag "e2d" enables the uploads database
|
||||
e2d = true;
|
||||
# "d2t" disables multimedia parsers (in case the uploads are malicious)
|
||||
d2t = true;
|
||||
# skips hashing file contents if path matches *.iso
|
||||
nohash = "\.iso$";
|
||||
};
|
||||
};
|
||||
};
|
||||
# you may increase the open file limit for the process
|
||||
openFilesLimit = 8192;
|
||||
};
|
||||
```
|
||||
|
||||
the passwordFile at /run/keys/copyparty/ could for example be generated by [agenix](https://github.com/ryantm/agenix), or you could just dump it in the nix store instead if that's acceptable
|
||||
|
||||
|
||||
# browser support
|
||||
|
||||
TLDR: yes
|
||||
@@ -1162,10 +1399,10 @@ interact with copyparty using non-browser clients
|
||||
* `(printf 'PUT /junk?pw=wark HTTP/1.1\r\n\r\n'; cat movie.mkv) | nc 127.0.0.1 3923`
|
||||
* `(printf 'PUT / HTTP/1.1\r\n\r\n'; cat movie.mkv) >/dev/tcp/127.0.0.1/3923`
|
||||
|
||||
* python: [up2k.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) is a command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
|
||||
* file uploads, file-search, folder sync, autoresume of aborted/broken uploads
|
||||
* can be downloaded from copyparty: controlpanel -> connect -> [up2k.py](http://127.0.0.1:3923/.cpr/a/up2k.py)
|
||||
* see [./bin/README.md#up2kpy](bin/README.md#up2kpy)
|
||||
* python: [u2c.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) is a command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
|
||||
* file uploads, file-search, [folder sync](#folder-sync), autoresume of aborted/broken uploads
|
||||
* can be downloaded from copyparty: controlpanel -> connect -> [u2c.py](http://127.0.0.1:3923/.cpr/a/u2c.py)
|
||||
* see [./bin/README.md#u2cpy](bin/README.md#u2cpy)
|
||||
|
||||
* FUSE: mount a copyparty server as a local filesystem
|
||||
* cross-platform python client available in [./bin/](bin/)
|
||||
@@ -1184,17 +1421,27 @@ you can provide passwords using header `PW: hunter2`, cookie `cppwd=hunter2`, ur
|
||||
NOTE: curl will not send the original filename if you use `-T` combined with url-params! Also, make sure to always leave a trailing slash in URLs unless you want to override the filename
|
||||
|
||||
|
||||
## folder sync
|
||||
|
||||
sync folders to/from copyparty
|
||||
|
||||
the commandline uploader [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy) with `--dr` is the best way to sync a folder to copyparty; verifies checksums and does files in parallel, and deletes unexpected files on the server after upload has finished which makes file-renames really cheap (it'll rename serverside and skip uploading)
|
||||
|
||||
alternatively there is [rclone](./docs/rclone.md) which allows for bidirectional sync and is *way* more flexible (stream files straight from sftp/s3/gcs to copyparty, ...), although there is no integrity check and it won't work with files over 100 MiB if copyparty is behind cloudflare
|
||||
|
||||
* starting from rclone v1.63 (currently [in beta](https://beta.rclone.org/?filter=latest)), rclone will also be faster than u2c.py
|
||||
|
||||
|
||||
## mount as drive
|
||||
|
||||
a remote copyparty server as a local filesystem; go to the control-panel and click `connect` to see a list of commands to do that
|
||||
|
||||
alternatively, some alternatives roughly sorted by speed (unreproducible benchmark), best first:
|
||||
|
||||
* [rclone-http](./docs/rclone.md) (25s), read-only
|
||||
* [rclone-webdav](./docs/rclone.md) (25s), read/WRITE ([v1.63-beta](https://beta.rclone.org/?filter=latest))
|
||||
* [rclone-http](./docs/rclone.md) (26s), read-only
|
||||
* [partyfuse.py](./bin/#partyfusepy) (35s), read-only
|
||||
* [rclone-ftp](./docs/rclone.md) (47s), read/WRITE
|
||||
* [rclone-webdav](./docs/rclone.md) (51s), read/WRITE
|
||||
* copyparty-1.5.0's webdav server is faster than rclone-1.60.0 (69s)
|
||||
* [partyfuse.py](./bin/#partyfusepy) (71s), read-only
|
||||
* davfs2 (103s), read/WRITE, *very fast* on small files
|
||||
* [win10-webdav](#webdav-server) (138s), read/WRITE
|
||||
* [win10-smb2](#smb-server) (387s), read/WRITE
|
||||
@@ -1202,6 +1449,27 @@ alternatively, some alternatives roughly sorted by speed (unreproducible benchma
|
||||
most clients will fail to mount the root of a copyparty server unless there is a root volume (so you get the admin-panel instead of a browser when accessing it) -- in that case, mount a specific volume instead
|
||||
|
||||
|
||||
# android app
|
||||
|
||||
upload to copyparty with one tap
|
||||
|
||||
<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>
|
||||
|
||||
the app is **NOT** the full copyparty server! just a basic upload client, nothing fancy yet
|
||||
|
||||
if you want to run the copyparty server on your android device, see [install on android](#install-on-android)
|
||||
|
||||
|
||||
# iOS shortcuts
|
||||
|
||||
there is no iPhone app, but the following shortcuts are almost as good:
|
||||
|
||||
* [upload to copyparty](https://www.icloud.com/shortcuts/41e98dd985cb4d3bb433222bc1e9e770) ([offline](https://github.com/9001/copyparty/raw/hovudstraum/contrib/ios/upload-to-copyparty.shortcut)) ([png](https://user-images.githubusercontent.com/241032/226118053-78623554-b0ed-482e-98e4-6d57ada58ea4.png)) based on the [original](https://www.icloud.com/shortcuts/ab415d5b4de3467b9ce6f151b439a5d7) by [Daedren](https://github.com/Daedren) (thx!)
|
||||
* can strip exif, upload files, pics, vids, links, clipboard
|
||||
* can download links and rehost the target file on copyparty (see first comment inside the shortcut)
|
||||
* pics become lowres if you share from gallery to shortcut, so better to launch the shortcut and pick stuff from there
|
||||
|
||||
|
||||
# performance
|
||||
|
||||
defaults are usually fine - expect `8 GiB/s` download, `1 GiB/s` upload
|
||||
@@ -1209,15 +1477,16 @@ defaults are usually fine - expect `8 GiB/s` download, `1 GiB/s` upload
|
||||
below are some tweaks roughly ordered by usefulness:
|
||||
|
||||
* `-q` disables logging and can help a bunch, even when combined with `-lo` to redirect logs to file
|
||||
* `--http-only` or `--https-only` (unless you want to support both protocols) will reduce the delay before a new connection is established
|
||||
* `--hist` pointing to a fast location (ssd) will make directory listings and searches faster when `-e2d` or `-e2t` is set
|
||||
* `--no-hash .` when indexing a network-disk if you don't care about the actual filehashes and only want the names/tags searchable
|
||||
* `--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)
|
||||
* `-j` enables multiprocessing (actual multithreading) and can make copyparty perform better in cpu-intensive workloads, for example:
|
||||
* huge amount of short-lived connections
|
||||
* `-j0` enables multiprocessing (actual multithreading), can reduce latency to `20+80/numCores` percent and generally improve performance in cpu-intensive workloads, for example:
|
||||
* lots of connections (many users or heavy clients)
|
||||
* simultaneous downloads and uploads saturating a 20gbps connection
|
||||
|
||||
...however it adds an overhead to internal communication so it might be a net loss, see if it works 4 u
|
||||
* using [pypy](https://www.pypy.org/) instead of [cpython](https://www.python.org/) *can* be 70% faster for some workloads, but slower for many others
|
||||
* and pypy can sometimes crash on startup with `-j0` (TODO make issue)
|
||||
|
||||
|
||||
## client-side
|
||||
@@ -1227,7 +1496,7 @@ when uploading files,
|
||||
* chrome is recommended, at least compared to firefox:
|
||||
* up to 90% faster when hashing, especially on SSDs
|
||||
* up to 40% faster when uploading over extremely fast internets
|
||||
* but [up2k.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) can be 40% faster than chrome again
|
||||
* but [u2c.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) can be 40% faster than chrome again
|
||||
|
||||
* if you're cpu-bottlenecked, or the browser is maxing a cpu core:
|
||||
* up to 30% faster uploads if you hide the upload status list by switching away from the `[🚀]` up2k ui-tab (or closing it)
|
||||
@@ -1270,6 +1539,7 @@ other misc notes:
|
||||
|
||||
* you can disable directory listings by giving permission `g` instead of `r`, only accepting direct URLs to files
|
||||
* combine this with volflag `c,fk` to generate filekeys (per-file accesskeys); users which have full read-access will then see URLs with `?k=...` appended to the end, and `g` users must provide that URL including the correct key to avoid a 404
|
||||
* the default filekey entropy is fairly small so give `--fk-salt` around 30 characters if you want filekeys longer than 16 chars
|
||||
* permissions `wG` lets users upload files and receive their own filekeys, still without being able to see other uploads
|
||||
|
||||
|
||||
@@ -1296,6 +1566,18 @@ by default, except for `GET` and `HEAD` operations, all requests must either:
|
||||
cors can be configured with `--acao` and `--acam`, or the protections entirely disabled with `--allow-csrf`
|
||||
|
||||
|
||||
## https
|
||||
|
||||
both 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)
|
||||
|
||||
copyparty doesn't speak HTTP/2 or QUIC, so using a reverse proxy would solve that as well
|
||||
|
||||
if [cfssl](https://github.com/cloudflare/cfssl/releases/latest) is installed, copyparty will automatically create a CA and server-cert on startup
|
||||
* the certs are written to `--crt-dir` for distribution, see `--help` for the other `--crt` options
|
||||
* this will be a self-signed certificate so you must install your `ca.pem` into all your browsers/devices
|
||||
* if you want to avoid the hassle of distributing certs manually, please consider using a reverse proxy
|
||||
|
||||
|
||||
# recovering from crashes
|
||||
|
||||
## client crashes
|
||||
@@ -1318,7 +1600,7 @@ however you can hit `F12` in the up2k tab and use the devtools to see how far yo
|
||||
|
||||
# HTTP API
|
||||
|
||||
see [devnotes](#./docs/devnotes.md#http-api)
|
||||
see [devnotes](./docs/devnotes.md#http-api)
|
||||
|
||||
|
||||
# dependencies
|
||||
@@ -1346,18 +1628,12 @@ enable [thumbnails](#thumbnails) of...
|
||||
* **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin`
|
||||
* **JPEG XL pictures:** `pyvips` or `ffmpeg`
|
||||
|
||||
enable [smb](#smb-server) support:
|
||||
enable [smb](#smb-server) support (**not** recommended):
|
||||
* `impacket==0.10.0`
|
||||
|
||||
`pyvips` gives higher quality thumbnails than `Pillow` and is 320% faster, using 270% more ram: `sudo apt install libvips42 && python3 -m pip install --user -U pyvips`
|
||||
|
||||
|
||||
## install recommended deps
|
||||
```
|
||||
python -m pip install --user -U jinja2 mutagen Pillow
|
||||
```
|
||||
|
||||
|
||||
## optional gpl stuff
|
||||
|
||||
some bundled tools have copyleft dependencies, see [./bin/#mtag](bin/#mtag)
|
||||
@@ -1369,20 +1645,25 @@ these are standalone programs and will never be imported / evaluated by copypart
|
||||
|
||||
the self-contained "binary" [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
|
||||
|
||||
you can reduce the sfx size by repacking it; see [./docs/devnotes.md#sfx-repack](#./docs/devnotes.md#sfx-repack)
|
||||
you can reduce the sfx size by repacking it; see [./docs/devnotes.md#sfx-repack](./docs/devnotes.md#sfx-repack)
|
||||
|
||||
|
||||
## copyparty.exe
|
||||
|
||||
download [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) or [copyparty64.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty64.exe)
|
||||
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+)
|
||||
|
||||

|
||||

|
||||
|
||||
can be convenient on old machines where installing python is problematic, however is **not recommended** and should be considered a last resort -- if possible, please use **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** instead
|
||||
can 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
|
||||
|
||||
* [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) is compatible with 32bit windows7, which means it uses an ancient copy of python (3.7.9) which cannot be upgraded and will definitely become a security hazard at some point
|
||||
* [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
|
||||
|
||||
* [copyparty64.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty64.exe) is identical except 64bit so it [works in WinPE](https://user-images.githubusercontent.com/241032/205454984-e6b550df-3c49-486d-9267-1614078dd0dd.png)
|
||||
* on win8 it needs [vc redist 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48145), on win10 it just works
|
||||
* some antivirus may freak out (false-positive), possibly [Avast, AVG, and McAfee](https://www.virustotal.com/gui/file/52391a1e9842cf70ad243ef83844d46d29c0044d101ee0138fcdd3c8de2237d6/detection)
|
||||
|
||||
* 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)
|
||||
|
||||
* dangerous and deprecated: [copyparty-winpe64.exe](https://github.com/9001/copyparty/releases/download/v1.6.8/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
|
||||
|
||||
meanwhile [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
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# [`up2k.py`](up2k.py)
|
||||
# [`u2c.py`](u2c.py)
|
||||
* command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
|
||||
* file uploads, file-search, autoresume of aborted/broken uploads
|
||||
* sync local folder to server
|
||||
|
||||
@@ -2,15 +2,25 @@ standalone programs which are executed by copyparty when an event happens (uploa
|
||||
|
||||
these programs either take zero arguments, or a filepath (the affected file), or a json message with filepath + additional info
|
||||
|
||||
run copyparty with `--help-hooks` for usage details / hook type explanations (xbu/xau/xiu/xbr/xar/xbd/xad)
|
||||
|
||||
> **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
|
||||
|
||||
|
||||
# after upload
|
||||
* [notify.py](notify.py) shows a desktop notification ([example](https://user-images.githubusercontent.com/241032/215335767-9c91ed24-d36e-4b6b-9766-fb95d12d163f.png))
|
||||
* [notify2.py](notify2.py) uses the json API to show more context
|
||||
* [image-noexif.py](image-noexif.py) removes image exif by overwriting / directly editing the uploaded file
|
||||
* [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))
|
||||
* [reject-mimetype.py](reject-mimetype.py) rejects uploads unless the mimetype is acceptable
|
||||
|
||||
|
||||
# upload batches
|
||||
these 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
|
||||
* [xiu.py](xiu.py) is a "minimal" example showing a list of filenames + total filesize
|
||||
* [xiu-sha.py](xiu-sha.py) produces a sha512 checksum list in the volume root
|
||||
|
||||
|
||||
# before upload
|
||||
* [reject-extension.py](reject-extension.py) rejects uploads if they match a list of file extensions
|
||||
|
||||
|
||||
@@ -13,9 +13,15 @@ example usage as global config:
|
||||
--xau f,t5,j,bin/hooks/discord-announce.py
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:c,xau=f,t5,j,bin/hooks/discord-announce.py
|
||||
-v srv/inc:inc:r:rw,ed:c,xau=f,t5,j,bin/hooks/discord-announce.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all uploads with the params listed below)
|
||||
|
||||
parameters explained,
|
||||
xbu = execute after upload
|
||||
f = fork; don't wait for it to finish
|
||||
t5 = timeout if it's still running after 5 sec
|
||||
j = provide upload information as json; not just the filename
|
||||
@@ -30,6 +36,7 @@ then use this to design your message: https://discohook.org/
|
||||
|
||||
def main():
|
||||
WEBHOOK = "https://discord.com/api/webhooks/1234/base64"
|
||||
WEBHOOK = "https://discord.com/api/webhooks/1066830390280597718/M1TDD110hQA-meRLMRhdurych8iyG35LDoI1YhzbrjGP--BXNZodZFczNVwK4Ce7Yme5"
|
||||
|
||||
# read info from copyparty
|
||||
inf = json.loads(sys.argv[1])
|
||||
|
||||
72
bin/hooks/image-noexif.py
Executable file
72
bin/hooks/image-noexif.py
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess as sp
|
||||
|
||||
|
||||
_ = r"""
|
||||
remove exif tags from uploaded images; the eventhook edition of
|
||||
https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/image-noexif.py
|
||||
|
||||
dependencies:
|
||||
exiftool / perl-Image-ExifTool
|
||||
|
||||
being an upload hook, this will take effect after upload completion
|
||||
but before copyparty has hashed/indexed the file, which means that
|
||||
copyparty will never index the original file, so deduplication will
|
||||
not work as expected... which is mostly OK but ehhh
|
||||
|
||||
note: modifies the file in-place, so don't set the `f` (fork) flag
|
||||
|
||||
example usages; either as global config (all volumes) or as volflag:
|
||||
--xau bin/hooks/image-noexif.py
|
||||
-v srv/inc:inc:r:rw,ed:c,xau=bin/hooks/image-noexif.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
explained:
|
||||
share fs-path srv/inc at /inc (readable by all, read-write for user ed)
|
||||
running this xau (execute-after-upload) plugin for all uploaded files
|
||||
"""
|
||||
|
||||
|
||||
# filetypes to process; ignores everything else
|
||||
EXTS = ("jpg", "jpeg", "avif", "heif", "heic")
|
||||
|
||||
|
||||
try:
|
||||
from copyparty.util import fsenc
|
||||
except:
|
||||
|
||||
def fsenc(p):
|
||||
return p.encode("utf-8")
|
||||
|
||||
|
||||
def main():
|
||||
fp = sys.argv[1]
|
||||
ext = fp.lower().split(".")[-1]
|
||||
if ext not in EXTS:
|
||||
return
|
||||
|
||||
cwd, fn = os.path.split(fp)
|
||||
os.chdir(cwd)
|
||||
f1 = fsenc(fn)
|
||||
cmd = [
|
||||
b"exiftool",
|
||||
b"-exif:all=",
|
||||
b"-iptc:all=",
|
||||
b"-xmp:all=",
|
||||
b"-P",
|
||||
b"-overwrite_original",
|
||||
b"--",
|
||||
f1,
|
||||
]
|
||||
sp.check_output(cmd)
|
||||
print("image-noexif: stripped")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except:
|
||||
pass
|
||||
@@ -17,8 +17,12 @@ depdencies:
|
||||
|
||||
example usages; either as global config (all volumes) or as volflag:
|
||||
--xau f,bin/hooks/notify.py
|
||||
-v srv/inc:inc:c,xau=f,bin/hooks/notify.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
-v srv/inc:inc:r:rw,ed:c,xau=f,bin/hooks/notify.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all uploads with the params listed below)
|
||||
|
||||
parameters explained,
|
||||
xau = execute after upload
|
||||
@@ -26,9 +30,23 @@ parameters explained,
|
||||
"""
|
||||
|
||||
|
||||
try:
|
||||
from copyparty.util import humansize
|
||||
except:
|
||||
|
||||
def humansize(n):
|
||||
return n
|
||||
|
||||
|
||||
def main():
|
||||
dp, fn = os.path.split(sys.argv[1])
|
||||
msg = "🏷️ {}\n📁 {}".format(fn, dp)
|
||||
fp = sys.argv[1]
|
||||
dp, fn = os.path.split(fp)
|
||||
try:
|
||||
sz = humansize(os.path.getsize(fp))
|
||||
except:
|
||||
sz = "?"
|
||||
|
||||
msg = "{} ({})\n📁 {}".format(fn, sz, dp)
|
||||
title = "File received"
|
||||
|
||||
if "com.termux" in sys.executable:
|
||||
|
||||
72
bin/hooks/notify2.py
Executable file
72
bin/hooks/notify2.py
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import subprocess as sp
|
||||
from datetime import datetime
|
||||
from plyer import notification
|
||||
|
||||
|
||||
_ = r"""
|
||||
same as notify.py but with additional info (uploader, ...)
|
||||
and also supports --xm (notify on 📟 message)
|
||||
|
||||
example usages; either as global config (all volumes) or as volflag:
|
||||
--xm f,j,bin/hooks/notify2.py
|
||||
--xau f,j,bin/hooks/notify2.py
|
||||
-v srv/inc:inc:r:rw,ed:c,xm=f,j,bin/hooks/notify2.py
|
||||
-v srv/inc:inc:r:rw,ed:c,xau=f,j,bin/hooks/notify2.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all uploads / msgs with the params listed below)
|
||||
|
||||
parameters explained,
|
||||
xau = execute after upload
|
||||
f = fork so it doesn't block uploads
|
||||
j = provide json instead of filepath list
|
||||
"""
|
||||
|
||||
|
||||
try:
|
||||
from copyparty.util import humansize
|
||||
except:
|
||||
|
||||
def humansize(n):
|
||||
return n
|
||||
|
||||
|
||||
def main():
|
||||
inf = json.loads(sys.argv[1])
|
||||
fp = inf["ap"]
|
||||
sz = humansize(inf["sz"])
|
||||
dp, fn = os.path.split(fp)
|
||||
mt = datetime.utcfromtimestamp(inf["mt"]).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
msg = f"{fn} ({sz})\n📁 {dp}"
|
||||
title = "File received"
|
||||
icon = "emblem-documents-symbolic" if sys.platform == "linux" else ""
|
||||
|
||||
if inf.get("txt"):
|
||||
msg = inf["txt"]
|
||||
title = "Message received"
|
||||
icon = "mail-unread-symbolic" if sys.platform == "linux" else ""
|
||||
|
||||
msg += f"\n👤 {inf['user']} ({inf['ip']})\n🕒 {mt}"
|
||||
|
||||
if "com.termux" in sys.executable:
|
||||
sp.run(["termux-notification", "-t", title, "-c", msg])
|
||||
return
|
||||
|
||||
notification.notify(
|
||||
title=title,
|
||||
message=msg,
|
||||
app_icon=icon,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -10,7 +10,12 @@ example usage as global config:
|
||||
--xbu c,bin/hooks/reject-extension.py
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:c,xbu=c,bin/hooks/reject-extension.py
|
||||
-v srv/inc:inc:r:rw,ed:c,xbu=c,bin/hooks/reject-extension.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all uploads with the params listed below)
|
||||
|
||||
parameters explained,
|
||||
xbu = execute before upload
|
||||
|
||||
@@ -17,7 +17,12 @@ example usage as global config:
|
||||
--xau c,bin/hooks/reject-mimetype.py
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:c,xau=c,bin/hooks/reject-mimetype.py
|
||||
-v srv/inc:inc:r:rw,ed:c,xau=c,bin/hooks/reject-mimetype.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all uploads with the params listed below)
|
||||
|
||||
parameters explained,
|
||||
xau = execute after upload
|
||||
|
||||
@@ -15,9 +15,15 @@ example usage as global config:
|
||||
--xm f,j,t3600,bin/hooks/wget.py
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:c,xm=f,j,t3600,bin/hooks/wget.py
|
||||
-v srv/inc:inc:r:rw,ed:c,xm=f,j,t3600,bin/hooks/wget.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all messages with the params listed below)
|
||||
|
||||
parameters explained,
|
||||
xm = execute on message-to-server-log
|
||||
f = fork so it doesn't block uploads
|
||||
j = provide message information as json; not just the text
|
||||
c3 = mute all output
|
||||
|
||||
108
bin/hooks/xiu-sha.py
Executable file
108
bin/hooks/xiu-sha.py
Executable file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
_ = r"""
|
||||
this hook will produce a single sha512 file which
|
||||
covers all recent uploads (plus metadata comments)
|
||||
|
||||
use this with --xiu, which makes copyparty buffer
|
||||
uploads until server is idle, providing file infos
|
||||
on stdin (filepaths or json)
|
||||
|
||||
example usage as global config:
|
||||
--xiu i5,j,bin/hooks/xiu-sha.py
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:r:rw,ed:c,xiu=i5,j,bin/hooks/xiu-sha.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on batches of uploads with the params listed below)
|
||||
|
||||
parameters explained,
|
||||
xiu = execute after uploads...
|
||||
i5 = ...after volume has been idle for 5sec
|
||||
j = provide json instead of filepath list
|
||||
|
||||
note the "f" (fork) flag is not set, so this xiu
|
||||
will block other xiu hooks while it's running
|
||||
"""
|
||||
|
||||
|
||||
try:
|
||||
from copyparty.util import fsenc
|
||||
except:
|
||||
|
||||
def fsenc(p):
|
||||
return p
|
||||
|
||||
|
||||
def humantime(ts):
|
||||
return datetime.utcfromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def find_files_root(inf):
|
||||
di = 9000
|
||||
for f1, f2 in zip(inf, inf[1:]):
|
||||
p1 = f1["ap"].replace("\\", "/").rsplit("/", 1)[0]
|
||||
p2 = f2["ap"].replace("\\", "/").rsplit("/", 1)[0]
|
||||
di = min(len(p1), len(p2), di)
|
||||
di = next((i for i in range(di) if p1[i] != p2[i]), di)
|
||||
|
||||
return di + 1
|
||||
|
||||
|
||||
def find_vol_root(inf):
|
||||
return len(inf[0]["ap"][: -len(inf[0]["vp"])])
|
||||
|
||||
|
||||
def main():
|
||||
zb = sys.stdin.buffer.read()
|
||||
zs = zb.decode("utf-8", "replace")
|
||||
inf = json.loads(zs)
|
||||
|
||||
# root directory (where to put the sha512 file);
|
||||
# di = find_files_root(inf) # next to the file closest to volume root
|
||||
di = find_vol_root(inf) # top of the entire volume
|
||||
|
||||
ret = []
|
||||
total_sz = 0
|
||||
for md in inf:
|
||||
ap = md["ap"]
|
||||
rp = ap[di:]
|
||||
total_sz += md["sz"]
|
||||
fsize = "{:,}".format(md["sz"])
|
||||
mtime = humantime(md["mt"])
|
||||
up_ts = humantime(md["at"])
|
||||
|
||||
h = hashlib.sha512()
|
||||
with open(fsenc(md["ap"]), "rb", 512 * 1024) as f:
|
||||
while True:
|
||||
buf = f.read(512 * 1024)
|
||||
if not buf:
|
||||
break
|
||||
|
||||
h.update(buf)
|
||||
|
||||
cksum = h.hexdigest()
|
||||
meta = " | ".join([md["wark"], up_ts, mtime, fsize, md["ip"]])
|
||||
ret.append("# {}\n{} *{}".format(meta, cksum, rp))
|
||||
|
||||
ret.append("# {} files, {} bytes total".format(len(inf), total_sz))
|
||||
ret.append("")
|
||||
ftime = datetime.utcnow().strftime("%Y-%m%d-%H%M%S.%f")
|
||||
fp = "{}xfer-{}.sha512".format(inf[0]["ap"][:di], ftime)
|
||||
with open(fsenc(fp), "wb") as f:
|
||||
f.write("\n".join(ret).encode("utf-8", "replace"))
|
||||
|
||||
print("wrote checksums to {}".format(fp))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
50
bin/hooks/xiu.py
Executable file
50
bin/hooks/xiu.py
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
_ = r"""
|
||||
this hook prints absolute filepaths + total size
|
||||
|
||||
use this with --xiu, which makes copyparty buffer
|
||||
uploads until server is idle, providing file infos
|
||||
on stdin (filepaths or json)
|
||||
|
||||
example usage as global config:
|
||||
--xiu i1,j,bin/hooks/xiu.py
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:r:rw,ed:c,xiu=i1,j,bin/hooks/xiu.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on batches of uploads with the params listed below)
|
||||
|
||||
parameters explained,
|
||||
xiu = execute after uploads...
|
||||
i1 = ...after volume has been idle for 1sec
|
||||
j = provide json instead of filepath list
|
||||
|
||||
note the "f" (fork) flag is not set, so this xiu
|
||||
will block other xiu hooks while it's running
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
zb = sys.stdin.buffer.read()
|
||||
zs = zb.decode("utf-8", "replace")
|
||||
inf = json.loads(zs)
|
||||
|
||||
total_sz = 0
|
||||
for upload in inf:
|
||||
sz = upload["sz"]
|
||||
total_sz += sz
|
||||
print("{:9} {}".format(sz, upload["ap"]))
|
||||
|
||||
print("{} files, {} bytes total".format(len(inf), total_sz))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -31,7 +31,7 @@ run [`install-deps.sh`](install-deps.sh) to build/install most dependencies requ
|
||||
*alternatively* (or preferably) use packages from your distro instead, then you'll need at least these:
|
||||
|
||||
* from distro: `numpy vamp-plugin-sdk beatroot-vamp mixxx-keyfinder ffmpeg`
|
||||
* from pypy: `keyfinder vamp`
|
||||
* from pip: `keyfinder vamp`
|
||||
|
||||
|
||||
# usage from copyparty
|
||||
|
||||
@@ -16,6 +16,10 @@ dep: ffmpeg
|
||||
"""
|
||||
|
||||
|
||||
# save beat timestamps to ".beats/filename.txt"
|
||||
SAVE = False
|
||||
|
||||
|
||||
def det(tf):
|
||||
# fmt: off
|
||||
sp.check_call([
|
||||
@@ -23,12 +27,11 @@ def det(tf):
|
||||
b"-nostdin",
|
||||
b"-hide_banner",
|
||||
b"-v", b"fatal",
|
||||
b"-ss", b"13",
|
||||
b"-y", b"-i", fsenc(sys.argv[1]),
|
||||
b"-map", b"0:a:0",
|
||||
b"-ac", b"1",
|
||||
b"-ar", b"22050",
|
||||
b"-t", b"300",
|
||||
b"-t", b"360",
|
||||
b"-f", b"f32le",
|
||||
fsenc(tf)
|
||||
])
|
||||
@@ -47,10 +50,29 @@ def det(tf):
|
||||
print(c["list"][0]["label"].split(" ")[0])
|
||||
return
|
||||
|
||||
# throws if detection failed:
|
||||
bpm = float(cl[-1]["timestamp"] - cl[1]["timestamp"])
|
||||
bpm = round(60 * ((len(cl) - 1) / bpm), 2)
|
||||
print(f"{bpm:.2f}")
|
||||
# throws if detection failed:
|
||||
beats = [float(x["timestamp"]) for x in cl]
|
||||
bds = [b - a for a, b in zip(beats, beats[1:])]
|
||||
bds.sort()
|
||||
n0 = int(len(bds) * 0.2)
|
||||
n1 = int(len(bds) * 0.75) + 1
|
||||
bds = bds[n0:n1]
|
||||
bpm = sum(bds)
|
||||
bpm = round(60 * (len(bds) / bpm), 2)
|
||||
print(f"{bpm:.2f}")
|
||||
|
||||
if SAVE:
|
||||
fdir, fname = os.path.split(sys.argv[1])
|
||||
bdir = os.path.join(fdir, ".beats")
|
||||
try:
|
||||
os.mkdir(fsenc(bdir))
|
||||
except:
|
||||
pass
|
||||
|
||||
fp = os.path.join(bdir, fname) + ".txt"
|
||||
with open(fsenc(fp), "wb") as f:
|
||||
txt = "\n".join([f"{x:.2f}" for x in beats])
|
||||
f.write(txt.encode("utf-8"))
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -61,7 +61,7 @@ def main():
|
||||
|
||||
os.chdir(cwd)
|
||||
f1 = fsenc(fn)
|
||||
f2 = os.path.join(b"noexif", f1)
|
||||
f2 = fsenc(os.path.join(b"noexif", fn))
|
||||
cmd = [
|
||||
b"exiftool",
|
||||
b"-exif:all=",
|
||||
|
||||
@@ -57,6 +57,7 @@ hash -r
|
||||
command -v python3 && pybin=python3 || pybin=python
|
||||
}
|
||||
|
||||
$pybin -c 'import numpy' ||
|
||||
$pybin -m pip install --user numpy
|
||||
|
||||
|
||||
@@ -224,7 +225,7 @@ install_vamp() {
|
||||
$pybin -m pip install --user vamp
|
||||
|
||||
cd "$td"
|
||||
echo '#include <vamp-sdk/Plugin.h>' | gcc -x c -c -o /dev/null - || [ -e ~/pe/vamp-sdk ] || {
|
||||
echo '#include <vamp-sdk/Plugin.h>' | g++ -x c++ -c -o /dev/null - || [ -e ~/pe/vamp-sdk ] || {
|
||||
printf '\033[33mcould not find the vamp-sdk, building from source\033[0m\n'
|
||||
(dl_files yolo https://code.soundsoftware.ac.uk/attachments/download/2588/vamp-plugin-sdk-2.9.0.tar.gz)
|
||||
sha512sum -c <(
|
||||
|
||||
@@ -4,8 +4,9 @@ set -e
|
||||
# runs copyparty (or any other program really) in a chroot
|
||||
#
|
||||
# assumption: these directories, and everything within, are owned by root
|
||||
sysdirs=( /bin /lib /lib32 /lib64 /sbin /usr )
|
||||
|
||||
sysdirs=(); for v in /bin /lib /lib32 /lib64 /sbin /usr /etc/alternatives ; do
|
||||
[ -e $v ] && sysdirs+=($v)
|
||||
done
|
||||
|
||||
# error-handler
|
||||
help() { cat <<'EOF'
|
||||
@@ -38,7 +39,7 @@ while true; do
|
||||
v="$1"; shift
|
||||
[ "$v" = -- ] && break # end of volumes
|
||||
[ "$#" -eq 0 ] && break # invalid usage
|
||||
vols+=( "$(realpath "$v")" )
|
||||
vols+=( "$(realpath "$v" || echo "$v")" )
|
||||
done
|
||||
pybin="$1"; shift
|
||||
pybin="$(command -v "$pybin")"
|
||||
@@ -82,7 +83,7 @@ jail="${jail%/}"
|
||||
printf '%s\n' "${sysdirs[@]}" "${vols[@]}" | sed -r 's`/$``' | LC_ALL=C sort | uniq |
|
||||
while IFS= read -r v; do
|
||||
[ -e "$v" ] || {
|
||||
# printf '\033[1;31mfolder does not exist:\033[0m %s\n' "/$v"
|
||||
printf '\033[1;31mfolder does not exist:\033[0m %s\n' "$v"
|
||||
continue
|
||||
}
|
||||
i1=$(stat -c%D.%i "$v" 2>/dev/null || echo a)
|
||||
@@ -97,9 +98,11 @@ done
|
||||
|
||||
cln() {
|
||||
rv=$?
|
||||
# cleanup if not in use
|
||||
lsof "$jail" | grep -qF "$jail" &&
|
||||
echo "chroot is in use, will not cleanup" ||
|
||||
wait -f -p rv $p || true
|
||||
cd /
|
||||
echo "stopping chroot..."
|
||||
lsof "$jail" | grep -F "$jail" &&
|
||||
echo "chroot is in use; will not unmount" ||
|
||||
{
|
||||
mount | grep -F " on $jail" |
|
||||
awk '{sub(/ type .*/,"");sub(/.* on /,"");print}' |
|
||||
@@ -115,6 +118,15 @@ mkdir -p "$jail/tmp"
|
||||
chmod 777 "$jail/tmp"
|
||||
|
||||
|
||||
# create a dev
|
||||
(cd $jail; mkdir -p dev; cd dev
|
||||
[ -e null ] || mknod -m 666 null c 1 3
|
||||
[ -e zero ] || mknod -m 666 zero c 1 5
|
||||
[ -e random ] || mknod -m 444 random c 1 8
|
||||
[ -e urandom ] || mknod -m 444 urandom c 1 9
|
||||
)
|
||||
|
||||
|
||||
# run copyparty
|
||||
export HOME=$(getent passwd $uid | cut -d: -f6)
|
||||
export USER=$(getent passwd $uid | cut -d: -f1)
|
||||
@@ -124,5 +136,6 @@ export LOGNAME="$USER"
|
||||
#echo "cpp [$cpp]"
|
||||
chroot --userspec=$uid:$gid "$jail" "$pybin" $pyarg "$cpp" "$@" &
|
||||
p=$!
|
||||
trap 'kill -USR1 $p' USR1
|
||||
trap 'kill $p' INT TERM
|
||||
wait
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
S_VERSION = "1.9"
|
||||
S_BUILD_DT = "2023-05-07"
|
||||
|
||||
"""
|
||||
up2k.py: upload to copyparty
|
||||
2023-01-13, v1.2, ed <irc.rizon.net>, MIT-Licensed
|
||||
https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py
|
||||
u2c.py: upload to copyparty
|
||||
2021, ed <irc.rizon.net>, MIT-Licensed
|
||||
https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py
|
||||
|
||||
- dependencies: requests
|
||||
- supports python 2.6, 2.7, and 3.3 through 3.12
|
||||
@@ -18,12 +21,15 @@ import math
|
||||
import time
|
||||
import atexit
|
||||
import signal
|
||||
import socket
|
||||
import base64
|
||||
import hashlib
|
||||
import platform
|
||||
import threading
|
||||
import datetime
|
||||
|
||||
EXE = sys.executable.endswith("exe")
|
||||
|
||||
try:
|
||||
import argparse
|
||||
except:
|
||||
@@ -34,7 +40,9 @@ except:
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
if sys.version_info > (2, 7):
|
||||
if EXE:
|
||||
raise
|
||||
elif sys.version_info > (2, 7):
|
||||
m = "\nERROR: need 'requests'; please run this command:\n {0} -m pip install --user requests\n"
|
||||
else:
|
||||
m = "requests/2.18.4 urllib3/1.23 chardet/3.0.4 certifi/2020.4.5.1 idna/2.7"
|
||||
@@ -51,6 +59,7 @@ PY2 = sys.version_info < (3,)
|
||||
if PY2:
|
||||
from Queue import Queue
|
||||
from urllib import quote, unquote
|
||||
from urlparse import urlsplit, urlunsplit
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
bytes = str
|
||||
@@ -58,6 +67,7 @@ else:
|
||||
from queue import Queue
|
||||
from urllib.parse import unquote_to_bytes as unquote
|
||||
from urllib.parse import quote_from_bytes as quote
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
unicode = str
|
||||
|
||||
@@ -245,7 +255,13 @@ def eprint(*a, **ka):
|
||||
|
||||
|
||||
def flushing_print(*a, **ka):
|
||||
_print(*a, **ka)
|
||||
try:
|
||||
_print(*a, **ka)
|
||||
except:
|
||||
v = " ".join(str(x) for x in a)
|
||||
v = v.encode("ascii", "replace").decode("ascii")
|
||||
_print(v, **ka)
|
||||
|
||||
if "flush" not in ka:
|
||||
sys.stdout.flush()
|
||||
|
||||
@@ -324,6 +340,32 @@ class CTermsize(object):
|
||||
ss = CTermsize()
|
||||
|
||||
|
||||
def undns(url):
|
||||
usp = urlsplit(url)
|
||||
hn = usp.hostname
|
||||
gai = None
|
||||
eprint("resolving host [{0}] ...".format(hn), end="")
|
||||
try:
|
||||
gai = socket.getaddrinfo(hn, None)
|
||||
hn = gai[0][4][0]
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
t = "\n\033[31mfailed to resolve upload destination host;\033[0m\ngai={0}\n"
|
||||
eprint(t.format(repr(gai)))
|
||||
raise
|
||||
|
||||
if usp.port:
|
||||
hn = "{0}:{1}".format(hn, usp.port)
|
||||
if usp.username or usp.password:
|
||||
hn = "{0}:{1}@{2}".format(usp.username, usp.password, hn)
|
||||
|
||||
usp = usp._replace(netloc=hn)
|
||||
url = urlunsplit(usp)
|
||||
eprint(" {0}".format(url))
|
||||
return url
|
||||
|
||||
|
||||
def _scd(err, top):
|
||||
"""non-recursive listing of directory contents, along with stat() info"""
|
||||
with os.scandir(top) as dh:
|
||||
@@ -372,6 +414,23 @@ def walkdir(err, top, seen):
|
||||
def walkdirs(err, tops):
|
||||
"""recursive statdir for a list of tops, yields [top, relpath, stat]"""
|
||||
sep = "{0}".format(os.sep).encode("ascii")
|
||||
if not VT100:
|
||||
za = []
|
||||
for td in tops:
|
||||
try:
|
||||
ap = os.path.abspath(os.path.realpath(td))
|
||||
if td[-1:] in (b"\\", b"/"):
|
||||
ap += sep
|
||||
except:
|
||||
# maybe cpython #88013 (ok)
|
||||
ap = td
|
||||
|
||||
za.append(ap)
|
||||
|
||||
za = [x if x.startswith(b"\\\\") else b"\\\\?\\" + x for x in za]
|
||||
za = [x.replace(b"/", b"\\") for x in za]
|
||||
tops = za
|
||||
|
||||
for top in tops:
|
||||
isdir = os.path.isdir(top)
|
||||
if top[-1:] == sep:
|
||||
@@ -520,7 +579,11 @@ def handshake(ar, file, search):
|
||||
except Exception as ex:
|
||||
em = str(ex).split("SSLError(")[-1].split("\nURL: ")[0].strip()
|
||||
|
||||
if sc == 422 or "<pre>partial upload exists at a different" in txt:
|
||||
if (
|
||||
sc == 422
|
||||
or "<pre>partial upload exists at a different" in txt
|
||||
or "<pre>source file busy; please try again" in txt
|
||||
):
|
||||
file.recheck = True
|
||||
return [], False
|
||||
elif sc == 409 or "<pre>upload rejected, file already exists" in txt:
|
||||
@@ -552,8 +615,8 @@ def handshake(ar, file, search):
|
||||
return r["hash"], r["sprs"]
|
||||
|
||||
|
||||
def upload(file, cid, pw):
|
||||
# type: (File, str, str) -> None
|
||||
def upload(file, cid, pw, stats):
|
||||
# type: (File, str, str, str) -> None
|
||||
"""upload one specific chunk, `cid` (a chunk-hash)"""
|
||||
|
||||
headers = {
|
||||
@@ -561,6 +624,10 @@ def upload(file, cid, pw):
|
||||
"X-Up2k-Wark": file.wark,
|
||||
"Content-Type": "application/octet-stream",
|
||||
}
|
||||
|
||||
if stats:
|
||||
headers["X-Up2k-Stat"] = stats
|
||||
|
||||
if pw:
|
||||
headers["Cookie"] = "=".join(["cppwd", pw])
|
||||
|
||||
@@ -615,6 +682,7 @@ class Ctl(object):
|
||||
return nfiles, nbytes
|
||||
|
||||
def __init__(self, ar, stats=None):
|
||||
self.ok = False
|
||||
self.ar = ar
|
||||
self.stats = stats or self._scan()
|
||||
if not self.stats:
|
||||
@@ -629,6 +697,8 @@ class Ctl(object):
|
||||
req_ses.verify = ar.te
|
||||
|
||||
self.filegen = walkdirs([], ar.files)
|
||||
self.recheck = [] # type: list[File]
|
||||
|
||||
if ar.safe:
|
||||
self._safe()
|
||||
else:
|
||||
@@ -647,11 +717,11 @@ class Ctl(object):
|
||||
self.t0 = time.time()
|
||||
self.t0_up = None
|
||||
self.spd = None
|
||||
self.eta = "99:99:99"
|
||||
|
||||
self.mutex = threading.Lock()
|
||||
self.q_handshake = Queue() # type: Queue[File]
|
||||
self.q_upload = Queue() # type: Queue[tuple[File, str]]
|
||||
self.recheck = [] # type: list[File]
|
||||
|
||||
self.st_hash = [None, "(idle, starting...)"] # type: tuple[File, int]
|
||||
self.st_up = [None, "(idle, starting...)"] # type: tuple[File, int]
|
||||
@@ -660,6 +730,8 @@ class Ctl(object):
|
||||
|
||||
self._fancy()
|
||||
|
||||
self.ok = True
|
||||
|
||||
def _safe(self):
|
||||
"""minimal basic slow boring fallback codepath"""
|
||||
search = self.ar.s
|
||||
@@ -693,7 +765,8 @@ class Ctl(object):
|
||||
ncs = len(hs)
|
||||
for nc, cid in enumerate(hs):
|
||||
print(" {0} up {1}".format(ncs - nc, cid))
|
||||
upload(file, cid, self.ar.a)
|
||||
stats = "{0}/0/0/{1}".format(nf, self.nfiles - nf)
|
||||
upload(file, cid, self.ar.a, stats)
|
||||
|
||||
print(" ok!")
|
||||
if file.recheck:
|
||||
@@ -768,12 +841,12 @@ class Ctl(object):
|
||||
eta = (self.nbytes - self.up_b) / (spd + 1)
|
||||
|
||||
spd = humansize(spd)
|
||||
eta = str(datetime.timedelta(seconds=int(eta)))
|
||||
self.eta = str(datetime.timedelta(seconds=int(eta)))
|
||||
sleft = humansize(self.nbytes - self.up_b)
|
||||
nleft = self.nfiles - self.up_f
|
||||
tail = "\033[K\033[u" if VT100 and not self.ar.ns else "\r"
|
||||
|
||||
t = "{0} eta @ {1}/s, {2}, {3}# left".format(eta, spd, sleft, nleft)
|
||||
t = "{0} eta @ {1}/s, {2}, {3}# left".format(self.eta, spd, sleft, nleft)
|
||||
eprint(txt + "\033]0;{0}\033\\\r{0}{1}".format(t, tail))
|
||||
|
||||
if not self.recheck:
|
||||
@@ -809,9 +882,9 @@ class Ctl(object):
|
||||
print(" ls ~{0}".format(srd))
|
||||
zb = self.ar.url.encode("utf-8")
|
||||
zb += quotep(rd.replace(b"\\", b"/"))
|
||||
r = req_ses.get(zb + b"?ls&dots", headers=headers)
|
||||
r = req_ses.get(zb + b"?ls<&dots", headers=headers)
|
||||
if not r:
|
||||
raise Exception("HTTP {}".format(r.status_code))
|
||||
raise Exception("HTTP {0}".format(r.status_code))
|
||||
|
||||
j = r.json()
|
||||
for f in j["dirs"] + j["files"]:
|
||||
@@ -886,6 +959,9 @@ class Ctl(object):
|
||||
self.handshaker_busy += 1
|
||||
|
||||
upath = file.abs.decode("utf-8", "replace")
|
||||
if not VT100:
|
||||
upath = upath.lstrip("\\?")
|
||||
|
||||
hs, sprs = handshake(self.ar, file, search)
|
||||
if search:
|
||||
if hs:
|
||||
@@ -951,11 +1027,23 @@ class Ctl(object):
|
||||
self.uploader_busy += 1
|
||||
self.t0_up = self.t0_up or time.time()
|
||||
|
||||
zs = "{0}/{1}/{2}/{3} {4}/{5} {6}"
|
||||
stats = zs.format(
|
||||
self.up_f,
|
||||
len(self.recheck),
|
||||
self.uploader_busy,
|
||||
self.nfiles - self.up_f,
|
||||
int(self.nbytes / (1024 * 1024)),
|
||||
int((self.nbytes - self.up_b) / (1024 * 1024)),
|
||||
self.eta,
|
||||
)
|
||||
|
||||
file, cid = task
|
||||
try:
|
||||
upload(file, cid, self.ar.a)
|
||||
except:
|
||||
eprint("upload failed, retrying: {0} #{1}\n".format(file.name, cid[:8]))
|
||||
upload(file, cid, self.ar.a, stats)
|
||||
except Exception as ex:
|
||||
t = "upload failed, retrying: {0} #{1} ({2})\n"
|
||||
eprint(t.format(file.name, cid[:8], ex))
|
||||
# handshake will fix it
|
||||
|
||||
with self.mutex:
|
||||
@@ -989,8 +1077,15 @@ def main():
|
||||
cores = (os.cpu_count() if hasattr(os, "cpu_count") else 0) or 2
|
||||
hcores = min(cores, 3) # 4% faster than 4+ on py3.9 @ r5-4500U
|
||||
|
||||
ver = "{0}, v{1}".format(S_BUILD_DT, S_VERSION)
|
||||
if "--version" in sys.argv:
|
||||
print(ver)
|
||||
return
|
||||
|
||||
sys.argv = [x for x in sys.argv if x != "--ws"]
|
||||
|
||||
# fmt: off
|
||||
ap = app = argparse.ArgumentParser(formatter_class=APF, epilog="""
|
||||
ap = app = argparse.ArgumentParser(formatter_class=APF, description="copyparty up2k uploader / filesearch tool, " + ver, epilog="""
|
||||
NOTE:
|
||||
source file/folder selection uses rsync syntax, meaning that:
|
||||
"foo" uploads the entire folder to URL/foo/
|
||||
@@ -1003,10 +1098,11 @@ source file/folder selection uses rsync syntax, meaning that:
|
||||
ap.add_argument("-a", metavar="PASSWORD", help="password or $filepath")
|
||||
ap.add_argument("-s", action="store_true", help="file-search (disables upload)")
|
||||
ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible")
|
||||
ap.add_argument("--version", action="store_true", help="show version and exit")
|
||||
|
||||
ap = app.add_argument_group("compatibility")
|
||||
ap.add_argument("--cls", action="store_true", help="clear screen before start")
|
||||
ap.add_argument("--ws", action="store_true", help="copyparty is running on windows; wait before deleting files after uploading")
|
||||
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)")
|
||||
|
||||
ap = app.add_argument_group("folder sync")
|
||||
ap.add_argument("--dl", action="store_true", help="delete local files after uploading")
|
||||
@@ -1026,7 +1122,16 @@ source file/folder selection uses rsync syntax, meaning that:
|
||||
ap.add_argument("-td", action="store_true", help="disable certificate check")
|
||||
# fmt: on
|
||||
|
||||
ar = app.parse_args()
|
||||
try:
|
||||
ar = app.parse_args()
|
||||
finally:
|
||||
if EXE and not sys.argv[1:]:
|
||||
eprint("*** hit enter to exit ***")
|
||||
try:
|
||||
input()
|
||||
except:
|
||||
pass
|
||||
|
||||
if ar.drd:
|
||||
ar.dr = True
|
||||
|
||||
@@ -1040,7 +1145,7 @@ source file/folder selection uses rsync syntax, meaning that:
|
||||
|
||||
ar.files = [
|
||||
os.path.abspath(os.path.realpath(x.encode("utf-8")))
|
||||
+ (x[-1:] if x[-1:] == os.sep else "").encode("utf-8")
|
||||
+ (x[-1:] if x[-1:] in ("\\", "/") else "").encode("utf-8")
|
||||
for x in ar.files
|
||||
]
|
||||
|
||||
@@ -1050,24 +1155,32 @@ source file/folder selection uses rsync syntax, meaning that:
|
||||
|
||||
if ar.a and ar.a.startswith("$"):
|
||||
fn = ar.a[1:]
|
||||
print("reading password from file [{}]".format(fn))
|
||||
print("reading password from file [{0}]".format(fn))
|
||||
with open(fn, "rb") as f:
|
||||
ar.a = f.read().decode("utf-8").strip()
|
||||
|
||||
for n in range(ar.rh):
|
||||
try:
|
||||
ar.url = undns(ar.url)
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
if n > ar.rh - 2:
|
||||
raise
|
||||
|
||||
if ar.cls:
|
||||
print("\x1b\x5b\x48\x1b\x5b\x32\x4a\x1b\x5b\x33\x4a", end="")
|
||||
eprint("\x1b\x5b\x48\x1b\x5b\x32\x4a\x1b\x5b\x33\x4a", end="")
|
||||
|
||||
ctl = Ctl(ar)
|
||||
|
||||
if ar.dr and not ar.drd:
|
||||
if ar.dr and not ar.drd and ctl.ok:
|
||||
print("\npass 2/2: delete")
|
||||
if getattr(ctl, "up_br") and ar.ws:
|
||||
# wait for up2k to mtime if there was uploads
|
||||
time.sleep(4)
|
||||
|
||||
ar.drd = True
|
||||
ar.z = True
|
||||
Ctl(ar, ctl.stats)
|
||||
ctl = Ctl(ar, ctl.stats)
|
||||
|
||||
sys.exit(0 if ctl.ok else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -1,7 +1,6 @@
|
||||
# when running copyparty behind a reverse proxy,
|
||||
# the following arguments are recommended:
|
||||
#
|
||||
# --http-only lower latency on initial connection
|
||||
# -i 127.0.0.1 only accept connections from nginx
|
||||
#
|
||||
# if you are doing location-based proxying (such as `/stuff` below)
|
||||
|
||||
@@ -1,14 +1,44 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cat >/dev/null <<'EOF'
|
||||
|
||||
NOTE: copyparty is now able to do this automatically;
|
||||
however you may wish to use this script instead if
|
||||
you have specific needs (or if copyparty breaks)
|
||||
|
||||
this script generates a new self-signed TLS certificate and
|
||||
replaces the default insecure one that comes with copyparty
|
||||
|
||||
as it is trivial to impersonate a copyparty server using the
|
||||
default certificate, it is highly recommended to do this
|
||||
|
||||
this will create a self-signed CA, and a Server certificate
|
||||
which gets signed by that CA -- you can run it multiple times
|
||||
with different server-FQDNs / IPs to create additional certs
|
||||
for all your different servers / (non-)copyparty services
|
||||
|
||||
EOF
|
||||
|
||||
|
||||
# ca-name and server-fqdn
|
||||
ca_name="$1"
|
||||
srv_fqdn="$2"
|
||||
|
||||
[ -z "$srv_fqdn" ] && {
|
||||
echo "need arg 1: ca name"
|
||||
echo "need arg 2: server fqdn and/or IPs, comma-separated"
|
||||
echo "optional arg 3: if set, write cert into copyparty cfg"
|
||||
[ -z "$srv_fqdn" ] && { cat <<'EOF'
|
||||
need arg 1: ca name
|
||||
need arg 2: server fqdn and/or IPs, comma-separated
|
||||
optional arg 3: if set, write cert into copyparty cfg
|
||||
|
||||
example:
|
||||
./cfssl.sh PartyCo partybox.local y
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
command -v cfssljson 2>/dev/null || {
|
||||
echo please install cfssl and try again
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -59,12 +89,14 @@ show() {
|
||||
}
|
||||
show ca.pem
|
||||
show "$srv_fqdn.pem"
|
||||
|
||||
echo
|
||||
echo "successfully generated new certificates"
|
||||
|
||||
# write cert into copyparty config
|
||||
[ -z "$3" ] || {
|
||||
mkdir -p ~/.config/copyparty
|
||||
cat "$srv_fqdn".{key,pem} ca.pem >~/.config/copyparty/cert.pem
|
||||
echo "successfully replaced copyparty certificate"
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>⇆🎉 redirect</title>
|
||||
<title>💾🎉 redirect</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<style>
|
||||
|
||||
|
||||
BIN
contrib/ios/upload-to-copyparty.shortcut
Normal file
BIN
contrib/ios/upload-to-copyparty.shortcut
Normal file
Binary file not shown.
@@ -1,7 +1,6 @@
|
||||
# when running copyparty behind a reverse proxy,
|
||||
# the following arguments are recommended:
|
||||
#
|
||||
# --http-only lower latency on initial connection
|
||||
# -i 127.0.0.1 only accept connections from nginx
|
||||
#
|
||||
# -nc must match or exceed the webserver's max number of concurrent clients;
|
||||
@@ -9,7 +8,7 @@
|
||||
# nginx default is 512 (worker_processes 1, worker_connections 512)
|
||||
#
|
||||
# you may also consider adding -j0 for CPU-intensive configurations
|
||||
# (not that i can really think of any good examples)
|
||||
# (5'000 requests per second, or 20gbps upload/download in parallel)
|
||||
#
|
||||
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
|
||||
|
||||
@@ -39,3 +38,9 @@ server {
|
||||
proxy_set_header Connection "Keep-Alive";
|
||||
}
|
||||
}
|
||||
|
||||
# default client_max_body_size (1M) blocks uploads larger than 256 MiB
|
||||
client_max_body_size 1024M;
|
||||
client_header_timeout 610m;
|
||||
client_body_timeout 610m;
|
||||
send_timeout 610m;
|
||||
|
||||
281
contrib/nixos/modules/copyparty.nix
Normal file
281
contrib/nixos/modules/copyparty.nix
Normal file
@@ -0,0 +1,281 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
mkKeyValue = key: value:
|
||||
if value == true then
|
||||
# sets with a true boolean value are coerced to just the key name
|
||||
key
|
||||
else if value == false then
|
||||
# or omitted completely when false
|
||||
""
|
||||
else
|
||||
(generators.mkKeyValueDefault { inherit mkValueString; } ": " key value);
|
||||
|
||||
mkAttrsString = value: (generators.toKeyValue { inherit mkKeyValue; } value);
|
||||
|
||||
mkValueString = value:
|
||||
if isList value then
|
||||
(concatStringsSep ", " (map mkValueString value))
|
||||
else if isAttrs value then
|
||||
"\n" + (mkAttrsString value)
|
||||
else
|
||||
(generators.mkValueStringDefault { } value);
|
||||
|
||||
mkSectionName = value: "[" + (escape [ "[" "]" ] value) + "]";
|
||||
|
||||
mkSection = name: attrs: ''
|
||||
${mkSectionName name}
|
||||
${mkAttrsString attrs}
|
||||
'';
|
||||
|
||||
mkVolume = name: attrs: ''
|
||||
${mkSectionName name}
|
||||
${attrs.path}
|
||||
${mkAttrsString {
|
||||
accs = attrs.access;
|
||||
flags = attrs.flags;
|
||||
}}
|
||||
'';
|
||||
|
||||
passwordPlaceholder = name: "{{password-${name}}}";
|
||||
|
||||
accountsWithPlaceholders = mapAttrs (name: attrs: passwordPlaceholder name);
|
||||
|
||||
configStr = ''
|
||||
${mkSection "global" cfg.settings}
|
||||
${mkSection "accounts" (accountsWithPlaceholders cfg.accounts)}
|
||||
${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)}
|
||||
'';
|
||||
|
||||
name = "copyparty";
|
||||
cfg = config.services.copyparty;
|
||||
configFile = pkgs.writeText "${name}.conf" configStr;
|
||||
runtimeConfigPath = "/run/${name}/${name}.conf";
|
||||
home = "/var/lib/${name}";
|
||||
defaultShareDir = "${home}/data";
|
||||
in {
|
||||
options.services.copyparty = {
|
||||
enable = mkEnableOption "web-based file manager";
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.copyparty;
|
||||
defaultText = "pkgs.copyparty";
|
||||
description = ''
|
||||
Package of the application to run, exposed for overriding purposes.
|
||||
'';
|
||||
};
|
||||
|
||||
openFilesLimit = mkOption {
|
||||
default = 4096;
|
||||
type = types.either types.int types.str;
|
||||
description = "Number of files to allow copyparty to open.";
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
type = types.attrs;
|
||||
description = ''
|
||||
Global settings to apply.
|
||||
Directly maps to values in the [global] section of the copyparty config.
|
||||
See `${getExe cfg.package} --help` for more details.
|
||||
'';
|
||||
default = {
|
||||
i = "127.0.0.1";
|
||||
no-reload = true;
|
||||
};
|
||||
example = literalExpression ''
|
||||
{
|
||||
i = "0.0.0.0";
|
||||
no-reload = true;
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
accounts = mkOption {
|
||||
type = types.attrsOf (types.submodule ({ ... }: {
|
||||
options = {
|
||||
passwordFile = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
Runtime file path to a file containing the user password.
|
||||
Must be readable by the copyparty user.
|
||||
'';
|
||||
example = "/run/keys/copyparty/ed";
|
||||
};
|
||||
};
|
||||
}));
|
||||
description = ''
|
||||
A set of copyparty accounts to create.
|
||||
'';
|
||||
default = { };
|
||||
example = literalExpression ''
|
||||
{
|
||||
ed.passwordFile = "/run/keys/copyparty/ed";
|
||||
};
|
||||
'';
|
||||
};
|
||||
|
||||
volumes = mkOption {
|
||||
type = types.attrsOf (types.submodule ({ ... }: {
|
||||
options = {
|
||||
path = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
Path of a directory to share.
|
||||
'';
|
||||
};
|
||||
access = mkOption {
|
||||
type = types.attrs;
|
||||
description = ''
|
||||
Attribute list of permissions and the users to apply them to.
|
||||
|
||||
The key must be a string containing any combination of allowed permission:
|
||||
"r" (read): list folder contents, download files
|
||||
"w" (write): upload files; need "r" to see the uploads
|
||||
"m" (move): move files and folders; need "w" at destination
|
||||
"d" (delete): permanently delete files and folders
|
||||
"g" (get): download files, but cannot see folder contents
|
||||
"G" (upget): "get", but can see filekeys of their own uploads
|
||||
|
||||
For example: "rwmd"
|
||||
|
||||
The value must be one of:
|
||||
an account name, defined in `accounts`
|
||||
a list of account names
|
||||
"*", which means "any account"
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
# wG = write-upget = see your own uploads only
|
||||
wG = "*";
|
||||
# read-write-modify-delete for users "ed" and "k"
|
||||
rwmd = ["ed" "k"];
|
||||
};
|
||||
'';
|
||||
};
|
||||
flags = mkOption {
|
||||
type = types.attrs;
|
||||
description = ''
|
||||
Attribute list of volume flags to apply.
|
||||
See `${getExe cfg.package} --help-flags` for more details.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
# "fk" enables filekeys (necessary for upget permission) (4 chars long)
|
||||
fk = 4;
|
||||
# scan for new files every 60sec
|
||||
scan = 60;
|
||||
# volflag "e2d" enables the uploads database
|
||||
e2d = true;
|
||||
# "d2t" disables multimedia parsers (in case the uploads are malicious)
|
||||
d2t = true;
|
||||
# skips hashing file contents if path matches *.iso
|
||||
nohash = "\.iso$";
|
||||
};
|
||||
'';
|
||||
default = { };
|
||||
};
|
||||
};
|
||||
}));
|
||||
description = "A set of copyparty volumes to create";
|
||||
default = {
|
||||
"/" = {
|
||||
path = defaultShareDir;
|
||||
access = { r = "*"; };
|
||||
};
|
||||
};
|
||||
example = literalExpression ''
|
||||
{
|
||||
"/" = {
|
||||
path = ${defaultShareDir};
|
||||
access = {
|
||||
# wG = write-upget = see your own uploads only
|
||||
wG = "*";
|
||||
# read-write-modify-delete for users "ed" and "k"
|
||||
rwmd = ["ed" "k"];
|
||||
};
|
||||
};
|
||||
};
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.copyparty = {
|
||||
description = "http file sharing hub";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
environment = {
|
||||
PYTHONUNBUFFERED = "true";
|
||||
XDG_CONFIG_HOME = "${home}/.config";
|
||||
};
|
||||
|
||||
preStart = let
|
||||
replaceSecretCommand = name: attrs:
|
||||
"${getExe pkgs.replace-secret} '${
|
||||
passwordPlaceholder name
|
||||
}' '${attrs.passwordFile}' ${runtimeConfigPath}";
|
||||
in ''
|
||||
set -euo pipefail
|
||||
install -m 600 ${configFile} ${runtimeConfigPath}
|
||||
${concatStringsSep "\n"
|
||||
(mapAttrsToList replaceSecretCommand cfg.accounts)}
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = "${getExe cfg.package} -c ${runtimeConfigPath}";
|
||||
|
||||
# Hardening options
|
||||
User = "copyparty";
|
||||
Group = "copyparty";
|
||||
RuntimeDirectory = name;
|
||||
RuntimeDirectoryMode = "0700";
|
||||
StateDirectory = [ name "${name}/data" "${name}/.config" ];
|
||||
StateDirectoryMode = "0700";
|
||||
WorkingDirectory = home;
|
||||
TemporaryFileSystem = "/:ro";
|
||||
BindReadOnlyPaths = [
|
||||
"/nix/store"
|
||||
"-/etc/resolv.conf"
|
||||
"-/etc/nsswitch.conf"
|
||||
"-/etc/hosts"
|
||||
"-/etc/localtime"
|
||||
] ++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts);
|
||||
BindPaths = [ home ] ++ (mapAttrsToList (k: v: v.path) cfg.volumes);
|
||||
# Would re-mount paths ignored by temporary root
|
||||
#ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
PrivateDevices = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectControlGroups = true;
|
||||
RestrictSUIDSGID = true;
|
||||
PrivateMounts = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectHostname = true;
|
||||
ProtectClock = true;
|
||||
ProtectProc = "invisible";
|
||||
ProcSubset = "pid";
|
||||
RestrictNamespaces = true;
|
||||
RemoveIPC = true;
|
||||
UMask = "0077";
|
||||
LimitNOFILE = cfg.openFilesLimit;
|
||||
NoNewPrivileges = true;
|
||||
LockPersonality = true;
|
||||
RestrictRealtime = true;
|
||||
};
|
||||
};
|
||||
|
||||
users.groups.copyparty = { };
|
||||
users.users.copyparty = {
|
||||
description = "Service user for copyparty";
|
||||
group = "copyparty";
|
||||
home = home;
|
||||
isSystemUser = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
54
contrib/package/arch/PKGBUILD
Normal file
54
contrib/package/arch/PKGBUILD
Normal file
@@ -0,0 +1,54 @@
|
||||
# Maintainer: icxes <dev.null@need.moe>
|
||||
pkgname=copyparty
|
||||
pkgver="1.7.2"
|
||||
pkgrel=1
|
||||
pkgdesc="Portable file sharing hub"
|
||||
arch=("any")
|
||||
url="https://github.com/9001/${pkgname}"
|
||||
license=('MIT')
|
||||
depends=("python" "lsof" "python-jinja")
|
||||
makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz")
|
||||
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
|
||||
"python-mutagen: music tags (alternative)"
|
||||
"python-pillow: thumbnails for images"
|
||||
"python-pyvips: thumbnails for images (higher quality, faster, uses more ram)"
|
||||
"libkeyfinder-git: detection of musical keys"
|
||||
"qm-vamp-plugins: BPM detection"
|
||||
"python-pyopenssl: ftps functionality"
|
||||
"python-impacket-git: smb support (bad idea)"
|
||||
)
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("etc/${pkgname}.d/init" )
|
||||
sha256sums=("fb261d45ce7cf146a3f620d1e3109eb5c584f8950e61a872e2d92d7b7447bae0")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
pushd copyparty/web
|
||||
make -j$(nproc)
|
||||
rm Makefile
|
||||
popd
|
||||
|
||||
python3 -m build -wn
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
python3 -m installer -d "$pkgdir" dist/*.whl
|
||||
|
||||
install -dm755 "${pkgdir}/etc/${pkgname}.d"
|
||||
install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty"
|
||||
install -Dm644 "contrib/package/arch/${pkgname}.conf" "${pkgdir}/etc/${pkgname}.d/init"
|
||||
install -Dm644 "contrib/package/arch/${pkgname}.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}.service"
|
||||
install -Dm644 "contrib/package/arch/prisonparty.service" "${pkgdir}/usr/lib/systemd/system/prisonparty.service"
|
||||
install -Dm644 "contrib/package/arch/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
|
||||
install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
|
||||
find /etc/${pkgname}.d -iname '*.conf' 2>/dev/null | grep -qE . && return
|
||||
echo "┏━━━━━━━━━━━━━━━──-"
|
||||
echo "┃ Configure ${pkgname} by adding .conf files into /etc/${pkgname}.d/"
|
||||
echo "┃ and maybe copy+edit one of the following to /etc/systemd/system/:"
|
||||
echo "┣━♦ /usr/lib/systemd/system/${pkgname}.service (standard)"
|
||||
echo "┣━♦ /usr/lib/systemd/system/prisonparty.service (chroot)"
|
||||
echo "┗━━━━━━━━━━━━━━━──-"
|
||||
}
|
||||
7
contrib/package/arch/copyparty.conf
Normal file
7
contrib/package/arch/copyparty.conf
Normal file
@@ -0,0 +1,7 @@
|
||||
## import all *.conf files from the current folder (/etc/copyparty.d)
|
||||
% ./
|
||||
|
||||
# add additional .conf files to this folder;
|
||||
# see example config files for reference:
|
||||
# https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf
|
||||
# https://github.com/9001/copyparty/tree/hovudstraum/docs/copyparty.d
|
||||
32
contrib/package/arch/copyparty.service
Normal file
32
contrib/package/arch/copyparty.service
Normal file
@@ -0,0 +1,32 @@
|
||||
# this will start `/usr/bin/copyparty-sfx.py`
|
||||
# and read config from `/etc/copyparty.d/*.conf`
|
||||
#
|
||||
# you probably want to:
|
||||
# change "User=cpp" and "/home/cpp/" to another user
|
||||
#
|
||||
# unless you add -q to disable logging, you may want to remove the
|
||||
# following line to allow buffering (slightly better performance):
|
||||
# Environment=PYTHONUNBUFFERED=x
|
||||
|
||||
[Unit]
|
||||
Description=copyparty file server
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
SyslogIdentifier=copyparty
|
||||
Environment=PYTHONUNBUFFERED=x
|
||||
WorkingDirectory=/var/lib/copyparty-jail
|
||||
ExecReload=/bin/kill -s USR1 $MAINPID
|
||||
|
||||
# user to run as + where the TLS certificate is (if any)
|
||||
User=cpp
|
||||
Environment=XDG_CONFIG_HOME=/home/cpp/.config
|
||||
|
||||
# stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running
|
||||
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
||||
|
||||
# run copyparty
|
||||
ExecStart=/usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
3
contrib/package/arch/index.md
Normal file
3
contrib/package/arch/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured
|
||||
|
||||
please add some `*.conf` files to `/etc/copyparty.d/`
|
||||
31
contrib/package/arch/prisonparty.service
Normal file
31
contrib/package/arch/prisonparty.service
Normal file
@@ -0,0 +1,31 @@
|
||||
# this will start `/usr/bin/copyparty-sfx.py`
|
||||
# in a chroot, preventing accidental access elsewhere
|
||||
# and read config from `/etc/copyparty.d/*.conf`
|
||||
#
|
||||
# expose additional filesystem locations to copyparty
|
||||
# by listing them between the last `1000` and `--`
|
||||
#
|
||||
# `1000 1000` = what user to run copyparty as
|
||||
#
|
||||
# unless you add -q to disable logging, you may want to remove the
|
||||
# following line to allow buffering (slightly better performance):
|
||||
# Environment=PYTHONUNBUFFERED=x
|
||||
|
||||
[Unit]
|
||||
Description=copyparty file server
|
||||
|
||||
[Service]
|
||||
SyslogIdentifier=prisonparty
|
||||
Environment=PYTHONUNBUFFERED=x
|
||||
WorkingDirectory=/var/lib/copyparty-jail
|
||||
ExecReload=/bin/kill -s USR1 $MAINPID
|
||||
|
||||
# stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running
|
||||
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
||||
|
||||
# run copyparty
|
||||
ExecStart=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail 1000 1000 /etc/copyparty.d -- \
|
||||
/usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
55
contrib/package/nix/copyparty/default.nix
Normal file
55
contrib/package/nix/copyparty/default.nix
Normal file
@@ -0,0 +1,55 @@
|
||||
{ lib, stdenv, makeWrapper, fetchurl, utillinux, python, jinja2, impacket, pyftpdlib, pyopenssl, pillow, pyvips, ffmpeg, mutagen,
|
||||
|
||||
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
|
||||
withThumbnails ? true,
|
||||
|
||||
# create thumbnails with PyVIPS; even faster, uses more memory
|
||||
# -- can be combined with Pillow to support more filetypes
|
||||
withFastThumbnails ? false,
|
||||
|
||||
# enable FFmpeg; thumbnails for most filetypes (also video and audio), extract audio metadata, transcode audio to opus
|
||||
# -- possibly dangerous if you allow anonymous uploads, since FFmpeg has a huge attack surface
|
||||
# -- can be combined with Thumbnails and/or FastThumbnails, since FFmpeg is slower than both
|
||||
withMediaProcessing ? true,
|
||||
|
||||
# if MediaProcessing is not enabled, you probably want this instead (less accurate, but much safer and faster)
|
||||
withBasicAudioMetadata ? false,
|
||||
|
||||
# enable FTPS support in the FTP server
|
||||
withFTPS ? false,
|
||||
|
||||
# samba/cifs server; dangerous and buggy, enable if you really need it
|
||||
withSMB ? false,
|
||||
|
||||
}:
|
||||
|
||||
let
|
||||
pinData = lib.importJSON ./pin.json;
|
||||
pyEnv = python.withPackages (ps:
|
||||
with ps; [
|
||||
jinja2
|
||||
]
|
||||
++ lib.optional withSMB impacket
|
||||
++ lib.optional withFTPS pyopenssl
|
||||
++ lib.optional withThumbnails pillow
|
||||
++ lib.optional withFastThumbnails pyvips
|
||||
++ lib.optional withMediaProcessing ffmpeg
|
||||
++ lib.optional withBasicAudioMetadata mutagen
|
||||
);
|
||||
in stdenv.mkDerivation {
|
||||
pname = "copyparty";
|
||||
version = pinData.version;
|
||||
src = fetchurl {
|
||||
url = pinData.url;
|
||||
hash = pinData.hash;
|
||||
};
|
||||
buildInputs = [ makeWrapper ];
|
||||
dontUnpack = true;
|
||||
dontBuild = true;
|
||||
installPhase = ''
|
||||
install -Dm755 $src $out/share/copyparty-sfx.py
|
||||
makeWrapper ${pyEnv.interpreter} $out/bin/copyparty \
|
||||
--set PATH '${lib.makeBinPath ([ utillinux ] ++ lib.optional withMediaProcessing ffmpeg)}:$PATH' \
|
||||
--add-flags "$out/share/copyparty-sfx.py"
|
||||
'';
|
||||
}
|
||||
5
contrib/package/nix/copyparty/pin.json
Normal file
5
contrib/package/nix/copyparty/pin.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.7.2/copyparty-sfx.py",
|
||||
"version": "1.7.2",
|
||||
"hash": "sha256-h22sRRO/bpydgRVmSVD05ZLuzsUxBCWU3izt9Eg9bf0="
|
||||
}
|
||||
77
contrib/package/nix/copyparty/update.py
Executable file
77
contrib/package/nix/copyparty/update.py
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Update the Nix package pin
|
||||
#
|
||||
# Usage: ./update.sh [PATH]
|
||||
# When the [PATH] is not set, it will fetch the latest release from the repo.
|
||||
# With [PATH] set, it will hash the given file and generate the URL,
|
||||
# base on the version contained within the file
|
||||
|
||||
import base64
|
||||
import json
|
||||
import hashlib
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
OUTPUT_FILE = Path("pin.json")
|
||||
TARGET_ASSET = "copyparty-sfx.py"
|
||||
HASH_TYPE = "sha256"
|
||||
LATEST_RELEASE_URL = "https://api.github.com/repos/9001/copyparty/releases/latest"
|
||||
DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET}"
|
||||
|
||||
|
||||
def get_formatted_hash(binary):
|
||||
hasher = hashlib.new("sha256")
|
||||
hasher.update(binary)
|
||||
asset_hash = hasher.digest()
|
||||
encoded_hash = base64.b64encode(asset_hash).decode("ascii")
|
||||
return f"{HASH_TYPE}-{encoded_hash}"
|
||||
|
||||
|
||||
def version_from_sfx(binary):
|
||||
result = re.search(b'^VER = "(.*)"$', binary, re.MULTILINE)
|
||||
if result:
|
||||
return result.groups(1)[0].decode("ascii")
|
||||
|
||||
raise ValueError("version not found in provided file")
|
||||
|
||||
|
||||
def remote_release_pin():
|
||||
import requests
|
||||
|
||||
response = requests.get(LATEST_RELEASE_URL).json()
|
||||
version = response["tag_name"].lstrip("v")
|
||||
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET][0]
|
||||
download_url = asset_info["browser_download_url"]
|
||||
asset = requests.get(download_url)
|
||||
formatted_hash = get_formatted_hash(asset.content)
|
||||
|
||||
result = {"url": download_url, "version": version, "hash": formatted_hash}
|
||||
return result
|
||||
|
||||
|
||||
def local_release_pin(path):
|
||||
asset = path.read_bytes()
|
||||
version = version_from_sfx(asset)
|
||||
download_url = DOWNLOAD_URL(version)
|
||||
formatted_hash = get_formatted_hash(asset)
|
||||
|
||||
result = {"url": download_url, "version": version, "hash": formatted_hash}
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1:
|
||||
asset_path = Path(sys.argv[1])
|
||||
result = local_release_pin(asset_path)
|
||||
else:
|
||||
result = remote_release_pin()
|
||||
|
||||
print(result)
|
||||
json_result = json.dumps(result, indent=4)
|
||||
OUTPUT_FILE.write_text(json_result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,4 +1,9 @@
|
||||
<!--
|
||||
NOTE: DEPRECATED; please use the javascript version instead:
|
||||
https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/minimal-up2k.js
|
||||
|
||||
----
|
||||
|
||||
save this as .epilogue.html inside a write-only folder to declutter the UI, makes it look like
|
||||
https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png
|
||||
|
||||
@@ -11,7 +16,7 @@
|
||||
|
||||
/* make the up2k ui REALLY minimal by hiding a bunch of stuff: */
|
||||
|
||||
#ops, #tree, #path, #epi+h2, /* main tabs and navigators (tree/breadcrumbs) */
|
||||
#ops, #tree, #path, #wfp, /* main tabs and navigators (tree/breadcrumbs) */
|
||||
|
||||
#u2conf tr:first-child>td[rowspan]:not(#u2btn_cw), /* most of the config options */
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ almost the same as minimal-up2k.html except this one...:
|
||||
var u2min = `
|
||||
<style>
|
||||
|
||||
#ops, #path, #tree, #files, #epi+div+h2,
|
||||
#ops, #path, #tree, #files, #wfp,
|
||||
#u2conf td.c+.c, #u2cards, #srch_dz, #srch_zd {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -55,5 +55,5 @@ var u2min = `
|
||||
if (!has(perms, 'read')) {
|
||||
var e2 = mknod('div');
|
||||
e2.innerHTML = u2min;
|
||||
ebi('wrap').insertBefore(e2, QS('#epi+h2'));
|
||||
ebi('wrap').insertBefore(e2, QS('#wfp'));
|
||||
}
|
||||
|
||||
208
contrib/plugins/rave.js
Normal file
208
contrib/plugins/rave.js
Normal file
@@ -0,0 +1,208 @@
|
||||
/* untz untz untz untz */
|
||||
|
||||
(function () {
|
||||
|
||||
var can, ctx, W, H, fft, buf, bars, barw, pv,
|
||||
hue = 0,
|
||||
ibeat = 0,
|
||||
beats = [9001],
|
||||
beats_url = '',
|
||||
uofs = 0,
|
||||
ops = ebi('ops'),
|
||||
raving = false,
|
||||
recalc = 0,
|
||||
cdown = 0,
|
||||
FC = 0.9,
|
||||
css = `<style>
|
||||
|
||||
#fft {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
body {
|
||||
box-shadow: inset 0 0 0 white;
|
||||
}
|
||||
#ops>a,
|
||||
#path>a {
|
||||
display: inline-block;
|
||||
}
|
||||
/*
|
||||
body.untz {
|
||||
animation: untz-body 200ms ease-out;
|
||||
}
|
||||
@keyframes untz-body {
|
||||
0% {inset 0 0 20em white}
|
||||
100% {inset 0 0 0 white}
|
||||
}
|
||||
*/
|
||||
:root, html.a, html.b, html.c, html.d, html.e {
|
||||
--row-alt: rgba(48,52,78,0.2);
|
||||
}
|
||||
#files td {
|
||||
background: none;
|
||||
}
|
||||
|
||||
</style>`;
|
||||
|
||||
QS('body').appendChild(mknod('div', null, css));
|
||||
|
||||
function rave_load() {
|
||||
console.log('rave_load');
|
||||
can = mknod('canvas', 'fft');
|
||||
QS('body').appendChild(can);
|
||||
ctx = can.getContext('2d');
|
||||
|
||||
fft = new AnalyserNode(actx, {
|
||||
"fftSize": 2048,
|
||||
"maxDecibels": 0,
|
||||
"smoothingTimeConstant": 0.7,
|
||||
});
|
||||
ibeat = 0;
|
||||
beats = [9001];
|
||||
buf = new Uint8Array(fft.frequencyBinCount);
|
||||
bars = buf.length * FC;
|
||||
afilt.filters.push(fft);
|
||||
if (!raving) {
|
||||
raving = true;
|
||||
raver();
|
||||
}
|
||||
beats_url = mp.au.src.split('?')[0].replace(/(.*\/)(.*)/, '$1.beats/$2.txt');
|
||||
console.log("reading beats from", beats_url);
|
||||
var xhr = new XHR();
|
||||
xhr.open('GET', beats_url, true);
|
||||
xhr.onload = readbeats;
|
||||
xhr.url = beats_url;
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function rave_unload() {
|
||||
qsr('#fft');
|
||||
can = null;
|
||||
}
|
||||
|
||||
function readbeats() {
|
||||
if (this.url != beats_url)
|
||||
return console.log('old beats??', this.url, beats_url);
|
||||
|
||||
var sbeats = this.responseText.replace(/\r/g, '').split(/\n/g);
|
||||
if (sbeats.length < 3)
|
||||
return;
|
||||
|
||||
beats = [];
|
||||
for (var a = 0; a < sbeats.length; a++)
|
||||
beats.push(parseFloat(sbeats[a]));
|
||||
|
||||
var end = beats.slice(-2),
|
||||
t = end[1],
|
||||
d = t - end[0];
|
||||
|
||||
while (d > 0.1 && t < 1200)
|
||||
beats.push(t += d);
|
||||
}
|
||||
|
||||
function hrand() {
|
||||
return Math.random() - 0.5;
|
||||
}
|
||||
|
||||
function raver() {
|
||||
if (!can) {
|
||||
raving = false;
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(raver);
|
||||
if (!mp || !mp.au || mp.au.paused)
|
||||
return;
|
||||
|
||||
if (--uofs >= 0) {
|
||||
document.body.style.marginLeft = hrand() * uofs + 'px';
|
||||
ebi('tree').style.marginLeft = hrand() * uofs + 'px';
|
||||
for (var a of QSA('#ops>a, #path>a, #pctl>a'))
|
||||
a.style.transform = 'translate(' + hrand() * uofs * 1 + 'px, ' + hrand() * uofs * 0.7 + 'px) rotate(' + Math.random() * uofs * 0.7 + 'deg)'
|
||||
}
|
||||
|
||||
if (--recalc < 0) {
|
||||
recalc = 60;
|
||||
var tree = ebi('tree'),
|
||||
x = tree.style.display == 'none' ? 0 : tree.offsetWidth;
|
||||
|
||||
//W = can.width = window.innerWidth - x;
|
||||
//H = can.height = window.innerHeight;
|
||||
//H = ebi('widget').offsetTop;
|
||||
W = can.width = bars;
|
||||
H = can.height = 512;
|
||||
barw = 1; //parseInt(0.8 + W / bars);
|
||||
can.style.left = x + 'px';
|
||||
can.style.width = (window.innerWidth - x) + 'px';
|
||||
can.style.height = ebi('widget').offsetTop + 'px';
|
||||
}
|
||||
|
||||
//if (--cdown == 1)
|
||||
// clmod(ops, 'untz');
|
||||
|
||||
fft.getByteFrequencyData(buf);
|
||||
|
||||
var imax = 0, vmax = 0;
|
||||
for (var a = 10; a < 50; a++)
|
||||
if (vmax < buf[a]) {
|
||||
vmax = buf[a];
|
||||
imax = a;
|
||||
}
|
||||
|
||||
hue = hue * 0.93 + imax * 0.07;
|
||||
|
||||
ctx.fillStyle = 'rgba(0,0,0,0)';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
ctx.fillStyle = 'hsla(' + (hue * 2.5) + ',100%,50%,0.7)';
|
||||
|
||||
var x = 0, mul = (H / 256) * 0.5;
|
||||
for (var a = 0; a < buf.length * FC; a++) {
|
||||
var v = buf[a] * mul * (1 + 0.69 * a / buf.length);
|
||||
ctx.fillRect(x, H - v, barw, v);
|
||||
x += barw;
|
||||
}
|
||||
|
||||
var t = mp.au.currentTime + 0.05;
|
||||
|
||||
if (ibeat >= beats.length || beats[ibeat] > t)
|
||||
return;
|
||||
|
||||
while (ibeat < beats.length && beats[ibeat++] < t)
|
||||
continue;
|
||||
|
||||
return untz();
|
||||
|
||||
var cv = 0;
|
||||
for (var a = 0; a < 128; a++)
|
||||
cv += buf[a];
|
||||
|
||||
if (cv - pv > 1000) {
|
||||
console.log(pv, cv, cv - pv);
|
||||
if (cdown < 0) {
|
||||
clmod(ops, 'untz', 1);
|
||||
cdown = 20;
|
||||
}
|
||||
}
|
||||
pv = cv;
|
||||
}
|
||||
|
||||
function untz() {
|
||||
console.log('untz');
|
||||
uofs = 14;
|
||||
document.body.animate([
|
||||
{ boxShadow: 'inset 0 0 1em #f0c' },
|
||||
{ boxShadow: 'inset 0 0 20em #f0c', offset: 0.2 },
|
||||
{ boxShadow: 'inset 0 0 0 #f0c' },
|
||||
], { duration: 200, iterations: 1 });
|
||||
}
|
||||
|
||||
afilt.plugs.push({
|
||||
"en": true,
|
||||
"load": rave_load,
|
||||
"unload": rave_unload
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -1,3 +1,6 @@
|
||||
# NOTE: this is now a built-in feature in copyparty
|
||||
# but you may still want this if you have specific needs
|
||||
#
|
||||
# systemd service which generates a new TLS certificate on each boot,
|
||||
# that way the one-year expiry time won't cause any issues --
|
||||
# just have everyone trust the ca.pem once every 10 years
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
# and share '/mnt' with anonymous read+write
|
||||
#
|
||||
# installation:
|
||||
# cp -pv copyparty.service /etc/systemd/system
|
||||
# restorecon -vr /etc/systemd/system/copyparty.service
|
||||
# wget https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py -O /usr/local/bin/copyparty-sfx.py
|
||||
# cp -pv copyparty.service /etc/systemd/system/
|
||||
# restorecon -vr /etc/systemd/system/copyparty.service # on fedora/rhel
|
||||
# firewall-cmd --permanent --add-port={80,443,3923}/tcp # --zone=libvirt
|
||||
# firewall-cmd --reload
|
||||
# systemctl daemon-reload && systemctl enable --now copyparty
|
||||
#
|
||||
# if it fails to start, first check this: systemctl status copyparty
|
||||
# then try starting it while viewing logs: journalctl -fan 100
|
||||
#
|
||||
# you may want to:
|
||||
# change "User=cpp" and "/home/cpp/" to another user
|
||||
# remove the nft lines to only listen on port 3923
|
||||
@@ -18,6 +22,7 @@
|
||||
# add '-i 127.0.0.1' to only allow local connections
|
||||
# add '-e2dsa' to enable filesystem scanning + indexing
|
||||
# add '-e2ts' to enable metadata indexing
|
||||
# remove '--ansi' to disable colored logs
|
||||
#
|
||||
# with `Type=notify`, copyparty will signal systemd when it is ready to
|
||||
# accept connections; correctly delaying units depending on copyparty.
|
||||
@@ -44,7 +49,7 @@ ExecReload=/bin/kill -s USR1 $MAINPID
|
||||
User=cpp
|
||||
Environment=XDG_CONFIG_HOME=/home/cpp/.config
|
||||
|
||||
# setup forwarding from ports 80 and 443 to port 3923
|
||||
# OPTIONAL: setup forwarding from ports 80 and 443 to port 3923
|
||||
ExecStartPre=+/bin/bash -c 'nft -n -a list table nat | awk "/ to :3923 /{print\$NF}" | xargs -rL1 nft delete rule nat prerouting handle; true'
|
||||
ExecStartPre=+nft add table ip nat
|
||||
ExecStartPre=+nft -- add chain ip nat prerouting { type nat hook prerouting priority -100 \; }
|
||||
@@ -55,7 +60,7 @@ ExecStartPre=+nft add rule ip nat prerouting tcp dport 443 redirect to :3923
|
||||
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
||||
|
||||
# copyparty settings
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -e2d -v /mnt::rw
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py --ansi -e2d -v /mnt::rw
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -6,12 +6,17 @@
|
||||
# 1) put copyparty-sfx.py and prisonparty.sh in /usr/local/bin
|
||||
# 2) cp -pv prisonparty.service /etc/systemd/system && systemctl enable --now prisonparty
|
||||
#
|
||||
# expose additional filesystem locations to copyparty
|
||||
# by listing them between the last `1000` and `--`
|
||||
#
|
||||
# `1000 1000` = what user to run copyparty as
|
||||
#
|
||||
# you may want to:
|
||||
# change '/mnt::rw' to another location or permission-set
|
||||
# (remember to change the '/mnt' chroot arg too)
|
||||
#
|
||||
# enable line-buffering for realtime logging (slight performance cost):
|
||||
# inside the [Service] block, add the following line:
|
||||
# unless you add -q to disable logging, you may want to remove the
|
||||
# following line to allow buffering (slightly better performance):
|
||||
# Environment=PYTHONUNBUFFERED=x
|
||||
|
||||
[Unit]
|
||||
@@ -19,7 +24,14 @@ Description=copyparty file server
|
||||
|
||||
[Service]
|
||||
SyslogIdentifier=prisonparty
|
||||
WorkingDirectory=/usr/local/bin
|
||||
Environment=PYTHONUNBUFFERED=x
|
||||
WorkingDirectory=/var/lib/copyparty-jail
|
||||
ExecReload=/bin/kill -s USR1 $MAINPID
|
||||
|
||||
# stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running
|
||||
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
||||
|
||||
# run copyparty
|
||||
ExecStart=/bin/bash /usr/local/bin/prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt -- \
|
||||
/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ rem removes the 47.6 MiB filesize limit when downloading from webdav
|
||||
rem + optionally allows/enables password-auth over plaintext http
|
||||
rem + optionally helps disable wpad, removing the 10sec latency
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
net session >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo sorry, you must run this as administrator
|
||||
@@ -20,30 +18,26 @@ echo OK;
|
||||
echo allow webdav basic-auth over plaintext http?
|
||||
echo Y: login works, but the password will be visible in wireshark etc
|
||||
echo N: login will NOT work unless you use https and valid certificates
|
||||
set c=.
|
||||
set /p "c=(Y/N): "
|
||||
echo(
|
||||
if /i not "!c!"=="y" goto :g1
|
||||
reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\WebClient\Parameters /v BasicAuthLevel /t REG_DWORD /d 0x2 /f
|
||||
rem default is 1 (require tls)
|
||||
choice
|
||||
if %errorlevel% equ 1 (
|
||||
reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\WebClient\Parameters /v BasicAuthLevel /t REG_DWORD /d 0x2 /f
|
||||
rem default is 1 (require tls)
|
||||
)
|
||||
|
||||
:g1
|
||||
echo(
|
||||
echo OK;
|
||||
echo do you want to disable wpad?
|
||||
echo can give a HUGE speed boost depending on network settings
|
||||
set c=.
|
||||
set /p "c=(Y/N): "
|
||||
echo(
|
||||
if /i not "!c!"=="y" goto :g2
|
||||
echo(
|
||||
echo i'm about to open the [Connections] tab in [Internet Properties] for you;
|
||||
echo please click [LAN settings] and disable [Automatically detect settings]
|
||||
echo(
|
||||
pause
|
||||
control inetcpl.cpl,,4
|
||||
choice
|
||||
if %errorlevel% equ 1 (
|
||||
echo(
|
||||
echo i'm about to open the [Connections] tab in [Internet Properties] for you;
|
||||
echo please click [LAN settings] and disable [Automatically detect settings]
|
||||
echo(
|
||||
pause
|
||||
control inetcpl.cpl,,4
|
||||
)
|
||||
|
||||
:g2
|
||||
net stop webclient
|
||||
net start webclient
|
||||
echo(
|
||||
|
||||
@@ -6,6 +6,10 @@ import platform
|
||||
import sys
|
||||
import time
|
||||
|
||||
# fmt: off
|
||||
_:tuple[int,int]=(0,0) # _____________________________________________________________________ hey there! if you are reading this, your python is too old to run copyparty without some help. Please use https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py or the pypi package instead, or see https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#building if you want to build it yourself :-) ************************************************************************************************************************************************
|
||||
# fmt: on
|
||||
|
||||
try:
|
||||
from typing import TYPE_CHECKING
|
||||
except:
|
||||
@@ -27,13 +31,20 @@ WINDOWS: Any = (
|
||||
else False
|
||||
)
|
||||
|
||||
VT100 = not WINDOWS or WINDOWS >= [10, 0, 14393]
|
||||
VT100 = "--ansi" in sys.argv or (
|
||||
os.environ.get("NO_COLOR", "").lower() in ("", "0", "false")
|
||||
and sys.stdout.isatty()
|
||||
and "--no-ansi" not in sys.argv
|
||||
and (not WINDOWS or WINDOWS >= [10, 0, 14393])
|
||||
)
|
||||
# introduced in anniversary update
|
||||
|
||||
ANYWIN = WINDOWS or sys.platform in ["msys", "cygwin"]
|
||||
|
||||
MACOS = platform.system() == "Darwin"
|
||||
|
||||
EXE = bool(getattr(sys, "frozen", False))
|
||||
|
||||
try:
|
||||
CORES = len(os.sched_getaffinity(0))
|
||||
except:
|
||||
|
||||
@@ -10,11 +10,9 @@ __url__ = "https://github.com/9001/copyparty/"
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import filecmp
|
||||
import locale
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
@@ -23,9 +21,10 @@ import traceback
|
||||
import uuid
|
||||
from textwrap import dedent
|
||||
|
||||
from .__init__ import ANYWIN, CORES, PY2, VT100, WINDOWS, E, EnvParams, unicode
|
||||
from .__init__ import ANYWIN, CORES, EXE, PY2, VT100, WINDOWS, E, EnvParams, unicode
|
||||
from .__version__ import CODENAME, S_BUILD_DT, S_VERSION
|
||||
from .authsrv import expand_config_file, re_vol
|
||||
from .authsrv import expand_config_file, re_vol, split_cfg_ln, upgrade_cfg_fmt
|
||||
from .cfg import flagcats, onedash
|
||||
from .svchub import SvcHub
|
||||
from .util import (
|
||||
IMPLICATIONS,
|
||||
@@ -37,6 +36,7 @@ from .util import (
|
||||
ansi_re,
|
||||
min_ex,
|
||||
py_desc,
|
||||
pybin,
|
||||
termsize,
|
||||
wrap,
|
||||
)
|
||||
@@ -53,8 +53,9 @@ try:
|
||||
except:
|
||||
HAVE_SSL = False
|
||||
|
||||
printed: list[str] = []
|
||||
u = unicode
|
||||
printed: list[str] = []
|
||||
zsid = uuid.uuid4().urn[4:]
|
||||
|
||||
|
||||
class RiceFormatter(argparse.HelpFormatter):
|
||||
@@ -183,7 +184,7 @@ def init_E(E: EnvParams) -> None:
|
||||
|
||||
with open_binary("copyparty", "z.tar") as tgz:
|
||||
with tarfile.open(fileobj=tgz) as tf:
|
||||
tf.extractall(tdn)
|
||||
tf.extractall(tdn) # nosec (archive is safe)
|
||||
|
||||
return tdn
|
||||
|
||||
@@ -198,7 +199,7 @@ def init_E(E: EnvParams) -> None:
|
||||
E.mod = _unpack()
|
||||
|
||||
if sys.platform == "win32":
|
||||
bdir = os.environ.get("APPDATA") or os.environ.get("TEMP")
|
||||
bdir = os.environ.get("APPDATA") or os.environ.get("TEMP") or "."
|
||||
E.cfg = os.path.normpath(bdir + "/copyparty")
|
||||
elif sys.platform == "darwin":
|
||||
E.cfg = os.path.expanduser("~/Library/Preferences/copyparty")
|
||||
@@ -239,6 +240,24 @@ def get_srvname() -> str:
|
||||
return ret
|
||||
|
||||
|
||||
def get_fk_salt(cert_path) -> str:
|
||||
fp = os.path.join(E.cfg, "fk-salt.txt")
|
||||
try:
|
||||
with open(fp, "rb") as f:
|
||||
ret = f.read().strip()
|
||||
except:
|
||||
if os.path.exists(cert_path):
|
||||
print("salt from cert")
|
||||
return unicode(os.path.getmtime(cert_path))
|
||||
else:
|
||||
print("salt from os.random")
|
||||
ret = base64.b64encode(os.urandom(18))
|
||||
with open(fp, "wb") as f:
|
||||
f.write(ret + b"\n")
|
||||
|
||||
return ret.decode("utf-8")
|
||||
|
||||
|
||||
def ensure_locale() -> None:
|
||||
safe = "en_US.UTF-8"
|
||||
for x in [
|
||||
@@ -258,30 +277,22 @@ def ensure_locale() -> None:
|
||||
warn(t.format(safe))
|
||||
|
||||
|
||||
def ensure_cert() -> None:
|
||||
def ensure_webdeps() -> None:
|
||||
ap = os.path.join(E.mod, "web/deps/mini-fa.woff")
|
||||
if os.path.exists(ap):
|
||||
return
|
||||
|
||||
warn(
|
||||
"""could not find webdeps;
|
||||
if you are running the sfx, or exe, or pypi package, or docker image,
|
||||
then this is a bug! Please let me know so I can fix it, thanks :-)
|
||||
https://github.com/9001/copyparty/issues/new?labels=bug&template=bug_report.md
|
||||
|
||||
however, if you are a dev, or running copyparty from source, and you want
|
||||
full client functionality, you will need to build or obtain the webdeps:
|
||||
https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#building
|
||||
"""
|
||||
the default cert (and the entire TLS support) is only here to enable the
|
||||
crypto.subtle javascript API, which is necessary due to the webkit guys
|
||||
being massive memers (https://www.chromium.org/blink/webcrypto)
|
||||
|
||||
i feel awful about this and so should they
|
||||
"""
|
||||
cert_insec = os.path.join(E.mod, "res/insecure.pem")
|
||||
cert_cfg = os.path.join(E.cfg, "cert.pem")
|
||||
if not os.path.exists(cert_cfg):
|
||||
shutil.copy(cert_insec, cert_cfg)
|
||||
|
||||
try:
|
||||
if filecmp.cmp(cert_cfg, cert_insec):
|
||||
lprint(
|
||||
"\033[33musing default TLS certificate; https will be insecure."
|
||||
+ "\033[36m\ncertificate location: {}\033[0m\n".format(cert_cfg)
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
# speaking of the default cert,
|
||||
# printf 'NO\n.\n.\n.\n.\ncopyparty-insecure\n.\n' | faketime '2000-01-01 00:00:00' openssl req -x509 -sha256 -newkey rsa:2048 -keyout insecure.pem -out insecure.pem -days $((($(printf %d 0x7fffffff)-$(date +%s --date=2000-01-01T00:00:00Z))/(60*60*24))) -nodes && ls -al insecure.pem && openssl x509 -in insecure.pem -text -noout
|
||||
)
|
||||
|
||||
|
||||
def configure_ssl_ver(al: argparse.Namespace) -> None:
|
||||
@@ -354,27 +365,28 @@ def configure_ssl_ciphers(al: argparse.Namespace) -> None:
|
||||
def args_from_cfg(cfg_path: str) -> list[str]:
|
||||
lines: list[str] = []
|
||||
expand_config_file(lines, cfg_path, "")
|
||||
lines = upgrade_cfg_fmt(None, argparse.Namespace(vc=False), lines, "")
|
||||
|
||||
ret: list[str] = []
|
||||
skip = False
|
||||
skip = True
|
||||
for ln in lines:
|
||||
if not ln:
|
||||
sn = ln.split(" #")[0].strip()
|
||||
if sn.startswith("["):
|
||||
skip = True
|
||||
if sn.startswith("[global]"):
|
||||
skip = False
|
||||
continue
|
||||
|
||||
if ln.startswith("#"):
|
||||
if skip or not sn.split("#")[0].strip():
|
||||
continue
|
||||
|
||||
if not ln.startswith("-"):
|
||||
continue
|
||||
|
||||
if skip:
|
||||
continue
|
||||
|
||||
try:
|
||||
ret.extend(ln.split(" ", 1))
|
||||
except:
|
||||
ret.append(ln)
|
||||
for k, v in split_cfg_ln(sn).items():
|
||||
k = k.lstrip("-")
|
||||
if not k:
|
||||
continue
|
||||
prefix = "-" if k in onedash else "--"
|
||||
if v is True:
|
||||
ret.append(prefix + k)
|
||||
else:
|
||||
ret.append(prefix + k + "=" + v)
|
||||
|
||||
return ret
|
||||
|
||||
@@ -468,7 +480,7 @@ def get_sects():
|
||||
"g" (get): download files, but cannot see folder contents
|
||||
"G" (upget): "get", but can see filekeys of their own uploads
|
||||
|
||||
too many volflags to list here, see the other sections
|
||||
too many volflags to list here, see --help-flags
|
||||
|
||||
example:\033[35m
|
||||
-a ed:hunter2 -v .::r:rw,ed -v ../inc:dump:w:rw,ed:c,nodupe \033[36m
|
||||
@@ -495,69 +507,13 @@ def get_sects():
|
||||
"""
|
||||
volflags are appended to volume definitions, for example,
|
||||
to create a write-only volume with the \033[33mnodupe\033[0m and \033[32mnosub\033[0m flags:
|
||||
\033[35m-v /mnt/inc:/inc:w\033[33m:c,nodupe\033[32m:c,nosub
|
||||
\033[35m-v /mnt/inc:/inc:w\033[33m:c,nodupe\033[32m:c,nosub\033[0m
|
||||
|
||||
\033[0muploads, general:
|
||||
\033[36mnodupe\033[35m rejects existing files (instead of symlinking them)
|
||||
\033[36mhardlink\033[35m does dedup with hardlinks instead of symlinks
|
||||
\033[36mneversymlink\033[35m disables symlink fallback; full copy instead
|
||||
\033[36mcopydupes\033[35m disables dedup, always saves full copies of dupes
|
||||
\033[36mnosub\033[35m forces all uploads into the top folder of the vfs
|
||||
\033[36mmagic$\033[35m enables filetype detection for nameless uploads
|
||||
\033[36mgz\033[35m allows server-side gzip of uploads with ?gz (also c,xz)
|
||||
\033[36mpk\033[35m forces server-side compression, optional arg: xz,9
|
||||
|
||||
\033[0mupload rules:
|
||||
\033[36mmaxn=250,600\033[35m max 250 uploads over 15min
|
||||
\033[36mmaxb=1g,300\033[35m max 1 GiB over 5min (suffixes: b, k, m, g)
|
||||
\033[36msz=1k-3m\033[35m allow filesizes between 1 KiB and 3MiB
|
||||
\033[36mdf=1g\033[35m ensure 1 GiB free disk space
|
||||
|
||||
\033[0mupload rotation:
|
||||
(moves all uploads into the specified folder structure)
|
||||
\033[36mrotn=100,3\033[35m 3 levels of subfolders with 100 entries in each
|
||||
\033[36mrotf=%Y-%m/%d-%H\033[35m date-formatted organizing
|
||||
\033[36mlifetime=3600\033[35m uploads are deleted after 1 hour
|
||||
|
||||
\033[0mdatabase, general:
|
||||
\033[36me2d\033[35m sets -e2d (all -e2* args can be set using ce2* volflags)
|
||||
\033[36md2ts\033[35m disables metadata collection for existing files
|
||||
\033[36md2ds\033[35m disables onboot indexing, overrides -e2ds*
|
||||
\033[36md2t\033[35m disables metadata collection, overrides -e2t*
|
||||
\033[36md2v\033[35m disables file verification, overrides -e2v*
|
||||
\033[36md2d\033[35m disables all database stuff, overrides -e2*
|
||||
\033[36mhist=/tmp/cdb\033[35m puts thumbnails and indexes at that location
|
||||
\033[36mscan=60\033[35m scan for new files every 60sec, same as --re-maxage
|
||||
\033[36mnohash=\\.iso$\033[35m skips hashing file contents if path matches *.iso
|
||||
\033[36mnoidx=\\.iso$\033[35m fully ignores the contents at paths matching *.iso
|
||||
\033[36mnoforget$\033[35m don't forget files when deleted from disk
|
||||
\033[36mdbd=[acid|swal|wal|yolo]\033[35m database speed-durability tradeoff
|
||||
\033[36mxlink$\033[35m cross-volume dupe detection / linking
|
||||
\033[36mxdev\033[35m do not descend into other filesystems
|
||||
\033[36mxvol\033[35m skip symlinks leaving the volume root
|
||||
|
||||
\033[0mdatabase, audio tags:
|
||||
"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...
|
||||
\033[36mmtp=.bpm=f,audio-bpm.py\033[35m uses the "audio-bpm.py" program to
|
||||
generate ".bpm" tags from uploads (f = overwrite tags)
|
||||
\033[36mmtp=ahash,vhash=media-hash.py\033[35m collects two tags at once
|
||||
|
||||
\033[0mthumbnails:
|
||||
\033[36mdthumb\033[35m disables all thumbnails
|
||||
\033[36mdvthumb\033[35m disables video thumbnails
|
||||
\033[36mdathumb\033[35m disables audio thumbnails (spectrograms)
|
||||
\033[36mdithumb\033[35m disables image thumbnails
|
||||
|
||||
\033[0mclient and ux:
|
||||
\033[36mhtml_head=TXT\033[35m includes TXT in the <head>
|
||||
\033[36mrobots\033[35m allows indexing by search engines (default)
|
||||
\033[36mnorobots\033[35m kindly asks search engines to leave
|
||||
|
||||
\033[0mothers:
|
||||
\033[36mfk=8\033[35m generates per-file accesskeys,
|
||||
which will then be required at the "g" permission
|
||||
\033[0m"""
|
||||
),
|
||||
if global config defines a volflag for all volumes,
|
||||
you can unset it for a specific volume with -flag
|
||||
"""
|
||||
).rstrip()
|
||||
+ build_flags_desc(),
|
||||
],
|
||||
[
|
||||
"hooks",
|
||||
@@ -567,6 +523,7 @@ def get_sects():
|
||||
execute a command (a program or script) before or after various events;
|
||||
\033[36mxbu\033[35m executes CMD before a file upload starts
|
||||
\033[36mxau\033[35m executes CMD after a file upload finishes
|
||||
\033[36mxiu\033[35m executes CMD after all uploads finish and volume is idle
|
||||
\033[36mxbr\033[35m executes CMD before a file rename/move
|
||||
\033[36mxar\033[35m executes CMD after a file rename/move
|
||||
\033[36mxbd\033[35m executes CMD before a file delete
|
||||
@@ -588,6 +545,7 @@ def get_sects():
|
||||
\033[36mj\033[35m provides json with info as 1st arg instead of filepath
|
||||
\033[36mwN\033[35m waits N sec after command has been started before continuing
|
||||
\033[36mtN\033[35m sets an N sec timeout before the command is abandoned
|
||||
\033[36miN\033[35m xiu only: volume must be idle for N sec (default = 5)
|
||||
|
||||
\033[36mkt\033[35m kills the entire process tree on timeout (default),
|
||||
\033[36mkm\033[35m kills just the main process
|
||||
@@ -598,6 +556,14 @@ def get_sects():
|
||||
\033[36mc2\033[35m show only stdout
|
||||
\033[36mc3\033[35m mute all process otput
|
||||
\033[0m
|
||||
each hook is executed once for each event, except for \033[36mxiu\033[0m
|
||||
which builds up a backlog of uploads, running the hook just once
|
||||
as soon as the volume has been idle for iN seconds (5 by default)
|
||||
|
||||
\033[36mxiu\033[0m is also unique in that it will pass the metadata to the
|
||||
executed program on STDIN instead of as argv arguments, and
|
||||
it also includes the wark (file-id/hash) as a json property
|
||||
|
||||
except for \033[36mxm\033[0m, only one hook / one action can run at a time,
|
||||
so it's recommended to use the \033[36mf\033[0m flag unless you really need
|
||||
to wait for the hook to finish before continuing (without \033[36mf\033[0m
|
||||
@@ -646,9 +612,9 @@ def get_sects():
|
||||
|
||||
\033[32macid\033[0m = extremely safe but slow; the old default. Should never lose any data no matter what
|
||||
|
||||
\033[32mswal\033[0m = 2.4x faster uploads yet 99.9%% as safe -- theoretical chance of losing metadata for the ~200 most recently uploaded files if there's a power-loss or your OS crashes
|
||||
\033[32mswal\033[0m = 2.4x faster uploads yet 99.9% as safe -- theoretical chance of losing metadata for the ~200 most recently uploaded files if there's a power-loss or your OS crashes
|
||||
|
||||
\033[32mwal\033[0m = another 21x faster on HDDs yet 90%% as safe; same pitfall as \033[33mswal\033[0m except more likely
|
||||
\033[32mwal\033[0m = another 21x faster on HDDs yet 90% as safe; same pitfall as \033[33mswal\033[0m except more likely
|
||||
|
||||
\033[32myolo\033[0m = another 1.5x faster, and removes the occasional sudden upload-pause while the disk syncs, but now you're at risk of losing the entire database in a powerloss / OS-crash
|
||||
|
||||
@@ -659,6 +625,17 @@ def get_sects():
|
||||
]
|
||||
|
||||
|
||||
def build_flags_desc():
|
||||
ret = ""
|
||||
for grp, flags in flagcats.items():
|
||||
ret += "\n\n\033[0m" + grp
|
||||
for k, v in flags.items():
|
||||
v = v.replace("\n", "\n ")
|
||||
ret += "\n \033[36m{}\033[35m {}".format(k, v)
|
||||
|
||||
return ret + "\033[0m"
|
||||
|
||||
|
||||
# fmt: off
|
||||
|
||||
|
||||
@@ -696,6 +673,7 @@ def add_upload(ap):
|
||||
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless -ed")
|
||||
ap2.add_argument("--plain-ip", action="store_true", help="when avoiding filename collisions by appending the uploader's ip to the filename: append the plaintext ip instead of salting and hashing the ip")
|
||||
ap2.add_argument("--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")
|
||||
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 SEC seconds ago)")
|
||||
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without -e2d; roughly 1 MiB RAM per 600")
|
||||
ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (very slow on windows)")
|
||||
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, ...)")
|
||||
@@ -704,6 +682,8 @@ def add_upload(ap):
|
||||
ap2.add_argument("--no-dedup", action="store_true", help="disable symlink/hardlink creation; copy file contents instead (volflag=copydupes")
|
||||
ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)")
|
||||
ap2.add_argument("--no-snap", action="store_true", help="disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually")
|
||||
ap2.add_argument("--rand", action="store_true", help="force randomized filenames, --nrand chars long (volflag=rand)")
|
||||
ap2.add_argument("--nrand", metavar="NUM", type=int, default=9, help="randomized filenames length (volflag=nrand)")
|
||||
ap2.add_argument("--magic", action="store_true", help="enable filetype detection on nameless uploads (volflag=magic)")
|
||||
ap2.add_argument("--df", metavar="GiB", type=float, default=0, help="ensure GiB free disk space by rejecting upload requests")
|
||||
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")
|
||||
@@ -723,21 +703,43 @@ def add_network(ap):
|
||||
ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances")
|
||||
else:
|
||||
ap2.add_argument("--freebind", action="store_true", help="allow listening on IPs which do not yet exist, for example if the network interfaces haven't finished going up. Only makes sense for IPs other than '0.0.0.0', '127.0.0.1', '::', and '::1'. May require running as root (unless net.ipv6.ip_nonlocal_bind)")
|
||||
ap2.add_argument("--s-thead", metavar="SEC", type=int, default=120, help="socket timeout (read request header)")
|
||||
ap2.add_argument("--s-tbody", metavar="SEC", type=float, default=186, help="socket timeout (read/write request/response bodies). Use 60 on fast servers (default is extremely safe). Disable with 0 if reverse-proxied for a 2%% speed boost")
|
||||
ap2.add_argument("--s-wr-sz", metavar="B", type=int, default=256*1024, help="socket write size in bytes")
|
||||
ap2.add_argument("--s-wr-slp", metavar="SEC", type=float, default=0, help="debug: socket write delay in seconds")
|
||||
ap2.add_argument("--rsp-slp", metavar="SEC", type=float, default=0, help="debug: response delay in seconds")
|
||||
ap2.add_argument("--rsp-jtr", metavar="SEC", type=float, default=0, help="debug: response delay, random duration 0..SEC")
|
||||
|
||||
|
||||
def add_tls(ap):
|
||||
def add_tls(ap, cert_path):
|
||||
ap2 = ap.add_argument_group('SSL/TLS options')
|
||||
ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls -- force plaintext")
|
||||
ap2.add_argument("--https-only", action="store_true", help="disable plaintext -- force tls")
|
||||
ap2.add_argument("--cert", metavar="PATH", type=u, default=cert_path, help="path to TLS certificate")
|
||||
ap2.add_argument("--ssl-ver", metavar="LIST", type=u, help="set allowed ssl/tls versions; [\033[32mhelp\033[0m] shows available versions; default is what your python version considers safe")
|
||||
ap2.add_argument("--ciphers", metavar="LIST", type=u, help="set allowed ssl/tls ciphers; [\033[32mhelp\033[0m] shows available ciphers")
|
||||
ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
|
||||
ap2.add_argument("--ssl-log", metavar="PATH", type=u, help="log master secrets for later decryption in wireshark")
|
||||
|
||||
|
||||
def add_cert(ap, cert_path):
|
||||
cert_dir = os.path.dirname(cert_path)
|
||||
ap2 = ap.add_argument_group('TLS certificate generator options')
|
||||
ap2.add_argument("--no-crt", action="store_true", help="disable automatic certificate creation")
|
||||
ap2.add_argument("--crt-ns", metavar="N,N", type=u, default="", help="comma-separated list of FQDNs (domains) to add into the certificate")
|
||||
ap2.add_argument("--crt-exact", action="store_true", help="do not add wildcard entries for each --crt-ns")
|
||||
ap2.add_argument("--crt-noip", action="store_true", help="do not add autodetected IP addresses into cert")
|
||||
ap2.add_argument("--crt-nolo", action="store_true", help="do not add 127.0.0.1 / localhost into cert")
|
||||
ap2.add_argument("--crt-dir", metavar="PATH", default=cert_dir, help="where to save the CA cert")
|
||||
ap2.add_argument("--crt-cdays", metavar="D", type=float, default=3650, help="ca-certificate expiration time in days")
|
||||
ap2.add_argument("--crt-sdays", metavar="D", type=float, default=365, help="server-cert expiration time in days")
|
||||
ap2.add_argument("--crt-cn", metavar="TXT", type=u, default="partyco", help="CA/server-cert common-name")
|
||||
ap2.add_argument("--crt-cnc", metavar="TXT", type=u, default="--crt-cn", help="override CA name")
|
||||
ap2.add_argument("--crt-cns", metavar="TXT", type=u, default="--crt-cn cpp", help="override server-cert name")
|
||||
ap2.add_argument("--crt-back", metavar="HRS", type=float, default=72, help="backdate in hours")
|
||||
ap2.add_argument("--crt-alg", metavar="S-N", type=u, default="ecdsa-256", help="algorithm and keysize; one of these: ecdsa-256 rsa-4096 rsa-2048")
|
||||
|
||||
|
||||
def add_zeroconf(ap):
|
||||
ap2 = ap.add_argument_group("Zeroconf options")
|
||||
ap2.add_argument("-z", action="store_true", help="enable all zeroconf backends (mdns, ssdp)")
|
||||
@@ -774,7 +776,7 @@ def add_zc_ssdp(ap):
|
||||
ap2.add_argument("--zs-off", metavar="NETS", type=u, default="", help="disable zeroconf on the comma-separated list of subnets and/or interface names/indexes")
|
||||
ap2.add_argument("--zsv", action="store_true", help="verbose SSDP")
|
||||
ap2.add_argument("--zsl", metavar="PATH", type=u, default="/?hc", help="location to include in the url (or a complete external URL), for example [\033[32mpriv/?pw=hunter2\033[0m] (goes directly to /priv/ with password hunter2) or [\033[32m?hc=priv&pw=hunter2\033[0m] (shows mounting options for /priv/ with password)")
|
||||
ap2.add_argument("--zsid", metavar="UUID", type=u, default=uuid.uuid4().urn[4:], help="USN (device identifier) to announce")
|
||||
ap2.add_argument("--zsid", metavar="UUID", type=u, default=zsid, help="USN (device identifier) to announce")
|
||||
|
||||
|
||||
def add_ftp(ap):
|
||||
@@ -782,6 +784,7 @@ def add_ftp(ap):
|
||||
ap2.add_argument("--ftp", metavar="PORT", type=int, help="enable FTP server on PORT, for example \033[32m3921")
|
||||
ap2.add_argument("--ftps", metavar="PORT", type=int, help="enable FTPS server on PORT, for example \033[32m3990")
|
||||
ap2.add_argument("--ftpv", action="store_true", help="verbose")
|
||||
ap2.add_argument("--ftp4", action="store_true", help="only listen on IPv4")
|
||||
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 SEC seconds ago)")
|
||||
ap2.add_argument("--ftp-nat", metavar="ADDR", type=u, help="the NAT address to use for passive connections")
|
||||
ap2.add_argument("--ftp-pr", metavar="P-P", type=u, help="the range of TCP ports to use for passive connections, for example \033[32m12000-13000")
|
||||
@@ -789,9 +792,11 @@ def add_ftp(ap):
|
||||
|
||||
def add_webdav(ap):
|
||||
ap2 = ap.add_argument_group('WebDAV options')
|
||||
ap2.add_argument("--daw", action="store_true", help="enable full write support. \033[1;31mWARNING:\033[0m This has side-effects -- PUT-operations will now \033[1;31mOVERWRITE\033[0m existing files, rather than inventing new filenames to avoid loss of data. You might want to instead set this as a volflag where needed. By not setting this flag, uploaded files can get written to a filename which the client does not expect (which might be okay, depending on client)")
|
||||
ap2.add_argument("--daw", action="store_true", help="enable full write support, even if client may not be webdav. \033[1;31mWARNING:\033[0m This has side-effects -- PUT-operations will now \033[1;31mOVERWRITE\033[0m existing files, rather than inventing new filenames to avoid loss of data. You might want to instead set this as a volflag where needed. By not setting this flag, uploaded files can get written to a filename which the client does not expect (which might be okay, depending on client)")
|
||||
ap2.add_argument("--dav-inf", action="store_true", help="allow depth:infinite requests (recursive file listing); extremely server-heavy but required for spec compliance -- luckily few clients rely on this")
|
||||
ap2.add_argument("--dav-mac", action="store_true", help="disable apple-garbage filter -- allow macos to create junk files (._* and .DS_Store, .Spotlight-*, .fseventsd, .Trashes, .AppleDouble, __MACOS)")
|
||||
ap2.add_argument("--dav-rt", action="store_true", help="show symlink-destination's lastmodified instead of the link itself; always enabled for recursive listings (volflag=davrt)")
|
||||
ap2.add_argument("--dav-auth", action="store_true", help="force auth for all folders (required by davfs2 when only some folders are world-readable) (volflag=davauth)")
|
||||
|
||||
|
||||
def add_smb(ap):
|
||||
@@ -808,9 +813,10 @@ def add_smb(ap):
|
||||
|
||||
|
||||
def add_hooks(ap):
|
||||
ap2 = ap.add_argument_group('hooks (see --help-hooks)')
|
||||
ap2 = ap.add_argument_group('event hooks (see --help-hooks)')
|
||||
ap2.add_argument("--xbu", metavar="CMD", type=u, action="append", help="execute CMD before a file upload starts")
|
||||
ap2.add_argument("--xau", metavar="CMD", type=u, action="append", help="execute CMD after a file upload finishes")
|
||||
ap2.add_argument("--xiu", metavar="CMD", type=u, action="append", help="execute CMD after all uploads finish and volume is idle")
|
||||
ap2.add_argument("--xbr", metavar="CMD", type=u, action="append", help="execute CMD before a file move/rename")
|
||||
ap2.add_argument("--xar", metavar="CMD", type=u, action="append", help="execute CMD after a file move/rename")
|
||||
ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="execute CMD before a file delete")
|
||||
@@ -844,7 +850,9 @@ def add_safety(ap, fk_salt):
|
||||
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --vague-403 --ban-404=50,60,1440 -nih")
|
||||
ap2.add_argument("-sss", action="store_true", help="further increase safety: Enable logging to disk, scan for dangerous symlinks.\n └─Alias of\033[32m -ss --no-dav --no-logues --no-readme -lo=cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz --ls=**,*,ln,p,r")
|
||||
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m; example [\033[32m**,*,ln,p,r\033[0m]")
|
||||
ap2.add_argument("--salt", type=u, default="hunter2", help="up2k file-hash salt; used to generate unpredictable internal identifiers for uploads -- doesn't really matter")
|
||||
ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)")
|
||||
ap2.add_argument("--xdev", action="store_true", help="stay within the filesystem of the volume root; do not descend into other devices (symlink or bind-mount to another HDD, ...) (volflag=xdev)")
|
||||
ap2.add_argument("--salt", type=u, default="hunter2", help="up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)")
|
||||
ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files -- this one DOES matter")
|
||||
ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles")
|
||||
ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to make something a dotfile")
|
||||
@@ -866,13 +874,15 @@ def add_shutdown(ap):
|
||||
ap2 = ap.add_argument_group('shutdown options')
|
||||
ap2.add_argument("--ign-ebind", action="store_true", help="continue running even if it's impossible to listen on some of the requested endpoints")
|
||||
ap2.add_argument("--ign-ebind-all", action="store_true", help="continue running even if it's impossible to receive connections at all")
|
||||
ap2.add_argument("--exit", metavar="WHEN", type=u, default="", help="shutdown after WHEN has finished; for example [\033[32midx\033[0m] will do volume indexing + metadata analysis")
|
||||
ap2.add_argument("--exit", metavar="WHEN", type=u, default="", help="shutdown after WHEN has finished; [\033[32mcfg\033[0m] config parsing, [\033[32midx\033[0m] volscan + multimedia indexing")
|
||||
|
||||
|
||||
def add_logging(ap):
|
||||
ap2 = ap.add_argument_group('logging options')
|
||||
ap2.add_argument("-q", action="store_true", help="quiet")
|
||||
ap2.add_argument("-lo", metavar="PATH", type=u, help="logfile, example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz")
|
||||
ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR")
|
||||
ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
|
||||
ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup")
|
||||
ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
|
||||
ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
|
||||
@@ -904,15 +914,15 @@ def add_thumbnail(ap):
|
||||
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 SEC seconds")
|
||||
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled")
|
||||
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than --th-poke seconds will get deleted every --th-clean seconds")
|
||||
ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for")
|
||||
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 -e2d will make these case-insensitive, and also automatically select thumbnails for all folders that contain pics, even if none match this pattern")
|
||||
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
||||
# https://github.com/libvips/libvips
|
||||
# ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
|
||||
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="bmp,dib,gif,icns,ico,jpg,jpeg,jp2,jpx,pcx,png,pbm,pgm,ppm,pnm,sgi,tga,tif,tiff,webp,xbm,dds,xpm,heif,heifs,heic,heics,avif,avifs", help="image formats to decode using pillow")
|
||||
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="jpg,jpeg,jp2,jpx,jxl,tif,tiff,png,webp,heic,avif,fit,fits,fts,exr,svg,hdr,ppm,pgm,pfm,gif,nii", help="image formats to decode using pyvips")
|
||||
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,dds,dib,fit,fits,fts,gif,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="av1,asf,avi,flv,m4v,mkv,mjpeg,mjpg,mpg,mpeg,mpg2,mpeg2,h264,avc,mts,h265,hevc,mov,3gp,mp4,ts,mpegts,nut,ogv,ogm,rm,vob,webm,wmv", help="video formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,m4a,ogg,opus,flac,alac,mp3,mp2,ac3,dts,wma,ra,wav,aif,aiff,au,alaw,ulaw,mulaw,amr,gsm,ape,tak,tta,wv,mpc", help="audio formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,dcx,dds,dib,emf,eps,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
|
||||
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips")
|
||||
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,dds,dib,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,m4a,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,ogg,okt,opus,ra,s3m,tak,tta,ulaw,wav,wma,wv,xm,xpk", help="audio formats to decode using ffmpeg")
|
||||
|
||||
|
||||
def add_transcoding(ap):
|
||||
@@ -923,27 +933,26 @@ def add_transcoding(ap):
|
||||
|
||||
def add_db_general(ap, hcores):
|
||||
ap2 = ap.add_argument_group('general db options')
|
||||
ap2.add_argument("-e2d", action="store_true", help="enable up2k database, making files searchable + enables upload deduplocation")
|
||||
ap2.add_argument("-e2d", action="store_true", help="enable up2k database, making files searchable + enables upload deduplication")
|
||||
ap2.add_argument("-e2ds", action="store_true", help="scan writable folders for new files on startup; sets -e2d")
|
||||
ap2.add_argument("-e2dsa", action="store_true", help="scans all folders on startup; sets -e2ds")
|
||||
ap2.add_argument("-e2v", action="store_true", help="verify file integrity; rehash all files and compare with db")
|
||||
ap2.add_argument("-e2vu", action="store_true", help="on hash mismatch: update the database with the new hash")
|
||||
ap2.add_argument("-e2vp", action="store_true", help="on hash mismatch: panic and quit copyparty")
|
||||
ap2.add_argument("--hist", metavar="PATH", type=u, help="where to store volume data (db, thumbs) (volflag=hist)")
|
||||
ap2.add_argument("--no-hash", metavar="PTN", type=u, help="regex: disable hashing of matching paths during e2ds folder scans (volflag=nohash)")
|
||||
ap2.add_argument("--no-idx", metavar="PTN", type=u, help="regex: disable indexing of matching paths during e2ds folder scans (volflag=noidx)")
|
||||
ap2.add_argument("--no-hash", metavar="PTN", type=u, help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (volflag=nohash)")
|
||||
ap2.add_argument("--no-idx", metavar="PTN", type=u, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scans (volflag=noidx)")
|
||||
ap2.add_argument("--no-dhash", action="store_true", help="disable rescan acceleration; do full database integrity check -- makes the db ~5%% smaller and bootup/rescans 3~10x slower")
|
||||
ap2.add_argument("--re-dhash", action="store_true", help="rebuild the cache if it gets out of sync (for example crash on startup during metadata scanning)")
|
||||
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 (volflag=noforget)")
|
||||
ap2.add_argument("--dbd", metavar="PROFILE", default="wal", help="database durability profile; sets the tradeoff between robustness and speed, see --help-dbd (volflag=dbd)")
|
||||
ap2.add_argument("--xlink", action="store_true", help="on upload: check all volumes for dupes, not just the target volume (volflag=xlink)")
|
||||
ap2.add_argument("--xdev", action="store_true", help="do not descend into other filesystems (symlink or bind-mount to another HDD, ...) (volflag=xdev)")
|
||||
ap2.add_argument("--xvol", action="store_true", help="skip symlinks leaving the volume root (volflag=xvol)")
|
||||
ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing")
|
||||
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="disk rescan volume interval, 0=off (volflag=scan)")
|
||||
ap2.add_argument("--db-act", metavar="SEC", type=float, default=10, help="defer any scheduled volume reindexing until SEC seconds after last db write (uploads, renames, ...)")
|
||||
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=45, help="search deadline -- terminate searches running for more than SEC seconds")
|
||||
ap2.add_argument("--srch-hits", metavar="N", type=int, default=7999, help="max search results to allow clients to fetch; 125 results will be shown initially")
|
||||
ap2.add_argument("--dotsrch", action="store_true", help="show dotfiles in search results (volflags: dotsrch | nodotsrch)")
|
||||
|
||||
|
||||
def add_db_metadata(ap):
|
||||
@@ -967,26 +976,32 @@ def add_db_metadata(ap):
|
||||
|
||||
def add_ui(ap, retry):
|
||||
ap2 = ap.add_argument_group('ui options')
|
||||
ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
|
||||
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language")
|
||||
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use")
|
||||
ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
|
||||
ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files matching REGEX in file list. Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\.(js|css)$\033[0m] (volflag=unlist)")
|
||||
ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable")
|
||||
ap2.add_argument("--mpmc", metavar="URL", type=u, default="", help="change the mediaplayer-toggle mouse cursor; URL to a folder with {2..5}.png inside (or disable with [\033[32m.\033[0m])")
|
||||
ap2.add_argument("--js-browser", metavar="L", type=u, help="URL to additional JS to include")
|
||||
ap2.add_argument("--css-browser", metavar="L", type=u, help="URL to additional CSS to include")
|
||||
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages")
|
||||
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)")
|
||||
ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext")
|
||||
ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
|
||||
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty", help="title / service-name to show in html documents")
|
||||
ap2.add_argument("--pb-url", metavar="URL", type=u, default="https://github.com/9001/copyparty", help="powered-by link; disable with -np")
|
||||
ap2.add_argument("--md-sbf", metavar="FLAGS", type=u, default="downloads forms modals popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for README.md docs (volflag=md_sbf); see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox")
|
||||
ap2.add_argument("--lg-sbf", metavar="FLAGS", type=u, default="downloads forms modals popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for prologue/epilogue docs (volflag=lg_sbf)")
|
||||
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible by -np)")
|
||||
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 for README.md docs (volflag=md_sbf); see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox")
|
||||
ap2.add_argument("--lg-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for prologue/epilogue docs (volflag=lg_sbf)")
|
||||
ap2.add_argument("--no-sb-md", action="store_true", help="don't sandbox README.md documents (volflags: no_sb_md | sb_md)")
|
||||
ap2.add_argument("--no-sb-lg", action="store_true", help="don't sandbox prologue/epilogue docs (volflags: no_sb_lg | sb_lg); enables non-js support")
|
||||
|
||||
|
||||
def add_debug(ap):
|
||||
ap2 = ap.add_argument_group('debug options')
|
||||
ap2.add_argument("--vc", action="store_true", help="verbose config file parser (explain config)")
|
||||
ap2.add_argument("--cgen", action="store_true", help="generate config file from current config (best-effort; probably buggy)")
|
||||
ap2.add_argument("--no-sendfile", action="store_true", help="disable sendfile; instead using a traditional file read loop")
|
||||
ap2.add_argument("--no-scandir", action="store_true", help="disable scandir; instead using listdir + stat on each file")
|
||||
ap2.add_argument("--no-fastboot", action="store_true", help="wait for up2k indexing before starting the httpd")
|
||||
@@ -1012,10 +1027,9 @@ def run_argparse(
|
||||
description="http file sharing hub v{} ({})".format(S_VERSION, S_BUILD_DT),
|
||||
)
|
||||
|
||||
try:
|
||||
fk_salt = unicode(os.path.getmtime(os.path.join(E.cfg, "cert.pem")))
|
||||
except:
|
||||
fk_salt = "hunter2"
|
||||
cert_path = os.path.join(E.cfg, "cert.pem")
|
||||
|
||||
fk_salt = get_fk_salt(cert_path)
|
||||
|
||||
hcores = min(CORES, 4) # optimal on py3.11 @ r5-4500U
|
||||
|
||||
@@ -1025,7 +1039,8 @@ def run_argparse(
|
||||
|
||||
add_general(ap, nc, srvname)
|
||||
add_network(ap)
|
||||
add_tls(ap)
|
||||
add_tls(ap, cert_path)
|
||||
add_cert(ap, cert_path)
|
||||
add_qr(ap, tty)
|
||||
add_zeroconf(ap)
|
||||
add_zc_mdns(ap)
|
||||
@@ -1105,9 +1120,12 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
showlic()
|
||||
sys.exit(0)
|
||||
|
||||
if EXE:
|
||||
print("pybin: {}\n".format(pybin), end="")
|
||||
|
||||
ensure_locale()
|
||||
if HAVE_SSL:
|
||||
ensure_cert()
|
||||
|
||||
ensure_webdeps()
|
||||
|
||||
for k, v in zip(argv[1:], argv[2:]):
|
||||
if k == "-c" and os.path.isfile(v):
|
||||
@@ -1137,7 +1155,8 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
if da:
|
||||
argv.extend(["--qr"])
|
||||
if ANYWIN or not os.geteuid():
|
||||
argv.extend(["-p80,443,3923", "--ign-ebind"])
|
||||
# win10 allows symlinks if admin; can be unexpected
|
||||
argv.extend(["-p80,443,3923", "--ign-ebind", "--no-dedup"])
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -1159,6 +1178,7 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]:
|
||||
try:
|
||||
al = run_argparse(argv, fmtr, retry, nc)
|
||||
dal = run_argparse([], fmtr, retry, nc)
|
||||
break
|
||||
except SystemExit:
|
||||
raise
|
||||
@@ -1168,17 +1188,23 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
|
||||
try:
|
||||
assert al # type: ignore
|
||||
assert dal # type: ignore
|
||||
al.E = E # __init__ is not shared when oxidized
|
||||
except:
|
||||
sys.exit(1)
|
||||
|
||||
if al.ansi:
|
||||
al.no_ansi = False
|
||||
elif not al.no_ansi:
|
||||
al.ansi = VT100
|
||||
|
||||
if WINDOWS and not al.keep_qem:
|
||||
try:
|
||||
disable_quickedit()
|
||||
except:
|
||||
lprint("\nfailed to disable quick-edit-mode:\n" + min_ex() + "\n")
|
||||
|
||||
if not VT100:
|
||||
if al.ansi:
|
||||
al.wintitle = ""
|
||||
|
||||
nstrs: list[str] = []
|
||||
@@ -1257,6 +1283,7 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
configure_ssl_ciphers(al)
|
||||
else:
|
||||
warn("ssl module does not exist; cannot enable https")
|
||||
al.http_only = True
|
||||
|
||||
if PY2 and WINDOWS and al.e2d:
|
||||
warn(
|
||||
@@ -1273,7 +1300,7 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
|
||||
# signal.signal(signal.SIGINT, sighandler)
|
||||
|
||||
SvcHub(al, argv, "".join(printed)).run()
|
||||
SvcHub(al, dal, argv, "".join(printed)).run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 6, 2)
|
||||
CODENAME = "cors k"
|
||||
BUILD_DT = (2023, 1, 29)
|
||||
VERSION = (1, 7, 4)
|
||||
CODENAME = "unlinked"
|
||||
BUILD_DT = (2023, 6, 11)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
@@ -14,6 +14,7 @@ from datetime import datetime
|
||||
|
||||
from .__init__ import ANYWIN, TYPE_CHECKING, WINDOWS
|
||||
from .bos import bos
|
||||
from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
|
||||
from .util import (
|
||||
IMPLICATIONS,
|
||||
META_NOBOTS,
|
||||
@@ -21,7 +22,7 @@ from .util import (
|
||||
UNPLICATIONS,
|
||||
Pebkac,
|
||||
absreal,
|
||||
fsenc,
|
||||
afsenc,
|
||||
get_df,
|
||||
humansize,
|
||||
relchk,
|
||||
@@ -36,7 +37,7 @@ if True: # pylint: disable=using-constant-test
|
||||
|
||||
from typing import Any, Generator, Optional, Union
|
||||
|
||||
from .util import RootLogger
|
||||
from .util import NamedLogger, RootLogger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
@@ -66,9 +67,9 @@ class AXS(object):
|
||||
self.upget: set[str] = set(upget or [])
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "AXS({})".format(
|
||||
return "AXS(%s)" % (
|
||||
", ".join(
|
||||
"{}={!r}".format(k, self.__dict__[k])
|
||||
"%s=%r" % (k, self.__dict__[k])
|
||||
for k in "uread uwrite umove udel uget upget".split()
|
||||
)
|
||||
)
|
||||
@@ -284,6 +285,8 @@ class VFS(object):
|
||||
self.vpath = vpath # absolute path in the virtual filesystem
|
||||
self.axs = axs
|
||||
self.flags = flags # config options
|
||||
self.root = self
|
||||
self.dev = 0 # st_dev
|
||||
self.nodes: dict[str, VFS] = {} # child nodes
|
||||
self.histtab: dict[str, str] = {} # all realpath->histpath
|
||||
self.dbv: Optional[VFS] = None # closest full/non-jump parent
|
||||
@@ -296,26 +299,42 @@ class VFS(object):
|
||||
self.apget: dict[str, list[str]] = {}
|
||||
|
||||
if realpath:
|
||||
rp = realpath + ("" if realpath.endswith(os.sep) else os.sep)
|
||||
vp = vpath + ("/" if vpath else "")
|
||||
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
|
||||
self.all_vols = {vpath: self} # flattened recursive
|
||||
self.all_aps = [(rp, self)]
|
||||
self.all_vps = [(vp, self)]
|
||||
else:
|
||||
self.histpath = ""
|
||||
self.all_vols = {}
|
||||
self.all_aps = []
|
||||
self.all_vps = []
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "VFS({})".format(
|
||||
return "VFS(%s)" % (
|
||||
", ".join(
|
||||
"{}={!r}".format(k, self.__dict__[k])
|
||||
"%s=%r" % (k, self.__dict__[k])
|
||||
for k in "realpath vpath axs flags".split()
|
||||
)
|
||||
)
|
||||
|
||||
def get_all_vols(self, outdict: dict[str, "VFS"]) -> None:
|
||||
def get_all_vols(
|
||||
self,
|
||||
vols: dict[str, "VFS"],
|
||||
aps: list[tuple[str, "VFS"]],
|
||||
vps: list[tuple[str, "VFS"]],
|
||||
) -> None:
|
||||
if self.realpath:
|
||||
outdict[self.vpath] = self
|
||||
vols[self.vpath] = self
|
||||
rp = self.realpath
|
||||
rp += "" if rp.endswith(os.sep) else os.sep
|
||||
vp = self.vpath + ("/" if self.vpath else "")
|
||||
aps.append((rp, self))
|
||||
vps.append((vp, self))
|
||||
|
||||
for v in self.nodes.values():
|
||||
v.get_all_vols(outdict)
|
||||
v.get_all_vols(vols, aps, vps)
|
||||
|
||||
def add(self, src: str, dst: str) -> "VFS":
|
||||
"""get existing, or add new path to the vfs"""
|
||||
@@ -355,7 +374,8 @@ class VFS(object):
|
||||
flags = {k: v for k, v in self.flags.items()}
|
||||
hist = flags.get("hist")
|
||||
if hist and hist != "-":
|
||||
flags["hist"] = "{}/{}".format(hist.rstrip("/"), name)
|
||||
zs = "{}/{}".format(hist.rstrip("/"), name)
|
||||
flags["hist"] = os.path.expanduser(zs) if zs.startswith("~") else zs
|
||||
|
||||
return flags
|
||||
|
||||
@@ -388,7 +408,11 @@ class VFS(object):
|
||||
self, vpath: str, uname: str
|
||||
) -> tuple[bool, bool, bool, bool, bool, bool]:
|
||||
"""can Read,Write,Move,Delete,Get,Upget"""
|
||||
vn, _ = self._find(undot(vpath))
|
||||
if vpath:
|
||||
vn, _ = self._find(undot(vpath))
|
||||
else:
|
||||
vn = self
|
||||
|
||||
c = vn.axs
|
||||
return (
|
||||
uname in c.uread or "*" in c.uread,
|
||||
@@ -543,9 +567,20 @@ class VFS(object):
|
||||
self.log("vfs.walk", t.format(seen[-1], fsroot, self.vpath, rem), 3)
|
||||
return
|
||||
|
||||
if "xdev" in self.flags or "xvol" in self.flags:
|
||||
rm1 = []
|
||||
for le in vfs_ls:
|
||||
ap = absreal(os.path.join(fsroot, le[0]))
|
||||
vn2 = self.chk_ap(ap)
|
||||
if not vn2 or not vn2.get("", uname, True, False):
|
||||
rm1.append(le)
|
||||
_ = [vfs_ls.remove(x) for x in rm1] # type: ignore
|
||||
|
||||
seen = seen[:] + [fsroot]
|
||||
rfiles = [x for x in vfs_ls if not stat.S_ISDIR(x[1].st_mode)]
|
||||
rdirs = [x for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]
|
||||
# if lstat: ignore folder symlinks since copyparty will never make those
|
||||
# (and we definitely don't want to descend into them)
|
||||
|
||||
rfiles.sort()
|
||||
rdirs.sort()
|
||||
@@ -576,6 +611,7 @@ class VFS(object):
|
||||
|
||||
def zipgen(
|
||||
self,
|
||||
vpath: str,
|
||||
vrem: str,
|
||||
flt: set[str],
|
||||
uname: str,
|
||||
@@ -587,7 +623,7 @@ class VFS(object):
|
||||
|
||||
# if multiselect: add all items to archive root
|
||||
# if single folder: the folder itself is the top-level item
|
||||
folder = "" if flt or not wrap else (vrem.split("/")[-1].lstrip(".") or "top")
|
||||
folder = "" if flt or not wrap else (vpath.split("/")[-1].lstrip(".") or "top")
|
||||
|
||||
g = self.walk(folder, vrem, [], uname, [[True, False]], dots, scandir, False)
|
||||
for _, _, vpath, apath, files, rd, vd in g:
|
||||
@@ -638,6 +674,44 @@ class VFS(object):
|
||||
for d in [{"vp": v, "ap": a, "st": n} for v, a, n in ret2]:
|
||||
yield d
|
||||
|
||||
def chk_ap(self, ap: str, st: Optional[os.stat_result] = None) -> Optional["VFS"]:
|
||||
aps = ap + os.sep
|
||||
if "xdev" in self.flags and not ANYWIN:
|
||||
if not st:
|
||||
ap2 = ap.replace("\\", "/") if ANYWIN else ap
|
||||
while ap2:
|
||||
try:
|
||||
st = bos.stat(ap2)
|
||||
break
|
||||
except:
|
||||
if "/" not in ap2:
|
||||
raise
|
||||
ap2 = ap2.rsplit("/", 1)[0]
|
||||
assert st
|
||||
|
||||
vdev = self.dev
|
||||
if not vdev:
|
||||
vdev = self.dev = bos.stat(self.realpath).st_dev
|
||||
|
||||
if vdev != st.st_dev:
|
||||
if self.log:
|
||||
t = "xdev: {}[{}] => {}[{}]"
|
||||
self.log("vfs", t.format(vdev, self.realpath, st.st_dev, ap), 3)
|
||||
|
||||
return None
|
||||
|
||||
if "xvol" in self.flags:
|
||||
for vap, vn in self.root.all_aps:
|
||||
if aps.startswith(vap):
|
||||
return vn
|
||||
|
||||
if self.log:
|
||||
self.log("vfs", "xvol: [{}]".format(ap), 3)
|
||||
|
||||
return None
|
||||
|
||||
return self
|
||||
|
||||
|
||||
if WINDOWS:
|
||||
re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
|
||||
@@ -653,11 +727,15 @@ class AuthSrv(object):
|
||||
args: argparse.Namespace,
|
||||
log_func: Optional["RootLogger"],
|
||||
warn_anonwrite: bool = True,
|
||||
dargs: Optional[argparse.Namespace] = None,
|
||||
) -> None:
|
||||
self.args = args
|
||||
self.dargs = dargs or args
|
||||
self.log_func = log_func
|
||||
self.warn_anonwrite = warn_anonwrite
|
||||
self.line_ctr = 0
|
||||
self.indent = ""
|
||||
self.desc = []
|
||||
|
||||
self.mutex = threading.Lock()
|
||||
self.reload()
|
||||
@@ -690,17 +768,47 @@ class AuthSrv(object):
|
||||
raise Exception("invalid config")
|
||||
|
||||
if src in mount.values():
|
||||
t = "warning: filesystem-path [{}] mounted in multiple locations:"
|
||||
t = "filesystem-path [{}] mounted in multiple locations:"
|
||||
t = t.format(src)
|
||||
for v in [k for k, v in mount.items() if v == src] + [dst]:
|
||||
t += "\n /{}".format(v)
|
||||
|
||||
self.log(t, c=3)
|
||||
raise Exception("invalid config")
|
||||
|
||||
if not bos.path.isdir(src):
|
||||
self.log("warning: filesystem-path does not exist: {}".format(src), 3)
|
||||
|
||||
mount[dst] = src
|
||||
daxs[dst] = AXS()
|
||||
mflags[dst] = {}
|
||||
|
||||
def _e(self, desc: Optional[str] = None) -> None:
|
||||
if not self.args.vc or not self.line_ctr:
|
||||
return
|
||||
|
||||
if not desc and not self.indent:
|
||||
self.log("")
|
||||
return
|
||||
|
||||
desc = desc or ""
|
||||
desc = desc.replace("[", "[\033[0m").replace("]", "\033[90m]")
|
||||
self.log(" >>> {}{}".format(self.indent, desc), "90")
|
||||
|
||||
def _l(self, ln: str, c: int, desc: str) -> None:
|
||||
if not self.args.vc or not self.line_ctr:
|
||||
return
|
||||
|
||||
if c < 10:
|
||||
c += 30
|
||||
|
||||
t = "\033[97m{:4} \033[{}m{}{}"
|
||||
if desc:
|
||||
t += " \033[0;90m# {}\033[0m"
|
||||
desc = desc.replace("[", "[\033[0m").replace("]", "\033[90m]")
|
||||
|
||||
self.log(t.format(self.line_ctr, c, self.indent, ln, desc))
|
||||
|
||||
def _parse_config_file(
|
||||
self,
|
||||
fp: str,
|
||||
@@ -710,61 +818,143 @@ class AuthSrv(object):
|
||||
mflags: dict[str, dict[str, Any]],
|
||||
mount: dict[str, str],
|
||||
) -> None:
|
||||
skip = False
|
||||
vol_src = None
|
||||
vol_dst = None
|
||||
self.desc = []
|
||||
self.line_ctr = 0
|
||||
|
||||
expand_config_file(cfg_lines, fp, "")
|
||||
if self.args.vc:
|
||||
lns = ["{:4}: {}".format(n, s) for n, s in enumerate(cfg_lines, 1)]
|
||||
self.log("expanded config file (unprocessed):\n" + "\n".join(lns))
|
||||
|
||||
cfg_lines = upgrade_cfg_fmt(self.log, self.args, cfg_lines, fp)
|
||||
|
||||
cat = ""
|
||||
catg = "[global]"
|
||||
cata = "[accounts]"
|
||||
catx = "accs:"
|
||||
catf = "flags:"
|
||||
ap: Optional[str] = None
|
||||
vp: Optional[str] = None
|
||||
for ln in cfg_lines:
|
||||
self.line_ctr += 1
|
||||
if not ln and vol_src is not None:
|
||||
vol_src = None
|
||||
vol_dst = None
|
||||
|
||||
if skip:
|
||||
if not ln:
|
||||
skip = False
|
||||
ln = ln.split(" #")[0].strip()
|
||||
if not ln.split("#")[0].strip():
|
||||
continue
|
||||
|
||||
if not ln or ln.startswith("#"):
|
||||
continue
|
||||
if re.match(r"^\[.*\]:$", ln):
|
||||
ln = ln[:-1]
|
||||
|
||||
if vol_src is None:
|
||||
if ln.startswith("u "):
|
||||
u, p = ln[2:].split(":", 1)
|
||||
acct[u] = p
|
||||
elif ln.startswith("-"):
|
||||
skip = True # argv
|
||||
subsection = ln in (catx, catf)
|
||||
if ln.startswith("[") or subsection:
|
||||
self._e()
|
||||
if ap is None and vp is not None:
|
||||
t = "the first line after [/{}] must be a filesystem path to share on that volume"
|
||||
raise Exception(t.format(vp))
|
||||
|
||||
cat = ln
|
||||
if not subsection:
|
||||
ap = vp = None
|
||||
self.indent = ""
|
||||
else:
|
||||
vol_src = ln
|
||||
self.indent = " "
|
||||
|
||||
if ln == catg:
|
||||
t = "begin commandline-arguments (anything from --help; dashes are optional)"
|
||||
self._l(ln, 6, t)
|
||||
elif ln == cata:
|
||||
self._l(ln, 5, "begin user-accounts section")
|
||||
elif ln.startswith("[/"):
|
||||
vp = ln[1:-1].strip("/")
|
||||
self._l(ln, 2, "define volume at URL [/{}]".format(vp))
|
||||
elif subsection:
|
||||
if ln == catx:
|
||||
self._l(ln, 5, "volume access config:")
|
||||
else:
|
||||
t = "volume-specific config (anything from --help-flags)"
|
||||
self._l(ln, 6, t)
|
||||
else:
|
||||
raise Exception("invalid section header")
|
||||
|
||||
self.indent = " " if subsection else " "
|
||||
continue
|
||||
|
||||
if vol_src and vol_dst is None:
|
||||
vol_dst = ln
|
||||
if not vol_dst.startswith("/"):
|
||||
raise Exception('invalid mountpoint "{}"'.format(vol_dst))
|
||||
|
||||
if vol_src.startswith("~"):
|
||||
vol_src = os.path.expanduser(vol_src)
|
||||
|
||||
# cfg files override arguments and previous files
|
||||
vol_src = absreal(vol_src)
|
||||
vol_dst = vol_dst.strip("/")
|
||||
self._map_volume(vol_src, vol_dst, mount, daxs, mflags)
|
||||
if cat == catg:
|
||||
self._l(ln, 6, "")
|
||||
zt = split_cfg_ln(ln)
|
||||
for zs, za in zt.items():
|
||||
zs = zs.lstrip("-")
|
||||
if za is True:
|
||||
self._e("└─argument [{}]".format(zs))
|
||||
else:
|
||||
self._e("└─argument [{}] with value [{}]".format(zs, za))
|
||||
continue
|
||||
|
||||
try:
|
||||
lvl, uname = ln.split(" ", 1)
|
||||
except:
|
||||
lvl = ln
|
||||
uname = "*"
|
||||
if cat == cata:
|
||||
try:
|
||||
u, p = [zs.strip() for zs in ln.split(":", 1)]
|
||||
self._l(ln, 5, "account [{}], password [{}]".format(u, p))
|
||||
acct[u] = p
|
||||
except:
|
||||
t = 'lines inside the [accounts] section must be "username: password"'
|
||||
raise Exception(t)
|
||||
continue
|
||||
|
||||
if lvl == "a":
|
||||
t = "WARNING (config-file): permission flag 'a' is deprecated; please use 'rw' instead"
|
||||
self.log(t, 1)
|
||||
if vp is not None and ap is None:
|
||||
ap = ln
|
||||
if ap.startswith("~"):
|
||||
ap = os.path.expanduser(ap)
|
||||
|
||||
assert vol_dst is not None
|
||||
self._read_vol_str(lvl, uname, daxs[vol_dst], mflags[vol_dst])
|
||||
ap = absreal(ap)
|
||||
self._l(ln, 2, "bound to filesystem-path [{}]".format(ap))
|
||||
self._map_volume(ap, vp, mount, daxs, mflags)
|
||||
continue
|
||||
|
||||
if cat == catx:
|
||||
err = ""
|
||||
try:
|
||||
self._l(ln, 5, "volume access config:")
|
||||
sk, sv = ln.split(":")
|
||||
if re.sub("[rwmdgG]", "", sk) or not sk:
|
||||
err = "invalid accs permissions list; "
|
||||
raise Exception(err)
|
||||
if " " in re.sub(", *", "", sv).strip():
|
||||
err = "list of users is not comma-separated; "
|
||||
raise Exception(err)
|
||||
self._read_vol_str(sk, sv.replace(" ", ""), daxs[vp], mflags[vp])
|
||||
continue
|
||||
except:
|
||||
err += "accs entries must be 'rwmdgG: user1, user2, ...'"
|
||||
raise Exception(err)
|
||||
|
||||
if cat == catf:
|
||||
err = ""
|
||||
try:
|
||||
self._l(ln, 6, "volume-specific config:")
|
||||
zd = split_cfg_ln(ln)
|
||||
fstr = ""
|
||||
for sk, sv in zd.items():
|
||||
bad = re.sub(r"[a-z0-9_-]", "", sk).lstrip("-")
|
||||
if bad:
|
||||
err = "bad characters [{}] in volflag name [{}]; "
|
||||
err = err.format(bad, sk)
|
||||
raise Exception(err)
|
||||
if sv is True:
|
||||
fstr += "," + sk
|
||||
else:
|
||||
fstr += ",{}={}".format(sk, sv)
|
||||
self._read_vol_str("c", fstr[1:], daxs[vp], mflags[vp])
|
||||
fstr = ""
|
||||
if fstr:
|
||||
self._read_vol_str("c", fstr[1:], daxs[vp], mflags[vp])
|
||||
continue
|
||||
except:
|
||||
err += "flags entries (volflags) must be one of the following:\n 'flag1, flag2, ...'\n 'key: value'\n 'flag1, flag2, key: value'"
|
||||
raise Exception(err)
|
||||
|
||||
raise Exception("unprocessable line in config")
|
||||
|
||||
self._e()
|
||||
self.line_ctr = 0
|
||||
|
||||
def _read_vol_str(
|
||||
self, lvl: str, uname: str, axs: AXS, flags: dict[str, Any]
|
||||
@@ -803,6 +993,13 @@ class AuthSrv(object):
|
||||
("G", axs.upget),
|
||||
]: # b bb bbb
|
||||
if ch in lvl:
|
||||
if un == "*":
|
||||
t = "└─add permission [{0}] for [everyone] -- {2}"
|
||||
else:
|
||||
t = "└─add permission [{0}] for user [{1}] -- {2}"
|
||||
|
||||
desc = permdescs.get(ch, "?")
|
||||
self._e(t.format(ch, un, desc))
|
||||
al.add(un)
|
||||
|
||||
def _read_volflag(
|
||||
@@ -812,7 +1009,20 @@ class AuthSrv(object):
|
||||
value: Union[str, bool, list[str]],
|
||||
is_list: bool,
|
||||
) -> None:
|
||||
if name not in ["mtp", "xbu", "xau", "xbr", "xar", "xbd", "xad", "xm"]:
|
||||
desc = flagdescs.get(name.lstrip("-"), "?").replace("\n", " ")
|
||||
|
||||
if re.match("^-[^-]+$", name):
|
||||
t = "└─unset volflag [{}] ({})"
|
||||
self._e(t.format(name[1:], desc))
|
||||
flags[name] = True
|
||||
return
|
||||
|
||||
if name not in "mtp xbu xau xiu xbr xar xbd xad xm".split():
|
||||
if value is True:
|
||||
t = "└─add volflag [{}] = {} ({})"
|
||||
else:
|
||||
t = "└─add volflag [{}] = [{}] ({})"
|
||||
self._e(t.format(name, value, desc))
|
||||
flags[name] = value
|
||||
return
|
||||
|
||||
@@ -825,6 +1035,7 @@ class AuthSrv(object):
|
||||
vals += [value]
|
||||
|
||||
flags[name] = vals
|
||||
self._e("volflag [{}] += {} ({})".format(name, vals, desc))
|
||||
|
||||
def reload(self) -> None:
|
||||
"""
|
||||
@@ -875,6 +1086,18 @@ class AuthSrv(object):
|
||||
lns: list[str] = []
|
||||
try:
|
||||
self._parse_config_file(cfg_fn, lns, acct, daxs, mflags, mount)
|
||||
|
||||
zs = "#\033[36m cfg files in "
|
||||
zst = [x[len(zs) :] for x in lns if x.startswith(zs)]
|
||||
for zs in list(set(zst)):
|
||||
self.log("discovered config files in " + zs, 6)
|
||||
|
||||
zs = "#\033[36m opening cfg file"
|
||||
zstt = [x.split(" -> ") for x in lns if x.startswith(zs)]
|
||||
zst = [(max(0, len(x) - 2) * " ") + "└" + x[-1] for x in zstt]
|
||||
t = "loaded {} config files:\n{}"
|
||||
self.log(t.format(len(zst), "\n".join(zst)))
|
||||
|
||||
except:
|
||||
lns = lns[: self.line_ctr]
|
||||
slns = ["{:4}: {}".format(n, s) for n, s in enumerate(lns, 1)]
|
||||
@@ -918,7 +1141,13 @@ class AuthSrv(object):
|
||||
|
||||
assert vfs
|
||||
vfs.all_vols = {}
|
||||
vfs.get_all_vols(vfs.all_vols)
|
||||
vfs.all_aps = []
|
||||
vfs.all_vps = []
|
||||
vfs.get_all_vols(vfs.all_vols, vfs.all_aps, vfs.all_vps)
|
||||
for vol in vfs.all_vols.values():
|
||||
vol.all_aps.sort(key=lambda x: len(x[0]), reverse=True)
|
||||
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
|
||||
vol.root = vfs
|
||||
|
||||
for perm in "read write move del get pget".split():
|
||||
axs_key = "u" + perm
|
||||
@@ -952,15 +1181,26 @@ class AuthSrv(object):
|
||||
if LEELOO_DALLAS in all_users:
|
||||
raise Exception("sorry, reserved username: " + LEELOO_DALLAS)
|
||||
|
||||
seenpwds = {}
|
||||
for usr, pwd in acct.items():
|
||||
if pwd in seenpwds:
|
||||
t = "accounts [{}] and [{}] have the same password; this is not supported"
|
||||
self.log(t.format(seenpwds[pwd], usr), 1)
|
||||
raise Exception("invalid config")
|
||||
seenpwds[pwd] = usr
|
||||
|
||||
promote = []
|
||||
demote = []
|
||||
for vol in vfs.all_vols.values():
|
||||
zb = hashlib.sha512(fsenc(vol.realpath)).digest()
|
||||
zb = hashlib.sha512(afsenc(vol.realpath)).digest()
|
||||
hid = base64.b32encode(zb).decode("ascii").lower()
|
||||
vflag = vol.flags.get("hist")
|
||||
if vflag == "-":
|
||||
pass
|
||||
elif vflag:
|
||||
if vflag.startswith("~"):
|
||||
vflag = os.path.expanduser(vflag)
|
||||
|
||||
vol.histpath = uncyg(vflag) if WINDOWS else vflag
|
||||
elif self.args.hist:
|
||||
for nch in range(len(hid)):
|
||||
@@ -974,7 +1214,7 @@ class AuthSrv(object):
|
||||
except:
|
||||
owner = None
|
||||
|
||||
me = fsenc(vol.realpath).rstrip()
|
||||
me = afsenc(vol.realpath).rstrip()
|
||||
if owner not in [None, me]:
|
||||
continue
|
||||
|
||||
@@ -1114,38 +1354,30 @@ class AuthSrv(object):
|
||||
if ptn:
|
||||
vol.flags[vf] = re.compile(ptn)
|
||||
|
||||
for k in ["e2t", "e2ts", "e2tsr", "e2v", "e2vu", "e2vp", "xdev", "xvol"]:
|
||||
if getattr(self.args, k):
|
||||
vol.flags[k] = True
|
||||
|
||||
for ga, vf in (
|
||||
("no_sb_md", "no_sb_md"),
|
||||
("no_sb_lg", "no_sb_lg"),
|
||||
("no_forget", "noforget"),
|
||||
("no_dupe", "nodupe"),
|
||||
("hardlink", "hardlink"),
|
||||
("never_symlink", "neversymlink"),
|
||||
("no_dedup", "copydupes"),
|
||||
("magic", "magic"),
|
||||
("xlink", "xlink"),
|
||||
):
|
||||
for ga, vf in vf_bmap().items():
|
||||
if getattr(self.args, ga):
|
||||
vol.flags[vf] = True
|
||||
|
||||
for ve, vd in (
|
||||
("sb_md", "no_sb_md"),
|
||||
("nodotsrch", "dotsrch"),
|
||||
("sb_lg", "no_sb_lg"),
|
||||
("sb_md", "no_sb_md"),
|
||||
):
|
||||
if ve in vol.flags:
|
||||
vol.flags.pop(vd, None)
|
||||
|
||||
for ga, vf in (
|
||||
("md_sbf", "md_sbf"),
|
||||
("lg_sbf", "lg_sbf"),
|
||||
):
|
||||
for ga, vf in vf_vmap().items():
|
||||
if vf not in vol.flags:
|
||||
vol.flags[vf] = getattr(self.args, ga)
|
||||
|
||||
for k in ("nrand",):
|
||||
if k not in vol.flags:
|
||||
vol.flags[k] = getattr(self.args, k)
|
||||
|
||||
for k in ("nrand",):
|
||||
if k in vol.flags:
|
||||
vol.flags[k] = int(vol.flags[k])
|
||||
|
||||
for k1, k2 in IMPLICATIONS:
|
||||
if k1 in vol.flags:
|
||||
vol.flags[k2] = True
|
||||
@@ -1171,7 +1403,7 @@ class AuthSrv(object):
|
||||
vol.flags["mth"] = self.args.mth
|
||||
|
||||
# append additive args from argv to volflags
|
||||
hooks = "xbu xau xbr xar xbd xad xm".split()
|
||||
hooks = "xbu xau xiu xbr xar xbd xad xm".split()
|
||||
for name in ["mtp"] + hooks:
|
||||
self._read_volflag(vol.flags, name, getattr(self.args, name), True)
|
||||
|
||||
@@ -1231,10 +1463,19 @@ class AuthSrv(object):
|
||||
if k in ints:
|
||||
vol.flags[k] = int(vol.flags[k])
|
||||
|
||||
if "lifetime" in vol.flags and "e2d" not in vol.flags:
|
||||
t = 'removing lifetime config from volume "/{}" because e2d is disabled'
|
||||
self.log(t.format(vol.vpath), 1)
|
||||
del vol.flags["lifetime"]
|
||||
if "e2d" not in vol.flags:
|
||||
if "lifetime" in vol.flags:
|
||||
t = 'removing lifetime config from volume "/{}" because e2d is disabled'
|
||||
self.log(t.format(vol.vpath), 1)
|
||||
del vol.flags["lifetime"]
|
||||
|
||||
needs_e2d = [x for x in hooks if x != "xm"]
|
||||
drop = [x for x in needs_e2d if vol.flags.get(x)]
|
||||
if drop:
|
||||
t = 'removing [{}] from volume "/{}" because e2d is disabled'
|
||||
self.log(t.format(", ".join(drop), vol.vpath), 1)
|
||||
for x in drop:
|
||||
vol.flags.pop(x)
|
||||
|
||||
if vol.flags.get("neversymlink") and not vol.flags.get("hardlink"):
|
||||
vol.flags["copydupes"] = True
|
||||
@@ -1295,6 +1536,12 @@ class AuthSrv(object):
|
||||
self.log(t, 1)
|
||||
errors = True
|
||||
|
||||
for vol in vfs.all_vols.values():
|
||||
for k in list(vol.flags.keys()):
|
||||
if re.match("^-[^-]+$", k):
|
||||
vol.flags.pop(k[1:], None)
|
||||
vol.flags.pop(k)
|
||||
|
||||
if errors:
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1334,6 +1581,12 @@ class AuthSrv(object):
|
||||
if t:
|
||||
self.log("\n\033[{}\033[0m\n".format(t))
|
||||
|
||||
zv, _ = vfs.get("/", "*", False, False)
|
||||
zs = zv.realpath.lower()
|
||||
if zs in ("/", "c:\\") or zs.startswith(r"c:\windows"):
|
||||
t = "you are sharing a system directory: {}\n"
|
||||
self.log(t.format(zv.realpath), c=1)
|
||||
|
||||
try:
|
||||
zv, _ = vfs.get("/", "*", False, True)
|
||||
if self.warn_anonwrite and os.getcwd() == zv.realpath:
|
||||
@@ -1483,32 +1736,298 @@ class AuthSrv(object):
|
||||
if not flag_r:
|
||||
sys.exit(0)
|
||||
|
||||
def cgen(self) -> None:
|
||||
ret = [
|
||||
"## WARNING:",
|
||||
"## there will probably be mistakes in",
|
||||
"## commandline-args (and maybe volflags)",
|
||||
"",
|
||||
]
|
||||
|
||||
csv = set("i p".split())
|
||||
lst = set("c ihead mtm mtp xad xar xau xiu xbd xbr xbu xm".split())
|
||||
askip = set("a v c vc cgen theme".split())
|
||||
|
||||
# keymap from argv to vflag
|
||||
amap = vf_bmap()
|
||||
amap.update(vf_vmap())
|
||||
amap.update(vf_cmap())
|
||||
vmap = {v: k for k, v in amap.items()}
|
||||
|
||||
args = {k: v for k, v in vars(self.args).items()}
|
||||
pops = []
|
||||
for k1, k2 in IMPLICATIONS:
|
||||
if args.get(k1):
|
||||
pops.append(k2)
|
||||
for pop in pops:
|
||||
args.pop(pop, None)
|
||||
|
||||
if args:
|
||||
ret.append("[global]")
|
||||
for k, v in args.items():
|
||||
if k in askip:
|
||||
continue
|
||||
if k in csv:
|
||||
v = ", ".join([str(za) for za in v])
|
||||
try:
|
||||
v2 = getattr(self.dargs, k)
|
||||
if v == v2:
|
||||
continue
|
||||
except:
|
||||
continue
|
||||
|
||||
dk = " " + k.replace("_", "-")
|
||||
if k in lst:
|
||||
for ve in v:
|
||||
ret.append("{}: {}".format(dk, ve))
|
||||
else:
|
||||
if v is True:
|
||||
ret.append(dk)
|
||||
elif v not in (False, None, ""):
|
||||
ret.append("{}: {}".format(dk, v))
|
||||
ret.append("")
|
||||
|
||||
if self.acct:
|
||||
ret.append("[accounts]")
|
||||
for u, p in self.acct.items():
|
||||
ret.append(" {}: {}".format(u, p))
|
||||
ret.append("")
|
||||
|
||||
for vol in self.vfs.all_vols.values():
|
||||
ret.append("[/{}]".format(vol.vpath))
|
||||
ret.append(" " + vol.realpath)
|
||||
ret.append(" accs:")
|
||||
perms = {
|
||||
"r": "uread",
|
||||
"w": "uwrite",
|
||||
"m": "umove",
|
||||
"d": "udel",
|
||||
"g": "uget",
|
||||
"G": "upget",
|
||||
}
|
||||
users = {}
|
||||
for pkey in perms.values():
|
||||
for uname in getattr(vol.axs, pkey):
|
||||
try:
|
||||
users[uname] += 1
|
||||
except:
|
||||
users[uname] = 1
|
||||
lusers = [(v, k) for k, v in users.items()]
|
||||
vperms = {}
|
||||
for _, uname in sorted(lusers):
|
||||
pstr = ""
|
||||
for pchar, pkey in perms.items():
|
||||
if uname in getattr(vol.axs, pkey):
|
||||
pstr += pchar
|
||||
if "g" in pstr and "G" in pstr:
|
||||
pstr = pstr.replace("g", "")
|
||||
try:
|
||||
vperms[pstr].append(uname)
|
||||
except:
|
||||
vperms[pstr] = [uname]
|
||||
for pstr, uname in vperms.items():
|
||||
ret.append(" {}: {}".format(pstr, ", ".join(uname)))
|
||||
trues = []
|
||||
vals = []
|
||||
for k, v in sorted(vol.flags.items()):
|
||||
try:
|
||||
ak = vmap[k]
|
||||
if getattr(self.args, ak) is v:
|
||||
continue
|
||||
except:
|
||||
pass
|
||||
|
||||
if k in lst:
|
||||
for ve in v:
|
||||
vals.append("{}: {}".format(k, ve))
|
||||
elif v is True:
|
||||
trues.append(k)
|
||||
elif v is not False:
|
||||
try:
|
||||
v = v.pattern
|
||||
except:
|
||||
pass
|
||||
|
||||
vals.append("{}: {}".format(k, v))
|
||||
pops = []
|
||||
for k1, k2 in IMPLICATIONS:
|
||||
if k1 in trues:
|
||||
pops.append(k2)
|
||||
trues = [x for x in trues if x not in pops]
|
||||
if trues:
|
||||
vals.append(", ".join(trues))
|
||||
if vals:
|
||||
ret.append(" flags:")
|
||||
for zs in vals:
|
||||
ret.append(" " + zs)
|
||||
ret.append("")
|
||||
|
||||
self.log("generated config:\n\n" + "\n".join(ret))
|
||||
|
||||
|
||||
def split_cfg_ln(ln: str) -> dict[str, Any]:
|
||||
# "a, b, c: 3" => {a:true, b:true, c:3}
|
||||
ret = {}
|
||||
while True:
|
||||
ln = ln.strip()
|
||||
if not ln:
|
||||
break
|
||||
ofs_sep = ln.find(",") + 1
|
||||
ofs_var = ln.find(":") + 1
|
||||
if not ofs_sep and not ofs_var:
|
||||
ret[ln] = True
|
||||
break
|
||||
if ofs_sep and (ofs_sep < ofs_var or not ofs_var):
|
||||
k, ln = ln.split(",", 1)
|
||||
ret[k.strip()] = True
|
||||
else:
|
||||
k, ln = ln.split(":", 1)
|
||||
ret[k.strip()] = ln.strip()
|
||||
break
|
||||
return ret
|
||||
|
||||
|
||||
def expand_config_file(ret: list[str], fp: str, ipath: str) -> None:
|
||||
"""expand all % file includes"""
|
||||
fp = absreal(fp)
|
||||
ipath += " -> " + fp
|
||||
ret.append("#\033[36m opening cfg file{}\033[0m".format(ipath))
|
||||
if len(ipath.split(" -> ")) > 64:
|
||||
raise Exception("hit max depth of 64 includes")
|
||||
|
||||
if os.path.isdir(fp):
|
||||
for fn in sorted(os.listdir(fp)):
|
||||
names = os.listdir(fp)
|
||||
ret.append("#\033[36m cfg files in {} => {}\033[0m".format(fp, names))
|
||||
for fn in sorted(names):
|
||||
fp2 = os.path.join(fp, fn)
|
||||
if not os.path.isfile(fp2):
|
||||
continue # dont recurse
|
||||
if not fp2.endswith(".conf") or fp2 in ipath:
|
||||
continue
|
||||
|
||||
expand_config_file(ret, fp2, ipath)
|
||||
return
|
||||
|
||||
ipath += " -> " + fp
|
||||
ret.append("#\033[36m opening cfg file{}\033[0m".format(ipath))
|
||||
|
||||
with open(fp, "rb") as f:
|
||||
for ln in [x.decode("utf-8").strip() for x in f]:
|
||||
for oln in [x.decode("utf-8").rstrip() for x in f]:
|
||||
ln = oln.split(" #")[0].strip()
|
||||
if ln.startswith("% "):
|
||||
pad = " " * len(oln.split("%")[0])
|
||||
fp2 = ln[1:].strip()
|
||||
fp2 = os.path.join(os.path.dirname(fp), fp2)
|
||||
ofs = len(ret)
|
||||
expand_config_file(ret, fp2, ipath)
|
||||
for n in range(ofs, len(ret)):
|
||||
ret[n] = pad + ret[n]
|
||||
continue
|
||||
|
||||
ret.append(ln)
|
||||
ret.append(oln)
|
||||
|
||||
ret.append("#\033[36m closed{}\033[0m".format(ipath))
|
||||
|
||||
|
||||
def upgrade_cfg_fmt(
|
||||
log: Optional["NamedLogger"], args: argparse.Namespace, orig: list[str], cfg_fp: str
|
||||
) -> list[str]:
|
||||
"""convert from v1 to v2 format"""
|
||||
zst = [x.split("#")[0].strip() for x in orig]
|
||||
zst = [x for x in zst if x]
|
||||
if (
|
||||
"[global]" in zst
|
||||
or "[accounts]" in zst
|
||||
or "accs:" in zst
|
||||
or "flags:" in zst
|
||||
or [x for x in zst if x.startswith("[/")]
|
||||
or len(zst) == len([x for x in zst if x.startswith("%")])
|
||||
):
|
||||
return orig
|
||||
|
||||
zst = [x for x in orig if "#\033[36m opening cfg file" not in x]
|
||||
incl = len(zst) != len(orig) - 1
|
||||
|
||||
t = "upgrading config file [{}] from v1 to v2"
|
||||
if not args.vc:
|
||||
t += ". Run with argument '--vc' to see the converted config if you want to upgrade"
|
||||
if incl:
|
||||
t += ". Please don't include v1 configs from v2 files or vice versa! Upgrade all of them at the same time."
|
||||
if log:
|
||||
log(t.format(cfg_fp), 3)
|
||||
|
||||
ret = []
|
||||
vp = ""
|
||||
ap = ""
|
||||
cat = ""
|
||||
catg = "[global]"
|
||||
cata = "[accounts]"
|
||||
catx = " accs:"
|
||||
catf = " flags:"
|
||||
for ln in orig:
|
||||
sn = ln.strip()
|
||||
if not sn:
|
||||
cat = vp = ap = ""
|
||||
if not sn.split("#")[0]:
|
||||
ret.append(ln)
|
||||
elif sn.startswith("-") and cat in ("", catg):
|
||||
if cat != catg:
|
||||
cat = catg
|
||||
ret.append(cat)
|
||||
sn = sn.lstrip("-")
|
||||
zst = sn.split(" ", 1)
|
||||
if len(zst) > 1:
|
||||
sn = "{}: {}".format(zst[0], zst[1].strip())
|
||||
ret.append(" " + sn)
|
||||
elif sn.startswith("u ") and cat in ("", catg, cata):
|
||||
if cat != cata:
|
||||
cat = cata
|
||||
ret.append(cat)
|
||||
s1, s2 = sn[1:].split(":", 1)
|
||||
ret.append(" {}: {}".format(s1.strip(), s2.strip()))
|
||||
elif not ap:
|
||||
ap = sn
|
||||
elif not vp:
|
||||
vp = "/" + sn.strip("/")
|
||||
cat = "[{}]".format(vp)
|
||||
ret.append(cat)
|
||||
ret.append(" " + ap)
|
||||
elif sn.startswith("c "):
|
||||
if cat != catf:
|
||||
cat = catf
|
||||
ret.append(cat)
|
||||
sn = sn[1:].strip()
|
||||
if "=" in sn:
|
||||
zst = sn.split("=", 1)
|
||||
sn = zst[0].replace(",", ", ")
|
||||
sn += ": " + zst[1]
|
||||
else:
|
||||
sn = sn.replace(",", ", ")
|
||||
ret.append(" " + sn)
|
||||
elif sn[:1] in "rwmdgG":
|
||||
if cat != catx:
|
||||
cat = catx
|
||||
ret.append(cat)
|
||||
zst = sn.split(" ")
|
||||
zst = [x for x in zst if x]
|
||||
if len(zst) == 1:
|
||||
zst.append("*")
|
||||
ret.append(" {}: {}".format(zst[0], ", ".join(zst[1:])))
|
||||
else:
|
||||
t = "did not understand line {} in the config"
|
||||
t1 = t
|
||||
n = 0
|
||||
for ln in orig:
|
||||
n += 1
|
||||
t += "\n{:4} {}".format(n, ln)
|
||||
if log:
|
||||
log(t, 1)
|
||||
else:
|
||||
print("\033[31m" + t)
|
||||
raise Exception(t1)
|
||||
|
||||
if args.vc and log:
|
||||
t = "new config syntax (copy/paste this to upgrade your config):\n"
|
||||
t += "\n# ======================[ begin upgraded config ]======================\n\n"
|
||||
for ln in ret:
|
||||
t += ln + "\n"
|
||||
t += "\n# ======================[ end of upgraded config ]======================\n"
|
||||
log(t)
|
||||
|
||||
return ret
|
||||
|
||||
217
copyparty/cert.py
Normal file
217
copyparty/cert.py
Normal file
@@ -0,0 +1,217 @@
|
||||
import os
|
||||
import errno
|
||||
import time
|
||||
import json
|
||||
import shutil
|
||||
import filecmp
|
||||
import calendar
|
||||
|
||||
from .util import runcmd, Netdev
|
||||
|
||||
|
||||
HAVE_CFSSL = True
|
||||
|
||||
|
||||
def ensure_cert(log: "RootLogger", args) -> None:
|
||||
"""
|
||||
the default cert (and the entire TLS support) is only here to enable the
|
||||
crypto.subtle javascript API, which is necessary due to the webkit guys
|
||||
being massive memers (https://www.chromium.org/blink/webcrypto)
|
||||
|
||||
i feel awful about this and so should they
|
||||
"""
|
||||
cert_insec = os.path.join(args.E.mod, "res/insecure.pem")
|
||||
cert_appdata = os.path.join(args.E.cfg, "cert.pem")
|
||||
if not os.path.isfile(args.cert):
|
||||
if cert_appdata != args.cert:
|
||||
raise Exception("certificate file does not exist: " + args.cert)
|
||||
|
||||
shutil.copy(cert_insec, args.cert)
|
||||
|
||||
with open(args.cert, "rb") as f:
|
||||
buf = f.read()
|
||||
o1 = buf.find(b" PRIVATE KEY-")
|
||||
o2 = buf.find(b" CERTIFICATE-")
|
||||
m = "unsupported certificate format: "
|
||||
if o1 < 0:
|
||||
raise Exception(m + "no private key inside pem")
|
||||
if o2 < 0:
|
||||
raise Exception(m + "no server certificate inside pem")
|
||||
if o1 > o2:
|
||||
raise Exception(m + "private key must appear before server certificate")
|
||||
|
||||
try:
|
||||
if filecmp.cmp(args.cert, cert_insec):
|
||||
t = "using default TLS certificate; https will be insecure:\033[36m {}"
|
||||
log("cert", t.format(args.cert), 3)
|
||||
except:
|
||||
pass
|
||||
|
||||
# speaking of the default cert,
|
||||
# printf 'NO\n.\n.\n.\n.\ncopyparty-insecure\n.\n' | faketime '2000-01-01 00:00:00' openssl req -x509 -sha256 -newkey rsa:2048 -keyout insecure.pem -out insecure.pem -days $((($(printf %d 0x7fffffff)-$(date +%s --date=2000-01-01T00:00:00Z))/(60*60*24))) -nodes && ls -al insecure.pem && openssl x509 -in insecure.pem -text -noout
|
||||
|
||||
|
||||
def _read_crt(args, fn):
|
||||
try:
|
||||
if not os.path.exists(os.path.join(args.crt_dir, fn)):
|
||||
return 0, {}
|
||||
|
||||
acmd = ["cfssl-certinfo", "-cert", fn]
|
||||
rc, so, se = runcmd(acmd, cwd=args.crt_dir)
|
||||
if rc:
|
||||
return 0, {}
|
||||
|
||||
inf = json.loads(so)
|
||||
zs = inf["not_after"]
|
||||
expiry = calendar.timegm(time.strptime(zs, "%Y-%m-%dT%H:%M:%SZ"))
|
||||
return expiry, inf
|
||||
except OSError as ex:
|
||||
if ex.errno == errno.ENOENT:
|
||||
raise
|
||||
return 0, {}
|
||||
except:
|
||||
return 0, {}
|
||||
|
||||
|
||||
def _gen_ca(log: "RootLogger", args):
|
||||
expiry = _read_crt(args, "ca.pem")[0]
|
||||
if time.time() + args.crt_cdays * 60 * 60 * 24 * 0.1 < expiry:
|
||||
return
|
||||
|
||||
backdate = "{}m".format(int(args.crt_back * 60))
|
||||
expiry = "{}m".format(int(args.crt_cdays * 60 * 24))
|
||||
cn = args.crt_cnc.replace("--crt-cn", args.crt_cn)
|
||||
algo, ksz = args.crt_alg.split("-")
|
||||
req = {
|
||||
"CN": cn,
|
||||
"CA": {"backdate": backdate, "expiry": expiry, "pathlen": 0},
|
||||
"key": {"algo": algo, "size": int(ksz)},
|
||||
"names": [{"O": cn}],
|
||||
}
|
||||
sin = json.dumps(req).encode("utf-8")
|
||||
log("cert", "creating new ca ...", 6)
|
||||
|
||||
cmd = "cfssl gencert -initca -"
|
||||
rc, so, se = runcmd(cmd.split(), 30, sin=sin)
|
||||
if rc:
|
||||
raise Exception("failed to create ca-cert: {}, {}".format(rc, se), 3)
|
||||
|
||||
cmd = "cfssljson -bare ca"
|
||||
sin = so.encode("utf-8")
|
||||
rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir)
|
||||
if rc:
|
||||
raise Exception("failed to translate ca-cert: {}, {}".format(rc, se), 3)
|
||||
|
||||
bname = os.path.join(args.crt_dir, "ca")
|
||||
os.rename(bname + "-key.pem", bname + ".key")
|
||||
os.unlink(bname + ".csr")
|
||||
|
||||
log("cert", "new ca OK", 2)
|
||||
|
||||
|
||||
def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
||||
names = args.crt_ns.split(",") if args.crt_ns else []
|
||||
if not args.crt_exact:
|
||||
for n in names[:]:
|
||||
names.append("*.{}".format(n))
|
||||
if not args.crt_noip:
|
||||
for ip in netdevs.keys():
|
||||
names.append(ip.split("/")[0])
|
||||
if args.crt_nolo:
|
||||
names = [x for x in names if x not in ("localhost", "127.0.0.1", "::1")]
|
||||
if not names:
|
||||
names = ["127.0.0.1"]
|
||||
if "127.0.0.1" in names or "::1" in names:
|
||||
names.append("localhost")
|
||||
names = list({x: 1 for x in names}.keys())
|
||||
|
||||
try:
|
||||
expiry, inf = _read_crt(args, "srv.pem")
|
||||
expired = time.time() + args.crt_sdays * 60 * 60 * 24 * 0.1 > expiry
|
||||
cert_insec = os.path.join(args.E.mod, "res/insecure.pem")
|
||||
for n in names:
|
||||
if n not in inf["sans"]:
|
||||
raise Exception("does not have {}".format(n))
|
||||
if expired:
|
||||
raise Exception("old server-cert has expired")
|
||||
if not filecmp.cmp(args.cert, cert_insec):
|
||||
return
|
||||
except Exception as ex:
|
||||
log("cert", "will create new server-cert; {}".format(ex))
|
||||
|
||||
log("cert", "creating server-cert ...", 6)
|
||||
|
||||
backdate = "{}m".format(int(args.crt_back * 60))
|
||||
expiry = "{}m".format(int(args.crt_sdays * 60 * 24))
|
||||
cfg = {
|
||||
"signing": {
|
||||
"default": {
|
||||
"backdate": backdate,
|
||||
"expiry": expiry,
|
||||
"usages": ["signing", "key encipherment", "server auth"],
|
||||
}
|
||||
}
|
||||
}
|
||||
with open(os.path.join(args.crt_dir, "cfssl.json"), "wb") as f:
|
||||
f.write(json.dumps(cfg).encode("utf-8"))
|
||||
|
||||
cn = args.crt_cns.replace("--crt-cn", args.crt_cn)
|
||||
algo, ksz = args.crt_alg.split("-")
|
||||
req = {
|
||||
"key": {"algo": algo, "size": int(ksz)},
|
||||
"names": [{"O": cn}],
|
||||
}
|
||||
sin = json.dumps(req).encode("utf-8")
|
||||
|
||||
cmd = "cfssl gencert -config=cfssl.json -ca ca.pem -ca-key ca.key -profile=www"
|
||||
acmd = cmd.split() + ["-hostname=" + ",".join(names), "-"]
|
||||
rc, so, se = runcmd(acmd, 30, sin=sin, cwd=args.crt_dir)
|
||||
if rc:
|
||||
raise Exception("failed to create cert: {}, {}".format(rc, se))
|
||||
|
||||
cmd = "cfssljson -bare srv"
|
||||
sin = so.encode("utf-8")
|
||||
rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir)
|
||||
if rc:
|
||||
raise Exception("failed to translate cert: {}, {}".format(rc, se))
|
||||
|
||||
bname = os.path.join(args.crt_dir, "srv")
|
||||
os.rename(bname + "-key.pem", bname + ".key")
|
||||
os.unlink(bname + ".csr")
|
||||
|
||||
with open(os.path.join(args.crt_dir, "ca.pem"), "rb") as f:
|
||||
ca = f.read()
|
||||
|
||||
with open(bname + ".key", "rb") as f:
|
||||
skey = f.read()
|
||||
|
||||
with open(bname + ".pem", "rb") as f:
|
||||
scrt = f.read()
|
||||
|
||||
with open(args.cert, "wb") as f:
|
||||
f.write(skey + scrt + ca)
|
||||
|
||||
log("cert", "new server-cert OK", 2)
|
||||
|
||||
|
||||
def gencert(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
||||
global HAVE_CFSSL
|
||||
|
||||
if args.http_only:
|
||||
return
|
||||
|
||||
if args.no_crt or not HAVE_CFSSL:
|
||||
ensure_cert(log, args)
|
||||
return
|
||||
|
||||
try:
|
||||
_gen_ca(log, args)
|
||||
_gen_srv(log, args, netdevs)
|
||||
except Exception as ex:
|
||||
HAVE_CFSSL = False
|
||||
log("cert", "could not create TLS certificates: {}".format(ex), 3)
|
||||
if getattr(ex, "errno", 0) == errno.ENOENT:
|
||||
t = "install cfssl if you want to fix this; https://github.com/cloudflare/cfssl/releases/latest"
|
||||
log("cert", t, 6)
|
||||
|
||||
ensure_cert(log, args)
|
||||
157
copyparty/cfg.py
Normal file
157
copyparty/cfg.py
Normal file
@@ -0,0 +1,157 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
# awk -F\" '/add_argument\("-[^-]/{print(substr($2,2))}' copyparty/__main__.py | sort | tr '\n' ' '
|
||||
zs = "a c e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vp e2vu ed emp i j lo mcr mte mth mtm mtp nb nc nid nih nw p q s ss sss v z zv"
|
||||
onedash = set(zs.split())
|
||||
|
||||
|
||||
def vf_bmap() -> dict[str, str]:
|
||||
"""argv-to-volflag: simple bools"""
|
||||
ret = {
|
||||
"never_symlink": "neversymlink",
|
||||
"no_dedup": "copydupes",
|
||||
"no_dupe": "nodupe",
|
||||
"no_forget": "noforget",
|
||||
"dav_auth": "davauth",
|
||||
"dav_rt": "davrt",
|
||||
}
|
||||
for k in (
|
||||
"dotsrch",
|
||||
"e2t",
|
||||
"e2ts",
|
||||
"e2tsr",
|
||||
"e2v",
|
||||
"e2vu",
|
||||
"e2vp",
|
||||
"grid",
|
||||
"hardlink",
|
||||
"magic",
|
||||
"no_sb_md",
|
||||
"no_sb_lg",
|
||||
"rand",
|
||||
"xdev",
|
||||
"xlink",
|
||||
"xvol",
|
||||
):
|
||||
ret[k] = k
|
||||
return ret
|
||||
|
||||
|
||||
def vf_vmap() -> dict[str, str]:
|
||||
"""argv-to-volflag: simple values"""
|
||||
ret = {}
|
||||
for k in ("lg_sbf", "md_sbf", "unlist"):
|
||||
ret[k] = k
|
||||
return ret
|
||||
|
||||
|
||||
def vf_cmap() -> dict[str, str]:
|
||||
"""argv-to-volflag: complex/lists"""
|
||||
ret = {}
|
||||
for k in ("dbd", "html_head", "mte", "mth", "nrand"):
|
||||
ret[k] = k
|
||||
return ret
|
||||
|
||||
|
||||
permdescs = {
|
||||
"r": "read; list folder contents, download files",
|
||||
"w": 'write; upload files; need "r" to see the uploads',
|
||||
"m": 'move; move files and folders; need "w" at destination',
|
||||
"d": "delete; permanently delete files and folders",
|
||||
"g": "get; download files, but cannot see folder contents",
|
||||
"G": 'upget; same as "g" but can see filekeys of their own uploads',
|
||||
}
|
||||
|
||||
|
||||
flagcats = {
|
||||
"uploads, general": {
|
||||
"nodupe": "rejects existing files (instead of symlinking them)",
|
||||
"hardlink": "does dedup with hardlinks instead of symlinks",
|
||||
"neversymlink": "disables symlink fallback; full copy instead",
|
||||
"copydupes": "disables dedup, always saves full copies of dupes",
|
||||
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
|
||||
"nosub": "forces all uploads into the top folder of the vfs",
|
||||
"magic": "enables filetype detection for nameless uploads",
|
||||
"gz": "allows server-side gzip of uploads with ?gz (also c,xz)",
|
||||
"pk": "forces server-side compression, optional arg: xz,9",
|
||||
},
|
||||
"upload rules": {
|
||||
"maxn=250,600": "max 250 uploads over 15min",
|
||||
"maxb=1g,300": "max 1 GiB over 5min (suffixes: b, k, m, g)",
|
||||
"rand": "force randomized filenames, 9 chars long by default",
|
||||
"nrand=N": "randomized filenames are N chars long",
|
||||
"sz=1k-3m": "allow filesizes between 1 KiB and 3MiB",
|
||||
"df=1g": "ensure 1 GiB free disk space",
|
||||
},
|
||||
"upload rotation\n(moves all uploads into the specified folder structure)": {
|
||||
"rotn=100,3": "3 levels of subfolders with 100 entries in each",
|
||||
"rotf=%Y-%m/%d-%H": "date-formatted organizing",
|
||||
"lifetime=3600": "uploads are deleted after 1 hour",
|
||||
},
|
||||
"database, general": {
|
||||
"e2d": "enable database; makes files searchable + enables upload dedup",
|
||||
"e2ds": "scan writable folders for new files on startup; also sets -e2d",
|
||||
"e2dsa": "scans all folders for new files on startup; also sets -e2d",
|
||||
"e2t": "enable multimedia indexing; makes it possible to search for tags",
|
||||
"e2ts": "scan existing files for tags on startup; also sets -e2t",
|
||||
"e2tsa": "delete all metadata from DB (full rescan); also sets -e2ts",
|
||||
"d2ts": "disables metadata collection for existing files",
|
||||
"d2ds": "disables onboot indexing, overrides -e2ds*",
|
||||
"d2t": "disables metadata collection, overrides -e2t*",
|
||||
"d2v": "disables file verification, overrides -e2v*",
|
||||
"d2d": "disables all database stuff, overrides -e2*",
|
||||
"hist=/tmp/cdb": "puts thumbnails and indexes at that location",
|
||||
"scan=60": "scan for new files every 60sec, same as --re-maxage",
|
||||
"nohash=\\.iso$": "skips hashing file contents if path matches *.iso",
|
||||
"noidx=\\.iso$": "fully ignores the contents at paths matching *.iso",
|
||||
"noforget": "don't forget files when deleted from disk",
|
||||
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
|
||||
"xlink": "cross-volume dupe detection / linking",
|
||||
"xdev": "do not descend into other filesystems",
|
||||
"xvol": "do not follow symlinks leaving the volume root",
|
||||
"dotsrch": "show dotfiles in search results",
|
||||
"nodotsrch": "hide dotfiles in search results (default)",
|
||||
},
|
||||
'database, audio tags\n"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...': {
|
||||
"mtp=.bpm=f,audio-bpm.py": 'uses the "audio-bpm.py" program to\ngenerate ".bpm" tags from uploads (f = overwrite tags)',
|
||||
"mtp=ahash,vhash=media-hash.py": "collects two tags at once",
|
||||
},
|
||||
"thumbnails": {
|
||||
"dthumb": "disables all thumbnails",
|
||||
"dvthumb": "disables video thumbnails",
|
||||
"dathumb": "disables audio thumbnails (spectrograms)",
|
||||
"dithumb": "disables image thumbnails",
|
||||
},
|
||||
"event hooks\n(better explained in --help-hooks)": {
|
||||
"xbu=CMD": "execute CMD before a file upload starts",
|
||||
"xau=CMD": "execute CMD after a file upload finishes",
|
||||
"xiu=CMD": "execute CMD after all uploads finish and volume is idle",
|
||||
"xbr=CMD": "execute CMD before a file rename/move",
|
||||
"xar=CMD": "execute CMD after a file rename/move",
|
||||
"xbd=CMD": "execute CMD before a file delete",
|
||||
"xad=CMD": "execute CMD after a file delete",
|
||||
"xm=CMD": "execute CMD on message",
|
||||
},
|
||||
"client and ux": {
|
||||
"grid": "show grid/thumbnails by default",
|
||||
"unlist": "dont list files matching REGEX",
|
||||
"html_head=TXT": "includes TXT in the <head>",
|
||||
"robots": "allows indexing by search engines (default)",
|
||||
"norobots": "kindly asks search engines to leave",
|
||||
"no_sb_md": "disable js sandbox for markdown files",
|
||||
"no_sb_lg": "disable js sandbox for prologue/epilogue",
|
||||
"sb_md": "enable js sandbox for markdown files (default)",
|
||||
"sb_lg": "enable js sandbox for prologue/epilogue (default)",
|
||||
"md_sbf": "list of markdown-sandbox safeguards to disable",
|
||||
"lg_sbf": "list of *logue-sandbox safeguards to disable",
|
||||
},
|
||||
"others": {
|
||||
"fk=8": 'generates per-file accesskeys,\nwhich will then be required at the "g" permission',
|
||||
"davauth": "ask webdav clients to login for all folders",
|
||||
"davrt": "show lastmod time of symlink destination, not the link itself\n(note: this option is always enabled for recursive listings)",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
flagdescs = {k.split("=")[0]: v for tab in flagcats.values() for k, v in tab.items()}
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import stat
|
||||
@@ -13,9 +14,21 @@ from pyftpdlib.filesystems import AbstractedFS, FilesystemError
|
||||
from pyftpdlib.handlers import FTPHandler
|
||||
from pyftpdlib.servers import FTPServer
|
||||
|
||||
from .__init__ import PY2, TYPE_CHECKING, E
|
||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, E
|
||||
from .authsrv import VFS
|
||||
from .bos import bos
|
||||
from .util import Daemon, Pebkac, exclude_dotfiles, fsenc, ipnorm
|
||||
from .util import (
|
||||
Daemon,
|
||||
Pebkac,
|
||||
exclude_dotfiles,
|
||||
fsenc,
|
||||
ipnorm,
|
||||
pybin,
|
||||
relchk,
|
||||
runhook,
|
||||
sanitize_fn,
|
||||
vjoin,
|
||||
)
|
||||
|
||||
try:
|
||||
from pyftpdlib.ioloop import IOLoop
|
||||
@@ -34,6 +47,12 @@ if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
class FSE(FilesystemError):
|
||||
def __init__(self, msg: str, severity: int = 0) -> None:
|
||||
super(FilesystemError, self).__init__(msg)
|
||||
self.severity = severity
|
||||
|
||||
|
||||
class FtpAuth(DummyAuthorizer):
|
||||
def __init__(self, hub: "SvcHub") -> None:
|
||||
super(FtpAuth, self).__init__()
|
||||
@@ -43,6 +62,7 @@ class FtpAuth(DummyAuthorizer):
|
||||
self, username: str, password: str, handler: Any
|
||||
) -> None:
|
||||
handler.username = "{}:{}".format(username, password)
|
||||
handler.uname = "*"
|
||||
|
||||
ip = handler.addr[0]
|
||||
if ip.startswith("::ffff:"):
|
||||
@@ -74,14 +94,14 @@ class FtpAuth(DummyAuthorizer):
|
||||
|
||||
raise AuthenticationFailed("Authentication failed.")
|
||||
|
||||
handler.username = uname
|
||||
handler.uname = handler.username = uname
|
||||
|
||||
def get_home_dir(self, username: str) -> str:
|
||||
return "/"
|
||||
|
||||
def has_user(self, username: str) -> bool:
|
||||
asrv = self.hub.asrv
|
||||
return username in asrv.acct
|
||||
return username in asrv.acct or username in asrv.iacct
|
||||
|
||||
def has_perm(self, username: str, perm: int, path: Optional[str] = None) -> bool:
|
||||
return True # handled at filesystem layer
|
||||
@@ -100,11 +120,11 @@ class FtpFs(AbstractedFS):
|
||||
def __init__(
|
||||
self, root: str, cmd_channel: Any
|
||||
) -> None: # pylint: disable=super-init-not-called
|
||||
self.h = self.cmd_channel = cmd_channel # type: FTPHandler
|
||||
self.h = cmd_channel # type: FTPHandler
|
||||
self.cmd_channel = cmd_channel # type: FTPHandler
|
||||
self.hub: "SvcHub" = cmd_channel.hub
|
||||
self.args = cmd_channel.args
|
||||
|
||||
self.uname = self.hub.asrv.iacct.get(cmd_channel.password, "*")
|
||||
self.uname = cmd_channel.uname
|
||||
|
||||
self.cwd = "/" # pyftpdlib convention of leading slash
|
||||
self.root = "/var/lib/empty"
|
||||
@@ -122,16 +142,36 @@ class FtpFs(AbstractedFS):
|
||||
w: bool = False,
|
||||
m: bool = False,
|
||||
d: bool = False,
|
||||
) -> str:
|
||||
) -> tuple[str, VFS, str]:
|
||||
try:
|
||||
vpath = vpath.replace("\\", "/").lstrip("/")
|
||||
vpath = vpath.replace("\\", "/").strip("/")
|
||||
rd, fn = os.path.split(vpath)
|
||||
if ANYWIN and relchk(rd):
|
||||
logging.warning("malicious vpath: %s", vpath)
|
||||
t = "Unsupported characters in [{}]"
|
||||
raise FSE(t.format(vpath), 1)
|
||||
|
||||
fn = sanitize_fn(fn or "", "", [".prologue.html", ".epilogue.html"])
|
||||
vpath = vjoin(rd, fn)
|
||||
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
|
||||
if not vfs.realpath:
|
||||
raise FilesystemError("no filesystem mounted at this path")
|
||||
t = "No filesystem mounted at [{}]"
|
||||
raise FSE(t.format(vpath))
|
||||
|
||||
return os.path.join(vfs.realpath, rem)
|
||||
if "xdev" in vfs.flags or "xvol" in vfs.flags:
|
||||
ap = vfs.canonical(rem)
|
||||
avfs = vfs.chk_ap(ap)
|
||||
t = "Permission denied in [{}]"
|
||||
if not avfs:
|
||||
raise FSE(t.format(vpath), 1)
|
||||
|
||||
cr, cw, cm, cd, _, _ = avfs.can_access("", self.h.uname)
|
||||
if r and not cr or w and not cw or m and not cm or d and not cd:
|
||||
raise FSE(t.format(vpath), 1)
|
||||
|
||||
return os.path.join(vfs.realpath, rem), vfs, rem
|
||||
except Pebkac as ex:
|
||||
raise FilesystemError(str(ex))
|
||||
raise FSE(str(ex))
|
||||
|
||||
def rv2a(
|
||||
self,
|
||||
@@ -140,7 +180,7 @@ class FtpFs(AbstractedFS):
|
||||
w: bool = False,
|
||||
m: bool = False,
|
||||
d: bool = False,
|
||||
) -> str:
|
||||
) -> tuple[str, VFS, str]:
|
||||
return self.v2a(os.path.join(self.cwd, vpath), r, w, m, d)
|
||||
|
||||
def ftp2fs(self, ftppath: str) -> str:
|
||||
@@ -154,7 +194,7 @@ class FtpFs(AbstractedFS):
|
||||
def validpath(self, path: str) -> bool:
|
||||
if "/.hist/" in path:
|
||||
if "/up2k." in path or path.endswith("/dir.txt"):
|
||||
raise FilesystemError("access to this file is forbidden")
|
||||
raise FSE("Access to this file is forbidden", 1)
|
||||
|
||||
return True
|
||||
|
||||
@@ -162,7 +202,7 @@ class FtpFs(AbstractedFS):
|
||||
r = "r" in mode
|
||||
w = "w" in mode or "a" in mode or "+" in mode
|
||||
|
||||
ap = self.rv2a(filename, r, w)
|
||||
ap = self.rv2a(filename, r, w)[0]
|
||||
if w:
|
||||
try:
|
||||
st = bos.stat(ap)
|
||||
@@ -171,7 +211,7 @@ class FtpFs(AbstractedFS):
|
||||
td = 0
|
||||
|
||||
if td < -1 or td > self.args.ftp_wt:
|
||||
raise FilesystemError("cannot open existing file for writing")
|
||||
raise FSE("Cannot open existing file for writing")
|
||||
|
||||
self.validpath(ap)
|
||||
return open(fsenc(ap), mode)
|
||||
@@ -180,9 +220,17 @@ class FtpFs(AbstractedFS):
|
||||
nwd = join(self.cwd, path)
|
||||
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
|
||||
ap = vfs.canonical(rem)
|
||||
if not bos.path.isdir(ap):
|
||||
try:
|
||||
st = bos.stat(ap)
|
||||
if not stat.S_ISDIR(st.st_mode):
|
||||
raise Exception()
|
||||
except:
|
||||
# returning 550 is library-default and suitable
|
||||
raise FilesystemError("Failed to change directory")
|
||||
raise FSE("No such file or directory")
|
||||
|
||||
avfs = vfs.chk_ap(ap, st)
|
||||
if not avfs:
|
||||
raise FSE("Permission denied", 1)
|
||||
|
||||
self.cwd = nwd
|
||||
(
|
||||
@@ -192,16 +240,18 @@ class FtpFs(AbstractedFS):
|
||||
self.can_delete,
|
||||
self.can_get,
|
||||
self.can_upget,
|
||||
) = self.hub.asrv.vfs.can_access(self.cwd.lstrip("/"), self.h.username)
|
||||
) = avfs.can_access("", self.h.uname)
|
||||
|
||||
def mkdir(self, path: str) -> None:
|
||||
ap = self.rv2a(path, w=True)
|
||||
bos.mkdir(ap)
|
||||
ap = self.rv2a(path, w=True)[0]
|
||||
bos.makedirs(ap) # filezilla expects this
|
||||
|
||||
def listdir(self, path: str) -> list[str]:
|
||||
vpath = join(self.cwd, path).lstrip("/")
|
||||
vpath = join(self.cwd, path)
|
||||
try:
|
||||
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, True, False)
|
||||
ap, vfs, rem = self.v2a(vpath, True, False)
|
||||
if not bos.path.isdir(ap):
|
||||
raise FSE("No such file or directory", 1)
|
||||
|
||||
fsroot, vfs_ls1, vfs_virt = vfs.ls(
|
||||
rem,
|
||||
@@ -217,8 +267,12 @@ class FtpFs(AbstractedFS):
|
||||
|
||||
vfs_ls.sort()
|
||||
return vfs_ls
|
||||
except:
|
||||
if vpath:
|
||||
except Exception as ex:
|
||||
# panic on malicious names
|
||||
if getattr(ex, "severity", 0):
|
||||
raise
|
||||
|
||||
if vpath.strip("/"):
|
||||
# display write-only folders as empty
|
||||
return []
|
||||
|
||||
@@ -227,43 +281,49 @@ class FtpFs(AbstractedFS):
|
||||
return list(sorted(list(r.keys())))
|
||||
|
||||
def rmdir(self, path: str) -> None:
|
||||
ap = self.rv2a(path, d=True)
|
||||
bos.rmdir(ap)
|
||||
ap = self.rv2a(path, d=True)[0]
|
||||
try:
|
||||
bos.rmdir(ap)
|
||||
except OSError as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
|
||||
def remove(self, path: str) -> None:
|
||||
if self.args.no_del:
|
||||
raise FilesystemError("the delete feature is disabled in server config")
|
||||
raise FSE("The delete feature is disabled in server config")
|
||||
|
||||
vp = join(self.cwd, path).lstrip("/")
|
||||
try:
|
||||
self.hub.up2k.handle_rm(self.uname, self.h.remote_ip, [vp], [])
|
||||
self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [], False)
|
||||
except Exception as ex:
|
||||
raise FilesystemError(str(ex))
|
||||
raise FSE(str(ex))
|
||||
|
||||
def rename(self, src: str, dst: str) -> None:
|
||||
if not self.can_move:
|
||||
raise FilesystemError("not allowed for user " + self.h.username)
|
||||
raise FSE("Not allowed for user " + self.h.uname)
|
||||
|
||||
if self.args.no_mv:
|
||||
t = "the rename/move feature is disabled in server config"
|
||||
raise FilesystemError(t)
|
||||
raise FSE("The rename/move feature is disabled in server config")
|
||||
|
||||
svp = join(self.cwd, src).lstrip("/")
|
||||
dvp = join(self.cwd, dst).lstrip("/")
|
||||
try:
|
||||
self.hub.up2k.handle_mv(self.uname, svp, dvp)
|
||||
except Exception as ex:
|
||||
raise FilesystemError(str(ex))
|
||||
raise FSE(str(ex))
|
||||
|
||||
def chmod(self, path: str, mode: str) -> None:
|
||||
pass
|
||||
|
||||
def stat(self, path: str) -> os.stat_result:
|
||||
try:
|
||||
ap = self.rv2a(path, r=True)
|
||||
ap = self.rv2a(path, r=True)[0]
|
||||
return bos.stat(ap)
|
||||
except:
|
||||
ap = self.rv2a(path)
|
||||
except FSE as ex:
|
||||
if ex.severity:
|
||||
raise
|
||||
|
||||
ap = self.rv2a(path)[0]
|
||||
st = bos.stat(ap)
|
||||
if not stat.S_ISDIR(st.st_mode):
|
||||
raise
|
||||
@@ -271,44 +331,50 @@ class FtpFs(AbstractedFS):
|
||||
return st
|
||||
|
||||
def utime(self, path: str, timeval: float) -> None:
|
||||
ap = self.rv2a(path, w=True)
|
||||
ap = self.rv2a(path, w=True)[0]
|
||||
return bos.utime(ap, (timeval, timeval))
|
||||
|
||||
def lstat(self, path: str) -> os.stat_result:
|
||||
ap = self.rv2a(path)
|
||||
ap = self.rv2a(path)[0]
|
||||
return bos.stat(ap)
|
||||
|
||||
def isfile(self, path: str) -> bool:
|
||||
try:
|
||||
st = self.stat(path)
|
||||
return stat.S_ISREG(st.st_mode)
|
||||
except:
|
||||
except Exception as ex:
|
||||
if getattr(ex, "severity", 0):
|
||||
raise
|
||||
|
||||
return False # expected for mojibake in ftp_SIZE()
|
||||
|
||||
def islink(self, path: str) -> bool:
|
||||
ap = self.rv2a(path)
|
||||
ap = self.rv2a(path)[0]
|
||||
return bos.path.islink(ap)
|
||||
|
||||
def isdir(self, path: str) -> bool:
|
||||
try:
|
||||
st = self.stat(path)
|
||||
return stat.S_ISDIR(st.st_mode)
|
||||
except:
|
||||
except Exception as ex:
|
||||
if getattr(ex, "severity", 0):
|
||||
raise
|
||||
|
||||
return True
|
||||
|
||||
def getsize(self, path: str) -> int:
|
||||
ap = self.rv2a(path)
|
||||
ap = self.rv2a(path)[0]
|
||||
return bos.path.getsize(ap)
|
||||
|
||||
def getmtime(self, path: str) -> float:
|
||||
ap = self.rv2a(path)
|
||||
ap = self.rv2a(path)[0]
|
||||
return bos.path.getmtime(ap)
|
||||
|
||||
def realpath(self, path: str) -> str:
|
||||
return path
|
||||
|
||||
def lexists(self, path: str) -> bool:
|
||||
ap = self.rv2a(path)
|
||||
ap = self.rv2a(path)[0]
|
||||
return bos.path.lexists(ap)
|
||||
|
||||
def get_user_by_uid(self, uid: int) -> str:
|
||||
@@ -322,16 +388,21 @@ class FtpHandler(FTPHandler):
|
||||
abstracted_fs = FtpFs
|
||||
hub: "SvcHub"
|
||||
args: argparse.Namespace
|
||||
uname: str
|
||||
|
||||
def __init__(self, conn: Any, server: Any, ioloop: Any = None) -> None:
|
||||
self.hub: "SvcHub" = FtpHandler.hub
|
||||
self.args: argparse.Namespace = FtpHandler.args
|
||||
self.uname = "*"
|
||||
|
||||
if PY2:
|
||||
FTPHandler.__init__(self, conn, server, ioloop)
|
||||
else:
|
||||
super(FtpHandler, self).__init__(conn, server, ioloop)
|
||||
|
||||
cip = self.remote_ip
|
||||
self.cli_ip = cip[7:] if cip.startswith("::ffff:") else cip
|
||||
|
||||
# abspath->vpath mapping to resolve log_transfer paths
|
||||
self.vfs_map: dict[str, str] = {}
|
||||
|
||||
@@ -341,8 +412,24 @@ class FtpHandler(FTPHandler):
|
||||
def ftp_STOR(self, file: str, mode: str = "w") -> Any:
|
||||
# Optional[str]
|
||||
vp = join(self.fs.cwd, file).lstrip("/")
|
||||
ap = self.fs.v2a(vp)
|
||||
ap, vfs, rem = self.fs.v2a(vp, w=True)
|
||||
self.vfs_map[ap] = vp
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu and not runhook(
|
||||
None,
|
||||
xbu,
|
||||
ap,
|
||||
vfs.canonical(rem),
|
||||
"",
|
||||
self.uname,
|
||||
0,
|
||||
0,
|
||||
self.cli_ip,
|
||||
0,
|
||||
"",
|
||||
):
|
||||
raise FSE("Upload blocked by xbu server config")
|
||||
|
||||
# print("ftp_STOR: {} {} => {}".format(vp, mode, ap))
|
||||
ret = FTPHandler.ftp_STOR(self, file, mode)
|
||||
# print("ftp_STOR: {} {} OK".format(vp, mode))
|
||||
@@ -363,15 +450,17 @@ class FtpHandler(FTPHandler):
|
||||
# print("xfer_end: {} => {}".format(ap, vp))
|
||||
if vp:
|
||||
vp, fn = os.path.split(vp)
|
||||
vfs, rem = self.hub.asrv.vfs.get(vp, self.username, False, True)
|
||||
vfs, rem = self.hub.asrv.vfs.get(vp, self.uname, False, True)
|
||||
vfs, rem = vfs.get_dbv(rem)
|
||||
self.hub.up2k.hash_file(
|
||||
vfs.realpath,
|
||||
vfs.vpath,
|
||||
vfs.flags,
|
||||
rem,
|
||||
fn,
|
||||
self.remote_ip,
|
||||
self.cli_ip,
|
||||
time.time(),
|
||||
self.uname,
|
||||
)
|
||||
|
||||
return FTPHandler.log_transfer(
|
||||
@@ -402,10 +491,10 @@ class Ftpd(object):
|
||||
h1 = SftpHandler
|
||||
except:
|
||||
t = "\nftps requires pyopenssl;\nplease run the following:\n\n {} -m pip install --user pyopenssl\n"
|
||||
print(t.format(sys.executable))
|
||||
print(t.format(pybin))
|
||||
sys.exit(1)
|
||||
|
||||
h1.certfile = os.path.join(self.args.E.cfg, "cert.pem")
|
||||
h1.certfile = self.args.cert
|
||||
h1.tls_control_required = True
|
||||
h1.tls_data_required = True
|
||||
|
||||
@@ -413,9 +502,9 @@ class Ftpd(object):
|
||||
|
||||
for h_lp in hs:
|
||||
h2, lp = h_lp
|
||||
h2.hub = hub
|
||||
h2.args = hub.args
|
||||
h2.authorizer = FtpAuth(hub)
|
||||
FtpHandler.hub = h2.hub = hub
|
||||
FtpHandler.args = h2.args = hub.args
|
||||
FtpHandler.authorizer = h2.authorizer = FtpAuth(hub)
|
||||
|
||||
if self.args.ftp_pr:
|
||||
p1, p2 = [int(x) for x in self.args.ftp_pr.split("-")]
|
||||
@@ -435,10 +524,21 @@ class Ftpd(object):
|
||||
lgr = logging.getLogger("pyftpdlib")
|
||||
lgr.setLevel(logging.DEBUG if self.args.ftpv else logging.INFO)
|
||||
|
||||
ips = self.args.i
|
||||
if "::" in ips:
|
||||
ips.append("0.0.0.0")
|
||||
|
||||
if self.args.ftp4:
|
||||
ips = [x for x in ips if ":" not in x]
|
||||
|
||||
ioloop = IOLoop()
|
||||
for ip in self.args.i:
|
||||
for ip in ips:
|
||||
for h, lp in hs:
|
||||
FTPServer((ip, int(lp)), h, ioloop)
|
||||
try:
|
||||
FTPServer((ip, int(lp)), h, ioloop)
|
||||
except:
|
||||
if ip != "0.0.0.0" or "::" not in ips:
|
||||
raise
|
||||
|
||||
Daemon(ioloop.loop, "ftp")
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -54,7 +54,6 @@ class HttpConn(object):
|
||||
self.args: argparse.Namespace = hsrv.args # mypy404
|
||||
self.E: EnvParams = self.args.E
|
||||
self.asrv: AuthSrv = hsrv.asrv # mypy404
|
||||
self.cert_path = hsrv.cert_path
|
||||
self.u2fh: Util.FHC = hsrv.u2fh # mypy404
|
||||
self.iphash: HMaccas = hsrv.broker.iphash
|
||||
self.bans: dict[str, int] = hsrv.bans
|
||||
@@ -103,17 +102,18 @@ class HttpConn(object):
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.log_func(self.log_src, msg, c)
|
||||
|
||||
def get_u2idx(self) -> U2idx:
|
||||
# one u2idx per tcp connection;
|
||||
def get_u2idx(self) -> Optional[U2idx]:
|
||||
# grab from a pool of u2idx instances;
|
||||
# sqlite3 fully parallelizes under python threads
|
||||
# but avoid running out of FDs by creating too many
|
||||
if not self.u2idx:
|
||||
self.u2idx = U2idx(self)
|
||||
self.u2idx = self.hsrv.get_u2idx(str(self.addr))
|
||||
|
||||
return self.u2idx
|
||||
|
||||
def _detect_https(self) -> bool:
|
||||
method = None
|
||||
if self.cert_path:
|
||||
if True:
|
||||
try:
|
||||
method = self.s.recv(4, socket.MSG_PEEK)
|
||||
except socket.timeout:
|
||||
@@ -147,7 +147,7 @@ class HttpConn(object):
|
||||
self.sr = None
|
||||
if self.args.https_only:
|
||||
is_https = True
|
||||
elif self.args.http_only or not HAVE_SSL:
|
||||
elif self.args.http_only:
|
||||
is_https = False
|
||||
else:
|
||||
# raise Exception("asdf")
|
||||
@@ -161,7 +161,7 @@ class HttpConn(object):
|
||||
self.log_src = self.log_src.replace("[36m", "[35m")
|
||||
try:
|
||||
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
ctx.load_cert_chain(self.cert_path)
|
||||
ctx.load_cert_chain(self.args.cert)
|
||||
if self.args.ssl_ver:
|
||||
ctx.options &= ~self.args.ssl_flags_en
|
||||
ctx.options |= self.args.ssl_flags_de
|
||||
@@ -215,3 +215,7 @@ class HttpConn(object):
|
||||
self.cli = HttpCli(self)
|
||||
if not self.cli.run():
|
||||
return
|
||||
|
||||
if self.u2idx:
|
||||
self.hsrv.put_u2idx(str(self.addr), self.u2idx)
|
||||
self.u2idx = None
|
||||
|
||||
@@ -11,9 +11,19 @@ import time
|
||||
|
||||
import queue
|
||||
|
||||
from .__init__ import ANYWIN, CORES, EXE, MACOS, TYPE_CHECKING, EnvParams
|
||||
|
||||
try:
|
||||
MNFE = ModuleNotFoundError
|
||||
except:
|
||||
MNFE = ImportError
|
||||
|
||||
try:
|
||||
import jinja2
|
||||
except ImportError:
|
||||
except MNFE:
|
||||
if EXE:
|
||||
raise
|
||||
|
||||
print(
|
||||
"""\033[1;31m
|
||||
you do not have jinja2 installed,\033[33m
|
||||
@@ -23,14 +33,30 @@ except ImportError:
|
||||
* (try another python version, if you have one)
|
||||
* (try copyparty.sfx instead)
|
||||
""".format(
|
||||
os.path.basename(sys.executable)
|
||||
sys.executable
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
except SyntaxError:
|
||||
if EXE:
|
||||
raise
|
||||
|
||||
print(
|
||||
"""\033[1;31m
|
||||
your jinja2 version is incompatible with your python version;\033[33m
|
||||
please try to replace it with an older version:\033[0m
|
||||
* {} -m pip install --user jinja2==2.11.3
|
||||
* (try another python version, if you have one)
|
||||
* (try copyparty.sfx instead)
|
||||
""".format(
|
||||
sys.executable
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
from .__init__ import ANYWIN, MACOS, TYPE_CHECKING, EnvParams
|
||||
from .bos import bos
|
||||
from .httpconn import HttpConn
|
||||
from .u2idx import U2idx
|
||||
from .util import (
|
||||
E_SCK,
|
||||
FHC,
|
||||
@@ -102,6 +128,9 @@ class HttpSrv(object):
|
||||
self.cb_ts = 0.0
|
||||
self.cb_v = ""
|
||||
|
||||
self.u2idx_free: dict[str, U2idx] = {}
|
||||
self.u2idx_n = 0
|
||||
|
||||
env = jinja2.Environment()
|
||||
env.loader = jinja2.FileSystemLoader(os.path.join(self.E.mod, "web"))
|
||||
jn = ["splash", "svcs", "browser", "browser2", "msg", "md", "mde", "cf"]
|
||||
@@ -119,12 +148,6 @@ class HttpSrv(object):
|
||||
|
||||
self.ssdp = SSDPr(broker)
|
||||
|
||||
cert_path = os.path.join(self.E.cfg, "cert.pem")
|
||||
if bos.path.exists(cert_path):
|
||||
self.cert_path = cert_path
|
||||
else:
|
||||
self.cert_path = ""
|
||||
|
||||
if self.tp_q:
|
||||
self.start_threads(4)
|
||||
|
||||
@@ -436,6 +459,9 @@ class HttpSrv(object):
|
||||
self.clients.remove(cli)
|
||||
self.ncli -= 1
|
||||
|
||||
if cli.u2idx:
|
||||
self.put_u2idx(str(addr), cli.u2idx)
|
||||
|
||||
def cachebuster(self) -> str:
|
||||
if time.time() - self.cb_ts < 1:
|
||||
return self.cb_v
|
||||
@@ -457,3 +483,31 @@ class HttpSrv(object):
|
||||
self.cb_v = v.decode("ascii")[-4:]
|
||||
self.cb_ts = time.time()
|
||||
return self.cb_v
|
||||
|
||||
def get_u2idx(self, ident: str) -> Optional[U2idx]:
|
||||
utab = self.u2idx_free
|
||||
for _ in range(100): # 5/0.05 = 5sec
|
||||
with self.mutex:
|
||||
if utab:
|
||||
if ident in utab:
|
||||
return utab.pop(ident)
|
||||
|
||||
return utab.pop(list(utab.keys())[0])
|
||||
|
||||
if self.u2idx_n < CORES:
|
||||
self.u2idx_n += 1
|
||||
return U2idx(self)
|
||||
|
||||
time.sleep(0.05)
|
||||
# not using conditional waits, on a hunch that
|
||||
# average performance will be faster like this
|
||||
# since most servers won't be fully saturated
|
||||
|
||||
return None
|
||||
|
||||
def put_u2idx(self, ident: str, u2idx: U2idx) -> None:
|
||||
with self.mutex:
|
||||
while ident in self.u2idx_free:
|
||||
ident += "a"
|
||||
|
||||
self.u2idx_free[ident] = u2idx
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import errno
|
||||
import random
|
||||
import select
|
||||
import socket
|
||||
@@ -11,6 +12,7 @@ from ipaddress import IPv4Network, IPv6Network
|
||||
from .__init__ import TYPE_CHECKING
|
||||
from .__init__ import unicode as U
|
||||
from .multicast import MC_Sck, MCast
|
||||
from .stolen.dnslib import AAAA
|
||||
from .stolen.dnslib import CLASS as DC
|
||||
from .stolen.dnslib import (
|
||||
NSEC,
|
||||
@@ -20,7 +22,6 @@ from .stolen.dnslib import (
|
||||
SRV,
|
||||
TXT,
|
||||
A,
|
||||
AAAA,
|
||||
DNSHeader,
|
||||
DNSQuestion,
|
||||
DNSRecord,
|
||||
@@ -277,6 +278,18 @@ class MDNS(MCast):
|
||||
zf = time.time() + 2
|
||||
self.probing = zf # cant unicast so give everyone an extra sec
|
||||
self.unsolicited = [zf, zf + 1, zf + 3, zf + 7] # rfc-8.3
|
||||
|
||||
try:
|
||||
self.run2()
|
||||
except OSError as ex:
|
||||
if ex.errno != errno.EBADF:
|
||||
raise
|
||||
|
||||
self.log("stopping due to {}".format(ex), "90")
|
||||
|
||||
self.log("stopped", 2)
|
||||
|
||||
def run2(self) -> None:
|
||||
last_hop = time.time()
|
||||
ihop = self.args.mc_hop
|
||||
while self.running:
|
||||
@@ -314,8 +327,6 @@ class MDNS(MCast):
|
||||
self.log(t.format(self.hn[:-1]), 2)
|
||||
self.probing = 0
|
||||
|
||||
self.log("stopped", 2)
|
||||
|
||||
def stop(self, panic=False) -> None:
|
||||
self.running = False
|
||||
for srv in self.srv.values():
|
||||
|
||||
@@ -8,9 +8,19 @@ import shutil
|
||||
import subprocess as sp
|
||||
import sys
|
||||
|
||||
from .__init__ import PY2, WINDOWS, E, unicode
|
||||
from .__init__ import EXE, PY2, WINDOWS, E, unicode
|
||||
from .bos import bos
|
||||
from .util import REKOBO_LKEY, fsenc, min_ex, retchk, runcmd, uncyg
|
||||
from .util import (
|
||||
FFMPEG_URL,
|
||||
REKOBO_LKEY,
|
||||
fsenc,
|
||||
min_ex,
|
||||
pybin,
|
||||
retchk,
|
||||
runcmd,
|
||||
sfsenc,
|
||||
uncyg,
|
||||
)
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Union
|
||||
@@ -259,7 +269,9 @@ class MTag(object):
|
||||
self.args = args
|
||||
self.usable = True
|
||||
self.prefer_mt = not args.no_mtag_ff
|
||||
self.backend = "ffprobe" if args.no_mutagen else "mutagen"
|
||||
self.backend = (
|
||||
"ffprobe" if args.no_mutagen or (HAVE_FFPROBE and EXE) else "mutagen"
|
||||
)
|
||||
self.can_ffprobe = HAVE_FFPROBE and not args.no_mtag_ff
|
||||
mappings = args.mtm
|
||||
or_ffprobe = " or FFprobe"
|
||||
@@ -285,9 +297,14 @@ class MTag(object):
|
||||
self.log(msg, c=3)
|
||||
|
||||
if not self.usable:
|
||||
if EXE:
|
||||
t = "copyparty.exe cannot use mutagen; need ffprobe.exe to read media tags: "
|
||||
self.log(t + FFMPEG_URL)
|
||||
return
|
||||
|
||||
msg = "need Mutagen{} to read media tags so please run this:\n{}{} -m pip install --user mutagen\n"
|
||||
pybin = os.path.basename(sys.executable)
|
||||
self.log(msg.format(or_ffprobe, " " * 37, pybin), c=1)
|
||||
pyname = os.path.basename(pybin)
|
||||
self.log(msg.format(or_ffprobe, " " * 37, pyname), c=1)
|
||||
return
|
||||
|
||||
# https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
|
||||
@@ -456,7 +473,10 @@ class MTag(object):
|
||||
self.log("mutagen: {}\033[0m".format(" ".join(zl)), "90")
|
||||
if not md.info.length and not md.info.codec:
|
||||
raise Exception()
|
||||
except:
|
||||
except Exception as ex:
|
||||
if self.args.mtag_v:
|
||||
self.log("mutagen-err [{}] @ [{}]".format(ex, abspath), "90")
|
||||
|
||||
return self.get_ffprobe(abspath) if self.can_ffprobe else {}
|
||||
|
||||
sz = bos.path.getsize(abspath)
|
||||
@@ -519,12 +539,15 @@ class MTag(object):
|
||||
|
||||
env = os.environ.copy()
|
||||
try:
|
||||
if EXE:
|
||||
raise Exception()
|
||||
|
||||
pypath = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||
zsl = [str(pypath)] + [str(x) for x in sys.path if x]
|
||||
pypath = str(os.pathsep.join(zsl))
|
||||
env["PYTHONPATH"] = pypath
|
||||
except:
|
||||
if not E.ox:
|
||||
if not E.ox and not EXE:
|
||||
raise
|
||||
|
||||
ret: dict[str, Any] = {}
|
||||
@@ -532,7 +555,7 @@ class MTag(object):
|
||||
try:
|
||||
cmd = [parser.bin, abspath]
|
||||
if parser.bin.endswith(".py"):
|
||||
cmd = [sys.executable] + cmd
|
||||
cmd = [pybin] + cmd
|
||||
|
||||
args = {
|
||||
"env": env,
|
||||
@@ -551,7 +574,7 @@ class MTag(object):
|
||||
else:
|
||||
cmd = ["nice"] + cmd
|
||||
|
||||
bcmd = [fsenc(x) for x in cmd]
|
||||
bcmd = [sfsenc(x) for x in cmd[:-1]] + [fsenc(cmd[-1])]
|
||||
rc, v, err = runcmd(bcmd, **args) # type: ignore
|
||||
retchk(rc, bcmd, err, self.log, 5, self.args.mtag_v)
|
||||
v = v.strip()
|
||||
|
||||
0
copyparty/res/__init__.py
Normal file
0
copyparty/res/__init__.py
Normal file
@@ -9,13 +9,13 @@ import sys
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
|
||||
from .__init__ import ANYWIN, TYPE_CHECKING
|
||||
from .__init__ import ANYWIN, EXE, TYPE_CHECKING
|
||||
from .authsrv import LEELOO_DALLAS, VFS
|
||||
from .bos import bos
|
||||
from .util import Daemon, min_ex
|
||||
from .util import Daemon, min_ex, pybin, runhook
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any
|
||||
from typing import Any, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .svchub import SvcHub
|
||||
@@ -42,8 +42,12 @@ class SMB(object):
|
||||
from impacket import smbserver
|
||||
from impacket.ntlm import compute_lmhash, compute_nthash
|
||||
except ImportError:
|
||||
if EXE:
|
||||
print("copyparty.exe cannot do SMB")
|
||||
sys.exit(1)
|
||||
|
||||
m = "\033[36m\n{}\033[31m\n\nERROR: need 'impacket'; please run this command:\033[33m\n {} -m pip install --user impacket\n\033[0m"
|
||||
print(m.format(min_ex(), sys.executable))
|
||||
print(m.format(min_ex(), pybin))
|
||||
sys.exit(1)
|
||||
|
||||
# patch vfs into smbserver.os
|
||||
@@ -109,6 +113,9 @@ class SMB(object):
|
||||
self.stop = srv.stop
|
||||
self.log("smb", "listening @ {}:{}".format(ip, port))
|
||||
|
||||
def nlog(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.log("smb", msg, c)
|
||||
|
||||
def start(self) -> None:
|
||||
Daemon(self.srv.start)
|
||||
|
||||
@@ -165,8 +172,15 @@ class SMB(object):
|
||||
yeet("blocked write (no --smbw): " + vpath)
|
||||
|
||||
vfs, ap = self._v2a("open", vpath, *a)
|
||||
if wr and not vfs.axs.uwrite:
|
||||
yeet("blocked write (no-write-acc): " + vpath)
|
||||
if wr:
|
||||
if not vfs.axs.uwrite:
|
||||
yeet("blocked write (no-write-acc): " + vpath)
|
||||
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu and not runhook(
|
||||
self.nlog, xbu, ap, vpath, "", "", 0, 0, "1.7.6.2", 0, ""
|
||||
):
|
||||
yeet("blocked by xbu server config: " + vpath)
|
||||
|
||||
ret = bos.open(ap, flags, *a, mode=chmod, **ka)
|
||||
if wr:
|
||||
@@ -194,11 +208,13 @@ class SMB(object):
|
||||
vfs, rem = vfs.get_dbv(rem)
|
||||
self.hub.up2k.hash_file(
|
||||
vfs.realpath,
|
||||
vfs.vpath,
|
||||
vfs.flags,
|
||||
rem,
|
||||
fn,
|
||||
"1.7.6.2",
|
||||
time.time(),
|
||||
"",
|
||||
)
|
||||
|
||||
def _rename(self, vp1: str, vp2: str) -> None:
|
||||
@@ -245,7 +261,7 @@ class SMB(object):
|
||||
yeet("blocked delete (no-del-acc): " + vpath)
|
||||
|
||||
vpath = vpath.replace("\\", "/").lstrip("/")
|
||||
self.hub.up2k.handle_rm(LEELOO_DALLAS, "1.7.6.2", [vpath], [])
|
||||
self.hub.up2k.handle_rm(LEELOO_DALLAS, "1.7.6.2", [vpath], [], False)
|
||||
|
||||
def _utime(self, vpath: str, times: tuple[float, float]) -> None:
|
||||
if not self.args.smbw:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import errno
|
||||
import re
|
||||
import select
|
||||
import socket
|
||||
@@ -8,7 +9,7 @@ from email.utils import formatdate
|
||||
|
||||
from .__init__ import TYPE_CHECKING
|
||||
from .multicast import MC_Sck, MCast
|
||||
from .util import CachedSet, min_ex, html_escape
|
||||
from .util import CachedSet, html_escape, min_ex
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .broker_util import BrokerCli
|
||||
@@ -129,6 +130,17 @@ class SSDPd(MCast):
|
||||
srv.hport = hp
|
||||
|
||||
self.log("listening")
|
||||
try:
|
||||
self.run2()
|
||||
except OSError as ex:
|
||||
if ex.errno != errno.EBADF:
|
||||
raise
|
||||
|
||||
self.log("stopping due to {}".format(ex), "90")
|
||||
|
||||
self.log("stopped", 2)
|
||||
|
||||
def run2(self) -> None:
|
||||
while self.running:
|
||||
rdy = select.select(self.srv, [], [], self.args.z_chk or 180)
|
||||
rx: list[socket.socket] = rdy[0] # type: ignore
|
||||
@@ -148,8 +160,6 @@ class SSDPd(MCast):
|
||||
)
|
||||
self.log(t, 6)
|
||||
|
||||
self.log("stopped", 2)
|
||||
|
||||
def stop(self) -> None:
|
||||
self.running = False
|
||||
for srv in self.srv.values():
|
||||
|
||||
@@ -28,13 +28,14 @@ if True: # pylint: disable=using-constant-test
|
||||
import typing
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from .__init__ import ANYWIN, MACOS, TYPE_CHECKING, VT100, EnvParams, unicode
|
||||
from .__init__ import ANYWIN, EXE, MACOS, TYPE_CHECKING, EnvParams, unicode
|
||||
from .authsrv import AuthSrv
|
||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE
|
||||
from .tcpsrv import TcpSrv
|
||||
from .th_srv import HAVE_PIL, HAVE_VIPS, HAVE_WEBP, ThumbSrv
|
||||
from .up2k import Up2k
|
||||
from .util import (
|
||||
FFMPEG_URL,
|
||||
VERSIONS,
|
||||
Daemon,
|
||||
Garda,
|
||||
@@ -44,6 +45,7 @@ from .util import (
|
||||
ansi_re,
|
||||
min_ex,
|
||||
mp,
|
||||
pybin,
|
||||
start_log_thrs,
|
||||
start_stackmon,
|
||||
)
|
||||
@@ -67,10 +69,18 @@ class SvcHub(object):
|
||||
put() can return a queue (if want_reply=True) which has a blocking get() with the response.
|
||||
"""
|
||||
|
||||
def __init__(self, args: argparse.Namespace, argv: list[str], printed: str) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
args: argparse.Namespace,
|
||||
dargs: argparse.Namespace,
|
||||
argv: list[str],
|
||||
printed: str,
|
||||
) -> None:
|
||||
self.args = args
|
||||
self.dargs = dargs
|
||||
self.argv = argv
|
||||
self.E: EnvParams = args.E
|
||||
self.no_ansi = args.no_ansi
|
||||
self.logf: Optional[typing.TextIO] = None
|
||||
self.logf_base_fn = ""
|
||||
self.stop_req = False
|
||||
@@ -119,6 +129,9 @@ class SvcHub(object):
|
||||
args.no_robots = True
|
||||
args.force_js = True
|
||||
|
||||
if not self._process_config():
|
||||
raise Exception("bad config")
|
||||
|
||||
self.log = self._log_disabled if args.q else self._log_enabled
|
||||
if args.lo:
|
||||
self._setup_logfile(printed)
|
||||
@@ -140,12 +153,9 @@ class SvcHub(object):
|
||||
self.log("root", t.format(args.j))
|
||||
|
||||
if not args.no_fpool and args.j != 1:
|
||||
t = "WARNING: --use-fpool combined with multithreading is untested and can probably cause undefined behavior"
|
||||
if ANYWIN:
|
||||
t = 'windows cannot do multithreading without --no-fpool, so enabling that -- note that upload performance will suffer if you have microsoft defender "real-time protection" enabled, so you probably want to use -j 1 instead'
|
||||
args.no_fpool = True
|
||||
|
||||
self.log("root", t, c=3)
|
||||
t = "WARNING: ignoring --use-fpool because multithreading (-j{}) is enabled"
|
||||
self.log("root", t.format(args.j), c=3)
|
||||
args.no_fpool = True
|
||||
|
||||
bri = "zy"[args.theme % 2 :][:1]
|
||||
ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
|
||||
@@ -155,7 +165,14 @@ class SvcHub(object):
|
||||
args.log_fk = re.compile(args.log_fk)
|
||||
|
||||
# initiate all services to manage
|
||||
self.asrv = AuthSrv(self.args, self.log)
|
||||
self.asrv = AuthSrv(self.args, self.log, dargs=self.dargs)
|
||||
|
||||
if args.cgen:
|
||||
self.asrv.cgen()
|
||||
|
||||
if args.exit == "cfg":
|
||||
sys.exit(0)
|
||||
|
||||
if args.ls:
|
||||
self.asrv.dbg_ls()
|
||||
|
||||
@@ -164,9 +181,6 @@ class SvcHub(object):
|
||||
|
||||
self.log("root", "max clients: {}".format(self.args.nc))
|
||||
|
||||
if not self._process_config():
|
||||
raise Exception("bad config")
|
||||
|
||||
self.tcpsrv = TcpSrv(self)
|
||||
self.up2k = Up2k(self)
|
||||
|
||||
@@ -180,6 +194,7 @@ class SvcHub(object):
|
||||
|
||||
self.args.th_dec = list(decs.keys())
|
||||
self.thumbsrv = None
|
||||
want_ff = False
|
||||
if not args.no_thumb:
|
||||
t = ", ".join(self.args.th_dec) or "(None available)"
|
||||
self.log("thumb", "decoder preference: {}".format(t))
|
||||
@@ -191,8 +206,12 @@ class SvcHub(object):
|
||||
if self.args.th_dec:
|
||||
self.thumbsrv = ThumbSrv(self)
|
||||
else:
|
||||
want_ff = True
|
||||
msg = "need either Pillow, pyvips, or FFmpeg to create thumbnails; for example:\n{0}{1} -m pip install --user Pillow\n{0}{1} -m pip install --user pyvips\n{0}apt install ffmpeg"
|
||||
msg = msg.format(" " * 37, os.path.basename(sys.executable))
|
||||
msg = msg.format(" " * 37, os.path.basename(pybin))
|
||||
if EXE:
|
||||
msg = "copyparty.exe cannot use Pillow or pyvips; need ffprobe.exe and ffmpeg.exe to create thumbnails"
|
||||
|
||||
self.log("thumb", msg, c=3)
|
||||
|
||||
if not args.no_acode and args.no_thumb:
|
||||
@@ -204,6 +223,10 @@ class SvcHub(object):
|
||||
msg = "setting --no-acode because either FFmpeg or FFprobe is not available"
|
||||
self.log("thumb", msg, c=6)
|
||||
args.no_acode = True
|
||||
want_ff = True
|
||||
|
||||
if want_ff and ANYWIN:
|
||||
self.log("thumb", "download FFmpeg to fix it:\033[0m " + FFMPEG_URL, 3)
|
||||
|
||||
args.th_poke = min(args.th_poke, args.th_maxage, args.ac_maxage)
|
||||
|
||||
@@ -323,6 +346,24 @@ class SvcHub(object):
|
||||
al.RS = R + "/" if R else ""
|
||||
al.SRS = "/" + R + "/" if R else "/"
|
||||
|
||||
if al.rsp_jtr:
|
||||
al.rsp_slp = 0.000001
|
||||
|
||||
al.th_covers = set(al.th_covers.split(","))
|
||||
|
||||
for k in "c".split(" "):
|
||||
vl = getattr(al, k)
|
||||
if not vl:
|
||||
continue
|
||||
|
||||
vl = [os.path.expanduser(x) if x.startswith("~") else x for x in vl]
|
||||
setattr(al, k, vl)
|
||||
|
||||
for k in "lo hist ssl_log".split(" "):
|
||||
vs = getattr(al, k)
|
||||
if vs and vs.startswith("~"):
|
||||
setattr(al, k, os.path.expanduser(vs))
|
||||
|
||||
return True
|
||||
|
||||
def _setlimits(self) -> None:
|
||||
@@ -377,6 +418,7 @@ class SvcHub(object):
|
||||
|
||||
def _setup_logfile(self, printed: str) -> None:
|
||||
base_fn = fn = sel_fn = self._logname()
|
||||
do_xz = fn.lower().endswith(".xz")
|
||||
if fn != self.args.lo:
|
||||
ctr = 0
|
||||
# yup this is a race; if started sufficiently concurrently, two
|
||||
@@ -388,7 +430,7 @@ class SvcHub(object):
|
||||
fn = sel_fn
|
||||
|
||||
try:
|
||||
if fn.lower().endswith(".xz"):
|
||||
if do_xz:
|
||||
import lzma
|
||||
|
||||
lh = lzma.open(fn, "wt", encoding="utf-8", errors="replace", preset=0)
|
||||
@@ -399,7 +441,7 @@ class SvcHub(object):
|
||||
|
||||
lh = codecs.open(fn, "w", encoding="utf-8", errors="replace")
|
||||
|
||||
argv = [sys.executable] + self.argv
|
||||
argv = [pybin] + self.argv
|
||||
if hasattr(shlex, "quote"):
|
||||
argv = [shlex.quote(x) for x in argv]
|
||||
else:
|
||||
@@ -606,8 +648,14 @@ class SvcHub(object):
|
||||
return
|
||||
|
||||
with self.log_mutex:
|
||||
ts = datetime.utcnow().strftime("%Y-%m%d-%H%M%S.%f")[:-3]
|
||||
self.logf.write("@{} [{}\033[0m] {}\n".format(ts, src, msg))
|
||||
zd = datetime.utcnow()
|
||||
ts = "%04d-%04d-%06d.%03d" % (
|
||||
zd.year,
|
||||
zd.month * 100 + zd.day,
|
||||
(zd.hour * 100 + zd.minute) * 100 + zd.second,
|
||||
zd.microsecond // 1000,
|
||||
)
|
||||
self.logf.write("@%s [%s\033[0m] %s\n" % (ts, src, msg))
|
||||
|
||||
now = time.time()
|
||||
if now >= self.next_day:
|
||||
@@ -634,26 +682,36 @@ class SvcHub(object):
|
||||
now = time.time()
|
||||
if now >= self.next_day:
|
||||
dt = datetime.utcfromtimestamp(now)
|
||||
print("\033[36m{}\033[0m\n".format(dt.strftime("%Y-%m-%d")), end="")
|
||||
zs = "{}\n" if self.no_ansi else "\033[36m{}\033[0m\n"
|
||||
zs = zs.format(dt.strftime("%Y-%m-%d"))
|
||||
print(zs, end="")
|
||||
self._set_next_day()
|
||||
if self.logf:
|
||||
self.logf.write(zs)
|
||||
|
||||
fmt = "\033[36m{} \033[33m{:21} \033[0m{}\n"
|
||||
if not VT100:
|
||||
fmt = "{} {:21} {}\n"
|
||||
fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n"
|
||||
if self.no_ansi:
|
||||
fmt = "%s %-21s %s\n"
|
||||
if "\033" in msg:
|
||||
msg = ansi_re.sub("", msg)
|
||||
if "\033" in src:
|
||||
src = ansi_re.sub("", src)
|
||||
elif c:
|
||||
if isinstance(c, int):
|
||||
msg = "\033[3{}m{}\033[0m".format(c, msg)
|
||||
msg = "\033[3%sm%s\033[0m" % (c, msg)
|
||||
elif "\033" not in c:
|
||||
msg = "\033[{}m{}\033[0m".format(c, msg)
|
||||
msg = "\033[%sm%s\033[0m" % (c, msg)
|
||||
else:
|
||||
msg = "{}{}\033[0m".format(c, msg)
|
||||
msg = "%s%s\033[0m" % (c, msg)
|
||||
|
||||
ts = datetime.utcfromtimestamp(now).strftime("%H:%M:%S.%f")[:-3]
|
||||
msg = fmt.format(ts, src, msg)
|
||||
zd = datetime.utcfromtimestamp(now)
|
||||
ts = "%02d:%02d:%02d.%03d" % (
|
||||
zd.hour,
|
||||
zd.minute,
|
||||
zd.second,
|
||||
zd.microsecond // 1000,
|
||||
)
|
||||
msg = fmt % (ts, src, msg)
|
||||
try:
|
||||
print(msg, end="")
|
||||
except UnicodeEncodeError:
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import calendar
|
||||
import time
|
||||
import stat
|
||||
import time
|
||||
import zlib
|
||||
|
||||
from .bos import bos
|
||||
|
||||
@@ -7,8 +7,9 @@ import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, VT100, unicode
|
||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
|
||||
from .stolen.qrcodegen import QrCode
|
||||
from .cert import gencert
|
||||
from .util import (
|
||||
E_ACCESS,
|
||||
E_ADDR_IN_USE,
|
||||
@@ -295,6 +296,7 @@ class TcpSrv(object):
|
||||
def _distribute_netdevs(self):
|
||||
self.hub.broker.say("set_netdevs", self.netdevs)
|
||||
self.hub.start_zeroconf()
|
||||
gencert(self.log, self.args, self.netdevs)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self.stopping = True
|
||||
@@ -322,7 +324,7 @@ class TcpSrv(object):
|
||||
if k not in netdevs:
|
||||
removed = "{} = {}".format(k, v)
|
||||
|
||||
t = "network change detected:\n added {}\nremoved {}"
|
||||
t = "network change detected:\n added {}\033[0;33m\nremoved {}"
|
||||
self.log("tcpsrv", t.format(added, removed), 3)
|
||||
self.netdevs = netdevs
|
||||
self._distribute_netdevs()
|
||||
@@ -501,7 +503,7 @@ class TcpSrv(object):
|
||||
zoom = 1
|
||||
|
||||
qr = qrc.render(zoom, pad)
|
||||
if not VT100:
|
||||
if self.args.no_ansi:
|
||||
return "{}\n{}".format(txt, qr)
|
||||
|
||||
halfc = "\033[40;48;5;{0}m{1}\033[47;48;5;{2}m"
|
||||
|
||||
@@ -12,14 +12,16 @@ import time
|
||||
|
||||
from queue import Queue
|
||||
|
||||
from .__init__ import TYPE_CHECKING
|
||||
from .__init__ import ANYWIN, TYPE_CHECKING
|
||||
from .bos import bos
|
||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
|
||||
from .util import (
|
||||
FFMPEG_URL,
|
||||
BytesIO,
|
||||
Cooldown,
|
||||
Daemon,
|
||||
Pebkac,
|
||||
afsenc,
|
||||
fsenc,
|
||||
min_ex,
|
||||
runcmd,
|
||||
@@ -82,14 +84,14 @@ def thumb_path(histpath: str, rem: str, mtime: float, fmt: str) -> str:
|
||||
# base64 = 64 = 4096
|
||||
rd, fn = vsplit(rem)
|
||||
if rd:
|
||||
h = hashlib.sha512(fsenc(rd)).digest()
|
||||
h = hashlib.sha512(afsenc(rd)).digest()
|
||||
b64 = base64.urlsafe_b64encode(h).decode("ascii")[:24]
|
||||
rd = "{}/{}/".format(b64[:2], b64[2:4]).lower() + b64
|
||||
else:
|
||||
rd = "top"
|
||||
|
||||
# could keep original filenames but this is safer re pathlen
|
||||
h = hashlib.sha512(fsenc(fn)).digest()
|
||||
h = hashlib.sha512(afsenc(fn)).digest()
|
||||
fn = base64.urlsafe_b64encode(h).decode("ascii")[:24]
|
||||
|
||||
if fmt in ("opus", "caf"):
|
||||
@@ -133,6 +135,8 @@ class ThumbSrv(object):
|
||||
msg = "cannot create audio/video thumbnails because some of the required programs are not available: "
|
||||
msg += ", ".join(missing)
|
||||
self.log(msg, c=3)
|
||||
if ANYWIN and self.args.no_acode:
|
||||
self.log("download FFmpeg to fix it:\033[0m " + FFMPEG_URL, 3)
|
||||
|
||||
if self.args.th_clean:
|
||||
Daemon(self.cleaner, "thumb.cln")
|
||||
@@ -196,12 +200,12 @@ class ThumbSrv(object):
|
||||
self.log("wait {}".format(tpath))
|
||||
except:
|
||||
thdir = os.path.dirname(tpath)
|
||||
bos.makedirs(thdir)
|
||||
bos.makedirs(os.path.join(thdir, "w"))
|
||||
|
||||
inf_path = os.path.join(thdir, "dir.txt")
|
||||
if not bos.path.exists(inf_path):
|
||||
with open(inf_path, "wb") as f:
|
||||
f.write(fsenc(os.path.dirname(abspath)))
|
||||
f.write(afsenc(os.path.dirname(abspath)))
|
||||
|
||||
self.busy[tpath] = [cond]
|
||||
do_conv = True
|
||||
@@ -268,9 +272,16 @@ class ThumbSrv(object):
|
||||
if not png_ok and tpath.endswith(".png"):
|
||||
raise Pebkac(400, "png only allowed for waveforms")
|
||||
|
||||
tdir, tfn = os.path.split(tpath)
|
||||
ttpath = os.path.join(tdir, "w", tfn)
|
||||
try:
|
||||
bos.unlink(ttpath)
|
||||
except:
|
||||
pass
|
||||
|
||||
for fun in funs:
|
||||
try:
|
||||
fun(abspath, tpath)
|
||||
fun(abspath, ttpath)
|
||||
break
|
||||
except Exception as ex:
|
||||
msg = "{} could not create thumbnail of {}\n{}"
|
||||
@@ -279,15 +290,20 @@ class ThumbSrv(object):
|
||||
self.log(msg, c)
|
||||
if getattr(ex, "returncode", 0) != 321:
|
||||
if fun == funs[-1]:
|
||||
with open(tpath, "wb") as _:
|
||||
with open(ttpath, "wb") as _:
|
||||
pass
|
||||
else:
|
||||
# ffmpeg may spawn empty files on windows
|
||||
try:
|
||||
os.unlink(tpath)
|
||||
os.unlink(ttpath)
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
bos.rename(ttpath, tpath)
|
||||
except:
|
||||
pass
|
||||
|
||||
with self.mutex:
|
||||
subs = self.busy[tpath]
|
||||
del self.busy[tpath]
|
||||
@@ -549,13 +565,24 @@ class ThumbSrv(object):
|
||||
if "ac" not in ret:
|
||||
raise Exception("not audio")
|
||||
|
||||
try:
|
||||
dur = ret[".dur"][1]
|
||||
except:
|
||||
dur = 0
|
||||
|
||||
src_opus = abspath.lower().endswith(".opus") or ret["ac"][1] == "opus"
|
||||
want_caf = tpath.endswith(".caf")
|
||||
tmp_opus = tpath
|
||||
if want_caf:
|
||||
tmp_opus = tpath.rsplit(".", 1)[0] + ".opus"
|
||||
tmp_opus = tpath + ".opus"
|
||||
try:
|
||||
bos.unlink(tmp_opus)
|
||||
except:
|
||||
pass
|
||||
|
||||
if not want_caf or (not src_opus and not bos.path.isfile(tmp_opus)):
|
||||
caf_src = abspath if src_opus else tmp_opus
|
||||
|
||||
if not want_caf or not src_opus:
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
@@ -572,7 +599,32 @@ class ThumbSrv(object):
|
||||
# fmt: on
|
||||
self._run_ff(cmd)
|
||||
|
||||
if want_caf:
|
||||
# iOS fails to play some "insufficiently complex" files
|
||||
# (average file shorter than 8 seconds), so of course we
|
||||
# fix that by mixing in some inaudible pink noise :^)
|
||||
# 6.3 sec seems like the cutoff so lets do 7, and
|
||||
# 7 sec of psyqui-musou.opus @ 3:50 is 174 KiB
|
||||
if want_caf and (dur < 20 or bos.path.getsize(caf_src) < 256 * 1024):
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
b"-nostdin",
|
||||
b"-v", b"error",
|
||||
b"-hide_banner",
|
||||
b"-i", fsenc(abspath),
|
||||
b"-filter_complex", b"anoisesrc=a=0.001:d=7:c=pink,asplit[l][r]; [l][r]amerge[s]; [0:a:0][s]amix",
|
||||
b"-map_metadata", b"-1",
|
||||
b"-ac", b"2",
|
||||
b"-c:a", b"libopus",
|
||||
b"-b:a", b"128k",
|
||||
b"-f", b"caf",
|
||||
fsenc(tpath)
|
||||
]
|
||||
# fmt: on
|
||||
self._run_ff(cmd)
|
||||
|
||||
elif want_caf:
|
||||
# simple remux should be safe
|
||||
# fmt: off
|
||||
cmd = [
|
||||
b"ffmpeg",
|
||||
@@ -589,6 +641,12 @@ class ThumbSrv(object):
|
||||
# fmt: on
|
||||
self._run_ff(cmd)
|
||||
|
||||
if tmp_opus != tpath:
|
||||
try:
|
||||
bos.unlink(tmp_opus)
|
||||
except:
|
||||
pass
|
||||
|
||||
def poke(self, tdir: str) -> None:
|
||||
if not self.poke_cd.poke(tdir):
|
||||
return
|
||||
|
||||
@@ -34,14 +34,14 @@ if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .httpconn import HttpConn
|
||||
from .httpsrv import HttpSrv
|
||||
|
||||
|
||||
class U2idx(object):
|
||||
def __init__(self, conn: "HttpConn") -> None:
|
||||
self.log_func = conn.log_func
|
||||
self.asrv = conn.asrv
|
||||
self.args = conn.args
|
||||
def __init__(self, hsrv: "HttpSrv") -> None:
|
||||
self.log_func = hsrv.log
|
||||
self.asrv = hsrv.asrv
|
||||
self.args = hsrv.args
|
||||
self.timeout = self.args.srch_time
|
||||
|
||||
if not HAVE_SQLITE3:
|
||||
@@ -51,7 +51,7 @@ class U2idx(object):
|
||||
self.active_id = ""
|
||||
self.active_cur: Optional["sqlite3.Cursor"] = None
|
||||
self.cur: dict[str, "sqlite3.Cursor"] = {}
|
||||
self.mem_cur = sqlite3.connect(":memory:").cursor()
|
||||
self.mem_cur = sqlite3.connect(":memory:", check_same_thread=False).cursor()
|
||||
self.mem_cur.execute(r"create table a (b text)")
|
||||
|
||||
self.p_end = 0.0
|
||||
@@ -101,7 +101,8 @@ class U2idx(object):
|
||||
uri = ""
|
||||
try:
|
||||
uri = "{}?mode=ro&nolock=1".format(Path(db_path).as_uri())
|
||||
cur = sqlite3.connect(uri, 2, uri=True).cursor()
|
||||
db = sqlite3.connect(uri, 2, uri=True, check_same_thread=False)
|
||||
cur = db.cursor()
|
||||
cur.execute('pragma table_info("up")').fetchone()
|
||||
self.log("ro: {}".format(db_path))
|
||||
except:
|
||||
@@ -112,7 +113,7 @@ class U2idx(object):
|
||||
if not cur:
|
||||
# on windows, this steals the write-lock from up2k.deferred_init --
|
||||
# seen on win 10.0.17763.2686, py 3.10.4, sqlite 3.37.2
|
||||
cur = sqlite3.connect(db_path, 2).cursor()
|
||||
cur = sqlite3.connect(db_path, 2, check_same_thread=False).cursor()
|
||||
self.log("opened {}".format(db_path))
|
||||
|
||||
self.cur[ptop] = cur
|
||||
@@ -120,10 +121,10 @@ class U2idx(object):
|
||||
|
||||
def search(
|
||||
self, vols: list[tuple[str, str, dict[str, Any]]], uq: str, lim: int
|
||||
) -> tuple[list[dict[str, Any]], list[str]]:
|
||||
) -> tuple[list[dict[str, Any]], list[str], bool]:
|
||||
"""search by query params"""
|
||||
if not HAVE_SQLITE3:
|
||||
return [], []
|
||||
return [], [], False
|
||||
|
||||
q = ""
|
||||
v: Union[str, int] = ""
|
||||
@@ -275,7 +276,7 @@ class U2idx(object):
|
||||
have_up: bool,
|
||||
have_mt: bool,
|
||||
lim: int,
|
||||
) -> tuple[list[dict[str, Any]], list[str]]:
|
||||
) -> tuple[list[dict[str, Any]], list[str], bool]:
|
||||
done_flag: list[bool] = []
|
||||
self.active_id = "{:.6f}_{}".format(
|
||||
time.time(), threading.current_thread().ident
|
||||
@@ -293,6 +294,7 @@ class U2idx(object):
|
||||
self.log("qs: {!r} {!r}".format(uq, uv))
|
||||
|
||||
ret = []
|
||||
seen_rps: set[str] = set()
|
||||
lim = min(lim, int(self.args.srch_hits))
|
||||
taglist = {}
|
||||
for (vtop, ptop, flags) in vols:
|
||||
@@ -311,16 +313,21 @@ class U2idx(object):
|
||||
|
||||
sret = []
|
||||
fk = flags.get("fk")
|
||||
dots = flags.get("dotsrch")
|
||||
c = cur.execute(uq, tuple(vuv))
|
||||
for hit in c:
|
||||
w, ts, sz, rd, fn, ip, at = hit[:7]
|
||||
lim -= 1
|
||||
if lim < 0:
|
||||
break
|
||||
|
||||
if rd.startswith("//") or fn.startswith("//"):
|
||||
rd, fn = s3dec(rd, fn)
|
||||
|
||||
rp = quotep("/".join([x for x in [vtop, rd, fn] if x]))
|
||||
if not dots and "/." in ("/" + rp):
|
||||
continue
|
||||
|
||||
if rp in seen_rps:
|
||||
continue
|
||||
|
||||
if not fk:
|
||||
suf = ""
|
||||
else:
|
||||
@@ -337,8 +344,12 @@ class U2idx(object):
|
||||
)[:fk]
|
||||
)
|
||||
|
||||
rp = quotep("/".join([x for x in [vtop, rd, fn] if x])) + suf
|
||||
sret.append({"ts": int(ts), "sz": sz, "rp": rp, "w": w[:16]})
|
||||
lim -= 1
|
||||
if lim < 0:
|
||||
break
|
||||
|
||||
seen_rps.add(rp)
|
||||
sret.append({"ts": int(ts), "sz": sz, "rp": rp + suf, "w": w[:16]})
|
||||
|
||||
for hit in sret:
|
||||
w = hit["w"]
|
||||
@@ -357,17 +368,9 @@ class U2idx(object):
|
||||
done_flag.append(True)
|
||||
self.active_id = ""
|
||||
|
||||
# undupe hits from multiple metadata keys
|
||||
if len(ret) > 1:
|
||||
ret = [ret[0]] + [
|
||||
y
|
||||
for x, y in zip(ret[:-1], ret[1:])
|
||||
if x["rp"].split("?")[0] != y["rp"].split("?")[0]
|
||||
]
|
||||
|
||||
ret.sort(key=itemgetter("rp"))
|
||||
|
||||
return ret, list(taglist.keys())
|
||||
return ret, list(taglist.keys()), lim < 0
|
||||
|
||||
def terminator(self, identifier: str, done_flag: list[bool]) -> None:
|
||||
for _ in range(self.timeout):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@ import os
|
||||
import platform
|
||||
import re
|
||||
import select
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
import stat
|
||||
@@ -30,7 +31,7 @@ from email.utils import formatdate
|
||||
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
|
||||
from queue import Queue
|
||||
|
||||
from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, VT100, WINDOWS
|
||||
from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, VT100, WINDOWS
|
||||
from .__version__ import S_BUILD_DT, S_VERSION
|
||||
from .stolen import surrogateescape
|
||||
|
||||
@@ -143,6 +144,8 @@ SYMTIME = sys.version_info > (3, 6) and os.utime in os.supports_follow_symlinks
|
||||
|
||||
META_NOBOTS = '<meta name="robots" content="noindex, nofollow">'
|
||||
|
||||
FFMPEG_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z"
|
||||
|
||||
HTTPCODE = {
|
||||
200: "OK",
|
||||
201: "Created",
|
||||
@@ -290,6 +293,19 @@ REKOBO_KEY = {
|
||||
REKOBO_LKEY = {k.lower(): v for k, v in REKOBO_KEY.items()}
|
||||
|
||||
|
||||
pybin = sys.executable or ""
|
||||
if EXE:
|
||||
pybin = ""
|
||||
for zsg in "python3 python".split():
|
||||
try:
|
||||
zsg = shutil.which(zsg)
|
||||
if zsg:
|
||||
pybin = zsg
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def py_desc() -> str:
|
||||
interp = platform.python_implementation()
|
||||
py_ver = ".".join([str(x) for x in sys.version_info])
|
||||
@@ -521,7 +537,7 @@ class _Unrecv(object):
|
||||
self.log = log
|
||||
self.buf: bytes = b""
|
||||
|
||||
def recv(self, nbytes: int) -> bytes:
|
||||
def recv(self, nbytes: int, spins: int = 1) -> bytes:
|
||||
if self.buf:
|
||||
ret = self.buf[:nbytes]
|
||||
self.buf = self.buf[nbytes:]
|
||||
@@ -532,6 +548,10 @@ class _Unrecv(object):
|
||||
ret = self.s.recv(nbytes)
|
||||
break
|
||||
except socket.timeout:
|
||||
spins -= 1
|
||||
if spins <= 0:
|
||||
ret = b""
|
||||
break
|
||||
continue
|
||||
except:
|
||||
ret = b""
|
||||
@@ -574,7 +594,7 @@ class _LUnrecv(object):
|
||||
self.log = log
|
||||
self.buf = b""
|
||||
|
||||
def recv(self, nbytes: int) -> bytes:
|
||||
def recv(self, nbytes: int, spins: int) -> bytes:
|
||||
if self.buf:
|
||||
ret = self.buf[:nbytes]
|
||||
self.buf = self.buf[nbytes:]
|
||||
@@ -593,7 +613,7 @@ class _LUnrecv(object):
|
||||
def recv_ex(self, nbytes: int, raise_on_trunc: bool = True) -> bytes:
|
||||
"""read an exact number of bytes"""
|
||||
try:
|
||||
ret = self.recv(nbytes)
|
||||
ret = self.recv(nbytes, 1)
|
||||
err = False
|
||||
except:
|
||||
ret = b""
|
||||
@@ -601,7 +621,7 @@ class _LUnrecv(object):
|
||||
|
||||
while not err and len(ret) < nbytes:
|
||||
try:
|
||||
ret += self.recv(nbytes - len(ret))
|
||||
ret += self.recv(nbytes - len(ret), 1)
|
||||
except OSError:
|
||||
err = True
|
||||
|
||||
@@ -652,6 +672,7 @@ class FHC(object):
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.cache: dict[str, FHC.CE] = {}
|
||||
self.aps: set[str] = set()
|
||||
|
||||
def close(self, path: str) -> None:
|
||||
try:
|
||||
@@ -663,6 +684,7 @@ class FHC(object):
|
||||
fh.close()
|
||||
|
||||
del self.cache[path]
|
||||
self.aps.remove(path)
|
||||
|
||||
def clean(self) -> None:
|
||||
if not self.cache:
|
||||
@@ -683,6 +705,7 @@ class FHC(object):
|
||||
return self.cache[path].fhs.pop()
|
||||
|
||||
def put(self, path: str, fh: typing.BinaryIO) -> None:
|
||||
self.aps.add(path)
|
||||
try:
|
||||
ce = self.cache[path]
|
||||
ce.fhs.append(fh)
|
||||
@@ -1153,20 +1176,12 @@ def ren_open(
|
||||
fun = kwargs.pop("fun", open)
|
||||
fdir = kwargs.pop("fdir", None)
|
||||
suffix = kwargs.pop("suffix", None)
|
||||
overwrite = kwargs.pop("overwrite", None)
|
||||
|
||||
if fname == os.devnull:
|
||||
with fun(fname, *args, **kwargs) as f:
|
||||
yield {"orz": (f, fname)}
|
||||
return
|
||||
|
||||
if overwrite:
|
||||
assert fdir
|
||||
fpath = os.path.join(fdir, fname)
|
||||
with fun(fsenc(fpath), *args, **kwargs) as f:
|
||||
yield {"orz": (f, fname)}
|
||||
return
|
||||
|
||||
if suffix:
|
||||
ext = fname.split(".")[-1]
|
||||
if len(ext) < 7:
|
||||
@@ -1281,7 +1296,7 @@ class MultipartParser(object):
|
||||
rfc1341/rfc1521/rfc2047/rfc2231/rfc2388/rfc6266/the-real-world
|
||||
(only the fallback non-js uploader relies on these filenames)
|
||||
"""
|
||||
for ln in read_header(self.sr):
|
||||
for ln in read_header(self.sr, 2, 2592000):
|
||||
self.log(ln)
|
||||
|
||||
m = self.re_ctype.match(ln)
|
||||
@@ -1481,15 +1496,15 @@ def get_boundary(headers: dict[str, str]) -> str:
|
||||
return m.group(2)
|
||||
|
||||
|
||||
def read_header(sr: Unrecv) -> list[str]:
|
||||
def read_header(sr: Unrecv, t_idle: int, t_tot: int) -> list[str]:
|
||||
t0 = time.time()
|
||||
ret = b""
|
||||
while True:
|
||||
if time.time() - t0 > 120:
|
||||
if time.time() - t0 >= t_tot:
|
||||
return []
|
||||
|
||||
try:
|
||||
ret += sr.recv(1024)
|
||||
ret += sr.recv(1024, t_idle // 2)
|
||||
except:
|
||||
if not ret:
|
||||
return []
|
||||
@@ -1513,10 +1528,32 @@ def read_header(sr: Unrecv) -> list[str]:
|
||||
return ret[:ofs].decode("utf-8", "surrogateescape").lstrip("\r\n").split("\r\n")
|
||||
|
||||
|
||||
def rand_name(fdir: str, fn: str, rnd: int) -> str:
|
||||
ok = False
|
||||
try:
|
||||
ext = "." + fn.rsplit(".", 1)[1]
|
||||
except:
|
||||
ext = ""
|
||||
|
||||
for extra in range(16):
|
||||
for _ in range(16):
|
||||
if ok:
|
||||
break
|
||||
|
||||
nc = rnd + extra
|
||||
nb = int((6 + 6 * nc) / 8)
|
||||
zb = os.urandom(nb)
|
||||
zb = base64.urlsafe_b64encode(zb)
|
||||
fn = zb[:nc].decode("utf-8") + ext
|
||||
ok = not os.path.exists(fsenc(os.path.join(fdir, fn)))
|
||||
|
||||
return fn
|
||||
|
||||
|
||||
def gen_filekey(salt: str, fspath: str, fsize: int, inode: int) -> str:
|
||||
return base64.urlsafe_b64encode(
|
||||
hashlib.sha512(
|
||||
"{} {} {} {}".format(salt, fspath, fsize, inode).encode("utf-8", "replace")
|
||||
("%s %s %s %s" % (salt, fspath, fsize, inode)).encode("utf-8", "replace")
|
||||
).digest()
|
||||
).decode("ascii")
|
||||
|
||||
@@ -1556,7 +1593,7 @@ def gen_filekey_dbg(
|
||||
|
||||
|
||||
def gencookie(k: str, v: str, r: str, tls: bool, dur: Optional[int]) -> str:
|
||||
v = v.replace(";", "")
|
||||
v = v.replace("%", "%25").replace(";", "%3B")
|
||||
if dur:
|
||||
exp = formatdate(time.time() + dur, usegmt=True)
|
||||
else:
|
||||
@@ -1625,7 +1662,7 @@ def uncyg(path: str) -> str:
|
||||
if len(path) > 2 and path[2] != "/":
|
||||
return path
|
||||
|
||||
return "{}:\\{}".format(path[1], path[3:])
|
||||
return "%s:\\%s" % (path[1], path[3:])
|
||||
|
||||
|
||||
def undot(path: str) -> str:
|
||||
@@ -1668,7 +1705,7 @@ def sanitize_fn(fn: str, ok: str, bad: list[str]) -> str:
|
||||
|
||||
bad = ["con", "prn", "aux", "nul"]
|
||||
for n in range(1, 10):
|
||||
bad += "com{0} lpt{0}".format(n).split(" ")
|
||||
bad += ("com%s lpt%s" % (n, n)).split(" ")
|
||||
|
||||
if fn.lower().split(".")[0] in bad:
|
||||
fn = "_" + fn
|
||||
@@ -1690,7 +1727,7 @@ def relchk(rp: str) -> str:
|
||||
|
||||
def absreal(fpath: str) -> str:
|
||||
try:
|
||||
return fsdec(os.path.abspath(os.path.realpath(fsenc(fpath))))
|
||||
return fsdec(os.path.abspath(os.path.realpath(afsenc(fpath))))
|
||||
except:
|
||||
if not WINDOWS:
|
||||
raise
|
||||
@@ -1810,6 +1847,32 @@ def _w8enc3(txt: str) -> bytes:
|
||||
return txt.encode(FS_ENCODING, "surrogateescape")
|
||||
|
||||
|
||||
def _msdec(txt: bytes) -> str:
|
||||
ret = txt.decode(FS_ENCODING, "surrogateescape")
|
||||
return ret[4:] if ret.startswith("\\\\?\\") else ret
|
||||
|
||||
|
||||
def _msaenc(txt: str) -> bytes:
|
||||
return txt.replace("/", "\\").encode(FS_ENCODING, "surrogateescape")
|
||||
|
||||
|
||||
def _uncify(txt: str) -> str:
|
||||
txt = txt.replace("/", "\\")
|
||||
if ":" not in txt and not txt.startswith("\\\\"):
|
||||
txt = absreal(txt)
|
||||
|
||||
return txt if txt.startswith("\\\\") else "\\\\?\\" + txt
|
||||
|
||||
|
||||
def _msenc(txt: str) -> bytes:
|
||||
txt = txt.replace("/", "\\")
|
||||
if ":" not in txt and not txt.startswith("\\\\"):
|
||||
txt = absreal(txt)
|
||||
|
||||
ret = txt.encode(FS_ENCODING, "surrogateescape")
|
||||
return ret if ret.startswith(b"\\\\") else b"\\\\?\\" + ret
|
||||
|
||||
|
||||
w8dec = _w8dec3 if not PY2 else _w8dec2
|
||||
w8enc = _w8enc3 if not PY2 else _w8enc2
|
||||
|
||||
@@ -1824,9 +1887,16 @@ def w8b64enc(txt: str) -> str:
|
||||
return base64.urlsafe_b64encode(w8enc(txt)).decode("ascii")
|
||||
|
||||
|
||||
if not PY2 or not WINDOWS:
|
||||
fsenc = w8enc
|
||||
if not PY2 and WINDOWS:
|
||||
sfsenc = w8enc
|
||||
afsenc = _msaenc
|
||||
fsenc = _msenc
|
||||
fsdec = _msdec
|
||||
uncify = _uncify
|
||||
elif not PY2 or not WINDOWS:
|
||||
fsenc = afsenc = sfsenc = w8enc
|
||||
fsdec = w8dec
|
||||
uncify = str
|
||||
else:
|
||||
# moonrunes become \x3f with bytestrings,
|
||||
# losing mojibake support is worth
|
||||
@@ -1836,8 +1906,9 @@ else:
|
||||
def _not_actually_mbcs_dec(txt: bytes) -> str:
|
||||
return txt
|
||||
|
||||
fsenc = _not_actually_mbcs_enc
|
||||
fsenc = afsenc = sfsenc = _not_actually_mbcs_enc
|
||||
fsdec = _not_actually_mbcs_dec
|
||||
uncify = str
|
||||
|
||||
|
||||
def s3enc(mem_cur: "sqlite3.Cursor", rd: str, fn: str) -> tuple[str, str]:
|
||||
@@ -1948,6 +2019,8 @@ def shut_socket(log: "NamedLogger", sck: socket.socket, timeout: int = 3) -> Non
|
||||
sck.shutdown(socket.SHUT_RDWR)
|
||||
except:
|
||||
pass
|
||||
except Exception as ex:
|
||||
log("shut({}): {}".format(fd, ex), "90")
|
||||
finally:
|
||||
td = time.time() - t0
|
||||
if td >= 1:
|
||||
@@ -2199,7 +2272,7 @@ def rmdirs(
|
||||
dirs = [os.path.join(top, x) for x in dirs]
|
||||
ok = []
|
||||
ng = []
|
||||
for d in dirs[::-1]:
|
||||
for d in reversed(dirs):
|
||||
a, b = rmdirs(logger, scandir, lstat, d, depth + 1)
|
||||
ok += a
|
||||
ng += b
|
||||
@@ -2214,18 +2287,21 @@ def rmdirs(
|
||||
return ok, ng
|
||||
|
||||
|
||||
def rmdirs_up(top: str) -> tuple[list[str], list[str]]:
|
||||
def rmdirs_up(top: str, stop: str) -> tuple[list[str], list[str]]:
|
||||
"""rmdir on self, then all parents"""
|
||||
if top == stop:
|
||||
return [], [top]
|
||||
|
||||
try:
|
||||
os.rmdir(fsenc(top))
|
||||
except:
|
||||
return [], [top]
|
||||
|
||||
par = os.path.dirname(top)
|
||||
if not par:
|
||||
if not par or par == stop:
|
||||
return [top], []
|
||||
|
||||
ok, ng = rmdirs_up(par)
|
||||
ok, ng = rmdirs_up(par, stop)
|
||||
return [top] + ok, ng
|
||||
|
||||
|
||||
@@ -2246,7 +2322,7 @@ def unescape_cookie(orig: str) -> str:
|
||||
ret += chr(int(esc[1:], 16))
|
||||
except:
|
||||
ret += esc
|
||||
esc = ""
|
||||
esc = ""
|
||||
|
||||
else:
|
||||
ret += ch
|
||||
@@ -2459,23 +2535,14 @@ def retchk(
|
||||
raise Exception(t)
|
||||
|
||||
|
||||
def _runhook(
|
||||
log: "NamedLogger",
|
||||
cmd: str,
|
||||
ap: str,
|
||||
vp: str,
|
||||
host: str,
|
||||
uname: str,
|
||||
ip: str,
|
||||
at: float,
|
||||
sz: int,
|
||||
txt: str,
|
||||
) -> bool:
|
||||
def _parsehook(
|
||||
log: Optional["NamedLogger"], cmd: str
|
||||
) -> tuple[bool, bool, bool, float, dict[str, Any], str]:
|
||||
chk = False
|
||||
fork = False
|
||||
jtxt = False
|
||||
wait = 0
|
||||
tout = 0
|
||||
wait = 0.0
|
||||
tout = 0.0
|
||||
kill = "t"
|
||||
cap = 0
|
||||
ocmd = cmd
|
||||
@@ -2495,34 +2562,111 @@ def _runhook(
|
||||
cap = int(arg[1:]) # 0=none 1=stdout 2=stderr 3=both
|
||||
elif arg.startswith("k"):
|
||||
kill = arg[1:] # [t]ree [m]ain [n]one
|
||||
elif arg.startswith("i"):
|
||||
pass
|
||||
else:
|
||||
t = "hook: invalid flag {} in {}"
|
||||
log(t.format(arg, ocmd))
|
||||
(log or print)(t.format(arg, ocmd))
|
||||
|
||||
env = os.environ.copy()
|
||||
# try:
|
||||
pypath = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||
zsl = [str(pypath)] + [str(x) for x in sys.path if x]
|
||||
pypath = str(os.pathsep.join(zsl))
|
||||
env["PYTHONPATH"] = pypath
|
||||
# except: if not E.ox: raise
|
||||
try:
|
||||
if EXE:
|
||||
raise Exception()
|
||||
|
||||
ka = {
|
||||
pypath = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||
zsl = [str(pypath)] + [str(x) for x in sys.path if x]
|
||||
pypath = str(os.pathsep.join(zsl))
|
||||
env["PYTHONPATH"] = pypath
|
||||
except:
|
||||
if not EXE:
|
||||
raise
|
||||
|
||||
sp_ka = {
|
||||
"env": env,
|
||||
"timeout": tout,
|
||||
"kill": kill,
|
||||
"capture": cap,
|
||||
}
|
||||
|
||||
if cmd.startswith("~"):
|
||||
cmd = os.path.expanduser(cmd)
|
||||
|
||||
return chk, fork, jtxt, wait, sp_ka, cmd
|
||||
|
||||
|
||||
def runihook(
|
||||
log: Optional["NamedLogger"],
|
||||
cmd: str,
|
||||
vol: "VFS",
|
||||
ups: list[tuple[str, int, int, str, str, str, int]],
|
||||
) -> bool:
|
||||
ocmd = cmd
|
||||
chk, fork, jtxt, wait, sp_ka, cmd = _parsehook(log, cmd)
|
||||
bcmd = [sfsenc(cmd)]
|
||||
if cmd.endswith(".py"):
|
||||
bcmd = [sfsenc(pybin)] + bcmd
|
||||
|
||||
vps = [vjoin(*list(s3dec(x[3], x[4]))) for x in ups]
|
||||
aps = [djoin(vol.realpath, x) for x in vps]
|
||||
if jtxt:
|
||||
# 0w 1mt 2sz 3rd 4fn 5ip 6at
|
||||
ja = [
|
||||
{
|
||||
"ap": uncify(ap), # utf8 for json
|
||||
"vp": vp,
|
||||
"wark": x[0][:16],
|
||||
"mt": x[1],
|
||||
"sz": x[2],
|
||||
"ip": x[5],
|
||||
"at": x[6],
|
||||
}
|
||||
for x, vp, ap in zip(ups, vps, aps)
|
||||
]
|
||||
sp_ka["sin"] = json.dumps(ja).encode("utf-8", "replace")
|
||||
else:
|
||||
sp_ka["sin"] = b"\n".join(fsenc(x) for x in aps)
|
||||
|
||||
t0 = time.time()
|
||||
if fork:
|
||||
Daemon(runcmd, ocmd, [bcmd], ka=sp_ka)
|
||||
else:
|
||||
rc, v, err = runcmd(bcmd, **sp_ka) # type: ignore
|
||||
if chk and rc:
|
||||
retchk(rc, bcmd, err, log, 5)
|
||||
return False
|
||||
|
||||
wait -= time.time() - t0
|
||||
if wait > 0:
|
||||
time.sleep(wait)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _runhook(
|
||||
log: Optional["NamedLogger"],
|
||||
cmd: str,
|
||||
ap: str,
|
||||
vp: str,
|
||||
host: str,
|
||||
uname: str,
|
||||
mt: float,
|
||||
sz: int,
|
||||
ip: str,
|
||||
at: float,
|
||||
txt: str,
|
||||
) -> bool:
|
||||
ocmd = cmd
|
||||
chk, fork, jtxt, wait, sp_ka, cmd = _parsehook(log, cmd)
|
||||
if jtxt:
|
||||
ja = {
|
||||
"ap": ap,
|
||||
"vp": vp,
|
||||
"mt": mt,
|
||||
"sz": sz,
|
||||
"ip": ip,
|
||||
"at": at or time.time(),
|
||||
"host": host,
|
||||
"user": uname,
|
||||
"at": at or time.time(),
|
||||
"sz": sz,
|
||||
"txt": txt,
|
||||
}
|
||||
arg = json.dumps(ja)
|
||||
@@ -2531,15 +2675,15 @@ def _runhook(
|
||||
|
||||
acmd = [cmd, arg]
|
||||
if cmd.endswith(".py"):
|
||||
acmd = [sys.executable] + acmd
|
||||
acmd = [pybin] + acmd
|
||||
|
||||
bcmd = [fsenc(x) for x in acmd]
|
||||
bcmd = [fsenc(x) if x == ap else sfsenc(x) for x in acmd]
|
||||
|
||||
t0 = time.time()
|
||||
if fork:
|
||||
Daemon(runcmd, ocmd, [acmd], ka=ka)
|
||||
Daemon(runcmd, ocmd, [bcmd], ka=sp_ka)
|
||||
else:
|
||||
rc, v, err = runcmd(bcmd, **ka) # type: ignore
|
||||
rc, v, err = runcmd(bcmd, **sp_ka) # type: ignore
|
||||
if chk and rc:
|
||||
retchk(rc, bcmd, err, log, 5)
|
||||
return False
|
||||
@@ -2552,24 +2696,25 @@ def _runhook(
|
||||
|
||||
|
||||
def runhook(
|
||||
log: "NamedLogger",
|
||||
log: Optional["NamedLogger"],
|
||||
cmds: list[str],
|
||||
ap: str,
|
||||
vp: str,
|
||||
host: str,
|
||||
uname: str,
|
||||
mt: float,
|
||||
sz: int,
|
||||
ip: str,
|
||||
at: float,
|
||||
sz: int,
|
||||
txt: str,
|
||||
) -> bool:
|
||||
vp = vp.replace("\\", "/")
|
||||
for cmd in cmds:
|
||||
try:
|
||||
if not _runhook(log, cmd, ap, vp, host, uname, ip, at, sz, txt):
|
||||
if not _runhook(log, cmd, ap, vp, host, uname, mt, sz, ip, at, txt):
|
||||
return False
|
||||
except Exception as ex:
|
||||
log("hook: {}".format(ex))
|
||||
(log or print)("hook: {}".format(ex))
|
||||
if ",c," in "," + cmd:
|
||||
return False
|
||||
break
|
||||
|
||||
@@ -6,7 +6,7 @@ pk: $(addsuffix .gz, $(wildcard *.js *.css))
|
||||
un: $(addsuffix .un, $(wildcard *.gz))
|
||||
|
||||
%.gz: %
|
||||
pigz -11 -J 34 -I 5730 $<
|
||||
pigz -11 -J 34 -I 573 $<
|
||||
|
||||
%.un: %
|
||||
pigz -d $<
|
||||
|
||||
1
copyparty/web/a/u2c.py
Symbolic link
1
copyparty/web/a/u2c.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../bin/u2c.py
|
||||
@@ -1 +0,0 @@
|
||||
../../../bin/up2k.py
|
||||
@@ -27,8 +27,8 @@ window.baguetteBox = (function () {
|
||||
isOverlayVisible = false,
|
||||
touch = {}, // start-pos
|
||||
touchFlag = false, // busy
|
||||
re_i = /.+\.(a?png|avif|bmp|gif|heif|jpe?g|jfif|svg|webp)(\?|$)/i,
|
||||
re_v = /.+\.(webm|mkv|mp4)(\?|$)/i,
|
||||
re_i = /^[^?]+\.(a?png|avif|bmp|gif|heif|jpe?g|jfif|svg|webp)(\?|$)/i,
|
||||
re_v = /^[^?]+\.(webm|mkv|mp4)(\?|$)/i,
|
||||
anims = ['slideIn', 'fadeIn', 'none'],
|
||||
data = {}, // all galleries
|
||||
imagesElements = [],
|
||||
@@ -580,6 +580,7 @@ window.baguetteBox = (function () {
|
||||
function hideOverlay(e) {
|
||||
ev(e);
|
||||
playvid(false);
|
||||
removeFromCache('#files');
|
||||
if (options.noScrollbars) {
|
||||
document.documentElement.style.overflowY = 'auto';
|
||||
document.body.style.overflowY = 'auto';
|
||||
@@ -812,10 +813,16 @@ window.baguetteBox = (function () {
|
||||
}
|
||||
|
||||
function vid() {
|
||||
if (currentIndex >= imagesElements.length)
|
||||
return;
|
||||
|
||||
return imagesElements[currentIndex].querySelector('video');
|
||||
}
|
||||
|
||||
function vidimg() {
|
||||
if (currentIndex >= imagesElements.length)
|
||||
return;
|
||||
|
||||
return imagesElements[currentIndex].querySelector('img, video');
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
--g-fsel-bg: #d39;
|
||||
--g-fsel-b1: #f4a;
|
||||
--g-fsel-ts: #804;
|
||||
--g-dfg: var(--srv-3);
|
||||
--g-fg: var(--a-hil);
|
||||
--g-bg: var(--bg-u2);
|
||||
--g-b1: var(--bg-u4);
|
||||
@@ -327,6 +328,7 @@ html.c {
|
||||
}
|
||||
html.cz {
|
||||
--bgg: var(--bg-u2);
|
||||
--srv-3: #fff;
|
||||
}
|
||||
html.cy {
|
||||
--fg: #fff;
|
||||
@@ -354,6 +356,7 @@ html.cy {
|
||||
--chk-fg: #fd0;
|
||||
|
||||
--srv-1: #f00;
|
||||
--srv-3: #fff;
|
||||
--op-aa-bg: #fff;
|
||||
|
||||
--u2-b1-bg: #f00;
|
||||
@@ -793,22 +796,31 @@ html.y #path a:hover {
|
||||
}
|
||||
.logue {
|
||||
padding: .2em 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.logue.hidden,
|
||||
.logue:empty {
|
||||
display: none;
|
||||
}
|
||||
#doc>iframe,
|
||||
.logue>iframe {
|
||||
background: var(--bgg);
|
||||
border-radius: .3em;
|
||||
border: 1px solid var(--bgg);
|
||||
border-width: 0 .3em 0 .3em;
|
||||
border-radius: .5em;
|
||||
visibility: hidden;
|
||||
border: none;
|
||||
margin: 0 -.3em;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
}
|
||||
#doc>iframe.focus,
|
||||
.logue>iframe.focus {
|
||||
box-shadow: 0 0 .1em .1em var(--a);
|
||||
}
|
||||
#pro.logue>iframe {
|
||||
height: 100vh;
|
||||
}
|
||||
#pro.logue {
|
||||
margin-bottom: .8em;
|
||||
}
|
||||
@@ -833,8 +845,9 @@ html.y #path a:hover {
|
||||
.mdo {
|
||||
max-width: 52em;
|
||||
}
|
||||
.mdo.sb {
|
||||
max-width: unset;
|
||||
.mdo.sb,
|
||||
#epi.logue.mdo>iframe {
|
||||
max-width: 54em;
|
||||
}
|
||||
.mdo,
|
||||
.mdo * {
|
||||
@@ -956,6 +969,9 @@ html.y #path a:hover {
|
||||
#ggrid>a.dir:before {
|
||||
content: '📂';
|
||||
}
|
||||
#ggrid>a.dir>span {
|
||||
color: var(--g-dfg);
|
||||
}
|
||||
#ggrid>a.au:before {
|
||||
content: '💾';
|
||||
}
|
||||
@@ -1002,6 +1018,9 @@ html.np_open #ggrid>a.au:before {
|
||||
background: var(--g-sel-bg);
|
||||
border-color: var(--g-sel-b1);
|
||||
}
|
||||
#ggrid>a.sel>span {
|
||||
color: var(--g-sel-fg);
|
||||
}
|
||||
#ggrid>a.sel,
|
||||
#ggrid>a[tt].sel {
|
||||
border-top: 1px solid var(--g-fsel-b1);
|
||||
@@ -1055,6 +1074,9 @@ html.np_open #ggrid>a.au:before {
|
||||
background: var(--bg-d3);
|
||||
box-shadow: -.2em .2em 0 var(--f-sel-sh), -.2em -.2em 0 var(--f-sel-sh);
|
||||
}
|
||||
#player {
|
||||
display: none;
|
||||
}
|
||||
#widget {
|
||||
position: fixed;
|
||||
font-size: 1.4em;
|
||||
@@ -1137,10 +1159,10 @@ html.y #widget.open {
|
||||
background: #fff;
|
||||
background: var(--bg-u3);
|
||||
}
|
||||
#wfm, #wzip, #wnp {
|
||||
#wfs, #wfm, #wzip, #wnp {
|
||||
display: none;
|
||||
}
|
||||
#wzip, #wnp {
|
||||
#wfs, #wzip, #wnp {
|
||||
margin-right: .2em;
|
||||
padding-right: .2em;
|
||||
border: 1px solid var(--bg-u5);
|
||||
@@ -1152,6 +1174,7 @@ html.y #widget.open {
|
||||
padding-left: .2em;
|
||||
border-left-width: .1em;
|
||||
}
|
||||
#wfs.act,
|
||||
#wfm.act {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -1175,6 +1198,13 @@ html.y #widget.open {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
#wfs {
|
||||
font-size: .36em;
|
||||
text-align: right;
|
||||
line-height: 1.3em;
|
||||
padding: 0 .3em 0 0;
|
||||
border-width: 0 .25em 0 0;
|
||||
}
|
||||
#wfm span,
|
||||
#wnp span {
|
||||
font-size: .6em;
|
||||
@@ -1313,6 +1343,10 @@ html.y #ops svg circle {
|
||||
padding: .3em .6em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#noie {
|
||||
color: #b60;
|
||||
margin: 0 0 0 .5em;
|
||||
}
|
||||
.opbox {
|
||||
padding: .5em;
|
||||
border-radius: 0 .3em .3em 0;
|
||||
@@ -1717,6 +1751,7 @@ html.y #tree.nowrap .ntree a+a:hover {
|
||||
display: none;
|
||||
}
|
||||
.ghead {
|
||||
background: var(--bg-u2);
|
||||
border-radius: .3em;
|
||||
padding: .2em .5em;
|
||||
line-height: 2.3em;
|
||||
@@ -2913,6 +2948,7 @@ html.b #treepar {
|
||||
html.b #wrap {
|
||||
margin-top: 2em;
|
||||
}
|
||||
html.by .ghead,
|
||||
html.bz .ghead {
|
||||
background: var(--bg);
|
||||
padding: .2em 0;
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<input type="file" name="f" multiple /><br />
|
||||
<input type="submit" value="start upload">
|
||||
</form>
|
||||
<a id="bbsw" href="?b=u"><br />switch to basic browser</a>
|
||||
<a id="bbsw" href="?b=u" rel="nofollow"><br />switch to basic browser</a>
|
||||
</div>
|
||||
|
||||
<div id="op_mkdir" class="opview opbox act">
|
||||
@@ -121,7 +121,7 @@
|
||||
|
||||
<div id="epi" class="logue">{{ "" if sb_lg else logues[1] }}</div>
|
||||
|
||||
<h2><a href="{{ r }}/?h" id="goh">control-panel</a></h2>
|
||||
<h2 id="wfp"><a href="{{ r }}/?h" id="goh">control-panel</a></h2>
|
||||
|
||||
<a href="#" id="repl">π</a>
|
||||
|
||||
@@ -135,8 +135,10 @@
|
||||
|
||||
<script>
|
||||
var SR = {{ r|tojson }},
|
||||
TS = "{{ ts }}",
|
||||
acct = "{{ acct }}",
|
||||
perms = {{ perms }},
|
||||
dgrid = {{ dgrid|tojson }},
|
||||
themes = {{ themes }},
|
||||
dtheme = "{{ dtheme }}",
|
||||
srvinf = "{{ srv_info }}",
|
||||
@@ -154,6 +156,8 @@
|
||||
sb_lg = "{{ sb_lg }}",
|
||||
lifetime = {{ lifetime }},
|
||||
turbolvl = {{ turbolvl }},
|
||||
idxh = {{ idxh }},
|
||||
frand = {{ frand|tojson }},
|
||||
u2sort = "{{ u2sort }}",
|
||||
have_emp = {{ have_emp|tojson }},
|
||||
txt_ext = "{{ txt_ext }}",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -231,11 +231,11 @@ function convert_markdown(md_text, dest_dom) {
|
||||
var nodes = md_dom.getElementsByTagName('a');
|
||||
for (var a = nodes.length - 1; a >= 0; a--) {
|
||||
var href = nodes[a].getAttribute('href');
|
||||
var txt = nodes[a].textContent;
|
||||
var txt = nodes[a].innerHTML;
|
||||
|
||||
if (!txt)
|
||||
nodes[a].textContent = href;
|
||||
else if (href !== txt)
|
||||
else if (href !== txt && !nodes[a].className)
|
||||
nodes[a].className = 'vis';
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ a {
|
||||
td a {
|
||||
margin: 0;
|
||||
}
|
||||
#w {
|
||||
color: #fff;
|
||||
background: #940;
|
||||
border-color: #b70;
|
||||
}
|
||||
.af,
|
||||
.logout {
|
||||
float: right;
|
||||
@@ -175,15 +180,19 @@ html.z a.g {
|
||||
border-color: #af4;
|
||||
box-shadow: 0 .3em 1em #7d0;
|
||||
}
|
||||
html.z input {
|
||||
color: #fff;
|
||||
background: #626;
|
||||
border: 1px solid #c2c;
|
||||
border-width: 1px 0 0 0;
|
||||
input {
|
||||
color: #a50;
|
||||
background: #fff;
|
||||
border: 1px solid #a50;
|
||||
border-radius: .5em;
|
||||
padding: .5em .7em;
|
||||
margin: 0 .5em 0 0;
|
||||
}
|
||||
html.z input {
|
||||
color: #fff;
|
||||
background: #626;
|
||||
border-color: #c2c;
|
||||
}
|
||||
html.z .num {
|
||||
border-color: #777;
|
||||
}
|
||||
|
||||
@@ -89,17 +89,20 @@
|
||||
</ul>
|
||||
|
||||
<h1 id="l">login for more:</h1>
|
||||
<ul>
|
||||
<div>
|
||||
<form method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
|
||||
<input type="hidden" name="act" value="login" />
|
||||
<input type="password" name="cppwd" />
|
||||
<input type="submit" value="Login" />
|
||||
{% if ahttps %}
|
||||
<a id="w" href="{{ ahttps }}">switch to https</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" id="repl">π</a>
|
||||
{%- if not this.args.nb %}
|
||||
<span id="pb"><span>powered by</span> <a href="{{ this.args.pb_url }}">copyparty</a></span>
|
||||
<span id="pb"><span>powered by</span> <a href="{{ this.args.pb_url }}">copyparty {{ver}}</a></span>
|
||||
{%- endif %}
|
||||
<script>
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ var Ls = {
|
||||
"t1": "handling",
|
||||
"u2": "tid siden noen sist skrev til serveren$N( opplastning / navneendring / ... )$N$N17d = 17 dager$N1h23 = 1 time 23 minutter$N4m56 = 4 minuter 56 sekunder",
|
||||
"v1": "koble til",
|
||||
"v2": "bruk denne serveren som en lokal harddisk$N$NADVARSEL: kommer til å vise passordet ditt!"
|
||||
"v2": "bruk denne serveren som en lokal harddisk$N$NADVARSEL: kommer til å vise passordet ditt!",
|
||||
"w1": "bytt til https",
|
||||
},
|
||||
"eng": {
|
||||
"d2": "shows the state of all active threads",
|
||||
|
||||
@@ -43,10 +43,9 @@
|
||||
<h1>WebDAV</h1>
|
||||
|
||||
<div class="os win">
|
||||
<p><em>note: rclone-FTP is a bit faster, so {% if args.ftp or args.ftps %}try that first{% else %}consider enabling FTP in server settings{% endif %}</em></p>
|
||||
<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>
|
||||
<pre>
|
||||
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=other{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>W:</b>
|
||||
</pre>
|
||||
{% if s %}
|
||||
@@ -64,9 +63,14 @@
|
||||
yum install davfs2
|
||||
{% if accs %}printf '%s\n' <b>{{ pw }}</b> k | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
<p>or you can use rclone instead, which is much slower but doesn't require root:</p>
|
||||
<p>make it automount on boot:</p>
|
||||
<pre>
|
||||
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=other{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
|
||||
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>{{ pw }}</b> k" >> /etc/davfs2/secrets
|
||||
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b> davfs rw,user,uid=1000,noauto 0 0" >> /etc/fstab
|
||||
</pre>
|
||||
<p>or you can use rclone instead, which is much slower but doesn't require root (plus it keeps lastmodified on upload):</p>
|
||||
<pre>
|
||||
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
{% if s %}
|
||||
@@ -106,10 +110,21 @@
|
||||
|
||||
<div class="os win">
|
||||
<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>
|
||||
{% if args.ftp %}
|
||||
<p>connect with plaintext FTP:</p>
|
||||
<pre>
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp or args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls={{ "false" if args.ftp else "true" }}
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>W:</b>
|
||||
</pre>
|
||||
{% endif %}
|
||||
{% if args.ftps %}
|
||||
<p>connect with TLS-encrypted FTPS:</p>
|
||||
<pre>
|
||||
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
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>W:</b>
|
||||
</pre>
|
||||
<p><em>note: if you are on LAN (or just dont have valid certificates), add <code>no_check_certificate=true</code> to the config command</em><br />---</p>
|
||||
{% endif %}
|
||||
<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>
|
||||
<pre>
|
||||
explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}<b>{{ pw }}</b>:k@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||
@@ -117,10 +132,21 @@
|
||||
</div>
|
||||
|
||||
<div class="os lin">
|
||||
{% if args.ftp %}
|
||||
<p>connect with plaintext FTP:</p>
|
||||
<pre>
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp or args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls={{ "false" if args.ftp else "true" }}
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
{% endif %}
|
||||
{% if args.ftps %}
|
||||
<p>connect with TLS-encrypted FTPS:</p>
|
||||
<pre>
|
||||
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
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
<p><em>note: if you are on LAN (or just dont have valid certificates), add <code>no_check_certificate=true</code> to the config command</em><br />---</p>
|
||||
{% endif %}
|
||||
<p>emergency alternative (gnome/gui-only):</p>
|
||||
<!-- gnome-bug: ignores vp -->
|
||||
<pre>
|
||||
@@ -155,7 +181,7 @@
|
||||
<p><em>note: if you are on LAN (or just dont have valid certificates), add <code>-td</code></em></p>
|
||||
{% endif %}
|
||||
<p>
|
||||
you can use <a href="{{ r }}/.cpr/a/up2k.py">up2k.py</a> to upload (sometimes faster than web-browsers)
|
||||
you can use <a href="{{ r }}/.cpr/a/u2c.py">u2c.py</a> to upload (sometimes faster than web-browsers)
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
@@ -42,6 +42,10 @@ html {
|
||||
text-shadow: 1px 1px 0 #000;
|
||||
color: #fff;
|
||||
}
|
||||
#toast.top {
|
||||
top: 2em;
|
||||
bottom: unset;
|
||||
}
|
||||
#toast a {
|
||||
color: inherit;
|
||||
text-shadow: inherit;
|
||||
@@ -447,6 +451,20 @@ html.y textarea:focus {
|
||||
padding: .2em .5em;
|
||||
border: .12em solid #aaa;
|
||||
}
|
||||
.mdo .mdth,
|
||||
.mdo .mdthl,
|
||||
.mdo .mdthr {
|
||||
margin: .5em .5em .5em 0;
|
||||
}
|
||||
.mdthl {
|
||||
float: left;
|
||||
}
|
||||
.mdthr {
|
||||
float: right;
|
||||
}
|
||||
hr {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@media screen {
|
||||
.mdo {
|
||||
|
||||
@@ -114,10 +114,10 @@ function up2k_flagbus() {
|
||||
do_take(now);
|
||||
return;
|
||||
}
|
||||
if (flag.owner && now - flag.owner[1] > 5000) {
|
||||
if (flag.owner && now - flag.owner[1] > 12000) {
|
||||
flag.owner = null;
|
||||
}
|
||||
if (flag.wants && now - flag.wants[1] > 5000) {
|
||||
if (flag.wants && now - flag.wants[1] > 12000) {
|
||||
flag.wants = null;
|
||||
}
|
||||
if (!flag.owner && !flag.wants) {
|
||||
@@ -772,6 +772,7 @@ function fsearch_explain(n) {
|
||||
|
||||
function up2k_init(subtle) {
|
||||
var r = {
|
||||
"tact": Date.now(),
|
||||
"init_deps": init_deps,
|
||||
"set_fsearch": set_fsearch,
|
||||
"gotallfiles": [gotallfiles] // hooks
|
||||
@@ -856,6 +857,7 @@ function up2k_init(subtle) {
|
||||
fdom_ctr = 0,
|
||||
biggest_file = 0;
|
||||
|
||||
bcfg_bind(uc, 'rand', 'u2rand', false, null, false);
|
||||
bcfg_bind(uc, 'multitask', 'multitask', true, null, false);
|
||||
bcfg_bind(uc, 'potato', 'potato', false, set_potato, false);
|
||||
bcfg_bind(uc, 'ask_up', 'ask_up', true, null, false);
|
||||
@@ -1363,6 +1365,10 @@ function up2k_init(subtle) {
|
||||
|
||||
if (uc.fsearch)
|
||||
entry.srch = 1;
|
||||
else if (uc.rand) {
|
||||
entry.rand = true;
|
||||
entry.name = 'a\n' + entry.name;
|
||||
}
|
||||
|
||||
if (biggest_file < entry.size)
|
||||
biggest_file = entry.size;
|
||||
@@ -1398,7 +1404,7 @@ function up2k_init(subtle) {
|
||||
ebi('u2tabw').className = 'ye';
|
||||
|
||||
setTimeout(function () {
|
||||
if (!actx || actx.state != 'suspended' || toast.tag == L.u_unpt)
|
||||
if (!actx || actx.state != 'suspended' || toast.visible)
|
||||
return;
|
||||
|
||||
toast.warn(30, "<div onclick=\"start_actx();toast.inf(3,'thanks!')\">please click this text to<br />unlock full upload speed</div>");
|
||||
@@ -1418,7 +1424,36 @@ function up2k_init(subtle) {
|
||||
}
|
||||
more_one_file();
|
||||
|
||||
var etaref = 0, etaskip = 0, utw_minh = 0, utw_read = 0;
|
||||
function linklist() {
|
||||
var ret = [],
|
||||
base = document.location.origin.replace(/\/$/, '');
|
||||
|
||||
for (var a = 0; a < st.files.length; a++) {
|
||||
var t = st.files[a],
|
||||
url = t.purl + uricom_enc(t.name);
|
||||
|
||||
if (t.fk)
|
||||
url += '?k=' + t.fk;
|
||||
|
||||
ret.push(base + url);
|
||||
}
|
||||
return ret.join('\r\n');
|
||||
}
|
||||
|
||||
ebi('luplinks').onclick = function (e) {
|
||||
ev(e);
|
||||
modal.alert(linklist());
|
||||
};
|
||||
|
||||
ebi('cuplinks').onclick = function (e) {
|
||||
ev(e);
|
||||
var txt = linklist();
|
||||
cliptxt(txt + '\n', function () {
|
||||
toast.inf(5, txt.split('\n').length + ' links copied to clipboard');
|
||||
});
|
||||
};
|
||||
|
||||
var etaref = 0, etaskip = 0, utw_minh = 0, utw_read = 0, utw_card = 0;
|
||||
function etafun() {
|
||||
var nhash = st.busy.head.length + st.busy.hash.length + st.todo.head.length + st.todo.hash.length,
|
||||
nsend = st.busy.upload.length + st.todo.upload.length,
|
||||
@@ -1431,6 +1466,12 @@ function up2k_init(subtle) {
|
||||
|
||||
//ebi('acc_info').innerHTML = humantime(st.time.busy) + ' ' + f2f(now / 1000, 1);
|
||||
|
||||
if (utw_card != pvis.act) {
|
||||
utw_card = pvis.act;
|
||||
utw_read = 9001;
|
||||
ebi('u2tabw').style.minHeight = '0px';
|
||||
}
|
||||
|
||||
if (++utw_read >= 20) {
|
||||
utw_read = 0;
|
||||
utw_minh = parseInt(ebi('u2tabw').style.minHeight || '0');
|
||||
@@ -1607,8 +1648,14 @@ function up2k_init(subtle) {
|
||||
running = true;
|
||||
while (true) {
|
||||
var now = Date.now(),
|
||||
blocktime = now - r.tact,
|
||||
is_busy = st.car < st.files.length;
|
||||
|
||||
if (blocktime > 2500)
|
||||
console.log('main thread blocked for ' + blocktime);
|
||||
|
||||
r.tact = now;
|
||||
|
||||
if (was_busy && !is_busy) {
|
||||
for (var a = 0; a < st.files.length; a++) {
|
||||
var t = st.files[a];
|
||||
@@ -1748,6 +1795,15 @@ function up2k_init(subtle) {
|
||||
})();
|
||||
|
||||
function uptoast() {
|
||||
if (st.busy.handshake.length)
|
||||
return;
|
||||
|
||||
for (var a = 0; a < st.files.length; a++) {
|
||||
var t = st.files[a];
|
||||
if (t.want_recheck && !t.rechecks)
|
||||
return;
|
||||
}
|
||||
|
||||
var sr = uc.fsearch,
|
||||
ok = pvis.ctr.ok,
|
||||
ng = pvis.ctr.ng,
|
||||
@@ -1770,6 +1826,7 @@ function up2k_init(subtle) {
|
||||
|
||||
timer.rm(etafun);
|
||||
timer.rm(donut.do);
|
||||
ebi('u2tabw').style.minHeight = '0px';
|
||||
utw_minh = 0;
|
||||
}
|
||||
|
||||
@@ -2003,6 +2060,8 @@ function up2k_init(subtle) {
|
||||
nbusy++;
|
||||
reading++;
|
||||
nchunk++;
|
||||
if (Date.now() - up2k.tact > 1500)
|
||||
tasker();
|
||||
}
|
||||
|
||||
function onmsg(d) {
|
||||
@@ -2213,13 +2272,24 @@ function up2k_init(subtle) {
|
||||
|
||||
t.sprs = response.sprs;
|
||||
|
||||
var rsp_purl = url_enc(response.purl);
|
||||
if (rsp_purl !== t.purl || response.name !== t.name) {
|
||||
// server renamed us (file exists / path restrictions)
|
||||
console.log("server-rename [" + t.purl + "] [" + t.name + "] to [" + rsp_purl + "] [" + response.name + "]");
|
||||
var fk = response.fk,
|
||||
rsp_purl = url_enc(response.purl),
|
||||
rename = rsp_purl !== t.purl || response.name !== t.name;
|
||||
|
||||
if (rename || fk) {
|
||||
if (rename)
|
||||
console.log("server-rename [" + t.purl + "] [" + t.name + "] to [" + rsp_purl + "] [" + response.name + "]");
|
||||
|
||||
t.purl = rsp_purl;
|
||||
t.name = response.name;
|
||||
pvis.seth(t.n, 0, linksplit(t.purl + uricom_enc(t.name)).join(' '));
|
||||
|
||||
var url = t.purl + uricom_enc(t.name);
|
||||
if (fk) {
|
||||
t.fk = fk;
|
||||
url += '?k=' + fk;
|
||||
}
|
||||
|
||||
pvis.seth(t.n, 0, linksplit(url).join(' '));
|
||||
}
|
||||
|
||||
var chunksize = get_chunksize(t.size),
|
||||
@@ -2322,16 +2392,17 @@ function up2k_init(subtle) {
|
||||
}
|
||||
|
||||
var err_pend = rsp.indexOf('partial upload exists at a different') + 1,
|
||||
err_srcb = rsp.indexOf('source file busy; please try again') + 1,
|
||||
err_plug = rsp.indexOf('upload blocked by x') + 1,
|
||||
err_dupe = rsp.indexOf('upload rejected, file already exists') + 1;
|
||||
|
||||
if (err_pend || err_plug || err_dupe) {
|
||||
if (err_pend || err_srcb || err_plug || err_dupe) {
|
||||
err = rsp;
|
||||
ofs = err.indexOf('\n/');
|
||||
if (ofs !== -1) {
|
||||
err = err.slice(0, ofs + 1) + linksplit(err.slice(ofs + 2).trimEnd()).join(' ');
|
||||
}
|
||||
if (!t.rechecks && err_pend) {
|
||||
if (!t.rechecks && (err_pend || err_srcb)) {
|
||||
t.rechecks = 0;
|
||||
t.want_recheck = true;
|
||||
}
|
||||
@@ -2368,6 +2439,8 @@ function up2k_init(subtle) {
|
||||
};
|
||||
if (t.srch)
|
||||
req.srch = 1;
|
||||
else if (t.rand)
|
||||
req.rand = true;
|
||||
|
||||
xhr.open('POST', t.purl, true);
|
||||
xhr.responseType = 'text';
|
||||
@@ -2871,7 +2944,7 @@ ebi('ico1').onclick = function () {
|
||||
if (QS('#op_up2k.act'))
|
||||
goto_up2k();
|
||||
|
||||
apply_perms(perms);
|
||||
apply_perms({ "perms": perms, "frand": frand });
|
||||
|
||||
|
||||
(function () {
|
||||
|
||||
@@ -17,6 +17,7 @@ var wah = '',
|
||||
MOBILE = TOUCH,
|
||||
CHROME = !!window.chrome,
|
||||
VCHROME = CHROME ? 1 : 0,
|
||||
IE = /Trident\//.test(navigator.userAgent),
|
||||
FIREFOX = ('netscape' in window) && / rv:/.test(navigator.userAgent),
|
||||
IPHONE = TOUCH && /iPhone|iPad|iPod/i.test(navigator.userAgent),
|
||||
LINUX = /Linux/.test(navigator.userAgent),
|
||||
@@ -111,12 +112,13 @@ if ((document.location + '').indexOf(',rej,') + 1)
|
||||
|
||||
try {
|
||||
console.hist = [];
|
||||
var CMAXHIST = 100;
|
||||
var hook = function (t) {
|
||||
var orig = console[t].bind(console),
|
||||
cfun = function () {
|
||||
console.hist.push(Date.now() + ' ' + t + ': ' + Array.from(arguments).join(', '));
|
||||
if (console.hist.length > 100)
|
||||
console.hist = console.hist.slice(50);
|
||||
if (console.hist.length > CMAXHIST)
|
||||
console.hist = console.hist.slice(CMAXHIST / 2);
|
||||
|
||||
orig.apply(console, arguments);
|
||||
};
|
||||
@@ -158,7 +160,7 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
|
||||
var html = [
|
||||
'<h1>you hit a bug!</h1>',
|
||||
'<p style="font-size:1.3em;margin:0">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>',
|
||||
'<p style="color:#fff">please send me a screenshot arigathanks gozaimuch: <a href="<ghi>" target="_blank">github issue</a> or <code>ed#2644</code></p>',
|
||||
'<p style="color:#fff">please send me a screenshot arigathanks gozaimuch: <a href="<ghi>" target="_blank">github issue</a></p>',
|
||||
'<p class="b">' + esc(url + ' @' + lineNo + ':' + columnNo), '<br />' + esc(String(msg)).replace(/\n/g, '<br />') + '</p>',
|
||||
'<p><b>UA:</b> ' + esc(navigator.userAgent + '')
|
||||
];
|
||||
@@ -331,6 +333,25 @@ if (!String.prototype.format)
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
new URL('/a/', 'https://a.com/');
|
||||
}
|
||||
catch (ex) {
|
||||
console.log('ie11 shim URL()');
|
||||
window.URL = function (url, base) {
|
||||
if (url.indexOf('//') < 0)
|
||||
url = base + '/' + url.replace(/^\/?/, '');
|
||||
else if (url.indexOf('//') == 0)
|
||||
url = 'https:' + url;
|
||||
|
||||
var x = url.split('?');
|
||||
return {
|
||||
"pathname": '/' + x[0].split('://')[1].replace(/[^/]+\//, ''),
|
||||
"search": x.length > 1 ? x[1] : ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/950146
|
||||
function import_js(url, cb) {
|
||||
var head = document.head || document.getElementsByTagName('head')[0];
|
||||
@@ -611,6 +632,29 @@ function vsplit(vp) {
|
||||
}
|
||||
|
||||
|
||||
function vjoin(p1, p2) {
|
||||
if (!p1)
|
||||
p1 = '';
|
||||
|
||||
if (!p2)
|
||||
p2 = '';
|
||||
|
||||
if (p1.endsWith('/'))
|
||||
p1 = p1.slice(0, -1);
|
||||
|
||||
if (p2.startsWith('/'))
|
||||
p2 = p2.slice(1);
|
||||
|
||||
if (!p1)
|
||||
return p2;
|
||||
|
||||
if (!p2)
|
||||
return p1;
|
||||
|
||||
return p1 + '/' + p2;
|
||||
}
|
||||
|
||||
|
||||
function uricom_enc(txt, do_fb_enc) {
|
||||
try {
|
||||
return encodeURIComponent(txt);
|
||||
@@ -698,7 +742,7 @@ function get_pwd() {
|
||||
if (pwd.length < 2)
|
||||
return null;
|
||||
|
||||
return pwd[1].split(';')[0];
|
||||
return decodeURIComponent(pwd[1].split(';')[0]);
|
||||
}
|
||||
|
||||
|
||||
@@ -978,6 +1022,7 @@ function sethash(hv) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function dl_file(url) {
|
||||
console.log('DL [%s]', url);
|
||||
var o = mknod('a');
|
||||
@@ -987,6 +1032,25 @@ function dl_file(url) {
|
||||
}
|
||||
|
||||
|
||||
function cliptxt(txt, ok) {
|
||||
var fb = function () {
|
||||
console.log('fb');
|
||||
var o = mknod('input');
|
||||
o.value = txt;
|
||||
document.body.appendChild(o);
|
||||
o.focus();
|
||||
o.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(o);
|
||||
ok();
|
||||
};
|
||||
try {
|
||||
navigator.clipboard.writeText(txt).then(ok, fb);
|
||||
}
|
||||
catch (ex) { fb(); }
|
||||
}
|
||||
|
||||
|
||||
var timer = (function () {
|
||||
var r = {};
|
||||
r.q = [];
|
||||
@@ -1142,13 +1206,13 @@ var tt = (function () {
|
||||
r.th.style.top = (e.pageY + 12 * sy) + 'px';
|
||||
};
|
||||
|
||||
if (IPHONE) {
|
||||
if (TOUCH) {
|
||||
var f1 = r.show,
|
||||
f2 = r.hide,
|
||||
q = [];
|
||||
|
||||
// if an onclick-handler creates a new timer,
|
||||
// iOS 13.1.2 delays the entire handler by up to 401ms,
|
||||
// webkits delay the entire handler by up to 401ms,
|
||||
// win by using a shared timer instead
|
||||
|
||||
timer.add(function () {
|
||||
@@ -1260,17 +1324,17 @@ var toast = (function () {
|
||||
r.tag = tag;
|
||||
};
|
||||
|
||||
r.ok = function (sec, txt, tag) {
|
||||
r.show('ok', sec, txt, tag);
|
||||
r.ok = function (sec, txt, tag, cls) {
|
||||
r.show('ok ' + (cls || ''), sec, txt, tag);
|
||||
};
|
||||
r.inf = function (sec, txt, tag) {
|
||||
r.show('inf', sec, txt, tag);
|
||||
r.inf = function (sec, txt, tag, cls) {
|
||||
r.show('inf ' + (cls || ''), sec, txt, tag);
|
||||
};
|
||||
r.warn = function (sec, txt, tag) {
|
||||
r.show('warn', sec, txt, tag);
|
||||
r.warn = function (sec, txt, tag, cls) {
|
||||
r.show('warn ' + (cls || ''), sec, txt, tag);
|
||||
};
|
||||
r.err = function (sec, txt, tag) {
|
||||
r.show('err', sec, txt, tag);
|
||||
r.err = function (sec, txt, tag, cls) {
|
||||
r.show('err ' + (cls || ''), sec, txt, tag);
|
||||
};
|
||||
|
||||
return r;
|
||||
@@ -1538,6 +1602,14 @@ function load_md_plug(md_text, plug_type, defer) {
|
||||
if (defer)
|
||||
md_plug[plug_type] = null;
|
||||
|
||||
if (plug_type == 'pre')
|
||||
try {
|
||||
md_text = md_thumbs(md_text);
|
||||
}
|
||||
catch (ex) {
|
||||
toast.warn(30, '' + ex);
|
||||
}
|
||||
|
||||
if (!have_emp)
|
||||
return md_text;
|
||||
|
||||
@@ -1578,6 +1650,47 @@ function load_md_plug(md_text, plug_type, defer) {
|
||||
|
||||
return md;
|
||||
}
|
||||
function md_thumbs(md) {
|
||||
if (!/(^|\n)<!-- th -->/.exec(md))
|
||||
return md;
|
||||
|
||||
// `!th[flags](some.jpg)`
|
||||
// flags: nothing or "l" or "r"
|
||||
|
||||
md = md.split(/!th\[/g);
|
||||
for (var a = 1; a < md.length; a++) {
|
||||
if (!/^[^\]!()]*\]\([^\][!()]+\)/.exec(md[a])) {
|
||||
md[a] = '!th[' + md[a];
|
||||
continue;
|
||||
}
|
||||
|
||||
var o1 = md[a].indexOf(']('),
|
||||
o2 = md[a].indexOf(')', o1),
|
||||
alt = md[a].slice(0, o1),
|
||||
flags = alt.split(','),
|
||||
url = md[a].slice(o1 + 2, o2),
|
||||
float = has(flags, 'l') ? 'left' : has(flags, 'r') ? 'right' : '';
|
||||
|
||||
if (!/[?&]cache/.exec(url))
|
||||
url += (url.indexOf('?') < 0 ? '?' : '&') + 'cache=i';
|
||||
|
||||
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);
|
||||
}
|
||||
return md.join('');
|
||||
}
|
||||
function md_th_set() {
|
||||
var els = QSA('.mdth');
|
||||
for (var a = 0, aa = els.length; a < aa; a++)
|
||||
els[a].onclick = md_th_click;
|
||||
}
|
||||
function md_th_click(e) {
|
||||
ev(e);
|
||||
var url = this.getAttribute('href').split('?')[0];
|
||||
if (window.sb_md)
|
||||
window.parent.postMessage("imshow " + url, "*");
|
||||
else
|
||||
thegrid.imshow(url);
|
||||
}
|
||||
|
||||
|
||||
var svg_decl = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
||||
@@ -1656,7 +1769,6 @@ function cprop(name) {
|
||||
|
||||
|
||||
function bchrome() {
|
||||
console.log(document.documentElement.className);
|
||||
var v, o = QS('meta[name=theme-color]');
|
||||
if (!o)
|
||||
return;
|
||||
|
||||
@@ -13,15 +13,21 @@
|
||||
|
||||
# other stuff
|
||||
|
||||
## [`example.conf`](example.conf)
|
||||
* example config file for `-c`
|
||||
|
||||
## [`versus.md`](versus.md)
|
||||
* similar software / alternatives (with pros/cons)
|
||||
|
||||
## [`changelog.md`](changelog.md)
|
||||
* occasionally grabbed from github release notes
|
||||
|
||||
## [`devnotes.md`](devnotes.md)
|
||||
* technical stuff
|
||||
|
||||
## [`rclone.md`](rclone.md)
|
||||
* notes on using rclone as a fuse client/server
|
||||
|
||||
## [`example.conf`](example.conf)
|
||||
* example config file for `-c`
|
||||
|
||||
|
||||
|
||||
# junk
|
||||
|
||||
@@ -1,3 +1,505 @@
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0513-0000 `v1.7.2` hard resolve
|
||||
|
||||
## new features
|
||||
* print a warning if `c:\`, `c:\windows*`, or all of `/` are shared
|
||||
* upgraded the docker image to v3.18 which enables the [chiptune player](https://a.ocv.me/pub/demo/music/chiptunes/#af-f6fb2e5f)
|
||||
* in config files, allow trailing `:` in section headers
|
||||
|
||||
## bugfixes
|
||||
* when `--hardlink` (or the volflag) is set, resolve symlinks before hardlinking
|
||||
* uploads could fail due to relative symlinks
|
||||
* really minor ux fixes
|
||||
* left-align `GET` in access logs
|
||||
* the upload panel didn't always shrink back down after uploads completed
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0507-1834 `v1.7.1` CräzY;PWDs
|
||||
|
||||
## new features
|
||||
* webdav:
|
||||
* support write-only folders
|
||||
* option `--dav-auth` / volflag `davauth` forces clients to always auth
|
||||
* helps clients such as `davfs2` see all folders if the root is anon-readable but some subfolders are not
|
||||
* alternatively you could configure your client to always send the password in the `PW` header
|
||||
* include usernames in http request logs
|
||||
* audio player:
|
||||
* consumes less power on phones when the screen is off
|
||||
* smoother playback cursor on short songs
|
||||
|
||||
## bugfixes
|
||||
* the characters `;` and `%` can now be used in passwords
|
||||
* but non-ascii characters (such as the ä in the release title) can, in fact, not
|
||||
* verify that all accounts have unique passwords on startup (#25)
|
||||
|
||||
## other changes
|
||||
* ftpd: log incorrect passwords only, not correct ones
|
||||
* `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)
|
||||
* `u2c` as in `up2k client`, or `up2k CLI`, or `upload-to-copyparty` -- good name
|
||||
* now the only things named "up2k" are the web-ui and the server backend which is way less confusing
|
||||
* 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)
|
||||
* no practical consequences aside from a warm fuzzy feeling of being in the future
|
||||
* 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)
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0429-2114 `v1.7.0` unlinked
|
||||
|
||||
don't get excited! nothing new and revolutionary, but `xvol` and `xdev` changed behavior so there's an above-average chance of fresh bugs
|
||||
|
||||
## new features
|
||||
* (#24): `xvol` and `xdev`, previously just hints to the filesystem indexer, now actively block access as well:
|
||||
* `xvol` stops users following symlinks leaving the volumes they have access to
|
||||
* so if you symlink `/home/ed/music` into `/srv/www/music` it'll get blocked
|
||||
* ...unless both folders are accessible through volumes, and the user has read-access to both
|
||||
* `xdev` stops users crossing the filesystem boundary of the volumes they have access to
|
||||
* 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
|
||||
* enabling these will add a slight performance hit; the unlikely worst-case is `14%` slower directory listings, `35%` slower download-as-tar
|
||||
* file selection summary (num files, size, audio duration) in the bottom right
|
||||
* [u2cli](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py): more aggressive resolving with `--rh`
|
||||
* [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
|
||||
* 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)
|
||||
|
||||
## bugfixes
|
||||
* (#23): stop autodeleting empty folders when moving or deleting files
|
||||
* but files which expire / [self-destruct](https://github.com/9001/copyparty#self-destruct) still clean up parent directories like before
|
||||
* ftp-server: some clients could fail to `mkdir` at first attempt (and also complain during rmdir)
|
||||
|
||||
## other changes
|
||||
* 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
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0426-2300 `v1.6.15` unexpected boost
|
||||
|
||||
## new features
|
||||
* 30% faster folder listings due to [the very last thing](https://github.com/9001/copyparty/commit/55c74ad164633a0a64dceb51f7f534da0422cbb5) i'd ever expect to be a bottleneck, [thx perf](https://docs.python.org/3.12/howto/perf_profiling.html)
|
||||
* option to see the lastmod timestamps of symlinks instead of the target files
|
||||
* 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
|
||||
* **webdav:** enabled by default (because rclone will want this), can be disabled with arg `--dav-rt` or volflag `davrt`
|
||||
* **http:** disabled by default, can be enabled per-request with urlparam `lt`
|
||||
* [u2cli](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py): option `--rh` to resolve server hostname only once at start of upload
|
||||
* fantastic for buggy networks, but it'll break TLS
|
||||
|
||||
## bugfixes
|
||||
* new arg `--s-tbody` specifies the network timeout before a dead connection gets dropped (default 3min)
|
||||
* before there was no timeout at all, which could hang uploads or possibly consume all server resources
|
||||
* ...but this is only relevant if your copyparty is directly exposed to the internet with no reverse proxy
|
||||
* with nginx/caddy/etc you can disable the timeout with `--s-tbody 0` for a 3% performance boost (*wow!*)
|
||||
* iPhone audio transcoder could turn bad and stop transcoding
|
||||
* ~~maybe android phones no longer pause playback at the end of an album~~
|
||||
* nope, that was due to [android's powersaver](https://github.com/9001/copyparty#fix-unreliable-playback-on-android), oh well
|
||||
* ***bonus unintended feature:*** navigate into other folders while a song is plaing
|
||||
* [installing from the source tarball](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#build-from-release-tarball) should be ok now
|
||||
* good base for making distro packages probably
|
||||
|
||||
## other changes
|
||||
* 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
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0424-0609 `v1.6.14` unsettable flags
|
||||
|
||||
## new features
|
||||
* unset a volflag (override a global option) by negating it (setting volflag `-flagname`)
|
||||
* new argument `--cert` to specify TLS certificate location
|
||||
* defaults to `~/.config/copyparty/cert.pem` like before
|
||||
|
||||
## bugfixes
|
||||
* in zip/tar downloads, always use the parent-folder name as the archive root
|
||||
* more reliable ftp authentication when providing password as username
|
||||
* connect-page: fix rclone ftps example
|
||||
|
||||
## other changes
|
||||
* stop suggesting `--http-only` and `--https-only` for performance since the difference is negligible
|
||||
* mention how some antivirus (avast, avg, mcafee) thinks that pillow's webp encoder is a virus, affecting `copyparty.exe`
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0420-2141 `v1.6.12` as seen on nixos
|
||||
|
||||
## new features
|
||||
* @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 🎉
|
||||
* 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)
|
||||
* complements the [arch package](https://github.com/9001/copyparty/tree/hovudstraum/contrib/package/arch) very well w
|
||||
|
||||
## bugfixes
|
||||
* fix an sqlite fd leak
|
||||
* with enough simultaneous traffic, copyparty could run out of file descriptors since it relied on the gc to close sqlite cursors
|
||||
* now there's a pool of cursors shared between the tcp connections instead, limited to the number of CPU cores
|
||||
* 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
|
||||
* ~~somehow explicitly closing the cursors didn't always work... maybe this was actually a python bug :\\/~~
|
||||
* yes, it does incomplete cleanup if opening a WAL database fails
|
||||
* multirange requests would fail with an error; now they get a 200 as expected (since they're kinda useless and not worth the overhead)
|
||||
* [the only software i've ever seen do that](https://apps.kde.org/discover/) now works as intended
|
||||
* expand `~/` filesystem paths in all remaining args: `-c`, `-lo`, `--hist`, `--ssl-log`, and the `hist` volflag
|
||||
* never use IPv6-format IPv4 (`::ffff:127.0.0.1`) in responses
|
||||
* [u2cli](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py): don't enter delete stage if some of the uploads failed
|
||||
* audio player in safari on touchbar macbooks
|
||||
* songs would play backwards because the touchbar keeps spamming play/pause
|
||||
* playback would stop when the preloader kicks in because safari sees the new audio object and freaks out
|
||||
|
||||
## other changes
|
||||
* added [windows quickstart / service example](https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/windows.md)
|
||||
* updated pyinstaller (it makes smaller exe files now)
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0401-2112 `v1.6.11` not joke
|
||||
|
||||
## new features
|
||||
* new event-hook: [exif stripper](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/image-noexif.py)
|
||||
* [markdown thumbnails](https://a.ocv.me/pub/demo/pics-vids/README.md?v) -- see [readme](https://github.com/9001/copyparty#markdown-viewer)
|
||||
* soon: support for [web-scrobbler](https://github.com/web-scrobbler/web-scrobbler/) - the [Last.fm](https://www.last.fm/user/tripflag) browser extension
|
||||
* will update here + readme with more info when [the v3](https://github.com/web-scrobbler/web-scrobbler/projects/5) is out
|
||||
|
||||
## bugfixes
|
||||
* more sqlite query-planner twiddling
|
||||
* deleting files is MUCH faster now, and uploads / bootup might be a bit better too
|
||||
* webdav optimizations / compliance
|
||||
* should make some webdav clients run faster than before
|
||||
* 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!***
|
||||
* does cool stuff such as [bidirectional sync](https://github.com/9001/copyparty#folder-sync) between copyparty and a local folder
|
||||
* [bpm detector](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/audio-bpm.py) is a bit more accurate
|
||||
* [u2cli](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) / commandline uploader: better error messages if something goes wrong
|
||||
* readme rendering could fail in firefox if certain addons were installed (not sure which)
|
||||
* event-hooks: more accurate usage examples
|
||||
|
||||
## other changes
|
||||
* @chinponya automated the prismjs build step (thx!)
|
||||
* updated some js deps (markedjs, codemirror)
|
||||
* copyparty.exe: updated Pillow to 9.5.0
|
||||
* 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))
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0320-2156 `v1.6.10` rclone sync
|
||||
|
||||
## new features
|
||||
* [iPhone "app"](https://github.com/9001/copyparty#ios-shortcuts) (upload shortcut) -- thanks @Daedren !
|
||||
* can strip exif, upload files, pics, vids, links, clipboard
|
||||
* can download links and rehost the target file on your server
|
||||
* support `rclone sync` to [sync folders](https://github.com/9001/copyparty#folder-sync) to/from copyparty
|
||||
* let webdav clients set lastmodified times during upload
|
||||
* let webdav clients replace files during upload
|
||||
|
||||
## bugfixes
|
||||
* [prisonparty](https://github.com/9001/copyparty/blob/hovudstraum/bin/prisonparty.sh): FFmpeg transcoding was slow because there was no `/dev/urandom`
|
||||
* iphones would fail to play *some* songs (low-bitrate and/or shorter than ~7 seconds)
|
||||
* due to either an iOS bug or an FFmpeg bug in the caf remuxing idk
|
||||
* fixed by mixing in white noise into songs if an iPhone asks for them
|
||||
* small correction in the docker readme regarding rootless podman
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0316-2106 `v1.6.9` index.html
|
||||
|
||||
## new features
|
||||
* option to show `index.html` instead of the folder listing
|
||||
* arg `--ih` makes it default-enabled
|
||||
* clients can enable/disable it in the `[⚙️]` settings tab
|
||||
* url-param `?v` skips it for a particular folder
|
||||
* faster folder-thumbnail validation on startup (mostly on conventional HDDs)
|
||||
|
||||
## bugfixes
|
||||
* "load more" button didn't always show up when search results got truncated
|
||||
* ux: tooltips could block buttons on android
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0312-1610 `v1.6.8` folder thumbs
|
||||
|
||||
* read-only demo server at https://a.ocv.me/pub/demo/
|
||||
* [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/)
|
||||
|
||||
## new features
|
||||
* folder thumbnails are indexed in the db
|
||||
* now supports non-lowercase names (`Cover.jpg`, `Folder.JPG`)
|
||||
* folders without a specific cover/folder image will show the first pic inside
|
||||
* when audio playback continues into an empty folder, keep trying for a bit
|
||||
* add no-index hints (google etc) in basic-browser HTML (`?b`, `?b=u`)
|
||||
* [commandline uploader](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) supports long filenames on win7
|
||||
|
||||
## bugfixes
|
||||
* rotated logfiles didn't get xz compressed
|
||||
* image-gallery links pointing to a deleted image shows an error instead of a crashpage
|
||||
|
||||
## other changes
|
||||
* folder thumbnails have purple text to differentiate from files
|
||||
* `copyparty32.exe` starts 30% faster (but is 6% larger)
|
||||
|
||||
----
|
||||
|
||||
# what to download?
|
||||
| download link | is it good? | description |
|
||||
| -- | -- | -- |
|
||||
| **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** | ✅ the best 👍 | runs anywhere! only needs python |
|
||||
| [a docker image](https://github.com/9001/copyparty/blob/hovudstraum/scripts/docker/README.md) | it's ok | good if you prefer docker 🐋 |
|
||||
| [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 |
|
||||
| [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)) |
|
||||
| [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! |
|
||||
| [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 |
|
||||
|
||||
* except for [up2k.exe](https://github.com/9001/copyparty/releases/latest/download/up2k.exe), all of the options above are equivalent
|
||||
* the zip and tar.gz files below are just source code
|
||||
* python packages are available at [PyPI](https://pypi.org/project/copyparty/#files)
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0305-2018 `v1.6.7` fix no-dedup + add up2k.exe
|
||||
|
||||
## new features
|
||||
* controlpanel-connect: add example for webdav automount
|
||||
|
||||
## bugfixes
|
||||
* fix a race which, in worst case (but unlikely on linux), **could cause data loss**
|
||||
* could only happen if `--no-dedup` or volflag `copydupes` was set (**not** default)
|
||||
* if two identical files were uploaded at the same time, there was a small chance that one of the files would become empty
|
||||
* check if you were affected by doing a search for zero-byte files using either of the following:
|
||||
* https://127.0.0.1:3923/#q=size%20%3D%200
|
||||
* `find -type f -size 0`
|
||||
* let me know if you lost something important and had logging enabled!
|
||||
* ftp: mkdir can do multiple levels at once (support filezilla)
|
||||
* fix flickering toast on upload finish
|
||||
* `[💤]` (upload-baton) could disengage if chrome decides to pause the background tab for 10sec (which it sometimes does)
|
||||
|
||||
----
|
||||
|
||||
## introducing [up2k.exe](https://github.com/9001/copyparty/releases/latest/download/up2k.exe)
|
||||
|
||||
the commandline up2k upload / filesearch client, now as a standalone windows exe
|
||||
* based on python 3.7 so it runs on 32bit windows7 or anything newer
|
||||
* *no https support* (saves space + the python3.7 openssl is getting old)
|
||||
* built from b39ff92f34e3fca389c78109d20d5454af761f8e so it can do long filepaths and mojibake
|
||||
|
||||
----
|
||||
|
||||
⭐️ **you probably want [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) below;**
|
||||
the exe is [not recommended](https://github.com/9001/copyparty#copypartyexe) for longterm use
|
||||
and the zip and tar.gz files are source code
|
||||
(python packages are available at [PyPI](https://pypi.org/project/copyparty/#files))
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0226-2030 `v1.6.6` r 2 0 0
|
||||
|
||||
two hundred releases wow
|
||||
* read-only demo server at https://a.ocv.me/pub/demo/
|
||||
* [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/)
|
||||
* currently fighting a ground fault so the demo server will be unreliable for a while
|
||||
|
||||
## new features
|
||||
* more docker containers! now runs on x64, x32, aarch64, armhf, ppc64, s390x
|
||||
* pls let me know if you actually run copyparty on an IBM mainframe 👍
|
||||
* new [event hook](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks) type `xiu` runs just once for all recent uploads
|
||||
* example hook [xiu-sha.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/xiu-sha.py) generates sha512 checksum files
|
||||
* new arg `--rsp-jtr` simulates connection jitter
|
||||
* copyparty.exe integrity selftest
|
||||
* ux:
|
||||
* return to previous page after logging in
|
||||
* show a warning on the login page if you're not using https
|
||||
* freebsd: detect `fetch` and return the [colorful sortable plaintext](https://user-images.githubusercontent.com/241032/215322619-ea5fd606-3654-40ad-94ee-2bc058647bb2.png) listing
|
||||
|
||||
## bugfixes
|
||||
* permit replacing empty files only during a `--blank-wt` grace period
|
||||
* lifetimes: keep upload-time when a size/mtime change triggers a reindex
|
||||
* during cleanup after an unlink, never rmdir the entire volume
|
||||
* rescan button in the controlpanel required volumes to be e2ds
|
||||
* dupes could get indexed with the wrong mtime
|
||||
* only affected the search index; the filesystem got the right one
|
||||
* ux: search results could include the same hit twice in case of overlapping volumes
|
||||
* ux: upload UI would remain expanded permanently after visiting a huge tab
|
||||
* ftp: return proper error messages when client does something illegal
|
||||
* ie11: support the back button
|
||||
|
||||
## other changes
|
||||
* [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) replaces copyparty64.exe -- now built for 64-bit windows 10
|
||||
* **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
|
||||
* has the latest security patches, but sfx.py is still better for long-term use
|
||||
* has pillow and mutagen; can make thumbnails and parse/index media
|
||||
* [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) is the old win7-compatible, dangerously-insecure edition
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0212-1411 `v1.6.5` windows smb fix + win10.exe
|
||||
|
||||
* read-only demo server at https://a.ocv.me/pub/demo/
|
||||
* [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/)
|
||||
|
||||
## bugfixes
|
||||
* **windows-only:** smb locations (network drives) could not be accessed
|
||||
* appeared in [v1.6.4](https://github.com/9001/copyparty/releases/tag/v1.6.4) while adding support for long filepaths (260chars+)
|
||||
|
||||
## other changes
|
||||
* removed tentative support for compressed chiptunes (xmgz, xmz, xmj, ...) since FFmpeg usually doesn't
|
||||
|
||||
----
|
||||
|
||||
# introducing [copyparty640.exe](https://github.com/9001/copyparty/releases/download/v1.6.5/copyparty640.exe)
|
||||
* 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))
|
||||
* __*much* safer__ than the old win7-compatible `copyparty.exe` and `copyparty64.exe`
|
||||
* but only `copyparty-sfx.py` takes advantage of the operating system security patches
|
||||
* includes pillow for thumbnails and mutagen for media indexing
|
||||
* around 10% slower (trying to figure out what's up with that)
|
||||
|
||||
starting from the next release,
|
||||
* `copyparty.exe` (win7 x32) will become `copyparty32.exe`
|
||||
* `copyparty640.exe` (win10) will be the new `copyparty.exe`
|
||||
* `copyparty64.exe` (win7 x64) will graduate
|
||||
|
||||
so 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)
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0211-1802 `v1.6.4` 🔧🎲🔗🐳🇦🎶
|
||||
|
||||
* read-only demo server at https://a.ocv.me/pub/demo/
|
||||
* [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)
|
||||
|
||||
## new features
|
||||
* 🔧 new [config syntax](https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf) (#20)
|
||||
* the new syntax is still kinda esoteric and funky but it's an improvement
|
||||
* old config files are still supported
|
||||
* `--vc` prints the autoconverted config which you can copy back into the config file to upgrade
|
||||
* `--vc` will also [annotate and explain](https://user-images.githubusercontent.com/241032/217356028-eb3e141f-80a6-4bc6-8d04-d8d1d874c3e9.png) the config files
|
||||
* new argument `--cgen` to generate config from commandline arguments
|
||||
* kinda buggy, especially the `[global]` section, so give it a lookover before saving it
|
||||
* 🎲 randomize filenames on upload
|
||||
* either optionally, using the 🎲 button in the up2k ui
|
||||
* or force-enabled; globally with `--rand` or per-volume with volflag `rand`
|
||||
* specify filename length with `nrand` (globally or volflag), default 9
|
||||
* 🔗 export a list of links to your recent uploads
|
||||
* `copy links` in the up2k tab (🚀) will copy links to all uploads since last page refresh,
|
||||
* `copy` in the unpost tab (🧯) will copy links to all your recent uploads (max 2000 files / 12 hours by default)
|
||||
* filekeys are included if that's enabled and you have access to view those (permissions `G` or `r`)
|
||||
* 🇦 [arch package](https://github.com/9001/copyparty/tree/hovudstraum/contrib/package/arch) -- added in #18, thx @icxes
|
||||
* maybe in aur soon!
|
||||
* 🐳 [docker containers](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) -- 5 editions,
|
||||
* [min](https://hub.docker.com/r/copyparty/min) (57 MiB), just copyparty without thumbnails or audio transcoding
|
||||
* [im](https://hub.docker.com/r/copyparty/im) (70 MiB), thumbnails of popular image formats + media tags with mutagen
|
||||
* [ac (163 MiB)](https://hub.docker.com/r/copyparty/ac) 🥇 adds audio/video thumbnails + audio transcoding + better tags
|
||||
* [iv](https://hub.docker.com/r/copyparty/iv) (211 MiB), makes heif/avic/jxl faster to thumbnail
|
||||
* [dj](https://hub.docker.com/r/copyparty/dj) (309 MiB), adds optional detection of musical key / bpm
|
||||
* 🎶 [chiptune player](https://a.ocv.me/pub/demo/music/chiptunes/#af-f6fb2e5f)
|
||||
* transcodes mod/xm/s3m/it/mo3/mptm/mt2/okt to opus
|
||||
* uses FFmpeg (libopenmpt) so the accuracy is not perfect, but most files play OK enough
|
||||
* not **yet** supported in the docker container since Alpine's FFmpeg was built without libopenmpt
|
||||
* windows: support long filepaths (over 260 chars)
|
||||
* uses the `//?/` winapi syntax to also support windows 7
|
||||
* `--ver` shows the server version on the control panel
|
||||
|
||||
## bugfixes
|
||||
* markdown files didn't scale properly in the document browser
|
||||
* detect and refuse multiple volume definitions sharing the same filesystem path
|
||||
* don't return incomplete transcodes if multiple clients try to play the same flac file
|
||||
* [prisonparty](https://github.com/9001/copyparty/blob/hovudstraum/bin/prisonparty.sh): more reliable chroot cleanup, sigusr1 for config reload
|
||||
* pypi packaging: compress web resources, include webdav.bat
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0131-2103 `v1.6.3` sandbox k
|
||||
|
||||
* read-only demo server at https://a.ocv.me/pub/demo/
|
||||
* and since [1.6.0](https://github.com/9001/copyparty/releases/tag/v1.6.2) only got 2 days of prime time,
|
||||
* [1.6 theme song](https://a.ocv.me/pub/demo/music/.bonus/#af-134e597c) (hosted on the demo server)
|
||||
* [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) / feature comparison
|
||||
|
||||
## new features
|
||||
* dotfiles are hidden from search results by default
|
||||
* use `--dotsrch` or volflags `dotsrch` / `nodotsrch` to specify otherwise
|
||||
* 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
|
||||
|
||||
## bugfixes
|
||||
* lots of cosmetic fixes for the new readme/prologue/epilogue sandbox
|
||||
* rushed it into the previous release when someone suggested it, bad idea
|
||||
* still flickers a bit (especially prologues), and hotkeys are blocked while the sandboxed document has focus
|
||||
* can be disabled with `--no-sb-md --no-sb-lg` (not recommended)
|
||||
* support webdav uploads from davfs2 (fix LOCK response)
|
||||
* always unlink files before overwriting them, in case they are hardlinks
|
||||
* was primarily an issue with `--daw` and webdav clients
|
||||
* on windows, replace characters in PUT filenames as necessary
|
||||
* [prisonparty](https://github.com/9001/copyparty/blob/hovudstraum/bin/prisonparty.sh): support opus transcoding on debian
|
||||
* `rm -rf .hist/ac` to clear the transcode cache if the old version broke some songs
|
||||
|
||||
## other changes
|
||||
* add `rel="nofollow"` to zip download links, basic-browser link
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0129-1842 `v1.6.2` cors k
|
||||
|
||||
[Ellie Goulding - Stay Awake (kors k Hardcore Bootleg).mp3](https://a.ocv.me/pub/demo/music/.bonus/#af-134e597c)
|
||||
* 👆 the read-only demo server at https://a.ocv.me/pub/demo/
|
||||
|
||||
## breaking changes
|
||||
but nothing is affected (that i know of):
|
||||
* all requests must pass [cors validation](https://github.com/9001/copyparty#cors)
|
||||
* but they almost definitely did already
|
||||
* sharex and others are OK since they don't supply an `Origin` header
|
||||
* [API calls](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#http-api) `?delete` and `?move` are now POST instead of GET
|
||||
* not aware of any clients using these
|
||||
|
||||
## known issues
|
||||
* the document sandbox is a bit laggy and sometimes eats hotkeys
|
||||
* disable it with `--no-sb-md --no-sb-lg` if you trust everyone who has write and/or move access
|
||||
|
||||
## new features
|
||||
* [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
|
||||
* [configurable cors](https://github.com/9001/copyparty#cors) (cross-origin resource sharing) behavior; defaults are mostly same as before
|
||||
* `--allow-csrf` disables all csrf protections and makes it intentionally trivial to send authenticated requests from other domains
|
||||
* sandboxed readme.md / prologues / epilogues
|
||||
* 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
|
||||
* unfortunately disables hotkeys while the text has focus + blocks dragdropping files onto that area, oh well
|
||||
* password can be provided through http header `PW:` (instead of cookie `cppwd` or or url-param `?pw`)
|
||||
* detect network changes (new NICs, IPs) and reconfigure / reannoucne zeroconf
|
||||
* fixes mdns when running as a systemd service and copyparty is started before networking is up
|
||||
* add `--freebind` to start listening on IPs before the NIC is up yet (linux-only)
|
||||
* per-volume deduplication-control with volflags `hardlink`, `neversymlink`, `copydupes`
|
||||
* detect curl and return a [colorful, sortable plaintext](https://user-images.githubusercontent.com/241032/215322619-ea5fd606-3654-40ad-94ee-2bc058647bb2.png) directory listing instead
|
||||
* add optional [powered-by-copyparty](https://user-images.githubusercontent.com/241032/215322626-11d1f02b-25f4-45df-a3d9-f8c51354a8eb.png) footnode on the controlpanel
|
||||
* can be disabled with `-nb` or redirected with `--pb-url`
|
||||
|
||||
## bugfixes
|
||||
* change some API calls (`?delete`, `?move`) from `GET` to `POST`
|
||||
* 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)
|
||||
* `--getmod` restores the GETs if you need the convenience and accept the risks
|
||||
* [u2cli](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) (command-line uploader):
|
||||
* recover from network hiccups
|
||||
* add `-ns` for slow uefi TTYs
|
||||
* separate login cookies for http / https
|
||||
* avoids an https login from getting accidentally sent over plaintext
|
||||
* sadly no longer possible to login with internet explorer 4.0 / windows 3.11
|
||||
* tar/zip-download of hidden folders
|
||||
* unpost filtering was buggy for non-ascii characters
|
||||
* moving a deduplicated file on a volume where deduplication was since disabled
|
||||
* improved the [linux 6.0.16](https://utcc.utoronto.ca/~cks/space/blog/linux/KernelBindBugIn6016) kernel bug [workaround](https://github.com/9001/copyparty/commit/9065226c3d634a9fc15b14a768116158bc1761ad) because there is similar funk in 5.x
|
||||
* add custom text selection colors because chrome is currently broken on fedora
|
||||
* blockdevs (`/dev/nvme0n1`) couldn't be downloaded as files
|
||||
* misc fixes for location-based reverse-proxying
|
||||
* macos dualstack thing
|
||||
|
||||
## other changes
|
||||
* added a collection of [cursed usecases](https://github.com/9001/copyparty/tree/hovudstraum/docs/cursed-usecases)
|
||||
* and [comparisons to similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) in case you ever wanna jump ship
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0112-0515 `v1.5.6` many hands
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# this file gets included twice from ../some.conf,
|
||||
# setting user permissions for a volume
|
||||
rw usr1
|
||||
r usr2
|
||||
% sibling.conf
|
||||
accs:
|
||||
rw: usr1
|
||||
r: usr2
|
||||
% sibling.conf
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# and this config file gets included from ./another.conf,
|
||||
# adding a final permission for each of the two volumes in ../some.conf
|
||||
m usr1 usr2
|
||||
m: usr1, usr2
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
# not actually YAML but lets pretend:
|
||||
# -*- mode: yaml -*-
|
||||
# vim: ft=yaml:
|
||||
|
||||
# lets make two volumes with the same accounts/permissions for both;
|
||||
# first declare the accounts just once:
|
||||
u usr1:passw0rd
|
||||
u usr2:letmein
|
||||
[accounts]
|
||||
usr1: passw0rd
|
||||
usr2: letmein
|
||||
|
||||
# and listen on 127.0.0.1 only, port 2434
|
||||
-i 127.0.0.1
|
||||
-p 2434
|
||||
[global]
|
||||
i: 127.0.0.1 # listen on 127.0.0.1 only,
|
||||
p: 2434 # port 2434
|
||||
e2ds # enable file indexing+scanning
|
||||
e2ts # and multimedia indexing+scanning
|
||||
# (inline comments are OK if there is 2 spaces before the #)
|
||||
|
||||
# share /usr/share/games from the server filesystem
|
||||
/usr/share/games
|
||||
/vidya
|
||||
# include config file with volume permissions
|
||||
% foo/another.conf
|
||||
[/vidya]
|
||||
/usr/share/games
|
||||
% foo/another.conf # include config file with volume permissions
|
||||
|
||||
# and share your ~/Music folder too
|
||||
~/Music
|
||||
/bangers
|
||||
% foo/another.conf
|
||||
[/bangers]
|
||||
~/Music
|
||||
% foo/another.conf
|
||||
|
||||
# which should result in each of the volumes getting the following permissions:
|
||||
# usr1 read/write/move
|
||||
|
||||
@@ -17,6 +17,6 @@ problem: `svchost.exe` is using 100% of a cpu core, and upon further inspection
|
||||
"solution": create a virtual filesystem which is intentionally slow and trick windows into reading it from there instead
|
||||
|
||||
* create a file called `AppxManifest.xml` and put something dumb in it
|
||||
* serve the file from a copyparty instance with `--rsp-slp=9` so every request will hang for 9 sec
|
||||
* serve the file from a copyparty instance with `--rsp-slp=1` so every request will hang for 1 sec
|
||||
* `net use m: http://127.0.0.1:3993/` (mount copyparty using the windows-native webdav client)
|
||||
* `mklink /d c:\windows\systemapps\microsoftwindows.client.cbs_cw5n1h2txyewy\AppxManifest.xml m:\AppxManifest.xml`
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* [future plans](#future-plans) - some improvement ideas
|
||||
* [design](#design)
|
||||
* [up2k](#up2k) - quick outline of the up2k protocol
|
||||
* [why not tus](#why-not-tus) - I didn't know about [tus](https://tus.io/)
|
||||
* [why chunk-hashes](#why-chunk-hashes) - a single sha512 would be better, right?
|
||||
* [http api](#http-api)
|
||||
* [read](#read)
|
||||
@@ -16,6 +17,7 @@
|
||||
* [building](#building)
|
||||
* [dev env setup](#dev-env-setup)
|
||||
* [just the sfx](#just-the-sfx)
|
||||
* [build from release tarball](#build-from-release-tarball) - uses the included prebuilt webdeps
|
||||
* [complete release](#complete-release)
|
||||
* [todo](#todo) - roughly sorted by priority
|
||||
* [discarded ideas](#discarded-ideas)
|
||||
@@ -25,13 +27,15 @@
|
||||
|
||||
some improvement ideas
|
||||
|
||||
* the JS is a mess -- a preact rewrite would be nice
|
||||
* the JS is a mess -- a ~~preact~~ rewrite would be nice
|
||||
* preferably without build dependencies like webpack/babel/node.js, maybe a python thing to assemble js files into main.js
|
||||
* good excuse to look at using virtual lists (browsers start to struggle when folders contain over 5000 files)
|
||||
* maybe preact / vdom isn't the best choice, could just wait for the Next Big Thing
|
||||
* the UX is a mess -- a proper design would be nice
|
||||
* very organic (much like the python/js), everything was an afterthought
|
||||
* true for both the layout and the visual flair
|
||||
* something like the tron board-room ui (or most other hollywood ones, like ironman) would be :100:
|
||||
* would preferably keep the information density, just more organized yet [not too boring](https://blog.rachelbinx.com/2023/02/unbearable-sameness/)
|
||||
* some of the python files are way too big
|
||||
* `up2k.py` ended up doing all the file indexing / db management
|
||||
* `httpcli.py` should be separated into modules in general
|
||||
@@ -64,6 +68,13 @@ regarding the frequent server log message during uploads;
|
||||
* on this http connection, `2.77 GiB` transferred, `102.9 MiB/s` average, `948` chunks handled
|
||||
* client says `4` uploads OK, `0` failed, `3` busy, `1` queued, `10042 MiB` total size, `7198 MiB` and `00:01:09` left
|
||||
|
||||
## why not tus
|
||||
|
||||
I didn't know about [tus](https://tus.io/) when I made this, but:
|
||||
* 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)
|
||||
* 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
|
||||
* and the hashing happens in a separate thread anyways so it's usually not a bottleneck
|
||||
|
||||
## why chunk-hashes
|
||||
|
||||
a single sha512 would be better, right?
|
||||
@@ -74,7 +85,7 @@ as a result, the hashes are much less useful than they could have been (search t
|
||||
|
||||
however it allows for hashing multiple chunks in parallel, greatly increasing upload speed from fast storage (NVMe, raid-0 and such)
|
||||
|
||||
* both the [browser uploader](https://github.com/9001/copyparty#uploading) and the [commandline one](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) does this now, allowing for fast uploading even from plaintext http
|
||||
* 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
|
||||
|
||||
hashwasm 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
|
||||
|
||||
@@ -100,6 +111,7 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
|
||||
| GET | `?ls&dots` | list files/folders at URL as JSON, including dotfiles |
|
||||
| GET | `?ls=t` | list files/folders at URL as plaintext |
|
||||
| GET | `?ls=v` | list files/folders at URL, terminal-formatted |
|
||||
| GET | `?lt` | in listings, use symlink timestamps rather than targets |
|
||||
| GET | `?b` | list files/folders at URL as simplified HTML |
|
||||
| GET | `?tree=.` | list one level of subdirectories inside URL |
|
||||
| GET | `?tree` | list one level of subdirectories for each level until URL |
|
||||
@@ -216,34 +228,55 @@ pip install mutagen # audio metadata
|
||||
pip install pyftpdlib # ftp server
|
||||
pip install impacket # smb server -- disable Windows Defender if you REALLY need this on windows
|
||||
pip install Pillow pyheif-pillow-opener pillow-avif-plugin # thumbnails
|
||||
pip install pyvips # faster thumbnails
|
||||
pip install psutil # better cleanup of stuck metadata parsers on windows
|
||||
pip install black==21.12b0 click==8.0.2 bandit pylint flake8 isort mypy # vscode tooling
|
||||
```
|
||||
|
||||
|
||||
## just the sfx
|
||||
|
||||
first grab the web-dependencies from a previous sfx (assuming you don't need to modify something in those):
|
||||
if you just want to modify the copyparty source code (py/html/css/js) then this is the easiest approach
|
||||
|
||||
```sh
|
||||
rm -rf copyparty/web/deps
|
||||
curl -L https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py >x.py
|
||||
python3 x.py --version
|
||||
rm x.py
|
||||
mv /tmp/pe-copyparty/copyparty/web/deps/ copyparty/web/deps/
|
||||
```
|
||||
|
||||
then build the sfx using any of the following examples:
|
||||
build the sfx using any of the following examples:
|
||||
|
||||
```sh
|
||||
./scripts/make-sfx.sh # regular edition
|
||||
./scripts/make-sfx.sh fast # build faster (worse js/css compression)
|
||||
./scripts/make-sfx.sh gz no-cm # gzip-compressed + no fancy markdown editor
|
||||
```
|
||||
|
||||
|
||||
## build from release tarball
|
||||
|
||||
uses the included prebuilt webdeps
|
||||
|
||||
if 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,
|
||||
|
||||
```bash
|
||||
python3 -m pip install --user -U build setuptools wheel jinja2 strip_hints
|
||||
bash scripts/run-tests.sh python3 # optional
|
||||
python3 -m build
|
||||
```
|
||||
|
||||
if you are unable to use `build`, you can use the old setuptools approach instead,
|
||||
|
||||
```bash
|
||||
python3 setup.py install --user setuptools wheel jinja2
|
||||
python3 setup.py build
|
||||
# you now have a wheel which you can install. or extract and repackage:
|
||||
python3 setup.py install --skip-build --prefix=/usr --root=$HOME/pe/copyparty
|
||||
```
|
||||
|
||||
|
||||
## complete release
|
||||
|
||||
also builds the sfx so skip the sfx section above
|
||||
|
||||
*WARNING: `rls.sh` has not yet been updated with the docker-images and arch/nix packaging*
|
||||
|
||||
does everything completely from scratch, straight from your local repo
|
||||
|
||||
in the `scripts` folder:
|
||||
|
||||
* run `make -C deps-docker` to build all dependencies
|
||||
|
||||
@@ -1,59 +1,69 @@
|
||||
# not actually YAML but lets pretend:
|
||||
# -*- mode: yaml -*-
|
||||
# vim: ft=yaml:
|
||||
|
||||
# append some arguments to the commandline;
|
||||
# the first space in a line counts as a separator,
|
||||
# any additional spaces are part of the value
|
||||
-e2dsa
|
||||
-e2ts
|
||||
-i 127.0.0.1
|
||||
# accepts anything listed in --help (leading dashes are optional)
|
||||
# and inline comments are OK if there is 2 spaces before the '#'
|
||||
[global]
|
||||
p: 8086, 3939 # listen on ports 8086 and 3939
|
||||
e2dsa # enable file indexing and filesystem scanning
|
||||
e2ts # and enable multimedia indexing
|
||||
z, qr # and zeroconf and qrcode (you can comma-separate arguments)
|
||||
|
||||
# create users:
|
||||
# u username:password
|
||||
u ed:123
|
||||
u k:k
|
||||
[accounts]
|
||||
ed: 123 # username: password
|
||||
k: k
|
||||
|
||||
# leave a blank line between volumes
|
||||
# (and also between users and volumes)
|
||||
# create volumes:
|
||||
[/] # create a volume at "/" (the webroot), which will
|
||||
. # share the contents of "." (the current directory)
|
||||
accs:
|
||||
r: * # everyone gets read-access, but
|
||||
rw: ed # the user "ed" gets read-write
|
||||
|
||||
# create a volume:
|
||||
# share "." (the current directory)
|
||||
# as "/" (the webroot) for the following users:
|
||||
# "r" grants read-access for anyone
|
||||
# "rw ed" grants read-write to ed
|
||||
.
|
||||
/
|
||||
r
|
||||
rw ed
|
||||
|
||||
# custom permissions for the "priv" folder:
|
||||
# user "k" can only see/read the contents
|
||||
# user "ed" gets read-write access
|
||||
./priv
|
||||
/priv
|
||||
r k
|
||||
rw ed
|
||||
|
||||
# this does the same thing,
|
||||
# and will cause an error on startup since /priv is already taken:
|
||||
./priv
|
||||
/priv
|
||||
r ed k
|
||||
w ed
|
||||
# let's specify different permissions for the "priv" subfolder
|
||||
# by creating another volume at that location:
|
||||
[/priv]
|
||||
./priv
|
||||
accs:
|
||||
r: k # the user "k" can see the contents,
|
||||
rw: ed # while "ed" gets read-write
|
||||
|
||||
# share /home/ed/Music/ as /music and let anyone read it
|
||||
# (this will replace any folder called "music" in the webroot)
|
||||
/home/ed/Music
|
||||
/music
|
||||
r
|
||||
[/music]
|
||||
/home/ed/Music
|
||||
accs:
|
||||
r: *
|
||||
|
||||
# and a folder where anyone can upload, but nobody can see the contents
|
||||
[/dump]
|
||||
/home/ed/inc
|
||||
accs:
|
||||
w: *
|
||||
flags:
|
||||
e2d # the e2d volflag enables the uploads database
|
||||
nodupe # the nodupe volflag rejects duplicate uploads
|
||||
# (see --help-flags for all available volflags to use)
|
||||
|
||||
# and a folder where anyone can upload
|
||||
# but nobody can see the contents
|
||||
# and set the e2d flag to enable the uploads database
|
||||
# and set the nodupe flag to reject duplicate uploads
|
||||
/home/ed/inc
|
||||
/dump
|
||||
w
|
||||
c e2d
|
||||
c nodupe
|
||||
# and anyone can access their own uploads, but nothing else
|
||||
[/sharex]
|
||||
/home/ed/inc/sharex
|
||||
accs:
|
||||
wG: * # wG = write-upget = see your own uploads only
|
||||
rwmd: ed, k # read-write-modify-delete for users "ed" and "k"
|
||||
flags:
|
||||
e2d, d2t, fk: 4
|
||||
# volflag "e2d" enables the uploads database,
|
||||
# "d2t" disables multimedia parsers (in case the uploads are malicious),
|
||||
# "dthumb" disables thumbnails (same reason),
|
||||
# "fk" enables filekeys (necessary for upget permission) (4 chars long)
|
||||
# -- note that its fine to combine all the volflags on
|
||||
# one line because only the last volflag has an argument
|
||||
|
||||
# this entire config file can be replaced with these arguments:
|
||||
# -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
|
||||
# -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
|
||||
# but note that the config file always wins in case of conflicts
|
||||
|
||||
4
docs/examples/README.md
Normal file
4
docs/examples/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
copyparty server config examples
|
||||
|
||||
[windows.md](windows.md) -- running copyparty as a service on windows
|
||||
|
||||
115
docs/examples/windows.md
Normal file
115
docs/examples/windows.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# running copyparty on windows
|
||||
|
||||
this is a complete example / quickstart for running copyparty on windows, optionally as a service (autostart on boot)
|
||||
|
||||
you 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)
|
||||
|
||||
* 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/)
|
||||
|
||||
then you probably want to download [FFmpeg](https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip) 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
|
||||
|
||||
|
||||
## the config file
|
||||
|
||||
open up notepad and save the following as `c:\users\you\documents\party.conf` (for example)
|
||||
|
||||
```yaml
|
||||
[global]
|
||||
lo: ~/logs/cpp-%Y-%m%d.xz # log to c:\users\you\logs\
|
||||
e2dsa, e2ts, no-dedup, z # sets 4 flags; see expl.
|
||||
p: 80, 443 # listen on ports 80 and 443, not 3923
|
||||
theme: 2 # default theme: protonmail-monokai
|
||||
lang: nor # default language: viking
|
||||
|
||||
[accounts] # usernames and passwords
|
||||
kevin: shangalabangala # kevin's password
|
||||
|
||||
[/] # create a volume available at /
|
||||
c:\pub # sharing this filesystem location
|
||||
accs: # and set permissions:
|
||||
r: * # everyone can read/download files,
|
||||
rwmd: kevin # kevin can read/write/move/delete
|
||||
|
||||
[/inc] # create another volume at /inc
|
||||
c:\pub\inc # sharing this filesystem location
|
||||
accs: # permissions:
|
||||
w: * # everyone can upload, but not browse
|
||||
rwmd: kevin # kevin is admin here too
|
||||
|
||||
[/music] # and a third volume at /music
|
||||
~/music # which shares c:\users\you\music
|
||||
accs:
|
||||
r: *
|
||||
rwmd: kevin
|
||||
```
|
||||
|
||||
|
||||
### config explained: [global]
|
||||
|
||||
the `[global]` section accepts any config parameters you can see when 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 --no-dedup -z -p 80,443 --theme 2 --lang nor`
|
||||
* `lo: ~/logs/cpp-%Y-%m%d.xz` writes compressed logs (the compression will make them delayed)
|
||||
* `e2dsa` enables the upload deduplicator and file indexer, which enables searching
|
||||
* `e2ts` enables music metadata indexing, making albums / titles etc. searchable too
|
||||
* `no-dedup` writes full dupes to disk instead of symlinking, since lots of windows software doesn't handle symlinks well
|
||||
* but the improved upload speed from `e2dsa` is not affected
|
||||
* `z` enables zeroconf, making the server available at `http://HOSTNAME.local/` from any other machine in the LAN
|
||||
* `p: 80,443` listens on the ports `80` and `443` instead of the default `3923`
|
||||
* `lang: nor` sets default language to viking
|
||||
|
||||
|
||||
### config explained: [accounts]
|
||||
|
||||
the `[accounts]` section defines all the user accounts, which can then be referenced when granting people access to the different volumes
|
||||
|
||||
|
||||
### config explained: volumes
|
||||
|
||||
then we create three volumes, one at `/`, one at `/inc`, and one at `/music`
|
||||
* `/` and `/music` are readable without requiring people to login (`r: *`) but you need to login as kevin to write/move/delete files (`rwmd: kevin`)
|
||||
* anyone can upload to `/inc` but you must be logged in as kevin to see the files inside
|
||||
|
||||
|
||||
## run copyparty
|
||||
|
||||
to test your config it's best to just run copyparty in a console to watch the output:
|
||||
|
||||
```batch
|
||||
copyparty.exe -c party.conf
|
||||
```
|
||||
|
||||
or if you wanna use `copyparty-sfx.py` instead of the exe (understandable),
|
||||
|
||||
```batch
|
||||
%localappdata%\programs\python\python311\python.exe copyparty-sfx.py -c party.conf
|
||||
```
|
||||
|
||||
(please adjust `python311` to match the python version you installed, i'm not good enough at windows to make that bit generic)
|
||||
|
||||
|
||||
## run it as a service
|
||||
|
||||
to 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
|
||||
|
||||
then either do this for `copyparty.exe`:
|
||||
```batch
|
||||
nssm install cpp %homedrive%%homepath%\downloads\copyparty.exe -c %homedrive%%homepath%\documents\party.conf
|
||||
```
|
||||
|
||||
or do this for `copyparty-sfx.py`:
|
||||
```batch
|
||||
nssm install cpp %localappdata%\programs\python\python311\python.exe %homedrive%%homepath%\downloads\copyparty-sfx.py -c %homedrive%%homepath%\documents\party.conf
|
||||
```
|
||||
|
||||
then 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):
|
||||
```batch
|
||||
nssm set cpp ObjectName .\yourAccoutName yourWindowsPassword
|
||||
nssm start cpp
|
||||
```
|
||||
|
||||
and that's it, all good
|
||||
|
||||
if it doesn't start, enable stderr logging so you can see what went wrong:
|
||||
```batch
|
||||
nssm set cpp AppStderr %homedrive%%homepath%\logs\cppsvc.err
|
||||
nssm set cpp AppStderrCreationDisposition 2
|
||||
```
|
||||
@@ -194,6 +194,9 @@ sqlite3 .hist/up2k.db 'select * from mt where k="fgsfds" or k="t:mtp"' | tee /de
|
||||
for ((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
|
||||
ffmpeg -y -f lavfi -i sine=frequency=1050:duration=2 -vf volume=0.1 -ac 1 -ar 44100 /dev/shm/a.wav
|
||||
|
||||
# better sine
|
||||
sox -DnV -r8000 -b8 -c1 /dev/shm/a.wav synth 1.1 sin 400 vol 0.02
|
||||
|
||||
# play icon calibration pics
|
||||
for 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
|
||||
|
||||
|
||||
2
docs/protocol-reference.sh
Normal file
2
docs/protocol-reference.sh
Normal file
@@ -0,0 +1,2 @@
|
||||
vsftpd a.conf -olisten=YES -olisten_port=3921 -orun_as_launching_user=YES -obackground=NO -olog_ftp_protocol=YES
|
||||
|
||||
@@ -14,6 +14,10 @@ when server is on another machine (1gbit LAN),
|
||||
|
||||
# creating the config file
|
||||
|
||||
the copyparty "connect" page at `/?hc` (so for example http://127.0.0.1:3923/?hc) will generate commands to autoconfigure rclone for your server
|
||||
|
||||
**if you prefer to configure rclone manually, continue reading:**
|
||||
|
||||
replace `hunter2` with your password, or remove the `hunter2` lines if you allow anonymous access
|
||||
|
||||
|
||||
@@ -22,14 +26,16 @@ replace `hunter2` with your password, or remove the `hunter2` lines if you allow
|
||||
(
|
||||
echo [cpp-rw]
|
||||
echo type = webdav
|
||||
echo vendor = other
|
||||
echo vendor = owncloud
|
||||
echo url = http://127.0.0.1:3923/
|
||||
echo headers = Cookie,cppwd=hunter2
|
||||
echo pacer_min_sleep = 0.01ms
|
||||
echo(
|
||||
echo [cpp-ro]
|
||||
echo type = http
|
||||
echo url = http://127.0.0.1:3923/
|
||||
echo headers = Cookie,cppwd=hunter2
|
||||
echo pacer_min_sleep = 0.01ms
|
||||
) > %userprofile%\.config\rclone\rclone.conf
|
||||
```
|
||||
|
||||
@@ -41,14 +47,16 @@ also install the windows dependencies: [winfsp](https://github.com/billziss-gh/w
|
||||
cat > ~/.config/rclone/rclone.conf <<'EOF'
|
||||
[cpp-rw]
|
||||
type = webdav
|
||||
vendor = other
|
||||
vendor = owncloud
|
||||
url = http://127.0.0.1:3923/
|
||||
headers = Cookie,cppwd=hunter2
|
||||
pacer_min_sleep = 0.01ms
|
||||
|
||||
[cpp-ro]
|
||||
type = http
|
||||
url = http://127.0.0.1:3923/
|
||||
headers = Cookie,cppwd=hunter2
|
||||
pacer_min_sleep = 0.01ms
|
||||
EOF
|
||||
```
|
||||
|
||||
@@ -62,6 +70,15 @@ rclone.exe mount --vfs-cache-mode writes --vfs-cache-max-age 5s --attr-timeout 5
|
||||
```
|
||||
|
||||
|
||||
# sync folders to/from copyparty
|
||||
|
||||
note 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
|
||||
|
||||
```
|
||||
rclone sync /usr/share/icons/ cpp-rw:fds/
|
||||
```
|
||||
|
||||
|
||||
# use rclone as server too, replacing copyparty
|
||||
|
||||
feels out of place but is too good not to mention
|
||||
@@ -70,3 +87,26 @@ feels out of place but is too good not to mention
|
||||
rclone.exe serve http --read-only .
|
||||
rclone.exe serve webdav .
|
||||
```
|
||||
|
||||
|
||||
# devnotes
|
||||
|
||||
copyparty supports and expects [the following](https://github.com/rclone/rclone/blob/46484022b08f8756050aa45505ea0db23e62df8b/backend/webdav/webdav.go#L575-L578) from rclone,
|
||||
|
||||
```go
|
||||
case "owncloud":
|
||||
f.canStream = true
|
||||
f.precision = time.Second
|
||||
f.useOCMtime = true
|
||||
f.hasOCMD5 = true
|
||||
f.hasOCSHA1 = true
|
||||
```
|
||||
|
||||
notably,
|
||||
* `useOCMtime` enables the `x-oc-mtime` header to retain mtime of uploads from rclone
|
||||
* `canStream` is supported but not required by us
|
||||
* `hasOCMD5` / `hasOCSHA1` is conveniently dontcare on both ends
|
||||
|
||||
there's a scary comment mentioning PROPSET of lastmodified which is not something we wish to support
|
||||
|
||||
and if `vendor=owncloud` ever stops working, try `vendor=fastmail` instead
|
||||
|
||||
438
docs/versus.md
438
docs/versus.md
@@ -7,6 +7,21 @@ there is probably some unintentional bias so please submit corrections
|
||||
currently up to date with [awesome-selfhosted](https://github.com/awesome-selfhosted/awesome-selfhosted) but that probably won't last
|
||||
|
||||
|
||||
## symbol legends
|
||||
|
||||
### ...in feature matrices:
|
||||
* `█` = absolutely
|
||||
* `╱` = partially
|
||||
* `•` = maybe?
|
||||
* ` ` = nope
|
||||
|
||||
### ...in reviews:
|
||||
* ✅ = advantages over copyparty
|
||||
* 💾 = what copyparty offers as an alternative
|
||||
* 🔵 = similarities
|
||||
* ⚠️ = disadvantages (something copyparty does "better")
|
||||
|
||||
|
||||
## toc
|
||||
|
||||
* top
|
||||
@@ -32,11 +47,15 @@ currently up to date with [awesome-selfhosted](https://github.com/awesome-selfho
|
||||
* [kodbox](#kodbox)
|
||||
* [filebrowser](#filebrowser)
|
||||
* [filegator](#filegator)
|
||||
* [sftpgo](#sftpgo)
|
||||
* [updog](#updog)
|
||||
* [goshs](#goshs)
|
||||
* [gimme-that](#gimme-that)
|
||||
* [ass](#ass)
|
||||
* [linx](#linx)
|
||||
* [h5ai](#h5ai)
|
||||
* [autoindex](#autoindex)
|
||||
* [miniserve](#miniserve)
|
||||
* [briefly considered](#briefly-considered)
|
||||
|
||||
|
||||
@@ -63,8 +82,8 @@ the table headers in the matrixes below are the different softwares, with a quic
|
||||
|
||||
the softwares,
|
||||
* `a` = [copyparty](https://github.com/9001/copyparty)
|
||||
* `b` = [hfs2](https://github.com/rejetto/hfs2)
|
||||
* `c` = [hfs3](https://www.rejetto.com/hfs/)
|
||||
* `b` = [hfs2](https://rejetto.com/hfs/)
|
||||
* `c` = [hfs3](https://github.com/rejetto/hfs)
|
||||
* `d` = [nextcloud](https://github.com/nextcloud/server)
|
||||
* `e` = [seafile](https://github.com/haiwen/seafile)
|
||||
* `f` = [rclone](https://github.com/rclone/rclone), specifically `rclone serve webdav .`
|
||||
@@ -73,6 +92,7 @@ the softwares,
|
||||
* `i` = [kodbox](https://github.com/kalcaddle/kodbox)
|
||||
* `j` = [filebrowser](https://github.com/filebrowser/filebrowser)
|
||||
* `k` = [filegator](https://github.com/filegator/filegator)
|
||||
* `l` = [sftpgo](https://github.com/drakkan/sftpgo)
|
||||
|
||||
some softwares not in the matrixes,
|
||||
* [updog](#updog)
|
||||
@@ -80,6 +100,9 @@ some softwares not in the matrixes,
|
||||
* [gimme-that](#gimmethat)
|
||||
* [ass](#ass)
|
||||
* [linx](#linx)
|
||||
* [h5ai](#h5ai)
|
||||
* [autoindex](#autoindex)
|
||||
* [miniserve](#miniserve)
|
||||
|
||||
symbol legend,
|
||||
* `█` = absolutely
|
||||
@@ -90,62 +113,64 @@ symbol legend,
|
||||
|
||||
## general
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| intuitive UX | | ╱ | █ | █ | █ | | █ | █ | █ | █ | █ |
|
||||
| config GUI | | █ | █ | █ | █ | | | █ | █ | █ | |
|
||||
| good documentation | | | | █ | █ | █ | █ | | | █ | █ |
|
||||
| runs on iOS | ╱ | | | | | ╱ | | | | | |
|
||||
| runs on Android | █ | | | | | █ | | | | | |
|
||||
| runs on WinXP | █ | █ | | | | █ | | | | | |
|
||||
| runs on Windows | █ | █ | █ | █ | █ | █ | █ | ╱ | █ | █ | █ |
|
||||
| runs on Linux | █ | ╱ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| runs on Macos | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| runs on FreeBSD | █ | | | • | █ | █ | █ | • | █ | █ | |
|
||||
| portable binary | █ | █ | █ | | | █ | █ | | | █ | |
|
||||
| zero setup, just go | █ | █ | █ | | | ╱ | █ | | | █ | |
|
||||
| android app | ╱ | | | █ | █ | | | | | | |
|
||||
| iOS app | | | | █ | █ | | | | | | |
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| intuitive UX | | ╱ | █ | █ | █ | | █ | █ | █ | █ | █ | █ |
|
||||
| config GUI | | █ | █ | █ | █ | | | █ | █ | █ | | █ |
|
||||
| good documentation | | | | █ | █ | █ | █ | | | █ | █ | ╱ |
|
||||
| runs on iOS | ╱ | | | | | ╱ | | | | | | |
|
||||
| runs on Android | █ | | | | | █ | | | | | | |
|
||||
| runs on WinXP | █ | █ | | | | █ | | | | | | |
|
||||
| runs on Windows | █ | █ | █ | █ | █ | █ | █ | ╱ | █ | █ | █ | █ |
|
||||
| runs on Linux | █ | ╱ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| runs on Macos | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| runs on FreeBSD | █ | | | • | █ | █ | █ | • | █ | █ | | █ |
|
||||
| portable binary | █ | █ | █ | | | █ | █ | | | █ | | █ |
|
||||
| zero setup, just go | █ | █ | █ | | | ╱ | █ | | | █ | | ╱ |
|
||||
| android app | ╱ | | | █ | █ | | | | | | | |
|
||||
| iOS app | ╱ | | | █ | █ | | | | | | | |
|
||||
|
||||
* `zero setup` = you can get a mostly working setup by just launching the app, without having to install any software or configure whatever
|
||||
* `a`/copyparty remarks:
|
||||
* no gui for server settings; only for client-side stuff
|
||||
* can theoretically run on iOS / iPads using [iSH](https://ish.app/), but only the iPad will offer sufficient multitasking i think
|
||||
* [android app](https://f-droid.org/en/packages/me.ocv.partyup/) is for uploading only
|
||||
* no iOS app but has [shortcuts](https://github.com/9001/copyparty#ios-shortcuts) for easy uploading
|
||||
* `b`/hfs2 runs on linux through wine
|
||||
* `f`/rclone must be started with the command `rclone serve webdav .` or similar
|
||||
* `h`/chibisafe has undocumented windows support
|
||||
* `i`/sftpgo must be launched with a command
|
||||
|
||||
|
||||
## file transfer
|
||||
|
||||
*the thing that copyparty is actually kinda good at*
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| download folder as zip | █ | █ | █ | █ | █ | | █ | | █ | █ | ╱ |
|
||||
| download folder as tar | █ | | | | | | | | | █ | |
|
||||
| upload | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| parallel uploads | █ | | | █ | █ | | • | | █ | | █ |
|
||||
| resumable uploads | █ | | | | | | | | █ | | █ |
|
||||
| upload segmenting | █ | | | | | | | █ | █ | | █ |
|
||||
| upload acceleration | █ | | | | | | | | █ | | █ |
|
||||
| upload verification | █ | | | █ | █ | | | | █ | | |
|
||||
| upload deduplication | █ | | | | █ | | | | █ | | |
|
||||
| upload a 999 TiB file | █ | | | | █ | █ | • | | █ | | █ |
|
||||
| keep last-modified time | █ | | | █ | █ | █ | | | | | |
|
||||
| upload rules | ╱ | ╱ | ╱ | ╱ | ╱ | | | ╱ | ╱ | | ╱ |
|
||||
| ┗ max disk usage | █ | █ | | | █ | | | | █ | | |
|
||||
| ┗ max filesize | █ | | | | | | | █ | | | █ |
|
||||
| ┗ max items in folder | █ | | | | | | | | | | |
|
||||
| ┗ max file age | █ | | | | | | | | █ | | |
|
||||
| ┗ max uploads over time | █ | | | | | | | | | | |
|
||||
| ┗ compress before write | █ | | | | | | | | | | |
|
||||
| ┗ randomize filename | █ | | | | | | | █ | █ | | |
|
||||
| ┗ mimetype reject-list | ╱ | | | | | | | | • | ╱ | |
|
||||
| ┗ extension reject-list | ╱ | | | | | | | █ | • | ╱ | |
|
||||
| checksums provided | | | | █ | █ | | | | █ | ╱ | |
|
||||
| cloud storage backend | ╱ | ╱ | ╱ | █ | █ | █ | ╱ | | | ╱ | █ |
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| download folder as zip | █ | █ | █ | █ | █ | | █ | | █ | █ | ╱ | █ |
|
||||
| download folder as tar | █ | | | | | | | | | █ | | |
|
||||
| upload | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| parallel uploads | █ | | | █ | █ | | • | | █ | | █ | |
|
||||
| resumable uploads | █ | | | | | | | | █ | | █ | ╱ |
|
||||
| upload segmenting | █ | | | | | | | █ | █ | | █ | ╱ |
|
||||
| upload acceleration | █ | | | | | | | | █ | | █ | |
|
||||
| upload verification | █ | | | █ | █ | | | | █ | | | |
|
||||
| upload deduplication | █ | | | | █ | | | | █ | | | |
|
||||
| upload a 999 TiB file | █ | | | | █ | █ | • | | █ | | █ | ╱ |
|
||||
| keep last-modified time | █ | | | █ | █ | █ | | | | | | █ |
|
||||
| upload rules | ╱ | ╱ | ╱ | ╱ | ╱ | | | ╱ | ╱ | | ╱ | ╱ |
|
||||
| ┗ max disk usage | █ | █ | | | █ | | | | █ | | | █ |
|
||||
| ┗ max filesize | █ | | | | | | | █ | | | █ | █ |
|
||||
| ┗ max items in folder | █ | | | | | | | | | | | ╱ |
|
||||
| ┗ max file age | █ | | | | | | | | █ | | | |
|
||||
| ┗ max uploads over time | █ | | | | | | | | | | | ╱ |
|
||||
| ┗ compress before write | █ | | | | | | | | | | | |
|
||||
| ┗ randomize filename | █ | | | | | | | █ | █ | | | |
|
||||
| ┗ mimetype reject-list | ╱ | | | | | | | | • | ╱ | | ╱ |
|
||||
| ┗ extension reject-list | ╱ | | | | | | | █ | • | ╱ | | ╱ |
|
||||
| checksums provided | | | | █ | █ | | | | █ | ╱ | | |
|
||||
| cloud storage backend | ╱ | ╱ | ╱ | █ | █ | █ | ╱ | | | ╱ | █ | █ |
|
||||
|
||||
* `upload segmenting` = files are sliced into chunks, making it possible to upload files larger than 100 MiB on cloudflare for example
|
||||
|
||||
@@ -162,25 +187,29 @@ symbol legend,
|
||||
* can provide checksums for single files on request
|
||||
* can probably do extension/mimetype rejection similar to copyparty
|
||||
* `k`/filegator download-as-zip is not streaming; it creates the full zipfile before download can start
|
||||
* `l`/sftpgo:
|
||||
* resumable/segmented uploads only over SFTP, not over HTTP
|
||||
* upload rules are totals only, not over time
|
||||
* can probably do extension/mimetype rejection similar to copyparty
|
||||
|
||||
|
||||
## protocols and client support
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| serve https | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| serve webdav | █ | | | █ | █ | █ | █ | | █ | | |
|
||||
| serve ftp | █ | | | | | █ | | | | | |
|
||||
| serve ftps | █ | | | | | █ | | | | | |
|
||||
| serve sftp | | | | | | █ | | | | | |
|
||||
| serve smb/cifs | ╱ | | | | | █ | | | | | |
|
||||
| serve dlna | | | | | | █ | | | | | |
|
||||
| listen on unix-socket | | | | █ | █ | | █ | █ | █ | | █ |
|
||||
| zeroconf | █ | | | | | | | | | | |
|
||||
| supports netscape 4 | ╱ | | | | | █ | | | | | • |
|
||||
| ...internet explorer 6 | ╱ | █ | | █ | | █ | | | | | • |
|
||||
| mojibake filenames | █ | | | • | • | █ | █ | • | • | • | |
|
||||
| undecodable filenames | █ | | | • | • | █ | | • | • | | |
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| serve https | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| serve webdav | █ | | | █ | █ | █ | █ | | █ | | | █ |
|
||||
| serve ftp | █ | | | | | █ | | | | | | █ |
|
||||
| serve ftps | █ | | | | | █ | | | | | | █ |
|
||||
| serve sftp | | | | | | █ | | | | | | █ |
|
||||
| serve smb/cifs | ╱ | | | | | █ | | | | | | |
|
||||
| serve dlna | | | | | | █ | | | | | | |
|
||||
| listen on unix-socket | | | | █ | █ | | █ | █ | █ | | █ | █ |
|
||||
| zeroconf | █ | | | | | | | | | | | |
|
||||
| supports netscape 4 | ╱ | | | | | █ | | | | | • | |
|
||||
| ...internet explorer 6 | ╱ | █ | | █ | | █ | | | | | • | |
|
||||
| mojibake filenames | █ | | | • | • | █ | █ | • | • | • | | ╱ |
|
||||
| undecodable filenames | █ | | | • | • | █ | | • | • | | | ╱ |
|
||||
|
||||
* `webdav` = protocol convenient for mounting a remote server as a local filesystem; see zeroconf:
|
||||
* `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
|
||||
@@ -190,57 +219,62 @@ symbol legend,
|
||||
* `a`/copyparty remarks:
|
||||
* extremely minimal samba/cifs server
|
||||
* 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))
|
||||
* `l`/sftpgo translates mojibake filenames into valid utf-8 (information loss)
|
||||
|
||||
|
||||
## server configuration
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| config from cmd args | █ | | | | | █ | █ | | | █ | |
|
||||
| config files | █ | █ | █ | ╱ | ╱ | █ | | █ | | █ | • |
|
||||
| runtime config reload | █ | █ | █ | | | | | █ | █ | █ | █ |
|
||||
| same-port http / https | █ | | | | | | | | | | |
|
||||
| listen multiple ports | █ | | | | | | | | | | |
|
||||
| virtual file system | █ | █ | █ | | | | █ | | | | |
|
||||
| reverse-proxy ok | █ | | █ | █ | █ | █ | █ | █ | • | • | • |
|
||||
| folder-rproxy ok | █ | | | | █ | █ | | • | • | • | • |
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| config from cmd args | █ | | | | | █ | █ | | | █ | | ╱ |
|
||||
| config files | █ | █ | █ | ╱ | ╱ | █ | | █ | | █ | • | ╱ |
|
||||
| runtime config reload | █ | █ | █ | | | | | █ | █ | █ | █ | |
|
||||
| same-port http / https | █ | | | | | | | | | | | |
|
||||
| listen multiple ports | █ | | | | | | | | | | | █ |
|
||||
| virtual file system | █ | █ | █ | | | | █ | | | | | █ |
|
||||
| reverse-proxy ok | █ | | █ | █ | █ | █ | █ | █ | • | • | • | █ |
|
||||
| folder-rproxy ok | █ | | | | █ | █ | | • | • | • | • | |
|
||||
|
||||
* `folder-rproxy` = reverse-proxying without dedicating an entire (sub)domain, using a subfolder instead
|
||||
* `l`/sftpgo:
|
||||
* config: users must be added through gui / api calls
|
||||
|
||||
|
||||
## server capabilities
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| accounts | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| single-sign-on | | | | █ | █ | | | | • | | |
|
||||
| token auth | | | | █ | █ | | | █ | | | |
|
||||
| per-volume permissions | █ | █ | █ | █ | █ | █ | █ | | █ | █ | ╱ |
|
||||
| per-folder permissions | ╱ | | | █ | █ | | █ | | █ | █ | ╱ |
|
||||
| per-file permissions | | | | █ | █ | | █ | | █ | | |
|
||||
| per-file passwords | █ | | | █ | █ | | █ | | █ | | |
|
||||
| unmap subfolders | █ | | | | | | █ | | | █ | ╱ |
|
||||
| index.html blocks list | | | | | | | █ | | | • | |
|
||||
| write-only folders | █ | | | | | | | | | | █ |
|
||||
| files stored as-is | █ | █ | █ | █ | | █ | █ | | | █ | █ |
|
||||
| file versioning | | | | █ | █ | | | | | | |
|
||||
| file encryption | | | | █ | █ | █ | | | | | |
|
||||
| file indexing | █ | | █ | █ | █ | | | █ | █ | █ | |
|
||||
| ┗ per-volume db | █ | | • | • | • | | | • | • | | |
|
||||
| ┗ db stored in folder | █ | | | | | | | • | • | █ | |
|
||||
| ┗ db stored out-of-tree | █ | | █ | █ | █ | | | • | • | █ | |
|
||||
| ┗ existing file tree | █ | | █ | | | | | | | █ | |
|
||||
| file action event hooks | █ | | | | | | | | | █ | |
|
||||
| one-way folder sync | █ | | | █ | █ | █ | | | | | |
|
||||
| full sync | | | | █ | █ | | | | | | |
|
||||
| speed throttle | | █ | █ | | | █ | | | █ | | |
|
||||
| anti-bruteforce | █ | █ | █ | █ | █ | | | | • | | |
|
||||
| dyndns updater | | █ | | | | | | | | | |
|
||||
| self-updater | | | █ | | | | | | | | |
|
||||
| log rotation | █ | | █ | █ | █ | | | • | █ | | |
|
||||
| upload tracking / log | █ | █ | • | █ | █ | | | █ | █ | | |
|
||||
| curl-friendly ls | █ | | | | | | | | | | |
|
||||
| curl-friendly upload | █ | | | | | █ | █ | • | | | |
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| accounts | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| per-account chroot | | | | | | | | | | | | █ |
|
||||
| single-sign-on | | | | █ | █ | | | | • | | | |
|
||||
| token auth | | | | █ | █ | | | █ | | | | |
|
||||
| 2fa | | | | █ | █ | | | | | | | █ |
|
||||
| per-volume permissions | █ | █ | █ | █ | █ | █ | █ | | █ | █ | ╱ | █ |
|
||||
| per-folder permissions | ╱ | | | █ | █ | | █ | | █ | █ | ╱ | █ |
|
||||
| per-file permissions | | | | █ | █ | | █ | | █ | | | |
|
||||
| per-file passwords | █ | | | █ | █ | | █ | | █ | | | |
|
||||
| unmap subfolders | █ | | | | | | █ | | | █ | ╱ | • |
|
||||
| index.html blocks list | | | | | | | █ | | | • | | |
|
||||
| write-only folders | █ | | | | | | | | | | █ | █ |
|
||||
| files stored as-is | █ | █ | █ | █ | | █ | █ | | | █ | █ | █ |
|
||||
| file versioning | | | | █ | █ | | | | | | | |
|
||||
| file encryption | | | | █ | █ | █ | | | | | | █ |
|
||||
| file indexing | █ | | █ | █ | █ | | | █ | █ | █ | | |
|
||||
| ┗ per-volume db | █ | | • | • | • | | | • | • | | | |
|
||||
| ┗ db stored in folder | █ | | | | | | | • | • | █ | | |
|
||||
| ┗ db stored out-of-tree | █ | | █ | █ | █ | | | • | • | █ | | |
|
||||
| ┗ existing file tree | █ | | █ | | | | | | | █ | | |
|
||||
| file action event hooks | █ | | | | | | | | | █ | | █ |
|
||||
| one-way folder sync | █ | | | █ | █ | █ | | | | | | |
|
||||
| full sync | | | | █ | █ | | | | | | | |
|
||||
| speed throttle | | █ | █ | | | █ | | | █ | | | █ |
|
||||
| anti-bruteforce | █ | █ | █ | █ | █ | | | | • | | | █ |
|
||||
| dyndns updater | | █ | | | | | | | | | | |
|
||||
| self-updater | | | █ | | | | | | | | | |
|
||||
| log rotation | █ | | █ | █ | █ | | | • | █ | | | █ |
|
||||
| upload tracking / log | █ | █ | • | █ | █ | | | █ | █ | | | ╱ |
|
||||
| curl-friendly ls | █ | | | | | | | | | | | |
|
||||
| curl-friendly upload | █ | | | | | █ | █ | • | | | | |
|
||||
|
||||
* `unmap subfolders` = "shadowing"; mounting a local folder in the middle of an existing filesystem tree in order to disable access below that path
|
||||
* `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
|
||||
@@ -253,7 +287,7 @@ symbol legend,
|
||||
* `curl-friendly ls` = returns a [sortable plaintext folder listing](https://user-images.githubusercontent.com/241032/215322619-ea5fd606-3654-40ad-94ee-2bc058647bb2.png) when curled
|
||||
* `curl-friendly upload` = uploading with curl is just `curl -T some.bin http://.../`
|
||||
* `a`/copyparty remarks:
|
||||
* one-way folder sync from local to server can be done efficiently with [up2k.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py), or with webdav and conventional rsync
|
||||
* 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
|
||||
* can hot-reload config files (with just a few exceptions)
|
||||
* can set per-folder permissions if that folder is made into a separate volume, so there is configuration overhead
|
||||
* [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
|
||||
@@ -261,54 +295,58 @@ symbol legend,
|
||||
* `k`/filegator remarks:
|
||||
* `per-* permissions` -- can limit a user to one folder and its subfolders
|
||||
* `unmap subfolders` -- can globally filter a list of paths
|
||||
* `l`/sftpgo:
|
||||
* `file action event hooks` also include on-download triggers
|
||||
* `upload tracking / log` in main logfile
|
||||
|
||||
|
||||
## client features
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k |
|
||||
| ---------------------- | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| single-page app | █ | | █ | █ | █ | | | █ | █ | █ | █ |
|
||||
| themes | █ | █ | | █ | | | | | █ | | |
|
||||
| directory tree nav | █ | ╱ | | | █ | | | | █ | | ╱ |
|
||||
| multi-column sorting | █ | | | | | | | | | | |
|
||||
| thumbnails | █ | | | ╱ | ╱ | | | █ | █ | ╱ | |
|
||||
| ┗ image thumbnails | █ | | | █ | █ | | | █ | █ | █ | |
|
||||
| ┗ video thumbnails | █ | | | █ | █ | | | | █ | | |
|
||||
| ┗ audio spectrograms | █ | | | | | | | | | | |
|
||||
| audio player | █ | | | █ | █ | | | | █ | ╱ | |
|
||||
| ┗ gapless playback | █ | | | | | | | | • | | |
|
||||
| ┗ audio equalizer | █ | | | | | | | | | | |
|
||||
| ┗ waveform seekbar | █ | | | | | | | | | | |
|
||||
| ┗ OS integration | █ | | | | | | | | | | |
|
||||
| ┗ transcode to lossy | █ | | | | | | | | | | |
|
||||
| video player | █ | | | █ | █ | | | | █ | █ | |
|
||||
| ┗ video transcoding | | | | | | | | | █ | | |
|
||||
| audio BPM detector | █ | | | | | | | | | | |
|
||||
| audio key detector | █ | | | | | | | | | | |
|
||||
| search by path / name | █ | █ | █ | █ | █ | | █ | | █ | █ | ╱ |
|
||||
| search by date / size | █ | | | | █ | | | █ | █ | | |
|
||||
| search by bpm / key | █ | | | | | | | | | | |
|
||||
| search by custom tags | | | | | | | | █ | █ | | |
|
||||
| search in file contents | | | | █ | █ | | | | █ | | |
|
||||
| search by custom parser | █ | | | | | | | | | | |
|
||||
| find local file | █ | | | | | | | | | | |
|
||||
| undo recent uploads | █ | | | | | | | | | | |
|
||||
| create directories | █ | | | █ | █ | ╱ | █ | █ | █ | █ | █ |
|
||||
| image viewer | █ | | | █ | █ | | | | █ | █ | █ |
|
||||
| markdown viewer | █ | | | | █ | | | | █ | ╱ | ╱ |
|
||||
| markdown editor | █ | | | | █ | | | | █ | ╱ | ╱ |
|
||||
| readme.md in listing | █ | | | █ | | | | | | | |
|
||||
| rename files | █ | █ | █ | █ | █ | ╱ | █ | | █ | █ | █ |
|
||||
| batch rename | █ | | | | | | | | █ | | |
|
||||
| cut / paste files | █ | █ | | █ | █ | | | | █ | | |
|
||||
| move files | █ | █ | | █ | █ | | █ | | █ | █ | █ |
|
||||
| delete files | █ | █ | | █ | █ | ╱ | █ | █ | █ | █ | █ |
|
||||
| copy files | | | | | █ | | | | █ | █ | █ |
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l |
|
||||
| ---------------------- | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| single-page app | █ | | █ | █ | █ | | | █ | █ | █ | █ | |
|
||||
| themes | █ | █ | | █ | | | | | █ | | | |
|
||||
| directory tree nav | █ | ╱ | | | █ | | | | █ | | ╱ | |
|
||||
| multi-column sorting | █ | | | | | | | | | | | |
|
||||
| thumbnails | █ | | | ╱ | ╱ | | | █ | █ | ╱ | | |
|
||||
| ┗ image thumbnails | █ | | | █ | █ | | | █ | █ | █ | | |
|
||||
| ┗ video thumbnails | █ | | | █ | █ | | | | █ | | | |
|
||||
| ┗ audio spectrograms | █ | | | | | | | | | | | |
|
||||
| audio player | █ | | | █ | █ | | | | █ | ╱ | | |
|
||||
| ┗ gapless playback | █ | | | | | | | | • | | | |
|
||||
| ┗ audio equalizer | █ | | | | | | | | | | | |
|
||||
| ┗ waveform seekbar | █ | | | | | | | | | | | |
|
||||
| ┗ OS integration | █ | | | | | | | | | | | |
|
||||
| ┗ transcode to lossy | █ | | | | | | | | | | | |
|
||||
| video player | █ | | | █ | █ | | | | █ | █ | | |
|
||||
| ┗ video transcoding | | | | | | | | | █ | | | |
|
||||
| audio BPM detector | █ | | | | | | | | | | | |
|
||||
| audio key detector | █ | | | | | | | | | | | |
|
||||
| search by path / name | █ | █ | █ | █ | █ | | █ | | █ | █ | ╱ | |
|
||||
| search by date / size | █ | | | | █ | | | █ | █ | | | |
|
||||
| search by bpm / key | █ | | | | | | | | | | | |
|
||||
| search by custom tags | | | | | | | | █ | █ | | | |
|
||||
| search in file contents | | | | █ | █ | | | | █ | | | |
|
||||
| search by custom parser | █ | | | | | | | | | | | |
|
||||
| find local file | █ | | | | | | | | | | | |
|
||||
| undo recent uploads | █ | | | | | | | | | | | |
|
||||
| create directories | █ | | | █ | █ | ╱ | █ | █ | █ | █ | █ | █ |
|
||||
| image viewer | █ | | | █ | █ | | | | █ | █ | █ | |
|
||||
| markdown viewer | █ | | | | █ | | | | █ | ╱ | ╱ | |
|
||||
| markdown editor | █ | | | | █ | | | | █ | ╱ | ╱ | |
|
||||
| readme.md in listing | █ | | | █ | | | | | | | | |
|
||||
| rename files | █ | █ | █ | █ | █ | ╱ | █ | | █ | █ | █ | █ |
|
||||
| batch rename | █ | | | | | | | | █ | | | |
|
||||
| cut / paste files | █ | █ | | █ | █ | | | | █ | | | |
|
||||
| move files | █ | █ | | █ | █ | | █ | | █ | █ | █ | |
|
||||
| delete files | █ | █ | | █ | █ | ╱ | █ | █ | █ | █ | █ | █ |
|
||||
| copy files | | | | | █ | | | | █ | █ | █ | |
|
||||
|
||||
* `single-page app` = multitasking; possible to continue navigating while uploading
|
||||
* `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
|
||||
* `search by custom tags` = ability to tag files through the UI and search by those
|
||||
* `find local file` = drop a file into the browser to see if it exists on the server
|
||||
* `undo recent uploads` = accounts without delete permissions have a time window where they can undo their own uploads
|
||||
* `a`/copyparty has teeny-tiny skips playing gapless albums depending on audio codec (opus best)
|
||||
* `b`/hfs2 has a very basic directory tree view, not showing sibling folders
|
||||
* `f`/rclone can do some file management (mkdir, rename, delete) when hosting througn webdav
|
||||
@@ -318,20 +356,21 @@ symbol legend,
|
||||
|
||||
## integration
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| OS alert on upload | █ | | | | | | | | | ╱ | |
|
||||
| discord | █ | | | | | | | | | ╱ | |
|
||||
| ┗ announce uploads | █ | | | | | | | | | | |
|
||||
| ┗ custom embeds | | | | | | | | | | | |
|
||||
| sharex | █ | | | █ | | █ | ╱ | █ | | | |
|
||||
| flameshot | | | | | | █ | | | | | |
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l |
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| OS alert on upload | █ | | | | | | | | | ╱ | | ╱ |
|
||||
| discord | █ | | | | | | | | | ╱ | | ╱ |
|
||||
| ┗ announce uploads | █ | | | | | | | | | | | ╱ |
|
||||
| ┗ custom embeds | | | | | | | | | | | | ╱ |
|
||||
| sharex | █ | | | █ | | █ | ╱ | █ | | | | |
|
||||
| flameshot | | | | | | █ | | | | | | |
|
||||
|
||||
* sharex `╱` = yes, but does not provide example sharex config
|
||||
* `a`/copyparty remarks:
|
||||
* `OS alert on upload` available as [a plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/notify.py)
|
||||
* `discord » announce uploads` available as [a plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/discord-announce.py)
|
||||
* `j`/filebrowser can probably pull those off with command runners similar to copyparty
|
||||
* `l`/sftpgo has nothing built-in but is very extensible
|
||||
|
||||
|
||||
## another matrix
|
||||
@@ -349,6 +388,7 @@ symbol legend,
|
||||
| kodbox | php | ░ gpl3 | 92 MB |
|
||||
| filebrowser | go | █ apl2 | 20 MB |
|
||||
| filegator | php | █ mit | • |
|
||||
| sftpgo | go | ‼ agpl | 44 MB |
|
||||
| updog | python | █ mit | 17 MB |
|
||||
| goshs | go | █ mit | 11 MB |
|
||||
| gimme-that | python | █ mit | 4.8 MB |
|
||||
@@ -362,7 +402,9 @@ symbol legend,
|
||||
# reviews
|
||||
|
||||
* ✅ are advantages over copyparty
|
||||
* ⚠️ are disadvantages
|
||||
* 💾 are what copyparty offers as an alternative
|
||||
* 🔵 are similarities
|
||||
* ⚠️ are disadvantages (something copyparty does "better")
|
||||
|
||||
## [copyparty](https://github.com/9001/copyparty)
|
||||
* resumable uploads which are verified server-side
|
||||
@@ -370,7 +412,7 @@ symbol legend,
|
||||
* both of the above are surprisingly uncommon features
|
||||
* very cross-platform (python, no dependencies)
|
||||
|
||||
## [hfs2](https://github.com/rejetto/hfs2)
|
||||
## [hfs2](https://rejetto.com/hfs/)
|
||||
* the OG, the legend
|
||||
* ⚠️ uploads not resumable / accelerated / integrity-checked
|
||||
* ⚠️ on cloudflare: max upload size 100 MiB
|
||||
@@ -379,7 +421,7 @@ symbol legend,
|
||||
* vfs with gui config, per-volume permissions
|
||||
* starting to show its age, hence the rewrite:
|
||||
|
||||
## [hfs3](https://www.rejetto.com/hfs/)
|
||||
## [hfs3](https://github.com/rejetto/hfs)
|
||||
* nodejs; cross-platform
|
||||
* vfs with gui config, per-volume permissions
|
||||
* still early development, let's revisit later
|
||||
@@ -394,10 +436,11 @@ symbol legend,
|
||||
* ⚠️ http/webdav only; no ftp, zeroconf
|
||||
* ⚠️ less awesome music player
|
||||
* ⚠️ doesn't run on android or ipads
|
||||
* ⚠️ AGPL licensed
|
||||
* ✅ great ui/ux
|
||||
* ✅ config gui
|
||||
* ✅ apps (android / iphone)
|
||||
* copyparty: android upload-only app
|
||||
* 💾 android upload-only app + iPhone upload shortcut
|
||||
* ✅ more granular permissions (per-file)
|
||||
* ✅ search: fulltext indexing of file contents
|
||||
* ✅ webauthn passwordless authentication
|
||||
@@ -412,10 +455,11 @@ symbol legend,
|
||||
* ⚠️ http/webdav only; no ftp, zeroconf
|
||||
* ⚠️ less awesome music player
|
||||
* ⚠️ doesn't run on android or ipads
|
||||
* ⚠️ AGPL licensed
|
||||
* ✅ great ui/ux
|
||||
* ✅ config gui
|
||||
* ✅ apps (android / iphone)
|
||||
* copyparty: android upload-only app
|
||||
* 💾 android upload-only app + iPhone upload shortcut
|
||||
* ✅ more granular permissions (per-file)
|
||||
* ✅ search: fulltext indexing of file contents
|
||||
|
||||
@@ -433,12 +477,12 @@ symbol legend,
|
||||
* ⚠️ on cloudflare: max upload size 100 MiB
|
||||
* ⚠️ doesn't support crazy filenames
|
||||
* ✅ per-url access control (copyparty is per-volume)
|
||||
* basic but really snappy ui
|
||||
* upload, rename, delete, ... see feature matrix
|
||||
* 🔵 basic but really snappy ui
|
||||
* 🔵 upload, rename, delete, ... see feature matrix
|
||||
|
||||
## [chibisafe](https://github.com/chibisafe/chibisafe)
|
||||
* nodejs; recommends docker
|
||||
* *it has upload segmenting!*
|
||||
* 🔵 *it has upload segmenting!*
|
||||
* ⚠️ but uploads are still not resumable / accelerated / integrity-checked
|
||||
* ⚠️ not portable
|
||||
* ⚠️ isolated on-disk file hierarchy, incompatible with other software
|
||||
@@ -449,13 +493,13 @@ symbol legend,
|
||||
* ✅ searchable image tags; delete by tag
|
||||
* ✅ browser extension to upload files to the server
|
||||
* ✅ reject uploads by file extension
|
||||
* copyparty: 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
|
||||
* 💾 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
|
||||
* ✅ token auth (api keys)
|
||||
|
||||
## [kodbox](https://github.com/kalcaddle/kodbox)
|
||||
* this thing is insane
|
||||
* php; [docker](https://hub.docker.com/r/kodcloud/kodbox)
|
||||
* *upload segmenting, acceleration, and integrity checking!*
|
||||
* 🔵 *upload segmenting, acceleration, and integrity checking!*
|
||||
* ⚠️ but uploads are not resumable(?)
|
||||
* ⚠️ not portable
|
||||
* ⚠️ isolated on-disk file hierarchy, incompatible with other software
|
||||
@@ -482,17 +526,41 @@ symbol legend,
|
||||
* ⚠️ but no directory tree for navigation
|
||||
* ✅ user signup
|
||||
* ✅ command runner / remote shell
|
||||
* supposed to have write-only folders but couldn't get it to work
|
||||
* 🔵 supposed to have write-only folders but couldn't get it to work
|
||||
|
||||
## [filegator](https://github.com/filegator/filegator)
|
||||
* go; cross-platform (windows, linux, mac)
|
||||
* 🔵 *it has upload segmenting and acceleration*
|
||||
* ⚠️ but uploads are still not integrity-checked
|
||||
* ⚠️ http only; no webdav / ftp / zeroconf
|
||||
* ⚠️ does not support symlinks
|
||||
* ⚠️ expensive download-as-zip feature
|
||||
* ⚠️ doesn't support crazy filenames
|
||||
* ⚠️ limited file search
|
||||
* *it has upload segmenting and acceleration*
|
||||
* ⚠️ but uploads are still not integrity-checked
|
||||
|
||||
## [sftpgo](https://github.com/drakkan/sftpgo)
|
||||
* go; cross-platform (windows, linux, mac)
|
||||
* ⚠️ http uploads not resumable / accelerated / integrity-checked
|
||||
* ⚠️ on cloudflare: max upload size 100 MiB
|
||||
* 🔵 sftp uploads are resumable
|
||||
* ⚠️ web UI is very minimal + a bit slow
|
||||
* ⚠️ no thumbnails / image viewer / audio player
|
||||
* ⚠️ basic file manager (no cut/paste/move)
|
||||
* ⚠️ no filesystem indexing / search
|
||||
* ⚠️ doesn't run on phones, tablets
|
||||
* ⚠️ no zeroconf (mdns/ssdp)
|
||||
* ⚠️ AGPL licensed
|
||||
* 🔵 ftp, ftps, webdav
|
||||
* ✅ sftp server
|
||||
* ✅ settings gui
|
||||
* ✅ acme (automatic tls certs)
|
||||
* 💾 relies on caddy/certbot/acme.sh
|
||||
* ✅ at-rest encryption
|
||||
* 💾 relies on LUKS/BitLocker
|
||||
* ✅ can use S3/GCS as storage backend
|
||||
* 💾 relies on rclone-mount
|
||||
* ✅ on-download event hook (otherwise same as copyparty)
|
||||
* ✅ more extensive permissions control
|
||||
|
||||
## [updog](https://github.com/sc0tfree/updog)
|
||||
* python; cross-platform
|
||||
@@ -508,9 +576,9 @@ symbol legend,
|
||||
* ⚠️ uploads not resumable / accelerated / integrity-checked
|
||||
* ⚠️ on cloudflare: max upload size 100 MiB
|
||||
* ✅ cool clipboard widget
|
||||
* copyparty: the markdown editor is an ok substitute
|
||||
* read-only and upload-only modes (same as copyparty's write-only)
|
||||
* https, webdav
|
||||
* 💾 the markdown editor is an ok substitute
|
||||
* 🔵 read-only and upload-only modes (same as copyparty's write-only)
|
||||
* 🔵 https, webdav, but no ftp
|
||||
|
||||
## [gimme-that](https://github.com/nejdetckenobi/gimme-that)
|
||||
* python, but with c dependencies
|
||||
@@ -519,8 +587,8 @@ symbol legend,
|
||||
* ⚠️ on cloudflare: max upload size 100 MiB
|
||||
* ⚠️ weird folder structure for uploads
|
||||
* ✅ clamav antivirus check on upload! neat
|
||||
* optional max-filesize, os-notification on uploads
|
||||
* copyparty: os-notification available as [a plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/notify.py)
|
||||
* 🔵 optional max-filesize, os-notification on uploads
|
||||
* 💾 os-notification available as [a plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/notify.py)
|
||||
|
||||
## [ass](https://github.com/tycrek/ass)
|
||||
* nodejs; recommends docker
|
||||
@@ -531,30 +599,52 @@ symbol legend,
|
||||
* ⚠️ on cloudflare: max upload size 100 MiB
|
||||
* ✅ token auth
|
||||
* ✅ gps metadata stripping
|
||||
* copyparty: possible with [a plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/image-noexif.py)
|
||||
* 💾 possible with [a plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/image-noexif.py)
|
||||
* ✅ discord integration (custom embeds, upload webhook)
|
||||
* copyparty: [upload webhook plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/discord-announce.py)
|
||||
* 💾 [upload webhook plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/discord-announce.py)
|
||||
* ✅ reject uploads by mimetype
|
||||
* copyparty: 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
|
||||
* ✅ can use S3 as storage backend; copyparty relies on rclone-mount for that
|
||||
* 💾 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
|
||||
* ✅ can use S3 as storage backend
|
||||
* 💾 relies on rclone-mount
|
||||
* ✅ custom 404 pages
|
||||
|
||||
## [linx](https://github.com/ZizzyDizzyMC/linx-server/)
|
||||
* originally [andreimarcu/linx-server](https://github.com/andreimarcu/linx-server) but development has ended
|
||||
* ⚠️ uploads not resumable / accelerated / integrity-checked
|
||||
* ⚠️ on cloudflare: max upload size 100 MiB
|
||||
* some of its unique features have been added to copyparty as former linx users have migrated
|
||||
* 🔵 some of its unique features have been added to copyparty as former linx users have migrated
|
||||
* file expiration timers, filename randomization
|
||||
* ✅ password-protected files
|
||||
* copyparty: password-protected folders + filekeys to skip the folder password seem to cover most usecases
|
||||
* 💾 password-protected folders + filekeys to skip the folder password seem to cover most usecases
|
||||
* ✅ file deletion keys
|
||||
* ✅ download files as torrents
|
||||
* ✅ remote uploads (send a link to the server and it downloads it)
|
||||
* copyparty: available as [a plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/wget.py)
|
||||
* ✅ can use S3 as storage backend; copyparty relies on rclone-mount for that
|
||||
* 💾 available as [a plugin](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/wget.py)
|
||||
* ✅ can use S3 as storage backend
|
||||
* 💾 relies on rclone-mount
|
||||
|
||||
## [h5ai](https://larsjung.de/h5ai/)
|
||||
* ⚠️ read only; no upload/move/delete
|
||||
* ⚠️ search hits the filesystem directly; not indexed/cached
|
||||
* ✅ slick ui
|
||||
* ✅ in-browser qr generator to share URLs
|
||||
* 🔵 directory tree, image viewer, thumbnails, download-as-tar
|
||||
|
||||
## [autoindex](https://github.com/nielsAD/autoindex)
|
||||
* ⚠️ read only; no upload/move/delete
|
||||
* ✅ directory cache for faster browsing of cloud storage
|
||||
* 💾 local index/cache for recursive search (names/attrs/tags), but not for browsing
|
||||
|
||||
## [miniserve](https://github.com/svenstaro/miniserve)
|
||||
* rust; cross-platform (windows, linux, mac)
|
||||
* ⚠️ uploads not resumable / accelerated / integrity-checked
|
||||
* ⚠️ on cloudflare: max upload size 100 MiB
|
||||
* ⚠️ no thumbnails / image viewer / audio player / file manager
|
||||
* ⚠️ no filesystem indexing / search
|
||||
* 🔵 upload, tar/zip download, qr-code
|
||||
* ✅ faster at loading huge folders
|
||||
|
||||
|
||||
# briefly considered
|
||||
* [pydio](https://github.com/pydio/cells): python/agpl3, looks great, fantastic ux -- but needs mariadb, systemwide install
|
||||
* [gossa](https://github.com/pldubouilh/gossa): go/mit, minimalistic, basic file upload, text editor, mkdir and rename (no delete/move)
|
||||
* [h5ai](https://larsjung.de/h5ai/): php/mit, slick ui, image viewer, directory tree, no upload feature
|
||||
|
||||
42
flake.lock
generated
Normal file
42
flake.lock
generated
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1678901627,
|
||||
"narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1680334310,
|
||||
"narHash": "sha256-ISWz16oGxBhF7wqAxefMPwFag6SlsA9up8muV79V9ck=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "884e3b68be02ff9d61a042bc9bd9dd2a358f95da",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"ref": "nixos-22.11",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
28
flake.nix
Normal file
28
flake.nix
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "nixpkgs/nixos-22.11";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
{
|
||||
nixosModules.default = ./contrib/nixos/modules/copyparty.nix;
|
||||
overlays.default = self: super: {
|
||||
copyparty =
|
||||
self.python3.pkgs.callPackage ./contrib/package/nix/copyparty {
|
||||
ffmpeg = self.ffmpeg-full;
|
||||
};
|
||||
};
|
||||
} // flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ self.overlays.default ];
|
||||
};
|
||||
in {
|
||||
packages = {
|
||||
inherit (pkgs) copyparty;
|
||||
default = self.packages.${system}.copyparty;
|
||||
};
|
||||
});
|
||||
}
|
||||
145
pyproject.toml
Normal file
145
pyproject.toml
Normal file
@@ -0,0 +1,145 @@
|
||||
[project]
|
||||
name = "copyparty"
|
||||
description = """
|
||||
Portable file server with accelerated resumable uploads, \
|
||||
deduplication, WebDAV, FTP, zeroconf, media indexer, \
|
||||
video thumbnails, audio transcoding, and write-only folders"""
|
||||
readme = "README.md"
|
||||
authors = [{ name = "ed", email = "copyparty@ocv.me" }]
|
||||
license = { text = "MIT" }
|
||||
requires-python = ">=3.3"
|
||||
dependencies = ["Jinja2"]
|
||||
dynamic = ["version"]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: Jython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Environment :: Console",
|
||||
"Environment :: No Input/Output (Daemon)",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"Intended Audience :: System Administrators",
|
||||
"Topic :: Communications :: File Sharing",
|
||||
"Topic :: Internet :: File Transfer Protocol (FTP)",
|
||||
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Source Code" = "https://github.com/9001/copyparty"
|
||||
"Bug Tracker" = "https://github.com/9001/copyparty/issues"
|
||||
"Demo Server" = "https://a.ocv.me/pub/demo/"
|
||||
|
||||
[project.optional-dependencies]
|
||||
thumbnails = ["Pillow"]
|
||||
thumbnails2 = ["pyvips"]
|
||||
audiotags = ["mutagen"]
|
||||
ftpd = ["pyftpdlib"]
|
||||
ftps = ["pyftpdlib", "pyopenssl"]
|
||||
|
||||
[project.scripts]
|
||||
copyparty = "copyparty.__main__:main"
|
||||
"u2c" = "copyparty.web.a.u2c:main"
|
||||
"partyfuse" = "copyparty.web.a.partyfuse:main"
|
||||
|
||||
# =====================================================================
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
# requires = ["hatchling"]
|
||||
# build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.version]
|
||||
source = "code"
|
||||
path = "copyparty/__version__.py"
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = { attr = "copyparty.__version__.__version__" }
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["copyparty*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
copyparty = [
|
||||
"res/COPYING.txt",
|
||||
"res/insecure.pem",
|
||||
"web/*.gz",
|
||||
"web/*.js",
|
||||
"web/*.css",
|
||||
"web/*.html",
|
||||
"web/a/*.bat",
|
||||
"web/dd/*.png",
|
||||
"web/deps/*.gz",
|
||||
"web/deps/*.woff*",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
|
||||
[tool.black]
|
||||
required-version = '21.12b0'
|
||||
target-version = ['py27']
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
include_trailing_comma = true
|
||||
|
||||
[tool.bandit]
|
||||
skips = ["B104", "B110", "B112"]
|
||||
|
||||
# =====================================================================
|
||||
|
||||
[tool.pylint.MAIN]
|
||||
py-version = "3.11"
|
||||
jobs = 2
|
||||
|
||||
[tool.pylint."MESSAGES CONTROL"]
|
||||
disable = [
|
||||
"missing-module-docstring",
|
||||
"missing-class-docstring",
|
||||
"missing-function-docstring",
|
||||
"import-outside-toplevel",
|
||||
"wrong-import-position",
|
||||
"raise-missing-from",
|
||||
"bare-except",
|
||||
"broad-exception-raised",
|
||||
"broad-exception-caught",
|
||||
"invalid-name",
|
||||
"line-too-long",
|
||||
"too-many-lines",
|
||||
"consider-using-f-string",
|
||||
"pointless-string-statement",
|
||||
]
|
||||
|
||||
[tool.pylint.FORMAT]
|
||||
expected-line-ending-format = "LF"
|
||||
|
||||
# =====================================================================
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.11"
|
||||
files = ["copyparty"]
|
||||
show_error_codes = true
|
||||
show_column_numbers = true
|
||||
pretty = true
|
||||
strict = true
|
||||
local_partial_types = true
|
||||
strict_equality = true
|
||||
warn_unreachable = true
|
||||
ignore_missing_imports = true
|
||||
follow_imports = "silent"
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
no_implicit_reexport = false
|
||||
@@ -34,7 +34,7 @@ set -e
|
||||
# 4823 copyparty-extras/copyparty-repack.sh
|
||||
# `- source files from github
|
||||
#
|
||||
# 23663 copyparty-extras/up2k.py
|
||||
# 23663 copyparty-extras/u2c.py
|
||||
# `- standalone utility to upload or search for files
|
||||
#
|
||||
# 32280 copyparty-extras/partyfuse.py
|
||||
@@ -147,7 +147,7 @@ repack sfx-lite "re no-dd no-cm no-hl gz"
|
||||
# copy lite-sfx.py to ./copyparty,
|
||||
# delete extracted source code
|
||||
( cd copyparty-extras/
|
||||
mv copyparty-*/bin/up2k.py .
|
||||
mv copyparty-*/bin/u2c.py .
|
||||
mv copyparty-*/bin/partyfuse.py .
|
||||
cp -pv sfx-lite/copyparty-sfx.py ../copyparty
|
||||
rm -rf copyparty-{0..9}*.*.*{0..9}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
# TODO easymde embeds codemirror on 3.17 due to new npm probably
|
||||
FROM alpine:3.16
|
||||
FROM alpine:3.18
|
||||
WORKDIR /z
|
||||
ENV ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \
|
||||
ver_hashwasm=4.9.0 \
|
||||
ver_marked=4.2.5 \
|
||||
ver_marked=4.3.0 \
|
||||
ver_mde=2.18.0 \
|
||||
ver_codemirror=5.65.11 \
|
||||
ver_codemirror=5.65.12 \
|
||||
ver_fontawesome=5.13.0 \
|
||||
ver_prism=1.29.0 \
|
||||
ver_zopfli=1.0.3
|
||||
|
||||
# versioncheck:
|
||||
# https://github.com/markedjs/marked/releases
|
||||
# https://github.com/Ionaru/easy-markdown-editor/tags
|
||||
# https://github.com/codemirror/codemirror5/releases
|
||||
# https://github.com/Daninet/hash-wasm/releases
|
||||
# https://github.com/openpgpjs/asmcrypto.js
|
||||
# https://github.com/google/zopfli/tags
|
||||
|
||||
|
||||
# download;
|
||||
# the scp url is regular latin from https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap
|
||||
@@ -22,6 +30,7 @@ RUN mkdir -p /z/dist/no-pk \
|
||||
&& wget https://github.com/FortAwesome/Font-Awesome/releases/download/$ver_fontawesome/fontawesome-free-$ver_fontawesome-web.zip -O fontawesome.zip \
|
||||
&& wget https://github.com/google/zopfli/archive/zopfli-$ver_zopfli.tar.gz -O zopfli.tgz \
|
||||
&& wget https://github.com/Daninet/hash-wasm/releases/download/v$ver_hashwasm/hash-wasm@$ver_hashwasm.zip -O hash-wasm.zip \
|
||||
&& wget https://github.com/PrismJS/prism/archive/refs/tags/v$ver_prism.tar.gz -O prism.tgz \
|
||||
&& (mkdir hash-wasm \
|
||||
&& cd hash-wasm \
|
||||
&& unzip ../hash-wasm.zip) \
|
||||
@@ -39,14 +48,11 @@ RUN mkdir -p /z/dist/no-pk \
|
||||
&& cd easy-markdown-editor* \
|
||||
&& npm install \
|
||||
&& npm i gulp-cli -g ) \
|
||||
&& tar -xf prism.tgz \
|
||||
&& unzip fontawesome.zip \
|
||||
&& tar -xf zopfli.tgz
|
||||
|
||||
|
||||
# todo
|
||||
# https://prismjs.com/download.html#themes=prism-funky&languages=markup+css+clike+javascript+autohotkey+bash+basic+batch+c+csharp+cpp+cmake+diff+docker+go+ini+java+json+kotlin+latex+less+lisp+lua+makefile+objectivec+perl+powershell+python+r+jsx+ruby+rust+sass+scss+sql+swift+systemd+toml+typescript+vbnet+verilog+vhdl+yaml&plugins=line-highlight+line-numbers+autolinker
|
||||
|
||||
|
||||
# build fonttools (which needs zopfli)
|
||||
RUN tar -xf zopfli.tgz \
|
||||
&& cd zopfli* \
|
||||
@@ -121,6 +127,12 @@ COPY shiftbase.py /z
|
||||
RUN /bin/ash /z/mini-fa.sh
|
||||
|
||||
|
||||
# build prismjs
|
||||
COPY genprism.py /z
|
||||
COPY genprism.sh /z
|
||||
RUN ./genprism.sh $ver_prism
|
||||
|
||||
|
||||
# compress
|
||||
COPY zopfli.makefile /z/dist/Makefile
|
||||
RUN cd /z/dist \
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
self := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
vend := $(self)/../../copyparty/web/deps
|
||||
|
||||
# prefers podman-docker (optionally rootless) over actual docker/moby
|
||||
|
||||
all:
|
||||
-service docker start
|
||||
-systemctl start docker
|
||||
|
||||
docker build -t build-copyparty-deps .
|
||||
|
||||
rm -rf $(vend)
|
||||
@@ -14,6 +13,7 @@ all:
|
||||
docker run --rm -i build-copyparty-deps:latest | \
|
||||
tar -xvC $(vend) --strip-components=1
|
||||
|
||||
touch $(vend)/__init__.py
|
||||
chown -R `stat $(self) -c %u:%g` $(vend)
|
||||
|
||||
purge:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user