Compare commits

...

112 Commits

Author SHA1 Message Date
ed
1111baacb2 v1.3.0 2022-05-22 17:02:38 +02:00
ed
1b9c913efb update deps (marked, codemirror, prism) 2022-05-22 16:49:18 +02:00
ed
3524c36e1b tl 2022-05-22 16:04:10 +02:00
ed
cf87cea9f8 ux, tl 2022-05-21 11:32:25 +02:00
ed
bfa34404b8 ux tweaks 2022-05-19 18:00:33 +02:00
ed
0aba5f35bf add confirms on colhide, bigtxt 2022-05-19 17:59:33 +02:00
ed
663bc0842a ux 2022-05-18 19:51:25 +02:00
ed
7d10c96e73 grammar 2022-05-18 19:33:20 +02:00
ed
6b2720fab0 dont switch to treeview on play into next folder 2022-05-18 19:24:47 +02:00
ed
e74ad5132a persist videoplayer prefs 2022-05-18 19:17:21 +02:00
ed
1f6f89c1fd apply default-language to splashpage 2022-05-18 19:02:36 +02:00
ed
4d55e60980 update flat-light ss 2022-05-16 19:01:32 +02:00
ed
ddaaccd5af ux tweaks 2022-05-16 18:56:53 +02:00
ed
c20b7dac3d ah whatever, still 16 years left 2022-05-15 17:23:52 +02:00
ed
1f779d5094 zip: add ntfs and unix extensions for utc time 2022-05-15 16:13:49 +02:00
ed
715401ca8e fix timezone in search, zipfiles, fuse 2022-05-15 13:51:44 +02:00
ed
e7cd922d8b translate splashpage and search too 2022-05-15 13:20:52 +02:00
ed
187feee0c1 add norwegian translation 2022-05-14 23:25:40 +02:00
ed
49e962a7dc dbtool: faster, add examples,
match on hashes rather than paths by default,
add no-clobber option to keep existing tags
2022-05-14 12:44:05 +02:00
ed
633ff601e5 perf + ux 2022-05-14 00:13:06 +02:00
ed
331cf37054 show loading progress for huge documents 2022-05-13 23:02:20 +02:00
ed
23e4b9002f support ?doc=mojibake 2022-05-13 18:10:55 +02:00
ed
c0de3c8053 v1.2.11 2022-05-13 17:24:50 +02:00
ed
a82a3b084a make search results unselectable 2022-05-13 17:18:19 +02:00
ed
67c298e66b don't embed huge docs (defer to ajax), closes #9 2022-05-13 17:08:17 +02:00
ed
c110ccb9ae v1.2.10 2022-05-13 01:44:00 +02:00
ed
0143380306 help the query planner 2022-05-13 01:41:39 +02:00
ed
af9000d3c8 v1.2.9 2022-05-12 23:10:54 +02:00
ed
097d798e5e steal colors from monokai 2022-05-12 23:06:37 +02:00
ed
1d9f9f221a louder 2022-05-12 20:55:37 +02:00
ed
214a367f48 be loud about segfaults and such 2022-05-12 20:26:48 +02:00
ed
2fb46551a2 avoid pointless recursion + show scan summary 2022-05-09 23:43:59 +02:00
ed
6bcf330ae0 symlink-checker: print base vpath in nonverbose mode 2022-05-09 20:17:03 +00:00
ed
2075a8b18c skip nonregular files when indexing filesystem 2022-05-09 19:56:17 +00:00
ed
1275ac6c42 start up2k indexing even if no interfaces could bind 2022-05-09 20:38:06 +02:00
ed
708f20b7af remove option to disable spa 2022-05-08 14:29:05 +02:00
ed
a2c0c708e8 focus password field if not logged in 2022-05-07 22:16:12 +02:00
ed
2f2c65d91e improve up2k error messages 2022-05-07 22:15:09 +02:00
ed
cd5fcc7ca7 fix file sel/play background on focus 2022-05-06 21:15:18 +02:00
ed
aa29e7be48 minimal support for browsers without css-variables 2022-05-03 00:52:26 +02:00
ed
93febe34b0 truncate huge ffmpeg errors 2022-05-03 00:32:00 +02:00
ed
f086e6d3c1 best-effort recovery when chrome desyncs the mediaSession 2022-05-02 19:08:37 +02:00
ed
22e51e1c96 compensate for play/pause fades by rewinding a bit 2022-05-02 19:07:16 +02:00
ed
63a5336f31 change modal ok/cancel focus with left/right keys 2022-05-02 19:06:51 +02:00
ed
bfc6c53cc5 ux 2022-05-02 19:06:08 +02:00
ed
236017f310 better dropzones on small screens 2022-05-02 01:08:31 +02:00
ed
0a1d9b4dfd nevermind, not reliable when rproxied 2022-05-01 22:35:34 +02:00
ed
b50d090946 add logout on inactivity + related errorhandling 2022-05-01 22:12:25 +02:00
ed
00b5db52cf notes 2022-05-01 12:02:27 +02:00
ed
24cb30e2c5 support login from ie4 / win3.11 2022-05-01 11:42:19 +02:00
ed
4549145ab5 fix filekeys in basic-html browser 2022-05-01 11:29:51 +02:00
ed
67b0217754 cleanup + readme 2022-04-30 23:37:27 +02:00
ed
ccae9efdf0 safer systemd example (unprivileged user + NAT for port 80 / 443) 2022-04-30 23:28:51 +02:00
ed
59d596b222 add service to autogenerate TLS certificates 2022-04-30 22:54:35 +02:00
ed
4878eb2c45 support symlinks as volume root 2022-04-30 20:26:26 +02:00
ed
7755392f57 redirect to webroot after login 2022-04-30 18:15:09 +02:00
ed
dc2ea20959 v1.2.8 2022-04-30 02:16:34 +02:00
ed
8eaea2bd17 ux 2022-04-30 00:37:31 +02:00
ed
58e559918f fix dynamic tree sizing 2022-04-30 00:04:06 +02:00
ed
f38a3fca5b case-insensitive cover check 2022-04-29 23:39:16 +02:00
ed
1ea145b384 wow when did that break 2022-04-29 23:37:38 +02:00
ed
0d9567575a avoid hashing busy uploads during rescan 2022-04-29 23:16:23 +02:00
ed
e82f176289 fix deadlock on rescan during upload 2022-04-29 23:14:51 +02:00
ed
d4b51c040e doc + ux 2022-04-29 23:13:37 +02:00
ed
125d0efbd8 good stuff 2022-04-29 02:06:56 +02:00
ed
3215afc504 immediately search on enter key 2022-04-28 22:53:37 +02:00
ed
c73ff3ce1b avoid sqlite deadlock on windows 2022-04-28 22:46:53 +02:00
ed
f9c159a051 add option to force up2k turbo + hide warning 2022-04-28 21:57:37 +02:00
ed
2ab1325c90 add option to load more search results 2022-04-28 21:55:01 +02:00
ed
5b0f7ff506 perfect 2022-04-28 10:36:56 +02:00
ed
9269bc84f2 skip more stuff windows doesn't like 2022-04-28 10:31:10 +02:00
ed
4e8b651e18 too much effort into this joke 2022-04-28 10:29:54 +02:00
ed
65b4f79534 add themes "vice" and "hot dog stand" 2022-04-27 22:33:01 +02:00
ed
5dd43dbc45 ignore bugs in chrome v102 2022-04-27 22:32:11 +02:00
ed
5f73074c7e fix audio playback on first visit 2022-04-27 22:31:33 +02:00
ed
f5d6ba27b2 handle invalid headers better 2022-04-27 22:30:19 +02:00
ed
73fa70b41f fix mostly-harmless xss 2022-04-27 22:29:16 +02:00
ed
2a1cda42e7 avoid deadlocks on windows 2022-04-27 22:27:49 +02:00
ed
1bd7e31466 more theme porting 2022-04-26 00:42:00 +02:00
ed
eb49e1fb4a conditional up2k column sizes depending on card 2022-04-24 23:48:23 +02:00
ed
9838c2f0ce golf 2022-04-24 23:47:15 +02:00
ed
6041df8370 start replacing class-scopes with css variables 2022-04-24 23:46:38 +02:00
ed
2933dce3ef mtime blank uploads + helptext 2022-04-24 22:58:11 +02:00
ed
dab377d37b v1.2.7 2022-04-16 23:44:28 +02:00
ed
f35e41baf1 allow unposting with write-only access 2022-04-16 23:35:04 +02:00
ed
c4083a2942 v1.2.6 2022-04-15 20:09:50 +02:00
ed
36c20bbe53 fix setting mtime on windows 2022-04-15 20:08:55 +02:00
ed
e34634f5af v1.2.5 2022-04-15 19:42:40 +02:00
ed
cba9e5b669 add hardlinks (symlink alternative) for up2k dedup 2022-04-15 19:13:53 +02:00
ed
1f3c46a6b0 forgot some css files 2022-04-15 17:11:46 +02:00
ed
799a5ffa47 v1.2.4 2022-04-14 21:45:22 +02:00
ed
b000707c10 detect poor ffmpeg builds 2022-04-14 18:20:48 +02:00
ed
feba4de1d6 make gallery linkable 2022-04-14 17:12:56 +02:00
ed
951fdb27ca dont scan orphaned volumes 2022-04-14 17:11:51 +02:00
ed
9697fb3d84 option to disable thumbnails per volume 2022-04-14 17:11:26 +02:00
ed
2dbed4500a add flat theme 2022-04-14 16:57:51 +02:00
ed
fd9d0e433d thumbnails: try FFmpeg for images too 2022-04-11 10:38:57 +02:00
ed
f096f3ef81 thumbnails: disable pdf because too scary 2022-04-10 23:02:09 +02:00
ed
cc4a063695 thumbnails: per-decoder filetype config 2022-04-10 22:59:45 +02:00
ed
b64cabc3c9 thumbnails: add pyvips as alt/supp. to pillow 2022-04-10 14:16:09 +02:00
ed
3dd460717c add flat theme 2022-04-09 23:05:54 +02:00
ed
bf658a522b naming 2022-04-09 20:41:08 +02:00
ed
e9be7e712d futureproof clipboard function 2022-04-09 19:38:05 +02:00
ed
e40cd2a809 optimize window resizing 2022-04-09 19:20:09 +02:00
ed
dbabeb9692 gallery: add animation preferences 2022-04-09 17:23:54 +02:00
ed
8dd37d76b0 fix drifting resize 2022-04-09 14:37:25 +02:00
ed
fd475aa358 textviewer: translate basic ansi/sgr colors 2022-04-09 00:50:54 +02:00
ed
f0988c0e32 filter some volflags from up2k dump 2022-04-08 21:56:24 +02:00
ed
0632f09bff rhel8 ignores flock and kills us anyways 2022-04-08 21:29:31 +02:00
ed
ba599aaca0 explain systemd jank 2022-04-08 20:39:22 +02:00
ed
ff05919e89 support mpc/musepack audio (streaming + thumbnailing) 2022-04-02 22:17:16 +02:00
ed
52e63fa101 dont crash when mediaplayer config is changed while music isnt playing 2022-03-28 23:17:02 +02:00
53 changed files with 3840 additions and 1916 deletions

View File

