mirror of
https://github.com/9001/copyparty.git
synced 2025-11-05 06:13:20 +00:00
Compare commits
443 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
231a03bcfd | ||
|
|
bc85723657 | ||
|
|
be32b743c6 | ||
|
|
83c9843059 | ||
|
|
11cf43626d | ||
|
|
a6dc5e2ce3 | ||
|
|
38593a0394 | ||
|
|
95309afeea | ||
|
|
c2bf6fe2a3 | ||
|
|
99ac324fbd | ||
|
|
5562de330f | ||
|
|
95014236ac | ||
|
|
6aa7386138 | ||
|
|
3226a1f588 | ||
|
|
b4cf890cd8 | ||
|
|
ce09e323af | ||
|
|
941aedb177 | ||
|
|
87a0d502a3 | ||
|
|
cab7c1b0b8 | ||
|
|
d5892341b6 | ||
|
|
646557a43e | ||
|
|
ed8d34ab43 | ||
|
|
5e34463c77 | ||
|
|
1b14eb7959 | ||
|
|
ed48c2d0ed | ||
|
|
26fe84b660 | ||
|
|
5938230270 | ||
|
|
1a33a047fa | ||
|
|
43a8bcefb9 | ||
|
|
2e740e513f | ||
|
|
8a21a86b61 | ||
|
|
f600116205 | ||
|
|
1c03705de8 | ||
|
|
f7e461fac6 | ||
|
|
03ce6c97ff | ||
|
|
ffd9e76e07 | ||
|
|
fc49cb1e67 | ||
|
|
f5712d9f25 | ||
|
|
161d57bdda | ||
|
|
bae0d440bf | ||
|
|
fff052dde1 | ||
|
|
73b06eaa02 | ||
|
|
08a8ebed17 | ||
|
|
74d07426b3 | ||
|
|
69a2bba99a | ||
|
|
4d685d78ee | ||
|
|
5845ec3f49 | ||
|
|
13373426fe | ||
|
|
8e55551a06 | ||
|
|
12a3f0ac31 | ||
|
|
18e33edc88 | ||
|
|
c72c5ad4ee | ||
|
|
0fbc81ab2f | ||
|
|
af0a34cf82 | ||
|
|
b4590c5398 | ||
|
|
f787a66230 | ||
|
|
b21a99fd62 | ||
|
|
eb16306cde | ||
|
|
7bc23687e3 | ||
|
|
e1eaa057f2 | ||
|
|
97c264ca3e | ||
|
|
cf848ab1f7 | ||
|
|
cf83f9b0fd | ||
|
|
d98e361083 | ||
|
|
ce7f5309c7 | ||
|
|
75c485ced7 | ||
|
|
9c6e2ec012 | ||
|
|
1a02948a61 | ||
|
|
8b05ba4ba1 | ||
|
|
21e2874cb7 | ||
|
|
360ed5c46c | ||
|
|
5099bc365d | ||
|
|
12986da147 | ||
|
|
23e72797bc | ||
|
|
ac7b6f8f55 | ||
|
|
981b9ff11e | ||
|
|
4186906f4c | ||
|
|
0850d24e0c | ||
|
|
7ab8334c96 | ||
|
|
a4d7329ab7 | ||
|
|
3f4eae6bce | ||
|
|
518cf4be57 | ||
|
|
71096182be | ||
|
|
6452e927ea | ||
|
|
bc70cfa6f0 | ||
|
|
2b6e5ebd2d | ||
|
|
c761bd799a | ||
|
|
2f7c2fdee4 | ||
|
|
70a76ec343 | ||
|
|
7c3f64abf2 | ||
|
|
f5f38f195c | ||
|
|
7e84f4f015 | ||
|
|
4802f8cf07 | ||
|
|
cc05e67d8f | ||
|
|
2b6b174517 | ||
|
|
a1d05e6e12 | ||
|
|
f95ceb6a9b | ||
|
|
8f91b0726d | ||
|
|
97807f4383 | ||
|
|
5f42237f2c | ||
|
|
68289cfa54 | ||
|
|
42ea30270f | ||
|
|
ebbbbf3d82 | ||
|
|
27516e2d16 | ||
|
|
84bb6f915e | ||
|
|
46752f758a | ||
|
|
34c4c22e61 | ||
|
|
af2d0b8421 | ||
|
|
638b05a49a | ||
|
|
7a13e8a7fc | ||
|
|
d9fa74711d | ||
|
|
41867f578f | ||
|
|
0bf41ed4ef | ||
|
|
d080b4a731 | ||
|
|
ca4232ada9 | ||
|
|
ad348f91c9 | ||
|
|
990f915f42 | ||
|
|
53d720217b | ||
|
|
7a06ff480d | ||
|
|
3ef551f788 | ||
|
|
f0125cdc36 | ||
|
|
ed5f6736df | ||
|
|
15d8be0fae | ||
|
|
46f3e61360 | ||
|
|
87ad8c98d4 | ||
|
|
9bbdc4100f | ||
|
|
c80307e8ff | ||
|
|
c1d77e1041 | ||
|
|
d9e83650dc | ||
|
|
f6d635acd9 | ||
|
|
0dbd8a01ff | ||
|
|
8d755d41e0 | ||
|
|
190473bd32 | ||
|
|
030d1ec254 | ||
|
|
5a2b91a084 | ||
|
|
a50a05e4e7 | ||
|
|
6cb5a87c79 | ||
|
|
b9f89ca552 | ||
|
|
26c9fd5dea | ||
|
|
e81a9b6fe0 | ||
|
|
452450e451 | ||
|
|
419dd2d1c7 | ||
|
|
ee86b06676 | ||
|
|
953183f16d | ||
|
|
228f71708b | ||
|
|
621471a7cb | ||
|
|
8b58e951e3 | ||
|
|
1db489a0aa | ||
|
|
be65c3c6cf | ||
|
|
46e7fa31fe | ||
|
|
66e21bd499 | ||
|
|
8cab4c01fd | ||
|
|
d52038366b | ||
|
|
4fcfd87f5b | ||
|
|
f893c6baa4 | ||
|
|
9a45549b66 | ||
|
|
ae3a01038b | ||
|
|
e47a2a4ca2 | ||
|
|
95ea6d5f78 | ||
|
|
7d290f6b8f | ||
|
|
9db617ed5a | ||
|
|
514456940a | ||
|
|
33feefd9cd | ||
|
|
65e14cf348 | ||
|
|
1d61bcc4f3 | ||
|
|
c38bbaca3c | ||
|
|
246d245ebc | ||
|
|
f269a710e2 | ||
|
|
051998429c | ||
|
|
432cdd640f | ||
|
|
9ed9b0964e | ||
|
|
6a97b3526d | ||
|
|
451d757996 | ||
|
|
f9e9eba3b1 | ||
|
|
2a9a6aebd9 | ||
|
|
adbb6c449e | ||
|
|
3993605324 | ||
|
|
0ae574ec2c | ||
|
|
c56ded828c | ||
|
|
02c7061945 | ||
|
|
9209e44cd3 | ||
|
|
ebed37394e | ||
|
|
4c7a2a7ec3 | ||
|
|
0a25a88a34 | ||
|
|
6aa9025347 | ||
|
|
a918cc67eb | ||
|
|
08f4695283 | ||
|
|
44e76d5eeb | ||
|
|
cfa36fd279 | ||
|
|
3d4166e006 | ||
|
|
07bac1c592 | ||
|
|
755f2ce1ba | ||
|
|
cca2844deb | ||
|
|
24a2f760b7 | ||
|
|
79bbd8fe38 | ||
|
|
35dce1e3e4 | ||
|
|
f886fdf913 | ||
|
|
4476f2f0da | ||
|
|
160f161700 | ||
|
|
c164fc58a2 | ||
|
|
0c625a4e62 | ||
|
|
bf3941cf7a | ||
|
|
3649e8288a | ||
|
|
9a45e26026 | ||
|
|
e65f127571 | ||
|
|
3bfc699787 | ||
|
|
955318428a | ||
|
|
f6279b356a | ||
|
|
4cc3cdc989 | ||
|
|
f9aa20a3ad | ||
|
|
129d33f1a0 | ||
|
|
1ad7a3f378 | ||
|
|
b533be8818 | ||
|
|
fb729e5166 | ||
|
|
d337ecdb20 | ||
|
|
5f1f0a48b0 | ||
|
|
e0f1cb94a5 | ||
|
|
a362ee2246 | ||
|
|
19f23c686e | ||
|
|
23b20ff4a6 | ||
|
|
72574da834 | ||
|
|
d5a79455d1 | ||
|
|
070d4b9da9 | ||
|
|
0ace22fffe | ||
|
|
9e483d7694 | ||
|
|
26458b7a06 | ||
|
|
b6a4604952 | ||
|
|
af752fbbc2 | ||
|
|
279c9d706a | ||
|
|
806e7b5530 | ||
|
|
f3dc6a217b | ||
|
|
7671d791fa | ||
|
|
8cd84608a5 | ||
|
|
980c6fc810 | ||
|
|
fb40a484c5 | ||
|
|
daa9dedcaa | ||
|
|
0d634345ac | ||
|
|
e648252479 | ||
|
|
179d7a9ad8 | ||
|
|
19bc962ad5 | ||
|
|
27cce086c6 | ||
|
|
fec0c620d4 | ||
|
|
05a1a31cab | ||
|
|
d020527c6f | ||
|
|
4451485664 | ||
|
|
a4e1a3738a | ||
|
|
4339dbeb8d | ||
|
|
5b0605774c | ||
|
|
e3684e25f8 | ||
|
|
1359213196 | ||
|
|
03efc6a169 | ||
|
|
15b5982211 | ||
|
|
0eb3a5d387 | ||
|
|
7f8777389c | ||
|
|
4eb20f10ad | ||
|
|
daa11df558 | ||
|
|
1bb0db30a0 | ||
|
|
02910b0020 | ||
|
|
23b8901c9c | ||
|
|
99f6ed0cd7 | ||
|
|
890c310880 | ||
|
|
0194eeb31f | ||
|
|
f9be4c62b1 | ||
|
|
027e8c18f1 | ||
|
|
4a3bb35a95 | ||
|
|
4bfb0d4494 | ||
|
|
7e0ef03a1e | ||
|
|
f7dbd95a54 | ||
|
|
515ee2290b | ||
|
|
b0c78910bb | ||
|
|
f4ca62b664 | ||
|
|
8eb8043a3d | ||
|
|
3e8541362a | ||
|
|
789724e348 | ||
|
|
5125b9532f | ||
|
|
ebc9de02b0 | ||
|
|
ec788fa491 | ||
|
|
9b5e264574 | ||
|
|
57c297274b | ||
|
|
e9bf092317 | ||
|
|
d173887324 | ||
|
|
99820d854c | ||
|
|
62df0a0eb2 | ||
|
|
600e9ac947 | ||
|
|
3ca41be2b4 | ||
|
|
5c7debd900 | ||
|
|
7fa5b23ce3 | ||
|
|
ff82738aaf | ||
|
|
bf5ee9d643 | ||
|
|
72a8593ecd | ||
|
|
bc3bbe07d4 | ||
|
|
c7cb64bfef | ||
|
|
629f537d06 | ||
|
|
9e988041b8 | ||
|
|
f9a8b5c9d7 | ||
|
|
b9c3538253 | ||
|
|
2bc0cdf017 | ||
|
|
02a91f60d4 | ||
|
|
fae83da197 | ||
|
|
0fe4aa6418 | ||
|
|
21a51bf0dc | ||
|
|
bcb353cc30 | ||
|
|
6af4508518 | ||
|
|
6a559bc28a | ||
|
|
0f5026cd20 | ||
|
|
a91b80a311 | ||
|
|
ec534701c8 | ||
|
|
af5169f67f | ||
|
|
18676c5e65 | ||
|
|
e2df6fda7b | ||
|
|
e9ae9782fe | ||
|
|
016dba4ca9 | ||
|
|
39c7ef305f | ||
|
|
849c1dc848 | ||
|
|
61414014fe | ||
|
|
578a915884 | ||
|
|
eacafb8a63 | ||
|
|
4446760f74 | ||
|
|
6da2a083f9 | ||
|
|
8837c8f822 | ||
|
|
bac301ed66 | ||
|
|
061db3906d | ||
|
|
fd7df5c952 | ||
|
|
a270019147 | ||
|
|
55e0209901 | ||
|
|
2b255fbbed | ||
|
|
8a2345a0fb | ||
|
|
bfa9f535aa | ||
|
|
f757623ad8 | ||
|
|
3c7465e268 | ||
|
|
108665fc4f | ||
|
|
ed519c9138 | ||
|
|
2dd2e2c57e | ||
|
|
6c3a976222 | ||
|
|
80cc26bd95 | ||
|
|
970fb84fd8 | ||
|
|
20cbcf6931 | ||
|
|
8fcde2a579 | ||
|
|
b32d1f8ad3 | ||
|
|
03513e0cb1 | ||
|
|
e041a2b197 | ||
|
|
d7d625be2a | ||
|
|
4121266678 | ||
|
|
22971a6be4 | ||
|
|
efbf8d7e0d | ||
|
|
397396ea4a | ||
|
|
e59b077c21 | ||
|
|
4bc39f3084 | ||
|
|
21c3570786 | ||
|
|
2f85c1fb18 | ||
|
|
1e27a4c2df | ||
|
|
456f575637 | ||
|
|
51546c9e64 | ||
|
|
83b4b70ef4 | ||
|
|
a5120d4f6f | ||
|
|
c95941e14f | ||
|
|
0dd531149d | ||
|
|
67da1b5219 | ||
|
|
919bd16437 | ||
|
|
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 |
5
.vscode/tasks.json
vendored
5
.vscode/tasks.json
vendored
@@ -9,7 +9,10 @@
|
|||||||
{
|
{
|
||||||
"label": "no_dbg",
|
"label": "no_dbg",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "${config:python.pythonPath} .vscode/launch.py"
|
"command": "${config:python.pythonPath}",
|
||||||
|
"args": [
|
||||||
|
".vscode/launch.py"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
606
README.md
606
README.md
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## summary
|
## summary
|
||||||
|
|
||||||
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 *any* web browser
|
||||||
|
|
||||||
* server runs on anything with `py2.7` or `py3.3+`
|
* server runs on anything with `py2.7` or `py3.3+`
|
||||||
* browse/upload with IE4 / netscape4.0 on win3.11 (heh)
|
* browse/upload with IE4 / netscape4.0 on win3.11 (heh)
|
||||||
@@ -19,55 +19,68 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
|||||||
## readme toc
|
## readme toc
|
||||||
|
|
||||||
* top
|
* top
|
||||||
* [quickstart](#quickstart)
|
* [quickstart](#quickstart) - download [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) and you're all set!
|
||||||
* [notes](#notes)
|
* [on debian](#on-debian) - recommended additional steps on debian
|
||||||
* [status](#status)
|
* [notes](#notes) - general notes
|
||||||
|
* [status](#status) - summary: all planned features work! now please enjoy the bloatening
|
||||||
|
* [testimonials](#testimonials) - small collection of user feedback
|
||||||
* [bugs](#bugs)
|
* [bugs](#bugs)
|
||||||
* [general bugs](#general-bugs)
|
* [general bugs](#general-bugs)
|
||||||
* [not my bugs](#not-my-bugs)
|
* [not my bugs](#not-my-bugs)
|
||||||
* [the browser](#the-browser)
|
* [accounts and volumes](#accounts-and-volumes) - per-folder, per-user permissions
|
||||||
* [tabs](#tabs)
|
* [the browser](#the-browser) - accessing a copyparty server using a web-browser
|
||||||
* [hotkeys](#hotkeys)
|
* [tabs](#tabs) - the main tabs in the ui
|
||||||
* [tree-mode](#tree-mode)
|
* [hotkeys](#hotkeys) - the browser has the following hotkeys (always qwerty)
|
||||||
* [thumbnails](#thumbnails)
|
* [navpane](#navpane) - switching between breadcrumbs or navpane
|
||||||
* [zip downloads](#zip-downloads)
|
* [thumbnails](#thumbnails) - press `g` to toggle image/video thumbnails instead of the file listing
|
||||||
* [uploading](#uploading)
|
* [zip downloads](#zip-downloads) - download folders (or file selections) as `zip` or `tar` files
|
||||||
* [file-search](#file-search)
|
* [uploading](#uploading) - web-browsers can upload using `bup` and `up2k`
|
||||||
* [markdown viewer](#markdown-viewer)
|
* [file-search](#file-search) - drop files/folders into up2k to see if they exist on the server
|
||||||
|
* [unpost](#unpost) - undo/delete accidental uploads
|
||||||
|
* [file manager](#file-manager) - cut/paste, rename, and delete files/folders (if you have permission)
|
||||||
|
* [batch rename](#batch-rename) - select some files and press F2 to bring up the rename UI
|
||||||
|
* [markdown viewer](#markdown-viewer) - and there are *two* editors
|
||||||
* [other tricks](#other-tricks)
|
* [other tricks](#other-tricks)
|
||||||
* [searching](#searching)
|
* [searching](#searching) - search by size, date, path/name, mp3-tags, ...
|
||||||
* [search configuration](#search-configuration)
|
* [server config](#server-config)
|
||||||
* [database location](#database-location)
|
* [file indexing](#file-indexing)
|
||||||
* [metadata from audio files](#metadata-from-audio-files)
|
* [upload rules](#upload-rules) - set upload rules using volume flags, some examples
|
||||||
* [file parser plugins](#file-parser-plugins)
|
* [compress uploads](#compress-uploads) - files can be autocompressed on upload
|
||||||
|
* [database location](#database-location) - can be stored in-volume (default) or elsewhere
|
||||||
|
* [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload
|
||||||
|
* [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags
|
||||||
* [complete examples](#complete-examples)
|
* [complete examples](#complete-examples)
|
||||||
* [browser support](#browser-support)
|
* [browser support](#browser-support) - TLDR: yes
|
||||||
* [client examples](#client-examples)
|
* [client examples](#client-examples) - interact with copyparty using non-browser clients
|
||||||
* [up2k](#up2k)
|
* [up2k](#up2k) - quick outline of the up2k protocol, see [uploading](#uploading) for the web-client
|
||||||
* [dependencies](#dependencies)
|
* [performance](#performance) - defaults are good for most cases
|
||||||
* [optional dependencies](#optional-dependencies)
|
* [dependencies](#dependencies) - mandatory deps
|
||||||
|
* [optional dependencies](#optional-dependencies) - install these to enable bonus features
|
||||||
* [install recommended deps](#install-recommended-deps)
|
* [install recommended deps](#install-recommended-deps)
|
||||||
* [optional gpl stuff](#optional-gpl-stuff)
|
* [optional gpl stuff](#optional-gpl-stuff)
|
||||||
* [sfx](#sfx)
|
* [sfx](#sfx) - there are two self-contained "binaries"
|
||||||
* [sfx repack](#sfx-repack)
|
* [sfx repack](#sfx-repack) - reduce the size of an sfx by removing features
|
||||||
* [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)
|
||||||
* [todo](#todo)
|
* [just the sfx](#just-the-sfx)
|
||||||
|
* [complete release](#complete-release)
|
||||||
|
* [todo](#todo) - roughly sorted by priority
|
||||||
|
* [discarded ideas](#discarded-ideas)
|
||||||
|
|
||||||
|
|
||||||
## quickstart
|
## quickstart
|
||||||
|
|
||||||
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 give everyone full access to 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](#accounts-and-volumes) etc
|
||||||
|
|
||||||
some recommended options:
|
some recommended options:
|
||||||
* `-e2dsa` enables general file indexing, see [search configuration](#search-configuration)
|
* `-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)
|
* `-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`
|
* `-v /mnt/music:/music:r:rw,foo -a foo:bar` shares `/mnt/music` as `/music`, `r`eadable by anyone, and read-write for user `foo`, password `bar`
|
||||||
* replace `:r:afoo` with `:rfoo` to only make the folder readable by `foo` and nobody else
|
* replace `:r:rw,foo` with `:r,foo` 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
|
* see [accounts and volumes](#accounts-and-volumes) for the syntax and other access levels (`r`ead, `w`rite, `m`ove, `d`elete)
|
||||||
* `--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
|
* `--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:
|
||||||
@@ -75,9 +88,24 @@ you may also want these, especially on servers:
|
|||||||
* [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to reverse-proxy behind nginx (for better https)
|
* [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to reverse-proxy behind nginx (for better https)
|
||||||
|
|
||||||
|
|
||||||
|
### on debian
|
||||||
|
|
||||||
|
recommended additional steps on debian
|
||||||
|
|
||||||
|
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:
|
general notes:
|
||||||
* paper-printing is affected by dark/light-mode! use lightmode for color, darkmode for grayscale
|
* 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
|
* because no browsers currently implement the media-query to do this properly orz
|
||||||
|
|
||||||
@@ -95,43 +123,52 @@ summary: all planned features work! now please enjoy the bloatening
|
|||||||
|
|
||||||
* backend stuff
|
* backend stuff
|
||||||
* ☑ sanic multipart parser
|
* ☑ sanic multipart parser
|
||||||
* ☑ load balancer (multiprocessing)
|
* ☑ multiprocessing (actual multithreading)
|
||||||
* ☑ volumes (mountpoints)
|
* ☑ volumes (mountpoints)
|
||||||
* ☑ accounts
|
* ☑ [accounts](#accounts-and-volumes)
|
||||||
* upload
|
* upload
|
||||||
* ☑ basic: plain multipart, ie6 support
|
* ☑ basic: plain multipart, ie6 support
|
||||||
* ☑ up2k: js, resumable, multithreaded
|
* ☑ [up2k](#uploading): js, resumable, multithreaded
|
||||||
* ☑ stash: simple PUT filedropper
|
* ☑ stash: simple PUT filedropper
|
||||||
|
* ☑ [unpost](#unpost): undo/delete accidental uploads
|
||||||
* ☑ symlink/discard existing files (content-matching)
|
* ☑ symlink/discard existing files (content-matching)
|
||||||
* download
|
* download
|
||||||
* ☑ single files in browser
|
* ☑ single files in browser
|
||||||
* ☑ folders as zip / tar files
|
* ☑ [folders as zip / tar files](#zip-downloads)
|
||||||
* ☑ FUSE client (read-only)
|
* ☑ FUSE client (read-only)
|
||||||
* browser
|
* browser
|
||||||
* ☑ tree-view
|
* ☑ navpane (directory tree sidebar)
|
||||||
* ☑ audio player
|
* ☑ file manager (cut/paste, delete, [batch-rename](#batch-rename))
|
||||||
* ☑ thumbnails
|
* ☑ audio player (with OS media controls)
|
||||||
* ☑ images using Pillow
|
* ☑ image gallery with webm player
|
||||||
* ☑ videos using FFmpeg
|
* ☑ [thumbnails](#thumbnails)
|
||||||
|
* ☑ ...of images using Pillow
|
||||||
|
* ☑ ...of videos using FFmpeg
|
||||||
* ☑ cache eviction (max-age; maybe max-size eventually)
|
* ☑ cache eviction (max-age; maybe max-size eventually)
|
||||||
* ☑ image gallery
|
|
||||||
* ☑ SPA (browse while uploading)
|
* ☑ SPA (browse while uploading)
|
||||||
* if you use the file-tree on the left only, not folders in the file list
|
* if you use the navpane to navigate, not folders in the file list
|
||||||
* server indexing
|
* server indexing
|
||||||
* ☑ locate files by contents
|
* ☑ [locate files by contents](#file-search)
|
||||||
* ☑ search by name/path/date/size
|
* ☑ search by name/path/date/size
|
||||||
* ☑ search by ID3-tags etc.
|
* ☑ [search by ID3-tags etc.](#searching)
|
||||||
* markdown
|
* markdown
|
||||||
* ☑ viewer
|
* ☑ [viewer](#markdown-viewer)
|
||||||
* ☑ editor (sure why not)
|
* ☑ editor (sure why not)
|
||||||
|
|
||||||
|
|
||||||
|
## testimonials
|
||||||
|
|
||||||
|
small collection of user feedback
|
||||||
|
|
||||||
|
`good enough`, `surprisingly correct`, `certified good software`, `just works`, `why`
|
||||||
|
|
||||||
|
|
||||||
# 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
|
||||||
* MacOS: `--th-ff-jpg` may fix thumbnails using macports-FFmpeg
|
* `--th-ff-jpg` may fix video thumbnails on some FFmpeg versions
|
||||||
|
|
||||||
## general bugs
|
## general bugs
|
||||||
|
|
||||||
@@ -141,61 +178,137 @@ summary: all planned features work! now please enjoy the bloatening
|
|||||||
|
|
||||||
## not my bugs
|
## not my bugs
|
||||||
|
|
||||||
* Windows: msys2-python 3.8.6 occasionally throws "RuntimeError: release unlocked lock" when leaving a scoped mutex in up2k
|
* Windows: folders cannot be accessed if the name ends with `.`
|
||||||
|
* python or windows bug
|
||||||
|
|
||||||
|
* Windows: msys2-python 3.8.6 occasionally throws `RuntimeError: release unlocked lock` when leaving a scoped mutex in up2k
|
||||||
* this is an msys2 bug, the regular windows edition of python is fine
|
* this is an msys2 bug, the regular windows edition of python is fine
|
||||||
|
|
||||||
|
* VirtualBox: sqlite throws `Disk I/O Error` when running in a VM and the up2k database is in a vboxsf
|
||||||
|
* use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db inside the vm instead
|
||||||
|
|
||||||
|
|
||||||
|
# accounts and volumes
|
||||||
|
|
||||||
|
per-folder, per-user permissions
|
||||||
|
* `-a usr:pwd` adds account `usr` with password `pwd`
|
||||||
|
* `-v .::r` adds current-folder `.` as the webroot, `r`eadable by anyone
|
||||||
|
* the syntax is `-v src:dst:perm:perm:...` so local-path, url-path, and one or more permissions to set
|
||||||
|
* when granting permissions to an account, the names are comma-separated: `-v .::r,usr1,usr2:rw,usr3,usr4`
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
* `r` (read): browse folder contents, download files, download as zip/tar
|
||||||
|
* `w` (write): upload files, move files *into* folder
|
||||||
|
* `m` (move): move files/folders *from* folder
|
||||||
|
* `d` (delete): delete files/folders
|
||||||
|
|
||||||
|
example:
|
||||||
|
* add accounts named u1, u2, u3 with passwords p1, p2, p3: `-a u1:p1 -a u2:p2 -a u3:p3`
|
||||||
|
* make folder `/srv` the root of the filesystem, read-only by anyone: `-v /srv::r`
|
||||||
|
* make folder `/mnt/music` available at `/music`, read-only for u1 and u2, read-write for u3: `-v /mnt/music:music:r,u1,u2:rw,u3`
|
||||||
|
* unauthorized users accessing the webroot can see that the `music` folder exists, but cannot open it
|
||||||
|
* make folder `/mnt/incoming` available at `/inc`, write-only for u1, read-move for u2: `-v /mnt/incoming:inc:w,u1:rm,u2`
|
||||||
|
* unauthorized users accessing the webroot can see that the `inc` folder exists, but cannot open it
|
||||||
|
* `u1` can open the `inc` folder, but cannot see the contents, only upload new files to it
|
||||||
|
* `u2` can browse it and move files *from* `/inc` into any folder where `u2` has write-access
|
||||||
|
|
||||||
|
|
||||||
# the browser
|
# the browser
|
||||||
|
|
||||||

|
accessing a copyparty server using a web-browser
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
## tabs
|
## tabs
|
||||||
|
|
||||||
* `[🔎]` search by size, date, path/name, mp3-tags ... see [searching](#searching)
|
the main tabs in the ui
|
||||||
* `[🚀]` and `[🎈]` are the uploaders, see [uploading](#uploading)
|
* `[🔎]` [search](#searching) by size, date, path/name, mp3-tags ...
|
||||||
* `[📂]` mkdir, create directories
|
* `[🧯]` [unpost](#unpost): undo/delete accidental uploads
|
||||||
* `[📝]` new-md, create a new markdown document
|
* `[🚀]` and `[🎈]` are the [uploaders](#uploading)
|
||||||
* `[📟]` send-msg, either to server-log or into textfiles if `--urlform save`
|
* `[📂]` mkdir: create directories
|
||||||
* `[⚙️]` client configuration options
|
* `[📝]` new-md: create a new markdown document
|
||||||
|
* `[📟]` send-msg: either to server-log or into textfiles if `--urlform save`
|
||||||
|
* `[🎺]` audio-player config options
|
||||||
|
* `[⚙️]` general client config options
|
||||||
|
|
||||||
|
|
||||||
## hotkeys
|
## hotkeys
|
||||||
|
|
||||||
the browser has the following hotkeys
|
the browser has the following hotkeys (always qwerty)
|
||||||
|
* `B` toggle breadcrumbs / navpane
|
||||||
* `I/K` prev/next folder
|
* `I/K` prev/next folder
|
||||||
* `P` parent folder
|
* `M` parent folder (or unexpand current)
|
||||||
* `G` toggle list / grid view
|
* `G` toggle list / grid view
|
||||||
* `T` toggle thumbnails / icons
|
* `T` toggle thumbnails / icons
|
||||||
|
* `ctrl-X` cut selected files/folders
|
||||||
|
* `ctrl-V` paste
|
||||||
|
* `F2` rename selected file/folder
|
||||||
|
* when a file/folder is selected (in not-grid-view):
|
||||||
|
* `Up/Down` move cursor
|
||||||
|
* shift+`Up/Down` select and move cursor
|
||||||
|
* ctrl+`Up/Down` move cursor and scroll viewport
|
||||||
|
* `Space` toggle file selection
|
||||||
|
* `Ctrl-A` toggle select all
|
||||||
* when playing audio:
|
* when playing audio:
|
||||||
* `0..9` jump to 10%..90%
|
|
||||||
* `U/O` skip 10sec back/forward
|
|
||||||
* `J/L` prev/next song
|
* `J/L` prev/next song
|
||||||
* `M` play/pause (also starts playing the folder)
|
* `U/O` skip 10sec back/forward
|
||||||
|
* `0..9` jump to 0%..90%
|
||||||
|
* `P` play/pause (also starts playing the folder)
|
||||||
|
* when viewing images / playing videos:
|
||||||
|
* `J/L, Left/Right` prev/next file
|
||||||
|
* `Home/End` first/last file
|
||||||
|
* `S` toggle selection
|
||||||
|
* `R` rotate clockwise (shift=ccw)
|
||||||
|
* `Esc` close viewer
|
||||||
|
* videos:
|
||||||
|
* `U/O` skip 10sec back/forward
|
||||||
|
* `P/K/Space` play/pause
|
||||||
|
* `F` fullscreen
|
||||||
|
* `C` continue playing next video
|
||||||
|
* `V` loop
|
||||||
|
* `M` mute
|
||||||
|
* when the navpane is open:
|
||||||
|
* `A/D` adjust tree width
|
||||||
* in the grid view:
|
* in the grid view:
|
||||||
* `S` toggle multiselect
|
* `S` toggle multiselect
|
||||||
* `A/D` zoom
|
* shift+`A/D` zoom
|
||||||
|
* in the markdown editor:
|
||||||
|
* `^s` save
|
||||||
|
* `^h` header
|
||||||
|
* `^k` autoformat table
|
||||||
|
* `^u` jump to next unicode character
|
||||||
|
* `^e` toggle editor / preview
|
||||||
|
* `^up, ^down` jump paragraphs
|
||||||
|
|
||||||
|
|
||||||
## tree-mode
|
## navpane
|
||||||
|
|
||||||
by default there's a breadcrumbs path; you can replace this with a tree-browser sidebar thing by clicking the 🌲
|
switching between breadcrumbs or navpane
|
||||||
|
|
||||||
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 the `🌲` or pressing the `B` hotkey to toggle between breadcrumbs path (default), or a navpane (tree-browser sidebar thing)
|
||||||
|
|
||||||
|
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
|
## thumbnails
|
||||||
|
|
||||||

|
press `g` to toggle image/video thumbnails instead of the file listing
|
||||||
|
|
||||||
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
|
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 dangerous your users are
|
||||||
|
|
||||||
|
images with the following names (see `--th-covers`) become the thumbnail of the folder they're in: `folder.png`, `folder.jpg`, `cover.png`, `cover.jpg`
|
||||||
|
|
||||||
|
in the grid/thumbnail view, if the audio player panel is open, songs will start playing when clicked
|
||||||
|
|
||||||
|
|
||||||
## zip downloads
|
## zip downloads
|
||||||
|
|
||||||
the `zip` link next to folders can produce various types of zip/tar files using these alternatives in the browser settings tab:
|
download folders (or file selections) as `zip` or `tar` files
|
||||||
|
|
||||||
|
select which type of archive you want in the browser settings tab:
|
||||||
|
|
||||||
| name | url-suffix | description |
|
| name | url-suffix | description |
|
||||||
|--|--|--|
|
|--|--|--|
|
||||||
@@ -205,31 +318,37 @@ the `zip` link next to folders can produce various types of zip/tar files using
|
|||||||
| `zip_crc` | `?zip=crc` | cp437 with crc32 computed early for truly ancient software |
|
| `zip_crc` | `?zip=crc` | cp437 with crc32 computed early for truly ancient software |
|
||||||
|
|
||||||
* hidden files (dotfiles) are excluded unless `-ed`
|
* hidden files (dotfiles) are excluded unless `-ed`
|
||||||
* the up2k.db is always excluded
|
* `up2k.db` and `dir.txt` is always excluded
|
||||||
* `zip_crc` will take longer to download since the server has to read each file twice
|
* `zip_crc` will take longer to download since the server has to read each file twice
|
||||||
* please let me know if you find a program old enough to actually need this
|
* this is only to support MS-DOS PKZIP v2.04g (october 1993) and older
|
||||||
|
* how are you accessing copyparty actually
|
||||||
|
|
||||||
you can also zip a selection of files or folders by clicking them in the browser, that brings up a selection editor and zip button in the bottom right
|
you can also zip a selection of files or folders by clicking them in the browser, that brings up a selection editor and zip button in the bottom right
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## uploading
|
## uploading
|
||||||
|
|
||||||
two upload methods are available in the html client:
|
web-browsers can upload using `bup` and `up2k`:
|
||||||
* `🎈 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
|
||||||
|
|
||||||
|
you can undo/delete uploads using `[🧯]` [unpost](#unpost)
|
||||||
|
|
||||||
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)
|
||||||
* files are processed in chunks, and each chunk is checksummed
|
* files are processed in chunks, and each chunk is checksummed
|
||||||
* uploads resume if they are interrupted (for example by a reboot)
|
* uploads autoresume if they are interrupted by network issues
|
||||||
|
* uploads resume if you reboot your browser or pc, just upload the same files again
|
||||||
* server detects any corruption; the client reuploads affected chunks
|
* server detects any corruption; the client reuploads affected chunks
|
||||||
* the client doesn't upload anything that already exists on the server
|
* the client doesn't upload anything that already exists on the server
|
||||||
|
* much higher speeds than ftp/scp/tarpipe on some internet connections (mainly american ones) thanks to parallel connections
|
||||||
* the last-modified timestamp of the file is preserved
|
* the last-modified timestamp of the file is preserved
|
||||||
|
|
||||||
see [up2k](#up2k) for details on how it works
|
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)
|
**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)
|
||||||
|
|
||||||
@@ -237,7 +356,7 @@ 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
|
||||||
* `[💭]` ask for confirmation before files are added to the list
|
* `[💭]` ask for confirmation before files are added to the list
|
||||||
* `[💤]` sync uploading between other copyparty tabs so only one is active
|
* `[💤]` sync uploading between other copyparty browser-tabs so only one is active
|
||||||
* `[🔎]` switch between upload and file-search mode
|
* `[🔎]` switch between upload and file-search mode
|
||||||
|
|
||||||
and then theres the tabs below it,
|
and then theres the tabs below it,
|
||||||
@@ -248,24 +367,92 @@ and then theres the tabs below it,
|
|||||||
* plus up to 3 entries each from `[done]` and `[que]` for context
|
* plus up to 3 entries each from `[done]` and `[que]` for context
|
||||||
* `[que]` is all the files that are still queued
|
* `[que]` is all the files that are still queued
|
||||||
|
|
||||||
|
|
||||||
### file-search
|
### file-search
|
||||||
|
|
||||||

|
drop files/folders into up2k to see if they exist on the server
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
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, this is no longer the case but now i've warmed up to the idea too much
|
* 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 (or just refresh the page)
|
||||||
|
|
||||||
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)
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
### unpost
|
||||||
|
|
||||||
|
undo/delete accidental uploads
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
you can unpost even if you don't have regular move/delete access, however only for files uploaded within the past `--unpost` seconds (default 12 hours) and the server must be running with `-e2d`
|
||||||
|
|
||||||
|
|
||||||
|
## file manager
|
||||||
|
|
||||||
|
cut/paste, rename, and delete files/folders (if you have permission)
|
||||||
|
|
||||||
|
you can move files across browser tabs (cut in one tab, paste in another)
|
||||||
|
|
||||||
|
|
||||||
|
## batch rename
|
||||||
|
|
||||||
|
select some files and press F2 to bring up the rename UI
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
quick explanation of the buttons,
|
||||||
|
* `[✅ apply rename]` confirms and begins renaming
|
||||||
|
* `[❌ cancel]` aborts and closes the rename window
|
||||||
|
* `[↺ reset]` reverts any filename changes back to the original name
|
||||||
|
* `[decode]` does a URL-decode on the filename, fixing stuff like `&` and `%20`
|
||||||
|
* `[advanced]` toggles advanced mode
|
||||||
|
|
||||||
|
advanced mode: rename files based on rules to decide the new names, based on the original name (regex), or based on the tags collected from the file (artist/title/...), or a mix of both
|
||||||
|
|
||||||
|
in advanced mode,
|
||||||
|
* `[case]` toggles case-sensitive regex
|
||||||
|
* `regex` is the regex pattern to apply to the original filename; any files which don't match will be skipped
|
||||||
|
* `format` is the new filename, taking values from regex capturing groups and/or from file tags
|
||||||
|
* very loosely based on foobar2000 syntax
|
||||||
|
* `presets` lets you save rename rules for later
|
||||||
|
|
||||||
|
available functions:
|
||||||
|
* `$lpad(text, length, pad_char)`
|
||||||
|
* `$rpad(text, length, pad_char)`
|
||||||
|
|
||||||
|
so,
|
||||||
|
|
||||||
|
say you have a file named [`meganeko - Eclipse - 07 Sirius A.mp3`](https://www.youtube.com/watch?v=-dtb0vDPruI) (absolutely fantastic album btw) and the tags are: `Album:Eclipse`, `Artist:meganeko`, `Title:Sirius A`, `tn:7`
|
||||||
|
|
||||||
|
you could use just regex to rename it:
|
||||||
|
* `regex` = `(.*) - (.*) - ([0-9]{2}) (.*)`
|
||||||
|
* `format` = `(3). (1) - (4)`
|
||||||
|
* `output` = `07. meganeko - Sirius A.mp3`
|
||||||
|
|
||||||
|
or you could use just tags:
|
||||||
|
* `format` = `$lpad((tn),2,0). (artist) - (title).(ext)`
|
||||||
|
* `output` = `7. meganeko - Sirius A.mp3`
|
||||||
|
|
||||||
|
or a mix of both:
|
||||||
|
* `regex` = ` - ([0-9]{2}) `
|
||||||
|
* `format` = `(1). (artist) - (title).(ext)`
|
||||||
|
* `output` = `07. meganeko - Sirius A.mp3`
|
||||||
|
|
||||||
|
the metadata keys you can use in the format field are the ones in the file-browser table header (whatever is collected with `-mte` and `-mtp`)
|
||||||
|
|
||||||
|
|
||||||
## markdown viewer
|
## markdown viewer
|
||||||
|
|
||||||
|
and there are *two* editors
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
* the document preview has a max-width which is the same as an A4 paper when printed
|
* the document preview has a max-width which is the same as an A4 paper when printed
|
||||||
@@ -275,10 +462,14 @@ 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
|
||||||
|
|
||||||
|
search by size, date, path/name, mp3-tags, ...
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
when started with `-e2dsa` copyparty will scan/index all your files. This avoids duplicates on upload, and also makes the volumes searchable through the web-ui:
|
when started with `-e2dsa` copyparty will scan/index all your files. This avoids duplicates on upload, and also makes the volumes searchable through the web-ui:
|
||||||
* make search queries by `size`/`date`/`directory-path`/`filename`, or...
|
* make search queries by `size`/`date`/`directory-path`/`filename`, or...
|
||||||
@@ -288,44 +479,90 @@ path/name queries are space-separated, AND'ed together, and words are negated wi
|
|||||||
* path: `shibayan -bossa` finds all files where one of the folders contain `shibayan` but filters out any results where `bossa` exists somewhere in the path
|
* path: `shibayan -bossa` finds all files where one of the folders contain `shibayan` but filters out any results where `bossa` exists somewhere in the path
|
||||||
* name: `demetori styx` gives you [good stuff](https://www.youtube.com/watch?v=zGh0g14ZJ8I&list=PL3A147BD151EE5218&index=9)
|
* name: `demetori styx` gives you [good stuff](https://www.youtube.com/watch?v=zGh0g14ZJ8I&list=PL3A147BD151EE5218&index=9)
|
||||||
|
|
||||||
add `-e2ts` to also scan/index tags from music files:
|
add the argument `-e2ts` to also scan/index tags from music files, which brings us over to:
|
||||||
|
|
||||||
|
|
||||||
## search configuration
|
# server config
|
||||||
|
|
||||||
searching relies on two databases, the up2k filetree (`-e2d`) and the metadata tags (`-e2t`). Configuration can be done through arguments, volume flags, or a mix of both.
|
## file indexing
|
||||||
|
|
||||||
|
file indexing relies on two databases, the up2k filetree (`-e2d`) and the metadata tags (`-e2t`). Configuration can be done through arguments, volume flags, or a mix of both.
|
||||||
|
|
||||||
through arguments:
|
through arguments:
|
||||||
* `-e2d` enables file indexing on upload
|
* `-e2d` enables file indexing on upload
|
||||||
* `-e2ds` scans writable folders on startup
|
* `-e2ds` scans writable folders for new files on startup
|
||||||
* `-e2dsa` scans all mounted volumes (including readonly ones)
|
* `-e2dsa` scans all mounted volumes (including readonly ones)
|
||||||
* `-e2t` enables metadata indexing on upload
|
* `-e2t` enables metadata indexing on upload
|
||||||
* `-e2ts` scans for tags in all files that don't have tags yet
|
* `-e2ts` scans for tags in all files that don't have tags yet
|
||||||
* `-e2tsr` deletes all existing tags, so a full reindex
|
* `-e2tsr` deletes all existing tags, does a full reindex
|
||||||
|
|
||||||
the same arguments can be set as volume flags, in addition to `d2d` and `d2t` for disabling:
|
the same arguments can be set as volume flags, in addition to `d2d` and `d2t` for disabling:
|
||||||
* `-v ~/music::r:ce2dsa:ce2tsr` does a full reindex of everything on startup
|
* `-v ~/music::r:c,e2dsa:c,e2tsr` does a full reindex of everything on startup
|
||||||
* `-v ~/music::r:cd2d` disables **all** indexing, even if any `-e2*` are on
|
* `-v ~/music::r:c,d2d` disables **all** indexing, even if any `-e2*` are on
|
||||||
* `-v ~/music::r:cd2t` disables all `-e2t*` (tags), does not affect `-e2d*`
|
* `-v ~/music::r:c,d2t` disables all `-e2t*` (tags), does not affect `-e2d*`
|
||||||
|
|
||||||
note:
|
note:
|
||||||
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those
|
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise
|
||||||
* the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher
|
* the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher
|
||||||
|
|
||||||
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:
|
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 `:c,dhash`, this has the following consequences:
|
||||||
* initial indexing is way faster, especially when the volume is on a networked disk
|
* initial indexing is way faster, especially when the volume is on a network disk
|
||||||
* makes it impossible to [file-search](#file-search)
|
* 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 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`
|
if you set `--no-hash`, you can enable hashing for specific volumes using flag `:c,ehash`
|
||||||
|
|
||||||
|
|
||||||
|
## upload rules
|
||||||
|
|
||||||
|
set upload rules using volume flags, some examples:
|
||||||
|
|
||||||
|
* `:c,sz=1k-3m` sets allowed filesize between 1 KiB and 3 MiB inclusive (suffixes: b, k, m, g)
|
||||||
|
* `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`:
|
||||||
|
* `:c,rotn=1000,2` moves uploads into subfolders, up to 1000 files in each folder before making a new one, two levels deep (must be at least 1)
|
||||||
|
* `:c,rotf=%Y/%m/%d/%H` enforces files to be uploaded into a structure of subfolders according to that date format
|
||||||
|
* if someone uploads to `/foo/bar` the path would be rewritten to `/foo/bar/2021/08/06/23` for example
|
||||||
|
* but the actual value is not verified, just the structure, so the uploader can choose any values which conform to the format string
|
||||||
|
* just to avoid additional complexity in up2k which is enough of a mess already
|
||||||
|
* `:c,lifetime=300` delete uploaded files when they become 5 minutes old
|
||||||
|
|
||||||
|
you can also set transaction limits which apply per-IP and per-volume, but these assume `-j 1` (default) otherwise the limits will be off, for example `-j 4` would allow anywhere between 1x and 4x the limits you set depending on which processing node the client gets routed to
|
||||||
|
|
||||||
|
* `:c,maxn=250,3600` allows 250 files over 1 hour from each IP (tracked per-volume)
|
||||||
|
* `:c,maxb=1g,300` allows 1 GiB total over 5 minutes from each IP (tracked per-volume)
|
||||||
|
|
||||||
|
|
||||||
|
## compress uploads
|
||||||
|
|
||||||
|
files can be autocompressed on upload
|
||||||
|
|
||||||
|
compression is either on user-request (if config allows) or forced by server-config
|
||||||
|
|
||||||
|
* volume flag `gz` allows gz compression
|
||||||
|
* volume flag `xz` allows lzma compression
|
||||||
|
* volume flag `pk` **forces** compression on all files
|
||||||
|
* url parameter `pk` requests compression with server-default algorithm
|
||||||
|
* url parameter `gz` or `xz` requests compression with a specific algorithm
|
||||||
|
* url parameter `xz` requests xz compression
|
||||||
|
|
||||||
|
things to note,
|
||||||
|
* the `gz` and `xz` arguments take a single optional argument, the compression level (range 0 to 9)
|
||||||
|
* the `pk` volume flag takes the optional argument `ALGORITHM,LEVEL` which will then be forced for all uploads, for example `gz,9` or `xz,0`
|
||||||
|
* default compression is gzip level 9
|
||||||
|
* all upload methods except up2k are supported
|
||||||
|
* the files will be indexed after compression, so dupe-detection and file-search will not work as expected
|
||||||
|
|
||||||
|
some examples,
|
||||||
|
|
||||||
|
|
||||||
## database location
|
## database location
|
||||||
|
|
||||||
|
can be stored in-volume (default) or elsewhere
|
||||||
|
|
||||||
copyparty creates a subfolder named `.hist` inside each volume where it stores the database, thumbnails, and some other stuff
|
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:
|
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)
|
* `--hist ~/.cache/copyparty -v ~/music::r:c,hist=-` sets `~/.cache/copyparty` as the default place to put volume info, but `~/music` gets the regular `.hist` subfolder (`-` restores default behavior)
|
||||||
|
|
||||||
note:
|
note:
|
||||||
* markdown edits are always stored in a local `.hist` subdirectory
|
* markdown edits are always stored in a local `.hist` subdirectory
|
||||||
@@ -335,32 +572,38 @@ note:
|
|||||||
|
|
||||||
## metadata from audio files
|
## metadata from audio files
|
||||||
|
|
||||||
|
set `-e2t` to index tags on upload
|
||||||
|
|
||||||
`-mte` decides which tags to index and display in the browser (and also the display order), this can be changed per-volume:
|
`-mte` decides which tags to index and display in the browser (and also the display order), this can be changed per-volume:
|
||||||
* `-v ~/music::r:cmte=title,artist` indexes and displays *title* followed by *artist*
|
* `-v ~/music::r:c,mte=title,artist` indexes and displays *title* followed by *artist*
|
||||||
|
|
||||||
if you add/remove a tag from `mte` you will need to run with `-e2tsr` once to rebuild the database, otherwise only new files will be affected
|
if you add/remove a tag from `mte` you will need to run with `-e2tsr` once to rebuild the database, otherwise only new files will be affected
|
||||||
|
|
||||||
|
but instead of using `-mte`, `-mth` is a better way to hide tags in the browser: these tags will not be displayed by default, but they still get indexed and become searchable, and users can choose to unhide them in the settings pane
|
||||||
|
|
||||||
`-mtm` can be used to add or redefine a metadata mapping, say you have media files with `foo` and `bar` tags and you want them to display as `qux` in the browser (preferring `foo` if both are present), then do `-mtm qux=foo,bar` and now you can `-mte artist,title,qux`
|
`-mtm` can be used to add or redefine a metadata mapping, say you have media files with `foo` and `bar` tags and you want them to display as `qux` in the browser (preferring `foo` if both are present), then do `-mtm qux=foo,bar` and now you can `-mte artist,title,qux`
|
||||||
|
|
||||||
tags that start with a `.` such as `.bpm` and `.dur`(ation) indicate numeric value
|
tags that start with a `.` such as `.bpm` and `.dur`(ation) indicate numeric value
|
||||||
|
|
||||||
see the beautiful mess of a dictionary in [mtag.py](https://github.com/9001/copyparty/blob/master/copyparty/mtag.py) for the default mappings (should cover mp3,opus,flac,m4a,wav,aif,)
|
see the beautiful mess of a dictionary in [mtag.py](https://github.com/9001/copyparty/blob/master/copyparty/mtag.py) for the default mappings (should cover mp3,opus,flac,m4a,wav,aif,)
|
||||||
|
|
||||||
`--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
|
* 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
|
||||||
|
|
||||||
|
|
||||||
## file parser plugins
|
## file parser plugins
|
||||||
|
|
||||||
copyparty can invoke external programs to collect additional metadata for files using `mtp` (as argument or volume flag), there is a default timeout of 30sec
|
provide custom parsers to index additional tags
|
||||||
|
|
||||||
|
copyparty can invoke external programs to collect additional metadata for files using `mtp` (either as argument or volume flag), there is a default timeout of 30sec
|
||||||
|
|
||||||
* `-mtp .bpm=~/bin/audio-bpm.py` will execute `~/bin/audio-bpm.py` with the audio file as argument 1 to provide the `.bpm` tag, if that does not exist in the audio metadata
|
* `-mtp .bpm=~/bin/audio-bpm.py` will execute `~/bin/audio-bpm.py` with the audio file as argument 1 to provide the `.bpm` tag, if that does not exist in the audio metadata
|
||||||
* `-mtp key=f,t5,~/bin/audio-key.py` uses `~/bin/audio-key.py` to get the `key` tag, replacing any existing metadata tag (`f,`), aborting if it takes longer than 5sec (`t5,`)
|
* `-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:c,mtp=.bpm=~/bin/audio-bpm.py:c,mtp=key=f,t5,~/bin/audio-key.py` both as a per-volume config wow this is getting ugly
|
||||||
|
|
||||||
*but wait, there's more!* `-mtp` can be used for non-audio files as well using the `a` flag: `ay` only do audio files, `an` only do non-audio files, or `ad` do all files (d as in dontcare)
|
*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)
|
||||||
|
|
||||||
@@ -376,30 +619,38 @@ copyparty can invoke external programs to collect additional metadata for files
|
|||||||
|
|
||||||
# browser support
|
# browser support
|
||||||
|
|
||||||
|
TLDR: yes
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
`ie` = internet-explorer, `ff` = firefox, `c` = chrome, `iOS` = iPhone/iPad, `Andr` = Android
|
`ie` = internet-explorer, `ff` = firefox, `c` = chrome, `iOS` = iPhone/iPad, `Andr` = Android
|
||||||
|
|
||||||
| feature | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
|
| feature | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
|
||||||
| --------------- | --- | --- | ---- | ---- | ----- | ---- | --- | ---- |
|
| --------------- | --- | ---- | ---- | ---- | ----- | ---- | --- | ---- |
|
||||||
| browse files | yep | yep | yep | yep | yep | yep | yep | yep |
|
| browse files | yep | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| basic uploader | yep | yep | yep | yep | yep | yep | yep | yep |
|
| thumbnail view | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| make directory | yep | yep | yep | yep | yep | yep | yep | yep |
|
| basic uploader | yep | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| send message | yep | yep | yep | yep | yep | yep | yep | yep |
|
| up2k | - | - | `*1` | `*1` | yep | yep | yep | yep |
|
||||||
| set sort order | - | yep | yep | yep | yep | yep | yep | yep |
|
| make directory | yep | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| zip selection | - | yep | yep | yep | yep | yep | yep | yep |
|
| send message | yep | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| directory tree | - | - | `*1` | yep | yep | yep | yep | yep |
|
| set sort order | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| up2k | - | - | yep | yep | yep | yep | yep | yep |
|
| zip selection | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| icons work | - | - | yep | yep | yep | yep | yep | yep |
|
| file rename | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| markdown editor | - | - | yep | yep | yep | yep | yep | yep |
|
| file cut/paste | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| markdown viewer | - | - | yep | yep | yep | yep | yep | yep |
|
| navpane | - | `*2` | yep | yep | yep | yep | yep | yep |
|
||||||
| play mp3/m4a | - | yep | yep | yep | yep | yep | yep | yep |
|
| image viewer | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| play ogg/opus | - | - | - | - | yep | yep | `*2` | yep |
|
| video player | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
|
| markdown editor | - | - | yep | yep | yep | yep | yep | yep |
|
||||||
|
| markdown viewer | - | - | yep | yep | yep | yep | yep | yep |
|
||||||
|
| play mp3/m4a | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
|
| play ogg/opus | - | - | - | - | yep | yep | `*3` | yep |
|
||||||
|
| **= feature =** | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
|
||||||
|
|
||||||
* internet explorer 6 to 8 behave the same
|
* internet explorer 6 to 8 behave the same
|
||||||
* firefox 52 and chrome 49 are the last winxp versions
|
* firefox 52 and chrome 49 are the last winxp versions
|
||||||
* `*1` only public folders (login session is dropped) and no history / back-button
|
* `*1` yes, but extremely slow (ie10: 1 MiB/s, ie11: 270 KiB/s)
|
||||||
* `*2` using a wasm decoder which can sometimes get stuck and consumes a bit more power
|
* `*2` causes a full-page refresh on each navigation
|
||||||
|
* `*3` 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:
|
||||||
|
|
||||||
@@ -411,22 +662,25 @@ quick summary of more eccentric web-browsers trying to view a directory index:
|
|||||||
| **lynx** (2.8.9/macports) | can browse, login, upload/mkdir/msg |
|
| **lynx** (2.8.9/macports) | can browse, login, upload/mkdir/msg |
|
||||||
| **w3m** (0.5.3/macports) | can browse, login, upload at 100kB/s, mkdir/msg |
|
| **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) |
|
| **netsurf** (3.10/arch) | is basically ie6 with much better css (javascript has almost no effect) |
|
||||||
|
| **opera** (11.60/winxp) | OK: thumbnails, image-viewer, zip-selection, rename/cut/paste. NG: up2k, navpane, markdown, audio |
|
||||||
| **ie4** and **netscape** 4.0 | can browse (text is yellow on white), upload with `?b=u` |
|
| **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 |
|
| **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl |
|
||||||
|
|
||||||
|
|
||||||
# client examples
|
# client examples
|
||||||
|
|
||||||
|
interact with copyparty using non-browser clients
|
||||||
|
|
||||||
* javascript: dump some state into a file (two separate examples)
|
* javascript: dump some state into a file (two separate examples)
|
||||||
* `await fetch('https://127.0.0.1:3923/', {method:"PUT", body: JSON.stringify(foo)});`
|
* `await fetch('https://127.0.0.1:3923/', {method:"PUT", body: JSON.stringify(foo)});`
|
||||||
* `var xhr = new XMLHttpRequest(); xhr.open('POST', 'https://127.0.0.1:3923/msgs?raw'); xhr.send('foo');`
|
* `var xhr = new XMLHttpRequest(); xhr.open('POST', 'https://127.0.0.1:3923/msgs?raw'); xhr.send('foo');`
|
||||||
|
|
||||||
* curl/wget: upload some files (post=file, chunk=stdin)
|
* curl/wget: upload some files (post=file, chunk=stdin)
|
||||||
* `post(){ curl -b cppwd=wark http://127.0.0.1:3923/ -F act=bput -F f=@"$1";}`
|
* `post(){ curl -b cppwd=wark -F act=bput -F f=@"$1" http://127.0.0.1:3923/;}`
|
||||||
`post movie.mkv`
|
`post movie.mkv`
|
||||||
* `post(){ wget --header='Cookie: cppwd=wark' http://127.0.0.1:3923/?raw --post-file="$1" -O-;}`
|
* `post(){ wget --header='Cookie: cppwd=wark' --post-file="$1" -O- http://127.0.0.1:3923/?raw;}`
|
||||||
`post movie.mkv`
|
`post movie.mkv`
|
||||||
* `chunk(){ curl -b cppwd=wark http://127.0.0.1:3923/ -T-;}`
|
* `chunk(){ curl -b cppwd=wark -T- http://127.0.0.1:3923/;}`
|
||||||
`chunk <movie.mkv`
|
`chunk <movie.mkv`
|
||||||
|
|
||||||
* FUSE: mount a copyparty server as a local filesystem
|
* FUSE: mount a copyparty server as a local filesystem
|
||||||
@@ -437,7 +691,7 @@ quick summary of more eccentric web-browsers trying to view a directory index:
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@@ -457,27 +711,49 @@ quick outline of the up2k protocol, see [uploading](#uploading) for the web-clie
|
|||||||
* client does another handshake with the hashlist; server replies with OK or a list of chunks to reupload
|
* client does another handshake with the hashlist; server replies with OK or a list of chunks to reupload
|
||||||
|
|
||||||
|
|
||||||
|
# performance
|
||||||
|
|
||||||
|
defaults are good for most cases
|
||||||
|
|
||||||
|
you can ignore the `cannot efficiently use multiple CPU cores` message, it's very unlikely to be a problem
|
||||||
|
|
||||||
|
below are some tweaks roughly ordered by usefulness:
|
||||||
|
|
||||||
|
* `-q` disables logging and can help a bunch, even when combined with `-lo` to redirect logs to file
|
||||||
|
* `--http-only` or `--https-only` (unless you want to support both protocols) will reduce the delay before a new connection is established
|
||||||
|
* `--hist` pointing to a fast location (ssd) will make directory listings and searches faster when `-e2d` or `-e2t` is set
|
||||||
|
* `--no-hash` when indexing a network-disk if you don't care about the actual filehashes and only want the names/tags searchable
|
||||||
|
* `-j` enables multiprocessing (actual multithreading) and can make copyparty perform better in cpu-intensive workloads, for example:
|
||||||
|
* huge amount of short-lived connections
|
||||||
|
* really heavy traffic (downloads/uploads)
|
||||||
|
|
||||||
|
...however it adds an overhead to internal communication so it might be a net loss, see if it works 4 u
|
||||||
|
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
|
|
||||||
|
mandatory deps:
|
||||||
* `jinja2` (is built into the SFX)
|
* `jinja2` (is built into the SFX)
|
||||||
|
|
||||||
|
|
||||||
## optional dependencies
|
## optional dependencies
|
||||||
|
|
||||||
|
install these to enable bonus features
|
||||||
|
|
||||||
enable music tags:
|
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)
|
||||||
|
|
||||||
enable image thumbnails:
|
enable thumbnails of images:
|
||||||
* `Pillow` (requires py2.7 or py3.5+)
|
* `Pillow` (requires py2.7 or py3.5+)
|
||||||
|
|
||||||
enable video thumbnails:
|
enable thumbnails of videos:
|
||||||
* `ffmpeg` and `ffprobe` somewhere in `$PATH`
|
* `ffmpeg` and `ffprobe` somewhere in `$PATH`
|
||||||
|
|
||||||
enable reading HEIF pictures:
|
enable thumbnails of HEIF pictures:
|
||||||
* `pyheif-pillow-opener` (requires Linux or a C compiler)
|
* `pyheif-pillow-opener` (requires Linux or a C compiler)
|
||||||
|
|
||||||
enable reading AVIF pictures:
|
enable thumbnails of AVIF pictures:
|
||||||
* `pillow-avif-plugin`
|
* `pillow-avif-plugin`
|
||||||
|
|
||||||
|
|
||||||
@@ -491,12 +767,12 @@ python -m pip install --user -U jinja2 mutagen Pillow
|
|||||||
|
|
||||||
some bundled tools have copyleft dependencies, see [./bin/#mtag](bin/#mtag)
|
some bundled tools have copyleft dependencies, see [./bin/#mtag](bin/#mtag)
|
||||||
|
|
||||||
these are standalone programs and will never be imported / evaluated by copyparty
|
these are standalone programs and will never be imported / evaluated by copyparty, and must be enabled through `-mtp` configs
|
||||||
|
|
||||||
|
|
||||||
# sfx
|
# sfx
|
||||||
|
|
||||||
currently there are two self-contained "binaries":
|
there are two self-contained "binaries":
|
||||||
* [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) -- pure python, works everywhere, **recommended**
|
* [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, kinda deprecated
|
* [copyparty-sfx.sh](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.sh) -- smaller, but only for linux and macos, kinda deprecated
|
||||||
|
|
||||||
@@ -507,10 +783,12 @@ pls note that `copyparty-sfx.sh` will fail if you rename `copyparty-sfx.py` to `
|
|||||||
|
|
||||||
## sfx repack
|
## sfx repack
|
||||||
|
|
||||||
if you don't need all the features you can repack the sfx and save a bunch of space; all you need is an sfx and a copy of this repo (nothing else to download or build, except for either msys2 or WSL if you're on windows)
|
reduce the size of an sfx by removing features
|
||||||
* `724K` original size as of v0.4.0
|
|
||||||
* `256K` after `./scripts/make-sfx.sh re no-ogv`
|
if you don't need all the features, you can repack the sfx and save a bunch of space; all you need is an sfx and a copy of this repo (nothing else to download or build, except if you're on windows then you need msys2 or WSL)
|
||||||
* `164K` after `./scripts/make-sfx.sh re no-ogv no-cm`
|
* `525k` size of original sfx.py as of v0.11.30
|
||||||
|
* `315k` after `./scripts/make-sfx.sh re no-ogv`
|
||||||
|
* `223k` after `./scripts/make-sfx.sh re no-ogv no-cm`
|
||||||
|
|
||||||
the features you can opt to drop are
|
the features you can opt to drop are
|
||||||
* `ogv`.js, the opus/vorbis decoder which is needed by apple devices to play foss audio files
|
* `ogv`.js, the opus/vorbis decoder which is needed by apple devices to play foss audio files
|
||||||
@@ -532,18 +810,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
|
||||||
|
|
||||||
|
grab the web-dependencies from a previous sfx (unless you need to modify something in those):
|
||||||
|
|
||||||
|
```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/deps/
|
||||||
|
```
|
||||||
|
|
||||||
|
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 skip the sfx section above
|
||||||
|
|
||||||
in the `scripts` folder:
|
in the `scripts` folder:
|
||||||
|
|
||||||
@@ -558,14 +863,18 @@ in the `scripts` folder:
|
|||||||
|
|
||||||
roughly sorted by priority
|
roughly sorted by priority
|
||||||
|
|
||||||
|
* hls framework for Someone Else to drop code into :^)
|
||||||
* readme.md as epilogue
|
* readme.md as epilogue
|
||||||
* single sha512 across all up2k chunks? maybe
|
|
||||||
|
|
||||||
|
## discarded ideas
|
||||||
|
|
||||||
* 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
|
||||||
|
* not worth the effort, just throw enough conncetions at it
|
||||||
discarded ideas
|
* single sha512 across all up2k chunks?
|
||||||
|
* crypto.subtle cannot into streaming, would have to use hashwasm, expensive
|
||||||
* separate sqlite table per tag
|
* separate sqlite table per tag
|
||||||
* performance fixed by skipping some indexes (`+mt.k`)
|
* performance fixed by skipping some indexes (`+mt.k`)
|
||||||
* audio fingerprinting
|
* audio fingerprinting
|
||||||
@@ -580,3 +889,6 @@ discarded ideas
|
|||||||
* nah
|
* nah
|
||||||
* look into android thumbnail cache file format
|
* look into android thumbnail cache file format
|
||||||
* absolutely not
|
* absolutely not
|
||||||
|
* indexedDB for hashes, cfg enable/clear/sz, 2gb avail, ~9k for 1g, ~4k for 100m, 500k items before autoeviction
|
||||||
|
* blank hashlist when up-ok to skip handshake
|
||||||
|
* too many confusing side-effects
|
||||||
|
|||||||
@@ -48,15 +48,21 @@ you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releas
|
|||||||
|
|
||||||
|
|
||||||
# [`dbtool.py`](dbtool.py)
|
# [`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 recommends to wipe the DB and reindex because it now collects additional metadata during analysis, but you have some really expensive `-mtp` parsers and want to copy over the tags from the old db
|
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.0), first move the old db aside, launch copyparty, let it rebuild the db until the point where it starts running mtp (colored messages as it adds the mtp tags), then CTRL-C and patch in the old mtp tags from the old db instead
|
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`:
|
so assuming you have `-mtp` parsers to provide the tags `key` and `.bpm`:
|
||||||
|
|
||||||
```
|
```
|
||||||
~/bin/dbtool.py -ls up2k.db
|
cd /mnt/nas/music/.hist
|
||||||
~/bin/dbtool.py -src up2k.db.v0.10.22 up2k.db -cmp
|
~/src/copyparty/bin/dbtool.py -ls up2k.db
|
||||||
~/bin/dbtool.py -src up2k.db.v0.10.22 up2k.db -rm-mtp-flag -copy key
|
~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -cmp
|
||||||
~/bin/dbtool.py -src up2k.db.v0.10.22 up2k.db -rm-mtp-flag -copy .bpm -vac
|
~/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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# [`prisonparty.sh`](prisonparty.sh)
|
||||||
|
* run copyparty in a chroot, preventing any accidental file access
|
||||||
|
* creates bindmounts for /bin, /lib, and so on, see `sysdirs=`
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ class Gateway(object):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def sendreq(self, *args, headers={}, **kwargs):
|
def sendreq(self, meth, path, headers, **kwargs):
|
||||||
if self.password:
|
if self.password:
|
||||||
headers["Cookie"] = "=".join(["cppwd", self.password])
|
headers["Cookie"] = "=".join(["cppwd", self.password])
|
||||||
|
|
||||||
@@ -354,21 +354,21 @@ class Gateway(object):
|
|||||||
if c.rx_path:
|
if c.rx_path:
|
||||||
raise Exception()
|
raise Exception()
|
||||||
|
|
||||||
c.request(*list(args), headers=headers, **kwargs)
|
c.request(meth, path, headers=headers, **kwargs)
|
||||||
c.rx = c.getresponse()
|
c.rx = c.getresponse()
|
||||||
return c
|
return c
|
||||||
except:
|
except:
|
||||||
tid = threading.current_thread().ident
|
tid = threading.current_thread().ident
|
||||||
dbg(
|
dbg(
|
||||||
"\033[1;37;44mbad conn {:x}\n {}\n {}\033[0m".format(
|
"\033[1;37;44mbad conn {:x}\n {} {}\n {}\033[0m".format(
|
||||||
tid, " ".join(str(x) for x in args), c.rx_path if c else "(null)"
|
tid, meth, path, c.rx_path if c else "(null)"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.closeconn(c)
|
self.closeconn(c)
|
||||||
c = self.getconn()
|
c = self.getconn()
|
||||||
try:
|
try:
|
||||||
c.request(*list(args), headers=headers, **kwargs)
|
c.request(meth, path, headers=headers, **kwargs)
|
||||||
c.rx = c.getresponse()
|
c.rx = c.getresponse()
|
||||||
return c
|
return c
|
||||||
except:
|
except:
|
||||||
@@ -386,7 +386,7 @@ class Gateway(object):
|
|||||||
path = dewin(path)
|
path = dewin(path)
|
||||||
|
|
||||||
web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots"
|
web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots"
|
||||||
c = self.sendreq("GET", web_path)
|
c = self.sendreq("GET", web_path, {})
|
||||||
if c.rx.status != 200:
|
if c.rx.status != 200:
|
||||||
self.closeconn(c)
|
self.closeconn(c)
|
||||||
log(
|
log(
|
||||||
@@ -440,7 +440,7 @@ class Gateway(object):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
c = self.sendreq("GET", web_path, headers={"Range": hdr_range})
|
c = self.sendreq("GET", web_path, {"Range": hdr_range})
|
||||||
if c.rx.status != http.client.PARTIAL_CONTENT:
|
if c.rx.status != http.client.PARTIAL_CONTENT:
|
||||||
self.closeconn(c)
|
self.closeconn(c)
|
||||||
raise Exception(
|
raise Exception(
|
||||||
|
|||||||
@@ -54,10 +54,13 @@ MACOS = platform.system() == "Darwin"
|
|||||||
info = log = dbg = None
|
info = log = dbg = None
|
||||||
|
|
||||||
|
|
||||||
print("{} v{} @ {}".format(
|
print(
|
||||||
platform.python_implementation(),
|
"{} v{} @ {}".format(
|
||||||
".".join([str(x) for x in sys.version_info]),
|
platform.python_implementation(),
|
||||||
sys.executable))
|
".".join([str(x) for x in sys.version_info]),
|
||||||
|
sys.executable,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -299,14 +302,14 @@ class Gateway(object):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def sendreq(self, *args, headers={}, **kwargs):
|
def sendreq(self, meth, path, headers, **kwargs):
|
||||||
tid = get_tid()
|
tid = get_tid()
|
||||||
if self.password:
|
if self.password:
|
||||||
headers["Cookie"] = "=".join(["cppwd", self.password])
|
headers["Cookie"] = "=".join(["cppwd", self.password])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
c = self.getconn(tid)
|
c = self.getconn(tid)
|
||||||
c.request(*list(args), headers=headers, **kwargs)
|
c.request(meth, path, headers=headers, **kwargs)
|
||||||
return c.getresponse()
|
return c.getresponse()
|
||||||
except:
|
except:
|
||||||
dbg("bad conn")
|
dbg("bad conn")
|
||||||
@@ -314,7 +317,7 @@ class Gateway(object):
|
|||||||
self.closeconn(tid)
|
self.closeconn(tid)
|
||||||
try:
|
try:
|
||||||
c = self.getconn(tid)
|
c = self.getconn(tid)
|
||||||
c.request(*list(args), headers=headers, **kwargs)
|
c.request(meth, path, headers=headers, **kwargs)
|
||||||
return c.getresponse()
|
return c.getresponse()
|
||||||
except:
|
except:
|
||||||
info("http connection failed:\n" + traceback.format_exc())
|
info("http connection failed:\n" + traceback.format_exc())
|
||||||
@@ -331,7 +334,7 @@ class Gateway(object):
|
|||||||
path = dewin(path)
|
path = dewin(path)
|
||||||
|
|
||||||
web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots&ls"
|
web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots&ls"
|
||||||
r = self.sendreq("GET", web_path)
|
r = self.sendreq("GET", web_path, {})
|
||||||
if r.status != 200:
|
if r.status != 200:
|
||||||
self.closeconn()
|
self.closeconn()
|
||||||
log(
|
log(
|
||||||
@@ -368,7 +371,7 @@ class Gateway(object):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
r = self.sendreq("GET", web_path, headers={"Range": hdr_range})
|
r = self.sendreq("GET", web_path, {"Range": hdr_range})
|
||||||
if r.status != http.client.PARTIAL_CONTENT:
|
if r.status != http.client.PARTIAL_CONTENT:
|
||||||
self.closeconn()
|
self.closeconn()
|
||||||
raise Exception(
|
raise Exception(
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
import shutil
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
DB_VER = 3
|
DB_VER1 = 3
|
||||||
|
DB_VER2 = 4
|
||||||
|
|
||||||
|
|
||||||
def die(msg):
|
def die(msg):
|
||||||
@@ -45,18 +48,21 @@ def compare(n1, d1, n2, d2, verbose):
|
|||||||
nt = next(d1.execute("select count(w) from up"))[0]
|
nt = next(d1.execute("select count(w) from up"))[0]
|
||||||
n = 0
|
n = 0
|
||||||
miss = 0
|
miss = 0
|
||||||
for w, rd, fn in d1.execute("select w, rd, fn from up"):
|
for w1, rd, fn in d1.execute("select w, rd, fn from up"):
|
||||||
n += 1
|
n += 1
|
||||||
if n % 25_000 == 0:
|
if n % 25_000 == 0:
|
||||||
m = f"\033[36mchecked {n:,} of {nt:,} files in {n1} against {n2}\033[0m"
|
m = f"\033[36mchecked {n:,} of {nt:,} files in {n1} against {n2}\033[0m"
|
||||||
print(m)
|
print(m)
|
||||||
|
|
||||||
q = "select w from up where substr(w,1,16) = ?"
|
if rd.split("/", 1)[0] == ".hist":
|
||||||
hit = d2.execute(q, (w[:16],)).fetchone()
|
continue
|
||||||
|
|
||||||
|
q = "select w from up where rd = ? and fn = ?"
|
||||||
|
hit = d2.execute(q, (rd, fn)).fetchone()
|
||||||
if not hit:
|
if not hit:
|
||||||
miss += 1
|
miss += 1
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f"file in {n1} missing in {n2}: [{w}] {rd}/{fn}")
|
print(f"file in {n1} missing in {n2}: [{w1}] {rd}/{fn}")
|
||||||
|
|
||||||
print(f" {miss} files in {n1} missing in {n2}\n")
|
print(f" {miss} files in {n1} missing in {n2}\n")
|
||||||
|
|
||||||
@@ -64,15 +70,30 @@ def compare(n1, d1, n2, d2, verbose):
|
|||||||
n = 0
|
n = 0
|
||||||
miss = {}
|
miss = {}
|
||||||
nmiss = 0
|
nmiss = 0
|
||||||
for w, k, v in d1.execute("select * from mt"):
|
for w1, k, v in d1.execute("select * from mt"):
|
||||||
|
|
||||||
n += 1
|
n += 1
|
||||||
if n % 100_000 == 0:
|
if n % 100_000 == 0:
|
||||||
m = f"\033[36mchecked {n:,} of {nt:,} tags in {n1} against {n2}, so far {nmiss} missing tags\033[0m"
|
m = f"\033[36mchecked {n:,} of {nt:,} tags in {n1} against {n2}, so far {nmiss} missing tags\033[0m"
|
||||||
print(m)
|
print(m)
|
||||||
|
|
||||||
v2 = d2.execute("select v from mt where w = ? and +k = ?", (w, k)).fetchone()
|
q = "select rd, fn from up where substr(w,1,16) = ?"
|
||||||
if v2:
|
rd, fn = d1.execute(q, (w1,)).fetchone()
|
||||||
v2 = v2[0]
|
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":
|
# if v != v2 and v2 and k in [".bpm", "key"] and n2 == "src":
|
||||||
# print(f"{w} [{rd}/{fn}] {k} = [{v}] / [{v2}]")
|
# print(f"{w} [{rd}/{fn}] {k} = [{v}] / [{v2}]")
|
||||||
@@ -99,9 +120,7 @@ def compare(n1, d1, n2, d2, verbose):
|
|||||||
miss[k] = 1
|
miss[k] = 1
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
q = "select rd, fn from up where substr(w,1,16) = ?"
|
print(f"missing in {n2}: [{w1}] [{rd}/{fn}] {k} = {v}")
|
||||||
rd, fn = d1.execute(q, (w,)).fetchone()
|
|
||||||
print(f"missing in {n2}: [{w}] [{rd}/{fn}] {k} = {v}")
|
|
||||||
|
|
||||||
for k, v in sorted(miss.items()):
|
for k, v in sorted(miss.items()):
|
||||||
if v:
|
if v:
|
||||||
@@ -114,24 +133,35 @@ def copy_mtp(d1, d2, tag, rm):
|
|||||||
nt = next(d1.execute("select count(w) from mt where k = ?", (tag,)))[0]
|
nt = next(d1.execute("select count(w) from mt where k = ?", (tag,)))[0]
|
||||||
n = 0
|
n = 0
|
||||||
ndone = 0
|
ndone = 0
|
||||||
for w, k, v in d1.execute("select * from mt where k = ?", (tag,)):
|
for w1, k, v in d1.execute("select * from mt where k = ?", (tag,)):
|
||||||
n += 1
|
n += 1
|
||||||
if n % 25_000 == 0:
|
if n % 25_000 == 0:
|
||||||
m = f"\033[36m{n:,} of {nt:,} tags checked, so far {ndone} copied\033[0m"
|
m = f"\033[36m{n:,} of {nt:,} tags checked, so far {ndone} copied\033[0m"
|
||||||
print(m)
|
print(m)
|
||||||
|
|
||||||
hit = d2.execute("select v from mt where w = ? and +k = ?", (w, k)).fetchone()
|
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:
|
if hit:
|
||||||
hit = hit[0]
|
hit = hit[0]
|
||||||
|
|
||||||
if hit != v:
|
if hit != v:
|
||||||
ndone += 1
|
ndone += 1
|
||||||
if hit is not None:
|
if hit is not None:
|
||||||
d2.execute("delete from mt where w = ? and +k = ?", (w, k))
|
d2.execute("delete from mt where w = ? and +k = ?", (w2, k))
|
||||||
|
|
||||||
d2.execute("insert into mt values (?,?,?)", (w, k, v))
|
d2.execute("insert into mt values (?,?,?)", (w2, k, v))
|
||||||
if rm:
|
if rm:
|
||||||
d2.execute("delete from mt where w = ? and +k = 't:mtp'", (w,))
|
d2.execute("delete from mt where w = ? and +k = 't:mtp'", (w2,))
|
||||||
|
|
||||||
d2.commit()
|
d2.commit()
|
||||||
print(f"copied {ndone} {tag} tags over")
|
print(f"copied {ndone} {tag} tags over")
|
||||||
@@ -140,7 +170,7 @@ def copy_mtp(d1, d2, tag, rm):
|
|||||||
def main():
|
def main():
|
||||||
os.system("")
|
os.system("")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
ap = argparse.ArgumentParser()
|
ap = argparse.ArgumentParser()
|
||||||
ap.add_argument("db", help="database to work on")
|
ap.add_argument("db", help="database to work on")
|
||||||
ap.add_argument("-src", metavar="DB", type=str, help="database to copy from")
|
ap.add_argument("-src", metavar="DB", type=str, help="database to copy from")
|
||||||
@@ -168,6 +198,23 @@ def main():
|
|||||||
db = sqlite3.connect(ar.db)
|
db = sqlite3.connect(ar.db)
|
||||||
ds = sqlite3.connect(ar.src) if ar.src else None
|
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"]]:
|
for d, n in [[ds, "src"], [db, "dst"]]:
|
||||||
if not d:
|
if not d:
|
||||||
continue
|
continue
|
||||||
@@ -176,8 +223,8 @@ def main():
|
|||||||
if ver == "corrupt":
|
if ver == "corrupt":
|
||||||
die("{} database appears to be corrupt, sorry")
|
die("{} database appears to be corrupt, sorry")
|
||||||
|
|
||||||
if ver != DB_VER:
|
if ver < DB_VER1 or ver > DB_VER2:
|
||||||
m = f"{n} db is version {ver}, this tool only supports version {DB_VER}, please upgrade it with copyparty first"
|
m = f"{n} db is version {ver}, this tool only supports versions between {DB_VER1} and {DB_VER2}, please upgrade it with copyparty first"
|
||||||
die(m)
|
die(m)
|
||||||
|
|
||||||
if ar.ls:
|
if ar.ls:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ some of these rely on libraries which are not MIT-compatible
|
|||||||
|
|
||||||
* [audio-bpm.py](./audio-bpm.py) detects the BPM of music using the BeatRoot Vamp Plugin; imports GPL2
|
* [audio-bpm.py](./audio-bpm.py) detects the BPM of music using the BeatRoot Vamp Plugin; imports GPL2
|
||||||
* [audio-key.py](./audio-key.py) detects the melodic key of music using the Mixxx fork of keyfinder; imports GPL3
|
* [audio-key.py](./audio-key.py) detects the melodic key of music using the Mixxx fork of keyfinder; imports GPL3
|
||||||
|
* [media-hash.py](./media-hash.py) generates checksums for audio and video streams; uses FFmpeg (LGPL or GPL)
|
||||||
|
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
@@ -18,7 +19,10 @@ run [`install-deps.sh`](install-deps.sh) to build/install most dependencies requ
|
|||||||
|
|
||||||
# usage from copyparty
|
# usage from copyparty
|
||||||
|
|
||||||
`copyparty -e2dsa -e2ts -mtp key=f,audio-key.py -mtp .bpm=f,audio-bpm.py`
|
`copyparty -e2dsa -e2ts` followed by any combination of these:
|
||||||
|
* `-mtp key=f,audio-key.py`
|
||||||
|
* `-mtp .bpm=f,audio-bpm.py`
|
||||||
|
* `-mtp ahash,vhash=f,media-hash.py`
|
||||||
|
|
||||||
* `f,` makes the detected value replace any existing values
|
* `f,` makes the detected value replace any existing values
|
||||||
* the `.` in `.bpm` indicates numeric value
|
* the `.` in `.bpm` indicates numeric value
|
||||||
@@ -29,6 +33,9 @@ run [`install-deps.sh`](install-deps.sh) to build/install most dependencies requ
|
|||||||
## usage with volume-flags
|
## usage with volume-flags
|
||||||
|
|
||||||
instead of affecting all volumes, you can set the options for just one volume like so:
|
instead of affecting all volumes, you can set the options for just one volume like so:
|
||||||
```
|
|
||||||
copyparty -v /mnt/nas/music:/music:r:cmtp=key=f,audio-key.py:cmtp=.bpm=f,audio-bpm.py:ce2dsa:ce2ts
|
`copyparty -v /mnt/nas/music:/music:r:c,e2dsa:c,e2ts` immediately followed by any combination of these:
|
||||||
```
|
|
||||||
|
* `:c,mtp=key=f,audio-key.py`
|
||||||
|
* `:c,mtp=.bpm=f,audio-bpm.py`
|
||||||
|
* `:c,mtp=ahash,vhash=f,media-hash.py`
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
73
bin/mtag/media-hash.py
Normal file
73
bin/mtag/media-hash.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import subprocess as sp
|
||||||
|
|
||||||
|
try:
|
||||||
|
from copyparty.util import fsenc
|
||||||
|
except:
|
||||||
|
|
||||||
|
def fsenc(p):
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
dep: ffmpeg
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def det():
|
||||||
|
# fmt: off
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-nostdin",
|
||||||
|
"-hide_banner",
|
||||||
|
"-v", "fatal",
|
||||||
|
"-i", fsenc(sys.argv[1]),
|
||||||
|
"-f", "framemd5",
|
||||||
|
"-"
|
||||||
|
]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
p = sp.Popen(cmd, stdout=sp.PIPE)
|
||||||
|
# ps = io.TextIOWrapper(p.stdout, encoding="utf-8")
|
||||||
|
ps = p.stdout
|
||||||
|
|
||||||
|
chans = {}
|
||||||
|
for ln in ps:
|
||||||
|
if ln.startswith(b"#stream#"):
|
||||||
|
break
|
||||||
|
|
||||||
|
m = re.match(r"^#media_type ([0-9]): ([a-zA-Z])", ln.decode("utf-8"))
|
||||||
|
if m:
|
||||||
|
chans[m.group(1)] = m.group(2)
|
||||||
|
|
||||||
|
hashers = [hashlib.sha512(), hashlib.sha512()]
|
||||||
|
for ln in ps:
|
||||||
|
n = int(ln[:1])
|
||||||
|
v = ln.rsplit(b",", 1)[-1].strip()
|
||||||
|
hashers[n].update(v)
|
||||||
|
|
||||||
|
r = {}
|
||||||
|
for k, v in chans.items():
|
||||||
|
dg = hashers[int(k)].digest()[:12]
|
||||||
|
dg = base64.urlsafe_b64encode(dg).decode("ascii")
|
||||||
|
r[v[0].lower() + "hash"] = dg
|
||||||
|
|
||||||
|
print(json.dumps(r, indent=4))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
det()
|
||||||
|
except:
|
||||||
|
pass # mute
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
39
bin/mtag/res/yt-ipr.conf
Normal file
39
bin/mtag/res/yt-ipr.conf
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# example config file to use copyparty as a youtube manifest collector,
|
||||||
|
# use with copyparty like: python copyparty.py -c yt-ipr.conf
|
||||||
|
#
|
||||||
|
# see docs/example.conf for a better explanation of the syntax, but
|
||||||
|
# newlines are block separators, so adding blank lines inside a volume definition is bad
|
||||||
|
# (use comments as separators instead)
|
||||||
|
|
||||||
|
|
||||||
|
# create user ed, password wark
|
||||||
|
u ed:wark
|
||||||
|
|
||||||
|
|
||||||
|
# create a volume at /ytm which stores files at ./srv/ytm
|
||||||
|
./srv/ytm
|
||||||
|
/ytm
|
||||||
|
# write-only, but read-write for user ed
|
||||||
|
w
|
||||||
|
rw ed
|
||||||
|
# rescan the volume on startup
|
||||||
|
c e2dsa
|
||||||
|
# collect tags from all new files since last scan
|
||||||
|
c e2ts
|
||||||
|
# optionally enable compression to make the files 50% smaller
|
||||||
|
c pk
|
||||||
|
# only allow uploads which are between 16k and 1m large
|
||||||
|
c sz=16k-1m
|
||||||
|
# allow up to 10 uploads over 5 minutes from each ip
|
||||||
|
c maxn=10,300
|
||||||
|
# move uploads into subfolders: YEAR-MONTH / DAY-HOUR / <upload>
|
||||||
|
c rotf=%Y-%m/%d-%H
|
||||||
|
# delete uploads when they are 24 hours old
|
||||||
|
c lifetime=86400
|
||||||
|
# add the parser and tell copyparty what tags it can expect from it
|
||||||
|
c mtp=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires=bin/mtag/yt-ipr.py
|
||||||
|
# decide which tags we want to index and in what order
|
||||||
|
c mte=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires
|
||||||
|
|
||||||
|
|
||||||
|
# create any other volumes you'd like down here, or merge this with an existing config file
|
||||||
47
bin/mtag/res/yt-ipr.user.js
Normal file
47
bin/mtag/res/yt-ipr.user.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name youtube-playerdata-hub
|
||||||
|
// @match https://youtube.com/*
|
||||||
|
// @match https://*.youtube.com/*
|
||||||
|
// @version 1.0
|
||||||
|
// @grant GM_addStyle
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
var server = 'https://127.0.0.1:3923/ytm?pw=wark',
|
||||||
|
interval = 60; // sec
|
||||||
|
|
||||||
|
var sent = {};
|
||||||
|
function send(txt, mf_url, desc) {
|
||||||
|
if (sent[mf_url])
|
||||||
|
return;
|
||||||
|
|
||||||
|
fetch(server + '&_=' + Date.now(), { method: "PUT", body: txt });
|
||||||
|
console.log('[yt-pdh] yeet %d bytes, %s', txt.length, desc);
|
||||||
|
sent[mf_url] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collect() {
|
||||||
|
try {
|
||||||
|
var pd = document.querySelector('ytd-watch-flexy');
|
||||||
|
if (!pd)
|
||||||
|
return console.log('[yt-pdh] no video found');
|
||||||
|
|
||||||
|
pd = pd.playerData;
|
||||||
|
var mu = pd.streamingData.dashManifestUrl || pd.streamingData.hlsManifestUrl;
|
||||||
|
if (!mu || !mu.length)
|
||||||
|
return console.log('[yt-pdh] no manifest found');
|
||||||
|
|
||||||
|
var desc = pd.videoDetails.videoId + ', ' + pd.videoDetails.title;
|
||||||
|
send(JSON.stringify(pd), mu, desc);
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.log("[yt-pdh]", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInterval(collect, interval * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
var scr = document.createElement('script');
|
||||||
|
scr.textContent = '(' + main.toString() + ')();';
|
||||||
|
(document.head || document.getElementsByTagName('head')[0]).appendChild(scr);
|
||||||
|
console.log('[yt-pdh] a');
|
||||||
198
bin/mtag/yt-ipr.py
Normal file
198
bin/mtag/yt-ipr.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import string
|
||||||
|
import urllib.request
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
"""
|
||||||
|
youtube initial player response
|
||||||
|
|
||||||
|
it's probably best to use this through a config file; see res/yt-ipr.conf
|
||||||
|
|
||||||
|
but if you want to use plain arguments instead then:
|
||||||
|
-v srv/ytm:ytm:w:rw,ed
|
||||||
|
:c,e2ts:c,e2dsa
|
||||||
|
:c,sz=16k-1m:c,maxn=10,300:c,rotf=%Y-%m/%d-%H
|
||||||
|
:c,mtp=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires=bin/mtag/yt-ipr.py
|
||||||
|
:c,mte=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires
|
||||||
|
|
||||||
|
see res/yt-ipr.user.js for the example userscript to go with this
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
with gzip.open(sys.argv[1], "rt", encoding="utf-8", errors="replace") as f:
|
||||||
|
txt = f.read()
|
||||||
|
except:
|
||||||
|
with open(sys.argv[1], "r", encoding="utf-8", errors="replace") as f:
|
||||||
|
txt = f.read()
|
||||||
|
|
||||||
|
txt = "{" + txt.split("{", 1)[1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
pd = json.loads(txt)
|
||||||
|
except json.decoder.JSONDecodeError as ex:
|
||||||
|
pd = json.loads(txt[: ex.pos])
|
||||||
|
|
||||||
|
# print(json.dumps(pd, indent=2))
|
||||||
|
|
||||||
|
if "videoDetails" in pd:
|
||||||
|
parse_youtube(pd)
|
||||||
|
else:
|
||||||
|
parse_freg(pd)
|
||||||
|
|
||||||
|
|
||||||
|
def get_expiration(url):
|
||||||
|
et = re.search(r"[?&]expire=([0-9]+)", url).group(1)
|
||||||
|
et = datetime.utcfromtimestamp(int(et))
|
||||||
|
return et.strftime("%Y-%m-%d, %H:%M")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_youtube(pd):
|
||||||
|
vd = pd["videoDetails"]
|
||||||
|
sd = pd["streamingData"]
|
||||||
|
|
||||||
|
et = sd["adaptiveFormats"][0]["url"]
|
||||||
|
et = get_expiration(et)
|
||||||
|
|
||||||
|
mf = []
|
||||||
|
if "dashManifestUrl" in sd:
|
||||||
|
mf.append("dash")
|
||||||
|
if "hlsManifestUrl" in sd:
|
||||||
|
mf.append("hls")
|
||||||
|
|
||||||
|
r = {
|
||||||
|
"yt-id": vd["videoId"],
|
||||||
|
"yt-title": vd["title"],
|
||||||
|
"yt-author": vd["author"],
|
||||||
|
"yt-channel": vd["channelId"],
|
||||||
|
"yt-views": vd["viewCount"],
|
||||||
|
"yt-private": vd["isPrivate"],
|
||||||
|
# "yt-expires": sd["expiresInSeconds"],
|
||||||
|
"yt-manifest": ",".join(mf),
|
||||||
|
"yt-expires": et,
|
||||||
|
}
|
||||||
|
print(json.dumps(r))
|
||||||
|
|
||||||
|
freg_conv(pd)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_freg(pd):
|
||||||
|
md = pd["metadata"]
|
||||||
|
r = {
|
||||||
|
"yt-id": md["id"],
|
||||||
|
"yt-title": md["title"],
|
||||||
|
"yt-author": md["channelName"],
|
||||||
|
"yt-channel": md["channelURL"].strip("/").split("/")[-1],
|
||||||
|
"yt-expires": get_expiration(list(pd["video"].values())[0]),
|
||||||
|
}
|
||||||
|
print(json.dumps(r))
|
||||||
|
|
||||||
|
|
||||||
|
def freg_conv(pd):
|
||||||
|
# based on getURLs.js v1.5 (2021-08-07)
|
||||||
|
# fmt: off
|
||||||
|
priority = {
|
||||||
|
"video": [
|
||||||
|
337, 315, 266, 138, # 2160p60
|
||||||
|
313, 336, # 2160p
|
||||||
|
308, # 1440p60
|
||||||
|
271, 264, # 1440p
|
||||||
|
335, 303, 299, # 1080p60
|
||||||
|
248, 169, 137, # 1080p
|
||||||
|
334, 302, 298, # 720p60
|
||||||
|
247, 136 # 720p
|
||||||
|
],
|
||||||
|
"audio": [
|
||||||
|
251, 141, 171, 140, 250, 249, 139
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
vid_id = pd["videoDetails"]["videoId"]
|
||||||
|
chan_id = pd["videoDetails"]["channelId"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
thumb_url = pd["microformat"]["playerMicroformatRenderer"]["thumbnail"]["thumbnails"][0]["url"]
|
||||||
|
start_ts = pd["microformat"]["playerMicroformatRenderer"]["liveBroadcastDetails"]["startTimestamp"]
|
||||||
|
except:
|
||||||
|
thumb_url = f"https://img.youtube.com/vi/{vid_id}/maxresdefault.jpg"
|
||||||
|
start_ts = ""
|
||||||
|
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"title": pd["videoDetails"]["title"],
|
||||||
|
"id": vid_id,
|
||||||
|
"channelName": pd["videoDetails"]["author"],
|
||||||
|
"channelURL": "https://www.youtube.com/channel/" + chan_id,
|
||||||
|
"description": pd["videoDetails"]["shortDescription"],
|
||||||
|
"thumbnailUrl": thumb_url,
|
||||||
|
"startTimestamp": start_ts,
|
||||||
|
}
|
||||||
|
|
||||||
|
if [x for x in vid_id if x not in string.ascii_letters + string.digits + "_-"]:
|
||||||
|
print(f"malicious json", file=sys.stderr)
|
||||||
|
return
|
||||||
|
|
||||||
|
basepath = os.path.dirname(sys.argv[1])
|
||||||
|
|
||||||
|
thumb_fn = f"{basepath}/{vid_id}.jpg"
|
||||||
|
tmp_fn = f"{thumb_fn}.{os.getpid()}"
|
||||||
|
if not os.path.exists(thumb_fn) and (
|
||||||
|
thumb_url.startswith("https://img.youtube.com/vi/")
|
||||||
|
or thumb_url.startswith("https://i.ytimg.com/vi/")
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(thumb_url) as fi:
|
||||||
|
with open(tmp_fn, "wb") as fo:
|
||||||
|
fo.write(fi.read())
|
||||||
|
|
||||||
|
os.rename(tmp_fn, thumb_fn)
|
||||||
|
except:
|
||||||
|
if os.path.exists(tmp_fn):
|
||||||
|
os.unlink(tmp_fn)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(thumb_fn, "rb") as f:
|
||||||
|
thumb = base64.b64encode(f.read()).decode("ascii")
|
||||||
|
except:
|
||||||
|
thumb = "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k="
|
||||||
|
|
||||||
|
metadata["thumbnail"] = "data:image/jpeg;base64," + thumb
|
||||||
|
|
||||||
|
ret = {
|
||||||
|
"metadata": metadata,
|
||||||
|
"version": "1.5",
|
||||||
|
"createTime": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for stream, itags in priority.items():
|
||||||
|
for itag in itags:
|
||||||
|
url = None
|
||||||
|
for afmt in pd["streamingData"]["adaptiveFormats"]:
|
||||||
|
if itag == afmt["itag"]:
|
||||||
|
url = afmt["url"]
|
||||||
|
break
|
||||||
|
|
||||||
|
if url:
|
||||||
|
ret[stream] = {itag: url}
|
||||||
|
break
|
||||||
|
|
||||||
|
fn = f"{basepath}/{vid_id}.urls.json"
|
||||||
|
with open(fn, "w", encoding="utf-8", errors="replace") as f:
|
||||||
|
f.write(json.dumps(ret, indent=4))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except:
|
||||||
|
# raise
|
||||||
|
pass
|
||||||
99
bin/prisonparty.sh
Normal file
99
bin/prisonparty.sh
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# runs copyparty (or any other program really) in a chroot
|
||||||
|
#
|
||||||
|
# assumption: these directories, and everything within, are owned by root
|
||||||
|
sysdirs=( /bin /lib /lib32 /lib64 /sbin /usr )
|
||||||
|
|
||||||
|
|
||||||
|
# error-handler
|
||||||
|
help() { cat <<'EOF'
|
||||||
|
|
||||||
|
usage:
|
||||||
|
./prisonparty.sh <ROOTDIR> <UID> <GID> [VOLDIR [VOLDIR...]] -- copyparty-sfx.py [...]"
|
||||||
|
|
||||||
|
example:
|
||||||
|
./prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt/nas/music -- copyparty-sfx.py -v /mnt/nas/music::rwmd"
|
||||||
|
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# read arguments
|
||||||
|
trap help EXIT
|
||||||
|
jail="$(realpath "$1")"; shift
|
||||||
|
uid="$1"; shift
|
||||||
|
gid="$1"; shift
|
||||||
|
|
||||||
|
vols=()
|
||||||
|
while true; do
|
||||||
|
v="$1"; shift
|
||||||
|
[ "$v" = -- ] && break # end of volumes
|
||||||
|
[ "$#" -eq 0 ] && break # invalid usage
|
||||||
|
vols+=( "$(realpath "$v")" )
|
||||||
|
done
|
||||||
|
pybin="$1"; shift
|
||||||
|
pybin="$(realpath "$pybin")"
|
||||||
|
cpp="$1"; shift
|
||||||
|
cpp="$(realpath "$cpp")"
|
||||||
|
cppdir="$(dirname "$cpp")"
|
||||||
|
trap - EXIT
|
||||||
|
|
||||||
|
|
||||||
|
# debug/vis
|
||||||
|
echo
|
||||||
|
echo "chroot-dir = $jail"
|
||||||
|
echo "user:group = $uid:$gid"
|
||||||
|
echo " copyparty = $cpp"
|
||||||
|
echo
|
||||||
|
printf '\033[33m%s\033[0m\n' "copyparty can access these folders and all their subdirectories:"
|
||||||
|
for v in "${vols[@]}"; do
|
||||||
|
printf '\033[36m ├─\033[0m %s \033[36m ── added by (You)\033[0m\n' "$v"
|
||||||
|
done
|
||||||
|
printf '\033[36m ├─\033[0m %s \033[36m ── where the copyparty binary is\033[0m\n' "$cppdir"
|
||||||
|
printf '\033[36m ╰─\033[0m %s \033[36m ── the folder you are currently in\033[0m\n' "$PWD"
|
||||||
|
vols+=("$cppdir" "$PWD")
|
||||||
|
echo
|
||||||
|
|
||||||
|
|
||||||
|
# remove any trailing slashes
|
||||||
|
jail="${jail%/}"
|
||||||
|
cppdir="${cppdir%/}"
|
||||||
|
|
||||||
|
|
||||||
|
# bind-mount system directories and volumes
|
||||||
|
printf '%s\n' "${sysdirs[@]}" "${vols[@]}" | LC_ALL=C sort |
|
||||||
|
while IFS= read -r v; do
|
||||||
|
[ -e "$v" ] || {
|
||||||
|
# printf '\033[1;31mfolder does not exist:\033[0m %s\n' "/$v"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i1=$(stat -c%D.%i "$v" 2>/dev/null || echo a)
|
||||||
|
i2=$(stat -c%D.%i "$jail$v" 2>/dev/null || echo b)
|
||||||
|
[ $i1 = $i2 ] && continue
|
||||||
|
|
||||||
|
mkdir -p "$jail$v"
|
||||||
|
mount --bind "$v" "$jail$v"
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
# create a tmp
|
||||||
|
mkdir -p "$jail/tmp"
|
||||||
|
chmod 777 "$jail/tmp"
|
||||||
|
|
||||||
|
|
||||||
|
# run copyparty
|
||||||
|
/sbin/chroot --userspec=$uid:$gid "$jail" "$pybin" "$cpp" "$@" && rv=0 || rv=$?
|
||||||
|
|
||||||
|
|
||||||
|
# cleanup if not in use
|
||||||
|
lsof "$jail" | grep -qF "$jail" &&
|
||||||
|
echo "chroot is in use, will not cleanup" ||
|
||||||
|
{
|
||||||
|
mount | grep -qF " on $jail" |
|
||||||
|
awk '{sub(/ type .*/,"");sub(/.* on /,"");print}' |
|
||||||
|
LC_ALL=C sort -r | tee /dev/stderr | tr '\n' '\0' | xargs -r0 umount
|
||||||
|
}
|
||||||
|
exit $rv
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
# when running copyparty behind a reverse-proxy,
|
# when running copyparty behind a reverse proxy,
|
||||||
# make sure that copyparty allows at least as many clients as the proxy does,
|
# the following arguments are recommended:
|
||||||
# so run copyparty with -nc 512 if your nginx has the default limits
|
#
|
||||||
# (worker_processes 1, worker_connections 512)
|
# -nc 512 important, see next paragraph
|
||||||
|
# --http-only lower latency on initial connection
|
||||||
|
# -i 127.0.0.1 only accept connections from nginx
|
||||||
|
#
|
||||||
|
# -nc must match or exceed the webserver's max number of concurrent clients;
|
||||||
|
# nginx default is 512 (worker_processes 1, worker_connections 512)
|
||||||
|
#
|
||||||
|
# you may also consider adding -j0 for CPU-intensive configurations
|
||||||
|
# (not that i can really think of any good examples)
|
||||||
|
|
||||||
upstream cpp {
|
upstream cpp {
|
||||||
server 127.0.0.1:3923;
|
server 127.0.0.1:3923;
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
#
|
#
|
||||||
# you may want to:
|
# you may want to:
|
||||||
# change '/usr/bin/python' to another interpreter
|
# change '/usr/bin/python' to another interpreter
|
||||||
# change '/mnt::a' to another location or permission-set
|
# change '/mnt::rw' to another location or permission-set
|
||||||
|
|
||||||
name="$SVCNAME"
|
name="$SVCNAME"
|
||||||
command_background=true
|
command_background=true
|
||||||
pidfile="/var/run/$SVCNAME.pid"
|
pidfile="/var/run/$SVCNAME.pid"
|
||||||
|
|
||||||
command="/usr/bin/python /usr/local/bin/copyparty-sfx.py"
|
command="/usr/bin/python /usr/local/bin/copyparty-sfx.py"
|
||||||
command_args="-q -v /mnt::a"
|
command_args="-q -v /mnt::rw"
|
||||||
|
|||||||
@@ -6,13 +6,28 @@
|
|||||||
#
|
#
|
||||||
# you may want to:
|
# you may want to:
|
||||||
# change '/usr/bin/python' to another interpreter
|
# change '/usr/bin/python' to another interpreter
|
||||||
# change '/mnt::a' to another location or permission-set
|
# change '/mnt::rw' to another location or permission-set
|
||||||
|
#
|
||||||
|
# with `Type=notify`, copyparty will signal systemd when it is ready to
|
||||||
|
# accept connections; correctly delaying units depending on copyparty.
|
||||||
|
# But note that journalctl will get the timestamps wrong due to
|
||||||
|
# python disabling line-buffering, so messages are out-of-order:
|
||||||
|
# https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png
|
||||||
|
#
|
||||||
|
# enable line-buffering for realtime logging (slight performance cost):
|
||||||
|
# modify ExecStart and prefix it with `/usr/bin/stdbuf -oL` like so:
|
||||||
|
# ExecStart=/usr/bin/stdbuf -oL /usr/bin/python3 [...]
|
||||||
|
# but some systemd versions require this instead (higher performance cost):
|
||||||
|
# inside the [Service] block, add the following line:
|
||||||
|
# Environment=PYTHONUNBUFFERED=x
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=copyparty file server
|
Description=copyparty file server
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::a
|
Type=notify
|
||||||
|
SyslogIdentifier=copyparty
|
||||||
|
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw
|
||||||
ExecStartPre=/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
ExecStartPre=/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|||||||
27
contrib/systemd/prisonparty.service
Normal file
27
contrib/systemd/prisonparty.service
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# this will start `/usr/local/bin/copyparty-sfx.py`
|
||||||
|
# in a chroot, preventing accidental access elsewhere
|
||||||
|
# and share '/mnt' with anonymous read+write
|
||||||
|
#
|
||||||
|
# installation:
|
||||||
|
# 1) put copyparty-sfx.py and prisonparty.sh in /usr/local/bin
|
||||||
|
# 2) cp -pv prisonparty.service /etc/systemd/system && systemctl enable --now prisonparty
|
||||||
|
#
|
||||||
|
# you may want to:
|
||||||
|
# change '/mnt::rw' to another location or permission-set
|
||||||
|
# (remember to change the '/mnt' chroot arg too)
|
||||||
|
#
|
||||||
|
# enable line-buffering for realtime logging (slight performance cost):
|
||||||
|
# inside the [Service] block, add the following line:
|
||||||
|
# Environment=PYTHONUNBUFFERED=x
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=copyparty file server
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
SyslogIdentifier=prisonparty
|
||||||
|
WorkingDirectory=/usr/local/bin
|
||||||
|
ExecStart=/bin/bash /usr/local/bin/prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt -- \
|
||||||
|
/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -9,6 +9,9 @@ import os
|
|||||||
PY2 = sys.version_info[0] == 2
|
PY2 = sys.version_info[0] == 2
|
||||||
if PY2:
|
if PY2:
|
||||||
sys.dont_write_bytecode = True
|
sys.dont_write_bytecode = True
|
||||||
|
unicode = unicode
|
||||||
|
else:
|
||||||
|
unicode = str
|
||||||
|
|
||||||
WINDOWS = False
|
WINDOWS = False
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
@@ -22,6 +25,28 @@ ANYWIN = WINDOWS or sys.platform in ["msys"]
|
|||||||
MACOS = platform.system() == "Darwin"
|
MACOS = platform.system() == "Darwin"
|
||||||
|
|
||||||
|
|
||||||
|
def get_unix_home():
|
||||||
|
try:
|
||||||
|
v = os.environ["XDG_CONFIG_HOME"]
|
||||||
|
if not v:
|
||||||
|
raise Exception()
|
||||||
|
ret = os.path.normpath(v)
|
||||||
|
os.listdir(ret)
|
||||||
|
return ret
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
v = os.path.expanduser("~/.config")
|
||||||
|
if v.startswith("~"):
|
||||||
|
raise Exception()
|
||||||
|
ret = os.path.normpath(v)
|
||||||
|
os.listdir(ret)
|
||||||
|
return ret
|
||||||
|
except:
|
||||||
|
return "/tmp"
|
||||||
|
|
||||||
|
|
||||||
class EnvParams(object):
|
class EnvParams(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.t0 = time.time()
|
self.t0 = time.time()
|
||||||
@@ -34,10 +59,7 @@ class EnvParams(object):
|
|||||||
elif sys.platform == "darwin":
|
elif sys.platform == "darwin":
|
||||||
self.cfg = os.path.expanduser("~/Library/Preferences/copyparty")
|
self.cfg = os.path.expanduser("~/Library/Preferences/copyparty")
|
||||||
else:
|
else:
|
||||||
self.cfg = os.path.normpath(
|
self.cfg = get_unix_home() + "/copyparty"
|
||||||
os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
|
||||||
+ "/copyparty"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.cfg = self.cfg.replace("\\", "/")
|
self.cfg = self.cfg.replace("\\", "/")
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -20,10 +20,11 @@ import threading
|
|||||||
import traceback
|
import traceback
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
||||||
from .__init__ import E, WINDOWS, VT100, PY2
|
from .__init__ import E, WINDOWS, VT100, PY2, unicode
|
||||||
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, alltrace
|
from .util import py_desc, align_tab, IMPLICATIONS, ansi_re
|
||||||
|
from .authsrv import re_vol
|
||||||
|
|
||||||
HAVE_SSL = True
|
HAVE_SSL = True
|
||||||
try:
|
try:
|
||||||
@@ -31,6 +32,8 @@ try:
|
|||||||
except:
|
except:
|
||||||
HAVE_SSL = False
|
HAVE_SSL = False
|
||||||
|
|
||||||
|
printed = ""
|
||||||
|
|
||||||
|
|
||||||
class RiceFormatter(argparse.HelpFormatter):
|
class RiceFormatter(argparse.HelpFormatter):
|
||||||
def _get_help_string(self, action):
|
def _get_help_string(self, action):
|
||||||
@@ -61,8 +64,19 @@ class Dodge11874(RiceFormatter):
|
|||||||
super(Dodge11874, self).__init__(*args, **kwargs)
|
super(Dodge11874, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def lprint(*a, **ka):
|
||||||
|
global printed
|
||||||
|
|
||||||
|
txt = " ".join(unicode(x) for x in a) + ka.get("end", "\n")
|
||||||
|
printed += txt
|
||||||
|
if not VT100:
|
||||||
|
txt = ansi_re.sub("", txt)
|
||||||
|
|
||||||
|
print(txt, **ka)
|
||||||
|
|
||||||
|
|
||||||
def warn(msg):
|
def warn(msg):
|
||||||
print("\033[1mwarning:\033[0;33m {}\033[0m\n".format(msg))
|
lprint("\033[1mwarning:\033[0;33m {}\033[0m\n".format(msg))
|
||||||
|
|
||||||
|
|
||||||
def ensure_locale():
|
def ensure_locale():
|
||||||
@@ -73,7 +87,7 @@ def ensure_locale():
|
|||||||
]:
|
]:
|
||||||
try:
|
try:
|
||||||
locale.setlocale(locale.LC_ALL, x)
|
locale.setlocale(locale.LC_ALL, x)
|
||||||
print("Locale:", x)
|
lprint("Locale:", x)
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
@@ -94,7 +108,7 @@ def ensure_cert():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if filecmp.cmp(cert_cfg, cert_insec):
|
if filecmp.cmp(cert_cfg, cert_insec):
|
||||||
print(
|
lprint(
|
||||||
"\033[33m using default TLS certificate; https will be insecure."
|
"\033[33m using default TLS certificate; https will be insecure."
|
||||||
+ "\033[36m\n certificate location: {}\033[0m\n".format(cert_cfg)
|
+ "\033[36m\n certificate location: {}\033[0m\n".format(cert_cfg)
|
||||||
)
|
)
|
||||||
@@ -123,7 +137,7 @@ def configure_ssl_ver(al):
|
|||||||
if "help" in sslver:
|
if "help" in sslver:
|
||||||
avail = [terse_sslver(x[6:]) for x in flags]
|
avail = [terse_sslver(x[6:]) for x in flags]
|
||||||
avail = " ".join(sorted(avail) + ["all"])
|
avail = " ".join(sorted(avail) + ["all"])
|
||||||
print("\navailable ssl/tls versions:\n " + avail)
|
lprint("\navailable ssl/tls versions:\n " + avail)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
al.ssl_flags_en = 0
|
al.ssl_flags_en = 0
|
||||||
@@ -143,7 +157,7 @@ def configure_ssl_ver(al):
|
|||||||
|
|
||||||
for k in ["ssl_flags_en", "ssl_flags_de"]:
|
for k in ["ssl_flags_en", "ssl_flags_de"]:
|
||||||
num = getattr(al, k)
|
num = getattr(al, k)
|
||||||
print("{}: {:8x} ({})".format(k, num, num))
|
lprint("{}: {:8x} ({})".format(k, num, num))
|
||||||
|
|
||||||
# think i need that beer now
|
# think i need that beer now
|
||||||
|
|
||||||
@@ -160,13 +174,13 @@ def configure_ssl_ciphers(al):
|
|||||||
try:
|
try:
|
||||||
ctx.set_ciphers(al.ciphers)
|
ctx.set_ciphers(al.ciphers)
|
||||||
except:
|
except:
|
||||||
print("\n\033[1;31mfailed to set ciphers\033[0m\n")
|
lprint("\n\033[1;31mfailed to set ciphers\033[0m\n")
|
||||||
|
|
||||||
if not hasattr(ctx, "get_ciphers"):
|
if not hasattr(ctx, "get_ciphers"):
|
||||||
print("cannot read cipher list: openssl or python too old")
|
lprint("cannot read cipher list: openssl or python too old")
|
||||||
else:
|
else:
|
||||||
ciphers = [x["description"] for x in ctx.get_ciphers()]
|
ciphers = [x["description"] for x in ctx.get_ciphers()]
|
||||||
print("\n ".join(["\nenabled ciphers:"] + align_tab(ciphers) + [""]))
|
lprint("\n ".join(["\nenabled ciphers:"] + align_tab(ciphers) + [""]))
|
||||||
|
|
||||||
if is_help:
|
if is_help:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -182,42 +196,40 @@ 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,
|
||||||
prog="copyparty",
|
prog="copyparty",
|
||||||
description="http file sharing hub v{} ({})".format(S_VERSION, S_BUILD_DT),
|
description="http file sharing hub v{} ({})".format(S_VERSION, S_BUILD_DT),
|
||||||
epilog=dedent(
|
)
|
||||||
"""
|
|
||||||
|
sects = [
|
||||||
|
[
|
||||||
|
"accounts",
|
||||||
|
"accounts and volumes",
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
-a takes username:password,
|
-a takes username:password,
|
||||||
-v takes src:dst:permset:permset:cflag:cflag:...
|
-v takes src:dst:perm1:perm2:permN:volflag1:volflag2:volflagN:...
|
||||||
where "permset" is accesslevel followed by username (no separator)
|
where "perm" is "accesslevels,username1,username2,..."
|
||||||
and "cflag" is config flags to set on this volume
|
and "volflag" is config flags to set on this volume
|
||||||
|
|
||||||
list of cflags:
|
list of accesslevels:
|
||||||
"cnodupe" rejects existing files (instead of symlinking them)
|
"r" (read): list folder contents, download files
|
||||||
"ce2d" sets -e2d (all -e2* args can be set using ce2* cflags)
|
"w" (write): upload files; need "r" to see the uploads
|
||||||
"cd2t" disables metadata collection, overrides -e2t*
|
"m" (move): move files and folders; need "w" at destination
|
||||||
"cd2d" disables all database stuff, overrides -e2*
|
"d" (delete): permanently delete files and folders
|
||||||
|
|
||||||
|
too many volflags to list here, see the other sections
|
||||||
|
|
||||||
example:\033[35m
|
example:\033[35m
|
||||||
-a ed:hunter2 -v .::r:aed -v ../inc:dump:w:aed:cnodupe \033[36m
|
-a ed:hunter2 -v .::r:rw,ed -v ../inc:dump:w:rw,ed:c,nodupe \033[36m
|
||||||
mount current directory at "/" with
|
mount current directory at "/" with
|
||||||
* r (read-only) for everyone
|
* r (read-only) for everyone
|
||||||
* a (read+write) for ed
|
* rw (read+write) for ed
|
||||||
mount ../inc at "/dump" with
|
mount ../inc at "/dump" with
|
||||||
* w (write-only) for everyone
|
* w (write-only) for everyone
|
||||||
* a (read+write) for ed
|
* rw (read+write) for ed
|
||||||
* reject duplicate files \033[0m
|
* reject duplicate files \033[0m
|
||||||
|
|
||||||
if no accounts or volumes are configured,
|
if no accounts or volumes are configured,
|
||||||
@@ -225,70 +237,138 @@ def run_argparse(argv, formatter):
|
|||||||
|
|
||||||
consider the config file for more flexible account/volume management,
|
consider the config file for more flexible account/volume management,
|
||||||
including dynamic reload at runtime (and being more readable w)
|
including dynamic reload at runtime (and being more readable w)
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"flags",
|
||||||
|
"list of volflags",
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
|
volflags are appended to volume definitions, for example,
|
||||||
|
to create a write-only volume with the \033[33mnodupe\033[0m and \033[32mnosub\033[0m flags:
|
||||||
|
\033[35m-v /mnt/inc:/inc:w\033[33m:c,nodupe\033[32m:c,nosub
|
||||||
|
|
||||||
|
\033[0muploads, general:
|
||||||
|
\033[36mnodupe\033[35m rejects existing files (instead of symlinking them)
|
||||||
|
\033[36mnosub\033[35m forces all uploads into the top folder of the vfs
|
||||||
|
\033[36mgz\033[35m allows server-side gzip of uploads with ?gz (also c,xz)
|
||||||
|
\033[36mpk\033[35m forces server-side compression, optional arg: xz,9
|
||||||
|
|
||||||
|
\033[0mupload rules:
|
||||||
|
\033[36mmaxn=250,600\033[35m max 250 uploads over 15min
|
||||||
|
\033[36mmaxb=1g,300\033[35m max 1 GiB over 5min (suffixes: b, k, m, g)
|
||||||
|
\033[36msz=1k-3m\033[35m allow filesizes between 1 KiB and 3MiB
|
||||||
|
|
||||||
|
\033[0mupload rotation:
|
||||||
|
(moves all uploads into the specified folder structure)
|
||||||
|
\033[36mrotn=100,3\033[35m 3 levels of subfolders with 100 entries in each
|
||||||
|
\033[36mrotf=%Y-%m/%d-%H\033[35m date-formatted organizing
|
||||||
|
\033[36mlifetime=3600\033[35m uploads are deleted after 1 hour
|
||||||
|
|
||||||
|
\033[0mdatabase, general:
|
||||||
|
\033[36me2d\033[35m sets -e2d (all -e2* args can be set using ce2* volflags)
|
||||||
|
\033[36md2t\033[35m disables metadata collection, overrides -e2t*
|
||||||
|
\033[36md2d\033[35m disables all database stuff, overrides -e2*
|
||||||
|
\033[36mdhash\033[35m disables file hashing on initial scans, also ehash
|
||||||
|
\033[36mhist=/tmp/cdb\033[35m puts thumbnails and indexes at that location
|
||||||
|
\033[36mscan=60\033[35m scan for new files every 60sec, same as --re-maxage
|
||||||
|
|
||||||
|
\033[0mdatabase, audio tags:
|
||||||
|
"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...
|
||||||
|
\033[36mmtp=.bpm=f,audio-bpm.py\033[35m uses the "audio-bpm.py" program to
|
||||||
|
generate ".bpm" tags from uploads (f = overwrite tags)
|
||||||
|
\033[36mmtp=ahash,vhash=media-hash.py\033[35m collects two tags at once
|
||||||
|
\033[0m"""
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"urlform",
|
||||||
|
"",
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
values for --urlform:
|
values for --urlform:
|
||||||
"stash" dumps the data to file and returns length + checksum
|
\033[36mstash\033[35m dumps the data to file and returns length + checksum
|
||||||
"save,get" dumps to file and returns the page like a GET
|
\033[36msave,get\033[35m dumps to file and returns the page like a GET
|
||||||
"print,get" prints the data in the log and returns GET
|
\033[36mprint,get\033[35m 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)
|
||||||
|
"""
|
||||||
values for --ls:
|
),
|
||||||
"USR" is a user to browse as; * is anonymous, ** is all users
|
],
|
||||||
"VOL" is a single volume to scan, default is * (all vols)
|
[
|
||||||
"FLAG" is flags;
|
"ls",
|
||||||
"v" in addition to realpaths, print usernames and vpaths
|
"volume inspection",
|
||||||
"ln" only prints symlinks leaving the volume mountpoint
|
dedent(
|
||||||
"p" exits 1 if any such symlinks are found
|
"""
|
||||||
"r" resumes startup after the listing
|
\033[35m--ls USR,VOL,FLAGS
|
||||||
|
\033[36mUSR\033[0m is a user to browse as; * is anonymous, ** is all users
|
||||||
|
\033[36mVOL\033[0m is a single volume to scan, default is * (all vols)
|
||||||
|
\033[36mFLAG\033[0m is flags;
|
||||||
|
\033[36mv\033[0m in addition to realpaths, print usernames and vpaths
|
||||||
|
\033[36mln\033[0m only prints symlinks leaving the volume mountpoint
|
||||||
|
\033[36mp\033[0m exits 1 if any such symlinks are found
|
||||||
|
\033[36mr\033[0m resumes startup after the listing
|
||||||
examples:
|
examples:
|
||||||
--ls '**' # list all files which are possible to read
|
--ls '**' # list all files which are possible to read
|
||||||
--ls '**,*,ln' # check for dangerous symlinks
|
--ls '**,*,ln' # check for dangerous symlinks
|
||||||
--ls '**,*,ln,p,r' # check, then start normally if safe
|
--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")
|
u = unicode
|
||||||
ap.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients")
|
ap2 = ap.add_argument_group('general options')
|
||||||
ap.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores")
|
ap2.add_argument("-c", metavar="PATH", type=u, action="append", help="add config file")
|
||||||
ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account, USER:PASS; example [ed:wark")
|
ap2.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients")
|
||||||
ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume, SRC:DST:FLAG; example [.::r], [/mnt/nas/music:/music:r:aed")
|
ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores")
|
||||||
ap.add_argument("-ed", action="store_true", help="enable ?dots")
|
ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="add account, USER:PASS; example [ed:wark")
|
||||||
ap.add_argument("-emp", action="store_true", help="enable markdown plugins")
|
ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="add volume, SRC:DST:FLAG; example [.::r], [/mnt/nas/music:/music:r:aed")
|
||||||
ap.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate")
|
ap2.add_argument("-ed", action="store_true", help="enable ?dots")
|
||||||
ap.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads")
|
ap2.add_argument("-emp", action="store_true", help="enable markdown plugins")
|
||||||
ap.add_argument("--sparse", metavar="MiB", type=int, default=4, help="up2k min.size threshold (mswin-only)")
|
ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate")
|
||||||
ap.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms; examples: [stash], [save,get]")
|
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,get", help="how to handle url-forms; examples: [stash], [save,get]")
|
||||||
|
|
||||||
|
ap2 = ap.add_argument_group('upload options')
|
||||||
|
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads")
|
||||||
|
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="up2k min.size threshold (mswin-only)")
|
||||||
|
ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('network options')
|
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("-i", metavar="IP", type=u, 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("-p", metavar="PORT", type=u, 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.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 = ap.add_argument_group('SSL/TLS options')
|
||||||
ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls")
|
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("--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("--ssl-ver", metavar="LIST", type=u, 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("--ciphers", metavar="LIST", type=u, 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-dbg", action="store_true", help="dump some tls info")
|
||||||
ap2.add_argument("--ssl-log", metavar="PATH", help="log master secrets")
|
ap2.add_argument("--ssl-log", metavar="PATH", type=u, help="log master secrets")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('opt-outs')
|
ap2 = ap.add_argument_group('opt-outs')
|
||||||
ap2.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
|
ap2.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
|
||||||
|
ap2.add_argument("--no-del", action="store_true", help="disable delete operations")
|
||||||
|
ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations")
|
||||||
ap2.add_argument("-nih", action="store_true", help="no info hostname")
|
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("-nid", action="store_true", help="no info disk-usage")
|
||||||
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
|
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
|
||||||
|
ap2.add_argument("--no-lifetime", action="store_true", help="disable automatic deletion of uploads after a certain time (lifetime volflag)")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('safety options')
|
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("--ls", metavar="U[,V[,F]]", type=u, 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.add_argument("--salt", type=u, default="hunter2", help="up2k file-hash salt")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('logging options')
|
ap2 = ap.add_argument_group('logging options')
|
||||||
ap2.add_argument("-q", action="store_true", help="quiet")
|
ap2.add_argument("-q", action="store_true", help="quiet")
|
||||||
|
ap2.add_argument("-lo", metavar="PATH", type=u, help="logfile, example: cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz")
|
||||||
|
ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup")
|
||||||
ap2.add_argument("--log-conn", action="store_true", help="print tcp-server msgs")
|
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("--log-htp", action="store_true", help="print http-server threadpool scaling")
|
||||||
ap2.add_argument("--lf-url", metavar="RE", type=str, default=r"^/\.cpr/|\?th=[wj]$", help="dont log URLs matching")
|
ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="dump incoming header")
|
||||||
|
ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|\?th=[wj]$", help="dont log URLs matching")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('admin panel options')
|
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-rescan", action="store_true", help="disable ?scan (volume reindexing)")
|
||||||
@@ -298,43 +378,66 @@ def run_argparse(argv, formatter):
|
|||||||
ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails")
|
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("--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-size", metavar="WxH", default="320x256", help="thumbnail res")
|
||||||
|
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=0, help="max num cpu cores to use, 0=all")
|
||||||
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image")
|
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-jpg", action="store_true", help="disable jpg output")
|
||||||
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
|
ap2.add_argument("--th-no-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-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-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-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled")
|
||||||
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age")
|
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age")
|
||||||
|
ap2.add_argument("--th-covers", metavar="N,N", type=u, 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('general db options')
|
||||||
ap2.add_argument("-e2d", action="store_true", help="enable up2k database")
|
ap2.add_argument("-e2d", action="store_true", help="enable up2k database")
|
||||||
ap2.add_argument("-e2ds", action="store_true", help="enable up2k db-scanner, sets -e2d")
|
ap2.add_argument("-e2ds", action="store_true", help="enable up2k db-scanner, sets -e2d")
|
||||||
ap2.add_argument("-e2dsa", action="store_true", help="scan all folders (for search), sets -e2ds")
|
ap2.add_argument("-e2dsa", action="store_true", help="scan all folders (for search), sets -e2ds")
|
||||||
|
ap2.add_argument("--hist", metavar="PATH", type=u, help="where to store volume data (db, thumbs)")
|
||||||
|
ap2.add_argument("--no-hash", action="store_true", help="disable hashing during e2ds folder scans")
|
||||||
|
ap2.add_argument("--re-int", metavar="SEC", type=int, default=30, help="disk rescan check interval")
|
||||||
|
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="disk rescan volume interval, 0=off, can be set per-volume with the 'scan' volflag")
|
||||||
|
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline")
|
||||||
|
|
||||||
|
ap2 = ap.add_argument_group('metadata db options')
|
||||||
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-mutagen", action="store_true", help="use FFprobe for tags instead")
|
||||||
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-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("--no-mtag-ff", action="store_true", help="never use FFprobe as tag reader")
|
||||||
ap2.add_argument("-mte", metavar="M,M,M", type=str, help="tags to index/display (comma-sep.)",
|
ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="add/replace metadata mapping")
|
||||||
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,ac,vc,res,.fps")
|
ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.)",
|
||||||
ap2.add_argument("-mtp", metavar="M=[f,]bin", action="append", type=str, help="read tag M using bin")
|
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,res,.fps,ahash,vhash")
|
||||||
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline")
|
ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.)",
|
||||||
|
default=".vq,.aq,vc,ac,res,.fps")
|
||||||
|
ap2.add_argument("-mtp", metavar="M=[f,]bin", type=u, action="append", help="read tag M using bin")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('appearance options')
|
ap2 = ap.add_argument_group('appearance options')
|
||||||
ap2.add_argument("--css-browser", metavar="L", help="URL to additional CSS to include")
|
ap2.add_argument("--css-browser", metavar="L", type=u, help="URL to additional CSS to include")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('debug options')
|
ap2 = ap.add_argument_group('debug options')
|
||||||
ap2.add_argument("--no-sendfile", action="store_true", help="disable sendfile")
|
ap2.add_argument("--no-sendfile", action="store_true", help="disable sendfile")
|
||||||
ap2.add_argument("--no-scandir", action="store_true", help="disable scandir")
|
ap2.add_argument("--no-scandir", action="store_true", help="disable scandir")
|
||||||
ap2.add_argument("--no-fastboot", action="store_true", help="wait for up2k indexing")
|
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")
|
ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead")
|
||||||
|
ap2.add_argument("--stackmon", metavar="P,S", type=u, help="write stacktrace to Path every S second")
|
||||||
return ap.parse_args(args=argv[1:])
|
ap2.add_argument("--log-thrs", metavar="SEC", type=float, help="list active threads every SEC")
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
ap2 = ap.add_argument_group("help sections")
|
||||||
|
for k, h, _ in sects:
|
||||||
|
ap2.add_argument("--help-" + k, action="store_true", help=h)
|
||||||
|
|
||||||
|
ret = ap.parse_args(args=argv[1:])
|
||||||
|
for k, h, t in sects:
|
||||||
|
k2 = "help_" + k.replace("-", "_")
|
||||||
|
if vars(ret)[k2]:
|
||||||
|
lprint("# {} help page".format(k))
|
||||||
|
lprint(t + "\033[0m")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
def main(argv=None):
|
||||||
time.strptime("19970815", "%Y%m%d") # python#7980
|
time.strptime("19970815", "%Y%m%d") # python#7980
|
||||||
@@ -347,7 +450,7 @@ def main(argv=None):
|
|||||||
desc = py_desc().replace("[", "\033[1;30m[")
|
desc = py_desc().replace("[", "\033[1;30m[")
|
||||||
|
|
||||||
f = '\033[36mcopyparty v{} "\033[35m{}\033[36m" ({})\n{}\033[0m\n'
|
f = '\033[36mcopyparty v{} "\033[35m{}\033[36m" ({})\n{}\033[0m\n'
|
||||||
print(f.format(S_VERSION, CODENAME, S_BUILD_DT, desc))
|
lprint(f.format(S_VERSION, CODENAME, S_BUILD_DT, desc))
|
||||||
|
|
||||||
ensure_locale()
|
ensure_locale()
|
||||||
if HAVE_SSL:
|
if HAVE_SSL:
|
||||||
@@ -361,7 +464,7 @@ def main(argv=None):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
msg = "\033[1;31mWARNING:\033[0;1m\n {} \033[0;33mwas replaced with\033[0;1m {} \033[0;33mand will be removed\n\033[0m"
|
msg = "\033[1;31mWARNING:\033[0;1m\n {} \033[0;33mwas replaced with\033[0;1m {} \033[0;33mand will be removed\n\033[0m"
|
||||||
print(msg.format(dk, nk))
|
lprint(msg.format(dk, nk))
|
||||||
argv[idx] = nk
|
argv[idx] = nk
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
@@ -370,15 +473,41 @@ def main(argv=None):
|
|||||||
except AssertionError:
|
except AssertionError:
|
||||||
al = run_argparse(argv, Dodge11874)
|
al = run_argparse(argv, Dodge11874)
|
||||||
|
|
||||||
if al.stackmon:
|
nstrs = []
|
||||||
fp, f = al.stackmon.rsplit(",", 1)
|
anymod = False
|
||||||
f = int(f)
|
for ostr in al.v or []:
|
||||||
t = threading.Thread(
|
m = re_vol.match(ostr)
|
||||||
target=stackmon,
|
if not m:
|
||||||
args=(fp, f),
|
# not our problem
|
||||||
)
|
nstrs.append(ostr)
|
||||||
t.daemon = True
|
continue
|
||||||
t.start()
|
|
||||||
|
src, dst, perms = m.groups()
|
||||||
|
na = [src, dst]
|
||||||
|
mod = False
|
||||||
|
for opt in perms.split(":"):
|
||||||
|
if re.match("c[^,]", opt):
|
||||||
|
mod = True
|
||||||
|
na.append("c," + opt[1:])
|
||||||
|
elif re.sub("^[rwmd]*", "", opt) and "," not in opt:
|
||||||
|
mod = True
|
||||||
|
perm = opt[0]
|
||||||
|
if perm == "a":
|
||||||
|
perm = "rw"
|
||||||
|
na.append(perm + "," + opt[1:])
|
||||||
|
else:
|
||||||
|
na.append(opt)
|
||||||
|
|
||||||
|
nstr = ":".join(na)
|
||||||
|
nstrs.append(nstr if mod else ostr)
|
||||||
|
if mod:
|
||||||
|
msg = "\033[1;31mWARNING:\033[0;1m\n -v {} \033[0;33mwas replaced with\033[0;1m\n -v {} \n\033[0m"
|
||||||
|
lprint(msg.format(ostr, nstr))
|
||||||
|
anymod = True
|
||||||
|
|
||||||
|
if anymod:
|
||||||
|
al.v = nstrs
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
# propagate implications
|
# propagate implications
|
||||||
for k1, k2 in IMPLICATIONS:
|
for k1, k2 in IMPLICATIONS:
|
||||||
@@ -410,12 +539,12 @@ 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 WINDOWS and sys.version_info < (3, 6):
|
if sys.version_info < (3, 6):
|
||||||
al.no_scandir = True
|
al.no_scandir = True
|
||||||
|
|
||||||
# signal.signal(signal.SIGINT, sighandler)
|
# signal.signal(signal.SIGINT, sighandler)
|
||||||
|
|
||||||
SvcHub(al).run()
|
SvcHub(al, argv, printed).run()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
VERSION = (0, 11, 19)
|
VERSION = (0, 13, 13)
|
||||||
CODENAME = "the grid"
|
CODENAME = "future-proof"
|
||||||
BUILD_DT = (2021, 6, 19)
|
BUILD_DT = (2021, 9, 3)
|
||||||
|
|
||||||
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,40 +5,229 @@ import re
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import stat
|
import stat
|
||||||
|
import time
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import threading
|
import threading
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from .__init__ import WINDOWS
|
from .__init__ import WINDOWS
|
||||||
from .util import IMPLICATIONS, undot, Pebkac, fsdec, fsenc, statdir, nuprint
|
from .util import (
|
||||||
|
IMPLICATIONS,
|
||||||
|
uncyg,
|
||||||
|
undot,
|
||||||
|
unhumanize,
|
||||||
|
absreal,
|
||||||
|
Pebkac,
|
||||||
|
fsenc,
|
||||||
|
statdir,
|
||||||
|
)
|
||||||
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
|
LEELOO_DALLAS = "leeloo_dallas"
|
||||||
|
|
||||||
|
|
||||||
|
class AXS(object):
|
||||||
|
def __init__(self, uread=None, uwrite=None, umove=None, udel=None):
|
||||||
|
self.uread = {} if uread is None else {k: 1 for k in uread}
|
||||||
|
self.uwrite = {} if uwrite is None else {k: 1 for k in uwrite}
|
||||||
|
self.umove = {} if umove is None else {k: 1 for k in umove}
|
||||||
|
self.udel = {} if udel is None else {k: 1 for k in udel}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "AXS({})".format(
|
||||||
|
", ".join(
|
||||||
|
"{}={!r}".format(k, self.__dict__[k])
|
||||||
|
for k in "uread uwrite umove udel".split()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Lim(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.nups = {} # num tracker
|
||||||
|
self.bups = {} # byte tracker list
|
||||||
|
self.bupc = {} # byte tracker cache
|
||||||
|
|
||||||
|
self.nosub = False # disallow subdirectories
|
||||||
|
|
||||||
|
self.smin = None # filesize min
|
||||||
|
self.smax = None # filesize max
|
||||||
|
|
||||||
|
self.bwin = None # bytes window
|
||||||
|
self.bmax = None # bytes max
|
||||||
|
self.nwin = None # num window
|
||||||
|
self.nmax = None # num max
|
||||||
|
|
||||||
|
self.rotn = None # rot num files
|
||||||
|
self.rotl = None # rot depth
|
||||||
|
self.rotf = None # rot datefmt
|
||||||
|
self.rot_re = None # rotf check
|
||||||
|
|
||||||
|
def set_rotf(self, fmt):
|
||||||
|
self.rotf = fmt
|
||||||
|
r = re.escape(fmt).replace("%Y", "[0-9]{4}").replace("%j", "[0-9]{3}")
|
||||||
|
r = re.sub("%[mdHMSWU]", "[0-9]{2}", r)
|
||||||
|
self.rot_re = re.compile("(^|/)" + r + "$")
|
||||||
|
|
||||||
|
def all(self, ip, rem, sz, abspath):
|
||||||
|
self.chk_nup(ip)
|
||||||
|
self.chk_bup(ip)
|
||||||
|
self.chk_rem(rem)
|
||||||
|
if sz != -1:
|
||||||
|
self.chk_sz(sz)
|
||||||
|
|
||||||
|
ap2, vp2 = self.rot(abspath)
|
||||||
|
if abspath == ap2:
|
||||||
|
return ap2, rem
|
||||||
|
|
||||||
|
return ap2, ("{}/{}".format(rem, vp2) if rem else vp2)
|
||||||
|
|
||||||
|
def chk_sz(self, sz):
|
||||||
|
if self.smin is not None and sz < self.smin:
|
||||||
|
raise Pebkac(400, "file too small")
|
||||||
|
|
||||||
|
if self.smax is not None and sz > self.smax:
|
||||||
|
raise Pebkac(400, "file too big")
|
||||||
|
|
||||||
|
def chk_rem(self, rem):
|
||||||
|
if self.nosub and rem:
|
||||||
|
raise Pebkac(500, "no subdirectories allowed")
|
||||||
|
|
||||||
|
def rot(self, path):
|
||||||
|
if not self.rotf and not self.rotn:
|
||||||
|
return path, ""
|
||||||
|
|
||||||
|
if self.rotf:
|
||||||
|
path = path.rstrip("/\\")
|
||||||
|
if self.rot_re.search(path.replace("\\", "/")):
|
||||||
|
return path, ""
|
||||||
|
|
||||||
|
suf = datetime.utcnow().strftime(self.rotf)
|
||||||
|
if path:
|
||||||
|
path += "/"
|
||||||
|
|
||||||
|
return path + suf, suf
|
||||||
|
|
||||||
|
ret = self.dive(path, self.rotl)
|
||||||
|
if not ret:
|
||||||
|
raise Pebkac(500, "no available slots in volume")
|
||||||
|
|
||||||
|
d = ret[len(path) :].strip("/\\").replace("\\", "/")
|
||||||
|
return ret, d
|
||||||
|
|
||||||
|
def dive(self, path, lvs):
|
||||||
|
items = bos.listdir(path)
|
||||||
|
|
||||||
|
if not lvs:
|
||||||
|
# at leaf level
|
||||||
|
return None if len(items) >= self.rotn else ""
|
||||||
|
|
||||||
|
dirs = [int(x) for x in items if x and all(y in "1234567890" for y in x)]
|
||||||
|
dirs.sort()
|
||||||
|
|
||||||
|
if not dirs:
|
||||||
|
# no branches yet; make one
|
||||||
|
sub = os.path.join(path, "0")
|
||||||
|
bos.mkdir(sub)
|
||||||
|
else:
|
||||||
|
# try newest branch only
|
||||||
|
sub = os.path.join(path, str(dirs[-1]))
|
||||||
|
|
||||||
|
ret = self.dive(sub, lvs - 1)
|
||||||
|
if ret is not None:
|
||||||
|
return os.path.join(sub, ret)
|
||||||
|
|
||||||
|
if len(dirs) >= self.rotn:
|
||||||
|
# full branch or root
|
||||||
|
return None
|
||||||
|
|
||||||
|
# make a branch
|
||||||
|
sub = os.path.join(path, str(dirs[-1] + 1))
|
||||||
|
bos.mkdir(sub)
|
||||||
|
ret = self.dive(sub, lvs - 1)
|
||||||
|
if ret is None:
|
||||||
|
raise Pebkac(500, "rotation bug")
|
||||||
|
|
||||||
|
return os.path.join(sub, ret)
|
||||||
|
|
||||||
|
def nup(self, ip):
|
||||||
|
try:
|
||||||
|
self.nups[ip].append(time.time())
|
||||||
|
except:
|
||||||
|
self.nups[ip] = [time.time()]
|
||||||
|
|
||||||
|
def bup(self, ip, nbytes):
|
||||||
|
v = [time.time(), nbytes]
|
||||||
|
try:
|
||||||
|
self.bups[ip].append(v)
|
||||||
|
self.bupc[ip] += nbytes
|
||||||
|
except:
|
||||||
|
self.bups[ip] = [v]
|
||||||
|
self.bupc[ip] = nbytes
|
||||||
|
|
||||||
|
def chk_nup(self, ip):
|
||||||
|
if not self.nmax or ip not in self.nups:
|
||||||
|
return
|
||||||
|
|
||||||
|
nups = self.nups[ip]
|
||||||
|
cutoff = time.time() - self.nwin
|
||||||
|
while nups and nups[0] < cutoff:
|
||||||
|
nups.pop(0)
|
||||||
|
|
||||||
|
if len(nups) >= self.nmax:
|
||||||
|
raise Pebkac(429, "too many uploads")
|
||||||
|
|
||||||
|
def chk_bup(self, ip):
|
||||||
|
if not self.bmax or ip not in self.bups:
|
||||||
|
return
|
||||||
|
|
||||||
|
bups = self.bups[ip]
|
||||||
|
cutoff = time.time() - self.bwin
|
||||||
|
mark = self.bupc[ip]
|
||||||
|
while bups and bups[0][0] < cutoff:
|
||||||
|
mark -= bups.pop(0)[1]
|
||||||
|
|
||||||
|
self.bupc[ip] = mark
|
||||||
|
if mark >= self.bmax:
|
||||||
|
raise Pebkac(429, "ingress saturated")
|
||||||
|
|
||||||
|
|
||||||
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=[], uadm=[], flags={}):
|
def __init__(self, log, realpath, vpath, axs, flags):
|
||||||
|
self.log = log
|
||||||
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.axs = axs # type: AXS
|
||||||
self.uwrite = uwrite # users who can write this
|
self.flags = flags # config options
|
||||||
self.uadm = uadm # users who are regular admins
|
|
||||||
self.flags = flags # config switches
|
|
||||||
self.nodes = {} # child nodes
|
self.nodes = {} # child nodes
|
||||||
self.histtab = None # all realpath->histpath
|
self.histtab = None # all realpath->histpath
|
||||||
self.dbv = None # closest full/non-jump parent
|
self.dbv = None # closest full/non-jump parent
|
||||||
|
self.lim = None # type: Lim # upload limits; only set for dbv
|
||||||
|
|
||||||
if realpath:
|
if realpath:
|
||||||
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
|
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
|
||||||
self.all_vols = {vpath: self} # flattened recursive
|
self.all_vols = {vpath: self} # flattened recursive
|
||||||
|
self.aread = {}
|
||||||
|
self.awrite = {}
|
||||||
|
self.amove = {}
|
||||||
|
self.adel = {}
|
||||||
else:
|
else:
|
||||||
self.histpath = None
|
self.histpath = None
|
||||||
self.all_vols = None
|
self.all_vols = None
|
||||||
|
self.aread = None
|
||||||
|
self.awrite = None
|
||||||
|
self.amove = None
|
||||||
|
self.adel = 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 uadm flags".split()
|
for k in "realpath vpath axs flags".split()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -62,11 +251,10 @@ class VFS(object):
|
|||||||
return self.nodes[name].add(src, dst)
|
return self.nodes[name].add(src, dst)
|
||||||
|
|
||||||
vn = VFS(
|
vn = VFS(
|
||||||
|
self.log,
|
||||||
os.path.join(self.realpath, name) if self.realpath else None,
|
os.path.join(self.realpath, name) if self.realpath else None,
|
||||||
"{}/{}".format(self.vpath, name).lstrip("/"),
|
"{}/{}".format(self.vpath, name).lstrip("/"),
|
||||||
self.uread,
|
self.axs,
|
||||||
self.uwrite,
|
|
||||||
self.uadm,
|
|
||||||
self._copy_flags(name),
|
self._copy_flags(name),
|
||||||
)
|
)
|
||||||
vn.dbv = self.dbv or self
|
vn.dbv = self.dbv or self
|
||||||
@@ -79,7 +267,7 @@ 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(self.log, src, vp, AXS(), {})
|
||||||
vn.dbv = self.dbv or self
|
vn.dbv = self.dbv or self
|
||||||
self.nodes[dst] = vn
|
self.nodes[dst] = vn
|
||||||
return vn
|
return vn
|
||||||
@@ -119,27 +307,37 @@ class VFS(object):
|
|||||||
return [self, vpath]
|
return [self, vpath]
|
||||||
|
|
||||||
def can_access(self, vpath, uname):
|
def can_access(self, vpath, uname):
|
||||||
"""return [readable,writable]"""
|
# type: (str, str) -> tuple[bool, bool, bool, bool]
|
||||||
|
"""can Read,Write,Move,Delete"""
|
||||||
vn, _ = self._find(vpath)
|
vn, _ = self._find(vpath)
|
||||||
|
c = vn.axs
|
||||||
return [
|
return [
|
||||||
uname in vn.uread or "*" in vn.uread,
|
uname in c.uread or "*" in c.uread,
|
||||||
uname in vn.uwrite or "*" in vn.uwrite,
|
uname in c.uwrite or "*" in c.uwrite,
|
||||||
|
uname in c.umove or "*" in c.umove,
|
||||||
|
uname in c.udel or "*" in c.udel,
|
||||||
]
|
]
|
||||||
|
|
||||||
def get(self, vpath, uname, will_read, will_write):
|
def get(self, vpath, uname, will_read, will_write, will_move=False, will_del=False):
|
||||||
# type: (str, str, bool, bool) -> tuple[VFS, str]
|
# type: (str, str, bool, bool, 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)
|
||||||
|
c = vn.axs
|
||||||
|
|
||||||
if will_read and (uname not in vn.uread and "*" not in vn.uread):
|
for req, d, msg in [
|
||||||
raise Pebkac(403, "you don't have read-access for this location")
|
[will_read, c.uread, "read"],
|
||||||
|
[will_write, c.uwrite, "write"],
|
||||||
if will_write and (uname not in vn.uwrite and "*" not in vn.uwrite):
|
[will_move, c.umove, "move"],
|
||||||
raise Pebkac(403, "you don't have write-access for this location")
|
[will_del, c.udel, "delete"],
|
||||||
|
]:
|
||||||
|
if req and (uname not in d and "*" not in d) and uname != LEELOO_DALLAS:
|
||||||
|
m = "you don't have {}-access for this location"
|
||||||
|
raise Pebkac(403, m.format(msg))
|
||||||
|
|
||||||
return vn, rem
|
return vn, rem
|
||||||
|
|
||||||
def get_dbv(self, vrem):
|
def get_dbv(self, vrem):
|
||||||
|
# type: (str) -> tuple[VFS, str]
|
||||||
dbv = self.dbv
|
dbv = self.dbv
|
||||||
if not dbv:
|
if not dbv:
|
||||||
return self, vrem
|
return self, vrem
|
||||||
@@ -148,68 +346,58 @@ class VFS(object):
|
|||||||
vrem = "/".join([x for x in vrem if x])
|
vrem = "/".join([x for x in vrem if x])
|
||||||
return dbv, vrem
|
return dbv, vrem
|
||||||
|
|
||||||
def canonical(self, rem):
|
def canonical(self, rem, resolve=True):
|
||||||
"""returns the canonical path (fully-resolved absolute fs path)"""
|
"""returns the canonical path (fully-resolved absolute fs path)"""
|
||||||
rp = self.realpath
|
rp = self.realpath
|
||||||
if rem:
|
if rem:
|
||||||
rp += "/" + rem
|
rp += "/" + rem
|
||||||
|
|
||||||
try:
|
return absreal(rp) if resolve else rp
|
||||||
return fsdec(os.path.realpath(fsenc(rp)))
|
|
||||||
except:
|
|
||||||
if not WINDOWS:
|
|
||||||
raise
|
|
||||||
|
|
||||||
# cpython bug introduced in 3.8, still exists in 3.9.1;
|
def ls(self, rem, uname, scandir, permsets, lstat=False):
|
||||||
# some win7sp1 and win10:20H2 boxes cannot realpath a
|
# type: (str, str, bool, list[list[bool]], bool) -> tuple[str, str, dict[str, VFS]]
|
||||||
# networked drive letter such as b"n:" or b"n:\\"
|
|
||||||
#
|
|
||||||
# requirements to trigger:
|
|
||||||
# * bytestring (not unicode str)
|
|
||||||
# * just the drive letter (subfolders are ok)
|
|
||||||
# * networked drive (regular disks and vmhgfs are ok)
|
|
||||||
# * on an enterprise network (idk, cannot repro with samba)
|
|
||||||
#
|
|
||||||
# hits the following exceptions in succession:
|
|
||||||
# * access denied at L601: "path = _getfinalpathname(path)"
|
|
||||||
# * "cant concat str to bytes" at L621: "return path + tail"
|
|
||||||
#
|
|
||||||
return os.path.realpath(rp)
|
|
||||||
|
|
||||||
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)
|
||||||
real = list(statdir(nuprint, scandir, lstat, abspath))
|
real = list(statdir(self.log, scandir, lstat, abspath))
|
||||||
real.sort()
|
real.sort()
|
||||||
if not rem:
|
if not rem:
|
||||||
for name, vn2 in sorted(self.nodes.items()):
|
# no vfs nodes in the list of real inodes
|
||||||
ok = uname in vn2.uread or "*" in vn2.uread
|
real = [x for x in real if x[0] not in self.nodes]
|
||||||
|
|
||||||
if not ok and incl_wo:
|
for name, vn2 in sorted(self.nodes.items()):
|
||||||
ok = uname in vn2.uwrite or "*" in vn2.uwrite
|
ok = False
|
||||||
|
axs = vn2.axs
|
||||||
|
axs = [axs.uread, axs.uwrite, axs.umove, axs.udel]
|
||||||
|
for pset in permsets:
|
||||||
|
ok = True
|
||||||
|
for req, lst in zip(pset, axs):
|
||||||
|
if req and uname not in lst and "*" not in lst:
|
||||||
|
ok = False
|
||||||
|
if ok:
|
||||||
|
break
|
||||||
|
|
||||||
if ok:
|
if ok:
|
||||||
virt_vis[name] = vn2
|
virt_vis[name] = vn2
|
||||||
|
|
||||||
# no vfs nodes in the list of real inodes
|
|
||||||
real = [x for x in real if x[0] not in self.nodes]
|
|
||||||
|
|
||||||
return [abspath, real, virt_vis]
|
return [abspath, real, virt_vis]
|
||||||
|
|
||||||
def walk(self, rel, rem, seen, uname, dots, scandir, lstat):
|
def walk(self, rel, rem, seen, uname, permsets, 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(
|
fsroot, vfs_ls, vfs_virt = self.ls(rem, uname, scandir, permsets, lstat=lstat)
|
||||||
rem, uname, scandir, incl_wo=False, lstat=lstat
|
dbv, vrem = self.get_dbv(rem)
|
||||||
)
|
|
||||||
|
|
||||||
if seen and not fsroot.startswith(seen[-1]) and fsroot in seen:
|
if (
|
||||||
print("bailing from symlink loop,\n {}\n {}".format(seen[-1], fsroot))
|
seen
|
||||||
|
and (not fsroot.startswith(seen[-1]) or fsroot == seen[-1])
|
||||||
|
and fsroot in seen
|
||||||
|
):
|
||||||
|
m = "bailing from symlink loop,\n prev: {}\n curr: {}\n from: {}/{}"
|
||||||
|
self.log("vfs.walk", m.format(seen[-1], fsroot, self.vpath, rem), 3)
|
||||||
return
|
return
|
||||||
|
|
||||||
seen = seen[:] + [fsroot]
|
seen = seen[:] + [fsroot]
|
||||||
@@ -219,7 +407,7 @@ class VFS(object):
|
|||||||
rfiles.sort()
|
rfiles.sort()
|
||||||
rdirs.sort()
|
rdirs.sort()
|
||||||
|
|
||||||
yield rel, fsroot, rfiles, rdirs, vfs_virt
|
yield dbv, vrem, rel, fsroot, rfiles, rdirs, vfs_virt
|
||||||
|
|
||||||
for rdir, _ in rdirs:
|
for rdir, _ in rdirs:
|
||||||
if not dots and rdir.startswith("."):
|
if not dots and rdir.startswith("."):
|
||||||
@@ -227,7 +415,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, seen, uname, dots, scandir, lstat):
|
for x in self.walk(wrel, wrem, seen, uname, permsets, dots, scandir, lstat):
|
||||||
yield x
|
yield x
|
||||||
|
|
||||||
for n, vfs in sorted(vfs_virt.items()):
|
for n, vfs in sorted(vfs_virt.items()):
|
||||||
@@ -235,16 +423,19 @@ class VFS(object):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
wrel = (rel + "/" + n).lstrip("/")
|
wrel = (rel + "/" + n).lstrip("/")
|
||||||
for x in vfs.walk(wrel, "", seen, uname, dots, scandir, lstat):
|
for x in vfs.walk(wrel, "", seen, uname, permsets, 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(
|
f1 = "{0}.hist{0}up2k.".format(os.sep)
|
||||||
"", vrem, [], uname, dots, scandir, False
|
f2a = os.sep + "dir.txt"
|
||||||
):
|
f2b = "{0}.hist{0}".format(os.sep)
|
||||||
|
|
||||||
|
g = self.walk("", vrem, [], uname, [[True]], dots, scandir, False)
|
||||||
|
for _, _, vpath, apath, files, rd, vd in g:
|
||||||
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]
|
||||||
|
|
||||||
@@ -275,24 +466,20 @@ class VFS(object):
|
|||||||
del vd[x]
|
del vd[x]
|
||||||
|
|
||||||
# up2k filetring based on actual abspath
|
# up2k filetring based on actual abspath
|
||||||
files = [x for x in files if "{0}.hist{0}up2k.".format(os.sep) not in x[1]]
|
files = [
|
||||||
|
x
|
||||||
|
for x in files
|
||||||
|
if f1 not in x[1] and (not x[1].endswith(f2a) or f2b not in x[1])
|
||||||
|
]
|
||||||
|
|
||||||
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, writable, admin):
|
|
||||||
is_readable = False
|
|
||||||
if uname in self.uread or "*" in self.uread:
|
|
||||||
readable.append(self.vpath)
|
|
||||||
is_readable = True
|
|
||||||
|
|
||||||
if uname in self.uwrite or "*" in self.uwrite:
|
if WINDOWS:
|
||||||
writable.append(self.vpath)
|
re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
|
||||||
if is_readable:
|
else:
|
||||||
admin.append(self.vpath)
|
re_vol = re.compile(r"^([^:]*):([^:]*):(.*)$")
|
||||||
|
|
||||||
for _, vn in sorted(self.nodes.items()):
|
|
||||||
vn.user_tree(uname, readable, writable, admin)
|
|
||||||
|
|
||||||
|
|
||||||
class AuthSrv(object):
|
class AuthSrv(object):
|
||||||
@@ -304,11 +491,6 @@ class AuthSrv(object):
|
|||||||
self.warn_anonwrite = warn_anonwrite
|
self.warn_anonwrite = warn_anonwrite
|
||||||
self.line_ctr = 0
|
self.line_ctr = 0
|
||||||
|
|
||||||
if WINDOWS:
|
|
||||||
self.re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
|
|
||||||
else:
|
|
||||||
self.re_vol = re.compile(r"^([^:]*):([^:]*):(.*)$")
|
|
||||||
|
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
self.reload()
|
self.reload()
|
||||||
|
|
||||||
@@ -326,7 +508,8 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
yield prev, True
|
yield prev, True
|
||||||
|
|
||||||
def _parse_config_file(self, fd, user, mread, mwrite, madm, mflags, mount):
|
def _parse_config_file(self, fd, acct, daxs, mflags, mount):
|
||||||
|
# type: (any, str, dict[str, AXS], any, str) -> None
|
||||||
vol_src = None
|
vol_src = None
|
||||||
vol_dst = None
|
vol_dst = None
|
||||||
self.line_ctr = 0
|
self.line_ctr = 0
|
||||||
@@ -342,7 +525,7 @@ class AuthSrv(object):
|
|||||||
if vol_src is None:
|
if vol_src is None:
|
||||||
if ln.startswith("u "):
|
if ln.startswith("u "):
|
||||||
u, p = ln[2:].split(":", 1)
|
u, p = ln[2:].split(":", 1)
|
||||||
user[u] = p
|
acct[u] = p
|
||||||
else:
|
else:
|
||||||
vol_src = ln
|
vol_src = ln
|
||||||
continue
|
continue
|
||||||
@@ -353,50 +536,53 @@ class AuthSrv(object):
|
|||||||
raise Exception('invalid mountpoint "{}"'.format(vol_dst))
|
raise Exception('invalid mountpoint "{}"'.format(vol_dst))
|
||||||
|
|
||||||
# cfg files override arguments and previous files
|
# cfg files override arguments and previous files
|
||||||
vol_src = fsdec(os.path.abspath(fsenc(vol_src)))
|
vol_src = bos.path.abspath(vol_src)
|
||||||
vol_dst = vol_dst.strip("/")
|
vol_dst = vol_dst.strip("/")
|
||||||
mount[vol_dst] = vol_src
|
mount[vol_dst] = vol_src
|
||||||
mread[vol_dst] = []
|
daxs[vol_dst] = AXS()
|
||||||
mwrite[vol_dst] = []
|
|
||||||
madm[vol_dst] = []
|
|
||||||
mflags[vol_dst] = {}
|
mflags[vol_dst] = {}
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if len(ln) > 1:
|
try:
|
||||||
lvl, uname = ln.split(" ")
|
lvl, uname = ln.split(" ", 1)
|
||||||
else:
|
except:
|
||||||
lvl = ln
|
lvl = ln
|
||||||
uname = "*"
|
uname = "*"
|
||||||
|
|
||||||
self._read_vol_str(
|
if lvl == "a":
|
||||||
lvl,
|
m = "WARNING (config-file): permission flag 'a' is deprecated; please use 'rw' instead"
|
||||||
uname,
|
self.log(m, 1)
|
||||||
mread[vol_dst],
|
|
||||||
mwrite[vol_dst],
|
self._read_vol_str(lvl, uname, daxs[vol_dst], mflags[vol_dst])
|
||||||
madm[vol_dst],
|
|
||||||
mflags[vol_dst],
|
def _read_vol_str(self, lvl, uname, axs, flags):
|
||||||
)
|
# type: (str, str, AXS, any) -> None
|
||||||
|
if lvl.strip("crwmd"):
|
||||||
|
raise Exception("invalid volume flag: {},{}".format(lvl, uname))
|
||||||
|
|
||||||
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:
|
||||||
uname, cval = uname.split("=", 1)
|
uname, cval = uname.split("=", 1)
|
||||||
|
|
||||||
self._read_volflag(mf, uname, cval, False)
|
self._read_volflag(flags, uname, cval, False)
|
||||||
return
|
return
|
||||||
|
|
||||||
if uname == "":
|
if uname == "":
|
||||||
uname = "*"
|
uname = "*"
|
||||||
|
|
||||||
if lvl in "ra":
|
for un in uname.split(","):
|
||||||
mr.append(uname)
|
if "r" in lvl:
|
||||||
|
axs.uread[un] = 1
|
||||||
|
|
||||||
if lvl in "wa":
|
if "w" in lvl:
|
||||||
mw.append(uname)
|
axs.uwrite[un] = 1
|
||||||
|
|
||||||
if lvl == "a":
|
if "m" in lvl:
|
||||||
ma.append(uname)
|
axs.umove[un] = 1
|
||||||
|
|
||||||
|
if "d" in lvl:
|
||||||
|
axs.udel[un] = 1
|
||||||
|
|
||||||
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"]:
|
||||||
@@ -418,63 +604,69 @@ class AuthSrv(object):
|
|||||||
before finally building the VFS
|
before finally building the VFS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = {} # username:password
|
acct = {} # username:password
|
||||||
mread = {} # mountpoint:[username]
|
daxs = {} # type: dict[str, AXS]
|
||||||
mwrite = {} # mountpoint:[username]
|
|
||||||
madm = {} # mountpoint:[username]
|
|
||||||
mflags = {} # mountpoint:[flag]
|
mflags = {} # mountpoint:[flag]
|
||||||
mount = {} # dst:src (mountpoint:realpath)
|
mount = {} # dst:src (mountpoint:realpath)
|
||||||
|
|
||||||
if self.args.a:
|
if self.args.a:
|
||||||
# list of username:password
|
# list of username:password
|
||||||
for u, p in [x.split(":", 1) for x in self.args.a]:
|
for x in self.args.a:
|
||||||
user[u] = p
|
try:
|
||||||
|
u, p = x.split(":", 1)
|
||||||
|
acct[u] = p
|
||||||
|
except:
|
||||||
|
m = '\n invalid value "{}" for argument -a, must be username:password'
|
||||||
|
raise Exception(m.format(x))
|
||||||
|
|
||||||
if self.args.v:
|
if self.args.v:
|
||||||
# list of src:dst:permset:permset:...
|
# list of src:dst:permset:permset:...
|
||||||
# permset is [rwa]username or [c]flag
|
# permset is <rwmd>[,username][,username] or <c>,<flag>[=args]
|
||||||
for v_str in self.args.v:
|
for v_str in self.args.v:
|
||||||
m = self.re_vol.match(v_str)
|
m = re_vol.match(v_str)
|
||||||
if not m:
|
if not m:
|
||||||
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 and src.startswith("/"):
|
if WINDOWS:
|
||||||
src = "{}:\\{}".format(src[1], src[3:])
|
src = uncyg(src)
|
||||||
|
|
||||||
# print("\n".join([src, dst, perms]))
|
# print("\n".join([src, dst, perms]))
|
||||||
src = fsdec(os.path.abspath(fsenc(src)))
|
src = bos.path.abspath(src)
|
||||||
dst = dst.strip("/")
|
dst = dst.strip("/")
|
||||||
mount[dst] = src
|
mount[dst] = src
|
||||||
mread[dst] = []
|
daxs[dst] = AXS()
|
||||||
mwrite[dst] = []
|
|
||||||
madm[dst] = []
|
|
||||||
mflags[dst] = {}
|
mflags[dst] = {}
|
||||||
|
|
||||||
perms = perms.split(":")
|
for x in perms.split(":"):
|
||||||
for (lvl, uname) in [[x[0], x[1:]] for x in perms]:
|
lvl, uname = x.split(",", 1) if "," in x else [x, ""]
|
||||||
self._read_vol_str(
|
self._read_vol_str(lvl, uname, daxs[dst], mflags[dst])
|
||||||
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(
|
self._parse_config_file(f, acct, daxs, mflags, mount)
|
||||||
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))
|
self.log(m.format(cfg_fn, self.line_ctr), 1)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
# case-insensitive; normalize
|
||||||
|
if WINDOWS:
|
||||||
|
cased = {}
|
||||||
|
for k, v in mount.items():
|
||||||
|
cased[k] = absreal(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("."), "", ["*"], ["*"])
|
axs = AXS(["*"], ["*"], None, None)
|
||||||
|
vfs = VFS(self.log_func, bos.path.abspath("."), "", axs, {})
|
||||||
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(None, "")
|
vfs = VFS(self.log_func, None, "", AXS(), {})
|
||||||
vfs.flags["d2d"] = True
|
vfs.flags["d2d"] = True
|
||||||
|
|
||||||
maxdepth = 0
|
maxdepth = 0
|
||||||
@@ -485,26 +677,34 @@ 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(
|
vfs = VFS(self.log_func, mount[dst], dst, daxs[dst], mflags[dst])
|
||||||
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.axs = daxs[dst]
|
||||||
v.uwrite = mwrite[dst]
|
|
||||||
v.uadm = madm[dst]
|
|
||||||
v.flags = mflags[dst]
|
v.flags = mflags[dst]
|
||||||
v.dbv = None
|
v.dbv = None
|
||||||
|
|
||||||
vfs.all_vols = {}
|
vfs.all_vols = {}
|
||||||
vfs.get_all_vols(vfs.all_vols)
|
vfs.get_all_vols(vfs.all_vols)
|
||||||
|
|
||||||
|
for perm in "read write move del".split():
|
||||||
|
axs_key = "u" + perm
|
||||||
|
unames = ["*"] + list(acct.keys())
|
||||||
|
umap = {x: [] for x in unames}
|
||||||
|
for usr in unames:
|
||||||
|
for mp, vol in vfs.all_vols.items():
|
||||||
|
if usr in getattr(vol.axs, axs_key):
|
||||||
|
umap[usr].append(mp)
|
||||||
|
setattr(vfs, "a" + perm, umap)
|
||||||
|
|
||||||
|
all_users = {}
|
||||||
missing_users = {}
|
missing_users = {}
|
||||||
for d in [mread, mwrite]:
|
for axs in daxs.values():
|
||||||
for _, ul in d.items():
|
for d in [axs.uread, axs.uwrite, axs.umove, axs.udel]:
|
||||||
for usr in ul:
|
for usr in d.keys():
|
||||||
if usr != "*" and usr not in user:
|
all_users[usr] = 1
|
||||||
|
if usr != "*" and usr not in acct:
|
||||||
missing_users[usr] = 1
|
missing_users[usr] = 1
|
||||||
|
|
||||||
if missing_users:
|
if missing_users:
|
||||||
@@ -515,6 +715,9 @@ class AuthSrv(object):
|
|||||||
)
|
)
|
||||||
raise Exception("invalid config")
|
raise Exception("invalid config")
|
||||||
|
|
||||||
|
if LEELOO_DALLAS in all_users:
|
||||||
|
raise Exception("sorry, reserved username: " + LEELOO_DALLAS)
|
||||||
|
|
||||||
promote = []
|
promote = []
|
||||||
demote = []
|
demote = []
|
||||||
for vol in vfs.all_vols.values():
|
for vol in vfs.all_vols.values():
|
||||||
@@ -524,16 +727,11 @@ class AuthSrv(object):
|
|||||||
if vflag == "-":
|
if vflag == "-":
|
||||||
pass
|
pass
|
||||||
elif vflag:
|
elif vflag:
|
||||||
if WINDOWS and vflag.startswith("/"):
|
vol.histpath = uncyg(vflag) if WINDOWS else vflag
|
||||||
vflag = "{}:\\{}".format(vflag[1], vflag[3:])
|
|
||||||
vol.histpath = vflag
|
|
||||||
elif self.args.hist:
|
elif self.args.hist:
|
||||||
for nch in range(len(hid)):
|
for nch in range(len(hid)):
|
||||||
hpath = os.path.join(self.args.hist, hid[: nch + 1])
|
hpath = os.path.join(self.args.hist, hid[: nch + 1])
|
||||||
try:
|
bos.makedirs(hpath)
|
||||||
os.makedirs(hpath)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
powner = os.path.join(hpath, "owner.txt")
|
powner = os.path.join(hpath, "owner.txt")
|
||||||
try:
|
try:
|
||||||
@@ -553,9 +751,9 @@ class AuthSrv(object):
|
|||||||
vol.histpath = hpath
|
vol.histpath = hpath
|
||||||
break
|
break
|
||||||
|
|
||||||
vol.histpath = os.path.realpath(vol.histpath)
|
vol.histpath = absreal(vol.histpath)
|
||||||
if vol.dbv:
|
if vol.dbv:
|
||||||
if os.path.exists(os.path.join(vol.histpath, "up2k.db")):
|
if bos.path.exists(os.path.join(vol.histpath, "up2k.db")):
|
||||||
promote.append(vol)
|
promote.append(vol)
|
||||||
vol.dbv = None
|
vol.dbv = None
|
||||||
else:
|
else:
|
||||||
@@ -578,10 +776,55 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
vfs.histtab = {v.realpath: v.histpath for v in vfs.all_vols.values()}
|
vfs.histtab = {v.realpath: v.histpath for v in vfs.all_vols.values()}
|
||||||
|
|
||||||
|
for vol in vfs.all_vols.values():
|
||||||
|
lim = Lim()
|
||||||
|
use = False
|
||||||
|
|
||||||
|
if vol.flags.get("nosub"):
|
||||||
|
use = True
|
||||||
|
lim.nosub = True
|
||||||
|
|
||||||
|
v = vol.flags.get("sz")
|
||||||
|
if v:
|
||||||
|
use = True
|
||||||
|
lim.smin, lim.smax = [unhumanize(x) for x in v.split("-")]
|
||||||
|
|
||||||
|
v = vol.flags.get("rotn")
|
||||||
|
if v:
|
||||||
|
use = True
|
||||||
|
lim.rotn, lim.rotl = [int(x) for x in v.split(",")]
|
||||||
|
|
||||||
|
v = vol.flags.get("rotf")
|
||||||
|
if v:
|
||||||
|
use = True
|
||||||
|
lim.set_rotf(v)
|
||||||
|
|
||||||
|
v = vol.flags.get("maxn")
|
||||||
|
if v:
|
||||||
|
use = True
|
||||||
|
lim.nmax, lim.nwin = [int(x) for x in v.split(",")]
|
||||||
|
|
||||||
|
v = vol.flags.get("maxb")
|
||||||
|
if v:
|
||||||
|
use = True
|
||||||
|
lim.bmax, lim.bwin = [unhumanize(x) for x in v.split(",")]
|
||||||
|
|
||||||
|
if use:
|
||||||
|
vol.lim = lim
|
||||||
|
|
||||||
|
for vol in vfs.all_vols.values():
|
||||||
|
if "pk" in vol.flags and "gz" not in vol.flags and "xz" not in vol.flags:
|
||||||
|
vol.flags["gz"] = False # def.pk
|
||||||
|
|
||||||
|
if "scan" in vol.flags:
|
||||||
|
vol.flags["scan"] = int(vol.flags["scan"])
|
||||||
|
elif self.args.re_maxage:
|
||||||
|
vol.flags["scan"] = self.args.re_maxage
|
||||||
|
|
||||||
all_mte = {}
|
all_mte = {}
|
||||||
errors = False
|
errors = False
|
||||||
for vol in vfs.all_vols.values():
|
for vol in vfs.all_vols.values():
|
||||||
if (self.args.e2ds and vol.uwrite) or self.args.e2dsa:
|
if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa:
|
||||||
vol.flags["e2ds"] = True
|
vol.flags["e2ds"] = True
|
||||||
|
|
||||||
if self.args.e2d or "e2ds" in vol.flags:
|
if self.args.e2d or "e2ds" in vol.flags:
|
||||||
@@ -599,9 +842,11 @@ class AuthSrv(object):
|
|||||||
if k1 in vol.flags:
|
if k1 in vol.flags:
|
||||||
vol.flags[k2] = True
|
vol.flags[k2] = True
|
||||||
|
|
||||||
# default tag-list if unset
|
# default tag cfgs if unset
|
||||||
if "mte" not in vol.flags:
|
if "mte" not in vol.flags:
|
||||||
vol.flags["mte"] = self.args.mte
|
vol.flags["mte"] = self.args.mte
|
||||||
|
if "mth" not in vol.flags:
|
||||||
|
vol.flags["mth"] = self.args.mth
|
||||||
|
|
||||||
# append parsers from argv to volume-flags
|
# append parsers from argv to volume-flags
|
||||||
self._read_volflag(vol.flags, "mtp", self.args.mtp, True)
|
self._read_volflag(vol.flags, "mtp", self.args.mtp, True)
|
||||||
@@ -670,6 +915,27 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
vfs.bubble_flags()
|
vfs.bubble_flags()
|
||||||
|
|
||||||
|
m = "volumes and permissions:\n"
|
||||||
|
for v in vfs.all_vols.values():
|
||||||
|
if not self.warn_anonwrite:
|
||||||
|
break
|
||||||
|
|
||||||
|
m += '\n\033[36m"/{}" \033[33m{}\033[0m'.format(v.vpath, v.realpath)
|
||||||
|
for txt, attr in [
|
||||||
|
[" read", "uread"],
|
||||||
|
[" write", "uwrite"],
|
||||||
|
[" move", "umove"],
|
||||||
|
["delete", "udel"],
|
||||||
|
]:
|
||||||
|
u = list(sorted(getattr(v.axs, attr).keys()))
|
||||||
|
u = ", ".join("\033[35meverybody\033[0m" if x == "*" else x for x in u)
|
||||||
|
u = u if u else "\033[36m--none--\033[0m"
|
||||||
|
m += "\n| {}: {}".format(txt, u)
|
||||||
|
m += "\n"
|
||||||
|
|
||||||
|
if self.warn_anonwrite and not self.args.no_voldump:
|
||||||
|
self.log(m)
|
||||||
|
|
||||||
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:
|
||||||
@@ -681,11 +947,13 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
self.vfs = vfs
|
self.vfs = vfs
|
||||||
self.user = user
|
self.acct = acct
|
||||||
self.iuser = {v: k for k, v in user.items()}
|
self.iacct = {v: k for k, v in acct.items()}
|
||||||
|
|
||||||
# import pprint
|
self.re_pwd = None
|
||||||
# pprint.pprint({"usr": user, "rd": mread, "wr": mwrite, "mnt": mount})
|
pwds = [re.escape(x) for x in self.iacct.keys()]
|
||||||
|
if pwds:
|
||||||
|
self.re_pwd = re.compile("=(" + "|".join(pwds) + ")([]&; ]|$)")
|
||||||
|
|
||||||
def dbg_ls(self):
|
def dbg_ls(self):
|
||||||
users = self.args.ls
|
users = self.args.ls
|
||||||
@@ -704,12 +972,12 @@ class AuthSrv(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if users == "**":
|
if users == "**":
|
||||||
users = list(self.user.keys()) + ["*"]
|
users = list(self.acct.keys()) + ["*"]
|
||||||
else:
|
else:
|
||||||
users = [users]
|
users = [users]
|
||||||
|
|
||||||
for u in users:
|
for u in users:
|
||||||
if u not in self.user and u != "*":
|
if u not in self.acct and u != "*":
|
||||||
raise Exception("user not found: " + u)
|
raise Exception("user not found: " + u)
|
||||||
|
|
||||||
if vols == "*":
|
if vols == "*":
|
||||||
@@ -725,8 +993,10 @@ class AuthSrv(object):
|
|||||||
raise Exception("volume not found: " + v)
|
raise Exception("volume not found: " + v)
|
||||||
|
|
||||||
self.log({"users": users, "vols": vols, "flags": flags})
|
self.log({"users": users, "vols": vols, "flags": flags})
|
||||||
|
m = "/{}: read({}) write({}) move({}) del({})"
|
||||||
for k, v in self.vfs.all_vols.items():
|
for k, v in self.vfs.all_vols.items():
|
||||||
self.log("/{}: read({}) write({})".format(k, v.uread, v.uwrite))
|
vc = v.axs
|
||||||
|
self.log(m.format(k, vc.uread, vc.uwrite, vc.umove, vc.udel))
|
||||||
|
|
||||||
flag_v = "v" in flags
|
flag_v = "v" in flags
|
||||||
flag_ln = "ln" in flags
|
flag_ln = "ln" in flags
|
||||||
@@ -740,13 +1010,15 @@ class AuthSrv(object):
|
|||||||
for u in users:
|
for u in users:
|
||||||
self.log("checking /{} as {}".format(v, u))
|
self.log("checking /{} as {}".format(v, u))
|
||||||
try:
|
try:
|
||||||
vn, _ = self.vfs.get(v, u, True, False)
|
vn, _ = self.vfs.get(v, u, True, False, False, False)
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
atop = vn.realpath
|
atop = vn.realpath
|
||||||
g = vn.walk("", "", [], u, True, not self.args.no_scandir, False)
|
g = vn.walk(
|
||||||
for vpath, apath, files, _, _ in g:
|
vn.vpath, "", [], u, [[True]], True, not self.args.no_scandir, False
|
||||||
|
)
|
||||||
|
for _, _, vpath, apath, files, _, _ in g:
|
||||||
fnames = [n[0] for n in files]
|
fnames = [n[0] for n in files]
|
||||||
vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames
|
vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames
|
||||||
vpaths = [vtop + x for x in vpaths]
|
vpaths = [vtop + x for x in vpaths]
|
||||||
@@ -766,7 +1038,7 @@ class AuthSrv(object):
|
|||||||
msg = [x[1] for x in files]
|
msg = [x[1] for x in files]
|
||||||
|
|
||||||
if msg:
|
if msg:
|
||||||
nuprint("\n".join(msg))
|
self.log("\n" + "\n".join(msg))
|
||||||
|
|
||||||
if n_bads and flag_p:
|
if n_bads and flag_p:
|
||||||
raise Exception("found symlink leaving volume, and strict is set")
|
raise Exception("found symlink leaving volume, and strict is set")
|
||||||
|
|||||||
0
copyparty/bos/__init__.py
Normal file
0
copyparty/bos/__init__.py
Normal file
59
copyparty/bos/bos.py
Normal file
59
copyparty/bos/bos.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
from ..util import fsenc, fsdec
|
||||||
|
from . import path
|
||||||
|
|
||||||
|
|
||||||
|
# grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c
|
||||||
|
# printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')"
|
||||||
|
|
||||||
|
|
||||||
|
def chmod(p, mode):
|
||||||
|
return os.chmod(fsenc(p), mode)
|
||||||
|
|
||||||
|
|
||||||
|
def listdir(p="."):
|
||||||
|
return [fsdec(x) for x in os.listdir(fsenc(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def lstat(p):
|
||||||
|
return os.lstat(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def makedirs(name, mode=0o755, exist_ok=True):
|
||||||
|
bname = fsenc(name)
|
||||||
|
try:
|
||||||
|
os.makedirs(bname, mode=mode)
|
||||||
|
except:
|
||||||
|
if not exist_ok or not os.path.isdir(bname):
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def mkdir(p, mode=0o755):
|
||||||
|
return os.mkdir(fsenc(p), mode=mode)
|
||||||
|
|
||||||
|
|
||||||
|
def rename(src, dst):
|
||||||
|
return os.rename(fsenc(src), fsenc(dst))
|
||||||
|
|
||||||
|
|
||||||
|
def replace(src, dst):
|
||||||
|
return os.replace(fsenc(src), fsenc(dst))
|
||||||
|
|
||||||
|
|
||||||
|
def rmdir(p):
|
||||||
|
return os.rmdir(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def stat(p):
|
||||||
|
return os.stat(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def unlink(p):
|
||||||
|
return os.unlink(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def utime(p, times=None):
|
||||||
|
return os.utime(fsenc(p), times)
|
||||||
33
copyparty/bos/path.py
Normal file
33
copyparty/bos/path.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
from ..util import fsenc, fsdec
|
||||||
|
|
||||||
|
|
||||||
|
def abspath(p):
|
||||||
|
return fsdec(os.path.abspath(fsenc(p)))
|
||||||
|
|
||||||
|
|
||||||
|
def exists(p):
|
||||||
|
return os.path.exists(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def getmtime(p):
|
||||||
|
return os.path.getmtime(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def getsize(p):
|
||||||
|
return os.path.getsize(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def isdir(p):
|
||||||
|
return os.path.isdir(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def islink(p):
|
||||||
|
return os.path.islink(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def realpath(p):
|
||||||
|
return fsdec(os.path.realpath(fsenc(p)))
|
||||||
@@ -4,17 +4,11 @@ from __future__ import print_function, unicode_literals
|
|||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from .__init__ import PY2, WINDOWS, VT100
|
|
||||||
from .broker_util import try_exec
|
from .broker_util import try_exec
|
||||||
from .broker_mpw import MpWorker
|
from .broker_mpw import MpWorker
|
||||||
from .util import mp
|
from .util import mp
|
||||||
|
|
||||||
|
|
||||||
if PY2 and not WINDOWS:
|
|
||||||
from multiprocessing.reduction import ForkingPickler
|
|
||||||
from StringIO import StringIO as MemesIO # pylint: disable=import-error
|
|
||||||
|
|
||||||
|
|
||||||
class BrokerMp(object):
|
class BrokerMp(object):
|
||||||
"""external api; manages MpWorkers"""
|
"""external api; manages MpWorkers"""
|
||||||
|
|
||||||
@@ -28,24 +22,19 @@ class BrokerMp(object):
|
|||||||
self.retpend_mutex = threading.Lock()
|
self.retpend_mutex = threading.Lock()
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
|
|
||||||
cores = self.args.j
|
self.num_workers = self.args.j or mp.cpu_count()
|
||||||
if not cores:
|
self.log("broker", "booting {} subprocesses".format(self.num_workers))
|
||||||
cores = mp.cpu_count()
|
for n in range(1, self.num_workers + 1):
|
||||||
|
|
||||||
self.log("broker", "booting {} subprocesses".format(cores))
|
|
||||||
for n in range(cores):
|
|
||||||
q_pend = mp.Queue(1)
|
q_pend = mp.Queue(1)
|
||||||
q_yield = mp.Queue(64)
|
q_yield = mp.Queue(64)
|
||||||
|
|
||||||
proc = mp.Process(target=MpWorker, args=(q_pend, q_yield, self.args, n))
|
proc = mp.Process(target=MpWorker, args=(q_pend, q_yield, self.args, n))
|
||||||
proc.q_pend = q_pend
|
proc.q_pend = q_pend
|
||||||
proc.q_yield = q_yield
|
proc.q_yield = q_yield
|
||||||
proc.nid = n
|
|
||||||
proc.clients = {}
|
proc.clients = {}
|
||||||
proc.workload = 0
|
|
||||||
|
|
||||||
thr = threading.Thread(
|
thr = threading.Thread(
|
||||||
target=self.collector, args=(proc,), name="mp-collector"
|
target=self.collector, args=(proc,), name="mp-sink-{}".format(n)
|
||||||
)
|
)
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
@@ -53,13 +42,6 @@ class BrokerMp(object):
|
|||||||
self.procs.append(proc)
|
self.procs.append(proc)
|
||||||
proc.start()
|
proc.start()
|
||||||
|
|
||||||
if not self.args.q:
|
|
||||||
thr = threading.Thread(
|
|
||||||
target=self.debug_load_balancer, name="mp-dbg-loadbalancer"
|
|
||||||
)
|
|
||||||
thr.daemon = True
|
|
||||||
thr.start()
|
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.log("broker", "shutting down")
|
self.log("broker", "shutting down")
|
||||||
for n, proc in enumerate(self.procs):
|
for n, proc in enumerate(self.procs):
|
||||||
@@ -89,20 +71,6 @@ class BrokerMp(object):
|
|||||||
if dest == "log":
|
if dest == "log":
|
||||||
self.log(*args)
|
self.log(*args)
|
||||||
|
|
||||||
elif dest == "workload":
|
|
||||||
with self.mutex:
|
|
||||||
proc.workload = args[0]
|
|
||||||
|
|
||||||
elif dest == "httpdrop":
|
|
||||||
addr = args[0]
|
|
||||||
|
|
||||||
with self.mutex:
|
|
||||||
del proc.clients[addr]
|
|
||||||
if not proc.clients:
|
|
||||||
proc.workload = 0
|
|
||||||
|
|
||||||
self.hub.tcpsrv.num_clients.add(-1)
|
|
||||||
|
|
||||||
elif dest == "retq":
|
elif dest == "retq":
|
||||||
# response from previous ipc call
|
# response from previous ipc call
|
||||||
with self.retpend_mutex:
|
with self.retpend_mutex:
|
||||||
@@ -128,38 +96,12 @@ class BrokerMp(object):
|
|||||||
returns a Queue object which eventually contains the response if want_retval
|
returns a Queue object which eventually contains the response if want_retval
|
||||||
(not-impl here since nothing uses it yet)
|
(not-impl here since nothing uses it yet)
|
||||||
"""
|
"""
|
||||||
if dest == "httpconn":
|
if dest == "listen":
|
||||||
sck, addr = args
|
for p in self.procs:
|
||||||
sck2 = sck
|
p.q_pend.put([0, dest, [args[0], len(self.procs)]])
|
||||||
if PY2:
|
|
||||||
buf = MemesIO()
|
|
||||||
ForkingPickler(buf).dump(sck)
|
|
||||||
sck2 = buf.getvalue()
|
|
||||||
|
|
||||||
proc = sorted(self.procs, key=lambda x: x.workload)[0]
|
elif dest == "cb_httpsrv_up":
|
||||||
proc.q_pend.put([0, dest, [sck2, addr]])
|
self.hub.cb_httpsrv_up()
|
||||||
|
|
||||||
with self.mutex:
|
|
||||||
proc.clients[addr] = 50
|
|
||||||
proc.workload += 50
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise Exception("what is " + str(dest))
|
raise Exception("what is " + str(dest))
|
||||||
|
|
||||||
def debug_load_balancer(self):
|
|
||||||
fmt = "\033[1m{}\033[0;36m{:4}\033[0m "
|
|
||||||
if not VT100:
|
|
||||||
fmt = "({}{:4})"
|
|
||||||
|
|
||||||
last = ""
|
|
||||||
while self.procs:
|
|
||||||
msg = ""
|
|
||||||
for proc in self.procs:
|
|
||||||
msg += fmt.format(len(proc.clients), proc.workload)
|
|
||||||
|
|
||||||
if msg != last:
|
|
||||||
last = msg
|
|
||||||
with self.hub.log_mutex:
|
|
||||||
print(msg)
|
|
||||||
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
# 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 signal
|
import signal
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from .__init__ import PY2, WINDOWS
|
|
||||||
from .broker_util import ExceptionalQueue
|
from .broker_util import ExceptionalQueue
|
||||||
from .httpsrv import HttpSrv
|
from .httpsrv import HttpSrv
|
||||||
from .util import FAKE_MP
|
from .util import FAKE_MP
|
||||||
|
from copyparty.authsrv import AuthSrv
|
||||||
if PY2 and not WINDOWS:
|
|
||||||
import pickle # nosec
|
|
||||||
|
|
||||||
|
|
||||||
class MpWorker(object):
|
class MpWorker(object):
|
||||||
@@ -25,22 +20,23 @@ class MpWorker(object):
|
|||||||
self.args = args
|
self.args = args
|
||||||
self.n = n
|
self.n = n
|
||||||
|
|
||||||
|
self.log = self._log_disabled if args.q and not args.lo else self._log_enabled
|
||||||
|
|
||||||
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_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)
|
for sig in [signal.SIGINT, signal.SIGTERM]:
|
||||||
|
signal.signal(sig, self.signal_handler)
|
||||||
|
|
||||||
# starting to look like a good idea
|
# starting to look like a good idea
|
||||||
self.asrv = AuthSrv(args, None, False)
|
self.asrv = AuthSrv(args, None, False)
|
||||||
|
|
||||||
# instantiate all services here (TODO: inheritance?)
|
# instantiate all services here (TODO: inheritance?)
|
||||||
self.httpsrv = HttpSrv(self, True)
|
self.httpsrv = HttpSrv(self, n)
|
||||||
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
|
||||||
@@ -49,19 +45,19 @@ class MpWorker(object):
|
|||||||
thr.start()
|
thr.start()
|
||||||
thr.join()
|
thr.join()
|
||||||
|
|
||||||
def signal_handler(self, signal, frame):
|
def signal_handler(self, sig, frame):
|
||||||
# print('k')
|
# print('k')
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def log(self, src, msg, c=0):
|
def _log_enabled(self, src, msg, c=0):
|
||||||
self.q_yield.put([0, "log", [src, msg, c]])
|
self.q_yield.put([0, "log", [src, msg, c]])
|
||||||
|
|
||||||
|
def _log_disabled(self, src, msg, c=0):
|
||||||
|
pass
|
||||||
|
|
||||||
def logw(self, msg, c=0):
|
def logw(self, msg, c=0):
|
||||||
self.log("mp{}".format(self.n), msg, c)
|
self.log("mp{}".format(self.n), msg, c)
|
||||||
|
|
||||||
def httpdrop(self, addr):
|
|
||||||
self.q_yield.put([0, "httpdrop", [addr]])
|
|
||||||
|
|
||||||
def main(self):
|
def main(self):
|
||||||
while True:
|
while True:
|
||||||
retq_id, dest, args = self.q_pend.get()
|
retq_id, dest, args = self.q_pend.get()
|
||||||
@@ -73,24 +69,8 @@ class MpWorker(object):
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
return
|
return
|
||||||
|
|
||||||
elif dest == "httpconn":
|
elif dest == "listen":
|
||||||
sck, addr = args
|
self.httpsrv.listen(args[0], args[1])
|
||||||
if PY2:
|
|
||||||
sck = pickle.loads(sck) # nosec
|
|
||||||
|
|
||||||
if self.args.log_conn:
|
|
||||||
self.log("%s %s" % addr, "|%sC-qpop" % ("-" * 4,), c="1;30")
|
|
||||||
|
|
||||||
self.httpsrv.accept(sck, addr)
|
|
||||||
|
|
||||||
with self.mutex:
|
|
||||||
if not self.workload_thr_alive:
|
|
||||||
self.workload_thr_alive = True
|
|
||||||
thr = threading.Thread(
|
|
||||||
target=self.thr_workload, name="mpw-workload"
|
|
||||||
)
|
|
||||||
thr.daemon = True
|
|
||||||
thr.start()
|
|
||||||
|
|
||||||
elif dest == "retq":
|
elif dest == "retq":
|
||||||
# response from previous ipc call
|
# response from previous ipc call
|
||||||
@@ -114,16 +94,3 @@ class MpWorker(object):
|
|||||||
|
|
||||||
self.q_yield.put([retq_id, dest, args])
|
self.q_yield.put([retq_id, dest, args])
|
||||||
return retq
|
return retq
|
||||||
|
|
||||||
def thr_workload(self):
|
|
||||||
"""announce workloads to MpSrv (the mp controller / loadbalancer)"""
|
|
||||||
# avoid locking in extract_filedata by tracking difference here
|
|
||||||
while True:
|
|
||||||
time.sleep(0.2)
|
|
||||||
with self.mutex:
|
|
||||||
if self.httpsrv.num_clients() == 0:
|
|
||||||
# no clients rn, termiante thread
|
|
||||||
self.workload_thr_alive = False
|
|
||||||
return
|
|
||||||
|
|
||||||
self.q_yield.put([0, "workload", [self.httpsrv.workload]])
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ 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
|
||||||
|
|
||||||
@@ -18,10 +17,10 @@ class BrokerThr(object):
|
|||||||
self.asrv = hub.asrv
|
self.asrv = hub.asrv
|
||||||
|
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
|
self.num_workers = 1
|
||||||
|
|
||||||
# instantiate all services here (TODO: inheritance?)
|
# instantiate all services here (TODO: inheritance?)
|
||||||
self.httpsrv = HttpSrv(self)
|
self.httpsrv = HttpSrv(self, None)
|
||||||
self.httpsrv.disconnect_func = self.httpdrop
|
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
# self.log("broker", "shutting down")
|
# self.log("broker", "shutting down")
|
||||||
@@ -29,12 +28,8 @@ class BrokerThr(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def put(self, want_retval, dest, *args):
|
def put(self, want_retval, dest, *args):
|
||||||
if dest == "httpconn":
|
if dest == "listen":
|
||||||
sck, addr = args
|
self.httpsrv.listen(args[0], 1)
|
||||||
if self.args.log_conn:
|
|
||||||
self.log("%s %s" % addr, "|%sC-qpop" % ("-" * 4,), c="1;30")
|
|
||||||
|
|
||||||
self.httpsrv.accept(sck, addr)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# new ipc invoking managed service in hub
|
# new ipc invoking managed service in hub
|
||||||
@@ -51,6 +46,3 @@ class BrokerThr(object):
|
|||||||
retq = ExceptionalQueue(1)
|
retq = ExceptionalQueue(1)
|
||||||
retq.put(rv)
|
retq.put(rv)
|
||||||
return retq
|
return retq
|
||||||
|
|
||||||
def httpdrop(self, addr):
|
|
||||||
self.hub.tcpsrv.num_clients.add(-1)
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@ from __future__ import print_function, unicode_literals
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
@@ -35,7 +34,6 @@ class HttpConn(object):
|
|||||||
|
|
||||||
self.args = hsrv.args
|
self.args = hsrv.args
|
||||||
self.asrv = hsrv.asrv
|
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
|
enth = HAVE_PIL and not self.args.no_thumb
|
||||||
@@ -44,8 +42,8 @@ class HttpConn(object):
|
|||||||
|
|
||||||
self.t0 = time.time()
|
self.t0 = time.time()
|
||||||
self.stopping = False
|
self.stopping = False
|
||||||
|
self.nreq = 0
|
||||||
self.nbyte = 0
|
self.nbyte = 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.lf_url = re.compile(self.args.lf_url) if self.args.lf_url else None
|
||||||
@@ -184,11 +182,7 @@ class HttpConn(object):
|
|||||||
self.sr = Unrecv(self.s)
|
self.sr = Unrecv(self.s)
|
||||||
|
|
||||||
while not self.stopping:
|
while not self.stopping:
|
||||||
if self.is_mp:
|
self.nreq += 1
|
||||||
self.workload += 50
|
|
||||||
if self.workload >= 2 ** 31:
|
|
||||||
self.workload = 100
|
|
||||||
|
|
||||||
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 math
|
||||||
|
import base64
|
||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
@@ -24,10 +26,16 @@ except ImportError:
|
|||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
from .__init__ import E, MACOS
|
from .__init__ import E, PY2, MACOS
|
||||||
from .authsrv import AuthSrv
|
from .util import spack, min_ex, start_stackmon, start_log_thrs
|
||||||
|
from .bos import bos
|
||||||
from .httpconn import HttpConn
|
from .httpconn import HttpConn
|
||||||
|
|
||||||
|
if PY2:
|
||||||
|
import Queue as queue
|
||||||
|
else:
|
||||||
|
import queue
|
||||||
|
|
||||||
|
|
||||||
class HttpSrv(object):
|
class HttpSrv(object):
|
||||||
"""
|
"""
|
||||||
@@ -35,19 +43,28 @@ 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, is_mp=False):
|
def __init__(self, broker, nid):
|
||||||
self.broker = broker
|
self.broker = broker
|
||||||
self.is_mp = is_mp
|
self.nid = nid
|
||||||
self.args = broker.args
|
self.args = broker.args
|
||||||
self.log = broker.log
|
self.log = broker.log
|
||||||
self.asrv = broker.asrv
|
self.asrv = broker.asrv
|
||||||
|
|
||||||
self.disconnect_func = None
|
self.name = "httpsrv" + ("-n{}-i{:x}".format(nid, os.getpid()) if nid else "")
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
|
self.stopping = False
|
||||||
|
|
||||||
self.clients = {}
|
self.tp_nthr = 0 # actual
|
||||||
self.workload = 0
|
self.tp_ncli = 0 # fading
|
||||||
self.workload_thr_alive = False
|
self.tp_time = None # latest worker collect
|
||||||
|
self.tp_q = None if self.args.no_htp else queue.LifoQueue()
|
||||||
|
|
||||||
|
self.srvs = []
|
||||||
|
self.ncli = 0 # exact
|
||||||
|
self.clients = {} # laggy
|
||||||
|
self.nclimax = 0
|
||||||
|
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"))
|
||||||
@@ -57,29 +74,162 @@ class HttpSrv(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cert_path = os.path.join(E.cfg, "cert.pem")
|
cert_path = os.path.join(E.cfg, "cert.pem")
|
||||||
if os.path.exists(cert_path):
|
if bos.path.exists(cert_path):
|
||||||
self.cert_path = cert_path
|
self.cert_path = cert_path
|
||||||
else:
|
else:
|
||||||
self.cert_path = None
|
self.cert_path = None
|
||||||
|
|
||||||
|
if self.tp_q:
|
||||||
|
self.start_threads(4)
|
||||||
|
|
||||||
|
name = "httpsrv-scaler" + ("-{}".format(nid) if nid else "")
|
||||||
|
t = threading.Thread(target=self.thr_scaler, name=name)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
if nid:
|
||||||
|
if self.args.stackmon:
|
||||||
|
start_stackmon(self.args.stackmon, nid)
|
||||||
|
|
||||||
|
if self.args.log_thrs:
|
||||||
|
start_log_thrs(self.log, self.args.log_thrs, nid)
|
||||||
|
|
||||||
|
def start_threads(self, n):
|
||||||
|
self.tp_nthr += n
|
||||||
|
if self.args.log_htp:
|
||||||
|
self.log(self.name, "workers += {} = {}".format(n, self.tp_nthr), 6)
|
||||||
|
|
||||||
|
for _ in range(n):
|
||||||
|
thr = threading.Thread(
|
||||||
|
target=self.thr_poolw,
|
||||||
|
name=self.name + "-poolw",
|
||||||
|
)
|
||||||
|
thr.daemon = True
|
||||||
|
thr.start()
|
||||||
|
|
||||||
|
def stop_threads(self, n):
|
||||||
|
self.tp_nthr -= n
|
||||||
|
if self.args.log_htp:
|
||||||
|
self.log(self.name, "workers -= {} = {}".format(n, self.tp_nthr), 6)
|
||||||
|
|
||||||
|
for _ in range(n):
|
||||||
|
self.tp_q.put(None)
|
||||||
|
|
||||||
|
def thr_scaler(self):
|
||||||
|
while True:
|
||||||
|
time.sleep(2 if self.tp_ncli else 30)
|
||||||
|
with self.mutex:
|
||||||
|
self.tp_ncli = max(self.ncli, self.tp_ncli - 2)
|
||||||
|
if self.tp_nthr > self.tp_ncli + 8:
|
||||||
|
self.stop_threads(4)
|
||||||
|
|
||||||
|
def listen(self, sck, nlisteners):
|
||||||
|
ip, port = sck.getsockname()
|
||||||
|
self.srvs.append(sck)
|
||||||
|
self.nclimax = math.ceil(self.args.nc * 1.0 / nlisteners)
|
||||||
|
t = threading.Thread(
|
||||||
|
target=self.thr_listen,
|
||||||
|
args=(sck,),
|
||||||
|
name="httpsrv-n{}-listen-{}-{}".format(self.nid or "0", ip, port),
|
||||||
|
)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def thr_listen(self, srv_sck):
|
||||||
|
"""listens on a shared tcp server"""
|
||||||
|
ip, port = srv_sck.getsockname()
|
||||||
|
fno = srv_sck.fileno()
|
||||||
|
msg = "subscribed @ {}:{} f{}".format(ip, port, fno)
|
||||||
|
self.log(self.name, msg)
|
||||||
|
self.broker.put(False, "cb_httpsrv_up")
|
||||||
|
while not self.stopping:
|
||||||
|
if self.args.log_conn:
|
||||||
|
self.log(self.name, "|%sC-ncli" % ("-" * 1,), c="1;30")
|
||||||
|
|
||||||
|
if self.ncli >= self.nclimax:
|
||||||
|
self.log(self.name, "at connection limit; waiting", 3)
|
||||||
|
while self.ncli >= self.nclimax:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
if self.args.log_conn:
|
||||||
|
self.log(self.name, "|%sC-acc1" % ("-" * 2,), c="1;30")
|
||||||
|
|
||||||
|
try:
|
||||||
|
sck, addr = srv_sck.accept()
|
||||||
|
except (OSError, socket.error) as ex:
|
||||||
|
self.log(self.name, "accept({}): {}".format(fno, ex), c=6)
|
||||||
|
time.sleep(0.02)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.args.log_conn:
|
||||||
|
m = "|{}C-acc2 \033[0;36m{} \033[3{}m{}".format(
|
||||||
|
"-" * 3, ip, port % 8, port
|
||||||
|
)
|
||||||
|
self.log("%s %s" % addr, m, c="1;30")
|
||||||
|
|
||||||
|
self.accept(sck, addr)
|
||||||
|
|
||||||
def accept(self, sck, addr):
|
def accept(self, sck, addr):
|
||||||
"""takes an incoming tcp connection and creates a thread to handle it"""
|
"""takes an incoming tcp connection and creates a thread to handle it"""
|
||||||
if self.args.log_conn:
|
now = time.time()
|
||||||
self.log("%s %s" % addr, "|%sC-cthr" % ("-" * 5,), c="1;30")
|
|
||||||
|
if now - (self.tp_time or now) > 300:
|
||||||
|
m = "httpserver threadpool died: tpt {:.2f}, now {:.2f}, nthr {}, ncli {}"
|
||||||
|
self.log(self.name, m.format(self.tp_time, now, self.tp_nthr, self.ncli), 1)
|
||||||
|
self.tp_time = None
|
||||||
|
self.tp_q = None
|
||||||
|
|
||||||
|
with self.mutex:
|
||||||
|
self.ncli += 1
|
||||||
|
if self.tp_q:
|
||||||
|
self.tp_time = self.tp_time or now
|
||||||
|
self.tp_ncli = max(self.tp_ncli, self.ncli)
|
||||||
|
if self.tp_nthr < self.ncli + 4:
|
||||||
|
self.start_threads(8)
|
||||||
|
|
||||||
|
self.tp_q.put((sck, addr))
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.args.no_htp:
|
||||||
|
m = "looks like the httpserver threadpool died; please make an issue on github and tell me the story of how you pulled that off, thanks and dog bless\n"
|
||||||
|
self.log(self.name, m, 1)
|
||||||
|
|
||||||
thr = threading.Thread(
|
thr = threading.Thread(
|
||||||
target=self.thr_client,
|
target=self.thr_client,
|
||||||
args=(sck, addr),
|
args=(sck, addr),
|
||||||
name="httpsrv-{}-{}".format(addr[0].split(".", 2)[-1][-6:], addr[1]),
|
name="httpconn-{}-{}".format(addr[0].split(".", 2)[-1][-6:], addr[1]),
|
||||||
)
|
)
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
def num_clients(self):
|
def thr_poolw(self):
|
||||||
with self.mutex:
|
while True:
|
||||||
return len(self.clients)
|
task = self.tp_q.get()
|
||||||
|
if not task:
|
||||||
|
break
|
||||||
|
|
||||||
|
with self.mutex:
|
||||||
|
self.tp_time = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
sck, addr = task
|
||||||
|
me = threading.current_thread()
|
||||||
|
me.name = "httpconn-{}-{}".format(
|
||||||
|
addr[0].split(".", 2)[-1][-6:], addr[1]
|
||||||
|
)
|
||||||
|
self.thr_client(sck, addr)
|
||||||
|
me.name = self.name + "-poolw"
|
||||||
|
except:
|
||||||
|
self.log(self.name, "thr_client: " + min_ex(), 3)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
|
self.stopping = True
|
||||||
|
for srv in self.srvs:
|
||||||
|
try:
|
||||||
|
srv.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
clients = list(self.clients.keys())
|
clients = list(self.clients.keys())
|
||||||
for cli in clients:
|
for cli in clients:
|
||||||
try:
|
try:
|
||||||
@@ -87,7 +237,14 @@ class HttpSrv(object):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.log("httpsrv-n", "ok bye")
|
if self.tp_q:
|
||||||
|
self.stop_threads(self.tp_nthr)
|
||||||
|
for _ in range(10):
|
||||||
|
time.sleep(0.05)
|
||||||
|
if self.tp_q.empty():
|
||||||
|
break
|
||||||
|
|
||||||
|
self.log(self.name, "ok bye")
|
||||||
|
|
||||||
def thr_client(self, sck, addr):
|
def thr_client(self, sck, addr):
|
||||||
"""thread managing one tcp client"""
|
"""thread managing one tcp client"""
|
||||||
@@ -97,25 +254,15 @@ class HttpSrv(object):
|
|||||||
with self.mutex:
|
with self.mutex:
|
||||||
self.clients[cli] = 0
|
self.clients[cli] = 0
|
||||||
|
|
||||||
if self.is_mp:
|
|
||||||
self.workload += 50
|
|
||||||
if not self.workload_thr_alive:
|
|
||||||
self.workload_thr_alive = True
|
|
||||||
thr = threading.Thread(
|
|
||||||
target=self.thr_workload, name="httpsrv-workload"
|
|
||||||
)
|
|
||||||
thr.daemon = True
|
|
||||||
thr.start()
|
|
||||||
|
|
||||||
fno = sck.fileno()
|
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" % ("-" * 4,), c="1;30")
|
||||||
|
|
||||||
cli.run()
|
cli.run()
|
||||||
|
|
||||||
except (OSError, socket.error) as ex:
|
except (OSError, socket.error) as ex:
|
||||||
if ex.errno not in [10038, 10054, 107, 57, 9]:
|
if ex.errno not in [10038, 10054, 107, 57, 49, 9]:
|
||||||
self.log(
|
self.log(
|
||||||
"%s %s" % addr,
|
"%s %s" % addr,
|
||||||
"run({}): {}".format(fno, ex),
|
"run({}): {}".format(fno, ex),
|
||||||
@@ -125,7 +272,7 @@ class HttpSrv(object):
|
|||||||
finally:
|
finally:
|
||||||
sck = cli.s
|
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" % ("-" * 5,), c="1;30")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fno = sck.fileno()
|
fno = sck.fileno()
|
||||||
@@ -138,42 +285,37 @@ class HttpSrv(object):
|
|||||||
"shut({}): {}".format(fno, 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:
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
del self.clients[cli]
|
del self.clients[cli]
|
||||||
|
self.ncli -= 1
|
||||||
|
|
||||||
if self.disconnect_func:
|
def cachebuster(self):
|
||||||
self.disconnect_func(addr) # pylint: disable=not-callable
|
if time.time() - self.cb_ts < 1:
|
||||||
|
return self.cb_v
|
||||||
|
|
||||||
def thr_workload(self):
|
with self.mutex:
|
||||||
"""indicates the python interpreter workload caused by this HttpSrv"""
|
if time.time() - self.cb_ts < 1:
|
||||||
# avoid locking in extract_filedata by tracking difference here
|
return self.cb_v
|
||||||
while True:
|
|
||||||
time.sleep(0.2)
|
|
||||||
with self.mutex:
|
|
||||||
if not self.clients:
|
|
||||||
# no clients rn, termiante thread
|
|
||||||
self.workload_thr_alive = False
|
|
||||||
self.workload = 0
|
|
||||||
return
|
|
||||||
|
|
||||||
total = 0
|
v = E.t0
|
||||||
with self.mutex:
|
try:
|
||||||
for cli in self.clients.keys():
|
with os.scandir(os.path.join(E.mod, "web")) as dh:
|
||||||
now = cli.workload
|
for fh in dh:
|
||||||
delta = now - self.clients[cli]
|
inf = fh.stat()
|
||||||
if delta < 0:
|
v = max(v, inf.st_mtime)
|
||||||
# was reset in HttpCli to prevent overflow
|
except:
|
||||||
delta = now
|
pass
|
||||||
|
|
||||||
total += delta
|
v = base64.urlsafe_b64encode(spack(b">xxL", int(v)))
|
||||||
self.clients[cli] = now
|
self.cb_v = v.decode("ascii")[-4:]
|
||||||
|
self.cb_ts = time.time()
|
||||||
self.workload = total
|
return self.cb_v
|
||||||
|
|||||||
@@ -7,11 +7,9 @@ import json
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
|
|
||||||
from .__init__ import PY2, WINDOWS
|
from .__init__ import PY2, WINDOWS, unicode
|
||||||
from .util import fsenc, fsdec, REKOBO_LKEY
|
from .util import fsenc, fsdec, uncyg, REKOBO_LKEY
|
||||||
|
from .bos import bos
|
||||||
if not PY2:
|
|
||||||
unicode = str
|
|
||||||
|
|
||||||
|
|
||||||
def have_ff(cmd):
|
def have_ff(cmd):
|
||||||
@@ -44,7 +42,10 @@ class MParser(object):
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
bp = os.path.expanduser(args)
|
bp = os.path.expanduser(args)
|
||||||
if os.path.exists(bp):
|
if WINDOWS:
|
||||||
|
bp = uncyg(bp)
|
||||||
|
|
||||||
|
if bos.path.exists(bp):
|
||||||
self.bin = bp
|
self.bin = bp
|
||||||
return
|
return
|
||||||
except:
|
except:
|
||||||
@@ -112,6 +113,19 @@ def parse_ffprobe(txt):
|
|||||||
ret = {} # processed
|
ret = {} # processed
|
||||||
md = {} # raw tags
|
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 = {}
|
have = {}
|
||||||
for strm in streams:
|
for strm in streams:
|
||||||
typ = strm.get("codec_type")
|
typ = strm.get("codec_type")
|
||||||
@@ -131,9 +145,7 @@ def parse_ffprobe(txt):
|
|||||||
]
|
]
|
||||||
|
|
||||||
if typ == "video":
|
if typ == "video":
|
||||||
if strm.get("DISPOSITION:attached_pic") == "1" or fmt.get(
|
if strm.get("DISPOSITION:attached_pic") == "1" or is_audio:
|
||||||
"format_name"
|
|
||||||
) in ["mp3", "ogg", "flac"]:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
kvm = [
|
kvm = [
|
||||||
@@ -177,7 +189,7 @@ def parse_ffprobe(txt):
|
|||||||
|
|
||||||
k = k[4:].strip()
|
k = k[4:].strip()
|
||||||
v = v.strip()
|
v = v.strip()
|
||||||
if k and v:
|
if k and v and k not in md:
|
||||||
md[k] = [v]
|
md[k] = [v]
|
||||||
|
|
||||||
for k in [".q", ".vq", ".aq"]:
|
for k in [".q", ".vq", ".aq"]:
|
||||||
@@ -216,37 +228,47 @@ def parse_ffprobe(txt):
|
|||||||
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
|
||||||
|
self.args = args
|
||||||
self.usable = True
|
self.usable = True
|
||||||
self.prefer_mt = False
|
self.prefer_mt = not args.no_mtag_ff
|
||||||
mappings = args.mtm
|
|
||||||
self.backend = "ffprobe" if args.no_mutagen else "mutagen"
|
self.backend = "ffprobe" if args.no_mutagen else "mutagen"
|
||||||
or_ffprobe = " or ffprobe"
|
self.can_ffprobe = (
|
||||||
|
HAVE_FFPROBE
|
||||||
|
and not args.no_mtag_ff
|
||||||
|
and (not WINDOWS or sys.version_info >= (3, 8))
|
||||||
|
)
|
||||||
|
mappings = args.mtm
|
||||||
|
or_ffprobe = " or FFprobe"
|
||||||
|
|
||||||
if self.backend == "mutagen":
|
if self.backend == "mutagen":
|
||||||
self.get = self.get_mutagen
|
self.get = self.get_mutagen
|
||||||
try:
|
try:
|
||||||
import mutagen
|
import mutagen
|
||||||
except:
|
except:
|
||||||
self.log("could not load mutagen, trying ffprobe instead", c=3)
|
self.log("could not load Mutagen, trying FFprobe instead", c=3)
|
||||||
self.backend = "ffprobe"
|
self.backend = "ffprobe"
|
||||||
|
|
||||||
if self.backend == "ffprobe":
|
if self.backend == "ffprobe":
|
||||||
|
self.usable = self.can_ffprobe
|
||||||
self.get = self.get_ffprobe
|
self.get = self.get_ffprobe
|
||||||
self.prefer_mt = True
|
self.prefer_mt = True
|
||||||
# about 20x slower
|
|
||||||
self.usable = HAVE_FFPROBE
|
|
||||||
|
|
||||||
if self.usable and WINDOWS and sys.version_info < (3, 8):
|
if not HAVE_FFPROBE:
|
||||||
self.usable = False
|
pass
|
||||||
|
|
||||||
|
elif args.no_mtag_ff:
|
||||||
|
msg = "found FFprobe but it was disabled by --no-mtag-ff"
|
||||||
|
self.log(msg, c=3)
|
||||||
|
|
||||||
|
elif WINDOWS and sys.version_info < (3, 8):
|
||||||
or_ffprobe = " or python >= 3.8"
|
or_ffprobe = " or python >= 3.8"
|
||||||
msg = "found ffprobe but your python is too old; need 3.8 or newer"
|
msg = "found FFprobe but your python is too old; need 3.8 or newer"
|
||||||
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\n"
|
msg = "need Mutagen{} to read media tags so please run this:\n{}{} -m pip install --user mutagen\n"
|
||||||
self.log(
|
pybin = os.path.basename(sys.executable)
|
||||||
msg.format(or_ffprobe, " " * 37, os.path.basename(sys.executable)), c=1
|
self.log(msg.format(or_ffprobe, " " * 37, pybin), 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
|
||||||
@@ -376,7 +398,7 @@ class MTag(object):
|
|||||||
v2 = r2.get(k)
|
v2 = r2.get(k)
|
||||||
if v1 == v2:
|
if v1 == v2:
|
||||||
print(" ", k, v1)
|
print(" ", k, v1)
|
||||||
elif v1 != "0000": # ffprobe date=0
|
elif v1 != "0000": # FFprobe date=0
|
||||||
diffs.append(k)
|
diffs.append(k)
|
||||||
print(" 1", k, v1)
|
print(" 1", k, v1)
|
||||||
print(" 2", k, v2)
|
print(" 2", k, v2)
|
||||||
@@ -397,20 +419,41 @@ class MTag(object):
|
|||||||
md = mutagen.File(fsenc(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 self.get_ffprobe(abspath) if self.can_ffprobe else {}
|
||||||
|
|
||||||
ret = {}
|
sz = bos.path.getsize(abspath)
|
||||||
try:
|
ret = {".q": [0, int((sz / md.info.length) / 128)]}
|
||||||
dur = int(md.info.length)
|
|
||||||
|
for attr, k, norm in [
|
||||||
|
["codec", "ac", unicode],
|
||||||
|
["channels", "chs", int],
|
||||||
|
["sample_rate", ".hz", int],
|
||||||
|
["bitrate", ".aq", int],
|
||||||
|
["length", ".dur", int],
|
||||||
|
]:
|
||||||
try:
|
try:
|
||||||
q = int(md.info.bitrate / 1024)
|
v = getattr(md.info, attr)
|
||||||
except:
|
except:
|
||||||
q = int((os.path.getsize(fsenc(abspath)) / dur) / 128)
|
if k != "ac":
|
||||||
|
continue
|
||||||
|
|
||||||
ret[".dur"] = [0, dur]
|
try:
|
||||||
ret[".q"] = [0, q]
|
v = str(md.info).split(".")[1]
|
||||||
except:
|
if v.startswith("ogg"):
|
||||||
pass
|
v = v[3:]
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not v:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if k == ".aq":
|
||||||
|
v /= 1000
|
||||||
|
|
||||||
|
if k == "ac" and v.startswith("mp4a.40."):
|
||||||
|
v = "aac"
|
||||||
|
|
||||||
|
ret[k] = [0, norm(v)]
|
||||||
|
|
||||||
return self.normalize_tags(ret, md)
|
return self.normalize_tags(ret, md)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import os
|
|
||||||
import tarfile
|
import tarfile
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from .sutil import errdesc
|
from .sutil import errdesc
|
||||||
from .util import Queue, fsenc
|
from .util import Queue, fsenc
|
||||||
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
class QFile(object):
|
class QFile(object):
|
||||||
@@ -33,10 +33,11 @@ class QFile(object):
|
|||||||
class StreamTar(object):
|
class StreamTar(object):
|
||||||
"""construct in-memory tar file from the given path"""
|
"""construct in-memory tar file from the given path"""
|
||||||
|
|
||||||
def __init__(self, fgen, **kwargs):
|
def __init__(self, log, fgen, **kwargs):
|
||||||
self.ci = 0
|
self.ci = 0
|
||||||
self.co = 0
|
self.co = 0
|
||||||
self.qfile = QFile()
|
self.qfile = QFile()
|
||||||
|
self.log = log
|
||||||
self.fgen = fgen
|
self.fgen = fgen
|
||||||
self.errf = None
|
self.errf = None
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ class StreamTar(object):
|
|||||||
|
|
||||||
yield None
|
yield None
|
||||||
if self.errf:
|
if self.errf:
|
||||||
os.unlink(self.errf["ap"])
|
bos.unlink(self.errf["ap"])
|
||||||
|
|
||||||
def ser(self, f):
|
def ser(self, f):
|
||||||
name = f["vp"]
|
name = f["vp"]
|
||||||
@@ -91,7 +92,8 @@ class StreamTar(object):
|
|||||||
errors.append([f["vp"], repr(ex)])
|
errors.append([f["vp"], repr(ex)])
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
self.errf = errdesc(errors)
|
self.errf, txt = errdesc(errors)
|
||||||
|
self.log("\n".join(([repr(self.errf)] + txt[1:])))
|
||||||
self.ser(self.errf)
|
self.ser(self.errf)
|
||||||
|
|
||||||
self.tar.close()
|
self.tar.close()
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
import time
|
||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
def errdesc(errors):
|
def errdesc(errors):
|
||||||
report = ["copyparty failed to add the following files to the archive:", ""]
|
report = ["copyparty failed to add the following files to the archive:", ""]
|
||||||
@@ -17,12 +18,11 @@ def errdesc(errors):
|
|||||||
tf_path = tf.name
|
tf_path = tf.name
|
||||||
tf.write("\r\n".join(report).encode("utf-8", "replace"))
|
tf.write("\r\n".join(report).encode("utf-8", "replace"))
|
||||||
|
|
||||||
dt = datetime.utcfromtimestamp(time.time())
|
dt = datetime.utcnow().strftime("%Y-%m%d-%H%M%S")
|
||||||
dt = dt.strftime("%Y-%m%d-%H%M%S")
|
|
||||||
|
|
||||||
os.chmod(tf_path, 0o444)
|
bos.chmod(tf_path, 0o444)
|
||||||
return {
|
return {
|
||||||
"vp": "archive-errors-{}.txt".format(dt),
|
"vp": "archive-errors-{}.txt".format(dt),
|
||||||
"ap": tf_path,
|
"ap": tf_path,
|
||||||
"st": os.stat(tf_path),
|
"st": bos.stat(tf_path),
|
||||||
}
|
}, report
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ import re
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import shlex
|
||||||
|
import string
|
||||||
|
import signal
|
||||||
|
import socket
|
||||||
import threading
|
import threading
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import calendar
|
import calendar
|
||||||
|
|
||||||
from .__init__ import PY2, WINDOWS, MACOS, VT100
|
from .__init__ import E, PY2, WINDOWS, ANYWIN, MACOS, VT100, unicode
|
||||||
from .util import mp
|
from .util import mp, start_log_thrs, start_stackmon, min_ex, ansi_re
|
||||||
from .authsrv import AuthSrv
|
from .authsrv import AuthSrv
|
||||||
from .tcpsrv import TcpSrv
|
from .tcpsrv import TcpSrv
|
||||||
from .up2k import Up2k
|
from .up2k import Up2k
|
||||||
@@ -28,17 +32,30 @@ class SvcHub(object):
|
|||||||
put() can return a queue (if want_reply=True) which has a blocking get() with the response.
|
put() can return a queue (if want_reply=True) which has a blocking get() with the response.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args):
|
def __init__(self, args, argv, printed):
|
||||||
self.args = args
|
self.args = args
|
||||||
|
self.argv = argv
|
||||||
|
self.logf = None
|
||||||
|
self.stop_req = False
|
||||||
|
self.stopping = False
|
||||||
|
self.stop_cond = threading.Condition()
|
||||||
|
self.httpsrv_up = 0
|
||||||
|
|
||||||
self.ansi_re = re.compile("\033\\[[^m]*m")
|
|
||||||
self.log_mutex = threading.Lock()
|
self.log_mutex = threading.Lock()
|
||||||
self.next_day = 0
|
self.next_day = 0
|
||||||
|
|
||||||
self.log = self._log_disabled if args.q else self._log_enabled
|
self.log = self._log_disabled if args.q else self._log_enabled
|
||||||
|
if args.lo:
|
||||||
|
self._setup_logfile(printed)
|
||||||
|
|
||||||
|
if args.stackmon:
|
||||||
|
start_stackmon(args.stackmon, 0)
|
||||||
|
|
||||||
|
if args.log_thrs:
|
||||||
|
start_log_thrs(self.log, args.log_thrs, 0)
|
||||||
|
|
||||||
# initiate all services to manage
|
# initiate all services to manage
|
||||||
self.asrv = AuthSrv(self.args, self.log, False)
|
self.asrv = AuthSrv(self.args, self.log)
|
||||||
if args.ls:
|
if args.ls:
|
||||||
self.asrv.dbg_ls()
|
self.asrv.dbg_ls()
|
||||||
|
|
||||||
@@ -69,22 +86,138 @@ class SvcHub(object):
|
|||||||
|
|
||||||
self.broker = Broker(self)
|
self.broker = Broker(self)
|
||||||
|
|
||||||
def run(self):
|
def thr_httpsrv_up(self):
|
||||||
thr = threading.Thread(target=self.tcpsrv.run, name="svchub-main")
|
time.sleep(5)
|
||||||
|
failed = self.broker.num_workers - self.httpsrv_up
|
||||||
|
if not failed:
|
||||||
|
return
|
||||||
|
|
||||||
|
m = "{}/{} workers failed to start"
|
||||||
|
m = m.format(failed, self.broker.num_workers)
|
||||||
|
self.log("root", m, 1)
|
||||||
|
os._exit(1)
|
||||||
|
|
||||||
|
def cb_httpsrv_up(self):
|
||||||
|
self.httpsrv_up += 1
|
||||||
|
if self.httpsrv_up != self.broker.num_workers:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.log("root", "workers OK\n")
|
||||||
|
self.up2k.init_vols()
|
||||||
|
|
||||||
|
thr = threading.Thread(target=self.sd_notify, name="sd-notify")
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
# winxp/py2.7 support: thr.join() kills signals
|
def _logname(self):
|
||||||
try:
|
dt = datetime.utcnow()
|
||||||
while True:
|
fn = self.args.lo
|
||||||
time.sleep(9001)
|
for fs in "YmdHMS":
|
||||||
|
fs = "%" + fs
|
||||||
|
if fs in fn:
|
||||||
|
fn = fn.replace(fs, dt.strftime(fs))
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
return fn
|
||||||
|
|
||||||
|
def _setup_logfile(self, printed):
|
||||||
|
base_fn = fn = sel_fn = self._logname()
|
||||||
|
if fn != self.args.lo:
|
||||||
|
ctr = 0
|
||||||
|
# yup this is a race; if started sufficiently concurrently, two
|
||||||
|
# copyparties can grab the same logfile (considered and ignored)
|
||||||
|
while os.path.exists(sel_fn):
|
||||||
|
ctr += 1
|
||||||
|
sel_fn = "{}.{}".format(fn, ctr)
|
||||||
|
|
||||||
|
fn = sel_fn
|
||||||
|
|
||||||
|
try:
|
||||||
|
import lzma
|
||||||
|
|
||||||
|
lh = lzma.open(fn, "wt", encoding="utf-8", errors="replace", preset=0)
|
||||||
|
|
||||||
|
except:
|
||||||
|
import codecs
|
||||||
|
|
||||||
|
lh = codecs.open(fn, "w", encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
lh.base_fn = base_fn
|
||||||
|
|
||||||
|
argv = [sys.executable] + self.argv
|
||||||
|
if hasattr(shlex, "quote"):
|
||||||
|
argv = [shlex.quote(x) for x in argv]
|
||||||
|
else:
|
||||||
|
argv = ['"{}"'.format(x) for x in argv]
|
||||||
|
|
||||||
|
msg = "[+] opened logfile [{}]\n".format(fn)
|
||||||
|
printed += msg
|
||||||
|
lh.write("t0: {:.3f}\nargv: {}\n\n{}".format(E.t0, " ".join(argv), printed))
|
||||||
|
self.logf = lh
|
||||||
|
print(msg, end="")
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.tcpsrv.run()
|
||||||
|
|
||||||
|
thr = threading.Thread(target=self.thr_httpsrv_up)
|
||||||
|
thr.daemon = True
|
||||||
|
thr.start()
|
||||||
|
|
||||||
|
for sig in [signal.SIGINT, signal.SIGTERM]:
|
||||||
|
signal.signal(sig, self.signal_handler)
|
||||||
|
|
||||||
|
# macos hangs after shutdown on sigterm with while-sleep,
|
||||||
|
# windows cannot ^c stop_cond (and win10 does the macos thing but winxp is fine??)
|
||||||
|
# linux is fine with both,
|
||||||
|
# never lucky
|
||||||
|
if ANYWIN:
|
||||||
|
# msys-python probably fine but >msys-python
|
||||||
|
thr = threading.Thread(target=self.stop_thr, name="svchub-sig")
|
||||||
|
thr.daemon = True
|
||||||
|
thr.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while not self.stop_req:
|
||||||
|
time.sleep(1)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.shutdown()
|
||||||
|
thr.join()
|
||||||
|
else:
|
||||||
|
self.stop_thr()
|
||||||
|
|
||||||
|
def stop_thr(self):
|
||||||
|
while not self.stop_req:
|
||||||
|
with self.stop_cond:
|
||||||
|
self.stop_cond.wait(9001)
|
||||||
|
|
||||||
|
self.shutdown()
|
||||||
|
|
||||||
|
def signal_handler(self, sig, frame):
|
||||||
|
if self.stopping:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stop_req = True
|
||||||
|
with self.stop_cond:
|
||||||
|
self.stop_cond.notify_all()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
if self.stopping:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stopping = True
|
||||||
|
self.stop_req = True
|
||||||
|
with self.stop_cond:
|
||||||
|
self.stop_cond.notify_all()
|
||||||
|
|
||||||
|
ret = 1
|
||||||
|
try:
|
||||||
with self.log_mutex:
|
with self.log_mutex:
|
||||||
print("OPYTHAT")
|
print("OPYTHAT")
|
||||||
|
|
||||||
self.tcpsrv.shutdown()
|
self.tcpsrv.shutdown()
|
||||||
self.broker.shutdown()
|
self.broker.shutdown()
|
||||||
|
self.up2k.shutdown()
|
||||||
if self.thumbsrv:
|
if self.thumbsrv:
|
||||||
self.thumbsrv.shutdown()
|
self.thumbsrv.shutdown()
|
||||||
|
|
||||||
@@ -97,11 +230,40 @@ class SvcHub(object):
|
|||||||
print("waiting for thumbsrv (10sec)...")
|
print("waiting for thumbsrv (10sec)...")
|
||||||
|
|
||||||
print("nailed it", end="")
|
print("nailed it", end="")
|
||||||
|
ret = 0
|
||||||
finally:
|
finally:
|
||||||
print("\033[0m")
|
print("\033[0m")
|
||||||
|
if self.logf:
|
||||||
|
self.logf.close()
|
||||||
|
|
||||||
|
sys.exit(ret)
|
||||||
|
|
||||||
def _log_disabled(self, src, msg, c=0):
|
def _log_disabled(self, src, msg, c=0):
|
||||||
pass
|
if not self.logf:
|
||||||
|
return
|
||||||
|
|
||||||
|
with self.log_mutex:
|
||||||
|
ts = datetime.utcnow().strftime("%Y-%m%d-%H%M%S.%f")[:-3]
|
||||||
|
self.logf.write("@{} [{}] {}\n".format(ts, src, msg))
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
if now >= self.next_day:
|
||||||
|
self._set_next_day()
|
||||||
|
|
||||||
|
def _set_next_day(self):
|
||||||
|
if self.next_day and self.logf and self.logf.base_fn != self._logname():
|
||||||
|
self.logf.close()
|
||||||
|
self._setup_logfile("")
|
||||||
|
|
||||||
|
dt = datetime.utcnow()
|
||||||
|
|
||||||
|
# unix timestamp of next 00:00:00 (leap-seconds safe)
|
||||||
|
day_now = dt.day
|
||||||
|
while dt.day == day_now:
|
||||||
|
dt += timedelta(hours=12)
|
||||||
|
|
||||||
|
dt = dt.replace(hour=0, minute=0, second=0)
|
||||||
|
self.next_day = calendar.timegm(dt.utctimetuple())
|
||||||
|
|
||||||
def _log_enabled(self, src, msg, c=0):
|
def _log_enabled(self, src, msg, c=0):
|
||||||
"""handles logging from all components"""
|
"""handles logging from all components"""
|
||||||
@@ -110,22 +272,15 @@ class SvcHub(object):
|
|||||||
if now >= self.next_day:
|
if now >= self.next_day:
|
||||||
dt = datetime.utcfromtimestamp(now)
|
dt = datetime.utcfromtimestamp(now)
|
||||||
print("\033[36m{}\033[0m\n".format(dt.strftime("%Y-%m-%d")), end="")
|
print("\033[36m{}\033[0m\n".format(dt.strftime("%Y-%m-%d")), end="")
|
||||||
|
self._set_next_day()
|
||||||
# unix timestamp of next 00:00:00 (leap-seconds safe)
|
|
||||||
day_now = dt.day
|
|
||||||
while dt.day == day_now:
|
|
||||||
dt += timedelta(hours=12)
|
|
||||||
|
|
||||||
dt = dt.replace(hour=0, minute=0, second=0)
|
|
||||||
self.next_day = calendar.timegm(dt.utctimetuple())
|
|
||||||
|
|
||||||
fmt = "\033[36m{} \033[33m{:21} \033[0m{}\n"
|
fmt = "\033[36m{} \033[33m{:21} \033[0m{}\n"
|
||||||
if not VT100:
|
if not VT100:
|
||||||
fmt = "{} {:21} {}\n"
|
fmt = "{} {:21} {}\n"
|
||||||
if "\033" in msg:
|
if "\033" in msg:
|
||||||
msg = self.ansi_re.sub("", msg)
|
msg = ansi_re.sub("", msg)
|
||||||
if "\033" in src:
|
if "\033" in src:
|
||||||
src = self.ansi_re.sub("", src)
|
src = ansi_re.sub("", src)
|
||||||
elif c:
|
elif c:
|
||||||
if isinstance(c, int):
|
if isinstance(c, int):
|
||||||
msg = "\033[3{}m{}".format(c, msg)
|
msg = "\033[3{}m{}".format(c, msg)
|
||||||
@@ -144,20 +299,20 @@ class SvcHub(object):
|
|||||||
except:
|
except:
|
||||||
print(msg.encode("ascii", "replace").decode(), end="")
|
print(msg.encode("ascii", "replace").decode(), end="")
|
||||||
|
|
||||||
|
if self.logf:
|
||||||
|
self.logf.write(msg)
|
||||||
|
|
||||||
def check_mp_support(self):
|
def check_mp_support(self):
|
||||||
vmin = sys.version_info[1]
|
vmin = sys.version_info[1]
|
||||||
if WINDOWS:
|
if WINDOWS:
|
||||||
msg = "need python 3.3 or newer for multiprocessing;"
|
msg = "need python 3.3 or newer for multiprocessing;"
|
||||||
if PY2:
|
if PY2 or vmin < 3:
|
||||||
# py2 pickler doesn't support winsock
|
|
||||||
return msg
|
|
||||||
elif vmin < 3:
|
|
||||||
return msg
|
return msg
|
||||||
elif MACOS:
|
elif MACOS:
|
||||||
return "multiprocessing is wonky on mac osx;"
|
return "multiprocessing is wonky on mac osx;"
|
||||||
else:
|
else:
|
||||||
msg = "need python 2.7 or 3.3+ for multiprocessing;"
|
msg = "need python 3.3+ for multiprocessing;"
|
||||||
if not PY2 and vmin < 3:
|
if PY2 or vmin < 3:
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -189,5 +344,24 @@ class SvcHub(object):
|
|||||||
if not err:
|
if not err:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self.log("root", err)
|
self.log("svchub", err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def sd_notify(self):
|
||||||
|
try:
|
||||||
|
addr = os.getenv("NOTIFY_SOCKET")
|
||||||
|
if not addr:
|
||||||
|
return
|
||||||
|
|
||||||
|
addr = unicode(addr)
|
||||||
|
if addr.startswith("@"):
|
||||||
|
addr = "\0" + addr[1:]
|
||||||
|
|
||||||
|
m = "".join(x for x in addr if x in string.printable)
|
||||||
|
self.log("sd_notify", m)
|
||||||
|
|
||||||
|
sck = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||||
|
sck.connect(addr)
|
||||||
|
sck.sendall(b"READY=1")
|
||||||
|
except:
|
||||||
|
self.log("sd_notify", min_ex())
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ from __future__ import print_function, unicode_literals
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import zlib
|
import zlib
|
||||||
import struct
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from .sutil import errdesc
|
from .sutil import errdesc
|
||||||
from .util import yieldfile, sanitize_fn
|
from .util import yieldfile, sanitize_fn, spack, sunpack
|
||||||
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
def dostime2unix(buf):
|
def dostime2unix(buf):
|
||||||
t, d = struct.unpack("<HH", buf)
|
t, d = sunpack(b"<HH", buf)
|
||||||
|
|
||||||
ts = (t & 0x1F) * 2
|
ts = (t & 0x1F) * 2
|
||||||
tm = (t >> 5) & 0x3F
|
tm = (t >> 5) & 0x3F
|
||||||
@@ -36,13 +36,13 @@ def unixtime2dos(ts):
|
|||||||
|
|
||||||
bd = ((dy - 1980) << 9) + (dm << 5) + dd
|
bd = ((dy - 1980) << 9) + (dm << 5) + dd
|
||||||
bt = (th << 11) + (tm << 5) + ts // 2
|
bt = (th << 11) + (tm << 5) + ts // 2
|
||||||
return struct.pack("<HH", bt, bd)
|
return spack(b"<HH", bt, bd)
|
||||||
|
|
||||||
|
|
||||||
def gen_fdesc(sz, crc32, z64):
|
def gen_fdesc(sz, crc32, z64):
|
||||||
ret = b"\x50\x4b\x07\x08"
|
ret = b"\x50\x4b\x07\x08"
|
||||||
fmt = "<LQQ" if z64 else "<LLL"
|
fmt = b"<LQQ" if z64 else b"<LLL"
|
||||||
ret += struct.pack(fmt, crc32, sz, sz)
|
ret += spack(fmt, crc32, sz, sz)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ def gen_hdr(h_pos, fn, sz, lastmod, utf8, crc32, pre_crc):
|
|||||||
req_ver = b"\x2d\x00" if z64 else b"\x0a\x00"
|
req_ver = b"\x2d\x00" if z64 else b"\x0a\x00"
|
||||||
|
|
||||||
if crc32:
|
if crc32:
|
||||||
crc32 = struct.pack("<L", crc32)
|
crc32 = spack(b"<L", crc32)
|
||||||
else:
|
else:
|
||||||
crc32 = b"\x00" * 4
|
crc32 = b"\x00" * 4
|
||||||
|
|
||||||
@@ -87,14 +87,14 @@ def gen_hdr(h_pos, fn, sz, lastmod, utf8, crc32, pre_crc):
|
|||||||
# however infozip does actual sz and it even works on winxp
|
# however infozip does actual sz and it even works on winxp
|
||||||
# (same reasning for z64 extradata later)
|
# (same reasning for z64 extradata later)
|
||||||
vsz = 0xFFFFFFFF if z64 else sz
|
vsz = 0xFFFFFFFF if z64 else sz
|
||||||
ret += struct.pack("<LL", vsz, vsz)
|
ret += spack(b"<LL", vsz, vsz)
|
||||||
|
|
||||||
# windows support (the "?" replace below too)
|
# windows support (the "?" replace below too)
|
||||||
fn = sanitize_fn(fn, ok="/")
|
fn = sanitize_fn(fn, "/", [])
|
||||||
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
|
||||||
ret += struct.pack("<HH", len(bfn), z64_len)
|
ret += spack(b"<HH", len(bfn), z64_len)
|
||||||
|
|
||||||
if h_pos is not None:
|
if h_pos is not None:
|
||||||
# 2b comment, 2b diskno
|
# 2b comment, 2b diskno
|
||||||
@@ -106,12 +106,12 @@ def gen_hdr(h_pos, fn, sz, lastmod, utf8, crc32, pre_crc):
|
|||||||
ret += b"\x01\x00\x00\x00\xa4\x81"
|
ret += b"\x01\x00\x00\x00\xa4\x81"
|
||||||
|
|
||||||
# 4b local-header-ofs
|
# 4b local-header-ofs
|
||||||
ret += struct.pack("<L", min(h_pos, 0xFFFFFFFF))
|
ret += spack(b"<L", min(h_pos, 0xFFFFFFFF))
|
||||||
|
|
||||||
ret += bfn
|
ret += bfn
|
||||||
|
|
||||||
if z64v:
|
if z64v:
|
||||||
ret += struct.pack("<HH" + "Q" * len(z64v), 1, len(z64v) * 8, *z64v)
|
ret += spack(b"<HH" + b"Q" * len(z64v), 1, len(z64v) * 8, *z64v)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ def gen_ecdr(items, cdir_pos, cdir_end):
|
|||||||
need_64 = nitems == 0xFFFF or 0xFFFFFFFF in [csz, cpos]
|
need_64 = nitems == 0xFFFF or 0xFFFFFFFF in [csz, cpos]
|
||||||
|
|
||||||
# 2b tnfiles, 2b dnfiles, 4b dir sz, 4b dir pos
|
# 2b tnfiles, 2b dnfiles, 4b dir sz, 4b dir pos
|
||||||
ret += struct.pack("<HHLL", nitems, nitems, csz, cpos)
|
ret += spack(b"<HHLL", nitems, nitems, csz, cpos)
|
||||||
|
|
||||||
# 2b comment length
|
# 2b comment length
|
||||||
ret += b"\x00\x00"
|
ret += b"\x00\x00"
|
||||||
@@ -163,7 +163,7 @@ def gen_ecdr64(items, cdir_pos, cdir_end):
|
|||||||
|
|
||||||
# 8b tnfiles, 8b dnfiles, 8b dir sz, 8b dir pos
|
# 8b tnfiles, 8b dnfiles, 8b dir sz, 8b dir pos
|
||||||
cdir_sz = cdir_end - cdir_pos
|
cdir_sz = cdir_end - cdir_pos
|
||||||
ret += struct.pack("<QQQQ", len(items), len(items), cdir_sz, cdir_pos)
|
ret += spack(b"<QQQQ", len(items), len(items), cdir_sz, cdir_pos)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -178,13 +178,14 @@ def gen_ecdr64_loc(ecdr64_pos):
|
|||||||
ret = b"\x50\x4b\x06\x07"
|
ret = b"\x50\x4b\x06\x07"
|
||||||
|
|
||||||
# 4b cdisk, 8b start of ecdr64, 4b ndisks
|
# 4b cdisk, 8b start of ecdr64, 4b ndisks
|
||||||
ret += struct.pack("<LQL", 0, ecdr64_pos, 1)
|
ret += spack(b"<LQL", 0, ecdr64_pos, 1)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class StreamZip(object):
|
class StreamZip(object):
|
||||||
def __init__(self, fgen, utf8=False, pre_crc=False):
|
def __init__(self, log, fgen, utf8=False, pre_crc=False):
|
||||||
|
self.log = log
|
||||||
self.fgen = fgen
|
self.fgen = fgen
|
||||||
self.utf8 = utf8
|
self.utf8 = utf8
|
||||||
self.pre_crc = pre_crc
|
self.pre_crc = pre_crc
|
||||||
@@ -247,8 +248,8 @@ class StreamZip(object):
|
|||||||
errors.append([f["vp"], repr(ex)])
|
errors.append([f["vp"], repr(ex)])
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
errf = errdesc(errors)
|
errf, txt = errdesc(errors)
|
||||||
print(repr(errf))
|
self.log("\n".join(([repr(errf)] + txt[1:])))
|
||||||
for x in self.ser(errf):
|
for x in self.ser(errf):
|
||||||
yield x
|
yield x
|
||||||
|
|
||||||
@@ -271,4 +272,4 @@ class StreamZip(object):
|
|||||||
yield self._ct(ecdr)
|
yield self._ct(ecdr)
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
os.unlink(errf["ap"])
|
bos.unlink(errf["ap"])
|
||||||
|
|||||||
@@ -2,11 +2,10 @@
|
|||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
import socket
|
import socket
|
||||||
import select
|
|
||||||
|
|
||||||
from .util import chkcmd, Counter
|
from .__init__ import MACOS, ANYWIN
|
||||||
|
from .util import chkcmd
|
||||||
|
|
||||||
|
|
||||||
class TcpSrv(object):
|
class TcpSrv(object):
|
||||||
@@ -20,7 +19,6 @@ class TcpSrv(object):
|
|||||||
self.args = hub.args
|
self.args = hub.args
|
||||||
self.log = hub.log
|
self.log = hub.log
|
||||||
|
|
||||||
self.num_clients = Counter()
|
|
||||||
self.stopping = False
|
self.stopping = False
|
||||||
|
|
||||||
ip = "127.0.0.1"
|
ip = "127.0.0.1"
|
||||||
@@ -32,14 +30,16 @@ class TcpSrv(object):
|
|||||||
for x in nonlocals:
|
for x in nonlocals:
|
||||||
eps[x] = "external"
|
eps[x] = "external"
|
||||||
|
|
||||||
|
msgs = []
|
||||||
|
m = "available @ http://{}:{}/ (\033[33m{}\033[0m)"
|
||||||
for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
|
for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
|
||||||
for port in sorted(self.args.p):
|
for port in sorted(self.args.p):
|
||||||
self.log(
|
msgs.append(m.format(ip, port, desc))
|
||||||
"tcpsrv",
|
|
||||||
"available @ http://{}:{}/ (\033[33m{}\033[0m)".format(
|
if msgs:
|
||||||
ip, port, desc
|
msgs[-1] += "\n"
|
||||||
),
|
for m in msgs:
|
||||||
)
|
self.log("tcpsrv", m)
|
||||||
|
|
||||||
self.srv = []
|
self.srv = []
|
||||||
for ip in self.args.i:
|
for ip in self.args.i:
|
||||||
@@ -66,37 +66,13 @@ class TcpSrv(object):
|
|||||||
for srv in self.srv:
|
for srv in self.srv:
|
||||||
srv.listen(self.args.nc)
|
srv.listen(self.args.nc)
|
||||||
ip, port = srv.getsockname()
|
ip, port = srv.getsockname()
|
||||||
self.log("tcpsrv", "listening @ {0}:{1}".format(ip, port))
|
fno = srv.fileno()
|
||||||
|
msg = "listening @ {}:{} f{}".format(ip, port, fno)
|
||||||
|
self.log("tcpsrv", msg)
|
||||||
|
if self.args.q:
|
||||||
|
print(msg)
|
||||||
|
|
||||||
while not self.stopping:
|
self.hub.broker.put(False, "listen", srv)
|
||||||
if self.args.log_conn:
|
|
||||||
self.log("tcpsrv", "|%sC-ncli" % ("-" * 1,), c="1;30")
|
|
||||||
|
|
||||||
if self.num_clients.v >= self.args.nc:
|
|
||||||
time.sleep(0.1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if self.args.log_conn:
|
|
||||||
self.log("tcpsrv", "|%sC-acc1" % ("-" * 2,), c="1;30")
|
|
||||||
|
|
||||||
ready, _, _ = select.select(self.srv, [], [])
|
|
||||||
for srv in ready:
|
|
||||||
if self.stopping:
|
|
||||||
break
|
|
||||||
|
|
||||||
sck, addr = srv.accept()
|
|
||||||
sip, sport = srv.getsockname()
|
|
||||||
if self.args.log_conn:
|
|
||||||
self.log(
|
|
||||||
"%s %s" % addr,
|
|
||||||
"|{}C-acc2 \033[0;36m{} \033[3{}m{}".format(
|
|
||||||
"-" * 3, sip, sport % 8, sport
|
|
||||||
),
|
|
||||||
c="1;30",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.num_clients.add()
|
|
||||||
self.hub.broker.put(False, "httpconn", sck, addr)
|
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.stopping = True
|
self.stopping = True
|
||||||
@@ -108,25 +84,100 @@ class TcpSrv(object):
|
|||||||
|
|
||||||
self.log("tcpsrv", "ok bye")
|
self.log("tcpsrv", "ok bye")
|
||||||
|
|
||||||
def detect_interfaces(self, listen_ips):
|
def ips_linux(self):
|
||||||
eps = {}
|
eps = {}
|
||||||
|
|
||||||
# get all ips and their interfaces
|
|
||||||
try:
|
try:
|
||||||
ip_addr, _ = chkcmd("ip", "addr")
|
txt, _ = chkcmd(["ip", "addr"])
|
||||||
except:
|
except:
|
||||||
ip_addr = None
|
return eps
|
||||||
|
|
||||||
if ip_addr:
|
r = re.compile(r"^\s+inet ([^ ]+)/.* (.*)")
|
||||||
r = re.compile(r"^\s+inet ([^ ]+)/.* (.*)")
|
for ln in txt.split("\n"):
|
||||||
for ln in ip_addr.split("\n"):
|
try:
|
||||||
try:
|
ip, dev = r.match(ln.rstrip()).groups()
|
||||||
ip, dev = r.match(ln.rstrip()).groups()
|
eps[ip] = dev
|
||||||
for lip in listen_ips:
|
except:
|
||||||
if lip in ["0.0.0.0", ip]:
|
pass
|
||||||
eps[ip] = dev
|
|
||||||
except:
|
return eps
|
||||||
pass
|
|
||||||
|
def ips_macos(self):
|
||||||
|
eps = {}
|
||||||
|
try:
|
||||||
|
txt, _ = chkcmd(["ifconfig"])
|
||||||
|
except:
|
||||||
|
return eps
|
||||||
|
|
||||||
|
rdev = re.compile(r"^([^ ]+):")
|
||||||
|
rip = re.compile(r"^\tinet ([0-9\.]+) ")
|
||||||
|
dev = None
|
||||||
|
for ln in txt.split("\n"):
|
||||||
|
m = rdev.match(ln)
|
||||||
|
if m:
|
||||||
|
dev = m.group(1)
|
||||||
|
|
||||||
|
m = rip.match(ln)
|
||||||
|
if m:
|
||||||
|
eps[m.group(1)] = dev
|
||||||
|
dev = None
|
||||||
|
|
||||||
|
return eps
|
||||||
|
|
||||||
|
def ips_windows_ipconfig(self):
|
||||||
|
eps = {}
|
||||||
|
try:
|
||||||
|
txt, _ = chkcmd(["ipconfig"])
|
||||||
|
except:
|
||||||
|
return eps
|
||||||
|
|
||||||
|
rdev = re.compile(r"(^[^ ].*):$")
|
||||||
|
rip = re.compile(r"^ +IPv?4? [^:]+: *([0-9\.]{7,15})$")
|
||||||
|
dev = None
|
||||||
|
for ln in txt.replace("\r", "").split("\n"):
|
||||||
|
m = rdev.match(ln)
|
||||||
|
if m:
|
||||||
|
dev = m.group(1).split(" adapter ", 1)[-1]
|
||||||
|
|
||||||
|
m = rip.match(ln)
|
||||||
|
if m and dev:
|
||||||
|
eps[m.group(1)] = dev
|
||||||
|
dev = None
|
||||||
|
|
||||||
|
return eps
|
||||||
|
|
||||||
|
def ips_windows_netsh(self):
|
||||||
|
eps = {}
|
||||||
|
try:
|
||||||
|
txt, _ = chkcmd("netsh interface ip show address".split())
|
||||||
|
except:
|
||||||
|
return eps
|
||||||
|
|
||||||
|
rdev = re.compile(r'.* "([^"]+)"$')
|
||||||
|
rip = re.compile(r".* IP\b.*: +([0-9\.]{7,15})$")
|
||||||
|
dev = None
|
||||||
|
for ln in txt.replace("\r", "").split("\n"):
|
||||||
|
m = rdev.match(ln)
|
||||||
|
if m:
|
||||||
|
dev = m.group(1)
|
||||||
|
|
||||||
|
m = rip.match(ln)
|
||||||
|
if m and dev:
|
||||||
|
eps[m.group(1)] = dev
|
||||||
|
dev = None
|
||||||
|
|
||||||
|
return eps
|
||||||
|
|
||||||
|
def detect_interfaces(self, listen_ips):
|
||||||
|
if MACOS:
|
||||||
|
eps = self.ips_macos()
|
||||||
|
elif ANYWIN:
|
||||||
|
eps = self.ips_windows_ipconfig() # sees more interfaces
|
||||||
|
eps.update(self.ips_windows_netsh()) # has better names
|
||||||
|
else:
|
||||||
|
eps = self.ips_linux()
|
||||||
|
|
||||||
|
if "0.0.0.0" not in listen_ips:
|
||||||
|
eps = {k: v for k, v in eps.items() if k in listen_ips}
|
||||||
|
|
||||||
default_route = None
|
default_route = None
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import os
|
|||||||
|
|
||||||
from .util import Cooldown
|
from .util import Cooldown
|
||||||
from .th_srv import thumb_path, THUMBABLE, FMT_FF
|
from .th_srv import thumb_path, THUMBABLE, FMT_FF
|
||||||
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
class ThumbCli(object):
|
class ThumbCli(object):
|
||||||
@@ -25,6 +26,9 @@ class ThumbCli(object):
|
|||||||
if is_vid and self.args.no_vthumb:
|
if is_vid and self.args.no_vthumb:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg"]:
|
||||||
|
return os.path.join(ptop, rem)
|
||||||
|
|
||||||
if fmt == "j" and self.args.th_no_jpg:
|
if fmt == "j" and self.args.th_no_jpg:
|
||||||
fmt = "w"
|
fmt = "w"
|
||||||
|
|
||||||
@@ -36,7 +40,7 @@ class ThumbCli(object):
|
|||||||
tpath = thumb_path(histpath, rem, mtime, fmt)
|
tpath = thumb_path(histpath, rem, mtime, fmt)
|
||||||
ret = None
|
ret = None
|
||||||
try:
|
try:
|
||||||
st = os.stat(tpath)
|
st = bos.stat(tpath)
|
||||||
if st.st_size:
|
if st.st_size:
|
||||||
ret = tpath
|
ret = tpath
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -9,22 +9,19 @@ import hashlib
|
|||||||
import threading
|
import threading
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
|
|
||||||
from .__init__ import PY2
|
from .__init__ import PY2, unicode
|
||||||
from .util import fsenc, runcmd, Queue, Cooldown, BytesIO, min_ex
|
from .util import fsenc, vsplit, runcmd, Queue, Cooldown, BytesIO, min_ex
|
||||||
|
from .bos import bos
|
||||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
|
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
|
||||||
|
|
||||||
|
|
||||||
if not PY2:
|
|
||||||
unicode = str
|
|
||||||
|
|
||||||
|
|
||||||
HAVE_PIL = False
|
HAVE_PIL = False
|
||||||
HAVE_HEIF = False
|
HAVE_HEIF = False
|
||||||
HAVE_AVIF = False
|
HAVE_AVIF = False
|
||||||
HAVE_WEBP = False
|
HAVE_WEBP = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image, ImageOps
|
from PIL import Image, ImageOps, ExifTags
|
||||||
|
|
||||||
HAVE_PIL = True
|
HAVE_PIL = True
|
||||||
try:
|
try:
|
||||||
@@ -53,7 +50,7 @@ except:
|
|||||||
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
||||||
# ffmpeg -formats
|
# 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_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"
|
FMT_FF = "av1 asf avi flv m4v mkv mjpeg mjpg mpg mpeg mpg2 mpeg2 h264 avc mts h265 hevc mov 3gp mp4 ts mpegts nut ogv ogm rm vob webm wmv"
|
||||||
|
|
||||||
if HAVE_HEIF:
|
if HAVE_HEIF:
|
||||||
FMT_PIL += " heif heifs heic heics"
|
FMT_PIL += " heif heifs heic heics"
|
||||||
@@ -77,21 +74,16 @@ def thumb_path(histpath, rem, mtime, fmt):
|
|||||||
# base16 = 16 = 256
|
# base16 = 16 = 256
|
||||||
# b64-lc = 38 = 1444
|
# b64-lc = 38 = 1444
|
||||||
# base64 = 64 = 4096
|
# base64 = 64 = 4096
|
||||||
try:
|
rd, fn = vsplit(rem)
|
||||||
rd, fn = rem.rsplit("/", 1)
|
|
||||||
except:
|
|
||||||
rd = ""
|
|
||||||
fn = rem
|
|
||||||
|
|
||||||
if rd:
|
if rd:
|
||||||
h = hashlib.sha512(fsenc(rd)).digest()[:24]
|
h = hashlib.sha512(fsenc(rd)).digest()
|
||||||
b64 = base64.urlsafe_b64encode(h).decode("ascii")[:24]
|
b64 = base64.urlsafe_b64encode(h).decode("ascii")[:24]
|
||||||
rd = "{}/{}/".format(b64[:2], b64[2:4]).lower() + b64
|
rd = "{}/{}/".format(b64[:2], b64[2:4]).lower() + b64
|
||||||
else:
|
else:
|
||||||
rd = "top"
|
rd = "top"
|
||||||
|
|
||||||
# could keep original filenames but this is safer re pathlen
|
# could keep original filenames but this is safer re pathlen
|
||||||
h = hashlib.sha512(fsenc(fn)).digest()[:24]
|
h = hashlib.sha512(fsenc(fn)).digest()
|
||||||
fn = base64.urlsafe_b64encode(h).decode("ascii")[:24]
|
fn = base64.urlsafe_b64encode(h).decode("ascii")[:24]
|
||||||
|
|
||||||
return "{}/th/{}/{}.{:x}.{}".format(
|
return "{}/th/{}/{}.{:x}.{}".format(
|
||||||
@@ -113,7 +105,10 @@ class ThumbSrv(object):
|
|||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
self.busy = {}
|
self.busy = {}
|
||||||
self.stopping = False
|
self.stopping = False
|
||||||
self.nthr = os.cpu_count() if hasattr(os, "cpu_count") else 4
|
self.nthr = self.args.th_mt
|
||||||
|
if not self.nthr:
|
||||||
|
self.nthr = os.cpu_count() if hasattr(os, "cpu_count") else 4
|
||||||
|
|
||||||
self.q = Queue(self.nthr * 4)
|
self.q = Queue(self.nthr * 4)
|
||||||
for n in range(self.nthr):
|
for n in range(self.nthr):
|
||||||
t = threading.Thread(
|
t = threading.Thread(
|
||||||
@@ -125,18 +120,19 @@ class ThumbSrv(object):
|
|||||||
if not self.args.no_vthumb and (not HAVE_FFMPEG or not HAVE_FFPROBE):
|
if not self.args.no_vthumb and (not HAVE_FFMPEG or not HAVE_FFPROBE):
|
||||||
missing = []
|
missing = []
|
||||||
if not HAVE_FFMPEG:
|
if not HAVE_FFMPEG:
|
||||||
missing.append("ffmpeg")
|
missing.append("FFmpeg")
|
||||||
|
|
||||||
if not HAVE_FFPROBE:
|
if not HAVE_FFPROBE:
|
||||||
missing.append("ffprobe")
|
missing.append("FFprobe")
|
||||||
|
|
||||||
msg = "cannot create video thumbnails because some of the required programs are not available: "
|
msg = "cannot create video thumbnails because some of the required programs are not available: "
|
||||||
msg += ", ".join(missing)
|
msg += ", ".join(missing)
|
||||||
self.log(msg, c=3)
|
self.log(msg, c=3)
|
||||||
|
|
||||||
t = threading.Thread(target=self.cleaner, name="thumb-cleaner")
|
if self.args.th_clean:
|
||||||
t.daemon = True
|
t = threading.Thread(target=self.cleaner, name="thumb-cleaner")
|
||||||
t.start()
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
def log(self, msg, c=0):
|
def log(self, msg, c=0):
|
||||||
self.log_func("thumb", msg, c)
|
self.log_func("thumb", msg, c)
|
||||||
@@ -162,13 +158,10 @@ class ThumbSrv(object):
|
|||||||
self.log("wait {}".format(tpath))
|
self.log("wait {}".format(tpath))
|
||||||
except:
|
except:
|
||||||
thdir = os.path.dirname(tpath)
|
thdir = os.path.dirname(tpath)
|
||||||
try:
|
bos.makedirs(thdir)
|
||||||
os.makedirs(thdir)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
inf_path = os.path.join(thdir, "dir.txt")
|
inf_path = os.path.join(thdir, "dir.txt")
|
||||||
if not os.path.exists(inf_path):
|
if not bos.path.exists(inf_path):
|
||||||
with open(inf_path, "wb") as f:
|
with open(inf_path, "wb") as f:
|
||||||
f.write(fsenc(os.path.dirname(abspath)))
|
f.write(fsenc(os.path.dirname(abspath)))
|
||||||
|
|
||||||
@@ -188,7 +181,7 @@ class ThumbSrv(object):
|
|||||||
cond.wait(3)
|
cond.wait(3)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
st = os.stat(tpath)
|
st = bos.stat(tpath)
|
||||||
if st.st_size:
|
if st.st_size:
|
||||||
return tpath
|
return tpath
|
||||||
except:
|
except:
|
||||||
@@ -205,7 +198,7 @@ class ThumbSrv(object):
|
|||||||
abspath, tpath = task
|
abspath, tpath = task
|
||||||
ext = abspath.split(".")[-1].lower()
|
ext = abspath.split(".")[-1].lower()
|
||||||
fun = None
|
fun = None
|
||||||
if not os.path.exists(tpath):
|
if not bos.path.exists(tpath):
|
||||||
if ext in FMT_PIL:
|
if ext in FMT_PIL:
|
||||||
fun = self.conv_pil
|
fun = self.conv_pil
|
||||||
elif ext in FMT_FF:
|
elif ext in FMT_FF:
|
||||||
@@ -215,8 +208,8 @@ class ThumbSrv(object):
|
|||||||
try:
|
try:
|
||||||
fun(abspath, tpath)
|
fun(abspath, tpath)
|
||||||
except:
|
except:
|
||||||
msg = "{} failed on {}\n{}"
|
msg = "{} could not create thumbnail of {}\n{}"
|
||||||
self.log(msg.format(fun.__name__, abspath, min_ex()), 3)
|
self.log(msg.format(fun.__name__, abspath, min_ex()), "1;30")
|
||||||
with open(tpath, "wb") as _:
|
with open(tpath, "wb") as _:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -231,21 +224,38 @@ class ThumbSrv(object):
|
|||||||
with self.mutex:
|
with self.mutex:
|
||||||
self.nthr -= 1
|
self.nthr -= 1
|
||||||
|
|
||||||
|
def fancy_pillow(self, im):
|
||||||
|
# exif_transpose is expensive (loads full image + unconditional copy)
|
||||||
|
r = max(*self.res) * 2
|
||||||
|
im.thumbnail((r, r), resample=Image.LANCZOS)
|
||||||
|
try:
|
||||||
|
k = next(k for k, v in ExifTags.TAGS.items() if v == "Orientation")
|
||||||
|
exif = im.getexif()
|
||||||
|
rot = int(exif[k])
|
||||||
|
del exif[k]
|
||||||
|
except:
|
||||||
|
rot = 1
|
||||||
|
|
||||||
|
rots = {8: Image.ROTATE_90, 3: Image.ROTATE_180, 6: Image.ROTATE_270}
|
||||||
|
if rot in rots:
|
||||||
|
im = im.transpose(rots[rot])
|
||||||
|
|
||||||
|
if self.args.th_no_crop:
|
||||||
|
im.thumbnail(self.res, resample=Image.LANCZOS)
|
||||||
|
else:
|
||||||
|
iw, ih = im.size
|
||||||
|
dw, dh = self.res
|
||||||
|
res = (min(iw, dw), min(ih, dh))
|
||||||
|
im = ImageOps.fit(im, res, method=Image.LANCZOS)
|
||||||
|
|
||||||
|
return im
|
||||||
|
|
||||||
def conv_pil(self, abspath, tpath):
|
def conv_pil(self, abspath, tpath):
|
||||||
with Image.open(fsenc(abspath)) as im:
|
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:
|
try:
|
||||||
im.thumbnail(res2, resample=Image.LANCZOS)
|
im = self.fancy_pillow(im)
|
||||||
if crop:
|
except Exception as ex:
|
||||||
iw, ih = im.size
|
self.log("fancy_pillow {}".format(ex), "1;30")
|
||||||
dw, dh = self.res
|
|
||||||
res = (min(iw, dw), min(ih, dh))
|
|
||||||
im = ImageOps.fit(im, res, method=Image.LANCZOS)
|
|
||||||
except:
|
|
||||||
im.thumbnail(self.res)
|
im.thumbnail(self.res)
|
||||||
|
|
||||||
fmts = ["RGB", "L"]
|
fmts = ["RGB", "L"]
|
||||||
@@ -260,13 +270,14 @@ class ThumbSrv(object):
|
|||||||
fmts += ["RGBA", "LA"]
|
fmts += ["RGBA", "LA"]
|
||||||
args["method"] = 6
|
args["method"] = 6
|
||||||
else:
|
else:
|
||||||
pass # default q = 75
|
# default q = 75
|
||||||
|
args["progressive"] = True
|
||||||
|
|
||||||
if im.mode not in fmts:
|
if im.mode not in fmts:
|
||||||
print("conv {}".format(im.mode))
|
# print("conv {}".format(im.mode))
|
||||||
im = im.convert("RGB")
|
im = im.convert("RGB")
|
||||||
|
|
||||||
im.save(tpath, quality=40, method=6)
|
im.save(tpath, **args)
|
||||||
|
|
||||||
def conv_ffmpeg(self, abspath, tpath):
|
def conv_ffmpeg(self, abspath, tpath):
|
||||||
ret, _ = ffprobe(abspath)
|
ret, _ = ffprobe(abspath)
|
||||||
@@ -296,8 +307,10 @@ class ThumbSrv(object):
|
|||||||
cmd += seek
|
cmd += seek
|
||||||
cmd += [
|
cmd += [
|
||||||
b"-i", fsenc(abspath),
|
b"-i", fsenc(abspath),
|
||||||
|
b"-map", b"0:v:0",
|
||||||
b"-vf", scale,
|
b"-vf", scale,
|
||||||
b"-vframes", b"1",
|
b"-frames:v", b"1",
|
||||||
|
b"-metadata:s:v:0", b"rotate=0",
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
@@ -315,11 +328,13 @@ class ThumbSrv(object):
|
|||||||
]
|
]
|
||||||
|
|
||||||
cmd += [fsenc(tpath)]
|
cmd += [fsenc(tpath)]
|
||||||
|
# self.log((b" ".join(cmd)).decode("utf-8"))
|
||||||
|
|
||||||
ret, sout, serr = runcmd(*cmd)
|
ret, sout, serr = runcmd(cmd)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
msg = ["ff: {}".format(x) for x in serr.split("\n")]
|
m = "FFmpeg failed (probably a corrupt video file):\n"
|
||||||
self.log("FFmpeg failed:\n" + "\n".join(msg), c="1;30")
|
m += "\n".join(["ff: {}".format(x) for x in serr.split("\n")])
|
||||||
|
self.log(m, c="1;30")
|
||||||
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
|
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
|
||||||
|
|
||||||
def poke(self, tdir):
|
def poke(self, tdir):
|
||||||
@@ -331,7 +346,7 @@ class ThumbSrv(object):
|
|||||||
p1 = os.path.dirname(tdir)
|
p1 = os.path.dirname(tdir)
|
||||||
p2 = os.path.dirname(p1)
|
p2 = os.path.dirname(p1)
|
||||||
for dp in [tdir, p1, p2]:
|
for dp in [tdir, p1, p2]:
|
||||||
os.utime(fsenc(dp), (ts, ts))
|
bos.utime(dp, (ts, ts))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -358,7 +373,7 @@ class ThumbSrv(object):
|
|||||||
prev_b64 = None
|
prev_b64 = None
|
||||||
prev_fp = None
|
prev_fp = None
|
||||||
try:
|
try:
|
||||||
ents = os.listdir(thumbpath)
|
ents = bos.listdir(thumbpath)
|
||||||
except:
|
except:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -369,7 +384,7 @@ class ThumbSrv(object):
|
|||||||
|
|
||||||
# "top" or b64 prefix/full (a folder)
|
# "top" or b64 prefix/full (a folder)
|
||||||
if len(f) <= 3 or len(f) == 24:
|
if len(f) <= 3 or len(f) == 24:
|
||||||
age = now - os.path.getmtime(fp)
|
age = now - bos.path.getmtime(fp)
|
||||||
if age > maxage:
|
if age > maxage:
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
safe = True
|
safe = True
|
||||||
@@ -401,7 +416,7 @@ class ThumbSrv(object):
|
|||||||
|
|
||||||
if b64 == prev_b64:
|
if b64 == prev_b64:
|
||||||
self.log("rm replaced [{}]".format(fp))
|
self.log("rm replaced [{}]".format(fp))
|
||||||
os.unlink(prev_fp)
|
bos.unlink(prev_fp)
|
||||||
|
|
||||||
prev_b64 = b64
|
prev_b64 = b64
|
||||||
prev_fp = fp
|
prev_fp = fp
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import time
|
|||||||
import threading
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .__init__ import unicode
|
||||||
from .util import s3dec, Pebkac, min_ex
|
from .util import s3dec, Pebkac, min_ex
|
||||||
|
from .bos import bos
|
||||||
from .up2k import up2k_wark_from_hashlist
|
from .up2k import up2k_wark_from_hashlist
|
||||||
|
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ class U2idx(object):
|
|||||||
self.timeout = self.args.srch_time
|
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 = {}
|
||||||
@@ -57,16 +59,19 @@ class U2idx(object):
|
|||||||
raise Pebkac(500, min_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
|
||||||
|
|
||||||
histpath = self.asrv.vfs.histtab[ptop]
|
histpath = self.asrv.vfs.histtab[ptop]
|
||||||
db_path = os.path.join(histpath, "up2k.db")
|
db_path = os.path.join(histpath, "up2k.db")
|
||||||
if not os.path.exists(db_path):
|
if not bos.path.exists(db_path):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
cur = sqlite3.connect(db_path).cursor()
|
cur = sqlite3.connect(db_path, 2).cursor()
|
||||||
self.cur[ptop] = cur
|
self.cur[ptop] = cur
|
||||||
return cur
|
return cur
|
||||||
|
|
||||||
@@ -83,10 +88,12 @@ class U2idx(object):
|
|||||||
is_date = False
|
is_date = False
|
||||||
kw_key = ["(", ")", "and ", "or ", "not "]
|
kw_key = ["(", ")", "and ", "or ", "not "]
|
||||||
kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "]
|
kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "]
|
||||||
ptn_mt = re.compile(r"^\.?[a-z]+$")
|
ptn_mt = re.compile(r"^\.?[a-z_-]+$")
|
||||||
mt_ctr = 0
|
mt_ctr = 0
|
||||||
mt_keycmp = "substr(up.w,1,16)"
|
mt_keycmp = "substr(up.w,1,16)"
|
||||||
mt_keycmp2 = None
|
mt_keycmp2 = None
|
||||||
|
ptn_lc = re.compile(r" (mt[0-9]+\.v) ([=<!>]+) \? $")
|
||||||
|
ptn_lcv = re.compile(r"[a-zA-Z]")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
uq = uq.strip()
|
uq = uq.strip()
|
||||||
@@ -179,6 +186,21 @@ class U2idx(object):
|
|||||||
va.append(v)
|
va.append(v)
|
||||||
is_key = True
|
is_key = True
|
||||||
|
|
||||||
|
# lowercase tag searches
|
||||||
|
m = ptn_lc.search(q)
|
||||||
|
if not m or not ptn_lcv.search(unicode(v)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
va.pop()
|
||||||
|
va.append(v.lower())
|
||||||
|
q = q[: m.start()]
|
||||||
|
|
||||||
|
field, oper = m.groups()
|
||||||
|
if oper in ["=", "=="]:
|
||||||
|
q += " {} like ? ".format(field)
|
||||||
|
else:
|
||||||
|
q += " lower({}) {} ? ".format(field, oper)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.run_query(vols, joins + "where " + q, va)
|
return self.run_query(vols, joins + "where " + q, va)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@@ -222,7 +244,7 @@ class U2idx(object):
|
|||||||
sret = []
|
sret = []
|
||||||
c = cur.execute(q, v)
|
c = cur.execute(q, v)
|
||||||
for hit in c:
|
for hit in c:
|
||||||
w, ts, sz, rd, fn = hit
|
w, ts, sz, rd, fn, ip, at = hit
|
||||||
lim -= 1
|
lim -= 1
|
||||||
if lim <= 0:
|
if lim <= 0:
|
||||||
break
|
break
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals
|
|||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import stat
|
||||||
import time
|
import time
|
||||||
import base64
|
import base64
|
||||||
import select
|
import select
|
||||||
@@ -16,8 +17,9 @@ import mimetypes
|
|||||||
import contextlib
|
import contextlib
|
||||||
import subprocess as sp # nosec
|
import subprocess as sp # nosec
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
from .__init__ import PY2, WINDOWS, ANYWIN
|
from .__init__ import PY2, WINDOWS, ANYWIN, VT100
|
||||||
from .stolen import surrogateescape
|
from .stolen import surrogateescape
|
||||||
|
|
||||||
FAKE_MP = False
|
FAKE_MP = False
|
||||||
@@ -42,6 +44,23 @@ else:
|
|||||||
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
|
from StringIO import StringIO as BytesIO
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
struct.unpack(b">i", b"idgi")
|
||||||
|
spack = struct.pack
|
||||||
|
sunpack = struct.unpack
|
||||||
|
except:
|
||||||
|
|
||||||
|
def spack(f, *a, **ka):
|
||||||
|
return struct.pack(f.decode("ascii"), *a, **ka)
|
||||||
|
|
||||||
|
def sunpack(f, *a, **ka):
|
||||||
|
return struct.unpack(f.decode("ascii"), *a, **ka)
|
||||||
|
|
||||||
|
|
||||||
|
ansi_re = re.compile("\033\\[[^mK]*[mK]")
|
||||||
|
|
||||||
|
|
||||||
surrogateescape.register_surrogateescape()
|
surrogateescape.register_surrogateescape()
|
||||||
FS_ENCODING = sys.getfilesystemencoding()
|
FS_ENCODING = sys.getfilesystemencoding()
|
||||||
if WINDOWS and PY2:
|
if WINDOWS and PY2:
|
||||||
@@ -61,6 +80,7 @@ HTTPCODE = {
|
|||||||
403: "Forbidden",
|
403: "Forbidden",
|
||||||
404: "Not Found",
|
404: "Not Found",
|
||||||
405: "Method Not Allowed",
|
405: "Method Not Allowed",
|
||||||
|
411: "Length Required",
|
||||||
413: "Payload Too Large",
|
413: "Payload Too Large",
|
||||||
416: "Requested Range Not Satisfiable",
|
416: "Requested Range Not Satisfiable",
|
||||||
422: "Unprocessable Entity",
|
422: "Unprocessable Entity",
|
||||||
@@ -123,20 +143,6 @@ REKOBO_KEY = {
|
|||||||
REKOBO_LKEY = {k.lower(): v for k, v in REKOBO_KEY.items()}
|
REKOBO_LKEY = {k.lower(): v for k, v in REKOBO_KEY.items()}
|
||||||
|
|
||||||
|
|
||||||
class Counter(object):
|
|
||||||
def __init__(self, v=0):
|
|
||||||
self.v = v
|
|
||||||
self.mutex = threading.Lock()
|
|
||||||
|
|
||||||
def add(self, delta=1):
|
|
||||||
with self.mutex:
|
|
||||||
self.v += delta
|
|
||||||
|
|
||||||
def set(self, absval):
|
|
||||||
with self.mutex:
|
|
||||||
self.v = absval
|
|
||||||
|
|
||||||
|
|
||||||
class Cooldown(object):
|
class Cooldown(object):
|
||||||
def __init__(self, maxage):
|
def __init__(self, maxage):
|
||||||
self.maxage = maxage
|
self.maxage = maxage
|
||||||
@@ -201,17 +207,22 @@ class ProgressPrinter(threading.Thread):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
msg = None
|
msg = None
|
||||||
|
fmt = " {}\033[K\r" if VT100 else " {} $\r"
|
||||||
while not self.end:
|
while not self.end:
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
if msg == self.msg or self.end:
|
if msg == self.msg or self.end:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
msg = self.msg
|
msg = self.msg
|
||||||
uprint(" {}\033[K\r".format(msg))
|
uprint(fmt.format(msg))
|
||||||
if PY2:
|
if PY2:
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
print("\033[K", end="")
|
if VT100:
|
||||||
|
print("\033[K", end="")
|
||||||
|
elif msg:
|
||||||
|
print("------------------------")
|
||||||
|
|
||||||
sys.stdout.flush() # necessary on win10 even w/ stderr btw
|
sys.stdout.flush() # necessary on win10 even w/ stderr btw
|
||||||
|
|
||||||
|
|
||||||
@@ -231,7 +242,7 @@ def nuprint(msg):
|
|||||||
|
|
||||||
def rice_tid():
|
def rice_tid():
|
||||||
tid = threading.current_thread().ident
|
tid = threading.current_thread().ident
|
||||||
c = struct.unpack(b"B" * 5, struct.pack(b">Q", tid)[-5:])
|
c = sunpack(b"B" * 5, spack(b">Q", tid)[-5:])
|
||||||
return "".join("\033[1;37;48;5;{}m{:02x}".format(x, x) for x in c) + "\033[0m"
|
return "".join("\033[1;37;48;5;{}m{:02x}".format(x, x) for x in c) + "\033[0m"
|
||||||
|
|
||||||
|
|
||||||
@@ -282,15 +293,76 @@ def alltrace():
|
|||||||
return "\n".join(rret + bret)
|
return "\n".join(rret + bret)
|
||||||
|
|
||||||
|
|
||||||
|
def start_stackmon(arg_str, nid):
|
||||||
|
suffix = "-{}".format(nid) if nid else ""
|
||||||
|
fp, f = arg_str.rsplit(",", 1)
|
||||||
|
f = int(f)
|
||||||
|
t = threading.Thread(
|
||||||
|
target=stackmon,
|
||||||
|
args=(fp, f, suffix),
|
||||||
|
name="stackmon" + suffix,
|
||||||
|
)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
|
||||||
|
def stackmon(fp, ival, suffix):
|
||||||
|
ctr = 0
|
||||||
|
while True:
|
||||||
|
ctr += 1
|
||||||
|
time.sleep(ival)
|
||||||
|
st = "{}, {}\n{}".format(ctr, time.time(), alltrace())
|
||||||
|
with open(fp + suffix, "wb") as f:
|
||||||
|
f.write(st.encode("utf-8", "replace"))
|
||||||
|
|
||||||
|
|
||||||
|
def start_log_thrs(logger, ival, nid):
|
||||||
|
ival = int(ival)
|
||||||
|
tname = lname = "log-thrs"
|
||||||
|
if nid:
|
||||||
|
tname = "logthr-n{}-i{:x}".format(nid, os.getpid())
|
||||||
|
lname = tname[3:]
|
||||||
|
|
||||||
|
t = threading.Thread(
|
||||||
|
target=log_thrs,
|
||||||
|
args=(logger, ival, lname),
|
||||||
|
name=tname,
|
||||||
|
)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
|
||||||
|
def log_thrs(log, ival, name):
|
||||||
|
while True:
|
||||||
|
time.sleep(ival)
|
||||||
|
tv = [x.name for x in threading.enumerate()]
|
||||||
|
tv = [
|
||||||
|
x.split("-")[0]
|
||||||
|
if x.startswith("httpconn-") or x.startswith("thumb-")
|
||||||
|
else "listen"
|
||||||
|
if "-listen-" in x
|
||||||
|
else x
|
||||||
|
for x in tv
|
||||||
|
if not x.startswith("pydevd.")
|
||||||
|
]
|
||||||
|
tv = ["{}\033[36m{}".format(v, k) for k, v in sorted(Counter(tv).items())]
|
||||||
|
log(name, "\033[0m \033[33m".join(tv), 3)
|
||||||
|
|
||||||
|
|
||||||
|
def vol_san(vols, txt):
|
||||||
|
for vol in vols:
|
||||||
|
txt = txt.replace(vol.realpath.encode("utf-8"), vol.vpath.encode("utf-8"))
|
||||||
|
|
||||||
|
return txt
|
||||||
|
|
||||||
|
|
||||||
def min_ex():
|
def min_ex():
|
||||||
et, ev, tb = sys.exc_info()
|
et, ev, tb = sys.exc_info()
|
||||||
tb = traceback.extract_tb(tb, 2)
|
tb = traceback.extract_tb(tb)
|
||||||
ex = [
|
fmt = "{} @ {} <{}>: {}"
|
||||||
"{} @ {} <{}>: {}".format(fp.split(os.sep)[-1], ln, fun, txt)
|
ex = [fmt.format(fp.split(os.sep)[-1], ln, fun, txt) for fp, ln, fun, txt in tb]
|
||||||
for fp, ln, fun, txt in tb
|
ex.append("[{}] {}".format(et.__name__, ev))
|
||||||
]
|
return "\n".join(ex[-8:])
|
||||||
ex.append("{}: {}".format(et.__name__, ev))
|
|
||||||
return "\n".join(ex)
|
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
@@ -351,7 +423,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:
|
||||||
@@ -628,6 +700,17 @@ def humansize(sz, terse=False):
|
|||||||
return ret.replace("iB", "").replace(" ", "")
|
return ret.replace("iB", "").replace(" ", "")
|
||||||
|
|
||||||
|
|
||||||
|
def unhumanize(sz):
|
||||||
|
try:
|
||||||
|
return float(sz)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
mul = sz[-1:].lower()
|
||||||
|
mul = {"k": 1024, "m": 1024 * 1024, "g": 1024 * 1024 * 1024}.get(mul, 1)
|
||||||
|
return float(sz[:-1]) * mul
|
||||||
|
|
||||||
|
|
||||||
def get_spd(nbyte, t0, t=None):
|
def get_spd(nbyte, t0, t=None):
|
||||||
if t is None:
|
if t is None:
|
||||||
t = time.time()
|
t = time.time()
|
||||||
@@ -648,6 +731,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("/"):
|
||||||
@@ -664,7 +757,7 @@ def undot(path):
|
|||||||
return "/".join(ret)
|
return "/".join(ret)
|
||||||
|
|
||||||
|
|
||||||
def sanitize_fn(fn, ok="", bad=[]):
|
def sanitize_fn(fn, ok, bad):
|
||||||
if "/" not in ok:
|
if "/" not in ok:
|
||||||
fn = fn.replace("\\", "/").split("/")[-1]
|
fn = fn.replace("\\", "/").split("/")[-1]
|
||||||
|
|
||||||
@@ -693,6 +786,19 @@ def sanitize_fn(fn, ok="", bad=[]):
|
|||||||
return fn.strip()
|
return fn.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def absreal(fpath):
|
||||||
|
try:
|
||||||
|
return fsdec(os.path.abspath(os.path.realpath(fsenc(fpath))))
|
||||||
|
except:
|
||||||
|
if not WINDOWS:
|
||||||
|
raise
|
||||||
|
|
||||||
|
# cpython bug introduced in 3.8, still exists in 3.9.1,
|
||||||
|
# some win7sp1 and win10:20H2 boxes cannot realpath a
|
||||||
|
# networked drive letter such as b"n:" or b"n:\\"
|
||||||
|
return os.path.abspath(os.path.realpath(fpath))
|
||||||
|
|
||||||
|
|
||||||
def u8safe(txt):
|
def u8safe(txt):
|
||||||
try:
|
try:
|
||||||
return txt.encode("utf-8", "xmlcharrefreplace").decode("utf-8", "replace")
|
return txt.encode("utf-8", "xmlcharrefreplace").decode("utf-8", "replace")
|
||||||
@@ -750,6 +856,13 @@ def unquotep(txt):
|
|||||||
return w8dec(unq2)
|
return w8dec(unq2)
|
||||||
|
|
||||||
|
|
||||||
|
def vsplit(vpath):
|
||||||
|
if "/" not in vpath:
|
||||||
|
return "", vpath
|
||||||
|
|
||||||
|
return vpath.rsplit("/", 1)
|
||||||
|
|
||||||
|
|
||||||
def w8dec(txt):
|
def w8dec(txt):
|
||||||
"""decodes filesystem-bytes to wtf8"""
|
"""decodes filesystem-bytes to wtf8"""
|
||||||
if PY2:
|
if PY2:
|
||||||
@@ -894,35 +1007,24 @@ def yieldfile(fn):
|
|||||||
yield buf
|
yield buf
|
||||||
|
|
||||||
|
|
||||||
def hashcopy(actor, fin, fout):
|
def hashcopy(fin, fout):
|
||||||
is_mp = actor.is_mp
|
|
||||||
hashobj = hashlib.sha512()
|
hashobj = hashlib.sha512()
|
||||||
tlen = 0
|
tlen = 0
|
||||||
for buf in fin:
|
for buf in fin:
|
||||||
if is_mp:
|
|
||||||
actor.workload += 1
|
|
||||||
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, actor=None):
|
def sendfile_py(lower, upper, f, s):
|
||||||
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(1024 * 32, remains))
|
buf = f.read(min(1024 * 32, remains))
|
||||||
if not buf:
|
if not buf:
|
||||||
@@ -960,6 +1062,9 @@ def sendfile_kern(lower, upper, f, s):
|
|||||||
|
|
||||||
|
|
||||||
def statdir(logger, scandir, lstat, top):
|
def statdir(logger, scandir, lstat, top):
|
||||||
|
if lstat and not os.supports_follow_symlinks:
|
||||||
|
scandir = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
btop = fsenc(top)
|
btop = fsenc(top)
|
||||||
if scandir and hasattr(os, "scandir"):
|
if scandir and hasattr(os, "scandir"):
|
||||||
@@ -969,8 +1074,7 @@ def statdir(logger, scandir, lstat, top):
|
|||||||
try:
|
try:
|
||||||
yield [fsdec(fh.name), fh.stat(follow_symlinks=not lstat)]
|
yield [fsdec(fh.name), fh.stat(follow_symlinks=not lstat)]
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
msg = "scan-stat: \033[36m{} @ {}"
|
logger(src, "[s] {} @ {}".format(repr(ex), fsdec(fh.path)), 6)
|
||||||
logger(msg.format(repr(ex), fsdec(fh.path)))
|
|
||||||
else:
|
else:
|
||||||
src = "listdir"
|
src = "listdir"
|
||||||
fun = os.lstat if lstat else os.stat
|
fun = os.lstat if lstat else os.stat
|
||||||
@@ -979,11 +1083,33 @@ def statdir(logger, scandir, lstat, top):
|
|||||||
try:
|
try:
|
||||||
yield [fsdec(name), fun(abspath)]
|
yield [fsdec(name), fun(abspath)]
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
msg = "list-stat: \033[36m{} @ {}"
|
logger(src, "[s] {} @ {}".format(repr(ex), fsdec(abspath)), 6)
|
||||||
logger(msg.format(repr(ex), fsdec(abspath)))
|
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger("{}: \033[31m{} @ {}".format(src, repr(ex), top))
|
logger(src, "{} @ {}".format(repr(ex), top), 1)
|
||||||
|
|
||||||
|
|
||||||
|
def rmdirs(logger, scandir, lstat, top):
|
||||||
|
if not os.path.exists(fsenc(top)) or not os.path.isdir(fsenc(top)):
|
||||||
|
top = os.path.dirname(top)
|
||||||
|
|
||||||
|
dirs = statdir(logger, scandir, lstat, top)
|
||||||
|
dirs = [x[0] for x in dirs if stat.S_ISDIR(x[1].st_mode)]
|
||||||
|
dirs = [os.path.join(top, x) for x in dirs]
|
||||||
|
ok = []
|
||||||
|
ng = []
|
||||||
|
for d in dirs[::-1]:
|
||||||
|
a, b = rmdirs(logger, scandir, lstat, d)
|
||||||
|
ok += a
|
||||||
|
ng += b
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.rmdir(fsenc(top))
|
||||||
|
ok.append(top)
|
||||||
|
except:
|
||||||
|
ng.append(top)
|
||||||
|
|
||||||
|
return ok, ng
|
||||||
|
|
||||||
|
|
||||||
def unescape_cookie(orig):
|
def unescape_cookie(orig):
|
||||||
@@ -1020,10 +1146,16 @@ def guess_mime(url, fallback="application/octet-stream"):
|
|||||||
except:
|
except:
|
||||||
return fallback
|
return fallback
|
||||||
|
|
||||||
return MIMES.get(ext) or mimetypes.guess_type(url)[0] or fallback
|
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", "replace")
|
stdout = stdout.decode("utf-8", "replace")
|
||||||
@@ -1031,8 +1163,8 @@ def runcmd(*argv):
|
|||||||
return [p.returncode, stdout, stderr]
|
return [p.returncode, stdout, stderr]
|
||||||
|
|
||||||
|
|
||||||
def chkcmd(*argv):
|
def chkcmd(argv):
|
||||||
ok, sout, serr = runcmd(*argv)
|
ok, sout, serr = runcmd(argv)
|
||||||
if ok != 0:
|
if ok != 0:
|
||||||
raise Exception(serr)
|
raise Exception(serr)
|
||||||
|
|
||||||
@@ -1054,10 +1186,7 @@ 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)
|
||||||
rv = f.read(4)
|
rv = f.read(4)
|
||||||
try:
|
return sunpack(b"I", rv)[0]
|
||||||
return struct.unpack(b"I", rv)[0]
|
|
||||||
except:
|
|
||||||
return struct.unpack("I", rv)[0]
|
|
||||||
|
|
||||||
|
|
||||||
def py_desc():
|
def py_desc():
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ window.baguetteBox = (function () {
|
|||||||
captions: true,
|
captions: true,
|
||||||
buttons: 'auto',
|
buttons: 'auto',
|
||||||
noScrollbars: false,
|
noScrollbars: false,
|
||||||
bodyClass: 'baguetteBox-open',
|
bodyClass: 'bbox-open',
|
||||||
titleTag: false,
|
titleTag: false,
|
||||||
async: false,
|
async: false,
|
||||||
preload: 2,
|
preload: 2,
|
||||||
@@ -22,37 +22,46 @@ window.baguetteBox = (function () {
|
|||||||
afterHide: null,
|
afterHide: null,
|
||||||
onChange: null,
|
onChange: null,
|
||||||
},
|
},
|
||||||
overlay, slider, previousButton, nextButton, closeButton,
|
overlay, slider, btnPrev, btnNext, btnHelp, btnRotL, btnRotR, btnSel, btnVmode, btnClose,
|
||||||
currentGallery = [],
|
currentGallery = [],
|
||||||
currentIndex = 0,
|
currentIndex = 0,
|
||||||
isOverlayVisible = false,
|
isOverlayVisible = false,
|
||||||
touch = {}, // start-pos
|
touch = {}, // start-pos
|
||||||
touchFlag = false, // busy
|
touchFlag = false, // busy
|
||||||
regex = /.+\.(gif|jpe?g|png|webp)/i,
|
re_i = /.+\.(gif|jpe?g|png|webp)(\?|$)/i,
|
||||||
|
re_v = /.+\.(webm|mp4)(\?|$)/i,
|
||||||
data = {}, // all galleries
|
data = {}, // all galleries
|
||||||
imagesElements = [],
|
imagesElements = [],
|
||||||
documentLastFocus = null;
|
documentLastFocus = null,
|
||||||
|
isFullscreen = false,
|
||||||
|
vmute = false,
|
||||||
|
vloop = false,
|
||||||
|
vnext = false,
|
||||||
|
resume_mp = false;
|
||||||
|
|
||||||
var overlayClickHandler = function (event) {
|
var onFSC = function (e) {
|
||||||
if (event.target.id.indexOf('baguette-img') !== -1) {
|
isFullscreen = !!document.fullscreenElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
var overlayClickHandler = function (e) {
|
||||||
|
if (e.target.id.indexOf('baguette-img') !== -1)
|
||||||
hideOverlay();
|
hideOverlay();
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var touchstartHandler = function (event) {
|
var touchstartHandler = function (e) {
|
||||||
touch.count++;
|
touch.count = e.touches.length;
|
||||||
if (touch.count > 1) {
|
if (touch.count > 1)
|
||||||
touch.multitouch = true;
|
touch.multitouch = true;
|
||||||
}
|
|
||||||
touch.startX = event.changedTouches[0].pageX;
|
touch.startX = e.changedTouches[0].pageX;
|
||||||
touch.startY = event.changedTouches[0].pageY;
|
touch.startY = e.changedTouches[0].pageY;
|
||||||
};
|
};
|
||||||
var touchmoveHandler = function (event) {
|
var touchmoveHandler = function (e) {
|
||||||
if (touchFlag || touch.multitouch) {
|
if (touchFlag || touch.multitouch)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
event.preventDefault ? event.preventDefault() : event.returnValue = false;
|
e.preventDefault ? e.preventDefault() : e.returnValue = false;
|
||||||
var touchEvent = event.touches[0] || event.changedTouches[0];
|
var touchEvent = e.touches[0] || e.changedTouches[0];
|
||||||
if (touchEvent.pageX - touch.startX > 40) {
|
if (touchEvent.pageX - touch.startX > 40) {
|
||||||
touchFlag = true;
|
touchFlag = true;
|
||||||
showPreviousImage();
|
showPreviousImage();
|
||||||
@@ -63,21 +72,24 @@ window.baguetteBox = (function () {
|
|||||||
hideOverlay();
|
hideOverlay();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var touchendHandler = function () {
|
var touchendHandler = function (e) {
|
||||||
touch.count--;
|
touch.count--;
|
||||||
if (touch.count <= 0) {
|
if (e && e.touches)
|
||||||
|
touch.count = e.touches.length;
|
||||||
|
|
||||||
|
if (touch.count <= 0)
|
||||||
touch.multitouch = false;
|
touch.multitouch = false;
|
||||||
}
|
|
||||||
touchFlag = false;
|
touchFlag = false;
|
||||||
};
|
};
|
||||||
var contextmenuHandler = function () {
|
var contextmenuHandler = function () {
|
||||||
touchendHandler();
|
touchendHandler();
|
||||||
};
|
};
|
||||||
|
|
||||||
var trapFocusInsideOverlay = function (event) {
|
var trapFocusInsideOverlay = function (e) {
|
||||||
if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(event.target))) {
|
if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(e.target))) {
|
||||||
event.stopPropagation();
|
e.stopPropagation();
|
||||||
initFocus();
|
btnClose.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,7 +100,7 @@ window.baguetteBox = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindImageClickListeners(selector, userOptions) {
|
function bindImageClickListeners(selector, userOptions) {
|
||||||
var galleryNodeList = document.querySelectorAll(selector);
|
var galleryNodeList = QSA(selector);
|
||||||
var selectorData = {
|
var selectorData = {
|
||||||
galleries: [],
|
galleries: [],
|
||||||
nodeList: galleryNodeList
|
nodeList: galleryNodeList
|
||||||
@@ -96,33 +108,26 @@ window.baguetteBox = (function () {
|
|||||||
data[selector] = selectorData;
|
data[selector] = selectorData;
|
||||||
|
|
||||||
[].forEach.call(galleryNodeList, function (galleryElement) {
|
[].forEach.call(galleryNodeList, function (galleryElement) {
|
||||||
if (userOptions && userOptions.filter) {
|
|
||||||
regex = userOptions.filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tagsNodeList = [];
|
var tagsNodeList = [];
|
||||||
if (galleryElement.tagName === 'A') {
|
if (galleryElement.tagName === 'A')
|
||||||
tagsNodeList = [galleryElement];
|
tagsNodeList = [galleryElement];
|
||||||
} else {
|
else
|
||||||
tagsNodeList = galleryElement.getElementsByTagName('a');
|
tagsNodeList = galleryElement.getElementsByTagName('a');
|
||||||
}
|
|
||||||
|
|
||||||
tagsNodeList = [].filter.call(tagsNodeList, function (element) {
|
tagsNodeList = [].filter.call(tagsNodeList, function (element) {
|
||||||
if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1) {
|
if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1)
|
||||||
return regex.test(element.href);
|
return re_i.test(element.href) || re_v.test(element.href);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if (tagsNodeList.length === 0) {
|
if (!tagsNodeList.length)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
var gallery = [];
|
var gallery = [];
|
||||||
[].forEach.call(tagsNodeList, function (imageElement, imageIndex) {
|
[].forEach.call(tagsNodeList, function (imageElement, imageIndex) {
|
||||||
var imageElementClickHandler = function (event) {
|
var imageElementClickHandler = function (e) {
|
||||||
if (event && event.ctrlKey)
|
if (ctrl(e))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
event.preventDefault ? event.preventDefault() : event.returnValue = false;
|
e.preventDefault ? e.preventDefault() : e.returnValue = false;
|
||||||
prepareOverlay(gallery, userOptions);
|
prepareOverlay(gallery, userOptions);
|
||||||
showOverlay(imageIndex);
|
showOverlay(imageIndex);
|
||||||
};
|
};
|
||||||
@@ -140,93 +145,232 @@ window.baguetteBox = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearCachedData() {
|
function clearCachedData() {
|
||||||
for (var selector in data) {
|
for (var selector in data)
|
||||||
if (data.hasOwnProperty(selector)) {
|
if (data.hasOwnProperty(selector))
|
||||||
removeFromCache(selector);
|
removeFromCache(selector);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFromCache(selector) {
|
function removeFromCache(selector) {
|
||||||
if (!data.hasOwnProperty(selector)) {
|
if (!data.hasOwnProperty(selector))
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
var galleries = data[selector].galleries;
|
var galleries = data[selector].galleries;
|
||||||
[].forEach.call(galleries, function (gallery) {
|
[].forEach.call(galleries, function (gallery) {
|
||||||
[].forEach.call(gallery, function (imageItem) {
|
[].forEach.call(gallery, function (imageItem) {
|
||||||
unbind(imageItem.imageElement, 'click', imageItem.eventHandler);
|
unbind(imageItem.imageElement, 'click', imageItem.eventHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (currentGallery === gallery) {
|
if (currentGallery === gallery)
|
||||||
currentGallery = [];
|
currentGallery = [];
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
delete data[selector];
|
delete data[selector];
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildOverlay() {
|
function buildOverlay() {
|
||||||
overlay = ebi('baguetteBox-overlay');
|
overlay = ebi('bbox-overlay');
|
||||||
if (overlay) {
|
if (!overlay) {
|
||||||
slider = ebi('baguetteBox-slider');
|
var ctr = mknod('div');
|
||||||
previousButton = ebi('previous-button');
|
ctr.innerHTML = (
|
||||||
nextButton = ebi('next-button');
|
'<div id="bbox-overlay" role="dialog">' +
|
||||||
closeButton = ebi('close-button');
|
'<div id="bbox-slider"></div>' +
|
||||||
return;
|
'<button id="bbox-prev" class="bbox-btn" type="button" aria-label="Previous"><</button>' +
|
||||||
|
'<button id="bbox-next" class="bbox-btn" type="button" aria-label="Next">></button>' +
|
||||||
|
'<div id="bbox-btns">' +
|
||||||
|
'<button id="bbox-help" type="button">?</button>' +
|
||||||
|
'<button id="bbox-rotl" type="button">↶</button>' +
|
||||||
|
'<button id="bbox-rotr" type="button">↷</button>' +
|
||||||
|
'<button id="bbox-tsel" type="button">sel</button>' +
|
||||||
|
'<button id="bbox-vmode" type="button" tt="a"></button>' +
|
||||||
|
'<button id="bbox-close" type="button" aria-label="Close">X</button>' +
|
||||||
|
'</div></div>'
|
||||||
|
);
|
||||||
|
overlay = ctr.firstChild;
|
||||||
|
QS('body').appendChild(overlay);
|
||||||
|
tt.att(overlay);
|
||||||
}
|
}
|
||||||
overlay = mknod('div');
|
slider = ebi('bbox-slider');
|
||||||
overlay.setAttribute('role', 'dialog');
|
btnPrev = ebi('bbox-prev');
|
||||||
overlay.id = 'baguetteBox-overlay';
|
btnNext = ebi('bbox-next');
|
||||||
document.getElementsByTagName('body')[0].appendChild(overlay);
|
btnHelp = ebi('bbox-help');
|
||||||
|
btnRotL = ebi('bbox-rotl');
|
||||||
slider = mknod('div');
|
btnRotR = ebi('bbox-rotr');
|
||||||
slider.id = 'baguetteBox-slider';
|
btnSel = ebi('bbox-tsel');
|
||||||
overlay.appendChild(slider);
|
btnVmode = ebi('bbox-vmode');
|
||||||
|
btnClose = ebi('bbox-close');
|
||||||
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();
|
bindEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyDownHandler(event) {
|
function halp() {
|
||||||
switch (event.keyCode) {
|
if (ebi('bbox-halp'))
|
||||||
case 37: // Left
|
return;
|
||||||
showPreviousImage();
|
|
||||||
break;
|
var list = [
|
||||||
case 39: // Right
|
['<b># hotkey</b>', '<b># operation</b>'],
|
||||||
showNextImage();
|
['escape', 'close'],
|
||||||
break;
|
['left, J', 'previous file'],
|
||||||
case 27: // Esc
|
['right, L', 'next file'],
|
||||||
hideOverlay();
|
['home', 'first file'],
|
||||||
break;
|
['end', 'last file'],
|
||||||
case 36: // Home
|
['R', 'rotate (shift=ccw)'],
|
||||||
showFirstImage(event);
|
['S', 'toggle file selection'],
|
||||||
break;
|
['space, P, K', 'video: play / pause'],
|
||||||
case 35: // End
|
['U', 'video: seek 10sec back'],
|
||||||
showLastImage(event);
|
['P', 'video: seek 10sec ahead'],
|
||||||
break;
|
['M', 'video: toggle mute'],
|
||||||
|
['V', 'video: toggle loop'],
|
||||||
|
['C', 'video: toggle auto-next'],
|
||||||
|
['F', 'video: toggle fullscreen'],
|
||||||
|
],
|
||||||
|
d = mknod('table'),
|
||||||
|
html = ['<tbody>'];
|
||||||
|
|
||||||
|
for (var a = 0; a < list.length; a++)
|
||||||
|
html.push('<tr><td>' + list[a][0] + '</td><td>' + list[a][1] + '</td></tr>');
|
||||||
|
|
||||||
|
d.innerHTML = html.join('\n') + '</tbody>';
|
||||||
|
d.setAttribute('id', 'bbox-halp');
|
||||||
|
d.onclick = function () {
|
||||||
|
overlay.removeChild(d);
|
||||||
|
};
|
||||||
|
overlay.appendChild(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyDownHandler(e) {
|
||||||
|
if (e.ctrlKey || e.altKey || e.metaKey || e.isComposing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var k = e.code + '', v = vid();
|
||||||
|
|
||||||
|
if (k == "ArrowLeft" || k == "KeyJ")
|
||||||
|
showPreviousImage();
|
||||||
|
else if (k == "ArrowRight" || k == "KeyL")
|
||||||
|
showNextImage();
|
||||||
|
else if (k == "Escape")
|
||||||
|
hideOverlay();
|
||||||
|
else if (k == "Home")
|
||||||
|
showFirstImage(e);
|
||||||
|
else if (k == "End")
|
||||||
|
showLastImage(e);
|
||||||
|
else if (k == "Space" || k == "KeyP" || k == "KeyK")
|
||||||
|
playpause();
|
||||||
|
else if (k == "KeyU" || k == "KeyO")
|
||||||
|
relseek(k == "KeyU" ? -10 : 10);
|
||||||
|
else if (k == "KeyM" && v) {
|
||||||
|
v.muted = vmute = !vmute;
|
||||||
|
mp_ctl();
|
||||||
}
|
}
|
||||||
|
else if (k == "KeyV" && v) {
|
||||||
|
vloop = !vloop;
|
||||||
|
vnext = vnext && !vloop;
|
||||||
|
setVmode();
|
||||||
|
}
|
||||||
|
else if (k == "KeyC" && v) {
|
||||||
|
vnext = !vnext;
|
||||||
|
vloop = vloop && !vnext;
|
||||||
|
setVmode();
|
||||||
|
}
|
||||||
|
else if (k == "KeyF")
|
||||||
|
try {
|
||||||
|
if (isFullscreen)
|
||||||
|
document.exitFullscreen();
|
||||||
|
else
|
||||||
|
v.requestFullscreen();
|
||||||
|
}
|
||||||
|
catch (ex) { }
|
||||||
|
else if (k == "KeyS")
|
||||||
|
tglsel();
|
||||||
|
else if (k == "KeyR")
|
||||||
|
rotn(e.shiftKey ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVmode() {
|
||||||
|
var v = vid();
|
||||||
|
ebi('bbox-vmode').style.display = v ? '' : 'none';
|
||||||
|
if (!v)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var msg = 'When video ends, ', tts = '', lbl;
|
||||||
|
if (vloop) {
|
||||||
|
lbl = 'Loop';
|
||||||
|
msg += 'repeat it';
|
||||||
|
tts = '$NHotkey: V';
|
||||||
|
}
|
||||||
|
else if (vnext) {
|
||||||
|
lbl = 'Cont';
|
||||||
|
msg += 'continue to next';
|
||||||
|
tts = '$NHotkey: C';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lbl = 'Stop';
|
||||||
|
msg += 'just stop'
|
||||||
|
}
|
||||||
|
btnVmode.setAttribute('aria-label', msg);
|
||||||
|
btnVmode.setAttribute('tt', msg + tts);
|
||||||
|
btnVmode.textContent = lbl;
|
||||||
|
|
||||||
|
v.loop = vloop
|
||||||
|
if (vloop && v.paused)
|
||||||
|
v.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
function tglVmode() {
|
||||||
|
if (vloop) {
|
||||||
|
vnext = true;
|
||||||
|
vloop = false;
|
||||||
|
}
|
||||||
|
else if (vnext)
|
||||||
|
vnext = false;
|
||||||
|
else
|
||||||
|
vloop = true;
|
||||||
|
|
||||||
|
setVmode();
|
||||||
|
if (tt.en)
|
||||||
|
tt.show.bind(this)();
|
||||||
|
}
|
||||||
|
|
||||||
|
function tglsel() {
|
||||||
|
var thumb = currentGallery[currentIndex].imageElement,
|
||||||
|
name = vsplit(thumb.href)[1],
|
||||||
|
files = msel.getall();
|
||||||
|
|
||||||
|
for (var a = 0; a < files.length; a++)
|
||||||
|
if (vsplit(files[a].vp)[1] == name)
|
||||||
|
clmod(ebi(files[a].id).closest('tr'), 'sel', 't');
|
||||||
|
|
||||||
|
msel.selui();
|
||||||
|
selbg();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selbg() {
|
||||||
|
var img = vidimg(),
|
||||||
|
thumb = currentGallery[currentIndex].imageElement,
|
||||||
|
name = vsplit(thumb.href)[1],
|
||||||
|
files = msel.getsel(),
|
||||||
|
sel = false;
|
||||||
|
|
||||||
|
for (var a = 0; a < files.length; a++)
|
||||||
|
if (vsplit(files[a].vp)[1] == name)
|
||||||
|
sel = true;
|
||||||
|
|
||||||
|
ebi('bbox-overlay').style.background = sel ?
|
||||||
|
'rgba(153,34,85,0.7)' : '';
|
||||||
|
|
||||||
|
img.style.borderRadius = sel ? '1em' : '';
|
||||||
|
btnSel.style.color = sel ? '#fff' : '';
|
||||||
|
btnSel.style.background = sel ? '#d48' : '';
|
||||||
|
btnSel.style.textShadow = sel ? '1px 1px 0 #b38' : '';
|
||||||
|
btnSel.style.boxShadow = sel ? '.15em .15em 0 #502' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyUpHandler(e) {
|
||||||
|
if (e.ctrlKey || e.altKey || e.metaKey || e.isComposing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var k = e.code + '';
|
||||||
|
|
||||||
|
if (k == "Space")
|
||||||
|
ev(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
var passiveSupp = false;
|
var passiveSupp = false;
|
||||||
@@ -248,9 +392,14 @@ window.baguetteBox = (function () {
|
|||||||
|
|
||||||
function bindEvents() {
|
function bindEvents() {
|
||||||
bind(overlay, 'click', overlayClickHandler);
|
bind(overlay, 'click', overlayClickHandler);
|
||||||
bind(previousButton, 'click', showPreviousImage);
|
bind(btnPrev, 'click', showPreviousImage);
|
||||||
bind(nextButton, 'click', showNextImage);
|
bind(btnNext, 'click', showNextImage);
|
||||||
bind(closeButton, 'click', hideOverlay);
|
bind(btnClose, 'click', hideOverlay);
|
||||||
|
bind(btnVmode, 'click', tglVmode);
|
||||||
|
bind(btnHelp, 'click', halp);
|
||||||
|
bind(btnRotL, 'click', rotl);
|
||||||
|
bind(btnRotR, 'click', rotr);
|
||||||
|
bind(btnSel, 'click', tglsel);
|
||||||
bind(slider, 'contextmenu', contextmenuHandler);
|
bind(slider, 'contextmenu', contextmenuHandler);
|
||||||
bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||||
bind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
bind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||||
@@ -260,20 +409,26 @@ window.baguetteBox = (function () {
|
|||||||
|
|
||||||
function unbindEvents() {
|
function unbindEvents() {
|
||||||
unbind(overlay, 'click', overlayClickHandler);
|
unbind(overlay, 'click', overlayClickHandler);
|
||||||
unbind(previousButton, 'click', showPreviousImage);
|
unbind(btnPrev, 'click', showPreviousImage);
|
||||||
unbind(nextButton, 'click', showNextImage);
|
unbind(btnNext, 'click', showNextImage);
|
||||||
unbind(closeButton, 'click', hideOverlay);
|
unbind(btnClose, 'click', hideOverlay);
|
||||||
|
unbind(btnVmode, 'click', tglVmode);
|
||||||
|
unbind(btnHelp, 'click', halp);
|
||||||
|
unbind(btnRotL, 'click', rotl);
|
||||||
|
unbind(btnRotR, 'click', rotr);
|
||||||
|
unbind(btnSel, 'click', tglsel);
|
||||||
unbind(slider, 'contextmenu', contextmenuHandler);
|
unbind(slider, 'contextmenu', contextmenuHandler);
|
||||||
unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||||
unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||||
unbind(overlay, 'touchend', touchendHandler);
|
unbind(overlay, 'touchend', touchendHandler);
|
||||||
unbind(document, 'focus', trapFocusInsideOverlay, true);
|
unbind(document, 'focus', trapFocusInsideOverlay, true);
|
||||||
|
timer.rm(rotn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareOverlay(gallery, userOptions) {
|
function prepareOverlay(gallery, userOptions) {
|
||||||
if (currentGallery === gallery) {
|
if (currentGallery === gallery)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
currentGallery = gallery;
|
currentGallery = gallery;
|
||||||
setOptions(userOptions);
|
setOptions(userOptions);
|
||||||
slider.innerHTML = '';
|
slider.innerHTML = '';
|
||||||
@@ -287,8 +442,8 @@ window.baguetteBox = (function () {
|
|||||||
fullImage.id = 'baguette-img-' + i;
|
fullImage.id = 'baguette-img-' + i;
|
||||||
imagesElements.push(fullImage);
|
imagesElements.push(fullImage);
|
||||||
|
|
||||||
imagesFiguresIds.push('baguetteBox-figure-' + i);
|
imagesFiguresIds.push('bbox-figure-' + i);
|
||||||
imagesCaptionsIds.push('baguetteBox-figcaption-' + i);
|
imagesCaptionsIds.push('bbox-figcaption-' + i);
|
||||||
slider.appendChild(imagesElements[i]);
|
slider.appendChild(imagesElements[i]);
|
||||||
}
|
}
|
||||||
overlay.setAttribute('aria-labelledby', imagesFiguresIds.join(' '));
|
overlay.setAttribute('aria-labelledby', imagesFiguresIds.join(' '));
|
||||||
@@ -296,23 +451,21 @@ window.baguetteBox = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setOptions(newOptions) {
|
function setOptions(newOptions) {
|
||||||
if (!newOptions) {
|
if (!newOptions)
|
||||||
newOptions = {};
|
newOptions = {};
|
||||||
}
|
|
||||||
for (var item in defaults) {
|
for (var item in defaults) {
|
||||||
options[item] = defaults[item];
|
options[item] = defaults[item];
|
||||||
if (typeof newOptions[item] !== 'undefined') {
|
if (typeof newOptions[item] !== 'undefined')
|
||||||
options[item] = newOptions[item];
|
options[item] = newOptions[item];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
slider.style.transition = (options.animation === 'fadeIn' ? 'opacity .4s ease' :
|
slider.style.transition = (options.animation === 'fadeIn' ? 'opacity .4s ease' :
|
||||||
options.animation === 'slideIn' ? '' : 'none');
|
options.animation === 'slideIn' ? '' : 'none');
|
||||||
|
|
||||||
if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1)) {
|
if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1))
|
||||||
options.buttons = false;
|
options.buttons = false;
|
||||||
}
|
|
||||||
|
|
||||||
previousButton.style.display = nextButton.style.display = (options.buttons ? '' : 'none');
|
btnPrev.style.display = btnNext.style.display = (options.buttons ? '' : 'none');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showOverlay(chosenImageIndex) {
|
function showOverlay(chosenImageIndex) {
|
||||||
@@ -320,11 +473,12 @@ window.baguetteBox = (function () {
|
|||||||
document.documentElement.style.overflowY = 'hidden';
|
document.documentElement.style.overflowY = 'hidden';
|
||||||
document.body.style.overflowY = 'scroll';
|
document.body.style.overflowY = 'scroll';
|
||||||
}
|
}
|
||||||
if (overlay.style.display === 'block') {
|
if (overlay.style.display === 'block')
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
bind(document, 'keydown', keyDownHandler);
|
bind(document, 'keydown', keyDownHandler);
|
||||||
|
bind(document, 'keyup', keyUpHandler);
|
||||||
|
bind(document, 'fullscreenchange', onFSC);
|
||||||
currentIndex = chosenImageIndex;
|
currentIndex = chosenImageIndex;
|
||||||
touch = {
|
touch = {
|
||||||
count: 0,
|
count: 0,
|
||||||
@@ -341,50 +495,48 @@ window.baguetteBox = (function () {
|
|||||||
// Fade in overlay
|
// Fade in overlay
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
overlay.className = 'visible';
|
overlay.className = 'visible';
|
||||||
if (options.bodyClass && document.body.classList) {
|
if (options.bodyClass && document.body.classList)
|
||||||
document.body.classList.add(options.bodyClass);
|
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.afterShow)
|
||||||
if (options.buttons) {
|
options.afterShow();
|
||||||
previousButton.focus();
|
}, 50);
|
||||||
} else {
|
|
||||||
closeButton.focus();
|
if (options.onChange)
|
||||||
}
|
options.onChange(currentIndex, imagesElements.length);
|
||||||
|
|
||||||
|
documentLastFocus = document.activeElement;
|
||||||
|
btnClose.focus();
|
||||||
|
isOverlayVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideOverlay(e) {
|
function hideOverlay(e) {
|
||||||
ev(e);
|
ev(e);
|
||||||
|
playvid(false);
|
||||||
if (options.noScrollbars) {
|
if (options.noScrollbars) {
|
||||||
document.documentElement.style.overflowY = 'auto';
|
document.documentElement.style.overflowY = 'auto';
|
||||||
document.body.style.overflowY = 'auto';
|
document.body.style.overflowY = 'auto';
|
||||||
}
|
}
|
||||||
if (overlay.style.display === 'none') {
|
if (overlay.style.display === 'none')
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
unbind(document, 'keydown', keyDownHandler);
|
unbind(document, 'keydown', keyDownHandler);
|
||||||
|
unbind(document, 'keyup', keyUpHandler);
|
||||||
|
unbind(document, 'fullscreenchange', onFSC);
|
||||||
// Fade out and hide the overlay
|
// Fade out and hide the overlay
|
||||||
overlay.className = '';
|
overlay.className = '';
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
overlay.style.display = 'none';
|
overlay.style.display = 'none';
|
||||||
if (options.bodyClass && document.body.classList) {
|
if (options.bodyClass && document.body.classList)
|
||||||
document.body.classList.remove(options.bodyClass);
|
document.body.classList.remove(options.bodyClass);
|
||||||
}
|
|
||||||
if (options.afterHide) {
|
var h = ebi('bbox-halp');
|
||||||
|
if (h)
|
||||||
|
h.parentNode.removeChild(h);
|
||||||
|
|
||||||
|
if (options.afterHide)
|
||||||
options.afterHide();
|
options.afterHide();
|
||||||
}
|
|
||||||
documentLastFocus && documentLastFocus.focus();
|
documentLastFocus && documentLastFocus.focus();
|
||||||
isOverlayVisible = false;
|
isOverlayVisible = false;
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -394,59 +546,68 @@ window.baguetteBox = (function () {
|
|||||||
var imageContainer = imagesElements[index];
|
var imageContainer = imagesElements[index];
|
||||||
var galleryItem = currentGallery[index];
|
var galleryItem = currentGallery[index];
|
||||||
|
|
||||||
if (typeof imageContainer === 'undefined' || typeof galleryItem === 'undefined') {
|
if (typeof imageContainer === 'undefined' || typeof galleryItem === 'undefined')
|
||||||
return; // out-of-bounds or gallery dirty
|
return; // out-of-bounds or gallery dirty
|
||||||
}
|
|
||||||
|
|
||||||
if (imageContainer.getElementsByTagName('img')[0]) {
|
if (imageContainer.querySelector('img, video'))
|
||||||
// image is loaded, cb and bail
|
// was loaded, cb and bail
|
||||||
if (callback) {
|
return callback ? callback() : null;
|
||||||
callback();
|
|
||||||
}
|
// maybe unloaded video
|
||||||
return;
|
while (imageContainer.firstChild)
|
||||||
}
|
imageContainer.removeChild(imageContainer.firstChild);
|
||||||
|
|
||||||
var imageElement = galleryItem.imageElement,
|
var imageElement = galleryItem.imageElement,
|
||||||
imageSrc = imageElement.href,
|
imageSrc = imageElement.href,
|
||||||
thumbnailElement = imageElement.getElementsByTagName('img')[0],
|
is_vid = re_v.test(imageSrc),
|
||||||
|
thumbnailElement = imageElement.querySelector('img, video'),
|
||||||
imageCaption = typeof options.captions === 'function' ?
|
imageCaption = typeof options.captions === 'function' ?
|
||||||
options.captions.call(currentGallery, imageElement) :
|
options.captions.call(currentGallery, imageElement) :
|
||||||
imageElement.getAttribute('data-caption') || imageElement.title;
|
imageElement.getAttribute('data-caption') || imageElement.title;
|
||||||
|
|
||||||
|
imageSrc += imageSrc.indexOf('?') < 0 ? '?cache' : '&cache';
|
||||||
|
|
||||||
|
if (is_vid && index != currentIndex)
|
||||||
|
return; // no preload
|
||||||
|
|
||||||
var figure = mknod('figure');
|
var figure = mknod('figure');
|
||||||
figure.id = 'baguetteBox-figure-' + index;
|
figure.id = 'bbox-figure-' + index;
|
||||||
figure.innerHTML = '<div class="baguetteBox-spinner">' +
|
figure.innerHTML = '<div class="bbox-spinner">' +
|
||||||
'<div class="baguetteBox-double-bounce1"></div>' +
|
'<div class="bbox-double-bounce1"></div>' +
|
||||||
'<div class="baguetteBox-double-bounce2"></div>' +
|
'<div class="bbox-double-bounce2"></div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
|
|
||||||
if (options.captions && imageCaption) {
|
if (options.captions && imageCaption) {
|
||||||
var figcaption = mknod('figcaption');
|
var figcaption = mknod('figcaption');
|
||||||
figcaption.id = 'baguetteBox-figcaption-' + index;
|
figcaption.id = 'bbox-figcaption-' + index;
|
||||||
figcaption.innerHTML = imageCaption;
|
figcaption.innerHTML = imageCaption;
|
||||||
figure.appendChild(figcaption);
|
figure.appendChild(figcaption);
|
||||||
}
|
}
|
||||||
imageContainer.appendChild(figure);
|
imageContainer.appendChild(figure);
|
||||||
|
|
||||||
var image = mknod('img');
|
var image = mknod(is_vid ? 'video' : 'img');
|
||||||
image.onload = function () {
|
clmod(imageContainer, 'vid', is_vid);
|
||||||
|
|
||||||
|
image.addEventListener(is_vid ? 'loadedmetadata' : 'load', function () {
|
||||||
// Remove loader element
|
// Remove loader element
|
||||||
var spinner = document.querySelector('#baguette-img-' + index + ' .baguetteBox-spinner');
|
var spinner = QS('#baguette-img-' + index + ' .bbox-spinner');
|
||||||
figure.removeChild(spinner);
|
figure.removeChild(spinner);
|
||||||
if (!options.async && callback) {
|
if (!options.async && callback)
|
||||||
callback();
|
callback();
|
||||||
}
|
});
|
||||||
};
|
|
||||||
image.setAttribute('src', imageSrc);
|
image.setAttribute('src', imageSrc);
|
||||||
image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
|
if (is_vid) {
|
||||||
if (options.titleTag && imageCaption) {
|
image.setAttribute('controls', 'controls');
|
||||||
image.title = imageCaption;
|
image.onended = vidEnd;
|
||||||
}
|
}
|
||||||
|
image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
|
||||||
|
if (options.titleTag && imageCaption)
|
||||||
|
image.title = imageCaption;
|
||||||
|
|
||||||
figure.appendChild(image);
|
figure.appendChild(image);
|
||||||
|
|
||||||
if (options.async && callback) {
|
if (options.async && callback)
|
||||||
callback();
|
callback();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showNextImage(e) {
|
function showNextImage(e) {
|
||||||
@@ -459,26 +620,20 @@ window.baguetteBox = (function () {
|
|||||||
return show(currentIndex - 1);
|
return show(currentIndex - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showFirstImage(event) {
|
function showFirstImage(e) {
|
||||||
if (event) {
|
if (e)
|
||||||
event.preventDefault();
|
e.preventDefault();
|
||||||
}
|
|
||||||
return show(0);
|
return show(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showLastImage(event) {
|
function showLastImage(e) {
|
||||||
if (event) {
|
if (e)
|
||||||
event.preventDefault();
|
e.preventDefault();
|
||||||
}
|
|
||||||
return show(currentGallery.length - 1);
|
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) {
|
function show(index, gallery) {
|
||||||
if (!isOverlayVisible && index >= 0 && index < gallery.length) {
|
if (!isOverlayVisible && index >= 0 && index < gallery.length) {
|
||||||
prepareOverlay(gallery, options);
|
prepareOverlay(gallery, options);
|
||||||
@@ -486,18 +641,25 @@ window.baguetteBox = (function () {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
if (options.animation) {
|
if (options.animation)
|
||||||
bounceAnimation('left');
|
bounceAnimation('left');
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (index >= imagesElements.length) {
|
if (index >= imagesElements.length) {
|
||||||
if (options.animation) {
|
if (options.animation)
|
||||||
bounceAnimation('right');
|
bounceAnimation('right');
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var v = vid();
|
||||||
|
if (v) {
|
||||||
|
v.src = '';
|
||||||
|
v.load();
|
||||||
|
v.parentNode.removeChild(v);
|
||||||
|
}
|
||||||
|
|
||||||
currentIndex = index;
|
currentIndex = index;
|
||||||
loadImage(currentIndex, function () {
|
loadImage(currentIndex, function () {
|
||||||
preloadNext(currentIndex);
|
preloadNext(currentIndex);
|
||||||
@@ -505,17 +667,130 @@ window.baguetteBox = (function () {
|
|||||||
});
|
});
|
||||||
updateOffset();
|
updateOffset();
|
||||||
|
|
||||||
if (options.onChange) {
|
if (options.onChange)
|
||||||
options.onChange(currentIndex, imagesElements.length);
|
options.onChange(currentIndex, imagesElements.length);
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
var prev_cw = 0, prev_ch = 0, unrot_timer = null;
|
||||||
* Triggers the bounce animation
|
function rotn(n) {
|
||||||
* @param {('left'|'right')} direction - Direction of the movement
|
var el = vidimg(),
|
||||||
*/
|
orot = parseInt(el.getAttribute('rot') || 0),
|
||||||
|
frot = orot + (n || 0) * 90;
|
||||||
|
|
||||||
|
if (!frot && !orot)
|
||||||
|
return; // reflow noop
|
||||||
|
|
||||||
|
var co = ebi('bbox-overlay'),
|
||||||
|
cw = co.clientWidth,
|
||||||
|
ch = co.clientHeight;
|
||||||
|
|
||||||
|
if (!n && prev_cw === cw && prev_ch === ch)
|
||||||
|
return; // reflow noop
|
||||||
|
|
||||||
|
prev_cw = cw;
|
||||||
|
prev_ch = ch;
|
||||||
|
var rot = frot,
|
||||||
|
iw = el.naturalWidth || el.videoWidth,
|
||||||
|
ih = el.naturalHeight || el.videoHeight,
|
||||||
|
magic = 4, // idk, works in enough browsers
|
||||||
|
dl = el.closest('div').querySelector('figcaption a'),
|
||||||
|
vw = cw,
|
||||||
|
vh = ch - dl.offsetHeight + magic,
|
||||||
|
pmag = Math.min(1, Math.min(vw / ih, vh / iw)),
|
||||||
|
wmag = Math.min(1, Math.min(vw / iw, vh / ih));
|
||||||
|
|
||||||
|
while (rot < 0) rot += 360;
|
||||||
|
while (rot >= 360) rot -= 360;
|
||||||
|
var q = rot == 90 || rot == 270 ? 1 : 0,
|
||||||
|
mag = q ? pmag : wmag;
|
||||||
|
|
||||||
|
el.style.cssText = 'max-width:none; max-height:none; position:absolute; display:block; margin:0';
|
||||||
|
if (!orot) {
|
||||||
|
el.style.width = iw * wmag + 'px';
|
||||||
|
el.style.height = ih * wmag + 'px';
|
||||||
|
el.style.left = (vw - iw * wmag) / 2 + 'px';
|
||||||
|
el.style.top = (vh - ih * wmag) / 2 - magic + 'px';
|
||||||
|
q = el.offsetHeight;
|
||||||
|
}
|
||||||
|
el.style.width = iw * mag + 'px';
|
||||||
|
el.style.height = ih * mag + 'px';
|
||||||
|
el.style.left = (vw - iw * mag) / 2 + 'px';
|
||||||
|
el.style.top = (vh - ih * mag) / 2 - magic + 'px';
|
||||||
|
el.style.transform = 'rotate(' + frot + 'deg)';
|
||||||
|
el.setAttribute('rot', frot);
|
||||||
|
timer.add(rotn);
|
||||||
|
if (!rot) {
|
||||||
|
clearTimeout(unrot_timer);
|
||||||
|
unrot_timer = setTimeout(unrot, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function rotl() {
|
||||||
|
rotn(-1);
|
||||||
|
}
|
||||||
|
function rotr() {
|
||||||
|
rotn(1);
|
||||||
|
}
|
||||||
|
function unrot() {
|
||||||
|
var el = vidimg(),
|
||||||
|
orot = el.getAttribute('rot'),
|
||||||
|
rot = parseInt(orot || 0);
|
||||||
|
|
||||||
|
while (rot < 0) rot += 360;
|
||||||
|
while (rot >= 360) rot -= 360;
|
||||||
|
if (rot || orot === null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
clmod(el, 'nt', 1);
|
||||||
|
el.removeAttribute('rot');
|
||||||
|
el.removeAttribute("style");
|
||||||
|
rot = el.offsetHeight;
|
||||||
|
clmod(el, 'nt');
|
||||||
|
timer.rm(rotn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function vid() {
|
||||||
|
return imagesElements[currentIndex].querySelector('video');
|
||||||
|
}
|
||||||
|
|
||||||
|
function vidimg() {
|
||||||
|
return imagesElements[currentIndex].querySelector('img, video');
|
||||||
|
}
|
||||||
|
|
||||||
|
function playvid(play) {
|
||||||
|
if (vid())
|
||||||
|
vid()[play ? 'play' : 'pause']();
|
||||||
|
}
|
||||||
|
|
||||||
|
function playpause() {
|
||||||
|
var v = vid();
|
||||||
|
if (v)
|
||||||
|
v[v.paused ? "play" : "pause"]();
|
||||||
|
}
|
||||||
|
|
||||||
|
function relseek(sec) {
|
||||||
|
if (vid())
|
||||||
|
vid().currentTime += sec;
|
||||||
|
}
|
||||||
|
|
||||||
|
function vidEnd() {
|
||||||
|
if (this == vid() && vnext)
|
||||||
|
showNextImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function mp_ctl() {
|
||||||
|
var v = vid();
|
||||||
|
if (!vmute && v && mp.au && !mp.au.paused) {
|
||||||
|
mp.fade_out();
|
||||||
|
resume_mp = true;
|
||||||
|
}
|
||||||
|
else if (resume_mp && (vmute || !v) && mp.au && mp.au.paused) {
|
||||||
|
mp.fade_in();
|
||||||
|
resume_mp = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function bounceAnimation(direction) {
|
function bounceAnimation(direction) {
|
||||||
slider.className = 'bounce-from-' + direction;
|
slider.className = 'bounce-from-' + direction;
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
@@ -524,31 +799,59 @@ window.baguetteBox = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateOffset() {
|
function updateOffset() {
|
||||||
var offset = -currentIndex * 100 + '%';
|
var offset = -currentIndex * 100 + '%',
|
||||||
|
xform = slider.style.perspective !== undefined;
|
||||||
|
|
||||||
if (options.animation === 'fadeIn') {
|
if (options.animation === 'fadeIn') {
|
||||||
slider.style.opacity = 0;
|
slider.style.opacity = 0;
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
slider.style.transform = 'translate3d(' + offset + ',0,0)';
|
xform ?
|
||||||
|
slider.style.transform = 'translate3d(' + offset + ',0,0)' :
|
||||||
|
slider.style.left = offset;
|
||||||
slider.style.opacity = 1;
|
slider.style.opacity = 1;
|
||||||
}, 400);
|
}, 400);
|
||||||
} else {
|
} else {
|
||||||
slider.style.transform = 'translate3d(' + offset + ',0,0)';
|
xform ?
|
||||||
|
slider.style.transform = 'translate3d(' + offset + ',0,0)' :
|
||||||
|
slider.style.left = offset;
|
||||||
}
|
}
|
||||||
|
playvid(false);
|
||||||
|
var v = vid();
|
||||||
|
if (v) {
|
||||||
|
playvid(true);
|
||||||
|
v.muted = vmute;
|
||||||
|
v.loop = vloop;
|
||||||
|
}
|
||||||
|
selbg();
|
||||||
|
mp_ctl();
|
||||||
|
setVmode();
|
||||||
|
|
||||||
|
var el = vidimg();
|
||||||
|
if (el.getAttribute('rot'))
|
||||||
|
timer.add(rotn);
|
||||||
|
else
|
||||||
|
timer.rm(rotn);
|
||||||
|
|
||||||
|
var prev = QS('.full-image.vis');
|
||||||
|
if (prev)
|
||||||
|
clmod(prev, 'vis');
|
||||||
|
|
||||||
|
clmod(el.closest('div'), 'vis', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function preloadNext(index) {
|
function preloadNext(index) {
|
||||||
if (index - currentIndex >= options.preload) {
|
if (index - currentIndex >= options.preload)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
loadImage(index + 1, function () {
|
loadImage(index + 1, function () {
|
||||||
preloadNext(index + 1);
|
preloadNext(index + 1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function preloadPrev(index) {
|
function preloadPrev(index) {
|
||||||
if (currentIndex - index >= options.preload) {
|
if (currentIndex - index >= options.preload)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
loadImage(index - 1, function () {
|
loadImage(index - 1, function () {
|
||||||
preloadPrev(index - 1);
|
preloadPrev(index - 1);
|
||||||
});
|
});
|
||||||
@@ -566,7 +869,8 @@ window.baguetteBox = (function () {
|
|||||||
unbindEvents();
|
unbindEvents();
|
||||||
clearCachedData();
|
clearCachedData();
|
||||||
unbind(document, 'keydown', keyDownHandler);
|
unbind(document, 'keydown', keyDownHandler);
|
||||||
document.getElementsByTagName('body')[0].removeChild(ebi('baguetteBox-overlay'));
|
unbind(document, 'keyup', keyUpHandler);
|
||||||
|
document.getElementsByTagName('body')[0].removeChild(ebi('bbox-overlay'));
|
||||||
data = {};
|
data = {};
|
||||||
currentGallery = [];
|
currentGallery = [];
|
||||||
currentIndex = 0;
|
currentIndex = 0;
|
||||||
@@ -577,6 +881,8 @@ window.baguetteBox = (function () {
|
|||||||
show: show,
|
show: show,
|
||||||
showNext: showNextImage,
|
showNext: showNextImage,
|
||||||
showPrevious: showPreviousImage,
|
showPrevious: showPreviousImage,
|
||||||
|
relseek: relseek,
|
||||||
|
playpause: playpause,
|
||||||
hide: hideOverlay,
|
hide: hideOverlay,
|
||||||
destroy: destroyPlugin
|
destroy: destroyPlugin
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,10 @@
|
|||||||
<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" media="screen" href="/.cpr/ui.css?_={{ ts }}">
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/upload.css{{ ts }}">
|
<link rel="stylesheet" media="screen" href="/.cpr/browser.css?_={{ ts }}">
|
||||||
{%- if css %}
|
{%- if css %}
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="{{ css }}{{ ts }}">
|
<link rel="stylesheet" media="screen" href="{{ css }}?_={{ ts }}">
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -39,32 +39,34 @@
|
|||||||
<div id="op_mkdir" class="opview opbox act">
|
<div id="op_mkdir" class="opview opbox act">
|
||||||
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
||||||
<input type="hidden" name="act" value="mkdir" />
|
<input type="hidden" name="act" value="mkdir" />
|
||||||
<input type="text" name="name" size="30">
|
📂<input type="text" name="name" size="30">
|
||||||
<input type="submit" value="mkdir">
|
<input type="submit" value="make directory">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="op_new_md" class="opview opbox">
|
<div id="op_new_md" class="opview opbox">
|
||||||
<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="new_md" />
|
<input type="hidden" name="act" value="new_md" />
|
||||||
<input type="text" name="name" size="30">
|
📝<input type="text" name="name" size="30">
|
||||||
<input type="submit" value="create doc">
|
<input type="submit" value="new markdown doc">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="op_msg" class="opview opbox act">
|
<div id="op_msg" class="opview opbox act">
|
||||||
<form method="post" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" action="{{ url_suf }}">
|
<form method="post" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" action="{{ url_suf }}">
|
||||||
<input type="text" name="msg" size="30">
|
📟<input type="text" name="msg" size="30">
|
||||||
<input type="submit" value="send msg">
|
<input type="submit" value="send msg to server log">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="op_unpost" class="opview opbox"></div>
|
||||||
|
|
||||||
<div id="op_up2k" class="opview"></div>
|
<div id="op_up2k" class="opview"></div>
|
||||||
|
|
||||||
<div id="op_cfg" class="opview opbox opwide"></div>
|
<div id="op_cfg" class="opview opbox opwide"></div>
|
||||||
|
|
||||||
<h1 id="path">
|
<h1 id="path">
|
||||||
<a href="#" id="entree" tt="show directory tree$NHotkey: B">🌲</a>
|
<a href="#" id="entree" tt="show navpane (directory tree sidebar)$NHotkey: B">🌲</a>
|
||||||
{%- for n in vpnodes %}
|
{%- for n in vpnodes %}
|
||||||
<a href="/{{ n[0] }}">{{ n[1] }}</a>
|
<a href="/{{ n[0] }}">{{ n[1] }}</a>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
@@ -110,7 +112,9 @@
|
|||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
|
<a href="#" id="repl">π</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -121,15 +125,19 @@
|
|||||||
<div id="widget"></div>
|
<div id="widget"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var perms = {{ perms }},
|
var acct = "{{ acct }}",
|
||||||
tag_order_cfg = {{ tag_order }},
|
perms = {{ perms }},
|
||||||
|
def_hcols = {{ def_hcols|tojson }},
|
||||||
have_up2k_idx = {{ have_up2k_idx|tojson }},
|
have_up2k_idx = {{ have_up2k_idx|tojson }},
|
||||||
have_tags_idx = {{ have_tags_idx|tojson }},
|
have_tags_idx = {{ have_tags_idx|tojson }},
|
||||||
|
have_mv = {{ have_mv|tojson }},
|
||||||
|
have_del = {{ have_del|tojson }},
|
||||||
|
have_unpost = {{ have_unpost|tojson }},
|
||||||
have_zip = {{ have_zip|tojson }};
|
have_zip = {{ have_zip|tojson }};
|
||||||
</script>
|
</script>
|
||||||
<script src="/.cpr/util.js{{ ts }}"></script>
|
<script src="/.cpr/util.js?_={{ ts }}"></script>
|
||||||
<script src="/.cpr/browser.js{{ ts }}"></script>
|
<script src="/.cpr/browser.js?_={{ ts }}"></script>
|
||||||
<script src="/.cpr/up2k.js{{ ts }}"></script>
|
<script src="/.cpr/up2k.js?_={{ ts }}"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -54,7 +54,7 @@
|
|||||||
<div>{{ logues[1] }}</div><br />
|
<div>{{ logues[1] }}</div><br />
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
<h2><a href="{{ url_suf }}{{ url_suf and '&' or '?' }}h">control-panel</a></h2>
|
<h2><a href="/{{ url_suf }}{{ url_suf and '&' or '?' }}h">control-panel</a></h2>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ html, body {
|
|||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
#repl {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: .2em;
|
||||||
|
border: none;
|
||||||
|
color: inherit;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
#mtw {
|
#mtw {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -15,6 +23,10 @@ html, body {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 1.5em;
|
padding: 0 1.5em;
|
||||||
}
|
}
|
||||||
|
#toast {
|
||||||
|
bottom: auto;
|
||||||
|
top: 1.4em;
|
||||||
|
}
|
||||||
pre, code, a {
|
pre, code, a {
|
||||||
color: #480;
|
color: #480;
|
||||||
background: #f7f7f7;
|
background: #f7f7f7;
|
||||||
@@ -26,7 +38,7 @@ pre, code, a {
|
|||||||
code {
|
code {
|
||||||
font-size: .96em;
|
font-size: .96em;
|
||||||
}
|
}
|
||||||
pre, code {
|
pre, code, tt {
|
||||||
font-family: 'scp', monospace, monospace;
|
font-family: 'scp', monospace, monospace;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
@@ -166,7 +178,7 @@ small {
|
|||||||
z-index: 99;
|
z-index: 99;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-family: monospace, monospace;
|
font-family: 'scp', monospace, monospace;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
line-height: .1em;
|
line-height: .1em;
|
||||||
@@ -502,6 +514,9 @@ blink {
|
|||||||
border-bottom: .07em solid #4ac;
|
border-bottom: .07em solid #4ac;
|
||||||
padding: 0 .3em;
|
padding: 0 .3em;
|
||||||
}
|
}
|
||||||
|
#repl {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
#toc>ul {
|
#toc>ul {
|
||||||
border-left: .1em solid #84c4dd;
|
border-left: .1em solid #84c4dd;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<!DOCTYPE html><html><head>
|
<!DOCTYPE html><html><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.7">
|
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
||||||
<link href="/.cpr/md.css" rel="stylesheet">
|
<link rel="stylesheet" href="/.cpr/ui.css?_={{ ts }}">
|
||||||
|
<link rel="stylesheet" href="/.cpr/md.css?_={{ ts }}">
|
||||||
{%- if edit %}
|
{%- if edit %}
|
||||||
<link href="/.cpr/md2.css" rel="stylesheet">
|
<link rel="stylesheet" href="/.cpr/md2.css?_={{ ts }}">
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -14,9 +15,9 @@
|
|||||||
<a id="lightswitch" href="#">go dark</a>
|
<a id="lightswitch" href="#">go dark</a>
|
||||||
<a id="navtoggle" href="#">hide nav</a>
|
<a id="navtoggle" href="#">hide nav</a>
|
||||||
{%- if edit %}
|
{%- if edit %}
|
||||||
<a id="save" href="?edit">save</a>
|
<a id="save" href="?edit" tt="Hotkey: ctrl-s">save</a>
|
||||||
<a id="sbs" href="#">sbs</a>
|
<a id="sbs" href="#" tt="editor and preview side by side">sbs</a>
|
||||||
<a id="nsbs" href="#">editor</a>
|
<a id="nsbs" href="#" tt="switch between editor and preview$NHotkey: ctrl-e">editor</a>
|
||||||
<div id="toolsbox">
|
<div id="toolsbox">
|
||||||
<a id="tools" href="#">tools</a>
|
<a id="tools" href="#">tools</a>
|
||||||
<a id="fmt_table" href="#">prettify table (ctrl-k)</a>
|
<a id="fmt_table" href="#">prettify table (ctrl-k)</a>
|
||||||
@@ -26,8 +27,8 @@
|
|||||||
<a id="help" href="#">help</a>
|
<a id="help" href="#">help</a>
|
||||||
</div>
|
</div>
|
||||||
{%- else %}
|
{%- else %}
|
||||||
<a href="?edit">edit (basic)</a>
|
<a href="?edit" tt="good: higher performance$Ngood: same document width as viewer$Nbad: assumes you know markdown">edit (basic)</a>
|
||||||
<a href="?edit2">edit (fancy)</a>
|
<a href="?edit2" tt="not in-house so probably less buggy">edit (fancy)</a>
|
||||||
<a href="?raw">view raw</a>
|
<a href="?raw">view raw</a>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="mp"></div>
|
<div id="mp"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<a href="#" id="repl">π</a>
|
||||||
|
|
||||||
{%- if edit %}
|
{%- if edit %}
|
||||||
<div id="helpbox">
|
<div id="helpbox">
|
||||||
@@ -131,25 +133,25 @@ var md_opt = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
var btn = document.getElementById("lightswitch");
|
var l = localStorage,
|
||||||
var toggle = function (e) {
|
drk = l.getItem('lightmode') != 1,
|
||||||
if (e) e.preventDefault();
|
btn = document.getElementById("lightswitch"),
|
||||||
var dark = !document.documentElement.getAttribute("class");
|
f = function (e) {
|
||||||
document.documentElement.setAttribute("class", dark ? "dark" : "");
|
if (e) { e.preventDefault(); drk = !drk; }
|
||||||
btn.innerHTML = "go " + (dark ? "light" : "dark");
|
document.documentElement.setAttribute("class", drk? "dark":"light");
|
||||||
if (window.localStorage)
|
btn.innerHTML = "go " + (drk ? "light":"dark");
|
||||||
localStorage.setItem('lightmode', dark ? 0 : 1);
|
l.setItem('lightmode', drk? 0:1);
|
||||||
};
|
};
|
||||||
btn.onclick = toggle;
|
|
||||||
if (window.localStorage && localStorage.getItem('lightmode') != 1)
|
btn.onclick = f;
|
||||||
toggle();
|
f();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
</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>
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ function md_plug_err(ex, js) {
|
|||||||
var lns = js.split('\n');
|
var lns = js.split('\n');
|
||||||
if (ln < lns.length) {
|
if (ln < lns.length) {
|
||||||
o = mknod('span');
|
o = mknod('span');
|
||||||
o.style.cssText = 'color:#ac2;font-size:.9em;font-family:scp;display:block';
|
o.style.cssText = "color:#ac2;font-size:.9em;font-family:'scp',monospace,monospace;display:block";
|
||||||
o.textContent = lns[ln - 1];
|
o.textContent = lns[ln - 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,7 @@ function md_plug_err(ex, js) {
|
|||||||
errbox.style.cssText = 'position:absolute;top:0;left:0;padding:1em .5em;background:#2b2b2b;color:#fc5'
|
errbox.style.cssText = 'position:absolute;top:0;left:0;padding:1em .5em;background:#2b2b2b;color:#fc5'
|
||||||
errbox.textContent = msg;
|
errbox.textContent = msg;
|
||||||
errbox.onclick = function () {
|
errbox.onclick = function () {
|
||||||
alert('' + ex.stack);
|
modal.alert('<pre>' + esc(ex.stack) + '</pre>');
|
||||||
};
|
};
|
||||||
if (o) {
|
if (o) {
|
||||||
errbox.appendChild(o);
|
errbox.appendChild(o);
|
||||||
@@ -530,3 +530,6 @@ dom_navtgl.onclick = function () {
|
|||||||
|
|
||||||
if (sread('hidenav') == 1)
|
if (sread('hidenav') == 1)
|
||||||
dom_navtgl.onclick();
|
dom_navtgl.onclick();
|
||||||
|
|
||||||
|
if (window['tt'])
|
||||||
|
tt.init();
|
||||||
|
|||||||
@@ -84,13 +84,10 @@ html.dark #save.force-save {
|
|||||||
#save.disabled {
|
#save.disabled {
|
||||||
opacity: .4;
|
opacity: .4;
|
||||||
}
|
}
|
||||||
#helpbox,
|
#helpbox {
|
||||||
#toast {
|
|
||||||
background: #f7f7f7;
|
background: #f7f7f7;
|
||||||
border-radius: .4em;
|
border-radius: .4em;
|
||||||
z-index: 9001;
|
z-index: 9001;
|
||||||
}
|
|
||||||
#helpbox {
|
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
@@ -107,19 +104,7 @@ html.dark #save.force-save {
|
|||||||
}
|
}
|
||||||
html.dark #helpbox {
|
html.dark #helpbox {
|
||||||
box-shadow: 0 .5em 2em #444;
|
box-shadow: 0 .5em 2em #444;
|
||||||
}
|
|
||||||
html.dark #helpbox,
|
|
||||||
html.dark #toast {
|
|
||||||
background: #222;
|
background: #222;
|
||||||
border: 1px solid #079;
|
border: 1px solid #079;
|
||||||
border-width: 1px 0;
|
border-width: 1px 0;
|
||||||
}
|
}
|
||||||
#toast {
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
padding: .6em 0;
|
|
||||||
position: fixed;
|
|
||||||
top: 30%;
|
|
||||||
transition: opacity 0.2s ease-in-out;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ function Modpoll() {
|
|||||||
|
|
||||||
var skip = null;
|
var skip = null;
|
||||||
|
|
||||||
if (ebi('toast'))
|
if (toast.visible)
|
||||||
skip = 'toast';
|
skip = 'toast';
|
||||||
|
|
||||||
else if (this.skip_one)
|
else if (this.skip_one)
|
||||||
@@ -285,16 +285,15 @@ function Modpoll() {
|
|||||||
console.log("modpoll diff |" + server_ref.length + "|, |" + server_now.length + "|");
|
console.log("modpoll diff |" + server_ref.length + "|, |" + server_now.length + "|");
|
||||||
this.modpoll.disabled = true;
|
this.modpoll.disabled = true;
|
||||||
var msg = [
|
var msg = [
|
||||||
"The document has changed on the server.<br />" +
|
"The document has changed on the server.",
|
||||||
"The changes will NOT be loaded into your editor automatically.",
|
"The changes will NOT be loaded into your editor automatically.",
|
||||||
|
"",
|
||||||
"Press F5 or CTRL-R to refresh the page,<br />" +
|
"Press F5 or CTRL-R to refresh the page,",
|
||||||
"replacing your document with the server copy.",
|
"replacing your document with the server copy.",
|
||||||
|
"",
|
||||||
"You can click this message to ignore and contnue."
|
"You can close this message to ignore and contnue."
|
||||||
];
|
];
|
||||||
return toast(false, "box-shadow:0 1em 2em rgba(64,64,64,0.8);font-weight:normal",
|
return toast.warn(0, msg.join('\n'));
|
||||||
36, "<p>" + msg.join('</p>\n<p>') + '</p>');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('modpoll eq');
|
console.log('modpoll eq');
|
||||||
@@ -323,57 +322,56 @@ function save(e) {
|
|||||||
var save_btn = ebi("save"),
|
var save_btn = ebi("save"),
|
||||||
save_cls = save_btn.getAttribute('class') + '';
|
save_cls = save_btn.getAttribute('class') + '';
|
||||||
|
|
||||||
if (save_cls.indexOf('disabled') >= 0) {
|
if (save_cls.indexOf('disabled') >= 0)
|
||||||
toast(true, ";font-size:2em;color:#c90", 9, "no changes");
|
return toast.inf(2, "no changes");
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var force = (save_cls.indexOf('force-save') >= 0);
|
var force = (save_cls.indexOf('force-save') >= 0);
|
||||||
if (force && !confirm('confirm that you wish to lose the changes made on the server since you opened this document')) {
|
function save2() {
|
||||||
alert('ok, aborted');
|
var txt = dom_src.value,
|
||||||
return;
|
fd = new FormData();
|
||||||
|
|
||||||
|
fd.append("act", "tput");
|
||||||
|
fd.append("lastmod", (force ? -1 : last_modified));
|
||||||
|
fd.append("body", txt);
|
||||||
|
|
||||||
|
var url = (document.location + '').split('?')[0];
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', url, true);
|
||||||
|
xhr.responseType = 'text';
|
||||||
|
xhr.onreadystatechange = save_cb;
|
||||||
|
xhr.btn = save_btn;
|
||||||
|
xhr.txt = txt;
|
||||||
|
|
||||||
|
modpoll.skip_one = true; // skip one iteration while we save
|
||||||
|
xhr.send(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
var txt = dom_src.value;
|
if (!force)
|
||||||
|
save2();
|
||||||
var fd = new FormData();
|
else
|
||||||
fd.append("act", "tput");
|
modal.confirm('confirm that you wish to lose the changes made on the server since you opened this document', save2, function () {
|
||||||
fd.append("lastmod", (force ? -1 : last_modified));
|
toast.inf(3, 'aborted');
|
||||||
fd.append("body", txt);
|
});
|
||||||
|
|
||||||
var url = (document.location + '').split('?')[0];
|
|
||||||
var xhr = new XMLHttpRequest();
|
|
||||||
xhr.open('POST', url, true);
|
|
||||||
xhr.responseType = 'text';
|
|
||||||
xhr.onreadystatechange = save_cb;
|
|
||||||
xhr.btn = save_btn;
|
|
||||||
xhr.txt = txt;
|
|
||||||
|
|
||||||
modpoll.skip_one = true; // skip one iteration while we save
|
|
||||||
xhr.send(fd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function save_cb() {
|
function save_cb() {
|
||||||
if (this.readyState != XMLHttpRequest.DONE)
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.status !== 200) {
|
if (this.status !== 200)
|
||||||
alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var r;
|
var r;
|
||||||
try {
|
try {
|
||||||
r = JSON.parse(this.responseText);
|
r = JSON.parse(this.responseText);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
alert('Failed to parse reply from server:\n\n' + this.responseText);
|
return toast.err(0, 'Failed to parse reply from server:\n\n' + this.responseText);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
if (!this.btn.classList.contains('force-save')) {
|
if (!clgot(this.btn, 'force-save')) {
|
||||||
this.btn.classList.add('force-save');
|
clmod(this.btn, 'force-save', 1);
|
||||||
var msg = [
|
var msg = [
|
||||||
'This file has been modified since you started editing it!\n',
|
'This file has been modified since you started editing it!\n',
|
||||||
'if you really want to overwrite, press save again.\n',
|
'if you really want to overwrite, press save again.\n',
|
||||||
@@ -383,15 +381,13 @@ function save_cb() {
|
|||||||
r.lastmod + ' lastmod on the server now,',
|
r.lastmod + ' lastmod on the server now,',
|
||||||
r.now + ' server time now,\n',
|
r.now + ' server time now,\n',
|
||||||
];
|
];
|
||||||
alert(msg.join('\n'));
|
return toast.err(0, msg.join('\n'));
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
alert('Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
|
return toast.err(0, 'Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.btn.classList.remove('force-save');
|
clmod(this.btn, 'force-save');
|
||||||
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
|
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
|
||||||
|
|
||||||
run_savechk(r.lastmod, this.txt, this.btn, 0);
|
run_savechk(r.lastmod, this.txt, this.btn, 0);
|
||||||
@@ -415,10 +411,8 @@ function savechk_cb() {
|
|||||||
if (this.readyState != XMLHttpRequest.DONE)
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.status !== 200) {
|
if (this.status !== 200)
|
||||||
alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var doc1 = this.txt.replace(/\r\n/g, "\n");
|
var doc1 = this.txt.replace(/\r\n/g, "\n");
|
||||||
var doc2 = this.responseText.replace(/\r\n/g, "\n");
|
var doc2 = this.responseText.replace(/\r\n/g, "\n");
|
||||||
@@ -431,58 +425,22 @@ function savechk_cb() {
|
|||||||
}, 100);
|
}, 100);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
alert(
|
modal.alert(
|
||||||
'Error! The document on the server does not appear to have saved correctly (your editor contents and the server copy is not identical). Place the document on your clipboard for now and check the server logs for hints\n\n' +
|
'Error! The document on the server does not appear to have saved correctly (your editor contents and the server copy is not identical). Place the document on your clipboard for now and check the server logs for hints\n\n' +
|
||||||
'Length: yours=' + doc1.length + ', server=' + doc2.length
|
'Length: yours=' + doc1.length + ', server=' + doc2.length
|
||||||
);
|
);
|
||||||
alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']');
|
modal.alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']');
|
||||||
alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']');
|
modal.alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
last_modified = this.lastmod;
|
last_modified = this.lastmod;
|
||||||
server_md = this.txt;
|
server_md = this.txt;
|
||||||
draw_md();
|
draw_md();
|
||||||
toast(true, ";font-size:6em;font-family:serif;color:#9b4", 4,
|
toast.ok(2, 'save OK' + (this.ntry ? '\nattempt ' + this.ntry : ''));
|
||||||
'OK✔️<span style="font-size:.2em;color:#999;position:absolute">' + this.ntry + '</span>');
|
|
||||||
|
|
||||||
modpoll.disabled = false;
|
modpoll.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toast(autoclose, style, width, msg) {
|
|
||||||
var ok = ebi("toast");
|
|
||||||
if (ok)
|
|
||||||
ok.parentNode.removeChild(ok);
|
|
||||||
|
|
||||||
style = "width:" + width + "em;left:calc(50% - " + (width / 2) + "em);" + style;
|
|
||||||
ok = mknod('div');
|
|
||||||
ok.setAttribute('id', 'toast');
|
|
||||||
ok.setAttribute('style', style);
|
|
||||||
ok.innerHTML = msg;
|
|
||||||
var parent = ebi('m');
|
|
||||||
document.documentElement.appendChild(ok);
|
|
||||||
|
|
||||||
var hide = function (delay) {
|
|
||||||
delay = delay || 0;
|
|
||||||
|
|
||||||
setTimeout(function () {
|
|
||||||
ok.style.opacity = 0;
|
|
||||||
}, delay);
|
|
||||||
|
|
||||||
setTimeout(function () {
|
|
||||||
if (ok.parentNode)
|
|
||||||
ok.parentNode.removeChild(ok);
|
|
||||||
}, delay + 250);
|
|
||||||
}
|
|
||||||
|
|
||||||
ok.onclick = function () {
|
|
||||||
hide(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (autoclose)
|
|
||||||
hide(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// firefox bug: initial selection offset isn't cleared properly through js
|
// firefox bug: initial selection offset isn't cleared properly through js
|
||||||
var ff_clearsel = (function () {
|
var ff_clearsel = (function () {
|
||||||
@@ -761,7 +719,7 @@ function fmt_table(e) {
|
|||||||
|
|
||||||
var ind2 = tab[a].match(re_ind)[0];
|
var ind2 = tab[a].match(re_ind)[0];
|
||||||
if (ind != ind2 && a != 1) // the table can be a list entry or something, ignore [0]
|
if (ind != ind2 && a != 1) // the table can be a list entry or something, ignore [0]
|
||||||
return alert(err + 'indentation mismatch on row#2 and ' + row_name + ',\n' + tab[a]);
|
return toast.err(7, err + 'indentation mismatch on row#2 and ' + row_name + ',\n' + tab[a]);
|
||||||
|
|
||||||
var t = tab[a].slice(ind.length);
|
var t = tab[a].slice(ind.length);
|
||||||
t = t.replace(re_lpipe, "");
|
t = t.replace(re_lpipe, "");
|
||||||
@@ -771,7 +729,7 @@ function fmt_table(e) {
|
|||||||
if (a == 0)
|
if (a == 0)
|
||||||
ncols = tab[a].length;
|
ncols = tab[a].length;
|
||||||
else if (ncols < tab[a].length)
|
else if (ncols < tab[a].length)
|
||||||
return alert(err + 'num.columns(' + row_name + ') exceeding row#2; ' + ncols + ' < ' + tab[a].length);
|
return toast.err(7, err + 'num.columns(' + row_name + ') exceeding row#2; ' + ncols + ' < ' + tab[a].length);
|
||||||
|
|
||||||
// if row has less columns than row2, fill them in
|
// if row has less columns than row2, fill them in
|
||||||
while (tab[a].length < ncols)
|
while (tab[a].length < ncols)
|
||||||
@@ -788,7 +746,7 @@ function fmt_table(e) {
|
|||||||
for (var col = 0; col < tab[1].length; col++) {
|
for (var col = 0; col < tab[1].length; col++) {
|
||||||
var m = tab[1][col].match(re_align);
|
var m = tab[1][col].match(re_align);
|
||||||
if (!m)
|
if (!m)
|
||||||
return alert(err + 'invalid column specification, row#2, col ' + (col + 1) + ', [' + tab[1][col] + ']');
|
return toast.err(7, err + 'invalid column specification, row#2, col ' + (col + 1) + ', [' + tab[1][col] + ']');
|
||||||
|
|
||||||
if (m[2]) {
|
if (m[2]) {
|
||||||
if (m[1])
|
if (m[1])
|
||||||
@@ -876,10 +834,9 @@ function mark_uni(e) {
|
|||||||
ptn = new RegExp('([^' + js_uni_whitelist + ']+)', 'g'),
|
ptn = new RegExp('([^' + js_uni_whitelist + ']+)', 'g'),
|
||||||
mod = txt.replace(/\r/g, "").replace(ptn, "\u2588\u2770$1\u2771");
|
mod = txt.replace(/\r/g, "").replace(ptn, "\u2588\u2770$1\u2771");
|
||||||
|
|
||||||
if (txt == mod) {
|
if (txt == mod)
|
||||||
alert('no results; no modifications were made');
|
return toast.inf(5, 'no results; no modifications were made');
|
||||||
return;
|
|
||||||
}
|
|
||||||
dom_src.value = mod;
|
dom_src.value = mod;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -893,10 +850,9 @@ function iter_uni(e) {
|
|||||||
re = new RegExp('([^' + js_uni_whitelist + ']+)'),
|
re = new RegExp('([^' + js_uni_whitelist + ']+)'),
|
||||||
m = re.exec(txt.slice(ofs));
|
m = re.exec(txt.slice(ofs));
|
||||||
|
|
||||||
if (!m) {
|
if (!m)
|
||||||
alert('no more hits from cursor onwards');
|
return toast.inf(5, 'no more hits from cursor onwards');
|
||||||
return;
|
|
||||||
}
|
|
||||||
ofs += m.index;
|
ofs += m.index;
|
||||||
|
|
||||||
dom_src.setSelectionRange(ofs, ofs + m[0].length, "forward");
|
dom_src.setSelectionRange(ofs, ofs + m[0].length, "forward");
|
||||||
@@ -911,12 +867,10 @@ function iter_uni(e) {
|
|||||||
function cfg_uni(e) {
|
function cfg_uni(e) {
|
||||||
if (e) e.preventDefault();
|
if (e) e.preventDefault();
|
||||||
|
|
||||||
var reply = prompt("unicode whitelist", esc_uni_whitelist);
|
modal.prompt("unicode whitelist", esc_uni_whitelist, function (reply) {
|
||||||
if (reply === null)
|
esc_uni_whitelist = reply;
|
||||||
return;
|
js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\'');
|
||||||
|
}, null);
|
||||||
esc_uni_whitelist = reply;
|
|
||||||
js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\'');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -924,10 +878,9 @@ function cfg_uni(e) {
|
|||||||
(function () {
|
(function () {
|
||||||
function keydown(ev) {
|
function keydown(ev) {
|
||||||
ev = ev || window.event;
|
ev = ev || window.event;
|
||||||
var kc = ev.keyCode || ev.which;
|
var kc = ev.code || ev.keyCode || ev.which;
|
||||||
var ctrl = ev.ctrlKey || ev.metaKey;
|
//console.log(ev.key, ev.code, ev.keyCode, ev.which);
|
||||||
//console.log(ev.code, kc);
|
if (ctrl(ev) && (ev.code == "KeyS" || kc == 83)) {
|
||||||
if (ctrl && (ev.code == "KeyS" || kc == 83)) {
|
|
||||||
save();
|
save();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -936,23 +889,15 @@ function cfg_uni(e) {
|
|||||||
if (d)
|
if (d)
|
||||||
d.click();
|
d.click();
|
||||||
}
|
}
|
||||||
if (document.activeElement == dom_src) {
|
if (document.activeElement != dom_src)
|
||||||
if (ev.code == "Tab" || kc == 9) {
|
return true;
|
||||||
md_indent(ev.shiftKey);
|
|
||||||
return false;
|
if (ctrl(ev)) {
|
||||||
}
|
if (ev.code == "KeyH" || kc == 72) {
|
||||||
if (ctrl && (ev.code == "KeyH" || kc == 72)) {
|
|
||||||
md_header(ev.shiftKey);
|
md_header(ev.shiftKey);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ctrl && (ev.code == "Home" || kc == 36)) {
|
if (ev.code == "KeyZ" || kc == 90) {
|
||||||
md_home(ev.shiftKey);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!ctrl && !ev.shiftKey && (ev.code == "Enter" || kc == 13)) {
|
|
||||||
return md_newline();
|
|
||||||
}
|
|
||||||
if (ctrl && (ev.code == "KeyZ" || kc == 90)) {
|
|
||||||
if (ev.shiftKey)
|
if (ev.shiftKey)
|
||||||
action_stack.redo();
|
action_stack.redo();
|
||||||
else
|
else
|
||||||
@@ -960,33 +905,45 @@ function cfg_uni(e) {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (ctrl && (ev.code == "KeyY" || kc == 89)) {
|
if (ev.code == "KeyY" || kc == 89) {
|
||||||
action_stack.redo();
|
action_stack.redo();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ctrl && !ev.shiftKey && kc == 8) {
|
if (ev.code == "KeyK") {
|
||||||
return md_backspace();
|
|
||||||
}
|
|
||||||
if (ctrl && (ev.code == "KeyK")) {
|
|
||||||
fmt_table();
|
fmt_table();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (ctrl && (ev.code == "KeyU")) {
|
if (ev.code == "KeyU") {
|
||||||
iter_uni();
|
iter_uni();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (ctrl && (ev.code == "KeyE")) {
|
if (ev.code == "KeyE") {
|
||||||
dom_nsbs.click();
|
dom_nsbs.click();
|
||||||
//fmt_table();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var up = ev.code == "ArrowUp" || kc == 38;
|
var up = ev.code == "ArrowUp" || kc == 38;
|
||||||
var dn = ev.code == "ArrowDown" || kc == 40;
|
var dn = ev.code == "ArrowDown" || kc == 40;
|
||||||
if (ctrl && (up || dn)) {
|
if (up || dn) {
|
||||||
md_p_jump(dn);
|
md_p_jump(dn);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
if (ev.code == "Tab" || kc == 9) {
|
||||||
|
md_indent(ev.shiftKey);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ev.code == "Home" || kc == 36) {
|
||||||
|
md_home(ev.shiftKey);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!ev.shiftKey && (ev.code == "Enter" || kc == 13)) {
|
||||||
|
return md_newline();
|
||||||
|
}
|
||||||
|
if (!ev.shiftKey && kc == 8) {
|
||||||
|
return md_backspace();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
document.onkeydown = keydown;
|
document.onkeydown = keydown;
|
||||||
ebi('save').onclick = save;
|
ebi('save').onclick = save;
|
||||||
|
|||||||
@@ -18,6 +18,19 @@ html, body {
|
|||||||
background: #f7f7f7;
|
background: #f7f7f7;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
#toast {
|
||||||
|
bottom: auto;
|
||||||
|
top: 1.4em;
|
||||||
|
}
|
||||||
|
#repl {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: .5em;
|
||||||
|
border: none;
|
||||||
|
color: inherit;
|
||||||
|
background: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
#mn {
|
#mn {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin: 1.3em 0 .7em 1em;
|
margin: 1.3em 0 .7em 1em;
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
<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 rel="stylesheet" href="/.cpr/ui.css?_={{ ts }}">
|
||||||
<link href="/.cpr/deps/mini-fa.css" rel="stylesheet">
|
<link rel="stylesheet" href="/.cpr/mde.css?_={{ ts }}">
|
||||||
<link href="/.cpr/deps/easymde.css" rel="stylesheet">
|
<link rel="stylesheet" href="/.cpr/deps/mini-fa.css?_={{ ts }}">
|
||||||
|
<link rel="stylesheet" href="/.cpr/deps/easymde.css?_={{ ts }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="mw">
|
<div id="mw">
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
<textarea id="mt" style="display:none" autocomplete="off">{{ md }}</textarea>
|
<textarea id="mt" style="display:none" autocomplete="off">{{ md }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<a href="#" id="repl">π</a>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
var last_modified = {{ lastmod }};
|
var last_modified = {{ lastmod }};
|
||||||
@@ -30,20 +32,19 @@ var md_opt = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var lightswitch = (function () {
|
var lightswitch = (function () {
|
||||||
var fun = function () {
|
var l = localStorage,
|
||||||
var dark = !document.documentElement.getAttribute("class");
|
drk = l.getItem('lightmode') != 1,
|
||||||
document.documentElement.setAttribute("class", dark ? "dark" : "");
|
f = function (e) {
|
||||||
if (window.localStorage)
|
if (e) drk = !drk;
|
||||||
localStorage.setItem('lightmode', dark ? 0 : 1);
|
document.documentElement.setAttribute("class", drk? "dark":"light");
|
||||||
};
|
l.setItem('lightmode', drk? 0:1);
|
||||||
if (window.localStorage && localStorage.getItem('lightmode') != 1)
|
};
|
||||||
fun();
|
f();
|
||||||
|
return f;
|
||||||
return fun;
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
</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>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ function set_jumpto() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function jumpto(ev) {
|
function jumpto(ev) {
|
||||||
var tgt = ev.target || ev.srcElement;
|
var tgt = ev.target;
|
||||||
var ln = null;
|
var ln = null;
|
||||||
while (tgt && !ln) {
|
while (tgt && !ln) {
|
||||||
ln = tgt.getAttribute('data-ln');
|
ln = tgt.getAttribute('data-ln');
|
||||||
@@ -96,65 +96,61 @@ function md_changed(mde, on_srv) {
|
|||||||
var md_now = mde.value();
|
var md_now = mde.value();
|
||||||
var save_btn = QS('.editor-toolbar button.save');
|
var save_btn = QS('.editor-toolbar button.save');
|
||||||
|
|
||||||
if (md_now == window.md_saved)
|
clmod(save_btn, 'disabled', md_now == window.md_saved);
|
||||||
save_btn.classList.add('disabled');
|
|
||||||
else
|
|
||||||
save_btn.classList.remove('disabled');
|
|
||||||
|
|
||||||
set_jumpto();
|
set_jumpto();
|
||||||
}
|
}
|
||||||
|
|
||||||
function save(mde) {
|
function save(mde) {
|
||||||
var save_btn = QS('.editor-toolbar button.save');
|
var save_btn = QS('.editor-toolbar button.save');
|
||||||
if (save_btn.classList.contains('disabled')) {
|
if (clgot(save_btn, 'disabled'))
|
||||||
alert('there is nothing to save');
|
return toast.inf(2, 'no changes');
|
||||||
return;
|
|
||||||
}
|
var force = clgot(save_btn, 'force-save');
|
||||||
var force = save_btn.classList.contains('force-save');
|
function save2() {
|
||||||
if (force && !confirm('confirm that you wish to lose the changes made on the server since you opened this document')) {
|
var txt = mde.value();
|
||||||
alert('ok, aborted');
|
|
||||||
return;
|
var fd = new FormData();
|
||||||
|
fd.append("act", "tput");
|
||||||
|
fd.append("lastmod", (force ? -1 : last_modified));
|
||||||
|
fd.append("body", txt);
|
||||||
|
|
||||||
|
var url = (document.location + '').split('?')[0];
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', url, true);
|
||||||
|
xhr.responseType = 'text';
|
||||||
|
xhr.onreadystatechange = save_cb;
|
||||||
|
xhr.btn = save_btn;
|
||||||
|
xhr.mde = mde;
|
||||||
|
xhr.txt = txt;
|
||||||
|
xhr.send(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
var txt = mde.value();
|
if (!force)
|
||||||
|
save2();
|
||||||
var fd = new FormData();
|
else
|
||||||
fd.append("act", "tput");
|
modal.confirm('confirm that you wish to lose the changes made on the server since you opened this document', save2, function () {
|
||||||
fd.append("lastmod", (force ? -1 : last_modified));
|
toast.inf(3, 'aborted');
|
||||||
fd.append("body", txt);
|
});
|
||||||
|
|
||||||
var url = (document.location + '').split('?')[0];
|
|
||||||
var xhr = new XMLHttpRequest();
|
|
||||||
xhr.open('POST', url, true);
|
|
||||||
xhr.responseType = 'text';
|
|
||||||
xhr.onreadystatechange = save_cb;
|
|
||||||
xhr.btn = save_btn;
|
|
||||||
xhr.mde = mde;
|
|
||||||
xhr.txt = txt;
|
|
||||||
xhr.send(fd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function save_cb() {
|
function save_cb() {
|
||||||
if (this.readyState != XMLHttpRequest.DONE)
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.status !== 200) {
|
if (this.status !== 200)
|
||||||
alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var r;
|
var r;
|
||||||
try {
|
try {
|
||||||
r = JSON.parse(this.responseText);
|
r = JSON.parse(this.responseText);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
alert('Failed to parse reply from server:\n\n' + this.responseText);
|
return toast.err(0, 'Failed to parse reply from server:\n\n' + this.responseText);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
if (!this.btn.classList.contains('force-save')) {
|
if (!clgot(this.btn, 'force-save')) {
|
||||||
this.btn.classList.add('force-save');
|
clmod(this.btn, 'force-save', 1);
|
||||||
var msg = [
|
var msg = [
|
||||||
'This file has been modified since you started editing it!\n',
|
'This file has been modified since you started editing it!\n',
|
||||||
'if you really want to overwrite, press save again.\n',
|
'if you really want to overwrite, press save again.\n',
|
||||||
@@ -164,15 +160,13 @@ function save_cb() {
|
|||||||
r.lastmod + ' lastmod on the server now,',
|
r.lastmod + ' lastmod on the server now,',
|
||||||
r.now + ' server time now,\n',
|
r.now + ' server time now,\n',
|
||||||
];
|
];
|
||||||
alert(msg.join('\n'));
|
return toast.err(0, msg.join('\n'));
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
alert('Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
|
return toast.err(0, 'Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.btn.classList.remove('force-save');
|
clmod(this.btn, 'force-save');
|
||||||
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
|
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
|
||||||
|
|
||||||
// download the saved doc from the server and compare
|
// download the saved doc from the server and compare
|
||||||
@@ -192,35 +186,23 @@ function save_chk() {
|
|||||||
if (this.readyState != XMLHttpRequest.DONE)
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.status !== 200) {
|
if (this.status !== 200)
|
||||||
alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var doc1 = this.txt.replace(/\r\n/g, "\n");
|
var doc1 = this.txt.replace(/\r\n/g, "\n");
|
||||||
var doc2 = this.responseText.replace(/\r\n/g, "\n");
|
var doc2 = this.responseText.replace(/\r\n/g, "\n");
|
||||||
if (doc1 != doc2) {
|
if (doc1 != doc2) {
|
||||||
alert(
|
modal.alert(
|
||||||
'Error! The document on the server does not appear to have saved correctly (your editor contents and the server copy is not identical). Place the document on your clipboard for now and check the server logs for hints\n\n' +
|
'Error! The document on the server does not appear to have saved correctly (your editor contents and the server copy is not identical). Place the document on your clipboard for now and check the server logs for hints\n\n' +
|
||||||
'Length: yours=' + doc1.length + ', server=' + doc2.length
|
'Length: yours=' + doc1.length + ', server=' + doc2.length
|
||||||
);
|
);
|
||||||
alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']');
|
modal.alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']');
|
||||||
alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']');
|
modal.alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
last_modified = this.lastmod;
|
last_modified = this.lastmod;
|
||||||
md_changed(this.mde, true);
|
md_changed(this.mde, true);
|
||||||
|
|
||||||
var ok = mknod('div');
|
toast.ok(2, 'save OK' + (this.ntry ? '\nattempt ' + this.ntry : ''));
|
||||||
ok.setAttribute('style', 'font-size:6em;font-family:serif;font-weight:bold;color:#cf6;background:#444;border-radius:.3em;padding:.6em 0;position:fixed;top:30%;left:calc(50% - 2em);width:4em;text-align:center;z-index:9001;transition:opacity 0.2s ease-in-out;opacity:1');
|
|
||||||
ok.innerHTML = 'OK✔️';
|
|
||||||
var parent = ebi('m');
|
|
||||||
document.documentElement.appendChild(ok);
|
|
||||||
setTimeout(function () {
|
|
||||||
ok.style.opacity = 0;
|
|
||||||
}, 500);
|
|
||||||
setTimeout(function () {
|
|
||||||
ok.parentNode.removeChild(ok);
|
|
||||||
}, 750);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,12 @@ html {
|
|||||||
background: #333;
|
background: #333;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
text-shadow: 1px 1px 0px #000;
|
text-shadow: 1px 1px 0px #000;
|
||||||
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
body {
|
|
||||||
padding-bottom: 5em;
|
|
||||||
}
|
|
||||||
#box {
|
#box {
|
||||||
padding: .5em 1em;
|
padding: .5em 1em;
|
||||||
background: #2c2c2c;
|
background: #2c2c2c;
|
||||||
@@ -28,4 +26,4 @@ pre {
|
|||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
color: #fc5;
|
color: #fc5;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" media="screen" href="/.cpr/msg.css?_={{ ts }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ html, body, #wrap {
|
|||||||
background: #f7f7f7;
|
background: #f7f7f7;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
|
html {
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
#wrap {
|
#wrap {
|
||||||
max-width: 40em;
|
max-width: 40em;
|
||||||
margin: 2em auto;
|
margin: 2em auto;
|
||||||
@@ -26,6 +29,12 @@ a {
|
|||||||
border-radius: .2em;
|
border-radius: .2em;
|
||||||
padding: .2em .8em;
|
padding: .2em .8em;
|
||||||
}
|
}
|
||||||
|
#repl {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
table {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
@@ -73,4 +82,4 @@ html.dark input {
|
|||||||
}
|
}
|
||||||
html.dark .num {
|
html.dark .num {
|
||||||
border-color: #777;
|
border-color: #777;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
<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" media="screen" href="/.cpr/splash.css?_={{ ts }}">
|
||||||
|
<link rel="stylesheet" media="screen" href="/.cpr/ui.css?_={{ ts }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -35,7 +36,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</td></tr></table>
|
</td></tr></table>
|
||||||
<div class="btns">
|
<div class="btns">
|
||||||
<a href="{{ avol[0] }}?stack">dump stack</a>
|
<a href="/?stack">dump stack</a>
|
||||||
</div>
|
</div>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
@@ -66,11 +67,13 @@
|
|||||||
</form>
|
</form>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<a href="#" id="repl">π</a>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
if (window.localStorage && localStorage.getItem('lightmode') != 1)
|
if (localStorage.getItem('lightmode') != 1)
|
||||||
document.documentElement.setAttribute("class", "dark");
|
document.documentElement.setAttribute("class", "dark");
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
<script src="/.cpr/util.js?_={{ ts }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
244
copyparty/web/ui.css
Normal file
244
copyparty/web/ui.css
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
html {
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
#tt, #toast {
|
||||||
|
position: fixed;
|
||||||
|
max-width: 34em;
|
||||||
|
max-width: min(34em, 90%);
|
||||||
|
max-width: min(34em, calc(100% - 7em));
|
||||||
|
background: #222;
|
||||||
|
border: 0 solid #777;
|
||||||
|
box-shadow: 0 .2em .5em #222;
|
||||||
|
border-radius: .4em;
|
||||||
|
z-index: 9001;
|
||||||
|
}
|
||||||
|
#tt {
|
||||||
|
max-width: min(34em, calc(100% - 3.3em));
|
||||||
|
overflow: hidden;
|
||||||
|
margin: .7em 0;
|
||||||
|
padding: 0 1.3em;
|
||||||
|
height: 0;
|
||||||
|
opacity: .1;
|
||||||
|
transition: opacity 0.14s, height 0.14s, padding 0.14s;
|
||||||
|
}
|
||||||
|
#toast {
|
||||||
|
bottom: 5em;
|
||||||
|
right: -1em;
|
||||||
|
line-height: 1.5em;
|
||||||
|
padding: 1em 1.3em;
|
||||||
|
margin-left: 3em;
|
||||||
|
border-width: .4em 0;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition:
|
||||||
|
transform .4s cubic-bezier(.2, 1.2, .5, 1),
|
||||||
|
right .4s cubic-bezier(.2, 1.2, .5, 1);
|
||||||
|
text-shadow: 1px 1px 0 #000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
#toast a {
|
||||||
|
color: inherit;
|
||||||
|
text-shadow: inherit;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border-radius: .3em;
|
||||||
|
padding: .2em .3em;
|
||||||
|
}
|
||||||
|
#toast a#toastc {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
overflow: hidden;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
padding: .3em 0;
|
||||||
|
margin: -.3em 0 0 0;
|
||||||
|
line-height: 1.3em;
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
text-shadow: none;
|
||||||
|
border-radius: .5em 0 0 .5em;
|
||||||
|
transition: left .3s, width .3s, padding .3s, opacity .3s;
|
||||||
|
}
|
||||||
|
#toastb {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
#toast.scroll #toastb {
|
||||||
|
overflow-y: scroll;
|
||||||
|
margin-right: -1.2em;
|
||||||
|
padding-right: .7em;
|
||||||
|
}
|
||||||
|
#toast pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#toast.vis {
|
||||||
|
right: 1.3em;
|
||||||
|
transform: unset;
|
||||||
|
}
|
||||||
|
#toast.vis #toastc {
|
||||||
|
left: -2em;
|
||||||
|
width: .4em;
|
||||||
|
padding: .3em .8em;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
#toast.inf {
|
||||||
|
background: #07a;
|
||||||
|
border-color: #0be;
|
||||||
|
}
|
||||||
|
#toast.inf #toastc {
|
||||||
|
background: #0be;
|
||||||
|
}
|
||||||
|
#toast.ok {
|
||||||
|
background: #380;
|
||||||
|
border-color: #8e4;
|
||||||
|
}
|
||||||
|
#toast.ok #toastc {
|
||||||
|
background: #8e4;
|
||||||
|
}
|
||||||
|
#toast.warn {
|
||||||
|
background: #960;
|
||||||
|
border-color: #fc0;
|
||||||
|
}
|
||||||
|
#toast.warn #toastc {
|
||||||
|
background: #fc0;
|
||||||
|
}
|
||||||
|
#toast.err {
|
||||||
|
background: #900;
|
||||||
|
border-color: #d06;
|
||||||
|
}
|
||||||
|
#toast.err #toastc {
|
||||||
|
background: #d06;
|
||||||
|
}
|
||||||
|
#tt.b {
|
||||||
|
padding: 0 2em;
|
||||||
|
border-radius: .5em;
|
||||||
|
box-shadow: 0 .2em 1em #000;
|
||||||
|
}
|
||||||
|
#tt.show {
|
||||||
|
padding: 1em 1.3em;
|
||||||
|
border-width: .4em 0;
|
||||||
|
height: auto;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
#tt.show.b {
|
||||||
|
padding: 1.5em 2em;
|
||||||
|
border-width: .5em 0;
|
||||||
|
}
|
||||||
|
#modalc code,
|
||||||
|
#tt code {
|
||||||
|
background: #3c3c3c;
|
||||||
|
padding: .1em .3em;
|
||||||
|
border-top: 1px solid #777;
|
||||||
|
border-radius: .3em;
|
||||||
|
line-height: 1.7em;
|
||||||
|
}
|
||||||
|
#tt em {
|
||||||
|
color: #f6a;
|
||||||
|
}
|
||||||
|
html.light #tt {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #888 #000 #777 #000;
|
||||||
|
}
|
||||||
|
html.light #tt,
|
||||||
|
html.light #toast {
|
||||||
|
box-shadow: 0 .3em 1em rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
#modalc code,
|
||||||
|
html.light #tt code {
|
||||||
|
background: #060;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
html.light #tt em {
|
||||||
|
color: #d38;
|
||||||
|
}
|
||||||
|
#modal {
|
||||||
|
position: fixed;
|
||||||
|
overflow: auto;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 9001;
|
||||||
|
background: rgba(64,64,64,0.6);
|
||||||
|
}
|
||||||
|
#modal>table {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#modal td {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#modalc {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
background: #f7f7f7;
|
||||||
|
color: #333;
|
||||||
|
text-shadow: none;
|
||||||
|
text-align: left;
|
||||||
|
margin: 3em;
|
||||||
|
padding: 1em 1.1em;
|
||||||
|
border-radius: .6em;
|
||||||
|
box-shadow: 0 .3em 3em rgba(0,0,0,0.5);
|
||||||
|
max-width: 50em;
|
||||||
|
max-height: 30em;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
#modalc {
|
||||||
|
min-width: 30em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#modalc li {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
#modalc h6 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
border-bottom: 1px solid #999;
|
||||||
|
margin: 0;
|
||||||
|
padding: .3em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#modalb {
|
||||||
|
position: sticky;
|
||||||
|
text-align: right;
|
||||||
|
padding-top: 1em;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
#modalb a {
|
||||||
|
color: #000;
|
||||||
|
background: #ccc;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: .3em;
|
||||||
|
padding: .5em 1em;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
#modalb a:focus,
|
||||||
|
#modalb a:hover {
|
||||||
|
background: #06d;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
#modalb a+a {
|
||||||
|
margin-left: .5em;
|
||||||
|
}
|
||||||
|
#modali {
|
||||||
|
display: block;
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
width: calc(100% - 1.25em);
|
||||||
|
margin: 1em -.1em 0 -.1em;
|
||||||
|
padding: .5em;
|
||||||
|
outline: none;
|
||||||
|
border: .25em solid #ccc;
|
||||||
|
border-radius: .4em;
|
||||||
|
}
|
||||||
|
#modali:focus {
|
||||||
|
border-color: #06d;
|
||||||
|
}
|
||||||
|
#repl_pre {
|
||||||
|
max-width: 24em;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,274 +0,0 @@
|
|||||||
|
|
||||||
#op_up2k {
|
|
||||||
padding: 0 1em 1em 1em;
|
|
||||||
}
|
|
||||||
#u2form {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 2px;
|
|
||||||
height: 2px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
#u2form input {
|
|
||||||
background: #444;
|
|
||||||
border: 0px solid #444;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
#u2err.err {
|
|
||||||
color: #f87;
|
|
||||||
padding: .5em;
|
|
||||||
}
|
|
||||||
#u2err.msg {
|
|
||||||
color: #999;
|
|
||||||
padding: .5em;
|
|
||||||
font-size: .9em;
|
|
||||||
}
|
|
||||||
#u2btn {
|
|
||||||
color: #eee;
|
|
||||||
background: #555;
|
|
||||||
background: -moz-linear-gradient(top, #367 0%, #489 50%, #38788a 51%, #367 100%);
|
|
||||||
background: -webkit-linear-gradient(top, #367 0%, #489 50%, #38788a 51%, #367 100%);
|
|
||||||
background: linear-gradient(to bottom, #367 0%, #489 50%, #38788a 51%, #367 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#489', endColorstr='#38788a', GradientType=0);
|
|
||||||
text-decoration: none;
|
|
||||||
line-height: 1.3em;
|
|
||||||
border: 1px solid #222;
|
|
||||||
border-radius: .4em;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.5em;
|
|
||||||
margin: .5em auto;
|
|
||||||
padding: .8em 0;
|
|
||||||
width: 16em;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: .4em .4em 0 #111;
|
|
||||||
}
|
|
||||||
#op_up2k.srch #u2btn {
|
|
||||||
background: linear-gradient(to bottom, #ca3 0%, #fd8 50%, #fc6 51%, #b92 100%);
|
|
||||||
text-shadow: 1px 1px 1px #fc6;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
#u2conf #u2btn {
|
|
||||||
margin: -1.5em 0;
|
|
||||||
padding: .8em 0;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 12em;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
#u2conf #u2btn_cw {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
#u2notbtn {
|
|
||||||
display: none;
|
|
||||||
text-align: center;
|
|
||||||
background: #333;
|
|
||||||
padding-top: 1em;
|
|
||||||
}
|
|
||||||
#u2notbtn * {
|
|
||||||
line-height: 1.3em;
|
|
||||||
}
|
|
||||||
#u2tab {
|
|
||||||
margin: 3em auto;
|
|
||||||
width: calc(100% - 2em);
|
|
||||||
max-width: 100em;
|
|
||||||
}
|
|
||||||
#op_up2k.srch #u2tab {
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
#u2tab td {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-width: 0 0px 1px 0;
|
|
||||||
padding: .1em .3em;
|
|
||||||
}
|
|
||||||
#u2tab td:nth-child(2) {
|
|
||||||
width: 5em;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
#u2tab td:nth-child(3) {
|
|
||||||
width: 40%;
|
|
||||||
}
|
|
||||||
#op_up2k.srch #u2tab td:nth-child(3) {
|
|
||||||
font-family: sans-serif;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
#u2tab tbody tr:hover td {
|
|
||||||
background: #222;
|
|
||||||
}
|
|
||||||
#u2cards {
|
|
||||||
padding: 1em 0 .3em 1em;
|
|
||||||
margin: 1.5em auto -2.5em auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
#u2cards.w {
|
|
||||||
width: 45em;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
#u2cards a {
|
|
||||||
padding: .2em 1em;
|
|
||||||
border: 1px solid #777;
|
|
||||||
border-width: 0 0 1px 0;
|
|
||||||
background: linear-gradient(to bottom, #333, #222);
|
|
||||||
}
|
|
||||||
#u2cards a:first-child {
|
|
||||||
border-radius: .4em 0 0 0;
|
|
||||||
}
|
|
||||||
#u2cards a:last-child {
|
|
||||||
border-radius: 0 .4em 0 0;
|
|
||||||
}
|
|
||||||
#u2cards a.act {
|
|
||||||
padding-bottom: .5em;
|
|
||||||
border-width: 1px 1px .1em 1px;
|
|
||||||
border-radius: .3em .3em 0 0;
|
|
||||||
margin-left: -1px;
|
|
||||||
background: linear-gradient(to bottom, #464, #333 80%);
|
|
||||||
box-shadow: 0 -.17em .67em #280;
|
|
||||||
border-color: #7c5 #583 #333 #583;
|
|
||||||
position: relative;
|
|
||||||
color: #fd7;
|
|
||||||
}
|
|
||||||
#u2cards span {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
#u2conf {
|
|
||||||
margin: 1em auto;
|
|
||||||
width: 30em;
|
|
||||||
}
|
|
||||||
#u2conf.has_btn {
|
|
||||||
width: 48em;
|
|
||||||
}
|
|
||||||
#u2conf * {
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
#u2conf .txtbox {
|
|
||||||
width: 3em;
|
|
||||||
color: #fff;
|
|
||||||
background: #444;
|
|
||||||
border: 1px solid #777;
|
|
||||||
font-size: 1.2em;
|
|
||||||
padding: .15em 0;
|
|
||||||
height: 1.05em;
|
|
||||||
}
|
|
||||||
#u2conf .txtbox.err {
|
|
||||||
background: #922;
|
|
||||||
}
|
|
||||||
#u2conf a {
|
|
||||||
color: #fff;
|
|
||||||
background: #c38;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: .1em;
|
|
||||||
font-size: 1.5em;
|
|
||||||
padding: .1em 0;
|
|
||||||
margin: 0 -1px;
|
|
||||||
width: 1.5em;
|
|
||||||
height: 1em;
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
bottom: -0.08em;
|
|
||||||
}
|
|
||||||
#u2conf input+a {
|
|
||||||
background: #d80;
|
|
||||||
}
|
|
||||||
#u2conf label {
|
|
||||||
font-size: 1.6em;
|
|
||||||
width: 2em;
|
|
||||||
height: 1em;
|
|
||||||
padding: .4em 0;
|
|
||||||
display: block;
|
|
||||||
border-radius: .25em;
|
|
||||||
}
|
|
||||||
#u2conf input[type="checkbox"] {
|
|
||||||
position: relative;
|
|
||||||
opacity: .02;
|
|
||||||
top: 2em;
|
|
||||||
}
|
|
||||||
#u2conf input[type="checkbox"]+label {
|
|
||||||
position: relative;
|
|
||||||
background: #603;
|
|
||||||
border-bottom: .2em solid #a16;
|
|
||||||
box-shadow: 0 .1em .3em #a00 inset;
|
|
||||||
}
|
|
||||||
#u2conf input[type="checkbox"]:checked+label {
|
|
||||||
background: #6a1;
|
|
||||||
border-bottom: .2em solid #efa;
|
|
||||||
box-shadow: 0 .1em .5em #0c0;
|
|
||||||
}
|
|
||||||
#u2conf input[type="checkbox"]+label:hover {
|
|
||||||
box-shadow: 0 .1em .3em #fb0;
|
|
||||||
border-color: #fb0;
|
|
||||||
}
|
|
||||||
#op_up2k.srch #u2conf td:nth-child(1)>*,
|
|
||||||
#op_up2k.srch #u2conf td:nth-child(2)>*,
|
|
||||||
#op_up2k.srch #u2conf td:nth-child(3)>* {
|
|
||||||
background: #777;
|
|
||||||
border-color: #ccc;
|
|
||||||
box-shadow: none;
|
|
||||||
opacity: .2;
|
|
||||||
}
|
|
||||||
#u2foot {
|
|
||||||
color: #fff;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
#u2foot span {
|
|
||||||
color: #999;
|
|
||||||
font-size: .9em;
|
|
||||||
}
|
|
||||||
#u2footfoot {
|
|
||||||
margin-bottom: -1em;
|
|
||||||
}
|
|
||||||
.prog {
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
#u2tab a>span {
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: italic;
|
|
||||||
color: #fff;
|
|
||||||
padding-left: .2em;
|
|
||||||
}
|
|
||||||
#u2cleanup {
|
|
||||||
float: right;
|
|
||||||
margin-bottom: -.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
html.light #u2btn {
|
|
||||||
box-shadow: .4em .4em 0 #ccc;
|
|
||||||
}
|
|
||||||
html.light #u2cards span {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
html.light #u2cards a {
|
|
||||||
background: linear-gradient(to bottom, #eee, #fff);
|
|
||||||
}
|
|
||||||
html.light #u2cards a.act {
|
|
||||||
color: #037;
|
|
||||||
background: inherit;
|
|
||||||
box-shadow: 0 -.17em .67em #0ad;
|
|
||||||
border-color: #09c #05a #eee #05a;
|
|
||||||
}
|
|
||||||
html.light #u2conf .txtbox {
|
|
||||||
background: #fff;
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
html.light #u2conf .txtbox.err {
|
|
||||||
background: #f96;
|
|
||||||
color: #300;
|
|
||||||
}
|
|
||||||
html.light #op_up2k.srch #u2btn {
|
|
||||||
border-color: #a80;
|
|
||||||
}
|
|
||||||
html.light #u2foot {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
html.light #u2tab tbody tr:hover td {
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
@@ -7,20 +7,18 @@ if (!window['console'])
|
|||||||
|
|
||||||
|
|
||||||
var is_touch = 'ontouchstart' in window,
|
var is_touch = 'ontouchstart' in window,
|
||||||
ANDROID = /(android)/i.test(navigator.userAgent);
|
IPHONE = /iPhone|iPad|iPod/i.test(navigator.userAgent),
|
||||||
|
ANDROID = /android/i.test(navigator.userAgent),
|
||||||
|
WINDOWS = navigator.platform ? navigator.platform == 'Win32' : /Windows/.test(navigator.userAgent);
|
||||||
|
|
||||||
|
|
||||||
|
var ebi = document.getElementById.bind(document),
|
||||||
|
QS = document.querySelector.bind(document),
|
||||||
|
QSA = document.querySelectorAll.bind(document),
|
||||||
|
mknod = document.createElement.bind(document);
|
||||||
|
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -31,29 +29,140 @@ function esc(txt) {
|
|||||||
}[c];
|
}[c];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function vis_exh(msg, url, lineNo, columnNo, error) {
|
window.onunhandledrejection = function (e) {
|
||||||
window.onerror = undefined;
|
console.log("REJ: " + e.reason);
|
||||||
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>',
|
try {
|
||||||
esc(String(msg)), '</p><p>', esc(url + ' @' + lineNo + ':' + columnNo), '</p>'];
|
console.hist = [];
|
||||||
|
var hook = function (t) {
|
||||||
|
var orig = console[t].bind(console),
|
||||||
|
cfun = function () {
|
||||||
|
console.hist.push(Date.now() + ' ' + t + ': ' + Array.from(arguments).join(', '));
|
||||||
|
if (console.hist.length > 100)
|
||||||
|
console.hist = console.hist.slice(50);
|
||||||
|
|
||||||
if (error) {
|
orig.apply(console, arguments);
|
||||||
var find = ['desc', 'stack', 'trace'];
|
};
|
||||||
for (var a = 0; a < find.length; a++)
|
|
||||||
if (String(error[find[a]]) !== 'undefined')
|
console['std' + t] = orig;
|
||||||
html.push('<h2>' + find[a] + '</h2>' +
|
console[t] = cfun;
|
||||||
esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
|
};
|
||||||
|
hook('log');
|
||||||
|
console.log('log-capture ok');
|
||||||
|
hook('debug');
|
||||||
|
hook('warn');
|
||||||
|
hook('error');
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
if (console.stdlog)
|
||||||
|
console.log = console.stdlog;
|
||||||
|
console.log(ex);
|
||||||
|
}
|
||||||
|
var crashed = false, ignexd = {};
|
||||||
|
function vis_exh(msg, url, lineNo, columnNo, error) {
|
||||||
|
if ((msg + '').indexOf('ResizeObserver') !== -1)
|
||||||
|
return; // chrome issue 809574 (benign, from <video>)
|
||||||
|
|
||||||
|
var ekey = url + '\n' + lineNo + '\n' + msg;
|
||||||
|
if (ignexd[ekey] || crashed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
crashed = true;
|
||||||
|
window.onerror = undefined;
|
||||||
|
var html = [
|
||||||
|
'<h1>you hit a bug!</h1>',
|
||||||
|
'<p style="font-size:1.3em;margin:0">try to <a href="#" onclick="localStorage.clear();location.reload();">reset copyparty settings</a> if you are stuck here, or <a href="#" onclick="ignex();">ignore this</a> / <a href="#" onclick="ignex(true);">ignore all</a></p>',
|
||||||
|
'<p style="color:#fff">please send me a screenshot arigathanks gozaimuch: <code>ed/irc.rizon.net</code> or <code>ed#2644</code></p>',
|
||||||
|
'<p class="b">' + esc(url + ' @' + lineNo + ':' + columnNo), '<br />' + esc(String(msg)) + '</p>',
|
||||||
|
'<p><b>UA:</b> ' + esc(navigator.userAgent + '')
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
var ua = '',
|
||||||
|
ad = navigator.userAgentData,
|
||||||
|
adb = ad.brands;
|
||||||
|
|
||||||
|
for (var a = 0; a < adb.length; a++)
|
||||||
|
if (!/Not.*A.*Brand/.exec(adb[a].brand))
|
||||||
|
ua += adb[a].brand + '/' + adb[a].version + ', ';
|
||||||
|
ua += ad.platform;
|
||||||
|
|
||||||
|
html.push('<br /><b>UAD:</b> ' + esc(ua.slice(0, 100)));
|
||||||
}
|
}
|
||||||
document.body.style.fontSize = '0.8em';
|
catch (e) { }
|
||||||
document.body.style.padding = '0 1em 1em 1em';
|
html.push('</p>');
|
||||||
hcroak(html.join('\n'));
|
|
||||||
|
try {
|
||||||
|
if (error) {
|
||||||
|
var find = ['desc', 'stack', 'trace'];
|
||||||
|
for (var a = 0; a < find.length; a++)
|
||||||
|
if (String(error[find[a]]) !== 'undefined')
|
||||||
|
html.push('<p class="b"><b>' + find[a] + ':</b><br />' +
|
||||||
|
esc(String(error[find[a]])).replace(/\n/g, '<br />\n') + '</p>');
|
||||||
|
}
|
||||||
|
ignexd[ekey] = true;
|
||||||
|
|
||||||
|
var ls = jcp(localStorage);
|
||||||
|
if (ls.fman_clip)
|
||||||
|
ls.fman_clip = ls.fman_clip.length + ' items';
|
||||||
|
|
||||||
|
var lsk = Object.keys(ls);
|
||||||
|
lsk.sort();
|
||||||
|
html.push('<p class="b">');
|
||||||
|
for (var a = 0; a < lsk.length; a++)
|
||||||
|
html.push(' <b>' + esc(lsk[a]) + '</b> <code>' + esc(ls[lsk[a]]) + '</code> ');
|
||||||
|
html.push('</p>');
|
||||||
|
}
|
||||||
|
catch (e) { }
|
||||||
|
|
||||||
|
if (console.hist.length) {
|
||||||
|
html.push('<p class="b"><b>console:</b><ul><li>' + Date.now() + ' @</li>');
|
||||||
|
for (var a = console.hist.length - 1, aa = Math.max(0, console.hist.length - 20); a >= aa; a--)
|
||||||
|
html.push('<li>' + esc(console.hist[a]) + '</li>');
|
||||||
|
html.push('</ul>')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var exbox = ebi('exbox');
|
||||||
|
if (!exbox) {
|
||||||
|
exbox = mknod('div');
|
||||||
|
exbox.setAttribute('id', 'exbox');
|
||||||
|
document.body.appendChild(exbox);
|
||||||
|
|
||||||
|
var s = mknod('style');
|
||||||
|
s.innerHTML = (
|
||||||
|
'#exbox{background:#333;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em;z-index:80386;position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;overflow:auto;width:calc(100% - 2em)} ' +
|
||||||
|
'#exbox,#exbox *{line-height:1.5em;overflow-wrap:break-word} ' +
|
||||||
|
'#exbox code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} ' +
|
||||||
|
'#exbox a{text-decoration:underline;color:#fc0} ' +
|
||||||
|
'#exbox h1{margin:.5em 1em 0 0;padding:0} ' +
|
||||||
|
'#exbox p.b{border-top:1px solid #999;margin:1em 0 0 0;font-size:1em} ' +
|
||||||
|
'#exbox ul, #exbox li {margin:0 0 0 .5em;padding:0} ' +
|
||||||
|
'#exbox b{color:#fff}'
|
||||||
|
);
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
exbox.innerHTML = html.join('\n').replace(/https?:\/\/[^ \/]+\//g, '/').replace(/js\?_=[a-zA-Z]{4}/g, 'js');
|
||||||
|
exbox.style.display = 'block';
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
document.body.innerHTML = html.join('\n');
|
||||||
|
}
|
||||||
|
throw 'fatal_err';
|
||||||
|
}
|
||||||
|
function ignex(all) {
|
||||||
|
var o = ebi('exbox');
|
||||||
|
o.style.display = 'none';
|
||||||
|
o.innerHTML = '';
|
||||||
|
crashed = false;
|
||||||
|
if (!all)
|
||||||
|
window.onerror = vis_exh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var ebi = document.getElementById.bind(document),
|
function ctrl(e) {
|
||||||
QS = document.querySelector.bind(document),
|
return e && (e.ctrlKey || e.metaKey);
|
||||||
QSA = document.querySelectorAll.bind(document),
|
}
|
||||||
mknod = document.createElement.bind(document);
|
|
||||||
|
|
||||||
|
|
||||||
function ev(e) {
|
function ev(e) {
|
||||||
@@ -67,6 +176,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;
|
||||||
}
|
}
|
||||||
@@ -87,6 +199,22 @@ if (!String.startsWith) {
|
|||||||
return this.substring(i, i + s.length) === s;
|
return this.substring(i, i + s.length) === s;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (!Element.prototype.matches) {
|
||||||
|
Element.prototype.matches =
|
||||||
|
Element.prototype.oMatchesSelector ||
|
||||||
|
Element.prototype.msMatchesSelector ||
|
||||||
|
Element.prototype.mozMatchesSelector ||
|
||||||
|
Element.prototype.webkitMatchesSelector;
|
||||||
|
}
|
||||||
|
if (!Element.prototype.closest) {
|
||||||
|
Element.prototype.closest = function (s) {
|
||||||
|
var el = this;
|
||||||
|
do {
|
||||||
|
if (el.matches(s)) return el;
|
||||||
|
el = el.parentElement || el.parentNode;
|
||||||
|
} while (el !== null && el.nodeType === 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/950146
|
// https://stackoverflow.com/a/950146
|
||||||
@@ -95,10 +223,10 @@ function import_js(url, cb) {
|
|||||||
var script = mknod('script');
|
var script = mknod('script');
|
||||||
script.type = 'text/javascript';
|
script.type = 'text/javascript';
|
||||||
script.src = url;
|
script.src = url;
|
||||||
|
|
||||||
script.onreadystatechange = cb;
|
|
||||||
script.onload = cb;
|
script.onload = cb;
|
||||||
|
script.onerror = function () {
|
||||||
|
toast.err(0, 'Failed to load module:\n' + url);
|
||||||
|
};
|
||||||
head.appendChild(script);
|
head.appendChild(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,90 +253,37 @@ function crc32(str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function clmod(obj, cls, add) {
|
function clmod(el, cls, add) {
|
||||||
var re = new RegExp('\\s*\\b' + cls + '\\s*\\b', 'g');
|
if (el.classList) {
|
||||||
if (add == 't')
|
var have = el.classList.contains(cls);
|
||||||
add = !re.test(obj.className);
|
if (add == 't')
|
||||||
|
add = !have;
|
||||||
|
|
||||||
obj.className = obj.className.replace(re, ' ') + (add ? ' ' + cls : '');
|
if (add != have)
|
||||||
|
el.classList[add ? 'add' : 'remove'](cls);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var re = new RegExp('\\s*\\b' + cls + '\\s*\\b', 'g'),
|
||||||
|
n1 = el.className;
|
||||||
|
|
||||||
|
if (add == 't')
|
||||||
|
add = !re.test(n1);
|
||||||
|
|
||||||
|
var n2 = n1.replace(re, ' ') + (add ? ' ' + cls : '');
|
||||||
|
|
||||||
|
if (n1 != n2)
|
||||||
|
el.className = n2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function sortfiles(nodes) {
|
function clgot(el, cls) {
|
||||||
var sopts = jread('fsort', [["href", 1, ""]]);
|
if (el.classList)
|
||||||
|
return el.classList.contains(cls);
|
||||||
|
|
||||||
try {
|
var lst = (el.getAttribute('class') + '').split(/ /g);
|
||||||
var is_srch = false;
|
return has(lst, cls);
|
||||||
if (nodes[0]['rp']) {
|
|
||||||
is_srch = true;
|
|
||||||
for (var b = 0, bb = nodes.length; b < bb; b++)
|
|
||||||
nodes[b].ext = nodes[b].rp.split('.').pop();
|
|
||||||
for (var b = 0; b < sopts.length; b++)
|
|
||||||
if (sopts[b][0] == 'href')
|
|
||||||
sopts[b][0] = 'rp';
|
|
||||||
}
|
|
||||||
for (var a = sopts.length - 1; a >= 0; a--) {
|
|
||||||
var name = sopts[a][0], rev = sopts[a][1], typ = sopts[a][2];
|
|
||||||
if (!name)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (name == 'ts')
|
|
||||||
typ = 'int';
|
|
||||||
|
|
||||||
if (name.indexOf('tags/') === 0) {
|
|
||||||
name = name.slice(5);
|
|
||||||
for (var b = 0, bb = nodes.length; b < bb; b++)
|
|
||||||
nodes[b]._sv = nodes[b].tags[name];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
for (var b = 0, bb = nodes.length; b < bb; b++) {
|
|
||||||
var v = nodes[b][name];
|
|
||||||
|
|
||||||
if ((v + '').indexOf('<a ') === 0)
|
|
||||||
v = v.split('>')[1];
|
|
||||||
else if (name == "href" && v) {
|
|
||||||
if (v.slice(-1) == '/')
|
|
||||||
v = '\t' + v;
|
|
||||||
|
|
||||||
v = uricom_dec(v)[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes[b]._sv = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var onodes = nodes.map(function (x) { return x; });
|
|
||||||
nodes.sort(function (n1, n2) {
|
|
||||||
var v1 = n1._sv,
|
|
||||||
v2 = n2._sv;
|
|
||||||
|
|
||||||
if (v1 === undefined) {
|
|
||||||
if (v2 === undefined) {
|
|
||||||
return onodes.indexOf(n1) - onodes.indexOf(n2);
|
|
||||||
}
|
|
||||||
return -1 * rev;
|
|
||||||
}
|
|
||||||
if (v2 === undefined) return 1 * rev;
|
|
||||||
|
|
||||||
var ret = rev * (typ == 'int' ? (v1 - v2) : (v1.localeCompare(v2)));
|
|
||||||
if (ret === 0)
|
|
||||||
ret = onodes.indexOf(n1) - onodes.indexOf(n2);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for (var b = 0, bb = nodes.length; b < bb; b++) {
|
|
||||||
delete nodes[b]._sv;
|
|
||||||
if (is_srch)
|
|
||||||
delete nodes[b].ext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
console.log("failed to apply sort config: " + ex);
|
|
||||||
console.log("resetting fsort " + sread('fsort'))
|
|
||||||
localStorage.removeItem('fsort');
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -316,6 +391,18 @@ function linksplit(rp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function vsplit(vp) {
|
||||||
|
if (vp.endsWith('/'))
|
||||||
|
vp = vp.slice(0, -1);
|
||||||
|
|
||||||
|
var ofs = vp.lastIndexOf('/') + 1,
|
||||||
|
base = vp.slice(0, ofs),
|
||||||
|
fn = vp.slice(ofs);
|
||||||
|
|
||||||
|
return [base, fn];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function uricom_enc(txt, do_fb_enc) {
|
function uricom_enc(txt, do_fb_enc) {
|
||||||
try {
|
try {
|
||||||
return encodeURIComponent(txt);
|
return encodeURIComponent(txt);
|
||||||
@@ -329,6 +416,16 @@ function uricom_enc(txt, do_fb_enc) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function url_enc(txt) {
|
||||||
|
var parts = txt.split('/'),
|
||||||
|
ret = [];
|
||||||
|
|
||||||
|
for (var a = 0; a < parts.length; a++)
|
||||||
|
ret.push(uricom_enc(parts[a]));
|
||||||
|
|
||||||
|
return ret.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function uricom_dec(txt) {
|
function uricom_dec(txt) {
|
||||||
try {
|
try {
|
||||||
@@ -341,6 +438,17 @@ function uricom_dec(txt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function uricom_adec(arr, li) {
|
||||||
|
var ret = [];
|
||||||
|
for (var a = 0; a < arr.length; a++) {
|
||||||
|
var txt = uricom_dec(arr[a])[0];
|
||||||
|
ret.push(li ? '<li>' + esc(txt) + '</li>' : txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function get_evpath() {
|
function get_evpath() {
|
||||||
var ret = document.location.pathname;
|
var ret = document.location.pathname;
|
||||||
|
|
||||||
@@ -359,6 +467,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);
|
||||||
}
|
}
|
||||||
@@ -371,6 +488,41 @@ function s2ms(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function f2f(val, nd) {
|
||||||
|
// 10.toFixed(1) returns 10.00 for certain values of 10
|
||||||
|
val = (val * Math.pow(10, nd)).toFixed(0).split('.')[0];
|
||||||
|
return nd ? (val.slice(0, -nd) || '0') + '.' + val.slice(-nd) : val;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function humansize(b, terse) {
|
||||||
|
var i = 0, u = terse ? ['B', 'K', 'M', 'G'] : ['B', 'KB', 'MB', 'GB'];
|
||||||
|
while (b >= 1000 && i < u.length) {
|
||||||
|
b /= 1024;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
return f2f(b, b >= 100 ? 0 : b >= 10 ? 1 : 2) + ' ' + u[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function humantime(v) {
|
||||||
|
if (v >= 60 * 60 * 24)
|
||||||
|
return v;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return /.*(..:..:..).*/.exec(new Date(v * 1000).toUTCString())[1];
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function clamp(v, a, b) {
|
||||||
|
return Math.min(Math.max(v, a), b);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function has(haystack, needle) {
|
function has(haystack, needle) {
|
||||||
for (var a = 0; a < haystack.length; a++)
|
for (var a = 0; a < haystack.length; a++)
|
||||||
if (haystack[a] == needle)
|
if (haystack[a] == needle)
|
||||||
@@ -380,20 +532,27 @@ function has(haystack, needle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function sread(key) {
|
function apop(arr, v) {
|
||||||
if (window.localStorage)
|
var ofs = arr.indexOf(v);
|
||||||
return localStorage.getItem(key);
|
if (ofs !== -1)
|
||||||
|
arr.splice(ofs, 1);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
|
function jcp(obj) {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function sread(key) {
|
||||||
|
return localStorage.getItem(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
function swrite(key, val) {
|
function swrite(key, val) {
|
||||||
if (window.localStorage) {
|
if (val === undefined || val === null)
|
||||||
if (val === undefined || val === null)
|
localStorage.removeItem(key);
|
||||||
localStorage.removeItem(key);
|
else
|
||||||
else
|
localStorage.setItem(key, val);
|
||||||
localStorage.setItem(key, val);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function jread(key, fb) {
|
function jread(key, fb) {
|
||||||
@@ -473,16 +632,61 @@ function hist_replace(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var timer = (function () {
|
||||||
|
var r = {};
|
||||||
|
r.q = [];
|
||||||
|
r.last = 0;
|
||||||
|
|
||||||
|
r.add = function (fun, run) {
|
||||||
|
r.rm(fun);
|
||||||
|
r.q.push(fun);
|
||||||
|
|
||||||
|
if (run)
|
||||||
|
fun();
|
||||||
|
};
|
||||||
|
|
||||||
|
r.rm = function (fun) {
|
||||||
|
apop(r.q, fun);
|
||||||
|
};
|
||||||
|
|
||||||
|
function doevents() {
|
||||||
|
if (crashed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Date.now() - r.last < 69)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var q = r.q.slice(0);
|
||||||
|
for (var a = 0; a < q.length; a++)
|
||||||
|
q[a]();
|
||||||
|
|
||||||
|
r.last = Date.now();
|
||||||
|
}
|
||||||
|
setInterval(doevents, 100);
|
||||||
|
|
||||||
|
return r;
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
var tt = (function () {
|
var tt = (function () {
|
||||||
var r = {
|
var r = {
|
||||||
"tt": mknod("div"),
|
"tt": mknod("div"),
|
||||||
"en": true
|
"en": true,
|
||||||
|
"el": null,
|
||||||
|
"skip": false
|
||||||
};
|
};
|
||||||
|
|
||||||
r.tt.setAttribute('id', 'tt');
|
r.tt.setAttribute('id', 'tt');
|
||||||
document.body.appendChild(r.tt);
|
document.body.appendChild(r.tt);
|
||||||
|
|
||||||
function show() {
|
r.show = function () {
|
||||||
|
if (r.skip) {
|
||||||
|
r.skip = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (QS('body.bbox-open'))
|
||||||
|
return;
|
||||||
|
|
||||||
var cfg = sread('tooltips');
|
var cfg = sread('tooltips');
|
||||||
if (cfg !== null && cfg != '1')
|
if (cfg !== null && cfg != '1')
|
||||||
return;
|
return;
|
||||||
@@ -491,21 +695,83 @@ var tt = (function () {
|
|||||||
if (!msg)
|
if (!msg)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
r.el = this;
|
||||||
var pos = this.getBoundingClientRect(),
|
var pos = this.getBoundingClientRect(),
|
||||||
left = pos.left < window.innerWidth / 2,
|
dir = this.getAttribute('ttd') || '',
|
||||||
top = pos.top < window.innerHeight / 2;
|
margin = parseFloat(this.getAttribute('ttm') || 0),
|
||||||
|
top = pos.top < window.innerHeight / 2,
|
||||||
|
big = this.className.indexOf(' ttb') !== -1;
|
||||||
|
|
||||||
r.tt.style.top = top ? pos.bottom + 'px' : 'auto';
|
if (dir.indexOf('u') + 1) top = false;
|
||||||
r.tt.style.bottom = top ? 'auto' : (window.innerHeight - pos.top) + 'px';
|
if (dir.indexOf('d') + 1) top = true;
|
||||||
r.tt.style.left = left ? pos.left + 'px' : 'auto';
|
|
||||||
r.tt.style.right = left ? 'auto' : (window.innerWidth - pos.right) + 'px';
|
clmod(r.tt, 'b', big);
|
||||||
|
r.tt.style.left = '0';
|
||||||
|
r.tt.style.top = '0';
|
||||||
|
|
||||||
r.tt.innerHTML = msg.replace(/\$N/g, "<br />");
|
r.tt.innerHTML = msg.replace(/\$N/g, "<br />");
|
||||||
|
r.el.addEventListener('mouseleave', r.hide);
|
||||||
|
window.addEventListener('scroll', r.hide);
|
||||||
clmod(r.tt, 'show', 1);
|
clmod(r.tt, 'show', 1);
|
||||||
|
|
||||||
|
var tw = r.tt.offsetWidth,
|
||||||
|
x = pos.left + (pos.right - pos.left) / 2 - tw / 2;
|
||||||
|
|
||||||
|
if (x + tw >= window.innerWidth - 24)
|
||||||
|
x = window.innerWidth - tw - 24;
|
||||||
|
|
||||||
|
if (x < 0)
|
||||||
|
x = 12;
|
||||||
|
|
||||||
|
r.tt.style.left = x + 'px';
|
||||||
|
r.tt.style.top = top ? (margin + pos.bottom) + 'px' : 'auto';
|
||||||
|
r.tt.style.bottom = top ? 'auto' : (margin + window.innerHeight - pos.top) + 'px';
|
||||||
|
};
|
||||||
|
|
||||||
|
r.hide = function (e) {
|
||||||
|
ev(e);
|
||||||
|
window.removeEventListener('scroll', r.hide);
|
||||||
|
clmod(r.tt, 'show');
|
||||||
|
if (r.el)
|
||||||
|
r.el.removeEventListener('mouseleave', r.hide);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (is_touch && IPHONE) {
|
||||||
|
var f1 = r.show,
|
||||||
|
f2 = r.hide,
|
||||||
|
q = [];
|
||||||
|
|
||||||
|
// if an onclick-handler creates a new timer,
|
||||||
|
// iOS 13.1.2 delays the entire handler by up to 401ms,
|
||||||
|
// win by using a shared timer instead
|
||||||
|
|
||||||
|
timer.add(function () {
|
||||||
|
while (q.length && Date.now() >= q[0][0])
|
||||||
|
q.shift()[1]();
|
||||||
|
});
|
||||||
|
|
||||||
|
r.show = function () {
|
||||||
|
q.push([Date.now() + 100, f1.bind(this)]);
|
||||||
|
};
|
||||||
|
r.hide = function () {
|
||||||
|
q.push([Date.now() + 100, f2.bind(this)]);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
r.tt.onclick = r.hide;
|
||||||
clmod(r.tt, 'show');
|
|
||||||
|
r.att = function (ctr) {
|
||||||
|
var _show = r.en ? r.show : null,
|
||||||
|
_hide = r.en ? r.hide : null,
|
||||||
|
o = ctr.querySelectorAll('*[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;
|
||||||
|
}
|
||||||
|
r.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
r.init = function () {
|
r.init = function () {
|
||||||
@@ -519,19 +785,322 @@ var tt = (function () {
|
|||||||
};
|
};
|
||||||
r.en = bcfg_get('tooltips', true)
|
r.en = bcfg_get('tooltips', true)
|
||||||
}
|
}
|
||||||
|
r.att(document);
|
||||||
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;
|
return r;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
function lf2br(txt) {
|
||||||
|
var html = '', hp = txt.split(/(?=<.?pre>)/i);
|
||||||
|
for (var a = 0; a < hp.length; a++)
|
||||||
|
html += hp[a].startsWith('<pre>') ? hp[a] :
|
||||||
|
hp[a].replace(/<br ?.?>\n/g, '\n').replace(/\n<br ?.?>/g, '\n').replace(/\n/g, '<br />\n');
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var toast = (function () {
|
||||||
|
var r = {},
|
||||||
|
te = null,
|
||||||
|
scrolling = false,
|
||||||
|
obj = mknod('div');
|
||||||
|
|
||||||
|
obj.setAttribute('id', 'toast');
|
||||||
|
document.body.appendChild(obj);
|
||||||
|
r.visible = false;
|
||||||
|
r.txt = null;
|
||||||
|
|
||||||
|
function scrollchk() {
|
||||||
|
if (scrolling)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var tb = ebi('toastb'),
|
||||||
|
vis = tb.offsetHeight,
|
||||||
|
all = tb.scrollHeight;
|
||||||
|
|
||||||
|
if (8 + vis >= all)
|
||||||
|
return;
|
||||||
|
|
||||||
|
clmod(obj, 'scroll', 1);
|
||||||
|
scrolling = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unscroll() {
|
||||||
|
timer.rm(scrollchk);
|
||||||
|
clmod(obj, 'scroll');
|
||||||
|
scrolling = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
r.hide = function (e) {
|
||||||
|
ev(e);
|
||||||
|
unscroll();
|
||||||
|
clearTimeout(te);
|
||||||
|
clmod(obj, 'vis');
|
||||||
|
r.visible = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
r.show = function (cl, ms, txt) {
|
||||||
|
clearTimeout(te);
|
||||||
|
if (ms)
|
||||||
|
te = setTimeout(r.hide, ms * 1000);
|
||||||
|
|
||||||
|
obj.innerHTML = '<a href="#" id="toastc">x</a><div id="toastb">' + lf2br(txt) + '</div>';
|
||||||
|
obj.className = cl;
|
||||||
|
ms += obj.offsetWidth;
|
||||||
|
obj.className += ' vis';
|
||||||
|
ebi('toastc').onclick = r.hide;
|
||||||
|
timer.add(scrollchk);
|
||||||
|
r.visible = true;
|
||||||
|
r.txt = txt;
|
||||||
|
};
|
||||||
|
|
||||||
|
r.ok = function (ms, txt) {
|
||||||
|
r.show('ok', ms, txt);
|
||||||
|
};
|
||||||
|
r.inf = function (ms, txt) {
|
||||||
|
r.show('inf', ms, txt);
|
||||||
|
};
|
||||||
|
r.warn = function (ms, txt) {
|
||||||
|
r.show('warn', ms, txt);
|
||||||
|
};
|
||||||
|
r.err = function (ms, txt) {
|
||||||
|
r.show('err', ms, txt);
|
||||||
|
};
|
||||||
|
|
||||||
|
return r;
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
var modal = (function () {
|
||||||
|
var r = {},
|
||||||
|
q = [],
|
||||||
|
o = null,
|
||||||
|
cb_up = null,
|
||||||
|
cb_ok = null,
|
||||||
|
cb_ng = null,
|
||||||
|
prim = '<a href="#" id="modal-ok">OK</a>',
|
||||||
|
sec = '<a href="#" id="modal-ng">Cancel</a>',
|
||||||
|
ok_cancel = WINDOWS ? prim + sec : sec + prim;
|
||||||
|
|
||||||
|
r.busy = false;
|
||||||
|
|
||||||
|
r.show = function (html) {
|
||||||
|
o = mknod('div');
|
||||||
|
o.setAttribute('id', 'modal');
|
||||||
|
o.innerHTML = '<table><tr><td><div id="modalc">' + html + '</div></td></tr></table>';
|
||||||
|
document.body.appendChild(o);
|
||||||
|
document.addEventListener('keydown', onkey);
|
||||||
|
r.busy = true;
|
||||||
|
|
||||||
|
var a = ebi('modal-ng');
|
||||||
|
if (a)
|
||||||
|
a.onclick = ng;
|
||||||
|
|
||||||
|
a = ebi('modal-ok');
|
||||||
|
a.onclick = ok;
|
||||||
|
|
||||||
|
var inp = ebi('modali');
|
||||||
|
(inp || a).focus();
|
||||||
|
if (inp)
|
||||||
|
setTimeout(function () {
|
||||||
|
inp.setSelectionRange(0, inp.value.length, "forward");
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
document.addEventListener('focus', onfocus);
|
||||||
|
timer.add(onfocus);
|
||||||
|
if (cb_up)
|
||||||
|
setTimeout(cb_up, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
r.hide = function () {
|
||||||
|
timer.rm(onfocus);
|
||||||
|
document.removeEventListener('focus', onfocus);
|
||||||
|
document.removeEventListener('keydown', onkey);
|
||||||
|
o.parentNode.removeChild(o);
|
||||||
|
r.busy = false;
|
||||||
|
setTimeout(next, 50);
|
||||||
|
};
|
||||||
|
function ok(e) {
|
||||||
|
ev(e);
|
||||||
|
var v = ebi('modali');
|
||||||
|
v = v ? v.value : true;
|
||||||
|
r.hide();
|
||||||
|
if (cb_ok)
|
||||||
|
cb_ok(v);
|
||||||
|
}
|
||||||
|
function ng(e) {
|
||||||
|
ev(e);
|
||||||
|
r.hide();
|
||||||
|
if (cb_ng)
|
||||||
|
cb_ng(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onfocus(e) {
|
||||||
|
var ctr = ebi('modalc');
|
||||||
|
if (!ctr || !ctr.contains || !document.activeElement || ctr.contains(document.activeElement))
|
||||||
|
return;
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
ebi('modal-ok').focus();
|
||||||
|
}, 20);
|
||||||
|
ev(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onkey(e) {
|
||||||
|
if (e.code == 'Enter') {
|
||||||
|
var a = ebi('modal-ng');
|
||||||
|
if (a && document.activeElement == a)
|
||||||
|
return ng();
|
||||||
|
|
||||||
|
return ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.code == 'Escape')
|
||||||
|
return ng();
|
||||||
|
}
|
||||||
|
|
||||||
|
function next() {
|
||||||
|
if (!r.busy && q.length)
|
||||||
|
q.shift()();
|
||||||
|
}
|
||||||
|
|
||||||
|
r.alert = function (html, cb, fun) {
|
||||||
|
q.push(function () {
|
||||||
|
_alert(lf2br(html), cb, fun);
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
function _alert(html, cb, fun) {
|
||||||
|
cb_ok = cb_ng = cb;
|
||||||
|
cb_up = fun;
|
||||||
|
html += '<div id="modalb"><a href="#" id="modal-ok">OK</a></div>';
|
||||||
|
r.show(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
r.confirm = function (html, cok, cng, fun) {
|
||||||
|
q.push(function () {
|
||||||
|
_confirm(lf2br(html), cok, cng, fun);
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
function _confirm(html, cok, cng, fun) {
|
||||||
|
cb_ok = cok;
|
||||||
|
cb_ng = cng === undefined ? cok : null;
|
||||||
|
cb_up = fun;
|
||||||
|
html += '<div id="modalb">' + ok_cancel + '</div>';
|
||||||
|
r.show(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
r.prompt = function (html, v, cok, cng, fun) {
|
||||||
|
q.push(function () {
|
||||||
|
_prompt(lf2br(html), v, cok, cng, fun);
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
function _prompt(html, v, cok, cng, fun) {
|
||||||
|
cb_ok = cok;
|
||||||
|
cb_ng = cng === undefined ? cok : null;
|
||||||
|
cb_up = fun;
|
||||||
|
html += '<input id="modali" type="text" /><div id="modalb">' + ok_cancel + '</div>';
|
||||||
|
r.show(html);
|
||||||
|
|
||||||
|
ebi('modali').value = v || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return r;
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
function winpopup(txt) {
|
||||||
|
fetch(get_evpath(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||||
|
},
|
||||||
|
body: 'msg=' + uricom_enc(Date.now() + ', ' + txt)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var last_repl = null;
|
||||||
|
function repl_load() {
|
||||||
|
var ipre = ebi('repl_pre'),
|
||||||
|
tb = ebi('modali');
|
||||||
|
|
||||||
|
function getpres() {
|
||||||
|
var o, ret = jread("repl_pre", []);
|
||||||
|
if (!ret.length)
|
||||||
|
ret = [
|
||||||
|
'var v=Object.keys(localStorage); v.sort(); JSON.stringify(v)',
|
||||||
|
'console.hist.slice(-10).join("\\n")'
|
||||||
|
];
|
||||||
|
|
||||||
|
ipre.innerHTML = '<option value=""></option>';
|
||||||
|
for (var a = 0; a < ret.length; a++) {
|
||||||
|
o = mknod('option');
|
||||||
|
o.setAttribute('value', ret[a]);
|
||||||
|
o.textContent = ret[a];
|
||||||
|
ipre.appendChild(o);
|
||||||
|
}
|
||||||
|
last_repl = ipre.value = (last_repl || (ret.length ? ret.slice(-1)[0] : ''));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
ebi('repl_pdel').onclick = function (e) {
|
||||||
|
var val = ipre.value,
|
||||||
|
pres = getpres();
|
||||||
|
|
||||||
|
apop(pres, val);
|
||||||
|
jwrite('repl_pre', pres);
|
||||||
|
getpres();
|
||||||
|
};
|
||||||
|
ebi('repl_pnew').onclick = function (e) {
|
||||||
|
var val = tb.value,
|
||||||
|
pres = getpres();
|
||||||
|
|
||||||
|
apop(pres, ipre.value);
|
||||||
|
pres.push(val);
|
||||||
|
jwrite('repl_pre', pres);
|
||||||
|
getpres();
|
||||||
|
ipre.value = val;
|
||||||
|
};
|
||||||
|
ipre.oninput = ipre.onchange = function () {
|
||||||
|
tb.value = last_repl = ipre.value;
|
||||||
|
};
|
||||||
|
tb.oninput = function () {
|
||||||
|
last_repl = this.value;
|
||||||
|
};
|
||||||
|
getpres();
|
||||||
|
tb.value = last_repl;
|
||||||
|
setTimeout(function () {
|
||||||
|
tb.setSelectionRange(0, tb.value.length, "forward");
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
function repl(e) {
|
||||||
|
ev(e);
|
||||||
|
var html = [
|
||||||
|
'<p>js repl (prefix with <code>,</code> to allow raise)</p>',
|
||||||
|
'<p><select id="repl_pre"></select>',
|
||||||
|
' <button id="repl_pdel">❌ del</button>',
|
||||||
|
' <button id="repl_pnew">💾 SAVE</button></p>'
|
||||||
|
];
|
||||||
|
|
||||||
|
modal.prompt(html.join(''), '', function (cmd) {
|
||||||
|
if (!cmd)
|
||||||
|
return toast.inf(3, 'eval aborted');
|
||||||
|
|
||||||
|
if (cmd.startsWith(','))
|
||||||
|
return modal.alert(esc(eval(cmd.slice(1)) + ''))
|
||||||
|
|
||||||
|
try {
|
||||||
|
modal.alert(esc(eval(cmd) + ''));
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
modal.alert('<h6>exception</h6>' + esc(ex + ''));
|
||||||
|
}
|
||||||
|
}, undefined, repl_load);
|
||||||
|
}
|
||||||
|
if (ebi('repl'))
|
||||||
|
ebi('repl').onclick = repl;
|
||||||
|
|||||||
@@ -1,39 +1,7 @@
|
|||||||
/* put filetype icons inline with text
|
/* video, alternative 1:
|
||||||
#ggrid>a>span:before,
|
top-left icon, just like the other formats
|
||||||
#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: '📂';
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
margin: -.1em -.4em;
|
|
||||||
text-shadow: 0 0 .1em #000;
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* put filetype icons top-left */
|
|
||||||
#ggrid>a:before {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
margin: -.1em -.4em;
|
|
||||||
text-shadow: 0 0 .1em #000;
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* video */
|
|
||||||
#ggrid>a:is(
|
#ggrid>a:is(
|
||||||
[href$=".mkv"i],
|
[href$=".mkv"i],
|
||||||
[href$=".mp4"i],
|
[href$=".mp4"i],
|
||||||
@@ -41,6 +9,40 @@
|
|||||||
):before {
|
):before {
|
||||||
content: '📺';
|
content: '📺';
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* video, alternative 2:
|
||||||
|
play-icon in the middle of the thumbnail
|
||||||
|
=======================================================================
|
||||||
|
*/
|
||||||
|
#ggrid>a:is(
|
||||||
|
[href$=".mkv"i],
|
||||||
|
[href$=".mp4"i],
|
||||||
|
[href$=".webm"i],
|
||||||
|
) {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#ggrid>a:is(
|
||||||
|
[href$=".mkv"i],
|
||||||
|
[href$=".mp4"i],
|
||||||
|
[href$=".webm"i],
|
||||||
|
):before {
|
||||||
|
content: '▶';
|
||||||
|
opacity: .8;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1em .5em 1em .7em;
|
||||||
|
border-radius: 9em;
|
||||||
|
line-height: 0;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: none;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
left: calc(50% - 1em);
|
||||||
|
top: calc(50% - 1.4em);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* audio */
|
/* audio */
|
||||||
@@ -56,6 +58,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* image */
|
/* image */
|
||||||
#ggrid>a:is(
|
#ggrid>a:is(
|
||||||
[href$=".jpg"i],
|
[href$=".jpg"i],
|
||||||
|
|||||||
@@ -10,19 +10,25 @@ u k:k
|
|||||||
# share "." (the current directory)
|
# share "." (the current directory)
|
||||||
# as "/" (the webroot) for the following users:
|
# as "/" (the webroot) for the following users:
|
||||||
# "r" grants read-access for anyone
|
# "r" grants read-access for anyone
|
||||||
# "a ed" grants read-write to ed
|
# "rw ed" grants read-write to ed
|
||||||
.
|
.
|
||||||
/
|
/
|
||||||
r
|
r
|
||||||
a ed
|
rw ed
|
||||||
|
|
||||||
# custom permissions for the "priv" folder:
|
# custom permissions for the "priv" folder:
|
||||||
# user "k" can see/read the contents
|
# user "k" can only see/read the contents
|
||||||
# and "ed" gets read-write access
|
# user "ed" gets read-write access
|
||||||
./priv
|
./priv
|
||||||
/priv
|
/priv
|
||||||
r k
|
r k
|
||||||
a ed
|
rw ed
|
||||||
|
|
||||||
|
# this does the same thing:
|
||||||
|
./priv
|
||||||
|
/priv
|
||||||
|
r ed k
|
||||||
|
w ed
|
||||||
|
|
||||||
# share /home/ed/Music/ as /music and let anyone read it
|
# share /home/ed/Music/ as /music and let anyone read it
|
||||||
# (this will replace any folder called "music" in the webroot)
|
# (this will replace any folder called "music" in the webroot)
|
||||||
@@ -41,5 +47,5 @@ c e2d
|
|||||||
c nodupe
|
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:a,ed -v priv:priv:r,k:rw,ed -v /home/ed/Music:music:r -v /home/ed/inc:dump:w:c,e2d:c,nodupe
|
||||||
# but note that the config file always wins in case of conflicts
|
# but note that the config file always wins in case of conflicts
|
||||||
|
|||||||
51
docs/hls.html
Normal file
51
docs/hls.html
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<!DOCTYPE html><html lang="en"><head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>hls-test</title>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
</head><body>
|
||||||
|
|
||||||
|
<video id="vid" controls></video>
|
||||||
|
<script src="hls.light.js"></script>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
var video = document.getElementById('vid');
|
||||||
|
var hls = new Hls({
|
||||||
|
debug: true,
|
||||||
|
autoStartLoad: false
|
||||||
|
});
|
||||||
|
hls.loadSource('live/v.m3u8');
|
||||||
|
hls.attachMedia(video);
|
||||||
|
hls.on(Hls.Events.MANIFEST_PARSED, function() {
|
||||||
|
hls.startLoad(0);
|
||||||
|
});
|
||||||
|
hls.on(Hls.Events.MEDIA_ATTACHED, function() {
|
||||||
|
video.muted = true;
|
||||||
|
video.play();
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
general good news:
|
||||||
|
- doesn't need fixed-length segments; ok to let x264 pick optimal keyframes and slice on those
|
||||||
|
- hls.js polls the m3u8 for new segments, scales the duration accordingly, seeking works great
|
||||||
|
- the sfx will grow by 66 KiB since that's how small hls.js can get, wait thats not good
|
||||||
|
|
||||||
|
# vod, creates m3u8 at the end, fixed keyframes, v bad
|
||||||
|
ffmpeg -hide_banner -threads 0 -flags -global_header -i ..\CowboyBebopMovie-OP1.webm -vf scale=1280:-4,format=yuv420p -ac 2 -c:a libopus -b:a 128k -c:v libx264 -preset slow -crf 24 -maxrate:v 5M -bufsize:v 10M -g 120 -keyint_min 120 -sc_threshold 0 -hls_time 4 -hls_playlist_type vod -hls_segment_filename v%05d.ts v.m3u8
|
||||||
|
|
||||||
|
# live, updates m3u8 as it goes, dynamic keyframes, streamable with hls.js
|
||||||
|
ffmpeg -hide_banner -threads 0 -flags -global_header -i ..\..\CowboyBebopMovie-OP1.webm -vf scale=1280:-4,format=yuv420p -ac 2 -c:a libopus -b:a 128k -c:v libx264 -preset slow -crf 24 -maxrate:v 5M -bufsize:v 10M -f segment -segment_list v.m3u8 -segment_format mpegts -segment_list_flags live v%05d.ts
|
||||||
|
|
||||||
|
# fmp4 (fragmented mp4), doesn't work with hls.js, gets duratoin 149:07:51 (536871s), probably the tkhd/mdhd 0xffffffff (timebase 8000? ok)
|
||||||
|
ffmpeg -re -hide_banner -threads 0 -flags +cgop -i ..\..\CowboyBebopMovie-OP1.webm -vf scale=1280:-4,format=yuv420p -ac 2 -c:a libopus -b:a 128k -c:v libx264 -preset slow -crf 24 -maxrate:v 5M -bufsize:v 10M -f segment -segment_list v.m3u8 -segment_format fmp4 -segment_list_flags live v%05d.mp4
|
||||||
|
|
||||||
|
# try 2, works, uses tempfiles for m3u8 updates, good, 6% smaller
|
||||||
|
ffmpeg -re -hide_banner -threads 0 -flags +cgop -i ..\..\CowboyBebopMovie-OP1.webm -vf scale=1280:-4,format=yuv420p -ac 2 -c:a libopus -b:a 128k -c:v libx264 -preset slow -crf 24 -maxrate:v 5M -bufsize:v 10M -f hls -hls_segment_type fmp4 -hls_list_size 0 -hls_segment_filename v%05d.mp4 v.m3u8
|
||||||
|
|
||||||
|
more notes
|
||||||
|
- adding -hls_flags single_file makes duration wack during playback (for both fmp4 and ts), ok once finalized and refreshed, gives no size reduction anyways
|
||||||
|
- bebop op has good keyframe spacing for testing hls.js, in particular it hops one seg back and immediately resumes if it hits eof with the explicit hls.startLoad(0); otherwise it jumps into the middle of a seg and becomes art
|
||||||
|
- can probably -c:v copy most of the time, is there a way to check for cgop? todo
|
||||||
|
|
||||||
|
*/
|
||||||
|
</script>
|
||||||
|
</body></html>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
#u2cleanup, #u2conf tr:first-child>td[rowspan]:not(#u2btn_cw), /* most of the config options */
|
#u2cleanup, #u2conf tr:first-child>td[rowspan]:not(#u2btn_cw), /* most of the config options */
|
||||||
|
|
||||||
#u2cards /* and the upload progress tabs */
|
#u2cards, #u2etaw /* and the upload progress tabs */
|
||||||
|
|
||||||
{display: none !important} /* do it! */
|
{display: none !important} /* do it! */
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
/* add some margins because now it's weird */
|
/* add some margins because now it's weird */
|
||||||
.opview {margin-top: 2.5em}
|
.opview {margin-top: 2.5em}
|
||||||
#op_up2k {margin-top: 3em}
|
#op_up2k {margin-top: 6em}
|
||||||
|
|
||||||
/* and embiggen the upload button */
|
/* and embiggen the upload button */
|
||||||
#u2conf #u2btn, #u2btn {padding:1.5em 0}
|
#u2conf #u2btn, #u2btn {padding:1.5em 0}
|
||||||
@@ -27,6 +27,9 @@
|
|||||||
/* adjust the button area a bit */
|
/* adjust the button area a bit */
|
||||||
#u2conf.has_btn {width: 35em !important; margin: 5em auto}
|
#u2conf.has_btn {width: 35em !important; margin: 5em auto}
|
||||||
|
|
||||||
|
/* a */
|
||||||
|
#op_up2k {min-height: 0}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<a href="#" onclick="this.parentNode.innerHTML='';">show advanced options</a>
|
<a href="#" onclick="this.parentNode.innerHTML='';">show advanced options</a>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ avg() { awk 'function pr(ncsz) {if (nsmp>0) {printf "%3s %s\n", csz, sum/nsmp} c
|
|||||||
dirs=("$HOME/vfs/ほげ" "$HOME/vfs/ほげ/ぴよ" "$HOME/vfs/$(printf \\xed\\x91)" "$HOME/vfs/$(printf \\xed\\x91/\\xed\\x92)")
|
dirs=("$HOME/vfs/ほげ" "$HOME/vfs/ほげ/ぴよ" "$HOME/vfs/$(printf \\xed\\x91)" "$HOME/vfs/$(printf \\xed\\x91/\\xed\\x92)")
|
||||||
mkdir -p "${dirs[@]}"
|
mkdir -p "${dirs[@]}"
|
||||||
for dir in "${dirs[@]}"; do for fn in ふが "$(printf \\xed\\x93)" 'qwe,rty;asd fgh+jkl%zxc&vbn <qwe>"rty'"'"'uio&asd fgh'; do echo "$dir" > "$dir/$fn.html"; done; done
|
for dir in "${dirs[@]}"; do for fn in ふが "$(printf \\xed\\x93)" 'qwe,rty;asd fgh+jkl%zxc&vbn <qwe>"rty'"'"'uio&asd fgh'; do echo "$dir" > "$dir/$fn.html"; done; done
|
||||||
|
# qw er+ty%20ui%%20op<as>df&gh&jk#zx'cv"bn`m=qw*er^ty?ui@op,as.df-gh_jk
|
||||||
|
|
||||||
##
|
##
|
||||||
## upload mojibake
|
## upload mojibake
|
||||||
@@ -79,6 +79,10 @@ command -v gdate && date() { gdate "$@"; }; while true; do t=$(date +%s.%N); (ti
|
|||||||
# get all up2k search result URLs
|
# get all up2k search result URLs
|
||||||
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"));
|
||||||
|
|
||||||
|
# rename all selected songs to <leading-track-number> + <Title> + <extension>
|
||||||
|
var sel=msel.getsel(), ci=find_file_col('Title')[0], re=[]; for (var a=0; a<sel.length; a++) { var url=sel[a].vp, tag=ebi(sel[a].id).closest('tr').querySelectorAll('td')[ci].textContent, name=uricom_dec(vsplit(url)[1])[0], m=/^([0-9]+[\. -]+)?.*(\.[^\.]+$)/.exec(name), name2=(m[1]||'')+tag+m[2], url2=vsplit(url)[0]+uricom_enc(name2,false); if (url!=url2) re.push([url, url2]); }
|
||||||
|
console.log(JSON.stringify(re, null, ' '));
|
||||||
|
function f() { if (!re.length) return treectl.goto(get_evpath()); var [u1,u2] = re.shift(); fetch(u1+'?move='+u2).then((rsp) => {if (rsp.ok) f(); }); }; f();
|
||||||
|
|
||||||
##
|
##
|
||||||
## bash oneliners
|
## bash oneliners
|
||||||
@@ -103,6 +107,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
|
||||||
@@ -113,6 +126,13 @@ e=6; s=10; d=~/dev/copyparty/srv/aus; n=1; p=0; e=$((e*60)); rm -rf $d; mkdir $d
|
|||||||
-v srv/aus:aus:r:ce2dsa:ce2ts:cmtp=fgsfds=bin/mtag/sleep.py
|
-v srv/aus:aus:r:ce2dsa:ce2ts:cmtp=fgsfds=bin/mtag/sleep.py
|
||||||
sqlite3 .hist/up2k.db 'select * from mt where k="fgsfds" or k="t:mtp"' | tee /dev/stderr | wc -l
|
sqlite3 .hist/up2k.db 'select * from mt where k="fgsfds" or k="t:mtp"' | tee /dev/stderr | wc -l
|
||||||
|
|
||||||
|
# generate the sine meme
|
||||||
|
for ((f=420;f<1200;f++)); do sz=$(ffmpeg -y -f lavfi -i sine=frequency=$f:duration=2 -vf volume=0.1 -ac 1 -ar 44100 -f s16le /dev/shm/a.wav 2>/dev/null; base64 -w0 </dev/shm/a.wav | gzip -c | wc -c); printf '%d %d\n' $f $sz; done | tee /dev/stderr | sort -nrk2,2
|
||||||
|
ffmpeg -y -f lavfi -i sine=frequency=1050:duration=2 -vf volume=0.1 -ac 1 -ar 44100 /dev/shm/a.wav
|
||||||
|
|
||||||
|
# play icon calibration pics
|
||||||
|
for w in 150 170 190 210 230 250; do for h in 130 150 170 190 210; do /c/Program\ Files/ImageMagick-7.0.11-Q16-HDRI/magick.exe convert -size ${w}x${h} xc:brown -fill orange -draw "circle $((w/2)),$((h/2)) $((w/2)),$((h/3))" $w-$h.png; done; done
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## vscode
|
## vscode
|
||||||
@@ -144,7 +164,7 @@ brew install python@2
|
|||||||
pip install virtualenv
|
pip install virtualenv
|
||||||
|
|
||||||
# readme toc
|
# readme toc
|
||||||
cat README.md | awk '!/^#/{next} {lv=length($1);sub(/[^ ]+ /,"");bab=$0;gsub(/ /,"-",bab)} {printf "%" ((lv-1)*4+1) "s [%s](#%s)\n", "*",$0,bab}'
|
cat README.md | awk 'function pr() { if (!h) {return}; if (/^ *[*!#]/||!s) {printf "%s\n",h;h=0;return}; if (/.../) {printf "%s - %s\n",h,$0;h=0}; }; /^#/{s=1;pr()} /^#* *(file indexing|install on android|dev env setup|just the sfx|complete release|optional gpl stuff)|`$/{s=0} /^#/{lv=length($1);sub(/[^ ]+ /,"");bab=$0;gsub(/ /,"-",bab); h=sprintf("%" ((lv-1)*4+1) "s [%s](#%s)", "*",$0,bab);next} !h{next} {sub(/:$/,"")} {pr()}' > toc; grep -E '^## readme toc' -B1000 -A2 <README.md >p1; grep -E '^## quickstart' -B2 -A999999 <README.md >p2; (cat p1; grep quickstart -A1000 <toc; cat p2) >README.md
|
||||||
|
|
||||||
# fix firefox phantom breakpoints,
|
# fix firefox phantom breakpoints,
|
||||||
# suggestions from bugtracker, doesnt work (debugger is not attachable)
|
# suggestions from bugtracker, doesnt work (debugger is not attachable)
|
||||||
@@ -157,7 +177,10 @@ dbg.asyncStore.pendingBreakpoints = {}
|
|||||||
about:config >> devtools.debugger.prefs-schema-version = -1
|
about:config >> devtools.debugger.prefs-schema-version = -1
|
||||||
|
|
||||||
# determine server version
|
# determine server version
|
||||||
git reset --hard origin/HEAD && git log --format=format:"%H %ai %d" --decorate=full > /dev/shm/revs && cat /dev/shm/revs | while read -r rev extra; do (git reset --hard $rev >/dev/null 2>/dev/null && dsz=$(cat copyparty/web/{util,browser,up2k}.js 2>/dev/null | diff -wNarU0 - <(cat /mnt/Users/ed/Downloads/ref/{util,browser,up2k}.js) | wc -c) && printf '%s %6s %s\n' "$rev" $dsz "$extra") </dev/null; done
|
git pull; git reset --hard origin/HEAD && git log --format=format:"%H %ai %d" --decorate=full > ../revs && cat ../{util,browser,up2k}.js >../vr && cat ../revs | while read -r rev extra; do (git reset --hard $rev >/dev/null 2>/dev/null && dsz=$(cat copyparty/web/{util,browser,up2k}.js >../vg 2>/dev/null && diff -wNarU0 ../{vg,vr} | wc -c) && printf '%s %6s %s\n' "$rev" $dsz "$extra") </dev/null; done
|
||||||
|
|
||||||
|
# download all sfx versions
|
||||||
|
curl https://api.github.com/repos/9001/copyparty/releases?per_page=100 | jq -r '.[] | .tag_name + " " + .name' | tr -d '\r' | while read v t; do fn="copyparty $v $t.py"; [ -e "$fn" ] || curl https://github.com/9001/copyparty/releases/download/$v/copyparty-sfx.py -Lo "$fn"; done
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -200,3 +223,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"
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
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.4 \
|
||||||
ver_mde=2.14.0 \
|
ver_mde=2.14.0 \
|
||||||
ver_codemirror=5.59.3 \
|
ver_codemirror=5.59.3 \
|
||||||
ver_fontawesome=5.13.0 \
|
ver_fontawesome=5.13.0 \
|
||||||
ver_zopfli=1.0.3
|
ver_zopfli=1.0.3
|
||||||
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
# sha512.hw.js https://github.com/Daninet/hash-wasm
|
|
||||||
# sha512.kc.js https://github.com/chm-diederichs/sha3-wasm
|
|
||||||
# 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' <sha512.ac.js.orig >sha512.ac.js; for fn in sha512.ac.js.orig sha512.ac.js; do wc -c <$fn; wc -c <$fn.gz ; for n in {1..9}; do printf '%8d %d bz\n' $(bzip2 -c$n <$fn | wc -c) $n; done; done
|
|
||||||
|
|
||||||
|
|
||||||
# download;
|
# download;
|
||||||
# the scp url is latin from https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap
|
# the scp url is latin from https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap
|
||||||
RUN mkdir -p /z/dist/no-pk \
|
RUN mkdir -p /z/dist/no-pk \
|
||||||
@@ -27,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 ) \
|
||||||
@@ -64,29 +63,27 @@ 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
|
||||||
RUN cd ogvjs-$ver_ogvjs \
|
RUN cd ogvjs-$ver_ogvjs \
|
||||||
&& cp -pv \
|
&& cp -pv \
|
||||||
ogv.js \
|
|
||||||
ogv-worker-audio.js \
|
ogv-worker-audio.js \
|
||||||
ogv-demuxer-ogg-wasm.js \
|
ogv-demuxer-ogg-wasm.js \
|
||||||
ogv-demuxer-ogg-wasm.wasm \
|
ogv-demuxer-ogg-wasm.wasm \
|
||||||
ogv-demuxer-webm-wasm.js \
|
|
||||||
ogv-demuxer-webm-wasm.wasm \
|
|
||||||
ogv-decoder-audio-opus-wasm.js \
|
ogv-decoder-audio-opus-wasm.js \
|
||||||
ogv-decoder-audio-opus-wasm.wasm \
|
ogv-decoder-audio-opus-wasm.wasm \
|
||||||
ogv-decoder-audio-vorbis-wasm.js \
|
ogv-decoder-audio-vorbis-wasm.js \
|
||||||
ogv-decoder-audio-vorbis-wasm.wasm \
|
ogv-decoder-audio-vorbis-wasm.wasm \
|
||||||
/z/dist
|
/z/dist \
|
||||||
|
&& cp -pv \
|
||||||
# ogv-demuxer-ogg.js \
|
ogv-es2017.js /z/dist/ogv.js
|
||||||
# ogv-demuxer-webm.js \
|
|
||||||
# ogv-decoder-audio-opus.js \
|
|
||||||
# ogv-decoder-audio-vorbis.js \
|
|
||||||
# dynamicaudio.swf \
|
|
||||||
|
|
||||||
|
|
||||||
# build marked
|
# build marked
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ all: $(addsuffix .gz, $(wildcard *.*))
|
|||||||
|
|
||||||
%.gz: %
|
%.gz: %
|
||||||
#brotli -q 11 $<
|
#brotli -q 11 $<
|
||||||
pigz -11 -J 34 -I 573 $<
|
pigz -11 -I 573 $<
|
||||||
|
|
||||||
# pigz -11 -J 34 -I 100 -F < $< > $@.first
|
# pigz -11 -J 34 -I 100 -F < $< > $@.first
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
set -e
|
set -e
|
||||||
echo
|
echo
|
||||||
|
|
||||||
|
help() { exec cat <<'EOF'
|
||||||
|
|
||||||
# optional args:
|
# optional args:
|
||||||
#
|
#
|
||||||
@@ -20,7 +21,14 @@ echo
|
|||||||
#
|
#
|
||||||
# `no-cm` saves ~90k by removing easymde/codemirror
|
# `no-cm` saves ~90k by removing easymde/codemirror
|
||||||
# (the fancy markdown editor)
|
# (the fancy markdown editor)
|
||||||
|
#
|
||||||
|
# `no-fnt` saves ~9k by removing the source-code-pro font
|
||||||
|
# (mainly used my the markdown viewer/editor)
|
||||||
|
#
|
||||||
|
# `no-dd` saves ~2k by removing the mouse cursor
|
||||||
|
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
# port install gnutar findutils gsed coreutils
|
# port install gnutar findutils gsed coreutils
|
||||||
gtar=$(command -v gtar || command -v gnutar) || true
|
gtar=$(command -v gtar || command -v gnutar) || true
|
||||||
@@ -29,6 +37,9 @@ gtar=$(command -v gtar || command -v gnutar) || true
|
|||||||
sed() { gsed "$@"; }
|
sed() { gsed "$@"; }
|
||||||
find() { gfind "$@"; }
|
find() { gfind "$@"; }
|
||||||
sort() { gsort "$@"; }
|
sort() { gsort "$@"; }
|
||||||
|
shuf() { gshuf "$@"; }
|
||||||
|
nproc() { gnproc; }
|
||||||
|
sha1sum() { shasum "$@"; }
|
||||||
unexpand() { gunexpand "$@"; }
|
unexpand() { gunexpand "$@"; }
|
||||||
command -v grealpath >/dev/null &&
|
command -v grealpath >/dev/null &&
|
||||||
realpath() { grealpath "$@"; }
|
realpath() { grealpath "$@"; }
|
||||||
@@ -57,14 +68,19 @@ use_gz=
|
|||||||
do_sh=1
|
do_sh=1
|
||||||
do_py=1
|
do_py=1
|
||||||
while [ ! -z "$1" ]; do
|
while [ ! -z "$1" ]; do
|
||||||
[ "$1" = clean ] && clean=1 && shift && continue
|
case $1 in
|
||||||
[ "$1" = re ] && repack=1 && shift && continue
|
clean) clean=1 ; ;;
|
||||||
[ "$1" = gz ] && use_gz=1 && shift && continue
|
re) repack=1 ; ;;
|
||||||
[ "$1" = no-ogv ] && no_ogv=1 && shift && continue
|
gz) use_gz=1 ; ;;
|
||||||
[ "$1" = no-cm ] && no_cm=1 && shift && continue
|
no-ogv) no_ogv=1 ; ;;
|
||||||
[ "$1" = no-sh ] && do_sh= && shift && continue
|
no-fnt) no_fnt=1 ; ;;
|
||||||
[ "$1" = no-py ] && do_py= && shift && continue
|
no-dd) no_dd=1 ; ;;
|
||||||
break
|
no-cm) no_cm=1 ; ;;
|
||||||
|
no-sh) do_sh= ; ;;
|
||||||
|
no-py) do_py= ; ;;
|
||||||
|
*) help ; ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
tmv() {
|
tmv() {
|
||||||
@@ -72,16 +88,23 @@ tmv() {
|
|||||||
mv t "$1"
|
mv t "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stamp=$(
|
||||||
|
for d in copyparty scripts; do
|
||||||
|
find $d -type f -printf '%TY-%Tm-%Td %TH:%TM:%TS %p\n'
|
||||||
|
done | sort | tail -n 1 | sha1sum | cut -c-16
|
||||||
|
)
|
||||||
|
|
||||||
rm -rf sfx/*
|
rm -rf sfx/*
|
||||||
mkdir -p sfx build
|
mkdir -p sfx build
|
||||||
cd sfx
|
cd sfx
|
||||||
|
|
||||||
[ $repack ] && {
|
tmpdir="$(
|
||||||
old="$(
|
printf '%s\n' "$TMPDIR" /tmp |
|
||||||
printf '%s\n' "$TMPDIR" /tmp |
|
awk '/./ {print; exit}'
|
||||||
awk '/./ {print; exit}'
|
)"
|
||||||
)/pe-copyparty"
|
|
||||||
|
|
||||||
|
[ $repack ] && {
|
||||||
|
old="$tmpdir/pe-copyparty"
|
||||||
echo "repack of files in $old"
|
echo "repack of files in $old"
|
||||||
cp -pR "$old/"*{dep-j2,copyparty} .
|
cp -pR "$old/"*{dep-j2,copyparty} .
|
||||||
}
|
}
|
||||||
@@ -123,6 +146,7 @@ cd sfx
|
|||||||
}
|
}
|
||||||
|
|
||||||
ver=
|
ver=
|
||||||
|
[ -z "$repack" ] &&
|
||||||
git describe --tags >/dev/null 2>/dev/null && {
|
git describe --tags >/dev/null 2>/dev/null && {
|
||||||
git_ver="$(git describe --tags)"; # v0.5.5-2-gb164aa0
|
git_ver="$(git describe --tags)"; # v0.5.5-2-gb164aa0
|
||||||
ver="$(printf '%s\n' "$git_ver" | sed -r 's/^v//')";
|
ver="$(printf '%s\n' "$git_ver" | sed -r 's/^v//')";
|
||||||
@@ -163,12 +187,12 @@ mkdir -p ../dist
|
|||||||
sfx_out=../dist/copyparty-sfx
|
sfx_out=../dist/copyparty-sfx
|
||||||
|
|
||||||
echo cleanup
|
echo cleanup
|
||||||
find .. -name '*.pyc' -delete
|
find -name '*.pyc' -delete
|
||||||
find .. -name __pycache__ -delete
|
find -name __pycache__ -delete
|
||||||
|
|
||||||
# especially prevent osx from leaking your lan ip (wtf apple)
|
# especially prevent osx from leaking your lan ip (wtf apple)
|
||||||
find .. -type f \( -name .DS_Store -or -name ._.DS_Store \) -delete
|
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/dbg-* copyparty/web/Makefile
|
rm -f copyparty/web/deps/*.full.* copyparty/web/dbg-* copyparty/web/Makefile
|
||||||
@@ -187,7 +211,24 @@ done
|
|||||||
rm -rf copyparty/web/mde.* copyparty/web/deps/easymde*
|
rm -rf copyparty/web/mde.* copyparty/web/deps/easymde*
|
||||||
echo h > copyparty/web/mde.html
|
echo h > copyparty/web/mde.html
|
||||||
f=copyparty/web/md.html
|
f=copyparty/web/md.html
|
||||||
sed -r '/edit2">edit \(fancy/d' <$f >t && tmv "$f"
|
sed -r '/edit2">edit \(fancy/d' <$f >t
|
||||||
|
tmv "$f"
|
||||||
|
}
|
||||||
|
|
||||||
|
[ $no_fnt ] && {
|
||||||
|
rm -f copyparty/web/deps/scp.woff2
|
||||||
|
f=copyparty/web/md.css
|
||||||
|
gzip -d "$f"
|
||||||
|
sed -r '/scp\.woff2/d' <$f >t
|
||||||
|
tmv "$f"
|
||||||
|
}
|
||||||
|
|
||||||
|
[ $no_dd ] && {
|
||||||
|
rm -rf copyparty/web/dd
|
||||||
|
f=copyparty/web/browser.css
|
||||||
|
gzip -d "$f"
|
||||||
|
sed -r 's/(cursor: ?)url\([^)]+\), ?(pointer)/\1\2/; /[0-9]+% \{cursor:/d; /animation: ?cursor/d' <$f >t
|
||||||
|
tmv "$f"
|
||||||
}
|
}
|
||||||
|
|
||||||
[ $repack ] ||
|
[ $repack ] ||
|
||||||
@@ -201,7 +242,8 @@ awk '/^LOREM_IPSUM_WORDS/{o=1;print "LOREM_IPSUM_WORDS = u\"a\"";next} !o; /"""/
|
|||||||
tmv "$f"
|
tmv "$f"
|
||||||
|
|
||||||
# up2k goes from 28k to 22k laff
|
# up2k goes from 28k to 22k laff
|
||||||
echo entabbening
|
awk 'BEGIN{gensub(//,"",1)}' </dev/null &&
|
||||||
|
echo entabbening &&
|
||||||
find | grep -E '\.css$' | while IFS= read -r f; do
|
find | grep -E '\.css$' | while IFS= read -r f; do
|
||||||
awk '{
|
awk '{
|
||||||
sub(/^[ \t]+/,"");
|
sub(/^[ \t]+/,"");
|
||||||
@@ -215,25 +257,61 @@ find | grep -E '\.css$' | while IFS= read -r f; do
|
|||||||
' <$f | sed 's/;\}$/}/' >t
|
' <$f | sed 's/;\}$/}/' >t
|
||||||
tmv "$f"
|
tmv "$f"
|
||||||
done
|
done
|
||||||
|
unexpand -h 2>/dev/null &&
|
||||||
find | grep -E '\.(js|html)$' | while IFS= read -r f; do
|
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() {
|
gzres() {
|
||||||
command -v pigz &&
|
command -v pigz &&
|
||||||
pk='pigz -11 -J 34 -I 100' ||
|
pk='pigz -11 -I 2560' ||
|
||||||
pk='gzip'
|
pk='gzip'
|
||||||
|
|
||||||
echo "$pk"
|
np=$(nproc)
|
||||||
find | grep -E '\.(js|css)$' | grep -vF /deps/ | while IFS= read -r f; do
|
echo "$pk #$np"
|
||||||
echo -n .
|
|
||||||
$pk "$f"
|
while IFS=' ' read -r _ f; do
|
||||||
done
|
while true; do
|
||||||
echo
|
na=$(ps auxwww | grep -F "$pk" | wc -l)
|
||||||
|
[ $na -le $np ] && break
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
echo -n .
|
||||||
|
$pk "$f" &
|
||||||
|
done < <(
|
||||||
|
find -printf '%s %p\n' |
|
||||||
|
grep -E '\.(js|css)$' |
|
||||||
|
grep -vF /deps/ |
|
||||||
|
sort -nr
|
||||||
|
)
|
||||||
|
wait
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
zdir="$tmpdir/cpp-mksfx"
|
||||||
|
[ -e "$zdir/$stamp" ] || rm -rf "$zdir"
|
||||||
|
mkdir -p "$zdir"
|
||||||
|
echo a > "$zdir/$stamp"
|
||||||
|
nf=$(ls -1 "$zdir"/arc.* | wc -l)
|
||||||
|
[ $nf -ge 2 ] && [ ! $repack ] && use_zdir=1 || use_zdir=
|
||||||
|
|
||||||
|
[ $use_zdir ] || {
|
||||||
|
echo "$nf alts += 1"
|
||||||
|
gzres
|
||||||
|
[ $repack ] ||
|
||||||
|
tar -cf "$zdir/arc.$(date +%s)" copyparty/web/*.gz
|
||||||
|
}
|
||||||
|
[ $use_zdir ] && {
|
||||||
|
arcs=("$zdir"/arc.*)
|
||||||
|
arc="${arcs[$RANDOM % ${#arcs[@]} ] }"
|
||||||
|
echo "using $arc"
|
||||||
|
tar -xf "$arc"
|
||||||
|
for f in copyparty/web/*.gz; do
|
||||||
|
rm "${f%.*}"
|
||||||
|
done
|
||||||
}
|
}
|
||||||
gzres
|
|
||||||
|
|
||||||
|
|
||||||
echo gen tarlist
|
echo gen tarlist
|
||||||
@@ -241,7 +319,7 @@ 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|br)$' list1; grep -E '\.(gz|br)$' list1) >list || true
|
(grep -vE '\.(gz|br)$' list1; grep -E '\.(gz|br)$' list1 | shuf) >list || true
|
||||||
|
|
||||||
echo creating tar
|
echo creating tar
|
||||||
args=(--owner=1000 --group=1000)
|
args=(--owner=1000 --group=1000)
|
||||||
|
|||||||
@@ -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
|
to edit this file, use HxD or "vim -b"
|
||||||
|
(there is 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
|
||||||
@@ -364,7 +364,7 @@ def confirm(rv):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
sys.exit(rv)
|
sys.exit(rv or 1)
|
||||||
|
|
||||||
|
|
||||||
def run(tmp, j2):
|
def run(tmp, j2):
|
||||||
@@ -380,7 +380,7 @@ def run(tmp, j2):
|
|||||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if not WINDOWS:
|
if not WINDOWS:
|
||||||
msg("\033[31mflock:", repr(ex))
|
msg("\033[31mflock:{!r}\033[0m".format(ex))
|
||||||
|
|
||||||
t = threading.Thread(target=utime, args=(tmp,))
|
t = threading.Thread(target=utime, args=(tmp,))
|
||||||
t.daemon = True
|
t.daemon = True
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ def tc1():
|
|||||||
pdirs = [x.replace("\\", "/") for x in pdirs]
|
pdirs = [x.replace("\\", "/") for x in pdirs]
|
||||||
udirs = [x.split("/", 2)[2] for x in pdirs]
|
udirs = [x.split("/", 2)[2] for x in pdirs]
|
||||||
perms = [x.rstrip("j/")[-1] for x in pdirs]
|
perms = [x.rstrip("j/")[-1] for x in pdirs]
|
||||||
|
perms = ["rw" if x == "a" else x for x in perms]
|
||||||
for pd, ud, p in zip(pdirs, udirs, perms):
|
for pd, ud, p in zip(pdirs, udirs, perms):
|
||||||
if ud[-1] == "j":
|
if ud[-1] == "j":
|
||||||
continue
|
continue
|
||||||
@@ -124,7 +125,7 @@ def tc1():
|
|||||||
|
|
||||||
arg = "{}:{}:{}".format(pd, ud, p, hp)
|
arg = "{}:{}:{}".format(pd, ud, p, hp)
|
||||||
if hp:
|
if hp:
|
||||||
arg += ":chist=" + hp
|
arg += ":c,hist=" + hp
|
||||||
|
|
||||||
args += ["-v", arg]
|
args += ["-v", arg]
|
||||||
|
|
||||||
@@ -147,14 +148,14 @@ def tc1():
|
|||||||
u = "{}{}/a.h264".format(ub, d)
|
u = "{}{}/a.h264".format(ub, d)
|
||||||
r = requests.get(u)
|
r = requests.get(u)
|
||||||
ok = bool(r)
|
ok = bool(r)
|
||||||
if ok != (p in ["a"]):
|
if ok != (p in ["rw"]):
|
||||||
raise Exception("get {} with perm {} at {}".format(ok, p, u))
|
raise Exception("get {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
# stat filesystem
|
# stat filesystem
|
||||||
for d, p in zip(pdirs, perms):
|
for d, p in zip(pdirs, perms):
|
||||||
u = "{}/a.h264".format(d)
|
u = "{}/a.h264".format(d)
|
||||||
ok = os.path.exists(u)
|
ok = os.path.exists(u)
|
||||||
if ok != (p in ["a", "w"]):
|
if ok != (p in ["rw", "w"]):
|
||||||
raise Exception("stat {} with perm {} at {}".format(ok, p, u))
|
raise Exception("stat {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
# GET thumbnail, vreify contents
|
# GET thumbnail, vreify contents
|
||||||
@@ -162,7 +163,7 @@ def tc1():
|
|||||||
u = "{}{}/a.h264?th=j".format(ub, d)
|
u = "{}{}/a.h264?th=j".format(ub, d)
|
||||||
r = requests.get(u)
|
r = requests.get(u)
|
||||||
ok = bool(r and r.content[:3] == b"\xff\xd8\xff")
|
ok = bool(r and r.content[:3] == b"\xff\xd8\xff")
|
||||||
if ok != (p in ["a"]):
|
if ok != (p in ["rw"]):
|
||||||
raise Exception("thumb {} with perm {} at {}".format(ok, p, u))
|
raise Exception("thumb {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
# check tags
|
# check tags
|
||||||
@@ -179,10 +180,10 @@ def tc1():
|
|||||||
r_ok = bool(j)
|
r_ok = bool(j)
|
||||||
w_ok = bool(r_ok and j.get("files"))
|
w_ok = bool(r_ok and j.get("files"))
|
||||||
|
|
||||||
if not r_ok or w_ok != (p in ["a"]):
|
if not r_ok or w_ok != (p in ["rw"]):
|
||||||
raise Exception("ls {} with perm {} at {}".format(ok, p, u))
|
raise Exception("ls {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
if (tag and p != "a") or (not tag and p == "a"):
|
if (tag and p != "rw") or (not tag and p == "rw"):
|
||||||
raise Exception("tag {} with perm {} at {}".format(tag, p, u))
|
raise Exception("tag {} with perm {} at {}".format(tag, p, u))
|
||||||
|
|
||||||
if tag is not None and tag != "48x32":
|
if tag is not None and tag != "48x32":
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ def uncomment(fpath):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("uncommenting", end="")
|
print("uncommenting", end="", flush=True)
|
||||||
for f in sys.argv[1:]:
|
for f in sys.argv[1:]:
|
||||||
print(".", end="")
|
print(".", end="", flush=True)
|
||||||
uncomment(f)
|
uncomment(f)
|
||||||
|
|
||||||
print("k")
|
print("k")
|
||||||
|
|||||||
1
setup.py
1
setup.py
@@ -99,6 +99,7 @@ args = {
|
|||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.7",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
|
|||||||
@@ -23,21 +23,28 @@ def hdr(query):
|
|||||||
|
|
||||||
|
|
||||||
class Cfg(Namespace):
|
class Cfg(Namespace):
|
||||||
def __init__(self, a=[], v=[], c=None):
|
def __init__(self, a=None, v=None, c=None):
|
||||||
super(Cfg, self).__init__(
|
super(Cfg, self).__init__(
|
||||||
a=a,
|
a=a or [],
|
||||||
v=v,
|
v=v or [],
|
||||||
c=c,
|
c=c,
|
||||||
rproxy=0,
|
rproxy=0,
|
||||||
ed=False,
|
ed=False,
|
||||||
|
nw=False,
|
||||||
|
unpost=600,
|
||||||
|
no_mv=False,
|
||||||
|
no_del=False,
|
||||||
no_zip=False,
|
no_zip=False,
|
||||||
|
no_voldump=True,
|
||||||
no_scandir=False,
|
no_scandir=False,
|
||||||
no_sendfile=True,
|
no_sendfile=True,
|
||||||
no_rescan=True,
|
no_rescan=True,
|
||||||
|
re_maxage=0,
|
||||||
ihead=False,
|
ihead=False,
|
||||||
nih=True,
|
nih=True,
|
||||||
mtp=[],
|
mtp=[],
|
||||||
mte="a",
|
mte="a",
|
||||||
|
mth="",
|
||||||
hist=None,
|
hist=None,
|
||||||
no_hash=False,
|
no_hash=False,
|
||||||
css_browser=None,
|
css_browser=None,
|
||||||
@@ -89,7 +96,7 @@ class TestHttpCli(unittest.TestCase):
|
|||||||
if not vol.startswith(top):
|
if not vol.startswith(top):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mode = vol[-2]
|
mode = vol[-2].replace("a", "rwmd")
|
||||||
usr = vol[-1]
|
usr = vol[-1]
|
||||||
if usr == "a":
|
if usr == "a":
|
||||||
usr = ""
|
usr = ""
|
||||||
@@ -98,7 +105,7 @@ class TestHttpCli(unittest.TestCase):
|
|||||||
vol += "/"
|
vol += "/"
|
||||||
|
|
||||||
top, sub = vol.split("/", 1)
|
top, sub = vol.split("/", 1)
|
||||||
vcfg.append("{0}/{1}:{1}:{2}{3}".format(top, sub, mode, usr))
|
vcfg.append("{0}/{1}:{1}:{2},{3}".format(top, sub, mode, usr))
|
||||||
|
|
||||||
pprint.pprint(vcfg)
|
pprint.pprint(vcfg)
|
||||||
|
|
||||||
|
|||||||
@@ -16,18 +16,21 @@ from copyparty import util
|
|||||||
|
|
||||||
|
|
||||||
class Cfg(Namespace):
|
class Cfg(Namespace):
|
||||||
def __init__(self, a=[], v=[], c=None):
|
def __init__(self, a=None, v=None, c=None):
|
||||||
ex = {k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()}
|
ex = {k: False for k in "nw e2d e2ds e2dsa e2t e2ts e2tsr".split()}
|
||||||
ex2 = {
|
ex2 = {
|
||||||
"mtp": [],
|
"mtp": [],
|
||||||
"mte": "a",
|
"mte": "a",
|
||||||
|
"mth": "",
|
||||||
"hist": None,
|
"hist": None,
|
||||||
"no_hash": False,
|
"no_hash": False,
|
||||||
"css_browser": None,
|
"css_browser": None,
|
||||||
|
"no_voldump": True,
|
||||||
|
"re_maxage": 0,
|
||||||
"rproxy": 0,
|
"rproxy": 0,
|
||||||
}
|
}
|
||||||
ex.update(ex2)
|
ex.update(ex2)
|
||||||
super(Cfg, self).__init__(a=a, v=v, c=c, **ex)
|
super(Cfg, self).__init__(a=a or [], v=v or [], c=c, **ex)
|
||||||
|
|
||||||
|
|
||||||
class TestVFS(unittest.TestCase):
|
class TestVFS(unittest.TestCase):
|
||||||
@@ -57,8 +60,8 @@ class TestVFS(unittest.TestCase):
|
|||||||
# type: (VFS, str, str) -> tuple[str, str, str]
|
# 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, [[True]])
|
||||||
r2 = vn.ls(rem, uname, False)
|
r2 = vn.ls(rem, uname, False, [[True]])
|
||||||
self.assertEqual(r1, r2)
|
self.assertEqual(r1, r2)
|
||||||
|
|
||||||
fsdir, real, virt = r1
|
fsdir, real, virt = r1
|
||||||
@@ -68,6 +71,11 @@ class TestVFS(unittest.TestCase):
|
|||||||
def log(self, src, msg, c=0):
|
def log(self, src, msg, c=0):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def assertAxs(self, dct, lst):
|
||||||
|
t1 = list(sorted(dct.keys()))
|
||||||
|
t2 = list(sorted(lst))
|
||||||
|
self.assertEqual(t1, t2)
|
||||||
|
|
||||||
def test(self):
|
def test(self):
|
||||||
td = os.path.join(self.td, "vfs")
|
td = os.path.join(self.td, "vfs")
|
||||||
os.mkdir(td)
|
os.mkdir(td)
|
||||||
@@ -88,53 +96,53 @@ class TestVFS(unittest.TestCase):
|
|||||||
self.assertEqual(vfs.nodes, {})
|
self.assertEqual(vfs.nodes, {})
|
||||||
self.assertEqual(vfs.vpath, "")
|
self.assertEqual(vfs.vpath, "")
|
||||||
self.assertEqual(vfs.realpath, td)
|
self.assertEqual(vfs.realpath, td)
|
||||||
self.assertEqual(vfs.uread, ["*"])
|
self.assertAxs(vfs.axs.uread, ["*"])
|
||||||
self.assertEqual(vfs.uwrite, ["*"])
|
self.assertAxs(vfs.axs.uwrite, ["*"])
|
||||||
|
|
||||||
# single read-only rootfs (relative path)
|
# single read-only rootfs (relative path)
|
||||||
vfs = AuthSrv(Cfg(v=["a/ab/::r"]), self.log).vfs
|
vfs = AuthSrv(Cfg(v=["a/ab/::r"]), self.log).vfs
|
||||||
self.assertEqual(vfs.nodes, {})
|
self.assertEqual(vfs.nodes, {})
|
||||||
self.assertEqual(vfs.vpath, "")
|
self.assertEqual(vfs.vpath, "")
|
||||||
self.assertEqual(vfs.realpath, os.path.join(td, "a", "ab"))
|
self.assertEqual(vfs.realpath, os.path.join(td, "a", "ab"))
|
||||||
self.assertEqual(vfs.uread, ["*"])
|
self.assertAxs(vfs.axs.uread, ["*"])
|
||||||
self.assertEqual(vfs.uwrite, [])
|
self.assertAxs(vfs.axs.uwrite, [])
|
||||||
|
|
||||||
# single read-only rootfs (absolute path)
|
# single read-only rootfs (absolute path)
|
||||||
vfs = AuthSrv(Cfg(v=[td + "//a/ac/../aa//::r"]), self.log).vfs
|
vfs = AuthSrv(Cfg(v=[td + "//a/ac/../aa//::r"]), self.log).vfs
|
||||||
self.assertEqual(vfs.nodes, {})
|
self.assertEqual(vfs.nodes, {})
|
||||||
self.assertEqual(vfs.vpath, "")
|
self.assertEqual(vfs.vpath, "")
|
||||||
self.assertEqual(vfs.realpath, os.path.join(td, "a", "aa"))
|
self.assertEqual(vfs.realpath, os.path.join(td, "a", "aa"))
|
||||||
self.assertEqual(vfs.uread, ["*"])
|
self.assertAxs(vfs.axs.uread, ["*"])
|
||||||
self.assertEqual(vfs.uwrite, [])
|
self.assertAxs(vfs.axs.uwrite, [])
|
||||||
|
|
||||||
# read-only rootfs with write-only subdirectory (read-write for k)
|
# read-only rootfs with write-only subdirectory (read-write for k)
|
||||||
vfs = AuthSrv(
|
vfs = AuthSrv(
|
||||||
Cfg(a=["k:k"], v=[".::r:ak", "a/ac/acb:a/ac/acb:w:ak"]),
|
Cfg(a=["k:k"], v=[".::r:rw,k", "a/ac/acb:a/ac/acb:w:rw,k"]),
|
||||||
self.log,
|
self.log,
|
||||||
).vfs
|
).vfs
|
||||||
self.assertEqual(len(vfs.nodes), 1)
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
self.assertEqual(vfs.vpath, "")
|
self.assertEqual(vfs.vpath, "")
|
||||||
self.assertEqual(vfs.realpath, td)
|
self.assertEqual(vfs.realpath, td)
|
||||||
self.assertEqual(vfs.uread, ["*", "k"])
|
self.assertAxs(vfs.axs.uread, ["*", "k"])
|
||||||
self.assertEqual(vfs.uwrite, ["k"])
|
self.assertAxs(vfs.axs.uwrite, ["k"])
|
||||||
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, os.path.join(td, "a"))
|
self.assertEqual(n.realpath, os.path.join(td, "a"))
|
||||||
self.assertEqual(n.uread, ["*", "k"])
|
self.assertAxs(n.axs.uread, ["*", "k"])
|
||||||
self.assertEqual(n.uwrite, ["k"])
|
self.assertAxs(n.axs.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, os.path.join(td, "a", "ac"))
|
self.assertEqual(n.realpath, os.path.join(td, "a", "ac"))
|
||||||
self.assertEqual(n.uread, ["*", "k"])
|
self.assertAxs(n.axs.uread, ["*", "k"])
|
||||||
self.assertEqual(n.uwrite, ["k"])
|
self.assertAxs(n.axs.uwrite, ["k"])
|
||||||
n = n.nodes["acb"]
|
n = n.nodes["acb"]
|
||||||
self.assertEqual(n.nodes, {})
|
self.assertEqual(n.nodes, {})
|
||||||
self.assertEqual(n.vpath, "a/ac/acb")
|
self.assertEqual(n.vpath, "a/ac/acb")
|
||||||
self.assertEqual(n.realpath, os.path.join(td, "a", "ac", "acb"))
|
self.assertEqual(n.realpath, os.path.join(td, "a", "ac", "acb"))
|
||||||
self.assertEqual(n.uread, ["k"])
|
self.assertAxs(n.axs.uread, ["k"])
|
||||||
self.assertEqual(n.uwrite, ["*", "k"])
|
self.assertAxs(n.axs.uwrite, ["*", "k"])
|
||||||
|
|
||||||
# something funky about the windows path normalization,
|
# something funky about the windows path normalization,
|
||||||
# doesn't really matter but makes the test messy, TODO?
|
# doesn't really matter but makes the test messy, TODO?
|
||||||
@@ -173,24 +181,24 @@ class TestVFS(unittest.TestCase):
|
|||||||
|
|
||||||
# admin-only rootfs with all-read-only subfolder
|
# admin-only rootfs with all-read-only subfolder
|
||||||
vfs = AuthSrv(
|
vfs = AuthSrv(
|
||||||
Cfg(a=["k:k"], v=[".::ak", "a:a:r"]),
|
Cfg(a=["k:k"], v=[".::rw,k", "a:a:r"]),
|
||||||
self.log,
|
self.log,
|
||||||
).vfs
|
).vfs
|
||||||
self.assertEqual(len(vfs.nodes), 1)
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
self.assertEqual(vfs.vpath, "")
|
self.assertEqual(vfs.vpath, "")
|
||||||
self.assertEqual(vfs.realpath, td)
|
self.assertEqual(vfs.realpath, td)
|
||||||
self.assertEqual(vfs.uread, ["k"])
|
self.assertAxs(vfs.axs.uread, ["k"])
|
||||||
self.assertEqual(vfs.uwrite, ["k"])
|
self.assertAxs(vfs.axs.uwrite, ["k"])
|
||||||
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, os.path.join(td, "a"))
|
self.assertEqual(n.realpath, os.path.join(td, "a"))
|
||||||
self.assertEqual(n.uread, ["*"])
|
self.assertAxs(n.axs.uread, ["*"])
|
||||||
self.assertEqual(n.uwrite, [])
|
self.assertAxs(n.axs.uwrite, [])
|
||||||
self.assertEqual(vfs.can_access("/", "*"), [False, False])
|
self.assertEqual(vfs.can_access("/", "*"), [False, False, False, False])
|
||||||
self.assertEqual(vfs.can_access("/", "k"), [True, True])
|
self.assertEqual(vfs.can_access("/", "k"), [True, True, False, False])
|
||||||
self.assertEqual(vfs.can_access("/a", "*"), [True, False])
|
self.assertEqual(vfs.can_access("/a", "*"), [True, False, False, False])
|
||||||
self.assertEqual(vfs.can_access("/a", "k"), [True, False])
|
self.assertEqual(vfs.can_access("/a", "k"), [True, False, False, False])
|
||||||
|
|
||||||
# breadth-first construction
|
# breadth-first construction
|
||||||
vfs = AuthSrv(
|
vfs = AuthSrv(
|
||||||
@@ -247,26 +255,26 @@ class TestVFS(unittest.TestCase):
|
|||||||
./src
|
./src
|
||||||
/dst
|
/dst
|
||||||
r a
|
r a
|
||||||
a asd
|
rw asd
|
||||||
"""
|
"""
|
||||||
).encode("utf-8")
|
).encode("utf-8")
|
||||||
)
|
)
|
||||||
|
|
||||||
au = AuthSrv(Cfg(c=[cfg_path]), self.log)
|
au = AuthSrv(Cfg(c=[cfg_path]), self.log)
|
||||||
self.assertEqual(au.user["a"], "123")
|
self.assertEqual(au.acct["a"], "123")
|
||||||
self.assertEqual(au.user["asd"], "fgh:jkl")
|
self.assertEqual(au.acct["asd"], "fgh:jkl")
|
||||||
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, None)
|
self.assertEqual(n.realpath, None)
|
||||||
self.assertEqual(n.uread, [])
|
self.assertAxs(n.axs.uread, [])
|
||||||
self.assertEqual(n.uwrite, [])
|
self.assertAxs(n.axs.uwrite, [])
|
||||||
self.assertEqual(len(n.nodes), 1)
|
self.assertEqual(len(n.nodes), 1)
|
||||||
n = n.nodes["dst"]
|
n = n.nodes["dst"]
|
||||||
self.assertEqual(n.vpath, "dst")
|
self.assertEqual(n.vpath, "dst")
|
||||||
self.assertEqual(n.realpath, os.path.join(td, "src"))
|
self.assertEqual(n.realpath, os.path.join(td, "src"))
|
||||||
self.assertEqual(n.uread, ["a", "asd"])
|
self.assertAxs(n.axs.uread, ["a", "asd"])
|
||||||
self.assertEqual(n.uwrite, ["asd"])
|
self.assertAxs(n.axs.uwrite, ["asd"])
|
||||||
self.assertEqual(len(n.nodes), 0)
|
self.assertEqual(len(n.nodes), 0)
|
||||||
|
|
||||||
os.unlink(cfg_path)
|
os.unlink(cfg_path)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ if MACOS:
|
|||||||
from copyparty.util import Unrecv
|
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()
|
||||||
stdout = stdout.decode("utf-8")
|
stdout = stdout.decode("utf-8")
|
||||||
@@ -39,8 +39,8 @@ def runcmd(*argv):
|
|||||||
return [p.returncode, stdout, stderr]
|
return [p.returncode, stdout, stderr]
|
||||||
|
|
||||||
|
|
||||||
def chkcmd(*argv):
|
def chkcmd(argv):
|
||||||
ok, sout, serr = runcmd(*argv)
|
ok, sout, serr = runcmd(argv)
|
||||||
if ok != 0:
|
if ok != 0:
|
||||||
raise Exception(serr)
|
raise Exception(serr)
|
||||||
|
|
||||||
@@ -60,12 +60,20 @@ def get_ramdisk():
|
|||||||
|
|
||||||
if os.path.exists("/Volumes"):
|
if os.path.exists("/Volumes"):
|
||||||
# hdiutil eject /Volumes/cptd/
|
# hdiutil eject /Volumes/cptd/
|
||||||
devname, _ = chkcmd("hdiutil", "attach", "-nomount", "ram://131072")
|
devname, _ = chkcmd("hdiutil attach -nomount ram://131072".split())
|
||||||
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])
|
||||||
|
with open("/Volumes/cptd/.metadata_never_index", "w") as f:
|
||||||
|
f.write("orz")
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree("/Volumes/cptd/.fseventsd")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
return subdir("/Volumes/cptd")
|
return subdir("/Volumes/cptd")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(repr(ex))
|
print(repr(ex))
|
||||||
@@ -108,6 +116,9 @@ 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, asrv, log, buf):
|
def __init__(self, args, asrv, log, buf):
|
||||||
@@ -116,13 +127,13 @@ class VHttpConn(object):
|
|||||||
self.addr = ("127.0.0.1", "42069")
|
self.addr = ("127.0.0.1", "42069")
|
||||||
self.args = args
|
self.args = args
|
||||||
self.asrv = asrv
|
self.asrv = asrv
|
||||||
self.is_mp = False
|
self.nid = None
|
||||||
self.log_func = log
|
self.log_func = log
|
||||||
self.log_src = "a"
|
self.log_src = "a"
|
||||||
self.lf_url = None
|
self.lf_url = None
|
||||||
self.hsrv = VHttpSrv()
|
self.hsrv = VHttpSrv()
|
||||||
|
self.nreq = 0
|
||||||
self.nbyte = 0
|
self.nbyte = 0
|
||||||
self.workload = 0
|
|
||||||
self.ico = None
|
self.ico = None
|
||||||
self.thumbcli = None
|
self.thumbcli = None
|
||||||
self.t0 = time.time()
|
self.t0 = time.time()
|
||||||
Reference in New Issue
Block a user