Compare commits

...

107 Commits

Author SHA1 Message Date
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
ed
96ceccd12a v1.2.3 2022-03-24 02:35:53 +01:00
ed
87994fe006 retry failed uploads with backoff 2022-03-24 02:29:59 +01:00
ed
fa12c81a03 zip-download files older than 1980-01-01 2022-03-24 01:31:50 +01:00
ed
344ce63455 basic-browser is implicitly not js 2022-03-21 01:20:47 +01:00
ed
ec4daacf9e v1.2.2 2022-03-20 06:15:57 +01:00
ed
f3e8308718 eh, better as volflags 2022-03-20 05:45:07 +01:00
ed
515ac5d941 show textfile name in document title 2022-03-20 03:40:21 +01:00
ed
954c7e7e50 add option to request noindex from crawlers 2022-03-20 03:23:42 +01:00
ed
67ff57f3a3 add option to disable html folder listings 2022-03-20 02:45:53 +01:00
ed
c10c70c1e5 misc 2022-03-04 21:30:31 +01:00
ed
04592a98d2 include all IPs + link status in server url listing 2022-03-04 21:29:28 +01:00
ed
c9c4aac6cf v1.2.1 2022-03-03 01:26:29 +01:00
ed
8b2c7586ce minimal py2 support for ftpd 2022-03-03 01:18:01 +01:00
ed
32e22dfe84 vendor asynchat for pyftpdlib 2022-03-03 01:16:52 +01:00
ed
d70b885722 failed attempt at upgrading scp 2022-03-03 00:17:03 +01:00
ed
ac6c4b13f5 add plaintext volume listing 2022-03-02 21:20:19 +01:00
ed
ececdad22d and increase debounce a bit 2022-03-02 01:56:05 +01:00
ed
bf659781b0 try some more spacing 2022-03-02 01:49:15 +01:00
ed
2c6bb195a4 search: get rid of inner-joins to fix -tags 2022-03-02 00:35:04 +01:00
ed
c032cd08b3 prisonparty: clean exit on sigterm/int 2022-02-27 20:07:28 +01:00
ed
39e7a7a231 sfx: prefer system pyftpdlib if available 2022-02-13 21:00:13 +01:00
ed
6e14cd2c39 graduate copyparty-sfx.sh 2022-02-13 20:44:03 +01:00
58 changed files with 2775 additions and 1499 deletions

View File