@@ -63,6 +63,7 @@ turn your phone or raspi into a portable file server with resumable uploads/down
* [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags, also see [./bin/mtag/README.md](./bin/mtag/README.md) * [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags, also see [./bin/mtag/README.md](./bin/mtag/README.md)
* [upload events](#upload-events) - trigger a script/program on each upload * [upload events](#upload-events) - trigger a script/program on each upload
* [hiding from google](#hiding-from-google) - tell search engines you dont wanna be indexed * [hiding from google](#hiding-from-google) - tell search engines you dont wanna be indexed
* [themes](#themes)
* [complete examples](#complete-examples) * [complete examples](#complete-examples)
* [browser support](#browser-support) - TLDR: yes * [browser support](#browser-support) - TLDR: yes
* [client examples](#client-examples) - interact with copyparty using non-browser clients * [client examples](#client-examples) - interact with copyparty using non-browser clients
@@ -174,7 +175,7 @@ feature summary
* ☑ image gallery with webm player * ☑ image gallery with webm player
* ☑ textfile browser with syntax hilighting * ☑ textfile browser with syntax hilighting
* ☑ [thumbnails](#thumbnails) * ☑ [thumbnails](#thumbnails)
* ☑ ...of images using Pillow * ☑ ...of images using Pillow, pyvips, or FFmpeg
* ☑ ...of videos using FFmpeg * ☑ ...of videos using FFmpeg
* ☑ ...of audio (spectrograms) using FFmpeg * ☑ ...of audio (spectrograms) using FFmpeg
* ☑ cache eviction (max-age; maybe max-size eventually) * ☑ cache eviction (max-age; maybe max-size eventually)
@@ -247,6 +248,8 @@ some improvement ideas
## 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
* 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...
@@ -403,7 +406,9 @@ press `g` to toggle grid-view instead of the file listing, and `t` toggles icon
![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)
it does static images with Pillow and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how dangerous your users are it does static images with Pillow / pyvips / FFmpeg, and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how dangerous your users are
* pyvips is 3x faster than Pillow, Pillow is 3x faster than FFmpeg
* disable thumbnails for specific volumes with volflag `dthumb` for all, or `dvthumb` / `dathumb` / `dithumb` for video/audio/images only
audio files are covnerted into spectrograms using FFmpeg unless you `--no-athumb` (and some FFmpeg builds may need `--th-ff-swr`) audio files are covnerted into spectrograms using FFmpeg unless you `--no-athumb` (and some FFmpeg builds may need `--th-ff-swr`)
@@ -804,6 +809,29 @@ tell search engines you dont wanna be indexed, either using the good old [robot
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
## themes
you can change the default theme with `--theme 2`, and add your own themes by modifying `browser.css` or providing your own css to `--css-browser`, then telling copyparty they exist by increasing `--themes`
<table><tr><td width="33%" align="center"><a href="https://user-images.githubusercontent.com/241032/165864907-17e2ac7d-319d-4f25-8718-2f376f614b51.png"><img src="https://user-images.githubusercontent.com/241032/165867551-fceb35dd-38f0-42bb-bef3-25ba651ca69b.png"></a>
0. classic dark</td><td width="33%" align="center"><a href="https://user-images.githubusercontent.com/241032/168644399-68938de5-da9b-445f-8d92-b51c74b5f345.png"><img src="https://user-images.githubusercontent.com/241032/168644404-8e1a2fdc-6e59-4c41-905e-ba5399ed686f.png"></a>
2. flat pm-monokai</td><td width="33%" align="center"><a href="https://user-images.githubusercontent.com/241032/165864901-db13a429-a5da-496d-8bc6-ce838547f69d.png"><img src="https://user-images.githubusercontent.com/241032/165867560-aa834aef-58dc-4abe-baef-7e562b647945.png"></a>
4. vice</td></tr><tr><td align="center"><a href="https://user-images.githubusercontent.com/241032/165864905-692682eb-6fb4-4d40-b6fe-27d2c7d3e2a7.png"><img src="https://user-images.githubusercontent.com/241032/165867555-080b73b6-6d85-41bb-a7c6-ad277c608365.png"></a>
1. classic light</td><td align="center"><a href="https://user-images.githubusercontent.com/241032/168645276-fb02fd19-190a-407a-b8d3-d58fee277e02.png"><img src="https://user-images.githubusercontent.com/241032/168645280-f0662b3c-9764-4875-a2e2-d91cc8199b23.png"></a>
3. flat light
</td><td align="center"><a href="https://user-images.githubusercontent.com/241032/165864898-10ce7052-a117-4fcf-845b-b56c91687908.png"><img src="https://user-images.githubusercontent.com/241032/165867562-f3003d45-dd2a-4564-8aae-fed44c1ae064.png"></a>
5. <a href="https://blog.codinghorror.com/a-tribute-to-the-windows-31-hot-dog-stand-color-scheme/">hotdog stand</a></td></tr></table>
the classname of the HTML tag is set according to the selected theme, which is used to set colors as css variables ++
* each theme *generally* has a dark theme (even numbers) and a light theme (odd numbers), showing in pairs
* the first theme (theme 0 and 1) is `html.a`, second theme (2 and 3) is `html.b`
* if a light theme is selected, `html.y` is set, otherwise `html.z` is
* so if the dark edition of the 2nd theme is selected, you use any of `html.b`, `html.z`, `html.bz` to specify rules
see the top of [./copyparty/web/browser.css](./copyparty/web/browser.css) where the color variables are set, and there's layout-specific stuff near the bottom
## complete examples ## complete examples
* read-only music server with bpm and key scanning * read-only music server with bpm and key scanning
@@ -853,7 +881,7 @@ quick summary of more eccentric web-browsers trying to view a directory index:
| **w3m** (0.5.3/macports) | can browse, login, upload at 100kB/s, mkdir/msg | | **w3m** (0.5.3/macports) | can browse, login, upload at 100kB/s, mkdir/msg |
| **netsurf** (3.10/arch) | is basically ie6 with much better css (javascript has almost no effect) | | **netsurf** (3.10/arch) | is basically ie6 with much better css (javascript has almost no effect) |
| **opera** (11.60/winxp) | OK: thumbnails, image-viewer, zip-selection, rename/cut/paste. NG: up2k, navpane, markdown, audio | | **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` | | **ie4** and **netscape** 4.0 | can browse, upload with `?b=u`, auth with `&pw=wark` |
| **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 |
@@ -1096,10 +1124,13 @@ enable music tags:
* or `ffprobe` (20x slower, more accurate, possibly dangerous depending on your distro and users) * or `ffprobe` (20x slower, more accurate, possibly dangerous depending on your distro and users)
enable [thumbnails](#thumbnails) of... enable [thumbnails](#thumbnails) of...
* **images:** `Pillow` (requires py2.7 or py3.5+) * **images:** `Pillow` and/or `pyvips` and/or `ffmpeg` (requires py2.7 or py3.5+)
* **videos/audio:** `ffmpeg` and `ffprobe` somewhere in `$PATH` * **videos/audio:** `ffmpeg` and `ffprobe` somewhere in `$PATH`
* **HEIF pictures:** `pyheif-pillow-opener` (requires Linux or a C compiler) * **HEIF pictures:** `pyvips` or `ffmpeg` or `pyheif-pillow-opener` (requires Linux or a C compiler)
* **AVIF pictures:** `pillow-avif-plugin` * **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin`
* **JPEG XL pictures:** `pyvips` or `ffmpeg`
`pyvips` gives higher quality thumbnails than `Pillow` and is 320% faster, using 270% more ram: `sudo apt install libvips42 && python3 -m pip install --user -U pyvips`
## install recommended deps ## install recommended deps
@@ -1144,12 +1175,16 @@ for the `re`pack to work, first run one of the sfx'es once to unpack it
install [Termux](https://termux.com/) (see [ocv.me/termux](https://ocv.me/termux/)) and then copy-paste this into Termux (long-tap) all at once: install [Termux](https://termux.com/) (see [ocv.me/termux](https://ocv.me/termux/)) and then copy-paste this into Termux (long-tap) all at once:
```sh ```sh
apt update && apt -y full-upgrade && termux-setup-storage && apt -y install python && python -m ensurepip && python -m pip install -U copyparty apt update && apt -y full-upgrade && apt update && termux-setup-storage && apt -y install python && python -m ensurepip && python -m pip install --user -U copyparty
echo $? echo $?
``` ```
after the initial setup, you can launch copyparty at any time by running `copyparty` anywhere in Termux after the initial setup, you can launch copyparty at any time by running `copyparty` anywhere in Termux
if you want thumbnails, `apt -y install ffmpeg`
* or if you want to use vips instead, `apt -y install libvips && python -m pip install --user -U wheel && python -m pip install --user -U pyvips && (cd /data/data/com.termux/files/usr/lib/; ln -s libgobject-2.0.so{,.0}; ln -s libvips.so{,.42})`
# reporting bugs # reporting bugs
@@ -1173,7 +1208,7 @@ python3 -m venv .venv
pip install jinja2 # mandatory pip install jinja2 # mandatory
pip install mutagen # audio metadata pip install mutagen # audio metadata
pip install Pillow pyheif-pillow-opener pillow-avif-plugin # thumbnails pip install Pillow pyheif-pillow-opener pillow-avif-plugin # thumbnails
pip install black bandit pylint flake8 # vscode tooling pip install black==21.12b0 bandit pylint flake8 # vscode tooling
``` ```

View File

@@ -42,6 +42,7 @@ import threading
import traceback import traceback
import http.client # py2: httplib import http.client # py2: httplib
import urllib.parse import urllib.parse
import calendar
from datetime import datetime from datetime import datetime
from urllib.parse import quote_from_bytes as quote from urllib.parse import quote_from_bytes as quote
from urllib.parse import unquote_to_bytes as unquote from urllib.parse import unquote_to_bytes as unquote
@@ -495,7 +496,7 @@ class Gateway(object):
ts = 60 * 60 * 24 * 2 ts = 60 * 60 * 24 * 2
try: try:
sz = int(fsize) sz = int(fsize)
ts = datetime.strptime(fdate, "%Y-%m-%d %H:%M:%S").timestamp() ts = calendar.timegm(time.strptime(fdate, "%Y-%m-%d %H:%M:%S"))
except: except:
info("bad HTML or OS [{}] [{}]".format(fdate, fsize)) info("bad HTML or OS [{}] [{}]".format(fdate, fsize))
# python cannot strptime(1959-01-01) on windows # python cannot strptime(1959-01-01) on windows

View File

@@ -45,6 +45,7 @@ import threading
import traceback import traceback
import http.client # py2: httplib import http.client # py2: httplib
import urllib.parse import urllib.parse
import calendar
from datetime import datetime from datetime import datetime
from urllib.parse import quote_from_bytes as quote from urllib.parse import quote_from_bytes as quote
from urllib.parse import unquote_to_bytes as unquote from urllib.parse import unquote_to_bytes as unquote
@@ -443,7 +444,7 @@ class Gateway(object):
ts = 60 * 60 * 24 * 2 ts = 60 * 60 * 24 * 2
try: try:
sz = int(fsize) sz = int(fsize)
ts = datetime.strptime(fdate, "%Y-%m-%d %H:%M:%S").timestamp() ts = calendar.timegm(time.strptime(fdate, "%Y-%m-%d %H:%M:%S"))
except: except:
info("bad HTML or OS [{}] [{}]".format(fdate, fsize)) info("bad HTML or OS [{}] [{}]".format(fdate, fsize))
# python cannot strptime(1959-01-01) on windows # python cannot strptime(1959-01-01) on windows

View File

@@ -8,7 +8,10 @@ import sqlite3
import argparse import argparse
DB_VER1 = 3 DB_VER1 = 3
DB_VER2 = 4 DB_VER2 = 5
BY_PATH = None
NC = None
def die(msg): def die(msg):
@@ -57,8 +60,13 @@ def compare(n1, d1, n2, d2, verbose):
if rd.split("/", 1)[0] == ".hist": if rd.split("/", 1)[0] == ".hist":
continue continue
q = "select w from up where rd = ? and fn = ?" if BY_PATH:
hit = d2.execute(q, (rd, fn)).fetchone() q = "select w from up where rd = ? and fn = ?"
hit = d2.execute(q, (rd, fn)).fetchone()
else:
q = "select w from up where substr(w,1,16) = ? and +w = ?"
hit = d2.execute(q, (w1[:16], w1)).fetchone()
if not hit: if not hit:
miss += 1 miss += 1
if verbose: if verbose:
@@ -70,27 +78,32 @@ def compare(n1, d1, n2, d2, verbose):
n = 0 n = 0
miss = {} miss = {}
nmiss = 0 nmiss = 0
for w1, k, v in d1.execute("select * from mt"): for w1s, k, v in d1.execute("select * from mt"):
n += 1 n += 1
if n % 100_000 == 0: if n % 100_000 == 0:
m = f"\033[36mchecked {n:,} of {nt:,} tags in {n1} against {n2}, so far {nmiss} missing tags\033[0m" m = f"\033[36mchecked {n:,} of {nt:,} tags in {n1} against {n2}, so far {nmiss} missing tags\033[0m"
print(m) print(m)
q = "select rd, fn from up where substr(w,1,16) = ?" q = "select w, rd, fn from up where substr(w,1,16) = ?"
rd, fn = d1.execute(q, (w1,)).fetchone() w1, rd, fn = d1.execute(q, (w1s,)).fetchone()
if rd.split("/", 1)[0] == ".hist": if rd.split("/", 1)[0] == ".hist":
continue continue
q = "select substr(w,1,16) from up where rd = ? and fn = ?" if BY_PATH:
w2 = d2.execute(q, (rd, fn)).fetchone() q = "select w from up where rd = ? and fn = ?"
w2 = d2.execute(q, (rd, fn)).fetchone()
else:
q = "select w from up where substr(w,1,16) = ? and +w = ?"
w2 = d2.execute(q, (w1s, w1)).fetchone()
if w2: if w2:
w2 = w2[0] w2 = w2[0]
v2 = None v2 = None
if w2: if w2:
v2 = d2.execute( v2 = d2.execute(
"select v from mt where w = ? and +k = ?", (w2, k) "select v from mt where w = ? and +k = ?", (w2[:16], k)
).fetchone() ).fetchone()
if v2: if v2:
v2 = v2[0] v2 = v2[0]
@@ -124,7 +137,7 @@ def compare(n1, d1, n2, d2, verbose):
for k, v in sorted(miss.items()): for k, v in sorted(miss.items()):
if v: if v:
print(f"{n1} has {v:6} more {k:<6} tags than {n2}") print(f"{n1} has {v:7} more {k:<7} tags than {n2}")
print(f"in total, {nmiss} missing tags in {n2}\n") print(f"in total, {nmiss} missing tags in {n2}\n")
@@ -132,47 +145,75 @@ def compare(n1, d1, n2, d2, verbose):
def copy_mtp(d1, d2, tag, rm): def copy_mtp(d1, d2, tag, rm):
nt = next(d1.execute("select count(w) from mt where k = ?", (tag,)))[0] nt = next(d1.execute("select count(w) from mt where k = ?", (tag,)))[0]
n = 0 n = 0
ndone = 0 ncopy = 0
for w1, k, v in d1.execute("select * from mt where k = ?", (tag,)): nskip = 0
for w1s, k, v in d1.execute("select * from mt where k = ?", (tag,)):
n += 1 n += 1
if n % 25_000 == 0: if n % 25_000 == 0:
m = f"\033[36m{n:,} of {nt:,} tags checked, so far {ndone} copied\033[0m" m = f"\033[36m{n:,} of {nt:,} tags checked, so far {ncopy} copied, {nskip} skipped\033[0m"
print(m) print(m)
q = "select rd, fn from up where substr(w,1,16) = ?" q = "select w, rd, fn from up where substr(w,1,16) = ?"
rd, fn = d1.execute(q, (w1,)).fetchone() w1, rd, fn = d1.execute(q, (w1s,)).fetchone()
if rd.split("/", 1)[0] == ".hist": if rd.split("/", 1)[0] == ".hist":
continue continue
q = "select substr(w,1,16) from up where rd = ? and fn = ?" if BY_PATH:
w2 = d2.execute(q, (rd, fn)).fetchone() q = "select w from up where rd = ? and fn = ?"
w2 = d2.execute(q, (rd, fn)).fetchone()
else:
q = "select w from up where substr(w,1,16) = ? and +w = ?"
w2 = d2.execute(q, (w1s, w1)).fetchone()
if not w2: if not w2:
continue continue
w2 = w2[0] w2s = w2[0][:16]
hit = d2.execute("select v from mt where w = ? and +k = ?", (w2, k)).fetchone() hit = d2.execute("select v from mt where w = ? and +k = ?", (w2s, k)).fetchone()
if hit: if hit:
hit = hit[0] hit = hit[0]
if hit != v: if hit != v:
ndone += 1 if NC and hit is not None:
if hit is not None: nskip += 1
d2.execute("delete from mt where w = ? and +k = ?", (w2, k)) continue
d2.execute("insert into mt values (?,?,?)", (w2, k, v)) ncopy += 1
if hit is not None:
d2.execute("delete from mt where w = ? and +k = ?", (w2s, k))
d2.execute("insert into mt values (?,?,?)", (w2s, k, v))
if rm: if rm:
d2.execute("delete from mt where w = ? and +k = 't:mtp'", (w2,)) d2.execute("delete from mt where w = ? and +k = 't:mtp'", (w2s,))
d2.commit() d2.commit()
print(f"copied {ndone} {tag} tags over") print(f"copied {ncopy} {tag} tags over, skipped {nskip}")
def examples():
print(
"""
# clearing the journal
./dbtool.py up2k.db
# copy tags ".bpm" and "key" from old.db to up2k.db, and remove the mtp flag from matching files (so copyparty won't run any mtps on it)
./dbtool.py -ls up2k.db
./dbtool.py -src old.db up2k.db -cmp
./dbtool.py -src old.v3 up2k.db -rm-mtp-flag -copy key
./dbtool.py -src old.v3 up2k.db -rm-mtp-flag -copy .bpm -vac
"""
)
def main(): def main():
global NC, BY_PATH
os.system("") os.system("")
print() print()
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument("db", help="database to work on") ap.add_argument("db", help="database to work on")
ap.add_argument("-h2", action="store_true", help="show examples")
ap.add_argument("-src", metavar="DB", type=str, help="database to copy from") ap.add_argument("-src", metavar="DB", type=str, help="database to copy from")
ap2 = ap.add_argument_group("informational / read-only stuff") ap2 = ap.add_argument_group("informational / read-only stuff")
@@ -185,11 +226,29 @@ def main():
ap2.add_argument( ap2.add_argument(
"-rm-mtp-flag", "-rm-mtp-flag",
action="store_true", action="store_true",
help="when an mtp tag is copied over, also mark that as done, so copyparty won't run mtp on it", help="when an mtp tag is copied over, also mark that file as done, so copyparty won't run any mtps on those files",
) )
ap2.add_argument("-vac", action="store_true", help="optimize DB") ap2.add_argument("-vac", action="store_true", help="optimize DB")
ap2 = ap.add_argument_group("behavior modifiers")
ap2.add_argument(
"-nc",
action="store_true",
help="no-clobber; don't replace/overwrite existing tags",
)
ap2.add_argument(
"-by-path",
action="store_true",
help="match files based on location rather than warks (content-hash), use this if the databases have different wark salts",
)
ar = ap.parse_args() ar = ap.parse_args()
if ar.h2:
examples()
return
NC = ar.nc
BY_PATH = ar.by_path
for v in [ar.db, ar.src]: for v in [ar.db, ar.src]:
if v and not os.path.exists(v): if v and not os.path.exists(v):

View File

@@ -29,6 +29,7 @@ however if your copyparty is behind a reverse-proxy, you may want to use [`share
### [`cfssl.sh`](cfssl.sh) ### [`cfssl.sh`](cfssl.sh)
* creates CA and server certificates using cfssl * creates CA and server certificates using cfssl
* give a 3rd argument to install it to your copyparty config * give a 3rd argument to install it to your copyparty config
* systemd service at [`systemd/cfssl.service`](systemd/cfssl.service)
# OS integration # OS integration
init-scripts to start copyparty as a service init-scripts to start copyparty as a service

View File

@@ -7,7 +7,7 @@ srv_fqdn="$2"
[ -z "$srv_fqdn" ] && { [ -z "$srv_fqdn" ] && {
echo "need arg 1: ca name" echo "need arg 1: ca name"
echo "need arg 2: server fqdn" echo "need arg 2: server fqdn and/or IPs, comma-separated"
echo "optional arg 3: if set, write cert into copyparty cfg" echo "optional arg 3: if set, write cert into copyparty cfg"
exit 1 exit 1
} }

View File

@@ -14,7 +14,6 @@ save one of these as `.epilogue.html` inside a folder to customize it:
## example browser-css ## example browser-css
point `--css-browser` to one of these by URL: point `--css-browser` to one of these by URL:
* [`browser.css`](browser.css) changes the background
* [`browser-icons.css`](browser-icons.css) adds filetype icons * [`browser-icons.css`](browser-icons.css) adds filetype icons

View File

@@ -1,30 +0,0 @@
html {
background: #222 url('/wp/wallhaven-mdjrqy.jpg') center / cover no-repeat fixed;
}
#files th {
background: rgba(32, 32, 32, 0.9) !important;
}
#ops,
#tree,
#files td {
background: rgba(32, 32, 32, 0.3) !important;
}
html.light {
background: #eee url('/wp/wallhaven-dpxl6l.png') center / cover no-repeat fixed;
}
html.light #files th {
background: rgba(255, 255, 255, 0.9) !important;
}
html.light .logue,
html.light #ops,
html.light #tree,
html.light #files td {
background: rgba(248, 248, 248, 0.8) !important;
}
#files * {
background: transparent !important;
}

View File

@@ -0,0 +1,23 @@
# systemd service which generates a new TLS certificate on each boot,
# that way the one-year expiry time won't cause any issues --
# just have everyone trust the ca.pem once every 10 years
#
# assumptions/placeholder values:
# * this script and copyparty runs as user "cpp"
# * copyparty repo is at ~cpp/dev/copyparty
# * CA is named partylan
# * server IPs = 10.1.2.3 and 192.168.123.1
# * server hostname = party.lan
[Unit]
Description=copyparty certificate generator
Before=copyparty.service
[Service]
User=cpp
Type=oneshot
SyslogIdentifier=cpp-cert
ExecStart=/bin/bash -c 'cd ~/dev/copyparty/contrib && ./cfssl.sh partylan 10.1.2.3,192.168.123.1,party.lan y'
[Install]
WantedBy=multi-user.target

View File

@@ -2,16 +2,22 @@
# and share '/mnt' with anonymous read+write # and share '/mnt' with anonymous read+write
# #
# installation: # installation:
# cp -pv copyparty.service /etc/systemd/system && systemctl enable --now copyparty # 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
# firewall-cmd --reload # firewall-cmd --reload
# systemctl daemon-reload && systemctl enable --now copyparty
# #
# you may want to: # you may want to:
# change "User=cpp" and "/home/cpp/" to another user
# remove the nft lines to only listen on port 3923
# and in the ExecStart= line:
# change '/usr/bin/python3' to another interpreter # change '/usr/bin/python3' to another interpreter
# change '/mnt::rw' to another location or permission-set # change '/mnt::rw' to another location or permission-set
# remove '-p 80,443,3923' to only listen on port 3923 # add '-q' to disable logging on busy servers
# add '-i 127.0.0.1' to only allow local connections # add '-i 127.0.0.1' to only allow local connections
# add '-e2dsa' to enable filesystem scanning + indexing
# add '-e2ts' to enable metadata indexing
# #
# with `Type=notify`, copyparty will signal systemd when it is ready to # with `Type=notify`, copyparty will signal systemd when it is ready to
# accept connections; correctly delaying units depending on copyparty. # accept connections; correctly delaying units depending on copyparty.
@@ -19,9 +25,11 @@
# python disabling line-buffering, so messages are out-of-order: # python disabling line-buffering, so messages are out-of-order:
# https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png # https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png
# #
# if you remove -q to enable logging, you may also want to remove the # unless you add -q to disable logging, you may want to remove the
# following line to enable buffering (slightly better performance): # following line to allow buffering (slightly better performance):
# Environment=PYTHONUNBUFFERED=x # Environment=PYTHONUNBUFFERED=x
#
# keep ExecStartPre before ExecStart, at least on rhel8
[Unit] [Unit]
Description=copyparty file server Description=copyparty file server
@@ -31,8 +39,23 @@ Type=notify
SyslogIdentifier=copyparty SyslogIdentifier=copyparty
Environment=PYTHONUNBUFFERED=x Environment=PYTHONUNBUFFERED=x
ExecReload=/bin/kill -s USR1 $MAINPID ExecReload=/bin/kill -s USR1 $MAINPID
ExecStartPre=/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -p 80,443,3923 -v /mnt::rw # user to run as + where the TLS certificate is (if any)
User=cpp
Environment=XDG_CONFIG_HOME=/home/cpp/.config
# setup forwarding from ports 80 and 443 to port 3923
ExecStartPre=+/bin/bash -c 'nft -n -a list table nat | awk "/ to :3923 /{print\$NF}" | xargs -rL1 nft delete rule nat prerouting handle; true'
ExecStartPre=+nft add table ip nat
ExecStartPre=+nft -- add chain ip nat prerouting { type nat hook prerouting priority -100 \; }
ExecStartPre=+nft add rule ip nat prerouting tcp dport 80 redirect to :3923
ExecStartPre=+nft add rule ip nat prerouting tcp dport 443 redirect to :3923
# stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
# copyparty settings
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -e2d -v /mnt::rw
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -291,9 +291,9 @@ def run_argparse(argv, formatter):
dedent( dedent(
""" """
-a takes username:password, -a takes username:password,
-v takes src:dst:perm1:perm2:permN:volflag1:volflag2:volflagN:... -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:...
where "perm" is "permissions,username1,username2,..." * "\033[33mperm\033[0m" is "permissions,username1,username2,..."
and "volflag" 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
@@ -365,6 +365,17 @@ def run_argparse(argv, formatter):
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[36mdthumb\033[35m disables all thumbnails
\033[36mdvthumb\033[35m disables video thumbnails
\033[36mdathumb\033[35m disables audio thumbnails (spectrograms)
\033[36mdithumb\033[35m disables image thumbnails
\033[0mclient and ux:
\033[36mhtml_head=TXT\033[35m includes TXT in the <head>
\033[36mrobots\033[35m allows indexing by search engines (default)
\033[36mnorobots\033[35m kindly asks search engines to leave
\033[0mothers: \033[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
@@ -373,7 +384,7 @@ def run_argparse(argv, formatter):
], ],
[ [
"urlform", "urlform",
"", "how to handle url-form POSTs",
dedent( dedent(
""" """
values for --urlform: values for --urlform:
@@ -412,38 +423,41 @@ def run_argparse(argv, formatter):
ap2.add_argument("-c", metavar="PATH", type=u, action="append", help="add config file") ap2.add_argument("-c", metavar="PATH", type=u, action="append", help="add config file")
ap2.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients") ap2.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients")
ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores, 0=all") ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores, 0=all")
ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="add account, USER:PASS; example [ed:wark") ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="add account, USER:PASS; example [ed:wark]")
ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="add volume, SRC:DST:FLAG; example [.::r], [/mnt/nas/music:/music:r:aed") ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="add volume, SRC:DST:FLAG; examples [.::r], [/mnt/nas/music:/music:r:aed]")
ap2.add_argument("-ed", action="store_true", help="enable ?dots") ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files")
ap2.add_argument("-emp", action="store_true", help="enable markdown plugins") ap2.add_argument("-emp", action="store_true", help="enable markdown plugins -- neat but dangerous, big XSS risk")
ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate") ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate")
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,get", help="how to handle url-forms; examples: [stash], [save,get]") ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,get", help="how to handle url-form POSTs; see --help-urlform")
ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="window title, for example '$ip-10.1.2.' or '$ip-'") ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="window title, for example '$ip-10.1.2.' or '$ip-'")
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") 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="up2k min.size threshold (mswin-only)") 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("--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") 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("--no-symlink", action="store_true", help="duplicate file contents instead") 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("--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("--reg-cap", metavar="N", type=int, default=9000, help="max number of uploads to keep in memory when running without -e2d")
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 = 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.)")
ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to bind (comma/range)") ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to bind (comma/range)")
ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to keep; 0 = tcp, 1 = origin (first x-fwd), 2 = cloudflare, 3 = nginx, -1 = closest proxy") ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to keep; 0 = tcp, 1 = origin (first x-fwd), 2 = cloudflare, 3 = nginx, -1 = closest proxy")
ap2.add_argument("--s-wr-sz", metavar="B", type=int, default=256*1024, help="socket write size in bytes") ap2.add_argument("--s-wr-sz", metavar="B", type=int, default=256*1024, help="socket write size in bytes")
ap2.add_argument("--s-wr-slp", metavar="SEC", type=float, default=0, help="socket write delay in seconds") ap2.add_argument("--s-wr-slp", metavar="SEC", type=float, default=0, help="debug: socket write delay in seconds")
ap2.add_argument("--rsp-slp", metavar="SEC", type=float, default=0, help="response delay in seconds") ap2.add_argument("--rsp-slp", metavar="SEC", type=float, default=0, help="debug: response delay in seconds")
ap2 = ap.add_argument_group('SSL/TLS options') ap2 = ap.add_argument_group('SSL/TLS options')
ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls") ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls -- force plaintext")
ap2.add_argument("--https-only", action="store_true", help="disable plaintext") ap2.add_argument("--https-only", action="store_true", help="disable plaintext -- force tls")
ap2.add_argument("--ssl-ver", metavar="LIST", type=u, help="set allowed ssl/tls versions; [help] shows available versions; default is what your python version considers safe") ap2.add_argument("--ssl-ver", metavar="LIST", type=u, help="set allowed ssl/tls versions; [help] shows available versions; default is what your python version considers safe")
ap2.add_argument("--ciphers", metavar="LIST", type=u, help="set allowed ssl/tls ciphers; [help] shows available ciphers") ap2.add_argument("--ciphers", metavar="LIST", type=u, help="set allowed ssl/tls ciphers; [help] shows available ciphers")
ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info") ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
ap2.add_argument("--ssl-log", metavar="PATH", type=u, help="log master secrets") ap2.add_argument("--ssl-log", metavar="PATH", type=u, help="log master secrets for later decryption in wireshark")
ap2 = ap.add_argument_group('FTP options') ap2 = ap.add_argument_group('FTP options')
ap2.add_argument("--ftp", metavar="PORT", type=int, help="enable FTP server on PORT, for example 3921") ap2.add_argument("--ftp", metavar="PORT", type=int, help="enable FTP server on PORT, for example 3921")
@@ -453,26 +467,27 @@ def run_argparse(argv, formatter):
ap2.add_argument("--ftp-pr", metavar="P-P", type=u, help="the range of TCP ports to use for passive connections, for example 12000-13000") ap2.add_argument("--ftp-pr", metavar="P-P", type=u, help="the range of TCP ports to use for passive connections, for example 12000-13000")
ap2 = ap.add_argument_group('opt-outs') ap2 = ap.add_argument_group('opt-outs')
ap2.add_argument("-nw", action="store_true", help="disable writes (benchmark)") ap2.add_argument("-nw", action="store_true", help="never write anything to disk (debug/benchmark)")
ap2.add_argument("--keep-qem", action="store_true", help="do not disable quick-edit-mode on windows") ap2.add_argument("--keep-qem", action="store_true", help="do not disable quick-edit-mode on windows (it is disabled to avoid accidental text selection which will deadlock copyparty)")
ap2.add_argument("--no-del", action="store_true", help="disable delete operations") ap2.add_argument("--no-del", action="store_true", help="disable delete operations")
ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations") ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations")
ap2.add_argument("-nih", action="store_true", help="no info hostname") ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI")
ap2.add_argument("-nid", action="store_true", help="no info disk-usage") ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI")
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar") ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
ap2.add_argument("--no-lifetime", action="store_true", help="disable automatic deletion of uploads after a certain time (lifetime volflag)") ap2.add_argument("--no-lifetime", action="store_true", help="disable automatic deletion of uploads after a certain time (lifetime volflag)")
ap2 = ap.add_argument_group('safety options') ap2 = ap.add_argument_group('safety options')
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, help="scan all volumes; 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") 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") ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files -- this one DOES matter")
ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles") ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles")
ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to make something a dotfile") ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to make something a dotfile")
ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings") ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings")
ap2.add_argument("--no-readme", action="store_true", help="disable rendering readme.md into directory listings") ap2.add_argument("--no-readme", action="store_true", help="disable rendering readme.md into directory listings")
ap2.add_argument("--vague-403", action="store_true", help="send 404 instead of 403 (security through ambiguity, very enterprise)") ap2.add_argument("--vague-403", action="store_true", help="send 404 instead of 403 (security through ambiguity, very enterprise)")
ap2.add_argument("--force-js", action="store_true", help="don't send HTML folder listings, force clients to use the embedded json instead") ap2.add_argument("--force-js", action="store_true", help="don't send folder listings as HTML, force clients to use the embedded json instead -- slight protection against misbehaving search engines which ignore --no-robots")
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 = ap.add_argument_group('yolo options') ap2 = ap.add_argument_group('yolo 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")
@@ -482,8 +497,8 @@ def run_argparse(argv, formatter):
ap2.add_argument("-q", action="store_true", help="quiet") ap2.add_argument("-q", action="store_true", help="quiet")
ap2.add_argument("-lo", metavar="PATH", type=u, help="logfile, example: cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz") ap2.add_argument("-lo", metavar="PATH", type=u, help="logfile, example: cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz")
ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup") ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup")
ap2.add_argument("--log-conn", action="store_true", help="print tcp-server msgs") ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
ap2.add_argument("--log-htp", action="store_true", help="print http-server threadpool scaling") ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="dump incoming header") ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="dump incoming header")
ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|\?th=[wj]$", help="dont log URLs matching") ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|\?th=[wj]$", help="dont log URLs matching")
@@ -500,55 +515,68 @@ def run_argparse(argv, formatter):
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=cores, help="num cpu cores to use for generating thumbnails") ap2.add_argument("--th-mt", metavar="CORES", type=int, default=cores, help="num cpu cores to use for generating thumbnails")
ap2.add_argument("--th-convt", metavar="SEC", type=int, default=60, help="conversion timeout in seconds") ap2.add_argument("--th-convt", metavar="SEC", type=int, default=60, help="conversion timeout in seconds")
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image") ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image")
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference")
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output") ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output") ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg for video thumbs") ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg output for video thumbs")
ap2.add_argument("--th-ff-swr", action="store_true", help="use swresample instead of soxr for audio thumbs") ap2.add_argument("--th-ff-swr", action="store_true", help="use swresample instead of soxr for audio thumbs")
ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown") ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown -- avoids doing keepalive pokes (updating the mtime) on thumbnail folders more often than SEC seconds")
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled") ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled")
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age") ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than --th-poke seconds will get deleted every --th-clean seconds")
ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat for") ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for")
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
# https://github.com/libvips/libvips
# ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="bmp,dib,gif,icns,ico,jpg,jpeg,jp2,jpx,pcx,png,pbm,pgm,ppm,pnm,sgi,tga,tif,tiff,webp,xbm,dds,xpm,heif,heifs,heic,heics,avif,avifs", help="image formats to decode using pillow")
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="jpg,jpeg,jp2,jpx,jxl,tif,tiff,png,webp,heic,avif,fit,fits,fts,exr,svg,hdr,ppm,pgm,pfm,gif,nii", help="image formats to decode using pyvips")
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,dds,dib,fit,fits,fts,gif,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="av1,asf,avi,flv,m4v,mkv,mjpeg,mjpg,mpg,mpeg,mpg2,mpeg2,h264,avc,mts,h265,hevc,mov,3gp,mp4,ts,mpegts,nut,ogv,ogm,rm,vob,webm,wmv", help="video formats to decode using ffmpeg")
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,m4a,ogg,opus,flac,alac,mp3,mp2,ac3,dts,wma,ra,wav,aif,aiff,au,alaw,ulaw,mulaw,amr,gsm,ape,tak,tta,wv,mpc", help="audio formats to decode using ffmpeg")
ap2 = ap.add_argument_group('transcoding options') ap2 = ap.add_argument_group('transcoding options')
ap2.add_argument("--no-acode", action="store_true", help="disable audio transcoding") ap2.add_argument("--no-acode", action="store_true", help="disable audio transcoding")
ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete transcode output after SEC seconds") ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete cached transcode output after SEC seconds")
ap2 = ap.add_argument_group('general db options') ap2 = ap.add_argument_group('general db options')
ap2.add_argument("-e2d", action="store_true", help="enable up2k database") ap2.add_argument("-e2d", action="store_true", help="enable up2k database, making files searchable + enables upload deduplocation")
ap2.add_argument("-e2ds", action="store_true", help="enable up2k db-scanner, 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="scan all folders (for search), sets -e2ds") ap2.add_argument("-e2dsa", action="store_true", help="scans all folders on startup; sets -e2ds")
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("--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("--srch-time", metavar="SEC", type=int, default=30, help="search deadline") ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline -- terminate searches running for more than SEC seconds")
ap2.add_argument("--srch-hits", metavar="N", type=int, default=1000, help="max search results") 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") 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="enable metadata scanner, sets -e2t") ap2.add_argument("-e2ts", action="store_true", help="scan existing files on startup; sets -e2t")
ap2.add_argument("-e2tsr", action="store_true", help="rescan all metadata, sets -e2ts") ap2.add_argument("-e2tsr", action="store_true", help="delete all metadata from DB and do a full rescan; sets -e2ts")
ap2.add_argument("--no-mutagen", action="store_true", help="use FFprobe for tags instead") ap2.add_argument("--no-mutagen", action="store_true", help="use FFprobe for tags instead; will catch more tags")
ap2.add_argument("--no-mtag-ff", action="store_true", help="never use FFprobe as tag reader") 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("-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,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,res,.fps")
ap2.add_argument("-mtp", metavar="M=[f,]bin", type=u, action="append", help="read tag M using bin") 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("--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("--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")
ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext") ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext")
ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty", help="title / service-name to show in html documents") ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty", help="title / service-name to show in html documents")
ap2 = ap.add_argument_group('debug options') ap2 = ap.add_argument_group('debug options')
ap2.add_argument("--no-sendfile", action="store_true", help="disable sendfile") ap2.add_argument("--no-sendfile", action="store_true", help="disable sendfile; instead using a traditional file read loop")
ap2.add_argument("--no-scandir", action="store_true", help="disable scandir") ap2.add_argument("--no-scandir", action="store_true", help="disable scandir; instead using listdir + stat on each file")
ap2.add_argument("--no-fastboot", action="store_true", help="wait for up2k indexing") ap2.add_argument("--no-fastboot", action="store_true", help="wait for up2k indexing before starting the httpd")
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")

View File

@@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (1, 2, 3) VERSION = (1, 3, 0)
CODENAME = "ftp btw" CODENAME = "god dag"
BUILD_DT = (2022, 3, 24) BUILD_DT = (2022, 5, 22)
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)

View File

@@ -11,12 +11,13 @@ import hashlib
import threading import threading
from datetime import datetime from datetime import datetime
from .__init__ import WINDOWS from .__init__ import ANYWIN, WINDOWS
from .util import ( from .util import (
IMPLICATIONS, IMPLICATIONS,
META_NOBOTS, META_NOBOTS,
uncyg, uncyg,
undot, undot,
relchk,
unhumanize, unhumanize,
absreal, absreal,
Pebkac, Pebkac,
@@ -335,6 +336,12 @@ class VFS(object):
): ):
# type: (str, str, bool, bool, bool, bool, bool) -> tuple[VFS, str] # type: (str, str, bool, bool, bool, bool, bool) -> tuple[VFS, str]
"""returns [vfsnode,fs_remainder] if user has the requested permissions""" """returns [vfsnode,fs_remainder] if user has the requested permissions"""
if ANYWIN:
mod = relchk(vpath)
if mod:
self.log("vfs", "invalid relpath [{}]".format(vpath))
raise Pebkac(404)
vn, rem = self._find(vpath) vn, rem = self._find(vpath)
c = vn.axs c = vn.axs
@@ -404,7 +411,7 @@ class VFS(object):
return [abspath, real, virt_vis] return [abspath, real, virt_vis]
def walk(self, rel, rem, seen, uname, permsets, dots, scandir, lstat): def walk(self, rel, rem, seen, uname, permsets, dots, scandir, lstat, subvols=True):
""" """
recursively yields from ./rem; recursively yields from ./rem;
rel is a unix-style user-defined vpath (not vfs-related) rel is a unix-style user-defined vpath (not vfs-related)
@@ -437,9 +444,14 @@ class VFS(object):
wrel = (rel + "/" + rdir).lstrip("/") wrel = (rel + "/" + rdir).lstrip("/")
wrem = (rem + "/" + rdir).lstrip("/") wrem = (rem + "/" + rdir).lstrip("/")
for x in self.walk(wrel, wrem, seen, uname, permsets, dots, scandir, lstat): for x in self.walk(
wrel, wrem, seen, uname, permsets, dots, scandir, lstat, subvols
):
yield x yield x
if not subvols:
return
for n, vfs in sorted(vfs_virt.items()): for n, vfs in sorted(vfs_virt.items()):
if not dots and n.startswith("."): if not dots and n.startswith("."):
continue continue
@@ -741,10 +753,10 @@ class AuthSrv(object):
unames = ["*"] + list(acct.keys()) unames = ["*"] + list(acct.keys())
umap = {x: [] for x in unames} umap = {x: [] for x in unames}
for usr in unames: for usr in unames:
for mp, vol in vfs.all_vols.items(): for vp, vol in vfs.all_vols.items():
axs = getattr(vol.axs, axs_key) axs = getattr(vol.axs, axs_key)
if usr in axs or "*" in axs: if usr in axs or "*" in axs:
umap[usr].append(mp) umap[usr].append(vp)
umap[usr].sort() umap[usr].sort()
setattr(vfs, "a" + perm, umap) setattr(vfs, "a" + perm, umap)
@@ -875,6 +887,17 @@ class AuthSrv(object):
vol.flags["html_head"] = "\n".join([x for x in h if x]) vol.flags["html_head"] = "\n".join([x for x in h if x])
for vol in vfs.all_vols.values():
if self.args.no_vthumb:
vol.flags["dvthumb"] = True
if self.args.no_athumb:
vol.flags["dathumb"] = True
if self.args.no_thumb or vol.flags.get("dthumb", False):
vol.flags["dthumb"] = True
vol.flags["dvthumb"] = True
vol.flags["dathumb"] = True
vol.flags["dithumb"] = True
for vol in vfs.all_vols.values(): for vol in vfs.all_vols.values():
fk = vol.flags.get("fk") fk = vol.flags.get("fk")
if fk: if fk:
@@ -1089,7 +1112,7 @@ class AuthSrv(object):
flag_p = "p" in flags flag_p = "p" in flags
flag_r = "r" in flags flag_r = "r" in flags
n_bads = 0 bads = []
for v in vols: for v in vols:
v = v[1:] v = v[1:]
vtop = "/{}/".format(v) if v else "/" vtop = "/{}/".format(v) if v else "/"
@@ -1101,10 +1124,19 @@ class AuthSrv(object):
continue continue
atop = vn.realpath atop = vn.realpath
safeabs = atop + os.sep
g = vn.walk( g = vn.walk(
vn.vpath, "", [], u, [[True]], True, not self.args.no_scandir, False vn.vpath,
"",
[],
u,
[[True]],
True,
not self.args.no_scandir,
False,
False,
) )
for _, _, vpath, apath, files, _, _ in g: for _, _, vpath, apath, files, dirs, _ in g:
fnames = [n[0] for n in files] fnames = [n[0] for n in files]
vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames
vpaths = [vtop + x for x in vpaths] vpaths = [vtop + x for x in vpaths]
@@ -1112,21 +1144,28 @@ class AuthSrv(object):
files = [[vpath + "/", apath + os.sep]] + list(zip(vpaths, apaths)) files = [[vpath + "/", apath + os.sep]] + list(zip(vpaths, apaths))
if flag_ln: if flag_ln:
files = [x for x in files if not x[1].startswith(atop + os.sep)] files = [x for x in files if not x[1].startswith(safeabs)]
n_bads += len(files) if files:
dirs[:] = [] # stop recursion
bads.append(files[0][0])
if flag_v: if not files:
msg = [ continue
elif flag_v:
msg = [""] + [
'# user "{}", vpath "{}"\n{}'.format(u, vp, ap) '# user "{}", vpath "{}"\n{}'.format(u, vp, ap)
for vp, ap in files for vp, ap in files
] ]
else: else:
msg = [x[1] for x in files] msg = ["user {}, vol {}: {} =>".format(u, vtop, files[0][0])]
msg += [x[1] for x in files]
if msg: self.log("\n".join(msg))
self.log("\n" + "\n".join(msg))
if n_bads and flag_p: if bads:
self.log("\n ".join(["found symlinks leaving volume:"] + bads))
if bads and flag_p:
raise Exception("found symlink leaving volume, and strict is set") raise Exception("found symlink leaving volume, and strict is set")
if not flag_r: if not flag_r:

View File

@@ -121,6 +121,12 @@ class HttpCli(object):
try: try:
self.mode, self.req, self.http_ver = headerlines[0].split(" ") self.mode, self.req, self.http_ver = headerlines[0].split(" ")
# normalize incoming headers to lowercase;
# outgoing headers however are Correct-Case
for header_line in headerlines[1:]:
k, v = header_line.split(":", 1)
self.headers[k.lower()] = v.strip()
except: except:
msg = " ]\n#[ ".join(headerlines) msg = " ]\n#[ ".join(headerlines)
raise Pebkac(400, "bad headers:\n#[ " + msg + " ]") raise Pebkac(400, "bad headers:\n#[ " + msg + " ]")
@@ -137,11 +143,9 @@ class HttpCli(object):
if self.args.rsp_slp: if self.args.rsp_slp:
time.sleep(self.args.rsp_slp) time.sleep(self.args.rsp_slp)
# normalize incoming headers to lowercase; self.ua = self.headers.get("user-agent", "")
# outgoing headers however are Correct-Case self.is_rclone = self.ua.startswith("rclone/")
for header_line in headerlines[1:]: self.is_ancient = self.ua.startswith("Mozilla/4.")
k, v = header_line.split(":", 1)
self.headers[k.lower()] = v.strip()
v = self.headers.get("connection", "").lower() v = self.headers.get("connection", "").lower()
self.keepalive = not v.startswith("close") and self.http_ver != "HTTP/1.0" self.keepalive = not v.startswith("close") and self.http_ver != "HTTP/1.0"
@@ -211,6 +215,14 @@ class HttpCli(object):
self.cookies = cookies self.cookies = cookies
self.vpath = unquotep(vpath) # not query, so + means + self.vpath = unquotep(vpath) # not query, so + means +
ok = "\x00" not in self.vpath
if ANYWIN:
ok = ok and not relchk(self.vpath)
if not ok:
self.log("invalid relpath [{}]".format(self.vpath))
return self.tx_404() and self.keepalive
pwd = None pwd = None
ba = self.headers.get("authorization") ba = self.headers.get("authorization")
if ba: if ba:
@@ -233,11 +245,9 @@ class HttpCli(object):
self.dvol = self.asrv.vfs.adel[self.uname] self.dvol = self.asrv.vfs.adel[self.uname]
self.gvol = self.asrv.vfs.aget[self.uname] self.gvol = self.asrv.vfs.aget[self.uname]
if pwd and "pw" in self.ouparam and pwd != cookies.get("cppwd"): if pwd:
self.out_headerlist.append(("Set-Cookie", self.get_pwd_cookie(pwd)[0])) self.out_headerlist.append(("Set-Cookie", self.get_pwd_cookie(pwd)[0]))
self.ua = self.headers.get("user-agent", "")
self.is_rclone = self.ua.startswith("rclone/")
if self.is_rclone: if self.is_rclone:
uparam["raw"] = False uparam["raw"] = False
uparam["dots"] = False uparam["dots"] = False
@@ -274,10 +284,11 @@ class HttpCli(object):
msg = str(ex) if pex == ex else min_ex() msg = str(ex) if pex == ex else min_ex()
self.log("{}\033[0m, {}".format(msg, self.vpath), 3) self.log("{}\033[0m, {}".format(msg, self.vpath), 3)
msg = "<pre>{}\r\nURL: {}\r\n".format(str(ex), self.vpath) msg = "{}\r\nURL: {}\r\n".format(str(ex), self.vpath)
if self.hint: if self.hint:
msg += "hint: {}\r\n".format(self.hint) msg += "hint: {}\r\n".format(self.hint)
msg = "<pre>" + html_escape(msg)
self.reply(msg.encode("utf-8", "replace"), status=pex.code, volsan=True) self.reply(msg.encode("utf-8", "replace"), status=pex.code, volsan=True)
return self.keepalive return self.keepalive
except Pebkac: except Pebkac:
@@ -344,8 +355,11 @@ class HttpCli(object):
return body return body
def loud_reply(self, body, *args, **kwargs): def loud_reply(self, body, *args, **kwargs):
if not kwargs.get("mime"):
kwargs["mime"] = "text/plain; charset=utf-8"
self.log(body.rstrip()) self.log(body.rstrip())
self.reply(b"<pre>" + body.encode("utf-8") + b"\r\n", *list(args), **kwargs) self.reply(body.encode("utf-8") + b"\r\n", *list(args), **kwargs)
def urlq(self, add, rm): def urlq(self, add, rm):
""" """
@@ -864,8 +878,9 @@ class HttpCli(object):
else: else:
# search by query params # search by query params
q = body["q"] q = body["q"]
self.log("qj: " + q) n = body.get("n", self.args.srch_hits)
hits, taglist = idx.search(vols, q) self.log("qj: {} |{}|".format(q, n))
hits, taglist = idx.search(vols, q, n)
msg = len(hits) msg = len(hits)
idx.p_end = time.time() idx.p_end = time.time()
@@ -995,9 +1010,15 @@ class HttpCli(object):
pwd = self.parser.require("cppwd", 64) pwd = self.parser.require("cppwd", 64)
self.parser.drop() self.parser.drop()
dst = "/?h" self.out_headerlist = [
x
for x in self.out_headerlist
if x[0] != "Set-Cookie" or "cppwd=" not in x[1]
]
dst = "/"
if self.vpath: if self.vpath:
dst = "/" + quotep(self.vpath) dst += quotep(self.vpath)
ck, msg = self.get_pwd_cookie(pwd) ck, msg = self.get_pwd_cookie(pwd)
html = self.j2("msg", h1=msg, h2='<a href="' + dst + '">ack</a>', redir=dst) html = self.j2("msg", h1=msg, h2='<a href="' + dst + '">ack</a>', redir=dst)
@@ -1007,13 +1028,17 @@ class HttpCli(object):
def get_pwd_cookie(self, pwd): def get_pwd_cookie(self, pwd):
if pwd in self.asrv.iacct: if pwd in self.asrv.iacct:
msg = "login ok" msg = "login ok"
dur = 60 * 60 * 24 * 365 dur = int(60 * 60 * self.args.logout)
else: else:
msg = "naw dude" msg = "naw dude"
pwd = "x" # nosec pwd = "x" # nosec
dur = None dur = None
return [gencookie("cppwd", pwd, dur), msg] r = gencookie("cppwd", pwd, dur)
if self.is_ancient:
r = r.rsplit(" ", 1)[0]
return [r, msg]
def handle_mkdir(self): def handle_mkdir(self):
new_dir = self.parser.require("name", 512) new_dir = self.parser.require("name", 512)
@@ -1046,6 +1071,7 @@ class HttpCli(object):
raise Pebkac(500, min_ex()) raise Pebkac(500, min_ex())
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/") vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
self.out_headers["X-New-Dir"] = quotep(sanitized)
self.redirect(vpath) self.redirect(vpath)
return True return True
@@ -1801,15 +1827,17 @@ class HttpCli(object):
self.redirect("", "?h#cc") self.redirect("", "?h#cc")
def tx_404(self, is_403=False): def tx_404(self, is_403=False):
rc = 404
if self.args.vague_403: if self.args.vague_403:
m = '<h1>404 not found &nbsp;┐( ´ -`)┌</h1><p>or maybe you don\'t have access -- try logging in or <a href="/?h">go home</a></p>' m = '<h1 id="n">404 not found &nbsp;┐( ´ -`)┌</h1><p id="o">or maybe you don\'t have access -- try logging in or <a href="/?h">go home</a></p>'
elif is_403: elif is_403:
m = '<h1>403 forbiddena &nbsp;~┻━┻</h1><p>you\'ll have to log in or <a href="/?h">go home</a></p>' m = '<h1 id="p">403 forbiddena &nbsp;~┻━┻</h1><p id="q">you\'ll have to log in or <a href="/?h">go home</a></p>'
rc = 403
else: else:
m = '<h1>404 not found &nbsp;┐( ´ -`)┌</h1><p><a href="/?h">go home</a></p>' m = '<h1 id="n">404 not found &nbsp;┐( ´ -`)┌</h1><p><a id="r" href="/?h">go home</a></p>'
html = self.j2("splash", this=self, qvpath=quotep(self.vpath), msg=m) html = self.j2("splash", this=self, qvpath=quotep(self.vpath), msg=m)
self.reply(html.encode("utf-8"), status=404) self.reply(html.encode("utf-8"), status=rc)
return True return True
def scanvol(self): def scanvol(self):
@@ -1852,7 +1880,7 @@ class HttpCli(object):
if self.args.no_stack: if self.args.no_stack:
raise Pebkac(403, "the stackdump feature is disabled in server config") raise Pebkac(403, "the stackdump feature is disabled in server config")
ret = "<pre>{}\n{}".format(time.time(), alltrace()) ret = "<pre>{}\n{}".format(time.time(), html_escape(alltrace()))
self.reply(ret.encode("utf-8")) self.reply(ret.encode("utf-8"))
def tx_tree(self): def tx_tree(self):
@@ -2097,9 +2125,7 @@ class HttpCli(object):
thp = None thp = None
if self.thumbcli: if self.thumbcli:
thp = self.thumbcli.get( thp = self.thumbcli.get(dbv, vrem, int(st.st_mtime), th_fmt)
dbv.realpath, vrem, int(st.st_mtime), th_fmt
)
if thp: if thp:
return self.tx_file(thp) return self.tx_file(thp)
@@ -2149,12 +2175,11 @@ class HttpCli(object):
free = humansize(sv.f_frsize * sv.f_bfree, True) free = humansize(sv.f_frsize * sv.f_bfree, True)
total = humansize(sv.f_frsize * sv.f_blocks, True) total = humansize(sv.f_frsize * sv.f_blocks, True)
srv_info.append(free + " free") srv_info.append("{} free of {}".format(free, total))
srv_info.append(total)
except: except:
pass pass
srv_info = "</span> /// <span>".join(srv_info) srv_info = "</span> // <span>".join(srv_info)
perms = [] perms = []
if self.can_read: if self.can_read:
@@ -2168,7 +2193,7 @@ class HttpCli(object):
if self.can_get: if self.can_get:
perms.append("get") perms.append("get")
url_suf = self.urlq({}, []) url_suf = self.urlq({}, ["k"])
is_ls = "ls" in self.uparam is_ls = "ls" in self.uparam
is_js = self.args.force_js or self.cookies.get("js") == "y" is_js = self.args.force_js or self.cookies.get("js") == "y"
@@ -2227,6 +2252,10 @@ class HttpCli(object):
"readme": readme, "readme": readme,
"title": html_escape(self.vpath, crlf=True), "title": html_escape(self.vpath, crlf=True),
"srv_info": srv_info, "srv_info": srv_info,
"lang": self.args.lang,
"dtheme": self.args.theme,
"themes": self.args.themes,
"turbolvl": self.args.turbo,
} }
if not self.can_read: if not self.can_read:
if is_ls: if is_ls:
@@ -2385,7 +2414,7 @@ class HttpCli(object):
continue continue
w = r[0][:16] w = r[0][:16]
q = "select k, v from mt where w = ? and k != 'x'" q = "select k, v from mt where w = ? and +k != 'x'"
try: try:
for k, v in icur.execute(q, (w,)): for k, v in icur.execute(q, (w,)):
taglist[k] = True taglist[k] = True
@@ -2410,14 +2439,19 @@ class HttpCli(object):
if doc: if doc:
doc = unquotep(doc.replace("+", " ").split("?")[0]) doc = unquotep(doc.replace("+", " ").split("?")[0])
j2a["docname"] = doc j2a["docname"] = doc
doctxt = None
if next((x for x in files if x["name"] == doc), None): if next((x for x in files if x["name"] == doc), None):
with open(os.path.join(abspath, doc), "rb") as f: docpath = os.path.join(abspath, doc)
doc = f.read().decode("utf-8", "replace") sz = bos.path.getsize(docpath)
if sz < 1024 * self.args.txt_max:
with open(fsenc(docpath), "rb") as f:
doctxt = f.read().decode("utf-8", "replace")
else: else:
self.log("doc 404: [{}]".format(doc), c=6) self.log("doc 404: [{}]".format(doc), c=6)
doc = "( textfile not found )" doctxt = "( textfile not found )"
j2a["doc"] = doc if doctxt is not None:
j2a["doc"] = doctxt
if not self.conn.hsrv.prism: if not self.conn.hsrv.prism:
j2a["no_prism"] = True j2a["no_prism"] = True

View File

@@ -17,7 +17,8 @@ from .util import Unrecv
from .httpcli import HttpCli from .httpcli import HttpCli
from .u2idx import U2idx from .u2idx import U2idx
from .th_cli import ThumbCli from .th_cli import ThumbCli
from .th_srv import HAVE_PIL from .th_srv import HAVE_PIL, HAVE_VIPS
from .mtag import HAVE_FFMPEG
from .ico import Ico from .ico import Ico
@@ -38,7 +39,7 @@ class HttpConn(object):
self.cert_path = hsrv.cert_path self.cert_path = hsrv.cert_path
self.u2fh = hsrv.u2fh self.u2fh = hsrv.u2fh
enth = HAVE_PIL 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 = ThumbCli(hsrv) if enth else None
self.ico = Ico(self.args) self.ico = Ico(self.args)

View File

@@ -70,6 +70,12 @@ class HttpSrv(object):
self.cb_ts = 0 self.cb_ts = 0
self.cb_v = 0 self.cb_v = 0
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 = {

View File

@@ -8,7 +8,7 @@ import shutil
import subprocess as sp import subprocess as sp
from .__init__ import PY2, WINDOWS, unicode from .__init__ import PY2, WINDOWS, unicode
from .util import fsenc, fsdec, uncyg, runcmd, REKOBO_LKEY from .util import fsenc, fsdec, uncyg, runcmd, retchk, REKOBO_LKEY
from .bos import bos from .bos import bos
@@ -82,8 +82,9 @@ def ffprobe(abspath, timeout=10):
b"--", b"--",
fsenc(abspath), fsenc(abspath),
] ]
rc = runcmd(cmd, timeout=timeout) rc, so, se = runcmd(cmd, timeout=timeout)
return parse_ffprobe(rc[1]) retchk(rc, cmd, se)
return parse_ffprobe(so)
def parse_ffprobe(txt): def parse_ffprobe(txt):
@@ -477,13 +478,13 @@ class MTag(object):
env["PYTHONPATH"] = pypath env["PYTHONPATH"] = pypath
ret = {} ret = {}
for tagname, mp in parsers.items(): for tagname, parser in parsers.items():
try: try:
cmd = [mp.bin, abspath] cmd = [parser.bin, abspath]
if mp.bin.endswith(".py"): if parser.bin.endswith(".py"):
cmd = [sys.executable] + cmd cmd = [sys.executable] + cmd
args = {"env": env, "timeout": mp.timeout} args = {"env": env, "timeout": parser.timeout}
if WINDOWS: if WINDOWS:
args["creationflags"] = 0x4000 args["creationflags"] = 0x4000
@@ -491,12 +492,14 @@ class MTag(object):
cmd = ["nice"] + cmd cmd = ["nice"] + cmd
cmd = [fsenc(x) for x in cmd] cmd = [fsenc(x) for x in cmd]
v = sp.check_output(cmd, **args).strip() rc, v, err = runcmd(cmd, **args)
retchk(rc, cmd, err, self.log, 5)
v = v.strip()
if not v: if not v:
continue continue
if "," not in tagname: if "," not in tagname:
ret[tagname] = v.decode("utf-8") ret[tagname] = v
else: else:
v = json.loads(v) v = json.loads(v)
for tag in tagname.split(","): for tag in tagname.split(","):

View File

@@ -17,7 +17,7 @@ from .util import mp, start_log_thrs, start_stackmon, min_ex, ansi_re
from .authsrv import AuthSrv from .authsrv import AuthSrv
from .tcpsrv import TcpSrv from .tcpsrv import TcpSrv
from .up2k import Up2k from .up2k import Up2k
from .th_srv import ThumbSrv, HAVE_PIL, HAVE_WEBP 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
@@ -70,6 +70,13 @@ class SvcHub(object):
self.log("root", m, c=3) self.log("root", m, c=3)
bri = "zy"[args.theme % 2 :][:1]
ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
args.theme = "{0}{1} {0} {1}".format(ch, bri)
if not args.hardlink and args.never_symlink:
args.no_dedup = True
# 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:
@@ -78,20 +85,30 @@ class SvcHub(object):
self.tcpsrv = TcpSrv(self) self.tcpsrv = TcpSrv(self)
self.up2k = Up2k(self) self.up2k = Up2k(self)
decs = {k: 1 for k in self.args.th_dec.split(",")}
if not HAVE_VIPS:
decs.pop("vips", None)
if not HAVE_PIL:
decs.pop("pil", None)
if not HAVE_FFMPEG or not HAVE_FFPROBE:
decs.pop("ff", None)
self.args.th_dec = list(decs.keys())
self.thumbsrv = None self.thumbsrv = None
if not args.no_thumb: if not args.no_thumb:
if HAVE_PIL: m = "decoder preference: {}".format(", ".join(self.args.th_dec))
if not HAVE_WEBP: self.log("thumb", m)
args.th_no_webp = True
msg = "setting --th-no-webp because either libwebp is not available or your Pillow is too old"
self.log("thumb", msg, c=3)
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"
self.log("thumb", msg, c=3)
if self.args.th_dec:
self.thumbsrv = ThumbSrv(self) self.thumbsrv = ThumbSrv(self)
else: else:
msg = "need Pillow to create thumbnails; for example:\n{}{} -m pip install --user Pillow\n" msg = "need either Pillow, pyvips, or FFmpeg to create thumbnails; for example:\n{0}{1} -m pip install --user Pillow\n{0}{1} -m pip install --user pyvips\n{0}apt install ffmpeg"
self.log( msg = msg.format(" " * 37, os.path.basename(sys.executable))
"thumb", msg.format(" " * 37, os.path.basename(sys.executable)), c=3 self.log("thumb", msg, c=3)
)
if not args.no_acode and args.no_thumb: if not args.no_acode and args.no_thumb:
msg = "setting --no-acode because --no-thumb (sorry)" msg = "setting --no-acode because --no-thumb (sorry)"
@@ -119,13 +136,16 @@ class SvcHub(object):
self.broker = Broker(self) self.broker = Broker(self)
def thr_httpsrv_up(self): def thr_httpsrv_up(self):
time.sleep(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
if not failed: if not failed:
return return
if self.args.ign_ebind_all: if self.args.ign_ebind_all:
if not self.tcpsrv.srv:
for _ in range(self.broker.num_workers):
self.broker.put(False, "cb_httpsrv_up")
return return
if self.args.ign_ebind and self.tcpsrv.srv: if self.args.ign_ebind and self.tcpsrv.srv:

View File

@@ -3,7 +3,7 @@ from __future__ import print_function, unicode_literals
import time import time
import zlib import zlib
from datetime import datetime import calendar
from .sutil import errdesc from .sutil import errdesc
from .util import yieldfile, sanitize_fn, spack, sunpack, min_ex from .util import yieldfile, sanitize_fn, spack, sunpack, min_ex
@@ -25,12 +25,12 @@ def dostime2unix(buf):
tf = "{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}" tf = "{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}"
iso = tf.format(*tt) iso = tf.format(*tt)
dt = datetime.strptime(iso, "%Y-%m-%d %H:%M:%S") dt = time.strptime(iso, "%Y-%m-%d %H:%M:%S")
return int(dt.timestamp()) return int(calendar.timegm(dt))
def unixtime2dos(ts): def unixtime2dos(ts):
tt = time.gmtime(ts) tt = time.gmtime(ts + 1)
dy, dm, dd, th, tm, ts = list(tt)[:6] dy, dm, dd, th, tm, ts = list(tt)[:6]
bd = ((dy - 1980) << 9) + (dm << 5) + dd bd = ((dy - 1980) << 9) + (dm << 5) + dd
@@ -76,7 +76,7 @@ def gen_hdr(h_pos, fn, sz, lastmod, utf8, crc32, pre_crc):
# 4b magic, 2b min-ver # 4b magic, 2b min-ver
ret = b"\x50\x4b\x03\x04" + req_ver ret = b"\x50\x4b\x03\x04" + req_ver
else: else:
# 4b magic, 2b spec-ver, 2b min-ver # 4b magic, 2b spec-ver (1b compat, 1b os (00 dos, 03 unix)), 2b min-ver
ret = b"\x50\x4b\x01\x02\x1e\x03" + req_ver ret = b"\x50\x4b\x01\x02\x1e\x03" + req_ver
ret += b"\x00" if pre_crc else b"\x08" # streaming ret += b"\x00" if pre_crc else b"\x08" # streaming
@@ -95,23 +95,34 @@ def gen_hdr(h_pos, fn, sz, lastmod, utf8, crc32, pre_crc):
fn = sanitize_fn(fn, "/", []) fn = sanitize_fn(fn, "/", [])
bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_") bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_")
# add ntfs (0x24) and/or unix (0x10) extrafields for utc, add z64 if requested
z64_len = len(z64v) * 8 + 4 if z64v else 0 z64_len = len(z64v) * 8 + 4 if z64v else 0
ret += spack(b"<HH", len(bfn), z64_len) ret += spack(b"<HH", len(bfn), 0x10 + z64_len)
if h_pos is not None: if h_pos is not None:
# 2b comment, 2b diskno # 2b comment, 2b diskno
ret += b"\x00" * 4 ret += b"\x00" * 4
# 2b internal.attr, 4b external.attr # 2b internal.attr, 4b external.attr
# infozip-macos: 0100 0000 a481 file:644 # infozip-macos: 0100 0000 a481 (spec-ver 1e03) file:644
# infozip-macos: 0100 0100 0080 file:000 # infozip-macos: 0100 0100 0080 (spec-ver 1e03) file:000
ret += b"\x01\x00\x00\x00\xa4\x81" # win10-zip: 0000 2000 0000 (spec-ver xx00) FILE_ATTRIBUTE_ARCHIVE
ret += b"\x00\x00\x00\x00\xa4\x81" # unx
# ret += b"\x00\x00\x20\x00\x00\x00" # fat
# 4b local-header-ofs # 4b local-header-ofs
ret += spack(b"<L", min(h_pos, 0xFFFFFFFF)) ret += spack(b"<L", min(h_pos, 0xFFFFFFFF))
ret += bfn ret += bfn
# ntfs: type 0a, size 20, rsvd, attr1, len 18, mtime, atime, ctime
# b"\xa3\x2f\x82\x41\x55\x68\xd8\x01" 1652616838.798941100 ~5.861518 132970904387989411 ~58615181
# nt = int((lastmod + 11644473600) * 10000000)
# ret += spack(b"<HHLHHQQQ", 0xA, 0x20, 0, 1, 0x18, nt, nt, nt)
# unix: type 0d, size 0c, atime, mtime, uid, gid
ret += spack(b"<HHLLHH", 0xD, 0xC, int(lastmod), int(lastmod), 1000, 1000)
if z64v: if z64v:
ret += spack(b"<HH" + b"Q" * len(z64v), 1, len(z64v) * 8, *z64v) ret += spack(b"<HH" + b"Q" * len(z64v), 1, len(z64v) * 8, *z64v)
@@ -205,7 +216,7 @@ class StreamZip(object):
st = f["st"] st = f["st"]
sz = st.st_size sz = st.st_size
ts = st.st_mtime + 1 ts = st.st_mtime
crc = None crc = None
if self.pre_crc: if self.pre_crc:

View File

@@ -4,7 +4,7 @@ from __future__ import print_function, unicode_literals
import os import os
from .util import Cooldown from .util import Cooldown
from .th_srv import thumb_path, THUMBABLE, FMT_FFV, FMT_FFA from .th_srv import thumb_path, HAVE_WEBP
from .bos import bos from .bos import bos
@@ -18,30 +18,53 @@ class ThumbCli(object):
# cache on both sides for less broker spam # cache on both sides for less broker spam
self.cooldown = Cooldown(self.args.th_poke) self.cooldown = Cooldown(self.args.th_poke)
try:
c = hsrv.th_cfg
except:
c = {k: {} for k in ["thumbable", "pil", "vips", "ffi", "ffv", "ffa"]}
self.thumbable = c["thumbable"]
self.fmt_pil = c["pil"]
self.fmt_vips = c["vips"]
self.fmt_ffi = c["ffi"]
self.fmt_ffv = c["ffv"]
self.fmt_ffa = c["ffa"]
# defer args.th_ff_jpg, can change at runtime
d = next((x for x in self.args.th_dec if x in ("vips", "pil")), None)
self.can_webp = HAVE_WEBP or d == "vips"
def log(self, msg, c=0): def log(self, msg, c=0):
self.log_func("thumbcli", msg, c) self.log_func("thumbcli", msg, c)
def get(self, ptop, rem, mtime, fmt): def get(self, dbv, rem, mtime, fmt):
ptop = dbv.realpath
ext = rem.rsplit(".")[-1].lower() ext = rem.rsplit(".")[-1].lower()
if ext not in THUMBABLE: if ext not in self.thumbable or "dthumb" in dbv.flags:
return None return None
is_vid = ext in FMT_FFV is_vid = ext in self.fmt_ffv
if is_vid and self.args.no_vthumb: if is_vid and "dvthumb" in dbv.flags:
return None return None
want_opus = fmt in ("opus", "caf") want_opus = fmt in ("opus", "caf")
is_au = ext in FMT_FFA is_au = ext in self.fmt_ffa
if is_au: if is_au:
if want_opus: if want_opus:
if self.args.no_acode: if self.args.no_acode:
return None return None
else: else:
if self.args.no_athumb: if "dathumb" in dbv.flags:
return None return None
elif want_opus: elif want_opus:
return None return None
is_img = not is_vid and not is_au
if is_img and "dithumb" in dbv.flags:
return None
preferred = self.args.th_dec[0] if self.args.th_dec else ""
if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg"]: if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg"]:
return os.path.join(ptop, rem) return os.path.join(ptop, rem)
@@ -49,7 +72,11 @@ class ThumbCli(object):
fmt = "w" fmt = "w"
if fmt == "w": if fmt == "w":
if self.args.th_no_webp or ((is_vid or is_au) and self.args.th_ff_jpg): if (
self.args.th_no_webp
or (is_img and not self.can_webp)
or (self.args.th_ff_jpg and (not is_img or preferred == "ff"))
):
fmt = "j" fmt = "j"
histpath = self.asrv.vfs.histtab.get(ptop) histpath = self.asrv.vfs.histtab.get(ptop)
@@ -58,15 +85,23 @@ class ThumbCli(object):
return None return None
tpath = thumb_path(histpath, rem, mtime, fmt) tpath = thumb_path(histpath, rem, mtime, fmt)
tpaths = [tpath]
if fmt == "w":
# also check for jpg (maybe webp is unavailable)
tpaths.append(tpath.rsplit(".", 1)[0] + ".jpg")
ret = None ret = None
try: abort = False
st = bos.stat(tpath) for tp in tpaths:
if st.st_size: try:
ret = tpath st = bos.stat(tp)
else: if st.st_size:
return None ret = tpath = tp
except: fmt = ret.rsplit(".")[1]
pass else:
abort = True
except:
pass
if ret: if ret:
tdir = os.path.dirname(tpath) tdir = os.path.dirname(tpath)
@@ -80,5 +115,8 @@ class ThumbCli(object):
return ret return ret
if abort:
return None
x = self.broker.put(True, "thumbsrv.get", ptop, rem, mtime, fmt) x = self.broker.put(True, "thumbsrv.get", ptop, rem, mtime, fmt)
return x.get() return x.get()

View File

@@ -47,31 +47,12 @@ try:
except: except:
pass pass
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html try:
# ffmpeg -formats import pyvips
FMT_PIL = "bmp dib gif icns ico jpg jpeg jp2 jpx pcx png pbm pgm ppm pnm sgi tga tif tiff webp xbm dds xpm"
FMT_FFV = "av1 asf avi flv m4v mkv mjpeg mjpg mpg mpeg mpg2 mpeg2 h264 avc mts h265 hevc mov 3gp mp4 ts mpegts nut ogv ogm rm vob webm wmv"
FMT_FFA = "aac m4a ogg opus flac alac mp3 mp2 ac3 dts wma ra wav aif aiff au alaw ulaw mulaw amr gsm ape tak tta wv"
if HAVE_HEIF: HAVE_VIPS = True
FMT_PIL += " heif heifs heic heics" except:
HAVE_VIPS = False
if HAVE_AVIF:
FMT_PIL += " avif avifs"
FMT_PIL, FMT_FFV, FMT_FFA = [
{x: True for x in y.split(" ") if x} for y in [FMT_PIL, FMT_FFV, FMT_FFA]
]
THUMBABLE = {}
if HAVE_PIL:
THUMBABLE.update(FMT_PIL)
if HAVE_FFMPEG and HAVE_FFPROBE:
THUMBABLE.update(FMT_FFV)
THUMBABLE.update(FMT_FFA)
def thumb_path(histpath, rem, mtime, fmt): def thumb_path(histpath, rem, mtime, fmt):
@@ -141,6 +122,37 @@ class ThumbSrv(object):
t.daemon = True t.daemon = True
t.start() t.start()
self.fmt_pil, self.fmt_vips, self.fmt_ffi, self.fmt_ffv, self.fmt_ffa = [
{x: True for x in y.split(",")}
for y in [
self.args.th_r_pil,
self.args.th_r_vips,
self.args.th_r_ffi,
self.args.th_r_ffv,
self.args.th_r_ffa,
]
]
if not HAVE_HEIF:
for f in "heif heifs heic heics".split(" "):
self.fmt_pil.pop(f, None)
if not HAVE_AVIF:
for f in "avif avifs".split(" "):
self.fmt_pil.pop(f, None)
self.thumbable = {}
if "pil" in self.args.th_dec:
self.thumbable.update(self.fmt_pil)
if "vips" in self.args.th_dec:
self.thumbable.update(self.fmt_vips)
if "ff" in self.args.th_dec:
for t in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
self.thumbable.update(t)
def log(self, msg, c=0): def log(self, msg, c=0):
self.log_func("thumb", msg, c) self.log_func("thumb", msg, c)
@@ -201,6 +213,16 @@ class ThumbSrv(object):
return None return None
def getcfg(self):
return {
"thumbable": self.thumbable,
"pil": self.fmt_pil,
"vips": self.fmt_vips,
"ffi": self.fmt_ffi,
"ffv": self.fmt_ffv,
"ffa": self.fmt_ffa,
}
def worker(self): def worker(self):
while not self.stopping: while not self.stopping:
task = self.q.get() task = self.q.get()
@@ -211,22 +233,29 @@ class ThumbSrv(object):
ext = abspath.split(".")[-1].lower() ext = abspath.split(".")[-1].lower()
fun = None fun = None
if not bos.path.exists(tpath): if not bos.path.exists(tpath):
if ext in FMT_PIL: for lib in self.args.th_dec:
fun = self.conv_pil if fun:
elif ext in FMT_FFV: break
fun = self.conv_ffmpeg elif lib == "pil" and ext in self.fmt_pil:
elif ext in FMT_FFA: fun = self.conv_pil
if tpath.endswith(".opus") or tpath.endswith(".caf"): elif lib == "vips" and ext in self.fmt_vips:
fun = self.conv_opus fun = self.conv_vips
else: elif lib == "ff" and ext in self.fmt_ffi or ext in self.fmt_ffv:
fun = self.conv_spec fun = self.conv_ffmpeg
elif lib == "ff" and ext in self.fmt_ffa:
if tpath.endswith(".opus") or tpath.endswith(".caf"):
fun = self.conv_opus
else:
fun = self.conv_spec
if fun: if fun:
try: try:
fun(abspath, tpath) fun(abspath, tpath)
except: except:
msg = "{} could not create thumbnail of {}\n{}" msg = "{} could not create thumbnail of {}\n{}"
self.log(msg.format(fun.__name__, abspath, min_ex()), "1;30") msg = msg.format(fun.__name__, abspath, min_ex())
c = 1 if "<Signals.SIG" in msg else "1;30"
self.log(msg, c)
with open(tpath, "wb") as _: with open(tpath, "wb") as _:
pass pass
@@ -296,11 +325,31 @@ class ThumbSrv(object):
im.save(tpath, **args) im.save(tpath, **args)
def conv_vips(self, abspath, tpath):
crops = ["centre", "none"]
if self.args.th_no_crop:
crops = ["none"]
w, h = self.res
kw = {"height": h, "size": "down", "intent": "relative"}
for c in crops:
try:
kw["crop"] = c
img = pyvips.Image.thumbnail(abspath, w, **kw)
break
except:
pass
img.write_to_file(tpath, Q=40)
def conv_ffmpeg(self, abspath, tpath): def conv_ffmpeg(self, abspath, tpath):
ret, _ = ffprobe(abspath) ret, _ = ffprobe(abspath)
if not ret:
return
ext = abspath.rsplit(".")[-1] ext = abspath.rsplit(".")[-1].lower()
if ext in ["h264", "h265"]: if ext in ["h264", "h265"] or ext in self.fmt_ffi:
seek = [] seek = []
else: else:
dur = ret[".dur"][1] if ".dur" in ret else 4 dur = ret[".dur"][1] if ".dur" in ret else 4
@@ -350,11 +399,38 @@ class ThumbSrv(object):
def _run_ff(self, cmd): def _run_ff(self, cmd):
# 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, sout, serr = runcmd(cmd, timeout=self.args.th_convt)
if ret != 0: if not ret:
m = "FFmpeg failed (probably a corrupt video file):\n" return
m += "\n".join(["ff: {}".format(x) for x in serr.split("\n")])
self.log(m, c="1;30") c = "1;30"
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1])) m = "FFmpeg failed (probably a corrupt video file):\n"
if cmd[-1].lower().endswith(b".webp") and (
"Error selecting an encoder" in serr
or "Automatic encoder selection failed" in serr
or "Default encoder for format webp" in serr
or "Please choose an encoder manually" in serr
):
self.args.th_ff_jpg = True
m = "FFmpeg failed because it was compiled without libwebp; enabling --th-ff-jpg to force jpeg output:\n"
c = 1
if (
"Requested resampling engine is unavailable" 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"
c = 1
lines = serr.strip("\n").split("\n")
if len(lines) > 50:
lines = lines[:25] + ["[...]"] + lines[-25:]
txt = "\n".join(["ff: " + str(x) for x in lines])
if len(txt) > 5000:
txt = txt[:2500] + "...\nff: [...]\nff: ..." + txt[-2500:]
self.log(m + txt, c=c)
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
def conv_spec(self, abspath, tpath): def conv_spec(self, abspath, tpath):
ret, _ = ffprobe(abspath) ret, _ = ffprobe(abspath)

View File

@@ -4,8 +4,8 @@ from __future__ import print_function, unicode_literals
import re import re
import os import os
import time import time
import calendar
import threading import threading
from datetime import datetime
from operator import itemgetter from operator import itemgetter
from .__init__ import ANYWIN, unicode from .__init__ import ANYWIN, unicode
@@ -21,6 +21,12 @@ except:
HAVE_SQLITE3 = False HAVE_SQLITE3 = False
try:
from pathlib import Path
except:
pass
class U2idx(object): class U2idx(object):
def __init__(self, conn): def __init__(self, conn):
self.log_func = conn.log_func self.log_func = conn.log_func
@@ -55,7 +61,7 @@ class U2idx(object):
uv = [wark[:16], wark] uv = [wark[:16], wark]
try: try:
return self.run_query(vols, uq, uv, True, False)[0] return self.run_query(vols, uq, uv, True, False, 99999)[0]
except: except:
raise Pebkac(500, min_ex()) raise Pebkac(500, min_ex())
@@ -76,11 +82,26 @@ class U2idx(object):
if not bos.path.exists(db_path): if not bos.path.exists(db_path):
return None return None
cur = sqlite3.connect(db_path, 2).cursor() cur = None
if ANYWIN:
uri = ""
try:
uri = "{}?mode=ro&nolock=1".format(Path(db_path).as_uri())
cur = sqlite3.connect(uri, 2, uri=True).cursor()
self.log("ro: {}".format(db_path))
except:
self.log("could not open read-only: {}\n{}".format(uri, min_ex()))
if not cur:
# on windows, this steals the write-lock from up2k.deferred_init --
# seen on win 10.0.17763.2686, py 3.10.4, sqlite 3.37.2
cur = sqlite3.connect(db_path, 2).cursor()
self.log("opened {}".format(db_path))
self.cur[ptop] = cur self.cur[ptop] = cur
return cur return cur
def search(self, vols, uq): def search(self, vols, uq, lim):
"""search by query params""" """search by query params"""
if not HAVE_SQLITE3: if not HAVE_SQLITE3:
return [] return []
@@ -169,18 +190,17 @@ class U2idx(object):
if is_date: if is_date:
is_date = False is_date = False
v = v.upper().rstrip("Z").replace(",", " ").replace("T", " ") v = re.sub(r"[tzTZ, ]+", " ", v).strip()
while " " in v:
v = v.replace(" ", " ")
for fmt in [ for fmt in [
"%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S",
"%Y-%m-%d %H:%M", "%Y-%m-%d %H:%M",
"%Y-%m-%d %H", "%Y-%m-%d %H",
"%Y-%m-%d", "%Y-%m-%d",
"%Y-%m",
"%Y",
]: ]:
try: try:
v = datetime.strptime(v, fmt).timestamp() v = calendar.timegm(time.strptime(v, fmt))
break break
except: except:
pass pass
@@ -222,11 +242,11 @@ class U2idx(object):
q += " lower({}) {} ? ) ".format(field, oper) q += " lower({}) {} ? ) ".format(field, oper)
try: try:
return self.run_query(vols, q, va, have_up, have_mt) return self.run_query(vols, q, va, have_up, have_mt, lim)
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): def run_query(self, vols, uq, uv, have_up, have_mt, lim):
done_flag = [] done_flag = []
self.active_id = "{:.6f}_{}".format( self.active_id = "{:.6f}_{}".format(
time.time(), threading.current_thread().ident time.time(), threading.current_thread().ident
@@ -255,7 +275,7 @@ class U2idx(object):
self.log("qs: {!r} {!r}".format(uq, uv)) self.log("qs: {!r} {!r}".format(uq, uv))
ret = [] ret = []
lim = int(self.args.srch_hits) lim = min(lim, int(self.args.srch_hits))
taglist = {} taglist = {}
for (vtop, ptop, flags) in vols: for (vtop, ptop, flags) in vols:
cur = self.get_cur(ptop) cur = self.get_cur(ptop)
@@ -278,7 +298,7 @@ class U2idx(object):
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
if lim <= 0: if lim < 0:
break break
if rd.startswith("//") or fn.startswith("//"): if rd.startswith("//") or fn.startswith("//"):
@@ -307,7 +327,7 @@ class U2idx(object):
w = hit["w"] w = hit["w"]
del hit["w"] del hit["w"]
tags = {} tags = {}
q2 = "select k, v from mt where w = ? and k != 'x'" q2 = "select k, v from mt where w = ? and +k != 'x'"
for k, v2 in cur.execute(q2, (w,)): for k, v2 in cur.execute(q2, (w,)):
taglist[k] = True taglist[k] = True
tags[k] = v2 tags[k] = v2

View File

@@ -95,7 +95,7 @@ class Up2k(object):
if ANYWIN: if ANYWIN:
# usually fails to set lastmod too quickly # usually fails to set lastmod too quickly
self.lastmod_q = Queue() self.lastmod_q = []
thr = threading.Thread(target=self._lastmodder, name="up2k-lastmod") thr = threading.Thread(target=self._lastmodder, name="up2k-lastmod")
thr.daemon = True thr.daemon = True
thr.start() thr.start()
@@ -470,9 +470,11 @@ class Up2k(object):
ft = "\033[0;32m{}{:.0}" ft = "\033[0;32m{}{:.0}"
ff = "\033[0;35m{}{:.0}" ff = "\033[0;35m{}{:.0}"
fv = "\033[0;36m{}:\033[1;30m{}" fv = "\033[0;36m{}:\033[1;30m{}"
fx = set(("html_head",))
a = [ a = [
(ft if v is True else ff if v is False else fv).format(k, str(v)) (ft if v is True else ff if v is False else fv).format(k, str(v))
for k, v in flags.items() for k, v in flags.items()
if k not in fx
] ]
if a: if a:
vpath = "?" vpath = "?"
@@ -552,12 +554,16 @@ class Up2k(object):
for d in all_vols for d in all_vols
if d != vol and (d.vpath.startswith(vol.vpath + "/") or not vol.vpath) if d != vol and (d.vpath.startswith(vol.vpath + "/") or not vol.vpath)
] ]
excl += [absreal(x) for x in excl]
excl += list(self.asrv.vfs.histtab.values())
if WINDOWS: if WINDOWS:
excl = [x.replace("/", "\\") for x in excl] excl = [x.replace("/", "\\") for x in excl]
excl = set(excl)
rtop = absreal(top)
n_add = n_rm = 0 n_add = n_rm = 0
try: try:
n_add = self._build_dir(dbw, top, set(excl), top, rei, reh, []) n_add = self._build_dir(dbw, top, excl, top, rtop, rei, reh, [])
n_rm = self._drop_lost(dbw[0], top) n_rm = self._drop_lost(dbw[0], top)
except: except:
m = "failed to index volume [{}]:\n{}" m = "failed to index volume [{}]:\n{}"
@@ -570,8 +576,7 @@ class Up2k(object):
return True, n_add or n_rm or do_vac return True, n_add or n_rm or do_vac
def _build_dir(self, dbw, top, excl, cdir, rei, reh, seen): def _build_dir(self, dbw, top, excl, cdir, rcdir, rei, reh, seen):
rcdir = absreal(cdir) # a bit expensive but worth
if rcdir in seen: if rcdir in seen:
m = "bailing from symlink loop,\n prev: {}\n curr: {}\n from: {}" m = "bailing from symlink loop,\n prev: {}\n curr: {}\n from: {}"
self.log(m.format(seen[-1], rcdir, cdir), 3) self.log(m.format(seen[-1], rcdir, cdir), 3)
@@ -579,11 +584,12 @@ class Up2k(object):
seen = seen + [rcdir] seen = seen + [rcdir]
self.pp.msg = "a{} {}".format(self.pp.n, cdir) self.pp.msg = "a{} {}".format(self.pp.n, cdir)
histpath = self.asrv.vfs.histtab[top]
ret = 0 ret = 0
seen_files = {} seen_files = {} # != inames; files-only for dropcheck
g = statdir(self.log_func, not self.args.no_scandir, False, cdir) g = statdir(self.log_func, not self.args.no_scandir, False, cdir)
for iname, inf in sorted(g): g = sorted(g)
inames = {x[0]: 1 for x in g}
for iname, inf in g:
abspath = os.path.join(cdir, iname) abspath = os.path.join(cdir, iname)
if rei and rei.search(abspath): if rei and rei.search(abspath):
continue continue
@@ -592,14 +598,20 @@ class Up2k(object):
lmod = int(inf.st_mtime) lmod = int(inf.st_mtime)
sz = inf.st_size sz = inf.st_size
if stat.S_ISDIR(inf.st_mode): if stat.S_ISDIR(inf.st_mode):
if abspath in excl or abspath == histpath: rap = absreal(abspath)
if abspath in excl or rap in excl:
continue
if iname == ".th" and bos.path.isdir(os.path.join(abspath, "top")):
# abandoned or foreign, skip
continue continue
# self.log(" dir: {}".format(abspath)) # self.log(" dir: {}".format(abspath))
try: try:
ret += self._build_dir(dbw, top, excl, abspath, rei, reh, seen) ret += self._build_dir(dbw, top, excl, abspath, rap, rei, reh, seen)
except: except:
m = "failed to index subdir [{}]:\n{}" m = "failed to index subdir [{}]:\n{}"
self.log(m.format(abspath, min_ex()), c=1) self.log(m.format(abspath, min_ex()), c=1)
elif not stat.S_ISREG(inf.st_mode):
self.log("skip type-{:x} file [{}]".format(inf.st_mode, abspath))
else: else:
# self.log("file: {}".format(abspath)) # self.log("file: {}".format(abspath))
seen_files[iname] = 1 seen_files[iname] = 1
@@ -607,6 +619,17 @@ class Up2k(object):
if WINDOWS: if WINDOWS:
rp = rp.replace("\\", "/").strip("/") rp = rp.replace("\\", "/").strip("/")
if rp.endswith(".PARTIAL") and time.time() - lmod < 60:
# rescan during upload
continue
if not sz and (
"{}.PARTIAL".format(iname) in inames
or ".{}.PARTIAL".format(iname) in inames
):
# placeholder for unfinished upload
continue
rd, fn = rp.rsplit("/", 1) if "/" in rp else ["", rp] rd, fn = rp.rsplit("/", 1) if "/" in rp else ["", rp]
sql = "select w, mt, sz from up where rd = ? and fn = ?" sql = "select w, mt, sz from up where rd = ? and fn = ?"
try: try:
@@ -776,6 +799,7 @@ class Up2k(object):
if self.mtag.prefer_mt and self.args.mtag_mt > 1: if self.mtag.prefer_mt and self.args.mtag_mt > 1:
mpool = self._start_mpool() mpool = self._start_mpool()
# TODO blocks writes to registry cursor; do chunks instead
conn = sqlite3.connect(db_path, timeout=15) conn = sqlite3.connect(db_path, timeout=15)
cur = conn.cursor() cur = conn.cursor()
c2 = conn.cursor() c2 = conn.cursor()
@@ -801,8 +825,8 @@ class Up2k(object):
n_tags = self._tag_file(c3, *args) n_tags = self._tag_file(c3, *args)
else: else:
mpool.put(["mtag"] + args) mpool.put(["mtag"] + args)
with self.mutex: # not registry cursor; do not self.mutex:
n_tags = len(self._flush_mpool(c3)) n_tags = len(self._flush_mpool(c3))
n_add += n_tags n_add += n_tags
n_buf += n_tags n_buf += n_tags
@@ -825,9 +849,6 @@ class Up2k(object):
cur.close() cur.close()
conn.close() conn.close()
with self.mutex:
gcur.connection.commit()
return n_add, n_rm, True return n_add, n_rm, True
def _flush_mpool(self, wcur): def _flush_mpool(self, wcur):
@@ -928,7 +949,7 @@ class Up2k(object):
n_done += 1 n_done += 1
for w in to_delete.keys(): for w in to_delete.keys():
q = "delete from mt where w = ? and k = 't:mtp'" q = "delete from mt where w = ? and +k = 't:mtp'"
cur.execute(q, (w,)) cur.execute(q, (w,))
to_delete = {} to_delete = {}
@@ -966,7 +987,7 @@ class Up2k(object):
with self.mutex: with self.mutex:
done = self._flush_mpool(wcur) done = self._flush_mpool(wcur)
for w in done: for w in done:
q = "delete from mt where w = ? and k = 't:mtp'" q = "delete from mt where w = ? and +k = 't:mtp'"
cur.execute(q, (w,)) cur.execute(q, (w,))
cur.connection.commit() cur.connection.commit()
@@ -1064,18 +1085,20 @@ class Up2k(object):
if parser == "mtag": if parser == "mtag":
parser = self.mtag.backend parser = self.mtag.backend
msg = "{} failed to read tags from {}:\n{}" self._log_tag_err(parser, abspath, ex)
self.log(msg.format(parser, abspath, ex), c=3)
q.task_done() q.task_done()
def _log_tag_err(self, parser, abspath, ex):
msg = "{} failed to read tags from {}:\n{}".format(parser, abspath, ex)
self.log(msg.lstrip(), c=1 if "<Signals.SIG" in msg else 3)
def _tag_file(self, write_cur, entags, wark, abspath, tags=None): def _tag_file(self, write_cur, entags, wark, abspath, tags=None):
if tags is None: if tags is None:
try: try:
tags = self.mtag.get(abspath) tags = self.mtag.get(abspath)
except Exception as ex: except Exception as ex:
msg = "failed to read tags from {}:\n{}" self._log_tag_err("", abspath, ex)
self.log(msg.format(abspath, ex), c=3)
return 0 return 0
if not bos.path.isfile(abspath): if not bos.path.isfile(abspath):
@@ -1092,7 +1115,7 @@ class Up2k(object):
for k in tags.keys(): for k in tags.keys():
q = "delete from mt where w = ? and ({})".format( q = "delete from mt where w = ? and ({})".format(
" or ".join(["k = ?"] * len(tags)) " or ".join(["+k = ?"] * len(tags))
) )
args = [wark[:16]] + list(tags.keys()) args = [wark[:16]] + list(tags.keys())
write_cur.execute(q, tuple(args)) write_cur.execute(q, tuple(args))
@@ -1106,7 +1129,8 @@ class Up2k(object):
return ret return ret
def _orz(self, db_path): def _orz(self, db_path):
return sqlite3.connect(db_path, check_same_thread=False).cursor() timeout = int(max(self.args.srch_time, 5) * 1.2)
return sqlite3.connect(db_path, timeout, check_same_thread=False).cursor()
# x.set_trace_callback(trace) # x.set_trace_callback(trace)
def _open_db(self, db_path): def _open_db(self, db_path):
@@ -1235,6 +1259,11 @@ class Up2k(object):
wark = self._get_wark(cj) wark = self._get_wark(cj)
now = time.time() now = time.time()
job = None job = None
try:
dev = bos.stat(os.path.join(cj["ptop"], cj["prel"])).st_dev
except:
dev = 0
with self.mutex: with self.mutex:
cur = self.cur.get(cj["ptop"]) cur = self.cur.get(cj["ptop"])
reg = self.registry[cj["ptop"]] reg = self.registry[cj["ptop"]]
@@ -1246,37 +1275,42 @@ class Up2k(object):
q = r"select * from up where substr(w,1,16) = ? and w = ?" q = r"select * from up where substr(w,1,16) = ? and w = ?"
argv = (wark[:16], wark) argv = (wark[:16], wark)
alts = []
cur = cur.execute(q, argv) cur = cur.execute(q, argv)
for _, dtime, dsize, dp_dir, dp_fn, ip, at in cur: for _, dtime, dsize, dp_dir, dp_fn, ip, at in cur:
if dp_dir.startswith("//") or dp_fn.startswith("//"): if dp_dir.startswith("//") or dp_fn.startswith("//"):
dp_dir, dp_fn = s3dec(dp_dir, dp_fn) dp_dir, dp_fn = s3dec(dp_dir, dp_fn)
if job and (dp_dir != cj["prel"] or dp_fn != cj["name"]): dp_abs = "/".join([cj["ptop"], dp_dir, dp_fn])
try:
st = bos.stat(dp_abs)
if stat.S_ISLNK(st.st_mode):
# broken symlink
raise Exception()
except:
continue continue
dp_abs = "/".join([cj["ptop"], dp_dir, dp_fn]) j = {
# relying on this to fail on broken symlinks "name": dp_fn,
try: "prel": dp_dir,
sz = bos.path.getsize(dp_abs) "vtop": cj["vtop"],
except: "ptop": cj["ptop"],
sz = 0 "size": dsize,
"lmod": dtime,
if sz: "addr": ip,
# self.log("--- " + wark + " " + dp_abs + " found file", 4) "at": at,
job = { "hash": [],
"name": dp_fn, "need": [],
"prel": dp_dir, "busy": {},
"vtop": cj["vtop"], }
"ptop": cj["ptop"], score = (
"size": dsize, (3 if st.st_dev == dev else 0)
"lmod": dtime, + (2 if dp_dir == cj["prel"] else 0)
"addr": ip, + (1 if dp_fn == cj["name"] else 0)
"at": at, )
"hash": [], alts.append([score, -len(alts), j])
"need": [],
"busy": {},
}
job = sorted(alts, reverse=True)[0][2] if alts else None
if job and wark in reg: if job and wark in reg:
# self.log("pop " + wark + " " + job["name"] + " handle_json db", 4) # self.log("pop " + wark + " " + job["name"] + " handle_json db", 4)
del reg[wark] del reg[wark]
@@ -1417,14 +1451,14 @@ class Up2k(object):
linked = False linked = False
try: try:
if self.args.no_symlink: if self.args.no_dedup:
raise Exception("disabled in config") raise Exception("disabled in config")
lsrc = src lsrc = src
ldst = dst ldst = dst
fs1 = bos.stat(os.path.dirname(src)).st_dev fs1 = bos.stat(os.path.dirname(src)).st_dev
fs2 = bos.stat(os.path.dirname(dst)).st_dev fs2 = bos.stat(os.path.dirname(dst)).st_dev
if fs1 == 0: if fs1 == 0 or fs2 == 0:
# py2 on winxp or other unsupported combination # py2 on winxp or other unsupported combination
raise OSError() raise OSError()
elif fs1 == fs2: elif fs1 == fs2:
@@ -1445,16 +1479,27 @@ class Up2k(object):
lsrc = nsrc[nc:] lsrc = nsrc[nc:]
hops = len(ndst[nc:]) - 1 hops = len(ndst[nc:]) - 1
lsrc = "../" * hops + "/".join(lsrc) lsrc = "../" * hops + "/".join(lsrc)
os.symlink(fsenc(lsrc), fsenc(ldst))
linked = True try:
if self.args.hardlink:
os.link(fsenc(src), fsenc(dst))
linked = True
except Exception as ex:
self.log("cannot hardlink: " + repr(ex))
if self.args.never_symlink:
raise Exception("symlink-fallback disabled in cfg")
if not linked:
os.symlink(fsenc(lsrc), fsenc(ldst))
linked = True
except Exception as ex: except Exception as ex:
self.log("cannot symlink; creating copy: " + repr(ex)) self.log("cannot link; creating copy: " + repr(ex))
shutil.copy2(fsenc(src), fsenc(dst)) shutil.copy2(fsenc(src), fsenc(dst))
if lmod and (not linked or SYMTIME): if lmod and (not linked or SYMTIME):
times = (int(time.time()), int(lmod)) times = (int(time.time()), int(lmod))
if ANYWIN: if ANYWIN:
self.lastmod_q.put([dst, 0, times]) self.lastmod_q.append([dst, 0, times])
else: else:
bos.utime(dst, times, False) bos.utime(dst, times, False)
@@ -1548,9 +1593,15 @@ class Up2k(object):
# self.log("--- " + wark + " " + dst + " finish_upload atomic " + dst, 4) # self.log("--- " + wark + " " + dst + " finish_upload atomic " + dst, 4)
atomic_move(src, dst) atomic_move(src, dst)
times = (int(time.time()), int(job["lmod"]))
if ANYWIN: if ANYWIN:
a = [dst, job["size"], (int(time.time()), int(job["lmod"]))] a = [dst, job["size"], times]
self.lastmod_q.put(a) self.lastmod_q.append(a)
elif not job["hash"]:
try:
bos.utime(dst, times)
except:
pass
a = [job[x] for x in "ptop wark prel name lmod size addr".split()] a = [job[x] for x in "ptop wark prel name lmod size addr".split()]
a += [job.get("at") or time.time()] a += [job.get("at") or time.time()]
@@ -1649,12 +1700,12 @@ class Up2k(object):
vn, rem = vn.get_dbv(rem) vn, rem = vn.get_dbv(rem)
unpost = False unpost = False
except: except:
# unpost with missing permissions? try read+write and verify with db # unpost with missing permissions? verify with db
if not self.args.unpost: if not self.args.unpost:
raise Pebkac(400, "the unpost feature is disabled in server config") raise Pebkac(400, "the unpost feature is disabled in server config")
unpost = True unpost = True
permsets = [[True, True]] permsets = [[False, True]]
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0]) vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0])
vn, rem = vn.get_dbv(rem) vn, rem = vn.get_dbv(rem)
_, _, _, _, dip, dat = self._find_from_vpath(vn.realpath, rem) _, _, _, _, dip, dat = self._find_from_vpath(vn.realpath, rem)
@@ -2038,9 +2089,8 @@ class Up2k(object):
def _lastmodder(self): def _lastmodder(self):
while True: while True:
ready = [] ready = self.lastmod_q
while not self.lastmod_q.empty(): self.lastmod_q = []
ready.append(self.lastmod_q.get())
# self.log("lmod: got {}".format(len(ready))) # self.log("lmod: got {}".format(len(ready)))
time.sleep(5) time.sleep(5)
@@ -2049,7 +2099,8 @@ class Up2k(object):
try: try:
bos.utime(path, times, False) bos.utime(path, times, False)
except: except:
self.log("lmod: failed to utime ({}, {})".format(path, times)) m = "lmod: failed to utime ({}, {}):\n{}"
self.log(m.format(path, times, min_ex()))
if self.args.sparse and self.args.sparse * 1024 * 1024 <= sz: if self.args.sparse and self.args.sparse * 1024 * 1024 <= sz:
try: try:
@@ -2144,8 +2195,7 @@ class Up2k(object):
if parsers: if parsers:
tags.update(self.mtag.get_bin(parsers, abspath)) tags.update(self.mtag.get_bin(parsers, abspath))
except Exception as ex: except Exception as ex:
msg = "failed to read tags from {}:\n{}" self._log_tag_err("", abspath, ex)
self.log(msg.format(abspath, ex), c=3)
continue continue
with self.mutex: with self.mutex:

View File

@@ -9,6 +9,7 @@ import time
import base64 import base64
import select import select
import struct import struct
import signal
import hashlib import hashlib
import platform import platform
import traceback import traceback
@@ -67,7 +68,7 @@ if WINDOWS and PY2:
FS_ENCODING = "utf-8" FS_ENCODING = "utf-8"
SYMTIME = sys.version_info >= (3, 6) and os.supports_follow_symlinks SYMTIME = sys.version_info >= (3, 6) and os.utime in os.supports_follow_symlinks
HTTP_TS_FMT = "%a, %d %b %Y %H:%M:%S GMT" HTTP_TS_FMT = "%a, %d %b %Y %H:%M:%S GMT"
@@ -912,6 +913,9 @@ def sanitize_fn(fn, ok, bad):
if "/" not in ok: if "/" not in ok:
fn = fn.replace("\\", "/").split("/")[-1] fn = fn.replace("\\", "/").split("/")[-1]
if fn.lower() in bad:
fn = "_" + fn
if ANYWIN: if ANYWIN:
remap = [ remap = [
["<", ""], ["<", ""],
@@ -927,16 +931,26 @@ def sanitize_fn(fn, ok, bad):
for a, b in [x for x in remap if x[0] not in ok]: for a, b in [x for x in remap if x[0] not in ok]:
fn = fn.replace(a, b) fn = fn.replace(a, b)
bad.extend(["con", "prn", "aux", "nul"]) bad = ["con", "prn", "aux", "nul"]
for n in range(1, 10): for n in range(1, 10):
bad += "com{0} lpt{0}".format(n).split(" ") bad += "com{0} lpt{0}".format(n).split(" ")
if fn.lower() in bad: if fn.lower().split(".")[0] in bad:
fn = "_" + fn fn = "_" + fn
return fn.strip() return fn.strip()
def relchk(rp):
if ANYWIN:
if "\n" in rp or "\r" in rp:
return "x\nx"
p = re.sub(r'[\\:*?"<>|]', "", rp)
if p != rp:
return "[{}]".format(p)
def absreal(fpath): def absreal(fpath):
try: try:
return fsdec(os.path.abspath(os.path.realpath(fsenc(fpath)))) return fsdec(os.path.abspath(os.path.realpath(fsenc(fpath))))
@@ -1235,7 +1249,7 @@ def statdir(logger, scandir, lstat, top):
if lstat and ANYWIN: if lstat and ANYWIN:
lstat = False lstat = False
if lstat and not os.supports_follow_symlinks: if lstat and (PY2 or os.stat not in os.supports_follow_symlinks):
scandir = False scandir = False
try: try:
@@ -1337,8 +1351,8 @@ def guess_mime(url, fallback="application/octet-stream"):
return ret return ret
def runcmd(argv, timeout=None): def runcmd(argv, timeout=None, **ka):
p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE) p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE, **ka)
if not timeout or PY2: if not timeout or PY2:
stdout, stderr = p.communicate() stdout, stderr = p.communicate()
else: else:
@@ -1353,9 +1367,10 @@ def runcmd(argv, timeout=None):
return [p.returncode, stdout, stderr] return [p.returncode, stdout, stderr]
def chkcmd(argv): def chkcmd(argv, **ka):
ok, sout, serr = runcmd(argv) ok, sout, serr = runcmd(argv, **ka)
if ok != 0: if ok != 0:
retchk(ok, argv, serr)
raise Exception(serr) raise Exception(serr)
return sout, serr return sout, serr
@@ -1372,6 +1387,46 @@ def mchkcmd(argv, timeout=10):
raise sp.CalledProcessError(rv, (argv[0], b"...", argv[-1])) raise sp.CalledProcessError(rv, (argv[0], b"...", argv[-1]))
def retchk(rc, cmd, serr, logger=None, color=None):
if rc < 0:
rc = 128 - rc
if rc < 126:
return
s = None
if rc > 128:
try:
s = str(signal.Signals(rc - 128))
except:
pass
elif rc == 126:
s = "invalid program"
elif rc == 127:
s = "program not found"
else:
s = "invalid retcode"
if s:
m = "{} <{}>".format(rc, s)
else:
m = str(rc)
try:
c = " ".join([fsdec(x) for x in cmd])
except:
c = str(cmd)
m = "error {} from [{}]".format(m, c)
if serr:
m += "\n" + serr
if logger:
logger(m, color)
else:
raise Exception(m)
def gzip_orig_sz(fn): def gzip_orig_sz(fn):
with open(fsenc(fn), "rb") as f: with open(fsenc(fn), "rb") as f:
f.seek(-4, 2) f.seek(-4, 2)

