Compare commits
426 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0da0122b9 | ||
|
|
879e83e24f | ||
|
|
64ad585318 | ||
|
|
f262aee800 | ||
|
|
d4da386172 | ||
|
|
5d92f4df49 | ||
|
|
6f8a588c4d | ||
|
|
7c8e368721 | ||
|
|
f7a43a8e46 | ||
|
|
02879713a2 | ||
|
|
acbb8267e1 | ||
|
|
8796c09f56 | ||
|
|
d636316a19 | ||
|
|
ed524d84bb | ||
|
|
f0cdd9f25d | ||
|
|
4e797a7156 | ||
|
|
136c0fdc2b | ||
|
|
cab999978e | ||
|
|
fabeebd96b | ||
|
|
b1cf588452 | ||
|
|
c354a38b4c | ||
|
|
a17c267d87 | ||
|
|
c1180d6f9c | ||
|
|
d3db6d296f | ||
|
|
eefa0518db | ||
|
|
945170e271 | ||
|
|
6c2c6090dc | ||
|
|
b2e233403d | ||
|
|
e397ec2e48 | ||
|
|
fade751a3e | ||
|
|
0f386c4b08 | ||
|
|
14bccbe45f | ||
|
|
55eb692134 | ||
|
|
b32d65207b | ||
|
|
64cac003d8 | ||
|
|
6dbfcddcda | ||
|
|
b4e0a34193 | ||
|
|
01c82b54a7 | ||
|
|
4ef3106009 | ||
|
|
aa3a971961 | ||
|
|
b9d0c8536b | ||
|
|
3313503ea5 | ||
|
|
d999d3a921 | ||
|
|
e7d00bae39 | ||
|
|
650e41c717 | ||
|
|
140f6e0389 | ||
|
|
5e111ba5ee | ||
|
|
95a599961e | ||
|
|
a55e0d6eb8 | ||
|
|
2fd2c6b948 | ||
|
|
7a936ea01e | ||
|
|
226c7c3045 | ||
|
|
a4239a466b | ||
|
|
d0eb014c38 | ||
|
|
e01ba8552a | ||
|
|
024303592a | ||
|
|
86419b8f47 | ||
|
|
f1358dbaba | ||
|
|
e8a653ca0c | ||
|
|
9bc09ce949 | ||
|
|
dc8e621d7c | ||
|
|
dee0950f74 | ||
|
|
143f72fe36 | ||
|
|
a7889fb6a2 | ||
|
|
987caec15d | ||
|
|
ab40ff5051 | ||
|
|
bed133d3dd | ||
|
|
829c8fca96 | ||
|
|
5b26ab0096 | ||
|
|
39554b4bc3 | ||
|
|
97d9c149f1 | ||
|
|
59688bc8d7 | ||
|
|
a18f63895f | ||
|
|
27433d6214 | ||
|
|
374c535cfa | ||
|
|
ac7815a0ae | ||
|
|
0c50ea1757 | ||
|
|
c057c5e8e8 | ||
|
|
46d667716e | ||
|
|
cba2e10d29 | ||
|
|
b1693f95cb | ||
|
|
3f00073256 | ||
|
|
d15000062d | ||
|
|
6cb3b35a54 | ||
|
|
b4031e8d43 | ||
|
|
a3ca0638cb | ||
|
|
a360ac29da | ||
|
|
9672b8c9b3 | ||
|
|
e70ecd98ef | ||
|
|
5f7ce78d7f | ||
|
|
2077dca66f | ||
|
|
91f010290c | ||
|
|
395e3386b7 | ||
|
|
a1dce0f24e | ||
|
|
c7770904e6 | ||
|
|
1690889ed8 | ||
|
|
842817d9e3 | ||
|
|
5fc04152bd | ||
|
|
1be85bdb26 | ||
|
|
2eafaa88a2 | ||
|
|
900cc463c3 | ||
|
|
97b999c463 | ||
|
|
a7cef91b8b | ||
|
|
a4a112c0ee | ||
|
|
e6bcee28d6 | ||
|
|
626b5770a5 | ||
|
|
c2f92cacc1 | ||
|
|
4f8a1f5f6a | ||
|
|
4a98b73915 | ||
|
|
00812cb1da | ||
|
|
16766e702e | ||
|
|
5e932a9504 | ||
|
|
ccab44daf2 | ||
|
|
8c52b88767 | ||
|
|
c9fd26255b | ||
|
|
0b9b8dbe72 | ||
|
|
b7723ac245 | ||
|
|
35b75c3db1 | ||
|
|
f902779050 | ||
|
|
fdddd36a5d | ||
|
|
c4ba123779 | ||
|
|
72e355eb2c | ||
|
|
43d409a5d9 | ||
|
|
b1fffc2246 | ||
|
|
edd3e53ab3 | ||
|
|
aa0b119031 | ||
|
|
eddce00765 | ||
|
|
6f4bde2111 | ||
|
|
f3035e8869 | ||
|
|
a9730499c0 | ||
|
|
b66843efe2 | ||
|
|
cc1aaea300 | ||
|
|
9ccc238799 | ||
|
|
8526ef9368 | ||
|
|
3c36727d07 | ||
|
|
ef33ce94cd | ||
|
|
d500baf5c5 | ||
|
|
deef32335e | ||
|
|
fc4b51ad00 | ||
|
|
fa762754bf | ||
|
|
29bd8f57c4 | ||
|
|
abc37354ef | ||
|
|
ee3333362f | ||
|
|
7c0c6b94a3 | ||
|
|
bac733113c | ||
|
|
32ab65d7cb | ||
|
|
c6744dc483 | ||
|
|
b9997d677d | ||
|
|
10defe6aef | ||
|
|
736aa125a8 | ||
|
|
eb48373b8b | ||
|
|
d4a7b7d84d | ||
|
|
2923a38b87 | ||
|
|
dabdaaee33 | ||
|
|
65e4d67c3e | ||
|
|
4b720f4150 | ||
|
|
2e85a25614 | ||
|
|
713fffcb8e | ||
|
|
8020b11ea0 | ||
|
|
2523d76756 | ||
|
|
7ede509973 | ||
|
|
7c1d97af3b | ||
|
|
95566e8388 | ||
|
|
76afb62b7b | ||
|
|
7dec922c70 | ||
|
|
c07e0110f8 | ||
|
|
2808734047 | ||
|
|
1f75314463 | ||
|
|
063fa3efde | ||
|
|
44693d79ec | ||
|
|
cea746377e | ||
|
|
59a98bd2b5 | ||
|
|
250aa28185 | ||
|
|
5280792cd7 | ||
|
|
2529aa151d | ||
|
|
fc658e5b9e | ||
|
|
a4bad62b60 | ||
|
|
e1d78d8b23 | ||
|
|
c7f826dbbe | ||
|
|
801da8079b | ||
|
|
7d797dba3f | ||
|
|
cda90c285e | ||
|
|
4b5a0787ab | ||
|
|
2048b7538e | ||
|
|
ac40dccc8f | ||
|
|
9ca8154651 | ||
|
|
db668ba491 | ||
|
|
edbafd94c2 | ||
|
|
2df76eb6e1 | ||
|
|
9b77c9ce7d | ||
|
|
dc2b67f155 | ||
|
|
9f32e9e11d | ||
|
|
7086d2a305 | ||
|
|
575615ca2d | ||
|
|
c0da4b09bf | ||
|
|
22880ccc9a | ||
|
|
e4001550c1 | ||
|
|
e9f65be86a | ||
|
|
3b9919a486 | ||
|
|
acc363133f | ||
|
|
8f2d502d4d | ||
|
|
2ae93ad715 | ||
|
|
bb590e364a | ||
|
|
e7fff77735 | ||
|
|
753e3cfbaf | ||
|
|
99e9cba1f7 | ||
|
|
fcc3336760 | ||
|
|
0dc3c23b42 | ||
|
|
6aa10ecedc | ||
|
|
93125bba4d | ||
|
|
fae5a36e6f | ||
|
|
fc9b729fc2 | ||
|
|
8620ae5bb7 | ||
|
|
01a851da28 | ||
|
|
309895d39d | ||
|
|
7ac0803ded | ||
|
|
cae5ccea62 | ||
|
|
3768cb4723 | ||
|
|
0815dce4c1 | ||
|
|
a62f744a18 | ||
|
|
163e3fce46 | ||
|
|
e76a50cb9d | ||
|
|
72fc76ef48 | ||
|
|
c47047c30d | ||
|
|
3b8f66c0d5 | ||
|
|
aa96a1acdc | ||
|
|
91cafc2511 | ||
|
|
23ca00bba8 | ||
|
|
a75a992951 | ||
|
|
4fbd6853f4 | ||
|
|
71c3ad63b3 | ||
|
|
e1324e37a5 | ||
|
|
a996a09bba | ||
|
|
18c763ac08 | ||
|
|
3d9fb753ba | ||
|
|
714fd1811a | ||
|
|
4364581705 | ||
|
|
ba02c9cc12 | ||
|
|
11eefaf968 | ||
|
|
5a968f9e47 | ||
|
|
6420c4bd03 | ||
|
|
0f9877201b | ||
|
|
9ba2dec9b2 | ||
|
|
ae9cfea939 | ||
|
|
cadaeeeace | ||
|
|
767696185b | ||
|
|
c1efd227b7 | ||
|
|
a50d0563c3 | ||
|
|
e5641ddd16 | ||
|
|
700111ffeb | ||
|
|
b8adeb824a | ||
|
|
30cc9defcb | ||
|
|
61875bd773 | ||
|
|
30905c6f5d | ||
|
|
9986136dfb | ||
|
|
1c0d978979 | ||
|
|
0a0364e9f8 | ||
|
|
3376fbde1a | ||
|
|
ac21fa7782 | ||
|
|
c1c8dc5e82 | ||
|
|
5a38311481 | ||
|
|
9f8edb7f32 | ||
|
|
c5a6ac8417 | ||
|
|
50e01d6904 | ||
|
|
9b46291a20 | ||
|
|
14497b2425 | ||
|
|
f7ceae5a5f | ||
|
|
c9492d16ba | ||
|
|
9fb9ada3aa | ||
|
|
db0abbfdda | ||
|
|
e7f0009e57 | ||
|
|
4444f0f6ff | ||
|
|
418842d2d3 | ||
|
|
cafe53c055 | ||
|
|
7673beef72 | ||
|
|
b28bfe64c0 | ||
|
|
135ece3fbd | ||
|
|
bd3640d256 | ||
|
|
fc0405c8f3 | ||
|
|
7df890d964 | ||
|
|
8341041857 | ||
|
|
1b7634932d | ||
|
|
48a3898aa6 | ||
|
|
5d13ebb4ac | ||
|
|
015b87ee99 | ||
|
|
0a48acf6be | ||
|
|
2b6a3afd38 | ||
|
|
18aa82fb2f | ||
|
|
f5407b2997 | ||
|
|
474d5a155b | ||
|
|
afcd98b794 | ||
|
|
4f80e44ff7 | ||
|
|
406e413594 | ||
|
|
033b50ae1b | ||
|
|
bee26e853b | ||
|
|
04a1f7040e | ||
|
|
f9d5bb3b29 | ||
|
|
ca0cd04085 | ||
|
|
999ee2e7bc | ||
|
|
1ff7f968e8 | ||
|
|
3966266207 | ||
|
|
d03e96a392 | ||
|
|
4c843c6df9 | ||
|
|
0896c5295c | ||
|
|
cc0c9839eb | ||
|
|
d0aa20e17c | ||
|
|
1a658dedb7 | ||
|
|
8d376b854c | ||
|
|
490c16b01d | ||
|
|
2437a4e864 | ||
|
|
007d948cb9 | ||
|
|
335fcc8535 | ||
|
|
9eaa9904e0 | ||
|
|
0778da6c4d | ||
|
|
a1bb10012d | ||
|
|
1441ccee4f | ||
|
|
491803d8b7 | ||
|
|
3dcc386b6f | ||
|
|
5aa54d1217 | ||
|
|
88b876027c | ||
|
|
fcc3aa98fd | ||
|
|
f2f5e266b4 | ||
|
|
e17bf8f325 | ||
|
|
d19cb32bf3 | ||
|
|
85a637af09 | ||
|
|
043e3c7dd6 | ||
|
|
8f59afb159 | ||
|
|
77f1e51444 | ||
|
|
22fc4bb938 | ||
|
|
50c7bba6ea | ||
|
|
551d99b71b | ||
|
|
b54b7213a7 | ||
|
|
a14943c8de | ||
|
|
a10cad54fc | ||
|
|
8568b7702a | ||
|
|
5d8cb34885 | ||
|
|
8d248333e8 | ||
|
|
99e2ef7f33 | ||
|
|
e767230383 | ||
|
|
90601314d6 | ||
|
|
9c5eac1274 | ||
|
|
50905439e4 | ||
|
|
a0c1239246 | ||
|
|
b8e851c332 | ||
|
|
baaf2eb24d | ||
|
|
e197895c10 | ||
|
|
cb75efa05d | ||
|
|
8b0cf2c982 | ||
|
|
fc7d9e1f9c | ||
|
|
10caafa34c | ||
|
|
22cc22225a | ||
|
|
22dff4b0e5 | ||
|
|
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 |
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -1,2 +1,2 @@
|
|||||||
Please include the following text somewhere in this PR description:
|
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/
|
This PR complies with the DCO; https://developercertificate.org/
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -37,6 +37,7 @@ up.*.txt
|
|||||||
.hist/
|
.hist/
|
||||||
scripts/docker/*.out
|
scripts/docker/*.out
|
||||||
scripts/docker/*.err
|
scripts/docker/*.err
|
||||||
|
/perf.*
|
||||||
|
|
||||||
# nix build output link
|
# nix build output link
|
||||||
result
|
result
|
||||||
|
|||||||
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@@ -9,14 +9,17 @@
|
|||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"justMyCode": false,
|
"justMyCode": false,
|
||||||
|
"env": {
|
||||||
|
"PYDEVD_DISABLE_FILE_VALIDATION": "1",
|
||||||
|
"PYTHONWARNINGS": "always", //error
|
||||||
|
},
|
||||||
"args": [
|
"args": [
|
||||||
//"-nw",
|
//"-nw",
|
||||||
"-ed",
|
"-ed",
|
||||||
"-emp",
|
"-emp",
|
||||||
"-e2dsa",
|
"-e2dsa",
|
||||||
"-e2ts",
|
"-e2ts",
|
||||||
"-mtp",
|
"-mtp=.bpm=f,bin/mtag/audio-bpm.py",
|
||||||
".bpm=f,bin/mtag/audio-bpm.py",
|
|
||||||
"-aed:wark",
|
"-aed:wark",
|
||||||
"-vsrv::r:rw,ed:c,dupe",
|
"-vsrv::r:rw,ed:c,dupe",
|
||||||
"-vdist:dist:r"
|
"-vdist:dist:r"
|
||||||
|
|||||||
12
.vscode/launch.py
vendored
12
.vscode/launch.py
vendored
@@ -30,10 +30,18 @@ except:
|
|||||||
|
|
||||||
argv = [os.path.expanduser(x) if x.startswith("~") else x for x in argv]
|
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:]
|
argv += sys.argv[1:]
|
||||||
|
|
||||||
if re.search(" -j ?[0-9]", " ".join(argv)):
|
if sfx:
|
||||||
argv = [sys.executable, "-m", "copyparty"] + argv
|
argv = [sys.executable, sfx] + argv
|
||||||
|
sp.check_call(argv)
|
||||||
|
elif re.search(" -j ?[0-9]", " ".join(argv)):
|
||||||
|
argv = [sys.executable, "-Wa", "-m", "copyparty"] + argv
|
||||||
sp.check_call(argv)
|
sp.check_call(argv)
|
||||||
else:
|
else:
|
||||||
sys.path.insert(0, os.getcwd())
|
sys.path.insert(0, os.getcwd())
|
||||||
|
|||||||
32
.vscode/settings.json
vendored
32
.vscode/settings.json
vendored
@@ -35,34 +35,18 @@
|
|||||||
"python.linting.flake8Enabled": true,
|
"python.linting.flake8Enabled": true,
|
||||||
"python.linting.banditEnabled": true,
|
"python.linting.banditEnabled": true,
|
||||||
"python.linting.mypyEnabled": true,
|
"python.linting.mypyEnabled": true,
|
||||||
"python.linting.mypyArgs": [
|
|
||||||
"--ignore-missing-imports",
|
|
||||||
"--follow-imports=silent",
|
|
||||||
"--show-column-numbers",
|
|
||||||
"--strict"
|
|
||||||
],
|
|
||||||
"python.linting.flake8Args": [
|
"python.linting.flake8Args": [
|
||||||
"--max-line-length=120",
|
"--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": [
|
"python.linting.banditArgs": [
|
||||||
"--ignore=B104"
|
"--ignore=B104,B110,B112"
|
||||||
],
|
|
||||||
"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"
|
|
||||||
],
|
],
|
||||||
// python3 -m isort --py=27 --profile=black copyparty/
|
// 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,
|
"editor.formatOnSave": true,
|
||||||
"[html]": {
|
"[html]": {
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
@@ -74,10 +58,6 @@
|
|||||||
"files.associations": {
|
"files.associations": {
|
||||||
"*.makefile": "makefile"
|
"*.makefile": "makefile"
|
||||||
},
|
},
|
||||||
"python.formatting.blackArgs": [
|
|
||||||
"-t",
|
|
||||||
"py27"
|
|
||||||
],
|
|
||||||
"python.linting.enabled": true,
|
"python.linting.enabled": true,
|
||||||
"python.pythonPath": "/usr/bin/python3"
|
"python.pythonPath": "/usr/bin/python3"
|
||||||
}
|
}
|
||||||
1
.vscode/tasks.json
vendored
1
.vscode/tasks.json
vendored
@@ -11,6 +11,7 @@
|
|||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "${config:python.pythonPath}",
|
"command": "${config:python.pythonPath}",
|
||||||
"args": [
|
"args": [
|
||||||
|
"-Wa", //-We
|
||||||
".vscode/launch.py"
|
".vscode/launch.py"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,43 @@
|
|||||||
* do something cool
|
* do something cool
|
||||||
|
|
||||||
really tho, send a PR or an issue or whatever, all appreciated, anything goes, just behave aight
|
really tho, send a PR or an issue or whatever, all appreciated, anything goes, just behave aight 👍👍
|
||||||
|
|
||||||
|
but to be more specific,
|
||||||
|
|
||||||
|
|
||||||
|
# contribution ideas
|
||||||
|
|
||||||
|
|
||||||
|
## documentation
|
||||||
|
|
||||||
|
I think we can agree that the documentation leaves a LOT to be desired. I've realized I'm not exactly qualified for this 😅 but maybe the [soon-to-come setup GUI](https://github.com/9001/copyparty/issues/57) will make this more manageable. The best documentation is the one that never had to be written, right? :> so I suppose we can give this a wait-and-see approach for a bit longer.
|
||||||
|
|
||||||
|
|
||||||
|
## crazy ideas & features
|
||||||
|
|
||||||
|
assuming they won't cause too much problems or side-effects :>
|
||||||
|
|
||||||
|
i think someone was working on a way to list directories over DNS for example...
|
||||||
|
|
||||||
|
if you wanna have a go at coding it up yourself then maybe mention the idea on discord before you get too far, otherwise just go nuts 👍
|
||||||
|
|
||||||
|
|
||||||
|
## others
|
||||||
|
|
||||||
|
aside from documentation and ideas, some other things that would be cool to have some help with is:
|
||||||
|
|
||||||
|
* **translations** -- the copyparty web-UI has translations for english and norwegian at the top of [browser.js](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/web/browser.js); if you'd like to add a translation for another language then that'd be welcome! and if that language has a grammar that doesn't fit into the way the strings are assembled, then we'll fix that as we go :>
|
||||||
|
|
||||||
|
* **UI ideas** -- at some point I was thinking of rewriting the UI in react/preact/something-not-vanilla-javascript, but I'll admit the comfiness of not having any build stage combined with raw performance has kinda convinced me otherwise :p but I'd be very open to ideas on how the UI could be improved, or be more intuitive.
|
||||||
|
|
||||||
|
* **docker improvements** -- I don't really know what I'm doing when it comes to containers, so I'm sure there's a *huge* room for improvement here, mainly regarding how you're supposed to use the container with kubernetes / docker-compose / any of the other popular ways to do things. At some point I swear I'll start learning about docker so I can pick up clach04's [docker-compose draft](https://github.com/9001/copyparty/issues/38) and learn how that stuff ticks, unless someone beats me to it!
|
||||||
|
|
||||||
|
* **packaging** for various linux distributions -- this could either be as simple as just plopping the sfx.py in the right place and calling that from systemd (the archlinux package [originally did this](https://github.com/9001/copyparty/pull/18)); maybe with a small config-file which would cause copyparty to load settings from `/etc/copyparty.d` (like the [archlinux package](https://github.com/9001/copyparty/tree/hovudstraum/contrib/package/arch) does with `copyparty.conf`), or it could be a proper installation of the copyparty python package into /usr/lib or similar (the archlinux package [eventually went for this approach](https://github.com/9001/copyparty/pull/26))
|
||||||
|
|
||||||
|
* [fpm](https://github.com/jordansissel/fpm) can probably help with the technical part of it, but someone needs to handle distro relations :-)
|
||||||
|
|
||||||
|
* **software integration** -- I'm sure there's a lot of usecases where copyparty could complement something else, or the other way around, so any ideas or any work in this regard would be dope. This doesn't necessarily have to be code inside copyparty itself;
|
||||||
|
|
||||||
|
* [hooks](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks) -- these are small programs which are called by copyparty when certain things happen (files are uploaded, someone hits a 404, etc.), and could be a fun way to add support for more usecases
|
||||||
|
|
||||||
|
* [parser plugins](https://github.com/9001/copyparty/tree/hovudstraum/bin/mtag) -- if you want to have copyparty analyze and index metadata for some oddball file-formats, then additional plugins would be neat :>
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2019 ed
|
Copyright (c) 2019 ed <oss@ocv.me>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
462
README.md
462
README.md
@@ -3,7 +3,7 @@
|
|||||||
turn almost any device into a file server with resumable uploads/downloads using [*any*](#browser-support) web browser
|
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
|
* server only needs Python (2 or 3), all dependencies optional
|
||||||
* 🔌 protocols: [http](#the-browser) // [ftp](#ftp-server) // [webdav](#webdav-server) // [smb/cifs](#smb-server)
|
* 🔌 protocols: [http](#the-browser) // [webdav](#webdav-server) // [ftp](#ftp-server) // [tftp](#tftp-server) // [smb/cifs](#smb-server)
|
||||||
* 📱 [android app](#android-app) // [iPhone shortcuts](#ios-shortcuts)
|
* 📱 [android app](#android-app) // [iPhone shortcuts](#ios-shortcuts)
|
||||||
|
|
||||||
👉 **[Get started](#quickstart)!** or visit the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running 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
|
||||||
@@ -20,13 +20,13 @@ turn almost any device into a file server with resumable uploads/downloads using
|
|||||||
* [testimonials](#testimonials) - small collection of user feedback
|
* [testimonials](#testimonials) - small collection of user feedback
|
||||||
* [motivations](#motivations) - project goals / philosophy
|
* [motivations](#motivations) - project goals / philosophy
|
||||||
* [notes](#notes) - general notes
|
* [notes](#notes) - general notes
|
||||||
* [bugs](#bugs)
|
* [bugs](#bugs) - roughly sorted by chance of encounter
|
||||||
* [general bugs](#general-bugs)
|
* [not my bugs](#not-my-bugs) - same order here too
|
||||||
* [not my bugs](#not-my-bugs)
|
|
||||||
* [breaking changes](#breaking-changes) - upgrade notes
|
* [breaking changes](#breaking-changes) - upgrade notes
|
||||||
* [FAQ](#FAQ) - "frequently" asked questions
|
* [FAQ](#FAQ) - "frequently" asked questions
|
||||||
* [accounts and volumes](#accounts-and-volumes) - per-folder, per-user permissions
|
* [accounts and volumes](#accounts-and-volumes) - per-folder, per-user permissions
|
||||||
* [shadowing](#shadowing) - hiding specific subfolders
|
* [shadowing](#shadowing) - hiding specific subfolders
|
||||||
|
* [dotfiles](#dotfiles) - unix-style hidden files/folders
|
||||||
* [the browser](#the-browser) - accessing a copyparty server using a web-browser
|
* [the browser](#the-browser) - accessing a copyparty server using a web-browser
|
||||||
* [tabs](#tabs) - the main tabs in the ui
|
* [tabs](#tabs) - the main tabs in the ui
|
||||||
* [hotkeys](#hotkeys) - the browser has the following hotkeys
|
* [hotkeys](#hotkeys) - the browser has the following hotkeys
|
||||||
@@ -40,7 +40,8 @@ turn almost any device into a file server with resumable uploads/downloads using
|
|||||||
* [file manager](#file-manager) - cut/paste, rename, and delete files/folders (if you have permission)
|
* [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
|
* [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
|
* [media player](#media-player) - plays almost every audio format there is
|
||||||
* [audio equalizer](#audio-equalizer) - bass boosted
|
* [audio equalizer](#audio-equalizer) - and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression)
|
||||||
|
* [fix unreliable playback on android](#fix-unreliable-playback-on-android) - due to phone / app settings
|
||||||
* [markdown viewer](#markdown-viewer) - and there are *two* editors
|
* [markdown viewer](#markdown-viewer) - and there are *two* editors
|
||||||
* [other tricks](#other-tricks)
|
* [other tricks](#other-tricks)
|
||||||
* [searching](#searching) - search by size, date, path/name, mp3-tags, ...
|
* [searching](#searching) - search by size, date, path/name, mp3-tags, ...
|
||||||
@@ -52,7 +53,9 @@ turn almost any device into a file server with resumable uploads/downloads using
|
|||||||
* [ftp server](#ftp-server) - an FTP server can be started using `--ftp 3921`
|
* [ftp server](#ftp-server) - an FTP server can be started using `--ftp 3921`
|
||||||
* [webdav server](#webdav-server) - with read-write support
|
* [webdav server](#webdav-server) - with read-write support
|
||||||
* [connecting to webdav from windows](#connecting-to-webdav-from-windows) - using the GUI
|
* [connecting to webdav from windows](#connecting-to-webdav-from-windows) - using the GUI
|
||||||
|
* [tftp server](#tftp-server) - a TFTP server (read/write) can be started using `--tftp 3969`
|
||||||
* [smb server](#smb-server) - unsafe, slow, not recommended for wan
|
* [smb server](#smb-server) - unsafe, slow, not recommended for wan
|
||||||
|
* [browser ux](#browser-ux) - tweaking the ui
|
||||||
* [file indexing](#file-indexing) - enables dedup and music search ++
|
* [file indexing](#file-indexing) - enables dedup and music search ++
|
||||||
* [exclude-patterns](#exclude-patterns) - to save some time
|
* [exclude-patterns](#exclude-patterns) - to save some time
|
||||||
* [filesystem guards](#filesystem-guards) - avoid traversing into other filesystems
|
* [filesystem guards](#filesystem-guards) - avoid traversing into other filesystems
|
||||||
@@ -65,10 +68,16 @@ turn almost any device into a file server with resumable uploads/downloads using
|
|||||||
* [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags
|
* [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags
|
||||||
* [event hooks](#event-hooks) - trigger a program on uploads, renames etc ([examples](./bin/hooks/))
|
* [event hooks](#event-hooks) - trigger a program on uploads, renames etc ([examples](./bin/hooks/))
|
||||||
* [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/))
|
* [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/))
|
||||||
|
* [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/))
|
||||||
|
* [identity providers](#identity-providers) - replace copyparty passwords with oauth and such
|
||||||
* [hiding from google](#hiding-from-google) - tell search engines you dont wanna be indexed
|
* [hiding from google](#hiding-from-google) - tell search engines you dont wanna be indexed
|
||||||
* [themes](#themes)
|
* [themes](#themes)
|
||||||
* [complete examples](#complete-examples)
|
* [complete examples](#complete-examples)
|
||||||
* [reverse-proxy](#reverse-proxy) - running copyparty next to other websites
|
* [reverse-proxy](#reverse-proxy) - running copyparty next to other websites
|
||||||
|
* [prometheus](#prometheus) - metrics/stats can be enabled
|
||||||
|
* [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)
|
||||||
|
* [fedora package](#fedora-package) - currently **NOT** available on [copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/)
|
||||||
* [nix package](#nix-package) - `nix profile install github:9001/copyparty`
|
* [nix package](#nix-package) - `nix profile install github:9001/copyparty`
|
||||||
* [nixos module](#nixos-module)
|
* [nixos module](#nixos-module)
|
||||||
* [browser support](#browser-support) - TLDR: yes
|
* [browser support](#browser-support) - TLDR: yes
|
||||||
@@ -79,9 +88,11 @@ turn almost any device into a file server with resumable uploads/downloads using
|
|||||||
* [iOS shortcuts](#iOS-shortcuts) - there is no iPhone app, but
|
* [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
|
* [performance](#performance) - defaults are usually fine - expect `8 GiB/s` download, `1 GiB/s` upload
|
||||||
* [client-side](#client-side) - when uploading files
|
* [client-side](#client-side) - when uploading files
|
||||||
* [security](#security) - some notes on hardening
|
* [security](#security) - there is a [discord server](https://discord.gg/25J8CdTT6G)
|
||||||
* [gotchas](#gotchas) - behavior that might be unexpected
|
* [gotchas](#gotchas) - behavior that might be unexpected
|
||||||
* [cors](#cors) - cross-site request config
|
* [cors](#cors) - cross-site request config
|
||||||
|
* [filekeys](#filekeys) - prevent filename bruteforcing
|
||||||
|
* [password hashing](#password-hashing) - you can hash passwords
|
||||||
* [https](#https) - both HTTP and HTTPS are accepted
|
* [https](#https) - both HTTP and HTTPS are accepted
|
||||||
* [recovering from crashes](#recovering-from-crashes)
|
* [recovering from crashes](#recovering-from-crashes)
|
||||||
* [client crashes](#client-crashes)
|
* [client crashes](#client-crashes)
|
||||||
@@ -101,9 +112,9 @@ turn almost any device into a file server with resumable uploads/downloads using
|
|||||||
|
|
||||||
just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** -- that's it! 🎉
|
just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** -- that's it! 🎉
|
||||||
|
|
||||||
* or install through pypi (python3 only): `python3 -m pip install --user -U copyparty`
|
* 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 if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead
|
||||||
* or [install through nix](#nix-package), or [on NixOS](#nixos-module)
|
* 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 are on android, [install copyparty in termux](#install-on-android)
|
||||||
* or if you prefer to [use docker](./scripts/docker/) 🐋 you can do that too
|
* or if you prefer to [use docker](./scripts/docker/) 🐋 you can do that too
|
||||||
* docker has all deps built-in, so skip this step:
|
* docker has all deps built-in, so skip this step:
|
||||||
@@ -111,8 +122,8 @@ just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/
|
|||||||
enable thumbnails (images/audio/video), media indexing, and audio transcoding by installing some recommended deps:
|
enable thumbnails (images/audio/video), media indexing, and audio transcoding by installing some recommended deps:
|
||||||
|
|
||||||
* **Alpine:** `apk add py3-pillow ffmpeg`
|
* **Alpine:** `apk add py3-pillow ffmpeg`
|
||||||
* **Debian:** `apt install python3-pil ffmpeg`
|
* **Debian:** `apt install --no-install-recommends python3-pil ffmpeg`
|
||||||
* **Fedora:** `dnf install python3-pillow ffmpeg`
|
* **Fedora:** rpmfusion + `dnf install python3-pillow ffmpeg`
|
||||||
* **FreeBSD:** `pkg install py39-sqlite3 py39-pillow ffmpeg`
|
* **FreeBSD:** `pkg install py39-sqlite3 py39-pillow ffmpeg`
|
||||||
* **MacOS:** `port install py-Pillow ffmpeg`
|
* **MacOS:** `port install py-Pillow ffmpeg`
|
||||||
* **MacOS** (alternative): `brew install pillow ffmpeg`
|
* **MacOS** (alternative): `brew install pillow ffmpeg`
|
||||||
@@ -123,7 +134,7 @@ enable thumbnails (images/audio/video), media indexing, and audio transcoding by
|
|||||||
|
|
||||||
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)
|
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 [complete windows example](./docs/examples/windows.md)
|
or see [some usage examples](#complete-examples) for inspiration, or the [complete windows example](./docs/examples/windows.md)
|
||||||
|
|
||||||
some recommended options:
|
some recommended options:
|
||||||
* `-e2dsa` enables general [file indexing](#file-indexing)
|
* `-e2dsa` enables general [file indexing](#file-indexing)
|
||||||
@@ -139,18 +150,19 @@ you may also want these, especially on servers:
|
|||||||
|
|
||||||
* [contrib/systemd/copyparty.service](contrib/systemd/copyparty.service) to run copyparty as a systemd service (see guide inside)
|
* [contrib/systemd/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/systemd/prisonparty.service](contrib/systemd/prisonparty.service) to run it in a chroot (for extra security)
|
||||||
|
* [contrib/openrc/copyparty](contrib/openrc/copyparty) to run copyparty on Alpine / Gentoo
|
||||||
* [contrib/rc/copyparty](contrib/rc/copyparty) to run copyparty on FreeBSD
|
* [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
|
* [nixos module](#nixos-module) to run copyparty on NixOS hosts
|
||||||
|
* [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to [reverse-proxy](#reverse-proxy) behind nginx (for better https)
|
||||||
|
|
||||||
and remember to open the ports you want; here's a complete example including every feature copyparty has to offer:
|
and remember to open the ports you want; here's a complete example including every feature copyparty has to offer:
|
||||||
```
|
```
|
||||||
firewall-cmd --permanent --add-port={80,443,3921,3923,3945,3990}/tcp # --zone=libvirt
|
firewall-cmd --permanent --add-port={80,443,3921,3923,3945,3990}/tcp # --zone=libvirt
|
||||||
firewall-cmd --permanent --add-port=12000-12099/tcp --permanent # --zone=libvirt
|
firewall-cmd --permanent --add-port=12000-12099/tcp # --zone=libvirt
|
||||||
firewall-cmd --permanent --add-port={1900,5353}/udp # --zone=libvirt
|
firewall-cmd --permanent --add-port={69,1900,3969,5353}/udp # --zone=libvirt
|
||||||
firewall-cmd --reload
|
firewall-cmd --reload
|
||||||
```
|
```
|
||||||
(1900:ssdp, 3921:ftp, 3923:http/https, 3945:smb, 3990:ftps, 5353:mdns, 12000:passive-ftp)
|
(69:tftp, 1900:ssdp, 3921:ftp, 3923:http/https, 3945:smb, 3969:tftp, 3990:ftps, 5353:mdns, 12000:passive-ftp)
|
||||||
|
|
||||||
|
|
||||||
## features
|
## features
|
||||||
@@ -161,6 +173,7 @@ firewall-cmd --reload
|
|||||||
* ☑ volumes (mountpoints)
|
* ☑ volumes (mountpoints)
|
||||||
* ☑ [accounts](#accounts-and-volumes)
|
* ☑ [accounts](#accounts-and-volumes)
|
||||||
* ☑ [ftp server](#ftp-server)
|
* ☑ [ftp server](#ftp-server)
|
||||||
|
* ☑ [tftp server](#tftp-server)
|
||||||
* ☑ [webdav server](#webdav-server)
|
* ☑ [webdav server](#webdav-server)
|
||||||
* ☑ [smb/cifs server](#smb-server)
|
* ☑ [smb/cifs server](#smb-server)
|
||||||
* ☑ [qr-code](#qr-code) for quick access
|
* ☑ [qr-code](#qr-code) for quick access
|
||||||
@@ -255,29 +268,42 @@ server notes:
|
|||||||
|
|
||||||
# bugs
|
# bugs
|
||||||
|
|
||||||
* Windows: python 2.7 cannot index non-ascii filenames with `-e2d`
|
roughly sorted by chance of encounter
|
||||||
* Windows: python 2.7 cannot handle filenames with mojibake
|
|
||||||
* `--th-ff-jpg` may fix video thumbnails on some FFmpeg versions (macos, some linux)
|
|
||||||
* `--th-ff-swr` may fix audio thumbnails on some FFmpeg versions
|
|
||||||
|
|
||||||
## general bugs
|
* general:
|
||||||
|
* `--th-ff-jpg` may fix video thumbnails on some FFmpeg versions (macos, some linux)
|
||||||
|
* `--th-ff-swr` may fix audio thumbnails on some FFmpeg versions
|
||||||
|
* if the `up2k.db` (filesystem index) is on a samba-share or network disk, you'll get unpredictable behavior if the share is disconnected for a bit
|
||||||
|
* use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db on a local disk instead
|
||||||
|
* all volumes must exist / be available on startup; up2k (mtp especially) gets funky otherwise
|
||||||
|
* probably more, pls let me know
|
||||||
|
|
||||||
* Windows: if the `up2k.db` (filesystem index) is on a samba-share or network disk, you'll get unpredictable behavior if the share is disconnected for a bit
|
* python 3.4 and older (including 2.7):
|
||||||
* use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db on a local disk instead
|
* many rare and exciting edge-cases because [python didn't handle EINTR yet](https://peps.python.org/pep-0475/)
|
||||||
* all volumes must exist / be available on startup; up2k (mtp especially) gets funky otherwise
|
* downloads from copyparty may suddenly fail, but uploads *should* be fine
|
||||||
* probably more, pls let me know
|
|
||||||
|
* python 2.7 on Windows:
|
||||||
|
* cannot index non-ascii filenames with `-e2d`
|
||||||
|
* cannot handle filenames with mojibake
|
||||||
|
|
||||||
## not my bugs
|
## not my bugs
|
||||||
|
|
||||||
|
same order here too
|
||||||
|
|
||||||
* [Chrome issue 1317069](https://bugs.chromium.org/p/chromium/issues/detail?id=1317069) -- if you try to upload a folder which contains symlinks by dragging it into the browser, the symlinked files will not get uploaded
|
* [Chrome issue 1317069](https://bugs.chromium.org/p/chromium/issues/detail?id=1317069) -- if you try to upload a folder which contains symlinks by dragging it into the browser, the symlinked files will not get uploaded
|
||||||
|
|
||||||
* [Chrome issue 1352210](https://bugs.chromium.org/p/chromium/issues/detail?id=1352210) -- plaintext http may be faster at filehashing than https (but also extremely CPU-intensive)
|
* [Chrome issue 1352210](https://bugs.chromium.org/p/chromium/issues/detail?id=1352210) -- plaintext http may be faster at filehashing than https (but also extremely CPU-intensive)
|
||||||
|
|
||||||
* [Firefox issue 1790500](https://bugzilla.mozilla.org/show_bug.cgi?id=1790500) -- entire browser can crash after uploading ~4000 small files
|
* [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)
|
* 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
|
* `AudioContext` will probably never be a viable workaround as apple introduces new issues faster than they fix current ones
|
||||||
* "future" because `AudioContext` can't maintain a stable playback speed in the current iOS version (15.7), maybe one day...
|
|
||||||
|
* iPhones: the preload feature (in the media-player-options tab) can cause a tiny audio glitch 20sec before the end of each song, but disabling it may cause worse iOS bugs to appear instead
|
||||||
|
* just a hunch, but disabling preloading may cause playback to stop entirely, or possibly mess with bluetooth speakers
|
||||||
|
* tried to add a tooltip regarding this but looks like apple broke my tooltips
|
||||||
|
|
||||||
* Windows: folders cannot be accessed if the name ends with `.`
|
* Windows: folders cannot be accessed if the name ends with `.`
|
||||||
* python or windows bug
|
* python or windows bug
|
||||||
@@ -287,6 +313,7 @@ server notes:
|
|||||||
|
|
||||||
* VirtualBox: sqlite throws `Disk I/O Error` when running in a VM and the up2k database is in a vboxsf
|
* VirtualBox: sqlite throws `Disk I/O Error` when running in a VM and the up2k database is in a vboxsf
|
||||||
* use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db inside the vm instead
|
* use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db inside the vm instead
|
||||||
|
* also happens on mergerfs, so put the db elsewhere
|
||||||
|
|
||||||
* Ubuntu: dragging files from certain folders into firefox or chrome is impossible
|
* Ubuntu: dragging files from certain folders into firefox or chrome is impossible
|
||||||
* due to snap security policies -- see `snap connections firefox` for the allowlist, `removable-media` permits all of `/mnt` and `/media` apparently
|
* due to snap security policies -- see `snap connections firefox` for the allowlist, `removable-media` permits all of `/mnt` and `/media` apparently
|
||||||
@@ -296,11 +323,13 @@ server notes:
|
|||||||
|
|
||||||
upgrade notes
|
upgrade notes
|
||||||
|
|
||||||
|
* `1.9.16` (2023-11-04):
|
||||||
|
* `--stats`/prometheus: `cpp_bans` renamed to `cpp_active_bans`, and that + `cpp_uptime` are gauges
|
||||||
* `1.6.0` (2023-01-29):
|
* `1.6.0` (2023-01-29):
|
||||||
* http-api: delete/move is now `POST` instead of `GET`
|
* http-api: delete/move is now `POST` instead of `GET`
|
||||||
* everything other than `GET` and `HEAD` must pass [cors validation](#cors)
|
* 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
|
* `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)
|
* **devs:** update third-party up2k clients (if those even exist)
|
||||||
|
|
||||||
|
|
||||||
@@ -315,11 +344,17 @@ upgrade notes
|
|||||||
* can I make copyparty download a file to my server if I give it a URL?
|
* can I make copyparty download a file to my server if I give it a URL?
|
||||||
* yes, using [hooks](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/wget.py)
|
* yes, using [hooks](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/wget.py)
|
||||||
|
|
||||||
|
* i want to learn python and/or programming and am considering looking at the copyparty source code in that occasion
|
||||||
|
* ```bash
|
||||||
|
_| _ __ _ _|_
|
||||||
|
(_| (_) | | (_) |_
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
# accounts and volumes
|
# accounts and volumes
|
||||||
|
|
||||||
per-folder, per-user permissions - if your setup is getting complex, consider making a [config file](./docs/example.conf) instead of using arguments
|
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)
|
* much easier to manage, and you can modify the config at runtime with `systemctl reload copyparty` or more conveniently using the `[reload cfg]` button in the control-panel (if the user has `a`/admin in any volume)
|
||||||
* changes to the `[global]` config section requires a restart to take effect
|
* changes to the `[global]` config section requires a restart to take effect
|
||||||
|
|
||||||
a quick summary can be seen using `--help-accounts`
|
a quick summary can be seen using `--help-accounts`
|
||||||
@@ -336,8 +371,12 @@ permissions:
|
|||||||
* `w` (write): upload files, move files *into* this folder
|
* `w` (write): upload files, move files *into* this folder
|
||||||
* `m` (move): move files/folders *from* this folder
|
* `m` (move): move files/folders *from* this folder
|
||||||
* `d` (delete): delete files/folders
|
* `d` (delete): delete files/folders
|
||||||
|
* `.` (dots): user can ask to show dotfiles in directory listings
|
||||||
* `g` (get): only download files, cannot see folder contents or zip/tar
|
* `g` (get): only download files, cannot see folder contents or zip/tar
|
||||||
* `G` (upget): same as `g` except uploaders get to see their own filekeys (see `fk` in examples below)
|
* `G` (upget): same as `g` except uploaders get to see their own [filekeys](#filekeys) (see `fk` in examples below)
|
||||||
|
* `h` (html): same as `g` except folders return their index.html, and filekeys are not necessary for index.html
|
||||||
|
* `a` (admin): can see upload time, uploader IPs, config-reload
|
||||||
|
* `A` ("all"): same as `rwmda.` (read/write/move/delete/admin/dotfiles)
|
||||||
|
|
||||||
examples:
|
examples:
|
||||||
* add accounts named u1, u2, u3 with passwords p1, p2, p3: `-a u1:p1 -a u2:p2 -a u3:p3`
|
* add accounts named u1, u2, u3 with passwords p1, p2, p3: `-a u1:p1 -a u2:p2 -a u3:p3`
|
||||||
@@ -349,7 +388,7 @@ examples:
|
|||||||
* `u1` can open the `inc` folder, but cannot see the contents, only upload new files to it
|
* `u1` can open the `inc` folder, but cannot see the contents, only upload new files to it
|
||||||
* `u2` can browse it and move files *from* `/inc` into any folder where `u2` has write-access
|
* `u2` can browse it and move files *from* `/inc` into any folder where `u2` has write-access
|
||||||
* make folder `/mnt/ss` available at `/i`, read-write for u1, get-only for everyone else, and enable filekeys: `-v /mnt/ss:i:rw,u1:g:c,fk=4`
|
* make folder `/mnt/ss` available at `/i`, read-write for u1, get-only for everyone else, and enable filekeys: `-v /mnt/ss:i:rw,u1:g:c,fk=4`
|
||||||
* `c,fk=4` sets the `fk` (filekey) volflag to 4, meaning each file gets a 4-character accesskey
|
* `c,fk=4` sets the `fk` ([filekey](#filekeys)) volflag to 4, meaning each file gets a 4-character accesskey
|
||||||
* `u1` can upload files, browse the folder, and see the generated filekeys
|
* `u1` can upload files, browse the folder, and see the generated filekeys
|
||||||
* other users cannot browse the folder, but can access the files if they have the full file URL with the filekey
|
* other users cannot browse the folder, but can access the files if they have the full file URL with the filekey
|
||||||
* replacing the `g` permission with `wg` would let anonymous users upload files, but not see the required filekey to access it
|
* replacing the `g` permission with `wg` would let anonymous users upload files, but not see the required filekey to access it
|
||||||
@@ -365,6 +404,17 @@ hiding specific subfolders by mounting another volume on top of them
|
|||||||
for example `-v /mnt::r -v /var/empty:web/certs:r` mounts the server folder `/mnt` as the webroot, but another volume is mounted at `/web/certs` -- so visitors can only see the contents of `/mnt` and `/mnt/web` (at URLs `/` and `/web`), but not `/mnt/web/certs` because URL `/web/certs` is mapped to `/var/empty`
|
for example `-v /mnt::r -v /var/empty:web/certs:r` mounts the server folder `/mnt` as the webroot, but another volume is mounted at `/web/certs` -- so visitors can only see the contents of `/mnt` and `/mnt/web` (at URLs `/` and `/web`), but not `/mnt/web/certs` because URL `/web/certs` is mapped to `/var/empty`
|
||||||
|
|
||||||
|
|
||||||
|
## dotfiles
|
||||||
|
|
||||||
|
unix-style hidden files/folders by starting the name with a dot
|
||||||
|
|
||||||
|
anyone can access these if they know the name, but they normally don't appear in directory listings
|
||||||
|
|
||||||
|
a client can request to see dotfiles in directory listings if global option `-ed` is specified, or the volume has volflag `dots`, or the user has permission `.`
|
||||||
|
|
||||||
|
dotfiles do not appear in search results unless one of the above is true, **and** the global option / volflag `dotsrch` is set
|
||||||
|
|
||||||
|
|
||||||
# the browser
|
# the browser
|
||||||
|
|
||||||
accessing a copyparty server using a web-browser
|
accessing a copyparty server using a web-browser
|
||||||
@@ -466,6 +516,7 @@ click the `🌲` or pressing the `B` hotkey to toggle between breadcrumbs path (
|
|||||||
## thumbnails
|
## thumbnails
|
||||||
|
|
||||||
press `g` or `田` to toggle grid-view instead of the file listing and `t` toggles icons / 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`
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -476,10 +527,14 @@ 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`)
|
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`
|
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), it will also try those names as dotfiles (`.folder.jpg` and so), and then fallback on the first picture in the folder (if it has any pictures at all)
|
||||||
|
|
||||||
in the grid/thumbnail view, if the audio player panel is open, songs will start playing when clicked
|
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 💾
|
* indicated by the audio files having the ▶ icon instead of 💾
|
||||||
|
|
||||||
|
enabling `multiselect` lets you click files to select them, and then shift-click another file for range-select
|
||||||
|
* `multiselect` is mostly intended for phones/tablets, but the `sel` option in the `[⚙️] settings` tab is better suited for desktop use, allowing selection by CTRL-clicking and range-selection with SHIFT-click, all without affecting regular clicking
|
||||||
|
|
||||||
|
|
||||||
## zip downloads
|
## zip downloads
|
||||||
|
|
||||||
@@ -490,12 +545,20 @@ select which type of archive you want in the `[⚙️] config` tab:
|
|||||||
| name | url-suffix | description |
|
| name | url-suffix | description |
|
||||||
|--|--|--|
|
|--|--|--|
|
||||||
| `tar` | `?tar` | plain gnutar, works great with `curl \| tar -xv` |
|
| `tar` | `?tar` | plain gnutar, works great with `curl \| tar -xv` |
|
||||||
|
| `pax` | `?tar=pax` | pax-format tar, futureproof, not as fast |
|
||||||
|
| `tgz` | `?tar=gz` | gzip compressed gnu-tar (slow), for `curl \| tar -xvz` |
|
||||||
|
| `txz` | `?tar=xz` | gnu-tar with xz / lzma compression (v.slow) |
|
||||||
| `zip` | `?zip=utf8` | works everywhere, glitchy filenames on win7 and older |
|
| `zip` | `?zip=utf8` | works everywhere, glitchy filenames on win7 and older |
|
||||||
| `zip_dos` | `?zip` | traditional cp437 (no unicode) to fix glitchy filenames |
|
| `zip_dos` | `?zip` | traditional cp437 (no unicode) to fix glitchy filenames |
|
||||||
| `zip_crc` | `?zip=crc` | cp437 with crc32 computed early for truly ancient software |
|
| `zip_crc` | `?zip=crc` | cp437 with crc32 computed early for truly ancient software |
|
||||||
|
|
||||||
* hidden files (dotfiles) are excluded unless `-ed`
|
* gzip default level is `3` (0=fast, 9=best), change with `?tar=gz:9`
|
||||||
|
* xz default level is `1` (0=fast, 9=best), change with `?tar=xz:9`
|
||||||
|
* bz2 default level is `2` (1=fast, 9=best), change with `?tar=bz2:9`
|
||||||
|
* hidden files ([dotfiles](#dotfiles)) are excluded unless account is allowed to list them
|
||||||
* `up2k.db` and `dir.txt` is always excluded
|
* `up2k.db` and `dir.txt` is always excluded
|
||||||
|
* bsdtar supports streaming unzipping: `curl foo?zip=utf8 | bsdtar -xv`
|
||||||
|
* good, because copyparty's zip is faster than tar on small files
|
||||||
* `zip_crc` will take longer to download since the server has to read each file twice
|
* `zip_crc` will take longer to download since the server has to read each file twice
|
||||||
* this is only to support MS-DOS PKZIP v2.04g (october 1993) and older
|
* this is only to support MS-DOS PKZIP v2.04g (october 1993) and older
|
||||||
* how are you accessing copyparty actually
|
* how are you accessing copyparty actually
|
||||||
@@ -504,10 +567,15 @@ you can also zip a selection of files or folders by clicking them in the browser
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
cool trick: download a folder by appending url-params `?tar&opus` to transcode all audio files (except aac|m4a|mp3|ogg|opus|wma) to opus before they're added to the archive
|
||||||
|
* super useful if you're 5 minutes away from takeoff and realize you don't have any music on your phone but your server only has flac files and downloading those will burn through all your data + there wouldn't be enough time anyways
|
||||||
|
* and url-params `&j` / `&w` produce jpeg/webm thumbnails/spectrograms instead of the original audio/video/images
|
||||||
|
* can also be used to pregenerate thumbnails; combine with `--th-maxage=9999999` or `--th-clean=0`
|
||||||
|
|
||||||
|
|
||||||
## uploading
|
## 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:
|
this initiates an upload using `up2k`; there are two uploaders available:
|
||||||
* `[🎈] bup`, the basic uploader, supports almost every browser since netscape 4.0
|
* `[🎈] bup`, the basic uploader, supports almost every browser since netscape 4.0
|
||||||
@@ -540,7 +608,8 @@ the up2k UI is the epitome of polished inutitive experiences:
|
|||||||
* "parallel uploads" specifies how many chunks to upload at the same time
|
* "parallel uploads" specifies how many chunks to upload at the same time
|
||||||
* `[🏃]` analysis of other files should continue while one is uploading
|
* `[🏃]` analysis of other files should continue while one is uploading
|
||||||
* `[🥔]` shows a simpler UI for faster uploads from slow devices
|
* `[🥔]` shows a simpler UI for faster uploads from slow devices
|
||||||
* `[💭]` ask for confirmation before files are added to the queue
|
* `[🎲]` generate random filenames during upload
|
||||||
|
* `[📅]` preserve last-modified timestamps; server times will match yours
|
||||||
* `[🔎]` switch between upload and [file-search](#file-search) mode
|
* `[🔎]` switch between upload and [file-search](#file-search) mode
|
||||||
* ignore `[🔎]` if you add files by dragging them into the browser
|
* ignore `[🔎]` if you add files by dragging them into the browser
|
||||||
|
|
||||||
@@ -602,6 +671,7 @@ file selection: click somewhere on the line (not the link itsef), then:
|
|||||||
* `up/down` to move
|
* `up/down` to move
|
||||||
* `shift-up/down` to move-and-select
|
* `shift-up/down` to move-and-select
|
||||||
* `ctrl-shift-up/down` to also scroll
|
* `ctrl-shift-up/down` to also scroll
|
||||||
|
* shift-click another line for range-select
|
||||||
|
|
||||||
* cut: select some files and `ctrl-x`
|
* cut: select some files and `ctrl-x`
|
||||||
* paste: `ctrl-v` in another folder
|
* paste: `ctrl-v` in another folder
|
||||||
@@ -671,7 +741,8 @@ some hilights:
|
|||||||
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)
|
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,
|
open the `[🎺]` media-player-settings tab to configure it,
|
||||||
* switches:
|
* "switches":
|
||||||
|
* `[🔀]` shuffles the files inside each folder
|
||||||
* `[preload]` starts loading the next track when it's about to end, reduces the silence between songs
|
* `[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
|
* `[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
|
* `[~s]` toggles the seekbar waveform display
|
||||||
@@ -681,11 +752,13 @@ open the `[🎺]` media-player-settings tab to configure it,
|
|||||||
* `[art]` shows album art on the lockscreen
|
* `[art]` shows album art on the lockscreen
|
||||||
* `[🎯]` keeps the playing song scrolled into view (good when using the player as a taskbar dock)
|
* `[🎯]` keeps the playing song scrolled into view (good when using the player as a taskbar dock)
|
||||||
* `[⟎]` shrinks the playback controls
|
* `[⟎]` shrinks the playback controls
|
||||||
* playback mode:
|
* "buttons":
|
||||||
|
* `[uncache]` may fix songs that won't play correctly due to bad files in browser cache
|
||||||
|
* "at end of folder":
|
||||||
* `[loop]` keeps looping the folder
|
* `[loop]` keeps looping the folder
|
||||||
* `[next]` plays into the next folder
|
* `[next]` plays into the next folder
|
||||||
* transcode:
|
* "transcode":
|
||||||
* `[flac]` convers `flac` and `wav` files into opus
|
* `[flac]` converts `flac` and `wav` files into opus
|
||||||
* `[aac]` converts `aac` and `m4a` files into opus
|
* `[aac]` converts `aac` and `m4a` files into opus
|
||||||
* `[oth]` converts all other known formats 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`
|
* `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`
|
||||||
@@ -694,12 +767,19 @@ open the `[🎺]` media-player-settings tab to configure it,
|
|||||||
|
|
||||||
### audio equalizer
|
### audio equalizer
|
||||||
|
|
||||||
bass boosted
|
and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression)
|
||||||
|
|
||||||
can also boost the volume in general, or increase/decrease stereo width (like [crossfeed](https://www.foobar2000.org/components/view/foo_dsp_meiercf) just worse)
|
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)
|
has the convenient side-effect of reducing the pause between songs, so gapless albums play better with the eq enabled (just make it flat)
|
||||||
|
|
||||||
|
not available on iPhones / iPads because AudioContext currently breaks background audio playback on iOS (15.7.8)
|
||||||
|
|
||||||
|
|
||||||
|
### fix unreliable playback on android
|
||||||
|
|
||||||
|
due to phone / app settings, android phones may randomly stop playing music when the power saver kicks in, especially at the end of an album -- you can fix it by [disabling power saving](https://user-images.githubusercontent.com/241032/235262123-c328cca9-3930-4948-bd18-3949b9fd3fcf.png) in the [app settings](https://user-images.githubusercontent.com/241032/235262121-2ffc51ae-7821-4310-a322-c3b7a507890c.png) of the browser you use for music streaming (preferably a dedicated one)
|
||||||
|
|
||||||
|
|
||||||
## markdown viewer
|
## markdown viewer
|
||||||
|
|
||||||
@@ -733,6 +813,8 @@ other notes,
|
|||||||
|
|
||||||
* files named `README.md` / `readme.md` will be rendered after directory listings unless `--no-readme` (but `.epilogue.html` takes precedence)
|
* files named `README.md` / `readme.md` will be rendered after directory listings unless `--no-readme` (but `.epilogue.html` takes precedence)
|
||||||
|
|
||||||
|
* `README.md` and `*logue.html` can contain placeholder values which are replaced server-side before embedding into directory listings; see `--help-exp`
|
||||||
|
|
||||||
|
|
||||||
## searching
|
## searching
|
||||||
|
|
||||||
@@ -758,9 +840,12 @@ for the above example to work, add the commandline argument `-e2ts` to also scan
|
|||||||
using arguments or config files, or a mix of both:
|
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)
|
* 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
|
* `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
|
* or click the `[reload cfg]` button in the control-panel if the user has `a`/admin in any volume
|
||||||
* changes to the `[global]` config section requires a restart to take effect
|
* changes to the `[global]` config section requires a restart to take effect
|
||||||
|
|
||||||
|
**NB:** as humongous as this readme is, there is also a lot of undocumented features. Run copyparty with `--help` to see all available global options; all of those can be used in the `[global]` section of config files, and everything listed in `--help-flags` can be used in volumes as volflags.
|
||||||
|
* if running in docker/podman, try this: `docker run --rm -it copyparty/ac --help`
|
||||||
|
|
||||||
|
|
||||||
## zeroconf
|
## zeroconf
|
||||||
|
|
||||||
@@ -818,6 +903,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
|
* 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
|
* 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
|
## webdav server
|
||||||
|
|
||||||
@@ -853,21 +945,41 @@ known client bugs:
|
|||||||
* latin-1 is fine, hiragana is not (not even as shift-jis on japanese xp)
|
* latin-1 is fine, hiragana is not (not even as shift-jis on japanese xp)
|
||||||
|
|
||||||
|
|
||||||
|
## tftp server
|
||||||
|
|
||||||
|
a TFTP server (read/write) can be started using `--tftp 3969` (you probably want [ftp](#ftp-server) instead unless you are *actually* communicating with hardware from the 90s (in which case we should definitely hang some time))
|
||||||
|
|
||||||
|
> that makes this the first RTX DECT Base that has been updated using copyparty 🎉
|
||||||
|
|
||||||
|
* based on [partftpy](https://github.com/9001/partftpy)
|
||||||
|
* no accounts; read from world-readable folders, write to world-writable, overwrite in world-deletable
|
||||||
|
* needs a dedicated port (cannot share with the HTTP/HTTPS API)
|
||||||
|
* run as root to use the spec-recommended port `69` (nice)
|
||||||
|
* can reply from a predefined portrange (good for firewalls)
|
||||||
|
* only supports the binary/octet/image transfer mode (no netascii)
|
||||||
|
* [RFC 7440](https://datatracker.ietf.org/doc/html/rfc7440) is **not** supported, so will be extremely slow over WAN
|
||||||
|
* expect 1100 KiB/s over 1000BASE-T, 400-500 KiB/s over wifi, 200 on bad wifi
|
||||||
|
|
||||||
|
some recommended TFTP clients:
|
||||||
|
* windows: `tftp.exe` (you probably already have it)
|
||||||
|
* linux: `tftp-hpa`, `atftp`
|
||||||
|
* `tftp 127.0.0.1 3969 -v -m binary -c put firmware.bin`
|
||||||
|
* `curl tftp://127.0.0.1:3969/firmware.bin` (read-only)
|
||||||
|
|
||||||
|
|
||||||
## smb server
|
## smb server
|
||||||
|
|
||||||
unsafe, slow, not recommended for wan, enable with `--smb` for read-only or `--smbw` for read-write
|
unsafe, slow, not recommended for wan, enable with `--smb` for read-only or `--smbw` for read-write
|
||||||
|
|
||||||
click the [connect](http://127.0.0.1:3923/?hc) button in the control-panel to see connection instructions for windows, linux, macos
|
click the [connect](http://127.0.0.1:3923/?hc) button in the control-panel to see connection instructions for windows, linux, macos
|
||||||
|
|
||||||
dependencies: `python3 -m pip install --user -U impacket==0.10.0`
|
dependencies: `python3 -m pip install --user -U impacket==0.11.0`
|
||||||
* newer versions of impacket will hopefully work just fine but there is monkeypatching so maybe not
|
* newer versions of impacket will hopefully work just fine but there is monkeypatching so maybe not
|
||||||
|
|
||||||
some **BIG WARNINGS** specific to SMB/CIFS, in decreasing importance:
|
some **BIG WARNINGS** specific to SMB/CIFS, in decreasing importance:
|
||||||
* not entirely confident that read-only is read-only
|
* not entirely confident that read-only is read-only
|
||||||
* the smb backend is not fully integrated with vfs, meaning there could be security issues (path traversal). Please use `--smb-port` (see below) and [prisonparty](./bin/prisonparty.sh)
|
* the smb backend is not fully integrated with vfs, meaning there could be security issues (path traversal). Please use `--smb-port` (see below) and [prisonparty](./bin/prisonparty.sh)
|
||||||
* account passwords work per-volume as expected, but account permissions are coalesced; all accounts have read-access to all volumes, and if a single account has write-access to some volume then all other accounts also do
|
* account passwords work per-volume as expected, and so does account permissions (read/write/move/delete), but `--smbw` must be given to allow write-access from smb
|
||||||
* if no accounts have write-access to a specific volume, or if `--smbw` is not set, then writing to that volume from smb *should* be impossible
|
|
||||||
* will be fixed once [impacket v0.11.0](https://github.com/SecureAuthCorp/impacket/commit/d923c00f75d54b972bca573a211a82f09b55261a) is released
|
|
||||||
* [shadowing](#shadowing) probably works as expected but no guarantees
|
* [shadowing](#shadowing) probably works as expected but no guarantees
|
||||||
|
|
||||||
and some minor issues,
|
and some minor issues,
|
||||||
@@ -878,7 +990,7 @@ and some minor issues,
|
|||||||
* win10 onwards does not allow connecting anonymously / without accounts
|
* win10 onwards does not allow connecting anonymously / without accounts
|
||||||
* on windows, creating a new file through rightclick --> new --> textfile throws an error due to impacket limitations -- hit OK and F5 to get your file
|
* on windows, creating a new file through rightclick --> new --> textfile throws an error due to impacket limitations -- hit OK and F5 to get your file
|
||||||
* python3 only
|
* python3 only
|
||||||
* slow
|
* slow (the builtin webdav support in windows is 5x faster, and rclone-webdav is 30x faster)
|
||||||
|
|
||||||
known client bugs:
|
known client bugs:
|
||||||
* on win7 only, `--smb1` is much faster than smb2 (default) because it keeps rescanning folders on smb2
|
* on win7 only, `--smb1` is much faster than smb2 (default) because it keeps rescanning folders on smb2
|
||||||
@@ -893,6 +1005,16 @@ authenticate with one of the following:
|
|||||||
* username `$password`, password `k`
|
* username `$password`, password `k`
|
||||||
|
|
||||||
|
|
||||||
|
## browser ux
|
||||||
|
|
||||||
|
tweaking the ui
|
||||||
|
|
||||||
|
* set default sort order globally with `--sort` or per-volume with the `sort` volflag; specify one or more comma-separated columns to sort by, and prefix the column name with `-` for reverse sort
|
||||||
|
* the column names you can use are visible as tooltips when hovering over the column headers in the directory listing, for example `href ext sz ts tags/.up_at tags/Cirle tags/.tn tags/Artist tags/Title`
|
||||||
|
* to sort in music order (album, track, artist, title) with filename as fallback, you could `--sort tags/Cirle,tags/.tn,tags/Artist,tags/Title,href`
|
||||||
|
* to sort by upload date, first enable showing the upload date in the listing with `-e2d -mte +.up_at` and then `--sort tags/.up_at`
|
||||||
|
|
||||||
|
|
||||||
## file indexing
|
## file indexing
|
||||||
|
|
||||||
enables dedup and music search ++
|
enables dedup and music search ++
|
||||||
@@ -912,14 +1034,14 @@ through arguments:
|
|||||||
* `--xlink` enables deduplication across volumes
|
* `--xlink` enables deduplication across volumes
|
||||||
|
|
||||||
the same arguments can be set as volflags, in addition to `d2d`, `d2ds`, `d2t`, `d2ts`, `d2v` for disabling:
|
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,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,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,d2ds` disables on-boot scans; only index new uploads
|
||||||
* `-v ~/music::r:c,d2ts` same except only affecting tags
|
* `-v ~/music::r:c,d2ts` same except only affecting tags
|
||||||
|
|
||||||
note:
|
note:
|
||||||
* the parser can finally handle `c,e2dsa,e2tsr` so you no longer have to `c,e2dsa:c,e2tsr`
|
* upload-times can be displayed in the file listing by enabling the `.up_at` metadata key, either globally with `-e2d -mte +.up_at` or per-volume with volflags `e2d,mte=+.up_at` (will have a ~17% performance impact on directory listings)
|
||||||
* `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
|
* `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
|
* 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!)
|
* deduplication is possible on windows if you run copyparty as administrator (not saying you should!)
|
||||||
@@ -933,6 +1055,8 @@ to save some time, you can provide a regex pattern for filepaths to only index
|
|||||||
|
|
||||||
similarly, you can fully ignore files/folders using `--no-idx [...]` and `:c,noidx=\.iso$`
|
similarly, you can fully ignore files/folders using `--no-idx [...]` and `:c,noidx=\.iso$`
|
||||||
|
|
||||||
|
* when running on macos, all the usual apple metadata files are excluded by default
|
||||||
|
|
||||||
if you set `--no-hash [...]` globally, you can enable hashing for specific volumes using flag `:c,nohash=`
|
if you set `--no-hash [...]` globally, you can enable hashing for specific volumes using flag `:c,nohash=`
|
||||||
|
|
||||||
### filesystem guards
|
### filesystem guards
|
||||||
@@ -941,7 +1065,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
|
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
|
### periodic rescan
|
||||||
|
|
||||||
@@ -958,6 +1086,8 @@ set upload rules using volflags, some examples:
|
|||||||
|
|
||||||
* `:c,sz=1k-3m` sets allowed filesize between 1 KiB and 3 MiB inclusive (suffixes: `b`, `k`, `m`, `g`)
|
* `:c,sz=1k-3m` sets allowed filesize between 1 KiB and 3 MiB inclusive (suffixes: `b`, `k`, `m`, `g`)
|
||||||
* `:c,df=4g` block uploads if there would be less than 4 GiB free disk space afterwards
|
* `:c,df=4g` block uploads if there would be less than 4 GiB free disk space afterwards
|
||||||
|
* `:c,vmaxb=1g` block uploads if total volume size would exceed 1 GiB afterwards
|
||||||
|
* `:c,vmaxn=4k` block uploads if volume would contain more than 4096 files afterwards
|
||||||
* `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`:
|
* `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`:
|
||||||
* `:c,rotn=1000,2` moves uploads into subfolders, up to 1000 files in each folder before making a new one, two levels deep (must be at least 1)
|
* `:c,rotn=1000,2` moves uploads into subfolders, up to 1000 files in each folder before making a new one, two levels deep (must be at least 1)
|
||||||
* `:c,rotf=%Y/%m/%d/%H` enforces files to be uploaded into a structure of subfolders according to that date format
|
* `:c,rotf=%Y/%m/%d/%H` enforces files to be uploaded into a structure of subfolders according to that date format
|
||||||
@@ -971,6 +1101,9 @@ you can also set transaction limits which apply per-IP and per-volume, but these
|
|||||||
* `:c,maxn=250,3600` allows 250 files over 1 hour from each IP (tracked per-volume)
|
* `:c,maxn=250,3600` allows 250 files over 1 hour from each IP (tracked per-volume)
|
||||||
* `:c,maxb=1g,300` allows 1 GiB total over 5 minutes from each IP (tracked per-volume)
|
* `:c,maxb=1g,300` allows 1 GiB total over 5 minutes from each IP (tracked per-volume)
|
||||||
|
|
||||||
|
notes:
|
||||||
|
* `vmaxb` and `vmaxn` requires either the `e2ds` volflag or `-e2dsa` global-option
|
||||||
|
|
||||||
|
|
||||||
## compress uploads
|
## compress uploads
|
||||||
|
|
||||||
@@ -1101,6 +1234,24 @@ note that this is way more complicated than the new [event hooks](#event-hooks)
|
|||||||
note that it will occupy the parsing threads, so fork anything expensive (or set `kn` to have copyparty fork it for you) -- otoh if you want to intentionally queue/singlethread you can combine it with `--mtag-mt 1`
|
note that it will occupy the parsing threads, so fork anything expensive (or set `kn` to have copyparty fork it for you) -- otoh if you want to intentionally queue/singlethread you can combine it with `--mtag-mt 1`
|
||||||
|
|
||||||
|
|
||||||
|
## handlers
|
||||||
|
|
||||||
|
redefine behavior with plugins ([examples](./bin/handlers/))
|
||||||
|
|
||||||
|
replace 404 and 403 errors with something completely different (that's it for now)
|
||||||
|
|
||||||
|
|
||||||
|
## identity providers
|
||||||
|
|
||||||
|
replace copyparty passwords with oauth and such
|
||||||
|
|
||||||
|
work is [ongoing](https://github.com/9001/copyparty/issues/62) to support authenticating / authorizing users based on a separate authentication proxy, which makes it possible to support oauth, single-sign-on, etc.
|
||||||
|
|
||||||
|
it is currently possible to specify `--idp-h-usr x-username`; copyparty will then skip password validation and blindly trust the username specified in the `X-Username` request header
|
||||||
|
|
||||||
|
the remaining stuff (accepting user groups through another header, creating volumes on the fly) are still to-do; configuration will probably [look like this](./docs/examples/docker/idp/copyparty.conf)
|
||||||
|
|
||||||
|
|
||||||
## hiding from google
|
## hiding from google
|
||||||
|
|
||||||
tell search engines you dont wanna be indexed, either using the good old [robots.txt](https://www.robotstxt.org/robotstxt.html) or through copyparty settings:
|
tell search engines you dont wanna be indexed, either using the good old [robots.txt](https://www.robotstxt.org/robotstxt.html) or through copyparty settings:
|
||||||
@@ -1137,9 +1288,33 @@ see the top of [./copyparty/web/browser.css](./copyparty/web/browser.css) where
|
|||||||
|
|
||||||
## complete examples
|
## complete examples
|
||||||
|
|
||||||
* [running on windows](./docs/examples/windows.md)
|
* see [running on windows](./docs/examples/windows.md) for a fancy windows setup
|
||||||
|
|
||||||
* read-only music server
|
* or use any of the examples below, just replace `python copyparty-sfx.py` with `copyparty.exe` if you're using the exe edition
|
||||||
|
|
||||||
|
* allow anyone to download or upload files into the current folder:
|
||||||
|
`python copyparty-sfx.py`
|
||||||
|
|
||||||
|
* enable searching and music indexing with `-e2dsa -e2ts`
|
||||||
|
|
||||||
|
* start an FTP server on port 3921 with `--ftp 3921`
|
||||||
|
|
||||||
|
* announce it on your LAN with `-z` so it appears in windows/Linux file managers
|
||||||
|
|
||||||
|
* anyone can upload, but nobody can see any files (even the uploader):
|
||||||
|
`python copyparty-sfx.py -e2dsa -v .::w`
|
||||||
|
|
||||||
|
* block uploads if there's less than 4 GiB free disk space with `--df 4`
|
||||||
|
|
||||||
|
* show a popup on new uploads with `--xau bin/hooks/notify.py`
|
||||||
|
|
||||||
|
* anyone can upload, and receive "secret" links for each upload they do:
|
||||||
|
`python copyparty-sfx.py -e2dsa -v .::wG:c,fk=8`
|
||||||
|
|
||||||
|
* anyone can browse (`r`), only `kevin` (password `okgo`) can upload/move/delete (`A`) files:
|
||||||
|
`python copyparty-sfx.py -e2dsa -a kevin:okgo -v .::r:A,kevin`
|
||||||
|
|
||||||
|
* read-only music server:
|
||||||
`python copyparty-sfx.py -v /mnt/nas/music:/music:r -e2dsa -e2ts --no-robots --force-js --theme 2`
|
`python copyparty-sfx.py -v /mnt/nas/music:/music:r -e2dsa -e2ts --no-robots --force-js --theme 2`
|
||||||
|
|
||||||
* ...with bpm and key scanning
|
* ...with bpm and key scanning
|
||||||
@@ -1162,6 +1337,7 @@ you can either:
|
|||||||
* 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
|
* 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
|
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
|
||||||
|
* **warning:** nginx-QUIC is still experimental and can make uploads much slower, so HTTP/2 is recommended for now
|
||||||
|
|
||||||
example webserver configs:
|
example webserver configs:
|
||||||
|
|
||||||
@@ -1169,6 +1345,98 @@ example webserver configs:
|
|||||||
* [apache2 config](contrib/apache/copyparty.conf) -- location-based
|
* [apache2 config](contrib/apache/copyparty.conf) -- location-based
|
||||||
|
|
||||||
|
|
||||||
|
## prometheus
|
||||||
|
|
||||||
|
metrics/stats can be enabled at URL `/.cpr/metrics` for grafana / prometheus / etc (openmetrics 1.0.0)
|
||||||
|
|
||||||
|
must be enabled with `--stats` since it reduces startup time a tiny bit, and you probably want `-e2dsa` too
|
||||||
|
|
||||||
|
the endpoint is only accessible by `admin` accounts, meaning the `a` in `rwmda` in the following example commandline: `python3 -m copyparty -a ed:wark -v /mnt/nas::rwmda,ed --stats -e2dsa`
|
||||||
|
|
||||||
|
follow a guide for setting up `node_exporter` except have it read from copyparty instead; example `/etc/prometheus/prometheus.yml` below
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: copyparty
|
||||||
|
metrics_path: /.cpr/metrics
|
||||||
|
basic_auth:
|
||||||
|
password: wark
|
||||||
|
static_configs:
|
||||||
|
- targets: ['192.168.123.1:3923']
|
||||||
|
```
|
||||||
|
|
||||||
|
currently the following metrics are available,
|
||||||
|
* `cpp_uptime_seconds` time since last copyparty restart
|
||||||
|
* `cpp_boot_unixtime_seconds` same but as an absolute timestamp
|
||||||
|
* `cpp_http_conns` number of open http(s) connections
|
||||||
|
* `cpp_http_reqs` number of http(s) requests handled
|
||||||
|
* `cpp_sus_reqs` number of 403/422/malicious requests
|
||||||
|
* `cpp_active_bans` number of currently banned IPs
|
||||||
|
* `cpp_total_bans` number of IPs banned since last restart
|
||||||
|
|
||||||
|
these are available unless `--nos-vst` is specified:
|
||||||
|
* `cpp_db_idle_seconds` time since last database activity (upload/rename/delete)
|
||||||
|
* `cpp_db_act_seconds` same but as an absolute timestamp
|
||||||
|
* `cpp_idle_vols` number of volumes which are idle / ready
|
||||||
|
* `cpp_busy_vols` number of volumes which are busy / indexing
|
||||||
|
* `cpp_offline_vols` number of volumes which are offline / unavailable
|
||||||
|
* `cpp_hashing_files` number of files queued for hashing / indexing
|
||||||
|
* `cpp_tagq_files` number of files queued for metadata scanning
|
||||||
|
* `cpp_mtpq_files` number of files queued for plugin-based analysis
|
||||||
|
|
||||||
|
and these are available per-volume only:
|
||||||
|
* `cpp_disk_size_bytes` total HDD size
|
||||||
|
* `cpp_disk_free_bytes` free HDD space
|
||||||
|
|
||||||
|
and these are per-volume and `total`:
|
||||||
|
* `cpp_vol_bytes` size of all files in volume
|
||||||
|
* `cpp_vol_files` number of files
|
||||||
|
* `cpp_dupe_bytes` disk space presumably saved by deduplication
|
||||||
|
* `cpp_dupe_files` number of dupe files
|
||||||
|
* `cpp_unf_bytes` currently unfinished / incoming uploads
|
||||||
|
|
||||||
|
some of the metrics have additional requirements to function correctly,
|
||||||
|
* `cpp_vol_*` requires either the `e2ds` volflag or `-e2dsa` global-option
|
||||||
|
|
||||||
|
the following options are available to disable some of the metrics:
|
||||||
|
* `--nos-hdd` disables `cpp_disk_*` which can prevent spinning up HDDs
|
||||||
|
* `--nos-vol` disables `cpp_vol_*` which reduces server startup time
|
||||||
|
* `--nos-vst` disables volume state, reducing the worst-case prometheus query time by 0.5 sec
|
||||||
|
* `--nos-dup` disables `cpp_dupe_*` which reduces the server load caused by prometheus queries
|
||||||
|
* `--nos-unf` disables `cpp_unf_*` for no particular purpose
|
||||||
|
|
||||||
|
note: the following metrics are counted incorrectly if multiprocessing is enabled with `-j`: `cpp_http_conns`, `cpp_http_reqs`, `cpp_sus_reqs`, `cpp_active_bans`, `cpp_total_bans`
|
||||||
|
|
||||||
|
|
||||||
|
# packages
|
||||||
|
|
||||||
|
the party might be closer than you think
|
||||||
|
|
||||||
|
if your distro/OS is not mentioned below, there might be some hints in the [«on servers»](#on-servers) section
|
||||||
|
|
||||||
|
|
||||||
|
## arch package
|
||||||
|
|
||||||
|
now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
|
||||||
|
|
||||||
|
it comes with a [systemd service](./contrib/package/arch/copyparty.service) and expects to find one or more [config files](./docs/example.conf) in `/etc/copyparty.d/`
|
||||||
|
|
||||||
|
|
||||||
|
## fedora package
|
||||||
|
|
||||||
|
currently **NOT** available on [copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/) , fedora is having issues with their build servers and won't be fixed for several months
|
||||||
|
|
||||||
|
if you previously installed copyparty from copr, you may run one of the following commands to upgrade to a more recent version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dnf install https://ocv.me/copyparty/fedora/37/python3-copyparty.fc37.noarch.rpm
|
||||||
|
dnf install https://ocv.me/copyparty/fedora/38/python3-copyparty.fc38.noarch.rpm
|
||||||
|
dnf install https://ocv.me/copyparty/fedora/39/python3-copyparty.fc39.noarch.rpm
|
||||||
|
```
|
||||||
|
|
||||||
|
to run copyparty as a service, use the [systemd service scripts](https://github.com/9001/copyparty/tree/hovudstraum/contrib/systemd), just replace `/usr/bin/python3 /usr/local/bin/copyparty-sfx.py` with `/usr/bin/copyparty`
|
||||||
|
|
||||||
|
|
||||||
## nix package
|
## nix package
|
||||||
|
|
||||||
`nix profile install github:9001/copyparty`
|
`nix profile install github:9001/copyparty`
|
||||||
@@ -1296,15 +1564,16 @@ TLDR: yes
|
|||||||
| navpane | - | yep | yep | yep | yep | yep | yep | yep |
|
| navpane | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| image viewer | - | yep | yep | yep | yep | yep | yep | yep |
|
| image viewer | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| video player | - | yep | yep | yep | yep | yep | yep | yep |
|
| video player | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| markdown editor | - | - | yep | yep | yep | yep | yep | yep |
|
| markdown editor | - | - | `*2` | `*2` | yep | yep | yep | yep |
|
||||||
| markdown viewer | - | yep | yep | yep | yep | yep | yep | yep |
|
| markdown viewer | - | `*2` | `*2` | `*2` | yep | yep | yep | yep |
|
||||||
| play mp3/m4a | - | yep | yep | yep | yep | yep | yep | yep |
|
| play mp3/m4a | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| play ogg/opus | - | - | - | - | yep | yep | `*3` | yep |
|
| play ogg/opus | - | - | - | - | yep | yep | `*3` | yep |
|
||||||
| **= feature =** | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
|
| **= feature =** | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
|
||||||
|
|
||||||
* internet explorer 6 to 8 behave the same
|
* internet explorer 6 through 8 behave the same
|
||||||
* firefox 52 and chrome 49 are the final winxp versions
|
* firefox 52 and chrome 49 are the final winxp versions
|
||||||
* `*1` yes, but extremely slow (ie10: `1 MiB/s`, ie11: `270 KiB/s`)
|
* `*1` yes, but extremely slow (ie10: `1 MiB/s`, ie11: `270 KiB/s`)
|
||||||
|
* `*2` only able to do plaintext documents (no markdown rendering)
|
||||||
* `*3` iOS 11 and newer, opus only, and requires FFmpeg on the server
|
* `*3` iOS 11 and newer, opus only, and requires FFmpeg on the server
|
||||||
|
|
||||||
quick summary of more eccentric web-browsers trying to view a directory index:
|
quick summary of more eccentric web-browsers trying to view a directory index:
|
||||||
@@ -1330,10 +1599,12 @@ interact with copyparty using non-browser clients
|
|||||||
* `var xhr = new XMLHttpRequest(); xhr.open('POST', '//127.0.0.1:3923/msgs?raw'); xhr.send('foo');`
|
* `var xhr = new XMLHttpRequest(); xhr.open('POST', '//127.0.0.1:3923/msgs?raw'); xhr.send('foo');`
|
||||||
|
|
||||||
* curl/wget: upload some files (post=file, chunk=stdin)
|
* curl/wget: upload some files (post=file, chunk=stdin)
|
||||||
* `post(){ curl -F act=bput -F f=@"$1" http://127.0.0.1:3923/?pw=wark;}`
|
* `post(){ curl -F f=@"$1" http://127.0.0.1:3923/?pw=wark;}`
|
||||||
`post movie.mkv`
|
`post movie.mkv` (gives HTML in return)
|
||||||
|
* `post(){ curl -F f=@"$1" 'http://127.0.0.1:3923/?want=url&pw=wark';}`
|
||||||
|
`post movie.mkv` (gives hotlink in return)
|
||||||
* `post(){ curl -H pw:wark -H rand:8 -T "$1" http://127.0.0.1:3923/;}`
|
* `post(){ curl -H pw:wark -H rand:8 -T "$1" http://127.0.0.1:3923/;}`
|
||||||
`post movie.mkv`
|
`post movie.mkv` (randomized filename)
|
||||||
* `post(){ wget --header='pw: wark' --post-file="$1" -O- http://127.0.0.1:3923/?raw;}`
|
* `post(){ wget --header='pw: wark' --post-file="$1" -O- http://127.0.0.1:3923/?raw;}`
|
||||||
`post movie.mkv`
|
`post movie.mkv`
|
||||||
* `chunk(){ curl -H pw:wark -T- http://127.0.0.1:3923/;}`
|
* `chunk(){ curl -H pw:wark -T- http://127.0.0.1:3923/;}`
|
||||||
@@ -1343,10 +1614,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 /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`
|
* `(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)
|
* 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
|
* file uploads, file-search, [folder sync](#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)
|
* can be downloaded from copyparty: controlpanel -> connect -> [u2c.py](http://127.0.0.1:3923/.cpr/a/u2c.py)
|
||||||
* see [./bin/README.md#up2kpy](bin/README.md#up2kpy)
|
* see [./bin/README.md#u2cpy](bin/README.md#u2cpy)
|
||||||
|
|
||||||
* FUSE: mount a copyparty server as a local filesystem
|
* FUSE: mount a copyparty server as a local filesystem
|
||||||
* cross-platform python client available in [./bin/](bin/)
|
* cross-platform python client available in [./bin/](bin/)
|
||||||
@@ -1355,6 +1626,10 @@ interact with copyparty using non-browser clients
|
|||||||
|
|
||||||
* sharex (screenshot utility): see [./contrib/sharex.sxcu](contrib/#sharexsxcu)
|
* sharex (screenshot utility): see [./contrib/sharex.sxcu](contrib/#sharexsxcu)
|
||||||
|
|
||||||
|
* contextlet (web browser integration); see [contrib contextlet](contrib/#send-to-cppcontextletjson)
|
||||||
|
|
||||||
|
* [igloo irc](https://iglooirc.com/): Method: `post` Host: `https://you.com/up/?want=url&pw=hunter2` Multipart: `yes` File parameter: `f`
|
||||||
|
|
||||||
copyparty returns a truncated sha512sum of your PUT/POST as base64; you can generate the same checksum locally to verify uplaods:
|
copyparty returns a truncated sha512sum of your PUT/POST as base64; you can generate the same checksum locally to verify uplaods:
|
||||||
|
|
||||||
b512(){ printf "$((sha512sum||shasum -a512)|sed -E 's/ .*//;s/(..)/\\x\1/g')"|base64|tr '+/' '-_'|head -c44;}
|
b512(){ printf "$((sha512sum||shasum -a512)|sed -E 's/ .*//;s/(..)/\\x\1/g')"|base64|tr '+/' '-_'|head -c44;}
|
||||||
@@ -1369,11 +1644,11 @@ NOTE: curl will not send the original filename if you use `-T` combined with url
|
|||||||
|
|
||||||
sync folders to/from copyparty
|
sync folders to/from copyparty
|
||||||
|
|
||||||
the commandline uploader [up2k.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#up2kpy) 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)
|
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
|
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 up2k.py
|
* starting from rclone v1.63, rclone is faster than u2c.py on low-latency connections
|
||||||
|
|
||||||
|
|
||||||
## mount as drive
|
## mount as drive
|
||||||
@@ -1382,7 +1657,7 @@ a remote copyparty server as a local filesystem; go to the control-panel and cl
|
|||||||
|
|
||||||
alternatively, some alternatives roughly sorted by speed (unreproducible benchmark), best first:
|
alternatively, some alternatives roughly sorted by speed (unreproducible benchmark), best first:
|
||||||
|
|
||||||
* [rclone-webdav](./docs/rclone.md) (25s), read/WRITE ([v1.63-beta](https://beta.rclone.org/?filter=latest))
|
* [rclone-webdav](./docs/rclone.md) (25s), read/WRITE (rclone v1.63 or later)
|
||||||
* [rclone-http](./docs/rclone.md) (26s), read-only
|
* [rclone-http](./docs/rclone.md) (26s), read-only
|
||||||
* [partyfuse.py](./bin/#partyfusepy) (35s), read-only
|
* [partyfuse.py](./bin/#partyfusepy) (35s), read-only
|
||||||
* [rclone-ftp](./docs/rclone.md) (47s), read/WRITE
|
* [rclone-ftp](./docs/rclone.md) (47s), read/WRITE
|
||||||
@@ -1421,15 +1696,16 @@ defaults are usually fine - expect `8 GiB/s` download, `1 GiB/s` upload
|
|||||||
below are some tweaks roughly ordered by usefulness:
|
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
|
* `-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
|
* `--hist` pointing to a fast location (ssd) will make directory listings and searches faster when `-e2d` or `-e2t` is set
|
||||||
|
* and also makes thumbnails load faster, regardless of e2d/e2t
|
||||||
* `--no-hash .` when indexing a network-disk if you don't care about the actual filehashes and only want the names/tags searchable
|
* `--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)
|
* `--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)
|
||||||
* `-j0` enables multiprocessing (actual multithreading), can reduce latency to `20+80/numCores` percent and generally improve performance in cpu-intensive workloads, for example:
|
* `-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)
|
* lots of connections (many users or heavy clients)
|
||||||
* simultaneous downloads and uploads saturating a 20gbps connection
|
* simultaneous downloads and uploads saturating a 20gbps connection
|
||||||
|
* if `-e2d` is enabled, `-j2` gives 4x performance for directory listings; `-j4` gives 16x
|
||||||
|
|
||||||
...however it adds an overhead to internal communication so it might be a net loss, see if it works 4 u
|
...however it also increases the server/filesystem/HDD load during uploads, and adds an overhead to internal communication, so it is usually a better idea to don't
|
||||||
* 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
|
* 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)
|
* and pypy can sometimes crash on startup with `-j0` (TODO make issue)
|
||||||
|
|
||||||
@@ -1438,10 +1714,10 @@ below are some tweaks roughly ordered by usefulness:
|
|||||||
|
|
||||||
when uploading files,
|
when uploading files,
|
||||||
|
|
||||||
* chrome is recommended, at least compared to firefox:
|
* chrome is recommended (unfortunately), at least compared to firefox:
|
||||||
* up to 90% faster when hashing, especially on SSDs
|
* up to 90% faster when hashing, especially on SSDs
|
||||||
* up to 40% faster when uploading over extremely fast internets
|
* 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:
|
* 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)
|
* up to 30% faster uploads if you hide the upload status list by switching away from the `[🚀]` up2k ui-tab (or closing it)
|
||||||
@@ -1452,10 +1728,14 @@ when uploading files,
|
|||||||
|
|
||||||
# security
|
# security
|
||||||
|
|
||||||
|
there is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone` for all important updates (at the lack of better ideas)
|
||||||
|
|
||||||
some notes on hardening
|
some notes on hardening
|
||||||
|
|
||||||
* set `--rproxy 0` if your copyparty is directly facing the internet (not through a reverse-proxy)
|
* set `--rproxy 0` if your copyparty is directly facing the internet (not through a reverse-proxy)
|
||||||
* cors doesn't work right otherwise
|
* cors doesn't work right otherwise
|
||||||
|
* if you allow anonymous uploads or otherwise don't trust the contents of a volume, you can prevent XSS with volflag `nohtml`
|
||||||
|
* this returns html documents as plaintext, and also disables markdown rendering
|
||||||
|
|
||||||
safety profiles:
|
safety profiles:
|
||||||
|
|
||||||
@@ -1469,9 +1749,7 @@ safety profiles:
|
|||||||
* `--unpost 0`, `--no-del`, `--no-mv` disables all move/delete support
|
* `--unpost 0`, `--no-del`, `--no-mv` disables all move/delete support
|
||||||
* `--hardlink` creates hardlinks instead of symlinks when deduplicating uploads, which is less maintenance
|
* `--hardlink` creates hardlinks instead of symlinks when deduplicating uploads, which is less maintenance
|
||||||
* however note if you edit one file it will also affect the other copies
|
* however note if you edit one file it will also affect the other copies
|
||||||
* `--vague-401` returns a "404 not found" instead of "401 unauthorized" which is a common enterprise meme
|
* `--vague-403` returns a "404 not found" instead of "401 unauthorized" which is a common enterprise meme
|
||||||
* `--ban-404=50,60,1440` ban client for 1440min (24h) if they hit 50 404's in 60min
|
|
||||||
* **NB:** will ban anyone who enables up2k turbo
|
|
||||||
* `--nih` removes the server hostname from directory listings
|
* `--nih` removes the server hostname from directory listings
|
||||||
|
|
||||||
* option `-sss` is a shortcut for the above plus:
|
* option `-sss` is a shortcut for the above plus:
|
||||||
@@ -1483,9 +1761,9 @@ safety profiles:
|
|||||||
other misc notes:
|
other misc notes:
|
||||||
|
|
||||||
* you can disable directory listings by giving permission `g` instead of `r`, only accepting direct URLs to files
|
* 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
|
* you may want [filekeys](#filekeys) to prevent filename bruteforcing
|
||||||
* the default filekey entropy is fairly small so give `--fk-salt` around 30 characters if you want filekeys longer than 16 chars
|
* permission `h` instead of `r` makes copyparty behave like a traditional webserver with directory listing/index disabled, returning index.html instead
|
||||||
* permissions `wG` lets users upload files and receive their own filekeys, still without being able to see other uploads
|
* compatibility with filekeys: index.html itself can be retrieved without the correct filekey, but all other files are protected
|
||||||
|
|
||||||
|
|
||||||
## gotchas
|
## gotchas
|
||||||
@@ -1493,10 +1771,12 @@ other misc notes:
|
|||||||
behavior that might be unexpected
|
behavior that might be unexpected
|
||||||
|
|
||||||
* users without read-access to a folder can still see the `.prologue.html` / `.epilogue.html` / `README.md` contents, for the purpose of showing a description on how to use the uploader for example
|
* users without read-access to a folder can still see the `.prologue.html` / `.epilogue.html` / `README.md` contents, for the purpose of showing a description on how to use the uploader for example
|
||||||
* users can submit `<script>`s which autorun for other visitors in a few ways;
|
* users can submit `<script>`s which autorun (in a sandbox) for other visitors in a few ways;
|
||||||
* uploading a `README.md` -- avoid with `--no-readme`
|
* uploading a `README.md` -- avoid with `--no-readme`
|
||||||
* renaming `some.html` to `.epilogue.html` -- avoid with either `--no-logues` or `--no-dot-ren`
|
* renaming `some.html` to `.epilogue.html` -- avoid with either `--no-logues` or `--no-dot-ren`
|
||||||
* the directory-listing embed is sandboxed (so any malicious scripts can't do any damage) but the markdown editor is not
|
* the directory-listing embed is sandboxed (so any malicious scripts can't do any damage) but the markdown editor is not 100% safe, see below
|
||||||
|
* markdown documents can contain html and `<script>`s; attempts are made to prevent scripts from executing (unless `-emp` is specified) but this is not 100% bulletproof, so setting the `nohtml` volflag is still the safest choice
|
||||||
|
* or eliminate the problem entirely by only giving write-access to trustworthy people :^)
|
||||||
|
|
||||||
|
|
||||||
## cors
|
## cors
|
||||||
@@ -1511,12 +1791,39 @@ 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`
|
cors can be configured with `--acao` and `--acam`, or the protections entirely disabled with `--allow-csrf`
|
||||||
|
|
||||||
|
|
||||||
|
## filekeys
|
||||||
|
|
||||||
|
prevent filename bruteforcing
|
||||||
|
|
||||||
|
volflag `c,fk` generates filekeys (per-file accesskeys) for all files; users which have full read-access (permission `r`) will then see URLs with the correct filekey `?k=...` appended to the end, and `g` users must provide that URL including the correct key to avoid a 404
|
||||||
|
|
||||||
|
by default, filekeys are generated based on salt (`--fk-salt`) + filesystem-path + file-size + inode (if not windows); add volflag `fka` to generate slightly weaker filekeys which will not be invalidated if the file is edited (only salt + path)
|
||||||
|
|
||||||
|
permissions `wG` (write + upget) lets users upload files and receive their own filekeys, still without being able to see other uploads
|
||||||
|
|
||||||
|
|
||||||
|
## password hashing
|
||||||
|
|
||||||
|
you can hash passwords before putting them into config files / providing them as arguments; see `--help-pwhash` for all the details
|
||||||
|
|
||||||
|
`--ah-alg argon2` enables it, and if you have any plaintext passwords then it'll print the hashed versions on startup so you can replace them
|
||||||
|
|
||||||
|
optionally also specify `--ah-cli` to enter an interactive mode where it will hash passwords without ever writing the plaintext ones to disk
|
||||||
|
|
||||||
|
the default configs take about 0.4 sec and 256 MiB RAM to process a new password on a decent laptop
|
||||||
|
|
||||||
|
|
||||||
## https
|
## 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)
|
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
|
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
|
# recovering from crashes
|
||||||
|
|
||||||
@@ -1553,6 +1860,8 @@ mandatory deps:
|
|||||||
|
|
||||||
install these to enable bonus features
|
install these to enable bonus features
|
||||||
|
|
||||||
|
enable hashed passwords in config: `argon2-cffi`
|
||||||
|
|
||||||
enable ftp-server:
|
enable ftp-server:
|
||||||
* for just plaintext FTP, `pyftpdlib` (is built into the SFX)
|
* for just plaintext FTP, `pyftpdlib` (is built into the SFX)
|
||||||
* with TLS encryption, `pyftpdlib pyopenssl`
|
* with TLS encryption, `pyftpdlib pyopenssl`
|
||||||
@@ -1569,7 +1878,7 @@ enable [thumbnails](#thumbnails) of...
|
|||||||
* **JPEG XL pictures:** `pyvips` or `ffmpeg`
|
* **JPEG XL pictures:** `pyvips` or `ffmpeg`
|
||||||
|
|
||||||
enable [smb](#smb-server) support (**not** recommended):
|
enable [smb](#smb-server) support (**not** recommended):
|
||||||
* `impacket==0.10.0`
|
* `impacket==0.11.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`
|
`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`
|
||||||
|
|
||||||
@@ -1599,14 +1908,15 @@ can be convenient on machines where installing python is problematic, however is
|
|||||||
* [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
|
* [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
|
||||||
|
|
||||||
* on win8 it needs [vc redist 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48145), on win10 it just works
|
* 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: [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
|
* dangerous and deprecated: [copyparty-winpe64.exe](https://github.com/9001/copyparty/releases/download/v1.8.7/copyparty-winpe64.exe) lets you [run copyparty in WinPE](https://user-images.githubusercontent.com/241032/205454984-e6b550df-3c49-486d-9267-1614078dd0dd.png) and is otherwise completely useless
|
||||||
|
|
||||||
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
|
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
|
||||||
|
|
||||||
then again, if you are already into downloading shady binaries from the internet, you may also want my [minimal builds](./scripts/pyinstaller#ffmpeg) of [ffmpeg](https://ocv.me/stuff/bin/ffmpeg.exe) and [ffprobe](https://ocv.me/stuff/bin/ffprobe.exe) which enables copyparty to extract multimedia-info, do audio-transcoding, and thumbnails/spectrograms/waveforms, however it's much better to instead grab a [recent official build](https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip) every once ina while if you can afford the size
|
then again, if you are already into downloading shady binaries from the internet, you may also want my [minimal builds](./scripts/pyinstaller#ffmpeg) of [ffmpeg](https://ocv.me/stuff/bin/ffmpeg.exe) and [ffprobe](https://ocv.me/stuff/bin/ffprobe.exe) which enables copyparty to extract multimedia-info, do audio-transcoding, and thumbnails/spectrograms/waveforms, however it's much better to instead grab a [recent official build](https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z) every once ina while if you can afford the size
|
||||||
|
|
||||||
|
|
||||||
# install on android
|
# install on android
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# [`up2k.py`](up2k.py)
|
# [`u2c.py`](u2c.py)
|
||||||
* command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
|
* command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
|
||||||
* file uploads, file-search, autoresume of aborted/broken uploads
|
* file uploads, file-search, autoresume of aborted/broken uploads
|
||||||
* sync local folder to server
|
* sync local folder to server
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ def examples():
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
global NC, BY_PATH
|
global NC, BY_PATH # pylint: disable=global-statement
|
||||||
os.system("")
|
os.system("")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
@@ -282,7 +282,8 @@ def main():
|
|||||||
if ver == "corrupt":
|
if ver == "corrupt":
|
||||||
die("{} database appears to be corrupt, sorry")
|
die("{} database appears to be corrupt, sorry")
|
||||||
|
|
||||||
if ver < DB_VER1 or ver > DB_VER2:
|
iver = int(ver)
|
||||||
|
if iver < DB_VER1 or iver > DB_VER2:
|
||||||
m = f"{n} db is version {ver}, this tool only supports versions between {DB_VER1} and {DB_VER2}, please upgrade it with copyparty first"
|
m = f"{n} db is version {ver}, this tool only supports versions between {DB_VER1} and {DB_VER2}, please upgrade it with copyparty first"
|
||||||
die(m)
|
die(m)
|
||||||
|
|
||||||
|
|||||||
35
bin/handlers/README.md
Normal file
35
bin/handlers/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
replace the standard 404 / 403 responses with plugins
|
||||||
|
|
||||||
|
|
||||||
|
# usage
|
||||||
|
|
||||||
|
load plugins either globally with `--on404 ~/dev/copyparty/bin/handlers/sorry.py` or for a specific volume with `:c,on404=~/handlers/sorry.py`
|
||||||
|
|
||||||
|
|
||||||
|
# api
|
||||||
|
|
||||||
|
each plugin must define a `main()` which takes 3 arguments;
|
||||||
|
|
||||||
|
* `cli` is an instance of [copyparty/httpcli.py](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/httpcli.py) (the monstrosity itself)
|
||||||
|
* `vn` is the VFS which overlaps with the requested URL, and
|
||||||
|
* `rem` is the URL remainder below the VFS mountpoint
|
||||||
|
* so `vn.vpath + rem` == `cli.vpath` == original request
|
||||||
|
|
||||||
|
|
||||||
|
# examples
|
||||||
|
|
||||||
|
## on404
|
||||||
|
|
||||||
|
* [sorry.py](answer.py) replies with a custom message instead of the usual 404
|
||||||
|
* [nooo.py](nooo.py) replies with an endless noooooooooooooo
|
||||||
|
* [never404.py](never404.py) 100% guarantee that 404 will never be a thing again as it automatically creates dummy files whenever necessary
|
||||||
|
* [caching-proxy.py](caching-proxy.py) transforms copyparty into a squid/varnish knockoff
|
||||||
|
|
||||||
|
## on403
|
||||||
|
|
||||||
|
* [ip-ok.py](ip-ok.py) disables security checks if client-ip is 1.2.3.4
|
||||||
|
|
||||||
|
|
||||||
|
# notes
|
||||||
|
|
||||||
|
* on403 only works for trivial stuff (basic http access) since I haven't been able to think of any good usecases for it (was just easy to add while doing on404)
|
||||||
36
bin/handlers/caching-proxy.py
Executable file
36
bin/handlers/caching-proxy.py
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
# assume each requested file exists on another webserver and
|
||||||
|
# download + mirror them as they're requested
|
||||||
|
# (basically pretend we're warnish)
|
||||||
|
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from copyparty.httpcli import HttpCli
|
||||||
|
|
||||||
|
|
||||||
|
def main(cli: "HttpCli", vn, rem):
|
||||||
|
url = "https://mirrors.edge.kernel.org/alpine/" + rem
|
||||||
|
abspath = os.path.join(vn.realpath, rem)
|
||||||
|
|
||||||
|
# sneaky trick to preserve a requests-session between downloads
|
||||||
|
# so it doesn't have to spend ages reopening https connections;
|
||||||
|
# luckily we can stash it inside the copyparty client session,
|
||||||
|
# name just has to be definitely unused so "hacapo_req_s" it is
|
||||||
|
req_s = getattr(cli.conn, "hacapo_req_s", None) or requests.Session()
|
||||||
|
setattr(cli.conn, "hacapo_req_s", req_s)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(abspath), exist_ok=True)
|
||||||
|
with req_s.get(url, stream=True, timeout=69) as r:
|
||||||
|
r.raise_for_status()
|
||||||
|
with open(abspath, "wb", 64 * 1024) as f:
|
||||||
|
for buf in r.iter_content(chunk_size=64 * 1024):
|
||||||
|
f.write(buf)
|
||||||
|
except:
|
||||||
|
os.unlink(abspath)
|
||||||
|
return "false"
|
||||||
|
|
||||||
|
return "retry"
|
||||||
6
bin/handlers/ip-ok.py
Executable file
6
bin/handlers/ip-ok.py
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
# disable permission checks and allow access if client-ip is 1.2.3.4
|
||||||
|
|
||||||
|
|
||||||
|
def main(cli, vn, rem):
|
||||||
|
if cli.ip == "1.2.3.4":
|
||||||
|
return "allow"
|
||||||
11
bin/handlers/never404.py
Executable file
11
bin/handlers/never404.py
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
# create a dummy file and let copyparty return it
|
||||||
|
|
||||||
|
|
||||||
|
def main(cli, vn, rem):
|
||||||
|
print("hello", cli.ip)
|
||||||
|
|
||||||
|
abspath = vn.canonical(rem)
|
||||||
|
with open(abspath, "wb") as f:
|
||||||
|
f.write(b"404? not on MY watch!")
|
||||||
|
|
||||||
|
return "retry"
|
||||||
16
bin/handlers/nooo.py
Executable file
16
bin/handlers/nooo.py
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
# reply with an endless "noooooooooooooooooooooooo"
|
||||||
|
|
||||||
|
|
||||||
|
def say_no():
|
||||||
|
yield b"n"
|
||||||
|
while True:
|
||||||
|
yield b"o" * 4096
|
||||||
|
|
||||||
|
|
||||||
|
def main(cli, vn, rem):
|
||||||
|
cli.send_headers(None, 404, "text/plain")
|
||||||
|
|
||||||
|
for chunk in say_no():
|
||||||
|
cli.s.sendall(chunk)
|
||||||
|
|
||||||
|
return "false"
|
||||||
7
bin/handlers/sorry.py
Executable file
7
bin/handlers/sorry.py
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
# sends a custom response instead of the usual 404
|
||||||
|
|
||||||
|
|
||||||
|
def main(cli, vn, rem):
|
||||||
|
msg = f"sorry {cli.ip} but {cli.vpath} doesn't exist"
|
||||||
|
|
||||||
|
return str(cli.reply(msg.encode("utf-8"), 404, "text/plain"))
|
||||||
123
bin/hooks/msg-log.py
Executable file
123
bin/hooks/msg-log.py
Executable file
@@ -0,0 +1,123 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# coding: utf-8
|
||||||
|
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
try:
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
except:
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
use copyparty as a dumb messaging server / guestbook thing;
|
||||||
|
initially contributed by @clach04 in https://github.com/9001/copyparty/issues/35 (thanks!)
|
||||||
|
|
||||||
|
Sample usage:
|
||||||
|
|
||||||
|
python copyparty-sfx.py --xm j,bin/hooks/msg-log.py
|
||||||
|
|
||||||
|
Where:
|
||||||
|
|
||||||
|
xm = execute on message-to-server-log
|
||||||
|
j = provide message information as json; not just the text - this script REQUIRES json
|
||||||
|
t10 = timeout and kill download after 10 secs
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# output filename
|
||||||
|
FILENAME = os.environ.get("COPYPARTY_MESSAGE_FILENAME", "") or "README.md"
|
||||||
|
|
||||||
|
# set True to write in descending order (newest message at top of file);
|
||||||
|
# note that this becomes very slow/expensive as the file gets bigger
|
||||||
|
DESCENDING = True
|
||||||
|
|
||||||
|
# the message template; the following parameters are provided by copyparty and can be referenced below:
|
||||||
|
# 'ap' = absolute filesystem path where the message was posted
|
||||||
|
# 'vp' = virtual path (URL 'path') where the message was posted
|
||||||
|
# 'mt' = 'at' = unix-timestamp when the message was posted
|
||||||
|
# 'datetime' = ISO-8601 time when the message was posted
|
||||||
|
# 'sz' = message size in bytes
|
||||||
|
# 'host' = the server hostname which the user was accessing (URL 'host')
|
||||||
|
# 'user' = username (if logged in), otherwise '*'
|
||||||
|
# 'txt' = the message text itself
|
||||||
|
# (uncomment the print(msg_info) to see if additional information has been introduced by copyparty since this was written)
|
||||||
|
TEMPLATE = """
|
||||||
|
🕒 %(datetime)s, 👤 %(user)s @ %(ip)s
|
||||||
|
%(txt)s
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def write_ascending(filepath, msg_text):
|
||||||
|
with open(filepath, "a", encoding="utf-8", errors="replace") as outfile:
|
||||||
|
outfile.write(msg_text)
|
||||||
|
|
||||||
|
|
||||||
|
def write_descending(filepath, msg_text):
|
||||||
|
lockpath = filepath + ".lock"
|
||||||
|
got_it = False
|
||||||
|
for _ in range(16):
|
||||||
|
try:
|
||||||
|
os.mkdir(lockpath)
|
||||||
|
got_it = True
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
time.sleep(0.1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not got_it:
|
||||||
|
return sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
oldpath = filepath + ".old"
|
||||||
|
os.rename(filepath, oldpath)
|
||||||
|
with open(oldpath, "r", encoding="utf-8", errors="replace") as infile, open(
|
||||||
|
filepath, "w", encoding="utf-8", errors="replace"
|
||||||
|
) as outfile:
|
||||||
|
outfile.write(msg_text)
|
||||||
|
while True:
|
||||||
|
buf = infile.read(4096)
|
||||||
|
if not buf:
|
||||||
|
break
|
||||||
|
outfile.write(buf)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.unlink(oldpath)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
os.rmdir(lockpath)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
if argv is None:
|
||||||
|
argv = sys.argv
|
||||||
|
|
||||||
|
msg_info = json.loads(sys.argv[1])
|
||||||
|
# print(msg_info)
|
||||||
|
|
||||||
|
try:
|
||||||
|
dt = datetime.fromtimestamp(msg_info["at"], timezone.utc)
|
||||||
|
except:
|
||||||
|
dt = datetime.utcfromtimestamp(msg_info["at"])
|
||||||
|
|
||||||
|
msg_info["datetime"] = dt.strftime("%Y-%m-%d, %H:%M:%S")
|
||||||
|
|
||||||
|
msg_text = TEMPLATE % msg_info
|
||||||
|
|
||||||
|
filepath = os.path.join(msg_info["ap"], FILENAME)
|
||||||
|
|
||||||
|
if DESCENDING and os.path.exists(filepath):
|
||||||
|
write_descending(filepath, msg_text)
|
||||||
|
else:
|
||||||
|
write_ascending(filepath, msg_text)
|
||||||
|
|
||||||
|
print(msg_text)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -4,7 +4,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from plyer import notification
|
from plyer import notification
|
||||||
|
|
||||||
|
|
||||||
@@ -43,7 +43,8 @@ def main():
|
|||||||
fp = inf["ap"]
|
fp = inf["ap"]
|
||||||
sz = humansize(inf["sz"])
|
sz = humansize(inf["sz"])
|
||||||
dp, fn = os.path.split(fp)
|
dp, fn = os.path.split(fp)
|
||||||
mt = datetime.utcfromtimestamp(inf["mt"]).strftime("%Y-%m-%d %H:%M:%S")
|
dt = datetime.fromtimestamp(inf["mt"], timezone.utc)
|
||||||
|
mt = dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
msg = f"{fn} ({sz})\n📁 {dp}"
|
msg = f"{fn} ({sz})\n📁 {dp}"
|
||||||
title = "File received"
|
title = "File received"
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ def main():
|
|||||||
if "://" not in url:
|
if "://" not in url:
|
||||||
url = "https://" + url
|
url = "https://" + url
|
||||||
|
|
||||||
|
proto = url.split("://")[0].lower()
|
||||||
|
if proto not in ("http", "https", "ftp", "ftps"):
|
||||||
|
raise Exception("bad proto {}".format(proto))
|
||||||
|
|
||||||
os.chdir(inf["ap"])
|
os.chdir(inf["ap"])
|
||||||
|
|
||||||
name = url.split("?")[0].split("/")[-1]
|
name = url.split("?")[0].split("/")[-1]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
_ = r"""
|
_ = r"""
|
||||||
@@ -43,8 +43,11 @@ except:
|
|||||||
return p
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
UTC = timezone.utc
|
||||||
|
|
||||||
|
|
||||||
def humantime(ts):
|
def humantime(ts):
|
||||||
return datetime.utcfromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
|
return datetime.fromtimestamp(ts, UTC).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
def find_files_root(inf):
|
def find_files_root(inf):
|
||||||
@@ -96,7 +99,7 @@ def main():
|
|||||||
|
|
||||||
ret.append("# {} files, {} bytes total".format(len(inf), total_sz))
|
ret.append("# {} files, {} bytes total".format(len(inf), total_sz))
|
||||||
ret.append("")
|
ret.append("")
|
||||||
ftime = datetime.utcnow().strftime("%Y-%m%d-%H%M%S.%f")
|
ftime = datetime.now(UTC).strftime("%Y-%m%d-%H%M%S.%f")
|
||||||
fp = "{}xfer-{}.sha512".format(inf[0]["ap"][:di], ftime)
|
fp = "{}xfer-{}.sha512".format(inf[0]["ap"][:di], ftime)
|
||||||
with open(fsenc(fp), "wb") as f:
|
with open(fsenc(fp), "wb") as f:
|
||||||
f.write("\n".join(ret).encode("utf-8", "replace"))
|
f.write("\n".join(ret).encode("utf-8", "replace"))
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ these do not have any problematic dependencies at all:
|
|||||||
* also available as an [event hook](../hooks/wget.py)
|
* also available as an [event hook](../hooks/wget.py)
|
||||||
|
|
||||||
|
|
||||||
|
## dangerous plugins
|
||||||
|
|
||||||
|
plugins in this section should only be used with appropriate precautions:
|
||||||
|
|
||||||
|
* [very-bad-idea.py](./very-bad-idea.py) combined with [meadup.js](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js) converts copyparty into a janky yet extremely flexible chromecast clone
|
||||||
|
* also adds a virtual keyboard by @steinuil to the basic-upload tab for comfy couch crowd control
|
||||||
|
* anything uploaded through the [android app](https://github.com/9001/party-up) (files or links) are executed on the server, meaning anyone can infect your PC with malware... so protect this with a password and keep it on a LAN!
|
||||||
|
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
|
|
||||||
run [`install-deps.sh`](install-deps.sh) to build/install most dependencies required by these programs (supports windows/linux/macos)
|
run [`install-deps.sh`](install-deps.sh) to build/install most dependencies required by these programs (supports windows/linux/macos)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ set -e
|
|||||||
# linux/alpine: requires gcc g++ make cmake patchelf {python3,ffmpeg,fftw,libsndfile}-dev py3-{wheel,pip} py3-numpy{,-dev}
|
# linux/alpine: requires gcc g++ make cmake patchelf {python3,ffmpeg,fftw,libsndfile}-dev py3-{wheel,pip} py3-numpy{,-dev}
|
||||||
# linux/debian: requires libav{codec,device,filter,format,resample,util}-dev {libfftw3,python3,libsndfile1}-dev python3-{numpy,pip} vamp-{plugin-sdk,examples} patchelf cmake
|
# linux/debian: requires libav{codec,device,filter,format,resample,util}-dev {libfftw3,python3,libsndfile1}-dev python3-{numpy,pip} vamp-{plugin-sdk,examples} patchelf cmake
|
||||||
# linux/fedora: requires gcc gcc-c++ make cmake patchelf {python3,ffmpeg,fftw,libsndfile}-devel python3-numpy vamp-plugin-sdk qm-vamp-plugins
|
# linux/fedora: requires gcc gcc-c++ make cmake patchelf {python3,ffmpeg,fftw,libsndfile}-devel python3-numpy vamp-plugin-sdk qm-vamp-plugins
|
||||||
|
# linux/arch: requires gcc make cmake patchelf python3 ffmpeg fftw libsndfile python-{numpy,wheel,pip,setuptools}
|
||||||
# win64: requires msys2-mingw64 environment
|
# win64: requires msys2-mingw64 environment
|
||||||
# macos: requires macports
|
# macos: requires macports
|
||||||
#
|
#
|
||||||
@@ -227,15 +228,16 @@ install_vamp() {
|
|||||||
cd "$td"
|
cd "$td"
|
||||||
echo '#include <vamp-sdk/Plugin.h>' | g++ -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'
|
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)
|
(dl_files yolo https://code.soundsoftware.ac.uk/attachments/download/2691/vamp-plugin-sdk-2.10.0.tar.gz)
|
||||||
sha512sum -c <(
|
sha512sum -c <(
|
||||||
echo "7ef7f837d19a08048b059e0da408373a7964ced452b290fae40b85d6d70ca9000bcfb3302cd0b4dc76cf2a848528456f78c1ce1ee0c402228d812bd347b6983b -"
|
echo "153b7f2fa01b77c65ad393ca0689742d66421017fd5931d216caa0fcf6909355fff74706fabbc062a3a04588a619c9b515a1dae00f21a57afd97902a355c48ed -"
|
||||||
) <vamp-plugin-sdk-2.9.0.tar.gz
|
) <vamp-plugin-sdk-2.10.0.tar.gz
|
||||||
tar -xf vamp-plugin-sdk-2.9.0.tar.gz
|
tar -xf vamp-plugin-sdk-2.10.0.tar.gz
|
||||||
rm -- *.tar.gz
|
rm -- *.tar.gz
|
||||||
ls -al
|
ls -al
|
||||||
cd vamp-plugin-sdk-*
|
cd vamp-plugin-sdk-*
|
||||||
./configure --prefix=$HOME/pe/vamp-sdk
|
printf '%s\n' "int main(int argc, char **argv) { return 0; }" > host/vamp-simple-host.cpp
|
||||||
|
./configure --disable-programs --prefix=$HOME/pe/vamp-sdk
|
||||||
make -j1 install
|
make -j1 install
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,8 +252,9 @@ install_vamp() {
|
|||||||
rm -- *.tar.gz
|
rm -- *.tar.gz
|
||||||
cd beatroot-vamp-v1.0
|
cd beatroot-vamp-v1.0
|
||||||
[ -e ~/pe/vamp-sdk ] &&
|
[ -e ~/pe/vamp-sdk ] &&
|
||||||
sed -ri 's`^(CFLAGS :=.*)`\1 -I'$HOME'/pe/vamp-sdk/include`' Makefile.linux
|
sed -ri 's`^(CFLAGS :=.*)`\1 -I'$HOME'/pe/vamp-sdk/include`' Makefile.linux ||
|
||||||
make -f Makefile.linux -j4 LDFLAGS=-L$HOME/pe/vamp-sdk/lib
|
sed -ri 's`^(CFLAGS :=.*)`\1 -I/usr/include/vamp-sdk`' Makefile.linux
|
||||||
|
make -f Makefile.linux -j4 LDFLAGS="-L$HOME/pe/vamp-sdk/lib -L/usr/lib64"
|
||||||
# /home/ed/vamp /home/ed/.vamp /usr/local/lib/vamp
|
# /home/ed/vamp /home/ed/.vamp /usr/local/lib/vamp
|
||||||
mkdir ~/vamp
|
mkdir ~/vamp
|
||||||
cp -pv beatroot-vamp.* ~/vamp/
|
cp -pv beatroot-vamp.* ~/vamp/
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
WARNING -- DANGEROUS PLUGIN --
|
||||||
|
if someone is able to upload files to a copyparty which is
|
||||||
|
running this plugin, they can execute malware on your machine
|
||||||
|
so please keep this on a LAN and protect it with a password
|
||||||
|
|
||||||
use copyparty as a chromecast replacement:
|
use copyparty as a chromecast replacement:
|
||||||
* post a URL and it will open in the default browser
|
* post a URL and it will open in the default browser
|
||||||
* upload a file and it will open in the default application
|
* upload a file and it will open in the default application
|
||||||
@@ -10,16 +15,17 @@ use copyparty as a chromecast replacement:
|
|||||||
|
|
||||||
the android app makes it a breeze to post pics and links:
|
the android app makes it a breeze to post pics and links:
|
||||||
https://github.com/9001/party-up/releases
|
https://github.com/9001/party-up/releases
|
||||||
(iOS devices have to rely on the web-UI)
|
|
||||||
|
|
||||||
goes without saying, but this is HELLA DANGEROUS,
|
iOS devices can use the web-UI or the shortcut instead:
|
||||||
GIVES RCE TO ANYONE WHO HAVE UPLOAD PERMISSIONS
|
https://github.com/9001/copyparty#ios-shortcuts
|
||||||
|
|
||||||
example copyparty config to use this:
|
example copyparty config to use this;
|
||||||
--urlform save,get -v.::w:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,c0,bin/mtag/very-bad-idea.py
|
lets the user "kevin" with password "hunter2" use this plugin:
|
||||||
|
-a kevin:hunter2 --urlform save,get -v.::w,kevin:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,c0,bin/mtag/very-bad-idea.py
|
||||||
|
|
||||||
recommended deps:
|
recommended deps:
|
||||||
apt install xdotool libnotify-bin
|
apt install xdotool libnotify-bin mpv
|
||||||
|
python3 -m pip install --user -U streamlink yt-dlp
|
||||||
https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js
|
https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js
|
||||||
|
|
||||||
and you probably want `twitter-unmute.user.js` from the res folder
|
and you probably want `twitter-unmute.user.js` from the res folder
|
||||||
@@ -63,8 +69,10 @@ set -e
|
|||||||
EOF
|
EOF
|
||||||
chmod 755 /usr/local/bin/chromium-browser
|
chmod 755 /usr/local/bin/chromium-browser
|
||||||
|
|
||||||
# start the server (note: replace `-v.::rw:` with `-v.::w:` to disallow retrieving uploaded stuff)
|
# start the server
|
||||||
cd ~/Downloads; python3 copyparty-sfx.py --urlform save,get -v.::rw:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,very-bad-idea.py
|
# note 1: replace hunter2 with a better password to access the server
|
||||||
|
# note 2: replace `-v.::rw` with `-v.::w` to disallow retrieving uploaded stuff
|
||||||
|
cd ~/Downloads; python3 copyparty-sfx.py -a kevin:hunter2 --urlform save,get -v.::rw,kevin:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,very-bad-idea.py
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -72,11 +80,23 @@ cd ~/Downloads; python3 copyparty-sfx.py --urlform save,get -v.::rw:c,e2d,e2t,mt
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import shutil
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
from urllib.parse import unquote_to_bytes as unquote
|
from urllib.parse import unquote_to_bytes as unquote
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
have_mpv = shutil.which("mpv")
|
||||||
|
have_vlc = shutil.which("vlc")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
if len(sys.argv) > 2 and sys.argv[1] == "x":
|
||||||
|
# invoked on commandline for testing;
|
||||||
|
# python3 very-bad-idea.py x msg=https://youtu.be/dQw4w9WgXcQ
|
||||||
|
txt = " ".join(sys.argv[2:])
|
||||||
|
txt = quote(txt.replace(" ", "+"))
|
||||||
|
return open_post(txt.encode("utf-8"))
|
||||||
|
|
||||||
fp = os.path.abspath(sys.argv[1])
|
fp = os.path.abspath(sys.argv[1])
|
||||||
with open(fp, "rb") as f:
|
with open(fp, "rb") as f:
|
||||||
txt = f.read(4096)
|
txt = f.read(4096)
|
||||||
@@ -92,7 +112,7 @@ def open_post(txt):
|
|||||||
try:
|
try:
|
||||||
k, v = txt.split(" ", 1)
|
k, v = txt.split(" ", 1)
|
||||||
except:
|
except:
|
||||||
open_url(txt)
|
return open_url(txt)
|
||||||
|
|
||||||
if k == "key":
|
if k == "key":
|
||||||
sp.call(["xdotool", "key"] + v.split(" "))
|
sp.call(["xdotool", "key"] + v.split(" "))
|
||||||
@@ -128,6 +148,17 @@ def open_url(txt):
|
|||||||
# else:
|
# else:
|
||||||
# sp.call(["xdotool", "getactivewindow", "windowminimize"]) # minimizes the focused windo
|
# sp.call(["xdotool", "getactivewindow", "windowminimize"]) # minimizes the focused windo
|
||||||
|
|
||||||
|
# mpv is probably smart enough to use streamlink automatically
|
||||||
|
if try_mpv(txt):
|
||||||
|
print("mpv got it")
|
||||||
|
return
|
||||||
|
|
||||||
|
# or maybe streamlink would be a good choice to open this
|
||||||
|
if try_streamlink(txt):
|
||||||
|
print("streamlink got it")
|
||||||
|
return
|
||||||
|
|
||||||
|
# nope,
|
||||||
# close any error messages:
|
# close any error messages:
|
||||||
sp.call(["xdotool", "search", "--name", "Error", "windowclose"])
|
sp.call(["xdotool", "search", "--name", "Error", "windowclose"])
|
||||||
# sp.call(["xdotool", "key", "ctrl+alt+d"]) # doesnt work at all
|
# sp.call(["xdotool", "key", "ctrl+alt+d"]) # doesnt work at all
|
||||||
@@ -136,4 +167,39 @@ def open_url(txt):
|
|||||||
sp.call(["xdg-open", txt])
|
sp.call(["xdg-open", txt])
|
||||||
|
|
||||||
|
|
||||||
|
def try_mpv(url):
|
||||||
|
t0 = time.time()
|
||||||
|
try:
|
||||||
|
print("trying mpv...")
|
||||||
|
sp.check_call(["mpv", "--fs", url])
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
# if it ran for 15 sec it probably succeeded and terminated
|
||||||
|
t = time.time()
|
||||||
|
return t - t0 > 15
|
||||||
|
|
||||||
|
|
||||||
|
def try_streamlink(url):
|
||||||
|
t0 = time.time()
|
||||||
|
try:
|
||||||
|
import streamlink
|
||||||
|
|
||||||
|
print("trying streamlink...")
|
||||||
|
streamlink.Streamlink().resolve_url(url)
|
||||||
|
|
||||||
|
if have_mpv:
|
||||||
|
args = "-m streamlink -p mpv -a --fs"
|
||||||
|
else:
|
||||||
|
args = "-m streamlink"
|
||||||
|
|
||||||
|
cmd = [sys.executable] + args.split() + [url, "best"]
|
||||||
|
t0 = time.time()
|
||||||
|
sp.check_call(cmd)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
# if it ran for 10 sec it probably succeeded and terminated
|
||||||
|
t = time.time()
|
||||||
|
return t - t0 > 10
|
||||||
|
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ def main():
|
|||||||
if "://" not in url:
|
if "://" not in url:
|
||||||
url = "https://" + url
|
url = "https://" + url
|
||||||
|
|
||||||
|
proto = url.split("://")[0].lower()
|
||||||
|
if proto not in ("http", "https", "ftp", "ftps"):
|
||||||
|
raise Exception("bad proto {}".format(proto))
|
||||||
|
|
||||||
os.chdir(fdir)
|
os.chdir(fdir)
|
||||||
|
|
||||||
name = url.split("?")[0].split("/")[-1]
|
name = url.split("?")[0].split("/")[-1]
|
||||||
|
|||||||
@@ -46,13 +46,20 @@ import traceback
|
|||||||
import http.client # py2: httplib
|
import http.client # py2: httplib
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import calendar
|
import calendar
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from urllib.parse import quote_from_bytes as quote
|
from urllib.parse import quote_from_bytes as quote
|
||||||
from urllib.parse import unquote_to_bytes as unquote
|
from urllib.parse import unquote_to_bytes as unquote
|
||||||
|
|
||||||
WINDOWS = sys.platform == "win32"
|
WINDOWS = sys.platform == "win32"
|
||||||
MACOS = platform.system() == "Darwin"
|
MACOS = platform.system() == "Darwin"
|
||||||
info = log = dbg = None
|
UTC = timezone.utc
|
||||||
|
|
||||||
|
|
||||||
|
def print(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
builtins.print(*list(args), **kwargs)
|
||||||
|
except:
|
||||||
|
builtins.print(termsafe(" ".join(str(x) for x in args)), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
print(
|
print(
|
||||||
@@ -64,6 +71,13 @@ print(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def null_log(msg):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
info = log = dbg = null_log
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from fuse import FUSE, FuseOSError, Operations
|
from fuse import FUSE, FuseOSError, Operations
|
||||||
except:
|
except:
|
||||||
@@ -83,13 +97,6 @@ except:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def print(*args, **kwargs):
|
|
||||||
try:
|
|
||||||
builtins.print(*list(args), **kwargs)
|
|
||||||
except:
|
|
||||||
builtins.print(termsafe(" ".join(str(x) for x in args)), **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def termsafe(txt):
|
def termsafe(txt):
|
||||||
try:
|
try:
|
||||||
return txt.encode(sys.stdout.encoding, "backslashreplace").decode(
|
return txt.encode(sys.stdout.encoding, "backslashreplace").decode(
|
||||||
@@ -118,10 +125,6 @@ def fancy_log(msg):
|
|||||||
print("{:10.6f} {} {}\n".format(time.time() % 900, rice_tid(), msg), end="")
|
print("{:10.6f} {} {}\n".format(time.time() % 900, rice_tid(), msg), end="")
|
||||||
|
|
||||||
|
|
||||||
def null_log(msg):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def hexler(binary):
|
def hexler(binary):
|
||||||
return binary.replace("\r", "\\r").replace("\n", "\\n")
|
return binary.replace("\r", "\\r").replace("\n", "\\n")
|
||||||
return " ".join(["{}\033[36m{:02x}\033[0m".format(b, ord(b)) for b in binary])
|
return " ".join(["{}\033[36m{:02x}\033[0m".format(b, ord(b)) for b in binary])
|
||||||
@@ -176,7 +179,7 @@ class RecentLog(object):
|
|||||||
def put(self, msg):
|
def put(self, msg):
|
||||||
msg = "{:10.6f} {} {}\n".format(time.time() % 900, rice_tid(), msg)
|
msg = "{:10.6f} {} {}\n".format(time.time() % 900, rice_tid(), msg)
|
||||||
if self.f:
|
if self.f:
|
||||||
fmsg = " ".join([datetime.utcnow().strftime("%H%M%S.%f"), str(msg)])
|
fmsg = " ".join([datetime.now(UTC).strftime("%H%M%S.%f"), str(msg)])
|
||||||
self.f.write(fmsg.encode("utf-8"))
|
self.f.write(fmsg.encode("utf-8"))
|
||||||
|
|
||||||
with self.mtx:
|
with self.mtx:
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ import sys
|
|||||||
import base64
|
import base64
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import argparse
|
import argparse
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from urllib.parse import quote_from_bytes as quote
|
from urllib.parse import quote_from_bytes as quote
|
||||||
from urllib.parse import unquote_to_bytes as unquote
|
from urllib.parse import unquote_to_bytes as unquote
|
||||||
|
|
||||||
|
|
||||||
FS_ENCODING = sys.getfilesystemencoding()
|
FS_ENCODING = sys.getfilesystemencoding()
|
||||||
|
UTC = timezone.utc
|
||||||
|
|
||||||
|
|
||||||
class APF(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):
|
class APF(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):
|
||||||
@@ -155,11 +156,10 @@ th {
|
|||||||
link = txt.decode("utf-8")[4:]
|
link = txt.decode("utf-8")[4:]
|
||||||
|
|
||||||
sz = "{:,}".format(sz)
|
sz = "{:,}".format(sz)
|
||||||
|
dt = datetime.fromtimestamp(at if at > 0 else mt, UTC)
|
||||||
v = [
|
v = [
|
||||||
w[:16],
|
w[:16],
|
||||||
datetime.utcfromtimestamp(at if at > 0 else mt).strftime(
|
dt.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"%Y-%m-%d %H:%M:%S"
|
|
||||||
),
|
|
||||||
sz,
|
sz,
|
||||||
imap.get(ip, ip),
|
imap.get(ip, ip),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ done
|
|||||||
help() { cat <<'EOF'
|
help() { cat <<'EOF'
|
||||||
|
|
||||||
usage:
|
usage:
|
||||||
./prisonparty.sh <ROOTDIR> <UID> <GID> [VOLDIR [VOLDIR...]] -- python3 copyparty-sfx.py [...]
|
./prisonparty.sh <ROOTDIR> <USER|UID> <GROUP|GID> [VOLDIR [VOLDIR...]] -- python3 copyparty-sfx.py [...]
|
||||||
|
|
||||||
example:
|
example:
|
||||||
./prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt/nas/music -- python3 copyparty-sfx.py -v /mnt/nas/music::rwmd
|
./prisonparty.sh /var/lib/copyparty-jail cpp cpp /mnt/nas/music -- python3 copyparty-sfx.py -v /mnt/nas/music::rwmd
|
||||||
|
|
||||||
example for running straight from source (instead of using an sfx):
|
example for running straight from source (instead of using an sfx):
|
||||||
PYTHONPATH=$PWD ./prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt/nas/music -- python3 -um copyparty -v /mnt/nas/music::rwmd
|
PYTHONPATH=$PWD ./prisonparty.sh /var/lib/copyparty-jail cpp cpp /mnt/nas/music -- python3 -um copyparty -v /mnt/nas/music::rwmd
|
||||||
|
|
||||||
note that if you have python modules installed as --user (such as bpm/key detectors),
|
note that if you have python modules installed as --user (such as bpm/key detectors),
|
||||||
you should add /home/foo/.local as a VOLDIR
|
you should add /home/foo/.local as a VOLDIR
|
||||||
@@ -28,6 +28,16 @@ exit 1
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
errs=
|
||||||
|
for c in awk chroot dirname getent lsof mknod mount realpath sed sort stat uniq; do
|
||||||
|
command -v $c >/dev/null || {
|
||||||
|
echo ERROR: command not found: $c
|
||||||
|
errs=1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
[ $errs ] && exit 1
|
||||||
|
|
||||||
|
|
||||||
# read arguments
|
# read arguments
|
||||||
trap help EXIT
|
trap help EXIT
|
||||||
jail="$(realpath "$1")"; shift
|
jail="$(realpath "$1")"; shift
|
||||||
@@ -58,11 +68,18 @@ cpp="$1"; shift
|
|||||||
}
|
}
|
||||||
trap - EXIT
|
trap - EXIT
|
||||||
|
|
||||||
|
usr="$(getent passwd $uid | cut -d: -f1)"
|
||||||
|
[ "$usr" ] || { echo "ERROR invalid username/uid $uid"; exit 1; }
|
||||||
|
uid="$(getent passwd $uid | cut -d: -f3)"
|
||||||
|
|
||||||
|
grp="$(getent group $gid | cut -d: -f1)"
|
||||||
|
[ "$grp" ] || { echo "ERROR invalid groupname/gid $gid"; exit 1; }
|
||||||
|
gid="$(getent group $gid | cut -d: -f3)"
|
||||||
|
|
||||||
# debug/vis
|
# debug/vis
|
||||||
echo
|
echo
|
||||||
echo "chroot-dir = $jail"
|
echo "chroot-dir = $jail"
|
||||||
echo "user:group = $uid:$gid"
|
echo "user:group = $uid:$gid ($usr:$grp)"
|
||||||
echo " copyparty = $cpp"
|
echo " copyparty = $cpp"
|
||||||
echo
|
echo
|
||||||
printf '\033[33m%s\033[0m\n' "copyparty can access these folders and all their subdirectories:"
|
printf '\033[33m%s\033[0m\n' "copyparty can access these folders and all their subdirectories:"
|
||||||
@@ -80,34 +97,39 @@ jail="${jail%/}"
|
|||||||
|
|
||||||
|
|
||||||
# bind-mount system directories and volumes
|
# bind-mount system directories and volumes
|
||||||
|
for a in {1..30}; do mkdir "$jail/.prisonlock" && break; sleep 0.1; done
|
||||||
printf '%s\n' "${sysdirs[@]}" "${vols[@]}" | sed -r 's`/$``' | LC_ALL=C sort | uniq |
|
printf '%s\n' "${sysdirs[@]}" "${vols[@]}" | sed -r 's`/$``' | LC_ALL=C sort | uniq |
|
||||||
while IFS= read -r v; do
|
while IFS= read -r v; do
|
||||||
[ -e "$v" ] || {
|
[ -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
|
continue
|
||||||
}
|
}
|
||||||
i1=$(stat -c%D.%i "$v" 2>/dev/null || echo a)
|
i1=$(stat -c%D.%i "$v/" 2>/dev/null || echo a)
|
||||||
i2=$(stat -c%D.%i "$jail$v" 2>/dev/null || echo b)
|
i2=$(stat -c%D.%i "$jail$v/" 2>/dev/null || echo b)
|
||||||
# echo "v [$v] i1 [$i1] i2 [$i2]"
|
|
||||||
[ $i1 = $i2 ] && continue
|
[ $i1 = $i2 ] && continue
|
||||||
|
mount | grep -qF " $jail$v " && echo wtf $i1 $i2 $v && continue
|
||||||
mkdir -p "$jail$v"
|
mkdir -p "$jail$v"
|
||||||
mount --bind "$v" "$jail$v"
|
mount --bind "$v" "$jail$v"
|
||||||
done
|
done
|
||||||
|
rmdir "$jail/.prisonlock" || true
|
||||||
|
|
||||||
|
|
||||||
cln() {
|
cln() {
|
||||||
rv=$?
|
trap - EXIT
|
||||||
wait -f -p rv $p || true
|
wait -f -n $p && rv=0 || rv=$?
|
||||||
cd /
|
cd /
|
||||||
echo "stopping chroot..."
|
echo "stopping chroot..."
|
||||||
lsof "$jail" | grep -F "$jail" &&
|
for a in {1..30}; do mkdir "$jail/.prisonlock" && break; sleep 0.1; done
|
||||||
|
lsof "$jail" 2>/dev/null | grep -F "$jail" &&
|
||||||
echo "chroot is in use; will not unmount" ||
|
echo "chroot is in use; will not unmount" ||
|
||||||
{
|
{
|
||||||
mount | grep -F " on $jail" |
|
mount | grep -F " on $jail" |
|
||||||
awk '{sub(/ type .*/,"");sub(/.* on /,"");print}' |
|
awk '{sub(/ type .*/,"");sub(/.* on /,"");print}' |
|
||||||
LC_ALL=C sort -r | tee /dev/stderr | tr '\n' '\0' | xargs -r0 umount
|
LC_ALL=C sort -r | while IFS= read -r v; do
|
||||||
|
umount "$v" && echo "umount OK: $v"
|
||||||
|
done
|
||||||
}
|
}
|
||||||
|
rmdir "$jail/.prisonlock" || true
|
||||||
exit $rv
|
exit $rv
|
||||||
}
|
}
|
||||||
trap cln EXIT
|
trap cln EXIT
|
||||||
@@ -128,8 +150,8 @@ chmod 777 "$jail/tmp"
|
|||||||
|
|
||||||
|
|
||||||
# run copyparty
|
# run copyparty
|
||||||
export HOME=$(getent passwd $uid | cut -d: -f6)
|
export HOME="$(getent passwd $uid | cut -d: -f6)"
|
||||||
export USER=$(getent passwd $uid | cut -d: -f1)
|
export USER="$usr"
|
||||||
export LOGNAME="$USER"
|
export LOGNAME="$USER"
|
||||||
#echo "pybin [$pybin]"
|
#echo "pybin [$pybin]"
|
||||||
#echo "pyarg [$pyarg]"
|
#echo "pyarg [$pyarg]"
|
||||||
@@ -137,5 +159,5 @@ export LOGNAME="$USER"
|
|||||||
chroot --userspec=$uid:$gid "$jail" "$pybin" $pyarg "$cpp" "$@" &
|
chroot --userspec=$uid:$gid "$jail" "$pybin" $pyarg "$cpp" "$@" &
|
||||||
p=$!
|
p=$!
|
||||||
trap 'kill -USR1 $p' USR1
|
trap 'kill -USR1 $p' USR1
|
||||||
trap 'kill $p' INT TERM
|
trap 'trap - INT TERM; kill $p' INT TERM
|
||||||
wait
|
wait
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
S_VERSION = "1.6"
|
S_VERSION = "1.14"
|
||||||
S_BUILD_DT = "2023-04-20"
|
S_BUILD_DT = "2024-01-27"
|
||||||
|
|
||||||
"""
|
"""
|
||||||
up2k.py: upload to copyparty
|
u2c.py: upload to copyparty
|
||||||
2021, ed <irc.rizon.net>, MIT-Licensed
|
2021, ed <irc.rizon.net>, MIT-Licensed
|
||||||
https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py
|
https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py
|
||||||
|
|
||||||
- dependencies: requests
|
- dependencies: requests
|
||||||
- supports python 2.6, 2.7, and 3.3 through 3.12
|
- supports python 2.6, 2.7, and 3.3 through 3.12
|
||||||
- if something breaks just try again and it'll autoresume
|
- if something breaks just try again and it'll autoresume
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import stat
|
import stat
|
||||||
@@ -21,6 +22,7 @@ import math
|
|||||||
import time
|
import time
|
||||||
import atexit
|
import atexit
|
||||||
import signal
|
import signal
|
||||||
|
import socket
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import platform
|
import platform
|
||||||
@@ -38,7 +40,7 @@ except:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import requests
|
import requests
|
||||||
except ImportError:
|
except ImportError as ex:
|
||||||
if EXE:
|
if EXE:
|
||||||
raise
|
raise
|
||||||
elif sys.version_info > (2, 7):
|
elif sys.version_info > (2, 7):
|
||||||
@@ -49,7 +51,7 @@ except ImportError:
|
|||||||
m = "\n ERROR: need these:\n" + "\n".join(m) + "\n"
|
m = "\n ERROR: need these:\n" + "\n".join(m) + "\n"
|
||||||
m += "\n for f in *.whl; do unzip $f; done; rm -r *.dist-info\n"
|
m += "\n for f in *.whl; do unzip $f; done; rm -r *.dist-info\n"
|
||||||
|
|
||||||
print(m.format(sys.executable))
|
print(m.format(sys.executable), "\nspecifically,", ex)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -58,6 +60,7 @@ PY2 = sys.version_info < (3,)
|
|||||||
if PY2:
|
if PY2:
|
||||||
from Queue import Queue
|
from Queue import Queue
|
||||||
from urllib import quote, unquote
|
from urllib import quote, unquote
|
||||||
|
from urlparse import urlsplit, urlunsplit
|
||||||
|
|
||||||
sys.dont_write_bytecode = True
|
sys.dont_write_bytecode = True
|
||||||
bytes = str
|
bytes = str
|
||||||
@@ -65,6 +68,7 @@ else:
|
|||||||
from queue import Queue
|
from queue import Queue
|
||||||
from urllib.parse import unquote_to_bytes as unquote
|
from urllib.parse import unquote_to_bytes as unquote
|
||||||
from urllib.parse import quote_from_bytes as quote
|
from urllib.parse import quote_from_bytes as quote
|
||||||
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
|
|
||||||
unicode = str
|
unicode = str
|
||||||
|
|
||||||
@@ -101,12 +105,14 @@ class File(object):
|
|||||||
# set by handshake
|
# set by handshake
|
||||||
self.recheck = False # duplicate; redo handshake after all files done
|
self.recheck = False # duplicate; redo handshake after all files done
|
||||||
self.ucids = [] # type: list[str] # chunks which need to be uploaded
|
self.ucids = [] # type: list[str] # chunks which need to be uploaded
|
||||||
self.wark = None # type: str
|
self.wark = "" # type: str
|
||||||
self.url = None # type: str
|
self.url = "" # type: str
|
||||||
|
self.nhs = 0
|
||||||
|
|
||||||
# set by upload
|
# set by upload
|
||||||
self.up_b = 0 # type: int
|
self.up_b = 0 # type: int
|
||||||
self.up_c = 0 # type: int
|
self.up_c = 0 # type: int
|
||||||
|
self.cd = 0
|
||||||
|
|
||||||
# t = "size({}) lmod({}) top({}) rel({}) abs({}) name({})\n"
|
# t = "size({}) lmod({}) top({}) rel({}) abs({}) name({})\n"
|
||||||
# eprint(t.format(self.size, self.lmod, self.top, self.rel, self.abs, self.name))
|
# eprint(t.format(self.size, self.lmod, self.top, self.rel, self.abs, self.name))
|
||||||
@@ -217,6 +223,7 @@ class MTHash(object):
|
|||||||
|
|
||||||
def hash_at(self, nch):
|
def hash_at(self, nch):
|
||||||
f = self.f
|
f = self.f
|
||||||
|
assert f
|
||||||
ofs = ofs0 = nch * self.csz
|
ofs = ofs0 = nch * self.csz
|
||||||
hashobj = hashlib.sha512()
|
hashobj = hashlib.sha512()
|
||||||
chunk_sz = chunk_rem = min(self.csz, self.sz - ofs)
|
chunk_sz = chunk_rem = min(self.csz, self.sz - ofs)
|
||||||
@@ -337,6 +344,32 @@ class CTermsize(object):
|
|||||||
ss = CTermsize()
|
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):
|
def _scd(err, top):
|
||||||
"""non-recursive listing of directory contents, along with stat() info"""
|
"""non-recursive listing of directory contents, along with stat() info"""
|
||||||
with os.scandir(top) as dh:
|
with os.scandir(top) as dh:
|
||||||
@@ -382,10 +415,11 @@ def walkdir(err, top, seen):
|
|||||||
err.append((ap, str(ex)))
|
err.append((ap, str(ex)))
|
||||||
|
|
||||||
|
|
||||||
def walkdirs(err, tops):
|
def walkdirs(err, tops, excl):
|
||||||
"""recursive statdir for a list of tops, yields [top, relpath, stat]"""
|
"""recursive statdir for a list of tops, yields [top, relpath, stat]"""
|
||||||
sep = "{0}".format(os.sep).encode("ascii")
|
sep = "{0}".format(os.sep).encode("ascii")
|
||||||
if not VT100:
|
if not VT100:
|
||||||
|
excl = excl.replace("/", r"\\")
|
||||||
za = []
|
za = []
|
||||||
for td in tops:
|
for td in tops:
|
||||||
try:
|
try:
|
||||||
@@ -402,6 +436,8 @@ def walkdirs(err, tops):
|
|||||||
za = [x.replace(b"/", b"\\") for x in za]
|
za = [x.replace(b"/", b"\\") for x in za]
|
||||||
tops = za
|
tops = za
|
||||||
|
|
||||||
|
ptn = re.compile(excl.encode("utf-8") or b"\n", re.I)
|
||||||
|
|
||||||
for top in tops:
|
for top in tops:
|
||||||
isdir = os.path.isdir(top)
|
isdir = os.path.isdir(top)
|
||||||
if top[-1:] == sep:
|
if top[-1:] == sep:
|
||||||
@@ -414,6 +450,8 @@ def walkdirs(err, tops):
|
|||||||
|
|
||||||
if isdir:
|
if isdir:
|
||||||
for ap, inf in walkdir(err, top, []):
|
for ap, inf in walkdir(err, top, []):
|
||||||
|
if ptn.match(ap):
|
||||||
|
continue
|
||||||
yield stop, ap[len(stop) :].lstrip(sep), inf
|
yield stop, ap[len(stop) :].lstrip(sep), inf
|
||||||
else:
|
else:
|
||||||
d, n = top.rsplit(sep, 1)
|
d, n = top.rsplit(sep, 1)
|
||||||
@@ -426,7 +464,7 @@ def quotep(btxt):
|
|||||||
if not PY2:
|
if not PY2:
|
||||||
quot1 = quot1.encode("ascii")
|
quot1 = quot1.encode("ascii")
|
||||||
|
|
||||||
return quot1.replace(b" ", b"+")
|
return quot1.replace(b" ", b"+") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
# from copyparty/util.py
|
# from copyparty/util.py
|
||||||
@@ -463,7 +501,7 @@ def up2k_chunksize(filesize):
|
|||||||
|
|
||||||
# mostly from copyparty/up2k.py
|
# mostly from copyparty/up2k.py
|
||||||
def get_hashlist(file, pcb, mth):
|
def get_hashlist(file, pcb, mth):
|
||||||
# type: (File, any, any) -> None
|
# type: (File, Any, Any) -> None
|
||||||
"""generates the up2k hashlist from file contents, inserts it into `file`"""
|
"""generates the up2k hashlist from file contents, inserts it into `file`"""
|
||||||
|
|
||||||
chunk_sz = up2k_chunksize(file.size)
|
chunk_sz = up2k_chunksize(file.size)
|
||||||
@@ -522,8 +560,11 @@ def handshake(ar, file, search):
|
|||||||
}
|
}
|
||||||
if search:
|
if search:
|
||||||
req["srch"] = 1
|
req["srch"] = 1
|
||||||
elif ar.dr:
|
else:
|
||||||
req["replace"] = True
|
if ar.touch:
|
||||||
|
req["umod"] = True
|
||||||
|
if ar.dr:
|
||||||
|
req["replace"] = True
|
||||||
|
|
||||||
headers = {"Content-Type": "text/plain"} # <=1.5.1 compat
|
headers = {"Content-Type": "text/plain"} # <=1.5.1 compat
|
||||||
if pw:
|
if pw:
|
||||||
@@ -563,7 +604,7 @@ def handshake(ar, file, search):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
eprint("handshake failed, retrying: {0}\n {1}\n\n".format(file.name, em))
|
eprint("handshake failed, retrying: {0}\n {1}\n\n".format(file.name, em))
|
||||||
time.sleep(1)
|
time.sleep(ar.cd)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = r.json()
|
r = r.json()
|
||||||
@@ -625,7 +666,7 @@ class Ctl(object):
|
|||||||
nfiles = 0
|
nfiles = 0
|
||||||
nbytes = 0
|
nbytes = 0
|
||||||
err = []
|
err = []
|
||||||
for _, _, inf in walkdirs(err, ar.files):
|
for _, _, inf in walkdirs(err, ar.files, ar.x):
|
||||||
if stat.S_ISDIR(inf.st_mode):
|
if stat.S_ISDIR(inf.st_mode):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -654,6 +695,7 @@ class Ctl(object):
|
|||||||
|
|
||||||
def __init__(self, ar, stats=None):
|
def __init__(self, ar, stats=None):
|
||||||
self.ok = False
|
self.ok = False
|
||||||
|
self.errs = 0
|
||||||
self.ar = ar
|
self.ar = ar
|
||||||
self.stats = stats or self._scan()
|
self.stats = stats or self._scan()
|
||||||
if not self.stats:
|
if not self.stats:
|
||||||
@@ -667,7 +709,7 @@ class Ctl(object):
|
|||||||
if ar.te:
|
if ar.te:
|
||||||
req_ses.verify = ar.te
|
req_ses.verify = ar.te
|
||||||
|
|
||||||
self.filegen = walkdirs([], ar.files)
|
self.filegen = walkdirs([], ar.files, ar.x)
|
||||||
self.recheck = [] # type: list[File]
|
self.recheck = [] # type: list[File]
|
||||||
|
|
||||||
if ar.safe:
|
if ar.safe:
|
||||||
@@ -701,7 +743,7 @@ class Ctl(object):
|
|||||||
|
|
||||||
self._fancy()
|
self._fancy()
|
||||||
|
|
||||||
self.ok = True
|
self.ok = not self.errs
|
||||||
|
|
||||||
def _safe(self):
|
def _safe(self):
|
||||||
"""minimal basic slow boring fallback codepath"""
|
"""minimal basic slow boring fallback codepath"""
|
||||||
@@ -835,6 +877,8 @@ class Ctl(object):
|
|||||||
self.st_hash = [file, ofs]
|
self.st_hash = [file, ofs]
|
||||||
|
|
||||||
def hasher(self):
|
def hasher(self):
|
||||||
|
ptn = re.compile(self.ar.x.encode("utf-8"), re.I) if self.ar.x else None
|
||||||
|
sep = "{0}".format(os.sep).encode("ascii")
|
||||||
prd = None
|
prd = None
|
||||||
ls = {}
|
ls = {}
|
||||||
for top, rel, inf in self.filegen:
|
for top, rel, inf in self.filegen:
|
||||||
@@ -853,7 +897,7 @@ class Ctl(object):
|
|||||||
print(" ls ~{0}".format(srd))
|
print(" ls ~{0}".format(srd))
|
||||||
zb = self.ar.url.encode("utf-8")
|
zb = self.ar.url.encode("utf-8")
|
||||||
zb += quotep(rd.replace(b"\\", b"/"))
|
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:
|
if not r:
|
||||||
raise Exception("HTTP {0}".format(r.status_code))
|
raise Exception("HTTP {0}".format(r.status_code))
|
||||||
|
|
||||||
@@ -867,13 +911,29 @@ class Ctl(object):
|
|||||||
if self.ar.drd:
|
if self.ar.drd:
|
||||||
dp = os.path.join(top, rd)
|
dp = os.path.join(top, rd)
|
||||||
lnodes = set(os.listdir(dp))
|
lnodes = set(os.listdir(dp))
|
||||||
bnames = [x for x in ls if x not in lnodes]
|
if ptn:
|
||||||
if bnames:
|
zs = dp.replace(sep, b"/").rstrip(b"/") + b"/"
|
||||||
vpath = self.ar.url.split("://")[-1].split("/", 1)[-1]
|
zls = [zs + x for x in lnodes]
|
||||||
names = [x.decode("utf-8", "replace") for x in bnames]
|
zls = [x for x in zls if not ptn.match(x)]
|
||||||
locs = [vpath + srd + "/" + x for x in names]
|
lnodes = [x.split(b"/")[-1] for x in zls]
|
||||||
print("DELETING ~{0}/#{1}".format(srd, len(names)))
|
bnames = [x for x in ls if x not in lnodes and x != b".hist"]
|
||||||
req_ses.post(self.ar.url + "?delete", json=locs)
|
vpath = self.ar.url.split("://")[-1].split("/", 1)[-1]
|
||||||
|
names = [x.decode("utf-8", "replace") for x in bnames]
|
||||||
|
locs = [vpath + srd + "/" + x for x in names]
|
||||||
|
while locs:
|
||||||
|
req = locs
|
||||||
|
while req:
|
||||||
|
print("DELETING ~%s/#%s" % (srd, len(req)))
|
||||||
|
r = req_ses.post(self.ar.url + "?delete", json=req)
|
||||||
|
if r.status_code == 413 and "json 2big" in r.text:
|
||||||
|
print(" (delete request too big; slicing...)")
|
||||||
|
req = req[: len(req) // 2]
|
||||||
|
continue
|
||||||
|
elif not r:
|
||||||
|
t = "delete request failed: %r %s"
|
||||||
|
raise Exception(t % (r, r.text))
|
||||||
|
break
|
||||||
|
locs = locs[len(req) :]
|
||||||
|
|
||||||
if isdir:
|
if isdir:
|
||||||
continue
|
continue
|
||||||
@@ -926,12 +986,21 @@ class Ctl(object):
|
|||||||
self.q_upload.put(None)
|
self.q_upload.put(None)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
upath = file.abs.decode("utf-8", "replace")
|
||||||
|
if not VT100:
|
||||||
|
upath = upath.lstrip("\\?")
|
||||||
|
|
||||||
|
file.nhs += 1
|
||||||
|
if file.nhs > 32:
|
||||||
|
print("ERROR: giving up on file %s" % (upath))
|
||||||
|
self.errs += 1
|
||||||
|
continue
|
||||||
|
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
self.handshaker_busy += 1
|
self.handshaker_busy += 1
|
||||||
|
|
||||||
upath = file.abs.decode("utf-8", "replace")
|
while time.time() < file.cd:
|
||||||
if not VT100:
|
time.sleep(0.1)
|
||||||
upath = upath[4:]
|
|
||||||
|
|
||||||
hs, sprs = handshake(self.ar, file, search)
|
hs, sprs = handshake(self.ar, file, search)
|
||||||
if search:
|
if search:
|
||||||
@@ -998,14 +1067,13 @@ class Ctl(object):
|
|||||||
self.uploader_busy += 1
|
self.uploader_busy += 1
|
||||||
self.t0_up = self.t0_up or time.time()
|
self.t0_up = self.t0_up or time.time()
|
||||||
|
|
||||||
zs = "{0}/{1}/{2}/{3} {4}/{5} {6}"
|
stats = "%d/%d/%d/%d %d/%d %s" % (
|
||||||
stats = zs.format(
|
|
||||||
self.up_f,
|
self.up_f,
|
||||||
len(self.recheck),
|
len(self.recheck),
|
||||||
self.uploader_busy,
|
self.uploader_busy,
|
||||||
self.nfiles - self.up_f,
|
self.nfiles - self.up_f,
|
||||||
int(self.nbytes / (1024 * 1024)),
|
self.nbytes // (1024 * 1024),
|
||||||
int((self.nbytes - self.up_b) / (1024 * 1024)),
|
(self.nbytes - self.up_b) // (1024 * 1024),
|
||||||
self.eta,
|
self.eta,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1015,6 +1083,7 @@ class Ctl(object):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
t = "upload failed, retrying: {0} #{1} ({2})\n"
|
t = "upload failed, retrying: {0} #{1} ({2})\n"
|
||||||
eprint(t.format(file.name, cid[:8], ex))
|
eprint(t.format(file.name, cid[:8], ex))
|
||||||
|
file.cd = time.time() + self.ar.cd
|
||||||
# handshake will fix it
|
# handshake will fix it
|
||||||
|
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
@@ -1068,11 +1137,14 @@ source file/folder selection uses rsync syntax, meaning that:
|
|||||||
ap.add_argument("-v", action="store_true", help="verbose")
|
ap.add_argument("-v", action="store_true", help="verbose")
|
||||||
ap.add_argument("-a", metavar="PASSWORD", help="password or $filepath")
|
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("-s", action="store_true", help="file-search (disables upload)")
|
||||||
|
ap.add_argument("-x", type=unicode, metavar="REGEX", default="", help="skip file if filesystem-abspath matches REGEX, example: '.*/\\.hist/.*'")
|
||||||
ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible")
|
ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible")
|
||||||
|
ap.add_argument("--touch", action="store_true", help="if last-modified timestamps differ, push local to server (need write+delete perms)")
|
||||||
ap.add_argument("--version", action="store_true", help="show version and exit")
|
ap.add_argument("--version", action="store_true", help="show version and exit")
|
||||||
|
|
||||||
ap = app.add_argument_group("compatibility")
|
ap = app.add_argument_group("compatibility")
|
||||||
ap.add_argument("--cls", action="store_true", help="clear screen before start")
|
ap.add_argument("--cls", action="store_true", help="clear screen before start")
|
||||||
|
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 = app.add_argument_group("folder sync")
|
||||||
ap.add_argument("--dl", action="store_true", help="delete local files after uploading")
|
ap.add_argument("--dl", action="store_true", help="delete local files after uploading")
|
||||||
@@ -1083,7 +1155,8 @@ source file/folder selection uses rsync syntax, meaning that:
|
|||||||
ap.add_argument("-j", type=int, metavar="THREADS", default=4, help="parallel connections")
|
ap.add_argument("-j", type=int, metavar="THREADS", default=4, help="parallel connections")
|
||||||
ap.add_argument("-J", type=int, metavar="THREADS", default=hcores, help="num cpu-cores to use for hashing; set 0 or 1 for single-core hashing")
|
ap.add_argument("-J", type=int, metavar="THREADS", default=hcores, help="num cpu-cores to use for hashing; set 0 or 1 for single-core hashing")
|
||||||
ap.add_argument("-nh", action="store_true", help="disable hashing while uploading")
|
ap.add_argument("-nh", action="store_true", help="disable hashing while uploading")
|
||||||
ap.add_argument("-ns", action="store_true", help="no status panel (for slow consoles)")
|
ap.add_argument("-ns", action="store_true", help="no status panel (for slow consoles and macos)")
|
||||||
|
ap.add_argument("--cd", type=float, metavar="SEC", default=5, help="delay before reattempting a failed handshake/upload")
|
||||||
ap.add_argument("--safe", action="store_true", help="use simple fallback approach")
|
ap.add_argument("--safe", action="store_true", help="use simple fallback approach")
|
||||||
ap.add_argument("-z", action="store_true", help="ZOOMIN' (skip uploading files if they exist at the destination with the ~same last-modified timestamp, so same as yolo / turbo with date-chk but even faster)")
|
ap.add_argument("-z", action="store_true", help="ZOOMIN' (skip uploading files if they exist at the destination with the ~same last-modified timestamp, so same as yolo / turbo with date-chk but even faster)")
|
||||||
|
|
||||||
@@ -1096,7 +1169,7 @@ source file/folder selection uses rsync syntax, meaning that:
|
|||||||
ar = app.parse_args()
|
ar = app.parse_args()
|
||||||
finally:
|
finally:
|
||||||
if EXE and not sys.argv[1:]:
|
if EXE and not sys.argv[1:]:
|
||||||
print("*** hit enter to exit ***")
|
eprint("*** hit enter to exit ***")
|
||||||
try:
|
try:
|
||||||
input()
|
input()
|
||||||
except:
|
except:
|
||||||
@@ -1129,8 +1202,18 @@ source file/folder selection uses rsync syntax, meaning that:
|
|||||||
with open(fn, "rb") as f:
|
with open(fn, "rb") as f:
|
||||||
ar.a = f.read().decode("utf-8").strip()
|
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:
|
if ar.cls:
|
||||||
print("\x1b\x5b\x48\x1b\x5b\x32\x4a\x1b\x5b\x33\x4a", end="")
|
eprint("\033[H\033[2J\033[3J", end="")
|
||||||
|
|
||||||
ctl = Ctl(ar)
|
ctl = Ctl(ar)
|
||||||
|
|
||||||
@@ -1140,6 +1223,9 @@ source file/folder selection uses rsync syntax, meaning that:
|
|||||||
ar.z = True
|
ar.z = True
|
||||||
ctl = Ctl(ar, ctl.stats)
|
ctl = Ctl(ar, ctl.stats)
|
||||||
|
|
||||||
|
if ctl.errs:
|
||||||
|
print("WARNING: %d errors" % (ctl.errs))
|
||||||
|
|
||||||
sys.exit(0 if ctl.ok else 1)
|
sys.exit(0 if ctl.ok else 1)
|
||||||
|
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ def main():
|
|||||||
ofs = ln.find("{")
|
ofs = ln.find("{")
|
||||||
j = json.loads(ln[ofs:])
|
j = json.loads(ln[ofs:])
|
||||||
except:
|
except:
|
||||||
pass
|
continue
|
||||||
|
|
||||||
w = j["wark"]
|
w = j["wark"]
|
||||||
if db.execute("select w from up where w = ?", (w,)).fetchone():
|
if db.execute("select w from up where w = ?", (w,)).fetchone():
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ however if your copyparty is behind a reverse-proxy, you may want to use [`share
|
|||||||
* `URL`: full URL to the root folder (with trailing slash) followed by `$regex:1|1$`
|
* `URL`: full URL to the root folder (with trailing slash) followed by `$regex:1|1$`
|
||||||
* `pw`: password (remove `Parameters` if anon-write)
|
* `pw`: password (remove `Parameters` if anon-write)
|
||||||
|
|
||||||
|
### [`send-to-cpp.contextlet.json`](send-to-cpp.contextlet.json)
|
||||||
|
* browser integration, kind of? custom rightclick actions and stuff
|
||||||
|
* rightclick a pic and send it to copyparty straight from your browser
|
||||||
|
* for the [contextlet](https://addons.mozilla.org/en-US/firefox/addon/contextlets/) firefox extension
|
||||||
|
|
||||||
### [`media-osd-bgone.ps1`](media-osd-bgone.ps1)
|
### [`media-osd-bgone.ps1`](media-osd-bgone.ps1)
|
||||||
* disables the [windows OSD popup](https://user-images.githubusercontent.com/241032/122821375-0e08df80-d2dd-11eb-9fd9-184e8aacf1d0.png) (the thing on the left) which appears every time you hit media hotkeys to adjust volume or change song while playing music with the copyparty web-ui, or most other audio players really
|
* disables the [windows OSD popup](https://user-images.githubusercontent.com/241032/122821375-0e08df80-d2dd-11eb-9fd9-184e8aacf1d0.png) (the thing on the left) which appears every time you hit media hotkeys to adjust volume or change song while playing music with the copyparty web-ui, or most other audio players really
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# when running copyparty behind a reverse proxy,
|
# when running copyparty behind a reverse proxy,
|
||||||
# the following arguments are recommended:
|
# the following arguments are recommended:
|
||||||
#
|
#
|
||||||
# --http-only lower latency on initial connection
|
|
||||||
# -i 127.0.0.1 only accept connections from nginx
|
# -i 127.0.0.1 only accept connections from nginx
|
||||||
#
|
#
|
||||||
# if you are doing location-based proxying (such as `/stuff` below)
|
# if you are doing location-based proxying (such as `/stuff` below)
|
||||||
|
|||||||
@@ -1,14 +1,44 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
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 and server-fqdn
|
||||||
ca_name="$1"
|
ca_name="$1"
|
||||||
srv_fqdn="$2"
|
srv_fqdn="$2"
|
||||||
|
|
||||||
[ -z "$srv_fqdn" ] && {
|
[ -z "$srv_fqdn" ] && { cat <<'EOF'
|
||||||
echo "need arg 1: ca name"
|
need arg 1: ca name
|
||||||
echo "need arg 2: server fqdn and/or IPs, comma-separated"
|
need arg 2: server fqdn and/or IPs, comma-separated
|
||||||
echo "optional arg 3: if set, write cert into copyparty cfg"
|
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
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,12 +89,14 @@ show() {
|
|||||||
}
|
}
|
||||||
show ca.pem
|
show ca.pem
|
||||||
show "$srv_fqdn.pem"
|
show "$srv_fqdn.pem"
|
||||||
|
echo
|
||||||
|
echo "successfully generated new certificates"
|
||||||
|
|
||||||
# write cert into copyparty config
|
# write cert into copyparty config
|
||||||
[ -z "$3" ] || {
|
[ -z "$3" ] || {
|
||||||
mkdir -p ~/.config/copyparty
|
mkdir -p ~/.config/copyparty
|
||||||
cat "$srv_fqdn".{key,pem} ca.pem >~/.config/copyparty/cert.pem
|
cat "$srv_fqdn".{key,pem} ca.pem >~/.config/copyparty/cert.pem
|
||||||
|
echo "successfully replaced copyparty certificate"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ a {
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
var a = document.getElementById('redir'),
|
var a = document.getElementById('redir'),
|
||||||
proto = window.location.protocol.indexOf('https') === 0 ? 'https' : 'http',
|
proto = location.protocol.indexOf('https') === 0 ? 'https' : 'http',
|
||||||
loc = window.location.hostname || '127.0.0.1',
|
loc = location.hostname || '127.0.0.1',
|
||||||
port = a.getAttribute('href').split(':').pop().split('/')[0],
|
port = a.getAttribute('href').split(':').pop().split('/')[0],
|
||||||
url = proto + '://' + loc + ':' + port + '/';
|
url = proto + '://' + loc + ':' + port + '/';
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ a.setAttribute('href', url);
|
|||||||
document.getElementById('desc').innerHTML = 'redirecting to';
|
document.getElementById('desc').innerHTML = 'redirecting to';
|
||||||
|
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
window.location.href = url;
|
location.href = url;
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# when running copyparty behind a reverse proxy,
|
# when running copyparty behind a reverse proxy,
|
||||||
# the following arguments are recommended:
|
# the following arguments are recommended:
|
||||||
#
|
#
|
||||||
# --http-only lower latency on initial connection
|
|
||||||
# -i 127.0.0.1 only accept connections from nginx
|
# -i 127.0.0.1 only accept connections from nginx
|
||||||
#
|
#
|
||||||
# -nc must match or exceed the webserver's max number of concurrent clients;
|
# -nc must match or exceed the webserver's max number of concurrent clients;
|
||||||
@@ -9,12 +8,12 @@
|
|||||||
# nginx default is 512 (worker_processes 1, worker_connections 512)
|
# nginx default is 512 (worker_processes 1, worker_connections 512)
|
||||||
#
|
#
|
||||||
# you may also consider adding -j0 for CPU-intensive configurations
|
# 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
|
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
|
||||||
|
|
||||||
upstream cpp {
|
upstream cpp {
|
||||||
server 127.0.0.1:3923;
|
server 127.0.0.1:3923 fail_timeout=1s;
|
||||||
keepalive 1;
|
keepalive 1;
|
||||||
}
|
}
|
||||||
server {
|
server {
|
||||||
@@ -35,6 +34,8 @@ server {
|
|||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
# NOTE: with cloudflare you want this instead:
|
||||||
|
#proxy_set_header X-Forwarded-For $http_cf_connecting_ip;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header Connection "Keep-Alive";
|
proxy_set_header Connection "Keep-Alive";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ in {
|
|||||||
"d" (delete): permanently delete files and folders
|
"d" (delete): permanently delete files and folders
|
||||||
"g" (get): download files, but cannot see folder contents
|
"g" (get): download files, but cannot see folder contents
|
||||||
"G" (upget): "get", but can see filekeys of their own uploads
|
"G" (upget): "get", but can see filekeys of their own uploads
|
||||||
|
"h" (html): "get", but folders return their index.html
|
||||||
|
"a" (admin): can see uploader IPs, config-reload
|
||||||
|
|
||||||
For example: "rwmd"
|
For example: "rwmd"
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +1,49 @@
|
|||||||
# Maintainer: icxes <dev.null@need.moe>
|
# Maintainer: icxes <dev.null@need.moe>
|
||||||
pkgname=copyparty
|
pkgname=copyparty
|
||||||
pkgver="1.6.11"
|
pkgver="1.9.31"
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Portable file sharing hub"
|
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||||
arch=("any")
|
arch=("any")
|
||||||
url="https://github.com/9001/${pkgname}"
|
url="https://github.com/9001/${pkgname}"
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
depends=("python" "lsof")
|
depends=("python" "lsof" "python-jinja")
|
||||||
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
|
makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz")
|
||||||
"python-jinja: faster html generator"
|
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
|
||||||
|
"cfssl: generate TLS certificates on startup (pointless when reverse-proxied)"
|
||||||
"python-mutagen: music tags (alternative)"
|
"python-mutagen: music tags (alternative)"
|
||||||
"python-pillow: thumbnails for images"
|
"python-pillow: thumbnails for images"
|
||||||
"python-pyvips: thumbnails for images (higher quality, faster, uses more ram)"
|
"python-pyvips: thumbnails for images (higher quality, faster, uses more ram)"
|
||||||
"libkeyfinder-git: detection of musical keys"
|
"libkeyfinder-git: detection of musical keys"
|
||||||
"qm-vamp-plugins: BPM detection"
|
"qm-vamp-plugins: BPM detection"
|
||||||
"python-pyopenssl: ftps functionality"
|
"python-pyopenssl: ftps functionality"
|
||||||
|
"python-argon2_cffi: hashed passwords in config"
|
||||||
"python-impacket-git: smb support (bad idea)"
|
"python-impacket-git: smb support (bad idea)"
|
||||||
)
|
)
|
||||||
source=("${url}/releases/download/v${pkgver}/${pkgname}-sfx.py"
|
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||||
"${pkgname}.conf"
|
|
||||||
"${pkgname}.service"
|
|
||||||
"prisonparty.service"
|
|
||||||
"index.md"
|
|
||||||
"https://raw.githubusercontent.com/9001/${pkgname}/v${pkgver}/bin/prisonparty.sh"
|
|
||||||
"https://raw.githubusercontent.com/9001/${pkgname}/v${pkgver}/LICENSE"
|
|
||||||
)
|
|
||||||
backup=("etc/${pkgname}.d/init" )
|
backup=("etc/${pkgname}.d/init" )
|
||||||
sha256sums=("d096e33ab666ef45213899dd3a10735f62b5441339cb7374f93b232d0b6c8d34"
|
sha256sums=("a8ec1faf8cb224515355226882fdb2d1ab1de42d96ff78e148b930318867a71e")
|
||||||
"b8565eba5e64dedba1cf6c7aac7e31c5a731ed7153d6810288a28f00a36c28b2"
|
|
||||||
"f65c207e0670f9d78ad2e399bda18d5502ff30d2ac79e0e7fc48e7fbdc39afdc"
|
build() {
|
||||||
"c4f396b083c9ec02ad50b52412c84d2a82be7f079b2d016e1c9fad22d68285ff"
|
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||||
"dba701de9fd584405917e923ea1e59dbb249b96ef23bad479cf4e42740b774c8"
|
|
||||||
"8e89d281483e22d11d111bed540652af35b66af6f14f49faae7b959f6cdc6475"
|
pushd copyparty/web
|
||||||
"cb2ce3d6277bf2f5a82ecf336cc44963bc6490bcf496ffbd75fc9e21abaa75f3"
|
make -j$(nproc)
|
||||||
)
|
rm Makefile
|
||||||
|
popd
|
||||||
|
|
||||||
|
python3 -m build -wn
|
||||||
|
}
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
cd "${srcdir}/"
|
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||||
|
python3 -m installer -d "$pkgdir" dist/*.whl
|
||||||
|
|
||||||
install -dm755 "${pkgdir}/etc/${pkgname}.d"
|
install -dm755 "${pkgdir}/etc/${pkgname}.d"
|
||||||
install -Dm755 "${pkgname}-sfx.py" "${pkgdir}/usr/bin/${pkgname}"
|
install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty"
|
||||||
install -Dm755 "prisonparty.sh" "${pkgdir}/usr/bin/prisonparty"
|
install -Dm644 "contrib/package/arch/${pkgname}.conf" "${pkgdir}/etc/${pkgname}.d/init"
|
||||||
install -Dm644 "${pkgname}.conf" "${pkgdir}/etc/${pkgname}.d/init"
|
install -Dm644 "contrib/package/arch/${pkgname}.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}.service"
|
||||||
install -Dm644 "${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 "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 "index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
|
|
||||||
install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||||
|
|
||||||
find /etc/${pkgname}.d -iname '*.conf' 2>/dev/null | grep -qE . && return
|
find /etc/${pkgname}.d -iname '*.conf' 2>/dev/null | grep -qE . && return
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# this will start `/usr/bin/copyparty-sfx.py`
|
# this will start `/usr/bin/copyparty-sfx.py`
|
||||||
# in a chroot, preventing accidental access elsewhere
|
# in a chroot, preventing accidental access elsewhere,
|
||||||
# and read config from `/etc/copyparty.d/*.conf`
|
# and read copyparty config from `/etc/copyparty.d/*.conf`
|
||||||
#
|
#
|
||||||
# expose additional filesystem locations to copyparty
|
# expose additional filesystem locations to copyparty
|
||||||
# by listing them between the last `1000` and `--`
|
# by listing them between the last `cpp` and `--`
|
||||||
#
|
#
|
||||||
# `1000 1000` = what user to run copyparty as
|
# `cpp cpp` = user/group to run copyparty as; can be IDs (1000 1000)
|
||||||
#
|
#
|
||||||
# unless you add -q to disable logging, you may want to remove the
|
# unless you add -q to disable logging, you may want to remove the
|
||||||
# following line to allow buffering (slightly better performance):
|
# following line to allow buffering (slightly better performance):
|
||||||
@@ -24,7 +24,9 @@ ExecReload=/bin/kill -s USR1 $MAINPID
|
|||||||
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
||||||
|
|
||||||
# run copyparty
|
# run copyparty
|
||||||
ExecStart=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail 1000 1000 /etc/copyparty.d -- \
|
ExecStart=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail cpp cpp \
|
||||||
|
/etc/copyparty.d \
|
||||||
|
-- \
|
||||||
/usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init
|
/usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
{ lib, stdenv, makeWrapper, fetchurl, utillinux, python, jinja2, impacket, pyftpdlib, pyopenssl, pillow, pyvips, ffmpeg, mutagen,
|
{ lib, stdenv, makeWrapper, fetchurl, utillinux, python, jinja2, impacket, pyftpdlib, pyopenssl, argon2-cffi, pillow, pyvips, ffmpeg, mutagen,
|
||||||
|
|
||||||
|
# use argon2id-hashed passwords in config files (sha2 is always available)
|
||||||
|
withHashedPasswords ? true,
|
||||||
|
|
||||||
|
# generate TLS certificates on startup (pointless when reverse-proxied)
|
||||||
|
withCertgen ? false,
|
||||||
|
|
||||||
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
|
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
|
||||||
withThumbnails ? true,
|
withThumbnails ? true,
|
||||||
@@ -31,10 +37,12 @@ let
|
|||||||
]
|
]
|
||||||
++ lib.optional withSMB impacket
|
++ lib.optional withSMB impacket
|
||||||
++ lib.optional withFTPS pyopenssl
|
++ lib.optional withFTPS pyopenssl
|
||||||
|
++ lib.optional withCertgen cfssl
|
||||||
++ lib.optional withThumbnails pillow
|
++ lib.optional withThumbnails pillow
|
||||||
++ lib.optional withFastThumbnails pyvips
|
++ lib.optional withFastThumbnails pyvips
|
||||||
++ lib.optional withMediaProcessing ffmpeg
|
++ lib.optional withMediaProcessing ffmpeg
|
||||||
++ lib.optional withBasicAudioMetadata mutagen
|
++ lib.optional withBasicAudioMetadata mutagen
|
||||||
|
++ lib.optional withHashedPasswords argon2-cffi
|
||||||
);
|
);
|
||||||
in stdenv.mkDerivation {
|
in stdenv.mkDerivation {
|
||||||
pname = "copyparty";
|
pname = "copyparty";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"url": "https://github.com/9001/copyparty/releases/download/v1.6.11/copyparty-sfx.py",
|
"url": "https://github.com/9001/copyparty/releases/download/v1.9.31/copyparty-sfx.py",
|
||||||
"version": "1.6.11",
|
"version": "1.9.31",
|
||||||
"hash": "sha256-0JbjOrZm70UhOJndOhBzX2K1RBM5y3N0+TsjLQtsjTQ="
|
"hash": "sha256-yp7qoiW5yzm2M7qVmYY7R+SyhZXlqL+JxsXV22aS+MM="
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ name="copyparty"
|
|||||||
rcvar="copyparty_enable"
|
rcvar="copyparty_enable"
|
||||||
copyparty_user="copyparty"
|
copyparty_user="copyparty"
|
||||||
copyparty_args="-e2dsa -v /storage:/storage:r" # change as you see fit
|
copyparty_args="-e2dsa -v /storage:/storage:r" # change as you see fit
|
||||||
copyparty_command="/usr/local/bin/python3.8 /usr/local/copyparty/copyparty-sfx.py ${copyparty_args}"
|
copyparty_command="/usr/local/bin/python3.9 /usr/local/copyparty/copyparty-sfx.py ${copyparty_args}"
|
||||||
pidfile="/var/run/copyparty/${name}.pid"
|
pidfile="/var/run/copyparty/${name}.pid"
|
||||||
command="/usr/sbin/daemon"
|
command="/usr/sbin/daemon"
|
||||||
command_args="-P ${pidfile} -r -f ${copyparty_command}"
|
command_args="-P ${pidfile} -r -f ${copyparty_command}"
|
||||||
|
|||||||
11
contrib/send-to-cpp.contextlet.json
Normal file
11
contrib/send-to-cpp.contextlet.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"code": "// https://addons.mozilla.org/en-US/firefox/addon/contextlets/\n// https://github.com/davidmhammond/contextlets\n\nvar url = 'http://partybox.local:3923/';\nvar pw = 'wark';\n\nvar xhr = new XMLHttpRequest();\nxhr.msg = this.info.linkUrl || this.info.srcUrl;\nxhr.open('POST', url, true);\nxhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8');\nxhr.setRequestHeader('PW', pw);\nxhr.send('msg=' + xhr.msg);\n",
|
||||||
|
"contexts": [
|
||||||
|
"link"
|
||||||
|
],
|
||||||
|
"icons": null,
|
||||||
|
"patterns": "",
|
||||||
|
"scope": "background",
|
||||||
|
"title": "send to cpp",
|
||||||
|
"type": "normal"
|
||||||
|
}
|
||||||
@@ -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,
|
# systemd service which generates a new TLS certificate on each boot,
|
||||||
# that way the one-year expiry time won't cause any issues --
|
# that way the one-year expiry time won't cause any issues --
|
||||||
# just have everyone trust the ca.pem once every 10 years
|
# just have everyone trust the ca.pem once every 10 years
|
||||||
|
|||||||
42
contrib/systemd/copyparty.conf
Normal file
42
contrib/systemd/copyparty.conf
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# not actually YAML but lets pretend:
|
||||||
|
# -*- mode: yaml -*-
|
||||||
|
# vim: ft=yaml:
|
||||||
|
|
||||||
|
|
||||||
|
# put this file in /etc/
|
||||||
|
|
||||||
|
|
||||||
|
[global]
|
||||||
|
e2dsa # enable file indexing and filesystem scanning
|
||||||
|
e2ts # and enable multimedia indexing
|
||||||
|
ansi # and colors in log messages
|
||||||
|
|
||||||
|
# disable logging to stdout/journalctl and log to a file instead;
|
||||||
|
# $LOGS_DIRECTORY is usually /var/log/copyparty (comes from systemd)
|
||||||
|
# and copyparty replaces %Y-%m%d with Year-MonthDay, so the
|
||||||
|
# full path will be something like /var/log/copyparty/2023-1130.txt
|
||||||
|
# (note: enable compression by adding .xz at the end)
|
||||||
|
q, lo: $LOGS_DIRECTORY/%Y-%m%d.log
|
||||||
|
|
||||||
|
# p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE)
|
||||||
|
# i: 127.0.0.1 # only allow connections from localhost (reverse-proxies)
|
||||||
|
# ftp: 3921 # enable ftp server on port 3921
|
||||||
|
# p: 3939 # listen on another port
|
||||||
|
# df: 16 # stop accepting uploads if less than 16 GB free disk space
|
||||||
|
# ver # show copyparty version in the controlpanel
|
||||||
|
# grid # show thumbnails/grid-view by default
|
||||||
|
# theme: 2 # monokai
|
||||||
|
# name: datasaver # change the server-name that's displayed in the browser
|
||||||
|
# stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow)
|
||||||
|
# no-robots, force-js # make it harder for search engines to read your server
|
||||||
|
|
||||||
|
|
||||||
|
[accounts]
|
||||||
|
ed: wark # username: password
|
||||||
|
|
||||||
|
|
||||||
|
[/] # create a volume at "/" (the webroot), which will
|
||||||
|
/mnt # share the contents of the "/mnt" folder
|
||||||
|
accs:
|
||||||
|
rw: * # everyone gets read-write access, but
|
||||||
|
rwmda: ed # the user "ed" gets read-write-move-delete-admin
|
||||||
@@ -1,27 +1,27 @@
|
|||||||
# this will start `/usr/local/bin/copyparty-sfx.py`
|
# this will start `/usr/local/bin/copyparty-sfx.py` and
|
||||||
# and share '/mnt' with anonymous read+write
|
# read copyparty config from `/etc/copyparty.conf`, for example:
|
||||||
|
# https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.conf
|
||||||
#
|
#
|
||||||
# installation:
|
# installation:
|
||||||
# wget https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py -O /usr/local/bin/copyparty-sfx.py
|
# 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/
|
# useradd -r -s /sbin/nologin -d /var/lib/copyparty copyparty
|
||||||
# restorecon -vr /etc/systemd/system/copyparty.service # on fedora/rhel
|
# firewall-cmd --permanent --add-port=3923/tcp # --zone=libvirt
|
||||||
# firewall-cmd --permanent --add-port={80,443,3923}/tcp # --zone=libvirt
|
|
||||||
# firewall-cmd --reload
|
# firewall-cmd --reload
|
||||||
|
# cp -pv copyparty.service /etc/systemd/system/
|
||||||
|
# cp -pv copyparty.conf /etc/
|
||||||
|
# restorecon -vr /etc/systemd/system/copyparty.service # on fedora/rhel
|
||||||
# systemctl daemon-reload && systemctl enable --now copyparty
|
# systemctl daemon-reload && systemctl enable --now copyparty
|
||||||
#
|
#
|
||||||
# if it fails to start, first check this: systemctl status copyparty
|
# if it fails to start, first check this: systemctl status copyparty
|
||||||
# then try starting it while viewing logs: journalctl -fan 100
|
# then try starting it while viewing logs:
|
||||||
|
# journalctl -fan 100
|
||||||
|
# tail -Fn 100 /var/log/copyparty/$(date +%Y-%m%d.log)
|
||||||
#
|
#
|
||||||
# you may want to:
|
# you may want to:
|
||||||
# change "User=cpp" and "/home/cpp/" to another user
|
# - change "User=copyparty" and "/var/lib/copyparty/" to another user
|
||||||
# remove the nft lines to only listen on port 3923
|
# - edit /etc/copyparty.conf to configure copyparty
|
||||||
# and in the ExecStart= line:
|
# and in the ExecStart= line:
|
||||||
# change '/usr/bin/python3' to another interpreter
|
# - change '/usr/bin/python3' to another interpreter
|
||||||
# change '/mnt::rw' to another location or permission-set
|
|
||||||
# add '-q' to disable logging on busy servers
|
|
||||||
# add '-i 127.0.0.1' to only allow local connections
|
|
||||||
# add '-e2dsa' to enable filesystem scanning + indexing
|
|
||||||
# add '-e2ts' to enable metadata indexing
|
|
||||||
#
|
#
|
||||||
# with `Type=notify`, copyparty will signal systemd when it is ready to
|
# with `Type=notify`, copyparty will signal systemd when it is ready to
|
||||||
# accept connections; correctly delaying units depending on copyparty.
|
# accept connections; correctly delaying units depending on copyparty.
|
||||||
@@ -29,11 +29,9 @@
|
|||||||
# python disabling line-buffering, so messages are out-of-order:
|
# python disabling line-buffering, so messages are out-of-order:
|
||||||
# https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png
|
# https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png
|
||||||
#
|
#
|
||||||
# unless you add -q to disable logging, you may want to remove the
|
########################################################################
|
||||||
# following line to allow buffering (slightly better performance):
|
########################################################################
|
||||||
# Environment=PYTHONUNBUFFERED=x
|
|
||||||
#
|
|
||||||
# keep ExecStartPre before ExecStart, at least on rhel8
|
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=copyparty file server
|
Description=copyparty file server
|
||||||
@@ -43,23 +41,52 @@ Type=notify
|
|||||||
SyslogIdentifier=copyparty
|
SyslogIdentifier=copyparty
|
||||||
Environment=PYTHONUNBUFFERED=x
|
Environment=PYTHONUNBUFFERED=x
|
||||||
ExecReload=/bin/kill -s USR1 $MAINPID
|
ExecReload=/bin/kill -s USR1 $MAINPID
|
||||||
|
PermissionsStartOnly=true
|
||||||
|
|
||||||
# user to run as + where the TLS certificate is (if any)
|
## user to run as + where the TLS certificate is (if any)
|
||||||
User=cpp
|
##
|
||||||
Environment=XDG_CONFIG_HOME=/home/cpp/.config
|
User=copyparty
|
||||||
|
Group=copyparty
|
||||||
|
WorkingDirectory=/var/lib/copyparty
|
||||||
|
Environment=XDG_CONFIG_HOME=/var/lib/copyparty/.config
|
||||||
|
|
||||||
# OPTIONAL: setup forwarding from ports 80 and 443 to port 3923
|
## OPTIONAL: allow copyparty to listen on low ports (like 80/443);
|
||||||
ExecStartPre=+/bin/bash -c 'nft -n -a list table nat | awk "/ to :3923 /{print\$NF}" | xargs -rL1 nft delete rule nat prerouting handle; true'
|
## you need to uncomment the "p: 80,443,3923" in the config too
|
||||||
ExecStartPre=+nft add table ip nat
|
## ------------------------------------------------------------
|
||||||
ExecStartPre=+nft -- add chain ip nat prerouting { type nat hook prerouting priority -100 \; }
|
## a slightly safer alternative is to enable partyalone.service
|
||||||
ExecStartPre=+nft add rule ip nat prerouting tcp dport 80 redirect to :3923
|
## which does portforwarding with nftables instead, but an even
|
||||||
ExecStartPre=+nft add rule ip nat prerouting tcp dport 443 redirect to :3923
|
## better option is to use a reverse-proxy (nginx/caddy/...)
|
||||||
|
##
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
|
||||||
# stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running
|
## some quick hardening; TODO port more from the nixos package
|
||||||
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
##
|
||||||
|
MemoryMax=50%
|
||||||
|
MemorySwapMax=50%
|
||||||
|
ProtectClock=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
ProtectHostname=true
|
||||||
|
ProtectKernelLogs=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectProc=invisible
|
||||||
|
RemoveIPC=true
|
||||||
|
RestrictNamespaces=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
|
||||||
# copyparty settings
|
## create a directory for logfiles;
|
||||||
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -e2d -v /mnt::rw
|
## this defines $LOGS_DIRECTORY which is used in copyparty.conf
|
||||||
|
##
|
||||||
|
LogsDirectory=copyparty
|
||||||
|
|
||||||
|
## finally, start copyparty and give it the config file:
|
||||||
|
##
|
||||||
|
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -c /etc/copyparty.conf
|
||||||
|
|
||||||
|
# NOTE: if you installed copyparty from an OS package repo (nice)
|
||||||
|
# then you probably want something like this instead:
|
||||||
|
#ExecStart=/usr/bin/copyparty -c /etc/copyparty.conf
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# this will start `/usr/local/bin/copyparty-sfx.py`
|
# this will start `/usr/local/bin/copyparty-sfx.py`
|
||||||
# in a chroot, preventing accidental access elsewhere
|
# in a chroot, preventing accidental access elsewhere,
|
||||||
# and share '/mnt' with anonymous read+write
|
# and share '/mnt' with anonymous read+write
|
||||||
#
|
#
|
||||||
# installation:
|
# installation:
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
# 2) cp -pv prisonparty.service /etc/systemd/system && systemctl enable --now prisonparty
|
# 2) cp -pv prisonparty.service /etc/systemd/system && systemctl enable --now prisonparty
|
||||||
#
|
#
|
||||||
# expose additional filesystem locations to copyparty
|
# expose additional filesystem locations to copyparty
|
||||||
# by listing them between the last `1000` and `--`
|
# by listing them between the last `cpp` and `--`
|
||||||
#
|
#
|
||||||
# `1000 1000` = what user to run copyparty as
|
# `cpp cpp` = user/group to run copyparty as; can be IDs (1000 1000)
|
||||||
#
|
#
|
||||||
# you may want to:
|
# you may want to:
|
||||||
# change '/mnt::rw' to another location or permission-set
|
# change '/mnt::rw' to another location or permission-set
|
||||||
@@ -32,7 +32,9 @@ ExecReload=/bin/kill -s USR1 $MAINPID
|
|||||||
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
||||||
|
|
||||||
# run copyparty
|
# run copyparty
|
||||||
ExecStart=/bin/bash /usr/local/bin/prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt -- \
|
ExecStart=/bin/bash /usr/local/bin/prisonparty.sh /var/lib/copyparty-jail cpp cpp \
|
||||||
|
/mnt \
|
||||||
|
-- \
|
||||||
/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw
|
/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|||||||
2
contrib/windows/copyparty-ctmp.bat
Executable file
2
contrib/windows/copyparty-ctmp.bat
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
rem run copyparty.exe on machines with busted environment variables
|
||||||
|
cmd /v /c "set TMP=\tmp && copyparty.exe"
|
||||||
@@ -6,6 +6,10 @@ import platform
|
|||||||
import sys
|
import sys
|
||||||
import time
|
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:
|
try:
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
except:
|
except:
|
||||||
@@ -19,7 +23,7 @@ if not PY2:
|
|||||||
unicode: Callable[[Any], str] = str
|
unicode: Callable[[Any], str] = str
|
||||||
else:
|
else:
|
||||||
sys.dont_write_bytecode = True
|
sys.dont_write_bytecode = True
|
||||||
unicode = unicode # noqa: F821 # pylint: disable=undefined-variable,self-assigning-variable
|
unicode = unicode # type: ignore
|
||||||
|
|
||||||
WINDOWS: Any = (
|
WINDOWS: Any = (
|
||||||
[int(x) for x in platform.version().split(".")]
|
[int(x) for x in platform.version().split(".")]
|
||||||
@@ -27,7 +31,12 @@ WINDOWS: Any = (
|
|||||||
else False
|
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
|
# introduced in anniversary update
|
||||||
|
|
||||||
ANYWIN = WINDOWS or sys.platform in ["msys", "cygwin"]
|
ANYWIN = WINDOWS or sys.platform in ["msys", "cygwin"]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
VERSION = (1, 6, 12)
|
VERSION = (1, 10, 0)
|
||||||
CODENAME = "cors k"
|
CODENAME = "tftp"
|
||||||
BUILD_DT = (2023, 4, 20)
|
BUILD_DT = (2024, 2, 15)
|
||||||
|
|
||||||
S_VERSION = ".".join(map(str, VERSION))
|
S_VERSION = ".".join(map(str, VERSION))
|
||||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,10 @@ def open(p: str, *a, **ka) -> int:
|
|||||||
return os.open(fsenc(p), *a, **ka)
|
return os.open(fsenc(p), *a, **ka)
|
||||||
|
|
||||||
|
|
||||||
|
def readlink(p: str) -> str:
|
||||||
|
return fsdec(os.readlink(fsenc(p)))
|
||||||
|
|
||||||
|
|
||||||
def rename(src: str, dst: str) -> None:
|
def rename(src: str, dst: str) -> None:
|
||||||
return os.rename(fsenc(src), fsenc(dst))
|
return os.rename(fsenc(src), fsenc(dst))
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import queue
|
|||||||
|
|
||||||
from .__init__ import CORES, TYPE_CHECKING
|
from .__init__ import CORES, TYPE_CHECKING
|
||||||
from .broker_mpw import MpWorker
|
from .broker_mpw import MpWorker
|
||||||
from .broker_util import try_exec
|
from .broker_util import ExceptionalQueue, try_exec
|
||||||
from .util import Daemon, mp
|
from .util import Daemon, mp
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -46,8 +46,8 @@ class BrokerMp(object):
|
|||||||
self.num_workers = self.args.j or CORES
|
self.num_workers = self.args.j or CORES
|
||||||
self.log("broker", "booting {} subprocesses".format(self.num_workers))
|
self.log("broker", "booting {} subprocesses".format(self.num_workers))
|
||||||
for n in range(1, self.num_workers + 1):
|
for n in range(1, self.num_workers + 1):
|
||||||
q_pend: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(1)
|
q_pend: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(1) # type: ignore
|
||||||
q_yield: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(64)
|
q_yield: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(64) # type: ignore
|
||||||
|
|
||||||
proc = MProcess(q_pend, q_yield, MpWorker, (q_pend, q_yield, self.args, n))
|
proc = MProcess(q_pend, q_yield, MpWorker, (q_pend, q_yield, self.args, n))
|
||||||
Daemon(self.collector, "mp-sink-{}".format(n), (proc,))
|
Daemon(self.collector, "mp-sink-{}".format(n), (proc,))
|
||||||
@@ -69,7 +69,7 @@ class BrokerMp(object):
|
|||||||
|
|
||||||
while procs:
|
while procs:
|
||||||
if procs[-1].is_alive():
|
if procs[-1].is_alive():
|
||||||
time.sleep(0.1)
|
time.sleep(0.05)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
procs.pop()
|
procs.pop()
|
||||||
@@ -107,6 +107,19 @@ class BrokerMp(object):
|
|||||||
if retq_id:
|
if retq_id:
|
||||||
proc.q_pend.put((retq_id, "retq", rv))
|
proc.q_pend.put((retq_id, "retq", rv))
|
||||||
|
|
||||||
|
def ask(self, dest: str, *args: Any) -> ExceptionalQueue:
|
||||||
|
|
||||||
|
# new non-ipc invoking managed service in hub
|
||||||
|
obj = self.hub
|
||||||
|
for node in dest.split("."):
|
||||||
|
obj = getattr(obj, node)
|
||||||
|
|
||||||
|
rv = try_exec(True, obj, *args)
|
||||||
|
|
||||||
|
retq = ExceptionalQueue(1)
|
||||||
|
retq.put(rv)
|
||||||
|
return retq
|
||||||
|
|
||||||
def say(self, dest: str, *args: Any) -> None:
|
def say(self, dest: str, *args: Any) -> None:
|
||||||
"""
|
"""
|
||||||
send message to non-hub component in other process,
|
send message to non-hub component in other process,
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class MpWorker(BrokerCli):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def logw(self, msg: str, c: Union[int, str] = 0) -> None:
|
def logw(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||||
self.log("mp{}".format(self.n), msg, c)
|
self.log("mp%d" % (self.n,), msg, c)
|
||||||
|
|
||||||
def main(self) -> None:
|
def main(self) -> None:
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
229
copyparty/cert.py
Normal file
229
copyparty/cert.py
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import calendar
|
||||||
|
import errno
|
||||||
|
import filecmp
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
|
||||||
|
from .util import Netdev, runcmd
|
||||||
|
|
||||||
|
HAVE_CFSSL = True
|
||||||
|
|
||||||
|
if True: # pylint: disable=using-constant-test
|
||||||
|
from .util import RootLogger
|
||||||
|
|
||||||
|
|
||||||
|
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 args.crt_nohn:
|
||||||
|
names.append(args.name)
|
||||||
|
names.append(args.name + ".local")
|
||||||
|
if not names:
|
||||||
|
names = ["127.0.0.1"]
|
||||||
|
if "127.0.0.1" in names or "::1" in names:
|
||||||
|
names.append("localhost")
|
||||||
|
names = list({x: 1 for x in names}.keys())
|
||||||
|
|
||||||
|
try:
|
||||||
|
expiry, inf = _read_crt(args, "srv.pem")
|
||||||
|
if "sans" not in inf:
|
||||||
|
raise Exception("no useable cert found")
|
||||||
|
|
||||||
|
expired = time.time() + args.crt_sdays * 60 * 60 * 24 * 0.5 > expiry
|
||||||
|
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")
|
||||||
|
try:
|
||||||
|
os.unlink(bname + ".key")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
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 (cfssl, cfssljson, cfssl-certinfo)"
|
||||||
|
log("cert", t, 6)
|
||||||
|
|
||||||
|
ensure_cert(log, args)
|
||||||
@@ -9,19 +9,32 @@ onedash = set(zs.split())
|
|||||||
def vf_bmap() -> dict[str, str]:
|
def vf_bmap() -> dict[str, str]:
|
||||||
"""argv-to-volflag: simple bools"""
|
"""argv-to-volflag: simple bools"""
|
||||||
ret = {
|
ret = {
|
||||||
|
"dav_auth": "davauth",
|
||||||
|
"dav_rt": "davrt",
|
||||||
|
"ed": "dots",
|
||||||
"never_symlink": "neversymlink",
|
"never_symlink": "neversymlink",
|
||||||
"no_dedup": "copydupes",
|
"no_dedup": "copydupes",
|
||||||
"no_dupe": "nodupe",
|
"no_dupe": "nodupe",
|
||||||
"no_forget": "noforget",
|
"no_forget": "noforget",
|
||||||
|
"no_robots": "norobots",
|
||||||
|
"no_thumb": "dthumb",
|
||||||
|
"no_vthumb": "dvthumb",
|
||||||
|
"no_athumb": "dathumb",
|
||||||
|
"th_no_crop": "nocrop",
|
||||||
}
|
}
|
||||||
for k in (
|
for k in (
|
||||||
"dotsrch",
|
"dotsrch",
|
||||||
|
"e2d",
|
||||||
|
"e2ds",
|
||||||
|
"e2dsa",
|
||||||
"e2t",
|
"e2t",
|
||||||
"e2ts",
|
"e2ts",
|
||||||
"e2tsr",
|
"e2tsr",
|
||||||
"e2v",
|
"e2v",
|
||||||
"e2vu",
|
"e2vu",
|
||||||
"e2vp",
|
"e2vp",
|
||||||
|
"exp",
|
||||||
|
"grid",
|
||||||
"hardlink",
|
"hardlink",
|
||||||
"magic",
|
"magic",
|
||||||
"no_sb_md",
|
"no_sb_md",
|
||||||
@@ -37,8 +50,23 @@ def vf_bmap() -> dict[str, str]:
|
|||||||
|
|
||||||
def vf_vmap() -> dict[str, str]:
|
def vf_vmap() -> dict[str, str]:
|
||||||
"""argv-to-volflag: simple values"""
|
"""argv-to-volflag: simple values"""
|
||||||
ret = {}
|
ret = {
|
||||||
for k in ("lg_sbf", "md_sbf"):
|
"no_hash": "nohash",
|
||||||
|
"no_idx": "noidx",
|
||||||
|
"re_maxage": "scan",
|
||||||
|
"th_convt": "convt",
|
||||||
|
"th_size": "thsize",
|
||||||
|
}
|
||||||
|
for k in (
|
||||||
|
"dbd",
|
||||||
|
"lg_sbf",
|
||||||
|
"md_sbf",
|
||||||
|
"nrand",
|
||||||
|
"rm_retry",
|
||||||
|
"sort",
|
||||||
|
"unlist",
|
||||||
|
"u2ts",
|
||||||
|
):
|
||||||
ret[k] = k
|
ret[k] = k
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -46,7 +74,23 @@ def vf_vmap() -> dict[str, str]:
|
|||||||
def vf_cmap() -> dict[str, str]:
|
def vf_cmap() -> dict[str, str]:
|
||||||
"""argv-to-volflag: complex/lists"""
|
"""argv-to-volflag: complex/lists"""
|
||||||
ret = {}
|
ret = {}
|
||||||
for k in ("dbd", "html_head", "mte", "mth", "nrand"):
|
for k in (
|
||||||
|
"exp_lg",
|
||||||
|
"exp_md",
|
||||||
|
"html_head",
|
||||||
|
"mte",
|
||||||
|
"mth",
|
||||||
|
"mtp",
|
||||||
|
"xad",
|
||||||
|
"xar",
|
||||||
|
"xau",
|
||||||
|
"xban",
|
||||||
|
"xbd",
|
||||||
|
"xbr",
|
||||||
|
"xbu",
|
||||||
|
"xiu",
|
||||||
|
"xm",
|
||||||
|
):
|
||||||
ret[k] = k
|
ret[k] = k
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -56,8 +100,12 @@ permdescs = {
|
|||||||
"w": 'write; upload files; need "r" to see the uploads',
|
"w": 'write; upload files; need "r" to see the uploads',
|
||||||
"m": 'move; move files and folders; need "w" at destination',
|
"m": 'move; move files and folders; need "w" at destination',
|
||||||
"d": "delete; permanently delete files and folders",
|
"d": "delete; permanently delete files and folders",
|
||||||
|
".": "dots; user can ask to show dotfiles in listings",
|
||||||
"g": "get; download files, but cannot see folder contents",
|
"g": "get; download files, but cannot see folder contents",
|
||||||
"G": 'upget; same as "g" but can see filekeys of their own uploads',
|
"G": 'upget; same as "g" but can see filekeys of their own uploads',
|
||||||
|
"h": 'html; same as "g" but folders return their index.html',
|
||||||
|
"a": "admin; can see uploader IPs, config-reload",
|
||||||
|
"A": "all; same as 'rwmda.' (read/write/move/delete/dotfiles)",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -75,9 +123,12 @@ flagcats = {
|
|||||||
},
|
},
|
||||||
"upload rules": {
|
"upload rules": {
|
||||||
"maxn=250,600": "max 250 uploads over 15min",
|
"maxn=250,600": "max 250 uploads over 15min",
|
||||||
"maxb=1g,300": "max 1 GiB over 5min (suffixes: b, k, m, g)",
|
"maxb=1g,300": "max 1 GiB over 5min (suffixes: b, k, m, g, t)",
|
||||||
|
"vmaxb=1g": "total volume size max 1 GiB (suffixes: b, k, m, g, t)",
|
||||||
|
"vmaxn=4k": "max 4096 files in volume (suffixes: b, k, m, g, t)",
|
||||||
"rand": "force randomized filenames, 9 chars long by default",
|
"rand": "force randomized filenames, 9 chars long by default",
|
||||||
"nrand=N": "randomized filenames are N chars long",
|
"nrand=N": "randomized filenames are N chars long",
|
||||||
|
"u2ts=fc": "[f]orce [c]lient-last-modified or [u]pload-time",
|
||||||
"sz=1k-3m": "allow filesizes between 1 KiB and 3MiB",
|
"sz=1k-3m": "allow filesizes between 1 KiB and 3MiB",
|
||||||
"df=1g": "ensure 1 GiB free disk space",
|
"df=1g": "ensure 1 GiB free disk space",
|
||||||
},
|
},
|
||||||
@@ -103,10 +154,11 @@ flagcats = {
|
|||||||
"nohash=\\.iso$": "skips hashing file contents if path matches *.iso",
|
"nohash=\\.iso$": "skips hashing file contents if path matches *.iso",
|
||||||
"noidx=\\.iso$": "fully ignores the contents at paths matching *.iso",
|
"noidx=\\.iso$": "fully ignores the contents at paths matching *.iso",
|
||||||
"noforget": "don't forget files when deleted from disk",
|
"noforget": "don't forget files when deleted from disk",
|
||||||
|
"fat32": "avoid excessive reindexing on android sdcardfs",
|
||||||
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
|
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
|
||||||
"xlink": "cross-volume dupe detection / linking",
|
"xlink": "cross-volume dupe detection / linking",
|
||||||
"xdev": "do not descend into other filesystems",
|
"xdev": "do not descend into other filesystems",
|
||||||
"xvol": "skip symlinks leaving the volume root",
|
"xvol": "do not follow symlinks leaving the volume root",
|
||||||
"dotsrch": "show dotfiles in search results",
|
"dotsrch": "show dotfiles in search results",
|
||||||
"nodotsrch": "hide dotfiles in search results (default)",
|
"nodotsrch": "hide dotfiles in search results (default)",
|
||||||
},
|
},
|
||||||
@@ -119,6 +171,13 @@ flagcats = {
|
|||||||
"dvthumb": "disables video thumbnails",
|
"dvthumb": "disables video thumbnails",
|
||||||
"dathumb": "disables audio thumbnails (spectrograms)",
|
"dathumb": "disables audio thumbnails (spectrograms)",
|
||||||
"dithumb": "disables image thumbnails",
|
"dithumb": "disables image thumbnails",
|
||||||
|
"thsize": "thumbnail res; WxH",
|
||||||
|
"nocrop": "disable center-cropping by default",
|
||||||
|
"convt": "conversion timeout in seconds",
|
||||||
|
},
|
||||||
|
"handlers\n(better explained in --help-handlers)": {
|
||||||
|
"on404=PY": "handle 404s by executing PY file",
|
||||||
|
"on403=PY": "handle 403s by executing PY file",
|
||||||
},
|
},
|
||||||
"event hooks\n(better explained in --help-hooks)": {
|
"event hooks\n(better explained in --help-hooks)": {
|
||||||
"xbu=CMD": "execute CMD before a file upload starts",
|
"xbu=CMD": "execute CMD before a file upload starts",
|
||||||
@@ -129,8 +188,12 @@ flagcats = {
|
|||||||
"xbd=CMD": "execute CMD before a file delete",
|
"xbd=CMD": "execute CMD before a file delete",
|
||||||
"xad=CMD": "execute CMD after a file delete",
|
"xad=CMD": "execute CMD after a file delete",
|
||||||
"xm=CMD": "execute CMD on message",
|
"xm=CMD": "execute CMD on message",
|
||||||
|
"xban=CMD": "execute CMD if someone gets banned",
|
||||||
},
|
},
|
||||||
"client and ux": {
|
"client and ux": {
|
||||||
|
"grid": "show grid/thumbnails by default",
|
||||||
|
"sort": "default sort order",
|
||||||
|
"unlist": "dont list files matching REGEX",
|
||||||
"html_head=TXT": "includes TXT in the <head>",
|
"html_head=TXT": "includes TXT in the <head>",
|
||||||
"robots": "allows indexing by search engines (default)",
|
"robots": "allows indexing by search engines (default)",
|
||||||
"norobots": "kindly asks search engines to leave",
|
"norobots": "kindly asks search engines to leave",
|
||||||
@@ -140,9 +203,15 @@ flagcats = {
|
|||||||
"sb_lg": "enable js sandbox for prologue/epilogue (default)",
|
"sb_lg": "enable js sandbox for prologue/epilogue (default)",
|
||||||
"md_sbf": "list of markdown-sandbox safeguards to disable",
|
"md_sbf": "list of markdown-sandbox safeguards to disable",
|
||||||
"lg_sbf": "list of *logue-sandbox safeguards to disable",
|
"lg_sbf": "list of *logue-sandbox safeguards to disable",
|
||||||
|
"nohtml": "return html and markdown as text/html",
|
||||||
},
|
},
|
||||||
"others": {
|
"others": {
|
||||||
"fk=8": 'generates per-file accesskeys,\nwhich will then be required at the "g" permission'
|
"dots": "allow all users with read-access to\nenable the option to show dotfiles in listings",
|
||||||
|
"fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
|
||||||
|
"fka=8": 'generates slightly weaker per-file accesskeys,\nwhich are then required at the "g" permission;\nnot affected by filesize or inode numbers',
|
||||||
|
"rm_retry": "ms-windows: timeout for deleting busy files",
|
||||||
|
"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)",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import errno
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import stat
|
import stat
|
||||||
@@ -11,9 +12,10 @@ import time
|
|||||||
from pyftpdlib.authorizers import AuthenticationFailed, DummyAuthorizer
|
from pyftpdlib.authorizers import AuthenticationFailed, DummyAuthorizer
|
||||||
from pyftpdlib.filesystems import AbstractedFS, FilesystemError
|
from pyftpdlib.filesystems import AbstractedFS, FilesystemError
|
||||||
from pyftpdlib.handlers import FTPHandler
|
from pyftpdlib.handlers import FTPHandler
|
||||||
|
from pyftpdlib.ioloop import IOLoop
|
||||||
from pyftpdlib.servers import FTPServer
|
from pyftpdlib.servers import FTPServer
|
||||||
|
|
||||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, E
|
from .__init__ import PY2, TYPE_CHECKING
|
||||||
from .authsrv import VFS
|
from .authsrv import VFS
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
from .util import (
|
from .util import (
|
||||||
@@ -29,15 +31,6 @@ from .util import (
|
|||||||
vjoin,
|
vjoin,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
|
||||||
from pyftpdlib.ioloop import IOLoop
|
|
||||||
except ImportError:
|
|
||||||
p = os.path.join(E.mod, "vend")
|
|
||||||
print("loading asynchat from " + p)
|
|
||||||
sys.path.append(p)
|
|
||||||
from pyftpdlib.ioloop import IOLoop
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .svchub import SvcHub
|
from .svchub import SvcHub
|
||||||
|
|
||||||
@@ -46,6 +39,12 @@ if True: # pylint: disable=using-constant-test
|
|||||||
from typing import Any, Optional
|
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):
|
class FtpAuth(DummyAuthorizer):
|
||||||
def __init__(self, hub: "SvcHub") -> None:
|
def __init__(self, hub: "SvcHub") -> None:
|
||||||
super(FtpAuth, self).__init__()
|
super(FtpAuth, self).__init__()
|
||||||
@@ -55,6 +54,7 @@ class FtpAuth(DummyAuthorizer):
|
|||||||
self, username: str, password: str, handler: Any
|
self, username: str, password: str, handler: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
handler.username = "{}:{}".format(username, password)
|
handler.username = "{}:{}".format(username, password)
|
||||||
|
handler.uname = "*"
|
||||||
|
|
||||||
ip = handler.addr[0]
|
ip = handler.addr[0]
|
||||||
if ip.startswith("::ffff:"):
|
if ip.startswith("::ffff:"):
|
||||||
@@ -71,10 +71,14 @@ class FtpAuth(DummyAuthorizer):
|
|||||||
raise AuthenticationFailed("banned")
|
raise AuthenticationFailed("banned")
|
||||||
|
|
||||||
asrv = self.hub.asrv
|
asrv = self.hub.asrv
|
||||||
if username == "anonymous":
|
uname = "*"
|
||||||
uname = "*"
|
if username != "anonymous":
|
||||||
else:
|
uname = ""
|
||||||
uname = asrv.iacct.get(password, "") or asrv.iacct.get(username, "") or "*"
|
for zs in (password, username):
|
||||||
|
zs = asrv.iacct.get(asrv.ah.hash(zs), "")
|
||||||
|
if zs:
|
||||||
|
uname = zs
|
||||||
|
break
|
||||||
|
|
||||||
if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
|
if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
|
||||||
g = self.hub.gpwd
|
g = self.hub.gpwd
|
||||||
@@ -83,17 +87,23 @@ class FtpAuth(DummyAuthorizer):
|
|||||||
if bonk:
|
if bonk:
|
||||||
logging.warning("client banned: invalid passwords")
|
logging.warning("client banned: invalid passwords")
|
||||||
bans[ip] = bonk
|
bans[ip] = bonk
|
||||||
|
try:
|
||||||
|
# only possible if multiprocessing disabled
|
||||||
|
self.hub.broker.httpsrv.bans[ip] = bonk # type: ignore
|
||||||
|
self.hub.broker.httpsrv.nban += 1 # type: ignore
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
raise AuthenticationFailed("Authentication failed.")
|
raise AuthenticationFailed("Authentication failed.")
|
||||||
|
|
||||||
handler.username = uname
|
handler.uname = handler.username = uname
|
||||||
|
|
||||||
def get_home_dir(self, username: str) -> str:
|
def get_home_dir(self, username: str) -> str:
|
||||||
return "/"
|
return "/"
|
||||||
|
|
||||||
def has_user(self, username: str) -> bool:
|
def has_user(self, username: str) -> bool:
|
||||||
asrv = self.hub.asrv
|
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:
|
def has_perm(self, username: str, perm: int, path: Optional[str] = None) -> bool:
|
||||||
return True # handled at filesystem layer
|
return True # handled at filesystem layer
|
||||||
@@ -112,25 +122,22 @@ class FtpFs(AbstractedFS):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self, root: str, cmd_channel: Any
|
self, root: str, cmd_channel: Any
|
||||||
) -> None: # pylint: disable=super-init-not-called
|
) -> 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.hub: "SvcHub" = cmd_channel.hub
|
||||||
self.args = cmd_channel.args
|
self.args = cmd_channel.args
|
||||||
|
self.uname = cmd_channel.uname
|
||||||
self.uname = self.hub.asrv.iacct.get(cmd_channel.password, "*")
|
|
||||||
|
|
||||||
self.cwd = "/" # pyftpdlib convention of leading slash
|
self.cwd = "/" # pyftpdlib convention of leading slash
|
||||||
self.root = "/var/lib/empty"
|
self.root = "/var/lib/empty"
|
||||||
|
|
||||||
self.can_read = self.can_write = self.can_move = False
|
self.can_read = self.can_write = self.can_move = False
|
||||||
self.can_delete = self.can_get = self.can_upget = False
|
self.can_delete = self.can_get = self.can_upget = False
|
||||||
|
self.can_admin = self.can_dot = False
|
||||||
|
|
||||||
self.listdirinfo = self.listdir
|
self.listdirinfo = self.listdir
|
||||||
self.chdir(".")
|
self.chdir(".")
|
||||||
|
|
||||||
def die(self, msg):
|
|
||||||
self.h.die(msg)
|
|
||||||
raise Exception()
|
|
||||||
|
|
||||||
def v2a(
|
def v2a(
|
||||||
self,
|
self,
|
||||||
vpath: str,
|
vpath: str,
|
||||||
@@ -140,21 +147,34 @@ class FtpFs(AbstractedFS):
|
|||||||
d: bool = False,
|
d: bool = False,
|
||||||
) -> tuple[str, VFS, str]:
|
) -> tuple[str, VFS, str]:
|
||||||
try:
|
try:
|
||||||
vpath = vpath.replace("\\", "/").lstrip("/")
|
vpath = vpath.replace("\\", "/").strip("/")
|
||||||
rd, fn = os.path.split(vpath)
|
rd, fn = os.path.split(vpath)
|
||||||
if ANYWIN and relchk(rd):
|
if relchk(rd):
|
||||||
logging.warning("malicious vpath: %s", vpath)
|
logging.warning("malicious vpath: %s", vpath)
|
||||||
self.die("Unsupported characters in filepath")
|
t = "Unsupported characters in [{}]"
|
||||||
|
raise FSE(t.format(vpath), 1)
|
||||||
|
|
||||||
fn = sanitize_fn(fn or "", "", [".prologue.html", ".epilogue.html"])
|
fn = sanitize_fn(fn or "", "", [".prologue.html", ".epilogue.html"])
|
||||||
vpath = vjoin(rd, fn)
|
vpath = vjoin(rd, fn)
|
||||||
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
|
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
|
||||||
if not vfs.realpath:
|
if not vfs.realpath:
|
||||||
self.die("No filesystem mounted at this path")
|
t = "No filesystem mounted at [{}]"
|
||||||
|
raise FSE(t.format(vpath))
|
||||||
|
|
||||||
|
if "xdev" in vfs.flags or "xvol" in vfs.flags:
|
||||||
|
ap = vfs.canonical(rem)
|
||||||
|
avfs = vfs.chk_ap(ap)
|
||||||
|
t = "Permission denied in [{}]"
|
||||||
|
if not avfs:
|
||||||
|
raise FSE(t.format(vpath), 1)
|
||||||
|
|
||||||
|
cr, cw, cm, cd, _, _, _, _ = avfs.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
|
return os.path.join(vfs.realpath, rem), vfs, rem
|
||||||
except Pebkac as ex:
|
except Pebkac as ex:
|
||||||
self.die(str(ex))
|
raise FSE(str(ex))
|
||||||
|
|
||||||
def rv2a(
|
def rv2a(
|
||||||
self,
|
self,
|
||||||
@@ -177,7 +197,7 @@ class FtpFs(AbstractedFS):
|
|||||||
def validpath(self, path: str) -> bool:
|
def validpath(self, path: str) -> bool:
|
||||||
if "/.hist/" in path:
|
if "/.hist/" in path:
|
||||||
if "/up2k." in path or path.endswith("/dir.txt"):
|
if "/up2k." in path or path.endswith("/dir.txt"):
|
||||||
self.die("Access to this file is forbidden")
|
raise FSE("Access to this file is forbidden", 1)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -194,7 +214,7 @@ class FtpFs(AbstractedFS):
|
|||||||
td = 0
|
td = 0
|
||||||
|
|
||||||
if td < -1 or td > self.args.ftp_wt:
|
if td < -1 or td > self.args.ftp_wt:
|
||||||
self.die("Cannot open existing file for writing")
|
raise FSE("Cannot open existing file for writing")
|
||||||
|
|
||||||
self.validpath(ap)
|
self.validpath(ap)
|
||||||
return open(fsenc(ap), mode)
|
return open(fsenc(ap), mode)
|
||||||
@@ -203,9 +223,17 @@ class FtpFs(AbstractedFS):
|
|||||||
nwd = join(self.cwd, path)
|
nwd = join(self.cwd, path)
|
||||||
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
|
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
|
||||||
ap = vfs.canonical(rem)
|
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
|
# returning 550 is library-default and suitable
|
||||||
self.die("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
|
self.cwd = nwd
|
||||||
(
|
(
|
||||||
@@ -215,16 +243,20 @@ class FtpFs(AbstractedFS):
|
|||||||
self.can_delete,
|
self.can_delete,
|
||||||
self.can_get,
|
self.can_get,
|
||||||
self.can_upget,
|
self.can_upget,
|
||||||
) = self.hub.asrv.vfs.can_access(self.cwd.lstrip("/"), self.h.username)
|
self.can_admin,
|
||||||
|
self.can_dot,
|
||||||
|
) = avfs.can_access("", self.h.uname)
|
||||||
|
|
||||||
def mkdir(self, path: str) -> None:
|
def mkdir(self, path: str) -> None:
|
||||||
ap = self.rv2a(path, w=True)[0]
|
ap = self.rv2a(path, w=True)[0]
|
||||||
bos.makedirs(ap) # filezilla expects this
|
bos.makedirs(ap) # filezilla expects this
|
||||||
|
|
||||||
def listdir(self, path: str) -> list[str]:
|
def listdir(self, path: str) -> list[str]:
|
||||||
vpath = join(self.cwd, path).lstrip("/")
|
vpath = join(self.cwd, path)
|
||||||
try:
|
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(
|
fsroot, vfs_ls1, vfs_virt = vfs.ls(
|
||||||
rem,
|
rem,
|
||||||
@@ -235,13 +267,17 @@ class FtpFs(AbstractedFS):
|
|||||||
vfs_ls = [x[0] for x in vfs_ls1]
|
vfs_ls = [x[0] for x in vfs_ls1]
|
||||||
vfs_ls.extend(vfs_virt.keys())
|
vfs_ls.extend(vfs_virt.keys())
|
||||||
|
|
||||||
if not self.args.ed:
|
if not self.can_dot:
|
||||||
vfs_ls = exclude_dotfiles(vfs_ls)
|
vfs_ls = exclude_dotfiles(vfs_ls)
|
||||||
|
|
||||||
vfs_ls.sort()
|
vfs_ls.sort()
|
||||||
return vfs_ls
|
return vfs_ls
|
||||||
except:
|
except Exception as ex:
|
||||||
if vpath:
|
# panic on malicious names
|
||||||
|
if getattr(ex, "severity", 0):
|
||||||
|
raise
|
||||||
|
|
||||||
|
if vpath.strip("/"):
|
||||||
# display write-only folders as empty
|
# display write-only folders as empty
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -251,31 +287,35 @@ class FtpFs(AbstractedFS):
|
|||||||
|
|
||||||
def rmdir(self, path: str) -> None:
|
def rmdir(self, path: str) -> None:
|
||||||
ap = self.rv2a(path, d=True)[0]
|
ap = self.rv2a(path, d=True)[0]
|
||||||
bos.rmdir(ap)
|
try:
|
||||||
|
bos.rmdir(ap)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno != errno.ENOENT:
|
||||||
|
raise
|
||||||
|
|
||||||
def remove(self, path: str) -> None:
|
def remove(self, path: str) -> None:
|
||||||
if self.args.no_del:
|
if self.args.no_del:
|
||||||
self.die("The delete feature is disabled in server config")
|
raise FSE("The delete feature is disabled in server config")
|
||||||
|
|
||||||
vp = join(self.cwd, path).lstrip("/")
|
vp = join(self.cwd, path).lstrip("/")
|
||||||
try:
|
try:
|
||||||
self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [])
|
self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [], False)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.die(str(ex))
|
raise FSE(str(ex))
|
||||||
|
|
||||||
def rename(self, src: str, dst: str) -> None:
|
def rename(self, src: str, dst: str) -> None:
|
||||||
if not self.can_move:
|
if not self.can_move:
|
||||||
self.die("Not allowed for user " + self.h.username)
|
raise FSE("Not allowed for user " + self.h.uname)
|
||||||
|
|
||||||
if self.args.no_mv:
|
if self.args.no_mv:
|
||||||
self.die("The rename/move feature is disabled in server config")
|
raise FSE("The rename/move feature is disabled in server config")
|
||||||
|
|
||||||
svp = join(self.cwd, src).lstrip("/")
|
svp = join(self.cwd, src).lstrip("/")
|
||||||
dvp = join(self.cwd, dst).lstrip("/")
|
dvp = join(self.cwd, dst).lstrip("/")
|
||||||
try:
|
try:
|
||||||
self.hub.up2k.handle_mv(self.uname, svp, dvp)
|
self.hub.up2k.handle_mv(self.uname, svp, dvp)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.die(str(ex))
|
raise FSE(str(ex))
|
||||||
|
|
||||||
def chmod(self, path: str, mode: str) -> None:
|
def chmod(self, path: str, mode: str) -> None:
|
||||||
pass
|
pass
|
||||||
@@ -284,7 +324,10 @@ class FtpFs(AbstractedFS):
|
|||||||
try:
|
try:
|
||||||
ap = self.rv2a(path, r=True)[0]
|
ap = self.rv2a(path, r=True)[0]
|
||||||
return bos.stat(ap)
|
return bos.stat(ap)
|
||||||
except:
|
except FSE as ex:
|
||||||
|
if ex.severity:
|
||||||
|
raise
|
||||||
|
|
||||||
ap = self.rv2a(path)[0]
|
ap = self.rv2a(path)[0]
|
||||||
st = bos.stat(ap)
|
st = bos.stat(ap)
|
||||||
if not stat.S_ISDIR(st.st_mode):
|
if not stat.S_ISDIR(st.st_mode):
|
||||||
@@ -304,7 +347,10 @@ class FtpFs(AbstractedFS):
|
|||||||
try:
|
try:
|
||||||
st = self.stat(path)
|
st = self.stat(path)
|
||||||
return stat.S_ISREG(st.st_mode)
|
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()
|
return False # expected for mojibake in ftp_SIZE()
|
||||||
|
|
||||||
def islink(self, path: str) -> bool:
|
def islink(self, path: str) -> bool:
|
||||||
@@ -315,7 +361,10 @@ class FtpFs(AbstractedFS):
|
|||||||
try:
|
try:
|
||||||
st = self.stat(path)
|
st = self.stat(path)
|
||||||
return stat.S_ISDIR(st.st_mode)
|
return stat.S_ISDIR(st.st_mode)
|
||||||
except:
|
except Exception as ex:
|
||||||
|
if getattr(ex, "severity", 0):
|
||||||
|
raise
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def getsize(self, path: str) -> int:
|
def getsize(self, path: str) -> int:
|
||||||
@@ -344,10 +393,12 @@ class FtpHandler(FTPHandler):
|
|||||||
abstracted_fs = FtpFs
|
abstracted_fs = FtpFs
|
||||||
hub: "SvcHub"
|
hub: "SvcHub"
|
||||||
args: argparse.Namespace
|
args: argparse.Namespace
|
||||||
|
uname: str
|
||||||
|
|
||||||
def __init__(self, conn: Any, server: Any, ioloop: Any = None) -> None:
|
def __init__(self, conn: Any, server: Any, ioloop: Any = None) -> None:
|
||||||
self.hub: "SvcHub" = FtpHandler.hub
|
self.hub: "SvcHub" = FtpHandler.hub
|
||||||
self.args: argparse.Namespace = FtpHandler.args
|
self.args: argparse.Namespace = FtpHandler.args
|
||||||
|
self.uname = "*"
|
||||||
|
|
||||||
if PY2:
|
if PY2:
|
||||||
FTPHandler.__init__(self, conn, server, ioloop)
|
FTPHandler.__init__(self, conn, server, ioloop)
|
||||||
@@ -355,7 +406,16 @@ class FtpHandler(FTPHandler):
|
|||||||
super(FtpHandler, self).__init__(conn, server, ioloop)
|
super(FtpHandler, self).__init__(conn, server, ioloop)
|
||||||
|
|
||||||
cip = self.remote_ip
|
cip = self.remote_ip
|
||||||
self.cli_ip = cip[7:] if cip.startswith("::ffff:") else cip
|
if cip.startswith("::ffff:"):
|
||||||
|
cip = cip[7:]
|
||||||
|
|
||||||
|
if self.args.ftp_ipa_re and not self.args.ftp_ipa_re.match(cip):
|
||||||
|
logging.warning("client rejected (--ftp-ipa): %s", cip)
|
||||||
|
self.connected = False
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.cli_ip = cip
|
||||||
|
|
||||||
# abspath->vpath mapping to resolve log_transfer paths
|
# abspath->vpath mapping to resolve log_transfer paths
|
||||||
self.vfs_map: dict[str, str] = {}
|
self.vfs_map: dict[str, str] = {}
|
||||||
@@ -363,14 +423,10 @@ class FtpHandler(FTPHandler):
|
|||||||
# reduce non-debug logging
|
# reduce non-debug logging
|
||||||
self.log_cmds_list = [x for x in self.log_cmds_list if x not in ("CWD", "XCWD")]
|
self.log_cmds_list = [x for x in self.log_cmds_list if x not in ("CWD", "XCWD")]
|
||||||
|
|
||||||
def die(self, msg):
|
|
||||||
self.respond("550 {}".format(msg))
|
|
||||||
raise FilesystemError(msg)
|
|
||||||
|
|
||||||
def ftp_STOR(self, file: str, mode: str = "w") -> Any:
|
def ftp_STOR(self, file: str, mode: str = "w") -> Any:
|
||||||
# Optional[str]
|
# Optional[str]
|
||||||
vp = join(self.fs.cwd, file).lstrip("/")
|
vp = join(self.fs.cwd, file).lstrip("/")
|
||||||
ap, vfs, rem = self.fs.v2a(vp)
|
ap, vfs, rem = self.fs.v2a(vp, w=True)
|
||||||
self.vfs_map[ap] = vp
|
self.vfs_map[ap] = vp
|
||||||
xbu = vfs.flags.get("xbu")
|
xbu = vfs.flags.get("xbu")
|
||||||
if xbu and not runhook(
|
if xbu and not runhook(
|
||||||
@@ -379,14 +435,14 @@ class FtpHandler(FTPHandler):
|
|||||||
ap,
|
ap,
|
||||||
vfs.canonical(rem),
|
vfs.canonical(rem),
|
||||||
"",
|
"",
|
||||||
self.username,
|
self.uname,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
self.cli_ip,
|
self.cli_ip,
|
||||||
0,
|
0,
|
||||||
"",
|
"",
|
||||||
):
|
):
|
||||||
self.die("Upload blocked by xbu server config")
|
raise FSE("Upload blocked by xbu server config")
|
||||||
|
|
||||||
# print("ftp_STOR: {} {} => {}".format(vp, mode, ap))
|
# print("ftp_STOR: {} {} => {}".format(vp, mode, ap))
|
||||||
ret = FTPHandler.ftp_STOR(self, file, mode)
|
ret = FTPHandler.ftp_STOR(self, file, mode)
|
||||||
@@ -408,7 +464,7 @@ class FtpHandler(FTPHandler):
|
|||||||
# print("xfer_end: {} => {}".format(ap, vp))
|
# print("xfer_end: {} => {}".format(ap, vp))
|
||||||
if vp:
|
if vp:
|
||||||
vp, fn = os.path.split(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)
|
vfs, rem = vfs.get_dbv(rem)
|
||||||
self.hub.up2k.hash_file(
|
self.hub.up2k.hash_file(
|
||||||
vfs.realpath,
|
vfs.realpath,
|
||||||
@@ -418,7 +474,7 @@ class FtpHandler(FTPHandler):
|
|||||||
fn,
|
fn,
|
||||||
self.cli_ip,
|
self.cli_ip,
|
||||||
time.time(),
|
time.time(),
|
||||||
self.username,
|
self.uname,
|
||||||
)
|
)
|
||||||
|
|
||||||
return FTPHandler.log_transfer(
|
return FTPHandler.log_transfer(
|
||||||
@@ -452,7 +508,7 @@ class Ftpd(object):
|
|||||||
print(t.format(pybin))
|
print(t.format(pybin))
|
||||||
sys.exit(1)
|
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_control_required = True
|
||||||
h1.tls_data_required = True
|
h1.tls_data_required = True
|
||||||
|
|
||||||
@@ -460,9 +516,9 @@ class Ftpd(object):
|
|||||||
|
|
||||||
for h_lp in hs:
|
for h_lp in hs:
|
||||||
h2, lp = h_lp
|
h2, lp = h_lp
|
||||||
h2.hub = hub
|
FtpHandler.hub = h2.hub = hub
|
||||||
h2.args = hub.args
|
FtpHandler.args = h2.args = hub.args
|
||||||
h2.authorizer = FtpAuth(hub)
|
FtpHandler.authorizer = h2.authorizer = FtpAuth(hub)
|
||||||
|
|
||||||
if self.args.ftp_pr:
|
if self.args.ftp_pr:
|
||||||
p1, p2 = [int(x) for x in self.args.ftp_pr.split("-")]
|
p1, p2 = [int(x) for x in self.args.ftp_pr.split("-")]
|
||||||
@@ -486,6 +542,9 @@ class Ftpd(object):
|
|||||||
if "::" in ips:
|
if "::" in ips:
|
||||||
ips.append("0.0.0.0")
|
ips.append("0.0.0.0")
|
||||||
|
|
||||||
|
if self.args.ftp4:
|
||||||
|
ips = [x for x in ips if ":" not in x]
|
||||||
|
|
||||||
ioloop = IOLoop()
|
ioloop = IOLoop()
|
||||||
for ip in ips:
|
for ip in ips:
|
||||||
for h, lp in hs:
|
for h, lp in hs:
|
||||||
|
|||||||
1223
copyparty/httpcli.py
1223
copyparty/httpcli.py
File diff suppressed because it is too large
Load Diff
@@ -50,11 +50,10 @@ class HttpConn(object):
|
|||||||
self.addr = addr
|
self.addr = addr
|
||||||
self.hsrv = hsrv
|
self.hsrv = hsrv
|
||||||
|
|
||||||
self.mutex: threading.Lock = hsrv.mutex # mypy404
|
self.u2mutex: threading.Lock = hsrv.u2mutex # mypy404
|
||||||
self.args: argparse.Namespace = hsrv.args # mypy404
|
self.args: argparse.Namespace = hsrv.args # mypy404
|
||||||
self.E: EnvParams = self.args.E
|
self.E: EnvParams = self.args.E
|
||||||
self.asrv: AuthSrv = hsrv.asrv # mypy404
|
self.asrv: AuthSrv = hsrv.asrv # mypy404
|
||||||
self.cert_path = hsrv.cert_path
|
|
||||||
self.u2fh: Util.FHC = hsrv.u2fh # mypy404
|
self.u2fh: Util.FHC = hsrv.u2fh # mypy404
|
||||||
self.iphash: HMaccas = hsrv.broker.iphash
|
self.iphash: HMaccas = hsrv.broker.iphash
|
||||||
self.bans: dict[str, int] = hsrv.bans
|
self.bans: dict[str, int] = hsrv.bans
|
||||||
@@ -94,7 +93,7 @@ class HttpConn(object):
|
|||||||
self.rproxy = ip
|
self.rproxy = ip
|
||||||
|
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.log_src = "{} \033[{}m{}".format(ip, color, self.addr[1]).ljust(26)
|
self.log_src = ("%s \033[%dm%d" % (ip, color, self.addr[1])).ljust(26)
|
||||||
return self.log_src
|
return self.log_src
|
||||||
|
|
||||||
def respath(self, res_name: str) -> str:
|
def respath(self, res_name: str) -> str:
|
||||||
@@ -113,32 +112,30 @@ class HttpConn(object):
|
|||||||
return self.u2idx
|
return self.u2idx
|
||||||
|
|
||||||
def _detect_https(self) -> bool:
|
def _detect_https(self) -> bool:
|
||||||
method = None
|
try:
|
||||||
if self.cert_path:
|
method = self.s.recv(4, socket.MSG_PEEK)
|
||||||
try:
|
except socket.timeout:
|
||||||
method = self.s.recv(4, socket.MSG_PEEK)
|
return False
|
||||||
except socket.timeout:
|
except AttributeError:
|
||||||
return False
|
# jython does not support msg_peek; forget about https
|
||||||
except AttributeError:
|
method = self.s.recv(4)
|
||||||
# jython does not support msg_peek; forget about https
|
self.sr = Util.Unrecv(self.s, self.log)
|
||||||
method = self.s.recv(4)
|
self.sr.buf = method
|
||||||
self.sr = Util.Unrecv(self.s, self.log)
|
|
||||||
self.sr.buf = method
|
|
||||||
|
|
||||||
# jython used to do this, they stopped since it's broken
|
# jython used to do this, they stopped since it's broken
|
||||||
# but reimplementing sendall is out of scope for now
|
# but reimplementing sendall is out of scope for now
|
||||||
if not getattr(self.s, "sendall", None):
|
if not getattr(self.s, "sendall", None):
|
||||||
self.s.sendall = self.s.send # type: ignore
|
self.s.sendall = self.s.send # type: ignore
|
||||||
|
|
||||||
if len(method) != 4:
|
if len(method) != 4:
|
||||||
err = "need at least 4 bytes in the first packet; got {}".format(
|
err = "need at least 4 bytes in the first packet; got {}".format(
|
||||||
len(method)
|
len(method)
|
||||||
)
|
)
|
||||||
if method:
|
if method:
|
||||||
self.log(err)
|
self.log(err)
|
||||||
|
|
||||||
self.s.send(b"HTTP/1.1 400 Bad Request\r\n\r\n" + err.encode("utf-8"))
|
self.s.send(b"HTTP/1.1 400 Bad Request\r\n\r\n" + err.encode("utf-8"))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return not method or not bool(PTN_HTTP.match(method))
|
return not method or not bool(PTN_HTTP.match(method))
|
||||||
|
|
||||||
@@ -148,7 +145,7 @@ class HttpConn(object):
|
|||||||
self.sr = None
|
self.sr = None
|
||||||
if self.args.https_only:
|
if self.args.https_only:
|
||||||
is_https = True
|
is_https = True
|
||||||
elif self.args.http_only or not HAVE_SSL:
|
elif self.args.http_only:
|
||||||
is_https = False
|
is_https = False
|
||||||
else:
|
else:
|
||||||
# raise Exception("asdf")
|
# raise Exception("asdf")
|
||||||
@@ -162,7 +159,7 @@ class HttpConn(object):
|
|||||||
self.log_src = self.log_src.replace("[36m", "[35m")
|
self.log_src = self.log_src.replace("[36m", "[35m")
|
||||||
try:
|
try:
|
||||||
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
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:
|
if self.args.ssl_ver:
|
||||||
ctx.options &= ~self.args.ssl_flags_en
|
ctx.options &= ~self.args.ssl_flags_en
|
||||||
ctx.options |= self.args.ssl_flags_de
|
ctx.options |= self.args.ssl_flags_de
|
||||||
@@ -179,7 +176,7 @@ class HttpConn(object):
|
|||||||
|
|
||||||
self.s = ctx.wrap_socket(self.s, server_side=True)
|
self.s = ctx.wrap_socket(self.s, server_side=True)
|
||||||
msg = [
|
msg = [
|
||||||
"\033[1;3{:d}m{}".format(c, s)
|
"\033[1;3%dm%s" % (c, s)
|
||||||
for c, s in zip([0, 5, 0], self.s.cipher()) # type: ignore
|
for c, s in zip([0, 5, 0], self.s.cipher()) # type: ignore
|
||||||
]
|
]
|
||||||
self.log(" ".join(msg) + "\033[0m")
|
self.log(" ".join(msg) + "\033[0m")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals
|
|||||||
import base64
|
import base64
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
@@ -33,13 +34,29 @@ except MNFE:
|
|||||||
* (try another python version, if you have one)
|
* (try another python version, if you have one)
|
||||||
* (try copyparty.sfx instead)
|
* (try copyparty.sfx instead)
|
||||||
""".format(
|
""".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)
|
sys.exit(1)
|
||||||
|
|
||||||
from .bos import bos
|
|
||||||
from .httpconn import HttpConn
|
from .httpconn import HttpConn
|
||||||
|
from .metrics import Metrics
|
||||||
from .u2idx import U2idx
|
from .u2idx import U2idx
|
||||||
from .util import (
|
from .util import (
|
||||||
E_SCK,
|
E_SCK,
|
||||||
@@ -49,6 +66,7 @@ from .util import (
|
|||||||
Magician,
|
Magician,
|
||||||
Netdev,
|
Netdev,
|
||||||
NetMap,
|
NetMap,
|
||||||
|
absreal,
|
||||||
ipnorm,
|
ipnorm,
|
||||||
min_ex,
|
min_ex,
|
||||||
shut_socket,
|
shut_socket,
|
||||||
@@ -82,18 +100,24 @@ class HttpSrv(object):
|
|||||||
# redefine in case of multiprocessing
|
# redefine in case of multiprocessing
|
||||||
socket.setdefaulttimeout(120)
|
socket.setdefaulttimeout(120)
|
||||||
|
|
||||||
|
self.t0 = time.time()
|
||||||
nsuf = "-n{}-i{:x}".format(nid, os.getpid()) if nid else ""
|
nsuf = "-n{}-i{:x}".format(nid, os.getpid()) if nid else ""
|
||||||
self.magician = Magician()
|
self.magician = Magician()
|
||||||
self.nm = NetMap([], {})
|
self.nm = NetMap([], {})
|
||||||
self.ssdp: Optional["SSDPr"] = None
|
self.ssdp: Optional["SSDPr"] = None
|
||||||
self.gpwd = Garda(self.args.ban_pw)
|
self.gpwd = Garda(self.args.ban_pw)
|
||||||
self.g404 = Garda(self.args.ban_404)
|
self.g404 = Garda(self.args.ban_404)
|
||||||
|
self.g403 = Garda(self.args.ban_403)
|
||||||
|
self.g422 = Garda(self.args.ban_422, False)
|
||||||
|
self.gmal = Garda(self.args.ban_422)
|
||||||
|
self.gurl = Garda(self.args.ban_url)
|
||||||
self.bans: dict[str, int] = {}
|
self.bans: dict[str, int] = {}
|
||||||
self.aclose: dict[str, int] = {}
|
self.aclose: dict[str, int] = {}
|
||||||
|
|
||||||
self.bound: set[tuple[str, int]] = set()
|
self.bound: set[tuple[str, int]] = set()
|
||||||
self.name = "hsrv" + nsuf
|
self.name = "hsrv" + nsuf
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
|
self.u2mutex = threading.Lock()
|
||||||
self.stopping = False
|
self.stopping = False
|
||||||
|
|
||||||
self.tp_nthr = 0 # actual
|
self.tp_nthr = 0 # actual
|
||||||
@@ -105,6 +129,10 @@ class HttpSrv(object):
|
|||||||
self.t_periodic: Optional[threading.Thread] = None
|
self.t_periodic: Optional[threading.Thread] = None
|
||||||
|
|
||||||
self.u2fh = FHC()
|
self.u2fh = FHC()
|
||||||
|
self.metrics = Metrics(self)
|
||||||
|
self.nreq = 0
|
||||||
|
self.nsus = 0
|
||||||
|
self.nban = 0
|
||||||
self.srvs: list[socket.socket] = []
|
self.srvs: list[socket.socket] = []
|
||||||
self.ncli = 0 # exact
|
self.ncli = 0 # exact
|
||||||
self.clients: set[HttpConn] = set() # laggy
|
self.clients: set[HttpConn] = set() # laggy
|
||||||
@@ -122,6 +150,12 @@ class HttpSrv(object):
|
|||||||
zs = os.path.join(self.E.mod, "web", "deps", "prism.js.gz")
|
zs = os.path.join(self.E.mod, "web", "deps", "prism.js.gz")
|
||||||
self.prism = os.path.exists(zs)
|
self.prism = os.path.exists(zs)
|
||||||
|
|
||||||
|
self.statics: set[str] = set()
|
||||||
|
self._build_statics()
|
||||||
|
|
||||||
|
self.ptn_cc = re.compile(r"[\x00-\x1f]")
|
||||||
|
self.ptn_hsafe = re.compile(r"[\x00-\x1f<>\"'&]")
|
||||||
|
|
||||||
self.mallow = "GET HEAD POST PUT DELETE OPTIONS".split()
|
self.mallow = "GET HEAD POST PUT DELETE OPTIONS".split()
|
||||||
if not self.args.no_dav:
|
if not self.args.no_dav:
|
||||||
zs = "PROPFIND PROPPATCH LOCK UNLOCK MKCOL COPY MOVE"
|
zs = "PROPFIND PROPPATCH LOCK UNLOCK MKCOL COPY MOVE"
|
||||||
@@ -132,12 +166,6 @@ class HttpSrv(object):
|
|||||||
|
|
||||||
self.ssdp = SSDPr(broker)
|
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:
|
if self.tp_q:
|
||||||
self.start_threads(4)
|
self.start_threads(4)
|
||||||
|
|
||||||
@@ -148,7 +176,7 @@ class HttpSrv(object):
|
|||||||
if self.args.log_thrs:
|
if self.args.log_thrs:
|
||||||
start_log_thrs(self.log, self.args.log_thrs, nid)
|
start_log_thrs(self.log, self.args.log_thrs, nid)
|
||||||
|
|
||||||
self.th_cfg: dict[str, Any] = {}
|
self.th_cfg: dict[str, set[str]] = {}
|
||||||
Daemon(self.post_init, "hsrv-init2")
|
Daemon(self.post_init, "hsrv-init2")
|
||||||
|
|
||||||
def post_init(self) -> None:
|
def post_init(self) -> None:
|
||||||
@@ -158,6 +186,14 @@ class HttpSrv(object):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _build_statics(self) -> None:
|
||||||
|
for dp, _, df in os.walk(os.path.join(self.E.mod, "web")):
|
||||||
|
for fn in df:
|
||||||
|
ap = absreal(os.path.join(dp, fn))
|
||||||
|
self.statics.add(ap)
|
||||||
|
if ap.endswith(".gz") or ap.endswith(".br"):
|
||||||
|
self.statics.add(ap[:-3])
|
||||||
|
|
||||||
def set_netdevs(self, netdevs: dict[str, Netdev]) -> None:
|
def set_netdevs(self, netdevs: dict[str, Netdev]) -> None:
|
||||||
ips = set()
|
ips = set()
|
||||||
for ip, _ in self.bound:
|
for ip, _ in self.bound:
|
||||||
@@ -185,7 +221,7 @@ class HttpSrv(object):
|
|||||||
def periodic(self) -> None:
|
def periodic(self) -> None:
|
||||||
while True:
|
while True:
|
||||||
time.sleep(2 if self.tp_ncli or self.ncli else 10)
|
time.sleep(2 if self.tp_ncli or self.ncli else 10)
|
||||||
with self.mutex:
|
with self.u2mutex, self.mutex:
|
||||||
self.u2fh.clean()
|
self.u2fh.clean()
|
||||||
if self.tp_q:
|
if self.tp_q:
|
||||||
self.tp_ncli = max(self.ncli, self.tp_ncli - 2)
|
self.tp_ncli = max(self.ncli, self.tp_ncli - 2)
|
||||||
@@ -331,7 +367,7 @@ class HttpSrv(object):
|
|||||||
if not self.t_periodic:
|
if not self.t_periodic:
|
||||||
name = "hsrv-pt"
|
name = "hsrv-pt"
|
||||||
if self.nid:
|
if self.nid:
|
||||||
name += "-{}".format(self.nid)
|
name += "-%d" % (self.nid,)
|
||||||
|
|
||||||
self.t_periodic = Daemon(self.periodic, name)
|
self.t_periodic = Daemon(self.periodic, name)
|
||||||
|
|
||||||
@@ -350,7 +386,7 @@ class HttpSrv(object):
|
|||||||
|
|
||||||
Daemon(
|
Daemon(
|
||||||
self.thr_client,
|
self.thr_client,
|
||||||
"httpconn-{}-{}".format(addr[0].split(".", 2)[-1][-6:], addr[1]),
|
"httpconn-%s-%d" % (addr[0].split(".", 2)[-1][-6:], addr[1]),
|
||||||
(sck, addr),
|
(sck, addr),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -367,9 +403,7 @@ class HttpSrv(object):
|
|||||||
try:
|
try:
|
||||||
sck, addr = task
|
sck, addr = task
|
||||||
me = threading.current_thread()
|
me = threading.current_thread()
|
||||||
me.name = "httpconn-{}-{}".format(
|
me.name = "httpconn-%s-%d" % (addr[0].split(".", 2)[-1][-6:], addr[1])
|
||||||
addr[0].split(".", 2)[-1][-6:], addr[1]
|
|
||||||
)
|
|
||||||
self.thr_client(sck, addr)
|
self.thr_client(sck, addr)
|
||||||
me.name = self.name + "-poolw"
|
me.name = self.name + "-poolw"
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ from __future__ import print_function, unicode_literals
|
|||||||
import argparse # typechk
|
import argparse # typechk
|
||||||
import colorsys
|
import colorsys
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import re
|
||||||
|
|
||||||
from .__init__ import PY2
|
from .__init__ import PY2
|
||||||
from .th_srv import HAVE_PIL
|
from .th_srv import HAVE_PIL, HAVE_PILF
|
||||||
from .util import BytesIO
|
from .util import BytesIO # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class Ico(object):
|
class Ico(object):
|
||||||
@@ -17,54 +18,79 @@ class Ico(object):
|
|||||||
def get(self, ext: str, as_thumb: bool, chrome: bool) -> tuple[str, bytes]:
|
def get(self, ext: str, as_thumb: bool, chrome: bool) -> tuple[str, bytes]:
|
||||||
"""placeholder to make thumbnails not break"""
|
"""placeholder to make thumbnails not break"""
|
||||||
|
|
||||||
zb = hashlib.sha1(ext.encode("utf-8")).digest()[2:4]
|
bext = ext.encode("ascii", "replace")
|
||||||
|
ext = bext.decode("utf-8")
|
||||||
|
zb = hashlib.sha1(bext).digest()[2:4]
|
||||||
if PY2:
|
if PY2:
|
||||||
zb = [ord(x) for x in zb]
|
zb = [ord(x) for x in zb] # type: ignore
|
||||||
|
|
||||||
c1 = colorsys.hsv_to_rgb(zb[0] / 256.0, 1, 0.3)
|
c1 = colorsys.hsv_to_rgb(zb[0] / 256.0, 1, 0.3)
|
||||||
c2 = colorsys.hsv_to_rgb(zb[0] / 256.0, 1, 1)
|
c2 = colorsys.hsv_to_rgb(zb[0] / 256.0, 0.8 if HAVE_PILF else 1, 1)
|
||||||
ci = [int(x * 255) for x in list(c1) + list(c2)]
|
ci = [int(x * 255) for x in list(c1) + list(c2)]
|
||||||
c = "".join(["{:02x}".format(x) for x in ci])
|
c = "".join(["%02x" % (x,) for x in ci])
|
||||||
|
|
||||||
w = 100
|
w = 100
|
||||||
h = 30
|
h = 30
|
||||||
if not self.args.th_no_crop and as_thumb:
|
if not self.args.th_no_crop and as_thumb:
|
||||||
sw, sh = self.args.th_size.split("x")
|
sw, sh = self.args.th_size.split("x")
|
||||||
h = int(100 / (float(sw) / float(sh)))
|
h = int(100.0 / (float(sw) / float(sh)))
|
||||||
w = 100
|
w = 100
|
||||||
|
|
||||||
if chrome and as_thumb:
|
if chrome:
|
||||||
# cannot handle more than ~2000 unique SVGs
|
# cannot handle more than ~2000 unique SVGs
|
||||||
|
if HAVE_PILF:
|
||||||
|
# pillow 10.1 made this the default font;
|
||||||
|
# svg: 3.7s, this: 36s
|
||||||
|
try:
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
# [.lt] are hard to see lowercase / unspaced
|
||||||
|
ext2 = re.sub("(.)", "\\1 ", ext).upper()
|
||||||
|
|
||||||
|
h = int(128.0 * h / w)
|
||||||
|
w = 128
|
||||||
|
img = Image.new("RGB", (w, h), "#" + c[:6])
|
||||||
|
pb = ImageDraw.Draw(img)
|
||||||
|
_, _, tw, th = pb.textbbox((0, 0), ext2, font_size=16)
|
||||||
|
xy = (int((w - tw) / 2), int((h - th) / 2))
|
||||||
|
pb.text(xy, ext2, fill="#" + c[6:], font_size=16)
|
||||||
|
|
||||||
|
img = img.resize((w * 2, h * 2), Image.NEAREST)
|
||||||
|
|
||||||
|
buf = BytesIO()
|
||||||
|
img.save(buf, format="PNG", compress_level=1)
|
||||||
|
return "image/png", buf.getvalue()
|
||||||
|
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
if HAVE_PIL:
|
if HAVE_PIL:
|
||||||
# svg: 3s, cache: 6s, this: 8s
|
# svg: 3s, cache: 6s, this: 8s
|
||||||
from PIL import Image, ImageDraw
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
h = int(64 * h / w)
|
h = int(64.0 * h / w)
|
||||||
w = 64
|
w = 64
|
||||||
img = Image.new("RGB", (w, h), "#" + c[:6])
|
img = Image.new("RGB", (w, h), "#" + c[:6])
|
||||||
pb = ImageDraw.Draw(img)
|
pb = ImageDraw.Draw(img)
|
||||||
tw, th = pb.textsize(ext)
|
try:
|
||||||
pb.text(((w - tw) // 2, (h - th) // 2), ext, fill="#" + c[6:])
|
_, _, tw, th = pb.textbbox((0, 0), ext)
|
||||||
|
except:
|
||||||
|
tw, th = pb.textsize(ext)
|
||||||
|
|
||||||
|
tw += len(ext)
|
||||||
|
cw = tw // len(ext)
|
||||||
|
x = ((w - tw) // 2) - (cw * 2) // 3
|
||||||
|
fill = "#" + c[6:]
|
||||||
|
for ch in ext:
|
||||||
|
pb.text((x, (h - th) // 2), " %s " % (ch,), fill=fill)
|
||||||
|
x += cw
|
||||||
|
|
||||||
img = img.resize((w * 3, h * 3), Image.NEAREST)
|
img = img.resize((w * 3, h * 3), Image.NEAREST)
|
||||||
|
|
||||||
buf = BytesIO()
|
buf = BytesIO()
|
||||||
img.save(buf, format="PNG", compress_level=1)
|
img.save(buf, format="PNG", compress_level=1)
|
||||||
return "image/png", buf.getvalue()
|
return "image/png", buf.getvalue()
|
||||||
|
|
||||||
elif False:
|
|
||||||
# 48s, too slow
|
|
||||||
import pyvips
|
|
||||||
|
|
||||||
h = int(192 * h / w)
|
|
||||||
w = 192
|
|
||||||
img = pyvips.Image.text(
|
|
||||||
ext, width=w, height=h, dpi=192, align=pyvips.Align.CENTRE
|
|
||||||
)
|
|
||||||
img = img.ifthenelse(ci[3:], ci[:3], blend=True)
|
|
||||||
# i = i.resize(3, kernel=pyvips.Kernel.NEAREST)
|
|
||||||
buf = img.write_to_buffer(".png[compression=1]")
|
|
||||||
return "image/png", buf
|
|
||||||
|
|
||||||
svg = """\
|
svg = """\
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg version="1.1" viewBox="0 0 100 {}" xmlns="http://www.w3.org/2000/svg"><g>
|
<svg version="1.1" viewBox="0 0 100 {}" xmlns="http://www.w3.org/2000/svg"><g>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import errno
|
||||||
import random
|
import random
|
||||||
import select
|
import select
|
||||||
import socket
|
import socket
|
||||||
@@ -277,12 +278,26 @@ class MDNS(MCast):
|
|||||||
zf = time.time() + 2
|
zf = time.time() + 2
|
||||||
self.probing = zf # cant unicast so give everyone an extra sec
|
self.probing = zf # cant unicast so give everyone an extra sec
|
||||||
self.unsolicited = [zf, zf + 1, zf + 3, zf + 7] # rfc-8.3
|
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()
|
last_hop = time.time()
|
||||||
ihop = self.args.mc_hop
|
ihop = self.args.mc_hop
|
||||||
while self.running:
|
while self.running:
|
||||||
timeout = (
|
timeout = (
|
||||||
0.02 + random.random() * 0.07
|
0.02 + random.random() * 0.07
|
||||||
if self.probing or self.q or self.defend or self.unsolicited
|
if self.probing or self.q or self.defend
|
||||||
|
else max(0.05, self.unsolicited[0] - time.time())
|
||||||
|
if self.unsolicited
|
||||||
else (last_hop + ihop if ihop else 180)
|
else (last_hop + ihop if ihop else 180)
|
||||||
)
|
)
|
||||||
rdy = select.select(self.srv, [], [], timeout)
|
rdy = select.select(self.srv, [], [], timeout)
|
||||||
@@ -314,8 +329,6 @@ class MDNS(MCast):
|
|||||||
self.log(t.format(self.hn[:-1]), 2)
|
self.log(t.format(self.hn[:-1]), 2)
|
||||||
self.probing = 0
|
self.probing = 0
|
||||||
|
|
||||||
self.log("stopped", 2)
|
|
||||||
|
|
||||||
def stop(self, panic=False) -> None:
|
def stop(self, panic=False) -> None:
|
||||||
self.running = False
|
self.running = False
|
||||||
for srv in self.srv.values():
|
for srv in self.srv.values():
|
||||||
@@ -502,6 +515,10 @@ class MDNS(MCast):
|
|||||||
for srv in self.srv.values():
|
for srv in self.srv.values():
|
||||||
tx.add(srv)
|
tx.add(srv)
|
||||||
|
|
||||||
|
if not self.unsolicited and self.args.zm_spam:
|
||||||
|
zf = time.time() + self.args.zm_spam + random.random() * 0.07
|
||||||
|
self.unsolicited.append(zf)
|
||||||
|
|
||||||
for srv, deadline in list(self.defend.items()):
|
for srv, deadline in list(self.defend.items()):
|
||||||
if now < deadline:
|
if now < deadline:
|
||||||
continue
|
continue
|
||||||
|
|||||||
233
copyparty/metrics.py
Normal file
233
copyparty/metrics.py
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
from .__init__ import TYPE_CHECKING
|
||||||
|
from .util import Pebkac, get_df, unhumanize
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .httpcli import HttpCli
|
||||||
|
from .httpsrv import HttpSrv
|
||||||
|
|
||||||
|
|
||||||
|
class Metrics(object):
|
||||||
|
def __init__(self, hsrv: "HttpSrv") -> None:
|
||||||
|
self.hsrv = hsrv
|
||||||
|
|
||||||
|
def tx(self, cli: "HttpCli") -> bool:
|
||||||
|
if not cli.avol:
|
||||||
|
raise Pebkac(403, "not allowed for user " + cli.uname)
|
||||||
|
|
||||||
|
args = cli.args
|
||||||
|
if not args.stats:
|
||||||
|
raise Pebkac(403, "the stats feature is not enabled in server config")
|
||||||
|
|
||||||
|
conn = cli.conn
|
||||||
|
vfs = conn.asrv.vfs
|
||||||
|
allvols = list(sorted(vfs.all_vols.items()))
|
||||||
|
|
||||||
|
idx = conn.get_u2idx()
|
||||||
|
if not idx or not hasattr(idx, "p_end"):
|
||||||
|
idx = None
|
||||||
|
|
||||||
|
ret: list[str] = []
|
||||||
|
|
||||||
|
def addc(k: str, v: str, desc: str) -> None:
|
||||||
|
zs = "# TYPE %s counter\n# HELP %s %s\n%s_created %s\n%s_total %s"
|
||||||
|
ret.append(zs % (k, k, desc, k, int(self.hsrv.t0), k, v))
|
||||||
|
|
||||||
|
def adduc(k: str, unit: str, v: str, desc: str) -> None:
|
||||||
|
k += "_" + unit
|
||||||
|
zs = "# TYPE %s counter\n# UNIT %s %s\n# HELP %s %s\n%s_created %s\n%s_total %s"
|
||||||
|
ret.append(zs % (k, k, unit, k, desc, k, int(self.hsrv.t0), k, v))
|
||||||
|
|
||||||
|
def addg(k: str, v: str, desc: str) -> None:
|
||||||
|
zs = "# TYPE %s gauge\n# HELP %s %s\n%s %s"
|
||||||
|
ret.append(zs % (k, k, desc, k, v))
|
||||||
|
|
||||||
|
def addug(k: str, unit: str, v: str, desc: str) -> None:
|
||||||
|
k += "_" + unit
|
||||||
|
zs = "# TYPE %s gauge\n# UNIT %s %s\n# HELP %s %s\n%s %s"
|
||||||
|
ret.append(zs % (k, k, unit, k, desc, k, v))
|
||||||
|
|
||||||
|
def addh(k: str, typ: str, desc: str) -> None:
|
||||||
|
zs = "# TYPE %s %s\n# HELP %s %s"
|
||||||
|
ret.append(zs % (k, typ, k, desc))
|
||||||
|
|
||||||
|
def addbh(k: str, desc: str) -> None:
|
||||||
|
zs = "# TYPE %s gauge\n# UNIT %s bytes\n# HELP %s %s"
|
||||||
|
ret.append(zs % (k, k, k, desc))
|
||||||
|
|
||||||
|
def addv(k: str, v: str) -> None:
|
||||||
|
ret.append("%s %s" % (k, v))
|
||||||
|
|
||||||
|
t = "time since last copyparty restart"
|
||||||
|
v = "{:.3f}".format(time.time() - self.hsrv.t0)
|
||||||
|
addug("cpp_uptime", "seconds", v, t)
|
||||||
|
|
||||||
|
# timestamps are gauges because initial value is not zero
|
||||||
|
t = "unixtime of last copyparty restart"
|
||||||
|
v = "{:.3f}".format(self.hsrv.t0)
|
||||||
|
addug("cpp_boot_unixtime", "seconds", v, t)
|
||||||
|
|
||||||
|
t = "number of open http(s) client connections"
|
||||||
|
addg("cpp_http_conns", str(self.hsrv.ncli), t)
|
||||||
|
|
||||||
|
t = "number of http(s) requests since last restart"
|
||||||
|
addc("cpp_http_reqs", str(self.hsrv.nreq), t)
|
||||||
|
|
||||||
|
t = "number of 403/422/malicious reqs since restart"
|
||||||
|
addc("cpp_sus_reqs", str(self.hsrv.nsus), t)
|
||||||
|
|
||||||
|
v = str(len(conn.bans or []))
|
||||||
|
addg("cpp_active_bans", v, "number of currently banned IPs")
|
||||||
|
|
||||||
|
t = "number of IPs banned since last restart"
|
||||||
|
addg("cpp_total_bans", str(self.hsrv.nban), t)
|
||||||
|
|
||||||
|
if not args.nos_vst:
|
||||||
|
x = self.hsrv.broker.ask("up2k.get_state")
|
||||||
|
vs = json.loads(x.get())
|
||||||
|
|
||||||
|
nvidle = 0
|
||||||
|
nvbusy = 0
|
||||||
|
nvoffline = 0
|
||||||
|
for v in vs["volstate"].values():
|
||||||
|
if v == "online, idle":
|
||||||
|
nvidle += 1
|
||||||
|
elif "OFFLINE" in v:
|
||||||
|
nvoffline += 1
|
||||||
|
else:
|
||||||
|
nvbusy += 1
|
||||||
|
|
||||||
|
addg("cpp_idle_vols", str(nvidle), "number of idle/ready volumes")
|
||||||
|
addg("cpp_busy_vols", str(nvbusy), "number of busy/indexing volumes")
|
||||||
|
addg("cpp_offline_vols", str(nvoffline), "number of offline volumes")
|
||||||
|
|
||||||
|
t = "time since last database activity (upload/rename/delete)"
|
||||||
|
addug("cpp_db_idle", "seconds", str(vs["dbwt"]), t)
|
||||||
|
|
||||||
|
t = "unixtime of last database activity (upload/rename/delete)"
|
||||||
|
addug("cpp_db_act", "seconds", str(vs["dbwu"]), t)
|
||||||
|
|
||||||
|
t = "number of files queued for hashing/indexing"
|
||||||
|
addg("cpp_hashing_files", str(vs["hashq"]), t)
|
||||||
|
|
||||||
|
t = "number of files queued for metadata scanning"
|
||||||
|
addg("cpp_tagq_files", str(vs["tagq"]), t)
|
||||||
|
|
||||||
|
try:
|
||||||
|
t = "number of files queued for plugin-based analysis"
|
||||||
|
addg("cpp_mtpq_files", str(int(vs["mtpq"])), t)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not args.nos_hdd:
|
||||||
|
addbh("cpp_disk_size_bytes", "total HDD size of volume")
|
||||||
|
addbh("cpp_disk_free_bytes", "free HDD space in volume")
|
||||||
|
for vpath, vol in allvols:
|
||||||
|
free, total = get_df(vol.realpath)
|
||||||
|
if free is None or total is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
addv('cpp_disk_size_bytes{vol="/%s"}' % (vpath), str(total))
|
||||||
|
addv('cpp_disk_free_bytes{vol="/%s"}' % (vpath), str(free))
|
||||||
|
|
||||||
|
if idx and not args.nos_vol:
|
||||||
|
addbh("cpp_vol_bytes", "num bytes of data in volume")
|
||||||
|
addh("cpp_vol_files", "gauge", "num files in volume")
|
||||||
|
addbh("cpp_vol_free_bytes", "free space (vmaxb) in volume")
|
||||||
|
addh("cpp_vol_free_files", "gauge", "free space (vmaxn) in volume")
|
||||||
|
tnbytes = 0
|
||||||
|
tnfiles = 0
|
||||||
|
|
||||||
|
volsizes = []
|
||||||
|
try:
|
||||||
|
ptops = [x.realpath for _, x in allvols]
|
||||||
|
x = self.hsrv.broker.ask("up2k.get_volsizes", ptops)
|
||||||
|
volsizes = x.get()
|
||||||
|
except Exception as ex:
|
||||||
|
cli.log("tx_stats get_volsizes: {!r}".format(ex), 3)
|
||||||
|
|
||||||
|
for (vpath, vol), (nbytes, nfiles) in zip(allvols, volsizes):
|
||||||
|
tnbytes += nbytes
|
||||||
|
tnfiles += nfiles
|
||||||
|
addv('cpp_vol_bytes{vol="/%s"}' % (vpath), str(nbytes))
|
||||||
|
addv('cpp_vol_files{vol="/%s"}' % (vpath), str(nfiles))
|
||||||
|
|
||||||
|
if vol.flags.get("vmaxb") or vol.flags.get("vmaxn"):
|
||||||
|
|
||||||
|
zi = unhumanize(vol.flags.get("vmaxb") or "0")
|
||||||
|
if zi:
|
||||||
|
v = str(zi - nbytes)
|
||||||
|
addv('cpp_vol_free_bytes{vol="/%s"}' % (vpath), v)
|
||||||
|
|
||||||
|
zi = unhumanize(vol.flags.get("vmaxn") or "0")
|
||||||
|
if zi:
|
||||||
|
v = str(zi - nfiles)
|
||||||
|
addv('cpp_vol_free_files{vol="/%s"}' % (vpath), v)
|
||||||
|
|
||||||
|
if volsizes:
|
||||||
|
addv('cpp_vol_bytes{vol="total"}', str(tnbytes))
|
||||||
|
addv('cpp_vol_files{vol="total"}', str(tnfiles))
|
||||||
|
|
||||||
|
if idx and not args.nos_dup:
|
||||||
|
addbh("cpp_dupe_bytes", "num dupe bytes in volume")
|
||||||
|
addh("cpp_dupe_files", "gauge", "num dupe files in volume")
|
||||||
|
tnbytes = 0
|
||||||
|
tnfiles = 0
|
||||||
|
for vpath, vol in allvols:
|
||||||
|
cur = idx.get_cur(vol.realpath)
|
||||||
|
if not cur:
|
||||||
|
continue
|
||||||
|
|
||||||
|
nbytes = 0
|
||||||
|
nfiles = 0
|
||||||
|
q = "select sz, count(*)-1 c from up group by w having c"
|
||||||
|
for sz, c in cur.execute(q):
|
||||||
|
nbytes += sz * c
|
||||||
|
nfiles += c
|
||||||
|
|
||||||
|
tnbytes += nbytes
|
||||||
|
tnfiles += nfiles
|
||||||
|
addv('cpp_dupe_bytes{vol="/%s"}' % (vpath), str(nbytes))
|
||||||
|
addv('cpp_dupe_files{vol="/%s"}' % (vpath), str(nfiles))
|
||||||
|
|
||||||
|
addv('cpp_dupe_bytes{vol="total"}', str(tnbytes))
|
||||||
|
addv('cpp_dupe_files{vol="total"}', str(tnfiles))
|
||||||
|
|
||||||
|
if not args.nos_unf:
|
||||||
|
addbh("cpp_unf_bytes", "incoming/unfinished uploads (num bytes)")
|
||||||
|
addh("cpp_unf_files", "gauge", "incoming/unfinished uploads (num files)")
|
||||||
|
tnbytes = 0
|
||||||
|
tnfiles = 0
|
||||||
|
try:
|
||||||
|
x = self.hsrv.broker.ask("up2k.get_unfinished")
|
||||||
|
xs = x.get()
|
||||||
|
xj = json.loads(xs)
|
||||||
|
for ptop, (nbytes, nfiles) in xj.items():
|
||||||
|
tnbytes += nbytes
|
||||||
|
tnfiles += nfiles
|
||||||
|
vol = next((x[1] for x in allvols if x[1].realpath == ptop), None)
|
||||||
|
if not vol:
|
||||||
|
t = "tx_stats get_unfinished: could not map {}"
|
||||||
|
cli.log(t.format(ptop), 3)
|
||||||
|
continue
|
||||||
|
|
||||||
|
addv('cpp_unf_bytes{vol="/%s"}' % (vol.vpath), str(nbytes))
|
||||||
|
addv('cpp_unf_files{vol="/%s"}' % (vol.vpath), str(nfiles))
|
||||||
|
|
||||||
|
addv('cpp_unf_bytes{vol="total"}', str(tnbytes))
|
||||||
|
addv('cpp_unf_files{vol="total"}', str(tnfiles))
|
||||||
|
|
||||||
|
except Exception as ex:
|
||||||
|
cli.log("tx_stats get_unfinished: {!r}".format(ex), 3)
|
||||||
|
|
||||||
|
ret.append("# EOF")
|
||||||
|
|
||||||
|
mime = "application/openmetrics-text; version=1.0.0; charset=utf-8"
|
||||||
|
mime = cli.uparam.get("mime") or mime
|
||||||
|
cli.reply("\n".join(ret).encode("utf-8"), mime=mime)
|
||||||
|
return True
|
||||||
@@ -8,7 +8,7 @@ import shutil
|
|||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from .__init__ import EXE, PY2, WINDOWS, E, unicode
|
from .__init__ import ANYWIN, EXE, PY2, WINDOWS, E, unicode
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
from .util import (
|
from .util import (
|
||||||
FFMPEG_URL,
|
FFMPEG_URL,
|
||||||
@@ -29,6 +29,9 @@ if True: # pylint: disable=using-constant-test
|
|||||||
|
|
||||||
|
|
||||||
def have_ff(scmd: str) -> bool:
|
def have_ff(scmd: str) -> bool:
|
||||||
|
if ANYWIN:
|
||||||
|
scmd += ".exe"
|
||||||
|
|
||||||
if PY2:
|
if PY2:
|
||||||
print("# checking {}".format(scmd))
|
print("# checking {}".format(scmd))
|
||||||
acmd = (scmd + " -version").encode("ascii").split(b" ")
|
acmd = (scmd + " -version").encode("ascii").split(b" ")
|
||||||
@@ -115,7 +118,7 @@ def ffprobe(
|
|||||||
b"--",
|
b"--",
|
||||||
fsenc(abspath),
|
fsenc(abspath),
|
||||||
]
|
]
|
||||||
rc, so, se = runcmd(cmd, timeout=timeout)
|
rc, so, se = runcmd(cmd, timeout=timeout, nice=True, oom=200)
|
||||||
retchk(rc, cmd, se)
|
retchk(rc, cmd, se)
|
||||||
return parse_ffprobe(so)
|
return parse_ffprobe(so)
|
||||||
|
|
||||||
@@ -237,7 +240,7 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
|
|||||||
if "/" in fps:
|
if "/" in fps:
|
||||||
fa, fb = fps.split("/")
|
fa, fb = fps.split("/")
|
||||||
try:
|
try:
|
||||||
fps = int(fa) * 1.0 / int(fb)
|
fps = float(fa) / float(fb)
|
||||||
except:
|
except:
|
||||||
fps = 9001
|
fps = 9001
|
||||||
|
|
||||||
@@ -258,7 +261,8 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
|
|||||||
if ".resw" in ret and ".resh" in ret:
|
if ".resw" in ret and ".resh" in ret:
|
||||||
ret["res"] = "{}x{}".format(ret[".resw"], ret[".resh"])
|
ret["res"] = "{}x{}".format(ret[".resw"], ret[".resh"])
|
||||||
|
|
||||||
zd = {k: (0, v) for k, v in ret.items()}
|
zero = int("0")
|
||||||
|
zd = {k: (zero, v) for k, v in ret.items()}
|
||||||
|
|
||||||
return zd, md
|
return zd, md
|
||||||
|
|
||||||
@@ -559,6 +563,8 @@ class MTag(object):
|
|||||||
|
|
||||||
args = {
|
args = {
|
||||||
"env": env,
|
"env": env,
|
||||||
|
"nice": True,
|
||||||
|
"oom": 300,
|
||||||
"timeout": parser.timeout,
|
"timeout": parser.timeout,
|
||||||
"kill": parser.kill,
|
"kill": parser.kill,
|
||||||
"capture": parser.capture,
|
"capture": parser.capture,
|
||||||
@@ -569,11 +575,6 @@ class MTag(object):
|
|||||||
zd.update(ret)
|
zd.update(ret)
|
||||||
args["sin"] = json.dumps(zd).encode("utf-8", "replace")
|
args["sin"] = json.dumps(zd).encode("utf-8", "replace")
|
||||||
|
|
||||||
if WINDOWS:
|
|
||||||
args["creationflags"] = 0x4000
|
|
||||||
else:
|
|
||||||
cmd = ["nice"] + cmd
|
|
||||||
|
|
||||||
bcmd = [sfsenc(x) for x in cmd[:-1]] + [fsenc(cmd[-1])]
|
bcmd = [sfsenc(x) for x in cmd[:-1]] + [fsenc(cmd[-1])]
|
||||||
rc, v, err = runcmd(bcmd, **args) # type: ignore
|
rc, v, err = runcmd(bcmd, **args) # type: ignore
|
||||||
retchk(rc, bcmd, err, self.log, 5, self.args.mtag_v)
|
retchk(rc, bcmd, err, self.log, 5, self.args.mtag_v)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from ipaddress import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .__init__ import MACOS, TYPE_CHECKING
|
from .__init__ import MACOS, TYPE_CHECKING
|
||||||
from .util import Netdev, find_prefix, min_ex, spack
|
from .util import Daemon, Netdev, find_prefix, min_ex, spack
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .svchub import SvcHub
|
from .svchub import SvcHub
|
||||||
@@ -228,6 +228,7 @@ class MCast(object):
|
|||||||
for srv in self.srv.values():
|
for srv in self.srv.values():
|
||||||
assert srv.ip in self.sips
|
assert srv.ip in self.sips
|
||||||
|
|
||||||
|
Daemon(self.hopper, "mc-hop")
|
||||||
return bound
|
return bound
|
||||||
|
|
||||||
def setup_socket(self, srv: MC_Sck) -> None:
|
def setup_socket(self, srv: MC_Sck) -> None:
|
||||||
@@ -299,33 +300,57 @@ class MCast(object):
|
|||||||
t = "failed to set IPv4 TTL/LOOP; announcements may not survive multiple switches/routers"
|
t = "failed to set IPv4 TTL/LOOP; announcements may not survive multiple switches/routers"
|
||||||
self.log(t, 3)
|
self.log(t, 3)
|
||||||
|
|
||||||
self.hop(srv)
|
if self.hop(srv, False):
|
||||||
|
self.log("igmp was already joined?? chilling for a sec", 3)
|
||||||
|
time.sleep(1.2)
|
||||||
|
|
||||||
|
self.hop(srv, True)
|
||||||
self.b4.sort(reverse=True)
|
self.b4.sort(reverse=True)
|
||||||
self.b6.sort(reverse=True)
|
self.b6.sort(reverse=True)
|
||||||
|
|
||||||
def hop(self, srv: MC_Sck) -> None:
|
def hop(self, srv: MC_Sck, on: bool) -> bool:
|
||||||
"""rejoin to keepalive on routers/switches without igmp-snooping"""
|
"""rejoin to keepalive on routers/switches without igmp-snooping"""
|
||||||
sck = srv.sck
|
sck = srv.sck
|
||||||
req = srv.mreq
|
req = srv.mreq
|
||||||
if ":" in srv.ip:
|
if ":" in srv.ip:
|
||||||
try:
|
if not on:
|
||||||
sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_LEAVE_GROUP, req)
|
try:
|
||||||
# linux does leaves/joins twice with 0.2~1.05s spacing
|
sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_LEAVE_GROUP, req)
|
||||||
time.sleep(1.2)
|
return True
|
||||||
except:
|
except:
|
||||||
pass
|
return False
|
||||||
|
else:
|
||||||
sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, req)
|
sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, req)
|
||||||
else:
|
else:
|
||||||
try:
|
if not on:
|
||||||
sck.setsockopt(socket.IPPROTO_IP, socket.IP_DROP_MEMBERSHIP, req)
|
try:
|
||||||
time.sleep(1.2)
|
sck.setsockopt(socket.IPPROTO_IP, socket.IP_DROP_MEMBERSHIP, req)
|
||||||
except:
|
return True
|
||||||
pass
|
except:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# t = "joining {} from ip {} idx {} with mreq {}"
|
||||||
|
# self.log(t.format(srv.grp, srv.ip, srv.idx, repr(srv.mreq)), 6)
|
||||||
|
sck.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, req)
|
||||||
|
|
||||||
# t = "joining {} from ip {} idx {} with mreq {}"
|
return True
|
||||||
# self.log(t.format(srv.grp, srv.ip, srv.idx, repr(srv.mreq)), 6)
|
|
||||||
sck.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, req)
|
def hopper(self):
|
||||||
|
while self.args.mc_hop and self.running:
|
||||||
|
time.sleep(self.args.mc_hop)
|
||||||
|
if not self.running:
|
||||||
|
return
|
||||||
|
|
||||||
|
for srv in self.srv.values():
|
||||||
|
self.hop(srv, False)
|
||||||
|
|
||||||
|
# linux does leaves/joins twice with 0.2~1.05s spacing
|
||||||
|
time.sleep(1.2)
|
||||||
|
if not self.running:
|
||||||
|
return
|
||||||
|
|
||||||
|
for srv in self.srv.values():
|
||||||
|
self.hop(srv, True)
|
||||||
|
|
||||||
def map_client(self, cip: str) -> Optional[MC_Sck]:
|
def map_client(self, cip: str) -> Optional[MC_Sck]:
|
||||||
try:
|
try:
|
||||||
|
|||||||
149
copyparty/pwhash.py
Normal file
149
copyparty/pwhash.py
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from .__init__ import unicode
|
||||||
|
|
||||||
|
|
||||||
|
class PWHash(object):
|
||||||
|
def __init__(self, args: argparse.Namespace):
|
||||||
|
self.args = args
|
||||||
|
|
||||||
|
try:
|
||||||
|
alg, ac = args.ah_alg.split(",")
|
||||||
|
except:
|
||||||
|
alg = args.ah_alg
|
||||||
|
ac = {}
|
||||||
|
|
||||||
|
if alg == "none":
|
||||||
|
alg = ""
|
||||||
|
|
||||||
|
self.alg = alg
|
||||||
|
self.ac = ac
|
||||||
|
if not alg:
|
||||||
|
self.on = False
|
||||||
|
self.hash = unicode
|
||||||
|
return
|
||||||
|
|
||||||
|
self.on = True
|
||||||
|
self.salt = args.ah_salt.encode("utf-8")
|
||||||
|
self.cache: dict[str, str] = {}
|
||||||
|
self.mutex = threading.Lock()
|
||||||
|
self.hash = self._cache_hash
|
||||||
|
|
||||||
|
if alg == "sha2":
|
||||||
|
self._hash = self._gen_sha2
|
||||||
|
elif alg == "scrypt":
|
||||||
|
self._hash = self._gen_scrypt
|
||||||
|
elif alg == "argon2":
|
||||||
|
self._hash = self._gen_argon2
|
||||||
|
else:
|
||||||
|
t = "unsupported password hashing algorithm [{}], must be one of these: argon2 scrypt sha2 none"
|
||||||
|
raise Exception(t.format(alg))
|
||||||
|
|
||||||
|
def _cache_hash(self, plain: str) -> str:
|
||||||
|
with self.mutex:
|
||||||
|
try:
|
||||||
|
return self.cache[plain]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not plain:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if len(plain) > 255:
|
||||||
|
raise Exception("password too long")
|
||||||
|
|
||||||
|
if len(self.cache) > 9000:
|
||||||
|
self.cache = {}
|
||||||
|
|
||||||
|
ret = self._hash(plain)
|
||||||
|
self.cache[plain] = ret
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def _gen_sha2(self, plain: str) -> str:
|
||||||
|
its = int(self.ac[0]) if self.ac else 424242
|
||||||
|
bplain = plain.encode("utf-8")
|
||||||
|
ret = b"\n"
|
||||||
|
for _ in range(its):
|
||||||
|
ret = hashlib.sha512(self.salt + bplain + ret).digest()
|
||||||
|
|
||||||
|
return "+" + base64.urlsafe_b64encode(ret[:24]).decode("utf-8")
|
||||||
|
|
||||||
|
def _gen_scrypt(self, plain: str) -> str:
|
||||||
|
cost = 2 << 13
|
||||||
|
its = 2
|
||||||
|
blksz = 8
|
||||||
|
para = 4
|
||||||
|
try:
|
||||||
|
cost = 2 << int(self.ac[0])
|
||||||
|
its = int(self.ac[1])
|
||||||
|
blksz = int(self.ac[2])
|
||||||
|
para = int(self.ac[3])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ret = plain.encode("utf-8")
|
||||||
|
for _ in range(its):
|
||||||
|
ret = hashlib.scrypt(ret, salt=self.salt, n=cost, r=blksz, p=para, dklen=24)
|
||||||
|
|
||||||
|
return "+" + base64.urlsafe_b64encode(ret).decode("utf-8")
|
||||||
|
|
||||||
|
def _gen_argon2(self, plain: str) -> str:
|
||||||
|
from argon2.low_level import Type as ArgonType
|
||||||
|
from argon2.low_level import hash_secret
|
||||||
|
|
||||||
|
time_cost = 3
|
||||||
|
mem_cost = 256
|
||||||
|
parallelism = 4
|
||||||
|
version = 19
|
||||||
|
try:
|
||||||
|
time_cost = int(self.ac[0])
|
||||||
|
mem_cost = int(self.ac[1])
|
||||||
|
parallelism = int(self.ac[2])
|
||||||
|
version = int(self.ac[3])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
bplain = plain.encode("utf-8")
|
||||||
|
|
||||||
|
bret = hash_secret(
|
||||||
|
secret=bplain,
|
||||||
|
salt=self.salt,
|
||||||
|
time_cost=time_cost,
|
||||||
|
memory_cost=mem_cost * 1024,
|
||||||
|
parallelism=parallelism,
|
||||||
|
hash_len=24,
|
||||||
|
type=ArgonType.ID,
|
||||||
|
version=version,
|
||||||
|
)
|
||||||
|
ret = bret.split(b"$")[-1].decode("utf-8")
|
||||||
|
return "+" + ret.replace("/", "_").replace("+", "-")
|
||||||
|
|
||||||
|
def stdin(self) -> None:
|
||||||
|
while True:
|
||||||
|
ln = sys.stdin.readline().strip()
|
||||||
|
if not ln:
|
||||||
|
break
|
||||||
|
print(self.hash(ln))
|
||||||
|
|
||||||
|
def cli(self) -> None:
|
||||||
|
import getpass
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
p1 = getpass.getpass("password> ")
|
||||||
|
p2 = getpass.getpass("again or just hit ENTER> ")
|
||||||
|
except EOFError:
|
||||||
|
return
|
||||||
|
|
||||||
|
if p2 and p1 != p2:
|
||||||
|
print("\033[31minputs don't match; try again\033[0m", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
print(self.hash(p1))
|
||||||
|
print()
|
||||||
0
copyparty/res/__init__.py
Normal file
0
copyparty/res/__init__.py
Normal file
@@ -32,6 +32,8 @@ class SMB(object):
|
|||||||
self.asrv = hub.asrv
|
self.asrv = hub.asrv
|
||||||
self.log = hub.log
|
self.log = hub.log
|
||||||
self.files: dict[int, tuple[float, str]] = {}
|
self.files: dict[int, tuple[float, str]] = {}
|
||||||
|
self.noacc = self.args.smba
|
||||||
|
self.accs = not self.args.smba
|
||||||
|
|
||||||
lg.setLevel(logging.DEBUG if self.args.smbvvv else logging.INFO)
|
lg.setLevel(logging.DEBUG if self.args.smbvvv else logging.INFO)
|
||||||
for x in ["impacket", "impacket.smbserver"]:
|
for x in ["impacket", "impacket.smbserver"]:
|
||||||
@@ -94,6 +96,14 @@ class SMB(object):
|
|||||||
|
|
||||||
port = int(self.args.smb_port)
|
port = int(self.args.smb_port)
|
||||||
srv = smbserver.SimpleSMBServer(listenAddress=ip, listenPort=port)
|
srv = smbserver.SimpleSMBServer(listenAddress=ip, listenPort=port)
|
||||||
|
try:
|
||||||
|
if self.accs:
|
||||||
|
srv.setAuthCallback(self._auth_cb)
|
||||||
|
except:
|
||||||
|
self.accs = False
|
||||||
|
self.noacc = True
|
||||||
|
t = "impacket too old; access permissions will not work! all accounts are admin!"
|
||||||
|
self.log("smb", t, 1)
|
||||||
|
|
||||||
ro = "no" if self.args.smbw else "yes" # (does nothing)
|
ro = "no" if self.args.smbw else "yes" # (does nothing)
|
||||||
srv.addShare("A", "/", readOnly=ro)
|
srv.addShare("A", "/", readOnly=ro)
|
||||||
@@ -119,24 +129,74 @@ class SMB(object):
|
|||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
Daemon(self.srv.start)
|
Daemon(self.srv.start)
|
||||||
|
|
||||||
def _v2a(self, caller: str, vpath: str, *a: Any) -> tuple[VFS, str]:
|
def _auth_cb(self, *a, **ka):
|
||||||
|
debug("auth-result: %s %s", a, ka)
|
||||||
|
conndata = ka["connData"]
|
||||||
|
auth_ok = conndata["Authenticated"]
|
||||||
|
uname = ka["user_name"] if auth_ok else "*"
|
||||||
|
uname = self.asrv.iacct.get(uname, uname) or "*"
|
||||||
|
oldname = conndata.get("partygoer", "*") or "*"
|
||||||
|
cli_ip = conndata["ClientIP"]
|
||||||
|
cli_hn = ka["host_name"]
|
||||||
|
if uname != "*":
|
||||||
|
conndata["partygoer"] = uname
|
||||||
|
info("client %s [%s] authed as %s", cli_ip, cli_hn, uname)
|
||||||
|
elif oldname != "*":
|
||||||
|
info("client %s [%s] keeping old auth as %s", cli_ip, cli_hn, oldname)
|
||||||
|
elif auth_ok:
|
||||||
|
info("client %s [%s] authed as [*] (anon)", cli_ip, cli_hn)
|
||||||
|
else:
|
||||||
|
info("client %s [%s] rejected", cli_ip, cli_hn)
|
||||||
|
|
||||||
|
def _uname(self) -> str:
|
||||||
|
if self.noacc:
|
||||||
|
return LEELOO_DALLAS
|
||||||
|
|
||||||
|
try:
|
||||||
|
# you found it! my single worst bit of code so far
|
||||||
|
# (if you can think of a better way to track users through impacket i'm all ears)
|
||||||
|
cf0 = inspect.currentframe().f_back.f_back
|
||||||
|
cf = cf0.f_back
|
||||||
|
for n in range(3):
|
||||||
|
cl = cf.f_locals
|
||||||
|
if "connData" in cl:
|
||||||
|
return cl["connData"]["partygoer"]
|
||||||
|
cf = cf.f_back
|
||||||
|
raise Exception()
|
||||||
|
except:
|
||||||
|
warning(
|
||||||
|
"nyoron... %s <<-- %s <<-- %s <<-- %s",
|
||||||
|
cf0.f_code.co_name,
|
||||||
|
cf0.f_back.f_code.co_name,
|
||||||
|
cf0.f_back.f_back.f_code.co_name,
|
||||||
|
cf0.f_back.f_back.f_back.f_code.co_name,
|
||||||
|
)
|
||||||
|
return "*"
|
||||||
|
|
||||||
|
def _v2a(
|
||||||
|
self, caller: str, vpath: str, *a: Any, uname="", perms=None
|
||||||
|
) -> tuple[VFS, str]:
|
||||||
vpath = vpath.replace("\\", "/").lstrip("/")
|
vpath = vpath.replace("\\", "/").lstrip("/")
|
||||||
# cf = inspect.currentframe().f_back
|
# cf = inspect.currentframe().f_back
|
||||||
# c1 = cf.f_back.f_code.co_name
|
# c1 = cf.f_back.f_code.co_name
|
||||||
# c2 = cf.f_code.co_name
|
# c2 = cf.f_code.co_name
|
||||||
debug('%s("%s", %s)\033[K\033[0m', caller, vpath, str(a))
|
if not uname:
|
||||||
|
uname = self._uname()
|
||||||
|
if not perms:
|
||||||
|
perms = [True, True]
|
||||||
|
|
||||||
# TODO find a way to grab `identity` in smbComSessionSetupAndX and smb2SessionSetup
|
debug('%s("%s", %s) %s @%s\033[K\033[0m', caller, vpath, str(a), perms, uname)
|
||||||
vfs, rem = self.asrv.vfs.get(vpath, LEELOO_DALLAS, True, True)
|
vfs, rem = self.asrv.vfs.get(vpath, uname, *perms)
|
||||||
return vfs, vfs.canonical(rem)
|
return vfs, vfs.canonical(rem)
|
||||||
|
|
||||||
def _listdir(self, vpath: str, *a: Any, **ka: Any) -> list[str]:
|
def _listdir(self, vpath: str, *a: Any, **ka: Any) -> list[str]:
|
||||||
vpath = vpath.replace("\\", "/").lstrip("/")
|
vpath = vpath.replace("\\", "/").lstrip("/")
|
||||||
# caller = inspect.currentframe().f_back.f_code.co_name
|
# caller = inspect.currentframe().f_back.f_code.co_name
|
||||||
debug('listdir("%s", %s)\033[K\033[0m', vpath, str(a))
|
uname = self._uname()
|
||||||
vfs, rem = self.asrv.vfs.get(vpath, LEELOO_DALLAS, False, False)
|
# debug('listdir("%s", %s) @%s\033[K\033[0m', vpath, str(a), uname)
|
||||||
|
vfs, rem = self.asrv.vfs.get(vpath, uname, False, False)
|
||||||
_, vfs_ls, vfs_virt = vfs.ls(
|
_, vfs_ls, vfs_virt = vfs.ls(
|
||||||
rem, LEELOO_DALLAS, not self.args.no_scandir, [[False, False]]
|
rem, uname, not self.args.no_scandir, [[False, False]]
|
||||||
)
|
)
|
||||||
dirs = [x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]
|
dirs = [x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]
|
||||||
fils = [x[0] for x in vfs_ls if x[0] not in dirs]
|
fils = [x[0] for x in vfs_ls if x[0] not in dirs]
|
||||||
@@ -149,8 +209,8 @@ class SMB(object):
|
|||||||
sz = 112 * 2 # ['.', '..']
|
sz = 112 * 2 # ['.', '..']
|
||||||
for n, fn in enumerate(ls):
|
for n, fn in enumerate(ls):
|
||||||
if sz >= 64000:
|
if sz >= 64000:
|
||||||
t = "listing only %d of %d files (%d byte); see impacket#1433"
|
t = "listing only %d of %d files (%d byte) in /%s; see impacket#1433"
|
||||||
warning(t, n, len(ls), sz)
|
warning(t, n, len(ls), sz, vpath)
|
||||||
break
|
break
|
||||||
|
|
||||||
nsz = len(fn.encode("utf-16", "replace"))
|
nsz = len(fn.encode("utf-16", "replace"))
|
||||||
@@ -171,10 +231,12 @@ class SMB(object):
|
|||||||
if wr and not self.args.smbw:
|
if wr and not self.args.smbw:
|
||||||
yeet("blocked write (no --smbw): " + vpath)
|
yeet("blocked write (no --smbw): " + vpath)
|
||||||
|
|
||||||
vfs, ap = self._v2a("open", vpath, *a)
|
uname = self._uname()
|
||||||
|
vfs, ap = self._v2a("open", vpath, *a, uname=uname, perms=[True, wr])
|
||||||
if wr:
|
if wr:
|
||||||
if not vfs.axs.uwrite:
|
if not vfs.axs.uwrite:
|
||||||
yeet("blocked write (no-write-acc): " + vpath)
|
t = "blocked write (no-write-acc %s): /%s @%s"
|
||||||
|
yeet(t % (vfs.axs.uwrite, vpath, uname))
|
||||||
|
|
||||||
xbu = vfs.flags.get("xbu")
|
xbu = vfs.flags.get("xbu")
|
||||||
if xbu and not runhook(
|
if xbu and not runhook(
|
||||||
@@ -204,7 +266,7 @@ class SMB(object):
|
|||||||
|
|
||||||
_, vp = self.files.pop(fd)
|
_, vp = self.files.pop(fd)
|
||||||
vp, fn = os.path.split(vp)
|
vp, fn = os.path.split(vp)
|
||||||
vfs, rem = self.hub.asrv.vfs.get(vp, LEELOO_DALLAS, False, True)
|
vfs, rem = self.hub.asrv.vfs.get(vp, self._uname(), False, True)
|
||||||
vfs, rem = vfs.get_dbv(rem)
|
vfs, rem = vfs.get_dbv(rem)
|
||||||
self.hub.up2k.hash_file(
|
self.hub.up2k.hash_file(
|
||||||
vfs.realpath,
|
vfs.realpath,
|
||||||
@@ -224,15 +286,18 @@ class SMB(object):
|
|||||||
vp1 = vp1.lstrip("/")
|
vp1 = vp1.lstrip("/")
|
||||||
vp2 = vp2.lstrip("/")
|
vp2 = vp2.lstrip("/")
|
||||||
|
|
||||||
vfs2, ap2 = self._v2a("rename", vp2, vp1)
|
uname = self._uname()
|
||||||
|
vfs2, ap2 = self._v2a("rename", vp2, vp1, uname=uname)
|
||||||
if not vfs2.axs.uwrite:
|
if not vfs2.axs.uwrite:
|
||||||
yeet("blocked rename (no-write-acc): " + vp2)
|
t = "blocked write (no-write-acc %s): /%s @%s"
|
||||||
|
yeet(t % (vfs2.axs.uwrite, vp2, uname))
|
||||||
|
|
||||||
vfs1, _ = self.asrv.vfs.get(vp1, LEELOO_DALLAS, True, True)
|
vfs1, _ = self.asrv.vfs.get(vp1, uname, True, True, True)
|
||||||
if not vfs1.axs.umove:
|
if not vfs1.axs.umove:
|
||||||
yeet("blocked rename (no-move-acc): " + vp1)
|
t = "blocked rename (no-move-acc %s): /%s @%s"
|
||||||
|
yeet(t % (vfs1.axs.umove, vp1, uname))
|
||||||
|
|
||||||
self.hub.up2k.handle_mv(LEELOO_DALLAS, vp1, vp2)
|
self.hub.up2k.handle_mv(uname, vp1, vp2)
|
||||||
try:
|
try:
|
||||||
bos.makedirs(ap2)
|
bos.makedirs(ap2)
|
||||||
except:
|
except:
|
||||||
@@ -242,52 +307,74 @@ class SMB(object):
|
|||||||
if not self.args.smbw:
|
if not self.args.smbw:
|
||||||
yeet("blocked mkdir (no --smbw): " + vpath)
|
yeet("blocked mkdir (no --smbw): " + vpath)
|
||||||
|
|
||||||
vfs, ap = self._v2a("mkdir", vpath)
|
uname = self._uname()
|
||||||
|
vfs, ap = self._v2a("mkdir", vpath, uname=uname)
|
||||||
if not vfs.axs.uwrite:
|
if not vfs.axs.uwrite:
|
||||||
yeet("blocked mkdir (no-write-acc): " + vpath)
|
t = "blocked mkdir (no-write-acc %s): /%s @%s"
|
||||||
|
yeet(t % (vfs.axs.uwrite, vpath, uname))
|
||||||
|
|
||||||
return bos.mkdir(ap)
|
return bos.mkdir(ap)
|
||||||
|
|
||||||
def _stat(self, vpath: str, *a: Any, **ka: Any) -> os.stat_result:
|
def _stat(self, vpath: str, *a: Any, **ka: Any) -> os.stat_result:
|
||||||
return bos.stat(self._v2a("stat", vpath, *a)[1], *a, **ka)
|
try:
|
||||||
|
ap = self._v2a("stat", vpath, *a, perms=[True, False])[1]
|
||||||
|
ret = bos.stat(ap, *a, **ka)
|
||||||
|
# debug(" `-stat:ok")
|
||||||
|
return ret
|
||||||
|
except:
|
||||||
|
# white lie: windows freaks out if we raise due to an offline volume
|
||||||
|
# debug(" `-stat:NOPE (faking a directory)")
|
||||||
|
ts = int(time.time())
|
||||||
|
return os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, ts, ts, ts))
|
||||||
|
|
||||||
def _unlink(self, vpath: str) -> None:
|
def _unlink(self, vpath: str) -> None:
|
||||||
if not self.args.smbw:
|
if not self.args.smbw:
|
||||||
yeet("blocked delete (no --smbw): " + vpath)
|
yeet("blocked delete (no --smbw): " + vpath)
|
||||||
|
|
||||||
# return bos.unlink(self._v2a("stat", vpath, *a)[1])
|
# return bos.unlink(self._v2a("stat", vpath, *a)[1])
|
||||||
vfs, ap = self._v2a("delete", vpath)
|
uname = self._uname()
|
||||||
|
vfs, ap = self._v2a(
|
||||||
|
"delete", vpath, uname=uname, perms=[True, False, False, True]
|
||||||
|
)
|
||||||
if not vfs.axs.udel:
|
if not vfs.axs.udel:
|
||||||
yeet("blocked delete (no-del-acc): " + vpath)
|
yeet("blocked delete (no-del-acc): " + vpath)
|
||||||
|
|
||||||
vpath = vpath.replace("\\", "/").lstrip("/")
|
vpath = vpath.replace("\\", "/").lstrip("/")
|
||||||
self.hub.up2k.handle_rm(LEELOO_DALLAS, "1.7.6.2", [vpath], [])
|
self.hub.up2k.handle_rm(uname, "1.7.6.2", [vpath], [], False)
|
||||||
|
|
||||||
def _utime(self, vpath: str, times: tuple[float, float]) -> None:
|
def _utime(self, vpath: str, times: tuple[float, float]) -> None:
|
||||||
if not self.args.smbw:
|
if not self.args.smbw:
|
||||||
yeet("blocked utime (no --smbw): " + vpath)
|
yeet("blocked utime (no --smbw): " + vpath)
|
||||||
|
|
||||||
vfs, ap = self._v2a("utime", vpath)
|
uname = self._uname()
|
||||||
|
vfs, ap = self._v2a("utime", vpath, uname=uname)
|
||||||
if not vfs.axs.uwrite:
|
if not vfs.axs.uwrite:
|
||||||
yeet("blocked utime (no-write-acc): " + vpath)
|
t = "blocked utime (no-write-acc %s): /%s @%s"
|
||||||
|
yeet(t % (vfs.axs.uwrite, vpath, uname))
|
||||||
|
|
||||||
return bos.utime(ap, times)
|
return bos.utime(ap, times)
|
||||||
|
|
||||||
def _p_exists(self, vpath: str) -> bool:
|
def _p_exists(self, vpath: str) -> bool:
|
||||||
|
# ap = "?"
|
||||||
try:
|
try:
|
||||||
bos.stat(self._v2a("p.exists", vpath)[1])
|
ap = self._v2a("p.exists", vpath, perms=[True, False])[1]
|
||||||
|
bos.stat(ap)
|
||||||
|
# debug(" `-exists((%s)->(%s)):ok", vpath, ap)
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
|
# debug(" `-exists((%s)->(%s)):NOPE", vpath, ap)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _p_getsize(self, vpath: str) -> int:
|
def _p_getsize(self, vpath: str) -> int:
|
||||||
st = bos.stat(self._v2a("p.getsize", vpath)[1])
|
st = bos.stat(self._v2a("p.getsize", vpath, perms=[True, False])[1])
|
||||||
return st.st_size
|
return st.st_size
|
||||||
|
|
||||||
def _p_isdir(self, vpath: str) -> bool:
|
def _p_isdir(self, vpath: str) -> bool:
|
||||||
try:
|
try:
|
||||||
st = bos.stat(self._v2a("p.isdir", vpath)[1])
|
st = bos.stat(self._v2a("p.isdir", vpath, perms=[True, False])[1])
|
||||||
return stat.S_ISDIR(st.st_mode)
|
ret = stat.S_ISDIR(st.st_mode)
|
||||||
|
# debug(" `-isdir:%s:%s", st.st_mode, ret)
|
||||||
|
return ret
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -319,6 +406,7 @@ class SMB(object):
|
|||||||
|
|
||||||
smbserver.os.path.abspath = self._hook
|
smbserver.os.path.abspath = self._hook
|
||||||
smbserver.os.path.expanduser = self._hook
|
smbserver.os.path.expanduser = self._hook
|
||||||
|
smbserver.os.path.expandvars = self._hook
|
||||||
smbserver.os.path.getatime = self._hook
|
smbserver.os.path.getatime = self._hook
|
||||||
smbserver.os.path.getctime = self._hook
|
smbserver.os.path.getctime = self._hook
|
||||||
smbserver.os.path.getmtime = self._hook
|
smbserver.os.path.getmtime = self._hook
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import errno
|
||||||
import re
|
import re
|
||||||
import select
|
import select
|
||||||
import socket
|
import socket
|
||||||
@@ -80,7 +81,7 @@ class SSDPr(object):
|
|||||||
ubase = "{}://{}:{}".format(proto, sip, sport)
|
ubase = "{}://{}:{}".format(proto, sip, sport)
|
||||||
zsl = self.args.zsl
|
zsl = self.args.zsl
|
||||||
url = zsl if "://" in zsl else ubase + "/" + zsl.lstrip("/")
|
url = zsl if "://" in zsl else ubase + "/" + zsl.lstrip("/")
|
||||||
name = "{} @ {}".format(self.args.doctitle, self.args.name)
|
name = self.args.doctitle
|
||||||
zs = zs.strip().format(c(ubase), c(url), c(name), c(self.args.zsid))
|
zs = zs.strip().format(c(ubase), c(url), c(name), c(self.args.zsid))
|
||||||
hc.reply(zs.encode("utf-8", "replace"))
|
hc.reply(zs.encode("utf-8", "replace"))
|
||||||
return False # close connectino
|
return False # close connectino
|
||||||
@@ -129,6 +130,17 @@ class SSDPd(MCast):
|
|||||||
srv.hport = hp
|
srv.hport = hp
|
||||||
|
|
||||||
self.log("listening")
|
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:
|
while self.running:
|
||||||
rdy = select.select(self.srv, [], [], self.args.z_chk or 180)
|
rdy = select.select(self.srv, [], [], self.args.z_chk or 180)
|
||||||
rx: list[socket.socket] = rdy[0] # type: ignore
|
rx: list[socket.socket] = rdy[0] # type: ignore
|
||||||
@@ -148,8 +160,6 @@ class SSDPd(MCast):
|
|||||||
)
|
)
|
||||||
self.log(t, 6)
|
self.log(t, 6)
|
||||||
|
|
||||||
self.log("stopped", 2)
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
self.running = False
|
self.running = False
|
||||||
for srv in self.srv.values():
|
for srv in self.srv.values():
|
||||||
@@ -204,7 +214,7 @@ CONFIGID.UPNP.ORG: 1
|
|||||||
srv.sck.sendto(zb, addr[:2])
|
srv.sck.sendto(zb, addr[:2])
|
||||||
|
|
||||||
if cip not in self.txc.c:
|
if cip not in self.txc.c:
|
||||||
self.log("{} [{}] --> {}".format(srv.name, srv.ip, cip), "6")
|
self.log("{} [{}] --> {}".format(srv.name, srv.ip, cip), 6)
|
||||||
|
|
||||||
self.txc.add(cip)
|
self.txc.add(cip)
|
||||||
self.txc.cln()
|
self.txc.cln()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
import stat
|
import stat
|
||||||
import tarfile
|
import tarfile
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ class StreamTar(StreamArc):
|
|||||||
self,
|
self,
|
||||||
log: "NamedLogger",
|
log: "NamedLogger",
|
||||||
fgen: Generator[dict[str, Any], None, None],
|
fgen: Generator[dict[str, Any], None, None],
|
||||||
|
cmp: str = "",
|
||||||
**kwargs: Any
|
**kwargs: Any
|
||||||
):
|
):
|
||||||
super(StreamTar, self).__init__(log, fgen)
|
super(StreamTar, self).__init__(log, fgen)
|
||||||
@@ -53,14 +55,41 @@ class StreamTar(StreamArc):
|
|||||||
self.qfile = QFile()
|
self.qfile = QFile()
|
||||||
self.errf: dict[str, Any] = {}
|
self.errf: dict[str, Any] = {}
|
||||||
|
|
||||||
# python 3.8 changed to PAX_FORMAT as default,
|
# python 3.8 changed to PAX_FORMAT as default;
|
||||||
# waste of space and don't care about the new features
|
# slower, bigger, and no particular advantage
|
||||||
fmt = tarfile.GNU_FORMAT
|
fmt = tarfile.GNU_FORMAT
|
||||||
self.tar = tarfile.open(fileobj=self.qfile, mode="w|", format=fmt) # type: ignore
|
if "pax" in cmp:
|
||||||
|
# unless a client asks for it (currently
|
||||||
|
# gnu-tar has wider support than pax-tar)
|
||||||
|
fmt = tarfile.PAX_FORMAT
|
||||||
|
cmp = re.sub(r"[^a-z0-9]*pax[^a-z0-9]*", "", cmp)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmp, zs = cmp.replace(":", ",").split(",")
|
||||||
|
lv = int(zs)
|
||||||
|
except:
|
||||||
|
lv = -1
|
||||||
|
|
||||||
|
arg = {"name": None, "fileobj": self.qfile, "mode": "w", "format": fmt}
|
||||||
|
if cmp == "gz":
|
||||||
|
fun = tarfile.TarFile.gzopen
|
||||||
|
arg["compresslevel"] = lv if lv >= 0 else 3
|
||||||
|
elif cmp == "bz2":
|
||||||
|
fun = tarfile.TarFile.bz2open
|
||||||
|
arg["compresslevel"] = lv if lv >= 0 else 2
|
||||||
|
elif cmp == "xz":
|
||||||
|
fun = tarfile.TarFile.xzopen
|
||||||
|
arg["preset"] = lv if lv >= 0 else 1
|
||||||
|
else:
|
||||||
|
fun = tarfile.open
|
||||||
|
arg["mode"] = "w|"
|
||||||
|
|
||||||
|
self.tar = fun(**arg)
|
||||||
|
|
||||||
Daemon(self._gen, "star-gen")
|
Daemon(self._gen, "star-gen")
|
||||||
|
|
||||||
def gen(self) -> Generator[Optional[bytes], None, None]:
|
def gen(self) -> Generator[Optional[bytes], None, None]:
|
||||||
|
buf = b""
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
buf = self.qfile.q.get()
|
buf = self.qfile.q.get()
|
||||||
@@ -72,6 +101,12 @@ class StreamTar(StreamArc):
|
|||||||
|
|
||||||
yield None
|
yield None
|
||||||
finally:
|
finally:
|
||||||
|
while buf:
|
||||||
|
try:
|
||||||
|
buf = self.qfile.q.get()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
if self.errf:
|
if self.errf:
|
||||||
bos.unlink(self.errf["ap"])
|
bos.unlink(self.errf["ap"])
|
||||||
|
|
||||||
@@ -101,6 +136,9 @@ class StreamTar(StreamArc):
|
|||||||
errors.append((f["vp"], f["err"]))
|
errors.append((f["vp"], f["err"]))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if self.stopped:
|
||||||
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.ser(f)
|
self.ser(f)
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class Adapter(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if True:
|
if True: # pylint: disable=using-constant-test
|
||||||
# Type of an IPv4 address (a string in "xxx.xxx.xxx.xxx" format)
|
# Type of an IPv4 address (a string in "xxx.xxx.xxx.xxx" format)
|
||||||
_IPv4Address = str
|
_IPv4Address = str
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .__init__ import CORES
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
|
from .th_cli import ThumbCli
|
||||||
|
from .util import UTC, vjoin
|
||||||
|
|
||||||
if True: # pylint: disable=using-constant-test
|
if True: # pylint: disable=using-constant-test
|
||||||
from typing import Any, Generator, Optional
|
from typing import Any, Generator, Optional
|
||||||
@@ -21,10 +25,78 @@ class StreamArc(object):
|
|||||||
):
|
):
|
||||||
self.log = log
|
self.log = log
|
||||||
self.fgen = fgen
|
self.fgen = fgen
|
||||||
|
self.stopped = False
|
||||||
|
|
||||||
def gen(self) -> Generator[Optional[bytes], None, None]:
|
def gen(self) -> Generator[Optional[bytes], None, None]:
|
||||||
raise Exception("override me")
|
raise Exception("override me")
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
self.stopped = True
|
||||||
|
|
||||||
|
|
||||||
|
def gfilter(
|
||||||
|
fgen: Generator[dict[str, Any], None, None],
|
||||||
|
thumbcli: ThumbCli,
|
||||||
|
uname: str,
|
||||||
|
vtop: str,
|
||||||
|
fmt: str,
|
||||||
|
) -> Generator[dict[str, Any], None, None]:
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
pend = []
|
||||||
|
with ThreadPoolExecutor(max_workers=CORES) as tp:
|
||||||
|
try:
|
||||||
|
for f in fgen:
|
||||||
|
task = tp.submit(enthumb, thumbcli, uname, vtop, f, fmt)
|
||||||
|
pend.append((task, f))
|
||||||
|
if pend[0][0].done() or len(pend) > CORES * 4:
|
||||||
|
task, f = pend.pop(0)
|
||||||
|
try:
|
||||||
|
f = task.result(600)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
yield f
|
||||||
|
|
||||||
|
for task, f in pend:
|
||||||
|
try:
|
||||||
|
f = task.result(600)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
yield f
|
||||||
|
except Exception as ex:
|
||||||
|
thumbcli.log("gfilter flushing ({})".format(ex))
|
||||||
|
for task, f in pend:
|
||||||
|
try:
|
||||||
|
task.result(600)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
thumbcli.log("gfilter flushed")
|
||||||
|
|
||||||
|
|
||||||
|
def enthumb(
|
||||||
|
thumbcli: ThumbCli, uname: str, vtop: str, f: dict[str, Any], fmt: str
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
rem = f["vp"]
|
||||||
|
ext = rem.rsplit(".", 1)[-1].lower()
|
||||||
|
if fmt == "opus" and ext in "aac|m4a|mp3|ogg|opus|wma".split("|"):
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
vp = vjoin(vtop, rem.split("/", 1)[1])
|
||||||
|
vn, rem = thumbcli.asrv.vfs.get(vp, uname, True, False)
|
||||||
|
dbv, vrem = vn.get_dbv(rem)
|
||||||
|
thp = thumbcli.get(dbv, vrem, f["st"].st_mtime, fmt)
|
||||||
|
if not thp:
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
ext = "jpg" if fmt == "j" else "webp" if fmt == "w" else fmt
|
||||||
|
sz = bos.path.getsize(thp)
|
||||||
|
st: os.stat_result = f["st"]
|
||||||
|
ts = st.st_mtime
|
||||||
|
f["ap"] = thp
|
||||||
|
f["vp"] = f["vp"].rsplit(".", 1)[0] + "." + ext
|
||||||
|
f["st"] = os.stat_result((st.st_mode, -1, -1, 1, 1000, 1000, sz, ts, ts, ts))
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
def errdesc(errors: list[tuple[str, str]]) -> tuple[dict[str, Any], list[str]]:
|
def errdesc(errors: list[tuple[str, str]]) -> tuple[dict[str, Any], list[str]]:
|
||||||
report = ["copyparty failed to add the following files to the archive:", ""]
|
report = ["copyparty failed to add the following files to the archive:", ""]
|
||||||
@@ -36,7 +108,7 @@ def errdesc(errors: list[tuple[str, str]]) -> tuple[dict[str, Any], list[str]]:
|
|||||||
tf_path = tf.name
|
tf_path = tf.name
|
||||||
tf.write("\r\n".join(report).encode("utf-8", "replace"))
|
tf.write("\r\n".join(report).encode("utf-8", "replace"))
|
||||||
|
|
||||||
dt = datetime.utcnow().strftime("%Y-%m%d-%H%M%S")
|
dt = datetime.now(UTC).strftime("%Y-%m%d-%H%M%S")
|
||||||
|
|
||||||
bos.chmod(tf_path, 0o444)
|
bos.chmod(tf_path, 0o444)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -28,23 +28,30 @@ if True: # pylint: disable=using-constant-test
|
|||||||
import typing
|
import typing
|
||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
from .__init__ import ANYWIN, EXE, MACOS, TYPE_CHECKING, VT100, EnvParams, unicode
|
from .__init__ import ANYWIN, EXE, MACOS, TYPE_CHECKING, EnvParams, unicode
|
||||||
from .authsrv import AuthSrv
|
from .authsrv import BAD_CFG, AuthSrv
|
||||||
|
from .cert import ensure_cert
|
||||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE
|
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE
|
||||||
from .tcpsrv import TcpSrv
|
from .tcpsrv import TcpSrv
|
||||||
from .th_srv import HAVE_PIL, HAVE_VIPS, HAVE_WEBP, ThumbSrv
|
from .th_srv import HAVE_PIL, HAVE_VIPS, HAVE_WEBP, ThumbSrv
|
||||||
from .up2k import Up2k
|
from .up2k import Up2k
|
||||||
from .util import (
|
from .util import (
|
||||||
|
DEF_EXP,
|
||||||
|
DEF_MTE,
|
||||||
|
DEF_MTH,
|
||||||
FFMPEG_URL,
|
FFMPEG_URL,
|
||||||
|
UTC,
|
||||||
VERSIONS,
|
VERSIONS,
|
||||||
Daemon,
|
Daemon,
|
||||||
Garda,
|
Garda,
|
||||||
HLog,
|
HLog,
|
||||||
HMaccas,
|
HMaccas,
|
||||||
|
ODict,
|
||||||
alltrace,
|
alltrace,
|
||||||
ansi_re,
|
ansi_re,
|
||||||
min_ex,
|
min_ex,
|
||||||
mp,
|
mp,
|
||||||
|
odfusion,
|
||||||
pybin,
|
pybin,
|
||||||
start_log_thrs,
|
start_log_thrs,
|
||||||
start_stackmon,
|
start_stackmon,
|
||||||
@@ -80,6 +87,7 @@ class SvcHub(object):
|
|||||||
self.dargs = dargs
|
self.dargs = dargs
|
||||||
self.argv = argv
|
self.argv = argv
|
||||||
self.E: EnvParams = args.E
|
self.E: EnvParams = args.E
|
||||||
|
self.no_ansi = args.no_ansi
|
||||||
self.logf: Optional[typing.TextIO] = None
|
self.logf: Optional[typing.TextIO] = None
|
||||||
self.logf_base_fn = ""
|
self.logf_base_fn = ""
|
||||||
self.stop_req = False
|
self.stop_req = False
|
||||||
@@ -98,11 +106,6 @@ class SvcHub(object):
|
|||||||
|
|
||||||
self.iphash = HMaccas(os.path.join(self.E.cfg, "iphash"), 8)
|
self.iphash = HMaccas(os.path.join(self.E.cfg, "iphash"), 8)
|
||||||
|
|
||||||
# for non-http clients (ftp)
|
|
||||||
self.bans: dict[str, int] = {}
|
|
||||||
self.gpwd = Garda(self.args.ban_pw)
|
|
||||||
self.g404 = Garda(self.args.ban_404)
|
|
||||||
|
|
||||||
if args.sss or args.s >= 3:
|
if args.sss or args.s >= 3:
|
||||||
args.ss = True
|
args.ss = True
|
||||||
args.no_dav = True
|
args.no_dav = True
|
||||||
@@ -118,7 +121,6 @@ class SvcHub(object):
|
|||||||
args.no_mv = True
|
args.no_mv = True
|
||||||
args.hardlink = True
|
args.hardlink = True
|
||||||
args.vague_403 = True
|
args.vague_403 = True
|
||||||
args.ban_404 = "50,60,1440"
|
|
||||||
args.nih = True
|
args.nih = True
|
||||||
|
|
||||||
if args.s:
|
if args.s:
|
||||||
@@ -129,8 +131,20 @@ class SvcHub(object):
|
|||||||
args.force_js = True
|
args.force_js = True
|
||||||
|
|
||||||
if not self._process_config():
|
if not self._process_config():
|
||||||
raise Exception("bad config")
|
raise Exception(BAD_CFG)
|
||||||
|
|
||||||
|
# for non-http clients (ftp, tftp)
|
||||||
|
self.bans: dict[str, int] = {}
|
||||||
|
self.gpwd = Garda(self.args.ban_pw)
|
||||||
|
self.g404 = Garda(self.args.ban_404)
|
||||||
|
self.g403 = Garda(self.args.ban_403)
|
||||||
|
self.g422 = Garda(self.args.ban_422, False)
|
||||||
|
self.gmal = Garda(self.args.ban_422)
|
||||||
|
self.gurl = Garda(self.args.ban_url)
|
||||||
|
|
||||||
|
self.log_div = 10 ** (6 - args.log_tdec)
|
||||||
|
self.log_efmt = "%02d:%02d:%02d.%0{}d".format(args.log_tdec)
|
||||||
|
self.log_dfmt = "%04d-%04d-%06d.%0{}d".format(args.log_tdec)
|
||||||
self.log = self._log_disabled if args.q else self._log_enabled
|
self.log = self._log_disabled if args.q else self._log_enabled
|
||||||
if args.lo:
|
if args.lo:
|
||||||
self._setup_logfile(printed)
|
self._setup_logfile(printed)
|
||||||
@@ -160,6 +174,14 @@ class SvcHub(object):
|
|||||||
ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
|
ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
|
||||||
args.theme = "{0}{1} {0} {1}".format(ch, bri)
|
args.theme = "{0}{1} {0} {1}".format(ch, bri)
|
||||||
|
|
||||||
|
if args.nih:
|
||||||
|
args.vname = ""
|
||||||
|
args.doctitle = args.doctitle.replace(" @ --name", "")
|
||||||
|
else:
|
||||||
|
args.vname = args.name
|
||||||
|
args.doctitle = args.doctitle.replace("--name", args.vname)
|
||||||
|
args.bname = args.bname.replace("--name", args.vname) or args.vname
|
||||||
|
|
||||||
if args.log_fk:
|
if args.log_fk:
|
||||||
args.log_fk = re.compile(args.log_fk)
|
args.log_fk = re.compile(args.log_fk)
|
||||||
|
|
||||||
@@ -181,6 +203,10 @@ class SvcHub(object):
|
|||||||
self.log("root", "max clients: {}".format(self.args.nc))
|
self.log("root", "max clients: {}".format(self.args.nc))
|
||||||
|
|
||||||
self.tcpsrv = TcpSrv(self)
|
self.tcpsrv = TcpSrv(self)
|
||||||
|
|
||||||
|
if not self.tcpsrv.srv and self.args.ign_ebind_all:
|
||||||
|
self.args.no_fastboot = True
|
||||||
|
|
||||||
self.up2k = Up2k(self)
|
self.up2k = Up2k(self)
|
||||||
|
|
||||||
decs = {k: 1 for k in self.args.th_dec.split(",")}
|
decs = {k: 1 for k in self.args.th_dec.split(",")}
|
||||||
@@ -238,9 +264,16 @@ class SvcHub(object):
|
|||||||
if args.ftp or args.ftps:
|
if args.ftp or args.ftps:
|
||||||
from .ftpd import Ftpd
|
from .ftpd import Ftpd
|
||||||
|
|
||||||
self.ftpd = Ftpd(self)
|
self.ftpd: Optional[Ftpd] = None
|
||||||
|
Daemon(self.start_ftpd, "start_ftpd")
|
||||||
zms += "f" if args.ftp else "F"
|
zms += "f" if args.ftp else "F"
|
||||||
|
|
||||||
|
if args.tftp:
|
||||||
|
from .tftpd import Tftpd
|
||||||
|
|
||||||
|
self.tftpd: Optional[Tftpd] = None
|
||||||
|
Daemon(self.start_ftpd, "start_tftpd")
|
||||||
|
|
||||||
if args.smb:
|
if args.smb:
|
||||||
# impacket.dcerpc is noisy about listen timeouts
|
# impacket.dcerpc is noisy about listen timeouts
|
||||||
sto = socket.getdefaulttimeout()
|
sto = socket.getdefaulttimeout()
|
||||||
@@ -268,6 +301,41 @@ class SvcHub(object):
|
|||||||
|
|
||||||
self.broker = Broker(self)
|
self.broker = Broker(self)
|
||||||
|
|
||||||
|
def start_ftpd(self) -> None:
|
||||||
|
time.sleep(30)
|
||||||
|
|
||||||
|
if hasattr(self, "ftpd") and not self.ftpd:
|
||||||
|
self.restart_ftpd()
|
||||||
|
|
||||||
|
if hasattr(self, "tftpd") and not self.tftpd:
|
||||||
|
self.restart_tftpd()
|
||||||
|
|
||||||
|
def restart_ftpd(self) -> None:
|
||||||
|
if not hasattr(self, "ftpd"):
|
||||||
|
return
|
||||||
|
|
||||||
|
from .ftpd import Ftpd
|
||||||
|
|
||||||
|
if self.ftpd:
|
||||||
|
return # todo
|
||||||
|
|
||||||
|
if not os.path.exists(self.args.cert):
|
||||||
|
ensure_cert(self.log, self.args)
|
||||||
|
|
||||||
|
self.ftpd = Ftpd(self)
|
||||||
|
self.log("root", "started FTPd")
|
||||||
|
|
||||||
|
def restart_tftpd(self) -> None:
|
||||||
|
if not hasattr(self, "tftpd"):
|
||||||
|
return
|
||||||
|
|
||||||
|
from .tftpd import Tftpd
|
||||||
|
|
||||||
|
if self.tftpd:
|
||||||
|
return # todo
|
||||||
|
|
||||||
|
self.tftpd = Tftpd(self)
|
||||||
|
|
||||||
def thr_httpsrv_up(self) -> None:
|
def thr_httpsrv_up(self) -> None:
|
||||||
time.sleep(1 if self.args.ign_ebind_all else 5)
|
time.sleep(1 if self.args.ign_ebind_all else 5)
|
||||||
expected = self.broker.num_workers * self.tcpsrv.nsrv
|
expected = self.broker.num_workers * self.tcpsrv.nsrv
|
||||||
@@ -299,12 +367,20 @@ class SvcHub(object):
|
|||||||
if self.httpsrv_up != self.broker.num_workers:
|
if self.httpsrv_up != self.broker.num_workers:
|
||||||
return
|
return
|
||||||
|
|
||||||
time.sleep(0.1) # purely cosmetic dw
|
ar = self.args
|
||||||
|
for _ in range(10 if ar.ftp or ar.ftps else 0):
|
||||||
|
time.sleep(0.03)
|
||||||
|
if self.ftpd:
|
||||||
|
break
|
||||||
|
|
||||||
if self.tcpsrv.qr:
|
if self.tcpsrv.qr:
|
||||||
self.log("qr-code", self.tcpsrv.qr)
|
self.log("qr-code", self.tcpsrv.qr)
|
||||||
else:
|
else:
|
||||||
self.log("root", "workers OK\n")
|
self.log("root", "workers OK\n")
|
||||||
|
|
||||||
|
self.after_httpsrv_up()
|
||||||
|
|
||||||
|
def after_httpsrv_up(self) -> None:
|
||||||
self.up2k.init_vols()
|
self.up2k.init_vols()
|
||||||
|
|
||||||
Daemon(self.sd_notify, "sd-notify")
|
Daemon(self.sd_notify, "sd-notify")
|
||||||
@@ -348,29 +424,91 @@ class SvcHub(object):
|
|||||||
if al.rsp_jtr:
|
if al.rsp_jtr:
|
||||||
al.rsp_slp = 0.000001
|
al.rsp_slp = 0.000001
|
||||||
|
|
||||||
al.th_covers = set(al.th_covers.split(","))
|
zsl = al.th_covers.split(",")
|
||||||
|
zsl = [x.strip() for x in zsl]
|
||||||
|
zsl = [x for x in zsl if x]
|
||||||
|
al.th_covers = set(zsl)
|
||||||
|
al.th_coversd = set(zsl + ["." + x for x in zsl])
|
||||||
|
|
||||||
for k in "c".split(" "):
|
for k in "c".split(" "):
|
||||||
vl = getattr(al, k)
|
vl = getattr(al, k)
|
||||||
if not vl:
|
if not vl:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
vl = [os.path.expanduser(x) if x.startswith("~") else x for x in vl]
|
vl = [os.path.expandvars(os.path.expanduser(x)) for x in vl]
|
||||||
setattr(al, k, vl)
|
setattr(al, k, vl)
|
||||||
|
|
||||||
for k in "lo hist ssl_log".split(" "):
|
for k in "lo hist ssl_log".split(" "):
|
||||||
vs = getattr(al, k)
|
vs = getattr(al, k)
|
||||||
if vs and vs.startswith("~"):
|
if vs:
|
||||||
setattr(al, k, os.path.expanduser(vs))
|
vs = os.path.expandvars(os.path.expanduser(vs))
|
||||||
|
setattr(al, k, vs)
|
||||||
|
|
||||||
|
for k in "sus_urls nonsus_urls".split(" "):
|
||||||
|
vs = getattr(al, k)
|
||||||
|
if not vs or vs == "no":
|
||||||
|
setattr(al, k, None)
|
||||||
|
else:
|
||||||
|
setattr(al, k, re.compile(vs))
|
||||||
|
|
||||||
|
for k in "tftp_lsf".split(" "):
|
||||||
|
vs = getattr(al, k)
|
||||||
|
if not vs or vs == "no":
|
||||||
|
setattr(al, k, None)
|
||||||
|
else:
|
||||||
|
setattr(al, k, re.compile("^" + vs + "$"))
|
||||||
|
|
||||||
|
if not al.sus_urls:
|
||||||
|
al.ban_url = "no"
|
||||||
|
elif al.ban_url == "no":
|
||||||
|
al.sus_urls = None
|
||||||
|
|
||||||
|
al.xff_hdr = al.xff_hdr.lower()
|
||||||
|
al.idp_h_usr = al.idp_h_usr.lower()
|
||||||
|
# al.idp_h_grp = al.idp_h_grp.lower()
|
||||||
|
|
||||||
|
al.xff_re = self._ipa2re(al.xff_src)
|
||||||
|
al.ipa_re = self._ipa2re(al.ipa)
|
||||||
|
al.ftp_ipa_re = self._ipa2re(al.ftp_ipa or al.ipa)
|
||||||
|
al.tftp_ipa_re = self._ipa2re(al.tftp_ipa or al.ipa)
|
||||||
|
|
||||||
|
mte = ODict.fromkeys(DEF_MTE.split(","), True)
|
||||||
|
al.mte = odfusion(mte, al.mte)
|
||||||
|
|
||||||
|
mth = ODict.fromkeys(DEF_MTH.split(","), True)
|
||||||
|
al.mth = odfusion(mth, al.mth)
|
||||||
|
|
||||||
|
exp = ODict.fromkeys(DEF_EXP.split(" "), True)
|
||||||
|
al.exp_md = odfusion(exp, al.exp_md.replace(" ", ","))
|
||||||
|
al.exp_lg = odfusion(exp, al.exp_lg.replace(" ", ","))
|
||||||
|
|
||||||
|
for k in ["no_hash", "no_idx"]:
|
||||||
|
ptn = getattr(self.args, k)
|
||||||
|
if ptn:
|
||||||
|
setattr(self.args, k, re.compile(ptn))
|
||||||
|
|
||||||
|
try:
|
||||||
|
zf1, zf2 = self.args.rm_retry.split("/")
|
||||||
|
self.args.rm_re_t = float(zf1)
|
||||||
|
self.args.rm_re_r = float(zf2)
|
||||||
|
except:
|
||||||
|
raise Exception("invalid --rm-retry [%s]" % (self.args.rm_retry,))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _ipa2re(self, txt) -> Optional[re.Pattern]:
|
||||||
|
if txt in ("any", "0", ""):
|
||||||
|
return None
|
||||||
|
|
||||||
|
zs = txt.replace(" ", "").replace(".", "\\.").replace(",", "|")
|
||||||
|
return re.compile("^(?:" + zs + ")")
|
||||||
|
|
||||||
def _setlimits(self) -> None:
|
def _setlimits(self) -> None:
|
||||||
try:
|
try:
|
||||||
import resource
|
import resource
|
||||||
|
|
||||||
soft, hard = [
|
soft, hard = [
|
||||||
x if x > 0 else 1024 * 1024
|
int(x) if x > 0 else 1024 * 1024
|
||||||
for x in list(resource.getrlimit(resource.RLIMIT_NOFILE))
|
for x in list(resource.getrlimit(resource.RLIMIT_NOFILE))
|
||||||
]
|
]
|
||||||
except:
|
except:
|
||||||
@@ -406,7 +544,7 @@ class SvcHub(object):
|
|||||||
self.args.nc = min(self.args.nc, soft // 2)
|
self.args.nc = min(self.args.nc, soft // 2)
|
||||||
|
|
||||||
def _logname(self) -> str:
|
def _logname(self) -> str:
|
||||||
dt = datetime.utcnow()
|
dt = datetime.now(UTC)
|
||||||
fn = str(self.args.lo)
|
fn = str(self.args.lo)
|
||||||
for fs in "YmdHMS":
|
for fs in "YmdHMS":
|
||||||
fs = "%" + fs
|
fs = "%" + fs
|
||||||
@@ -427,12 +565,17 @@ class SvcHub(object):
|
|||||||
sel_fn = "{}.{}".format(fn, ctr)
|
sel_fn = "{}.{}".format(fn, ctr)
|
||||||
|
|
||||||
fn = sel_fn
|
fn = sel_fn
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(fn))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if do_xz:
|
if do_xz:
|
||||||
import lzma
|
import lzma
|
||||||
|
|
||||||
lh = lzma.open(fn, "wt", encoding="utf-8", errors="replace", preset=0)
|
lh = lzma.open(fn, "wt", encoding="utf-8", errors="replace", preset=0)
|
||||||
|
self.args.no_logflush = True
|
||||||
else:
|
else:
|
||||||
lh = open(fn, "wt", encoding="utf-8", errors="replace")
|
lh = open(fn, "wt", encoding="utf-8", errors="replace")
|
||||||
except:
|
except:
|
||||||
@@ -593,19 +736,25 @@ class SvcHub(object):
|
|||||||
ret = 1
|
ret = 1
|
||||||
try:
|
try:
|
||||||
self.pr("OPYTHAT")
|
self.pr("OPYTHAT")
|
||||||
|
tasks = []
|
||||||
slp = 0.0
|
slp = 0.0
|
||||||
|
|
||||||
if self.mdns:
|
if self.mdns:
|
||||||
Daemon(self.mdns.stop)
|
tasks.append(Daemon(self.mdns.stop, "mdns"))
|
||||||
slp = time.time() + 0.5
|
slp = time.time() + 0.5
|
||||||
|
|
||||||
if self.ssdp:
|
if self.ssdp:
|
||||||
Daemon(self.ssdp.stop)
|
tasks.append(Daemon(self.ssdp.stop, "ssdp"))
|
||||||
slp = time.time() + 0.5
|
slp = time.time() + 0.5
|
||||||
|
|
||||||
self.broker.shutdown()
|
self.broker.shutdown()
|
||||||
self.tcpsrv.shutdown()
|
self.tcpsrv.shutdown()
|
||||||
self.up2k.shutdown()
|
self.up2k.shutdown()
|
||||||
|
|
||||||
|
if hasattr(self, "smbd"):
|
||||||
|
slp = max(slp, time.time() + 0.5)
|
||||||
|
tasks.append(Daemon(self.smbd.stop, "smbd"))
|
||||||
|
|
||||||
if self.thumbsrv:
|
if self.thumbsrv:
|
||||||
self.thumbsrv.shutdown()
|
self.thumbsrv.shutdown()
|
||||||
|
|
||||||
@@ -615,17 +764,19 @@ class SvcHub(object):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if n == 3:
|
if n == 3:
|
||||||
self.pr("waiting for thumbsrv (10sec)...")
|
self.log("root", "waiting for thumbsrv (10sec)...")
|
||||||
|
|
||||||
if hasattr(self, "smbd"):
|
if hasattr(self, "smbd"):
|
||||||
slp = max(slp, time.time() + 0.5)
|
zf = max(time.time() - slp, 0)
|
||||||
Daemon(self.kill9, a=(1,))
|
Daemon(self.kill9, a=(zf + 0.5,))
|
||||||
Daemon(self.smbd.stop)
|
|
||||||
|
|
||||||
while time.time() < slp:
|
while time.time() < slp:
|
||||||
time.sleep(0.1)
|
if not next((x for x in tasks if x.is_alive), None):
|
||||||
|
break
|
||||||
|
|
||||||
self.pr("nailed it", end="")
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
self.log("root", "nailed it")
|
||||||
ret = self.retcode
|
ret = self.retcode
|
||||||
except:
|
except:
|
||||||
self.pr("\033[31m[ error during shutdown ]\n{}\033[0m".format(min_ex()))
|
self.pr("\033[31m[ error during shutdown ]\n{}\033[0m".format(min_ex()))
|
||||||
@@ -635,7 +786,7 @@ class SvcHub(object):
|
|||||||
print("\033]0;\033\\", file=sys.stderr, end="")
|
print("\033]0;\033\\", file=sys.stderr, end="")
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|
||||||
self.pr("\033[0m")
|
self.pr("\033[0m", end="")
|
||||||
if self.logf:
|
if self.logf:
|
||||||
self.logf.close()
|
self.logf.close()
|
||||||
|
|
||||||
@@ -647,11 +798,34 @@ class SvcHub(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
with self.log_mutex:
|
with self.log_mutex:
|
||||||
ts = datetime.utcnow().strftime("%Y-%m%d-%H%M%S.%f")[:-3]
|
zd = datetime.now(UTC)
|
||||||
self.logf.write("@{} [{}\033[0m] {}\n".format(ts, src, msg))
|
ts = self.log_dfmt % (
|
||||||
|
zd.year,
|
||||||
|
zd.month * 100 + zd.day,
|
||||||
|
(zd.hour * 100 + zd.minute) * 100 + zd.second,
|
||||||
|
zd.microsecond // self.log_div,
|
||||||
|
)
|
||||||
|
|
||||||
|
if c and not self.args.no_ansi:
|
||||||
|
if isinstance(c, int):
|
||||||
|
msg = "\033[3%sm%s\033[0m" % (c, msg)
|
||||||
|
elif "\033" not in c:
|
||||||
|
msg = "\033[%sm%s\033[0m" % (c, msg)
|
||||||
|
else:
|
||||||
|
msg = "%s%s\033[0m" % (c, msg)
|
||||||
|
|
||||||
|
if "\033" in src:
|
||||||
|
src += "\033[0m"
|
||||||
|
|
||||||
|
if "\033" in msg:
|
||||||
|
msg += "\033[0m"
|
||||||
|
|
||||||
|
self.logf.write("@%s [%-21s] %s\n" % (ts, src, msg))
|
||||||
|
if not self.args.no_logflush:
|
||||||
|
self.logf.flush()
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now >= self.next_day:
|
if int(now) >= self.next_day:
|
||||||
self._set_next_day()
|
self._set_next_day()
|
||||||
|
|
||||||
def _set_next_day(self) -> None:
|
def _set_next_day(self) -> None:
|
||||||
@@ -659,7 +833,7 @@ class SvcHub(object):
|
|||||||
self.logf.close()
|
self.logf.close()
|
||||||
self._setup_logfile("")
|
self._setup_logfile("")
|
||||||
|
|
||||||
dt = datetime.utcnow()
|
dt = datetime.now(UTC)
|
||||||
|
|
||||||
# unix timestamp of next 00:00:00 (leap-seconds safe)
|
# unix timestamp of next 00:00:00 (leap-seconds safe)
|
||||||
day_now = dt.day
|
day_now = dt.day
|
||||||
@@ -667,34 +841,50 @@ class SvcHub(object):
|
|||||||
dt += timedelta(hours=12)
|
dt += timedelta(hours=12)
|
||||||
|
|
||||||
dt = dt.replace(hour=0, minute=0, second=0)
|
dt = dt.replace(hour=0, minute=0, second=0)
|
||||||
self.next_day = calendar.timegm(dt.utctimetuple())
|
try:
|
||||||
|
tt = dt.utctimetuple()
|
||||||
|
except:
|
||||||
|
# still makes me hella uncomfortable
|
||||||
|
tt = dt.timetuple()
|
||||||
|
|
||||||
|
self.next_day = calendar.timegm(tt)
|
||||||
|
|
||||||
def _log_enabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None:
|
def _log_enabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None:
|
||||||
"""handles logging from all components"""
|
"""handles logging from all components"""
|
||||||
with self.log_mutex:
|
with self.log_mutex:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now >= self.next_day:
|
if int(now) >= self.next_day:
|
||||||
dt = datetime.utcfromtimestamp(now)
|
dt = datetime.fromtimestamp(now, UTC)
|
||||||
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()
|
self._set_next_day()
|
||||||
|
if self.logf:
|
||||||
|
self.logf.write(zs)
|
||||||
|
|
||||||
fmt = "\033[36m{} \033[33m{:21} \033[0m{}\n"
|
fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n"
|
||||||
if not VT100:
|
if self.no_ansi:
|
||||||
fmt = "{} {:21} {}\n"
|
fmt = "%s %-21s %s\n"
|
||||||
if "\033" in msg:
|
if "\033" in msg:
|
||||||
msg = ansi_re.sub("", msg)
|
msg = ansi_re.sub("", msg)
|
||||||
if "\033" in src:
|
if "\033" in src:
|
||||||
src = ansi_re.sub("", src)
|
src = ansi_re.sub("", src)
|
||||||
elif c:
|
elif c:
|
||||||
if isinstance(c, int):
|
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:
|
elif "\033" not in c:
|
||||||
msg = "\033[{}m{}\033[0m".format(c, msg)
|
msg = "\033[%sm%s\033[0m" % (c, msg)
|
||||||
else:
|
else:
|
||||||
msg = "{}{}\033[0m".format(c, msg)
|
msg = "%s%s\033[0m" % (c, msg)
|
||||||
|
|
||||||
ts = datetime.utcfromtimestamp(now).strftime("%H:%M:%S.%f")[:-3]
|
zd = datetime.fromtimestamp(now, UTC)
|
||||||
msg = fmt.format(ts, src, msg)
|
ts = self.log_efmt % (
|
||||||
|
zd.hour,
|
||||||
|
zd.minute,
|
||||||
|
zd.second,
|
||||||
|
zd.microsecond // self.log_div,
|
||||||
|
)
|
||||||
|
msg = fmt % (ts, src, msg)
|
||||||
try:
|
try:
|
||||||
print(msg, end="")
|
print(msg, end="")
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
@@ -708,6 +898,8 @@ class SvcHub(object):
|
|||||||
|
|
||||||
if self.logf:
|
if self.logf:
|
||||||
self.logf.write(msg)
|
self.logf.write(msg)
|
||||||
|
if not self.args.no_logflush:
|
||||||
|
self.logf.flush()
|
||||||
|
|
||||||
def pr(self, *a: Any, **ka: Any) -> None:
|
def pr(self, *a: Any, **ka: Any) -> None:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ class StreamZip(StreamArc):
|
|||||||
fgen: Generator[dict[str, Any], None, None],
|
fgen: Generator[dict[str, Any], None, None],
|
||||||
utf8: bool = False,
|
utf8: bool = False,
|
||||||
pre_crc: bool = False,
|
pre_crc: bool = False,
|
||||||
|
**kwargs: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
super(StreamZip, self).__init__(log, fgen)
|
super(StreamZip, self).__init__(log, fgen)
|
||||||
|
|
||||||
@@ -275,6 +276,7 @@ class StreamZip(StreamArc):
|
|||||||
def gen(self) -> Generator[bytes, None, None]:
|
def gen(self) -> Generator[bytes, None, None]:
|
||||||
errf: dict[str, Any] = {}
|
errf: dict[str, Any] = {}
|
||||||
errors = []
|
errors = []
|
||||||
|
mbuf = b""
|
||||||
try:
|
try:
|
||||||
for f in self.fgen:
|
for f in self.fgen:
|
||||||
if "err" in f:
|
if "err" in f:
|
||||||
@@ -283,13 +285,20 @@ class StreamZip(StreamArc):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
for x in self.ser(f):
|
for x in self.ser(f):
|
||||||
yield x
|
mbuf += x
|
||||||
|
if len(mbuf) >= 16384:
|
||||||
|
yield mbuf
|
||||||
|
mbuf = b""
|
||||||
except GeneratorExit:
|
except GeneratorExit:
|
||||||
raise
|
raise
|
||||||
except:
|
except:
|
||||||
ex = min_ex(5, True).replace("\n", "\n-- ")
|
ex = min_ex(5, True).replace("\n", "\n-- ")
|
||||||
errors.append((f["vp"], ex))
|
errors.append((f["vp"], ex))
|
||||||
|
|
||||||
|
if mbuf:
|
||||||
|
yield mbuf
|
||||||
|
mbuf = b""
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
errf, txt = errdesc(errors)
|
errf, txt = errdesc(errors)
|
||||||
self.log("\n".join(([repr(errf)] + txt[1:])))
|
self.log("\n".join(([repr(errf)] + txt[1:])))
|
||||||
@@ -299,20 +308,23 @@ class StreamZip(StreamArc):
|
|||||||
cdir_pos = self.pos
|
cdir_pos = self.pos
|
||||||
for name, sz, ts, crc, h_pos in self.items:
|
for name, sz, ts, crc, h_pos in self.items:
|
||||||
buf = gen_hdr(h_pos, name, sz, ts, self.utf8, crc, self.pre_crc)
|
buf = gen_hdr(h_pos, name, sz, ts, self.utf8, crc, self.pre_crc)
|
||||||
yield self._ct(buf)
|
mbuf += self._ct(buf)
|
||||||
|
if len(mbuf) >= 16384:
|
||||||
|
yield mbuf
|
||||||
|
mbuf = b""
|
||||||
cdir_end = self.pos
|
cdir_end = self.pos
|
||||||
|
|
||||||
_, need_64 = gen_ecdr(self.items, cdir_pos, cdir_end)
|
_, need_64 = gen_ecdr(self.items, cdir_pos, cdir_end)
|
||||||
if need_64:
|
if need_64:
|
||||||
ecdir64_pos = self.pos
|
ecdir64_pos = self.pos
|
||||||
buf = gen_ecdr64(self.items, cdir_pos, cdir_end)
|
buf = gen_ecdr64(self.items, cdir_pos, cdir_end)
|
||||||
yield self._ct(buf)
|
mbuf += self._ct(buf)
|
||||||
|
|
||||||
buf = gen_ecdr64_loc(ecdir64_pos)
|
buf = gen_ecdr64_loc(ecdir64_pos)
|
||||||
yield self._ct(buf)
|
mbuf += self._ct(buf)
|
||||||
|
|
||||||
ecdr, _ = gen_ecdr(self.items, cdir_pos, cdir_end)
|
ecdr, _ = gen_ecdr(self.items, cdir_pos, cdir_end)
|
||||||
yield self._ct(ecdr)
|
yield mbuf + self._ct(ecdr)
|
||||||
finally:
|
finally:
|
||||||
if errf:
|
if errf:
|
||||||
bos.unlink(errf["ap"])
|
bos.unlink(errf["ap"])
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ import socket
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, VT100, unicode
|
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
|
||||||
|
from .cert import gencert
|
||||||
from .stolen.qrcodegen import QrCode
|
from .stolen.qrcodegen import QrCode
|
||||||
from .util import (
|
from .util import (
|
||||||
E_ACCESS,
|
E_ACCESS,
|
||||||
E_ADDR_IN_USE,
|
E_ADDR_IN_USE,
|
||||||
E_ADDR_NOT_AVAIL,
|
E_ADDR_NOT_AVAIL,
|
||||||
E_UNREACH,
|
E_UNREACH,
|
||||||
|
IP6ALL,
|
||||||
Netdev,
|
Netdev,
|
||||||
min_ex,
|
min_ex,
|
||||||
sunpack,
|
sunpack,
|
||||||
@@ -239,6 +241,11 @@ class TcpSrv(object):
|
|||||||
raise OSError(E_ADDR_IN_USE[0], "")
|
raise OSError(E_ADDR_IN_USE[0], "")
|
||||||
self.srv.append(srv)
|
self.srv.append(srv)
|
||||||
except (OSError, socket.error) as ex:
|
except (OSError, socket.error) as ex:
|
||||||
|
try:
|
||||||
|
srv.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
if ex.errno in E_ADDR_IN_USE:
|
if ex.errno in E_ADDR_IN_USE:
|
||||||
e = "\033[1;31mport {} is busy on interface {}\033[0m".format(port, ip)
|
e = "\033[1;31mport {} is busy on interface {}\033[0m".format(port, ip)
|
||||||
elif ex.errno in E_ADDR_NOT_AVAIL:
|
elif ex.errno in E_ADDR_NOT_AVAIL:
|
||||||
@@ -253,6 +260,9 @@ class TcpSrv(object):
|
|||||||
srvs: list[socket.socket] = []
|
srvs: list[socket.socket] = []
|
||||||
for srv in self.srv:
|
for srv in self.srv:
|
||||||
ip, port = srv.getsockname()[:2]
|
ip, port = srv.getsockname()[:2]
|
||||||
|
if ip == IP6ALL:
|
||||||
|
ip = "::" # jython
|
||||||
|
|
||||||
try:
|
try:
|
||||||
srv.listen(self.args.nc)
|
srv.listen(self.args.nc)
|
||||||
try:
|
try:
|
||||||
@@ -274,6 +284,8 @@ class TcpSrv(object):
|
|||||||
srv.close()
|
srv.close()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
t = "\n\nERROR: could not open listening socket, probably because one of the server ports ({}) is busy on one of the requested interfaces ({}); avoid this issue by specifying a different port (-p 3939) and/or a specific interface to listen on (-i 192.168.56.1)\n"
|
||||||
|
self.log("tcpsrv", t.format(port, ip), 1)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
bound.append((ip, port))
|
bound.append((ip, port))
|
||||||
@@ -295,6 +307,9 @@ class TcpSrv(object):
|
|||||||
def _distribute_netdevs(self):
|
def _distribute_netdevs(self):
|
||||||
self.hub.broker.say("set_netdevs", self.netdevs)
|
self.hub.broker.say("set_netdevs", self.netdevs)
|
||||||
self.hub.start_zeroconf()
|
self.hub.start_zeroconf()
|
||||||
|
gencert(self.log, self.args, self.netdevs)
|
||||||
|
self.hub.restart_ftpd()
|
||||||
|
self.hub.restart_tftpd()
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
self.stopping = True
|
self.stopping = True
|
||||||
@@ -501,7 +516,7 @@ class TcpSrv(object):
|
|||||||
zoom = 1
|
zoom = 1
|
||||||
|
|
||||||
qr = qrc.render(zoom, pad)
|
qr = qrc.render(zoom, pad)
|
||||||
if not VT100:
|
if self.args.no_ansi:
|
||||||
return "{}\n{}".format(txt, qr)
|
return "{}\n{}".format(txt, qr)
|
||||||
|
|
||||||
halfc = "\033[40;48;5;{0}m{1}\033[47;48;5;{2}m"
|
halfc = "\033[40;48;5;{0}m{1}\033[47;48;5;{2}m"
|
||||||
|
|||||||
309
copyparty/tftpd.py
Normal file
309
copyparty/tftpd.py
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
try:
|
||||||
|
from types import SimpleNamespace
|
||||||
|
except:
|
||||||
|
|
||||||
|
class SimpleNamespace(object):
|
||||||
|
def __init__(self, **attr):
|
||||||
|
self.__dict__.update(attr)
|
||||||
|
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from partftpy import TftpContexts, TftpServer, TftpStates
|
||||||
|
from partftpy.TftpShared import TftpException
|
||||||
|
|
||||||
|
from .__init__ import PY2, TYPE_CHECKING
|
||||||
|
from .authsrv import VFS
|
||||||
|
from .bos import bos
|
||||||
|
from .util import BytesIO, Daemon, exclude_dotfiles, runhook, undot
|
||||||
|
|
||||||
|
if True: # pylint: disable=using-constant-test
|
||||||
|
from typing import Any, Union
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .svchub import SvcHub
|
||||||
|
|
||||||
|
|
||||||
|
lg = logging.getLogger("tftp")
|
||||||
|
debug, info, warning, error = (lg.debug, lg.info, lg.warning, lg.error)
|
||||||
|
|
||||||
|
|
||||||
|
def _serverInitial(self, pkt: Any, raddress: str, rport: int) -> bool:
|
||||||
|
info("connection from %s:%s", raddress, rport)
|
||||||
|
ret = _orig_serverInitial(self, pkt, raddress, rport)
|
||||||
|
ptn = _hub[0].args.tftp_ipa_re
|
||||||
|
if ptn and not ptn.match(raddress):
|
||||||
|
yeet("client rejected (--tftp-ipa): %s" % (raddress,))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
# patch ipa-check into partftpd
|
||||||
|
_hub: list["SvcHub"] = []
|
||||||
|
_orig_serverInitial = TftpStates.TftpServerState.serverInitial
|
||||||
|
TftpStates.TftpServerState.serverInitial = _serverInitial
|
||||||
|
|
||||||
|
|
||||||
|
class Tftpd(object):
|
||||||
|
def __init__(self, hub: "SvcHub") -> None:
|
||||||
|
self.hub = hub
|
||||||
|
self.args = hub.args
|
||||||
|
self.asrv = hub.asrv
|
||||||
|
self.log = hub.log
|
||||||
|
|
||||||
|
_hub[:] = []
|
||||||
|
_hub.append(hub)
|
||||||
|
|
||||||
|
lg.setLevel(logging.DEBUG if self.args.tftpv else logging.INFO)
|
||||||
|
for x in ["partftpy", "partftpy.TftpStates", "partftpy.TftpServer"]:
|
||||||
|
lgr = logging.getLogger(x)
|
||||||
|
lgr.setLevel(logging.DEBUG if self.args.tftpv else logging.INFO)
|
||||||
|
|
||||||
|
# patch vfs into partftpy
|
||||||
|
TftpContexts.open = self._open
|
||||||
|
TftpStates.open = self._open
|
||||||
|
|
||||||
|
fos = SimpleNamespace()
|
||||||
|
for k in os.__dict__:
|
||||||
|
try:
|
||||||
|
setattr(fos, k, getattr(os, k))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
fos.access = self._access
|
||||||
|
fos.mkdir = self._mkdir
|
||||||
|
fos.unlink = self._unlink
|
||||||
|
fos.sep = "/"
|
||||||
|
TftpContexts.os = fos
|
||||||
|
TftpServer.os = fos
|
||||||
|
TftpStates.os = fos
|
||||||
|
|
||||||
|
fop = SimpleNamespace()
|
||||||
|
for k in os.path.__dict__:
|
||||||
|
try:
|
||||||
|
setattr(fop, k, getattr(os.path, k))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
fop.abspath = self._p_abspath
|
||||||
|
fop.exists = self._p_exists
|
||||||
|
fop.isdir = self._p_isdir
|
||||||
|
fop.normpath = self._p_normpath
|
||||||
|
fos.path = fop
|
||||||
|
|
||||||
|
self._disarm(fos)
|
||||||
|
|
||||||
|
ip = next((x for x in self.args.i if ":" not in x), None)
|
||||||
|
if not ip:
|
||||||
|
self.log("tftp", "IPv6 not supported for tftp; listening on 0.0.0.0", 3)
|
||||||
|
ip = "0.0.0.0"
|
||||||
|
|
||||||
|
self.ip = ip
|
||||||
|
self.port = int(self.args.tftp)
|
||||||
|
self.srv = TftpServer.TftpServer("/", self._ls)
|
||||||
|
self.stop = self.srv.stop
|
||||||
|
|
||||||
|
ports = []
|
||||||
|
if self.args.tftp_pr:
|
||||||
|
p1, p2 = [int(x) for x in self.args.tftp_pr.split("-")]
|
||||||
|
ports = list(range(p1, p2 + 1))
|
||||||
|
|
||||||
|
Daemon(self.srv.listen, "tftp", [self.ip, self.port], ka={"ports": ports})
|
||||||
|
|
||||||
|
def nlog(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||||
|
self.log("tftp", msg, c)
|
||||||
|
|
||||||
|
def _v2a(self, caller: str, vpath: str, perms: list, *a: Any) -> tuple[VFS, str]:
|
||||||
|
vpath = vpath.replace("\\", "/").lstrip("/")
|
||||||
|
if not perms:
|
||||||
|
perms = [True, True]
|
||||||
|
|
||||||
|
debug('%s("%s", %s) %s\033[K\033[0m', caller, vpath, str(a), perms)
|
||||||
|
vfs, rem = self.asrv.vfs.get(vpath, "*", *perms)
|
||||||
|
return vfs, vfs.canonical(rem)
|
||||||
|
|
||||||
|
def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any:
|
||||||
|
# generate file listing if vpath is dir.txt and return as file object
|
||||||
|
if not force:
|
||||||
|
vpath, fn = os.path.split(vpath.replace("\\", "/"))
|
||||||
|
ptn = self.args.tftp_lsf
|
||||||
|
if not ptn or not ptn.match(fn.lower()):
|
||||||
|
return None
|
||||||
|
|
||||||
|
vn, rem = self.asrv.vfs.get(vpath, "*", True, False)
|
||||||
|
fsroot, vfs_ls, vfs_virt = vn.ls(
|
||||||
|
rem,
|
||||||
|
"*",
|
||||||
|
not self.args.no_scandir,
|
||||||
|
[[True, False]],
|
||||||
|
)
|
||||||
|
dnames = set([x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)])
|
||||||
|
dirs1 = [(v.st_mtime, v.st_size, k + "/") for k, v in vfs_ls if k in dnames]
|
||||||
|
fils1 = [(v.st_mtime, v.st_size, k) for k, v in vfs_ls if k not in dnames]
|
||||||
|
real1 = dirs1 + fils1
|
||||||
|
realt = [(datetime.fromtimestamp(mt), sz, fn) for mt, sz, fn in real1]
|
||||||
|
reals = [
|
||||||
|
(
|
||||||
|
"%04d-%02d-%02d %02d:%02d:%02d"
|
||||||
|
% (
|
||||||
|
zd.year,
|
||||||
|
zd.month,
|
||||||
|
zd.day,
|
||||||
|
zd.hour,
|
||||||
|
zd.minute,
|
||||||
|
zd.second,
|
||||||
|
),
|
||||||
|
sz,
|
||||||
|
fn,
|
||||||
|
)
|
||||||
|
for zd, sz, fn in realt
|
||||||
|
]
|
||||||
|
virs = [("????-??-?? ??:??:??", 0, k + "/") for k in vfs_virt.keys()]
|
||||||
|
ls = virs + reals
|
||||||
|
|
||||||
|
if "*" not in vn.axs.udot:
|
||||||
|
names = set(exclude_dotfiles([x[2] for x in ls]))
|
||||||
|
ls = [x for x in ls if x[2] in names]
|
||||||
|
|
||||||
|
try:
|
||||||
|
biggest = max([x[1] for x in ls])
|
||||||
|
except:
|
||||||
|
biggest = 0
|
||||||
|
|
||||||
|
perms = []
|
||||||
|
if "*" in vn.axs.uread:
|
||||||
|
perms.append("read")
|
||||||
|
if "*" in vn.axs.udot:
|
||||||
|
perms.append("hidden")
|
||||||
|
if "*" in vn.axs.uwrite:
|
||||||
|
if "*" in vn.axs.udel:
|
||||||
|
perms.append("overwrite")
|
||||||
|
else:
|
||||||
|
perms.append("write")
|
||||||
|
|
||||||
|
fmt = "{{}} {{:{},}} {{}}"
|
||||||
|
fmt = fmt.format(len("{:,}".format(biggest)))
|
||||||
|
retl = ["# permissions: %s" % (", ".join(perms),)]
|
||||||
|
retl += [fmt.format(*x) for x in ls]
|
||||||
|
ret = "\n".join(retl).encode("utf-8", "replace")
|
||||||
|
return BytesIO(ret)
|
||||||
|
|
||||||
|
def _open(self, vpath: str, mode: str, *a: Any, **ka: Any) -> Any:
|
||||||
|
rd = wr = False
|
||||||
|
if mode == "rb":
|
||||||
|
rd = True
|
||||||
|
elif mode == "wb":
|
||||||
|
wr = True
|
||||||
|
else:
|
||||||
|
raise Exception("bad mode %s" % (mode,))
|
||||||
|
|
||||||
|
vfs, ap = self._v2a("open", vpath, [rd, wr])
|
||||||
|
if wr:
|
||||||
|
if "*" not in vfs.axs.uwrite:
|
||||||
|
yeet("blocked write; folder not world-writable: /%s" % (vpath,))
|
||||||
|
|
||||||
|
if bos.path.exists(ap) and "*" not in vfs.axs.udel:
|
||||||
|
yeet("blocked write; folder not world-deletable: /%s" % (vpath,))
|
||||||
|
|
||||||
|
xbu = vfs.flags.get("xbu")
|
||||||
|
if xbu and not runhook(
|
||||||
|
self.nlog, xbu, ap, vpath, "", "", 0, 0, "8.3.8.7", 0, ""
|
||||||
|
):
|
||||||
|
yeet("blocked by xbu server config: " + vpath)
|
||||||
|
|
||||||
|
if not self.args.tftp_nols and bos.path.isdir(ap):
|
||||||
|
return self._ls(vpath, "", 0, True)
|
||||||
|
|
||||||
|
return open(ap, mode, *a, **ka)
|
||||||
|
|
||||||
|
def _mkdir(self, vpath: str, *a) -> None:
|
||||||
|
vfs, ap = self._v2a("mkdir", vpath, [])
|
||||||
|
if "*" not in vfs.axs.uwrite:
|
||||||
|
yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,))
|
||||||
|
|
||||||
|
return bos.mkdir(ap)
|
||||||
|
|
||||||
|
def _unlink(self, vpath: str) -> None:
|
||||||
|
# return bos.unlink(self._v2a("stat", vpath, *a)[1])
|
||||||
|
vfs, ap = self._v2a("delete", vpath, [True, False, False, True])
|
||||||
|
|
||||||
|
try:
|
||||||
|
inf = bos.stat(ap)
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not stat.S_ISREG(inf.st_mode) or inf.st_size:
|
||||||
|
yeet("attempted delete of non-empty file")
|
||||||
|
|
||||||
|
vpath = vpath.replace("\\", "/").lstrip("/")
|
||||||
|
self.hub.up2k.handle_rm("*", "8.3.8.7", [vpath], [], False)
|
||||||
|
|
||||||
|
def _access(self, *a: Any) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _p_abspath(self, vpath: str) -> str:
|
||||||
|
return "/" + undot(vpath)
|
||||||
|
|
||||||
|
def _p_normpath(self, *a: Any) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _p_exists(self, vpath: str) -> bool:
|
||||||
|
try:
|
||||||
|
ap = self._v2a("p.exists", vpath, [False, False])[1]
|
||||||
|
bos.stat(ap)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _p_isdir(self, vpath: str) -> bool:
|
||||||
|
try:
|
||||||
|
st = bos.stat(self._v2a("p.isdir", vpath, [False, False])[1])
|
||||||
|
ret = stat.S_ISDIR(st.st_mode)
|
||||||
|
return ret
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _hook(self, *a: Any, **ka: Any) -> None:
|
||||||
|
src = inspect.currentframe().f_back.f_code.co_name
|
||||||
|
error("\033[31m%s:hook(%s)\033[0m", src, a)
|
||||||
|
raise Exception("nope")
|
||||||
|
|
||||||
|
def _disarm(self, fos: SimpleNamespace) -> None:
|
||||||
|
fos.chmod = self._hook
|
||||||
|
fos.chown = self._hook
|
||||||
|
fos.close = self._hook
|
||||||
|
fos.ftruncate = self._hook
|
||||||
|
fos.lchown = self._hook
|
||||||
|
fos.link = self._hook
|
||||||
|
fos.listdir = self._hook
|
||||||
|
fos.lstat = self._hook
|
||||||
|
fos.open = self._hook
|
||||||
|
fos.remove = self._hook
|
||||||
|
fos.rename = self._hook
|
||||||
|
fos.replace = self._hook
|
||||||
|
fos.scandir = self._hook
|
||||||
|
fos.stat = self._hook
|
||||||
|
fos.symlink = self._hook
|
||||||
|
fos.truncate = self._hook
|
||||||
|
fos.utime = self._hook
|
||||||
|
fos.walk = self._hook
|
||||||
|
|
||||||
|
fos.path.expanduser = self._hook
|
||||||
|
fos.path.expandvars = self._hook
|
||||||
|
fos.path.getatime = self._hook
|
||||||
|
fos.path.getctime = self._hook
|
||||||
|
fos.path.getmtime = self._hook
|
||||||
|
fos.path.getsize = self._hook
|
||||||
|
fos.path.isabs = self._hook
|
||||||
|
fos.path.isfile = self._hook
|
||||||
|
fos.path.islink = self._hook
|
||||||
|
fos.path.realpath = self._hook
|
||||||
|
|
||||||
|
|
||||||
|
def yeet(msg: str) -> None:
|
||||||
|
warning(msg)
|
||||||
|
raise TftpException(msg)
|
||||||
@@ -31,7 +31,7 @@ class ThumbCli(object):
|
|||||||
if not c:
|
if not c:
|
||||||
raise Exception()
|
raise Exception()
|
||||||
except:
|
except:
|
||||||
c = {k: {} for k in ["thumbable", "pil", "vips", "ffi", "ffv", "ffa"]}
|
c = {k: set() for k in ["thumbable", "pil", "vips", "ffi", "ffv", "ffa"]}
|
||||||
|
|
||||||
self.thumbable = c["thumbable"]
|
self.thumbable = c["thumbable"]
|
||||||
self.fmt_pil = c["pil"]
|
self.fmt_pil = c["pil"]
|
||||||
@@ -94,7 +94,7 @@ class ThumbCli(object):
|
|||||||
self.log("no histpath for [{}]".format(ptop))
|
self.log("no histpath for [{}]".format(ptop))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
tpath = thumb_path(histpath, rem, mtime, fmt)
|
tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa)
|
||||||
tpaths = [tpath]
|
tpaths = [tpath]
|
||||||
if fmt == "w":
|
if fmt == "w":
|
||||||
# also check for jpg (maybe webp is unavailable)
|
# also check for jpg (maybe webp is unavailable)
|
||||||
@@ -108,6 +108,7 @@ class ThumbCli(object):
|
|||||||
if st.st_size:
|
if st.st_size:
|
||||||
ret = tpath = tp
|
ret = tpath = tp
|
||||||
fmt = ret.rsplit(".")[1]
|
fmt = ret.rsplit(".")[1]
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
abort = True
|
abort = True
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ import time
|
|||||||
from queue import Queue
|
from queue import Queue
|
||||||
|
|
||||||
from .__init__ import ANYWIN, TYPE_CHECKING
|
from .__init__ import ANYWIN, TYPE_CHECKING
|
||||||
|
from .authsrv import VFS
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
|
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
|
||||||
from .util import (
|
from .util import (
|
||||||
FFMPEG_URL,
|
FFMPEG_URL,
|
||||||
BytesIO,
|
BytesIO, # type: ignore
|
||||||
Cooldown,
|
Cooldown,
|
||||||
Daemon,
|
Daemon,
|
||||||
Pebkac,
|
Pebkac,
|
||||||
@@ -27,6 +28,7 @@ from .util import (
|
|||||||
runcmd,
|
runcmd,
|
||||||
statdir,
|
statdir,
|
||||||
vsplit,
|
vsplit,
|
||||||
|
wunlink,
|
||||||
)
|
)
|
||||||
|
|
||||||
if True: # pylint: disable=using-constant-test
|
if True: # pylint: disable=using-constant-test
|
||||||
@@ -36,14 +38,21 @@ if TYPE_CHECKING:
|
|||||||
from .svchub import SvcHub
|
from .svchub import SvcHub
|
||||||
|
|
||||||
HAVE_PIL = False
|
HAVE_PIL = False
|
||||||
|
HAVE_PILF = False
|
||||||
HAVE_HEIF = False
|
HAVE_HEIF = False
|
||||||
HAVE_AVIF = False
|
HAVE_AVIF = False
|
||||||
HAVE_WEBP = False
|
HAVE_WEBP = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import ExifTags, Image, ImageOps
|
from PIL import ExifTags, Image, ImageFont, ImageOps
|
||||||
|
|
||||||
HAVE_PIL = True
|
HAVE_PIL = True
|
||||||
|
try:
|
||||||
|
ImageFont.load_default(size=16)
|
||||||
|
HAVE_PILF = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
Image.new("RGB", (2, 2)).save(BytesIO(), format="webp")
|
Image.new("RGB", (2, 2)).save(BytesIO(), format="webp")
|
||||||
HAVE_WEBP = True
|
HAVE_WEBP = True
|
||||||
@@ -78,17 +87,23 @@ except:
|
|||||||
HAVE_VIPS = False
|
HAVE_VIPS = False
|
||||||
|
|
||||||
|
|
||||||
def thumb_path(histpath: str, rem: str, mtime: float, fmt: str) -> str:
|
def thumb_path(histpath: str, rem: str, mtime: float, fmt: str, ffa: set[str]) -> str:
|
||||||
# base16 = 16 = 256
|
# base16 = 16 = 256
|
||||||
# b64-lc = 38 = 1444
|
# b64-lc = 38 = 1444
|
||||||
# base64 = 64 = 4096
|
# base64 = 64 = 4096
|
||||||
rd, fn = vsplit(rem)
|
rd, fn = vsplit(rem)
|
||||||
if rd:
|
if not rd:
|
||||||
h = hashlib.sha512(afsenc(rd)).digest()
|
rd = "\ntop"
|
||||||
b64 = base64.urlsafe_b64encode(h).decode("ascii")[:24]
|
|
||||||
rd = "{}/{}/".format(b64[:2], b64[2:4]).lower() + b64
|
# spectrograms are never cropped; strip fullsize flag
|
||||||
else:
|
ext = rem.split(".")[-1].lower()
|
||||||
rd = "top"
|
if ext in ffa and fmt in ("wf", "jf"):
|
||||||
|
fmt = fmt[:1]
|
||||||
|
|
||||||
|
rd += "\n" + fmt
|
||||||
|
h = hashlib.sha512(afsenc(rd)).digest()
|
||||||
|
b64 = base64.urlsafe_b64encode(h).decode("ascii")[:24]
|
||||||
|
rd = ("%s/%s/" % (b64[:2], b64[2:4])).lower() + b64
|
||||||
|
|
||||||
# could keep original filenames but this is safer re pathlen
|
# could keep original filenames but this is safer re pathlen
|
||||||
h = hashlib.sha512(afsenc(fn)).digest()
|
h = hashlib.sha512(afsenc(fn)).digest()
|
||||||
@@ -97,10 +112,11 @@ def thumb_path(histpath: str, rem: str, mtime: float, fmt: str) -> str:
|
|||||||
if fmt in ("opus", "caf"):
|
if fmt in ("opus", "caf"):
|
||||||
cat = "ac"
|
cat = "ac"
|
||||||
else:
|
else:
|
||||||
fmt = "webp" if fmt == "w" else "png" if fmt == "p" else "jpg"
|
fc = fmt[:1]
|
||||||
|
fmt = "webp" if fc == "w" else "png" if fc == "p" else "jpg"
|
||||||
cat = "th"
|
cat = "th"
|
||||||
|
|
||||||
return "{}/{}/{}/{}.{:x}.{}".format(histpath, cat, rd, fn, int(mtime), fmt)
|
return "%s/%s/%s/%s.%x.%s" % (histpath, cat, rd, fn, int(mtime), fmt)
|
||||||
|
|
||||||
|
|
||||||
class ThumbSrv(object):
|
class ThumbSrv(object):
|
||||||
@@ -110,16 +126,16 @@ class ThumbSrv(object):
|
|||||||
self.args = hub.args
|
self.args = hub.args
|
||||||
self.log_func = hub.log
|
self.log_func = hub.log
|
||||||
|
|
||||||
res = hub.args.th_size.split("x")
|
|
||||||
self.res = tuple([int(x) for x in res])
|
|
||||||
self.poke_cd = Cooldown(self.args.th_poke)
|
self.poke_cd = Cooldown(self.args.th_poke)
|
||||||
|
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
self.busy: dict[str, list[threading.Condition]] = {}
|
self.busy: dict[str, list[threading.Condition]] = {}
|
||||||
|
self.ram: dict[str, float] = {}
|
||||||
|
self.memcond = threading.Condition(self.mutex)
|
||||||
self.stopping = False
|
self.stopping = False
|
||||||
self.nthr = max(1, self.args.th_mt)
|
self.nthr = max(1, self.args.th_mt)
|
||||||
|
|
||||||
self.q: Queue[Optional[tuple[str, str]]] = Queue(self.nthr * 4)
|
self.q: Queue[Optional[tuple[str, str, str, VFS]]] = Queue(self.nthr * 4)
|
||||||
for n in range(self.nthr):
|
for n in range(self.nthr):
|
||||||
Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr))
|
Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr))
|
||||||
|
|
||||||
@@ -184,20 +200,24 @@ class ThumbSrv(object):
|
|||||||
with self.mutex:
|
with self.mutex:
|
||||||
return not self.nthr
|
return not self.nthr
|
||||||
|
|
||||||
|
def getres(self, vn: VFS) -> tuple[int, int]:
|
||||||
|
w, h = vn.flags["thsize"].split("x")
|
||||||
|
return int(w), int(h)
|
||||||
|
|
||||||
def get(self, ptop: str, rem: str, mtime: float, fmt: str) -> Optional[str]:
|
def get(self, ptop: str, rem: str, mtime: float, fmt: str) -> Optional[str]:
|
||||||
histpath = self.asrv.vfs.histtab.get(ptop)
|
histpath = self.asrv.vfs.histtab.get(ptop)
|
||||||
if not histpath:
|
if not histpath:
|
||||||
self.log("no histpath for [{}]".format(ptop))
|
self.log("no histpath for [{}]".format(ptop))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
tpath = thumb_path(histpath, rem, mtime, fmt)
|
tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa)
|
||||||
abspath = os.path.join(ptop, rem)
|
abspath = os.path.join(ptop, rem)
|
||||||
cond = threading.Condition(self.mutex)
|
cond = threading.Condition(self.mutex)
|
||||||
do_conv = False
|
do_conv = False
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
try:
|
try:
|
||||||
self.busy[tpath].append(cond)
|
self.busy[tpath].append(cond)
|
||||||
self.log("wait {}".format(tpath))
|
self.log("joined waiting room for %s" % (tpath,))
|
||||||
except:
|
except:
|
||||||
thdir = os.path.dirname(tpath)
|
thdir = os.path.dirname(tpath)
|
||||||
bos.makedirs(os.path.join(thdir, "w"))
|
bos.makedirs(os.path.join(thdir, "w"))
|
||||||
@@ -211,8 +231,14 @@ class ThumbSrv(object):
|
|||||||
do_conv = True
|
do_conv = True
|
||||||
|
|
||||||
if do_conv:
|
if do_conv:
|
||||||
self.q.put((abspath, tpath))
|
allvols = list(self.asrv.vfs.all_vols.values())
|
||||||
self.log("conv {} \033[0m{}".format(tpath, abspath), c=6)
|
vn = next((x for x in allvols if x.realpath == ptop), None)
|
||||||
|
if not vn:
|
||||||
|
self.log("ptop [{}] not in {}".format(ptop, allvols), 3)
|
||||||
|
vn = self.asrv.vfs.all_aps[0][1]
|
||||||
|
|
||||||
|
self.q.put((abspath, tpath, fmt, vn))
|
||||||
|
self.log("conv {} :{} \033[0m{}".format(tpath, fmt, abspath), c=6)
|
||||||
|
|
||||||
while not self.stopping:
|
while not self.stopping:
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
@@ -242,13 +268,30 @@ class ThumbSrv(object):
|
|||||||
"ffa": self.fmt_ffa,
|
"ffa": self.fmt_ffa,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def wait4ram(self, need: float, ttpath: str) -> None:
|
||||||
|
ram = self.args.th_ram_max
|
||||||
|
if need > ram * 0.99:
|
||||||
|
t = "file too big; need %.2f GiB RAM, but --th-ram-max is only %.1f"
|
||||||
|
raise Exception(t % (need, ram))
|
||||||
|
|
||||||
|
while True:
|
||||||
|
with self.mutex:
|
||||||
|
used = sum([v for k, v in self.ram.items() if k != ttpath]) + need
|
||||||
|
if used < ram:
|
||||||
|
# self.log("XXX self.ram: %s" % (self.ram,), 5)
|
||||||
|
self.ram[ttpath] = need
|
||||||
|
return
|
||||||
|
with self.memcond:
|
||||||
|
# self.log("at RAM limit; used %.2f GiB, need %.2f more" % (used-need, need), 1)
|
||||||
|
self.memcond.wait(3)
|
||||||
|
|
||||||
def worker(self) -> None:
|
def worker(self) -> None:
|
||||||
while not self.stopping:
|
while not self.stopping:
|
||||||
task = self.q.get()
|
task = self.q.get()
|
||||||
if not task:
|
if not task:
|
||||||
break
|
break
|
||||||
|
|
||||||
abspath, tpath = task
|
abspath, tpath, fmt, vn = task
|
||||||
ext = abspath.split(".")[-1].lower()
|
ext = abspath.split(".")[-1].lower()
|
||||||
png_ok = False
|
png_ok = False
|
||||||
funs = []
|
funs = []
|
||||||
@@ -274,10 +317,14 @@ class ThumbSrv(object):
|
|||||||
|
|
||||||
tdir, tfn = os.path.split(tpath)
|
tdir, tfn = os.path.split(tpath)
|
||||||
ttpath = os.path.join(tdir, "w", tfn)
|
ttpath = os.path.join(tdir, "w", tfn)
|
||||||
|
try:
|
||||||
|
wunlink(self.log, ttpath, vn.flags)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
for fun in funs:
|
for fun in funs:
|
||||||
try:
|
try:
|
||||||
fun(abspath, ttpath)
|
fun(abspath, ttpath, fmt, vn)
|
||||||
break
|
break
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
msg = "{} could not create thumbnail of {}\n{}"
|
msg = "{} could not create thumbnail of {}\n{}"
|
||||||
@@ -291,7 +338,7 @@ class ThumbSrv(object):
|
|||||||
else:
|
else:
|
||||||
# ffmpeg may spawn empty files on windows
|
# ffmpeg may spawn empty files on windows
|
||||||
try:
|
try:
|
||||||
os.unlink(ttpath)
|
wunlink(self.log, ttpath, vn.flags)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -303,17 +350,22 @@ class ThumbSrv(object):
|
|||||||
with self.mutex:
|
with self.mutex:
|
||||||
subs = self.busy[tpath]
|
subs = self.busy[tpath]
|
||||||
del self.busy[tpath]
|
del self.busy[tpath]
|
||||||
|
self.ram.pop(ttpath, None)
|
||||||
|
|
||||||
for x in subs:
|
for x in subs:
|
||||||
with x:
|
with x:
|
||||||
x.notify_all()
|
x.notify_all()
|
||||||
|
|
||||||
|
with self.memcond:
|
||||||
|
self.memcond.notify_all()
|
||||||
|
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
self.nthr -= 1
|
self.nthr -= 1
|
||||||
|
|
||||||
def fancy_pillow(self, im: "Image.Image") -> "Image.Image":
|
def fancy_pillow(self, im: "Image.Image", fmt: str, vn: VFS) -> "Image.Image":
|
||||||
# exif_transpose is expensive (loads full image + unconditional copy)
|
# exif_transpose is expensive (loads full image + unconditional copy)
|
||||||
r = max(*self.res) * 2
|
res = self.getres(vn)
|
||||||
|
r = max(*res) * 2
|
||||||
im.thumbnail((r, r), resample=Image.LANCZOS)
|
im.thumbnail((r, r), resample=Image.LANCZOS)
|
||||||
try:
|
try:
|
||||||
k = next(k for k, v in ExifTags.TAGS.items() if v == "Orientation")
|
k = next(k for k, v in ExifTags.TAGS.items() if v == "Orientation")
|
||||||
@@ -327,23 +379,24 @@ class ThumbSrv(object):
|
|||||||
if rot in rots:
|
if rot in rots:
|
||||||
im = im.transpose(rots[rot])
|
im = im.transpose(rots[rot])
|
||||||
|
|
||||||
if self.args.th_no_crop:
|
if fmt.endswith("f"):
|
||||||
im.thumbnail(self.res, resample=Image.LANCZOS)
|
im.thumbnail(res, resample=Image.LANCZOS)
|
||||||
else:
|
else:
|
||||||
iw, ih = im.size
|
iw, ih = im.size
|
||||||
dw, dh = self.res
|
dw, dh = res
|
||||||
res = (min(iw, dw), min(ih, dh))
|
res = (min(iw, dw), min(ih, dh))
|
||||||
im = ImageOps.fit(im, res, method=Image.LANCZOS)
|
im = ImageOps.fit(im, res, method=Image.LANCZOS)
|
||||||
|
|
||||||
return im
|
return im
|
||||||
|
|
||||||
def conv_pil(self, abspath: str, tpath: str) -> None:
|
def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||||
|
self.wait4ram(0.2, tpath)
|
||||||
with Image.open(fsenc(abspath)) as im:
|
with Image.open(fsenc(abspath)) as im:
|
||||||
try:
|
try:
|
||||||
im = self.fancy_pillow(im)
|
im = self.fancy_pillow(im, fmt, vn)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.log("fancy_pillow {}".format(ex), "90")
|
self.log("fancy_pillow {}".format(ex), "90")
|
||||||
im.thumbnail(self.res)
|
im.thumbnail(self.getres(vn))
|
||||||
|
|
||||||
fmts = ["RGB", "L"]
|
fmts = ["RGB", "L"]
|
||||||
args = {"quality": 40}
|
args = {"quality": 40}
|
||||||
@@ -354,7 +407,7 @@ class ThumbSrv(object):
|
|||||||
# method 0 = pillow-default, fast
|
# method 0 = pillow-default, fast
|
||||||
# method 4 = ffmpeg-default
|
# method 4 = ffmpeg-default
|
||||||
# method 6 = max, slow
|
# method 6 = max, slow
|
||||||
fmts += ["RGBA", "LA"]
|
fmts.extend(("RGBA", "LA"))
|
||||||
args["method"] = 6
|
args["method"] = 6
|
||||||
else:
|
else:
|
||||||
# default q = 75
|
# default q = 75
|
||||||
@@ -366,12 +419,13 @@ class ThumbSrv(object):
|
|||||||
|
|
||||||
im.save(tpath, **args)
|
im.save(tpath, **args)
|
||||||
|
|
||||||
def conv_vips(self, abspath: str, tpath: str) -> None:
|
def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||||
|
self.wait4ram(0.2, tpath)
|
||||||
crops = ["centre", "none"]
|
crops = ["centre", "none"]
|
||||||
if self.args.th_no_crop:
|
if fmt.endswith("f"):
|
||||||
crops = ["none"]
|
crops = ["none"]
|
||||||
|
|
||||||
w, h = self.res
|
w, h = self.getres(vn)
|
||||||
kw = {"height": h, "size": "down", "intent": "relative"}
|
kw = {"height": h, "size": "down", "intent": "relative"}
|
||||||
|
|
||||||
for c in crops:
|
for c in crops:
|
||||||
@@ -383,10 +437,12 @@ class ThumbSrv(object):
|
|||||||
if c == crops[-1]:
|
if c == crops[-1]:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
assert img # type: ignore
|
||||||
img.write_to_file(tpath, Q=40)
|
img.write_to_file(tpath, Q=40)
|
||||||
|
|
||||||
def conv_ffmpeg(self, abspath: str, tpath: str) -> None:
|
def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||||
ret, _ = ffprobe(abspath, int(self.args.th_convt / 2))
|
self.wait4ram(0.2, tpath)
|
||||||
|
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||||
if not ret:
|
if not ret:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -398,12 +454,13 @@ class ThumbSrv(object):
|
|||||||
seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")]
|
seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")]
|
||||||
|
|
||||||
scale = "scale={0}:{1}:force_original_aspect_ratio="
|
scale = "scale={0}:{1}:force_original_aspect_ratio="
|
||||||
if self.args.th_no_crop:
|
if fmt.endswith("f"):
|
||||||
scale += "decrease,setsar=1:1"
|
scale += "decrease,setsar=1:1"
|
||||||
else:
|
else:
|
||||||
scale += "increase,crop={0}:{1},setsar=1:1"
|
scale += "increase,crop={0}:{1},setsar=1:1"
|
||||||
|
|
||||||
bscale = scale.format(*list(self.res)).encode("utf-8")
|
res = self.getres(vn)
|
||||||
|
bscale = scale.format(*list(res)).encode("utf-8")
|
||||||
# fmt: off
|
# fmt: off
|
||||||
cmd = [
|
cmd = [
|
||||||
b"ffmpeg",
|
b"ffmpeg",
|
||||||
@@ -435,11 +492,11 @@ class ThumbSrv(object):
|
|||||||
]
|
]
|
||||||
|
|
||||||
cmd += [fsenc(tpath)]
|
cmd += [fsenc(tpath)]
|
||||||
self._run_ff(cmd)
|
self._run_ff(cmd, vn)
|
||||||
|
|
||||||
def _run_ff(self, cmd: list[bytes]) -> None:
|
def _run_ff(self, cmd: list[bytes], vn: VFS, oom: int = 400) -> None:
|
||||||
# self.log((b" ".join(cmd)).decode("utf-8"))
|
# self.log((b" ".join(cmd)).decode("utf-8"))
|
||||||
ret, _, serr = runcmd(cmd, timeout=self.args.th_convt)
|
ret, _, serr = runcmd(cmd, timeout=vn.flags["convt"], nice=True, oom=oom)
|
||||||
if not ret:
|
if not ret:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -482,13 +539,26 @@ class ThumbSrv(object):
|
|||||||
self.log(t + txt, c=c)
|
self.log(t + txt, c=c)
|
||||||
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
|
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
|
||||||
|
|
||||||
def conv_waves(self, abspath: str, tpath: str) -> None:
|
def conv_waves(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||||
ret, _ = ffprobe(abspath, int(self.args.th_convt / 2))
|
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||||
if "ac" not in ret:
|
if "ac" not in ret:
|
||||||
raise Exception("not audio")
|
raise Exception("not audio")
|
||||||
|
|
||||||
flt = (
|
# jt_versi.xm: 405M/839s
|
||||||
b"[0:a:0]"
|
dur = ret[".dur"][1] if ".dur" in ret else 300
|
||||||
|
need = 0.2 + dur / 3000
|
||||||
|
speedup = b""
|
||||||
|
if need > self.args.th_ram_max * 0.7:
|
||||||
|
self.log("waves too big (need %.2f GiB); trying to optimize" % (need,))
|
||||||
|
need = 0.2 + dur / 4200 # only helps about this much...
|
||||||
|
speedup = b"aresample=8000,"
|
||||||
|
if need > self.args.th_ram_max * 0.96:
|
||||||
|
raise Exception("file too big; cannot waves")
|
||||||
|
|
||||||
|
self.wait4ram(need, tpath)
|
||||||
|
|
||||||
|
flt = b"[0:a:0]" + speedup
|
||||||
|
flt += (
|
||||||
b"compand=.3|.3:1|1:-90/-60|-60/-40|-40/-30|-20/-20:6:0:-90:0.2"
|
b"compand=.3|.3:1|1:-90/-60|-60/-40|-40/-30|-20/-20:6:0:-90:0.2"
|
||||||
b",volume=2"
|
b",volume=2"
|
||||||
b",showwavespic=s=2048x64:colors=white"
|
b",showwavespic=s=2048x64:colors=white"
|
||||||
@@ -508,13 +578,22 @@ class ThumbSrv(object):
|
|||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
cmd += [fsenc(tpath)]
|
cmd += [fsenc(tpath)]
|
||||||
self._run_ff(cmd)
|
self._run_ff(cmd, vn)
|
||||||
|
|
||||||
def conv_spec(self, abspath: str, tpath: str) -> None:
|
def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||||
ret, _ = ffprobe(abspath, int(self.args.th_convt / 2))
|
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||||
if "ac" not in ret:
|
if "ac" not in ret:
|
||||||
raise Exception("not audio")
|
raise Exception("not audio")
|
||||||
|
|
||||||
|
# https://trac.ffmpeg.org/ticket/10797
|
||||||
|
# expect 1 GiB every 600 seconds when duration is tricky;
|
||||||
|
# simple filetypes are generally safer so let's special-case those
|
||||||
|
safe = ("flac", "wav", "aif", "aiff", "opus")
|
||||||
|
coeff = 1800 if abspath.split(".")[-1].lower() in safe else 600
|
||||||
|
dur = ret[".dur"][1] if ".dur" in ret else 300
|
||||||
|
need = 0.2 + dur / coeff
|
||||||
|
self.wait4ram(need, tpath)
|
||||||
|
|
||||||
fc = "[0:a:0]aresample=48000{},showspectrumpic=s=640x512,crop=780:544:70:50[o]"
|
fc = "[0:a:0]aresample=48000{},showspectrumpic=s=640x512,crop=780:544:70:50[o]"
|
||||||
|
|
||||||
if self.args.th_ff_swr:
|
if self.args.th_ff_swr:
|
||||||
@@ -551,13 +630,14 @@ class ThumbSrv(object):
|
|||||||
]
|
]
|
||||||
|
|
||||||
cmd += [fsenc(tpath)]
|
cmd += [fsenc(tpath)]
|
||||||
self._run_ff(cmd)
|
self._run_ff(cmd, vn)
|
||||||
|
|
||||||
def conv_opus(self, abspath: str, tpath: str) -> None:
|
def conv_opus(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||||
if self.args.no_acode:
|
if self.args.no_acode:
|
||||||
raise Exception("disabled in server config")
|
raise Exception("disabled in server config")
|
||||||
|
|
||||||
ret, _ = ffprobe(abspath, int(self.args.th_convt / 2))
|
self.wait4ram(0.2, tpath)
|
||||||
|
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||||
if "ac" not in ret:
|
if "ac" not in ret:
|
||||||
raise Exception("not audio")
|
raise Exception("not audio")
|
||||||
|
|
||||||
@@ -570,11 +650,15 @@ class ThumbSrv(object):
|
|||||||
want_caf = tpath.endswith(".caf")
|
want_caf = tpath.endswith(".caf")
|
||||||
tmp_opus = tpath
|
tmp_opus = tpath
|
||||||
if want_caf:
|
if want_caf:
|
||||||
tmp_opus = tpath.rsplit(".", 1)[0] + ".opus"
|
tmp_opus = tpath + ".opus"
|
||||||
|
try:
|
||||||
|
wunlink(self.log, tmp_opus, vn.flags)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
caf_src = abspath if src_opus else tmp_opus
|
caf_src = abspath if src_opus else tmp_opus
|
||||||
|
|
||||||
if not want_caf or (not src_opus and not bos.path.isfile(tmp_opus)):
|
if not want_caf or not src_opus:
|
||||||
# fmt: off
|
# fmt: off
|
||||||
cmd = [
|
cmd = [
|
||||||
b"ffmpeg",
|
b"ffmpeg",
|
||||||
@@ -589,7 +673,7 @@ class ThumbSrv(object):
|
|||||||
fsenc(tmp_opus)
|
fsenc(tmp_opus)
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
self._run_ff(cmd)
|
self._run_ff(cmd, vn, oom=300)
|
||||||
|
|
||||||
# iOS fails to play some "insufficiently complex" files
|
# iOS fails to play some "insufficiently complex" files
|
||||||
# (average file shorter than 8 seconds), so of course we
|
# (average file shorter than 8 seconds), so of course we
|
||||||
@@ -613,7 +697,7 @@ class ThumbSrv(object):
|
|||||||
fsenc(tpath)
|
fsenc(tpath)
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
self._run_ff(cmd)
|
self._run_ff(cmd, vn, oom=300)
|
||||||
|
|
||||||
elif want_caf:
|
elif want_caf:
|
||||||
# simple remux should be safe
|
# simple remux should be safe
|
||||||
@@ -631,7 +715,13 @@ class ThumbSrv(object):
|
|||||||
fsenc(tpath)
|
fsenc(tpath)
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
self._run_ff(cmd)
|
self._run_ff(cmd, vn, oom=300)
|
||||||
|
|
||||||
|
if tmp_opus != tpath:
|
||||||
|
try:
|
||||||
|
wunlink(self.log, tmp_opus, vn.flags)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
def poke(self, tdir: str) -> None:
|
def poke(self, tdir: str) -> None:
|
||||||
if not self.poke_cd.poke(tdir):
|
if not self.poke_cd.poke(tdir):
|
||||||
@@ -656,7 +746,10 @@ class ThumbSrv(object):
|
|||||||
else:
|
else:
|
||||||
self.log("\033[Jcln {} ({})/\033[A".format(histpath, vol))
|
self.log("\033[Jcln {} ({})/\033[A".format(histpath, vol))
|
||||||
|
|
||||||
ndirs += self.clean(histpath)
|
try:
|
||||||
|
ndirs += self.clean(histpath)
|
||||||
|
except Exception as ex:
|
||||||
|
self.log("\033[Jcln err in %s: %r" % (histpath, ex), 3)
|
||||||
|
|
||||||
self.log("\033[Jcln ok; rm {} dirs".format(ndirs))
|
self.log("\033[Jcln ok; rm {} dirs".format(ndirs))
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import time
|
|||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
|
||||||
from .__init__ import ANYWIN, TYPE_CHECKING, unicode
|
from .__init__ import ANYWIN, TYPE_CHECKING, unicode
|
||||||
|
from .authsrv import LEELOO_DALLAS, VFS
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
from .up2k import up2k_wark_from_hashlist
|
from .up2k import up2k_wark_from_hashlist
|
||||||
from .util import (
|
from .util import (
|
||||||
@@ -20,6 +21,7 @@ from .util import (
|
|||||||
min_ex,
|
min_ex,
|
||||||
quotep,
|
quotep,
|
||||||
s3dec,
|
s3dec,
|
||||||
|
vjoin,
|
||||||
)
|
)
|
||||||
|
|
||||||
if HAVE_SQLITE3:
|
if HAVE_SQLITE3:
|
||||||
@@ -61,7 +63,7 @@ class U2idx(object):
|
|||||||
self.log_func("u2idx", msg, c)
|
self.log_func("u2idx", msg, c)
|
||||||
|
|
||||||
def fsearch(
|
def fsearch(
|
||||||
self, vols: list[tuple[str, str, dict[str, Any]]], body: dict[str, Any]
|
self, uname: str, vols: list[VFS], body: dict[str, Any]
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""search by up2k hashlist"""
|
"""search by up2k hashlist"""
|
||||||
if not HAVE_SQLITE3:
|
if not HAVE_SQLITE3:
|
||||||
@@ -69,13 +71,13 @@ class U2idx(object):
|
|||||||
|
|
||||||
fsize = body["size"]
|
fsize = body["size"]
|
||||||
fhash = body["hash"]
|
fhash = body["hash"]
|
||||||
wark = up2k_wark_from_hashlist(self.args.salt, fsize, fhash)
|
wark = up2k_wark_from_hashlist(self.args.warksalt, fsize, fhash)
|
||||||
|
|
||||||
uq = "substr(w,1,16) = ? and w = ?"
|
uq = "substr(w,1,16) = ? and w = ?"
|
||||||
uv: list[Union[str, int]] = [wark[:16], wark]
|
uv: list[Union[str, int]] = [wark[:16], wark]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.run_query(vols, uq, uv, True, False, 99999)[0]
|
return self.run_query(uname, vols, uq, uv, False, 99999)[0]
|
||||||
except:
|
except:
|
||||||
raise Pebkac(500, min_ex())
|
raise Pebkac(500, min_ex())
|
||||||
|
|
||||||
@@ -101,7 +103,7 @@ class U2idx(object):
|
|||||||
uri = ""
|
uri = ""
|
||||||
try:
|
try:
|
||||||
uri = "{}?mode=ro&nolock=1".format(Path(db_path).as_uri())
|
uri = "{}?mode=ro&nolock=1".format(Path(db_path).as_uri())
|
||||||
db = sqlite3.connect(uri, 2, uri=True, check_same_thread=False)
|
db = sqlite3.connect(uri, timeout=2, uri=True, check_same_thread=False)
|
||||||
cur = db.cursor()
|
cur = db.cursor()
|
||||||
cur.execute('pragma table_info("up")').fetchone()
|
cur.execute('pragma table_info("up")').fetchone()
|
||||||
self.log("ro: {}".format(db_path))
|
self.log("ro: {}".format(db_path))
|
||||||
@@ -113,14 +115,14 @@ class U2idx(object):
|
|||||||
if not cur:
|
if not cur:
|
||||||
# on windows, this steals the write-lock from up2k.deferred_init --
|
# 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
|
# seen on win 10.0.17763.2686, py 3.10.4, sqlite 3.37.2
|
||||||
cur = sqlite3.connect(db_path, 2, check_same_thread=False).cursor()
|
cur = sqlite3.connect(db_path, timeout=2, check_same_thread=False).cursor()
|
||||||
self.log("opened {}".format(db_path))
|
self.log("opened {}".format(db_path))
|
||||||
|
|
||||||
self.cur[ptop] = cur
|
self.cur[ptop] = cur
|
||||||
return cur
|
return cur
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
self, vols: list[tuple[str, str, dict[str, Any]]], uq: str, lim: int
|
self, uname: str, vols: list[VFS], uq: str, lim: int
|
||||||
) -> tuple[list[dict[str, Any]], list[str], bool]:
|
) -> tuple[list[dict[str, Any]], list[str], bool]:
|
||||||
"""search by query params"""
|
"""search by query params"""
|
||||||
if not HAVE_SQLITE3:
|
if not HAVE_SQLITE3:
|
||||||
@@ -129,7 +131,6 @@ class U2idx(object):
|
|||||||
q = ""
|
q = ""
|
||||||
v: Union[str, int] = ""
|
v: Union[str, int] = ""
|
||||||
va: list[Union[str, int]] = []
|
va: list[Union[str, int]] = []
|
||||||
have_up = False # query has up.* operands
|
|
||||||
have_mt = False
|
have_mt = False
|
||||||
is_key = True
|
is_key = True
|
||||||
is_size = False
|
is_size = False
|
||||||
@@ -174,21 +175,21 @@ class U2idx(object):
|
|||||||
if v == "size":
|
if v == "size":
|
||||||
v = "up.sz"
|
v = "up.sz"
|
||||||
is_size = True
|
is_size = True
|
||||||
have_up = True
|
|
||||||
|
|
||||||
elif v == "date":
|
elif v == "date":
|
||||||
v = "up.mt"
|
v = "up.mt"
|
||||||
is_date = True
|
is_date = True
|
||||||
have_up = True
|
|
||||||
|
elif v == "up_at":
|
||||||
|
v = "up.at"
|
||||||
|
is_date = True
|
||||||
|
|
||||||
elif v == "path":
|
elif v == "path":
|
||||||
v = "trim(?||up.rd,'/')"
|
v = "trim(?||up.rd,'/')"
|
||||||
va.append("\nrd")
|
va.append("\nrd")
|
||||||
have_up = True
|
|
||||||
|
|
||||||
elif v == "name":
|
elif v == "name":
|
||||||
v = "up.fn"
|
v = "up.fn"
|
||||||
have_up = True
|
|
||||||
|
|
||||||
elif v == "tags" or ptn_mt.match(v):
|
elif v == "tags" or ptn_mt.match(v):
|
||||||
have_mt = True
|
have_mt = True
|
||||||
@@ -264,19 +265,24 @@ class U2idx(object):
|
|||||||
q += " lower({}) {} ? ) ".format(field, oper)
|
q += " lower({}) {} ? ) ".format(field, oper)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.run_query(vols, q, va, have_up, have_mt, lim)
|
return self.run_query(uname, vols, q, va, have_mt, lim)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise Pebkac(500, repr(ex))
|
raise Pebkac(500, repr(ex))
|
||||||
|
|
||||||
def run_query(
|
def run_query(
|
||||||
self,
|
self,
|
||||||
vols: list[tuple[str, str, dict[str, Any]]],
|
uname: str,
|
||||||
|
vols: list[VFS],
|
||||||
uq: str,
|
uq: str,
|
||||||
uv: list[Union[str, int]],
|
uv: list[Union[str, int]],
|
||||||
have_up: bool,
|
|
||||||
have_mt: bool,
|
have_mt: bool,
|
||||||
lim: int,
|
lim: int,
|
||||||
) -> tuple[list[dict[str, Any]], list[str], bool]:
|
) -> tuple[list[dict[str, Any]], list[str], bool]:
|
||||||
|
if self.args.srch_dbg:
|
||||||
|
t = "searching across all %s volumes in which the user has 'r' (full read access):\n %s"
|
||||||
|
zs = "\n ".join(["/%s = %s" % (x.vpath, x.realpath) for x in vols])
|
||||||
|
self.log(t % (len(vols), zs), 5)
|
||||||
|
|
||||||
done_flag: list[bool] = []
|
done_flag: list[bool] = []
|
||||||
self.active_id = "{:.6f}_{}".format(
|
self.active_id = "{:.6f}_{}".format(
|
||||||
time.time(), threading.current_thread().ident
|
time.time(), threading.current_thread().ident
|
||||||
@@ -295,13 +301,35 @@ class U2idx(object):
|
|||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
seen_rps: set[str] = set()
|
seen_rps: set[str] = set()
|
||||||
lim = min(lim, int(self.args.srch_hits))
|
clamp = int(self.args.srch_hits)
|
||||||
|
if lim >= clamp:
|
||||||
|
lim = clamp
|
||||||
|
clamped = True
|
||||||
|
else:
|
||||||
|
clamped = False
|
||||||
|
|
||||||
taglist = {}
|
taglist = {}
|
||||||
for (vtop, ptop, flags) in vols:
|
for vol in vols:
|
||||||
|
if lim < 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
vtop = vol.vpath
|
||||||
|
ptop = vol.realpath
|
||||||
|
flags = vol.flags
|
||||||
|
|
||||||
cur = self.get_cur(ptop)
|
cur = self.get_cur(ptop)
|
||||||
if not cur:
|
if not cur:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
excl = []
|
||||||
|
for vp2 in self.asrv.vfs.all_vols.keys():
|
||||||
|
if vp2.startswith((vtop + "/").lstrip("/")) and vtop != vp2:
|
||||||
|
excl.append(vp2[len(vtop) :].lstrip("/"))
|
||||||
|
|
||||||
|
if self.args.srch_dbg:
|
||||||
|
t = "searching in volume /%s (%s), excludelist %s"
|
||||||
|
self.log(t % (vtop, ptop, excl), 5)
|
||||||
|
|
||||||
self.active_cur = cur
|
self.active_cur = cur
|
||||||
|
|
||||||
vuv = []
|
vuv = []
|
||||||
@@ -313,7 +341,8 @@ class U2idx(object):
|
|||||||
|
|
||||||
sret = []
|
sret = []
|
||||||
fk = flags.get("fk")
|
fk = flags.get("fk")
|
||||||
dots = flags.get("dotsrch")
|
dots = flags.get("dotsrch") and uname in vol.axs.udot
|
||||||
|
fk_alg = 2 if "fka" in flags else 1
|
||||||
c = cur.execute(uq, tuple(vuv))
|
c = cur.execute(uq, tuple(vuv))
|
||||||
for hit in c:
|
for hit in c:
|
||||||
w, ts, sz, rd, fn, ip, at = hit[:7]
|
w, ts, sz, rd, fn, ip, at = hit[:7]
|
||||||
@@ -321,6 +350,13 @@ class U2idx(object):
|
|||||||
if rd.startswith("//") or fn.startswith("//"):
|
if rd.startswith("//") or fn.startswith("//"):
|
||||||
rd, fn = s3dec(rd, fn)
|
rd, fn = s3dec(rd, fn)
|
||||||
|
|
||||||
|
if rd in excl or any([x for x in excl if rd.startswith(x + "/")]):
|
||||||
|
if self.args.srch_dbg:
|
||||||
|
zs = vjoin(vjoin(vtop, rd), fn)
|
||||||
|
t = "database inconsistency in volume '/%s'; ignoring: %s"
|
||||||
|
self.log(t % (vtop, zs), 1)
|
||||||
|
continue
|
||||||
|
|
||||||
rp = quotep("/".join([x for x in [vtop, rd, fn] if x]))
|
rp = quotep("/".join([x for x in [vtop, rd, fn] if x]))
|
||||||
if not dots and "/." in ("/" + rp):
|
if not dots and "/." in ("/" + rp):
|
||||||
continue
|
continue
|
||||||
@@ -333,21 +369,35 @@ class U2idx(object):
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
ap = absreal(os.path.join(ptop, rd, fn))
|
ap = absreal(os.path.join(ptop, rd, fn))
|
||||||
inf = bos.stat(ap)
|
ino = 0 if ANYWIN or fk_alg == 2 else bos.stat(ap).st_ino
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
suf = (
|
suf = "?k=" + gen_filekey(
|
||||||
"?k="
|
fk_alg,
|
||||||
+ gen_filekey(
|
self.args.fk_salt,
|
||||||
self.args.fk_salt, ap, sz, 0 if ANYWIN else inf.st_ino
|
ap,
|
||||||
)[:fk]
|
sz,
|
||||||
)
|
ino,
|
||||||
|
)[:fk]
|
||||||
|
|
||||||
lim -= 1
|
lim -= 1
|
||||||
if lim < 0:
|
if lim < 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if self.args.srch_dbg:
|
||||||
|
t = "in volume '/%s': hit: %s"
|
||||||
|
self.log(t % (vtop, rp), 5)
|
||||||
|
|
||||||
|
zs = vjoin(vtop, rp)
|
||||||
|
chk_vn, _ = self.asrv.vfs.get(zs, LEELOO_DALLAS, True, False)
|
||||||
|
chk_vn = chk_vn.dbv or chk_vn
|
||||||
|
if chk_vn.vpath != vtop:
|
||||||
|
raise Exception(
|
||||||
|
"database inconsistency! in volume '/%s' (%s), found file [%s] which belongs to volume '/%s' (%s)"
|
||||||
|
% (vtop, ptop, zs, chk_vn.vpath, chk_vn.realpath)
|
||||||
|
)
|
||||||
|
|
||||||
seen_rps.add(rp)
|
seen_rps.add(rp)
|
||||||
sret.append({"ts": int(ts), "sz": sz, "rp": rp + suf, "w": w[:16]})
|
sret.append({"ts": int(ts), "sz": sz, "rp": rp + suf, "w": w[:16]})
|
||||||
|
|
||||||
@@ -365,12 +415,16 @@ class U2idx(object):
|
|||||||
ret.extend(sret)
|
ret.extend(sret)
|
||||||
# print("[{}] {}".format(ptop, sret))
|
# print("[{}] {}".format(ptop, sret))
|
||||||
|
|
||||||
|
if self.args.srch_dbg:
|
||||||
|
t = "in volume '/%s': got %d hits, %d total so far"
|
||||||
|
self.log(t % (vtop, len(sret), len(ret)), 5)
|
||||||
|
|
||||||
done_flag.append(True)
|
done_flag.append(True)
|
||||||
self.active_id = ""
|
self.active_id = ""
|
||||||
|
|
||||||
ret.sort(key=itemgetter("rp"))
|
ret.sort(key=itemgetter("rp"))
|
||||||
|
|
||||||
return ret, list(taglist.keys()), lim < 0
|
return ret, list(taglist.keys()), lim < 0 and not clamped
|
||||||
|
|
||||||
def terminator(self, identifier: str, done_flag: list[bool]) -> None:
|
def terminator(self, identifier: str, done_flag: list[bool]) -> None:
|
||||||
for _ in range(self.timeout):
|
for _ in range(self.timeout):
|
||||||
|
|||||||
1088
copyparty/up2k.py
1088
copyparty/up2k.py
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ pk: $(addsuffix .gz, $(wildcard *.js *.css))
|
|||||||
un: $(addsuffix .un, $(wildcard *.gz))
|
un: $(addsuffix .un, $(wildcard *.gz))
|
||||||
|
|
||||||
%.gz: %
|
%.gz: %
|
||||||
pigz -11 -J 34 -I 5730 $<
|
pigz -11 -J 34 -I 573 $<
|
||||||
|
|
||||||
%.un: %
|
%.un: %
|
||||||
pigz -d $<
|
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
|
|
||||||
@@ -127,7 +127,7 @@ window.baguetteBox = (function () {
|
|||||||
var gallery = [];
|
var gallery = [];
|
||||||
[].forEach.call(tagsNodeList, function (imageElement, imageIndex) {
|
[].forEach.call(tagsNodeList, function (imageElement, imageIndex) {
|
||||||
var imageElementClickHandler = function (e) {
|
var imageElementClickHandler = function (e) {
|
||||||
if (ctrl(e))
|
if (ctrl(e) || e && e.shiftKey)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
e.preventDefault ? e.preventDefault() : e.returnValue = false;
|
e.preventDefault ? e.preventDefault() : e.returnValue = false;
|
||||||
@@ -255,19 +255,19 @@ window.baguetteBox = (function () {
|
|||||||
if (anymod(e, true))
|
if (anymod(e, true))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var k = e.code + '', v = vid(), pos = -1;
|
var k = (e.code || e.key) + '', v = vid(), pos = -1;
|
||||||
|
|
||||||
if (k == "BracketLeft")
|
if (k == "BracketLeft")
|
||||||
setloop(1);
|
setloop(1);
|
||||||
else if (k == "BracketRight")
|
else if (k == "BracketRight")
|
||||||
setloop(2);
|
setloop(2);
|
||||||
else if (e.shiftKey)
|
else if (e.shiftKey && k != "KeyR" && k != "R")
|
||||||
return;
|
return;
|
||||||
else if (k == "ArrowLeft" || k == "KeyJ")
|
else if (k == "ArrowLeft" || k == "KeyJ" || k == "Left" || k == "j")
|
||||||
showPreviousImage();
|
showPreviousImage();
|
||||||
else if (k == "ArrowRight" || k == "KeyL")
|
else if (k == "ArrowRight" || k == "KeyL" || k == "Right" || k == "l")
|
||||||
showNextImage();
|
showNextImage();
|
||||||
else if (k == "Escape")
|
else if (k == "Escape" || k == "Esc")
|
||||||
hideOverlay();
|
hideOverlay();
|
||||||
else if (k == "Home")
|
else if (k == "Home")
|
||||||
showFirstImage(e);
|
showFirstImage(e);
|
||||||
@@ -295,9 +295,9 @@ window.baguetteBox = (function () {
|
|||||||
}
|
}
|
||||||
else if (k == "KeyF")
|
else if (k == "KeyF")
|
||||||
tglfull();
|
tglfull();
|
||||||
else if (k == "KeyS")
|
else if (k == "KeyS" || k == "s")
|
||||||
tglsel();
|
tglsel();
|
||||||
else if (k == "KeyR")
|
else if (k == "KeyR" || k == "r" || k == "R")
|
||||||
rotn(e.shiftKey ? -1 : 1);
|
rotn(e.shiftKey ? -1 : 1);
|
||||||
else if (k == "KeyY")
|
else if (k == "KeyY")
|
||||||
dlpic();
|
dlpic();
|
||||||
@@ -310,7 +310,7 @@ window.baguetteBox = (function () {
|
|||||||
options = {};
|
options = {};
|
||||||
setOptions(o);
|
setOptions(o);
|
||||||
if (tt.en)
|
if (tt.en)
|
||||||
tt.show.bind(this)();
|
tt.show.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVmode() {
|
function setVmode() {
|
||||||
@@ -356,7 +356,7 @@ window.baguetteBox = (function () {
|
|||||||
|
|
||||||
setVmode();
|
setVmode();
|
||||||
if (tt.en)
|
if (tt.en)
|
||||||
tt.show.bind(this)();
|
tt.show.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findfile() {
|
function findfile() {
|
||||||
@@ -376,7 +376,12 @@ window.baguetteBox = (function () {
|
|||||||
else
|
else
|
||||||
(vid() || ebi('bbox-overlay')).requestFullscreen();
|
(vid() || ebi('bbox-overlay')).requestFullscreen();
|
||||||
}
|
}
|
||||||
catch (ex) { alert(ex); }
|
catch (ex) {
|
||||||
|
if (IPHONE)
|
||||||
|
alert('sorry, apple decided to make this impossible on iphones (should work on ipad tho)');
|
||||||
|
else
|
||||||
|
alert(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function tglsel() {
|
function tglsel() {
|
||||||
@@ -519,7 +524,7 @@ window.baguetteBox = (function () {
|
|||||||
options[item] = newOptions[item];
|
options[item] = newOptions[item];
|
||||||
}
|
}
|
||||||
|
|
||||||
var an = options.animation = sread('ganim') || anims[ANIM ? 0 : 2];
|
var an = options.animation = sread('ganim', anims) || anims[ANIM ? 0 : 2];
|
||||||
btnAnim.textContent = ['⇄', '⮺', '⚡'][anims.indexOf(an)];
|
btnAnim.textContent = ['⇄', '⮺', '⚡'][anims.indexOf(an)];
|
||||||
btnAnim.setAttribute('tt', 'animation: ' + an);
|
btnAnim.setAttribute('tt', 'animation: ' + an);
|
||||||
|
|
||||||
@@ -610,7 +615,43 @@ window.baguetteBox = (function () {
|
|||||||
|
|
||||||
documentLastFocus && documentLastFocus.focus();
|
documentLastFocus && documentLastFocus.focus();
|
||||||
isOverlayVisible = false;
|
isOverlayVisible = false;
|
||||||
}, 500);
|
unvid();
|
||||||
|
unfig();
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unvid(keep) {
|
||||||
|
var vids = QSA('#bbox-overlay video');
|
||||||
|
for (var a = vids.length - 1; a >= 0; a--) {
|
||||||
|
var v = vids[a];
|
||||||
|
if (v == keep)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
v.src = '';
|
||||||
|
v.load();
|
||||||
|
|
||||||
|
var p = v.parentNode;
|
||||||
|
p.removeChild(v);
|
||||||
|
p.parentNode.removeChild(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unfig(keep) {
|
||||||
|
var figs = QSA('#bbox-overlay figure'),
|
||||||
|
npre = options.preload || 0,
|
||||||
|
k = [];
|
||||||
|
|
||||||
|
if (keep === undefined)
|
||||||
|
keep = -9;
|
||||||
|
|
||||||
|
for (var a = keep - npre; a <= keep + npre; a++)
|
||||||
|
k.push('bbox-figure-' + a);
|
||||||
|
|
||||||
|
for (var a = figs.length - 1; a >= 0; a--) {
|
||||||
|
var f = figs[a];
|
||||||
|
if (!has(k, f.getAttribute('id')))
|
||||||
|
f.parentNode.removeChild(f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadImage(index, callback) {
|
function loadImage(index, callback) {
|
||||||
@@ -703,6 +744,7 @@ window.baguetteBox = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function show(index, gallery) {
|
function show(index, gallery) {
|
||||||
|
gallery = gallery || currentGallery;
|
||||||
if (!isOverlayVisible && index >= 0 && index < gallery.length) {
|
if (!isOverlayVisible && index >= 0 && index < gallery.length) {
|
||||||
prepareOverlay(gallery, options);
|
prepareOverlay(gallery, options);
|
||||||
showOverlay(index);
|
showOverlay(index);
|
||||||
@@ -715,12 +757,10 @@ window.baguetteBox = (function () {
|
|||||||
if (index >= imagesElements.length)
|
if (index >= imagesElements.length)
|
||||||
return bounceAnimation('right');
|
return bounceAnimation('right');
|
||||||
|
|
||||||
var v = vid();
|
try {
|
||||||
if (v) {
|
vid().pause();
|
||||||
v.src = '';
|
|
||||||
v.load();
|
|
||||||
v.parentNode.removeChild(v);
|
|
||||||
}
|
}
|
||||||
|
catch (ex) { }
|
||||||
|
|
||||||
currentIndex = index;
|
currentIndex = index;
|
||||||
loadImage(currentIndex, function () {
|
loadImage(currentIndex, function () {
|
||||||
@@ -729,6 +769,15 @@ window.baguetteBox = (function () {
|
|||||||
});
|
});
|
||||||
updateOffset();
|
updateOffset();
|
||||||
|
|
||||||
|
if (options.animation == 'none')
|
||||||
|
unvid(vid());
|
||||||
|
else
|
||||||
|
setTimeout(function () {
|
||||||
|
unvid(vid());
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
unfig(index);
|
||||||
|
|
||||||
if (options.onChange)
|
if (options.onChange)
|
||||||
options.onChange(currentIndex, imagesElements.length);
|
options.onChange(currentIndex, imagesElements.length);
|
||||||
|
|
||||||
@@ -870,7 +919,7 @@ window.baguetteBox = (function () {
|
|||||||
|
|
||||||
if (loopB !== null) {
|
if (loopB !== null) {
|
||||||
timer.add(loopchk);
|
timer.add(loopchk);
|
||||||
sethash(window.location.hash.slice(1).split('&')[0] + '&t=' + (loopA || 0) + '-' + loopB);
|
sethash(location.hash.slice(1).split('&')[0] + '&t=' + (loopA || 0) + '-' + loopB);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -968,7 +1017,7 @@ window.baguetteBox = (function () {
|
|||||||
clmod(btnPrev, 'off', 't');
|
clmod(btnPrev, 'off', 't');
|
||||||
clmod(btnNext, 'off', 't');
|
clmod(btnNext, 'off', 't');
|
||||||
|
|
||||||
if (Date.now() - ctime <= 500)
|
if (Date.now() - ctime <= 500 && !IPHONE)
|
||||||
tglfull();
|
tglfull();
|
||||||
|
|
||||||
ctime = Date.now();
|
ctime = Date.now();
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
--u2-sbtn-b1: #999;
|
--u2-sbtn-b1: #999;
|
||||||
--u2-txt-bg: var(--bg-u5);
|
--u2-txt-bg: var(--bg-u5);
|
||||||
--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));
|
--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));
|
||||||
|
--u2-tab-b1: rgba(128,128,128,0.8);
|
||||||
--u2-tab-1-fg: #fd7;
|
--u2-tab-1-fg: #fd7;
|
||||||
--u2-tab-1-bg: linear-gradient(to bottom, var(#353), var(--bg) 80%);
|
--u2-tab-1-bg: linear-gradient(to bottom, var(#353), var(--bg) 80%);
|
||||||
--u2-tab-1-b1: #7c5;
|
--u2-tab-1-b1: #7c5;
|
||||||
@@ -270,6 +271,7 @@ html.bz {
|
|||||||
--btn-1h-fg: #000;
|
--btn-1h-fg: #000;
|
||||||
--txt-sh: a;
|
--txt-sh: a;
|
||||||
|
|
||||||
|
--u2-tab-b1: var(--bg-u5);
|
||||||
--u2-tab-1-fg: var(--fg-max);
|
--u2-tab-1-fg: var(--fg-max);
|
||||||
--u2-tab-1-bg: var(--bg);
|
--u2-tab-1-bg: var(--bg);
|
||||||
|
|
||||||
@@ -329,6 +331,7 @@ html.c {
|
|||||||
html.cz {
|
html.cz {
|
||||||
--bgg: var(--bg-u2);
|
--bgg: var(--bg-u2);
|
||||||
--srv-3: #fff;
|
--srv-3: #fff;
|
||||||
|
--u2-tab-b1: var(--bg-d3);
|
||||||
}
|
}
|
||||||
html.cy {
|
html.cy {
|
||||||
--fg: #fff;
|
--fg: #fff;
|
||||||
@@ -411,10 +414,11 @@ html.dz {
|
|||||||
--op-aa-bg: var(--bg-d2);
|
--op-aa-bg: var(--bg-d2);
|
||||||
--op-a-sh: rgba(0,0,0,0.5);
|
--op-a-sh: rgba(0,0,0,0.5);
|
||||||
|
|
||||||
--u2-btn-b1: #999;
|
--u2-btn-b1: var(--fg-weak);
|
||||||
--u2-sbtn-b1: #999;
|
--u2-sbtn-b1: var(--fg-weak);
|
||||||
--u2-txt-bg: var(--bg-u5);
|
--u2-txt-bg: var(--bg-u5);
|
||||||
--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));
|
--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));
|
||||||
|
--u2-tab-b1: var(--fg-weak);
|
||||||
--u2-tab-1-fg: #fff;
|
--u2-tab-1-fg: #fff;
|
||||||
--u2-tab-1-bg: linear-gradient(to bottom, var(#353), var(--bg) 80%);
|
--u2-tab-1-bg: linear-gradient(to bottom, var(#353), var(--bg) 80%);
|
||||||
--u2-tab-1-b1: #7c5;
|
--u2-tab-1-b1: #7c5;
|
||||||
@@ -423,6 +427,12 @@ html.dz {
|
|||||||
--u2-b-fg: #fff;
|
--u2-b-fg: #fff;
|
||||||
--u2-b1-bg: #3a3;
|
--u2-b1-bg: #3a3;
|
||||||
--u2-b2-bg: #3a3;
|
--u2-b2-bg: #3a3;
|
||||||
|
--u2-o-bg: var(--btn-bg);
|
||||||
|
--u2-o-b1: var(--bg-u5);
|
||||||
|
--u2-o-h-bg: var(--fg-weak);
|
||||||
|
--u2-o-1-bg: var(--fg-weak);
|
||||||
|
--u2-o-1-b1: var(--a);
|
||||||
|
--u2-o-1h-bg: var(--a);
|
||||||
--u2-inf-bg: #07a;
|
--u2-inf-bg: #07a;
|
||||||
--u2-inf-b1: #0be;
|
--u2-inf-b1: #0be;
|
||||||
--u2-ok-bg: #380;
|
--u2-ok-bg: #380;
|
||||||
@@ -483,6 +493,7 @@ html.dz {
|
|||||||
--err-ts: #500;
|
--err-ts: #500;
|
||||||
|
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
|
font-family: 'scp', monospace, monospace;
|
||||||
}
|
}
|
||||||
html.dy {
|
html.dy {
|
||||||
--fg: #000;
|
--fg: #000;
|
||||||
@@ -717,18 +728,22 @@ a:hover {
|
|||||||
html.y #files thead th {
|
html.y #files thead th {
|
||||||
box-shadow: 0 1px 0 rgba(0,0,0,0.12);
|
box-shadow: 0 1px 0 rgba(0,0,0,0.12);
|
||||||
}
|
}
|
||||||
|
html #files.hhpick thead th {
|
||||||
|
color: #f7d;
|
||||||
|
background: #000;
|
||||||
|
box-shadow: .1em .2em 0 #f6c inset, -.1em -.1em 0 #f6c inset;
|
||||||
|
}
|
||||||
#files td {
|
#files td {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: .3em .5em;
|
padding: .3em .5em;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
|
max-width: var(--file-td-w);
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
#files tr:nth-child(2n) td {
|
#files tr:nth-child(2n) td {
|
||||||
background: var(--row-alt);
|
background: var(--row-alt);
|
||||||
}
|
}
|
||||||
#files td+td+td {
|
|
||||||
max-width: 30em;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
#files td+td {
|
#files td+td {
|
||||||
box-shadow: 1px 0 0 0 rgba(128,128,128,var(--f-sh1)) inset, 0 1px 0 rgba(255,255,255,var(--f-sh2)) inset, 0 -1px 0 rgba(255,255,255,var(--f-sh2)) inset;
|
box-shadow: 1px 0 0 0 rgba(128,128,128,var(--f-sh1)) inset, 0 1px 0 rgba(255,255,255,var(--f-sh2)) inset, 0 -1px 0 rgba(255,255,255,var(--f-sh2)) inset;
|
||||||
}
|
}
|
||||||
@@ -803,6 +818,10 @@ html.y #path a:hover {
|
|||||||
.logue:empty {
|
.logue:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.logue.raw {
|
||||||
|
white-space: pre;
|
||||||
|
font-family: 'scp', 'consolas', monospace;
|
||||||
|
}
|
||||||
#doc>iframe,
|
#doc>iframe,
|
||||||
.logue>iframe {
|
.logue>iframe {
|
||||||
background: var(--bgg);
|
background: var(--bgg);
|
||||||
@@ -851,7 +870,7 @@ html.y #path a:hover {
|
|||||||
}
|
}
|
||||||
.mdo,
|
.mdo,
|
||||||
.mdo * {
|
.mdo * {
|
||||||
line-height: 1.4em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
#srv_info,
|
#srv_info,
|
||||||
#srv_info2,
|
#srv_info2,
|
||||||
@@ -1159,10 +1178,10 @@ html.y #widget.open {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
background: var(--bg-u3);
|
background: var(--bg-u3);
|
||||||
}
|
}
|
||||||
#wfm, #wzip, #wnp {
|
#wfs, #wfm, #wzip, #wnp {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#wzip, #wnp {
|
#wfs, #wzip, #wnp {
|
||||||
margin-right: .2em;
|
margin-right: .2em;
|
||||||
padding-right: .2em;
|
padding-right: .2em;
|
||||||
border: 1px solid var(--bg-u5);
|
border: 1px solid var(--bg-u5);
|
||||||
@@ -1174,6 +1193,7 @@ html.y #widget.open {
|
|||||||
padding-left: .2em;
|
padding-left: .2em;
|
||||||
border-left-width: .1em;
|
border-left-width: .1em;
|
||||||
}
|
}
|
||||||
|
#wfs.act,
|
||||||
#wfm.act {
|
#wfm.act {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@@ -1197,6 +1217,13 @@ html.y #widget.open {
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
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,
|
#wfm span,
|
||||||
#wnp span {
|
#wnp span {
|
||||||
font-size: .6em;
|
font-size: .6em;
|
||||||
@@ -1212,7 +1239,8 @@ html.y #widget.open {
|
|||||||
#wfm a.hide {
|
#wfm a.hide {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#files tbody tr.fcut td {
|
#files tbody tr.fcut td,
|
||||||
|
#ggrid>a.fcut {
|
||||||
animation: fcut .5s ease-out;
|
animation: fcut .5s ease-out;
|
||||||
}
|
}
|
||||||
@keyframes fcut {
|
@keyframes fcut {
|
||||||
@@ -1379,6 +1407,9 @@ input[type="checkbox"]:checked+label {
|
|||||||
color: #0e0;
|
color: #0e0;
|
||||||
color: var(--a);
|
color: var(--a);
|
||||||
}
|
}
|
||||||
|
html.dz input {
|
||||||
|
font-family: 'scp', monospace, monospace;
|
||||||
|
}
|
||||||
.opwide div>span>input+label {
|
.opwide div>span>input+label {
|
||||||
padding: .3em 0 .3em .3em;
|
padding: .3em 0 .3em .3em;
|
||||||
margin: 0 0 0 -.3em;
|
margin: 0 0 0 -.3em;
|
||||||
@@ -1387,14 +1418,17 @@ input[type="checkbox"]:checked+label {
|
|||||||
.opview input.i {
|
.opview input.i {
|
||||||
width: calc(100% - 16.2em);
|
width: calc(100% - 16.2em);
|
||||||
}
|
}
|
||||||
|
input.drc_v,
|
||||||
input.eq_gain {
|
input.eq_gain {
|
||||||
width: 3em;
|
width: 3em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0 .6em;
|
margin: 0 .6em;
|
||||||
}
|
}
|
||||||
|
#audio_drc table,
|
||||||
#audio_eq table {
|
#audio_eq table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
#audio_drc td,
|
||||||
#audio_eq td {
|
#audio_eq td {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -1403,11 +1437,15 @@ input.eq_gain {
|
|||||||
display: block;
|
display: block;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
#au_drc,
|
||||||
#au_eq {
|
#au_eq {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: .5em;
|
margin-top: .5em;
|
||||||
padding: 1.3em .3em;
|
padding: 1.3em .3em;
|
||||||
}
|
}
|
||||||
|
#au_drc {
|
||||||
|
padding: .4em .3em;
|
||||||
|
}
|
||||||
#ico1 {
|
#ico1 {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -1448,6 +1486,8 @@ input.eq_gain {
|
|||||||
width: calc(100% - 2em);
|
width: calc(100% - 2em);
|
||||||
margin: .3em 0 0 1.4em;
|
margin: .3em 0 0 1.4em;
|
||||||
}
|
}
|
||||||
|
@media (max-width: 130em) { #srch_form.tags #tq_raw { width: calc(100% - 34em) } }
|
||||||
|
@media (max-width: 95em) { #srch_form.tags #tq_raw { width: calc(100% - 2em) } }
|
||||||
#tq_raw td+td {
|
#tq_raw td+td {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -1559,6 +1599,7 @@ html.cz .btn {
|
|||||||
border-bottom: .2em solid #709;
|
border-bottom: .2em solid #709;
|
||||||
}
|
}
|
||||||
html.dz .btn {
|
html.dz .btn {
|
||||||
|
font-size: 1em;
|
||||||
box-shadow: 0 0 0 .1em #080 inset;
|
box-shadow: 0 0 0 .1em #080 inset;
|
||||||
}
|
}
|
||||||
html.dz .tgl.btn.on {
|
html.dz .tgl.btn.on {
|
||||||
@@ -1602,6 +1643,12 @@ html.cz .tgl.btn.on {
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
border-top: 1px solid var(--bg-u5);
|
border-top: 1px solid var(--bg-u5);
|
||||||
}
|
}
|
||||||
|
#tree li.offline>a:first-child:before {
|
||||||
|
content: '❌';
|
||||||
|
position: absolute;
|
||||||
|
margin-left: -.25em;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
#tree ul a.sel {
|
#tree ul a.sel {
|
||||||
background: #000;
|
background: #000;
|
||||||
background: var(--bg-d3);
|
background: var(--bg-d3);
|
||||||
@@ -1610,7 +1657,9 @@ html.cz .tgl.btn.on {
|
|||||||
color: var(--fg-max);
|
color: var(--fg-max);
|
||||||
}
|
}
|
||||||
#tree ul a.hl {
|
#tree ul a.hl {
|
||||||
|
color: #fff;
|
||||||
color: var(--btn-1-fg);
|
color: var(--btn-1-fg);
|
||||||
|
background: #000;
|
||||||
background: var(--btn-1-bg);
|
background: var(--btn-1-bg);
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
@@ -1726,6 +1775,7 @@ html.y #tree.nowrap .ntree a+a:hover {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
#thumbs,
|
#thumbs,
|
||||||
|
#au_prescan,
|
||||||
#au_fullpre,
|
#au_fullpre,
|
||||||
#au_os_seek,
|
#au_os_seek,
|
||||||
#au_osd_cv,
|
#au_osd_cv,
|
||||||
@@ -1733,7 +1783,8 @@ html.y #tree.nowrap .ntree a+a:hover {
|
|||||||
opacity: .3;
|
opacity: .3;
|
||||||
}
|
}
|
||||||
#griden.on+#thumbs,
|
#griden.on+#thumbs,
|
||||||
#au_preload.on+#au_fullpre,
|
#au_preload.on+#au_prescan,
|
||||||
|
#au_preload.on+#au_prescan+#au_fullpre,
|
||||||
#au_os_ctl.on+#au_os_seek,
|
#au_os_ctl.on+#au_os_seek,
|
||||||
#au_os_ctl.on+#au_os_seek+#au_osd_cv,
|
#au_os_ctl.on+#au_os_seek+#au_osd_cv,
|
||||||
#u2turbo.on+#u2tdate {
|
#u2turbo.on+#u2tdate {
|
||||||
@@ -1743,6 +1794,8 @@ html.y #tree.nowrap .ntree a+a:hover {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.ghead {
|
.ghead {
|
||||||
|
background: #fff;
|
||||||
|
background: var(--bg-u2);
|
||||||
border-radius: .3em;
|
border-radius: .3em;
|
||||||
padding: .2em .5em;
|
padding: .2em .5em;
|
||||||
line-height: 2.3em;
|
line-height: 2.3em;
|
||||||
@@ -1772,6 +1825,7 @@ html.y #tree.nowrap .ntree a+a:hover {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
#rui {
|
#rui {
|
||||||
|
background: #fff;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -1828,6 +1882,7 @@ html.y #tree.nowrap .ntree a+a:hover {
|
|||||||
}
|
}
|
||||||
#doc {
|
#doc {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
background: #fff;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
margin: -1em 0 .5em 0;
|
margin: -1em 0 .5em 0;
|
||||||
padding: 1em 0 1em 0;
|
padding: 1em 0 1em 0;
|
||||||
@@ -1844,6 +1899,10 @@ html.y #doc {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
}
|
}
|
||||||
|
#docul li.bn span {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--fg-max);
|
||||||
|
}
|
||||||
#doc.prism {
|
#doc.prism {
|
||||||
padding-left: 3em;
|
padding-left: 3em;
|
||||||
}
|
}
|
||||||
@@ -2082,12 +2141,12 @@ html.y #bbox-overlay figcaption a {
|
|||||||
}
|
}
|
||||||
.bbox-btn,
|
.bbox-btn,
|
||||||
#bbox-btns {
|
#bbox-btns {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
animation: opacity .2s infinite ease-in-out;
|
animation: opacity .2s infinite ease-in-out;
|
||||||
}
|
}
|
||||||
.bbox-btn.off,
|
.bbox-btn.off,
|
||||||
#bbox-btns.off {
|
#bbox-btns.off {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
#bbox-overlay button {
|
#bbox-overlay button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -2123,6 +2182,7 @@ html.y #bbox-overlay figcaption a {
|
|||||||
}
|
}
|
||||||
#bbox-halp {
|
#bbox-halp {
|
||||||
color: var(--fg-max);
|
color: var(--fg-max);
|
||||||
|
background: #fff;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -2357,7 +2417,7 @@ html.y #bbox-overlay figcaption a {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
#u2bm sup {
|
#u2bm sup {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
#u2notbtn {
|
#u2notbtn {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -2456,7 +2516,7 @@ html.y #bbox-overlay figcaption a {
|
|||||||
width: 21em;
|
width: 21em;
|
||||||
}
|
}
|
||||||
#u2cards {
|
#u2cards {
|
||||||
padding: 1em 1em .3em 1em;
|
padding: 1em 1em .42em 1em;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -2464,14 +2524,14 @@ html.y #bbox-overlay figcaption a {
|
|||||||
min-width: 24em;
|
min-width: 24em;
|
||||||
}
|
}
|
||||||
#u2cards.w {
|
#u2cards.w {
|
||||||
width: 44em;
|
width: 48em;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
#u2cards.ww {
|
#u2cards.ww {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
#u2etaw.w {
|
#u2etaw.w {
|
||||||
width: 52em;
|
width: 55em;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin: 2em auto -2.7em auto;
|
margin: 2em auto -2.7em auto;
|
||||||
}
|
}
|
||||||
@@ -2481,7 +2541,8 @@ html.y #bbox-overlay figcaption a {
|
|||||||
#u2cards a {
|
#u2cards a {
|
||||||
padding: .2em 1em;
|
padding: .2em 1em;
|
||||||
background: var(--u2-tab-bg);
|
background: var(--u2-tab-bg);
|
||||||
border: 1px solid rgba(128,128,128,0.8);
|
border: 1px solid #999;
|
||||||
|
border-color: var(--u2-tab-b1);
|
||||||
border-width: 0 0 1px 0;
|
border-width: 0 0 1px 0;
|
||||||
}
|
}
|
||||||
#u2cards a:first-child {
|
#u2cards a:first-child {
|
||||||
@@ -2515,10 +2576,10 @@ html.y #bbox-overlay figcaption a {
|
|||||||
width: 30em;
|
width: 30em;
|
||||||
}
|
}
|
||||||
#u2conf.w {
|
#u2conf.w {
|
||||||
width: 48em;
|
width: 51em;
|
||||||
}
|
}
|
||||||
#u2conf.ww {
|
#u2conf.ww {
|
||||||
width: 78em;
|
width: 82em;
|
||||||
}
|
}
|
||||||
#u2conf.ww #u2c3w {
|
#u2conf.ww #u2c3w {
|
||||||
width: 29em;
|
width: 29em;
|
||||||
@@ -2740,6 +2801,9 @@ html.c .opbox,
|
|||||||
html.a .opbox {
|
html.a .opbox {
|
||||||
margin: 1.5em 0 0 0;
|
margin: 1.5em 0 0 0;
|
||||||
}
|
}
|
||||||
|
html.dz .opview input.i {
|
||||||
|
width: calc(100% - 18em);
|
||||||
|
}
|
||||||
html.c #tree,
|
html.c #tree,
|
||||||
html.c #treeh,
|
html.c #treeh,
|
||||||
html.a #tree,
|
html.a #tree,
|
||||||
@@ -2792,6 +2856,9 @@ html.a #u2btn {
|
|||||||
html.ay #u2btn {
|
html.ay #u2btn {
|
||||||
box-shadow: .4em .4em 0 #ccc;
|
box-shadow: .4em .4em 0 #ccc;
|
||||||
}
|
}
|
||||||
|
html.dz #u2btn {
|
||||||
|
letter-spacing: -.033em;
|
||||||
|
}
|
||||||
html.c #u2conf.ww #u2btn,
|
html.c #u2conf.ww #u2btn,
|
||||||
html.a #u2conf.ww #u2btn {
|
html.a #u2conf.ww #u2btn {
|
||||||
margin: -2em .5em -3em 0;
|
margin: -2em .5em -3em 0;
|
||||||
@@ -2939,6 +3006,7 @@ html.b #treepar {
|
|||||||
html.b #wrap {
|
html.b #wrap {
|
||||||
margin-top: 2em;
|
margin-top: 2em;
|
||||||
}
|
}
|
||||||
|
html.by .ghead,
|
||||||
html.bz .ghead {
|
html.bz .ghead {
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
padding: .2em 0;
|
padding: .2em 0;
|
||||||
@@ -2953,13 +3021,13 @@ html.b .btn {
|
|||||||
top: -.1em;
|
top: -.1em;
|
||||||
}
|
}
|
||||||
html.b #op_up2k.srch sup {
|
html.b #op_up2k.srch sup {
|
||||||
color: #fc0;
|
color: #fc0;
|
||||||
}
|
}
|
||||||
html.by #u2btn sup {
|
html.by #u2btn sup {
|
||||||
color: #06b;
|
color: #06b;
|
||||||
}
|
}
|
||||||
html.by #op_up2k.srch sup {
|
html.by #op_up2k.srch sup {
|
||||||
color: #b70;
|
color: #b70;
|
||||||
}
|
}
|
||||||
html.bz #u2cards a.act {
|
html.bz #u2cards a.act {
|
||||||
box-shadow: 0 -.1em .2em var(--bg-d2);
|
box-shadow: 0 -.1em .2em var(--bg-d2);
|
||||||
@@ -3008,6 +3076,16 @@ html.d #treepar {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 32em) {
|
||||||
|
#u2conf {
|
||||||
|
font-size: .9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 28em) {
|
||||||
|
#u2conf {
|
||||||
|
font-size: .8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
@media (min-width: 70em) {
|
@media (min-width: 70em) {
|
||||||
#barpos,
|
#barpos,
|
||||||
#barbuf {
|
#barbuf {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/browser.css?_={{ ts }}">
|
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/browser.css?_={{ ts }}">
|
||||||
{%- if css %}
|
{%- if css %}
|
||||||
<link rel="stylesheet" media="screen" href="{{ css }}?_={{ ts }}">
|
<link rel="stylesheet" media="screen" href="{{ css }}_={{ ts }}">
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
<div id="op_player" class="opview opbox opwide"></div>
|
<div id="op_player" class="opview opbox opwide"></div>
|
||||||
|
|
||||||
<div id="op_bup" class="opview opbox act">
|
<div id="op_bup" class="opview opbox {% if not ls0 %}act{% endif %}">
|
||||||
<div id="u2err"></div>
|
<div id="u2err"></div>
|
||||||
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
||||||
<input type="hidden" name="act" value="bput" />
|
<input type="hidden" name="act" value="bput" />
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<a id="bbsw" href="?b=u" rel="nofollow"><br />switch to basic browser</a>
|
<a id="bbsw" href="?b=u" rel="nofollow"><br />switch to basic browser</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="op_mkdir" class="opview opbox act">
|
<div id="op_mkdir" class="opview opbox {% if not ls0 %}act{% endif %}">
|
||||||
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
||||||
<input type="hidden" name="act" value="mkdir" />
|
<input type="hidden" name="act" value="mkdir" />
|
||||||
📂<input type="text" name="name" class="i" placeholder="awesome mix vol.1">
|
📂<input type="text" name="name" class="i" placeholder="awesome mix vol.1">
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="op_msg" class="opview opbox act">
|
<div id="op_msg" class="opview opbox {% if not ls0 %}act{% endif %}">
|
||||||
<form method="post" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" action="{{ url_suf }}">
|
<form method="post" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" action="{{ url_suf }}">
|
||||||
📟<input type="text" name="msg" class="i" placeholder="lorem ipsum dolor sit amet">
|
📟<input type="text" name="msg" class="i" placeholder="lorem ipsum dolor sit amet">
|
||||||
<input type="submit" value="send msg to srv log">
|
<input type="submit" value="send msg to srv log">
|
||||||
@@ -135,43 +135,28 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
var SR = {{ r|tojson }},
|
var SR = {{ r|tojson }},
|
||||||
|
CGV = {{ cgv|tojson }},
|
||||||
TS = "{{ ts }}",
|
TS = "{{ ts }}",
|
||||||
acct = "{{ acct }}",
|
|
||||||
perms = {{ perms }},
|
|
||||||
themes = {{ themes }},
|
|
||||||
dtheme = "{{ dtheme }}",
|
dtheme = "{{ dtheme }}",
|
||||||
srvinf = "{{ srv_info }}",
|
srvinf = "{{ srv_info }}",
|
||||||
|
s_name = "{{ s_name }}",
|
||||||
lang = "{{ lang }}",
|
lang = "{{ lang }}",
|
||||||
dfavico = "{{ favico }}",
|
dfavico = "{{ favico }}",
|
||||||
def_hcols = {{ def_hcols|tojson }},
|
|
||||||
have_up2k_idx = {{ have_up2k_idx|tojson }},
|
|
||||||
have_tags_idx = {{ have_tags_idx|tojson }},
|
have_tags_idx = {{ have_tags_idx|tojson }},
|
||||||
have_acode = {{ have_acode|tojson }},
|
|
||||||
have_mv = {{ have_mv|tojson }},
|
|
||||||
have_del = {{ have_del|tojson }},
|
|
||||||
have_unpost = {{ have_unpost }},
|
|
||||||
have_zip = {{ have_zip|tojson }},
|
|
||||||
sb_md = "{{ sb_md }}",
|
|
||||||
sb_lg = "{{ sb_lg }}",
|
sb_lg = "{{ sb_lg }}",
|
||||||
lifetime = {{ lifetime }},
|
|
||||||
turbolvl = {{ turbolvl }},
|
|
||||||
idxh = {{ idxh }},
|
|
||||||
frand = {{ frand|tojson }},
|
|
||||||
u2sort = "{{ u2sort }}",
|
|
||||||
have_emp = {{ have_emp|tojson }},
|
|
||||||
txt_ext = "{{ txt_ext }}",
|
txt_ext = "{{ txt_ext }}",
|
||||||
logues = {{ logues|tojson if sb_lg else "[]" }},
|
logues = {{ logues|tojson if sb_lg else "[]" }},
|
||||||
readme = {{ readme|tojson }},
|
|
||||||
ls0 = {{ ls0|tojson }};
|
ls0 = {{ ls0|tojson }};
|
||||||
|
|
||||||
document.documentElement.className = localStorage.theme || dtheme;
|
var STG = window.localStorage;
|
||||||
|
document.documentElement.className = (STG && STG.cpp_thm) || dtheme;
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ r }}/.cpr/util.js?_={{ ts }}"></script>
|
<script src="{{ r }}/.cpr/util.js?_={{ ts }}"></script>
|
||||||
<script src="{{ r }}/.cpr/baguettebox.js?_={{ ts }}"></script>
|
<script src="{{ r }}/.cpr/baguettebox.js?_={{ ts }}"></script>
|
||||||
<script src="{{ r }}/.cpr/browser.js?_={{ ts }}"></script>
|
<script src="{{ r }}/.cpr/browser.js?_={{ ts }}"></script>
|
||||||
<script src="{{ r }}/.cpr/up2k.js?_={{ ts }}"></script>
|
<script src="{{ r }}/.cpr/up2k.js?_={{ ts }}"></script>
|
||||||
{%- if js %}
|
{%- if js %}
|
||||||
<script src="{{ js }}?_={{ ts }}"></script>
|
<script src="{{ js }}_={{ ts }}"></script>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>{{ svcname }}</title>
|
<title>{{ s_doctitle }}</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<span id="lno">L#</span>
|
<span id="lno">L#</span>
|
||||||
{%- else %}
|
{%- else %}
|
||||||
<a href="{{ arg_base }}edit" tt="good: higher performance$Ngood: same document width as viewer$Nbad: assumes you know markdown">edit (basic)</a>
|
<a href="{{ arg_base }}edit" tt="good: higher performance$Ngood: same document width as viewer$Nbad: assumes you know markdown">edit (basic)</a>
|
||||||
<a href="{{ arg_base }}edit2" tt="not in-house so probably less buggy">edit (fancy)</a>
|
<a href="{{ arg_base }}edit2" id="edit2" tt="not in-house so probably less buggy">edit (fancy)</a>
|
||||||
<a href="{{ arg_base }}">view raw</a>
|
<a href="{{ arg_base }}">view raw</a>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -139,16 +139,15 @@ var md_opt = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
var l = localStorage,
|
var l = window.localStorage,
|
||||||
drk = l.light != 1,
|
drk = (l && l.light) != 1,
|
||||||
btn = document.getElementById("lightswitch"),
|
btn = document.getElementById("lightswitch"),
|
||||||
f = function (e) {
|
f = function (e) {
|
||||||
if (e) { e.preventDefault(); drk = !drk; }
|
if (e) { e.preventDefault(); drk = !drk; }
|
||||||
document.documentElement.className = drk? "z":"y";
|
document.documentElement.className = drk? "z":"y";
|
||||||
btn.innerHTML = "go " + (drk ? "light":"dark");
|
btn.innerHTML = "go " + (drk ? "light":"dark");
|
||||||
l.light = drk? 0:1;
|
try { l.light = drk? 0:1; } catch (ex) { }
|
||||||
};
|
};
|
||||||
|
|
||||||
btn.onclick = f;
|
btn.onclick = f;
|
||||||
f();
|
f();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ var img_load = (function () {
|
|||||||
var r = {};
|
var r = {};
|
||||||
r.callbacks = [];
|
r.callbacks = [];
|
||||||
|
|
||||||
function fire() {
|
var fire = function () {
|
||||||
for (var a = 0; a < r.callbacks.length; a++)
|
for (var a = 0; a < r.callbacks.length; a++)
|
||||||
r.callbacks[a]();
|
r.callbacks[a]();
|
||||||
}
|
}
|
||||||
@@ -212,8 +212,15 @@ function convert_markdown(md_text, dest_dom) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
var md_html = marked.parse(md_text, marked_opts);
|
var md_html = marked.parse(md_text, marked_opts);
|
||||||
|
if (!have_emp)
|
||||||
|
md_html = DOMPurify.sanitize(md_html);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
|
if (IE) {
|
||||||
|
dest_dom.innerHTML = 'IE cannot into markdown ;_;';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (ext)
|
if (ext)
|
||||||
md_plug_err(ex, ext[1]);
|
md_plug_err(ex, ext[1]);
|
||||||
|
|
||||||
@@ -470,7 +477,7 @@ img_load.callbacks = [toc.refresh];
|
|||||||
// scroll handler
|
// scroll handler
|
||||||
var redraw = (function () {
|
var redraw = (function () {
|
||||||
var sbs = true;
|
var sbs = true;
|
||||||
function onresize() {
|
var onresize = function () {
|
||||||
if (window.matchMedia)
|
if (window.matchMedia)
|
||||||
sbs = window.matchMedia('(min-width: 64em)').matches;
|
sbs = window.matchMedia('(min-width: 64em)').matches;
|
||||||
|
|
||||||
@@ -483,7 +490,7 @@ var redraw = (function () {
|
|||||||
onscroll();
|
onscroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onscroll() {
|
var onscroll = function () {
|
||||||
toc.refresh();
|
toc.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,6 +512,13 @@ dom_navtgl.onclick = function () {
|
|||||||
redraw();
|
redraw();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!HTTPS && location.hostname != '127.0.0.1') try {
|
||||||
|
ebi('edit2').onclick = function (e) {
|
||||||
|
toast.err(0, "the fancy editor is only available over https");
|
||||||
|
return ev(e);
|
||||||
|
}
|
||||||
|
} catch (ex) { }
|
||||||
|
|
||||||
if (sread('hidenav') == 1)
|
if (sread('hidenav') == 1)
|
||||||
dom_navtgl.onclick();
|
dom_navtgl.onclick();
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ var action_stack = null;
|
|||||||
var nlines = 0;
|
var nlines = 0;
|
||||||
var draw_md = (function () {
|
var draw_md = (function () {
|
||||||
var delay = 1;
|
var delay = 1;
|
||||||
function draw_md() {
|
var draw_md = function () {
|
||||||
var t0 = Date.now();
|
var t0 = Date.now();
|
||||||
var src = dom_src.value;
|
var src = dom_src.value;
|
||||||
convert_markdown(src, dom_pre);
|
convert_markdown(src, dom_pre);
|
||||||
@@ -135,7 +135,7 @@ img_load.callbacks = [function () {
|
|||||||
|
|
||||||
// resize handler
|
// resize handler
|
||||||
redraw = (function () {
|
redraw = (function () {
|
||||||
function onresize() {
|
var onresize = function () {
|
||||||
var y = (dom_hbar.offsetTop + dom_hbar.offsetHeight) + 'px';
|
var y = (dom_hbar.offsetTop + dom_hbar.offsetHeight) + 'px';
|
||||||
dom_wrap.style.top = y;
|
dom_wrap.style.top = y;
|
||||||
dom_swrap.style.top = y;
|
dom_swrap.style.top = y;
|
||||||
@@ -143,12 +143,12 @@ redraw = (function () {
|
|||||||
map_src = genmap(dom_ref, map_src);
|
map_src = genmap(dom_ref, map_src);
|
||||||
map_pre = genmap(dom_pre, map_pre);
|
map_pre = genmap(dom_pre, map_pre);
|
||||||
}
|
}
|
||||||
function setsbs() {
|
var setsbs = function () {
|
||||||
dom_wrap.className = '';
|
dom_wrap.className = '';
|
||||||
dom_swrap.className = '';
|
dom_swrap.className = '';
|
||||||
onresize();
|
onresize();
|
||||||
}
|
}
|
||||||
function modetoggle() {
|
var modetoggle = function () {
|
||||||
var mode = dom_nsbs.innerHTML;
|
var mode = dom_nsbs.innerHTML;
|
||||||
dom_nsbs.innerHTML = mode == 'editor' ? 'preview' : 'editor';
|
dom_nsbs.innerHTML = mode == 'editor' ? 'preview' : 'editor';
|
||||||
mode += ' single';
|
mode += ' single';
|
||||||
@@ -163,7 +163,7 @@ redraw = (function () {
|
|||||||
dom_sbs.onclick = setsbs;
|
dom_sbs.onclick = setsbs;
|
||||||
dom_nsbs.onclick = modetoggle;
|
dom_nsbs.onclick = modetoggle;
|
||||||
|
|
||||||
onresize();
|
(IE ? modetoggle : onresize)();
|
||||||
return onresize;
|
return onresize;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ redraw = (function () {
|
|||||||
(function () {
|
(function () {
|
||||||
var skip_src = false, skip_pre = false;
|
var skip_src = false, skip_pre = false;
|
||||||
|
|
||||||
function scroll(src, srcmap, dst, dstmap) {
|
var scroll = function (src, srcmap, dst, dstmap) {
|
||||||
var y = src.scrollTop;
|
var y = src.scrollTop;
|
||||||
if (y < 8) {
|
if (y < 8) {
|
||||||
dst.scrollTop = 0;
|
dst.scrollTop = 0;
|
||||||
@@ -278,6 +278,7 @@ function Modpoll() {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var new_md = this.responseText,
|
var new_md = this.responseText,
|
||||||
|
new_mt = this.getResponseHeader('X-Lastmod3') || r.lastmod,
|
||||||
server_ref = server_md.replace(/\r/g, ''),
|
server_ref = server_md.replace(/\r/g, ''),
|
||||||
server_now = new_md.replace(/\r/g, '');
|
server_now = new_md.replace(/\r/g, '');
|
||||||
|
|
||||||
@@ -285,6 +286,7 @@ function Modpoll() {
|
|||||||
if (r.initial && server_ref != server_now)
|
if (r.initial && server_ref != server_now)
|
||||||
return modal.confirm('Your browser decided to show an outdated copy of the document!\n\nDo you want to load the latest version from the server instead?', function () {
|
return modal.confirm('Your browser decided to show an outdated copy of the document!\n\nDo you want to load the latest version from the server instead?', function () {
|
||||||
dom_src.value = server_md = new_md;
|
dom_src.value = server_md = new_md;
|
||||||
|
last_modified = new_mt;
|
||||||
draw_md();
|
draw_md();
|
||||||
}, null);
|
}, null);
|
||||||
|
|
||||||
@@ -898,12 +900,12 @@ var set_lno = (function () {
|
|||||||
pv = null,
|
pv = null,
|
||||||
lno = ebi('lno');
|
lno = ebi('lno');
|
||||||
|
|
||||||
function poke() {
|
var poke = function () {
|
||||||
clearTimeout(t);
|
clearTimeout(t);
|
||||||
t = setTimeout(fire, 20);
|
t = setTimeout(fire, 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fire() {
|
var fire = function () {
|
||||||
try {
|
try {
|
||||||
clearTimeout(t);
|
clearTimeout(t);
|
||||||
|
|
||||||
@@ -928,8 +930,14 @@ var set_lno = (function () {
|
|||||||
|
|
||||||
// hotkeys / toolbar
|
// hotkeys / toolbar
|
||||||
(function () {
|
(function () {
|
||||||
function keydown(ev) {
|
var keydown = function (ev) {
|
||||||
ev = ev || window.event;
|
if (!ev && window.event) {
|
||||||
|
ev = window.event;
|
||||||
|
if (dev_fbw == 1) {
|
||||||
|
toast.warn(10, 'hello from fallback code ;_;\ncheck console trace');
|
||||||
|
console.error('using window.event');
|
||||||
|
}
|
||||||
|
}
|
||||||
var kc = ev.code || ev.keyCode || ev.which,
|
var kc = ev.code || ev.keyCode || ev.which,
|
||||||
editing = document.activeElement == dom_src;
|
editing = document.activeElement == dom_src;
|
||||||
|
|
||||||
@@ -1001,7 +1009,7 @@ var set_lno = (function () {
|
|||||||
md_home(ev.shiftKey);
|
md_home(ev.shiftKey);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ev.shiftKey && (ev.code == "Enter" || kc == 13)) {
|
if (!ev.shiftKey && ((ev.code + '').endsWith("Enter") || kc == 13)) {
|
||||||
return md_newline();
|
return md_newline();
|
||||||
}
|
}
|
||||||
if (!ev.shiftKey && kc == 8) {
|
if (!ev.shiftKey && kc == 8) {
|
||||||
@@ -1056,7 +1064,7 @@ action_stack = (function () {
|
|||||||
var ignore = false;
|
var ignore = false;
|
||||||
var ref = dom_src.value;
|
var ref = dom_src.value;
|
||||||
|
|
||||||
function diff(from, to, cpos) {
|
var diff = function (from, to, cpos) {
|
||||||
if (from === to)
|
if (from === to)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -1087,14 +1095,14 @@ action_stack = (function () {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function undiff(from, change) {
|
var undiff = function (from, change) {
|
||||||
return {
|
return {
|
||||||
txt: from.substring(0, change.car) + change.txt + from.substring(change.cdr),
|
txt: from.substring(0, change.car) + change.txt + from.substring(change.cdr),
|
||||||
cpos: change.cpos
|
cpos: change.cpos
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function apply(src, dst) {
|
var apply = function (src, dst) {
|
||||||
dbg('undos(%d) redos(%d)', hist.un.length, hist.re.length);
|
dbg('undos(%d) redos(%d)', hist.un.length, hist.re.length);
|
||||||
|
|
||||||
if (src.length === 0)
|
if (src.length === 0)
|
||||||
@@ -1118,7 +1126,7 @@ action_stack = (function () {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function schedule_push() {
|
var schedule_push = function () {
|
||||||
if (ignore) {
|
if (ignore) {
|
||||||
ignore = false;
|
ignore = false;
|
||||||
return;
|
return;
|
||||||
@@ -1129,7 +1137,7 @@ action_stack = (function () {
|
|||||||
sched_timer = setTimeout(push, 500);
|
sched_timer = setTimeout(push, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function undo() {
|
var undo = function () {
|
||||||
if (hist.re.length == 0) {
|
if (hist.re.length == 0) {
|
||||||
clearTimeout(sched_timer);
|
clearTimeout(sched_timer);
|
||||||
push();
|
push();
|
||||||
@@ -1137,11 +1145,11 @@ action_stack = (function () {
|
|||||||
return apply(hist.un, hist.re);
|
return apply(hist.un, hist.re);
|
||||||
}
|
}
|
||||||
|
|
||||||
function redo() {
|
var redo = function () {
|
||||||
return apply(hist.re, hist.un);
|
return apply(hist.re, hist.un);
|
||||||
}
|
}
|
||||||
|
|
||||||
function push() {
|
var push = function () {
|
||||||
var newtxt = dom_src.value;
|
var newtxt = dom_src.value;
|
||||||
var change = diff(ref, newtxt, sched_cpos);
|
var change = diff(ref, newtxt, sched_cpos);
|
||||||
if (change !== null)
|
if (change !== null)
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ var md_opt = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var lightswitch = (function () {
|
var lightswitch = (function () {
|
||||||
var l = localStorage,
|
var l = window.localStorage,
|
||||||
drk = l.light != 1,
|
drk = (l && l.light) != 1,
|
||||||
f = function (e) {
|
f = function (e) {
|
||||||
if (e) drk = !drk;
|
if (e) drk = !drk;
|
||||||
document.documentElement.className = drk? "z":"y";
|
document.documentElement.className = drk? "z":"y";
|
||||||
l.light = drk? 0:1;
|
try { l.light = drk? 0:1; } catch (ex) { }
|
||||||
};
|
};
|
||||||
f();
|
f();
|
||||||
return f;
|
return f;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>{{ svcname }}</title>
|
<title>{{ s_doctitle }}</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||||
<meta name="theme-color" content="#333">
|
<meta name="theme-color" content="#333">
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
{%- if redir %}
|
{%- if redir %}
|
||||||
<script>
|
<script>
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
window.location.replace("{{ redir }}");
|
location.replace("{{ redir }}");
|
||||||
}, 1000);
|
}, 1000);
|
||||||
</script>
|
</script>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>{{ svcname }}</title>
|
<title>{{ s_doctitle }}</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||||
<meta name="theme-color" content="#333">
|
<meta name="theme-color" content="#333">
|
||||||
@@ -110,7 +110,8 @@ var SR = {{ r|tojson }},
|
|||||||
lang="{{ lang }}",
|
lang="{{ lang }}",
|
||||||
dfavico="{{ favico }}";
|
dfavico="{{ favico }}";
|
||||||
|
|
||||||
document.documentElement.className=localStorage.theme||"{{ this.args.theme }}";
|
var STG = window.localStorage;
|
||||||
|
document.documentElement.className = (STG && STG.cpp_thm) || "{{ this.args.theme }}";
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ r }}/.cpr/util.js?_={{ ts }}"></script>
|
<script src="{{ r }}/.cpr/util.js?_={{ ts }}"></script>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ var Ls = {
|
|||||||
"v2": "use this server as a local HDD$N$NWARNING: this will show your password!",
|
"v2": "use this server as a local HDD$N$NWARNING: this will show your password!",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
d = Ls[sread("lang") || lang];
|
d = Ls[sread("cpp_lang", ["eng", "nor"]) || lang] || Ls.eng || Ls.nor;
|
||||||
|
|
||||||
for (var k in (d || {})) {
|
for (var k in (d || {})) {
|
||||||
var f = k.slice(-1),
|
var f = k.slice(-1),
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>{{ args.doctitle }} @ {{ args.name }}</title>
|
<title>{{ s_doctitle }}</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||||
<meta name="theme-color" content="#333">
|
<meta name="theme-color" content="#333">
|
||||||
{{ html_head }}
|
{{ html_head }}
|
||||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}">
|
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}">
|
||||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||||
|
<style>ul{padding-left:1.3em}li{margin:.4em 0}</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -48,9 +49,13 @@
|
|||||||
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 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>
|
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>W:</b>
|
||||||
</pre>
|
</pre>
|
||||||
{% if s %}
|
<ul>
|
||||||
<p><em>note: if you are on LAN (or just dont have valid certificates), add <code>--no-check-certificate</code> to the mount command</em><br />---</p>
|
{% if s %}
|
||||||
{% endif %}
|
<li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>
|
||||||
|
{% endif %}
|
||||||
|
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
|
||||||
|
<li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<p>if you want to use the native WebDAV client in windows instead (slow and buggy), first run <a href="{{ r }}/.cpr/a/webdav-cfg.bat">webdav-cfg.bat</a> to remove the 47 MiB filesize limit (also fixes latency and password login), then connect:</p>
|
<p>if you want to use the native WebDAV client in windows instead (slow and buggy), first run <a href="{{ r }}/.cpr/a/webdav-cfg.bat">webdav-cfg.bat</a> to remove the 47 MiB filesize limit (also fixes latency and password login), then connect:</p>
|
||||||
<pre>
|
<pre>
|
||||||
@@ -73,10 +78,13 @@
|
|||||||
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 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>
|
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b>
|
||||||
</pre>
|
</pre>
|
||||||
{% if s %}
|
<ul>
|
||||||
<p><em>note: if you are on LAN (or just dont have valid certificates), add <code>--no-check-certificate</code> to the mount command</em><br />---</p>
|
{% if s %}
|
||||||
{% endif %}
|
<li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>
|
||||||
|
{% endif %}
|
||||||
|
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
|
||||||
|
<li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li>
|
||||||
|
</ul>
|
||||||
<p>or the emergency alternative (gnome/gui-only):</p>
|
<p>or the emergency alternative (gnome/gui-only):</p>
|
||||||
<!-- gnome-bug: ignores vp -->
|
<!-- gnome-bug: ignores vp -->
|
||||||
<pre>
|
<pre>
|
||||||
@@ -110,10 +118,27 @@
|
|||||||
|
|
||||||
<div class="os win">
|
<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>
|
<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>
|
<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>
|
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>W:</b>
|
||||||
</pre>
|
</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>
|
||||||
|
{% endif %}
|
||||||
|
<ul>
|
||||||
|
{% if args.ftps %}
|
||||||
|
<li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>
|
||||||
|
{% endif %}
|
||||||
|
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
|
||||||
|
<li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li>
|
||||||
|
</ul>
|
||||||
<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>
|
<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>
|
<pre>
|
||||||
explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}<b>{{ pw }}</b>:k@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}<b>{{ pw }}</b>:k@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||||
@@ -121,10 +146,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="os lin">
|
<div class="os lin">
|
||||||
|
{% if args.ftp %}
|
||||||
|
<p>connect with plaintext FTP:</p>
|
||||||
<pre>
|
<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>
|
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>mp</b>
|
||||||
</pre>
|
</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>
|
||||||
|
{% endif %}
|
||||||
|
<ul>
|
||||||
|
{% if args.ftps %}
|
||||||
|
<li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>
|
||||||
|
{% endif %}
|
||||||
|
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
|
||||||
|
<li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li>
|
||||||
|
</ul>
|
||||||
<p>emergency alternative (gnome/gui-only):</p>
|
<p>emergency alternative (gnome/gui-only):</p>
|
||||||
<!-- gnome-bug: ignores vp -->
|
<!-- gnome-bug: ignores vp -->
|
||||||
<pre>
|
<pre>
|
||||||
@@ -156,10 +198,10 @@
|
|||||||
partyfuse.py{% if accs %} -a <b>{{ pw }}</b>{% endif %} http{{ s }}://{{ ep }}/{{ rvp }} <b><span class="os win">W:</span><span class="os lin mac">mp</span></b>
|
partyfuse.py{% if accs %} -a <b>{{ pw }}</b>{% endif %} http{{ s }}://{{ ep }}/{{ rvp }} <b><span class="os win">W:</span><span class="os lin mac">mp</span></b>
|
||||||
</pre>
|
</pre>
|
||||||
{% if s %}
|
{% if s %}
|
||||||
<p><em>note: if you are on LAN (or just dont have valid certificates), add <code>-td</code></em></p>
|
<ul><li>if you are on LAN (or just dont have valid certificates), add <code>-td</code></li></ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p>
|
<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>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
@@ -196,7 +238,8 @@ var SR = {{ r|tojson }},
|
|||||||
lang="{{ lang }}",
|
lang="{{ lang }}",
|
||||||
dfavico="{{ favico }}";
|
dfavico="{{ favico }}";
|
||||||
|
|
||||||
document.documentElement.className=localStorage.theme||"{{ args.theme }}";
|
var STG = window.localStorage;
|
||||||
|
document.documentElement.className = (STG && STG.cpp_thm) || "{{ args.theme }}";
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ r }}/.cpr/util.js?_={{ ts }}"></script>
|
<script src="{{ r }}/.cpr/util.js?_={{ ts }}"></script>
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
|
:root {
|
||||||
|
--fg: #ccc;
|
||||||
|
--fg-max: #fff;
|
||||||
|
--bg-u2: #2b2b2b;
|
||||||
|
--bg-u5: #444;
|
||||||
|
}
|
||||||
|
html.y {
|
||||||
|
--fg: #222;
|
||||||
|
--fg-max: #000;
|
||||||
|
--bg-u2: #f7f7f7;
|
||||||
|
--bg-u5: #ccc;
|
||||||
|
}
|
||||||
|
html.bz {
|
||||||
|
--bg-u2: #202231;
|
||||||
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'scp';
|
font-family: 'scp';
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: local('Source Code Pro Regular'), local('SourceCodePro-Regular'), url(deps/scp.woff2) format('woff2');
|
src: local('Source Code Pro Regular'), local('SourceCodePro-Regular'), url(deps/scp.woff2) format('woff2');
|
||||||
}
|
}
|
||||||
html {
|
html {
|
||||||
|
text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
#tt, #toast {
|
#tt, #toast {
|
||||||
@@ -12,7 +29,9 @@ html {
|
|||||||
max-width: min(34em, 90%);
|
max-width: min(34em, 90%);
|
||||||
max-width: min(34em, calc(100% - 7em));
|
max-width: min(34em, calc(100% - 7em));
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
|
color: var(--fg);
|
||||||
background: #333;
|
background: #333;
|
||||||
|
background: var(--bg-u2);
|
||||||
border: 0 solid #777;
|
border: 0 solid #777;
|
||||||
box-shadow: 0 .2em .5em #111;
|
box-shadow: 0 .2em .5em #111;
|
||||||
border-radius: .4em;
|
border-radius: .4em;
|
||||||
@@ -73,7 +92,7 @@ html {
|
|||||||
#toastb {
|
#toastb {
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 1px;
|
padding: .1em;
|
||||||
}
|
}
|
||||||
#toast.scroll #toastb {
|
#toast.scroll #toastb {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
@@ -86,6 +105,9 @@ html {
|
|||||||
#toast pre {
|
#toast pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
#toast.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
#toast.vis {
|
#toast.vis {
|
||||||
right: 1.3em;
|
right: 1.3em;
|
||||||
transform: inherit;
|
transform: inherit;
|
||||||
@@ -125,6 +147,10 @@ html {
|
|||||||
#toast.err #toastc {
|
#toast.err #toastc {
|
||||||
background: #d06;
|
background: #d06;
|
||||||
}
|
}
|
||||||
|
#toast code {
|
||||||
|
padding: 0 .2em;
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
#tth {
|
#tth {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: #111;
|
background: #111;
|
||||||
@@ -157,9 +183,10 @@ html {
|
|||||||
#modalc code,
|
#modalc code,
|
||||||
#tt code {
|
#tt code {
|
||||||
color: #eee;
|
color: #eee;
|
||||||
|
color: var(--fg-max);
|
||||||
background: #444;
|
background: #444;
|
||||||
|
background: var(--bg-u5);
|
||||||
padding: .1em .3em;
|
padding: .1em .3em;
|
||||||
border-top: 1px solid #777;
|
|
||||||
border-radius: .3em;
|
border-radius: .3em;
|
||||||
line-height: 1.7em;
|
line-height: 1.7em;
|
||||||
}
|
}
|
||||||
@@ -167,22 +194,15 @@ html {
|
|||||||
color: #f6a;
|
color: #f6a;
|
||||||
}
|
}
|
||||||
html.y #tt {
|
html.y #tt {
|
||||||
color: #333;
|
|
||||||
background: #fff;
|
|
||||||
border-color: #888 #000 #777 #000;
|
border-color: #888 #000 #777 #000;
|
||||||
}
|
}
|
||||||
html.bz #tt {
|
html.bz #tt {
|
||||||
background: #202231;
|
|
||||||
border-color: #3b3f58;
|
border-color: #3b3f58;
|
||||||
}
|
}
|
||||||
html.y #tt,
|
html.y #tt,
|
||||||
html.y #toast {
|
html.y #toast {
|
||||||
box-shadow: 0 .3em 1em rgba(0,0,0,0.4);
|
box-shadow: 0 .3em 1em rgba(0,0,0,0.4);
|
||||||
}
|
}
|
||||||
html.y #tt code {
|
|
||||||
background: #060;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
#modalc code {
|
#modalc code {
|
||||||
color: #060;
|
color: #060;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -320,6 +340,9 @@ html.y .btn:focus {
|
|||||||
box-shadow: 0 .1em .2em #037 inset;
|
box-shadow: 0 .1em .2em #037 inset;
|
||||||
outline: #037 solid .1em;
|
outline: #037 solid .1em;
|
||||||
}
|
}
|
||||||
|
input[type="submit"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
input[type="text"]:focus,
|
input[type="text"]:focus,
|
||||||
input:not([type]):focus,
|
input:not([type]):focus,
|
||||||
textarea:focus {
|
textarea:focus {
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ function U2pvis(act, btns, uc, st) {
|
|||||||
if (sread('potato') === null) {
|
if (sread('potato') === null) {
|
||||||
btn.click();
|
btn.click();
|
||||||
toast.inf(30, L.u_gotpot);
|
toast.inf(30, L.u_gotpot);
|
||||||
localStorage.removeItem('potato');
|
sdrop('potato');
|
||||||
}
|
}
|
||||||
|
|
||||||
u2f.appendChild(ode);
|
u2f.appendChild(ode);
|
||||||
@@ -588,7 +588,7 @@ function U2pvis(act, btns, uc, st) {
|
|||||||
btns[a].onclick = function (e) {
|
btns[a].onclick = function (e) {
|
||||||
ev(e);
|
ev(e);
|
||||||
var newtab = this.getAttribute('act');
|
var newtab = this.getAttribute('act');
|
||||||
function go() {
|
var go = function () {
|
||||||
for (var b = 0; b < btns.length; b++) {
|
for (var b = 0; b < btns.length; b++) {
|
||||||
btns[b].className = (
|
btns[b].className = (
|
||||||
btns[b].getAttribute('act') == newtab) ? 'act' : '';
|
btns[b].getAttribute('act') == newtab) ? 'act' : '';
|
||||||
@@ -723,7 +723,7 @@ function Donut(uc, st) {
|
|||||||
|
|
||||||
function strobe() {
|
function strobe() {
|
||||||
var txt = strobes.pop();
|
var txt = strobes.pop();
|
||||||
wintitle(txt);
|
wintitle(txt, false);
|
||||||
if (!txt)
|
if (!txt)
|
||||||
clearInterval(tstrober);
|
clearInterval(tstrober);
|
||||||
}
|
}
|
||||||
@@ -807,7 +807,7 @@ function up2k_init(subtle) {
|
|||||||
function init_deps() {
|
function init_deps() {
|
||||||
if (!loading_deps && !got_deps()) {
|
if (!loading_deps && !got_deps()) {
|
||||||
var fn = 'sha512.' + sha_js + '.js',
|
var fn = 'sha512.' + sha_js + '.js',
|
||||||
m = L.u_https1 + ' <a href="' + (window.location + '').replace(':', 's:') + '">' + L.u_https2 + '</a> ' + L.u_https3;
|
m = L.u_https1 + ' <a href="' + (location + '').replace(':', 's:') + '">' + L.u_https2 + '</a> ' + L.u_https3;
|
||||||
|
|
||||||
showmodal('<h1>loading ' + fn + '</h1>');
|
showmodal('<h1>loading ' + fn + '</h1>');
|
||||||
import_js(SR + '/.cpr/deps/' + fn, unmodal);
|
import_js(SR + '/.cpr/deps/' + fn, unmodal);
|
||||||
@@ -852,7 +852,7 @@ function up2k_init(subtle) {
|
|||||||
|
|
||||||
setmsg(suggest_up2k, 'msg');
|
setmsg(suggest_up2k, 'msg');
|
||||||
|
|
||||||
var parallel_uploads = icfg_get('nthread'),
|
var parallel_uploads = ebi('nthread').value = icfg_get('nthread', u2j),
|
||||||
uc = {},
|
uc = {},
|
||||||
fdom_ctr = 0,
|
fdom_ctr = 0,
|
||||||
biggest_file = 0;
|
biggest_file = 0;
|
||||||
@@ -861,6 +861,8 @@ function up2k_init(subtle) {
|
|||||||
bcfg_bind(uc, 'multitask', 'multitask', true, null, false);
|
bcfg_bind(uc, 'multitask', 'multitask', true, null, false);
|
||||||
bcfg_bind(uc, 'potato', 'potato', false, set_potato, false);
|
bcfg_bind(uc, 'potato', 'potato', false, set_potato, false);
|
||||||
bcfg_bind(uc, 'ask_up', 'ask_up', true, null, false);
|
bcfg_bind(uc, 'ask_up', 'ask_up', true, null, false);
|
||||||
|
bcfg_bind(uc, 'umod', 'umod', false, null, false);
|
||||||
|
bcfg_bind(uc, 'u2ts', 'u2ts', !u2ts.endsWith('u'), set_u2ts, false);
|
||||||
bcfg_bind(uc, 'fsearch', 'fsearch', false, set_fsearch, false);
|
bcfg_bind(uc, 'fsearch', 'fsearch', false, set_fsearch, false);
|
||||||
|
|
||||||
bcfg_bind(uc, 'flag_en', 'flag_en', false, apply_flag_cfg);
|
bcfg_bind(uc, 'flag_en', 'flag_en', false, apply_flag_cfg);
|
||||||
@@ -893,6 +895,7 @@ function up2k_init(subtle) {
|
|||||||
"bytes": {
|
"bytes": {
|
||||||
"total": 0,
|
"total": 0,
|
||||||
"hashed": 0,
|
"hashed": 0,
|
||||||
|
"inflight": 0,
|
||||||
"uploaded": 0,
|
"uploaded": 0,
|
||||||
"finished": 0
|
"finished": 0
|
||||||
},
|
},
|
||||||
@@ -971,7 +974,7 @@ function up2k_init(subtle) {
|
|||||||
if (++nenters <= 0)
|
if (++nenters <= 0)
|
||||||
nenters = 1;
|
nenters = 1;
|
||||||
|
|
||||||
if (onover.bind(this)(e))
|
if (onover.call(this, e))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
var mup, up = QS('#up_zd');
|
var mup, up = QS('#up_zd');
|
||||||
@@ -995,16 +998,29 @@ function up2k_init(subtle) {
|
|||||||
function onoverb(e) {
|
function onoverb(e) {
|
||||||
// zones are alive; disable cuo2duo branch
|
// zones are alive; disable cuo2duo branch
|
||||||
document.body.ondragover = document.body.ondrop = null;
|
document.body.ondragover = document.body.ondrop = null;
|
||||||
return onover.bind(this)(e);
|
return onover.call(this, e);
|
||||||
}
|
}
|
||||||
function onover(e) {
|
function onover(e) {
|
||||||
|
return onovercmn(this, e, false);
|
||||||
|
}
|
||||||
|
function onoverbtn(e) {
|
||||||
|
return onovercmn(this, e, true);
|
||||||
|
}
|
||||||
|
function onovercmn(self, e, btn) {
|
||||||
try {
|
try {
|
||||||
var ok = false, dt = e.dataTransfer.types;
|
var ok = false, dt = e.dataTransfer.types;
|
||||||
for (var a = 0; a < dt.length; a++)
|
for (var a = 0; a < dt.length; a++)
|
||||||
if (dt[a] == 'Files')
|
if (dt[a] == 'Files')
|
||||||
ok = true;
|
ok = true;
|
||||||
else if (dt[a] == 'text/uri-list')
|
else if (dt[a] == 'text/uri-list') {
|
||||||
return true;
|
if (btn) {
|
||||||
|
ok = true;
|
||||||
|
if (toast.txt == L.u_uri)
|
||||||
|
toast.hide();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return toast.inf(10, L.u_uri) || true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!ok)
|
if (!ok)
|
||||||
return true;
|
return true;
|
||||||
@@ -1020,13 +1036,16 @@ function up2k_init(subtle) {
|
|||||||
document.body.ondragenter = document.body.ondragleave = document.body.ondragover = null;
|
document.body.ondragenter = document.body.ondragleave = document.body.ondragover = null;
|
||||||
return modal.alert('your browser does not support drag-and-drop uploading');
|
return modal.alert('your browser does not support drag-and-drop uploading');
|
||||||
}
|
}
|
||||||
|
if (btn)
|
||||||
|
return;
|
||||||
|
|
||||||
clmod(ebi('drops'), 'vis', 1);
|
clmod(ebi('drops'), 'vis', 1);
|
||||||
var v = this.getAttribute('v');
|
var v = self.getAttribute('v');
|
||||||
if (v)
|
if (v)
|
||||||
clmod(ebi(v), 'hl', 1);
|
clmod(ebi(v), 'hl', 1);
|
||||||
}
|
}
|
||||||
function offdrag(e) {
|
function offdrag(e) {
|
||||||
ev(e);
|
noope(e);
|
||||||
|
|
||||||
var v = this.getAttribute('v');
|
var v = this.getAttribute('v');
|
||||||
if (v)
|
if (v)
|
||||||
@@ -1045,6 +1064,8 @@ function up2k_init(subtle) {
|
|||||||
document.body.ondragleave = offdrag;
|
document.body.ondragleave = offdrag;
|
||||||
document.body.ondragover = onover;
|
document.body.ondragover = onover;
|
||||||
document.body.ondrop = gotfile;
|
document.body.ondrop = gotfile;
|
||||||
|
ebi('u2btn').ondrop = gotfile;
|
||||||
|
ebi('u2btn').ondragover = onoverbtn;
|
||||||
|
|
||||||
var drops = [ebi('up_dz'), ebi('srch_dz')];
|
var drops = [ebi('up_dz'), ebi('srch_dz')];
|
||||||
for (var a = 0; a < 2; a++) {
|
for (var a = 0; a < 2; a++) {
|
||||||
@@ -1088,7 +1109,7 @@ function up2k_init(subtle) {
|
|||||||
function gotfile(e) {
|
function gotfile(e) {
|
||||||
ev(e);
|
ev(e);
|
||||||
nenters = 0;
|
nenters = 0;
|
||||||
offdrag.bind(this)();
|
offdrag.call(this);
|
||||||
var dz = this && this.getAttribute('id');
|
var dz = this && this.getAttribute('id');
|
||||||
if (!dz && e && e.clientY)
|
if (!dz && e && e.clientY)
|
||||||
// cuo2duo fallback
|
// cuo2duo fallback
|
||||||
@@ -1132,7 +1153,7 @@ function up2k_init(subtle) {
|
|||||||
dst = good_files;
|
dst = good_files;
|
||||||
|
|
||||||
if (is_itemlist) {
|
if (is_itemlist) {
|
||||||
if (fobj.kind !== 'file')
|
if (fobj.kind !== 'file' && fobj.type !== 'text/uri-list')
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1144,6 +1165,8 @@ function up2k_init(subtle) {
|
|||||||
}
|
}
|
||||||
catch (ex) { }
|
catch (ex) { }
|
||||||
fobj = fobj.getAsFile();
|
fobj = fobj.getAsFile();
|
||||||
|
if (!fobj)
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (fobj.size < 1)
|
if (fobj.size < 1)
|
||||||
@@ -1311,7 +1334,8 @@ function up2k_init(subtle) {
|
|||||||
return modal.confirm(msg.join('') + '</ul>', function () {
|
return modal.confirm(msg.join('') + '</ul>', function () {
|
||||||
start_actx();
|
start_actx();
|
||||||
up_them(good_files);
|
up_them(good_files);
|
||||||
toast.inf(15, L.u_unpt, L.u_unpt);
|
if (have_up2k_idx)
|
||||||
|
toast.inf(15, L.u_unpt, L.u_unpt);
|
||||||
}, null);
|
}, null);
|
||||||
|
|
||||||
up_them(good_files);
|
up_them(good_files);
|
||||||
@@ -1319,6 +1343,7 @@ function up2k_init(subtle) {
|
|||||||
|
|
||||||
function up_them(good_files) {
|
function up_them(good_files) {
|
||||||
start_actx();
|
start_actx();
|
||||||
|
draw_turbo();
|
||||||
var evpath = get_evpath(),
|
var evpath = get_evpath(),
|
||||||
draw_each = good_files.length < 50;
|
draw_each = good_files.length < 50;
|
||||||
|
|
||||||
@@ -1341,7 +1366,7 @@ function up2k_init(subtle) {
|
|||||||
name = good_files[a][1],
|
name = good_files[a][1],
|
||||||
fdir = evpath,
|
fdir = evpath,
|
||||||
now = Date.now(),
|
now = Date.now(),
|
||||||
lmod = fobj.lastModified || now,
|
lmod = uc.u2ts ? (fobj.lastModified || now) : 0,
|
||||||
ofs = name.lastIndexOf('/') + 1;
|
ofs = name.lastIndexOf('/') + 1;
|
||||||
|
|
||||||
if (ofs) {
|
if (ofs) {
|
||||||
@@ -1369,6 +1394,8 @@ function up2k_init(subtle) {
|
|||||||
entry.rand = true;
|
entry.rand = true;
|
||||||
entry.name = 'a\n' + entry.name;
|
entry.name = 'a\n' + entry.name;
|
||||||
}
|
}
|
||||||
|
else if (uc.umod)
|
||||||
|
entry.umod = true;
|
||||||
|
|
||||||
if (biggest_file < entry.size)
|
if (biggest_file < entry.size)
|
||||||
biggest_file = entry.size;
|
biggest_file = entry.size;
|
||||||
@@ -1385,7 +1412,7 @@ function up2k_init(subtle) {
|
|||||||
|
|
||||||
pvis.addfile([
|
pvis.addfile([
|
||||||
uc.fsearch ? esc(entry.name) : linksplit(
|
uc.fsearch ? esc(entry.name) : linksplit(
|
||||||
entry.purl + uricom_enc(entry.name)).join(' '),
|
entry.purl + uricom_enc(entry.name)).join(' / '),
|
||||||
'📐 ' + L.u_hashing,
|
'📐 ' + L.u_hashing,
|
||||||
''
|
''
|
||||||
], entry.size, draw_each);
|
], entry.size, draw_each);
|
||||||
@@ -1517,17 +1544,21 @@ function up2k_init(subtle) {
|
|||||||
if (uc.fsearch)
|
if (uc.fsearch)
|
||||||
t.push(['u2etat', st.bytes.hashed, st.bytes.hashed, st.time.hashing]);
|
t.push(['u2etat', st.bytes.hashed, st.bytes.hashed, st.time.hashing]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var b_up = st.bytes.inflight + st.bytes.uploaded,
|
||||||
|
b_fin = st.bytes.inflight + st.bytes.finished;
|
||||||
|
|
||||||
if (nsend) {
|
if (nsend) {
|
||||||
st.time.uploading += td;
|
st.time.uploading += td;
|
||||||
t.push(['u2etau', st.bytes.uploaded, st.bytes.finished, st.time.uploading]);
|
t.push(['u2etau', b_up, b_fin, st.time.uploading]);
|
||||||
}
|
}
|
||||||
if ((nhash || nsend) && !uc.fsearch) {
|
if ((nhash || nsend) && !uc.fsearch) {
|
||||||
if (!st.bytes.finished) {
|
if (!b_fin) {
|
||||||
ebi('u2etat').innerHTML = L.u_etaprep;
|
ebi('u2etat').innerHTML = L.u_etaprep;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
st.time.busy += td;
|
st.time.busy += td;
|
||||||
t.push(['u2etat', st.bytes.finished, st.bytes.finished, st.time.busy]);
|
t.push(['u2etat', b_fin, b_fin, st.time.busy]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (var a = 0; a < t.length; a++) {
|
for (var a = 0; a < t.length; a++) {
|
||||||
@@ -1568,7 +1599,7 @@ function up2k_init(subtle) {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
st.oserr = true;
|
st.oserr = true;
|
||||||
var msg = HTTPS ? L.u_emtleak3 : L.u_emtleak2.format((window.location + '').replace(':', 's:'));
|
var msg = HTTPS ? L.u_emtleak3 : L.u_emtleak2.format((location + '').replace(':', 's:'));
|
||||||
modal.alert(L.u_emtleak1 + msg + (CHROME ? L.u_emtleakc : FIREFOX ? L.u_emtleakf : ''));
|
modal.alert(L.u_emtleak1 + msg + (CHROME ? L.u_emtleakc : FIREFOX ? L.u_emtleakf : ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1634,11 +1665,11 @@ function up2k_init(subtle) {
|
|||||||
var running = false,
|
var running = false,
|
||||||
was_busy = false;
|
was_busy = false;
|
||||||
|
|
||||||
function defer() {
|
var defer = function () {
|
||||||
running = false;
|
running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function taskerd() {
|
var taskerd = function () {
|
||||||
if (running)
|
if (running)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -1668,7 +1699,7 @@ function up2k_init(subtle) {
|
|||||||
is_busy = st.todo.handshake.length;
|
is_busy = st.todo.handshake.length;
|
||||||
try {
|
try {
|
||||||
if (!is_busy && !uc.fsearch && !msel.getsel().length && (!mp.au || mp.au.paused))
|
if (!is_busy && !uc.fsearch && !msel.getsel().length && (!mp.au || mp.au.paused))
|
||||||
treectl.goto(get_evpath());
|
treectl.goto();
|
||||||
}
|
}
|
||||||
catch (ex) { }
|
catch (ex) { }
|
||||||
}
|
}
|
||||||
@@ -1826,6 +1857,7 @@ function up2k_init(subtle) {
|
|||||||
|
|
||||||
timer.rm(etafun);
|
timer.rm(etafun);
|
||||||
timer.rm(donut.do);
|
timer.rm(donut.do);
|
||||||
|
ebi('u2tabw').style.minHeight = '0px';
|
||||||
utw_minh = 0;
|
utw_minh = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1935,7 +1967,7 @@ function up2k_init(subtle) {
|
|||||||
st.bytes.hashed += cdr - car;
|
st.bytes.hashed += cdr - car;
|
||||||
st.etac.h++;
|
st.etac.h++;
|
||||||
|
|
||||||
function orz(e) {
|
var orz = function (e) {
|
||||||
bpend--;
|
bpend--;
|
||||||
segm_next();
|
segm_next();
|
||||||
hash_calc(nch, e.target.result);
|
hash_calc(nch, e.target.result);
|
||||||
@@ -2139,6 +2171,9 @@ function up2k_init(subtle) {
|
|||||||
|
|
||||||
function exec_head() {
|
function exec_head() {
|
||||||
var t = st.todo.head.shift();
|
var t = st.todo.head.shift();
|
||||||
|
if (t.done)
|
||||||
|
return console.log('done; skip head1', t.name, t);
|
||||||
|
|
||||||
st.busy.head.push(t);
|
st.busy.head.push(t);
|
||||||
|
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
@@ -2151,6 +2186,9 @@ function up2k_init(subtle) {
|
|||||||
st.todo.head.unshift(t);
|
st.todo.head.unshift(t);
|
||||||
};
|
};
|
||||||
function orz(e) {
|
function orz(e) {
|
||||||
|
if (t.done)
|
||||||
|
return console.log('done; skip head2', t.name, t);
|
||||||
|
|
||||||
var ok = false;
|
var ok = false;
|
||||||
if (xhr.status == 200) {
|
if (xhr.status == 200) {
|
||||||
var srv_sz = xhr.getResponseHeader('Content-Length'),
|
var srv_sz = xhr.getResponseHeader('Content-Length'),
|
||||||
@@ -2197,6 +2235,9 @@ function up2k_init(subtle) {
|
|||||||
keepalive = t.keepalive,
|
keepalive = t.keepalive,
|
||||||
me = Date.now();
|
me = Date.now();
|
||||||
|
|
||||||
|
if (t.done)
|
||||||
|
return console.log('done; skip hs', t.name, t);
|
||||||
|
|
||||||
st.busy.handshake.push(t);
|
st.busy.handshake.push(t);
|
||||||
t.keepalive = undefined;
|
t.keepalive = undefined;
|
||||||
t.t_busied = me;
|
t.t_busied = me;
|
||||||
@@ -2206,10 +2247,9 @@ function up2k_init(subtle) {
|
|||||||
|
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.onerror = function () {
|
xhr.onerror = function () {
|
||||||
if (t.t_busied != me) {
|
if (t.t_busied != me) // t.done ok
|
||||||
console.log('zombie handshake onerror,', t.name, t);
|
return console.log('zombie handshake onerror', t.name, t);
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!toast.visible)
|
if (!toast.visible)
|
||||||
toast.warn(9.98, L.u_eneths + "\n\nfile: " + t.name, t);
|
toast.warn(9.98, L.u_eneths + "\n\nfile: " + t.name, t);
|
||||||
|
|
||||||
@@ -2218,11 +2258,10 @@ function up2k_init(subtle) {
|
|||||||
st.todo.handshake.unshift(t);
|
st.todo.handshake.unshift(t);
|
||||||
t.keepalive = keepalive;
|
t.keepalive = keepalive;
|
||||||
};
|
};
|
||||||
function orz(e) {
|
var orz = function (e) {
|
||||||
if (t.t_busied != me) {
|
if (t.t_busied != me || t.done)
|
||||||
console.log('zombie handshake onload,', t.name, t);
|
return console.log('zombie handshake onload', t.name, t);
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (xhr.status == 200) {
|
if (xhr.status == 200) {
|
||||||
t.t_handshake = Date.now();
|
t.t_handshake = Date.now();
|
||||||
if (keepalive) {
|
if (keepalive) {
|
||||||
@@ -2254,7 +2293,7 @@ function up2k_init(subtle) {
|
|||||||
cdiff = (Math.abs(diff) <= 2) ? '3c0' : 'f0b',
|
cdiff = (Math.abs(diff) <= 2) ? '3c0' : 'f0b',
|
||||||
sdiff = '<span style="color:#' + cdiff + '">diff ' + diff;
|
sdiff = '<span style="color:#' + cdiff + '">diff ' + diff;
|
||||||
|
|
||||||
msg.push(linksplit(hit.rp).join('') + '<br /><small>' + tr + ' (srv), ' + tu + ' (You), ' + sdiff + '</small></span>');
|
msg.push(linksplit(hit.rp).join(' / ') + '<br /><small>' + tr + ' (srv), ' + tu + ' (You), ' + sdiff + '</small></span>');
|
||||||
}
|
}
|
||||||
msg = msg.join('<br />\n');
|
msg = msg.join('<br />\n');
|
||||||
}
|
}
|
||||||
@@ -2288,7 +2327,7 @@ function up2k_init(subtle) {
|
|||||||
url += '?k=' + fk;
|
url += '?k=' + fk;
|
||||||
}
|
}
|
||||||
|
|
||||||
pvis.seth(t.n, 0, linksplit(url).join(' '));
|
pvis.seth(t.n, 0, linksplit(url).join(' / '));
|
||||||
}
|
}
|
||||||
|
|
||||||
var chunksize = get_chunksize(t.size),
|
var chunksize = get_chunksize(t.size),
|
||||||
@@ -2372,15 +2411,12 @@ function up2k_init(subtle) {
|
|||||||
pvis.seth(t.n, 2, L.u_ehstmp, t);
|
pvis.seth(t.n, 2, L.u_ehstmp, t);
|
||||||
|
|
||||||
var err = "",
|
var err = "",
|
||||||
rsp = (xhr.responseText + ''),
|
rsp = unpre(xhr.responseText),
|
||||||
ofs = rsp.lastIndexOf('\nURL: ');
|
ofs = rsp.lastIndexOf('\nURL: ');
|
||||||
|
|
||||||
if (ofs !== -1)
|
if (ofs !== -1)
|
||||||
rsp = rsp.slice(0, ofs);
|
rsp = rsp.slice(0, ofs);
|
||||||
|
|
||||||
if (rsp.indexOf('<pre>') === 0)
|
|
||||||
rsp = rsp.slice(5);
|
|
||||||
|
|
||||||
if (rsp.indexOf('rate-limit ') !== -1) {
|
if (rsp.indexOf('rate-limit ') !== -1) {
|
||||||
var penalty = rsp.replace(/.*rate-limit /, "").split(' ')[0];
|
var penalty = rsp.replace(/.*rate-limit /, "").split(' ')[0];
|
||||||
console.log("rate-limit: " + penalty);
|
console.log("rate-limit: " + penalty);
|
||||||
@@ -2399,7 +2435,7 @@ function up2k_init(subtle) {
|
|||||||
err = rsp;
|
err = rsp;
|
||||||
ofs = err.indexOf('\n/');
|
ofs = err.indexOf('\n/');
|
||||||
if (ofs !== -1) {
|
if (ofs !== -1) {
|
||||||
err = err.slice(0, ofs + 1) + linksplit(err.slice(ofs + 2).trimEnd()).join(' ');
|
err = err.slice(0, ofs + 1) + linksplit(err.slice(ofs + 2).trimEnd()).join(' / ');
|
||||||
}
|
}
|
||||||
if (!t.rechecks && (err_pend || err_srcb)) {
|
if (!t.rechecks && (err_pend || err_srcb)) {
|
||||||
t.rechecks = 0;
|
t.rechecks = 0;
|
||||||
@@ -2440,6 +2476,8 @@ function up2k_init(subtle) {
|
|||||||
req.srch = 1;
|
req.srch = 1;
|
||||||
else if (t.rand)
|
else if (t.rand)
|
||||||
req.rand = true;
|
req.rand = true;
|
||||||
|
else if (t.umod)
|
||||||
|
req.umod = true;
|
||||||
|
|
||||||
xhr.open('POST', t.purl, true);
|
xhr.open('POST', t.purl, true);
|
||||||
xhr.responseType = 'text';
|
xhr.responseType = 'text';
|
||||||
@@ -2482,14 +2520,17 @@ function up2k_init(subtle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function exec_upload() {
|
function exec_upload() {
|
||||||
var upt = st.todo.upload.shift();
|
var upt = st.todo.upload.shift(),
|
||||||
|
t = st.files[upt.nfile],
|
||||||
|
npart = upt.npart,
|
||||||
|
tries = 0;
|
||||||
|
|
||||||
|
if (t.done)
|
||||||
|
return console.log('done; skip chunk', t.name, t);
|
||||||
|
|
||||||
st.busy.upload.push(upt);
|
st.busy.upload.push(upt);
|
||||||
st.nfile.upload = upt.nfile;
|
st.nfile.upload = upt.nfile;
|
||||||
|
|
||||||
var npart = upt.npart,
|
|
||||||
t = st.files[upt.nfile],
|
|
||||||
tries = 0;
|
|
||||||
|
|
||||||
if (!t.t_uploading)
|
if (!t.t_uploading)
|
||||||
t.t_uploading = Date.now();
|
t.t_uploading = Date.now();
|
||||||
|
|
||||||
@@ -2502,8 +2543,9 @@ function up2k_init(subtle) {
|
|||||||
if (cdr >= t.size)
|
if (cdr >= t.size)
|
||||||
cdr = t.size;
|
cdr = t.size;
|
||||||
|
|
||||||
function orz(xhr) {
|
var orz = function (xhr) {
|
||||||
var txt = ((xhr.response && xhr.response.err) || xhr.responseText) + '';
|
st.bytes.inflight -= xhr.bsent;
|
||||||
|
var txt = unpre((xhr.response && xhr.response.err) || xhr.responseText);
|
||||||
if (txt.indexOf('upload blocked by x') + 1) {
|
if (txt.indexOf('upload blocked by x') + 1) {
|
||||||
apop(st.busy.upload, upt);
|
apop(st.busy.upload, upt);
|
||||||
apop(t.postlist, npart);
|
apop(t.postlist, npart);
|
||||||
@@ -2531,7 +2573,7 @@ function up2k_init(subtle) {
|
|||||||
}
|
}
|
||||||
orz2(xhr);
|
orz2(xhr);
|
||||||
}
|
}
|
||||||
function orz2(xhr) {
|
var orz2 = function (xhr) {
|
||||||
apop(st.busy.upload, upt);
|
apop(st.busy.upload, upt);
|
||||||
apop(t.postlist, npart);
|
apop(t.postlist, npart);
|
||||||
if (!t.postlist.length) {
|
if (!t.postlist.length) {
|
||||||
@@ -2547,7 +2589,10 @@ function up2k_init(subtle) {
|
|||||||
btot = Math.floor(st.bytes.total / 1024 / 1024);
|
btot = Math.floor(st.bytes.total / 1024 / 1024);
|
||||||
|
|
||||||
xhr.upload.onprogress = function (xev) {
|
xhr.upload.onprogress = function (xev) {
|
||||||
pvis.prog(t, npart, xev.loaded);
|
var nb = xev.loaded;
|
||||||
|
st.bytes.inflight += nb - xhr.bsent;
|
||||||
|
xhr.bsent = nb;
|
||||||
|
pvis.prog(t, npart, nb);
|
||||||
};
|
};
|
||||||
xhr.onload = function (xev) {
|
xhr.onload = function (xev) {
|
||||||
try { orz(xhr); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); }
|
try { orz(xhr); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); }
|
||||||
@@ -2556,6 +2601,8 @@ function up2k_init(subtle) {
|
|||||||
if (crashed)
|
if (crashed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
st.bytes.inflight -= (xhr.bsent || 0);
|
||||||
|
|
||||||
if (!toast.visible)
|
if (!toast.visible)
|
||||||
toast.warn(9.98, L.u_cuerr.format(npart, Math.ceil(t.size / chunksize), t.name), t);
|
toast.warn(9.98, L.u_cuerr.format(npart, Math.ceil(t.size / chunksize), t.name), t);
|
||||||
|
|
||||||
@@ -2572,6 +2619,7 @@ function up2k_init(subtle) {
|
|||||||
if (xhr.overrideMimeType)
|
if (xhr.overrideMimeType)
|
||||||
xhr.overrideMimeType('Content-Type', 'application/octet-stream');
|
xhr.overrideMimeType('Content-Type', 'application/octet-stream');
|
||||||
|
|
||||||
|
xhr.bsent = 0;
|
||||||
xhr.responseType = 'text';
|
xhr.responseType = 'text';
|
||||||
xhr.send(t.fobj.slice(car, cdr));
|
xhr.send(t.fobj.slice(car, cdr));
|
||||||
}
|
}
|
||||||
@@ -2589,7 +2637,7 @@ function up2k_init(subtle) {
|
|||||||
wpx = window.innerWidth,
|
wpx = window.innerWidth,
|
||||||
fpx = parseInt(getComputedStyle(bar)['font-size']),
|
fpx = parseInt(getComputedStyle(bar)['font-size']),
|
||||||
wem = wpx * 1.0 / fpx,
|
wem = wpx * 1.0 / fpx,
|
||||||
wide = wem > 54 ? 'w' : '',
|
wide = wem > 57 ? 'w' : '',
|
||||||
parent = ebi(wide ? 'u2btn_cw' : 'u2btn_ct'),
|
parent = ebi(wide ? 'u2btn_cw' : 'u2btn_ct'),
|
||||||
btn = ebi('u2btn');
|
btn = ebi('u2btn');
|
||||||
|
|
||||||
@@ -2598,7 +2646,7 @@ function up2k_init(subtle) {
|
|||||||
ebi('u2conf').className = ebi('u2cards').className = ebi('u2etaw').className = wide;
|
ebi('u2conf').className = ebi('u2cards').className = ebi('u2etaw').className = wide;
|
||||||
}
|
}
|
||||||
|
|
||||||
wide = wem > 82 ? 'ww' : wide;
|
wide = wem > 86 ? 'ww' : wide;
|
||||||
parent = ebi(wide == 'ww' ? 'u2c3w' : 'u2c3t');
|
parent = ebi(wide == 'ww' ? 'u2c3w' : 'u2c3t');
|
||||||
var its = [ebi('u2etaw'), ebi('u2cards')];
|
var its = [ebi('u2etaw'), ebi('u2cards')];
|
||||||
if (its[0].parentNode !== parent) {
|
if (its[0].parentNode !== parent) {
|
||||||
@@ -2609,8 +2657,7 @@ function up2k_init(subtle) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', onresize);
|
onresize100.add(onresize, true);
|
||||||
onresize();
|
|
||||||
|
|
||||||
if (MOBILE) {
|
if (MOBILE) {
|
||||||
// android-chrome wobbles for a bit; firefox / iOS-safari are OK
|
// android-chrome wobbles for a bit; firefox / iOS-safari are OK
|
||||||
@@ -2656,7 +2703,11 @@ function up2k_init(subtle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parallel_uploads = v;
|
parallel_uploads = v;
|
||||||
swrite('nthread', v);
|
if (v == u2j)
|
||||||
|
sdrop('nthread');
|
||||||
|
else
|
||||||
|
swrite('nthread', v);
|
||||||
|
|
||||||
clmod(obj, 'err');
|
clmod(obj, 'err');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2669,8 +2720,11 @@ function up2k_init(subtle) {
|
|||||||
if (parallel_uploads > 16)
|
if (parallel_uploads > 16)
|
||||||
parallel_uploads = 16;
|
parallel_uploads = 16;
|
||||||
|
|
||||||
|
if (parallel_uploads > 7)
|
||||||
|
toast.warn(10, L.u_maxconn);
|
||||||
|
|
||||||
obj.value = parallel_uploads;
|
obj.value = parallel_uploads;
|
||||||
bumpthread({ "target": 1 })
|
bumpthread({ "target": 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
function tgl_fsearch() {
|
function tgl_fsearch() {
|
||||||
@@ -2678,6 +2732,16 @@ function up2k_init(subtle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function draw_turbo() {
|
function draw_turbo() {
|
||||||
|
if (turbolvl < 0 && uc.turbo) {
|
||||||
|
bcfg_set('u2turbo', uc.turbo = false);
|
||||||
|
toast.err(10, L.u_turbo_c);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uc.turbo && !has(perms, 'read')) {
|
||||||
|
bcfg_set('u2turbo', uc.turbo = false);
|
||||||
|
toast.warn(30, L.u_turbo_g);
|
||||||
|
}
|
||||||
|
|
||||||
var msg = (turbolvl || !uc.turbo) ? null : uc.fsearch ? L.u_ts : L.u_tu,
|
var msg = (turbolvl || !uc.turbo) ? null : uc.fsearch ? L.u_ts : L.u_tu,
|
||||||
html = ebi('u2foot').innerHTML;
|
html = ebi('u2foot').innerHTML;
|
||||||
|
|
||||||
@@ -2776,6 +2840,8 @@ function up2k_init(subtle) {
|
|||||||
|
|
||||||
function set_fsearch(new_state) {
|
function set_fsearch(new_state) {
|
||||||
var fixed = false,
|
var fixed = false,
|
||||||
|
persist = new_state !== undefined,
|
||||||
|
preferred = bcfg_get('fsearch', undefined),
|
||||||
can_write = false;
|
can_write = false;
|
||||||
|
|
||||||
if (!ebi('fsearch')) {
|
if (!ebi('fsearch')) {
|
||||||
@@ -2792,8 +2858,14 @@ function up2k_init(subtle) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (new_state === undefined)
|
||||||
|
new_state = preferred;
|
||||||
|
|
||||||
if (new_state !== undefined)
|
if (new_state !== undefined)
|
||||||
bcfg_set('fsearch', uc.fsearch = new_state);
|
if (persist)
|
||||||
|
bcfg_set('fsearch', uc.fsearch = new_state);
|
||||||
|
else
|
||||||
|
bcfg_upd_ui('fsearch', uc.fsearch = new_state);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
clmod(ebi('u2c3w'), 's', !can_write);
|
clmod(ebi('u2c3w'), 's', !can_write);
|
||||||
@@ -2816,6 +2888,9 @@ function up2k_init(subtle) {
|
|||||||
ebi('u2cards').style.display = ebi('u2tab').style.display = potato ? 'none' : '';
|
ebi('u2cards').style.display = ebi('u2tab').style.display = potato ? 'none' : '';
|
||||||
ebi('u2mu').style.display = potato ? '' : 'none';
|
ebi('u2mu').style.display = potato ? '' : 'none';
|
||||||
|
|
||||||
|
if (u2ts.startsWith('f') || !sread('u2ts'))
|
||||||
|
uc.u2ts = bcfg_upd_ui('u2ts', !u2ts.endsWith('u'));
|
||||||
|
|
||||||
draw_turbo();
|
draw_turbo();
|
||||||
draw_life();
|
draw_life();
|
||||||
onresize();
|
onresize();
|
||||||
@@ -2840,12 +2915,24 @@ function up2k_init(subtle) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function set_u2sort() {
|
function set_u2sort(en) {
|
||||||
if (u2sort.indexOf('f') < 0)
|
if (u2sort.indexOf('f') < 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
bcfg_set('u2sort', uc.az = u2sort.indexOf('n') + 1);
|
var fen = uc.az = u2sort.indexOf('n') + 1;
|
||||||
localStorage.removeItem('u2sort');
|
bcfg_upd_ui('u2sort', fen);
|
||||||
|
if (en != fen)
|
||||||
|
toast.warn(10, L.ul_btnlk);
|
||||||
|
}
|
||||||
|
|
||||||
|
function set_u2ts(en) {
|
||||||
|
if (u2ts.indexOf('f') < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var fen = !u2ts.endsWith('u');
|
||||||
|
bcfg_upd_ui('u2ts', fen);
|
||||||
|
if (en != fen)
|
||||||
|
toast.warn(10, L.ul_btnlk);
|
||||||
}
|
}
|
||||||
|
|
||||||
function set_hashw() {
|
function set_hashw() {
|
||||||
@@ -2943,7 +3030,7 @@ ebi('ico1').onclick = function () {
|
|||||||
if (QS('#op_up2k.act'))
|
if (QS('#op_up2k.act'))
|
||||||
goto_up2k();
|
goto_up2k();
|
||||||
|
|
||||||
apply_perms({ "perms": perms, "frand": frand });
|
apply_perms({ "perms": perms, "frand": frand, "u2ts": u2ts });
|
||||||
|
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
|
|||||||
@@ -6,13 +6,21 @@ if (!window.console || !console.log)
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (window.CGV)
|
||||||
|
for (var k in CGV)
|
||||||
|
window[k] = CGV[k];
|
||||||
|
|
||||||
|
|
||||||
var wah = '',
|
var wah = '',
|
||||||
|
STG = null,
|
||||||
|
NOAC = 'autocorrect="off" autocapitalize="off"',
|
||||||
L, tt, treectl, thegrid, up2k, asmCrypto, hashwasm, vbar, marked,
|
L, tt, treectl, thegrid, up2k, asmCrypto, hashwasm, vbar, marked,
|
||||||
CB = '?_=' + Date.now(),
|
T0 = Date.now(),
|
||||||
|
CB = '?_=' + Math.floor(T0 / 1000).toString(36),
|
||||||
R = SR.slice(1),
|
R = SR.slice(1),
|
||||||
RS = R ? "/" + R : "",
|
RS = R ? "/" + R : "",
|
||||||
HALFMAX = 8192 * 8192 * 8192 * 8192,
|
HALFMAX = 8192 * 8192 * 8192 * 8192,
|
||||||
HTTPS = (window.location + '').indexOf('https:') === 0,
|
HTTPS = ('' + location).indexOf('https:') === 0,
|
||||||
TOUCH = 'ontouchstart' in window,
|
TOUCH = 'ontouchstart' in window,
|
||||||
MOBILE = TOUCH,
|
MOBILE = TOUCH,
|
||||||
CHROME = !!window.chrome,
|
CHROME = !!window.chrome,
|
||||||
@@ -33,6 +41,16 @@ if (!window.Notification || !Notification.permission)
|
|||||||
if (!window.FormData)
|
if (!window.FormData)
|
||||||
window.FormData = false;
|
window.FormData = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
STG = window.localStorage;
|
||||||
|
STG.STG;
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
STG = null;
|
||||||
|
if ((ex + '').indexOf('sandbox') < 0)
|
||||||
|
console.log('no localStorage: ' + ex);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
CB = '?' + document.currentScript.src.split('?').pop();
|
CB = '?' + document.currentScript.src.split('?').pop();
|
||||||
|
|
||||||
@@ -139,29 +157,41 @@ catch (ex) {
|
|||||||
}
|
}
|
||||||
var crashed = false, ignexd = {}, evalex_fatal = false;
|
var crashed = false, ignexd = {}, evalex_fatal = false;
|
||||||
function vis_exh(msg, url, lineNo, columnNo, error) {
|
function vis_exh(msg, url, lineNo, columnNo, error) {
|
||||||
if ((msg + '').indexOf('ResizeObserver') + 1)
|
|
||||||
return; // chrome issue 809574 (benign, from <video>)
|
|
||||||
|
|
||||||
if ((msg + '').indexOf('l2d.js') + 1)
|
|
||||||
return; // `t` undefined in tapEvent -> hitTestSimpleCustom
|
|
||||||
|
|
||||||
if (!/\.js($|\?)/.exec('' + url))
|
|
||||||
return; // chrome debugger
|
|
||||||
|
|
||||||
if ((url + '').indexOf(' > eval') + 1 && !evalex_fatal)
|
|
||||||
return; // md timer
|
|
||||||
|
|
||||||
var ekey = url + '\n' + lineNo + '\n' + msg;
|
var ekey = url + '\n' + lineNo + '\n' + msg;
|
||||||
if (ignexd[ekey] || crashed)
|
if (ignexd[ekey] || crashed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
msg = String(msg);
|
||||||
|
url = String(url);
|
||||||
|
|
||||||
|
if (msg.indexOf('ResizeObserver') + 1)
|
||||||
|
return; // chrome issue 809574 (benign, from <video>)
|
||||||
|
|
||||||
|
if (msg.indexOf('l2d.js') + 1)
|
||||||
|
return; // `t` undefined in tapEvent -> hitTestSimpleCustom
|
||||||
|
|
||||||
|
if (!/\.js($|\?)/.exec(url))
|
||||||
|
return; // chrome debugger
|
||||||
|
|
||||||
|
if (url.indexOf(' > eval') + 1 && !evalex_fatal)
|
||||||
|
return; // md timer
|
||||||
|
|
||||||
|
if (IE && url.indexOf('prism.js') + 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (url.indexOf('easymde.js') + 1)
|
||||||
|
return; // clicking the preview pane
|
||||||
|
|
||||||
|
if (url.indexOf('deps/marked.js') + 1 && !window.WebAssembly)
|
||||||
|
return; // ff<52
|
||||||
|
|
||||||
crashed = true;
|
crashed = true;
|
||||||
window.onerror = undefined;
|
window.onerror = undefined;
|
||||||
var html = [
|
var html = [
|
||||||
'<h1>you hit a bug!</h1>',
|
'<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="font-size:1.3em;margin:0;line-height:2em">try to <a href="#" onclick="localStorage.clear();location.reload();">reset copyparty settings</a> if you are stuck here, or <a href="#" onclick="ignex();">ignore this</a> / <a href="#" onclick="ignex(true);">ignore all</a> / <a href="?b=u">basic</a></p>',
|
||||||
'<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">new github issue</a></p>',
|
||||||
'<p class="b">' + esc(url + ' @' + lineNo + ':' + columnNo), '<br />' + esc(String(msg)).replace(/\n/g, '<br />') + '</p>',
|
'<p class="b">' + esc(url + ' @' + lineNo + ':' + columnNo), '<br />' + esc(msg).replace(/\n/g, '<br />') + '</p>',
|
||||||
'<p><b>UA:</b> ' + esc(navigator.userAgent + '')
|
'<p><b>UA:</b> ' + esc(navigator.userAgent + '')
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -190,19 +220,24 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
|
|||||||
}
|
}
|
||||||
ignexd[ekey] = true;
|
ignexd[ekey] = true;
|
||||||
|
|
||||||
var ls = jcp(localStorage);
|
var ls = {},
|
||||||
if (ls.fman_clip)
|
lsk = Object.keys(localStorage),
|
||||||
ls.fman_clip = ls.fman_clip.length + ' items';
|
nka = lsk.length,
|
||||||
|
nk = Math.min(200, nka);
|
||||||
|
|
||||||
var lsk = Object.keys(ls);
|
for (var a = 0; a < nk; a++) {
|
||||||
lsk.sort();
|
var k = lsk[a],
|
||||||
html.push('<p class="b">');
|
v = localStorage.getItem(k);
|
||||||
for (var a = 0; a < lsk.length; a++) {
|
|
||||||
if (ls[lsk[a]].length > 9000)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
html.push(' <b>' + esc(lsk[a]) + '</b> <code>' + esc(ls[lsk[a]]) + '</code> ');
|
ls[k] = v.length > 256 ? v.slice(0, 32) + '[...' + v.length + 'b]' : v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lsk = Object.keys(ls);
|
||||||
|
lsk.sort();
|
||||||
|
html.push('<p class="b"><b>' + nka + ': </b>');
|
||||||
|
for (var a = 0; a < nk; a++)
|
||||||
|
html.push(' <b>' + esc(lsk[a]) + '</b> <code>' + esc(ls[lsk[a]]) + '</code> ');
|
||||||
|
|
||||||
html.push('</p>');
|
html.push('</p>');
|
||||||
}
|
}
|
||||||
catch (e) { }
|
catch (e) { }
|
||||||
@@ -225,7 +260,7 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
|
|||||||
'#exbox{background:#222;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em;z-index:80386;position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;overflow:auto;width:calc(100% - 2em)} ' +
|
'#exbox{background:#222;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em;z-index:80386;position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;overflow:auto;width:calc(100% - 2em)} ' +
|
||||||
'#exbox,#exbox *{line-height:1.5em;overflow-wrap:break-word} ' +
|
'#exbox,#exbox *{line-height:1.5em;overflow-wrap:break-word} ' +
|
||||||
'#exbox code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} ' +
|
'#exbox code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} ' +
|
||||||
'#exbox a{text-decoration:underline;color:#fc0} ' +
|
'#exbox a{text-decoration:underline;color:#fc0;background:#222;border:none} ' +
|
||||||
'#exbox h1{margin:.5em 1em 0 0;padding:0} ' +
|
'#exbox h1{margin:.5em 1em 0 0;padding:0} ' +
|
||||||
'#exbox p.b{border-top:1px solid #999;margin:1em 0 0 0;font-size:1em} ' +
|
'#exbox p.b{border-top:1px solid #999;margin:1em 0 0 0;font-size:1em} ' +
|
||||||
'#exbox ul, #exbox li {margin:0 0 0 .5em;padding:0} ' +
|
'#exbox ul, #exbox li {margin:0 0 0 .5em;padding:0} ' +
|
||||||
@@ -264,8 +299,15 @@ function anymod(e, shift_ok) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var dev_fbw = sread('dev_fbw');
|
||||||
function ev(e) {
|
function ev(e) {
|
||||||
e = e || window.event;
|
if (!e && window.event) {
|
||||||
|
e = window.event;
|
||||||
|
if (dev_fbw == 1) {
|
||||||
|
toast.warn(10, 'hello from fallback code ;_;\ncheck console trace');
|
||||||
|
console.error('using window.event');
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!e)
|
if (!e)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -284,7 +326,7 @@ function ev(e) {
|
|||||||
|
|
||||||
|
|
||||||
function noope(e) {
|
function noope(e) {
|
||||||
ev(e);
|
try { ev(e); } catch (ex) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -352,14 +394,30 @@ catch (ex) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!window.Set)
|
||||||
|
window.Set = function () {
|
||||||
|
var r = this;
|
||||||
|
r.size = 0;
|
||||||
|
r.d = {};
|
||||||
|
r.add = function (k) {
|
||||||
|
if (!r.d[k]) {
|
||||||
|
r.d[k] = 1;
|
||||||
|
r.size++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
r.has = function (k) {
|
||||||
|
return r.d[k];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// https://stackoverflow.com/a/950146
|
// https://stackoverflow.com/a/950146
|
||||||
function import_js(url, cb) {
|
function import_js(url, cb, ecb) {
|
||||||
var head = document.head || document.getElementsByTagName('head')[0];
|
var head = document.head || document.getElementsByTagName('head')[0];
|
||||||
var script = mknod('script');
|
var script = mknod('script');
|
||||||
script.type = 'text/javascript';
|
script.type = 'text/javascript';
|
||||||
script.src = url;
|
script.src = url + '?_=' + (window.TS || 'a');
|
||||||
script.onload = cb;
|
script.onload = cb;
|
||||||
script.onerror = function () {
|
script.onerror = ecb || function () {
|
||||||
var m = 'Failed to load module:\n' + url;
|
var m = 'Failed to load module:\n' + url;
|
||||||
console.log(m);
|
console.log(m);
|
||||||
toast.err(0, m);
|
toast.err(0, m);
|
||||||
@@ -368,6 +426,34 @@ function import_js(url, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function unsmart(txt) {
|
||||||
|
return !IPHONE ? txt : (txt.
|
||||||
|
replace(/[\u2014]/g, "--").
|
||||||
|
replace(/[\u2022]/g, "*").
|
||||||
|
replace(/[\u2018\u2019]/g, "'").
|
||||||
|
replace(/[\u201c\u201d]/g, '"'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function namesan(txt, win, fslash) {
|
||||||
|
if (win)
|
||||||
|
txt = (txt.
|
||||||
|
replace(/</g, "<").
|
||||||
|
replace(/>/g, ">").
|
||||||
|
replace(/:/g, ":").
|
||||||
|
replace(/"/g, """).
|
||||||
|
replace(/\\/g, "\").
|
||||||
|
replace(/\|/g, "|").
|
||||||
|
replace(/\?/g, "?").
|
||||||
|
replace(/\*/g, "*"));
|
||||||
|
|
||||||
|
if (fslash)
|
||||||
|
txt = txt.replace(/\//g, "/");
|
||||||
|
|
||||||
|
return txt;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var crctab = (function () {
|
var crctab = (function () {
|
||||||
var c, tab = [];
|
var c, tab = [];
|
||||||
for (var n = 0; n < 256; n++) {
|
for (var n = 0; n < 256; n++) {
|
||||||
@@ -462,7 +548,7 @@ function yscroll() {
|
|||||||
|
|
||||||
function showsort(tab) {
|
function showsort(tab) {
|
||||||
var v, vn, v1, v2, th = tab.tHead,
|
var v, vn, v1, v2, th = tab.tHead,
|
||||||
sopts = jread('fsort', [["href", 1, ""]]);
|
sopts = jread('fsort', jcp(dsort));
|
||||||
|
|
||||||
th && (th = th.rows[0]) && (th = th.cells);
|
th && (th = th.rows[0]) && (th = th.cells);
|
||||||
|
|
||||||
@@ -601,9 +687,8 @@ function linksplit(rp, id) {
|
|||||||
}
|
}
|
||||||
var vlink = esc(uricom_dec(link));
|
var vlink = esc(uricom_dec(link));
|
||||||
|
|
||||||
if (link.indexOf('/') !== -1) {
|
if (link.indexOf('/') !== -1)
|
||||||
vlink = vlink.slice(0, -1) + '<span>/</span>';
|
vlink = vlink.slice(0, -1);
|
||||||
}
|
|
||||||
|
|
||||||
if (!rp) {
|
if (!rp) {
|
||||||
if (q)
|
if (q)
|
||||||
@@ -735,17 +820,6 @@ function noq_href(el) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function get_pwd() {
|
|
||||||
var k = HTTPS ? 's=' : 'd=',
|
|
||||||
pwd = ('; ' + document.cookie).split('; cppw' + k);
|
|
||||||
|
|
||||||
if (pwd.length < 2)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return pwd[1].split(';')[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function unix2iso(ts) {
|
function unix2iso(ts) {
|
||||||
return new Date(ts * 1000).toISOString().replace("T", " ").slice(0, -5);
|
return new Date(ts * 1000).toISOString().replace("T", " ").slice(0, -5);
|
||||||
}
|
}
|
||||||
@@ -866,9 +940,17 @@ function jcp(obj) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function sread(key) {
|
function sdrop(key) {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(key);
|
STG.removeItem(key);
|
||||||
|
}
|
||||||
|
catch (ex) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
function sread(key, al) {
|
||||||
|
try {
|
||||||
|
var ret = STG.getItem(key);
|
||||||
|
return (!al || has(al, ret)) ? ret : null;
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
return null;
|
return null;
|
||||||
@@ -878,9 +960,9 @@ function sread(key) {
|
|||||||
function swrite(key, val) {
|
function swrite(key, val) {
|
||||||
try {
|
try {
|
||||||
if (val === undefined || val === null)
|
if (val === undefined || val === null)
|
||||||
localStorage.removeItem(key);
|
STG.removeItem(key);
|
||||||
else
|
else
|
||||||
localStorage.setItem(key, val);
|
STG.setItem(key, val);
|
||||||
}
|
}
|
||||||
catch (e) { }
|
catch (e) { }
|
||||||
}
|
}
|
||||||
@@ -890,7 +972,13 @@ function jread(key, fb) {
|
|||||||
if (!str)
|
if (!str)
|
||||||
return fb;
|
return fb;
|
||||||
|
|
||||||
return JSON.parse(str);
|
try {
|
||||||
|
// '' throws, null is ok, sasuga
|
||||||
|
return JSON.parse(str);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return fb;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function jwrite(key, val) {
|
function jwrite(key, val) {
|
||||||
@@ -909,7 +997,7 @@ function fcfg_get(name, defval) {
|
|||||||
val = parseFloat(sread(name));
|
val = parseFloat(sread(name));
|
||||||
|
|
||||||
if (!isNum(val))
|
if (!isNum(val))
|
||||||
return parseFloat(o ? o.value : defval);
|
return parseFloat(o && o.value !== '' ? o.value : defval);
|
||||||
|
|
||||||
if (o)
|
if (o)
|
||||||
o.value = val;
|
o.value = val;
|
||||||
@@ -954,13 +1042,14 @@ function bcfg_set(name, val) {
|
|||||||
function bcfg_upd_ui(name, val) {
|
function bcfg_upd_ui(name, val) {
|
||||||
var o = ebi(name);
|
var o = ebi(name);
|
||||||
if (!o)
|
if (!o)
|
||||||
return;
|
return val;
|
||||||
|
|
||||||
if (o.getAttribute('type') == 'checkbox')
|
if (o.getAttribute('type') == 'checkbox')
|
||||||
o.checked = val;
|
o.checked = val;
|
||||||
else if (o) {
|
else if (o) {
|
||||||
clmod(o, 'on', val);
|
clmod(o, 'on', val);
|
||||||
}
|
}
|
||||||
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
function bcfg_bind(obj, oname, cname, defval, cb, un_ev) {
|
function bcfg_bind(obj, oname, cname, defval, cb, un_ev) {
|
||||||
@@ -1034,7 +1123,7 @@ function dl_file(url) {
|
|||||||
|
|
||||||
function cliptxt(txt, ok) {
|
function cliptxt(txt, ok) {
|
||||||
var fb = function () {
|
var fb = function () {
|
||||||
console.log('fb');
|
console.log('clip-fb');
|
||||||
var o = mknod('input');
|
var o = mknod('input');
|
||||||
o.value = txt;
|
o.value = txt;
|
||||||
document.body.appendChild(o);
|
document.body.appendChild(o);
|
||||||
@@ -1051,10 +1140,13 @@ function cliptxt(txt, ok) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var timer = (function () {
|
function Debounce(delay) {
|
||||||
var r = {};
|
var r = this;
|
||||||
|
r.delay = delay;
|
||||||
|
r.timer = 0;
|
||||||
|
r.t_hit = 0;
|
||||||
|
r.t_run = 0;
|
||||||
r.q = [];
|
r.q = [];
|
||||||
r.last = 0;
|
|
||||||
|
|
||||||
r.add = function (fun, run) {
|
r.add = function (fun, run) {
|
||||||
r.rm(fun);
|
r.rm(fun);
|
||||||
@@ -1068,7 +1160,67 @@ var timer = (function () {
|
|||||||
apop(r.q, fun);
|
apop(r.q, fun);
|
||||||
};
|
};
|
||||||
|
|
||||||
function doevents() {
|
r.run = function () {
|
||||||
|
if (crashed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
r.t_run = Date.now();
|
||||||
|
|
||||||
|
var q = r.q.slice(0);
|
||||||
|
for (var a = 0; a < q.length; a++)
|
||||||
|
q[a]();
|
||||||
|
};
|
||||||
|
|
||||||
|
r.hit = function () {
|
||||||
|
if (crashed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var now = Date.now(),
|
||||||
|
td_hit = now - r.t_hit,
|
||||||
|
td_run = now - r.t_run;
|
||||||
|
|
||||||
|
if (td_run >= r.delay * 2)
|
||||||
|
r.t_run = now;
|
||||||
|
|
||||||
|
if (td_run >= r.delay && td_run <= r.delay * 2) {
|
||||||
|
// r.delay is also deadline
|
||||||
|
clearTimeout(r.timer);
|
||||||
|
return r.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (td_hit < r.delay / 5)
|
||||||
|
return;
|
||||||
|
|
||||||
|
clearTimeout(r.timer);
|
||||||
|
r.timer = setTimeout(r.run, r.delay);
|
||||||
|
r.t_hit = now;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var onresize100 = new Debounce(100);
|
||||||
|
window.addEventListener('resize', onresize100.hit);
|
||||||
|
|
||||||
|
|
||||||
|
var timer = (function () {
|
||||||
|
var r = {};
|
||||||
|
r.q = [];
|
||||||
|
r.last = 0;
|
||||||
|
r.fs = 0;
|
||||||
|
r.fc = 0;
|
||||||
|
|
||||||
|
r.add = function (fun, run) {
|
||||||
|
r.rm(fun);
|
||||||
|
r.q.push(fun);
|
||||||
|
|
||||||
|
if (run)
|
||||||
|
fun();
|
||||||
|
};
|
||||||
|
|
||||||
|
r.rm = function (fun) {
|
||||||
|
apop(r.q, fun);
|
||||||
|
};
|
||||||
|
|
||||||
|
var doevents = function () {
|
||||||
if (crashed)
|
if (crashed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -1080,6 +1232,7 @@ var timer = (function () {
|
|||||||
q[a]();
|
q[a]();
|
||||||
|
|
||||||
r.last = Date.now();
|
r.last = Date.now();
|
||||||
|
//r.fc++; if (r.last - r.fs >= 2000) { console.log(r.last - r.fs, r.fc); r.fs = r.last; r.fc = 0; }
|
||||||
}
|
}
|
||||||
setInterval(doevents, 100);
|
setInterval(doevents, 100);
|
||||||
|
|
||||||
@@ -1104,7 +1257,7 @@ var tt = (function () {
|
|||||||
var prev = null;
|
var prev = null;
|
||||||
r.cshow = function () {
|
r.cshow = function () {
|
||||||
if (this !== prev)
|
if (this !== prev)
|
||||||
r.show.bind(this)();
|
r.show.call(this);
|
||||||
|
|
||||||
prev = this;
|
prev = this;
|
||||||
};
|
};
|
||||||
@@ -1116,7 +1269,7 @@ var tt = (function () {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (Date.now() - r.lvis < 400)
|
if (Date.now() - r.lvis < 400)
|
||||||
return r.show.bind(this)();
|
return r.show.call(this);
|
||||||
|
|
||||||
tev = setTimeout(r.show.bind(this), 800);
|
tev = setTimeout(r.show.bind(this), 800);
|
||||||
if (TOUCH)
|
if (TOUCH)
|
||||||
@@ -1264,6 +1417,11 @@ function lf2br(txt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function unpre(txt) {
|
||||||
|
return ('' + txt).replace(/^<pre>/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var toast = (function () {
|
var toast = (function () {
|
||||||
var r = {},
|
var r = {},
|
||||||
te = null,
|
te = null,
|
||||||
@@ -1274,8 +1432,11 @@ var toast = (function () {
|
|||||||
r.visible = false;
|
r.visible = false;
|
||||||
r.txt = null;
|
r.txt = null;
|
||||||
r.tag = obj; // filler value (null is scary)
|
r.tag = obj; // filler value (null is scary)
|
||||||
|
r.p_txt = '';
|
||||||
|
r.p_sec = 0;
|
||||||
|
r.p_t = 0;
|
||||||
|
|
||||||
function scrollchk() {
|
var scrollchk = function () {
|
||||||
if (scrolling)
|
if (scrolling)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -1290,26 +1451,47 @@ var toast = (function () {
|
|||||||
scrolling = true;
|
scrolling = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function unscroll() {
|
var unscroll = function () {
|
||||||
timer.rm(scrollchk);
|
timer.rm(scrollchk);
|
||||||
clmod(obj, 'scroll');
|
clmod(obj, 'scroll');
|
||||||
scrolling = false;
|
scrolling = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
r.hide = function (e) {
|
r.hide = function (e) {
|
||||||
ev(e);
|
if (this === ebi('toastc'))
|
||||||
|
ev(e);
|
||||||
|
|
||||||
unscroll();
|
unscroll();
|
||||||
clearTimeout(te);
|
clearTimeout(te);
|
||||||
clmod(obj, 'vis');
|
clmod(obj, 'vis');
|
||||||
r.visible = false;
|
r.visible = false;
|
||||||
r.tag = obj;
|
r.tag = obj;
|
||||||
|
if (!window.WebAssembly)
|
||||||
|
te = setTimeout(function () {
|
||||||
|
obj.className = 'hide';
|
||||||
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
r.show = function (cl, sec, txt, tag) {
|
r.show = function (cl, sec, txt, tag) {
|
||||||
|
txt = (txt + '').slice(0, 16384);
|
||||||
|
|
||||||
|
var same = r.visible && txt == r.p_txt && r.p_sec == sec,
|
||||||
|
delta = Date.now() - r.p_t;
|
||||||
|
|
||||||
|
if (same && delta < 100)
|
||||||
|
return;
|
||||||
|
|
||||||
|
r.p_txt = txt;
|
||||||
|
r.p_sec = sec;
|
||||||
|
r.p_t = Date.now();
|
||||||
|
|
||||||
clearTimeout(te);
|
clearTimeout(te);
|
||||||
if (sec)
|
if (sec)
|
||||||
te = setTimeout(r.hide, sec * 1000);
|
te = setTimeout(r.hide, sec * 1000);
|
||||||
|
|
||||||
|
if (same && delta < 1000)
|
||||||
|
return;
|
||||||
|
|
||||||
if (txt.indexOf('<body>') + 1)
|
if (txt.indexOf('<body>') + 1)
|
||||||
txt = txt.slice(0, txt.indexOf('<')) + ' [...]';
|
txt = txt.slice(0, txt.indexOf('<')) + ' [...]';
|
||||||
|
|
||||||
@@ -1360,6 +1542,7 @@ var modal = (function () {
|
|||||||
r.load();
|
r.load();
|
||||||
|
|
||||||
r.busy = false;
|
r.busy = false;
|
||||||
|
r.nofocus = 0;
|
||||||
|
|
||||||
r.show = function (html) {
|
r.show = function (html) {
|
||||||
o = mknod('div', 'modal');
|
o = mknod('div', 'modal');
|
||||||
@@ -1373,6 +1556,7 @@ var modal = (function () {
|
|||||||
a.onclick = ng;
|
a.onclick = ng;
|
||||||
|
|
||||||
a = ebi('modal-ok');
|
a = ebi('modal-ok');
|
||||||
|
a.addEventListener('blur', onblur);
|
||||||
a.onclick = ok;
|
a.onclick = ok;
|
||||||
|
|
||||||
var inp = ebi('modali');
|
var inp = ebi('modali');
|
||||||
@@ -1383,6 +1567,7 @@ var modal = (function () {
|
|||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
document.addEventListener('focus', onfocus);
|
document.addEventListener('focus', onfocus);
|
||||||
|
document.addEventListener('selectionchange', onselch);
|
||||||
timer.add(onfocus);
|
timer.add(onfocus);
|
||||||
if (cb_up)
|
if (cb_up)
|
||||||
setTimeout(cb_up, 1);
|
setTimeout(cb_up, 1);
|
||||||
@@ -1390,13 +1575,18 @@ var modal = (function () {
|
|||||||
|
|
||||||
r.hide = function () {
|
r.hide = function () {
|
||||||
timer.rm(onfocus);
|
timer.rm(onfocus);
|
||||||
|
try {
|
||||||
|
ebi('modal-ok').removeEventListener('blur', onblur);
|
||||||
|
}
|
||||||
|
catch (ex) { }
|
||||||
|
document.removeEventListener('selectionchange', onselch);
|
||||||
document.removeEventListener('focus', onfocus);
|
document.removeEventListener('focus', onfocus);
|
||||||
document.removeEventListener('keydown', onkey);
|
document.removeEventListener('keydown', onkey);
|
||||||
o.parentNode.removeChild(o);
|
o.parentNode.removeChild(o);
|
||||||
r.busy = false;
|
r.busy = false;
|
||||||
setTimeout(next, 50);
|
setTimeout(next, 50);
|
||||||
};
|
};
|
||||||
function ok(e) {
|
var ok = function (e) {
|
||||||
ev(e);
|
ev(e);
|
||||||
var v = ebi('modali');
|
var v = ebi('modali');
|
||||||
v = v ? v.value : true;
|
v = v ? v.value : true;
|
||||||
@@ -1404,27 +1594,45 @@ var modal = (function () {
|
|||||||
if (cb_ok)
|
if (cb_ok)
|
||||||
cb_ok(v);
|
cb_ok(v);
|
||||||
}
|
}
|
||||||
function ng(e) {
|
var ng = function (e) {
|
||||||
ev(e);
|
ev(e);
|
||||||
r.hide();
|
r.hide();
|
||||||
if (cb_ng)
|
if (cb_ng)
|
||||||
cb_ng(null);
|
cb_ng(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onfocus(e) {
|
var onselch = function () {
|
||||||
|
try {
|
||||||
|
if (window.getSelection() + '')
|
||||||
|
r.nofocus = 15;
|
||||||
|
}
|
||||||
|
catch (ex) { }
|
||||||
|
};
|
||||||
|
|
||||||
|
var onblur = function () {
|
||||||
|
r.nofocus = 3;
|
||||||
|
};
|
||||||
|
|
||||||
|
var onfocus = function (e) {
|
||||||
|
if (MOBILE)
|
||||||
|
return;
|
||||||
|
|
||||||
var ctr = ebi('modalc');
|
var ctr = ebi('modalc');
|
||||||
if (!ctr || !ctr.contains || !document.activeElement || ctr.contains(document.activeElement))
|
if (!ctr || !ctr.contains || !document.activeElement || ctr.contains(document.activeElement))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
|
if (--r.nofocus >= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
if (ctr = ebi('modal-ok'))
|
if (ctr = ebi('modal-ok'))
|
||||||
ctr.focus();
|
ctr.focus();
|
||||||
}, 20);
|
}, 20);
|
||||||
ev(e);
|
ev(e);
|
||||||
}
|
};
|
||||||
|
|
||||||
function onkey(e) {
|
var onkey = function (e) {
|
||||||
var k = e.code,
|
var k = (e.code || e.key) + '',
|
||||||
eok = ebi('modal-ok'),
|
eok = ebi('modal-ok'),
|
||||||
eng = ebi('modal-ng'),
|
eng = ebi('modal-ng'),
|
||||||
ae = document.activeElement;
|
ae = document.activeElement;
|
||||||
@@ -1432,21 +1640,21 @@ var modal = (function () {
|
|||||||
if (k == 'Space' && ae && (ae === eok || ae === eng))
|
if (k == 'Space' && ae && (ae === eok || ae === eng))
|
||||||
k = 'Enter';
|
k = 'Enter';
|
||||||
|
|
||||||
if (k == 'Enter') {
|
if (k.endsWith('Enter')) {
|
||||||
if (ae && ae == eng)
|
if (ae && ae == eng)
|
||||||
return ng();
|
return ng(e);
|
||||||
|
|
||||||
return ok();
|
return ok(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((k == 'ArrowLeft' || k == 'ArrowRight') && eng && (ae == eok || ae == eng))
|
if ((k == 'ArrowLeft' || k == 'ArrowRight' || k == 'Left' || k == 'Right') && eng && (ae == eok || ae == eng))
|
||||||
return (ae == eok ? eng : eok).focus() || ev(e);
|
return (ae == eok ? eng : eok).focus() || ev(e);
|
||||||
|
|
||||||
if (k == 'Escape')
|
if (k == 'Escape' || k == 'Esc')
|
||||||
return ng();
|
return ng(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
function next() {
|
var next = function () {
|
||||||
if (!r.busy && q.length)
|
if (!r.busy && q.length)
|
||||||
q.shift()();
|
q.shift()();
|
||||||
}
|
}
|
||||||
@@ -1457,7 +1665,7 @@ var modal = (function () {
|
|||||||
});
|
});
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
function _alert(html, cb, fun) {
|
var _alert = function (html, cb, fun) {
|
||||||
cb_ok = cb_ng = cb;
|
cb_ok = cb_ng = cb;
|
||||||
cb_up = fun;
|
cb_up = fun;
|
||||||
html += '<div id="modalb"><a href="#" id="modal-ok">OK</a></div>';
|
html += '<div id="modalb"><a href="#" id="modal-ok">OK</a></div>';
|
||||||
@@ -1470,7 +1678,7 @@ var modal = (function () {
|
|||||||
});
|
});
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
function _confirm(html, cok, cng, fun, btns) {
|
var _confirm = function (html, cok, cng, fun, btns) {
|
||||||
cb_ok = cok;
|
cb_ok = cok;
|
||||||
cb_ng = cng === undefined ? cok : cng;
|
cb_ng = cng === undefined ? cok : cng;
|
||||||
cb_up = fun;
|
cb_up = fun;
|
||||||
@@ -1484,11 +1692,11 @@ var modal = (function () {
|
|||||||
});
|
});
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
function _prompt(html, v, cok, cng, fun) {
|
var _prompt = function (html, v, cok, cng, fun) {
|
||||||
cb_ok = cok;
|
cb_ok = cok;
|
||||||
cb_ng = cng === undefined ? cok : null;
|
cb_ng = cng === undefined ? cok : null;
|
||||||
cb_up = fun;
|
cb_up = fun;
|
||||||
html += '<input id="modali" type="text" /><div id="modalb">' + ok_cancel + '</div>';
|
html += '<input id="modali" type="text" ' + NOAC + ' /><div id="modalb">' + ok_cancel + '</div>';
|
||||||
r.show(html);
|
r.show(html);
|
||||||
|
|
||||||
ebi('modali').value = v || '';
|
ebi('modali').value = v || '';
|
||||||
@@ -1520,7 +1728,7 @@ function repl_load() {
|
|||||||
ret = [
|
ret = [
|
||||||
'var v=Object.keys(localStorage); v.sort(); JSON.stringify(v)',
|
'var v=Object.keys(localStorage); v.sort(); JSON.stringify(v)',
|
||||||
"for (var a of QSA('#files a[id]')) a.setAttribute('download','')",
|
"for (var a of QSA('#files a[id]')) a.setAttribute('download','')",
|
||||||
'console.hist.slice(-10).join("\\n")'
|
'console.hist.slice(-50).join("\\n")'
|
||||||
];
|
];
|
||||||
|
|
||||||
ipre.innerHTML = '<option value=""></option>';
|
ipre.innerHTML = '<option value=""></option>';
|
||||||
@@ -1576,6 +1784,8 @@ function repl(e) {
|
|||||||
if (!cmd)
|
if (!cmd)
|
||||||
return toast.inf(3, 'eval aborted');
|
return toast.inf(3, 'eval aborted');
|
||||||
|
|
||||||
|
cmd = unsmart(cmd);
|
||||||
|
|
||||||
if (cmd.startsWith(',')) {
|
if (cmd.startsWith(',')) {
|
||||||
evalex_fatal = true;
|
evalex_fatal = true;
|
||||||
return modal.alert(esc(eval(cmd.slice(1)) + ''));
|
return modal.alert(esc(eval(cmd.slice(1)) + ''));
|
||||||
@@ -1614,7 +1824,7 @@ function load_md_plug(md_text, plug_type, defer) {
|
|||||||
return md_text;
|
return md_text;
|
||||||
|
|
||||||
var find = '\n```copyparty_' + plug_type + '\n',
|
var find = '\n```copyparty_' + plug_type + '\n',
|
||||||
md = md_text.replace(/\r/g, ''),
|
md = '\n' + md_text.replace(/\r/g, '') + '\n',
|
||||||
ofs = md.indexOf(find),
|
ofs = md.indexOf(find),
|
||||||
ofs2 = md.indexOf('\n```', ofs + 1);
|
ofs2 = md.indexOf('\n```', ofs + 1);
|
||||||
|
|
||||||
@@ -1701,7 +1911,7 @@ var favico = (function () {
|
|||||||
r.en = true;
|
r.en = true;
|
||||||
r.tag = null;
|
r.tag = null;
|
||||||
|
|
||||||
function gx(txt) {
|
var gx = function (txt) {
|
||||||
return (svg_decl +
|
return (svg_decl +
|
||||||
'<svg version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">\n' +
|
'<svg version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">\n' +
|
||||||
(r.bg ? '<rect width="100%" height="100%" rx="16" fill="#' + r.bg + '" />\n' : '') +
|
(r.bg ? '<rect width="100%" height="100%" rx="16" fill="#' + r.bg + '" />\n' : '') +
|
||||||
@@ -1718,21 +1928,17 @@ var favico = (function () {
|
|||||||
var b64;
|
var b64;
|
||||||
try {
|
try {
|
||||||
b64 = btoa(svg ? svg_decl + svg : gx(r.txt));
|
b64 = btoa(svg ? svg_decl + svg : gx(r.txt));
|
||||||
//console.log('f1');
|
|
||||||
}
|
}
|
||||||
catch (e1) {
|
catch (e1) {
|
||||||
try {
|
try {
|
||||||
b64 = btoa(gx(encodeURIComponent(r.txt).replace(/%([0-9A-F]{2})/g,
|
b64 = btoa(gx(encodeURIComponent(r.txt).replace(/%([0-9A-F]{2})/g,
|
||||||
function x(m, v) { return String.fromCharCode('0x' + v); })));
|
function x(m, v) { return String.fromCharCode('0x' + v); })));
|
||||||
//console.log('f2');
|
|
||||||
}
|
}
|
||||||
catch (e2) {
|
catch (e2) {
|
||||||
try {
|
try {
|
||||||
b64 = btoa(gx(unescape(encodeURIComponent(r.txt))));
|
b64 = btoa(gx(unescape(encodeURIComponent(r.txt))));
|
||||||
//console.log('f3');
|
|
||||||
}
|
}
|
||||||
catch (e3) {
|
catch (e3) {
|
||||||
//console.log('fe');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1769,7 +1975,6 @@ function cprop(name) {
|
|||||||
|
|
||||||
|
|
||||||
function bchrome() {
|
function bchrome() {
|
||||||
console.log(document.documentElement.className);
|
|
||||||
var v, o = QS('meta[name=theme-color]');
|
var v, o = QS('meta[name=theme-color]');
|
||||||
if (!o)
|
if (!o)
|
||||||
return;
|
return;
|
||||||
@@ -1787,16 +1992,20 @@ function xhrchk(xhr, prefix, e404, lvl, tag) {
|
|||||||
if (xhr.status < 400 && xhr.status >= 200)
|
if (xhr.status < 400 && xhr.status >= 200)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (xhr.status == 403)
|
if (tag === undefined)
|
||||||
|
tag = prefix;
|
||||||
|
|
||||||
|
var errtxt = (xhr.response && xhr.response.err) || xhr.responseText,
|
||||||
|
fun = toast[lvl || 'err'],
|
||||||
|
is_cf = /[Cc]loud[f]lare|>Just a mo[m]ent|#cf-b[u]bbles|Chec[k]ing your br[o]wser|\/chall[e]nge-platform|"chall[e]nge-error|nable Ja[v]aScript and cook/.test(errtxt);
|
||||||
|
|
||||||
|
if (xhr.status == 403 && !is_cf)
|
||||||
return toast.err(0, prefix + (L && L.xhr403 || "403: access denied\n\ntry pressing F5, maybe you got logged out"), tag);
|
return toast.err(0, prefix + (L && L.xhr403 || "403: access denied\n\ntry pressing F5, maybe you got logged out"), tag);
|
||||||
|
|
||||||
if (xhr.status == 404)
|
if (xhr.status == 404)
|
||||||
return toast.err(0, prefix + e404, tag);
|
return toast.err(0, prefix + e404, tag);
|
||||||
|
|
||||||
var errtxt = (xhr.response && xhr.response.err) || xhr.responseText,
|
if (is_cf && (xhr.status == 403 || xhr.status == 503)) {
|
||||||
fun = toast[lvl || 'err'];
|
|
||||||
|
|
||||||
if (xhr.status == 503 && /[Cc]loud[f]lare|>Just a mo[m]ent|#cf-b[u]bbles|Chec[k]ing your br[o]wser/.test(errtxt)) {
|
|
||||||
var now = Date.now(), td = now - cf_cha_t;
|
var now = Date.now(), td = now - cf_cha_t;
|
||||||
if (td < 15000)
|
if (td < 15000)
|
||||||
return;
|
return;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user