@@ -62,6 +62,8 @@ turn your phone or raspi into a portable file server with resumable uploads/down
* [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload * [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload
* [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
* [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
@@ -83,7 +85,7 @@ turn your phone or raspi into a portable file server with resumable uploads/down
* [optional dependencies](#optional-dependencies) - install these to enable bonus features * [optional dependencies](#optional-dependencies) - install these to enable bonus features
* [install recommended deps](#install-recommended-deps) * [install recommended deps](#install-recommended-deps)
* [optional gpl stuff](#optional-gpl-stuff) * [optional gpl stuff](#optional-gpl-stuff)
* [sfx](#sfx) - there are two self-contained "binaries" * [sfx](#sfx) - the self-contained "binary"
* [sfx repack](#sfx-repack) - reduce the size of an sfx by removing features * [sfx repack](#sfx-repack) - reduce the size of an sfx by removing features
* [install on android](#install-on-android) * [install on android](#install-on-android)
* [reporting bugs](#reporting-bugs) - ideas for context to include in bug reports * [reporting bugs](#reporting-bugs) - ideas for context to include in bug reports
@@ -173,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)
@@ -246,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...
@@ -402,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`)
@@ -792,6 +798,40 @@ and it will occupy the parsing threads, so fork anything expensive, or if you wa
if this becomes popular maybe there should be a less janky way to do it actually if this becomes popular maybe there should be a less janky way to do it actually
## hiding from google
tell search engines you dont wanna be indexed, either using the good old [robots.txt](https://www.robotstxt.org/robotstxt.html) or through copyparty settings:
* `--no-robots` adds HTTP (`X-Robots-Tag`) and HTML (`<meta>`) headers with `noindex, nofollow` globally
* volume-flag `[...]:c,norobots` does the same thing for that single volume
* volume-flag `[...]:c,robots` ALLOWS search-engine crawling for that volume, even if `--no-robots` is set globally
also, `--force-js` disables the plain HTML folder listing, making things harder to parse for search engines
## 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/165864904-c5b67ddd-f383-4b9e-9f5a-a3bde183d256.png"><img src="https://user-images.githubusercontent.com/241032/165867556-077b6068-2488-4fae-bf88-1fce40e719bc.png"></a>
2. flat dark</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/165864903-7fba1cb9-036b-4f11-90d5-28b7c0724353.png"><img src="https://user-images.githubusercontent.com/241032/165867557-b5cc0010-d880-48b1-8156-9c84f7bbc521.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
@@ -841,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 |
@@ -1075,15 +1115,22 @@ mandatory deps:
install these to enable bonus features install these to enable bonus features
enable ftp-server:
* for just plaintext FTP, `pyftpdlib` (is built into the SFX)
* with TLS encryption, `pyftpdlib pyopenssl`
enable music tags: enable music tags:
* either `mutagen` (fast, pure-python, skips a few tags, makes copyparty GPL? idk) * either `mutagen` (fast, pure-python, skips a few tags, makes copyparty GPL? idk)
* or `ffprobe` (20x slower, more accurate, possibly dangerous depending on your distro and users) * or `ffprobe` (20x slower, more accurate, possibly dangerous depending on your distro and users)
enable [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
@@ -1101,13 +1148,7 @@ these are standalone programs and will never be imported / evaluated by copypart
# sfx # sfx
there are two self-contained "binaries": the self-contained "binary" [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) will unpack itself and run copyparty, assuming you have python installed of course
* [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) -- pure python, works everywhere, **recommended**
* [copyparty-sfx.sh](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.sh) -- smaller, but only for linux and macos, kinda deprecated
launch either of them (**use sfx.py on systemd**) and it'll unpack and run copyparty, assuming you have python installed of course
pls note that `copyparty-sfx.sh` will fail if you rename `copyparty-sfx.py` to `copyparty.py` and keep it in the same folder because `sys.path` is funky
## sfx repack ## sfx repack
@@ -1134,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
@@ -1163,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
``` ```
@@ -1182,8 +1227,8 @@ mv /tmp/pe-copyparty/copyparty/web/deps/ copyparty/web/deps/
then build the sfx using any of the following examples: then build the sfx using any of the following examples:
```sh ```sh
./scripts/make-sfx.sh # both python and sh editions ./scripts/make-sfx.sh # regular edition
./scripts/make-sfx.sh no-sh gz # just python with gzip ./scripts/make-sfx.sh gz no-cm # gzip-compressed + no fancy markdown editor
``` ```

View File

@@ -4,8 +4,8 @@ set -e
# install dependencies for audio-*.py # install dependencies for audio-*.py
# #
# linux/alpine: requires {python3,ffmpeg,fftw}-dev py3-{wheel,pip} py3-numpy{,-dev} patchelf cmake # linux/alpine: requires gcc g++ make cmake patchelf {python3,ffmpeg,fftw,libsndfile}-dev py3-{wheel,pip} py3-numpy{,-dev}
# linux/debian: requires libav{codec,device,filter,format,resample,util}-dev {libfftw3,python3}-dev python3-{numpy,pip} vamp-{plugin-sdk,examples} patchelf cmake # linux/debian: requires libav{codec,device,filter,format,resample,util}-dev {libfftw3,python3,libsndfile1}-dev python3-{numpy,pip} vamp-{plugin-sdk,examples} patchelf cmake
# win64: requires msys2-mingw64 environment # win64: requires msys2-mingw64 environment
# macos: requires macports # macos: requires macports
# #

View File

@@ -122,5 +122,7 @@ export LOGNAME="$USER"
#echo "pybin [$pybin]" #echo "pybin [$pybin]"
#echo "pyarg [$pyarg]" #echo "pyarg [$pyarg]"
#echo "cpp [$cpp]" #echo "cpp [$cpp]"
chroot --userspec=$uid:$gid "$jail" "$pybin" $pyarg "$cpp" "$@" chroot --userspec=$uid:$gid "$jail" "$pybin" $pyarg "$cpp" "$@" &
p=$!
trap 'kill $p' INT TERM
wait

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,24 +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 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("--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")
@@ -480,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")
@@ -498,54 +515,66 @@ 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("--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("--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("--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, 0) VERSION = (1, 2, 9)
CODENAME = "ftp btw" CODENAME = "ftp btw"
BUILD_DT = (2022, 2, 13) BUILD_DT = (2022, 5, 12)
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,11 +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,
uncyg, uncyg,
undot, undot,
relchk,
unhumanize, unhumanize,
absreal, absreal,
Pebkac, Pebkac,
@@ -334,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
@@ -403,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)
@@ -436,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
@@ -740,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)
@@ -861,6 +874,30 @@ class AuthSrv(object):
if use: if use:
vol.lim = lim vol.lim = lim
if self.args.no_robots:
for vol in vfs.all_vols.values():
# volflag "robots" overrides global "norobots", allowing indexing by search engines for this vol
if not vol.flags.get("robots"):
vol.flags["norobots"] = True
for vol in vfs.all_vols.values():
h = [vol.flags.get("html_head", self.args.html_head)]
if vol.flags.get("norobots"):
h.insert(0, META_NOBOTS)
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:
@@ -1075,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 "/"
@@ -1087,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]
@@ -1098,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

@@ -7,20 +7,33 @@ import stat
import time import time
import logging import logging
import threading import threading
from typing import TYPE_CHECKING
from .__init__ import E, PY2
from .util import Pebkac, fsenc, exclude_dotfiles
from .bos import bos
try:
from pyftpdlib.ioloop import IOLoop
except ImportError:
p = os.path.join(E.mod, "vend")
print("loading asynchat from " + p)
sys.path.append(p)
from pyftpdlib.ioloop import IOLoop
from pyftpdlib.authorizers import DummyAuthorizer, AuthenticationFailed from pyftpdlib.authorizers import DummyAuthorizer, AuthenticationFailed
from pyftpdlib.filesystems import AbstractedFS, FilesystemError from pyftpdlib.filesystems import AbstractedFS, FilesystemError
from pyftpdlib.handlers import FTPHandler from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer from pyftpdlib.servers import FTPServer
from pyftpdlib.ioloop import IOLoop
from pyftpdlib.log import config_logging from pyftpdlib.log import config_logging
from .__init__ import E
from .util import Pebkac, fsenc, exclude_dotfiles
from .bos import bos
if TYPE_CHECKING: try:
from .svchub import SvcHub from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .svchub import SvcHub
except ImportError:
pass
class FtpAuth(DummyAuthorizer): class FtpAuth(DummyAuthorizer):
@@ -248,7 +261,10 @@ class FtpHandler(FTPHandler):
abstracted_fs = FtpFs abstracted_fs = FtpFs
def __init__(self, conn, server, ioloop=None): def __init__(self, conn, server, ioloop=None):
super(FtpHandler, self).__init__(conn, server, ioloop) if PY2:
FTPHandler.__init__(self, conn, server, ioloop)
else:
super(FtpHandler, self).__init__(conn, server, ioloop)
# abspath->vpath mapping to resolve log_transfer paths # abspath->vpath mapping to resolve log_transfer paths
self.vfs_map = {} self.vfs_map = {}

View File

@@ -65,6 +65,11 @@ class HttpCli(object):
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
"Cache-Control": "no-store; max-age=0", "Cache-Control": "no-store; max-age=0",
} }
h = self.args.html_head
if self.args.no_robots:
h = META_NOBOTS + (("\n" + h) if h else "")
self.out_headers["X-Robots-Tag"] = "noindex, nofollow"
self.html_head = h
def log(self, msg, c=0): def log(self, msg, c=0):
ptn = self.asrv.re_pwd ptn = self.asrv.re_pwd
@@ -93,6 +98,7 @@ class HttpCli(object):
if ka: if ka:
ka["ts"] = self.conn.hsrv.cachebuster() ka["ts"] = self.conn.hsrv.cachebuster()
ka["svcname"] = self.args.doctitle ka["svcname"] = self.args.doctitle
ka["html_head"] = self.html_head
return tpl.render(**ka) return tpl.render(**ka)
return tpl return tpl
@@ -115,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 + " ]")
@@ -131,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"
@@ -205,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:
@@ -227,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
@@ -268,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:
@@ -338,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):
""" """
@@ -858,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()
@@ -989,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)
@@ -1001,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)
@@ -1040,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
@@ -1677,13 +1709,15 @@ class HttpCli(object):
boundary = "\roll\tide" boundary = "\roll\tide"
targs = { targs = {
"ts": self.conn.hsrv.cachebuster(),
"svcname": self.args.doctitle,
"html_head": self.html_head,
"edit": "edit" in self.uparam, "edit": "edit" in self.uparam,
"title": html_escape(self.vpath, crlf=True), "title": html_escape(self.vpath, crlf=True),
"lastmod": int(ts_md * 1000), "lastmod": int(ts_md * 1000),
"md_plug": "true" if self.args.emp else "false", "md_plug": "true" if self.args.emp else "false",
"md_chk_rate": self.args.mcr, "md_chk_rate": self.args.mcr,
"md": boundary, "md": boundary,
"ts": self.conn.hsrv.cachebuster(),
"arg_base": arg_base, "arg_base": arg_base,
} }
html = template.render(**targs).encode("utf-8", "replace") html = template.render(**targs).encode("utf-8", "replace")
@@ -1732,6 +1766,31 @@ class HttpCli(object):
vstate = {} vstate = {}
vs = {"scanning": None, "hashq": None, "tagq": None, "mtpq": None} vs = {"scanning": None, "hashq": None, "tagq": None, "mtpq": None}
if self.uparam.get("ls") in ["v", "t", "txt"]:
if self.uname == "*":
txt = "howdy stranger (you're not logged in)"
else:
txt = "welcome back {}".format(self.uname)
if vstate:
txt += "\nstatus:"
for k in ["scanning", "hashq", "tagq", "mtpq"]:
txt += " {}({})".format(k, vs[k])
if rvol:
txt += "\nyou can browse:"
for v in rvol:
txt += "\n " + v
if wvol:
txt += "\nyou can upload to:"
for v in wvol:
txt += "\n " + v
txt = txt.encode("utf-8", "replace") + b"\n"
self.reply(txt, mime="text/plain; charset=utf-8")
return True
html = self.j2( html = self.j2(
"splash", "splash",
this=self, this=self,
@@ -1768,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>404 not found &nbsp;┐( ´ -`)┌</h1><p>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>403 forbiddena &nbsp;~┻━┻</h1><p>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>404 not found &nbsp;┐( ´ -`)┌</h1><p><a 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):
@@ -1819,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):
@@ -2041,6 +2102,12 @@ class HttpCli(object):
): ):
raise Pebkac(403) raise Pebkac(403)
self.html_head = vn.flags.get("html_head", "")
if vn.flags.get("norobots"):
self.out_headers["X-Robots-Tag"] = "noindex, nofollow"
else:
self.out_headers.pop("X-Robots-Tag", None)
is_dir = stat.S_ISDIR(st.st_mode) is_dir = stat.S_ISDIR(st.st_mode)
if self.can_read: if self.can_read:
th_fmt = self.uparam.get("th") th_fmt = self.uparam.get("th")
@@ -2058,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)
@@ -2110,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:
@@ -2129,13 +2193,14 @@ 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.cookies.get("js") == "y" is_js = self.args.force_js or self.cookies.get("js") == "y"
tpl = "browser" tpl = "browser"
if "b" in self.uparam: if "b" in self.uparam:
tpl = "browser2" tpl = "browser2"
is_js = False
logues = ["", ""] logues = ["", ""]
if not self.args.no_logues: if not self.args.no_logues:
@@ -2187,6 +2252,9 @@ 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,
"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:

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

@@ -5,7 +5,7 @@ import tarfile
import threading import threading
from .sutil import errdesc from .sutil import errdesc
from .util import Queue, fsenc from .util import Queue, fsenc, min_ex
from .bos import bos from .bos import bos
@@ -88,8 +88,9 @@ class StreamTar(object):
try: try:
self.ser(f) self.ser(f)
except Exception as ex: except Exception:
errors.append([f["vp"], repr(ex)]) ex = min_ex(5, True).replace("\n", "\n-- ")
errors.append([f["vp"], ex])
if errors: if errors:
self.errf, txt = errdesc(errors) self.errf, txt = errdesc(errors)

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:
@@ -362,7 +382,7 @@ class SvcHub(object):
src = ansi_re.sub("", src) src = ansi_re.sub("", src)
elif c: elif c:
if isinstance(c, int): if isinstance(c, int):
msg = "\033[3{}m{}".format(c, msg) msg = "\033[3{}m{}\033[0m".format(c, msg)
elif "\033" not in c: elif "\033" not in c:
msg = "\033[{}m{}\033[0m".format(c, msg) msg = "\033[{}m{}\033[0m".format(c, msg)
else: else:

View File

@@ -1,13 +1,12 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import os
import time import time
import zlib import zlib
from datetime import datetime from datetime import datetime
from .sutil import errdesc from .sutil import errdesc
from .util import yieldfile, sanitize_fn, spack, sunpack from .util import yieldfile, sanitize_fn, spack, sunpack, min_ex
from .bos import bos from .bos import bos
@@ -36,7 +35,10 @@ def unixtime2dos(ts):
bd = ((dy - 1980) << 9) + (dm << 5) + dd bd = ((dy - 1980) << 9) + (dm << 5) + dd
bt = (th << 11) + (tm << 5) + ts // 2 bt = (th << 11) + (tm << 5) + ts // 2
return spack(b"<HH", bt, bd) try:
return spack(b"<HH", bt, bd)
except:
return b"\x00\x00\x21\x00"
def gen_fdesc(sz, crc32, z64): def gen_fdesc(sz, crc32, z64):
@@ -244,8 +246,9 @@ class StreamZip(object):
try: try:
for x in self.ser(f): for x in self.ser(f):
yield x yield x
except Exception as ex: except Exception:
errors.append([f["vp"], repr(ex)]) ex = min_ex(5, True).replace("\n", "\n-- ")
errors.append([f["vp"], ex])
if errors: if errors:
errf, txt = errdesc(errors) errf, txt = errdesc(errors)

View File

@@ -57,13 +57,19 @@ class TcpSrv(object):
msgs = [] msgs = []
title_tab = {} title_tab = {}
title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")] title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")]
m = "available @ http://{}:{}/ (\033[33m{}\033[0m)" m = "available @ {}://{}:{}/ (\033[33m{}\033[0m)"
for ip, desc in sorted(eps.items(), key=lambda x: x[1]): for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
for port in sorted(self.args.p): for port in sorted(self.args.p):
if port not in ok.get(ip, ok.get("0.0.0.0", [])): if port not in ok.get(ip, ok.get("0.0.0.0", [])):
continue continue
msgs.append(m.format(ip, port, desc)) proto = " http"
if self.args.http_only:
pass
elif self.args.https_only or port == 443:
proto = "https"
msgs.append(m.format(proto, ip, port, desc))
if not self.args.wintitle: if not self.args.wintitle:
continue continue
@@ -144,10 +150,15 @@ class TcpSrv(object):
return eps return eps
r = re.compile(r"^\s+inet ([^ ]+)/.* (.*)") r = re.compile(r"^\s+inet ([^ ]+)/.* (.*)")
ri = re.compile(r"^\s*[0-9]+\s*:.*")
up = False
for ln in txt.split("\n"): for ln in txt.split("\n"):
if ri.match(ln):
up = "UP" in re.split("[>,< ]", ln)
try: try:
ip, dev = r.match(ln.rstrip()).groups() ip, dev = r.match(ln.rstrip()).groups()
eps[ip] = dev eps[ip] = dev + ("" if up else ", \033[31mLINK-DOWN")
except: except:
pass pass
@@ -177,6 +188,7 @@ class TcpSrv(object):
def ips_windows_ipconfig(self): def ips_windows_ipconfig(self):
eps = {} eps = {}
offs = {}
try: try:
txt, _ = chkcmd(["ipconfig"]) txt, _ = chkcmd(["ipconfig"])
except: except:
@@ -184,18 +196,29 @@ class TcpSrv(object):
rdev = re.compile(r"(^[^ ].*):$") rdev = re.compile(r"(^[^ ].*):$")
rip = re.compile(r"^ +IPv?4? [^:]+: *([0-9\.]{7,15})$") rip = re.compile(r"^ +IPv?4? [^:]+: *([0-9\.]{7,15})$")
roff = re.compile(r".*: Media disconnected$")
dev = None dev = None
for ln in txt.replace("\r", "").split("\n"): for ln in txt.replace("\r", "").split("\n"):
m = rdev.match(ln) m = rdev.match(ln)
if m: if m:
if dev and dev not in eps.values():
offs[dev] = 1
dev = m.group(1).split(" adapter ", 1)[-1] dev = m.group(1).split(" adapter ", 1)[-1]
if dev and roff.match(ln):
offs[dev] = 1
dev = None
m = rip.match(ln) m = rip.match(ln)
if m and dev: if m and dev:
eps[m.group(1)] = dev eps[m.group(1)] = dev
dev = None dev = None
return eps if dev and dev not in eps.values():
offs[dev] = 1
return eps, offs
def ips_windows_netsh(self): def ips_windows_netsh(self):
eps = {} eps = {}
@@ -215,7 +238,6 @@ class TcpSrv(object):
m = rip.match(ln) m = rip.match(ln)
if m and dev: if m and dev:
eps[m.group(1)] = dev eps[m.group(1)] = dev
dev = None
return eps return eps
@@ -223,8 +245,11 @@ class TcpSrv(object):
if MACOS: if MACOS:
eps = self.ips_macos() eps = self.ips_macos()
elif ANYWIN: elif ANYWIN:
eps = self.ips_windows_ipconfig() # sees more interfaces eps, off = self.ips_windows_ipconfig() # sees more interfaces + link state
eps.update(self.ips_windows_netsh()) # has better names eps.update(self.ips_windows_netsh()) # has better names
for k, v in eps.items():
if v in off:
eps[k] += ", \033[31mLINK-DOWN"
else: else:
eps = self.ips_linux() eps = self.ips_linux()

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

@@ -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
@@ -51,11 +57,11 @@ class U2idx(object):
fhash = body["hash"] fhash = body["hash"]
wark = up2k_wark_from_hashlist(self.args.salt, fsize, fhash) wark = up2k_wark_from_hashlist(self.args.salt, fsize, fhash)
uq = "where substr(w,1,16) = ? and w = ?" uq = "substr(w,1,16) = ? and w = ?"
uv = [wark[:16], wark] uv = [wark[:16], wark]
try: try:
return self.run_query(vols, uq, uv)[0] return self.run_query(vols, uq, uv, True, False, 99999)[0]
except: except:
raise Pebkac(500, min_ex()) raise Pebkac(500, min_ex())
@@ -76,28 +82,42 @@ 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 []
q = "" q = ""
va = [] va = []
joins = "" have_up = False # query has up.* operands
have_mt = False
is_key = True is_key = True
is_size = False is_size = False
is_date = False is_date = False
field_end = "" # closing parenthesis or whatever
kw_key = ["(", ")", "and ", "or ", "not "] kw_key = ["(", ")", "and ", "or ", "not "]
kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "] kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "]
ptn_mt = re.compile(r"^\.?[a-z_-]+$") ptn_mt = re.compile(r"^\.?[a-z_-]+$")
mt_ctr = 0 ptn_lc = re.compile(r" (mt\.v) ([=<!>]+) \? \) $")
mt_keycmp = "substr(up.w,1,16)"
mt_keycmp2 = None
ptn_lc = re.compile(r" (mt[0-9]+\.v) ([=<!>]+) \? $")
ptn_lcv = re.compile(r"[a-zA-Z]") ptn_lcv = re.compile(r"[a-zA-Z]")
while True: while True:
@@ -133,29 +153,31 @@ class U2idx(object):
if v == "size": if v == "size":
v = "up.sz" v = "up.sz"
is_size = True is_size = True
have_up = True
elif v == "date": elif v == "date":
v = "up.mt" v = "up.mt"
is_date = True is_date = True
have_up = True
elif v == "path": elif v == "path":
v = "trim(?||up.rd,'/')" v = "trim(?||up.rd,'/')"
va.append("\nrd") va.append("\nrd")
have_up = True
elif v == "name": elif v == "name":
v = "up.fn" v = "up.fn"
have_up = True
elif v == "tags" or ptn_mt.match(v): elif v == "tags" or ptn_mt.match(v):
mt_ctr += 1 have_mt = True
mt_keycmp2 = "mt{}.w".format(mt_ctr) field_end = ") "
joins += "inner join mt mt{} on {} = {} ".format(
mt_ctr, mt_keycmp, mt_keycmp2
)
mt_keycmp = mt_keycmp2
if v == "tags": if v == "tags":
v = "mt{0}.v".format(mt_ctr) vq = "mt.v"
else: else:
v = "+mt{0}.k = '{1}' and mt{0}.v".format(mt_ctr, v) vq = "+mt.k = '{}' and mt.v".format(v)
v = "exists(select 1 from mt where mt.w = mtw and " + vq
else: else:
raise Pebkac(400, "invalid key [" + v + "]") raise Pebkac(400, "invalid key [" + v + "]")
@@ -201,6 +223,10 @@ class U2idx(object):
va.append(v) va.append(v)
is_key = True is_key = True
if field_end:
q += field_end
field_end = ""
# lowercase tag searches # lowercase tag searches
m = ptn_lc.search(q) m = ptn_lc.search(q)
if not m or not ptn_lcv.search(unicode(v)): if not m or not ptn_lcv.search(unicode(v)):
@@ -212,16 +238,16 @@ class U2idx(object):
field, oper = m.groups() field, oper = m.groups()
if oper in ["=", "=="]: if oper in ["=", "=="]:
q += " {} like ? ".format(field) q += " {} like ? ) ".format(field)
else: else:
q += " lower({}) {} ? ".format(field, oper) q += " lower({}) {} ? ) ".format(field, oper)
try: try:
return self.run_query(vols, joins + "where " + q, va) 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): 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
@@ -240,14 +266,17 @@ class U2idx(object):
if not uq or not uv: if not uq or not uv:
uq = "select * from up" uq = "select * from up"
uv = () uv = ()
elif have_mt:
uq = "select up.*, substr(up.w,1,16) mtw from up where " + uq
uv = tuple(uv)
else: else:
uq = "select up.* from up " + uq uq = "select up.* from up where " + uq
uv = tuple(uv) uv = tuple(uv)
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)
@@ -268,9 +297,9 @@ class U2idx(object):
fk = flags.get("fk") fk = flags.get("fk")
c = cur.execute(uq, vuv) c = cur.execute(uq, vuv)
for hit in c: for hit in c:
w, ts, sz, rd, fn, ip, at = hit 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("//"):

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):
@@ -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):
@@ -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):
@@ -1137,9 +1161,9 @@ class Up2k(object):
m = "database is version {}, this copyparty only supports versions <= {}" m = "database is version {}, this copyparty only supports versions <= {}"
raise Exception(m.format(ver, DB_VER)) raise Exception(m.format(ver, DB_VER))
msg = "creating new DB (old is bad); backup: {}" msg = "creating new DB (old is bad); backup: "
if ver: if ver:
msg = "creating new DB (too old to upgrade); backup: {}" msg = "creating new DB (too old to upgrade); backup: "
cur = self._backup_db(db_path, cur, ver, msg) cur = self._backup_db(db_path, cur, ver, msg)
db = cur.connection db = cur.connection
@@ -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,10 +68,12 @@ 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"
META_NOBOTS = '<meta name="robots" content="noindex, nofollow">'
HTTPCODE = { HTTPCODE = {
200: "OK", 200: "OK",
204: "No Content", 204: "No Content",
@@ -483,13 +486,13 @@ def vol_san(vols, txt):
return txt return txt
def min_ex(): def min_ex(max_lines=8, reverse=False):
et, ev, tb = sys.exc_info() et, ev, tb = sys.exc_info()
tb = traceback.extract_tb(tb) tb = traceback.extract_tb(tb)
fmt = "{} @ {} <{}>: {}" fmt = "{} @ {} <{}>: {}"
ex = [fmt.format(fp.split(os.sep)[-1], ln, fun, txt) for fp, ln, fun, txt in tb] ex = [fmt.format(fp.split(os.sep)[-1], ln, fun, txt) for fp, ln, fun, txt in tb]
ex.append("[{}] {}".format(et.__name__, ev)) ex.append("[{}] {}".format(et.__name__, ev))
return "\n".join(ex[-8:]) return "\n".join(ex[-max_lines:][:: -1 if reverse else 1])
@contextlib.contextmanager @contextlib.contextmanager
@@ -910,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 = [
["<", ""], ["<", ""],
@@ -925,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))))
@@ -1233,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:
@@ -1335,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:
@@ -1351,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
@@ -1370,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,6 +29,7 @@ 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,
@@ -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';
@@ -397,6 +409,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 +427,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 +473,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 +539,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 +826,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

@@ -6,6 +6,7 @@
<title>⇆🎉 {{ title }}</title> <title>⇆🎉 {{ title }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.8"> <meta name="viewport" content="width=device-width, initial-scale=0.8">
{{ html_head }}
<link rel="stylesheet" media="screen" href="/.cpr/ui.css?_={{ ts }}"> <link rel="stylesheet" media="screen" href="/.cpr/ui.css?_={{ ts }}">
<link rel="stylesheet" media="screen" href="/.cpr/browser.css?_={{ ts }}"> <link rel="stylesheet" media="screen" href="/.cpr/browser.css?_={{ ts }}">
{%- if css %} {%- if css %}
@@ -34,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">
@@ -133,6 +135,9 @@
<script> <script>
var acct = "{{ acct }}", var acct = "{{ acct }}",
perms = {{ perms }}, perms = {{ perms }},
themes = {{ themes }},
dtheme = "{{ dtheme }}",
srvinf = "{{ srv_info }}",
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 }},
@@ -141,14 +146,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

@@ -6,6 +6,7 @@
<title>{{ title }}</title> <title>{{ title }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.8"> <meta name="viewport" content="width=device-width, initial-scale=0.8">
{{ html_head }}
<style> <style>
html{font-family:sans-serif} html{font-family:sans-serif}
td{border:1px solid #999;border-width:1px 1px 0 0;padding:0 5px} td{border:1px solid #999;border-width:1px 1px 0 0;padding:0 5px}
@@ -44,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;
} }
@@ -308,23 +305,23 @@ 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;
} }
} }
@@ -432,16 +429,16 @@ 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

@@ -3,6 +3,7 @@
<title>📝🎉 {{ title }}</title> <title>📝🎉 {{ title }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.7"> <meta name="viewport" content="width=device-width, initial-scale=0.7">
{{ html_head }}
<link rel="stylesheet" href="/.cpr/ui.css?_={{ ts }}"> <link rel="stylesheet" href="/.cpr/ui.css?_={{ ts }}">
<link rel="stylesheet" href="/.cpr/md.css?_={{ ts }}"> <link rel="stylesheet" href="/.cpr/md.css?_={{ ts }}">
{%- if edit %} {%- if edit %}
@@ -135,13 +136,13 @@ 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;

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,7 +255,7 @@ 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.onreadystatechange = r.cb;
@@ -268,7 +268,7 @@ function Modpoll() {
return; return;
} }
if (this.readyState != XMLHttpRequest.DONE) if (this.readyState != XHR.DONE)
return; return;
if (this.status !== 200) { if (this.status !== 200) {
@@ -309,7 +309,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 +321,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,7 +336,7 @@ 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.onreadystatechange = save_cb;
@@ -356,7 +356,7 @@ function save(e) {
} }
function save_cb() { function save_cb() {
if (this.readyState != XMLHttpRequest.DONE) if (this.readyState != XHR.DONE)
return; return;
if (this.status !== 200) if (this.status !== 200)
@@ -397,7 +397,7 @@ 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.onreadystatechange = savechk_cb;
@@ -409,7 +409,7 @@ function run_savechk(lastmod, txt, btn, ntry) {
} }
function savechk_cb() { function savechk_cb() {
if (this.readyState != XMLHttpRequest.DONE) if (this.readyState != XHR.DONE)
return; return;
if (this.status !== 200) if (this.status !== 200)
@@ -678,7 +678,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 +829,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 +989,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

@@ -3,6 +3,7 @@
<title>📝🎉 {{ title }}</title> <title>📝🎉 {{ title }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.7"> <meta name="viewport" content="width=device-width, initial-scale=0.7">
{{ html_head }}
<link rel="stylesheet" href="/.cpr/ui.css?_={{ ts }}"> <link rel="stylesheet" href="/.cpr/ui.css?_={{ ts }}">
<link rel="stylesheet" href="/.cpr/mde.css?_={{ ts }}"> <link rel="stylesheet" href="/.cpr/mde.css?_={{ ts }}">
<link rel="stylesheet" href="/.cpr/deps/mini-fa.css?_={{ ts }}"> <link rel="stylesheet" href="/.cpr/deps/mini-fa.css?_={{ ts }}">
@@ -33,11 +34,11 @@ 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;

View File

@@ -114,7 +114,7 @@ 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.onreadystatechange = save_cb;
@@ -133,7 +133,7 @@ function save(mde) {
} }
function save_cb() { function save_cb() {
if (this.readyState != XMLHttpRequest.DONE) if (this.readyState != XHR.DONE)
return; return;
if (this.status !== 200) if (this.status !== 200)
@@ -170,7 +170,7 @@ 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.onreadystatechange = save_chk;
@@ -182,7 +182,7 @@ function save_cb() {
} }
function save_chk() { function save_chk() {
if (this.readyState != XMLHttpRequest.DONE) if (this.readyState != XHR.DONE)
return; return;
if (this.status !== 200) if (this.status !== 200)

View File

@@ -6,6 +6,7 @@
<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 }}
<link rel="stylesheet" media="screen" href="/.cpr/msg.css?_={{ ts }}"> <link rel="stylesheet" media="screen" href="/.cpr/msg.css?_={{ ts }}">
</head> </head>

View File

@@ -88,27 +88,27 @@ blockquote {
} }
html.dark, html.z,
html.dark body, html.z body,
html.dark #wrap { html.z #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 .btns a,
html.dark a.r { html.z 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 +117,6 @@ 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;
} }

View File

@@ -6,6 +6,7 @@
<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 }}
<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>
@@ -96,10 +97,15 @@
<a href="#" id="repl">π</a> <a href="#" id="repl">π</a>
<script> <script>
document.documentElement.setAttribute("class", localStorage.lightmode == 1 ? "light" : "dark"); document.documentElement.className = localStorage.light == 1 ? "y" : "z";
</script> </script>
<script src="/.cpr/util.js?_={{ ts }}"></script> <script src="/.cpr/util.js?_={{ ts }}"></script>
<script>tt.init();</script> <script>
tt.init();
{%- if this.uname == '*' %}
QS('input[name="cppwd"]').focus();
{%- endif %}
</script>
</body> </body>
</html> </html>

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;
@@ -157,23 +158,24 @@ 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.y #tt,
html.light #toast { html.y #toast {
box-shadow: 0 .3em 1em rgba(0,0,0,0.4); box-shadow: 0 .3em 1em rgba(0,0,0,0.4);
} }
#modalc code, #modalc code,
html.light #tt code { html.y #tt code {
background: #060; background: #060;
color: #fff; color: #fff;
} }
html.light #tt em { html.y #tt em {
color: #d38; color: #d38;
} }
html.light #tth { html.y #tth {
color: #000; color: #000;
background: #fff; background: #fff;
} }
@@ -273,9 +275,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 +285,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 +416,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 +445,48 @@ 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.z .mdo h2 {
background: #444; background: #444;
border-bottom: .22em solid #555; border-bottom: .22em solid #555;
} }
html.dark .mdo td, html.z .mdo td,
html.dark .mdo th { 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) {
@@ -624,11 +626,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,12 +646,6 @@ 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) { if (!String.prototype.format) {
@@ -671,8 +667,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 +707,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;
@@ -1173,7 +1169,7 @@ function up2k_init(subtle) {
var t = st.todo.handshake[0], var t = st.todo.handshake[0],
cd = t.cooldown; cd = t.cooldown;
if (cd && cd - Date.now() > 0) if (cd && cd > Date.now())
return false; return false;
// keepalive or verify // keepalive or verify
@@ -1370,6 +1366,14 @@ function up2k_init(subtle) {
return taskerd; return taskerd;
})(); })();
function chill(t) {
var now = Date.now();
if ((t.coolmul || 0) < 2 || now - t.cooldown < t.coolmul * 700)
t.coolmul = Math.min((t.coolmul || 0.5) * 2, 32);
t.cooldown = Math.max(t.cooldown || 1, Date.now() + t.coolmul * 1000);
}
///// /////
//// ////
/// hashing /// hashing
@@ -1468,7 +1472,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;
} }
} }
@@ -1756,8 +1759,12 @@ function up2k_init(subtle) {
pvis.move(t.n, 'ok'); pvis.move(t.n, 'ok');
} }
else t.t_uploaded = undefined; else {
if (t.t_uploaded)
chill(t);
t.t_uploaded = undefined;
}
tasker(); tasker();
} }
else { else {
@@ -1804,11 +1811,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 ? "finalize upload" : t.srch ? "perform search" : "initiate upload";
xhr.status, t.name) + ( xhrchk(xhr, "server rejected the request to " + 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) {
@@ -1867,9 +1871,9 @@ 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, "server rejected upload (chunk {0} of {1});\n\nfile: {2}\n\nerror ".format(npart, Math.ceil(t.size / chunksize), t.name), "404, target folder not found (???)");
xhr.status, t.name) + (txt || "no further information"));
return; chill(t);
} }
orz2(xhr); orz2(xhr);
} }
@@ -1896,7 +1900,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, "failed to upload chunk {0} of {1};\nprobably harmless, continuing\n\nfile: {2}".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);
@@ -1920,6 +1924,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']),
@@ -1929,22 +1934,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;
} }
} }
} }
@@ -2018,9 +2020,12 @@ function up2k_init(subtle) {
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(msgu, '').replace(msgs, '');
if (html !== ohtml) if (html !== ohtml)
@@ -2057,13 +2062,15 @@ function up2k_init(subtle) {
try { try {
var ico = uc.fsearch ? '🔎' : '🚀', var ico = uc.fsearch ? '🔎' : '🚀',
desc = uc.fsearch ? 'Search' : 'Upload'; desc = uc.fsearch ? 'S E A R C H' : 'U P L O A D';
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

@@ -14,7 +14,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 +90,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;
@@ -327,11 +331,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, ""]]);
@@ -872,7 +886,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 +1186,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 +1390,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 + "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

@@ -3,6 +3,12 @@ echo not a script
exit 1 exit 1
##
## add index.html banners
find -name index.html | sed -r 's/index.html$//' | while IFS= read -r dir; do f="$dir/.prologue.html"; [ -e "$f" ] || echo '<h1><a href="index.html">open index.html</a></h1>' >"$f"; done
## ##
## delete all partial uploads ## delete all partial uploads
## (supports linux/macos, probably windows+msys2) ## (supports linux/macos, probably windows+msys2)
@@ -95,6 +101,7 @@ var t=[]; var b=document.location.href.split('#')[0].slice(0, -1); document.quer
# debug md-editor line tracking # debug md-editor line tracking
var s=mknod('style');s.innerHTML='*[data-ln]:before {content:attr(data-ln)!important;color:#f0c;background:#000;position:absolute;left:-1.5em;font-size:1rem}';document.head.appendChild(s); var s=mknod('style');s.innerHTML='*[data-ln]:before {content:attr(data-ln)!important;color:#f0c;background:#000;position:absolute;left:-1.5em;font-size:1rem}';document.head.appendChild(s);
## ##
## bash oneliners ## bash oneliners
@@ -199,6 +206,7 @@ git remote add all git@github.com:9001/copyparty.git
git remote set-url --add --push all git@gitlab.com:9001/copyparty.git git remote set-url --add --push all git@gitlab.com:9001/copyparty.git
git remote set-url --add --push all git@github.com:9001/copyparty.git git remote set-url --add --push all git@github.com:9001/copyparty.git
## ##
## http 206 ## http 206

View File

@@ -12,21 +12,18 @@ set -e
# #
# output summary (filesizes and contents): # output summary (filesizes and contents):
# #
# 535672 copyparty-extras/sfx-full/copyparty-sfx.sh
# 550760 copyparty-extras/sfx-full/copyparty-sfx.py # 550760 copyparty-extras/sfx-full/copyparty-sfx.py
# `- original unmodified sfx from github # `- original unmodified sfx from github
# #
# 572923 copyparty-extras/sfx-full/copyparty-sfx-gz.py # 572923 copyparty-extras/sfx-full/copyparty-sfx-gz.py
# `- unmodified but recompressed from bzip2 to gzip # `- unmodified but recompressed from bzip2 to gzip
# #
# 341792 copyparty-extras/sfx-ent/copyparty-sfx.sh
# 353975 copyparty-extras/sfx-ent/copyparty-sfx.py # 353975 copyparty-extras/sfx-ent/copyparty-sfx.py
# 376934 copyparty-extras/sfx-ent/copyparty-sfx-gz.py # 376934 copyparty-extras/sfx-ent/copyparty-sfx-gz.py
# `- removed iOS ogg/opus/vorbis audio decoder, # `- removed iOS ogg/opus/vorbis audio decoder,
# removed the audio tray mouse cursor, # removed the audio tray mouse cursor,
# "enterprise edition" # "enterprise edition"
# #
# 259288 copyparty-extras/sfx-lite/copyparty-sfx.sh
# 270004 copyparty-extras/sfx-lite/copyparty-sfx.py # 270004 copyparty-extras/sfx-lite/copyparty-sfx.py
# 293159 copyparty-extras/sfx-lite/copyparty-sfx-gz.py # 293159 copyparty-extras/sfx-lite/copyparty-sfx-gz.py
# `- also removed the codemirror markdown editor # `- also removed the codemirror markdown editor
@@ -81,7 +78,7 @@ cache="$od/.copyparty-repack.cache"
# fallback to awk (sorry) # fallback to awk (sorry)
awk -F\" '/"browser_download_url".*(\.tar\.gz|-sfx\.)/ {print$4}' awk -F\" '/"browser_download_url".*(\.tar\.gz|-sfx\.)/ {print$4}'
) | ) |
grep -E '(sfx\.(sh|py)|tar\.gz)$' | grep -E '(sfx\.py|tar\.gz)$' |
tee /dev/stderr | tee /dev/stderr |
tr -d '\r' | tr '\n' '\0' | tr -d '\r' | tr '\n' '\0' |
xargs -0 bash -c 'dl_files "$@"' _ xargs -0 bash -c 'dl_files "$@"' _
@@ -139,11 +136,11 @@ repack() {
) )
} }
repack sfx-full "re gz no-sh" repack sfx-full "re gz"
repack sfx-ent "re no-dd" repack sfx-ent "re no-dd"
repack sfx-ent "re no-dd gz no-sh" repack sfx-ent "re no-dd gz"
repack sfx-lite "re no-dd no-cm no-hl" repack sfx-lite "re no-dd no-cm no-hl"
repack sfx-lite "re no-dd no-cm no-hl gz no-sh" repack sfx-lite "re no-dd no-cm no-hl gz"
# move fuse and up2k clients into copyparty-extras/, # move fuse and up2k clients into copyparty-extras/,

View File

@@ -4,13 +4,13 @@ ENV ver_asmcrypto=5b994303a9d3e27e0915f72a10b6c2c51535a4dc \
ver_hashwasm=4.9.0 \ ver_hashwasm=4.9.0 \
ver_marked=4.0.12 \ ver_marked=4.0.12 \
ver_mde=2.16.1 \ ver_mde=2.16.1 \
ver_codemirror=5.65.1 \ ver_codemirror=5.65.3 \
ver_fontawesome=5.13.0 \ ver_fontawesome=5.13.0 \
ver_zopfli=1.0.3 ver_zopfli=1.0.3
# download; # download;
# the scp url is latin from https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap # the scp url is regular latin from https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap
RUN mkdir -p /z/dist/no-pk \ RUN mkdir -p /z/dist/no-pk \
&& wget https://fonts.gstatic.com/s/sourcecodepro/v11/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2 -O scp.woff2 \ && wget https://fonts.gstatic.com/s/sourcecodepro/v11/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2 -O scp.woff2 \
&& apk add cmake make g++ git bash npm patch wget tar pigz brotli gzip unzip python3 python3-dev brotli py3-brotli \ && apk add cmake make g++ git bash npm patch wget tar pigz brotli gzip unzip python3 python3-dev brotli py3-brotli \
@@ -118,6 +118,7 @@ RUN cd easy-markdown-editor-$ver_mde \
# build fontawesome and scp # build fontawesome and scp
COPY mini-fa.sh /z COPY mini-fa.sh /z
COPY mini-fa.css /z COPY mini-fa.css /z
COPY shiftbase.py /z
RUN /bin/ash /z/mini-fa.sh RUN /bin/ash /z/mini-fa.sh

View File

@@ -29,3 +29,10 @@ pyftsubset "$orig_woff" --unicodes-file=/z/icon.list --no-ignore-missing-unicode
# scp is easier, just want basic latin # scp is easier, just want basic latin
pyftsubset /z/scp.woff2 --unicodes="20-7e,ab,b7,bb,2022" --no-ignore-missing-unicodes --flavor=woff2 --output-file=/z/dist/no-pk/scp.woff2 --verbose pyftsubset /z/scp.woff2 --unicodes="20-7e,ab,b7,bb,2022" --no-ignore-missing-unicodes --flavor=woff2 --output-file=/z/dist/no-pk/scp.woff2 --verbose
exit 0
# kinda works but ruins hinting on windows, just use the old version of the font which has correct baseline
python3 shiftbase.py /z/dist/no-pk/scp.woff2
cd /z/dist/no-pk/
mv scp.woff2.woff2 scp.woff2

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env python3
import sys
from fontTools.ttLib import TTFont, newTable
def main():
woff = sys.argv[1]
font = TTFont(woff)
print(repr(font["hhea"].__dict__))
print(repr(font["OS/2"].__dict__))
# font["hhea"].ascent = round(base_asc * mul)
# font["hhea"].descent = round(base_desc * mul)
# font["OS/2"].usWinAscent = round(base_asc * mul)
font["OS/2"].usWinDescent = round(font["OS/2"].usWinDescent * 1.1)
font["OS/2"].sTypoDescender = round(font["OS/2"].sTypoDescender * 1.1)
try:
del font["post"].mapping["Delta#1"]
except:
pass
font.save(woff + ".woff2")
if __name__ == "__main__":
main()

View File

@@ -14,8 +14,6 @@ help() { exec cat <<'EOF'
# #
# `gz` creates a gzip-compressed python sfx instead of bzip2 # `gz` creates a gzip-compressed python sfx instead of bzip2
# #
# `no-sh` makes just the python sfx, skips the sh/unix sfx
#
# `no-cm` saves ~82k by removing easymde/codemirror # `no-cm` saves ~82k by removing easymde/codemirror
# (the fancy markdown editor) # (the fancy markdown editor)
# #
@@ -64,8 +62,6 @@ pybin=$(command -v python3 || command -v python) || {
} }
use_gz= use_gz=
do_sh=1
do_py=1
zopf=2560 zopf=2560
while [ ! -z "$1" ]; do while [ ! -z "$1" ]; do
case $1 in case $1 in
@@ -76,8 +72,6 @@ while [ ! -z "$1" ]; do
no-hl) no_hl=1 ; ;; no-hl) no_hl=1 ; ;;
no-dd) no_dd=1 ; ;; no-dd) no_dd=1 ; ;;
no-cm) no_cm=1 ; ;; no-cm) no_cm=1 ; ;;
no-sh) do_sh= ; ;;
no-py) do_py= ; ;;
fast) zopf=100 ; ;; fast) zopf=100 ; ;;
*) help ; ;; *) help ; ;;
esac esac
@@ -147,6 +141,14 @@ tmpdir="$(
mkdir dep-ftp/ mkdir dep-ftp/
mv pyftpdlib dep-ftp/ mv pyftpdlib dep-ftp/
echo collecting asyncore, asynchat
for n in asyncore.py asynchat.py; do
f=../build/$n
[ -e "$f" ] ||
(url=https://raw.githubusercontent.com/python/cpython/c4d45ee670c09d4f6da709df072ec80cb7dfad22/Lib/$n;
wget -O$f "$url" || curl -L "$url" >$f)
done
# msys2 tar is bad, make the best of it # msys2 tar is bad, make the best of it
echo collecting source echo collecting source
[ $clean ] && { [ $clean ] && {
@@ -157,6 +159,12 @@ tmpdir="$(
(cd .. && tar -cf tar copyparty) && tar -xf ../tar (cd .. && tar -cf tar copyparty) && tar -xf ../tar
} }
rm -f ../tar rm -f ../tar
# insert asynchat
mkdir copyparty/vend
for n in asyncore.py asynchat.py; do
awk 'NR<4||NR>27;NR==4{print"# license: https://opensource.org/licenses/ISC\n"}' ../build/$n >copyparty/vend/$n
done
} }
ver= ver=
@@ -258,7 +266,7 @@ rm have
find | grep -E '\.py$' | find | grep -E '\.py$' |
grep -vE '__version__' | grep -vE '__version__' |
tr '\n' '\0' | tr '\n' '\0' |
xargs -0 $pybin ../scripts/uncomment.py xargs -0 "$pybin" ../scripts/uncomment.py
f=dep-j2/jinja2/constants.py f=dep-j2/jinja2/constants.py
awk '/^LOREM_IPSUM_WORDS/{o=1;print "LOREM_IPSUM_WORDS = u\"a\"";next} !o; /"""/{o=0}' <$f >t awk '/^LOREM_IPSUM_WORDS/{o=1;print "LOREM_IPSUM_WORDS = u\"a\"";next} !o; /"""/{o=0}' <$f >t
@@ -348,7 +356,14 @@ for d in copyparty dep-j2 dep-ftp; do find $d -type f; done |
sed -r 's/(.*)\.(.*)/\2 \1/' | LC_ALL=C sort | sed -r 's/(.*)\.(.*)/\2 \1/' | LC_ALL=C sort |
sed -r 's/([^ ]*) (.*)/\2.\1/' | grep -vE '/list1?$' > list1 sed -r 's/([^ ]*) (.*)/\2.\1/' | grep -vE '/list1?$' > list1
(grep -vE '\.(gz|br)$' list1; grep -E '\.(gz|br)$' list1 | shuf) >list || true for n in {1..50}; do
(grep -vE '\.(gz|br)$' list1; grep -E '\.(gz|br)$' list1 | shuf) >list || true
s=$(md5sum list | cut -c-16)
grep -q $s "$zdir/h" && continue
echo $s >> "$zdir/h"
break
done
[ $n -eq 50 ] && exit
echo creating tar echo creating tar
args=(--owner=1000 --group=1000) args=(--owner=1000 --group=1000)
@@ -363,41 +378,27 @@ pe=bz2
echo compressing tar echo compressing tar
# detect best level; bzip2 -7 is usually better than -9 # detect best level; bzip2 -7 is usually better than -9
[ $do_py ] && { for n in {2..9}; do cp tar t.$n; $pc -$n t.$n & done; wait; mv -v $(ls -1S t.*.$pe | tail -n 1) tar.bz2; } for n in {2..9}; do cp tar t.$n; $pc -$n t.$n & done; wait; mv -v $(ls -1S t.*.$pe | tail -n 1) tar.bz2
[ $do_sh ] && { for n in {2..9}; do cp tar t.$n; xz -ze$n t.$n & done; wait; mv -v $(ls -1S t.*.xz | tail -n 1) tar.xz; }
rm t.* || true rm t.* || true
exts=() exts=()
[ $do_sh ] && { echo creating sfx
exts+=(.sh)
echo creating unix sfx py=../scripts/sfx.py
( suf=
sed "s/PACK_TS/$ts/; s/PACK_HTS/$hts/; s/CPP_VER/$ver/" <../scripts/sfx.sh | [ $use_gz ] && {
grep -E '^sfx_eof$' -B 9001; sed -r 's/"r:bz2"/"r:gz"/' <$py >$py.t
cat tar.xz py=$py.t
) >$sfx_out.sh suf=-gz
} }
"$pybin" $py --sfx-make tar.bz2 $ver $ts
mv sfx.out $sfx_out$suf.py
[ $do_py ] && { exts+=($suf.py)
echo creating generic sfx [ $use_gz ] &&
rm $py
py=../scripts/sfx.py
suf=
[ $use_gz ] && {
sed -r 's/"r:bz2"/"r:gz"/' <$py >$py.t
py=$py.t
suf=-gz
}
$pybin $py --sfx-make tar.bz2 $ver $ts
mv sfx.out $sfx_out$suf.py
exts+=($suf.py)
[ $use_gz ] &&
rm $py
}
chmod 755 $sfx_out* chmod 755 $sfx_out*
@@ -408,4 +409,4 @@ for ext in ${exts[@]}; do
done done
# apk add bash python3 tar xz bzip2 # apk add bash python3 tar xz bzip2
# while true; do ./make-sfx.sh; for f in ..//dist/copyparty-sfx.{sh,py}; do mv $f $f.$(wc -c <$f | awk '{print$1}'); done; done # while true; do ./make-sfx.sh; f=../dist/copyparty-sfx.py; mv $f $f.$(wc -c <$f | awk '{print$1}'); done