View File

@@ -17,12 +17,11 @@ window.baguetteBox = (function () {
titleTag: false, titleTag: false,
async: false, async: false,
preload: 2, preload: 2,
animation: 'slideIn',
afterShow: null, afterShow: null,
afterHide: null, afterHide: null,
onChange: null, onChange: null,
}, },
overlay, slider, btnPrev, btnNext, btnHelp, btnRotL, btnRotR, btnSel, btnVmode, btnClose, overlay, slider, btnPrev, btnNext, btnHelp, btnAnim, btnRotL, btnRotR, btnSel, btnVmode, btnClose,
currentGallery = [], currentGallery = [],
currentIndex = 0, currentIndex = 0,
isOverlayVisible = false, isOverlayVisible = false,
@@ -30,13 +29,14 @@ window.baguetteBox = (function () {
touchFlag = false, // busy touchFlag = false, // busy
re_i = /.+\.(gif|jpe?g|png|webp)(\?|$)/i, re_i = /.+\.(gif|jpe?g|png|webp)(\?|$)/i,
re_v = /.+\.(webm|mp4)(\?|$)/i, re_v = /.+\.(webm|mp4)(\?|$)/i,
anims = ['slideIn', 'fadeIn', 'none'],
data = {}, // all galleries data = {}, // all galleries
imagesElements = [], imagesElements = [],
documentLastFocus = null, documentLastFocus = null,
isFullscreen = false, isFullscreen = false,
vmute = false, vmute = false,
vloop = false, vloop = sread('vmode') == 'L',
vnext = false, vnext = sread('vmode') == 'C',
resume_mp = false; resume_mp = false;
var onFSC = function (e) { var onFSC = function (e) {
@@ -178,6 +178,7 @@ window.baguetteBox = (function () {
'<button id="bbox-next" class="bbox-btn" type="button" aria-label="Next">&gt;</button>' + '<button id="bbox-next" class="bbox-btn" type="button" aria-label="Next">&gt;</button>' +
'<div id="bbox-btns">' + '<div id="bbox-btns">' +
'<button id="bbox-help" type="button">?</button>' + '<button id="bbox-help" type="button">?</button>' +
'<button id="bbox-anim" type="button" tt="a">-</button>' +
'<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>' +
@@ -193,6 +194,7 @@ window.baguetteBox = (function () {
btnPrev = ebi('bbox-prev'); btnPrev = ebi('bbox-prev');
btnNext = ebi('bbox-next'); btnNext = ebi('bbox-next');
btnHelp = ebi('bbox-help'); btnHelp = ebi('bbox-help');
btnAnim = ebi('bbox-anim');
btnRotL = ebi('bbox-rotl'); btnRotL = ebi('bbox-rotl');
btnRotR = ebi('bbox-rotr'); btnRotR = ebi('bbox-rotr');
btnSel = ebi('bbox-tsel'); btnSel = ebi('bbox-tsel');
@@ -284,6 +286,16 @@ window.baguetteBox = (function () {
rotn(e.shiftKey ? -1 : 1); rotn(e.shiftKey ? -1 : 1);
} }
function anim() {
var i = (anims.indexOf(options.animation) + 1) % anims.length,
o = options;
swrite('ganim', anims[i]);
options = {};
setOptions(o);
if (tt.en)
tt.show.bind(this)();
}
function setVmode() { function setVmode() {
var v = vid(); var v = vid();
ebi('bbox-vmode').style.display = v ? '' : 'none'; ebi('bbox-vmode').style.display = v ? '' : 'none';
@@ -308,6 +320,7 @@ window.baguetteBox = (function () {
btnVmode.setAttribute('aria-label', msg); btnVmode.setAttribute('aria-label', msg);
btnVmode.setAttribute('tt', msg + tts); btnVmode.setAttribute('tt', msg + tts);
btnVmode.textContent = lbl; btnVmode.textContent = lbl;
swrite('vmode', lbl[0]);
v.loop = vloop v.loop = vloop
if (vloop && v.paused) if (vloop && v.paused)
@@ -397,6 +410,7 @@ window.baguetteBox = (function () {
bind(btnClose, 'click', hideOverlay); bind(btnClose, 'click', hideOverlay);
bind(btnVmode, 'click', tglVmode); bind(btnVmode, 'click', tglVmode);
bind(btnHelp, 'click', halp); bind(btnHelp, 'click', halp);
bind(btnAnim, 'click', anim);
bind(btnRotL, 'click', rotl); bind(btnRotL, 'click', rotl);
bind(btnRotR, 'click', rotr); bind(btnRotR, 'click', rotr);
bind(btnSel, 'click', tglsel); bind(btnSel, 'click', tglsel);
@@ -414,6 +428,7 @@ window.baguetteBox = (function () {
unbind(btnClose, 'click', hideOverlay); unbind(btnClose, 'click', hideOverlay);
unbind(btnVmode, 'click', tglVmode); unbind(btnVmode, 'click', tglVmode);
unbind(btnHelp, 'click', halp); unbind(btnHelp, 'click', halp);
unbind(btnAnim, 'click', anim);
unbind(btnRotL, 'click', rotl); unbind(btnRotL, 'click', rotl);
unbind(btnRotR, 'click', rotr); unbind(btnRotR, 'click', rotr);
unbind(btnSel, 'click', tglsel); unbind(btnSel, 'click', tglsel);
@@ -459,7 +474,12 @@ window.baguetteBox = (function () {
if (typeof newOptions[item] !== 'undefined') if (typeof newOptions[item] !== 'undefined')
options[item] = newOptions[item]; options[item] = newOptions[item];
} }
slider.style.transition = (options.animation === 'fadeIn' ? 'opacity .4s ease' :
var an = options.animation = sread('ganim') || anims[ANIM ? 0 : 2];
btnAnim.textContent = ['⇄', '⮺', '⚡'][anims.indexOf(an)];
btnAnim.setAttribute('tt', 'animation: ' + an);
slider.style.transition = (options.animation === 'fadeIn' ? 'opacity .3s ease' :
options.animation === 'slideIn' ? '' : 'none'); options.animation === 'slideIn' ? '' : 'none');
if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1)) if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1))
@@ -520,6 +540,7 @@ window.baguetteBox = (function () {
if (overlay.style.display === 'none') if (overlay.style.display === 'none')
return; return;
sethash('');
unbind(document, 'keydown', keyDownHandler); unbind(document, 'keydown', keyDownHandler);
unbind(document, 'keyup', keyUpHandler); unbind(document, 'keyup', keyUpHandler);
unbind(document, 'fullscreenchange', onFSC); unbind(document, 'fullscreenchange', onFSC);
@@ -806,7 +827,7 @@ window.baguetteBox = (function () {
slider.style.transform = 'translate3d(' + offset + ',0,0)' : slider.style.transform = 'translate3d(' + offset + ',0,0)' :
slider.style.left = offset; slider.style.left = offset;
slider.style.opacity = 1; slider.style.opacity = 1;
}, 400); }, 100);
} else { } else {
xform ? xform ?
slider.style.transform = 'translate3d(' + offset + ',0,0)' : slider.style.transform = 'translate3d(' + offset + ',0,0)' :

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,7 @@
<input type="file" name="f" multiple /><br /> <input type="file" name="f" multiple /><br />
<input type="submit" value="start upload"> <input type="submit" value="start upload">
</form> </form>
<a id="bbsw" href="?b=u"><br />switch to basic browser</a>
</div> </div>
<div id="op_mkdir" class="opview opbox act"> <div id="op_mkdir" class="opview opbox act">
@@ -67,7 +68,7 @@
<div id="op_cfg" class="opview opbox opwide"></div> <div id="op_cfg" class="opview opbox opwide"></div>
<h1 id="path"> <h1 id="path">
<a href="#" id="entree" tt="show navpane (directory tree sidebar)$NHotkey: B">🌲</a> <a href="#" id="entree">🌲</a>
{%- for n in vpnodes %} {%- for n in vpnodes %}
<a href="/{{ n[0] }}">{{ n[1] }}</a> <a href="/{{ n[0] }}">{{ n[1] }}</a>
{%- endfor %} {%- endfor %}
@@ -119,7 +120,7 @@
<div id="epi" class="logue">{{ logues[1] }}</div> <div id="epi" class="logue">{{ logues[1] }}</div>
<h2><a href="/?h">control-panel</a></h2> <h2><a href="/?h" id="goh">control-panel</a></h2>
<a href="#" id="repl">π</a> <a href="#" id="repl">π</a>
@@ -134,6 +135,10 @@
<script> <script>
var acct = "{{ acct }}", var acct = "{{ acct }}",
perms = {{ perms }}, perms = {{ perms }},
themes = {{ themes }},
dtheme = "{{ dtheme }}",
srvinf = "{{ srv_info }}",
lang = "{{ lang }}",
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 }},
@@ -142,14 +147,16 @@
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 }},
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 }},
ls0 = {{ ls0|tojson }}; ls0 = {{ ls0|tojson }};
document.documentElement.setAttribute("class", localStorage.lightmode == 1 ? "light" : "dark"); document.documentElement.className = localStorage.theme || dtheme;
</script> </script>
<script src="/.cpr/util.js?_={{ ts }}"></script> <script src="/.cpr/util.js?_={{ ts }}"></script>
<script src="/.cpr/baguettebox.js?_={{ ts }}"></script>
<script src="/.cpr/browser.js?_={{ ts }}"></script> <script src="/.cpr/browser.js?_={{ ts }}"></script>
<script src="/.cpr/up2k.js?_={{ ts }}"></script> <script src="/.cpr/up2k.js?_={{ ts }}"></script>
{%- if js %} {%- if js %}

File diff suppressed because it is too large Load Diff

View File

@@ -45,7 +45,9 @@
<tr><td></td><td><a href="../{{ url_suf }}">parent folder</a></td><td>-</td><td>-</td></tr> <tr><td></td><td><a href="../{{ url_suf }}">parent folder</a></td><td>-</td><td>-</td></tr>
{%- for f in files %} {%- for f in files %}
<tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}{{ url_suf }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td><td>{{ f.dt }}</td></tr> <tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}{{
'&' + url_suf[1:] if url_suf[:1] == '?' and '?' in f.href else url_suf
}}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td><td>{{ f.dt }}</td></tr>
{%- endfor %} {%- endfor %}
</tbody> </tbody>

View File

@@ -161,7 +161,7 @@ blink {
height: 1.05em; height: 1.05em;
margin: -.2em .3em -.2em -.4em; margin: -.2em .3em -.2em -.4em;
display: inline-block; display: inline-block;
border: 1px solid rgba(0,0,0,0.2); border: 1px solid rgba(154,154,154,0.6);
border-width: .2em .2em 0 0; border-width: .2em .2em 0 0;
transform: rotate(45deg); transform: rotate(45deg);
} }
@@ -219,48 +219,45 @@ blink {
html.dark, html.z,
html.dark body { html.z body {
background: #222; background: #222;
color: #ccc; color: #ccc;
} }
html.dark #toc a { html.z #toc a {
color: #ccc; color: #ccc;
border-left: .4em solid #444; border-left: .4em solid #444;
border-bottom: .1em solid #333; border-bottom: .1em solid #333;
} }
html.dark #toc a.act { html.z #toc a.act {
color: #fff; color: #fff;
border-left: .4em solid #3ad; border-left: .4em solid #3ad;
} }
html.dark #toc li { html.z #toc li {
border-width: 0; border-width: 0;
} }
html.dark #mn a:not(:last-child)::after { html.z #mn a {
border-color: rgba(255,255,255,0.3);
}
html.dark #mn a {
color: #ccc; color: #ccc;
} }
html.dark #mn { html.z #mn {
border-bottom: 1px solid #333; border-bottom: 1px solid #333;
} }
html.dark #mn, html.z #mn,
html.dark #mh { html.z #mh {
background: #222; background: #222;
} }
html.dark #mh a { html.z #mh a {
color: #ccc; color: #ccc;
background: none; background: none;
} }
html.dark #mh a:hover { html.z #mh a:hover {
background: #333; background: #333;
color: #fff; color: #fff;
} }
html.dark #toolsbox { html.z #toolsbox {
background: #222; background: #222;
} }
html.dark #toolsbox.open { html.z #toolsbox.open {
box-shadow: 0 .2em .2em #069; box-shadow: 0 .2em .2em #069;
border-radius: 0 0 .4em .4em; border-radius: 0 0 .4em .4em;
} }
@@ -307,24 +304,24 @@ blink {
} }
html.dark #toc { html.z #toc {
background: #282828; background: #282828;
border-top: 1px solid #2c2c2c; border-top: 1px solid #2c2c2c;
box-shadow: 0 0 1em #181818; box-shadow: 0 0 1em #181818;
} }
html.dark #toc, html.z #toc,
html.dark #mw { html.z #mw {
scrollbar-color: #b80 #282828; scrollbar-color: #b80 #282828;
} }
html.dark #toc::-webkit-scrollbar-track { html.z #toc::-webkit-scrollbar-track {
background: #282828; background: #282828;
} }
html.dark #toc::-webkit-scrollbar { html.z #toc::-webkit-scrollbar {
background: #282828; background: #282828;
width: .8em; width: .8em;
} }
html.dark #toc::-webkit-scrollbar-thumb { html.z #toc::-webkit-scrollbar-thumb {
background: #b80; background: #b80;
} }
} }
@@ -431,17 +428,17 @@ blink {
} }
html.dark .mdo a { html.z .mdo a {
color: #000; color: #000;
} }
html.dark .mdo pre, html.z .mdo pre,
html.dark .mdo code { html.z .mdo code {
color: #240; color: #240;
} }
html.dark .mdo p>em, html.z .mdo p>em,
html.dark .mdo li>em, html.z .mdo li>em,
html.dark .mdo td>em { html.z .mdo td>em {
color: #940; color: #940;
} }
} }

