Compare commits

...

51 Commits

Author SHA1 Message Date
ed
cf83f9b0fd v0.13.6 2021-08-27 00:09:36 +02:00
ed
d98e361083 quick debounce 2021-08-26 23:59:17 +02:00
ed
ce7f5309c7 tweak toast bg 2021-08-26 23:46:04 +02:00
ed
75c485ced7 misc toast rice and html escaping 2021-08-26 23:45:28 +02:00
ed
9c6e2ec012 misc modal rice and html escaping 2021-08-26 23:23:56 +02:00
ed
1a02948a61 prevent text selection on most buttons 2021-08-26 23:01:24 +02:00
ed
8b05ba4ba1 stop counting eta when we don't hold the flag 2021-08-26 22:51:07 +02:00
ed
21e2874cb7 warning when another browser tab holds the flag 2021-08-26 22:50:22 +02:00
ed
360ed5c46c release the up2k flag when disabling it 2021-08-26 22:48:57 +02:00
ed
5099bc365d better eta for fsearch 2021-08-26 22:47:43 +02:00
ed
12986da147 might be useful some time 2021-08-26 22:45:50 +02:00
ed
23e72797bc remove some more ansi escapes on win7 2021-08-26 22:45:36 +02:00
ed
ac7b6f8f55 update turbo hint for fsearch 2021-08-26 20:44:36 +02:00
ed
981b9ff11e more accurate eta 2021-08-26 20:43:52 +02:00
ed
4186906f4c pause hashing as well when parallel uploads is 0 2021-08-26 20:43:27 +02:00
ed
0850d24e0c improve spacing on narrow screens 2021-08-26 20:42:20 +02:00
ed
7ab8334c96 remove debug 2021-08-26 01:16:59 +02:00
ed
a4d7329ab7 revert to fixed MiB/s in upload tab 2021-08-26 01:13:20 +02:00
ed
3f4eae6bce yolo search + show in bz + md search 2021-08-26 00:57:49 +02:00
ed
518cf4be57 set fsearch tag on tasks 2021-08-26 00:54:00 +02:00
ed
71096182be toFixed is busted, workaround 2021-08-26 00:51:35 +02:00
ed
6452e927ea download-eta accuracy + misc ux 2021-08-26 00:40:12 +02:00
ed
bc70cfa6f0 fix tmi 2021-08-25 09:02:34 +02:00
ed
2b6e5ebd2d update minimal-up2k 2021-08-25 08:26:38 +02:00
ed
c761bd799a add pane with total eta for all uploads 2021-08-25 02:06:29 +02:00
ed
2f7c2fdee4 add colors to status column in up2k ui 2021-08-24 00:32:53 +02:00
ed
70a76ec343 add toast on upload/fsearch completion 2021-08-24 00:31:01 +02:00
ed
7c3f64abf2 fix navpane h.scroll bug 2021-08-24 00:29:11 +02:00
ed
f5f38f195c use scp.woff in browser too 2021-08-24 00:28:16 +02:00
ed
7e84f4f015 fence focus inside modals 2021-08-24 00:26:54 +02:00
ed
4802f8cf07 better msg when unposting a deleted file 2021-08-24 00:24:50 +02:00
ed
cc05e67d8f add summaries to readme toc 2021-08-22 17:23:42 +02:00
ed
2b6b174517 the smallest nitpick 2021-08-20 19:25:57 +02:00
ed
a1d05e6e12 folder thumbnail fix 2021-08-20 19:22:25 +02:00
ed
f95ceb6a9b fix toc 2021-08-17 08:54:19 +02:00
ed
8f91b0726d add missing hotkey hint 2021-08-17 00:24:27 +02:00
ed
97807f4383 update screenshots 2021-08-17 00:23:12 +02:00
ed
5f42237f2c v0.13.5 2021-08-16 08:40:26 +02:00
ed
68289cfa54 v0.13.4 2021-08-16 08:18:52 +02:00
ed
42ea30270f up2k-ui: post absolute URLs 2021-08-16 08:16:52 +02:00
ed
ebbbbf3d82 misc old-browser support 2021-08-16 00:22:30 +02:00
ed
27516e2d16 scroll navpane to open folder on load 2021-08-16 00:07:31 +02:00
ed
84bb6f915e fix unpost ui for nonroot volumes 2021-08-16 00:03:05 +02:00
ed
46752f758a fix bup into volumes with upload rules 2021-08-15 23:59:41 +02:00
ed
34c4c22e61 v0.13.3 2021-08-14 22:46:15 +02:00
ed
af2d0b8421 upgrade permsets in smoketest 2021-08-14 22:45:33 +02:00
ed
638b05a49a fix image-viewer touch handler 2021-08-14 22:40:54 +02:00
ed
7a13e8a7fc clear transform on 0deg rotate 2021-08-14 21:13:15 +02:00
ed
d9fa74711d cheaper shadows 2021-08-14 18:17:40 +02:00
ed
41867f578f image viewer: add rotation 2021-08-14 18:06:53 +02:00
ed
0bf41ed4ef exif orientation for thumbnails 2021-08-14 17:45:44 +02:00
19 changed files with 777 additions and 261 deletions

218
README.md
View File

