Compare commits

...

170 Commits

Author SHA1 Message Date
ed
500e3157b9 v1.3.16 2022-08-18 19:24:06 +02:00
ed
eba86b1d23 default-disable mt on https-desktop-chrome 2022-08-18 19:01:33 +02:00
ed
b69a563fc2 gc massage 2022-08-18 18:03:33 +02:00
ed
a900c36395 v1.3.15 2022-08-18 01:02:19 +02:00
ed
1d9b324d3e explain w/a wasm leaks in workers (chrome bug) 2022-08-18 01:02:06 +02:00
ed
539e7b8efe help chrome gc by reusing one filereader 2022-08-18 00:05:32 +02:00
ed
50a477ee47 up2k-hook-ytid: upload into subdirs by id 2022-08-15 21:52:41 +02:00
ed
7000123a8b v1.3.14 2022-08-15 20:25:31 +02:00
ed
d48a7d2398 provide tagparsers with uploader info 2022-08-15 20:23:17 +02:00
ed
389a00ce59 v1.3.13 2022-08-15 19:11:21 +02:00
ed
7a460de3c2 windows db fix 2022-08-15 18:01:28 +02:00
ed
8ea1f4a751 idx multimedia format/container type 2022-08-15 17:56:13 +02:00
ed
1c69ccc6cd v1.3.12 2022-08-13 00:58:49 +02:00
ed
84b5bbd3b6 u2cli: bail from recursive symlinks + verbose errors 2022-08-13 00:28:08 +02:00
ed
9ccd327298 add directory hashing (boots ~3x faster) 2022-08-12 23:17:18 +02:00
ed
11df36f3cf add option to exit after scanning volumes 2022-08-12 21:20:13 +02:00
ed
f62dd0e3cc support fips-cpython and maybe make-sfx on macos 2022-08-12 16:36:20 +02:00
ed
ad18b6e15e stop reindexing empty files on startup 2022-08-12 16:31:36 +02:00
ed
c00b80ca29 v1.3.11 2022-08-10 23:35:21 +02:00
ed
92ed4ba3f8 parallelize python hashing too 2022-08-10 23:12:01 +02:00
ed
7de9775dd9 lol android 2022-08-10 20:35:12 +02:00
ed
5ce9060e5c up2k.js: do hashing in web-workers 2022-08-10 01:09:54 +02:00
ed
f727d5cb5a new cloudflare memes, thx nh 2022-08-09 09:00:22 +02:00
ed
4735fb1ebb u2cli: better msg on bad tls certs 2022-08-09 00:11:34 +02:00
ed
c7d05cc13d up2k-hook-ytid: log discovered IDs + support audio rips 2022-08-05 19:26:24 +02:00
ed
51c152ff4a indicate sqlite thread-safety + some cleanup 2022-08-05 01:20:16 +02:00
ed
eeed2a840c v1.3.10 2022-08-04 01:40:14 +02:00
ed
4aaa111925 v1.3.9 2022-08-04 00:39:37 +02:00
ed
e31248f018 include version info on startup and in crash dumps 2022-08-04 00:11:52 +02:00
ed
8b4cf022f2 bbox: tweak end-of-gallery animation 2022-08-03 22:56:51 +02:00
ed
4e7455268a tag-scanner perf 2022-08-03 22:33:20 +02:00
ed
680f8ae814 add xdev/xvol indexing guards 2022-08-03 22:20:28 +02:00
ed
90555a4cea clean-shutdown while hashing huge files 2022-08-03 21:06:10 +02:00
ed
56a62db591 force-exit by hammering ctrl-c 2022-08-03 20:58:23 +02:00
ed
cf51997680 fix make-sfx.sh on windows/msys2 2022-08-03 20:01:54 +02:00
ed
f05cc18d61 add missing polyfill 2022-08-03 19:42:42 +02:00
ed
5384c2e0f5 reentrant cleanup 2022-08-02 20:56:05 +02:00
ed
9bfbf80a0e ui: fix navpane covering files on horizontal scroll 2022-08-02 20:48:26 +02:00
ed
f874d7754f ui: toggle sorting folders before files (default-on) 2022-08-02 20:47:17 +02:00
ed
a669f79480 windows upload perf (fat32, smb) 2022-08-02 20:39:51 +02:00
ed
1c3894743a fix filekeys inside symlinked volumes 2022-08-02 20:26:51 +02:00
ed
75cdf17df4 cache sparsefile-support on windows too 2022-08-02 06:58:25 +02:00
ed
de7dd1e60a more visible upload errors on mobile 2022-08-02 06:17:13 +02:00
ed
0ee574a718 forget uploads that failed to initialize 2022-08-02 06:15:18 +02:00
ed
faac894706 oh 2022-07-29 00:13:18 +02:00
ed
dac2fad48e v1.3.8 2022-07-27 16:07:26 +02:00
ed
77f624b01e improve shumantime + use it everywhere 2022-07-27 15:07:04 +02:00
ed
e24ffebfc8 indicate write-activity on splashpage 2022-07-27 14:53:15 +02:00
ed
70d07d1609 perf 2022-07-27 14:01:30 +02:00
ed
bfb3303d87 include client total ETA in upload logs 2022-07-27 12:07:51 +02:00
ed
660705a436 defer volume reindexing on db activity 2022-07-27 11:48:47 +02:00
ed
74a3f97671 cleanup + bump deps 2022-07-27 00:15:49 +02:00
ed
b3e35bb494 async lsof w/ timeout 2022-07-26 22:38:13 +02:00
ed
76adac7c72 up2k-hook-ytid: add mp4/webm/mkv metadata scanner 2022-07-26 22:09:18 +02:00
ed
5dc75ebb67 async e2ts / e2v + forget deleted shadowed 2022-07-26 12:47:40 +02:00
ed
d686ce12b6 lsof db on stuck transaction 2022-07-25 02:07:59 +02:00
ed
d3c40a423e mutagen: support nullduration tags 2022-07-25 01:21:34 +02:00
ed
2fb1e6dab8 mute exception on zip abort 2022-07-25 01:20:38 +02:00
ed
10430b347f fix dumb prisonparty bug 2022-07-22 20:49:35 +02:00
ed
e0e3f6ac3e up2k-hook-ytid: add override 2022-07-22 10:47:10 +02:00
ed
c694cbffdc a11y: improve skip-to-files 2022-07-20 23:44:57 +02:00
ed
bdd0e5d771 a11y: enter = onclick 2022-07-20 23:32:02 +02:00
ed
aa98e427f0 audio-eq: add crossfeed 2022-07-20 01:54:59 +02:00
ed
daa6f4c94c add video hotkeys for digit-seeking 2022-07-17 23:45:02 +02:00
ed
4a76663fb2 ensure free disk space 2022-07-17 22:33:08 +02:00
ed
cebda5028a v1.3.7 2022-07-16 20:48:23 +02:00
ed
3fa377a580 sqlite diag 2022-07-16 20:43:26 +02:00
ed
a11c1005a8 v1.3.6 2022-07-16 03:58:58 +02:00
ed
4a6aea9328 hopefully got this right 2022-07-16 02:24:53 +02:00
ed
4ca041e93e improve autopotato accuracy 2022-07-16 02:23:50 +02:00
ed
52a866a405 batch progress writes 2022-07-16 02:12:56 +02:00
ed
8b6bd0e6ac rescue some exceptions from the promise maelstroms 2022-07-15 23:42:37 +02:00
ed
780fc4639a bbox: chrome doesnt override video onclick 2022-07-15 22:36:35 +02:00
ed
3692fc9d83 bbox: doubletap pic for fullscreen 2022-07-15 22:29:44 +02:00
ed
c2a0b1b4c6 autopotato 2022-07-15 02:39:32 +02:00
ed
21bbdb5419 fix audio-eq on recent chromes 2022-07-15 02:07:48 +02:00
ed
aa1c08962c golf 2022-07-15 02:07:13 +02:00
ed
8a5d0399dd sfx: dont hang supervisors 2022-07-15 02:04:00 +02:00
ed
f2cd0b0c4a sfx: avoid name collisions across reboots 2022-07-15 02:03:41 +02:00
ed
c2b66bbe73 add potato mode 2022-07-14 02:33:35 +02:00
ed
48b957f1d5 add -e2v (file integrity checker) 2022-07-13 00:48:39 +02:00
ed
3683984c8d abort volume indexing on ^C 2022-07-12 21:46:07 +02:00
ed
a3431512d8 push queue/status info to server 2022-07-12 21:22:02 +02:00
ed
d832b787e7 upload smallest-file-first by default 2022-07-12 20:48:38 +02:00
ed
6f75b02723 misc 2022-07-12 03:16:30 +02:00
ed
b8241710bd md-editor fixes 2022-07-12 02:53:33 +02:00
ed
d638404b6a better runahead strategy for 100 GiB+ files 2022-07-12 02:30:49 +02:00
ed
9362ca3ed9 py2 fixes 2022-07-11 23:53:18 +02:00
ed
d1a03c6d17 zerobyte semantics 2022-07-11 23:17:33 +02:00
ed
c6c31702c2 cheaper file deletion 2022-07-11 01:50:18 +02:00
ed
bd2d88c96e add another up2k-hook example 2022-07-11 00:52:59 +02:00
ed
76b1857e4e add support for up2k hooks 2022-07-09 14:02:35 +02:00
ed
095bd17d10 mtp/vidchk: grab some frames at the start too 2022-07-09 13:10:00 +02:00
ed
204bfac3fa mtp/vidchk: write ffprobe metadata to file 2022-07-09 04:33:19 +02:00
ed
ac49b0ca93 mtp: add rclone uploader 2022-07-08 23:47:27 +02:00
ed
c5b04f6fef mtp daisychaining 2022-07-08 22:29:05 +02:00
ed
5c58fda46d only clean thumbs if there are thumbs to clean 2022-07-08 21:13:10 +02:00
ed
062730c70c cleanup 2022-07-06 11:12:36 +02:00
ed
cade1990ce v1.3.5 2022-07-06 02:29:11 +02:00
ed
59b6e61816 build fstab from relabels when mtab is unreadable 2022-07-06 02:28:34 +02:00
ed
daff7ff158 v1.3.4 2022-07-06 00:12:10 +02:00
ed
0862860961 misc cleanup 2022-07-06 00:00:56 +02:00
ed
1cb24045a0 dont thumb empty files 2022-07-05 23:45:47 +02:00
ed
622358b172 flag to control mtp timeout kill behavior 2022-07-05 23:38:49 +02:00
ed
7998884a9d adopt the osd hider 2022-07-05 23:36:44 +02:00
ed
51ddecd101 improve readme 2022-07-05 23:27:48 +02:00
ed
7a35ab1d1e bbox: video seek / loop url params 2022-07-05 20:37:05 +02:00
ed
48564ba52a bbox: add A-B video loop 2022-07-05 19:53:43 +02:00
ed
49efffd740 bbox: tap left/right side of image for prev/next 2022-07-05 19:33:09 +02:00
ed
d6ac224c8f bbox: tap to show/hide buttons 2022-07-05 19:18:21 +02:00
ed
a772b8c3f2 bbox: add fullscreen for images too 2022-07-05 19:06:02 +02:00
ed
b580953dcd bbox: fix crash on swipe during close 2022-07-05 18:49:52 +02:00
ed
d86653c763 ux 2022-07-05 00:13:08 +02:00
ed
dded4fca76 option to specify favicon + default-enable it 2022-07-05 00:06:22 +02:00
ed
36365ffa6b explain the donut 2022-07-04 22:17:37 +02:00
ed
0f9aeeaa27 bump codemirror to 5.65.6 2022-07-04 22:15:52 +02:00
ed
d8ebcd0ef7 lol dpi 2022-07-04 22:13:28 +02:00
ed
6e445487b1 satisfy cloudflare DDoS protection 2022-07-03 16:04:28 +02:00
ed
6605e461c7 improve mtp section 2022-07-03 14:23:56 +02:00
ed
40ce4e2275 cleanup 2022-07-03 13:55:48 +02:00
ed
8fef9e363e recursive kill mtp on timeout 2022-07-03 04:57:15 +02:00
ed
4792c2770d fix a spin 2022-07-03 02:39:15 +02:00
ed
87bb49da36 new mtp: video integrity checker 2022-07-03 01:50:38 +02:00
ed
1c0071d9ce perf 2022-07-03 01:40:30 +02:00
ed
efded35c2e ffmpeg saying the fps is 1/0 yeah okay 2022-07-02 00:39:46 +02:00
ed
1d74240b9a ux: hide uploads table until something happens 2022-07-01 09:16:23 +02:00
ed
098184ff7b add write-only up2k ui simplifier 2022-07-01 00:55:36 +02:00
ed
4083533916 vt100 listing: reset color at eof 2022-06-29 22:41:51 +02:00
ed
feb1acd43a v1.3.3 2022-06-27 22:57:05 +02:00
ed
a9591db734 cleanup 2022-06-27 22:56:29 +02:00
ed
9ebf148cbe support android9 sdcardfs on sdcard 2022-06-27 22:15:35 +02:00
ed
a473e5e19a always include custom css/js 2022-06-27 17:24:30 +02:00
ed
5d3034c231 detect sparse support from st_blocks 2022-06-23 18:23:42 +02:00
ed
c3a895af64 android sdcardfs can be fat32 2022-06-23 16:27:30 +02:00
ed
cea5aecbf2 v1.3.2 2022-06-20 01:31:29 +02:00
ed
0e61e70670 audioplayer continues to next folder by default 2022-06-20 00:20:13 +02:00
ed
1e333c0939 fix doc traversal 2022-06-19 23:32:36 +02:00
ed
917b6ec03c naming 2022-06-19 22:58:20 +02:00
ed
fe67c52ead configurable list of sparse-supporting filesystems +
close nonsparse files after each write to force flush
2022-06-19 22:38:52 +02:00
ed
909c7bee3e ignore md plugin errors 2022-06-19 20:28:45 +02:00
ed
27ca54d138 md: ol appeared as ul 2022-06-19 19:05:41 +02:00
ed
2147c3a646 run markdown plugins in directory listings 2022-06-19 18:17:22 +02:00
ed
a99120116f ux: breadcrumb ctrl-click 2022-06-19 17:51:03 +02:00
ed
802efeaff2 dont let tags imply subdirectories when renaming 2022-06-19 16:06:39 +02:00
ed
9ad3af1ef6 misc tweaks 2022-06-19 16:05:48 +02:00
ed
715727b811 add changelog 2022-06-17 15:33:57 +02:00
ed
c6eaa7b836 aight good to know 2022-06-17 00:37:56 +02:00
ed
c2fceea2a5 v1.3.1 2022-06-16 21:56:12 +02:00
ed
190e11f7ea update deps + misc 2022-06-16 21:43:40 +02:00
ed
ad7413a5ff add .PARTIAL suffix to bup uploads too +
aggressive limits checking
2022-06-16 21:00:41 +02:00
ed
903b9e627a ux snappiness + keepalive on http-1.0 2022-06-16 20:33:09 +02:00
ed
c5c1e96cf8 ux: button to reset hidden columns 2022-06-16 19:06:28 +02:00
ed
62fbb04c9d allow moving files between filesystems 2022-06-16 18:46:50 +02:00
ed
728dc62d0b optimize nonsparse uploads (fat32, exfat, hpfs) 2022-06-16 17:51:42 +02:00
ed
2dfe1b1c6b add themes: hacker, hi-con 2022-06-16 12:21:21 +02:00
ed
35d4a1a6af ux: delay loading animation + focus outlines + explain ng 2022-06-16 11:02:05 +02:00
ed
eb3fa5aa6b add safety profiles + improve helptext + speed 2022-06-16 10:21:44 +02:00
ed
438384425a add types, isort, errorhandling 2022-06-16 01:07:15 +02:00
ed
0b6f102436 fix multiprocessing ftpd 2022-06-12 16:37:56 +02:00
ed
c9b7ec72d8 add hotkey Y to download current song / vid / pic 2022-06-09 17:23:11 +02:00
ed
256c7f1789 add option to see errors from mtp parsers 2022-06-09 14:46:35 +02:00
ed
4e5a323c62 more cleanup 2022-06-08 01:05:35 +02:00
ed
f4a3bbd237 fix ansify prepending bracket to all logfiles 2022-06-07 23:45:54 +02:00
ed
fe73f2d579 cleanup 2022-06-07 23:08:43 +02:00
ed
f79fcc7073 discover local ip under termux 2022-06-07 23:03:16 +02:00
ed
4c4b3790c7 fix read-spin on d/c during json post + errorhandling 2022-06-07 19:02:52 +02:00
ed
bd60b464bb fix misleading log-msg 2022-06-07 14:12:55 +02:00
ed
6bce852765 ux: treepar positioning 2022-06-06 22:05:13 +02:00
ed
3b19a5a59d improve a11y jumpers 2022-05-25 20:31:12 +02:00
ed
f024583011 add a11y jumpers 2022-05-24 09:09:54 +02:00
88 changed files with 10577 additions and 2856 deletions

13
.gitignore vendored
View File

@@ -5,13 +5,16 @@ __pycache__/
MANIFEST.in MANIFEST.in
MANIFEST MANIFEST
copyparty.egg-info/ copyparty.egg-info/
buildenv/
build/
dist/
sfx/
py2/
.venv/ .venv/
/buildenv/
/build/
/dist/
/py2/
/sfx*
/unt/
/log/
# ide # ide
*.sublime-workspace *.sublime-workspace

25
.vscode/settings.json vendored
View File

@@ -23,7 +23,6 @@
"terminal.ansiBrightWhite": "#ffffff", "terminal.ansiBrightWhite": "#ffffff",
}, },
"python.testing.pytestEnabled": false, "python.testing.pytestEnabled": false,
"python.testing.nosetestsEnabled": false,
"python.testing.unittestEnabled": true, "python.testing.unittestEnabled": true,
"python.testing.unittestArgs": [ "python.testing.unittestArgs": [
"-v", "-v",
@@ -35,18 +34,40 @@
"python.linting.pylintEnabled": true, "python.linting.pylintEnabled": true,
"python.linting.flake8Enabled": true, "python.linting.flake8Enabled": true,
"python.linting.banditEnabled": true, "python.linting.banditEnabled": true,
"python.linting.mypyEnabled": true,
"python.linting.mypyArgs": [
"--ignore-missing-imports",
"--follow-imports=silent",
"--show-column-numbers",
"--strict"
],
"python.linting.flake8Args": [ "python.linting.flake8Args": [
"--max-line-length=120", "--max-line-length=120",
"--ignore=E722,F405,E203,W503,W293,E402", "--ignore=E722,F405,E203,W503,W293,E402,E501,E128",
], ],
"python.linting.banditArgs": [ "python.linting.banditArgs": [
"--ignore=B104" "--ignore=B104"
], ],
"python.linting.pylintArgs": [
"--disable=missing-module-docstring",
"--disable=missing-class-docstring",
"--disable=missing-function-docstring",
"--disable=wrong-import-position",
"--disable=raise-missing-from",
"--disable=bare-except",
"--disable=invalid-name",
"--disable=line-too-long",
"--disable=consider-using-f-string"
],
// python3 -m isort --py=27 --profile=black copyparty/
"python.formatting.provider": "black", "python.formatting.provider": "black",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"[html]": { "[html]": {
"editor.formatOnSave": false, "editor.formatOnSave": false,
}, },
"[css]": {
"editor.formatOnSave": false,
},
"files.associations": { "files.associations": {
"*.makefile": "makefile" "*.makefile": "makefile"
}, },

204
README.md
View File

@@ -9,11 +9,12 @@
turn your phone or raspi into a portable file server with resumable uploads/downloads using *any* web browser turn your phone or raspi into a portable file server with resumable uploads/downloads using *any* web browser
* server only needs `py2.7` or `py3.3+`, all dependencies optional * server only needs `py2.7` or `py3.3+`, all dependencies optional
* browse/upload with IE4 / netscape4.0 on win3.11 (heh) * browse/upload with [IE4](#browser-support) / netscape4.0 on win3.11 (heh)
* *resumable* uploads need `firefox 34+` / `chrome 41+` / `safari 7+` for full speed * *resumable* uploads need `firefox 34+` / `chrome 41+` / `safari 7+`
* code standard: `black`
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer) // [ie4](#browser-support) try the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running from a basement in finland
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer)
## get the app ## get the app
@@ -43,7 +44,7 @@ turn your phone or raspi into a portable file server with resumable uploads/down
* [tabs](#tabs) - the main tabs in the ui * [tabs](#tabs) - the main tabs in the ui
* [hotkeys](#hotkeys) - the browser has the following hotkeys * [hotkeys](#hotkeys) - the browser has the following hotkeys
* [navpane](#navpane) - switching between breadcrumbs or navpane * [navpane](#navpane) - switching between breadcrumbs or navpane
* [thumbnails](#thumbnails) - press `g` to toggle grid-view instead of the file listing * [thumbnails](#thumbnails) - press `g` or `田` to toggle grid-view instead of the file listing
* [zip downloads](#zip-downloads) - download folders (or file selections) as `zip` or `tar` files * [zip downloads](#zip-downloads) - download folders (or file selections) as `zip` or `tar` files
* [uploading](#uploading) - drag files/folders into the web-browser to upload * [uploading](#uploading) - drag files/folders into the web-browser to upload
* [file-search](#file-search) - dropping files into the browser also lets you see if they exist on the server * [file-search](#file-search) - dropping files into the browser also lets you see if they exist on the server
@@ -55,8 +56,11 @@ turn your phone or raspi into a portable file server with resumable uploads/down
* [searching](#searching) - search by size, date, path/name, mp3-tags, ... * [searching](#searching) - search by size, date, path/name, mp3-tags, ...
* [server config](#server-config) - using arguments or config files, or a mix of both * [server config](#server-config) - using arguments or config files, or a mix of both
* [ftp-server](#ftp-server) - an FTP server can be started using `--ftp 3921` * [ftp-server](#ftp-server) - an FTP server can be started using `--ftp 3921`
* [file indexing](#file-indexing) * [file indexing](#file-indexing) - enables dedup and music search ++
* [upload rules](#upload-rules) - set upload rules using volume flags * [exclude-patterns](#exclude-patterns) - to save some time
* [filesystem guards](#filesystem-guards) - avoid traversing into other filesystems
* [periodic rescan](#periodic-rescan) - filesystem monitoring
* [upload rules](#upload-rules) - set upload rules using volflags
* [compress uploads](#compress-uploads) - files can be autocompressed on upload * [compress uploads](#compress-uploads) - files can be autocompressed on upload
* [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else * [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else
* [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload * [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload
@@ -101,7 +105,7 @@ turn your phone or raspi into a portable file server with resumable uploads/down
download **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** and you're all set! download **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** and you're all set!
running the sfx without arguments (for example doubleclicking it on Windows) will give everyone read/write access to the current folder; see `-h` for help if you want [accounts and volumes](#accounts-and-volumes) etc running the sfx without arguments (for example doubleclicking it on Windows) will give everyone read/write access to the current folder; you may want [accounts and volumes](#accounts-and-volumes)
some recommended options: some recommended options:
* `-e2dsa` enables general [file indexing](#file-indexing) * `-e2dsa` enables general [file indexing](#file-indexing)
@@ -109,7 +113,7 @@ some recommended options:
* `-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` * `-v /mnt/music:/music:r:rw,foo -a foo:bar` shares `/mnt/music` as `/music`, `r`eadable by anyone, and read-write for user `foo`, password `bar`
* replace `:r:rw,foo` with `:r,foo` to only make the folder readable by `foo` and nobody else * replace `:r:rw,foo` with `:r,foo` to only make the folder readable by `foo` and nobody else
* see [accounts and volumes](#accounts-and-volumes) for the syntax and other permissions (`r`ead, `w`rite, `m`ove, `d`elete, `g`et) * see [accounts and volumes](#accounts-and-volumes) for the syntax and other permissions (`r`ead, `w`rite, `m`ove, `d`elete, `g`et)
* `--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 (see `--help-ls`)
### on servers ### on servers
@@ -167,7 +171,7 @@ feature summary
* download * download
* ☑ single files in browser * ☑ single files in browser
* ☑ [folders as zip / tar files](#zip-downloads) * ☑ [folders as zip / tar files](#zip-downloads)
* ☑ FUSE client (read-only) *[FUSE client](https://github.com/9001/copyparty/tree/hovudstraum/bin#copyparty-fusepy) (read-only)
* browser * browser
* ☑ [navpane](#navpane) (directory tree sidebar) * ☑ [navpane](#navpane) (directory tree sidebar)
* ☑ file manager (cut/paste, delete, [batch-rename](#batch-rename)) * ☑ file manager (cut/paste, delete, [batch-rename](#batch-rename))
@@ -203,6 +207,7 @@ project goals / philosophy
* inverse linux philosophy -- do all the things, and do an *okay* job * inverse linux philosophy -- do all the things, and do an *okay* job
* quick drop-in service to get a lot of features in a pinch * quick drop-in service to get a lot of features in a pinch
* there are probably [better alternatives](https://github.com/awesome-selfhosted/awesome-selfhosted) if you have specific/long-term needs * there are probably [better alternatives](https://github.com/awesome-selfhosted/awesome-selfhosted) if you have specific/long-term needs
* but the resumable multithreaded uploads are p slick ngl
* run anywhere, support everything * run anywhere, support everything
* as many web-browsers and python versions as possible * as many web-browsers and python versions as possible
* every browser should at least be able to browse, download, upload files * every browser should at least be able to browse, download, upload files
@@ -241,15 +246,21 @@ some improvement ideas
## general bugs ## general bugs
* Windows: if the up2k db is on a samba-share or network disk, you'll get unpredictable behavior if the share is disconnected for a bit * Windows: if the `up2k.db` (filesystem index) is on a samba-share or network disk, you'll get unpredictable behavior if the share is disconnected for a bit
* use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db on a local disk instead * use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db on a local disk instead
* all volumes must exist / be available on startup; up2k (mtp especially) gets funky otherwise * all volumes must exist / be available on startup; up2k (mtp especially) gets funky otherwise
* [the database can get stuck](https://github.com/9001/copyparty/issues/10)
* has only happened once but that is once too many
* luckily not dangerous for file integrity and doesn't really stop uploads or anything like that
* but would really appreciate some logs if anyone ever runs into it again
* probably more, pls let me know * probably more, pls let me know
## not my bugs ## not my bugs
* [Chrome issue 1317069](https://bugs.chromium.org/p/chromium/issues/detail?id=1317069) -- if you try to upload a folder which contains symlinks by dragging it into the browser, the symlinked files will not get uploaded * [Chrome issue 1317069](https://bugs.chromium.org/p/chromium/issues/detail?id=1317069) -- if you try to upload a folder which contains symlinks by dragging it into the browser, the symlinked files will not get uploaded
* [Chrome issue 1352210](https://bugs.chromium.org/p/chromium/issues/detail?id=1352210) -- plaintext http may be faster at filehashing than https (but also extremely CPU-intensive)
* iPhones: the volume control doesn't work because [apple doesn't want it to](https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html#//apple_ref/doc/uid/TP40009523-CH5-SW11) * iPhones: the volume control doesn't work because [apple doesn't want it to](https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html#//apple_ref/doc/uid/TP40009523-CH5-SW11)
* *future workaround:* enable the equalizer, make it all-zero, and set a negative boost to reduce the volume * *future workaround:* enable the equalizer, make it all-zero, and set a negative boost to reduce the volume
* "future" because `AudioContext` is broken in the current iOS version (15.1), maybe one day... * "future" because `AudioContext` is broken in the current iOS version (15.1), maybe one day...
@@ -273,7 +284,7 @@ some improvement ideas
* you can also do this with linux filesystem permissions; `chmod 111 music` will make it possible to access files and folders inside the `music` folder but not list the immediate contents -- also works with other software, not just copyparty * you can also do this with linux filesystem permissions; `chmod 111 music` will make it possible to access files and folders inside the `music` folder but not list the immediate contents -- also works with other software, not just copyparty
* can I make copyparty download a file to my server if I give it a URL? * can I make copyparty download a file to my server if I give it a URL?
* not officially, but there is a [terrible hack](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/wget.py) which makes it possible * not really, but there is a [terrible hack](https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/wget.py) which makes it possible
# accounts and volumes # accounts and volumes
@@ -281,6 +292,8 @@ some improvement ideas
per-folder, per-user permissions - if your setup is getting complex, consider making a [config file](./docs/example.conf) instead of using arguments per-folder, per-user permissions - if your setup is getting complex, consider making a [config file](./docs/example.conf) instead of using arguments
* much easier to manage, and you can modify the config at runtime with `systemctl reload copyparty` or more conveniently using the `[reload cfg]` button in the control-panel (if logged in as admin) * much easier to manage, and you can modify the config at runtime with `systemctl reload copyparty` or more conveniently using the `[reload cfg]` button in the control-panel (if logged in as admin)
a quick summary can be seen using `--help-accounts`
configuring accounts/volumes with arguments: configuring accounts/volumes with arguments:
* `-a usr:pwd` adds account `usr` with password `pwd` * `-a usr:pwd` adds account `usr` with password `pwd`
* `-v .::r` adds current-folder `.` as the webroot, `r`eadable by anyone * `-v .::r` adds current-folder `.` as the webroot, `r`eadable by anyone
@@ -305,7 +318,7 @@ examples:
* `u1` can open the `inc` folder, but cannot see the contents, only upload new files to it * `u1` can open the `inc` folder, but cannot see the contents, only upload new files to it
* `u2` can browse it and move files *from* `/inc` into any folder where `u2` has write-access * `u2` can browse it and move files *from* `/inc` into any folder where `u2` has write-access
* make folder `/mnt/ss` available at `/i`, read-write for u1, get-only for everyone else, and enable accesskeys: `-v /mnt/ss:i:rw,u1:g:c,fk=4` * make folder `/mnt/ss` available at `/i`, read-write for u1, get-only for everyone else, and enable accesskeys: `-v /mnt/ss:i:rw,u1:g:c,fk=4`
* `c,fk=4` sets the `fk` volume-flag to 4, meaning each file gets a 4-character accesskey * `c,fk=4` sets the `fk` volflag to 4, meaning each file gets a 4-character accesskey
* `u1` can upload files, browse the folder, and see the generated accesskeys * `u1` can upload files, browse the folder, and see the generated accesskeys
* other users cannot browse the folder, but can access the files if they have the full file URL with the accesskey * other users cannot browse the folder, but can access the files if they have the full file URL with the accesskey
@@ -337,7 +350,7 @@ the browser has the following hotkeys (always qwerty)
* `I/K` prev/next folder * `I/K` prev/next folder
* `M` parent folder (or unexpand current) * `M` parent folder (or unexpand current)
* `V` toggle folders / textfiles in the navpane * `V` toggle folders / textfiles in the navpane
* `G` toggle list / [grid view](#thumbnails) * `G` toggle list / [grid view](#thumbnails) -- same as `田` bottom-right
* `T` toggle thumbnails / icons * `T` toggle thumbnails / icons
* `ESC` close various things * `ESC` close various things
* `ctrl-X` cut selected files/folders * `ctrl-X` cut selected files/folders
@@ -358,19 +371,24 @@ the browser has the following hotkeys (always qwerty)
* `U/O` skip 10sec back/forward * `U/O` skip 10sec back/forward
* `0..9` jump to 0%..90% * `0..9` jump to 0%..90%
* `P` play/pause (also starts playing the folder) * `P` play/pause (also starts playing the folder)
* `Y` download file
* when viewing images / playing videos: * when viewing images / playing videos:
* `J/L, Left/Right` prev/next file * `J/L, Left/Right` prev/next file
* `Home/End` first/last file * `Home/End` first/last file
* `F` toggle fullscreen
* `S` toggle selection * `S` toggle selection
* `R` rotate clockwise (shift=ccw) * `R` rotate clockwise (shift=ccw)
* `Y` download file
* `Esc` close viewer * `Esc` close viewer
* videos: * videos:
* `U/O` skip 10sec back/forward * `U/O` skip 10sec back/forward
* `0..9` jump to 0%..90%
* `P/K/Space` play/pause * `P/K/Space` play/pause
* `F` fullscreen
* `C` continue playing next video
* `V` loop
* `M` mute * `M` mute
* `C` continue playing next video
* `V` loop entire file
* `[` loop range (start)
* `]` loop range (end)
* when the navpane is open: * when the navpane is open:
* `A/D` adjust tree width * `A/D` adjust tree width
* in the [grid view](#thumbnails): * in the [grid view](#thumbnails):
@@ -402,7 +420,7 @@ click the `🌲` or pressing the `B` hotkey to toggle between breadcrumbs path (
## thumbnails ## thumbnails
press `g` to toggle grid-view instead of the file listing, and `t` toggles icons / thumbnails press `g` or `田` to toggle grid-view instead of the file listing and `t` toggles icons / thumbnails
![copyparty-thumbs-fs8](https://user-images.githubusercontent.com/241032/129636211-abd20fa2-a953-4366-9423-1c88ebb96ba9.png) ![copyparty-thumbs-fs8](https://user-images.githubusercontent.com/241032/129636211-abd20fa2-a953-4366-9423-1c88ebb96ba9.png)
@@ -444,13 +462,13 @@ you can also zip a selection of files or folders by clicking them in the browser
## uploading ## uploading
drag files/folders into the web-browser to upload drag files/folders into the web-browser to upload (or use the [command-line uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#up2kpy))
this initiates an upload using `up2k`; there are two uploaders available: this initiates an upload using `up2k`; there are two uploaders available:
* `[🎈] bup`, the basic uploader, supports almost every browser since netscape 4.0 * `[🎈] bup`, the basic uploader, supports almost every browser since netscape 4.0
* `[🚀] up2k`, the fancy one * `[🚀] up2k`, the good / fancy one
you can also undo/delete uploads by using `[🧯]` [unpost](#unpost) NB: you can undo/delete your own uploads with `[🧯]` [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)
@@ -462,7 +480,7 @@ up2k has several advantages:
* much higher speeds than ftp/scp/tarpipe on some internet connections (mainly american ones) thanks to parallel connections * 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, or watch a [demo video](https://a.ocv.me/pub/demo/pics-vids/#gf-0f6f5c0d)
![copyparty-upload-fs8](https://user-images.githubusercontent.com/241032/129635371-48fc54ca-fa91-48e3-9b1d-ba413e4b68cb.png) ![copyparty-upload-fs8](https://user-images.githubusercontent.com/241032/129635371-48fc54ca-fa91-48e3-9b1d-ba413e4b68cb.png)
@@ -473,8 +491,8 @@ see [up2k](#up2k) for details on how it works
the up2k UI is the epitome of polished inutitive experiences: the up2k UI is the epitome of polished inutitive experiences:
* "parallel uploads" specifies how many chunks to upload at the same time * "parallel uploads" specifies how many chunks to upload at the same time
* `[🏃]` analysis of other files should continue while one is uploading * `[🏃]` analysis of other files should continue while one is uploading
* `[🥔]` shows a simpler UI for faster uploads from slow devices
* `[💭]` ask for confirmation before files are added to the queue * `[💭]` ask for confirmation before files are added to the queue
* `[💤]` sync uploading between other copyparty browser-tabs so only one is active
* `[🔎]` switch between upload and [file-search](#file-search) mode * `[🔎]` switch between upload and [file-search](#file-search) mode
* ignore `[🔎]` if you add files by dragging them into the browser * ignore `[🔎]` if you add files by dragging them into the browser
@@ -486,7 +504,7 @@ 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
note that since up2k has to read each file twice, `[🎈 bup]` can *theoretically* be up to 2x faster in some extreme cases (files bigger than your ram, combined with an internet connection faster than the read-speed of your HDD, or if you're uploading from a cuo2duo) note that since up2k has to read each file twice, `[🎈] bup` can *theoretically* be up to 2x faster in some extreme cases (files bigger than your ram, combined with an internet connection faster than the read-speed of your HDD, or if you're uploading from a cuo2duo)
if you are resuming a massive upload and want to skip hashing the files which already finished, you can enable `turbo` in the `[⚙️] config` tab, but please read the tooltip on that button if you are resuming a massive upload and want to skip hashing the files which already finished, you can enable `turbo` in the `[⚙️] config` tab, but please read the tooltip on that button
@@ -597,7 +615,7 @@ and there are *two* editors
* get a plaintext file listing by adding `?ls=t` to a URL, or a compact colored one with `?ls=v` (for unix terminals) * get a plaintext file listing by adding `?ls=t` to a URL, or a compact colored one with `?ls=v` (for unix terminals)
* 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 * if you are using media hotkeys to switch songs and are getting tired of seeing the OSD popup which Windows doesn't let you disable, consider [./contrib/media-osd-bgone.ps1](contrib/#media-osd-bgoneps1)
* click the bottom-left `π` to open a javascript prompt for debugging * click the bottom-left `π` to open a javascript prompt for debugging
@@ -620,7 +638,9 @@ 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 the argument `-e2ts` to also scan/index tags from music files, which brings us over to: the `raw` field allows for more complex stuff such as `( tags like *nhato* or tags like *taishi* ) and ( not tags like *nhato* or not tags like *taishi* )` which finds all songs by either nhato or taishi, excluding collabs (terrible example, why would you do that)
for the above example to work, add the commandline argument `-e2ts` to also scan/index tags from music files, which brings us over to:
# server config # server config
@@ -645,7 +665,9 @@ an FTP server can be started using `--ftp 3921`, and/or `--ftps` for explicit T
## file indexing ## file indexing
file indexing relies on two database tables, the up2k filetree (`-e2d`) and the metadata tags (`-e2t`), stored in `.hist/up2k.db`. Configuration can be done through arguments, volume flags, or a mix of both. enables dedup and music search ++
file indexing relies on two database tables, the up2k filetree (`-e2d`) and the metadata tags (`-e2t`), stored in `.hist/up2k.db`. Configuration can be done through arguments, volflags, or a mix of both.
through arguments: through arguments:
* `-e2d` enables file indexing on upload * `-e2d` enables file indexing on upload
@@ -654,8 +676,11 @@ through arguments:
* `-e2t` enables metadata indexing on upload * `-e2t` enables metadata indexing on upload
* `-e2ts` also scans for tags in all files that don't have tags yet * `-e2ts` also scans for tags in all files that don't have tags yet
* `-e2tsr` also deletes all existing tags, doing a full reindex * `-e2tsr` also deletes all existing tags, doing a full reindex
* `-e2v` verfies file integrity at startup, comparing hashes from the db
* `-e2vu` patches the database with the new hashes from the filesystem
* `-e2vp` panics and kills copyparty instead
the same arguments can be set as volume flags, in addition to `d2d`, `d2ds`, `d2t`, `d2ts` for disabling: the same arguments can be set as volflags, in addition to `d2d`, `d2ds`, `d2t`, `d2ts`, `d2v` for disabling:
* `-v ~/music::r:c,e2dsa,e2tsr` does a full reindex of everything on startup * `-v ~/music::r:c,e2dsa,e2tsr` does a full reindex of everything on startup
* `-v ~/music::r:c,d2d` disables **all** indexing, even if any `-e2*` are on * `-v ~/music::r:c,d2d` disables **all** indexing, even if any `-e2*` are on
* `-v ~/music::r:c,d2t` disables all `-e2t*` (tags), does not affect `-e2d*` * `-v ~/music::r:c,d2t` disables all `-e2t*` (tags), does not affect `-e2d*`
@@ -667,7 +692,9 @@ note:
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise * `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise
* the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher * the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher
to save some time, you can provide a regex pattern for filepaths to only index by filename/path/size/last-modified (and not the hash of the file contents) by setting `--no-hash \.iso$` or the volume-flag `:c,nohash=\.iso$`, this has the following consequences: ### exclude-patterns
to save some time, you can provide a regex pattern for filepaths to only index by filename/path/size/last-modified (and not the hash of the file contents) by setting `--no-hash \.iso$` or the volflag `:c,nohash=\.iso$`, this has the following consequences:
* initial indexing is way faster, especially when the volume is on a network 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
@@ -676,12 +703,29 @@ similarly, you can fully ignore files/folders using `--no-idx [...]` and `:c,noi
if you set `--no-hash [...]` globally, you can enable hashing for specific volumes using flag `:c,nohash=` if you set `--no-hash [...]` globally, you can enable hashing for specific volumes using flag `:c,nohash=`
### filesystem guards
avoid traversing into other filesystems using `--xdev` / volflag `:c,xdev`, skipping any symlinks or bind-mounts to another HDD for example
and/or you can `--xvol` / `:c,xvol` to ignore all symlinks leaving the volume's top directory, but still allow bind-mounts pointing elsewhere
**NB: only affects the indexer** -- users can still access anything inside a volume, unless shadowed by another volume
### periodic rescan
filesystem monitoring; if copyparty is not the only software doing stuff on your filesystem, you may want to enable periodic rescans to keep the index up to date
argument `--re-maxage 60` will rescan all volumes every 60 sec, same as volflag `:c,scan=60` to specify it per-volume
uploads are disabled while a rescan is happening, so rescans will be delayed by `--db-act` (default 10 sec) when there is write-activity going on (uploads, renames, ...)
## upload rules ## upload rules
set upload rules using volume flags, some examples: set upload rules using volflags, some examples:
* `:c,sz=1k-3m` sets allowed filesize between 1 KiB and 3 MiB inclusive (suffixes: `b`, `k`, `m`, `g`) * `:c,sz=1k-3m` sets allowed filesize between 1 KiB and 3 MiB inclusive (suffixes: `b`, `k`, `m`, `g`)
* `:c,df=4g` block uploads if there would be less than 4 GiB free disk space afterwards
* `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`: * `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`:
* `:c,rotn=1000,2` moves uploads into subfolders, up to 1000 files in each folder before making a new one, two levels deep (must be at least 1) * `:c,rotn=1000,2` moves uploads into subfolders, up to 1000 files in each folder before making a new one, two levels deep (must be at least 1)
* `:c,rotf=%Y/%m/%d/%H` enforces files to be uploaded into a structure of subfolders according to that date format * `:c,rotf=%Y/%m/%d/%H` enforces files to be uploaded into a structure of subfolders according to that date format
@@ -700,16 +744,16 @@ you can also set transaction limits which apply per-IP and per-volume, but these
files can be autocompressed on upload, either on user-request (if config allows) or forced by server-config files can be autocompressed on upload, either on user-request (if config allows) or forced by server-config
* volume flag `gz` allows gz compression * volflag `gz` allows gz compression
* volume flag `xz` allows lzma compression * volflag `xz` allows lzma compression
* volume flag `pk` **forces** compression on all files * volflag `pk` **forces** compression on all files
* url parameter `pk` requests compression with server-default algorithm * url parameter `pk` requests compression with server-default algorithm
* url parameter `gz` or `xz` requests compression with a specific algorithm * url parameter `gz` or `xz` requests compression with a specific algorithm
* url parameter `xz` requests xz compression * url parameter `xz` requests xz compression
things to note, things to note,
* the `gz` and `xz` arguments take a single optional argument, the compression level (range 0 to 9) * 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` * the `pk` volflag 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 * default compression is gzip level 9
* all upload methods except up2k are supported * 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 * the files will be indexed after compression, so dupe-detection and file-search will not work as expected
@@ -729,7 +773,7 @@ in-volume (`.hist/up2k.db`, default) or somewhere else
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=` volflag, or a mix of both:
* `--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) * `--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:
@@ -767,27 +811,32 @@ see the beautiful mess of a dictionary in [mtag.py](https://github.com/9001/copy
provide custom parsers to index additional tags, also see [./bin/mtag/README.md](./bin/mtag/README.md) provide custom parsers to index additional tags, also see [./bin/mtag/README.md](./bin/mtag/README.md)
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 copyparty can invoke external programs to collect additional metadata for files using `mtp` (either as argument or volflag), there is a default timeout of 30sec, and only files which contain audio get analyzed by default (see ay/an/ad below)
* `-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: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 * `-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 (default), `an` only do non-audio files, or `ad` do all files (d as in dontcare)
* "audio file" also means videos btw, as long as there is an audio stream
* `-mtp ext=an,~/bin/file-ext.py` runs `~/bin/file-ext.py` to get the `ext` tag only if file is not audio (`an`) * `-mtp ext=an,~/bin/file-ext.py` runs `~/bin/file-ext.py` to get the `ext` tag only if file is not audio (`an`)
* `-mtp arch,built,ver,orig=an,eexe,edll,~/bin/exe.py` runs `~/bin/exe.py` to get properties about windows-binaries only if file is not audio (`an`) and file extension is exe or dll * `-mtp arch,built,ver,orig=an,eexe,edll,~/bin/exe.py` runs `~/bin/exe.py` to get properties about windows-binaries only if file is not audio (`an`) and file extension is exe or dll
you can control how the parser is killed if it times out with option `kt` killing the entire process tree (default), `km` just the main process, or `kn` let it continue running until copyparty is terminated
if something doesn't work, try `--mtag-v` for verbose error messages
## upload events ## upload events
trigger a script/program on each upload like so: trigger a script/program on each upload like so:
``` ```
-v /mnt/inc:inc:w:c,mte=+a1:c,mtp=a1=ad,/usr/bin/notify-send -v /mnt/inc:inc:w:c,mte=+x1:c,mtp=x1=ad,kn,/usr/bin/notify-send
``` ```
so filesystem location `/mnt/inc` shared at `/inc`, write-only for everyone, appending `a1` to the list of tags to index, and using `/usr/bin/notify-send` to "provide" that tag so filesystem location `/mnt/inc` shared at `/inc`, write-only for everyone, appending `x1` to the list of tags to index (`mte`), and using `/usr/bin/notify-send` to "provide" tag `x1` for any filetype (`ad`) with kill-on-timeout disabled (`kn`)
that'll run the command `notify-send` with the path to the uploaded file as the first and only argument (so on linux it'll show a notification on-screen) that'll run the command `notify-send` with the path to the uploaded file as the first and only argument (so on linux it'll show a notification on-screen)
@@ -803,8 +852,8 @@ if this becomes popular maybe there should be a less janky way to do it actually
tell search engines you dont wanna be indexed, either using the good old [robots.txt](https://www.robotstxt.org/robotstxt.html) or through copyparty settings: tell search engines you dont wanna be indexed, either using the good old [robots.txt](https://www.robotstxt.org/robotstxt.html) or through copyparty settings:
* `--no-robots` adds HTTP (`X-Robots-Tag`) and HTML (`<meta>`) headers with `noindex, nofollow` globally * `--no-robots` adds HTTP (`X-Robots-Tag`) and HTML (`<meta>`) headers with `noindex, nofollow` globally
* volume-flag `[...]:c,norobots` does the same thing for that single volume * volflag `[...]:c,norobots` does the same thing for that single volume
* volume-flag `[...]:c,robots` ALLOWS search-engine crawling for that volume, even if `--no-robots` is set globally * volflag `[...]:c,robots` ALLOWS search-engine crawling for that volume, even if `--no-robots` is set globally
also, `--force-js` disables the plain HTML folder listing, making things harder to parse for search engines also, `--force-js` disables the plain HTML folder listing, making things harder to parse for search engines
@@ -834,8 +883,17 @@ see the top of [./copyparty/web/browser.css](./copyparty/web/browser.css) where
## complete examples ## complete examples
* read-only music server with bpm and key scanning * read-only music server
`python copyparty-sfx.py -v /mnt/nas/music:/music:r -e2dsa -e2ts -mtp .bpm=f,audio-bpm.py -mtp key=f,audio-key.py` `python copyparty-sfx.py -v /mnt/nas/music:/music:r -e2dsa -e2ts --no-robots --force-js --theme 2`
* ...with bpm and key scanning
`-mtp .bpm=f,audio-bpm.py -mtp key=f,audio-key.py`
* ...with a read-write folder for `kevin` whose password is `okgo`
`-a kevin:okgo -v /mnt/nas/inc:/inc:rw,kevin`
* ...with logging to disk
`-lo log/cpp-%Y-%m%d-%H%M%S.txt.xz`
# browser support # browser support
@@ -882,6 +940,7 @@ quick summary of more eccentric web-browsers trying to view a directory index:
| **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 | | **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, upload with `?b=u`, auth with `&pw=wark` | | **ie4** and **netscape** 4.0 | can browse, upload with `?b=u`, auth with `&pw=wark` |
| **ncsa mosaic** 2.7 | does not get a pass, [pic1](https://user-images.githubusercontent.com/241032/174189227-ae816026-cf6f-4be5-a26e-1b3b072c1b2f.png) - [pic2](https://user-images.githubusercontent.com/241032/174189225-5651c059-5152-46e9-ac26-7e98e497901b.png) |
| **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl | | **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl |
@@ -940,17 +999,29 @@ quick outline of the up2k protocol, see [uploading](#uploading) for the web-clie
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)
regarding the frequent server log message during uploads;
`6.0M 106M/s 2.77G 102.9M/s n948 thank 4/0/3/1 10042/7198 00:01:09`
* this chunk was `6 MiB`, uploaded at `106 MiB/s`
* on this http connection, `2.77 GiB` transferred, `102.9 MiB/s` average, `948` chunks handled
* client says `4` uploads OK, `0` failed, `3` busy, `1` queued, `10042 MiB` total size, `7198 MiB` and `00:01:09` left
## why chunk-hashes ## why chunk-hashes
a single sha512 would be better, right? a single sha512 would be better, right?
this is due to `crypto.subtle` not providing a streaming api (or the option to seed the sha512 hasher with a starting hash) this is due to `crypto.subtle` [not yet](https://github.com/w3c/webcrypto/issues/73) providing a streaming api (or the option to seed the sha512 hasher with a starting hash)
as a result, the hashes are much less useful than they could have been (search the server by sha512, provide the sha512 in the response http headers, ...) as a result, the hashes are much less useful than they could have been (search the server by sha512, provide the sha512 in the response http headers, ...)
however it allows for hashing multiple chunks in parallel, greatly increasing upload speed from fast storage (NVMe, raid-0 and such)
* both the [browser uploader](#uploading) and the [commandline one](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) does this now, allowing for fast uploading even from plaintext http
hashwasm would solve the streaming issue but reduces hashing speed for sha512 (xxh128 does 6 GiB/s), and it would make old browsers and [iphones](https://bugs.webkit.org/show_bug.cgi?id=228552) unsupported hashwasm would solve the streaming issue but reduces hashing speed for sha512 (xxh128 does 6 GiB/s), and it would make old browsers and [iphones](https://bugs.webkit.org/show_bug.cgi?id=228552) unsupported
* blake2 might be a better choice since xxh is non-cryptographic, but that gets ~15 MiB/s on slower androids
# performance # performance
@@ -980,6 +1051,7 @@ when uploading files,
* if you're cpu-bottlenecked, or the browser is maxing a cpu core: * if you're cpu-bottlenecked, or the browser is maxing a cpu core:
* up to 30% faster uploads if you hide the upload status list by switching away from the `[🚀]` up2k ui-tab (or closing it) * up to 30% faster uploads if you hide the upload status list by switching away from the `[🚀]` up2k ui-tab (or closing it)
* optionally you can switch to the lightweight potato ui by clicking the `[🥔]`
* switching to another browser-tab also works, the favicon will update every 10 seconds in that case * switching to another browser-tab also works, the favicon will update every 10 seconds in that case
* unlikely to be a problem, but can happen when uploding many small files, or your internet is too fast, or PC too slow * unlikely to be a problem, but can happen when uploding many small files, or your internet is too fast, or PC too slow
@@ -988,16 +1060,28 @@ when uploading files,
some notes on hardening some notes on hardening
on public copyparty instances with anonymous upload enabled: * option `-s` is a shortcut to set the following options:
* `--no-thumb` disables thumbnails and audio transcoding to stop copyparty from running `FFmpeg`/`Pillow`/`VIPS` on uploaded files, which is a [good idea](https://www.cvedetails.com/vulnerability-list.php?vendor_id=3611) if anonymous upload is enabled
* `--no-mtag-ff` uses `mutagen` to grab music tags instead of `FFmpeg`, which is safer and faster but less accurate
* `--dotpart` hides uploads from directory listings while they're still incoming
* `--no-robots` and `--force-js` makes life harder for crawlers, see [hiding from google](#hiding-from-google)
* users can upload html/css/js which will evaluate for other visitors in a few ways, * option `-ss` is a shortcut for the above plus:
* unless `--no-readme` is set: by uploading/modifying a file named `readme.md` * `--no-logues` and `--no-readme` disables support for readme's and prologues / epilogues in directory listings, which otherwise lets people upload arbitrary `<script>` tags
* if `move` access is granted AND none of `--no-logues`, `--no-dot-mv`, `--no-dot-ren` is set: by uploading some .html file and renaming it to `.epilogue.html` (uploading it directly is blocked) * `--unpost 0`, `--no-del`, `--no-mv` disables all move/delete support
* `--hardlink` creates hardlinks instead of symlinks when deduplicating uploads, which is less maintenance
* however note if you edit one file it will also affect the other copies
* `--vague-403` returns a "404 not found" instead of "403 forbidden" which is a common enterprise meme
* `--nih` removes the server hostname from directory listings
other misc: * option `-sss` is a shortcut for the above plus:
* `-lo cpp-%Y-%m%d-%H%M%S.txt.xz` enables logging to disk
* `-ls **,*,ln,p,r` does a scan on startup for any dangerous symlinks
other misc notes:
* you can disable directory listings by giving permission `g` instead of `r`, only accepting direct URLs to files * you can disable directory listings by giving permission `g` instead of `r`, only accepting direct URLs to files
* combine this with volume-flag `c,fk` to generate per-file accesskeys; users which have full read-access will then see URLs with `?k=...` appended to the end, and `g` users must provide that URL including the correct key to avoid a 404 * combine this with volflag `c,fk` to generate per-file accesskeys; users which have full read-access will then see URLs with `?k=...` appended to the end, and `g` users must provide that URL including the correct key to avoid a 404
## gotchas ## gotchas
@@ -1190,25 +1274,32 @@ if you want thumbnails, `apt -y install ffmpeg`
ideas for context to include in bug reports ideas for context to include in bug reports
in general, commandline arguments (and config file if any)
if something broke during an upload (replacing FILENAME with a part of the filename that broke): if something broke during an upload (replacing FILENAME with a part of the filename that broke):
``` ```
journalctl -aS '48 hour ago' -u copyparty | grep -C10 FILENAME | tee bug.log journalctl -aS '48 hour ago' -u copyparty | grep -C10 FILENAME | tee bug.log
``` ```
if there's a wall of base64 in the log (thread stacks) then please include that, especially if you run into something freezing up or getting stuck, for example `OperationalError('database is locked')` -- alternatively you can visit `/?stack` to see the stacks live, so http://127.0.0.1:3923/?stack for example
# building # building
## dev env setup ## dev env setup
mostly optional; if you need a working env for vscode or similar you need python 3.9 or newer due to type hints
the rest is 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 pip install jinja2 strip_hints # MANDATORY
pip install mutagen # audio metadata pip install mutagen # audio metadata
pip install pyftpdlib # ftp server
pip install Pillow pyheif-pillow-opener pillow-avif-plugin # thumbnails pip install Pillow pyheif-pillow-opener pillow-avif-plugin # thumbnails
pip install black==21.12b0 bandit pylint flake8 # vscode tooling pip install black==21.12b0 click==8.0.2 bandit pylint flake8 isort mypy # vscode tooling
``` ```
@@ -1239,10 +1330,7 @@ also builds the sfx so skip the sfx section above
in the `scripts` folder: in the `scripts` folder:
* run `make -C deps-docker` to build all dependencies * run `make -C deps-docker` to build all dependencies
* `git tag v1.2.3 && git push origin --tags` * run `./rls.sh 1.2.3` which uploads to pypi + creates github release + sfx
* upload to pypi with `make-pypi-release.(sh|bat)`
* create github release with `make-tgz-release.sh`
* create sfx with `make-sfx.sh`
# todo # todo
@@ -1269,7 +1357,7 @@ roughly sorted by priority
* up2k partials ui * up2k partials ui
* feels like there isn't much point * feels like there isn't much point
* cache sha512 chunks on client * cache sha512 chunks on client
* too dangerous * too dangerous -- overtaken by turbo mode
* comment field * comment field
* nah * nah
* look into android thumbnail cache file format * look into android thumbnail cache file format

View File

@@ -42,7 +42,7 @@ run [`install-deps.sh`](install-deps.sh) to build/install most dependencies requ
* `mtp` modules will not run if a file has existing tags in the db, so clear out the tags with `-e2tsr` the first time you launch with new `mtp` options * `mtp` modules will not run if a file has existing tags in the db, so clear out the tags with `-e2tsr` the first time you launch with new `mtp` options
## usage with volume-flags ## usage with volflags
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:

View File

@@ -17,7 +17,7 @@ except:
""" """
calculates various checksums for uploads, calculates various checksums for uploads,
usage: -mtp crc32,md5,sha1,sha256b=bin/mtag/cksum.py usage: -mtp crc32,md5,sha1,sha256b=ad,bin/mtag/cksum.py
""" """

View File

@@ -43,7 +43,6 @@ PS: this requires e2ts to be functional,
import os import os
import sys import sys
import time
import filecmp import filecmp
import subprocess as sp import subprocess as sp
@@ -90,4 +89,7 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
main() try:
main()
except:
pass

38
bin/mtag/mousepad.py Normal file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python3
import os
import sys
import subprocess as sp
"""
mtp test -- opens a texteditor
usage:
-vsrv/v1:v1:r:c,mte=+x1:c,mtp=x1=ad,p,bin/mtag/mousepad.py
explained:
c,mte: list of tags to index in this volume
c,mtp: add new tag provider
x1: dummy tag to provide
ad: dontcare if audio or not
p: priority 1 (run after initial tag-scan with ffprobe or mutagen)
"""
def main():
env = os.environ.copy()
env["DISPLAY"] = ":0.0"
if False:
# open the uploaded file
fp = sys.argv[-1]
else:
# display stdin contents (`oth_tags`)
fp = "/dev/stdin"
p = sp.Popen(["/usr/bin/mousepad", fp])
p.communicate()
main()

76
bin/mtag/rclone-upload.py Normal file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python
import json
import os
import subprocess as sp
import sys
import time
try:
from copyparty.util import fsenc
except:
def fsenc(p):
return p.encode("utf-8")
_ = r"""
first checks the tag "vidchk" which must be "ok" to continue,
then uploads all files to some cloud storage (RCLONE_REMOTE)
and DELETES THE ORIGINAL FILES if rclone returns 0 ("success")
deps:
rclone
usage:
-mtp x2=t43200,ay,p2,bin/mtag/rclone-upload.py
explained:
t43200: timeout 12h
ay: only process files which contain audio (including video with audio)
p2: set priority 2 (after vidchk's suggested priority of 1),
so the output of vidchk will be passed in here
complete usage example as vflags along with vidchk:
-vsrv/vidchk:vidchk:r:rw,ed:c,e2dsa,e2ts,mtp=vidchk=t600,p,bin/mtag/vidchk.py:c,mtp=rupload=t43200,ay,p2,bin/mtag/rclone-upload.py:c,mte=+vidchk,rupload
setup: see https://rclone.org/drive/
if you wanna use this script standalone / separately from copyparty,
either set CONDITIONAL_UPLOAD False or provide the following stdin:
{"vidchk":"ok"}
"""
RCLONE_REMOTE = "notmybox"
CONDITIONAL_UPLOAD = True
def main():
fp = sys.argv[1]
if CONDITIONAL_UPLOAD:
zb = sys.stdin.buffer.read()
zs = zb.decode("utf-8", "replace")
md = json.loads(zs)
chk = md.get("vidchk", None)
if chk != "ok":
print(f"vidchk={chk}", file=sys.stderr)
sys.exit(1)
dst = f"{RCLONE_REMOTE}:".encode("utf-8")
cmd = [b"rclone", b"copy", b"--", fsenc(fp), dst]
t0 = time.time()
try:
sp.check_call(cmd)
except:
print("rclone failed", file=sys.stderr)
sys.exit(1)
print(f"{time.time() - t0:.1f} sec")
os.unlink(fsenc(fp))
if __name__ == "__main__":
main()

View File

@@ -16,7 +16,7 @@ goes without saying, but this is HELLA DANGEROUS,
GIVES RCE TO ANYONE WHO HAVE UPLOAD PERMISSIONS GIVES RCE TO ANYONE WHO HAVE UPLOAD PERMISSIONS
example copyparty config to use this: example copyparty config to use this:
--urlform save,get -v.::w:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,bin/mtag/very-bad-idea.py --urlform save,get -v.::w:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,bin/mtag/very-bad-idea.py
recommended deps: recommended deps:
apt install xdotool libnotify-bin apt install xdotool libnotify-bin
@@ -63,8 +63,8 @@ set -e
EOF EOF
chmod 755 /usr/local/bin/chromium-browser chmod 755 /usr/local/bin/chromium-browser
# start the server (note: replace `-v.::rw:` with `-v.::r:` to disallow retrieving uploaded stuff) # start the server (note: replace `-v.::rw:` with `-v.::w:` to disallow retrieving uploaded stuff)
cd ~/Downloads; python3 copyparty-sfx.py --urlform save,get -v.::rw:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,very-bad-idea.py cd ~/Downloads; python3 copyparty-sfx.py --urlform save,get -v.::rw:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,very-bad-idea.py
""" """

118
bin/mtag/vidchk.py Executable file
View File

@@ -0,0 +1,118 @@
#!/usr/bin/env python3
import json
import re
import sys
import subprocess as sp
try:
from copyparty.util import fsenc
except:
def fsenc(p):
return p.encode("utf-8")
_ = r"""
inspects video files for errors and such
plus stores a bunch of metadata to filename.ff.json
usage:
-mtp vidchk=t600,ay,p,bin/mtag/vidchk.py
explained:
t600: timeout 10min
ay: only process files which contain audio (including video with audio)
p: set priority 1 (lowest priority after initial ffprobe/mutagen for base tags),
makes copyparty feed base tags into this script as json
if you wanna use this script standalone / separately from copyparty,
provide the video resolution on stdin as json: {"res":"1920x1080"}
"""
FAST = True # parse entire file at container level
# FAST = False # fully decode audio and video streams
# warnings to ignore
harmless = re.compile("^Unsupported codec with id ")
def wfilter(lines):
return [x for x in lines if not harmless.search(x)]
def errchk(so, se, rc):
if rc:
err = (so + se).decode("utf-8", "replace").split("\n", 1)
err = wfilter(err) or err
return f"ERROR {rc}: {err[0]}"
if se:
err = se.decode("utf-8", "replace").split("\n", 1)
err = wfilter(err)
if err:
return f"Warning: {err[0]}"
return None
def main():
fp = sys.argv[1]
zb = sys.stdin.buffer.read()
zs = zb.decode("utf-8", "replace")
md = json.loads(zs)
try:
w, h = [int(x) for x in md["res"].split("x")]
if not w + h:
raise Exception()
except:
return "could not determine resolution"
# grab streams/format metadata + 2 seconds of frames at the start and end
zs = "ffprobe -hide_banner -v warning -of json -show_streams -show_format -show_packets -show_data_hash crc32 -read_intervals %+2,999999%+2"
cmd = zs.encode("ascii").split(b" ") + [fsenc(fp)]
p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE)
so, se = p.communicate()
# spaces to tabs, drops filesize from 69k to 48k
so = b"\n".join(
[
b"\t" * int((len(x) - len(x.lstrip())) / 4) + x.lstrip()
for x in (so or b"").split(b"\n")
]
)
with open(fsenc(f"{fp}.ff.json"), "wb") as f:
f.write(so)
err = errchk(so, se, p.returncode)
if err:
return err
if min(w, h) < 1080:
return "resolution too small"
zs = (
"ffmpeg -y -hide_banner -nostdin -v warning"
+ " -err_detect +crccheck+bitstream+buffer+careful+compliant+aggressive+explode"
+ " -xerror -i"
)
cmd = zs.encode("ascii").split(b" ") + [fsenc(fp)]
if FAST:
zs = "-c copy -f null -"
else:
zs = "-vcodec rawvideo -acodec pcm_s16le -f null -"
cmd += zs.encode("ascii").split(b" ")
p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE)
so, se = p.communicate()
return errchk(so, se, p.returncode)
if __name__ == "__main__":
print(main() or "ok")

View File

@@ -11,13 +11,13 @@ sysdirs=( /bin /lib /lib32 /lib64 /sbin /usr )
help() { cat <<'EOF' help() { cat <<'EOF'
usage: usage:
./prisonparty.sh <ROOTDIR> <UID> <GID> [VOLDIR [VOLDIR...]] -- python3 copyparty-sfx.py [...]" ./prisonparty.sh <ROOTDIR> <UID> <GID> [VOLDIR [VOLDIR...]] -- python3 copyparty-sfx.py [...]
example: example:
./prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt/nas/music -- python3 copyparty-sfx.py -v /mnt/nas/music::rwmd" ./prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt/nas/music -- python3 copyparty-sfx.py -v /mnt/nas/music::rwmd
example for running straight from source (instead of using an sfx): example for running straight from source (instead of using an sfx):
PYTHONPATH=$PWD ./prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt/nas/music -- python3 -um copyparty -v /mnt/nas/music::rwmd" PYTHONPATH=$PWD ./prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt/nas/music -- python3 -um copyparty -v /mnt/nas/music::rwmd
note that if you have python modules installed as --user (such as bpm/key detectors), note that if you have python modules installed as --user (such as bpm/key detectors),
you should add /home/foo/.local as a VOLDIR you should add /home/foo/.local as a VOLDIR

View File

@@ -3,11 +3,11 @@ from __future__ import print_function, unicode_literals
""" """
up2k.py: upload to copyparty up2k.py: upload to copyparty
2021-11-28, v0.13, ed <irc.rizon.net>, MIT-Licensed 2022-08-13, v0.18, ed <irc.rizon.net>, MIT-Licensed
https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py
- dependencies: requests - dependencies: requests
- supports python 2.6, 2.7, and 3.3 through 3.10 - supports python 2.6, 2.7, and 3.3 through 3.11
- almost zero error-handling - almost zero error-handling
- but if something breaks just try again and it'll autoresume - but if something breaks just try again and it'll autoresume
@@ -22,12 +22,30 @@ import atexit
import signal import signal
import base64 import base64
import hashlib import hashlib
import argparse
import platform import platform
import threading import threading
import requests
import datetime import datetime
try:
import argparse
except:
m = "\n ERROR: need 'argparse'; download it here:\n https://github.com/ThomasWaldmann/argparse/raw/master/argparse.py\n"
print(m)
raise
try:
import requests
except:
if sys.version_info > (2, 7):
m = "\n ERROR: need 'requests'; run this:\n python -m pip install --user requests\n"
else:
m = "requests/2.18.4 urllib3/1.23 chardet/3.0.4 certifi/2020.4.5.1 idna/2.7"
m = [" https://pypi.org/project/" + x + "/#files" for x in m.split()]
m = "\n ERROR: need these:\n" + "\n".join(m) + "\n"
print(m)
raise
# from copyparty/__init__.py # from copyparty/__init__.py
PY2 = sys.version_info[0] == 2 PY2 = sys.version_info[0] == 2
@@ -76,15 +94,15 @@ class File(object):
self.up_b = 0 # type: int self.up_b = 0 # type: int
self.up_c = 0 # type: int self.up_c = 0 # type: int
# m = "size({}) lmod({}) top({}) rel({}) abs({}) name({})\n" # t = "size({}) lmod({}) top({}) rel({}) abs({}) name({})\n"
# eprint(m.format(self.size, self.lmod, self.top, self.rel, self.abs, self.name)) # eprint(t.format(self.size, self.lmod, self.top, self.rel, self.abs, self.name))
class FileSlice(object): class FileSlice(object):
"""file-like object providing a fixed window into a file""" """file-like object providing a fixed window into a file"""
def __init__(self, file, cid): def __init__(self, file, cid):
# type: (File, str) -> FileSlice # type: (File, str) -> None
self.car, self.len = file.kchunks[cid] self.car, self.len = file.kchunks[cid]
self.cdr = self.car + self.len self.cdr = self.car + self.len
@@ -125,6 +143,89 @@ class FileSlice(object):
return ret return ret
class MTHash(object):
def __init__(self, cores):
self.f = None
self.sz = 0
self.csz = 0
self.omutex = threading.Lock()
self.imutex = threading.Lock()
self.work_q = Queue()
self.done_q = Queue()
self.thrs = []
for _ in range(cores):
t = threading.Thread(target=self.worker)
t.daemon = True
t.start()
self.thrs.append(t)
def hash(self, f, fsz, chunksz, pcb=None, pcb_opaque=None):
with self.omutex:
self.f = f
self.sz = fsz
self.csz = chunksz
chunks = {}
nchunks = int(math.ceil(fsz / chunksz))
for nch in range(nchunks):
self.work_q.put(nch)
ex = ""
for nch in range(nchunks):
qe = self.done_q.get()
try:
nch, dig, ofs, csz = qe
chunks[nch] = [dig, ofs, csz]
except:
ex = ex or qe
if pcb:
pcb(pcb_opaque, chunksz * nch)
if ex:
raise Exception(ex)
ret = []
for n in range(nchunks):
ret.append(chunks[n])
self.f = None
self.csz = 0
self.sz = 0
return ret
def worker(self):
while True:
ofs = self.work_q.get()
try:
v = self.hash_at(ofs)
except Exception as ex:
v = str(ex)
self.done_q.put(v)
def hash_at(self, nch):
f = self.f
ofs = ofs0 = nch * self.csz
hashobj = hashlib.sha512()
chunk_sz = chunk_rem = min(self.csz, self.sz - ofs)
while chunk_rem > 0:
with self.imutex:
f.seek(ofs)
buf = f.read(min(chunk_rem, 1024 * 1024 * 12))
if not buf:
raise Exception("EOF at " + str(ofs))
hashobj.update(buf)
chunk_rem -= len(buf)
ofs += len(buf)
digest = hashobj.digest()[:33]
digest = base64.urlsafe_b64encode(digest).decode("utf-8")
return nch, digest, ofs0, chunk_sz
_print = print _print = print
@@ -150,13 +251,11 @@ if not VT100:
def termsize(): def termsize():
import os
env = os.environ env = os.environ
def ioctl_GWINSZ(fd): def ioctl_GWINSZ(fd):
try: try:
import fcntl, termios, struct, os import fcntl, termios, struct
cr = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234")) cr = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234"))
except: except:
@@ -217,8 +316,8 @@ class CTermsize(object):
eprint("\033[s\033[r\033[u") eprint("\033[s\033[r\033[u")
else: else:
self.g = 1 + self.h - margin self.g = 1 + self.h - margin
m = "{0}\033[{1}A".format("\n" * margin, margin) t = "{0}\033[{1}A".format("\n" * margin, margin)
eprint("{0}\033[s\033[1;{1}r\033[u".format(m, self.g - 1)) eprint("{0}\033[s\033[1;{1}r\033[u".format(t, self.g - 1))
ss = CTermsize() ss = CTermsize()
@@ -231,8 +330,8 @@ def _scd(err, top):
abspath = os.path.join(top, fh.name) abspath = os.path.join(top, fh.name)
try: try:
yield [abspath, fh.stat()] yield [abspath, fh.stat()]
except: except Exception as ex:
err.append(abspath) err.append((abspath, str(ex)))
def _lsd(err, top): def _lsd(err, top):
@@ -241,8 +340,8 @@ def _lsd(err, top):
abspath = os.path.join(top, name) abspath = os.path.join(top, name)
try: try:
yield [abspath, os.stat(abspath)] yield [abspath, os.stat(abspath)]
except: except Exception as ex:
err.append(abspath) err.append((abspath, str(ex)))
if hasattr(os, "scandir"): if hasattr(os, "scandir"):
@@ -251,15 +350,21 @@ else:
statdir = _lsd statdir = _lsd
def walkdir(err, top): def walkdir(err, top, seen):
"""recursive statdir""" """recursive statdir"""
atop = os.path.abspath(os.path.realpath(top))
if atop in seen:
err.append((top, "recursive-symlink"))
return
seen = seen[:] + [atop]
for ap, inf in sorted(statdir(err, top)): for ap, inf in sorted(statdir(err, top)):
if stat.S_ISDIR(inf.st_mode): if stat.S_ISDIR(inf.st_mode):
try: try:
for x in walkdir(err, ap): for x in walkdir(err, ap, seen):
yield x yield x
except: except Exception as ex:
err.append(ap) err.append((ap, str(ex)))
else: else:
yield ap, inf yield ap, inf
@@ -274,7 +379,7 @@ def walkdirs(err, tops):
stop = os.path.dirname(top) stop = os.path.dirname(top)
if os.path.isdir(top): if os.path.isdir(top):
for ap, inf in walkdir(err, top): for ap, inf in walkdir(err, top, []):
yield stop, ap[len(stop) :].lstrip(sep), inf yield stop, ap[len(stop) :].lstrip(sep), inf
else: else:
d, n = top.rsplit(sep, 1) d, n = top.rsplit(sep, 1)
@@ -323,8 +428,8 @@ def up2k_chunksize(filesize):
# mostly from copyparty/up2k.py # mostly from copyparty/up2k.py
def get_hashlist(file, pcb): def get_hashlist(file, pcb, mth):
# type: (File, any) -> None # type: (File, any, any) -> None
"""generates the up2k hashlist from file contents, inserts it into `file`""" """generates the up2k hashlist from file contents, inserts it into `file`"""
chunk_sz = up2k_chunksize(file.size) chunk_sz = up2k_chunksize(file.size)
@@ -332,7 +437,12 @@ def get_hashlist(file, pcb):
file_ofs = 0 file_ofs = 0
ret = [] ret = []
with open(file.abs, "rb", 512 * 1024) as f: with open(file.abs, "rb", 512 * 1024) as f:
if mth and file.size >= 1024 * 512:
ret = mth.hash(f, file.size, chunk_sz, pcb, file)
file_rem = 0
while file_rem > 0: while file_rem > 0:
# same as `hash_at` except for `imutex` / bufsz
hashobj = hashlib.sha512() hashobj = hashlib.sha512()
chunk_sz = chunk_rem = min(chunk_sz, file_rem) chunk_sz = chunk_rem = min(chunk_sz, file_rem)
while chunk_rem > 0: while chunk_rem > 0:
@@ -360,7 +470,7 @@ def get_hashlist(file, pcb):
def handshake(req_ses, url, file, pw, search): def handshake(req_ses, url, file, pw, search):
# type: (requests.Session, str, File, any, bool) -> List[str] # type: (requests.Session, str, File, any, bool) -> list[str]
""" """
performs a handshake with the server; reply is: performs a handshake with the server; reply is:
if search, a list of search results if search, a list of search results
@@ -389,8 +499,9 @@ def handshake(req_ses, url, file, pw, search):
try: try:
r = req_ses.post(url, headers=headers, json=req) r = req_ses.post(url, headers=headers, json=req)
break break
except: except Exception as ex:
eprint("handshake failed, retrying: {0}\n".format(file.name)) em = str(ex).split("SSLError(")[-1]
eprint("handshake failed, retrying: {0}\n {1}\n\n".format(file.name, em))
time.sleep(1) time.sleep(1)
try: try:
@@ -399,7 +510,7 @@ def handshake(req_ses, url, file, pw, search):
raise Exception(r.text) raise Exception(r.text)
if search: if search:
return r["hits"] return r["hits"], False
try: try:
pre, url = url.split("://") pre, url = url.split("://")
@@ -411,7 +522,7 @@ def handshake(req_ses, url, file, pw, search):
file.name = r["name"] file.name = r["name"]
file.wark = r["wark"] file.wark = r["wark"]
return r["hash"] return r["hash"], r["sprs"]
def upload(req_ses, file, cid, pw): def upload(req_ses, file, cid, pw):
@@ -471,12 +582,19 @@ class Ctl(object):
if err: if err:
eprint("\n# failed to access {0} paths:\n".format(len(err))) eprint("\n# failed to access {0} paths:\n".format(len(err)))
for x in err: for ap, msg in err:
eprint(x.decode("utf-8", "replace") + "\n") if ar.v:
eprint("{0}\n `-{1}\n\n".format(ap.decode("utf-8", "replace"), msg))
else:
eprint(ap.decode("utf-8", "replace") + "\n")
eprint("^ failed to access those {0} paths ^\n\n".format(len(err))) eprint("^ failed to access those {0} paths ^\n\n".format(len(err)))
if not ar.v:
eprint("hint: set -v for detailed error messages\n")
if not ar.ok: if not ar.ok:
eprint("aborting because --ok is not set\n") eprint("hint: aborting because --ok is not set\n")
return return
eprint("found {0} files, {1}\n\n".format(nfiles, humansize(nbytes))) eprint("found {0} files, {1}\n\n".format(nfiles, humansize(nbytes)))
@@ -491,11 +609,37 @@ class Ctl(object):
self.filegen = walkdirs([], ar.files) self.filegen = walkdirs([], ar.files)
if ar.safe: if ar.safe:
self.safe() self._safe()
else: else:
self.fancy() self.hash_f = 0
self.hash_c = 0
self.hash_b = 0
self.up_f = 0
self.up_c = 0
self.up_b = 0
self.up_br = 0
self.hasher_busy = 1
self.handshaker_busy = 0
self.uploader_busy = 0
self.serialized = False
def safe(self): self.t0 = time.time()
self.t0_up = None
self.spd = None
self.mutex = threading.Lock()
self.q_handshake = Queue() # type: Queue[File]
self.q_recheck = Queue() # type: Queue[File] # partial upload exists [...]
self.q_upload = Queue() # type: Queue[tuple[File, str]]
self.st_hash = [None, "(idle, starting...)"] # type: tuple[File, int]
self.st_up = [None, "(idle, starting...)"] # type: tuple[File, int]
self.mth = MTHash(ar.J) if ar.J > 1 else None
self._fancy()
def _safe(self):
"""minimal basic slow boring fallback codepath""" """minimal basic slow boring fallback codepath"""
search = self.ar.s search = self.ar.s
for nf, (top, rel, inf) in enumerate(self.filegen): for nf, (top, rel, inf) in enumerate(self.filegen):
@@ -503,12 +647,12 @@ class Ctl(object):
upath = file.abs.decode("utf-8", "replace") upath = file.abs.decode("utf-8", "replace")
print("{0} {1}\n hash...".format(self.nfiles - nf, upath)) print("{0} {1}\n hash...".format(self.nfiles - nf, upath))
get_hashlist(file, None) get_hashlist(file, None, None)
burl = self.ar.url[:12] + self.ar.url[8:].split("/")[0] + "/" burl = self.ar.url[:12] + self.ar.url[8:].split("/")[0] + "/"
while True: while True:
print(" hs...") print(" hs...")
hs = handshake(req_ses, self.ar.url, file, self.ar.a, search) hs, _ = handshake(req_ses, self.ar.url, file, self.ar.a, search)
if search: if search:
if hs: if hs:
for hit in hs: for hit in hs:
@@ -529,29 +673,7 @@ class Ctl(object):
print(" ok!") print(" ok!")
def fancy(self): def _fancy(self):
self.hash_f = 0
self.hash_c = 0
self.hash_b = 0
self.up_f = 0
self.up_c = 0
self.up_b = 0
self.up_br = 0
self.hasher_busy = 1
self.handshaker_busy = 0
self.uploader_busy = 0
self.t0 = time.time()
self.t0_up = None
self.spd = None
self.mutex = threading.Lock()
self.q_handshake = Queue() # type: Queue[File]
self.q_recheck = Queue() # type: Queue[File] # partial upload exists [...]
self.q_upload = Queue() # type: Queue[tuple[File, str]]
self.st_hash = [None, "(idle, starting...)"] # type: tuple[File, int]
self.st_up = [None, "(idle, starting...)"] # type: tuple[File, int]
if VT100: if VT100:
atexit.register(self.cleanup_vt100) atexit.register(self.cleanup_vt100)
ss.scroll_region(3) ss.scroll_region(3)
@@ -597,8 +719,8 @@ class Ctl(object):
if "/" in name: if "/" in name:
name = "\033[36m{0}\033[0m/{1}".format(*name.rsplit("/", 1)) name = "\033[36m{0}\033[0m/{1}".format(*name.rsplit("/", 1))
m = "{0:6.1f}% {1} {2}\033[K" t = "{0:6.1f}% {1} {2}\033[K"
txt += m.format(p, self.nfiles - f, name) txt += t.format(p, self.nfiles - f, name)
txt += "\033[{0}H ".format(ss.g + 2) txt += "\033[{0}H ".format(ss.g + 2)
else: else:
@@ -614,11 +736,12 @@ class Ctl(object):
spd = humansize(spd) spd = humansize(spd)
eta = str(datetime.timedelta(seconds=int(eta))) eta = str(datetime.timedelta(seconds=int(eta)))
left = humansize(self.nbytes - self.up_b) sleft = humansize(self.nbytes - self.up_b)
nleft = self.nfiles - self.up_f
tail = "\033[K\033[u" if VT100 else "\r" tail = "\033[K\033[u" if VT100 else "\r"
m = "eta: {0} @ {1}/s, {2} left".format(eta, spd, left) t = "{0} eta @ {1}/s, {2}, {3}# left".format(eta, spd, sleft, nleft)
eprint(txt + "\033]0;{0}\033\\\r{1}{2}".format(m, m, tail)) eprint(txt + "\033]0;{0}\033\\\r{0}{1}".format(t, tail))
def cleanup_vt100(self): def cleanup_vt100(self):
ss.scroll_region(None) ss.scroll_region(None)
@@ -677,7 +800,7 @@ class Ctl(object):
time.sleep(0.05) time.sleep(0.05)
get_hashlist(file, self.cb_hasher) get_hashlist(file, self.cb_hasher, self.mth)
with self.mutex: with self.mutex:
self.hash_f += 1 self.hash_f += 1
self.hash_c += len(file.cids) self.hash_c += len(file.cids)
@@ -709,7 +832,7 @@ class Ctl(object):
upath = file.abs.decode("utf-8", "replace") upath = file.abs.decode("utf-8", "replace")
try: try:
hs = handshake(req_ses, self.ar.url, file, self.ar.a, search) hs, sprs = handshake(req_ses, self.ar.url, file, self.ar.a, search)
except Exception as ex: except Exception as ex:
if q == self.q_handshake and "<pre>partial upload exists" in str(ex): if q == self.q_handshake and "<pre>partial upload exists" in str(ex):
self.q_recheck.put(file) self.q_recheck.put(file)
@@ -720,8 +843,8 @@ class Ctl(object):
if search: if search:
if hs: if hs:
for hit in hs: for hit in hs:
m = "found: {0}\n {1}{2}\n" t = "found: {0}\n {1}{2}\n"
print(m.format(upath, burl, hit["rp"]), end="") print(t.format(upath, burl, hit["rp"]), end="")
else: else:
print("NOT found: {0}\n".format(upath), end="") print("NOT found: {0}\n".format(upath), end="")
@@ -734,6 +857,12 @@ class Ctl(object):
continue continue
with self.mutex: with self.mutex:
if not sprs and not self.serialized:
t = "server filesystem does not support sparse files; serializing uploads\n"
eprint(t)
self.serialized = True
for _ in range(self.ar.j - 1):
self.q_upload.put(None)
if not hs: if not hs:
# all chunks done # all chunks done
self.up_f += 1 self.up_f += 1
@@ -800,6 +929,9 @@ def main():
if not VT100: if not VT100:
os.system("rem") # enables colors os.system("rem") # enables colors
cores = os.cpu_count() if hasattr(os, "cpu_count") else 4
hcores = min(cores, 3) # 4% faster than 4+ on py3.9 @ r5-4500U
# fmt: off # fmt: off
ap = app = argparse.ArgumentParser(formatter_class=APF, epilog=""" ap = app = argparse.ArgumentParser(formatter_class=APF, epilog="""
NOTE: NOTE:
@@ -810,11 +942,13 @@ source file/folder selection uses rsync syntax, meaning that:
ap.add_argument("url", type=unicode, help="server url, including destination folder") ap.add_argument("url", type=unicode, help="server url, including destination folder")
ap.add_argument("files", type=unicode, nargs="+", help="files and/or folders to process") ap.add_argument("files", type=unicode, nargs="+", help="files and/or folders to process")
ap.add_argument("-v", action="store_true", help="verbose")
ap.add_argument("-a", metavar="PASSWORD", help="password") ap.add_argument("-a", metavar="PASSWORD", help="password")
ap.add_argument("-s", action="store_true", help="file-search (disables upload)") ap.add_argument("-s", action="store_true", help="file-search (disables upload)")
ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible") ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible")
ap = app.add_argument_group("performance tweaks") ap = app.add_argument_group("performance tweaks")
ap.add_argument("-j", type=int, metavar="THREADS", default=4, help="parallel connections") ap.add_argument("-j", type=int, metavar="THREADS", default=4, help="parallel connections")
ap.add_argument("-J", type=int, metavar="THREADS", default=hcores, help="num cpu-cores to use for hashing; set 0 or 1 for single-core hashing")
ap.add_argument("-nh", action="store_true", help="disable hashing while uploading") ap.add_argument("-nh", action="store_true", help="disable hashing while uploading")
ap.add_argument("--safe", action="store_true", help="use simple fallback approach") ap.add_argument("--safe", action="store_true", help="use simple fallback approach")
ap.add_argument("-z", action="store_true", help="ZOOMIN' (skip uploading files if they exist at the destination with the ~same last-modified timestamp, so same as yolo / turbo with date-chk but even faster)") ap.add_argument("-z", action="store_true", help="ZOOMIN' (skip uploading files if they exist at the destination with the ~same last-modified timestamp, so same as yolo / turbo with date-chk but even faster)")

View File

@@ -22,6 +22,9 @@ however if your copyparty is behind a reverse-proxy, you may want to use [`share
* `URL`: full URL to the root folder (with trailing slash) followed by `$regex:1|1$` * `URL`: full URL to the root folder (with trailing slash) followed by `$regex:1|1$`
* `pw`: password (remove `Parameters` if anon-write) * `pw`: password (remove `Parameters` if anon-write)
### [`media-osd-bgone.ps1`](media-osd-bgone.ps1)
* disables the [windows OSD popup](https://user-images.githubusercontent.com/241032/122821375-0e08df80-d2dd-11eb-9fd9-184e8aacf1d0.png) (the thing on the left) which appears every time you hit media hotkeys to adjust volume or change song while playing music with the copyparty web-ui, or most other audio players really
### [`explorer-nothumbs-nofoldertypes.reg`](explorer-nothumbs-nofoldertypes.reg) ### [`explorer-nothumbs-nofoldertypes.reg`](explorer-nothumbs-nofoldertypes.reg)
* disables thumbnails and folder-type detection in windows explorer * disables thumbnails and folder-type detection in windows explorer
* makes it way faster (especially for slow/networked locations (such as copyparty-fuse)) * makes it way faster (especially for slow/networked locations (such as copyparty-fuse))

104
contrib/media-osd-bgone.ps1 Normal file
View File

@@ -0,0 +1,104 @@
# media-osd-bgone.ps1: disable media-control OSD on win10do
# v1.1, 2021-06-25, ed <irc.rizon.net>, MIT-licensed
# https://github.com/9001/copyparty/blob/hovudstraum/contrib/media-osd-bgone.ps1
#
# locates the first window that looks like the media OSD and minimizes it;
# doing this once after each reboot should do the trick
# (adjust the width/height filter if it doesn't work)
#
# ---------------------------------------------------------------------
#
# tip: save the following as "media-osd-bgone.bat" next to this script:
# start cmd /c "powershell -command ""set-executionpolicy -scope process bypass; .\media-osd-bgone.ps1"" & ping -n 2 127.1 >nul"
#
# then create a shortcut to that bat-file and move the shortcut here:
# %appdata%\Microsoft\Windows\Start Menu\Programs\Startup
#
# and now this will autorun on bootup
Add-Type -TypeDefinition @"
using System;
using System.IO;
using System.Threading;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace A {
public class B : Control {
[DllImport("user32.dll")]
static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, int dwExtraInfo);
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
[DllImport("user32.dll", SetLastError=true)]
static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);
[DllImport("user32.dll")]
static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[StructLayout(LayoutKind.Sequential)]
public struct RECT {
public int x;
public int y;
public int x2;
public int y2;
}
bool fa() {
RECT r;
IntPtr it = IntPtr.Zero;
while ((it = FindWindowEx(IntPtr.Zero, it, "NativeHWNDHost", "")) != IntPtr.Zero) {
if (FindWindowEx(it, IntPtr.Zero, "DirectUIHWND", "") == IntPtr.Zero)
continue;
if (!GetWindowRect(it, out r))
continue;
int w = r.x2 - r.x + 1;
int h = r.y2 - r.y + 1;
Console.WriteLine("[*] hwnd {0:x} @ {1}x{2} sz {3}x{4}", it, r.x, r.y, w, h);
if (h != 141)
continue;
ShowWindow(it, 6);
Console.WriteLine("[+] poof");
return true;
}
return false;
}
void fb() {
keybd_event((byte)Keys.VolumeMute, 0, 0, 0);
keybd_event((byte)Keys.VolumeMute, 0, 2, 0);
Thread.Sleep(500);
keybd_event((byte)Keys.VolumeMute, 0, 0, 0);
keybd_event((byte)Keys.VolumeMute, 0, 2, 0);
while (true) {
if (fa()) {
break;
}
Console.WriteLine("[!] not found");
Thread.Sleep(1000);
}
this.Invoke((MethodInvoker)delegate {
Application.Exit();
});
}
public void Run() {
Console.WriteLine("[+] hi");
new Thread(new ThreadStart(fb)).Start();
Application.Run();
Console.WriteLine("[+] bye");
}
}
}
"@ -ReferencedAssemblies System.Windows.Forms
(New-Object -TypeName A.B).Run()

View File

@@ -11,6 +11,15 @@ save one of these as `.epilogue.html` inside a folder to customize it:
## example browser-js
point `--js-browser` to one of these by URL:
* [`minimal-up2k.js`](minimal-up2k.js) is similar to the above `minimal-up2k.html` except it applies globally to all write-only folders
* [`up2k-hooks.js`](up2k-hooks.js) lets you specify a ruleset for files to skip uploading
* [`up2k-hook-ytid.js`](up2k-hook-ytid.js) is a more specific example checking youtube-IDs against some API
## example browser-css ## example browser-css
point `--css-browser` to one of these by URL: point `--css-browser` to one of these by URL:

View File

@@ -7,7 +7,7 @@
/* make the up2k ui REALLY minimal by hiding a bunch of stuff: */ /* make the up2k ui REALLY minimal by hiding a bunch of stuff: */
#ops, #tree, #path, #wrap>h2:last-child, /* main tabs and navigators (tree/breadcrumbs) */ #ops, #tree, #path, #epi+h2, /* main tabs and navigators (tree/breadcrumbs) */
#u2conf tr:first-child>td[rowspan]:not(#u2btn_cw), /* most of the config options */ #u2conf tr:first-child>td[rowspan]:not(#u2btn_cw), /* most of the config options */

View File

@@ -0,0 +1,59 @@
/*
makes the up2k ui REALLY minimal by hiding a bunch of stuff
almost the same as minimal-up2k.html except this one...:
-- applies to every write-only folder when used with --js-browser
-- only applies if javascript is enabled
-- doesn't hide the total upload ETA display
-- looks slightly better
*/
var u2min = `
<style>
#ops, #path, #tree, #files, #epi+div+h2,
#u2conf td.c+.c, #u2cards, #srch_dz, #srch_zd {
display: none !important;
}
#u2conf {margin:5em auto 0 auto !important}
#u2conf.ww {width:70em}
#u2conf.w {width:50em}
#u2conf.w .c,
#u2conf.w #u2btn_cw {text-align:left}
#u2conf.w #u2btn_cw {width:70%}
#u2etaw {margin:3em auto}
#u2etaw.w {
text-align: center;
margin: -3.5em auto 5em auto;
}
#u2etaw.w #u2etas {margin-right:-37em}
#u2etaw.w #u2etas.o {margin-top:-2.2em}
#u2etaw.ww {margin:-1em auto}
#u2etaw.ww #u2etas {padding-left:4em}
#u2etas {
background: none !important;
border: none !important;
}
#wrap {margin-left:2em !important}
.logue {
border: none !important;
margin: 2em auto !important;
}
.logue:before {content:'' !important}
</style>
<a href="#" onclick="this.parentNode.innerHTML='';">show advanced options</a>
`;
if (!has(perms, 'read')) {
var e2 = mknod('div');
e2.innerHTML = u2min;
ebi('wrap').insertBefore(e2, QS('#epi+h2'));
}

View File

@@ -0,0 +1,239 @@
// way more specific example --
// assumes all files dropped into the uploader have a youtube-id somewhere in the filename,
// locates the youtube-ids and passes them to an API which returns a list of IDs which should be uploaded
//
// also tries to find the youtube-id in the embedded metadata
//
// assumes copyparty is behind nginx as /ytq is a standalone service which must be rproxied in place
function up2k_namefilter(good_files, nil_files, bad_files, hooks) {
var passthru = up2k.uc.fsearch;
if (passthru)
return hooks[0](good_files, nil_files, bad_files, hooks.slice(1));
a_up2k_namefilter(good_files, nil_files, bad_files, hooks).then(() => { });
}
function bstrpos(buf, ptn) {
var ofs = 0,
ch0 = ptn[0],
sz = buf.byteLength;
while (true) {
ofs = buf.indexOf(ch0, ofs);
if (ofs < 0 || ofs >= sz)
return -1;
for (var a = 1; a < ptn.length; a++)
if (buf[ofs + a] !== ptn[a])
break;
if (a === ptn.length)
return ofs;
++ofs;
}
}
async function a_up2k_namefilter(good_files, nil_files, bad_files, hooks) {
var t0 = Date.now(),
yt_ids = new Set(),
textdec = new TextDecoder('latin1'),
md_ptn = new TextEncoder().encode('youtube.com/watch?v='),
file_ids = [], // all IDs found for each good_files
md_only = [], // `${id} ${fn}` where ID was only found in metadata
mofs = 0,
mnchk = 0,
mfile = '';
for (var a = 0; a < good_files.length; a++) {
var [fobj, name] = good_files[a],
cname = name, // will clobber
sz = fobj.size,
ids = [],
fn_ids = [],
md_ids = [],
id_ok = false,
m;
// all IDs found in this file
file_ids.push(ids);
// look for ID in filename; reduce the
// metadata-scan intensity if the id looks safe
m = /[\[(-]([\w-]{11})[\])]?\.(?:mp4|webm|mkv|flv|opus|ogg|mp3|m4a|aac)$/i.exec(name);
id_ok = !!m;
while (true) {
// fuzzy catch-all;
// some ytdl fork did %(title)-%(id).%(ext) ...
m = /(?:^|[^\w])([\w-]{11})(?:$|[^\w-])/.exec(cname);
if (!m)
break;
cname = cname.replace(m[1], '');
yt_ids.add(m[1]);
fn_ids.unshift(m[1]);
}
// look for IDs in video metadata,
if (/\.(mp4|webm|mkv|flv|opus|ogg|mp3|m4a|aac)$/i.exec(name)) {
toast.show('inf r', 0, `analyzing file ${a + 1} / ${good_files.length} :\n${name}\n\nhave analysed ${++mnchk} files in ${(Date.now() - t0) / 1000} seconds, ${humantime((good_files.length - (a + 1)) * (((Date.now() - t0) / 1000) / mnchk))} remaining,\n\nbiggest offset so far is ${mofs}, in this file:\n\n${mfile}`);
// check first and last 128 MiB;
// pWxOroN5WCo.mkv @ 6edb98 (6.92M)
// Nf-nN1wF5Xo.mp4 @ 4a98034 (74.6M)
var chunksz = 1024 * 1024 * 2, // byte
aspan = id_ok ? 128 : 512; // MiB
aspan = parseInt(Math.min(sz / 2, aspan * 1024 * 1024) / chunksz) * chunksz;
for (var side = 0; side < 2; side++) {
var ofs = side ? Math.max(0, sz - aspan) : 0,
nchunks = aspan / chunksz;
for (var chunk = 0; chunk < nchunks; chunk++) {
var bchunk = await fobj.slice(ofs, ofs + chunksz + 16).arrayBuffer(),
uchunk = new Uint8Array(bchunk, 0, bchunk.byteLength),
bofs = bstrpos(uchunk, md_ptn),
absofs = Math.min(ofs + bofs, (sz - ofs) + bofs),
txt = bofs < 0 ? '' : textdec.decode(uchunk.subarray(bofs)),
m;
//console.log(`side ${ side }, chunk ${ chunk }, ofs ${ ofs }, bchunk ${ bchunk.byteLength }, txt ${ txt.length }`);
while (true) {
// mkv/webm have [a-z] immediately after url
m = /(youtube\.com\/watch\?v=[\w-]{11})/.exec(txt);
if (!m)
break;
txt = txt.replace(m[1], '');
m = m[1].slice(-11);
console.log(`found ${m} @${bofs}, ${name} `);
yt_ids.add(m);
if (!has(fn_ids, m) && !has(md_ids, m)) {
md_ids.push(m);
md_only.push(`${m} ${name}`);
}
else
// id appears several times; make it preferred
md_ids.unshift(m);
// bail after next iteration
chunk = nchunks - 1;
side = 9;
if (mofs < absofs) {
mofs = absofs;
mfile = name;
}
}
ofs += chunksz;
if (ofs >= sz)
break;
}
}
}
for (var yi of md_ids)
ids.push(yi);
for (var yi of fn_ids)
if (!has(ids, yi))
ids.push(yi);
}
if (md_only.length)
console.log('recovered the following youtube-IDs by inspecting metadata:\n\n' + md_only.join('\n'));
else if (yt_ids.size)
console.log('did not discover any additional youtube-IDs by inspecting metadata; all the IDs also existed in the filenames');
else
console.log('failed to find any youtube-IDs at all, sorry');
if (false) {
var msg = `finished analysing ${mnchk} files in ${(Date.now() - t0) / 1000} seconds,\n\nbiggest offset was ${mofs} in this file:\n\n${mfile}`,
mfun = function () { toast.ok(0, msg); };
mfun();
setTimeout(mfun, 200);
return hooks[0]([], [], [], hooks.slice(1));
}
toast.inf(5, `running query for ${yt_ids.size} youtube-IDs...`);
var xhr = new XHR();
xhr.open('POST', '/ytq', true);
xhr.setRequestHeader('Content-Type', 'text/plain');
xhr.onload = xhr.onerror = function () {
if (this.status != 200)
return toast.err(0, `sorry, database query failed ;_;\n\nplease let us know so we can look at it, thx!!\n\nerror ${this.status}: ${(this.response && this.response.err) || this.responseText}`);
process_id_list(this.responseText);
};
xhr.send(Array.from(yt_ids).join('\n'));
function process_id_list(txt) {
var wanted_ids = new Set(txt.trim().split('\n')),
name_id = {},
wanted_names = new Set(), // basenames with a wanted ID
wanted_files = new Set(); // filedrops
for (var a = 0; a < good_files.length; a++) {
var name = good_files[a][1];
for (var b = 0; b < file_ids[a].length; b++)
if (wanted_ids.has(file_ids[a][b])) {
wanted_files.add(good_files[a]);
var m = /(.*)\.(mp4|webm|mkv|flv|opus|ogg|mp3|m4a|aac)$/i.exec(name);
if (!m)
continue;
wanted_names.add(m[1]);
name_id[m[1]] = file_ids[a][b];
break;
}
}
// add all files with the same basename as each explicitly wanted file
// (infojson/chatlog/etc when ID was discovered from metadata)
for (var a = 0; a < good_files.length; a++) {
var name = good_files[a][1];
for (var b = 0; b < 3; b++) {
name = name.replace(/\.[^\.]+$/, '');
if (wanted_names.has(name)) {
wanted_files.add(good_files[a]);
var subdir = `${name_id[name]}-${Date.now()}-${a}`;
good_files[a][1] = subdir + '/' + good_files[a][1].split(/\//g).pop();
break;
}
}
}
function upload_filtered() {
if (!wanted_files.size)
return modal.alert('Good news -- turns out we already have all those.\n\nBut thank you for checking in!');
hooks[0](Array.from(wanted_files), nil_files, bad_files, hooks.slice(1));
}
function upload_all() {
hooks[0](good_files, nil_files, bad_files, hooks.slice(1));
}
var n_skip = good_files.length - wanted_files.size,
msg = `you added ${good_files.length} files; ${good_files.length == n_skip ? 'all' : n_skip} of them were skipped --\neither because we already have them,\nor because there is no youtube-ID in your filenames.\n\n<code>OK</code> / <code>Enter</code> = continue uploading just the ${wanted_files.size} files we definitely need\n\n<code>Cancel</code> / <code>ESC</code> = override the filter; upload ALL the files you added`;
if (!n_skip)
upload_filtered();
else
modal.confirm(msg, upload_filtered, upload_all);
};
}
up2k_hooks.push(function () {
up2k.gotallfiles.unshift(up2k_namefilter);
});

View File

@@ -0,0 +1,45 @@
// hooks into up2k
function up2k_namefilter(good_files, nil_files, bad_files, hooks) {
// is called when stuff is dropped into the browser,
// after iterating through the directory tree and discovering all files,
// before the upload confirmation dialogue is shown
// good_files will successfully upload
// nil_files are empty files and will show an alert in the final hook
// bad_files are unreadable and cannot be uploaded
var file_lists = [good_files, nil_files, bad_files];
// build a list of filenames
var filenames = [];
for (var lst of file_lists)
for (var ent of lst)
filenames.push(ent[1]);
toast.inf(5, "running database query...");
// simulate delay while passing the list to some api for checking
setTimeout(function () {
// only keep webm files as an example
var new_lists = [];
for (var lst of file_lists) {
var keep = [];
new_lists.push(keep);
for (var ent of lst)
if (/\.webm$/.test(ent[1]))
keep.push(ent);
}
// finally, call the next hook in the chain
[good_files, nil_files, bad_files] = new_lists;
hooks[0](good_files, nil_files, bad_files, hooks.slice(1));
}, 1000);
}
// register
up2k_hooks.push(function () {
up2k.gotallfiles.unshift(up2k_namefilter);
});

View File

@@ -4,7 +4,7 @@
# installation: # installation:
# cp -pv copyparty.service /etc/systemd/system # cp -pv copyparty.service /etc/systemd/system
# restorecon -vr /etc/systemd/system/copyparty.service # restorecon -vr /etc/systemd/system/copyparty.service
# firewall-cmd --permanent --add-port={80,443,3923}/tcp # firewall-cmd --permanent --add-port={80,443,3923}/tcp # --zone=libvirt
# firewall-cmd --reload # firewall-cmd --reload
# systemctl daemon-reload && systemctl enable --now copyparty # systemctl daemon-reload && systemctl enable --now copyparty
# #

View File

@@ -1,32 +1,41 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import platform
import time
import sys
import os import os
import platform
import sys
import time
try:
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
except:
TYPE_CHECKING = False
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 unicode = unicode # noqa: F821 # pylint: disable=undefined-variable,self-assigning-variable
else: else:
unicode = str unicode = str
WINDOWS = False WINDOWS: Any = (
if platform.system() == "Windows": [int(x) for x in platform.version().split(".")]
WINDOWS = [int(x) for x in platform.version().split(".")] if platform.system() == "Windows"
else False
)
VT100 = not WINDOWS or WINDOWS >= [10, 0, 14393] VT100 = not WINDOWS or WINDOWS >= [10, 0, 14393]
# introduced in anniversary update # introduced in anniversary update
ANYWIN = WINDOWS or sys.platform in ["msys"] ANYWIN = WINDOWS or sys.platform in ["msys", "cygwin"]
MACOS = platform.system() == "Darwin" MACOS = platform.system() == "Darwin"
def get_unixdir(): def get_unixdir() -> str:
paths = [ paths: list[tuple[Callable[..., str], str]] = [
(os.environ.get, "XDG_CONFIG_HOME"), (os.environ.get, "XDG_CONFIG_HOME"),
(os.path.expanduser, "~/.config"), (os.path.expanduser, "~/.config"),
(os.environ.get, "TMPDIR"), (os.environ.get, "TMPDIR"),
@@ -43,7 +52,7 @@ def get_unixdir():
continue continue
p = os.path.normpath(p) p = os.path.normpath(p)
chk(p) chk(p) # type: ignore
p = os.path.join(p, "copyparty") p = os.path.join(p, "copyparty")
if not os.path.isdir(p): if not os.path.isdir(p):
os.mkdir(p) os.mkdir(p)
@@ -56,7 +65,7 @@ def get_unixdir():
class EnvParams(object): class EnvParams(object):
def __init__(self): def __init__(self) -> None:
self.t0 = time.time() self.t0 = time.time()
self.mod = os.path.dirname(os.path.realpath(__file__)) self.mod = os.path.dirname(os.path.realpath(__file__))
if self.mod.endswith("__init__"): if self.mod.endswith("__init__"):

View File

@@ -8,35 +8,59 @@ __copyright__ = 2019
__license__ = "MIT" __license__ = "MIT"
__url__ = "https://github.com/9001/copyparty/" __url__ = "https://github.com/9001/copyparty/"
import re import argparse
import os
import sys
import time
import shutil
import filecmp import filecmp
import locale import locale
import argparse import os
import re
import shutil
import sys
import threading import threading
import time
import traceback import traceback
from textwrap import dedent from textwrap import dedent
from .__init__ import E, WINDOWS, ANYWIN, VT100, PY2, unicode from .__init__ import ANYWIN, PY2, VT100, WINDOWS, E, unicode
from .__version__ import S_VERSION, S_BUILD_DT, CODENAME from .__version__ import CODENAME, S_BUILD_DT, S_VERSION
from .svchub import SvcHub
from .util import py_desc, align_tab, IMPLICATIONS, ansi_re, min_ex
from .authsrv import re_vol from .authsrv import re_vol
from .svchub import SvcHub
from .util import (
IMPLICATIONS,
JINJA_VER,
PYFTPD_VER,
SQLITE_VER,
align_tab,
ansi_re,
min_ex,
py_desc,
termsize,
wrap,
)
HAVE_SSL = True
try: try:
from types import FrameType
from typing import Any, Optional
except:
pass
try:
HAVE_SSL = True
import ssl import ssl
except: except:
HAVE_SSL = False HAVE_SSL = False
printed = "" printed: list[str] = []
class RiceFormatter(argparse.HelpFormatter): class RiceFormatter(argparse.HelpFormatter):
def _get_help_string(self, action): def __init__(self, *args: Any, **kwargs: Any) -> None:
if PY2:
kwargs["width"] = termsize()[0]
super(RiceFormatter, self).__init__(*args, **kwargs)
def _get_help_string(self, action: argparse.Action) -> str:
""" """
same as ArgumentDefaultsHelpFormatter(HelpFormatter) same as ArgumentDefaultsHelpFormatter(HelpFormatter)
except the help += [...] line now has colors except the help += [...] line now has colors
@@ -45,41 +69,69 @@ class RiceFormatter(argparse.HelpFormatter):
if not VT100: if not VT100:
fmt = " (default: %(default)s)" fmt = " (default: %(default)s)"
help = action.help ret = unicode(action.help)
if "%(default)" not in action.help: if "%(default)" not in ret:
if action.default is not argparse.SUPPRESS: if action.default is not argparse.SUPPRESS:
defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE] defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE]
if action.option_strings or action.nargs in defaulting_nargs: if action.option_strings or action.nargs in defaulting_nargs:
help += fmt ret += fmt
return help return ret
def _fill_text(self, text, width, indent): def _fill_text(self, text: str, width: int, indent: str) -> str:
"""same as RawDescriptionHelpFormatter(HelpFormatter)""" """same as RawDescriptionHelpFormatter(HelpFormatter)"""
return "".join(indent + line + "\n" for line in text.splitlines()) return "".join(indent + line + "\n" for line in text.splitlines())
def __add_whitespace(self, idx: int, iWSpace: int, text: str) -> str:
return (" " * iWSpace) + text if idx else text
def _split_lines(self, text: str, width: int) -> list[str]:
# https://stackoverflow.com/a/35925919
textRows = text.splitlines()
ptn = re.compile(r"\s*[0-9\-]{0,}\.?\s*")
for idx, line in enumerate(textRows):
search = ptn.search(line)
if not line.strip():
textRows[idx] = " "
elif search:
lWSpace = search.end()
lines = [
self.__add_whitespace(i, lWSpace, x)
for i, x in enumerate(wrap(line, width, width - 1))
]
textRows[idx] = lines
return [item for sublist in textRows for item in sublist]
class Dodge11874(RiceFormatter): class Dodge11874(RiceFormatter):
def __init__(self, *args, **kwargs): def __init__(self, *args: Any, **kwargs: Any) -> None:
kwargs["width"] = 9003 kwargs["width"] = 9003
super(Dodge11874, self).__init__(*args, **kwargs) super(Dodge11874, self).__init__(*args, **kwargs)
def lprint(*a, **ka): class BasicDodge11874(
global printed argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter
):
def __init__(self, *args: Any, **kwargs: Any) -> None:
kwargs["width"] = 9003
super(BasicDodge11874, self).__init__(*args, **kwargs)
txt = " ".join(unicode(x) for x in a) + ka.get("end", "\n")
printed += txt def lprint(*a: Any, **ka: Any) -> None:
eol = ka.pop("end", "\n")
txt: str = " ".join(unicode(x) for x in a) + eol
printed.append(txt)
if not VT100: if not VT100:
txt = ansi_re.sub("", txt) txt = ansi_re.sub("", txt)
print(txt, **ka) print(txt, end="", **ka)
def warn(msg): def warn(msg: str) -> None:
lprint("\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() -> None:
for x in [ for x in [
"en_US.UTF-8", "en_US.UTF-8",
"English_United States.UTF8", "English_United States.UTF8",
@@ -87,13 +139,13 @@ def ensure_locale():
]: ]:
try: try:
locale.setlocale(locale.LC_ALL, x) locale.setlocale(locale.LC_ALL, x)
lprint("Locale:", x) lprint("Locale: {}\n".format(x))
break break
except: except:
continue continue
def ensure_cert(): def ensure_cert() -> None:
""" """
the default cert (and the entire TLS support) is only here to enable the the default cert (and the entire TLS support) is only here to enable the
crypto.subtle javascript API, which is necessary due to the webkit guys crypto.subtle javascript API, which is necessary due to the webkit guys
@@ -119,8 +171,8 @@ def ensure_cert():
# printf 'NO\n.\n.\n.\n.\ncopyparty-insecure\n.\n' | faketime '2000-01-01 00:00:00' openssl req -x509 -sha256 -newkey rsa:2048 -keyout insecure.pem -out insecure.pem -days $((($(printf %d 0x7fffffff)-$(date +%s --date=2000-01-01T00:00:00Z))/(60*60*24))) -nodes && ls -al insecure.pem && openssl x509 -in insecure.pem -text -noout # printf 'NO\n.\n.\n.\n.\ncopyparty-insecure\n.\n' | faketime '2000-01-01 00:00:00' openssl req -x509 -sha256 -newkey rsa:2048 -keyout insecure.pem -out insecure.pem -days $((($(printf %d 0x7fffffff)-$(date +%s --date=2000-01-01T00:00:00Z))/(60*60*24))) -nodes && ls -al insecure.pem && openssl x509 -in insecure.pem -text -noout
def configure_ssl_ver(al): def configure_ssl_ver(al: argparse.Namespace) -> None:
def terse_sslver(txt): def terse_sslver(txt: str) -> str:
txt = txt.lower() txt = txt.lower()
for c in ["_", "v", "."]: for c in ["_", "v", "."]:
txt = txt.replace(c, "") txt = txt.replace(c, "")
@@ -135,8 +187,8 @@ def configure_ssl_ver(al):
flags = [k for k in ssl.__dict__ if ptn.match(k)] flags = [k for k in ssl.__dict__ if ptn.match(k)]
# SSLv2 SSLv3 TLSv1 TLSv1_1 TLSv1_2 TLSv1_3 # SSLv2 SSLv3 TLSv1 TLSv1_1 TLSv1_2 TLSv1_3
if "help" in sslver: if "help" in sslver:
avail = [terse_sslver(x[6:]) for x in flags] avail1 = [terse_sslver(x[6:]) for x in flags]
avail = " ".join(sorted(avail) + ["all"]) avail = " ".join(sorted(avail1) + ["all"])
lprint("\navailable ssl/tls versions:\n " + avail) lprint("\navailable ssl/tls versions:\n " + avail)
sys.exit(0) sys.exit(0)
@@ -157,12 +209,12 @@ 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)
lprint("{}: {:8x} ({})".format(k, num, num)) lprint("{0}: {1:8x} ({1})".format(k, num))
# think i need that beer now # think i need that beer now
def configure_ssl_ciphers(al): def configure_ssl_ciphers(al: argparse.Namespace) -> None:
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
if al.ssl_ver: if al.ssl_ver:
ctx.options &= ~al.ssl_flags_en ctx.options &= ~al.ssl_flags_en
@@ -186,8 +238,8 @@ def configure_ssl_ciphers(al):
sys.exit(0) sys.exit(0)
def args_from_cfg(cfg_path): def args_from_cfg(cfg_path: str) -> list[str]:
ret = [] ret: list[str] = []
skip = False skip = False
with open(cfg_path, "rb") as f: with open(cfg_path, "rb") as f:
for ln in [x.decode("utf-8").strip() for x in f]: for ln in [x.decode("utf-8").strip() for x in f]:
@@ -212,29 +264,30 @@ def args_from_cfg(cfg_path):
return ret return ret
def sighandler(sig=None, frame=None): def sighandler(sig: Optional[int] = None, frame: Optional[FrameType] = None) -> None:
msg = [""] * 5 msg = [""] * 5
for th in threading.enumerate(): for th in threading.enumerate():
stk = sys._current_frames()[th.ident] # type: ignore
msg.append(str(th)) msg.append(str(th))
msg.extend(traceback.format_stack(sys._current_frames()[th.ident])) msg.extend(traceback.format_stack(stk))
msg.append("\n") msg.append("\n")
print("\n".join(msg)) print("\n".join(msg))
def disable_quickedit(): def disable_quickedit() -> None:
import ctypes
import atexit import atexit
import ctypes
from ctypes import wintypes from ctypes import wintypes
def ecb(ok, fun, args): def ecb(ok: bool, fun: Any, args: list[Any]) -> list[Any]:
if not ok: if not ok:
err = ctypes.get_last_error() err: int = ctypes.get_last_error() # type: ignore
if err: if err:
raise ctypes.WinError(err) raise ctypes.WinError(err) # type: ignore
return args return args
k32 = ctypes.WinDLL("kernel32", use_last_error=True) k32 = ctypes.WinDLL(str("kernel32"), use_last_error=True) # type: ignore
if PY2: if PY2:
wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD) wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD)
@@ -244,14 +297,14 @@ def disable_quickedit():
k32.GetConsoleMode.argtypes = (wintypes.HANDLE, wintypes.LPDWORD) k32.GetConsoleMode.argtypes = (wintypes.HANDLE, wintypes.LPDWORD)
k32.SetConsoleMode.argtypes = (wintypes.HANDLE, wintypes.DWORD) k32.SetConsoleMode.argtypes = (wintypes.HANDLE, wintypes.DWORD)
def cmode(out, mode=None): def cmode(out: bool, mode: Optional[int] = None) -> int:
h = k32.GetStdHandle(-11 if out else -10) h = k32.GetStdHandle(-11 if out else -10)
if mode: if mode:
return k32.SetConsoleMode(h, mode) return k32.SetConsoleMode(h, mode) # type: ignore
mode = wintypes.DWORD() cmode = wintypes.DWORD()
k32.GetConsoleMode(h, ctypes.byref(mode)) k32.GetConsoleMode(h, ctypes.byref(cmode))
return mode.value return cmode.value
# disable quickedit # disable quickedit
mode = orig_in = cmode(False) mode = orig_in = cmode(False)
@@ -270,7 +323,7 @@ def disable_quickedit():
cmode(True, mode | 4) cmode(True, mode | 4)
def run_argparse(argv, formatter): def run_argparse(argv: list[str], formatter: Any, retry: bool) -> argparse.Namespace:
ap = argparse.ArgumentParser( ap = argparse.ArgumentParser(
formatter_class=formatter, formatter_class=formatter,
prog="copyparty", prog="copyparty",
@@ -282,7 +335,8 @@ def run_argparse(argv, formatter):
except: except:
fk_salt = "hunter2" fk_salt = "hunter2"
cores = os.cpu_count() if hasattr(os, "cpu_count") else 4 cores = (os.cpu_count() if hasattr(os, "cpu_count") else 0) or 4
hcores = min(cores, 3) # 4% faster than 4+ on py3.9 @ r5-4500U
sects = [ sects = [
[ [
@@ -294,7 +348,7 @@ def run_argparse(argv, formatter):
-v takes src:dst:\033[33mperm\033[0m1:\033[33mperm\033[0m2:\033[33mperm\033[0mN:\033[32mvolflag\033[0m1:\033[32mvolflag\033[0m2:\033[32mvolflag\033[0mN:... -v takes src:dst:\033[33mperm\033[0m1:\033[33mperm\033[0m2:\033[33mperm\033[0mN:\033[32mvolflag\033[0m1:\033[32mvolflag\033[0m2:\033[32mvolflag\033[0mN:...
* "\033[33mperm\033[0m" is "permissions,username1,username2,..." * "\033[33mperm\033[0m" is "permissions,username1,username2,..."
* "\033[32mvolflag\033[0m" is config flags to set on this volume * "\033[32mvolflag\033[0m" is config flags to set on this volume
list of permissions: list of permissions:
"r" (read): list folder contents, download files "r" (read): list folder contents, download files
"w" (write): upload files; need "r" to see the uploads "w" (write): upload files; need "r" to see the uploads
@@ -313,7 +367,7 @@ def run_argparse(argv, formatter):
* w (write-only) for everyone * w (write-only) for everyone
* rw (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,
current folder will be read/write for everyone current folder will be read/write for everyone
@@ -336,46 +390,50 @@ def run_argparse(argv, formatter):
\033[36mnosub\033[35m forces all uploads into the top folder of the vfs \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[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[36mpk\033[35m forces server-side compression, optional arg: xz,9
\033[0mupload rules: \033[0mupload rules:
\033[36mmaxn=250,600\033[35m max 250 uploads over 15min \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[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[36msz=1k-3m\033[35m allow filesizes between 1 KiB and 3MiB
\033[36mdf=1g\033[35m ensure 1 GiB free disk space
\033[0mupload rotation: \033[0mupload rotation:
(moves all uploads into the specified folder structure) (moves all uploads into the specified folder structure)
\033[36mrotn=100,3\033[35m 3 levels of subfolders with 100 entries in each \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[36mrotf=%Y-%m/%d-%H\033[35m date-formatted organizing
\033[36mlifetime=3600\033[35m uploads are deleted after 1 hour \033[36mlifetime=3600\033[35m uploads are deleted after 1 hour
\033[0mdatabase, general: \033[0mdatabase, general:
\033[36me2d\033[35m sets -e2d (all -e2* args can be set using ce2* volflags) \033[36me2d\033[35m sets -e2d (all -e2* args can be set using ce2* volflags)
\033[36md2ts\033[35m disables metadata collection for existing files \033[36md2ts\033[35m disables metadata collection for existing files
\033[36md2ds\033[35m disables onboot indexing, overrides -e2ds* \033[36md2ds\033[35m disables onboot indexing, overrides -e2ds*
\033[36md2t\033[35m disables metadata collection, overrides -e2t* \033[36md2t\033[35m disables metadata collection, overrides -e2t*
\033[36md2v\033[35m disables file verification, overrides -e2v*
\033[36md2d\033[35m disables all database stuff, overrides -e2* \033[36md2d\033[35m disables all database stuff, overrides -e2*
\033[36mnohash=\\.iso$\033[35m skips hashing file contents if path matches *.iso
\033[36mnoidx=\\.iso$\033[35m fully ignores the contents at paths matching *.iso
\033[36mhist=/tmp/cdb\033[35m puts thumbnails and indexes at that location \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[36mscan=60\033[35m scan for new files every 60sec, same as --re-maxage
\033[36mnohash=\\.iso$\033[35m skips hashing file contents if path matches *.iso
\033[36mnoidx=\\.iso$\033[35m fully ignores the contents at paths matching *.iso
\033[36mxdev\033[35m do not descend into other filesystems
\033[36mxvol\033[35m skip symlinks leaving the volume root
\033[0mdatabase, audio tags: \033[0mdatabase, audio tags:
"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ... "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 \033[36mmtp=.bpm=f,audio-bpm.py\033[35m uses the "audio-bpm.py" program to
generate ".bpm" tags from uploads (f = overwrite tags) generate ".bpm" tags from uploads (f = overwrite tags)
\033[36mmtp=ahash,vhash=media-hash.py\033[35m collects two tags at once \033[36mmtp=ahash,vhash=media-hash.py\033[35m collects two tags at once
\033[0mthumbnails: \033[0mthumbnails:
\033[36mdthumb\033[35m disables all thumbnails \033[36mdthumb\033[35m disables all thumbnails
\033[36mdvthumb\033[35m disables video thumbnails \033[36mdvthumb\033[35m disables video thumbnails
\033[36mdathumb\033[35m disables audio thumbnails (spectrograms) \033[36mdathumb\033[35m disables audio thumbnails (spectrograms)
\033[36mdithumb\033[35m disables image thumbnails \033[36mdithumb\033[35m disables image thumbnails
\033[0mclient and ux: \033[0mclient and ux:
\033[36mhtml_head=TXT\033[35m includes TXT in the <head> \033[36mhtml_head=TXT\033[35m includes TXT in the <head>
\033[36mrobots\033[35m allows indexing by search engines (default) \033[36mrobots\033[35m allows indexing by search engines (default)
\033[36mnorobots\033[35m kindly asks search engines to leave \033[36mnorobots\033[35m kindly asks search engines to leave
\033[0mothers: \033[0mothers:
\033[36mfk=8\033[35m generates per-file accesskeys, \033[36mfk=8\033[35m generates per-file accesskeys,
which will then be required at the "g" permission which will then be required at the "g" permission
@@ -433,15 +491,17 @@ def run_argparse(argv, formatter):
ap2 = ap.add_argument_group('upload options') ap2 = ap.add_argument_group('upload options')
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless -ed") ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless -ed")
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled") ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled")
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without -e2d; roughly 1 MiB RAM per 600")
ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload") ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload")
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even if copyparty thinks you're better off without -- probably useful on nfs and cow filesystems (zfs, btrfs)") ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even if copyparty thinks you're better off without -- probably useful on nfs and cow filesystems (zfs, btrfs)")
ap2.add_argument("--hardlink", action="store_true", help="prefer hardlinks instead of symlinks when possible (within same filesystem)") ap2.add_argument("--hardlink", action="store_true", help="prefer hardlinks instead of symlinks when possible (within same filesystem)")
ap2.add_argument("--never-symlink", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made") ap2.add_argument("--never-symlink", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made")
ap2.add_argument("--no-dedup", action="store_true", help="disable symlink/hardlink creation; copy file contents instead") ap2.add_argument("--no-dedup", action="store_true", help="disable symlink/hardlink creation; copy file contents instead")
ap2.add_argument("--reg-cap", metavar="N", type=int, default=9000, help="max number of uploads to keep in memory when running without -e2d") ap2.add_argument("--df", metavar="GiB", type=float, default=0, help="ensure GiB free disk space by rejecting upload requests")
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; 0 = off and warn if enabled, 1 = off, 2 = on, 3 = on and disable datecheck") ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; 0 = off and warn if enabled, 1 = off, 2 = on, 3 = on and disable datecheck")
ap2.add_argument("--u2sort", metavar="TXT", type=u, default="s", help="upload order; s=smallest-first, n=alphabetical, fs=force-s, fn=force-n -- alphabetical is a bit slower on fiber/LAN but makes it easier to eyeball if everything went fine")
ap2 = ap.add_argument_group('network options') ap2 = ap.add_argument_group('network options')
ap2.add_argument("-i", metavar="IP", type=u, 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.)")
@@ -477,6 +537,9 @@ def run_argparse(argv, formatter):
ap2.add_argument("--no-lifetime", action="store_true", help="disable automatic deletion of uploads after a certain time (lifetime volflag)") 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("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js")
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, 404 on 403.\n └─Alias of\033[32m -s --no-dot-mv --no-dot-ren --unpost=0 --no-del --no-mv --hardlink --vague-403 -nih")
ap2.add_argument("-sss", action="store_true", help="further increase safety: Enable logging to disk, scan for dangerous symlinks.\n └─Alias of\033[32m -ss -lo=cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz --ls=**,*,ln,p,r")
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, help="do a sanity/safety check of all volumes on startup; arguments USER,VOL,FLAGS; example [**,*,ln,p,r]") ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, help="do a sanity/safety check of all volumes on startup; arguments USER,VOL,FLAGS; example [**,*,ln,p,r]")
ap2.add_argument("--salt", type=u, default="hunter2", help="up2k file-hash salt; used to generate unpredictable internal identifiers for uploads -- doesn't really matter") ap2.add_argument("--salt", type=u, default="hunter2", help="up2k file-hash salt; used to generate unpredictable internal identifiers for uploads -- doesn't really matter")
ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files -- this one DOES matter") ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files -- this one DOES matter")
@@ -489,9 +552,10 @@ def run_argparse(argv, formatter):
ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything") ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything")
ap2.add_argument("--logout", metavar="H", type=float, default="8086", help="logout clients after H hours of inactivity (0.0028=10sec, 0.1=6min, 24=day, 168=week, 720=month, 8760=year)") ap2.add_argument("--logout", metavar="H", type=float, default="8086", help="logout clients after H hours of inactivity (0.0028=10sec, 0.1=6min, 24=day, 168=week, 720=month, 8760=year)")
ap2 = ap.add_argument_group('yolo options') ap2 = ap.add_argument_group('shutdown options')
ap2.add_argument("--ign-ebind", action="store_true", help="continue running even if it's impossible to listen on some of the requested endpoints") ap2.add_argument("--ign-ebind", action="store_true", help="continue running even if it's impossible to listen on some of the requested endpoints")
ap2.add_argument("--ign-ebind-all", action="store_true", help="continue running even if it's impossible to receive connections at all") ap2.add_argument("--ign-ebind-all", action="store_true", help="continue running even if it's impossible to receive connections at all")
ap2.add_argument("--exit", metavar="WHEN", type=u, default="", help="shutdown after WHEN has finished; for example 'idx' will do volume indexing + metadata analysis")
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")
@@ -541,13 +605,21 @@ def run_argparse(argv, formatter):
ap2.add_argument("-e2d", action="store_true", help="enable up2k database, making files searchable + enables upload deduplocation") ap2.add_argument("-e2d", action="store_true", help="enable up2k database, making files searchable + enables upload deduplocation")
ap2.add_argument("-e2ds", action="store_true", help="scan writable folders for new files on startup; sets -e2d") ap2.add_argument("-e2ds", action="store_true", help="scan writable folders for new files on startup; sets -e2d")
ap2.add_argument("-e2dsa", action="store_true", help="scans all folders on startup; sets -e2ds") ap2.add_argument("-e2dsa", action="store_true", help="scans all folders on startup; sets -e2ds")
ap2.add_argument("-e2v", action="store_true", help="verify file integrity; rehash all files and compare with db")
ap2.add_argument("-e2vu", action="store_true", help="on hash mismatch: update the database with the new hash")
ap2.add_argument("-e2vp", action="store_true", help="on hash mismatch: panic and quit copyparty")
ap2.add_argument("--hist", metavar="PATH", type=u, help="where to store volume data (db, thumbs)") ap2.add_argument("--hist", metavar="PATH", type=u, help="where to store volume data (db, thumbs)")
ap2.add_argument("--no-hash", metavar="PTN", type=u, help="regex: disable hashing of matching paths during e2ds folder scans") ap2.add_argument("--no-hash", metavar="PTN", type=u, help="regex: disable hashing of matching paths during e2ds folder scans")
ap2.add_argument("--no-idx", metavar="PTN", type=u, help="regex: disable indexing of matching paths during e2ds folder scans") ap2.add_argument("--no-idx", metavar="PTN", type=u, help="regex: disable indexing of matching paths during e2ds folder scans")
ap2.add_argument("--no-dhash", action="store_true", help="disable rescan acceleration; do full database integrity check -- makes the db ~5%% smaller and bootup/rescans 3~10x slower")
ap2.add_argument("--xdev", action="store_true", help="do not descend into other filesystems (symlink or bind-mount to another HDD, ...)")
ap2.add_argument("--xvol", action="store_true", help="skip symlinks leaving the volume root")
ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing")
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="disk rescan volume interval, 0=off, can be set per-volume with the 'scan' volflag") 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("--db-act", metavar="SEC", type=float, default=10, help="defer any scheduled volume reindexing until SEC seconds after last db write (uploads, renames, ...)")
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline -- terminate searches running for more than SEC seconds") ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline -- terminate searches running for more than SEC seconds")
ap2.add_argument("--srch-hits", metavar="N", type=int, default=7999, help="max search results to allow clients to fetch; 125 results will be shown initially") ap2.add_argument("--srch-hits", metavar="N", type=int, default=7999, help="max search results to allow clients to fetch; 125 results will be shown initially")
ap2 = ap.add_argument_group('metadata db options') ap2 = ap.add_argument_group('metadata db options')
ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing; makes it possible to search for artist/title/codec/resolution/...") ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing; makes it possible to search for artist/title/codec/resolution/...")
ap2.add_argument("-e2ts", action="store_true", help="scan existing files on startup; sets -e2t") ap2.add_argument("-e2ts", action="store_true", help="scan existing files on startup; sets -e2t")
@@ -555,17 +627,19 @@ def run_argparse(argv, formatter):
ap2.add_argument("--no-mutagen", action="store_true", help="use FFprobe for tags instead; will catch more tags") ap2.add_argument("--no-mutagen", action="store_true", help="use FFprobe for tags instead; will catch more tags")
ap2.add_argument("--no-mtag-ff", action="store_true", help="never use FFprobe as tag reader; is probably safer") ap2.add_argument("--no-mtag-ff", action="store_true", help="never use FFprobe as tag reader; is probably safer")
ap2.add_argument("--mtag-mt", metavar="CORES", type=int, default=cores, help="num cpu cores to use for tag scanning") ap2.add_argument("--mtag-mt", metavar="CORES", type=int, default=cores, help="num cpu cores to use for tag scanning")
ap2.add_argument("--mtag-v", action="store_true", help="verbose tag scanning; print errors from mtp subprocesses and such")
ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="add/replace metadata mapping") ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="add/replace metadata mapping")
ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.)", ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.)",
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,res,.fps,ahash,vhash") default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash")
ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.)", 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") default=".vq,.aq,vc,ac,fmt,res,.fps")
ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="read tag M using program BIN to parse the file") ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="read tag M using program BIN to parse the file")
ap2 = ap.add_argument_group('ui options') ap2 = ap.add_argument_group('ui options')
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language") ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language")
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use") ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use")
ap2.add_argument("--themes", metavar="NUM", type=int, default=6, help="number of themes installed") ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="favicon text [ foreground [ background ] ], set blank to disable")
ap2.add_argument("--js-browser", metavar="L", type=u, help="URL to additional JS to include") ap2.add_argument("--js-browser", metavar="L", type=u, help="URL to additional JS to include")
ap2.add_argument("--css-browser", metavar="L", type=u, help="URL to additional CSS to include") ap2.add_argument("--css-browser", metavar="L", type=u, help="URL to additional CSS to include")
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages") ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages")
@@ -580,6 +654,7 @@ def run_argparse(argv, formatter):
ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead") 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") ap2.add_argument("--stackmon", metavar="P,S", type=u, help="write stacktrace to Path every S second")
ap2.add_argument("--log-thrs", metavar="SEC", type=float, help="list active threads every SEC") ap2.add_argument("--log-thrs", metavar="SEC", type=float, help="list active threads every SEC")
ap2.add_argument("--log-fk", metavar="REGEX", type=u, default="", help="log filekey params for files where path matches REGEX; '.' (a single dot) = all files")
# fmt: on # fmt: on
ap2 = ap.add_argument_group("help sections") ap2 = ap.add_argument_group("help sections")
@@ -597,7 +672,7 @@ def run_argparse(argv, formatter):
return ret return ret
def main(argv=None): def main(argv: Optional[list[str]] = None) -> None:
time.strptime("19970815", "%Y%m%d") # python#7980 time.strptime("19970815", "%Y%m%d") # python#7980
if WINDOWS: if WINDOWS:
os.system("rem") # enables colors os.system("rem") # enables colors
@@ -605,10 +680,17 @@ def main(argv=None):
if argv is None: if argv is None:
argv = sys.argv argv = sys.argv
desc = py_desc().replace("[", "\033[1;30m[") f = '\033[36mcopyparty v{} "\033[35m{}\033[36m" ({})\n{}\033[0;36m\n sqlite v{} | jinja2 v{} | pyftpd v{}\n\033[0m'
f = f.format(
f = '\033[36mcopyparty v{} "\033[35m{}\033[36m" ({})\n{}\033[0m\n' S_VERSION,
lprint(f.format(S_VERSION, CODENAME, S_BUILD_DT, desc)) CODENAME,
S_BUILD_DT,
py_desc().replace("[", "\033[1;30m["),
SQLITE_VER,
JINJA_VER,
PYFTPD_VER,
)
lprint(f)
ensure_locale() ensure_locale()
if HAVE_SSL: if HAVE_SSL:
@@ -619,7 +701,7 @@ def main(argv=None):
supp = args_from_cfg(v) supp = args_from_cfg(v)
argv.extend(supp) argv.extend(supp)
deprecated = [] deprecated: list[tuple[str, str]] = []
for dk, nk in deprecated: for dk, nk in deprecated:
try: try:
idx = argv.index(dk) idx = argv.index(dk)
@@ -637,21 +719,28 @@ def main(argv=None):
except: except:
pass pass
try: retry = False
al = run_argparse(argv, RiceFormatter) for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]:
except AssertionError: try:
al = run_argparse(argv, Dodge11874) al = run_argparse(argv, fmtr, retry)
except SystemExit:
raise
except:
retry = True
lprint("\n[ {} ]:\n{}\n".format(fmtr, min_ex()))
assert al
if WINDOWS and not al.keep_qem: if WINDOWS and not al.keep_qem:
try: try:
disable_quickedit() disable_quickedit()
except: except:
print("\nfailed to disable quick-edit-mode:\n" + min_ex() + "\n") lprint("\nfailed to disable quick-edit-mode:\n" + min_ex() + "\n")
if not VT100: if not VT100:
al.wintitle = "" al.wintitle = ""
nstrs = [] nstrs: list[str] = []
anymod = False anymod = False
for ostr in al.v or []: for ostr in al.v or []:
m = re_vol.match(ostr) m = re_vol.match(ostr)
@@ -702,6 +791,12 @@ def main(argv=None):
except: except:
raise Exception("invalid value for -p") raise Exception("invalid value for -p")
for arg, kname, okays in [["--u2sort", "u2sort", "s n fs fn"]]:
val = unicode(getattr(al, kname))
if val not in okays.split():
zs = "argument {} cannot be '{}'; try one of these: {}"
raise Exception(zs.format(arg, val, okays))
if HAVE_SSL: if HAVE_SSL:
if al.ssl_ver: if al.ssl_ver:
configure_ssl_ver(al) configure_ssl_ver(al)
@@ -722,7 +817,7 @@ def main(argv=None):
# signal.signal(signal.SIGINT, sighandler) # signal.signal(signal.SIGINT, sighandler)
SvcHub(al, argv, printed).run() SvcHub(al, argv, "".join(printed)).run()
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (1, 3, 0) VERSION = (1, 3, 16)
CODENAME = "god dag" CODENAME = "god dag"
BUILD_DT = (2022, 5, 22) BUILD_DT = (2022, 8, 18)
S_VERSION = ".".join(map(str, VERSION)) S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

File diff suppressed because it is too large Load Diff

View File

@@ -2,23 +2,30 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import os import os
from ..util import fsenc, fsdec, SYMTIME
from ..util import SYMTIME, fsdec, fsenc
from . import path from . import path
try:
from typing import Optional
except:
pass
_ = (path,)
# grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c # 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/.$//')" # printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')"
def chmod(p, mode): def chmod(p: str, mode: int) -> None:
return os.chmod(fsenc(p), mode) return os.chmod(fsenc(p), mode)
def listdir(p="."): def listdir(p: str = ".") -> list[str]:
return [fsdec(x) for x in os.listdir(fsenc(p))] return [fsdec(x) for x in os.listdir(fsenc(p))]
def makedirs(name, mode=0o755, exist_ok=True): def makedirs(name: str, mode: int = 0o755, exist_ok: bool = True) -> None:
bname = fsenc(name) bname = fsenc(name)
try: try:
os.makedirs(bname, mode) os.makedirs(bname, mode)
@@ -27,31 +34,33 @@ def makedirs(name, mode=0o755, exist_ok=True):
raise raise
def mkdir(p, mode=0o755): def mkdir(p: str, mode: int = 0o755) -> None:
return os.mkdir(fsenc(p), mode) return os.mkdir(fsenc(p), mode)
def rename(src, dst): def rename(src: str, dst: str) -> None:
return os.rename(fsenc(src), fsenc(dst)) return os.rename(fsenc(src), fsenc(dst))
def replace(src, dst): def replace(src: str, dst: str) -> None:
return os.replace(fsenc(src), fsenc(dst)) return os.replace(fsenc(src), fsenc(dst))
def rmdir(p): def rmdir(p: str) -> None:
return os.rmdir(fsenc(p)) return os.rmdir(fsenc(p))
def stat(p): def stat(p: str) -> os.stat_result:
return os.stat(fsenc(p)) return os.stat(fsenc(p))
def unlink(p): def unlink(p: str) -> None:
return os.unlink(fsenc(p)) return os.unlink(fsenc(p))
def utime(p, times=None, follow_symlinks=True): def utime(
p: str, times: Optional[tuple[float, float]] = None, follow_symlinks: bool = True
) -> None:
if SYMTIME: if SYMTIME:
return os.utime(fsenc(p), times, follow_symlinks=follow_symlinks) return os.utime(fsenc(p), times, follow_symlinks=follow_symlinks)
else: else:
@@ -60,7 +69,7 @@ def utime(p, times=None, follow_symlinks=True):
if hasattr(os, "lstat"): if hasattr(os, "lstat"):
def lstat(p): def lstat(p: str) -> os.stat_result:
return os.lstat(fsenc(p)) return os.lstat(fsenc(p))
else: else:

View File

@@ -2,43 +2,44 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import os import os
from ..util import fsenc, fsdec, SYMTIME
from ..util import SYMTIME, fsdec, fsenc
def abspath(p): def abspath(p: str) -> str:
return fsdec(os.path.abspath(fsenc(p))) return fsdec(os.path.abspath(fsenc(p)))
def exists(p): def exists(p: str) -> bool:
return os.path.exists(fsenc(p)) return os.path.exists(fsenc(p))
def getmtime(p, follow_symlinks=True): def getmtime(p: str, follow_symlinks: bool = True) -> float:
if not follow_symlinks and SYMTIME: if not follow_symlinks and SYMTIME:
return os.lstat(fsenc(p)).st_mtime return os.lstat(fsenc(p)).st_mtime
else: else:
return os.path.getmtime(fsenc(p)) return os.path.getmtime(fsenc(p))
def getsize(p): def getsize(p: str) -> int:
return os.path.getsize(fsenc(p)) return os.path.getsize(fsenc(p))
def isfile(p): def isfile(p: str) -> bool:
return os.path.isfile(fsenc(p)) return os.path.isfile(fsenc(p))
def isdir(p): def isdir(p: str) -> bool:
return os.path.isdir(fsenc(p)) return os.path.isdir(fsenc(p))
def islink(p): def islink(p: str) -> bool:
return os.path.islink(fsenc(p)) return os.path.islink(fsenc(p))
def lexists(p): def lexists(p: str) -> bool:
return os.path.lexists(fsenc(p)) return os.path.lexists(fsenc(p))
def realpath(p): def realpath(p: str) -> str:
return fsdec(os.path.realpath(fsenc(p))) return fsdec(os.path.realpath(fsenc(p)))

View File

@@ -1,37 +1,56 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import time
import threading import threading
import time
from .broker_util import try_exec import queue
from .__init__ import TYPE_CHECKING
from .broker_mpw import MpWorker from .broker_mpw import MpWorker
from .broker_util import try_exec
from .util import mp from .util import mp
if TYPE_CHECKING:
from .svchub import SvcHub
try:
from typing import Any
except:
pass
class MProcess(mp.Process):
def __init__(
self,
q_pend: queue.Queue[tuple[int, str, list[Any]]],
q_yield: queue.Queue[tuple[int, str, list[Any]]],
target: Any,
args: Any,
) -> None:
super(MProcess, self).__init__(target=target, args=args)
self.q_pend = q_pend
self.q_yield = q_yield
class BrokerMp(object): class BrokerMp(object):
"""external api; manages MpWorkers""" """external api; manages MpWorkers"""
def __init__(self, hub): def __init__(self, hub: "SvcHub") -> None:
self.hub = hub self.hub = hub
self.log = hub.log self.log = hub.log
self.args = hub.args self.args = hub.args
self.procs = [] self.procs = []
self.retpend = {}
self.retpend_mutex = threading.Lock()
self.mutex = threading.Lock() self.mutex = threading.Lock()
self.num_workers = self.args.j or mp.cpu_count() self.num_workers = self.args.j or mp.cpu_count()
self.log("broker", "booting {} subprocesses".format(self.num_workers)) self.log("broker", "booting {} subprocesses".format(self.num_workers))
for n in range(1, self.num_workers + 1): for n in range(1, self.num_workers + 1):
q_pend = mp.Queue(1) q_pend: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(1)
q_yield = mp.Queue(64) q_yield: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(64)
proc = mp.Process(target=MpWorker, args=(q_pend, q_yield, self.args, n)) proc = MProcess(q_pend, q_yield, MpWorker, (q_pend, q_yield, self.args, n))
proc.q_pend = q_pend
proc.q_yield = q_yield
proc.clients = {}
thr = threading.Thread( thr = threading.Thread(
target=self.collector, args=(proc,), name="mp-sink-{}".format(n) target=self.collector, args=(proc,), name="mp-sink-{}".format(n)
@@ -42,11 +61,11 @@ class BrokerMp(object):
self.procs.append(proc) self.procs.append(proc)
proc.start() proc.start()
def shutdown(self): def shutdown(self) -> None:
self.log("broker", "shutting down") self.log("broker", "shutting down")
for n, proc in enumerate(self.procs): for n, proc in enumerate(self.procs):
thr = threading.Thread( thr = threading.Thread(
target=proc.q_pend.put([0, "shutdown", []]), target=proc.q_pend.put((0, "shutdown", [])),
name="mp-shutdown-{}-{}".format(n, len(self.procs)), name="mp-shutdown-{}-{}".format(n, len(self.procs)),
) )
thr.start() thr.start()
@@ -62,12 +81,12 @@ class BrokerMp(object):
procs.pop() procs.pop()
def reload(self): def reload(self) -> None:
self.log("broker", "reloading") self.log("broker", "reloading")
for _, proc in enumerate(self.procs): for _, proc in enumerate(self.procs):
proc.q_pend.put([0, "reload", []]) proc.q_pend.put((0, "reload", []))
def collector(self, proc): def collector(self, proc: MProcess) -> None:
"""receive message from hub in other process""" """receive message from hub in other process"""
while True: while True:
msg = proc.q_yield.get() msg = proc.q_yield.get()
@@ -78,10 +97,7 @@ class BrokerMp(object):
elif dest == "retq": elif dest == "retq":
# response from previous ipc call # response from previous ipc call
with self.retpend_mutex: raise Exception("invalid broker_mp usage")
retq = self.retpend.pop(retq_id)
retq.put(args)
else: else:
# new ipc invoking managed service in hub # new ipc invoking managed service in hub
@@ -93,9 +109,9 @@ class BrokerMp(object):
rv = try_exec(retq_id, obj, *args) rv = try_exec(retq_id, obj, *args)
if retq_id: if retq_id:
proc.q_pend.put([retq_id, "retq", rv]) proc.q_pend.put((retq_id, "retq", rv))
def put(self, want_retval, dest, *args): def say(self, dest: str, *args: Any) -> None:
""" """
send message to non-hub component in other process, send message to non-hub component in other process,
returns a Queue object which eventually contains the response if want_retval returns a Queue object which eventually contains the response if want_retval
@@ -103,7 +119,7 @@ class BrokerMp(object):
""" """
if dest == "listen": if dest == "listen":
for p in self.procs: for p in self.procs:
p.q_pend.put([0, dest, [args[0], len(self.procs)]]) p.q_pend.put((0, dest, [args[0], len(self.procs)]))
elif dest == "cb_httpsrv_up": elif dest == "cb_httpsrv_up":
self.hub.cb_httpsrv_up() self.hub.cb_httpsrv_up()

View File

@@ -1,20 +1,38 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import sys import argparse
import signal import signal
import sys
import threading import threading
from .broker_util import ExceptionalQueue import queue
from .authsrv import AuthSrv
from .broker_util import BrokerCli, ExceptionalQueue
from .httpsrv import HttpSrv from .httpsrv import HttpSrv
from .util import FAKE_MP from .util import FAKE_MP
from copyparty.authsrv import AuthSrv
try:
from types import FrameType
from typing import Any, Optional, Union
except:
pass
class MpWorker(object): class MpWorker(BrokerCli):
"""one single mp instance""" """one single mp instance"""
def __init__(self, q_pend, q_yield, args, n): def __init__(
self,
q_pend: queue.Queue[tuple[int, str, list[Any]]],
q_yield: queue.Queue[tuple[int, str, list[Any]]],
args: argparse.Namespace,
n: int,
) -> None:
super(MpWorker, self).__init__()
self.q_pend = q_pend self.q_pend = q_pend
self.q_yield = q_yield self.q_yield = q_yield
self.args = args self.args = args
@@ -22,7 +40,7 @@ class MpWorker(object):
self.log = self._log_disabled if args.q and not args.lo else self._log_enabled self.log = self._log_disabled if args.q and not args.lo else self._log_enabled
self.retpend = {} self.retpend: dict[int, Any] = {}
self.retpend_mutex = threading.Lock() self.retpend_mutex = threading.Lock()
self.mutex = threading.Lock() self.mutex = threading.Lock()
@@ -45,20 +63,20 @@ class MpWorker(object):
thr.start() thr.start()
thr.join() thr.join()
def signal_handler(self, sig, frame): def signal_handler(self, sig: Optional[int], frame: Optional[FrameType]) -> None:
# print('k') # print('k')
pass pass
def _log_enabled(self, src, msg, c=0): def _log_enabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None:
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): def _log_disabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None:
pass pass
def logw(self, msg, c=0): def logw(self, msg: str, c: Union[int, str] = 0) -> None:
self.log("mp{}".format(self.n), msg, c) self.log("mp{}".format(self.n), msg, c)
def main(self): def main(self) -> None:
while True: while True:
retq_id, dest, args = self.q_pend.get() retq_id, dest, args = self.q_pend.get()
@@ -87,15 +105,14 @@ class MpWorker(object):
else: else:
raise Exception("what is " + str(dest)) raise Exception("what is " + str(dest))
def put(self, want_retval, dest, *args): def ask(self, dest: str, *args: Any) -> ExceptionalQueue:
if want_retval: retq = ExceptionalQueue(1)
retq = ExceptionalQueue(1) retq_id = id(retq)
retq_id = id(retq) with self.retpend_mutex:
with self.retpend_mutex: self.retpend[retq_id] = retq
self.retpend[retq_id] = retq
else:
retq = None
retq_id = 0
self.q_yield.put([retq_id, dest, args]) self.q_yield.put((retq_id, dest, list(args)))
return retq return retq
def say(self, dest: str, *args: Any) -> None:
self.q_yield.put((0, dest, list(args)))

View File

@@ -3,14 +3,25 @@ from __future__ import print_function, unicode_literals
import threading import threading
from .__init__ import TYPE_CHECKING
from .broker_util import BrokerCli, ExceptionalQueue, try_exec
from .httpsrv import HttpSrv from .httpsrv import HttpSrv
from .broker_util import ExceptionalQueue, try_exec
if TYPE_CHECKING:
from .svchub import SvcHub
try:
from typing import Any
except:
pass
class BrokerThr(object): class BrokerThr(BrokerCli):
"""external api; behaves like BrokerMP but using plain threads""" """external api; behaves like BrokerMP but using plain threads"""
def __init__(self, hub): def __init__(self, hub: "SvcHub") -> None:
super(BrokerThr, self).__init__()
self.hub = hub self.hub = hub
self.log = hub.log self.log = hub.log
self.args = hub.args self.args = hub.args
@@ -23,29 +34,35 @@ class BrokerThr(object):
self.httpsrv = HttpSrv(self, None) self.httpsrv = HttpSrv(self, None)
self.reload = self.noop self.reload = self.noop
def shutdown(self): def shutdown(self) -> None:
# self.log("broker", "shutting down") # self.log("broker", "shutting down")
self.httpsrv.shutdown() self.httpsrv.shutdown()
def noop(self): def noop(self) -> None:
pass pass
def put(self, want_retval, dest, *args): def ask(self, dest: str, *args: Any) -> ExceptionalQueue:
# new ipc invoking managed service in hub
obj = self.hub
for node in dest.split("."):
obj = getattr(obj, node)
rv = try_exec(True, obj, *args)
# pretend we're broker_mp
retq = ExceptionalQueue(1)
retq.put(rv)
return retq
def say(self, dest: str, *args: Any) -> None:
if dest == "listen": if dest == "listen":
self.httpsrv.listen(args[0], 1) self.httpsrv.listen(args[0], 1)
return
else: # new ipc invoking managed service in hub
# new ipc invoking managed service in hub obj = self.hub
obj = self.hub for node in dest.split("."):
for node in dest.split("."): obj = getattr(obj, node)
obj = getattr(obj, node)
# TODO will deadlock if dest performs another ipc try_exec(False, obj, *args)
rv = try_exec(want_retval, obj, *args)
if not want_retval:
return
# pretend we're broker_mp
retq = ExceptionalQueue(1)
retq.put(rv)
return retq

View File

@@ -1,17 +1,30 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import argparse
import traceback import traceback
from .util import Pebkac, Queue from queue import Queue
from .__init__ import TYPE_CHECKING
from .authsrv import AuthSrv
from .util import Pebkac
try:
from typing import Any, Optional, Union
from .util import RootLogger
except:
pass
if TYPE_CHECKING:
from .httpsrv import HttpSrv
class ExceptionalQueue(Queue, object): class ExceptionalQueue(Queue, object):
def get(self, block=True, timeout=None): def get(self, block: bool = True, timeout: Optional[float] = None) -> Any:
rv = super(ExceptionalQueue, self).get(block, timeout) rv = super(ExceptionalQueue, self).get(block, timeout)
# TODO: how expensive is this?
if isinstance(rv, list): if isinstance(rv, list):
if rv[0] == "exception": if rv[0] == "exception":
if rv[1] == "pebkac": if rv[1] == "pebkac":
@@ -22,7 +35,26 @@ class ExceptionalQueue(Queue, object):
return rv return rv
def try_exec(want_retval, func, *args): class BrokerCli(object):
"""
helps mypy understand httpsrv.broker but still fails a few levels deeper,
for example resolving httpconn.* in httpcli -- see lines tagged #mypy404
"""
def __init__(self) -> None:
self.log: "RootLogger" = None
self.args: argparse.Namespace = None
self.asrv: AuthSrv = None
self.httpsrv: "HttpSrv" = None
def ask(self, dest: str, *args: Any) -> ExceptionalQueue:
return ExceptionalQueue(1)
def say(self, dest: str, *args: Any) -> None:
pass
def try_exec(want_retval: Union[bool, int], func: Any, *args: list[Any]) -> Any:
try: try:
return func(*args) return func(*args)

154
copyparty/fsutil.py Normal file
View File

@@ -0,0 +1,154 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
import os
import re
import time
from .__init__ import ANYWIN, MACOS
from .authsrv import AXS, VFS
from .bos import bos
from .util import chkcmd, min_ex
try:
from typing import Optional, Union
from .util import RootLogger
except:
pass
class Fstab(object):
def __init__(self, log: "RootLogger"):
self.log_func = log
self.trusted = False
self.tab: Optional[VFS] = None
self.cache: dict[str, str] = {}
self.age = 0.0
def log(self, msg: str, c: Union[int, str] = 0) -> None:
self.log_func("fstab", msg + "\033[K", c)
def get(self, path: str) -> str:
if len(self.cache) > 9000:
self.age = time.time()
self.tab = None
self.cache = {}
fs = "ext4"
msg = "failed to determine filesystem at [{}]; assuming {}\n{}"
if ANYWIN:
fs = "vfat"
try:
path = self._winpath(path)
except:
self.log(msg.format(path, fs, min_ex()), 3)
return fs
path = path.lstrip("/")
try:
return self.cache[path]
except:
pass
try:
fs = self.get_w32(path) if ANYWIN else self.get_unix(path)
except:
self.log(msg.format(path, fs, min_ex()), 3)
fs = fs.lower()
self.cache[path] = fs
self.log("found {} at {}".format(fs, path))
return fs
def _winpath(self, path: str) -> str:
# try to combine volume-label + st_dev (vsn)
path = path.replace("/", "\\")
vid = path.split(":", 1)[0].strip("\\").split("\\", 1)[0]
try:
return "{}*{}".format(vid, bos.stat(path).st_dev)
except:
return vid
def build_fallback(self) -> None:
self.tab = VFS(self.log_func, "idk", "/", AXS(), {})
self.trusted = False
def build_tab(self) -> None:
self.log("building tab")
sptn = r"^.*? on (.*) type ([^ ]+) \(.*"
if MACOS:
sptn = r"^.*? on (.*) \(([^ ]+), .*"
ptn = re.compile(sptn)
so, _ = chkcmd(["mount"])
tab1: list[tuple[str, str]] = []
for ln in so.split("\n"):
m = ptn.match(ln)
if not m:
continue
zs1, zs2 = m.groups()
tab1.append((str(zs1), str(zs2)))
tab1.sort(key=lambda x: (len(x[0]), x[0]))
path1, fs1 = tab1[0]
tab = VFS(self.log_func, fs1, path1, AXS(), {})
for path, fs in tab1[1:]:
tab.add(fs, path.lstrip("/"))
self.tab = tab
def relabel(self, path: str, nval: str) -> None:
assert self.tab
self.cache = {}
if ANYWIN:
path = self._winpath(path)
path = path.lstrip("/")
ptn = re.compile(r"^[^\\/]*")
vn, rem = self.tab._find(path)
if not self.trusted:
# no mtab access; have to build as we go
if "/" in rem:
self.tab.add("idk", os.path.join(vn.vpath, rem.split("/")[0]))
if rem:
self.tab.add(nval, path)
else:
vn.realpath = nval
return
visit = [vn]
while visit:
vn = visit.pop()
vn.realpath = ptn.sub(nval, vn.realpath)
visit.extend(list(vn.nodes.values()))
def get_unix(self, path: str) -> str:
if not self.tab:
try:
self.build_tab()
self.trusted = True
except:
# prisonparty or other restrictive environment
self.log("failed to build tab:\n{}".format(min_ex()), 3)
self.build_fallback()
assert self.tab
ret = self.tab._find(path)[0]
if self.trusted or path == ret.vpath:
return ret.realpath.split("/")[0]
else:
return "idk"
def get_w32(self, path: str) -> str:
if not self.tab:
self.build_fallback()
assert self.tab
ret = self.tab._find(path)[0]
return ret.realpath

View File

@@ -1,16 +1,23 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import os import argparse
import sys
import stat
import time
import logging import logging
import os
import stat
import sys
import threading import threading
import time
from .__init__ import E, PY2 from pyftpdlib.authorizers import AuthenticationFailed, DummyAuthorizer
from .util import Pebkac, fsenc, exclude_dotfiles from pyftpdlib.filesystems import AbstractedFS, FilesystemError
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.log import config_logging
from pyftpdlib.servers import FTPServer
from .__init__ import PY2, TYPE_CHECKING, E
from .bos import bos from .bos import bos
from .util import Pebkac, exclude_dotfiles, fsenc
try: try:
from pyftpdlib.ioloop import IOLoop from pyftpdlib.ioloop import IOLoop
@@ -20,65 +27,64 @@ except ImportError:
sys.path.append(p) sys.path.append(p)
from pyftpdlib.ioloop import IOLoop from pyftpdlib.ioloop import IOLoop
from pyftpdlib.authorizers import DummyAuthorizer, AuthenticationFailed
from pyftpdlib.filesystems import AbstractedFS, FilesystemError
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer
from pyftpdlib.log import config_logging
if TYPE_CHECKING:
from .svchub import SvcHub
try: try:
from typing import TYPE_CHECKING import typing
from typing import Any, Optional
if TYPE_CHECKING: except:
from .svchub import SvcHub
except ImportError:
pass pass
class FtpAuth(DummyAuthorizer): class FtpAuth(DummyAuthorizer):
def __init__(self): def __init__(self, hub: "SvcHub") -> None:
super(FtpAuth, self).__init__() super(FtpAuth, self).__init__()
self.hub = None # type: SvcHub self.hub = hub
def validate_authentication(self, username, password, handler): def validate_authentication(
self, username: str, password: str, handler: Any
) -> None:
asrv = self.hub.asrv asrv = self.hub.asrv
if username == "anonymous": if username == "anonymous":
password = "" password = ""
uname = "*" uname = "*"
if password: if password:
uname = asrv.iacct.get(password, None) uname = asrv.iacct.get(password, "")
handler.username = uname handler.username = uname
if password and not uname: if password and not uname:
raise AuthenticationFailed("Authentication failed.") raise AuthenticationFailed("Authentication failed.")
def get_home_dir(self, username): def get_home_dir(self, username: str) -> str:
return "/" return "/"
def has_user(self, username): def has_user(self, username: str) -> bool:
asrv = self.hub.asrv asrv = self.hub.asrv
return username in asrv.acct return username in asrv.acct
def has_perm(self, username, perm, path=None): def has_perm(self, username: str, perm: int, path: Optional[str] = None) -> bool:
return True # handled at filesystem layer return True # handled at filesystem layer
def get_perms(self, username): def get_perms(self, username: str) -> str:
return "elradfmwMT" return "elradfmwMT"
def get_msg_login(self, username): def get_msg_login(self, username: str) -> str:
return "sup {}".format(username) return "sup {}".format(username)
def get_msg_quit(self, username): def get_msg_quit(self, username: str) -> str:
return "cya" return "cya"
class FtpFs(AbstractedFS): class FtpFs(AbstractedFS):
def __init__(self, root, cmd_channel): def __init__(
self, root: str, cmd_channel: Any
) -> None: # pylint: disable=super-init-not-called
self.h = self.cmd_channel = cmd_channel # type: FTPHandler self.h = self.cmd_channel = cmd_channel # type: FTPHandler
self.hub = cmd_channel.hub # type: SvcHub self.hub: "SvcHub" = cmd_channel.hub
self.args = cmd_channel.args self.args = cmd_channel.args
self.uname = self.hub.asrv.iacct.get(cmd_channel.password, "*") self.uname = self.hub.asrv.iacct.get(cmd_channel.password, "*")
@@ -89,7 +95,14 @@ class FtpFs(AbstractedFS):
self.listdirinfo = self.listdir self.listdirinfo = self.listdir
self.chdir(".") self.chdir(".")
def v2a(self, vpath, r=False, w=False, m=False, d=False): def v2a(
self,
vpath: str,
r: bool = False,
w: bool = False,
m: bool = False,
d: bool = False,
) -> str:
try: try:
vpath = vpath.replace("\\", "/").lstrip("/") vpath = vpath.replace("\\", "/").lstrip("/")
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d) vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
@@ -100,25 +113,32 @@ class FtpFs(AbstractedFS):
except Pebkac as ex: except Pebkac as ex:
raise FilesystemError(str(ex)) raise FilesystemError(str(ex))
def rv2a(self, vpath, r=False, w=False, m=False, d=False): def rv2a(
self,
vpath: str,
r: bool = False,
w: bool = False,
m: bool = False,
d: bool = False,
) -> str:
return self.v2a(os.path.join(self.cwd, vpath), r, w, m, d) return self.v2a(os.path.join(self.cwd, vpath), r, w, m, d)
def ftp2fs(self, ftppath): def ftp2fs(self, ftppath: str) -> str:
# return self.v2a(ftppath) # return self.v2a(ftppath)
return ftppath # self.cwd must be vpath return ftppath # self.cwd must be vpath
def fs2ftp(self, fspath): def fs2ftp(self, fspath: str) -> str:
# raise NotImplementedError() # raise NotImplementedError()
return fspath return fspath
def validpath(self, path): def validpath(self, path: str) -> bool:
if "/.hist/" in path: if "/.hist/" in path:
if "/up2k." in path or path.endswith("/dir.txt"): if "/up2k." in path or path.endswith("/dir.txt"):
raise FilesystemError("access to this file is forbidden") raise FilesystemError("access to this file is forbidden")
return True return True
def open(self, filename, mode): def open(self, filename: str, mode: str) -> typing.IO[Any]:
r = "r" in mode r = "r" in mode
w = "w" in mode or "a" in mode or "+" in mode w = "w" in mode or "a" in mode or "+" in mode
@@ -129,24 +149,24 @@ class FtpFs(AbstractedFS):
self.validpath(ap) self.validpath(ap)
return open(fsenc(ap), mode) return open(fsenc(ap), mode)
def chdir(self, path): def chdir(self, path: str) -> None:
self.cwd = join(self.cwd, path) self.cwd = join(self.cwd, path)
x = self.hub.asrv.vfs.can_access(self.cwd.lstrip("/"), self.h.username) x = self.hub.asrv.vfs.can_access(self.cwd.lstrip("/"), self.h.username)
self.can_read, self.can_write, self.can_move, self.can_delete, self.can_get = x self.can_read, self.can_write, self.can_move, self.can_delete, self.can_get = x
def mkdir(self, path): def mkdir(self, path: str) -> None:
ap = self.rv2a(path, w=True) ap = self.rv2a(path, w=True)
bos.mkdir(ap) bos.mkdir(ap)
def listdir(self, path): def listdir(self, path: str) -> list[str]:
vpath = join(self.cwd, path).lstrip("/") vpath = join(self.cwd, path).lstrip("/")
try: try:
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, True, False) vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, True, False)
fsroot, vfs_ls, vfs_virt = vfs.ls( fsroot, vfs_ls1, vfs_virt = vfs.ls(
rem, self.uname, not self.args.no_scandir, [[True], [False, True]] rem, self.uname, not self.args.no_scandir, [[True], [False, True]]
) )
vfs_ls = [x[0] for x in vfs_ls] vfs_ls = [x[0] for x in vfs_ls1]
vfs_ls.extend(vfs_virt.keys()) vfs_ls.extend(vfs_virt.keys())
if not self.args.ed: if not self.args.ed:
@@ -154,7 +174,7 @@ class FtpFs(AbstractedFS):
vfs_ls.sort() vfs_ls.sort()
return vfs_ls return vfs_ls
except Exception as ex: except:
if vpath: if vpath:
# display write-only folders as empty # display write-only folders as empty
return [] return []
@@ -163,44 +183,39 @@ class FtpFs(AbstractedFS):
r = {x.split("/")[0]: 1 for x in self.hub.asrv.vfs.all_vols.keys()} r = {x.split("/")[0]: 1 for x in self.hub.asrv.vfs.all_vols.keys()}
return list(sorted(list(r.keys()))) return list(sorted(list(r.keys())))
def rmdir(self, path): def rmdir(self, path: str) -> None:
ap = self.rv2a(path, d=True) ap = self.rv2a(path, d=True)
bos.rmdir(ap) bos.rmdir(ap)
def remove(self, path): def remove(self, path: str) -> None:
if self.args.no_del: if self.args.no_del:
raise FilesystemError("the delete feature is disabled in server config") raise FilesystemError("the delete feature is disabled in server config")
vp = join(self.cwd, path).lstrip("/") vp = join(self.cwd, path).lstrip("/")
x = self.hub.broker.put(
True, "up2k.handle_rm", self.uname, self.h.remote_ip, [vp]
)
try: try:
x.get() self.hub.up2k.handle_rm(self.uname, self.h.remote_ip, [vp])
except Exception as ex: except Exception as ex:
raise FilesystemError(str(ex)) raise FilesystemError(str(ex))
def rename(self, src, dst): def rename(self, src: str, dst: str) -> None:
if not self.can_move: if not self.can_move:
raise FilesystemError("not allowed for user " + self.h.username) raise FilesystemError("not allowed for user " + self.h.username)
if self.args.no_mv: if self.args.no_mv:
m = "the rename/move feature is disabled in server config" t = "the rename/move feature is disabled in server config"
raise FilesystemError(m) raise FilesystemError(t)
svp = join(self.cwd, src).lstrip("/") svp = join(self.cwd, src).lstrip("/")
dvp = join(self.cwd, dst).lstrip("/") dvp = join(self.cwd, dst).lstrip("/")
x = self.hub.broker.put(True, "up2k.handle_mv", self.uname, svp, dvp)
try: try:
x.get() self.hub.up2k.handle_mv(self.uname, svp, dvp)
except Exception as ex: except Exception as ex:
raise FilesystemError(str(ex)) raise FilesystemError(str(ex))
def chmod(self, path, mode): def chmod(self, path: str, mode: str) -> None:
pass pass
def stat(self, path): def stat(self, path: str) -> os.stat_result:
try: try:
ap = self.rv2a(path, r=True) ap = self.rv2a(path, r=True)
return bos.stat(ap) return bos.stat(ap)
@@ -212,64 +227,70 @@ class FtpFs(AbstractedFS):
return st return st
def utime(self, path, timeval): def utime(self, path: str, timeval: float) -> None:
ap = self.rv2a(path, w=True) ap = self.rv2a(path, w=True)
return bos.utime(ap, (timeval, timeval)) return bos.utime(ap, (timeval, timeval))
def lstat(self, path): def lstat(self, path: str) -> os.stat_result:
ap = self.rv2a(path) ap = self.rv2a(path)
return bos.lstat(ap) return bos.lstat(ap)
def isfile(self, path): def isfile(self, path: str) -> bool:
st = self.stat(path) st = self.stat(path)
return stat.S_ISREG(st.st_mode) return stat.S_ISREG(st.st_mode)
def islink(self, path): def islink(self, path: str) -> bool:
ap = self.rv2a(path) ap = self.rv2a(path)
return bos.path.islink(ap) return bos.path.islink(ap)
def isdir(self, path): def isdir(self, path: str) -> bool:
try: try:
st = self.stat(path) st = self.stat(path)
return stat.S_ISDIR(st.st_mode) return stat.S_ISDIR(st.st_mode)
except: except:
return True return True
def getsize(self, path): def getsize(self, path: str) -> int:
ap = self.rv2a(path) ap = self.rv2a(path)
return bos.path.getsize(ap) return bos.path.getsize(ap)
def getmtime(self, path): def getmtime(self, path: str) -> float:
ap = self.rv2a(path) ap = self.rv2a(path)
return bos.path.getmtime(ap) return bos.path.getmtime(ap)
def realpath(self, path): def realpath(self, path: str) -> str:
return path return path
def lexists(self, path): def lexists(self, path: str) -> bool:
ap = self.rv2a(path) ap = self.rv2a(path)
return bos.path.lexists(ap) return bos.path.lexists(ap)
def get_user_by_uid(self, uid): def get_user_by_uid(self, uid: int) -> str:
return "root" return "root"
def get_group_by_uid(self, gid): def get_group_by_uid(self, gid: int) -> str:
return "root" return "root"
class FtpHandler(FTPHandler): class FtpHandler(FTPHandler):
abstracted_fs = FtpFs abstracted_fs = FtpFs
hub: "SvcHub" = None
args: argparse.Namespace = None
def __init__(self, conn: Any, server: Any, ioloop: Any = None) -> None:
self.hub: "SvcHub" = FtpHandler.hub
self.args: argparse.Namespace = FtpHandler.args
def __init__(self, conn, server, ioloop=None):
if PY2: if PY2:
FTPHandler.__init__(self, conn, server, ioloop) FTPHandler.__init__(self, conn, server, ioloop)
else: else:
super(FtpHandler, self).__init__(conn, server, ioloop) super(FtpHandler, self).__init__(conn, server, ioloop)
# abspath->vpath mapping to resolve log_transfer paths # abspath->vpath mapping to resolve log_transfer paths
self.vfs_map = {} self.vfs_map: dict[str, str] = {}
def ftp_STOR(self, file, mode="w"): def ftp_STOR(self, file: str, mode: str = "w") -> Any:
# Optional[str]
vp = join(self.fs.cwd, file).lstrip("/") vp = join(self.fs.cwd, file).lstrip("/")
ap = self.fs.v2a(vp) ap = self.fs.v2a(vp)
self.vfs_map[ap] = vp self.vfs_map[ap] = vp
@@ -278,7 +299,16 @@ class FtpHandler(FTPHandler):
# print("ftp_STOR: {} {} OK".format(vp, mode)) # print("ftp_STOR: {} {} OK".format(vp, mode))
return ret return ret
def log_transfer(self, cmd, filename, receive, completed, elapsed, bytes): def log_transfer(
self,
cmd: str,
filename: bytes,
receive: bool,
completed: bool,
elapsed: float,
bytes: int,
) -> Any:
# None
ap = filename.decode("utf-8", "replace") ap = filename.decode("utf-8", "replace")
vp = self.vfs_map.pop(ap, None) vp = self.vfs_map.pop(ap, None)
# print("xfer_end: {} => {}".format(ap, vp)) # print("xfer_end: {} => {}".format(ap, vp))
@@ -286,9 +316,7 @@ class FtpHandler(FTPHandler):
vp, fn = os.path.split(vp) vp, fn = os.path.split(vp)
vfs, rem = self.hub.asrv.vfs.get(vp, self.username, False, True) vfs, rem = self.hub.asrv.vfs.get(vp, self.username, False, True)
vfs, rem = vfs.get_dbv(rem) vfs, rem = vfs.get_dbv(rem)
self.hub.broker.put( self.hub.up2k.hash_file(
False,
"up2k.hash_file",
vfs.realpath, vfs.realpath,
vfs.flags, vfs.flags,
rem, rem,
@@ -313,7 +341,7 @@ except:
class Ftpd(object): class Ftpd(object):
def __init__(self, hub): def __init__(self, hub: "SvcHub") -> None:
self.hub = hub self.hub = hub
self.args = hub.args self.args = hub.args
@@ -322,24 +350,23 @@ class Ftpd(object):
hs.append([FtpHandler, self.args.ftp]) hs.append([FtpHandler, self.args.ftp])
if self.args.ftps: if self.args.ftps:
try: try:
h = SftpHandler h1 = SftpHandler
except: except:
m = "\nftps requires pyopenssl;\nplease run the following:\n\n {} -m pip install --user pyopenssl\n" t = "\nftps requires pyopenssl;\nplease run the following:\n\n {} -m pip install --user pyopenssl\n"
print(m.format(sys.executable)) print(t.format(sys.executable))
sys.exit(1) sys.exit(1)
h.certfile = os.path.join(E.cfg, "cert.pem") h1.certfile = os.path.join(E.cfg, "cert.pem")
h.tls_control_required = True h1.tls_control_required = True
h.tls_data_required = True h1.tls_data_required = True
hs.append([h, self.args.ftps]) hs.append([h1, self.args.ftps])
for h in hs: for h_lp in hs:
h, lp = h h2, lp = h_lp
h.hub = hub h2.hub = hub
h.args = hub.args h2.args = hub.args
h.authorizer = FtpAuth() h2.authorizer = FtpAuth(hub)
h.authorizer.hub = hub
if self.args.ftp_pr: if self.args.ftp_pr:
p1, p2 = [int(x) for x in self.args.ftp_pr.split("-")] p1, p2 = [int(x) for x in self.args.ftp_pr.split("-")]
@@ -351,10 +378,10 @@ class Ftpd(object):
else: else:
p1 += d + 1 p1 += d + 1
h.passive_ports = list(range(p1, p2 + 1)) h2.passive_ports = list(range(p1, p2 + 1))
if self.args.ftp_nat: if self.args.ftp_nat:
h.masquerade_address = self.args.ftp_nat h2.masquerade_address = self.args.ftp_nat
if self.args.ftp_dbg: if self.args.ftp_dbg:
config_logging(level=logging.DEBUG) config_logging(level=logging.DEBUG)
@@ -364,11 +391,11 @@ class Ftpd(object):
for h, lp in hs: for h, lp in hs:
FTPServer((ip, int(lp)), h, ioloop) FTPServer((ip, int(lp)), h, ioloop)
t = threading.Thread(target=ioloop.loop) thr = threading.Thread(target=ioloop.loop, name="ftp")
t.daemon = True thr.daemon = True
t.start() thr.start()
def join(p1, p2): def join(p1: str, p2: str) -> str:
w = os.path.join(p1, p2.replace("\\", "/")) w = os.path.join(p1, p2.replace("\\", "/"))
return os.path.normpath(w).replace("\\", "/") return os.path.normpath(w).replace("\\", "/")

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,36 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import re import argparse # typechk
import os import os
import time import re
import socket import socket
import threading # typechk
import time
HAVE_SSL = True
try: try:
HAVE_SSL = True
import ssl import ssl
except: except:
HAVE_SSL = False HAVE_SSL = False
from .__init__ import E from . import util as Util
from .util import Unrecv from .__init__ import TYPE_CHECKING, E
from .authsrv import AuthSrv # typechk
from .httpcli import HttpCli from .httpcli import HttpCli
from .u2idx import U2idx from .ico import Ico
from .mtag import HAVE_FFMPEG
from .th_cli import ThumbCli from .th_cli import ThumbCli
from .th_srv import HAVE_PIL, HAVE_VIPS from .th_srv import HAVE_PIL, HAVE_VIPS
from .mtag import HAVE_FFMPEG from .u2idx import U2idx
from .ico import Ico
try:
from typing import Optional, Pattern, Union
except:
pass
if TYPE_CHECKING:
from .httpsrv import HttpSrv
class HttpConn(object): class HttpConn(object):
@@ -28,31 +39,37 @@ class HttpConn(object):
creates an HttpCli for each request (Connection: Keep-Alive) creates an HttpCli for each request (Connection: Keep-Alive)
""" """
def __init__(self, sck, addr, hsrv): def __init__(
self, sck: socket.socket, addr: tuple[str, int], hsrv: "HttpSrv"
) -> None:
self.s = sck self.s = sck
self.sr: Optional[Util._Unrecv] = None
self.addr = addr self.addr = addr
self.hsrv = hsrv self.hsrv = hsrv
self.mutex = hsrv.mutex self.mutex: threading.Lock = hsrv.mutex # mypy404
self.args = hsrv.args self.args: argparse.Namespace = hsrv.args # mypy404
self.asrv = hsrv.asrv self.asrv: AuthSrv = hsrv.asrv # mypy404
self.cert_path = hsrv.cert_path self.cert_path = hsrv.cert_path
self.u2fh = hsrv.u2fh self.u2fh: Util.FHC = hsrv.u2fh # mypy404
enth = (HAVE_PIL or HAVE_VIPS or HAVE_FFMPEG) and not self.args.no_thumb enth = (HAVE_PIL or HAVE_VIPS or HAVE_FFMPEG) and not self.args.no_thumb
self.thumbcli = ThumbCli(hsrv) if enth else None self.thumbcli: Optional[ThumbCli] = ThumbCli(hsrv) if enth else None # mypy404
self.ico = Ico(self.args) self.ico: Ico = Ico(self.args) # mypy404
self.t0 = time.time() self.t0: float = time.time() # mypy404
self.stopping = False self.stopping = False
self.nreq = 0 self.nreq: int = 0 # mypy404
self.nbyte = 0 self.nbyte: int = 0 # mypy404
self.u2idx = None self.u2idx: Optional[U2idx] = None
self.log_func = hsrv.log self.log_func: "Util.RootLogger" = hsrv.log # mypy404
self.lf_url = re.compile(self.args.lf_url) if self.args.lf_url else None self.log_src: str = "httpconn" # mypy404
self.lf_url: Optional[Pattern[str]] = (
re.compile(self.args.lf_url) if self.args.lf_url else None
) # mypy404
self.set_rproxy() self.set_rproxy()
def shutdown(self): def shutdown(self) -> None:
self.stopping = True self.stopping = True
try: try:
self.s.shutdown(socket.SHUT_RDWR) self.s.shutdown(socket.SHUT_RDWR)
@@ -60,7 +77,7 @@ class HttpConn(object):
except: except:
pass pass
def set_rproxy(self, ip=None): def set_rproxy(self, ip: Optional[str] = None) -> str:
if ip is None: if ip is None:
color = 36 color = 36
ip = self.addr[0] ip = self.addr[0]
@@ -73,35 +90,37 @@ class HttpConn(object):
self.log_src = "{} \033[{}m{}".format(ip, color, self.addr[1]).ljust(26) self.log_src = "{} \033[{}m{}".format(ip, color, self.addr[1]).ljust(26)
return self.log_src return self.log_src
def respath(self, res_name): def respath(self, res_name: str) -> str:
return os.path.join(E.mod, "web", res_name) return os.path.join(E.mod, "web", res_name)
def log(self, msg, c=0): def log(self, msg: str, c: Union[int, str] = 0) -> None:
self.log_func(self.log_src, msg, c) self.log_func(self.log_src, msg, c)
def get_u2idx(self): def get_u2idx(self) -> U2idx:
# one u2idx per tcp connection;
# sqlite3 fully parallelizes under python threads
if not self.u2idx: if not self.u2idx:
self.u2idx = U2idx(self) self.u2idx = U2idx(self)
return self.u2idx return self.u2idx
def _detect_https(self): def _detect_https(self) -> bool:
method = None method = None
if self.cert_path: if self.cert_path:
try: try:
method = self.s.recv(4, socket.MSG_PEEK) method = self.s.recv(4, socket.MSG_PEEK)
except socket.timeout: except socket.timeout:
return return False
except AttributeError: except AttributeError:
# jython does not support msg_peek; forget about https # jython does not support msg_peek; forget about https
method = self.s.recv(4) method = self.s.recv(4)
self.sr = Unrecv(self.s) self.sr = Util.Unrecv(self.s, self.log)
self.sr.buf = method self.sr.buf = method
# jython used to do this, they stopped since it's broken # jython used to do this, they stopped since it's broken
# but reimplementing sendall is out of scope for now # but reimplementing sendall is out of scope for now
if not getattr(self.s, "sendall", None): if not getattr(self.s, "sendall", None):
self.s.sendall = self.s.send self.s.sendall = self.s.send # type: ignore
if len(method) != 4: if len(method) != 4:
err = "need at least 4 bytes in the first packet; got {}".format( err = "need at least 4 bytes in the first packet; got {}".format(
@@ -111,17 +130,18 @@ class HttpConn(object):
self.log(err) self.log(err)
self.s.send(b"HTTP/1.1 400 Bad Request\r\n\r\n" + err.encode("utf-8")) self.s.send(b"HTTP/1.1 400 Bad Request\r\n\r\n" + err.encode("utf-8"))
return return False
return method not in [None, b"GET ", b"HEAD", b"POST", b"PUT ", b"OPTI"] return method not in [None, b"GET ", b"HEAD", b"POST", b"PUT ", b"OPTI"]
def run(self): def run(self) -> None:
self.sr = None self.sr = None
if self.args.https_only: if self.args.https_only:
is_https = True is_https = True
elif self.args.http_only or not HAVE_SSL: elif self.args.http_only or not HAVE_SSL:
is_https = False is_https = False
else: else:
# raise Exception("asdf")
is_https = self._detect_https() is_https = self._detect_https()
if is_https: if is_https:
@@ -150,14 +170,15 @@ class HttpConn(object):
self.s = ctx.wrap_socket(self.s, server_side=True) self.s = ctx.wrap_socket(self.s, server_side=True)
msg = [ msg = [
"\033[1;3{:d}m{}".format(c, s) "\033[1;3{:d}m{}".format(c, s)
for c, s in zip([0, 5, 0], self.s.cipher()) for c, s in zip([0, 5, 0], self.s.cipher()) # type: ignore
] ]
self.log(" ".join(msg) + "\033[0m") self.log(" ".join(msg) + "\033[0m")
if self.args.ssl_dbg and hasattr(self.s, "shared_ciphers"): if self.args.ssl_dbg and hasattr(self.s, "shared_ciphers"):
overlap = [y[::-1] for y in self.s.shared_ciphers()] ciphers = self.s.shared_ciphers()
lines = [str(x) for x in (["TLS cipher overlap:"] + overlap)] assert ciphers
self.log("\n".join(lines)) overlap = [str(y[::-1]) for y in ciphers]
self.log("TLS cipher overlap:" + "\n".join(overlap))
for k, v in [ for k, v in [
["compression", self.s.compression()], ["compression", self.s.compression()],
["ALPN proto", self.s.selected_alpn_protocol()], ["ALPN proto", self.s.selected_alpn_protocol()],
@@ -182,7 +203,7 @@ class HttpConn(object):
return return
if not self.sr: if not self.sr:
self.sr = Unrecv(self.s) self.sr = Util.Unrecv(self.s, self.log)
while not self.stopping: while not self.stopping:
self.nreq += 1 self.nreq += 1

View File

@@ -1,13 +1,15 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import os
import sys
import time
import math
import base64 import base64
import math
import os
import socket import socket
import sys
import threading import threading
import time
import queue
try: try:
import jinja2 import jinja2
@@ -26,15 +28,18 @@ except ImportError:
) )
sys.exit(1) sys.exit(1)
from .__init__ import E, PY2, MACOS from .__init__ import MACOS, TYPE_CHECKING, E
from .util import FHC, spack, min_ex, start_stackmon, start_log_thrs
from .bos import bos from .bos import bos
from .httpconn import HttpConn from .httpconn import HttpConn
from .util import FHC, min_ex, spack, start_log_thrs, start_stackmon
if PY2: if TYPE_CHECKING:
import Queue as queue from .broker_util import BrokerCli
else:
import queue try:
from typing import Any, Optional
except:
pass
class HttpSrv(object): class HttpSrv(object):
@@ -43,7 +48,7 @@ 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, nid): def __init__(self, broker: "BrokerCli", nid: Optional[int]) -> None:
self.broker = broker self.broker = broker
self.nid = nid self.nid = nid
self.args = broker.args self.args = broker.args
@@ -58,29 +63,25 @@ class HttpSrv(object):
self.tp_nthr = 0 # actual self.tp_nthr = 0 # actual
self.tp_ncli = 0 # fading self.tp_ncli = 0 # fading
self.tp_time = None # latest worker collect self.tp_time = 0.0 # latest worker collect
self.tp_q = None if self.args.no_htp else queue.LifoQueue() self.tp_q: Optional[queue.LifoQueue[Any]] = (
self.t_periodic = None None if self.args.no_htp else queue.LifoQueue()
)
self.t_periodic: Optional[threading.Thread] = None
self.u2fh = FHC() self.u2fh = FHC()
self.srvs = [] self.srvs: list[socket.socket] = []
self.ncli = 0 # exact self.ncli = 0 # exact
self.clients = {} # laggy self.clients: set[HttpConn] = set() # laggy
self.nclimax = 0 self.nclimax = 0
self.cb_ts = 0 self.cb_ts = 0.0
self.cb_v = 0 self.cb_v = ""
try:
x = self.broker.put(True, "thumbsrv.getcfg")
self.th_cfg = x.get()
except:
pass
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"))
self.j2 = { self.j2 = {
x: env.get_template(x + ".html") x: env.get_template(x + ".html")
for x in ["splash", "browser", "browser2", "msg", "md", "mde"] for x in ["splash", "browser", "browser2", "msg", "md", "mde", "cf"]
} }
self.prism = os.path.exists(os.path.join(E.mod, "web", "deps", "prism.js.gz")) self.prism = os.path.exists(os.path.join(E.mod, "web", "deps", "prism.js.gz"))
@@ -88,7 +89,7 @@ class HttpSrv(object):
if bos.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 = ""
if self.tp_q: if self.tp_q:
self.start_threads(4) self.start_threads(4)
@@ -100,7 +101,19 @@ class HttpSrv(object):
if self.args.log_thrs: if self.args.log_thrs:
start_log_thrs(self.log, self.args.log_thrs, nid) start_log_thrs(self.log, self.args.log_thrs, nid)
def start_threads(self, n): self.th_cfg: dict[str, Any] = {}
t = threading.Thread(target=self.post_init, name="hsrv-init2")
t.daemon = True
t.start()
def post_init(self) -> None:
try:
x = self.broker.ask("thumbsrv.getcfg")
self.th_cfg = x.get()
except:
pass
def start_threads(self, n: int) -> None:
self.tp_nthr += n self.tp_nthr += n
if self.args.log_htp: if self.args.log_htp:
self.log(self.name, "workers += {} = {}".format(n, self.tp_nthr), 6) self.log(self.name, "workers += {} = {}".format(n, self.tp_nthr), 6)
@@ -113,15 +126,16 @@ class HttpSrv(object):
thr.daemon = True thr.daemon = True
thr.start() thr.start()
def stop_threads(self, n): def stop_threads(self, n: int) -> None:
self.tp_nthr -= n self.tp_nthr -= n
if self.args.log_htp: if self.args.log_htp:
self.log(self.name, "workers -= {} = {}".format(n, self.tp_nthr), 6) self.log(self.name, "workers -= {} = {}".format(n, self.tp_nthr), 6)
assert self.tp_q
for _ in range(n): for _ in range(n):
self.tp_q.put(None) self.tp_q.put(None)
def periodic(self): def periodic(self) -> None:
while True: while True:
time.sleep(2 if self.tp_ncli or self.ncli else 10) time.sleep(2 if self.tp_ncli or self.ncli else 10)
with self.mutex: with self.mutex:
@@ -135,7 +149,7 @@ class HttpSrv(object):
self.t_periodic = None self.t_periodic = None
return return
def listen(self, sck, nlisteners): def listen(self, sck: socket.socket, nlisteners: int) -> None:
ip, port = sck.getsockname() ip, port = sck.getsockname()
self.srvs.append(sck) self.srvs.append(sck)
self.nclimax = math.ceil(self.args.nc * 1.0 / nlisteners) self.nclimax = math.ceil(self.args.nc * 1.0 / nlisteners)
@@ -147,17 +161,17 @@ class HttpSrv(object):
t.daemon = True t.daemon = True
t.start() t.start()
def thr_listen(self, srv_sck): def thr_listen(self, srv_sck: socket.socket) -> None:
"""listens on a shared tcp server""" """listens on a shared tcp server"""
ip, port = srv_sck.getsockname() ip, port = srv_sck.getsockname()
fno = srv_sck.fileno() fno = srv_sck.fileno()
msg = "subscribed @ {}:{} f{}".format(ip, port, fno) msg = "subscribed @ {}:{} f{} p{}".format(ip, port, fno, os.getpid())
self.log(self.name, msg) self.log(self.name, msg)
def fun(): def fun() -> None:
self.broker.put(False, "cb_httpsrv_up") self.broker.say("cb_httpsrv_up")
threading.Thread(target=fun).start() threading.Thread(target=fun, name="sig-hsrv-up1").start()
while not self.stopping: while not self.stopping:
if self.args.log_conn: if self.args.log_conn:
@@ -179,21 +193,21 @@ class HttpSrv(object):
continue continue
if self.args.log_conn: if self.args.log_conn:
m = "|{}C-acc2 \033[0;36m{} \033[3{}m{}".format( t = "|{}C-acc2 \033[0;36m{} \033[3{}m{}".format(
"-" * 3, ip, port % 8, port "-" * 3, ip, port % 8, port
) )
self.log("%s %s" % addr, m, c="1;30") self.log("%s %s" % addr, t, c="1;30")
self.accept(sck, addr) self.accept(sck, addr)
def accept(self, sck, addr): def accept(self, sck: socket.socket, addr: tuple[str, int]) -> None:
"""takes an incoming tcp connection and creates a thread to handle it""" """takes an incoming tcp connection and creates a thread to handle it"""
now = time.time() now = time.time()
if now - (self.tp_time or now) > 300: if now - (self.tp_time or now) > 300:
m = "httpserver threadpool died: tpt {:.2f}, now {:.2f}, nthr {}, ncli {}" t = "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.log(self.name, t.format(self.tp_time, now, self.tp_nthr, self.ncli), 1)
self.tp_time = None self.tp_time = 0
self.tp_q = None self.tp_q = None
with self.mutex: with self.mutex:
@@ -203,10 +217,10 @@ class HttpSrv(object):
if self.nid: if self.nid:
name += "-{}".format(self.nid) name += "-{}".format(self.nid)
t = threading.Thread(target=self.periodic, name=name) thr = threading.Thread(target=self.periodic, name=name)
self.t_periodic = t self.t_periodic = thr
t.daemon = True thr.daemon = True
t.start() thr.start()
if self.tp_q: if self.tp_q:
self.tp_time = self.tp_time or now self.tp_time = self.tp_time or now
@@ -218,8 +232,8 @@ class HttpSrv(object):
return return
if not self.args.no_htp: 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" t = "looks like the httpserver threadpool died; please make an issue on github and tell me the story of how you pulled that off, thanks and dog bless\n"
self.log(self.name, m, 1) self.log(self.name, t, 1)
thr = threading.Thread( thr = threading.Thread(
target=self.thr_client, target=self.thr_client,
@@ -229,14 +243,15 @@ class HttpSrv(object):
thr.daemon = True thr.daemon = True
thr.start() thr.start()
def thr_poolw(self): def thr_poolw(self) -> None:
assert self.tp_q
while True: while True:
task = self.tp_q.get() task = self.tp_q.get()
if not task: if not task:
break break
with self.mutex: with self.mutex:
self.tp_time = None self.tp_time = 0
try: try:
sck, addr = task sck, addr = task
@@ -246,10 +261,13 @@ class HttpSrv(object):
) )
self.thr_client(sck, addr) self.thr_client(sck, addr)
me.name = self.name + "-poolw" me.name = self.name + "-poolw"
except: except Exception as ex:
self.log(self.name, "thr_client: " + min_ex(), 3) if str(ex).startswith("client d/c "):
self.log(self.name, "thr_client: " + str(ex), 6)
else:
self.log(self.name, "thr_client: " + min_ex(), 3)
def shutdown(self): def shutdown(self) -> None:
self.stopping = True self.stopping = True
for srv in self.srvs: for srv in self.srvs:
try: try:
@@ -257,7 +275,7 @@ class HttpSrv(object):
except: except:
pass pass
clients = list(self.clients.keys()) clients = list(self.clients)
for cli in clients: for cli in clients:
try: try:
cli.shutdown() cli.shutdown()
@@ -273,13 +291,13 @@ class HttpSrv(object):
self.log(self.name, "ok bye") self.log(self.name, "ok bye")
def thr_client(self, sck, addr): def thr_client(self, sck: socket.socket, addr: tuple[str, int]) -> None:
"""thread managing one tcp client""" """thread managing one tcp client"""
sck.settimeout(120) sck.settimeout(120)
cli = HttpConn(sck, addr, self) cli = HttpConn(sck, addr, self)
with self.mutex: with self.mutex:
self.clients[cli] = 0 self.clients.add(cli)
fno = sck.fileno() fno = sck.fileno()
try: try:
@@ -322,10 +340,10 @@ class HttpSrv(object):
raise raise
finally: finally:
with self.mutex: with self.mutex:
del self.clients[cli] self.clients.remove(cli)
self.ncli -= 1 self.ncli -= 1
def cachebuster(self): def cachebuster(self) -> str:
if time.time() - self.cb_ts < 1: if time.time() - self.cb_ts < 1:
return self.cb_v return self.cb_v

View File

@@ -1,28 +1,28 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import hashlib import argparse # typechk
import colorsys import colorsys
import hashlib
from .__init__ import PY2 from .__init__ import PY2
class Ico(object): class Ico(object):
def __init__(self, args): def __init__(self, args: argparse.Namespace) -> None:
self.args = args self.args = args
def get(self, ext, as_thumb): def get(self, ext: str, as_thumb: bool) -> tuple[str, bytes]:
"""placeholder to make thumbnails not break""" """placeholder to make thumbnails not break"""
h = hashlib.md5(ext.encode("utf-8")).digest()[:2] zb = hashlib.sha1(ext.encode("utf-8")).digest()[2:4]
if PY2: if PY2:
h = [ord(x) for x in h] zb = [ord(x) for x in zb]
c1 = colorsys.hsv_to_rgb(h[0] / 256.0, 1, 0.3) c1 = colorsys.hsv_to_rgb(zb[0] / 256.0, 1, 0.3)
c2 = colorsys.hsv_to_rgb(h[0] / 256.0, 1, 1) c2 = colorsys.hsv_to_rgb(zb[0] / 256.0, 1, 1)
c = list(c1) + list(c2) ci = [int(x * 255) for x in list(c1) + list(c2)]
c = [int(x * 255) for x in c] c = "".join(["{:02x}".format(x) for x in ci])
c = "".join(["{:02x}".format(x) for x in c])
h = 30 h = 30
if not self.args.th_no_crop and as_thumb: if not self.args.th_no_crop and as_thumb:
@@ -37,6 +37,6 @@ class Ico(object):
fill="#{}" font-family="monospace" font-size="14px" style="letter-spacing:.5px">{}</text> fill="#{}" font-family="monospace" font-size="14px" style="letter-spacing:.5px">{}</text>
</g></svg> </g></svg>
""" """
svg = svg.format(h, c[:6], c[6:], ext).encode("utf-8") svg = svg.format(h, c[:6], c[6:], ext)
return ["image/svg+xml", svg] return "image/svg+xml", svg.encode("utf-8")

View File

@@ -1,18 +1,26 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import os import argparse
import sys
import json import json
import os
import shutil import shutil
import subprocess as sp import subprocess as sp
import sys
from .__init__ import PY2, WINDOWS, unicode from .__init__ import PY2, WINDOWS, unicode
from .util import fsenc, fsdec, uncyg, runcmd, retchk, REKOBO_LKEY
from .bos import bos from .bos import bos
from .util import REKOBO_LKEY, fsenc, min_ex, retchk, runcmd, uncyg
try:
from typing import Any, Union
from .util import RootLogger
except:
pass
def have_ff(cmd): def have_ff(cmd: str) -> bool:
if PY2: if PY2:
print("# checking {}".format(cmd)) print("# checking {}".format(cmd))
cmd = (cmd + " -version").encode("ascii").split(b" ") cmd = (cmd + " -version").encode("ascii").split(b" ")
@@ -30,13 +38,15 @@ HAVE_FFPROBE = have_ff("ffprobe")
class MParser(object): class MParser(object):
def __init__(self, cmdline): def __init__(self, cmdline: str) -> None:
self.tag, args = cmdline.split("=", 1) self.tag, args = cmdline.split("=", 1)
self.tags = self.tag.split(",") self.tags = self.tag.split(",")
self.timeout = 30 self.timeout = 30
self.force = False self.force = False
self.kill = "t" # tree; all children recursively
self.audio = "y" self.audio = "y"
self.pri = 0 # priority; higher = later
self.ext = [] self.ext = []
while True: while True:
@@ -58,6 +68,10 @@ class MParser(object):
self.audio = arg[1:] # [r]equire [n]ot [d]ontcare self.audio = arg[1:] # [r]equire [n]ot [d]ontcare
continue continue
if arg.startswith("k"):
self.kill = arg[1:] # [t]ree [m]ain [n]one
continue
if arg == "f": if arg == "f":
self.force = True self.force = True
continue continue
@@ -70,10 +84,16 @@ class MParser(object):
self.ext.append(arg[1:]) self.ext.append(arg[1:])
continue continue
if arg.startswith("p"):
self.pri = int(arg[1:] or "1")
continue
raise Exception() raise Exception()
def ffprobe(abspath, timeout=10): def ffprobe(
abspath: str, timeout: int = 10
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]:
cmd = [ cmd = [
b"ffprobe", b"ffprobe",
b"-hide_banner", b"-hide_banner",
@@ -87,15 +107,15 @@ def ffprobe(abspath, timeout=10):
return parse_ffprobe(so) return parse_ffprobe(so)
def parse_ffprobe(txt): def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]:
"""ffprobe -show_format -show_streams""" """ffprobe -show_format -show_streams"""
streams = [] streams = []
fmt = {} fmt = {}
g = None g = {}
for ln in [x.rstrip("\r") for x in txt.split("\n")]: for ln in [x.rstrip("\r") for x in txt.split("\n")]:
try: try:
k, v = ln.split("=", 1) sk, sv = ln.split("=", 1)
g[k] = v g[sk] = sv
continue continue
except: except:
pass pass
@@ -109,8 +129,8 @@ def parse_ffprobe(txt):
fmt = g fmt = g
streams = [fmt] + streams streams = [fmt] + streams
ret = {} # processed ret: dict[str, Any] = {} # processed
md = {} # raw tags md: dict[str, list[Any]] = {} # raw tags
is_audio = fmt.get("format_name") in ["mp3", "ogg", "flac", "wav"] is_audio = fmt.get("format_name") in ["mp3", "ogg", "flac", "wav"]
if fmt.get("filename", "").split(".")[-1].lower() in ["m4a", "aac"]: if fmt.get("filename", "").split(".")[-1].lower() in ["m4a", "aac"]:
@@ -158,52 +178,55 @@ def parse_ffprobe(txt):
] ]
if typ == "format": if typ == "format":
kvm = [["duration", ".dur"], ["bit_rate", ".q"]] kvm = [["duration", ".dur"], ["bit_rate", ".q"], ["format_name", "fmt"]]
for sk, rk in kvm: for sk, rk in kvm:
v = strm.get(sk) v1 = strm.get(sk)
if v is None: if v1 is None:
continue continue
if rk.startswith("."): if rk.startswith("."):
try: try:
v = float(v) zf = float(v1)
v2 = ret.get(rk) v2 = ret.get(rk)
if v2 is None or v > v2: if v2 is None or zf > v2:
ret[rk] = v ret[rk] = zf
except: except:
# sqlite doesnt care but the code below does # sqlite doesnt care but the code below does
if v not in ["N/A"]: if v1 not in ["N/A"]:
ret[rk] = v ret[rk] = v1
else: else:
ret[rk] = v ret[rk] = v1
if ret.get("vc") == "ansi": # shellscript if ret.get("vc") == "ansi": # shellscript
return {}, {} return {}, {}
for strm in streams: for strm in streams:
for k, v in strm.items(): for sk, sv in strm.items():
if not k.startswith("TAG:"): if not sk.startswith("TAG:"):
continue continue
k = k[4:].strip() sk = sk[4:].strip()
v = v.strip() sv = sv.strip()
if k and v and k not in md: if sk and sv and sk not in md:
md[k] = [v] md[sk] = [sv]
for k in [".q", ".vq", ".aq"]: for sk in [".q", ".vq", ".aq"]:
if k in ret: if sk in ret:
ret[k] /= 1000 # bit_rate=320000 ret[sk] /= 1000 # bit_rate=320000
for k in [".q", ".vq", ".aq", ".resw", ".resh"]: for sk in [".q", ".vq", ".aq", ".resw", ".resh"]:
if k in ret: if sk in ret:
ret[k] = int(ret[k]) ret[sk] = int(ret[sk])
if ".fps" in ret: if ".fps" in ret:
fps = ret[".fps"] fps = ret[".fps"]
if "/" in fps: if "/" in fps:
fa, fb = fps.split("/") fa, fb = fps.split("/")
fps = int(fa) * 1.0 / int(fb) try:
fps = int(fa) * 1.0 / int(fb)
except:
fps = 9001
if fps < 1000 and fmt.get("format_name") not in ["image2", "png_pipe"]: if fps < 1000 and fmt.get("format_name") not in ["image2", "png_pipe"]:
ret[".fps"] = round(fps, 3) ret[".fps"] = round(fps, 3)
@@ -216,16 +239,19 @@ def parse_ffprobe(txt):
if ".q" in ret: if ".q" in ret:
del ret[".q"] del ret[".q"]
if "fmt" in ret:
ret["fmt"] = ret["fmt"].split(",")[0]
if ".resw" in ret and ".resh" in ret: if ".resw" in ret and ".resh" in ret:
ret["res"] = "{}x{}".format(ret[".resw"], ret[".resh"]) ret["res"] = "{}x{}".format(ret[".resw"], ret[".resh"])
ret = {k: [0, v] for k, v in ret.items()} zd = {k: (0, v) for k, v in ret.items()}
return ret, md return zd, md
class MTag(object): class MTag(object):
def __init__(self, log_func, args): def __init__(self, log_func: "RootLogger", args: argparse.Namespace) -> None:
self.log_func = log_func self.log_func = log_func
self.args = args self.args = args
self.usable = True self.usable = True
@@ -242,7 +268,7 @@ class MTag(object):
if self.backend == "mutagen": if self.backend == "mutagen":
self.get = self.get_mutagen self.get = self.get_mutagen
try: try:
import mutagen import mutagen # noqa: F401 # pylint: disable=unused-import,import-outside-toplevel
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"
@@ -339,31 +365,33 @@ class MTag(object):
} }
# self.get = self.compare # self.get = self.compare
def log(self, msg, c=0): def log(self, msg: str, c: Union[int, str] = 0) -> None:
self.log_func("mtag", msg, c) self.log_func("mtag", msg, c)
def normalize_tags(self, ret, md): def normalize_tags(
for k, v in dict(md).items(): self, parser_output: dict[str, tuple[int, Any]], md: dict[str, list[Any]]
if not v: ) -> dict[str, Union[str, float]]:
for sk, tv in dict(md).items():
if not tv:
continue continue
k = k.lower().split("::")[0].strip() sk = sk.lower().split("::")[0].strip()
mk = self.rmap.get(k) key_mapping = self.rmap.get(sk)
if not mk: if not key_mapping:
continue continue
pref, mk = mk priority, alias = key_mapping
if mk not in ret or ret[mk][0] > pref: if alias not in parser_output or parser_output[alias][0] > priority:
ret[mk] = [pref, v[0]] parser_output[alias] = (priority, tv[0])
# take first value # take first value (lowest priority / most preferred)
ret = {k: unicode(v[1]).strip() for k, v in ret.items()} ret = {sk: unicode(tv[1]).strip() for sk, tv in parser_output.items()}
# track 3/7 => track 3 # track 3/7 => track 3
for k, v in ret.items(): for sk, tv in ret.items():
if k[0] == ".": if sk[0] == ".":
v = v.split("/")[0].strip().lstrip("0") sv = str(tv).split("/")[0].strip().lstrip("0")
ret[k] = v or 0 ret[sk] = sv or 0
# normalize key notation to rkeobo # normalize key notation to rkeobo
okey = ret.get("key") okey = ret.get("key")
@@ -373,7 +401,7 @@ class MTag(object):
return ret return ret
def compare(self, abspath): def compare(self, abspath: str) -> dict[str, Union[str, float]]:
if abspath.endswith(".au"): if abspath.endswith(".au"):
return {} return {}
@@ -411,7 +439,9 @@ class MTag(object):
return r1 return r1
def get_mutagen(self, abspath): def get_mutagen(self, abspath: str) -> dict[str, Union[str, float]]:
ret: dict[str, tuple[int, Any]] = {}
if not bos.path.isfile(abspath): if not bos.path.isfile(abspath):
return {} return {}
@@ -421,11 +451,14 @@ class MTag(object):
md = mutagen.File(fsenc(abspath), easy=True) md = mutagen.File(fsenc(abspath), easy=True)
if not md.info.length and not md.info.codec: if not md.info.length and not md.info.codec:
raise Exception() raise Exception()
except Exception as ex: except:
return self.get_ffprobe(abspath) if self.can_ffprobe else {} return self.get_ffprobe(abspath) if self.can_ffprobe else {}
sz = bos.path.getsize(abspath) sz = bos.path.getsize(abspath)
ret = {".q": [0, int((sz / md.info.length) / 128)]} try:
ret[".q"] = (0, int((sz / md.info.length) / 128))
except:
pass
for attr, k, norm in [ for attr, k, norm in [
["codec", "ac", unicode], ["codec", "ac", unicode],
@@ -456,44 +489,51 @@ class MTag(object):
if k == "ac" and v.startswith("mp4a.40."): if k == "ac" and v.startswith("mp4a.40."):
v = "aac" v = "aac"
ret[k] = [0, norm(v)] ret[k] = (0, norm(v))
return self.normalize_tags(ret, md) return self.normalize_tags(ret, md)
def get_ffprobe(self, abspath): def get_ffprobe(self, abspath: str) -> dict[str, Union[str, float]]:
if not bos.path.isfile(abspath): if not bos.path.isfile(abspath):
return {} return {}
ret, md = ffprobe(abspath) ret, md = ffprobe(abspath)
return self.normalize_tags(ret, md) return self.normalize_tags(ret, md)
def get_bin(self, parsers, abspath): def get_bin(
self, parsers: dict[str, MParser], abspath: str, oth_tags: dict[str, Any]
) -> dict[str, Any]:
if not bos.path.isfile(abspath): if not bos.path.isfile(abspath):
return {} return {}
pypath = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) pypath = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
pypath = [str(pypath)] + [str(x) for x in sys.path if x] zsl = [str(pypath)] + [str(x) for x in sys.path if x]
pypath = str(os.pathsep.join(pypath)) pypath = str(os.pathsep.join(zsl))
env = os.environ.copy() env = os.environ.copy()
env["PYTHONPATH"] = pypath env["PYTHONPATH"] = pypath
ret = {} ret: dict[str, Any] = {}
for tagname, parser in parsers.items(): for tagname, parser in sorted(parsers.items(), key=lambda x: (x[1].pri, x[0])):
try: try:
cmd = [parser.bin, abspath] cmd = [parser.bin, abspath]
if parser.bin.endswith(".py"): if parser.bin.endswith(".py"):
cmd = [sys.executable] + cmd cmd = [sys.executable] + cmd
args = {"env": env, "timeout": parser.timeout} args = {"env": env, "timeout": parser.timeout, "kill": parser.kill}
if parser.pri:
zd = oth_tags.copy()
zd.update(ret)
args["sin"] = json.dumps(zd).encode("utf-8", "replace")
if WINDOWS: if WINDOWS:
args["creationflags"] = 0x4000 args["creationflags"] = 0x4000
else: else:
cmd = ["nice"] + cmd cmd = ["nice"] + cmd
cmd = [fsenc(x) for x in cmd] bcmd = [fsenc(x) for x in cmd]
rc, v, err = runcmd(cmd, **args) rc, v, err = runcmd(bcmd, **args) # type: ignore
retchk(rc, cmd, err, self.log, 5) retchk(rc, bcmd, err, self.log, 5, self.args.mtag_v)
v = v.strip() v = v.strip()
if not v: if not v:
continue continue
@@ -501,11 +541,13 @@ class MTag(object):
if "," not in tagname: if "," not in tagname:
ret[tagname] = v ret[tagname] = v
else: else:
v = json.loads(v) zj = json.loads(v)
for tag in tagname.split(","): for tag in tagname.split(","):
if tag and tag in v: if tag and tag in zj:
ret[tag] = v[tag] ret[tag] = zj[tag]
except: except:
pass if self.args.mtag_v:
t = "mtag error: tagname {}, parser {}, file {} => {}"
self.log(t.format(tagname, parser.bin, abspath, min_ex()))
return ret return ret

View File

@@ -4,20 +4,29 @@ from __future__ import print_function, unicode_literals
import tarfile import tarfile
import threading import threading
from .sutil import errdesc from queue import Queue
from .util import Queue, fsenc, min_ex
from .bos import bos from .bos import bos
from .sutil import StreamArc, errdesc
from .util import fsenc, min_ex
try:
from typing import Any, Generator, Optional
from .util import NamedLogger
except:
pass
class QFile(object): class QFile(object): # inherit io.StringIO for painful typing
"""file-like object which buffers writes into a queue""" """file-like object which buffers writes into a queue"""
def __init__(self): def __init__(self) -> None:
self.q = Queue(64) self.q: Queue[Optional[bytes]] = Queue(64)
self.bq = [] self.bq: list[bytes] = []
self.nq = 0 self.nq = 0
def write(self, buf): def write(self, buf: Optional[bytes]) -> None:
if buf is None or self.nq >= 240 * 1024: if buf is None or self.nq >= 240 * 1024:
self.q.put(b"".join(self.bq)) self.q.put(b"".join(self.bq))
self.bq = [] self.bq = []
@@ -30,40 +39,47 @@ class QFile(object):
self.nq += len(buf) self.nq += len(buf)
class StreamTar(object): class StreamTar(StreamArc):
"""construct in-memory tar file from the given path""" """construct in-memory tar file from the given path"""
def __init__(self, log, fgen, **kwargs): def __init__(
self,
log: "NamedLogger",
fgen: Generator[dict[str, Any], None, None],
**kwargs: Any
):
super(StreamTar, self).__init__(log, fgen)
self.ci = 0 self.ci = 0
self.co = 0 self.co = 0
self.qfile = QFile() self.qfile = QFile()
self.log = log self.errf: dict[str, Any] = {}
self.fgen = fgen
self.errf = None
# python 3.8 changed to PAX_FORMAT as default, # python 3.8 changed to PAX_FORMAT as default,
# waste of space and don't care about the new features # waste of space and don't care about the new features
fmt = tarfile.GNU_FORMAT fmt = tarfile.GNU_FORMAT
self.tar = tarfile.open(fileobj=self.qfile, mode="w|", format=fmt) self.tar = tarfile.open(fileobj=self.qfile, mode="w|", format=fmt) # type: ignore
w = threading.Thread(target=self._gen, name="star-gen") w = threading.Thread(target=self._gen, name="star-gen")
w.daemon = True w.daemon = True
w.start() w.start()
def gen(self): def gen(self) -> Generator[Optional[bytes], None, None]:
while True: try:
buf = self.qfile.q.get() while True:
if not buf: buf = self.qfile.q.get()
break if not buf:
break
self.co += len(buf) self.co += len(buf)
yield buf yield buf
yield None yield None
if self.errf: finally:
bos.unlink(self.errf["ap"]) if self.errf:
bos.unlink(self.errf["ap"])
def ser(self, f): def ser(self, f: dict[str, Any]) -> None:
name = f["vp"] name = f["vp"]
src = f["ap"] src = f["ap"]
fsi = f["st"] fsi = f["st"]
@@ -76,21 +92,21 @@ class StreamTar(object):
inf.gid = 0 inf.gid = 0
self.ci += inf.size self.ci += inf.size
with open(fsenc(src), "rb", 512 * 1024) as f: with open(fsenc(src), "rb", 512 * 1024) as fo:
self.tar.addfile(inf, f) self.tar.addfile(inf, fo)
def _gen(self): def _gen(self) -> None:
errors = [] errors = []
for f in self.fgen: for f in self.fgen:
if "err" in f: if "err" in f:
errors.append([f["vp"], f["err"]]) errors.append((f["vp"], f["err"]))
continue continue
try: try:
self.ser(f) self.ser(f)
except Exception: except:
ex = min_ex(5, True).replace("\n", "\n-- ") ex = min_ex(5, True).replace("\n", "\n-- ")
errors.append([f["vp"], ex]) errors.append((f["vp"], ex))
if errors: if errors:
self.errf, txt = errdesc(errors) self.errf, txt = errdesc(errors)

View File

@@ -12,23 +12,28 @@ Original source: misc/python/surrogateescape.py in https://bitbucket.org/haypo/m
# This code is released under the Python license and the BSD 2-clause license # This code is released under the Python license and the BSD 2-clause license
import platform
import codecs import codecs
import platform
import sys import sys
PY3 = sys.version_info[0] > 2 PY3 = sys.version_info[0] > 2
WINDOWS = platform.system() == "Windows" WINDOWS = platform.system() == "Windows"
FS_ERRORS = "surrogateescape" FS_ERRORS = "surrogateescape"
try:
from typing import Any
except:
pass
def u(text):
def u(text: Any) -> str:
if PY3: if PY3:
return text return text
else: else:
return text.decode("unicode_escape") return text.decode("unicode_escape")
def b(data): def b(data: Any) -> bytes:
if PY3: if PY3:
return data.encode("latin1") return data.encode("latin1")
else: else:
@@ -43,7 +48,7 @@ else:
bytes_chr = chr bytes_chr = chr
def surrogateescape_handler(exc): def surrogateescape_handler(exc: Any) -> tuple[str, int]:
""" """
Pure Python implementation of the PEP 383: the "surrogateescape" error Pure Python implementation of the PEP 383: the "surrogateescape" error
handler of Python 3. Undecodable bytes will be replaced by a Unicode handler of Python 3. Undecodable bytes will be replaced by a Unicode
@@ -74,7 +79,7 @@ class NotASurrogateError(Exception):
pass pass
def replace_surrogate_encode(mystring): def replace_surrogate_encode(mystring: str) -> str:
""" """
Returns a (unicode) string, not the more logical bytes, because the codecs Returns a (unicode) string, not the more logical bytes, because the codecs
register_error functionality expects this. register_error functionality expects this.
@@ -100,7 +105,7 @@ def replace_surrogate_encode(mystring):
return str().join(decoded) return str().join(decoded)
def replace_surrogate_decode(mybytes): def replace_surrogate_decode(mybytes: bytes) -> str:
""" """
Returns a (unicode) string Returns a (unicode) string
""" """
@@ -121,7 +126,7 @@ def replace_surrogate_decode(mybytes):
return str().join(decoded) return str().join(decoded)
def encodefilename(fn): def encodefilename(fn: str) -> bytes:
if FS_ENCODING == "ascii": if FS_ENCODING == "ascii":
# ASCII encoder of Python 2 expects that the error handler returns a # ASCII encoder of Python 2 expects that the error handler returns a
# Unicode string encodable to ASCII, whereas our surrogateescape error # Unicode string encodable to ASCII, whereas our surrogateescape error
@@ -161,7 +166,7 @@ def encodefilename(fn):
return fn.encode(FS_ENCODING, FS_ERRORS) return fn.encode(FS_ENCODING, FS_ERRORS)
def decodefilename(fn): def decodefilename(fn: bytes) -> str:
return fn.decode(FS_ENCODING, FS_ERRORS) return fn.decode(FS_ENCODING, FS_ERRORS)
@@ -181,7 +186,7 @@ if WINDOWS and not PY3:
FS_ENCODING = codecs.lookup(FS_ENCODING).name FS_ENCODING = codecs.lookup(FS_ENCODING).name
def register_surrogateescape(): def register_surrogateescape() -> None:
""" """
Registers the surrogateescape error handler on Python 2 (only) Registers the surrogateescape error handler on Python 2 (only)
""" """

View File

@@ -1,14 +1,34 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import time
import tempfile import tempfile
from datetime import datetime from datetime import datetime
from .bos import bos from .bos import bos
try:
from typing import Any, Generator, Optional
def errdesc(errors): from .util import NamedLogger
except:
pass
class StreamArc(object):
def __init__(
self,
log: "NamedLogger",
fgen: Generator[dict[str, Any], None, None],
**kwargs: Any
):
self.log = log
self.fgen = fgen
def gen(self) -> Generator[Optional[bytes], None, None]:
pass
def errdesc(errors: list[tuple[str, str]]) -> tuple[dict[str, Any], list[str]]:
report = ["copyparty failed to add the following files to the archive:", ""] report = ["copyparty failed to add the following files to the archive:", ""]
for fn, err in errors: for fn, err in errors:

View File

@@ -1,51 +1,98 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import argparse
import base64
import calendar
import gzip
import os import os
import sys import re
import time
import shlex import shlex
import string
import signal import signal
import socket import socket
import string
import sys
import threading import threading
import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
import calendar
from .__init__ import E, PY2, WINDOWS, ANYWIN, MACOS, VT100, unicode try:
from .util import mp, start_log_thrs, start_stackmon, min_ex, ansi_re from types import FrameType
import typing
from typing import Any, Optional, Union
except:
pass
from .__init__ import ANYWIN, MACOS, PY2, VT100, WINDOWS, E, unicode
from .authsrv import AuthSrv from .authsrv import AuthSrv
from .tcpsrv import TcpSrv
from .up2k import Up2k
from .th_srv import ThumbSrv, HAVE_PIL, HAVE_VIPS, HAVE_WEBP
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE from .mtag import HAVE_FFMPEG, HAVE_FFPROBE
from .tcpsrv import TcpSrv
from .th_srv import HAVE_PIL, HAVE_VIPS, HAVE_WEBP, ThumbSrv
from .up2k import Up2k
from .util import (
VERSIONS,
alltrace,
ansi_re,
min_ex,
mp,
start_log_thrs,
start_stackmon,
)
class SvcHub(object): class SvcHub(object):
""" """
Hosts all services which cannot be parallelized due to reliance on monolithic resources. Hosts all services which cannot be parallelized due to reliance on monolithic resources.
Creates a Broker which does most of the heavy stuff; hosted services can use this to perform work: Creates a Broker which does most of the heavy stuff; hosted services can use this to perform work:
hub.broker.put(want_reply, destination, args_list). hub.broker.<say|ask>(destination, args_list).
Either BrokerThr (plain threads) or BrokerMP (multiprocessing) is used depending on configuration. Either BrokerThr (plain threads) or BrokerMP (multiprocessing) is used depending on configuration.
Nothing is returned synchronously; if you want any value returned from the call, Nothing is returned synchronously; if you want any value returned from the call,
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, argv, printed): def __init__(self, args: argparse.Namespace, argv: list[str], printed: str) -> None:
self.args = args self.args = args
self.argv = argv self.argv = argv
self.logf = None self.logf: Optional[typing.TextIO] = None
self.logf_base_fn = ""
self.stop_req = False self.stop_req = False
self.reload_req = False
self.stopping = False self.stopping = False
self.stopped = False
self.reload_req = False
self.reloading = False self.reloading = False
self.stop_cond = threading.Condition() self.stop_cond = threading.Condition()
self.nsigs = 3
self.retcode = 0 self.retcode = 0
self.httpsrv_up = 0 self.httpsrv_up = 0
self.log_mutex = threading.Lock() self.log_mutex = threading.Lock()
self.next_day = 0 self.next_day = 0
self.tstack = 0.0
if args.sss or args.s >= 3:
args.ss = True
args.lo = args.lo or "cpp-%Y-%m%d-%H%M%S.txt.xz"
args.ls = args.ls or "**,*,ln,p,r"
if args.ss or args.s >= 2:
args.s = True
args.no_logues = True
args.no_readme = True
args.unpost = 0
args.no_del = True
args.no_mv = True
args.hardlink = True
args.vague_403 = True
args.nih = True
if args.s:
args.dotpart = True
args.no_thumb = True
args.no_mtag_ff = True
args.no_robots = True
args.force_js = True
self.log = self._log_disabled if args.q else self._log_enabled self.log = self._log_disabled if args.q else self._log_enabled
if args.lo: if args.lo:
@@ -59,16 +106,16 @@ class SvcHub(object):
if not args.use_fpool and args.j != 1: if not args.use_fpool and args.j != 1:
args.no_fpool = True args.no_fpool = True
m = "multithreading enabled with -j {}, so disabling fpool -- this can reduce upload performance on some filesystems" t = "multithreading enabled with -j {}, so disabling fpool -- this can reduce upload performance on some filesystems"
self.log("root", m.format(args.j)) self.log("root", t.format(args.j))
if not args.no_fpool and args.j != 1: if not args.no_fpool and args.j != 1:
m = "WARNING: --use-fpool combined with multithreading is untested and can probably cause undefined behavior" t = "WARNING: --use-fpool combined with multithreading is untested and can probably cause undefined behavior"
if ANYWIN: if ANYWIN:
m = 'windows cannot do multithreading without --no-fpool, so enabling that -- note that upload performance will suffer if you have microsoft defender "real-time protection" enabled, so you probably want to use -j 1 instead' t = 'windows cannot do multithreading without --no-fpool, so enabling that -- note that upload performance will suffer if you have microsoft defender "real-time protection" enabled, so you probably want to use -j 1 instead'
args.no_fpool = True args.no_fpool = True
self.log("root", m, c=3) self.log("root", t, c=3)
bri = "zy"[args.theme % 2 :][:1] bri = "zy"[args.theme % 2 :][:1]
ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)] ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
@@ -77,6 +124,9 @@ class SvcHub(object):
if not args.hardlink and args.never_symlink: if not args.hardlink and args.never_symlink:
args.no_dedup = True args.no_dedup = True
if args.log_fk:
args.log_fk = re.compile(args.log_fk)
# initiate all services to manage # initiate all services to manage
self.asrv = AuthSrv(self.args, self.log) self.asrv = AuthSrv(self.args, self.log)
if args.ls: if args.ls:
@@ -96,8 +146,8 @@ class SvcHub(object):
self.args.th_dec = list(decs.keys()) self.args.th_dec = list(decs.keys())
self.thumbsrv = None self.thumbsrv = None
if not args.no_thumb: if not args.no_thumb:
m = "decoder preference: {}".format(", ".join(self.args.th_dec)) t = ", ".join(self.args.th_dec) or "(None available)"
self.log("thumb", m) self.log("thumb", "decoder preference: {}".format(t))
if "pil" in self.args.th_dec and not HAVE_WEBP: if "pil" in self.args.th_dec and not HAVE_WEBP:
msg = "disabling webp thumbnails because either libwebp is not available or your Pillow is too old" msg = "disabling webp thumbnails because either libwebp is not available or your Pillow is too old"
@@ -131,11 +181,11 @@ class SvcHub(object):
if self.check_mp_enable(): if self.check_mp_enable():
from .broker_mp import BrokerMp as Broker from .broker_mp import BrokerMp as Broker
else: else:
from .broker_thr import BrokerThr as Broker from .broker_thr import BrokerThr as Broker # type: ignore
self.broker = Broker(self) self.broker = Broker(self)
def thr_httpsrv_up(self): def thr_httpsrv_up(self) -> None:
time.sleep(1 if self.args.ign_ebind_all else 5) time.sleep(1 if self.args.ign_ebind_all else 5)
expected = self.broker.num_workers * self.tcpsrv.nsrv expected = self.broker.num_workers * self.tcpsrv.nsrv
failed = expected - self.httpsrv_up failed = expected - self.httpsrv_up
@@ -145,20 +195,23 @@ class SvcHub(object):
if self.args.ign_ebind_all: if self.args.ign_ebind_all:
if not self.tcpsrv.srv: if not self.tcpsrv.srv:
for _ in range(self.broker.num_workers): for _ in range(self.broker.num_workers):
self.broker.put(False, "cb_httpsrv_up") self.broker.say("cb_httpsrv_up")
return return
if self.args.ign_ebind and self.tcpsrv.srv: if self.args.ign_ebind and self.tcpsrv.srv:
return return
m = "{}/{} workers failed to start" t = "{}/{} workers failed to start"
m = m.format(failed, expected) t = t.format(failed, expected)
self.log("root", m, 1) self.log("root", t, 1)
self.retcode = 1 self.retcode = 1
self.sigterm()
def sigterm(self) -> None:
os.kill(os.getpid(), signal.SIGTERM) os.kill(os.getpid(), signal.SIGTERM)
def cb_httpsrv_up(self): def cb_httpsrv_up(self) -> None:
self.httpsrv_up += 1 self.httpsrv_up += 1
if self.httpsrv_up != self.broker.num_workers: if self.httpsrv_up != self.broker.num_workers:
return return
@@ -171,9 +224,9 @@ class SvcHub(object):
thr.daemon = True thr.daemon = True
thr.start() thr.start()
def _logname(self): def _logname(self) -> str:
dt = datetime.utcnow() dt = datetime.utcnow()
fn = self.args.lo fn = str(self.args.lo)
for fs in "YmdHMS": for fs in "YmdHMS":
fs = "%" + fs fs = "%" + fs
if fs in fn: if fs in fn:
@@ -181,7 +234,7 @@ class SvcHub(object):
return fn return fn
def _setup_logfile(self, printed): def _setup_logfile(self, printed: str) -> None:
base_fn = fn = sel_fn = self._logname() base_fn = fn = sel_fn = self._logname()
if fn != self.args.lo: if fn != self.args.lo:
ctr = 0 ctr = 0
@@ -203,8 +256,6 @@ class SvcHub(object):
lh = codecs.open(fn, "w", encoding="utf-8", errors="replace") lh = codecs.open(fn, "w", encoding="utf-8", errors="replace")
lh.base_fn = base_fn
argv = [sys.executable] + self.argv argv = [sys.executable] + self.argv
if hasattr(shlex, "quote"): if hasattr(shlex, "quote"):
argv = [shlex.quote(x) for x in argv] argv = [shlex.quote(x) for x in argv]
@@ -215,12 +266,13 @@ class SvcHub(object):
printed += msg printed += msg
lh.write("t0: {:.3f}\nargv: {}\n\n{}".format(E.t0, " ".join(argv), printed)) lh.write("t0: {:.3f}\nargv: {}\n\n{}".format(E.t0, " ".join(argv), printed))
self.logf = lh self.logf = lh
self.logf_base_fn = base_fn
print(msg, end="") print(msg, end="")
def run(self): def run(self) -> None:
self.tcpsrv.run() self.tcpsrv.run()
thr = threading.Thread(target=self.thr_httpsrv_up) thr = threading.Thread(target=self.thr_httpsrv_up, name="sig-hsrv-up2")
thr.daemon = True thr.daemon = True
thr.start() thr.start()
@@ -248,21 +300,23 @@ class SvcHub(object):
pass pass
self.shutdown() self.shutdown()
thr.join() # cant join; eats signals on win10
while not self.stopped:
time.sleep(0.1)
else: else:
self.stop_thr() self.stop_thr()
def reload(self): def reload(self) -> str:
if self.reloading: if self.reloading:
return "cannot reload; already in progress" return "cannot reload; already in progress"
self.reloading = True self.reloading = True
t = threading.Thread(target=self._reload) t = threading.Thread(target=self._reload, name="reloading")
t.daemon = True t.daemon = True
t.start() t.start()
return "reload initiated" return "reload initiated"
def _reload(self): def _reload(self) -> None:
self.log("root", "reload scheduled") self.log("root", "reload scheduled")
with self.up2k.mutex: with self.up2k.mutex:
self.asrv.reload() self.asrv.reload()
@@ -271,7 +325,7 @@ class SvcHub(object):
self.reloading = False self.reloading = False
def stop_thr(self): def stop_thr(self) -> None:
while not self.stop_req: while not self.stop_req:
with self.stop_cond: with self.stop_cond:
self.stop_cond.wait(9001) self.stop_cond.wait(9001)
@@ -282,11 +336,24 @@ class SvcHub(object):
self.shutdown() self.shutdown()
def signal_handler(self, sig, frame): def signal_handler(self, sig: int, frame: Optional[FrameType]) -> None:
if self.stopping: if self.stopping:
return if self.nsigs <= 0:
try:
threading.Thread(target=self.pr, args=("OMBO BREAKER",)).start()
time.sleep(0.1)
except:
pass
if sig == signal.SIGUSR1: if ANYWIN:
os.system("taskkill /f /pid {}".format(os.getpid()))
else:
os.kill(os.getpid(), signal.SIGKILL)
else:
self.nsigs -= 1
return
if not ANYWIN and sig == signal.SIGUSR1:
self.reload_req = True self.reload_req = True
else: else:
self.stop_req = True self.stop_req = True
@@ -294,7 +361,7 @@ class SvcHub(object):
with self.stop_cond: with self.stop_cond:
self.stop_cond.notify_all() self.stop_cond.notify_all()
def shutdown(self): def shutdown(self) -> None:
if self.stopping: if self.stopping:
return return
@@ -307,9 +374,7 @@ class SvcHub(object):
ret = 1 ret = 1
try: try:
with self.log_mutex: self.pr("OPYTHAT")
print("OPYTHAT")
self.tcpsrv.shutdown() self.tcpsrv.shutdown()
self.broker.shutdown() self.broker.shutdown()
self.up2k.shutdown() self.up2k.shutdown()
@@ -322,22 +387,26 @@ class SvcHub(object):
break break
if n == 3: if n == 3:
print("waiting for thumbsrv (10sec)...") self.pr("waiting for thumbsrv (10sec)...")
print("nailed it", end="") self.pr("nailed it", end="")
ret = self.retcode ret = self.retcode
except:
self.pr("\033[31m[ error during shutdown ]\n{}\033[0m".format(min_ex()))
raise
finally: finally:
if self.args.wintitle: if self.args.wintitle:
print("\033]0;\033\\", file=sys.stderr, end="") print("\033]0;\033\\", file=sys.stderr, end="")
sys.stderr.flush() sys.stderr.flush()
print("\033[0m") self.pr("\033[0m")
if self.logf: if self.logf:
self.logf.close() self.logf.close()
self.stopped = True
sys.exit(ret) sys.exit(ret)
def _log_disabled(self, src, msg, c=0): def _log_disabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None:
if not self.logf: if not self.logf:
return return
@@ -349,8 +418,8 @@ class SvcHub(object):
if now >= self.next_day: if now >= self.next_day:
self._set_next_day() self._set_next_day()
def _set_next_day(self): def _set_next_day(self) -> None:
if self.next_day and self.logf and self.logf.base_fn != self._logname(): if self.next_day and self.logf and self.logf_base_fn != self._logname():
self.logf.close() self.logf.close()
self._setup_logfile("") self._setup_logfile("")
@@ -364,7 +433,7 @@ class SvcHub(object):
dt = dt.replace(hour=0, minute=0, second=0) dt = dt.replace(hour=0, minute=0, second=0)
self.next_day = calendar.timegm(dt.utctimetuple()) self.next_day = calendar.timegm(dt.utctimetuple())
def _log_enabled(self, src, msg, c=0): def _log_enabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None:
"""handles logging from all components""" """handles logging from all components"""
with self.log_mutex: with self.log_mutex:
now = time.time() now = time.time()
@@ -401,7 +470,11 @@ class SvcHub(object):
if self.logf: if self.logf:
self.logf.write(msg) self.logf.write(msg)
def check_mp_support(self): def pr(self, *a: Any, **ka: Any) -> None:
with self.log_mutex:
print(*a, **ka)
def check_mp_support(self) -> str:
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;"
@@ -415,16 +488,16 @@ class SvcHub(object):
return msg return msg
try: try:
x = mp.Queue(1) x: mp.Queue[tuple[str, str]] = mp.Queue(1)
x.put(["foo", "bar"]) x.put(("foo", "bar"))
if x.get()[0] != "foo": if x.get()[0] != "foo":
raise Exception() raise Exception()
except: except:
return "multiprocessing is not supported on your platform;" return "multiprocessing is not supported on your platform;"
return None return ""
def check_mp_enable(self): def check_mp_enable(self) -> bool:
if self.args.j == 1: if self.args.j == 1:
return False return False
@@ -447,21 +520,34 @@ class SvcHub(object):
self.log("svchub", "cannot efficiently use multiple CPU cores") self.log("svchub", "cannot efficiently use multiple CPU cores")
return False return False
def sd_notify(self): def sd_notify(self) -> None:
try: try:
addr = os.getenv("NOTIFY_SOCKET") zb = os.getenv("NOTIFY_SOCKET")
if not addr: if not zb:
return return
addr = unicode(addr) addr = unicode(zb)
if addr.startswith("@"): if addr.startswith("@"):
addr = "\0" + addr[1:] addr = "\0" + addr[1:]
m = "".join(x for x in addr if x in string.printable) t = "".join(x for x in addr if x in string.printable)
self.log("sd_notify", m) self.log("sd_notify", t)
sck = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) sck = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
sck.connect(addr) sck.connect(addr)
sck.sendall(b"READY=1") sck.sendall(b"READY=1")
except: except:
self.log("sd_notify", min_ex()) self.log("sd_notify", min_ex())
def log_stacks(self) -> None:
td = time.time() - self.tstack
if td < 300:
self.log("stacks", "cooldown {}".format(td))
return
self.tstack = time.time()
zs = "{}\n{}".format(VERSIONS, alltrace())
zb = zs.encode("utf-8", "replace")
zb = gzip.compress(zb)
zs = base64.b64encode(zb).decode("ascii")
self.log("stacks", zs)

View File

@@ -1,16 +1,23 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import calendar
import time import time
import zlib import zlib
import calendar
from .sutil import errdesc
from .util import yieldfile, sanitize_fn, spack, sunpack, min_ex
from .bos import bos from .bos import bos
from .sutil import StreamArc, errdesc
from .util import min_ex, sanitize_fn, spack, sunpack, yieldfile
try:
from typing import Any, Generator, Optional
from .util import NamedLogger
except:
pass
def dostime2unix(buf): def dostime2unix(buf: bytes) -> int:
t, d = sunpack(b"<HH", buf) t, d = sunpack(b"<HH", buf)
ts = (t & 0x1F) * 2 ts = (t & 0x1F) * 2
@@ -29,7 +36,7 @@ def dostime2unix(buf):
return int(calendar.timegm(dt)) return int(calendar.timegm(dt))
def unixtime2dos(ts): def unixtime2dos(ts: int) -> bytes:
tt = time.gmtime(ts + 1) tt = time.gmtime(ts + 1)
dy, dm, dd, th, tm, ts = list(tt)[:6] dy, dm, dd, th, tm, ts = list(tt)[:6]
@@ -41,14 +48,22 @@ def unixtime2dos(ts):
return b"\x00\x00\x21\x00" return b"\x00\x00\x21\x00"
def gen_fdesc(sz, crc32, z64): def gen_fdesc(sz: int, crc32: int, z64: bool) -> bytes:
ret = b"\x50\x4b\x07\x08" ret = b"\x50\x4b\x07\x08"
fmt = b"<LQQ" if z64 else b"<LLL" fmt = b"<LQQ" if z64 else b"<LLL"
ret += spack(fmt, crc32, sz, sz) ret += spack(fmt, crc32, sz, sz)
return ret return ret
def gen_hdr(h_pos, fn, sz, lastmod, utf8, crc32, pre_crc): def gen_hdr(
h_pos: Optional[int],
fn: str,
sz: int,
lastmod: int,
utf8: bool,
icrc32: int,
pre_crc: bool,
) -> bytes:
""" """
does regular file headers does regular file headers
and the central directory meme if h_pos is set and the central directory meme if h_pos is set
@@ -67,8 +82,8 @@ def gen_hdr(h_pos, fn, sz, lastmod, utf8, crc32, pre_crc):
# confusingly this doesn't bump if h_pos # confusingly this doesn't bump if h_pos
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 icrc32:
crc32 = spack(b"<L", crc32) crc32 = spack(b"<L", icrc32)
else: else:
crc32 = b"\x00" * 4 crc32 = b"\x00" * 4
@@ -129,7 +144,9 @@ def gen_hdr(h_pos, fn, sz, lastmod, utf8, crc32, pre_crc):
return ret return ret
def gen_ecdr(items, cdir_pos, cdir_end): def gen_ecdr(
items: list[tuple[str, int, int, int, int]], cdir_pos: int, cdir_end: int
) -> tuple[bytes, bool]:
""" """
summary of all file headers, summary of all file headers,
usually the zipfile footer unless something clamps usually the zipfile footer unless something clamps
@@ -154,10 +171,12 @@ def gen_ecdr(items, cdir_pos, cdir_end):
# 2b comment length # 2b comment length
ret += b"\x00\x00" ret += b"\x00\x00"
return [ret, need_64] return ret, need_64
def gen_ecdr64(items, cdir_pos, cdir_end): def gen_ecdr64(
items: list[tuple[str, int, int, int, int]], cdir_pos: int, cdir_end: int
) -> bytes:
""" """
z64 end of central directory z64 end of central directory
added when numfiles or a headerptr clamps added when numfiles or a headerptr clamps
@@ -181,7 +200,7 @@ def gen_ecdr64(items, cdir_pos, cdir_end):
return ret return ret
def gen_ecdr64_loc(ecdr64_pos): def gen_ecdr64_loc(ecdr64_pos: int) -> bytes:
""" """
z64 end of central directory locator z64 end of central directory locator
points to ecdr64 points to ecdr64
@@ -196,21 +215,27 @@ def gen_ecdr64_loc(ecdr64_pos):
return ret return ret
class StreamZip(object): class StreamZip(StreamArc):
def __init__(self, log, fgen, utf8=False, pre_crc=False): def __init__(
self.log = log self,
self.fgen = fgen log: "NamedLogger",
fgen: Generator[dict[str, Any], None, None],
utf8: bool = False,
pre_crc: bool = False,
) -> None:
super(StreamZip, self).__init__(log, fgen)
self.utf8 = utf8 self.utf8 = utf8
self.pre_crc = pre_crc self.pre_crc = pre_crc
self.pos = 0 self.pos = 0
self.items = [] self.items: list[tuple[str, int, int, int, int]] = []
def _ct(self, buf): def _ct(self, buf: bytes) -> bytes:
self.pos += len(buf) self.pos += len(buf)
return buf return buf
def ser(self, f): def ser(self, f: dict[str, Any]) -> Generator[bytes, None, None]:
name = f["vp"] name = f["vp"]
src = f["ap"] src = f["ap"]
st = f["st"] st = f["st"]
@@ -218,9 +243,8 @@ class StreamZip(object):
sz = st.st_size sz = st.st_size
ts = st.st_mtime ts = st.st_mtime
crc = None crc = 0
if self.pre_crc: if self.pre_crc:
crc = 0
for buf in yieldfile(src): for buf in yieldfile(src):
crc = zlib.crc32(buf, crc) crc = zlib.crc32(buf, crc)
@@ -230,7 +254,6 @@ class StreamZip(object):
buf = gen_hdr(None, name, sz, ts, self.utf8, crc, self.pre_crc) buf = gen_hdr(None, name, sz, ts, self.utf8, crc, self.pre_crc)
yield self._ct(buf) yield self._ct(buf)
crc = crc or 0
for buf in yieldfile(src): for buf in yieldfile(src):
if not self.pre_crc: if not self.pre_crc:
crc = zlib.crc32(buf, crc) crc = zlib.crc32(buf, crc)
@@ -239,7 +262,7 @@ class StreamZip(object):
crc &= 0xFFFFFFFF crc &= 0xFFFFFFFF
self.items.append([name, sz, ts, crc, h_pos]) self.items.append((name, sz, ts, crc, h_pos))
z64 = sz >= 4 * 1024 * 1024 * 1024 z64 = sz >= 4 * 1024 * 1024 * 1024
@@ -247,43 +270,46 @@ class StreamZip(object):
buf = gen_fdesc(sz, crc, z64) buf = gen_fdesc(sz, crc, z64)
yield self._ct(buf) yield self._ct(buf)
def gen(self): def gen(self) -> Generator[bytes, None, None]:
errors = [] errors = []
for f in self.fgen: try:
if "err" in f: for f in self.fgen:
errors.append([f["vp"], f["err"]]) if "err" in f:
continue errors.append((f["vp"], f["err"]))
continue
try: try:
for x in self.ser(f): for x in self.ser(f):
yield x
except GeneratorExit:
raise
except:
ex = min_ex(5, True).replace("\n", "\n-- ")
errors.append((f["vp"], ex))
if errors:
errf, txt = errdesc(errors)
self.log("\n".join(([repr(errf)] + txt[1:])))
for x in self.ser(errf):
yield x yield x
except Exception:
ex = min_ex(5, True).replace("\n", "\n-- ")
errors.append([f["vp"], ex])
if errors: cdir_pos = self.pos
errf, txt = errdesc(errors) for name, sz, ts, crc, h_pos in self.items:
self.log("\n".join(([repr(errf)] + txt[1:]))) buf = gen_hdr(h_pos, name, sz, ts, self.utf8, crc, self.pre_crc)
for x in self.ser(errf): yield self._ct(buf)
yield x cdir_end = self.pos
cdir_pos = self.pos _, need_64 = gen_ecdr(self.items, cdir_pos, cdir_end)
for name, sz, ts, crc, h_pos in self.items: if need_64:
buf = gen_hdr(h_pos, name, sz, ts, self.utf8, crc, self.pre_crc) ecdir64_pos = self.pos
yield self._ct(buf) buf = gen_ecdr64(self.items, cdir_pos, cdir_end)
cdir_end = self.pos yield self._ct(buf)
_, need_64 = gen_ecdr(self.items, cdir_pos, cdir_end) buf = gen_ecdr64_loc(ecdir64_pos)
if need_64: yield self._ct(buf)
ecdir64_pos = self.pos
buf = gen_ecdr64(self.items, cdir_pos, cdir_end)
yield self._ct(buf)
buf = gen_ecdr64_loc(ecdir64_pos) ecdr, _ = gen_ecdr(self.items, cdir_pos, cdir_end)
yield self._ct(buf) yield self._ct(ecdr)
finally:
ecdr, _ = gen_ecdr(self.items, cdir_pos, cdir_end) if errors:
yield self._ct(ecdr) bos.unlink(errf["ap"])
if errors:
bos.unlink(errf["ap"])

View File

@@ -1,13 +1,17 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import os
import re import re
import sys
import socket import socket
import sys
from .__init__ import MACOS, ANYWIN, unicode from .__init__ import ANYWIN, MACOS, TYPE_CHECKING, unicode
from .util import chkcmd from .util import chkcmd
if TYPE_CHECKING:
from .svchub import SvcHub
class TcpSrv(object): class TcpSrv(object):
""" """
@@ -15,16 +19,16 @@ class TcpSrv(object):
which then uses the least busy HttpSrv to handle it which then uses the least busy HttpSrv to handle it
""" """
def __init__(self, hub): def __init__(self, hub: "SvcHub"):
self.hub = hub self.hub = hub
self.args = hub.args self.args = hub.args
self.log = hub.log self.log = hub.log
self.stopping = False self.stopping = False
self.srv = [] self.srv: list[socket.socket] = []
self.nsrv = 0 self.nsrv = 0
ok = {} ok: dict[str, list[int]] = {}
for ip in self.args.i: for ip in self.args.i:
ok[ip] = [] ok[ip] = []
for port in self.args.p: for port in self.args.p:
@@ -34,8 +38,8 @@ class TcpSrv(object):
ok[ip].append(port) ok[ip].append(port)
except Exception as ex: except Exception as ex:
if self.args.ign_ebind or self.args.ign_ebind_all: if self.args.ign_ebind or self.args.ign_ebind_all:
m = "could not listen on {}:{}: {}" t = "could not listen on {}:{}: {}"
self.log("tcpsrv", m.format(ip, port, ex), c=3) self.log("tcpsrv", t.format(ip, port, ex), c=3)
else: else:
raise raise
@@ -55,9 +59,9 @@ class TcpSrv(object):
eps[x] = "external" eps[x] = "external"
msgs = [] msgs = []
title_tab = {} title_tab: dict[str, dict[str, int]] = {}
title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")] title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")]
m = "available @ {}://{}:{}/ (\033[33m{}\033[0m)" t = "available @ {}://{}:{}/ (\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):
if port not in ok.get(ip, ok.get("0.0.0.0", [])): if port not in ok.get(ip, ok.get("0.0.0.0", [])):
@@ -69,7 +73,7 @@ class TcpSrv(object):
elif self.args.https_only or port == 443: elif self.args.https_only or port == 443:
proto = "https" proto = "https"
msgs.append(m.format(proto, ip, port, desc)) msgs.append(t.format(proto, ip, port, desc))
if not self.args.wintitle: if not self.args.wintitle:
continue continue
@@ -98,13 +102,13 @@ class TcpSrv(object):
if msgs: if msgs:
msgs[-1] += "\n" msgs[-1] += "\n"
for m in msgs: for t in msgs:
self.log("tcpsrv", m) self.log("tcpsrv", t)
if self.args.wintitle: if self.args.wintitle:
self._set_wintitle(title_tab) self._set_wintitle(title_tab)
def _listen(self, ip, port): def _listen(self, ip: str, port: int) -> None:
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) srv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
@@ -120,19 +124,19 @@ class TcpSrv(object):
raise raise
raise Exception(e) raise Exception(e)
def run(self): def run(self) -> None:
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()
fno = srv.fileno() fno = srv.fileno()
msg = "listening @ {}:{} f{}".format(ip, port, fno) msg = "listening @ {}:{} f{} p{}".format(ip, port, fno, os.getpid())
self.log("tcpsrv", msg) self.log("tcpsrv", msg)
if self.args.q: if self.args.q:
print(msg) print(msg)
self.hub.broker.put(False, "listen", srv) self.hub.broker.say("listen", srv)
def shutdown(self): def shutdown(self) -> None:
self.stopping = True self.stopping = True
try: try:
for srv in self.srv: for srv in self.srv:
@@ -142,30 +146,59 @@ class TcpSrv(object):
self.log("tcpsrv", "ok bye") self.log("tcpsrv", "ok bye")
def ips_linux(self): def ips_linux_ifconfig(self) -> dict[str, str]:
eps = {} # for termux
try:
txt, _ = chkcmd(["ifconfig"])
except:
return {}
eps: dict[str, str] = {}
dev = None
ip = None
up = None
for ln in (txt + "\n").split("\n"):
if not ln.strip() and dev and ip:
eps[ip] = dev + ("" if up else ", \033[31mLINK-DOWN")
dev = ip = up = None
continue
if ln == ln.lstrip():
dev = re.split(r"[: ]", ln)[0]
if "UP" in re.split(r"[<>, \t]", ln):
up = True
m = re.match(r"^\s+inet\s+([^ ]+)", ln)
if m:
ip = m.group(1)
return eps
def ips_linux(self) -> dict[str, str]:
try: try:
txt, _ = chkcmd(["ip", "addr"]) txt, _ = chkcmd(["ip", "addr"])
except: except:
return eps return self.ips_linux_ifconfig()
r = re.compile(r"^\s+inet ([^ ]+)/.* (.*)") r = re.compile(r"^\s+inet ([^ ]+)/.* (.*)")
ri = re.compile(r"^\s*[0-9]+\s*:.*") ri = re.compile(r"^\s*[0-9]+\s*:.*")
up = False up = False
eps: dict[str, str] = {}
for ln in txt.split("\n"): for ln in txt.split("\n"):
if ri.match(ln): if ri.match(ln):
up = "UP" in re.split("[>,< ]", ln) up = "UP" in re.split("[>,< ]", ln)
try: try:
ip, dev = r.match(ln.rstrip()).groups() ip, dev = r.match(ln.rstrip()).groups() # type: ignore
eps[ip] = dev + ("" if up else ", \033[31mLINK-DOWN") eps[ip] = dev + ("" if up else ", \033[31mLINK-DOWN")
except: except:
pass pass
return eps return eps
def ips_macos(self): def ips_macos(self) -> dict[str, str]:
eps = {} eps: dict[str, str] = {}
try: try:
txt, _ = chkcmd(["ifconfig"]) txt, _ = chkcmd(["ifconfig"])
except: except:
@@ -173,7 +206,7 @@ class TcpSrv(object):
rdev = re.compile(r"^([^ ]+):") rdev = re.compile(r"^([^ ]+):")
rip = re.compile(r"^\tinet ([0-9\.]+) ") rip = re.compile(r"^\tinet ([0-9\.]+) ")
dev = None dev = "UNKNOWN"
for ln in txt.split("\n"): for ln in txt.split("\n"):
m = rdev.match(ln) m = rdev.match(ln)
if m: if m:
@@ -182,17 +215,17 @@ class TcpSrv(object):
m = rip.match(ln) m = rip.match(ln)
if m: if m:
eps[m.group(1)] = dev eps[m.group(1)] = dev
dev = None dev = "UNKNOWN"
return eps return eps
def ips_windows_ipconfig(self): def ips_windows_ipconfig(self) -> tuple[dict[str, str], set[str]]:
eps = {} eps: dict[str, str] = {}
offs = {} offs: set[str] = set()
try: try:
txt, _ = chkcmd(["ipconfig"]) txt, _ = chkcmd(["ipconfig"])
except: except:
return eps return eps, offs
rdev = re.compile(r"(^[^ ].*):$") rdev = re.compile(r"(^[^ ].*):$")
rip = re.compile(r"^ +IPv?4? [^:]+: *([0-9\.]{7,15})$") rip = re.compile(r"^ +IPv?4? [^:]+: *([0-9\.]{7,15})$")
@@ -202,12 +235,12 @@ class TcpSrv(object):
m = rdev.match(ln) m = rdev.match(ln)
if m: if m:
if dev and dev not in eps.values(): if dev and dev not in eps.values():
offs[dev] = 1 offs.add(dev)
dev = m.group(1).split(" adapter ", 1)[-1] dev = m.group(1).split(" adapter ", 1)[-1]
if dev and roff.match(ln): if dev and roff.match(ln):
offs[dev] = 1 offs.add(dev)
dev = None dev = None
m = rip.match(ln) m = rip.match(ln)
@@ -216,12 +249,12 @@ class TcpSrv(object):
dev = None dev = None
if dev and dev not in eps.values(): if dev and dev not in eps.values():
offs[dev] = 1 offs.add(dev)
return eps, offs return eps, offs
def ips_windows_netsh(self): def ips_windows_netsh(self) -> dict[str, str]:
eps = {} eps: dict[str, str] = {}
try: try:
txt, _ = chkcmd("netsh interface ip show address".split()) txt, _ = chkcmd("netsh interface ip show address".split())
except: except:
@@ -241,7 +274,7 @@ class TcpSrv(object):
return eps return eps
def detect_interfaces(self, listen_ips): def detect_interfaces(self, listen_ips: list[str]) -> dict[str, str]:
if MACOS: if MACOS:
eps = self.ips_macos() eps = self.ips_macos()
elif ANYWIN: elif ANYWIN:
@@ -268,7 +301,6 @@ class TcpSrv(object):
]: ]:
try: try:
s.connect((ip, 1)) s.connect((ip, 1))
# raise OSError(13, "a")
default_route = s.getsockname()[0] default_route = s.getsockname()[0]
break break
except (OSError, socket.error) as ex: except (OSError, socket.error) as ex:
@@ -289,23 +321,23 @@ class TcpSrv(object):
return eps return eps
def _set_wintitle(self, vars): def _set_wintitle(self, vs: dict[str, dict[str, int]]) -> None:
vars["all"] = vars.get("all", {"Local-Only": 1}) vs["all"] = vs.get("all", {"Local-Only": 1})
vars["pub"] = vars.get("pub", vars["all"]) vs["pub"] = vs.get("pub", vs["all"])
vars2 = {} vs2 = {}
for k, eps in vars.items(): for k, eps in vs.items():
vars2[k] = { vs2[k] = {
ep: 1 ep: 1
for ep in eps.keys() for ep in eps.keys()
if ":" not in ep or ep.split(":")[0] not in eps if ":" not in ep or ep.split(":")[0] not in eps
} }
title = "" title = ""
vars = vars2 vs = vs2
for p in self.args.wintitle.split(" "): for p in self.args.wintitle.split(" "):
if p.startswith("$"): if p.startswith("$"):
p = " and ".join(sorted(vars.get(p[1:], {"(None)": 1}).keys())) p = " and ".join(sorted(vs.get(p[1:], {"(None)": 1}).keys()))
title += "{} ".format(p) title += "{} ".format(p)

View File

@@ -3,13 +3,23 @@ from __future__ import print_function, unicode_literals
import os import os
from .util import Cooldown from .__init__ import TYPE_CHECKING
from .th_srv import thumb_path, HAVE_WEBP from .authsrv import VFS
from .bos import bos from .bos import bos
from .th_srv import HAVE_WEBP, thumb_path
from .util import Cooldown
try:
from typing import Optional, Union
except:
pass
if TYPE_CHECKING:
from .httpsrv import HttpSrv
class ThumbCli(object): class ThumbCli(object):
def __init__(self, hsrv): def __init__(self, hsrv: "HttpSrv") -> None:
self.broker = hsrv.broker self.broker = hsrv.broker
self.log_func = hsrv.log self.log_func = hsrv.log
self.args = hsrv.args self.args = hsrv.args
@@ -34,10 +44,10 @@ class ThumbCli(object):
d = next((x for x in self.args.th_dec if x in ("vips", "pil")), None) d = next((x for x in self.args.th_dec if x in ("vips", "pil")), None)
self.can_webp = HAVE_WEBP or d == "vips" self.can_webp = HAVE_WEBP or d == "vips"
def log(self, msg, c=0): def log(self, msg: str, c: Union[int, str] = 0) -> None:
self.log_func("thumbcli", msg, c) self.log_func("thumbcli", msg, c)
def get(self, dbv, rem, mtime, fmt): def get(self, dbv: VFS, rem: str, mtime: float, fmt: str) -> Optional[str]:
ptop = dbv.realpath ptop = dbv.realpath
ext = rem.rsplit(".")[-1].lower() ext = rem.rsplit(".")[-1].lower()
if ext not in self.thumbable or "dthumb" in dbv.flags: if ext not in self.thumbable or "dthumb" in dbv.flags:
@@ -106,17 +116,20 @@ class ThumbCli(object):
if ret: if ret:
tdir = os.path.dirname(tpath) tdir = os.path.dirname(tpath)
if self.cooldown.poke(tdir): if self.cooldown.poke(tdir):
self.broker.put(False, "thumbsrv.poke", tdir) self.broker.say("thumbsrv.poke", tdir)
if want_opus: if want_opus:
# audio files expire individually # audio files expire individually
if self.cooldown.poke(tpath): if self.cooldown.poke(tpath):
self.broker.put(False, "thumbsrv.poke", tpath) self.broker.say("thumbsrv.poke", tpath)
return ret return ret
if abort: if abort:
return None return None
x = self.broker.put(True, "thumbsrv.get", ptop, rem, mtime, fmt) if not bos.path.getsize(os.path.join(ptop, rem)):
return x.get() return None
x = self.broker.ask("thumbsrv.get", ptop, rem, mtime, fmt)
return x.get() # type: ignore

View File

@@ -1,19 +1,28 @@
# 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 shutil
import base64 import base64
import hashlib import hashlib
import threading import os
import shutil
import subprocess as sp import subprocess as sp
import threading
import time
from .__init__ import PY2, unicode from queue import Queue
from .util import fsenc, vsplit, statdir, runcmd, Queue, Cooldown, BytesIO, min_ex
from .__init__ import TYPE_CHECKING
from .bos import bos from .bos import bos
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
from .util import BytesIO, Cooldown, fsenc, min_ex, runcmd, statdir, vsplit
try:
from typing import Optional, Union
except:
pass
if TYPE_CHECKING:
from .svchub import SvcHub
HAVE_PIL = False HAVE_PIL = False
HAVE_HEIF = False HAVE_HEIF = False
@@ -21,7 +30,7 @@ HAVE_AVIF = False
HAVE_WEBP = False HAVE_WEBP = False
try: try:
from PIL import Image, ImageOps, ExifTags from PIL import ExifTags, Image, ImageOps
HAVE_PIL = True HAVE_PIL = True
try: try:
@@ -39,7 +48,7 @@ try:
pass pass
try: try:
import pillow_avif import pillow_avif # noqa: F401 # pylint: disable=unused-import
HAVE_AVIF = True HAVE_AVIF = True
except: except:
@@ -48,14 +57,13 @@ except:
pass pass
try: try:
import pyvips
HAVE_VIPS = True HAVE_VIPS = True
import pyvips
except: except:
HAVE_VIPS = False HAVE_VIPS = False
def thumb_path(histpath, rem, mtime, fmt): def thumb_path(histpath: str, rem: str, mtime: float, fmt: str) -> str:
# base16 = 16 = 256 # base16 = 16 = 256
# b64-lc = 38 = 1444 # b64-lc = 38 = 1444
# base64 = 64 = 4096 # base64 = 64 = 4096
@@ -81,7 +89,7 @@ def thumb_path(histpath, rem, mtime, fmt):
class ThumbSrv(object): class ThumbSrv(object):
def __init__(self, hub): def __init__(self, hub: "SvcHub") -> None:
self.hub = hub self.hub = hub
self.asrv = hub.asrv self.asrv = hub.asrv
self.args = hub.args self.args = hub.args
@@ -92,17 +100,17 @@ class ThumbSrv(object):
self.poke_cd = Cooldown(self.args.th_poke) self.poke_cd = Cooldown(self.args.th_poke)
self.mutex = threading.Lock() self.mutex = threading.Lock()
self.busy = {} self.busy: dict[str, list[threading.Condition]] = {}
self.stopping = False self.stopping = False
self.nthr = max(1, self.args.th_mt) self.nthr = max(1, self.args.th_mt)
self.q = Queue(self.nthr * 4) self.q: Queue[Optional[tuple[str, str]]] = Queue(self.nthr * 4)
for n in range(self.nthr): for n in range(self.nthr):
t = threading.Thread( thr = threading.Thread(
target=self.worker, name="thumb-{}-{}".format(n, self.nthr) target=self.worker, name="thumb-{}-{}".format(n, self.nthr)
) )
t.daemon = True thr.daemon = True
t.start() thr.start()
want_ff = not self.args.no_vthumb or not self.args.no_athumb want_ff = not self.args.no_vthumb or not self.args.no_athumb
if want_ff and (not HAVE_FFMPEG or not HAVE_FFPROBE): if want_ff and (not HAVE_FFMPEG or not HAVE_FFPROBE):
@@ -123,7 +131,7 @@ class ThumbSrv(object):
t.start() t.start()
self.fmt_pil, self.fmt_vips, self.fmt_ffi, self.fmt_ffv, self.fmt_ffa = [ self.fmt_pil, self.fmt_vips, self.fmt_ffi, self.fmt_ffv, self.fmt_ffa = [
{x: True for x in y.split(",")} set(y.split(","))
for y in [ for y in [
self.args.th_r_pil, self.args.th_r_pil,
self.args.th_r_vips, self.args.th_r_vips,
@@ -135,37 +143,37 @@ class ThumbSrv(object):
if not HAVE_HEIF: if not HAVE_HEIF:
for f in "heif heifs heic heics".split(" "): for f in "heif heifs heic heics".split(" "):
self.fmt_pil.pop(f, None) self.fmt_pil.discard(f)
if not HAVE_AVIF: if not HAVE_AVIF:
for f in "avif avifs".split(" "): for f in "avif avifs".split(" "):
self.fmt_pil.pop(f, None) self.fmt_pil.discard(f)
self.thumbable = {} self.thumbable: set[str] = set()
if "pil" in self.args.th_dec: if "pil" in self.args.th_dec:
self.thumbable.update(self.fmt_pil) self.thumbable |= self.fmt_pil
if "vips" in self.args.th_dec: if "vips" in self.args.th_dec:
self.thumbable.update(self.fmt_vips) self.thumbable |= self.fmt_vips
if "ff" in self.args.th_dec: if "ff" in self.args.th_dec:
for t in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]: for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
self.thumbable.update(t) self.thumbable |= zss
def log(self, msg, c=0): def log(self, msg: str, c: Union[int, str] = 0) -> None:
self.log_func("thumb", msg, c) self.log_func("thumb", msg, c)
def shutdown(self): def shutdown(self) -> None:
self.stopping = True self.stopping = True
for _ in range(self.nthr): for _ in range(self.nthr):
self.q.put(None) self.q.put(None)
def stopped(self): def stopped(self) -> bool:
with self.mutex: with self.mutex:
return not self.nthr return not self.nthr
def get(self, ptop, rem, mtime, fmt): def get(self, ptop: str, rem: str, mtime: float, fmt: str) -> Optional[str]:
histpath = self.asrv.vfs.histtab.get(ptop) histpath = self.asrv.vfs.histtab.get(ptop)
if not histpath: if not histpath:
self.log("no histpath for [{}]".format(ptop)) self.log("no histpath for [{}]".format(ptop))
@@ -192,7 +200,7 @@ class ThumbSrv(object):
do_conv = True do_conv = True
if do_conv: if do_conv:
self.q.put([abspath, tpath]) self.q.put((abspath, tpath))
self.log("conv {} \033[0m{}".format(tpath, abspath), c=6) self.log("conv {} \033[0m{}".format(tpath, abspath), c=6)
while not self.stopping: while not self.stopping:
@@ -213,7 +221,7 @@ class ThumbSrv(object):
return None return None
def getcfg(self): def getcfg(self) -> dict[str, set[str]]:
return { return {
"thumbable": self.thumbable, "thumbable": self.thumbable,
"pil": self.fmt_pil, "pil": self.fmt_pil,
@@ -223,7 +231,7 @@ class ThumbSrv(object):
"ffa": self.fmt_ffa, "ffa": self.fmt_ffa,
} }
def worker(self): def worker(self) -> None:
while not self.stopping: while not self.stopping:
task = self.q.get() task = self.q.get()
if not task: if not task:
@@ -254,7 +262,7 @@ class ThumbSrv(object):
except: except:
msg = "{} could not create thumbnail of {}\n{}" msg = "{} could not create thumbnail of {}\n{}"
msg = msg.format(fun.__name__, abspath, min_ex()) msg = msg.format(fun.__name__, abspath, min_ex())
c = 1 if "<Signals.SIG" in msg else "1;30" c: Union[str, int] = 1 if "<Signals.SIG" in msg else "1;30"
self.log(msg, c) self.log(msg, c)
with open(tpath, "wb") as _: with open(tpath, "wb") as _:
pass pass
@@ -270,7 +278,7 @@ class ThumbSrv(object):
with self.mutex: with self.mutex:
self.nthr -= 1 self.nthr -= 1
def fancy_pillow(self, im): def fancy_pillow(self, im: "Image.Image") -> "Image.Image":
# exif_transpose is expensive (loads full image + unconditional copy) # exif_transpose is expensive (loads full image + unconditional copy)
r = max(*self.res) * 2 r = max(*self.res) * 2
im.thumbnail((r, r), resample=Image.LANCZOS) im.thumbnail((r, r), resample=Image.LANCZOS)
@@ -296,7 +304,7 @@ class ThumbSrv(object):
return im return im
def conv_pil(self, abspath, tpath): def conv_pil(self, abspath: str, tpath: str) -> None:
with Image.open(fsenc(abspath)) as im: with Image.open(fsenc(abspath)) as im:
try: try:
im = self.fancy_pillow(im) im = self.fancy_pillow(im)
@@ -325,7 +333,7 @@ class ThumbSrv(object):
im.save(tpath, **args) im.save(tpath, **args)
def conv_vips(self, abspath, tpath): def conv_vips(self, abspath: str, tpath: str) -> None:
crops = ["centre", "none"] crops = ["centre", "none"]
if self.args.th_no_crop: if self.args.th_no_crop:
crops = ["none"] crops = ["none"]
@@ -343,18 +351,17 @@ class ThumbSrv(object):
img.write_to_file(tpath, Q=40) img.write_to_file(tpath, Q=40)
def conv_ffmpeg(self, abspath, tpath): def conv_ffmpeg(self, abspath: str, tpath: str) -> None:
ret, _ = ffprobe(abspath) ret, _ = ffprobe(abspath)
if not ret: if not ret:
return return
ext = abspath.rsplit(".")[-1].lower() ext = abspath.rsplit(".")[-1].lower()
if ext in ["h264", "h265"] or ext in self.fmt_ffi: if ext in ["h264", "h265"] or ext in self.fmt_ffi:
seek = [] seek: list[bytes] = []
else: else:
dur = ret[".dur"][1] if ".dur" in ret else 4 dur = ret[".dur"][1] if ".dur" in ret else 4
seek = "{:.0f}".format(dur / 3) seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")]
seek = [b"-ss", seek.encode("utf-8")]
scale = "scale={0}:{1}:force_original_aspect_ratio=" scale = "scale={0}:{1}:force_original_aspect_ratio="
if self.args.th_no_crop: if self.args.th_no_crop:
@@ -362,7 +369,7 @@ class ThumbSrv(object):
else: else:
scale += "increase,crop={0}:{1},setsar=1:1" scale += "increase,crop={0}:{1},setsar=1:1"
scale = scale.format(*list(self.res)).encode("utf-8") bscale = scale.format(*list(self.res)).encode("utf-8")
# fmt: off # fmt: off
cmd = [ cmd = [
b"ffmpeg", b"ffmpeg",
@@ -374,7 +381,7 @@ class ThumbSrv(object):
cmd += [ cmd += [
b"-i", fsenc(abspath), b"-i", fsenc(abspath),
b"-map", b"0:v:0", b"-map", b"0:v:0",
b"-vf", scale, b"-vf", bscale,
b"-frames:v", b"1", b"-frames:v", b"1",
b"-metadata:s:v:0", b"rotate=0", b"-metadata:s:v:0", b"rotate=0",
] ]
@@ -396,14 +403,14 @@ class ThumbSrv(object):
cmd += [fsenc(tpath)] cmd += [fsenc(tpath)]
self._run_ff(cmd) self._run_ff(cmd)
def _run_ff(self, cmd): def _run_ff(self, cmd: list[bytes]) -> None:
# self.log((b" ".join(cmd)).decode("utf-8")) # self.log((b" ".join(cmd)).decode("utf-8"))
ret, sout, serr = runcmd(cmd, timeout=self.args.th_convt) ret, _, serr = runcmd(cmd, timeout=self.args.th_convt)
if not ret: if not ret:
return return
c = "1;30" c: Union[str, int] = "1;30"
m = "FFmpeg failed (probably a corrupt video file):\n" t = "FFmpeg failed (probably a corrupt video file):\n"
if cmd[-1].lower().endswith(b".webp") and ( if cmd[-1].lower().endswith(b".webp") and (
"Error selecting an encoder" in serr "Error selecting an encoder" in serr
or "Automatic encoder selection failed" in serr or "Automatic encoder selection failed" in serr
@@ -411,14 +418,14 @@ class ThumbSrv(object):
or "Please choose an encoder manually" in serr or "Please choose an encoder manually" in serr
): ):
self.args.th_ff_jpg = True self.args.th_ff_jpg = True
m = "FFmpeg failed because it was compiled without libwebp; enabling --th-ff-jpg to force jpeg output:\n" t = "FFmpeg failed because it was compiled without libwebp; enabling --th-ff-jpg to force jpeg output:\n"
c = 1 c = 1
if ( if (
"Requested resampling engine is unavailable" in serr "Requested resampling engine is unavailable" in serr
or "output pad on Parsed_aresample_" in serr or "output pad on Parsed_aresample_" in serr
): ):
m = "FFmpeg failed because it was compiled without libsox; you must set --th-ff-swr to force swr resampling:\n" t = "FFmpeg failed because it was compiled without libsox; you must set --th-ff-swr to force swr resampling:\n"
c = 1 c = 1
lines = serr.strip("\n").split("\n") lines = serr.strip("\n").split("\n")
@@ -429,10 +436,10 @@ class ThumbSrv(object):
if len(txt) > 5000: if len(txt) > 5000:
txt = txt[:2500] + "...\nff: [...]\nff: ..." + txt[-2500:] txt = txt[:2500] + "...\nff: [...]\nff: ..." + txt[-2500:]
self.log(m + txt, c=c) self.log(t + txt, c=c)
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1])) raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
def conv_spec(self, abspath, tpath): def conv_spec(self, abspath: str, tpath: str) -> None:
ret, _ = ffprobe(abspath) ret, _ = ffprobe(abspath)
if "ac" not in ret: if "ac" not in ret:
raise Exception("not audio") raise Exception("not audio")
@@ -474,7 +481,7 @@ class ThumbSrv(object):
cmd += [fsenc(tpath)] cmd += [fsenc(tpath)]
self._run_ff(cmd) self._run_ff(cmd)
def conv_opus(self, abspath, tpath): def conv_opus(self, abspath: str, tpath: str) -> None:
if self.args.no_acode: if self.args.no_acode:
raise Exception("disabled in server config") raise Exception("disabled in server config")
@@ -522,7 +529,7 @@ class ThumbSrv(object):
# fmt: on # fmt: on
self._run_ff(cmd) self._run_ff(cmd)
def poke(self, tdir): def poke(self, tdir: str) -> None:
if not self.poke_cd.poke(tdir): if not self.poke_cd.poke(tdir):
return return
@@ -534,7 +541,7 @@ class ThumbSrv(object):
except: except:
pass pass
def cleaner(self): def cleaner(self) -> None:
interval = self.args.th_clean interval = self.args.th_clean
while True: while True:
time.sleep(interval) time.sleep(interval)
@@ -549,26 +556,27 @@ class ThumbSrv(object):
self.log("\033[Jcln ok; rm {} dirs".format(ndirs)) self.log("\033[Jcln ok; rm {} dirs".format(ndirs))
def clean(self, histpath): def clean(self, histpath: str) -> int:
ret = 0 ret = 0
for cat in ["th", "ac"]: for cat in ["th", "ac"]:
ret += self._clean(histpath, cat, None) top = os.path.join(histpath, cat)
if not bos.path.isdir(top):
continue
ret += self._clean(cat, top)
return ret return ret
def _clean(self, histpath, cat, thumbpath): def _clean(self, cat: str, thumbpath: str) -> int:
if not thumbpath:
thumbpath = os.path.join(histpath, cat)
# self.log("cln {}".format(thumbpath)) # self.log("cln {}".format(thumbpath))
exts = ["jpg", "webp"] if cat == "th" else ["opus", "caf"] exts = ["jpg", "webp"] if cat == "th" else ["opus", "caf"]
maxage = getattr(self.args, cat + "_maxage") maxage = getattr(self.args, cat + "_maxage")
now = time.time() now = time.time()
prev_b64 = None prev_b64 = None
prev_fp = None prev_fp = ""
try: try:
ents = statdir(self.log, not self.args.no_scandir, False, thumbpath) t1 = statdir(self.log_func, not self.args.no_scandir, False, thumbpath)
ents = sorted(list(ents)) ents = sorted(list(t1))
except: except:
return 0 return 0
@@ -583,7 +591,7 @@ class ThumbSrv(object):
if age > maxage: if age > maxage:
with self.mutex: with self.mutex:
safe = True safe = True
for k in self.busy.keys(): for k in self.busy:
if k.lower().replace("\\", "/").startswith(cmp): if k.lower().replace("\\", "/").startswith(cmp):
safe = False safe = False
break break
@@ -593,7 +601,7 @@ class ThumbSrv(object):
self.log("rm -rf [{}]".format(fp)) self.log("rm -rf [{}]".format(fp))
shutil.rmtree(fp, ignore_errors=True) shutil.rmtree(fp, ignore_errors=True)
else: else:
self._clean(histpath, cat, fp) ndirs += self._clean(cat, fp)
continue continue

View File

@@ -1,34 +1,37 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import re
import os
import time
import calendar import calendar
import os
import re
import threading import threading
import time
from operator import itemgetter from operator import itemgetter
from .__init__ import ANYWIN, unicode from .__init__ import ANYWIN, TYPE_CHECKING, unicode
from .util import absreal, s3dec, Pebkac, min_ex, gen_filekey, quotep
from .bos import bos from .bos import bos
from .up2k import up2k_wark_from_hashlist from .up2k import up2k_wark_from_hashlist
from .util import HAVE_SQLITE3, Pebkac, absreal, gen_filekey, min_ex, quotep, s3dec
if HAVE_SQLITE3:
try:
HAVE_SQLITE3 = True
import sqlite3 import sqlite3
except:
HAVE_SQLITE3 = False
try: try:
from pathlib import Path from pathlib import Path
except: except:
pass pass
try:
from typing import Any, Optional, Union
except:
pass
if TYPE_CHECKING:
from .httpconn import HttpConn
class U2idx(object): class U2idx(object):
def __init__(self, conn): def __init__(self, conn: "HttpConn") -> None:
self.log_func = conn.log_func self.log_func = conn.log_func
self.asrv = conn.asrv self.asrv = conn.asrv
self.args = conn.args self.args = conn.args
@@ -38,17 +41,21 @@ class U2idx(object):
self.log("your python does not have sqlite3; searching will be disabled") self.log("your python does not have sqlite3; searching will be disabled")
return return
self.cur = {} self.active_id = ""
self.mem_cur = sqlite3.connect(":memory:") self.active_cur: Optional["sqlite3.Cursor"] = None
self.cur: dict[str, "sqlite3.Cursor"] = {}
self.mem_cur = sqlite3.connect(":memory:").cursor()
self.mem_cur.execute(r"create table a (b text)") self.mem_cur.execute(r"create table a (b text)")
self.p_end = None self.p_end = 0.0
self.p_dur = 0 self.p_dur = 0.0
def log(self, msg, c=0): def log(self, msg: str, c: Union[int, str] = 0) -> None:
self.log_func("u2idx", msg, c) self.log_func("u2idx", msg, c)
def fsearch(self, vols, body): def fsearch(
self, vols: list[tuple[str, str, dict[str, Any]]], body: dict[str, Any]
) -> list[dict[str, Any]]:
"""search by up2k hashlist""" """search by up2k hashlist"""
if not HAVE_SQLITE3: if not HAVE_SQLITE3:
return [] return []
@@ -58,14 +65,14 @@ class U2idx(object):
wark = up2k_wark_from_hashlist(self.args.salt, fsize, fhash) wark = up2k_wark_from_hashlist(self.args.salt, fsize, fhash)
uq = "substr(w,1,16) = ? and w = ?" uq = "substr(w,1,16) = ? and w = ?"
uv = [wark[:16], wark] uv: list[Union[str, int]] = [wark[:16], wark]
try: try:
return self.run_query(vols, uq, uv, True, False, 99999)[0] return self.run_query(vols, uq, uv, True, False, 99999)[0]
except: except:
raise Pebkac(500, min_ex()) raise Pebkac(500, min_ex())
def get_cur(self, ptop): def get_cur(self, ptop: str) -> Optional["sqlite3.Cursor"]:
if not HAVE_SQLITE3: if not HAVE_SQLITE3:
return None return None
@@ -101,13 +108,16 @@ class U2idx(object):
self.cur[ptop] = cur self.cur[ptop] = cur
return cur return cur
def search(self, vols, uq, lim): def search(
self, vols: list[tuple[str, str, dict[str, Any]]], uq: str, lim: int
) -> tuple[list[dict[str, Any]], list[str]]:
"""search by query params""" """search by query params"""
if not HAVE_SQLITE3: if not HAVE_SQLITE3:
return [] return [], []
q = "" q = ""
va = [] v: Union[str, int] = ""
va: list[Union[str, int]] = []
have_up = False # query has up.* operands have_up = False # query has up.* operands
have_mt = False have_mt = False
is_key = True is_key = True
@@ -200,7 +210,7 @@ class U2idx(object):
"%Y", "%Y",
]: ]:
try: try:
v = calendar.timegm(time.strptime(v, fmt)) v = calendar.timegm(time.strptime(str(v), fmt))
break break
except: except:
pass pass
@@ -228,11 +238,12 @@ class U2idx(object):
# lowercase tag searches # lowercase tag searches
m = ptn_lc.search(q) m = ptn_lc.search(q)
if not m or not ptn_lcv.search(unicode(v)): zs = unicode(v)
if not m or not ptn_lcv.search(zs):
continue continue
va.pop() va.pop()
va.append(v.lower()) va.append(zs.lower())
q = q[: m.start()] q = q[: m.start()]
field, oper = m.groups() field, oper = m.groups()
@@ -246,8 +257,16 @@ class U2idx(object):
except Exception as ex: except Exception as ex:
raise Pebkac(500, repr(ex)) raise Pebkac(500, repr(ex))
def run_query(self, vols, uq, uv, have_up, have_mt, lim): def run_query(
done_flag = [] self,
vols: list[tuple[str, str, dict[str, Any]]],
uq: str,
uv: list[Union[str, int]],
have_up: bool,
have_mt: bool,
lim: int,
) -> tuple[list[dict[str, Any]], list[str]]:
done_flag: list[bool] = []
self.active_id = "{:.6f}_{}".format( self.active_id = "{:.6f}_{}".format(
time.time(), threading.current_thread().ident time.time(), threading.current_thread().ident
) )
@@ -264,13 +283,11 @@ class U2idx(object):
if not uq or not uv: if not uq or not uv:
uq = "select * from up" uq = "select * from up"
uv = () uv = []
elif have_mt: elif have_mt:
uq = "select up.*, substr(up.w,1,16) mtw from up where " + uq uq = "select up.*, substr(up.w,1,16) mtw from up where " + uq
uv = tuple(uv)
else: else:
uq = "select up.* from up where " + uq uq = "select up.* from up where " + uq
uv = tuple(uv)
self.log("qs: {!r} {!r}".format(uq, uv)) self.log("qs: {!r} {!r}".format(uq, uv))
@@ -290,11 +307,10 @@ class U2idx(object):
v = vtop + "/" v = vtop + "/"
vuv.append(v) vuv.append(v)
vuv = tuple(vuv)
sret = [] sret = []
fk = flags.get("fk") fk = flags.get("fk")
c = cur.execute(uq, vuv) c = cur.execute(uq, tuple(vuv))
for hit in c: for hit in c:
w, ts, sz, rd, fn, ip, at = hit[:7] w, ts, sz, rd, fn, ip, at = hit[:7]
lim -= 1 lim -= 1
@@ -338,7 +354,7 @@ class U2idx(object):
# print("[{}] {}".format(ptop, sret)) # print("[{}] {}".format(ptop, sret))
done_flag.append(True) done_flag.append(True)
self.active_id = None self.active_id = ""
# undupe hits from multiple metadata keys # undupe hits from multiple metadata keys
if len(ret) > 1: if len(ret) > 1:
@@ -352,11 +368,12 @@ class U2idx(object):
return ret, list(taglist.keys()) return ret, list(taglist.keys())
def terminator(self, identifier, done_flag): def terminator(self, identifier: str, done_flag: list[bool]) -> None:
for _ in range(self.timeout): for _ in range(self.timeout):
time.sleep(1) time.sleep(1)
if done_flag: if done_flag:
return return
if identifier == self.active_id: if identifier == self.active_id:
assert self.active_cur
self.active_cur.connection.interrupt() self.active_cur.connection.interrupt()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@ window.baguetteBox = (function () {
afterHide: null, afterHide: null,
onChange: null, onChange: null,
}, },
overlay, slider, btnPrev, btnNext, btnHelp, btnAnim, btnRotL, btnRotR, btnSel, btnVmode, btnClose, overlay, slider, btnPrev, btnNext, btnHelp, btnAnim, btnRotL, btnRotR, btnSel, btnFull, btnVmode, btnClose,
currentGallery = [], currentGallery = [],
currentIndex = 0, currentIndex = 0,
isOverlayVisible = false, isOverlayVisible = false,
@@ -37,6 +37,9 @@ window.baguetteBox = (function () {
vmute = false, vmute = false,
vloop = sread('vmode') == 'L', vloop = sread('vmode') == 'L',
vnext = sread('vmode') == 'C', vnext = sread('vmode') == 'C',
loopA = null,
loopB = null,
url_ts = null,
resume_mp = false; resume_mp = false;
var onFSC = function (e) { var onFSC = function (e) {
@@ -182,6 +185,7 @@ window.baguetteBox = (function () {
'<button id="bbox-rotl" type="button">↶</button>' + '<button id="bbox-rotl" type="button">↶</button>' +
'<button id="bbox-rotr" type="button">↷</button>' + '<button id="bbox-rotr" type="button">↷</button>' +
'<button id="bbox-tsel" type="button">sel</button>' + '<button id="bbox-tsel" type="button">sel</button>' +
'<button id="bbox-full" type="button">⛶</button>' +
'<button id="bbox-vmode" type="button" tt="a"></button>' + '<button id="bbox-vmode" type="button" tt="a"></button>' +
'<button id="bbox-close" type="button" aria-label="Close">X</button>' + '<button id="bbox-close" type="button" aria-label="Close">X</button>' +
'</div></div>' '</div></div>'
@@ -198,9 +202,9 @@ window.baguetteBox = (function () {
btnRotL = ebi('bbox-rotl'); btnRotL = ebi('bbox-rotl');
btnRotR = ebi('bbox-rotr'); btnRotR = ebi('bbox-rotr');
btnSel = ebi('bbox-tsel'); btnSel = ebi('bbox-tsel');
btnFull = ebi('bbox-full');
btnVmode = ebi('bbox-vmode'); btnVmode = ebi('bbox-vmode');
btnClose = ebi('bbox-close'); btnClose = ebi('bbox-close');
bindEvents();
} }
function halp() { function halp() {
@@ -215,23 +219,26 @@ window.baguetteBox = (function () {
['home', 'first file'], ['home', 'first file'],
['end', 'last file'], ['end', 'last file'],
['R', 'rotate (shift=ccw)'], ['R', 'rotate (shift=ccw)'],
['F', 'toggle fullscreen'],
['S', 'toggle file selection'], ['S', 'toggle file selection'],
['space, P, K', 'video: play / pause'], ['space, P, K', 'video: play / pause'],
['U', 'video: seek 10sec back'], ['U', 'video: seek 10sec back'],
['P', 'video: seek 10sec ahead'], ['P', 'video: seek 10sec ahead'],
['0..9', 'video: seek 0%..90%'],
['M', 'video: toggle mute'], ['M', 'video: toggle mute'],
['V', 'video: toggle loop'], ['V', 'video: toggle loop'],
['C', 'video: toggle auto-next'], ['C', 'video: toggle auto-next'],
['F', 'video: toggle fullscreen'], ['<code>[</code>, <code>]</code>', 'video: loop start / end'],
], ],
d = mknod('table'), d = mknod('table', 'bbox-halp'),
html = ['<tbody>']; html = ['<tbody>'];
for (var a = 0; a < list.length; a++) for (var a = 0; a < list.length; a++)
html.push('<tr><td>' + list[a][0] + '</td><td>' + list[a][1] + '</td></tr>'); html.push('<tr><td>' + list[a][0] + '</td><td>' + list[a][1] + '</td></tr>');
html.push('<tr><td colspan="2">tap middle of img to hide btns</td></tr>');
html.push('<tr><td colspan="2">tap left/right sides for prev/next</td></tr>');
d.innerHTML = html.join('\n') + '</tbody>'; d.innerHTML = html.join('\n') + '</tbody>';
d.setAttribute('id', 'bbox-halp');
d.onclick = function () { d.onclick = function () {
overlay.removeChild(d); overlay.removeChild(d);
}; };
@@ -242,7 +249,7 @@ window.baguetteBox = (function () {
if (e.ctrlKey || e.altKey || e.metaKey || e.isComposing || modal.busy) if (e.ctrlKey || e.altKey || e.metaKey || e.isComposing || modal.busy)
return; return;
var k = e.code + '', v = vid(); var k = e.code + '', v = vid(), pos = -1;
if (k == "ArrowLeft" || k == "KeyJ") if (k == "ArrowLeft" || k == "KeyJ")
showPreviousImage(); showPreviousImage();
@@ -258,6 +265,8 @@ window.baguetteBox = (function () {
playpause(); playpause();
else if (k == "KeyU" || k == "KeyO") else if (k == "KeyU" || k == "KeyO")
relseek(k == "KeyU" ? -10 : 10); relseek(k == "KeyU" ? -10 : 10);
else if (k.indexOf('Digit') === 0)
vid().currentTime = vid().duration * parseInt(k.slice(-1)) * 0.1;
else if (k == "KeyM" && v) { else if (k == "KeyM" && v) {
v.muted = vmute = !vmute; v.muted = vmute = !vmute;
mp_ctl(); mp_ctl();
@@ -273,17 +282,17 @@ window.baguetteBox = (function () {
setVmode(); setVmode();
} }
else if (k == "KeyF") else if (k == "KeyF")
try { tglfull();
if (isFullscreen)
document.exitFullscreen();
else
v.requestFullscreen();
}
catch (ex) { }
else if (k == "KeyS") else if (k == "KeyS")
tglsel(); tglsel();
else if (k == "KeyR") else if (k == "KeyR")
rotn(e.shiftKey ? -1 : 1); rotn(e.shiftKey ? -1 : 1);
else if (k == "KeyY")
dlpic();
else if (k == "BracketLeft")
setloop(1);
else if (k == "BracketRight")
setloop(2);
} }
function anim() { function anim() {
@@ -342,19 +351,39 @@ window.baguetteBox = (function () {
tt.show.bind(this)(); tt.show.bind(this)();
} }
function tglsel() { function findfile() {
var thumb = currentGallery[currentIndex].imageElement, var thumb = currentGallery[currentIndex].imageElement,
name = vsplit(thumb.href)[1].split('?')[0], name = vsplit(thumb.href)[1].split('?')[0],
files = msel.getall(); files = msel.getall();
for (var a = 0; a < files.length; a++) for (var a = 0; a < files.length; a++)
if (vsplit(files[a].vp)[1] == name) if (vsplit(files[a].vp)[1] == name)
clmod(ebi(files[a].id).closest('tr'), 'sel', 't'); return [name, a, files, ebi(files[a].id)];
}
function tglfull() {
try {
if (isFullscreen)
document.exitFullscreen();
else
(vid() || ebi('bbox-overlay')).requestFullscreen();
}
catch (ex) { alert(ex); }
}
function tglsel() {
var o = findfile()[3];
clmod(o.closest('tr'), 'sel', 't');
msel.selui(); msel.selui();
selbg(); selbg();
} }
function dlpic() {
var url = findfile()[3].href;
url += (url.indexOf('?') < 0 ? '?' : '&') + 'cache';
dl_file(url);
}
function selbg() { function selbg() {
var img = vidimg(), var img = vidimg(),
thumb = currentGallery[currentIndex].imageElement, thumb = currentGallery[currentIndex].imageElement,
@@ -404,6 +433,9 @@ window.baguetteBox = (function () {
var nonPassiveEvent = passiveSupp ? { passive: true } : null; var nonPassiveEvent = passiveSupp ? { passive: true } : null;
function bindEvents() { function bindEvents() {
bind(document, 'keydown', keyDownHandler);
bind(document, 'keyup', keyUpHandler);
bind(document, 'fullscreenchange', onFSC);
bind(overlay, 'click', overlayClickHandler); bind(overlay, 'click', overlayClickHandler);
bind(btnPrev, 'click', showPreviousImage); bind(btnPrev, 'click', showPreviousImage);
bind(btnNext, 'click', showNextImage); bind(btnNext, 'click', showNextImage);
@@ -414,6 +446,7 @@ window.baguetteBox = (function () {
bind(btnRotL, 'click', rotl); bind(btnRotL, 'click', rotl);
bind(btnRotR, 'click', rotr); bind(btnRotR, 'click', rotr);
bind(btnSel, 'click', tglsel); bind(btnSel, 'click', tglsel);
bind(btnFull, 'click', tglfull);
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);
@@ -422,6 +455,9 @@ window.baguetteBox = (function () {
} }
function unbindEvents() { function unbindEvents() {
unbind(document, 'keydown', keyDownHandler);
unbind(document, 'keyup', keyUpHandler);
unbind(document, 'fullscreenchange', onFSC);
unbind(overlay, 'click', overlayClickHandler); unbind(overlay, 'click', overlayClickHandler);
unbind(btnPrev, 'click', showPreviousImage); unbind(btnPrev, 'click', showPreviousImage);
unbind(btnNext, 'click', showNextImage); unbind(btnNext, 'click', showNextImage);
@@ -432,6 +468,7 @@ window.baguetteBox = (function () {
unbind(btnRotL, 'click', rotl); unbind(btnRotL, 'click', rotl);
unbind(btnRotR, 'click', rotr); unbind(btnRotR, 'click', rotr);
unbind(btnSel, 'click', tglsel); unbind(btnSel, 'click', tglsel);
unbind(btnFull, 'click', tglfull);
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);
@@ -452,9 +489,8 @@ window.baguetteBox = (function () {
var imagesFiguresIds = []; var imagesFiguresIds = [];
var imagesCaptionsIds = []; var imagesCaptionsIds = [];
for (var i = 0, fullImage; i < gallery.length; i++) { for (var i = 0, fullImage; i < gallery.length; i++) {
fullImage = mknod('div'); fullImage = mknod('div', 'baguette-img-' + i);
fullImage.className = 'full-image'; fullImage.className = 'full-image';
fullImage.id = 'baguette-img-' + i;
imagesElements.push(fullImage); imagesElements.push(fullImage);
imagesFiguresIds.push('bbox-figure-' + i); imagesFiguresIds.push('bbox-figure-' + i);
@@ -496,9 +532,7 @@ window.baguetteBox = (function () {
if (overlay.style.display === 'block') if (overlay.style.display === 'block')
return; return;
bind(document, 'keydown', keyDownHandler); bindEvents();
bind(document, 'keyup', keyUpHandler);
bind(document, 'fullscreenchange', onFSC);
currentIndex = chosenImageIndex; currentIndex = chosenImageIndex;
touch = { touch = {
count: 0, count: 0,
@@ -510,6 +544,10 @@ window.baguetteBox = (function () {
preloadPrev(currentIndex); preloadPrev(currentIndex);
}); });
clmod(ebi('bbox-btns'), 'off');
clmod(btnPrev, 'off');
clmod(btnNext, 'off');
updateOffset(); updateOffset();
overlay.style.display = 'block'; overlay.style.display = 'block';
// Fade in overlay // Fade in overlay
@@ -522,9 +560,10 @@ window.baguetteBox = (function () {
options.afterShow(); options.afterShow();
}, 50); }, 50);
if (options.onChange) if (options.onChange && !url_ts)
options.onChange(currentIndex, imagesElements.length); options.onChange(currentIndex, imagesElements.length);
url_ts = null;
documentLastFocus = document.activeElement; documentLastFocus = document.activeElement;
btnClose.focus(); btnClose.focus();
isOverlayVisible = true; isOverlayVisible = true;
@@ -541,9 +580,13 @@ window.baguetteBox = (function () {
return; return;
sethash(''); sethash('');
unbind(document, 'keydown', keyDownHandler); unbindEvents();
unbind(document, 'keyup', keyUpHandler); try {
unbind(document, 'fullscreenchange', onFSC); document.exitFullscreen();
isFullscreen = false;
}
catch (ex) { }
// Fade out and hide the overlay // Fade out and hide the overlay
overlay.className = ''; overlay.className = '';
setTimeout(function () { setTimeout(function () {
@@ -589,16 +632,14 @@ window.baguetteBox = (function () {
if (is_vid && index != currentIndex) if (is_vid && index != currentIndex)
return; // no preload return; // no preload
var figure = mknod('figure'); var figure = mknod('figure', 'bbox-figure-' + index);
figure.id = 'bbox-figure-' + index;
figure.innerHTML = '<div class="bbox-spinner">' + figure.innerHTML = '<div class="bbox-spinner">' +
'<div class="bbox-double-bounce1"></div>' + '<div class="bbox-double-bounce1"></div>' +
'<div class="bbox-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', 'bbox-figcaption-' + index);
figcaption.id = 'bbox-figcaption-' + index;
figcaption.innerHTML = imageCaption; figcaption.innerHTML = imageCaption;
figure.appendChild(figcaption); figure.appendChild(figcaption);
} }
@@ -658,18 +699,12 @@ window.baguetteBox = (function () {
showOverlay(index); showOverlay(index);
return true; return true;
} }
if (index < 0) {
if (options.animation)
bounceAnimation('left');
return false; if (index < 0)
} return bounceAnimation('left');
if (index >= imagesElements.length) {
if (options.animation)
bounceAnimation('right');
return false; if (index >= imagesElements.length)
} return bounceAnimation('right');
var v = vid(); var v = vid();
if (v) { if (v) {
@@ -777,8 +812,18 @@ window.baguetteBox = (function () {
} }
function playvid(play) { function playvid(play) {
if (vid()) if (!play) {
vid()[play ? 'play' : 'pause'](); timer.rm(loopchk);
loopA = loopB = null;
}
var v = vid();
if (!v)
return;
v[play ? 'play' : 'pause']();
if (play && loopA !== null && v.currentTime < loopA)
v.currentTime = loopA;
} }
function playpause() { function playpause() {
@@ -797,6 +842,38 @@ window.baguetteBox = (function () {
showNextImage(); showNextImage();
} }
function setloop(side) {
var v = vid();
if (!v)
return;
var t = v.currentTime;
if (side == 1) loopA = t;
if (side == 2) loopB = t;
if (side)
toast.inf(5, 'Loop' + (side == 1 ? 'A' : 'B') + ': ' + f2f(t, 2));
if (loopB !== null) {
timer.add(loopchk);
sethash(window.location.hash.slice(1).split('&')[0] + '&t=' + (loopA || 0) + '-' + loopB);
}
}
function loopchk() {
if (loopB === null)
return;
var v = vid();
if (!v || v.paused || v.currentTime < loopB)
return;
v.currentTime = loopA || 0;
}
function urltime(txt) {
url_ts = txt;
}
function mp_ctl() { function mp_ctl() {
var v = vid(); var v = vid();
if (!vmute && v && mp.au && !mp.au.paused) { if (!vmute && v && mp.au && !mp.au.paused) {
@@ -810,10 +887,11 @@ window.baguetteBox = (function () {
} }
function bounceAnimation(direction) { function bounceAnimation(direction) {
slider.className = 'bounce-from-' + direction; slider.className = options.animation == 'slideIn' ? 'bounce-from-' + direction : 'eog';
setTimeout(function () { setTimeout(function () {
slider.className = ''; slider.className = '';
}, 400); }, 300);
return false;
} }
function updateOffset() { function updateOffset() {
@@ -839,6 +917,15 @@ window.baguetteBox = (function () {
playvid(true); playvid(true);
v.muted = vmute; v.muted = vmute;
v.loop = vloop; v.loop = vloop;
if (url_ts) {
var seek = ('' + url_ts).split('-');
v.currentTime = seek[0];
if (seek.length > 1) {
loopA = parseFloat(seek[0]);
loopB = parseFloat(seek[1]);
setloop();
}
}
} }
selbg(); selbg();
mp_ctl(); mp_ctl();
@@ -850,6 +937,28 @@ window.baguetteBox = (function () {
else else
timer.rm(rotn); timer.rm(rotn);
var ctime = 0;
el.onclick = v ? null : function (e) {
var rc = e.target.getBoundingClientRect(),
x = e.clientX - rc.left,
fx = x / (rc.right - rc.left);
if (fx < 0.3)
return showPreviousImage();
if (fx > 0.7)
return showNextImage();
clmod(ebi('bbox-btns'), 'off', 't');
clmod(btnPrev, 'off', 't');
clmod(btnNext, 'off', 't');
if (Date.now() - ctime <= 500)
tglfull();
ctime = Date.now();
};
var prev = QS('.full-image.vis'); var prev = QS('.full-image.vis');
if (prev) if (prev)
clmod(prev, 'vis'); clmod(prev, 'vis');
@@ -886,8 +995,6 @@ window.baguetteBox = (function () {
function destroyPlugin() { function destroyPlugin() {
unbindEvents(); unbindEvents();
clearCachedData(); clearCachedData();
unbind(document, 'keydown', keyDownHandler);
unbind(document, 'keyup', keyUpHandler);
document.getElementsByTagName('body')[0].removeChild(ebi('bbox-overlay')); document.getElementsByTagName('body')[0].removeChild(ebi('bbox-overlay'));
data = {}; data = {};
currentGallery = []; currentGallery = [];
@@ -900,6 +1007,7 @@ window.baguetteBox = (function () {
showNext: showNextImage, showNext: showNextImage,
showPrevious: showPreviousImage, showPrevious: showPreviousImage,
relseek: relseek, relseek: relseek,
urltime: urltime,
playpause: playpause, playpause: playpause,
hide: hideOverlay, hide: hideOverlay,
destroy: destroyPlugin destroy: destroyPlugin

View File

@@ -238,6 +238,7 @@ html.b {
--u2-txt-bg: transparent; --u2-txt-bg: transparent;
--u2-tab-1-sh: var(--bg); --u2-tab-1-sh: var(--bg);
--u2-b1-bg: rgba(128,128,128,0.15); --u2-b1-bg: rgba(128,128,128,0.15);
--u2-b2-bg: var(--u2-b1-bg);
--u2-o-bg: var(--btn-bg); --u2-o-bg: var(--btn-bg);
--u2-o-h-bg: var(--btn-h-bg); --u2-o-h-bg: var(--btn-h-bg);
@@ -258,7 +259,7 @@ html.bz {
--bg-d2: #34384e; --bg-d2: #34384e;
--bg-d3: #34384e; --bg-d3: #34384e;
--row-alt: rgba(139, 150, 205, 0.06); --row-alt: #181a27;
--btn-bg: #202231; --btn-bg: #202231;
--btn-h-bg: #2d2f45; --btn-h-bg: #2d2f45;
@@ -308,7 +309,7 @@ html.c {
--a-gray: #0ae; --a-gray: #0ae;
--tab-alt: #6ef; --tab-alt: #6ef;
--row-alt: rgba(180,0,255,0.3); --row-alt: #47237d;
--scroll: #ff0; --scroll: #ff0;
--btn-fg: #fff; --btn-fg: #fff;
@@ -352,9 +353,220 @@ html.cy {
--srv-1: #f00; --srv-1: #f00;
--op-aa-bg: #fff; --op-aa-bg: #fff;
--u2-b1-bg: #f00;
--u2-b2-bg: #f00;
--u2-o-bg: #ff0; --u2-o-bg: #ff0;
--u2-o-1-bg: #f00; --u2-o-1-bg: #f00;
} }
html.dz {
--fg: #4d4;
--fg-max: #fff;
--fg2-max: #fff;
--fg-weak: #2a2;
--bg-u7: #020;
--bg-u6: #020;
--bg-u5: #050;
--bg-u4: #020;
--bg-u3: #020;
--bg-u2: #020;
--bg-u1: #020;
--bg: #010;
--bgg: var(--bg);
--bg-d1: #000;
--bg-d2: #020;
--bg-d3: #000;
--bg-max: #000;
--tab-alt: #6f6;
--row-alt: #030;
--scroll: #0f0;
--a: #9f9;
--a-b: #cfc;
--a-hil: #cfc;
--a-dark: #afa;
--a-gray: #2a2;
--btn-fg: var(--a);
--btn-bg: rgba(64,128,64,0.15);
--btn-h-fg: var(--a-hil);
--btn-h-bg: #050;
--btn-1-fg: #000;
--btn-1-bg: #4f4;
--btn-1h-fg: var(--btn-1-fg);
--btn-1h-bg: #3f3;
--chk-fg: var(--tab-alt);
--txt-sh: var(--bg-d2);
--txt-bg: var(--btn-bg);
--op-aa-fg: var(--a);
--op-aa-bg: var(--bg-d2);
--op-a-sh: rgba(0,0,0,0.5);
--u2-btn-b1: #999;
--u2-sbtn-b1: #999;
--u2-txt-bg: var(--bg-u5);
--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));
--u2-tab-1-fg: #fff;
--u2-tab-1-bg: linear-gradient(to bottom, var(#353), var(--bg) 80%);
--u2-tab-1-b1: #7c5;
--u2-tab-1-b2: #583;
--u2-tab-1-sh: #280;
--u2-b-fg: #fff;
--u2-b1-bg: #3a3;
--u2-b2-bg: #3a3;
--u2-inf-bg: #07a;
--u2-inf-b1: #0be;
--u2-ok-bg: #380;
--u2-ok-b1: #8e4;
--u2-err-bg: #900;
--u2-err-b1: #d06;
--ud-b1: #888;
--sort-1: #fff;
--sort-2: #3f3;
--srv-1: #3e3;
--srv-2: #1a1;
--srv-3: #0f0;
--srv-3b: #070;
--tree-bg: #010;
--g-play-bg: #750;
--g-play-b1: #c90;
--g-play-b2: #da4;
--g-play-sh: #b83;
--g-sel-fg: #fff;
--g-sel-bg: #925;
--g-sel-b1: #c37;
--g-sel-sh: #b36;
--g-fsel-bg: #d39;
--g-fsel-b1: #d48;
--g-fsel-ts: #804;
--g-fg: var(--a-hil);
--g-bg: var(--bg-u2);
--g-b1: var(--bg-u4);
--g-b2: var(--bg-u5);
--g-g1: var(--bg-u2);
--g-g2: var(--bg-u5);
--g-f-bg: var(--bg-u4);
--g-f-b1: var(--bg-u5);
--g-f-fg: var(--a-hil);
--g-sh: rgba(0,0,0,0.3);
--f-sh1: 0.33;
--f-sh2: 0.02;
--f-sh3: 0.2;
--f-h-b1: #3b3;
--f-play-bg: #fc5;
--f-play-fg: #000;
--f-sel-sh: #fc0;
--f-gray: #999;
--fm-off: #f6c;
--mp-sh: var(--bg-d3);
--mp-b-bg: rgba(0,0,0,0.2);
--err-fg: #fff;
--err-bg: #a20;
--err-b1: #f00;
--err-ts: #500;
text-shadow: none;
}
html.dy {
--fg: #000;
--fg-max: #000;
--fg-weak: #000;
--bg-d3: #fff;
--bg-d2: #fff;
--bg-d1: #fff;
--bg: #fff;
--bg-u1: #fff;
--bg-u2: #fff;
--bg-u3: #fff;
--bg-u4: #fff;
--bg-u5: #fff;
--bg-u6: #fff;
--bg-max: #fff;
--tab-alt: #000;
--row-alt: #eee;
--scroll: #000;
--a: #000;
--a-b: #000;
--a-hil: #000;
--a-gray: #bbb;
--a-dark: #000;
--btn-fg: #000;
--btn-h-fg: #000;
--btn-h-bg: #fff;
--btn-1-fg: #fff;
--btn-1-bg: #000;
--btn-1h-bg: #555;
--chk-fg: a;
--txt-sh: a;
--txt-bg: a;
--op-a-sh: a;
--u2-txt-bg: a;
--u2-tab-1-sh: a;
--u2-tab-1-b1: a;
--u2-tab-1-b2: a;
--u2-tab-1-fg: a;
--u2-tab-1-bg: a;
--u2-b1-bg: #000;
--u2-b2-bg: #000;
--u2-o-h-bg: #999;
--u2-o-1h-bg: #999;
--u2-o-bg: #eee;
--u2-o-1-bg: #000;
--ud-b1: a;
--sort-1: a;
--sort-2: a;
--srv-1: a;
--srv-2: a;
--srv-3: a;
--srv-3b: a;
--tree-bg: #fff;
--g-sel-bg: #000;
--g-fsel-bg: #444;
--g-fsel-ts: #000;
--g-fg: a;
--g-bg: a;
--g-b1: a;
--g-b2: a;
--g-g1: a;
--g-g2: a;
--g-f-bg: a;
--g-f-b1: a;
--g-sh: a;
--f-sh1: a;
--f-sh2: a;
--f-sh3: a;
--f-sel-sh: #000;
--fm-off: a;
--mp-sh: a;
--mp-b-bg: #fff;
}
* { * {
line-height: 1.2em; line-height: 1.2em;
} }
@@ -379,6 +591,27 @@ html, body {
pre, code, tt, #doc, #doc>code { pre, code, tt, #doc, #doc>code {
font-family: 'scp', monospace, monospace; font-family: 'scp', monospace, monospace;
} }
.ayjump {
position: fixed;
overflow: hidden;
width: 0;
height: 0;
}
html .ayjump:focus {
z-index: 80386;
color: #fff;
color: var(--a-hil);
background: #069;
background: var(--bg-u2);
border: .2em solid var(--a);
box-shadow: none;
outline: none;
width: auto;
height: auto;
top: .5em;
left: .5em;
padding: .5em .7em;
}
#path, #path,
#path * { #path * {
font-size: 1em; font-size: 1em;
@@ -477,6 +710,7 @@ html.y #files thead th {
#files td { #files td {
margin: 0; margin: 0;
padding: .3em .5em; padding: .3em .5em;
background: var(--bg);
} }
#files tr:nth-child(2n) td { #files tr:nth-child(2n) td {
background: var(--row-alt); background: var(--row-alt);
@@ -493,6 +727,7 @@ html.y #files thead th {
} }
#files td:first-child { #files td:first-child {
border-radius: .25em 0 0 .25em; border-radius: .25em 0 0 .25em;
white-space: nowrap;
} }
#files td:last-child { #files td:last-child {
border-radius: 0 .25em .25em 0; border-radius: 0 .25em .25em 0;
@@ -848,6 +1083,13 @@ html.y #widget.open {
@keyframes spin { @keyframes spin {
100% {transform: rotate(360deg)} 100% {transform: rotate(360deg)}
} }
@media (prefers-reduced-motion) {
@keyframes spin { }
}
@keyframes fadein {
0% {opacity: 0}
100% {opacity: 1}
}
#wtoggle { #wtoggle {
position: absolute; position: absolute;
white-space: nowrap; white-space: nowrap;
@@ -989,7 +1231,6 @@ html.y #widget.open {
font-size: 1.5em; font-size: 1.5em;
padding: .25em .4em; padding: .25em .4em;
margin: 0; margin: 0;
outline: none;
} }
#ops a.act { #ops a.act {
color: #fff; color: #fff;
@@ -1176,7 +1417,7 @@ html {
z-index: 1; z-index: 1;
position: fixed; position: fixed;
background: var(--tree-bg); background: var(--tree-bg);
left: -.75em; left: -.98em;
width: calc(var(--nav-sz) - 0.5em); width: calc(var(--nav-sz) - 0.5em);
border-bottom: 1px solid var(--bg-u5); border-bottom: 1px solid var(--bg-u5);
overflow: hidden; overflow: hidden;
@@ -1219,10 +1460,16 @@ html.c .btn,
html.a .btn { html.a .btn {
border-radius: .2em; border-radius: .2em;
} }
html.ca .btn { html.cz .btn {
box-shadow: 0 .1em .6em rgba(255,0,185,0.5); box-shadow: 0 .1em .6em rgba(255,0,185,0.5);
border-bottom: .2em solid #709; border-bottom: .2em solid #709;
} }
html.dz .btn {
box-shadow: 0 0 0 .1em #080 inset;
}
html.dz .tgl.btn.on {
box-shadow: 0 0 0 .1em var(--btn-1-bg) inset;
}
.btn:hover { .btn:hover {
color: var(--btn-h-fg); color: var(--btn-h-fg);
background: var(--btn-h-bg); background: var(--btn-h-bg);
@@ -1234,7 +1481,7 @@ html.ca .btn {
color: var(--btn-1-fg); color: var(--btn-1-fg);
text-shadow: none; text-shadow: none;
} }
html.ca .tgl.btn.on { html.cz .tgl.btn.on {
box-shadow: 0 .1em .8em rgba(255,205,0,0.9); box-shadow: 0 .1em .8em rgba(255,205,0,0.9);
border-bottom: .2em solid #e90; border-bottom: .2em solid #e90;
} }
@@ -1312,7 +1559,8 @@ html.y #tree.nowrap .ntree a+a:hover {
margin: 1em .3em 1em 1em; margin: 1em .3em 1em 1em;
padding: 0 1.2em 0 0; padding: 0 1.2em 0 0;
font-size: 4em; font-size: 4em;
animation: spin 1s linear infinite; opacity: 0;
animation: 1s linear .15s infinite forwards spin, .2s ease .15s 1 forwards fadein;
position: absolute; position: absolute;
z-index: 9; z-index: 9;
} }
@@ -1351,9 +1599,6 @@ html.y #tree.nowrap .ntree a+a:hover {
margin: .7em 0 .7em .5em; margin: .7em 0 .7em .5em;
padding-left: .5em; padding-left: .5em;
} }
.opwide>div.fill {
display: block;
}
.opwide>div>div>a { .opwide>div>div>a {
line-height: 2em; line-height: 2em;
} }
@@ -1608,6 +1853,7 @@ a.btn,
.full-image img, .full-image img,
.full-image video { .full-image video {
display: inline-block; display: inline-block;
outline: none;
width: auto; width: auto;
height: auto; height: auto;
max-width: 100%; max-width: 100%;
@@ -1663,10 +1909,13 @@ html.y #bbox-overlay figcaption a {
transition: left .2s ease, transform .2s ease; transition: left .2s ease, transform .2s ease;
} }
.bounce-from-right { .bounce-from-right {
animation: bounceFromRight .4s ease-out; animation: bounceFromRight .3s ease-out;
} }
.bounce-from-left { .bounce-from-left {
animation: bounceFromLeft .4s ease-out; animation: bounceFromLeft .3s ease-out;
}
.eog {
animation: eog .2s;
} }
@keyframes bounceFromRight { @keyframes bounceFromRight {
0% {margin-left: 0} 0% {margin-left: 0}
@@ -1678,6 +1927,9 @@ html.y #bbox-overlay figcaption a {
50% {margin-left: 30px} 50% {margin-left: 30px}
100% {margin-left: 0} 100% {margin-left: 0}
} }
@keyframes eog {
0% {filter: brightness(1.5)}
}
#bbox-next, #bbox-next,
#bbox-prev { #bbox-prev {
top: 50%; top: 50%;
@@ -1688,6 +1940,15 @@ html.y #bbox-overlay figcaption a {
.bbox-btn { .bbox-btn {
position: fixed; position: fixed;
} }
.bbox-btn,
#bbox-btns {
opacity: 1;
animation: opacity .2s infinite ease-in-out;
}
.bbox-btn.off,
#bbox-btns.off {
opacity: 0;
}
#bbox-overlay button { #bbox-overlay button {
cursor: pointer; cursor: pointer;
outline: none; outline: none;
@@ -1732,7 +1993,7 @@ html.y #bbox-overlay figcaption a {
#bbox-halp td { #bbox-halp td {
padding: .2em .5em; padding: .2em .5em;
} }
#bbox-halp td:first-child { #bbox-halp td:first-child:not([colspan]) {
text-align: right; text-align: right;
} }
.bbox-spinner { .bbox-spinner {
@@ -1912,7 +2173,6 @@ html.y #bbox-overlay figcaption a {
#u2form input { #u2form input {
background: var(--bg-u5); background: var(--bg-u5);
border: 0px solid var(--bg-u5); border: 0px solid var(--bg-u5);
outline: none;
} }
#u2err.err { #u2err.err {
color: var(--a-dark); color: var(--a-dark);
@@ -1968,12 +2228,20 @@ html.y #bbox-overlay figcaption a {
#u2notbtn * { #u2notbtn * {
line-height: 1.3em; line-height: 1.3em;
} }
#u2mu div {
height: 1.2em;
overflow: hidden;
}
#u2tabw { #u2tabw {
min-height: 0; min-height: 0;
transition: min-height .2s; transition: min-height .2s;
margin: 2em 0; margin: 2em 0;
} }
#u2tabw.na>table {
display: none;
}
#u2tab { #u2tab {
table-layout: fixed;
border-collapse: collapse; border-collapse: collapse;
width: calc(100% - 2em); width: calc(100% - 2em);
max-width: 100em; max-width: 100em;
@@ -1983,6 +2251,7 @@ html.y #bbox-overlay figcaption a {
max-width: none; max-width: none;
} }
#u2tab td { #u2tab td {
word-wrap: break-word;
border: 1px solid rgba(128,128,128,0.8); border: 1px solid rgba(128,128,128,0.8);
border-width: 0 0px 1px 0; border-width: 0 0px 1px 0;
padding: .2em .3em; padding: .2em .3em;
@@ -1997,7 +2266,19 @@ html.y #bbox-overlay figcaption a {
#u2tab.up.ok td:nth-child(3), #u2tab.up.ok td:nth-child(3),
#u2tab.up.bz td:nth-child(3), #u2tab.up.bz td:nth-child(3),
#u2tab.up.q td:nth-child(3) { #u2tab.up.q td:nth-child(3) {
width: 19em; width: 18em;
}
@media (max-width: 65em) {
#u2tab {
font-size: .9em;
}
}
@media (max-width: 50em) {
#u2tab.up.ok td:nth-child(3),
#u2tab.up.bz td:nth-child(3),
#u2tab.up.q td:nth-child(3) {
width: 16em;
}
} }
#op_up2k.srch td.prog { #op_up2k.srch td.prog {
font-family: sans-serif; font-family: sans-serif;
@@ -2097,7 +2378,7 @@ html.y #bbox-overlay figcaption a {
width: 48em; width: 48em;
} }
#u2conf.ww { #u2conf.ww {
width: 74em; width: 78em;
} }
#u2conf.ww #u2c3w { #u2conf.ww #u2c3w {
width: 29em; width: 29em;
@@ -2109,7 +2390,6 @@ html.y #bbox-overlay figcaption a {
margin: 0; margin: 0;
padding: 0; padding: 0;
border: none; border: none;
outline: none;
} }
#u2conf .txtbox { #u2conf .txtbox {
width: 3em; width: 3em;
@@ -2138,6 +2418,9 @@ html.y #bbox-overlay figcaption a {
position: relative; position: relative;
bottom: -0.08em; bottom: -0.08em;
} }
#u2conf input+a.b {
background: var(--u2-b2-bg);
}
html.b #u2conf a.b:hover { html.b #u2conf a.b:hover {
background: var(--btn-h-bg); background: var(--btn-h-bg);
} }
@@ -2196,11 +2479,11 @@ html.b #u2conf a.b:hover {
color: var(--fg-max); color: var(--fg-max);
font-style: italic; font-style: italic;
text-align: center; text-align: center;
font-size: .9em; font-size: 1.2em;
margin: 1em 0; margin: .8em 0;
} }
#u2foot .warn { #u2foot .warn {
font-size: 1.3em; font-size: 1.2em;
padding: .5em .8em; padding: .5em .8em;
margin: 1em -.6em; margin: 1em -.6em;
border-width: .1em 0; border-width: .1em 0;
@@ -2214,6 +2497,9 @@ html.b #u2conf a.b:hover {
font-size: .9em; font-size: .9em;
font-weight: normal; font-weight: normal;
} }
#u2foot>*+* {
margin-top: 1.5em;
}
.prog { .prog {
font-family: 'scp', monospace, monospace; font-family: 'scp', monospace, monospace;
} }
@@ -2272,9 +2558,11 @@ html.a #pctl a {
margin-right: .5em; margin-right: .5em;
box-shadow: -.02em -.02em .3em rgba(0,0,0,0.2) inset; box-shadow: -.02em -.02em .3em rgba(0,0,0,0.2) inset;
} }
html.d #pctl,
html.b #pctl { html.b #pctl {
left: .5em; left: .5em;
} }
html.d #ops,
html.c #ops, html.c #ops,
html.a #ops { html.a #ops {
margin: 1.7em 1.5em 0 1.5em; margin: 1.7em 1.5em 0 1.5em;
@@ -2365,9 +2653,6 @@ html.c #u2cards,
html.a #u2cards { html.a #u2cards {
margin: 0 auto -1em auto; margin: 0 auto -1em auto;
} }
html.a #u2conf input+a.b {
background: var(--u2-b2-bg);
}
html.c #u2foot:empty, html.c #u2foot:empty,
html.a #u2foot:empty { html.a #u2foot:empty {
margin-bottom: -1em; margin-bottom: -1em;
@@ -2428,6 +2713,9 @@ html.b #acc_info {
html.b #wtoggle { html.b #wtoggle {
border-radius: .1em 0 0 0; border-radius: .1em 0 0 0;
} }
html.d #barpos,
html.d #barbuf,
html.d #pvol,
html.b #barpos, html.b #barpos,
html.b #barbuf, html.b #barbuf,
html.b #pvol { html.b #pvol {
@@ -2471,10 +2759,14 @@ html.b #treeh,
html.b #tree li { html.b #tree li {
border: none; border: none;
} }
html.b #tree li {
margin-left: .8em;
}
html.b .ntree a { html.b .ntree a {
padding: .6em .2em; padding: .6em .2em;
} }
html.b #treepar { html.b #treepar {
margin-left: .62em;
border-bottom: .2em solid var(--f-h-b1); border-bottom: .2em solid var(--f-h-b1);
} }
html.b #wrap { html.b #wrap {
@@ -2538,6 +2830,17 @@ html.cy #files tbody div a:last-child {
html.dz * {
border-radius: 0 !important;
}
html.d #treepar {
border-bottom: .2em solid var(--f-h-b1);
}
@media (min-width: 70em) { @media (min-width: 70em) {
#barpos, #barpos,
#barbuf { #barbuf {
@@ -2547,7 +2850,9 @@ html.cy #files tbody div a:last-child {
height: 1.6em; height: 1.6em;
bottom: auto; bottom: auto;
} }
html.d #barpos,
html.b #barpos, html.b #barpos,
html.d #barbuf,
html.b #barbuf { html.b #barbuf {
width: calc(100% - 19em); width: calc(100% - 19em);
left: 8em; left: 8em;
@@ -2559,12 +2864,15 @@ html.cy #files tbody div a:last-child {
#pvol { #pvol {
max-width: 9em; max-width: 9em;
} }
html.d #ops,
html.b #ops { html.b #ops {
padding-left: 1.7em; padding-left: 1.7em;
} }
html.d .opview,
html.b .opview { html.b .opview {
margin: 1em; margin: 1em;
} }
html.d #path,
html.b #path { html.b #path {
padding-left: 1.3em; padding-left: 1.3em;
} }

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>⇆🎉 {{ title }}</title> <title>{{ title }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.8"> <meta name="viewport" content="width=device-width, initial-scale=0.8">
{{ html_head }} {{ html_head }}
@@ -139,6 +139,7 @@
dtheme = "{{ dtheme }}", dtheme = "{{ dtheme }}",
srvinf = "{{ srv_info }}", srvinf = "{{ srv_info }}",
lang = "{{ lang }}", lang = "{{ lang }}",
dfavico = "{{ favico }}",
def_hcols = {{ def_hcols|tojson }}, 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 }},
@@ -147,7 +148,9 @@
have_del = {{ have_del|tojson }}, have_del = {{ have_del|tojson }},
have_unpost = {{ have_unpost|tojson }}, have_unpost = {{ have_unpost|tojson }},
have_zip = {{ have_zip|tojson }}, have_zip = {{ have_zip|tojson }},
turbolvl = {{ turbolvl|tojson }}, turbolvl = {{ turbolvl }},
u2sort = "{{ u2sort }}",
have_emp = {{ have_emp|tojson }},
txt_ext = "{{ txt_ext }}", txt_ext = "{{ txt_ext }}",
{% if no_prism %}no_prism = 1,{% endif %} {% if no_prism %}no_prism = 1,{% endif %}
readme = {{ readme|tojson }}, readme = {{ readme|tojson }},

View File

@@ -11,6 +11,7 @@ var Ls = {
"q": "quality / bitrate", "q": "quality / bitrate",
"Ac": "audio codec", "Ac": "audio codec",
"Vc": "video codec", "Vc": "video codec",
"Fmt": "format / container",
"Ahash": "audio checksum", "Ahash": "audio checksum",
"Vhash": "video checksum", "Vhash": "video checksum",
"Res": "resolution", "Res": "resolution",
@@ -36,13 +37,16 @@ var Ls = {
"ot_msg": "msg: send a message to the server log", "ot_msg": "msg: send a message to the server log",
"ot_mp": "media player options", "ot_mp": "media player options",
"ot_cfg": "configuration options", "ot_cfg": "configuration options",
"ot_u2i": 'up2k: upload files (if you have write-access) or toggle into the search-mode to see if they exist somewhere on the server$N$Nuploads are resumable, multithreaded, and file timestamps are preserved, but it uses more CPU than the basic uploader', "ot_u2i": 'up2k: upload files (if you have write-access) or toggle into the search-mode to see if they exist somewhere on the server$N$Nuploads are resumable, multithreaded, and file timestamps are preserved, but it uses more CPU than the basic uploader<br /><br />during uploads, this icon becomes a progress indicator!',
"ot_u2w": 'up2k: upload files with resume support (close your browser and drop the same files in later)$N$Nmultithreaded, and file timestamps are preserved, but it uses more CPU than the basic uploader', "ot_u2w": 'up2k: upload files with resume support (close your browser and drop the same files in later)$N$Nmultithreaded, and file timestamps are preserved, but it uses more CPU than the basic uploader<br /><br />during uploads, this icon becomes a progress indicator!',
"ab_mkdir": "make directory", "ab_mkdir": "make directory",
"ab_mkdoc": "new markdown doc", "ab_mkdoc": "new markdown doc",
"ab_msg": "send msg to srv log", "ab_msg": "send msg to srv log",
"ay_path": "skip to folders",
"ay_files": "skip to files",
"wt_ren": "rename selected items$NHotkey: F2", "wt_ren": "rename selected items$NHotkey: F2",
"wt_del": "delete selected items$NHotkey: ctrl-K", "wt_del": "delete selected items$NHotkey: ctrl-K",
"wt_cut": "cut selected items &lt;small&gt;(then paste somewhere else)&lt;/small&gt;$NHotkey: ctrl-X", "wt_cut": "cut selected items &lt;small&gt;(then paste somewhere else)&lt;/small&gt;$NHotkey: ctrl-X",
@@ -60,6 +64,7 @@ var Ls = {
"ul_par": "parallel uploads:", "ul_par": "parallel uploads:",
"ut_mt": "continue hashing other files while uploading$N$Nmaybe disable if your CPU or HDD is a bottleneck", "ut_mt": "continue hashing other files while uploading$N$Nmaybe disable if your CPU or HDD is a bottleneck",
"ut_ask": "ask for confirmation before upload starts", "ut_ask": "ask for confirmation before upload starts",
"ut_pot": "improve upload speed on slow devices$Nby making the UI less complex",
"ut_srch": "don't actually upload, instead check if the files already $N exist on the server (will scan all folders you can read)", "ut_srch": "don't actually upload, instead check if the files already $N exist on the server (will scan all folders you can read)",
"ut_par": "pause uploads by setting it to 0$N$Nincrease if your connection is slow / high latency$N$Nkeep it 1 on LAN or if the server HDD is a bottleneck", "ut_par": "pause uploads by setting it to 0$N$Nincrease if your connection is slow / high latency$N$Nkeep it 1 on LAN or if the server HDD is a bottleneck",
"ul_btn": "drop files / folders<br>here (or click me)", "ul_btn": "drop files / folders<br>here (or click me)",
@@ -75,7 +80,7 @@ var Ls = {
"ut_etat": "average &lt;em&gt;total&lt;/em&gt; speed and estimated time until finish", "ut_etat": "average &lt;em&gt;total&lt;/em&gt; speed and estimated time until finish",
"uct_ok": "completed successfully", "uct_ok": "completed successfully",
"uct_ng": "failed / rejected / not-found", "uct_ng": "no-good: failed / rejected / not-found",
"uct_done": "ok and ng combined", "uct_done": "ok and ng combined",
"uct_bz": "hashing or uploading", "uct_bz": "hashing or uploading",
"uct_q": "idle, pending", "uct_q": "idle, pending",
@@ -98,9 +103,11 @@ var Ls = {
"cl_favico": "favicon", "cl_favico": "favicon",
"cl_keytype": "key notation", "cl_keytype": "key notation",
"cl_hiddenc": "hidden columns", "cl_hiddenc": "hidden columns",
"cl_reset": "(reset)",
"ct_thumb": "in icon view, toggle icons or thumbnails$NHotkey: T", "ct_thumb": "in icon view, toggle icons or thumbnails$NHotkey: T",
"ct_dots": "show hidden files (if server permits)", "ct_dots": "show hidden files (if server permits)",
"ct_dir1st": "sort folders before files",
"ct_readme": "show README.md in folder listings", "ct_readme": "show README.md in folder listings",
"cut_turbo": "the yolo button, you probably DO NOT want to enable this:$N$Nuse this if you were uploading a huge amount of files and had to restart for some reason, and want to continue the upload ASAP$N$Nthis replaces the hash-check with a simple <em>&quot;does this have the same filesize on the server?&quot;</em> so if the file contents are different it will NOT be uploaded$N$Nyou should turn this off when the upload is done, and then &quot;upload&quot; the same files again to let the client verify them", "cut_turbo": "the yolo button, you probably DO NOT want to enable this:$N$Nuse this if you were uploading a huge amount of files and had to restart for some reason, and want to continue the upload ASAP$N$Nthis replaces the hash-check with a simple <em>&quot;does this have the same filesize on the server?&quot;</em> so if the file contents are different it will NOT be uploaded$N$Nyou should turn this off when the upload is done, and then &quot;upload&quot; the same files again to let the client verify them",
@@ -109,6 +116,10 @@ var Ls = {
"cut_flag": "ensure only one tab is uploading at a time $N -- other tabs must have this enabled too $N -- only affects tabs on the same domain", "cut_flag": "ensure only one tab is uploading at a time $N -- other tabs must have this enabled too $N -- only affects tabs on the same domain",
"cut_az": "upload files in alphabetical order, rather than smallest-file-first$N$Nalphabetical order can make it easier to eyeball if something went wrong on the server, but it makes uploading slightly slower on fiber / LAN",
"cut_mt": "use multithreading to accelerate file hashing$N$Nthis uses web-workers and requires$Nmore RAM (up to 512 MiB extra)$N$N30% faster https, 4.5x faster http,$Nand 5.3x faster on android phones",
"cft_text": "favicon text (blank and refresh to disable)", "cft_text": "favicon text (blank and refresh to disable)",
"cft_fg": "foreground color", "cft_fg": "foreground color",
"cft_bg": "background color", "cft_bg": "background color",
@@ -139,7 +150,7 @@ var Ls = {
"mt_caac": "convert aac / m4a to opus\">aac", "mt_caac": "convert aac / m4a to opus\">aac",
"mt_coth": "convert all others (not mp3) to opus\">oth", "mt_coth": "convert all others (not mp3) to opus\">oth",
"mt_tint": "background level (0-100) on the seekbar$Nto make buffering less distracting", "mt_tint": "background level (0-100) on the seekbar$Nto make buffering less distracting",
"mt_eq": "enables the equalizer and gain control;$Nboost 0 = unmodified 100% volume$N$Nenabling the equalizer makes gapless albums fully gapless, so leave it on with all the values at zero if you care about that", "mt_eq": "enables the equalizer and gain control;$N$Nboost &lt;code&gt;0&lt;/code&gt; = standard 100% volume (unmodified)$N$Nwidth &lt;code&gt;1 &nbsp;&lt;/code&gt; = standard stereo (unmodified)$Nwidth &lt;code&gt;0.5&lt;/code&gt; = 50% left-right crossfeed$Nwidth &lt;code&gt;0 &nbsp;&lt;/code&gt; = mono$N$Nboost &lt;code&gt;-0.8&lt;/code&gt; &amp; width &lt;code&gt;10&lt;/code&gt; = vocal removal :^)$N$Nenabling the equalizer makes gapless albums fully gapless, so leave it on with all the values at zero (except width = 1) if you care about that",
"mb_play": "play", "mb_play": "play",
"mm_hashplay": "play this audio file?", "mm_hashplay": "play this audio file?",
@@ -244,6 +255,7 @@ var Ls = {
"md_eshow": "cannot show ", "md_eshow": "cannot show ",
"xhr403": "403: Access denied\n\ntry pressing F5, maybe you got logged out", "xhr403": "403: Access denied\n\ntry pressing F5, maybe you got logged out",
"cf_ok": "sorry about that -- DD" + wah + "oS protection kicked in\n\nthings should resume in about 30 sec\n\nif nothing happens, hit F5 to reload the page",
"tl_xe1": "could not list subfolders:\n\nerror ", "tl_xe1": "could not list subfolders:\n\nerror ",
"tl_xe2": "404: Folder not found", "tl_xe2": "404: Folder not found",
"fl_xe1": "could not list files in folder:\n\nerror ", "fl_xe1": "could not list files in folder:\n\nerror ",
@@ -278,8 +290,12 @@ var Ls = {
"u_https1": "you should", "u_https1": "you should",
"u_https2": "switch to https", "u_https2": "switch to https",
"u_https3": "for much better performance", "u_https3": "for better performance",
"u_ancient": 'your browser is impressively ancient -- maybe you should <a href="#" onclick="goto(\'bup\')">use bup instead</a>', "u_ancient": 'your browser is impressively ancient -- maybe you should <a href="#" onclick="goto(\'bup\')">use bup instead</a>',
"u_nowork": "need firefox 53+ or chrome 57+ or iOS 11+",
"u_enpot": 'switch to <a href="#">potato UI</a> (may improve upload speed)',
"u_depot": 'switch to <a href="#">fancy UI</a> (may reduce upload speed)',
"u_gotpot": 'switching to the potato UI for improved upload speed,\n\nfeel free to disagree and switch back!',
"u_ever": "this is the basic uploader; up2k needs at least<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_ever": "this is the basic uploader; up2k needs at least<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1",
"u_su2k": 'this is the basic uploader; <a href="#" id="u2yea">up2k</a> is better', "u_su2k": 'this is the basic uploader; <a href="#" id="u2yea">up2k</a> is better',
"u_ewrite": 'you do not have write-access to this folder', "u_ewrite": 'you do not have write-access to this folder',
@@ -297,9 +313,15 @@ var Ls = {
"u_upping": 'uploading', "u_upping": 'uploading',
"u_cuerr": "failed to upload chunk {0} of {1};\nprobably harmless, continuing\n\nfile: {2}", "u_cuerr": "failed to upload chunk {0} of {1};\nprobably harmless, continuing\n\nfile: {2}",
"u_cuerr2": "server rejected upload (chunk {0} of {1});\n\nfile: {2}\n\nerror ", "u_cuerr2": "server rejected upload (chunk {0} of {1});\n\nfile: {2}\n\nerror ",
"u_ehstmp": "will retry; see bottom-right",
"u_ehsfin": "server rejected the request to finalize upload", "u_ehsfin": "server rejected the request to finalize upload",
"u_ehssrch": "server rejected the request to perform search", "u_ehssrch": "server rejected the request to perform search",
"u_ehsinit": "server rejected the request to initiate upload", "u_ehsinit": "server rejected the request to initiate upload",
"u_ehsdf": "server ran out of disk space!\n\nwill keep retrying, in case someone\nfrees up enough space to continue",
"u_emtleak1": "it looks like your webbrowser may have a memory leak;\nplease",
"u_emtleak2": ' <a href="{0}">switch to https (recommended)</a> or ',
"u_emtleak3": ' ',
"u_emtleak4": "try the following:\n<ul><li>hit <code>F5</code> to refresh the page</li><li>then disable the &nbsp;<code>mt</code>&nbsp; button in the &nbsp;<code>⚙️ settings</code></li><li>and try that upload again</li></ul>Uploads will be a bit slower, but oh well.\nSorry for the trouble !",
"u_s404": "not found on server", "u_s404": "not found on server",
"u_expl": "explain", "u_expl": "explain",
"u_tu": '<p class="warn">WARNING: turbo enabled, <span>&nbsp;client may not detect and resume incomplete uploads; see turbo-button tooltip</span></p>', "u_tu": '<p class="warn">WARNING: turbo enabled, <span>&nbsp;client may not detect and resume incomplete uploads; see turbo-button tooltip</span></p>',
@@ -331,6 +353,7 @@ var Ls = {
"q": "kvalitet / bitrate", "q": "kvalitet / bitrate",
"Ac": "lyd-format", "Ac": "lyd-format",
"Vc": "video-format", "Vc": "video-format",
"Fmt": "format / innpakning",
"Ahash": "lyd-kontrollsum", "Ahash": "lyd-kontrollsum",
"Vhash": "video-kontrollsum", "Vhash": "video-kontrollsum",
"Res": "oppløsning", "Res": "oppløsning",
@@ -356,13 +379,16 @@ var Ls = {
"ot_msg": "msg: send en beskjed til serverloggen", "ot_msg": "msg: send en beskjed til serverloggen",
"ot_mp": "musikkspiller-instillinger", "ot_mp": "musikkspiller-instillinger",
"ot_cfg": "andre innstillinger", "ot_cfg": "andre innstillinger",
"ot_u2i": 'up2k: last opp filer (hvis du har skrivetilgang) eller bytt til søkemodus for å sjekke om filene finnes et-eller-annet sted på serveren$N$Nopplastninger kan gjenopptas etter avbrudd, skjer stykkevis for potensielt høyere ytelse, og ivaretar datostempling -- men bruker litt mer prosessorkraft enn den primitive opplasteren bup', "ot_u2i": 'up2k: last opp filer (hvis du har skrivetilgang) eller bytt til søkemodus for å sjekke om filene finnes et-eller-annet sted på serveren$N$Nopplastninger kan gjenopptas etter avbrudd, skjer stykkevis for potensielt høyere ytelse, og ivaretar datostempling -- men bruker litt mer prosessorkraft enn den primitive opplasteren bup<br /><br />mens opplastninger foregår så vises fremdriften her oppe!',
"ot_u2w": 'up2k: filopplastning med støtte for å gjenoppta avbrutte opplastninger -- steng ned nettleseren og dra de samme filene inn i nettleseren igjen for å plukke opp igjen der du slapp$N$Nopplastninger skjer stykkevis for potensielt høyere ytelse, og ivaretar datostempling -- men bruker litt mer prosessorkraft enn den primitive opplasteren "bup"', "ot_u2w": 'up2k: filopplastning med støtte for å gjenoppta avbrutte opplastninger -- steng ned nettleseren og dra de samme filene inn i nettleseren igjen for å plukke opp igjen der du slapp$N$Nopplastninger skjer stykkevis for potensielt høyere ytelse, og ivaretar datostempling -- men bruker litt mer prosessorkraft enn den primitive opplasteren "bup"<br /><br />mens opplastninger foregår så vises fremdriften her oppe!',
"ab_mkdir": "lag mappe", "ab_mkdir": "lag mappe",
"ab_mkdoc": "nytt dokument", "ab_mkdoc": "nytt dokument",
"ab_msg": "send melding", "ab_msg": "send melding",
"ay_path": "gå videre til mapper",
"ay_files": "gå videre til filer",
"wt_ren": "gi nye navn til de valgte filene$NSnarvei: F2", "wt_ren": "gi nye navn til de valgte filene$NSnarvei: F2",
"wt_del": "slett de valgte filene$NSnarvei: ctrl-K", "wt_del": "slett de valgte filene$NSnarvei: ctrl-K",
"wt_cut": "klipp ut de valgte filene &lt;small&gt;(for å lime inn et annet sted)&lt;/small&gt;$NSnarvei: ctrl-X", "wt_cut": "klipp ut de valgte filene &lt;small&gt;(for å lime inn et annet sted)&lt;/small&gt;$NSnarvei: ctrl-X",
@@ -380,6 +406,7 @@ var Ls = {
"ul_par": "samtidige handl.:", "ul_par": "samtidige handl.:",
"ut_mt": "fortsett å befare køen mens opplastning foregår$N$Nskru denne av dersom du har en$Ntreg prosessor eller harddisk", "ut_mt": "fortsett å befare køen mens opplastning foregår$N$Nskru denne av dersom du har en$Ntreg prosessor eller harddisk",
"ut_ask": "bekreft filutvalg før opplastning starter", "ut_ask": "bekreft filutvalg før opplastning starter",
"ut_pot": "forbedre ytelsen på trege enheter ved å$Nforenkle brukergrensesnittet",
"ut_srch": "utfør søk istedenfor å laste opp --$Nleter igjennom alle mappene du har lov til å se", "ut_srch": "utfør søk istedenfor å laste opp --$Nleter igjennom alle mappene du har lov til å se",
"ut_par": "sett til 0 for å midlertidig stanse opplastning$N$Nhøye verdier (4 eller 8) kan gi bedre ytelse,$Nspesielt på trege internettlinjer$N$Nbør ikke være høyere enn 1 på LAN$Neller hvis serveren sin harddisk er treg", "ut_par": "sett til 0 for å midlertidig stanse opplastning$N$Nhøye verdier (4 eller 8) kan gi bedre ytelse,$Nspesielt på trege internettlinjer$N$Nbør ikke være høyere enn 1 på LAN$Neller hvis serveren sin harddisk er treg",
"ul_btn": "slipp filer / mapper<br>her (eller klikk meg)", "ul_btn": "slipp filer / mapper<br>her (eller klikk meg)",
@@ -418,9 +445,11 @@ var Ls = {
"cl_favico": "favicon", "cl_favico": "favicon",
"cl_keytype": "notasjon for musikalsk dur", "cl_keytype": "notasjon for musikalsk dur",
"cl_hiddenc": "skjulte kolonner", "cl_hiddenc": "skjulte kolonner",
"cl_reset": "(nullstill)",
"ct_thumb": "vis miniatyrbilder istedenfor ikoner$NSnarvei: T", "ct_thumb": "vis miniatyrbilder istedenfor ikoner$NSnarvei: T",
"ct_dots": "vis skjulte filer (gitt at serveren tillater det)", "ct_dots": "vis skjulte filer (gitt at serveren tillater det)",
"ct_dir1st": "sorter slik at mapper kommer foran filer",
"ct_readme": "vis README.md nedenfor filene", "ct_readme": "vis README.md nedenfor filene",
"cut_turbo": "forenklet befaring ved opplastning; bør sannsynlig <em>ikke</em> skrus på:$N$Nnyttig dersom du var midt i en svær opplastning som måtte restartes av en eller annen grunn, og du vil komme igang igjen så raskt som overhodet mulig.$N$Nnår denne er skrudd på så forenkles befaringen kraftig; istedenfor å utføre en trygg sjekk på om filene finnes på serveren i god stand, så sjekkes kun om <em>filstørrelsen</em> stemmer. Så dersom en korrupt fil skulle befinne seg på serveren allerede, på samme sted med samme størrelse og navn, så blir det <em>ikke oppdaget</em>.$N$Ndet anbefales å kun benytte denne funksjonen for å komme seg raskt igjennom selve opplastningen, for så å skru den av, og til slutt &quot;laste opp&quot; de samme filene én gang til -- slik at integriteten kan verifiseres", "cut_turbo": "forenklet befaring ved opplastning; bør sannsynlig <em>ikke</em> skrus på:$N$Nnyttig dersom du var midt i en svær opplastning som måtte restartes av en eller annen grunn, og du vil komme igang igjen så raskt som overhodet mulig.$N$Nnår denne er skrudd på så forenkles befaringen kraftig; istedenfor å utføre en trygg sjekk på om filene finnes på serveren i god stand, så sjekkes kun om <em>filstørrelsen</em> stemmer. Så dersom en korrupt fil skulle befinne seg på serveren allerede, på samme sted med samme størrelse og navn, så blir det <em>ikke oppdaget</em>.$N$Ndet anbefales å kun benytte denne funksjonen for å komme seg raskt igjennom selve opplastningen, for så å skru den av, og til slutt &quot;laste opp&quot; de samme filene én gang til -- slik at integriteten kan verifiseres",
@@ -429,6 +458,10 @@ var Ls = {
"cut_flag": "samkjører nettleserfaner slik at bare én $N kan holde på med befaring / opplastning $N -- andre faner må også ha denne skrudd på $N -- fungerer kun innenfor samme domene", "cut_flag": "samkjører nettleserfaner slik at bare én $N kan holde på med befaring / opplastning $N -- andre faner må også ha denne skrudd på $N -- fungerer kun innenfor samme domene",
"cut_az": "last opp filer i alfabetisk rekkefølge, istedenfor minste-fil-først$N$Nalfabetisk kan gjøre det lettere å anslå om alt gikk bra, men er bittelitt tregere på fiber / LAN",
"cut_mt": "raskere befaring ved å bruke hele CPU'en$N$Ndenne funksjonen anvender web-workers$Nog krever mer RAM (opptil 512 MiB ekstra)$N$N30% raskere https, 4.5x raskere http,$Nog 5.3x raskere på android-telefoner",
"cft_text": "ikontekst (blank ut og last siden på nytt for å deaktivere)", "cft_text": "ikontekst (blank ut og last siden på nytt for å deaktivere)",
"cft_fg": "farge", "cft_fg": "farge",
"cft_bg": "bakgrunnsfarge", "cft_bg": "bakgrunnsfarge",
@@ -459,7 +492,7 @@ var Ls = {
"mt_caac": "konverter aac / m4a-filer til to opus\">aac", "mt_caac": "konverter aac / m4a-filer til to opus\">aac",
"mt_coth": "konverter alt annet (men ikke mp3) til opus\">andre", "mt_coth": "konverter alt annet (men ikke mp3) til opus\">andre",
"mt_tint": "nivå av bakgrunnsfarge på søkestripa (0-100),$Ngjør oppdateringer mindre distraherende", "mt_tint": "nivå av bakgrunnsfarge på søkestripa (0-100),$Ngjør oppdateringer mindre distraherende",
"mt_eq": "aktiver tonekontroll og forsterker;$Nboost 0 = normal volumskala$N$Nreduserer også dødtid imellom sangfiler", "mt_eq": "aktiver tonekontroll og forsterker;$N$Nboost &lt;code&gt;0&lt;/code&gt; = normal volumskala$N$Nwidth &lt;code&gt;1 &nbsp;&lt;/code&gt; = normal stereo$Nwidth &lt;code&gt;0.5&lt;/code&gt; = 50% blanding venstre-høyre$Nwidth &lt;code&gt;0 &nbsp;&lt;/code&gt; = mono$N$Nboost &lt;code&gt;-0.8&lt;/code&gt; &amp; width &lt;code&gt;10&lt;/code&gt; = instrumental :^)$N$Nreduserer også dødtid imellom sangfiler",
"mb_play": "lytt", "mb_play": "lytt",
"mm_hashplay": "spill denne sangen?", "mm_hashplay": "spill denne sangen?",
@@ -564,6 +597,7 @@ var Ls = {
"md_eshow": "kan ikke vise ", "md_eshow": "kan ikke vise ",
"xhr403": "403: Tilgang nektet\n\nkanskje du ble logget ut? prøv å trykk F5", "xhr403": "403: Tilgang nektet\n\nkanskje du ble logget ut? prøv å trykk F5",
"cf_ok": "beklager -- liten tilfeldig kontroll, alt OK\n\nting skal fortsette om ca. 30 sekunder\n\nhvis ikkeno skjer, trykk F5 for å laste siden på nytt",
"tl_xe1": "kunne ikke hente undermapper:\n\nfeil ", "tl_xe1": "kunne ikke hente undermapper:\n\nfeil ",
"tl_xe2": "404: Mappen finnes ikke", "tl_xe2": "404: Mappen finnes ikke",
"fl_xe1": "kunne ikke hente filer i mappen:\n\nfeil ", "fl_xe1": "kunne ikke hente filer i mappen:\n\nfeil ",
@@ -589,8 +623,8 @@ var Ls = {
"un_max": "viser de første 2000 filene (bruk filteret for å innsnevre)", "un_max": "viser de første 2000 filene (bruk filteret for å innsnevre)",
"un_avail": "{0} filer kan slettes", "un_avail": "{0} filer kan slettes",
"un_m2": "sortert etter opplastningstid &ndash; nyeste først:", "un_m2": "sortert etter opplastningstid &ndash; nyeste først:",
"un_no1": "men nei, her var det jaggu ingenting", "un_no1": "men nei, her var det jaggu ikkeno som slettes kan",
"un_no2": "men nei, her var det jaggu ingenting som passer overens med filteret", "un_no2": "men nei, her var det jaggu ingenting som passet overens med filteret",
"un_next": "slett de neste {0} filene nedenfor", "un_next": "slett de neste {0} filene nedenfor",
"un_del": "slett", "un_del": "slett",
"un_m3": "henter listen med nylig opplastede filer...", "un_m3": "henter listen med nylig opplastede filer...",
@@ -598,8 +632,12 @@ var Ls = {
"u_https1": "du burde", "u_https1": "du burde",
"u_https2": "bytte til https", "u_https2": "bytte til https",
"u_https3": "for mye høyere hastighet", "u_https3": "for høyere hastighet",
"u_ancient": 'nettleseren din er prehistorisk -- mulig du burde <a href="#" onclick="goto(\'bup\')">bruke bup istedenfor</a>', "u_ancient": 'nettleseren din er prehistorisk -- mulig du burde <a href="#" onclick="goto(\'bup\')">bruke bup istedenfor</a>',
"u_nowork": "krever firefox 53+, chrome 57+, eller iOS 11+",
"u_enpot": 'bytt til <a href="#">enkelt UI</a> (gir sannsynlig raskere opplastning)',
"u_depot": 'bytt til <a href="#">snæsent UI</a> (gir sannsynlig tregere opplastning)',
"u_gotpot": 'byttet til et enklere UI for å laste opp raskere,\n\ndu kan gjerne bytte tilbake altså!',
"u_ever": "dette er den primitive opplasteren; up2k krever minst:<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_ever": "dette er den primitive opplasteren; up2k krever minst:<br>chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1",
"u_su2k": 'dette er den primitive opplasteren; <a href="#" id="u2yea">up2k</a> er bedre', "u_su2k": 'dette er den primitive opplasteren; <a href="#" id="u2yea">up2k</a> er bedre',
"u_ewrite": 'du har ikke skrivetilgang i denne mappen', "u_ewrite": 'du har ikke skrivetilgang i denne mappen',
@@ -616,10 +654,16 @@ var Ls = {
"u_hashing": 'les', "u_hashing": 'les',
"u_upping": 'sender', "u_upping": 'sender',
"u_cuerr": "kunne ikke laste opp del {0} av {1};\nsikkert harmløst, fortsetter\n\nfil: {2}", "u_cuerr": "kunne ikke laste opp del {0} av {1};\nsikkert harmløst, fortsetter\n\nfil: {2}",
"u_cuerr2": "server nektet opplastningen (del {0} of {1});\n\nfile: {2}\n\nerror ", "u_cuerr2": "server nektet opplastningen (del {0} av {1});\n\nfile: {2}\n\nerror ",
"u_ehstmp": "prøver igjen; se mld nederst",
"u_ehsfin": "server nektet forespørselen om å ferdigstille filen", "u_ehsfin": "server nektet forespørselen om å ferdigstille filen",
"u_ehssrch": "server nektet forespørselen om å utføre søk", "u_ehssrch": "server nektet forespørselen om å utføre søk",
"u_ehsinit": "server nektet forespørselen om å begynne en ny opplastning", "u_ehsinit": "server nektet forespørselen om å begynne en ny opplastning",
"u_ehsdf": "serveren er full!\n\nprøver igjen regelmessig,\ni tilfelle noen rydder litt...",
"u_emtleak1": "uff, det er mulig at nettleseren din har en minnelekkasje...\nForeslår",
"u_emtleak2": ' helst at du <a href="{0}">bytter til https</a>, eller ',
"u_emtleak3": ' at du ',
"u_emtleak4": "prøver følgende:\n<ul><li>trykk F5 for å laste siden på nytt</li><li>så skru av &nbsp;<code>mt</code>&nbsp; bryteren under &nbsp;<code>⚙️ innstillinger</code></li><li>og forsøk den samme opplastningen igjen</li></ul>Opplastning vil gå litt tregere, men det får så være.\nBeklager bryderiet !",
"u_s404": "ikke funnet på serveren", "u_s404": "ikke funnet på serveren",
"u_expl": "forklar", "u_expl": "forklar",
"u_tu": '<p class="warn">ADVARSEL: turbo er på, <span>&nbsp;avbrutte opplastninger vil muligens ikke oppdages og gjenopptas; hold musepekeren over turbo-knappen for mer info</span></p>', "u_tu": '<p class="warn">ADVARSEL: turbo er på, <span>&nbsp;avbrutte opplastninger vil muligens ikke oppdages og gjenopptas; hold musepekeren over turbo-knappen for mer info</span></p>',
@@ -707,6 +751,10 @@ ebi('op_up2k').innerHTML = (
' <label for="multitask" tt="' + L.ut_mt + '">🏃</label>\n' + ' <label for="multitask" tt="' + L.ut_mt + '">🏃</label>\n' +
' </td>\n' + ' </td>\n' +
' <td class="c" rowspan="2">\n' + ' <td class="c" rowspan="2">\n' +
' <input type="checkbox" id="potato" />\n' +
' <label for="potato" tt="' + L.ut_pot + '">🥔</label>\n' +
' </td>\n' +
' <td class="c" rowspan="2">\n' +
' <input type="checkbox" id="ask_up" />\n' + ' <input type="checkbox" id="ask_up" />\n' +
' <label for="ask_up" tt="' + L.ut_ask + '">💭</label>\n' + ' <label for="ask_up" tt="' + L.ut_ask + '">💭</label>\n' +
' </td>\n' + ' </td>\n' +
@@ -729,7 +777,7 @@ ebi('op_up2k').innerHTML = (
'<div id="u2notbtn"></div>\n' + '<div id="u2notbtn"></div>\n' +
'<div id="u2btn_ct">\n' + '<div id="u2btn_ct">\n' +
' <div id="u2btn">\n' + ' <div id="u2btn" tabindex="0">\n' +
' <span id="u2bm"></span>\n' + L.ul_btn + ' <span id="u2bm"></span>\n' + L.ul_btn +
' </div>\n' + ' </div>\n' +
'</div>\n' + '</div>\n' +
@@ -753,7 +801,7 @@ ebi('op_up2k').innerHTML = (
'</div>\n' + '</div>\n' +
'<div id="u2tabw"><table id="u2tab">\n' + '<div id="u2tabw" class="na"><table id="u2tab">\n' +
' <thead>\n' + ' <thead>\n' +
' <tr>\n' + ' <tr>\n' +
' <td>' + L.utl_name + '</td>\n' + ' <td>' + L.utl_name + '</td>\n' +
@@ -762,10 +810,10 @@ ebi('op_up2k').innerHTML = (
' </tr>\n' + ' </tr>\n' +
' </thead>\n' + ' </thead>\n' +
' <tbody></tbody>\n' + ' <tbody></tbody>\n' +
'</table></div>\n' + '</table><div id="u2mu"></div></div>\n' +
'<p id="u2flagblock"><b>' + L.ul_flagblk + '</p>\n' + '<p id="u2flagblock"><b>' + L.ul_flagblk + '</p>\n' +
'<p id="u2foot"></p>' '<div id="u2foot"></div>'
); );
@@ -792,6 +840,7 @@ ebi('op_cfg').innerHTML = (
' <a id="griden" class="tgl btn" href="#" tt="' + L.wt_grid + '">田 the grid</a>\n' + ' <a id="griden" class="tgl btn" href="#" tt="' + L.wt_grid + '">田 the grid</a>\n' +
' <a id="thumbs" class="tgl btn" href="#" tt="' + L.ct_thumb + '">🖼️ thumbs</a>\n' + ' <a id="thumbs" class="tgl btn" href="#" tt="' + L.ct_thumb + '">🖼️ thumbs</a>\n' +
' <a id="dotfiles" class="tgl btn" href="#" tt="' + L.ct_dots + '">dotfiles</a>\n' + ' <a id="dotfiles" class="tgl btn" href="#" tt="' + L.ct_dots + '">dotfiles</a>\n' +
' <a id="dir1st" class="tgl btn" href="#" tt="' + L.ct_dir1st + '">📁 first</a>\n' +
' <a id="ireadme" class="tgl btn" href="#" tt="' + L.ct_readme + '">📜 readme</a>\n' + ' <a id="ireadme" class="tgl btn" href="#" tt="' + L.ct_readme + '">📜 readme</a>\n' +
' </div>\n' + ' </div>\n' +
'</div>\n' + '</div>\n' +
@@ -811,9 +860,11 @@ ebi('op_cfg').innerHTML = (
'<div>\n' + '<div>\n' +
' <h3>' + L.cl_uopts + '</h3>\n' + ' <h3>' + L.cl_uopts + '</h3>\n' +
' <div>\n' + ' <div>\n' +
' <a id="hashw" class="tgl btn" href="#" tt="' + L.cut_mt + '">mt</a>\n' +
' <a id="u2turbo" class="tgl btn ttb" href="#" tt="' + L.cut_turbo + '">turbo</a>\n' + ' <a id="u2turbo" class="tgl btn ttb" href="#" tt="' + L.cut_turbo + '">turbo</a>\n' +
' <a id="u2tdate" class="tgl btn ttb" href="#" tt="' + L.cut_datechk + '">date-chk</a>\n' + ' <a id="u2tdate" class="tgl btn ttb" href="#" tt="' + L.cut_datechk + '">date-chk</a>\n' +
' <a id="flag_en" class="tgl btn" href="#" tt="' + L.cut_flag + '">💤</a>\n' + ' <a id="flag_en" class="tgl btn" href="#" tt="' + L.cut_flag + '">💤</a>\n' +
' <a id="u2sort" class="tgl btn" href="#" tt="' + L.cut_az + '">az</a>\n' +
' </td>\n' + ' </td>\n' +
' </div>\n' + ' </div>\n' +
'</div>\n' + '</div>\n' +
@@ -827,7 +878,7 @@ ebi('op_cfg').innerHTML = (
' </div>\n' + ' </div>\n' +
'</div>\n' + '</div>\n' +
'<div><h3>' + L.cl_keytype + '</h3><div id="key_notation"></div></div>\n' + '<div><h3>' + L.cl_keytype + '</h3><div id="key_notation"></div></div>\n' +
'<div class="fill"><h3>' + L.cl_hiddenc + '</h3><div id="hcols"></div></div>' '<div><h3>' + L.cl_hiddenc + ' <a href="#" id="hcolsr">' + L.cl_reset + '</h3><div id="hcols"></div></div>'
); );
@@ -880,7 +931,7 @@ function opclick(e) {
goto(dest); goto(dest);
var input = QS('.opview.act input:not([type="hidden"])') var input = QS('.opview.act input:not([type="hidden"])')
if (input && !is_touch) { if (input && !TOUCH) {
tt.skip = true; tt.skip = true;
input.focus(); input.focus();
} }
@@ -1000,7 +1051,7 @@ var mpl = (function () {
'<div><h3>' + L.ml_eq + '</h3><div id="audio_eq"></div></div>'); '<div><h3>' + L.ml_eq + '</h3><div id="audio_eq"></div></div>');
var r = { var r = {
"pb_mode": (sread('pb_mode') || 'loop').split('-')[0], "pb_mode": (sread('pb_mode') || 'next').split('-')[0],
"os_ctl": bcfg_get('au_os_ctl', have_mctl) && have_mctl, "os_ctl": bcfg_get('au_os_ctl', have_mctl) && have_mctl,
}; };
bcfg_bind(r, 'preload', 'au_preload', true); bcfg_bind(r, 'preload', 'au_preload', true);
@@ -1491,11 +1542,15 @@ var pbar = (function () {
return; return;
var sm = bc.w * 1.0 / mp.au.duration, var sm = bc.w * 1.0 / mp.au.duration,
gk = bc.h + '' + light; gk = bc.h + '' + light,
dz = themen == 'dz',
dy = themen == 'dy';
if (gradh != gk) { if (gradh != gk) {
gradh = gk; gradh = gk;
grad = glossy_grad(bc, 85, [35, 40, 37, 35], light ? [45, 56, 50, 45] : [42, 51, 47, 42]); grad = glossy_grad(bc, dz ? 120 : 85,
dy ? [0, 0, 0, 0] : [35, 40, 37, 35],
dy ? [20, 24, 22, 20] : light ? [45, 56, 50, 45] : [42, 51, 47, 42]);
} }
bctx.fillStyle = grad; bctx.fillStyle = grad;
for (var a = 0; a < mp.au.buffered.length; a++) { for (var a = 0; a < mp.au.buffered.length; a++) {
@@ -1517,18 +1572,20 @@ var pbar = (function () {
if (!mp || !mp.au || isNaN(adur = mp.au.duration) || isNaN(apos = mp.au.currentTime) || apos < 0 || adur < apos) if (!mp || !mp.au || isNaN(adur = mp.au.duration) || isNaN(apos = mp.au.currentTime) || apos < 0 || adur < apos)
return; // not-init || unsupp-codec return; // not-init || unsupp-codec
var sm = bc.w * 1.0 / adur; var sm = bc.w * 1.0 / adur,
dz = themen == 'dz',
dy = themen == 'dy';
pctx.fillStyle = light ? 'rgba(0,64,0,0.15)' : 'rgba(204,255,128,0.15)'; pctx.fillStyle = light && !dy ? 'rgba(0,64,0,0.15)' : 'rgba(204,255,128,0.15)';
for (var p = 1, mins = adur / 10; p <= mins; p++) for (var p = 1, mins = adur / 10; p <= mins; p++)
pctx.fillRect(Math.floor(sm * p * 10), 0, 2, pc.h); pctx.fillRect(Math.floor(sm * p * 10), 0, 2, pc.h);
pctx.fillStyle = light ? 'rgba(0,64,0,0.5)' : 'rgba(192,255,96,0.5)'; pctx.fillStyle = light && !dy ? 'rgba(0,64,0,0.5)' : 'rgba(192,255,96,0.5)';
for (var p = 1, mins = adur / 60; p <= mins; p++) for (var p = 1, mins = adur / 60; p <= mins; p++)
pctx.fillRect(Math.floor(sm * p * 60), 0, 2, pc.h); pctx.fillRect(Math.floor(sm * p * 60), 0, 2, pc.h);
pctx.font = '.5em sans-serif'; pctx.font = '.5em sans-serif';
pctx.fillStyle = light ? 'rgba(0,64,0,0.9)' : 'rgba(192,255,96,1)'; pctx.fillStyle = dz ? '#0f0' : dy ? '#999' : light ? 'rgba(0,64,0,0.9)' : 'rgba(192,255,96,1)';
for (var p = 1, mins = adur / 60; p <= mins; p++) { for (var p = 1, mins = adur / 60; p <= mins; p++) {
pctx.fillText(p, Math.floor(sm * p * 60 + 3), pc.h / 3); pctx.fillText(p, Math.floor(sm * p * 60 + 3), pc.h / 3);
} }
@@ -1591,11 +1648,18 @@ var vbar = (function () {
if (!mp) if (!mp)
return; return;
var gh = h + '' + light; var gh = h + '' + light,
dz = themen == 'dz',
dy = themen == 'dy';
if (gradh != gh) { if (gradh != gh) {
gradh = gh; gradh = gh;
grad1 = glossy_grad(r.can, 50, light ? [50, 55, 52, 48] : [45, 52, 47, 43], light ? [54, 60, 52, 47] : [42, 51, 47, 42]); grad1 = glossy_grad(r.can, dz ? 120 : 50,
grad2 = glossy_grad(r.can, 205, [10, 15, 13, 10], [16, 20, 18, 16]); dy ? [0, 0, 0, 0] : light ? [50, 55, 52, 48] : [45, 52, 47, 43],
dy ? [20, 24, 22, 20] : light ? [54, 60, 52, 47] : [42, 51, 47, 42]);
grad2 = glossy_grad(r.can, dz ? 120 : 205,
dz ? [100, 100, 100, 100] : dy ? [0, 0, 0, 0] : [10, 15, 13, 10],
dz ? [10, 14, 12, 10] : dy ? [90, 90, 90, 90] : [16, 20, 18, 16]);
} }
ctx.fillStyle = grad2; ctx.fillRect(0, 0, w, h); ctx.fillStyle = grad2; ctx.fillRect(0, 0, w, h);
ctx.fillStyle = grad1; ctx.fillRect(0, 0, w * mp.vol, h); ctx.fillStyle = grad1; ctx.fillRect(0, 0, w * mp.vol, h);
@@ -1637,7 +1701,7 @@ var vbar = (function () {
if (e.button === 0) if (e.button === 0)
can.onmousemove = null; can.onmousemove = null;
}; };
if (is_touch) { if (TOUCH) {
can.ontouchstart = mousedown; can.ontouchstart = mousedown;
can.ontouchmove = mousemove; can.ontouchmove = mousemove;
} }
@@ -1694,6 +1758,14 @@ function prev_song(e) {
return song_skip(-1); return song_skip(-1);
} }
function dl_song() {
if (!mp || !mp.au)
return;
var url = mp.tracks[mp.au.tid];
url += (url.indexOf('?') < 0 ? '?' : '&') + 'cache=987';
dl_file(url);
}
function playpause(e) { function playpause(e) {
@@ -1734,7 +1806,7 @@ function playpause(e) {
seek_au_mul(x * 1.0 / rect.width); seek_au_mul(x * 1.0 / rect.width);
}; };
if (!is_touch) if (!TOUCH)
bar.onwheel = function (e) { bar.onwheel = function (e) {
var dist = Math.sign(e.deltaY) * 10; var dist = Math.sign(e.deltaY) * 10;
if (Math.abs(e.deltaY) < 30 && !e.deltaMode) if (Math.abs(e.deltaY) < 30 && !e.deltaMode)
@@ -1774,7 +1846,7 @@ var mpui = (function () {
if (++nth > 69) { if (++nth > 69) {
// android-chrome breaks aspect ratio with unannounced viewport changes // android-chrome breaks aspect ratio with unannounced viewport changes
nth = 0; nth = 0;
if (is_touch) { if (MOBILE) {
nth = 1; nth = 1;
pbar.onresize(); pbar.onresize();
vbar.onresize(); vbar.onresize();
@@ -1842,6 +1914,7 @@ var audio_eq = (function () {
"gains": [4, 3, 2, 1, 0, 0, 1, 2, 3, 4], "gains": [4, 3, 2, 1, 0, 0, 1, 2, 3, 4],
"filters": [], "filters": [],
"amp": 0, "amp": 0,
"chw": 1,
"last_au": null, "last_au": null,
"acst": {} "acst": {}
}; };
@@ -1893,6 +1966,7 @@ var audio_eq = (function () {
try { try {
r.amp = fcfg_get('au_eq_amp', r.amp); r.amp = fcfg_get('au_eq_amp', r.amp);
r.chw = fcfg_get('au_eq_chw', r.chw);
var gains = jread('au_eq_gain', r.gains); var gains = jread('au_eq_gain', r.gains);
if (r.gains.length == gains.length) if (r.gains.length == gains.length)
r.gains = gains; r.gains = gains;
@@ -1902,12 +1976,14 @@ var audio_eq = (function () {
r.draw = function () { r.draw = function () {
jwrite('au_eq_gain', r.gains); jwrite('au_eq_gain', r.gains);
swrite('au_eq_amp', r.amp); swrite('au_eq_amp', r.amp);
swrite('au_eq_chw', r.chw);
var txt = QSA('input.eq_gain'); var txt = QSA('input.eq_gain');
for (var a = 0; a < r.bands.length; a++) for (var a = 0; a < r.bands.length; a++)
txt[a].value = r.gains[a]; txt[a].value = r.gains[a];
QS('input.eq_gain[band="amp"]').value = r.amp; QS('input.eq_gain[band="amp"]').value = r.amp;
QS('input.eq_gain[band="chw"]').value = r.chw;
}; };
r.stop = function () { r.stop = function () {
@@ -1977,16 +2053,47 @@ var audio_eq = (function () {
for (var a = r.filters.length - 1; a >= 0; a--) for (var a = r.filters.length - 1; a >= 0; a--)
r.filters[a].connect(a > 0 ? r.filters[a - 1] : actx.destination); r.filters[a].connect(a > 0 ? r.filters[a - 1] : actx.destination);
if (Math.round(r.chw * 25) != 25) {
var split = actx.createChannelSplitter(2),
merge = actx.createChannelMerger(2),
lg1 = actx.createGain(),
lg2 = actx.createGain(),
rg1 = actx.createGain(),
rg2 = actx.createGain(),
vg1 = 1 - (1 - r.chw) / 2,
vg2 = 1 - vg1;
console.log('chw', vg1, vg2);
merge.connect(r.filters[r.filters.length - 1]);
lg1.gain.value = rg2.gain.value = vg1;
lg2.gain.value = rg1.gain.value = vg2;
lg1.connect(merge, 0, 0);
rg1.connect(merge, 0, 0);
lg2.connect(merge, 0, 1);
rg2.connect(merge, 0, 1);
split.connect(lg1, 0);
split.connect(lg2, 0);
split.connect(rg1, 1);
split.connect(rg2, 1);
r.filters.push(split);
mp.acs.channelCountMode = 'explicit';
}
mp.acs.connect(r.filters[r.filters.length - 1]); mp.acs.connect(r.filters[r.filters.length - 1]);
} }
function eq_step(e) { function eq_step(e) {
ev(e); ev(e);
var band = parseInt(this.getAttribute('band')), var sb = this.getAttribute('band'),
band = parseInt(sb),
step = parseFloat(this.getAttribute('step')); step = parseFloat(this.getAttribute('step'));
if (isNaN(band)) if (sb == 'amp')
r.amp = Math.round((r.amp + step * 0.2) * 100) / 100; r.amp = Math.round((r.amp + step * 0.2) * 100) / 100;
else if (sb == 'chw')
r.chw = Math.round((r.chw + step * 0.2) * 100) / 100;
else else
r.gains[band] += step; r.gains[band] += step;
@@ -1996,15 +2103,18 @@ var audio_eq = (function () {
function adj_band(that, step) { function adj_band(that, step) {
var err = false; var err = false;
try { try {
var band = parseInt(that.getAttribute('band')), var sb = that.getAttribute('band'),
band = parseInt(sb),
vs = that.value, vs = that.value,
v = parseFloat(vs); v = parseFloat(vs);
if (isNaN(v) || v + '' != vs) if (isNaN(v) || v + '' != vs)
throw new Error('inval band'); throw new Error('inval band');
if (isNaN(band)) if (sb == 'amp')
r.amp = Math.round((v + step * 0.2) * 100) / 100; r.amp = Math.round((v + step * 0.2) * 100) / 100;
else if (sb == 'chw')
r.chw = Math.round((v + step * 0.2) * 100) / 100;
else else
r.gains[band] = v + step; r.gains[band] = v + step;
@@ -2041,6 +2151,7 @@ var audio_eq = (function () {
vs.push([a, hz, r.gains[a]]); vs.push([a, hz, r.gains[a]]);
} }
vs.push(["amp", "boost", r.amp]); vs.push(["amp", "boost", r.amp]);
vs.push(["chw", "width", r.chw]);
for (var a = 0; a < vs.length; a++) { for (var a = 0; a < vs.length; a++) {
var b = vs[a][0]; var b = vs[a][0];
@@ -2163,6 +2274,12 @@ function play(tid, is_ev, seek) {
if (window.thegrid) if (window.thegrid)
thegrid.loadsel(); thegrid.loadsel();
try {
if (actx.state == 'suspended')
actx.resume();
}
catch (ex) { }
try { try {
mp.au.play(); mp.au.play();
if (mp.au.paused) if (mp.au.paused)
@@ -2292,9 +2409,13 @@ function scan_hash(v) {
ts = null; ts = null;
if (m.length > 3) { if (m.length > 3) {
m = /^&[Tt=0]*([0-9]+[Mm:])?0*([0-9]+)[Ss]?$/.exec(m[3]); var tm = /^&[Tt=0]*([0-9]+[Mm:])?0*([0-9]+)[Ss]?$/.exec(m[3]);
if (m) { if (tm) {
ts = parseInt(m[1] || 0) * 60 + parseInt(m[2] || 0); ts = parseInt(tm[1] || 0) * 60 + parseInt(tm[2] || 0);
}
tm = /^&[Tt=0]*([0-9\.]+)-([0-9\.]+)$/.exec(m[3]);
if (tm) {
ts = '' + tm[1] + '-' + tm[2];
} }
} }
@@ -2330,6 +2451,7 @@ function eval_hash() {
return; return;
clearInterval(t); clearInterval(t);
baguetteBox.urltime(ts);
var im = QS('#ggrid a[ref="' + id + '"]'); var im = QS('#ggrid a[ref="' + id + '"]');
im.click(); im.click();
im.scrollIntoView(); im.scrollIntoView();
@@ -2352,8 +2474,23 @@ function eval_hash() {
(function () { (function () {
var d = mknod('div'); for (var a = 0; a < 2; a++)
d.setAttribute('id', 'acc_info'); (function (a) {
var d = mknod('a');
d.setAttribute('href', '#');
d.setAttribute('class', 'ayjump');
d.innerHTML = a ? L.ay_path : L.ay_files;
document.body.insertBefore(d, ebi('ops'));
d.onclick = function (e) {
ev(e);
if (a)
QS(treectl.hidden ? '#path a:nth-last-child(2)' : '#treeul a.hl').focus();
else
QS(thegrid.en ? '#ggrid a' : '#files tbody tr[tabindex]').focus();
};
})(a);
var d = mknod('div', 'acc_info');
document.body.insertBefore(d, ebi('ops')); document.body.insertBefore(d, ebi('ops'));
})(); })();
@@ -2362,7 +2499,8 @@ function sortfiles(nodes) {
if (!nodes.length) if (!nodes.length)
return nodes; return nodes;
var sopts = jread('fsort', [["href", 1, ""]]); var sopts = jread('fsort', [["href", 1, ""]]),
dir1st = sread('dir1st') !== '0';
try { try {
var is_srch = false; var is_srch = false;
@@ -2393,14 +2531,10 @@ function sortfiles(nodes) {
if ((v + '').indexOf('<a ') === 0) if ((v + '').indexOf('<a ') === 0)
v = v.split('>')[1]; v = v.split('>')[1];
else if (name == "href" && v) { else if (name == "href" && v)
if (v.split('?')[0].slice(-1) == '/')
v = '\t' + v;
v = uricom_dec(v)[0]; v = uricom_dec(v)[0];
}
nodes[b]._sv = v; nodes[b]._sv = v
} }
} }
@@ -2429,6 +2563,13 @@ function sortfiles(nodes) {
if (is_srch) if (is_srch)
delete nodes[b].ext; delete nodes[b].ext;
} }
if (dir1st) {
var r1 = [], r2 = [];
for (var b = 0, bb = nodes.length; b < bb; b++)
(nodes[b].href.split('?')[0].slice(-1) == '/' ? r1 : r2).push(nodes[b]);
nodes = r1.concat(r2);
}
} }
catch (ex) { catch (ex) {
console.log("failed to apply sort config: " + ex); console.log("failed to apply sort config: " + ex);
@@ -2599,6 +2740,8 @@ var fileman = (function () {
if (!md.hasOwnProperty(k)) if (!md.hasOwnProperty(k))
continue; continue;
md[k] = (md[k] + '').replace(/[\/\\]/g, '-');
if (k.startsWith('.')) if (k.startsWith('.'))
md[k.slice(1)] = md[k]; md[k.slice(1)] = md[k];
} }
@@ -2616,8 +2759,7 @@ var fileman = (function () {
var rui = ebi('rui'); var rui = ebi('rui');
if (!rui) { if (!rui) {
rui = mknod('div'); rui = mknod('div', 'rui');
rui.setAttribute('id', 'rui');
document.body.appendChild(rui); document.body.appendChild(rui);
} }
@@ -3100,10 +3242,9 @@ var showfile = (function () {
return; return;
qsr('#prism_css'); qsr('#prism_css');
var el = mknod('link'); var el = mknod('link', 'prism_css');
el.rel = 'stylesheet'; el.rel = 'stylesheet';
el.href = '/.cpr/deps/prism' + (light ? '' : 'd') + '.css'; el.href = '/.cpr/deps/prism' + (light ? '' : 'd') + '.css';
el.setAttribute('id', 'prism_css');
document.head.appendChild(el); document.head.appendChild(el);
}; };
@@ -3217,8 +3358,7 @@ var showfile = (function () {
fun = function (el) { }; fun = function (el) { };
qsr('#doc'); qsr('#doc');
var el = mknod('pre'); var el = mknod('pre', 'doc');
el.setAttribute('id', 'doc');
el.setAttribute('tabindex', '0'); el.setAttribute('tabindex', '0');
clmod(ebi('wrap'), 'doc', !is_md); clmod(ebi('wrap'), 'doc', !is_md);
if (is_md) { if (is_md) {
@@ -3246,9 +3386,8 @@ var showfile = (function () {
hfun(get_evpath() + '?doc=' + url.split('/').pop()); hfun(get_evpath() + '?doc=' + url.split('/').pop());
qsr('#docname'); qsr('#docname');
el = mknod('span'); el = mknod('span', 'docname');
el.textContent = tname; el.textContent = tname;
el.setAttribute('id', 'docname');
ebi('path').appendChild(el); ebi('path').appendChild(el);
r.updtree(); r.updtree();
@@ -3266,9 +3405,10 @@ var showfile = (function () {
for (var a = 0; a < src.length; a++) { for (var a = 0; a < src.length; a++) {
var m = /^([0-9;]+)m/.exec(src[a]); var m = /^([0-9;]+)m/.exec(src[a]);
if (!m) { if (!m) {
if (a || src[a]) if (a)
out.push('\x1b[' + src[a]); out.push('\x1b[');
out.push(src[a]);
continue; continue;
} }
@@ -3382,9 +3522,8 @@ var showfile = (function () {
var thegrid = (function () { var thegrid = (function () {
var lfiles = ebi('files'), var lfiles = ebi('files'),
gfiles = mknod('div'); gfiles = mknod('div', 'gfiles');
gfiles.setAttribute('id', 'gfiles');
gfiles.style.display = 'none'; gfiles.style.display = 'none';
gfiles.innerHTML = ( gfiles.innerHTML = (
'<div id="ghead" class="ghead">' + '<div id="ghead" class="ghead">' +
@@ -3781,6 +3920,7 @@ function tree_neigh(n) {
treectl.dir_cb = tree_scrollto; treectl.dir_cb = tree_scrollto;
links[act].click(); links[act].click();
links[act].focus();
} }
@@ -3882,6 +4022,9 @@ document.onkeydown = function (e) {
} }
} }
if (k == 'Enter' && ae && (ae.onclick || ae.hasAttribute('tabIndex')))
return ev(e) && ae.click() || true;
if (aet && aet != 'a' && aet != 'tr' && aet != 'pre') if (aet && aet != 'a' && aet != 'tr' && aet != 'pre')
return; return;
@@ -3920,6 +4063,9 @@ document.onkeydown = function (e) {
if (n !== 0) if (n !== 0)
return seek_au_rel(n) || true; return seek_au_rel(n) || true;
if (k == 'KeyY')
return dl_song();
n = k == 'KeyI' ? -1 : k == 'KeyK' ? 1 : 0; n = k == 'KeyI' ? -1 : k == 'KeyK' ? 1 : 0;
if (n !== 0) if (n !== 0)
return tree_neigh(n); return tree_neigh(n);
@@ -4078,7 +4224,7 @@ document.onkeydown = function (e) {
clearTimeout(defer_timeout); clearTimeout(defer_timeout);
clearTimeout(search_timeout); clearTimeout(search_timeout);
search_timeout = setTimeout(do_search, search_timeout = setTimeout(do_search,
v && v.length < (is_touch ? 4 : 3) ? 1000 : 500); v && v.length < (MOBILE ? 4 : 3) ? 1000 : 500);
} }
} }
@@ -4317,6 +4463,9 @@ var treectl = (function () {
bcfg_bind(r, 'dots', 'dotfiles', false, function (v) { bcfg_bind(r, 'dots', 'dotfiles', false, function (v) {
r.goto(get_evpath()); r.goto(get_evpath());
}); });
bcfg_bind(r, 'dir1st', 'dir1st', true, function (v) {
treectl.gentab(get_evpath(), treectl.lsc);
});
setwrap(bcfg_bind(r, 'wtree', 'wraptree', true, setwrap)); setwrap(bcfg_bind(r, 'wtree', 'wraptree', true, setwrap));
setwrap(bcfg_bind(r, 'parpane', 'parpane', true, onscroll)); setwrap(bcfg_bind(r, 'parpane', 'parpane', true, onscroll));
bcfg_bind(r, 'htree', 'hovertree', false, reload_tree); bcfg_bind(r, 'htree', 'hovertree', false, reload_tree);
@@ -4513,9 +4662,9 @@ var treectl = (function () {
return ta[a]; return ta[a];
}; };
r.goto = function (url, push) { r.goto = function (url, push, back) {
get_tree("", url, true); get_tree("", url, true);
r.reqls(url, push, true); r.reqls(url, push, true, back);
}; };
function get_tree(top, dst, rst) { function get_tree(top, dst, rst) {
@@ -4589,7 +4738,7 @@ var treectl = (function () {
} }
function reload_tree() { function reload_tree() {
var cdir = get_vpath(), var cdir = r.nextdir || get_vpath(),
links = QSA('#treeul a+a'), links = QSA('#treeul a+a'),
nowrap = QS('#tree.nowrap') && QS('#hovertree.on'), nowrap = QS('#tree.nowrap') && QS('#hovertree.on'),
act = null; act = null;
@@ -4684,9 +4833,10 @@ var treectl = (function () {
thegrid.setvis(true); thegrid.setvis(true);
} }
r.reqls = function (url, hpush, no_tree) { r.reqls = function (url, hpush, no_tree, back) {
var xhr = new XHR(); var xhr = new XHR();
xhr.top = url; xhr.top = url;
xhr.back = back
xhr.hpush = hpush; xhr.hpush = hpush;
xhr.ts = Date.now(); xhr.ts = Date.now();
xhr.open('GET', xhr.top + '?ls' + (r.dots ? '&dots' : ''), true); xhr.open('GET', xhr.top + '?ls' + (r.dots ? '&dots' : ''), true);
@@ -4695,6 +4845,7 @@ var treectl = (function () {
if (hpush && !no_tree) if (hpush && !no_tree)
get_tree('.', xhr.top); get_tree('.', xhr.top);
r.nextdir = xhr.top;
enspin(thegrid.en ? '#gfiles' : '#files'); enspin(thegrid.en ? '#gfiles' : '#files');
} }
@@ -4717,6 +4868,7 @@ var treectl = (function () {
if (!xhrchk(this, L.fl_xe1, L.fl_xe2)) if (!xhrchk(this, L.fl_xe1, L.fl_xe2))
return; return;
r.nextdir = null;
var cur = ebi('files').getAttribute('ts'); var cur = ebi('files').getAttribute('ts');
if (cur && parseInt(cur) > this.ts) { if (cur && parseInt(cur) > this.ts) {
console.log("reject ls"); console.log("reject ls");
@@ -4752,6 +4904,12 @@ var treectl = (function () {
if (res.readme) if (res.readme)
show_readme(res.readme); show_readme(res.readme);
if (this.hpush && !this.back) {
var ofs = ebi('wrap').offsetTop;
if (document.documentElement.scrollTop > ofs)
document.documentElement.scrollTop = ofs;
}
wintitle(); wintitle();
var fun = r.ls_cb; var fun = r.ls_cb;
if (fun) { if (fun) {
@@ -4761,6 +4919,7 @@ var treectl = (function () {
} }
r.gentab = function (top, res) { r.gentab = function (top, res) {
r.lsc = res;
var nodes = res.dirs.concat(res.files), var nodes = res.dirs.concat(res.files),
html = mk_files_header(res.taglist), html = mk_files_header(res.taglist),
seen = {}; seen = {};
@@ -4773,7 +4932,6 @@ var treectl = (function () {
bhref = tn.href.split('?')[0], bhref = tn.href.split('?')[0],
fname = uricom_dec(bhref)[0], fname = uricom_dec(bhref)[0],
hname = esc(fname), hname = esc(fname),
sortv = (bhref.slice(-1) == '/' ? '\t' : '') + hname,
id = 'f-' + ('00000000' + crc32(fname)).slice(-8), id = 'f-' + ('00000000' + crc32(fname)).slice(-8),
lang = showfile.getlang(fname); lang = showfile.getlang(fname);
@@ -4788,8 +4946,8 @@ var treectl = (function () {
tn.lead = '<a href="?doc=' + tn.href + '" class="doc' + (lang ? ' bri' : '') + tn.lead = '<a href="?doc=' + tn.href + '" class="doc' + (lang ? ' bri' : '') +
'" hl="' + id + '" name="' + hname + '">-txt-</a>'; '" hl="' + id + '" name="' + hname + '">-txt-</a>';
var ln = ['<tr><td>' + tn.lead + '</td><td sortv="' + sortv + var ln = ['<tr><td>' + tn.lead + '</td><td><a href="' +
'"><a href="' + top + tn.href + '" id="' + id + '">' + hname + '</a>', tn.sz]; top + tn.href + '" id="' + id + '">' + hname + '</a>', tn.sz];
for (var b = 0; b < res.taglist.length; b++) { for (var b = 0; b < res.taglist.length; b++) {
var k = res.taglist[b], var k = res.taglist[b],
@@ -4927,7 +5085,7 @@ var treectl = (function () {
if (url.search.indexOf('doc=') + 1 && hbase == cbase) if (url.search.indexOf('doc=') + 1 && hbase == cbase)
return showfile.show(hbase + showfile.sname(url.search), true); return showfile.show(hbase + showfile.sname(url.search), true);
r.goto(url.pathname); r.goto(url.pathname, false, true);
}; };
hist_replace(get_evpath() + window.location.hash); hist_replace(get_evpath() + window.location.hash);
@@ -5088,21 +5246,6 @@ function mk_files_header(taglist) {
var filecols = (function () { var filecols = (function () {
var hidden = jread('filecols', []); var hidden = jread('filecols', []);
if (JSON.stringify(def_hcols) != sread('hfilecols')) {
console.log("applying default hidden-cols");
jwrite('hfilecols', def_hcols);
for (var a = 0; a < def_hcols.length; a++) {
var t = def_hcols[a];
t = t.slice(0, 1).toUpperCase() + t.slice(1);
if (t.startsWith("."))
t = t.slice(1);
if (hidden.indexOf(t) == -1)
hidden.push(t);
}
jwrite("filecols", hidden);
}
var add_btns = function () { var add_btns = function () {
var ths = QSA('#files th>span'); var ths = QSA('#files th>span');
for (var a = 0, aa = ths.length; a < aa; a++) { for (var a = 0, aa = ths.length; a < aa; a++) {
@@ -5179,7 +5322,6 @@ var filecols = (function () {
tt.att(QS('#files thead')); tt.att(QS('#files thead'));
} }
}; };
set_style();
var toggle = function (name) { var toggle = function (name) {
var ofs = hidden.indexOf(name); var ofs = hidden.indexOf(name);
@@ -5199,6 +5341,31 @@ var filecols = (function () {
set_style(); set_style();
}; };
ebi('hcolsr').onclick = function (e) {
ev(e);
reset(true);
};
function reset(force) {
if (force || JSON.stringify(def_hcols) != sread('hfilecols')) {
console.log("applying default hidden-cols");
hidden = [];
jwrite('hfilecols', def_hcols);
for (var a = 0; a < def_hcols.length; a++) {
var t = def_hcols[a];
t = t.slice(0, 1).toUpperCase() + t.slice(1);
if (t.startsWith("."))
t = t.slice(1);
if (hidden.indexOf(t) == -1)
hidden.push(t);
}
jwrite("filecols", hidden);
}
set_style();
}
reset();
try { try {
var ci = find_file_col('dur'), var ci = find_file_col('dur'),
i = ci[0], i = ci[0],
@@ -5216,6 +5383,7 @@ var filecols = (function () {
"add_btns": add_btns, "add_btns": add_btns,
"set_style": set_style, "set_style": set_style,
"toggle": toggle, "toggle": toggle,
"reset": reset
}; };
})(); })();
@@ -5330,7 +5498,7 @@ var mukey = (function () {
})(); })();
var light, theme; var light, theme, themen;
var settheme = (function () { var settheme = (function () {
var ax = 'abcdefghijklmnopqrstuvwx'; var ax = 'abcdefghijklmnopqrstuvwx';
@@ -5338,6 +5506,7 @@ var settheme = (function () {
if (!/^[a-x][yz]/.exec(theme)) if (!/^[a-x][yz]/.exec(theme))
theme = dtheme; theme = dtheme;
themen = theme.split(/ /)[0];
light = !!(theme.indexOf('y') + 1); light = !!(theme.indexOf('y') + 1);
function freshen() { function freshen() {
@@ -5351,7 +5520,7 @@ var settheme = (function () {
showfile.setstyle(); showfile.setstyle();
var html = [], itheme = ax.indexOf(theme[0]) * 2 + (light ? 1 : 0), var html = [], itheme = ax.indexOf(theme[0]) * 2 + (light ? 1 : 0),
names = ['classic dark', 'classic light', 'pm-monokai', 'flat light', 'vice', 'hotdog stand']; names = ['classic dark', 'classic light', 'pm-monokai', 'flat light', 'vice', 'hotdog stand', 'hacker', 'hi-con'];
for (var a = 0; a < themes; a++) for (var a = 0; a < themes; a++)
html.push('<a href="#" class="btn tgl' + (a == itheme ? ' on' : '') + html.push('<a href="#" class="btn tgl' + (a == itheme ? ' on' : '') +
@@ -5373,6 +5542,7 @@ var settheme = (function () {
var c = ax[Math.floor(i / 2)], var c = ax[Math.floor(i / 2)],
l = light ? 'y' : 'z'; l = light ? 'y' : 'z';
theme = c + l + ' ' + c + ' ' + l; theme = c + l + ' ' + c + ' ' + l;
themen = c + l;
swrite('theme', theme); swrite('theme', theme);
freshen(); freshen();
} }
@@ -5402,7 +5572,7 @@ var settheme = (function () {
L = Ls[this.textContent]; L = Ls[this.textContent];
swrite("lang", this.textContent); swrite("lang", this.textContent);
freshen(); freshen();
modal.confirm(L.lang_set, location.reload.bind(location), null); modal.confirm(Ls.eng.lang_set + "\n\n" + Ls.nor.lang_set, location.reload.bind(location), null);
}; };
freshen(); freshen();
@@ -5727,13 +5897,34 @@ function show_md(md, name, div, url, depth) {
}); });
} }
md_plug = {}
md = load_md_plug(md, 'pre');
md = load_md_plug(md, 'post');
var marked_opts = {
headerPrefix: 'md-',
breaks: true,
gfm: true
};
var ext = md_plug.pre;
if (ext)
Object.assign(marked_opts, ext[0]);
try { try {
clmod(div, 'mdo', 1); clmod(div, 'mdo', 1);
div.innerHTML = marked.parse(md, { div.innerHTML = marked.parse(md, marked_opts);
headerPrefix: 'md-',
breaks: true, ext = md_plug.post;
gfm: true ext = ext ? [ext[0].render, ext[0].render2] : [];
}); for (var a = 0; a < ext.length; a++)
if (ext[a])
try {
ext[a](div);
}
catch (ex) {
console.log(ex);
}
var els = QSA('#epi a'); var els = QSA('#epi a');
for (var a = 0, aa = els.length; a < aa; a++) { for (var a = 0, aa = els.length; a < aa; a++) {
var href = els[a].getAttribute('href'); var href = els[a].getAttribute('href');
@@ -5823,7 +6014,7 @@ var unpost = (function () {
html.push("<table><thead><tr><td></td><td>time</td><td>size</td><td>file</td></tr></thead><tbody>"); html.push("<table><thead><tr><td></td><td>time</td><td>size</td><td>file</td></tr></thead><tbody>");
} }
else else
html.push(filt.value ? L.un_no2 : L.un_no1); html.push('-- <em>' + (filt.value ? L.un_no2 : L.un_no1) + '</em>');
var mods = [1000, 100, 10]; var mods = [1000, 100, 10];
for (var a = 0; a < res.length; a++) { for (var a = 0; a < res.length; a++) {
@@ -5947,6 +6138,9 @@ function wintitle(txt) {
ebi('path').onclick = function (e) { ebi('path').onclick = function (e) {
if (ctrl(e))
return true;
var a = e.target.closest('a[href]'); var a = e.target.closest('a[href]');
if (!a || !(a = a.getAttribute('href') + '') || !a.endsWith('/')) if (!a || !(a = a.getAttribute('href') + '') || !a.endsWith('/'))
return; return;
@@ -5974,9 +6168,11 @@ ebi('files').onclick = ebi('docul').onclick = function (e) {
tgt = e.target.closest('a[hl]'); tgt = e.target.closest('a[hl]');
if (tgt) { if (tgt) {
var fun = function () { var a = ebi(tgt.getAttribute('hl')),
showfile.show(noq_href(ebi(tgt.getAttribute('hl'))), tgt.getAttribute('lang')); fun = function () {
}, szs = ft2dict(tgt.closest('tr'))[0].sz, showfile.show(noq_href(a), tgt.getAttribute('lang'));
},
szs = ft2dict(a.closest('tr'))[0].sz,
sz = parseInt(szs.replace(/[, ]/g, '')); sz = parseInt(szs.replace(/[, ]/g, ''));
if (sz < 1024 * 1024) if (sz < 1024 * 1024)

27
copyparty/web/cf.html Normal file
View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ svcname }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.8">
</head>
<body>
<div id="box" style="opacity: 0; font-family: sans-serif">
<h3>please press F5 to reload the page</h3>
<p>sorry for the inconvenience</p>
</div>
<script>
setTimeout(function() {
document.getElementById('box').style.opacity = 1;
}, 500);
parent.toast.ok(30, parent.L.cf_ok);
parent.qsr('#cf_frame');
</script>
</body>
</html>

View File

@@ -13,8 +13,7 @@ audio_eq.apply = function () {
var can = ebi('fft_can'); var can = ebi('fft_can');
if (!can) { if (!can) {
can = mknod('canvas'); can = mknod('canvas', 'fft_can');
can.setAttribute('id', 'fft_can');
can.style.cssText = 'position:absolute;left:0;bottom:5em;width:' + w + 'px;height:' + h + 'px;z-index:9001'; can.style.cssText = 'position:absolute;left:0;bottom:5em;width:' + w + 'px;height:' + h + 'px;z-index:9001';
document.body.appendChild(can); document.body.appendChild(can);
can.width = w; can.width = w;

View File

@@ -1,6 +1,6 @@
<!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">
{{ html_head }} {{ html_head }}
@@ -127,10 +127,12 @@ write markdown (most html is 🙆 too)
<script> <script>
var last_modified = {{ lastmod }}; var last_modified = {{ lastmod }},
have_emp = {{ have_emp|tojson }},
dfavico = "{{ favico }}";
var md_opt = { var md_opt = {
link_md_as_html: false, link_md_as_html: false,
allow_plugins: {{ md_plug }},
modpoll_freq: {{ md_chk_rate }} modpoll_freq: {{ md_chk_rate }}
}; };

View File

@@ -20,10 +20,6 @@ var dbg = function () { };
// dbg = console.log // dbg = console.log
// plugins
var md_plug = {};
// dodge browser issues // dodge browser issues
(function () { (function () {
var ua = navigator.userAgent; var ua = navigator.userAgent;
@@ -160,7 +156,7 @@ function copydom(src, dst, lv) {
} }
function md_plug_err(ex, js) { md_plug_err = function (ex, js) {
qsr('#md_errbox'); qsr('#md_errbox');
if (!ex) if (!ex)
return; return;
@@ -177,8 +173,7 @@ function md_plug_err(ex, js) {
o.textContent = lns[ln - 1]; o.textContent = lns[ln - 1];
} }
} }
var errbox = mknod('div'); var errbox = mknod('div', 'md_errbox');
errbox.setAttribute('id', 'md_errbox');
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 () {
@@ -197,50 +192,12 @@ function md_plug_err(ex, js) {
} }
function load_plug(md_text, plug_type) {
if (!md_opt.allow_plugins)
return md_text;
var find = '\n```copyparty_' + plug_type + '\n';
var ofs = md_text.indexOf(find);
if (ofs === -1)
return md_text;
var ofs2 = md_text.indexOf('\n```', ofs + 1);
if (ofs2 == -1)
return md_text;
var js = md_text.slice(ofs + find.length, ofs2 + 1);
var md = md_text.slice(0, ofs + 1) + md_text.slice(ofs2 + 4);
var old_plug = md_plug[plug_type];
if (!old_plug || old_plug[1] != js) {
js = 'const x = { ' + js + ' }; x;';
try {
var x = eval(js);
}
catch (ex) {
md_plug[plug_type] = null;
md_plug_err(ex, js);
return md;
}
if (x['ctor']) {
x['ctor']();
delete x['ctor'];
}
md_plug[plug_type] = [x, js];
}
return md;
}
function convert_markdown(md_text, dest_dom) { function convert_markdown(md_text, dest_dom) {
md_text = md_text.replace(/\r/g, ''); md_text = md_text.replace(/\r/g, '');
md_plug_err(null); md_plug_err(null);
md_text = load_plug(md_text, 'pre'); md_text = load_md_plug(md_text, 'pre');
md_text = load_plug(md_text, 'post'); md_text = load_md_plug(md_text, 'post');
var marked_opts = { var marked_opts = {
//headerPrefix: 'h-', //headerPrefix: 'h-',
@@ -248,7 +205,7 @@ function convert_markdown(md_text, dest_dom) {
gfm: true gfm: true
}; };
var ext = md_plug['pre']; var ext = md_plug.pre;
if (ext) if (ext)
Object.assign(marked_opts, ext[0]); Object.assign(marked_opts, ext[0]);
@@ -349,7 +306,7 @@ function convert_markdown(md_text, dest_dom) {
el.innerHTML = '<a href="#' + id + '">' + el.innerHTML + '</a>'; el.innerHTML = '<a href="#' + id + '">' + el.innerHTML + '</a>';
} }
ext = md_plug['post']; ext = md_plug.post;
if (ext && ext[0].render) if (ext && ext[0].render)
try { try {
ext[0].render(md_dom); ext[0].render(md_dom);

View File

@@ -36,6 +36,11 @@
width: 55em; width: 55em;
width: min(55em, calc(100% - 2em)); width: min(55em, calc(100% - 2em));
} }
#mtw.single.editor,
#mw.single.editor {
width: calc(100% - 1em);
left: .5em;
}
#mp { #mp {

View File

@@ -16,8 +16,7 @@ var dom_sbs = ebi('sbs');
var dom_nsbs = ebi('nsbs'); var dom_nsbs = ebi('nsbs');
var dom_tbox = ebi('toolsbox'); var dom_tbox = ebi('toolsbox');
var dom_ref = (function () { var dom_ref = (function () {
var d = mknod('div'); var d = mknod('div', 'mtr');
d.setAttribute('id', 'mtr');
dom_swrap.appendChild(d); dom_swrap.appendChild(d);
d = ebi('mtr'); d = ebi('mtr');
// hide behind the textarea (offsetTop is not computed if display:none) // hide behind the textarea (offsetTop is not computed if display:none)
@@ -509,6 +508,20 @@ function setsel(s) {
} }
// cut/copy current line
function md_cut(cut) {
var s = linebounds();
if (s.car != s.cdr)
return;
dom_src.setSelectionRange(s.n1, s.n2 + 1, 'forward');
setTimeout(function () {
var i = cut ? s.n1 : s.car;
dom_src.setSelectionRange(i, i, 'forward');
}, 1);
}
// indent/dedent // indent/dedent
function md_indent(dedent) { function md_indent(dedent) {
var s = getsel(), var s = getsel(),
@@ -955,6 +968,10 @@ var set_lno = (function () {
md_p_jump(dn); md_p_jump(dn);
return false; return false;
} }
if (ev.code == "KeyX" || ev.code == "KeyC") {
md_cut(ev.code == "KeyX");
return true; //sic
}
} }
else { else {
if (ev.code == "Tab" || kc == 9) { if (ev.code == "Tab" || kc == 9) {

View File

@@ -1,6 +1,6 @@
<!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">
{{ html_head }} {{ html_head }}
@@ -25,10 +25,12 @@
<a href="#" id="repl">π</a> <a href="#" id="repl">π</a>
<script> <script>
var last_modified = {{ lastmod }}; var last_modified = {{ lastmod }},
have_emp = {{ have_emp|tojson }},
dfavico = "{{ favico }}";
var md_opt = { var md_opt = {
link_md_as_html: false, link_md_as_html: false,
allow_plugins: {{ md_plug }},
modpoll_freq: {{ md_chk_rate }} modpoll_freq: {{ md_chk_rate }}
}; };

View File

@@ -36,6 +36,7 @@
<tr><td>hash-q</td><td>{{ hashq }}</td></tr> <tr><td>hash-q</td><td>{{ hashq }}</td></tr>
<tr><td>tag-q</td><td>{{ tagq }}</td></tr> <tr><td>tag-q</td><td>{{ tagq }}</td></tr>
<tr><td>mtp-q</td><td>{{ mtpq }}</td></tr> <tr><td>mtp-q</td><td>{{ mtpq }}</td></tr>
<tr><td>db-act</td><td id="u">{{ dbwt }}</td></tr>
</table> </table>
</td><td> </td><td>
<table class="vols"> <table class="vols">
@@ -50,8 +51,8 @@
</table> </table>
</td></tr></table> </td></tr></table>
<div class="btns"> <div class="btns">
<a id="d" href="/?stack" tt="shows the state of all active threads">dump stack</a> <a id="d" href="/?stack">dump stack</a>
<a id="e" href="/?reload=cfg" tt="reload config files (accounts/volumes/volflags),$Nand rescan all e2ds volumes">reload cfg</a> <a id="e" href="/?reload=cfg">reload cfg</a>
</div> </div>
{%- endif %} {%- endif %}
@@ -97,7 +98,9 @@
<a href="#" id="repl">π</a> <a href="#" id="repl">π</a>
<script> <script>
var lang="{{ this.args.lang }}"; var lang="{{ lang }}",
dfavico="{{ favico }}";
document.documentElement.className=localStorage.theme||"{{ this.args.theme }}"; document.documentElement.className=localStorage.theme||"{{ this.args.theme }}";
</script> </script>

View File

@@ -23,6 +23,12 @@ var Ls = {
"r1": "gå hjem", "r1": "gå hjem",
".s1": "kartlegg", ".s1": "kartlegg",
"t1": "handling", "t1": "handling",
"u2": "tid siden noen sist skrev til serveren$N( opplastning / navneendring / ... )$N$N17d = 17 dager$N1h23 = 1 time 23 minutter$N4m56 = 4 minuter 56 sekunder",
},
"eng": {
"d2": "shows the state of all active threads",
"e2": "reload config files (accounts/volumes/volflags),$Nand rescan all e2ds volumes",
"u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds",
} }
}, },
d = Ls[sread("lang") || lang]; d = Ls[sread("lang") || lang];
@@ -40,5 +46,10 @@ for (var k in (d || {})) {
} }
tt.init(); tt.init();
if (!ebi('c')) var o = QS('input[name="cppwd"]');
QS('input[name="cppwd"]').focus(); if (!ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)
o.focus();
o = ebi('u');
if (o && /[0-9]+$/.exec(o.innerHTML))
o.innerHTML = shumantime(o.innerHTML);

View File

@@ -190,6 +190,18 @@ html.y #tth {
color: #000; color: #000;
background: #fff; background: #fff;
} }
#cf_frame {
position: fixed;
z-index: 573;
top: 3em;
left: 50%;
width: 40em;
height: 30em;
margin-left: -20.2em;
border-radius: .4em;
border: .4em solid var(--fg);
box-shadow: 0 2em 4em 1em var(--bg-max);
}
#modal { #modal {
position: fixed; position: fixed;
overflow: auto; overflow: auto;
@@ -239,6 +251,9 @@ html.y #tth {
padding: .3em; padding: .3em;
text-align: center; text-align: center;
} }
#modalc a {
color: #07b;
}
#modalb { #modalb {
position: sticky; position: sticky;
text-align: right; text-align: right;
@@ -281,15 +296,19 @@ html.y #tth {
max-width: 24em; max-width: 24em;
} }
*:focus, *:focus,
*:focus+label,
#pctl *:focus, #pctl *:focus,
.btn:focus { .btn:focus {
box-shadow: 0 .1em .2em #fc0 inset; box-shadow: 0 .1em .2em #fc0 inset;
outline: #fc0 solid .1em;
border-radius: .2em; border-radius: .2em;
} }
html.y *:focus, html.y *:focus,
html.y *:focus+label,
html.y #pctl *:focus, html.y #pctl *:focus,
html.y .btn:focus { html.y .btn:focus {
box-shadow: 0 .1em .2em #037 inset; box-shadow: 0 .1em .2em #037 inset;
outline: #037 solid .1em;
} }
input[type="text"]:focus, input[type="text"]:focus,
input:not([type]):focus, input:not([type]):focus,
@@ -376,11 +395,13 @@ html.y textarea:focus {
padding-left: 2em; padding-left: 2em;
border-left: .3em solid #ddd; border-left: .3em solid #ddd;
} }
.mdo ul>li, .mdo ul>li {
.mdo ol>li {
margin: .7em 0; margin: .7em 0;
list-style-type: disc; list-style-type: disc;
} }
.mdo ol>li {
margin: .7em 0 .7em 2em;
}
.mdo strong { .mdo strong {
color: #000; color: #000;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -6,19 +6,44 @@ if (!window['console'])
}; };
var is_touch = 'ontouchstart' in window, var wah = '',
is_https = (window.location + '').indexOf('https:') === 0, HALFMAX = 8192 * 8192 * 8192 * 8192,
IPHONE = is_touch && /iPhone|iPad|iPod/i.test(navigator.userAgent), HTTPS = (window.location + '').indexOf('https:') === 0,
TOUCH = 'ontouchstart' in window,
MOBILE = TOUCH,
CHROME = !!window.chrome,
IPHONE = TOUCH && /iPhone|iPad|iPod/i.test(navigator.userAgent),
WINDOWS = navigator.platform ? navigator.platform == 'Win32' : /Windows/.test(navigator.userAgent); WINDOWS = navigator.platform ? navigator.platform == 'Win32' : /Windows/.test(navigator.userAgent);
try {
if (navigator.userAgentData.mobile)
MOBILE = true;
if (navigator.userAgentData.platform == 'Windows')
WINDOWS = true;
if (navigator.userAgentData.brands.some(function (d) { return d.brand == 'Chromium' }))
CHROME = true;
}
catch (ex) { }
var ebi = document.getElementById.bind(document), var ebi = document.getElementById.bind(document),
QS = document.querySelector.bind(document), QS = document.querySelector.bind(document),
QSA = document.querySelectorAll.bind(document), QSA = document.querySelectorAll.bind(document),
mknod = document.createElement.bind(document),
XHR = XMLHttpRequest; XHR = XMLHttpRequest;
function mknod(et, eid) {
var ret = document.createElement(et);
if (eid)
ret.id = eid;
return ret;
}
function qsr(sel) { function qsr(sel) {
var el = QS(sel); var el = QS(sel);
if (el) if (el)
@@ -85,15 +110,18 @@ catch (ex) {
} }
var crashed = false, ignexd = {}; var crashed = false, ignexd = {};
function vis_exh(msg, url, lineNo, columnNo, error) { function vis_exh(msg, url, lineNo, columnNo, error) {
if ((msg + '').indexOf('ResizeObserver') !== -1) if ((msg + '').indexOf('ResizeObserver') + 1)
return; // chrome issue 809574 (benign, from <video>) return; // chrome issue 809574 (benign, from <video>)
if ((msg + '').indexOf('l2d.js') !== -1) if ((msg + '').indexOf('l2d.js') + 1)
return; // `t` undefined in tapEvent -> hitTestSimpleCustom return; // `t` undefined in tapEvent -> hitTestSimpleCustom
if (!/\.js($|\?)/.exec('' + url)) if (!/\.js($|\?)/.exec('' + url))
return; // chrome debugger return; // chrome debugger
if ((url + '').indexOf(' > eval') + 1)
return; // md timer
var ekey = url + '\n' + lineNo + '\n' + msg; var ekey = url + '\n' + lineNo + '\n' + msg;
if (ignexd[ekey] || crashed) if (ignexd[ekey] || crashed)
return; return;
@@ -156,8 +184,7 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
try { try {
var exbox = ebi('exbox'); var exbox = ebi('exbox');
if (!exbox) { if (!exbox) {
exbox = mknod('div'); exbox = mknod('div', 'exbox');
exbox.setAttribute('id', 'exbox');
document.body.appendChild(exbox); document.body.appendChild(exbox);
var s = mknod('style'); var s = mknod('style');
@@ -218,6 +245,11 @@ function ev(e) {
} }
function noope(e) {
ev(e);
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
if (!String.prototype.endsWith) if (!String.prototype.endsWith)
String.prototype.endsWith = function (search, this_len) { String.prototype.endsWith = function (search, this_len) {
@@ -443,6 +475,16 @@ function sortTable(table, col, cb) {
} }
return reverse * (a.localeCompare(b)); return reverse * (a.localeCompare(b));
}); });
if (sread('dir1st') !== '0') {
var r1 = [], r2 = [];
for (var i = 0; i < tr.length; i++) {
var cell = tr[vl[i][1]].cells[1],
href = cell.getAttribute('sortv') || cell.textContent.trim();
(href.split('?')[0].slice(-1) == '/' ? r1 : r2).push(vl[i]);
}
vl = r1.concat(r2);
}
for (i = 0; i < tr.length; ++i) tb.appendChild(tr[vl[i][1]]); for (i = 0; i < tr.length; ++i) tb.appendChild(tr[vl[i][1]]);
if (cb) cb(); if (cb) cb();
} }
@@ -626,7 +668,7 @@ function humansize(b, terse) {
function humantime(v) { function humantime(v) {
if (v >= 60 * 60 * 24) if (v >= 60 * 60 * 24)
return v; return shumantime(v);
try { try {
return /.*(..:..:..).*/.exec(new Date(v * 1000).toUTCString())[1]; return /.*(..:..:..).*/.exec(new Date(v * 1000).toUTCString())[1];
@@ -637,12 +679,39 @@ function humantime(v) {
} }
function shumantime(v) {
if (v < 10)
return f2f(v, 2) + 's';
if (v < 60)
return f2f(v, 1) + 's';
v = parseInt(v);
var st = [[60 * 60 * 24, 60 * 60, 'd'], [60 * 60, 60, 'h'], [60, 1, 'm']];
for (var a = 0; a < st.length; a++) {
var m1 = st[a][0],
m2 = st[a][1],
ch = st[a][2];
if (v < m1)
continue;
var v1 = parseInt(v / m1),
v2 = ('0' + parseInt((v % m1) / m2)).slice(-2);
return v1 + ch + (v1 >= 10 ? '' : v2);
}
}
function clamp(v, a, b) { function clamp(v, a, b) {
return Math.min(Math.max(v, a), b); return Math.min(Math.max(v, a), b);
} }
function has(haystack, needle) { function has(haystack, needle) {
try { return haystack.includes(needle); } catch (ex) { }
for (var a = 0; a < haystack.length; a++) for (var a = 0; a < haystack.length; a++)
if (haystack[a] == needle) if (haystack[a] == needle)
return true; return true;
@@ -815,6 +884,14 @@ function sethash(hv) {
} }
} }
function dl_file(url) {
console.log('DL [%s]', url);
var o = mknod('a');
o.setAttribute('href', url);
o.setAttribute('download', '');
o.click();
}
var timer = (function () { var timer = (function () {
var r = {}; var r = {};
@@ -854,8 +931,8 @@ var timer = (function () {
var tt = (function () { var tt = (function () {
var r = { var r = {
"tt": mknod("div"), "tt": mknod("div", 'tt'),
"th": mknod("div"), "th": mknod("div", 'tth'),
"en": true, "en": true,
"el": null, "el": null,
"skip": false, "skip": false,
@@ -863,8 +940,6 @@ var tt = (function () {
}; };
r.th.innerHTML = '?'; r.th.innerHTML = '?';
r.tt.setAttribute('id', 'tt');
r.th.setAttribute('id', 'tth');
document.body.appendChild(r.tt); document.body.appendChild(r.tt);
document.body.appendChild(r.th); document.body.appendChild(r.th);
@@ -886,7 +961,7 @@ var tt = (function () {
return r.show.bind(this)(); return r.show.bind(this)();
tev = setTimeout(r.show.bind(this), 800); tev = setTimeout(r.show.bind(this), 800);
if (is_touch) if (TOUCH)
return; return;
this.addEventListener('mousemove', r.move); this.addEventListener('mousemove', r.move);
@@ -1033,9 +1108,8 @@ var toast = (function () {
var r = {}, var r = {},
te = null, te = null,
scrolling = false, scrolling = false,
obj = mknod('div'); obj = mknod('div', 'toast');
obj.setAttribute('id', 'toast');
document.body.appendChild(obj); document.body.appendChild(obj);
r.visible = false; r.visible = false;
r.txt = null; r.txt = null;
@@ -1118,8 +1192,7 @@ var modal = (function () {
r.busy = false; r.busy = false;
r.show = function (html) { r.show = function (html) {
o = mknod('div'); o = mknod('div', 'modal');
o.setAttribute('id', 'modal');
o.innerHTML = '<table><tr><td><div id="modalc">' + html + '</div></td></tr></table>'; o.innerHTML = '<table><tr><td><div id="modalc">' + html + '</div></td></tr></table>';
document.body.appendChild(o); document.body.appendChild(o);
document.addEventListener('keydown', onkey); document.addEventListener('keydown', onkey);
@@ -1174,7 +1247,8 @@ var modal = (function () {
return; return;
setTimeout(function () { setTimeout(function () {
ebi('modal-ok').focus(); if (ctr = ebi('modal-ok'))
ctr.focus();
}, 20); }, 20);
ev(e); ev(e);
} }
@@ -1347,6 +1421,49 @@ if (ebi('repl'))
ebi('repl').onclick = repl; ebi('repl').onclick = repl;
var md_plug = {};
var md_plug_err = function (ex, js) {
if (ex)
console.log(ex, js);
};
function load_md_plug(md_text, plug_type) {
if (!have_emp)
return md_text;
var find = '\n```copyparty_' + plug_type + '\n';
var ofs = md_text.indexOf(find);
if (ofs === -1)
return md_text;
var ofs2 = md_text.indexOf('\n```', ofs + 1);
if (ofs2 == -1)
return md_text;
var js = md_text.slice(ofs + find.length, ofs2 + 1);
var md = md_text.slice(0, ofs + 1) + md_text.slice(ofs2 + 4);
var old_plug = md_plug[plug_type];
if (!old_plug || old_plug[1] != js) {
js = 'const x = { ' + js + ' }; x;';
try {
var x = eval(js);
if (x['ctor']) {
x['ctor']();
delete x['ctor'];
}
}
catch (ex) {
md_plug[plug_type] = null;
md_plug_err(ex, js);
return md;
}
md_plug[plug_type] = [x, js];
}
return md;
}
var svg_decl = '<?xml version="1.0" encoding="UTF-8"?>\n'; var svg_decl = '<?xml version="1.0" encoding="UTF-8"?>\n';
@@ -1372,12 +1489,24 @@ var favico = (function () {
var b64; var b64;
try { try {
b64 = btoa(svg ? svg_decl + svg : gx(r.txt)); b64 = btoa(svg ? svg_decl + svg : gx(r.txt));
//console.log('f1');
} }
catch (ex) { catch (e1) {
b64 = encodeURIComponent(r.txt).replace(/%([0-9A-F]{2})/g, try {
function x(m, v) { return String.fromCharCode('0x' + v); }); b64 = btoa(gx(encodeURIComponent(r.txt).replace(/%([0-9A-F]{2})/g,
function x(m, v) { return String.fromCharCode('0x' + v); })));
b64 = btoa(gx(unescape(encodeURIComponent(r.txt)))); //console.log('f2');
}
catch (e2) {
try {
b64 = btoa(gx(unescape(encodeURIComponent(r.txt))));
//console.log('f3');
}
catch (e3) {
//console.log('fe');
return;
}
}
} }
if (!r.tag) { if (!r.tag) {
@@ -1390,9 +1519,13 @@ var favico = (function () {
r.init = function () { r.init = function () {
clearTimeout(r.to); clearTimeout(r.to);
scfg_bind(r, 'txt', 'icot', '', r.upd); var dv = (window.dfavico || '').trim().split(/ +/),
scfg_bind(r, 'fg', 'icof', 'fc5', r.upd); fg = dv.length < 2 ? 'fc5' : dv[1].toLowerCase() == 'none' ? '' : dv[1],
scfg_bind(r, 'bg', 'icob', '222', r.upd); bg = dv.length < 3 ? '222' : dv[2].toLowerCase() == 'none' ? '' : dv[2];
scfg_bind(r, 'txt', 'icot', dv[0], r.upd);
scfg_bind(r, 'fg', 'icof', fg, r.upd);
scfg_bind(r, 'bg', 'icob', bg, r.upd);
r.upd(); r.upd();
}; };
@@ -1401,6 +1534,7 @@ var favico = (function () {
})(); })();
var cf_cha_t = 0;
function xhrchk(xhr, prefix, e404) { function xhrchk(xhr, prefix, e404) {
if (xhr.status < 400 && xhr.status >= 200) if (xhr.status < 400 && xhr.status >= 200)
return true; return true;
@@ -1411,6 +1545,23 @@ function xhrchk(xhr, prefix, e404) {
if (xhr.status == 404) if (xhr.status == 404)
return toast.err(0, prefix + e404); return toast.err(0, prefix + e404);
return toast.err(0, prefix + xhr.status + ": " + ( var errtxt = (xhr.response && xhr.response.err) || xhr.responseText,
(xhr.response && xhr.response.err) || xhr.responseText)); fun = toast.err;
if (xhr.status == 503 && /[Cc]loud[f]lare|>Just a mo[m]ent|#cf-b[u]bbles|Chec[k]ing your br[o]wser/.test(errtxt)) {
var now = Date.now(), td = now - cf_cha_t;
if (td < 15000)
return;
cf_cha_t = now;
errtxt = 'Clou' + wah + 'dflare protection kicked in\n\n<strong>trying to fix it...</strong>';
fun = toast.warn;
qsr('#cf_frame');
var fr = mknod('iframe', 'cf_frame');
fr.src = '/?cf_challenge';
document.body.appendChild(fr);
}
return fun(0, prefix + xhr.status + ": " + errtxt);
} }

106
copyparty/web/w.hash.js Normal file
View File

@@ -0,0 +1,106 @@
"use strict";
function hex2u8(txt) {
return new Uint8Array(txt.match(/.{2}/g).map(function (b) { return parseInt(b, 16); }));
}
var subtle = null;
try {
subtle = crypto.subtle;
subtle.digest('SHA-512', new Uint8Array(1)).then(
function (x) { },
function (x) { load_fb(); }
);
}
catch (ex) {
load_fb();
}
function load_fb() {
subtle = null;
importScripts('/.cpr/deps/sha512.hw.js');
}
var reader = null,
gc1, gc2, gc3,
busy = false;
onmessage = (d) => {
if (busy)
return postMessage(["panic", 'worker got another task while busy']);
if (!reader)
reader = new FileReader();
var [nchunk, fobj, car, cdr] = d.data,
t0 = Date.now();
reader.onload = function (e) {
try {
// chrome gc forgets the filereader output; remind it
// (for some chromes, also necessary for subtle)
gc1 = e.target.result;
gc2 = new Uint8Array(gc1, 0, 1);
gc3 = new Uint8Array(gc1, gc1.byteLength - 1);
//console.log('[ w] %d HASH bgin', nchunk);
postMessage(["read", nchunk, cdr - car, Date.now() - t0]);
hash_calc(gc1);
}
catch (ex) {
postMessage(["panic", ex + '']);
}
};
reader.onerror = function () {
busy = false;
var err = reader.error + '';
if (err.indexOf('NotReadableError') !== -1 || // win10-chrome defender
err.indexOf('NotFoundError') !== -1 // macos-firefox permissions
)
return postMessage(["fail", 'OS-error', err + ' @ ' + car]);
postMessage(["ferr", err]);
};
//console.log('[ w] %d read bgin', nchunk);
busy = true;
reader.readAsArrayBuffer(
File.prototype.slice.call(fobj, car, cdr));
var hash_calc = function (buf) {
var hash_done = function (hashbuf) {
// stop gc from attempting to free early
if (!gc1 || !gc2 || !gc3)
return console.log('torch went out');
gc1 = gc2 = gc3 = null;
busy = false;
try {
var hslice = new Uint8Array(hashbuf).subarray(0, 33);
//console.log('[ w] %d HASH DONE', nchunk);
postMessage(["done", nchunk, hslice, cdr - car]);
}
catch (ex) {
postMessage(["panic", ex + '']);
}
};
// stop gc from attempting to free early
if (!gc1 || !gc2 || !gc3)
console.log('torch went out');
if (subtle)
subtle.digest('SHA-512', buf).then(hash_done);
else {
// note: lifting u8buf counterproductive for the chrome gc bug
var u8buf = new Uint8Array(buf);
hashwasm.sha512(u8buf).then(function (v) {
hash_done(hex2u8(v))
});
}
};
}

View File

@@ -13,6 +13,9 @@
# other stuff # other stuff
## [`changelog.md`](changelog.md)
* occasionally grabbed from github release notes
## [`rclone.md`](rclone.md) ## [`rclone.md`](rclone.md)
* notes on using rclone as a fuse client/server * notes on using rclone as a fuse client/server

2875
docs/changelog.md Normal file

File diff suppressed because it is too large Load Diff

10
docs/notes.md Normal file
View File

@@ -0,0 +1,10 @@
# up2k.js
## potato detection
* tsk 0.25/8.4/31.5 bzw 1.27/22.9/18 = 77% (38.4s, 49.7s)
* 4c locale #1313, ff-102,deb-11 @ ryzen4500u wifi -> win10
* profiling shows 2sec heavy gc every 2sec
* tsk 0.41/4.1/10 bzw 1.41/9.9/7 = 73% (13.3s, 18.2s)
* 4c locale #1313, ch-103,deb-11 @ ryzen4500u wifi -> win10

View File

@@ -48,7 +48,10 @@ avg() { awk 'function pr(ncsz) {if (nsmp>0) {printf "%3s %s\n", csz, sum/nsmp} c
## time between first and last upload ## time between first and last upload
python3 -um copyparty -nw -v srv::rw -i 127.0.0.1 2>&1 | tee log python3 -um copyparty -nw -v srv::rw -i 127.0.0.1 2>&1 | tee log
cat log | awk '!/"purl"/{next} {s=$1;sub(/[^m]+m/,"");gsub(/:/," ");t=60*(60*$1+$2)+$3} !a{a=t;sa=s} {b=t;sb=s} END {print b-a,sa,sb}' cat log | awk '!/"purl"/{next} {s=$1;sub(/[^m]+m/,"");gsub(/:/," ");t=60*(60*$1+$2)+$3} t<p{t+=86400} !a{a=t;sa=s} {b=t;sb=s} END {print b-a,sa,sb}'
# or if the client youre measuring dies for ~15sec every once ina while and you wanna filter those out,
cat log | awk '!/"purl"/{next} {s=$1;sub(/[^m]+m/,"");gsub(/:/," ");t=60*(60*$1+$2)+$3} t<p{t+=86400} !p{a=t;p=t;r=0;next} t-p>1{printf "%.3f += %.3f - %.3f (%.3f) # %.3f -> %.3f\n",r,p,a,p-a,p,t;r+=p-a;a=t} {p=t} END {print r+p-a}'
## ##
@@ -182,7 +185,7 @@ brew install python@2
pip install virtualenv pip install virtualenv
# readme toc # readme toc
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(/ .*/,"");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; rm p1 p2 toc 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()} /^#* *(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(/ .*/,"");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; rm p1 p2 toc
# 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)
@@ -200,6 +203,9 @@ git pull; git reset --hard origin/HEAD && git log --format=format:"%H %ai %d" --
# download all sfx versions # 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="$(printf '%s\n' "copyparty $v $t.py" | tr / -)"; [ -e "$fn" ] || curl https://github.com/9001/copyparty/releases/download/$v/copyparty-sfx.py -Lo "$fn"; done 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="$(printf '%s\n' "copyparty $v $t.py" | tr / -)"; [ -e "$fn" ] || curl https://github.com/9001/copyparty/releases/download/$v/copyparty-sfx.py -Lo "$fn"; done
# convert releasenotes to changelog
curl https://api.github.com/repos/9001/copyparty/releases?per_page=100 | jq -r '.[] | "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ \n# \(.created_at) `\(.tag_name)` \(.name)\n\n\(.body)\n\n\n"' | sed -r 's/^# ([0-9]{4}-)([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})Z /# \1\2\3-\4\5 /' > changelog.md
# push to multiple git remotes # push to multiple git remotes
git config -l | grep '^remote' git config -l | grep '^remote'
git remote add all git@github.com:9001/copyparty.git git remote add all git@github.com:9001/copyparty.git

View File

@@ -1,10 +1,10 @@
FROM alpine:3.15 FROM alpine:3.16
WORKDIR /z WORKDIR /z
ENV ver_asmcrypto=5b994303a9d3e27e0915f72a10b6c2c51535a4dc \ ENV ver_asmcrypto=5b994303a9d3e27e0915f72a10b6c2c51535a4dc \
ver_hashwasm=4.9.0 \ ver_hashwasm=4.9.0 \
ver_marked=4.0.16 \ ver_marked=4.0.18 \
ver_mde=2.16.1 \ ver_mde=2.16.1 \
ver_codemirror=5.65.4 \ ver_codemirror=5.65.7 \
ver_fontawesome=5.13.0 \ ver_fontawesome=5.13.0 \
ver_zopfli=1.0.3 ver_zopfli=1.0.3
@@ -32,7 +32,7 @@ RUN mkdir -p /z/dist/no-pk \
&& npm install \ && npm install \
&& npm i grunt uglify-js -g ) \ && npm i grunt uglify-js -g ) \
&& (tar -xf codemirror.tgz \ && (tar -xf codemirror.tgz \
&& cd CodeMirror-$ver_codemirror \ && cd codemirror5-$ver_codemirror \
&& npm install ) \ && npm install ) \
&& (tar -xf mde.tgz \ && (tar -xf mde.tgz \
&& cd easy-markdown-editor* \ && cd easy-markdown-editor* \
@@ -87,7 +87,7 @@ RUN cd marked-$ver_marked \
# build codemirror # build codemirror
COPY codemirror.patch /z/ COPY codemirror.patch /z/
RUN cd CodeMirror-$ver_codemirror \ RUN cd codemirror5-$ver_codemirror \
&& patch -p1 < /z/codemirror.patch \ && patch -p1 < /z/codemirror.patch \
&& sed -ri '/^var urlRE = /d' mode/gfm/gfm.js \ && sed -ri '/^var urlRE = /d' mode/gfm/gfm.js \
&& npm run build \ && npm run build \

View File

@@ -23,4 +23,4 @@ purge:
sh: sh:
@printf "\n\033[1;31mopening a shell in the most recently created docker image\033[0m\n" @printf "\n\033[1;31mopening a shell in the most recently created docker image\033[0m\n"
docker run --rm -it `docker images -aq | head -n 1` /bin/bash docker run --rm -it `docker images -aq | head -n 1` /bin/ash

View File

@@ -1,5 +1,5 @@
diff --git a/src/Lexer.js b/src/Lexer.js diff --git a/src/Lexer.js b/src/Lexer.js
adds linetracking to marked.js v4.0.6; adds linetracking to marked.js v4.0.17;
add data-ln="%d" to most tags, %d is the source markdown line add data-ln="%d" to most tags, %d is the source markdown line
--- a/src/Lexer.js --- a/src/Lexer.js
+++ b/src/Lexer.js +++ b/src/Lexer.js

View File

@@ -14,10 +14,6 @@ gtar=$(command -v gtar || command -v gnutar) || true
realpath() { grealpath "$@"; } realpath() { grealpath "$@"; }
} }
which md5sum 2>/dev/null >/dev/null &&
md5sum=md5sum ||
md5sum="md5 -r"
mode="$1" mode="$1"
[ -z "$mode" ] && [ -z "$mode" ] &&
@@ -90,6 +86,15 @@ function have() {
have setuptools have setuptools
have wheel have wheel
have twine have twine
# remove type hints to support python < 3.9
rm -rf build/pypi
mkdir -p build/pypi
cp -pR setup.py README.md LICENSE copyparty tests bin scripts/strip_hints build/pypi/
cd build/pypi
tar --strip-components=2 -xf ../strip-hints-0.1.10.tar.gz strip-hints-0.1.10/src/strip_hints
python3 -c 'from strip_hints.a import uh; uh("copyparty")'
./setup.py clean2 ./setup.py clean2
./setup.py sdist bdist_wheel --universal ./setup.py sdist bdist_wheel --universal

View File

@@ -26,6 +26,11 @@ help() { exec cat <<'EOF'
# (browsers will try to use 'Consolas' instead) # (browsers will try to use 'Consolas' instead)
# #
# `no-dd` saves ~2k by removing the mouse cursor # `no-dd` saves ~2k by removing the mouse cursor
#
# ---------------------------------------------------------------------
#
# if you are on windows, you can use msys2:
# PATH=/c/Users/$USER/AppData/Local/Programs/Python/Python310:"$PATH" ./make-sfx.sh fast
EOF EOF
} }
@@ -64,6 +69,9 @@ pybin=$(command -v python3 || command -v python) || {
exit 1 exit 1
} }
[ $CSN ] ||
CSN=sfx
langs= langs=
use_gz= use_gz=
zopf=2560 zopf=2560
@@ -76,7 +84,7 @@ while [ ! -z "$1" ]; do
no-hl) no_hl=1 ; ;; no-hl) no_hl=1 ; ;;
no-dd) no_dd=1 ; ;; no-dd) no_dd=1 ; ;;
no-cm) no_cm=1 ; ;; no-cm) no_cm=1 ; ;;
fast) zopf=100 ; ;; fast) zopf= ; ;;
lang) shift;langs="$1"; ;; lang) shift;langs="$1"; ;;
*) help ; ;; *) help ; ;;
esac esac
@@ -94,9 +102,9 @@ stamp=$(
done | sort | tail -n 1 | sha1sum | cut -c-16 done | sort | tail -n 1 | sha1sum | cut -c-16
) )
rm -rf sfx/* rm -rf $CSN/*
mkdir -p sfx build mkdir -p $CSN build
cd sfx cd $CSN
tmpdir="$( tmpdir="$(
printf '%s\n' "$TMPDIR" /tmp | printf '%s\n' "$TMPDIR" /tmp |
@@ -106,7 +114,7 @@ tmpdir="$(
[ $repack ] && { [ $repack ] && {
old="$tmpdir/pe-copyparty" old="$tmpdir/pe-copyparty"
echo "repack of files in $old" echo "repack of files in $old"
cp -pR "$old/"*{dep-j2,dep-ftp,copyparty} . cp -pR "$old/"*{py2,j2,ftp,copyparty} .
} }
[ $repack ] || { [ $repack ] || {
@@ -130,8 +138,8 @@ tmpdir="$(
mv MarkupSafe-*/src/markupsafe . mv MarkupSafe-*/src/markupsafe .
rm -rf MarkupSafe-* markupsafe/_speedups.c rm -rf MarkupSafe-* markupsafe/_speedups.c
mkdir dep-j2/ mkdir j2/
mv {markupsafe,jinja2} dep-j2/ mv {markupsafe,jinja2} j2/
echo collecting pyftpdlib echo collecting pyftpdlib
f="../build/pyftpdlib-1.5.6.tar.gz" f="../build/pyftpdlib-1.5.6.tar.gz"
@@ -143,8 +151,8 @@ tmpdir="$(
mv pyftpdlib-release-*/pyftpdlib . mv pyftpdlib-release-*/pyftpdlib .
rm -rf pyftpdlib-release-* pyftpdlib/test rm -rf pyftpdlib-release-* pyftpdlib/test
mkdir dep-ftp/ mkdir ftp/
mv pyftpdlib dep-ftp/ mv pyftpdlib ftp/
echo collecting asyncore, asynchat echo collecting asyncore, asynchat
for n in asyncore.py asynchat.py; do for n in asyncore.py asynchat.py; do
@@ -154,6 +162,24 @@ tmpdir="$(
wget -O$f "$url" || curl -L "$url" >$f) wget -O$f "$url" || curl -L "$url" >$f)
done done
# enable this to dynamically remove type hints at startup,
# in case a future python version can use them for performance
true || (
echo collecting strip-hints
f=../build/strip-hints-0.1.10.tar.gz
[ -e $f ] ||
(url=https://files.pythonhosted.org/packages/9c/d4/312ddce71ee10f7e0ab762afc027e07a918f1c0e1be5b0069db5b0e7542d/strip-hints-0.1.10.tar.gz;
wget -O$f "$url" || curl -L "$url" >$f)
tar -zxf $f
mv strip-hints-0.1.10/src/strip_hints .
rm -rf strip-hints-* strip_hints/import_hooks*
sed -ri 's/[a-z].* as import_hooks$/"""a"""/' strip_hints/*.py
cp -pR ../scripts/strip_hints/ .
)
cp -pR ../scripts/py2/ .
# msys2 tar is bad, make the best of it # msys2 tar is bad, make the best of it
echo collecting source echo collecting source
[ $clean ] && { [ $clean ] && {
@@ -170,6 +196,9 @@ tmpdir="$(
for n in asyncore.py asynchat.py; do for n in asyncore.py asynchat.py; do
awk 'NR<4||NR>27;NR==4{print"# license: https://opensource.org/licenses/ISC\n"}' ../build/$n >copyparty/vend/$n awk 'NR<4||NR>27;NR==4{print"# license: https://opensource.org/licenses/ISC\n"}' ../build/$n >copyparty/vend/$n
done done
# remove type hints before build instead
(cd copyparty; "$pybin" ../../scripts/strip_hints/a.py; rm uh)
} }
ver= ver=
@@ -211,7 +240,7 @@ ts=$(date -u +%s)
hts=$(date -u +%Y-%m%d-%H%M%S) # --date=@$ts (thx osx) hts=$(date -u +%Y-%m%d-%H%M%S) # --date=@$ts (thx osx)
mkdir -p ../dist mkdir -p ../dist
sfx_out=../dist/copyparty-sfx sfx_out=../dist/copyparty-$CSN
echo cleanup echo cleanup
find -name '*.pyc' -delete find -name '*.pyc' -delete
@@ -274,17 +303,23 @@ rm have
tmv "$f" tmv "$f"
done done
[ $repack ] || [ $repack ] || {
find | grep -E '\.py$' | # uncomment
grep -vE '__version__' | find | grep -E '\.py$' |
tr '\n' '\0' | grep -vE '__version__' |
xargs -0 "$pybin" ../scripts/uncomment.py tr '\n' '\0' |
xargs -0 "$pybin" ../scripts/uncomment.py
f=dep-j2/jinja2/constants.py # py2-compat
#find | grep -E '\.py$' | while IFS= read -r x; do
# sed -ri '/: TypeAlias = /d' "$x"; done
}
f=j2/jinja2/constants.py
awk '/^LOREM_IPSUM_WORDS/{o=1;print "LOREM_IPSUM_WORDS = u\"a\"";next} !o; /"""/{o=0}' <$f >t awk '/^LOREM_IPSUM_WORDS/{o=1;print "LOREM_IPSUM_WORDS = u\"a\"";next} !o; /"""/{o=0}' <$f >t
tmv "$f" tmv "$f"
grep -rLE '^#[^a-z]*coding: utf-8' dep-j2 | grep -rLE '^#[^a-z]*coding: utf-8' j2 |
while IFS= read -r f; do while IFS= read -r f; do
(echo "# coding: utf-8"; cat "$f") >t (echo "# coding: utf-8"; cat "$f") >t
tmv "$f" tmv "$f"
@@ -313,7 +348,7 @@ find | grep -E '\.(js|html)$' | while IFS= read -r f; do
done done
gzres() { gzres() {
command -v pigz && command -v pigz && [ $zopf ] &&
pk="pigz -11 -I $zopf" || pk="pigz -11 -I $zopf" ||
pk='gzip' pk='gzip'
@@ -339,7 +374,7 @@ gzres() {
} }
zdir="$tmpdir/cpp-mksfx" zdir="$tmpdir/cpp-mk$CSN"
[ -e "$zdir/$stamp" ] || rm -rf "$zdir" [ -e "$zdir/$stamp" ] || rm -rf "$zdir"
mkdir -p "$zdir" mkdir -p "$zdir"
echo a > "$zdir/$stamp" echo a > "$zdir/$stamp"
@@ -354,7 +389,8 @@ nf=$(ls -1 "$zdir"/arc.* | wc -l)
} }
[ $use_zdir ] && { [ $use_zdir ] && {
arcs=("$zdir"/arc.*) arcs=("$zdir"/arc.*)
arc="${arcs[$RANDOM % ${#arcs[@]} ] }" n=$(( $RANDOM % ${#arcs[@]} ))
arc="${arcs[n]}"
echo "using $arc" echo "using $arc"
tar -xf "$arc" tar -xf "$arc"
for f in copyparty/web/*.gz; do for f in copyparty/web/*.gz; do
@@ -364,13 +400,13 @@ nf=$(ls -1 "$zdir"/arc.* | wc -l)
echo gen tarlist echo gen tarlist
for d in copyparty dep-j2 dep-ftp; do find $d -type f; done | for d in copyparty j2 ftp py2; do find $d -type f; done | # strip_hints
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
for n in {1..50}; do for n in {1..50}; do
(grep -vE '\.(gz|br)$' list1; grep -E '\.(gz|br)$' list1 | shuf) >list || true (grep -vE '\.(gz|br)$' list1; grep -E '\.(gz|br)$' list1 | (shuf||gshuf) ) >list || true
s=$(md5sum list | cut -c-16) s=$( (sha1sum||shasum) < list | cut -c-16)
grep -q $s "$zdir/h" && continue grep -q $s "$zdir/h" && continue
echo $s >> "$zdir/h" echo $s >> "$zdir/h"
break break
@@ -390,7 +426,7 @@ pe=bz2
echo compressing tar echo compressing tar
# detect best level; bzip2 -7 is usually better than -9 # detect best level; bzip2 -7 is usually better than -9
for n in {2..9}; do cp tar t.$n; $pc -$n t.$n & done; wait; mv -v $(ls -1S t.*.$pe | tail -n 1) tar.bz2 for n in {2..9}; do cp tar t.$n; nice $pc -$n t.$n & done; wait; mv -v $(ls -1S t.*.$pe | tail -n 1) tar.bz2
rm t.* || true rm t.* || true
exts=() exts=()

View File

@@ -8,7 +8,7 @@ cmd = sys.argv[1]
if cmd == "cpp": if cmd == "cpp":
from copyparty.__main__ import main from copyparty.__main__ import main
argv = ["__main__", "-v", "srv::r", "-v", "../../yt:yt:r"] argv = ["__main__", "-vsrv::r:c,e2ds,e2ts"]
main(argv=argv) main(argv=argv)
elif cmd == "test": elif cmd == "test":
@@ -29,6 +29,6 @@ else:
# #
# python -m vmprof -o prof --lines ./scripts/profile.py test # python -m vmprof -o prof --lines ./scripts/profile.py test
# linux: ~/.local/bin/vmprofshow prof tree | grep -vF '[1m 0.' # linux: ~/.local/bin/vmprofshow prof tree | awk '$2>1{n=5} !n{next} 1;{n--} !n{print""}'
# macos: ~/Library/Python/3.9/bin/vmprofshow prof tree | grep -vF '[1m 0.' # macos: ~/Library/Python/3.9/bin/vmprofshow prof tree
# win: %appdata%\..\Roaming\Python\Python39\Scripts\vmprofshow.exe prof tree # win: %appdata%\..\Roaming\Python\Python39\Scripts\vmprofshow.exe prof tree

View File

@@ -0,0 +1,4 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from Queue import Queue, LifoQueue, PriorityQueue, Empty, Full

View File

@@ -1,7 +1,13 @@
#!/bin/bash #!/bin/bash
set -e set -e
cd ~/dev/copyparty/scripts parallel=2
[ -e make-sfx.sh ] || cd scripts
[ -e make-sfx.sh ] && [ -e deps-docker ] || {
echo cd into the scripts folder first
exit 1
}
v=$1 v=$1
@@ -21,14 +27,36 @@ v=$1
./make-tgz-release.sh $v ./make-tgz-release.sh $v
} }
rm -f ../dist/copyparty-sfx.* rm -f ../dist/copyparty-sfx*
f=../dist/copyparty-sfx.py shift
./make-sfx.sh ./make-sfx.sh "$@"
$f -h f=../dist/copyparty-sfx
[ -e $f.py ] ||
f=../dist/copyparty-sfx-gz
$f.py -h >/dev/null
[ $parallel -gt 1 ] && {
printf '\033[%s' s 2r H "0;1;37;44mbruteforcing sfx size -- press enter to terminate" K u "7m $* " K $'27m\n'
trap "rm -f .sfx-run; printf '\033[%s' s r u" INT TERM EXIT
touch .sfx-run
min=99999999
for ((a=0; a<$parallel; a++)); do
while [ -e .sfx-run ]; do
CSN=sfx$a ./make-sfx.sh re "$@"
sz=$(wc -c <$f$a.py | awk '{print$1}')
[ $sz -ge $min ] && continue
mv $f$a.py $f.py.$sz
min=$sz
done &
done
read
exit
}
while true; do while true; do
mv $f $f.$(wc -c <$f | awk '{print$1}') mv $f.py $f.$(wc -c <$f.py | awk '{print$1}').py
./make-sfx.sh re $ar ./make-sfx.sh re "$@"
done done
# git tag -d v$v; git push --delete origin v$v # git tag -d v$v; git push --delete origin v$v

View File

@@ -1,13 +1,23 @@
#!/bin/bash #!/bin/bash
set -ex set -ex
rm -rf unt
mkdir -p unt/srv
cp -pR copyparty tests unt/
cd unt
python3 ../scripts/strip_hints/a.py
pids=() pids=()
for py in python{2,3}; do for py in python{2,3}; do
PYTHONPATH=
[ $py = python2 ] && PYTHONPATH=../scripts/py2
export PYTHONPATH
nice $py -m unittest discover -s tests >/dev/null & nice $py -m unittest discover -s tests >/dev/null &
pids+=($!) pids+=($!)
done done
python3 scripts/test/smoketest.py & python3 ../scripts/test/smoketest.py &
pids+=($!) pids+=($!)
for pid in ${pids[@]}; do for pid in ${pids[@]}; do

View File

@@ -11,6 +11,7 @@ copyparty/broker_mp.py,
copyparty/broker_mpw.py, copyparty/broker_mpw.py,
copyparty/broker_thr.py, copyparty/broker_thr.py,
copyparty/broker_util.py, copyparty/broker_util.py,
copyparty/fsutil.py,
copyparty/ftpd.py, copyparty/ftpd.py,
copyparty/httpcli.py, copyparty/httpcli.py,
copyparty/httpconn.py, copyparty/httpconn.py,
@@ -42,6 +43,7 @@ copyparty/web/browser.html,
copyparty/web/browser.js, copyparty/web/browser.js,
copyparty/web/browser2.html, copyparty/web/browser2.html,
copyparty/web/copyparty.gif, copyparty/web/copyparty.gif,
copyparty/web/cf.html,
copyparty/web/dd, copyparty/web/dd,
copyparty/web/dd/2.png, copyparty/web/dd/2.png,
copyparty/web/dd/3.png, copyparty/web/dd/3.png,
@@ -75,3 +77,4 @@ copyparty/web/splash.js,
copyparty/web/ui.css, copyparty/web/ui.css,
copyparty/web/up2k.js, copyparty/web/up2k.js,
copyparty/web/util.js, copyparty/web/util.js,
copyparty/web/w.hash.js,

View File

@@ -213,22 +213,26 @@ def yieldfile(fn):
def hashfile(fn): def hashfile(fn):
h = hashlib.md5() h = hashlib.sha1()
for block in yieldfile(fn): for block in yieldfile(fn):
h.update(block) h.update(block)
return h.hexdigest() return h.hexdigest()[:24]
def unpack(): def unpack():
"""unpacks the tar yielded by `data`""" """unpacks the tar yielded by `data`"""
name = "pe-copyparty" name = "pe-copyparty"
tag = "v" + str(STAMP) tag = "v" + str(STAMP)
withpid = "{}.{}".format(name, os.getpid())
top = tempfile.gettempdir() top = tempfile.gettempdir()
opj = os.path.join opj = os.path.join
final = opj(top, name) final = opj(top, name)
mine = opj(top, withpid) for suf in range(0, 9001):
withpid = "{}.{}.{}".format(name, os.getpid(), suf)
mine = opj(top, withpid)
if not os.path.exists(mine):
break
tar = opj(mine, "tar") tar = opj(mine, "tar")
try: try:
@@ -360,11 +364,12 @@ def utime(top):
def confirm(rv): def confirm(rv):
msg() msg()
msg("retcode", rv if rv else traceback.format_exc()) msg("retcode", rv if rv else traceback.format_exc())
msg("*** hit enter to exit ***") if WINDOWS:
try: msg("*** hit enter to exit ***")
raw_input() if PY2 else input() try:
except: raw_input() if PY2 else input()
pass except:
pass
sys.exit(rv or 1) sys.exit(rv or 1)
@@ -379,9 +384,20 @@ def run(tmp, j2, ftp):
t.daemon = True t.daemon = True
t.start() t.start()
ld = (("", ""), (j2, "dep-j2"), (ftp, "dep-ftp")) ld = (("", ""), (j2, "j2"), (ftp, "ftp"), (not PY2, "py2"))
ld = [os.path.join(tmp, b) for a, b in ld if not a] ld = [os.path.join(tmp, b) for a, b in ld if not a]
# skip 1
# enable this to dynamically remove type hints at startup,
# in case a future python version can use them for performance
if sys.version_info < (3, 10) and False:
sys.path.insert(0, ld[0])
from strip_hints.a import uh
uh(tmp + "/copyparty")
# skip 0
if any([re.match(r"^-.*j[0-9]", x) for x in sys.argv]): if any([re.match(r"^-.*j[0-9]", x) for x in sys.argv]):
run_s(ld) run_s(ld)
else: else:

View File

@@ -47,7 +47,7 @@ grep -E '/(python|pypy)[0-9\.-]*$' >$dir/pys || true
printf '\033[1;30mlooking for jinja2 in [%s]\033[0m\n' "$_py" >&2 printf '\033[1;30mlooking for jinja2 in [%s]\033[0m\n' "$_py" >&2
$_py -c 'import jinja2' 2>/dev/null || continue $_py -c 'import jinja2' 2>/dev/null || continue
printf '%s\n' "$_py" printf '%s\n' "$_py"
mv $dir/{,x.}dep-j2 mv $dir/{,x.}j2
break break
done)" done)"

72
scripts/strip_hints/a.py Normal file
View File

@@ -0,0 +1,72 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
import re
import os
import sys
from strip_hints import strip_file_to_string
# list unique types used in hints:
# rm -rf unt && cp -pR copyparty unt && (cd unt && python3 ../scripts/strip_hints/a.py)
# diff -wNarU1 copyparty unt | grep -E '^\-' | sed -r 's/[^][, ]+://g; s/[^][, ]+[[(]//g; s/[],()<>{} -]/\n/g' | grep -E .. | sort | uniq -c | sort -n
def pr(m):
sys.stderr.write(m)
sys.stderr.flush()
def uh(top):
if os.path.exists(top + "/uh"):
return
# pr("building support for your python ver")
pr("unhinting")
files = []
for (dp, _, fns) in os.walk(top):
for fn in fns:
if not fn.endswith(".py"):
continue
fp = os.path.join(dp, fn)
files.append(fp)
try:
import multiprocessing as mp
with mp.Pool(os.cpu_count()) as pool:
pool.map(uh1, files)
except Exception as ex:
print("\nnon-mp fallback due to {}\n".format(ex))
for fp in files:
uh1(fp)
pr("k\n\n")
with open(top + "/uh", "wb") as f:
f.write(b"a")
def uh1(fp):
pr(".")
cs = strip_file_to_string(fp, no_ast=True, to_empty=True)
libs = "typing|types|collections\.abc"
ptn = re.compile(r"^(\s*)(from (?:{0}) import |import (?:{0})\b).*".format(libs))
# remove expensive imports too
lns = []
for ln in cs.split("\n"):
m = ptn.match(ln)
if m:
ln = m.group(1) + "raise Exception()"
lns.append(ln)
cs = "\n".join(lns)
with open(fp, "wb") as f:
f.write(cs.encode("utf-8"))
if __name__ == "__main__":
uh(".")

View File

@@ -58,13 +58,13 @@ class CState(threading.Thread):
remotes.append("?") remotes.append("?")
remotes_ok = False remotes_ok = False
m = [] ta = []
for conn, remote in zip(self.cs, remotes): for conn, remote in zip(self.cs, remotes):
stage = len(conn.st) stage = len(conn.st)
m.append(f"\033[3{colors[stage]}m{remote}") ta.append(f"\033[3{colors[stage]}m{remote}")
m = " ".join(m) t = " ".join(ta)
print(f"{m}\033[0m\n\033[A", end="") print(f"{t}\033[0m\n\033[A", end="")
def allget(cs, urls): def allget(cs, urls):

View File

@@ -72,6 +72,8 @@ def tc1(vflags):
for _ in range(10): for _ in range(10):
try: try:
os.mkdir(td) os.mkdir(td)
if os.path.exists(td):
break
except: except:
time.sleep(0.1) # win10 time.sleep(0.1) # win10

View File

@@ -3,6 +3,7 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import io import io
import os
import sys import sys
import tokenize import tokenize
@@ -10,6 +11,7 @@ import tokenize
def uncomment(fpath): def uncomment(fpath):
"""modified https://stackoverflow.com/a/62074206""" """modified https://stackoverflow.com/a/62074206"""
print(".", end="", flush=True)
with open(fpath, "rb") as f: with open(fpath, "rb") as f:
orig = f.read().decode("utf-8") orig = f.read().decode("utf-8")
@@ -66,9 +68,15 @@ def uncomment(fpath):
def main(): def main():
print("uncommenting", end="", flush=True) print("uncommenting", end="", flush=True)
for f in sys.argv[1:]: try:
print(".", end="", flush=True) import multiprocessing as mp
uncomment(f)
with mp.Pool(os.cpu_count()) as pool:
pool.map(uncomment, sys.argv[1:])
except Exception as ex:
print("\nnon-mp fallback due to {}\n".format(ex))
for f in sys.argv[1:]:
uncomment(f)
print("k") print("k")

View File

@@ -10,9 +10,10 @@ import pprint
import tarfile import tarfile
import tempfile import tempfile
import unittest import unittest
from argparse import Namespace
from tests import util as tu from tests import util as tu
from tests.util import Cfg
from copyparty.authsrv import AuthSrv from copyparty.authsrv import AuthSrv
from copyparty.httpcli import HttpCli from copyparty.httpcli import HttpCli
@@ -22,55 +23,6 @@ def hdr(query):
return h.format(query).encode("utf-8") return h.format(query).encode("utf-8")
class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None):
super(Cfg, self).__init__(
a=a or [],
v=v or [],
c=c,
rproxy=0,
rsp_slp=0,
s_wr_slp=0,
s_wr_sz=512 * 1024,
ed=False,
nw=False,
unpost=600,
no_mv=False,
no_del=False,
no_zip=False,
no_thumb=False,
no_athumb=False,
no_vthumb=False,
no_voldump=True,
no_scandir=False,
no_sendfile=True,
no_rescan=True,
no_logues=False,
no_readme=False,
re_maxage=0,
ihead=False,
nih=True,
mtp=[],
mte="a",
mth="",
textfiles="",
doctitle="",
html_head="",
theme=0,
themes=0,
turbo=0,
logout=573,
hist=None,
no_idx=None,
no_hash=None,
force_js=False,
no_robots=False,
js_browser=None,
css_browser=None,
**{k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr no_acode".split()}
)
class TestHttpCli(unittest.TestCase): class TestHttpCli(unittest.TestCase):
def setUp(self): def setUp(self):
self.td = tu.get_ramdisk() self.td = tu.get_ramdisk()

View File

@@ -8,43 +8,14 @@ import shutil
import tempfile import tempfile
import unittest import unittest
from textwrap import dedent from textwrap import dedent
from argparse import Namespace
from tests import util as tu from tests import util as tu
from tests.util import Cfg
from copyparty.authsrv import AuthSrv, VFS from copyparty.authsrv import AuthSrv, VFS
from copyparty import util from copyparty import util
class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None):
ex = "nw e2d e2ds e2dsa e2t e2ts e2tsr no_logues no_readme no_acode force_js no_robots no_thumb no_athumb no_vthumb"
ex = {k: False for k in ex.split()}
ex2 = {
"mtp": [],
"mte": "a",
"mth": "",
"doctitle": "",
"html_head": "",
"hist": None,
"no_idx": None,
"no_hash": None,
"js_browser": None,
"css_browser": None,
"no_voldump": True,
"re_maxage": 0,
"rproxy": 0,
"rsp_slp": 0,
"s_wr_slp": 0,
"s_wr_sz": 512 * 1024,
"theme": 0,
"themes": 0,
"turbo": 0,
"logout": 573,
}
ex.update(ex2)
super(Cfg, self).__init__(a=a or [], v=v or [], c=c, **ex)
class TestVFS(unittest.TestCase): class TestVFS(unittest.TestCase):
def setUp(self): def setUp(self):
self.td = tu.get_ramdisk() self.td = tu.get_ramdisk()
@@ -84,7 +55,7 @@ class TestVFS(unittest.TestCase):
pass pass
def assertAxs(self, dct, lst): def assertAxs(self, dct, lst):
t1 = list(sorted(dct.keys())) t1 = list(sorted(dct))
t2 = list(sorted(lst)) t2 = list(sorted(lst))
self.assertEqual(t1, t2) self.assertEqual(t1, t2)
@@ -207,10 +178,10 @@ class TestVFS(unittest.TestCase):
self.assertEqual(n.realpath, os.path.join(td, "a")) self.assertEqual(n.realpath, os.path.join(td, "a"))
self.assertAxs(n.axs.uread, ["*"]) self.assertAxs(n.axs.uread, ["*"])
self.assertAxs(n.axs.uwrite, []) self.assertAxs(n.axs.uwrite, [])
self.assertEqual(vfs.can_access("/", "*"), [False, False, False, False, False]) self.assertEqual(vfs.can_access("/", "*"), (False, False, False, False, False))
self.assertEqual(vfs.can_access("/", "k"), [True, True, False, False, False]) self.assertEqual(vfs.can_access("/", "k"), (True, True, False, False, False))
self.assertEqual(vfs.can_access("/a", "*"), [True, False, False, False, False]) self.assertEqual(vfs.can_access("/a", "*"), (True, False, False, False, False))
self.assertEqual(vfs.can_access("/a", "k"), [True, False, False, False, False]) self.assertEqual(vfs.can_access("/a", "k"), (True, False, False, False, False))
# breadth-first construction # breadth-first construction
vfs = AuthSrv( vfs = AuthSrv(
@@ -278,7 +249,7 @@ class TestVFS(unittest.TestCase):
n = au.vfs n = au.vfs
# root was not defined, so PWD with no access to anyone # root was not defined, so PWD with no access to anyone
self.assertEqual(n.vpath, "") self.assertEqual(n.vpath, "")
self.assertEqual(n.realpath, None) self.assertEqual(n.realpath, "")
self.assertAxs(n.axs.uread, []) self.assertAxs(n.axs.uread, [])
self.assertAxs(n.axs.uwrite, []) self.assertAxs(n.axs.uwrite, [])
self.assertEqual(len(n.nodes), 1) self.assertEqual(len(n.nodes), 1)

View File

@@ -7,6 +7,7 @@ import threading
import tempfile import tempfile
import platform import platform
import subprocess as sp import subprocess as sp
from argparse import Namespace
WINDOWS = platform.system() == "Windows" WINDOWS = platform.system() == "Windows"
@@ -89,8 +90,45 @@ def get_ramdisk():
return subdir(ret) return subdir(ret)
class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None):
ka = {}
ex = "e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp xdev xvol ed emp force_js ihead no_acode no_athumb no_del no_logues no_mv no_readme no_robots no_scandir no_thumb no_vthumb no_zip nid nih nw"
ka.update(**{k: False for k in ex.split()})
ex = "no_rescan no_sendfile no_voldump"
ka.update(**{k: True for k in ex.split()})
ex = "css_browser hist js_browser no_hash no_idx"
ka.update(**{k: None for k in ex.split()})
ex = "re_maxage rproxy rsp_slp s_wr_slp theme themes turbo df"
ka.update(**{k: 0 for k in ex.split()})
ex = "doctitle favico html_head mth textfiles log_fk"
ka.update(**{k: "" for k in ex.split()})
super(Cfg, self).__init__(
a=a or [],
v=v or [],
c=c,
s_wr_sz=512 * 1024,
unpost=600,
u2sort="s",
mtp=[],
mte="a",
lang="eng",
logout=573,
**ka
)
class NullBroker(object): class NullBroker(object):
def put(*args): def say(*args):
pass
def ask(*args):
pass pass
@@ -128,7 +166,7 @@ class VHttpSrv(object):
class VHttpConn(object): class VHttpConn(object):
def __init__(self, args, asrv, log, buf): def __init__(self, args, asrv, log, buf):
self.s = VSock(buf) self.s = VSock(buf)
self.sr = Unrecv(self.s) self.sr = Unrecv(self.s, None)
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