View File

@@ -135,22 +135,22 @@ var md_opt = {
}; };
(function () { (function () {
var l = localStorage, var l = localStorage,
drk = l.lightmode != 1, drk = l.light != 1,
btn = document.getElementById("lightswitch"), btn = document.getElementById("lightswitch"),
f = function (e) { f = function (e) {
if (e) { e.preventDefault(); drk = !drk; } if (e) { e.preventDefault(); drk = !drk; }
document.documentElement.setAttribute("class", drk? "dark":"light"); document.documentElement.className = drk? "z":"y";
btn.innerHTML = "go " + (drk ? "light":"dark"); btn.innerHTML = "go " + (drk ? "light":"dark");
l.lightmode = drk? 0:1; l.light = drk? 0:1;
}; };
btn.onclick = f; btn.onclick = f;
f(); f();
})(); })();
</script> </script>
<script src="/.cpr/util.js?_={{ ts }}"></script> <script src="/.cpr/util.js?_={{ ts }}"></script>
<script src="/.cpr/deps/marked.js?_={{ ts }}"></script> <script src="/.cpr/deps/marked.js?_={{ ts }}"></script>
<script src="/.cpr/md.js?_={{ ts }}"></script> <script src="/.cpr/md.js?_={{ ts }}"></script>
{%- if edit %} {%- if edit %}

View File

@@ -278,7 +278,7 @@ function convert_markdown(md_text, dest_dom) {
if (!txt) if (!txt)
nodes[a].textContent = href; nodes[a].textContent = href;
else if (href !== txt) else if (href !== txt)
nodes[a].setAttribute('class', 'vis'); nodes[a].className = 'vis';
} }
// todo-lists (should probably be a marked extension) // todo-lists (should probably be a marked extension)
@@ -294,7 +294,7 @@ function convert_markdown(md_text, dest_dom) {
var clas = done ? 'done' : 'pend'; var clas = done ? 'done' : 'pend';
var char = done ? 'Y' : 'N'; var char = done ? 'Y' : 'N';
dom_li.setAttribute('class', 'task-list-item'); dom_li.className = 'task-list-item';
dom_li.style.listStyleType = 'none'; dom_li.style.listStyleType = 'none';
var html = dom_li.innerHTML; var html = dom_li.innerHTML;
dom_li.innerHTML = dom_li.innerHTML =
@@ -468,11 +468,11 @@ function init_toc() {
for (var a = 0; a < anchors.length; a++) { for (var a = 0; a < anchors.length; a++) {
if (anchors[a].active) { if (anchors[a].active) {
anchors[a].active = false; anchors[a].active = false;
links[a].setAttribute('class', ''); links[a].className = '';
} }
} }
anchors[hit].active = true; anchors[hit].active = true;
links[hit].setAttribute('class', 'act'); links[hit].className = 'act';
} }
var pane_height = parseInt(getComputedStyle(dom_toc).height); var pane_height = parseInt(getComputedStyle(dom_toc).height);

