mirror of
https://github.com/9001/copyparty.git
synced 2025-11-06 14:53:17 +00:00
Compare commits
132 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed48c2d0ed | ||
|
|
26fe84b660 | ||
|
|
5938230270 | ||
|
|
1a33a047fa | ||
|
|
43a8bcefb9 | ||
|
|
2e740e513f | ||
|
|
8a21a86b61 | ||
|
|
f600116205 | ||
|
|
1c03705de8 | ||
|
|
f7e461fac6 | ||
|
|
03ce6c97ff | ||
|
|
ffd9e76e07 | ||
|
|
fc49cb1e67 | ||
|
|
f5712d9f25 | ||
|
|
161d57bdda | ||
|
|
bae0d440bf | ||
|
|
fff052dde1 | ||
|
|
73b06eaa02 | ||
|
|
08a8ebed17 | ||
|
|
74d07426b3 | ||
|
|
69a2bba99a | ||
|
|
4d685d78ee | ||
|
|
5845ec3f49 | ||
|
|
13373426fe | ||
|
|
8e55551a06 | ||
|
|
12a3f0ac31 | ||
|
|
18e33edc88 | ||
|
|
c72c5ad4ee | ||
|
|
0fbc81ab2f | ||
|
|
af0a34cf82 | ||
|
|
b4590c5398 | ||
|
|
f787a66230 | ||
|
|
b21a99fd62 | ||
|
|
eb16306cde | ||
|
|
7bc23687e3 | ||
|
|
e1eaa057f2 | ||
|
|
97c264ca3e | ||
|
|
cf848ab1f7 | ||
|
|
cf83f9b0fd | ||
|
|
d98e361083 | ||
|
|
ce7f5309c7 | ||
|
|
75c485ced7 | ||
|
|
9c6e2ec012 | ||
|
|
1a02948a61 | ||
|
|
8b05ba4ba1 | ||
|
|
21e2874cb7 | ||
|
|
360ed5c46c | ||
|
|
5099bc365d | ||
|
|
12986da147 | ||
|
|
23e72797bc | ||
|
|
ac7b6f8f55 | ||
|
|
981b9ff11e | ||
|
|
4186906f4c | ||
|
|
0850d24e0c | ||
|
|
7ab8334c96 | ||
|
|
a4d7329ab7 | ||
|
|
3f4eae6bce | ||
|
|
518cf4be57 | ||
|
|
71096182be | ||
|
|
6452e927ea | ||
|
|
bc70cfa6f0 | ||
|
|
2b6e5ebd2d | ||
|
|
c761bd799a | ||
|
|
2f7c2fdee4 | ||
|
|
70a76ec343 | ||
|
|
7c3f64abf2 | ||
|
|
f5f38f195c | ||
|
|
7e84f4f015 | ||
|
|
4802f8cf07 | ||
|
|
cc05e67d8f | ||
|
|
2b6b174517 | ||
|
|
a1d05e6e12 | ||
|
|
f95ceb6a9b | ||
|
|
8f91b0726d | ||
|
|
97807f4383 | ||
|
|
5f42237f2c | ||
|
|
68289cfa54 | ||
|
|
42ea30270f | ||
|
|
ebbbbf3d82 | ||
|
|
27516e2d16 | ||
|
|
84bb6f915e | ||
|
|
46752f758a | ||
|
|
34c4c22e61 | ||
|
|
af2d0b8421 | ||
|
|
638b05a49a | ||
|
|
7a13e8a7fc | ||
|
|
d9fa74711d | ||
|
|
41867f578f | ||
|
|
0bf41ed4ef | ||
|
|
d080b4a731 | ||
|
|
ca4232ada9 | ||
|
|
ad348f91c9 | ||
|
|
990f915f42 | ||
|
|
53d720217b | ||
|
|
7a06ff480d | ||
|
|
3ef551f788 | ||
|
|
f0125cdc36 | ||
|
|
ed5f6736df | ||
|
|
15d8be0fae | ||
|
|
46f3e61360 | ||
|
|
87ad8c98d4 | ||
|
|
9bbdc4100f | ||
|
|
c80307e8ff | ||
|
|
c1d77e1041 | ||
|
|
d9e83650dc | ||
|
|
f6d635acd9 | ||
|
|
0dbd8a01ff | ||
|
|
8d755d41e0 | ||
|
|
190473bd32 | ||
|
|
030d1ec254 | ||
|
|
5a2b91a084 | ||
|
|
a50a05e4e7 | ||
|
|
6cb5a87c79 | ||
|
|
b9f89ca552 | ||
|
|
26c9fd5dea | ||
|
|
e81a9b6fe0 | ||
|
|
452450e451 | ||
|
|
419dd2d1c7 | ||
|
|
ee86b06676 | ||
|
|
953183f16d | ||
|
|
228f71708b | ||
|
|
621471a7cb | ||
|
|
8b58e951e3 | ||
|
|
1db489a0aa | ||
|
|
be65c3c6cf | ||
|
|
46e7fa31fe | ||
|
|
66e21bd499 | ||
|
|
8cab4c01fd | ||
|
|
d52038366b | ||
|
|
4fcfd87f5b | ||
|
|
f893c6baa4 | ||
|
|
9a45549b66 |
252
README.md
252
README.md
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## summary
|
## summary
|
||||||
|
|
||||||
turn your phone or raspi into a portable file server with resumable uploads/downloads using IE6 or any other browser
|
turn your phone or raspi into a portable file server with resumable uploads/downloads using *any* web browser
|
||||||
|
|
||||||
* server runs on anything with `py2.7` or `py3.3+`
|
* server runs on anything with `py2.7` or `py3.3+`
|
||||||
* browse/upload with IE4 / netscape4.0 on win3.11 (heh)
|
* browse/upload with IE4 / netscape4.0 on win3.11 (heh)
|
||||||
@@ -19,48 +19,53 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
|||||||
## readme toc
|
## readme toc
|
||||||
|
|
||||||
* top
|
* top
|
||||||
* [quickstart](#quickstart)
|
* [quickstart](#quickstart) - download [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) and you're all set!
|
||||||
* [on debian](#on-debian)
|
* [on debian](#on-debian) - recommended additional steps on debian
|
||||||
* [notes](#notes)
|
* [notes](#notes) - general notes
|
||||||
* [status](#status)
|
* [status](#status) - summary: all planned features work! now please enjoy the bloatening
|
||||||
* [testimonials](#testimonials)
|
* [testimonials](#testimonials) - small collection of user feedback
|
||||||
* [bugs](#bugs)
|
* [bugs](#bugs)
|
||||||
* [general bugs](#general-bugs)
|
* [general bugs](#general-bugs)
|
||||||
* [not my bugs](#not-my-bugs)
|
* [not my bugs](#not-my-bugs)
|
||||||
* [the browser](#the-browser)
|
* [accounts and volumes](#accounts-and-volumes) - per-folder, per-user permissions
|
||||||
* [tabs](#tabs)
|
* [the browser](#the-browser) - accessing a copyparty server using a web-browser
|
||||||
* [hotkeys](#hotkeys)
|
* [tabs](#tabs) - the main tabs in the ui
|
||||||
* [navpane](#navpane)
|
* [hotkeys](#hotkeys) - the browser has the following hotkeys (always qwerty)
|
||||||
* [thumbnails](#thumbnails)
|
* [navpane](#navpane) - switching between breadcrumbs or navpane
|
||||||
* [zip downloads](#zip-downloads)
|
* [thumbnails](#thumbnails) - press `g` to toggle image/video thumbnails instead of the file listing
|
||||||
* [uploading](#uploading)
|
* [zip downloads](#zip-downloads) - download folders (or file selections) as `zip` or `tar` files
|
||||||
* [file-search](#file-search)
|
* [uploading](#uploading) - web-browsers can upload using `bup` and `up2k`
|
||||||
* [file manager](#file-manager)
|
* [file-search](#file-search) - drop files/folders into up2k to see if they exist on the server
|
||||||
* [batch rename](#batch-rename)
|
* [unpost](#unpost) - undo/delete accidental uploads
|
||||||
* [markdown viewer](#markdown-viewer)
|
* [file manager](#file-manager) - cut/paste, rename, and delete files/folders (if you have permission)
|
||||||
|
* [batch rename](#batch-rename) - select some files and press F2 to bring up the rename UI
|
||||||
|
* [markdown viewer](#markdown-viewer) - and there are *two* editors
|
||||||
* [other tricks](#other-tricks)
|
* [other tricks](#other-tricks)
|
||||||
* [searching](#searching)
|
* [searching](#searching) - search by size, date, path/name, mp3-tags, ...
|
||||||
* [search configuration](#search-configuration)
|
* [server config](#server-config)
|
||||||
* [database location](#database-location)
|
* [file indexing](#file-indexing)
|
||||||
* [metadata from audio files](#metadata-from-audio-files)
|
* [upload rules](#upload-rules) - set upload rules using volume flags, some examples
|
||||||
* [file parser plugins](#file-parser-plugins)
|
* [compress uploads](#compress-uploads) - files can be autocompressed on upload
|
||||||
|
* [database location](#database-location) - can be stored in-volume (default) or elsewhere
|
||||||
|
* [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload
|
||||||
|
* [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags
|
||||||
* [complete examples](#complete-examples)
|
* [complete examples](#complete-examples)
|
||||||
* [browser support](#browser-support)
|
* [browser support](#browser-support) - TLDR: yes
|
||||||
* [client examples](#client-examples)
|
* [client examples](#client-examples) - interact with copyparty using non-browser clients
|
||||||
* [up2k](#up2k)
|
* [up2k](#up2k) - quick outline of the up2k protocol, see [uploading](#uploading) for the web-client
|
||||||
* [performance](#performance)
|
* [performance](#performance) - defaults are good for most cases
|
||||||
* [dependencies](#dependencies)
|
* [dependencies](#dependencies) - mandatory deps
|
||||||
* [optional dependencies](#optional-dependencies)
|
* [optional dependencies](#optional-dependencies) - install these to enable bonus features
|
||||||
* [install recommended deps](#install-recommended-deps)
|
* [install recommended deps](#install-recommended-deps)
|
||||||
* [optional gpl stuff](#optional-gpl-stuff)
|
* [optional gpl stuff](#optional-gpl-stuff)
|
||||||
* [sfx](#sfx)
|
* [sfx](#sfx) - there are two self-contained "binaries"
|
||||||
* [sfx repack](#sfx-repack)
|
* [sfx repack](#sfx-repack) - reduce the size of an sfx by removing features
|
||||||
* [install on android](#install-on-android)
|
* [install on android](#install-on-android)
|
||||||
* [building](#building)
|
* [building](#building)
|
||||||
* [dev env setup](#dev-env-setup)
|
* [dev env setup](#dev-env-setup)
|
||||||
* [just the sfx](#just-the-sfx)
|
* [just the sfx](#just-the-sfx)
|
||||||
* [complete release](#complete-release)
|
* [complete release](#complete-release)
|
||||||
* [todo](#todo)
|
* [todo](#todo) - roughly sorted by priority
|
||||||
* [discarded ideas](#discarded-ideas)
|
* [discarded ideas](#discarded-ideas)
|
||||||
|
|
||||||
|
|
||||||
@@ -85,7 +90,9 @@ you may also want these, especially on servers:
|
|||||||
|
|
||||||
### on debian
|
### on debian
|
||||||
|
|
||||||
recommended steps to enable audio metadata and thumbnails (from images and videos):
|
recommended additional steps on debian
|
||||||
|
|
||||||
|
enable audio metadata and thumbnails (from images and videos):
|
||||||
|
|
||||||
* as root, run the following:
|
* as root, run the following:
|
||||||
`apt install python3 python3-pip python3-dev ffmpeg`
|
`apt install python3 python3-pip python3-dev ffmpeg`
|
||||||
@@ -98,7 +105,7 @@ recommended steps to enable audio metadata and thumbnails (from images and video
|
|||||||
|
|
||||||
## notes
|
## notes
|
||||||
|
|
||||||
general:
|
general notes:
|
||||||
* paper-printing is affected by dark/light-mode! use lightmode for color, darkmode for grayscale
|
* paper-printing is affected by dark/light-mode! use lightmode for color, darkmode for grayscale
|
||||||
* because no browsers currently implement the media-query to do this properly orz
|
* because no browsers currently implement the media-query to do this properly orz
|
||||||
|
|
||||||
@@ -123,7 +130,7 @@ summary: all planned features work! now please enjoy the bloatening
|
|||||||
* ☑ basic: plain multipart, ie6 support
|
* ☑ basic: plain multipart, ie6 support
|
||||||
* ☑ [up2k](#uploading): js, resumable, multithreaded
|
* ☑ [up2k](#uploading): js, resumable, multithreaded
|
||||||
* ☑ stash: simple PUT filedropper
|
* ☑ stash: simple PUT filedropper
|
||||||
* ☑ unpost: undo/delete accidental uploads
|
* ☑ [unpost](#unpost): undo/delete accidental uploads
|
||||||
* ☑ symlink/discard existing files (content-matching)
|
* ☑ symlink/discard existing files (content-matching)
|
||||||
* download
|
* download
|
||||||
* ☑ single files in browser
|
* ☑ single files in browser
|
||||||
@@ -183,6 +190,7 @@ small collection of user feedback
|
|||||||
|
|
||||||
# accounts and volumes
|
# accounts and volumes
|
||||||
|
|
||||||
|
per-folder, per-user permissions
|
||||||
* `-a usr:pwd` adds account `usr` with password `pwd`
|
* `-a usr:pwd` adds account `usr` with password `pwd`
|
||||||
* `-v .::r` adds current-folder `.` as the webroot, `r`eadable by anyone
|
* `-v .::r` adds current-folder `.` as the webroot, `r`eadable by anyone
|
||||||
* the syntax is `-v src:dst:perm:perm:...` so local-path, url-path, and one or more permissions to set
|
* the syntax is `-v src:dst:perm:perm:...` so local-path, url-path, and one or more permissions to set
|
||||||
@@ -207,14 +215,17 @@ example:
|
|||||||
|
|
||||||
# the browser
|
# the browser
|
||||||
|
|
||||||

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

|
||||||
|
|
||||||
|
|
||||||
## tabs
|
## tabs
|
||||||
|
|
||||||
* `[🔎]` search by size, date, path/name, mp3-tags ... see [searching](#searching)
|
the main tabs in the ui
|
||||||
* `[🧯]` unpost: undo/delete accidental uploads
|
* `[🔎]` [search](#searching) by size, date, path/name, mp3-tags ...
|
||||||
* `[🚀]` and `[🎈]` are the uploaders, see [uploading](#uploading)
|
* `[🧯]` [unpost](#unpost): undo/delete accidental uploads
|
||||||
|
* `[🚀]` and `[🎈]` are the [uploaders](#uploading)
|
||||||
* `[📂]` mkdir: create directories
|
* `[📂]` mkdir: create directories
|
||||||
* `[📝]` new-md: create a new markdown document
|
* `[📝]` new-md: create a new markdown document
|
||||||
* `[📟]` send-msg: either to server-log or into textfiles if `--urlform save`
|
* `[📟]` send-msg: either to server-log or into textfiles if `--urlform save`
|
||||||
@@ -224,7 +235,7 @@ example:
|
|||||||
|
|
||||||
## hotkeys
|
## hotkeys
|
||||||
|
|
||||||
the browser has the following hotkeys (assumes qwerty, ignores actual layout)
|
the browser has the following hotkeys (always qwerty)
|
||||||
* `B` toggle breadcrumbs / navpane
|
* `B` toggle breadcrumbs / navpane
|
||||||
* `I/K` prev/next folder
|
* `I/K` prev/next folder
|
||||||
* `M` parent folder (or unexpand current)
|
* `M` parent folder (or unexpand current)
|
||||||
@@ -247,13 +258,15 @@ the browser has the following hotkeys (assumes qwerty, ignores actual layout)
|
|||||||
* when viewing images / playing videos:
|
* when viewing images / playing videos:
|
||||||
* `J/L, Left/Right` prev/next file
|
* `J/L, Left/Right` prev/next file
|
||||||
* `Home/End` first/last file
|
* `Home/End` first/last file
|
||||||
|
* `S` toggle selection
|
||||||
|
* `R` rotate clockwise (shift=ccw)
|
||||||
* `Esc` close viewer
|
* `Esc` close viewer
|
||||||
* videos:
|
* videos:
|
||||||
* `U/O` skip 10sec back/forward
|
* `U/O` skip 10sec back/forward
|
||||||
* `P/K/Space` play/pause
|
* `P/K/Space` play/pause
|
||||||
* `F` fullscreen
|
* `F` fullscreen
|
||||||
* `C` continue playing next video
|
* `C` continue playing next video
|
||||||
* `R` loop
|
* `V` loop
|
||||||
* `M` mute
|
* `M` mute
|
||||||
* when the navpane is open:
|
* when the navpane is open:
|
||||||
* `A/D` adjust tree width
|
* `A/D` adjust tree width
|
||||||
@@ -271,14 +284,18 @@ the browser has the following hotkeys (assumes qwerty, ignores actual layout)
|
|||||||
|
|
||||||
## navpane
|
## navpane
|
||||||
|
|
||||||
by default there's a breadcrumbs path; you can replace this with a navpane (tree-browser sidebar thing) by clicking the `🌲` or pressing the `B` hotkey
|
switching between breadcrumbs or navpane
|
||||||
|
|
||||||
|
click the `🌲` or pressing the `B` hotkey to toggle between breadcrumbs path (default), or a navpane (tree-browser sidebar thing)
|
||||||
|
|
||||||
click `[-]` and `[+]` (or hotkeys `A`/`D`) to adjust the size, and the `[a]` toggles if the tree should widen dynamically as you go deeper or stay fixed-size
|
click `[-]` and `[+]` (or hotkeys `A`/`D`) to adjust the size, and the `[a]` toggles if the tree should widen dynamically as you go deeper or stay fixed-size
|
||||||
|
|
||||||
|
|
||||||
## thumbnails
|
## thumbnails
|
||||||
|
|
||||||

|
press `g` to toggle image/video thumbnails instead of the file listing
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
it does static images with Pillow and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how dangerous your users are
|
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
|
||||||
|
|
||||||
@@ -289,7 +306,9 @@ in the grid/thumbnail view, if the audio player panel is open, songs will start
|
|||||||
|
|
||||||
## zip downloads
|
## zip downloads
|
||||||
|
|
||||||
the `zip` link next to folders can produce various types of zip/tar files using these alternatives in the browser settings tab:
|
download folders (or file selections) as `zip` or `tar` files
|
||||||
|
|
||||||
|
select which type of archive you want in the browser settings tab:
|
||||||
|
|
||||||
| name | url-suffix | description |
|
| name | url-suffix | description |
|
||||||
|--|--|--|
|
|--|--|--|
|
||||||
@@ -306,15 +325,16 @@ the `zip` link next to folders can produce various types of zip/tar files using
|
|||||||
|
|
||||||
you can also zip a selection of files or folders by clicking them in the browser, that brings up a selection editor and zip button in the bottom right
|
you can also zip a selection of files or folders by clicking them in the browser, that brings up a selection editor and zip button in the bottom right
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## uploading
|
## uploading
|
||||||
|
|
||||||
two upload methods are available in the html client:
|
web-browsers can upload using `bup` and `up2k`:
|
||||||
* `[🎈] bup`, the basic uploader, supports almost every browser since netscape 4.0
|
* `[🎈] bup`, the basic uploader, supports almost every browser since netscape 4.0
|
||||||
* `[🚀] up2k`, the fancy one
|
* `[🚀] up2k`, the fancy one
|
||||||
|
|
||||||
you can undo/delete uploads using `[🧯] unpost` if the server is running with `-e2d`
|
you can undo/delete uploads using `[🧯]` [unpost](#unpost)
|
||||||
|
|
||||||
up2k has several advantages:
|
up2k has several advantages:
|
||||||
* you can drop folders into the browser (files are added recursively)
|
* you can drop folders into the browser (files are added recursively)
|
||||||
@@ -328,7 +348,7 @@ up2k has several advantages:
|
|||||||
|
|
||||||
see [up2k](#up2k) for details on how it works
|
see [up2k](#up2k) for details on how it works
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
**protip:** you can avoid scaring away users with [docs/minimal-up2k.html](docs/minimal-up2k.html) which makes it look [much simpler](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png)
|
**protip:** you can avoid scaring away users with [docs/minimal-up2k.html](docs/minimal-up2k.html) which makes it look [much simpler](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png)
|
||||||
|
|
||||||
@@ -336,7 +356,7 @@ the up2k UI is the epitome of polished inutitive experiences:
|
|||||||
* "parallel uploads" specifies how many chunks to upload at the same time
|
* "parallel uploads" specifies how many chunks to upload at the same time
|
||||||
* `[🏃]` analysis of other files should continue while one is uploading
|
* `[🏃]` analysis of other files should continue while one is uploading
|
||||||
* `[💭]` ask for confirmation before files are added to the list
|
* `[💭]` ask for confirmation before files are added to the list
|
||||||
* `[💤]` sync uploading between other copyparty tabs so only one is active
|
* `[💤]` sync uploading between other copyparty browser-tabs so only one is active
|
||||||
* `[🔎]` switch between upload and file-search mode
|
* `[🔎]` switch between upload and file-search mode
|
||||||
|
|
||||||
and then theres the tabs below it,
|
and then theres the tabs below it,
|
||||||
@@ -347,11 +367,14 @@ and then theres the tabs below it,
|
|||||||
* plus up to 3 entries each from `[done]` and `[que]` for context
|
* plus up to 3 entries each from `[done]` and `[que]` for context
|
||||||
* `[que]` is all the files that are still queued
|
* `[que]` is all the files that are still queued
|
||||||
|
|
||||||
|
|
||||||
### file-search
|
### file-search
|
||||||
|
|
||||||

|
drop files/folders into up2k to see if they exist on the server
|
||||||
|
|
||||||
in the `[🚀 up2k]` tab, after toggling the `[🔎]` switch green, any files/folders you drop onto the dropzone will be hashed on the client-side. Each hash is sent to the server which checks if that file exists somewhere already
|

|
||||||
|
|
||||||
|
in the `[🚀 up2k]` tab, after toggling the `[🔎]` switch green, any files/folders you drop onto the dropzone will be hashed on the client-side. Each hash is sent to the server which checks if that file exists somewhere
|
||||||
|
|
||||||
files go into `[ok]` if they exist (and you get a link to where it is), otherwise they land in `[ng]`
|
files go into `[ok]` if they exist (and you get a link to where it is), otherwise they land in `[ng]`
|
||||||
* the main reason filesearch is combined with the uploader is cause the code was too spaghetti to separate it out somewhere else, this is no longer the case but now i've warmed up to the idea too much
|
* the main reason filesearch is combined with the uploader is cause the code was too spaghetti to separate it out somewhere else, this is no longer the case but now i've warmed up to the idea too much
|
||||||
@@ -363,19 +386,28 @@ note that since up2k has to read the file twice, `[🎈 bup]` can be up to 2x fa
|
|||||||
up2k has saved a few uploads from becoming corrupted in-transfer already; caught an android phone on wifi redhanded in wireshark with a bitflip, however bup with https would *probably* have noticed as well (thanks to tls also functioning as an integrity check)
|
up2k has saved a few uploads from becoming corrupted in-transfer already; caught an android phone on wifi redhanded in wireshark with a bitflip, however bup with https would *probably* have noticed as well (thanks to tls also functioning as an integrity check)
|
||||||
|
|
||||||
|
|
||||||
|
### unpost
|
||||||
|
|
||||||
|
undo/delete accidental uploads
|
||||||
|
|
||||||
|

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

|
|
||||||
|
|
||||||
select some files and press F2 to bring up the rename UI
|
select some files and press F2 to bring up the rename UI
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
quick explanation of the buttons,
|
quick explanation of the buttons,
|
||||||
* `[✅ apply rename]` confirms and begins renaming
|
* `[✅ apply rename]` confirms and begins renaming
|
||||||
* `[❌ cancel]` aborts and closes the rename window
|
* `[❌ cancel]` aborts and closes the rename window
|
||||||
@@ -419,6 +451,8 @@ the metadata keys you can use in the format field are the ones in the file-brows
|
|||||||
|
|
||||||
## markdown viewer
|
## markdown viewer
|
||||||
|
|
||||||
|
and there are *two* editors
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
* the document preview has a max-width which is the same as an A4 paper when printed
|
* the document preview has a max-width which is the same as an A4 paper when printed
|
||||||
@@ -431,9 +465,11 @@ the metadata keys you can use in the format field are the ones in the file-brows
|
|||||||
* if you are using media hotkeys to switch songs and are getting tired of seeing the OSD popup which Windows doesn't let you disable, consider https://ocv.me/dev/?media-osd-bgone.ps1
|
* if you are using media hotkeys to switch songs and are getting tired of seeing the OSD popup which Windows doesn't let you disable, consider https://ocv.me/dev/?media-osd-bgone.ps1
|
||||||
|
|
||||||
|
|
||||||
# searching
|
## searching
|
||||||
|
|
||||||

|
search by size, date, path/name, mp3-tags, ...
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
when started with `-e2dsa` copyparty will scan/index all your files. This avoids duplicates on upload, and also makes the volumes searchable through the web-ui:
|
when started with `-e2dsa` copyparty will scan/index all your files. This avoids duplicates on upload, and also makes the volumes searchable through the web-ui:
|
||||||
* make search queries by `size`/`date`/`directory-path`/`filename`, or...
|
* make search queries by `size`/`date`/`directory-path`/`filename`, or...
|
||||||
@@ -443,12 +479,14 @@ path/name queries are space-separated, AND'ed together, and words are negated wi
|
|||||||
* path: `shibayan -bossa` finds all files where one of the folders contain `shibayan` but filters out any results where `bossa` exists somewhere in the path
|
* path: `shibayan -bossa` finds all files where one of the folders contain `shibayan` but filters out any results where `bossa` exists somewhere in the path
|
||||||
* name: `demetori styx` gives you [good stuff](https://www.youtube.com/watch?v=zGh0g14ZJ8I&list=PL3A147BD151EE5218&index=9)
|
* name: `demetori styx` gives you [good stuff](https://www.youtube.com/watch?v=zGh0g14ZJ8I&list=PL3A147BD151EE5218&index=9)
|
||||||
|
|
||||||
add `-e2ts` to also scan/index tags from music files:
|
add the argument `-e2ts` to also scan/index tags from music files, which brings us over to:
|
||||||
|
|
||||||
|
|
||||||
## search configuration
|
# server config
|
||||||
|
|
||||||
searching relies on two databases, the up2k filetree (`-e2d`) and the metadata tags (`-e2t`). Configuration can be done through arguments, volume flags, or a mix of both.
|
## file indexing
|
||||||
|
|
||||||
|
file indexing relies on two databases, the up2k filetree (`-e2d`) and the metadata tags (`-e2t`). Configuration can be done through arguments, volume flags, or a mix of both.
|
||||||
|
|
||||||
through arguments:
|
through arguments:
|
||||||
* `-e2d` enables file indexing on upload
|
* `-e2d` enables file indexing on upload
|
||||||
@@ -467,16 +505,60 @@ note:
|
|||||||
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise
|
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise
|
||||||
* the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher
|
* the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher
|
||||||
|
|
||||||
you can choose to only index filename/path/size/last-modified (and not the hash of the file contents) by setting `--no-hash` or the volume-flag `cdhash`, this has the following consequences:
|
you can choose to only index filename/path/size/last-modified (and not the hash of the file contents) by setting `--no-hash` or the volume-flag `:c,dhash`, this has the following consequences:
|
||||||
* initial indexing is way faster, especially when the volume is on a network disk
|
* initial indexing is way faster, especially when the volume is on a network disk
|
||||||
* makes it impossible to [file-search](#file-search)
|
* makes it impossible to [file-search](#file-search)
|
||||||
* if someone uploads the same file contents, the upload will not be detected as a dupe, so it will not get symlinked or rejected
|
* if someone uploads the same file contents, the upload will not be detected as a dupe, so it will not get symlinked or rejected
|
||||||
|
|
||||||
if you set `--no-hash`, you can enable hashing for specific volumes using flag `cehash`
|
if you set `--no-hash`, you can enable hashing for specific volumes using flag `:c,ehash`
|
||||||
|
|
||||||
|
|
||||||
|
## upload rules
|
||||||
|
|
||||||
|
set upload rules using volume flags, some examples:
|
||||||
|
|
||||||
|
* `:c,sz=1k-3m` sets allowed filesize between 1 KiB and 3 MiB inclusive (suffixes: b, k, m, g)
|
||||||
|
* `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`:
|
||||||
|
* `:c,rotn=1000,2` moves uploads into subfolders, up to 1000 files in each folder before making a new one, two levels deep (must be at least 1)
|
||||||
|
* `:c,rotf=%Y/%m/%d/%H` enforces files to be uploaded into a structure of subfolders according to that date format
|
||||||
|
* if someone uploads to `/foo/bar` the path would be rewritten to `/foo/bar/2021/08/06/23` for example
|
||||||
|
* but the actual value is not verified, just the structure, so the uploader can choose any values which conform to the format string
|
||||||
|
* just to avoid additional complexity in up2k which is enough of a mess already
|
||||||
|
* `:c,lifetime=300` delete uploaded files when they become 5 minutes old
|
||||||
|
|
||||||
|
you can also set transaction limits which apply per-IP and per-volume, but these assume `-j 1` (default) otherwise the limits will be off, for example `-j 4` would allow anywhere between 1x and 4x the limits you set depending on which processing node the client gets routed to
|
||||||
|
|
||||||
|
* `:c,maxn=250,3600` allows 250 files over 1 hour from each IP (tracked per-volume)
|
||||||
|
* `:c,maxb=1g,300` allows 1 GiB total over 5 minutes from each IP (tracked per-volume)
|
||||||
|
|
||||||
|
|
||||||
|
## compress uploads
|
||||||
|
|
||||||
|
files can be autocompressed on upload
|
||||||
|
|
||||||
|
compression is either on user-request (if config allows) or forced by server-config
|
||||||
|
|
||||||
|
* volume flag `gz` allows gz compression
|
||||||
|
* volume flag `xz` allows lzma compression
|
||||||
|
* volume flag `pk` **forces** compression on all files
|
||||||
|
* url parameter `pk` requests compression with server-default algorithm
|
||||||
|
* url parameter `gz` or `xz` requests compression with a specific algorithm
|
||||||
|
* url parameter `xz` requests xz compression
|
||||||
|
|
||||||
|
things to note,
|
||||||
|
* the `gz` and `xz` arguments take a single optional argument, the compression level (range 0 to 9)
|
||||||
|
* the `pk` volume flag takes the optional argument `ALGORITHM,LEVEL` which will then be forced for all uploads, for example `gz,9` or `xz,0`
|
||||||
|
* default compression is gzip level 9
|
||||||
|
* all upload methods except up2k are supported
|
||||||
|
* the files will be indexed after compression, so dupe-detection and file-search will not work as expected
|
||||||
|
|
||||||
|
some examples,
|
||||||
|
|
||||||
|
|
||||||
## database location
|
## database location
|
||||||
|
|
||||||
|
can be stored in-volume (default) or elsewhere
|
||||||
|
|
||||||
copyparty creates a subfolder named `.hist` inside each volume where it stores the database, thumbnails, and some other stuff
|
copyparty creates a subfolder named `.hist` inside each volume where it stores the database, thumbnails, and some other stuff
|
||||||
|
|
||||||
this can instead be kept in a single place using the `--hist` argument, or the `hist=` volume flag, or a mix of both:
|
this can instead be kept in a single place using the `--hist` argument, or the `hist=` volume flag, or a mix of both:
|
||||||
@@ -490,6 +572,8 @@ note:
|
|||||||
|
|
||||||
## metadata from audio files
|
## metadata from audio files
|
||||||
|
|
||||||
|
set `-e2t` to index tags on upload
|
||||||
|
|
||||||
`-mte` decides which tags to index and display in the browser (and also the display order), this can be changed per-volume:
|
`-mte` decides which tags to index and display in the browser (and also the display order), this can be changed per-volume:
|
||||||
* `-v ~/music::r:c,mte=title,artist` indexes and displays *title* followed by *artist*
|
* `-v ~/music::r:c,mte=title,artist` indexes and displays *title* followed by *artist*
|
||||||
|
|
||||||
@@ -513,6 +597,8 @@ see the beautiful mess of a dictionary in [mtag.py](https://github.com/9001/copy
|
|||||||
|
|
||||||
## file parser plugins
|
## file parser plugins
|
||||||
|
|
||||||
|
provide custom parsers to index additional tags
|
||||||
|
|
||||||
copyparty can invoke external programs to collect additional metadata for files using `mtp` (either as argument or volume flag), there is a default timeout of 30sec
|
copyparty can invoke external programs to collect additional metadata for files using `mtp` (either as argument or volume flag), there is a default timeout of 30sec
|
||||||
|
|
||||||
* `-mtp .bpm=~/bin/audio-bpm.py` will execute `~/bin/audio-bpm.py` with the audio file as argument 1 to provide the `.bpm` tag, if that does not exist in the audio metadata
|
* `-mtp .bpm=~/bin/audio-bpm.py` will execute `~/bin/audio-bpm.py` with the audio file as argument 1 to provide the `.bpm` tag, if that does not exist in the audio metadata
|
||||||
@@ -533,32 +619,38 @@ copyparty can invoke external programs to collect additional metadata for files
|
|||||||
|
|
||||||
# browser support
|
# browser support
|
||||||
|
|
||||||
|
TLDR: yes
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
`ie` = internet-explorer, `ff` = firefox, `c` = chrome, `iOS` = iPhone/iPad, `Andr` = Android
|
`ie` = internet-explorer, `ff` = firefox, `c` = chrome, `iOS` = iPhone/iPad, `Andr` = Android
|
||||||
|
|
||||||
| feature | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
|
| feature | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
|
||||||
| --------------- | --- | --- | ---- | ---- | ----- | ---- | --- | ---- |
|
| --------------- | --- | ---- | ---- | ---- | ----- | ---- | --- | ---- |
|
||||||
| browse files | yep | yep | yep | yep | yep | yep | yep | yep |
|
| browse files | yep | yep | yep | yep | yep | yep | yep | yep |
|
||||||
|
| thumbnail view | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| basic uploader | yep | yep | yep | yep | yep | yep | yep | yep |
|
| basic uploader | yep | yep | yep | yep | yep | yep | yep | yep |
|
||||||
|
| up2k | - | - | `*1` | `*1` | yep | yep | yep | yep |
|
||||||
| make directory | yep | yep | yep | yep | yep | yep | yep | yep |
|
| make directory | yep | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| send message | yep | yep | yep | yep | yep | yep | yep | yep |
|
| send message | yep | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| set sort order | - | yep | yep | yep | yep | yep | yep | yep |
|
| set sort order | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| zip selection | - | yep | yep | yep | yep | yep | yep | yep |
|
| zip selection | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| navpane | - | - | `*1` | yep | yep | yep | yep | yep |
|
| file rename | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| up2k | - | - | yep | yep | yep | yep | yep | yep |
|
| file cut/paste | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
|
| navpane | - | `*2` | yep | yep | yep | yep | yep | yep |
|
||||||
|
| image viewer | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
|
| video player | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| markdown editor | - | - | yep | yep | yep | yep | yep | yep |
|
| markdown editor | - | - | yep | yep | yep | yep | yep | yep |
|
||||||
| markdown viewer | - | - | yep | yep | yep | yep | yep | yep |
|
| markdown viewer | - | - | yep | yep | yep | yep | yep | yep |
|
||||||
| play mp3/m4a | - | yep | yep | yep | yep | yep | yep | yep |
|
| play mp3/m4a | - | yep | yep | yep | yep | yep | yep | yep |
|
||||||
| play ogg/opus | - | - | - | - | yep | yep | `*2` | yep |
|
| play ogg/opus | - | - | - | - | yep | yep | `*3` | yep |
|
||||||
| thumbnail view | - | - | - | - | yep | yep | yep | yep |
|
|
||||||
| image viewer | - | - | - | - | yep | yep | yep | yep |
|
|
||||||
| **= feature =** | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
|
| **= feature =** | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
|
||||||
|
|
||||||
* internet explorer 6 to 8 behave the same
|
* internet explorer 6 to 8 behave the same
|
||||||
* firefox 52 and chrome 49 are the last winxp versions
|
* firefox 52 and chrome 49 are the last winxp versions
|
||||||
* `*1` only public folders (login session is dropped) and no history / back-button
|
* `*1` yes, but extremely slow (ie10: 1 MiB/s, ie11: 270 KiB/s)
|
||||||
* `*2` using a wasm decoder which can sometimes get stuck and consumes a bit more power
|
* `*2` causes a full-page refresh on each navigation
|
||||||
|
* `*3` using a wasm decoder which can sometimes get stuck and consumes a bit more power
|
||||||
|
|
||||||
quick summary of more eccentric web-browsers trying to view a directory index:
|
quick summary of more eccentric web-browsers trying to view a directory index:
|
||||||
|
|
||||||
@@ -570,22 +662,25 @@ quick summary of more eccentric web-browsers trying to view a directory index:
|
|||||||
| **lynx** (2.8.9/macports) | can browse, login, upload/mkdir/msg |
|
| **lynx** (2.8.9/macports) | can browse, login, upload/mkdir/msg |
|
||||||
| **w3m** (0.5.3/macports) | can browse, login, upload at 100kB/s, mkdir/msg |
|
| **w3m** (0.5.3/macports) | can browse, login, upload at 100kB/s, mkdir/msg |
|
||||||
| **netsurf** (3.10/arch) | is basically ie6 with much better css (javascript has almost no effect) |
|
| **netsurf** (3.10/arch) | is basically ie6 with much better css (javascript has almost no effect) |
|
||||||
|
| **opera** (11.60/winxp) | OK: thumbnails, image-viewer, zip-selection, rename/cut/paste. NG: up2k, navpane, markdown, audio |
|
||||||
| **ie4** and **netscape** 4.0 | can browse (text is yellow on white), upload with `?b=u` |
|
| **ie4** and **netscape** 4.0 | can browse (text is yellow on white), upload with `?b=u` |
|
||||||
| **SerenityOS** (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 |
|
||||||
|
|
||||||
|
|
||||||
# client examples
|
# client examples
|
||||||
|
|
||||||
|
interact with copyparty using non-browser clients
|
||||||
|
|
||||||
* javascript: dump some state into a file (two separate examples)
|
* javascript: dump some state into a file (two separate examples)
|
||||||
* `await fetch('https://127.0.0.1:3923/', {method:"PUT", body: JSON.stringify(foo)});`
|
* `await fetch('https://127.0.0.1:3923/', {method:"PUT", body: JSON.stringify(foo)});`
|
||||||
* `var xhr = new XMLHttpRequest(); xhr.open('POST', 'https://127.0.0.1:3923/msgs?raw'); xhr.send('foo');`
|
* `var xhr = new XMLHttpRequest(); xhr.open('POST', 'https://127.0.0.1:3923/msgs?raw'); xhr.send('foo');`
|
||||||
|
|
||||||
* curl/wget: upload some files (post=file, chunk=stdin)
|
* curl/wget: upload some files (post=file, chunk=stdin)
|
||||||
* `post(){ curl -b cppwd=wark http://127.0.0.1:3923/ -F act=bput -F f=@"$1";}`
|
* `post(){ curl -b cppwd=wark -F act=bput -F f=@"$1" http://127.0.0.1:3923/;}`
|
||||||
`post movie.mkv`
|
`post movie.mkv`
|
||||||
* `post(){ wget --header='Cookie: cppwd=wark' http://127.0.0.1:3923/?raw --post-file="$1" -O-;}`
|
* `post(){ wget --header='Cookie: cppwd=wark' --post-file="$1" -O- http://127.0.0.1:3923/?raw;}`
|
||||||
`post movie.mkv`
|
`post movie.mkv`
|
||||||
* `chunk(){ curl -b cppwd=wark http://127.0.0.1:3923/ -T-;}`
|
* `chunk(){ curl -b cppwd=wark -T- http://127.0.0.1:3923/;}`
|
||||||
`chunk <movie.mkv`
|
`chunk <movie.mkv`
|
||||||
|
|
||||||
* FUSE: mount a copyparty server as a local filesystem
|
* FUSE: mount a copyparty server as a local filesystem
|
||||||
@@ -618,7 +713,9 @@ quick outline of the up2k protocol, see [uploading](#uploading) for the web-clie
|
|||||||
|
|
||||||
# performance
|
# performance
|
||||||
|
|
||||||
defaults are good for most cases, don't mind the `cannot efficiently use multiple CPU cores` message, it's very unlikely to be a problem
|
defaults are good for most cases
|
||||||
|
|
||||||
|
you can ignore the `cannot efficiently use multiple CPU cores` message, it's very unlikely to be a problem
|
||||||
|
|
||||||
below are some tweaks roughly ordered by usefulness:
|
below are some tweaks roughly ordered by usefulness:
|
||||||
|
|
||||||
@@ -635,11 +732,14 @@ below are some tweaks roughly ordered by usefulness:
|
|||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
|
|
||||||
|
mandatory deps:
|
||||||
* `jinja2` (is built into the SFX)
|
* `jinja2` (is built into the SFX)
|
||||||
|
|
||||||
|
|
||||||
## optional dependencies
|
## optional dependencies
|
||||||
|
|
||||||
|
install these to enable bonus features
|
||||||
|
|
||||||
enable music tags:
|
enable music tags:
|
||||||
* either `mutagen` (fast, pure-python, skips a few tags, makes copyparty GPL? idk)
|
* either `mutagen` (fast, pure-python, skips a few tags, makes copyparty GPL? idk)
|
||||||
* or `ffprobe` (20x slower, more accurate, possibly dangerous depending on your distro and users)
|
* or `ffprobe` (20x slower, more accurate, possibly dangerous depending on your distro and users)
|
||||||
@@ -672,7 +772,7 @@ these are standalone programs and will never be imported / evaluated by copypart
|
|||||||
|
|
||||||
# sfx
|
# sfx
|
||||||
|
|
||||||
currently there are two self-contained "binaries":
|
there are two self-contained "binaries":
|
||||||
* [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) -- pure python, works everywhere, **recommended**
|
* [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) -- pure python, works everywhere, **recommended**
|
||||||
* [copyparty-sfx.sh](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.sh) -- smaller, but only for linux and macos, kinda deprecated
|
* [copyparty-sfx.sh](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.sh) -- smaller, but only for linux and macos, kinda deprecated
|
||||||
|
|
||||||
@@ -683,6 +783,8 @@ pls note that `copyparty-sfx.sh` will fail if you rename `copyparty-sfx.py` to `
|
|||||||
|
|
||||||
## sfx repack
|
## sfx repack
|
||||||
|
|
||||||
|
reduce the size of an sfx by removing features
|
||||||
|
|
||||||
if you don't need all the features, you can repack the sfx and save a bunch of space; all you need is an sfx and a copy of this repo (nothing else to download or build, except if you're on windows then you need msys2 or WSL)
|
if you don't need all the features, you can repack the sfx and save a bunch of space; all you need is an sfx and a copy of this repo (nothing else to download or build, except if you're on windows then you need msys2 or WSL)
|
||||||
* `525k` size of original sfx.py as of v0.11.30
|
* `525k` size of original sfx.py as of v0.11.30
|
||||||
* `315k` after `./scripts/make-sfx.sh re no-ogv`
|
* `315k` after `./scripts/make-sfx.sh re no-ogv`
|
||||||
@@ -726,7 +828,7 @@ pip install black bandit pylint flake8 # vscode tooling
|
|||||||
|
|
||||||
## just the sfx
|
## just the sfx
|
||||||
|
|
||||||
unless you need to modify something in the web-dependencies, it's faster to grab those from a previous release:
|
grab the web-dependencies from a previous sfx (unless you need to modify something in those):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
rm -rf copyparty/web/deps
|
rm -rf copyparty/web/deps
|
||||||
@@ -746,7 +848,7 @@ then build the sfx using any of the following examples:
|
|||||||
|
|
||||||
## complete release
|
## complete release
|
||||||
|
|
||||||
also builds the sfx so disregard the sfx section above
|
also builds the sfx so skip the sfx section above
|
||||||
|
|
||||||
in the `scripts` folder:
|
in the `scripts` folder:
|
||||||
|
|
||||||
|
|||||||
@@ -61,3 +61,8 @@ cd /mnt/nas/music/.hist
|
|||||||
~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy key
|
~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy key
|
||||||
~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy .bpm -vac
|
~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy .bpm -vac
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# [`prisonparty.sh`](prisonparty.sh)
|
||||||
|
* run copyparty in a chroot, preventing any accidental file access
|
||||||
|
* creates bindmounts for /bin, /lib, and so on, see `sysdirs=`
|
||||||
|
|||||||
39
bin/mtag/res/yt-ipr.conf
Normal file
39
bin/mtag/res/yt-ipr.conf
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# example config file to use copyparty as a youtube manifest collector,
|
||||||
|
# use with copyparty like: python copyparty.py -c yt-ipr.conf
|
||||||
|
#
|
||||||
|
# see docs/example.conf for a better explanation of the syntax, but
|
||||||
|
# newlines are block separators, so adding blank lines inside a volume definition is bad
|
||||||
|
# (use comments as separators instead)
|
||||||
|
|
||||||
|
|
||||||
|
# create user ed, password wark
|
||||||
|
u ed:wark
|
||||||
|
|
||||||
|
|
||||||
|
# create a volume at /ytm which stores files at ./srv/ytm
|
||||||
|
./srv/ytm
|
||||||
|
/ytm
|
||||||
|
# write-only, but read-write for user ed
|
||||||
|
w
|
||||||
|
rw ed
|
||||||
|
# rescan the volume on startup
|
||||||
|
c e2dsa
|
||||||
|
# collect tags from all new files since last scan
|
||||||
|
c e2ts
|
||||||
|
# optionally enable compression to make the files 50% smaller
|
||||||
|
c pk
|
||||||
|
# only allow uploads which are between 16k and 1m large
|
||||||
|
c sz=16k-1m
|
||||||
|
# allow up to 10 uploads over 5 minutes from each ip
|
||||||
|
c maxn=10,300
|
||||||
|
# move uploads into subfolders: YEAR-MONTH / DAY-HOUR / <upload>
|
||||||
|
c rotf=%Y-%m/%d-%H
|
||||||
|
# delete uploads when they are 24 hours old
|
||||||
|
c lifetime=86400
|
||||||
|
# add the parser and tell copyparty what tags it can expect from it
|
||||||
|
c mtp=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires=bin/mtag/yt-ipr.py
|
||||||
|
# decide which tags we want to index and in what order
|
||||||
|
c mte=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires
|
||||||
|
|
||||||
|
|
||||||
|
# create any other volumes you'd like down here, or merge this with an existing config file
|
||||||
47
bin/mtag/res/yt-ipr.user.js
Normal file
47
bin/mtag/res/yt-ipr.user.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name youtube-playerdata-hub
|
||||||
|
// @match https://youtube.com/*
|
||||||
|
// @match https://*.youtube.com/*
|
||||||
|
// @version 1.0
|
||||||
|
// @grant GM_addStyle
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
var server = 'https://127.0.0.1:3923/ytm?pw=wark',
|
||||||
|
interval = 60; // sec
|
||||||
|
|
||||||
|
var sent = {};
|
||||||
|
function send(txt, mf_url, desc) {
|
||||||
|
if (sent[mf_url])
|
||||||
|
return;
|
||||||
|
|
||||||
|
fetch(server + '&_=' + Date.now(), { method: "PUT", body: txt });
|
||||||
|
console.log('[yt-pdh] yeet %d bytes, %s', txt.length, desc);
|
||||||
|
sent[mf_url] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collect() {
|
||||||
|
try {
|
||||||
|
var pd = document.querySelector('ytd-watch-flexy');
|
||||||
|
if (!pd)
|
||||||
|
return console.log('[yt-pdh] no video found');
|
||||||
|
|
||||||
|
pd = pd.playerData;
|
||||||
|
var mu = pd.streamingData.dashManifestUrl || pd.streamingData.hlsManifestUrl;
|
||||||
|
if (!mu || !mu.length)
|
||||||
|
return console.log('[yt-pdh] no manifest found');
|
||||||
|
|
||||||
|
var desc = pd.videoDetails.videoId + ', ' + pd.videoDetails.title;
|
||||||
|
send(JSON.stringify(pd), mu, desc);
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.log("[yt-pdh]", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInterval(collect, interval * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
var scr = document.createElement('script');
|
||||||
|
scr.textContent = '(' + main.toString() + ')();';
|
||||||
|
(document.head || document.getElementsByTagName('head')[0]).appendChild(scr);
|
||||||
|
console.log('[yt-pdh] a');
|
||||||
198
bin/mtag/yt-ipr.py
Normal file
198
bin/mtag/yt-ipr.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import string
|
||||||
|
import urllib.request
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
"""
|
||||||
|
youtube initial player response
|
||||||
|
|
||||||
|
it's probably best to use this through a config file; see res/yt-ipr.conf
|
||||||
|
|
||||||
|
but if you want to use plain arguments instead then:
|
||||||
|
-v srv/ytm:ytm:w:rw,ed
|
||||||
|
:c,e2ts:c,e2dsa
|
||||||
|
:c,sz=16k-1m:c,maxn=10,300:c,rotf=%Y-%m/%d-%H
|
||||||
|
:c,mtp=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires=bin/mtag/yt-ipr.py
|
||||||
|
:c,mte=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires
|
||||||
|
|
||||||
|
see res/yt-ipr.user.js for the example userscript to go with this
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
with gzip.open(sys.argv[1], "rt", encoding="utf-8", errors="replace") as f:
|
||||||
|
txt = f.read()
|
||||||
|
except:
|
||||||
|
with open(sys.argv[1], "r", encoding="utf-8", errors="replace") as f:
|
||||||
|
txt = f.read()
|
||||||
|
|
||||||
|
txt = "{" + txt.split("{", 1)[1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
pd = json.loads(txt)
|
||||||
|
except json.decoder.JSONDecodeError as ex:
|
||||||
|
pd = json.loads(txt[: ex.pos])
|
||||||
|
|
||||||
|
# print(json.dumps(pd, indent=2))
|
||||||
|
|
||||||
|
if "videoDetails" in pd:
|
||||||
|
parse_youtube(pd)
|
||||||
|
else:
|
||||||
|
parse_freg(pd)
|
||||||
|
|
||||||
|
|
||||||
|
def get_expiration(url):
|
||||||
|
et = re.search(r"[?&]expire=([0-9]+)", url).group(1)
|
||||||
|
et = datetime.utcfromtimestamp(int(et))
|
||||||
|
return et.strftime("%Y-%m-%d, %H:%M")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_youtube(pd):
|
||||||
|
vd = pd["videoDetails"]
|
||||||
|
sd = pd["streamingData"]
|
||||||
|
|
||||||
|
et = sd["adaptiveFormats"][0]["url"]
|
||||||
|
et = get_expiration(et)
|
||||||
|
|
||||||
|
mf = []
|
||||||
|
if "dashManifestUrl" in sd:
|
||||||
|
mf.append("dash")
|
||||||
|
if "hlsManifestUrl" in sd:
|
||||||
|
mf.append("hls")
|
||||||
|
|
||||||
|
r = {
|
||||||
|
"yt-id": vd["videoId"],
|
||||||
|
"yt-title": vd["title"],
|
||||||
|
"yt-author": vd["author"],
|
||||||
|
"yt-channel": vd["channelId"],
|
||||||
|
"yt-views": vd["viewCount"],
|
||||||
|
"yt-private": vd["isPrivate"],
|
||||||
|
# "yt-expires": sd["expiresInSeconds"],
|
||||||
|
"yt-manifest": ",".join(mf),
|
||||||
|
"yt-expires": et,
|
||||||
|
}
|
||||||
|
print(json.dumps(r))
|
||||||
|
|
||||||
|
freg_conv(pd)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_freg(pd):
|
||||||
|
md = pd["metadata"]
|
||||||
|
r = {
|
||||||
|
"yt-id": md["id"],
|
||||||
|
"yt-title": md["title"],
|
||||||
|
"yt-author": md["channelName"],
|
||||||
|
"yt-channel": md["channelURL"].strip("/").split("/")[-1],
|
||||||
|
"yt-expires": get_expiration(list(pd["video"].values())[0]),
|
||||||
|
}
|
||||||
|
print(json.dumps(r))
|
||||||
|
|
||||||
|
|
||||||
|
def freg_conv(pd):
|
||||||
|
# based on getURLs.js v1.5 (2021-08-07)
|
||||||
|
# fmt: off
|
||||||
|
priority = {
|
||||||
|
"video": [
|
||||||
|
337, 315, 266, 138, # 2160p60
|
||||||
|
313, 336, # 2160p
|
||||||
|
308, # 1440p60
|
||||||
|
271, 264, # 1440p
|
||||||
|
335, 303, 299, # 1080p60
|
||||||
|
248, 169, 137, # 1080p
|
||||||
|
334, 302, 298, # 720p60
|
||||||
|
247, 136 # 720p
|
||||||
|
],
|
||||||
|
"audio": [
|
||||||
|
251, 141, 171, 140, 250, 249, 139
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
vid_id = pd["videoDetails"]["videoId"]
|
||||||
|
chan_id = pd["videoDetails"]["channelId"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
thumb_url = pd["microformat"]["playerMicroformatRenderer"]["thumbnail"]["thumbnails"][0]["url"]
|
||||||
|
start_ts = pd["microformat"]["playerMicroformatRenderer"]["liveBroadcastDetails"]["startTimestamp"]
|
||||||
|
except:
|
||||||
|
thumb_url = f"https://img.youtube.com/vi/{vid_id}/maxresdefault.jpg"
|
||||||
|
start_ts = ""
|
||||||
|
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"title": pd["videoDetails"]["title"],
|
||||||
|
"id": vid_id,
|
||||||
|
"channelName": pd["videoDetails"]["author"],
|
||||||
|
"channelURL": "https://www.youtube.com/channel/" + chan_id,
|
||||||
|
"description": pd["videoDetails"]["shortDescription"],
|
||||||
|
"thumbnailUrl": thumb_url,
|
||||||
|
"startTimestamp": start_ts,
|
||||||
|
}
|
||||||
|
|
||||||
|
if [x for x in vid_id if x not in string.ascii_letters + string.digits + "_-"]:
|
||||||
|
print(f"malicious json", file=sys.stderr)
|
||||||
|
return
|
||||||
|
|
||||||
|
basepath = os.path.dirname(sys.argv[1])
|
||||||
|
|
||||||
|
thumb_fn = f"{basepath}/{vid_id}.jpg"
|
||||||
|
tmp_fn = f"{thumb_fn}.{os.getpid()}"
|
||||||
|
if not os.path.exists(thumb_fn) and (
|
||||||
|
thumb_url.startswith("https://img.youtube.com/vi/")
|
||||||
|
or thumb_url.startswith("https://i.ytimg.com/vi/")
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(thumb_url) as fi:
|
||||||
|
with open(tmp_fn, "wb") as fo:
|
||||||
|
fo.write(fi.read())
|
||||||
|
|
||||||
|
os.rename(tmp_fn, thumb_fn)
|
||||||
|
except:
|
||||||
|
if os.path.exists(tmp_fn):
|
||||||
|
os.unlink(tmp_fn)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(thumb_fn, "rb") as f:
|
||||||
|
thumb = base64.b64encode(f.read()).decode("ascii")
|
||||||
|
except:
|
||||||
|
thumb = "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k="
|
||||||
|
|
||||||
|
metadata["thumbnail"] = "data:image/jpeg;base64," + thumb
|
||||||
|
|
||||||
|
ret = {
|
||||||
|
"metadata": metadata,
|
||||||
|
"version": "1.5",
|
||||||
|
"createTime": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for stream, itags in priority.items():
|
||||||
|
for itag in itags:
|
||||||
|
url = None
|
||||||
|
for afmt in pd["streamingData"]["adaptiveFormats"]:
|
||||||
|
if itag == afmt["itag"]:
|
||||||
|
url = afmt["url"]
|
||||||
|
break
|
||||||
|
|
||||||
|
if url:
|
||||||
|
ret[stream] = {itag: url}
|
||||||
|
break
|
||||||
|
|
||||||
|
fn = f"{basepath}/{vid_id}.urls.json"
|
||||||
|
with open(fn, "w", encoding="utf-8", errors="replace") as f:
|
||||||
|
f.write(json.dumps(ret, indent=4))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except:
|
||||||
|
# raise
|
||||||
|
pass
|
||||||
99
bin/prisonparty.sh
Normal file
99
bin/prisonparty.sh
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# runs copyparty (or any other program really) in a chroot
|
||||||
|
#
|
||||||
|
# assumption: these directories, and everything within, are owned by root
|
||||||
|
sysdirs=( /bin /lib /lib32 /lib64 /sbin /usr )
|
||||||
|
|
||||||
|
|
||||||
|
# error-handler
|
||||||
|
help() { cat <<'EOF'
|
||||||
|
|
||||||
|
usage:
|
||||||
|
./prisonparty.sh <ROOTDIR> <UID> <GID> [VOLDIR [VOLDIR...]] -- copyparty-sfx.py [...]"
|
||||||
|
|
||||||
|
example:
|
||||||
|
./prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt/nas/music -- copyparty-sfx.py -v /mnt/nas/music::rwmd"
|
||||||
|
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# read arguments
|
||||||
|
trap help EXIT
|
||||||
|
jail="$(realpath "$1")"; shift
|
||||||
|
uid="$1"; shift
|
||||||
|
gid="$1"; shift
|
||||||
|
|
||||||
|
vols=()
|
||||||
|
while true; do
|
||||||
|
v="$1"; shift
|
||||||
|
[ "$v" = -- ] && break # end of volumes
|
||||||
|
[ "$#" -eq 0 ] && break # invalid usage
|
||||||
|
vols+=( "$(realpath "$v")" )
|
||||||
|
done
|
||||||
|
pybin="$1"; shift
|
||||||
|
pybin="$(realpath "$pybin")"
|
||||||
|
cpp="$1"; shift
|
||||||
|
cpp="$(realpath "$cpp")"
|
||||||
|
cppdir="$(dirname "$cpp")"
|
||||||
|
trap - EXIT
|
||||||
|
|
||||||
|
|
||||||
|
# debug/vis
|
||||||
|
echo
|
||||||
|
echo "chroot-dir = $jail"
|
||||||
|
echo "user:group = $uid:$gid"
|
||||||
|
echo " copyparty = $cpp"
|
||||||
|
echo
|
||||||
|
printf '\033[33m%s\033[0m\n' "copyparty can access these folders and all their subdirectories:"
|
||||||
|
for v in "${vols[@]}"; do
|
||||||
|
printf '\033[36m ├─\033[0m %s \033[36m ── added by (You)\033[0m\n' "$v"
|
||||||
|
done
|
||||||
|
printf '\033[36m ├─\033[0m %s \033[36m ── where the copyparty binary is\033[0m\n' "$cppdir"
|
||||||
|
printf '\033[36m ╰─\033[0m %s \033[36m ── the folder you are currently in\033[0m\n' "$PWD"
|
||||||
|
vols+=("$cppdir" "$PWD")
|
||||||
|
echo
|
||||||
|
|
||||||
|
|
||||||
|
# remove any trailing slashes
|
||||||
|
jail="${jail%/}"
|
||||||
|
cppdir="${cppdir%/}"
|
||||||
|
|
||||||
|
|
||||||
|
# bind-mount system directories and volumes
|
||||||
|
printf '%s\n' "${sysdirs[@]}" "${vols[@]}" | LC_ALL=C sort |
|
||||||
|
while IFS= read -r v; do
|
||||||
|
[ -e "$v" ] || {
|
||||||
|
# printf '\033[1;31mfolder does not exist:\033[0m %s\n' "/$v"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i1=$(stat -c%D.%i "$v" 2>/dev/null || echo a)
|
||||||
|
i2=$(stat -c%D.%i "$jail$v" 2>/dev/null || echo b)
|
||||||
|
[ $i1 = $i2 ] && continue
|
||||||
|
|
||||||
|
mkdir -p "$jail$v"
|
||||||
|
mount --bind "$v" "$jail$v"
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
# create a tmp
|
||||||
|
mkdir -p "$jail/tmp"
|
||||||
|
chmod 777 "$jail/tmp"
|
||||||
|
|
||||||
|
|
||||||
|
# run copyparty
|
||||||
|
/sbin/chroot --userspec=$uid:$gid "$jail" "$pybin" "$cpp" "$@" && rv=0 || rv=$?
|
||||||
|
|
||||||
|
|
||||||
|
# cleanup if not in use
|
||||||
|
lsof "$jail" | grep -qF "$jail" &&
|
||||||
|
echo "chroot is in use, will not cleanup" ||
|
||||||
|
{
|
||||||
|
mount | grep -qF " on $jail" |
|
||||||
|
awk '{sub(/ type .*/,"");sub(/.* on /,"");print}' |
|
||||||
|
LC_ALL=C sort -r | tee /dev/stderr | tr '\n' '\0' | xargs -r0 umount
|
||||||
|
}
|
||||||
|
exit $rv
|
||||||
@@ -8,11 +8,11 @@
|
|||||||
#
|
#
|
||||||
# you may want to:
|
# you may want to:
|
||||||
# change '/usr/bin/python' to another interpreter
|
# change '/usr/bin/python' to another interpreter
|
||||||
# change '/mnt::a' to another location or permission-set
|
# change '/mnt::rw' to another location or permission-set
|
||||||
|
|
||||||
name="$SVCNAME"
|
name="$SVCNAME"
|
||||||
command_background=true
|
command_background=true
|
||||||
pidfile="/var/run/$SVCNAME.pid"
|
pidfile="/var/run/$SVCNAME.pid"
|
||||||
|
|
||||||
command="/usr/bin/python /usr/local/bin/copyparty-sfx.py"
|
command="/usr/bin/python /usr/local/bin/copyparty-sfx.py"
|
||||||
command_args="-q -v /mnt::a"
|
command_args="-q -v /mnt::rw"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
#
|
#
|
||||||
# you may want to:
|
# you may want to:
|
||||||
# change '/usr/bin/python' to another interpreter
|
# change '/usr/bin/python' to another interpreter
|
||||||
# change '/mnt::a' to another location or permission-set
|
# change '/mnt::rw' to another location or permission-set
|
||||||
#
|
#
|
||||||
# with `Type=notify`, copyparty will signal systemd when it is ready to
|
# 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.
|
||||||
@@ -27,7 +27,7 @@ Description=copyparty file server
|
|||||||
[Service]
|
[Service]
|
||||||
Type=notify
|
Type=notify
|
||||||
SyslogIdentifier=copyparty
|
SyslogIdentifier=copyparty
|
||||||
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::a
|
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw
|
||||||
ExecStartPre=/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
ExecStartPre=/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|||||||
27
contrib/systemd/prisonparty.service
Normal file
27
contrib/systemd/prisonparty.service
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# this will start `/usr/local/bin/copyparty-sfx.py`
|
||||||
|
# in a chroot, preventing accidental access elsewhere
|
||||||
|
# and share '/mnt' with anonymous read+write
|
||||||
|
#
|
||||||
|
# installation:
|
||||||
|
# 1) put copyparty-sfx.py and prisonparty.sh in /usr/local/bin
|
||||||
|
# 2) cp -pv prisonparty.service /etc/systemd/system && systemctl enable --now prisonparty
|
||||||
|
#
|
||||||
|
# you may want to:
|
||||||
|
# change '/mnt::rw' to another location or permission-set
|
||||||
|
# (remember to change the '/mnt' chroot arg too)
|
||||||
|
#
|
||||||
|
# enable line-buffering for realtime logging (slight performance cost):
|
||||||
|
# inside the [Service] block, add the following line:
|
||||||
|
# Environment=PYTHONUNBUFFERED=x
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=copyparty file server
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
SyslogIdentifier=prisonparty
|
||||||
|
WorkingDirectory=/usr/local/bin
|
||||||
|
ExecStart=/bin/bash /usr/local/bin/prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt -- \
|
||||||
|
/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -25,6 +25,28 @@ ANYWIN = WINDOWS or sys.platform in ["msys"]
|
|||||||
MACOS = platform.system() == "Darwin"
|
MACOS = platform.system() == "Darwin"
|
||||||
|
|
||||||
|
|
||||||
|
def get_unix_home():
|
||||||
|
try:
|
||||||
|
v = os.environ["XDG_CONFIG_HOME"]
|
||||||
|
if not v:
|
||||||
|
raise Exception()
|
||||||
|
ret = os.path.normpath(v)
|
||||||
|
os.listdir(ret)
|
||||||
|
return ret
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
v = os.path.expanduser("~/.config")
|
||||||
|
if v.startswith("~"):
|
||||||
|
raise Exception()
|
||||||
|
ret = os.path.normpath(v)
|
||||||
|
os.listdir(ret)
|
||||||
|
return ret
|
||||||
|
except:
|
||||||
|
return "/tmp"
|
||||||
|
|
||||||
|
|
||||||
class EnvParams(object):
|
class EnvParams(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.t0 = time.time()
|
self.t0 = time.time()
|
||||||
@@ -37,10 +59,7 @@ class EnvParams(object):
|
|||||||
elif sys.platform == "darwin":
|
elif sys.platform == "darwin":
|
||||||
self.cfg = os.path.expanduser("~/Library/Preferences/copyparty")
|
self.cfg = os.path.expanduser("~/Library/Preferences/copyparty")
|
||||||
else:
|
else:
|
||||||
self.cfg = os.path.normpath(
|
self.cfg = get_unix_home() + "/copyparty"
|
||||||
os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
|
||||||
+ "/copyparty"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.cfg = self.cfg.replace("\\", "/")
|
self.cfg = self.cfg.replace("\\", "/")
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from textwrap import dedent
|
|||||||
from .__init__ import E, WINDOWS, VT100, PY2, unicode
|
from .__init__ import E, WINDOWS, VT100, PY2, unicode
|
||||||
from .__version__ import S_VERSION, S_BUILD_DT, CODENAME
|
from .__version__ import S_VERSION, S_BUILD_DT, CODENAME
|
||||||
from .svchub import SvcHub
|
from .svchub import SvcHub
|
||||||
from .util import py_desc, align_tab, IMPLICATIONS
|
from .util import py_desc, align_tab, IMPLICATIONS, ansi_re
|
||||||
from .authsrv import re_vol
|
from .authsrv import re_vol
|
||||||
|
|
||||||
HAVE_SSL = True
|
HAVE_SSL = True
|
||||||
@@ -67,8 +67,12 @@ class Dodge11874(RiceFormatter):
|
|||||||
def lprint(*a, **ka):
|
def lprint(*a, **ka):
|
||||||
global printed
|
global printed
|
||||||
|
|
||||||
printed += " ".join(unicode(x) for x in a) + ka.get("end", "\n")
|
txt = " ".join(unicode(x) for x in a) + ka.get("end", "\n")
|
||||||
print(*a, **ka)
|
printed += txt
|
||||||
|
if not VT100:
|
||||||
|
txt = ansi_re.sub("", txt)
|
||||||
|
|
||||||
|
print(txt, **ka)
|
||||||
|
|
||||||
|
|
||||||
def warn(msg):
|
def warn(msg):
|
||||||
@@ -197,12 +201,18 @@ def run_argparse(argv, formatter):
|
|||||||
formatter_class=formatter,
|
formatter_class=formatter,
|
||||||
prog="copyparty",
|
prog="copyparty",
|
||||||
description="http file sharing hub v{} ({})".format(S_VERSION, S_BUILD_DT),
|
description="http file sharing hub v{} ({})".format(S_VERSION, S_BUILD_DT),
|
||||||
epilog=dedent(
|
)
|
||||||
|
|
||||||
|
sects = [
|
||||||
|
[
|
||||||
|
"accounts",
|
||||||
|
"accounts and volumes",
|
||||||
|
dedent(
|
||||||
"""
|
"""
|
||||||
-a takes username:password,
|
-a takes username:password,
|
||||||
-v takes src:dst:perm1:perm2:permN:cflag1:cflag2:cflagN:...
|
-v takes src:dst:perm1:perm2:permN:volflag1:volflag2:volflagN:...
|
||||||
where "perm" is "accesslevels,username1,username2,..."
|
where "perm" is "accesslevels,username1,username2,..."
|
||||||
and "cflag" is config flags to set on this volume
|
and "volflag" is config flags to set on this volume
|
||||||
|
|
||||||
list of accesslevels:
|
list of accesslevels:
|
||||||
"r" (read): list folder contents, download files
|
"r" (read): list folder contents, download files
|
||||||
@@ -210,11 +220,7 @@ def run_argparse(argv, formatter):
|
|||||||
"m" (move): move files and folders; need "w" at destination
|
"m" (move): move files and folders; need "w" at destination
|
||||||
"d" (delete): permanently delete files and folders
|
"d" (delete): permanently delete files and folders
|
||||||
|
|
||||||
list of cflags:
|
too many volflags to list here, see the other sections
|
||||||
"c,nodupe" rejects existing files (instead of symlinking them)
|
|
||||||
"c,e2d" sets -e2d (all -e2* args can be set using ce2* cflags)
|
|
||||||
"c,d2t" disables metadata collection, overrides -e2t*
|
|
||||||
"c,d2d" disables all database stuff, overrides -e2*
|
|
||||||
|
|
||||||
example:\033[35m
|
example:\033[35m
|
||||||
-a ed:hunter2 -v .::r:rw,ed -v ../inc:dump:w:rw,ed:c,nodupe \033[36m
|
-a ed:hunter2 -v .::r:rw,ed -v ../inc:dump:w:rw,ed:c,nodupe \033[36m
|
||||||
@@ -231,29 +237,86 @@ def run_argparse(argv, formatter):
|
|||||||
|
|
||||||
consider the config file for more flexible account/volume management,
|
consider the config file for more flexible account/volume management,
|
||||||
including dynamic reload at runtime (and being more readable w)
|
including dynamic reload at runtime (and being more readable w)
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"flags",
|
||||||
|
"list of volflags",
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
|
volflags are appended to volume definitions, for example,
|
||||||
|
to create a write-only volume with the \033[33mnodupe\033[0m and \033[32mnosub\033[0m flags:
|
||||||
|
\033[35m-v /mnt/inc:/inc:w\033[33m:c,nodupe\033[32m:c,nosub
|
||||||
|
|
||||||
|
\033[0muploads, general:
|
||||||
|
\033[36mnodupe\033[35m rejects existing files (instead of symlinking them)
|
||||||
|
\033[36mnosub\033[35m forces all uploads into the top folder of the vfs
|
||||||
|
\033[36mgz\033[35m allows server-side gzip of uploads with ?gz (also c,xz)
|
||||||
|
\033[36mpk\033[35m forces server-side compression, optional arg: xz,9
|
||||||
|
|
||||||
|
\033[0mupload rules:
|
||||||
|
\033[36mmaxn=250,600\033[35m max 250 uploads over 15min
|
||||||
|
\033[36mmaxb=1g,300\033[35m max 1 GiB over 5min (suffixes: b, k, m, g)
|
||||||
|
\033[36msz=1k-3m\033[35m allow filesizes between 1 KiB and 3MiB
|
||||||
|
|
||||||
|
\033[0mupload rotation:
|
||||||
|
(moves all uploads into the specified folder structure)
|
||||||
|
\033[36mrotn=100,3\033[35m 3 levels of subfolders with 100 entries in each
|
||||||
|
\033[36mrotf=%Y-%m/%d-%H\033[35m date-formatted organizing
|
||||||
|
\033[36mlifetime=3600\033[35m uploads are deleted after 1 hour
|
||||||
|
|
||||||
|
\033[0mdatabase, general:
|
||||||
|
\033[36me2d\033[35m sets -e2d (all -e2* args can be set using ce2* volflags)
|
||||||
|
\033[36md2t\033[35m disables metadata collection, overrides -e2t*
|
||||||
|
\033[36md2d\033[35m disables all database stuff, overrides -e2*
|
||||||
|
\033[36mdhash\033[35m disables file hashing on initial scans, also ehash
|
||||||
|
\033[36mhist=/tmp/cdb\033[35m puts thumbnails and indexes at that location
|
||||||
|
\033[36mscan=60\033[35m scan for new files every 60sec, same as --re-maxage
|
||||||
|
|
||||||
|
\033[0mdatabase, audio tags:
|
||||||
|
"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...
|
||||||
|
\033[36mmtp=.bpm=f,audio-bpm.py\033[35m uses the "audio-bpm.py" program to
|
||||||
|
generate ".bpm" tags from uploads (f = overwrite tags)
|
||||||
|
\033[36mmtp=ahash,vhash=media-hash.py\033[35m collects two tags at once
|
||||||
|
\033[0m"""
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"urlform",
|
||||||
|
"",
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
values for --urlform:
|
values for --urlform:
|
||||||
"stash" dumps the data to file and returns length + checksum
|
\033[36mstash\033[35m dumps the data to file and returns length + checksum
|
||||||
"save,get" dumps to file and returns the page like a GET
|
\033[36msave,get\033[35m dumps to file and returns the page like a GET
|
||||||
"print,get" prints the data in the log and returns GET
|
\033[36mprint,get\033[35m prints the data in the log and returns GET
|
||||||
(leave out the ",get" to return an error instead)
|
(leave out the ",get" to return an error instead)
|
||||||
|
"""
|
||||||
values for --ls:
|
),
|
||||||
"USR" is a user to browse as; * is anonymous, ** is all users
|
],
|
||||||
"VOL" is a single volume to scan, default is * (all vols)
|
[
|
||||||
"FLAG" is flags;
|
"ls",
|
||||||
"v" in addition to realpaths, print usernames and vpaths
|
"volume inspection",
|
||||||
"ln" only prints symlinks leaving the volume mountpoint
|
dedent(
|
||||||
"p" exits 1 if any such symlinks are found
|
"""
|
||||||
"r" resumes startup after the listing
|
\033[35m--ls USR,VOL,FLAGS
|
||||||
|
\033[36mUSR\033[0m is a user to browse as; * is anonymous, ** is all users
|
||||||
|
\033[36mVOL\033[0m is a single volume to scan, default is * (all vols)
|
||||||
|
\033[36mFLAG\033[0m is flags;
|
||||||
|
\033[36mv\033[0m in addition to realpaths, print usernames and vpaths
|
||||||
|
\033[36mln\033[0m only prints symlinks leaving the volume mountpoint
|
||||||
|
\033[36mp\033[0m exits 1 if any such symlinks are found
|
||||||
|
\033[36mr\033[0m resumes startup after the listing
|
||||||
examples:
|
examples:
|
||||||
--ls '**' # list all files which are possible to read
|
--ls '**' # list all files which are possible to read
|
||||||
--ls '**,*,ln' # check for dangerous symlinks
|
--ls '**,*,ln' # check for dangerous symlinks
|
||||||
--ls '**,*,ln,p,r' # check, then start normally if safe
|
--ls '**,*,ln,p,r' # check, then start normally if safe
|
||||||
\033[0m
|
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
)
|
],
|
||||||
|
]
|
||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
u = unicode
|
u = unicode
|
||||||
ap2 = ap.add_argument_group('general options')
|
ap2 = ap.add_argument_group('general options')
|
||||||
@@ -292,6 +355,7 @@ def run_argparse(argv, formatter):
|
|||||||
ap2.add_argument("-nih", action="store_true", help="no info hostname")
|
ap2.add_argument("-nih", action="store_true", help="no info hostname")
|
||||||
ap2.add_argument("-nid", action="store_true", help="no info disk-usage")
|
ap2.add_argument("-nid", action="store_true", help="no info disk-usage")
|
||||||
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
|
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
|
||||||
|
ap2.add_argument("--no-lifetime", action="store_true", help="disable automatic deletion of uploads after a certain time (lifetime volflag)")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('safety options')
|
ap2 = ap.add_argument_group('safety options')
|
||||||
ap2.add_argument("--ls", metavar="U[,V[,F]]", 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="scan all volumes; arguments USER,VOL,FLAGS; example [**,*,ln,p,r]")
|
||||||
@@ -314,6 +378,7 @@ def run_argparse(argv, formatter):
|
|||||||
ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails")
|
ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails")
|
||||||
ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails")
|
ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails")
|
||||||
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res")
|
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res")
|
||||||
|
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=0, help="max num cpu cores to use, 0=all")
|
||||||
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image")
|
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image")
|
||||||
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
|
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
|
||||||
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
|
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
|
||||||
@@ -330,7 +395,7 @@ def run_argparse(argv, formatter):
|
|||||||
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", action="store_true", help="disable hashing during e2ds folder scans")
|
ap2.add_argument("--no-hash", action="store_true", help="disable hashing during e2ds folder scans")
|
||||||
ap2.add_argument("--re-int", metavar="SEC", type=int, default=30, help="disk rescan check interval")
|
ap2.add_argument("--re-int", metavar="SEC", type=int, default=30, help="disk rescan check interval")
|
||||||
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="disk rescan volume interval (0=off)")
|
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")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('metadata db options')
|
ap2 = ap.add_argument_group('metadata db options')
|
||||||
@@ -357,10 +422,22 @@ def run_argparse(argv, formatter):
|
|||||||
ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead")
|
ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead")
|
||||||
ap2.add_argument("--stackmon", metavar="P,S", type=u, help="write stacktrace to Path every S second")
|
ap2.add_argument("--stackmon", metavar="P,S", type=u, help="write stacktrace to Path every S second")
|
||||||
ap2.add_argument("--log-thrs", metavar="SEC", type=float, help="list active threads every SEC")
|
ap2.add_argument("--log-thrs", metavar="SEC", type=float, help="list active threads every SEC")
|
||||||
|
|
||||||
return ap.parse_args(args=argv[1:])
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
ap2 = ap.add_argument_group("help sections")
|
||||||
|
for k, h, _ in sects:
|
||||||
|
ap2.add_argument("--help-" + k, action="store_true", help=h)
|
||||||
|
|
||||||
|
ret = ap.parse_args(args=argv[1:])
|
||||||
|
for k, h, t in sects:
|
||||||
|
k2 = "help_" + k.replace("-", "_")
|
||||||
|
if vars(ret)[k2]:
|
||||||
|
lprint("# {} help page".format(k))
|
||||||
|
lprint(t + "\033[0m")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
def main(argv=None):
|
||||||
time.strptime("19970815", "%Y%m%d") # python#7980
|
time.strptime("19970815", "%Y%m%d") # python#7980
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
VERSION = (0, 12, 12)
|
VERSION = (0, 13, 11)
|
||||||
CODENAME = "fil\033[33med"
|
CODENAME = "future-proof"
|
||||||
BUILD_DT = (2021, 8, 6)
|
BUILD_DT = (2021, 8, 30)
|
||||||
|
|
||||||
S_VERSION = ".".join(map(str, VERSION))
|
S_VERSION = ".".join(map(str, VERSION))
|
||||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||||
|
|||||||
@@ -5,15 +5,29 @@ import re
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import stat
|
import stat
|
||||||
|
import time
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import threading
|
import threading
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from .__init__ import WINDOWS
|
from .__init__ import WINDOWS
|
||||||
from .util import IMPLICATIONS, uncyg, undot, absreal, Pebkac, fsdec, fsenc, statdir
|
from .util import (
|
||||||
|
IMPLICATIONS,
|
||||||
|
uncyg,
|
||||||
|
undot,
|
||||||
|
unhumanize,
|
||||||
|
absreal,
|
||||||
|
Pebkac,
|
||||||
|
fsenc,
|
||||||
|
statdir,
|
||||||
|
)
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
|
LEELOO_DALLAS = "leeloo_dallas"
|
||||||
|
|
||||||
|
|
||||||
class AXS(object):
|
class AXS(object):
|
||||||
def __init__(self, uread=None, uwrite=None, umove=None, udel=None):
|
def __init__(self, uread=None, uwrite=None, umove=None, udel=None):
|
||||||
self.uread = {} if uread is None else {k: 1 for k in uread}
|
self.uread = {} if uread is None else {k: 1 for k in uread}
|
||||||
@@ -30,6 +44,156 @@ class AXS(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Lim(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.nups = {} # num tracker
|
||||||
|
self.bups = {} # byte tracker list
|
||||||
|
self.bupc = {} # byte tracker cache
|
||||||
|
|
||||||
|
self.nosub = False # disallow subdirectories
|
||||||
|
|
||||||
|
self.smin = None # filesize min
|
||||||
|
self.smax = None # filesize max
|
||||||
|
|
||||||
|
self.bwin = None # bytes window
|
||||||
|
self.bmax = None # bytes max
|
||||||
|
self.nwin = None # num window
|
||||||
|
self.nmax = None # num max
|
||||||
|
|
||||||
|
self.rotn = None # rot num files
|
||||||
|
self.rotl = None # rot depth
|
||||||
|
self.rotf = None # rot datefmt
|
||||||
|
self.rot_re = None # rotf check
|
||||||
|
|
||||||
|
def set_rotf(self, fmt):
|
||||||
|
self.rotf = fmt
|
||||||
|
r = re.escape(fmt).replace("%Y", "[0-9]{4}").replace("%j", "[0-9]{3}")
|
||||||
|
r = re.sub("%[mdHMSWU]", "[0-9]{2}", r)
|
||||||
|
self.rot_re = re.compile("(^|/)" + r + "$")
|
||||||
|
|
||||||
|
def all(self, ip, rem, sz, abspath):
|
||||||
|
self.chk_nup(ip)
|
||||||
|
self.chk_bup(ip)
|
||||||
|
self.chk_rem(rem)
|
||||||
|
if sz != -1:
|
||||||
|
self.chk_sz(sz)
|
||||||
|
|
||||||
|
ap2, vp2 = self.rot(abspath)
|
||||||
|
if abspath == ap2:
|
||||||
|
return ap2, rem
|
||||||
|
|
||||||
|
return ap2, ("{}/{}".format(rem, vp2) if rem else vp2)
|
||||||
|
|
||||||
|
def chk_sz(self, sz):
|
||||||
|
if self.smin is not None and sz < self.smin:
|
||||||
|
raise Pebkac(400, "file too small")
|
||||||
|
|
||||||
|
if self.smax is not None and sz > self.smax:
|
||||||
|
raise Pebkac(400, "file too big")
|
||||||
|
|
||||||
|
def chk_rem(self, rem):
|
||||||
|
if self.nosub and rem:
|
||||||
|
raise Pebkac(500, "no subdirectories allowed")
|
||||||
|
|
||||||
|
def rot(self, path):
|
||||||
|
if not self.rotf and not self.rotn:
|
||||||
|
return path, ""
|
||||||
|
|
||||||
|
if self.rotf:
|
||||||
|
path = path.rstrip("/\\")
|
||||||
|
if self.rot_re.search(path.replace("\\", "/")):
|
||||||
|
return path, ""
|
||||||
|
|
||||||
|
suf = datetime.utcnow().strftime(self.rotf)
|
||||||
|
if path:
|
||||||
|
path += "/"
|
||||||
|
|
||||||
|
return path + suf, suf
|
||||||
|
|
||||||
|
ret = self.dive(path, self.rotl)
|
||||||
|
if not ret:
|
||||||
|
raise Pebkac(500, "no available slots in volume")
|
||||||
|
|
||||||
|
d = ret[len(path) :].strip("/\\").replace("\\", "/")
|
||||||
|
return ret, d
|
||||||
|
|
||||||
|
def dive(self, path, lvs):
|
||||||
|
items = bos.listdir(path)
|
||||||
|
|
||||||
|
if not lvs:
|
||||||
|
# at leaf level
|
||||||
|
return None if len(items) >= self.rotn else ""
|
||||||
|
|
||||||
|
dirs = [int(x) for x in items if x and all(y in "1234567890" for y in x)]
|
||||||
|
dirs.sort()
|
||||||
|
|
||||||
|
if not dirs:
|
||||||
|
# no branches yet; make one
|
||||||
|
sub = os.path.join(path, "0")
|
||||||
|
bos.mkdir(sub)
|
||||||
|
else:
|
||||||
|
# try newest branch only
|
||||||
|
sub = os.path.join(path, str(dirs[-1]))
|
||||||
|
|
||||||
|
ret = self.dive(sub, lvs - 1)
|
||||||
|
if ret is not None:
|
||||||
|
return os.path.join(sub, ret)
|
||||||
|
|
||||||
|
if len(dirs) >= self.rotn:
|
||||||
|
# full branch or root
|
||||||
|
return None
|
||||||
|
|
||||||
|
# make a branch
|
||||||
|
sub = os.path.join(path, str(dirs[-1] + 1))
|
||||||
|
bos.mkdir(sub)
|
||||||
|
ret = self.dive(sub, lvs - 1)
|
||||||
|
if ret is None:
|
||||||
|
raise Pebkac(500, "rotation bug")
|
||||||
|
|
||||||
|
return os.path.join(sub, ret)
|
||||||
|
|
||||||
|
def nup(self, ip):
|
||||||
|
try:
|
||||||
|
self.nups[ip].append(time.time())
|
||||||
|
except:
|
||||||
|
self.nups[ip] = [time.time()]
|
||||||
|
|
||||||
|
def bup(self, ip, nbytes):
|
||||||
|
v = [time.time(), nbytes]
|
||||||
|
try:
|
||||||
|
self.bups[ip].append(v)
|
||||||
|
self.bupc[ip] += nbytes
|
||||||
|
except:
|
||||||
|
self.bups[ip] = [v]
|
||||||
|
self.bupc[ip] = nbytes
|
||||||
|
|
||||||
|
def chk_nup(self, ip):
|
||||||
|
if not self.nmax or ip not in self.nups:
|
||||||
|
return
|
||||||
|
|
||||||
|
nups = self.nups[ip]
|
||||||
|
cutoff = time.time() - self.nwin
|
||||||
|
while nups and nups[0] < cutoff:
|
||||||
|
nups.pop(0)
|
||||||
|
|
||||||
|
if len(nups) >= self.nmax:
|
||||||
|
raise Pebkac(429, "too many uploads")
|
||||||
|
|
||||||
|
def chk_bup(self, ip):
|
||||||
|
if not self.bmax or ip not in self.bups:
|
||||||
|
return
|
||||||
|
|
||||||
|
bups = self.bups[ip]
|
||||||
|
cutoff = time.time() - self.bwin
|
||||||
|
mark = self.bupc[ip]
|
||||||
|
while bups and bups[0][0] < cutoff:
|
||||||
|
mark -= bups.pop(0)[1]
|
||||||
|
|
||||||
|
self.bupc[ip] = mark
|
||||||
|
if mark >= self.bmax:
|
||||||
|
raise Pebkac(429, "ingress saturated")
|
||||||
|
|
||||||
|
|
||||||
class VFS(object):
|
class VFS(object):
|
||||||
"""single level in the virtual fs"""
|
"""single level in the virtual fs"""
|
||||||
|
|
||||||
@@ -42,6 +206,7 @@ class VFS(object):
|
|||||||
self.nodes = {} # child nodes
|
self.nodes = {} # child nodes
|
||||||
self.histtab = None # all realpath->histpath
|
self.histtab = None # all realpath->histpath
|
||||||
self.dbv = None # closest full/non-jump parent
|
self.dbv = None # closest full/non-jump parent
|
||||||
|
self.lim = None # type: Lim # upload limits; only set for dbv
|
||||||
|
|
||||||
if realpath:
|
if realpath:
|
||||||
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
|
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
|
||||||
@@ -165,13 +330,14 @@ class VFS(object):
|
|||||||
[will_move, c.umove, "move"],
|
[will_move, c.umove, "move"],
|
||||||
[will_del, c.udel, "delete"],
|
[will_del, c.udel, "delete"],
|
||||||
]:
|
]:
|
||||||
if req and (uname not in d and "*" not in d):
|
if req and (uname not in d and "*" not in d) and uname != LEELOO_DALLAS:
|
||||||
m = "you don't have {}-access for this location"
|
m = "you don't have {}-access for this location"
|
||||||
raise Pebkac(403, m.format(msg))
|
raise Pebkac(403, m.format(msg))
|
||||||
|
|
||||||
return vn, rem
|
return vn, rem
|
||||||
|
|
||||||
def get_dbv(self, vrem):
|
def get_dbv(self, vrem):
|
||||||
|
# type: (str) -> tuple[VFS, str]
|
||||||
dbv = self.dbv
|
dbv = self.dbv
|
||||||
if not dbv:
|
if not dbv:
|
||||||
return self, vrem
|
return self, vrem
|
||||||
@@ -391,6 +557,9 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
def _read_vol_str(self, lvl, uname, axs, flags):
|
def _read_vol_str(self, lvl, uname, axs, flags):
|
||||||
# type: (str, str, AXS, any) -> None
|
# type: (str, str, AXS, any) -> None
|
||||||
|
if lvl.strip("crwmd"):
|
||||||
|
raise Exception("invalid volume flag: {},{}".format(lvl, uname))
|
||||||
|
|
||||||
if lvl == "c":
|
if lvl == "c":
|
||||||
cval = True
|
cval = True
|
||||||
if "=" in uname:
|
if "=" in uname:
|
||||||
@@ -546,6 +715,9 @@ class AuthSrv(object):
|
|||||||
)
|
)
|
||||||
raise Exception("invalid config")
|
raise Exception("invalid config")
|
||||||
|
|
||||||
|
if LEELOO_DALLAS in all_users:
|
||||||
|
raise Exception("sorry, reserved username: " + LEELOO_DALLAS)
|
||||||
|
|
||||||
promote = []
|
promote = []
|
||||||
demote = []
|
demote = []
|
||||||
for vol in vfs.all_vols.values():
|
for vol in vfs.all_vols.values():
|
||||||
@@ -604,6 +776,51 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
vfs.histtab = {v.realpath: v.histpath for v in vfs.all_vols.values()}
|
vfs.histtab = {v.realpath: v.histpath for v in vfs.all_vols.values()}
|
||||||
|
|
||||||
|
for vol in vfs.all_vols.values():
|
||||||
|
lim = Lim()
|
||||||
|
use = False
|
||||||
|
|
||||||
|
if vol.flags.get("nosub"):
|
||||||
|
use = True
|
||||||
|
lim.nosub = True
|
||||||
|
|
||||||
|
v = vol.flags.get("sz")
|
||||||
|
if v:
|
||||||
|
use = True
|
||||||
|
lim.smin, lim.smax = [unhumanize(x) for x in v.split("-")]
|
||||||
|
|
||||||
|
v = vol.flags.get("rotn")
|
||||||
|
if v:
|
||||||
|
use = True
|
||||||
|
lim.rotn, lim.rotl = [int(x) for x in v.split(",")]
|
||||||
|
|
||||||
|
v = vol.flags.get("rotf")
|
||||||
|
if v:
|
||||||
|
use = True
|
||||||
|
lim.set_rotf(v)
|
||||||
|
|
||||||
|
v = vol.flags.get("maxn")
|
||||||
|
if v:
|
||||||
|
use = True
|
||||||
|
lim.nmax, lim.nwin = [int(x) for x in v.split(",")]
|
||||||
|
|
||||||
|
v = vol.flags.get("maxb")
|
||||||
|
if v:
|
||||||
|
use = True
|
||||||
|
lim.bmax, lim.bwin = [unhumanize(x) for x in v.split(",")]
|
||||||
|
|
||||||
|
if use:
|
||||||
|
vol.lim = lim
|
||||||
|
|
||||||
|
for vol in vfs.all_vols.values():
|
||||||
|
if "pk" in vol.flags and "gz" not in vol.flags and "xz" not in vol.flags:
|
||||||
|
vol.flags["gz"] = False # def.pk
|
||||||
|
|
||||||
|
if "scan" in vol.flags:
|
||||||
|
vol.flags["scan"] = int(vol.flags["scan"])
|
||||||
|
elif self.args.re_maxage:
|
||||||
|
vol.flags["scan"] = self.args.re_maxage
|
||||||
|
|
||||||
all_mte = {}
|
all_mte = {}
|
||||||
errors = False
|
errors = False
|
||||||
for vol in vfs.all_vols.values():
|
for vol in vfs.all_vols.values():
|
||||||
|
|||||||
@@ -13,10 +13,15 @@ import ctypes
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import calendar
|
import calendar
|
||||||
|
|
||||||
|
try:
|
||||||
|
import lzma
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
from .__init__ import E, PY2, WINDOWS, ANYWIN, unicode
|
from .__init__ import E, PY2, WINDOWS, ANYWIN, unicode
|
||||||
from .util import * # noqa # pylint: disable=unused-wildcard-import
|
from .util import * # noqa # pylint: disable=unused-wildcard-import
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
from .authsrv import AuthSrv
|
from .authsrv import AuthSrv, Lim
|
||||||
from .szip import StreamZip
|
from .szip import StreamZip
|
||||||
from .star import StreamTar
|
from .star import StreamTar
|
||||||
|
|
||||||
@@ -107,7 +112,7 @@ class HttpCli(object):
|
|||||||
self.http_ver = "HTTP/1.1"
|
self.http_ver = "HTTP/1.1"
|
||||||
# self.log("pebkac at httpcli.run #1: " + repr(ex))
|
# self.log("pebkac at httpcli.run #1: " + repr(ex))
|
||||||
self.keepalive = False
|
self.keepalive = False
|
||||||
self.loud_reply(unicode(ex), status=ex.code)
|
self.loud_reply(unicode(ex), status=ex.code, volsan=True)
|
||||||
return self.keepalive
|
return self.keepalive
|
||||||
|
|
||||||
# time.sleep(0.4)
|
# time.sleep(0.4)
|
||||||
@@ -180,6 +185,9 @@ class HttpCli(object):
|
|||||||
if kc in cookies and ku not in uparam:
|
if kc in cookies and ku not in uparam:
|
||||||
uparam[ku] = cookies[kc]
|
uparam[ku] = cookies[kc]
|
||||||
|
|
||||||
|
if len(uparam) > 10 or len(cookies) > 50:
|
||||||
|
raise Pebkac(400, "u wot m8")
|
||||||
|
|
||||||
self.uparam = uparam
|
self.uparam = uparam
|
||||||
self.cookies = cookies
|
self.cookies = cookies
|
||||||
self.vpath = unquotep(vpath) # not query, so + means +
|
self.vpath = unquotep(vpath) # not query, so + means +
|
||||||
@@ -216,19 +224,24 @@ class HttpCli(object):
|
|||||||
else:
|
else:
|
||||||
raise Pebkac(400, 'invalid HTTP mode "{0}"'.format(self.mode))
|
raise Pebkac(400, 'invalid HTTP mode "{0}"'.format(self.mode))
|
||||||
|
|
||||||
except Pebkac as ex:
|
except Exception as ex:
|
||||||
|
pex = ex
|
||||||
|
if not hasattr(ex, "code"):
|
||||||
|
pex = Pebkac(500)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# self.log("pebkac at httpcli.run #2: " + repr(ex))
|
|
||||||
post = self.mode in ["POST", "PUT"] or "content-length" in self.headers
|
post = self.mode in ["POST", "PUT"] or "content-length" in self.headers
|
||||||
if not self._check_nonfatal(ex, post):
|
if not self._check_nonfatal(pex, post):
|
||||||
self.keepalive = False
|
self.keepalive = False
|
||||||
|
|
||||||
self.log("{}\033[0m, {}".format(str(ex), self.vpath), 3)
|
msg = str(ex) if pex == ex else min_ex()
|
||||||
|
self.log("{}\033[0m, {}".format(msg, self.vpath), 3)
|
||||||
|
|
||||||
msg = "<pre>{}\r\nURL: {}\r\n".format(str(ex), self.vpath)
|
msg = "<pre>{}\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)
|
||||||
|
|
||||||
self.reply(msg.encode("utf-8", "replace"), status=ex.code)
|
self.reply(msg.encode("utf-8", "replace"), status=pex.code, volsan=True)
|
||||||
return self.keepalive
|
return self.keepalive
|
||||||
except Pebkac:
|
except Pebkac:
|
||||||
return False
|
return False
|
||||||
@@ -261,8 +274,12 @@ class HttpCli(object):
|
|||||||
except:
|
except:
|
||||||
raise Pebkac(400, "client d/c while replying headers")
|
raise Pebkac(400, "client d/c while replying headers")
|
||||||
|
|
||||||
def reply(self, body, status=200, mime=None, headers=None):
|
def reply(self, body, status=200, mime=None, headers=None, volsan=False):
|
||||||
# TODO something to reply with user-supplied values safely
|
# TODO something to reply with user-supplied values safely
|
||||||
|
|
||||||
|
if volsan:
|
||||||
|
body = vol_san(self.asrv.vfs.all_vols.values(), body)
|
||||||
|
|
||||||
self.send_headers(len(body), status, mime, headers)
|
self.send_headers(len(body), status, mime, headers)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -491,7 +508,11 @@ class HttpCli(object):
|
|||||||
def dump_to_file(self):
|
def dump_to_file(self):
|
||||||
reader, remains = self.get_body_reader()
|
reader, remains = self.get_body_reader()
|
||||||
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||||
|
lim = vfs.get_dbv(rem)[0].lim
|
||||||
fdir = os.path.join(vfs.realpath, rem)
|
fdir = os.path.join(vfs.realpath, rem)
|
||||||
|
if lim:
|
||||||
|
fdir, rem = lim.all(self.ip, rem, remains, fdir)
|
||||||
|
bos.makedirs(fdir)
|
||||||
|
|
||||||
addr = self.ip.replace(":", ".")
|
addr = self.ip.replace(":", ".")
|
||||||
fn = "put-{:.6f}-{}.bin".format(time.time(), addr)
|
fn = "put-{:.6f}-{}.bin".format(time.time(), addr)
|
||||||
@@ -499,9 +520,70 @@ class HttpCli(object):
|
|||||||
if self.args.nw:
|
if self.args.nw:
|
||||||
path = os.devnull
|
path = os.devnull
|
||||||
|
|
||||||
with open(fsenc(path), "wb", 512 * 1024) as f:
|
open_f = open
|
||||||
|
open_a = [fsenc(path), "wb", 512 * 1024]
|
||||||
|
open_ka = {}
|
||||||
|
|
||||||
|
# user-request || config-force
|
||||||
|
if ("gz" in vfs.flags or "xz" in vfs.flags) and (
|
||||||
|
"pk" in vfs.flags
|
||||||
|
or "pk" in self.uparam
|
||||||
|
or "gz" in self.uparam
|
||||||
|
or "xz" in self.uparam
|
||||||
|
):
|
||||||
|
fb = {"gz": 9, "xz": 0} # default/fallback level
|
||||||
|
lv = {} # selected level
|
||||||
|
alg = None # selected algo (gz=preferred)
|
||||||
|
|
||||||
|
# user-prefs first
|
||||||
|
if "gz" in self.uparam or "pk" in self.uparam: # def.pk
|
||||||
|
alg = "gz"
|
||||||
|
if "xz" in self.uparam:
|
||||||
|
alg = "xz"
|
||||||
|
if alg:
|
||||||
|
v = self.uparam.get(alg)
|
||||||
|
lv[alg] = fb[alg] if v is None else int(v)
|
||||||
|
|
||||||
|
if alg not in vfs.flags:
|
||||||
|
alg = "gz" if "gz" in vfs.flags else "xz"
|
||||||
|
|
||||||
|
# then server overrides
|
||||||
|
pk = vfs.flags.get("pk")
|
||||||
|
if pk is not None:
|
||||||
|
# config-forced on
|
||||||
|
alg = alg or "gz" # def.pk
|
||||||
|
try:
|
||||||
|
# config-forced opts
|
||||||
|
alg, lv = pk.split(",")
|
||||||
|
lv[alg] = int(lv)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
lv[alg] = lv.get(alg) or fb.get(alg)
|
||||||
|
|
||||||
|
self.log("compressing with {} level {}".format(alg, lv.get(alg)))
|
||||||
|
if alg == "gz":
|
||||||
|
open_f = gzip.GzipFile
|
||||||
|
open_a = [fsenc(path), "wb", lv[alg], None, 0x5FEE6600] # 2021-01-01
|
||||||
|
elif alg == "xz":
|
||||||
|
open_f = lzma.open
|
||||||
|
open_a = [fsenc(path), "wb"]
|
||||||
|
open_ka = {"preset": lv[alg]}
|
||||||
|
else:
|
||||||
|
self.log("fallthrough? thats a bug", 1)
|
||||||
|
|
||||||
|
with open_f(*open_a, **open_ka) as f:
|
||||||
post_sz, _, sha_b64 = hashcopy(reader, f)
|
post_sz, _, sha_b64 = hashcopy(reader, f)
|
||||||
|
|
||||||
|
if lim:
|
||||||
|
lim.nup(self.ip)
|
||||||
|
lim.bup(self.ip, post_sz)
|
||||||
|
try:
|
||||||
|
lim.chk_sz(post_sz)
|
||||||
|
except:
|
||||||
|
bos.unlink(path)
|
||||||
|
raise
|
||||||
|
|
||||||
if not self.args.nw:
|
if not self.args.nw:
|
||||||
vfs, vrem = vfs.get_dbv(rem)
|
vfs, vrem = vfs.get_dbv(rem)
|
||||||
self.conn.hsrv.broker.put(
|
self.conn.hsrv.broker.put(
|
||||||
@@ -583,7 +665,7 @@ class HttpCli(object):
|
|||||||
try:
|
try:
|
||||||
remains = int(self.headers["content-length"])
|
remains = int(self.headers["content-length"])
|
||||||
except:
|
except:
|
||||||
raise Pebkac(400, "you must supply a content-length for JSON POST")
|
raise Pebkac(411)
|
||||||
|
|
||||||
if remains > 1024 * 1024:
|
if remains > 1024 * 1024:
|
||||||
raise Pebkac(413, "json 2big")
|
raise Pebkac(413, "json 2big")
|
||||||
@@ -614,12 +696,9 @@ class HttpCli(object):
|
|||||||
if self.vpath.endswith(k):
|
if self.vpath.endswith(k):
|
||||||
self.vpath = self.vpath[: -len(k)]
|
self.vpath = self.vpath[: -len(k)]
|
||||||
|
|
||||||
sub = None
|
|
||||||
name = undot(body["name"])
|
name = undot(body["name"])
|
||||||
if "/" in name:
|
if "/" in name:
|
||||||
sub, name = name.rsplit("/", 1)
|
raise Pebkac(400, "your client is old; press CTRL-SHIFT-R and try again")
|
||||||
self.vpath = "/".join([self.vpath, sub]).strip("/")
|
|
||||||
body["name"] = name
|
|
||||||
|
|
||||||
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||||
dbv, vrem = vfs.get_dbv(rem)
|
dbv, vrem = vfs.get_dbv(rem)
|
||||||
@@ -630,7 +709,7 @@ class HttpCli(object):
|
|||||||
body["addr"] = self.ip
|
body["addr"] = self.ip
|
||||||
body["vcfg"] = dbv.flags
|
body["vcfg"] = dbv.flags
|
||||||
|
|
||||||
if sub:
|
if rem:
|
||||||
try:
|
try:
|
||||||
dst = os.path.join(vfs.realpath, rem)
|
dst = os.path.join(vfs.realpath, rem)
|
||||||
if not bos.path.isdir(dst):
|
if not bos.path.isdir(dst):
|
||||||
@@ -650,9 +729,6 @@ class HttpCli(object):
|
|||||||
|
|
||||||
x = self.conn.hsrv.broker.put(True, "up2k.handle_json", body)
|
x = self.conn.hsrv.broker.put(True, "up2k.handle_json", body)
|
||||||
ret = x.get()
|
ret = x.get()
|
||||||
if sub:
|
|
||||||
ret["name"] = "/".join([sub, ret["name"]])
|
|
||||||
|
|
||||||
ret = json.dumps(ret)
|
ret = json.dumps(ret)
|
||||||
self.log(ret)
|
self.log(ret)
|
||||||
self.reply(ret.encode("utf-8"), mime="application/json")
|
self.reply(ret.encode("utf-8"), mime="application/json")
|
||||||
@@ -797,7 +873,7 @@ class HttpCli(object):
|
|||||||
self.parser.drop()
|
self.parser.drop()
|
||||||
|
|
||||||
ck, msg = self.get_pwd_cookie(pwd)
|
ck, msg = self.get_pwd_cookie(pwd)
|
||||||
html = self.j2("msg", h1=msg, h2='<a href="/">ack</a>', redir="/")
|
html = self.j2("msg", h1=msg, h2='<a href="/?h">ack</a>', redir="/?h")
|
||||||
self.reply(html.encode("utf-8"), headers={"Set-Cookie": ck})
|
self.reply(html.encode("utf-8"), headers={"Set-Cookie": ck})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -880,6 +956,15 @@ class HttpCli(object):
|
|||||||
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||||
self._assert_safe_rem(rem)
|
self._assert_safe_rem(rem)
|
||||||
|
|
||||||
|
upload_vpath = self.vpath
|
||||||
|
lim = vfs.get_dbv(rem)[0].lim
|
||||||
|
fdir_base = os.path.join(vfs.realpath, rem)
|
||||||
|
if lim:
|
||||||
|
fdir_base, rem = lim.all(self.ip, rem, -1, fdir_base)
|
||||||
|
upload_vpath = "{}/{}".format(vfs.vpath, rem).strip("/")
|
||||||
|
if not nullwrite:
|
||||||
|
bos.makedirs(fdir_base)
|
||||||
|
|
||||||
files = []
|
files = []
|
||||||
errmsg = ""
|
errmsg = ""
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
@@ -889,12 +974,9 @@ class HttpCli(object):
|
|||||||
self.log("discarding incoming file without filename")
|
self.log("discarding incoming file without filename")
|
||||||
# fallthrough
|
# fallthrough
|
||||||
|
|
||||||
|
fdir = fdir_base
|
||||||
|
fname = sanitize_fn(p_file, "", [".prologue.html", ".epilogue.html"])
|
||||||
if p_file and not nullwrite:
|
if p_file and not nullwrite:
|
||||||
fdir = os.path.join(vfs.realpath, rem)
|
|
||||||
fname = sanitize_fn(
|
|
||||||
p_file, "", [".prologue.html", ".epilogue.html"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not bos.path.isdir(fdir):
|
if not bos.path.isdir(fdir):
|
||||||
raise Pebkac(404, "that folder does not exist")
|
raise Pebkac(404, "that folder does not exist")
|
||||||
|
|
||||||
@@ -905,14 +987,28 @@ class HttpCli(object):
|
|||||||
fname = os.devnull
|
fname = os.devnull
|
||||||
fdir = ""
|
fdir = ""
|
||||||
|
|
||||||
|
if lim:
|
||||||
|
lim.chk_bup(self.ip)
|
||||||
|
lim.chk_nup(self.ip)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with ren_open(fname, "wb", 512 * 1024, **open_args) as f:
|
with ren_open(fname, "wb", 512 * 1024, **open_args) as f:
|
||||||
f, fname = f["orz"]
|
f, fname = f["orz"]
|
||||||
self.log("writing to {}/{}".format(fdir, fname))
|
abspath = os.path.join(fdir, fname)
|
||||||
|
self.log("writing to {}".format(abspath))
|
||||||
sz, sha512_hex, _ = hashcopy(p_data, f)
|
sz, sha512_hex, _ = hashcopy(p_data, f)
|
||||||
if sz == 0:
|
if sz == 0:
|
||||||
raise Pebkac(400, "empty files in post")
|
raise Pebkac(400, "empty files in post")
|
||||||
|
|
||||||
|
if lim:
|
||||||
|
lim.nup(self.ip)
|
||||||
|
lim.bup(self.ip, sz)
|
||||||
|
try:
|
||||||
|
lim.chk_sz(sz)
|
||||||
|
except:
|
||||||
|
bos.unlink(abspath)
|
||||||
|
raise
|
||||||
|
|
||||||
files.append([sz, sha512_hex, p_file, fname])
|
files.append([sz, sha512_hex, p_file, fname])
|
||||||
dbv, vrem = vfs.get_dbv(rem)
|
dbv, vrem = vfs.get_dbv(rem)
|
||||||
self.conn.hsrv.broker.put(
|
self.conn.hsrv.broker.put(
|
||||||
@@ -944,7 +1040,9 @@ class HttpCli(object):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
except Pebkac as ex:
|
except Pebkac as ex:
|
||||||
errmsg = unicode(ex)
|
errmsg = vol_san(
|
||||||
|
self.asrv.vfs.all_vols.values(), unicode(ex).encode("utf-8")
|
||||||
|
).decode("utf-8")
|
||||||
|
|
||||||
td = max(0.1, time.time() - t0)
|
td = max(0.1, time.time() - t0)
|
||||||
sz_total = sum(x[0] for x in files)
|
sz_total = sum(x[0] for x in files)
|
||||||
@@ -964,7 +1062,7 @@ class HttpCli(object):
|
|||||||
errmsg = "ERROR: " + errmsg
|
errmsg = "ERROR: " + errmsg
|
||||||
|
|
||||||
for sz, sha512, ofn, lfn in files:
|
for sz, sha512, ofn, lfn in files:
|
||||||
vpath = (self.vpath + "/" if self.vpath else "") + lfn
|
vpath = "{}/{}".format(upload_vpath, lfn).strip("/")
|
||||||
msg += 'sha512: {} // {} bytes // <a href="/{}">{}</a>\n'.format(
|
msg += 'sha512: {} // {} bytes // <a href="/{}">{}</a>\n'.format(
|
||||||
sha512[:56], sz, quotep(vpath), html_escape(ofn, crlf=True)
|
sha512[:56], sz, quotep(vpath), html_escape(ofn, crlf=True)
|
||||||
)
|
)
|
||||||
@@ -1023,6 +1121,20 @@ class HttpCli(object):
|
|||||||
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||||
self._assert_safe_rem(rem)
|
self._assert_safe_rem(rem)
|
||||||
|
|
||||||
|
clen = int(self.headers.get("content-length", -1))
|
||||||
|
if clen == -1:
|
||||||
|
raise Pebkac(411)
|
||||||
|
|
||||||
|
rp, fn = vsplit(rem)
|
||||||
|
fp = os.path.join(vfs.realpath, rp)
|
||||||
|
lim = vfs.get_dbv(rem)[0].lim
|
||||||
|
if lim:
|
||||||
|
fp, rp = lim.all(self.ip, rp, clen, fp)
|
||||||
|
bos.makedirs(fp)
|
||||||
|
|
||||||
|
fp = os.path.join(fp, fn)
|
||||||
|
rem = "{}/{}".format(rp, fn).strip("/")
|
||||||
|
|
||||||
if not rem.endswith(".md"):
|
if not rem.endswith(".md"):
|
||||||
raise Pebkac(400, "only markdown pls")
|
raise Pebkac(400, "only markdown pls")
|
||||||
|
|
||||||
@@ -1034,7 +1146,6 @@ class HttpCli(object):
|
|||||||
self.reply(response.encode("utf-8"))
|
self.reply(response.encode("utf-8"))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
fp = os.path.join(vfs.realpath, rem)
|
|
||||||
srv_lastmod = srv_lastmod3 = -1
|
srv_lastmod = srv_lastmod3 = -1
|
||||||
try:
|
try:
|
||||||
st = bos.stat(fp)
|
st = bos.stat(fp)
|
||||||
@@ -1088,6 +1199,15 @@ class HttpCli(object):
|
|||||||
with open(fsenc(fp), "wb", 512 * 1024) as f:
|
with open(fsenc(fp), "wb", 512 * 1024) as f:
|
||||||
sz, sha512, _ = hashcopy(p_data, f)
|
sz, sha512, _ = hashcopy(p_data, f)
|
||||||
|
|
||||||
|
if lim:
|
||||||
|
lim.nup(self.ip)
|
||||||
|
lim.bup(self.ip, sz)
|
||||||
|
try:
|
||||||
|
lim.chk_sz(sz)
|
||||||
|
except:
|
||||||
|
bos.unlink(fp)
|
||||||
|
raise
|
||||||
|
|
||||||
new_lastmod = bos.stat(fp).st_mtime
|
new_lastmod = bos.stat(fp).st_mtime
|
||||||
new_lastmod3 = int(new_lastmod * 1000)
|
new_lastmod3 = int(new_lastmod * 1000)
|
||||||
sha512 = sha512[:56]
|
sha512 = sha512[:56]
|
||||||
@@ -1579,7 +1699,7 @@ class HttpCli(object):
|
|||||||
|
|
||||||
q = "select sz, rd, fn, at from up where ip=? and at>?"
|
q = "select sz, rd, fn, at from up where ip=? and at>?"
|
||||||
for sz, rd, fn, at in cur.execute(q, (self.ip, lim)):
|
for sz, rd, fn, at in cur.execute(q, (self.ip, lim)):
|
||||||
vp = "/" + "/".join([rd, fn]).strip("/")
|
vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x)
|
||||||
if filt and filt not in vp:
|
if filt and filt not in vp:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -1662,7 +1782,7 @@ class HttpCli(object):
|
|||||||
for fn in self.args.th_covers.split(","):
|
for fn in self.args.th_covers.split(","):
|
||||||
fp = os.path.join(abspath, fn)
|
fp = os.path.join(abspath, fn)
|
||||||
if bos.path.exists(fp):
|
if bos.path.exists(fp):
|
||||||
vrem = "{}/{}".format(vrem.rstrip("/"), fn)
|
vrem = "{}/{}".format(vrem.rstrip("/"), fn).strip("/")
|
||||||
is_dir = False
|
is_dir = False
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ def errdesc(errors):
|
|||||||
tf_path = tf.name
|
tf_path = tf.name
|
||||||
tf.write("\r\n".join(report).encode("utf-8", "replace"))
|
tf.write("\r\n".join(report).encode("utf-8", "replace"))
|
||||||
|
|
||||||
dt = datetime.utcfromtimestamp(time.time())
|
dt = datetime.utcnow().strftime("%Y-%m%d-%H%M%S")
|
||||||
dt = dt.strftime("%Y-%m%d-%H%M%S")
|
|
||||||
|
|
||||||
bos.chmod(tf_path, 0o444)
|
bos.chmod(tf_path, 0o444)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from datetime import datetime, timedelta
|
|||||||
import calendar
|
import calendar
|
||||||
|
|
||||||
from .__init__ import E, PY2, WINDOWS, ANYWIN, MACOS, VT100, unicode
|
from .__init__ import E, PY2, WINDOWS, ANYWIN, MACOS, VT100, unicode
|
||||||
from .util import mp, start_log_thrs, start_stackmon, min_ex
|
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
|
||||||
@@ -41,7 +41,6 @@ class SvcHub(object):
|
|||||||
self.stop_cond = threading.Condition()
|
self.stop_cond = threading.Condition()
|
||||||
self.httpsrv_up = 0
|
self.httpsrv_up = 0
|
||||||
|
|
||||||
self.ansi_re = re.compile("\033\\[[^m]*m")
|
|
||||||
self.log_mutex = threading.Lock()
|
self.log_mutex = threading.Lock()
|
||||||
self.next_day = 0
|
self.next_day = 0
|
||||||
|
|
||||||
@@ -111,7 +110,7 @@ class SvcHub(object):
|
|||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
def _logname(self):
|
def _logname(self):
|
||||||
dt = datetime.utcfromtimestamp(time.time())
|
dt = datetime.utcnow()
|
||||||
fn = self.args.lo
|
fn = self.args.lo
|
||||||
for fs in "YmdHMS":
|
for fs in "YmdHMS":
|
||||||
fs = "%" + fs
|
fs = "%" + fs
|
||||||
@@ -244,8 +243,7 @@ class SvcHub(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
with self.log_mutex:
|
with self.log_mutex:
|
||||||
ts = datetime.utcfromtimestamp(time.time())
|
ts = datetime.utcnow().strftime("%Y-%m%d-%H%M%S.%f")[:-3]
|
||||||
ts = ts.strftime("%Y-%m%d-%H%M%S.%f")[:-3]
|
|
||||||
self.logf.write("@{} [{}] {}\n".format(ts, src, msg))
|
self.logf.write("@{} [{}] {}\n".format(ts, src, msg))
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
@@ -257,7 +255,7 @@ class SvcHub(object):
|
|||||||
self.logf.close()
|
self.logf.close()
|
||||||
self._setup_logfile("")
|
self._setup_logfile("")
|
||||||
|
|
||||||
dt = datetime.utcfromtimestamp(time.time())
|
dt = datetime.utcnow()
|
||||||
|
|
||||||
# unix timestamp of next 00:00:00 (leap-seconds safe)
|
# unix timestamp of next 00:00:00 (leap-seconds safe)
|
||||||
day_now = dt.day
|
day_now = dt.day
|
||||||
@@ -280,9 +278,9 @@ class SvcHub(object):
|
|||||||
if not VT100:
|
if not VT100:
|
||||||
fmt = "{} {:21} {}\n"
|
fmt = "{} {:21} {}\n"
|
||||||
if "\033" in msg:
|
if "\033" in msg:
|
||||||
msg = self.ansi_re.sub("", msg)
|
msg = ansi_re.sub("", msg)
|
||||||
if "\033" in src:
|
if "\033" in src:
|
||||||
src = self.ansi_re.sub("", src)
|
src = ansi_re.sub("", src)
|
||||||
elif c:
|
elif c:
|
||||||
if isinstance(c, int):
|
if isinstance(c, int):
|
||||||
msg = "\033[3{}m{}".format(c, msg)
|
msg = "\033[3{}m{}".format(c, msg)
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ class TcpSrv(object):
|
|||||||
eps = self.ips_linux()
|
eps = self.ips_linux()
|
||||||
|
|
||||||
if "0.0.0.0" not in listen_ips:
|
if "0.0.0.0" not in listen_ips:
|
||||||
eps = {k: v for k, v in eps if k in listen_ips}
|
eps = {k: v for k, v in eps.items() if k in listen_ips}
|
||||||
|
|
||||||
default_route = None
|
default_route = None
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ HAVE_AVIF = False
|
|||||||
HAVE_WEBP = False
|
HAVE_WEBP = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image, ImageOps
|
from PIL import Image, ImageOps, ExifTags
|
||||||
|
|
||||||
HAVE_PIL = True
|
HAVE_PIL = True
|
||||||
try:
|
try:
|
||||||
@@ -105,7 +105,10 @@ class ThumbSrv(object):
|
|||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
self.busy = {}
|
self.busy = {}
|
||||||
self.stopping = False
|
self.stopping = False
|
||||||
|
self.nthr = self.args.th_mt
|
||||||
|
if not self.nthr:
|
||||||
self.nthr = os.cpu_count() if hasattr(os, "cpu_count") else 4
|
self.nthr = os.cpu_count() if hasattr(os, "cpu_count") else 4
|
||||||
|
|
||||||
self.q = Queue(self.nthr * 4)
|
self.q = Queue(self.nthr * 4)
|
||||||
for n in range(self.nthr):
|
for n in range(self.nthr):
|
||||||
t = threading.Thread(
|
t = threading.Thread(
|
||||||
@@ -221,21 +224,38 @@ class ThumbSrv(object):
|
|||||||
with self.mutex:
|
with self.mutex:
|
||||||
self.nthr -= 1
|
self.nthr -= 1
|
||||||
|
|
||||||
def conv_pil(self, abspath, tpath):
|
def fancy_pillow(self, im):
|
||||||
with Image.open(fsenc(abspath)) as im:
|
# exif_transpose is expensive (loads full image + unconditional copy)
|
||||||
crop = not self.args.th_no_crop
|
r = max(*self.res) * 2
|
||||||
res2 = self.res
|
im.thumbnail((r, r), resample=Image.LANCZOS)
|
||||||
if crop:
|
|
||||||
res2 = (res2[0] * 2, res2[1] * 2)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
im.thumbnail(res2, resample=Image.LANCZOS)
|
k = next(k for k, v in ExifTags.TAGS.items() if v == "Orientation")
|
||||||
if crop:
|
exif = im.getexif()
|
||||||
|
rot = int(exif[k])
|
||||||
|
del exif[k]
|
||||||
|
except:
|
||||||
|
rot = 1
|
||||||
|
|
||||||
|
rots = {8: Image.ROTATE_90, 3: Image.ROTATE_180, 6: Image.ROTATE_270}
|
||||||
|
if rot in rots:
|
||||||
|
im = im.transpose(rots[rot])
|
||||||
|
|
||||||
|
if self.args.th_no_crop:
|
||||||
|
im.thumbnail(self.res, resample=Image.LANCZOS)
|
||||||
|
else:
|
||||||
iw, ih = im.size
|
iw, ih = im.size
|
||||||
dw, dh = self.res
|
dw, dh = self.res
|
||||||
res = (min(iw, dw), min(ih, dh))
|
res = (min(iw, dw), min(ih, dh))
|
||||||
im = ImageOps.fit(im, res, method=Image.LANCZOS)
|
im = ImageOps.fit(im, res, method=Image.LANCZOS)
|
||||||
except:
|
|
||||||
|
return im
|
||||||
|
|
||||||
|
def conv_pil(self, abspath, tpath):
|
||||||
|
with Image.open(fsenc(abspath)) as im:
|
||||||
|
try:
|
||||||
|
im = self.fancy_pillow(im)
|
||||||
|
except Exception as ex:
|
||||||
|
self.log("fancy_pillow {}".format(ex), "1;30")
|
||||||
im.thumbnail(self.res)
|
im.thumbnail(self.res)
|
||||||
|
|
||||||
fmts = ["RGB", "L"]
|
fmts = ["RGB", "L"]
|
||||||
@@ -289,6 +309,7 @@ class ThumbSrv(object):
|
|||||||
b"-map", b"0:v:0",
|
b"-map", b"0:v:0",
|
||||||
b"-vf", scale,
|
b"-vf", scale,
|
||||||
b"-frames:v", b"1",
|
b"-frames:v", b"1",
|
||||||
|
b"-metadata:s:v:0", b"rotate=0",
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
@@ -306,6 +327,7 @@ class ThumbSrv(object):
|
|||||||
]
|
]
|
||||||
|
|
||||||
cmd += [fsenc(tpath)]
|
cmd += [fsenc(tpath)]
|
||||||
|
# self.log((b" ".join(cmd)).decode("utf-8"))
|
||||||
|
|
||||||
ret, sout, serr = runcmd(cmd)
|
ret, sout, serr = runcmd(cmd)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class U2idx(object):
|
|||||||
is_date = False
|
is_date = False
|
||||||
kw_key = ["(", ")", "and ", "or ", "not "]
|
kw_key = ["(", ")", "and ", "or ", "not "]
|
||||||
kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "]
|
kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "]
|
||||||
ptn_mt = re.compile(r"^\.?[a-z]+$")
|
ptn_mt = re.compile(r"^\.?[a-z_-]+$")
|
||||||
mt_ctr = 0
|
mt_ctr = 0
|
||||||
mt_keycmp = "substr(up.w,1,16)"
|
mt_keycmp = "substr(up.w,1,16)"
|
||||||
mt_keycmp2 = None
|
mt_keycmp2 = None
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from .util import (
|
|||||||
min_ex,
|
min_ex,
|
||||||
)
|
)
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
from .authsrv import AuthSrv
|
from .authsrv import AuthSrv, LEELOO_DALLAS
|
||||||
from .mtag import MTag, MParser
|
from .mtag import MTag, MParser
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -176,27 +176,26 @@ class Up2k(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _sched_rescan(self):
|
def _sched_rescan(self):
|
||||||
maxage = self.args.re_maxage
|
|
||||||
volage = {}
|
volage = {}
|
||||||
while True:
|
while True:
|
||||||
time.sleep(self.args.re_int)
|
time.sleep(self.args.re_int)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
vpaths = list(sorted(self.asrv.vfs.all_vols.keys()))
|
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
if maxage:
|
for vp, vol in sorted(self.asrv.vfs.all_vols.items()):
|
||||||
for vp in vpaths:
|
maxage = vol.flags.get("scan")
|
||||||
|
if not maxage:
|
||||||
|
continue
|
||||||
|
|
||||||
if vp not in volage:
|
if vp not in volage:
|
||||||
volage[vp] = now
|
volage[vp] = now
|
||||||
|
|
||||||
if now - volage[vp] >= maxage:
|
if now - volage[vp] >= maxage:
|
||||||
self.need_rescan[vp] = 1
|
self.need_rescan[vp] = 1
|
||||||
|
|
||||||
if not self.need_rescan:
|
|
||||||
continue
|
|
||||||
|
|
||||||
vols = list(sorted(self.need_rescan.keys()))
|
vols = list(sorted(self.need_rescan.keys()))
|
||||||
self.need_rescan = {}
|
self.need_rescan = {}
|
||||||
|
|
||||||
|
if vols:
|
||||||
err = self.rescan(self.asrv.vfs.all_vols, vols)
|
err = self.rescan(self.asrv.vfs.all_vols, vols)
|
||||||
if err:
|
if err:
|
||||||
for v in vols:
|
for v in vols:
|
||||||
@@ -207,6 +206,42 @@ class Up2k(object):
|
|||||||
for v in vols:
|
for v in vols:
|
||||||
volage[v] = now
|
volage[v] = now
|
||||||
|
|
||||||
|
if self.args.no_lifetime:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for vp, vol in sorted(self.asrv.vfs.all_vols.items()):
|
||||||
|
lifetime = vol.flags.get("lifetime")
|
||||||
|
if not lifetime:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cur = self.cur.get(vol.realpath)
|
||||||
|
if not cur:
|
||||||
|
continue
|
||||||
|
|
||||||
|
nrm = 0
|
||||||
|
deadline = time.time() - int(lifetime)
|
||||||
|
q = "select rd, fn from up where at > 0 and at < ? limit 100"
|
||||||
|
while True:
|
||||||
|
with self.mutex:
|
||||||
|
hits = cur.execute(q, (deadline,)).fetchall()
|
||||||
|
|
||||||
|
if not hits:
|
||||||
|
break
|
||||||
|
|
||||||
|
for rd, fn in hits:
|
||||||
|
if rd.startswith("//") or fn.startswith("//"):
|
||||||
|
rd, fn = s3dec(rd, fn)
|
||||||
|
|
||||||
|
fvp = "{}/{}".format(rd, fn).strip("/")
|
||||||
|
if vp:
|
||||||
|
fvp = "{}/{}".format(vp, fvp)
|
||||||
|
|
||||||
|
self._handle_rm(LEELOO_DALLAS, None, fvp)
|
||||||
|
nrm += 1
|
||||||
|
|
||||||
|
if nrm:
|
||||||
|
self.log("{} files graduated in {}".format(nrm, vp))
|
||||||
|
|
||||||
def _vis_job_progress(self, job):
|
def _vis_job_progress(self, job):
|
||||||
perc = 100 - (len(job["need"]) * 100.0 / len(job["hash"]))
|
perc = 100 - (len(job["need"]) * 100.0 / len(job["hash"]))
|
||||||
path = os.path.join(job["ptop"], job["prel"], job["name"])
|
path = os.path.join(job["ptop"], job["prel"], job["name"])
|
||||||
@@ -1148,6 +1183,16 @@ class Up2k(object):
|
|||||||
cur.connection.commit()
|
cur.connection.commit()
|
||||||
|
|
||||||
if not job:
|
if not job:
|
||||||
|
vfs = self.asrv.vfs.all_vols[cj["vtop"]]
|
||||||
|
if vfs.lim:
|
||||||
|
ap1 = os.path.join(cj["ptop"], cj["prel"])
|
||||||
|
ap2, cj["prel"] = vfs.lim.all(
|
||||||
|
cj["addr"], cj["prel"], cj["size"], ap1
|
||||||
|
)
|
||||||
|
bos.makedirs(ap2)
|
||||||
|
vfs.lim.nup(cj["addr"])
|
||||||
|
vfs.lim.bup(cj["addr"], cj["size"])
|
||||||
|
|
||||||
job = {
|
job = {
|
||||||
"wark": wark,
|
"wark": wark,
|
||||||
"t0": now,
|
"t0": now,
|
||||||
@@ -1178,8 +1223,12 @@ class Up2k(object):
|
|||||||
|
|
||||||
self._new_upload(job)
|
self._new_upload(job)
|
||||||
|
|
||||||
|
purl = "{}/{}".format(job["vtop"], job["prel"]).strip("/")
|
||||||
|
purl = "/{}/".format(purl) if purl else "/"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"name": job["name"],
|
"name": job["name"],
|
||||||
|
"purl": purl,
|
||||||
"size": job["size"],
|
"size": job["size"],
|
||||||
"lmod": job["lmod"],
|
"lmod": job["lmod"],
|
||||||
"hash": job["need"],
|
"hash": job["need"],
|
||||||
@@ -1230,7 +1279,7 @@ class Up2k(object):
|
|||||||
hops = len(ndst[nc:]) - 1
|
hops = len(ndst[nc:]) - 1
|
||||||
lsrc = "../" * hops + "/".join(lsrc)
|
lsrc = "../" * hops + "/".join(lsrc)
|
||||||
os.symlink(fsenc(lsrc), fsenc(ldst))
|
os.symlink(fsenc(lsrc), fsenc(ldst))
|
||||||
except (AttributeError, OSError) as ex:
|
except Exception as ex:
|
||||||
self.log("cannot symlink; creating copy: " + repr(ex))
|
self.log("cannot symlink; creating copy: " + repr(ex))
|
||||||
shutil.copy2(fsenc(src), fsenc(dst))
|
shutil.copy2(fsenc(src), fsenc(dst))
|
||||||
|
|
||||||
@@ -1379,7 +1428,11 @@ class Up2k(object):
|
|||||||
ptop = vn.realpath
|
ptop = vn.realpath
|
||||||
atop = vn.canonical(rem, False)
|
atop = vn.canonical(rem, False)
|
||||||
adir, fn = os.path.split(atop)
|
adir, fn = os.path.split(atop)
|
||||||
|
try:
|
||||||
st = bos.lstat(atop)
|
st = bos.lstat(atop)
|
||||||
|
except:
|
||||||
|
raise Pebkac(400, "file not found on disk (already deleted?)")
|
||||||
|
|
||||||
scandir = not self.args.no_scandir
|
scandir = not self.args.no_scandir
|
||||||
if stat.S_ISLNK(st.st_mode) or stat.S_ISREG(st.st_mode):
|
if stat.S_ISLNK(st.st_mode) or stat.S_ISREG(st.st_mode):
|
||||||
dbv, vrem = self.asrv.vfs.get(vpath, uname, *permsets[0])
|
dbv, vrem = self.asrv.vfs.get(vpath, uname, *permsets[0])
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import subprocess as sp # nosec
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
|
||||||
from .__init__ import PY2, WINDOWS, ANYWIN
|
from .__init__ import PY2, WINDOWS, ANYWIN, VT100
|
||||||
from .stolen import surrogateescape
|
from .stolen import surrogateescape
|
||||||
|
|
||||||
FAKE_MP = False
|
FAKE_MP = False
|
||||||
@@ -58,6 +58,9 @@ except:
|
|||||||
return struct.unpack(f.decode("ascii"), *a, **ka)
|
return struct.unpack(f.decode("ascii"), *a, **ka)
|
||||||
|
|
||||||
|
|
||||||
|
ansi_re = re.compile("\033\\[[^mK]*[mK]")
|
||||||
|
|
||||||
|
|
||||||
surrogateescape.register_surrogateescape()
|
surrogateescape.register_surrogateescape()
|
||||||
FS_ENCODING = sys.getfilesystemencoding()
|
FS_ENCODING = sys.getfilesystemencoding()
|
||||||
if WINDOWS and PY2:
|
if WINDOWS and PY2:
|
||||||
@@ -77,6 +80,7 @@ HTTPCODE = {
|
|||||||
403: "Forbidden",
|
403: "Forbidden",
|
||||||
404: "Not Found",
|
404: "Not Found",
|
||||||
405: "Method Not Allowed",
|
405: "Method Not Allowed",
|
||||||
|
411: "Length Required",
|
||||||
413: "Payload Too Large",
|
413: "Payload Too Large",
|
||||||
416: "Requested Range Not Satisfiable",
|
416: "Requested Range Not Satisfiable",
|
||||||
422: "Unprocessable Entity",
|
422: "Unprocessable Entity",
|
||||||
@@ -203,17 +207,22 @@ class ProgressPrinter(threading.Thread):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
msg = None
|
msg = None
|
||||||
|
fmt = " {}\033[K\r" if VT100 else " {} $\r"
|
||||||
while not self.end:
|
while not self.end:
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
if msg == self.msg or self.end:
|
if msg == self.msg or self.end:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
msg = self.msg
|
msg = self.msg
|
||||||
uprint(" {}\033[K\r".format(msg))
|
uprint(fmt.format(msg))
|
||||||
if PY2:
|
if PY2:
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
if VT100:
|
||||||
print("\033[K", end="")
|
print("\033[K", end="")
|
||||||
|
elif msg:
|
||||||
|
print("------------------------")
|
||||||
|
|
||||||
sys.stdout.flush() # necessary on win10 even w/ stderr btw
|
sys.stdout.flush() # necessary on win10 even w/ stderr btw
|
||||||
|
|
||||||
|
|
||||||
@@ -340,6 +349,13 @@ def log_thrs(log, ival, name):
|
|||||||
log(name, "\033[0m \033[33m".join(tv), 3)
|
log(name, "\033[0m \033[33m".join(tv), 3)
|
||||||
|
|
||||||
|
|
||||||
|
def vol_san(vols, txt):
|
||||||
|
for vol in vols:
|
||||||
|
txt = txt.replace(vol.realpath.encode("utf-8"), vol.vpath.encode("utf-8"))
|
||||||
|
|
||||||
|
return txt
|
||||||
|
|
||||||
|
|
||||||
def min_ex():
|
def min_ex():
|
||||||
et, ev, tb = sys.exc_info()
|
et, ev, tb = sys.exc_info()
|
||||||
tb = traceback.extract_tb(tb)
|
tb = traceback.extract_tb(tb)
|
||||||
@@ -684,6 +700,17 @@ def humansize(sz, terse=False):
|
|||||||
return ret.replace("iB", "").replace(" ", "")
|
return ret.replace("iB", "").replace(" ", "")
|
||||||
|
|
||||||
|
|
||||||
|
def unhumanize(sz):
|
||||||
|
try:
|
||||||
|
return float(sz)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
mul = sz[-1:].lower()
|
||||||
|
mul = {"k": 1024, "m": 1024 * 1024, "g": 1024 * 1024 * 1024}.get(mul, 1)
|
||||||
|
return float(sz[:-1]) * mul
|
||||||
|
|
||||||
|
|
||||||
def get_spd(nbyte, t0, t=None):
|
def get_spd(nbyte, t0, t=None):
|
||||||
if t is None:
|
if t is None:
|
||||||
t = time.time()
|
t = time.time()
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ window.baguetteBox = (function () {
|
|||||||
afterHide: null,
|
afterHide: null,
|
||||||
onChange: null,
|
onChange: null,
|
||||||
},
|
},
|
||||||
overlay, slider, btnPrev, btnNext, btnHelp, btnVmode, btnClose,
|
overlay, slider, btnPrev, btnNext, btnHelp, btnRotL, btnRotR, btnSel, btnVmode, btnClose,
|
||||||
currentGallery = [],
|
currentGallery = [],
|
||||||
currentIndex = 0,
|
currentIndex = 0,
|
||||||
isOverlayVisible = false,
|
isOverlayVisible = false,
|
||||||
@@ -49,7 +49,7 @@ window.baguetteBox = (function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var touchstartHandler = function (e) {
|
var touchstartHandler = function (e) {
|
||||||
touch.count++;
|
touch.count = e.touches.length;
|
||||||
if (touch.count > 1)
|
if (touch.count > 1)
|
||||||
touch.multitouch = true;
|
touch.multitouch = true;
|
||||||
|
|
||||||
@@ -72,8 +72,11 @@ window.baguetteBox = (function () {
|
|||||||
hideOverlay();
|
hideOverlay();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var touchendHandler = function () {
|
var touchendHandler = function (e) {
|
||||||
touch.count--;
|
touch.count--;
|
||||||
|
if (e && e.touches)
|
||||||
|
touch.count = e.touches.length;
|
||||||
|
|
||||||
if (touch.count <= 0)
|
if (touch.count <= 0)
|
||||||
touch.multitouch = false;
|
touch.multitouch = false;
|
||||||
|
|
||||||
@@ -175,6 +178,9 @@ window.baguetteBox = (function () {
|
|||||||
'<button id="bbox-next" class="bbox-btn" type="button" aria-label="Next">></button>' +
|
'<button id="bbox-next" class="bbox-btn" type="button" aria-label="Next">></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-rotl" type="button">↶</button>' +
|
||||||
|
'<button id="bbox-rotr" type="button">↷</button>' +
|
||||||
|
'<button id="bbox-tsel" type="button">sel</button>' +
|
||||||
'<button id="bbox-vmode" type="button" tt="a"></button>' +
|
'<button id="bbox-vmode" type="button" tt="a"></button>' +
|
||||||
'<button id="bbox-close" type="button" aria-label="Close">X</button>' +
|
'<button id="bbox-close" type="button" aria-label="Close">X</button>' +
|
||||||
'</div></div>'
|
'</div></div>'
|
||||||
@@ -187,6 +193,9 @@ 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');
|
||||||
|
btnRotL = ebi('bbox-rotl');
|
||||||
|
btnRotR = ebi('bbox-rotr');
|
||||||
|
btnSel = ebi('bbox-tsel');
|
||||||
btnVmode = ebi('bbox-vmode');
|
btnVmode = ebi('bbox-vmode');
|
||||||
btnClose = ebi('bbox-close');
|
btnClose = ebi('bbox-close');
|
||||||
bindEvents();
|
bindEvents();
|
||||||
@@ -203,11 +212,13 @@ window.baguetteBox = (function () {
|
|||||||
['right, L', 'next file'],
|
['right, L', 'next file'],
|
||||||
['home', 'first file'],
|
['home', 'first file'],
|
||||||
['end', 'last file'],
|
['end', 'last file'],
|
||||||
|
['R', 'rotate (shift=ccw)'],
|
||||||
|
['S', 'toggle file selection'],
|
||||||
['space, P, K', 'video: play / pause'],
|
['space, P, K', 'video: play / pause'],
|
||||||
['U', 'video: seek 10sec back'],
|
['U', 'video: seek 10sec back'],
|
||||||
['P', 'video: seek 10sec ahead'],
|
['P', 'video: seek 10sec ahead'],
|
||||||
['M', 'video: toggle mute'],
|
['M', 'video: toggle mute'],
|
||||||
['R', 'video: toggle loop'],
|
['V', 'video: toggle loop'],
|
||||||
['C', 'video: toggle auto-next'],
|
['C', 'video: toggle auto-next'],
|
||||||
['F', 'video: toggle fullscreen'],
|
['F', 'video: toggle fullscreen'],
|
||||||
],
|
],
|
||||||
@@ -249,7 +260,7 @@ window.baguetteBox = (function () {
|
|||||||
v.muted = vmute = !vmute;
|
v.muted = vmute = !vmute;
|
||||||
mp_ctl();
|
mp_ctl();
|
||||||
}
|
}
|
||||||
else if (k == "KeyR" && v) {
|
else if (k == "KeyV" && v) {
|
||||||
vloop = !vloop;
|
vloop = !vloop;
|
||||||
vnext = vnext && !vloop;
|
vnext = vnext && !vloop;
|
||||||
setVmode();
|
setVmode();
|
||||||
@@ -267,6 +278,10 @@ window.baguetteBox = (function () {
|
|||||||
v.requestFullscreen();
|
v.requestFullscreen();
|
||||||
}
|
}
|
||||||
catch (ex) { }
|
catch (ex) { }
|
||||||
|
else if (k == "KeyS")
|
||||||
|
tglsel();
|
||||||
|
else if (k == "KeyR")
|
||||||
|
rotn(e.shiftKey ? -1 : 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVmode() {
|
function setVmode() {
|
||||||
@@ -279,7 +294,7 @@ window.baguetteBox = (function () {
|
|||||||
if (vloop) {
|
if (vloop) {
|
||||||
lbl = 'Loop';
|
lbl = 'Loop';
|
||||||
msg += 'repeat it';
|
msg += 'repeat it';
|
||||||
tts = '$NHotkey: R';
|
tts = '$NHotkey: V';
|
||||||
}
|
}
|
||||||
else if (vnext) {
|
else if (vnext) {
|
||||||
lbl = 'Cont';
|
lbl = 'Cont';
|
||||||
@@ -314,6 +329,40 @@ window.baguetteBox = (function () {
|
|||||||
tt.show.bind(this)();
|
tt.show.bind(this)();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tglsel() {
|
||||||
|
var thumb = currentGallery[currentIndex].imageElement,
|
||||||
|
name = vsplit(thumb.href)[1],
|
||||||
|
files = msel.getall();
|
||||||
|
|
||||||
|
for (var a = 0; a < files.length; a++)
|
||||||
|
if (vsplit(files[a].vp)[1] == name)
|
||||||
|
clmod(ebi(files[a].id).closest('tr'), 'sel', 't');
|
||||||
|
|
||||||
|
msel.selui();
|
||||||
|
selbg();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selbg() {
|
||||||
|
var img = vidimg(),
|
||||||
|
thumb = currentGallery[currentIndex].imageElement,
|
||||||
|
name = vsplit(thumb.href)[1],
|
||||||
|
files = msel.getsel(),
|
||||||
|
sel = false;
|
||||||
|
|
||||||
|
for (var a = 0; a < files.length; a++)
|
||||||
|
if (vsplit(files[a].vp)[1] == name)
|
||||||
|
sel = true;
|
||||||
|
|
||||||
|
ebi('bbox-overlay').style.background = sel ?
|
||||||
|
'rgba(153,34,85,0.7)' : '';
|
||||||
|
|
||||||
|
img.style.borderRadius = sel ? '1em' : '';
|
||||||
|
btnSel.style.color = sel ? '#fff' : '';
|
||||||
|
btnSel.style.background = sel ? '#d48' : '';
|
||||||
|
btnSel.style.textShadow = sel ? '1px 1px 0 #b38' : '';
|
||||||
|
btnSel.style.boxShadow = sel ? '.15em .15em 0 #502' : '';
|
||||||
|
}
|
||||||
|
|
||||||
function keyUpHandler(e) {
|
function keyUpHandler(e) {
|
||||||
if (e.ctrlKey || e.altKey || e.metaKey || e.isComposing)
|
if (e.ctrlKey || e.altKey || e.metaKey || e.isComposing)
|
||||||
return;
|
return;
|
||||||
@@ -348,6 +397,9 @@ 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(btnRotL, 'click', rotl);
|
||||||
|
bind(btnRotR, 'click', rotr);
|
||||||
|
bind(btnSel, 'click', tglsel);
|
||||||
bind(slider, 'contextmenu', contextmenuHandler);
|
bind(slider, 'contextmenu', contextmenuHandler);
|
||||||
bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||||
bind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
bind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||||
@@ -362,11 +414,15 @@ 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(btnRotL, 'click', rotl);
|
||||||
|
unbind(btnRotR, 'click', rotr);
|
||||||
|
unbind(btnSel, 'click', tglsel);
|
||||||
unbind(slider, 'contextmenu', contextmenuHandler);
|
unbind(slider, 'contextmenu', contextmenuHandler);
|
||||||
unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||||
unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||||
unbind(overlay, 'touchend', touchendHandler);
|
unbind(overlay, 'touchend', touchendHandler);
|
||||||
unbind(document, 'focus', trapFocusInsideOverlay, true);
|
unbind(document, 'focus', trapFocusInsideOverlay, true);
|
||||||
|
timer.rm(rotn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareOverlay(gallery, userOptions) {
|
function prepareOverlay(gallery, userOptions) {
|
||||||
@@ -617,10 +673,91 @@ window.baguetteBox = (function () {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var prev_cw = 0, prev_ch = 0, unrot_timer = null;
|
||||||
|
function rotn(n) {
|
||||||
|
var el = vidimg(),
|
||||||
|
orot = parseInt(el.getAttribute('rot') || 0),
|
||||||
|
frot = orot + (n || 0) * 90;
|
||||||
|
|
||||||
|
if (!frot && !orot)
|
||||||
|
return; // reflow noop
|
||||||
|
|
||||||
|
var co = ebi('bbox-overlay'),
|
||||||
|
cw = co.clientWidth,
|
||||||
|
ch = co.clientHeight;
|
||||||
|
|
||||||
|
if (!n && prev_cw === cw && prev_ch === ch)
|
||||||
|
return; // reflow noop
|
||||||
|
|
||||||
|
prev_cw = cw;
|
||||||
|
prev_ch = ch;
|
||||||
|
var rot = frot,
|
||||||
|
iw = el.naturalWidth || el.videoWidth,
|
||||||
|
ih = el.naturalHeight || el.videoHeight,
|
||||||
|
magic = 4, // idk, works in enough browsers
|
||||||
|
dl = el.closest('div').querySelector('figcaption a'),
|
||||||
|
vw = cw,
|
||||||
|
vh = ch - dl.offsetHeight + magic,
|
||||||
|
pmag = Math.min(1, Math.min(vw / ih, vh / iw)),
|
||||||
|
wmag = Math.min(1, Math.min(vw / iw, vh / ih));
|
||||||
|
|
||||||
|
while (rot < 0) rot += 360;
|
||||||
|
while (rot >= 360) rot -= 360;
|
||||||
|
var q = rot == 90 || rot == 270 ? 1 : 0,
|
||||||
|
mag = q ? pmag : wmag;
|
||||||
|
|
||||||
|
el.style.cssText = 'max-width:none; max-height:none; position:absolute; display:block; margin:0';
|
||||||
|
if (!orot) {
|
||||||
|
el.style.width = iw * wmag + 'px';
|
||||||
|
el.style.height = ih * wmag + 'px';
|
||||||
|
el.style.left = (vw - iw * wmag) / 2 + 'px';
|
||||||
|
el.style.top = (vh - ih * wmag) / 2 - magic + 'px';
|
||||||
|
q = el.offsetHeight;
|
||||||
|
}
|
||||||
|
el.style.width = iw * mag + 'px';
|
||||||
|
el.style.height = ih * mag + 'px';
|
||||||
|
el.style.left = (vw - iw * mag) / 2 + 'px';
|
||||||
|
el.style.top = (vh - ih * mag) / 2 - magic + 'px';
|
||||||
|
el.style.transform = 'rotate(' + frot + 'deg)';
|
||||||
|
el.setAttribute('rot', frot);
|
||||||
|
timer.add(rotn);
|
||||||
|
if (!rot) {
|
||||||
|
clearTimeout(unrot_timer);
|
||||||
|
unrot_timer = setTimeout(unrot, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function rotl() {
|
||||||
|
rotn(-1);
|
||||||
|
}
|
||||||
|
function rotr() {
|
||||||
|
rotn(1);
|
||||||
|
}
|
||||||
|
function unrot() {
|
||||||
|
var el = vidimg(),
|
||||||
|
orot = el.getAttribute('rot'),
|
||||||
|
rot = parseInt(orot || 0);
|
||||||
|
|
||||||
|
while (rot < 0) rot += 360;
|
||||||
|
while (rot >= 360) rot -= 360;
|
||||||
|
if (rot || orot === null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
clmod(el, 'nt', 1);
|
||||||
|
el.removeAttribute('rot');
|
||||||
|
el.removeAttribute("style");
|
||||||
|
rot = el.offsetHeight;
|
||||||
|
clmod(el, 'nt');
|
||||||
|
timer.rm(rotn);
|
||||||
|
}
|
||||||
|
|
||||||
function vid() {
|
function vid() {
|
||||||
return imagesElements[currentIndex].querySelector('video');
|
return imagesElements[currentIndex].querySelector('video');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function vidimg() {
|
||||||
|
return imagesElements[currentIndex].querySelector('img, video');
|
||||||
|
}
|
||||||
|
|
||||||
function playvid(play) {
|
function playvid(play) {
|
||||||
if (vid())
|
if (vid())
|
||||||
vid()[play ? 'play' : 'pause']();
|
vid()[play ? 'play' : 'pause']();
|
||||||
@@ -662,15 +799,21 @@ window.baguetteBox = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateOffset() {
|
function updateOffset() {
|
||||||
var offset = -currentIndex * 100 + '%';
|
var offset = -currentIndex * 100 + '%',
|
||||||
|
xform = slider.style.perspective !== undefined;
|
||||||
|
|
||||||
if (options.animation === 'fadeIn') {
|
if (options.animation === 'fadeIn') {
|
||||||
slider.style.opacity = 0;
|
slider.style.opacity = 0;
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
slider.style.transform = 'translate3d(' + offset + ',0,0)';
|
xform ?
|
||||||
|
slider.style.transform = 'translate3d(' + offset + ',0,0)' :
|
||||||
|
slider.style.left = offset;
|
||||||
slider.style.opacity = 1;
|
slider.style.opacity = 1;
|
||||||
}, 400);
|
}, 400);
|
||||||
} else {
|
} else {
|
||||||
slider.style.transform = 'translate3d(' + offset + ',0,0)';
|
xform ?
|
||||||
|
slider.style.transform = 'translate3d(' + offset + ',0,0)' :
|
||||||
|
slider.style.left = offset;
|
||||||
}
|
}
|
||||||
playvid(false);
|
playvid(false);
|
||||||
var v = vid();
|
var v = vid();
|
||||||
@@ -679,8 +822,21 @@ window.baguetteBox = (function () {
|
|||||||
v.muted = vmute;
|
v.muted = vmute;
|
||||||
v.loop = vloop;
|
v.loop = vloop;
|
||||||
}
|
}
|
||||||
|
selbg();
|
||||||
mp_ctl();
|
mp_ctl();
|
||||||
setVmode();
|
setVmode();
|
||||||
|
|
||||||
|
var el = vidimg();
|
||||||
|
if (el.getAttribute('rot'))
|
||||||
|
timer.add(rotn);
|
||||||
|
else
|
||||||
|
timer.rm(rotn);
|
||||||
|
|
||||||
|
var prev = QS('.full-image.vis');
|
||||||
|
if (prev)
|
||||||
|
clmod(prev, 'vis');
|
||||||
|
|
||||||
|
clmod(el.closest('div'), 'vis', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function preloadNext(index) {
|
function preloadNext(index) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,10 @@
|
|||||||
<title>⇆🎉 {{ title }}</title>
|
<title>⇆🎉 {{ title }}</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/browser.css?_={{ ts }}">
|
<link rel="stylesheet" media="screen" href="/.cpr/ui.css?_={{ ts }}">
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/upload.css?_={{ ts }}">
|
<link rel="stylesheet" media="screen" href="/.cpr/browser.css?_={{ ts }}">
|
||||||
{%- if css %}
|
{%- if css %}
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="{{ css }}?_={{ ts }}">
|
<link rel="stylesheet" media="screen" href="{{ css }}?_={{ ts }}">
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -114,6 +114,8 @@
|
|||||||
|
|
||||||
<h2><a href="/?h">control-panel</a></h2>
|
<h2><a href="/?h">control-panel</a></h2>
|
||||||
|
|
||||||
|
<a href="#" id="repl">π</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{%- if srv_info %}
|
{%- if srv_info %}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ ebi('widget').innerHTML = (
|
|||||||
'<div id="wtoggle">' +
|
'<div id="wtoggle">' +
|
||||||
'<span id="wfm"><a' +
|
'<span id="wfm"><a' +
|
||||||
' href="#" id="fren" tt="rename selected item$NHotkey: F2">✎<span>name</span></a><a' +
|
' href="#" id="fren" tt="rename selected item$NHotkey: F2">✎<span>name</span></a><a' +
|
||||||
' href="#" id="fdel" tt="delete selected items$NHotkey: ctrl-K">⌫<span>delete</span></a><a' +
|
' href="#" id="fdel" tt="delete selected items$NHotkey: ctrl-K">⌫<span>del.</span></a><a' +
|
||||||
' href="#" id="fcut" tt="cut selected items <small>(then paste somewhere else)</small>$NHotkey: ctrl-X">✂<span>cut</span></a><a' +
|
' href="#" id="fcut" tt="cut selected items <small>(then paste somewhere else)</small>$NHotkey: ctrl-X">✂<span>cut</span></a><a' +
|
||||||
' href="#" id="fpst" tt="paste a previously cut/copied selection$NHotkey: ctrl-V">📋<span>paste</span></a>' +
|
' href="#" id="fpst" tt="paste a previously cut/copied selection$NHotkey: ctrl-V">📋<span>paste</span></a>' +
|
||||||
'</span><span id="wzip"><a' +
|
'</span><span id="wzip"><a' +
|
||||||
@@ -40,10 +40,10 @@ ebi('widget').innerHTML = (
|
|||||||
' href="#" id="selinv" tt="invert selection">sel.<br />inv.</a><a' +
|
' href="#" id="selinv" tt="invert selection">sel.<br />inv.</a><a' +
|
||||||
' href="#" id="selzip" tt="download selection as archive">zip</a>' +
|
' href="#" id="selzip" tt="download selection as archive">zip</a>' +
|
||||||
'</span><span id="wnp"><a' +
|
'</span><span id="wnp"><a' +
|
||||||
' href="#" id="npirc" tt="copy irc-formatted track info">📋irc</a><a' +
|
' href="#" id="npirc" tt="copy irc-formatted track info">📋<span>irc</span></a><a' +
|
||||||
' href="#" id="nptxt" tt="copy plaintext track info">📋txt</a>' +
|
' href="#" id="nptxt" tt="copy plaintext track info">📋<span>txt</span></a>' +
|
||||||
'</span><a' +
|
'</span><a' +
|
||||||
' href="#" id="wtgrid" tt="toggle grid/list view">田</a><a' +
|
' href="#" id="wtgrid" tt="toggle grid/list view$NHotkey: G">田</a><a' +
|
||||||
' href="#" id="wtico">♫</a>' +
|
' href="#" id="wtico">♫</a>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div id="widgeti">' +
|
'<div id="widgeti">' +
|
||||||
@@ -102,6 +102,12 @@ ebi('op_up2k').innerHTML = (
|
|||||||
' </div>\n' +
|
' </div>\n' +
|
||||||
'</div>\n' +
|
'</div>\n' +
|
||||||
|
|
||||||
|
'<div id="u2etaw"><div id="u2etas"><div class="o">\n' +
|
||||||
|
' hash: <span id="u2etah" tt="average <em>hashing</em> speed, and estimated time until finish">(no uploads are queued yet)</span><br />\n' +
|
||||||
|
' send: <span id="u2etau" tt="average <em>upload</em> speed and estimated time until finish">(no uploads are queued yet)</span><br />\n' +
|
||||||
|
' </div><span class="o">done: </span><span id="u2etat" tt="average <em>total</em> speed and estimated time until finish">(no uploads are queued yet)</span>\n' +
|
||||||
|
'</div></div>\n' +
|
||||||
|
|
||||||
'<div id="u2cards">\n' +
|
'<div id="u2cards">\n' +
|
||||||
' <a href="#" act="ok" tt="completed successfully">ok <span>0</span></a><a\n' +
|
' <a href="#" act="ok" tt="completed successfully">ok <span>0</span></a><a\n' +
|
||||||
' href="#" act="ng" tt="failed / rejected / not-found">ng <span>0</span></a><a\n' +
|
' href="#" act="ng" tt="failed / rejected / not-found">ng <span>0</span></a><a\n' +
|
||||||
@@ -121,6 +127,7 @@ ebi('op_up2k').innerHTML = (
|
|||||||
' <tbody></tbody>\n' +
|
' <tbody></tbody>\n' +
|
||||||
'</table>\n' +
|
'</table>\n' +
|
||||||
|
|
||||||
|
'<p id="u2flagblock"><b>the files were added to the queue</b><br />however there is a busy up2k in another browser tab,<br />so waiting for that to finish first</p>\n' +
|
||||||
'<p id="u2foot"></p>\n' +
|
'<p id="u2foot"></p>\n' +
|
||||||
'<p id="u2footfoot" data-perm="write">( you can use the <a href="#" id="u2nope">basic uploader</a> if you don\'t need lastmod timestamps, resumable uploads, or progress bars )</p>'
|
'<p id="u2footfoot" data-perm="write">( you can use the <a href="#" id="u2nope">basic uploader</a> if you don\'t need lastmod timestamps, resumable uploads, or progress bars )</p>'
|
||||||
);
|
);
|
||||||
@@ -160,6 +167,7 @@ ebi('tree').innerHTML = (
|
|||||||
' <a href="#" class="btn" step="2" id="twobytwo" tt="Hotkey: A">+</a>\n' +
|
' <a href="#" class="btn" step="2" id="twobytwo" tt="Hotkey: A">+</a>\n' +
|
||||||
' <a href="#" class="btn" step="-2" id="twig" tt="Hotkey: D">–</a>\n' +
|
' <a href="#" class="btn" step="-2" id="twig" tt="Hotkey: D">–</a>\n' +
|
||||||
' <a href="#" class="tgl btn" id="dyntree" tt="autogrow as tree expands">a</a>\n' +
|
' <a href="#" class="tgl btn" id="dyntree" tt="autogrow as tree expands">a</a>\n' +
|
||||||
|
' <a href="#" class="btn" id="visdir" tt="scroll to selected folder">v</a>\n' +
|
||||||
'</div>\n' +
|
'</div>\n' +
|
||||||
'<ul id="treeul"></ul>\n' +
|
'<ul id="treeul"></ul>\n' +
|
||||||
'<div id="thx_ff"> </div>'
|
'<div id="thx_ff"> </div>'
|
||||||
@@ -276,7 +284,7 @@ var mpl = (function () {
|
|||||||
r.os_ctl = !r.os_ctl && have_mctl;
|
r.os_ctl = !r.os_ctl && have_mctl;
|
||||||
bcfg_set('au_os_ctl', r.os_ctl);
|
bcfg_set('au_os_ctl', r.os_ctl);
|
||||||
if (!have_mctl)
|
if (!have_mctl)
|
||||||
toast.err(5, 'need firefox 82+ or chrome 73+');
|
toast.err(5, 'need firefox 82+ or chrome 73+\n(or iOS 15+ supposedly)');
|
||||||
};
|
};
|
||||||
|
|
||||||
ebi('au_osd_cv').onclick = function (e) {
|
ebi('au_osd_cv').onclick = function (e) {
|
||||||
@@ -391,6 +399,7 @@ function MPlayer() {
|
|||||||
r.au_native2 = null;
|
r.au_native2 = null;
|
||||||
r.au_ogvjs = null;
|
r.au_ogvjs = null;
|
||||||
r.au_ogvjs2 = null;
|
r.au_ogvjs2 = null;
|
||||||
|
r.loading = false;
|
||||||
r.tracks = {};
|
r.tracks = {};
|
||||||
r.order = [];
|
r.order = [];
|
||||||
|
|
||||||
@@ -414,18 +423,14 @@ function MPlayer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r.vol = sread('vol');
|
r.vol = clamp(fcfg_get('vol', 0.5), 0, 1);
|
||||||
if (r.vol !== null)
|
|
||||||
r.vol = parseFloat(r.vol);
|
|
||||||
else
|
|
||||||
r.vol = 0.5;
|
|
||||||
|
|
||||||
r.expvol = function (v) {
|
r.expvol = function (v) {
|
||||||
return 0.5 * v + 0.5 * v * v;
|
return 0.5 * v + 0.5 * v * v;
|
||||||
};
|
};
|
||||||
|
|
||||||
r.setvol = function (vol) {
|
r.setvol = function (vol) {
|
||||||
r.vol = Math.max(Math.min(vol, 1), 0);
|
r.vol = clamp(vol, 0, 1);
|
||||||
swrite('vol', vol);
|
swrite('vol', vol);
|
||||||
r.stopfade(true);
|
r.stopfade(true);
|
||||||
|
|
||||||
@@ -672,7 +677,7 @@ var pbar = (function () {
|
|||||||
|
|
||||||
bctx.clearRect(0, 0, bc.w, bc.h);
|
bctx.clearRect(0, 0, bc.w, bc.h);
|
||||||
|
|
||||||
if (!mp.au)
|
if (!mp.au || mp.loading)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var sm = bc.w * 1.0 / mp.au.duration,
|
var sm = bc.w * 1.0 / mp.au.duration,
|
||||||
@@ -694,25 +699,26 @@ var pbar = (function () {
|
|||||||
r.drawpos = function () {
|
r.drawpos = function () {
|
||||||
var bc = r.buf,
|
var bc = r.buf,
|
||||||
pc = r.pos,
|
pc = r.pos,
|
||||||
pctx = pc.ctx;
|
pctx = pc.ctx,
|
||||||
|
apos, adur;
|
||||||
|
|
||||||
pctx.clearRect(0, 0, pc.w, pc.h);
|
pctx.clearRect(0, 0, pc.w, pc.h);
|
||||||
|
|
||||||
if (!mp.au || isNaN(mp.au.duration) || isNaN(mp.au.currentTime))
|
if (!mp.au || mp.loading || isNaN(adur = mp.au.duration) || isNaN(apos = mp.au.currentTime) || apos < 0 || adur < apos)
|
||||||
return; // not-init || unsupp-codec
|
return; // not-init || unsupp-codec
|
||||||
|
|
||||||
var sm = bc.w * 1.0 / mp.au.duration;
|
var sm = bc.w * 1.0 / adur;
|
||||||
|
|
||||||
pctx.fillStyle = light ? 'rgba(0,64,0,0.15)' : 'rgba(204,255,128,0.15)';
|
pctx.fillStyle = light ? 'rgba(0,64,0,0.15)' : 'rgba(204,255,128,0.15)';
|
||||||
for (var p = 1, mins = mp.au.duration / 10; p <= mins; p++)
|
for (var p = 1, mins = adur / 10; p <= mins; p++)
|
||||||
pctx.fillRect(Math.floor(sm * p * 10), 0, 2, pc.h);
|
pctx.fillRect(Math.floor(sm * p * 10), 0, 2, pc.h);
|
||||||
|
|
||||||
pctx.fillStyle = light ? 'rgba(0,64,0,0.5)' : 'rgba(192,255,96,0.5)';
|
pctx.fillStyle = light ? 'rgba(0,64,0,0.5)' : 'rgba(192,255,96,0.5)';
|
||||||
for (var p = 1, mins = mp.au.duration / 60; p <= mins; p++)
|
for (var p = 1, mins = adur / 60; p <= mins; p++)
|
||||||
pctx.fillRect(Math.floor(sm * p * 60), 0, 2, pc.h);
|
pctx.fillRect(Math.floor(sm * p * 60), 0, 2, pc.h);
|
||||||
|
|
||||||
var w = 8,
|
var w = 8,
|
||||||
x = sm * mp.au.currentTime;
|
x = sm * apos;
|
||||||
|
|
||||||
pctx.fillStyle = '#573'; pctx.fillRect((x - w / 2) - 1, 0, w + 2, pc.h);
|
pctx.fillStyle = '#573'; pctx.fillRect((x - w / 2) - 1, 0, w + 2, pc.h);
|
||||||
pctx.fillStyle = '#dfc'; pctx.fillRect((x - w / 2), 0, 8, pc.h);
|
pctx.fillStyle = '#dfc'; pctx.fillRect((x - w / 2), 0, 8, pc.h);
|
||||||
@@ -723,8 +729,8 @@ var pbar = (function () {
|
|||||||
pctx.font = '1em sans-serif';
|
pctx.font = '1em sans-serif';
|
||||||
|
|
||||||
var m = pctx.measureText.bind(pctx),
|
var m = pctx.measureText.bind(pctx),
|
||||||
t1 = s2ms(mp.au.duration),
|
t1 = s2ms(adur),
|
||||||
t2 = s2ms(mp.au.currentTime),
|
t2 = s2ms(apos),
|
||||||
yt = pc.h / 3 * 2.1,
|
yt = pc.h / 3 * 2.1,
|
||||||
xt1 = pc.w - (m(t1).width + 12),
|
xt1 = pc.w - (m(t1).width + 12),
|
||||||
xt2 = x < pc.w / 2 ? (x + 12) : (Math.min(pc.w - m(t1 + ":88").width, x - 12) - m(t2).width);
|
xt2 = x < pc.w / 2 ? (x + 12) : (Math.min(pc.w - m(t1 + ":88").width, x - 12) - m(t2).width);
|
||||||
@@ -921,14 +927,16 @@ function playpause(e) {
|
|||||||
var mpui = (function () {
|
var mpui = (function () {
|
||||||
var r = {},
|
var r = {},
|
||||||
nth = 0,
|
nth = 0,
|
||||||
timeout = null,
|
|
||||||
preloaded = null;
|
preloaded = null;
|
||||||
|
|
||||||
r.progress_updater = function () {
|
r.progress_updater = function () {
|
||||||
clearTimeout(timeout);
|
timer.add(updater_impl, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
function updater_impl() {
|
||||||
if (!mp.au) {
|
if (!mp.au) {
|
||||||
widget.paused(true);
|
widget.paused(true);
|
||||||
|
timer.rm(updater_impl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -955,7 +963,7 @@ var mpui = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// preload next song
|
// preload next song
|
||||||
if (mpl.preload && preloaded != mp.au.src) {
|
if (mpl.preload && !mp.loading && preloaded != mp.au.src) {
|
||||||
var pos = mp.au.currentTime,
|
var pos = mp.au.currentTime,
|
||||||
len = mp.au.duration;
|
len = mp.au.duration;
|
||||||
|
|
||||||
@@ -970,9 +978,9 @@ var mpui = (function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mp.au.paused)
|
if (mp.au.paused)
|
||||||
timeout = setTimeout(r.progress_updater, 100);
|
timer.rm(updater_impl);
|
||||||
};
|
}
|
||||||
r.progress_updater();
|
r.progress_updater();
|
||||||
return r;
|
return r;
|
||||||
})();
|
})();
|
||||||
@@ -1011,6 +1019,21 @@ function need_ogv_for(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function start_sinegen() {
|
||||||
|
var af = 'data:audio/wav;base64,UklGRlaxAgBXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAATElTVBoAAABJTkZPSVNGVA4AAABMYXZmNTguNzYuMTAwAGRhdGEQsQIAAAB',
|
||||||
|
body = 'iArcE8AYCCeEKggzaDeQOmA/0D/QPmQ/kDtsNggzhCgMJ8Qa4BGMCAQCe/Un7EPn+9h/1fvMm8hzxaPAM8AzwZ/Ac8SXyfvMf9f32D/lI+539//9';
|
||||||
|
|
||||||
|
while (af.length < 235304)
|
||||||
|
af += body;
|
||||||
|
|
||||||
|
var au = new Audio(af.slice(0, 235304));
|
||||||
|
au.onplay = au.pause.bind(au);
|
||||||
|
au.volume = 0.5;
|
||||||
|
au.play();
|
||||||
|
return au;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var audio_eq = (function () {
|
var audio_eq = (function () {
|
||||||
var r = {
|
var r = {
|
||||||
"en": false,
|
"en": false,
|
||||||
@@ -1270,24 +1293,41 @@ function play(tid, is_ev, seek, call_depth) {
|
|||||||
|
|
||||||
var url = mp.tracks[tid];
|
var url = mp.tracks[tid];
|
||||||
if (need_ogv_for(url)) {
|
if (need_ogv_for(url)) {
|
||||||
|
var m = /.* Version\/([0-9]+)\.[0-9\.]+ Mobile\/[^ ]+ Safari\/[0-9\.]+$/.exec(navigator.userAgent),
|
||||||
|
safari = m ? parseInt(m[1]) : 99;
|
||||||
|
|
||||||
if (mp.au_ogvjs) {
|
if (mp.au_ogvjs) {
|
||||||
mp.au = mp.au_ogvjs;
|
mp.au = mp.au_ogvjs;
|
||||||
}
|
}
|
||||||
else if (window['OGVPlayer']) {
|
else if (window['OGVPlayer']) {
|
||||||
|
mp.loading = true;
|
||||||
|
try {
|
||||||
mp.au = mp.au_ogvjs = new OGVPlayer();
|
mp.au = mp.au_ogvjs = new OGVPlayer();
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
return toast.err(30, 'your browser cannot play ogg/vorbis/opus\n\n' + ex +
|
||||||
|
'\n\n<a href="#" onclick="new OGVPlayer();">click here</a> for a full crash report');
|
||||||
|
}
|
||||||
attempt_play = is_ev;
|
attempt_play = is_ev;
|
||||||
mp.au.addEventListener('error', evau_error, true);
|
mp.au.onerror = evau_error;
|
||||||
mp.au.addEventListener('progress', pbar.drawpos);
|
mp.au.onprogress = pbar.drawpos;
|
||||||
mp.au.addEventListener('ended', next_song);
|
mp.au.onended = next_song;
|
||||||
|
mp.au.onloadedmetadata = mp.au.onloadeddata = function () {
|
||||||
|
mp.loading = false;
|
||||||
|
};
|
||||||
widget.open();
|
widget.open();
|
||||||
}
|
}
|
||||||
|
else if (safari < 14) {
|
||||||
|
return toast.err(0, 'because this is an apple device,\nsafari 14 or newer is required to play ogg/vorbis/opus files\n\nyou are using safari ' + safari + '\n(every iOS browser is actually safari)');
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
if (call_depth !== undefined)
|
if (call_depth !== undefined)
|
||||||
return alert('failed to load ogv.js');
|
return toast.err(0, 'failed to load ogv.js:\ncannot play ogg/opus in this browser\n(try a non-apple device)');
|
||||||
|
|
||||||
show_modal('<h1>loading ogv.js</h1><h2>thanks apple</h2>');
|
toast.inf(0, '<h1>loading ogv.js</h1><h2>thanks apple</h2>');
|
||||||
|
|
||||||
import_js('/.cpr/deps/ogv.js', function () {
|
import_js('/.cpr/deps/ogv.js', function () {
|
||||||
|
toast.hide();
|
||||||
play(tid, false, seek, 1);
|
play(tid, false, seek, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1297,18 +1337,26 @@ function play(tid, is_ev, seek, call_depth) {
|
|||||||
else {
|
else {
|
||||||
if (!mp.au_native) {
|
if (!mp.au_native) {
|
||||||
mp.au = mp.au_native = new Audio();
|
mp.au = mp.au_native = new Audio();
|
||||||
mp.au.addEventListener('error', evau_error, true);
|
mp.au.onerror = evau_error;
|
||||||
mp.au.addEventListener('progress', pbar.drawpos);
|
mp.au.onprogress = pbar.drawpos;
|
||||||
mp.au.addEventListener('ended', next_song);
|
mp.au.onended = next_song;
|
||||||
widget.open();
|
widget.open();
|
||||||
}
|
}
|
||||||
mp.au = mp.au_native;
|
mp.au = mp.au_native;
|
||||||
|
mp.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
audio_eq.apply();
|
audio_eq.apply();
|
||||||
|
|
||||||
|
url += (url.indexOf('?') < 0 ? '?cache' : '&cache');
|
||||||
|
if (mp.au.src == url)
|
||||||
|
mp.au.currentTime = 0;
|
||||||
|
else {
|
||||||
|
mp.loading = mp.au == mp.au_ogvjs;
|
||||||
|
mp.au.src = url;
|
||||||
|
}
|
||||||
|
|
||||||
mp.au.tid = tid;
|
mp.au.tid = tid;
|
||||||
mp.au.src = url + (url.indexOf('?') < 0 ? '?cache' : '&cache');
|
|
||||||
mp.au.volume = mp.expvol(mp.vol);
|
mp.au.volume = mp.expvol(mp.vol);
|
||||||
var oid = 'a' + tid;
|
var oid = 'a' + tid;
|
||||||
setclass(oid, 'play act');
|
setclass(oid, 'play act');
|
||||||
@@ -1349,7 +1397,7 @@ function play(tid, is_ev, seek, call_depth) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
toast.err(0, 'playback failed: ' + ex);
|
toast.err(0, esc('playback failed: ' + ex));
|
||||||
}
|
}
|
||||||
setclass(oid, 'play');
|
setclass(oid, 'play');
|
||||||
setTimeout(next_song, 500);
|
setTimeout(next_song, 500);
|
||||||
@@ -1383,7 +1431,7 @@ function evau_error(e) {
|
|||||||
|
|
||||||
err += '\n\nFile: «' + uricom_dec(eplaya.src.split('/').slice(-1)[0])[0] + '»';
|
err += '\n\nFile: «' + uricom_dec(eplaya.src.split('/').slice(-1)[0])[0] + '»';
|
||||||
|
|
||||||
alert(err);
|
toast.warn(15, esc(err + ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1424,9 +1472,16 @@ function autoplay_blocked(seek) {
|
|||||||
go.textContent = 'Play "' + fn + '"';
|
go.textContent = 'Play "' + fn + '"';
|
||||||
go.onclick = function (e) {
|
go.onclick = function (e) {
|
||||||
unblocked(e);
|
unblocked(e);
|
||||||
|
if (mp.au !== mp.au_ogvjs)
|
||||||
// chrome 91 may permanently taint on a failed play()
|
// chrome 91 may permanently taint on a failed play()
|
||||||
// depending on win10 settings or something? idk
|
// depending on win10 settings or something? idk
|
||||||
mp.au_native = mp.au_ogvjs = null;
|
mp.au_native = null;
|
||||||
|
else
|
||||||
|
// iOS browsers allow playing ogg/vorbis/opus in the background
|
||||||
|
// if there is an <audio> tag which at some point played audio
|
||||||
|
if (!mp.sinegen)
|
||||||
|
mp.sinegen = start_sinegen();
|
||||||
|
|
||||||
play(tid, true, seek);
|
play(tid, true, seek);
|
||||||
mp.fade_in();
|
mp.fade_in();
|
||||||
};
|
};
|
||||||
@@ -1460,6 +1515,83 @@ function play_linked() {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
function sortfiles(nodes) {
|
||||||
|
var sopts = jread('fsort', [["href", 1, ""]]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var is_srch = false;
|
||||||
|
if (nodes[0]['rp']) {
|
||||||
|
is_srch = true;
|
||||||
|
for (var b = 0, bb = nodes.length; b < bb; b++)
|
||||||
|
nodes[b].ext = nodes[b].rp.split('.').pop();
|
||||||
|
for (var b = 0; b < sopts.length; b++)
|
||||||
|
if (sopts[b][0] == 'href')
|
||||||
|
sopts[b][0] = 'rp';
|
||||||
|
}
|
||||||
|
for (var a = sopts.length - 1; a >= 0; a--) {
|
||||||
|
var name = sopts[a][0], rev = sopts[a][1], typ = sopts[a][2];
|
||||||
|
if (!name)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (name == 'ts')
|
||||||
|
typ = 'int';
|
||||||
|
|
||||||
|
if (name.indexOf('tags/') === 0) {
|
||||||
|
name = name.slice(5);
|
||||||
|
for (var b = 0, bb = nodes.length; b < bb; b++)
|
||||||
|
nodes[b]._sv = nodes[b].tags[name];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (var b = 0, bb = nodes.length; b < bb; b++) {
|
||||||
|
var v = nodes[b][name];
|
||||||
|
|
||||||
|
if ((v + '').indexOf('<a ') === 0)
|
||||||
|
v = v.split('>')[1];
|
||||||
|
else if (name == "href" && v) {
|
||||||
|
if (v.slice(-1) == '/')
|
||||||
|
v = '\t' + v;
|
||||||
|
|
||||||
|
v = uricom_dec(v)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes[b]._sv = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var onodes = nodes.map(function (x) { return x; });
|
||||||
|
nodes.sort(function (n1, n2) {
|
||||||
|
var v1 = n1._sv,
|
||||||
|
v2 = n2._sv;
|
||||||
|
|
||||||
|
if (v1 === undefined) {
|
||||||
|
if (v2 === undefined) {
|
||||||
|
return onodes.indexOf(n1) - onodes.indexOf(n2);
|
||||||
|
}
|
||||||
|
return -1 * rev;
|
||||||
|
}
|
||||||
|
if (v2 === undefined) return 1 * rev;
|
||||||
|
|
||||||
|
var ret = rev * (typ == 'int' ? (v1 - v2) : (v1.localeCompare(v2)));
|
||||||
|
if (ret === 0)
|
||||||
|
ret = onodes.indexOf(n1) - onodes.indexOf(n2);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (var b = 0, bb = nodes.length; b < bb; b++) {
|
||||||
|
delete nodes[b]._sv;
|
||||||
|
if (is_srch)
|
||||||
|
delete nodes[b].ext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.log("failed to apply sort config: " + ex);
|
||||||
|
console.log("resetting fsort " + sread('fsort'))
|
||||||
|
localStorage.removeItem('fsort');
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function fmt_ren(re, md, fmt) {
|
function fmt_ren(re, md, fmt) {
|
||||||
var ptr = 0;
|
var ptr = 0;
|
||||||
@@ -1567,14 +1699,17 @@ var fileman = (function () {
|
|||||||
r = {};
|
r = {};
|
||||||
|
|
||||||
r.clip = null;
|
r.clip = null;
|
||||||
|
try {
|
||||||
r.bus = new BroadcastChannel("fileman_bus");
|
r.bus = new BroadcastChannel("fileman_bus");
|
||||||
|
}
|
||||||
|
catch (ex) { }
|
||||||
|
|
||||||
r.render = function () {
|
r.render = function () {
|
||||||
if (r.clip === null)
|
if (r.clip === null)
|
||||||
r.clip = jread('fman_clip', []);
|
r.clip = jread('fman_clip', []);
|
||||||
|
|
||||||
var nsel = msel.getsel().length;
|
var nsel = msel.getsel().length;
|
||||||
clmod(bren, 'en', nsel == 1);
|
clmod(bren, 'en', nsel);
|
||||||
clmod(bdel, 'en', nsel);
|
clmod(bdel, 'en', nsel);
|
||||||
clmod(bcut, 'en', nsel);
|
clmod(bcut, 'en', nsel);
|
||||||
clmod(bpst, 'en', r.clip && r.clip.length);
|
clmod(bpst, 'en', r.clip && r.clip.length);
|
||||||
@@ -1583,7 +1718,7 @@ var fileman = (function () {
|
|||||||
bcut.style.display = have_mv && has(perms, 'move') ? '' : 'none';
|
bcut.style.display = have_mv && has(perms, 'move') ? '' : 'none';
|
||||||
bpst.style.display = have_mv && has(perms, 'write') ? '' : 'none';
|
bpst.style.display = have_mv && has(perms, 'write') ? '' : 'none';
|
||||||
bpst.setAttribute('tt', 'paste ' + r.clip.length + ' items$NHotkey: ctrl-V');
|
bpst.setAttribute('tt', 'paste ' + r.clip.length + ' items$NHotkey: ctrl-V');
|
||||||
ebi('wfm').style.display = QS('#wfm a.en:not([display])') ? '' : 'none';
|
clmod(ebi('wfm'), 'act', QS('#wfm a.en:not([style])'));
|
||||||
};
|
};
|
||||||
|
|
||||||
r.rename = function (e) {
|
r.rename = function (e) {
|
||||||
@@ -1606,7 +1741,7 @@ var fileman = (function () {
|
|||||||
|
|
||||||
var vsp = vsplit(vp);
|
var vsp = vsplit(vp);
|
||||||
if (base != vsp[0])
|
if (base != vsp[0])
|
||||||
return toast.err(0, 'bug:\n' + base + '\n' + vsp[0]);
|
return toast.err(0, esc('bug:\n' + base + '\n' + vsp[0]));
|
||||||
|
|
||||||
var vars = ft2dict(ebi(sel[a].id).closest('tr'));
|
var vars = ft2dict(ebi(sel[a].id).closest('tr'));
|
||||||
mkeys = vars[1].concat(vars[2]);
|
mkeys = vars[1].concat(vars[2]);
|
||||||
@@ -1664,6 +1799,7 @@ var fileman = (function () {
|
|||||||
'</table></div>'
|
'</table></div>'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
var cheap = f.length > 500;
|
||||||
if (sel.length == 1)
|
if (sel.length == 1)
|
||||||
html.push(
|
html.push(
|
||||||
'<div><table id="rn_f">\n' +
|
'<div><table id="rn_f">\n' +
|
||||||
@@ -1676,8 +1812,9 @@ var fileman = (function () {
|
|||||||
for (var a = 0; a < f.length; a++)
|
for (var a = 0; a < f.length; a++)
|
||||||
html.push(
|
html.push(
|
||||||
'<tr><td>' +
|
'<tr><td>' +
|
||||||
'<button class="rn_dec" n="' + a + '">decode</button>',
|
(cheap ? '</td>' :
|
||||||
'<button class="rn_reset" n="' + a + '">↺ reset</button></td>',
|
'<button class="rn_dec" n="' + a + '">decode</button>' +
|
||||||
|
'<button class="rn_reset" n="' + a + '">↺ reset</button></td>') +
|
||||||
'<td><input type="text" id="rn_new" n="' + a + '" /></td>' +
|
'<td><input type="text" id="rn_new" n="' + a + '" /></td>' +
|
||||||
'<td><input type="text" id="rn_old" n="' + a + '" readonly /></td></tr>');
|
'<td><input type="text" id="rn_old" n="' + a + '" readonly /></td></tr>');
|
||||||
}
|
}
|
||||||
@@ -1698,6 +1835,7 @@ var fileman = (function () {
|
|||||||
f[a].inew = QS('#rn_new' + k);
|
f[a].inew = QS('#rn_new' + k);
|
||||||
f[a].inew.value = f[a].iold.value = f[a].ofn;
|
f[a].inew.value = f[a].iold.value = f[a].ofn;
|
||||||
|
|
||||||
|
if (!cheap)
|
||||||
(function (a) {
|
(function (a) {
|
||||||
f[a].inew.onkeydown = function (e) {
|
f[a].inew.onkeydown = function (e) {
|
||||||
rn_ok(a, true);
|
rn_ok(a, true);
|
||||||
@@ -1708,10 +1846,12 @@ var fileman = (function () {
|
|||||||
if (e.key == 'Enter')
|
if (e.key == 'Enter')
|
||||||
return rn_apply();
|
return rn_apply();
|
||||||
};
|
};
|
||||||
QS('.rn_dec' + k).onclick = function () {
|
QS('.rn_dec' + k).onclick = function (e) {
|
||||||
|
ev(e);
|
||||||
f[a].inew.value = uricom_dec(f[a].inew.value)[0];
|
f[a].inew.value = uricom_dec(f[a].inew.value)[0];
|
||||||
};
|
};
|
||||||
QS('.rn_reset' + k).onclick = function () {
|
QS('.rn_reset' + k).onclick = function (e) {
|
||||||
|
ev(e);
|
||||||
rn_reset(a);
|
rn_reset(a);
|
||||||
};
|
};
|
||||||
})(a);
|
})(a);
|
||||||
@@ -1776,7 +1916,7 @@ var fileman = (function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
inew.onclick = function () {
|
inew.onclick = function () {
|
||||||
var name = prompt('provide a name for your new preset', ifmt.value);
|
modal.prompt('provide a name for your new preset', ifmt.value, function (name) {
|
||||||
if (!name)
|
if (!name)
|
||||||
return toast.warn(3, 'aborted');
|
return toast.warn(3, 'aborted');
|
||||||
|
|
||||||
@@ -1784,6 +1924,7 @@ var fileman = (function () {
|
|||||||
jwrite('rn_pre', presets);
|
jwrite('rn_pre', presets);
|
||||||
spresets();
|
spresets();
|
||||||
ipre.value = name;
|
ipre.value = name;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
idel.onclick = function () {
|
idel.onclick = function () {
|
||||||
delete presets[ipre.value];
|
delete presets[ipre.value];
|
||||||
@@ -1821,7 +1962,7 @@ var fileman = (function () {
|
|||||||
re = new RegExp(ptn, cs ? 'i' : '');
|
re = new RegExp(ptn, cs ? 'i' : '');
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
return toast.err(5, 'invalid regex:\n' + ex);
|
return toast.err(5, esc('invalid regex:\n' + ex));
|
||||||
}
|
}
|
||||||
toast.hide();
|
toast.hide();
|
||||||
|
|
||||||
@@ -1853,7 +1994,7 @@ var fileman = (function () {
|
|||||||
return rn_cancel();
|
return rn_cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.inf(0, 'renaming ' + f.length + ' items\n\n' + f[0].ofn);
|
toast.inf(0, esc('renaming ' + f.length + ' items\n\n' + f[0].ofn));
|
||||||
var dst = base + uricom_enc(f[0].inew.value, false);
|
var dst = base + uricom_enc(f[0].inew.value, false);
|
||||||
|
|
||||||
function rename_cb() {
|
function rename_cb() {
|
||||||
@@ -1891,12 +2032,6 @@ var fileman = (function () {
|
|||||||
if (!sel.length)
|
if (!sel.length)
|
||||||
return toast.err(3, 'select at least 1 item to delete');
|
return toast.err(3, 'select at least 1 item to delete');
|
||||||
|
|
||||||
if (!confirm('===== DANGER =====\nDELETE these ' + vps.length + ' items?\n\n' + vps.join('\n')))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!confirm('Last chance! Delete?'))
|
|
||||||
return;
|
|
||||||
|
|
||||||
function deleter() {
|
function deleter() {
|
||||||
var xhr = new XMLHttpRequest(),
|
var xhr = new XMLHttpRequest(),
|
||||||
vp = vps.shift();
|
vp = vps.shift();
|
||||||
@@ -1906,7 +2041,7 @@ var fileman = (function () {
|
|||||||
treectl.goto(get_evpath());
|
treectl.goto(get_evpath());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toast.inf(0, 'deleting ' + (vps.length + 1) + ' items\n\n' + vp);
|
toast.inf(0, esc('deleting ' + (vps.length + 1) + ' items\n\n' + vp));
|
||||||
|
|
||||||
xhr.open('GET', vp + '?delete', true);
|
xhr.open('GET', vp + '?delete', true);
|
||||||
xhr.onreadystatechange = delete_cb;
|
xhr.onreadystatechange = delete_cb;
|
||||||
@@ -1923,7 +2058,10 @@ var fileman = (function () {
|
|||||||
}
|
}
|
||||||
deleter();
|
deleter();
|
||||||
}
|
}
|
||||||
deleter();
|
|
||||||
|
modal.confirm('<h6 style="color:#900">DANGER</h6>\n<b>DELETE these ' + vps.length + ' items?</b><ul>' + uricom_adec(vps, true).join('') + '</ul>', function () {
|
||||||
|
modal.confirm('<b>Last chance!</b> Delete?', deleter, null);
|
||||||
|
}, null);
|
||||||
};
|
};
|
||||||
|
|
||||||
r.cut = function (e) {
|
r.cut = function (e) {
|
||||||
@@ -1937,16 +2075,21 @@ var fileman = (function () {
|
|||||||
if (!sel.length)
|
if (!sel.length)
|
||||||
return toast.err(3, 'select at least 1 item to cut');
|
return toast.err(3, 'select at least 1 item to cut');
|
||||||
|
|
||||||
|
var els = [];
|
||||||
for (var a = 0; a < sel.length; a++) {
|
for (var a = 0; a < sel.length; a++) {
|
||||||
vps.push(sel[a].vp);
|
vps.push(sel[a].vp);
|
||||||
var cl = ebi(sel[a].id).closest('tr').classList,
|
if (sel.length < 100) {
|
||||||
inv = cl.contains('c1');
|
els.push(ebi(sel[a].id).closest('tr'));
|
||||||
|
clmod(els[a], 'fcut');
|
||||||
cl.remove(inv ? 'c1' : 'c2');
|
}
|
||||||
cl.add(inv ? 'c2' : 'c1');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.inf(1, 'cut ' + sel.length + ' items');
|
setTimeout(function () {
|
||||||
|
for (var a = 0; a < els.length; a++)
|
||||||
|
clmod(els[a], 'fcut', 1);
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
toast.inf(1.5, 'cut ' + sel.length + ' items');
|
||||||
jwrite('fman_clip', vps);
|
jwrite('fman_clip', vps);
|
||||||
r.tx(1);
|
r.tx(1);
|
||||||
};
|
};
|
||||||
@@ -1981,14 +2124,11 @@ var fileman = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (exists.length)
|
if (exists.length)
|
||||||
alert('these ' + exists.length + ' items cannot be pasted here (names already exist):\n\n' + uricom_adec(exists).join('\n'));
|
toast.warn(30, 'these ' + exists.length + ' items cannot be pasted here (names already exist):<ul>' + uricom_adec(exists, true).join('') + '</ul>');
|
||||||
|
|
||||||
if (!req.length)
|
if (!req.length)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!confirm('paste these ' + req.length + ' items here?\n\n' + uricom_adec(req).join('\n')))
|
|
||||||
return;
|
|
||||||
|
|
||||||
function paster() {
|
function paster() {
|
||||||
var xhr = new XMLHttpRequest(),
|
var xhr = new XMLHttpRequest(),
|
||||||
vp = req.shift();
|
vp = req.shift();
|
||||||
@@ -1999,7 +2139,7 @@ var fileman = (function () {
|
|||||||
r.tx(srcdir);
|
r.tx(srcdir);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toast.inf(0, 'pasting ' + (req.length + 1) + ' items\n\n' + uricom_dec(vp)[0]);
|
toast.inf(0, esc('pasting ' + (req.length + 1) + ' items\n\n' + uricom_dec(vp)[0]));
|
||||||
|
|
||||||
var dst = get_evpath() + vp.split('/').slice(-1)[0];
|
var dst = get_evpath() + vp.split('/').slice(-1)[0];
|
||||||
|
|
||||||
@@ -2018,20 +2158,29 @@ var fileman = (function () {
|
|||||||
}
|
}
|
||||||
paster();
|
paster();
|
||||||
}
|
}
|
||||||
paster();
|
|
||||||
|
|
||||||
|
modal.confirm('paste these ' + req.length + ' items here?<ul>' + uricom_adec(req, true).join('') + '</ul>', function () {
|
||||||
|
paster();
|
||||||
jwrite('fman_clip', []);
|
jwrite('fman_clip', []);
|
||||||
|
}, null);
|
||||||
};
|
};
|
||||||
|
|
||||||
r.bus.onmessage = function (e) {
|
function onmsg(msg) {
|
||||||
r.clip = null;
|
r.clip = null;
|
||||||
r.render();
|
r.render();
|
||||||
var me = get_evpath();
|
if (msg == get_evpath())
|
||||||
if (e && e.data == me)
|
treectl.goto(msg);
|
||||||
treectl.goto(e.data);
|
}
|
||||||
|
|
||||||
|
if (r.bus)
|
||||||
|
r.bus.onmessage = function (e) {
|
||||||
|
onmsg(e ? e.data : 1)
|
||||||
};
|
};
|
||||||
|
|
||||||
r.tx = function (msg) {
|
r.tx = function (msg) {
|
||||||
|
if (!r.bus)
|
||||||
|
return onmsg(msg);
|
||||||
|
|
||||||
r.bus.postMessage(msg);
|
r.bus.postMessage(msg);
|
||||||
r.bus.onmessage();
|
r.bus.onmessage();
|
||||||
};
|
};
|
||||||
@@ -2053,14 +2202,16 @@ var thegrid = (function () {
|
|||||||
gfiles.style.display = 'none';
|
gfiles.style.display = 'none';
|
||||||
gfiles.innerHTML = (
|
gfiles.innerHTML = (
|
||||||
'<div id="ghead">' +
|
'<div id="ghead">' +
|
||||||
'<a href="#" class="tgl btn" id="gridsel" tt="enable file selection; ctrl-click a file to override$NHotkey: S">multiselect</a> zoom ' +
|
'<a href="#" class="tgl btn" id="gridsel" tt="enable file selection; ctrl-click a file to override$NHotkey: S">multiselect</a> <span>zoom: ' +
|
||||||
'<a href="#" class="btn" z="-1.2" tt="Hotkey: shift-A">–</a> ' +
|
'<a href="#" class="btn" z="-1.2" tt="Hotkey: shift-A">–</a> ' +
|
||||||
'<a href="#" class="btn" z="1.2" tt="Hotkey: shift-D">+</a> sort by: ' +
|
'<a href="#" class="btn" z="1.2" tt="Hotkey: shift-D">+</a></span> <span>chop: ' +
|
||||||
|
'<a href="#" class="btn" l="-1" tt="truncate filenames more (show less)">–</a> ' +
|
||||||
|
'<a href="#" class="btn" l="1" tt="truncate filenames less (show more)">+</a></span> <span>sort by: ' +
|
||||||
'<a href="#" s="href">name</a>, ' +
|
'<a href="#" s="href">name</a>, ' +
|
||||||
'<a href="#" s="sz">size</a>, ' +
|
'<a href="#" s="sz">size</a>, ' +
|
||||||
'<a href="#" s="ts">date</a>, ' +
|
'<a href="#" s="ts">date</a>, ' +
|
||||||
'<a href="#" s="ext">type</a>' +
|
'<a href="#" s="ext">type</a>' +
|
||||||
'</div>' +
|
'</span></div>' +
|
||||||
'<div id="ggrid"></div>'
|
'<div id="ggrid"></div>'
|
||||||
);
|
);
|
||||||
lfiles.parentNode.insertBefore(gfiles, lfiles);
|
lfiles.parentNode.insertBefore(gfiles, lfiles);
|
||||||
@@ -2069,7 +2220,8 @@ var thegrid = (function () {
|
|||||||
'thumbs': bcfg_get('thumbs', true),
|
'thumbs': bcfg_get('thumbs', true),
|
||||||
'en': bcfg_get('griden', false),
|
'en': bcfg_get('griden', false),
|
||||||
'sel': bcfg_get('gridsel', false),
|
'sel': bcfg_get('gridsel', false),
|
||||||
'sz': fcfg_get('gridsz', 10),
|
'sz': clamp(fcfg_get('gridsz', 10), 4, 40),
|
||||||
|
'ln': clamp(icfg_get('gridln', 3), 1, 7),
|
||||||
'isdirty': true,
|
'isdirty': true,
|
||||||
'bbox': null
|
'bbox': null
|
||||||
};
|
};
|
||||||
@@ -2089,8 +2241,7 @@ var thegrid = (function () {
|
|||||||
loadgrid();
|
loadgrid();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
lfiles.style.display = '';
|
ungrid();
|
||||||
gfiles.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
pbar.onresize();
|
pbar.onresize();
|
||||||
vbar.onresize();
|
vbar.onresize();
|
||||||
@@ -2099,11 +2250,15 @@ var thegrid = (function () {
|
|||||||
var btnclick = function (e) {
|
var btnclick = function (e) {
|
||||||
ev(e);
|
ev(e);
|
||||||
var s = this.getAttribute('s'),
|
var s = this.getAttribute('s'),
|
||||||
z = this.getAttribute('z');
|
z = this.getAttribute('z'),
|
||||||
|
l = this.getAttribute('l');
|
||||||
|
|
||||||
if (z)
|
if (z)
|
||||||
return setsz(z > 0 ? r.sz * z : r.sz / (-z));
|
return setsz(z > 0 ? r.sz * z : r.sz / (-z));
|
||||||
|
|
||||||
|
if (l)
|
||||||
|
return setln(parseInt(l));
|
||||||
|
|
||||||
var t = lfiles.tHead.rows[0].cells;
|
var t = lfiles.tHead.rows[0].cells;
|
||||||
for (var a = 0; a < t.length; a++)
|
for (var a = 0; a < t.length; a++)
|
||||||
if (t[a].getAttribute('name') == s) {
|
if (t[a].getAttribute('name') == s) {
|
||||||
@@ -2114,7 +2269,7 @@ var thegrid = (function () {
|
|||||||
r.setdirty();
|
r.setdirty();
|
||||||
};
|
};
|
||||||
|
|
||||||
var links = QSA('#ghead>a');
|
var links = QSA('#ghead a');
|
||||||
for (var a = 0; a < links.length; a++)
|
for (var a = 0; a < links.length; a++)
|
||||||
links[a].onclick = btnclick;
|
links[a].onclick = btnclick;
|
||||||
|
|
||||||
@@ -2127,19 +2282,35 @@ var thegrid = (function () {
|
|||||||
|
|
||||||
r.setvis = function (vis) {
|
r.setvis = function (vis) {
|
||||||
(r.en ? gfiles : lfiles).style.display = vis ? '' : 'none';
|
(r.en ? gfiles : lfiles).style.display = vis ? '' : 'none';
|
||||||
}
|
};
|
||||||
|
|
||||||
r.setdirty = function () {
|
r.setdirty = function () {
|
||||||
r.dirty = true;
|
r.dirty = true;
|
||||||
if (r.en) {
|
if (r.en) {
|
||||||
loadgrid();
|
loadgrid();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function setln(v) {
|
||||||
|
if (v) {
|
||||||
|
r.ln += v;
|
||||||
|
if (r.ln < 1) r.ln = 1;
|
||||||
|
if (r.ln > 7) r.ln = v < 0 ? 7 : 99;
|
||||||
|
swrite('gridln', r.ln);
|
||||||
|
setTimeout(r.tippen, 20);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
document.documentElement.style.setProperty('--grid-ln', r.ln);
|
||||||
|
}
|
||||||
|
catch (ex) { }
|
||||||
|
}
|
||||||
|
setln();
|
||||||
|
|
||||||
function setsz(v) {
|
function setsz(v) {
|
||||||
if (v !== undefined) {
|
if (v !== undefined) {
|
||||||
r.sz = v;
|
r.sz = clamp(v, 4, 40);
|
||||||
swrite('gridsz', r.sz);
|
swrite('gridsz', r.sz);
|
||||||
|
setTimeout(r.tippen, 20);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
document.documentElement.style.setProperty('--grid-sz', r.sz + 'em');
|
document.documentElement.style.setProperty('--grid-sz', r.sz + 'em');
|
||||||
@@ -2155,7 +2326,7 @@ var thegrid = (function () {
|
|||||||
var oth = ebi(this.getAttribute('ref')),
|
var oth = ebi(this.getAttribute('ref')),
|
||||||
href = this.getAttribute('href'),
|
href = this.getAttribute('href'),
|
||||||
aplay = ebi('a' + oth.getAttribute('id')),
|
aplay = ebi('a' + oth.getAttribute('id')),
|
||||||
is_img = /\.(gif|jpe?g|png|webp)(\?|$)/i.test(href),
|
is_img = /\.(gif|jpe?g|png|webp|webm|mp4)(\?|$)/i.test(href),
|
||||||
in_tree = null,
|
in_tree = null,
|
||||||
have_sel = QS('#files tr.sel'),
|
have_sel = QS('#files tr.sel'),
|
||||||
td = oth.closest('td').nextSibling,
|
td = oth.closest('td').nextSibling,
|
||||||
@@ -2197,14 +2368,56 @@ var thegrid = (function () {
|
|||||||
var ths = QSA('#ggrid>a');
|
var ths = QSA('#ggrid>a');
|
||||||
|
|
||||||
for (var a = 0, aa = ths.length; a < aa; a++) {
|
for (var a = 0, aa = ths.length; a < aa; a++) {
|
||||||
var tr = ebi(ths[a].getAttribute('ref')).closest('tr');
|
var tr = ebi(ths[a].getAttribute('ref')).closest('tr'),
|
||||||
ths[a].setAttribute('class', tr.getAttribute('class'));
|
cl = tr.getAttribute('class') || '';
|
||||||
|
|
||||||
|
if (ths[a].getAttribute('href').endsWith('/'))
|
||||||
|
cl += ' dir';
|
||||||
|
|
||||||
|
ths[a].setAttribute('class', cl);
|
||||||
}
|
}
|
||||||
var uns = QS('#ggrid a[ref="unsearch"]');
|
var uns = QS('#ggrid a[ref="unsearch"]');
|
||||||
if (uns)
|
if (uns)
|
||||||
uns.onclick = function () {
|
uns.onclick = function () {
|
||||||
ebi('unsearch').click();
|
ebi('unsearch').click();
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
r.tippen = function () {
|
||||||
|
var els = QSA('#ggrid>a>span'),
|
||||||
|
aa = els.length;
|
||||||
|
|
||||||
|
if (!aa)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var cs = window.getComputedStyle(els[0]),
|
||||||
|
fs = parseFloat(cs.lineHeight),
|
||||||
|
pad = parseFloat(cs.paddingTop),
|
||||||
|
pels = [],
|
||||||
|
todo = [];
|
||||||
|
|
||||||
|
for (var a = 0; a < aa; a++) {
|
||||||
|
var vis = Math.round((els[a].offsetHeight - pad) / fs),
|
||||||
|
all = Math.round((els[a].scrollHeight - pad) / fs),
|
||||||
|
par = els[a].parentNode;
|
||||||
|
|
||||||
|
pels.push(par);
|
||||||
|
todo.push(vis < all ? par.getAttribute('ttt') : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var a = 0; a < todo.length; a++) {
|
||||||
|
if (todo[a])
|
||||||
|
pels[a].setAttribute('tt', todo[a]);
|
||||||
|
else
|
||||||
|
pels[a].removeAttribute('tt');
|
||||||
|
}
|
||||||
|
|
||||||
|
tt.att(ebi('ggrid'));
|
||||||
|
};
|
||||||
|
|
||||||
|
function ungrid() {
|
||||||
|
lfiles.style.display = '';
|
||||||
|
gfiles.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadgrid() {
|
function loadgrid() {
|
||||||
@@ -2222,6 +2435,7 @@ var thegrid = (function () {
|
|||||||
for (var a = 0, aa = files.length; a < aa; a++) {
|
for (var a = 0, aa = files.length; a < aa; a++) {
|
||||||
var ao = files[a],
|
var ao = files[a],
|
||||||
href = esc(ao.getAttribute('href')),
|
href = esc(ao.getAttribute('href')),
|
||||||
|
name = uricom_dec(vsplit(href)[1])[0],
|
||||||
ref = ao.getAttribute('id'),
|
ref = ao.getAttribute('id'),
|
||||||
isdir = href.split('?')[0].slice(-1)[0] == '/',
|
isdir = href.split('?')[0].slice(-1)[0] == '/',
|
||||||
ac = isdir ? ' class="dir"' : '',
|
ac = isdir ? ' class="dir"' : '',
|
||||||
@@ -2254,7 +2468,8 @@ var thegrid = (function () {
|
|||||||
ihref = '/.cpr/ico/' + ihref.slice(0, -1);
|
ihref = '/.cpr/ico/' + ihref.slice(0, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
html.push('<a href="' + href + '" ref="' + ref + '"><img src="' +
|
html.push('<a href="' + href + '" ref="' + ref +
|
||||||
|
'"' + ac + ' ttt="' + esc(name) + '"><img src="' +
|
||||||
ihref + '" /><span' + ac + '>' + ao.innerHTML + '</span></a>');
|
ihref + '" /><span' + ac + '>' + ao.innerHTML + '</span></a>');
|
||||||
}
|
}
|
||||||
ebi('ggrid').innerHTML = html.join('\n');
|
ebi('ggrid').innerHTML = html.join('\n');
|
||||||
@@ -2266,6 +2481,7 @@ var thegrid = (function () {
|
|||||||
r.dirty = false;
|
r.dirty = false;
|
||||||
r.bagit();
|
r.bagit();
|
||||||
r.loadsel();
|
r.loadsel();
|
||||||
|
setTimeout(r.tippen, 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
r.bagit = function () {
|
r.bagit = function () {
|
||||||
@@ -2303,7 +2519,8 @@ var thegrid = (function () {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
function tree_scrollto() {
|
function tree_scrollto(e) {
|
||||||
|
ev(e);
|
||||||
var act = QS('#treeul a.hl'),
|
var act = QS('#treeul a.hl'),
|
||||||
ul = act ? act.offsetParent : null;
|
ul = act ? act.offsetParent : null;
|
||||||
|
|
||||||
@@ -2763,7 +2980,7 @@ var treectl = (function () {
|
|||||||
var treectl = {
|
var treectl = {
|
||||||
"hidden": true,
|
"hidden": true,
|
||||||
"ls_cb": null,
|
"ls_cb": null,
|
||||||
"dir_cb": null
|
"dir_cb": tree_scrollto
|
||||||
},
|
},
|
||||||
entreed = false,
|
entreed = false,
|
||||||
fixedpos = false,
|
fixedpos = false,
|
||||||
@@ -2771,9 +2988,7 @@ var treectl = (function () {
|
|||||||
prev_winh = null,
|
prev_winh = null,
|
||||||
dyn = bcfg_get('dyntree', true),
|
dyn = bcfg_get('dyntree', true),
|
||||||
dots = bcfg_get('dotfiles', false),
|
dots = bcfg_get('dotfiles', false),
|
||||||
treesz = icfg_get('treesz', 16);
|
treesz = clamp(icfg_get('treesz', 16), 4, 50);
|
||||||
|
|
||||||
treesz = Math.min(Math.max(treesz, 4), 50);
|
|
||||||
|
|
||||||
treectl.entree = function (e) {
|
treectl.entree = function (e) {
|
||||||
ev(e);
|
ev(e);
|
||||||
@@ -2816,7 +3031,7 @@ var treectl = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onscroll() {
|
function onscroll() {
|
||||||
if (!entreed || treectl.hidden)
|
if (!entreed || treectl.hidden || document.visibilityState == 'hidden')
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var tree = ebi('tree'),
|
var tree = ebi('tree'),
|
||||||
@@ -2852,12 +3067,7 @@ var treectl = (function () {
|
|||||||
tree.style.height = treeh < 10 ? '' : treeh + 'px';
|
tree.style.height = treeh < 10 ? '' : treeh + 'px';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
timer.add(onscroll, true);
|
||||||
function periodic() {
|
|
||||||
onscroll();
|
|
||||||
setTimeout(periodic, document.visibilityState ? 100 : 5000);
|
|
||||||
}
|
|
||||||
periodic();
|
|
||||||
|
|
||||||
function onresize(e) {
|
function onresize(e) {
|
||||||
if (!entreed || treectl.hidden)
|
if (!entreed || treectl.hidden)
|
||||||
@@ -3072,7 +3282,13 @@ var treectl = (function () {
|
|||||||
}
|
}
|
||||||
html.push('</tbody>');
|
html.push('</tbody>');
|
||||||
html = html.join('\n');
|
html = html.join('\n');
|
||||||
|
try {
|
||||||
ebi('files').innerHTML = html;
|
ebi('files').innerHTML = html;
|
||||||
|
}
|
||||||
|
catch (ex) { //ie9
|
||||||
|
window.location.href = this.top;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.hpush)
|
if (this.hpush)
|
||||||
hist_push(this.top);
|
hist_push(this.top);
|
||||||
@@ -3156,6 +3372,7 @@ var treectl = (function () {
|
|||||||
|
|
||||||
ebi('entree').onclick = treectl.entree;
|
ebi('entree').onclick = treectl.entree;
|
||||||
ebi('detree').onclick = treectl.detree;
|
ebi('detree').onclick = treectl.detree;
|
||||||
|
ebi('visdir').onclick = tree_scrollto;
|
||||||
ebi('dotfiles').onclick = tdots;
|
ebi('dotfiles').onclick = tdots;
|
||||||
ebi('dyntree').onclick = dyntree;
|
ebi('dyntree').onclick = dyntree;
|
||||||
ebi('twig').onclick = scaletree;
|
ebi('twig').onclick = scaletree;
|
||||||
@@ -3689,6 +3906,16 @@ var msel = (function () {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
r.sel = [];
|
r.sel = [];
|
||||||
|
if (r.all && r.all.length) {
|
||||||
|
for (var a = 0; a < r.all.length; a++) {
|
||||||
|
var ao = r.all[a];
|
||||||
|
ao.sel = clgot(ebi(ao.id).closest('tr'), 'sel');
|
||||||
|
if (ao.sel)
|
||||||
|
r.sel.push(ao);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
r.all = [];
|
r.all = [];
|
||||||
var links = QSA('#files tbody td:nth-child(2) a:last-child'),
|
var links = QSA('#files tbody td:nth-child(2) a:last-child'),
|
||||||
vbase = get_evpath();
|
vbase = get_evpath();
|
||||||
@@ -3698,7 +3925,7 @@ var msel = (function () {
|
|||||||
item = {};
|
item = {};
|
||||||
|
|
||||||
item.id = links[a].getAttribute('id');
|
item.id = links[a].getAttribute('id');
|
||||||
item.sel = links[a].closest('tr').classList.contains('sel');
|
item.sel = clgot(links[a].closest('tr'), 'sel');
|
||||||
item.vp = href.indexOf('/') !== -1 ? href : vbase + href;
|
item.vp = href.indexOf('/') !== -1 ? href : vbase + href;
|
||||||
|
|
||||||
r.all.push(item);
|
r.all.push(item);
|
||||||
@@ -3717,8 +3944,11 @@ var msel = (function () {
|
|||||||
r.load();
|
r.load();
|
||||||
return r.all;
|
return r.all;
|
||||||
};
|
};
|
||||||
r.selui = function () {
|
r.selui = function (reset) {
|
||||||
r.sel = r.all = null;
|
r.sel = null;
|
||||||
|
if (reset)
|
||||||
|
r.all = null;
|
||||||
|
|
||||||
clmod(ebi('wtoggle'), 'sel', r.getsel().length);
|
clmod(ebi('wtoggle'), 'sel', r.getsel().length);
|
||||||
thegrid.loadsel();
|
thegrid.loadsel();
|
||||||
fileman.render();
|
fileman.render();
|
||||||
@@ -3777,7 +4007,7 @@ var msel = (function () {
|
|||||||
for (var a = 0, aa = tds.length; a < aa; a++) {
|
for (var a = 0, aa = tds.length; a < aa; a++) {
|
||||||
tds[a].onclick = r.seltgl;
|
tds[a].onclick = r.seltgl;
|
||||||
}
|
}
|
||||||
r.selui();
|
r.selui(true);
|
||||||
arcfmt.render();
|
arcfmt.render();
|
||||||
fileman.render();
|
fileman.render();
|
||||||
ebi('selzip').style.display = ebi('unsearch') ? 'none' : '';
|
ebi('selzip').style.display = ebi('unsearch') ? 'none' : '';
|
||||||
|
|||||||
@@ -8,140 +8,14 @@ html, body {
|
|||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
#repl {
|
||||||
|
|
||||||
|
|
||||||
#tt, #toast {
|
|
||||||
position: fixed;
|
|
||||||
max-width: 34em;
|
|
||||||
background: #222;
|
|
||||||
border: 0 solid #777;
|
|
||||||
box-shadow: 0 .2em .5em #222;
|
|
||||||
border-radius: .4em;
|
|
||||||
z-index: 9001;
|
|
||||||
}
|
|
||||||
#tt {
|
|
||||||
overflow: hidden;
|
|
||||||
margin-top: 1em;
|
|
||||||
padding: 0 1.3em;
|
|
||||||
height: 0;
|
|
||||||
opacity: .1;
|
|
||||||
transition: opacity 0.14s, height 0.14s, padding 0.14s;
|
|
||||||
}
|
|
||||||
#toast {
|
|
||||||
top: 1.4em;
|
|
||||||
right: -1em;
|
|
||||||
line-height: 1.5em;
|
|
||||||
padding: 1em 1.3em;
|
|
||||||
border-width: .4em 0;
|
|
||||||
transform: translateX(100%);
|
|
||||||
transition:
|
|
||||||
transform .4s cubic-bezier(.2, 1.2, .5, 1),
|
|
||||||
right .4s cubic-bezier(.2, 1.2, .5, 1);
|
|
||||||
text-shadow: 1px 1px 0 #000;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
#toast pre {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
#toastc {
|
|
||||||
display: inline-block;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
overflow: hidden;
|
top: 0;
|
||||||
left: 0;
|
right: .2em;
|
||||||
width: 0;
|
|
||||||
opacity: 0;
|
|
||||||
padding: .3em 0;
|
|
||||||
margin: -.3em 0 0 0;
|
|
||||||
line-height: 1.5em;
|
|
||||||
color: #000;
|
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
color: inherit;
|
||||||
text-shadow: none;
|
background: none;
|
||||||
border-radius: .5em 0 0 .5em;
|
|
||||||
transition: left .3s, width .3s, padding .3s, opacity .3s;
|
|
||||||
}
|
}
|
||||||
#toast.vis {
|
|
||||||
right: 1.3em;
|
|
||||||
transform: unset;
|
|
||||||
}
|
|
||||||
#toast.vis #toastc {
|
|
||||||
left: -2em;
|
|
||||||
width: .4em;
|
|
||||||
padding: .3em .8em;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
#toast.inf {
|
|
||||||
background: #07a;
|
|
||||||
border-color: #0be;
|
|
||||||
}
|
|
||||||
#toast.inf #toastc {
|
|
||||||
background: #0be;
|
|
||||||
}
|
|
||||||
#toast.ok {
|
|
||||||
background: #4a0;
|
|
||||||
border-color: #8e4;
|
|
||||||
}
|
|
||||||
#toast.ok #toastc {
|
|
||||||
background: #8e4;
|
|
||||||
}
|
|
||||||
#toast.warn {
|
|
||||||
background: #970;
|
|
||||||
border-color: #fc0;
|
|
||||||
}
|
|
||||||
#toast.warn #toastc {
|
|
||||||
background: #fc0;
|
|
||||||
}
|
|
||||||
#toast.err {
|
|
||||||
background: #900;
|
|
||||||
border-color: #d06;
|
|
||||||
}
|
|
||||||
#toast.err #toastc {
|
|
||||||
background: #d06;
|
|
||||||
}
|
|
||||||
#tt.b {
|
|
||||||
padding: 0 2em;
|
|
||||||
border-radius: .5em;
|
|
||||||
box-shadow: 0 .2em 1em #000;
|
|
||||||
}
|
|
||||||
#tt.show {
|
|
||||||
padding: 1em 1.3em;
|
|
||||||
border-width: .4em 0;
|
|
||||||
height: auto;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
#tt.show.b {
|
|
||||||
padding: 1.5em 2em;
|
|
||||||
border-width: .5em 0;
|
|
||||||
}
|
|
||||||
#tt code {
|
|
||||||
background: #3c3c3c;
|
|
||||||
padding: .1em .3em;
|
|
||||||
border-top: 1px solid #777;
|
|
||||||
border-radius: .3em;
|
|
||||||
line-height: 1.7em;
|
|
||||||
}
|
|
||||||
#tt em {
|
|
||||||
color: #f6a;
|
|
||||||
}
|
|
||||||
html.light #tt {
|
|
||||||
background: #fff;
|
|
||||||
border-color: #888 #000 #777 #000;
|
|
||||||
}
|
|
||||||
html.light #tt,
|
|
||||||
html.light #toast {
|
|
||||||
box-shadow: 0 .3em 1em rgba(0,0,0,0.4);
|
|
||||||
}
|
|
||||||
html.light #tt code {
|
|
||||||
background: #060;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
html.light #tt em {
|
|
||||||
color: #d38;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#mtw {
|
#mtw {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -149,6 +23,10 @@ html.light #tt em {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 1.5em;
|
padding: 0 1.5em;
|
||||||
}
|
}
|
||||||
|
#toast {
|
||||||
|
bottom: auto;
|
||||||
|
top: 1.4em;
|
||||||
|
}
|
||||||
pre, code, a {
|
pre, code, a {
|
||||||
color: #480;
|
color: #480;
|
||||||
background: #f7f7f7;
|
background: #f7f7f7;
|
||||||
@@ -636,6 +514,9 @@ blink {
|
|||||||
border-bottom: .07em solid #4ac;
|
border-bottom: .07em solid #4ac;
|
||||||
padding: 0 .3em;
|
padding: 0 .3em;
|
||||||
}
|
}
|
||||||
|
#repl {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
#toc>ul {
|
#toc>ul {
|
||||||
border-left: .1em solid #84c4dd;
|
border-left: .1em solid #84c4dd;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<!DOCTYPE html><html><head>
|
<!DOCTYPE html><html><head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>📝🎉 {{ title }}</title> <!-- 📜 -->
|
<title>📝🎉 {{ title }}</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
||||||
<link href="/.cpr/md.css?_={{ ts }}" rel="stylesheet">
|
<link rel="stylesheet" href="/.cpr/ui.css?_={{ ts }}">
|
||||||
|
<link rel="stylesheet" href="/.cpr/md.css?_={{ ts }}">
|
||||||
{%- if edit %}
|
{%- if edit %}
|
||||||
<link href="/.cpr/md2.css?_={{ ts }}" rel="stylesheet">
|
<link rel="stylesheet" href="/.cpr/md2.css?_={{ ts }}">
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="mp"></div>
|
<div id="mp"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<a href="#" id="repl">π</a>
|
||||||
|
|
||||||
{%- if edit %}
|
{%- if edit %}
|
||||||
<div id="helpbox">
|
<div id="helpbox">
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ function md_plug_err(ex, js) {
|
|||||||
errbox.style.cssText = 'position:absolute;top:0;left:0;padding:1em .5em;background:#2b2b2b;color:#fc5'
|
errbox.style.cssText = 'position:absolute;top:0;left:0;padding:1em .5em;background:#2b2b2b;color:#fc5'
|
||||||
errbox.textContent = msg;
|
errbox.textContent = msg;
|
||||||
errbox.onclick = function () {
|
errbox.onclick = function () {
|
||||||
alert('' + ex.stack);
|
modal.alert('<pre>' + esc(ex.stack) + '</pre>');
|
||||||
};
|
};
|
||||||
if (o) {
|
if (o) {
|
||||||
errbox.appendChild(o);
|
errbox.appendChild(o);
|
||||||
|
|||||||
@@ -326,12 +326,10 @@ function save(e) {
|
|||||||
return toast.inf(2, "no changes");
|
return toast.inf(2, "no changes");
|
||||||
|
|
||||||
var force = (save_cls.indexOf('force-save') >= 0);
|
var force = (save_cls.indexOf('force-save') >= 0);
|
||||||
if (force && !confirm('confirm that you wish to lose the changes made on the server since you opened this document'))
|
function save2() {
|
||||||
return toast.inf(3, 'aborted');
|
var txt = dom_src.value,
|
||||||
|
fd = new FormData();
|
||||||
|
|
||||||
var txt = dom_src.value;
|
|
||||||
|
|
||||||
var fd = new FormData();
|
|
||||||
fd.append("act", "tput");
|
fd.append("act", "tput");
|
||||||
fd.append("lastmod", (force ? -1 : last_modified));
|
fd.append("lastmod", (force ? -1 : last_modified));
|
||||||
fd.append("body", txt);
|
fd.append("body", txt);
|
||||||
@@ -348,24 +346,32 @@ function save(e) {
|
|||||||
xhr.send(fd);
|
xhr.send(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!force)
|
||||||
|
save2();
|
||||||
|
else
|
||||||
|
modal.confirm('confirm that you wish to lose the changes made on the server since you opened this document', save2, function () {
|
||||||
|
toast.inf(3, 'aborted');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function save_cb() {
|
function save_cb() {
|
||||||
if (this.readyState != XMLHttpRequest.DONE)
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.status !== 200)
|
if (this.status !== 200)
|
||||||
return alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||||
|
|
||||||
var r;
|
var r;
|
||||||
try {
|
try {
|
||||||
r = JSON.parse(this.responseText);
|
r = JSON.parse(this.responseText);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
return alert('Failed to parse reply from server:\n\n' + this.responseText);
|
return toast.err(0, 'Failed to parse reply from server:\n\n' + this.responseText);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
if (!this.btn.classList.contains('force-save')) {
|
if (!clgot(this.btn, 'force-save')) {
|
||||||
this.btn.classList.add('force-save');
|
clmod(this.btn, 'force-save', 1);
|
||||||
var msg = [
|
var msg = [
|
||||||
'This file has been modified since you started editing it!\n',
|
'This file has been modified since you started editing it!\n',
|
||||||
'if you really want to overwrite, press save again.\n',
|
'if you really want to overwrite, press save again.\n',
|
||||||
@@ -375,15 +381,13 @@ function save_cb() {
|
|||||||
r.lastmod + ' lastmod on the server now,',
|
r.lastmod + ' lastmod on the server now,',
|
||||||
r.now + ' server time now,\n',
|
r.now + ' server time now,\n',
|
||||||
];
|
];
|
||||||
alert(msg.join('\n'));
|
return toast.err(0, msg.join('\n'));
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
alert('Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
|
return toast.err(0, 'Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.btn.classList.remove('force-save');
|
clmod(this.btn, 'force-save');
|
||||||
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
|
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
|
||||||
|
|
||||||
run_savechk(r.lastmod, this.txt, this.btn, 0);
|
run_savechk(r.lastmod, this.txt, this.btn, 0);
|
||||||
@@ -407,10 +411,8 @@ function savechk_cb() {
|
|||||||
if (this.readyState != XMLHttpRequest.DONE)
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.status !== 200) {
|
if (this.status !== 200)
|
||||||
alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var doc1 = this.txt.replace(/\r\n/g, "\n");
|
var doc1 = this.txt.replace(/\r\n/g, "\n");
|
||||||
var doc2 = this.responseText.replace(/\r\n/g, "\n");
|
var doc2 = this.responseText.replace(/\r\n/g, "\n");
|
||||||
@@ -423,12 +425,12 @@ function savechk_cb() {
|
|||||||
}, 100);
|
}, 100);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
alert(
|
modal.alert(
|
||||||
'Error! The document on the server does not appear to have saved correctly (your editor contents and the server copy is not identical). Place the document on your clipboard for now and check the server logs for hints\n\n' +
|
'Error! The document on the server does not appear to have saved correctly (your editor contents and the server copy is not identical). Place the document on your clipboard for now and check the server logs for hints\n\n' +
|
||||||
'Length: yours=' + doc1.length + ', server=' + doc2.length
|
'Length: yours=' + doc1.length + ', server=' + doc2.length
|
||||||
);
|
);
|
||||||
alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']');
|
modal.alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']');
|
||||||
alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']');
|
modal.alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -865,12 +867,10 @@ function iter_uni(e) {
|
|||||||
function cfg_uni(e) {
|
function cfg_uni(e) {
|
||||||
if (e) e.preventDefault();
|
if (e) e.preventDefault();
|
||||||
|
|
||||||
var reply = prompt("unicode whitelist", esc_uni_whitelist);
|
modal.prompt("unicode whitelist", esc_uni_whitelist, function (reply) {
|
||||||
if (reply === null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
esc_uni_whitelist = reply;
|
esc_uni_whitelist = reply;
|
||||||
js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\'');
|
js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\'');
|
||||||
|
}, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,19 @@ html, body {
|
|||||||
background: #f7f7f7;
|
background: #f7f7f7;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
#toast {
|
||||||
|
bottom: auto;
|
||||||
|
top: 1.4em;
|
||||||
|
}
|
||||||
|
#repl {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: .5em;
|
||||||
|
border: none;
|
||||||
|
color: inherit;
|
||||||
|
background: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
#mn {
|
#mn {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin: 1.3em 0 .7em 1em;
|
margin: 1.3em 0 .7em 1em;
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
<title>📝🎉 {{ title }}</title>
|
<title>📝🎉 {{ title }}</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
||||||
<link href="/.cpr/mde.css?_={{ ts }}" rel="stylesheet">
|
<link rel="stylesheet" href="/.cpr/ui.css?_={{ ts }}">
|
||||||
<link href="/.cpr/deps/mini-fa.css?_={{ ts }}" rel="stylesheet">
|
<link rel="stylesheet" href="/.cpr/mde.css?_={{ ts }}">
|
||||||
<link href="/.cpr/deps/easymde.css?_={{ ts }}" rel="stylesheet">
|
<link rel="stylesheet" href="/.cpr/deps/mini-fa.css?_={{ ts }}">
|
||||||
|
<link rel="stylesheet" href="/.cpr/deps/easymde.css?_={{ ts }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="mw">
|
<div id="mw">
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
<textarea id="mt" style="display:none" autocomplete="off">{{ md }}</textarea>
|
<textarea id="mt" style="display:none" autocomplete="off">{{ md }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<a href="#" id="repl">π</a>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
var last_modified = {{ lastmod }};
|
var last_modified = {{ lastmod }};
|
||||||
|
|||||||
@@ -96,23 +96,17 @@ function md_changed(mde, on_srv) {
|
|||||||
var md_now = mde.value();
|
var md_now = mde.value();
|
||||||
var save_btn = QS('.editor-toolbar button.save');
|
var save_btn = QS('.editor-toolbar button.save');
|
||||||
|
|
||||||
if (md_now == window.md_saved)
|
clmod(save_btn, 'disabled', md_now == window.md_saved);
|
||||||
save_btn.classList.add('disabled');
|
|
||||||
else
|
|
||||||
save_btn.classList.remove('disabled');
|
|
||||||
|
|
||||||
set_jumpto();
|
set_jumpto();
|
||||||
}
|
}
|
||||||
|
|
||||||
function save(mde) {
|
function save(mde) {
|
||||||
var save_btn = QS('.editor-toolbar button.save');
|
var save_btn = QS('.editor-toolbar button.save');
|
||||||
if (save_btn.classList.contains('disabled'))
|
if (clgot(save_btn, 'disabled'))
|
||||||
return toast.inf(2, 'no changes');
|
return toast.inf(2, 'no changes');
|
||||||
|
|
||||||
var force = save_btn.classList.contains('force-save');
|
var force = clgot(save_btn, 'force-save');
|
||||||
if (force && !confirm('confirm that you wish to lose the changes made on the server since you opened this document'))
|
function save2() {
|
||||||
return toast.inf(3, 'aborted');
|
|
||||||
|
|
||||||
var txt = mde.value();
|
var txt = mde.value();
|
||||||
|
|
||||||
var fd = new FormData();
|
var fd = new FormData();
|
||||||
@@ -131,24 +125,32 @@ function save(mde) {
|
|||||||
xhr.send(fd);
|
xhr.send(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!force)
|
||||||
|
save2();
|
||||||
|
else
|
||||||
|
modal.confirm('confirm that you wish to lose the changes made on the server since you opened this document', save2, function () {
|
||||||
|
toast.inf(3, 'aborted');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function save_cb() {
|
function save_cb() {
|
||||||
if (this.readyState != XMLHttpRequest.DONE)
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.status !== 200)
|
if (this.status !== 200)
|
||||||
return alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||||
|
|
||||||
var r;
|
var r;
|
||||||
try {
|
try {
|
||||||
r = JSON.parse(this.responseText);
|
r = JSON.parse(this.responseText);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
return alert('Failed to parse reply from server:\n\n' + this.responseText);
|
return toast.err(0, 'Failed to parse reply from server:\n\n' + this.responseText);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
if (!this.btn.classList.contains('force-save')) {
|
if (!clgot(this.btn, 'force-save')) {
|
||||||
this.btn.classList.add('force-save');
|
clmod(this.btn, 'force-save', 1);
|
||||||
var msg = [
|
var msg = [
|
||||||
'This file has been modified since you started editing it!\n',
|
'This file has been modified since you started editing it!\n',
|
||||||
'if you really want to overwrite, press save again.\n',
|
'if you really want to overwrite, press save again.\n',
|
||||||
@@ -158,15 +160,13 @@ function save_cb() {
|
|||||||
r.lastmod + ' lastmod on the server now,',
|
r.lastmod + ' lastmod on the server now,',
|
||||||
r.now + ' server time now,\n',
|
r.now + ' server time now,\n',
|
||||||
];
|
];
|
||||||
alert(msg.join('\n'));
|
return toast.err(0, msg.join('\n'));
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
alert('Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
|
return toast.err(0, 'Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.btn.classList.remove('force-save');
|
clmod(this.btn, 'force-save');
|
||||||
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
|
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
|
||||||
|
|
||||||
// download the saved doc from the server and compare
|
// download the saved doc from the server and compare
|
||||||
@@ -186,35 +186,23 @@ function save_chk() {
|
|||||||
if (this.readyState != XMLHttpRequest.DONE)
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.status !== 200) {
|
if (this.status !== 200)
|
||||||
alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var doc1 = this.txt.replace(/\r\n/g, "\n");
|
var doc1 = this.txt.replace(/\r\n/g, "\n");
|
||||||
var doc2 = this.responseText.replace(/\r\n/g, "\n");
|
var doc2 = this.responseText.replace(/\r\n/g, "\n");
|
||||||
if (doc1 != doc2) {
|
if (doc1 != doc2) {
|
||||||
alert(
|
modal.alert(
|
||||||
'Error! The document on the server does not appear to have saved correctly (your editor contents and the server copy is not identical). Place the document on your clipboard for now and check the server logs for hints\n\n' +
|
'Error! The document on the server does not appear to have saved correctly (your editor contents and the server copy is not identical). Place the document on your clipboard for now and check the server logs for hints\n\n' +
|
||||||
'Length: yours=' + doc1.length + ', server=' + doc2.length
|
'Length: yours=' + doc1.length + ', server=' + doc2.length
|
||||||
);
|
);
|
||||||
alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']');
|
modal.alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']');
|
||||||
alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']');
|
modal.alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
last_modified = this.lastmod;
|
last_modified = this.lastmod;
|
||||||
md_changed(this.mde, true);
|
md_changed(this.mde, true);
|
||||||
|
|
||||||
var ok = mknod('div');
|
toast.ok(2, 'save OK' + (this.ntry ? '\nattempt ' + this.ntry : ''));
|
||||||
ok.setAttribute('style', 'font-size:6em;font-family:serif;font-weight:bold;color:#cf6;background:#444;border-radius:.3em;padding:.6em 0;position:fixed;top:30%;left:calc(50% - 2em);width:4em;text-align:center;z-index:9001;transition:opacity 0.2s ease-in-out;opacity:1');
|
|
||||||
ok.innerHTML = 'OK✔️';
|
|
||||||
var parent = ebi('m');
|
|
||||||
document.documentElement.appendChild(ok);
|
|
||||||
setTimeout(function () {
|
|
||||||
ok.style.opacity = 0;
|
|
||||||
}, 500);
|
|
||||||
setTimeout(function () {
|
|
||||||
ok.parentNode.removeChild(ok);
|
|
||||||
}, 750);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ html {
|
|||||||
background: #333;
|
background: #333;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
text-shadow: 1px 1px 0px #000;
|
text-shadow: 1px 1px 0px #000;
|
||||||
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<title>copyparty</title>
|
<title>copyparty</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/msg.css?_={{ ts }}">
|
<link rel="stylesheet" media="screen" href="/.cpr/msg.css?_={{ ts }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ html, body, #wrap {
|
|||||||
background: #f7f7f7;
|
background: #f7f7f7;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
|
html {
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
#wrap {
|
#wrap {
|
||||||
max-width: 40em;
|
max-width: 40em;
|
||||||
margin: 2em auto;
|
margin: 2em auto;
|
||||||
@@ -26,6 +29,12 @@ a {
|
|||||||
border-radius: .2em;
|
border-radius: .2em;
|
||||||
padding: .2em .8em;
|
padding: .2em .8em;
|
||||||
}
|
}
|
||||||
|
#repl {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
table {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
<title>copyparty</title>
|
<title>copyparty</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/splash.css?_={{ ts }}">
|
<link rel="stylesheet" media="screen" href="/.cpr/splash.css?_={{ ts }}">
|
||||||
|
<link rel="stylesheet" media="screen" href="/.cpr/ui.css?_={{ ts }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -66,11 +67,13 @@
|
|||||||
</form>
|
</form>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<a href="#" id="repl">π</a>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
if (localStorage.getItem('lightmode') != 1)
|
if (localStorage.getItem('lightmode') != 1)
|
||||||
document.documentElement.setAttribute("class", "dark");
|
document.documentElement.setAttribute("class", "dark");
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
<script src="/.cpr/util.js?_={{ ts }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
232
copyparty/web/ui.css
Normal file
232
copyparty/web/ui.css
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
html {
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
#tt, #toast {
|
||||||
|
position: fixed;
|
||||||
|
max-width: 34em;
|
||||||
|
max-width: min(34em, 90%);
|
||||||
|
max-width: min(34em, calc(100% - 7em));
|
||||||
|
background: #222;
|
||||||
|
border: 0 solid #777;
|
||||||
|
box-shadow: 0 .2em .5em #222;
|
||||||
|
border-radius: .4em;
|
||||||
|
z-index: 9001;
|
||||||
|
}
|
||||||
|
#tt {
|
||||||
|
max-width: min(34em, calc(100% - 3.3em));
|
||||||
|
overflow: hidden;
|
||||||
|
margin: .7em 0;
|
||||||
|
padding: 0 1.3em;
|
||||||
|
height: 0;
|
||||||
|
opacity: .1;
|
||||||
|
transition: opacity 0.14s, height 0.14s, padding 0.14s;
|
||||||
|
}
|
||||||
|
#toast {
|
||||||
|
bottom: 5em;
|
||||||
|
right: -1em;
|
||||||
|
line-height: 1.5em;
|
||||||
|
padding: 1em 1.3em;
|
||||||
|
margin-left: 3em;
|
||||||
|
border-width: .4em 0;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition:
|
||||||
|
transform .4s cubic-bezier(.2, 1.2, .5, 1),
|
||||||
|
right .4s cubic-bezier(.2, 1.2, .5, 1);
|
||||||
|
text-shadow: 1px 1px 0 #000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
#toast a {
|
||||||
|
color: inherit;
|
||||||
|
text-shadow: inherit;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border-radius: .3em;
|
||||||
|
padding: .2em .3em;
|
||||||
|
}
|
||||||
|
#toast a#toastc {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
overflow: hidden;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
padding: .3em 0;
|
||||||
|
margin: -.3em 0 0 0;
|
||||||
|
line-height: 1.5em;
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
text-shadow: none;
|
||||||
|
border-radius: .5em 0 0 .5em;
|
||||||
|
transition: left .3s, width .3s, padding .3s, opacity .3s;
|
||||||
|
}
|
||||||
|
#toast pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#toast.vis {
|
||||||
|
right: 1.3em;
|
||||||
|
transform: unset;
|
||||||
|
}
|
||||||
|
#toast.vis #toastc {
|
||||||
|
left: -2em;
|
||||||
|
width: .4em;
|
||||||
|
padding: .3em .8em;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
#toast.inf {
|
||||||
|
background: #07a;
|
||||||
|
border-color: #0be;
|
||||||
|
}
|
||||||
|
#toast.inf #toastc {
|
||||||
|
background: #0be;
|
||||||
|
}
|
||||||
|
#toast.ok {
|
||||||
|
background: #380;
|
||||||
|
border-color: #8e4;
|
||||||
|
}
|
||||||
|
#toast.ok #toastc {
|
||||||
|
background: #8e4;
|
||||||
|
}
|
||||||
|
#toast.warn {
|
||||||
|
background: #960;
|
||||||
|
border-color: #fc0;
|
||||||
|
}
|
||||||
|
#toast.warn #toastc {
|
||||||
|
background: #fc0;
|
||||||
|
}
|
||||||
|
#toast.err {
|
||||||
|
background: #900;
|
||||||
|
border-color: #d06;
|
||||||
|
}
|
||||||
|
#toast.err #toastc {
|
||||||
|
background: #d06;
|
||||||
|
}
|
||||||
|
#tt.b {
|
||||||
|
padding: 0 2em;
|
||||||
|
border-radius: .5em;
|
||||||
|
box-shadow: 0 .2em 1em #000;
|
||||||
|
}
|
||||||
|
#tt.show {
|
||||||
|
padding: 1em 1.3em;
|
||||||
|
border-width: .4em 0;
|
||||||
|
height: auto;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
#tt.show.b {
|
||||||
|
padding: 1.5em 2em;
|
||||||
|
border-width: .5em 0;
|
||||||
|
}
|
||||||
|
#modalc code,
|
||||||
|
#tt code {
|
||||||
|
background: #3c3c3c;
|
||||||
|
padding: .1em .3em;
|
||||||
|
border-top: 1px solid #777;
|
||||||
|
border-radius: .3em;
|
||||||
|
line-height: 1.7em;
|
||||||
|
}
|
||||||
|
#tt em {
|
||||||
|
color: #f6a;
|
||||||
|
}
|
||||||
|
html.light #tt {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #888 #000 #777 #000;
|
||||||
|
}
|
||||||
|
html.light #tt,
|
||||||
|
html.light #toast {
|
||||||
|
box-shadow: 0 .3em 1em rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
#modalc code,
|
||||||
|
html.light #tt code {
|
||||||
|
background: #060;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
html.light #tt em {
|
||||||
|
color: #d38;
|
||||||
|
}
|
||||||
|
#modal {
|
||||||
|
position: fixed;
|
||||||
|
overflow: auto;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 9001;
|
||||||
|
background: rgba(64,64,64,0.6);
|
||||||
|
}
|
||||||
|
#modal>table {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#modal td {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#modalc {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
background: #f7f7f7;
|
||||||
|
color: #333;
|
||||||
|
text-shadow: none;
|
||||||
|
text-align: left;
|
||||||
|
margin: 3em;
|
||||||
|
padding: 1em 1.1em;
|
||||||
|
border-radius: .6em;
|
||||||
|
box-shadow: 0 .3em 3em rgba(0,0,0,0.5);
|
||||||
|
max-width: 50em;
|
||||||
|
max-height: 30em;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
#modalc {
|
||||||
|
min-width: 30em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#modalc li {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
#modalc h6 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
border-bottom: 1px solid #999;
|
||||||
|
margin: 0;
|
||||||
|
padding: .3em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#modalb {
|
||||||
|
position: sticky;
|
||||||
|
text-align: right;
|
||||||
|
padding-top: 1em;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
#modalb a {
|
||||||
|
color: #000;
|
||||||
|
background: #ccc;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: .3em;
|
||||||
|
padding: .5em 1em;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
#modalb a:focus,
|
||||||
|
#modalb a:hover {
|
||||||
|
background: #06d;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
#modalb a+a {
|
||||||
|
margin-left: .5em;
|
||||||
|
}
|
||||||
|
#modali {
|
||||||
|
display: block;
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
width: calc(100% - 1.25em);
|
||||||
|
margin: 1em -.1em 0 -.1em;
|
||||||
|
padding: .5em;
|
||||||
|
outline: none;
|
||||||
|
border: .25em solid #ccc;
|
||||||
|
border-radius: .4em;
|
||||||
|
}
|
||||||
|
#modali:focus {
|
||||||
|
border-color: #06d;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,302 +0,0 @@
|
|||||||
|
|
||||||
#op_up2k {
|
|
||||||
padding: 0 1em 1em 1em;
|
|
||||||
}
|
|
||||||
#u2form {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 2px;
|
|
||||||
height: 2px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
#u2form input {
|
|
||||||
background: #444;
|
|
||||||
border: 0px solid #444;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
#u2err.err {
|
|
||||||
color: #f87;
|
|
||||||
padding: .5em;
|
|
||||||
}
|
|
||||||
#u2err.msg {
|
|
||||||
color: #999;
|
|
||||||
padding: .5em;
|
|
||||||
font-size: .9em;
|
|
||||||
}
|
|
||||||
#u2btn {
|
|
||||||
color: #eee;
|
|
||||||
background: #555;
|
|
||||||
background: -moz-linear-gradient(top, #367 0%, #489 50%, #38788a 51%, #367 100%);
|
|
||||||
background: -webkit-linear-gradient(top, #367 0%, #489 50%, #38788a 51%, #367 100%);
|
|
||||||
background: linear-gradient(to bottom, #367 0%, #489 50%, #38788a 51%, #367 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#489', endColorstr='#38788a', GradientType=0);
|
|
||||||
text-decoration: none;
|
|
||||||
line-height: 1.3em;
|
|
||||||
border: 1px solid #222;
|
|
||||||
border-radius: .4em;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.5em;
|
|
||||||
margin: .5em auto;
|
|
||||||
padding: .8em 0;
|
|
||||||
width: 16em;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: .4em .4em 0 #111;
|
|
||||||
}
|
|
||||||
#op_up2k.srch #u2btn {
|
|
||||||
background: linear-gradient(to bottom, #ca3 0%, #fd8 50%, #fc6 51%, #b92 100%);
|
|
||||||
text-shadow: 1px 1px 1px #fc6;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
#u2conf #u2btn {
|
|
||||||
margin: -1.5em 0;
|
|
||||||
padding: .8em 0;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 12em;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
#u2conf #u2btn_cw {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
#u2notbtn {
|
|
||||||
display: none;
|
|
||||||
text-align: center;
|
|
||||||
background: #333;
|
|
||||||
padding-top: 1em;
|
|
||||||
}
|
|
||||||
#u2notbtn * {
|
|
||||||
line-height: 1.3em;
|
|
||||||
}
|
|
||||||
#u2tab {
|
|
||||||
margin: 3em auto;
|
|
||||||
width: calc(100% - 2em);
|
|
||||||
max-width: 100em;
|
|
||||||
}
|
|
||||||
#op_up2k.srch #u2tab {
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
#u2tab td {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-width: 0 0px 1px 0;
|
|
||||||
padding: .1em .3em;
|
|
||||||
}
|
|
||||||
#u2tab td:nth-child(2) {
|
|
||||||
width: 5em;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
#u2tab td:nth-child(3) {
|
|
||||||
width: 40%;
|
|
||||||
}
|
|
||||||
#op_up2k.srch td.prog {
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-size: 1em;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
#u2tab tbody tr:hover td {
|
|
||||||
background: #222;
|
|
||||||
}
|
|
||||||
#u2cards {
|
|
||||||
padding: 1em 0 .3em 1em;
|
|
||||||
margin: 1.5em auto -2.5em auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
#u2cards.w {
|
|
||||||
width: 45em;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
#u2cards a {
|
|
||||||
padding: .2em 1em;
|
|
||||||
border: 1px solid #777;
|
|
||||||
border-width: 0 0 1px 0;
|
|
||||||
background: linear-gradient(to bottom, #333, #222);
|
|
||||||
}
|
|
||||||
#u2cards a:first-child {
|
|
||||||
border-radius: .4em 0 0 0;
|
|
||||||
}
|
|
||||||
#u2cards a:last-child {
|
|
||||||
border-radius: 0 .4em 0 0;
|
|
||||||
}
|
|
||||||
#u2cards a.act {
|
|
||||||
padding-bottom: .5em;
|
|
||||||
border-width: 1px 1px .1em 1px;
|
|
||||||
border-radius: .3em .3em 0 0;
|
|
||||||
margin-left: -1px;
|
|
||||||
background: linear-gradient(to bottom, #464, #333 80%);
|
|
||||||
box-shadow: 0 -.17em .67em #280;
|
|
||||||
border-color: #7c5 #583 #333 #583;
|
|
||||||
position: relative;
|
|
||||||
color: #fd7;
|
|
||||||
}
|
|
||||||
#u2cards span {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
#u2conf {
|
|
||||||
margin: 1em auto;
|
|
||||||
width: 30em;
|
|
||||||
}
|
|
||||||
#u2conf.has_btn {
|
|
||||||
width: 48em;
|
|
||||||
}
|
|
||||||
#u2conf * {
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
#u2conf .txtbox {
|
|
||||||
width: 3em;
|
|
||||||
color: #fff;
|
|
||||||
background: #444;
|
|
||||||
border: 1px solid #777;
|
|
||||||
font-size: 1.2em;
|
|
||||||
padding: .15em 0;
|
|
||||||
height: 1.05em;
|
|
||||||
}
|
|
||||||
#u2conf .txtbox.err {
|
|
||||||
background: #922;
|
|
||||||
}
|
|
||||||
#u2conf a {
|
|
||||||
color: #fff;
|
|
||||||
background: #c38;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: .1em;
|
|
||||||
font-size: 1.5em;
|
|
||||||
padding: .1em 0;
|
|
||||||
margin: 0 -1px;
|
|
||||||
width: 1.5em;
|
|
||||||
height: 1em;
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
bottom: -0.08em;
|
|
||||||
}
|
|
||||||
#u2conf input+a {
|
|
||||||
background: #d80;
|
|
||||||
}
|
|
||||||
#u2conf label {
|
|
||||||
font-size: 1.6em;
|
|
||||||
width: 2em;
|
|
||||||
height: 1em;
|
|
||||||
padding: .4em 0;
|
|
||||||
display: block;
|
|
||||||
border-radius: .25em;
|
|
||||||
}
|
|
||||||
#u2conf input[type="checkbox"] {
|
|
||||||
position: relative;
|
|
||||||
opacity: .02;
|
|
||||||
top: 2em;
|
|
||||||
}
|
|
||||||
#u2conf input[type="checkbox"]+label {
|
|
||||||
position: relative;
|
|
||||||
background: #603;
|
|
||||||
border-bottom: .2em solid #a16;
|
|
||||||
box-shadow: 0 .1em .3em #a00 inset;
|
|
||||||
}
|
|
||||||
#u2conf input[type="checkbox"]:checked+label {
|
|
||||||
background: #6a1;
|
|
||||||
border-bottom: .2em solid #efa;
|
|
||||||
box-shadow: 0 .1em .5em #0c0;
|
|
||||||
}
|
|
||||||
#u2conf input[type="checkbox"]+label:hover {
|
|
||||||
box-shadow: 0 .1em .3em #fb0;
|
|
||||||
border-color: #fb0;
|
|
||||||
}
|
|
||||||
#op_up2k.srch #u2conf td:nth-child(1)>*,
|
|
||||||
#op_up2k.srch #u2conf td:nth-child(2)>*,
|
|
||||||
#op_up2k.srch #u2conf td:nth-child(3)>* {
|
|
||||||
background: #777;
|
|
||||||
border-color: #ccc;
|
|
||||||
box-shadow: none;
|
|
||||||
opacity: .2;
|
|
||||||
}
|
|
||||||
#u2foot {
|
|
||||||
color: #fff;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
#u2foot .warn {
|
|
||||||
font-size: 1.3em;
|
|
||||||
padding: .5em .8em;
|
|
||||||
margin: 1em -.6em;
|
|
||||||
color: #f74;
|
|
||||||
background: #322;
|
|
||||||
border: 1px solid #633;
|
|
||||||
border-width: .1em 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
#u2foot .warn span {
|
|
||||||
color: #f86;
|
|
||||||
}
|
|
||||||
html.light #u2foot .warn {
|
|
||||||
color: #b00;
|
|
||||||
background: #fca;
|
|
||||||
border-color: #f70;
|
|
||||||
}
|
|
||||||
html.light #u2foot .warn span {
|
|
||||||
color: #930;
|
|
||||||
}
|
|
||||||
#u2foot span {
|
|
||||||
color: #999;
|
|
||||||
font-size: .9em;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
#u2footfoot {
|
|
||||||
margin-bottom: -1em;
|
|
||||||
}
|
|
||||||
.prog {
|
|
||||||
font-family: monospace, monospace;
|
|
||||||
}
|
|
||||||
#u2tab a>span {
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: italic;
|
|
||||||
color: #fff;
|
|
||||||
padding-left: .2em;
|
|
||||||
}
|
|
||||||
#u2cleanup {
|
|
||||||
float: right;
|
|
||||||
margin-bottom: -.3em;
|
|
||||||
}
|
|
||||||
.fsearch_explain {
|
|
||||||
padding-left: .7em;
|
|
||||||
font-size: 1.1em;
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
html.light #u2btn {
|
|
||||||
box-shadow: .4em .4em 0 #ccc;
|
|
||||||
}
|
|
||||||
html.light #u2cards span {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
html.light #u2cards a {
|
|
||||||
background: linear-gradient(to bottom, #eee, #fff);
|
|
||||||
}
|
|
||||||
html.light #u2cards a.act {
|
|
||||||
color: #037;
|
|
||||||
background: inherit;
|
|
||||||
box-shadow: 0 -.17em .67em #0ad;
|
|
||||||
border-color: #09c #05a #eee #05a;
|
|
||||||
}
|
|
||||||
html.light #u2conf .txtbox {
|
|
||||||
background: #fff;
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
html.light #u2conf .txtbox.err {
|
|
||||||
background: #f96;
|
|
||||||
color: #300;
|
|
||||||
}
|
|
||||||
html.light #op_up2k.srch #u2btn {
|
|
||||||
border-color: #a80;
|
|
||||||
}
|
|
||||||
html.light #u2foot {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
html.light #u2tab tbody tr:hover td {
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
@@ -39,7 +39,8 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
|
|||||||
|
|
||||||
crashed = true;
|
crashed = true;
|
||||||
window.onerror = undefined;
|
window.onerror = undefined;
|
||||||
var html = ['<h1>you hit a bug!</h1><p style="font-size:1.3em;margin:0">try to <a href="#" onclick="localStorage.clear();location.reload();">reset copyparty settings</a> if you are stuck here, or <a href="#" onclick="ignex();">ignore this</a> / <a href="#" onclick="ignex(true);">ignore all</a></p><p>please send me a screenshot arigathanks gozaimuch: <code>ed/irc.rizon.net</code> or <code>ed#2644</code><br /> (and if you can, press F12 and include the "Console" tab in the screenshot too)</p><p>',
|
var con = is_touch ? '' : '<br /> (and if you can, press F12 and include the "Console" tab in the screenshot too)',
|
||||||
|
html = ['<h1>you hit a bug!</h1><p style="font-size:1.3em;margin:0">try to <a href="#" onclick="localStorage.clear();location.reload();">reset copyparty settings</a> if you are stuck here, or <a href="#" onclick="ignex();">ignore this</a> / <a href="#" onclick="ignex(true);">ignore all</a></p><p>please send me a screenshot arigathanks gozaimuch: <code>ed/irc.rizon.net</code> or <code>ed#2644</code>' + con + '</p><p>',
|
||||||
esc(url + ' @' + lineNo + ':' + columnNo), '<br />' + esc(String(msg)) + '</p>'];
|
esc(url + ' @' + lineNo + ':' + columnNo), '<br />' + esc(String(msg)) + '</p>'];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -63,10 +64,10 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
|
|||||||
document.body.appendChild(exbox);
|
document.body.appendChild(exbox);
|
||||||
|
|
||||||
var s = mknod('style');
|
var s = mknod('style');
|
||||||
s.innerHTML = '#exbox{background:#333;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em;z-index:80386;position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%} #exbox h1{margin:.5em 1em 0 0;padding:0} #exbox h3{border-top:1px solid #999;margin:1em 0 0 0} #exbox a{text-decoration:underline;color:#fc0} #exbox code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} #exbox *{line-height:1.5em}';
|
s.innerHTML = '#exbox{background:#333;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em;z-index:80386;position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;overflow:auto;width:calc(100% - 2em)} #exbox h1{margin:.5em 1em 0 0;padding:0} #exbox h3{border-top:1px solid #999;margin:1em 0 0 0} #exbox a{text-decoration:underline;color:#fc0} #exbox code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} #exbox,#exbox *{line-height:1.5em;overflow-wrap:break-word}';
|
||||||
document.head.appendChild(s);
|
document.head.appendChild(s);
|
||||||
}
|
}
|
||||||
exbox.innerHTML = html.join('\n');
|
exbox.innerHTML = html.join('\n').replace(/https?:\/\/[^ \/]+\//g, '/');
|
||||||
exbox.style.display = 'block';
|
exbox.style.display = 'block';
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
@@ -123,11 +124,18 @@ if (!String.startsWith) {
|
|||||||
return this.substring(i, i + s.length) === s;
|
return this.substring(i, i + s.length) === s;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (!Element.prototype.matches) {
|
||||||
|
Element.prototype.matches =
|
||||||
|
Element.prototype.oMatchesSelector ||
|
||||||
|
Element.prototype.msMatchesSelector ||
|
||||||
|
Element.prototype.mozMatchesSelector ||
|
||||||
|
Element.prototype.webkitMatchesSelector;
|
||||||
|
}
|
||||||
if (!Element.prototype.closest) {
|
if (!Element.prototype.closest) {
|
||||||
Element.prototype.closest = function (s) {
|
Element.prototype.closest = function (s) {
|
||||||
var el = this;
|
var el = this;
|
||||||
do {
|
do {
|
||||||
if (el.msMatchesSelector(s)) return el;
|
if (el.matches(s)) return el;
|
||||||
el = el.parentElement || el.parentNode;
|
el = el.parentElement || el.parentNode;
|
||||||
} while (el !== null && el.nodeType === 1);
|
} while (el !== null && el.nodeType === 1);
|
||||||
}
|
}
|
||||||
@@ -140,10 +148,10 @@ function import_js(url, cb) {
|
|||||||
var script = mknod('script');
|
var script = mknod('script');
|
||||||
script.type = 'text/javascript';
|
script.type = 'text/javascript';
|
||||||
script.src = url;
|
script.src = url;
|
||||||
|
|
||||||
script.onreadystatechange = cb;
|
|
||||||
script.onload = cb;
|
script.onload = cb;
|
||||||
|
script.onerror = function () {
|
||||||
|
toast.err(0, 'Failed to load module:\n' + url);
|
||||||
|
};
|
||||||
head.appendChild(script);
|
head.appendChild(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,90 +178,37 @@ function crc32(str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function clmod(obj, cls, add) {
|
function clmod(el, cls, add) {
|
||||||
var re = new RegExp('\\s*\\b' + cls + '\\s*\\b', 'g');
|
if (el.classList) {
|
||||||
|
var have = el.classList.contains(cls);
|
||||||
if (add == 't')
|
if (add == 't')
|
||||||
add = !re.test(obj.className);
|
add = !have;
|
||||||
|
|
||||||
obj.className = obj.className.replace(re, ' ') + (add ? ' ' + cls : '');
|
if (add != have)
|
||||||
|
el.classList[add ? 'add' : 'remove'](cls);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var re = new RegExp('\\s*\\b' + cls + '\\s*\\b', 'g'),
|
||||||
|
n1 = el.className;
|
||||||
|
|
||||||
|
if (add == 't')
|
||||||
|
add = !re.test(n1);
|
||||||
|
|
||||||
|
var n2 = n1.replace(re, ' ') + (add ? ' ' + cls : '');
|
||||||
|
|
||||||
|
if (n1 != n2)
|
||||||
|
el.className = n2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function sortfiles(nodes) {
|
function clgot(el, cls) {
|
||||||
var sopts = jread('fsort', [["href", 1, ""]]);
|
if (el.classList)
|
||||||
|
return el.classList.contains(cls);
|
||||||
|
|
||||||
try {
|
var lst = (el.getAttribute('class') + '').split(/ /g);
|
||||||
var is_srch = false;
|
return has(lst, cls);
|
||||||
if (nodes[0]['rp']) {
|
|
||||||
is_srch = true;
|
|
||||||
for (var b = 0, bb = nodes.length; b < bb; b++)
|
|
||||||
nodes[b].ext = nodes[b].rp.split('.').pop();
|
|
||||||
for (var b = 0; b < sopts.length; b++)
|
|
||||||
if (sopts[b][0] == 'href')
|
|
||||||
sopts[b][0] = 'rp';
|
|
||||||
}
|
|
||||||
for (var a = sopts.length - 1; a >= 0; a--) {
|
|
||||||
var name = sopts[a][0], rev = sopts[a][1], typ = sopts[a][2];
|
|
||||||
if (!name)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (name == 'ts')
|
|
||||||
typ = 'int';
|
|
||||||
|
|
||||||
if (name.indexOf('tags/') === 0) {
|
|
||||||
name = name.slice(5);
|
|
||||||
for (var b = 0, bb = nodes.length; b < bb; b++)
|
|
||||||
nodes[b]._sv = nodes[b].tags[name];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
for (var b = 0, bb = nodes.length; b < bb; b++) {
|
|
||||||
var v = nodes[b][name];
|
|
||||||
|
|
||||||
if ((v + '').indexOf('<a ') === 0)
|
|
||||||
v = v.split('>')[1];
|
|
||||||
else if (name == "href" && v) {
|
|
||||||
if (v.slice(-1) == '/')
|
|
||||||
v = '\t' + v;
|
|
||||||
|
|
||||||
v = uricom_dec(v)[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes[b]._sv = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var onodes = nodes.map(function (x) { return x; });
|
|
||||||
nodes.sort(function (n1, n2) {
|
|
||||||
var v1 = n1._sv,
|
|
||||||
v2 = n2._sv;
|
|
||||||
|
|
||||||
if (v1 === undefined) {
|
|
||||||
if (v2 === undefined) {
|
|
||||||
return onodes.indexOf(n1) - onodes.indexOf(n2);
|
|
||||||
}
|
|
||||||
return -1 * rev;
|
|
||||||
}
|
|
||||||
if (v2 === undefined) return 1 * rev;
|
|
||||||
|
|
||||||
var ret = rev * (typ == 'int' ? (v1 - v2) : (v1.localeCompare(v2)));
|
|
||||||
if (ret === 0)
|
|
||||||
ret = onodes.indexOf(n1) - onodes.indexOf(n2);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for (var b = 0, bb = nodes.length; b < bb; b++) {
|
|
||||||
delete nodes[b]._sv;
|
|
||||||
if (is_srch)
|
|
||||||
delete nodes[b].ext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
console.log("failed to apply sort config: " + ex);
|
|
||||||
console.log("resetting fsort " + sread('fsort'))
|
|
||||||
localStorage.removeItem('fsort');
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -386,6 +341,16 @@ function uricom_enc(txt, do_fb_enc) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function url_enc(txt) {
|
||||||
|
var parts = txt.split('/'),
|
||||||
|
ret = [];
|
||||||
|
|
||||||
|
for (var a = 0; a < parts.length; a++)
|
||||||
|
ret.push(uricom_enc(parts[a]));
|
||||||
|
|
||||||
|
return ret.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function uricom_dec(txt) {
|
function uricom_dec(txt) {
|
||||||
try {
|
try {
|
||||||
@@ -398,10 +363,12 @@ function uricom_dec(txt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function uricom_adec(arr) {
|
function uricom_adec(arr, li) {
|
||||||
var ret = [];
|
var ret = [];
|
||||||
for (var a = 0; a < arr.length; a++)
|
for (var a = 0; a < arr.length; a++) {
|
||||||
ret.push(uricom_dec(arr[a])[0]);
|
var txt = uricom_dec(arr[a])[0];
|
||||||
|
ret.push(li ? '<li>' + esc(txt) + '</li>' : txt);
|
||||||
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -446,6 +413,41 @@ function s2ms(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function f2f(val, nd) {
|
||||||
|
// 10.toFixed(1) returns 10.00 for certain values of 10
|
||||||
|
val = (val * Math.pow(10, nd)).toFixed(0).split('.')[0];
|
||||||
|
return nd ? (val.slice(0, -nd) || '0') + '.' + val.slice(-nd) : val;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function humansize(b, terse) {
|
||||||
|
var i = 0, u = terse ? ['B', 'K', 'M', 'G'] : ['B', 'KB', 'MB', 'GB'];
|
||||||
|
while (b >= 1000 && i < u.length) {
|
||||||
|
b /= 1024;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
return f2f(b, b >= 100 ? 0 : b >= 10 ? 1 : 2) + ' ' + u[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function humantime(v) {
|
||||||
|
if (v >= 60 * 60 * 24)
|
||||||
|
return v;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return /.*(..:..:..).*/.exec(new Date(v * 1000).toUTCString())[1];
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function clamp(v, a, b) {
|
||||||
|
return Math.min(Math.max(v, a), b);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function has(haystack, needle) {
|
function has(haystack, needle) {
|
||||||
for (var a = 0; a < haystack.length; a++)
|
for (var a = 0; a < haystack.length; a++)
|
||||||
if (haystack[a] == needle)
|
if (haystack[a] == needle)
|
||||||
@@ -555,6 +557,39 @@ function hist_replace(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var timer = (function () {
|
||||||
|
var r = {};
|
||||||
|
r.q = [];
|
||||||
|
r.last = 0;
|
||||||
|
|
||||||
|
r.add = function (fun, run) {
|
||||||
|
r.rm(fun);
|
||||||
|
r.q.push(fun);
|
||||||
|
|
||||||
|
if (run)
|
||||||
|
fun();
|
||||||
|
};
|
||||||
|
|
||||||
|
r.rm = function (fun) {
|
||||||
|
apop(r.q, fun);
|
||||||
|
};
|
||||||
|
|
||||||
|
function doevents() {
|
||||||
|
if (Date.now() - r.last < 69)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var q = r.q.slice(0);
|
||||||
|
for (var a = 0; a < q.length; a++)
|
||||||
|
q[a]();
|
||||||
|
|
||||||
|
r.last = Date.now();
|
||||||
|
}
|
||||||
|
setInterval(doevents, 100);
|
||||||
|
|
||||||
|
return r;
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
var tt = (function () {
|
var tt = (function () {
|
||||||
var r = {
|
var r = {
|
||||||
"tt": mknod("div"),
|
"tt": mknod("div"),
|
||||||
@@ -571,6 +606,8 @@ var tt = (function () {
|
|||||||
r.skip = false;
|
r.skip = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (QS('body.bbox-open'))
|
||||||
|
return;
|
||||||
|
|
||||||
var cfg = sread('tooltips');
|
var cfg = sread('tooltips');
|
||||||
if (cfg !== null && cfg != '1')
|
if (cfg !== null && cfg != '1')
|
||||||
@@ -583,28 +620,39 @@ var tt = (function () {
|
|||||||
r.el = this;
|
r.el = this;
|
||||||
var pos = this.getBoundingClientRect(),
|
var pos = this.getBoundingClientRect(),
|
||||||
dir = this.getAttribute('ttd') || '',
|
dir = this.getAttribute('ttd') || '',
|
||||||
left = pos.left < window.innerWidth / 2,
|
|
||||||
top = pos.top < window.innerHeight / 2,
|
top = pos.top < window.innerHeight / 2,
|
||||||
big = this.className.indexOf(' ttb') !== -1;
|
big = this.className.indexOf(' ttb') !== -1;
|
||||||
|
|
||||||
if (dir.indexOf('u') + 1) top = false;
|
if (dir.indexOf('u') + 1) top = false;
|
||||||
if (dir.indexOf('d') + 1) top = true;
|
if (dir.indexOf('d') + 1) top = true;
|
||||||
if (dir.indexOf('l') + 1) left = false;
|
|
||||||
if (dir.indexOf('r') + 1) left = true;
|
|
||||||
|
|
||||||
clmod(r.tt, 'b', big);
|
clmod(r.tt, 'b', big);
|
||||||
r.tt.style.top = top ? pos.bottom + 'px' : 'auto';
|
r.tt.style.left = '0';
|
||||||
r.tt.style.bottom = top ? 'auto' : (window.innerHeight - pos.top) + 'px';
|
r.tt.style.top = '0';
|
||||||
r.tt.style.left = left ? pos.left + 'px' : 'auto';
|
|
||||||
r.tt.style.right = left ? 'auto' : (window.innerWidth - pos.right) + 'px';
|
|
||||||
|
|
||||||
r.tt.innerHTML = msg.replace(/\$N/g, "<br />");
|
r.tt.innerHTML = msg.replace(/\$N/g, "<br />");
|
||||||
|
var tw = r.tt.offsetWidth,
|
||||||
|
x = pos.left + (pos.right - pos.left) / 2 - tw / 2;
|
||||||
|
|
||||||
|
if (x < 0)
|
||||||
|
x = 8;
|
||||||
|
|
||||||
|
if (x + tw >= window.innerWidth - 8) {
|
||||||
|
x = window.innerWidth - tw - 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
r.tt.style.left = x + 'px';
|
||||||
|
r.tt.style.top = top ? pos.bottom + 'px' : 'auto';
|
||||||
|
r.tt.style.bottom = top ? 'auto' : (window.innerHeight - pos.top) + 'px';
|
||||||
|
|
||||||
r.el.addEventListener('mouseleave', r.hide);
|
r.el.addEventListener('mouseleave', r.hide);
|
||||||
|
window.addEventListener('scroll', r.hide);
|
||||||
clmod(r.tt, 'show', 1);
|
clmod(r.tt, 'show', 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
r.hide = function (e) {
|
r.hide = function (e) {
|
||||||
ev(e);
|
ev(e);
|
||||||
|
window.removeEventListener('scroll', r.hide);
|
||||||
clmod(r.tt, 'show');
|
clmod(r.tt, 'show');
|
||||||
if (r.el)
|
if (r.el)
|
||||||
r.el.removeEventListener('mouseleave', r.hide);
|
r.el.removeEventListener('mouseleave', r.hide);
|
||||||
@@ -612,13 +660,23 @@ var tt = (function () {
|
|||||||
|
|
||||||
if (is_touch && IPHONE) {
|
if (is_touch && IPHONE) {
|
||||||
var f1 = r.show,
|
var f1 = r.show,
|
||||||
f2 = r.hide;
|
f2 = r.hide,
|
||||||
|
q = [];
|
||||||
|
|
||||||
|
// if an onclick-handler creates a new timer,
|
||||||
|
// iOS 13.1.2 delays the entire handler by up to 401ms,
|
||||||
|
// win by using a shared timer instead
|
||||||
|
|
||||||
|
timer.add(function () {
|
||||||
|
while (q.length && Date.now() >= q[0][0])
|
||||||
|
q.shift()[1]();
|
||||||
|
});
|
||||||
|
|
||||||
r.show = function () {
|
r.show = function () {
|
||||||
setTimeout(f1.bind(this), 301);
|
q.push([Date.now() + 100, f1.bind(this)]);
|
||||||
};
|
};
|
||||||
r.hide = function () {
|
r.hide = function () {
|
||||||
setTimeout(f2.bind(this), 301);
|
q.push([Date.now() + 100, f2.bind(this)]);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,14 +714,25 @@ var tt = (function () {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
function lf2br(txt) {
|
||||||
|
var html = '', hp = txt.split(/(?=<.?pre>)/i);
|
||||||
|
for (var a = 0; a < hp.length; a++)
|
||||||
|
html += hp[a].startsWith('<pre>') ? hp[a] :
|
||||||
|
hp[a].replace(/<br ?.?>\n/g, '\n').replace(/\n<br ?.?>/g, '\n').replace(/\n/g, '<br />\n');
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var toast = (function () {
|
var toast = (function () {
|
||||||
var r = {},
|
var r = {},
|
||||||
te = null,
|
te = null,
|
||||||
visible = false,
|
|
||||||
obj = mknod('div');
|
obj = mknod('div');
|
||||||
|
|
||||||
obj.setAttribute('id', 'toast');
|
obj.setAttribute('id', 'toast');
|
||||||
document.body.appendChild(obj);;
|
document.body.appendChild(obj);
|
||||||
|
r.visible = false;
|
||||||
|
r.txt = null;
|
||||||
|
|
||||||
r.hide = function (e) {
|
r.hide = function (e) {
|
||||||
ev(e);
|
ev(e);
|
||||||
@@ -677,17 +746,13 @@ var toast = (function () {
|
|||||||
if (ms)
|
if (ms)
|
||||||
te = setTimeout(r.hide, ms * 1000);
|
te = setTimeout(r.hide, ms * 1000);
|
||||||
|
|
||||||
var html = '', hp = txt.split(/(?=<.?pre>)/i);
|
obj.innerHTML = '<a href="#" id="toastc">x</a>' + lf2br(txt);
|
||||||
for (var a = 0; a < hp.length; a++)
|
|
||||||
html += hp[a].startsWith('<pre>') ? hp[a] :
|
|
||||||
hp[a].replace(/<br ?.?>\n/g, '\n').replace(/\n<br ?.?>/g, '\n').replace(/\n/g, '<br />\n');
|
|
||||||
|
|
||||||
obj.innerHTML = '<a href="#" id="toastc">x</a>' + html;
|
|
||||||
obj.className = cl;
|
obj.className = cl;
|
||||||
ms += obj.offsetWidth;
|
ms += obj.offsetWidth;
|
||||||
obj.className += ' vis';
|
obj.className += ' vis';
|
||||||
ebi('toastc').onclick = r.hide;
|
ebi('toastc').onclick = r.hide;
|
||||||
r.visible = true;
|
r.visible = true;
|
||||||
|
r.txt = txt;
|
||||||
};
|
};
|
||||||
|
|
||||||
r.ok = function (ms, txt) {
|
r.ok = function (ms, txt) {
|
||||||
@@ -705,3 +770,166 @@ var toast = (function () {
|
|||||||
|
|
||||||
return r;
|
return r;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
var modal = (function () {
|
||||||
|
var r = {},
|
||||||
|
q = [],
|
||||||
|
o = null,
|
||||||
|
cb_ok = null,
|
||||||
|
cb_ng = null;
|
||||||
|
|
||||||
|
r.busy = false;
|
||||||
|
|
||||||
|
r.show = function (html) {
|
||||||
|
o = mknod('div');
|
||||||
|
o.setAttribute('id', 'modal');
|
||||||
|
o.innerHTML = '<table><tr><td><div id="modalc">' + html + '</div></td></tr></table>';
|
||||||
|
document.body.appendChild(o);
|
||||||
|
document.addEventListener('keydown', onkey);
|
||||||
|
r.busy = true;
|
||||||
|
|
||||||
|
var a = ebi('modal-ng');
|
||||||
|
if (a)
|
||||||
|
a.onclick = ng;
|
||||||
|
|
||||||
|
a = ebi('modal-ok');
|
||||||
|
a.onclick = ok;
|
||||||
|
|
||||||
|
var inp = ebi('modali');
|
||||||
|
(inp || a).focus();
|
||||||
|
if (inp)
|
||||||
|
setTimeout(function () {
|
||||||
|
inp.setSelectionRange(0, inp.value.length, "forward");
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
document.addEventListener('focus', onfocus);
|
||||||
|
timer.add(onfocus);
|
||||||
|
};
|
||||||
|
|
||||||
|
r.hide = function () {
|
||||||
|
timer.rm(onfocus);
|
||||||
|
document.removeEventListener('focus', onfocus);
|
||||||
|
document.removeEventListener('keydown', onkey);
|
||||||
|
o.parentNode.removeChild(o);
|
||||||
|
r.busy = false;
|
||||||
|
setTimeout(next, 50);
|
||||||
|
};
|
||||||
|
function ok(e) {
|
||||||
|
ev(e);
|
||||||
|
var v = ebi('modali');
|
||||||
|
v = v ? v.value : true;
|
||||||
|
r.hide();
|
||||||
|
if (cb_ok)
|
||||||
|
cb_ok(v);
|
||||||
|
}
|
||||||
|
function ng(e) {
|
||||||
|
ev(e);
|
||||||
|
r.hide();
|
||||||
|
if (cb_ng)
|
||||||
|
cb_ng(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onfocus(e) {
|
||||||
|
var ctr = ebi('modalc');
|
||||||
|
if (!ctr || !ctr.contains || !document.activeElement || ctr.contains(document.activeElement))
|
||||||
|
return;
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
ebi('modal-ok').focus();
|
||||||
|
}, 20);
|
||||||
|
ev(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onkey(e) {
|
||||||
|
if (e.code == 'Enter') {
|
||||||
|
var a = ebi('modal-ng');
|
||||||
|
if (a && document.activeElement == a)
|
||||||
|
return ng();
|
||||||
|
|
||||||
|
return ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.code == 'Escape')
|
||||||
|
return ng();
|
||||||
|
}
|
||||||
|
|
||||||
|
function next() {
|
||||||
|
if (!r.busy && q.length)
|
||||||
|
q.shift()();
|
||||||
|
}
|
||||||
|
|
||||||
|
r.alert = function (html, cb) {
|
||||||
|
q.push(function () {
|
||||||
|
_alert(lf2br(html), cb);
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
function _alert(html, cb) {
|
||||||
|
cb_ok = cb_ng = cb;
|
||||||
|
html += '<div id="modalb"><a href="#" id="modal-ok">OK</a></div>';
|
||||||
|
r.show(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
r.confirm = function (html, cok, cng) {
|
||||||
|
q.push(function () {
|
||||||
|
_confirm(lf2br(html), cok, cng);
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
function _confirm(html, cok, cng) {
|
||||||
|
cb_ok = cok;
|
||||||
|
cb_ng = cng === undefined ? cok : null;
|
||||||
|
html += '<div id="modalb"><a href="#" id="modal-ok">OK</a><a href="#" id="modal-ng">Cancel</a></div>';
|
||||||
|
r.show(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
r.prompt = function (html, v, cok, cng) {
|
||||||
|
q.push(function () {
|
||||||
|
_prompt(lf2br(html), v, cok, cng);
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
function _prompt(html, v, cok, cng) {
|
||||||
|
cb_ok = cok;
|
||||||
|
cb_ng = cng === undefined ? cok : null;
|
||||||
|
html += '<input id="modali" type="text" /><div id="modalb"><a href="#" id="modal-ok">OK</a><a href="#" id="modal-ng">Cancel</a></div>';
|
||||||
|
r.show(html);
|
||||||
|
|
||||||
|
ebi('modali').value = v || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return r;
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
function winpopup(txt) {
|
||||||
|
fetch(get_evpath(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||||
|
},
|
||||||
|
body: 'msg=' + uricom_enc(Date.now() + ', ' + txt)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function repl(e) {
|
||||||
|
ev(e);
|
||||||
|
modal.prompt('js repl (prefix with <code>,</code> to allow raise)', 'var v=Object.keys(localStorage); v.sort(); JSON.stringify(v)', function (cmd) {
|
||||||
|
if (!cmd)
|
||||||
|
return toast.inf(3, 'eval aborted');
|
||||||
|
|
||||||
|
if (cmd.startsWith(','))
|
||||||
|
return modal.alert(esc(eval(cmd.slice(1)) + ''))
|
||||||
|
|
||||||
|
try {
|
||||||
|
modal.alert(esc(eval(cmd) + ''));
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
modal.alert('<h6>exception</h6>' + esc(ex + ''));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (ebi('repl'))
|
||||||
|
ebi('repl').onclick = repl;
|
||||||
|
|||||||
@@ -1,36 +1,3 @@
|
|||||||
/* put filetype icons inline with text
|
|
||||||
#ggrid>a>span:before,
|
|
||||||
#ggrid>a>span.dir:before {
|
|
||||||
display: inline;
|
|
||||||
line-height: 0;
|
|
||||||
font-size: 1.7em;
|
|
||||||
margin: -.7em .1em -.5em -.6em;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/* move folder icons top-left */
|
|
||||||
#ggrid>a>span.dir:before {
|
|
||||||
content: initial;
|
|
||||||
}
|
|
||||||
#ggrid>a[href$="/"]:before {
|
|
||||||
content: '📂';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* put filetype icons top-left */
|
|
||||||
#ggrid>a:before {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
padding: .3em 0;
|
|
||||||
margin: -.4em;
|
|
||||||
text-shadow: 0 0 .1em #000;
|
|
||||||
background: linear-gradient(135deg,rgba(255,255,255,0) 50%,rgba(255,255,255,0.2));
|
|
||||||
border-radius: .3em;
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* video */
|
/* video */
|
||||||
#ggrid>a:is(
|
#ggrid>a:is(
|
||||||
[href$=".mkv"i],
|
[href$=".mkv"i],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
#u2cleanup, #u2conf tr:first-child>td[rowspan]:not(#u2btn_cw), /* most of the config options */
|
#u2cleanup, #u2conf tr:first-child>td[rowspan]:not(#u2btn_cw), /* most of the config options */
|
||||||
|
|
||||||
#u2cards /* and the upload progress tabs */
|
#u2cards, #u2etaw /* and the upload progress tabs */
|
||||||
|
|
||||||
{display: none !important} /* do it! */
|
{display: none !important} /* do it! */
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
/* add some margins because now it's weird */
|
/* add some margins because now it's weird */
|
||||||
.opview {margin-top: 2.5em}
|
.opview {margin-top: 2.5em}
|
||||||
#op_up2k {margin-top: 3em}
|
#op_up2k {margin-top: 6em}
|
||||||
|
|
||||||
/* and embiggen the upload button */
|
/* and embiggen the upload button */
|
||||||
#u2conf #u2btn, #u2btn {padding:1.5em 0}
|
#u2conf #u2btn, #u2btn {padding:1.5em 0}
|
||||||
|
|||||||
@@ -126,6 +126,10 @@ e=6; s=10; d=~/dev/copyparty/srv/aus; n=1; p=0; e=$((e*60)); rm -rf $d; mkdir $d
|
|||||||
-v srv/aus:aus:r:ce2dsa:ce2ts:cmtp=fgsfds=bin/mtag/sleep.py
|
-v srv/aus:aus:r:ce2dsa:ce2ts:cmtp=fgsfds=bin/mtag/sleep.py
|
||||||
sqlite3 .hist/up2k.db 'select * from mt where k="fgsfds" or k="t:mtp"' | tee /dev/stderr | wc -l
|
sqlite3 .hist/up2k.db 'select * from mt where k="fgsfds" or k="t:mtp"' | tee /dev/stderr | wc -l
|
||||||
|
|
||||||
|
# generate the sine meme
|
||||||
|
for ((f=420;f<1200;f++)); do sz=$(ffmpeg -y -f lavfi -i sine=frequency=$f:duration=2 -vf volume=0.1 -ac 1 -ar 44100 -f s16le /dev/shm/a.wav 2>/dev/null; base64 -w0 </dev/shm/a.wav | gzip -c | wc -c); printf '%d %d\n' $f $sz; done | tee /dev/stderr | sort -nrk2,2
|
||||||
|
ffmpeg -y -f lavfi -i sine=frequency=1050:duration=2 -vf volume=0.1 -ac 1 -ar 44100 /dev/shm/a.wav
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## vscode
|
## vscode
|
||||||
@@ -157,7 +161,7 @@ brew install python@2
|
|||||||
pip install virtualenv
|
pip install virtualenv
|
||||||
|
|
||||||
# readme toc
|
# readme toc
|
||||||
cat README.md | awk '!/^#/{next} {lv=length($1);sub(/[^ ]+ /,"");bab=$0;gsub(/ /,"-",bab)} {printf "%" ((lv-1)*4+1) "s [%s](#%s)\n", "*",$0,bab}'
|
cat README.md | awk 'function pr() { if (!h) {return}; if (/^ *[*!#]/||!s) {printf "%s\n",h;h=0;return}; if (/.../) {printf "%s - %s\n",h,$0;h=0}; }; /^#/{s=1;pr()} /^#* *(file indexing|install on android|dev env setup|just the sfx|complete release|optional gpl stuff)|`$/{s=0} /^#/{lv=length($1);sub(/[^ ]+ /,"");bab=$0;gsub(/ /,"-",bab); h=sprintf("%" ((lv-1)*4+1) "s [%s](#%s)", "*",$0,bab);next} !h{next} {sub(/:$/,"")} {pr()}' > toc; grep -E '^## readme toc' -B1000 -A2 <README.md >p1; grep -E '^## quickstart' -B2 -A999999 <README.md >p2; (cat p1; grep quickstart -A1000 <toc; cat p2) >README.md
|
||||||
|
|
||||||
# fix firefox phantom breakpoints,
|
# fix firefox phantom breakpoints,
|
||||||
# suggestions from bugtracker, doesnt work (debugger is not attachable)
|
# suggestions from bugtracker, doesnt work (debugger is not attachable)
|
||||||
@@ -173,7 +177,7 @@ about:config >> devtools.debugger.prefs-schema-version = -1
|
|||||||
git pull; git reset --hard origin/HEAD && git log --format=format:"%H %ai %d" --decorate=full > ../revs && cat ../{util,browser,up2k}.js >../vr && cat ../revs | while read -r rev extra; do (git reset --hard $rev >/dev/null 2>/dev/null && dsz=$(cat copyparty/web/{util,browser,up2k}.js >../vg 2>/dev/null && diff -wNarU0 ../{vg,vr} | wc -c) && printf '%s %6s %s\n' "$rev" $dsz "$extra") </dev/null; done
|
git pull; git reset --hard origin/HEAD && git log --format=format:"%H %ai %d" --decorate=full > ../revs && cat ../{util,browser,up2k}.js >../vr && cat ../revs | while read -r rev extra; do (git reset --hard $rev >/dev/null 2>/dev/null && dsz=$(cat copyparty/web/{util,browser,up2k}.js >../vg 2>/dev/null && diff -wNarU0 ../{vg,vr} | wc -c) && printf '%s %6s %s\n' "$rev" $dsz "$extra") </dev/null; done
|
||||||
|
|
||||||
# download all sfx versions
|
# download all sfx versions
|
||||||
curl https://api.github.com/repos/9001/copyparty/releases?per_page=100 | jq -r '.[] | .tag_name + " " + .name' | while read v t; do fn="copyparty $v $t.py"; [ -e $fn ] || curl https://github.com/9001/copyparty/releases/download/$v/copyparty-sfx.py -Lo "$fn"; done
|
curl https://api.github.com/repos/9001/copyparty/releases?per_page=100 | jq -r '.[] | .tag_name + " " + .name' | tr -d '\r' | while read v t; do fn="copyparty $v $t.py"; [ -e "$fn" ] || curl https://github.com/9001/copyparty/releases/download/$v/copyparty-sfx.py -Lo "$fn"; done
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ WORKDIR /z
|
|||||||
ENV ver_asmcrypto=5b994303a9d3e27e0915f72a10b6c2c51535a4dc \
|
ENV ver_asmcrypto=5b994303a9d3e27e0915f72a10b6c2c51535a4dc \
|
||||||
ver_hashwasm=4.7.0 \
|
ver_hashwasm=4.7.0 \
|
||||||
ver_marked=1.1.0 \
|
ver_marked=1.1.0 \
|
||||||
ver_ogvjs=1.8.0 \
|
ver_ogvjs=1.8.4 \
|
||||||
ver_mde=2.14.0 \
|
ver_mde=2.14.0 \
|
||||||
ver_codemirror=5.59.3 \
|
ver_codemirror=5.59.3 \
|
||||||
ver_fontawesome=5.13.0 \
|
ver_fontawesome=5.13.0 \
|
||||||
@@ -74,23 +74,16 @@ RUN cd hash-wasm \
|
|||||||
# build ogvjs
|
# build ogvjs
|
||||||
RUN cd ogvjs-$ver_ogvjs \
|
RUN cd ogvjs-$ver_ogvjs \
|
||||||
&& cp -pv \
|
&& cp -pv \
|
||||||
ogv.js \
|
|
||||||
ogv-worker-audio.js \
|
ogv-worker-audio.js \
|
||||||
ogv-demuxer-ogg-wasm.js \
|
ogv-demuxer-ogg-wasm.js \
|
||||||
ogv-demuxer-ogg-wasm.wasm \
|
ogv-demuxer-ogg-wasm.wasm \
|
||||||
ogv-demuxer-webm-wasm.js \
|
|
||||||
ogv-demuxer-webm-wasm.wasm \
|
|
||||||
ogv-decoder-audio-opus-wasm.js \
|
ogv-decoder-audio-opus-wasm.js \
|
||||||
ogv-decoder-audio-opus-wasm.wasm \
|
ogv-decoder-audio-opus-wasm.wasm \
|
||||||
ogv-decoder-audio-vorbis-wasm.js \
|
ogv-decoder-audio-vorbis-wasm.js \
|
||||||
ogv-decoder-audio-vorbis-wasm.wasm \
|
ogv-decoder-audio-vorbis-wasm.wasm \
|
||||||
/z/dist
|
/z/dist \
|
||||||
|
&& cp -pv \
|
||||||
# ogv-demuxer-ogg.js \
|
ogv-es2017.js /z/dist/ogv.js
|
||||||
# ogv-demuxer-webm.js \
|
|
||||||
# ogv-decoder-audio-opus.js \
|
|
||||||
# ogv-decoder-audio-vorbis.js \
|
|
||||||
# dynamicaudio.swf \
|
|
||||||
|
|
||||||
|
|
||||||
# build marked
|
# build marked
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ all: $(addsuffix .gz, $(wildcard *.*))
|
|||||||
|
|
||||||
%.gz: %
|
%.gz: %
|
||||||
#brotli -q 11 $<
|
#brotli -q 11 $<
|
||||||
pigz -11 -J 34 -I 573 $<
|
pigz -11 -I 573 $<
|
||||||
|
|
||||||
# pigz -11 -J 34 -I 100 -F < $< > $@.first
|
# pigz -11 -J 34 -I 100 -F < $< > $@.first
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ gtar=$(command -v gtar || command -v gnutar) || true
|
|||||||
sed() { gsed "$@"; }
|
sed() { gsed "$@"; }
|
||||||
find() { gfind "$@"; }
|
find() { gfind "$@"; }
|
||||||
sort() { gsort "$@"; }
|
sort() { gsort "$@"; }
|
||||||
|
shuf() { gshuf "$@"; }
|
||||||
sha1sum() { shasum "$@"; }
|
sha1sum() { shasum "$@"; }
|
||||||
unexpand() { gunexpand "$@"; }
|
unexpand() { gunexpand "$@"; }
|
||||||
command -v grealpath >/dev/null &&
|
command -v grealpath >/dev/null &&
|
||||||
@@ -239,7 +240,8 @@ awk '/^LOREM_IPSUM_WORDS/{o=1;print "LOREM_IPSUM_WORDS = u\"a\"";next} !o; /"""/
|
|||||||
tmv "$f"
|
tmv "$f"
|
||||||
|
|
||||||
# up2k goes from 28k to 22k laff
|
# up2k goes from 28k to 22k laff
|
||||||
echo entabbening
|
awk 'BEGIN{gensub(//,"",1)}' </dev/null &&
|
||||||
|
echo entabbening &&
|
||||||
find | grep -E '\.css$' | while IFS= read -r f; do
|
find | grep -E '\.css$' | while IFS= read -r f; do
|
||||||
awk '{
|
awk '{
|
||||||
sub(/^[ \t]+/,"");
|
sub(/^[ \t]+/,"");
|
||||||
@@ -253,6 +255,7 @@ find | grep -E '\.css$' | while IFS= read -r f; do
|
|||||||
' <$f | sed 's/;\}$/}/' >t
|
' <$f | sed 's/;\}$/}/' >t
|
||||||
tmv "$f"
|
tmv "$f"
|
||||||
done
|
done
|
||||||
|
unexpand -h 2>/dev/null &&
|
||||||
find | grep -E '\.(js|html)$' | while IFS= read -r f; do
|
find | grep -E '\.(js|html)$' | while IFS= read -r f; do
|
||||||
unexpand -t 4 --first-only <"$f" >t
|
unexpand -t 4 --first-only <"$f" >t
|
||||||
tmv "$f"
|
tmv "$f"
|
||||||
@@ -301,7 +304,7 @@ for d in copyparty dep-j2; do find $d -type f; done |
|
|||||||
sed -r 's/(.*)\.(.*)/\2 \1/' | LC_ALL=C sort |
|
sed -r 's/(.*)\.(.*)/\2 \1/' | LC_ALL=C sort |
|
||||||
sed -r 's/([^ ]*) (.*)/\2.\1/' | grep -vE '/list1?$' > list1
|
sed -r 's/([^ ]*) (.*)/\2.\1/' | grep -vE '/list1?$' > list1
|
||||||
|
|
||||||
(grep -vE '\.(gz|br)$' list1; grep -E '\.(gz|br)$' list1) >list || true
|
(grep -vE '\.(gz|br)$' list1; grep -E '\.(gz|br)$' list1 | shuf) >list || true
|
||||||
|
|
||||||
echo creating tar
|
echo creating tar
|
||||||
args=(--owner=1000 --group=1000)
|
args=(--owner=1000 --group=1000)
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ def confirm(rv):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
sys.exit(rv)
|
sys.exit(rv or 1)
|
||||||
|
|
||||||
|
|
||||||
def run(tmp, j2):
|
def run(tmp, j2):
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ def tc1():
|
|||||||
pdirs = [x.replace("\\", "/") for x in pdirs]
|
pdirs = [x.replace("\\", "/") for x in pdirs]
|
||||||
udirs = [x.split("/", 2)[2] for x in pdirs]
|
udirs = [x.split("/", 2)[2] for x in pdirs]
|
||||||
perms = [x.rstrip("j/")[-1] for x in pdirs]
|
perms = [x.rstrip("j/")[-1] for x in pdirs]
|
||||||
|
perms = ["rw" if x == "a" else x for x in perms]
|
||||||
for pd, ud, p in zip(pdirs, udirs, perms):
|
for pd, ud, p in zip(pdirs, udirs, perms):
|
||||||
if ud[-1] == "j":
|
if ud[-1] == "j":
|
||||||
continue
|
continue
|
||||||
@@ -147,14 +148,14 @@ def tc1():
|
|||||||
u = "{}{}/a.h264".format(ub, d)
|
u = "{}{}/a.h264".format(ub, d)
|
||||||
r = requests.get(u)
|
r = requests.get(u)
|
||||||
ok = bool(r)
|
ok = bool(r)
|
||||||
if ok != (p in ["a"]):
|
if ok != (p in ["rw"]):
|
||||||
raise Exception("get {} with perm {} at {}".format(ok, p, u))
|
raise Exception("get {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
# stat filesystem
|
# stat filesystem
|
||||||
for d, p in zip(pdirs, perms):
|
for d, p in zip(pdirs, perms):
|
||||||
u = "{}/a.h264".format(d)
|
u = "{}/a.h264".format(d)
|
||||||
ok = os.path.exists(u)
|
ok = os.path.exists(u)
|
||||||
if ok != (p in ["a", "w"]):
|
if ok != (p in ["rw", "w"]):
|
||||||
raise Exception("stat {} with perm {} at {}".format(ok, p, u))
|
raise Exception("stat {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
# GET thumbnail, vreify contents
|
# GET thumbnail, vreify contents
|
||||||
@@ -162,7 +163,7 @@ def tc1():
|
|||||||
u = "{}{}/a.h264?th=j".format(ub, d)
|
u = "{}{}/a.h264?th=j".format(ub, d)
|
||||||
r = requests.get(u)
|
r = requests.get(u)
|
||||||
ok = bool(r and r.content[:3] == b"\xff\xd8\xff")
|
ok = bool(r and r.content[:3] == b"\xff\xd8\xff")
|
||||||
if ok != (p in ["a"]):
|
if ok != (p in ["rw"]):
|
||||||
raise Exception("thumb {} with perm {} at {}".format(ok, p, u))
|
raise Exception("thumb {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
# check tags
|
# check tags
|
||||||
@@ -179,10 +180,10 @@ def tc1():
|
|||||||
r_ok = bool(j)
|
r_ok = bool(j)
|
||||||
w_ok = bool(r_ok and j.get("files"))
|
w_ok = bool(r_ok and j.get("files"))
|
||||||
|
|
||||||
if not r_ok or w_ok != (p in ["a"]):
|
if not r_ok or w_ok != (p in ["rw"]):
|
||||||
raise Exception("ls {} with perm {} at {}".format(ok, p, u))
|
raise Exception("ls {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
if (tag and p != "a") or (not tag and p == "a"):
|
if (tag and p != "rw") or (not tag and p == "rw"):
|
||||||
raise Exception("tag {} with perm {} at {}".format(tag, p, u))
|
raise Exception("tag {} with perm {} at {}".format(tag, p, u))
|
||||||
|
|
||||||
if tag is not None and tag != "48x32":
|
if tag is not None and tag != "48x32":
|
||||||
|
|||||||
1
setup.py
1
setup.py
@@ -99,6 +99,7 @@ args = {
|
|||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.7",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class Cfg(Namespace):
|
|||||||
no_scandir=False,
|
no_scandir=False,
|
||||||
no_sendfile=True,
|
no_sendfile=True,
|
||||||
no_rescan=True,
|
no_rescan=True,
|
||||||
|
re_maxage=0,
|
||||||
ihead=False,
|
ihead=False,
|
||||||
nih=True,
|
nih=True,
|
||||||
mtp=[],
|
mtp=[],
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class Cfg(Namespace):
|
|||||||
"no_hash": False,
|
"no_hash": False,
|
||||||
"css_browser": None,
|
"css_browser": None,
|
||||||
"no_voldump": True,
|
"no_voldump": True,
|
||||||
|
"re_maxage": 0,
|
||||||
"rproxy": 0,
|
"rproxy": 0,
|
||||||
}
|
}
|
||||||
ex.update(ex2)
|
ex.update(ex2)
|
||||||
|
|||||||
Reference in New Issue
Block a user