@@ -6,7 +6,7 @@
## 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+`
* browse/upload with IE4 / netscape4.0 on win3.11 (heh)
@@ -19,49 +19,53 @@ turn your phone or raspi into a portable file server with resumable uploads/down
## readme toc
* top
* [quickstart](#quickstart)
* [on debian](#on-debian)
* [notes](#notes)
* [status](#status)
* [testimonials](#testimonials)
* [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) - recommended additional steps on debian
* [notes](#notes) - general notes
* [status](#status) - summary: all planned features work! now please enjoy the bloatening
* [testimonials](#testimonials) - small collection of user feedback
* [bugs](#bugs)
* [general bugs](#general-bugs)
* [not my bugs](#not-my-bugs)
* [the browser](#the-browser)
* [tabs](#tabs)
* [hotkeys](#hotkeys)
* [navpane](#navpane)
* [thumbnails](#thumbnails)
* [zip downloads](#zip-downloads)
* [uploading](#uploading)
* [file-search](#file-search)
* [file manager](#file-manager)
* [batch rename](#batch-rename)
* [markdown viewer](#markdown-viewer)
* [accounts and volumes](#accounts-and-volumes) - per-folder, per-user permissions
* [the browser](#the-browser) - accessing a copyparty server using a web-browser
* [tabs](#tabs) - the main tabs in the ui
* [hotkeys](#hotkeys) - the browser has the following hotkeys (always qwerty)
* [navpane](#navpane) - switching between breadcrumbs or navpane
* [thumbnails](#thumbnails) - press `g` to toggle image/video thumbnails instead of the file listing
* [zip downloads](#zip-downloads) - download folders (or file selections) as `zip` or `tar` files
* [uploading](#uploading) - web-browsers can upload using `bup` and `up2k`
* [file-search](#file-search) - drop files/folders into up2k to see if they exist on the server
* [unpost](#unpost) - undo/delete accidental uploads
* [file manager](#file-manager) - cut/paste, rename, and delete files/folders (if you have permission)
* [batch rename](#batch-rename) - select some files and press F2 to bring up the rename UI
* [markdown viewer](#markdown-viewer) - and there are *two* editors
* [other tricks](#other-tricks)
* [searching](#searching)
* [searching](#searching) - search by size, date, path/name, mp3-tags, ...
* [server config](#server-config)
* [upload rules](#upload-rules)
* [database location](#database-location)upload rules
* [metadata from audio files](#metadata-from-audio-files)
* [file parser plugins](#file-parser-plugins)
* [file indexing](#file-indexing)
* [upload rules](#upload-rules) - set upload rules using volume flags, some examples
* [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)
* [browser support](#browser-support)
* [client examples](#client-examples)
* [up2k](#up2k)
* [performance](#performance)
* [dependencies](#dependencies)
* [optional dependencies](#optional-dependencies)
* [browser support](#browser-support) - TLDR: yes
* [client examples](#client-examples) - interact with copyparty using non-browser clients
* [up2k](#up2k) - quick outline of the up2k protocol, see [uploading](#uploading) for the web-client
* [performance](#performance) - defaults are good for most cases
* [dependencies](#dependencies) - mandatory deps
* [optional dependencies](#optional-dependencies) - install these to enable bonus features
* [install recommended deps](#install-recommended-deps)
* [optional gpl stuff](#optional-gpl-stuff)
* [sfx](#sfx)
* [sfx repack](#sfx-repack)
* [sfx](#sfx) - there are two self-contained "binaries"
* [sfx repack](#sfx-repack) - reduce the size of an sfx by removing features
* [install on android](#install-on-android)
* [building](#building)
* [dev env setup](#dev-env-setup)
* [just the sfx](#just-the-sfx)
* [complete release](#complete-release)
* [todo](#todo)
* [todo](#todo) - roughly sorted by priority
* [discarded ideas](#discarded-ideas)
@@ -86,7 +90,9 @@ you may also want these, especially on servers:
### 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:
`apt install python3 python3-pip python3-dev ffmpeg`
@@ -99,7 +105,7 @@ recommended steps to enable audio metadata and thumbnails (from images and video
## notes
general:
general notes:
* 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
@@ -124,7 +130,7 @@ summary: all planned features work! now please enjoy the bloatening
* ☑ basic: plain multipart, ie6 support
* ☑ [up2k](#uploading): js, resumable, multithreaded
* ☑ stash: simple PUT filedropper
* ☑ unpost: undo/delete accidental uploads
*[unpost](#unpost): undo/delete accidental uploads
* ☑ symlink/discard existing files (content-matching)
* download
* ☑ single files in browser
@@ -184,6 +190,7 @@ small collection of user feedback
# accounts and volumes
per-folder, per-user permissions
* `-a usr:pwd` adds account `usr` with password `pwd`
* `-v .::r` adds current-folder `.` as the webroot, `r`eadable by anyone
* the syntax is `-v src:dst:perm:perm:...` so local-path, url-path, and one or more permissions to set
@@ -208,14 +215,17 @@ example:
# the browser
![copyparty-browser-fs8](https://user-images.githubusercontent.com/241032/115978054-65106380-a57d-11eb-98f8-59e3dee73557.png)
accessing a copyparty server using a web-browser
![copyparty-browser-fs8](https://user-images.githubusercontent.com/241032/129635359-d6dd9b07-8079-4020-ad77-2bfdb9ebd8d5.png)
## tabs
* `[🔎]` search by size, date, path/name, mp3-tags ... see [searching](#searching)
* `[🧯]` unpost: undo/delete accidental uploads
* `[🚀]` and `[🎈]` are the uploaders, see [uploading](#uploading)
the main tabs in the ui
* `[🔎]` [search](#searching) by size, date, path/name, mp3-tags ...
* `[🧯]` [unpost](#unpost): undo/delete accidental uploads
* `[🚀]` and `[🎈]` are the [uploaders](#uploading)
* `[📂]` mkdir: create directories
* `[📝]` new-md: create a new markdown document
* `[📟]` send-msg: either to server-log or into textfiles if `--urlform save`
@@ -225,7 +235,7 @@ example:
## hotkeys
the browser has the following hotkeys (assumes qwerty, ignores actual layout)
the browser has the following hotkeys (always qwerty)
* `B` toggle breadcrumbs / navpane
* `I/K` prev/next folder
* `M` parent folder (or unexpand current)
@@ -249,13 +259,14 @@ the browser has the following hotkeys (assumes qwerty, ignores actual layout)
* `J/L, Left/Right` prev/next file
* `Home/End` first/last file
* `S` toggle selection
* `R` rotate clockwise (shift=ccw)
* `Esc` close viewer
* videos:
* `U/O` skip 10sec back/forward
* `P/K/Space` play/pause
* `F` fullscreen
* `C` continue playing next video
* `R` loop
* `V` loop
* `M` mute
* when the navpane is open:
* `A/D` adjust tree width
@@ -273,14 +284,18 @@ the browser has the following hotkeys (assumes qwerty, ignores actual layout)
## 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
## thumbnails
![copyparty-thumbs-fs8](https://user-images.githubusercontent.com/241032/120070302-10836b00-c08a-11eb-8eb4-82004a34c342.png)
press `g` to toggle image/video thumbnails instead of the file listing
![copyparty-thumbs-fs8](https://user-images.githubusercontent.com/241032/129636211-abd20fa2-a953-4366-9423-1c88ebb96ba9.png)
it does static images with Pillow and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how dangerous your users are
@@ -291,7 +306,9 @@ in the grid/thumbnail view, if the audio player panel is open, songs will start
## 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 |
|--|--|--|
@@ -308,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
![copyparty-zipsel-fs8](https://user-images.githubusercontent.com/241032/116008321-372a2e00-a614-11eb-9a4a-4a1fd9074224.png)
![copyparty-zipsel-fs8](https://user-images.githubusercontent.com/241032/129635374-e5136e01-470a-49b1-a762-848e8a4c9cdc.png)
## 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
* `[🚀] 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:
* you can drop folders into the browser (files are added recursively)
@@ -330,7 +348,7 @@ up2k has several advantages:
see [up2k](#up2k) for details on how it works
![copyparty-upload-fs8](https://user-images.githubusercontent.com/241032/115978061-680b5400-a57d-11eb-9ef6-cbb5f60aeccc.png)
![copyparty-upload-fs8](https://user-images.githubusercontent.com/241032/129635371-48fc54ca-fa91-48e3-9b1d-ba413e4b68cb.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)
@@ -338,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
* `[🏃]` analysis of other files should continue while one is uploading
* `[💭]` 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
and then theres the tabs below it,
@@ -349,11 +367,14 @@ and then theres the tabs below it,
* plus up to 3 entries each from `[done]` and `[que]` for context
* `[que]` is all the files that are still queued
### file-search
![copyparty-fsearch-fs8](https://user-images.githubusercontent.com/241032/116008320-36919780-a614-11eb-803f-04162326a700.png)
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
![copyparty-fsearch-fs8](https://user-images.githubusercontent.com/241032/129635361-c79286f0-b8f1-440e-aaf4-6e929428fac9.png)
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]`
* 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
@@ -365,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)
### unpost
undo/delete accidental uploads
![copyparty-unpost-fs8](https://user-images.githubusercontent.com/241032/129635368-3afa6634-c20f-418c-90dc-ec411f3b3897.png)
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
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)
## batch rename
![batch-rename-fs8](https://user-images.githubusercontent.com/241032/128434204-eb136680-3c07-4ec7-92e0-ae86af20c241.png)
select some files and press F2 to bring up the rename UI
![batch-rename-fs8](https://user-images.githubusercontent.com/241032/128434204-eb136680-3c07-4ec7-92e0-ae86af20c241.png)
quick explanation of the buttons,
* `[✅ apply rename]` confirms and begins renaming
* `[❌ cancel]` aborts and closes the rename window
@@ -421,6 +451,8 @@ the metadata keys you can use in the format field are the ones in the file-brows
## markdown viewer
and there are *two* editors
![copyparty-md-read-fs8](https://user-images.githubusercontent.com/241032/115978057-66419080-a57d-11eb-8539-d2be843991aa.png)
* the document preview has a max-width which is the same as an A4 paper when printed
@@ -435,7 +467,9 @@ the metadata keys you can use in the format field are the ones in the file-brows
## searching
![copyparty-search-fs8](https://user-images.githubusercontent.com/241032/115978060-6772bd80-a57d-11eb-81d3-174e869b72c3.png)
search by size, date, path/name, mp3-tags, ...
![copyparty-search-fs8](https://user-images.githubusercontent.com/241032/129635365-c0ff2a9f-0ee5-4fc3-8bb6-006033cf67b8.png)
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...
@@ -450,6 +484,8 @@ add the argument `-e2ts` to also scan/index tags from music files, which brings
# server config
## 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:
@@ -479,7 +515,7 @@ if you set `--no-hash`, you can enable hashing for specific volumes using flag `
## upload rules
you can set upload rules using volume flags, some examples:
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`:
@@ -498,7 +534,9 @@ you can also set transaction limits which apply per-IP and per-volume, but these
## compress uploads
files can be autocompressed on upload, either on user-request (if config allows) or forced by server-config
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
@@ -519,6 +557,8 @@ some examples,
## 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
this can instead be kept in a single place using the `--hist` argument, or the `hist=` volume flag, or a mix of both:
@@ -532,6 +572,8 @@ note:
## 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:
* `-v ~/music::r:c,mte=title,artist` indexes and displays *title* followed by *artist*
@@ -555,6 +597,8 @@ see the beautiful mess of a dictionary in [mtag.py](https://github.com/9001/copy
## 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
* `-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
@@ -575,32 +619,38 @@ copyparty can invoke external programs to collect additional metadata for files
# browser support
TLDR: yes
![copyparty-ie4-fs8](https://user-images.githubusercontent.com/241032/118192791-fb31fe00-b446-11eb-9647-898ea8efc1f7.png)
`ie` = internet-explorer, `ff` = firefox, `c` = chrome, `iOS` = iPhone/iPad, `Andr` = Android
| feature | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
| --------------- | --- | --- | ---- | ---- | ----- | ---- | --- | ---- |
| browse files | yep | yep | yep | yep | yep | yep | yep | yep |
| basic uploader | 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 |
| set sort order | - | yep | yep | yep | yep | yep | yep | yep |
| zip selection | - | yep | yep | yep | yep | yep | yep | yep |
| navpane | - | - | `*1` | yep | yep | yep | yep | yep |
| up2k | - | - | yep | yep | yep | yep | yep | yep |
| markdown editor | - | - | yep | yep | yep | yep | yep | yep |
| markdown viewer | - | - | yep | yep | yep | yep | yep | yep |
| play mp3/m4a | - | yep | yep | yep | yep | yep | yep | yep |
| play ogg/opus | - | - | - | - | yep | yep | `*2` | 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 |
| --------------- | --- | ---- | ---- | ---- | ----- | ---- | --- | ---- |
| 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 |
| up2k | - | - | `*1` | `*1` | yep | yep | yep | yep |
| make directory | 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 |
| zip selection | - | yep | yep | yep | yep | yep | yep | yep |
| file rename | - | yep | 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 viewer | - | - | yep | yep | yep | yep | yep | yep |
| play mp3/m4a | - | yep | yep | yep | yep | yep | yep | yep |
| play ogg/opus | - | - | - | - | yep | yep | `*3` | yep |
| **= feature =** | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
* internet explorer 6 to 8 behave the same
* firefox 52 and chrome 49 are the last winxp versions
* `*1` only public folders (login session is dropped) and no history / back-button
* `*2` using a wasm decoder which can sometimes get stuck and consumes a bit more power
* `*1` yes, but extremely slow (ie10: 1 MiB/s, ie11: 270 KiB/s)
* `*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:
@@ -612,12 +662,15 @@ quick summary of more eccentric web-browsers trying to view a directory index:
| **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 |
| **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` |
| **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl |
# client examples
interact with copyparty using non-browser clients
* javascript: dump some state into a file (two separate examples)
* `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');`
@@ -660,7 +713,9 @@ quick outline of the up2k protocol, see [uploading](#uploading) for the web-clie
# 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:
@@ -677,11 +732,14 @@ below are some tweaks roughly ordered by usefulness:
# dependencies
mandatory deps:
* `jinja2` (is built into the SFX)
## optional dependencies
install these to enable bonus features
enable music tags:
* 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)
@@ -714,7 +772,7 @@ these are standalone programs and will never be imported / evaluated by copypart
# 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.sh](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.sh) -- smaller, but only for linux and macos, kinda deprecated
@@ -725,6 +783,8 @@ pls note that `copyparty-sfx.sh` will fail if you rename `copyparty-sfx.py` to `
## 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)
* `525k` size of original sfx.py as of v0.11.30
* `315k` after `./scripts/make-sfx.sh re no-ogv`
@@ -768,7 +828,7 @@ pip install black bandit pylint flake8 # vscode tooling
## 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
rm -rf copyparty/web/deps
@@ -788,7 +848,7 @@ then build the sfx using any of the following examples:
## 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:

View File

@@ -378,6 +378,7 @@ def run_argparse(argv, formatter):
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("--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-jpg", action="store_true", help="disable jpg output")
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")

View File

@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (0, 13, 2)
VERSION = (0, 13, 6)
CODENAME = "future-proof"
BUILD_DT = (2021, 8, 12)
BUILD_DT = (2021, 8, 27)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -956,10 +956,14 @@ class HttpCli(object):
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
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 = []
errmsg = ""
@@ -986,8 +990,6 @@ class HttpCli(object):
if lim:
lim.chk_bup(self.ip)
lim.chk_nup(self.ip)
if not nullwrite:
bos.makedirs(fdir)
try:
with ren_open(fname, "wb", 512 * 1024, **open_args) as f:
@@ -1038,7 +1040,9 @@ class HttpCli(object):
raise
except Pebkac as ex:
errmsg = volsan(self.asrv.vfs.all_vols.values(), 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)
sz_total = sum(x[0] for x in files)
@@ -1058,7 +1062,7 @@ class HttpCli(object):
errmsg = "ERROR: " + errmsg
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(
sha512[:56], sz, quotep(vpath), html_escape(ofn, crlf=True)
)
@@ -1695,7 +1699,7 @@ class HttpCli(object):
q = "select sz, rd, fn, at from up where ip=? and at>?"
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:
continue
@@ -1778,7 +1782,7 @@ class HttpCli(object):
for fn in self.args.th_covers.split(","):
fp = os.path.join(abspath, fn)
if bos.path.exists(fp):
vrem = "{}/{}".format(vrem.rstrip("/"), fn)
vrem = "{}/{}".format(vrem.rstrip("/"), fn).strip("/")
is_dir = False
break

View File

@@ -21,7 +21,7 @@ HAVE_AVIF = False
HAVE_WEBP = False
try:
from PIL import Image, ImageOps
from PIL import Image, ImageOps, ExifTags
HAVE_PIL = True
try:
@@ -105,7 +105,10 @@ class ThumbSrv(object):
self.mutex = threading.Lock()
self.busy = {}
self.stopping = False
self.nthr = os.cpu_count() if hasattr(os, "cpu_count") else 4
self.nthr = self.args.th_mt
if not self.nthr:
self.nthr = os.cpu_count() if hasattr(os, "cpu_count") else 4
self.q = Queue(self.nthr * 4)
for n in range(self.nthr):
t = threading.Thread(
@@ -221,21 +224,38 @@ class ThumbSrv(object):
with self.mutex:
self.nthr -= 1
def fancy_pillow(self, im):
# exif_transpose is expensive (loads full image + unconditional copy)
r = max(*self.res) * 2
im.thumbnail((r, r), resample=Image.LANCZOS)
try:
k = next(k for k, v in ExifTags.TAGS.items() if v == "Orientation")
exif = im.getexif()
rot = int(exif[k])
del exif[k]
except:
rot = 1
rots = {8: Image.ROTATE_90, 3: Image.ROTATE_180, 6: Image.ROTATE_270}
if rot in rots:
im = im.transpose(rots[rot])
if self.args.th_no_crop:
im.thumbnail(self.res, resample=Image.LANCZOS)
else:
iw, ih = im.size
dw, dh = self.res
res = (min(iw, dw), min(ih, dh))
im = ImageOps.fit(im, res, method=Image.LANCZOS)
return im
def conv_pil(self, abspath, tpath):
with Image.open(fsenc(abspath)) as im:
crop = not self.args.th_no_crop
res2 = self.res
if crop:
res2 = (res2[0] * 2, res2[1] * 2)
try:
im.thumbnail(res2, resample=Image.LANCZOS)
if crop:
iw, ih = im.size
dw, dh = self.res
res = (min(iw, dw), min(ih, dh))
im = ImageOps.fit(im, res, method=Image.LANCZOS)
except:
im = self.fancy_pillow(im)
except Exception as ex:
self.log("fancy_pillow {}".format(ex), "1;30")
im.thumbnail(self.res)
fmts = ["RGB", "L"]
@@ -289,6 +309,7 @@ class ThumbSrv(object):
b"-map", b"0:v:0",
b"-vf", scale,
b"-frames:v", b"1",
b"-metadata:s:v:0", b"rotate=0",
]
# fmt: on
@@ -306,6 +327,7 @@ class ThumbSrv(object):
]
cmd += [fsenc(tpath)]
# self.log((b" ".join(cmd)).decode("utf-8"))
ret, sout, serr = runcmd(cmd)
if ret != 0:

View File

@@ -1223,7 +1223,8 @@ class Up2k(object):
self._new_upload(job)
purl = "/{}/".format("{}/{}".format(job["vtop"], job["prel"]).strip("/"))
purl = "{}/{}".format(job["vtop"], job["prel"]).strip("/")
purl = "/{}/".format(purl) if purl else "/"
return {
"name": job["name"],
@@ -1427,7 +1428,11 @@ class Up2k(object):
ptop = vn.realpath
atop = vn.canonical(rem, False)
adir, fn = os.path.split(atop)
st = bos.lstat(atop)
try:
st = bos.lstat(atop)
except:
raise Pebkac(400, "file not found on disk (already deleted?)")
scandir = not self.args.no_scandir
if stat.S_ISLNK(st.st_mode) or stat.S_ISREG(st.st_mode):
dbv, vrem = self.asrv.vfs.get(vpath, uname, *permsets[0])

View File

@@ -19,7 +19,7 @@ import subprocess as sp # nosec
from datetime import datetime
from collections import Counter
from .__init__ import PY2, WINDOWS, ANYWIN
from .__init__ import PY2, WINDOWS, ANYWIN, VT100
from .stolen import surrogateescape
FAKE_MP = False
@@ -58,7 +58,7 @@ except:
return struct.unpack(f.decode("ascii"), *a, **ka)
ansi_re = re.compile("\033\\[[^m]*m")
ansi_re = re.compile("\033\\[[^mK]*[mK]")
surrogateescape.register_surrogateescape()
@@ -207,17 +207,22 @@ class ProgressPrinter(threading.Thread):
def run(self):
msg = None
fmt = " {}\033[K\r" if VT100 else " {} $\r"
while not self.end:
time.sleep(0.1)
if msg == self.msg or self.end:
continue
msg = self.msg
uprint(" {}\033[K\r".format(msg))
uprint(fmt.format(msg))
if PY2:
sys.stdout.flush()
print("\033[K", end="")
if VT100:
print("\033[K", end="")
elif msg:
print("------------------------")
sys.stdout.flush() # necessary on win10 even w/ stderr btw
@@ -346,10 +351,8 @@ def log_thrs(log, ival, name):
def vol_san(vols, txt):
for vol in vols:
txt = txt.replace(
vol.realpath.encode("utf-8"), vol.vpath.encode("utf-8")
)
txt = txt.replace(vol.realpath.encode("utf-8"), vol.vpath.encode("utf-8"))
return txt

View File

@@ -22,7 +22,7 @@ window.baguetteBox = (function () {
afterHide: null,
onChange: null,
},
overlay, slider, btnPrev, btnNext, btnHelp, btnSel, btnVmode, btnClose,
overlay, slider, btnPrev, btnNext, btnHelp, btnRotL, btnRotR, btnSel, btnVmode, btnClose,
currentGallery = [],
currentIndex = 0,
isOverlayVisible = false,
@@ -49,7 +49,7 @@ window.baguetteBox = (function () {
};
var touchstartHandler = function (e) {
touch.count++;
touch.count = e.touches.length;
if (touch.count > 1)
touch.multitouch = true;
@@ -72,8 +72,11 @@ window.baguetteBox = (function () {
hideOverlay();
}
};
var touchendHandler = function () {
var touchendHandler = function (e) {
touch.count--;
if (e && e.touches)
touch.count = e.touches.length;
if (touch.count <= 0)
touch.multitouch = false;
@@ -175,6 +178,8 @@ window.baguetteBox = (function () {
'<button id="bbox-next" class="bbox-btn" type="button" aria-label="Next">&gt;</button>' +
'<div id="bbox-btns">' +
'<button id="bbox-help" type="button">?</button>' +
'<button id="bbox-rotl" type="button">↶</button>' +
'<button id="bbox-rotr" type="button">↷</button>' +
'<button id="bbox-tsel" type="button">sel</button>' +
'<button id="bbox-vmode" type="button" tt="a"></button>' +
'<button id="bbox-close" type="button" aria-label="Close">X</button>' +
@@ -188,6 +193,8 @@ window.baguetteBox = (function () {
btnPrev = ebi('bbox-prev');
btnNext = ebi('bbox-next');
btnHelp = ebi('bbox-help');
btnRotL = ebi('bbox-rotl');
btnRotR = ebi('bbox-rotr');
btnSel = ebi('bbox-tsel');
btnVmode = ebi('bbox-vmode');
btnClose = ebi('bbox-close');
@@ -205,12 +212,13 @@ window.baguetteBox = (function () {
['right, L', 'next file'],
['home', 'first file'],
['end', 'last file'],
['R', 'rotate (shift=ccw)'],
['S', 'toggle file selection'],
['space, P, K', 'video: play / pause'],
['U', 'video: seek 10sec back'],
['P', 'video: seek 10sec ahead'],
['M', 'video: toggle mute'],
['R', 'video: toggle loop'],
['V', 'video: toggle loop'],
['C', 'video: toggle auto-next'],
['F', 'video: toggle fullscreen'],
],
@@ -252,7 +260,7 @@ window.baguetteBox = (function () {
v.muted = vmute = !vmute;
mp_ctl();
}
else if (k == "KeyR" && v) {
else if (k == "KeyV" && v) {
vloop = !vloop;
vnext = vnext && !vloop;
setVmode();
@@ -272,6 +280,8 @@ window.baguetteBox = (function () {
catch (ex) { }
else if (k == "KeyS")
tglsel();
else if (k == "KeyR")
rotn(e.shiftKey ? -1 : 1);
}
function setVmode() {
@@ -284,7 +294,7 @@ window.baguetteBox = (function () {
if (vloop) {
lbl = 'Loop';
msg += 'repeat it';
tts = '$NHotkey: R';
tts = '$NHotkey: V';
}
else if (vnext) {
lbl = 'Cont';
@@ -333,7 +343,7 @@ window.baguetteBox = (function () {
}
function selbg() {
var img = imagesElements[currentIndex].querySelector('img, video'),
var img = vidimg(),
thumb = currentGallery[currentIndex].imageElement,
name = vsplit(thumb.href)[1],
files = msel.getsel(),
@@ -346,11 +356,11 @@ window.baguetteBox = (function () {
ebi('bbox-overlay').style.background = sel ?
'rgba(153,34,85,0.7)' : '';
img.style.boxShadow = sel ? '0 0 3em #f4a' : '';
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) {
@@ -387,6 +397,8 @@ window.baguetteBox = (function () {
bind(btnClose, 'click', hideOverlay);
bind(btnVmode, 'click', tglVmode);
bind(btnHelp, 'click', halp);
bind(btnRotL, 'click', rotl);
bind(btnRotR, 'click', rotr);
bind(btnSel, 'click', tglsel);
bind(slider, 'contextmenu', contextmenuHandler);
bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
@@ -402,12 +414,15 @@ window.baguetteBox = (function () {
unbind(btnClose, 'click', hideOverlay);
unbind(btnVmode, 'click', tglVmode);
unbind(btnHelp, 'click', halp);
unbind(btnRotL, 'click', rotl);
unbind(btnRotR, 'click', rotr);
unbind(btnSel, 'click', tglsel);
unbind(slider, 'contextmenu', contextmenuHandler);
unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
unbind(overlay, 'touchend', touchendHandler);
unbind(document, 'focus', trapFocusInsideOverlay, true);
timer.rm(rotn);
}
function prepareOverlay(gallery, userOptions) {
@@ -658,10 +673,91 @@ window.baguetteBox = (function () {
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() {
return imagesElements[currentIndex].querySelector('video');
}
function vidimg() {
return imagesElements[currentIndex].querySelector('img, video');
}
function playvid(play) {
if (vid())
vid()[play ? 'play' : 'pause']();
@@ -703,15 +799,21 @@ window.baguetteBox = (function () {
}
function updateOffset() {
var offset = -currentIndex * 100 + '%';
var offset = -currentIndex * 100 + '%',
xform = slider.style.perspective !== undefined;
if (options.animation === 'fadeIn') {
slider.style.opacity = 0;
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;
}, 400);
} else {
slider.style.transform = 'translate3d(' + offset + ',0,0)';
xform ?
slider.style.transform = 'translate3d(' + offset + ',0,0)' :
slider.style.left = offset;
}
playvid(false);
var v = vid();
@@ -723,6 +825,18 @@ window.baguetteBox = (function () {
selbg();
mp_ctl();
setVmode();
var el = vidimg();
if (el.getAttribute('rot'))
timer.add(rotn);
else
timer.rm(rotn);
var prev = QS('.full-image.vis');
if (prev)
clmod(prev, 'vis');
clmod(el.closest('div'), 'vis', 1);
}
function preloadNext(index) {

View File

@@ -1,6 +1,10 @@
:root {
--grid-sz: 10em;
}
@font-face {
font-family: 'scp';
src: local('Source Code Pro Regular'), local('SourceCodePro-Regular'), url(/.cpr/deps/scp.woff2) format('woff2');
}
* {
line-height: 1.2em;
}
@@ -23,7 +27,7 @@ html, body {
padding: 0;
}
pre, code, tt {
font-family: monospace, monospace;
font-family: 'scp', monospace, monospace;
}
#path,
#path * {
@@ -113,7 +117,7 @@ a, #files tbody div a:last-child {
border-top: 1px solid #383838;
}
#files tbody td:nth-child(3) {
font-family: monospace, monospace;
font-family: 'scp', monospace, monospace;
text-align: right;
padding-right: 1em;
white-space: nowrap;
@@ -417,17 +421,10 @@ html.light #ggrid a.sel {
html.light #wfm a:not(.en) {
color: #c4a;
}
#files tbody tr.c1 td {
animation: fcut1 .5s ease-out;
#files tbody tr.fcut td {
animation: fcut .5s ease-out;
}
#files tbody tr.c2 td {
animation: fcut2 .5s ease-out;
}
@keyframes fcut1 {
0% {opacity:0}
100% {opacity:1}
}
@keyframes fcut2 {
@keyframes fcut {
0% {opacity:0}
100% {opacity:1}
}
@@ -533,24 +530,9 @@ html.light #wfm a:not(.en) {
box-shadow: 0 -.15em .2em #000 inset;
padding-bottom: .3em;
}
#ops i {
font-size: 1.5em;
}
#ops i:before {
content: 'x';
color: #282828;
text-shadow: 0 0 .08em #01a7e1;
position: relative;
}
#ops i:after {
content: 'x';
color: #282828;
text-shadow: 0 0 .08em #ff3f1a;
margin-left: -.35em;
font-size: 1.05em;
}
#ops,
.opbox {
.opbox,
#u2etas {
border: 1px solid #3a3a3a;
box-shadow: 0 0 1em #222 inset;
}
@@ -699,6 +681,7 @@ input.eq_gain {
left: 0;
bottom: 0;
top: 7em;
overflow-x: hidden;
overflow-y: auto;
-ms-scroll-chaining: none;
overscroll-behavior-y: none;
@@ -709,6 +692,10 @@ input.eq_gain {
position: sticky;
z-index: 1;
top: 0;
height: 2.2em;
line-height: 2.2em;
border-bottom: 1px solid #555;
overflow: hidden;
}
#thx_ff {
padding: 5em 0;
@@ -726,8 +713,8 @@ input.eq_gain {
}
#treeul {
position: relative;
left: -1.7em;
width: calc(100% + 1.3em);
left: -2em;
width: calc(100% + 2em);
}
.btn {
padding: .2em .4em;
@@ -788,7 +775,9 @@ input.eq_gain {
color: #fff;
}
#treeul a:first-child {
font-family: monospace, monospace;
font-family: 'scp', monospace, monospace;
font-size: 1.2em;
line-height: 0;
}
.dumb_loader_thing {
display: inline-block;
@@ -896,6 +885,7 @@ html.light #ghead {
}
#ggrid a {
display: inline-block;
width: 10em;
width: var(--grid-sz);
vertical-align: top;
overflow-wrap: break-word;
@@ -909,7 +899,9 @@ html.light #ghead {
}
#ggrid a img {
border-radius: .2em;
max-width: 10em;
max-width: var(--grid-sz);
max-height: 8em;
max-height: calc(var(--grid-sz)/1.25);
margin: 0 auto;
display: block;
@@ -978,7 +970,7 @@ html.light #rui {
padding: .2em 0 .2em .5em;
}
#rn_vadv input {
font-family: monospace, monospace;
font-family: 'scp', monospace, monospace;
}
#rui td+td,
#rui td input[type="text"] {
@@ -1011,8 +1003,12 @@ html.light #rui {
#pvol,
#barbuf,
#barpos,
a.btn,
#u2btn,
#u2conf label,
#rui label,
#modal-ok,
#modal-ng,
#ops {
-webkit-user-select: none;
-moz-user-select: none;
@@ -1051,7 +1047,8 @@ html.light {
}
html.light #ops,
html.light .opbox,
html.light #srch_form {
html.light #srch_form,
html.light #u2etas {
background: #f7f7f7;
box-shadow: 0 0 .3em #ddd;
border-color: #f7f7f7;
@@ -1251,6 +1248,7 @@ html.light #files tr.sel a:hover {
}
html.light #treeh {
background: #eee;
border-color: #ddd;
}
html.light #tree {
scrollbar-color: #a70 #ddd;
@@ -1294,7 +1292,7 @@ html.light #tree::-webkit-scrollbar {
opacity: 0;
position: fixed;
overflow: hidden;
touch-action: none;
touch-action: pinch-zoom;
top: 0;
left: 0;
width: 100%;
@@ -1328,6 +1326,14 @@ html.light #tree::-webkit-scrollbar {
max-height: calc(100% - 1.4em);
margin-bottom: 1.4em;
vertical-align: middle;
transition: transform .23s, left .23s, top .23s, width .23s, height .23s;
}
.full-image img.nt,
.full-image video.nt {
transition: none;
}
.full-image.vis img,
.full-image.vis video {
box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
}
.full-image video {
@@ -1341,6 +1347,7 @@ html.light #tree::-webkit-scrollbar {
text-align: center;
white-space: normal;
color: #ccc;
z-index: 1;
}
#bbox-overlay figcaption a {
background: rgba(0, 0, 0, 0.6);
@@ -1545,7 +1552,7 @@ html.light #bbox-overlay figcaption a {
color: #333;
}
#u2conf #u2btn {
margin: -1.5em 0;
margin: -2.4em 0;
padding: .8em 0;
width: 100%;
max-width: 12em;
@@ -1564,6 +1571,7 @@ html.light #bbox-overlay figcaption a {
line-height: 1.3em;
}
#u2tab {
border-collapse: collapse;
margin: 3em auto;
width: calc(100% - 2em);
max-width: 100em;
@@ -1574,7 +1582,7 @@ html.light #bbox-overlay figcaption a {
#u2tab td {
border: 1px solid #ccc;
border-width: 0 0px 1px 0;
padding: .1em .3em;
padding: .2em .3em;
}
#u2tab td:nth-child(2) {
width: 5em;
@@ -1591,17 +1599,50 @@ html.light #bbox-overlay figcaption a {
#u2tab tbody tr:hover td {
background: #222;
}
#u2etas {
background: #333;
padding: .2em .5em;
border-radius: .5em;
border-width: .25em 0;
width: 17em;
text-align: center;
white-space: nowrap;
display: inline-block;
font-family: 'scp', monospace, monospace;
}
#u2etas.o {
width: 20em;
}
#u2etas .o {
display: none;
}
#u2etas.o .o {
display: unset;
}
#u2etaw {
width: 18em;
font-size: .94em;
margin: 1.8em auto .5em auto;
}
#u2etas.o #u2etaw {
width: 21em;
}
#u2cards {
padding: 1em 0 .3em 1em;
margin: 1.5em auto -2.5em auto;
padding: 1em 1em .3em 1em;
margin: 0 auto -1.5em auto;
white-space: nowrap;
text-align: center;
overflow: hidden;
}
#u2cards.w {
width: 45em;
width: 44em;
text-align: left;
}
#u2etaw.w {
width: 52em;
text-align: right;
margin: 3em auto -2.7em auto;
}
#u2cards a {
padding: .2em 1em;
border: 1px solid #777;
@@ -1708,6 +1749,14 @@ html.light #bbox-overlay figcaption a {
box-shadow: none;
opacity: .2;
}
#u2flagblock {
display: none;
margin: 2em auto 4em auto;
padding: .5em;
max-width: 27em;
text-align: center;
border: .2em dashed rgba(128, 128, 128, 0.3);
}
#u2foot {
color: #fff;
font-style: italic;
@@ -1742,7 +1791,27 @@ html.light #u2foot .warn span {
margin-bottom: -1em;
}
.prog {
font-family: monospace, monospace;
font-family: 'scp', monospace, monospace;
}
#u2tab span.inf,
#u2tab span.ok,
#u2tab span.err {
color: #fff;
padding: .22em;
border-radius: .2em;
border: 2px solid #f0f;
}
#u2tab span.inf {
background: #07a;
border-color: #0be;
}
#u2tab span.ok {
background: #380;
border-color: #8e4;
}
#u2tab span.err {
background: #900;
border-color: #d06;
}
#u2tab a>span {
font-weight: bold;

View File

@@ -43,7 +43,7 @@ ebi('widget').innerHTML = (
' href="#" id="npirc" tt="copy irc-formatted track info">📋irc</a><a' +
' href="#" id="nptxt" tt="copy plaintext track info">📋txt</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>' +
'</div>' +
'<div id="widgeti">' +
@@ -102,6 +102,12 @@ ebi('op_up2k').innerHTML = (
' </div>\n' +
'</div>\n' +
'<div id="u2etaw"><div id="u2etas"><div class="o">\n' +
' hash: <span id="u2etah" tt="average &lt;em&gt;hashing&lt;/em&gt; speed, and estimated time until finish">(no uploads are queued yet)</span><br />\n' +
' send: <span id="u2etau" tt="average &lt;em&gt;upload&lt;/em&gt; 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 &lt;em&gt;total&lt;/em&gt; speed and estimated time until finish">(no uploads are queued yet)</span>\n' +
'</div></div>\n' +
'<div id="u2cards">\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' +
@@ -121,6 +127,7 @@ ebi('op_up2k').innerHTML = (
' <tbody></tbody>\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="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="twig" tt="Hotkey: D">&ndash;</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' +
'<ul id="treeul"></ul>\n' +
'<div id="thx_ff">&nbsp;</div>'
@@ -1352,7 +1360,7 @@ function play(tid, is_ev, seek, call_depth) {
return true;
}
catch (ex) {
toast.err(0, 'playback failed: ' + ex);
toast.err(0, esc('playback failed: ' + ex));
}
setclass(oid, 'play');
setTimeout(next_song, 500);
@@ -1386,7 +1394,7 @@ function evau_error(e) {
err += '\n\nFile: «' + uricom_dec(eplaya.src.split('/').slice(-1)[0])[0] + '»';
toast.warn(15, err);
toast.warn(15, esc(err + ''));
}
@@ -1657,7 +1665,7 @@ var fileman = (function () {
r.clip = jread('fman_clip', []);
var nsel = msel.getsel().length;
clmod(bren, 'en', nsel == 1);
clmod(bren, 'en', nsel);
clmod(bdel, 'en', nsel);
clmod(bcut, 'en', nsel);
clmod(bpst, 'en', r.clip && r.clip.length);
@@ -1689,7 +1697,7 @@ var fileman = (function () {
var vsp = vsplit(vp);
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'));
mkeys = vars[1].concat(vars[2]);
@@ -1905,7 +1913,7 @@ var fileman = (function () {
re = new RegExp(ptn, cs ? 'i' : '');
}
catch (ex) {
return toast.err(5, 'invalid regex:\n' + ex);
return toast.err(5, esc('invalid regex:\n' + ex));
}
toast.hide();
@@ -1937,7 +1945,7 @@ var fileman = (function () {
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);
function rename_cb() {
@@ -1984,7 +1992,7 @@ var fileman = (function () {
treectl.goto(get_evpath());
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.onreadystatechange = delete_cb;
@@ -2002,8 +2010,8 @@ var fileman = (function () {
deleter();
}
modal.confirm('===== DANGER =====\nDELETE these ' + vps.length + ' items?\n\n' + uricom_adec(vps).join('\n'), function () {
modal.confirm('Last chance! Delete?', deleter, null);
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);
};
@@ -2018,17 +2026,21 @@ var fileman = (function () {
if (!sel.length)
return toast.err(3, 'select at least 1 item to cut');
if (sel.length < 100)
for (var a = 0; a < sel.length; a++) {
vps.push(sel[a].vp);
var cl = ebi(sel[a].id).closest('tr').classList,
inv = cl.contains('c1');
cl.remove(inv ? 'c1' : 'c2');
cl.add(inv ? 'c2' : 'c1');
var els = [];
for (var a = 0; a < sel.length; a++) {
vps.push(sel[a].vp);
if (sel.length < 100) {
els.push(ebi(sel[a].id).closest('tr'));
clmod(els[a], 'fcut');
}
}
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);
r.tx(1);
};
@@ -2063,7 +2075,7 @@ var fileman = (function () {
}
if (exists.length)
toast.warn(30, '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)
return;
@@ -2078,7 +2090,7 @@ var fileman = (function () {
r.tx(srcdir);
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];
@@ -2098,7 +2110,7 @@ var fileman = (function () {
paster();
}
modal.confirm('paste these ' + req.length + ' items here?\n\n' + uricom_adec(req).join('\n'), function () {
modal.confirm('paste these ' + req.length + ' items here?<ul>' + uricom_adec(req, true).join('') + '</ul>', function () {
paster();
jwrite('fman_clip', []);
}, null);
@@ -2391,7 +2403,8 @@ var thegrid = (function () {
})();
function tree_scrollto() {
function tree_scrollto(e) {
ev(e);
var act = QS('#treeul a.hl'),
ul = act ? act.offsetParent : null;
@@ -2851,7 +2864,7 @@ var treectl = (function () {
var treectl = {
"hidden": true,
"ls_cb": null,
"dir_cb": null
"dir_cb": tree_scrollto
},
entreed = false,
fixedpos = false,
@@ -3155,7 +3168,13 @@ var treectl = (function () {
}
html.push('</tbody>');
html = html.join('\n');
ebi('files').innerHTML = html;
try {
ebi('files').innerHTML = html;
}
catch (ex) { //ie9
window.location.href = this.top;
return;
}
if (this.hpush)
hist_push(this.top);
@@ -3239,6 +3258,7 @@ var treectl = (function () {
ebi('entree').onclick = treectl.entree;
ebi('detree').onclick = treectl.detree;
ebi('visdir').onclick = tree_scrollto;
ebi('dotfiles').onclick = tdots;
ebi('dyntree').onclick = dyntree;
ebi('twig').onclick = scaletree;
@@ -3775,7 +3795,7 @@ var msel = (function () {
if (r.all && r.all.length) {
for (var a = 0; a < r.all.length; a++) {
var ao = r.all[a];
ao.sel = ebi(ao.id).closest('tr').classList.contains('sel');
ao.sel = clgot(ebi(ao.id).closest('tr'), 'sel');
if (ao.sel)
r.sel.push(ao);
}
@@ -3791,7 +3811,7 @@ var msel = (function () {
item = {};
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;
r.all.push(item);

View File

@@ -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.textContent = msg;
errbox.onclick = function () {
modal.alert('<pre>' + ex.stack + '</pre>');
modal.alert('<pre>' + esc(ex.stack) + '</pre>');
};
if (o) {
errbox.appendChild(o);

View File

@@ -370,8 +370,8 @@ function save_cb() {
}
if (!r.ok) {
if (!this.btn.classList.contains('force-save')) {
this.btn.classList.add('force-save');
if (!clgot(this.btn, 'force-save')) {
clmod(this.btn, 'force-save', 1);
var msg = [
'This file has been modified since you started editing it!\n',
'if you really want to overwrite, press save again.\n',
@@ -387,7 +387,7 @@ function save_cb() {
return toast.err(0, 'Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
}
this.btn.classList.remove('force-save');
clmod(this.btn, 'force-save');
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
run_savechk(r.lastmod, this.txt, this.btn, 0);

View File

@@ -96,20 +96,16 @@ function md_changed(mde, on_srv) {
var md_now = mde.value();
var save_btn = QS('.editor-toolbar button.save');
if (md_now == window.md_saved)
save_btn.classList.add('disabled');
else
save_btn.classList.remove('disabled');
clmod(save_btn, 'disabled', md_now == window.md_saved);
set_jumpto();
}
function save(mde) {
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');
var force = save_btn.classList.contains('force-save');
var force = clgot(save_btn, 'force-save');
function save2() {
var txt = mde.value();
@@ -153,8 +149,8 @@ function save_cb() {
}
if (!r.ok) {
if (!this.btn.classList.contains('force-save')) {
this.btn.classList.add('force-save');
if (!clgot(this.btn, 'force-save')) {
clmod(this.btn, 'force-save', 1);
var msg = [
'This file has been modified since you started editing it!\n',
'if you really want to overwrite, press save again.\n',
@@ -170,7 +166,7 @@ function save_cb() {
return toast.err(0, 'Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
}
this.btn.classList.remove('force-save');
clmod(this.btn, 'force-save');
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
// download the saved doc from the server and compare

View File

@@ -73,7 +73,7 @@
background: #8e4;
}
#toast.warn {
background: #970;
background: #960;
border-color: #fc0;
}
#toast.warn #toastc {
@@ -165,6 +165,16 @@ html.light #tt em {
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;

View File

@@ -141,6 +141,15 @@ function U2pvis(act, btns) {
r.tail = -1;
r.wsz = 3;
var markup = {
'404': '<span class="err">404</span>',
'ERROR': '<span class="err">ERROR</span>',
'OS-error': '<span class="err">OS-error</span>',
'found': '<span class="inf">found</span>',
'YOLO': '<span class="inf">YOLO</span>',
'done': '<span class="ok">done</span>',
};
r.addfile = function (entry, sz, draw) {
r.tab.push({
"hn": entry[0],
@@ -185,7 +194,7 @@ function U2pvis(act, btns) {
return;
var obj = ebi('f{0}{1}'.format(nfile, field.slice(1)));
obj.innerHTML = html;
obj.innerHTML = field == 'ht' ? (markup[html] || html) : html;
if (field == 'hp') {
obj.style.color = '';
obj.style.background = '';
@@ -227,7 +236,7 @@ function U2pvis(act, btns) {
p = r.perc(nb, 0, fobj.size, fobj.t_hashing);
fo.hp = '{0}%, {1}, {2} MB/s'.format(
p[0].toFixed(2), p[1], p[2].toFixed(2)
f2f(p[0], 2), p[1], f2f(p[2], 2)
);
if (!r.is_act(fo.in))
return;
@@ -249,7 +258,7 @@ function U2pvis(act, btns) {
var p = r.perc(fo.bd, fo.bd0, fo.bt, fobj.t_uploading);
fo.hp = '{0}%, {1}, {2} MB/s'.format(
p[0].toFixed(2), p[1], p[2].toFixed(2)
f2f(p[0], 2), p[1], f2f(p[2], 2)
);
if (!r.is_act(fo.in))
@@ -424,7 +433,7 @@ function U2pvis(act, btns) {
td1 = '<td id="f' + nfile,
td = '</td>' + td1,
ret = td1 + 'n">' + row.hn +
td + 't">' + row.ht +
td + 't">' + (markup[row.ht] || row.ht) +
td + 'p" class="prog">' + row.hp + '</td>';
if (as_html)
@@ -511,7 +520,7 @@ function up2k_init(subtle) {
import_js('/.cpr/deps/' + fn, unmodal);
if (is_https)
ebi('u2foot').innerHTML = shame + ' so <em>this</em> uploader will do like 500kB/s at best';
ebi('u2foot').innerHTML = shame + ' so <em>this</em> uploader will do like 500 KiB/s at best';
else
ebi('u2foot').innerHTML = 'seems like ' + shame + ' so do that if you want more performance <span style="color:#' +
(sha_js == 'ac' ? 'c84">(expecting 20' : '8a5">(but dont worry too much, expect 100') + ' MiB/s)</span>';
@@ -587,8 +596,15 @@ function up2k_init(subtle) {
"upload": []
},
"bytes": {
"total": 0,
"hashed": 0,
"uploaded": 0
"uploaded": 0,
"finished": 0
},
"time": {
"hashing": 0,
"uploading": 0,
"busy": 0
}
};
@@ -709,11 +725,11 @@ function up2k_init(subtle) {
match = false;
if (match) {
var msg = ['directory iterator got stuck on the following {0} items; good chance your browser is about to spinlock:'.format(missing.length)];
var msg = ['directory iterator got stuck on the following {0} items; good chance your browser is about to spinlock:<ul>'.format(missing.length)];
for (var a = 0; a < Math.min(20, missing.length); a++)
msg.push(missing[a]);
msg.push('<li>' + esc(missing[a]) + '</li>');
return modal.alert(msg.join('\n-- '), function () {
return modal.alert(msg.join('') + '</ul>', function () {
read_dirs(rd, [], [], good, bad, spins);
});
}
@@ -784,12 +800,12 @@ function up2k_init(subtle) {
});
}
var msg = ['upload these ' + good_files.length + ' files?'];
var msg = ['{0} these {1} files?<ul>'.format(fsearch ? 'search' : 'upload', good_files.length)];
for (var a = 0, aa = Math.min(20, good_files.length); a < aa; a++)
msg.push(good_files[a][1]);
msg.push('<li>' + esc(good_files[a][1]) + '</li>');
if (ask_up && !fsearch)
return modal.confirm(msg.join('\n'), function () { up_them(good_files); }, null);
return modal.confirm(msg.join('') + '</ul>', function () { up_them(good_files); }, null);
up_them(good_files);
}
@@ -805,13 +821,13 @@ function up2k_init(subtle) {
for (var a = 0; a < good_files.length; a++) {
var fobj = good_files[a][0],
name = good_files[a][1],
fdir = '',
fdir = evpath,
now = Date.now(),
lmod = fobj.lastModified || now,
ofs = name.lastIndexOf('/') + 1;
if (ofs) {
fdir = name.slice(0, ofs);
fdir += url_enc(name.slice(0, ofs));
name = name.slice(ofs);
}
@@ -828,6 +844,9 @@ function up2k_init(subtle) {
},
key = entry.name + '\n' + entry.size;
if (fsearch)
entry.srch = 1;
if (seen[key])
continue;
@@ -840,6 +859,7 @@ function up2k_init(subtle) {
''
], fobj.size, draw_each);
st.bytes.total += fobj.size;
st.files.push(entry);
if (turbo)
push_t(st.todo.head, entry);
@@ -878,6 +898,75 @@ function up2k_init(subtle) {
}
ebi('u2cleanup').onclick = u2cleanup;
var etaref = 0, etaskip = 0;
function etafun() {
var nhash = st.busy.head.length + st.busy.hash.length + st.todo.head.length + st.todo.hash.length,
nsend = st.busy.upload.length + st.todo.upload.length,
now = Date.now(),
td = (now - (etaref || now)) / 1000.0;
etaref = now;
//ebi('acc_info').innerHTML = f2f(st.time.busy, 1) + ' ' + f2f(now / 1000, 1);
if (!nhash)
ebi('u2etah').innerHTML = 'Done ({0}, {1} files)'.format(humansize(st.bytes.hashed), pvis.ctr["ok"] + pvis.ctr["ng"]);
if (!nsend && !nhash)
ebi('u2etau').innerHTML = ebi('u2etat').innerHTML = (
'Done ({0}, {1} files)'.format(humansize(st.bytes.uploaded), pvis.ctr["ok"] + pvis.ctr["ng"]));
if (!st.busy.hash.length && !hashing_permitted())
nhash = 0;
if (!parallel_uploads || !(nhash + nsend) || (flag && flag.owner && !flag.ours))
return;
var t = [];
if (nhash) {
st.time.hashing += td;
t.push(['u2etah', st.bytes.hashed, st.bytes.hashed, st.time.hashing]);
if (fsearch)
t.push(['u2etat', st.bytes.hashed, st.bytes.hashed, st.time.hashing]);
}
if (nsend) {
st.time.uploading += td;
t.push(['u2etau', st.bytes.uploaded, st.bytes.finished, st.time.uploading]);
}
if ((nhash || nsend) && !fsearch) {
if (!st.bytes.finished) {
ebi('u2etat').innerHTML = '(preparing to upload)';
}
else {
st.time.busy += td;
t.push(['u2etat', st.bytes.finished, st.bytes.finished, st.time.busy]);
}
}
for (var a = 0; a < t.length; a++) {
var rem = st.bytes.total - t[a][2],
bps = t[a][1] / t[a][3],
eta = Math.floor(rem / bps);
if (t[a][1] < 1024 || t[a][3] < 0.1) {
ebi(t[a][0]).innerHTML = '(preparing to upload)';
continue;
}
if (etaskip)
continue;
if (eta < 60 * 60 * 24)
try {
eta = /.*(..:..:..).*/.exec(new Date(eta * 1000).toUTCString())[1];
}
catch (ex) { }
ebi(t[a][0]).innerHTML = '{0}, {1}/s, {2}'.format(
humansize(rem), humansize(bps, 1), eta);
}
if (++etaskip > 2)
etaskip = 0;
}
/////
////
/// actuator
@@ -916,8 +1005,11 @@ function up2k_init(subtle) {
}
function hashing_permitted() {
if (!parallel_uploads)
return false;
if (multitask) {
var ahead = st.bytes.hashed - st.bytes.uploaded;
var ahead = st.bytes.hashed - st.bytes.finished;
return ahead < 1024 * 1024 * 1024 * 4 &&
st.todo.handshake.length + st.busy.handshake.length < 16;
}
@@ -961,11 +1053,40 @@ function up2k_init(subtle) {
window[(is_busy ? "add" : "remove") +
"EventListener"]("beforeunload", warn_uploader_busy);
if (!is_busy) {
var k = fsearch ? 'searches' : 'uploads',
ks = fsearch ? 'Search' : 'Upload',
tok = fsearch ? 'successful (found on server)' : 'completed successfully',
tng = fsearch ? 'failed (NOT found on server)' : 'failed, sorry',
ok = pvis.ctr["ok"],
ng = pvis.ctr["ng"],
t = ask_up ? 0 : 10;
if (ok && ng)
toast.warn(t, 'Finished, but some {0} failed:\n{1} {2},\n{3} {4}'.format(k, ok, tok, ng, tng));
else if (ok > 1)
toast.ok(t, 'All {1} {0} {2}'.format(k, ok, tok));
else if (ok)
toast.ok(t, '{0} {1}'.format(ks, tok));
else if (ng > 1)
toast.err(t, 'All {1} {0} {2}'.format(k, ng, tng));
else if (ng)
toast.err(t, '{0} {1}'.format(ks, tng));
timer.rm(etafun);
}
else {
timer.add(etafun, false);
ebi('u2etas').style.textAlign = 'left';
}
etafun();
}
if (flag) {
if (is_busy) {
flag.take(now);
ebi('u2flagblock').style.display = flag.ours ? '' : 'block';
if (!flag.ours)
return defer();
}
@@ -1101,7 +1222,6 @@ function up2k_init(subtle) {
function exec_hash() {
var t = st.todo.hash.shift();
st.busy.hash.push(t);
st.bytes.hashed += t.size;
t.bytes_uploaded = 0;
var bpend = 0,
@@ -1127,6 +1247,7 @@ function up2k_init(subtle) {
cdr = t.size;
bpend += cdr - car;
st.bytes.hashed += cdr - car;
function orz(e) {
if (!min_filebuf && nch == 1) {
@@ -1157,11 +1278,11 @@ function up2k_init(subtle) {
if (handled) {
pvis.move(t.n, 'ng');
apop(st.busy.hash, t);
st.bytes.uploaded += t.size;
st.bytes.finished += t.size;
return tasker();
}
toast.err(0, 'y o u b r o k e i t\nfile: ' + t.name + '\nerror: ' + err);
toast.err(0, 'y o u b r o k e i t\nfile: ' + esc(t.name + '') + '\nerror: ' + err);
};
reader.readAsArrayBuffer(
bobslice.call(t.fobj, car, cdr));
@@ -1249,21 +1370,22 @@ function up2k_init(subtle) {
}
}
apop(st.busy.head, t);
if (!ok)
if (!ok && !t.srch)
return push_t(st.todo.hash, t);
t.done = true;
st.bytes.hashed += t.size;
st.bytes.uploaded += t.size;
pvis.seth(t.n, 1, 'YOLO');
st.bytes.finished += t.size;
pvis.move(t.n, 'bz');
pvis.seth(t.n, 1, ok ? 'YOLO' : '404');
pvis.seth(t.n, 2, "turbo'd");
pvis.move(t.n, 'ok');
pvis.move(t.n, ok ? 'ok' : 'ng');
};
xhr.onload = function (e) {
try { orz(e); } catch (ex) { vis_exh(ex + '', '', '', '', ex); }
};
xhr.open('HEAD', t.purl + t.name, true);
xhr.open('HEAD', t.purl + uricom_enc(t.name) + '?raw', true);
xhr.send();
}
@@ -1334,18 +1456,19 @@ function up2k_init(subtle) {
pvis.seth(t.n, 1, smsg);
pvis.move(t.n, smsg == '404' ? 'ng' : 'ok');
apop(st.busy.handshake, t);
st.bytes.uploaded += t.size;
st.bytes.finished += t.size;
t.done = true;
tasker();
return;
}
if (response.purl !== t.purl || response.name !== t.name) {
var rsp_purl = url_enc(response.purl);
if (rsp_purl !== t.purl || response.name !== t.name) {
// server renamed us (file exists / path restrictions)
console.log("server-rename [" + t.purl + "] [" + t.name + "] to [" + response.purl + "] [" + response.name + "]");
t.purl = response.purl;
console.log("server-rename [" + t.purl + "] [" + t.name + "] to [" + rsp_purl + "] [" + response.name + "]");
t.purl = rsp_purl;
t.name = response.name;
pvis.seth(t.n, 0, linksplit(t.purl + t.name).join(' '));
pvis.seth(t.n, 0, linksplit(uricom_dec(t.purl)[0] + t.name).join(' '));
}
var chunksize = get_chunksize(t.size),
@@ -1374,7 +1497,7 @@ function up2k_init(subtle) {
pvis.prog(t, 0, cbd[0]);
var done = true,
msg = '&#x1f3b7;&#x1f41b;';
msg = 'done';
if (t.postlist.length) {
var arr = st.todo.upload,
@@ -1401,12 +1524,12 @@ function up2k_init(subtle) {
if (done) {
t.done = true;
st.bytes.uploaded += t.size - t.bytes_uploaded;
st.bytes.finished += t.size - t.bytes_uploaded;
var spd1 = (t.size / ((t.t_hashed - t.t_hashing) / 1000.)) / (1024 * 1024.),
spd2 = (t.size / ((t.t_uploaded - t.t_uploading) / 1000.)) / (1024 * 1024.);
pvis.seth(t.n, 2, 'hash {0}, up {1} MB/s'.format(
spd1.toFixed(2), spd2.toFixed(2)));
f2f(spd1, 2), isNaN(spd2) ? '--' : f2f(spd2, 2)));
pvis.move(t.n, 'ok');
}
@@ -1434,7 +1557,7 @@ function up2k_init(subtle) {
return;
}
st.bytes.uploaded += t.size;
st.bytes.finished += t.size;
if (rsp.indexOf('partial upload exists') !== -1 ||
rsp.indexOf('file already exists') !== -1) {
err = rsp;
@@ -1469,7 +1592,7 @@ function up2k_init(subtle) {
"lmod": t.lmod,
"hash": t.hash
};
if (fsearch)
if (t.srch)
req.srch = 1;
xhr.open('POST', t.purl, true);
@@ -1505,6 +1628,7 @@ function up2k_init(subtle) {
var txt = ((xhr.response && xhr.response.err) || xhr.responseText) + '';
if (xhr.status == 200) {
pvis.prog(t, npart, cdr - car);
st.bytes.finished += cdr - car;
st.bytes.uploaded += cdr - car;
t.bytes_uploaded += cdr - car;
}
@@ -1572,6 +1696,7 @@ function up2k_init(subtle) {
parent.appendChild(btn);
ebi('u2conf').setAttribute('class', wide ? 'has_btn' : '');
ebi('u2cards').setAttribute('class', wide ? 'w' : '');
ebi('u2etaw').setAttribute('class', wide ? 'w' : '');
}
}
window.addEventListener('resize', onresize);
@@ -1659,7 +1784,7 @@ function up2k_init(subtle) {
function draw_turbo() {
var msgu = '<p class="warn">WARNING: turbo enabled, <span>&nbsp;client may not detect and resume incomplete uploads; see turbo-button tooltip</span></p>',
msgs = '<p class="warn">WARNING: turbo enabled, <span>&nbsp;search may give false-positives; see turbo-button tooltip</span></p>',
msgs = '<p class="warn">WARNING: turbo enabled, <span>&nbsp;search results can be incorrect; see turbo-button tooltip</span></p>',
msg = fsearch ? msgs : msgu,
omsg = fsearch ? msgu : msgs,
html = ebi('u2foot').innerHTML,
@@ -1703,11 +1828,10 @@ function up2k_init(subtle) {
catch (ex) { }
try {
var fun = fsearch ? 'add' : 'remove',
ico = fsearch ? '🔎' : '🚀',
var ico = fsearch ? '🔎' : '🚀',
desc = fsearch ? 'Search' : 'Upload';
ebi('op_up2k').classList[fun]('srch');
clmod(ebi('op_up2k'), 'srch', fsearch);
ebi('u2bm').innerHTML = ico + ' <sup>' + desc + '</sup>';
}
catch (ex) { }
@@ -1733,6 +1857,9 @@ function up2k_init(subtle) {
}
}
else if (!flag_en && flag) {
if (flag.ours)
flag.give();
flag.ch.close();
flag = false;
}
@@ -1767,6 +1894,10 @@ function up2k_init(subtle) {
for (var a = nodes.length - 1; a >= 0; a--)
nodes[a].addEventListener('touchend', nop, false);
ebi('u2etas').onclick = function () {
clmod(ebi('u2etas'), 'o', 't');
};
set_fsearch();
bumpthread({ "target": 1 });
if (parallel_uploads < 1)

View File

@@ -123,11 +123,18 @@ if (!String.startsWith) {
return this.substring(i, i + s.length) === s;
};
}
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.oMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.webkitMatchesSelector;
}
if (!Element.prototype.closest) {
Element.prototype.closest = function (s) {
var el = this;
do {
if (el.msMatchesSelector(s)) return el;
if (el.matches(s)) return el;
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
}
@@ -170,12 +177,28 @@ function crc32(str) {
}
function clmod(obj, cls, add) {
function clmod(el, cls, add) {
if (el.classList) {
if (add == 't')
add = el.classList.contains(cls) ? 0 : 1;
return el.classList[add ? 'add' : 'remove'](cls);
}
var re = new RegExp('\\s*\\b' + cls + '\\s*\\b', 'g');
if (add == 't')
add = !re.test(obj.className);
add = !re.test(el.className);
obj.className = obj.className.replace(re, ' ') + (add ? ' ' + cls : '');
el.className = el.className.replace(re, ' ') + (add ? ' ' + cls : '');
}
function clgot(el, cls) {
if (el.classList)
return el.classList.contains(cls);
var lst = (el.getAttribute('class') + '').split(/ /g);
return has(lst, cls);
}
@@ -308,6 +331,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) {
try {
@@ -320,10 +353,12 @@ function uricom_dec(txt) {
}
function uricom_adec(arr) {
function uricom_adec(arr, li) {
var ret = [];
for (var a = 0; a < arr.length; a++)
ret.push(uricom_dec(arr[a])[0]);
for (var a = 0; a < arr.length; a++) {
var txt = uricom_dec(arr[a])[0];
ret.push(li ? '<li>' + esc(txt) + '</li>' : txt);
}
return ret;
}
@@ -368,6 +403,23 @@ 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 has(haystack, needle) {
for (var a = 0; a < haystack.length; a++)
if (haystack[a] == needle)
@@ -494,7 +546,7 @@ var timer = (function () {
apop(r.q, fun);
};
function impl() {
function doevents() {
if (Date.now() - r.last < 69)
return;
@@ -504,7 +556,7 @@ var timer = (function () {
r.last = Date.now();
}
setInterval(impl, 100);
setInterval(doevents, 100);
return r;
})();
@@ -629,6 +681,7 @@ var toast = (function () {
obj.setAttribute('id', 'toast');
document.body.appendChild(obj);
r.visible = false;
r.txt = null;
r.hide = function (e) {
ev(e);
@@ -648,6 +701,7 @@ var toast = (function () {
obj.className += ' vis';
ebi('toastc').onclick = r.hide;
r.visible = true;
r.txt = txt;
};
r.ok = function (ms, txt) {
@@ -692,11 +746,15 @@ var modal = (function () {
a.onclick = ok;
(ebi('modali') || a).focus();
document.addEventListener('focus', onfocus);
timer.add(onfocus);
};
r.hide = function () {
o.parentNode.removeChild(o);
timer.rm(onfocus);
document.removeEventListener('focus', onfocus);
document.removeEventListener('keydown', onkey);
o.parentNode.removeChild(o);
r.busy = false;
setTimeout(next, 50);
};
@@ -715,6 +773,17 @@ var modal = (function () {
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');
@@ -775,3 +844,14 @@ var modal = (function () {
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)
});
}

View File

@@ -11,7 +11,7 @@
#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! */
@@ -19,7 +19,7 @@
/* add some margins because now it's weird */
.opview {margin-top: 2.5em}
#op_up2k {margin-top: 3em}
#op_up2k {margin-top: 6em}
/* and embiggen the upload button */
#u2conf #u2btn, #u2btn {padding:1.5em 0}

View File

@@ -157,7 +157,7 @@ brew install python@2
pip install virtualenv
# 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,
# suggestions from bugtracker, doesnt work (debugger is not attachable)
@@ -173,7 +173,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
# 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
##

View File

@@ -108,6 +108,7 @@ def tc1():
pdirs = [x.replace("\\", "/") for x in pdirs]
udirs = [x.split("/", 2)[2] 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):
if ud[-1] == "j":
continue
@@ -147,14 +148,14 @@ def tc1():
u = "{}{}/a.h264".format(ub, d)
r = requests.get(u)
ok = bool(r)
if ok != (p in ["a"]):
if ok != (p in ["rw"]):
raise Exception("get {} with perm {} at {}".format(ok, p, u))
# stat filesystem
for d, p in zip(pdirs, perms):
u = "{}/a.h264".format(d)
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))
# GET thumbnail, vreify contents
@@ -162,7 +163,7 @@ def tc1():
u = "{}{}/a.h264?th=j".format(ub, d)
r = requests.get(u)
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))
# check tags
@@ -179,10 +180,10 @@ def tc1():
r_ok = bool(j)
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))
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))
if tag is not None and tag != "48x32":