View File

@@ -61,7 +61,7 @@
position: relative; position: relative;
scrollbar-color: #eb0 #f7f7f7; scrollbar-color: #eb0 #f7f7f7;
} }
html.dark #mt { html.z #mt {
color: #eee; color: #eee;
background: #222; background: #222;
border: 1px solid #777; border: 1px solid #777;
@@ -77,7 +77,7 @@ html.dark #mt {
background: #f97; background: #f97;
border-radius: .15em; border-radius: .15em;
} }
html.dark #save.force-save { html.z #save.force-save {
color: #fca; color: #fca;
background: #720; background: #720;
} }
@@ -102,7 +102,7 @@ html.dark #save.force-save {
#helpclose { #helpclose {
display: block; display: block;
} }
html.dark #helpbox { html.z #helpbox {
box-shadow: 0 .5em 2em #444; box-shadow: 0 .5em 2em #444;
background: #222; background: #222;
border: 1px solid #079; border: 1px solid #079;

View File

@@ -144,16 +144,16 @@ redraw = (function () {
map_pre = genmap(dom_pre, map_pre); map_pre = genmap(dom_pre, map_pre);
} }
function setsbs() { function setsbs() {
dom_wrap.setAttribute('class', ''); dom_wrap.className = '';
dom_swrap.setAttribute('class', ''); dom_swrap.className = '';
onresize(); onresize();
} }
function modetoggle() { function modetoggle() {
var mode = dom_nsbs.innerHTML; var mode = dom_nsbs.innerHTML;
dom_nsbs.innerHTML = mode == 'editor' ? 'preview' : 'editor'; dom_nsbs.innerHTML = mode == 'editor' ? 'preview' : 'editor';
mode += ' single'; mode += ' single';
dom_wrap.setAttribute('class', mode); dom_wrap.className = mode;
dom_swrap.setAttribute('class', mode); dom_swrap.className = mode;
onresize(); onresize();
} }
@@ -255,10 +255,10 @@ function Modpoll() {
console.log('modpoll...'); console.log('modpoll...');
var url = (document.location + '').split('?')[0] + '?raw&_=' + Date.now(); var url = (document.location + '').split('?')[0] + '?raw&_=' + Date.now();
var xhr = new XMLHttpRequest(); var xhr = new XHR();
xhr.open('GET', url, true); xhr.open('GET', url, true);
xhr.responseType = 'text'; xhr.responseType = 'text';
xhr.onreadystatechange = r.cb; xhr.onload = xhr.onerror = r.cb;
xhr.send(); xhr.send();
}; };
@@ -268,9 +268,6 @@ function Modpoll() {
return; return;
} }
if (this.readyState != XMLHttpRequest.DONE)
return;
if (this.status !== 200) { if (this.status !== 200) {
console.log('modpoll err ' + this.status + ": " + this.responseText); console.log('modpoll err ' + this.status + ": " + this.responseText);
return; return;
@@ -309,7 +306,7 @@ var modpoll = new Modpoll();
window.onbeforeunload = function (e) { window.onbeforeunload = function (e) {
if ((ebi("save").getAttribute('class') + '').indexOf('disabled') >= 0) if ((ebi("save").className + '').indexOf('disabled') >= 0)
return; //nice (todo) return; //nice (todo)
e.preventDefault(); //ff e.preventDefault(); //ff
@@ -321,7 +318,7 @@ window.onbeforeunload = function (e) {
function save(e) { function save(e) {
if (e) e.preventDefault(); if (e) e.preventDefault();
var save_btn = ebi("save"), var save_btn = ebi("save"),
save_cls = save_btn.getAttribute('class') + ''; save_cls = save_btn.className + '';
if (save_cls.indexOf('disabled') >= 0) if (save_cls.indexOf('disabled') >= 0)
return toast.inf(2, "no changes"); return toast.inf(2, "no changes");
@@ -336,10 +333,10 @@ function save(e) {
fd.append("body", txt); fd.append("body", txt);
var url = (document.location + '').split('?')[0]; var url = (document.location + '').split('?')[0];
var xhr = new XMLHttpRequest(); var xhr = new XHR();
xhr.open('POST', url, true); xhr.open('POST', url, true);
xhr.responseType = 'text'; xhr.responseType = 'text';
xhr.onreadystatechange = save_cb; xhr.onload = xhr.onerror = save_cb;
xhr.btn = save_btn; xhr.btn = save_btn;
xhr.txt = txt; xhr.txt = txt;
@@ -356,9 +353,6 @@ function save(e) {
} }
function save_cb() { function save_cb() {
if (this.readyState != XMLHttpRequest.DONE)
return;
if (this.status !== 200) if (this.status !== 200)
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, "")); return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
@@ -397,10 +391,10 @@ function save_cb() {
function run_savechk(lastmod, txt, btn, ntry) { function run_savechk(lastmod, txt, btn, ntry) {
// download the saved doc from the server and compare // download the saved doc from the server and compare
var url = (document.location + '').split('?')[0] + '?raw&_=' + Date.now(); var url = (document.location + '').split('?')[0] + '?raw&_=' + Date.now();
var xhr = new XMLHttpRequest(); var xhr = new XHR();
xhr.open('GET', url, true); xhr.open('GET', url, true);
xhr.responseType = 'text'; xhr.responseType = 'text';
xhr.onreadystatechange = savechk_cb; xhr.onload = xhr.onerror = savechk_cb;
xhr.lastmod = lastmod; xhr.lastmod = lastmod;
xhr.txt = txt; xhr.txt = txt;
xhr.btn = btn; xhr.btn = btn;
@@ -409,9 +403,6 @@ function run_savechk(lastmod, txt, btn, ntry) {
} }
function savechk_cb() { function savechk_cb() {
if (this.readyState != XMLHttpRequest.DONE)
return;
if (this.status !== 200) if (this.status !== 200)
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, "")); return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
@@ -678,7 +669,7 @@ function reLastIndexOf(txt, ptn, end) {
// table formatter // table formatter
function fmt_table(e) { function fmt_table(e) {
if (e) e.preventDefault(); if (e) e.preventDefault();
//dom_tbox.setAttribute('class', ''); //dom_tbox.className = '';
var txt = dom_src.value, var txt = dom_src.value,
ofs = dom_src.selectionStart, ofs = dom_src.selectionStart,
@@ -829,7 +820,7 @@ function fmt_table(e) {
// show unicode // show unicode
function mark_uni(e) { function mark_uni(e) {
if (e) e.preventDefault(); if (e) e.preventDefault();
dom_tbox.setAttribute('class', ''); dom_tbox.className = '';
var txt = dom_src.value, var txt = dom_src.value,
ptn = new RegExp('([^' + js_uni_whitelist + ']+)', 'g'), ptn = new RegExp('([^' + js_uni_whitelist + ']+)', 'g'),
@@ -989,14 +980,14 @@ var set_lno = (function () {
ebi('tools').onclick = function (e) { ebi('tools').onclick = function (e) {
if (e) e.preventDefault(); if (e) e.preventDefault();
var is_open = dom_tbox.getAttribute('class') != 'open'; var is_open = dom_tbox.className != 'open';
dom_tbox.setAttribute('class', is_open ? 'open' : ''); dom_tbox.className = is_open ? 'open' : '';
}; };
ebi('help').onclick = function (e) { ebi('help').onclick = function (e) {
if (e) e.preventDefault(); if (e) e.preventDefault();
dom_tbox.setAttribute('class', ''); dom_tbox.className = '';
var dom = ebi('helpbox'); var dom = ebi('helpbox');
var dtxt = dom.getElementsByTagName('textarea'); var dtxt = dom.getElementsByTagName('textarea');

View File

@@ -84,24 +84,24 @@ html .editor-toolbar>button.save.force-save {
/* darkmode */ /* darkmode */
html.dark .mdo, html.z .mdo,
html.dark .CodeMirror { html.z .CodeMirror {
border-color: #222; border-color: #222;
} }
html.dark, html.z,
html.dark body, html.z body,
html.dark .CodeMirror { html.z .CodeMirror {
background: #222; background: #222;
color: #ccc; color: #ccc;
} }
html.dark .CodeMirror-cursor { html.z .CodeMirror-cursor {
border-color: #fff; border-color: #fff;
} }
html.dark .CodeMirror-selected { html.z .CodeMirror-selected {
box-shadow: 0 0 1px #0cf inset; box-shadow: 0 0 1px #0cf inset;
} }
html.dark .CodeMirror-selected, html.z .CodeMirror-selected,
html.dark .CodeMirror-selectedtext { html.z .CodeMirror-selectedtext {
border-radius: .1em; border-radius: .1em;
background: #246; background: #246;
color: #fff; color: #fff;
@@ -109,37 +109,37 @@ html.dark .CodeMirror-selectedtext {
html.dark #mn a { html.z #mn a {
color: #ccc; color: #ccc;
} }
html.dark #mn a:not(:last-child):after { html.z #mn a:not(:last-child):after {
border-color: rgba(255,255,255,0.3); border-color: rgba(255,255,255,0.3);
} }
html.dark .editor-toolbar { html.z .editor-toolbar {
border-color: #2c2c2c; border-color: #2c2c2c;
background: #1c1c1c; background: #1c1c1c;
} }
html.dark .editor-toolbar>i.separator { html.z .editor-toolbar>i.separator {
border-left: 1px solid #444; border-left: 1px solid #444;
border-right: 1px solid #111; border-right: 1px solid #111;
} }
html.dark .editor-toolbar>button { html.z .editor-toolbar>button {
margin-left: -1px; border: 1px solid rgba(255,255,255,0.1); margin-left: -1px; border: 1px solid rgba(255,255,255,0.1);
color: #aaa; color: #aaa;
} }
html.dark .editor-toolbar>button:hover { html.z .editor-toolbar>button:hover {
color: #333; color: #333;
} }
html.dark .editor-toolbar>button.active { html.z .editor-toolbar>button.active {
color: #333; color: #333;
border-color: #ec1; border-color: #ec1;
background: #c90; background: #c90;
} }
html.dark .editor-toolbar::after, html.z .editor-toolbar::after,
html.dark .editor-toolbar::before { html.z .editor-toolbar::before {
background: none; background: none;
} }
@@ -150,6 +150,6 @@ html.dark .editor-toolbar::before {
padding: 1em; padding: 1em;
background: #f7f7f7; background: #f7f7f7;
} }
html.dark .mdo { html.z .mdo {
background: #1c1c1c; background: #1c1c1c;
} }

View File

@@ -34,18 +34,18 @@ var md_opt = {
var lightswitch = (function () { var lightswitch = (function () {
var l = localStorage, var l = localStorage,
drk = l.lightmode != 1, drk = l.light != 1,
f = function (e) { f = function (e) {
if (e) drk = !drk; if (e) drk = !drk;
document.documentElement.setAttribute("class", drk? "dark":"light"); document.documentElement.className = drk? "z":"y";
l.lightmode = drk? 0:1; l.light = drk? 0:1;
}; };
f(); f();
return f; return f;
})(); })();
</script> </script>
<script src="/.cpr/util.js?_={{ ts }}"></script> <script src="/.cpr/util.js?_={{ ts }}"></script>
<script src="/.cpr/deps/marked.js?_={{ ts }}"></script> <script src="/.cpr/deps/marked.js?_={{ ts }}"></script>
<script src="/.cpr/deps/easymde.js?_={{ ts }}"></script> <script src="/.cpr/deps/easymde.js?_={{ ts }}"></script>
<script src="/.cpr/mde.js?_={{ ts }}"></script> <script src="/.cpr/mde.js?_={{ ts }}"></script>

View File

@@ -114,10 +114,10 @@ function save(mde) {
fd.append("body", txt); fd.append("body", txt);
var url = (document.location + '').split('?')[0]; var url = (document.location + '').split('?')[0];
var xhr = new XMLHttpRequest(); var xhr = new XHR();
xhr.open('POST', url, true); xhr.open('POST', url, true);
xhr.responseType = 'text'; xhr.responseType = 'text';
xhr.onreadystatechange = save_cb; xhr.onload = xhr.onerror = save_cb;
xhr.btn = save_btn; xhr.btn = save_btn;
xhr.mde = mde; xhr.mde = mde;
xhr.txt = txt; xhr.txt = txt;
@@ -133,9 +133,6 @@ function save(mde) {
} }
function save_cb() { function save_cb() {
if (this.readyState != XMLHttpRequest.DONE)
return;
if (this.status !== 200) if (this.status !== 200)
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, "")); return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
@@ -170,10 +167,10 @@ function save_cb() {
// download the saved doc from the server and compare // download the saved doc from the server and compare
var url = (document.location + '').split('?')[0] + '?raw'; var url = (document.location + '').split('?')[0] + '?raw';
var xhr = new XMLHttpRequest(); var xhr = new XHR();
xhr.open('GET', url, true); xhr.open('GET', url, true);
xhr.responseType = 'text'; xhr.responseType = 'text';
xhr.onreadystatechange = save_chk; xhr.onload = xhr.onerror = save_chk;
xhr.btn = this.save_btn; xhr.btn = this.save_btn;
xhr.mde = this.mde; xhr.mde = this.mde;
xhr.txt = this.txt; xhr.txt = this.txt;
@@ -182,9 +179,6 @@ function save_cb() {
} }
function save_chk() { function save_chk() {
if (this.readyState != XMLHttpRequest.DONE)
return;
if (this.status !== 200) if (this.status !== 200)
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, "")); return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));

View File

@@ -2,49 +2,49 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>{{ svcname }}</title> <title>{{ svcname }}</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 }}
<link rel="stylesheet" media="screen" href="/.cpr/msg.css?_={{ ts }}"> <link rel="stylesheet" media="screen" href="/.cpr/msg.css?_={{ ts }}">
</head> </head>
<body> <body>
<div id="box"> <div id="box">
{%- if h1 %}
<h1>{{ h1 }}</h1>
{%- endif %}
{%- if h2 %}
<h2>{{ h2 }}</h2>
{%- endif %}
{%- if p %}
<p>{{ p }}</p>
{%- endif %}
{%- if pre %} {%- if h1 %}
<pre>{{ pre }}</pre> <h1>{{ h1 }}</h1>
{%- endif %} {%- endif %}
{%- if html %} {%- if h2 %}
{{ html }} <h2>{{ h2 }}</h2>
{%- endif %} {%- endif %}
{%- if click %} {%- if p %}
<script>document.getElementsByTagName("a")[0].click()</script> <p>{{ p }}</p>
{%- endif %} {%- endif %}
</div>
{%- if redir %} {%- if pre %}
<script> <pre>{{ pre }}</pre>
setTimeout(function() { {%- endif %}
window.location.replace("{{ redir }}");
}, 1000); {%- if html %}
</script> {{ html }}
{%- endif %} {%- endif %}
{%- if click %}
<script>document.getElementsByTagName("a")[0].click()</script>
{%- endif %}
</div>
{%- if redir %}
<script>
setTimeout(function() {
window.location.replace("{{ redir }}");
}, 1000);
</script>
{%- endif %}
</body> </body>
</html> </html>

View File

@@ -1,9 +1,7 @@
html, body, #wrap { html {
color: #333; color: #333;
background: #f7f7f7; background: #f7f7f7;
font-family: sans-serif; font-family: sans-serif;
}
html {
touch-action: manipulation; touch-action: manipulation;
} }
#wrap { #wrap {
@@ -38,7 +36,6 @@ a+a {
margin: -.2em 0 0 .5em; margin: -.2em 0 0 .5em;
} }
.logout, .logout,
.btns a,
a.r { a.r {
color: #c04; color: #c04;
border-color: #c7a; border-color: #c7a;
@@ -88,27 +85,24 @@ blockquote {
} }
html.dark, html.z {
html.dark body,
html.dark #wrap {
background: #222; background: #222;
color: #ccc; color: #ccc;
} }
html.dark h1 { html.z h1 {
border-color: #777; border-color: #777;
} }
html.dark a { html.z a {
color: #fff; color: #fff;
background: #057; background: #057;
border-color: #37a; border-color: #37a;
} }
html.dark .logout, html.z .logout,
html.dark .btns a, html.z a.r {
html.dark a.r {
background: #804; background: #804;
border-color: #c28; border-color: #c28;
} }
html.dark input { html.z input {
color: #fff; color: #fff;
background: #626; background: #626;
border: 1px solid #c2c; border: 1px solid #c2c;
@@ -117,6 +111,12 @@ html.dark input {
padding: .5em .7em; padding: .5em .7em;
margin: 0 .5em 0 0; margin: 0 .5em 0 0;
} }
html.dark .num { html.z .num {
border-color: #777; border-color: #777;
} }
html.bz {
color: #bbd;
background: #11121d;
}

View File

@@ -2,105 +2,106 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>{{ svcname }}</title> <title>{{ svcname }}</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 }}
<link rel="stylesheet" media="screen" href="/.cpr/splash.css?_={{ ts }}"> <link rel="stylesheet" media="screen" href="/.cpr/splash.css?_={{ ts }}">
<link rel="stylesheet" media="screen" href="/.cpr/ui.css?_={{ ts }}"> <link rel="stylesheet" media="screen" href="/.cpr/ui.css?_={{ ts }}">
</head> </head>
<body> <body>
<div id="wrap"> <div id="wrap">
<a href="/?h" class="refresh">refresh</a> <a id="a" href="/?h" class="refresh">refresh</a>
{%- if this.uname == '*' %} {%- if this.uname == '*' %}
<p>howdy stranger &nbsp; <small>(you're not logged in)</small></p> <p id="b">howdy stranger &nbsp; <small>(you're not logged in)</small></p>
{%- else %} {%- else %}
<a href="/?pw=x" class="logout">logout</a> <a id="c" href="/?pw=x" class="logout">logout</a>
<p>welcome back, <strong>{{ this.uname }}</strong></p> <p><span id="m">welcome back,</span> <strong>{{ this.uname }}</strong></p>
{%- endif %} {%- endif %}
{%- if msg %} {%- if msg %}
<div id="msg"> <div id="msg">
{{ msg }} {{ msg }}
</div> </div>
{%- endif %} {%- endif %}
{%- if avol %} {%- if avol %}
<h1>admin panel:</h1> <h1>admin panel:</h1>
<table><tr><td> <!-- hehehe --> <table><tr><td> <!-- hehehe -->
<table class="num"> <table class="num">
<tr><td>scanning</td><td>{{ scanning }}</td></tr> <tr><td>scanning</td><td>{{ scanning }}</td></tr>
<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>
</table> </table>
</td><td> </td><td>
<table class="vols"> <table class="vols">
<thead><tr><th>vol</th><th>action</th><th>status</th></tr></thead> <thead><tr><th>vol</th><th id="t">action</th><th>status</th></tr></thead>
<tbody> <tbody>
{% for mp in avol %} {% for mp in avol %}
{%- if mp in vstate and vstate[mp] %} {%- if mp in vstate and vstate[mp] %}
<tr><td><a href="{{ mp }}{{ url_suf }}">{{ mp }}</a></td><td><a href="{{ mp }}?scan">rescan</a></td><td>{{ vstate[mp] }}</td></tr> <tr><td><a href="{{ mp }}{{ url_suf }}">{{ mp }}</a></td><td><a class="s" href="{{ mp }}?scan">rescan</a></td><td>{{ vstate[mp] }}</td></tr>
{%- endif %} {%- endif %}
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</td></tr></table> </td></tr></table>
<div class="btns"> <div class="btns">
<a href="/?stack" tt="shows the state of all active threads">dump stack</a> <a id="d" href="/?stack" tt="shows the state of all active threads">dump stack</a>
<a href="/?reload=cfg" tt="reload config files (accounts/volumes/volflags),$Nand rescan all e2ds volumes">reload cfg</a> <a id="e" href="/?reload=cfg" tt="reload config files (accounts/volumes/volflags),$Nand rescan all e2ds volumes">reload cfg</a>
</div> </div>
{%- endif %} {%- endif %}
{%- if rvol %} {%- if rvol %}
<h1>you can browse these:</h1> <h1 id="f">you can browse:</h1>
<ul> <ul>
{% for mp in rvol %} {% for mp in rvol %}
<li><a href="{{ mp }}{{ url_suf }}">{{ mp }}</a></li> <li><a href="{{ mp }}{{ url_suf }}">{{ mp }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{%- endif %} {%- endif %}
{%- if wvol %} {%- if wvol %}
<h1>you can upload to:</h1> <h1 id="g">you can upload to:</h1>
<ul> <ul>
{% for mp in wvol %} {% for mp in wvol %}
<li><a href="{{ mp }}{{ url_suf }}">{{ mp }}</a></li> <li><a href="{{ mp }}{{ url_suf }}">{{ mp }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{%- endif %} {%- endif %}
<h1 id="cc">client config:</h1> <h1 id="cc">client config:</h1>
<ul> <ul>
{% if k304 %} {% if k304 %}
<li><a href="/?k304=n">disable k304</a> (currently enabled) <li><a id="h" href="/?k304=n">disable k304</a> (currently enabled)
{%- else %} {%- else %}
<li><a href="/?k304=y" class="r">enable k304</a> (currently disabled) <li><a id="i" href="/?k304=y" class="r">enable k304</a> (currently disabled)
{% endif %} {% endif %}
<blockquote>enabling this will disconnect your client on every HTTP 304, which can prevent some buggy browsers/proxies from getting stuck (suddenly not being able to load pages), <em>but</em> it will also make things slower in general</blockquote></li> <blockquote id="j">enabling this will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general</blockquote></li>
<li><a href="/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li> <li><a id="k" href="/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
</ul> </ul>
<h1>login for more:</h1> <h1 id="l">login for more:</h1>
<ul> <ul>
<form method="post" enctype="multipart/form-data" action="/{{ qvpath }}"> <form method="post" enctype="multipart/form-data" action="/{{ qvpath }}">
<input type="hidden" name="act" value="login" /> <input type="hidden" name="act" value="login" />
<input type="password" name="cppwd" /> <input type="password" name="cppwd" />
<input type="submit" value="Login" /> <input type="submit" value="Login" />
</form> </form>
</ul> </ul>
</div> </div>
<a href="#" id="repl">π</a> <a href="#" id="repl">π</a>
<script> <script>
document.documentElement.setAttribute("class", localStorage.lightmode == 1 ? "light" : "dark"); var lang="{{ this.args.lang }}";
document.documentElement.className=localStorage.theme||"{{ this.args.theme }}";
</script> </script>
<script src="/.cpr/util.js?_={{ ts }}"></script> <script src="/.cpr/util.js?_={{ ts }}"></script>
<script>tt.init();</script> <script src="/.cpr/splash.js?_={{ ts }}"></script>
</body> </body>
</html> </html>

44
copyparty/web/splash.js Normal file
View File

@@ -0,0 +1,44 @@
var Ls = {
"nor": {
"a1": "oppdater",
"b1": "halloien &nbsp; <small>(du er ikke logget inn)</small>",
"c1": "logg ut",
"d1": "tilstand",
"d2": "vis tilstanden til alle tråder",
"e1": "last innst.",
"e2": "leser inn konfigurasjonsfiler på nytt$N(kontoer, volumer, volumbrytere)$Nog kartlegger alle e2ds-volumer",
"f1": "du kan betrakte:",
"g1": "du kan laste opp til:",
"cc1": "klient-konfigurasjon",
"h1": "skru av k304",
"i1": "skru på k304",
"j1": "k304 bryter tilkoplingen for hver HTTP 304. Dette hjelper visse mellomtjenere som kan sette seg fast / plutselig slutter å laste sider, men det reduserer også ytelsen betydelig",
"k1": "nullstill innstillinger",
"l1": "logg inn:",
"m1": "velkommen tilbake,",
"n1": "404: filen finnes ikke &nbsp;┐( ´ -`)┌",
"o1": 'eller kanskje du ikke har tilgang? prøv å logge inn eller <a href="/?h">gå hjem</a>',
"p1": "403: tilgang nektet &nbsp;~┻━┻",
"q1": 'du må logge inn eller <a href="/?h">gå hjem</a>',
"r1": "gå hjem",
".s1": "kartlegg",
"t1": "handling",
}
},
d = Ls[sread("lang") || lang];
for (var k in (d || {})) {
var f = k.slice(-1),
i = k.slice(0, -1),
o = QSA(i.startsWith('.') ? i : '#' + i);
for (var a = 0; a < o.length; a++)
if (f == 1)
o[a].innerHTML = d[k];
else if (f == 2)
o[a].setAttribute("tt", d[k]);
}
tt.init();
if (!ebi('c'))
QS('input[name="cppwd"]').focus();

View File

@@ -11,6 +11,7 @@ html {
max-width: 34em; max-width: 34em;
max-width: min(34em, 90%); max-width: min(34em, 90%);
max-width: min(34em, calc(100% - 7em)); max-width: min(34em, calc(100% - 7em));
color: #ddd;
background: #333; background: #333;
border: 0 solid #777; border: 0 solid #777;
box-shadow: 0 .2em .5em #111; box-shadow: 0 .2em .5em #111;
@@ -74,6 +75,9 @@ html {
margin-right: -1.2em; margin-right: -1.2em;
padding-right: .7em; padding-right: .7em;
} }
#toast.r #toastb {
text-align: right;
}
#toast pre { #toast pre {
margin: 0; margin: 0;
} }
@@ -157,23 +161,32 @@ html {
#tt em { #tt em {
color: #f6a; color: #f6a;
} }
html.light #tt { html.y #tt {
color: #333;
background: #fff; background: #fff;
border-color: #888 #000 #777 #000; border-color: #888 #000 #777 #000;
} }
html.light #tt, html.bz #tt {
html.light #toast { background: #202231;
border-color: #3b3f58;
}
html.y #tt,
html.y #toast {
box-shadow: 0 .3em 1em rgba(0,0,0,0.4); box-shadow: 0 .3em 1em rgba(0,0,0,0.4);
} }
#modalc code, html.y #tt code {
html.light #tt code {
background: #060; background: #060;
color: #fff; color: #fff;
} }
html.light #tt em { #modalc code {
color: #060;
background: transparent;
border: 1px solid #ccc;
}
html.y #tt em {
color: #d38; color: #d38;
} }
html.light #tth { html.y #tth {
color: #000; color: #000;
background: #fff; background: #fff;
} }
@@ -273,9 +286,9 @@ html.light #tth {
box-shadow: 0 .1em .2em #fc0 inset; box-shadow: 0 .1em .2em #fc0 inset;
border-radius: .2em; border-radius: .2em;
} }
html.light *:focus, html.y *:focus,
html.light #pctl *:focus, html.y #pctl *:focus,
html.light .btn:focus { html.y .btn:focus {
box-shadow: 0 .1em .2em #037 inset; box-shadow: 0 .1em .2em #037 inset;
} }
input[type="text"]:focus, input[type="text"]:focus,
@@ -283,9 +296,9 @@ input:not([type]):focus,
textarea:focus { textarea:focus {
box-shadow: 0 .1em .3em #fc0, 0 -.1em .3em #fc0; box-shadow: 0 .1em .3em #fc0, 0 -.1em .3em #fc0;
} }
html.light input[type="text"]:focus, html.y input[type="text"]:focus,
html.light input:not([type]):focus, html.y input:not([type]):focus,
html.light textarea:focus { html.y textarea:focus {
box-shadow: 0 .1em .3em #037, 0 -.1em .3em #037; box-shadow: 0 .1em .3em #037, 0 -.1em .3em #037;
} }
@@ -414,7 +427,7 @@ html.light textarea:focus {
overflow-wrap: break-word; overflow-wrap: break-word;
word-wrap: break-word; /*ie*/ word-wrap: break-word; /*ie*/
} }
html.light .mdo a, html.y .mdo a,
.mdo a { .mdo a {
color: #fff; color: #fff;
background: #39b; background: #39b;
@@ -443,48 +456,58 @@ html.light textarea:focus {
html.dark .mdo a { html.z .mdo a {
background: #057; background: #057;
} }
html.dark .mdo h1 a, html.dark .mdo h4 a, html.z .mdo h1 a, html.z .mdo h4 a,
html.dark .mdo h2 a, html.dark .mdo h5 a, html.z .mdo h2 a, html.z .mdo h5 a,
html.dark .mdo h3 a, html.dark .mdo h6 a { html.z .mdo h3 a, html.z .mdo h6 a {
color: inherit; color: inherit;
background: none; background: none;
} }
html.dark .mdo pre, html.z .mdo pre,
html.dark .mdo code { html.z .mdo code {
color: #8c0; color: #8c0;
background: #1a1a1a; background: #1a1a1a;
border: .07em solid #333; border: .07em solid #333;
} }
html.dark .mdo ul, html.z .mdo ul,
html.dark .mdo ol { html.z .mdo ol {
border-color: #444; border-color: #444;
} }
html.dark .mdo strong { html.z .mdo strong {
color: #fff; color: #fff;
} }
html.dark .mdo p>em, html.z .mdo p>em,
html.dark .mdo li>em, html.z .mdo li>em,
html.dark .mdo td>em { html.z .mdo td>em {
color: #f94; color: #f94;
border-color: #666; border-color: #666;
} }
html.dark .mdo h1 { html.z .mdo h1 {
background: #383838; background: #383838;
border-top: .4em solid #b80; border-top: .4em solid #b80;
border-bottom: .4em solid #4c4c4c; border-bottom: .4em solid #4c4c4c;
} }
html.dark .mdo h2 { html.bz .mdo h1 {
background: #202231;
border: 1px solid #2d2f45;
border-width: 0 0 .4em 0;
}
html.z .mdo h2 {
background: #444; background: #444;
border-bottom: .22em solid #555; border-bottom: .22em solid #555;
} }
html.dark .mdo td, html.bz .mdo h2,
html.dark .mdo th { html.bz .mdo h3 {
background: transparent;
border-color: #3b3f58;
}
html.z .mdo td,
html.z .mdo th {
border-color: #444; border-color: #444;
} }
html.dark .mdo blockquote { html.z .mdo blockquote {
background: #282828; background: #282828;
border: .07em dashed #444; border: .07em dashed #444;
} }

View File

@@ -135,7 +135,7 @@ function up2k_flagbus() {
} }
function U2pvis(act, btns) { function U2pvis(act, btns, uc) {
var r = this; var r = this;
r.act = act; r.act = act;
r.ctr = { "ok": 0, "ng": 0, "bz": 0, "q": 0 }; r.ctr = { "ok": 0, "ng": 0, "bz": 0, "q": 0 };
@@ -425,7 +425,9 @@ function U2pvis(act, btns) {
html.push(r.genrow(a, true).replace(/><td>/, "><td>b ")); html.push(r.genrow(a, true).replace(/><td>/, "><td>b "));
} }
} }
ebi('u2tab').tBodies[0].innerHTML = html.join('\n'); var el = ebi('u2tab');
el.tBodies[0].innerHTML = html.join('\n');
el.className = (uc.fsearch ? 'srch ' : 'up ') + r.act;
}; };
r.genrow = function (nfile, as_html) { r.genrow = function (nfile, as_html) {
@@ -566,12 +568,12 @@ function Donut(uc, st) {
function fsearch_explain(n) { function fsearch_explain(n) {
if (n) if (n)
return toast.inf(60, 'your access to this folder is Read-Only\n\n' + (acct == '*' ? 'you are currently not logged in' : 'you are currently logged in as "' + acct + '"')); return toast.inf(60, L.ue_ro + (acct == '*' ? L.ue_nl : L.ue_la).format(acct));
if (bcfg_get('fsearch', false)) if (bcfg_get('fsearch', false))
return toast.inf(60, 'you are currently in file-search mode\n\nswitch to upload-mode by clicking the green magnifying glass (next to the big yellow search button), and try uploading again\n\nsorry'); return toast.inf(60, L.ue_sr);
return toast.inf(60, 'try again, it should work now'); return toast.inf(60, L.ue_ta);
} }
@@ -590,14 +592,7 @@ function up2k_init(subtle) {
ebi('u2notbtn').innerHTML = ''; ebi('u2notbtn').innerHTML = '';
} }
var suggest_up2k = 'this is the basic uploader; <a href="#" id="u2yea">up2k</a> is better'; var suggest_up2k = L.u_su2k;
var shame = 'your browser <a href="https://www.chromium.org/blink/webcrypto">disables sha512</a> unless you <a href="' + (window.location + '').replace(':', 's:') + '">use https</a>',
is_https = (window.location + '').indexOf('https:') === 0;
if (is_https)
// chrome<37 firefox<34 edge<12 opera<24 safari<7
shame = 'your browser is impressively ancient';
function got_deps() { function got_deps() {
return subtle || window.asmCrypto || window.hashwasm; return subtle || window.asmCrypto || window.hashwasm;
@@ -606,15 +601,18 @@ function up2k_init(subtle) {
var loading_deps = false; var loading_deps = false;
function init_deps() { function init_deps() {
if (!loading_deps && !got_deps()) { if (!loading_deps && !got_deps()) {
var fn = 'sha512.' + sha_js + '.js'; var fn = 'sha512.' + sha_js + '.js',
showmodal('<h1>loading ' + fn + '</h1><h2>since ' + shame + '</h2><h4>thanks chrome</h4>'); m = L.u_https1 + ' <a href="' + (window.location + '').replace(':', 's:') + '">' + L.u_https2 + '</a> ' + L.u_https3;
showmodal('<h1>loading ' + fn + '</h1>');
import_js('/.cpr/deps/' + fn, unmodal); import_js('/.cpr/deps/' + fn, unmodal);
if (is_https) if (is_https) {
ebi('u2foot').innerHTML = shame + ' so <em>this</em> uploader will do like 500 KiB/s at best'; // chrome<37 firefox<34 edge<12 opera<24 safari<7
else m = L.u_ancient;
ebi('u2foot').innerHTML = 'seems like ' + shame + ' so do that if you want more performance <span style="color:#' + setmsg('');
(sha_js == 'ac' ? 'c84">(expecting 20' : '8a5">(but dont worry too much, expect 100') + ' MiB/s)</span>'; }
ebi('u2foot').innerHTML = '<big>' + m + '</big>';
} }
loading_deps = true; loading_deps = true;
} }
@@ -624,11 +622,11 @@ function up2k_init(subtle) {
function setmsg(msg, type) { function setmsg(msg, type) {
if (msg !== undefined) { if (msg !== undefined) {
ebi('u2err').setAttribute('class', type); ebi('u2err').className = type;
ebi('u2err').innerHTML = msg; ebi('u2err').innerHTML = msg;
} }
else { else {
ebi('u2err').setAttribute('class', ''); ebi('u2err').className = '';
ebi('u2err').innerHTML = ''; ebi('u2err').innerHTML = '';
} }
if (msg == suggest_up2k) { if (msg == suggest_up2k) {
@@ -644,24 +642,8 @@ function up2k_init(subtle) {
return false; return false;
} }
ebi('u2nope').onclick = function (e) {
ev(e);
setmsg(suggest_up2k, 'msg');
goto('bup');
};
setmsg(suggest_up2k, 'msg'); setmsg(suggest_up2k, 'msg');
if (!String.prototype.format) {
String.prototype.format = function () {
var args = arguments;
return this.replace(/{(\d+)}/g, function (match, number) {
return typeof args[number] != 'undefined' ?
args[number] : match;
});
};
}
var parallel_uploads = icfg_get('nthread'), var parallel_uploads = icfg_get('nthread'),
uc = {}, uc = {},
fdom_ctr = 0, fdom_ctr = 0,
@@ -671,8 +653,8 @@ function up2k_init(subtle) {
bcfg_bind(uc, 'ask_up', 'ask_up', true, null, false); bcfg_bind(uc, 'ask_up', 'ask_up', true, null, false);
bcfg_bind(uc, 'flag_en', 'flag_en', false, apply_flag_cfg); bcfg_bind(uc, 'flag_en', 'flag_en', false, apply_flag_cfg);
bcfg_bind(uc, 'fsearch', 'fsearch', false, set_fsearch, false); bcfg_bind(uc, 'fsearch', 'fsearch', false, set_fsearch, false);
bcfg_bind(uc, 'turbo', 'u2turbo', false, draw_turbo, false); bcfg_bind(uc, 'turbo', 'u2turbo', turbolvl > 1, draw_turbo, false);
bcfg_bind(uc, 'datechk', 'u2tdate', true, null, false); bcfg_bind(uc, 'datechk', 'u2tdate', turbolvl < 3, null, false);
var st = { var st = {
"files": [], "files": [],
@@ -711,7 +693,7 @@ function up2k_init(subtle) {
}); });
} }
var pvis = new U2pvis("bz", '#u2cards'), var pvis = new U2pvis("bz", '#u2cards', uc),
donut = new Donut(uc, st); donut = new Donut(uc, st);
var bobslice = null; var bobslice = null;
@@ -719,7 +701,7 @@ function up2k_init(subtle) {
bobslice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice; bobslice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
if (!bobslice || !window.FileReader || !window.FileList) if (!bobslice || !window.FileReader || !window.FileList)
return un2k("this is the basic uploader; up2k needs at least<br />chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1"); return un2k(L.u_ever);
var flag = false; var flag = false;
apply_flag_cfg(); apply_flag_cfg();
@@ -741,14 +723,14 @@ function up2k_init(subtle) {
var mup, up = QS('#up_zd'); var mup, up = QS('#up_zd');
var msr, sr = QS('#srch_zd'); var msr, sr = QS('#srch_zd');
if (!has(perms, 'write')) if (!has(perms, 'write'))
mup = 'you do not have write-access to this folder'; mup = L.u_ewrite;
if (!has(perms, 'read')) if (!has(perms, 'read'))
msr = 'you do not have read-access to this folder'; msr = L.u_eread;
if (!have_up2k_idx) if (!have_up2k_idx)
msr = 'file-search is not enabled in server config'; msr = L.u_enoi;
up.querySelector('span').textContent = mup || 'drop it here'; up.querySelector('span').textContent = mup || L.udt_drop;
sr.querySelector('span').textContent = msr || 'drop it here'; sr.querySelector('span').textContent = msr || L.udt_drop;
clmod(up, 'err', mup); clmod(up, 'err', mup);
clmod(sr, 'err', msr); clmod(sr, 'err', msr);
clmod(up, 'ok', !mup); clmod(up, 'ok', !mup);
@@ -974,22 +956,22 @@ function up2k_init(subtle) {
function gotallfiles(good_files, nil_files, bad_files) { function gotallfiles(good_files, nil_files, bad_files) {
var ntot = good_files.concat(nil_files, bad_files).length; var ntot = good_files.concat(nil_files, bad_files).length;
if (bad_files.length) { if (bad_files.length) {
var msg = 'These {0} files (of {1} total) were skipped, possibly due to filesystem permissions:\n'.format(bad_files.length, ntot); var msg = L.u_badf.format(bad_files.length, ntot);
for (var a = 0, aa = Math.min(20, bad_files.length); a < aa; a++) for (var a = 0, aa = Math.min(20, bad_files.length); a < aa; a++)
msg += '-- ' + bad_files[a][1] + '\n'; msg += '-- ' + bad_files[a][1] + '\n';
msg += '\nMaybe it works better if you select just one file'; msg += L.u_just1;
return modal.alert(msg, function () { return modal.alert(msg, function () {
gotallfiles(good_files, nil_files, []); gotallfiles(good_files, nil_files, []);
}); });
} }
if (nil_files.length) { if (nil_files.length) {
var msg = 'These {0} files (of {1} total) are blank/empty; upload them anyways?\n'.format(nil_files.length, ntot); var msg = L.u_blankf.format(nil_files.length, ntot);
for (var a = 0, aa = Math.min(20, nil_files.length); a < aa; a++) for (var a = 0, aa = Math.min(20, nil_files.length); a < aa; a++)
msg += '-- ' + nil_files[a][1] + '\n'; msg += '-- ' + nil_files[a][1] + '\n';
msg += '\nMaybe it works better if you select just one file'; msg += L.u_just1;
return modal.confirm(msg, function () { return modal.confirm(msg, function () {
gotallfiles(good_files.concat(nil_files), [], []); gotallfiles(good_files.concat(nil_files), [], []);
}, function () { }, function () {
@@ -1003,12 +985,15 @@ function up2k_init(subtle) {
return a < b ? -1 : a > b ? 1 : 0; return a < b ? -1 : a > b ? 1 : 0;
}); });
var msg = ['{0} these {1} files?<ul>'.format(uc.fsearch ? 'search' : 'upload', good_files.length)]; var msg = [L.u_asku.format(good_files.length, esc(get_vpath())) + '<ul>'];
for (var a = 0, aa = Math.min(20, good_files.length); a < aa; a++) for (var a = 0, aa = Math.min(20, good_files.length); a < aa; a++)
msg.push('<li>' + esc(good_files[a][1]) + '</li>'); msg.push('<li>' + esc(good_files[a][1]) + '</li>');
if (uc.ask_up && !uc.fsearch) if (uc.ask_up && !uc.fsearch)
return modal.confirm(msg.join('') + '</ul>', function () { up_them(good_files); }, null); return modal.confirm(msg.join('') + '</ul>', function () {
up_them(good_files);
toast.inf(15, L.u_unpt);
}, null);
up_them(good_files); up_them(good_files);
} }
@@ -1060,7 +1045,7 @@ function up2k_init(subtle) {
pvis.addfile([ pvis.addfile([
uc.fsearch ? esc(entry.name) : linksplit( uc.fsearch ? esc(entry.name) : linksplit(
entry.purl + uricom_enc(entry.name)).join(' '), entry.purl + uricom_enc(entry.name)).join(' '),
'📐 hash', '📐 ' + L.u_hashing,
'' ''
], fobj.size, draw_each); ], fobj.size, draw_each);
@@ -1108,11 +1093,11 @@ function up2k_init(subtle) {
} }
if (!nhash) if (!nhash)
ebi('u2etah').innerHTML = 'Done ({0}, {1} files)'.format(humansize(st.bytes.hashed), pvis.ctr["ok"] + pvis.ctr["ng"]); ebi('u2etah').innerHTML = L.u_etadone.format(humansize(st.bytes.hashed), pvis.ctr["ok"] + pvis.ctr["ng"]);
if (!nsend && !nhash) if (!nsend && !nhash)
ebi('u2etau').innerHTML = ebi('u2etat').innerHTML = ( ebi('u2etau').innerHTML = ebi('u2etat').innerHTML = (
'Done ({0}, {1} files)'.format(humansize(st.bytes.uploaded), pvis.ctr["ok"] + pvis.ctr["ng"])); L.u_etadone.format(humansize(st.bytes.uploaded), pvis.ctr["ok"] + pvis.ctr["ng"]));
if (!st.busy.hash.length && !hashing_permitted()) if (!st.busy.hash.length && !hashing_permitted())
nhash = 0; nhash = 0;
@@ -1133,7 +1118,7 @@ function up2k_init(subtle) {
} }
if ((nhash || nsend) && !uc.fsearch) { if ((nhash || nsend) && !uc.fsearch) {
if (!st.bytes.finished) { if (!st.bytes.finished) {
ebi('u2etat').innerHTML = '(preparing to upload)'; ebi('u2etat').innerHTML = L.u_etaprep;
} }
else { else {
st.time.busy += td; st.time.busy += td;
@@ -1146,7 +1131,7 @@ function up2k_init(subtle) {
eta = Math.floor(rem / bps); eta = Math.floor(rem / bps);
if (t[a][1] < 1024 || t[a][3] < 0.1) { if (t[a][1] < 1024 || t[a][3] < 0.1) {
ebi(t[a][0]).innerHTML = '(preparing to upload)'; ebi(t[a][0]).innerHTML = L.u_etaprep;
continue; continue;
} }
@@ -1264,24 +1249,21 @@ function up2k_init(subtle) {
donut.on(is_busy); donut.on(is_busy);
if (!is_busy) { if (!is_busy) {
var k = uc.fsearch ? 'searches' : 'uploads', var sr = uc.fsearch,
ks = uc.fsearch ? 'Search' : 'Upload',
tok = uc.fsearch ? 'successful (found on server)' : 'completed successfully',
tng = uc.fsearch ? 'failed (NOT found on server)' : 'failed, sorry',
ok = pvis.ctr["ok"], ok = pvis.ctr["ok"],
ng = pvis.ctr["ng"], ng = pvis.ctr["ng"],
t = uc.ask_up ? 0 : 10; t = uc.ask_up ? 0 : 10;
if (ok && ng) if (ok && ng)
toast.warn(t, 'Finished, but some {0} failed:\n{1} {2},\n{3} {4}'.format(k, ok, tok, ng, tng)); toast.warn(t, (sr ? L.ur_sm : L.ur_um).format(ok, ng));
else if (ok > 1) else if (ok > 1)
toast.ok(t, 'All {1} {0} {2}'.format(k, ok, tok)); toast.ok(t, (sr ? L.ur_aso : L.ur_auo).format(ok));
else if (ok) else if (ok)
toast.ok(t, '{0} {1}'.format(ks, tok)); toast.ok(t, sr ? L.ur_1so : L.ur_1uo);
else if (ng > 1) else if (ng > 1)
toast.err(t, 'All {1} {0} {2}'.format(k, ng, tng)); toast.err(t, (sr ? L.ur_asn : L.ur_aun).format(ng));
else if (ng) else if (ng)
toast.err(t, '{0} {1}'.format(ks, tng)); toast.err(t, sr ? L.ur_1sn : L.ur_1un);
timer.rm(etafun); timer.rm(etafun);
timer.rm(donut.do); timer.rm(donut.do);
@@ -1476,7 +1458,6 @@ function up2k_init(subtle) {
min_filebuf = 1; min_filebuf = 1;
var td = Date.now() - t0; var td = Date.now() - t0;
if (td > 50) { if (td > 50) {
ebi('u2foot').innerHTML += "<p>excessive filereader latency (" + td + " ms), increasing readahead</p>";
min_filebuf = 32 * 1024 * 1024; min_filebuf = 32 * 1024 * 1024;
} }
} }
@@ -1535,7 +1516,7 @@ function up2k_init(subtle) {
t.t_hashed = Date.now(); t.t_hashed = Date.now();
pvis.seth(t.n, 2, 'hashing done'); pvis.seth(t.n, 2, L.u_hashdone);
pvis.seth(t.n, 1, '📦 wait'); pvis.seth(t.n, 1, '📦 wait');
apop(st.busy.hash, t); apop(st.busy.hash, t);
st.todo.handshake.push(t); st.todo.handshake.push(t);
@@ -1663,8 +1644,8 @@ function up2k_init(subtle) {
if (!response || !response.hits || !response.hits.length) { if (!response || !response.hits || !response.hits.length) {
smsg = '404'; smsg = '404';
msg = ('not found on server <a href="#" onclick="fsearch_explain(' + msg = (L.u_s404 + ' <a href="#" onclick="fsearch_explain(' +
(has(perms, 'write') ? '0' : '1') + ')" class="fsearch_explain">(explain)</a>'); (has(perms, 'write') ? '0' : '1') + ')" class="fsearch_explain">(' + L.u_expl + ')</a>');
} }
else { else {
smsg = 'found'; smsg = 'found';
@@ -1739,7 +1720,7 @@ function up2k_init(subtle) {
'npart': t.postlist[a] 'npart': t.postlist[a]
}); });
msg = 'uploading'; msg = L.u_upping;
done = false; done = false;
if (sort) if (sort)
@@ -1816,11 +1797,8 @@ function up2k_init(subtle) {
tasker(); tasker();
return; return;
} }
toast.err(0, "server broke; hs-err {0} on file [{1}]:\n".format( err = t.t_uploading ? L.u_ehsfin : t.srch ? L.u_ehssrch : L.u_ehsinit;
xhr.status, t.name) + ( xhrchk(xhr, err + ";\n\nfile: " + t.name + "\n\nerror ", "404, target folder not found");
(xhr.response && xhr.response.err) ||
(xhr.responseText && xhr.responseText) ||
"no further information"));
} }
} }
xhr.onload = function (e) { xhr.onload = function (e) {
@@ -1879,8 +1857,7 @@ function up2k_init(subtle) {
console.log("ignoring dupe-segment error", t); console.log("ignoring dupe-segment error", t);
} }
else { else {
toast.err(0, "server broke; cu-err {0} on file [{1}]:\n".format( xhrchk(xhr, L.u_cuerr2.format(npart, Math.ceil(t.size / chunksize), t.name), "404, target folder not found (???)");
xhr.status, t.name) + (txt || "no further information"));
chill(t); chill(t);
} }
@@ -1909,7 +1886,7 @@ function up2k_init(subtle) {
return; return;
if (!toast.visible) if (!toast.visible)
toast.warn(9.98, "failed to upload a chunk;\nprobably harmless, continuing\n\n" + t.name); toast.warn(9.98, L.u_cuerr.format(npart, Math.ceil(t.size / chunksize), t.name));
console.log('chunkpit onerror,', ++tries, t); console.log('chunkpit onerror,', ++tries, t);
orz2(xhr); orz2(xhr);
@@ -1933,6 +1910,7 @@ function up2k_init(subtle) {
// //
function onresize(e) { function onresize(e) {
// 10x faster than matchMedia('(min-width
var bar = ebi('ops'), var bar = ebi('ops'),
wpx = window.innerWidth, wpx = window.innerWidth,
fpx = parseInt(getComputedStyle(bar)['font-size']), fpx = parseInt(getComputedStyle(bar)['font-size']),
@@ -1942,22 +1920,19 @@ function up2k_init(subtle) {
parent = ebi(wide && write ? 'u2btn_cw' : 'u2btn_ct'), parent = ebi(wide && write ? 'u2btn_cw' : 'u2btn_ct'),
btn = ebi('u2btn'); btn = ebi('u2btn');
//console.log([wpx, fpx, wem]);
if (btn.parentNode !== parent) { if (btn.parentNode !== parent) {
parent.appendChild(btn); parent.appendChild(btn);
ebi('u2conf').setAttribute('class', wide); ebi('u2conf').className = ebi('u2cards').className = ebi('u2etaw').className = wide;
ebi('u2cards').setAttribute('class', wide);
ebi('u2etaw').setAttribute('class', wide);
} }
wide = write && wem > 78 ? 'ww' : wide; wide = write && wem > 78 ? 'ww' : wide;
parent = ebi(wide == 'ww' && write ? 'u2c3w' : 'u2c3t'); parent = ebi(wide == 'ww' && write ? 'u2c3w' : 'u2c3t');
var its = [ebi('u2etaw'), ebi('u2cards')]; var its = [ebi('u2etaw'), ebi('u2cards')];
if (its[0].parentNode !== parent) { if (its[0].parentNode !== parent) {
ebi('u2conf').setAttribute('class', wide); ebi('u2conf').className = wide;
for (var a = 0; a < 2; a++) { for (var a = 0; a < 2; a++) {
parent.appendChild(its[a]); parent.appendChild(its[a]);
its[a].setAttribute('class', wide); its[a].className = wide;
} }
} }
} }
@@ -2024,17 +1999,18 @@ function up2k_init(subtle) {
} }
function draw_turbo() { function draw_turbo() {
var msgu = '<p class="warn">WARNING: turbo enabled, <span>&nbsp;client may not detect and resume incomplete uploads; see turbo-button tooltip</span></p>', var msg = uc.fsearch ? L.u_ts : L.u_tu,
msgs = '<p class="warn">WARNING: turbo enabled, <span>&nbsp;search results can be incorrect; see turbo-button tooltip</span></p>', omsg = uc.fsearch ? L.u_tu : L.u_ts,
msg = uc.fsearch ? msgs : msgu,
omsg = uc.fsearch ? msgu : msgs,
html = ebi('u2foot').innerHTML, html = ebi('u2foot').innerHTML,
ohtml = html; ohtml = html;
if (uc.turbo && html.indexOf(msg) === -1) if (turbolvl || !uc.turbo)
msg = null;
if (msg && html.indexOf(msg) === -1)
html = html.replace(omsg, '') + msg; html = html.replace(omsg, '') + msg;
else if (!uc.turbo) else if (!msg)
html = html.replace(msgu, '').replace(msgs, ''); html = html.replace(L.u_tu, '').replace(L.u_ts, '');
if (html !== ohtml) if (html !== ohtml)
ebi('u2foot').innerHTML = html; ebi('u2foot').innerHTML = html;
@@ -2070,13 +2046,15 @@ function up2k_init(subtle) {
try { try {
var ico = uc.fsearch ? '🔎' : '🚀', var ico = uc.fsearch ? '🔎' : '🚀',
desc = uc.fsearch ? 'Search' : 'Upload'; desc = uc.fsearch ? L.ul_btns : L.ul_btnu;
clmod(ebi('op_up2k'), 'srch', uc.fsearch); clmod(ebi('op_up2k'), 'srch', uc.fsearch);
ebi('u2bm').innerHTML = ico + ' <sup>' + desc + '</sup>'; ebi('u2bm').innerHTML = ico + '&nbsp; <sup>' + desc + '</sup>';
} }
catch (ex) { } catch (ex) { }
ebi('u2tab').className = (uc.fsearch ? 'srch ' : 'up ') + pvis.act;
draw_turbo(); draw_turbo();
onresize(); onresize();
} }

View File

@@ -7,6 +7,7 @@ if (!window['console'])
var is_touch = 'ontouchstart' in window, var is_touch = 'ontouchstart' in window,
is_https = (window.location + '').indexOf('https:') === 0,
IPHONE = is_touch && /iPhone|iPad|iPod/i.test(navigator.userAgent), IPHONE = is_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);
@@ -14,7 +15,8 @@ var is_touch = 'ontouchstart' in window,
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); mknod = document.createElement.bind(document),
XHR = XMLHttpRequest;
function qsr(sel) { function qsr(sel) {
@@ -89,6 +91,9 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
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))
return; // chrome debugger
var ekey = url + '\n' + lineNo + '\n' + msg; var ekey = url + '\n' + lineNo + '\n' + msg;
if (ignexd[ekey] || crashed) if (ignexd[ekey] || crashed)
return; return;
@@ -249,6 +254,14 @@ if (!Element.prototype.closest)
} while (el !== null && el.nodeType === 1); } while (el !== null && el.nodeType === 1);
}; };
if (!String.prototype.format)
String.prototype.format = function () {
var args = arguments;
return this.replace(/{(\d+)}/g, function (match, number) {
return typeof args[number] != 'undefined' ?
args[number] : match;
});
};
// https://stackoverflow.com/a/950146 // https://stackoverflow.com/a/950146
function import_js(url, cb) { function import_js(url, cb) {
@@ -312,7 +325,7 @@ function clmod(el, cls, add) {
var n2 = n1.replace(re, ' ') + (add ? ' ' + cls : ''); var n2 = n1.replace(re, ' ') + (add ? ' ' + cls : '');
if (!n1 == !n2) if (n1 == n2)
return false; return false;
el.className = n2; el.className = n2;
@@ -327,11 +340,21 @@ function clgot(el, cls) {
if (el.classList) if (el.classList)
return el.classList.contains(cls); return el.classList.contains(cls);
var lst = (el.getAttribute('class') + '').split(/ /g); var lst = (el.className + '').split(/ /g);
return has(lst, cls); return has(lst, cls);
} }
var ANIM = true;
if (window.matchMedia) {
var mq = window.matchMedia('(prefers-reduced-motion: reduce)');
mq.onchange = function () {
ANIM = !mq.matches;
};
ANIM = !mq.matches;
}
function showsort(tab) { function showsort(tab) {
var v, vn, v1, v2, th = tab.tHead, var v, vn, v1, v2, th = tab.tHead,
sopts = jread('fsort', [["href", 1, ""]]); sopts = jread('fsort', [["href", 1, ""]]);
@@ -448,7 +471,7 @@ function linksplit(rp, id) {
q = '?' + q[1]; q = '?' + q[1];
} }
if (rp && rp.charAt(0) == '/') if (rp && rp[0] == '/')
rp = rp.slice(1); rp = rp.slice(1);
while (rp) { while (rp) {
@@ -872,7 +895,7 @@ var tt = (function () {
}; };
r.getmsg = function (el) { r.getmsg = function (el) {
if (QS('body.bbox-open')) if (IPHONE && QS('body.bbox-open'))
return; return;
var cfg = sread('tooltips'); var cfg = sread('tooltips');
@@ -1172,6 +1195,9 @@ var modal = (function () {
return ok(); return ok();
} }
if ((k == 'ArrowLeft' || k == 'ArrowRight') && eng && (ae == eok || ae == eng))
return (ae == eok ? eng : eok).focus() || ev(e);
if (k == 'Escape') if (k == 'Escape')
return ng(); return ng();
} }
@@ -1373,3 +1399,18 @@ var favico = (function () {
r.to = setTimeout(r.init, 100); r.to = setTimeout(r.init, 100);
return r; return r;
})(); })();
function xhrchk(xhr, prefix, e404) {
if (xhr.status < 400 && xhr.status >= 200)
return true;
if (xhr.status == 403)
return toast.err(0, prefix + (window.L && L.xhr403 || "403: access denied\n\ntry pressing F5, maybe you got logged out"));
if (xhr.status == 404)
return toast.err(0, prefix + e404);
return toast.err(0, prefix + xhr.status + ": " + (
(xhr.response && xhr.response.err) || xhr.responseText));
}

4
docs/notes.bat Normal file
View File

@@ -0,0 +1,4 @@
rem appending a static ip to a dhcp nic on windows 10-1703 or later
netsh interface ipv4 show interface
netsh interface ipv4 set interface interface="Ethernet 2" dhcpstaticipcoexistence=enabled
netsh interface ipv4 add address "Ethernet 2" 10.1.2.4 255.255.255.0

View File

@@ -2,9 +2,9 @@ FROM alpine:3.15
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.12 \ ver_marked=4.0.16 \
ver_mde=2.16.1 \ ver_mde=2.16.1 \
ver_codemirror=5.65.2 \ ver_codemirror=5.65.4 \
ver_fontawesome=5.13.0 \ ver_fontawesome=5.13.0 \
ver_zopfli=1.0.3 ver_zopfli=1.0.3
@@ -43,8 +43,6 @@ RUN mkdir -p /z/dist/no-pk \
# todo # todo
# https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/highlight.min.js
# https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/default.min.css
# https://prismjs.com/download.html#themes=prism-funky&languages=markup+css+clike+javascript+autohotkey+bash+basic+batch+c+csharp+cpp+cmake+diff+docker+go+ini+java+json+kotlin+latex+less+lisp+lua+makefile+objectivec+perl+powershell+python+r+jsx+ruby+rust+sass+scss+sql+swift+systemd+toml+typescript+vbnet+verilog+vhdl+yaml&plugins=line-highlight+line-numbers+autolinker # https://prismjs.com/download.html#themes=prism-funky&languages=markup+css+clike+javascript+autohotkey+bash+basic+batch+c+csharp+cpp+cmake+diff+docker+go+ini+java+json+kotlin+latex+less+lisp+lua+makefile+objectivec+perl+powershell+python+r+jsx+ruby+rust+sass+scss+sql+swift+systemd+toml+typescript+vbnet+verilog+vhdl+yaml&plugins=line-highlight+line-numbers+autolinker

View File

@@ -3,13 +3,13 @@ adds linetracking to marked.js v4.0.6;
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
@@ -50,4 +50,5 @@ function mangle(text) { @@ -52,4 +52,5 @@ function mangle(text) {
export class Lexer { export class Lexer {
constructor(options) { constructor(options) {
+ this.ln = 1; // like most editors, start couting from 1 + this.ln = 1; // like most editors, start couting from 1
this.tokens = []; this.tokens = [];
this.tokens.links = Object.create(null); this.tokens.links = Object.create(null);
@@ -127,4 +128,15 @@ export class Lexer { @@ -128,4 +129,15 @@ export class Lexer {
} }
+ set_ln(token, ln = this.ln) { + set_ln(token, ln = this.ln) {
@@ -25,9 +25,9 @@ add data-ln="%d" to most tags, %d is the source markdown line
+ +
/** /**
* Lexing * Lexing
@@ -134,7 +146,11 @@ export class Lexer { @@ -140,7 +152,11 @@ export class Lexer {
src = src.replace(/^ +$/gm, '');
} }
- let token, lastToken, cutSrc, lastParagraphClipped; - let token, lastToken, cutSrc, lastParagraphClipped;
+ let token, lastToken, cutSrc, lastParagraphClipped, ln; + let token, lastToken, cutSrc, lastParagraphClipped, ln;
@@ -38,111 +38,112 @@ add data-ln="%d" to most tags, %d is the source markdown line
+ +
if (this.options.extensions if (this.options.extensions
&& this.options.extensions.block && this.options.extensions.block
@@ -142,4 +158,5 @@ export class Lexer { @@ -148,4 +164,5 @@ export class Lexer {
if (token = extTokenizer.call({ lexer: this }, src, tokens)) { if (token = extTokenizer.call({ lexer: this }, src, tokens)) {
src = src.substring(token.raw.length); src = src.substring(token.raw.length);
+ this.set_ln(token, ln); + this.set_ln(token, ln);
tokens.push(token); tokens.push(token);
return true; return true;
@@ -153,4 +170,5 @@ export class Lexer { @@ -159,4 +176,5 @@ export class Lexer {
if (token = this.tokenizer.space(src)) { if (token = this.tokenizer.space(src)) {
src = src.substring(token.raw.length); src = src.substring(token.raw.length);
+ this.set_ln(token, ln); // is \n if not type + this.set_ln(token, ln); // is \n if not type
if (token.type) { if (token.raw.length === 1 && tokens.length > 0) {
tokens.push(token); // if there's a single \n as a spacer, it's terminating the last line,
@@ -162,4 +180,5 @@ export class Lexer { @@ -172,4 +190,5 @@ export class Lexer {
if (token = this.tokenizer.code(src)) { if (token = this.tokenizer.code(src)) {
src = src.substring(token.raw.length); src = src.substring(token.raw.length);
+ this.set_ln(token, ln); + this.set_ln(token, ln);
lastToken = tokens[tokens.length - 1]; lastToken = tokens[tokens.length - 1];
// An indented code block cannot interrupt a paragraph. // An indented code block cannot interrupt a paragraph.
@@ -177,4 +196,5 @@ export class Lexer { @@ -187,4 +206,5 @@ export class Lexer {
if (token = this.tokenizer.fences(src)) { if (token = this.tokenizer.fences(src)) {
src = src.substring(token.raw.length); src = src.substring(token.raw.length);
+ this.set_ln(token, ln); + this.set_ln(token, ln);
tokens.push(token); tokens.push(token);
continue; continue;
@@ -184,4 +204,5 @@ export class Lexer { @@ -194,4 +214,5 @@ export class Lexer {
if (token = this.tokenizer.heading(src)) { if (token = this.tokenizer.heading(src)) {
src = src.substring(token.raw.length); src = src.substring(token.raw.length);
+ this.set_ln(token, ln); + this.set_ln(token, ln);
tokens.push(token); tokens.push(token);
continue; continue;
@@ -191,4 +212,5 @@ export class Lexer { @@ -201,4 +222,5 @@ export class Lexer {
if (token = this.tokenizer.hr(src)) { if (token = this.tokenizer.hr(src)) {
src = src.substring(token.raw.length); src = src.substring(token.raw.length);
+ this.set_ln(token, ln); + this.set_ln(token, ln);
tokens.push(token); tokens.push(token);
continue; continue;
@@ -198,4 +220,5 @@ export class Lexer { @@ -208,4 +230,5 @@ export class Lexer {
if (token = this.tokenizer.blockquote(src)) { if (token = this.tokenizer.blockquote(src)) {
src = src.substring(token.raw.length); src = src.substring(token.raw.length);
+ this.set_ln(token, ln); + this.set_ln(token, ln);
tokens.push(token); tokens.push(token);
continue; continue;
@@ -205,4 +228,5 @@ export class Lexer { @@ -215,4 +238,5 @@ export class Lexer {
if (token = this.tokenizer.list(src)) { if (token = this.tokenizer.list(src)) {
src = src.substring(token.raw.length); src = src.substring(token.raw.length);
+ this.set_ln(token, ln); + this.set_ln(token, ln);
tokens.push(token); tokens.push(token);
continue; continue;
@@ -212,4 +236,5 @@ export class Lexer { @@ -222,4 +246,5 @@ export class Lexer {
if (token = this.tokenizer.html(src)) { if (token = this.tokenizer.html(src)) {
src = src.substring(token.raw.length); src = src.substring(token.raw.length);
+ this.set_ln(token, ln); + this.set_ln(token, ln);
tokens.push(token); tokens.push(token);
continue; continue;
@@ -219,4 +244,5 @@ export class Lexer { @@ -229,4 +254,5 @@ export class Lexer {
if (token = this.tokenizer.def(src)) { if (token = this.tokenizer.def(src)) {
src = src.substring(token.raw.length); src = src.substring(token.raw.length);
+ this.set_ln(token, ln); + this.set_ln(token, ln);
lastToken = tokens[tokens.length - 1]; lastToken = tokens[tokens.length - 1];
if (lastToken && (lastToken.type === 'paragraph' || lastToken.type === 'text')) { if (lastToken && (lastToken.type === 'paragraph' || lastToken.type === 'text')) {
@@ -236,4 +262,5 @@ export class Lexer { @@ -246,4 +272,5 @@ export class Lexer {
if (token = this.tokenizer.table(src)) { if (token = this.tokenizer.table(src)) {
src = src.substring(token.raw.length); src = src.substring(token.raw.length);
+ this.set_ln(token, ln); + this.set_ln(token, ln);
tokens.push(token); tokens.push(token);
continue; continue;
@@ -243,4 +270,5 @@ export class Lexer { @@ -253,4 +280,5 @@ export class Lexer {
if (token = this.tokenizer.lheading(src)) { if (token = this.tokenizer.lheading(src)) {
src = src.substring(token.raw.length); src = src.substring(token.raw.length);
+ this.set_ln(token, ln); + this.set_ln(token, ln);
tokens.push(token); tokens.push(token);
continue; continue;
@@ -263,4 +291,5 @@ export class Lexer { @@ -273,4 +301,5 @@ export class Lexer {
} }
if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) { if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) {
+ this.set_ln(token, ln); + this.set_ln(token, ln);
lastToken = tokens[tokens.length - 1]; lastToken = tokens[tokens.length - 1];
if (lastParagraphClipped && lastToken.type === 'paragraph') { if (lastParagraphClipped && lastToken.type === 'paragraph') {
@@ -280,4 +309,6 @@ export class Lexer { @@ -290,4 +319,6 @@ export class Lexer {
if (token = this.tokenizer.text(src)) { if (token = this.tokenizer.text(src)) {
src = src.substring(token.raw.length); src = src.substring(token.raw.length);
+ this.set_ln(token, ln); + this.set_ln(token, ln);
+ this.ln++; + this.ln++;
lastToken = tokens[tokens.length - 1]; lastToken = tokens[tokens.length - 1];
if (lastToken && lastToken.type === 'text') { if (lastToken && lastToken.type === 'text') {
@@ -355,4 +386,5 @@ export class Lexer { @@ -365,4 +396,5 @@ export class Lexer {
if (token = extTokenizer.call({ lexer: this }, src, tokens)) { if (token = extTokenizer.call({ lexer: this }, src, tokens)) {
src = src.substring(token.raw.length); src = src.substring(token.raw.length);
+ this.ln = token.ln || this.ln; + this.ln = token.ln || this.ln;
tokens.push(token); tokens.push(token);
return true; return true;
@@ -420,4 +452,6 @@ export class Lexer { @@ -430,4 +462,6 @@ export class Lexer {
if (token = this.tokenizer.br(src)) { if (token = this.tokenizer.br(src)) {
src = src.substring(token.raw.length); src = src.substring(token.raw.length);
+ // no need to reset (no more blockTokens anyways) + // no need to reset (no more blockTokens anyways)
+ token.ln = this.ln++; + token.ln = this.ln++;
tokens.push(token); tokens.push(token);
continue; continue;
@@ -462,4 +496,5 @@ export class Lexer { @@ -472,4 +506,5 @@ export class Lexer {
if (token = this.tokenizer.inlineText(cutSrc, smartypants)) { if (token = this.tokenizer.inlineText(cutSrc, smartypants)) {
src = src.substring(token.raw.length); src = src.substring(token.raw.length);
+ this.ln = token.ln || this.ln; + this.ln = token.ln || this.ln;
if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started
prevChar = token.raw.slice(-1); prevChar = token.raw.slice(-1);
diff --git a/src/Parser.js b/src/Parser.js diff --git a/src/Parser.js b/src/Parser.js
index a22a2bc..884ad66 100644
--- a/src/Parser.js --- a/src/Parser.js
+++ b/src/Parser.js +++ b/src/Parser.js
@@ -18,4 +18,5 @@ export class Parser { @@ -18,4 +18,5 @@ export class Parser {
@@ -205,6 +206,7 @@ diff --git a/src/Parser.js b/src/Parser.js
// Run any renderer extensions // Run any renderer extensions
if (this.options.extensions && this.options.extensions.renderers && this.options.extensions.renderers[token.type]) { if (this.options.extensions && this.options.extensions.renderers && this.options.extensions.renderers[token.type]) {
diff --git a/src/Renderer.js b/src/Renderer.js diff --git a/src/Renderer.js b/src/Renderer.js
index 7c36a75..aa1a53a 100644
--- a/src/Renderer.js --- a/src/Renderer.js
+++ b/src/Renderer.js +++ b/src/Renderer.js
@@ -11,6 +11,12 @@ export class Renderer { @@ -11,6 +11,12 @@ export class Renderer {
@@ -214,10 +216,10 @@ diff --git a/src/Renderer.js b/src/Renderer.js
} }
+ tag_ln(n) { + tag_ln(n) {
+ this.ln = ' data-ln="' + n + '"'; + this.ln = ` data-ln="${n}"`;
+ return this; + return this;
+ }; + };
+ +
code(code, infostring, escaped) { code(code, infostring, escaped) {
const lang = (infostring || '').match(/\S*/)[0]; const lang = (infostring || '').match(/\S*/)[0];
@@ -26,10 +32,10 @@ export class Renderer { @@ -26,10 +32,10 @@ export class Renderer {
@@ -233,65 +235,65 @@ diff --git a/src/Renderer.js b/src/Renderer.js
+ return '<pre' + this.ln + '><code class="' + return '<pre' + this.ln + '><code class="'
+ this.options.langPrefix + this.options.langPrefix
+ escape(lang, true) + escape(lang, true)
@@ -40,5 +46,5 @@ export class Renderer { @@ -43,5 +49,5 @@ export class Renderer {
*/
blockquote(quote) { blockquote(quote) {
- return '<blockquote>\n' + quote + '</blockquote>\n'; - return `<blockquote>\n${quote}</blockquote>\n`;
+ return '<blockquote' + this.ln + '>\n' + quote + '</blockquote>\n'; + return `<blockquote${this.ln}>\n${quote}</blockquote>\n`;
} }
@@ -51,4 +57,5 @@ export class Renderer { @@ -59,9 +65,9 @@ export class Renderer {
return '<h' if (this.options.headerIds) {
+ level const id = this.options.headerPrefix + slugger.slug(raw);
+ + this.ln - return `<h${level} id="${id}">${text}</h${level}>\n`;
+ ' id="' + return `<h${level}${this.ln} id="${id}">${text}</h${level}>\n`;
+ this.options.headerPrefix
@@ -61,5 +68,5 @@ export class Renderer {
} }
// ignore IDs // ignore IDs
- return '<h' + level + '>' + text + '</h' + level + '>\n'; - return `<h${level}>${text}</h${level}>\n`;
+ return '<h' + level + this.ln + '>' + text + '</h' + level + '>\n'; + return `<h${level}${this.ln}>${text}</h${level}>\n`;
} }
@@ -75,5 +82,5 @@ export class Renderer { @@ -80,5 +86,5 @@ export class Renderer {
*/
listitem(text) { listitem(text) {
- return '<li>' + text + '</li>\n'; - return `<li>${text}</li>\n`;
+ return '<li' + this.ln + '>' + text + '</li>\n'; + return `<li${this.ln}>${text}</li>\n`;
} }
@@ -87,5 +94,5 @@ export class Renderer { @@ -95,5 +101,5 @@ export class Renderer {
*/
paragraph(text) { paragraph(text) {
- return '<p>' + text + '</p>\n'; - return `<p>${text}</p>\n`;
+ return '<p' + this.ln + '>' + text + '</p>\n'; + return `<p${this.ln}>${text}</p>\n`;
} }
@@ -102,5 +109,5 @@ export class Renderer { @@ -117,5 +123,5 @@ export class Renderer {
*/
tablerow(content) { tablerow(content) {
- return '<tr>\n' + content + '</tr>\n'; - return `<tr>\n${content}</tr>\n`;
+ return '<tr' + this.ln + '>\n' + content + '</tr>\n'; + return `<tr${this.ln}>\n${content}</tr>\n`;
} }
@@ -127,5 +134,5 @@ export class Renderer { @@ -151,5 +157,5 @@ export class Renderer {
br() { br() {
- return this.options.xhtml ? '<br/>' : '<br>'; - return this.options.xhtml ? '<br/>' : '<br>';
+ return this.options.xhtml ? '<br' + this.ln + '/>' : '<br' + this.ln + '>'; + return this.options.xhtml ? `<br${this.ln}/>` : `<br${this.ln}>`;
} }
@@ -153,5 +160,5 @@ export class Renderer { @@ -190,5 +196,5 @@ export class Renderer {
} }
- let out = '<img src="' + href + '" alt="' + text + '"'; - let out = `<img src="${href}" alt="${text}"`;
+ let out = '<img' + this.ln + ' src="' + href + '" alt="' + text + '"'; + let out = `<img${this.ln} src="${href}" alt="${text}"`;
if (title) { if (title) {
out += ' title="' + title + '"'; out += ` title="${title}"`;
diff --git a/src/Tokenizer.js b/src/Tokenizer.js diff --git a/src/Tokenizer.js b/src/Tokenizer.js
index e8a69b6..2cc772b 100644
--- a/src/Tokenizer.js --- a/src/Tokenizer.js
+++ b/src/Tokenizer.js +++ b/src/Tokenizer.js
@@ -297,4 +297,7 @@ export class Tokenizer { @@ -302,4 +302,7 @@ export class Tokenizer {
const l = list.items.length; const l = list.items.length;
+ // each nested list gets +1 ahead; this hack makes every listgroup -1 but atleast it doesn't get infinitely bad + // each nested list gets +1 ahead; this hack makes every listgroup -1 but atleast it doesn't get infinitely bad

View File

@@ -14,6 +14,9 @@ help() { exec cat <<'EOF'
# #
# `gz` creates a gzip-compressed python sfx instead of bzip2 # `gz` creates a gzip-compressed python sfx instead of bzip2
# #
# `lang` limits which languages/translations to include,
# for example `lang eng` or `lang eng|nor`
#
# `no-cm` saves ~82k by removing easymde/codemirror # `no-cm` saves ~82k by removing easymde/codemirror
# (the fancy markdown editor) # (the fancy markdown editor)
# #
@@ -61,6 +64,7 @@ pybin=$(command -v python3 || command -v python) || {
exit 1 exit 1
} }
langs=
use_gz= use_gz=
zopf=2560 zopf=2560
while [ ! -z "$1" ]; do while [ ! -z "$1" ]; do
@@ -73,6 +77,7 @@ while [ ! -z "$1" ]; do
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=100 ; ;;
lang) shift;langs="$1"; ;;
*) help ; ;; *) help ; ;;
esac esac
shift shift
@@ -262,6 +267,13 @@ rm have
tmv "$f" tmv "$f"
} }
[ $langs ] &&
for f in copyparty/web/{browser.js,splash.js}; do
gzip -d "$f.gz" || true
awk '/^\}/{l=0} !l; /^var Ls =/{l=1;next} o; /^\t["}]/{o=0} /^\t"'"$langs"'"/{o=1;print}' <$f >t
tmv "$f"
done
[ $repack ] || [ $repack ] ||
find | grep -E '\.py$' | find | grep -E '\.py$' |
grep -vE '__version__' | grep -vE '__version__' |

View File

@@ -71,6 +71,7 @@ copyparty/web/msg.css,
copyparty/web/msg.html, copyparty/web/msg.html,
copyparty/web/splash.css, copyparty/web/splash.css,
copyparty/web/splash.html, copyparty/web/splash.html,
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,

View File

@@ -342,14 +342,15 @@ def get_payload():
def utime(top): def utime(top):
# avoid cleaners
i = 0 i = 0
files = [os.path.join(dp, p) for dp, dd, df in os.walk(top) for p in dd + df] files = [os.path.join(dp, p) for dp, dd, df in os.walk(top) for p in dd + df]
while WINDOWS: while WINDOWS or os.path.exists("/etc/systemd"):
t = int(time.time()) t = int(time.time())
if i: if i:
msg("utime {}, {}".format(i, t)) msg("utime {}, {}".format(i, t))
for f in files: for f in [top] + files:
os.utime(f, (t, t)) os.utime(f, (t, t))
i += 1 i += 1
@@ -374,16 +375,6 @@ def run(tmp, j2, ftp):
msg("sfxdir:", tmp) msg("sfxdir:", tmp)
msg() msg()
# block systemd-tmpfiles-clean.timer
try:
import fcntl
fd = os.open(tmp, os.O_RDONLY)
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except Exception as ex:
if not WINDOWS:
msg("\033[31mflock:{!r}\033[0m".format(ex))
t = threading.Thread(target=utime, args=(tmp,)) t = threading.Thread(target=utime, args=(tmp,))
t.daemon = True t.daemon = True
t.start() t.start()

View File

@@ -114,9 +114,10 @@ args = {
"install_requires": ["jinja2"], "install_requires": ["jinja2"],
"extras_require": { "extras_require": {
"thumbnails": ["Pillow"], "thumbnails": ["Pillow"],
"thumbnails2": ["pyvips"],
"audiotags": ["mutagen"], "audiotags": ["mutagen"],
"ftpd": ["pyftpdlib"], "ftpd": ["pyftpdlib"],
"ftps": ["pyopenssl"], "ftps": ["pyftpdlib", "pyopenssl"],
}, },
"entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]}, "entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]},
"scripts": ["bin/copyparty-fuse.py", "bin/up2k.py"], "scripts": ["bin/copyparty-fuse.py", "bin/up2k.py"],

View File

@@ -38,6 +38,9 @@ class Cfg(Namespace):
no_mv=False, no_mv=False,
no_del=False, no_del=False,
no_zip=False, no_zip=False,
no_thumb=False,
no_athumb=False,
no_vthumb=False,
no_voldump=True, no_voldump=True,
no_scandir=False, no_scandir=False,
no_sendfile=True, no_sendfile=True,
@@ -53,6 +56,10 @@ class Cfg(Namespace):
textfiles="", textfiles="",
doctitle="", doctitle="",
html_head="", html_head="",
theme=0,
themes=0,
turbo=0,
logout=573,
hist=None, hist=None,
no_idx=None, no_idx=None,
no_hash=None, no_hash=None,

View File

@@ -17,7 +17,7 @@ from copyparty import util
class Cfg(Namespace): class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None): 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" 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()} ex = {k: False for k in ex.split()}
ex2 = { ex2 = {
"mtp": [], "mtp": [],
@@ -36,6 +36,10 @@ class Cfg(Namespace):
"rsp_slp": 0, "rsp_slp": 0,
"s_wr_slp": 0, "s_wr_slp": 0,
"s_wr_sz": 512 * 1024, "s_wr_sz": 512 * 1024,
"theme": 0,
"themes": 0,
"turbo": 0,
"logout": 573,
} }
ex.update(ex2) ex.update(ex2)
super(Cfg, self).__init__(a=a or [], v=v or [], c=c, **ex) super(Cfg, self).__init__(a=a or [], v=v or [], c=c, **ex)