Compare commits

...

55 Commits

Author SHA1 Message Date
ed
a203e33347 v0.10.20 2021-05-16 07:51:39 +02:00
ed
3b8f697dd4 include links in bup summary 2021-05-16 07:51:22 +02:00
ed
78ba16f722 log filtering by url regex 2021-05-16 07:29:34 +02:00
ed
0fcfe79994 general-purpose file parsing 2021-05-16 07:04:18 +02:00
ed
c0e6df4b63 let it gooo 2021-05-16 05:27:04 +02:00
ed
322abdcb43 more dino support 2021-05-16 05:04:44 +02:00
ed
31100787ce ahh whatever 2021-05-16 03:21:49 +02:00
ed
c57d721be4 ie11 doesnt support sha512 2021-05-16 03:11:37 +02:00
ed
3b5a03e977 this too 2021-05-16 02:34:36 +02:00
ed
ed807ee43e native sha512 on old iphones 2021-05-16 02:25:00 +02:00
ed
073c130ae6 respect tooltip pref in up2k 2021-05-16 02:18:54 +02:00
ed
8810e0be13 add option to log headers 2021-05-16 02:11:09 +02:00
ed
f93016ab85 dont suggest bup if no write-access 2021-05-16 00:30:32 +02:00
ed
b19cf260c2 drop the control-panel link too 2021-05-14 20:07:48 +02:00
ed
db03e1e7eb readme 2021-05-14 16:38:07 +02:00
ed
e0d975e36a v0.10.19 2021-05-14 00:00:15 +02:00
ed
cfeb15259f not careful enough 2021-05-13 23:29:15 +02:00
ed
3b3f8fc8fb careful rice 2021-05-13 23:00:51 +02:00
ed
88bd2c084c misc 2021-05-13 22:58:36 +02:00
ed
bd367389b0 broke windows 2021-05-13 22:58:23 +02:00
ed
58ba71a76f option to hide incomplete uploads 2021-05-13 22:56:52 +02:00
ed
d03e34d55d v0.10.18 2021-05-13 17:42:06 +02:00
ed
24f239a46c ui tweaks 2021-05-13 17:41:14 +02:00
ed
2c0826f85a conditional sections in volume listing 2021-05-13 17:24:37 +02:00
ed
c061461d01 fix md perm reqs + dyn up2k modeset 2021-05-13 17:22:31 +02:00
ed
e7982a04fe explicit redirect to single non-roots 2021-05-13 16:54:31 +02:00
ed
33b91a7513 set password cookie expiration 2021-05-13 16:23:28 +02:00
ed
9bb1323e44 rclone faster + query params correctness 2021-05-13 16:02:30 +02:00
ed
e62bb807a5 better 2021-05-13 01:36:14 +02:00
ed
3fc0d2cc4a better 2021-05-13 00:43:25 +02:00
ed
0c786b0766 v0.10.17 2021-05-12 23:39:54 +02:00
ed
68c7528911 yes good 2021-05-12 23:26:30 +02:00
ed
26e18ae800 disallow uploading logues 2021-05-12 23:22:43 +02:00
ed
c30dc0b546 write-only QoL mostly 2021-05-12 23:06:13 +02:00
ed
f94aa46a11 open write-only folders from tree 2021-05-12 21:50:32 +02:00
ed
403261a293 support pyinstaller 2021-05-12 21:21:07 +02:00
ed
c7d9cbb11f show logues in write-only folders 2021-05-12 21:20:59 +02:00
ed
57e1c53cbb mention volume flags in the cfg-file example 2021-05-02 09:48:19 +02:00
ed
0754b553dd v0.10.16 2021-05-02 09:18:19 +02:00
ed
50661d941b cfg-parser: fix wildcard permissions 2021-05-02 09:16:14 +02:00
ed
c5db7c1a0c pickle needs this ;_; 2021-04-29 22:41:57 +02:00
ed
2cef5365f7 readme again 2021-04-27 09:26:14 +02:00
ed
fbc4e94007 readme (realized this was confusing) 2021-04-27 09:24:50 +02:00
ed
037ed5a2ad readme 2021-04-26 04:02:22 +02:00
ed
69dfa55705 readme 2021-04-26 04:01:47 +02:00
ed
a79a5c4e3e readme + ui tweaks 2021-04-25 22:44:50 +02:00
ed
7e80eabfe6 readme 2021-04-25 21:42:45 +02:00
ed
375b72770d readme 2021-04-25 04:34:06 +02:00
ed
e2dd683def does this look better 2021-04-25 03:04:24 +02:00
ed
9eba50c6e4 readme 2021-04-25 03:00:47 +02:00
ed
5a579dba52 sfx: help bzip2 make smaller archives 2021-04-24 22:07:09 +02:00
ed
e86c719575 sfx: cooperate better with other instances 2021-04-24 22:06:50 +02:00
ed
0e87f35547 ui tweaks 2021-04-24 22:06:21 +02:00
ed
b6d3d791a5 shave 2021-04-24 20:08:07 +02:00
ed
c9c3302664 a 2021-04-24 19:22:15 +02:00
31 changed files with 809 additions and 486 deletions

2
.vscode/launch.py vendored
View File

@@ -12,7 +12,7 @@ sys.path.insert(0, os.getcwd())
import jstyleson import jstyleson
from copyparty.__main__ import main as copyparty from copyparty.__main__ import main as copyparty
with open(".vscode/launch.json", "r") as f: with open(".vscode/launch.json", "r", encoding="utf-8") as f:
tj = f.read() tj = f.read()
oj = jstyleson.loads(tj) oj = jstyleson.loads(tj)

152
README.md
View File