View File

@@ -4,34 +4,31 @@ set -e
cd ~/dev/copyparty/scripts cd ~/dev/copyparty/scripts
v=$1 v=$1
printf '%s\n' "$v" | grep -qE '^[0-9\.]+$' || exit 1
grep -E "(${v//./, })" ../copyparty/__version__.py || exit 1
git push all [ "$v" = sfx ] || {
git tag v$v printf '%s\n' "$v" | grep -qE '^[0-9\.]+$' || exit 1
git push all --tags grep -E "(${v//./, })" ../copyparty/__version__.py || exit 1
rm -rf ../dist git push all
git tag v$v
git push all --tags
./make-pypi-release.sh u rm -rf ../dist
(cd .. && python3 ./setup.py clean2)
./make-tgz-release.sh $v ./make-pypi-release.sh u
(cd .. && python3 ./setup.py clean2)
./make-tgz-release.sh $v
}
rm -f ../dist/copyparty-sfx.* rm -f ../dist/copyparty-sfx.*
./make-sfx.sh no-sh f=../dist/copyparty-sfx.py
../dist/copyparty-sfx.py -h ./make-sfx.sh
$f -h
ar=
while true; do while true; do
for ((a=0; a<100; a++)); do mv $f $f.$(wc -c <$f | awk '{print$1}')
for f in ../dist/copyparty-sfx.{py,sh}; do ./make-sfx.sh re $ar
[ -e $f ] || continue;
mv $f $f.$(wc -c <$f | awk '{print$1}')
done
./make-sfx.sh re $ar
done
ar=no-sh
done done
# git tag -d v$v; git push --delete origin v$v # git tag -d v$v; git push --delete origin v$v

