Compare commits
279 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecead109ab | ||
|
|
765294c263 | ||
|
|
d6b5351207 | ||
|
|
a2009bcc6b | ||
|
|
12709a8a0a | ||
|
|
c055baefd2 | ||
|
|
56522599b5 | ||
|
|
664f53b75d | ||
|
|
87200d9f10 | ||
|
|
5c3d0b6520 | ||
|
|
bd49979f4a | ||
|
|
7e606cdd9f | ||
|
|
8b4b7fa794 | ||
|
|
05345ddf8b | ||
|
|
66adb470ad | ||
|
|
e15c8fd146 | ||
|
|
0f09b98a39 | ||
|
|
b4d6f4e24d | ||
|
|
3217fa625b | ||
|
|
e719ff8a47 | ||
|
|
9fcf528d45 | ||
|
|
1ddbf5a158 | ||
|
|
64bf4574b0 | ||
|
|
5649d26077 | ||
|
|
92f923effe | ||
|
|
0d46d548b9 | ||
|
|
062df3f0c3 | ||
|
|
789fb53b8e | ||
|
|
351db5a18f | ||
|
|
aabbd271c8 | ||
|
|
aae8e0171e | ||
|
|
45827a2458 | ||
|
|
726030296f | ||
|
|
6659ab3881 | ||
|
|
c6a103609e | ||
|
|
c6b3f035e5 | ||
|
|
2b0a7e378e | ||
|
|
b75ce909c8 | ||
|
|
229c3f5dab | ||
|
|
ec73094506 | ||
|
|
c7650c9326 | ||
|
|
d94c6d4e72 | ||
|
|
3cc8760733 | ||
|
|
a2f6973495 | ||
|
|
f8648fa651 | ||
|
|
177aa038df | ||
|
|
e0a14ec881 | ||
|
|
9366512f2f | ||
|
|
ea38b8041a | ||
|
|
f1870daf0d | ||
|
|
9722441aad | ||
|
|
9d014087f4 | ||
|
|
83b4038b85 | ||
|
|
1e0a448feb | ||
|
|
fb81de3b36 | ||
|
|
aa4f352301 | ||
|
|
f1a1c2ea45 | ||
|
|
6249bd4163 | ||
|
|
2579dc64ce | ||
|
|
356512270a | ||
|
|
bed27f2b43 | ||
|
|
54013d861b | ||
|
|
ec100210dc | ||
|
|
3ab1acf32c | ||
|
|
8c28266418 | ||
|
|
7f8b8dcb92 | ||
|
|
6dd39811d4 | ||
|
|
35e2138e3e | ||
|
|
239b4e9fe6 | ||
|
|
2fcd0e7e72 | ||
|
|
357347ce3a | ||
|
|
36dc1107fb | ||
|
|
0a3bbc4b4a | ||
|
|
855b93dcf6 | ||
|
|
89b79ba267 | ||
|
|
f5651b7d94 | ||
|
|
1881019ede | ||
|
|
caba4e974c | ||
|
|
bc3c9613bc | ||
|
|
15a3ee252e | ||
|
|
be055961ae | ||
|
|
e3031bdeec | ||
|
|
75917b9f7c | ||
|
|
910732e02c | ||
|
|
264b497681 | ||
|
|
372b949622 | ||
|
|
789a602914 | ||
|
|
093e955100 | ||
|
|
c32a89bebf | ||
|
|
c0bebe9f9f | ||
|
|
57579b2fe5 | ||
|
|
51d14a6b4d | ||
|
|
c50f1b64e5 | ||
|
|
98aaab02c5 | ||
|
|
0fc7973d8b | ||
|
|
10362aa02e | ||
|
|
0a8e759fe6 | ||
|
|
d70981cdd1 | ||
|
|
e08c03b886 | ||
|
|
56086e8984 | ||
|
|
1aa9033022 | ||
|
|
076e103d53 | ||
|
|
38c00ea8fc | ||
|
|
415757af43 | ||
|
|
e72ed8c0ed | ||
|
|
32f9c6b5bb | ||
|
|
6251584ef6 | ||
|
|
f3e413bc28 | ||
|
|
6f6cc8f3f8 | ||
|
|
8b081e9e69 | ||
|
|
c8a510d10e | ||
|
|
6f834f6679 | ||
|
|
cf2d6650ac | ||
|
|
cd52dea488 | ||
|
|
6ea75df05d | ||
|
|
4846e1e8d6 | ||
|
|
fc024f789d | ||
|
|
473e773aea | ||
|
|
48a2e1a353 | ||
|
|
6da63fbd79 | ||
|
|
5bec37fcee | ||
|
|
3fd0ba0a31 | ||
|
|
241a143366 | ||
|
|
a537064da7 | ||
|
|
f3dfd24c92 | ||
|
|
fa0a7f50bb | ||
|
|
44a78a7e21 | ||
|
|
6b75cbf747 | ||
|
|
e7b18ab9fe | ||
|
|
aa12830015 | ||
|
|
f156e00064 | ||
|
|
d53c212516 | ||
|
|
ca27f8587c | ||
|
|
88ce008e16 | ||
|
|
081d2cc5d7 | ||
|
|
60ac68d000 | ||
|
|
fbe656957d | ||
|
|
5534c78c17 | ||
|
|
a45a53fdce | ||
|
|
972a56e738 | ||
|
|
5e03b3ca38 | ||
|
|
1078d933b4 | ||
|
|
d6bf300d80 | ||
|
|
a359d64d44 | ||
|
|
22396e8c33 | ||
|
|
5ded5a4516 | ||
|
|
79c7639aaf | ||
|
|
5bbf875385 | ||
|
|
5e159432af | ||
|
|
1d6ae409f6 | ||
|
|
9d729d3d1a | ||
|
|
4dd5d4e1b7 | ||
|
|
acd8149479 | ||
|
|
b97a1088fa | ||
|
|
b77bed3324 | ||
|
|
a2b7c85a1f | ||
|
|
b28533f850 | ||
|
|
bd8c7e538a | ||
|
|
89e48cff24 | ||
|
|
ae90a7b7b6 | ||
|
|
6fc1be04da | ||
|
|
0061d29534 | ||
|
|
a891f34a93 | ||
|
|
d6a1e62a95 | ||
|
|
cda36ea8b4 | ||
|
|
909a76434a | ||
|
|
39348ef659 | ||
|
|
99d30edef3 | ||
|
|
b63ab15bf9 | ||
|
|
485cb4495c | ||
|
|
df018eb1f2 | ||
|
|
49aa47a9b8 | ||
|
|
7d20eb202a | ||
|
|
c533da9129 | ||
|
|
5cba31a814 | ||
|
|
1d824cb26c | ||
|
|
83b903d60e | ||
|
|
9c8ccabe8e | ||
|
|
b1f2c4e70d | ||
|
|
273ca0c8da | ||
|
|
d6f516b34f | ||
|
|
83127858ca | ||
|
|
d89329757e | ||
|
|
49ffec5320 | ||
|
|
2eaae2b66a | ||
|
|
ea4441e25c | ||
|
|
e5f34042f9 | ||
|
|
271096874a | ||
|
|
8efd780a72 | ||
|
|
41bcf7308d | ||
|
|
d102bb3199 | ||
|
|
d0bed95415 | ||
|
|
2528729971 | ||
|
|
292c18b3d0 | ||
|
|
0be7c5e2d8 | ||
|
|
eb5aaddba4 | ||
|
|
d8fd82bcb5 | ||
|
|
97be495861 | ||
|
|
8b53c159fc | ||
|
|
81e281f703 | ||
|
|
3948214050 | ||
|
|
c5e9a643e7 | ||
|
|
d25881d5c3 | ||
|
|
38d8d9733f | ||
|
|
118ebf668d | ||
|
|
a86f09fa46 | ||
|
|
dd4fb35c8f | ||
|
|
621eb4cf95 | ||
|
|
deea66ad0b | ||
|
|
bf99445377 | ||
|
|
7b54a63396 | ||
|
|
0fcb015f9a | ||
|
|
0a22b1ffb6 | ||
|
|
68cecc52ab | ||
|
|
53657ccfff | ||
|
|
96223fda01 | ||
|
|
374ff3433e | ||
|
|
5d63949e98 | ||
|
|
6b065d507d | ||
|
|
e79997498a | ||
|
|
f7ee02ec35 | ||
|
|
69dc433e1c | ||
|
|
c880cd848c | ||
|
|
5752b6db48 | ||
|
|
b36f905eab | ||
|
|
483dd527c6 | ||
|
|
e55678e28f | ||
|
|
3f4a8b9d6f | ||
|
|
02a856ecb4 | ||
|
|
4dff726310 | ||
|
|
cbc449036f | ||
|
|
8f53152220 | ||
|
|
bbb1e165d6 | ||
|
|
fed8d94885 | ||
|
|
58040cc0ed | ||
|
|
03d692db66 | ||
|
|
903f8e8453 | ||
|
|
405ae1308e | ||
|
|
8a0f583d71 | ||
|
|
b6d7017491 | ||
|
|
0f0217d203 | ||
|
|
a203e33347 | ||
|
|
3b8f697dd4 | ||
|
|
78ba16f722 | ||
|
|
0fcfe79994 | ||
|
|
c0e6df4b63 | ||
|
|
322abdcb43 | ||
|
|
31100787ce | ||
|
|
c57d721be4 | ||
|
|
3b5a03e977 | ||
|
|
ed807ee43e | ||
|
|
073c130ae6 | ||
|
|
8810e0be13 | ||
|
|
f93016ab85 | ||
|
|
b19cf260c2 | ||
|
|
db03e1e7eb | ||
|
|
e0d975e36a | ||
|
|
cfeb15259f | ||
|
|
3b3f8fc8fb | ||
|
|
88bd2c084c | ||
|
|
bd367389b0 | ||
|
|
58ba71a76f | ||
|
|
d03e34d55d | ||
|
|
24f239a46c | ||
|
|
2c0826f85a | ||
|
|
c061461d01 | ||
|
|
e7982a04fe | ||
|
|
33b91a7513 | ||
|
|
9bb1323e44 | ||
|
|
e62bb807a5 | ||
|
|
3fc0d2cc4a | ||
|
|
0c786b0766 | ||
|
|
68c7528911 | ||
|
|
26e18ae800 | ||
|
|
c30dc0b546 | ||
|
|
f94aa46a11 | ||
|
|
403261a293 | ||
|
|
c7d9cbb11f | ||
|
|
57e1c53cbb |
17
.vscode/launch.json
vendored
17
.vscode/launch.json
vendored
@@ -16,12 +16,9 @@
|
|||||||
"-e2ts",
|
"-e2ts",
|
||||||
"-mtp",
|
"-mtp",
|
||||||
".bpm=f,bin/mtag/audio-bpm.py",
|
".bpm=f,bin/mtag/audio-bpm.py",
|
||||||
"-a",
|
"-aed:wark",
|
||||||
"ed:wark",
|
"-vsrv::r:aed:cnodupe",
|
||||||
"-v",
|
"-vdist:dist:r"
|
||||||
"srv::r:aed:cnodupe",
|
|
||||||
"-v",
|
|
||||||
"dist:dist:r"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -43,5 +40,13 @@
|
|||||||
"${file}"
|
"${file}"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Python: Current File",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${file}",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"justMyCode": false
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
30
.vscode/launch.py
vendored
30
.vscode/launch.py
vendored
@@ -3,16 +3,18 @@
|
|||||||
# launches 10x faster than mspython debugpy
|
# launches 10x faster than mspython debugpy
|
||||||
# and is stoppable with ^C
|
# and is stoppable with ^C
|
||||||
|
|
||||||
|
import re
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
print(sys.executable)
|
||||||
|
|
||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
sys.path.insert(0, os.getcwd())
|
|
||||||
|
|
||||||
import jstyleson
|
import jstyleson
|
||||||
from copyparty.__main__ import main as copyparty
|
import subprocess as sp
|
||||||
|
|
||||||
with open(".vscode/launch.json", "r") as f:
|
|
||||||
|
with open(".vscode/launch.json", "r", encoding="utf-8") as f:
|
||||||
tj = f.read()
|
tj = f.read()
|
||||||
|
|
||||||
oj = jstyleson.loads(tj)
|
oj = jstyleson.loads(tj)
|
||||||
@@ -25,11 +27,19 @@ except:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
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]
|
||||||
try:
|
|
||||||
copyparty(["a"] + argv)
|
if re.search(" -j ?[0-9]", " ".join(argv)):
|
||||||
except SystemExit as ex:
|
argv = [sys.executable, "-m", "copyparty"] + argv
|
||||||
if ex.code:
|
sp.check_call(argv)
|
||||||
raise
|
else:
|
||||||
|
sys.path.insert(0, os.getcwd())
|
||||||
|
from copyparty.__main__ import main as copyparty
|
||||||
|
|
||||||
|
try:
|
||||||
|
copyparty(["a"] + argv)
|
||||||
|
except SystemExit as ex:
|
||||||
|
if ex.code:
|
||||||
|
raise
|
||||||
|
|
||||||
print("\n\033[32mokke\033[0m")
|
print("\n\033[32mokke\033[0m")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
265
README.md
265
README.md
@@ -9,24 +9,28 @@
|
|||||||
turn your phone or raspi into a portable file server with resumable uploads/downloads using IE6 or any other browser
|
turn your phone or raspi into a portable file server with resumable uploads/downloads using IE6 or any other browser
|
||||||
|
|
||||||
* server runs on anything with `py2.7` or `py3.3+`
|
* server runs on anything with `py2.7` or `py3.3+`
|
||||||
* *resumable* uploads need `firefox 12+` / `chrome 6+` / `safari 6+` / `IE 10+`
|
* browse/upload with IE4 / netscape4.0 on win3.11 (heh)
|
||||||
|
* *resumable* uploads need `firefox 34+` / `chrome 41+` / `safari 7+` for full speed
|
||||||
* code standard: `black`
|
* code standard: `black`
|
||||||
|
|
||||||
📷 screenshots: [browser](#the-browser) // [upload](#uploading) // [md-viewer](#markdown-viewer) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [ie4](#browser-support)
|
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [thumbnails](#thumbnails) // [md-viewer](#markdown-viewer) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [ie4](#browser-support)
|
||||||
|
|
||||||
|
|
||||||
## readme toc
|
## readme toc
|
||||||
|
|
||||||
* top
|
* top
|
||||||
* [quickstart](#quickstart)
|
* [quickstart](#quickstart)
|
||||||
|
* [on debian](#on-debian)
|
||||||
* [notes](#notes)
|
* [notes](#notes)
|
||||||
* [status](#status)
|
* [status](#status)
|
||||||
* [bugs](#bugs)
|
* [bugs](#bugs)
|
||||||
|
* [general bugs](#general-bugs)
|
||||||
* [not my bugs](#not-my-bugs)
|
* [not my bugs](#not-my-bugs)
|
||||||
* [the browser](#the-browser)
|
* [the browser](#the-browser)
|
||||||
* [tabs](#tabs)
|
* [tabs](#tabs)
|
||||||
* [hotkeys](#hotkeys)
|
* [hotkeys](#hotkeys)
|
||||||
* [tree-mode](#tree-mode)
|
* [tree-mode](#tree-mode)
|
||||||
|
* [thumbnails](#thumbnails)
|
||||||
* [zip downloads](#zip-downloads)
|
* [zip downloads](#zip-downloads)
|
||||||
* [uploading](#uploading)
|
* [uploading](#uploading)
|
||||||
* [file-search](#file-search)
|
* [file-search](#file-search)
|
||||||
@@ -34,6 +38,7 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
|||||||
* [other tricks](#other-tricks)
|
* [other tricks](#other-tricks)
|
||||||
* [searching](#searching)
|
* [searching](#searching)
|
||||||
* [search configuration](#search-configuration)
|
* [search configuration](#search-configuration)
|
||||||
|
* [database location](#database-location)
|
||||||
* [metadata from audio files](#metadata-from-audio-files)
|
* [metadata from audio files](#metadata-from-audio-files)
|
||||||
* [file parser plugins](#file-parser-plugins)
|
* [file parser plugins](#file-parser-plugins)
|
||||||
* [complete examples](#complete-examples)
|
* [complete examples](#complete-examples)
|
||||||
@@ -41,12 +46,16 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
|||||||
* [client examples](#client-examples)
|
* [client examples](#client-examples)
|
||||||
* [up2k](#up2k)
|
* [up2k](#up2k)
|
||||||
* [dependencies](#dependencies)
|
* [dependencies](#dependencies)
|
||||||
|
* [optional dependencies](#optional-dependencies)
|
||||||
|
* [install recommended deps](#install-recommended-deps)
|
||||||
* [optional gpl stuff](#optional-gpl-stuff)
|
* [optional gpl stuff](#optional-gpl-stuff)
|
||||||
* [sfx](#sfx)
|
* [sfx](#sfx)
|
||||||
* [sfx repack](#sfx-repack)
|
* [sfx repack](#sfx-repack)
|
||||||
* [install on android](#install-on-android)
|
* [install on android](#install-on-android)
|
||||||
* [dev env setup](#dev-env-setup)
|
* [building](#building)
|
||||||
* [how to release](#how-to-release)
|
* [dev env setup](#dev-env-setup)
|
||||||
|
* [just the sfx](#just-the-sfx)
|
||||||
|
* [complete release](#complete-release)
|
||||||
* [todo](#todo)
|
* [todo](#todo)
|
||||||
|
|
||||||
|
|
||||||
@@ -54,25 +63,53 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
|||||||
|
|
||||||
download [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) and you're all set!
|
download [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) and you're all set!
|
||||||
|
|
||||||
running the sfx without arguments (for example doubleclicking it on Windows) will let anyone access the current folder; see `-h` for help if you want accounts and volumes etc
|
running the sfx without arguments (for example doubleclicking it on Windows) will give everyone full access to the current folder; see `-h` for help if you want accounts and volumes etc
|
||||||
|
|
||||||
|
some recommended options:
|
||||||
|
* `-e2dsa` enables general file indexing, see [search configuration](#search-configuration)
|
||||||
|
* `-e2ts` enables audio metadata indexing (needs either FFprobe or mutagen), see [optional dependencies](#optional-dependencies)
|
||||||
|
* `-v /mnt/music:/music:r:afoo -a foo:bar` shares `/mnt/music` as `/music`, `r`eadable by anyone, with user `foo` as `a`dmin (read/write), password `bar`
|
||||||
|
* the syntax is `-v src:dst:perm:perm:...` so local-path, url-path, and one or more permissions to set
|
||||||
|
* replace `:r:afoo` with `:rfoo` to only make the folder readable by `foo` and nobody else
|
||||||
|
* in addition to `r`ead and `a`dmin, `w`rite makes a folder write-only, so cannot list/access files in it
|
||||||
|
* `--ls '**,*,ln,p,r'` to crash on startup if any of the volumes contain a symlink which point outside the volume, as that could give users unintended access
|
||||||
|
|
||||||
you may also want these, especially on servers:
|
you may also want these, especially on servers:
|
||||||
* [contrib/systemd/copyparty.service](contrib/systemd/copyparty.service) to run copyparty as a systemd service
|
* [contrib/systemd/copyparty.service](contrib/systemd/copyparty.service) to run copyparty as a systemd service
|
||||||
* [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to reverse-proxy behind nginx (for legit https)
|
* [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to reverse-proxy behind nginx (for better https)
|
||||||
|
|
||||||
|
|
||||||
|
### on debian
|
||||||
|
|
||||||
|
recommended steps to enable audio metadata and thumbnails (from images and videos):
|
||||||
|
|
||||||
|
* as root, run the following:
|
||||||
|
`apt install python3 python3-pip python3-dev ffmpeg`
|
||||||
|
|
||||||
|
* then, as the user which will be running copyparty (so hopefully not root), run this:
|
||||||
|
`python3 -m pip install --user -U Pillow pillow-avif-plugin`
|
||||||
|
|
||||||
|
(skipped `pyheif-pillow-opener` because apparently debian is too old to build it)
|
||||||
|
|
||||||
|
|
||||||
## notes
|
## notes
|
||||||
|
|
||||||
|
general:
|
||||||
|
* paper-printing is affected by dark/light-mode! use lightmode for color, darkmode for grayscale
|
||||||
|
* because no browsers currently implement the media-query to do this properly orz
|
||||||
|
|
||||||
|
browser-specific:
|
||||||
* iPhone/iPad: use Firefox to download files
|
* iPhone/iPad: use Firefox to download files
|
||||||
* Android-Chrome: increase "parallel uploads" for higher speed (android bug)
|
* Android-Chrome: increase "parallel uploads" for higher speed (android bug)
|
||||||
* Android-Firefox: takes a while to select files (their fix for ☝️)
|
* Android-Firefox: takes a while to select files (their fix for ☝️)
|
||||||
* Desktop-Firefox: ~~may use gigabytes of RAM if your files are massive~~ *seems to be OK now*
|
* Desktop-Firefox: ~~may use gigabytes of RAM if your files are massive~~ *seems to be OK now*
|
||||||
* paper-printing is affected by dark/light-mode! use lightmode for color, darkmode for grayscale
|
* Desktop-Firefox: may stop you from deleting folders you've uploaded until you visit `about:memory` and click `Minimize memory usage`
|
||||||
* because no browsers currently implement the media-query to do this properly orz
|
|
||||||
|
|
||||||
|
|
||||||
## status
|
## status
|
||||||
|
|
||||||
|
summary: all planned features work! now please enjoy the bloatening
|
||||||
|
|
||||||
* backend stuff
|
* backend stuff
|
||||||
* ☑ sanic multipart parser
|
* ☑ sanic multipart parser
|
||||||
* ☑ load balancer (multiprocessing)
|
* ☑ load balancer (multiprocessing)
|
||||||
@@ -89,10 +126,14 @@ you may also want these, especially on servers:
|
|||||||
* ☑ FUSE client (read-only)
|
* ☑ FUSE client (read-only)
|
||||||
* browser
|
* browser
|
||||||
* ☑ tree-view
|
* ☑ tree-view
|
||||||
* ☑ media player
|
* ☑ audio player (with OS media controls)
|
||||||
* ✖ thumbnails
|
* ☑ thumbnails
|
||||||
* ✖ SPA (browse while uploading)
|
* ☑ images using Pillow
|
||||||
* currently safe using the file-tree on the left only, not folders in the file list
|
* ☑ videos using FFmpeg
|
||||||
|
* ☑ cache eviction (max-age; maybe max-size eventually)
|
||||||
|
* ☑ image gallery
|
||||||
|
* ☑ SPA (browse while uploading)
|
||||||
|
* if you use the file-tree on the left only, not folders in the file list
|
||||||
* server indexing
|
* server indexing
|
||||||
* ☑ locate files by contents
|
* ☑ locate files by contents
|
||||||
* ☑ search by name/path/date/size
|
* ☑ search by name/path/date/size
|
||||||
@@ -101,19 +142,27 @@ you may also want these, especially on servers:
|
|||||||
* ☑ viewer
|
* ☑ viewer
|
||||||
* ☑ editor (sure why not)
|
* ☑ editor (sure why not)
|
||||||
|
|
||||||
summary: it works! you can use it! (but technically not even close to beta)
|
|
||||||
|
|
||||||
|
|
||||||
# bugs
|
# bugs
|
||||||
|
|
||||||
* Windows: python 3.7 and older cannot read tags with ffprobe, so use mutagen or upgrade
|
* Windows: python 3.7 and older cannot read tags with ffprobe, so use mutagen or upgrade
|
||||||
* Windows: python 2.7 cannot index non-ascii filenames with `-e2d`
|
* Windows: python 2.7 cannot index non-ascii filenames with `-e2d`
|
||||||
* Windows: python 2.7 cannot handle filenames with mojibake
|
* Windows: python 2.7 cannot handle filenames with mojibake
|
||||||
* hiding the contents at url `/d1/d2/d3` using `-v :d1/d2/d3:cd2d` has the side-effect of creating databases (for files/tags) inside folders d1 and d2, and those databases take precedence over the main db at the top of the vfs - this means all files in d2 and below will be reindexed unless you already had a vfs entry at or below d2
|
* MacOS: `--th-ff-jpg` may fix thumbnails using macports-FFmpeg
|
||||||
|
|
||||||
|
## general bugs
|
||||||
|
|
||||||
|
* all volumes must exist / be available on startup; up2k (mtp especially) gets funky otherwise
|
||||||
|
* cannot mount something at `/d1/d2/d3` unless `d2` exists inside `d1`
|
||||||
|
* dupe files will not have metadata (audio tags etc) displayed in the file listing
|
||||||
|
* because they don't get `up` entries in the db (probably best fix) and `tx_browser` does not `lstat`
|
||||||
* probably more, pls let me know
|
* probably more, pls let me know
|
||||||
|
|
||||||
## not my bugs
|
## not my bugs
|
||||||
|
|
||||||
|
* Windows: folders cannot be accessed if the name ends with `.`
|
||||||
|
* python or windows bug
|
||||||
|
|
||||||
* Windows: msys2-python 3.8.6 occasionally throws "RuntimeError: release unlocked lock" when leaving a scoped mutex in up2k
|
* Windows: msys2-python 3.8.6 occasionally throws "RuntimeError: release unlocked lock" when leaving a scoped mutex in up2k
|
||||||
* this is an msys2 bug, the regular windows edition of python is fine
|
* this is an msys2 bug, the regular windows edition of python is fine
|
||||||
|
|
||||||
@@ -136,18 +185,39 @@ summary: it works! you can use it! (but technically not even close to beta)
|
|||||||
## hotkeys
|
## hotkeys
|
||||||
|
|
||||||
the browser has the following hotkeys
|
the browser has the following hotkeys
|
||||||
* `0..9` jump to 10%..90%
|
* `B` toggle breadcrumbs / directory tree
|
||||||
* `U/O` skip 10sec back/forward
|
|
||||||
* `J/L` prev/next song
|
|
||||||
* `I/K` prev/next folder
|
* `I/K` prev/next folder
|
||||||
* `P` parent folder
|
* `M` parent folder
|
||||||
|
* `G` toggle list / grid view
|
||||||
|
* `T` toggle thumbnails / icons
|
||||||
|
* when playing audio:
|
||||||
|
* `0..9` jump to 10%..90%
|
||||||
|
* `U/O` skip 10sec back/forward
|
||||||
|
* `J/L` prev/next song
|
||||||
|
* `P` play/pause (also starts playing the folder)
|
||||||
|
* when tree-sidebar is open:
|
||||||
|
* `A/D` adjust tree width
|
||||||
|
* in the grid view:
|
||||||
|
* `S` toggle multiselect
|
||||||
|
* shift+`A/D` zoom
|
||||||
|
|
||||||
|
|
||||||
## tree-mode
|
## tree-mode
|
||||||
|
|
||||||
by default there's a breadcrumbs path; you can replace this with a tree-browser sidebar thing by clicking the 🌲
|
by default there's a breadcrumbs path; you can replace this with a tree-browser sidebar thing by clicking the `🌲` or pressing the `B` hotkey
|
||||||
|
|
||||||
click `[-]` and `[+]` to adjust the size, and the `[a]` toggles if the tree should widen dynamically as you go deeper or stay fixed-size
|
click `[-]` and `[+]` (or hotkeys `A`/`D`) to adjust the size, and the `[a]` toggles if the tree should widen dynamically as you go deeper or stay fixed-size
|
||||||
|
|
||||||
|
|
||||||
|
## thumbnails
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
it does static images with Pillow and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how destructive your users are
|
||||||
|
|
||||||
|
images named `folder.jpg` and `folder.png` become the thumbnail of the folder they're in
|
||||||
|
|
||||||
|
in the grid/thumbnail view, if the audio player panel is open, songs will start playing when clicked
|
||||||
|
|
||||||
|
|
||||||
## zip downloads
|
## zip downloads
|
||||||
@@ -173,8 +243,8 @@ you can also zip a selection of files or folders by clicking them in the browser
|
|||||||
## uploading
|
## uploading
|
||||||
|
|
||||||
two upload methods are available in the html client:
|
two upload methods are available in the html client:
|
||||||
* 🎈 bup, the basic uploader, supports almost every browser since netscape 4.0
|
* `🎈 bup`, the basic uploader, supports almost every browser since netscape 4.0
|
||||||
* 🚀 up2k, the fancy one
|
* `🚀 up2k`, the fancy one
|
||||||
|
|
||||||
up2k has several advantages:
|
up2k has several advantages:
|
||||||
* you can drop folders into the browser (files are added recursively)
|
* you can drop folders into the browser (files are added recursively)
|
||||||
@@ -188,6 +258,8 @@ see [up2k](#up2k) for details on how it works
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
**protip:** you can avoid scaring away users with [docs/minimal-up2k.html](docs/minimal-up2k.html) which makes it look [much simpler](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png)
|
||||||
|
|
||||||
the up2k UI is the epitome of polished inutitive experiences:
|
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
|
||||||
@@ -207,14 +279,14 @@ and then theres the tabs below it,
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
in the 🚀 up2k tab, after toggling the `[🔎]` switch green, any files/folders you drop onto the dropzone will be hashed on the client-side. Each hash is sent to the server which checks if that file exists somewhere already
|
in the `[🚀 up2k]` tab, after toggling the `[🔎]` switch green, any files/folders you drop onto the dropzone will be hashed on the client-side. Each hash is sent to the server which checks if that file exists somewhere already
|
||||||
|
|
||||||
files go into `[ok]` if they exist (and you get a link to where it is), otherwise they land in `[ng]`
|
files go into `[ok]` if they exist (and you get a link to where it is), otherwise they land in `[ng]`
|
||||||
* the main reason filesearch is combined with the uploader is cause the code was too spaghetti to separate it out somewhere else
|
* the main reason filesearch is combined with the uploader is cause the code was too spaghetti to separate it out somewhere else, this is no longer the case but now i've warmed up to the idea too much
|
||||||
|
|
||||||
adding the same file multiple times is blocked, so if you first search for a file and then decide to upload it, you have to click the `[cleanup]` button to discard `[done]` files
|
adding the same file multiple times is blocked, so if you first search for a file and then decide to upload it, you have to click the `[cleanup]` button to discard `[done]` files
|
||||||
|
|
||||||
note that since up2k has to read the file twice, 🎈 bup can be up to 2x faster if your internet connection is faster than the read-speed of your HDD
|
note that since up2k has to read the file twice, `[🎈 bup]` can be up to 2x faster in extreme cases (if your internet connection is faster than the read-speed of your HDD)
|
||||||
|
|
||||||
up2k has saved a few uploads from becoming corrupted in-transfer already; caught an android phone on wifi redhanded in wireshark with a bitflip, however bup with https would *probably* have noticed as well thanks to tls also functioning as an integrity check
|
up2k has saved a few uploads from becoming corrupted in-transfer already; caught an android phone on wifi redhanded in wireshark with a bitflip, however bup with https would *probably* have noticed as well thanks to tls also functioning as an integrity check
|
||||||
|
|
||||||
@@ -230,6 +302,8 @@ up2k has saved a few uploads from becoming corrupted in-transfer already; caught
|
|||||||
|
|
||||||
* you can link a particular timestamp in an audio file by adding it to the URL, such as `&20` / `&20s` / `&1m20` / `&t=1:20` after the `.../#af-c8960dab`
|
* you can link a particular timestamp in an audio file by adding it to the URL, such as `&20` / `&20s` / `&1m20` / `&t=1:20` after the `.../#af-c8960dab`
|
||||||
|
|
||||||
|
* if you are using media hotkeys to switch songs and are getting tired of seeing the OSD popup which Windows doesn't let you disable, consider https://ocv.me/dev/?media-osd-bgone.ps1
|
||||||
|
|
||||||
|
|
||||||
# searching
|
# searching
|
||||||
|
|
||||||
@@ -263,7 +337,29 @@ the same arguments can be set as volume flags, in addition to `d2d` and `d2t` fo
|
|||||||
* `-v ~/music::r:cd2d` disables **all** indexing, even if any `-e2*` are on
|
* `-v ~/music::r:cd2d` disables **all** indexing, even if any `-e2*` are on
|
||||||
* `-v ~/music::r:cd2t` disables all `-e2t*` (tags), does not affect `-e2d*`
|
* `-v ~/music::r:cd2t` disables all `-e2t*` (tags), does not affect `-e2d*`
|
||||||
|
|
||||||
`e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and cause `e2ts` to reindex those
|
note:
|
||||||
|
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those
|
||||||
|
* the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher
|
||||||
|
|
||||||
|
you can choose to only index filename/path/size/last-modified (and not the hash of the file contents) by setting `--no-hash` or the volume-flag `cdhash`, this has the following consequences:
|
||||||
|
* initial indexing is way faster, especially when the volume is on a networked disk
|
||||||
|
* makes it impossible to [file-search](#file-search)
|
||||||
|
* if someone uploads the same file contents, the upload will not be detected as a dupe, so it will not get symlinked or rejected
|
||||||
|
|
||||||
|
if you set `--no-hash`, you can enable hashing for specific volumes using flag `cehash`
|
||||||
|
|
||||||
|
|
||||||
|
## database location
|
||||||
|
|
||||||
|
copyparty creates a subfolder named `.hist` inside each volume where it stores the database, thumbnails, and some other stuff
|
||||||
|
|
||||||
|
this can instead be kept in a single place using the `--hist` argument, or the `hist=` volume flag, or a mix of both:
|
||||||
|
* `--hist ~/.cache/copyparty -v ~/music::r:chist=-` sets `~/.cache/copyparty` as the default place to put volume info, but `~/music` gets the regular `.hist` subfolder (`-` restores default behavior)
|
||||||
|
|
||||||
|
note:
|
||||||
|
* markdown edits are always stored in a local `.hist` subdirectory
|
||||||
|
* on windows the volflag path is cyglike, so `/c/temp` means `C:\temp` but use regular paths for `--hist`
|
||||||
|
* you can use cygpaths for volumes too, `-v C:\Users::r` and `-v /c/users::r` both work
|
||||||
|
|
||||||
|
|
||||||
## metadata from audio files
|
## metadata from audio files
|
||||||
@@ -282,6 +378,7 @@ see the beautiful mess of a dictionary in [mtag.py](https://github.com/9001/copy
|
|||||||
`--no-mutagen` disables mutagen and uses ffprobe instead, which...
|
`--no-mutagen` disables mutagen and uses ffprobe instead, which...
|
||||||
* is about 20x slower than mutagen
|
* is about 20x slower than mutagen
|
||||||
* catches a few tags that mutagen doesn't
|
* catches a few tags that mutagen doesn't
|
||||||
|
* melodic key, video resolution, framerate, pixfmt
|
||||||
* avoids pulling any GPL code into copyparty
|
* avoids pulling any GPL code into copyparty
|
||||||
* more importantly runs ffprobe on incoming files which is bad if your ffmpeg has a cve
|
* more importantly runs ffprobe on incoming files which is bad if your ffmpeg has a cve
|
||||||
|
|
||||||
@@ -294,6 +391,11 @@ copyparty can invoke external programs to collect additional metadata for files
|
|||||||
* `-mtp key=f,t5,~/bin/audio-key.py` uses `~/bin/audio-key.py` to get the `key` tag, replacing any existing metadata tag (`f,`), aborting if it takes longer than 5sec (`t5,`)
|
* `-mtp key=f,t5,~/bin/audio-key.py` uses `~/bin/audio-key.py` to get the `key` tag, replacing any existing metadata tag (`f,`), aborting if it takes longer than 5sec (`t5,`)
|
||||||
* `-v ~/music::r:cmtp=.bpm=~/bin/audio-bpm.py:cmtp=key=f,t5,~/bin/audio-key.py` both as a per-volume config wow this is getting ugly
|
* `-v ~/music::r:cmtp=.bpm=~/bin/audio-bpm.py:cmtp=key=f,t5,~/bin/audio-key.py` both as a per-volume config wow this is getting ugly
|
||||||
|
|
||||||
|
*but wait, there's more!* `-mtp` can be used for non-audio files as well using the `a` flag: `ay` only do audio files, `an` only do non-audio files, or `ad` do all files (d as in dontcare)
|
||||||
|
|
||||||
|
* `-mtp ext=an,~/bin/file-ext.py` runs `~/bin/file-ext.py` to get the `ext` tag only if file is not audio (`an`)
|
||||||
|
* `-mtp arch,built,ver,orig=an,eexe,edll,~/bin/exe.py` runs `~/bin/exe.py` to get properties about windows-binaries only if file is not audio (`an`) and file extension is exe or dll
|
||||||
|
|
||||||
|
|
||||||
## complete examples
|
## complete examples
|
||||||
|
|
||||||
@@ -303,7 +405,7 @@ copyparty can invoke external programs to collect additional metadata for files
|
|||||||
|
|
||||||
# browser support
|
# browser support
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
`ie` = internet-explorer, `ff` = firefox, `c` = chrome, `iOS` = iPhone/iPad, `Andr` = Android
|
`ie` = internet-explorer, `ff` = firefox, `c` = chrome, `iOS` = iPhone/iPad, `Andr` = Android
|
||||||
|
|
||||||
@@ -329,14 +431,18 @@ copyparty can invoke external programs to collect additional metadata for files
|
|||||||
* `*2` using a wasm decoder which can sometimes get stuck and consumes a bit more power
|
* `*2` using a wasm decoder which can sometimes get stuck and consumes a bit more power
|
||||||
|
|
||||||
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:
|
||||||
* safari (14.0.3/macos) is chrome with janky wasm, so playing opus can deadlock the javascript engine
|
|
||||||
* safari (14.0.1/iOS) same as macos, except it recovers from the deadlocks if you poke it a bit
|
| browser | will it blend |
|
||||||
* links (2.21/macports) can browse, login, upload/mkdir/msg
|
| ------- | ------------- |
|
||||||
* lynx (2.8.9/macports) can browse, login, upload/mkdir/msg
|
| **safari** (14.0.3/macos) | is chrome with janky wasm, so playing opus can deadlock the javascript engine |
|
||||||
* w3m (0.5.3/macports) can browse, login, upload at 100kB/s, mkdir/msg
|
| **safari** (14.0.1/iOS) | same as macos, except it recovers from the deadlocks if you poke it a bit |
|
||||||
* netsurf (3.10/arch) is basically ie6 with much better css (javascript has almost no effect)
|
| **links** (2.21/macports) | can browse, login, upload/mkdir/msg |
|
||||||
* ie4 and netscape 4.0 can browse (text is yellow on white), upload with `?b=u`
|
| **lynx** (2.8.9/macports) | can browse, login, upload/mkdir/msg |
|
||||||
* SerenityOS (22d13d8) hits a page fault, works with `?b=u`, file input not-impl, url params are multiplying
|
| **w3m** (0.5.3/macports) | can browse, login, upload at 100kB/s, mkdir/msg |
|
||||||
|
| **netsurf** (3.10/arch) | is basically ie6 with much better css (javascript has almost no effect) |
|
||||||
|
| **ie4** and **netscape** 4.0 | can browse (text is yellow on white), upload with `?b=u` |
|
||||||
|
| **SerenityOS** (22d13d8) | hits a page fault, works with `?b=u`, file input not-impl, url params are multiplying |
|
||||||
|
|
||||||
|
|
||||||
# client examples
|
# client examples
|
||||||
|
|
||||||
@@ -356,9 +462,11 @@ quick summary of more eccentric web-browsers trying to view a directory index:
|
|||||||
* cross-platform python client available in [./bin/](bin/)
|
* cross-platform python client available in [./bin/](bin/)
|
||||||
* [rclone](https://rclone.org/) as client can give ~5x performance, see [./docs/rclone.md](docs/rclone.md)
|
* [rclone](https://rclone.org/) as client can give ~5x performance, see [./docs/rclone.md](docs/rclone.md)
|
||||||
|
|
||||||
|
* sharex (screenshot utility): see [./contrib/sharex.sxcu](contrib/#sharexsxcu)
|
||||||
|
|
||||||
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|head -c43;}
|
b512(){ printf "$((sha512sum||shasum -a512)|sed -E 's/ .*//;s/(..)/\\x\1/g')"|base64|tr '+/' '-_'|head -c44;}
|
||||||
b512 <movie.mkv
|
b512 <movie.mkv
|
||||||
|
|
||||||
|
|
||||||
@@ -367,7 +475,7 @@ copyparty returns a truncated sha512sum of your PUT/POST as base64; you can gene
|
|||||||
quick outline of the up2k protocol, see [uploading](#uploading) for the web-client
|
quick outline of the up2k protocol, see [uploading](#uploading) for the web-client
|
||||||
* the up2k client splits a file into an "optimal" number of chunks
|
* the up2k client splits a file into an "optimal" number of chunks
|
||||||
* 1 MiB each, unless that becomes more than 256 chunks
|
* 1 MiB each, unless that becomes more than 256 chunks
|
||||||
* tries 1.5M, 2M, 3, 4, 6, ... until <= 256# or chunksize >= 32M
|
* tries 1.5M, 2M, 3, 4, 6, ... until <= 256 chunks or size >= 32M
|
||||||
* client posts the list of hashes, filename, size, last-modified
|
* client posts the list of hashes, filename, size, last-modified
|
||||||
* server creates the `wark`, an identifier for this upload
|
* server creates the `wark`, an identifier for this upload
|
||||||
* `sha512( salt + filesize + chunk_hashes )`
|
* `sha512( salt + filesize + chunk_hashes )`
|
||||||
@@ -382,13 +490,31 @@ quick outline of the up2k protocol, see [uploading](#uploading) for the web-clie
|
|||||||
|
|
||||||
* `jinja2` (is built into the SFX)
|
* `jinja2` (is built into the SFX)
|
||||||
|
|
||||||
**optional,** enables music tags:
|
|
||||||
|
## optional dependencies
|
||||||
|
|
||||||
|
enable music tags:
|
||||||
* either `mutagen` (fast, pure-python, skips a few tags, makes copyparty GPL? idk)
|
* either `mutagen` (fast, pure-python, skips a few tags, makes copyparty GPL? idk)
|
||||||
* or `FFprobe` (20x slower, more accurate, possibly dangerous depending on your distro and users)
|
* or `FFprobe` (20x slower, more accurate, possibly dangerous depending on your distro and users)
|
||||||
|
|
||||||
**optional,** will eventually enable thumbnails:
|
enable image thumbnails:
|
||||||
* `Pillow` (requires py2.7 or py3.5+)
|
* `Pillow` (requires py2.7 or py3.5+)
|
||||||
|
|
||||||
|
enable video thumbnails:
|
||||||
|
* `ffmpeg` and `ffprobe` somewhere in `$PATH`
|
||||||
|
|
||||||
|
enable reading HEIF pictures:
|
||||||
|
* `pyheif-pillow-opener` (requires Linux or a C compiler)
|
||||||
|
|
||||||
|
enable reading AVIF pictures:
|
||||||
|
* `pillow-avif-plugin`
|
||||||
|
|
||||||
|
|
||||||
|
## install recommended deps
|
||||||
|
```
|
||||||
|
python -m pip install --user -U jinja2 mutagen Pillow
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## optional gpl stuff
|
## optional gpl stuff
|
||||||
|
|
||||||
@@ -399,9 +525,9 @@ these are standalone programs and will never be imported / evaluated by copypart
|
|||||||
|
|
||||||
# sfx
|
# sfx
|
||||||
|
|
||||||
currently there are two self-contained binaries:
|
currently there are two self-contained "binaries":
|
||||||
* [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) -- pure python, works everywhere
|
* [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) -- pure python, works everywhere, **recommended**
|
||||||
* [copyparty-sfx.sh](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.sh) -- smaller, but only for linux and macos
|
* [copyparty-sfx.sh](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.sh) -- smaller, but only for linux and macos, kinda deprecated
|
||||||
|
|
||||||
launch either of them (**use sfx.py on systemd**) and it'll unpack and run copyparty, assuming you have python installed of course
|
launch either of them (**use sfx.py on systemd**) and it'll unpack and run copyparty, assuming you have python installed of course
|
||||||
|
|
||||||
@@ -435,18 +561,45 @@ echo $?
|
|||||||
after the initial setup, you can launch copyparty at any time by running `copyparty` anywhere in Termux
|
after the initial setup, you can launch copyparty at any time by running `copyparty` anywhere in Termux
|
||||||
|
|
||||||
|
|
||||||
# dev env setup
|
# building
|
||||||
|
|
||||||
|
## dev env setup
|
||||||
|
|
||||||
|
mostly optional; if you need a working env for vscode or similar
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python3 -m venv .venv
|
python3 -m venv .venv
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
pip install jinja2 # mandatory deps
|
pip install jinja2 # mandatory
|
||||||
pip install Pillow # thumbnail deps
|
pip install mutagen # audio metadata
|
||||||
|
pip install Pillow pyheif-pillow-opener pillow-avif-plugin # thumbnails
|
||||||
pip install black bandit pylint flake8 # vscode tooling
|
pip install black bandit pylint flake8 # vscode tooling
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
# how to release
|
## just the sfx
|
||||||
|
|
||||||
|
unless you need to modify something in the web-dependencies, it's faster to grab those from a previous release:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rm -rf copyparty/web/deps
|
||||||
|
curl -L https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py >x.py
|
||||||
|
python3 x.py -h
|
||||||
|
rm x.py
|
||||||
|
mv /tmp/pe-copyparty/copyparty/web/deps/ copyparty/web/
|
||||||
|
```
|
||||||
|
|
||||||
|
then build the sfx using any of the following examples:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./scripts/make-sfx.sh # both python and sh editions
|
||||||
|
./scripts/make-sfx.sh no-sh gz # just python with gzip
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## complete release
|
||||||
|
|
||||||
|
also builds the sfx so disregard the sfx section above
|
||||||
|
|
||||||
in the `scripts` folder:
|
in the `scripts` folder:
|
||||||
|
|
||||||
@@ -461,20 +614,26 @@ in the `scripts` folder:
|
|||||||
|
|
||||||
roughly sorted by priority
|
roughly sorted by priority
|
||||||
|
|
||||||
* separate sqlite table per tag
|
|
||||||
* audio fingerprinting
|
|
||||||
* readme.md as epilogue
|
* readme.md as epilogue
|
||||||
* reduce up2k roundtrips
|
* reduce up2k roundtrips
|
||||||
* start from a chunk index and just go
|
* start from a chunk index and just go
|
||||||
* terminate client on bad data
|
* terminate client on bad data
|
||||||
* `os.copy_file_range` for up2k cloning
|
|
||||||
* support pillow-simd
|
|
||||||
* figure out the deal with pixel3a not being connectable as hotspot
|
|
||||||
* pixel3a having unpredictable 3sec latency in general :||||
|
|
||||||
|
|
||||||
discarded ideas
|
discarded ideas
|
||||||
|
|
||||||
|
* single sha512 across all up2k chunks?
|
||||||
|
* crypto.subtle cannot into streaming, would have to use hashwasm, expensive
|
||||||
|
* separate sqlite table per tag
|
||||||
|
* performance fixed by skipping some indexes (`+mt.k`)
|
||||||
|
* audio fingerprinting
|
||||||
|
* only makes sense if there can be a wasm client and that doesn't exist yet (except for olaf which is agpl hence counts as not existing)
|
||||||
|
* `os.copy_file_range` for up2k cloning
|
||||||
|
* almost never hit this path anyways
|
||||||
* up2k partials ui
|
* up2k partials ui
|
||||||
|
* feels like there isn't much point
|
||||||
* cache sha512 chunks on client
|
* cache sha512 chunks on client
|
||||||
|
* too dangerous
|
||||||
* comment field
|
* comment field
|
||||||
|
* nah
|
||||||
* look into android thumbnail cache file format
|
* look into android thumbnail cache file format
|
||||||
|
* absolutely not
|
||||||
|
|||||||
@@ -45,3 +45,19 @@ you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releas
|
|||||||
# [`mtag/`](mtag/)
|
# [`mtag/`](mtag/)
|
||||||
* standalone programs which perform misc. file analysis
|
* standalone programs which perform misc. file analysis
|
||||||
* copyparty can Popen programs like these during file indexing to collect additional metadata
|
* copyparty can Popen programs like these during file indexing to collect additional metadata
|
||||||
|
|
||||||
|
|
||||||
|
# [`dbtool.py`](dbtool.py)
|
||||||
|
upgrade utility which can show db info and help transfer data between databases, for example when a new version of copyparty is incompatible with the old DB and automatically rebuilds the DB from scratch, but you have some really expensive `-mtp` parsers and want to copy over the tags from the old db
|
||||||
|
|
||||||
|
for that example (upgrading to v0.11.20), first launch the new version of copyparty like usual, let it make a backup of the old db and rebuild the new db until the point where it starts running mtp (colored messages as it adds the mtp tags), that's when you hit CTRL-C and patch in the old mtp tags from the old db instead
|
||||||
|
|
||||||
|
so assuming you have `-mtp` parsers to provide the tags `key` and `.bpm`:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd /mnt/nas/music/.hist
|
||||||
|
~/src/copyparty/bin/dbtool.py -ls up2k.db
|
||||||
|
~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -cmp
|
||||||
|
~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy key
|
||||||
|
~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy .bpm -vac
|
||||||
|
```
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ MACOS = platform.system() == "Darwin"
|
|||||||
info = log = dbg = None
|
info = log = dbg = None
|
||||||
|
|
||||||
|
|
||||||
|
print("{} v{} @ {}".format(
|
||||||
|
platform.python_implementation(),
|
||||||
|
".".join([str(x) for x in sys.version_info]),
|
||||||
|
sys.executable))
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from fuse import FUSE, FuseOSError, Operations
|
from fuse import FUSE, FuseOSError, Operations
|
||||||
except:
|
except:
|
||||||
|
|||||||
245
bin/dbtool.py
Executable file
245
bin/dbtool.py
Executable file
@@ -0,0 +1,245 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import shutil
|
||||||
|
import sqlite3
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
DB_VER1 = 3
|
||||||
|
DB_VER2 = 4
|
||||||
|
|
||||||
|
|
||||||
|
def die(msg):
|
||||||
|
print("\033[31m\n" + msg + "\n\033[0m")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def read_ver(db):
|
||||||
|
for tab in ["ki", "kv"]:
|
||||||
|
try:
|
||||||
|
c = db.execute(r"select v from {} where k = 'sver'".format(tab))
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
rows = c.fetchall()
|
||||||
|
if rows:
|
||||||
|
return int(rows[0][0])
|
||||||
|
|
||||||
|
return "corrupt"
|
||||||
|
|
||||||
|
|
||||||
|
def ls(db):
|
||||||
|
nfiles = next(db.execute("select count(w) from up"))[0]
|
||||||
|
ntags = next(db.execute("select count(w) from mt"))[0]
|
||||||
|
print(f"{nfiles} files")
|
||||||
|
print(f"{ntags} tags\n")
|
||||||
|
|
||||||
|
print("number of occurences for each tag,")
|
||||||
|
print(" 'x' = file has no tags")
|
||||||
|
print(" 't:mtp' = the mtp flag (file not mtp processed yet)")
|
||||||
|
print()
|
||||||
|
for k, nk in db.execute("select k, count(k) from mt group by k order by k"):
|
||||||
|
print(f"{nk:9} {k}")
|
||||||
|
|
||||||
|
|
||||||
|
def compare(n1, d1, n2, d2, verbose):
|
||||||
|
nt = next(d1.execute("select count(w) from up"))[0]
|
||||||
|
n = 0
|
||||||
|
miss = 0
|
||||||
|
for w1, rd, fn in d1.execute("select w, rd, fn from up"):
|
||||||
|
n += 1
|
||||||
|
if n % 25_000 == 0:
|
||||||
|
m = f"\033[36mchecked {n:,} of {nt:,} files in {n1} against {n2}\033[0m"
|
||||||
|
print(m)
|
||||||
|
|
||||||
|
if rd.split("/", 1)[0] == ".hist":
|
||||||
|
continue
|
||||||
|
|
||||||
|
q = "select w from up where rd = ? and fn = ?"
|
||||||
|
hit = d2.execute(q, (rd, fn)).fetchone()
|
||||||
|
if not hit:
|
||||||
|
miss += 1
|
||||||
|
if verbose:
|
||||||
|
print(f"file in {n1} missing in {n2}: [{w1}] {rd}/{fn}")
|
||||||
|
|
||||||
|
print(f" {miss} files in {n1} missing in {n2}\n")
|
||||||
|
|
||||||
|
nt = next(d1.execute("select count(w) from mt"))[0]
|
||||||
|
n = 0
|
||||||
|
miss = {}
|
||||||
|
nmiss = 0
|
||||||
|
for w1, k, v in d1.execute("select * from mt"):
|
||||||
|
|
||||||
|
n += 1
|
||||||
|
if n % 100_000 == 0:
|
||||||
|
m = f"\033[36mchecked {n:,} of {nt:,} tags in {n1} against {n2}, so far {nmiss} missing tags\033[0m"
|
||||||
|
print(m)
|
||||||
|
|
||||||
|
q = "select rd, fn from up where substr(w,1,16) = ?"
|
||||||
|
rd, fn = d1.execute(q, (w1,)).fetchone()
|
||||||
|
if rd.split("/", 1)[0] == ".hist":
|
||||||
|
continue
|
||||||
|
|
||||||
|
q = "select substr(w,1,16) from up where rd = ? and fn = ?"
|
||||||
|
w2 = d2.execute(q, (rd, fn)).fetchone()
|
||||||
|
if w2:
|
||||||
|
w2 = w2[0]
|
||||||
|
|
||||||
|
v2 = None
|
||||||
|
if w2:
|
||||||
|
v2 = d2.execute(
|
||||||
|
"select v from mt where w = ? and +k = ?", (w2, k)
|
||||||
|
).fetchone()
|
||||||
|
if v2:
|
||||||
|
v2 = v2[0]
|
||||||
|
|
||||||
|
# if v != v2 and v2 and k in [".bpm", "key"] and n2 == "src":
|
||||||
|
# print(f"{w} [{rd}/{fn}] {k} = [{v}] / [{v2}]")
|
||||||
|
|
||||||
|
if v2 is not None:
|
||||||
|
if k.startswith("."):
|
||||||
|
try:
|
||||||
|
diff = abs(float(v) - float(v2))
|
||||||
|
if diff > float(v) / 0.9:
|
||||||
|
v2 = None
|
||||||
|
else:
|
||||||
|
v2 = v
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if v != v2:
|
||||||
|
v2 = None
|
||||||
|
|
||||||
|
if v2 is None:
|
||||||
|
nmiss += 1
|
||||||
|
try:
|
||||||
|
miss[k] += 1
|
||||||
|
except:
|
||||||
|
miss[k] = 1
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f"missing in {n2}: [{w1}] [{rd}/{fn}] {k} = {v}")
|
||||||
|
|
||||||
|
for k, v in sorted(miss.items()):
|
||||||
|
if v:
|
||||||
|
print(f"{n1} has {v:6} more {k:<6} tags than {n2}")
|
||||||
|
|
||||||
|
print(f"in total, {nmiss} missing tags in {n2}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def copy_mtp(d1, d2, tag, rm):
|
||||||
|
nt = next(d1.execute("select count(w) from mt where k = ?", (tag,)))[0]
|
||||||
|
n = 0
|
||||||
|
ndone = 0
|
||||||
|
for w1, k, v in d1.execute("select * from mt where k = ?", (tag,)):
|
||||||
|
n += 1
|
||||||
|
if n % 25_000 == 0:
|
||||||
|
m = f"\033[36m{n:,} of {nt:,} tags checked, so far {ndone} copied\033[0m"
|
||||||
|
print(m)
|
||||||
|
|
||||||
|
q = "select rd, fn from up where substr(w,1,16) = ?"
|
||||||
|
rd, fn = d1.execute(q, (w1,)).fetchone()
|
||||||
|
if rd.split("/", 1)[0] == ".hist":
|
||||||
|
continue
|
||||||
|
|
||||||
|
q = "select substr(w,1,16) from up where rd = ? and fn = ?"
|
||||||
|
w2 = d2.execute(q, (rd, fn)).fetchone()
|
||||||
|
if not w2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
w2 = w2[0]
|
||||||
|
hit = d2.execute("select v from mt where w = ? and +k = ?", (w2, k)).fetchone()
|
||||||
|
if hit:
|
||||||
|
hit = hit[0]
|
||||||
|
|
||||||
|
if hit != v:
|
||||||
|
ndone += 1
|
||||||
|
if hit is not None:
|
||||||
|
d2.execute("delete from mt where w = ? and +k = ?", (w2, k))
|
||||||
|
|
||||||
|
d2.execute("insert into mt values (?,?,?)", (w2, k, v))
|
||||||
|
if rm:
|
||||||
|
d2.execute("delete from mt where w = ? and +k = 't:mtp'", (w2,))
|
||||||
|
|
||||||
|
d2.commit()
|
||||||
|
print(f"copied {ndone} {tag} tags over")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
os.system("")
|
||||||
|
print()
|
||||||
|
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("db", help="database to work on")
|
||||||
|
ap.add_argument("-src", metavar="DB", type=str, help="database to copy from")
|
||||||
|
|
||||||
|
ap2 = ap.add_argument_group("informational / read-only stuff")
|
||||||
|
ap2.add_argument("-v", action="store_true", help="verbose")
|
||||||
|
ap2.add_argument("-ls", action="store_true", help="list summary for db")
|
||||||
|
ap2.add_argument("-cmp", action="store_true", help="compare databases")
|
||||||
|
|
||||||
|
ap2 = ap.add_argument_group("options which modify target db")
|
||||||
|
ap2.add_argument("-copy", metavar="TAG", type=str, help="mtp tag to copy over")
|
||||||
|
ap2.add_argument(
|
||||||
|
"-rm-mtp-flag",
|
||||||
|
action="store_true",
|
||||||
|
help="when an mtp tag is copied over, also mark that as done, so copyparty won't run mtp on it",
|
||||||
|
)
|
||||||
|
ap2.add_argument("-vac", action="store_true", help="optimize DB")
|
||||||
|
|
||||||
|
ar = ap.parse_args()
|
||||||
|
|
||||||
|
for v in [ar.db, ar.src]:
|
||||||
|
if v and not os.path.exists(v):
|
||||||
|
die("database must exist")
|
||||||
|
|
||||||
|
db = sqlite3.connect(ar.db)
|
||||||
|
ds = sqlite3.connect(ar.src) if ar.src else None
|
||||||
|
|
||||||
|
# revert journals
|
||||||
|
for d, p in [[db, ar.db], [ds, ar.src]]:
|
||||||
|
if not d:
|
||||||
|
continue
|
||||||
|
|
||||||
|
pj = "{}-journal".format(p)
|
||||||
|
if not os.path.exists(pj):
|
||||||
|
continue
|
||||||
|
|
||||||
|
d.execute("create table foo (bar int)")
|
||||||
|
d.execute("drop table foo")
|
||||||
|
|
||||||
|
if ar.copy:
|
||||||
|
db.close()
|
||||||
|
shutil.copy2(ar.db, "{}.bak.dbtool.{:x}".format(ar.db, int(time.time())))
|
||||||
|
db = sqlite3.connect(ar.db)
|
||||||
|
|
||||||
|
for d, n in [[ds, "src"], [db, "dst"]]:
|
||||||
|
if not d:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ver = read_ver(d)
|
||||||
|
if ver == "corrupt":
|
||||||
|
die("{} database appears to be corrupt, sorry")
|
||||||
|
|
||||||
|
if ver < DB_VER1 or ver > 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"
|
||||||
|
die(m)
|
||||||
|
|
||||||
|
if ar.ls:
|
||||||
|
ls(db)
|
||||||
|
|
||||||
|
if ar.cmp:
|
||||||
|
if not ds:
|
||||||
|
die("need src db to compare against")
|
||||||
|
|
||||||
|
compare("src", ds, "dst", db, ar.v)
|
||||||
|
compare("dst", db, "src", ds, ar.v)
|
||||||
|
|
||||||
|
if ar.copy:
|
||||||
|
copy_mtp(ds, db, ar.copy, ar.rm_mtp_flag)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -60,7 +60,7 @@ def main():
|
|||||||
try:
|
try:
|
||||||
det(tf)
|
det(tf)
|
||||||
except:
|
except:
|
||||||
pass
|
pass # mute
|
||||||
finally:
|
finally:
|
||||||
os.unlink(tf)
|
os.unlink(tf)
|
||||||
|
|
||||||
|
|||||||
123
bin/mtag/audio-key-slicing.py
Executable file
123
bin/mtag/audio-key-slicing.py
Executable file
@@ -0,0 +1,123 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import subprocess as sp
|
||||||
|
|
||||||
|
import keyfinder
|
||||||
|
|
||||||
|
from copyparty.util import fsenc
|
||||||
|
|
||||||
|
"""
|
||||||
|
dep: github/mixxxdj/libkeyfinder
|
||||||
|
dep: pypi/keyfinder
|
||||||
|
dep: ffmpeg
|
||||||
|
|
||||||
|
note: this is a janky edition of the regular audio-key.py,
|
||||||
|
slicing the files at 20sec intervals and keeping 5sec from each,
|
||||||
|
surprisingly accurate but still garbage (446 ok, 69 bad, 13% miss)
|
||||||
|
|
||||||
|
it is fast tho
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_duration():
|
||||||
|
# TODO provide ffprobe tags to mtp as json
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
dur = sp.check_output([
|
||||||
|
"ffprobe",
|
||||||
|
"-hide_banner",
|
||||||
|
"-v", "fatal",
|
||||||
|
"-show_streams",
|
||||||
|
"-show_format",
|
||||||
|
fsenc(sys.argv[1])
|
||||||
|
])
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
dur = dur.decode("ascii", "replace").split("\n")
|
||||||
|
dur = [x.split("=")[1] for x in dur if x.startswith("duration=")]
|
||||||
|
dur = [float(x) for x in dur if re.match(r"^[0-9\.,]+$", x)]
|
||||||
|
return list(sorted(dur))[-1] if dur else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_segs(dur):
|
||||||
|
# keep first 5s of each 20s,
|
||||||
|
# keep entire last segment
|
||||||
|
ofs = 0
|
||||||
|
segs = []
|
||||||
|
while True:
|
||||||
|
seg = [ofs, 5]
|
||||||
|
segs.append(seg)
|
||||||
|
if dur - ofs < 20:
|
||||||
|
seg[-1] = int(dur - seg[0])
|
||||||
|
break
|
||||||
|
|
||||||
|
ofs += 20
|
||||||
|
|
||||||
|
return segs
|
||||||
|
|
||||||
|
|
||||||
|
def slice(tf):
|
||||||
|
dur = get_duration()
|
||||||
|
dur = min(dur, 600) # max 10min
|
||||||
|
segs = get_segs(dur)
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-nostdin",
|
||||||
|
"-hide_banner",
|
||||||
|
"-v", "fatal",
|
||||||
|
"-y"
|
||||||
|
]
|
||||||
|
|
||||||
|
for seg in segs:
|
||||||
|
cmd.extend([
|
||||||
|
"-ss", str(seg[0]),
|
||||||
|
"-i", fsenc(sys.argv[1])
|
||||||
|
])
|
||||||
|
|
||||||
|
filt = ""
|
||||||
|
for n, seg in enumerate(segs):
|
||||||
|
filt += "[{}:a:0]atrim=duration={}[a{}]; ".format(n, seg[1], n)
|
||||||
|
|
||||||
|
prev = "a0"
|
||||||
|
for n in range(1, len(segs)):
|
||||||
|
nxt = "b{}".format(n)
|
||||||
|
filt += "[{}][a{}]acrossfade=d=0.5[{}]; ".format(prev, n, nxt)
|
||||||
|
prev = nxt
|
||||||
|
|
||||||
|
cmd.extend([
|
||||||
|
"-filter_complex", filt[:-2],
|
||||||
|
"-map", "[{}]".format(nxt),
|
||||||
|
"-sample_fmt", "s16",
|
||||||
|
tf
|
||||||
|
])
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
# print(cmd)
|
||||||
|
sp.check_call(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def det(tf):
|
||||||
|
slice(tf)
|
||||||
|
print(keyfinder.key(tf).camelot())
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".flac", delete=False) as f:
|
||||||
|
f.write(b"h")
|
||||||
|
tf = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
det(tf)
|
||||||
|
finally:
|
||||||
|
os.unlink(tf)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,18 +1,54 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import subprocess as sp
|
||||||
import keyfinder
|
import keyfinder
|
||||||
|
|
||||||
|
from copyparty.util import fsenc
|
||||||
|
|
||||||
"""
|
"""
|
||||||
dep: github/mixxxdj/libkeyfinder
|
dep: github/mixxxdj/libkeyfinder
|
||||||
dep: pypi/keyfinder
|
dep: pypi/keyfinder
|
||||||
dep: ffmpeg
|
dep: ffmpeg
|
||||||
|
|
||||||
note: cannot fsenc
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
try:
|
# tried trimming the first/last 5th, bad idea,
|
||||||
print(keyfinder.key(sys.argv[1]).camelot())
|
# misdetects 9a law field (Sphere Caliber) as 10b,
|
||||||
except:
|
# obvious when mixing 9a ghostly parapara ship
|
||||||
pass
|
|
||||||
|
|
||||||
|
def det(tf):
|
||||||
|
# fmt: off
|
||||||
|
sp.check_call([
|
||||||
|
"ffmpeg",
|
||||||
|
"-nostdin",
|
||||||
|
"-hide_banner",
|
||||||
|
"-v", "fatal",
|
||||||
|
"-y", "-i", fsenc(sys.argv[1]),
|
||||||
|
"-t", "300",
|
||||||
|
"-sample_fmt", "s16",
|
||||||
|
tf
|
||||||
|
])
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
print(keyfinder.key(tf).camelot())
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".flac", delete=False) as f:
|
||||||
|
f.write(b"h")
|
||||||
|
tf = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
det(tf)
|
||||||
|
except:
|
||||||
|
pass # mute
|
||||||
|
finally:
|
||||||
|
os.unlink(tf)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|||||||
96
bin/mtag/exe.py
Normal file
96
bin/mtag/exe.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import pefile
|
||||||
|
|
||||||
|
"""
|
||||||
|
retrieve exe info,
|
||||||
|
example for multivalue providers
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def unk(v):
|
||||||
|
return "unk({:04x})".format(v)
|
||||||
|
|
||||||
|
|
||||||
|
class PE2(pefile.PE):
|
||||||
|
def __init__(self, *a, **ka):
|
||||||
|
for k in [
|
||||||
|
# -- parse_data_directories:
|
||||||
|
"parse_import_directory",
|
||||||
|
"parse_export_directory",
|
||||||
|
# "parse_resources_directory",
|
||||||
|
"parse_debug_directory",
|
||||||
|
"parse_relocations_directory",
|
||||||
|
"parse_directory_tls",
|
||||||
|
"parse_directory_load_config",
|
||||||
|
"parse_delay_import_directory",
|
||||||
|
"parse_directory_bound_imports",
|
||||||
|
# -- full_load:
|
||||||
|
"parse_rich_header",
|
||||||
|
]:
|
||||||
|
setattr(self, k, self.noop)
|
||||||
|
|
||||||
|
super(PE2, self).__init__(*a, **ka)
|
||||||
|
|
||||||
|
def noop(*a, **ka):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
pe = PE2(sys.argv[1], fast_load=False)
|
||||||
|
except:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
arch = pe.FILE_HEADER.Machine
|
||||||
|
if arch == 0x14C:
|
||||||
|
arch = "x86"
|
||||||
|
elif arch == 0x8664:
|
||||||
|
arch = "x64"
|
||||||
|
else:
|
||||||
|
arch = unk(arch)
|
||||||
|
|
||||||
|
try:
|
||||||
|
buildtime = time.gmtime(pe.FILE_HEADER.TimeDateStamp)
|
||||||
|
buildtime = time.strftime("%Y-%m-%d_%H:%M:%S", buildtime)
|
||||||
|
except:
|
||||||
|
buildtime = "invalid"
|
||||||
|
|
||||||
|
ui = pe.OPTIONAL_HEADER.Subsystem
|
||||||
|
if ui == 2:
|
||||||
|
ui = "GUI"
|
||||||
|
elif ui == 3:
|
||||||
|
ui = "cmdline"
|
||||||
|
else:
|
||||||
|
ui = unk(ui)
|
||||||
|
|
||||||
|
extra = {}
|
||||||
|
if hasattr(pe, "FileInfo"):
|
||||||
|
for v1 in pe.FileInfo:
|
||||||
|
for v2 in v1:
|
||||||
|
if v2.name != "StringFileInfo":
|
||||||
|
continue
|
||||||
|
|
||||||
|
for v3 in v2.StringTable:
|
||||||
|
for k, v in v3.entries.items():
|
||||||
|
v = v.decode("utf-8", "replace").strip()
|
||||||
|
if not v:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if k in [b"FileVersion", b"ProductVersion"]:
|
||||||
|
extra["ver"] = v
|
||||||
|
|
||||||
|
if k in [b"OriginalFilename", b"InternalName"]:
|
||||||
|
extra["orig"] = v
|
||||||
|
|
||||||
|
r = {
|
||||||
|
"arch": arch,
|
||||||
|
"built": buildtime,
|
||||||
|
"ui": ui,
|
||||||
|
"cksum": "{:08x}".format(pe.OPTIONAL_HEADER.CheckSum),
|
||||||
|
}
|
||||||
|
r.update(extra)
|
||||||
|
|
||||||
|
print(json.dumps(r, indent=4))
|
||||||
9
bin/mtag/file-ext.py
Normal file
9
bin/mtag/file-ext.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
"""
|
||||||
|
example that just prints the file extension
|
||||||
|
"""
|
||||||
|
|
||||||
|
print(sys.argv[1].split(".")[-1])
|
||||||
@@ -9,6 +9,16 @@
|
|||||||
* assumes the webserver and copyparty is running on the same server/IP
|
* assumes the webserver and copyparty is running on the same server/IP
|
||||||
* modify `10.13.1.1` as necessary if you wish to support browsers without javascript
|
* modify `10.13.1.1` as necessary if you wish to support browsers without javascript
|
||||||
|
|
||||||
|
### [`sharex.sxcu`](sharex.sxcu)
|
||||||
|
* sharex config file to upload screenshots and grab the URL
|
||||||
|
* `RequestURL`: full URL to the target folder
|
||||||
|
* `pw`: password (remove the `pw` line if anon-write)
|
||||||
|
|
||||||
|
however if your copyparty is behind a reverse-proxy, you may want to use [`sharex-html.sxcu`](sharex-html.sxcu) instead:
|
||||||
|
* `RequestURL`: full URL to the target folder
|
||||||
|
* `URL`: full URL to the root folder (with trailing slash) followed by `$regex:1|1$`
|
||||||
|
* `pw`: password (remove `Parameters` if anon-write)
|
||||||
|
|
||||||
### [`explorer-nothumbs-nofoldertypes.reg`](explorer-nothumbs-nofoldertypes.reg)
|
### [`explorer-nothumbs-nofoldertypes.reg`](explorer-nothumbs-nofoldertypes.reg)
|
||||||
* disables thumbnails and folder-type detection in windows explorer
|
* disables thumbnails and folder-type detection in windows explorer
|
||||||
* makes it way faster (especially for slow/networked locations (such as copyparty-fuse))
|
* makes it way faster (especially for slow/networked locations (such as copyparty-fuse))
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
# when running copyparty behind a reverse-proxy,
|
||||||
|
# make sure that copyparty allows at least as many clients as the proxy does,
|
||||||
|
# so run copyparty with -nc 512 if your nginx has the default limits
|
||||||
|
# (worker_processes 1, worker_connections 512)
|
||||||
|
|
||||||
upstream cpp {
|
upstream cpp {
|
||||||
server 127.0.0.1:3923;
|
server 127.0.0.1:3923;
|
||||||
keepalive 120;
|
keepalive 120;
|
||||||
|
|||||||
19
contrib/sharex-html.sxcu
Normal file
19
contrib/sharex-html.sxcu
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"Version": "13.5.0",
|
||||||
|
"Name": "copyparty-html",
|
||||||
|
"DestinationType": "ImageUploader",
|
||||||
|
"RequestMethod": "POST",
|
||||||
|
"RequestURL": "http://127.0.0.1:3923/sharex",
|
||||||
|
"Parameters": {
|
||||||
|
"pw": "wark"
|
||||||
|
},
|
||||||
|
"Body": "MultipartFormData",
|
||||||
|
"Arguments": {
|
||||||
|
"act": "bput"
|
||||||
|
},
|
||||||
|
"FileFormName": "f",
|
||||||
|
"RegexList": [
|
||||||
|
"bytes // <a href=\"/([^\"]+)\""
|
||||||
|
],
|
||||||
|
"URL": "http://127.0.0.1:3923/$regex:1|1$"
|
||||||
|
}
|
||||||
17
contrib/sharex.sxcu
Normal file
17
contrib/sharex.sxcu
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"Version": "13.5.0",
|
||||||
|
"Name": "copyparty",
|
||||||
|
"DestinationType": "ImageUploader",
|
||||||
|
"RequestMethod": "POST",
|
||||||
|
"RequestURL": "http://127.0.0.1:3923/sharex",
|
||||||
|
"Parameters": {
|
||||||
|
"pw": "wark",
|
||||||
|
"j": null
|
||||||
|
},
|
||||||
|
"Body": "MultipartFormData",
|
||||||
|
"Arguments": {
|
||||||
|
"act": "bput"
|
||||||
|
},
|
||||||
|
"FileFormName": "f",
|
||||||
|
"URL": "$json:files[0].url$"
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import platform
|
import platform
|
||||||
|
import time
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -23,7 +24,11 @@ MACOS = platform.system() == "Darwin"
|
|||||||
|
|
||||||
class EnvParams(object):
|
class EnvParams(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.t0 = time.time()
|
||||||
self.mod = os.path.dirname(os.path.realpath(__file__))
|
self.mod = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
if self.mod.endswith("__init__"):
|
||||||
|
self.mod = os.path.dirname(self.mod)
|
||||||
|
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
self.cfg = os.path.normpath(os.environ["APPDATA"] + "/copyparty")
|
self.cfg = os.path.normpath(os.environ["APPDATA"] + "/copyparty")
|
||||||
elif sys.platform == "darwin":
|
elif sys.platform == "darwin":
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from textwrap import dedent
|
|||||||
from .__init__ import E, WINDOWS, VT100, PY2
|
from .__init__ import E, WINDOWS, VT100, PY2
|
||||||
from .__version__ import S_VERSION, S_BUILD_DT, CODENAME
|
from .__version__ import S_VERSION, S_BUILD_DT, CODENAME
|
||||||
from .svchub import SvcHub
|
from .svchub import SvcHub
|
||||||
from .util import py_desc, align_tab, IMPLICATIONS
|
from .util import py_desc, align_tab, IMPLICATIONS, alltrace
|
||||||
|
|
||||||
HAVE_SSL = True
|
HAVE_SSL = True
|
||||||
try:
|
try:
|
||||||
@@ -182,6 +182,16 @@ def sighandler(sig=None, frame=None):
|
|||||||
print("\n".join(msg))
|
print("\n".join(msg))
|
||||||
|
|
||||||
|
|
||||||
|
def stackmon(fp, ival):
|
||||||
|
ctr = 0
|
||||||
|
while True:
|
||||||
|
ctr += 1
|
||||||
|
time.sleep(ival)
|
||||||
|
st = "{}, {}\n{}".format(ctr, time.time(), alltrace())
|
||||||
|
with open(fp, "wb") as f:
|
||||||
|
f.write(st.encode("utf-8", "replace"))
|
||||||
|
|
||||||
|
|
||||||
def run_argparse(argv, formatter):
|
def run_argparse(argv, formatter):
|
||||||
ap = argparse.ArgumentParser(
|
ap = argparse.ArgumentParser(
|
||||||
formatter_class=formatter,
|
formatter_class=formatter,
|
||||||
@@ -222,34 +232,80 @@ def run_argparse(argv, formatter):
|
|||||||
"print,get" prints the data in the log and returns GET
|
"print,get" prints the data in the log and returns GET
|
||||||
(leave out the ",get" to return an error instead)
|
(leave out the ",get" to return an error instead)
|
||||||
|
|
||||||
--ciphers help = available ssl/tls ciphers,
|
values for --ls:
|
||||||
--ssl-ver help = available ssl/tls versions,
|
"USR" is a user to browse as; * is anonymous, ** is all users
|
||||||
default is what python considers safe, usually >= TLS1
|
"VOL" is a single volume to scan, default is * (all vols)
|
||||||
|
"FLAG" is flags;
|
||||||
|
"v" in addition to realpaths, print usernames and vpaths
|
||||||
|
"ln" only prints symlinks leaving the volume mountpoint
|
||||||
|
"p" exits 1 if any such symlinks are found
|
||||||
|
"r" resumes startup after the listing
|
||||||
|
examples:
|
||||||
|
--ls '**' # list all files which are possible to read
|
||||||
|
--ls '**,*,ln' # check for dangerous symlinks
|
||||||
|
--ls '**,*,ln,p,r' # check, then start normally if safe
|
||||||
|
\033[0m
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
# fmt: off
|
# fmt: off
|
||||||
ap.add_argument("-c", metavar="PATH", type=str, action="append", help="add config file")
|
ap.add_argument("-c", metavar="PATH", type=str, action="append", help="add config file")
|
||||||
ap.add_argument("-i", metavar="IP", type=str, default="0.0.0.0", help="ip to bind (comma-sep.)")
|
|
||||||
ap.add_argument("-p", metavar="PORT", type=str, default="3923", help="ports to bind (comma/range)")
|
|
||||||
ap.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients")
|
ap.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients")
|
||||||
ap.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores")
|
ap.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores")
|
||||||
ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account")
|
ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account, USER:PASS; example [ed:wark")
|
||||||
ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume")
|
ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume, SRC:DST:FLAG; example [.::r], [/mnt/nas/music:/music:r:aed")
|
||||||
ap.add_argument("-q", action="store_true", help="quiet")
|
|
||||||
ap.add_argument("--log-conn", action="store_true", help="print tcp-server msgs")
|
|
||||||
ap.add_argument("-ed", action="store_true", help="enable ?dots")
|
ap.add_argument("-ed", action="store_true", help="enable ?dots")
|
||||||
ap.add_argument("-emp", action="store_true", help="enable markdown plugins")
|
ap.add_argument("-emp", action="store_true", help="enable markdown plugins")
|
||||||
ap.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate")
|
ap.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate")
|
||||||
ap.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
|
ap.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads")
|
||||||
ap.add_argument("-nih", action="store_true", help="no info hostname")
|
|
||||||
ap.add_argument("-nid", action="store_true", help="no info disk-usage")
|
|
||||||
ap.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
|
|
||||||
ap.add_argument("--no-sendfile", action="store_true", help="disable sendfile (for debugging)")
|
|
||||||
ap.add_argument("--no-scandir", action="store_true", help="disable scandir (for debugging)")
|
|
||||||
ap.add_argument("--sparse", metavar="MiB", type=int, default=4, help="up2k min.size threshold (mswin-only)")
|
ap.add_argument("--sparse", metavar="MiB", type=int, default=4, help="up2k min.size threshold (mswin-only)")
|
||||||
ap.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms")
|
ap.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms; examples: [stash], [save,get]")
|
||||||
ap.add_argument("--salt", type=str, default="hunter2", help="up2k file-hash salt")
|
|
||||||
|
ap2 = ap.add_argument_group('network options')
|
||||||
|
ap2.add_argument("-i", metavar="IP", type=str, default="0.0.0.0", help="ip to bind (comma-sep.)")
|
||||||
|
ap2.add_argument("-p", metavar="PORT", type=str, default="3923", help="ports to bind (comma/range)")
|
||||||
|
ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to keep; 0 = tcp, 1 = origin (first x-fwd), 2 = cloudflare, 3 = nginx, -1 = closest proxy")
|
||||||
|
|
||||||
|
ap2 = ap.add_argument_group('SSL/TLS options')
|
||||||
|
ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls")
|
||||||
|
ap2.add_argument("--https-only", action="store_true", help="disable plaintext")
|
||||||
|
ap2.add_argument("--ssl-ver", metavar="LIST", type=str, help="set allowed ssl/tls versions; [help] shows available versions; default is what your python version considers safe")
|
||||||
|
ap2.add_argument("--ciphers", metavar="LIST", help="set allowed ssl/tls ciphers; [help] shows available ciphers")
|
||||||
|
ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
|
||||||
|
ap2.add_argument("--ssl-log", metavar="PATH", help="log master secrets")
|
||||||
|
|
||||||
|
ap2 = ap.add_argument_group('opt-outs')
|
||||||
|
ap2.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
|
||||||
|
ap2.add_argument("-nih", action="store_true", help="no info hostname")
|
||||||
|
ap2.add_argument("-nid", action="store_true", help="no info disk-usage")
|
||||||
|
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
|
||||||
|
|
||||||
|
ap2 = ap.add_argument_group('safety options')
|
||||||
|
ap2.add_argument("--ls", metavar="U[,V[,F]]", help="scan all volumes; arguments USER,VOL,FLAGS; example [**,*,ln,p,r]")
|
||||||
|
ap2.add_argument("--salt", type=str, default="hunter2", help="up2k file-hash salt")
|
||||||
|
|
||||||
|
ap2 = ap.add_argument_group('logging options')
|
||||||
|
ap2.add_argument("-q", action="store_true", help="quiet")
|
||||||
|
ap2.add_argument("--log-conn", action="store_true", help="print tcp-server msgs")
|
||||||
|
ap2.add_argument("--ihead", metavar="HEADER", action='append', help="dump incoming header")
|
||||||
|
ap2.add_argument("--lf-url", metavar="RE", type=str, default=r"^/\.cpr/|\?th=[wj]$", help="dont log URLs matching")
|
||||||
|
|
||||||
|
ap2 = ap.add_argument_group('admin panel options')
|
||||||
|
ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)")
|
||||||
|
ap2.add_argument("--no-stack", action="store_true", help="disable ?stack (list all stacks)")
|
||||||
|
|
||||||
|
ap2 = ap.add_argument_group('thumbnail options')
|
||||||
|
ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails")
|
||||||
|
ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails")
|
||||||
|
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res")
|
||||||
|
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image")
|
||||||
|
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
|
||||||
|
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
|
||||||
|
ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg for video thumbs")
|
||||||
|
ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown")
|
||||||
|
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval")
|
||||||
|
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age")
|
||||||
|
ap2.add_argument("--th-covers", metavar="N,N", type=str, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat for")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('database options')
|
ap2 = ap.add_argument_group('database options')
|
||||||
ap2.add_argument("-e2d", action="store_true", help="enable up2k database")
|
ap2.add_argument("-e2d", action="store_true", help="enable up2k database")
|
||||||
@@ -258,21 +314,24 @@ def run_argparse(argv, formatter):
|
|||||||
ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing")
|
ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing")
|
||||||
ap2.add_argument("-e2ts", action="store_true", help="enable metadata scanner, sets -e2t")
|
ap2.add_argument("-e2ts", action="store_true", help="enable metadata scanner, sets -e2t")
|
||||||
ap2.add_argument("-e2tsr", action="store_true", help="rescan all metadata, sets -e2ts")
|
ap2.add_argument("-e2tsr", action="store_true", help="rescan all metadata, sets -e2ts")
|
||||||
|
ap2.add_argument("--hist", metavar="PATH", type=str, help="where to store volume state")
|
||||||
|
ap2.add_argument("--no-hash", action="store_true", help="disable hashing during e2ds folder scans")
|
||||||
ap2.add_argument("--no-mutagen", action="store_true", help="use ffprobe for tags instead")
|
ap2.add_argument("--no-mutagen", action="store_true", help="use ffprobe for tags instead")
|
||||||
ap2.add_argument("--no-mtag-mt", action="store_true", help="disable tag-read parallelism")
|
ap2.add_argument("--no-mtag-mt", action="store_true", help="disable tag-read parallelism")
|
||||||
ap2.add_argument("-mtm", metavar="M=t,t,t", action="append", type=str, help="add/replace metadata mapping")
|
ap2.add_argument("-mtm", metavar="M=t,t,t", action="append", type=str, help="add/replace metadata mapping")
|
||||||
ap2.add_argument("-mte", metavar="M,M,M", type=str, help="tags to index/display (comma-sep.)",
|
ap2.add_argument("-mte", metavar="M,M,M", type=str, help="tags to index/display (comma-sep.)",
|
||||||
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q")
|
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,ac,vc,res,.fps")
|
||||||
ap2.add_argument("-mtp", metavar="M=[f,]bin", action="append", type=str, help="read tag M using bin")
|
ap2.add_argument("-mtp", metavar="M=[f,]bin", action="append", type=str, help="read tag M using bin")
|
||||||
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline")
|
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('SSL/TLS options')
|
ap2 = ap.add_argument_group('appearance options')
|
||||||
ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls")
|
ap2.add_argument("--css-browser", metavar="L", help="URL to additional CSS to include")
|
||||||
ap2.add_argument("--https-only", action="store_true", help="disable plaintext")
|
|
||||||
ap2.add_argument("--ssl-ver", metavar="LIST", type=str, help="ssl/tls versions to allow")
|
ap2 = ap.add_argument_group('debug options')
|
||||||
ap2.add_argument("--ciphers", metavar="LIST", help="set allowed ciphers")
|
ap2.add_argument("--no-sendfile", action="store_true", help="disable sendfile")
|
||||||
ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
|
ap2.add_argument("--no-scandir", action="store_true", help="disable scandir")
|
||||||
ap2.add_argument("--ssl-log", metavar="PATH", help="log master secrets")
|
ap2.add_argument("--no-fastboot", action="store_true", help="wait for up2k indexing")
|
||||||
|
ap2.add_argument("--stackmon", metavar="P,S", help="write stacktrace to Path every S second")
|
||||||
|
|
||||||
return ap.parse_args(args=argv[1:])
|
return ap.parse_args(args=argv[1:])
|
||||||
# fmt: on
|
# fmt: on
|
||||||
@@ -312,6 +371,16 @@ def main(argv=None):
|
|||||||
except AssertionError:
|
except AssertionError:
|
||||||
al = run_argparse(argv, Dodge11874)
|
al = run_argparse(argv, Dodge11874)
|
||||||
|
|
||||||
|
if al.stackmon:
|
||||||
|
fp, f = al.stackmon.rsplit(",", 1)
|
||||||
|
f = int(f)
|
||||||
|
t = threading.Thread(
|
||||||
|
target=stackmon,
|
||||||
|
args=(fp, f),
|
||||||
|
)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
# propagate implications
|
# propagate implications
|
||||||
for k1, k2 in IMPLICATIONS:
|
for k1, k2 in IMPLICATIONS:
|
||||||
if getattr(al, k1):
|
if getattr(al, k1):
|
||||||
@@ -342,6 +411,9 @@ def main(argv=None):
|
|||||||
+ " (if you crash with codec errors then that is why)"
|
+ " (if you crash with codec errors then that is why)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if sys.version_info < (3, 6):
|
||||||
|
al.no_scandir = True
|
||||||
|
|
||||||
# signal.signal(signal.SIGINT, sighandler)
|
# signal.signal(signal.SIGINT, sighandler)
|
||||||
|
|
||||||
SvcHub(al).run()
|
SvcHub(al).run()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
VERSION = (0, 10, 16)
|
VERSION = (0, 11, 30)
|
||||||
CODENAME = "zip it"
|
CODENAME = "the grid"
|
||||||
BUILD_DT = (2021, 5, 2)
|
BUILD_DT = (2021, 7, 1)
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -5,35 +5,49 @@ import re
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import stat
|
import stat
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from .__init__ import PY2, WINDOWS
|
from .__init__ import WINDOWS
|
||||||
from .util import IMPLICATIONS, undot, Pebkac, fsdec, fsenc, statdir, nuprint
|
from .util import IMPLICATIONS, uncyg, undot, Pebkac, fsdec, fsenc, statdir, nuprint
|
||||||
|
|
||||||
|
|
||||||
class VFS(object):
|
class VFS(object):
|
||||||
"""single level in the virtual fs"""
|
"""single level in the virtual fs"""
|
||||||
|
|
||||||
def __init__(self, realpath, vpath, uread=[], uwrite=[], flags={}):
|
def __init__(self, realpath, vpath, uread=[], uwrite=[], uadm=[], flags={}):
|
||||||
self.realpath = realpath # absolute path on host filesystem
|
self.realpath = realpath # absolute path on host filesystem
|
||||||
self.vpath = vpath # absolute path in the virtual filesystem
|
self.vpath = vpath # absolute path in the virtual filesystem
|
||||||
self.uread = uread # users who can read this
|
self.uread = uread # users who can read this
|
||||||
self.uwrite = uwrite # users who can write this
|
self.uwrite = uwrite # users who can write this
|
||||||
|
self.uadm = uadm # users who are regular admins
|
||||||
self.flags = flags # config switches
|
self.flags = flags # config switches
|
||||||
self.nodes = {} # child nodes
|
self.nodes = {} # child nodes
|
||||||
self.all_vols = {vpath: self} # flattened recursive
|
self.histtab = None # all realpath->histpath
|
||||||
|
self.dbv = None # closest full/non-jump parent
|
||||||
|
|
||||||
|
if realpath:
|
||||||
|
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
|
||||||
|
self.all_vols = {vpath: self} # flattened recursive
|
||||||
|
else:
|
||||||
|
self.histpath = None
|
||||||
|
self.all_vols = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "VFS({})".format(
|
return "VFS({})".format(
|
||||||
", ".join(
|
", ".join(
|
||||||
"{}={!r}".format(k, self.__dict__[k])
|
"{}={!r}".format(k, self.__dict__[k])
|
||||||
for k in "realpath vpath uread uwrite flags".split()
|
for k in "realpath vpath uread uwrite uadm flags".split()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _trk(self, vol):
|
def get_all_vols(self, outdict):
|
||||||
self.all_vols[vol.vpath] = vol
|
if self.realpath:
|
||||||
return vol
|
outdict[self.vpath] = self
|
||||||
|
|
||||||
|
for v in self.nodes.values():
|
||||||
|
v.get_all_vols(outdict)
|
||||||
|
|
||||||
def add(self, src, dst):
|
def add(self, src, dst):
|
||||||
"""get existing, or add new path to the vfs"""
|
"""get existing, or add new path to the vfs"""
|
||||||
@@ -45,18 +59,19 @@ class VFS(object):
|
|||||||
name, dst = dst.split("/", 1)
|
name, dst = dst.split("/", 1)
|
||||||
if name in self.nodes:
|
if name in self.nodes:
|
||||||
# exists; do not manipulate permissions
|
# exists; do not manipulate permissions
|
||||||
return self._trk(self.nodes[name].add(src, dst))
|
return self.nodes[name].add(src, dst)
|
||||||
|
|
||||||
vn = VFS(
|
vn = VFS(
|
||||||
"{}/{}".format(self.realpath, name),
|
os.path.join(self.realpath, name) if self.realpath else None,
|
||||||
"{}/{}".format(self.vpath, name).lstrip("/"),
|
"{}/{}".format(self.vpath, name).lstrip("/"),
|
||||||
self.uread,
|
self.uread,
|
||||||
self.uwrite,
|
self.uwrite,
|
||||||
self.flags,
|
self.uadm,
|
||||||
|
self._copy_flags(name),
|
||||||
)
|
)
|
||||||
self._trk(vn)
|
vn.dbv = self.dbv or self
|
||||||
self.nodes[name] = vn
|
self.nodes[name] = vn
|
||||||
return self._trk(vn.add(src, dst))
|
return vn.add(src, dst)
|
||||||
|
|
||||||
if dst in self.nodes:
|
if dst in self.nodes:
|
||||||
# leaf exists; return as-is
|
# leaf exists; return as-is
|
||||||
@@ -65,8 +80,26 @@ class VFS(object):
|
|||||||
# leaf does not exist; create and keep permissions blank
|
# leaf does not exist; create and keep permissions blank
|
||||||
vp = "{}/{}".format(self.vpath, dst).lstrip("/")
|
vp = "{}/{}".format(self.vpath, dst).lstrip("/")
|
||||||
vn = VFS(src, vp)
|
vn = VFS(src, vp)
|
||||||
|
vn.dbv = self.dbv or self
|
||||||
self.nodes[dst] = vn
|
self.nodes[dst] = vn
|
||||||
return self._trk(vn)
|
return vn
|
||||||
|
|
||||||
|
def _copy_flags(self, name):
|
||||||
|
flags = {k: v for k, v in self.flags.items()}
|
||||||
|
hist = flags.get("hist")
|
||||||
|
if hist and hist != "-":
|
||||||
|
flags["hist"] = "{}/{}".format(hist.rstrip("/"), name)
|
||||||
|
|
||||||
|
return flags
|
||||||
|
|
||||||
|
def bubble_flags(self):
|
||||||
|
if self.dbv:
|
||||||
|
for k, v in self.dbv.flags.items():
|
||||||
|
if k not in ["hist"]:
|
||||||
|
self.flags[k] = v
|
||||||
|
|
||||||
|
for v in self.nodes.values():
|
||||||
|
v.bubble_flags()
|
||||||
|
|
||||||
def _find(self, vpath):
|
def _find(self, vpath):
|
||||||
"""return [vfs,remainder]"""
|
"""return [vfs,remainder]"""
|
||||||
@@ -94,6 +127,7 @@ class VFS(object):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, vpath, uname, will_read, will_write):
|
def get(self, vpath, uname, will_read, will_write):
|
||||||
|
# type: (str, str, bool, bool) -> tuple[VFS, str]
|
||||||
"""returns [vfsnode,fs_remainder] if user has the requested permissions"""
|
"""returns [vfsnode,fs_remainder] if user has the requested permissions"""
|
||||||
vn, rem = self._find(vpath)
|
vn, rem = self._find(vpath)
|
||||||
|
|
||||||
@@ -105,6 +139,15 @@ class VFS(object):
|
|||||||
|
|
||||||
return vn, rem
|
return vn, rem
|
||||||
|
|
||||||
|
def get_dbv(self, vrem):
|
||||||
|
dbv = self.dbv
|
||||||
|
if not dbv:
|
||||||
|
return self, vrem
|
||||||
|
|
||||||
|
vrem = [self.vpath[len(dbv.vpath) + 1 :], vrem]
|
||||||
|
vrem = "/".join([x for x in vrem if x])
|
||||||
|
return dbv, vrem
|
||||||
|
|
||||||
def canonical(self, rem):
|
def canonical(self, rem):
|
||||||
"""returns the canonical path (fully-resolved absolute fs path)"""
|
"""returns the canonical path (fully-resolved absolute fs path)"""
|
||||||
rp = self.realpath
|
rp = self.realpath
|
||||||
@@ -133,7 +176,8 @@ class VFS(object):
|
|||||||
#
|
#
|
||||||
return os.path.realpath(rp)
|
return os.path.realpath(rp)
|
||||||
|
|
||||||
def ls(self, rem, uname, scandir, lstat=False):
|
def ls(self, rem, uname, scandir, incl_wo=False, lstat=False):
|
||||||
|
# type: (str, str, bool, bool, bool) -> tuple[str, str, dict[str, VFS]]
|
||||||
"""return user-readable [fsdir,real,virt] items at vpath"""
|
"""return user-readable [fsdir,real,virt] items at vpath"""
|
||||||
virt_vis = {} # nodes readable by user
|
virt_vis = {} # nodes readable by user
|
||||||
abspath = self.canonical(rem)
|
abspath = self.canonical(rem)
|
||||||
@@ -141,7 +185,12 @@ class VFS(object):
|
|||||||
real.sort()
|
real.sort()
|
||||||
if not rem:
|
if not rem:
|
||||||
for name, vn2 in sorted(self.nodes.items()):
|
for name, vn2 in sorted(self.nodes.items()):
|
||||||
if uname in vn2.uread or "*" in vn2.uread:
|
ok = uname in vn2.uread or "*" in vn2.uread
|
||||||
|
|
||||||
|
if not ok and incl_wo:
|
||||||
|
ok = uname in vn2.uwrite or "*" in vn2.uwrite
|
||||||
|
|
||||||
|
if ok:
|
||||||
virt_vis[name] = vn2
|
virt_vis[name] = vn2
|
||||||
|
|
||||||
# no vfs nodes in the list of real inodes
|
# no vfs nodes in the list of real inodes
|
||||||
@@ -149,13 +198,21 @@ class VFS(object):
|
|||||||
|
|
||||||
return [abspath, real, virt_vis]
|
return [abspath, real, virt_vis]
|
||||||
|
|
||||||
def walk(self, rel, rem, uname, dots, scandir, lstat=False):
|
def walk(self, rel, rem, seen, uname, dots, scandir, lstat):
|
||||||
"""
|
"""
|
||||||
recursively yields from ./rem;
|
recursively yields from ./rem;
|
||||||
rel is a unix-style user-defined vpath (not vfs-related)
|
rel is a unix-style user-defined vpath (not vfs-related)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fsroot, vfs_ls, vfs_virt = self.ls(rem, uname, scandir, lstat)
|
fsroot, vfs_ls, vfs_virt = self.ls(
|
||||||
|
rem, uname, scandir, incl_wo=False, lstat=lstat
|
||||||
|
)
|
||||||
|
|
||||||
|
if seen and not fsroot.startswith(seen[-1]) and fsroot in seen:
|
||||||
|
print("bailing from symlink loop,\n {}\n {}".format(seen[-1], fsroot))
|
||||||
|
return
|
||||||
|
|
||||||
|
seen = seen[:] + [fsroot]
|
||||||
rfiles = [x for x in vfs_ls if not stat.S_ISDIR(x[1].st_mode)]
|
rfiles = [x for x in vfs_ls if not stat.S_ISDIR(x[1].st_mode)]
|
||||||
rdirs = [x for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]
|
rdirs = [x for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]
|
||||||
|
|
||||||
@@ -170,7 +227,7 @@ class VFS(object):
|
|||||||
|
|
||||||
wrel = (rel + "/" + rdir).lstrip("/")
|
wrel = (rel + "/" + rdir).lstrip("/")
|
||||||
wrem = (rem + "/" + rdir).lstrip("/")
|
wrem = (rem + "/" + rdir).lstrip("/")
|
||||||
for x in self.walk(wrel, wrem, uname, scandir, lstat):
|
for x in self.walk(wrel, wrem, seen, uname, dots, scandir, lstat):
|
||||||
yield x
|
yield x
|
||||||
|
|
||||||
for n, vfs in sorted(vfs_virt.items()):
|
for n, vfs in sorted(vfs_virt.items()):
|
||||||
@@ -178,14 +235,16 @@ class VFS(object):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
wrel = (rel + "/" + n).lstrip("/")
|
wrel = (rel + "/" + n).lstrip("/")
|
||||||
for x in vfs.walk(wrel, "", uname, scandir, lstat):
|
for x in vfs.walk(wrel, "", seen, uname, dots, scandir, lstat):
|
||||||
yield x
|
yield x
|
||||||
|
|
||||||
def zipgen(self, vrem, flt, uname, dots, scandir):
|
def zipgen(self, vrem, flt, uname, dots, scandir):
|
||||||
if flt:
|
if flt:
|
||||||
flt = {k: True for k in flt}
|
flt = {k: True for k in flt}
|
||||||
|
|
||||||
for vpath, apath, files, rd, vd in self.walk("", vrem, uname, dots, scandir):
|
for vpath, apath, files, rd, vd in self.walk(
|
||||||
|
"", vrem, [], uname, dots, scandir, False
|
||||||
|
):
|
||||||
if flt:
|
if flt:
|
||||||
files = [x for x in files if x[0] in flt]
|
files = [x for x in files if x[0] in flt]
|
||||||
|
|
||||||
@@ -221,17 +280,19 @@ class VFS(object):
|
|||||||
for f in [{"vp": v, "ap": a, "st": n[1]} for v, a, n in files]:
|
for f in [{"vp": v, "ap": a, "st": n[1]} for v, a, n in files]:
|
||||||
yield f
|
yield f
|
||||||
|
|
||||||
def user_tree(self, uname, readable=False, writable=False):
|
def user_tree(self, uname, readable, writable, admin):
|
||||||
ret = []
|
is_readable = False
|
||||||
opt1 = readable and (uname in self.uread or "*" in self.uread)
|
if uname in self.uread or "*" in self.uread:
|
||||||
opt2 = writable and (uname in self.uwrite or "*" in self.uwrite)
|
readable.append(self.vpath)
|
||||||
if opt1 or opt2:
|
is_readable = True
|
||||||
ret.append(self.vpath)
|
|
||||||
|
if uname in self.uwrite or "*" in self.uwrite:
|
||||||
|
writable.append(self.vpath)
|
||||||
|
if is_readable:
|
||||||
|
admin.append(self.vpath)
|
||||||
|
|
||||||
for _, vn in sorted(self.nodes.items()):
|
for _, vn in sorted(self.nodes.items()):
|
||||||
ret.extend(vn.user_tree(uname, readable, writable))
|
vn.user_tree(uname, readable, writable, admin)
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
class AuthSrv(object):
|
class AuthSrv(object):
|
||||||
@@ -252,7 +313,8 @@ class AuthSrv(object):
|
|||||||
self.reload()
|
self.reload()
|
||||||
|
|
||||||
def log(self, msg, c=0):
|
def log(self, msg, c=0):
|
||||||
self.log_func("auth", msg, c)
|
if self.log_func:
|
||||||
|
self.log_func("auth", msg, c)
|
||||||
|
|
||||||
def laggy_iter(self, iterable):
|
def laggy_iter(self, iterable):
|
||||||
"""returns [value,isFinalValue]"""
|
"""returns [value,isFinalValue]"""
|
||||||
@@ -264,7 +326,7 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
yield prev, True
|
yield prev, True
|
||||||
|
|
||||||
def _parse_config_file(self, fd, user, mread, mwrite, mflags, mount):
|
def _parse_config_file(self, fd, user, mread, mwrite, madm, mflags, mount):
|
||||||
vol_src = None
|
vol_src = None
|
||||||
vol_dst = None
|
vol_dst = None
|
||||||
self.line_ctr = 0
|
self.line_ctr = 0
|
||||||
@@ -296,6 +358,7 @@ class AuthSrv(object):
|
|||||||
mount[vol_dst] = vol_src
|
mount[vol_dst] = vol_src
|
||||||
mread[vol_dst] = []
|
mread[vol_dst] = []
|
||||||
mwrite[vol_dst] = []
|
mwrite[vol_dst] = []
|
||||||
|
madm[vol_dst] = []
|
||||||
mflags[vol_dst] = {}
|
mflags[vol_dst] = {}
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -306,10 +369,15 @@ class AuthSrv(object):
|
|||||||
uname = "*"
|
uname = "*"
|
||||||
|
|
||||||
self._read_vol_str(
|
self._read_vol_str(
|
||||||
lvl, uname, mread[vol_dst], mwrite[vol_dst], mflags[vol_dst]
|
lvl,
|
||||||
|
uname,
|
||||||
|
mread[vol_dst],
|
||||||
|
mwrite[vol_dst],
|
||||||
|
madm[vol_dst],
|
||||||
|
mflags[vol_dst],
|
||||||
)
|
)
|
||||||
|
|
||||||
def _read_vol_str(self, lvl, uname, mr, mw, mf):
|
def _read_vol_str(self, lvl, uname, mr, mw, ma, mf):
|
||||||
if lvl == "c":
|
if lvl == "c":
|
||||||
cval = True
|
cval = True
|
||||||
if "=" in uname:
|
if "=" in uname:
|
||||||
@@ -327,6 +395,9 @@ class AuthSrv(object):
|
|||||||
if lvl in "wa":
|
if lvl in "wa":
|
||||||
mw.append(uname)
|
mw.append(uname)
|
||||||
|
|
||||||
|
if lvl == "a":
|
||||||
|
ma.append(uname)
|
||||||
|
|
||||||
def _read_volflag(self, flags, name, value, is_list):
|
def _read_volflag(self, flags, name, value, is_list):
|
||||||
if name not in ["mtp"]:
|
if name not in ["mtp"]:
|
||||||
flags[name] = value
|
flags[name] = value
|
||||||
@@ -350,6 +421,7 @@ class AuthSrv(object):
|
|||||||
user = {} # username:password
|
user = {} # username:password
|
||||||
mread = {} # mountpoint:[username]
|
mread = {} # mountpoint:[username]
|
||||||
mwrite = {} # mountpoint:[username]
|
mwrite = {} # mountpoint:[username]
|
||||||
|
madm = {} # mountpoint:[username]
|
||||||
mflags = {} # mountpoint:[flag]
|
mflags = {} # mountpoint:[flag]
|
||||||
mount = {} # dst:src (mountpoint:realpath)
|
mount = {} # dst:src (mountpoint:realpath)
|
||||||
|
|
||||||
@@ -367,34 +439,53 @@ class AuthSrv(object):
|
|||||||
raise Exception("invalid -v argument: [{}]".format(v_str))
|
raise Exception("invalid -v argument: [{}]".format(v_str))
|
||||||
|
|
||||||
src, dst, perms = m.groups()
|
src, dst, perms = m.groups()
|
||||||
|
if WINDOWS:
|
||||||
|
src = uncyg(src)
|
||||||
|
|
||||||
# print("\n".join([src, dst, perms]))
|
# print("\n".join([src, dst, perms]))
|
||||||
src = fsdec(os.path.abspath(fsenc(src)))
|
src = fsdec(os.path.abspath(fsenc(src)))
|
||||||
dst = dst.strip("/")
|
dst = dst.strip("/")
|
||||||
mount[dst] = src
|
mount[dst] = src
|
||||||
mread[dst] = []
|
mread[dst] = []
|
||||||
mwrite[dst] = []
|
mwrite[dst] = []
|
||||||
|
madm[dst] = []
|
||||||
mflags[dst] = {}
|
mflags[dst] = {}
|
||||||
|
|
||||||
perms = perms.split(":")
|
perms = perms.split(":")
|
||||||
for (lvl, uname) in [[x[0], x[1:]] for x in perms]:
|
for (lvl, uname) in [[x[0], x[1:]] for x in perms]:
|
||||||
self._read_vol_str(lvl, uname, mread[dst], mwrite[dst], mflags[dst])
|
self._read_vol_str(
|
||||||
|
lvl, uname, mread[dst], mwrite[dst], madm[dst], mflags[dst]
|
||||||
|
)
|
||||||
|
|
||||||
if self.args.c:
|
if self.args.c:
|
||||||
for cfg_fn in self.args.c:
|
for cfg_fn in self.args.c:
|
||||||
with open(cfg_fn, "rb") as f:
|
with open(cfg_fn, "rb") as f:
|
||||||
try:
|
try:
|
||||||
self._parse_config_file(f, user, mread, mwrite, mflags, mount)
|
self._parse_config_file(
|
||||||
|
f, user, mread, mwrite, madm, mflags, mount
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
m = "\n\033[1;31m\nerror in config file {} on line {}:\n\033[0m"
|
m = "\n\033[1;31m\nerror in config file {} on line {}:\n\033[0m"
|
||||||
print(m.format(cfg_fn, self.line_ctr))
|
print(m.format(cfg_fn, self.line_ctr))
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
# case-insensitive; normalize
|
||||||
|
if WINDOWS:
|
||||||
|
cased = {}
|
||||||
|
for k, v in mount.items():
|
||||||
|
try:
|
||||||
|
cased[k] = fsdec(os.path.realpath(fsenc(v)))
|
||||||
|
except:
|
||||||
|
cased[k] = v
|
||||||
|
|
||||||
|
mount = cased
|
||||||
|
|
||||||
if not mount:
|
if not mount:
|
||||||
# -h says our defaults are CWD at root and read/write for everyone
|
# -h says our defaults are CWD at root and read/write for everyone
|
||||||
vfs = VFS(os.path.abspath("."), "", ["*"], ["*"])
|
vfs = VFS(os.path.abspath("."), "", ["*"], ["*"])
|
||||||
elif "" not in mount:
|
elif "" not in mount:
|
||||||
# there's volumes but no root; make root inaccessible
|
# there's volumes but no root; make root inaccessible
|
||||||
vfs = VFS(os.path.abspath("."), "")
|
vfs = VFS(None, "")
|
||||||
vfs.flags["d2d"] = True
|
vfs.flags["d2d"] = True
|
||||||
|
|
||||||
maxdepth = 0
|
maxdepth = 0
|
||||||
@@ -405,13 +496,20 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
if dst == "":
|
if dst == "":
|
||||||
# rootfs was mapped; fully replaces the default CWD vfs
|
# rootfs was mapped; fully replaces the default CWD vfs
|
||||||
vfs = VFS(mount[dst], dst, mread[dst], mwrite[dst], mflags[dst])
|
vfs = VFS(
|
||||||
|
mount[dst], dst, mread[dst], mwrite[dst], madm[dst], mflags[dst]
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
v = vfs.add(mount[dst], dst)
|
v = vfs.add(mount[dst], dst)
|
||||||
v.uread = mread[dst]
|
v.uread = mread[dst]
|
||||||
v.uwrite = mwrite[dst]
|
v.uwrite = mwrite[dst]
|
||||||
|
v.uadm = madm[dst]
|
||||||
v.flags = mflags[dst]
|
v.flags = mflags[dst]
|
||||||
|
v.dbv = None
|
||||||
|
|
||||||
|
vfs.all_vols = {}
|
||||||
|
vfs.get_all_vols(vfs.all_vols)
|
||||||
|
|
||||||
missing_users = {}
|
missing_users = {}
|
||||||
for d in [mread, mwrite]:
|
for d in [mread, mwrite]:
|
||||||
@@ -428,6 +526,67 @@ class AuthSrv(object):
|
|||||||
)
|
)
|
||||||
raise Exception("invalid config")
|
raise Exception("invalid config")
|
||||||
|
|
||||||
|
promote = []
|
||||||
|
demote = []
|
||||||
|
for vol in vfs.all_vols.values():
|
||||||
|
hid = hashlib.sha512(fsenc(vol.realpath)).digest()
|
||||||
|
hid = base64.b32encode(hid).decode("ascii").lower()
|
||||||
|
vflag = vol.flags.get("hist")
|
||||||
|
if vflag == "-":
|
||||||
|
pass
|
||||||
|
elif vflag:
|
||||||
|
vol.histpath = uncyg(vflag) if WINDOWS else vflag
|
||||||
|
elif self.args.hist:
|
||||||
|
for nch in range(len(hid)):
|
||||||
|
hpath = os.path.join(self.args.hist, hid[: nch + 1])
|
||||||
|
try:
|
||||||
|
os.makedirs(hpath)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
powner = os.path.join(hpath, "owner.txt")
|
||||||
|
try:
|
||||||
|
with open(powner, "rb") as f:
|
||||||
|
owner = f.read().rstrip()
|
||||||
|
except:
|
||||||
|
owner = None
|
||||||
|
|
||||||
|
me = fsenc(vol.realpath).rstrip()
|
||||||
|
if owner not in [None, me]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if owner is None:
|
||||||
|
with open(powner, "wb") as f:
|
||||||
|
f.write(me)
|
||||||
|
|
||||||
|
vol.histpath = hpath
|
||||||
|
break
|
||||||
|
|
||||||
|
vol.histpath = os.path.realpath(vol.histpath)
|
||||||
|
if vol.dbv:
|
||||||
|
if os.path.exists(os.path.join(vol.histpath, "up2k.db")):
|
||||||
|
promote.append(vol)
|
||||||
|
vol.dbv = None
|
||||||
|
else:
|
||||||
|
demote.append(vol)
|
||||||
|
|
||||||
|
# discard jump-vols
|
||||||
|
for v in demote:
|
||||||
|
vfs.all_vols.pop(v.vpath)
|
||||||
|
|
||||||
|
if promote:
|
||||||
|
msg = [
|
||||||
|
"\n the following jump-volumes were generated to assist the vfs.\n As they contain a database (probably from v0.11.11 or older),\n they are promoted to full volumes:"
|
||||||
|
]
|
||||||
|
for vol in promote:
|
||||||
|
msg.append(
|
||||||
|
" /{} ({}) ({})".format(vol.vpath, vol.realpath, vol.histpath)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.log("\n\n".join(msg) + "\n", c=3)
|
||||||
|
|
||||||
|
vfs.histtab = {v.realpath: v.histpath for v in vfs.all_vols.values()}
|
||||||
|
|
||||||
all_mte = {}
|
all_mte = {}
|
||||||
errors = False
|
errors = False
|
||||||
for vol in vfs.all_vols.values():
|
for vol in vfs.all_vols.values():
|
||||||
@@ -437,6 +596,10 @@ class AuthSrv(object):
|
|||||||
if self.args.e2d or "e2ds" in vol.flags:
|
if self.args.e2d or "e2ds" in vol.flags:
|
||||||
vol.flags["e2d"] = True
|
vol.flags["e2d"] = True
|
||||||
|
|
||||||
|
if self.args.no_hash:
|
||||||
|
if "ehash" not in vol.flags:
|
||||||
|
vol.flags["dhash"] = True
|
||||||
|
|
||||||
for k in ["e2t", "e2ts", "e2tsr"]:
|
for k in ["e2t", "e2ts", "e2tsr"]:
|
||||||
if getattr(self.args, k):
|
if getattr(self.args, k):
|
||||||
vol.flags[k] = True
|
vol.flags[k] = True
|
||||||
@@ -470,8 +633,10 @@ class AuthSrv(object):
|
|||||||
# verify tags mentioned by -mt[mp] are used by -mte
|
# verify tags mentioned by -mt[mp] are used by -mte
|
||||||
local_mtp = {}
|
local_mtp = {}
|
||||||
local_only_mtp = {}
|
local_only_mtp = {}
|
||||||
for a in vol.flags.get("mtp", []) + vol.flags.get("mtm", []):
|
tags = vol.flags.get("mtp", []) + vol.flags.get("mtm", [])
|
||||||
a = a.split("=")[0]
|
tags = [x.split("=")[0] for x in tags]
|
||||||
|
tags = [y for x in tags for y in x.split(",")]
|
||||||
|
for a in tags:
|
||||||
local_mtp[a] = True
|
local_mtp[a] = True
|
||||||
local = True
|
local = True
|
||||||
for b in self.args.mtp or []:
|
for b in self.args.mtp or []:
|
||||||
@@ -500,8 +665,10 @@ class AuthSrv(object):
|
|||||||
self.log(m.format(vol.vpath, mtp), 1)
|
self.log(m.format(vol.vpath, mtp), 1)
|
||||||
errors = True
|
errors = True
|
||||||
|
|
||||||
for mtp in self.args.mtp or []:
|
tags = self.args.mtp or []
|
||||||
mtp = mtp.split("=")[0]
|
tags = [x.split("=")[0] for x in tags]
|
||||||
|
tags = [y for x in tags for y in x.split(",")]
|
||||||
|
for mtp in tags:
|
||||||
if mtp not in all_mte:
|
if mtp not in all_mte:
|
||||||
m = 'metadata tag "{}" is defined by "-mtm" or "-mtp", but is not used by "-mte" (or by any "cmte" volume-flag)'
|
m = 'metadata tag "{}" is defined by "-mtm" or "-mtp", but is not used by "-mte" (or by any "cmte" volume-flag)'
|
||||||
self.log(m.format(mtp), 1)
|
self.log(m.format(mtp), 1)
|
||||||
@@ -510,6 +677,8 @@ class AuthSrv(object):
|
|||||||
if errors:
|
if errors:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
vfs.bubble_flags()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
v, _ = vfs.get("/", "*", False, True)
|
v, _ = vfs.get("/", "*", False, True)
|
||||||
if self.warn_anonwrite and os.getcwd() == v.realpath:
|
if self.warn_anonwrite and os.getcwd() == v.realpath:
|
||||||
@@ -524,5 +693,97 @@ class AuthSrv(object):
|
|||||||
self.user = user
|
self.user = user
|
||||||
self.iuser = {v: k for k, v in user.items()}
|
self.iuser = {v: k for k, v in user.items()}
|
||||||
|
|
||||||
|
self.re_pwd = None
|
||||||
|
pwds = [re.escape(x) for x in self.iuser.keys()]
|
||||||
|
if pwds:
|
||||||
|
self.re_pwd = re.compile("=(" + "|".join(pwds) + ")([]&; ]|$)")
|
||||||
|
|
||||||
# import pprint
|
# import pprint
|
||||||
# pprint.pprint({"usr": user, "rd": mread, "wr": mwrite, "mnt": mount})
|
# pprint.pprint({"usr": user, "rd": mread, "wr": mwrite, "mnt": mount})
|
||||||
|
|
||||||
|
def dbg_ls(self):
|
||||||
|
users = self.args.ls
|
||||||
|
vols = "*"
|
||||||
|
flags = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
users, vols = users.split(",", 1)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
vols, flags = vols.split(",", 1)
|
||||||
|
flags = flags.split(",")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if users == "**":
|
||||||
|
users = list(self.user.keys()) + ["*"]
|
||||||
|
else:
|
||||||
|
users = [users]
|
||||||
|
|
||||||
|
for u in users:
|
||||||
|
if u not in self.user and u != "*":
|
||||||
|
raise Exception("user not found: " + u)
|
||||||
|
|
||||||
|
if vols == "*":
|
||||||
|
vols = ["/" + x for x in self.vfs.all_vols.keys()]
|
||||||
|
else:
|
||||||
|
vols = [vols]
|
||||||
|
|
||||||
|
for v in vols:
|
||||||
|
if not v.startswith("/"):
|
||||||
|
raise Exception("volumes must start with /")
|
||||||
|
|
||||||
|
if v[1:] not in self.vfs.all_vols:
|
||||||
|
raise Exception("volume not found: " + v)
|
||||||
|
|
||||||
|
self.log({"users": users, "vols": vols, "flags": flags})
|
||||||
|
for k, v in self.vfs.all_vols.items():
|
||||||
|
self.log("/{}: read({}) write({})".format(k, v.uread, v.uwrite))
|
||||||
|
|
||||||
|
flag_v = "v" in flags
|
||||||
|
flag_ln = "ln" in flags
|
||||||
|
flag_p = "p" in flags
|
||||||
|
flag_r = "r" in flags
|
||||||
|
|
||||||
|
n_bads = 0
|
||||||
|
for v in vols:
|
||||||
|
v = v[1:]
|
||||||
|
vtop = "/{}/".format(v) if v else "/"
|
||||||
|
for u in users:
|
||||||
|
self.log("checking /{} as {}".format(v, u))
|
||||||
|
try:
|
||||||
|
vn, _ = self.vfs.get(v, u, True, False)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
atop = vn.realpath
|
||||||
|
g = vn.walk("", "", [], u, True, not self.args.no_scandir, False)
|
||||||
|
for vpath, apath, files, _, _ in g:
|
||||||
|
fnames = [n[0] for n in files]
|
||||||
|
vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames
|
||||||
|
vpaths = [vtop + x for x in vpaths]
|
||||||
|
apaths = [os.path.join(apath, n) for n in fnames]
|
||||||
|
files = [[vpath + "/", apath + os.sep]] + list(zip(vpaths, apaths))
|
||||||
|
|
||||||
|
if flag_ln:
|
||||||
|
files = [x for x in files if not x[1].startswith(atop + os.sep)]
|
||||||
|
n_bads += len(files)
|
||||||
|
|
||||||
|
if flag_v:
|
||||||
|
msg = [
|
||||||
|
'# user "{}", vpath "{}"\n{}'.format(u, vp, ap)
|
||||||
|
for vp, ap in files
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
msg = [x[1] for x in files]
|
||||||
|
|
||||||
|
if msg:
|
||||||
|
nuprint("\n".join(msg))
|
||||||
|
|
||||||
|
if n_bads and flag_p:
|
||||||
|
raise Exception("found symlink leaving volume, and strict is set")
|
||||||
|
|
||||||
|
if not flag_r:
|
||||||
|
sys.exit(0)
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ class BrokerMp(object):
|
|||||||
proc.clients = {}
|
proc.clients = {}
|
||||||
proc.workload = 0
|
proc.workload = 0
|
||||||
|
|
||||||
thr = threading.Thread(target=self.collector, args=(proc,))
|
thr = threading.Thread(
|
||||||
|
target=self.collector, args=(proc,), name="mp-collector"
|
||||||
|
)
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
@@ -52,14 +54,19 @@ class BrokerMp(object):
|
|||||||
proc.start()
|
proc.start()
|
||||||
|
|
||||||
if not self.args.q:
|
if not self.args.q:
|
||||||
thr = threading.Thread(target=self.debug_load_balancer)
|
thr = threading.Thread(
|
||||||
|
target=self.debug_load_balancer, name="mp-dbg-loadbalancer"
|
||||||
|
)
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.log("broker", "shutting down")
|
self.log("broker", "shutting down")
|
||||||
for proc in self.procs:
|
for n, proc in enumerate(self.procs):
|
||||||
thr = threading.Thread(target=proc.q_pend.put([0, "shutdown", []]))
|
thr = threading.Thread(
|
||||||
|
target=proc.q_pend.put([0, "shutdown", []]),
|
||||||
|
name="mp-shutdown-{}-{}".format(n, len(self.procs)),
|
||||||
|
)
|
||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
from copyparty.authsrv import AuthSrv
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -27,20 +28,23 @@ class MpWorker(object):
|
|||||||
self.retpend = {}
|
self.retpend = {}
|
||||||
self.retpend_mutex = threading.Lock()
|
self.retpend_mutex = threading.Lock()
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
self.workload_thr_active = False
|
self.workload_thr_alive = False
|
||||||
|
|
||||||
# we inherited signal_handler from parent,
|
# we inherited signal_handler from parent,
|
||||||
# replace it with something harmless
|
# replace it with something harmless
|
||||||
if not FAKE_MP:
|
if not FAKE_MP:
|
||||||
signal.signal(signal.SIGINT, self.signal_handler)
|
signal.signal(signal.SIGINT, self.signal_handler)
|
||||||
|
|
||||||
|
# starting to look like a good idea
|
||||||
|
self.asrv = AuthSrv(args, None, False)
|
||||||
|
|
||||||
# instantiate all services here (TODO: inheritance?)
|
# instantiate all services here (TODO: inheritance?)
|
||||||
self.httpsrv = HttpSrv(self)
|
self.httpsrv = HttpSrv(self, True)
|
||||||
self.httpsrv.disconnect_func = self.httpdrop
|
self.httpsrv.disconnect_func = self.httpdrop
|
||||||
|
|
||||||
# on winxp and some other platforms,
|
# on winxp and some other platforms,
|
||||||
# use thr.join() to block all signals
|
# use thr.join() to block all signals
|
||||||
thr = threading.Thread(target=self.main)
|
thr = threading.Thread(target=self.main, name="mpw-main")
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
thr.join()
|
thr.join()
|
||||||
@@ -64,6 +68,7 @@ class MpWorker(object):
|
|||||||
|
|
||||||
# self.logw("work: [{}]".format(d[0]))
|
# self.logw("work: [{}]".format(d[0]))
|
||||||
if dest == "shutdown":
|
if dest == "shutdown":
|
||||||
|
self.httpsrv.shutdown()
|
||||||
self.logw("ok bye")
|
self.logw("ok bye")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
return
|
return
|
||||||
@@ -75,13 +80,15 @@ class MpWorker(object):
|
|||||||
|
|
||||||
if self.args.log_conn:
|
if self.args.log_conn:
|
||||||
self.log("%s %s" % addr, "|%sC-qpop" % ("-" * 4,), c="1;30")
|
self.log("%s %s" % addr, "|%sC-qpop" % ("-" * 4,), c="1;30")
|
||||||
|
|
||||||
self.httpsrv.accept(sck, addr)
|
self.httpsrv.accept(sck, addr)
|
||||||
|
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
if not self.workload_thr_active:
|
if not self.workload_thr_alive:
|
||||||
self.workload_thr_alive = True
|
self.workload_thr_alive = True
|
||||||
thr = threading.Thread(target=self.thr_workload)
|
thr = threading.Thread(
|
||||||
|
target=self.thr_workload, name="mpw-workload"
|
||||||
|
)
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import print_function, unicode_literals
|
|||||||
|
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
from .authsrv import AuthSrv
|
||||||
from .httpsrv import HttpSrv
|
from .httpsrv import HttpSrv
|
||||||
from .broker_util import ExceptionalQueue, try_exec
|
from .broker_util import ExceptionalQueue, try_exec
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ class BrokerThr(object):
|
|||||||
self.hub = hub
|
self.hub = hub
|
||||||
self.log = hub.log
|
self.log = hub.log
|
||||||
self.args = hub.args
|
self.args = hub.args
|
||||||
|
self.asrv = hub.asrv
|
||||||
|
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
|
|
||||||
@@ -23,6 +25,7 @@ class BrokerThr(object):
|
|||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
# self.log("broker", "shutting down")
|
# self.log("broker", "shutting down")
|
||||||
|
self.httpsrv.shutdown()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def put(self, want_retval, dest, *args):
|
def put(self, want_retval, dest, *args):
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
@@ -16,6 +16,9 @@ from .__init__ import E
|
|||||||
from .util import Unrecv
|
from .util import Unrecv
|
||||||
from .httpcli import HttpCli
|
from .httpcli import HttpCli
|
||||||
from .u2idx import U2idx
|
from .u2idx import U2idx
|
||||||
|
from .th_cli import ThumbCli
|
||||||
|
from .th_srv import HAVE_PIL
|
||||||
|
from .ico import Ico
|
||||||
|
|
||||||
|
|
||||||
class HttpConn(object):
|
class HttpConn(object):
|
||||||
@@ -30,16 +33,32 @@ class HttpConn(object):
|
|||||||
self.hsrv = hsrv
|
self.hsrv = hsrv
|
||||||
|
|
||||||
self.args = hsrv.args
|
self.args = hsrv.args
|
||||||
self.auth = hsrv.auth
|
self.asrv = hsrv.asrv
|
||||||
|
self.is_mp = hsrv.is_mp
|
||||||
self.cert_path = hsrv.cert_path
|
self.cert_path = hsrv.cert_path
|
||||||
|
|
||||||
|
enth = HAVE_PIL and not self.args.no_thumb
|
||||||
|
self.thumbcli = ThumbCli(hsrv.broker) if enth else None
|
||||||
|
self.ico = Ico(self.args)
|
||||||
|
|
||||||
self.t0 = time.time()
|
self.t0 = time.time()
|
||||||
|
self.stopping = False
|
||||||
|
self.nreq = 0
|
||||||
self.nbyte = 0
|
self.nbyte = 0
|
||||||
self.workload = 0
|
self.workload = 0
|
||||||
self.u2idx = None
|
self.u2idx = None
|
||||||
self.log_func = hsrv.log
|
self.log_func = hsrv.log
|
||||||
|
self.lf_url = re.compile(self.args.lf_url) if self.args.lf_url else None
|
||||||
self.set_rproxy()
|
self.set_rproxy()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
self.stopping = True
|
||||||
|
try:
|
||||||
|
self.s.shutdown(socket.SHUT_RDWR)
|
||||||
|
self.s.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
def set_rproxy(self, ip=None):
|
def set_rproxy(self, ip=None):
|
||||||
if ip is None:
|
if ip is None:
|
||||||
color = 36
|
color = 36
|
||||||
@@ -61,7 +80,7 @@ class HttpConn(object):
|
|||||||
|
|
||||||
def get_u2idx(self):
|
def get_u2idx(self):
|
||||||
if not self.u2idx:
|
if not self.u2idx:
|
||||||
self.u2idx = U2idx(self.args, self.log_func)
|
self.u2idx = U2idx(self)
|
||||||
|
|
||||||
return self.u2idx
|
return self.u2idx
|
||||||
|
|
||||||
@@ -153,7 +172,7 @@ class HttpConn(object):
|
|||||||
self.log("client rejected our certificate (nice)")
|
self.log("client rejected our certificate (nice)")
|
||||||
|
|
||||||
elif "ALERT_CERTIFICATE_UNKNOWN" in em:
|
elif "ALERT_CERTIFICATE_UNKNOWN" in em:
|
||||||
# chrome-android keeps doing this
|
# android-chrome keeps doing this
|
||||||
pass
|
pass
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -164,7 +183,13 @@ class HttpConn(object):
|
|||||||
if not self.sr:
|
if not self.sr:
|
||||||
self.sr = Unrecv(self.s)
|
self.sr = Unrecv(self.s)
|
||||||
|
|
||||||
while True:
|
while not self.stopping:
|
||||||
|
if self.is_mp:
|
||||||
|
self.workload += 50
|
||||||
|
if self.workload >= 2 ** 31:
|
||||||
|
self.workload = 100
|
||||||
|
|
||||||
|
self.nreq += 1
|
||||||
cli = HttpCli(self)
|
cli = HttpCli(self)
|
||||||
if not cli.run():
|
if not cli.run():
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from __future__ import print_function, unicode_literals
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import base64
|
||||||
|
import struct
|
||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
@@ -26,7 +28,6 @@ except ImportError:
|
|||||||
|
|
||||||
from .__init__ import E, MACOS
|
from .__init__ import E, MACOS
|
||||||
from .httpconn import HttpConn
|
from .httpconn import HttpConn
|
||||||
from .authsrv import AuthSrv
|
|
||||||
|
|
||||||
|
|
||||||
class HttpSrv(object):
|
class HttpSrv(object):
|
||||||
@@ -35,10 +36,12 @@ class HttpSrv(object):
|
|||||||
relying on MpSrv for performance (HttpSrv is just plain threads)
|
relying on MpSrv for performance (HttpSrv is just plain threads)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, broker):
|
def __init__(self, broker, is_mp=False):
|
||||||
self.broker = broker
|
self.broker = broker
|
||||||
|
self.is_mp = is_mp
|
||||||
self.args = broker.args
|
self.args = broker.args
|
||||||
self.log = broker.log
|
self.log = broker.log
|
||||||
|
self.asrv = broker.asrv
|
||||||
|
|
||||||
self.disconnect_func = None
|
self.disconnect_func = None
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
@@ -46,7 +49,8 @@ class HttpSrv(object):
|
|||||||
self.clients = {}
|
self.clients = {}
|
||||||
self.workload = 0
|
self.workload = 0
|
||||||
self.workload_thr_alive = False
|
self.workload_thr_alive = False
|
||||||
self.auth = AuthSrv(self.args, self.log)
|
self.cb_ts = 0
|
||||||
|
self.cb_v = 0
|
||||||
|
|
||||||
env = jinja2.Environment()
|
env = jinja2.Environment()
|
||||||
env.loader = jinja2.FileSystemLoader(os.path.join(E.mod, "web"))
|
env.loader = jinja2.FileSystemLoader(os.path.join(E.mod, "web"))
|
||||||
@@ -66,7 +70,11 @@ class HttpSrv(object):
|
|||||||
if self.args.log_conn:
|
if self.args.log_conn:
|
||||||
self.log("%s %s" % addr, "|%sC-cthr" % ("-" * 5,), c="1;30")
|
self.log("%s %s" % addr, "|%sC-cthr" % ("-" * 5,), c="1;30")
|
||||||
|
|
||||||
thr = threading.Thread(target=self.thr_client, args=(sck, addr))
|
thr = threading.Thread(
|
||||||
|
target=self.thr_client,
|
||||||
|
args=(sck, addr),
|
||||||
|
name="httpsrv-{}-{}".format(addr[0].split(".", 2)[-1][-6:], addr[1]),
|
||||||
|
)
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
@@ -75,7 +83,14 @@ class HttpSrv(object):
|
|||||||
return len(self.clients)
|
return len(self.clients)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.log("ok bye")
|
clients = list(self.clients.keys())
|
||||||
|
for cli in clients:
|
||||||
|
try:
|
||||||
|
cli.shutdown()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.log("httpsrv-n", "ok bye")
|
||||||
|
|
||||||
def thr_client(self, sck, addr):
|
def thr_client(self, sck, addr):
|
||||||
"""thread managing one tcp client"""
|
"""thread managing one tcp client"""
|
||||||
@@ -84,39 +99,54 @@ class HttpSrv(object):
|
|||||||
cli = HttpConn(sck, addr, self)
|
cli = HttpConn(sck, addr, self)
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
self.clients[cli] = 0
|
self.clients[cli] = 0
|
||||||
self.workload += 50
|
|
||||||
|
|
||||||
if not self.workload_thr_alive:
|
if self.is_mp:
|
||||||
self.workload_thr_alive = True
|
self.workload += 50
|
||||||
thr = threading.Thread(target=self.thr_workload)
|
if not self.workload_thr_alive:
|
||||||
thr.daemon = True
|
self.workload_thr_alive = True
|
||||||
thr.start()
|
thr = threading.Thread(
|
||||||
|
target=self.thr_workload, name="httpsrv-workload"
|
||||||
|
)
|
||||||
|
thr.daemon = True
|
||||||
|
thr.start()
|
||||||
|
|
||||||
|
fno = sck.fileno()
|
||||||
try:
|
try:
|
||||||
if self.args.log_conn:
|
if self.args.log_conn:
|
||||||
self.log("%s %s" % addr, "|%sC-crun" % ("-" * 6,), c="1;30")
|
self.log("%s %s" % addr, "|%sC-crun" % ("-" * 6,), c="1;30")
|
||||||
|
|
||||||
cli.run()
|
cli.run()
|
||||||
|
|
||||||
|
except (OSError, socket.error) as ex:
|
||||||
|
if ex.errno not in [10038, 10054, 107, 57, 9]:
|
||||||
|
self.log(
|
||||||
|
"%s %s" % addr,
|
||||||
|
"run({}): {}".format(fno, ex),
|
||||||
|
c=6,
|
||||||
|
)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
sck = cli.s
|
||||||
if self.args.log_conn:
|
if self.args.log_conn:
|
||||||
self.log("%s %s" % addr, "|%sC-cdone" % ("-" * 7,), c="1;30")
|
self.log("%s %s" % addr, "|%sC-cdone" % ("-" * 7,), c="1;30")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
fno = sck.fileno()
|
||||||
sck.shutdown(socket.SHUT_RDWR)
|
sck.shutdown(socket.SHUT_RDWR)
|
||||||
sck.close()
|
sck.close()
|
||||||
except (OSError, socket.error) as ex:
|
except (OSError, socket.error) as ex:
|
||||||
if not MACOS:
|
if not MACOS:
|
||||||
self.log(
|
self.log(
|
||||||
"%s %s" % addr,
|
"%s %s" % addr,
|
||||||
"shut({}): {}".format(sck.fileno(), ex),
|
"shut({}): {}".format(fno, ex),
|
||||||
c="1;30",
|
c="1;30",
|
||||||
)
|
)
|
||||||
if ex.errno not in [10038, 10054, 107, 57, 9]:
|
if ex.errno not in [10038, 10054, 107, 57, 49, 9]:
|
||||||
# 10038 No longer considered a socket
|
# 10038 No longer considered a socket
|
||||||
# 10054 Foribly closed by remote
|
# 10054 Foribly closed by remote
|
||||||
# 107 Transport endpoint not connected
|
# 107 Transport endpoint not connected
|
||||||
# 57 Socket is not connected
|
# 57 Socket is not connected
|
||||||
|
# 49 Can't assign requested address (wifi down)
|
||||||
# 9 Bad file descriptor
|
# 9 Bad file descriptor
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
@@ -151,3 +181,25 @@ class HttpSrv(object):
|
|||||||
self.clients[cli] = now
|
self.clients[cli] = now
|
||||||
|
|
||||||
self.workload = total
|
self.workload = total
|
||||||
|
|
||||||
|
def cachebuster(self):
|
||||||
|
if time.time() - self.cb_ts < 1:
|
||||||
|
return self.cb_v
|
||||||
|
|
||||||
|
with self.mutex:
|
||||||
|
if time.time() - self.cb_ts < 1:
|
||||||
|
return self.cb_v
|
||||||
|
|
||||||
|
v = E.t0
|
||||||
|
try:
|
||||||
|
with os.scandir(os.path.join(E.mod, "web")) as dh:
|
||||||
|
for fh in dh:
|
||||||
|
inf = fh.stat(follow_symlinks=False)
|
||||||
|
v = max(v, inf.st_mtime)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
v = base64.urlsafe_b64encode(struct.pack(">xxL", int(v)))
|
||||||
|
self.cb_v = v.decode("ascii")[-4:]
|
||||||
|
self.cb_ts = time.time()
|
||||||
|
return self.cb_v
|
||||||
|
|||||||
42
copyparty/ico.py
Normal file
42
copyparty/ico.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import colorsys
|
||||||
|
|
||||||
|
from .__init__ import PY2
|
||||||
|
|
||||||
|
|
||||||
|
class Ico(object):
|
||||||
|
def __init__(self, args):
|
||||||
|
self.args = args
|
||||||
|
|
||||||
|
def get(self, ext, as_thumb):
|
||||||
|
"""placeholder to make thumbnails not break"""
|
||||||
|
|
||||||
|
h = hashlib.md5(ext.encode("utf-8")).digest()[:2]
|
||||||
|
if PY2:
|
||||||
|
h = [ord(x) for x in h]
|
||||||
|
|
||||||
|
c1 = colorsys.hsv_to_rgb(h[0] / 256.0, 1, 0.3)
|
||||||
|
c2 = colorsys.hsv_to_rgb(h[0] / 256.0, 1, 1)
|
||||||
|
c = list(c1) + list(c2)
|
||||||
|
c = [int(x * 255) for x in c]
|
||||||
|
c = "".join(["{:02x}".format(x) for x in c])
|
||||||
|
|
||||||
|
h = 30
|
||||||
|
if not self.args.th_no_crop and as_thumb:
|
||||||
|
w, h = self.args.th_size.split("x")
|
||||||
|
h = int(100 / (float(w) / float(h)))
|
||||||
|
|
||||||
|
svg = """\
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg version="1.1" viewBox="0 0 100 {}" xmlns="http://www.w3.org/2000/svg"><g>
|
||||||
|
<rect width="100%" height="100%" fill="#{}" />
|
||||||
|
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" xml:space="preserve"
|
||||||
|
fill="#{}" font-family="monospace" font-size="14px" style="letter-spacing:.5px">{}</text>
|
||||||
|
</g></svg>
|
||||||
|
"""
|
||||||
|
svg = svg.format(h, c[:6], c[6:], ext).encode("utf-8")
|
||||||
|
|
||||||
|
return ["image/svg+xml", svg]
|
||||||
@@ -1,19 +1,232 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import re
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
|
|
||||||
from .__init__ import PY2, WINDOWS
|
from .__init__ import PY2, WINDOWS
|
||||||
from .util import fsenc, fsdec, REKOBO_LKEY
|
from .util import fsenc, fsdec, uncyg, REKOBO_LKEY
|
||||||
|
|
||||||
if not PY2:
|
if not PY2:
|
||||||
unicode = str
|
unicode = str
|
||||||
|
|
||||||
|
|
||||||
|
def have_ff(cmd):
|
||||||
|
if PY2:
|
||||||
|
print("# checking {}".format(cmd))
|
||||||
|
cmd = (cmd + " -version").encode("ascii").split(b" ")
|
||||||
|
try:
|
||||||
|
sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE).communicate()
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return bool(shutil.which(cmd))
|
||||||
|
|
||||||
|
|
||||||
|
HAVE_FFMPEG = have_ff("ffmpeg")
|
||||||
|
HAVE_FFPROBE = have_ff("ffprobe")
|
||||||
|
|
||||||
|
|
||||||
|
class MParser(object):
|
||||||
|
def __init__(self, cmdline):
|
||||||
|
self.tag, args = cmdline.split("=", 1)
|
||||||
|
self.tags = self.tag.split(",")
|
||||||
|
|
||||||
|
self.timeout = 30
|
||||||
|
self.force = False
|
||||||
|
self.audio = "y"
|
||||||
|
self.ext = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
bp = os.path.expanduser(args)
|
||||||
|
if WINDOWS:
|
||||||
|
bp = uncyg(bp)
|
||||||
|
|
||||||
|
if os.path.exists(bp):
|
||||||
|
self.bin = bp
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
arg, args = args.split(",", 1)
|
||||||
|
arg = arg.lower()
|
||||||
|
|
||||||
|
if arg.startswith("a"):
|
||||||
|
self.audio = arg[1:] # [r]equire [n]ot [d]ontcare
|
||||||
|
continue
|
||||||
|
|
||||||
|
if arg == "f":
|
||||||
|
self.force = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if arg.startswith("t"):
|
||||||
|
self.timeout = int(arg[1:])
|
||||||
|
continue
|
||||||
|
|
||||||
|
if arg.startswith("e"):
|
||||||
|
self.ext.append(arg[1:])
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
|
||||||
|
def ffprobe(abspath):
|
||||||
|
cmd = [
|
||||||
|
b"ffprobe",
|
||||||
|
b"-hide_banner",
|
||||||
|
b"-show_streams",
|
||||||
|
b"-show_format",
|
||||||
|
b"--",
|
||||||
|
fsenc(abspath),
|
||||||
|
]
|
||||||
|
p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE)
|
||||||
|
r = p.communicate()
|
||||||
|
txt = r[0].decode("utf-8", "replace")
|
||||||
|
return parse_ffprobe(txt)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ffprobe(txt):
|
||||||
|
"""ffprobe -show_format -show_streams"""
|
||||||
|
streams = []
|
||||||
|
fmt = {}
|
||||||
|
g = None
|
||||||
|
for ln in [x.rstrip("\r") for x in txt.split("\n")]:
|
||||||
|
try:
|
||||||
|
k, v = ln.split("=", 1)
|
||||||
|
g[k] = v
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if ln == "[STREAM]":
|
||||||
|
g = {}
|
||||||
|
streams.append(g)
|
||||||
|
|
||||||
|
if ln == "[FORMAT]":
|
||||||
|
g = {"codec_type": "format"} # heh
|
||||||
|
fmt = g
|
||||||
|
|
||||||
|
streams = [fmt] + streams
|
||||||
|
ret = {} # processed
|
||||||
|
md = {} # raw tags
|
||||||
|
|
||||||
|
is_audio = fmt.get("format_name") in ["mp3", "ogg", "flac", "wav"]
|
||||||
|
if fmt.get("filename", "").split(".")[-1].lower() in ["m4a", "aac"]:
|
||||||
|
is_audio = True
|
||||||
|
|
||||||
|
# if audio file, ensure audio stream appears first
|
||||||
|
if (
|
||||||
|
is_audio
|
||||||
|
and len(streams) > 2
|
||||||
|
and streams[1].get("codec_type") != "audio"
|
||||||
|
and streams[2].get("codec_type") == "audio"
|
||||||
|
):
|
||||||
|
streams = [fmt, streams[2], streams[1]] + streams[3:]
|
||||||
|
|
||||||
|
have = {}
|
||||||
|
for strm in streams:
|
||||||
|
typ = strm.get("codec_type")
|
||||||
|
if typ in have:
|
||||||
|
continue
|
||||||
|
|
||||||
|
have[typ] = True
|
||||||
|
kvm = []
|
||||||
|
|
||||||
|
if typ == "audio":
|
||||||
|
kvm = [
|
||||||
|
["codec_name", "ac"],
|
||||||
|
["channel_layout", "chs"],
|
||||||
|
["sample_rate", ".hz"],
|
||||||
|
["bit_rate", ".aq"],
|
||||||
|
["duration", ".dur"],
|
||||||
|
]
|
||||||
|
|
||||||
|
if typ == "video":
|
||||||
|
if strm.get("DISPOSITION:attached_pic") == "1" or is_audio:
|
||||||
|
continue
|
||||||
|
|
||||||
|
kvm = [
|
||||||
|
["codec_name", "vc"],
|
||||||
|
["pix_fmt", "pixfmt"],
|
||||||
|
["r_frame_rate", ".fps"],
|
||||||
|
["bit_rate", ".vq"],
|
||||||
|
["width", ".resw"],
|
||||||
|
["height", ".resh"],
|
||||||
|
["duration", ".dur"],
|
||||||
|
]
|
||||||
|
|
||||||
|
if typ == "format":
|
||||||
|
kvm = [["duration", ".dur"], ["bit_rate", ".q"]]
|
||||||
|
|
||||||
|
for sk, rk in kvm:
|
||||||
|
v = strm.get(sk)
|
||||||
|
if v is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if rk.startswith("."):
|
||||||
|
try:
|
||||||
|
v = float(v)
|
||||||
|
v2 = ret.get(rk)
|
||||||
|
if v2 is None or v > v2:
|
||||||
|
ret[rk] = v
|
||||||
|
except:
|
||||||
|
# sqlite doesnt care but the code below does
|
||||||
|
if v not in ["N/A"]:
|
||||||
|
ret[rk] = v
|
||||||
|
else:
|
||||||
|
ret[rk] = v
|
||||||
|
|
||||||
|
if ret.get("vc") == "ansi": # shellscript
|
||||||
|
return {}, {}
|
||||||
|
|
||||||
|
for strm in streams:
|
||||||
|
for k, v in strm.items():
|
||||||
|
if not k.startswith("TAG:"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
k = k[4:].strip()
|
||||||
|
v = v.strip()
|
||||||
|
if k and v and k not in md:
|
||||||
|
md[k] = [v]
|
||||||
|
|
||||||
|
for k in [".q", ".vq", ".aq"]:
|
||||||
|
if k in ret:
|
||||||
|
ret[k] /= 1000 # bit_rate=320000
|
||||||
|
|
||||||
|
for k in [".q", ".vq", ".aq", ".resw", ".resh"]:
|
||||||
|
if k in ret:
|
||||||
|
ret[k] = int(ret[k])
|
||||||
|
|
||||||
|
if ".fps" in ret:
|
||||||
|
fps = ret[".fps"]
|
||||||
|
if "/" in fps:
|
||||||
|
fa, fb = fps.split("/")
|
||||||
|
fps = int(fa) * 1.0 / int(fb)
|
||||||
|
|
||||||
|
if fps < 1000 and fmt.get("format_name") not in ["image2", "png_pipe"]:
|
||||||
|
ret[".fps"] = round(fps, 3)
|
||||||
|
else:
|
||||||
|
del ret[".fps"]
|
||||||
|
|
||||||
|
if ".dur" in ret:
|
||||||
|
if ret[".dur"] < 0.1:
|
||||||
|
del ret[".dur"]
|
||||||
|
if ".q" in ret:
|
||||||
|
del ret[".q"]
|
||||||
|
|
||||||
|
if ".resw" in ret and ".resh" in ret:
|
||||||
|
ret["res"] = "{}x{}".format(ret[".resw"], ret[".resh"])
|
||||||
|
|
||||||
|
ret = {k: [0, v] for k, v in ret.items()}
|
||||||
|
|
||||||
|
return ret, md
|
||||||
|
|
||||||
|
|
||||||
class MTag(object):
|
class MTag(object):
|
||||||
def __init__(self, log_func, args):
|
def __init__(self, log_func, args):
|
||||||
self.log_func = log_func
|
self.log_func = log_func
|
||||||
@@ -35,15 +248,7 @@ class MTag(object):
|
|||||||
self.get = self.get_ffprobe
|
self.get = self.get_ffprobe
|
||||||
self.prefer_mt = True
|
self.prefer_mt = True
|
||||||
# about 20x slower
|
# about 20x slower
|
||||||
if PY2:
|
self.usable = HAVE_FFPROBE
|
||||||
cmd = [b"ffprobe", b"-version"]
|
|
||||||
try:
|
|
||||||
sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE)
|
|
||||||
except:
|
|
||||||
self.usable = False
|
|
||||||
else:
|
|
||||||
if not shutil.which("ffprobe"):
|
|
||||||
self.usable = False
|
|
||||||
|
|
||||||
if self.usable and WINDOWS and sys.version_info < (3, 8):
|
if self.usable and WINDOWS and sys.version_info < (3, 8):
|
||||||
self.usable = False
|
self.usable = False
|
||||||
@@ -52,8 +257,10 @@ class MTag(object):
|
|||||||
self.log(msg, c=1)
|
self.log(msg, c=1)
|
||||||
|
|
||||||
if not self.usable:
|
if not self.usable:
|
||||||
msg = "need mutagen{} to read media tags so please run this:\n {} -m pip install --user mutagen"
|
msg = "need mutagen{} to read media tags so please run this:\n{}{} -m pip install --user mutagen\n"
|
||||||
self.log(msg.format(or_ffprobe, os.path.basename(sys.executable)), c=1)
|
self.log(
|
||||||
|
msg.format(or_ffprobe, " " * 37, os.path.basename(sys.executable)), c=1
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
|
# https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
|
||||||
@@ -201,7 +408,7 @@ class MTag(object):
|
|||||||
import mutagen
|
import mutagen
|
||||||
|
|
||||||
try:
|
try:
|
||||||
md = mutagen.File(abspath, easy=True)
|
md = mutagen.File(fsenc(abspath), easy=True)
|
||||||
x = md.info.length
|
x = md.info.length
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
return {}
|
return {}
|
||||||
@@ -212,7 +419,7 @@ class MTag(object):
|
|||||||
try:
|
try:
|
||||||
q = int(md.info.bitrate / 1024)
|
q = int(md.info.bitrate / 1024)
|
||||||
except:
|
except:
|
||||||
q = int((os.path.getsize(abspath) / dur) / 128)
|
q = int((os.path.getsize(fsenc(abspath)) / dur) / 128)
|
||||||
|
|
||||||
ret[".dur"] = [0, dur]
|
ret[".dur"] = [0, dur]
|
||||||
ret[".q"] = [0, q]
|
ret[".q"] = [0, q]
|
||||||
@@ -222,101 +429,7 @@ class MTag(object):
|
|||||||
return self.normalize_tags(ret, md)
|
return self.normalize_tags(ret, md)
|
||||||
|
|
||||||
def get_ffprobe(self, abspath):
|
def get_ffprobe(self, abspath):
|
||||||
cmd = [b"ffprobe", b"-hide_banner", b"--", fsenc(abspath)]
|
ret, md = ffprobe(abspath)
|
||||||
p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE)
|
|
||||||
r = p.communicate()
|
|
||||||
txt = r[1].decode("utf-8", "replace")
|
|
||||||
txt = [x.rstrip("\r") for x in txt.split("\n")]
|
|
||||||
|
|
||||||
"""
|
|
||||||
note:
|
|
||||||
tags which contain newline will be truncated on first \n,
|
|
||||||
ffprobe emits \n and spacepads the : to align visually
|
|
||||||
note:
|
|
||||||
the Stream ln always mentions Audio: if audio
|
|
||||||
the Stream ln usually has kb/s, is more accurate
|
|
||||||
the Duration ln always has kb/s
|
|
||||||
the Metadata: after Chapter may contain BPM info,
|
|
||||||
title : Tempo: 126.0
|
|
||||||
|
|
||||||
Input #0, wav,
|
|
||||||
Metadata:
|
|
||||||
date : <OK>
|
|
||||||
Duration:
|
|
||||||
Chapter #
|
|
||||||
Metadata:
|
|
||||||
title : <NG>
|
|
||||||
|
|
||||||
Input #0, mp3,
|
|
||||||
Metadata:
|
|
||||||
album : <OK>
|
|
||||||
Duration:
|
|
||||||
Stream #0:0: Audio:
|
|
||||||
Stream #0:1: Video:
|
|
||||||
Metadata:
|
|
||||||
comment : <NG>
|
|
||||||
"""
|
|
||||||
|
|
||||||
ptn_md_beg = re.compile("^( +)Metadata:$")
|
|
||||||
ptn_md_kv = re.compile("^( +)([^:]+) *: (.*)")
|
|
||||||
ptn_dur = re.compile("^ *Duration: ([^ ]+)(, |$)")
|
|
||||||
ptn_br1 = re.compile("^ *Duration: .*, bitrate: ([0-9]+) kb/s(, |$)")
|
|
||||||
ptn_br2 = re.compile("^ *Stream.*: Audio:.* ([0-9]+) kb/s(, |$)")
|
|
||||||
ptn_audio = re.compile("^ *Stream .*: Audio: ")
|
|
||||||
ptn_au_parent = re.compile("^ *(Input #|Stream .*: Audio: )")
|
|
||||||
|
|
||||||
ret = {}
|
|
||||||
md = {}
|
|
||||||
in_md = False
|
|
||||||
is_audio = False
|
|
||||||
au_parent = False
|
|
||||||
for ln in txt:
|
|
||||||
m = ptn_md_kv.match(ln)
|
|
||||||
if m and in_md and len(m.group(1)) == in_md:
|
|
||||||
_, k, v = [x.strip() for x in m.groups()]
|
|
||||||
if k != "" and v != "":
|
|
||||||
md[k] = [v]
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
in_md = False
|
|
||||||
|
|
||||||
m = ptn_md_beg.match(ln)
|
|
||||||
if m and au_parent:
|
|
||||||
in_md = len(m.group(1)) + 2
|
|
||||||
continue
|
|
||||||
|
|
||||||
au_parent = bool(ptn_au_parent.search(ln))
|
|
||||||
|
|
||||||
if ptn_audio.search(ln):
|
|
||||||
is_audio = True
|
|
||||||
|
|
||||||
m = ptn_dur.search(ln)
|
|
||||||
if m:
|
|
||||||
sec = 0
|
|
||||||
tstr = m.group(1)
|
|
||||||
if tstr.lower() != "n/a":
|
|
||||||
try:
|
|
||||||
tf = tstr.split(",")[0].split(".")[0].split(":")
|
|
||||||
for f in tf:
|
|
||||||
sec *= 60
|
|
||||||
sec += int(f)
|
|
||||||
except:
|
|
||||||
self.log("invalid timestr from ffprobe: [{}]".format(tstr), c=3)
|
|
||||||
|
|
||||||
ret[".dur"] = sec
|
|
||||||
m = ptn_br1.search(ln)
|
|
||||||
if m:
|
|
||||||
ret[".q"] = m.group(1)
|
|
||||||
|
|
||||||
m = ptn_br2.search(ln)
|
|
||||||
if m:
|
|
||||||
ret[".q"] = m.group(1)
|
|
||||||
|
|
||||||
if not is_audio:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
ret = {k: [0, v] for k, v in ret.items()}
|
|
||||||
|
|
||||||
return self.normalize_tags(ret, md)
|
return self.normalize_tags(ret, md)
|
||||||
|
|
||||||
def get_bin(self, parsers, abspath):
|
def get_bin(self, parsers, abspath):
|
||||||
@@ -327,10 +440,10 @@ class MTag(object):
|
|||||||
env["PYTHONPATH"] = pypath
|
env["PYTHONPATH"] = pypath
|
||||||
|
|
||||||
ret = {}
|
ret = {}
|
||||||
for tagname, (binpath, timeout) in parsers.items():
|
for tagname, mp in parsers.items():
|
||||||
try:
|
try:
|
||||||
cmd = [sys.executable, binpath, abspath]
|
cmd = [sys.executable, mp.bin, abspath]
|
||||||
args = {"env": env, "timeout": timeout}
|
args = {"env": env, "timeout": mp.timeout}
|
||||||
|
|
||||||
if WINDOWS:
|
if WINDOWS:
|
||||||
args["creationflags"] = 0x4000
|
args["creationflags"] = 0x4000
|
||||||
@@ -339,8 +452,16 @@ class MTag(object):
|
|||||||
|
|
||||||
cmd = [fsenc(x) for x in cmd]
|
cmd = [fsenc(x) for x in cmd]
|
||||||
v = sp.check_output(cmd, **args).strip()
|
v = sp.check_output(cmd, **args).strip()
|
||||||
if v:
|
if not v:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "," not in tagname:
|
||||||
ret[tagname] = v.decode("utf-8")
|
ret[tagname] = v.decode("utf-8")
|
||||||
|
else:
|
||||||
|
v = json.loads(v)
|
||||||
|
for tag in tagname.split(","):
|
||||||
|
if tag and tag in v:
|
||||||
|
ret[tag] = v[tag]
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import tarfile
|
import tarfile
|
||||||
import threading
|
import threading
|
||||||
@@ -42,7 +45,7 @@ class StreamTar(object):
|
|||||||
fmt = tarfile.GNU_FORMAT
|
fmt = tarfile.GNU_FORMAT
|
||||||
self.tar = tarfile.open(fileobj=self.qfile, mode="w|", format=fmt)
|
self.tar = tarfile.open(fileobj=self.qfile, mode="w|", format=fmt)
|
||||||
|
|
||||||
w = threading.Thread(target=self._gen)
|
w = threading.Thread(target=self._gen, name="star-gen")
|
||||||
w.daemon = True
|
w.daemon = True
|
||||||
w.start()
|
w.start()
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
@@ -9,9 +10,11 @@ from datetime import datetime, timedelta
|
|||||||
import calendar
|
import calendar
|
||||||
|
|
||||||
from .__init__ import PY2, WINDOWS, MACOS, VT100
|
from .__init__ import PY2, WINDOWS, MACOS, VT100
|
||||||
|
from .util import mp
|
||||||
|
from .authsrv import AuthSrv
|
||||||
from .tcpsrv import TcpSrv
|
from .tcpsrv import TcpSrv
|
||||||
from .up2k import Up2k
|
from .up2k import Up2k
|
||||||
from .util import mp
|
from .th_srv import ThumbSrv, HAVE_PIL, HAVE_WEBP
|
||||||
|
|
||||||
|
|
||||||
class SvcHub(object):
|
class SvcHub(object):
|
||||||
@@ -35,9 +38,28 @@ class SvcHub(object):
|
|||||||
self.log = self._log_disabled if args.q else self._log_enabled
|
self.log = self._log_disabled if args.q else self._log_enabled
|
||||||
|
|
||||||
# initiate all services to manage
|
# initiate all services to manage
|
||||||
|
self.asrv = AuthSrv(self.args, self.log, False)
|
||||||
|
if args.ls:
|
||||||
|
self.asrv.dbg_ls()
|
||||||
|
|
||||||
self.tcpsrv = TcpSrv(self)
|
self.tcpsrv = TcpSrv(self)
|
||||||
self.up2k = Up2k(self)
|
self.up2k = Up2k(self)
|
||||||
|
|
||||||
|
self.thumbsrv = None
|
||||||
|
if not args.no_thumb:
|
||||||
|
if HAVE_PIL:
|
||||||
|
if not HAVE_WEBP:
|
||||||
|
args.th_no_webp = True
|
||||||
|
msg = "setting --th-no-webp because either libwebp is not available or your Pillow is too old"
|
||||||
|
self.log("thumb", msg, c=3)
|
||||||
|
|
||||||
|
self.thumbsrv = ThumbSrv(self)
|
||||||
|
else:
|
||||||
|
msg = "need Pillow to create thumbnails; for example:\n{}{} -m pip install --user Pillow\n"
|
||||||
|
self.log(
|
||||||
|
"thumb", msg.format(" " * 37, os.path.basename(sys.executable)), c=3
|
||||||
|
)
|
||||||
|
|
||||||
# decide which worker impl to use
|
# decide which worker impl to use
|
||||||
if self.check_mp_enable():
|
if self.check_mp_enable():
|
||||||
from .broker_mp import BrokerMp as Broker
|
from .broker_mp import BrokerMp as Broker
|
||||||
@@ -48,7 +70,7 @@ class SvcHub(object):
|
|||||||
self.broker = Broker(self)
|
self.broker = Broker(self)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
thr = threading.Thread(target=self.tcpsrv.run)
|
thr = threading.Thread(target=self.tcpsrv.run, name="svchub-main")
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
@@ -63,7 +85,20 @@ class SvcHub(object):
|
|||||||
|
|
||||||
self.tcpsrv.shutdown()
|
self.tcpsrv.shutdown()
|
||||||
self.broker.shutdown()
|
self.broker.shutdown()
|
||||||
print("nailed it")
|
if self.thumbsrv:
|
||||||
|
self.thumbsrv.shutdown()
|
||||||
|
|
||||||
|
for n in range(200): # 10s
|
||||||
|
time.sleep(0.05)
|
||||||
|
if self.thumbsrv.stopped():
|
||||||
|
break
|
||||||
|
|
||||||
|
if n == 3:
|
||||||
|
print("waiting for thumbsrv (10sec)...")
|
||||||
|
|
||||||
|
print("nailed it", end="")
|
||||||
|
finally:
|
||||||
|
print("\033[0m")
|
||||||
|
|
||||||
def _log_disabled(self, src, msg, c=0):
|
def _log_disabled(self, src, msg, c=0):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import zlib
|
import zlib
|
||||||
@@ -87,7 +90,7 @@ def gen_hdr(h_pos, fn, sz, lastmod, utf8, crc32, pre_crc):
|
|||||||
ret += struct.pack("<LL", vsz, vsz)
|
ret += struct.pack("<LL", vsz, vsz)
|
||||||
|
|
||||||
# windows support (the "?" replace below too)
|
# windows support (the "?" replace below too)
|
||||||
fn = sanitize_fn(fn, "/")
|
fn = sanitize_fn(fn, ok="/")
|
||||||
bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_")
|
bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_")
|
||||||
|
|
||||||
z64_len = len(z64v) * 8 + 4 if z64v else 0
|
z64_len = len(z64v) * 8 + 4 if z64v else 0
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class TcpSrv(object):
|
|||||||
self.log = hub.log
|
self.log = hub.log
|
||||||
|
|
||||||
self.num_clients = Counter()
|
self.num_clients = Counter()
|
||||||
|
self.stopping = False
|
||||||
|
|
||||||
ip = "127.0.0.1"
|
ip = "127.0.0.1"
|
||||||
eps = {ip: "local only"}
|
eps = {ip: "local only"}
|
||||||
@@ -67,7 +68,7 @@ class TcpSrv(object):
|
|||||||
ip, port = srv.getsockname()
|
ip, port = srv.getsockname()
|
||||||
self.log("tcpsrv", "listening @ {0}:{1}".format(ip, port))
|
self.log("tcpsrv", "listening @ {0}:{1}".format(ip, port))
|
||||||
|
|
||||||
while True:
|
while not self.stopping:
|
||||||
if self.args.log_conn:
|
if self.args.log_conn:
|
||||||
self.log("tcpsrv", "|%sC-ncli" % ("-" * 1,), c="1;30")
|
self.log("tcpsrv", "|%sC-ncli" % ("-" * 1,), c="1;30")
|
||||||
|
|
||||||
@@ -78,8 +79,18 @@ class TcpSrv(object):
|
|||||||
if self.args.log_conn:
|
if self.args.log_conn:
|
||||||
self.log("tcpsrv", "|%sC-acc1" % ("-" * 2,), c="1;30")
|
self.log("tcpsrv", "|%sC-acc1" % ("-" * 2,), c="1;30")
|
||||||
|
|
||||||
ready, _, _ = select.select(self.srv, [], [])
|
try:
|
||||||
|
# macos throws bad-fd
|
||||||
|
ready, _, _ = select.select(self.srv, [], [])
|
||||||
|
except:
|
||||||
|
ready = []
|
||||||
|
if not self.stopping:
|
||||||
|
raise
|
||||||
|
|
||||||
for srv in ready:
|
for srv in ready:
|
||||||
|
if self.stopping:
|
||||||
|
break
|
||||||
|
|
||||||
sck, addr = srv.accept()
|
sck, addr = srv.accept()
|
||||||
sip, sport = srv.getsockname()
|
sip, sport = srv.getsockname()
|
||||||
if self.args.log_conn:
|
if self.args.log_conn:
|
||||||
@@ -95,6 +106,13 @@ class TcpSrv(object):
|
|||||||
self.hub.broker.put(False, "httpconn", sck, addr)
|
self.hub.broker.put(False, "httpconn", sck, addr)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
|
self.stopping = True
|
||||||
|
try:
|
||||||
|
for srv in self.srv:
|
||||||
|
srv.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
self.log("tcpsrv", "ok bye")
|
self.log("tcpsrv", "ok bye")
|
||||||
|
|
||||||
def detect_interfaces(self, listen_ips):
|
def detect_interfaces(self, listen_ips):
|
||||||
|
|||||||
55
copyparty/th_cli.py
Normal file
55
copyparty/th_cli.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .util import Cooldown
|
||||||
|
from .th_srv import thumb_path, THUMBABLE, FMT_FF
|
||||||
|
|
||||||
|
|
||||||
|
class ThumbCli(object):
|
||||||
|
def __init__(self, broker):
|
||||||
|
self.broker = broker
|
||||||
|
self.args = broker.args
|
||||||
|
self.asrv = broker.asrv
|
||||||
|
|
||||||
|
# cache on both sides for less broker spam
|
||||||
|
self.cooldown = Cooldown(self.args.th_poke)
|
||||||
|
|
||||||
|
def get(self, ptop, rem, mtime, fmt):
|
||||||
|
ext = rem.rsplit(".")[-1].lower()
|
||||||
|
if ext not in THUMBABLE:
|
||||||
|
return None
|
||||||
|
|
||||||
|
is_vid = ext in FMT_FF
|
||||||
|
if is_vid and self.args.no_vthumb:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if fmt == "j" and self.args.th_no_jpg:
|
||||||
|
fmt = "w"
|
||||||
|
|
||||||
|
if fmt == "w":
|
||||||
|
if self.args.th_no_webp or (is_vid and self.args.th_ff_jpg):
|
||||||
|
fmt = "j"
|
||||||
|
|
||||||
|
histpath = self.asrv.vfs.histtab[ptop]
|
||||||
|
tpath = thumb_path(histpath, rem, mtime, fmt)
|
||||||
|
ret = None
|
||||||
|
try:
|
||||||
|
st = os.stat(tpath)
|
||||||
|
if st.st_size:
|
||||||
|
ret = tpath
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if ret:
|
||||||
|
tdir = os.path.dirname(tpath)
|
||||||
|
if self.cooldown.poke(tdir):
|
||||||
|
self.broker.put(False, "thumbsrv.poke", tdir)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
x = self.broker.put(True, "thumbsrv.get", ptop, rem, mtime, fmt)
|
||||||
|
return x.get()
|
||||||
409
copyparty/th_srv.py
Normal file
409
copyparty/th_srv.py
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import shutil
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import threading
|
||||||
|
import subprocess as sp
|
||||||
|
|
||||||
|
from .__init__ import PY2
|
||||||
|
from .util import fsenc, runcmd, Queue, Cooldown, BytesIO, min_ex
|
||||||
|
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
|
||||||
|
|
||||||
|
|
||||||
|
if not PY2:
|
||||||
|
unicode = str
|
||||||
|
|
||||||
|
|
||||||
|
HAVE_PIL = False
|
||||||
|
HAVE_HEIF = False
|
||||||
|
HAVE_AVIF = False
|
||||||
|
HAVE_WEBP = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import Image, ImageOps
|
||||||
|
|
||||||
|
HAVE_PIL = True
|
||||||
|
try:
|
||||||
|
Image.new("RGB", (2, 2)).save(BytesIO(), format="webp")
|
||||||
|
HAVE_WEBP = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pyheif_pillow_opener import register_heif_opener
|
||||||
|
|
||||||
|
register_heif_opener()
|
||||||
|
HAVE_HEIF = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pillow_avif
|
||||||
|
|
||||||
|
HAVE_AVIF = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
||||||
|
# ffmpeg -formats
|
||||||
|
FMT_PIL = "bmp dib gif icns ico jpg jpeg jp2 jpx pcx png pbm pgm ppm pnm sgi tga tif tiff webp xbm dds xpm"
|
||||||
|
FMT_FF = "av1 asf avi flv m4v mkv mjpeg mjpg mpg mpeg mpg2 mpeg2 h264 avc h265 hevc mov 3gp mp4 ts mpegts nut ogv ogm rm vob webm wmv"
|
||||||
|
|
||||||
|
if HAVE_HEIF:
|
||||||
|
FMT_PIL += " heif heifs heic heics"
|
||||||
|
|
||||||
|
if HAVE_AVIF:
|
||||||
|
FMT_PIL += " avif avifs"
|
||||||
|
|
||||||
|
FMT_PIL, FMT_FF = [{x: True for x in y.split(" ") if x} for y in [FMT_PIL, FMT_FF]]
|
||||||
|
|
||||||
|
|
||||||
|
THUMBABLE = {}
|
||||||
|
|
||||||
|
if HAVE_PIL:
|
||||||
|
THUMBABLE.update(FMT_PIL)
|
||||||
|
|
||||||
|
if HAVE_FFMPEG and HAVE_FFPROBE:
|
||||||
|
THUMBABLE.update(FMT_FF)
|
||||||
|
|
||||||
|
|
||||||
|
def thumb_path(histpath, rem, mtime, fmt):
|
||||||
|
# base16 = 16 = 256
|
||||||
|
# b64-lc = 38 = 1444
|
||||||
|
# base64 = 64 = 4096
|
||||||
|
try:
|
||||||
|
rd, fn = rem.rsplit("/", 1)
|
||||||
|
except:
|
||||||
|
rd = ""
|
||||||
|
fn = rem
|
||||||
|
|
||||||
|
if rd:
|
||||||
|
h = hashlib.sha512(fsenc(rd)).digest()
|
||||||
|
b64 = base64.urlsafe_b64encode(h).decode("ascii")[:24]
|
||||||
|
rd = "{}/{}/".format(b64[:2], b64[2:4]).lower() + b64
|
||||||
|
else:
|
||||||
|
rd = "top"
|
||||||
|
|
||||||
|
# could keep original filenames but this is safer re pathlen
|
||||||
|
h = hashlib.sha512(fsenc(fn)).digest()
|
||||||
|
fn = base64.urlsafe_b64encode(h).decode("ascii")[:24]
|
||||||
|
|
||||||
|
return "{}/th/{}/{}.{:x}.{}".format(
|
||||||
|
histpath, rd, fn, int(mtime), "webp" if fmt == "w" else "jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ThumbSrv(object):
|
||||||
|
def __init__(self, hub):
|
||||||
|
self.hub = hub
|
||||||
|
self.asrv = hub.asrv
|
||||||
|
self.args = hub.args
|
||||||
|
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.mutex = threading.Lock()
|
||||||
|
self.busy = {}
|
||||||
|
self.stopping = False
|
||||||
|
self.nthr = os.cpu_count() if hasattr(os, "cpu_count") else 4
|
||||||
|
self.q = Queue(self.nthr * 4)
|
||||||
|
for n in range(self.nthr):
|
||||||
|
t = threading.Thread(
|
||||||
|
target=self.worker, name="thumb-{}-{}".format(n, self.nthr)
|
||||||
|
)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
if not self.args.no_vthumb and (not HAVE_FFMPEG or not HAVE_FFPROBE):
|
||||||
|
missing = []
|
||||||
|
if not HAVE_FFMPEG:
|
||||||
|
missing.append("ffmpeg")
|
||||||
|
|
||||||
|
if not HAVE_FFPROBE:
|
||||||
|
missing.append("ffprobe")
|
||||||
|
|
||||||
|
msg = "cannot create video thumbnails because some of the required programs are not available: "
|
||||||
|
msg += ", ".join(missing)
|
||||||
|
self.log(msg, c=3)
|
||||||
|
|
||||||
|
t = threading.Thread(target=self.cleaner, name="thumb-cleaner")
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def log(self, msg, c=0):
|
||||||
|
self.log_func("thumb", msg, c)
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
self.stopping = True
|
||||||
|
for _ in range(self.nthr):
|
||||||
|
self.q.put(None)
|
||||||
|
|
||||||
|
def stopped(self):
|
||||||
|
with self.mutex:
|
||||||
|
return not self.nthr
|
||||||
|
|
||||||
|
def get(self, ptop, rem, mtime, fmt):
|
||||||
|
histpath = self.asrv.vfs.histtab[ptop]
|
||||||
|
tpath = thumb_path(histpath, rem, mtime, fmt)
|
||||||
|
abspath = os.path.join(ptop, rem)
|
||||||
|
cond = threading.Condition(self.mutex)
|
||||||
|
do_conv = False
|
||||||
|
with self.mutex:
|
||||||
|
try:
|
||||||
|
self.busy[tpath].append(cond)
|
||||||
|
self.log("wait {}".format(tpath))
|
||||||
|
except:
|
||||||
|
thdir = os.path.dirname(tpath)
|
||||||
|
try:
|
||||||
|
os.makedirs(thdir)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
inf_path = os.path.join(thdir, "dir.txt")
|
||||||
|
if not os.path.exists(inf_path):
|
||||||
|
with open(inf_path, "wb") as f:
|
||||||
|
f.write(fsenc(os.path.dirname(abspath)))
|
||||||
|
|
||||||
|
self.busy[tpath] = [cond]
|
||||||
|
do_conv = True
|
||||||
|
|
||||||
|
if do_conv:
|
||||||
|
self.q.put([abspath, tpath])
|
||||||
|
self.log("conv {} \033[0m{}".format(tpath, abspath), c=6)
|
||||||
|
|
||||||
|
while not self.stopping:
|
||||||
|
with self.mutex:
|
||||||
|
if tpath not in self.busy:
|
||||||
|
break
|
||||||
|
|
||||||
|
with cond:
|
||||||
|
cond.wait(3)
|
||||||
|
|
||||||
|
try:
|
||||||
|
st = os.stat(tpath)
|
||||||
|
if st.st_size:
|
||||||
|
return tpath
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def worker(self):
|
||||||
|
while not self.stopping:
|
||||||
|
task = self.q.get()
|
||||||
|
if not task:
|
||||||
|
break
|
||||||
|
|
||||||
|
abspath, tpath = task
|
||||||
|
ext = abspath.split(".")[-1].lower()
|
||||||
|
fun = None
|
||||||
|
if not os.path.exists(tpath):
|
||||||
|
if ext in FMT_PIL:
|
||||||
|
fun = self.conv_pil
|
||||||
|
elif ext in FMT_FF:
|
||||||
|
fun = self.conv_ffmpeg
|
||||||
|
|
||||||
|
if fun:
|
||||||
|
try:
|
||||||
|
fun(abspath, tpath)
|
||||||
|
except:
|
||||||
|
msg = "{} failed on {}\n{}"
|
||||||
|
self.log(msg.format(fun.__name__, abspath, min_ex()), 3)
|
||||||
|
with open(tpath, "wb") as _:
|
||||||
|
pass
|
||||||
|
|
||||||
|
with self.mutex:
|
||||||
|
subs = self.busy[tpath]
|
||||||
|
del self.busy[tpath]
|
||||||
|
|
||||||
|
for x in subs:
|
||||||
|
with x:
|
||||||
|
x.notify_all()
|
||||||
|
|
||||||
|
with self.mutex:
|
||||||
|
self.nthr -= 1
|
||||||
|
|
||||||
|
def conv_pil(self, abspath, tpath):
|
||||||
|
with Image.open(fsenc(abspath)) as im:
|
||||||
|
crop = not self.args.th_no_crop
|
||||||
|
res2 = self.res
|
||||||
|
if crop:
|
||||||
|
res2 = (res2[0] * 2, res2[1] * 2)
|
||||||
|
|
||||||
|
try:
|
||||||
|
im.thumbnail(res2, resample=Image.LANCZOS)
|
||||||
|
if crop:
|
||||||
|
iw, ih = im.size
|
||||||
|
dw, dh = self.res
|
||||||
|
res = (min(iw, dw), min(ih, dh))
|
||||||
|
im = ImageOps.fit(im, res, method=Image.LANCZOS)
|
||||||
|
except:
|
||||||
|
im.thumbnail(self.res)
|
||||||
|
|
||||||
|
fmts = ["RGB", "L"]
|
||||||
|
args = {"quality": 40}
|
||||||
|
|
||||||
|
if tpath.endswith(".webp"):
|
||||||
|
# quality 80 = pillow-default
|
||||||
|
# quality 75 = ffmpeg-default
|
||||||
|
# method 0 = pillow-default, fast
|
||||||
|
# method 4 = ffmpeg-default
|
||||||
|
# method 6 = max, slow
|
||||||
|
fmts += ["RGBA", "LA"]
|
||||||
|
args["method"] = 6
|
||||||
|
else:
|
||||||
|
pass # default q = 75
|
||||||
|
|
||||||
|
if im.mode not in fmts:
|
||||||
|
print("conv {}".format(im.mode))
|
||||||
|
im = im.convert("RGB")
|
||||||
|
|
||||||
|
im.save(tpath, quality=40, method=6)
|
||||||
|
|
||||||
|
def conv_ffmpeg(self, abspath, tpath):
|
||||||
|
ret, _ = ffprobe(abspath)
|
||||||
|
|
||||||
|
ext = abspath.rsplit(".")[-1]
|
||||||
|
if ext in ["h264", "h265"]:
|
||||||
|
seek = []
|
||||||
|
else:
|
||||||
|
dur = ret[".dur"][1] if ".dur" in ret else 4
|
||||||
|
seek = "{:.0f}".format(dur / 3)
|
||||||
|
seek = [b"-ss", seek.encode("utf-8")]
|
||||||
|
|
||||||
|
scale = "scale={0}:{1}:force_original_aspect_ratio="
|
||||||
|
if self.args.th_no_crop:
|
||||||
|
scale += "decrease,setsar=1:1"
|
||||||
|
else:
|
||||||
|
scale += "increase,crop={0}:{1},setsar=1:1"
|
||||||
|
|
||||||
|
scale = scale.format(*list(self.res)).encode("utf-8")
|
||||||
|
# fmt: off
|
||||||
|
cmd = [
|
||||||
|
b"ffmpeg",
|
||||||
|
b"-nostdin",
|
||||||
|
b"-v", b"error",
|
||||||
|
b"-hide_banner"
|
||||||
|
]
|
||||||
|
cmd += seek
|
||||||
|
cmd += [
|
||||||
|
b"-i", fsenc(abspath),
|
||||||
|
b"-vf", scale,
|
||||||
|
b"-vframes", b"1",
|
||||||
|
]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
if tpath.endswith(".jpg"):
|
||||||
|
cmd += [
|
||||||
|
b"-q:v",
|
||||||
|
b"6", # default=??
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
cmd += [
|
||||||
|
b"-q:v",
|
||||||
|
b"50", # default=75
|
||||||
|
b"-compression_level:v",
|
||||||
|
b"6", # default=4, 0=fast, 6=max
|
||||||
|
]
|
||||||
|
|
||||||
|
cmd += [fsenc(tpath)]
|
||||||
|
|
||||||
|
ret, sout, serr = runcmd(*cmd)
|
||||||
|
if ret != 0:
|
||||||
|
msg = ["ff: {}".format(x) for x in serr.split("\n")]
|
||||||
|
self.log("FFmpeg failed:\n" + "\n".join(msg), c="1;30")
|
||||||
|
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
|
||||||
|
|
||||||
|
def poke(self, tdir):
|
||||||
|
if not self.poke_cd.poke(tdir):
|
||||||
|
return
|
||||||
|
|
||||||
|
ts = int(time.time())
|
||||||
|
try:
|
||||||
|
p1 = os.path.dirname(tdir)
|
||||||
|
p2 = os.path.dirname(p1)
|
||||||
|
for dp in [tdir, p1, p2]:
|
||||||
|
os.utime(fsenc(dp), (ts, ts))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cleaner(self):
|
||||||
|
interval = self.args.th_clean
|
||||||
|
while True:
|
||||||
|
time.sleep(interval)
|
||||||
|
ndirs = 0
|
||||||
|
for vol, histpath in self.asrv.vfs.histtab.items():
|
||||||
|
if histpath.startswith(vol):
|
||||||
|
self.log("\033[Jcln {}/\033[A".format(histpath))
|
||||||
|
else:
|
||||||
|
self.log("\033[Jcln {} ({})/\033[A".format(histpath, vol))
|
||||||
|
|
||||||
|
ndirs += self.clean(histpath)
|
||||||
|
|
||||||
|
self.log("\033[Jcln ok; rm {} dirs".format(ndirs))
|
||||||
|
|
||||||
|
def clean(self, histpath):
|
||||||
|
thumbpath = os.path.join(histpath, "th")
|
||||||
|
# self.log("cln {}".format(thumbpath))
|
||||||
|
maxage = self.args.th_maxage
|
||||||
|
now = time.time()
|
||||||
|
prev_b64 = None
|
||||||
|
prev_fp = None
|
||||||
|
try:
|
||||||
|
ents = os.listdir(thumbpath)
|
||||||
|
except:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
ndirs = 0
|
||||||
|
for f in sorted(ents):
|
||||||
|
fp = os.path.join(thumbpath, f)
|
||||||
|
cmp = fp.lower().replace("\\", "/")
|
||||||
|
|
||||||
|
# "top" or b64 prefix/full (a folder)
|
||||||
|
if len(f) <= 3 or len(f) == 24:
|
||||||
|
age = now - os.path.getmtime(fp)
|
||||||
|
if age > maxage:
|
||||||
|
with self.mutex:
|
||||||
|
safe = True
|
||||||
|
for k in self.busy.keys():
|
||||||
|
if k.lower().replace("\\", "/").startswith(cmp):
|
||||||
|
safe = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if safe:
|
||||||
|
ndirs += 1
|
||||||
|
self.log("rm -rf [{}]".format(fp))
|
||||||
|
shutil.rmtree(fp, ignore_errors=True)
|
||||||
|
else:
|
||||||
|
ndirs += self.clean(fp)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# thumb file
|
||||||
|
try:
|
||||||
|
b64, ts, ext = f.split(".")
|
||||||
|
if len(b64) != 24 or len(ts) != 8 or ext not in ["jpg", "webp"]:
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
ts = int(ts, 16)
|
||||||
|
except:
|
||||||
|
if f != "dir.txt":
|
||||||
|
self.log("foreign file in thumbs dir: [{}]".format(fp), 1)
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
if b64 == prev_b64:
|
||||||
|
self.log("rm replaced [{}]".format(fp))
|
||||||
|
os.unlink(prev_fp)
|
||||||
|
|
||||||
|
prev_b64 = b64
|
||||||
|
prev_fp = fp
|
||||||
|
|
||||||
|
return ndirs
|
||||||
@@ -7,7 +7,7 @@ import time
|
|||||||
import threading
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from .util import u8safe, s3dec, html_escape, Pebkac
|
from .util import s3dec, Pebkac, min_ex
|
||||||
from .up2k import up2k_wark_from_hashlist
|
from .up2k import up2k_wark_from_hashlist
|
||||||
|
|
||||||
|
|
||||||
@@ -19,13 +19,14 @@ except:
|
|||||||
|
|
||||||
|
|
||||||
class U2idx(object):
|
class U2idx(object):
|
||||||
def __init__(self, args, log_func):
|
def __init__(self, conn):
|
||||||
self.args = args
|
self.log_func = conn.log_func
|
||||||
self.log_func = log_func
|
self.asrv = conn.asrv
|
||||||
self.timeout = args.srch_time
|
self.args = conn.args
|
||||||
|
self.timeout = self.args.srch_time
|
||||||
|
|
||||||
if not HAVE_SQLITE3:
|
if not HAVE_SQLITE3:
|
||||||
self.log("could not load sqlite3; searchign wqill be disabled")
|
self.log("your python does not have sqlite3; searching will be disabled")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.cur = {}
|
self.cur = {}
|
||||||
@@ -47,57 +48,146 @@ class U2idx(object):
|
|||||||
fhash = body["hash"]
|
fhash = body["hash"]
|
||||||
wark = up2k_wark_from_hashlist(self.args.salt, fsize, fhash)
|
wark = up2k_wark_from_hashlist(self.args.salt, fsize, fhash)
|
||||||
|
|
||||||
uq = "substr(w,1,16) = ? and w = ?"
|
uq = "where substr(w,1,16) = ? and w = ?"
|
||||||
uv = [wark[:16], wark]
|
uv = [wark[:16], wark]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.run_query(vols, uq, uv, {})[0]
|
return self.run_query(vols, uq, uv)[0]
|
||||||
except Exception as ex:
|
except:
|
||||||
raise Pebkac(500, repr(ex))
|
raise Pebkac(500, min_ex())
|
||||||
|
|
||||||
def get_cur(self, ptop):
|
def get_cur(self, ptop):
|
||||||
|
if not HAVE_SQLITE3:
|
||||||
|
return None
|
||||||
|
|
||||||
cur = self.cur.get(ptop)
|
cur = self.cur.get(ptop)
|
||||||
if cur:
|
if cur:
|
||||||
return cur
|
return cur
|
||||||
|
|
||||||
cur = _open(ptop)
|
histpath = self.asrv.vfs.histtab[ptop]
|
||||||
if not cur:
|
db_path = os.path.join(histpath, "up2k.db")
|
||||||
|
if not os.path.exists(db_path):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
cur = sqlite3.connect(db_path, 2).cursor()
|
||||||
self.cur[ptop] = cur
|
self.cur[ptop] = cur
|
||||||
return cur
|
return cur
|
||||||
|
|
||||||
def search(self, vols, body):
|
def search(self, vols, uq):
|
||||||
"""search by query params"""
|
"""search by query params"""
|
||||||
if not HAVE_SQLITE3:
|
if not HAVE_SQLITE3:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
qobj = {}
|
q = ""
|
||||||
_conv_sz(qobj, body, "sz_min", "up.sz >= ?")
|
va = []
|
||||||
_conv_sz(qobj, body, "sz_max", "up.sz <= ?")
|
joins = ""
|
||||||
_conv_dt(qobj, body, "dt_min", "up.mt >= ?")
|
is_key = True
|
||||||
_conv_dt(qobj, body, "dt_max", "up.mt <= ?")
|
is_size = False
|
||||||
for seg, dk in [["path", "up.rd"], ["name", "up.fn"]]:
|
is_date = False
|
||||||
if seg in body:
|
kw_key = ["(", ")", "and ", "or ", "not "]
|
||||||
_conv_txt(qobj, body, seg, dk)
|
kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "]
|
||||||
|
ptn_mt = re.compile(r"^\.?[a-z]+$")
|
||||||
|
mt_ctr = 0
|
||||||
|
mt_keycmp = "substr(up.w,1,16)"
|
||||||
|
mt_keycmp2 = None
|
||||||
|
|
||||||
uq, uv = _sqlize(qobj)
|
while True:
|
||||||
|
uq = uq.strip()
|
||||||
|
if not uq:
|
||||||
|
break
|
||||||
|
|
||||||
qobj = {}
|
ok = False
|
||||||
if "tags" in body:
|
for kw in kw_key + kw_val:
|
||||||
_conv_txt(qobj, body, "tags", "mt.v")
|
if uq.startswith(kw):
|
||||||
|
is_key = kw in kw_key
|
||||||
|
uq = uq[len(kw) :]
|
||||||
|
ok = True
|
||||||
|
q += kw
|
||||||
|
break
|
||||||
|
|
||||||
if "adv" in body:
|
if ok:
|
||||||
_conv_adv(qobj, body, "adv")
|
continue
|
||||||
|
|
||||||
|
v, uq = (uq + " ").split(" ", 1)
|
||||||
|
if is_key:
|
||||||
|
is_key = False
|
||||||
|
|
||||||
|
if v == "size":
|
||||||
|
v = "up.sz"
|
||||||
|
is_size = True
|
||||||
|
|
||||||
|
elif v == "date":
|
||||||
|
v = "up.mt"
|
||||||
|
is_date = True
|
||||||
|
|
||||||
|
elif v == "path":
|
||||||
|
v = "up.rd"
|
||||||
|
|
||||||
|
elif v == "name":
|
||||||
|
v = "up.fn"
|
||||||
|
|
||||||
|
elif v == "tags" or ptn_mt.match(v):
|
||||||
|
mt_ctr += 1
|
||||||
|
mt_keycmp2 = "mt{}.w".format(mt_ctr)
|
||||||
|
joins += "inner join mt mt{} on {} = {} ".format(
|
||||||
|
mt_ctr, mt_keycmp, mt_keycmp2
|
||||||
|
)
|
||||||
|
mt_keycmp = mt_keycmp2
|
||||||
|
if v == "tags":
|
||||||
|
v = "mt{0}.v".format(mt_ctr)
|
||||||
|
else:
|
||||||
|
v = "+mt{0}.k = '{1}' and mt{0}.v".format(mt_ctr, v)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Pebkac(400, "invalid key [" + v + "]")
|
||||||
|
|
||||||
|
q += v + " "
|
||||||
|
continue
|
||||||
|
|
||||||
|
head = ""
|
||||||
|
tail = ""
|
||||||
|
|
||||||
|
if is_date:
|
||||||
|
is_date = False
|
||||||
|
v = v.upper().rstrip("Z").replace(",", " ").replace("T", " ")
|
||||||
|
while " " in v:
|
||||||
|
v = v.replace(" ", " ")
|
||||||
|
|
||||||
|
for fmt in [
|
||||||
|
"%Y-%m-%d %H:%M:%S",
|
||||||
|
"%Y-%m-%d %H:%M",
|
||||||
|
"%Y-%m-%d %H",
|
||||||
|
"%Y-%m-%d",
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
v = datetime.strptime(v, fmt).timestamp()
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif is_size:
|
||||||
|
is_size = False
|
||||||
|
v = int(float(v) * 1024 * 1024)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if v.startswith("*"):
|
||||||
|
head = "'%'||"
|
||||||
|
v = v[1:]
|
||||||
|
|
||||||
|
if v.endswith("*"):
|
||||||
|
tail = "||'%'"
|
||||||
|
v = v[:-1]
|
||||||
|
|
||||||
|
q += " {}?{} ".format(head, tail)
|
||||||
|
va.append(v)
|
||||||
|
is_key = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.run_query(vols, uq, uv, qobj)
|
return self.run_query(vols, joins + "where " + q, va)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise Pebkac(500, repr(ex))
|
raise Pebkac(500, repr(ex))
|
||||||
|
|
||||||
def run_query(self, vols, uq, uv, targs):
|
def run_query(self, vols, uq, uv):
|
||||||
self.log("qs: {} {} , {}".format(uq, repr(uv), repr(targs)))
|
|
||||||
|
|
||||||
done_flag = []
|
done_flag = []
|
||||||
self.active_id = "{:.6f}_{}".format(
|
self.active_id = "{:.6f}_{}".format(
|
||||||
time.time(), threading.current_thread().ident
|
time.time(), threading.current_thread().ident
|
||||||
@@ -108,39 +198,19 @@ class U2idx(object):
|
|||||||
self.active_id,
|
self.active_id,
|
||||||
done_flag,
|
done_flag,
|
||||||
),
|
),
|
||||||
|
name="u2idx-terminator",
|
||||||
)
|
)
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
if not targs:
|
if not uq or not uv:
|
||||||
if not uq:
|
q = "select * from up"
|
||||||
q = "select * from up"
|
v = ()
|
||||||
v = ()
|
|
||||||
else:
|
|
||||||
q = "select * from up where " + uq
|
|
||||||
v = tuple(uv)
|
|
||||||
else:
|
else:
|
||||||
q = "select up.* from up"
|
q = "select up.* from up " + uq
|
||||||
keycmp = "substr(up.w,1,16)"
|
v = tuple(uv)
|
||||||
where = []
|
|
||||||
v = []
|
|
||||||
ctr = 0
|
|
||||||
for tq, tv in sorted(targs.items()):
|
|
||||||
ctr += 1
|
|
||||||
tq = tq.split("\n")[0]
|
|
||||||
keycmp2 = "mt{}.w".format(ctr)
|
|
||||||
q += " inner join mt mt{} on {} = {}".format(ctr, keycmp, keycmp2)
|
|
||||||
keycmp = keycmp2
|
|
||||||
where.append(tq.replace("mt.", keycmp[:-1]))
|
|
||||||
v.append(tv)
|
|
||||||
|
|
||||||
if uq:
|
self.log("qs: {!r} {!r}".format(q, v))
|
||||||
where.append(uq)
|
|
||||||
v.extend(uv)
|
|
||||||
|
|
||||||
q += " where " + (" and ".join(where))
|
|
||||||
|
|
||||||
# self.log("q2: {} {}".format(q, repr(v)))
|
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
lim = 1000
|
lim = 1000
|
||||||
@@ -163,7 +233,7 @@ 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)
|
||||||
|
|
||||||
rp = os.path.join(vtop, rd, fn).replace("\\", "/")
|
rp = "/".join([x for x in [vtop, rd, fn] if x])
|
||||||
sret.append({"ts": int(ts), "sz": sz, "rp": rp, "w": w[:16]})
|
sret.append({"ts": int(ts), "sz": sz, "rp": rp, "w": w[:16]})
|
||||||
|
|
||||||
for hit in sret:
|
for hit in sret:
|
||||||
@@ -178,6 +248,7 @@ class U2idx(object):
|
|||||||
hit["tags"] = tags
|
hit["tags"] = tags
|
||||||
|
|
||||||
ret.extend(sret)
|
ret.extend(sret)
|
||||||
|
# print("[{}] {}".format(ptop, sret))
|
||||||
|
|
||||||
done_flag.append(True)
|
done_flag.append(True)
|
||||||
self.active_id = None
|
self.active_id = None
|
||||||
@@ -198,84 +269,3 @@ class U2idx(object):
|
|||||||
|
|
||||||
if identifier == self.active_id:
|
if identifier == self.active_id:
|
||||||
self.active_cur.connection.interrupt()
|
self.active_cur.connection.interrupt()
|
||||||
|
|
||||||
|
|
||||||
def _open(ptop):
|
|
||||||
db_path = os.path.join(ptop, ".hist", "up2k.db")
|
|
||||||
if os.path.exists(db_path):
|
|
||||||
return sqlite3.connect(db_path).cursor()
|
|
||||||
|
|
||||||
|
|
||||||
def _conv_sz(q, body, k, sql):
|
|
||||||
if k in body:
|
|
||||||
q[sql] = int(float(body[k]) * 1024 * 1024)
|
|
||||||
|
|
||||||
|
|
||||||
def _conv_dt(q, body, k, sql):
|
|
||||||
if k not in body:
|
|
||||||
return
|
|
||||||
|
|
||||||
v = body[k].upper().rstrip("Z").replace(",", " ").replace("T", " ")
|
|
||||||
while " " in v:
|
|
||||||
v = v.replace(" ", " ")
|
|
||||||
|
|
||||||
for fmt in ["%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d %H", "%Y-%m-%d"]:
|
|
||||||
try:
|
|
||||||
ts = datetime.strptime(v, fmt).timestamp()
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
ts = None
|
|
||||||
|
|
||||||
if ts:
|
|
||||||
q[sql] = ts
|
|
||||||
|
|
||||||
|
|
||||||
def _conv_txt(q, body, k, sql):
|
|
||||||
for v in body[k].split(" "):
|
|
||||||
inv = ""
|
|
||||||
if v.startswith("-"):
|
|
||||||
inv = "not"
|
|
||||||
v = v[1:]
|
|
||||||
|
|
||||||
if not v:
|
|
||||||
continue
|
|
||||||
|
|
||||||
head = "'%'||"
|
|
||||||
if v.startswith("^"):
|
|
||||||
head = ""
|
|
||||||
v = v[1:]
|
|
||||||
|
|
||||||
tail = "||'%'"
|
|
||||||
if v.endswith("$"):
|
|
||||||
tail = ""
|
|
||||||
v = v[:-1]
|
|
||||||
|
|
||||||
qk = "{} {} like {}?{}".format(sql, inv, head, tail)
|
|
||||||
q[qk + "\n" + v] = u8safe(v)
|
|
||||||
|
|
||||||
|
|
||||||
def _conv_adv(q, body, k):
|
|
||||||
ptn = re.compile(r"^(\.?[a-z]+) *(==?|!=|<=?|>=?) *(.*)$")
|
|
||||||
|
|
||||||
parts = body[k].split(" ")
|
|
||||||
parts = [x.strip() for x in parts if x.strip()]
|
|
||||||
|
|
||||||
for part in parts:
|
|
||||||
m = ptn.match(part)
|
|
||||||
if not m:
|
|
||||||
p = html_escape(part)
|
|
||||||
raise Pebkac(400, "invalid argument [" + p + "]")
|
|
||||||
|
|
||||||
k, op, v = m.groups()
|
|
||||||
qk = "mt.k = '{}' and mt.v {} ?".format(k, op)
|
|
||||||
q[qk + "\n" + v] = u8safe(v)
|
|
||||||
|
|
||||||
|
|
||||||
def _sqlize(qobj):
|
|
||||||
keys = []
|
|
||||||
values = []
|
|
||||||
for k, v in sorted(qobj.items()):
|
|
||||||
keys.append(k.split("\n")[0])
|
|
||||||
values.append(v)
|
|
||||||
|
|
||||||
return " and ".join(keys), values
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ import threading
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
import contextlib
|
import contextlib
|
||||||
import subprocess as sp # nosec
|
import subprocess as sp # nosec
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from .__init__ import PY2, WINDOWS, ANYWIN
|
from .__init__ import PY2, WINDOWS, ANYWIN
|
||||||
from .stolen import surrogateescape
|
from .stolen import surrogateescape
|
||||||
@@ -34,10 +35,12 @@ if not PY2:
|
|||||||
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 queue import Queue
|
from queue import Queue
|
||||||
|
from io import BytesIO
|
||||||
else:
|
else:
|
||||||
from urllib import unquote # pylint: disable=no-name-in-module
|
from urllib import unquote # pylint: disable=no-name-in-module
|
||||||
from urllib import quote # pylint: disable=no-name-in-module
|
from urllib import quote # pylint: disable=no-name-in-module
|
||||||
from Queue import Queue # pylint: disable=import-error,no-name-in-module
|
from Queue import Queue # pylint: disable=import-error,no-name-in-module
|
||||||
|
from StringIO import StringIO as BytesIO
|
||||||
|
|
||||||
surrogateescape.register_surrogateescape()
|
surrogateescape.register_surrogateescape()
|
||||||
FS_ENCODING = sys.getfilesystemencoding()
|
FS_ENCODING = sys.getfilesystemencoding()
|
||||||
@@ -45,10 +48,14 @@ if WINDOWS and PY2:
|
|||||||
FS_ENCODING = "utf-8"
|
FS_ENCODING = "utf-8"
|
||||||
|
|
||||||
|
|
||||||
|
HTTP_TS_FMT = "%a, %d %b %Y %H:%M:%S GMT"
|
||||||
|
|
||||||
|
|
||||||
HTTPCODE = {
|
HTTPCODE = {
|
||||||
200: "OK",
|
200: "OK",
|
||||||
204: "No Content",
|
204: "No Content",
|
||||||
206: "Partial Content",
|
206: "Partial Content",
|
||||||
|
302: "Found",
|
||||||
304: "Not Modified",
|
304: "Not Modified",
|
||||||
400: "Bad Request",
|
400: "Bad Request",
|
||||||
403: "Forbidden",
|
403: "Forbidden",
|
||||||
@@ -72,6 +79,13 @@ IMPLICATIONS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
MIMES = {
|
||||||
|
"md": "text/plain; charset=UTF-8",
|
||||||
|
"opus": "audio/ogg; codecs=opus",
|
||||||
|
"webp": "image/webp",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
REKOBO_KEY = {
|
REKOBO_KEY = {
|
||||||
v: ln.split(" ", 1)[0]
|
v: ln.split(" ", 1)[0]
|
||||||
for ln in """
|
for ln in """
|
||||||
@@ -123,6 +137,32 @@ class Counter(object):
|
|||||||
self.v = absval
|
self.v = absval
|
||||||
|
|
||||||
|
|
||||||
|
class Cooldown(object):
|
||||||
|
def __init__(self, maxage):
|
||||||
|
self.maxage = maxage
|
||||||
|
self.mutex = threading.Lock()
|
||||||
|
self.hist = {}
|
||||||
|
self.oldest = 0
|
||||||
|
|
||||||
|
def poke(self, key):
|
||||||
|
with self.mutex:
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
ret = False
|
||||||
|
v = self.hist.get(key, 0)
|
||||||
|
if now - v > self.maxage:
|
||||||
|
self.hist[key] = now
|
||||||
|
ret = True
|
||||||
|
|
||||||
|
if self.oldest - now > self.maxage * 2:
|
||||||
|
self.hist = {
|
||||||
|
k: v for k, v in self.hist.items() if now - v < self.maxage
|
||||||
|
}
|
||||||
|
self.oldest = sorted(self.hist.values())[0]
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class Unrecv(object):
|
class Unrecv(object):
|
||||||
"""
|
"""
|
||||||
undo any number of socket recv ops
|
undo any number of socket recv ops
|
||||||
@@ -153,7 +193,7 @@ class ProgressPrinter(threading.Thread):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
threading.Thread.__init__(self)
|
threading.Thread.__init__(self, name="pp")
|
||||||
self.daemon = True
|
self.daemon = True
|
||||||
self.msg = None
|
self.msg = None
|
||||||
self.end = False
|
self.end = False
|
||||||
@@ -168,6 +208,8 @@ class ProgressPrinter(threading.Thread):
|
|||||||
|
|
||||||
msg = self.msg
|
msg = self.msg
|
||||||
uprint(" {}\033[K\r".format(msg))
|
uprint(" {}\033[K\r".format(msg))
|
||||||
|
if PY2:
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
print("\033[K", end="")
|
print("\033[K", end="")
|
||||||
sys.stdout.flush() # necessary on win10 even w/ stderr btw
|
sys.stdout.flush() # necessary on win10 even w/ stderr btw
|
||||||
@@ -212,6 +254,45 @@ def trace(*args, **kwargs):
|
|||||||
nuprint(msg)
|
nuprint(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def alltrace():
|
||||||
|
threads = {}
|
||||||
|
names = dict([(t.ident, t.name) for t in threading.enumerate()])
|
||||||
|
for tid, stack in sys._current_frames().items():
|
||||||
|
name = "{} ({:x})".format(names.get(tid), tid)
|
||||||
|
threads[name] = stack
|
||||||
|
|
||||||
|
rret = []
|
||||||
|
bret = []
|
||||||
|
for name, stack in sorted(threads.items()):
|
||||||
|
ret = ["\n\n# {}".format(name)]
|
||||||
|
pad = None
|
||||||
|
for fn, lno, name, line in traceback.extract_stack(stack):
|
||||||
|
fn = os.sep.join(fn.split(os.sep)[-3:])
|
||||||
|
ret.append('File: "{}", line {}, in {}'.format(fn, lno, name))
|
||||||
|
if line:
|
||||||
|
ret.append(" " + str(line.strip()))
|
||||||
|
if "self.not_empty.wait()" in line:
|
||||||
|
pad = " " * 4
|
||||||
|
|
||||||
|
if pad:
|
||||||
|
bret += [ret[0]] + [pad + x for x in ret[1:]]
|
||||||
|
else:
|
||||||
|
rret += ret
|
||||||
|
|
||||||
|
return "\n".join(rret + bret)
|
||||||
|
|
||||||
|
|
||||||
|
def min_ex():
|
||||||
|
et, ev, tb = sys.exc_info()
|
||||||
|
tb = traceback.extract_tb(tb, 2)
|
||||||
|
ex = [
|
||||||
|
"{} @ {} <{}>: {}".format(fp.split(os.sep)[-1], ln, fun, txt)
|
||||||
|
for fp, ln, fun, txt in tb
|
||||||
|
]
|
||||||
|
ex.append("{}: {}".format(et.__name__, ev))
|
||||||
|
return "\n".join(ex)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def ren_open(fname, *args, **kwargs):
|
def ren_open(fname, *args, **kwargs):
|
||||||
fdir = kwargs.pop("fdir", None)
|
fdir = kwargs.pop("fdir", None)
|
||||||
@@ -222,6 +303,11 @@ def ren_open(fname, *args, **kwargs):
|
|||||||
yield {"orz": [f, fname]}
|
yield {"orz": [f, fname]}
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if suffix:
|
||||||
|
ext = fname.split(".")[-1]
|
||||||
|
if len(ext) < 7:
|
||||||
|
suffix += "." + ext
|
||||||
|
|
||||||
orig_name = fname
|
orig_name = fname
|
||||||
bname = fname
|
bname = fname
|
||||||
ext = ""
|
ext = ""
|
||||||
@@ -242,7 +328,7 @@ def ren_open(fname, *args, **kwargs):
|
|||||||
else:
|
else:
|
||||||
fpath = fname
|
fpath = fname
|
||||||
|
|
||||||
if suffix and os.path.exists(fpath):
|
if suffix and os.path.exists(fsenc(fpath)):
|
||||||
fpath += suffix
|
fpath += suffix
|
||||||
fname += suffix
|
fname += suffix
|
||||||
ext += suffix
|
ext += suffix
|
||||||
@@ -265,7 +351,7 @@ def ren_open(fname, *args, **kwargs):
|
|||||||
if not b64:
|
if not b64:
|
||||||
b64 = (bname + ext).encode("utf-8", "replace")
|
b64 = (bname + ext).encode("utf-8", "replace")
|
||||||
b64 = hashlib.sha512(b64).digest()[:12]
|
b64 = hashlib.sha512(b64).digest()[:12]
|
||||||
b64 = base64.urlsafe_b64encode(b64).decode("utf-8").rstrip("=")
|
b64 = base64.urlsafe_b64encode(b64).decode("utf-8")
|
||||||
|
|
||||||
badlen = len(fname)
|
badlen = len(fname)
|
||||||
while len(fname) >= badlen:
|
while len(fname) >= badlen:
|
||||||
@@ -521,8 +607,10 @@ def read_header(sr):
|
|||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
sr.unrecv(ret[ofs + 4 :])
|
if len(ret) > ofs + 4:
|
||||||
return ret[:ofs].decode("utf-8", "surrogateescape").split("\r\n")
|
sr.unrecv(ret[ofs + 4 :])
|
||||||
|
|
||||||
|
return ret[:ofs].decode("utf-8", "surrogateescape").lstrip("\r\n").split("\r\n")
|
||||||
|
|
||||||
|
|
||||||
def humansize(sz, terse=False):
|
def humansize(sz, terse=False):
|
||||||
@@ -560,6 +648,16 @@ def s2hms(s, optional_h=False):
|
|||||||
return "{}:{:02}:{:02}".format(h, m, s)
|
return "{}:{:02}:{:02}".format(h, m, s)
|
||||||
|
|
||||||
|
|
||||||
|
def uncyg(path):
|
||||||
|
if len(path) < 2 or not path.startswith("/"):
|
||||||
|
return path
|
||||||
|
|
||||||
|
if len(path) > 2 and path[2] != "/":
|
||||||
|
return path
|
||||||
|
|
||||||
|
return "{}:\\{}".format(path[1], path[3:])
|
||||||
|
|
||||||
|
|
||||||
def undot(path):
|
def undot(path):
|
||||||
ret = []
|
ret = []
|
||||||
for node in path.split("/"):
|
for node in path.split("/"):
|
||||||
@@ -576,7 +674,7 @@ def undot(path):
|
|||||||
return "/".join(ret)
|
return "/".join(ret)
|
||||||
|
|
||||||
|
|
||||||
def sanitize_fn(fn, ok=""):
|
def sanitize_fn(fn, ok="", bad=[]):
|
||||||
if "/" not in ok:
|
if "/" not in ok:
|
||||||
fn = fn.replace("\\", "/").split("/")[-1]
|
fn = fn.replace("\\", "/").split("/")[-1]
|
||||||
|
|
||||||
@@ -592,15 +690,15 @@ def sanitize_fn(fn, ok=""):
|
|||||||
["?", "?"],
|
["?", "?"],
|
||||||
["*", "*"],
|
["*", "*"],
|
||||||
]
|
]
|
||||||
for bad, good in [x for x in remap if x[0] not in ok]:
|
for a, b in [x for x in remap if x[0] not in ok]:
|
||||||
fn = fn.replace(bad, good)
|
fn = fn.replace(a, b)
|
||||||
|
|
||||||
bad = ["con", "prn", "aux", "nul"]
|
bad.extend(["con", "prn", "aux", "nul"])
|
||||||
for n in range(1, 10):
|
for n in range(1, 10):
|
||||||
bad += "com{0} lpt{0}".format(n).split(" ")
|
bad += "com{0} lpt{0}".format(n).split(" ")
|
||||||
|
|
||||||
if fn.lower() in bad:
|
if fn.lower() in bad:
|
||||||
fn = "_" + fn
|
fn = "_" + fn
|
||||||
|
|
||||||
return fn.strip()
|
return fn.strip()
|
||||||
|
|
||||||
@@ -616,6 +714,11 @@ def exclude_dotfiles(filepaths):
|
|||||||
return [x for x in filepaths if not x.split("/")[-1].startswith(".")]
|
return [x for x in filepaths if not x.split("/")[-1].startswith(".")]
|
||||||
|
|
||||||
|
|
||||||
|
def http_ts(ts):
|
||||||
|
file_dt = datetime.utcfromtimestamp(ts)
|
||||||
|
return file_dt.strftime(HTTP_TS_FMT)
|
||||||
|
|
||||||
|
|
||||||
def html_escape(s, quote=False, crlf=False):
|
def html_escape(s, quote=False, crlf=False):
|
||||||
"""html.escape but also newlines"""
|
"""html.escape but also newlines"""
|
||||||
s = s.replace("&", "&").replace("<", "<").replace(">", ">")
|
s = s.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||||
@@ -722,6 +825,8 @@ def s3dec(rd, fn):
|
|||||||
|
|
||||||
|
|
||||||
def atomic_move(src, dst):
|
def atomic_move(src, dst):
|
||||||
|
src = fsenc(src)
|
||||||
|
dst = fsenc(dst)
|
||||||
if not PY2:
|
if not PY2:
|
||||||
os.replace(src, dst)
|
os.replace(src, dst)
|
||||||
else:
|
else:
|
||||||
@@ -800,30 +905,36 @@ def yieldfile(fn):
|
|||||||
|
|
||||||
|
|
||||||
def hashcopy(actor, fin, fout):
|
def hashcopy(actor, fin, fout):
|
||||||
u32_lim = int((2 ** 31) * 0.9)
|
is_mp = actor.is_mp
|
||||||
hashobj = hashlib.sha512()
|
hashobj = hashlib.sha512()
|
||||||
tlen = 0
|
tlen = 0
|
||||||
for buf in fin:
|
for buf in fin:
|
||||||
actor.workload += 1
|
if is_mp:
|
||||||
if actor.workload > u32_lim:
|
actor.workload += 1
|
||||||
actor.workload = 100 # prevent overflow
|
if actor.workload > 2 ** 31:
|
||||||
|
actor.workload = 100
|
||||||
|
|
||||||
tlen += len(buf)
|
tlen += len(buf)
|
||||||
hashobj.update(buf)
|
hashobj.update(buf)
|
||||||
fout.write(buf)
|
fout.write(buf)
|
||||||
|
|
||||||
digest32 = hashobj.digest()[:32]
|
digest = hashobj.digest()[:33]
|
||||||
digest_b64 = base64.urlsafe_b64encode(digest32).decode("utf-8").rstrip("=")
|
digest_b64 = base64.urlsafe_b64encode(digest).decode("utf-8")
|
||||||
|
|
||||||
return tlen, hashobj.hexdigest(), digest_b64
|
return tlen, hashobj.hexdigest(), digest_b64
|
||||||
|
|
||||||
|
|
||||||
def sendfile_py(lower, upper, f, s):
|
def sendfile_py(lower, upper, f, s, actor=None):
|
||||||
remains = upper - lower
|
remains = upper - lower
|
||||||
f.seek(lower)
|
f.seek(lower)
|
||||||
while remains > 0:
|
while remains > 0:
|
||||||
|
if actor:
|
||||||
|
actor.workload += 1
|
||||||
|
if actor.workload > 2 ** 31:
|
||||||
|
actor.workload = 100
|
||||||
|
|
||||||
# time.sleep(0.01)
|
# time.sleep(0.01)
|
||||||
buf = f.read(min(4096, remains))
|
buf = f.read(min(1024 * 32, remains))
|
||||||
if not buf:
|
if not buf:
|
||||||
return remains
|
return remains
|
||||||
|
|
||||||
@@ -913,18 +1024,26 @@ def unescape_cookie(orig):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def guess_mime(url):
|
def guess_mime(url, fallback="application/octet-stream"):
|
||||||
if url.endswith(".md"):
|
try:
|
||||||
return ["text/plain; charset=UTF-8"]
|
_, ext = url.rsplit(".", 1)
|
||||||
|
except:
|
||||||
|
return fallback
|
||||||
|
|
||||||
return mimetypes.guess_type(url)
|
ret = MIMES.get(ext) or mimetypes.guess_type(url)[0] or fallback
|
||||||
|
|
||||||
|
if ";" not in ret:
|
||||||
|
if ret.startswith("text/") or ret.endswith("/javascript"):
|
||||||
|
ret += "; charset=UTF-8"
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def runcmd(*argv):
|
def runcmd(*argv):
|
||||||
p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE)
|
p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE)
|
||||||
stdout, stderr = p.communicate()
|
stdout, stderr = p.communicate()
|
||||||
stdout = stdout.decode("utf-8")
|
stdout = stdout.decode("utf-8", "replace")
|
||||||
stderr = stderr.decode("utf-8")
|
stderr = stderr.decode("utf-8", "replace")
|
||||||
return [p.returncode, stdout, stderr]
|
return [p.returncode, stdout, stderr]
|
||||||
|
|
||||||
|
|
||||||
@@ -936,6 +1055,17 @@ def chkcmd(*argv):
|
|||||||
return sout, serr
|
return sout, serr
|
||||||
|
|
||||||
|
|
||||||
|
def mchkcmd(argv, timeout=10):
|
||||||
|
if PY2:
|
||||||
|
with open(os.devnull, "wb") as f:
|
||||||
|
rv = sp.call(argv, stdout=f, stderr=f)
|
||||||
|
else:
|
||||||
|
rv = sp.call(argv, stdout=sp.DEVNULL, stderr=sp.DEVNULL, timeout=timeout)
|
||||||
|
|
||||||
|
if rv:
|
||||||
|
raise sp.CalledProcessError(rv, (argv[0], b"...", argv[-1]))
|
||||||
|
|
||||||
|
|
||||||
def gzip_orig_sz(fn):
|
def gzip_orig_sz(fn):
|
||||||
with open(fsenc(fn), "rb") as f:
|
with open(fsenc(fn), "rb") as f:
|
||||||
f.seek(-4, 2)
|
f.seek(-4, 2)
|
||||||
|
|||||||
583
copyparty/web/baguettebox.js
Normal file
583
copyparty/web/baguettebox.js
Normal file
@@ -0,0 +1,583 @@
|
|||||||
|
/*!
|
||||||
|
* baguetteBox.js
|
||||||
|
* @author feimosi
|
||||||
|
* @version 1.11.1-mod
|
||||||
|
* @url https://github.com/feimosi/baguetteBox.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
window.baguetteBox = (function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var options = {},
|
||||||
|
defaults = {
|
||||||
|
captions: true,
|
||||||
|
buttons: 'auto',
|
||||||
|
noScrollbars: false,
|
||||||
|
bodyClass: 'baguetteBox-open',
|
||||||
|
titleTag: false,
|
||||||
|
async: false,
|
||||||
|
preload: 2,
|
||||||
|
animation: 'slideIn',
|
||||||
|
afterShow: null,
|
||||||
|
afterHide: null,
|
||||||
|
onChange: null,
|
||||||
|
},
|
||||||
|
overlay, slider, previousButton, nextButton, closeButton,
|
||||||
|
currentGallery = [],
|
||||||
|
currentIndex = 0,
|
||||||
|
isOverlayVisible = false,
|
||||||
|
touch = {}, // start-pos
|
||||||
|
touchFlag = false, // busy
|
||||||
|
regex = /.+\.(gif|jpe?g|png|webp)/i,
|
||||||
|
data = {}, // all galleries
|
||||||
|
imagesElements = [],
|
||||||
|
documentLastFocus = null;
|
||||||
|
|
||||||
|
var overlayClickHandler = function (event) {
|
||||||
|
if (event.target.id.indexOf('baguette-img') !== -1) {
|
||||||
|
hideOverlay();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var touchstartHandler = function (event) {
|
||||||
|
touch.count++;
|
||||||
|
if (touch.count > 1) {
|
||||||
|
touch.multitouch = true;
|
||||||
|
}
|
||||||
|
touch.startX = event.changedTouches[0].pageX;
|
||||||
|
touch.startY = event.changedTouches[0].pageY;
|
||||||
|
};
|
||||||
|
var touchmoveHandler = function (event) {
|
||||||
|
if (touchFlag || touch.multitouch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault ? event.preventDefault() : event.returnValue = false;
|
||||||
|
var touchEvent = event.touches[0] || event.changedTouches[0];
|
||||||
|
if (touchEvent.pageX - touch.startX > 40) {
|
||||||
|
touchFlag = true;
|
||||||
|
showPreviousImage();
|
||||||
|
} else if (touchEvent.pageX - touch.startX < -40) {
|
||||||
|
touchFlag = true;
|
||||||
|
showNextImage();
|
||||||
|
} else if (touch.startY - touchEvent.pageY > 100) {
|
||||||
|
hideOverlay();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var touchendHandler = function () {
|
||||||
|
touch.count--;
|
||||||
|
if (touch.count <= 0) {
|
||||||
|
touch.multitouch = false;
|
||||||
|
}
|
||||||
|
touchFlag = false;
|
||||||
|
};
|
||||||
|
var contextmenuHandler = function () {
|
||||||
|
touchendHandler();
|
||||||
|
};
|
||||||
|
|
||||||
|
var trapFocusInsideOverlay = function (event) {
|
||||||
|
if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(event.target))) {
|
||||||
|
event.stopPropagation();
|
||||||
|
initFocus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function run(selector, userOptions) {
|
||||||
|
buildOverlay();
|
||||||
|
removeFromCache(selector);
|
||||||
|
return bindImageClickListeners(selector, userOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindImageClickListeners(selector, userOptions) {
|
||||||
|
var galleryNodeList = document.querySelectorAll(selector);
|
||||||
|
var selectorData = {
|
||||||
|
galleries: [],
|
||||||
|
nodeList: galleryNodeList
|
||||||
|
};
|
||||||
|
data[selector] = selectorData;
|
||||||
|
|
||||||
|
[].forEach.call(galleryNodeList, function (galleryElement) {
|
||||||
|
if (userOptions && userOptions.filter) {
|
||||||
|
regex = userOptions.filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tagsNodeList = [];
|
||||||
|
if (galleryElement.tagName === 'A') {
|
||||||
|
tagsNodeList = [galleryElement];
|
||||||
|
} else {
|
||||||
|
tagsNodeList = galleryElement.getElementsByTagName('a');
|
||||||
|
}
|
||||||
|
|
||||||
|
tagsNodeList = [].filter.call(tagsNodeList, function (element) {
|
||||||
|
if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1) {
|
||||||
|
return regex.test(element.href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (tagsNodeList.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gallery = [];
|
||||||
|
[].forEach.call(tagsNodeList, function (imageElement, imageIndex) {
|
||||||
|
var imageElementClickHandler = function (event) {
|
||||||
|
if (event && (event.ctrlKey || event.metaKey))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
event.preventDefault ? event.preventDefault() : event.returnValue = false;
|
||||||
|
prepareOverlay(gallery, userOptions);
|
||||||
|
showOverlay(imageIndex);
|
||||||
|
};
|
||||||
|
var imageItem = {
|
||||||
|
eventHandler: imageElementClickHandler,
|
||||||
|
imageElement: imageElement
|
||||||
|
};
|
||||||
|
bind(imageElement, 'click', imageElementClickHandler);
|
||||||
|
gallery.push(imageItem);
|
||||||
|
});
|
||||||
|
selectorData.galleries.push(gallery);
|
||||||
|
});
|
||||||
|
|
||||||
|
return selectorData.galleries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCachedData() {
|
||||||
|
for (var selector in data) {
|
||||||
|
if (data.hasOwnProperty(selector)) {
|
||||||
|
removeFromCache(selector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFromCache(selector) {
|
||||||
|
if (!data.hasOwnProperty(selector)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var galleries = data[selector].galleries;
|
||||||
|
[].forEach.call(galleries, function (gallery) {
|
||||||
|
[].forEach.call(gallery, function (imageItem) {
|
||||||
|
unbind(imageItem.imageElement, 'click', imageItem.eventHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentGallery === gallery) {
|
||||||
|
currentGallery = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
delete data[selector];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOverlay() {
|
||||||
|
overlay = ebi('baguetteBox-overlay');
|
||||||
|
if (overlay) {
|
||||||
|
slider = ebi('baguetteBox-slider');
|
||||||
|
previousButton = ebi('previous-button');
|
||||||
|
nextButton = ebi('next-button');
|
||||||
|
closeButton = ebi('close-button');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
overlay = mknod('div');
|
||||||
|
overlay.setAttribute('role', 'dialog');
|
||||||
|
overlay.id = 'baguetteBox-overlay';
|
||||||
|
document.getElementsByTagName('body')[0].appendChild(overlay);
|
||||||
|
|
||||||
|
slider = mknod('div');
|
||||||
|
slider.id = 'baguetteBox-slider';
|
||||||
|
overlay.appendChild(slider);
|
||||||
|
|
||||||
|
previousButton = mknod('button');
|
||||||
|
previousButton.setAttribute('type', 'button');
|
||||||
|
previousButton.id = 'previous-button';
|
||||||
|
previousButton.setAttribute('aria-label', 'Previous');
|
||||||
|
previousButton.innerHTML = '<';
|
||||||
|
overlay.appendChild(previousButton);
|
||||||
|
|
||||||
|
nextButton = mknod('button');
|
||||||
|
nextButton.setAttribute('type', 'button');
|
||||||
|
nextButton.id = 'next-button';
|
||||||
|
nextButton.setAttribute('aria-label', 'Next');
|
||||||
|
nextButton.innerHTML = '>';
|
||||||
|
overlay.appendChild(nextButton);
|
||||||
|
|
||||||
|
closeButton = mknod('button');
|
||||||
|
closeButton.setAttribute('type', 'button');
|
||||||
|
closeButton.id = 'close-button';
|
||||||
|
closeButton.setAttribute('aria-label', 'Close');
|
||||||
|
closeButton.innerHTML = '×';
|
||||||
|
overlay.appendChild(closeButton);
|
||||||
|
|
||||||
|
previousButton.className = nextButton.className = closeButton.className = 'baguetteBox-button';
|
||||||
|
|
||||||
|
bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyDownHandler(event) {
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case 37: // Left
|
||||||
|
showPreviousImage();
|
||||||
|
break;
|
||||||
|
case 39: // Right
|
||||||
|
showNextImage();
|
||||||
|
break;
|
||||||
|
case 27: // Esc
|
||||||
|
hideOverlay();
|
||||||
|
break;
|
||||||
|
case 36: // Home
|
||||||
|
showFirstImage(event);
|
||||||
|
break;
|
||||||
|
case 35: // End
|
||||||
|
showLastImage(event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var passiveSupp = false;
|
||||||
|
try {
|
||||||
|
var opts = {
|
||||||
|
get passive() {
|
||||||
|
passiveSupp = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('test', null, opts);
|
||||||
|
window.removeEventListener('test', null, opts);
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
passiveSupp = false;
|
||||||
|
}
|
||||||
|
var passiveEvent = passiveSupp ? { passive: false } : null;
|
||||||
|
var nonPassiveEvent = passiveSupp ? { passive: true } : null;
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
bind(overlay, 'click', overlayClickHandler);
|
||||||
|
bind(previousButton, 'click', showPreviousImage);
|
||||||
|
bind(nextButton, 'click', showNextImage);
|
||||||
|
bind(closeButton, 'click', hideOverlay);
|
||||||
|
bind(slider, 'contextmenu', contextmenuHandler);
|
||||||
|
bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||||
|
bind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||||
|
bind(overlay, 'touchend', touchendHandler);
|
||||||
|
bind(document, 'focus', trapFocusInsideOverlay, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unbindEvents() {
|
||||||
|
unbind(overlay, 'click', overlayClickHandler);
|
||||||
|
unbind(previousButton, 'click', showPreviousImage);
|
||||||
|
unbind(nextButton, 'click', showNextImage);
|
||||||
|
unbind(closeButton, 'click', hideOverlay);
|
||||||
|
unbind(slider, 'contextmenu', contextmenuHandler);
|
||||||
|
unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||||
|
unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||||
|
unbind(overlay, 'touchend', touchendHandler);
|
||||||
|
unbind(document, 'focus', trapFocusInsideOverlay, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareOverlay(gallery, userOptions) {
|
||||||
|
if (currentGallery === gallery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentGallery = gallery;
|
||||||
|
setOptions(userOptions);
|
||||||
|
slider.innerHTML = '';
|
||||||
|
imagesElements.length = 0;
|
||||||
|
|
||||||
|
var imagesFiguresIds = [];
|
||||||
|
var imagesCaptionsIds = [];
|
||||||
|
for (var i = 0, fullImage; i < gallery.length; i++) {
|
||||||
|
fullImage = mknod('div');
|
||||||
|
fullImage.className = 'full-image';
|
||||||
|
fullImage.id = 'baguette-img-' + i;
|
||||||
|
imagesElements.push(fullImage);
|
||||||
|
|
||||||
|
imagesFiguresIds.push('baguetteBox-figure-' + i);
|
||||||
|
imagesCaptionsIds.push('baguetteBox-figcaption-' + i);
|
||||||
|
slider.appendChild(imagesElements[i]);
|
||||||
|
}
|
||||||
|
overlay.setAttribute('aria-labelledby', imagesFiguresIds.join(' '));
|
||||||
|
overlay.setAttribute('aria-describedby', imagesCaptionsIds.join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOptions(newOptions) {
|
||||||
|
if (!newOptions) {
|
||||||
|
newOptions = {};
|
||||||
|
}
|
||||||
|
for (var item in defaults) {
|
||||||
|
options[item] = defaults[item];
|
||||||
|
if (typeof newOptions[item] !== 'undefined') {
|
||||||
|
options[item] = newOptions[item];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slider.style.transition = (options.animation === 'fadeIn' ? 'opacity .4s ease' :
|
||||||
|
options.animation === 'slideIn' ? '' : 'none');
|
||||||
|
|
||||||
|
if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1)) {
|
||||||
|
options.buttons = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousButton.style.display = nextButton.style.display = (options.buttons ? '' : 'none');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showOverlay(chosenImageIndex) {
|
||||||
|
if (options.noScrollbars) {
|
||||||
|
document.documentElement.style.overflowY = 'hidden';
|
||||||
|
document.body.style.overflowY = 'scroll';
|
||||||
|
}
|
||||||
|
if (overlay.style.display === 'block') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bind(document, 'keydown', keyDownHandler);
|
||||||
|
currentIndex = chosenImageIndex;
|
||||||
|
touch = {
|
||||||
|
count: 0,
|
||||||
|
startX: null,
|
||||||
|
startY: null
|
||||||
|
};
|
||||||
|
loadImage(currentIndex, function () {
|
||||||
|
preloadNext(currentIndex);
|
||||||
|
preloadPrev(currentIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateOffset();
|
||||||
|
overlay.style.display = 'block';
|
||||||
|
// Fade in overlay
|
||||||
|
setTimeout(function () {
|
||||||
|
overlay.className = 'visible';
|
||||||
|
if (options.bodyClass && document.body.classList) {
|
||||||
|
document.body.classList.add(options.bodyClass);
|
||||||
|
}
|
||||||
|
if (options.afterShow) {
|
||||||
|
options.afterShow();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
if (options.onChange) {
|
||||||
|
options.onChange(currentIndex, imagesElements.length);
|
||||||
|
}
|
||||||
|
documentLastFocus = document.activeElement;
|
||||||
|
initFocus();
|
||||||
|
isOverlayVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initFocus() {
|
||||||
|
if (options.buttons) {
|
||||||
|
previousButton.focus();
|
||||||
|
} else {
|
||||||
|
closeButton.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideOverlay(e) {
|
||||||
|
ev(e);
|
||||||
|
if (options.noScrollbars) {
|
||||||
|
document.documentElement.style.overflowY = 'auto';
|
||||||
|
document.body.style.overflowY = 'auto';
|
||||||
|
}
|
||||||
|
if (overlay.style.display === 'none') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unbind(document, 'keydown', keyDownHandler);
|
||||||
|
// Fade out and hide the overlay
|
||||||
|
overlay.className = '';
|
||||||
|
setTimeout(function () {
|
||||||
|
overlay.style.display = 'none';
|
||||||
|
if (options.bodyClass && document.body.classList) {
|
||||||
|
document.body.classList.remove(options.bodyClass);
|
||||||
|
}
|
||||||
|
if (options.afterHide) {
|
||||||
|
options.afterHide();
|
||||||
|
}
|
||||||
|
documentLastFocus && documentLastFocus.focus();
|
||||||
|
isOverlayVisible = false;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadImage(index, callback) {
|
||||||
|
var imageContainer = imagesElements[index];
|
||||||
|
var galleryItem = currentGallery[index];
|
||||||
|
|
||||||
|
if (typeof imageContainer === 'undefined' || typeof galleryItem === 'undefined') {
|
||||||
|
return; // out-of-bounds or gallery dirty
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageContainer.getElementsByTagName('img')[0]) {
|
||||||
|
// image is loaded, cb and bail
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageElement = galleryItem.imageElement,
|
||||||
|
imageSrc = imageElement.href,
|
||||||
|
thumbnailElement = imageElement.getElementsByTagName('img')[0],
|
||||||
|
imageCaption = typeof options.captions === 'function' ?
|
||||||
|
options.captions.call(currentGallery, imageElement) :
|
||||||
|
imageElement.getAttribute('data-caption') || imageElement.title;
|
||||||
|
|
||||||
|
var figure = mknod('figure');
|
||||||
|
figure.id = 'baguetteBox-figure-' + index;
|
||||||
|
figure.innerHTML = '<div class="baguetteBox-spinner">' +
|
||||||
|
'<div class="baguetteBox-double-bounce1"></div>' +
|
||||||
|
'<div class="baguetteBox-double-bounce2"></div>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
if (options.captions && imageCaption) {
|
||||||
|
var figcaption = mknod('figcaption');
|
||||||
|
figcaption.id = 'baguetteBox-figcaption-' + index;
|
||||||
|
figcaption.innerHTML = imageCaption;
|
||||||
|
figure.appendChild(figcaption);
|
||||||
|
}
|
||||||
|
imageContainer.appendChild(figure);
|
||||||
|
|
||||||
|
var image = mknod('img');
|
||||||
|
image.onload = function () {
|
||||||
|
// Remove loader element
|
||||||
|
var spinner = document.querySelector('#baguette-img-' + index + ' .baguetteBox-spinner');
|
||||||
|
figure.removeChild(spinner);
|
||||||
|
if (!options.async && callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
image.setAttribute('src', imageSrc);
|
||||||
|
image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
|
||||||
|
if (options.titleTag && imageCaption) {
|
||||||
|
image.title = imageCaption;
|
||||||
|
}
|
||||||
|
figure.appendChild(image);
|
||||||
|
|
||||||
|
if (options.async && callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNextImage(e) {
|
||||||
|
ev(e);
|
||||||
|
return show(currentIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPreviousImage(e) {
|
||||||
|
ev(e);
|
||||||
|
return show(currentIndex - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFirstImage(event) {
|
||||||
|
if (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
return show(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLastImage(event) {
|
||||||
|
if (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
return show(currentGallery.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the gallery to a specific index
|
||||||
|
* @param `index` {number} - the position of the image
|
||||||
|
* @param `gallery` {array} - gallery which should be opened, if omitted assumes the currently opened one
|
||||||
|
* @return {boolean} - true on success or false if the index is invalid
|
||||||
|
*/
|
||||||
|
function show(index, gallery) {
|
||||||
|
if (!isOverlayVisible && index >= 0 && index < gallery.length) {
|
||||||
|
prepareOverlay(gallery, options);
|
||||||
|
showOverlay(index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (index < 0) {
|
||||||
|
if (options.animation) {
|
||||||
|
bounceAnimation('left');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (index >= imagesElements.length) {
|
||||||
|
if (options.animation) {
|
||||||
|
bounceAnimation('right');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex = index;
|
||||||
|
loadImage(currentIndex, function () {
|
||||||
|
preloadNext(currentIndex);
|
||||||
|
preloadPrev(currentIndex);
|
||||||
|
});
|
||||||
|
updateOffset();
|
||||||
|
|
||||||
|
if (options.onChange) {
|
||||||
|
options.onChange(currentIndex, imagesElements.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers the bounce animation
|
||||||
|
* @param {('left'|'right')} direction - Direction of the movement
|
||||||
|
*/
|
||||||
|
function bounceAnimation(direction) {
|
||||||
|
slider.className = 'bounce-from-' + direction;
|
||||||
|
setTimeout(function () {
|
||||||
|
slider.className = '';
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOffset() {
|
||||||
|
var offset = -currentIndex * 100 + '%';
|
||||||
|
if (options.animation === 'fadeIn') {
|
||||||
|
slider.style.opacity = 0;
|
||||||
|
setTimeout(function () {
|
||||||
|
slider.style.transform = 'translate3d(' + offset + ',0,0)';
|
||||||
|
slider.style.opacity = 1;
|
||||||
|
}, 400);
|
||||||
|
} else {
|
||||||
|
slider.style.transform = 'translate3d(' + offset + ',0,0)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function preloadNext(index) {
|
||||||
|
if (index - currentIndex >= options.preload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadImage(index + 1, function () {
|
||||||
|
preloadNext(index + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function preloadPrev(index) {
|
||||||
|
if (currentIndex - index >= options.preload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadImage(index - 1, function () {
|
||||||
|
preloadPrev(index - 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bind(element, event, callback, options) {
|
||||||
|
element.addEventListener(event, callback, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unbind(element, event, callback, options) {
|
||||||
|
element.removeEventListener(event, callback, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyPlugin() {
|
||||||
|
unbindEvents();
|
||||||
|
clearCachedData();
|
||||||
|
unbind(document, 'keydown', keyDownHandler);
|
||||||
|
document.getElementsByTagName('body')[0].removeChild(ebi('baguetteBox-overlay'));
|
||||||
|
data = {};
|
||||||
|
currentGallery = [];
|
||||||
|
currentIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
run: run,
|
||||||
|
show: show,
|
||||||
|
showNext: showNextImage,
|
||||||
|
showPrevious: showPreviousImage,
|
||||||
|
hide: hideOverlay,
|
||||||
|
destroy: destroyPlugin
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
:root {
|
||||||
|
--grid-sz: 10em;
|
||||||
|
}
|
||||||
* {
|
* {
|
||||||
line-height: 1.2em;
|
line-height: 1.2em;
|
||||||
}
|
}
|
||||||
@@ -22,6 +25,35 @@ html, body {
|
|||||||
body {
|
body {
|
||||||
padding-bottom: 5em;
|
padding-bottom: 5em;
|
||||||
}
|
}
|
||||||
|
#tt {
|
||||||
|
position: fixed;
|
||||||
|
max-width: 34em;
|
||||||
|
background: #222;
|
||||||
|
border: 0 solid #555;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 1em;
|
||||||
|
padding: 0 1em;
|
||||||
|
height: 0;
|
||||||
|
opacity: .1;
|
||||||
|
transition: opacity 0.14s, height 0.14s, padding 0.14s;
|
||||||
|
box-shadow: 0 .2em .5em #222;
|
||||||
|
border-radius: .4em;
|
||||||
|
z-index: 9001;
|
||||||
|
}
|
||||||
|
#tt.show {
|
||||||
|
padding: 1em;
|
||||||
|
height: auto;
|
||||||
|
border-width: .2em 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
#tt code {
|
||||||
|
background: #3c3c3c;
|
||||||
|
padding: .2em .3em;
|
||||||
|
border-top: 1px solid #777;
|
||||||
|
border-radius: .3em;
|
||||||
|
font-family: monospace, monospace;
|
||||||
|
line-height: 2em;
|
||||||
|
}
|
||||||
#path,
|
#path,
|
||||||
#path * {
|
#path * {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
@@ -50,6 +82,7 @@ body {
|
|||||||
#files tbody a {
|
#files tbody a {
|
||||||
display: block;
|
display: block;
|
||||||
padding: .3em 0;
|
padding: .3em 0;
|
||||||
|
scroll-margin-top: 45vh;
|
||||||
}
|
}
|
||||||
#files tbody div a {
|
#files tbody div a {
|
||||||
color: #f5a;
|
color: #f5a;
|
||||||
@@ -64,33 +97,38 @@ a, #files tbody div a:last-child {
|
|||||||
background: #161616;
|
background: #161616;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
#files thead {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
#files thead a {
|
#files thead a {
|
||||||
color: #999;
|
color: #999;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
#files tr:hover {
|
#files tr:hover td {
|
||||||
background: #1c1c1c;
|
background: #1c1c1c;
|
||||||
}
|
}
|
||||||
#files thead th {
|
#files thead th {
|
||||||
padding: .5em 1.3em .3em 1.3em;
|
padding: .5em .3em .3em .3em;
|
||||||
|
border-right: 2px solid #3c3c3c;
|
||||||
|
border-bottom: 2px solid #444;
|
||||||
|
background: #333;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
#files thead th+th {
|
||||||
|
border-left: 2px solid #2a2a2a;
|
||||||
|
}
|
||||||
#files thead th:last-child {
|
#files thead th:last-child {
|
||||||
background: #444;
|
border-right: none;
|
||||||
border-radius: .7em .7em 0 0;
|
|
||||||
}
|
}
|
||||||
#files thead th:first-child {
|
#files tbody {
|
||||||
background: #222;
|
background: #222;
|
||||||
}
|
}
|
||||||
#files tbody,
|
|
||||||
#files thead th:nth-child(2) {
|
|
||||||
background: #222;
|
|
||||||
border-radius: 0 .7em 0 0;
|
|
||||||
}
|
|
||||||
#files td {
|
#files td {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 .5em;
|
padding: 0 .5em;
|
||||||
border-bottom: 1px solid #111;
|
border-bottom: 1px solid #111;
|
||||||
|
border-left: 1px solid #2c2c2c;
|
||||||
}
|
}
|
||||||
#files td+td+td {
|
#files td+td+td {
|
||||||
max-width: 30em;
|
max-width: 30em;
|
||||||
@@ -177,14 +215,49 @@ a, #files tbody div a:last-child {
|
|||||||
margin: -.2em;
|
margin: -.2em;
|
||||||
}
|
}
|
||||||
#files tbody a.play.act {
|
#files tbody a.play.act {
|
||||||
color: #840;
|
color: #720;
|
||||||
text-shadow: 0 0 .3em #b80;
|
text-shadow: 0 0 .3em #b80;
|
||||||
}
|
}
|
||||||
#files tbody tr.sel td {
|
#ggrid a.play,
|
||||||
|
html.light #ggrid a.play {
|
||||||
|
color: #fff;
|
||||||
|
background: #750;
|
||||||
|
border-color: #c90;
|
||||||
|
border-top: 1px solid #da4;
|
||||||
|
box-shadow: 0 .1em 1.2em #b83;
|
||||||
|
}
|
||||||
|
#files tbody tr.sel td,
|
||||||
|
#ggrid a.sel,
|
||||||
|
html.light #ggrid a.sel {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: #925;
|
background: #925;
|
||||||
border-color: #c37;
|
border-color: #c37;
|
||||||
}
|
}
|
||||||
|
#files tbody tr.sel:hover td,
|
||||||
|
#ggrid a.sel:hover,
|
||||||
|
html.light #ggrid a.sel:hover {
|
||||||
|
color: #fff;
|
||||||
|
background: #d39;
|
||||||
|
border-color: #d48;
|
||||||
|
text-shadow: 1px 1px 0 #804;
|
||||||
|
}
|
||||||
|
#ggrid a.sel,
|
||||||
|
html.light #ggrid a.sel {
|
||||||
|
border-top: 1px solid #d48;
|
||||||
|
box-shadow: 0 .1em 1.2em #b36;
|
||||||
|
transition: all 0.2s cubic-bezier(.2, 2.2, .5, 1); /* https://cubic-bezier.com/#.4,2,.7,1 */
|
||||||
|
}
|
||||||
|
#ggrid a.sel img,
|
||||||
|
#ggrid a.play img {
|
||||||
|
opacity: .7;
|
||||||
|
filter: contrast(130%) brightness(107%);
|
||||||
|
}
|
||||||
|
#ggrid a.sel img {
|
||||||
|
box-shadow: 0 0 1em #b36;
|
||||||
|
}
|
||||||
|
#ggrid a.play img {
|
||||||
|
box-shadow: 0 0 1em #b83;
|
||||||
|
}
|
||||||
#files tr.sel a {
|
#files tr.sel a {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
@@ -238,6 +311,7 @@ a, #files tbody div a:last-child {
|
|||||||
height: 6em;
|
height: 6em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
touch-action: none;
|
||||||
transition: bottom 0.15s;
|
transition: bottom 0.15s;
|
||||||
}
|
}
|
||||||
#widget.open {
|
#widget.open {
|
||||||
@@ -252,7 +326,10 @@ a, #files tbody div a:last-child {
|
|||||||
background: #3c3c3c;
|
background: #3c3c3c;
|
||||||
}
|
}
|
||||||
#wtico {
|
#wtico {
|
||||||
cursor: url(/.cpr/dd/1.png), pointer;
|
cursor: url(/.cpr/dd/4.png), pointer;
|
||||||
|
animation: cursor 500ms;
|
||||||
|
}
|
||||||
|
#wtico:hover {
|
||||||
animation: cursor 500ms infinite;
|
animation: cursor 500ms infinite;
|
||||||
}
|
}
|
||||||
@keyframes cursor {
|
@keyframes cursor {
|
||||||
@@ -260,7 +337,7 @@ a, #files tbody div a:last-child {
|
|||||||
30% {cursor: url(/.cpr/dd/3.png), pointer}
|
30% {cursor: url(/.cpr/dd/3.png), pointer}
|
||||||
50% {cursor: url(/.cpr/dd/4.png), pointer}
|
50% {cursor: url(/.cpr/dd/4.png), pointer}
|
||||||
75% {cursor: url(/.cpr/dd/5.png), pointer}
|
75% {cursor: url(/.cpr/dd/5.png), pointer}
|
||||||
85% {cursor: url(/.cpr/dd/1.png), pointer}
|
85% {cursor: url(/.cpr/dd/4.png), pointer}
|
||||||
}
|
}
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
100% {transform: rotate(360deg)}
|
100% {transform: rotate(360deg)}
|
||||||
@@ -281,29 +358,48 @@ a, #files tbody div a:last-child {
|
|||||||
padding: .2em 0 0 .07em;
|
padding: .2em 0 0 .07em;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
#wzip {
|
#wzip, #wnp {
|
||||||
display: none;
|
display: none;
|
||||||
margin-right: .3em;
|
margin-right: .3em;
|
||||||
padding-right: .3em;
|
padding-right: .3em;
|
||||||
border-right: .1em solid #555;
|
border-right: .1em solid #555;
|
||||||
}
|
}
|
||||||
|
#wnp a {
|
||||||
|
position: relative;
|
||||||
|
font-size: .47em;
|
||||||
|
margin: 0 .1em;
|
||||||
|
top: -.4em;
|
||||||
|
}
|
||||||
|
#wnp a+a {
|
||||||
|
margin-left: .33em;
|
||||||
|
}
|
||||||
#wtoggle,
|
#wtoggle,
|
||||||
#wtoggle * {
|
#wtoggle * {
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
}
|
}
|
||||||
|
#wtoggle.np {
|
||||||
|
width: 5.5em;
|
||||||
|
}
|
||||||
#wtoggle.sel {
|
#wtoggle.sel {
|
||||||
width: 6.4em;
|
width: 6.4em;
|
||||||
}
|
}
|
||||||
#wtoggle.sel #wzip {
|
#wtoggle.sel #wzip,
|
||||||
|
#wtoggle.np #wnp {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
#wtoggle.sel #wzip a {
|
#wtoggle.sel.np #wnp {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#wzip a {
|
||||||
font-size: .4em;
|
font-size: .4em;
|
||||||
padding: 0 .3em;
|
padding: 0 .3em;
|
||||||
margin: -.3em .2em;
|
margin: -.3em .2em;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
#wzip a+a {
|
||||||
|
margin-left: .8em;
|
||||||
|
}
|
||||||
#wtoggle.sel #wzip #selzip {
|
#wtoggle.sel #wzip #selzip {
|
||||||
top: -.6em;
|
top: -.6em;
|
||||||
padding: .4em .3em;
|
padding: .4em .3em;
|
||||||
@@ -418,6 +514,7 @@ a, #files tbody div a:last-child {
|
|||||||
padding: .3em .6em;
|
padding: .3em .6em;
|
||||||
border-radius: .3em;
|
border-radius: .3em;
|
||||||
border-width: .15em 0;
|
border-width: .15em 0;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.opbox {
|
.opbox {
|
||||||
background: #2d2d2d;
|
background: #2d2d2d;
|
||||||
@@ -431,20 +528,56 @@ a, #files tbody div a:last-child {
|
|||||||
margin: .5em;
|
margin: .5em;
|
||||||
}
|
}
|
||||||
.opview input[type=text] {
|
.opview input[type=text] {
|
||||||
color: #fff;
|
|
||||||
background: #383838;
|
background: #383838;
|
||||||
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: 0 0 .3em #222;
|
box-shadow: 0 0 .3em #222;
|
||||||
border-bottom: 1px solid #fc5;
|
border-bottom: 1px solid #fc5;
|
||||||
border-radius: .2em;
|
border-radius: .2em;
|
||||||
padding: .2em .3em;
|
padding: .2em .3em;
|
||||||
}
|
}
|
||||||
|
.opview input.err,
|
||||||
|
html.light .opview input[type="text"].err {
|
||||||
|
color: #fff;
|
||||||
|
background: #a20;
|
||||||
|
border-color: #f00;
|
||||||
|
box-shadow: 0 0 .7em #f00;
|
||||||
|
text-shadow: 1px 1px 0 #500;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
input[type="checkbox"]+label {
|
input[type="checkbox"]+label {
|
||||||
color: #f5a;
|
color: #f5a;
|
||||||
}
|
}
|
||||||
input[type="checkbox"]:checked+label {
|
input[type="checkbox"]:checked+label {
|
||||||
color: #fc5;
|
color: #fc5;
|
||||||
}
|
}
|
||||||
|
input[type="radio"]:checked+label {
|
||||||
|
color: #fc0;
|
||||||
|
}
|
||||||
|
html.light input[type="radio"]:checked+label {
|
||||||
|
color: #07c;
|
||||||
|
}
|
||||||
|
input.eq_gain {
|
||||||
|
width: 3em;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 .6em;
|
||||||
|
}
|
||||||
|
#audio_eq table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
#audio_eq td {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#audio_eq a.eq_step {
|
||||||
|
font-size: 1.5em;
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
#au_eq {
|
||||||
|
display: block;
|
||||||
|
margin-top: .5em;
|
||||||
|
padding: 1.3em .3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -474,9 +607,20 @@ input[type="checkbox"]:checked+label {
|
|||||||
#srch_q {
|
#srch_q {
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
color: #f80;
|
color: #f80;
|
||||||
height: 1em;
|
min-height: 1em;
|
||||||
margin: .2em 0 -1em 1.6em;
|
margin: .2em 0 -1em 1.6em;
|
||||||
}
|
}
|
||||||
|
#tq_raw {
|
||||||
|
width: calc(100% - 2em);
|
||||||
|
margin: .3em 0 0 1.4em;
|
||||||
|
}
|
||||||
|
#tq_raw td+td {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#op_search #q_raw {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
#files td div span {
|
#files td div span {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 0 .4em;
|
padding: 0 .4em;
|
||||||
@@ -500,6 +644,7 @@ input[type="checkbox"]:checked+label {
|
|||||||
}
|
}
|
||||||
#wrap {
|
#wrap {
|
||||||
margin-top: 2em;
|
margin-top: 2em;
|
||||||
|
min-height: 90vh;
|
||||||
}
|
}
|
||||||
#tree {
|
#tree {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -507,14 +652,20 @@ input[type="checkbox"]:checked+label {
|
|||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
top: 7em;
|
top: 7em;
|
||||||
padding-top: .2em;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
-ms-scroll-chaining: none;
|
-ms-scroll-chaining: none;
|
||||||
overscroll-behavior-y: none;
|
overscroll-behavior-y: none;
|
||||||
scrollbar-color: #eb0 #333;
|
scrollbar-color: #eb0 #333;
|
||||||
}
|
}
|
||||||
|
#treeh {
|
||||||
|
background: #333;
|
||||||
|
position: sticky;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
#thx_ff {
|
#thx_ff {
|
||||||
padding: 5em 0;
|
padding: 5em 0;
|
||||||
|
/* widget */
|
||||||
}
|
}
|
||||||
#tree::-webkit-scrollbar-track,
|
#tree::-webkit-scrollbar-track,
|
||||||
#tree::-webkit-scrollbar {
|
#tree::-webkit-scrollbar {
|
||||||
@@ -531,23 +682,21 @@ input[type="checkbox"]:checked+label {
|
|||||||
left: -1.7em;
|
left: -1.7em;
|
||||||
width: calc(100% + 1.3em);
|
width: calc(100% + 1.3em);
|
||||||
}
|
}
|
||||||
.tglbtn,
|
.btn {
|
||||||
#tree>a+a {
|
|
||||||
padding: .2em .4em;
|
padding: .2em .4em;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
background: #2a2a2a;
|
background: #2a2a2a;
|
||||||
box-shadow: 0 .1em .2em #222 inset;
|
box-shadow: 0 .1em .2em #222 inset;
|
||||||
border-radius: .3em;
|
border-radius: .3em;
|
||||||
margin: .2em;
|
margin: .2em;
|
||||||
|
white-space: pre;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -.2em;
|
top: -.2em;
|
||||||
}
|
}
|
||||||
.tglbtn:hover,
|
.btn:hover {
|
||||||
#tree>a+a:hover {
|
|
||||||
background: #805;
|
background: #805;
|
||||||
}
|
}
|
||||||
.tglbtn.on,
|
.tgl.btn.on {
|
||||||
#tree>a+a.on {
|
|
||||||
background: #fc4;
|
background: #fc4;
|
||||||
color: #400;
|
color: #400;
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
@@ -555,6 +704,7 @@ input[type="checkbox"]:checked+label {
|
|||||||
#detree {
|
#detree {
|
||||||
padding: .3em .5em;
|
padding: .3em .5em;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
#tree ul,
|
#tree ul,
|
||||||
#tree li {
|
#tree li {
|
||||||
@@ -576,15 +726,14 @@ input[type="checkbox"]:checked+label {
|
|||||||
#treeul a.hl {
|
#treeul a.hl {
|
||||||
color: #400;
|
color: #400;
|
||||||
background: #fc4;
|
background: #fc4;
|
||||||
border-radius: .3em;
|
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
#treeul a {
|
#treeul a {
|
||||||
|
border-radius: .3em;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
#treeul a+a {
|
#treeul a+a {
|
||||||
width: calc(100% - 2em);
|
width: calc(100% - 2em);
|
||||||
background: #333;
|
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
}
|
}
|
||||||
#treeul a+a:hover {
|
#treeul a+a:hover {
|
||||||
@@ -608,34 +757,20 @@ input[type="checkbox"]:checked+label {
|
|||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
#files th:hover .cfg,
|
#files th:hover .cfg {
|
||||||
#files th.min .cfg {
|
|
||||||
display: block;
|
display: block;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
border-radius: .2em;
|
border-radius: .2em;
|
||||||
margin: -1.3em auto 0 auto;
|
margin: -1.3em auto 0 auto;
|
||||||
background: #444;
|
background: #444;
|
||||||
}
|
}
|
||||||
#files th.min .cfg {
|
#files>thead>tr>th.min,
|
||||||
margin: -.6em;
|
#files td.min {
|
||||||
}
|
display: none;
|
||||||
#files>thead>tr>th.min span {
|
|
||||||
position: absolute;
|
|
||||||
transform: rotate(270deg);
|
|
||||||
background: linear-gradient(90deg, rgba(68,68,68,0), rgba(68,68,68,0.5) 70%, #444);
|
|
||||||
margin-left: -4.6em;
|
|
||||||
padding: .4em;
|
|
||||||
top: 5.4em;
|
|
||||||
width: 8em;
|
|
||||||
text-align: right;
|
|
||||||
letter-spacing: .04em;
|
|
||||||
}
|
}
|
||||||
#files td:nth-child(2n) {
|
#files td:nth-child(2n) {
|
||||||
color: #f5a;
|
color: #f5a;
|
||||||
}
|
}
|
||||||
#files td.min a {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#files tr.play td,
|
#files tr.play td,
|
||||||
#files tr.play div a {
|
#files tr.play div a {
|
||||||
background: #fc4;
|
background: #fc4;
|
||||||
@@ -650,46 +785,105 @@ input[type="checkbox"]:checked+label {
|
|||||||
color: #300;
|
color: #300;
|
||||||
background: #fea;
|
background: #fea;
|
||||||
}
|
}
|
||||||
#op_cfg {
|
.opwide {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
margin-right: 1.5em;
|
margin-right: 1.5em;
|
||||||
}
|
}
|
||||||
#op_cfg>div>a {
|
.opwide>div {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
border-left: .2em solid #4c4c4c;
|
||||||
|
margin-left: .5em;
|
||||||
|
padding-left: .5em;
|
||||||
|
}
|
||||||
|
.opwide>div.fill {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.opwide>div>div>a {
|
||||||
line-height: 2em;
|
line-height: 2em;
|
||||||
}
|
}
|
||||||
#op_cfg>div>span {
|
#op_cfg>div>div>span {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: .2em .4em;
|
padding: .2em .4em;
|
||||||
}
|
}
|
||||||
#op_cfg h3 {
|
.opbox h3 {
|
||||||
margin: .8em 0 0 .6em;
|
margin: .8em 0 0 .6em;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-bottom: 1px solid #555;
|
border-bottom: 1px solid #555;
|
||||||
}
|
}
|
||||||
#opdesc {
|
#thumbs,
|
||||||
display: none;
|
#au_osd_cv {
|
||||||
|
opacity: .3;
|
||||||
}
|
}
|
||||||
#ops:hover #opdesc {
|
#griden.on+#thumbs,
|
||||||
display: block;
|
#au_os_ctl.on+#au_osd_cv {
|
||||||
background: linear-gradient(0deg,#555, #4c4c4c 80%, #444);
|
opacity: 1;
|
||||||
box-shadow: 0 .3em 1em #222;
|
|
||||||
padding: 1em;
|
|
||||||
border-radius: .3em;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 3;
|
|
||||||
top: 6em;
|
|
||||||
right: 1.5em;
|
|
||||||
}
|
}
|
||||||
#ops:hover #opdesc.off {
|
#ghead {
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#opdesc code {
|
|
||||||
background: #3c3c3c;
|
background: #3c3c3c;
|
||||||
padding: .2em .3em;
|
border: 1px solid #444;
|
||||||
border-top: 1px solid #777;
|
|
||||||
border-radius: .3em;
|
border-radius: .3em;
|
||||||
font-family: monospace, monospace;
|
padding: .5em;
|
||||||
line-height: 2em;
|
margin: 0 1.5em 1em .4em;
|
||||||
|
position: sticky;
|
||||||
|
top: -.3em;
|
||||||
|
}
|
||||||
|
html.light #ghead {
|
||||||
|
background: #f7f7f7;
|
||||||
|
border-color: #ddd;
|
||||||
|
}
|
||||||
|
#ghead .btn {
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
#ggrid {
|
||||||
|
padding-top: .5em;
|
||||||
|
}
|
||||||
|
#ggrid a {
|
||||||
|
display: inline-block;
|
||||||
|
width: var(--grid-sz);
|
||||||
|
vertical-align: top;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
background: #383838;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-top: 1px solid #555;
|
||||||
|
box-shadow: 0 .1em .2em #222;
|
||||||
|
border-radius: .3em;
|
||||||
|
padding: .3em;
|
||||||
|
margin: .5em;
|
||||||
|
}
|
||||||
|
#ggrid a img {
|
||||||
|
border-radius: .2em;
|
||||||
|
max-width: var(--grid-sz);
|
||||||
|
max-height: calc(var(--grid-sz)/1.25);
|
||||||
|
margin: 0 auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#ggrid a span {
|
||||||
|
padding: .2em .3em;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#ggrid span.dir:before {
|
||||||
|
content: '📂';
|
||||||
|
line-height: 0;
|
||||||
|
font-size: 2em;
|
||||||
|
margin: -.7em .1em -.5em -.3em;
|
||||||
|
}
|
||||||
|
#ggrid a:hover {
|
||||||
|
background: #444;
|
||||||
|
border-color: #555;
|
||||||
|
color: #fd9;
|
||||||
|
}
|
||||||
|
html.light #ggrid a {
|
||||||
|
background: #f7f7f7;
|
||||||
|
border-color: #ddd;
|
||||||
|
box-shadow: 0 .1em .2em #ddd;
|
||||||
|
}
|
||||||
|
html.light #ggrid a:hover {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #ccc;
|
||||||
|
color: #015;
|
||||||
|
box-shadow: 0 .1em .5em #aaa;
|
||||||
}
|
}
|
||||||
#pvol,
|
#pvol,
|
||||||
#barbuf,
|
#barbuf,
|
||||||
@@ -705,11 +899,35 @@ input[type="checkbox"]:checked+label {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
html.light {
|
html.light {
|
||||||
color: #333;
|
color: #333;
|
||||||
background: #eee;
|
background: #eee;
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
html.light #tt {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #888;
|
||||||
|
box-shadow: 0 .3em 1em rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
html.light #tt code {
|
||||||
|
background: #060;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
html.light #ops,
|
html.light #ops,
|
||||||
html.light .opbox,
|
html.light .opbox,
|
||||||
html.light #srch_form {
|
html.light #srch_form {
|
||||||
@@ -726,18 +944,15 @@ html.light #ops a.act {
|
|||||||
html.light #op_cfg h3 {
|
html.light #op_cfg h3 {
|
||||||
border-color: #ccc;
|
border-color: #ccc;
|
||||||
}
|
}
|
||||||
html.light .tglbtn,
|
html.light .btn {
|
||||||
html.light #tree > a + a {
|
|
||||||
color: #666;
|
color: #666;
|
||||||
background: #ddd;
|
background: #ddd;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
html.light .tglbtn:hover,
|
html.light .btn:hover {
|
||||||
html.light #tree > a + a:hover {
|
|
||||||
background: #caf;
|
background: #caf;
|
||||||
}
|
}
|
||||||
html.light .tglbtn.on,
|
html.light .tgl.btn.on {
|
||||||
html.light #tree > a + a.on {
|
|
||||||
background: #4a0;
|
background: #4a0;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
@@ -756,8 +971,14 @@ html.light #treeul a.hl {
|
|||||||
background: #07a;
|
background: #07a;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
html.light #treeul a.hl:hover {
|
||||||
|
background: #059;
|
||||||
|
}
|
||||||
html.light #tree li {
|
html.light #tree li {
|
||||||
border-color: #ddd #fff #f7f7f7 #fff;
|
border-color: #f7f7f7 #fff #ddd #fff;
|
||||||
|
}
|
||||||
|
html.light #tree a:hover {
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
html.light #tree ul {
|
html.light #tree ul {
|
||||||
border-color: #ccc;
|
border-color: #ccc;
|
||||||
@@ -775,12 +996,14 @@ html.light #files {
|
|||||||
}
|
}
|
||||||
html.light #files thead th {
|
html.light #files thead th {
|
||||||
background: #eee;
|
background: #eee;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-top: none;
|
||||||
}
|
}
|
||||||
html.light #files tr td {
|
html.light #files thead th+th {
|
||||||
border-top: 1px solid #ddd;
|
border-left: 1px solid #f7f7f7;
|
||||||
}
|
}
|
||||||
html.light #files td {
|
html.light #files td {
|
||||||
border-bottom: 1px solid #f7f7f7;
|
border-color: #fff #fff #ddd #ddd;
|
||||||
}
|
}
|
||||||
html.light #files tbody tr:last-child td {
|
html.light #files tbody tr:last-child td {
|
||||||
border-bottom: .2em solid #ccc;
|
border-bottom: .2em solid #ccc;
|
||||||
@@ -788,25 +1011,28 @@ html.light #files tbody tr:last-child td {
|
|||||||
html.light #files td:nth-child(2n) {
|
html.light #files td:nth-child(2n) {
|
||||||
color: #d38;
|
color: #d38;
|
||||||
}
|
}
|
||||||
html.light #files tr:hover td {
|
html.light #files tr.play td:nth-child(2n) {
|
||||||
background: #fff;
|
color: #c16;
|
||||||
}
|
}
|
||||||
html.light #files tbody a.play {
|
html.light #files tbody a.play {
|
||||||
color: #c0f;
|
color: #c0f;
|
||||||
}
|
}
|
||||||
html.light tr.play td {
|
html.light #files tbody a.play.act {
|
||||||
|
color: #90c;
|
||||||
|
}
|
||||||
|
html.light #files tr.play td {
|
||||||
background: #fc5;
|
background: #fc5;
|
||||||
|
border-color: #eb1;
|
||||||
|
}
|
||||||
|
html.light #files tr:hover td {
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
html.light tr.play a {
|
html.light tr.play a {
|
||||||
color: #406;
|
color: #406;
|
||||||
}
|
}
|
||||||
html.light #files th:hover .cfg,
|
html.light #files th:hover .cfg {
|
||||||
html.light #files th.min .cfg {
|
|
||||||
background: #ccc;
|
background: #ccc;
|
||||||
}
|
}
|
||||||
html.light #files > thead > tr > th.min span {
|
|
||||||
background: linear-gradient(90deg, rgba(204,204,204,0), rgba(204,204,204,0.5) 70%, #ccc);
|
|
||||||
}
|
|
||||||
html.light #blocked {
|
html.light #blocked {
|
||||||
background: #eee;
|
background: #eee;
|
||||||
}
|
}
|
||||||
@@ -816,7 +1042,24 @@ html.light #blk_abrt a {
|
|||||||
box-shadow: 0 .2em .4em #ddd;
|
box-shadow: 0 .2em .4em #ddd;
|
||||||
}
|
}
|
||||||
html.light #widget a {
|
html.light #widget a {
|
||||||
color: #fc5;
|
color: #06a;
|
||||||
|
}
|
||||||
|
html.light #wtoggle,
|
||||||
|
html.light #widgeti {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
html.light #wtoggle {
|
||||||
|
box-shadow: 0 0 .5em #bbb;
|
||||||
|
}
|
||||||
|
html.light #widget.open {
|
||||||
|
border-top: .2em solid #f7f7f7;
|
||||||
|
}
|
||||||
|
html.light #wzip,
|
||||||
|
html.light #wnp {
|
||||||
|
border-color: #ccc;
|
||||||
|
}
|
||||||
|
html.light #barbuf {
|
||||||
|
background: none;
|
||||||
}
|
}
|
||||||
html.light #files tr.sel:hover td {
|
html.light #files tr.sel:hover td {
|
||||||
background: #c37;
|
background: #c37;
|
||||||
@@ -833,20 +1076,15 @@ html.light #files tr.sel a.play.act {
|
|||||||
html.light input[type="checkbox"] + label {
|
html.light input[type="checkbox"] + label {
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
html.light .opwide>div {
|
||||||
|
border-color: #ccc;
|
||||||
|
}
|
||||||
html.light .opview input[type="text"] {
|
html.light .opview input[type="text"] {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #333;
|
color: #333;
|
||||||
box-shadow: 0 0 2px #888;
|
box-shadow: 0 0 2px #888;
|
||||||
border-color: #38d;
|
border-color: #38d;
|
||||||
}
|
}
|
||||||
html.light #ops:hover #opdesc {
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 .3em 1em #ccc;
|
|
||||||
}
|
|
||||||
html.light #opdesc code {
|
|
||||||
background: #060;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
html.light #u2tab a>span,
|
html.light #u2tab a>span,
|
||||||
html.light #files td div span {
|
html.light #files td div span {
|
||||||
color: #000;
|
color: #000;
|
||||||
@@ -856,9 +1094,6 @@ html.light #path {
|
|||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
box-shadow: 0 0 .3em #bbb;
|
box-shadow: 0 0 .3em #bbb;
|
||||||
}
|
}
|
||||||
html.light #path a {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
html.light #path a:not(:last-child)::after {
|
html.light #path a:not(:last-child)::after {
|
||||||
border-color: #ccc;
|
border-color: #ccc;
|
||||||
background: none;
|
background: none;
|
||||||
@@ -867,7 +1102,7 @@ html.light #path a:not(:last-child)::after {
|
|||||||
}
|
}
|
||||||
html.light #path a:hover {
|
html.light #path a:hover {
|
||||||
background: none;
|
background: none;
|
||||||
color: #60a;
|
color: #90d;
|
||||||
}
|
}
|
||||||
html.light #files tbody div a {
|
html.light #files tbody div a {
|
||||||
color: #d38;
|
color: #d38;
|
||||||
@@ -877,6 +1112,9 @@ html.light #files tr.sel a:hover {
|
|||||||
color: #000;
|
color: #000;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
html.light #treeh {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
html.light #tree {
|
html.light #tree {
|
||||||
scrollbar-color: #a70 #ddd;
|
scrollbar-color: #a70 #ddd;
|
||||||
}
|
}
|
||||||
@@ -886,4 +1124,162 @@ html.light #tree::-webkit-scrollbar {
|
|||||||
}
|
}
|
||||||
#tree::-webkit-scrollbar-thumb {
|
#tree::-webkit-scrollbar-thumb {
|
||||||
background: #da0;
|
background: #da0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#baguetteBox-overlay {
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
position: fixed;
|
||||||
|
overflow: hidden;
|
||||||
|
touch-action: none;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1000000;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
transition: opacity .3s ease;
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay .full-image {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay .full-image figure {
|
||||||
|
display: inline;
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay .full-image img {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay .full-image figcaption {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.8;
|
||||||
|
white-space: normal;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay figcaption a {
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: .4em;
|
||||||
|
padding: .3em .6em;
|
||||||
|
}
|
||||||
|
#baguetteBox-overlay .full-image:before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
height: 50%;
|
||||||
|
width: 1px;
|
||||||
|
margin-right: -1px;
|
||||||
|
}
|
||||||
|
#baguetteBox-slider {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: left .2s ease, transform .2s ease;
|
||||||
|
}
|
||||||
|
#baguetteBox-slider.bounce-from-right {
|
||||||
|
animation: bounceFromRight .4s ease-out;
|
||||||
|
}
|
||||||
|
#baguetteBox-slider.bounce-from-left {
|
||||||
|
animation: bounceFromLeft .4s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes bounceFromRight {
|
||||||
|
0% {margin-left: 0}
|
||||||
|
50% {margin-left: -30px}
|
||||||
|
100% {margin-left: 0}
|
||||||
|
}
|
||||||
|
@keyframes bounceFromLeft {
|
||||||
|
0% {margin-left: 0}
|
||||||
|
50% {margin-left: 30px}
|
||||||
|
100% {margin-left: 0}
|
||||||
|
}
|
||||||
|
.baguetteBox-button#next-button,
|
||||||
|
.baguetteBox-button#previous-button {
|
||||||
|
top: 50%;
|
||||||
|
top: calc(50% - 30px);
|
||||||
|
width: 44px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
.baguetteBox-button {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 15%;
|
||||||
|
background: rgba(50, 50, 50, 0.5);
|
||||||
|
color: #ddd;
|
||||||
|
font: 1.6em sans-serif;
|
||||||
|
transition: background-color .3s ease;
|
||||||
|
}
|
||||||
|
.baguetteBox-button:focus,
|
||||||
|
.baguetteBox-button:hover {
|
||||||
|
background: rgba(50, 50, 50, 0.9);
|
||||||
|
}
|
||||||
|
#next-button {
|
||||||
|
right: 2%;
|
||||||
|
}
|
||||||
|
#previous-button {
|
||||||
|
left: 2%;
|
||||||
|
}
|
||||||
|
#close-button {
|
||||||
|
top: 20px;
|
||||||
|
right: 2%;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
.baguetteBox-button svg {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.baguetteBox-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -20px;
|
||||||
|
margin-left: -20px;
|
||||||
|
}
|
||||||
|
.baguetteBox-double-bounce1,
|
||||||
|
.baguetteBox-double-bounce2 {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
opacity: .6;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
animation: bounce 2s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
.baguetteBox-double-bounce2 {
|
||||||
|
animation-delay: -1s;
|
||||||
|
}
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% {transform: scale(0)}
|
||||||
|
50% {transform: scale(1)}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,144 +2,134 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>⇆🎉 {{ title }}</title>
|
<title>⇆🎉 {{ title }}</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">
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/browser.css{{ ts }}">
|
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/browser.css?_={{ ts }}">
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/upload.css{{ ts }}">
|
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/upload.css?_={{ ts }}">
|
||||||
|
{%- if css %}
|
||||||
|
<link rel="stylesheet" type="text/css" media="screen" href="{{ css }}?_={{ ts }}">
|
||||||
|
{%- endif %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="ops">
|
<div id="ops"></div>
|
||||||
<a href="#" data-dest="" data-desc="close submenu">---</a>
|
|
||||||
{%- if have_up2k_idx %}
|
|
||||||
<a href="#" data-perm="read" data-dest="search" data-desc="search for files by attributes, path/name, music tags, or any combination of those.<br /><br /><code>foo bar</code> = must contain both foo and bar,<br /><code>foo -bar</code> = must contain foo but not bar,<br /><code>^yana .opus$</code> = must start with yana and have the opus extension">🔎</a>
|
|
||||||
<a href="#" data-dest="up2k" data-desc="up2k: upload files (if you have write-access) or toggle into the search-mode and drag files onto the search button to see if they exist somewhere on the server">🚀</a>
|
|
||||||
{%- else %}
|
|
||||||
<a href="#" data-perm="write" data-dest="up2k" data-desc="up2k: upload files with resume support (close your browser and drop the same files in later)">🚀</a>
|
|
||||||
{%- endif %}
|
|
||||||
<a href="#" data-perm="write" data-dest="bup" data-desc="bup: basic uploader, even supports netscape 4.0">🎈</a>
|
|
||||||
<a href="#" data-perm="write" data-dest="mkdir" data-desc="mkdir: create a new directory">📂</a>
|
|
||||||
<a href="#" data-perm="write" data-dest="new_md" data-desc="new-md: create a new markdown document">📝</a>
|
|
||||||
<a href="#" data-perm="write" data-dest="msg" data-desc="msg: send a message to the server log">📟</a>
|
|
||||||
<a href="#" data-dest="cfg" data-desc="configuration options">⚙️</a>
|
|
||||||
<div id="opdesc"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="op_search" class="opview">
|
<div id="op_search" class="opview">
|
||||||
{%- if have_tags_idx %}
|
{%- if have_tags_idx %}
|
||||||
<div id="srch_form" class="tags"></div>
|
<div id="srch_form" class="tags"></div>
|
||||||
{%- else %}
|
{%- else %}
|
||||||
<div id="srch_form"></div>
|
<div id="srch_form"></div>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
<div id="srch_q"></div>
|
<div id="srch_q"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{%- include 'upload.html' %}
|
<div id="op_player" class="opview opbox opwide"></div>
|
||||||
|
|
||||||
<div id="op_cfg" class="opview opbox">
|
<div id="op_bup" class="opview opbox act">
|
||||||
<h3>switches</h3>
|
<div id="u2err"></div>
|
||||||
<div>
|
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
||||||
<a id="tooltips" class="tglbtn" href="#">tooltips</a>
|
<input type="hidden" name="act" value="bput" />
|
||||||
<a id="lightmode" class="tglbtn" href="#">lightmode</a>
|
<input type="file" name="f" multiple><br />
|
||||||
</div>
|
<input type="submit" value="start upload">
|
||||||
{%- if have_zip %}
|
</form>
|
||||||
<h3>folder download</h3>
|
</div>
|
||||||
<div id="arc_fmt"></div>
|
|
||||||
{%- endif %}
|
<div id="op_mkdir" class="opview opbox act">
|
||||||
<h3>key notation</h3>
|
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
||||||
<div id="key_notation"></div>
|
<input type="hidden" name="act" value="mkdir" />
|
||||||
</div>
|
<input type="text" name="name" size="30">
|
||||||
|
<input type="submit" value="mkdir">
|
||||||
<h1 id="path">
|
</form>
|
||||||
<a href="#" id="entree">🌲</a>
|
</div>
|
||||||
{%- for n in vpnodes %}
|
|
||||||
<a href="/{{ n[0] }}">{{ n[1] }}</a>
|
<div id="op_new_md" class="opview opbox">
|
||||||
{%- endfor %}
|
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
||||||
</h1>
|
<input type="hidden" name="act" value="new_md" />
|
||||||
|
<input type="text" name="name" size="30">
|
||||||
<div id="tree">
|
<input type="submit" value="create doc">
|
||||||
<a href="#" id="detree">🍞...</a>
|
</form>
|
||||||
<a href="#" step="2" id="twobytwo">+</a>
|
</div>
|
||||||
<a href="#" step="-2" id="twig">–</a>
|
|
||||||
<a href="#" class="tglbtn" id="dyntree">a</a>
|
<div id="op_msg" class="opview opbox act">
|
||||||
<ul id="treeul"></ul>
|
<form method="post" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" action="{{ url_suf }}">
|
||||||
<div id="thx_ff"> </div>
|
<input type="text" name="msg" size="30">
|
||||||
</div>
|
<input type="submit" value="send msg">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="op_up2k" class="opview"></div>
|
||||||
|
|
||||||
|
<div id="op_cfg" class="opview opbox opwide"></div>
|
||||||
|
|
||||||
|
<h1 id="path">
|
||||||
|
<a href="#" id="entree" tt="show directory tree$NHotkey: B">🌲</a>
|
||||||
|
{%- for n in vpnodes %}
|
||||||
|
<a href="/{{ n[0] }}">{{ n[1] }}</a>
|
||||||
|
{%- endfor %}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div id="tree"></div>
|
||||||
|
|
||||||
<div id="wrap">
|
<div id="wrap">
|
||||||
|
|
||||||
<div id="pro" class="logue">{{ logues[0] }}</div>
|
<div id="pro" class="logue">{{ logues[0] }}</div>
|
||||||
|
|
||||||
<table id="files">
|
<table id="files">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th name="lead"><span>c</span></th>
|
<th name="lead"><span>c</span></th>
|
||||||
<th name="href"><span>File Name</span></th>
|
<th name="href"><span>File Name</span></th>
|
||||||
<th name="sz" sort="int"><span>Size</span></th>
|
<th name="sz" sort="int"><span>Size</span></th>
|
||||||
{%- for k in taglist %}
|
{%- for k in taglist %}
|
||||||
{%- if k.startswith('.') %}
|
{%- if k.startswith('.') %}
|
||||||
<th name="tags/{{ k }}" sort="int"><span>{{ k[1:] }}</span></th>
|
<th name="tags/{{ k }}" sort="int"><span>{{ k[1:] }}</span></th>
|
||||||
{%- else %}
|
{%- else %}
|
||||||
<th name="tags/{{ k }}"><span>{{ k[0]|upper }}{{ k[1:] }}</span></th>
|
<th name="tags/{{ k }}"><span>{{ k[0]|upper }}{{ k[1:] }}</span></th>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
<th name="ext"><span>T</span></th>
|
<th name="ext"><span>T</span></th>
|
||||||
<th name="ts"><span>Date</span></th>
|
<th name="ts"><span>Date</span></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
{%- for f in files %}
|
{%- for f in files %}
|
||||||
<tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td>
|
<tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td>
|
||||||
{%- if f.tags is defined %}
|
{%- if f.tags is defined %}
|
||||||
{%- for k in taglist %}
|
{%- for k in taglist %}
|
||||||
<td>{{ f.tags[k] }}</td>
|
<td>{{ f.tags[k] }}</td>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
<td>{{ f.ext }}</td><td>{{ f.dt }}</td></tr>
|
<td>{{ f.ext }}</td><td>{{ f.dt }}</td></tr>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div id="epi" class="logue">{{ logues[1] }}</div>
|
<div id="epi" class="logue">{{ logues[1] }}</div>
|
||||||
|
|
||||||
<h2><a href="?h">control-panel</a></h2>
|
<h2><a href="/?h">control-panel</a></h2>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{%- if srv_info %}
|
{%- if srv_info %}
|
||||||
<div id="srv_info"><span>{{ srv_info }}</span></div>
|
<div id="srv_info"><span>{{ srv_info }}</span></div>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
<div id="widget">
|
<div id="widget"></div>
|
||||||
<div id="wtoggle">
|
|
||||||
<span id="wzip">
|
|
||||||
<a href="#" id="selall">sel.<br />all</a>
|
|
||||||
<a href="#" id="selinv">sel.<br />inv.</a>
|
|
||||||
<a href="#" id="selzip">zip</a>
|
|
||||||
</span><a
|
|
||||||
href="#" id="wtico">♫</a>
|
|
||||||
</div>
|
|
||||||
<div id="widgeti">
|
|
||||||
<div id="pctl"><a href="#" id="bprev">⏮</a><a href="#" id="bplay">▶</a><a href="#" id="bnext">⏭</a></div>
|
|
||||||
<canvas id="pvol" width="288" height="38"></canvas>
|
|
||||||
<canvas id="barpos"></canvas>
|
|
||||||
<canvas id="barbuf"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var tag_order_cfg = {{ tag_order }};
|
var perms = {{ perms }},
|
||||||
</script>
|
tag_order_cfg = {{ tag_order }},
|
||||||
<script src="/.cpr/util.js{{ ts }}"></script>
|
have_up2k_idx = {{ have_up2k_idx|tojson }},
|
||||||
<script src="/.cpr/browser.js{{ ts }}"></script>
|
have_tags_idx = {{ have_tags_idx|tojson }},
|
||||||
<script src="/.cpr/up2k.js{{ ts }}"></script>
|
have_zip = {{ have_zip|tojson }};
|
||||||
<script>
|
</script>
|
||||||
apply_perms({{ perms }});
|
<script src="/.cpr/util.js?_={{ ts }}"></script>
|
||||||
</script>
|
<script src="/.cpr/browser.js?_={{ ts }}"></script>
|
||||||
|
<script src="/.cpr/up2k.js?_={{ ts }}"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,54 +2,59 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>{{ title }}</title>
|
<title>{{ title }}</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">
|
||||||
|
<style>
|
||||||
|
html{font-family:sans-serif}
|
||||||
|
td{border:1px solid #999;border-width:1px 1px 0 0;padding:0 5px}
|
||||||
|
a{display:block}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{%- if srv_info %}
|
{%- if srv_info %}
|
||||||
<p><span>{{ srv_info }}</span></p>
|
<p><span>{{ srv_info }}</span></p>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
{%- if have_b_u %}
|
{%- if have_b_u %}
|
||||||
<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" />
|
||||||
<input type="file" name="f" multiple /><br />
|
<input type="file" name="f" multiple /><br />
|
||||||
<input type="submit" value="start upload" />
|
<input type="submit" value="start upload" />
|
||||||
</form>
|
</form>
|
||||||
<br />
|
<br />
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
{%- if logues[0] %}
|
{%- if logues[0] %}
|
||||||
<div>{{ logues[0] }}</div><br />
|
<div>{{ logues[0] }}</div><br />
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
<table id="files">
|
<table id="files">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th name="lead"><span>c</span></th>
|
<th name="lead"><span>c</span></th>
|
||||||
<th name="href"><span>File Name</span></th>
|
<th name="href"><span>File Name</span></th>
|
||||||
<th name="sz" sort="int"><span>Size</span></th>
|
<th name="sz" sort="int"><span>Size</span></th>
|
||||||
<th name="ts"><span>Date</span></th>
|
<th name="ts"><span>Date</span></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td></td><td><a href="../{{ url_suf }}">parent folder</a></td><td>-</td><td>-</td></tr>
|
<tr><td></td><td><a href="../{{ url_suf }}">parent folder</a></td><td>-</td><td>-</td></tr>
|
||||||
|
|
||||||
{%- for f in files %}
|
{%- for f in files %}
|
||||||
<tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}{{ url_suf }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td><td>{{ f.dt }}</td></tr>
|
<tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}{{ url_suf }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td><td>{{ f.dt }}</td></tr>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{%- if logues[1] %}
|
{%- if logues[1] %}
|
||||||
<div>{{ logues[1] }}</div><br />
|
<div>{{ logues[1] }}</div><br />
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
<h2><a href="{{ url_suf }}&h">control-panel</a></h2>
|
<h2><a href="/{{ url_suf }}{{ url_suf and '&' or '?' }}h">control-panel</a></h2>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
61
copyparty/web/dbg-audio.js
Normal file
61
copyparty/web/dbg-audio.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
var ofun = audio_eq.apply.bind(audio_eq);
|
||||||
|
audio_eq.apply = function () {
|
||||||
|
var ac1 = mp.ac;
|
||||||
|
ofun();
|
||||||
|
var ac = mp.ac,
|
||||||
|
w = 2048,
|
||||||
|
h = 256;
|
||||||
|
|
||||||
|
if (!audio_eq.filters.length) {
|
||||||
|
audio_eq.ana = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var can = ebi('fft_can');
|
||||||
|
if (!can) {
|
||||||
|
can = mknod('canvas');
|
||||||
|
can.setAttribute('id', 'fft_can');
|
||||||
|
can.style.cssText = 'position:absolute;left:0;bottom:5em;width:' + w + 'px;height:' + h + 'px;z-index:9001';
|
||||||
|
document.body.appendChild(can);
|
||||||
|
can.width = w;
|
||||||
|
can.height = h;
|
||||||
|
}
|
||||||
|
var cc = can.getContext('2d');
|
||||||
|
if (!ac)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var ana = ac.createAnalyser();
|
||||||
|
ana.smoothingTimeConstant = 0;
|
||||||
|
ana.fftSize = 8192;
|
||||||
|
|
||||||
|
audio_eq.filters[0].connect(ana);
|
||||||
|
audio_eq.ana = ana;
|
||||||
|
|
||||||
|
var buf = new Uint8Array(ana.frequencyBinCount),
|
||||||
|
colw = can.width / buf.length;
|
||||||
|
|
||||||
|
cc.fillStyle = '#fc0';
|
||||||
|
function draw() {
|
||||||
|
if (ana == audio_eq.ana)
|
||||||
|
requestAnimationFrame(draw);
|
||||||
|
|
||||||
|
ana.getByteFrequencyData(buf);
|
||||||
|
|
||||||
|
cc.clearRect(0, 0, can.width, can.height);
|
||||||
|
|
||||||
|
/*var x = 0, w = 1;
|
||||||
|
for (var a = 0; a < buf.length; a++) {
|
||||||
|
cc.fillRect(x, h - buf[a], w, h);
|
||||||
|
x += w;
|
||||||
|
}*/
|
||||||
|
var mul = Math.pow(w, 4) / buf.length;
|
||||||
|
for (var x = 0; x < w; x++) {
|
||||||
|
var a = Math.floor(Math.pow(x, 4) / mul),
|
||||||
|
v = buf[a];
|
||||||
|
|
||||||
|
cc.fillRect(x, h - v, 1, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draw();
|
||||||
|
};
|
||||||
|
audio_eq.apply();
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 248 B |
@@ -3,9 +3,9 @@
|
|||||||
<title>📝🎉 {{ title }}</title> <!-- 📜 -->
|
<title>📝🎉 {{ title }}</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.7">
|
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
||||||
<link href="/.cpr/md.css" rel="stylesheet">
|
<link href="/.cpr/md.css?_={{ ts }}" rel="stylesheet">
|
||||||
{%- if edit %}
|
{%- if edit %}
|
||||||
<link href="/.cpr/md2.css" rel="stylesheet">
|
<link href="/.cpr/md2.css?_={{ ts }}" rel="stylesheet">
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -146,10 +146,10 @@ var md_opt = {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<script src="/.cpr/util.js"></script>
|
<script src="/.cpr/util.js?_={{ ts }}"></script>
|
||||||
<script src="/.cpr/deps/marked.js"></script>
|
<script src="/.cpr/deps/marked.js?_={{ ts }}"></script>
|
||||||
<script src="/.cpr/md.js"></script>
|
<script src="/.cpr/md.js?_={{ ts }}"></script>
|
||||||
{%- if edit %}
|
{%- if edit %}
|
||||||
<script src="/.cpr/md2.js"></script>
|
<script src="/.cpr/md2.js?_={{ ts }}"></script>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</body></html>
|
</body></html>
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
<title>📝🎉 {{ title }}</title>
|
<title>📝🎉 {{ title }}</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.7">
|
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
||||||
<link href="/.cpr/mde.css" rel="stylesheet">
|
<link href="/.cpr/mde.css?_={{ ts }}" rel="stylesheet">
|
||||||
<link href="/.cpr/deps/mini-fa.css" rel="stylesheet">
|
<link href="/.cpr/deps/mini-fa.css?_={{ ts }}" rel="stylesheet">
|
||||||
<link href="/.cpr/deps/easymde.css" rel="stylesheet">
|
<link href="/.cpr/deps/easymde.css?_={{ ts }}" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="mw">
|
<div id="mw">
|
||||||
@@ -43,7 +43,7 @@ var lightswitch = (function () {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<script src="/.cpr/util.js"></script>
|
<script src="/.cpr/util.js?_={{ ts }}"></script>
|
||||||
<script src="/.cpr/deps/easymde.js"></script>
|
<script src="/.cpr/deps/easymde.js?_={{ ts }}"></script>
|
||||||
<script src="/.cpr/mde.js"></script>
|
<script src="/.cpr/mde.js?_={{ ts }}"></script>
|
||||||
</body></html>
|
</body></html>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<title>copyparty</title>
|
<title>copyparty</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">
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/msg.css">
|
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/msg.css?_={{ ts }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -26,6 +26,26 @@ a {
|
|||||||
border-radius: .2em;
|
border-radius: .2em;
|
||||||
padding: .2em .8em;
|
padding: .2em .8em;
|
||||||
}
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.vols td,
|
||||||
|
.vols th {
|
||||||
|
padding: .3em .6em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.num {
|
||||||
|
border-right: 1px solid #bbb;
|
||||||
|
}
|
||||||
|
.num td {
|
||||||
|
padding: .1em .7em .1em 0;
|
||||||
|
}
|
||||||
|
.num td:first-child {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.btns {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
html.dark,
|
html.dark,
|
||||||
@@ -50,4 +70,7 @@ html.dark input {
|
|||||||
border-radius: .5em;
|
border-radius: .5em;
|
||||||
padding: .5em .7em;
|
padding: .5em .7em;
|
||||||
margin: 0 .5em 0 0;
|
margin: 0 .5em 0 0;
|
||||||
|
}
|
||||||
|
html.dark .num {
|
||||||
|
border-color: #777;
|
||||||
}
|
}
|
||||||
@@ -6,30 +6,60 @@
|
|||||||
<title>copyparty</title>
|
<title>copyparty</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">
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/splash.css">
|
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/splash.css?_={{ ts }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="wrap">
|
<div id="wrap">
|
||||||
<p>hello {{ this.uname }}</p>
|
<p>hello {{ this.uname }}</p>
|
||||||
|
|
||||||
|
{%- if avol %}
|
||||||
|
<h1>admin panel:</h1>
|
||||||
|
<table><tr><td> <!-- hehehe -->
|
||||||
|
<table class="num">
|
||||||
|
<tr><td>scanning</td><td>{{ scanning }}</td></tr>
|
||||||
|
<tr><td>hash-q</td><td>{{ hashq }}</td></tr>
|
||||||
|
<tr><td>tag-q</td><td>{{ tagq }}</td></tr>
|
||||||
|
<tr><td>mtp-q</td><td>{{ mtpq }}</td></tr>
|
||||||
|
</table>
|
||||||
|
</td><td>
|
||||||
|
<table class="vols">
|
||||||
|
<thead><tr><th>vol</th><th>action</th><th>status</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for mp in avol %}
|
||||||
|
{%- if mp in vstate and vstate[mp] %}
|
||||||
|
<tr><td><a href="{{ mp }}{{ url_suf }}">{{ mp }}</a></td><td><a href="{{ mp }}?scan">rescan</a></td><td>{{ vstate[mp] }}</td></tr>
|
||||||
|
{%- endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td></tr></table>
|
||||||
|
<div class="btns">
|
||||||
|
<a href="{{ avol[0] }}?stack">dump stack</a>
|
||||||
|
</div>
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
{%- if rvol %}
|
||||||
<h1>you can browse these:</h1>
|
<h1>you can browse these:</h1>
|
||||||
<ul>
|
<ul>
|
||||||
{% for mp in rvol %}
|
{% for mp in rvol %}
|
||||||
<li><a href="/{{ mp }}{{ url_suf }}">/{{ mp }}</a></li>
|
<li><a href="{{ mp }}{{ url_suf }}">{{ mp }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
{%- if wvol %}
|
||||||
<h1>you can upload to:</h1>
|
<h1>you can upload to:</h1>
|
||||||
<ul>
|
<ul>
|
||||||
{% for mp in wvol %}
|
{% for mp in wvol %}
|
||||||
<li><a href="/{{ mp }}{{ url_suf }}">/{{ mp }}</a></li>
|
<li><a href="{{ mp }}{{ url_suf }}">{{ mp }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
<h1>login for more:</h1>
|
<h1>login for more:</h1>
|
||||||
<ul>
|
<ul>
|
||||||
<form method="post" enctype="multipart/form-data" action="/{{ url_suf }}">
|
<form method="post" enctype="multipart/form-data" action="/">
|
||||||
<input type="hidden" name="act" value="login" />
|
<input type="hidden" name="act" value="login" />
|
||||||
<input type="password" name="cppwd" />
|
<input type="password" name="cppwd" />
|
||||||
<input type="submit" value="Login" />
|
<input type="submit" value="Login" />
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
window.onerror = vis_exh;
|
|
||||||
|
|
||||||
|
|
||||||
function goto_up2k() {
|
function goto_up2k() {
|
||||||
if (up2k === false)
|
if (up2k === false)
|
||||||
@@ -16,16 +14,19 @@ function goto_up2k() {
|
|||||||
|
|
||||||
// chrome requires https to use crypto.subtle,
|
// chrome requires https to use crypto.subtle,
|
||||||
// usually it's undefined but some chromes throw on invoke
|
// usually it's undefined but some chromes throw on invoke
|
||||||
var up2k = null;
|
var up2k = null,
|
||||||
|
sha_js = window.WebAssembly ? 'hw' : 'ac', // ff53,c57,sa11
|
||||||
|
m = 'will use ' + sha_js + ' instead of native sha512 due to';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
crypto.subtle.digest(
|
var cf = crypto.subtle || crypto.webkitSubtle;
|
||||||
'SHA-512', new Uint8Array(1)
|
cf.digest('SHA-512', new Uint8Array(1)).then(
|
||||||
).then(
|
function (x) { console.log('sha-ok'); up2k = up2k_init(cf); },
|
||||||
function (x) { up2k = up2k_init(true) },
|
function (x) { console.log(m, x); up2k = up2k_init(false); }
|
||||||
function (x) { up2k = up2k_init(false) }
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
|
console.log(m, ex);
|
||||||
try {
|
try {
|
||||||
up2k = up2k_init(false);
|
up2k = up2k_init(false);
|
||||||
}
|
}
|
||||||
@@ -141,7 +142,7 @@ function U2pvis(act, btns) {
|
|||||||
this.tail = -1;
|
this.tail = -1;
|
||||||
this.wsz = 3;
|
this.wsz = 3;
|
||||||
|
|
||||||
this.addfile = function (entry, sz) {
|
this.addfile = function (entry, sz, draw) {
|
||||||
this.tab.push({
|
this.tab.push({
|
||||||
"hn": entry[0],
|
"hn": entry[0],
|
||||||
"ht": entry[1],
|
"ht": entry[1],
|
||||||
@@ -155,6 +156,9 @@ function U2pvis(act, btns) {
|
|||||||
"bd0": 0 // upload start
|
"bd0": 0 // upload start
|
||||||
});
|
});
|
||||||
this.ctr["q"]++;
|
this.ctr["q"]++;
|
||||||
|
if (!draw)
|
||||||
|
return;
|
||||||
|
|
||||||
this.drawcard("q");
|
this.drawcard("q");
|
||||||
if (this.act == "q") {
|
if (this.act == "q") {
|
||||||
this.addrow(this.tab.length - 1);
|
this.addrow(this.tab.length - 1);
|
||||||
@@ -255,6 +259,41 @@ function U2pvis(act, btns) {
|
|||||||
var obj = ebi('f{0}p'.format(fobj.n)),
|
var obj = ebi('f{0}p'.format(fobj.n)),
|
||||||
o1 = p[0] - 2, o2 = p[0] - 0.1, o3 = p[0];
|
o1 = p[0] - 2, o2 = p[0] - 0.1, o3 = p[0];
|
||||||
|
|
||||||
|
if (!obj) { //} || true) {
|
||||||
|
var msg = [
|
||||||
|
"act", this.act,
|
||||||
|
"in", fo.in,
|
||||||
|
"is_act", this.is_act(fo.in),
|
||||||
|
"head", this.head,
|
||||||
|
"tail", this.tail,
|
||||||
|
"nfile", fobj.n,
|
||||||
|
"name", fobj.name,
|
||||||
|
"sz", fobj.size,
|
||||||
|
"bytesDelta", delta,
|
||||||
|
"bytesDone", fo.bd,
|
||||||
|
],
|
||||||
|
m2 = '',
|
||||||
|
ds = QSA("#u2tab>tbody>tr>td:first-child>a:last-child");
|
||||||
|
|
||||||
|
for (var a = 0; a < msg.length; a += 2)
|
||||||
|
m2 += msg[a] + '=' + msg[a + 1] + ', ';
|
||||||
|
|
||||||
|
console.log(m2);
|
||||||
|
|
||||||
|
for (var a = 0, aa = ds.length; a < aa; a++) {
|
||||||
|
var id = ds[a].parentNode.getAttribute('id').slice(1, -1);
|
||||||
|
console.log("dom %d/%d = [%s] in(%s) is_act(%s) %s",
|
||||||
|
a, aa, id, this.tab[id].in, this.is_act(fo.in), ds[a].textContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var a = 0, aa = this.tab.length; a < aa; a++)
|
||||||
|
if (this.is_act(this.tab[a].in))
|
||||||
|
console.log("tab %d/%d = sz %s", a, aa, this.tab[a].bt);
|
||||||
|
|
||||||
|
console.log("a");
|
||||||
|
throw 42;
|
||||||
|
}
|
||||||
|
|
||||||
obj.innerHTML = fo.hp;
|
obj.innerHTML = fo.hp;
|
||||||
obj.style.color = '#fff';
|
obj.style.color = '#fff';
|
||||||
obj.style.background = 'linear-gradient(90deg, #050, #270 ' + o1 + '%, #4b0 ' + o2 + '%, #333 ' + o3 + '%, #333 99%, #777)';
|
obj.style.background = 'linear-gradient(90deg, #050, #270 ' + o1 + '%, #4b0 ' + o2 + '%, #333 ' + o3 + '%, #333 99%, #777)';
|
||||||
@@ -275,12 +314,14 @@ function U2pvis(act, btns) {
|
|||||||
this.drawcard(oldcat);
|
this.drawcard(oldcat);
|
||||||
this.drawcard(newcat);
|
this.drawcard(newcat);
|
||||||
if (this.is_act(newcat)) {
|
if (this.is_act(newcat)) {
|
||||||
this.tail++;
|
this.tail = Math.max(this.tail, nfile + 1);
|
||||||
if (!ebi('f' + nfile))
|
if (!ebi('f' + nfile))
|
||||||
this.addrow(nfile);
|
this.addrow(nfile);
|
||||||
}
|
}
|
||||||
else if (this.is_act(oldcat)) {
|
else if (this.is_act(oldcat)) {
|
||||||
this.head++;
|
while (this.head < Math.min(this.tab.length, this.tail) && (this.head == nfile || !this.is_act(this.tab[this.head].in)))
|
||||||
|
this.head++;
|
||||||
|
|
||||||
if (!bz_act) {
|
if (!bz_act) {
|
||||||
var tr = ebi("f" + nfile);
|
var tr = ebi("f" + nfile);
|
||||||
tr.parentNode.removeChild(tr);
|
tr.parentNode.removeChild(tr);
|
||||||
@@ -349,8 +390,21 @@ function U2pvis(act, btns) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.head == -1) {
|
if (this.head == -1) {
|
||||||
this.head = this.tab.length;
|
var precard = has(["ok", "ng", "done"], this.act) ? {} : this.act == "bz" ? { "ok": 1, "ng": 1 } : { "ok": 1, "ng": 1, "bz": 1 },
|
||||||
this.tail = this.head - 1;
|
postcard = has(["ok", "ng", "done"], this.act) ? { "bz": 1, "q": 1 } : this.act == "bz" ? { "q": 1 } : {};
|
||||||
|
|
||||||
|
for (var a = 0; a < this.tab.length; a++) {
|
||||||
|
var rt = this.tab[a].in;
|
||||||
|
if (precard[rt]) {
|
||||||
|
this.head = a + 1;
|
||||||
|
this.tail = a;
|
||||||
|
}
|
||||||
|
else if (postcard[rt]) {
|
||||||
|
this.head = a;
|
||||||
|
this.tail = a - 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (card == "bz") {
|
if (card == "bz") {
|
||||||
for (var a = this.head - 1; a >= this.head - this.wsz && a >= 0; a--) {
|
for (var a = this.head - 1; a >= this.head - this.wsz && a >= 0; a--) {
|
||||||
@@ -401,9 +455,7 @@ function U2pvis(act, btns) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function up2k_init(have_crypto) {
|
function up2k_init(subtle) {
|
||||||
//have_crypto = false;
|
|
||||||
|
|
||||||
// show modal message
|
// show modal message
|
||||||
function showmodal(msg) {
|
function showmodal(msg) {
|
||||||
ebi('u2notbtn').innerHTML = msg;
|
ebi('u2notbtn').innerHTML = msg;
|
||||||
@@ -420,55 +472,67 @@ function up2k_init(have_crypto) {
|
|||||||
ebi('u2notbtn').innerHTML = '';
|
ebi('u2notbtn').innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var suggest_up2k = 'this is the basic uploader; <a href="#" id="u2yea">up2k</a> is better';
|
||||||
|
|
||||||
var shame = 'your browser <a href="https://www.chromium.org/blink/webcrypto">disables sha512</a> unless you <a href="' + (window.location + '').replace(':', 's:') + '">use https</a>',
|
var shame = 'your browser <a href="https://www.chromium.org/blink/webcrypto">disables sha512</a> unless you <a href="' + (window.location + '').replace(':', 's:') + '">use https</a>',
|
||||||
is_https = (window.location + '').indexOf('https:') === 0;
|
is_https = (window.location + '').indexOf('https:') === 0;
|
||||||
|
|
||||||
if (is_https)
|
if (is_https)
|
||||||
// chrome<37 firefox<34 edge<12 ie<11 opera<24 safari<10.1
|
// chrome<37 firefox<34 edge<12 opera<24 safari<7
|
||||||
shame = 'your browser is impressively ancient';
|
shame = 'your browser is impressively ancient';
|
||||||
|
|
||||||
// upload ui hidden by default, clicking the header shows it
|
// upload ui hidden by default, clicking the header shows it
|
||||||
function init_deps() {
|
function init_deps() {
|
||||||
if (!have_crypto && !window.asmCrypto) {
|
if (!subtle && !window.asmCrypto) {
|
||||||
showmodal('<h1>loading sha512.js</h1><h2>since ' + shame + '</h2><h4>thanks chrome</h4>');
|
var fn = 'sha512.' + sha_js + '.js';
|
||||||
import_js('/.cpr/deps/sha512.js', unmodal);
|
showmodal('<h1>loading ' + fn + '</h1><h2>since ' + shame + '</h2><h4>thanks chrome</h4>');
|
||||||
|
import_js('/.cpr/deps/' + fn, unmodal);
|
||||||
|
|
||||||
if (is_https)
|
if (is_https)
|
||||||
ebi('u2foot').innerHTML = shame + ' so <em>this</em> uploader will do like 500kB/s at best';
|
ebi('u2foot').innerHTML = shame + ' so <em>this</em> uploader will do like 500kB/s at best';
|
||||||
else
|
else
|
||||||
ebi('u2foot').innerHTML = 'seems like ' + shame + ' so do that if you want more performance';
|
ebi('u2foot').innerHTML = 'seems like ' + shame + ' so do that if you want more performance <span style="color:#' +
|
||||||
|
(sha_js == 'ac' ? 'c84">(expecting 20' : '8a5">(but dont worry too much, expect 100') + ' MiB/s)</span>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// show uploader if the user only has write-access
|
// show uploader if the user only has write-access
|
||||||
if (!ebi('files'))
|
if (perms.length && !has(perms, 'read'))
|
||||||
goto('up2k');
|
goto('up2k');
|
||||||
|
|
||||||
// shows or clears an error message in the basic uploader ui
|
// shows or clears a message in the basic uploader ui
|
||||||
function setmsg(msg) {
|
function setmsg(msg, type) {
|
||||||
if (msg !== undefined) {
|
if (msg !== undefined) {
|
||||||
ebi('u2err').setAttribute('class', 'err');
|
ebi('u2err').setAttribute('class', type);
|
||||||
ebi('u2err').innerHTML = msg;
|
ebi('u2err').innerHTML = msg;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
ebi('u2err').setAttribute('class', '');
|
ebi('u2err').setAttribute('class', '');
|
||||||
ebi('u2err').innerHTML = '';
|
ebi('u2err').innerHTML = '';
|
||||||
}
|
}
|
||||||
|
if (msg == suggest_up2k) {
|
||||||
|
ebi('u2yea').onclick = function (e) {
|
||||||
|
ev(e);
|
||||||
|
goto('up2k');
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// switches to the basic uploader with msg as error message
|
// switches to the basic uploader with msg as error message
|
||||||
function un2k(msg) {
|
function un2k(msg) {
|
||||||
setmsg(msg);
|
setmsg(msg, 'err');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle user intent to use the basic uploader instead
|
// handle user intent to use the basic uploader instead
|
||||||
ebi('u2nope').onclick = function (e) {
|
ebi('u2nope').onclick = function (e) {
|
||||||
ev(e);
|
ev(e);
|
||||||
setmsg();
|
setmsg(suggest_up2k, 'msg');
|
||||||
goto('bup');
|
goto('bup');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setmsg(suggest_up2k, 'msg');
|
||||||
|
|
||||||
if (!String.prototype.format) {
|
if (!String.prototype.format) {
|
||||||
String.prototype.format = function () {
|
String.prototype.format = function () {
|
||||||
var args = arguments;
|
var args = arguments;
|
||||||
@@ -587,14 +651,50 @@ function up2k_init(have_crypto) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function read_dirs(rd, pf, dirs, good, bad) {
|
function rd_flatten(pf, dirs) {
|
||||||
|
var ret = jcp(pf);
|
||||||
|
for (var a = 0; a < dirs.length; a++)
|
||||||
|
ret.push(dirs.fullPath || '');
|
||||||
|
|
||||||
|
ret.sort();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rd_missing_ref = [];
|
||||||
|
function read_dirs(rd, pf, dirs, good, bad, spins) {
|
||||||
|
spins = spins || 0;
|
||||||
|
if (++spins == 5)
|
||||||
|
rd_missing_ref = rd_flatten(pf, dirs);
|
||||||
|
|
||||||
|
if (spins == 200) {
|
||||||
|
var missing = rd_flatten(pf, dirs),
|
||||||
|
match = rd_missing_ref.length == missing.length,
|
||||||
|
aa = match ? missing.length : 0;
|
||||||
|
|
||||||
|
missing.sort();
|
||||||
|
for (var a = 0; a < aa; a++)
|
||||||
|
if (rd_missing_ref[a] != missing[a])
|
||||||
|
match = false;
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
var msg = ['directory iterator got stuck on the following {0} items; good chance your browser is about to spinlock:'.format(missing.length)];
|
||||||
|
for (var a = 0; a < Math.min(20, missing.length); a++)
|
||||||
|
msg.push(missing[a]);
|
||||||
|
|
||||||
|
alert(msg.join('\n-- '));
|
||||||
|
dirs = [];
|
||||||
|
pf = [];
|
||||||
|
}
|
||||||
|
spins = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (!dirs.length) {
|
if (!dirs.length) {
|
||||||
if (!pf.length)
|
if (!pf.length)
|
||||||
return gotallfiles(good, bad);
|
return gotallfiles(good, bad);
|
||||||
|
|
||||||
console.log("retry pf, " + pf.length);
|
console.log("retry pf, " + pf.length);
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
read_dirs(rd, pf, dirs, good, bad);
|
read_dirs(rd, pf, dirs, good, bad, spins);
|
||||||
}, 50);
|
}, 50);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -634,7 +734,7 @@ function up2k_init(have_crypto) {
|
|||||||
dirs.shift();
|
dirs.shift();
|
||||||
rd = null;
|
rd = null;
|
||||||
}
|
}
|
||||||
return read_dirs(rd, pf, dirs, good, bad);
|
return read_dirs(rd, pf, dirs, good, bad, spins);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,41 +759,50 @@ function up2k_init(have_crypto) {
|
|||||||
if (ask_up && !fsearch && !confirm(msg.join('\n')))
|
if (ask_up && !fsearch && !confirm(msg.join('\n')))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var seen = {},
|
||||||
|
evpath = get_evpath(),
|
||||||
|
draw_each = good_files.length < 50;
|
||||||
|
|
||||||
|
for (var a = 0; a < st.files.length; a++)
|
||||||
|
seen[st.files[a].name + '\n' + st.files[a].size] = 1;
|
||||||
|
|
||||||
for (var a = 0; a < good_files.length; a++) {
|
for (var a = 0; a < good_files.length; a++) {
|
||||||
var fobj = good_files[a][0],
|
var fobj = good_files[a][0],
|
||||||
now = Date.now(),
|
now = Date.now(),
|
||||||
lmod = fobj.lastModified || now;
|
lmod = fobj.lastModified || now;
|
||||||
|
|
||||||
var entry = {
|
var entry = {
|
||||||
"n": parseInt(st.files.length.toString()),
|
"n": st.files.length,
|
||||||
"t0": now,
|
"t0": now,
|
||||||
"fobj": fobj,
|
"fobj": fobj,
|
||||||
"name": good_files[a][1],
|
"name": good_files[a][1],
|
||||||
"size": fobj.size,
|
"size": fobj.size,
|
||||||
"lmod": lmod / 1000,
|
"lmod": lmod / 1000,
|
||||||
"purl": get_evpath(),
|
"purl": evpath,
|
||||||
"done": false,
|
"done": false,
|
||||||
"hash": []
|
"hash": []
|
||||||
};
|
},
|
||||||
|
key = entry.name + '\n' + entry.size;
|
||||||
|
|
||||||
var skip = false;
|
if (seen[key])
|
||||||
for (var b = 0; b < st.files.length; b++)
|
|
||||||
if (entry.name == st.files[b].name &&
|
|
||||||
entry.size == st.files[b].size)
|
|
||||||
skip = true;
|
|
||||||
|
|
||||||
if (skip)
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
seen[key] = 1;
|
||||||
|
|
||||||
pvis.addfile([
|
pvis.addfile([
|
||||||
fsearch ? esc(entry.name) : linksplit(
|
fsearch ? esc(entry.name) : linksplit(
|
||||||
esc(uricom_dec(entry.purl)[0] + entry.name)).join(' '),
|
uricom_dec(entry.purl)[0] + entry.name).join(' '),
|
||||||
'📐 hash',
|
'📐 hash',
|
||||||
''
|
''
|
||||||
], fobj.size);
|
], fobj.size, draw_each);
|
||||||
|
|
||||||
st.files.push(entry);
|
st.files.push(entry);
|
||||||
st.todo.hash.push(entry);
|
st.todo.hash.push(entry);
|
||||||
}
|
}
|
||||||
|
if (!draw_each) {
|
||||||
|
pvis.drawcard("q");
|
||||||
|
pvis.changecard(pvis.act);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ebi('u2btn').addEventListener('drop', gotfile, false);
|
ebi('u2btn').addEventListener('drop', gotfile, false);
|
||||||
|
|
||||||
@@ -729,9 +838,17 @@ function up2k_init(have_crypto) {
|
|||||||
|
|
||||||
function handshakes_permitted() {
|
function handshakes_permitted() {
|
||||||
var lim = multitask ? 1 : 0;
|
var lim = multitask ? 1 : 0;
|
||||||
return lim >=
|
|
||||||
|
if (lim <
|
||||||
st.todo.upload.length +
|
st.todo.upload.length +
|
||||||
st.busy.upload.length;
|
st.busy.upload.length)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var cd = st.todo.handshake.length ? st.todo.handshake[0].cooldown : 0;
|
||||||
|
if (cd && cd - Date.now() > 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hashing_permitted() {
|
function hashing_permitted() {
|
||||||
@@ -762,7 +879,7 @@ function up2k_init(have_crypto) {
|
|||||||
|
|
||||||
clearTimeout(tto);
|
clearTimeout(tto);
|
||||||
running = true;
|
running = true;
|
||||||
while (true) {
|
while (window['vis_exh']) {
|
||||||
var is_busy = 0 !=
|
var is_busy = 0 !=
|
||||||
st.todo.hash.length +
|
st.todo.hash.length +
|
||||||
st.todo.handshake.length +
|
st.todo.handshake.length +
|
||||||
@@ -792,6 +909,14 @@ function up2k_init(have_crypto) {
|
|||||||
|
|
||||||
var mou_ikkai = false;
|
var mou_ikkai = false;
|
||||||
|
|
||||||
|
if (st.busy.handshake.length > 0 &&
|
||||||
|
st.busy.handshake[0].busied < Date.now() - 30 * 1000
|
||||||
|
) {
|
||||||
|
console.log("retrying stuck handshake");
|
||||||
|
var t = st.busy.handshake.shift();
|
||||||
|
st.todo.handshake.unshift(t);
|
||||||
|
}
|
||||||
|
|
||||||
if (st.todo.handshake.length > 0 &&
|
if (st.todo.handshake.length > 0 &&
|
||||||
st.busy.handshake.length == 0 && (
|
st.busy.handshake.length == 0 && (
|
||||||
st.todo.handshake[0].t4 || (
|
st.todo.handshake[0].t4 || (
|
||||||
@@ -877,6 +1002,10 @@ function up2k_init(have_crypto) {
|
|||||||
return base64;
|
return base64;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hex2u8(txt) {
|
||||||
|
return new Uint8Array(txt.match(/.{2}/g).map(function (b) { return parseInt(b, 16); }));
|
||||||
|
}
|
||||||
|
|
||||||
function get_chunksize(filesize) {
|
function get_chunksize(filesize) {
|
||||||
var chunksize = 1024 * 1024,
|
var chunksize = 1024 * 1024,
|
||||||
stepsize = 512 * 1024;
|
stepsize = 512 * 1024;
|
||||||
@@ -923,7 +1052,7 @@ function up2k_init(have_crypto) {
|
|||||||
|
|
||||||
bpend += cdr - car;
|
bpend += cdr - car;
|
||||||
|
|
||||||
reader.onload = function (e) {
|
function orz(e) {
|
||||||
if (!min_filebuf && nch == 1) {
|
if (!min_filebuf && nch == 1) {
|
||||||
min_filebuf = 1;
|
min_filebuf = 1;
|
||||||
var td = Date.now() - t0;
|
var td = Date.now() - t0;
|
||||||
@@ -933,9 +1062,30 @@ function up2k_init(have_crypto) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
hash_calc(nch, e.target.result);
|
hash_calc(nch, e.target.result);
|
||||||
|
}
|
||||||
|
reader.onload = function (e) {
|
||||||
|
try { orz(e); } catch (ex) { vis_exh(ex + '', '', '', '', ex); }
|
||||||
};
|
};
|
||||||
reader.onerror = function () {
|
reader.onerror = function () {
|
||||||
alert('y o u b r o k e i t\nerror: ' + reader.error);
|
var err = reader.error + '';
|
||||||
|
var handled = false;
|
||||||
|
|
||||||
|
if (err.indexOf('NotReadableError') !== -1 || // win10-chrome defender
|
||||||
|
err.indexOf('NotFoundError') !== -1 // macos-firefox permissions
|
||||||
|
) {
|
||||||
|
pvis.seth(t.n, 1, 'OS-error');
|
||||||
|
pvis.seth(t.n, 2, err);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handled) {
|
||||||
|
pvis.move(t.n, 'ng');
|
||||||
|
st.busy.hash.splice(st.busy.hash.indexOf(t), 1);
|
||||||
|
st.bytes.uploaded += t.size;
|
||||||
|
return tasker();
|
||||||
|
}
|
||||||
|
|
||||||
|
alert('y o u b r o k e i t\nfile: ' + t.name + '\nerror: ' + err);
|
||||||
};
|
};
|
||||||
reader.readAsArrayBuffer(
|
reader.readAsArrayBuffer(
|
||||||
bobslice.call(t.fobj, car, cdr));
|
bobslice.call(t.fobj, car, cdr));
|
||||||
@@ -947,8 +1097,8 @@ function up2k_init(have_crypto) {
|
|||||||
while (segm_next());
|
while (segm_next());
|
||||||
|
|
||||||
var hash_done = function (hashbuf) {
|
var hash_done = function (hashbuf) {
|
||||||
var hslice = new Uint8Array(hashbuf).subarray(0, 32),
|
var hslice = new Uint8Array(hashbuf).subarray(0, 33),
|
||||||
b64str = buf2b64(hslice).replace(/=$/, '');
|
b64str = buf2b64(hslice);
|
||||||
|
|
||||||
hashtab[nch] = b64str;
|
hashtab[nch] = b64str;
|
||||||
t.hash.push(nch);
|
t.hash.push(nch);
|
||||||
@@ -973,16 +1123,25 @@ function up2k_init(have_crypto) {
|
|||||||
pvis.seth(t.n, 1, '📦 wait');
|
pvis.seth(t.n, 1, '📦 wait');
|
||||||
st.busy.hash.splice(st.busy.hash.indexOf(t), 1);
|
st.busy.hash.splice(st.busy.hash.indexOf(t), 1);
|
||||||
st.todo.handshake.push(t);
|
st.todo.handshake.push(t);
|
||||||
|
tasker();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (have_crypto)
|
if (subtle)
|
||||||
crypto.subtle.digest('SHA-512', buf).then(hash_done);
|
subtle.digest('SHA-512', buf).then(hash_done);
|
||||||
else {
|
else setTimeout(function () {
|
||||||
var hasher = new asmCrypto.Sha512();
|
var u8buf = new Uint8Array(buf);
|
||||||
hasher.process(new Uint8Array(buf));
|
if (sha_js == 'hw') {
|
||||||
hasher.finish();
|
hashwasm.sha512(u8buf).then(function (v) {
|
||||||
hash_done(hasher.result);
|
hash_done(hex2u8(v))
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var hasher = new asmCrypto.Sha512();
|
||||||
|
hasher.process(u8buf);
|
||||||
|
hasher.finish();
|
||||||
|
hash_done(hasher.result);
|
||||||
|
}
|
||||||
|
}, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
t.t1 = Date.now();
|
t.t1 = Date.now();
|
||||||
@@ -995,11 +1154,28 @@ function up2k_init(have_crypto) {
|
|||||||
//
|
//
|
||||||
|
|
||||||
function exec_handshake() {
|
function exec_handshake() {
|
||||||
var t = st.todo.handshake.shift();
|
var t = st.todo.handshake.shift(),
|
||||||
|
me = Date.now();
|
||||||
|
|
||||||
st.busy.handshake.push(t);
|
st.busy.handshake.push(t);
|
||||||
|
t.busied = me;
|
||||||
|
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.onload = function (e) {
|
xhr.onerror = function () {
|
||||||
|
if (t.busied != me) {
|
||||||
|
console.log('zombie handshake onerror,', t);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('handshake onerror, retrying', t);
|
||||||
|
st.busy.handshake.splice(st.busy.handshake.indexOf(t), 1);
|
||||||
|
st.todo.handshake.unshift(t);
|
||||||
|
tasker();
|
||||||
|
};
|
||||||
|
function orz(e) {
|
||||||
|
if (t.busied != me) {
|
||||||
|
console.log('zombie handshake onload,', t);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (xhr.status == 200) {
|
if (xhr.status == 200) {
|
||||||
var response = JSON.parse(xhr.responseText);
|
var response = JSON.parse(xhr.responseText);
|
||||||
|
|
||||||
@@ -1014,7 +1190,7 @@ function up2k_init(have_crypto) {
|
|||||||
else {
|
else {
|
||||||
smsg = 'found';
|
smsg = 'found';
|
||||||
var hit = response.hits[0],
|
var hit = response.hits[0],
|
||||||
msg = linksplit(esc(hit.rp)).join(''),
|
msg = linksplit(hit.rp).join(''),
|
||||||
tr = unix2iso(hit.ts),
|
tr = unix2iso(hit.ts),
|
||||||
tu = unix2iso(t.lmod),
|
tu = unix2iso(t.lmod),
|
||||||
diff = parseInt(t.lmod) - parseInt(hit.ts),
|
diff = parseInt(t.lmod) - parseInt(hit.ts),
|
||||||
@@ -1035,8 +1211,9 @@ function up2k_init(have_crypto) {
|
|||||||
|
|
||||||
if (response.name !== t.name) {
|
if (response.name !== t.name) {
|
||||||
// file exists; server renamed us
|
// file exists; server renamed us
|
||||||
|
console.log("server-rename [" + t.name + "] to [" + response.name + "]");
|
||||||
t.name = response.name;
|
t.name = response.name;
|
||||||
pvis.seth(t.n, 0, linksplit(esc(t.purl + t.name)).join(' '));
|
pvis.seth(t.n, 0, linksplit(t.purl + t.name).join(' '));
|
||||||
}
|
}
|
||||||
|
|
||||||
var chunksize = get_chunksize(t.size),
|
var chunksize = get_chunksize(t.size),
|
||||||
@@ -1106,6 +1283,15 @@ function up2k_init(have_crypto) {
|
|||||||
if (rsp.indexOf('<pre>') === 0)
|
if (rsp.indexOf('<pre>') === 0)
|
||||||
rsp = rsp.slice(5);
|
rsp = rsp.slice(5);
|
||||||
|
|
||||||
|
if (rsp.indexOf('rate-limit ') !== -1) {
|
||||||
|
var penalty = rsp.replace(/.*rate-limit /, "").split(' ')[0];
|
||||||
|
console.log("rate-limit: " + penalty);
|
||||||
|
t.cooldown = Date.now() + parseFloat(penalty) * 1000;
|
||||||
|
st.busy.handshake.splice(st.busy.handshake.indexOf(t), 1);
|
||||||
|
st.todo.handshake.unshift(t);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
st.bytes.uploaded += t.size;
|
st.bytes.uploaded += t.size;
|
||||||
if (rsp.indexOf('partial upload exists') !== -1 ||
|
if (rsp.indexOf('partial upload exists') !== -1 ||
|
||||||
rsp.indexOf('file already exists') !== -1) {
|
rsp.indexOf('file already exists') !== -1) {
|
||||||
@@ -1130,6 +1316,9 @@ function up2k_init(have_crypto) {
|
|||||||
(xhr.responseText && xhr.responseText) ||
|
(xhr.responseText && xhr.responseText) ||
|
||||||
"no further information"));
|
"no further information"));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
xhr.onload = function (e) {
|
||||||
|
try { orz(e); } catch (ex) { vis_exh(ex + '', '', '', '', ex); }
|
||||||
};
|
};
|
||||||
|
|
||||||
var req = {
|
var req = {
|
||||||
@@ -1170,40 +1359,56 @@ function up2k_init(have_crypto) {
|
|||||||
if (cdr >= t.size)
|
if (cdr >= t.size)
|
||||||
cdr = t.size;
|
cdr = t.size;
|
||||||
|
|
||||||
var xhr = new XMLHttpRequest();
|
function orz(xhr) {
|
||||||
xhr.upload.onprogress = function (xev) {
|
var txt = ((xhr.response && xhr.response.err) || xhr.responseText) + '';
|
||||||
pvis.prog(t, npart, xev.loaded);
|
|
||||||
};
|
|
||||||
xhr.onload = function (xev) {
|
|
||||||
if (xhr.status == 200) {
|
if (xhr.status == 200) {
|
||||||
pvis.prog(t, npart, cdr - car);
|
pvis.prog(t, npart, cdr - car);
|
||||||
st.bytes.uploaded += cdr - car;
|
st.bytes.uploaded += cdr - car;
|
||||||
t.bytes_uploaded += cdr - car;
|
t.bytes_uploaded += cdr - car;
|
||||||
st.busy.upload.splice(st.busy.upload.indexOf(upt), 1);
|
|
||||||
t.postlist.splice(t.postlist.indexOf(npart), 1);
|
|
||||||
if (t.postlist.length == 0) {
|
|
||||||
t.t4 = Date.now();
|
|
||||||
pvis.seth(t.n, 1, 'verifying');
|
|
||||||
st.todo.handshake.unshift(t);
|
|
||||||
}
|
|
||||||
tasker();
|
|
||||||
}
|
}
|
||||||
else
|
else if (txt.indexOf('already got that') !== -1) {
|
||||||
|
console.log("ignoring dupe-segment error", t);
|
||||||
|
}
|
||||||
|
else {
|
||||||
alert("server broke; cu-err {0} on file [{1}]:\n".format(
|
alert("server broke; cu-err {0} on file [{1}]:\n".format(
|
||||||
xhr.status, t.name) + (
|
xhr.status, t.name) + (txt || "no further information"));
|
||||||
(xhr.response && xhr.response.err) ||
|
return;
|
||||||
(xhr.responseText && xhr.responseText) ||
|
}
|
||||||
"no further information"));
|
st.busy.upload.splice(st.busy.upload.indexOf(upt), 1);
|
||||||
};
|
t.postlist.splice(t.postlist.indexOf(npart), 1);
|
||||||
xhr.open('POST', t.purl + 'chunkpit.php', true);
|
if (t.postlist.length == 0) {
|
||||||
xhr.setRequestHeader("X-Up2k-Hash", t.hash[npart]);
|
t.t4 = Date.now();
|
||||||
xhr.setRequestHeader("X-Up2k-Wark", t.wark);
|
pvis.seth(t.n, 1, 'verifying');
|
||||||
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
st.todo.handshake.unshift(t);
|
||||||
if (xhr.overrideMimeType)
|
}
|
||||||
xhr.overrideMimeType('Content-Type', 'application/octet-stream');
|
tasker();
|
||||||
|
}
|
||||||
|
function do_send() {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.upload.onprogress = function (xev) {
|
||||||
|
pvis.prog(t, npart, xev.loaded);
|
||||||
|
};
|
||||||
|
xhr.onload = function (xev) {
|
||||||
|
try { orz(xhr); } catch (ex) { vis_exh(ex + '', '', '', '', ex); }
|
||||||
|
};
|
||||||
|
xhr.onerror = function (xev) {
|
||||||
|
if (!window['vis_exh'])
|
||||||
|
return;
|
||||||
|
|
||||||
xhr.responseType = 'text';
|
console.log('chunkpit onerror, retrying', t);
|
||||||
xhr.send(bobslice.call(t.fobj, car, cdr));
|
do_send();
|
||||||
|
};
|
||||||
|
xhr.open('POST', t.purl + 'chunkpit.php', true);
|
||||||
|
xhr.setRequestHeader("X-Up2k-Hash", t.hash[npart]);
|
||||||
|
xhr.setRequestHeader("X-Up2k-Wark", t.wark);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
||||||
|
if (xhr.overrideMimeType)
|
||||||
|
xhr.overrideMimeType('Content-Type', 'application/octet-stream');
|
||||||
|
|
||||||
|
xhr.responseType = 'text';
|
||||||
|
xhr.send(bobslice.call(t.fobj, car, cdr));
|
||||||
|
}
|
||||||
|
do_send();
|
||||||
}
|
}
|
||||||
|
|
||||||
/////
|
/////
|
||||||
@@ -1217,7 +1422,7 @@ function up2k_init(have_crypto) {
|
|||||||
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,
|
wide = wem > 54,
|
||||||
parent = ebi(wide ? 'u2btn_cw' : 'u2btn_ct'),
|
parent = ebi(wide && has(perms, 'write') ? 'u2btn_cw' : 'u2btn_ct'),
|
||||||
btn = ebi('u2btn');
|
btn = ebi('u2btn');
|
||||||
|
|
||||||
//console.log([wpx, fpx, wem]);
|
//console.log([wpx, fpx, wem]);
|
||||||
@@ -1230,26 +1435,28 @@ function up2k_init(have_crypto) {
|
|||||||
window.addEventListener('resize', onresize);
|
window.addEventListener('resize', onresize);
|
||||||
onresize();
|
onresize();
|
||||||
|
|
||||||
function desc_show(e) {
|
if (is_touch) {
|
||||||
var msg = this.getAttribute('alt'),
|
// android-chrome wobbles for a bit; firefox / iOS-safari are OK
|
||||||
cdesc = ebi('u2cdesc');
|
setTimeout(onresize, 20);
|
||||||
|
setTimeout(onresize, 100);
|
||||||
|
setTimeout(onresize, 500);
|
||||||
|
}
|
||||||
|
|
||||||
cdesc.innerHTML = msg.replace(/\$N/g, "<br />");
|
var o = QSA('#u2conf *[tt]');
|
||||||
cdesc.setAttribute('class', 'show');
|
|
||||||
}
|
|
||||||
function desc_hide(e) {
|
|
||||||
ebi('u2cdesc').setAttribute('class', '');
|
|
||||||
}
|
|
||||||
var o = QSA('#u2conf *[alt]');
|
|
||||||
for (var a = o.length - 1; a >= 0; a--) {
|
for (var a = o.length - 1; a >= 0; a--) {
|
||||||
o[a].parentNode.getElementsByTagName('input')[0].setAttribute('alt', o[a].getAttribute('alt'));
|
o[a].parentNode.getElementsByTagName('input')[0].setAttribute('tt', o[a].getAttribute('tt'));
|
||||||
}
|
}
|
||||||
var o = QSA('#u2conf *[alt]');
|
tt.init();
|
||||||
for (var a = 0; a < o.length; a++) {
|
|
||||||
o[a].onfocus = desc_show;
|
function bumpthread2(e) {
|
||||||
o[a].onblur = desc_hide;
|
if (e.ctrlKey || e.altKey || e.metaKey || e.isComposing)
|
||||||
o[a].onmouseenter = desc_show;
|
return;
|
||||||
o[a].onmouseleave = desc_hide;
|
|
||||||
|
if (e.code == 'ArrowUp')
|
||||||
|
bumpthread(1);
|
||||||
|
|
||||||
|
if (e.code == 'ArrowDown')
|
||||||
|
bumpthread(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function bumpthread(dir) {
|
function bumpthread(dir) {
|
||||||
@@ -1262,7 +1469,7 @@ function up2k_init(have_crypto) {
|
|||||||
if (dir.target) {
|
if (dir.target) {
|
||||||
clmod(obj, 'err', 1);
|
clmod(obj, 'err', 1);
|
||||||
var v = Math.floor(parseInt(obj.value));
|
var v = Math.floor(parseInt(obj.value));
|
||||||
if (v < 1 || v > 8 || v !== v)
|
if (v < 0 || v > 64 || v !== v)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
parallel_uploads = v;
|
parallel_uploads = v;
|
||||||
@@ -1273,11 +1480,11 @@ function up2k_init(have_crypto) {
|
|||||||
|
|
||||||
parallel_uploads += dir;
|
parallel_uploads += dir;
|
||||||
|
|
||||||
if (parallel_uploads < 1)
|
if (parallel_uploads < 0)
|
||||||
parallel_uploads = 1;
|
parallel_uploads = 0;
|
||||||
|
|
||||||
if (parallel_uploads > 8)
|
if (parallel_uploads > 16)
|
||||||
parallel_uploads = 8;
|
parallel_uploads = 16;
|
||||||
|
|
||||||
obj.value = parallel_uploads;
|
obj.value = parallel_uploads;
|
||||||
bumpthread({ "target": 1 })
|
bumpthread({ "target": 1 })
|
||||||
@@ -1298,15 +1505,20 @@ function up2k_init(have_crypto) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function set_fsearch(new_state) {
|
function set_fsearch(new_state) {
|
||||||
var perms = document.body.getAttribute('perms'),
|
var fixed = false;
|
||||||
read_only = false;
|
|
||||||
|
|
||||||
if (!ebi('fsearch')) {
|
if (!ebi('fsearch')) {
|
||||||
new_state = false;
|
new_state = false;
|
||||||
}
|
}
|
||||||
else if (perms && perms.indexOf('write') === -1) {
|
else if (perms.length) {
|
||||||
new_state = true;
|
if (!has(perms, 'write')) {
|
||||||
read_only = true;
|
new_state = true;
|
||||||
|
fixed = true;
|
||||||
|
}
|
||||||
|
if (!has(perms, 'read')) {
|
||||||
|
new_state = false;
|
||||||
|
fixed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (new_state !== undefined) {
|
if (new_state !== undefined) {
|
||||||
@@ -1315,7 +1527,7 @@ function up2k_init(have_crypto) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
QS('label[for="fsearch"]').style.opacity = read_only ? '0' : '1';
|
QS('label[for="fsearch"]').style.display = QS('#fsearch').style.display = fixed ? 'none' : '';
|
||||||
}
|
}
|
||||||
catch (ex) { }
|
catch (ex) { }
|
||||||
|
|
||||||
@@ -1328,6 +1540,8 @@ function up2k_init(have_crypto) {
|
|||||||
ebi('u2bm').innerHTML = ico + ' <sup>' + desc + '</sup>';
|
ebi('u2bm').innerHTML = ico + ' <sup>' + desc + '</sup>';
|
||||||
}
|
}
|
||||||
catch (ex) { }
|
catch (ex) { }
|
||||||
|
|
||||||
|
onresize();
|
||||||
}
|
}
|
||||||
|
|
||||||
function tgl_flag_en() {
|
function tgl_flag_en() {
|
||||||
@@ -1366,6 +1580,7 @@ function up2k_init(have_crypto) {
|
|||||||
bumpthread(-1);
|
bumpthread(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ebi('nthread').onkeydown = bumpthread2;
|
||||||
ebi('nthread').addEventListener('input', bumpthread, false);
|
ebi('nthread').addEventListener('input', bumpthread, false);
|
||||||
ebi('multitask').addEventListener('click', tgl_multitask, false);
|
ebi('multitask').addEventListener('click', tgl_multitask, false);
|
||||||
ebi('ask_up').addEventListener('click', tgl_ask_up, false);
|
ebi('ask_up').addEventListener('click', tgl_ask_up, false);
|
||||||
@@ -1379,7 +1594,10 @@ function up2k_init(have_crypto) {
|
|||||||
nodes[a].addEventListener('touchend', nop, false);
|
nodes[a].addEventListener('touchend', nop, false);
|
||||||
|
|
||||||
set_fsearch();
|
set_fsearch();
|
||||||
bumpthread({ "target": 1 })
|
bumpthread({ "target": 1 });
|
||||||
|
if (parallel_uploads < 1)
|
||||||
|
bumpthread(1);
|
||||||
|
|
||||||
return { "init_deps": init_deps, "set_fsearch": set_fsearch }
|
return { "init_deps": init_deps, "set_fsearch": set_fsearch }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1391,5 +1609,9 @@ function warn_uploader_busy(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
tt.init();
|
||||||
|
|
||||||
if (QS('#op_up2k.act'))
|
if (QS('#op_up2k.act'))
|
||||||
goto_up2k();
|
goto_up2k();
|
||||||
|
|
||||||
|
apply_perms(perms);
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
color: #f87;
|
color: #f87;
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
}
|
}
|
||||||
|
#u2err.msg {
|
||||||
|
color: #999;
|
||||||
|
padding: .5em;
|
||||||
|
font-size: .9em;
|
||||||
|
}
|
||||||
#u2btn {
|
#u2btn {
|
||||||
color: #eee;
|
color: #eee;
|
||||||
background: #555;
|
background: #555;
|
||||||
@@ -92,6 +97,7 @@
|
|||||||
#u2cards {
|
#u2cards {
|
||||||
padding: 1em 0 .3em 1em;
|
padding: 1em 0 .3em 1em;
|
||||||
margin: 1.5em auto -2.5em auto;
|
margin: 1.5em auto -2.5em auto;
|
||||||
|
white-space: nowrap;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -205,33 +211,14 @@
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
opacity: .2;
|
opacity: .2;
|
||||||
}
|
}
|
||||||
#u2cdesc {
|
|
||||||
position: absolute;
|
|
||||||
width: 34em;
|
|
||||||
left: calc(50% - 15em);
|
|
||||||
background: #222;
|
|
||||||
border: 0 solid #555;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 0 -2em;
|
|
||||||
padding: 0 1em;
|
|
||||||
height: 0;
|
|
||||||
opacity: .1;
|
|
||||||
transition: all 0.14s ease-in-out;
|
|
||||||
box-shadow: 0 .2em .5em #222;
|
|
||||||
border-radius: .4em;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
#u2cdesc.show {
|
|
||||||
padding: 1em;
|
|
||||||
height: auto;
|
|
||||||
border-width: .2em 0;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
#u2foot {
|
#u2foot {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
#u2foot span {
|
||||||
|
color: #999;
|
||||||
|
font-size: .9em;
|
||||||
|
}
|
||||||
#u2footfoot {
|
#u2footfoot {
|
||||||
margin-bottom: -1em;
|
margin-bottom: -1em;
|
||||||
}
|
}
|
||||||
@@ -276,10 +263,6 @@ html.light #u2conf .txtbox.err {
|
|||||||
background: #f96;
|
background: #f96;
|
||||||
color: #300;
|
color: #300;
|
||||||
}
|
}
|
||||||
html.light #u2cdesc {
|
|
||||||
background: #fff;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
html.light #op_up2k.srch #u2btn {
|
html.light #op_up2k.srch #u2btn {
|
||||||
border-color: #a80;
|
border-color: #a80;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
|
|
||||||
<div id="op_bup" class="opview opbox act">
|
|
||||||
<div id="u2err"></div>
|
|
||||||
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
|
||||||
<input type="hidden" name="act" value="bput" />
|
|
||||||
<input type="file" name="f" multiple><br />
|
|
||||||
<input type="submit" value="start upload">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="op_mkdir" class="opview opbox act">
|
|
||||||
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
|
||||||
<input type="hidden" name="act" value="mkdir" />
|
|
||||||
<input type="text" name="name" size="30">
|
|
||||||
<input type="submit" value="mkdir">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="op_new_md" class="opview opbox">
|
|
||||||
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
|
||||||
<input type="hidden" name="act" value="new_md" />
|
|
||||||
<input type="text" name="name" size="30">
|
|
||||||
<input type="submit" value="create doc">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="op_msg" class="opview opbox act">
|
|
||||||
<form method="post" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" action="{{ url_suf }}">
|
|
||||||
<input type="text" name="msg" size="30">
|
|
||||||
<input type="submit" value="send msg">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="op_up2k" class="opview">
|
|
||||||
<form id="u2form" method="post" enctype="multipart/form-data" onsubmit="return false;"></form>
|
|
||||||
|
|
||||||
<table id="u2conf">
|
|
||||||
<tr>
|
|
||||||
<td>parallel uploads</td>
|
|
||||||
<td rowspan="2">
|
|
||||||
<input type="checkbox" id="multitask" />
|
|
||||||
<label for="multitask" alt="continue hashing other files while uploading">🏃</label>
|
|
||||||
</td>
|
|
||||||
<td rowspan="2">
|
|
||||||
<input type="checkbox" id="ask_up" />
|
|
||||||
<label for="ask_up" alt="ask for confirmation befofre upload starts">💭</label>
|
|
||||||
</td>
|
|
||||||
<td rowspan="2">
|
|
||||||
<input type="checkbox" id="flag_en" />
|
|
||||||
<label for="flag_en" alt="ensure only one tab is uploading at a time $N (other tabs must have this enabled too)">💤</label>
|
|
||||||
</td>
|
|
||||||
{%- if have_up2k_idx %}
|
|
||||||
<td data-perm="read" rowspan="2">
|
|
||||||
<input type="checkbox" id="fsearch" />
|
|
||||||
<label for="fsearch" alt="don't actually upload, instead check if the files already $N exist on the server (will scan all folders you can read)">🔎</label>
|
|
||||||
</td>
|
|
||||||
{%- endif %}
|
|
||||||
<td data-perm="read" rowspan="2" id="u2btn_cw"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="#" id="nthread_sub">–</a><input
|
|
||||||
class="txtbox" id="nthread" value="2"/><a
|
|
||||||
href="#" id="nthread_add">+</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div id="u2cdesc"></div>
|
|
||||||
|
|
||||||
<div id="u2notbtn"></div>
|
|
||||||
|
|
||||||
<div id="u2btn_ct">
|
|
||||||
<div id="u2btn">
|
|
||||||
<span id="u2bm"></span><br />
|
|
||||||
drag/drop files<br />
|
|
||||||
and folders here<br />
|
|
||||||
(or click me)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="u2cards">
|
|
||||||
<a href="#" act="ok">ok <span>0</span></a><a
|
|
||||||
href="#" act="ng">ng <span>0</span></a><a
|
|
||||||
href="#" act="done">done <span>0</span></a><a
|
|
||||||
href="#" act="bz" class="act">busy <span>0</span></a><a
|
|
||||||
href="#" act="q">que <span>0</span></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table id="u2tab">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<td>filename</td>
|
|
||||||
<td>status</td>
|
|
||||||
<td>progress<a href="#" id="u2cleanup">cleanup</a></td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p id="u2foot"></p>
|
|
||||||
<p id="u2footfoot">( if you don't need lastmod timestamps, resumable uploads or progress bars just use the <a href="#" id="u2nope">basic uploader</a>)</p>
|
|
||||||
</div>
|
|
||||||
@@ -6,21 +6,11 @@ if (!window['console'])
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
var clickev = window.Touch ? 'touchstart' : 'click',
|
var is_touch = 'ontouchstart' in window,
|
||||||
ANDROID = /(android)/i.test(navigator.userAgent);
|
ANDROID = /(android)/i.test(navigator.userAgent);
|
||||||
|
|
||||||
|
|
||||||
// error handler for mobile devices
|
// error handler for mobile devices
|
||||||
function hcroak(msg) {
|
|
||||||
document.body.innerHTML = msg;
|
|
||||||
window.onerror = undefined;
|
|
||||||
throw 'fatal_err';
|
|
||||||
}
|
|
||||||
function croak(msg) {
|
|
||||||
document.body.textContent = msg;
|
|
||||||
window.onerror = undefined;
|
|
||||||
throw msg;
|
|
||||||
}
|
|
||||||
function esc(txt) {
|
function esc(txt) {
|
||||||
return txt.replace(/[&"<>]/g, function (c) {
|
return txt.replace(/[&"<>]/g, function (c) {
|
||||||
return {
|
return {
|
||||||
@@ -32,9 +22,12 @@ function esc(txt) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function vis_exh(msg, url, lineNo, columnNo, error) {
|
function vis_exh(msg, url, lineNo, columnNo, error) {
|
||||||
|
if (!window.onerror)
|
||||||
|
return;
|
||||||
|
|
||||||
window.onerror = undefined;
|
window.onerror = undefined;
|
||||||
window['vis_exh'] = null;
|
window['vis_exh'] = null;
|
||||||
var html = ['<h1>you hit a bug!</h1><p>please screenshot this error and send me a copy arigathanks gozaimuch (ed/irc.rizon.net or ed#2644)</p><p>',
|
var html = ['<h1>you hit a bug!</h1><p>please send me a screenshot arigathanks gozaimuch: <code>ed/irc.rizon.net</code> or <code>ed#2644</code><br /> (and if you can, press F12 and include the "Console" tab in the screenshot too)</p><p>',
|
||||||
esc(String(msg)), '</p><p>', esc(url + ' @' + lineNo + ':' + columnNo), '</p>'];
|
esc(String(msg)), '</p><p>', esc(url + ' @' + lineNo + ':' + columnNo), '</p>'];
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -44,9 +37,13 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
|
|||||||
html.push('<h2>' + find[a] + '</h2>' +
|
html.push('<h2>' + find[a] + '</h2>' +
|
||||||
esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
|
esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
|
||||||
}
|
}
|
||||||
document.body.style.fontSize = '0.8em';
|
document.body.innerHTML = html.join('\n');
|
||||||
document.body.style.padding = '0 1em 1em 1em';
|
|
||||||
hcroak(html.join('\n'));
|
var s = mknod('style');
|
||||||
|
s.innerHTML = 'body{background:#333;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em} code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} *{line-height:1.5em}';
|
||||||
|
document.head.appendChild(s);
|
||||||
|
|
||||||
|
throw 'fatal_err';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -67,6 +64,9 @@ function ev(e) {
|
|||||||
if (e.stopPropagation)
|
if (e.stopPropagation)
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (e.stopImmediatePropagation)
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
|
||||||
e.returnValue = false;
|
e.returnValue = false;
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
@@ -135,7 +135,7 @@ function clmod(obj, cls, add) {
|
|||||||
|
|
||||||
|
|
||||||
function sortfiles(nodes) {
|
function sortfiles(nodes) {
|
||||||
var sopts = jread('fsort', [["lead", -1, ""], ["href", 1, ""]]);
|
var sopts = jread('fsort', [["href", 1, ""]]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var is_srch = false;
|
var is_srch = false;
|
||||||
@@ -152,6 +152,9 @@ function sortfiles(nodes) {
|
|||||||
if (!name)
|
if (!name)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (name == 'ts')
|
||||||
|
typ = 'int';
|
||||||
|
|
||||||
if (name.indexOf('tags/') === 0) {
|
if (name.indexOf('tags/') === 0) {
|
||||||
name = name.slice(5);
|
name = name.slice(5);
|
||||||
for (var b = 0, bb = nodes.length; b < bb; b++)
|
for (var b = 0, bb = nodes.length; b < bb; b++)
|
||||||
@@ -163,8 +166,12 @@ function sortfiles(nodes) {
|
|||||||
|
|
||||||
if ((v + '').indexOf('<a ') === 0)
|
if ((v + '').indexOf('<a ') === 0)
|
||||||
v = v.split('>')[1];
|
v = v.split('>')[1];
|
||||||
else if (name == "href" && v)
|
else if (name == "href" && v) {
|
||||||
|
if (v.slice(-1) == '/')
|
||||||
|
v = '\t' + v;
|
||||||
|
|
||||||
v = uricom_dec(v)[0]
|
v = uricom_dec(v)[0]
|
||||||
|
}
|
||||||
|
|
||||||
nodes[b]._sv = v;
|
nodes[b]._sv = v;
|
||||||
}
|
}
|
||||||
@@ -198,6 +205,8 @@ function sortfiles(nodes) {
|
|||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
console.log("failed to apply sort config: " + ex);
|
console.log("failed to apply sort config: " + ex);
|
||||||
|
console.log("resetting fsort " + sread('fsort'))
|
||||||
|
localStorage.removeItem('fsort');
|
||||||
}
|
}
|
||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
@@ -276,63 +285,6 @@ function makeSortable(table, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
var ops = QSA('#ops>a');
|
|
||||||
for (var a = 0; a < ops.length; a++) {
|
|
||||||
ops[a].onclick = opclick;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
function opclick(e) {
|
|
||||||
ev(e);
|
|
||||||
|
|
||||||
var dest = this.getAttribute('data-dest');
|
|
||||||
goto(dest);
|
|
||||||
|
|
||||||
swrite('opmode', dest || null);
|
|
||||||
|
|
||||||
var input = QS('.opview.act input:not([type="hidden"])')
|
|
||||||
if (input)
|
|
||||||
input.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function goto(dest) {
|
|
||||||
var obj = QSA('.opview.act');
|
|
||||||
for (var a = obj.length - 1; a >= 0; a--)
|
|
||||||
clmod(obj[a], 'act');
|
|
||||||
|
|
||||||
obj = QSA('#ops>a');
|
|
||||||
for (var a = obj.length - 1; a >= 0; a--)
|
|
||||||
clmod(obj[a], 'act');
|
|
||||||
|
|
||||||
if (dest) {
|
|
||||||
var ui = ebi('op_' + dest);
|
|
||||||
clmod(ui, 'act', true);
|
|
||||||
QS('#ops>a[data-dest=' + dest + ']').className += " act";
|
|
||||||
|
|
||||||
var fn = window['goto_' + dest];
|
|
||||||
if (fn)
|
|
||||||
fn();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window['treectl'])
|
|
||||||
treectl.onscroll();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
goto();
|
|
||||||
var op = sread('opmode');
|
|
||||||
if (op !== null && op !== '.')
|
|
||||||
try {
|
|
||||||
goto(op);
|
|
||||||
}
|
|
||||||
catch (ex) { }
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
function linksplit(rp) {
|
function linksplit(rp) {
|
||||||
var ret = [];
|
var ret = [];
|
||||||
var apath = '/';
|
var apath = '/';
|
||||||
@@ -349,12 +301,16 @@ function linksplit(rp) {
|
|||||||
link = rp.slice(0, ofs + 1);
|
link = rp.slice(0, ofs + 1);
|
||||||
rp = rp.slice(ofs + 1);
|
rp = rp.slice(ofs + 1);
|
||||||
}
|
}
|
||||||
var vlink = link;
|
var vlink = esc(link),
|
||||||
if (link.indexOf('/') !== -1)
|
elink = uricom_enc(link);
|
||||||
vlink = link.slice(0, -1) + '<span>/</span>';
|
|
||||||
|
|
||||||
ret.push('<a href="' + apath + link + '">' + vlink + '</a>');
|
if (link.indexOf('/') !== -1) {
|
||||||
apath += link;
|
vlink = vlink.slice(0, -1) + '<span>/</span>';
|
||||||
|
elink = elink.slice(0, -3) + '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.push('<a href="' + apath + elink + '">' + vlink + '</a>');
|
||||||
|
apath += elink;
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -403,6 +359,15 @@ function get_vpath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function get_pwd() {
|
||||||
|
var pwd = ('; ' + document.cookie).split('; cppwd=');
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
@@ -424,6 +389,11 @@ function has(haystack, needle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function jcp(obj) {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function sread(key) {
|
function sread(key) {
|
||||||
if (window.localStorage)
|
if (window.localStorage)
|
||||||
return localStorage.getItem(key);
|
return localStorage.getItem(key);
|
||||||
@@ -456,11 +426,15 @@ function jwrite(key, val) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function icfg_get(name, defval) {
|
function icfg_get(name, defval) {
|
||||||
|
return parseInt(fcfg_get(name, defval));
|
||||||
|
}
|
||||||
|
|
||||||
|
function fcfg_get(name, defval) {
|
||||||
var o = ebi(name);
|
var o = ebi(name);
|
||||||
|
|
||||||
var val = parseInt(sread(name));
|
var val = parseFloat(sread(name));
|
||||||
if (isNaN(val))
|
if (isNaN(val))
|
||||||
return parseInt(o ? o.value : defval);
|
return parseFloat(o ? o.value : defval);
|
||||||
|
|
||||||
if (o)
|
if (o)
|
||||||
o.value = val;
|
o.value = val;
|
||||||
@@ -511,3 +485,67 @@ function hist_replace(url) {
|
|||||||
console.log("h-repl " + url);
|
console.log("h-repl " + url);
|
||||||
history.replaceState(url, url, url);
|
history.replaceState(url, url, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var tt = (function () {
|
||||||
|
var r = {
|
||||||
|
"tt": mknod("div"),
|
||||||
|
"en": true
|
||||||
|
};
|
||||||
|
|
||||||
|
r.tt.setAttribute('id', 'tt');
|
||||||
|
document.body.appendChild(r.tt);
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
var cfg = sread('tooltips');
|
||||||
|
if (cfg !== null && cfg != '1')
|
||||||
|
return;
|
||||||
|
|
||||||
|
var msg = this.getAttribute('tt');
|
||||||
|
if (!msg)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var pos = this.getBoundingClientRect(),
|
||||||
|
left = pos.left < window.innerWidth / 2,
|
||||||
|
top = pos.top < window.innerHeight / 2;
|
||||||
|
|
||||||
|
r.tt.style.top = top ? pos.bottom + 'px' : 'auto';
|
||||||
|
r.tt.style.bottom = top ? 'auto' : (window.innerHeight - pos.top) + 'px';
|
||||||
|
r.tt.style.left = left ? pos.left + 'px' : 'auto';
|
||||||
|
r.tt.style.right = left ? 'auto' : (window.innerWidth - pos.right) + 'px';
|
||||||
|
|
||||||
|
r.tt.innerHTML = msg.replace(/\$N/g, "<br />");
|
||||||
|
clmod(r.tt, 'show', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
clmod(r.tt, 'show');
|
||||||
|
}
|
||||||
|
|
||||||
|
r.init = function () {
|
||||||
|
var ttb = ebi('tooltips');
|
||||||
|
if (ttb) {
|
||||||
|
ttb.onclick = function (e) {
|
||||||
|
ev(e);
|
||||||
|
r.en = !r.en;
|
||||||
|
bcfg_set('tooltips', r.en);
|
||||||
|
r.init();
|
||||||
|
};
|
||||||
|
r.en = bcfg_get('tooltips', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _show = r.en ? show : null,
|
||||||
|
_hide = r.en ? hide : null;
|
||||||
|
|
||||||
|
var o = QSA('*[tt]');
|
||||||
|
for (var a = o.length - 1; a >= 0; a--) {
|
||||||
|
o[a].onfocus = _show;
|
||||||
|
o[a].onblur = _hide;
|
||||||
|
o[a].onmouseenter = _show;
|
||||||
|
o[a].onmouseleave = _hide;
|
||||||
|
}
|
||||||
|
hide();
|
||||||
|
};
|
||||||
|
|
||||||
|
return r;
|
||||||
|
})();
|
||||||
|
|||||||
22
docs/README.md
Normal file
22
docs/README.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# example `.epilogue.html`
|
||||||
|
save one of these as `.epilogue.html` inside a folder to customize it:
|
||||||
|
|
||||||
|
* [`minimal-up2k.html`](minimal-up2k.html) will [simplify the upload ui](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# example browser-css
|
||||||
|
point `--css-browser` to one of these by URL:
|
||||||
|
|
||||||
|
* [`browser.css`](browser.css) changes the background
|
||||||
|
* [`browser-icons.css`](browser-icons.css) adds filetype icons
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# other stuff
|
||||||
|
|
||||||
|
## [`rclone.md`](rclone.md)
|
||||||
|
* notes on using rclone as a fuse client/server
|
||||||
|
|
||||||
|
## [`example.conf`](example.conf)
|
||||||
|
* example config file for `-c` which never really happened
|
||||||
95
docs/biquad.html
Normal file
95
docs/biquad.html
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<!DOCTYPE html><html><head></head><body><script>
|
||||||
|
|
||||||
|
setTimeout(location.reload.bind(location), 700);
|
||||||
|
document.documentElement.scrollLeft = 0;
|
||||||
|
|
||||||
|
var can = document.createElement('canvas'),
|
||||||
|
cc = can.getContext('2d'),
|
||||||
|
w = 2048,
|
||||||
|
h = 1024;
|
||||||
|
|
||||||
|
w = 2048;
|
||||||
|
|
||||||
|
can.width = w;
|
||||||
|
can.height = h;
|
||||||
|
document.body.appendChild(can);
|
||||||
|
can.style.cssText = 'width:' + w + 'px;height:' + h + 'px';
|
||||||
|
|
||||||
|
cc.fillStyle = '#000';
|
||||||
|
cc.fillRect(0, 0, w, h);
|
||||||
|
|
||||||
|
var cfg = [ // hz, q, g
|
||||||
|
[31.25 * 0.88, 0, 1.4], // shelf
|
||||||
|
[31.25 * 1.04, 0.7, 0.96], // peak
|
||||||
|
[62.5, 0.7, 1],
|
||||||
|
[125, 0.8, 1],
|
||||||
|
[250, 0.9, 1.03],
|
||||||
|
[500, 0.9, 1.1],
|
||||||
|
[1000, 0.9, 1.1],
|
||||||
|
[2000, 0.9, 1.105],
|
||||||
|
[4000, 0.88, 1.05],
|
||||||
|
[8000 * 1.006, 0.73, 1.24],
|
||||||
|
//[16000 * 1.00, 0.5, 1.75], // peak.v1
|
||||||
|
//[16000 * 1.19, 0, 1.8] // shelf.v1
|
||||||
|
[16000 * 0.89, 0.7, 1.26], // peak
|
||||||
|
[16000 * 1.13, 0.82, 1.09], // peak
|
||||||
|
[16000 * 1.205, 0, 1.9] // shelf
|
||||||
|
];
|
||||||
|
|
||||||
|
var freqs = new Float32Array(22000),
|
||||||
|
sum = new Float32Array(freqs.length),
|
||||||
|
ac = new AudioContext(),
|
||||||
|
step = w / freqs.length,
|
||||||
|
colors = [
|
||||||
|
'rgba(255, 0, 0, 0.7)',
|
||||||
|
'rgba(0, 224, 0, 0.7)',
|
||||||
|
'rgba(0, 64, 255, 0.7)'
|
||||||
|
];
|
||||||
|
|
||||||
|
var order = [];
|
||||||
|
|
||||||
|
for (var a = 0; a < cfg.length; a += 2)
|
||||||
|
order.push(a);
|
||||||
|
|
||||||
|
for (var a = 1; a < cfg.length; a += 2)
|
||||||
|
order.push(a);
|
||||||
|
|
||||||
|
for (var ia = 0; ia < order.length; ia++) {
|
||||||
|
var a = order[ia],
|
||||||
|
fi = ac.createBiquadFilter(),
|
||||||
|
mag = new Float32Array(freqs.length),
|
||||||
|
phase = new Float32Array(freqs.length);
|
||||||
|
|
||||||
|
for (var b = 0; b < freqs.length; b++)
|
||||||
|
freqs[b] = b;
|
||||||
|
|
||||||
|
fi.type = a == 0 ? 'lowshelf' : a == cfg.length - 1 ? 'highshelf' : 'peaking';
|
||||||
|
fi.frequency.value = cfg[a][0];
|
||||||
|
fi.Q.value = cfg[a][1];
|
||||||
|
fi.gain.value = 1;
|
||||||
|
|
||||||
|
fi.getFrequencyResponse(freqs, mag, phase);
|
||||||
|
cc.fillStyle = colors[a % colors.length];
|
||||||
|
for (var b = 0; b < sum.length; b++) {
|
||||||
|
mag[b] -= 1;
|
||||||
|
sum[b] += mag[b] * cfg[a][2];
|
||||||
|
var y = h - (mag[b] * h * 3);
|
||||||
|
cc.fillRect(b * step, y, step, h - y);
|
||||||
|
cc.fillRect(b * step - 1, y - 1, 3, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var min = 999999, max = 0;
|
||||||
|
for (var a = 0; a < sum.length; a++) {
|
||||||
|
min = Math.min(min, sum[a]);
|
||||||
|
max = Math.max(max, sum[a]);
|
||||||
|
}
|
||||||
|
cc.fillStyle = 'rgba(255,255,255,1)';
|
||||||
|
for (var a = 0; a < sum.length; a++) {
|
||||||
|
var v = (sum[a] - min) / (max - min);
|
||||||
|
cc.fillRect(a * step, 0, step, v * h / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
cc.fillRect(0, 460, w, 1);
|
||||||
|
|
||||||
|
</script></body></html>
|
||||||
66
docs/browser-icons.css
Normal file
66
docs/browser-icons.css
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/* put filetype icons inline with text
|
||||||
|
#ggrid>a>span:before,
|
||||||
|
#ggrid>a>span.dir:before {
|
||||||
|
display: inline;
|
||||||
|
line-height: 0;
|
||||||
|
font-size: 1.7em;
|
||||||
|
margin: -.7em .1em -.5em -.6em;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/* move folder icons top-left */
|
||||||
|
#ggrid>a>span.dir:before {
|
||||||
|
content: initial;
|
||||||
|
}
|
||||||
|
#ggrid>a[href$="/"]:before {
|
||||||
|
content: '📂';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* put filetype icons top-left */
|
||||||
|
#ggrid>a:before {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
padding: .3em 0;
|
||||||
|
margin: -.4em;
|
||||||
|
text-shadow: 0 0 .1em #000;
|
||||||
|
background: linear-gradient(135deg,rgba(255,255,255,0) 50%,rgba(255,255,255,0.2));
|
||||||
|
border-radius: .3em;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* video */
|
||||||
|
#ggrid>a:is(
|
||||||
|
[href$=".mkv"i],
|
||||||
|
[href$=".mp4"i],
|
||||||
|
[href$=".webm"i],
|
||||||
|
):before {
|
||||||
|
content: '📺';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* audio */
|
||||||
|
#ggrid>a:is(
|
||||||
|
[href$=".mp3"i],
|
||||||
|
[href$=".ogg"i],
|
||||||
|
[href$=".opus"i],
|
||||||
|
[href$=".flac"i],
|
||||||
|
[href$=".m4a"i],
|
||||||
|
[href$=".aac"i],
|
||||||
|
):before {
|
||||||
|
content: '🎵';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* image */
|
||||||
|
#ggrid>a:is(
|
||||||
|
[href$=".jpg"i],
|
||||||
|
[href$=".jpeg"i],
|
||||||
|
[href$=".png"i],
|
||||||
|
[href$=".gif"i],
|
||||||
|
[href$=".webp"i],
|
||||||
|
):before {
|
||||||
|
content: '🎨';
|
||||||
|
}
|
||||||
29
docs/browser.css
Normal file
29
docs/browser.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
html {
|
||||||
|
background: #333 url('/wp/wallhaven-mdjrqy.jpg') center / cover no-repeat fixed;
|
||||||
|
}
|
||||||
|
#files th {
|
||||||
|
background: rgba(32, 32, 32, 0.9) !important;
|
||||||
|
}
|
||||||
|
#ops,
|
||||||
|
#treeul,
|
||||||
|
#files td {
|
||||||
|
background: rgba(32, 32, 32, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
html.light {
|
||||||
|
background: #eee url('/wp/wallhaven-dpxl6l.png') center / cover no-repeat fixed;
|
||||||
|
}
|
||||||
|
html.light #files th {
|
||||||
|
background: rgba(255, 255, 255, 0.9) !important;
|
||||||
|
}
|
||||||
|
html.light #ops,
|
||||||
|
html.light #treeul,
|
||||||
|
html.light #files td {
|
||||||
|
background: rgba(248, 248, 248, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#files * {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
@@ -32,9 +32,13 @@ r
|
|||||||
|
|
||||||
# and a folder where anyone can upload
|
# and a folder where anyone can upload
|
||||||
# but nobody can see the contents
|
# but nobody can see the contents
|
||||||
|
# and set the e2d flag to enable the uploads database
|
||||||
|
# and set the nodupe flag to reject duplicate uploads
|
||||||
/home/ed/inc
|
/home/ed/inc
|
||||||
/dump
|
/dump
|
||||||
w
|
w
|
||||||
|
c e2d
|
||||||
|
c nodupe
|
||||||
|
|
||||||
# this entire config file can be replaced with these arguments:
|
# this entire config file can be replaced with these arguments:
|
||||||
# -u ed:123 -u k:k -v .::r:aed -v priv:priv:rk:aed -v /home/ed/Music:music:r -v /home/ed/inc:dump:w
|
# -u ed:123 -u k:k -v .::r:aed -v priv:priv:rk:aed -v /home/ed/Music:music:r -v /home/ed/inc:dump:w
|
||||||
|
|||||||
32
docs/minimal-up2k.html
Normal file
32
docs/minimal-up2k.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<!--
|
||||||
|
save this as .epilogue.html inside a write-only folder to declutter the UI, makes it look like
|
||||||
|
https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png
|
||||||
|
-->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
/* make the up2k ui REALLY minimal by hiding a bunch of stuff: */
|
||||||
|
|
||||||
|
#ops, #tree, #path, #wrap>h2:last-child, /* main tabs and navigators (tree/breadcrumbs) */
|
||||||
|
|
||||||
|
#u2cleanup, #u2conf tr:first-child>td[rowspan]:not(#u2btn_cw), /* most of the config options */
|
||||||
|
|
||||||
|
#u2cards /* and the upload progress tabs */
|
||||||
|
|
||||||
|
{display: none !important} /* do it! */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* add some margins because now it's weird */
|
||||||
|
.opview {margin-top: 2.5em}
|
||||||
|
#op_up2k {margin-top: 3em}
|
||||||
|
|
||||||
|
/* and embiggen the upload button */
|
||||||
|
#u2conf #u2btn, #u2btn {padding:1.5em 0}
|
||||||
|
|
||||||
|
/* adjust the button area a bit */
|
||||||
|
#u2conf.has_btn {width: 35em !important; margin: 5em auto}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<a href="#" onclick="this.parentNode.innerHTML='';">show advanced options</a>
|
||||||
@@ -80,6 +80,16 @@ command -v gdate && date() { gdate "$@"; }; while true; do t=$(date +%s.%N); (ti
|
|||||||
var t=[]; var b=document.location.href.split('#')[0].slice(0, -1); document.querySelectorAll('#u2tab .prog a').forEach((x) => {t.push(b+encodeURI(x.getAttribute("href")))}); console.log(t.join("\n"));
|
var t=[]; var b=document.location.href.split('#')[0].slice(0, -1); document.querySelectorAll('#u2tab .prog a').forEach((x) => {t.push(b+encodeURI(x.getAttribute("href")))}); console.log(t.join("\n"));
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## bash oneliners
|
||||||
|
|
||||||
|
# get the size and video-id of all youtube vids in folder, assuming filename ends with -id.ext, and create a copyparty search query
|
||||||
|
find -maxdepth 1 -printf '%s %p\n' | sort -n | awk '!/-([0-9a-zA-Z_-]{11})\.(mkv|mp4|webm)$/{next} {sub(/\.[^\.]+$/,"");n=length($0);v=substr($0,n-10);print $1, v}' | tee /dev/stderr | awk 'BEGIN {p="("} {printf("%s name like *-%s.* ",p,$2);p="or"} END {print ")\n"}' | cat >&2
|
||||||
|
|
||||||
|
# unique stacks in a stackdump
|
||||||
|
f=a; rm -rf stacks; mkdir stacks; grep -E '^#' $f | while IFS= read -r n; do awk -v n="$n" '!$0{o=0} o; $0==n{o=1}' <$f >stacks/f; h=$(sha1sum <stacks/f | cut -c-16); mv stacks/f stacks/$h-"$n"; done ; find stacks/ | sort | uniq -cw24
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## sqlite3 stuff
|
## sqlite3 stuff
|
||||||
|
|
||||||
@@ -93,6 +103,15 @@ cat warks | while IFS= read -r x; do sqlite3 up2k.db "delete from mt where w = '
|
|||||||
# dump all dbs
|
# dump all dbs
|
||||||
find -iname up2k.db | while IFS= read -r x; do sqlite3 "$x" 'select substr(w,1,12), rd, fn from up' | sed -r 's/\|/ \| /g' | while IFS= read -r y; do printf '%s | %s\n' "$x" "$y"; done; done
|
find -iname up2k.db | while IFS= read -r x; do sqlite3 "$x" 'select substr(w,1,12), rd, fn from up' | sed -r 's/\|/ \| /g' | while IFS= read -r y; do printf '%s | %s\n' "$x" "$y"; done; done
|
||||||
|
|
||||||
|
# unschedule mtp scan for all files somewhere under "enc/"
|
||||||
|
sqlite3 -readonly up2k.db 'select substr(up.w,1,16) from up inner join mt on mt.w = substr(up.w,1,16) where rd like "enc/%" and +mt.k = "t:mtp"' > keys; awk '{printf "delete from mt where w = \"%s\" and +k = \"t:mtp\";\n", $0}' <keys | tee /dev/stderr | sqlite3 up2k.db
|
||||||
|
|
||||||
|
# compare metadata key "key" between two databases
|
||||||
|
sqlite3 -readonly up2k.db.key-full 'select w, v from mt where k = "key" order by w' > k1; sqlite3 -readonly up2k.db 'select w, v from mt where k = "key" order by w' > k2; ok=0; ng=0; while IFS='|' read w k2; do k1="$(grep -E "^$w" k1 | sed -r 's/.*\|//')"; [ "$k1" = "$k2" ] && ok=$((ok+1)) || { ng=$((ng+1)); printf '%3s %3s %s\n' "$k1" "$k2" "$(sqlite3 -readonly up2k.db.key-full "select * from up where substr(w,1,16) = '$w'" | sed -r 's/\|/ | /g')"; }; done < <(cat k2); echo "match $ok diff $ng"
|
||||||
|
|
||||||
|
# actually this is much better
|
||||||
|
sqlite3 -readonly up2k.db.key-full 'select w, v from mt where k = "key" order by w' > k1; sqlite3 -readonly up2k.db 'select mt.w, mt.v, up.rd, up.fn from mt inner join up on mt.w = substr(up.w,1,16) where mt.k = "key" order by up.rd, up.fn' > k2; ok=0; ng=0; while IFS='|' read w k2 path; do k1="$(grep -E "^$w" k1 | sed -r 's/.*\|//')"; [ "$k1" = "$k2" ] && ok=$((ok+1)) || { ng=$((ng+1)); printf '%3s %3s %s\n' "$k1" "$k2" "$path"; }; done < <(cat k2); echo "match $ok diff $ng"
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## media
|
## media
|
||||||
@@ -146,6 +165,9 @@ dbg.asyncStore.pendingBreakpoints = {}
|
|||||||
# fix firefox phantom breakpoints
|
# fix firefox phantom breakpoints
|
||||||
about:config >> devtools.debugger.prefs-schema-version = -1
|
about:config >> devtools.debugger.prefs-schema-version = -1
|
||||||
|
|
||||||
|
# determine server version
|
||||||
|
git pull; git reset --hard origin/HEAD && git log --format=format:"%H %ai %d" --decorate=full > ../revs && cat ../{util,browser}.js >../vr && cat ../revs | while read -r rev extra; do (git reset --hard $rev >/dev/null 2>/dev/null && dsz=$(cat copyparty/web/{util,browser}.js >../vg 2>/dev/null && diff -wNarU0 ../{vg,vr} | wc -c) && printf '%s %6s %s\n' "$rev" $dsz "$extra") </dev/null; done
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## http 206
|
## http 206
|
||||||
@@ -187,3 +209,4 @@ mk() { rm -rf /tmp/foo; sudo -u ed bash -c 'mkdir /tmp/foo; echo hi > /tmp/foo/b
|
|||||||
mk && t0="$(date)" && while true; do date -s "$(date '+ 1 hour')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo "$t0"
|
mk && t0="$(date)" && while true; do date -s "$(date '+ 1 hour')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo "$t0"
|
||||||
mk && sudo -u ed flock /tmp/foo sleep 40 & sleep 1; ps aux | grep -E 'sleep 40$' && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo "$t0"
|
mk && sudo -u ed flock /tmp/foo sleep 40 & sleep 1; ps aux | grep -E 'sleep 40$' && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo "$t0"
|
||||||
mk && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; tar -cf/dev/null /tmp/foo; done; echo "$t0"
|
mk && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; tar -cf/dev/null /tmp/foo; done; echo "$t0"
|
||||||
|
|
||||||
|
|||||||
82
docs/nuitka.txt
Normal file
82
docs/nuitka.txt
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# recipe for building an exe with nuitka (extreme jank edition)
|
||||||
|
#
|
||||||
|
# NOTE: win7 and win10 builds both work on win10 but
|
||||||
|
# on win7 they immediately c0000005 in kernelbase.dll
|
||||||
|
#
|
||||||
|
# first install python-3.6.8-amd64.exe
|
||||||
|
# [x] add to path
|
||||||
|
#
|
||||||
|
# copypaste the rest of this file into cmd
|
||||||
|
|
||||||
|
rem from pypi
|
||||||
|
cd \users\ed\downloads
|
||||||
|
python -m pip install --user Nuitka-0.6.14.7.tar.gz
|
||||||
|
|
||||||
|
rem https://github.com/brechtsanders/winlibs_mingw/releases/download/10.2.0-11.0.0-8.0.0-r5/winlibs-x86_64-posix-seh-gcc-10.2.0-llvm-11.0.0-mingw-w64-8.0.0-r5.zip
|
||||||
|
mkdir C:\Users\ed\AppData\Local\Nuitka\
|
||||||
|
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\
|
||||||
|
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\gcc\
|
||||||
|
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\gcc\x86_64\
|
||||||
|
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\gcc\x86_64\10.2.0-11.0.0-8.0.0-r5\
|
||||||
|
copy c:\users\ed\downloads\winlibs-x86_64-posix-seh-gcc-10.2.0-llvm-11.0.0-mingw-w64-8.0.0-r5.zip C:\Users\ed\AppData\Local\Nuitka\Nuitka\gcc\x86_64\10.2.0-11.0.0-8.0.0-r5\winlibs-x86_64-posix-seh-gcc-10.2.0-llvm-11.0.0-mingw-w64-8.0.0-r5.zip
|
||||||
|
|
||||||
|
rem https://github.com/ccache/ccache/releases/download/v3.7.12/ccache-3.7.12-windows-32.zip
|
||||||
|
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\ccache\
|
||||||
|
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\ccache\v3.7.12\
|
||||||
|
copy c:\users\ed\downloads\ccache-3.7.12-windows-32.zip C:\Users\ed\AppData\Local\Nuitka\Nuitka\ccache\v3.7.12\ccache-3.7.12-windows-32.zip
|
||||||
|
|
||||||
|
rem https://dependencywalker.com/depends22_x64.zip
|
||||||
|
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\depends\
|
||||||
|
mkdir C:\Users\ed\AppData\Local\Nuitka\Nuitka\depends\x86_64\
|
||||||
|
copy c:\users\ed\downloads\depends22_x64.zip C:\Users\ed\AppData\Local\Nuitka\Nuitka\depends\x86_64\depends22_x64.zip
|
||||||
|
|
||||||
|
cd \
|
||||||
|
rd /s /q %appdata%\..\local\temp\pe-copyparty
|
||||||
|
cd \users\ed\downloads
|
||||||
|
python copyparty-sfx.py -h
|
||||||
|
cd %appdata%\..\local\temp\pe-copyparty\copyparty
|
||||||
|
|
||||||
|
python
|
||||||
|
import os, re
|
||||||
|
os.rename('../dep-j2/jinja2', '../jinja2')
|
||||||
|
os.rename('../dep-j2/markupsafe', '../markupsafe')
|
||||||
|
|
||||||
|
print("# nuitka dies if .__init__.stuff is imported")
|
||||||
|
with open('__init__.py','r',encoding='utf-8') as f:
|
||||||
|
t1 = f.read()
|
||||||
|
|
||||||
|
with open('util.py','r',encoding='utf-8') as f:
|
||||||
|
t2 = f.read().split('\n')[3:]
|
||||||
|
|
||||||
|
t2 = [x for x in t2 if 'from .__init__' not in x]
|
||||||
|
t = t1 + '\n'.join(t2)
|
||||||
|
with open('__init__.py','w',encoding='utf-8') as f:
|
||||||
|
f.write('\n')
|
||||||
|
|
||||||
|
with open('util.py','w',encoding='utf-8') as f:
|
||||||
|
f.write(t)
|
||||||
|
|
||||||
|
print("# local-imports fail, prefix module names")
|
||||||
|
ptn = re.compile(r'^( *from )(\.[^ ]+ import .*)')
|
||||||
|
for d, _, fs in os.walk('.'):
|
||||||
|
for f in fs:
|
||||||
|
fp = os.path.join(d, f)
|
||||||
|
if not fp.endswith('.py'):
|
||||||
|
continue
|
||||||
|
t = ''
|
||||||
|
with open(fp,'r',encoding='utf-8') as f:
|
||||||
|
for ln in [x.rstrip('\r\n') for x in f]:
|
||||||
|
m = ptn.match(ln)
|
||||||
|
if not m:
|
||||||
|
t += ln + '\n'
|
||||||
|
continue
|
||||||
|
p1, p2 = m.groups()
|
||||||
|
t += "{}copyparty{}\n".format(p1, p2).replace("__init__", "util")
|
||||||
|
with open(fp,'w',encoding='utf-8') as f:
|
||||||
|
f.write(t)
|
||||||
|
|
||||||
|
exit()
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
rd /s /q bout & python -m nuitka --standalone --onefile --windows-onefile-tempdir --python-flag=no_site --assume-yes-for-downloads --include-data-dir=copyparty\web=copyparty\web --include-data-dir=copyparty\res=copyparty\res --run --output-dir=bout --mingw64 --include-package=markupsafe --include-package=jinja2 copyparty
|
||||||
32
docs/tcp-debug.sh
Normal file
32
docs/tcp-debug.sh
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
(cd ~/dev/copyparty && strace -Tttyyvfs 256 -o strace.strace python3 -um copyparty -i 127.0.0.1 --http-only --stackmon /dev/shm/cpps,10 ) 2>&1 | tee /dev/stderr > ~/log-copyparty-$(date +%Y-%m%d-%H%M%S).txt
|
||||||
|
|
||||||
|
14/Jun/2021:16:34:02 1623688447.212405 death
|
||||||
|
14/Jun/2021:16:35:02 1623688502.420860 back
|
||||||
|
|
||||||
|
tcpdump -nni lo -w /home/ed/lo.pcap
|
||||||
|
|
||||||
|
# 16:35:25.324662 IP 127.0.0.1.48632 > 127.0.0.1.3920: Flags [F.], seq 849, ack 544, win 359, options [nop,nop,TS val 809396796 ecr 809396796], length 0
|
||||||
|
|
||||||
|
tcpdump -nnr /home/ed/lo.pcap | awk '/ > 127.0.0.1.3920: /{sub(/ > .*/,"");sub(/.*\./,"");print}' | sort -n | uniq | while IFS= read -r port; do echo; tcpdump -nnr /home/ed/lo.pcap 2>/dev/null | grep -E "\.$port( > |: F)" | sed -r 's/ > .*, /, /'; done | grep -E '^16:35:0.*length [^0]' -C50
|
||||||
|
|
||||||
|
16:34:02.441732 IP 127.0.0.1.48638, length 0
|
||||||
|
16:34:02.441738 IP 127.0.0.1.3920, length 0
|
||||||
|
16:34:02.441744 IP 127.0.0.1.48638, length 0
|
||||||
|
16:34:02.441756 IP 127.0.0.1.48638, length 791
|
||||||
|
16:34:02.441759 IP 127.0.0.1.3920, length 0
|
||||||
|
16:35:02.445529 IP 127.0.0.1.48638, length 0
|
||||||
|
16:35:02.489194 IP 127.0.0.1.3920, length 0
|
||||||
|
16:35:02.515595 IP 127.0.0.1.3920, length 216
|
||||||
|
16:35:02.515600 IP 127.0.0.1.48638, length 0
|
||||||
|
|
||||||
|
grep 48638 "$(find ~ -maxdepth 1 -name log-copyparty-\*.txt | sort | tail -n 1)"
|
||||||
|
|
||||||
|
1623688502.510380 48638 rh
|
||||||
|
1623688502.511291 48638 Unrecv direct ...
|
||||||
|
1623688502.511827 48638 rh = 791
|
||||||
|
16:35:02.518 127.0.0.1 48638 shut(8): [Errno 107] Socket not connected
|
||||||
|
Exception in thread httpsrv-0.1-48638:
|
||||||
|
|
||||||
|
grep 48638 ~/dev/copyparty/strace.strace
|
||||||
|
14561 16:35:02.506310 <... accept4 resumed> {sa_family=AF_INET, sin_port=htons(48638), sin_addr=inet_addr("127.0.0.1")}, [16], SOCK_CLOEXEC) = 8<TCP:[127.0.0.1:3920->127.0.0.1:48638]> <0.000012>
|
||||||
|
15230 16:35:02.510725 write(1<pipe:[256639555]>, "1623688502.510380 48638 rh\n", 27 <unfinished ...>
|
||||||
@@ -92,20 +92,34 @@ chmod 755 \
|
|||||||
copyparty-extras/copyparty-*/{scripts,bin}/*
|
copyparty-extras/copyparty-*/{scripts,bin}/*
|
||||||
|
|
||||||
|
|
||||||
# extract and repack the sfx with less features enabled
|
# extract the sfx
|
||||||
( cd copyparty-extras/sfx-full/
|
( cd copyparty-extras/sfx-full/
|
||||||
./copyparty-sfx.py -h
|
./copyparty-sfx.py -h
|
||||||
cd ../copyparty-*/
|
|
||||||
./scripts/make-sfx.sh re no-ogv no-cm
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# put new sfx into copyparty-extras/sfx-lite/,
|
repack() {
|
||||||
# fuse client into copyparty-extras/,
|
|
||||||
|
# do the repack
|
||||||
|
(cd copyparty-extras/copyparty-*/
|
||||||
|
./scripts/make-sfx.sh $2
|
||||||
|
)
|
||||||
|
|
||||||
|
# put new sfx into copyparty-extras/$name/,
|
||||||
|
( cd copyparty-extras/
|
||||||
|
mv copyparty-*/dist/* $1/
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
repack sfx-full "re gz no-sh"
|
||||||
|
repack sfx-lite "re no-ogv no-cm"
|
||||||
|
repack sfx-lite "re no-ogv no-cm gz no-sh"
|
||||||
|
|
||||||
|
|
||||||
|
# move fuse client into copyparty-extras/,
|
||||||
# copy lite-sfx.py to ./copyparty,
|
# copy lite-sfx.py to ./copyparty,
|
||||||
# delete extracted source code
|
# delete extracted source code
|
||||||
( cd copyparty-extras/
|
( cd copyparty-extras/
|
||||||
mv copyparty-*/dist/* sfx-lite/
|
|
||||||
mv copyparty-*/bin/copyparty-fuse.py .
|
mv copyparty-*/bin/copyparty-fuse.py .
|
||||||
cp -pv sfx-lite/copyparty-sfx.py ../copyparty
|
cp -pv sfx-lite/copyparty-sfx.py ../copyparty
|
||||||
rm -rf copyparty-{0..9}*.*.*{0..9}
|
rm -rf copyparty-{0..9}*.*.*{0..9}
|
||||||
@@ -119,6 +133,7 @@ true
|
|||||||
|
|
||||||
|
|
||||||
# create the bundle
|
# create the bundle
|
||||||
|
printf '\n\n'
|
||||||
fn=copyparty-$(date +%Y-%m%d-%H%M%S).tgz
|
fn=copyparty-$(date +%Y-%m%d-%H%M%S).tgz
|
||||||
tar -czvf "$od/$fn" *
|
tar -czvf "$od/$fn" *
|
||||||
cd "$od"
|
cd "$od"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
FROM alpine:3.13
|
FROM alpine:3.13
|
||||||
WORKDIR /z
|
WORKDIR /z
|
||||||
ENV ver_asmcrypto=5b994303a9d3e27e0915f72a10b6c2c51535a4dc \
|
ENV ver_asmcrypto=5b994303a9d3e27e0915f72a10b6c2c51535a4dc \
|
||||||
|
ver_hashwasm=4.7.0 \
|
||||||
ver_marked=1.1.0 \
|
ver_marked=1.1.0 \
|
||||||
ver_ogvjs=1.8.0 \
|
ver_ogvjs=1.8.0 \
|
||||||
ver_mde=2.14.0 \
|
ver_mde=2.14.0 \
|
||||||
@@ -21,7 +22,11 @@ RUN mkdir -p /z/dist/no-pk \
|
|||||||
&& wget https://github.com/codemirror/CodeMirror/archive/$ver_codemirror.tar.gz -O codemirror.tgz \
|
&& wget https://github.com/codemirror/CodeMirror/archive/$ver_codemirror.tar.gz -O codemirror.tgz \
|
||||||
&& wget https://github.com/FortAwesome/Font-Awesome/releases/download/$ver_fontawesome/fontawesome-free-$ver_fontawesome-web.zip -O fontawesome.zip \
|
&& wget https://github.com/FortAwesome/Font-Awesome/releases/download/$ver_fontawesome/fontawesome-free-$ver_fontawesome-web.zip -O fontawesome.zip \
|
||||||
&& wget https://github.com/google/zopfli/archive/zopfli-$ver_zopfli.tar.gz -O zopfli.tgz \
|
&& wget https://github.com/google/zopfli/archive/zopfli-$ver_zopfli.tar.gz -O zopfli.tgz \
|
||||||
|
&& wget https://github.com/Daninet/hash-wasm/releases/download/v$ver_hashwasm/hash-wasm@$ver_hashwasm.zip -O hash-wasm.zip \
|
||||||
&& unzip ogvjs.zip \
|
&& unzip ogvjs.zip \
|
||||||
|
&& (mkdir hash-wasm \
|
||||||
|
&& cd hash-wasm \
|
||||||
|
&& unzip ../hash-wasm.zip) \
|
||||||
&& (tar -xf asmcrypto.tgz \
|
&& (tar -xf asmcrypto.tgz \
|
||||||
&& cd asmcrypto.js-$ver_asmcrypto \
|
&& cd asmcrypto.js-$ver_asmcrypto \
|
||||||
&& npm install ) \
|
&& npm install ) \
|
||||||
@@ -58,7 +63,12 @@ RUN tar -xf zopfli.tgz \
|
|||||||
RUN cd asmcrypto.js-$ver_asmcrypto \
|
RUN cd asmcrypto.js-$ver_asmcrypto \
|
||||||
&& echo "export { Sha512 } from './hash/sha512/sha512';" > src/entry-export_all.ts \
|
&& echo "export { Sha512 } from './hash/sha512/sha512';" > src/entry-export_all.ts \
|
||||||
&& node -r esm build.js \
|
&& node -r esm build.js \
|
||||||
&& mv asmcrypto.all.es5.js /z/dist/sha512.js
|
&& awk '/HMAC state/{o=1} /var HEAP/{o=0} /function hmac_reset/{o=1} /return \{/{o=0} /var __extends =/{o=1} /var Hash =/{o=0} /hmac_|pbkdf2_/{next} o{next} {gsub(/IllegalStateError/,"Exception")} {sub(/^ +/,"");sub(/^\/\/ .*/,"");sub(/;$/," ;")} 1' < asmcrypto.all.es5.js > /z/dist/sha512.ac.js
|
||||||
|
|
||||||
|
|
||||||
|
# build hash-wasm
|
||||||
|
RUN cd hash-wasm \
|
||||||
|
&& mv sha512.umd.min.js /z/dist/sha512.hw.js
|
||||||
|
|
||||||
|
|
||||||
# build ogvjs
|
# build ogvjs
|
||||||
|
|||||||
12
scripts/install-githooks.sh
Executable file
12
scripts/install-githooks.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
[ -e setup.py ] || ..
|
||||||
|
[ -e setup.py ] || {
|
||||||
|
echo u wot
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cd .git/hooks
|
||||||
|
rm -f pre-commit
|
||||||
|
ln -s ../../scripts/run-tests.sh pre-commit
|
||||||
@@ -11,6 +11,10 @@ echo
|
|||||||
# `re` does a repack of an sfx which you already executed once
|
# `re` does a repack of an sfx which you already executed once
|
||||||
# (grabs files from the sfx-created tempdir), overrides `clean`
|
# (grabs files from the sfx-created tempdir), overrides `clean`
|
||||||
#
|
#
|
||||||
|
# `gz` creates a gzip-compressed python sfx instead of bzip2
|
||||||
|
#
|
||||||
|
# `no-sh` makes just the python sfx, skips the sh/unix sfx
|
||||||
|
#
|
||||||
# `no-ogv` saves ~500k by removing the opus/vorbis audio codecs
|
# `no-ogv` saves ~500k by removing the opus/vorbis audio codecs
|
||||||
# (only affects apple devices; everything else has native support)
|
# (only affects apple devices; everything else has native support)
|
||||||
#
|
#
|
||||||
@@ -32,6 +36,10 @@ gtar=$(command -v gtar || command -v gnutar) || true
|
|||||||
[ -e /opt/local/bin/bzip2 ] &&
|
[ -e /opt/local/bin/bzip2 ] &&
|
||||||
bzip2() { /opt/local/bin/bzip2 "$@"; }
|
bzip2() { /opt/local/bin/bzip2 "$@"; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gawk=$(command -v gawk || command -v gnuawk || command -v awk)
|
||||||
|
awk() { $gawk "$@"; }
|
||||||
|
|
||||||
pybin=$(command -v python3 || command -v python) || {
|
pybin=$(command -v python3 || command -v python) || {
|
||||||
echo need python
|
echo need python
|
||||||
exit 1
|
exit 1
|
||||||
@@ -163,7 +171,7 @@ find .. -type f \( -name .DS_Store -or -name ._.DS_Store \) -delete
|
|||||||
find .. -type f -name ._\* | while IFS= read -r f; do cmp <(printf '\x00\x05\x16') <(head -c 3 -- "$f") && rm -f -- "$f"; done
|
find .. -type f -name ._\* | while IFS= read -r f; do cmp <(printf '\x00\x05\x16') <(head -c 3 -- "$f") && rm -f -- "$f"; done
|
||||||
|
|
||||||
echo use smol web deps
|
echo use smol web deps
|
||||||
rm -f copyparty/web/deps/*.full.* copyparty/web/{Makefile,splash.js}
|
rm -f copyparty/web/deps/*.full.* copyparty/web/dbg-* copyparty/web/Makefile
|
||||||
|
|
||||||
# it's fine dw
|
# it's fine dw
|
||||||
grep -lE '\.full\.(js|css)' copyparty/web/* |
|
grep -lE '\.full\.(js|css)' copyparty/web/* |
|
||||||
@@ -194,17 +202,46 @@ tmv "$f"
|
|||||||
|
|
||||||
# up2k goes from 28k to 22k laff
|
# up2k goes from 28k to 22k laff
|
||||||
echo entabbening
|
echo entabbening
|
||||||
find | grep -E '\.(js|css|html)$' | while IFS= read -r f; do
|
find | grep -E '\.css$' | while IFS= read -r f; do
|
||||||
|
awk '{
|
||||||
|
sub(/^[ \t]+/,"");
|
||||||
|
sub(/[ \t]+$/,"");
|
||||||
|
$0=gensub(/^([a-z-]+) *: *(.*[^ ]) *;$/,"\\1:\\2;","1");
|
||||||
|
sub(/ +\{$/,"{");
|
||||||
|
gsub(/, /,",")
|
||||||
|
}
|
||||||
|
!/\}$/ {printf "%s",$0;next}
|
||||||
|
1
|
||||||
|
' <$f | sed 's/;\}$/}/' >t
|
||||||
|
tmv "$f"
|
||||||
|
done
|
||||||
|
find | grep -E '\.(js|html)$' | while IFS= read -r f; do
|
||||||
unexpand -t 4 --first-only <"$f" >t
|
unexpand -t 4 --first-only <"$f" >t
|
||||||
tmv "$f"
|
tmv "$f"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
||||||
|
gzres() {
|
||||||
|
command -v pigz &&
|
||||||
|
pk='pigz -11 -J 34 -I 100' ||
|
||||||
|
pk='gzip'
|
||||||
|
|
||||||
|
echo "$pk"
|
||||||
|
find | grep -E '\.(js|css)$' | grep -vF /deps/ | while IFS= read -r f; do
|
||||||
|
echo -n .
|
||||||
|
$pk "$f"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
gzres
|
||||||
|
|
||||||
|
|
||||||
echo gen tarlist
|
echo gen tarlist
|
||||||
for d in copyparty dep-j2; do find $d -type f; done |
|
for d in copyparty dep-j2; do find $d -type f; done |
|
||||||
sed -r 's/(.*)\.(.*)/\2 \1/' | LC_ALL=C sort |
|
sed -r 's/(.*)\.(.*)/\2 \1/' | LC_ALL=C sort |
|
||||||
sed -r 's/([^ ]*) (.*)/\2.\1/' | grep -vE '/list1?$' > list1
|
sed -r 's/([^ ]*) (.*)/\2.\1/' | grep -vE '/list1?$' > list1
|
||||||
|
|
||||||
(grep -vE 'gz$' list1; grep -E 'gz$' list1) >list
|
(grep -vE '\.(gz|br)$' list1; grep -E '\.(gz|br)$' list1) >list || true
|
||||||
|
|
||||||
echo creating tar
|
echo creating tar
|
||||||
args=(--owner=1000 --group=1000)
|
args=(--owner=1000 --group=1000)
|
||||||
|
|||||||
34
scripts/profile.py
Normal file
34
scripts/profile.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, ".")
|
||||||
|
cmd = sys.argv[1]
|
||||||
|
|
||||||
|
if cmd == "cpp":
|
||||||
|
from copyparty.__main__ import main
|
||||||
|
|
||||||
|
argv = ["__main__", "-v", "srv::r", "-v", "../../yt:yt:r"]
|
||||||
|
main(argv=argv)
|
||||||
|
|
||||||
|
elif cmd == "test":
|
||||||
|
from unittest import main
|
||||||
|
|
||||||
|
argv = ["__main__", "discover", "-s", "tests"]
|
||||||
|
main(module=None, argv=argv)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
# import dis; print(dis.dis(main))
|
||||||
|
|
||||||
|
|
||||||
|
# macos:
|
||||||
|
# option1) python3.9 -m pip install --user -U vmprof==0.4.9
|
||||||
|
# option2) python3.9 -m pip install --user -U https://github.com/vmprof/vmprof-python/archive/refs/heads/master.zip
|
||||||
|
#
|
||||||
|
# python -m vmprof -o prof --lines ./scripts/profile.py test
|
||||||
|
|
||||||
|
# linux: ~/.local/bin/vmprofshow prof tree | grep -vF '[1m 0.'
|
||||||
|
# macos: ~/Library/Python/3.9/bin/vmprofshow prof tree | grep -vF '[1m 0.'
|
||||||
|
# win: %appdata%\..\Roaming\Python\Python39\Scripts\vmprofshow.exe prof tree
|
||||||
15
scripts/run-tests.sh
Executable file
15
scripts/run-tests.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
pids=()
|
||||||
|
for py in python{2,3}; do
|
||||||
|
nice $py -m unittest discover -s tests >/dev/null &
|
||||||
|
pids+=($!)
|
||||||
|
done
|
||||||
|
|
||||||
|
python3 scripts/test/smoketest.py &
|
||||||
|
pids+=($!)
|
||||||
|
|
||||||
|
for pid in ${pids[@]}; do
|
||||||
|
wait $pid
|
||||||
|
done
|
||||||
@@ -6,10 +6,10 @@ import re, os, sys, time, shutil, signal, threading, tarfile, hashlib, platform,
|
|||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
|
|
||||||
"""
|
"""
|
||||||
run me with any version of python, i will unpack and run copyparty
|
pls don't edit this file with a text editor,
|
||||||
|
it breaks the compressed stuff at the end
|
||||||
|
|
||||||
(but please don't edit this file with a text editor
|
run me with any version of python, i will unpack and run copyparty
|
||||||
since that would probably corrupt the binary stuff at the end)
|
|
||||||
|
|
||||||
there's zero binaries! just plaintext python scripts all the way down
|
there's zero binaries! just plaintext python scripts all the way down
|
||||||
so you can easily unpack the archive and inspect it for shady stuff
|
so you can easily unpack the archive and inspect it for shady stuff
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ grep -E '/(python|pypy)[0-9\.-]*$' >$dir/pys || true
|
|||||||
printf '\033[1;30mlooking for jinja2 in [%s]\033[0m\n' "$_py" >&2
|
printf '\033[1;30mlooking for jinja2 in [%s]\033[0m\n' "$_py" >&2
|
||||||
$_py -c 'import jinja2' 2>/dev/null || continue
|
$_py -c 'import jinja2' 2>/dev/null || continue
|
||||||
printf '%s\n' "$_py"
|
printf '%s\n' "$_py"
|
||||||
mv $dir/{,x.}jinja2
|
mv $dir/{,x.}dep-j2
|
||||||
break
|
break
|
||||||
done)"
|
done)"
|
||||||
|
|
||||||
|
|||||||
@@ -17,14 +17,15 @@ __license__ = "MIT"
|
|||||||
__url__ = "https://github.com/9001/copyparty/"
|
__url__ = "https://github.com/9001/copyparty/"
|
||||||
|
|
||||||
|
|
||||||
def get_spd(nbyte, nsec):
|
def get_spd(nbyte, nfiles, nsec):
|
||||||
if not nsec:
|
if not nsec:
|
||||||
return "0.000 MB 0.000 sec 0.000 MB/s"
|
return "0.000 MB 0 files 0.000 sec 0.000 MB/s 0.000 f/s"
|
||||||
|
|
||||||
mb = nbyte / (1024 * 1024.0)
|
mb = nbyte / (1024 * 1024.0)
|
||||||
spd = mb / nsec
|
spd = mb / nsec
|
||||||
|
nspd = nfiles / nsec
|
||||||
|
|
||||||
return f"{mb:.3f} MB {nsec:.3f} sec {spd:.3f} MB/s"
|
return f"{mb:.3f} MB {nfiles} files {nsec:.3f} sec {spd:.3f} MB/s {nspd:.3f} f/s"
|
||||||
|
|
||||||
|
|
||||||
class Inf(object):
|
class Inf(object):
|
||||||
@@ -36,6 +37,7 @@ class Inf(object):
|
|||||||
self.mtx_reports = threading.Lock()
|
self.mtx_reports = threading.Lock()
|
||||||
|
|
||||||
self.n_byte = 0
|
self.n_byte = 0
|
||||||
|
self.n_file = 0
|
||||||
self.n_sec = 0
|
self.n_sec = 0
|
||||||
self.n_done = 0
|
self.n_done = 0
|
||||||
self.t0 = t0
|
self.t0 = t0
|
||||||
@@ -63,7 +65,8 @@ class Inf(object):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
msgs = msgs[-64:]
|
msgs = msgs[-64:]
|
||||||
msgs = [f"{get_spd(self.n_byte, self.n_sec)} {x}" for x in msgs]
|
spd = get_spd(self.n_byte, len(self.reports), self.n_sec)
|
||||||
|
msgs = [f"{spd} {x}" for x in msgs]
|
||||||
print("\n".join(msgs))
|
print("\n".join(msgs))
|
||||||
|
|
||||||
def report(self, fn, n_byte, n_sec):
|
def report(self, fn, n_byte, n_sec):
|
||||||
@@ -131,8 +134,9 @@ def main():
|
|||||||
|
|
||||||
num_threads = 8
|
num_threads = 8
|
||||||
read_sz = 32 * 1024
|
read_sz = 32 * 1024
|
||||||
|
targs = (q, inf, read_sz)
|
||||||
for _ in range(num_threads):
|
for _ in range(num_threads):
|
||||||
thr = threading.Thread(target=worker, args=(q, inf, read_sz,))
|
thr = threading.Thread(target=worker, args=targs)
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
@@ -151,14 +155,14 @@ def main():
|
|||||||
log = inf.reports
|
log = inf.reports
|
||||||
log.sort()
|
log.sort()
|
||||||
for nbyte, nsec, fn in log[-64:]:
|
for nbyte, nsec, fn in log[-64:]:
|
||||||
print(f"{get_spd(nbyte, nsec)} {fn}")
|
spd = get_spd(nbyte, len(log), nsec)
|
||||||
|
print(f"{spd} {fn}")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("\n".join(inf.errors))
|
print("\n".join(inf.errors))
|
||||||
|
|
||||||
print(get_spd(inf.n_byte, t2 - t0))
|
print(get_spd(inf.n_byte, len(log), t2 - t0))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|||||||
105
scripts/test/race.py
Normal file
105
scripts/test/race.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
import http.client
|
||||||
|
|
||||||
|
|
||||||
|
class Conn(object):
|
||||||
|
def __init__(self, ip, port):
|
||||||
|
self.s = http.client.HTTPConnection(ip, port, timeout=260)
|
||||||
|
self.st = []
|
||||||
|
|
||||||
|
def get(self, vpath):
|
||||||
|
self.st = [time.time()]
|
||||||
|
|
||||||
|
self.s.request("GET", vpath)
|
||||||
|
self.st.append(time.time())
|
||||||
|
|
||||||
|
ret = self.s.getresponse()
|
||||||
|
self.st.append(time.time())
|
||||||
|
|
||||||
|
if ret.status < 200 or ret.status >= 400:
|
||||||
|
raise Exception(ret.status)
|
||||||
|
|
||||||
|
ret = ret.read()
|
||||||
|
self.st.append(time.time())
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def get_json(self, vpath):
|
||||||
|
ret = self.get(vpath)
|
||||||
|
return json.loads(ret)
|
||||||
|
|
||||||
|
|
||||||
|
class CState(threading.Thread):
|
||||||
|
def __init__(self, cs):
|
||||||
|
threading.Thread.__init__(self)
|
||||||
|
self.daemon = True
|
||||||
|
self.cs = cs
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
colors = [5, 1, 3, 2, 7]
|
||||||
|
remotes = []
|
||||||
|
remotes_ok = False
|
||||||
|
while True:
|
||||||
|
time.sleep(0.001)
|
||||||
|
if not remotes_ok:
|
||||||
|
remotes = []
|
||||||
|
remotes_ok = True
|
||||||
|
for conn in self.cs:
|
||||||
|
try:
|
||||||
|
remotes.append(conn.s.sock.getsockname()[1])
|
||||||
|
except:
|
||||||
|
remotes.append("?")
|
||||||
|
remotes_ok = False
|
||||||
|
|
||||||
|
m = []
|
||||||
|
for conn, remote in zip(self.cs, remotes):
|
||||||
|
stage = len(conn.st)
|
||||||
|
m.append(f"\033[3{colors[stage]}m{remote}")
|
||||||
|
|
||||||
|
m = " ".join(m)
|
||||||
|
print(f"{m}\033[0m\n\033[A", end="")
|
||||||
|
|
||||||
|
|
||||||
|
def allget(cs, urls):
|
||||||
|
thrs = []
|
||||||
|
for c, url in zip(cs, urls):
|
||||||
|
t = threading.Thread(target=c.get, args=(url,))
|
||||||
|
t.start()
|
||||||
|
thrs.append(t)
|
||||||
|
|
||||||
|
for t in thrs:
|
||||||
|
t.join()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
os.system("")
|
||||||
|
|
||||||
|
ip, port = sys.argv[1].split(":")
|
||||||
|
port = int(port)
|
||||||
|
|
||||||
|
cs = []
|
||||||
|
for _ in range(64):
|
||||||
|
cs.append(Conn(ip, 3923))
|
||||||
|
|
||||||
|
CState(cs)
|
||||||
|
|
||||||
|
urlbase = "/doujin/c95"
|
||||||
|
j = cs[0].get_json(f"{urlbase}?ls")
|
||||||
|
urls = []
|
||||||
|
for d in j["dirs"]:
|
||||||
|
urls.append(f"{urlbase}/{d['href']}?th=w")
|
||||||
|
|
||||||
|
for n in range(100):
|
||||||
|
print(n)
|
||||||
|
allget(cs, urls)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
209
scripts/test/smoketest.py
Normal file
209
scripts/test/smoketest.py
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import shlex
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
import tempfile
|
||||||
|
import requests
|
||||||
|
import threading
|
||||||
|
import subprocess as sp
|
||||||
|
|
||||||
|
|
||||||
|
CPP = []
|
||||||
|
|
||||||
|
|
||||||
|
class Cpp(object):
|
||||||
|
def __init__(self, args):
|
||||||
|
args = [sys.executable, "-m", "copyparty"] + args
|
||||||
|
print(" ".join([shlex.quote(x) for x in args]))
|
||||||
|
|
||||||
|
self.ls_pre = set(list(os.listdir()))
|
||||||
|
self.p = sp.Popen(args)
|
||||||
|
# , stdout=sp.PIPE, stderr=sp.PIPE)
|
||||||
|
|
||||||
|
self.t = threading.Thread(target=self._run)
|
||||||
|
self.t.daemon = True
|
||||||
|
self.t.start()
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
self.so, self.se = self.p.communicate()
|
||||||
|
|
||||||
|
def stop(self, wait):
|
||||||
|
if wait:
|
||||||
|
os.kill(self.p.pid, signal.SIGINT)
|
||||||
|
self.t.join(timeout=2)
|
||||||
|
else:
|
||||||
|
self.p.kill() # macos py3.8
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
t = os.listdir()
|
||||||
|
for f in t:
|
||||||
|
if f not in self.ls_pre and f.startswith("up."):
|
||||||
|
os.unlink(f)
|
||||||
|
|
||||||
|
def await_idle(self, ub, timeout):
|
||||||
|
req = ["scanning</td><td>False", "hash-q</td><td>0", "tag-q</td><td>0"]
|
||||||
|
lim = int(timeout * 10)
|
||||||
|
u = ub + "?h"
|
||||||
|
for n in range(lim):
|
||||||
|
try:
|
||||||
|
time.sleep(0.1)
|
||||||
|
r = requests.get(u, timeout=0.1)
|
||||||
|
for x in req:
|
||||||
|
if x not in r.text:
|
||||||
|
print("ST: {}/{} miss {}".format(n, lim, x))
|
||||||
|
raise Exception()
|
||||||
|
print("ST: idle")
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def tc1():
|
||||||
|
ub = "http://127.0.0.1:4321/"
|
||||||
|
td = os.path.join("srv", "smoketest")
|
||||||
|
try:
|
||||||
|
shutil.rmtree(td)
|
||||||
|
except:
|
||||||
|
if os.path.exists(td):
|
||||||
|
raise
|
||||||
|
|
||||||
|
for _ in range(10):
|
||||||
|
try:
|
||||||
|
os.mkdir(td)
|
||||||
|
except:
|
||||||
|
time.sleep(0.1) # win10
|
||||||
|
|
||||||
|
assert os.path.exists(td)
|
||||||
|
|
||||||
|
vidp = os.path.join(tempfile.gettempdir(), "smoketest.h264")
|
||||||
|
if not os.path.exists(vidp):
|
||||||
|
cmd = "ffmpeg -f lavfi -i testsrc=48x32:3 -t 1 -c:v libx264 -tune animation -preset veryslow -crf 69"
|
||||||
|
sp.check_call(cmd.split(" ") + [vidp])
|
||||||
|
|
||||||
|
with open(vidp, "rb") as f:
|
||||||
|
ovid = f.read()
|
||||||
|
|
||||||
|
args = [
|
||||||
|
"-p4321",
|
||||||
|
"-e2dsa",
|
||||||
|
"-e2tsr",
|
||||||
|
"--no-mutagen",
|
||||||
|
"--th-ff-jpg",
|
||||||
|
"--hist",
|
||||||
|
os.path.join(td, "dbm"),
|
||||||
|
]
|
||||||
|
pdirs = []
|
||||||
|
hpaths = {}
|
||||||
|
|
||||||
|
for d1 in ["r", "w", "a"]:
|
||||||
|
pdirs.append("{}/{}".format(td, d1))
|
||||||
|
pdirs.append("{}/{}/j".format(td, d1))
|
||||||
|
for d2 in ["r", "w", "a"]:
|
||||||
|
d = os.path.join(td, d1, "j", d2)
|
||||||
|
pdirs.append(d)
|
||||||
|
os.makedirs(d)
|
||||||
|
|
||||||
|
pdirs = [x.replace("\\", "/") for x in pdirs]
|
||||||
|
udirs = [x.split("/", 2)[2] for x in pdirs]
|
||||||
|
perms = [x.rstrip("j/")[-1] for x in pdirs]
|
||||||
|
for pd, ud, p in zip(pdirs, udirs, perms):
|
||||||
|
if ud[-1] == "j":
|
||||||
|
continue
|
||||||
|
|
||||||
|
hp = None
|
||||||
|
if pd.endswith("st/a"):
|
||||||
|
hp = hpaths[ud] = os.path.join(td, "db1")
|
||||||
|
elif pd[:-1].endswith("a/j/"):
|
||||||
|
hpaths[ud] = os.path.join(td, "dbm")
|
||||||
|
hp = None
|
||||||
|
else:
|
||||||
|
hp = "-"
|
||||||
|
hpaths[ud] = os.path.join(pd, ".hist")
|
||||||
|
|
||||||
|
arg = "{}:{}:{}".format(pd, ud, p, hp)
|
||||||
|
if hp:
|
||||||
|
arg += ":chist=" + hp
|
||||||
|
|
||||||
|
args += ["-v", arg]
|
||||||
|
|
||||||
|
# return
|
||||||
|
cpp = Cpp(args)
|
||||||
|
CPP.append(cpp)
|
||||||
|
cpp.await_idle(ub, 3)
|
||||||
|
|
||||||
|
for d in udirs:
|
||||||
|
vid = ovid + "\n{}".format(d).encode("utf-8")
|
||||||
|
try:
|
||||||
|
requests.post(ub + d, data={"act": "bput"}, files={"f": ("a.h264", vid)})
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
cpp.clean()
|
||||||
|
|
||||||
|
# GET permission
|
||||||
|
for d, p in zip(udirs, perms):
|
||||||
|
u = "{}{}/a.h264".format(ub, d)
|
||||||
|
r = requests.get(u)
|
||||||
|
ok = bool(r)
|
||||||
|
if ok != (p in ["a"]):
|
||||||
|
raise Exception("get {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
|
# stat filesystem
|
||||||
|
for d, p in zip(pdirs, perms):
|
||||||
|
u = "{}/a.h264".format(d)
|
||||||
|
ok = os.path.exists(u)
|
||||||
|
if ok != (p in ["a", "w"]):
|
||||||
|
raise Exception("stat {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
|
# GET thumbnail, vreify contents
|
||||||
|
for d, p in zip(udirs, perms):
|
||||||
|
u = "{}{}/a.h264?th=j".format(ub, d)
|
||||||
|
r = requests.get(u)
|
||||||
|
ok = bool(r and r.content[:3] == b"\xff\xd8\xff")
|
||||||
|
if ok != (p in ["a"]):
|
||||||
|
raise Exception("thumb {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
|
# check tags
|
||||||
|
cpp.await_idle(ub, 5)
|
||||||
|
for d, p in zip(udirs, perms):
|
||||||
|
u = "{}{}?ls".format(ub, d)
|
||||||
|
r = requests.get(u)
|
||||||
|
j = r.json() if r else False
|
||||||
|
tag = None
|
||||||
|
if j:
|
||||||
|
for f in j["files"]:
|
||||||
|
tag = tag or f["tags"].get("res")
|
||||||
|
|
||||||
|
r_ok = bool(j)
|
||||||
|
w_ok = bool(r_ok and j.get("files"))
|
||||||
|
|
||||||
|
if not r_ok or w_ok != (p in ["a"]):
|
||||||
|
raise Exception("ls {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
|
if (tag and p != "a") or (not tag and p == "a"):
|
||||||
|
raise Exception("tag {} with perm {} at {}".format(tag, p, u))
|
||||||
|
|
||||||
|
if tag is not None and tag != "48x32":
|
||||||
|
raise Exception("tag [{}] at {}".format(tag, u))
|
||||||
|
|
||||||
|
cpp.stop(True)
|
||||||
|
|
||||||
|
|
||||||
|
def run(tc):
|
||||||
|
try:
|
||||||
|
tc()
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
CPP[0].stop(False)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
run(tc1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
55
setup.py
55
setup.py
@@ -5,22 +5,7 @@ from __future__ import print_function
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
|
from setuptools import setup, Command, find_packages
|
||||||
setuptools_available = True
|
|
||||||
try:
|
|
||||||
# need setuptools to build wheel
|
|
||||||
from setuptools import setup, Command, find_packages
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# works in a pinch
|
|
||||||
setuptools_available = False
|
|
||||||
from distutils.core import setup, Command
|
|
||||||
|
|
||||||
from distutils.spawn import spawn
|
|
||||||
|
|
||||||
if "bdist_wheel" in sys.argv and not setuptools_available:
|
|
||||||
print("cannot build wheel without setuptools")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
NAME = "copyparty"
|
NAME = "copyparty"
|
||||||
@@ -100,9 +85,8 @@ args = {
|
|||||||
"author_email": "copyparty@ocv.me",
|
"author_email": "copyparty@ocv.me",
|
||||||
"url": "https://github.com/9001/copyparty",
|
"url": "https://github.com/9001/copyparty",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"data_files": data_files,
|
|
||||||
"classifiers": [
|
"classifiers": [
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 4 - Beta",
|
||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
"Programming Language :: Python :: 2",
|
"Programming Language :: Python :: 2",
|
||||||
@@ -120,35 +104,16 @@ args = {
|
|||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
"Environment :: No Input/Output (Daemon)",
|
"Environment :: No Input/Output (Daemon)",
|
||||||
"Topic :: Communications :: File Sharing",
|
"Topic :: Communications :: File Sharing",
|
||||||
|
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
||||||
],
|
],
|
||||||
|
"include_package_data": True,
|
||||||
|
"data_files": data_files,
|
||||||
|
"packages": find_packages(),
|
||||||
|
"install_requires": ["jinja2"],
|
||||||
|
"extras_require": {"thumbnails": ["Pillow"], "audiotags": ["mutagen"]},
|
||||||
|
"entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]},
|
||||||
|
"scripts": ["bin/copyparty-fuse.py"],
|
||||||
"cmdclass": {"clean2": clean2},
|
"cmdclass": {"clean2": clean2},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if setuptools_available:
|
|
||||||
args.update(
|
|
||||||
{
|
|
||||||
"packages": find_packages(),
|
|
||||||
"install_requires": ["jinja2"],
|
|
||||||
"extras_require": {"thumbnails": ["Pillow"]},
|
|
||||||
"include_package_data": True,
|
|
||||||
"entry_points": {
|
|
||||||
"console_scripts": ["copyparty = copyparty.__main__:main"]
|
|
||||||
},
|
|
||||||
"scripts": ["bin/copyparty-fuse.py"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
args.update(
|
|
||||||
{
|
|
||||||
"packages": ["copyparty", "copyparty.stolen"],
|
|
||||||
"scripts": ["bin/copyparty-fuse.py"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# import pprint
|
|
||||||
# pprint.PrettyPrinter().pprint(args)
|
|
||||||
# sys.exit(0)
|
|
||||||
|
|
||||||
setup(**args)
|
setup(**args)
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import time
|
|||||||
import shutil
|
import shutil
|
||||||
import pprint
|
import pprint
|
||||||
import tarfile
|
import tarfile
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from copyparty.authsrv import AuthSrv
|
|
||||||
from copyparty.httpcli import HttpCli
|
|
||||||
|
|
||||||
from tests import util as tu
|
from tests import util as tu
|
||||||
|
from copyparty.authsrv import AuthSrv
|
||||||
|
from copyparty.httpcli import HttpCli
|
||||||
|
|
||||||
|
|
||||||
def hdr(query):
|
def hdr(query):
|
||||||
@@ -28,25 +28,33 @@ class Cfg(Namespace):
|
|||||||
a=a,
|
a=a,
|
||||||
v=v,
|
v=v,
|
||||||
c=c,
|
c=c,
|
||||||
|
rproxy=0,
|
||||||
ed=False,
|
ed=False,
|
||||||
no_zip=False,
|
no_zip=False,
|
||||||
no_scandir=False,
|
no_scandir=False,
|
||||||
no_sendfile=True,
|
no_sendfile=True,
|
||||||
|
no_rescan=True,
|
||||||
|
ihead=False,
|
||||||
nih=True,
|
nih=True,
|
||||||
mtp=[],
|
mtp=[],
|
||||||
mte="a",
|
mte="a",
|
||||||
|
hist=None,
|
||||||
|
no_hash=False,
|
||||||
|
css_browser=None,
|
||||||
**{k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()}
|
**{k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestHttpCli(unittest.TestCase):
|
class TestHttpCli(unittest.TestCase):
|
||||||
def test(self):
|
def setUp(self):
|
||||||
td = os.path.join(tu.get_ramdisk(), "vfs")
|
self.td = tu.get_ramdisk()
|
||||||
try:
|
|
||||||
shutil.rmtree(td)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
os.chdir(tempfile.gettempdir())
|
||||||
|
shutil.rmtree(self.td)
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
td = os.path.join(self.td, "vfs")
|
||||||
os.mkdir(td)
|
os.mkdir(td)
|
||||||
os.chdir(td)
|
os.chdir(td)
|
||||||
|
|
||||||
@@ -95,7 +103,7 @@ class TestHttpCli(unittest.TestCase):
|
|||||||
pprint.pprint(vcfg)
|
pprint.pprint(vcfg)
|
||||||
|
|
||||||
self.args = Cfg(v=vcfg, a=["o:o", "x:x"])
|
self.args = Cfg(v=vcfg, a=["o:o", "x:x"])
|
||||||
self.auth = AuthSrv(self.args, self.log)
|
self.asrv = AuthSrv(self.args, self.log)
|
||||||
vfiles = [x for x in allfiles if x.startswith(top)]
|
vfiles = [x for x in allfiles if x.startswith(top)]
|
||||||
for fp in vfiles:
|
for fp in vfiles:
|
||||||
rok, wok = self.can_rw(fp)
|
rok, wok = self.can_rw(fp)
|
||||||
@@ -184,12 +192,12 @@ class TestHttpCli(unittest.TestCase):
|
|||||||
def put(self, url):
|
def put(self, url):
|
||||||
buf = "PUT /{0} HTTP/1.1\r\nCookie: cppwd=o\r\nConnection: close\r\nContent-Length: {1}\r\n\r\nok {0}\n"
|
buf = "PUT /{0} HTTP/1.1\r\nCookie: cppwd=o\r\nConnection: close\r\nContent-Length: {1}\r\n\r\nok {0}\n"
|
||||||
buf = buf.format(url, len(url) + 4).encode("utf-8")
|
buf = buf.format(url, len(url) + 4).encode("utf-8")
|
||||||
conn = tu.VHttpConn(self.args, self.auth, self.log, buf)
|
conn = tu.VHttpConn(self.args, self.asrv, self.log, buf)
|
||||||
HttpCli(conn).run()
|
HttpCli(conn).run()
|
||||||
return conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
|
return conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
|
||||||
|
|
||||||
def curl(self, url, binary=False):
|
def curl(self, url, binary=False):
|
||||||
conn = tu.VHttpConn(self.args, self.auth, self.log, hdr(url))
|
conn = tu.VHttpConn(self.args, self.asrv, self.log, hdr(url))
|
||||||
HttpCli(conn).run()
|
HttpCli(conn).run()
|
||||||
if binary:
|
if binary:
|
||||||
h, b = conn.s._reply.split(b"\r\n\r\n", 1)
|
h, b = conn.s._reply.split(b"\r\n\r\n", 1)
|
||||||
|
|||||||
@@ -7,24 +7,37 @@ import json
|
|||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from copyparty.authsrv import AuthSrv
|
|
||||||
from copyparty import util
|
|
||||||
|
|
||||||
from tests import util as tu
|
from tests import util as tu
|
||||||
|
from copyparty.authsrv import AuthSrv, VFS
|
||||||
|
from copyparty import util
|
||||||
|
|
||||||
|
|
||||||
class Cfg(Namespace):
|
class Cfg(Namespace):
|
||||||
def __init__(self, a=[], v=[], c=None):
|
def __init__(self, a=[], v=[], c=None):
|
||||||
ex = {k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()}
|
ex = {k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()}
|
||||||
ex["mtp"] = []
|
ex2 = {
|
||||||
ex["mte"] = "a"
|
"mtp": [],
|
||||||
|
"mte": "a",
|
||||||
|
"hist": None,
|
||||||
|
"no_hash": False,
|
||||||
|
"css_browser": None,
|
||||||
|
"rproxy": 0,
|
||||||
|
}
|
||||||
|
ex.update(ex2)
|
||||||
super(Cfg, self).__init__(a=a, v=v, c=c, **ex)
|
super(Cfg, self).__init__(a=a, v=v, c=c, **ex)
|
||||||
|
|
||||||
|
|
||||||
class TestVFS(unittest.TestCase):
|
class TestVFS(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.td = tu.get_ramdisk()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
os.chdir(tempfile.gettempdir())
|
||||||
|
shutil.rmtree(self.td)
|
||||||
|
|
||||||
def dump(self, vfs):
|
def dump(self, vfs):
|
||||||
print(json.dumps(vfs, indent=4, sort_keys=True, default=lambda o: o.__dict__))
|
print(json.dumps(vfs, indent=4, sort_keys=True, default=lambda o: o.__dict__))
|
||||||
|
|
||||||
@@ -41,6 +54,7 @@ class TestVFS(unittest.TestCase):
|
|||||||
self.assertEqual(util.undot(query), response)
|
self.assertEqual(util.undot(query), response)
|
||||||
|
|
||||||
def ls(self, vfs, vpath, uname):
|
def ls(self, vfs, vpath, uname):
|
||||||
|
# type: (VFS, str, str) -> tuple[str, str, str]
|
||||||
"""helper for resolving and listing a folder"""
|
"""helper for resolving and listing a folder"""
|
||||||
vn, rem = vfs.get(vpath, uname, True, False)
|
vn, rem = vfs.get(vpath, uname, True, False)
|
||||||
r1 = vn.ls(rem, uname, False)
|
r1 = vn.ls(rem, uname, False)
|
||||||
@@ -55,12 +69,7 @@ class TestVFS(unittest.TestCase):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def test(self):
|
def test(self):
|
||||||
td = os.path.join(tu.get_ramdisk(), "vfs")
|
td = os.path.join(self.td, "vfs")
|
||||||
try:
|
|
||||||
shutil.rmtree(td)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
os.mkdir(td)
|
os.mkdir(td)
|
||||||
os.chdir(td)
|
os.chdir(td)
|
||||||
|
|
||||||
@@ -111,13 +120,13 @@ class TestVFS(unittest.TestCase):
|
|||||||
n = vfs.nodes["a"]
|
n = vfs.nodes["a"]
|
||||||
self.assertEqual(len(vfs.nodes), 1)
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
self.assertEqual(n.vpath, "a")
|
self.assertEqual(n.vpath, "a")
|
||||||
self.assertEqual(n.realpath, td + "/a")
|
self.assertEqual(n.realpath, os.path.join(td, "a"))
|
||||||
self.assertEqual(n.uread, ["*", "k"])
|
self.assertEqual(n.uread, ["*", "k"])
|
||||||
self.assertEqual(n.uwrite, ["k"])
|
self.assertEqual(n.uwrite, ["k"])
|
||||||
n = n.nodes["ac"]
|
n = n.nodes["ac"]
|
||||||
self.assertEqual(len(vfs.nodes), 1)
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
self.assertEqual(n.vpath, "a/ac")
|
self.assertEqual(n.vpath, "a/ac")
|
||||||
self.assertEqual(n.realpath, td + "/a/ac")
|
self.assertEqual(n.realpath, os.path.join(td, "a", "ac"))
|
||||||
self.assertEqual(n.uread, ["*", "k"])
|
self.assertEqual(n.uread, ["*", "k"])
|
||||||
self.assertEqual(n.uwrite, ["k"])
|
self.assertEqual(n.uwrite, ["k"])
|
||||||
n = n.nodes["acb"]
|
n = n.nodes["acb"]
|
||||||
@@ -227,7 +236,7 @@ class TestVFS(unittest.TestCase):
|
|||||||
self.assertEqual(list(v1), list(v2))
|
self.assertEqual(list(v1), list(v2))
|
||||||
|
|
||||||
# config file parser
|
# config file parser
|
||||||
cfg_path = os.path.join(tu.get_ramdisk(), "test.cfg")
|
cfg_path = os.path.join(self.td, "test.cfg")
|
||||||
with open(cfg_path, "wb") as f:
|
with open(cfg_path, "wb") as f:
|
||||||
f.write(
|
f.write(
|
||||||
dedent(
|
dedent(
|
||||||
@@ -249,7 +258,7 @@ class TestVFS(unittest.TestCase):
|
|||||||
n = au.vfs
|
n = au.vfs
|
||||||
# root was not defined, so PWD with no access to anyone
|
# root was not defined, so PWD with no access to anyone
|
||||||
self.assertEqual(n.vpath, "")
|
self.assertEqual(n.vpath, "")
|
||||||
self.assertEqual(n.realpath, td)
|
self.assertEqual(n.realpath, None)
|
||||||
self.assertEqual(n.uread, [])
|
self.assertEqual(n.uread, [])
|
||||||
self.assertEqual(n.uwrite, [])
|
self.assertEqual(n.uwrite, [])
|
||||||
self.assertEqual(len(n.nodes), 1)
|
self.assertEqual(len(n.nodes), 1)
|
||||||
@@ -260,6 +269,4 @@ class TestVFS(unittest.TestCase):
|
|||||||
self.assertEqual(n.uwrite, ["asd"])
|
self.assertEqual(n.uwrite, ["asd"])
|
||||||
self.assertEqual(len(n.nodes), 0)
|
self.assertEqual(len(n.nodes), 0)
|
||||||
|
|
||||||
os.chdir(tempfile.gettempdir())
|
|
||||||
shutil.rmtree(td)
|
|
||||||
os.unlink(cfg_path)
|
os.unlink(cfg_path)
|
||||||
|
|||||||
@@ -1,16 +1,36 @@
|
|||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import shutil
|
||||||
import jinja2
|
import jinja2
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import platform
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
|
|
||||||
from copyparty.util import Unrecv
|
|
||||||
|
|
||||||
|
WINDOWS = platform.system() == "Windows"
|
||||||
|
ANYWIN = WINDOWS or sys.platform in ["msys"]
|
||||||
|
MACOS = platform.system() == "Darwin"
|
||||||
|
|
||||||
J2_ENV = jinja2.Environment(loader=jinja2.BaseLoader)
|
J2_ENV = jinja2.Environment(loader=jinja2.BaseLoader)
|
||||||
J2_FILES = J2_ENV.from_string("{{ files|join('\n') }}")
|
J2_FILES = J2_ENV.from_string("{{ files|join('\n') }}")
|
||||||
|
|
||||||
|
|
||||||
|
def nah(*a, **ka):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if MACOS:
|
||||||
|
import posixpath
|
||||||
|
|
||||||
|
posixpath.islink = nah
|
||||||
|
os.path.islink = nah
|
||||||
|
# 25% faster; until any tests do symlink stuff
|
||||||
|
|
||||||
|
|
||||||
|
from copyparty.util import Unrecv
|
||||||
|
|
||||||
|
|
||||||
def runcmd(*argv):
|
def runcmd(*argv):
|
||||||
p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE)
|
p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE)
|
||||||
stdout, stderr = p.communicate()
|
stdout, stderr = p.communicate()
|
||||||
@@ -28,18 +48,25 @@ def chkcmd(*argv):
|
|||||||
|
|
||||||
|
|
||||||
def get_ramdisk():
|
def get_ramdisk():
|
||||||
|
def subdir(top):
|
||||||
|
ret = os.path.join(top, "cptd-{}".format(os.getpid()))
|
||||||
|
shutil.rmtree(ret, True)
|
||||||
|
os.mkdir(ret)
|
||||||
|
return ret
|
||||||
|
|
||||||
for vol in ["/dev/shm", "/Volumes/cptd"]: # nosec (singleton test)
|
for vol in ["/dev/shm", "/Volumes/cptd"]: # nosec (singleton test)
|
||||||
if os.path.exists(vol):
|
if os.path.exists(vol):
|
||||||
return vol
|
return subdir(vol)
|
||||||
|
|
||||||
if os.path.exists("/Volumes"):
|
if os.path.exists("/Volumes"):
|
||||||
devname, _ = chkcmd("hdiutil", "attach", "-nomount", "ram://32768")
|
# hdiutil eject /Volumes/cptd/
|
||||||
|
devname, _ = chkcmd("hdiutil", "attach", "-nomount", "ram://131072")
|
||||||
devname = devname.strip()
|
devname = devname.strip()
|
||||||
print("devname: [{}]".format(devname))
|
print("devname: [{}]".format(devname))
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
try:
|
try:
|
||||||
_, _ = chkcmd("diskutil", "eraseVolume", "HFS+", "cptd", devname)
|
_, _ = chkcmd("diskutil", "eraseVolume", "HFS+", "cptd", devname)
|
||||||
return "/Volumes/cptd"
|
return subdir("/Volumes/cptd")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(repr(ex))
|
print(repr(ex))
|
||||||
time.sleep(0.25)
|
time.sleep(0.25)
|
||||||
@@ -50,7 +77,7 @@ def get_ramdisk():
|
|||||||
try:
|
try:
|
||||||
os.mkdir(ret)
|
os.mkdir(ret)
|
||||||
finally:
|
finally:
|
||||||
return ret
|
return subdir(ret)
|
||||||
|
|
||||||
|
|
||||||
class NullBroker(object):
|
class NullBroker(object):
|
||||||
@@ -81,17 +108,25 @@ class VHttpSrv(object):
|
|||||||
aliases = ["splash", "browser", "browser2", "msg", "md", "mde"]
|
aliases = ["splash", "browser", "browser2", "msg", "md", "mde"]
|
||||||
self.j2 = {x: J2_FILES for x in aliases}
|
self.j2 = {x: J2_FILES for x in aliases}
|
||||||
|
|
||||||
|
def cachebuster(self):
|
||||||
|
return "a"
|
||||||
|
|
||||||
|
|
||||||
class VHttpConn(object):
|
class VHttpConn(object):
|
||||||
def __init__(self, args, auth, log, buf):
|
def __init__(self, args, asrv, log, buf):
|
||||||
self.s = VSock(buf)
|
self.s = VSock(buf)
|
||||||
self.sr = Unrecv(self.s)
|
self.sr = Unrecv(self.s)
|
||||||
self.addr = ("127.0.0.1", "42069")
|
self.addr = ("127.0.0.1", "42069")
|
||||||
self.args = args
|
self.args = args
|
||||||
self.auth = auth
|
self.asrv = asrv
|
||||||
|
self.is_mp = False
|
||||||
self.log_func = log
|
self.log_func = log
|
||||||
self.log_src = "a"
|
self.log_src = "a"
|
||||||
|
self.lf_url = None
|
||||||
self.hsrv = VHttpSrv()
|
self.hsrv = VHttpSrv()
|
||||||
|
self.nreq = 0
|
||||||
self.nbyte = 0
|
self.nbyte = 0
|
||||||
self.workload = 0
|
self.workload = 0
|
||||||
|
self.ico = None
|
||||||
|
self.thumbcli = None
|
||||||
self.t0 = time.time()
|
self.t0 = time.time()
|
||||||
Reference in New Issue
Block a user