@@ -9,9 +9,12 @@
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 IE6 or any other browser
* server runs on anything with `py2.7` or `py3.3+` * server runs on anything with `py2.7` or `py3.3+`
* *resumable* uploads need `firefox 12+` / `chrome 6+` / `safari 6+` / `IE 10+` * browse/upload with IE4 / netscape4.0 on win3.11 (heh)
* *resumable* uploads need `firefox 34+` / `chrome 37+` / `safari 7+`
* code standard: `black` * code standard: `black`
📷 screenshots: [browser](#the-browser) // [upload](#uploading) // [md-viewer](#markdown-viewer) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [ie4](#browser-support)
## readme toc ## readme toc
@@ -20,8 +23,16 @@ turn your phone or raspi into a portable file server with resumable uploads/down
* [notes](#notes) * [notes](#notes)
* [status](#status) * [status](#status)
* [bugs](#bugs) * [bugs](#bugs)
* [usage](#usage) * [not my bugs](#not-my-bugs)
* [the browser](#the-browser)
* [tabs](#tabs)
* [hotkeys](#hotkeys)
* [tree-mode](#tree-mode)
* [zip downloads](#zip-downloads) * [zip downloads](#zip-downloads)
* [uploading](#uploading)
* [file-search](#file-search)
* [markdown viewer](#markdown-viewer)
* [other tricks](#other-tricks)
* [searching](#searching) * [searching](#searching)
* [search configuration](#search-configuration) * [search configuration](#search-configuration)
* [metadata from audio files](#metadata-from-audio-files) * [metadata from audio files](#metadata-from-audio-files)
@@ -29,6 +40,7 @@ turn your phone or raspi into a portable file server with resumable uploads/down
* [complete examples](#complete-examples) * [complete examples](#complete-examples)
* [browser support](#browser-support) * [browser support](#browser-support)
* [client examples](#client-examples) * [client examples](#client-examples)
* [up2k](#up2k)
* [dependencies](#dependencies) * [dependencies](#dependencies)
* [optional gpl stuff](#optional-gpl-stuff) * [optional gpl stuff](#optional-gpl-stuff)
* [sfx](#sfx) * [sfx](#sfx)
@@ -53,9 +65,9 @@ you may also want these, especially on servers:
## notes ## notes
* iPhone/iPad: use Firefox to download files * iPhone/iPad: use Firefox to download files
* Android-Chrome: set max "parallel uploads" for 200% upload speed (android bug) * Android-Chrome: increase "parallel uploads" for higher speed (android bug)
* Android-Firefox: takes a while to select files (in order to avoid the above android-chrome issue) * Android-Firefox: takes a while to select files (their fix for ☝️)
* Desktop-Firefox: may use gigabytes of RAM if your connection is great and your files are massive * Desktop-Firefox: ~~may use gigabytes of RAM if your files are massive~~ *seems to be OK now*
* 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
@@ -107,16 +119,38 @@ summary: it works! you can use it! (but technically not even close to beta)
* this is an msys2 bug, the regular windows edition of python is fine * this is an msys2 bug, the regular windows edition of python is fine
# usage # the browser
![copyparty-browser-fs8](https://user-images.githubusercontent.com/241032/115978054-65106380-a57d-11eb-98f8-59e3dee73557.png)
## tabs
* `[🔎]` search by size, date, path/name, mp3-tags ... see [searching](#searching)
* `[🚀]` and `[🎈]` are the uploaders, see [uploading](#uploading)
* `[📂]` mkdir, create directories
* `[📝]` new-md, create a new markdown document
* `[📟]` send-msg, either to server-log or into textfiles if `--urlform save`
* `[⚙️]` client configuration options
## hotkeys
the browser has the following hotkeys the browser has the following hotkeys
* `0..9` jump to 10%..90%
* `U/O` skip 10sec back/forward
* `J/L` prev/next song
* `I/K` prev/next folder * `I/K` prev/next folder
* `P` parent folder * `P` parent folder
* when playing audio:
* `0..9` jump to 10%..90%
* `U/O` skip 10sec back/forward
* `J/L` prev/next song
* `J` also starts playing the folder
you can link a particular timestamp in an audio file by adding it to the URL, such as `&20` / `&20s` / `&1m20` / `&1:20` after the `.../#af-c8960dab`
## tree-mode
by default there's a breadcrumbs path; you can replace this with a tree-browser sidebar thing by clicking the 🌲
click `[-]` and `[+]` to adjust the size, and the `[a]` toggles if the tree should widen dynamically as you go deeper or stay fixed-size
## zip downloads ## zip downloads
@@ -135,12 +169,80 @@ the `zip` link next to folders can produce various types of zip/tar files using
* `zip_crc` will take longer to download since the server has to read each file twice * `zip_crc` will take longer to download since the server has to read each file twice
* please let me know if you find a program old enough to actually need this * please let me know if you find a program old enough to actually need this
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)
## uploading
two upload methods are available in the html client:
* 🎈 bup, the basic uploader, supports almost every browser since netscape 4.0
* 🚀 up2k, the fancy one
up2k has several advantages:
* you can drop folders into the browser (files are added recursively)
* files are processed in chunks, and each chunk is checksummed
* uploads resume if they are interrupted (for example by a reboot)
* server detects any corruption; the client reuploads affected chunks
* the client doesn't upload anything that already exists on the server
* the last-modified timestamp of the file is preserved
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)
**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)
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
* `[🔎]` switch between upload and file-search mode
and then theres the tabs below it,
* `[ok]` is uploads which completed successfully
* `[ng]` is the uploads which failed / got rejected (already exists, ...)
* `[done]` shows a combined list of `[ok]` and `[ng]`, chronological order
* `[busy]` files which are currently hashing, pending-upload, or uploading
* 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)
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
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
adding the same file multiple times is blocked, so if you first search for a file and then decide to upload it, you have to click the `[cleanup]` button to discard `[done]` files
note that since up2k has to read the file twice, 🎈 bup can be up to 2x faster if your internet connection is faster than the read-speed of your HDD
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
## markdown viewer
![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
## other tricks
* you can link a particular timestamp in an audio file by adding it to the URL, such as `&20` / `&20s` / `&1m20` / `&t=1:20` after the `.../#af-c8960dab`
# searching # searching
![copyparty-search-fs8](https://user-images.githubusercontent.com/241032/115978060-6772bd80-a57d-11eb-81d3-174e869b72c3.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: 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...
* drag/drop a local file to see if the same contents exist somewhere on the server (you get the URL if it does) * drag/drop a local file to see if the same contents exist somewhere on the server, see [file-search](#file-search)
path/name queries are space-separated, AND'ed together, and words are negated with a `-` prefix, so for example: path/name queries are space-separated, AND'ed together, and words are negated with a `-` prefix, so for example:
* 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
@@ -197,6 +299,10 @@ copyparty can invoke external programs to collect additional metadata for files
* `-mtp key=f,t5,~/bin/audio-key.py` uses `~/bin/audio-key.py` to get the `key` tag, replacing any existing metadata tag (`f,`), aborting if it takes longer than 5sec (`t5,`) * `-mtp key=f,t5,~/bin/audio-key.py` uses `~/bin/audio-key.py` to get the `key` tag, replacing any existing metadata tag (`f,`), aborting if it takes longer than 5sec (`t5,`)
* `-v ~/music::r:cmtp=.bpm=~/bin/audio-bpm.py:cmtp=key=f,t5,~/bin/audio-key.py` both as a per-volume config wow this is getting ugly * `-v ~/music::r:cmtp=.bpm=~/bin/audio-bpm.py:cmtp=key=f,t5,~/bin/audio-key.py` both as a per-volume config wow this is getting ugly
*but wait, there's more!* `-mtp` can be used for non-audio files as well using the `a` flag: `ay` only do audio files, `an` audio files are skipped, or `ad` always do it (d as in dontcare)
* `-mtp ext=an,~/bin/file-ext.py` runs `~/bin/file-ext.py` to get the `ext` tag only if file is not audio (`an`)
## complete examples ## complete examples
@@ -206,6 +312,8 @@ copyparty can invoke external programs to collect additional metadata for files
# browser support # browser support
![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 `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 |
@@ -236,7 +344,7 @@ 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)
* netscape 4.0 and 4.5 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 (22d13d8) hits a page fault, works with `?b=u`, file input not-impl, url params are multiplying * SerenityOS (22d13d8) hits a page fault, works with `?b=u`, file input not-impl, url params are multiplying
# client examples # client examples
@@ -263,6 +371,22 @@ copyparty returns a truncated sha512sum of your PUT/POST as base64; you can gene
b512 <movie.mkv b512 <movie.mkv
# up2k
quick outline of the up2k protocol, see [uploading](#uploading) for the web-client
* the up2k client splits a file into an "optimal" number of chunks
* 1 MiB each, unless that becomes more than 256 chunks
* tries 1.5M, 2M, 3, 4, 6, ... until <= 256# or chunksize >= 32M
* client posts the list of hashes, filename, size, last-modified
* server creates the `wark`, an identifier for this upload
* `sha512( salt + filesize + chunk_hashes )`
* and a sparse file is created for the chunks to drop into
* client uploads each chunk
* header entries for the chunk-hash and wark
* server writes chunks into place based on the hash
* client does another handshake with the hashlist; server replies with OK or a list of chunks to reupload
# dependencies # dependencies
* `jinja2` (is built into the SFX) * `jinja2` (is built into the SFX)
@@ -279,12 +403,12 @@ copyparty returns a truncated sha512sum of your PUT/POST as base64; you can gene
some bundled tools have copyleft dependencies, see [./bin/#mtag](bin/#mtag) some bundled tools have copyleft dependencies, see [./bin/#mtag](bin/#mtag)
these are standalone and will never be imported / evaluated by copyparty these are standalone programs and will never be imported / evaluated by copyparty
# sfx # sfx
currently there are two self-contained binaries: currently there are two self-contained "binaries":
* [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) -- pure python, works everywhere * [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) -- pure python, works everywhere
* [copyparty-sfx.sh](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.sh) -- smaller, but only for linux and macos * [copyparty-sfx.sh](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.sh) -- smaller, but only for linux and macos

9
bin/mtag/file-ext.py Normal file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env python
import sys
"""
example that just prints the file extension
"""
print(sys.argv[1].split(".")[-1])

View File

@@ -24,6 +24,9 @@ MACOS = platform.system() == "Darwin"
class EnvParams(object): class EnvParams(object):
def __init__(self): def __init__(self):
self.mod = os.path.dirname(os.path.realpath(__file__)) self.mod = os.path.dirname(os.path.realpath(__file__))
if self.mod.endswith("__init__"):
self.mod = os.path.dirname(self.mod)
if sys.platform == "win32": if sys.platform == "win32":
self.cfg = os.path.normpath(os.environ["APPDATA"] + "/copyparty") self.cfg = os.path.normpath(os.environ["APPDATA"] + "/copyparty")
elif sys.platform == "darwin": elif sys.platform == "darwin":

View File

@@ -237,16 +237,14 @@ def run_argparse(argv, formatter):
ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account") ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account")
ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume") ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume")
ap.add_argument("-q", action="store_true", help="quiet") ap.add_argument("-q", action="store_true", help="quiet")
ap.add_argument("--log-conn", action="store_true", help="print tcp-server msgs")
ap.add_argument("-ed", action="store_true", help="enable ?dots") ap.add_argument("-ed", action="store_true", help="enable ?dots")
ap.add_argument("-emp", action="store_true", help="enable markdown plugins") ap.add_argument("-emp", action="store_true", help="enable markdown plugins")
ap.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate") ap.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate")
ap.add_argument("-nw", action="store_true", help="disable writes (benchmark)") ap.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
ap.add_argument("-nih", action="store_true", help="no info hostname") ap.add_argument("-nih", action="store_true", help="no info hostname")
ap.add_argument("-nid", action="store_true", help="no info disk-usage") ap.add_argument("-nid", action="store_true", help="no info disk-usage")
ap.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads")
ap.add_argument("--no-zip", action="store_true", help="disable download as zip/tar") ap.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
ap.add_argument("--no-sendfile", action="store_true", help="disable sendfile (for debugging)")
ap.add_argument("--no-scandir", action="store_true", help="disable scandir (for debugging)")
ap.add_argument("--sparse", metavar="MiB", type=int, default=4, help="up2k min.size threshold (mswin-only)") ap.add_argument("--sparse", metavar="MiB", type=int, default=4, help="up2k min.size threshold (mswin-only)")
ap.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms") ap.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms")
ap.add_argument("--salt", type=str, default="hunter2", help="up2k file-hash salt") ap.add_argument("--salt", type=str, default="hunter2", help="up2k file-hash salt")
@@ -274,6 +272,13 @@ def run_argparse(argv, formatter):
ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info") ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
ap2.add_argument("--ssl-log", metavar="PATH", help="log master secrets") ap2.add_argument("--ssl-log", metavar="PATH", help="log master secrets")
ap2 = ap.add_argument_group('debug options')
ap2.add_argument("--log-conn", action="store_true", help="print tcp-server msgs")
ap2.add_argument("--no-sendfile", action="store_true", help="disable sendfile")
ap2.add_argument("--no-scandir", action="store_true", help="disable scandir")
ap2.add_argument("--ihead", metavar="HEADER", action='append', help="dump incoming header")
ap2.add_argument("--lf-url", metavar="RE", type=str, default=r"^/\.cpr/", help="dont log URLs matching")
return ap.parse_args(args=argv[1:]) return ap.parse_args(args=argv[1:])
# fmt: on # fmt: on

View File

@@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (0, 10, 15) VERSION = (0, 10, 20)
CODENAME = "zip it" CODENAME = "zip it"
BUILD_DT = (2021, 4, 24) BUILD_DT = (2021, 5, 16)
S_VERSION = ".".join(map(str, VERSION)) S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -141,7 +141,12 @@ class VFS(object):
real.sort() real.sort()
if not rem: if not rem:
for name, vn2 in sorted(self.nodes.items()): for name, vn2 in sorted(self.nodes.items()):
if uname in vn2.uread or "*" in vn2.uread: if (
uname in vn2.uread
or "*" in vn2.uread
or uname in vn2.uwrite
or "*" in vn2.uwrite
):
virt_vis[name] = vn2 virt_vis[name] = vn2
# no vfs nodes in the list of real inodes # no vfs nodes in the list of real inodes
@@ -241,6 +246,7 @@ class AuthSrv(object):
self.args = args self.args = args
self.log_func = log_func self.log_func = log_func
self.warn_anonwrite = warn_anonwrite self.warn_anonwrite = warn_anonwrite
self.line_ctr = 0
if WINDOWS: if WINDOWS:
self.re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$") self.re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
@@ -266,7 +272,9 @@ class AuthSrv(object):
def _parse_config_file(self, fd, user, mread, mwrite, mflags, mount): def _parse_config_file(self, fd, user, mread, mwrite, mflags, mount):
vol_src = None vol_src = None
vol_dst = None vol_dst = None
self.line_ctr = 0
for ln in [x.decode("utf-8").strip() for x in fd]: for ln in [x.decode("utf-8").strip() for x in fd]:
self.line_ctr += 1
if not ln and vol_src is not None: if not ln and vol_src is not None:
vol_src = None vol_src = None
vol_dst = None vol_dst = None
@@ -296,7 +304,12 @@ class AuthSrv(object):
mflags[vol_dst] = {} mflags[vol_dst] = {}
continue continue
lvl, uname = ln.split(" ") if len(ln) > 1:
lvl, uname = ln.split(" ")
else:
lvl = ln
uname = "*"
self._read_vol_str( self._read_vol_str(
lvl, uname, mread[vol_dst], mwrite[vol_dst], mflags[vol_dst] lvl, uname, mread[vol_dst], mwrite[vol_dst], mflags[vol_dst]
) )
@@ -374,7 +387,12 @@ class AuthSrv(object):
if self.args.c: if self.args.c:
for cfg_fn in self.args.c: for cfg_fn in self.args.c:
with open(cfg_fn, "rb") as f: with open(cfg_fn, "rb") as f:
self._parse_config_file(f, user, mread, mwrite, mflags, mount) try:
self._parse_config_file(f, user, mread, mwrite, mflags, mount)
except:
m = "\n\033[1;31m\nerror in config file {} on line {}:\n\033[0m"
print(m.format(cfg_fn, self.line_ctr))
raise
if not mount: if not mount:
# -h says our defaults are CWD at root and read/write for everyone # -h says our defaults are CWD at root and read/write for everyone

View File

@@ -100,6 +100,16 @@ class HttpCli(object):
self.ip = v.split(",")[0] self.ip = v.split(",")[0]
self.log_src = self.conn.set_rproxy(self.ip) self.log_src = self.conn.set_rproxy(self.ip)
if self.args.ihead:
keys = self.args.ihead
if "*" in keys:
keys = list(sorted(self.headers.keys()))
for k in keys:
v = self.headers.get(k)
if v is not None:
self.log("[H] {}: \033[33m[{}]".format(k, v), 6)
# split req into vpath + uparam # split req into vpath + uparam
uparam = {} uparam = {}
if "?" not in self.req: if "?" not in self.req:
@@ -120,29 +130,35 @@ class HttpCli(object):
else: else:
uparam[k.lower()] = False uparam[k.lower()] = False
self.ouparam = {k: v for k, v in uparam.items()}
cookies = self.headers.get("cookie") or {}
if cookies:
cookies = [x.split("=", 1) for x in cookies.split(";") if "=" in x]
cookies = {k.strip(): unescape_cookie(v) for k, v in cookies}
for kc, ku in [["cppwd", "pw"], ["b", "b"]]:
if kc in cookies and ku not in uparam:
uparam[ku] = cookies[kc]
self.uparam = uparam self.uparam = uparam
self.cookies = cookies
self.vpath = unquotep(vpath) self.vpath = unquotep(vpath)
pwd = None pwd = uparam.get("pw")
if "cookie" in self.headers:
cookies = self.headers["cookie"].split(";")
for k, v in [x.split("=", 1) for x in cookies]:
if k.strip() != "cppwd":
continue
pwd = unescape_cookie(v)
break
pwd = uparam.get("pw", pwd)
self.uname = self.auth.iuser.get(pwd, "*") self.uname = self.auth.iuser.get(pwd, "*")
if self.uname: if self.uname:
self.rvol = self.auth.vfs.user_tree(self.uname, readable=True) self.rvol = self.auth.vfs.user_tree(self.uname, readable=True)
self.wvol = self.auth.vfs.user_tree(self.uname, writable=True) self.wvol = self.auth.vfs.user_tree(self.uname, writable=True)
ua = self.headers.get("user-agent", "") ua = self.headers.get("user-agent", "")
if ua.startswith("rclone/"): self.is_rclone = ua.startswith("rclone/")
if self.is_rclone:
uparam["raw"] = False uparam["raw"] = False
uparam["dots"] = False uparam["dots"] = False
uparam["b"] = False
cookies["b"] = False
self.do_log = not self.conn.lf_url or not self.conn.lf_url.match(self.req)
try: try:
if self.mode in ["GET", "HEAD"]: if self.mode in ["GET", "HEAD"]:
@@ -218,7 +234,14 @@ class HttpCli(object):
removing anything in rm, adding pairs in add removing anything in rm, adding pairs in add
""" """
kv = {k: v for k, v in self.uparam.items() if k not in rm} if self.is_rclone:
return ""
kv = {
k: v
for k, v in self.uparam.items()
if k not in rm and self.cookies.get(k) != v
}
kv.update(add) kv.update(add)
if not kv: if not kv:
return "" return ""
@@ -226,18 +249,37 @@ class HttpCli(object):
r = ["{}={}".format(k, quotep(v)) if v else k for k, v in kv.items()] r = ["{}={}".format(k, quotep(v)) if v else k for k, v in kv.items()]
return "?" + "&amp;".join(r) return "?" + "&amp;".join(r)
def redirect(
self, vpath, suf="", msg="aight", flavor="go to", click=True, use302=False
):
html = self.j2(
"msg",
h2='<a href="/{}">{} /{}</a>'.format(
quotep(vpath) + suf, flavor, html_escape(vpath, crlf=True) + suf
),
pre=msg,
click=click,
).encode("utf-8", "replace")
if use302:
h = {"Location": "/" + vpath, "Cache-Control": "no-cache"}
self.reply(html, status=302, headers=h)
else:
self.reply(html)
def handle_get(self): def handle_get(self):
logmsg = "{:4} {}".format(self.mode, self.req) if self.do_log:
logmsg = "{:4} {}".format(self.mode, self.req)
if "range" in self.headers: if "range" in self.headers:
try: try:
rval = self.headers["range"].split("=", 1)[1] rval = self.headers["range"].split("=", 1)[1]
except: except:
rval = self.headers["range"] rval = self.headers["range"]
logmsg += " [\033[36m" + rval + "\033[0m]" logmsg += " [\033[36m" + rval + "\033[0m]"
self.log(logmsg) self.log(logmsg)
# "embedded" resources # "embedded" resources
if self.vpath.startswith(".cpr"): if self.vpath.startswith(".cpr"):
@@ -248,16 +290,18 @@ class HttpCli(object):
return self.tx_tree() return self.tx_tree()
# conditional redirect to single volumes # conditional redirect to single volumes
if self.vpath == "" and not self.uparam: if self.vpath == "" and not self.ouparam:
nread = len(self.rvol) nread = len(self.rvol)
nwrite = len(self.wvol) nwrite = len(self.wvol)
if nread + nwrite == 1 or (self.rvol == self.wvol and nread == 1): if nread + nwrite == 1 or (self.rvol == self.wvol and nread == 1):
if nread == 1: if nread == 1:
self.vpath = self.rvol[0] vpath = self.rvol[0]
else: else:
self.vpath = self.wvol[0] vpath = self.wvol[0]
self.absolute_urls = True if self.vpath != vpath:
self.redirect(vpath, flavor="redirecting to", use302=True)
return True
self.readable, self.writable = self.conn.auth.vfs.can_access( self.readable, self.writable = self.conn.auth.vfs.can_access(
self.vpath, self.uname self.vpath, self.uname
@@ -276,7 +320,9 @@ class HttpCli(object):
return self.tx_browser() return self.tx_browser()
def handle_options(self): def handle_options(self):
self.log("OPTIONS " + self.req) if self.do_log:
self.log("OPTIONS " + self.req)
self.send_headers( self.send_headers(
None, None,
204, 204,
@@ -645,13 +691,16 @@ class HttpCli(object):
if pwd in self.auth.iuser: if pwd in self.auth.iuser:
msg = "login ok" msg = "login ok"
dt = datetime.utcfromtimestamp(time.time() + 60 * 60 * 24 * 365)
exp = dt.strftime("%a, %d %b %Y %H:%M:%S GMT")
else: else:
msg = "naw dude" msg = "naw dude"
pwd = "x" # nosec pwd = "x" # nosec
exp = "Fri, 15 Aug 1997 01:00:00 GMT"
h = {"Set-Cookie": "cppwd={}; Path=/; SameSite=Lax".format(pwd)} ck = "cppwd={}; Path=/; Expires={}; SameSite=Lax".format(pwd, exp)
html = self.j2("msg", h1=msg, h2='<a href="/">ack</a>', redir="/") html = self.j2("msg", h1=msg, h2='<a href="/">ack</a>', redir="/")
self.reply(html.encode("utf-8"), headers=h) self.reply(html.encode("utf-8"), headers={"Set-Cookie": ck})
return True return True
def handle_mkdir(self): def handle_mkdir(self):
@@ -680,14 +729,7 @@ class HttpCli(object):
raise Pebkac(500, "mkdir failed, check the logs") raise Pebkac(500, "mkdir failed, check the logs")
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/") vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
esc_paths = [quotep(vpath), html_escape(vpath, crlf=True)] self.redirect(vpath)
html = self.j2(
"msg",
h2='<a href="/{}">go to /{}</a>'.format(*esc_paths),
pre="aight",
click=True,
)
self.reply(html.encode("utf-8", "replace"))
return True return True
def handle_new_md(self): def handle_new_md(self):
@@ -714,15 +756,7 @@ class HttpCli(object):
f.write(b"`GRUNNUR`\n") f.write(b"`GRUNNUR`\n")
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/") vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
html = self.j2( self.redirect(vpath, "?edit")
"msg",
h2='<a href="/{}?edit">go to /{}?edit</a>'.format(
quotep(vpath), html_escape(vpath)
),
pre="aight",
click=True,
)
self.reply(html.encode("utf-8", "replace"))
return True return True
def handle_plain_upload(self): def handle_plain_upload(self):
@@ -741,7 +775,9 @@ class HttpCli(object):
if p_file and not nullwrite: if p_file and not nullwrite:
fdir = os.path.join(vfs.realpath, rem) fdir = os.path.join(vfs.realpath, rem)
fname = sanitize_fn(p_file) fname = sanitize_fn(
p_file, bad=[".prologue.html", ".epilogue.html"]
)
if not os.path.isdir(fsenc(fdir)): if not os.path.isdir(fsenc(fdir)):
raise Pebkac(404, "that folder does not exist") raise Pebkac(404, "that folder does not exist")
@@ -761,7 +797,7 @@ class HttpCli(object):
if sz == 0: if sz == 0:
raise Pebkac(400, "empty files in post") raise Pebkac(400, "empty files in post")
files.append([sz, sha512_hex]) files.append([sz, sha512_hex, p_file, fname])
self.conn.hsrv.broker.put( self.conn.hsrv.broker.put(
False, "up2k.hash_file", vfs.realpath, vfs.flags, rem, fname False, "up2k.hash_file", vfs.realpath, vfs.flags, rem, fname
) )
@@ -770,12 +806,16 @@ class HttpCli(object):
except Pebkac: except Pebkac:
if fname != os.devnull: if fname != os.devnull:
fp = os.path.join(fdir, fname) fp = os.path.join(fdir, fname)
fp2 = fp
if self.args.dotpart:
fp2 = os.path.join(fdir, "." + fname)
suffix = ".PARTIAL" suffix = ".PARTIAL"
try: try:
os.rename(fsenc(fp), fsenc(fp + suffix)) os.rename(fsenc(fp), fsenc(fp2 + suffix))
except: except:
fp = fp[: -len(suffix)] fp2 = fp2[: -len(suffix) - 1]
os.rename(fsenc(fp), fsenc(fp + suffix)) os.rename(fsenc(fp), fsenc(fp2 + suffix))
raise raise
@@ -792,10 +832,13 @@ class HttpCli(object):
errmsg = "ERROR: " + errmsg errmsg = "ERROR: " + errmsg
status = "ERROR" status = "ERROR"
msg = "{0} // {1} bytes // {2:.3f} MiB/s\n".format(status, sz_total, spd) msg = "{} // {} bytes // {:.3f} MiB/s\n".format(status, sz_total, spd)
for sz, sha512 in files: for sz, sha512, ofn, lfn in files:
msg += "sha512: {0} // {1} bytes\n".format(sha512[:56], sz) vpath = self.vpath + "/" + lfn
msg += 'sha512: {} // {} bytes // <a href="/{}">{}</a>\n'.format(
sha512[:56], sz, quotep(vpath), html_escape(ofn, crlf=True)
)
# truncated SHA-512 prevents length extension attacks; # truncated SHA-512 prevents length extension attacks;
# using SHA-512/224, optionally SHA-512/256 = :64 # using SHA-512/224, optionally SHA-512/256 = :64
@@ -803,32 +846,13 @@ class HttpCli(object):
self.log("{} {}".format(vspd, msg)) self.log("{} {}".format(vspd, msg))
if not nullwrite: if not nullwrite:
# TODO this is bad
log_fn = "up.{:.6f}.txt".format(t0) log_fn = "up.{:.6f}.txt".format(t0)
with open(log_fn, "wb") as f: with open(log_fn, "wb") as f:
f.write( ft = "{}:{}".format(self.ip, self.addr[1])
( ft = "{}\n{}\n{}\n".format(ft, msg.rstrip(), errmsg)
"\n".join( f.write(ft.encode("utf-8"))
unicode(x)
for x in [
":".join(unicode(x) for x in [self.ip, self.addr[1]]),
msg.rstrip(),
]
)
+ "\n"
+ errmsg
+ "\n"
).encode("utf-8")
)
html = self.j2( self.redirect(self.vpath, msg=msg, flavor="return to", click=False)
"msg",
h2='<a href="/{}">return to /{}</a>'.format(
quotep(self.vpath), html_escape(self.vpath)
),
pre=msg,
)
self.reply(html.encode("utf-8", "replace"))
self.parser.drop() self.parser.drop()
return True return True
@@ -928,13 +952,14 @@ class HttpCli(object):
return True return True
def _chk_lastmod(self, file_ts): def _chk_lastmod(self, file_ts):
date_fmt = "%a, %d %b %Y %H:%M:%S GMT"
file_dt = datetime.utcfromtimestamp(file_ts) file_dt = datetime.utcfromtimestamp(file_ts)
file_lastmod = file_dt.strftime("%a, %d %b %Y %H:%M:%S GMT") file_lastmod = file_dt.strftime(date_fmt)
cli_lastmod = self.headers.get("if-modified-since") cli_lastmod = self.headers.get("if-modified-since")
if cli_lastmod: if cli_lastmod:
try: try:
cli_dt = time.strptime(cli_lastmod, "%a, %d %b %Y %H:%M:%S GMT") cli_dt = time.strptime(cli_lastmod, date_fmt)
cli_ts = calendar.timegm(cli_dt) cli_ts = calendar.timegm(cli_dt)
return file_lastmod, int(file_ts) > int(cli_ts) return file_lastmod, int(file_ts) > int(cli_ts)
except Exception as ex: except Exception as ex:
@@ -1093,7 +1118,9 @@ class HttpCli(object):
logmsg += unicode(status) + logtail logmsg += unicode(status) + logtail
if self.mode == "HEAD" or not do_send: if self.mode == "HEAD" or not do_send:
self.log(logmsg) if self.do_log:
self.log(logmsg)
return True return True
ret = True ret = True
@@ -1107,7 +1134,9 @@ class HttpCli(object):
logmsg += " \033[31m" + unicode(upper - remains) + "\033[0m" logmsg += " \033[31m" + unicode(upper - remains) + "\033[0m"
spd = self._spd((upper - lower) - remains) spd = self._spd((upper - lower) - remains)
self.log("{}, {}".format(logmsg, spd)) if self.do_log:
self.log("{}, {}".format(logmsg, spd))
return ret return ret
def tx_zip(self, fmt, uarg, vn, rem, items, dots): def tx_zip(self, fmt, uarg, vn, rem, items, dots):
@@ -1216,7 +1245,9 @@ class HttpCli(object):
logmsg += unicode(status) logmsg += unicode(status)
if self.mode == "HEAD" or not do_send: if self.mode == "HEAD" or not do_send:
self.log(logmsg) if self.do_log:
self.log(logmsg)
return True return True
try: try:
@@ -1230,7 +1261,9 @@ class HttpCli(object):
self.log(logmsg + " \033[31md/c\033[0m") self.log(logmsg + " \033[31md/c\033[0m")
return False return False
self.log(logmsg + " " + unicode(len(html))) if self.do_log:
self.log(logmsg + " " + unicode(len(html)))
return True return True
def tx_mounts(self): def tx_mounts(self):
@@ -1360,6 +1393,21 @@ class HttpCli(object):
if "b" in self.uparam: if "b" in self.uparam:
tpl = "browser2" tpl = "browser2"
logues = ["", ""]
for n, fn in enumerate([".prologue.html", ".epilogue.html"]):
fn = os.path.join(abspath, fn)
if os.path.exists(fsenc(fn)):
with open(fsenc(fn), "rb") as f:
logues[n] = f.read().decode("utf-8")
ls_ret = {
"dirs": [],
"files": [],
"taglist": [],
"srvinf": srv_info,
"perms": perms,
"logues": logues,
}
j2a = { j2a = {
"vdir": quotep(self.vpath), "vdir": quotep(self.vpath),
"vpnodes": vpnodes, "vpnodes": vpnodes,
@@ -1373,13 +1421,15 @@ class HttpCli(object):
"have_zip": (not self.args.no_zip), "have_zip": (not self.args.no_zip),
"have_b_u": (self.writable and self.uparam.get("b") == "u"), "have_b_u": (self.writable and self.uparam.get("b") == "u"),
"url_suf": url_suf, "url_suf": url_suf,
"logues": ["", ""], "logues": logues,
"title": html_escape(self.vpath, crlf=True), "title": html_escape(self.vpath, crlf=True),
"srv_info": srv_info, "srv_info": srv_info,
} }
if not self.readable: if not self.readable:
if is_ls: if is_ls:
raise Pebkac(403) ret = json.dumps(ls_ret)
self.reply(ret.encode("utf-8", "replace"), mime="application/json")
return True
if not os.path.isdir(fsenc(abspath)): if not os.path.isdir(fsenc(abspath)):
raise Pebkac(404) raise Pebkac(404)
@@ -1526,24 +1576,12 @@ class HttpCli(object):
for f in dirs: for f in dirs:
f["tags"] = {} f["tags"] = {}
logues = ["", ""]
for n, fn in enumerate([".prologue.html", ".epilogue.html"]):
fn = os.path.join(abspath, fn)
if os.path.exists(fsenc(fn)):
with open(fsenc(fn), "rb") as f:
logues[n] = f.read().decode("utf-8")
if is_ls: if is_ls:
[x.pop(k) for k in ["name", "dt"] for y in [dirs, files] for x in y] [x.pop(k) for k in ["name", "dt"] for y in [dirs, files] for x in y]
ret = { ls_ret["dirs"] = dirs
"dirs": dirs, ls_ret["files"] = files
"files": files, ls_ret["taglist"] = taglist
"srvinf": srv_info, ret = json.dumps(ls_ret)
"perms": perms,
"logues": logues,
"taglist": taglist,
}
ret = json.dumps(ret)
self.reply(ret.encode("utf-8", "replace"), mime="application/json") self.reply(ret.encode("utf-8", "replace"), mime="application/json")
return True return True

View File

@@ -1,6 +1,7 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import re
import os import os
import sys import sys
import time import time
@@ -38,6 +39,7 @@ class HttpConn(object):
self.workload = 0 self.workload = 0
self.u2idx = None self.u2idx = None
self.log_func = hsrv.log self.log_func = hsrv.log
self.lf_url = re.compile(self.args.lf_url) if self.args.lf_url else None
self.set_rproxy() self.set_rproxy()
def set_rproxy(self, ip=None): def set_rproxy(self, ip=None):

View File

@@ -87,7 +87,7 @@ def gen_hdr(h_pos, fn, sz, lastmod, utf8, crc32, pre_crc):
ret += struct.pack("<LL", vsz, vsz) ret += struct.pack("<LL", vsz, vsz)
# windows support (the "?" replace below too) # windows support (the "?" replace below too)
fn = sanitize_fn(fn, "/") fn = sanitize_fn(fn, ok="/")
bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_") bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_")
z64_len = len(z64v) * 8 + 4 if z64v else 0 z64_len = len(z64v) * 8 + 4 if z64v else 0

View File

@@ -511,6 +511,7 @@ class Up2k(object):
def _run_all_mtp(self): def _run_all_mtp(self):
t0 = time.time() t0 = time.time()
self.mtp_audio = {}
self.mtp_force = {} self.mtp_force = {}
self.mtp_parsers = {} self.mtp_parsers = {}
for ptop, flags in self.flags.items(): for ptop, flags in self.flags.items():
@@ -527,8 +528,9 @@ class Up2k(object):
entags = self.entags[ptop] entags = self.entags[ptop]
force = {} audio = {} # [r]equire [n]ot [d]ontcare
timeout = {} force = {} # bool
timeout = {} # int
parsers = {} parsers = {}
for parser in self.flags[ptop]["mtp"]: for parser in self.flags[ptop]["mtp"]:
orig = parser orig = parser
@@ -536,6 +538,8 @@ class Up2k(object):
if tag not in entags: if tag not in entags:
continue continue
audio[tag] = "y"
while True: while True:
try: try:
bp = os.path.expanduser(parser) bp = os.path.expanduser(parser)
@@ -549,6 +553,10 @@ class Up2k(object):
arg, parser = parser.split(",", 1) arg, parser = parser.split(",", 1)
arg = arg.lower() arg = arg.lower()
if arg.startswith("a"):
audio[tag] = arg[1:]
continue
if arg == "f": if arg == "f":
force[tag] = True force[tag] = True
continue continue
@@ -563,6 +571,8 @@ class Up2k(object):
self.log("invalid argument: " + orig, 1) self.log("invalid argument: " + orig, 1)
return return
# todo audio/force => parser attributes
self.mtp_audio[ptop] = audio
self.mtp_force[ptop] = force self.mtp_force[ptop] = force
self.mtp_parsers[ptop] = parsers self.mtp_parsers[ptop] = parsers
@@ -596,8 +606,8 @@ class Up2k(object):
have = cur.execute(q, (w,)).fetchall() have = cur.execute(q, (w,)).fetchall()
have = [x[0] for x in have] have = [x[0] for x in have]
if ".dur" not in have and ".dur" in entags: parsers = self._get_parsers(ptop, have)
# skip non-audio if not parsers:
to_delete[w] = True to_delete[w] = True
n_left -= 1 n_left -= 1
continue continue
@@ -605,10 +615,7 @@ class Up2k(object):
if w in in_progress: if w in in_progress:
continue continue
task_parsers = { jobs.append([parsers, None, w, abspath])
k: v for k, v in parsers.items() if k in force or k not in have
}
jobs.append([task_parsers, None, w, abspath])
in_progress[w] = True in_progress[w] = True
done = self._flush_mpool(wcur) done = self._flush_mpool(wcur)
@@ -667,6 +674,26 @@ class Up2k(object):
wcur.close() wcur.close()
cur.close() cur.close()
def _get_parsers(self, ptop, have):
audio = self.mtp_audio[ptop]
force = self.mtp_force[ptop]
entags = self.entags[ptop]
parsers = {}
for k, v in self.mtp_parsers[ptop].items():
if ".dur" in entags:
if ".dur" in have:
# is audio, require non-audio?
if audio[k] == "n":
continue
# is not audio, require audio?
elif audio[k] == "y":
continue
parsers[k] = v
parsers = {k: v for k, v in parsers.items() if k in force or k not in have}
return parsers
def _start_mpool(self): def _start_mpool(self):
# mp.pool.ThreadPool and concurrent.futures.ThreadPoolExecutor # mp.pool.ThreadPool and concurrent.futures.ThreadPoolExecutor
# both do crazy runahead so lets reinvent another wheel # both do crazy runahead so lets reinvent another wheel
@@ -891,7 +918,7 @@ class Up2k(object):
if cj["ptop"] not in self.registry: if cj["ptop"] not in self.registry:
raise Pebkac(410, "location unavailable") raise Pebkac(410, "location unavailable")
cj["name"] = sanitize_fn(cj["name"]) cj["name"] = sanitize_fn(cj["name"], bad=[".prologue.html", ".epilogue.html"])
cj["poke"] = time.time() cj["poke"] = time.time()
wark = self._get_wark(cj) wark = self._get_wark(cj)
now = time.time() now = time.time()
@@ -1198,6 +1225,9 @@ class Up2k(object):
# raise Exception("aaa") # raise Exception("aaa")
tnam = job["name"] + ".PARTIAL" tnam = job["name"] + ".PARTIAL"
if self.args.dotpart:
tnam = "." + tnam
suffix = ".{:.6f}-{}".format(job["t0"], job["addr"]) suffix = ".{:.6f}-{}".format(job["t0"], job["addr"])
with ren_open(tnam, "wb", fdir=pdir, suffix=suffix) as f: with ren_open(tnam, "wb", fdir=pdir, suffix=suffix) as f:
f, job["tnam"] = f["orz"] f, job["tnam"] = f["orz"]
@@ -1305,13 +1335,9 @@ class Up2k(object):
abspath = os.path.join(ptop, rd, fn) abspath = os.path.join(ptop, rd, fn)
tags = self.mtag.get(abspath) tags = self.mtag.get(abspath)
ntags1 = len(tags) ntags1 = len(tags)
if self.mtp_parsers.get(ptop, {}): parsers = self._get_parsers(ptop, tags)
parser = { if parsers:
k: v tags.update(self.mtag.get_bin(parsers, abspath))
for k, v in self.mtp_parsers[ptop].items()
if k in self.mtp_force[ptop] or k not in tags
}
tags.update(self.mtag.get_bin(parser, abspath))
with self.mutex: with self.mutex:
cur = self.cur[ptop] cur = self.cur[ptop]

View File

@@ -49,6 +49,7 @@ HTTPCODE = {
200: "OK", 200: "OK",
204: "No Content", 204: "No Content",
206: "Partial Content", 206: "Partial Content",
302: "Found",
304: "Not Modified", 304: "Not Modified",
400: "Bad Request", 400: "Bad Request",
403: "Forbidden", 403: "Forbidden",
@@ -576,7 +577,7 @@ def undot(path):
return "/".join(ret) return "/".join(ret)
def sanitize_fn(fn, ok=""): def sanitize_fn(fn, ok="", bad=[]):
if "/" not in ok: if "/" not in ok:
fn = fn.replace("\\", "/").split("/")[-1] fn = fn.replace("\\", "/").split("/")[-1]
@@ -592,15 +593,15 @@ def sanitize_fn(fn, ok=""):
["?", ""], ["?", ""],
["*", ""], ["*", ""],
] ]
for bad, good in [x for x in remap if x[0] not in ok]: for a, b in [x for x in remap if x[0] not in ok]:
fn = fn.replace(bad, good) fn = fn.replace(a, b)
bad = ["con", "prn", "aux", "nul"] bad.extend(["con", "prn", "aux", "nul"])
for n in range(1, 10): for n in range(1, 10):
bad += "com{0} lpt{0}".format(n).split(" ") bad += "com{0} lpt{0}".format(n).split(" ")
if fn.lower() in bad: if fn.lower() in bad:
fn = "_" + fn fn = "_" + fn
return fn.strip() return fn.strip()

View File

@@ -6,7 +6,7 @@ html,body,tr,th,td,#files,a {
background: none; background: none;
font-weight: inherit; font-weight: inherit;
font-size: inherit; font-size: inherit;
padding: none; padding: 0;
border: none; border: none;
} }
html { html {
@@ -90,15 +90,13 @@ a, #files tbody div a:last-child {
#files td { #files td {
margin: 0; margin: 0;
padding: 0 .5em; padding: 0 .5em;
}
#files td {
border-bottom: 1px solid #111; border-bottom: 1px solid #111;
} }
#files td+td+td { #files td+td+td {
max-width: 30em; max-width: 30em;
overflow: hidden; overflow: hidden;
} }
#files tr td { #files tr+tr td {
border-top: 1px solid #383838; border-top: 1px solid #383838;
} }
#files tbody td:nth-child(3) { #files tbody td:nth-child(3) {
@@ -187,6 +185,16 @@ a, #files tbody div a:last-child {
background: #925; background: #925;
border-color: #c37; border-color: #c37;
} }
#files tr.sel a {
color: #fff;
}
#files tr.sel a.play {
color: #fc5;
}
#files tr.sel a.play.act {
color: #fff;
text-shadow: 0 0 1px #fff;
}
#blocked { #blocked {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -343,10 +351,10 @@ a, #files tbody div a:last-child {
width: calc(100% - 10.5em); width: calc(100% - 10.5em);
background: rgba(0,0,0,0.2); background: rgba(0,0,0,0.2);
} }
@media (min-width: 90em) { @media (min-width: 80em) {
#barpos, #barpos,
#barbuf { #barbuf {
width: calc(100% - 24em); width: calc(100% - 21em);
left: 9.8em; left: 9.8em;
top: .7em; top: .7em;
height: 1.6em; height: 1.6em;
@@ -356,6 +364,9 @@ a, #files tbody div a:last-child {
bottom: -3.2em; bottom: -3.2em;
height: 3.2em; height: 3.2em;
} }
#pvol {
max-width: 9em;
}
} }
@@ -407,6 +418,7 @@ a, #files tbody div a:last-child {
padding: .3em .6em; padding: .3em .6em;
border-radius: .3em; border-radius: .3em;
border-width: .15em 0; border-width: .15em 0;
white-space: nowrap;
} }
.opbox { .opbox {
background: #2d2d2d; background: #2d2d2d;
@@ -487,9 +499,6 @@ input[type="checkbox"]:checked+label {
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
} }
#files td div a:last-child {
width: 100%;
}
#wrap { #wrap {
margin-top: 2em; margin-top: 2em;
} }
@@ -508,9 +517,7 @@ input[type="checkbox"]:checked+label {
#thx_ff { #thx_ff {
padding: 5em 0; padding: 5em 0;
} }
#tree::-webkit-scrollbar-track { #tree::-webkit-scrollbar-track,
background: #333;
}
#tree::-webkit-scrollbar { #tree::-webkit-scrollbar {
background: #333; background: #333;
} }
@@ -821,6 +828,9 @@ html.light #files tr.sel td {
html.light #files tr.sel a { html.light #files tr.sel a {
color: #fff; color: #fff;
} }
html.light #files tr.sel a.play.act {
color: #fb0;
}
html.light input[type="checkbox"] + label { html.light input[type="checkbox"] + label {
color: #333; color: #333;
} }
@@ -868,3 +878,13 @@ html.light #files tr.sel a:hover {
color: #000; color: #000;
background: #fff; background: #fff;
} }
html.light #tree {
scrollbar-color: #a70 #ddd;
}
html.light #tree::-webkit-scrollbar-track,
html.light #tree::-webkit-scrollbar {
background: #ddd;
}
#tree::-webkit-scrollbar-thumb {
background: #da0;
}

View File

@@ -21,7 +21,7 @@
{%- endif %} {%- endif %}
<a href="#" data-perm="write" data-dest="bup" data-desc="bup: basic uploader, even supports netscape 4.0">🎈</a> <a href="#" data-perm="write" data-dest="bup" data-desc="bup: basic uploader, even supports netscape 4.0">🎈</a>
<a href="#" data-perm="write" data-dest="mkdir" data-desc="mkdir: create a new directory">📂</a> <a href="#" data-perm="write" data-dest="mkdir" data-desc="mkdir: create a new directory">📂</a>
<a href="#" data-perm="write" data-dest="new_md" data-desc="new-md: create a new markdown document">📝</a> <a href="#" data-perm="read write" data-dest="new_md" data-desc="new-md: create a new markdown document">📝</a>
<a href="#" data-perm="write" data-dest="msg" data-desc="msg: send a message to the server log">📟</a> <a href="#" data-perm="write" data-dest="msg" data-desc="msg: send a message to the server log">📟</a>
<a href="#" data-dest="cfg" data-desc="configuration options">⚙️</a> <a href="#" data-dest="cfg" data-desc="configuration options">⚙️</a>
<div id="opdesc"></div> <div id="opdesc"></div>

View File

@@ -1306,26 +1306,26 @@ function despin(sel) {
function apply_perms(perms) { function apply_perms(perms) {
perms = perms || []; perms = perms || [];
var o = QSA('#ops>a[data-perm]'); var o = QSA('#ops>a[data-perm], #u2footfoot');
for (var a = 0; a < o.length; a++) for (var a = 0; a < o.length; a++) {
o[a].style.display = 'none'; var display = 'inline';
var needed = o[a].getAttribute('data-perm').split(' ');
for (var a = 0; a < perms.length; a++) { for (var b = 0; b < needed.length; b++) {
o = QSA('#ops>a[data-perm="' + perms[a] + '"]'); if (!has(perms, needed[b])) {
for (var b = 0; b < o.length; b++) display = 'none';
o[b].style.display = 'inline'; }
}
o[a].style.display = display;
} }
var act = QS('#ops>a.act'); var act = QS('#ops>a.act');
if (act) { if (act && act.style.display === 'none')
var areq = act.getAttribute('data-perm'); goto();
if (areq && !has(perms, areq))
goto();
}
document.body.setAttribute('perms', perms.join(' ')); document.body.setAttribute('perms', perms.join(' '));
var have_write = has(perms, "write"), var have_write = has(perms, "write"),
have_read = has(perms, "read"),
tds = QSA('#u2conf td'); tds = QSA('#u2conf td');
for (var a = 0; a < tds.length; a++) { for (var a = 0; a < tds.length; a++) {
@@ -1336,6 +1336,11 @@ function apply_perms(perms) {
if (window['up2k']) if (window['up2k'])
up2k.set_fsearch(); up2k.set_fsearch();
ebi('widget').style.display = have_read ? '' : 'none';
ebi('files').style.display = have_read ? '' : 'none';
if (!have_read)
goto('up2k');
} }

View File

@@ -6,6 +6,11 @@
<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">
<style>
html{font-family:sans-serif}
td{border:1px solid #999;border-width:1px 1px 0 0;padding:0 5px}
a{display:block}
</style>
</head> </head>
<body> <body>
@@ -49,7 +54,7 @@
<div>{{ logues[1] }}</div><br /> <div>{{ logues[1] }}</div><br />
{%- endif %} {%- endif %}
<h2><a href="{{ url_suf }}&amp;h">control-panel</a></h2> <h2><a href="{{ url_suf }}{{ url_suf and '&amp;' or '?' }}h">control-panel</a></h2>
</body> </body>
</html> </html>

View File

@@ -594,12 +594,3 @@ blink {
color: #940; color: #940;
} }
} }
/*
*[data-ln]:before {
content: attr(data-ln);
font-size: .8em;
margin: 0 .4em;
color: #f0c;
}
*/

View File

@@ -1,126 +1,125 @@
#toc { #toc {
display: none; display: none;
} }
#mtw { #mtw {
display: block; display: block;
position: fixed; position: fixed;
left: .5em; left: .5em;
bottom: 0; bottom: 0;
width: calc(100% - 56em); width: calc(100% - 56em);
} }
#mw { #mw {
left: calc(100% - 55em); left: calc(100% - 55em);
overflow-y: auto; overflow-y: auto;
position: fixed; position: fixed;
bottom: 0; bottom: 0;
} }
/* single-screen */ /* single-screen */
#mtw.preview, #mtw.preview,
#mw.editor { #mw.editor {
opacity: 0; opacity: 0;
z-index: 1; z-index: 1;
} }
#mw.preview, #mw.preview,
#mtw.editor { #mtw.editor {
z-index: 5; z-index: 5;
} }
#mtw.single, #mtw.single,
#mw.single { #mw.single {
margin: 0; margin: 0;
left: 1em; left: 1em;
left: max(1em, calc((100% - 56em) / 2)); left: max(1em, calc((100% - 56em) / 2));
} }
#mtw.single { #mtw.single {
width: 55em; width: 55em;
width: min(55em, calc(100% - 2em)); width: min(55em, calc(100% - 2em));
} }
#mp { #mp {
position: relative; position: relative;
} }
#mt, #mtr { #mt, #mtr {
width: 100%; width: 100%;
height: calc(100% - 1px); height: calc(100% - 1px);
color: #444; color: #444;
background: #f7f7f7; background: #f7f7f7;
border: 1px solid #999; border: 1px solid #999;
outline: none; outline: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
font-family: 'consolas', monospace, monospace; font-family: 'consolas', monospace, monospace;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
word-wrap: break-word; /*ie*/ word-wrap: break-word; /*ie*/
overflow-y: scroll; overflow-y: scroll;
line-height: 1.3em; line-height: 1.3em;
font-size: .9em; font-size: .9em;
position: relative; position: relative;
scrollbar-color: #eb0 #f7f7f7; scrollbar-color: #eb0 #f7f7f7;
} }
html.dark #mt { html.dark #mt {
color: #eee; color: #eee;
background: #222; background: #222;
border: 1px solid #777; border: 1px solid #777;
scrollbar-color: #b80 #282828; scrollbar-color: #b80 #282828;
} }
#mtr { #mtr {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
} }
#save.force-save { #save.force-save {
color: #400; color: #400;
background: #f97; background: #f97;
border-radius: .15em; border-radius: .15em;
} }
html.dark #save.force-save { html.dark #save.force-save {
color: #fca; color: #fca;
background: #720; background: #720;
} }
#save.disabled { #save.disabled {
opacity: .4; opacity: .4;
} }
#helpbox, #helpbox,
#toast { #toast {
background: #f7f7f7; background: #f7f7f7;
border-radius: .4em; border-radius: .4em;
z-index: 9001; z-index: 9001;
} }
#helpbox { #helpbox {
display: none; display: none;
position: fixed; position: fixed;
padding: 2em; padding: 2em;
top: 4em; top: 4em;
overflow-y: auto; overflow-y: auto;
box-shadow: 0 .5em 2em #777; box-shadow: 0 .5em 2em #777;
height: calc(100% - 12em); height: calc(100% - 12em);
left: calc(50% - 15em); left: calc(50% - 15em);
right: 0; right: 0;
width: 30em; width: 30em;
} }
#helpclose { #helpclose {
display: block; display: block;
} }
html.dark #helpbox { html.dark #helpbox {
box-shadow: 0 .5em 2em #444; box-shadow: 0 .5em 2em #444;
} }
html.dark #helpbox, html.dark #helpbox,
html.dark #toast { html.dark #toast {
background: #222; background: #222;
border: 1px solid #079; border: 1px solid #079;
border-width: 1px 0; border-width: 1px 0;
} }
#toast { #toast {
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
padding: .6em 0; padding: .6em 0;
position: fixed; position: fixed;
z-index: 9001; top: 30%;
top: 30%; transition: opacity 0.2s ease-in-out;
transition: opacity 0.2s ease-in-out; opacity: 1;
opacity: 1;
} }

View File

@@ -1142,14 +1142,3 @@ action_stack = (function () {
_ref: ref _ref: ref
} }
})(); })();
/*
ebi('help').onclick = function () {
var c1 = getComputedStyle(dom_src).cssText.split(';');
var c2 = getComputedStyle(dom_ref).cssText.split(';');
var max = Math.min(c1.length, c2.length);
for (var a = 0; a < max; a++)
if (c1[a] !== c2[a])
console.log(c1[a] + '\n' + c2[a]);
}
*/

View File

@@ -8,68 +8,58 @@ html .editor-toolbar>i.separator { border-left: 1px solid #ccc; }
html .editor-toolbar.disabled-for-preview>button:not(.no-disable) { opacity: .35 } html .editor-toolbar.disabled-for-preview>button:not(.no-disable) { opacity: .35 }
html { html {
line-height: 1.5em; line-height: 1.5em;
} }
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
min-height: 100%; min-height: 100%;
font-family: sans-serif; font-family: sans-serif;
background: #f7f7f7; background: #f7f7f7;
color: #333; color: #333;
} }
#mn { #mn {
font-weight: normal; font-weight: normal;
margin: 1.3em 0 .7em 1em; margin: 1.3em 0 .7em 1em;
} }
#mn a { #mn a {
color: #444; color: #444;
margin: 0 0 0 -.2em; margin: 0 0 0 -.2em;
padding: 0 0 0 .4em; padding: 0 0 0 .4em;
text-decoration: none; text-decoration: none;
/* ie: */ /* ie: */
border-bottom: .1em solid #777\9; border-bottom: .1em solid #777\9;
margin-right: 1em\9; margin-right: 1em\9;
} }
#mn a:first-child { #mn a:first-child {
padding-left: .5em; padding-left: .5em;
} }
#mn a:last-child { #mn a:last-child {
padding-right: .5em; padding-right: .5em;
} }
#mn a:not(:last-child):after { #mn a:not(:last-child):after {
content: ''; content: '';
width: 1.05em; width: 1.05em;
height: 1.05em; height: 1.05em;
margin: -.2em .3em -.2em -.4em; margin: -.2em .3em -.2em -.4em;
display: inline-block; display: inline-block;
border: 1px solid rgba(0,0,0,0.2); border: 1px solid rgba(0,0,0,0.2);
border-width: .2em .2em 0 0; border-width: .2em .2em 0 0;
transform: rotate(45deg); transform: rotate(45deg);
} }
#mn a:hover { #mn a:hover {
color: #000; color: #000;
text-decoration: underline; text-decoration: underline;
} }
html .editor-toolbar>button.disabled { html .editor-toolbar>button.disabled {
opacity: .35; opacity: .35;
pointer-events: none; pointer-events: none;
} }
html .editor-toolbar>button.save.force-save { html .editor-toolbar>button.save.force-save {
background: #f97; background: #f97;
} }
/*
*[data-ln]:before {
content: attr(data-ln);
font-size: .8em;
margin: 0 .4em;
color: #f0c;
}
.cm-header { font-size: .4em !important }
*/
@@ -101,29 +91,29 @@ html .editor-toolbar>button.save.force-save {
line-height: 1.1em; line-height: 1.1em;
} }
.mdo a { .mdo a {
color: #fff; color: #fff;
background: #39b; background: #39b;
text-decoration: none; text-decoration: none;
padding: 0 .3em; padding: 0 .3em;
border: none; border: none;
border-bottom: .07em solid #079; border-bottom: .07em solid #079;
} }
.mdo h2 { .mdo h2 {
color: #fff; color: #fff;
background: #555; background: #555;
margin-top: 2em; margin-top: 2em;
border-bottom: .22em solid #999; border-bottom: .22em solid #999;
border-top: none; border-top: none;
} }
.mdo h1 { .mdo h1 {
color: #fff; color: #fff;
background: #444; background: #444;
font-weight: normal; font-weight: normal;
border-top: .4em solid #fb0; border-top: .4em solid #fb0;
border-bottom: .4em solid #777; border-bottom: .4em solid #777;
border-radius: 0 1em 0 1em; border-radius: 0 1em 0 1em;
margin: 3em 0 1em 0; margin: 3em 0 1em 0;
padding: .5em 0; padding: .5em 0;
} }
h1, h2 { h1, h2 {
line-height: 1.5em; line-height: 1.5em;
@@ -197,14 +187,14 @@ th {
/* mde support */ /* mde support */
.mdo { .mdo {
padding: 1em; padding: 1em;
background: #f7f7f7; background: #f7f7f7;
} }
html.dark .mdo { html.dark .mdo {
background: #1c1c1c; background: #1c1c1c;
} }
.CodeMirror { .CodeMirror {
background: #f7f7f7; background: #f7f7f7;
} }
@@ -214,108 +204,108 @@ html.dark .mdo {
/* darkmode */ /* darkmode */
html.dark .mdo, html.dark .mdo,
html.dark .CodeMirror { html.dark .CodeMirror {
border-color: #222; border-color: #222;
} }
html.dark, html.dark,
html.dark body, html.dark body,
html.dark .CodeMirror { html.dark .CodeMirror {
background: #222; background: #222;
color: #ccc; color: #ccc;
} }
html.dark .CodeMirror-cursor { html.dark .CodeMirror-cursor {
border-color: #fff; border-color: #fff;
} }
html.dark .CodeMirror-selected { html.dark .CodeMirror-selected {
box-shadow: 0 0 1px #0cf inset; box-shadow: 0 0 1px #0cf inset;
} }
html.dark .CodeMirror-selected, html.dark .CodeMirror-selected,
html.dark .CodeMirror-selectedtext { html.dark .CodeMirror-selectedtext {
border-radius: .1em; border-radius: .1em;
background: #246; background: #246;
color: #fff; color: #fff;
} }
html.dark .mdo a { html.dark .mdo a {
background: #057; background: #057;
} }
html.dark .mdo h1 a, html.dark .mdo h4 a, html.dark .mdo h1 a, html.dark .mdo h4 a,
html.dark .mdo h2 a, html.dark .mdo h5 a, html.dark .mdo h2 a, html.dark .mdo h5 a,
html.dark .mdo h3 a, html.dark .mdo h6 a { html.dark .mdo h3 a, html.dark .mdo h6 a {
color: inherit; color: inherit;
background: none; background: none;
} }
html.dark pre, html.dark pre,
html.dark code { html.dark code {
color: #8c0; color: #8c0;
background: #1a1a1a; background: #1a1a1a;
border: .07em solid #333; border: .07em solid #333;
} }
html.dark .mdo ul, html.dark .mdo ul,
html.dark .mdo ol { html.dark .mdo ol {
border-color: #444; border-color: #444;
} }
html.dark .mdo>ul, html.dark .mdo>ul,
html.dark .mdo>ol { html.dark .mdo>ol {
border-color: #555; border-color: #555;
} }
html.dark strong { html.dark strong {
color: #fff; color: #fff;
} }
html.dark p>em, html.dark p>em,
html.dark li>em, html.dark li>em,
html.dark td>em { html.dark td>em {
color: #f94; color: #f94;
border-color: #666; border-color: #666;
} }
html.dark h1 { html.dark h1 {
background: #383838; background: #383838;
border-top: .4em solid #b80; border-top: .4em solid #b80;
border-bottom: .4em solid #4c4c4c; border-bottom: .4em solid #4c4c4c;
} }
html.dark h2 { html.dark h2 {
background: #444; background: #444;
border-bottom: .22em solid #555; border-bottom: .22em solid #555;
} }
html.dark td, html.dark td,
html.dark th { html.dark th {
border-color: #444; border-color: #444;
} }
html.dark blockquote { html.dark blockquote {
background: #282828; background: #282828;
border: .07em dashed #444; border: .07em dashed #444;
} }
html.dark #mn a { html.dark #mn a {
color: #ccc; color: #ccc;
} }
html.dark #mn a:not(:last-child):after { html.dark #mn a:not(:last-child):after {
border-color: rgba(255,255,255,0.3); border-color: rgba(255,255,255,0.3);
} }
html.dark .editor-toolbar { html.dark .editor-toolbar {
border-color: #2c2c2c; border-color: #2c2c2c;
background: #1c1c1c; background: #1c1c1c;
} }
html.dark .editor-toolbar>i.separator { html.dark .editor-toolbar>i.separator {
border-left: 1px solid #444; border-left: 1px solid #444;
border-right: 1px solid #111; border-right: 1px solid #111;
} }
html.dark .editor-toolbar>button { html.dark .editor-toolbar>button {
margin-left: -1px; border: 1px solid rgba(255,255,255,0.1); margin-left: -1px; border: 1px solid rgba(255,255,255,0.1);
color: #aaa; color: #aaa;
} }
html.dark .editor-toolbar>button:hover { html.dark .editor-toolbar>button:hover {
color: #333; color: #333;
} }
html.dark .editor-toolbar>button.active { html.dark .editor-toolbar>button.active {
color: #333; color: #333;
border-color: #ec1; border-color: #ec1;
background: #c90; background: #c90;
} }
html.dark .editor-toolbar::after, html.dark .editor-toolbar::after,
html.dark .editor-toolbar::before { html.dark .editor-toolbar::before {
background: none; background: none;
} }

View File

@@ -3,7 +3,7 @@ html,body,tr,th,td,#files,a {
background: none; background: none;
font-weight: inherit; font-weight: inherit;
font-size: inherit; font-size: inherit;
padding: none; padding: 0;
border: none; border: none;
} }
html { html {
@@ -20,8 +20,8 @@ body {
padding-bottom: 5em; padding-bottom: 5em;
} }
#box { #box {
padding: .5em 1em; padding: .5em 1em;
background: #2c2c2c; background: #2c2c2c;
} }
pre { pre {
font-family: monospace, monospace; font-family: monospace, monospace;

View File

@@ -13,19 +13,23 @@
<div id="wrap"> <div id="wrap">
<p>hello {{ this.uname }}</p> <p>hello {{ this.uname }}</p>
{%- if rvol %}
<h1>you can browse these:</h1> <h1>you can browse these:</h1>
<ul> <ul>
{% for mp in rvol %} {% for mp in rvol %}
<li><a href="/{{ mp }}{{ url_suf }}">/{{ mp }}</a></li> <li><a href="/{{ mp }}{{ url_suf }}">/{{ mp }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{%- endif %}
{%- if wvol %}
<h1>you can upload to:</h1> <h1>you can upload to:</h1>
<ul> <ul>
{% for mp in wvol %} {% for mp in wvol %}
<li><a href="/{{ mp }}{{ url_suf }}">/{{ mp }}</a></li> <li><a href="/{{ mp }}{{ url_suf }}">/{{ mp }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{%- endif %}
<h1>login for more:</h1> <h1>login for more:</h1>
<ul> <ul>

View File

@@ -18,10 +18,9 @@ function goto_up2k() {
// usually it's undefined but some chromes throw on invoke // usually it's undefined but some chromes throw on invoke
var up2k = null; var up2k = null;
try { try {
crypto.subtle.digest( var cf = crypto.subtle || crypto.webkitSubtle;
'SHA-512', new Uint8Array(1) cf.digest('SHA-512', new Uint8Array(1)).then(
).then( function (x) { up2k = up2k_init(cf) },
function (x) { up2k = up2k_init(true) },
function (x) { up2k = up2k_init(false) } function (x) { up2k = up2k_init(false) }
); );
} }
@@ -401,9 +400,7 @@ function U2pvis(act, btns) {
} }
function up2k_init(have_crypto) { function up2k_init(subtle) {
//have_crypto = false;
// show modal message // show modal message
function showmodal(msg) { function showmodal(msg) {
ebi('u2notbtn').innerHTML = msg; ebi('u2notbtn').innerHTML = msg;
@@ -420,16 +417,18 @@ function up2k_init(have_crypto) {
ebi('u2notbtn').innerHTML = ''; ebi('u2notbtn').innerHTML = '';
} }
var suggest_up2k = 'this is the basic uploader; <a href="#" id="u2yea">up2k</a> is better';
var shame = 'your browser <a href="https://www.chromium.org/blink/webcrypto">disables sha512</a> unless you <a href="' + (window.location + '').replace(':', 's:') + '">use https</a>', var shame = 'your browser <a href="https://www.chromium.org/blink/webcrypto">disables sha512</a> unless you <a href="' + (window.location + '').replace(':', 's:') + '">use https</a>',
is_https = (window.location + '').indexOf('https:') === 0; is_https = (window.location + '').indexOf('https:') === 0;
if (is_https) if (is_https)
// chrome<37 firefox<34 edge<12 ie<11 opera<24 safari<10.1 // chrome<37 firefox<34 edge<12 opera<24 safari<7
shame = 'your browser is impressively ancient'; shame = 'your browser is impressively ancient';
// upload ui hidden by default, clicking the header shows it // upload ui hidden by default, clicking the header shows it
function init_deps() { function init_deps() {
if (!have_crypto && !window.asmCrypto) { if (!subtle && !window.asmCrypto) {
showmodal('<h1>loading sha512.js</h1><h2>since ' + shame + '</h2><h4>thanks chrome</h4>'); showmodal('<h1>loading sha512.js</h1><h2>since ' + shame + '</h2><h4>thanks chrome</h4>');
import_js('/.cpr/deps/sha512.js', unmodal); import_js('/.cpr/deps/sha512.js', unmodal);
@@ -441,34 +440,43 @@ function up2k_init(have_crypto) {
} }
// show uploader if the user only has write-access // show uploader if the user only has write-access
if (!ebi('files')) var perms = document.body.getAttribute('perms');
if (perms && !has(perms.split(' '), 'read'))
goto('up2k'); goto('up2k');
// shows or clears an error message in the basic uploader ui // shows or clears a message in the basic uploader ui
function setmsg(msg) { function setmsg(msg, type) {
if (msg !== undefined) { if (msg !== undefined) {
ebi('u2err').setAttribute('class', 'err'); ebi('u2err').setAttribute('class', type);
ebi('u2err').innerHTML = msg; ebi('u2err').innerHTML = msg;
} }
else { else {
ebi('u2err').setAttribute('class', ''); ebi('u2err').setAttribute('class', '');
ebi('u2err').innerHTML = ''; ebi('u2err').innerHTML = '';
} }
if (msg == suggest_up2k) {
ebi('u2yea').onclick = function (e) {
ev(e);
goto('up2k');
};
}
} }
// switches to the basic uploader with msg as error message // switches to the basic uploader with msg as error message
function un2k(msg) { function un2k(msg) {
setmsg(msg); setmsg(msg, 'err');
return false; return false;
} }
// handle user intent to use the basic uploader instead // handle user intent to use the basic uploader instead
ebi('u2nope').onclick = function (e) { ebi('u2nope').onclick = function (e) {
ev(e); ev(e);
setmsg(); setmsg(suggest_up2k, 'msg');
goto('bup'); goto('bup');
}; };
setmsg(suggest_up2k, 'msg');
if (!String.prototype.format) { if (!String.prototype.format) {
String.prototype.format = function () { String.prototype.format = function () {
var args = arguments; var args = arguments;
@@ -975,14 +983,14 @@ function up2k_init(have_crypto) {
st.todo.handshake.push(t); st.todo.handshake.push(t);
}; };
if (have_crypto) if (subtle)
crypto.subtle.digest('SHA-512', buf).then(hash_done); subtle.digest('SHA-512', buf).then(hash_done);
else { else setTimeout(function () {
var hasher = new asmCrypto.Sha512(); var hasher = new asmCrypto.Sha512();
hasher.process(new Uint8Array(buf)); hasher.process(new Uint8Array(buf));
hasher.finish(); hasher.finish();
hash_done(hasher.result); hash_done(hasher.result);
} }, 1);
}; };
t.t1 = Date.now(); t.t1 = Date.now();
@@ -1231,6 +1239,10 @@ function up2k_init(have_crypto) {
onresize(); onresize();
function desc_show(e) { function desc_show(e) {
var cfg = sread('tooltips');
if (cfg !== null && cfg != '1')
return;
var msg = this.getAttribute('alt'), var msg = this.getAttribute('alt'),
cdesc = ebi('u2cdesc'); cdesc = ebi('u2cdesc');
@@ -1299,14 +1311,21 @@ function up2k_init(have_crypto) {
function set_fsearch(new_state) { function set_fsearch(new_state) {
var perms = document.body.getAttribute('perms'), var perms = document.body.getAttribute('perms'),
read_only = false; fixed = false;
if (!ebi('fsearch')) { if (!ebi('fsearch')) {
new_state = false; new_state = false;
} }
else if (perms && perms.indexOf('write') === -1) { else if (perms) {
new_state = true; perms = perms.split(' ');
read_only = true; if (!has(perms, 'write')) {
new_state = true;
fixed = true;
}
if (!has(perms, 'read')) {
new_state = false;
fixed = true;
}
} }
if (new_state !== undefined) { if (new_state !== undefined) {
@@ -1315,7 +1334,7 @@ function up2k_init(have_crypto) {
} }
try { try {
QS('label[for="fsearch"]').style.opacity = read_only ? '0' : '1'; QS('label[for="fsearch"]').style.display = QS('#fsearch').style.display = fixed ? 'none' : '';
} }
catch (ex) { } catch (ex) { }

View File

@@ -19,6 +19,11 @@
color: #f87; color: #f87;
padding: .5em; padding: .5em;
} }
#u2err.msg {
color: #999;
padding: .5em;
font-size: .9em;
}
#u2btn { #u2btn {
color: #eee; color: #eee;
background: #555; background: #555;
@@ -86,12 +91,13 @@
font-family: sans-serif; font-family: sans-serif;
width: auto; width: auto;
} }
#u2tab tr+tr:hover td { #u2tab tbody tr:hover td {
background: #222; background: #222;
} }
#u2cards { #u2cards {
padding: 1em 0 .3em 1em; padding: 1em 0 .3em 1em;
margin: 1.5em auto -2.5em auto; margin: 1.5em auto -2.5em auto;
white-space: nowrap;
text-align: center; text-align: center;
overflow: hidden; overflow: hidden;
} }
@@ -217,7 +223,7 @@
padding: 0 1em; padding: 0 1em;
height: 0; height: 0;
opacity: .1; opacity: .1;
transition: all 0.14s ease-in-out; transition: all 0.14s ease-in-out;
box-shadow: 0 .2em .5em #222; box-shadow: 0 .2em .5em #222;
border-radius: .4em; border-radius: .4em;
z-index: 1; z-index: 1;
@@ -286,3 +292,6 @@ html.light #op_up2k.srch #u2btn {
html.light #u2foot { html.light #u2foot {
color: #000; color: #000;
} }
html.light #u2tab tbody tr:hover td {
background: #fff;
}

View File

@@ -36,7 +36,7 @@
<table id="u2conf"> <table id="u2conf">
<tr> <tr>
<td>parallel uploads</td> <td><br />parallel uploads:</td>
<td rowspan="2"> <td rowspan="2">
<input type="checkbox" id="multitask" /> <input type="checkbox" id="multitask" />
<label for="multitask" alt="continue hashing other files while uploading">🏃</label> <label for="multitask" alt="continue hashing other files while uploading">🏃</label>
@@ -61,7 +61,7 @@
<td> <td>
<a href="#" id="nthread_sub">&ndash;</a><input <a href="#" id="nthread_sub">&ndash;</a><input
class="txtbox" id="nthread" value="2"/><a class="txtbox" id="nthread" value="2"/><a
href="#" id="nthread_add">+</a> href="#" id="nthread_add">+</a><br />&nbsp;
</td> </td>
</tr> </tr>
</table> </table>
@@ -99,5 +99,5 @@
</table> </table>
<p id="u2foot"></p> <p id="u2foot"></p>
<p id="u2footfoot">( if you don't need lastmod timestamps, resumable uploads or progress bars just use the <a href="#" id="u2nope">basic uploader</a>)</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>
</div> </div>

View File

@@ -32,9 +32,13 @@ r
# and a folder where anyone can upload # and a folder where anyone can upload
# but nobody can see the contents # but nobody can see the contents
# and set the e2d flag to enable the uploads database
# and set the nodupe flag to reject duplicate uploads
/home/ed/inc /home/ed/inc
/dump /dump
w w
c e2d
c nodupe
# this entire config file can be replaced with these arguments: # this entire config file can be replaced with these arguments:
# -u ed:123 -u k:k -v .::r:aed -v priv:priv:rk:aed -v /home/ed/Music:music:r -v /home/ed/inc:dump:w # -u ed:123 -u k:k -v .::r:aed -v priv:priv:rk:aed -v /home/ed/Music:music:r -v /home/ed/inc:dump:w

32
docs/minimal-up2k.html Normal file
View File

@@ -0,0 +1,32 @@
<!--
save this as .epilogue.html inside a write-only folder to declutter the UI, makes it look like
https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png
-->
<style>
/* make the up2k ui REALLY minimal by hiding a bunch of stuff: */
#ops, #tree, #path, #wrap>h2:last-child /* main tabs and navigators (tree/breadcrumbs) */
#u2cleanup, #u2conf tr:first-child>td[rowspan]:not(#u2btn_cw), /* most of the config options */
#u2cards /* and the upload progress tabs */
{display: none !important} /* do it! */
/* add some margins because now it's weird */
.opview {margin-top: 2.5em}
#op_up2k {margin-top: 3em}
/* and embiggen the upload button */
#u2conf #u2btn, #u2btn {padding:1.5em 0}
/* adjust the button area a bit */
#u2conf.has_btn {width: 35em !important; margin: 5em auto}
</style>
<a href="#" onclick="this.parentNode.innerHTML='';">show advanced options</a>

View File

@@ -117,7 +117,7 @@ cd sfx
ver= ver=
git describe --tags >/dev/null 2>/dev/null && { git describe --tags >/dev/null 2>/dev/null && {
git_ver="$(git describe --tags)"; # v0.5.5-2-gb164aa0 git_ver="$(git describe --tags)"; # v0.5.5-2-gb164aa0
ver="$(printf '%s\n' "$git_ver" | sed -r 's/^v//; s/-g?/./g')"; ver="$(printf '%s\n' "$git_ver" | sed -r 's/^v//')";
t_ver= t_ver=
printf '%s\n' "$git_ver" | grep -qE '^v[0-9\.]+$' && { printf '%s\n' "$git_ver" | grep -qE '^v[0-9\.]+$' && {
@@ -163,7 +163,7 @@ find .. -type f \( -name .DS_Store -or -name ._.DS_Store \) -delete
find .. -type f -name ._\* | while IFS= read -r f; do cmp <(printf '\x00\x05\x16') <(head -c 3 -- "$f") && rm -f -- "$f"; done find .. -type f -name ._\* | while IFS= read -r f; do cmp <(printf '\x00\x05\x16') <(head -c 3 -- "$f") && rm -f -- "$f"; done
echo use smol web deps echo use smol web deps
rm -f copyparty/web/deps/*.full.* copyparty/web/{Makefile,splash.js} rm -f copyparty/web/deps/*.full.* copyparty/web/Makefile
# it's fine dw # it's fine dw
grep -lE '\.full\.(js|css)' copyparty/web/* | grep -lE '\.full\.(js|css)' copyparty/web/* |
@@ -199,12 +199,19 @@ find | grep -E '\.(js|css|html)$' | while IFS= read -r f; do
tmv "$f" tmv "$f"
done done
echo gen tarlist
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/' | grep -vE '/list1?$' > list1
(grep -vE 'gz$' list1; grep -E 'gz$' list1) >list
echo creating tar echo creating tar
args=(--owner=1000 --group=1000) args=(--owner=1000 --group=1000)
[ "$OSTYPE" = msys ] && [ "$OSTYPE" = msys ] &&
args=() args=()
tar -cf tar "${args[@]}" --numeric-owner copyparty dep-j2 tar -cf tar "${args[@]}" --numeric-owner -T list
pc=bzip2 pc=bzip2
pe=bz2 pe=bz2

View File

@@ -2,7 +2,8 @@
# coding: latin-1 # coding: latin-1
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import os, sys, time, shutil, threading, tarfile, hashlib, platform, tempfile, traceback import re, os, sys, time, shutil, signal, threading, tarfile, hashlib, platform, tempfile, traceback
import subprocess as sp
""" """
run me with any version of python, i will unpack and run copyparty run me with any version of python, i will unpack and run copyparty
@@ -29,19 +30,18 @@ PY2 = sys.version_info[0] == 2
WINDOWS = sys.platform in ["win32", "msys"] WINDOWS = sys.platform in ["win32", "msys"]
sys.dont_write_bytecode = True sys.dont_write_bytecode = True
me = os.path.abspath(os.path.realpath(__file__)) me = os.path.abspath(os.path.realpath(__file__))
cpp = None
def eprint(*args, **kwargs): def eprint(*a, **ka):
kwargs["file"] = sys.stderr ka["file"] = sys.stderr
print(*args, **kwargs) print(*a, **ka)
def msg(*args, **kwargs): def msg(*a, **ka):
if args: if a:
args = ["[SFX]", args[0]] + list(args[1:]) a = ["[SFX]", a[0]] + list(a[1:])
eprint(*args, **kwargs) eprint(*a, **ka)
# skip 1 # skip 1
@@ -156,6 +156,9 @@ def encode(data, size, cksum, ver, ts):
skip = True skip = True
continue continue
if ln.strip().startswith("# fmt: "):
continue
unpk += ln + "\n" unpk += ln + "\n"
for k, v in [ for k, v in [
@@ -209,11 +212,11 @@ def yieldfile(fn):
def hashfile(fn): def hashfile(fn):
hasher = hashlib.md5() h = hashlib.md5()
for block in yieldfile(fn): for block in yieldfile(fn):
hasher.update(block) h.update(block)
return hasher.hexdigest() return h.hexdigest()
def unpack(): def unpack():
@@ -222,9 +225,10 @@ def unpack():
tag = "v" + str(STAMP) tag = "v" + str(STAMP)
withpid = "{}.{}".format(name, os.getpid()) withpid = "{}.{}".format(name, os.getpid())
top = tempfile.gettempdir() top = tempfile.gettempdir()
final = os.path.join(top, name) opj = os.path.join
mine = os.path.join(top, withpid) final = opj(top, name)
tar = os.path.join(mine, "tar") mine = opj(top, withpid)
tar = opj(mine, "tar")
try: try:
if tag in os.listdir(final): if tag in os.listdir(final):
@@ -233,28 +237,24 @@ def unpack():
except: except:
pass pass
nwrite = 0 sz = 0
os.mkdir(mine) os.mkdir(mine)
with open(tar, "wb") as f: with open(tar, "wb") as f:
for buf in get_payload(): for buf in get_payload():
nwrite += len(buf) sz += len(buf)
f.write(buf) f.write(buf)
if nwrite != SIZE: ck = hashfile(tar)
t = "\n\n bad file:\n expected {} bytes, got {}\n".format(SIZE, nwrite) if ck != CKSUM:
raise Exception(t) t = "\n\nexpected {} ({} byte)\nobtained {} ({} byte)\nsfx corrupt"
raise Exception(t.format(CKSUM, SIZE, ck, sz))
cksum = hashfile(tar)
if cksum != CKSUM:
t = "\n\n bad file:\n {} expected,\n {} obtained\n".format(CKSUM, cksum)
raise Exception(t)
with tarfile.open(tar, "r:bz2") as tf: with tarfile.open(tar, "r:bz2") as tf:
tf.extractall(mine) tf.extractall(mine)
os.remove(tar) os.remove(tar)
with open(os.path.join(mine, tag), "wb") as f: with open(opj(mine, tag), "wb") as f:
f.write(b"h\n") f.write(b"h\n")
try: try:
@@ -272,25 +272,25 @@ def unpack():
except: except:
pass pass
for fn in u8(os.listdir(top)):
if fn.startswith(name) and fn != withpid:
try:
old = opj(top, fn)
if time.time() - os.path.getmtime(old) > 86400:
shutil.rmtree(old)
except:
pass
try: try:
os.symlink(mine, final) os.symlink(mine, final)
except: except:
try: try:
os.rename(mine, final) os.rename(mine, final)
return final
except: except:
msg("reloc fail,", mine) msg("reloc fail,", mine)
return mine
for fn in u8(os.listdir(top)): return mine
if fn.startswith(name) and fn not in [name, withpid]:
try:
old = os.path.join(top, fn)
if time.time() - os.path.getmtime(old) > 10:
shutil.rmtree(old)
except:
pass
return final
def get_payload(): def get_payload():
@@ -307,37 +307,33 @@ def get_payload():
if ofs < 0: if ofs < 0:
raise Exception("could not find archive marker") raise Exception("could not find archive marker")
# start reading from the final b"\n" # start at final b"\n"
fpos = ofs + len(ptn) - 3 fpos = ofs + len(ptn) - 3
# msg("tar found at", fpos)
f.seek(fpos) f.seek(fpos)
dpos = 0 dpos = 0
leftovers = b"" rem = b""
while True: while True:
rbuf = f.read(1024 * 32) rbuf = f.read(1024 * 32)
if rbuf: if rbuf:
buf = leftovers + rbuf buf = rem + rbuf
ofs = buf.rfind(b"\n") ofs = buf.rfind(b"\n")
if len(buf) <= 4: if len(buf) <= 4:
leftovers = buf rem = buf
continue continue
if ofs >= len(buf) - 4: if ofs >= len(buf) - 4:
leftovers = buf[ofs:] rem = buf[ofs:]
buf = buf[:ofs] buf = buf[:ofs]
else: else:
leftovers = b"\n# " rem = b"\n# "
else: else:
buf = leftovers buf = rem
fpos += len(buf) + 1 fpos += len(buf) + 1
buf = ( for a, b in [[b"\n# ", b""], [b"\n#r", b"\r"], [b"\n#n", b"\n"]]:
buf.replace(b"\n# ", b"") buf = buf.replace(a, b)
.replace(b"\n#r", b"\r")
.replace(b"\n#n", b"\n")
)
dpos += len(buf) - 1
dpos += len(buf) - 1
yield buf yield buf
if not rbuf: if not rbuf:
@@ -361,7 +357,7 @@ def utime(top):
def confirm(rv): def confirm(rv):
msg() msg()
msg(traceback.format_exc()) msg("retcode", rv if rv else traceback.format_exc())
msg("*** hit enter to exit ***") msg("*** hit enter to exit ***")
try: try:
raw_input() if PY2 else input() raw_input() if PY2 else input()
@@ -371,10 +367,8 @@ def confirm(rv):
sys.exit(rv) sys.exit(rv)
def run(tmp, j2ver): def run(tmp, j2):
global cpp msg("jinja2:", j2 or "bundled")
msg("jinja2:", j2ver or "bundled")
msg("sfxdir:", tmp) msg("sfxdir:", tmp)
msg() msg()
@@ -384,7 +378,6 @@ def run(tmp, j2ver):
fd = os.open(tmp, os.O_RDONLY) fd = os.open(tmp, os.O_RDONLY)
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
tmp = os.readlink(tmp) # can't flock a symlink, even with O_NOFOLLOW
except Exception as ex: except Exception as ex:
if not WINDOWS: if not WINDOWS:
msg("\033[31mflock:", repr(ex)) msg("\033[31mflock:", repr(ex))
@@ -394,22 +387,39 @@ def run(tmp, j2ver):
t.start() t.start()
ld = [tmp, os.path.join(tmp, "dep-j2")] ld = [tmp, os.path.join(tmp, "dep-j2")]
if j2ver: if j2:
del ld[-1] del ld[-1]
if any([re.match(r"^-.*j[0-9]", x) for x in sys.argv]):
run_s(ld)
else:
run_i(ld)
def run_i(ld):
for x in ld: for x in ld:
sys.path.insert(0, x) sys.path.insert(0, x)
try: from copyparty.__main__ import main as p
from copyparty.__main__ import main as copyparty
copyparty() p()
except SystemExit as ex:
if ex.code: def run_s(ld):
confirm(ex.code) # fmt: off
except: c = "import sys,runpy;" + "".join(['sys.path.insert(0,r"' + x + '");' for x in ld]) + 'runpy.run_module("copyparty",run_name="__main__")'
confirm(1) c = [str(x) for x in [sys.executable, "-c", c] + list(sys.argv[1:])]
# fmt: on
msg("\n", c, "\n")
p = sp.Popen(c)
def bye(*a):
p.send_signal(signal.SIGINT)
signal.signal(signal.SIGTERM, bye)
p.wait()
raise SystemExit(p.returncode)
def main(): def main():
@@ -443,14 +453,23 @@ def main():
# skip 0 # skip 0
tmp = unpack() tmp = os.path.realpath(unpack())
try: try:
from jinja2 import __version__ as j2ver from jinja2 import __version__ as j2
except: except:
j2ver = None j2 = None
run(tmp, j2ver) try:
run(tmp, j2)
except SystemExit as ex:
c = ex.code
if c not in [0, -15]:
confirm(ex.code)
except KeyboardInterrupt:
pass
except:
confirm(0)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -17,14 +17,15 @@ __license__ = "MIT"
__url__ = "https://github.com/9001/copyparty/" __url__ = "https://github.com/9001/copyparty/"
def get_spd(nbyte, nsec): def get_spd(nbyte, nfiles, nsec):
if not nsec: if not nsec:
return "0.000 MB 0.000 sec 0.000 MB/s" return "0.000 MB 0 files 0.000 sec 0.000 MB/s 0.000 f/s"
mb = nbyte / (1024 * 1024.0) mb = nbyte / (1024 * 1024.0)
spd = mb / nsec spd = mb / nsec
nspd = nfiles / nsec
return f"{mb:.3f} MB {nsec:.3f} sec {spd:.3f} MB/s" return f"{mb:.3f} MB {nfiles} files {nsec:.3f} sec {spd:.3f} MB/s {nspd:.3f} f/s"
class Inf(object): class Inf(object):
@@ -36,6 +37,7 @@ class Inf(object):
self.mtx_reports = threading.Lock() self.mtx_reports = threading.Lock()
self.n_byte = 0 self.n_byte = 0
self.n_file = 0
self.n_sec = 0 self.n_sec = 0
self.n_done = 0 self.n_done = 0
self.t0 = t0 self.t0 = t0
@@ -63,7 +65,8 @@ class Inf(object):
continue continue
msgs = msgs[-64:] msgs = msgs[-64:]
msgs = [f"{get_spd(self.n_byte, self.n_sec)} {x}" for x in msgs] spd = get_spd(self.n_byte, len(self.reports), self.n_sec)
msgs = [f"{spd} {x}" for x in msgs]
print("\n".join(msgs)) print("\n".join(msgs))
def report(self, fn, n_byte, n_sec): def report(self, fn, n_byte, n_sec):
@@ -131,8 +134,9 @@ def main():
num_threads = 8 num_threads = 8
read_sz = 32 * 1024 read_sz = 32 * 1024
targs = (q, inf, read_sz)
for _ in range(num_threads): for _ in range(num_threads):
thr = threading.Thread(target=worker, args=(q, inf, read_sz,)) thr = threading.Thread(target=worker, args=targs)
thr.daemon = True thr.daemon = True
thr.start() thr.start()
@@ -151,14 +155,14 @@ def main():
log = inf.reports log = inf.reports
log.sort() log.sort()
for nbyte, nsec, fn in log[-64:]: for nbyte, nsec, fn in log[-64:]:
print(f"{get_spd(nbyte, nsec)} {fn}") spd = get_spd(nbyte, len(log), nsec)
print(f"{spd} {fn}")
print() print()
print("\n".join(inf.errors)) print("\n".join(inf.errors))
print(get_spd(inf.n_byte, t2 - t0)) print(get_spd(inf.n_byte, len(log), t2 - t0))
if __name__ == "__main__": if __name__ == "__main__":
main() main()