View File

@@ -32,6 +32,9 @@ copyparty/th_srv.py,
copyparty/u2idx.py, copyparty/u2idx.py,
copyparty/up2k.py, copyparty/up2k.py,
copyparty/util.py, copyparty/util.py,
copyparty/vend,
copyparty/vend/asynchat.py,
copyparty/vend/asyncore.py,
copyparty/web, copyparty/web,
copyparty/web/baguettebox.js, copyparty/web/baguettebox.js,
copyparty/web/browser.css, copyparty/web/browser.css,

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
@@ -368,28 +369,18 @@ def confirm(rv):
sys.exit(rv or 1) sys.exit(rv or 1)
def run(tmp, j2): def run(tmp, j2, ftp):
msg("jinja2:", j2 or "bundled") msg("jinja2:", j2 or "bundled")
msg("pyftpd:", ftp or "bundled")
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()
ld = [os.path.join(tmp, x) for x in ["", "dep-ftp", "dep-j2"]] ld = (("", ""), (j2, "dep-j2"), (ftp, "dep-ftp"))
if j2: ld = [os.path.join(tmp, b) for a, b in ld if not a]
del ld[-1]
if any([re.match(r"^-.*j[0-9]", x) for x in sys.argv]): if any([re.match(r"^-.*j[0-9]", x) for x in sys.argv]):
run_s(ld) run_s(ld)
@@ -462,7 +453,12 @@ def main():
j2 = None j2 = None
try: try:
run(tmp, j2) from pyftpdlib.__init__ import __ver__ as ftp
except:
ftp = None
try:
run(tmp, j2, ftp)
except SystemExit as ex: except SystemExit as ex:
c = ex.code c = ex.code
if c not in [0, -15]: if c not in [0, -15]:

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,
@@ -52,9 +55,16 @@ class Cfg(Namespace):
mth="", mth="",
textfiles="", textfiles="",
doctitle="", doctitle="",
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,
force_js=False,
no_robots=False,
js_browser=None, js_browser=None,
css_browser=None, css_browser=None,
**{k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr no_acode".split()} **{k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr no_acode".split()}

View File

@@ -17,13 +17,14 @@ 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" 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": [],
"mte": "a", "mte": "a",
"mth": "", "mth": "",
"doctitle": "", "doctitle": "",
"html_head": "",
"hist": None, "hist": None,
"no_idx": None, "no_idx": None,
"no_hash": None, "no_hash": None,
@@ -35,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)