Compare commits

...

25 Commits

Author SHA1 Message Date
ed
d3ccacccb1 v1.16.1 2024-11-15 22:18:11 +00:00
ed
df386c8fbc ux: fix paste msg + cleanup css 2024-11-15 22:11:51 +00:00
ed
4d15dd6e17 cbz thumbnails 2024-11-15 21:33:37 +00:00
ed
56a0499636 fix gallery links when msel enabled 2024-11-15 20:04:13 +00:00
ed
10fc4768e8 fix dl from jumpvols with -j0 2024-11-15 19:29:44 +00:00
ed
2b63d7d10d detect invalid config (prevent db loss) 2024-11-15 08:04:58 +00:00
ed
1f177528c1 fix advanced options for password-hashing
and allow raising scrypt ram usage past OpenSSL's default 32 MiB
2024-11-15 00:42:08 +00:00
ed
fc3bbb70a3 update pkgs to 1.16.0 2024-11-10 20:00:38 +00:00
ed
ce3cab0295 v1.16.0 2024-11-10 19:32:37 +00:00
ed
c784e5285e u2c: adaptive connection:keepalive expiration 2024-11-10 17:43:40 +00:00
ed
2bf9055cae detect free RAM on startup for sane defaults
* if free ram on startup is less than 2 GiB,
   use smaller chunks for parallel file hashing

* if --th-max-ram is lower than 0.25 (256 MiB),
   print a warning that thumbnails will not work

* make thumbnail cleaner immediately do a sweep on startup,
   forgetting any failed conversions so they can be retried
   in case the memory limit was increased since last run
2024-11-10 15:43:19 +00:00
ed
8aba5aed4f list active downloads in controlpanel 2024-11-10 02:12:18 +00:00
ed
0ce7cf5e10 update comparison / versus.md 2024-11-09 14:44:03 +00:00
ed
96edcbccd7 https://ocv.me/stuff/goed-gedaan.jpg 2024-11-08 22:11:33 +00:00
ed
4603afb6de don't consume ctrl-shift-c (devtools inspector) 2024-11-08 21:51:54 +00:00
ed
56317b00af filecopy: ui for resolving name conflicts 2024-11-08 02:12:28 +00:00
ed
cacec9c1f3 support copying files/folders; closes #115
behaves according to the target volume's deduplication config;
will create symlinks / hardlinks instead if dedup is enabled
2024-11-07 21:41:53 +00:00
ed
44ee07f0b2 IdP: async reload; closes #114
whenever a new idp user is registered, up2k will continuously
reload in the background until all users have been processed

just like before, this blocks up2k uploads from each user
until said user makes it into a reload, but as of now,
reloads will batch and execute without interrupting read-access

needs further testing before next release,
probably some rough edges to sand down
2024-11-04 22:31:48 +00:00
ed
6a8d5e1731 ui: batch-rename: remember last regex + format 2024-11-02 18:06:39 +00:00
ed
d9962f65b3 ui: folder loading indicator stole focus
show a spinning halfcircle around the +/- instead of
moving the focus to the selected folder in the sidebar,
since that could mess with keyboard scrolling
2024-11-02 17:58:30 +00:00
ed
119e88d87b bubble OS-filesystem errors to client
send a 500 or 404 if a folder is inaccessible or does not exist

previously it would return an empty directory listing instead
2024-11-02 17:38:17 +00:00
ed
71d9e010d9 ui: make hotkey-help less eager to show itself
would appear when typing `?` into textboxes
2024-10-30 19:40:48 +00:00
ed
5718caa957 ui: url-options to set grid/thumbs on/off 2024-10-30 19:24:00 +00:00
ed
efd8a32ed6 ui: show switch-to-https on 403s too 2024-10-28 03:38:15 +00:00
ed
b22d700e16 update pkgs to 1.15.10 2024-10-27 09:27:38 +00:00
44 changed files with 1392 additions and 353 deletions

View File

@@ -428,7 +428,7 @@ configuring accounts/volumes with arguments:
permissions: permissions:
* `r` (read): browse folder contents, download files, download as zip/tar, see filekeys/dirkeys * `r` (read): browse folder contents, download files, download as zip/tar, see filekeys/dirkeys
* `w` (write): upload files, move files *into* this folder * `w` (write): upload files, move/copy files *into* this folder
* `m` (move): move files/folders *from* this folder * `m` (move): move files/folders *from* this folder
* `d` (delete): delete files/folders * `d` (delete): delete files/folders
* `.` (dots): user can ask to show dotfiles in directory listings * `.` (dots): user can ask to show dotfiles in directory listings
@@ -508,7 +508,8 @@ the browser has the following hotkeys (always qwerty)
* `ESC` close various things * `ESC` close various things
* `ctrl-K` delete selected files/folders * `ctrl-K` delete selected files/folders
* `ctrl-X` cut selected files/folders * `ctrl-X` cut selected files/folders
* `ctrl-V` paste * `ctrl-C` copy selected files/folders to clipboard
* `ctrl-V` paste (move/copy)
* `Y` download selected files * `Y` download selected files
* `F2` [rename](#batch-rename) selected file/folder * `F2` [rename](#batch-rename) selected file/folder
* when a file/folder is selected (in not-grid-view): * when a file/folder is selected (in not-grid-view):
@@ -577,6 +578,7 @@ click the `🌲` or pressing the `B` hotkey to toggle between breadcrumbs path (
press `g` or `` to toggle grid-view instead of the file listing and `t` toggles icons / thumbnails press `g` or `` to toggle grid-view instead of the file listing and `t` toggles icons / thumbnails
* can be made default globally with `--grid` or per-volume with volflag `grid` * can be made default globally with `--grid` or per-volume with volflag `grid`
* enable by adding `?imgs` to a link, or disable with `?imgs=0`
![copyparty-thumbs-fs8](https://user-images.githubusercontent.com/241032/129636211-abd20fa2-a953-4366-9423-1c88ebb96ba9.png) ![copyparty-thumbs-fs8](https://user-images.githubusercontent.com/241032/129636211-abd20fa2-a953-4366-9423-1c88ebb96ba9.png)
@@ -756,10 +758,11 @@ file selection: click somewhere on the line (not the link itself), then:
* shift-click another line for range-select * shift-click another line for range-select
* cut: select some files and `ctrl-x` * cut: select some files and `ctrl-x`
* copy: select some files and `ctrl-c`
* paste: `ctrl-v` in another folder * paste: `ctrl-v` in another folder
* rename: `F2` * rename: `F2`
you can move files across browser tabs (cut in one tab, paste in another) you can copy/move files across browser tabs (cut/copy in one tab, paste in another)
## shares ## shares
@@ -1684,6 +1687,7 @@ scrape_configs:
currently the following metrics are available, currently the following metrics are available,
* `cpp_uptime_seconds` time since last copyparty restart * `cpp_uptime_seconds` time since last copyparty restart
* `cpp_boot_unixtime_seconds` same but as an absolute timestamp * `cpp_boot_unixtime_seconds` same but as an absolute timestamp
* `cpp_active_dl` number of active downloads
* `cpp_http_conns` number of open http(s) connections * `cpp_http_conns` number of open http(s) connections
* `cpp_http_reqs` number of http(s) requests handled * `cpp_http_reqs` number of http(s) requests handled
* `cpp_sus_reqs` number of 403/422/malicious requests * `cpp_sus_reqs` number of 403/422/malicious requests

View File

@@ -2,7 +2,7 @@ standalone programs which are executed by copyparty when an event happens (uploa
these programs either take zero arguments, or a filepath (the affected file), or a json message with filepath + additional info these programs either take zero arguments, or a filepath (the affected file), or a json message with filepath + additional info
run copyparty with `--help-hooks` for usage details / hook type explanations (xm/xbu/xau/xiu/xbr/xar/xbd/xad/xban) run copyparty with `--help-hooks` for usage details / hook type explanations (xm/xbu/xau/xiu/xbc/xac/xbr/xar/xbd/xad/xban)
> **note:** in addition to event hooks (the stuff described here), copyparty has another api to run your programs/scripts while providing way more information such as audio tags / video codecs / etc and optionally daisychaining data between scripts in a processing pipeline; if that's what you want then see [mtp plugins](../mtag/) instead > **note:** in addition to event hooks (the stuff described here), copyparty has another api to run your programs/scripts while providing way more information such as audio tags / video codecs / etc and optionally daisychaining data between scripts in a processing pipeline; if that's what you want then see [mtp plugins](../mtag/) instead

View File

@@ -393,7 +393,8 @@ class Gateway(object):
if r.status != 200: if r.status != 200:
self.closeconn() self.closeconn()
info("http error %s reading dir %r", r.status, web_path) info("http error %s reading dir %r", r.status, web_path)
raise FuseOSError(errno.ENOENT) err = errno.ENOENT if r.status == 404 else errno.EIO
raise FuseOSError(err)
ctype = r.getheader("Content-Type", "") ctype = r.getheader("Content-Type", "")
if ctype == "application/json": if ctype == "application/json":

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
S_VERSION = "2.5" S_VERSION = "2.6"
S_BUILD_DT = "2024-10-18" S_BUILD_DT = "2024-11-10"
""" """
u2c.py: upload to copyparty u2c.py: upload to copyparty
@@ -189,6 +189,8 @@ class HCli(object):
return C(self.addr, self.port, timeout=timeout, **args) return C(self.addr, self.port, timeout=timeout, **args)
def req(self, meth, vpath, hdrs, body=None, ctype=None): def req(self, meth, vpath, hdrs, body=None, ctype=None):
now = time.time()
hdrs.update(self.base_hdrs) hdrs.update(self.base_hdrs)
if self.ar.a: if self.ar.a:
hdrs["PW"] = self.ar.a hdrs["PW"] = self.ar.a
@@ -201,7 +203,9 @@ class HCli(object):
# large timeout for handshakes (safededup) # large timeout for handshakes (safededup)
conns = self.hconns if ctype == MJ else self.conns conns = self.hconns if ctype == MJ else self.conns
c = conns.pop() if conns else self._connect(999 if ctype == MJ else 128) while conns and self.ar.cxp < now - conns[0][0]:
conns.pop(0)[1].close()
c = conns.pop()[1] if conns else self._connect(999 if ctype == MJ else 128)
try: try:
c.request(meth, vpath, body, hdrs) c.request(meth, vpath, body, hdrs)
if PY27: if PY27:
@@ -210,8 +214,15 @@ class HCli(object):
rsp = c.getresponse() rsp = c.getresponse()
data = rsp.read() data = rsp.read()
conns.append(c) conns.append((time.time(), c))
return rsp.status, data.decode("utf-8") return rsp.status, data.decode("utf-8")
except http_client.BadStatusLine:
if self.ar.cxp > 4:
t = "\nWARNING: --cxp probably too high; reducing from %d to 4"
print(t % (self.ar.cxp,))
self.ar.cxp = 4
c.close()
raise
except: except:
c.close() c.close()
raise raise
@@ -1142,7 +1153,10 @@ class Ctl(object):
if self.ar.drd: if self.ar.drd:
dp = os.path.join(top, rd) dp = os.path.join(top, rd)
try:
lnodes = set(os.listdir(dp)) lnodes = set(os.listdir(dp))
except:
lnodes = list(ls) # fs eio; don't delete
if ptn: if ptn:
zs = dp.replace(sep, b"/").rstrip(b"/") + b"/" zs = dp.replace(sep, b"/").rstrip(b"/") + b"/"
zls = [zs + x for x in lnodes] zls = [zs + x for x in lnodes]
@@ -1500,6 +1514,7 @@ source file/folder selection uses rsync syntax, meaning that:
ap.add_argument("--szm", type=int, metavar="MiB", default=96, help="max size of each POST (default is cloudflare max)") ap.add_argument("--szm", type=int, metavar="MiB", default=96, help="max size of each POST (default is cloudflare max)")
ap.add_argument("-nh", action="store_true", help="disable hashing while uploading") ap.add_argument("-nh", action="store_true", help="disable hashing while uploading")
ap.add_argument("-ns", action="store_true", help="no status panel (for slow consoles and macos)") ap.add_argument("-ns", action="store_true", help="no status panel (for slow consoles and macos)")
ap.add_argument("--cxp", type=float, metavar="SEC", default=57, help="assume http connections expired after SEConds")
ap.add_argument("--cd", type=float, metavar="SEC", default=5, help="delay before reattempting a failed handshake/upload") ap.add_argument("--cd", type=float, metavar="SEC", default=5, help="delay before reattempting a failed handshake/upload")
ap.add_argument("--safe", action="store_true", help="use simple fallback approach") ap.add_argument("--safe", action="store_true", help="use simple fallback approach")
ap.add_argument("-z", action="store_true", help="ZOOMIN' (skip uploading files if they exist at the destination with the ~same last-modified timestamp, so same as yolo / turbo with date-chk but even faster)") ap.add_argument("-z", action="store_true", help="ZOOMIN' (skip uploading files if they exist at the destination with the ~same last-modified timestamp, so same as yolo / turbo with date-chk but even faster)")

View File

@@ -1,6 +1,6 @@
# Maintainer: icxes <dev.null@need.moe> # Maintainer: icxes <dev.null@need.moe>
pkgname=copyparty pkgname=copyparty
pkgver="1.15.9" pkgver="1.16.0"
pkgrel=1 pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any") arch=("any")
@@ -21,7 +21,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
) )
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}.d/init" ) backup=("etc/${pkgname}.d/init" )
sha256sums=("f75668c752468cab3e4d7bb93323ab9ac7309ee74a138d3597ef0edabd9235de") sha256sums=("8a802bbb4392ead6bc92bcb1c71ecd1855a05a5b4d0312499c33f65424c12a00")
build() { build() {
cd "${srcdir}/${pkgname}-${pkgver}" cd "${srcdir}/${pkgname}-${pkgver}"

View File

@@ -1,5 +1,5 @@
{ {
"url": "https://github.com/9001/copyparty/releases/download/v1.15.9/copyparty-sfx.py", "url": "https://github.com/9001/copyparty/releases/download/v1.16.0/copyparty-sfx.py",
"version": "1.15.9", "version": "1.16.0",
"hash": "sha256-pxX1RkfO3h1XyPQnrwkFp8laOS4sqXAwbsi2V18VnVY=" "hash": "sha256-H9imF66HcE6I/gGPZdJ5zkzATC3Vkc4luTAbRy8GRh4="
} }

View File

@@ -80,6 +80,7 @@ web/deps/prismd.css
web/deps/scp.woff2 web/deps/scp.woff2
web/deps/sha512.ac.js web/deps/sha512.ac.js
web/deps/sha512.hw.js web/deps/sha512.hw.js
web/iiam.gif
web/md.css web/md.css
web/md.html web/md.html
web/md.js web/md.js

View File

@@ -50,6 +50,8 @@ from .util import (
PARTFTPY_VER, PARTFTPY_VER,
PY_DESC, PY_DESC,
PYFTPD_VER, PYFTPD_VER,
RAM_AVAIL,
RAM_TOTAL,
SQLITE_VER, SQLITE_VER,
UNPLICATIONS, UNPLICATIONS,
Daemon, Daemon,
@@ -684,6 +686,8 @@ def get_sects():
\033[36mxbu\033[35m executes CMD before a file upload starts \033[36mxbu\033[35m executes CMD before a file upload starts
\033[36mxau\033[35m executes CMD after a file upload finishes \033[36mxau\033[35m executes CMD after a file upload finishes
\033[36mxiu\033[35m executes CMD after all uploads finish and volume is idle \033[36mxiu\033[35m executes CMD after all uploads finish and volume is idle
\033[36mxbc\033[35m executes CMD before a file copy
\033[36mxac\033[35m executes CMD after a file copy
\033[36mxbr\033[35m executes CMD before a file rename/move \033[36mxbr\033[35m executes CMD before a file rename/move
\033[36mxar\033[35m executes CMD after a file rename/move \033[36mxar\033[35m executes CMD after a file rename/move
\033[36mxbd\033[35m executes CMD before a file delete \033[36mxbd\033[35m executes CMD before a file delete
@@ -874,8 +878,9 @@ def get_sects():
use argon2id with timecost 3, 256 MiB, 4 threads, version 19 (0x13/v1.3) use argon2id with timecost 3, 256 MiB, 4 threads, version 19 (0x13/v1.3)
\033[36m--ah-alg scrypt\033[0m # which is the same as: \033[36m--ah-alg scrypt\033[0m # which is the same as:
\033[36m--ah-alg scrypt,13,2,8,4\033[0m \033[36m--ah-alg scrypt,13,2,8,4,32\033[0m
use scrypt with cost 2**13, 2 iterations, blocksize 8, 4 threads use scrypt with cost 2**13, 2 iterations, blocksize 8, 4 threads,
and allow using up to 32 MiB RAM (ram=cost*blksz roughly)
\033[36m--ah-alg sha2\033[0m # which is the same as: \033[36m--ah-alg sha2\033[0m # which is the same as:
\033[36m--ah-alg sha2,424242\033[0m \033[36m--ah-alg sha2,424242\033[0m
@@ -1201,6 +1206,8 @@ def add_hooks(ap):
ap2.add_argument("--xbu", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file upload starts") ap2.add_argument("--xbu", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file upload starts")
ap2.add_argument("--xau", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file upload finishes") ap2.add_argument("--xau", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file upload finishes")
ap2.add_argument("--xiu", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after all uploads finish and volume is idle") ap2.add_argument("--xiu", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after all uploads finish and volume is idle")
ap2.add_argument("--xbc", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file copy")
ap2.add_argument("--xac", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file copy")
ap2.add_argument("--xbr", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file move/rename") ap2.add_argument("--xbr", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file move/rename")
ap2.add_argument("--xar", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file move/rename") ap2.add_argument("--xar", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file move/rename")
ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file delete") ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file delete")
@@ -1233,6 +1240,7 @@ def add_optouts(ap):
ap2.add_argument("--no-dav", action="store_true", help="disable webdav support") ap2.add_argument("--no-dav", action="store_true", help="disable webdav support")
ap2.add_argument("--no-del", action="store_true", help="disable delete operations") ap2.add_argument("--no-del", action="store_true", help="disable delete operations")
ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations") ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations")
ap2.add_argument("--no-cp", action="store_true", help="disable copy operations")
ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m in <title>") ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m in <title>")
ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI") ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI")
ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI") ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI")
@@ -1316,9 +1324,12 @@ def add_admin(ap):
ap2.add_argument("--no-reload", action="store_true", help="disable ?reload=cfg (reload users/volumes/volflags from config file)") ap2.add_argument("--no-reload", action="store_true", help="disable ?reload=cfg (reload users/volumes/volflags from config file)")
ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)") ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)")
ap2.add_argument("--no-stack", action="store_true", help="disable ?stack (list all stacks)") ap2.add_argument("--no-stack", action="store_true", help="disable ?stack (list all stacks)")
ap2.add_argument("--dl-list", metavar="LVL", type=int, default=2, help="who can see active downloads in the controlpanel? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=everyone")
def add_thumbnail(ap): def add_thumbnail(ap):
th_ram = (RAM_AVAIL or RAM_TOTAL or 9) * 0.6
th_ram = int(max(min(th_ram, 6), 1) * 10) / 10
ap2 = ap.add_argument_group('thumbnail options') ap2 = ap.add_argument_group('thumbnail options')
ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails (volflag=dthumb)") ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails (volflag=dthumb)")
ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails (volflag=dvthumb)") ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails (volflag=dvthumb)")
@@ -1326,7 +1337,7 @@ def add_thumbnail(ap):
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)") ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)")
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails") ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails")
ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="conversion timeout in seconds (volflag=convt)") ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="conversion timeout in seconds (volflag=convt)")
ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=6.0, help="max memory usage (GiB) permitted by thumbnailer; not very accurate") ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=th_ram, help="max memory usage (GiB) permitted by thumbnailer; not very accurate")
ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)") ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)")
ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)") ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)")
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference") ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference")
@@ -1341,12 +1352,12 @@ def add_thumbnail(ap):
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
# https://github.com/libvips/libvips # https://github.com/libvips/libvips
# ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:' # ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,dcx,dds,dib,emf,eps,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow") ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips") ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips")
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,dds,dib,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg") ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg") ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg") ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz", help="audio formats to decompress before passing to ffmpeg") ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz", help="audio/image formats to decompress before passing to ffmpeg")
def add_transcoding(ap): def add_transcoding(ap):

View File

@@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (1, 15, 10) VERSION = (1, 16, 1)
CODENAME = "fill the drives" CODENAME = "COPYparty"
BUILD_DT = (2024, 10, 26) BUILD_DT = (2024, 11, 15)
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

@@ -367,18 +367,19 @@ class VFS(object):
self.ahtml: dict[str, list[str]] = {} self.ahtml: dict[str, list[str]] = {}
self.aadmin: dict[str, list[str]] = {} self.aadmin: dict[str, list[str]] = {}
self.adot: dict[str, list[str]] = {} self.adot: dict[str, list[str]] = {}
self.all_vols: dict[str, VFS] = {}
if realpath: if realpath:
rp = realpath + ("" if realpath.endswith(os.sep) else os.sep) rp = realpath + ("" if realpath.endswith(os.sep) else os.sep)
vp = vpath + ("/" if vpath else "") vp = vpath + ("/" if vpath else "")
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
self.all_vols = {vpath: self} # flattened recursive self.all_vols = {vpath: self} # flattened recursive
self.all_nodes = {vpath: self} # also jumpvols
self.all_aps = [(rp, self)] self.all_aps = [(rp, self)]
self.all_vps = [(vp, self)] self.all_vps = [(vp, self)]
else: else:
self.histpath = "" self.histpath = ""
self.all_vols = {} self.all_vols = {}
self.all_nodes = {}
self.all_aps = [] self.all_aps = []
self.all_vps = [] self.all_vps = []
@@ -396,9 +397,11 @@ class VFS(object):
def get_all_vols( def get_all_vols(
self, self,
vols: dict[str, "VFS"], vols: dict[str, "VFS"],
nodes: dict[str, "VFS"],
aps: list[tuple[str, "VFS"]], aps: list[tuple[str, "VFS"]],
vps: list[tuple[str, "VFS"]], vps: list[tuple[str, "VFS"]],
) -> None: ) -> None:
nodes[self.vpath] = self
if self.realpath: if self.realpath:
vols[self.vpath] = self vols[self.vpath] = self
rp = self.realpath rp = self.realpath
@@ -408,7 +411,7 @@ class VFS(object):
vps.append((vp, self)) vps.append((vp, self))
for v in self.nodes.values(): for v in self.nodes.values():
v.get_all_vols(vols, aps, vps) v.get_all_vols(vols, nodes, aps, vps)
def add(self, src: str, dst: str) -> "VFS": def add(self, src: str, dst: str) -> "VFS":
"""get existing, or add new path to the vfs""" """get existing, or add new path to the vfs"""
@@ -591,10 +594,11 @@ class VFS(object):
scandir: bool, scandir: bool,
permsets: list[list[bool]], permsets: list[list[bool]],
lstat: bool = False, lstat: bool = False,
throw: bool = False,
) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]: ) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]:
"""replaces _ls for certain shares (single-file, or file selection)""" """replaces _ls for certain shares (single-file, or file selection)"""
vn, rem = self.shr_src # type: ignore vn, rem = self.shr_src # type: ignore
abspath, real, _ = vn.ls(rem, "\n", scandir, permsets, lstat) abspath, real, _ = vn.ls(rem, "\n", scandir, permsets, lstat, throw)
real = [x for x in real if os.path.basename(x[0]) in self.shr_files] real = [x for x in real if os.path.basename(x[0]) in self.shr_files]
return abspath, real, {} return abspath, real, {}
@@ -605,11 +609,12 @@ class VFS(object):
scandir: bool, scandir: bool,
permsets: list[list[bool]], permsets: list[list[bool]],
lstat: bool = False, lstat: bool = False,
throw: bool = False,
) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]: ) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]:
"""return user-readable [fsdir,real,virt] items at vpath""" """return user-readable [fsdir,real,virt] items at vpath"""
virt_vis = {} # nodes readable by user virt_vis = {} # nodes readable by user
abspath = self.canonical(rem) abspath = self.canonical(rem)
real = list(statdir(self.log, scandir, lstat, abspath)) real = list(statdir(self.log, scandir, lstat, abspath, throw))
real.sort() real.sort()
if not rem: if not rem:
# no vfs nodes in the list of real inodes # no vfs nodes in the list of real inodes
@@ -671,6 +676,10 @@ class VFS(object):
""" """
recursively yields from ./rem; recursively yields from ./rem;
rel is a unix-style user-defined vpath (not vfs-related) rel is a unix-style user-defined vpath (not vfs-related)
NOTE: don't invoke this function from a dbv; subvols are only
descended into if rem is blank due to the _ls `if not rem:`
which intention is to prevent unintended access to subvols
""" """
fsroot, vfs_ls, vfs_virt = self.ls(rem, uname, scandir, permsets, lstat=lstat) fsroot, vfs_ls, vfs_virt = self.ls(rem, uname, scandir, permsets, lstat=lstat)
@@ -911,7 +920,7 @@ class AuthSrv(object):
self._reload() self._reload()
return True return True
broker.ask("_reload_blocking", False).get() broker.ask("reload", False, True).get()
return True return True
def _map_volume_idp( def _map_volume_idp(
@@ -1381,7 +1390,7 @@ class AuthSrv(object):
flags[name] = True flags[name] = True
return return
zs = "mtp on403 on404 xbu xau xiu xbr xar xbd xad xm xban" zs = "mtp on403 on404 xbu xau xiu xbc xac xbr xar xbd xad xm xban"
if name not in zs.split(): if name not in zs.split():
if value is True: if value is True:
t = "└─add volflag [{}] = {} ({})" t = "└─add volflag [{}] = {} ({})"
@@ -1529,10 +1538,11 @@ class AuthSrv(object):
assert vfs # type: ignore assert vfs # type: ignore
vfs.all_vols = {} vfs.all_vols = {}
vfs.all_nodes = {}
vfs.all_aps = [] vfs.all_aps = []
vfs.all_vps = [] vfs.all_vps = []
vfs.get_all_vols(vfs.all_vols, vfs.all_aps, vfs.all_vps) vfs.get_all_vols(vfs.all_vols, vfs.all_nodes, vfs.all_aps, vfs.all_vps)
for vol in vfs.all_vols.values(): for vol in vfs.all_nodes.values():
vol.all_aps.sort(key=lambda x: len(x[0]), reverse=True) vol.all_aps.sort(key=lambda x: len(x[0]), reverse=True)
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True) vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
vol.root = vfs vol.root = vfs
@@ -1583,7 +1593,7 @@ class AuthSrv(object):
vfs.nodes[shr] = vfs.all_vols[shr] = shv vfs.nodes[shr] = vfs.all_vols[shr] = shv
for vol in shv.nodes.values(): for vol in shv.nodes.values():
vfs.all_vols[vol.vpath] = vol vfs.all_vols[vol.vpath] = vfs.all_nodes[vol.vpath] = vol
vol.get_dbv = vol._get_share_src vol.get_dbv = vol._get_share_src
vol.ls = vol._ls_nope vol.ls = vol._ls_nope
@@ -1726,7 +1736,19 @@ class AuthSrv(object):
self.log("\n\n".join(ta) + "\n", c=3) self.log("\n\n".join(ta) + "\n", c=3)
vfs.histtab = {zv.realpath: zv.histpath for zv in vfs.all_vols.values()} rhisttab = {}
vfs.histtab = {}
for zv in vfs.all_vols.values():
histp = zv.histpath
is_shr = shr and zv.vpath.split("/")[0] == shr
if histp and not is_shr and histp in rhisttab:
zv2 = rhisttab[histp]
t = "invalid config; multiple volumes share the same histpath (database location):\n histpath: %s\n volume 1: /%s [%s]\n volume 2: %s [%s]"
t = t % (histp, zv2.vpath, zv2.realpath, zv.vpath, zv.realpath)
self.log(t, 1)
raise Exception(t)
rhisttab[histp] = zv
vfs.histtab[zv.realpath] = histp
for vol in vfs.all_vols.values(): for vol in vfs.all_vols.values():
lim = Lim(self.log_func) lim = Lim(self.log_func)
@@ -1785,12 +1807,12 @@ class AuthSrv(object):
vol.lim = lim vol.lim = lim
if self.args.no_robots: if self.args.no_robots:
for vol in vfs.all_vols.values(): for vol in vfs.all_nodes.values():
# volflag "robots" overrides global "norobots", allowing indexing by search engines for this vol # volflag "robots" overrides global "norobots", allowing indexing by search engines for this vol
if not vol.flags.get("robots"): if not vol.flags.get("robots"):
vol.flags["norobots"] = True vol.flags["norobots"] = True
for vol in vfs.all_vols.values(): for vol in vfs.all_nodes.values():
if self.args.no_vthumb: if self.args.no_vthumb:
vol.flags["dvthumb"] = True vol.flags["dvthumb"] = True
if self.args.no_athumb: if self.args.no_athumb:
@@ -1802,7 +1824,7 @@ class AuthSrv(object):
vol.flags["dithumb"] = True vol.flags["dithumb"] = True
have_fk = False have_fk = False
for vol in vfs.all_vols.values(): for vol in vfs.all_nodes.values():
fk = vol.flags.get("fk") fk = vol.flags.get("fk")
fka = vol.flags.get("fka") fka = vol.flags.get("fka")
if fka and not fk: if fka and not fk:
@@ -1834,7 +1856,7 @@ class AuthSrv(object):
zs = os.path.join(E.cfg, "fk-salt.txt") zs = os.path.join(E.cfg, "fk-salt.txt")
self.log(t % (fk_len, 16, zs), 3) self.log(t % (fk_len, 16, zs), 3)
for vol in vfs.all_vols.values(): for vol in vfs.all_nodes.values():
if "pk" in vol.flags and "gz" not in vol.flags and "xz" not in vol.flags: if "pk" in vol.flags and "gz" not in vol.flags and "xz" not in vol.flags:
vol.flags["gz"] = False # def.pk vol.flags["gz"] = False # def.pk
@@ -1845,7 +1867,7 @@ class AuthSrv(object):
all_mte = {} all_mte = {}
errors = False errors = False
for vol in vfs.all_vols.values(): for vol in vfs.all_nodes.values():
if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa: if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa:
vol.flags["e2ds"] = True vol.flags["e2ds"] = True
@@ -1936,7 +1958,7 @@ class AuthSrv(object):
vol.flags[k] = odfusion(getattr(self.args, k), vol.flags[k]) vol.flags[k] = odfusion(getattr(self.args, k), vol.flags[k])
# append additive args from argv to volflags # append additive args from argv to volflags
hooks = "xbu xau xiu xbr xar xbd xad xm xban".split() hooks = "xbu xau xiu xbc xac xbr xar xbd xad xm xban".split()
for name in "mtp on404 on403".split() + hooks: for name in "mtp on404 on403".split() + hooks:
self._read_volflag(vol.flags, name, getattr(self.args, name), True) self._read_volflag(vol.flags, name, getattr(self.args, name), True)
@@ -2063,7 +2085,7 @@ class AuthSrv(object):
errors = True errors = True
have_daw = False have_daw = False
for vol in vfs.all_vols.values(): for vol in vfs.all_nodes.values():
daw = vol.flags.get("daw") or self.args.daw daw = vol.flags.get("daw") or self.args.daw
if daw: if daw:
vol.flags["daw"] = True vol.flags["daw"] = True
@@ -2078,13 +2100,12 @@ class AuthSrv(object):
self.log("--smb can only be used when --ah-alg is none", 1) self.log("--smb can only be used when --ah-alg is none", 1)
errors = True errors = True
for vol in vfs.all_vols.values(): for vol in vfs.all_nodes.values():
for k in list(vol.flags.keys()): for k in list(vol.flags.keys()):
if re.match("^-[^-]+$", k): if re.match("^-[^-]+$", k):
vol.flags.pop(k[1:], None) vol.flags.pop(k[1:], None)
vol.flags.pop(k) vol.flags.pop(k)
for vol in vfs.all_vols.values():
if vol.flags.get("dots"): if vol.flags.get("dots"):
for name in vol.axs.uread: for name in vol.axs.uread:
vol.axs.udot.add(name) vol.axs.udot.add(name)
@@ -2226,6 +2247,11 @@ class AuthSrv(object):
for x, y in vfs.all_vols.items() for x, y in vfs.all_vols.items()
if x != shr and not x.startswith(shrs) if x != shr and not x.startswith(shrs)
} }
vfs.all_nodes = {
x: y
for x, y in vfs.all_nodes.items()
if x != shr and not x.startswith(shrs)
}
assert db and cur and cur2 and shv # type: ignore assert db and cur and cur2 and shv # type: ignore
for row in cur.execute("select * from sh"): for row in cur.execute("select * from sh"):
@@ -2387,7 +2413,7 @@ class AuthSrv(object):
self._reload() self._reload()
return True, "new password OK" return True, "new password OK"
broker.ask("_reload_blocking", False, False).get() broker.ask("reload", False, False).get()
return True, "new password OK" return True, "new password OK"
def setup_chpw(self, acct: dict[str, str]) -> None: def setup_chpw(self, acct: dict[str, str]) -> None:
@@ -2639,7 +2665,7 @@ class AuthSrv(object):
] ]
csv = set("i p th_covers zm_on zm_off zs_on zs_off".split()) csv = set("i p th_covers zm_on zm_off zs_on zs_off".split())
zs = "c ihead ohead mtm mtp on403 on404 xad xar xau xiu xban xbd xbr xbu xm" zs = "c ihead ohead mtm mtp on403 on404 xac xad xar xau xiu xban xbc xbd xbr xbu xm"
lst = set(zs.split()) lst = set(zs.split())
askip = set("a v c vc cgen exp_lg exp_md theme".split()) askip = set("a v c vc cgen exp_lg exp_md theme".split())
fskip = set("exp_lg exp_md mv_re_r mv_re_t rm_re_r rm_re_t".split()) fskip = set("exp_lg exp_md mv_re_r mv_re_t rm_re_r rm_re_t".split())

View File

@@ -43,6 +43,9 @@ class BrokerMp(object):
self.procs = [] self.procs = []
self.mutex = threading.Lock() self.mutex = threading.Lock()
self.retpend: dict[int, Any] = {}
self.retpend_mutex = threading.Lock()
self.num_workers = self.args.j or CORES self.num_workers = self.args.j or CORES
self.log("broker", "booting {} subprocesses".format(self.num_workers)) self.log("broker", "booting {} subprocesses".format(self.num_workers))
for n in range(1, self.num_workers + 1): for n in range(1, self.num_workers + 1):
@@ -54,6 +57,8 @@ class BrokerMp(object):
self.procs.append(proc) self.procs.append(proc)
proc.start() proc.start()
Daemon(self.periodic, "mp-periodic")
def shutdown(self) -> None: def shutdown(self) -> None:
self.log("broker", "shutting down") self.log("broker", "shutting down")
for n, proc in enumerate(self.procs): for n, proc in enumerate(self.procs):
@@ -90,8 +95,10 @@ class BrokerMp(object):
self.log(*args) self.log(*args)
elif dest == "retq": elif dest == "retq":
# response from previous ipc call with self.retpend_mutex:
raise Exception("invalid broker_mp usage") retq = self.retpend.pop(retq_id)
retq.put(args[0])
else: else:
# new ipc invoking managed service in hub # new ipc invoking managed service in hub
@@ -109,7 +116,6 @@ class BrokerMp(object):
proc.q_pend.put((retq_id, "retq", rv)) proc.q_pend.put((retq_id, "retq", rv))
def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]: def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]:
# new non-ipc invoking managed service in hub # new non-ipc invoking managed service in hub
obj = self.hub obj = self.hub
for node in dest.split("."): for node in dest.split("."):
@@ -121,17 +127,30 @@ class BrokerMp(object):
retq.put(rv) retq.put(rv)
return retq return retq
def wask(self, dest: str, *args: Any) -> list[Union[ExceptionalQueue, NotExQueue]]:
# call from hub to workers
ret = []
for p in self.procs:
retq = ExceptionalQueue(1)
retq_id = id(retq)
with self.retpend_mutex:
self.retpend[retq_id] = retq
p.q_pend.put((retq_id, dest, list(args)))
ret.append(retq)
return ret
def say(self, dest: str, *args: Any) -> None: def say(self, dest: str, *args: Any) -> None:
""" """
send message to non-hub component in other process, send message to non-hub component in other process,
returns a Queue object which eventually contains the response if want_retval returns a Queue object which eventually contains the response if want_retval
(not-impl here since nothing uses it yet) (not-impl here since nothing uses it yet)
""" """
if dest == "listen": if dest == "httpsrv.listen":
for p in self.procs: for p in self.procs:
p.q_pend.put((0, dest, [args[0], len(self.procs)])) p.q_pend.put((0, dest, [args[0], len(self.procs)]))
elif dest == "set_netdevs": elif dest == "httpsrv.set_netdevs":
for p in self.procs: for p in self.procs:
p.q_pend.put((0, dest, list(args))) p.q_pend.put((0, dest, list(args)))
@@ -140,3 +159,19 @@ class BrokerMp(object):
else: else:
raise Exception("what is " + str(dest)) raise Exception("what is " + str(dest))
def periodic(self) -> None:
while True:
time.sleep(1)
tdli = {}
tdls = {}
qs = self.wask("httpsrv.read_dls")
for q in qs:
qr = q.get()
dli, dls = qr
tdli.update(dli)
tdls.update(dls)
tdl = (tdli, tdls)
for p in self.procs:
p.q_pend.put((0, "httpsrv.write_dls", tdl))

View File

@@ -82,37 +82,38 @@ class MpWorker(BrokerCli):
while True: while True:
retq_id, dest, args = self.q_pend.get() retq_id, dest, args = self.q_pend.get()
# self.logw("work: [{}]".format(d[0])) if dest == "retq":
# response from previous ipc call
with self.retpend_mutex:
retq = self.retpend.pop(retq_id)
retq.put(args)
continue
if dest == "shutdown": if dest == "shutdown":
self.httpsrv.shutdown() self.httpsrv.shutdown()
self.logw("ok bye") self.logw("ok bye")
sys.exit(0) sys.exit(0)
return return
elif dest == "reload": if dest == "reload":
self.logw("mpw.asrv reloading") self.logw("mpw.asrv reloading")
self.asrv.reload() self.asrv.reload()
self.logw("mpw.asrv reloaded") self.logw("mpw.asrv reloaded")
continue
elif dest == "reload_sessions": if dest == "reload_sessions":
with self.asrv.mutex: with self.asrv.mutex:
self.asrv.load_sessions() self.asrv.load_sessions()
continue
elif dest == "listen": obj = self
self.httpsrv.listen(args[0], args[1]) for node in dest.split("."):
obj = getattr(obj, node)
elif dest == "set_netdevs": rv = obj(*args) # type: ignore
self.httpsrv.set_netdevs(args[0]) if retq_id:
self.say("retq", rv, retq_id=retq_id)
elif dest == "retq":
# response from previous ipc call
with self.retpend_mutex:
retq = self.retpend.pop(retq_id)
retq.put(args)
else:
raise Exception("what is " + str(dest))
def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]: def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]:
retq = ExceptionalQueue(1) retq = ExceptionalQueue(1)
@@ -123,5 +124,5 @@ class MpWorker(BrokerCli):
self.q_yield.put((retq_id, dest, list(args))) self.q_yield.put((retq_id, dest, list(args)))
return retq return retq
def say(self, dest: str, *args: Any) -> None: def say(self, dest: str, *args: Any, retq_id=0) -> None:
self.q_yield.put((0, dest, list(args))) self.q_yield.put((retq_id, dest, list(args)))

View File

@@ -53,11 +53,11 @@ class BrokerThr(BrokerCli):
return NotExQueue(obj(*args)) # type: ignore return NotExQueue(obj(*args)) # type: ignore
def say(self, dest: str, *args: Any) -> None: def say(self, dest: str, *args: Any) -> None:
if dest == "listen": if dest == "httpsrv.listen":
self.httpsrv.listen(args[0], 1) self.httpsrv.listen(args[0], 1)
return return
if dest == "set_netdevs": if dest == "httpsrv.set_netdevs":
self.httpsrv.set_netdevs(args[0]) self.httpsrv.set_netdevs(args[0])
return return

View File

@@ -103,10 +103,12 @@ def vf_cmap() -> dict[str, str]:
"mte", "mte",
"mth", "mth",
"mtp", "mtp",
"xac",
"xad", "xad",
"xar", "xar",
"xau", "xau",
"xban", "xban",
"xbc",
"xbd", "xbd",
"xbr", "xbr",
"xbu", "xbu",
@@ -212,6 +214,8 @@ flagcats = {
"xbu=CMD": "execute CMD before a file upload starts", "xbu=CMD": "execute CMD before a file upload starts",
"xau=CMD": "execute CMD after a file upload finishes", "xau=CMD": "execute CMD after a file upload finishes",
"xiu=CMD": "execute CMD after all uploads finish and volume is idle", "xiu=CMD": "execute CMD after all uploads finish and volume is idle",
"xbc=CMD": "execute CMD before a file copy",
"xac=CMD": "execute CMD after a file copy",
"xbr=CMD": "execute CMD before a file rename/move", "xbr=CMD": "execute CMD before a file rename/move",
"xar=CMD": "execute CMD after a file rename/move", "xar=CMD": "execute CMD after a file rename/move",
"xbd=CMD": "execute CMD before a file delete", "xbd=CMD": "execute CMD before a file delete",

View File

@@ -296,6 +296,7 @@ class FtpFs(AbstractedFS):
self.uname, self.uname,
not self.args.no_scandir, not self.args.no_scandir,
[[True, False], [False, True]], [[True, False], [False, True]],
throw=True,
) )
vfs_ls = [x[0] for x in vfs_ls1] vfs_ls = [x[0] for x in vfs_ls1]
vfs_ls.extend(vfs_virt.keys()) vfs_ls.extend(vfs_virt.keys())

View File

@@ -186,6 +186,7 @@ class HttpCli(object):
self.rem = " " self.rem = " "
self.vpath = " " self.vpath = " "
self.vpaths = " " self.vpaths = " "
self.dl_id = ""
self.gctx = " " # additional context for garda self.gctx = " " # additional context for garda
self.trailing_slash = True self.trailing_slash = True
self.uname = " " self.uname = " "
@@ -637,7 +638,7 @@ class HttpCli(object):
avn.can_access("", self.uname) if avn else [False] * 8 avn.can_access("", self.uname) if avn else [False] * 8
) )
self.avn = avn self.avn = avn
self.vn = vn self.vn = vn # note: do not dbv due to walk/zipgen
self.rem = rem self.rem = rem
self.s.settimeout(self.args.s_tbody or None) self.s.settimeout(self.args.s_tbody or None)
@@ -726,6 +727,11 @@ class HttpCli(object):
except Pebkac: except Pebkac:
return False return False
finally:
if self.dl_id:
self.conn.hsrv.dli.pop(self.dl_id, None)
self.conn.hsrv.dls.pop(self.dl_id, None)
def dip(self) -> str: def dip(self) -> str:
if self.args.plain_ip: if self.args.plain_ip:
return self.ip.replace(":", ".") return self.ip.replace(":", ".")
@@ -1196,6 +1202,9 @@ class HttpCli(object):
if "move" in self.uparam: if "move" in self.uparam:
return self.handle_mv() return self.handle_mv()
if "copy" in self.uparam:
return self.handle_cp()
if not self.vpath and self.ouparam: if not self.vpath and self.ouparam:
if "reload" in self.uparam: if "reload" in self.uparam:
return self.handle_reload() return self.handle_reload()
@@ -1215,6 +1224,9 @@ class HttpCli(object):
if "shares" in self.uparam: if "shares" in self.uparam:
return self.tx_shares() return self.tx_shares()
if "dls" in self.uparam:
return self.tx_dls()
if "h" in self.uparam: if "h" in self.uparam:
return self.tx_mounts() return self.tx_mounts()
@@ -1450,6 +1462,7 @@ class HttpCli(object):
not self.args.no_scandir, not self.args.no_scandir,
[[True, False]], [[True, False]],
lstat="davrt" not in vn.flags, lstat="davrt" not in vn.flags,
throw=True,
) )
if not self.can_read: if not self.can_read:
vfs_ls = [] vfs_ls = []
@@ -1790,6 +1803,9 @@ class HttpCli(object):
if "move" in self.uparam: if "move" in self.uparam:
return self.handle_mv() return self.handle_mv()
if "copy" in self.uparam:
return self.handle_cp()
if "delete" in self.uparam: if "delete" in self.uparam:
return self.handle_rm([]) return self.handle_rm([])
@@ -3683,6 +3699,8 @@ class HttpCli(object):
self.args.s_wr_sz, self.args.s_wr_sz,
self.args.s_wr_slp, self.args.s_wr_slp,
not self.args.no_poll, not self.args.no_poll,
{},
"",
) )
res.close() res.close()
@@ -3729,6 +3747,7 @@ class HttpCli(object):
editions: dict[str, tuple[str, int]] = {} editions: dict[str, tuple[str, int]] = {}
for ext in ("", ".gz"): for ext in ("", ".gz"):
if ptop is not None: if ptop is not None:
assert job and ap_data # type: ignore # !rm
sz = job["size"] sz = job["size"]
file_ts = job["lmod"] file_ts = job["lmod"]
editions["plain"] = (ap_data, sz) editions["plain"] = (ap_data, sz)
@@ -3897,7 +3916,21 @@ class HttpCli(object):
self.send_headers(length=upper - lower, status=status, mime=mime) self.send_headers(length=upper - lower, status=status, mime=mime)
return True return True
dls = self.conn.hsrv.dls
if upper - lower > 0x400000: # 4m
now = time.time()
self.dl_id = "%s:%s" % (self.ip, self.addr[1])
dls[self.dl_id] = (now, 0)
self.conn.hsrv.dli[self.dl_id] = (
now,
upper - lower,
self.vn,
self.vpath,
self.uname,
)
if ptop is not None: if ptop is not None:
assert job and ap_data # type: ignore # !rm
return self.tx_pipe( return self.tx_pipe(
ptop, req_path, ap_data, job, lower, upper, status, mime, logmsg ptop, req_path, ap_data, job, lower, upper, status, mime, logmsg
) )
@@ -3916,6 +3949,8 @@ class HttpCli(object):
self.args.s_wr_sz, self.args.s_wr_sz,
self.args.s_wr_slp, self.args.s_wr_slp,
not self.args.no_poll, not self.args.no_poll,
dls,
self.dl_id,
) )
if remains > 0: if remains > 0:
@@ -4066,6 +4101,8 @@ class HttpCli(object):
wr_sz, wr_sz,
wr_slp, wr_slp,
not self.args.no_poll, not self.args.no_poll,
self.conn.hsrv.dls,
self.dl_id,
) )
spd = self._spd((upper - lower) - remains) spd = self._spd((upper - lower) - remains)
@@ -4151,6 +4188,18 @@ class HttpCli(object):
self.log("transcoding to [{}]".format(cfmt)) self.log("transcoding to [{}]".format(cfmt))
fgen = gfilter(fgen, self.thumbcli, self.uname, vpath, cfmt) fgen = gfilter(fgen, self.thumbcli, self.uname, vpath, cfmt)
now = time.time()
self.dl_id = "%s:%s" % (self.ip, self.addr[1])
self.conn.hsrv.dli[self.dl_id] = (
now,
0,
self.vn,
"%s :%s" % (self.vpath, ext),
self.uname,
)
dls = self.conn.hsrv.dls
dls[self.dl_id] = (time.time(), 0)
bgen = packer( bgen = packer(
self.log, self.log,
self.asrv, self.asrv,
@@ -4159,6 +4208,7 @@ class HttpCli(object):
pre_crc="crc" in uarg, pre_crc="crc" in uarg,
cmp=uarg if cancmp or uarg == "pax" else "", cmp=uarg if cancmp or uarg == "pax" else "",
) )
n = 0
bsent = 0 bsent = 0
for buf in bgen.gen(): for buf in bgen.gen():
if not buf: if not buf:
@@ -4172,6 +4222,11 @@ class HttpCli(object):
bgen.stop() bgen.stop()
break break
n += 1
if n >= 4:
n = 0
dls[self.dl_id] = (time.time(), bsent)
spd = self._spd(bsent) spd = self._spd(bsent)
self.log("{}, {}".format(logmsg, spd)) self.log("{}, {}".format(logmsg, spd))
return True return True
@@ -4429,6 +4484,32 @@ class HttpCli(object):
assert vstate.items and vs # type: ignore # !rm assert vstate.items and vs # type: ignore # !rm
dls = dl_list = []
if self.conn.hsrv.tdls:
zi = self.args.dl_list
if zi == 2 or (zi == 1 and self.avol):
dl_list = self.get_dls()
for t0, t1, sent, sz, vp, dl_id, uname in dl_list:
rem = sz - sent
td = max(0.1, now - t0)
rd, fn = vsplit(vp)
if not rd:
rd = "/"
erd = quotep(rd)
rds = rd.replace("/", " / ")
spd = humansize(sent / td, True) + "/s"
hsent = humansize(sent, True)
idle = s2hms(now - t1, True)
usr = "%s @%s" % (dl_id, uname) if dl_id else uname
if sz and sent and td:
eta = s2hms((sz - sent) / (sent / td), True)
perc = int(100 * sent / sz)
else:
eta = perc = "--"
fn = html_escape(fn) if fn else self.conn.hsrv.iiam
dls.append((perc, hsent, spd, eta, idle, usr, erd, rds, fn))
fmt = self.uparam.get("ls", "") fmt = self.uparam.get("ls", "")
if not fmt and (self.ua.startswith("curl/") or self.ua.startswith("fetch")): if not fmt and (self.ua.startswith("curl/") or self.ua.startswith("fetch")):
fmt = "v" fmt = "v"
@@ -4450,6 +4531,12 @@ class HttpCli(object):
txt += "\n%s" % (", ".join((str(x) for x in zt)),) txt += "\n%s" % (", ".join((str(x) for x in zt)),)
txt += "\n" txt += "\n"
if dls:
txt += "\n\nactive downloads:"
for zt in dls:
txt += "\n%s" % (", ".join((str(x) for x in zt)),)
txt += "\n"
if rvol: if rvol:
txt += "\nyou can browse:" txt += "\nyou can browse:"
for v in rvol: for v in rvol:
@@ -4473,6 +4560,7 @@ class HttpCli(object):
avol=avol, avol=avol,
in_shr=self.args.shr and self.vpath.startswith(self.args.shr1), in_shr=self.args.shr and self.vpath.startswith(self.args.shr1),
vstate=vstate, vstate=vstate,
dls=dls,
ups=ups, ups=ups,
scanning=vs["scanning"], scanning=vs["scanning"],
hashq=vs["hashq"], hashq=vs["hashq"],
@@ -4535,8 +4623,14 @@ class HttpCli(object):
t = t.format(self.args.SR) t = t.format(self.args.SR)
qv = quotep(self.vpaths) + self.ourlq() qv = quotep(self.vpaths) + self.ourlq()
in_shr = self.args.shr and self.vpath.startswith(self.args.shr1) html = self.j2s(
html = self.j2s("splash", this=self, qvpath=qv, in_shr=in_shr, msg=t) "splash",
this=self,
qvpath=qv,
msg=t,
in_shr=self.args.shr and self.vpath.startswith(self.args.shr1),
ahttps="" if self.is_https else "https://" + self.host + self.req,
)
self.reply(html.encode("utf-8"), status=rc) self.reply(html.encode("utf-8"), status=rc)
return True return True
@@ -4584,7 +4678,7 @@ class HttpCli(object):
if self.args.no_reload: if self.args.no_reload:
raise Pebkac(403, "the reload feature is disabled in server config") raise Pebkac(403, "the reload feature is disabled in server config")
x = self.conn.hsrv.broker.ask("reload") x = self.conn.hsrv.broker.ask("reload", True, True)
return self.redirect("", "?h", x.get(), "return to", False) return self.redirect("", "?h", x.get(), "return to", False)
def tx_stack(self) -> bool: def tx_stack(self) -> bool:
@@ -4687,6 +4781,40 @@ class HttpCli(object):
ret["a"] = dirs ret["a"] = dirs
return ret return ret
def get_dls(self) -> list[list[Any]]:
ret = []
dls = self.conn.hsrv.tdls
for dl_id, (t0, sz, vn, vp, uname) in self.conn.hsrv.tdli.items():
t1, sent = dls[dl_id]
if sent > 0x100000: # 1m; buffers 2~4
sent -= 0x100000
if self.uname not in vn.axs.uread:
vp = ""
elif self.uname not in vn.axs.udot and (vp.startswith(".") or "/." in vp):
vp = ""
if self.uname not in vn.axs.uadmin:
dl_id = uname = ""
ret.append([t0, t1, sent, sz, vp, dl_id, uname])
return ret
def tx_dls(self) -> bool:
ret = [
{
"t0": x[0],
"t1": x[1],
"sent": x[2],
"size": x[3],
"path": x[4],
"conn": x[5],
"uname": x[6],
}
for x in self.get_dls()
]
zs = json.dumps(ret, separators=(",\n", ": "))
self.reply(zs.encode("utf-8", "replace"), mime="application/json")
return True
def tx_ups(self) -> bool: def tx_ups(self) -> bool:
idx = self.conn.get_u2idx() idx = self.conn.get_u2idx()
if not idx or not hasattr(idx, "p_end"): if not idx or not hasattr(idx, "p_end"):
@@ -4872,7 +5000,7 @@ class HttpCli(object):
cur.connection.commit() cur.connection.commit()
if reload: if reload:
self.conn.hsrv.broker.ask("_reload_blocking", False, False).get() self.conn.hsrv.broker.ask("reload", False, False).get()
self.conn.hsrv.broker.ask("up2k.wake_rescanner").get() self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
self.redirect(self.args.SRS + "?shares") self.redirect(self.args.SRS + "?shares")
@@ -4963,7 +5091,7 @@ class HttpCli(object):
cur.execute(q, (skey, fn)) cur.execute(q, (skey, fn))
cur.connection.commit() cur.connection.commit()
self.conn.hsrv.broker.ask("_reload_blocking", False, False).get() self.conn.hsrv.broker.ask("reload", False, False).get()
self.conn.hsrv.broker.ask("up2k.wake_rescanner").get() self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
fn = quotep(fns[0]) if len(fns) == 1 else "" fn = quotep(fns[0]) if len(fns) == 1 else ""
@@ -5014,16 +5142,39 @@ class HttpCli(object):
return self._mv(self.vpath, dst.lstrip("/")) return self._mv(self.vpath, dst.lstrip("/"))
def _mv(self, vsrc: str, vdst: str) -> bool: def _mv(self, vsrc: str, vdst: str) -> bool:
if not self.can_move:
raise Pebkac(403, "not allowed for user " + self.uname)
if self.args.no_mv: if self.args.no_mv:
raise Pebkac(403, "the rename/move feature is disabled in server config") raise Pebkac(403, "the rename/move feature is disabled in server config")
self.asrv.vfs.get(vsrc, self.uname, True, False, True)
self.asrv.vfs.get(vdst, self.uname, False, True)
x = self.conn.hsrv.broker.ask("up2k.handle_mv", self.uname, self.ip, vsrc, vdst) x = self.conn.hsrv.broker.ask("up2k.handle_mv", self.uname, self.ip, vsrc, vdst)
self.loud_reply(x.get(), status=201) self.loud_reply(x.get(), status=201)
return True return True
def handle_cp(self) -> bool:
# full path of new loc (incl filename)
dst = self.uparam.get("copy")
if self.is_vproxied and dst and dst.startswith(self.args.SR):
dst = dst[len(self.args.RS) :]
if not dst:
raise Pebkac(400, "need dst vpath")
return self._cp(self.vpath, dst.lstrip("/"))
def _cp(self, vsrc: str, vdst: str) -> bool:
if self.args.no_cp:
raise Pebkac(403, "the copy feature is disabled in server config")
self.asrv.vfs.get(vsrc, self.uname, True, False)
self.asrv.vfs.get(vdst, self.uname, False, True)
x = self.conn.hsrv.broker.ask("up2k.handle_cp", self.uname, self.ip, vsrc, vdst)
self.loud_reply(x.get(), status=201)
return True
def tx_ls(self, ls: dict[str, Any]) -> bool: def tx_ls(self, ls: dict[str, Any]) -> bool:
dirs = ls["dirs"] dirs = ls["dirs"]
files = ls["files"] files = ls["files"]
@@ -5453,6 +5604,7 @@ class HttpCli(object):
not self.args.no_scandir, not self.args.no_scandir,
[[True, False], [False, True]], [[True, False], [False, True]],
lstat="lt" in self.uparam, lstat="lt" in self.uparam,
throw=True,
) )
stats = {k: v for k, v in vfs_ls} stats = {k: v for k, v in vfs_ls}
ls_names = [x[0] for x in vfs_ls] ls_names = [x[0] for x in vfs_ls]

View File

@@ -81,6 +81,7 @@ from .util import (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from .authsrv import VFS
from .broker_util import BrokerCli from .broker_util import BrokerCli
from .ssdp import SSDPr from .ssdp import SSDPr
@@ -130,6 +131,12 @@ class HttpSrv(object):
self.bans: dict[str, int] = {} self.bans: dict[str, int] = {}
self.aclose: dict[str, int] = {} self.aclose: dict[str, int] = {}
dli: dict[str, tuple[float, int, "VFS", str, str]] = {} # info
dls: dict[str, tuple[float, int]] = {} # state
self.dli = self.tdli = dli
self.dls = self.tdls = dls
self.iiam = '<img src="%s.cpr/iiam.gif" />' % (self.args.SRS,)
self.bound: set[tuple[str, int]] = set() self.bound: set[tuple[str, int]] = set()
self.name = "hsrv" + nsuf self.name = "hsrv" + nsuf
self.mutex = threading.Lock() self.mutex = threading.Lock()
@@ -205,6 +212,9 @@ class HttpSrv(object):
self.start_threads(4) self.start_threads(4)
if nid: if nid:
self.tdli = {}
self.tdls = {}
if self.args.stackmon: if self.args.stackmon:
start_stackmon(self.args.stackmon, nid) start_stackmon(self.args.stackmon, nid)
@@ -579,3 +589,32 @@ class HttpSrv(object):
ident += "a" ident += "a"
self.u2idx_free[ident] = u2idx self.u2idx_free[ident] = u2idx
def read_dls(
self,
) -> tuple[
dict[str, tuple[float, int, str, str, str]], dict[str, tuple[float, int]]
]:
"""
mp-broker asking for local dl-info + dl-state;
reduce overhead by sending just the vfs vpath
"""
dli = {k: (a, b, c.vpath, d, e) for k, (a, b, c, d, e) in self.dli.items()}
return (dli, self.dls)
def write_dls(
self,
sdli: dict[str, tuple[float, int, str, str, str]],
dls: dict[str, tuple[float, int]],
) -> None:
"""
mp-broker pushing total dl-info + dl-state;
swap out the vfs vpath with the vfs node
"""
dli: dict[str, tuple[float, int, "VFS", str, str]] = {}
for k, (a, b, c, d, e) in sdli.items():
vn = self.asrv.vfs.all_nodes[c]
dli[k] = (a, b, vn, d, e)
self.tdli = dli
self.tdls = dls

View File

@@ -72,6 +72,9 @@ class Metrics(object):
v = "{:.3f}".format(self.hsrv.t0) v = "{:.3f}".format(self.hsrv.t0)
addug("cpp_boot_unixtime", "seconds", v, t) addug("cpp_boot_unixtime", "seconds", v, t)
t = "number of active downloads"
addg("cpp_active_dl", str(len(self.hsrv.tdls)), t)
t = "number of open http(s) client connections" t = "number of open http(s) client connections"
addg("cpp_http_conns", str(self.hsrv.ncli), t) addg("cpp_http_conns", str(self.hsrv.ncli), t)

View File

@@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals
import argparse import argparse
import json import json
import os import os
import re
import shutil import shutil
import subprocess as sp import subprocess as sp
import sys import sys
@@ -62,6 +63,9 @@ def have_ff(scmd: str) -> bool:
HAVE_FFMPEG = not os.environ.get("PRTY_NO_FFMPEG") and have_ff("ffmpeg") HAVE_FFMPEG = not os.environ.get("PRTY_NO_FFMPEG") and have_ff("ffmpeg")
HAVE_FFPROBE = not os.environ.get("PRTY_NO_FFPROBE") and have_ff("ffprobe") HAVE_FFPROBE = not os.environ.get("PRTY_NO_FFPROBE") and have_ff("ffprobe")
CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif".split())
CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b")
class MParser(object): class MParser(object):
def __init__(self, cmdline: str) -> None: def __init__(self, cmdline: str) -> None:
@@ -126,6 +130,7 @@ def au_unpk(
log: "NamedLogger", fmt_map: dict[str, str], abspath: str, vn: Optional[VFS] = None log: "NamedLogger", fmt_map: dict[str, str], abspath: str, vn: Optional[VFS] = None
) -> str: ) -> str:
ret = "" ret = ""
maxsz = 1024 * 1024 * 64
try: try:
ext = abspath.split(".")[-1].lower() ext = abspath.split(".")[-1].lower()
au, pk = fmt_map[ext].split(".") au, pk = fmt_map[ext].split(".")
@@ -148,17 +153,41 @@ def au_unpk(
zf = zipfile.ZipFile(abspath, "r") zf = zipfile.ZipFile(abspath, "r")
zil = zf.infolist() zil = zf.infolist()
zil = [x for x in zil if x.filename.lower().split(".")[-1] == au] zil = [x for x in zil if x.filename.lower().split(".")[-1] == au]
if not zil:
raise Exception("no audio inside zip")
fi = zf.open(zil[0]) fi = zf.open(zil[0])
elif pk == "cbz":
import zipfile
zf = zipfile.ZipFile(abspath, "r")
znil = [(x.filename.lower(), x) for x in zf.infolist()]
nf = len(znil)
znil = [x for x in znil if x[0].split(".")[-1] in CBZ_PICS]
znil = [x for x in znil if "cover" in x[0]] or znil
znil = [x for x in znil if CBZ_01.search(x[0])] or znil
t = "cbz: %d files, %d hits" % (nf, len(znil))
if znil:
t += ", using " + znil[0][1].filename
log(t)
if not znil:
raise Exception("no images inside cbz")
fi = zf.open(znil[0][1])
else: else:
raise Exception("unknown compression %s" % (pk,)) raise Exception("unknown compression %s" % (pk,))
fsz = 0
with os.fdopen(fd, "wb") as fo: with os.fdopen(fd, "wb") as fo:
while True: while True:
buf = fi.read(32768) buf = fi.read(32768)
if not buf: if not buf:
break break
fsz += len(buf)
if fsz > maxsz:
raise Exception("zipbomb defused")
fo.write(buf) fo.write(buf)
return ret return ret

View File

@@ -24,17 +24,13 @@ class PWHash(object):
def __init__(self, args: argparse.Namespace): def __init__(self, args: argparse.Namespace):
self.args = args self.args = args
try: zsl = args.ah_alg.split(",")
alg, ac = args.ah_alg.split(",") alg = zsl[0]
except:
alg = args.ah_alg
ac = {}
if alg == "none": if alg == "none":
alg = "" alg = ""
self.alg = alg self.alg = alg
self.ac = ac self.ac = zsl[1:]
if not alg: if not alg:
self.on = False self.on = False
self.hash = unicode self.hash = unicode
@@ -90,17 +86,23 @@ class PWHash(object):
its = 2 its = 2
blksz = 8 blksz = 8
para = 4 para = 4
ramcap = 0 # openssl 1.1 = 32 MiB
try: try:
cost = 2 << int(self.ac[0]) cost = 2 << int(self.ac[0])
its = int(self.ac[1]) its = int(self.ac[1])
blksz = int(self.ac[2]) blksz = int(self.ac[2])
para = int(self.ac[3]) para = int(self.ac[3])
ramcap = int(self.ac[4]) * 1024 * 1024
except: except:
pass pass
cfg = {"salt": self.salt, "n": cost, "r": blksz, "p": para, "dklen": 24}
if ramcap:
cfg["maxmem"] = ramcap
ret = plain.encode("utf-8") ret = plain.encode("utf-8")
for _ in range(its): for _ in range(its):
ret = hashlib.scrypt(ret, salt=self.salt, n=cost, r=blksz, p=para, dklen=24) ret = hashlib.scrypt(ret, **cfg)
return "+" + base64.urlsafe_b64encode(ret).decode("utf-8") return "+" + base64.urlsafe_b64encode(ret).decode("utf-8")

View File

@@ -112,7 +112,7 @@ class SvcHub(object):
self.stopping = False self.stopping = False
self.stopped = False self.stopped = False
self.reload_req = False self.reload_req = False
self.reloading = 0 self.reload_mutex = threading.Lock()
self.stop_cond = threading.Condition() self.stop_cond = threading.Condition()
self.nsigs = 3 self.nsigs = 3
self.retcode = 0 self.retcode = 0
@@ -211,6 +211,15 @@ class SvcHub(object):
t = "WARNING: --s-rd-sz (%d) is larger than --iobuf (%d); this may lead to reduced performance" t = "WARNING: --s-rd-sz (%d) is larger than --iobuf (%d); this may lead to reduced performance"
self.log("root", t % (args.s_rd_sz, args.iobuf), 3) self.log("root", t % (args.s_rd_sz, args.iobuf), 3)
zs = ""
if args.th_ram_max < 0.22:
zs = "generate thumbnails"
elif args.th_ram_max < 1:
zs = "generate audio waveforms or spectrograms"
if zs:
t = "WARNING: --th-ram-max is very small (%.2f GiB); will not be able to %s"
self.log("root", t % (args.th_ram_max, zs), 3)
if args.chpw and args.idp_h_usr: if args.chpw and args.idp_h_usr:
t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr" t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr"
self.log("root", t, 1) self.log("root", t, 1)
@@ -1004,41 +1013,18 @@ class SvcHub(object):
except: except:
self.log("root", "ssdp startup failed;\n" + min_ex(), 3) self.log("root", "ssdp startup failed;\n" + min_ex(), 3)
def reload(self) -> str: def reload(self, rescan_all_vols: bool, up2k: bool) -> str:
with self.up2k.mutex: t = "config has been reloaded"
if self.reloading: with self.reload_mutex:
return "cannot reload; already in progress"
self.reloading = 1
Daemon(self._reload, "reloading")
return "reload initiated"
def _reload(self, rescan_all_vols: bool = True, up2k: bool = True) -> None:
with self.up2k.mutex:
if self.reloading != 1:
return
self.reloading = 2
self.log("root", "reloading config") self.log("root", "reloading config")
self.asrv.reload(9 if up2k else 4) self.asrv.reload(9 if up2k else 4)
if up2k: if up2k:
self.up2k.reload(rescan_all_vols) self.up2k.reload(rescan_all_vols)
t += "; volumes are now reinitializing"
else: else:
self.log("root", "reload done") self.log("root", "reload done")
self.broker.reload() self.broker.reload()
self.reloading = 0 return t
def _reload_blocking(self, rescan_all_vols: bool = True, up2k: bool = True) -> None:
while True:
with self.up2k.mutex:
if self.reloading < 2:
self.reloading = 1
break
time.sleep(0.05)
# try to handle multiple pending IdP reloads at once:
time.sleep(0.2)
self._reload(rescan_all_vols=rescan_all_vols, up2k=up2k)
def _reload_sessions(self) -> None: def _reload_sessions(self) -> None:
with self.asrv.mutex: with self.asrv.mutex:
@@ -1052,7 +1038,7 @@ class SvcHub(object):
if self.reload_req: if self.reload_req:
self.reload_req = False self.reload_req = False
self.reload() self.reload(True, True)
self.shutdown() self.shutdown()

View File

@@ -100,7 +100,7 @@ def gen_hdr(
# spec says to put zeros when !crc if bit3 (streaming) # spec says to put zeros when !crc if bit3 (streaming)
# however infozip does actual sz and it even works on winxp # however infozip does actual sz and it even works on winxp
# (same reasning for z64 extradata later) # (same reasoning for z64 extradata later)
vsz = 0xFFFFFFFF if z64 else sz vsz = 0xFFFFFFFF if z64 else sz
ret += spack(b"<LL", vsz, vsz) ret += spack(b"<LL", vsz, vsz)

View File

@@ -371,7 +371,7 @@ class TcpSrv(object):
if self.args.q: if self.args.q:
print(msg) print(msg)
self.hub.broker.say("listen", srv) self.hub.broker.say("httpsrv.listen", srv)
self.srv = srvs self.srv = srvs
self.bound = bound self.bound = bound
@@ -379,7 +379,7 @@ class TcpSrv(object):
self._distribute_netdevs() self._distribute_netdevs()
def _distribute_netdevs(self): def _distribute_netdevs(self):
self.hub.broker.say("set_netdevs", self.netdevs) self.hub.broker.say("httpsrv.set_netdevs", self.netdevs)
self.hub.start_zeroconf() self.hub.start_zeroconf()
gencert(self.log, self.args, self.netdevs) gencert(self.log, self.args, self.netdevs)
self.hub.restart_ftpd() self.hub.restart_ftpd()

View File

@@ -269,6 +269,7 @@ class Tftpd(object):
"*", "*",
not self.args.no_scandir, not self.args.no_scandir,
[[True, False]], [[True, False]],
throw=True,
) )
dnames = set([x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]) dnames = set([x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)])
dirs1 = [(v.st_mtime, v.st_size, k + "/") for k, v in vfs_ls if k in dnames] dirs1 = [(v.st_mtime, v.st_size, k + "/") for k, v in vfs_ls if k in dnames]

View File

@@ -20,7 +20,6 @@ from .util import (
FFMPEG_URL, FFMPEG_URL,
Cooldown, Cooldown,
Daemon, Daemon,
Pebkac,
afsenc, afsenc,
fsenc, fsenc,
min_ex, min_ex,
@@ -164,6 +163,7 @@ class ThumbSrv(object):
self.ram: dict[str, float] = {} self.ram: dict[str, float] = {}
self.memcond = threading.Condition(self.mutex) self.memcond = threading.Condition(self.mutex)
self.stopping = False self.stopping = False
self.rm_nullthumbs = True # forget failed conversions on startup
self.nthr = max(1, self.args.th_mt) self.nthr = max(1, self.args.th_mt)
self.q: Queue[Optional[tuple[str, str, str, VFS]]] = Queue(self.nthr * 4) self.q: Queue[Optional[tuple[str, str, str, VFS]]] = Queue(self.nthr * 4)
@@ -862,7 +862,6 @@ class ThumbSrv(object):
def cleaner(self) -> None: def cleaner(self) -> None:
interval = self.args.th_clean interval = self.args.th_clean
while True: while True:
time.sleep(interval)
ndirs = 0 ndirs = 0
for vol, histpath in self.asrv.vfs.histtab.items(): for vol, histpath in self.asrv.vfs.histtab.items():
if histpath.startswith(vol): if histpath.startswith(vol):
@@ -876,6 +875,8 @@ class ThumbSrv(object):
self.log("\033[Jcln err in %s: %r" % (histpath, ex), 3) self.log("\033[Jcln err in %s: %r" % (histpath, ex), 3)
self.log("\033[Jcln ok; rm {} dirs".format(ndirs)) self.log("\033[Jcln ok; rm {} dirs".format(ndirs))
self.rm_nullthumbs = False
time.sleep(interval)
def clean(self, histpath: str) -> int: def clean(self, histpath: str) -> int:
ret = 0 ret = 0
@@ -896,7 +897,9 @@ class ThumbSrv(object):
prev_b64 = None prev_b64 = None
prev_fp = "" prev_fp = ""
try: try:
t1 = statdir(self.log_func, not self.args.no_scandir, False, thumbpath) t1 = statdir(
self.log_func, not self.args.no_scandir, False, thumbpath, False
)
ents = sorted(list(t1)) ents = sorted(list(t1))
except: except:
return 0 return 0
@@ -937,6 +940,10 @@ class ThumbSrv(object):
continue continue
if self.rm_nullthumbs and not inf.st_size:
bos.unlink(fp)
continue
if b64 == prev_b64: if b64 == prev_b64:
self.log("rm replaced [{}]".format(fp)) self.log("rm replaced [{}]".format(fp))
bos.unlink(prev_fp) bos.unlink(prev_fp)

View File

@@ -89,6 +89,8 @@ zsg = "avif,avifs,bmp,gif,heic,heics,heif,heifs,ico,j2p,j2k,jp2,jpeg,jpg,jpx,png
CV_EXTS = set(zsg.split(",")) CV_EXTS = set(zsg.split(","))
SBUSY = "cannot receive uploads right now;\nserver busy with %s.\nPlease wait; the client will retry..."
HINT_HISTPATH = "you could try moving the database to another location (preferably an SSD or NVME drive) using either the --hist argument (global option for all volumes), or the hist volflag (just for this volume)" HINT_HISTPATH = "you could try moving the database to another location (preferably an SSD or NVME drive) using either the --hist argument (global option for all volumes), or the hist volflag (just for this volume)"
@@ -125,12 +127,22 @@ class Up2k(object):
self.args = hub.args self.args = hub.args
self.log_func = hub.log self.log_func = hub.log
self.vfs = self.asrv.vfs
self.acct = self.asrv.acct
self.iacct = self.asrv.iacct
self.grps = self.asrv.grps
self.salt = self.args.warksalt self.salt = self.args.warksalt
self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$") self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$")
self.gid = 0 self.gid = 0
self.gt0 = 0
self.gt1 = 0
self.stop = False self.stop = False
self.mutex = threading.Lock() self.mutex = threading.Lock()
self.reload_mutex = threading.Lock()
self.reload_flag = 0
self.reloading = False
self.blocked: Optional[str] = None self.blocked: Optional[str] = None
self.pp: Optional[ProgressPrinter] = None self.pp: Optional[ProgressPrinter] = None
self.rescan_cond = threading.Condition() self.rescan_cond = threading.Condition()
@@ -203,7 +215,38 @@ class Up2k(object):
Daemon(self.deferred_init, "up2k-deferred-init") Daemon(self.deferred_init, "up2k-deferred-init")
def unpp(self) -> None:
self.gt1 = time.time()
if self.pp:
self.pp.end = True
self.pp = None
def reload(self, rescan_all_vols: bool) -> None: def reload(self, rescan_all_vols: bool) -> None:
n = 2 if rescan_all_vols else 1
with self.reload_mutex:
if self.reload_flag < n:
self.reload_flag = n
with self.rescan_cond:
self.rescan_cond.notify_all()
def _reload_thr(self) -> None:
while self.pp:
time.sleep(0.1)
while True:
with self.reload_mutex:
if not self.reload_flag:
break
rav = self.reload_flag == 2
self.reload_flag = 0
gt1 = self.gt1
with self.mutex:
self._reload(rav)
while gt1 == self.gt1 or self.pp:
time.sleep(0.1)
self.reloading = False
def _reload(self, rescan_all_vols: bool) -> None:
"""mutex(main) me""" """mutex(main) me"""
self.log("reload #{} scheduled".format(self.gid + 1)) self.log("reload #{} scheduled".format(self.gid + 1))
all_vols = self.asrv.vfs.all_vols all_vols = self.asrv.vfs.all_vols
@@ -228,10 +271,7 @@ class Up2k(object):
with self.mutex, self.reg_mutex: with self.mutex, self.reg_mutex:
self._drop_caches() self._drop_caches()
if self.pp: self.unpp()
self.pp.end = True
self.pp = None
return return
if not self.pp and self.args.exit == "idx": if not self.pp and self.args.exit == "idx":
@@ -311,8 +351,8 @@ class Up2k(object):
def _active_uploads(self, uname: str) -> list[tuple[float, int, int, str]]: def _active_uploads(self, uname: str) -> list[tuple[float, int, int, str]]:
ret = [] ret = []
for vtop in self.asrv.vfs.aread[uname]: for vtop in self.vfs.aread.get(uname) or []:
vfs = self.asrv.vfs.all_vols.get(vtop) vfs = self.vfs.all_vols.get(vtop)
if not vfs: # dbv only if not vfs: # dbv only
continue continue
ptop = vfs.realpath ptop = vfs.realpath
@@ -485,6 +525,12 @@ class Up2k(object):
if self.stop: if self.stop:
return return
with self.reload_mutex:
if self.reload_flag and not self.reloading:
self.reloading = True
zs = "up2k-reload-%d" % (self.gid,)
Daemon(self._reload_thr, zs)
now = time.time() now = time.time()
if now < cooldown: if now < cooldown:
# self.log("SR: cd - now = {:.2f}".format(cooldown - now), 5) # self.log("SR: cd - now = {:.2f}".format(cooldown - now), 5)
@@ -521,7 +567,7 @@ class Up2k(object):
raise raise
with self.mutex: with self.mutex:
for vp, vol in sorted(self.asrv.vfs.all_vols.items()): for vp, vol in sorted(self.vfs.all_vols.items()):
maxage = vol.flags.get("scan") maxage = vol.flags.get("scan")
if not maxage: if not maxage:
continue continue
@@ -554,7 +600,7 @@ class Up2k(object):
if vols: if vols:
cooldown = now + 10 cooldown = now + 10
err = self.rescan(self.asrv.vfs.all_vols, vols, False, False) err = self.rescan(self.vfs.all_vols, vols, False, False)
if err: if err:
for v in vols: for v in vols:
self.need_rescan.add(v) self.need_rescan.add(v)
@@ -567,7 +613,7 @@ class Up2k(object):
def _check_lifetimes(self) -> float: def _check_lifetimes(self) -> float:
now = time.time() now = time.time()
timeout = now + 9001 timeout = now + 9001
for vp, vol in sorted(self.asrv.vfs.all_vols.items()): for vp, vol in sorted(self.vfs.all_vols.items()):
lifetime = vol.flags.get("lifetime") lifetime = vol.flags.get("lifetime")
if not lifetime: if not lifetime:
continue continue
@@ -621,7 +667,7 @@ class Up2k(object):
maxage = self.args.shr_rt * 60 maxage = self.args.shr_rt * 60
low = now - maxage low = now - maxage
vn = self.asrv.vfs.nodes.get(self.args.shr.strip("/")) vn = self.vfs.nodes.get(self.args.shr.strip("/"))
active = vn and vn.nodes active = vn and vn.nodes
db = sqlite3.connect(self.args.shr_db, timeout=2) db = sqlite3.connect(self.args.shr_db, timeout=2)
@@ -646,7 +692,7 @@ class Up2k(object):
db.commit() db.commit()
if reload: if reload:
Daemon(self.hub._reload_blocking, "sharedrop", (False, False)) Daemon(self.hub.reload, "sharedrop", (False, False))
q = "select min(t1) from sh where t1 > ?" q = "select min(t1) from sh where t1 > ?"
(earliest,) = cur.execute(q, (1,)).fetchone() (earliest,) = cur.execute(q, (1,)).fetchone()
@@ -672,7 +718,7 @@ class Up2k(object):
return 2 return 2
ret = 9001 ret = 9001
for _, vol in sorted(self.asrv.vfs.all_vols.items()): for _, vol in sorted(self.vfs.all_vols.items()):
rp = vol.realpath rp = vol.realpath
cur = self.cur.get(rp) cur = self.cur.get(rp)
if not cur: if not cur:
@@ -774,6 +820,8 @@ class Up2k(object):
with self.mutex: with self.mutex:
gid = self.gid gid = self.gid
self.gt0 = time.time()
nspin = 0 nspin = 0
while True: while True:
nspin += 1 nspin += 1
@@ -796,6 +844,11 @@ class Up2k(object):
if gid: if gid:
self.log("reload #%d running" % (gid,)) self.log("reload #%d running" % (gid,))
self.vfs = self.asrv.vfs
self.acct = self.asrv.acct
self.iacct = self.asrv.iacct
self.grps = self.asrv.grps
vols = list(all_vols.values()) vols = list(all_vols.values())
t0 = time.time() t0 = time.time()
have_e2d = False have_e2d = False
@@ -859,7 +912,7 @@ class Up2k(object):
self._drop_caches() self._drop_caches()
for vol in vols: for vol in vols:
if self.stop: if self.stop or gid != self.gid:
break break
en = set(vol.flags.get("mte", {})) en = set(vol.flags.get("mte", {}))
@@ -990,7 +1043,7 @@ class Up2k(object):
if self.mtag: if self.mtag:
Daemon(self._run_all_mtp, "up2k-mtp-scan", (gid,)) Daemon(self._run_all_mtp, "up2k-mtp-scan", (gid,))
else: else:
self.pp = None self.unpp()
return have_e2d return have_e2d
@@ -998,7 +1051,7 @@ class Up2k(object):
self, ptop: str, flags: dict[str, Any] self, ptop: str, flags: dict[str, Any]
) -> Optional[tuple["sqlite3.Cursor", str]]: ) -> Optional[tuple["sqlite3.Cursor", str]]:
"""mutex(main,reg) me""" """mutex(main,reg) me"""
histpath = self.asrv.vfs.histtab.get(ptop) histpath = self.vfs.histtab.get(ptop)
if not histpath: if not histpath:
self.log("no histpath for [{}]".format(ptop)) self.log("no histpath for [{}]".format(ptop))
return None return None
@@ -1011,7 +1064,7 @@ class Up2k(object):
return None return None
vpath = "?" vpath = "?"
for k, v in self.asrv.vfs.all_vols.items(): for k, v in self.vfs.all_vols.items():
if v.realpath == ptop: if v.realpath == ptop:
vpath = k vpath = k
@@ -1178,7 +1231,7 @@ class Up2k(object):
def _verify_db_cache(self, cur: "sqlite3.Cursor", vpath: str) -> None: def _verify_db_cache(self, cur: "sqlite3.Cursor", vpath: str) -> None:
# check if list of intersecting volumes changed since last use; drop caches if so # check if list of intersecting volumes changed since last use; drop caches if so
prefix = (vpath + "/").lstrip("/") prefix = (vpath + "/").lstrip("/")
zsl = [x for x in self.asrv.vfs.all_vols if x.startswith(prefix)] zsl = [x for x in self.vfs.all_vols if x.startswith(prefix)]
zsl = [x[len(prefix) :] for x in zsl] zsl = [x[len(prefix) :] for x in zsl]
zsl.sort() zsl.sort()
zb = hashlib.sha1("\n".join(zsl).encode("utf-8", "replace")).digest() zb = hashlib.sha1("\n".join(zsl).encode("utf-8", "replace")).digest()
@@ -1223,7 +1276,7 @@ class Up2k(object):
if d != vol and (d.vpath.startswith(vol.vpath + "/") or not vol.vpath) if d != vol and (d.vpath.startswith(vol.vpath + "/") or not vol.vpath)
] ]
excl += [absreal(x) for x in excl] excl += [absreal(x) for x in excl]
excl += list(self.asrv.vfs.histtab.values()) excl += list(self.vfs.histtab.values())
if WINDOWS: if WINDOWS:
excl = [x.replace("/", "\\") for x in excl] excl = [x.replace("/", "\\") for x in excl]
else: else:
@@ -1347,7 +1400,7 @@ class Up2k(object):
rds = rd + "/" if rd else "" rds = rd + "/" if rd else ""
cdirs = cdir + os.sep cdirs = cdir + os.sep
g = statdir(self.log_func, not self.args.no_scandir, True, cdir) g = statdir(self.log_func, not self.args.no_scandir, True, cdir, False)
gl = sorted(g) gl = sorted(g)
partials = set([x[0] for x in gl if "PARTIAL" in x[0]]) partials = set([x[0] for x in gl if "PARTIAL" in x[0]])
for iname, inf in gl: for iname, inf in gl:
@@ -1411,7 +1464,7 @@ class Up2k(object):
t = "failed to index subdir [{}]:\n{}" t = "failed to index subdir [{}]:\n{}"
self.log(t.format(abspath, min_ex()), c=1) self.log(t.format(abspath, min_ex()), c=1)
elif not stat.S_ISREG(inf.st_mode): elif not stat.S_ISREG(inf.st_mode):
self.log("skip type-{:x} file [{}]".format(inf.st_mode, abspath)) self.log("skip type-0%o file [%s]" % (inf.st_mode, abspath))
else: else:
# self.log("file: {}".format(abspath)) # self.log("file: {}".format(abspath))
if rp.endswith(".PARTIAL") and time.time() - lmod < 60: if rp.endswith(".PARTIAL") and time.time() - lmod < 60:
@@ -1733,7 +1786,7 @@ class Up2k(object):
excl = [ excl = [
d[len(vol.vpath) :].lstrip("/") d[len(vol.vpath) :].lstrip("/")
for d in self.asrv.vfs.all_vols for d in self.vfs.all_vols
if d != vol.vpath and (d.startswith(vol.vpath + "/") or not vol.vpath) if d != vol.vpath and (d.startswith(vol.vpath + "/") or not vol.vpath)
] ]
qexa: list[str] = [] qexa: list[str] = []
@@ -1885,7 +1938,7 @@ class Up2k(object):
def _drop_caches(self) -> None: def _drop_caches(self) -> None:
"""mutex(main,reg) me""" """mutex(main,reg) me"""
self.log("dropping caches for a full filesystem scan") self.log("dropping caches for a full filesystem scan")
for vol in self.asrv.vfs.all_vols.values(): for vol in self.vfs.all_vols.values():
reg = self.register_vpath(vol.realpath, vol.flags) reg = self.register_vpath(vol.realpath, vol.flags)
if not reg: if not reg:
continue continue
@@ -2113,7 +2166,7 @@ class Up2k(object):
self._run_one_mtp(ptop, gid) self._run_one_mtp(ptop, gid)
vtop = "\n" vtop = "\n"
for vol in self.asrv.vfs.all_vols.values(): for vol in self.vfs.all_vols.values():
if vol.realpath == ptop: if vol.realpath == ptop:
vtop = vol.vpath vtop = vol.vpath
if "running mtp" in self.volstate.get(vtop, ""): if "running mtp" in self.volstate.get(vtop, ""):
@@ -2123,7 +2176,7 @@ class Up2k(object):
msg = "mtp finished in {:.2f} sec ({})" msg = "mtp finished in {:.2f} sec ({})"
self.log(msg.format(td, s2hms(td, True))) self.log(msg.format(td, s2hms(td, True)))
self.pp = None self.unpp()
if self.args.exit == "idx": if self.args.exit == "idx":
self.hub.sigterm() self.hub.sigterm()
@@ -2765,6 +2818,9 @@ class Up2k(object):
) -> dict[str, Any]: ) -> dict[str, Any]:
# busy_aps is u2fh (always undefined if -j0) so this is safe # busy_aps is u2fh (always undefined if -j0) so this is safe
self.busy_aps = busy_aps self.busy_aps = busy_aps
if self.reload_flag or self.reloading:
raise Pebkac(503, SBUSY % ("fs-reload",))
got_lock = False got_lock = False
try: try:
# bit expensive; 3.9=10x 3.11=2x # bit expensive; 3.9=10x 3.11=2x
@@ -2773,8 +2829,7 @@ class Up2k(object):
with self.reg_mutex: with self.reg_mutex:
ret = self._handle_json(cj) ret = self._handle_json(cj)
else: else:
t = "cannot receive uploads right now;\nserver busy with {}.\nPlease wait; the client will retry..." raise Pebkac(503, SBUSY % (self.blocked or "[unknown]",))
raise Pebkac(503, t.format(self.blocked or "[unknown]"))
except TypeError: except TypeError:
if not PY2: if not PY2:
raise raise
@@ -2816,7 +2871,7 @@ class Up2k(object):
if True: if True:
jcur = self.cur.get(ptop) jcur = self.cur.get(ptop)
reg = self.registry[ptop] reg = self.registry[ptop]
vfs = self.asrv.vfs.all_vols[cj["vtop"]] vfs = self.vfs.all_vols[cj["vtop"]]
n4g = bool(vfs.flags.get("noforget")) n4g = bool(vfs.flags.get("noforget"))
noclone = bool(vfs.flags.get("noclone")) noclone = bool(vfs.flags.get("noclone"))
rand = vfs.flags.get("rand") or cj.get("rand") rand = vfs.flags.get("rand") or cj.get("rand")
@@ -2840,7 +2895,7 @@ class Up2k(object):
alts: list[tuple[int, int, dict[str, Any], "sqlite3.Cursor", str, str]] = [] alts: list[tuple[int, int, dict[str, Any], "sqlite3.Cursor", str, str]] = []
for ptop, cur in vols: for ptop, cur in vols:
allv = self.asrv.vfs.all_vols allv = self.vfs.all_vols
cvfs = next((v for v in allv.values() if v.realpath == ptop), vfs) cvfs = next((v for v in allv.values() if v.realpath == ptop), vfs)
vtop = cj["vtop"] if cur == jcur else cvfs.vpath vtop = cj["vtop"] if cur == jcur else cvfs.vpath
@@ -3083,7 +3138,7 @@ class Up2k(object):
vp, vp,
job["host"], job["host"],
job["user"], job["user"],
self.asrv.vfs.get_perms(job["vtop"], job["user"]), self.vfs.get_perms(job["vtop"], job["user"]),
job["lmod"], job["lmod"],
job["size"], job["size"],
job["addr"], job["addr"],
@@ -3095,7 +3150,7 @@ class Up2k(object):
self.log(t, 1) self.log(t, 1)
raise Pebkac(403, t) raise Pebkac(403, t)
if hr.get("reloc"): if hr.get("reloc"):
x = pathmod(self.asrv.vfs, dst, vp, hr["reloc"]) x = pathmod(self.vfs, dst, vp, hr["reloc"])
if x: if x:
zvfs = vfs zvfs = vfs
pdir, _, job["name"], (vfs, rem) = x pdir, _, job["name"], (vfs, rem) = x
@@ -3555,7 +3610,7 @@ class Up2k(object):
wake_sr = False wake_sr = False
try: try:
flt = job["life"] flt = job["life"]
vfs = self.asrv.vfs.all_vols[job["vtop"]] vfs = self.vfs.all_vols[job["vtop"]]
vlt = vfs.flags["lifetime"] vlt = vfs.flags["lifetime"]
if vlt and flt > 1 and flt < vlt: if vlt and flt > 1 and flt < vlt:
upt -= vlt - flt upt -= vlt - flt
@@ -3731,7 +3786,7 @@ class Up2k(object):
djoin(vtop, rd, fn), djoin(vtop, rd, fn),
host, host,
usr, usr,
self.asrv.vfs.get_perms(djoin(vtop, rd, fn), usr), self.vfs.get_perms(djoin(vtop, rd, fn), usr),
ts, ts,
sz, sz,
ip, ip,
@@ -3841,13 +3896,13 @@ class Up2k(object):
partial = "" partial = ""
if not unpost: if not unpost:
permsets = [[True, False, False, True]] permsets = [[True, False, False, True]]
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0]) vn0, rem0 = self.vfs.get(vpath, uname, *permsets[0])
vn, rem = vn.get_dbv(rem) vn, rem = vn0.get_dbv(rem0)
else: else:
# unpost with missing permissions? verify with db # unpost with missing permissions? verify with db
permsets = [[False, True]] permsets = [[False, True]]
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0]) vn0, rem0 = self.vfs.get(vpath, uname, *permsets[0])
vn, rem = vn.get_dbv(rem) vn, rem = vn0.get_dbv(rem0)
ptop = vn.realpath ptop = vn.realpath
with self.mutex, self.reg_mutex: with self.mutex, self.reg_mutex:
abrt_cfg = self.flags.get(ptop, {}).get("u2abort", 1) abrt_cfg = self.flags.get(ptop, {}).get("u2abort", 1)
@@ -3903,7 +3958,9 @@ class Up2k(object):
scandir = not self.args.no_scandir scandir = not self.args.no_scandir
if is_dir: if is_dir:
g = vn.walk("", rem, [], uname, permsets, True, scandir, True) # note: deletion inside shares would require a rewrite here;
# shares necessitate get_dbv which is incompatible with walk
g = vn0.walk("", rem0, [], uname, permsets, True, scandir, True)
if unpost: if unpost:
raise Pebkac(400, "cannot unpost folders") raise Pebkac(400, "cannot unpost folders")
elif stat.S_ISLNK(st.st_mode) or stat.S_ISREG(st.st_mode): elif stat.S_ISLNK(st.st_mode) or stat.S_ISREG(st.st_mode):
@@ -3911,7 +3968,7 @@ class Up2k(object):
vpath_dir = vsplit(vpath)[0] vpath_dir = vsplit(vpath)[0]
g = [(vn, voldir, vpath_dir, adir, [(fn, 0)], [], {})] # type: ignore g = [(vn, voldir, vpath_dir, adir, [(fn, 0)], [], {})] # type: ignore
else: else:
self.log("rm: skip type-{:x} file [{}]".format(st.st_mode, atop)) self.log("rm: skip type-0%o file [%s]" % (st.st_mode, atop))
return 0, [], [] return 0, [], []
xbd = vn.flags.get("xbd") xbd = vn.flags.get("xbd")
@@ -3951,7 +4008,7 @@ class Up2k(object):
vpath, vpath,
"", "",
uname, uname,
self.asrv.vfs.get_perms(vpath, uname), self.vfs.get_perms(vpath, uname),
stl.st_mtime, stl.st_mtime,
st.st_size, st.st_size,
ip, ip,
@@ -3991,7 +4048,7 @@ class Up2k(object):
vpath, vpath,
"", "",
uname, uname,
self.asrv.vfs.get_perms(vpath, uname), self.vfs.get_perms(vpath, uname),
stl.st_mtime, stl.st_mtime,
st.st_size, st.st_size,
ip, ip,
@@ -4011,17 +4068,226 @@ class Up2k(object):
return n_files, ok + ok2, ng + ng2 return n_files, ok + ok2, ng + ng2
def handle_cp(self, uname: str, ip: str, svp: str, dvp: str) -> str:
if svp == dvp or dvp.startswith(svp + "/"):
raise Pebkac(400, "cp: cannot copy parent into subfolder")
svn, srem = self.vfs.get(svp, uname, True, False)
svn_dbv, _ = svn.get_dbv(srem)
sabs = svn.canonical(srem, False)
curs: set["sqlite3.Cursor"] = set()
self.db_act = self.vol_act[svn_dbv.realpath] = time.time()
st = bos.stat(sabs)
if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):
with self.mutex:
try:
ret = self._cp_file(uname, ip, svp, dvp, curs)
finally:
for v in curs:
v.connection.commit()
return ret
if not stat.S_ISDIR(st.st_mode):
raise Pebkac(400, "cannot copy type-0%o file" % (st.st_mode,))
permsets = [[True, False]]
scandir = not self.args.no_scandir
# don't use svn_dbv; would skip subvols due to _ls `if not rem:`
g = svn.walk("", srem, [], uname, permsets, True, scandir, True)
with self.mutex:
try:
for dbv, vrem, _, atop, files, rd, vd in g:
for fn in files:
self.db_act = self.vol_act[dbv.realpath] = time.time()
svpf = "/".join(x for x in [dbv.vpath, vrem, fn[0]] if x)
if not svpf.startswith(svp + "/"): # assert
self.log(min_ex(), 1)
t = "cp: bug at %s, top %s%s"
raise Pebkac(500, t % (svpf, svp, SEESLOG))
dvpf = dvp + svpf[len(svp) :]
self._cp_file(uname, ip, svpf, dvpf, curs)
for v in curs:
v.connection.commit()
curs.clear()
finally:
for v in curs:
v.connection.commit()
return "k"
def _cp_file(
self, uname: str, ip: str, svp: str, dvp: str, curs: set["sqlite3.Cursor"]
) -> str:
"""mutex(main) me; will mutex(reg)"""
svn, srem = self.vfs.get(svp, uname, True, False)
svn_dbv, srem_dbv = svn.get_dbv(srem)
dvn, drem = self.vfs.get(dvp, uname, False, True)
dvn, drem = dvn.get_dbv(drem)
sabs = svn.canonical(srem, False)
dabs = dvn.canonical(drem)
drd, dfn = vsplit(drem)
if bos.path.exists(dabs):
raise Pebkac(400, "cp2: target file exists")
st = stl = bos.lstat(sabs)
if stat.S_ISLNK(stl.st_mode):
is_link = True
try:
st = bos.stat(sabs)
except:
pass # broken symlink; keep as-is
elif not stat.S_ISREG(st.st_mode):
self.log("skipping type-0%o file [%s]" % (st.st_mode, sabs))
return ""
else:
is_link = False
ftime = stl.st_mtime
fsize = st.st_size
xbc = svn.flags.get("xbc")
xac = dvn.flags.get("xac")
if xbc:
if not runhook(
self.log,
None,
self,
"xbc",
xbc,
sabs,
svp,
"",
uname,
self.vfs.get_perms(svp, uname),
ftime,
fsize,
ip,
time.time(),
"",
):
t = "copy blocked by xbr server config: {}".format(svp)
self.log(t, 1)
raise Pebkac(405, t)
bos.makedirs(os.path.dirname(dabs))
c1, w, ftime_, fsize_, ip, at = self._find_from_vpath(
svn_dbv.realpath, srem_dbv
)
c2 = self.cur.get(dvn.realpath)
if w:
assert c1 # !rm
if c2 and c2 != c1:
self._copy_tags(c1, c2, w)
curs.add(c1)
if c2:
self.db_add(
c2,
{}, # skip upload hooks
drd,
dfn,
ftime,
fsize,
dvn.realpath,
dvn.vpath,
w,
w,
"",
"",
ip or "",
at or 0,
)
curs.add(c2)
else:
self.log("not found in src db: [{}]".format(svp))
try:
if is_link and st != stl:
# relink non-broken symlinks to still work after the move,
# but only resolve 1st level to maintain relativity
dlink = bos.readlink(sabs)
dlink = os.path.join(os.path.dirname(sabs), dlink)
dlink = bos.path.abspath(dlink)
self._symlink(dlink, dabs, dvn.flags, lmod=ftime)
else:
self._symlink(sabs, dabs, dvn.flags, lmod=ftime)
except OSError as ex:
if ex.errno != errno.EXDEV:
raise
self.log("using plain copy (%s):\n %s\n %s" % (ex.strerror, sabs, dabs))
b1, b2 = fsenc(sabs), fsenc(dabs)
is_link = os.path.islink(b1) # due to _relink
try:
shutil.copy2(b1, b2)
except:
try:
wunlink(self.log, dabs, dvn.flags)
except:
pass
if not is_link:
raise
# broken symlink? keep it as-is
try:
zb = os.readlink(b1)
os.symlink(zb, b2)
except:
wunlink(self.log, dabs, dvn.flags)
raise
if is_link:
try:
times = (int(time.time()), int(ftime))
bos.utime(dabs, times, False)
except:
pass
if xac:
runhook(
self.log,
None,
self,
"xac",
xac,
dabs,
dvp,
"",
uname,
self.vfs.get_perms(dvp, uname),
ftime,
fsize,
ip,
time.time(),
"",
)
return "k"
def handle_mv(self, uname: str, ip: str, svp: str, dvp: str) -> str: def handle_mv(self, uname: str, ip: str, svp: str, dvp: str) -> str:
if svp == dvp or dvp.startswith(svp + "/"): if svp == dvp or dvp.startswith(svp + "/"):
raise Pebkac(400, "mv: cannot move parent into subfolder") raise Pebkac(400, "mv: cannot move parent into subfolder")
svn, srem = self.asrv.vfs.get(svp, uname, True, False, True) svn, srem = self.vfs.get(svp, uname, True, False, True)
svn, srem = svn.get_dbv(srem) jail, jail_rem = svn.get_dbv(srem)
sabs = svn.canonical(srem, False) sabs = svn.canonical(srem, False)
curs: set["sqlite3.Cursor"] = set() curs: set["sqlite3.Cursor"] = set()
self.db_act = self.vol_act[svn.realpath] = time.time() self.db_act = self.vol_act[jail.realpath] = time.time()
if not srem: if not jail_rem:
raise Pebkac(400, "mv: cannot move a mountpoint") raise Pebkac(400, "mv: cannot move a mountpoint")
st = bos.lstat(sabs) st = bos.lstat(sabs)
@@ -4035,7 +4301,9 @@ class Up2k(object):
return ret return ret
jail = svn.get_dbv(srem)[0] if not stat.S_ISDIR(st.st_mode):
raise Pebkac(400, "cannot move type-0%o file" % (st.st_mode,))
permsets = [[True, False, True]] permsets = [[True, False, True]]
scandir = not self.args.no_scandir scandir = not self.args.no_scandir
@@ -4047,13 +4315,13 @@ class Up2k(object):
raise Pebkac(400, "mv: source folder contains other volumes") raise Pebkac(400, "mv: source folder contains other volumes")
g = svn.walk("", srem, [], uname, permsets, True, scandir, True) g = svn.walk("", srem, [], uname, permsets, True, scandir, True)
with self.mutex:
try:
for dbv, vrem, _, atop, files, rd, vd in g: for dbv, vrem, _, atop, files, rd, vd in g:
if dbv != jail: if dbv != jail:
# the actual check (avoid toctou) # the actual check (avoid toctou)
raise Pebkac(400, "mv: source folder contains other volumes") raise Pebkac(400, "mv: source folder contains other volumes")
with self.mutex:
try:
for fn in files: for fn in files:
self.db_act = self.vol_act[dbv.realpath] = time.time() self.db_act = self.vol_act[dbv.realpath] = time.time()
svpf = "/".join(x for x in [dbv.vpath, vrem, fn[0]] if x) svpf = "/".join(x for x in [dbv.vpath, vrem, fn[0]] if x)
@@ -4064,12 +4332,14 @@ class Up2k(object):
dvpf = dvp + svpf[len(svp) :] dvpf = dvp + svpf[len(svp) :]
self._mv_file(uname, ip, svpf, dvpf, curs) self._mv_file(uname, ip, svpf, dvpf, curs)
for v in curs:
v.connection.commit()
curs.clear()
finally: finally:
for v in curs: for v in curs:
v.connection.commit() v.connection.commit()
curs.clear()
rm_ok, rm_ng = rmdirs(self.log_func, scandir, True, sabs, 1) rm_ok, rm_ng = rmdirs(self.log_func, scandir, True, sabs, 1)
for zsl in (rm_ok, rm_ng): for zsl in (rm_ok, rm_ng):
@@ -4082,7 +4352,7 @@ class Up2k(object):
rem = ap[len(sabs) :].replace(os.sep, "/").lstrip("/") rem = ap[len(sabs) :].replace(os.sep, "/").lstrip("/")
vp = vjoin(dvp, rem) vp = vjoin(dvp, rem)
try: try:
dvn, drem = self.asrv.vfs.get(vp, uname, False, True) dvn, drem = self.vfs.get(vp, uname, False, True)
bos.mkdir(dvn.canonical(drem)) bos.mkdir(dvn.canonical(drem))
except: except:
pass pass
@@ -4093,10 +4363,10 @@ class Up2k(object):
self, uname: str, ip: str, svp: str, dvp: str, curs: set["sqlite3.Cursor"] self, uname: str, ip: str, svp: str, dvp: str, curs: set["sqlite3.Cursor"]
) -> str: ) -> str:
"""mutex(main) me; will mutex(reg)""" """mutex(main) me; will mutex(reg)"""
svn, srem = self.asrv.vfs.get(svp, uname, True, False, True) svn, srem = self.vfs.get(svp, uname, True, False, True)
svn, srem = svn.get_dbv(srem) svn, srem = svn.get_dbv(srem)
dvn, drem = self.asrv.vfs.get(dvp, uname, False, True) dvn, drem = self.vfs.get(dvp, uname, False, True)
dvn, drem = dvn.get_dbv(drem) dvn, drem = dvn.get_dbv(drem)
sabs = svn.canonical(srem, False) sabs = svn.canonical(srem, False)
@@ -4140,7 +4410,7 @@ class Up2k(object):
svp, svp,
"", "",
uname, uname,
self.asrv.vfs.get_perms(svp, uname), self.vfs.get_perms(svp, uname),
ftime, ftime,
fsize, fsize,
ip, ip,
@@ -4180,7 +4450,7 @@ class Up2k(object):
dvp, dvp,
"", "",
uname, uname,
self.asrv.vfs.get_perms(dvp, uname), self.vfs.get_perms(dvp, uname),
ftime, ftime,
fsize, fsize,
ip, ip,
@@ -4293,7 +4563,7 @@ class Up2k(object):
dvp, dvp,
"", "",
uname, uname,
self.asrv.vfs.get_perms(dvp, uname), self.vfs.get_perms(dvp, uname),
ftime, ftime,
fsize, fsize,
ip, ip,
@@ -4606,7 +4876,7 @@ class Up2k(object):
vp_chk, vp_chk,
job["host"], job["host"],
job["user"], job["user"],
self.asrv.vfs.get_perms(vp_chk, job["user"]), self.vfs.get_perms(vp_chk, job["user"]),
job["lmod"], job["lmod"],
job["size"], job["size"],
job["addr"], job["addr"],
@@ -4618,7 +4888,7 @@ class Up2k(object):
self.log(t, 1) self.log(t, 1)
raise Pebkac(403, t) raise Pebkac(403, t)
if hr.get("reloc"): if hr.get("reloc"):
x = pathmod(self.asrv.vfs, ap_chk, vp_chk, hr["reloc"]) x = pathmod(self.vfs, ap_chk, vp_chk, hr["reloc"])
if x: if x:
zvfs = vfs zvfs = vfs
pdir, _, job["name"], (vfs, rem) = x pdir, _, job["name"], (vfs, rem) = x
@@ -4725,7 +4995,7 @@ class Up2k(object):
def _snap_reg(self, ptop: str, reg: dict[str, dict[str, Any]]) -> None: def _snap_reg(self, ptop: str, reg: dict[str, dict[str, Any]]) -> None:
now = time.time() now = time.time()
histpath = self.asrv.vfs.histtab.get(ptop) histpath = self.vfs.histtab.get(ptop)
if not histpath: if not histpath:
return return
@@ -4973,7 +5243,7 @@ class Up2k(object):
else: else:
fvp, fn = vsplit(fvp) fvp, fn = vsplit(fvp)
x = pathmod(self.asrv.vfs, "", req_vp, {"vp": fvp, "fn": fn}) x = pathmod(self.vfs, "", req_vp, {"vp": fvp, "fn": fn})
if not x: if not x:
t = "hook_fx(%s): failed to resolve %s based on %s" t = "hook_fx(%s): failed to resolve %s based on %s"
self.log(t % (act, fvp, req_vp)) self.log(t % (act, fvp, req_vp))

View File

@@ -436,6 +436,27 @@ UNHUMANIZE_UNITS = {
VF_CAREFUL = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1} VF_CAREFUL = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1}
def read_ram() -> tuple[float, float]:
a = b = 0
try:
with open("/proc/meminfo", "rb", 0x10000) as f:
zsl = f.read(0x10000).decode("ascii", "replace").split("\n")
p = re.compile("^MemTotal:.* kB")
zs = next((x for x in zsl if p.match(x)))
a = int((int(zs.split()[1]) / 0x100000) * 100) / 100
p = re.compile("^MemAvailable:.* kB")
zs = next((x for x in zsl if p.match(x)))
b = int((int(zs.split()[1]) / 0x100000) * 100) / 100
except:
pass
return a, b
RAM_TOTAL, RAM_AVAIL = read_ram()
pybin = sys.executable or "" pybin = sys.executable or ""
if EXE: if EXE:
pybin = "" pybin = ""
@@ -1030,6 +1051,7 @@ class MTHash(object):
self.sz = 0 self.sz = 0
self.csz = 0 self.csz = 0
self.stop = False self.stop = False
self.readsz = 1024 * 1024 * (2 if (RAM_AVAIL or 2) < 1 else 12)
self.omutex = threading.Lock() self.omutex = threading.Lock()
self.imutex = threading.Lock() self.imutex = threading.Lock()
self.work_q: Queue[int] = Queue() self.work_q: Queue[int] = Queue()
@@ -1105,7 +1127,7 @@ class MTHash(object):
while chunk_rem > 0: while chunk_rem > 0:
with self.imutex: with self.imutex:
f.seek(ofs) f.seek(ofs)
buf = f.read(min(chunk_rem, 1024 * 1024 * 12)) buf = f.read(min(chunk_rem, self.readsz))
if not buf: if not buf:
raise Exception("EOF at " + str(ofs)) raise Exception("EOF at " + str(ofs))
@@ -2791,7 +2813,10 @@ def sendfile_py(
bufsz: int, bufsz: int,
slp: float, slp: float,
use_poll: bool, use_poll: bool,
dls: dict[str, tuple[float, int]],
dl_id: str,
) -> int: ) -> int:
sent = 0
remains = upper - lower remains = upper - lower
f.seek(lower) f.seek(lower)
while remains > 0: while remains > 0:
@@ -2808,6 +2833,10 @@ def sendfile_py(
except: except:
return remains return remains
if dl_id:
sent += len(buf)
dls[dl_id] = (time.time(), sent)
return 0 return 0
@@ -2820,6 +2849,8 @@ def sendfile_kern(
bufsz: int, bufsz: int,
slp: float, slp: float,
use_poll: bool, use_poll: bool,
dls: dict[str, tuple[float, int]],
dl_id: str,
) -> int: ) -> int:
out_fd = s.fileno() out_fd = s.fileno()
in_fd = f.fileno() in_fd = f.fileno()
@@ -2832,7 +2863,7 @@ def sendfile_kern(
while ofs < upper: while ofs < upper:
stuck = stuck or time.time() stuck = stuck or time.time()
try: try:
req = min(2 ** 30, upper - ofs) req = min(0x2000000, upper - ofs) # 32 MiB
if use_poll: if use_poll:
poll.poll(10000) poll.poll(10000)
else: else:
@@ -2856,13 +2887,16 @@ def sendfile_kern(
return upper - ofs return upper - ofs
ofs += n ofs += n
if dl_id:
dls[dl_id] = (time.time(), ofs - lower)
# print("sendfile: ok, sent {} now, {} total, {} remains".format(n, ofs - lower, upper - ofs)) # print("sendfile: ok, sent {} now, {} total, {} remains".format(n, ofs - lower, upper - ofs))
return 0 return 0
def statdir( def statdir(
logger: Optional["RootLogger"], scandir: bool, lstat: bool, top: str logger: Optional["RootLogger"], scandir: bool, lstat: bool, top: str, throw: bool
) -> Generator[tuple[str, os.stat_result], None, None]: ) -> Generator[tuple[str, os.stat_result], None, None]:
if lstat and ANYWIN: if lstat and ANYWIN:
lstat = False lstat = False
@@ -2898,6 +2932,12 @@ def statdir(
logger(src, "[s] {} @ {}".format(repr(ex), fsdec(abspath)), 6) logger(src, "[s] {} @ {}".format(repr(ex), fsdec(abspath)), 6)
except Exception as ex: except Exception as ex:
if throw:
zi = getattr(ex, "errno", 0)
if zi == errno.ENOENT:
raise Pebkac(404, str(ex))
raise
t = "{} @ {}".format(repr(ex), top) t = "{} @ {}".format(repr(ex), top)
if logger: if logger:
logger(src, t, 1) logger(src, t, 1)
@@ -2906,7 +2946,7 @@ def statdir(
def dir_is_empty(logger: "RootLogger", scandir: bool, top: str): def dir_is_empty(logger: "RootLogger", scandir: bool, top: str):
for _ in statdir(logger, scandir, False, top): for _ in statdir(logger, scandir, False, top, False):
return False return False
return True return True
@@ -2919,7 +2959,7 @@ def rmdirs(
top = os.path.dirname(top) top = os.path.dirname(top)
depth -= 1 depth -= 1
stats = statdir(logger, scandir, lstat, top) stats = statdir(logger, scandir, lstat, top, False)
dirs = [x[0] for x in stats if stat.S_ISDIR(x[1].st_mode)] dirs = [x[0] for x in stats if stat.S_ISDIR(x[1].st_mode)]
dirs = [os.path.join(top, x) for x in dirs] dirs = [os.path.join(top, x) for x in dirs]
ok = [] ok = []

View File

@@ -188,7 +188,6 @@ html.y {
--srv-1: #555; --srv-1: #555;
--srv-2: #c83; --srv-2: #c83;
--srv-3: #c0a; --srv-3: #c0a;
--srv-3b: rgba(255,68,204,0.6);
--tree-bg: #fff; --tree-bg: #fff;
@@ -286,6 +285,7 @@ html.bz {
--f-h-b1: #34384e; --f-h-b1: #34384e;
--mp-sh: #11121d; --mp-sh: #11121d;
/*--mp-b-bg: #2c3044;*/ /*--mp-b-bg: #2c3044;*/
--f-play-bg: var(--btn-1-bg);
} }
html.by { html.by {
--bg: #f2f2f2; --bg: #f2f2f2;
@@ -389,8 +389,6 @@ html.cy {
} }
html.dz { html.dz {
--fg: #4d4; --fg: #4d4;
--fg-max: #fff;
--fg2-max: #fff;
--fg-weak: #2a2; --fg-weak: #2a2;
--bg-u6: #020; --bg-u6: #020;
@@ -400,11 +398,9 @@ html.dz {
--bg-u2: #020; --bg-u2: #020;
--bg-u1: #020; --bg-u1: #020;
--bg: #010; --bg: #010;
--bgg: var(--bg);
--bg-d1: #000; --bg-d1: #000;
--bg-d2: #020; --bg-d2: #020;
--bg-d3: #000; --bg-d3: #000;
--bg-max: #000;
--tab-alt: #6f6; --tab-alt: #6f6;
--row-alt: #030; --row-alt: #030;
@@ -417,45 +413,21 @@ html.dz {
--a-dark: #afa; --a-dark: #afa;
--a-gray: #2a2; --a-gray: #2a2;
--btn-fg: var(--a);
--btn-bg: rgba(64,128,64,0.15); --btn-bg: rgba(64,128,64,0.15);
--btn-h-fg: var(--a-hil);
--btn-h-bg: #050; --btn-h-bg: #050;
--btn-1-fg: #000; --btn-1-fg: #000;
--btn-1-bg: #4f4; --btn-1-bg: #4f4;
--btn-1h-fg: var(--btn-1-fg);
--btn-1h-bg: #3f3; --btn-1h-bg: #3f3;
--btn-bs: 0 0 0 .1em #080 inset; --btn-bs: 0 0 0 .1em #080 inset;
--btn-1-bs: a; --btn-1-bs: a;
--chk-fg: var(--tab-alt);
--txt-sh: var(--bg-d2);
--txt-bg: var(--btn-bg);
--op-aa-fg: var(--a);
--op-aa-bg: var(--bg-d2);
--op-a-sh: rgba(0,0,0,0.5);
--u2-btn-b1: var(--fg-weak); --u2-btn-b1: var(--fg-weak);
--u2-sbtn-b1: var(--fg-weak); --u2-sbtn-b1: var(--fg-weak);
--u2-txt-bg: var(--bg-u5);
--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));
--u2-tab-b1: var(--fg-weak); --u2-tab-b1: var(--fg-weak);
--u2-tab-1-fg: #fff; --u2-tab-1-fg: #fff;
--u2-tab-1-bg: linear-gradient(to bottom, #151, var(--bg) 80%); --u2-tab-1-bg: linear-gradient(to bottom, #151, var(--bg) 80%);
--u2-tab-1-b1: #7c5;
--u2-tab-1-b2: #583;
--u2-tab-1-sh: #280;
--u2-b-fg: #fff;
--u2-b1-bg: #3a3; --u2-b1-bg: #3a3;
--u2-b2-bg: #3a3; --u2-b2-bg: #3a3;
--u2-inf-bg: #07a;
--u2-inf-b1: #0be;
--u2-ok-bg: #380;
--u2-ok-b1: #8e4;
--u2-err-bg: #900;
--u2-err-b1: #d06;
--ud-b1: #888;
--sort-1: #fff; --sort-1: #fff;
--sort-2: #3f3; --sort-2: #3f3;
@@ -467,47 +439,12 @@ html.dz {
--tree-bg: #010; --tree-bg: #010;
--g-play-bg: #750;
--g-play-b1: #c90;
--g-play-b2: #da4;
--g-play-sh: #b83;
--g-sel-fg: #fff;
--g-sel-bg: #925;
--g-sel-b1: #c37; --g-sel-b1: #c37;
--g-sel-sh: #b36; --g-sel-sh: #b36;
--g-fsel-bg: #d39;
--g-fsel-b1: #d48; --g-fsel-b1: #d48;
--g-fsel-ts: #804;
--g-fg: var(--a-hil);
--g-bg: var(--bg-u2);
--g-b1: var(--bg-u4);
--g-b2: var(--bg-u5);
--g-g1: var(--bg-u2);
--g-g2: var(--bg-u5);
--g-f-bg: var(--bg-u4);
--g-f-b1: var(--bg-u5);
--g-f-fg: var(--a-hil);
--g-sh: rgba(0,0,0,0.3);
--f-sh1: 0.33;
--f-sh2: 0.02;
--f-sh3: 0.2;
--f-h-b1: #3b3; --f-h-b1: #3b3;
--f-play-bg: #fc5;
--f-play-fg: #000;
--f-sel-sh: #fc0;
--f-gray: #999;
--fm-off: #f6c;
--mp-sh: var(--bg-d3);
--err-fg: #fff;
--err-bg: #a20;
--err-b1: #f00;
--err-ts: #500;
text-shadow: none; text-shadow: none;
font-family: 'scp', monospace, monospace; font-family: 'scp', monospace, monospace;
font-family: var(--font-mono), 'scp', monospace, monospace; font-family: var(--font-mono), 'scp', monospace, monospace;
@@ -1710,6 +1647,18 @@ html.dz .btn {
background: var(--btn-1-bg); background: var(--btn-1-bg);
text-shadow: none; text-shadow: none;
} }
#tree ul a.ld::before {
font-weight: bold;
font-family: sans-serif;
display: inline-block;
text-align: center;
width: 1em;
margin: 0 .3em 0 -1.3em;
color: var(--fg-max);
opacity: 0;
content: '◠';
animation: .5s linear infinite forwards spin, ease .25s 1 forwards fadein;
}
#tree ul a.par { #tree ul a.par {
color: var(--fg-max); color: var(--fg-max);
} }
@@ -1931,11 +1880,10 @@ html.y #tree.nowrap .ntree a+a:hover {
#rn_f.m td+td { #rn_f.m td+td {
width: 50%; width: 50%;
} }
#rn_f .err td { #rn_f .err td,
background: var(--err-bg); #rn_f .err input[readonly],
color: var(--fg-max); #rui .ng input[readonly] {
} color: var(--err-fg);
#rn_f .err input[readonly] {
background: var(--err-bg); background: var(--err-bg);
} }
#rui input[readonly] { #rui input[readonly] {

View File

@@ -37,8 +37,9 @@ var Ls = {
["T", "toggle thumbnails / icons"], ["T", "toggle thumbnails / icons"],
["🡅 A/D", "thumbnail size"], ["🡅 A/D", "thumbnail size"],
["ctrl-K", "delete selected"], ["ctrl-K", "delete selected"],
["ctrl-X", "cut selected"], ["ctrl-X", "cut selection to clipboard"],
["ctrl-V", "paste into folder"], ["ctrl-C", "copy selection to clipboard"],
["ctrl-V", "paste (move/copy) here"],
["Y", "download selected"], ["Y", "download selected"],
["F2", "rename selected"], ["F2", "rename selected"],
@@ -83,7 +84,7 @@ var Ls = {
["I/K", "prev/next file"], ["I/K", "prev/next file"],
["M", "close textfile"], ["M", "close textfile"],
["E", "edit textfile"], ["E", "edit textfile"],
["S", "select file (for cut/rename)"], ["S", "select file (for cut/copy/rename)"],
] ]
], ],
@@ -133,6 +134,7 @@ var Ls = {
"wt_ren": "rename selected items$NHotkey: F2", "wt_ren": "rename selected items$NHotkey: F2",
"wt_del": "delete selected items$NHotkey: ctrl-K", "wt_del": "delete selected items$NHotkey: ctrl-K",
"wt_cut": "cut selected items &lt;small&gt;(then paste somewhere else)&lt;/small&gt;$NHotkey: ctrl-X", "wt_cut": "cut selected items &lt;small&gt;(then paste somewhere else)&lt;/small&gt;$NHotkey: ctrl-X",
"wt_cpy": "copy selected items to clipboard$N(to paste them somewhere else)$NHotkey: ctrl-C",
"wt_pst": "paste a previously cut / copied selection$NHotkey: ctrl-V", "wt_pst": "paste a previously cut / copied selection$NHotkey: ctrl-V",
"wt_selall": "select all files$NHotkey: ctrl-A (when file focused)", "wt_selall": "select all files$NHotkey: ctrl-A (when file focused)",
"wt_selinv": "invert selection", "wt_selinv": "invert selection",
@@ -327,6 +329,7 @@ var Ls = {
"fr_emore": "select at least one item to rename", "fr_emore": "select at least one item to rename",
"fd_emore": "select at least one item to delete", "fd_emore": "select at least one item to delete",
"fc_emore": "select at least one item to cut", "fc_emore": "select at least one item to cut",
"fcp_emore": "select at least one item to copy to clipboard",
"fs_sc": "share the folder you're in", "fs_sc": "share the folder you're in",
"fs_ss": "share the selected files", "fs_ss": "share the selected files",
@@ -379,16 +382,28 @@ var Ls = {
"fc_ok": "cut {0} items", "fc_ok": "cut {0} items",
"fc_warn": 'cut {0} items\n\nbut: only <b>this</b> browser-tab can paste them\n(since the selection is so absolutely massive)', "fc_warn": 'cut {0} items\n\nbut: only <b>this</b> browser-tab can paste them\n(since the selection is so absolutely massive)',
"fp_ecut": "first cut some files / folders to paste / move\n\nnote: you can cut / paste across different browser tabs", "fcc_ok": "copied {0} items to clipboard",
"fp_ename": "these {0} items cannot be moved here (names already exist):", "fcc_warn": 'copied {0} items to clipboard\n\nbut: only <b>this</b> browser-tab can paste them\n(since the selection is so absolutely massive)',
"fp_apply": "use these names",
"fp_ecut": "first cut or copy some files / folders to paste / move\n\nnote: you can cut / paste across different browser tabs",
"fp_ename": "{0} items cannot be moved here because the names are already taken. Give them new names below to continue, or blank the name to skip them:",
"fcp_ename": "{0} items cannot be copied here because the names are already taken. Give them new names below to continue, or blank the name to skip them:",
"fp_emore": "there are still some filename collisions left to fix",
"fp_ok": "move OK", "fp_ok": "move OK",
"fcp_ok": "copy OK",
"fp_busy": "moving {0} items...\n\n{1}", "fp_busy": "moving {0} items...\n\n{1}",
"fcp_busy": "copying {0} items...\n\n{1}",
"fp_err": "move failed:\n", "fp_err": "move failed:\n",
"fcp_err": "copy failed:\n",
"fp_confirm": "move these {0} items here?", "fp_confirm": "move these {0} items here?",
"fcp_confirm": "copy these {0} items here?",
"fp_etab": 'failed to read clipboard from other browser tab', "fp_etab": 'failed to read clipboard from other browser tab',
"fp_name": "uploading a file from your device. Give it a name:", "fp_name": "uploading a file from your device. Give it a name:",
"fp_both_m": '<h6>choose what to paste</h6><code>Enter</code> = Move {0} files from «{1}»\n<code>ESC</code> = Upload {2} files from your device', "fp_both_m": '<h6>choose what to paste</h6><code>Enter</code> = Move {0} files from «{1}»\n<code>ESC</code> = Upload {2} files from your device',
"fcp_both_m": '<h6>choose what to paste</h6><code>Enter</code> = Copy {0} files from «{1}»\n<code>ESC</code> = Upload {2} files from your device',
"fp_both_b": '<a href="#" id="modal-ok">Move</a><a href="#" id="modal-ng">Upload</a>', "fp_both_b": '<a href="#" id="modal-ok">Move</a><a href="#" id="modal-ng">Upload</a>',
"fcp_both_b": '<a href="#" id="modal-ok">Copy</a><a href="#" id="modal-ng">Upload</a>',
"mk_noname": "type a name into the text field on the left before you do that :p", "mk_noname": "type a name into the text field on the left before you do that :p",
@@ -400,7 +415,7 @@ var Ls = {
"tvt_dl": "download this file$NHotkey: Y\">💾 download", "tvt_dl": "download this file$NHotkey: Y\">💾 download",
"tvt_prev": "show previous document$NHotkey: i\">⬆ prev", "tvt_prev": "show previous document$NHotkey: i\">⬆ prev",
"tvt_next": "show next document$NHotkey: K\">⬇ next", "tvt_next": "show next document$NHotkey: K\">⬇ next",
"tvt_sel": "select file &nbsp; ( for cut / delete / ... )$NHotkey: S\">sel", "tvt_sel": "select file &nbsp; ( for cut / copy / delete / ... )$NHotkey: S\">sel",
"tvt_edit": "open file in text editor$NHotkey: E\">✏️ edit", "tvt_edit": "open file in text editor$NHotkey: E\">✏️ edit",
"gt_vau": "don't show videos, just play the audio\">🎧", "gt_vau": "don't show videos, just play the audio\">🎧",
@@ -605,8 +620,9 @@ var Ls = {
["T", "miniatyrbilder på/av"], ["T", "miniatyrbilder på/av"],
["🡅 A/D", "ikonstørrelse"], ["🡅 A/D", "ikonstørrelse"],
["ctrl-K", "slett valgte"], ["ctrl-K", "slett valgte"],
["ctrl-X", "klipp ut"], ["ctrl-X", "klipp ut valgte"],
["ctrl-V", "lim inn"], ["ctrl-C", "kopiér til utklippstavle"],
["ctrl-V", "lim inn (flytt/kopiér)"],
["Y", "last ned valgte"], ["Y", "last ned valgte"],
["F2", "endre navn på valgte"], ["F2", "endre navn på valgte"],
@@ -702,7 +718,8 @@ var Ls = {
"wt_ren": "gi nye navn til de valgte filene$NSnarvei: F2", "wt_ren": "gi nye navn til de valgte filene$NSnarvei: F2",
"wt_del": "slett de valgte filene$NSnarvei: ctrl-K", "wt_del": "slett de valgte filene$NSnarvei: ctrl-K",
"wt_cut": "klipp ut de valgte filene &lt;small&gt;(for å lime inn et annet sted)&lt;/small&gt;$NSnarvei: ctrl-X", "wt_cut": "klipp ut de valgte filene &lt;small&gt;(for å lime inn et annet sted)&lt;/small&gt;$NSnarvei: ctrl-X",
"wt_pst": "lim inn filer (som tidligere ble klippet ut et annet sted)$NSnarvei: ctrl-V", "wt_cpy": "kopiér de valgte filene til utklippstavlen$N(for å lime inn et annet sted)$NSnarvei: ctrl-C",
"wt_pst": "lim inn filer (som tidligere ble klippet ut / kopiert et annet sted)$NSnarvei: ctrl-V",
"wt_selall": "velg alle filer$NSnarvei: ctrl-A (mens fokus er på en fil)", "wt_selall": "velg alle filer$NSnarvei: ctrl-A (mens fokus er på en fil)",
"wt_selinv": "inverter utvalg", "wt_selinv": "inverter utvalg",
"wt_selzip": "last ned de valgte filene som et arkiv", "wt_selzip": "last ned de valgte filene som et arkiv",
@@ -845,7 +862,7 @@ var Ls = {
"mt_oscv": "vis album-cover på infoskjermen\">bilde", "mt_oscv": "vis album-cover på infoskjermen\">bilde",
"mt_follow": "bla slik at sangen som spilles alltid er synlig\">🎯", "mt_follow": "bla slik at sangen som spilles alltid er synlig\">🎯",
"mt_compact": "tettpakket avspillerpanel\">⟎", "mt_compact": "tettpakket avspillerpanel\">⟎",
"mt_uncache": "prøv denne hvis en sang ikke spiller riktig\">uncache", "mt_uncache": "prøv denne hvis en sang ikke spiller riktig\">oppfrisk",
"mt_mloop": "repeter hele mappen\">🔁 gjenta", "mt_mloop": "repeter hele mappen\">🔁 gjenta",
"mt_mnext": "hopp til neste mappe og fortsett\">📂 neste", "mt_mnext": "hopp til neste mappe og fortsett\">📂 neste",
"mt_cflac": "konverter flac / wav-filer til opus\">flac", "mt_cflac": "konverter flac / wav-filer til opus\">flac",
@@ -896,6 +913,7 @@ var Ls = {
"fr_emore": "velg minst én fil som skal få nytt navn", "fr_emore": "velg minst én fil som skal få nytt navn",
"fd_emore": "velg minst én fil som skal slettes", "fd_emore": "velg minst én fil som skal slettes",
"fc_emore": "velg minst én fil som skal klippes ut", "fc_emore": "velg minst én fil som skal klippes ut",
"fcp_emore": "velg minst én fil som skal kopieres til utklippstavlen",
"fs_sc": "del mappen du er i nå", "fs_sc": "del mappen du er i nå",
"fs_ss": "del de valgte filene", "fs_ss": "del de valgte filene",
@@ -948,16 +966,28 @@ var Ls = {
"fc_ok": "klippet ut {0} filer", "fc_ok": "klippet ut {0} filer",
"fc_warn": 'klippet ut {0} filer\n\nmen: kun <b>denne</b> nettleserfanen har mulighet til å lime dem inn et annet sted, siden antallet filer er helt hinsides', "fc_warn": 'klippet ut {0} filer\n\nmen: kun <b>denne</b> nettleserfanen har mulighet til å lime dem inn et annet sted, siden antallet filer er helt hinsides',
"fp_ecut": "du må klippe ut noen filer / mapper først\n\nmerk: du kan gjerne jobbe på kryss av nettleserfaner; klippe ut i én fane, lime inn i en annen", "fcc_ok": "kopierte {0} filer til utklippstavlen",
"fp_ename": "disse {0} filene kan ikke flyttes til målmappen fordi det allerede finnes filer med samme navn:", "fcc_warn": 'kopierte {0} filer til utklippstavlen\n\nmen: kun <b>denne</b> nettleserfanen har mulighet til å lime dem inn et annet sted, siden antallet filer er helt hinsides',
"fp_apply": "bekreft og lim inn nå",
"fp_ecut": "du må klippe ut eller kopiere noen filer / mapper først\n\nmerk: du kan gjerne jobbe på kryss av nettleserfaner; klippe ut i én fane, lime inn i en annen",
"fp_ename": "{0} filer kan ikke flyttes til målmappen fordi det allerede finnes filer med samme navn. Gi dem nye navn nedenfor, eller gi dem et blankt navn for å hoppe over dem:",
"fcp_ename": "{0} filer kan ikke kopieres til målmappen fordi det allerede finnes filer med samme navn. Gi dem nye navn nedenfor, eller gi dem et blankt navn for å hoppe over dem:",
"fp_emore": "det er fortsatt flere navn som må endres",
"fp_ok": "flytting OK", "fp_ok": "flytting OK",
"fcp_ok": "kopiering OK",
"fp_busy": "flytter {0} filer...\n\n{1}", "fp_busy": "flytter {0} filer...\n\n{1}",
"fcp_busy": "kopierer {0} filer...\n\n{1}",
"fp_err": "flytting feilet:\n", "fp_err": "flytting feilet:\n",
"fcp_err": "kopiering feilet:\n",
"fp_confirm": "flytt disse {0} filene hit?", "fp_confirm": "flytt disse {0} filene hit?",
"fcp_confirm": "kopiér disse {0} filene hit?",
"fp_etab": 'kunne ikke lese listen med filer ifra den andre nettleserfanen', "fp_etab": 'kunne ikke lese listen med filer ifra den andre nettleserfanen',
"fp_name": "Laster opp én fil fra enheten din. Velg filnavn:", "fp_name": "Laster opp én fil fra enheten din. Velg filnavn:",
"fp_both_m": '<h6>hva skal limes inn her?</h6><code>Enter</code> = Flytt {0} filer fra «{1}»\n<code>ESC</code> = Last opp {2} filer fra enheten din', "fp_both_m": '<h6>hva skal limes inn her?</h6><code>Enter</code> = Flytt {0} filer fra «{1}»\n<code>ESC</code> = Last opp {2} filer fra enheten din',
"fcp_both_m": '<h6>hva skal limes inn her?</h6><code>Enter</code> = Kopiér {0} filer fra «{1}»\n<code>ESC</code> = Last opp {2} filer fra enheten din',
"fp_both_b": '<a href="#" id="modal-ok">Flytt</a><a href="#" id="modal-ng">Last opp</a>', "fp_both_b": '<a href="#" id="modal-ok">Flytt</a><a href="#" id="modal-ng">Last opp</a>',
"fcp_both_b": '<a href="#" id="modal-ok">Kopiér</a><a href="#" id="modal-ng">Last opp</a>',
"mk_noname": "skriv inn et navn i tekstboksen til venstre først :p", "mk_noname": "skriv inn et navn i tekstboksen til venstre først :p",
@@ -1176,6 +1206,7 @@ var Ls = {
["🡅 A/D", "缩略图大小"], ["🡅 A/D", "缩略图大小"],
["ctrl-K", "删除选中项"], ["ctrl-K", "删除选中项"],
["ctrl-X", "剪切选中项"], ["ctrl-X", "剪切选中项"],
["ctrl-C", "复制选中项"], //m
["ctrl-V", "粘贴到文件夹"], ["ctrl-V", "粘贴到文件夹"],
["Y", "下载选中项"], ["Y", "下载选中项"],
["F2", "重命名选中项"], ["F2", "重命名选中项"],
@@ -1271,6 +1302,7 @@ var Ls = {
"wt_ren": "重命名选中的项目$N快捷键: F2", "wt_ren": "重命名选中的项目$N快捷键: F2",
"wt_del": "删除选中的项目$N快捷键: ctrl-K", "wt_del": "删除选中的项目$N快捷键: ctrl-K",
"wt_cut": "剪切选中的项目&lt;small&gt;(然后粘贴到其他地方)&lt;/small&gt;$N快捷键: ctrl-X", "wt_cut": "剪切选中的项目&lt;small&gt;(然后粘贴到其他地方)&lt;/small&gt;$N快捷键: ctrl-X",
"wt_cpy": "将选中的项目复制到剪贴板&lt;small&gt;(然后粘贴到其他地方)&lt;/small&gt;$N快捷键: ctrl-C", //m
"wt_pst": "粘贴之前剪切/复制的选择$N快捷键: ctrl-V", "wt_pst": "粘贴之前剪切/复制的选择$N快捷键: ctrl-V",
"wt_selall": "选择所有文件$N快捷键: ctrl-A当文件被聚焦时", "wt_selall": "选择所有文件$N快捷键: ctrl-A当文件被聚焦时",
"wt_selinv": "反转选择", "wt_selinv": "反转选择",
@@ -1465,6 +1497,7 @@ var Ls = {
"fr_emore": "选择至少一个项目以重命名", "fr_emore": "选择至少一个项目以重命名",
"fd_emore": "选择至少一个项目以删除", "fd_emore": "选择至少一个项目以删除",
"fc_emore": "选择至少一个项目以剪切", "fc_emore": "选择至少一个项目以剪切",
"fcp_emore": "选择至少一个要复制到剪贴板的项目", //m
"fs_sc": "分享你所在的文件夹", "fs_sc": "分享你所在的文件夹",
"fs_ss": "分享选定的文件", "fs_ss": "分享选定的文件",
@@ -1517,16 +1550,28 @@ var Ls = {
"fc_ok": "剪切 {0} 项", "fc_ok": "剪切 {0} 项",
"fc_warn": '剪切 {0} 项\n\n但只有 <b>这个</b> 浏览器标签页可以粘贴它们\n因为选择非常庞大', "fc_warn": '剪切 {0} 项\n\n但只有 <b>这个</b> 浏览器标签页可以粘贴它们\n因为选择非常庞大',
"fp_ecut": "首先剪切一些文件/文件夹以粘贴/移动\n\n注意你可以在不同的浏览器标签页之间剪切/粘贴", "fcc_ok": "已将 {0} 项复制到剪贴板", //m
"fp_ename": "这些 {0} 项不能移动到这里(名称已存在):", "fcc_warn": '已将 {0} 项复制到剪贴板\n\n但只有 <b>这个</b> 浏览器标签页可以粘贴它们\n因为选择非常庞大', //m
"fp_apply": "确认并立即粘贴", //m
"fp_ecut": "首先剪切或复制一些文件/文件夹以粘贴/移动\n\n注意你可以在不同的浏览器标签页之间剪切/粘贴", //m
"fp_ename": "{0} 项不能移动到这里,因为名称已被占用。请在下方输入新名称以继续,或将名称留空以跳过这些项:", //m
"fcp_ename": "{0} 项不能复制到这里,因为名称已被占用。请在下方输入新名称以继续,或将名称留空以跳过这些项:", //m
"fp_emore": "还有一些文件名冲突需要解决", //m
"fp_ok": "移动成功", "fp_ok": "移动成功",
"fcp_ok": "复制成功", //m
"fp_busy": "正在移动 {0} 项...\n\n{1}", "fp_busy": "正在移动 {0} 项...\n\n{1}",
"fcp_busy": "正在复制 {0} 项...\n\n{1}", //m
"fp_err": "移动失败:\n", "fp_err": "移动失败:\n",
"fcp_err": "复制失败:\n", //m
"fp_confirm": "将这些 {0} 项移动到这里?", "fp_confirm": "将这些 {0} 项移动到这里?",
"fcp_confirm": "将这些 {0} 项复制到这里?", //m
"fp_etab": '无法从其他浏览器标签页读取剪贴板', "fp_etab": '无法从其他浏览器标签页读取剪贴板',
"fp_name": "从你的设备上传一个文件。给它一个名字:", "fp_name": "从你的设备上传一个文件。给它一个名字:",
"fp_both_m": '<h6>选择粘贴内容</h6><code>Enter</code> = 从 «{1}» 移动 {0} 个文件\n<code>ESC</code> = 从你的设备上传 {2} 个文件', "fp_both_m": '<h6>选择粘贴内容</h6><code>Enter</code> = 从 «{1}» 移动 {0} 个文件\n<code>ESC</code> = 从你的设备上传 {2} 个文件',
"fcp_both_m": '<h6>选择粘贴内容</h6><code>Enter</code> = 从 «{1}» 复制 {0} 个文件\n<code>ESC</code> = 从你的设备上传 {2} 个文件', //m
"fp_both_b": '<a href="#" id="modal-ok">移动</a><a href="#" id="modal-ng">上传</a>', "fp_both_b": '<a href="#" id="modal-ok">移动</a><a href="#" id="modal-ng">上传</a>',
"fcp_both_b": '<a href="#" id="modal-ok">复制</a><a href="#" id="modal-ng">上传</a>', //m
"mk_noname": "在左侧文本框中输入名称,然后再执行此操作 :p", "mk_noname": "在左侧文本框中输入名称,然后再执行此操作 :p",
@@ -1771,6 +1816,7 @@ ebi('widget').innerHTML = (
' href="#" id="fren" tt="' + L.wt_ren + '">✎<span>name</span></a><a' + ' href="#" id="fren" tt="' + L.wt_ren + '">✎<span>name</span></a><a' +
' href="#" id="fdel" tt="' + L.wt_del + '">⌫<span>del.</span></a><a' + ' href="#" id="fdel" tt="' + L.wt_del + '">⌫<span>del.</span></a><a' +
' href="#" id="fcut" tt="' + L.wt_cut + '">✂<span>cut</span></a><a' + ' href="#" id="fcut" tt="' + L.wt_cut + '">✂<span>cut</span></a><a' +
' href="#" id="fcpy" tt="' + L.wt_cpy + '">⧉<span>copy</span></a><a' +
' href="#" id="fpst" tt="' + L.wt_pst + '">📋<span>paste</span></a>' + ' href="#" id="fpst" tt="' + L.wt_pst + '">📋<span>paste</span></a>' +
'</span><span id="wzip"><a' + '</span><span id="wzip"><a' +
' href="#" id="selall" tt="' + L.wt_selall + '">sel.<br />all</a><a' + ' href="#" id="selall" tt="' + L.wt_selall + '">sel.<br />all</a><a' +
@@ -2118,8 +2164,9 @@ function set_files_html(html) {
// actx breaks background album playback on ios // actx breaks background album playback on ios
var ACtx = !IPHONE && (window.AudioContext || window.webkitAudioContext), var ACtx = !IPHONE && (window.AudioContext || window.webkitAudioContext),
ACB = sread('au_cbv') || 1, ACB = sread('au_cbv') || 1,
noih = /[?&]v\b/.exec('' + location),
hash0 = location.hash, hash0 = location.hash,
sloc0 = '' + location,
noih = /[?&]v\b/.exec(sloc0),
ldks = [], ldks = [],
dks = {}, dks = {},
dk, mp; dk, mp;
@@ -4091,6 +4138,12 @@ function eval_hash() {
if (!im) if (!im)
return toast.warn(10, L.im_hnf); return toast.warn(10, L.im_hnf);
if (thegrid.sel)
setTimeout(function () {
thegrid.sel = true;
}, 1);
thegrid.sel = false;
im.click(); im.click();
im.scrollIntoView(); im.scrollIntoView();
}, 50); }, 50);
@@ -4376,6 +4429,7 @@ var fileman = (function () {
var bren = ebi('fren'), var bren = ebi('fren'),
bdel = ebi('fdel'), bdel = ebi('fdel'),
bcut = ebi('fcut'), bcut = ebi('fcut'),
bcpy = ebi('fcpy'),
bpst = ebi('fpst'), bpst = ebi('fpst'),
bshr = ebi('fshr'), bshr = ebi('fshr'),
t_paste, t_paste,
@@ -4388,14 +4442,19 @@ var fileman = (function () {
catch (ex) { } catch (ex) { }
r.render = function () { r.render = function () {
if (r.clip === null) if (r.clip === null) {
r.clip = jread('fman_clip', []).slice(1); r.clip = jread('fman_clip', []).slice(1);
r.ccp = r.clip.length && r.clip[0] == '//c';
if (r.ccp)
r.clip.shift();
}
var sel = msel.getsel(), var sel = msel.getsel(),
nsel = sel.length, nsel = sel.length,
enren = nsel, enren = nsel,
endel = nsel, endel = nsel,
encut = nsel, encut = nsel,
encpy = nsel,
enpst = r.clip && r.clip.length, enpst = r.clip && r.clip.length,
hren = !(have_mv && has(perms, 'write') && has(perms, 'move')), hren = !(have_mv && has(perms, 'write') && has(perms, 'move')),
hdel = !(have_del && has(perms, 'delete')), hdel = !(have_del && has(perms, 'delete')),
@@ -4409,6 +4468,7 @@ var fileman = (function () {
clmod(bren, 'en', enren); clmod(bren, 'en', enren);
clmod(bdel, 'en', endel); clmod(bdel, 'en', endel);
clmod(bcut, 'en', encut); clmod(bcut, 'en', encut);
clmod(bcpy, 'en', encpy);
clmod(bpst, 'en', enpst); clmod(bpst, 'en', enpst);
clmod(bshr, 'en', 1); clmod(bshr, 'en', 1);
@@ -4703,9 +4763,9 @@ var fileman = (function () {
var html = sel.length > 1 ? ['<div>'] : [ var html = sel.length > 1 ? ['<div>'] : [
'<div>', '<div>',
'<button class="rn_dec" n="0" tt="' + L.frt_dec + '</button>', '<button class="rn_dec" id="rn_dec_0" tt="' + L.frt_dec + '</button>',
'//', '//',
'<button class="rn_reset" n="0" tt="' + L.frt_rst + '</button>' '<button class="rn_reset" id="rn_reset_0" tt="' + L.frt_rst + '</button>'
]; ];
html = html.concat([ html = html.concat([
@@ -4732,8 +4792,8 @@ var fileman = (function () {
if (sel.length == 1) if (sel.length == 1)
html.push( html.push(
'<div><table id="rn_f">\n' + '<div><table id="rn_f">\n' +
'<tr><td>old:</td><td><input type="text" id="rn_old" n="0" readonly /></td></tr>\n' + '<tr><td>old:</td><td><input type="text" id="rn_old_0" readonly /></td></tr>\n' +
'<tr><td>new:</td><td><input type="text" id="rn_new" n="0" /></td></tr>'); '<tr><td>new:</td><td><input type="text" id="rn_new_0" /></td></tr>');
else { else {
html.push( html.push(
'<div><table id="rn_f" class="m">' + '<div><table id="rn_f" class="m">' +
@@ -4742,10 +4802,10 @@ var fileman = (function () {
html.push( html.push(
'<tr><td>' + '<tr><td>' +
(cheap ? '</td>' : (cheap ? '</td>' :
'<button class="rn_dec" n="' + a + '">decode</button>' + '<button class="rn_dec" id="rn_dec_' + a + '">decode</button>' +
'<button class="rn_reset" n="' + a + '">' + t_rst + '</button></td>') + '<button class="rn_reset" id="rn_reset_' + a + '">' + t_rst + '</button></td>') +
'<td><input type="text" id="rn_new" n="' + a + '" /></td>' + '<td><input type="text" id="rn_new_' + a + '" /></td>' +
'<td><input type="text" id="rn_old" n="' + a + '" readonly /></td></tr>'); '<td><input type="text" id="rn_old_' + a + '" readonly /></td></tr>');
} }
html.push('</table></div>'); html.push('</table></div>');
@@ -4759,9 +4819,8 @@ var fileman = (function () {
rui.innerHTML = html.join('\n'); rui.innerHTML = html.join('\n');
for (var a = 0; a < f.length; a++) { for (var a = 0; a < f.length; a++) {
var k = '[n="' + a + '"]'; f[a].iold = ebi('rn_old_' + a);
f[a].iold = QS('#rn_old' + k); f[a].inew = ebi('rn_new_' + a);
f[a].inew = QS('#rn_new' + k);
f[a].inew.value = f[a].iold.value = f[a].ofn; f[a].inew.value = f[a].iold.value = f[a].ofn;
if (!cheap) if (!cheap)
@@ -4772,11 +4831,11 @@ var fileman = (function () {
if (kc.endsWith('Enter')) if (kc.endsWith('Enter'))
return rn_apply(); return rn_apply();
}; };
QS('.rn_dec' + k).onclick = function (e) { ebi('rn_dec_' + a).onclick = function (e) {
ev(e); ev(e);
f[a].inew.value = uricom_dec(f[a].inew.value); f[a].inew.value = uricom_dec(f[a].inew.value);
}; };
QS('.rn_reset' + k).onclick = function (e) { ebi('rn_reset_' + a).onclick = function (e) {
ev(e); ev(e);
rn_reset(a); rn_reset(a);
}; };
@@ -4819,6 +4878,9 @@ var fileman = (function () {
inew = ebi('rn_pnew'), inew = ebi('rn_pnew'),
defp = '$lpad((tn),2,0). [(artist) - ](title).(ext)'; defp = '$lpad((tn),2,0). [(artist) - ](title).(ext)';
ire.value = sread('cpp_rn_re') || '';
ifmt.value = sread('cpp_rn_fmt') || '';
var presets = {}; var presets = {};
presets[defp] = ['', defp]; presets[defp] = ['', defp];
presets = jread("rn_pre", presets); presets = jread("rn_pre", presets);
@@ -4909,6 +4971,8 @@ var fileman = (function () {
function rn_apply(e) { function rn_apply(e) {
ev(e); ev(e);
swrite('cpp_rn_re', ire.value);
swrite('cpp_rn_fmt', ifmt.value);
if (r.win || r.slash) { if (r.win || r.slash) {
var changed = 0; var changed = 0;
for (var a = 0; a < f.length; a++) { for (var a = 0; a < f.length; a++) {
@@ -4958,7 +5022,6 @@ var fileman = (function () {
}; };
r.delete = function (e) { r.delete = function (e) {
ev(e);
var sel = msel.getsel(), var sel = msel.getsel(),
vps = []; vps = [];
@@ -4968,6 +5031,8 @@ var fileman = (function () {
if (!sel.length) if (!sel.length)
return toast.err(3, L.fd_emore); return toast.err(3, L.fd_emore);
ev(e);
if (clgot(bdel, 'hide')) if (clgot(bdel, 'hide'))
return toast.err(3, L.fd_eperm); return toast.err(3, L.fd_eperm);
@@ -5007,13 +5072,15 @@ var fileman = (function () {
}; };
r.cut = function (e) { r.cut = function (e) {
ev(e);
var sel = msel.getsel(), var sel = msel.getsel(),
vps = []; stamp = Date.now(),
vps = [stamp];
if (!sel.length) if (!sel.length)
return toast.err(3, L.fc_emore); return toast.err(3, L.fc_emore);
ev(e);
if (clgot(bcut, 'hide')) if (clgot(bcut, 'hide'))
return toast.err(3, L.fc_eperm); return toast.err(3, L.fc_eperm);
@@ -5040,9 +5107,11 @@ var fileman = (function () {
catch (ex) { } catch (ex) { }
}, 1); }, 1);
r.ccp = false;
r.clip = vps.slice(1);
try { try {
var stamp = Date.now(); vps = JSON.stringify(vps);
vps = JSON.stringify([stamp].concat(vps));
if (vps.length > 1024 * 1024) if (vps.length > 1024 * 1024)
throw 'a'; throw 'a';
@@ -5056,6 +5125,60 @@ var fileman = (function () {
} }
}; };
r.cpy = function (e) {
var sel = msel.getsel(),
stamp = Date.now(),
vps = [stamp, '//c'];
if (!sel.length)
return toast.err(3, L.fcp_emore);
ev(e);
var els = [], griden = thegrid.en;
for (var a = 0; a < sel.length; a++) {
vps.push(sel[a].vp);
if (sel.length < 100)
try {
if (griden)
els.push(QS('#ggrid>a[ref="' + sel[a].id + '"]'));
else
els.push(ebi(sel[a].id).closest('tr'));
clmod(els[a], 'fcut');
}
catch (ex) { }
}
setTimeout(function () {
try {
for (var a = 0; a < els.length; a++)
clmod(els[a], 'fcut', 1);
}
catch (ex) { }
}, 1);
if (vps.length < 3)
vps.pop();
r.ccp = true;
r.clip = vps.slice(2);
try {
vps = JSON.stringify(vps);
if (vps.length > 1024 * 1024)
throw 'a';
swrite('fman_clip', vps);
r.tx(stamp);
if (sel.length)
toast.inf(1.5, L.fcc_ok.format(sel.length));
}
catch (ex) {
toast.warn(30, L.fcc_warn.format(sel.length));
}
};
document.onpaste = function (e) { document.onpaste = function (e) {
var xfer = e.clipboardData || window.clipboardData; var xfer = e.clipboardData || window.clipboardData;
if (!xfer || !xfer.files || !xfer.files.length) if (!xfer || !xfer.files || !xfer.files.length)
@@ -5071,9 +5194,9 @@ var fileman = (function () {
return r.clip_up(files); return r.clip_up(files);
var src = r.clip.length == 1 ? r.clip[0] : vsplit(r.clip[0])[0], var src = r.clip.length == 1 ? r.clip[0] : vsplit(r.clip[0])[0],
msg = L.fp_both_m.format(r.clip.length, src, files.length); msg = (r.ccp ? L.fcp_both_m : L.fp_both_m).format(r.clip.length, src, files.length);
modal.confirm(msg, r.paste, function () { r.clip_up(files); }, null, L.fp_both_b); modal.confirm(msg, r.paste, function () { r.clip_up(files); }, null, (r.ccp ? L.fcp_both_b : L.fp_both_b));
}; };
r.clip_up = function (files) { r.clip_up = function (files) {
@@ -5129,65 +5252,147 @@ var fileman = (function () {
if (clgot(bpst, 'hide')) if (clgot(bpst, 'hide'))
return toast.err(3, L.fp_eperm); return toast.err(3, L.fp_eperm);
var req = [], var html = [
exists = [], '<div>',
'<button id="rn_cancel" tt="' + L.frt_abrt + '</button>',
'<button id="rn_apply">✅ ' + L.fp_apply + '</button>',
' &nbsp; src: ' + esc(r.clip[0].replace(/[^/]+$/, '')),
'</div>',
'<p id="cnmt"></p>',
'<div><table id="rn_f" class="m">',
'<tr><td>' + L.fr_lnew + '</td><td>' + L.fr_lold + '</td></tr>',
],
ui = false,
f = [],
indir = [], indir = [],
srcdir = vsplit(r.clip[0])[0], srcdir = vsplit(r.clip[0])[0],
links = QSA('#files tbody td:nth-child(2) a'); links = QSA('#files tbody td:nth-child(2) a');
for (var a = 0, aa = links.length; a < aa; a++) for (var a = 0, aa = links.length; a < aa; a++)
indir.push(vsplit(noq_href(links[a]))[1]); indir.push(uricom_dec(vsplit(noq_href(links[a]))[1]));
for (var a = 0; a < r.clip.length; a++) { for (var a = 0; a < r.clip.length; a++) {
var found = false; var t = {
for (var b = 0; b < indir.length; b++) { 'ok': true,
if (r.clip[a].endsWith('/' + indir[b])) { 'src': r.clip[a],
exists.push(r.clip[a]); 'dst': uricom_dec(r.clip[a].split('/').pop()),
found = true; };
} f.push(t);
}
if (!found) for (var b = 0; b < indir.length; b++)
req.push(r.clip[a]); if (t.dst == indir[b]) {
t.ok = false;
ui = true;
} }
if (exists.length) html.push('<tr' + (!t.ok ? ' class="ng"' : '') + '><td><input type="text" id="rn_new_' + a + '" value="' + esc(t.dst) + '" /></td><td><input type="text" id="rn_old_' + a + '" value="' + esc(t.dst) + '" readonly /></td></tr>');
toast.warn(30, L.fp_ename.format(exists.length) + '<ul>' + uricom_adec(exists, true).join('') + '</ul>'); }
if (!req.length)
return;
function paster() { function paster() {
var xhr = new XHR(), var t = f.shift();
vp = req.shift(); if (!t) {
toast.ok(2, r.ccp ? L.fcp_ok : L.fp_ok);
if (!vp) {
toast.ok(2, L.fp_ok);
treectl.goto(); treectl.goto();
r.tx(srcdir); r.tx(srcdir);
return; return;
} }
toast.show('inf r', 0, esc(L.fp_busy.format(req.length + 1, uricom_dec(vp)))); if (!t.dst)
return paster();
var dst = get_evpath() + vp.split('/').pop(); toast.show('inf r', 0, esc((r.ccp ? L.fcp_busy : L.fp_busy).format(f.length + 1, uricom_dec(t.src))));
xhr.open('POST', vp + '?move=' + dst, true); var xhr = new XHR(),
act = r.ccp ? '?copy=' : '?move=',
dst = get_evpath() + uricom_enc(t.dst);
xhr.open('POST', t.src + act + dst, true);
xhr.onload = xhr.onerror = paste_cb; xhr.onload = xhr.onerror = paste_cb;
xhr.send(); xhr.send();
} }
function paste_cb() { function paste_cb() {
if (this.status !== 201) { if (this.status !== 201) {
var msg = unpre(this.responseText); var msg = unpre(this.responseText);
toast.err(9, L.fp_err + msg); toast.err(9, (r.ccp ? L.fcp_err : L.fp_err) + msg);
return; return;
} }
paster(); paster();
} }
function okgo() {
modal.confirm(L.fp_confirm.format(req.length) + '<ul>' + uricom_adec(req, true).join('') + '</ul>', function () {
paster(); paster();
jwrite('fman_clip', [Date.now()]); jwrite('fman_clip', [Date.now()]);
}, null); }
if (!ui) {
var src = [];
for (var a = 0; a < f.length; a++)
src.push(f[a].src);
return modal.confirm((r.ccp ? L.fcp_confirm : L.fp_confirm).format(f.length) + '<ul>' + uricom_adec(src, true).join('') + '</ul>', okgo, null);
}
var rui = ebi('rui');
if (!rui) {
rui = mknod('div', 'rui');
document.body.appendChild(rui);
}
html.push('</table>');
rui.innerHTML = html.join('\n');
tt.att(rui);
function rn_apply(e) {
for (var a = 0; a < f.length; a++)
if (!f[a].ok) {
toast.err(30, L.fp_emore);
return setcnmt(true);
}
rn_cancel(e);
okgo();
}
function rn_cancel(e) {
ev(e);
rui.parentNode.removeChild(rui);
}
ebi('rn_cancel').onclick = rn_cancel;
ebi('rn_apply').onclick = rn_apply;
var first_bad = 0;
function setcnmt(sel) {
var nbad = 0;
for (var a = 0; a < f.length; a++) {
if (f[a].ok)
continue;
if (!nbad)
first_bad = a;
nbad += 1;
}
ebi('cnmt').innerHTML = (r.ccp ? L.fcp_ename : L.fp_ename).format(nbad);
if (sel && nbad) {
var el = ebi('rn_new_' + first_bad);
el.focus();
el.setSelectionRange(0, el.value.lastIndexOf('.'), "forward");
}
}
setcnmt(true);
for (var a = 0; a < f.length; a++)
(function (a) {
var inew = ebi('rn_new_' + a);
inew.onkeydown = function (e) {
if (((e.code || e.key) + '').endsWith('Enter'))
return rn_apply();
}; };
inew.oninput = function (e) {
f[a].dst = this.value;
f[a].ok = true;
if (f[a].dst)
for (var b = 0; b < indir.length; b++)
if (indir[b] == this.value)
f[a].ok = false;
clmod(this.closest('tr'), 'ng', !f[a].ok);
setcnmt();
};
})(a);
}
function onmsg(msg) { function onmsg(msg) {
r.clip = null; r.clip = null;
@@ -5225,6 +5430,7 @@ var fileman = (function () {
bren.onclick = r.rename; bren.onclick = r.rename;
bdel.onclick = r.delete; bdel.onclick = r.delete;
bcut.onclick = r.cut; bcut.onclick = r.cut;
bcpy.onclick = r.cpy;
bpst.onclick = r.paste; bpst.onclick = r.paste;
bshr.onclick = r.share; bshr.onclick = r.share;
@@ -6047,6 +6253,19 @@ var thegrid = (function () {
toast.warn(10, L.ul_btnlk); toast.warn(10, L.ul_btnlk);
}; };
if (/[?&]grid\b/.exec(sloc0))
swrite('griden', /[?&]grid=0\b/.exec(sloc0) ? 0 : 1)
if (/[?&]thumb\b/.exec(sloc0))
swrite('thumbs', /[?&]thumb=0\b/.exec(sloc0) ? 0 : 1)
if (/[?&]imgs\b/.exec(sloc0)) {
var n = /[?&]imgs=0\b/.exec(sloc0) ? 0 : 1;
swrite('griden', n);
if (n)
swrite('thumbs', 1);
}
bcfg_bind(r, 'thumbs', 'thumbs', true, r.setdirty); bcfg_bind(r, 'thumbs', 'thumbs', true, r.setdirty);
bcfg_bind(r, 'ihop', 'ihop', true); bcfg_bind(r, 'ihop', 'ihop', true);
bcfg_bind(r, 'vau', 'gridvau', false); bcfg_bind(r, 'vau', 'gridvau', false);
@@ -6126,8 +6345,6 @@ function tree_neigh(n) {
links[act].click(); links[act].click();
else else
treectl.treego.call(links[act]); treectl.treego.call(links[act]);
links[act].focus();
} }
@@ -6225,9 +6442,6 @@ var ahotkeys = function (e) {
ae = document.activeElement, ae = document.activeElement,
aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : ''; aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : '';
if (e.key == '?')
return hkhelp();
if (k == 'Escape' || k == 'Esc') { if (k == 'Escape' || k == 'Esc') {
ae && ae.blur(); ae && ae.blur();
tt.hide(); tt.hide();
@@ -6308,15 +6522,24 @@ var ahotkeys = function (e) {
if (aet && aet != 'a' && aet != 'tr' && aet != 'td' && aet != 'div' && aet != 'pre') if (aet && aet != 'a' && aet != 'tr' && aet != 'td' && aet != 'div' && aet != 'pre')
return; return;
if (ctrl(e)) { if (e.key == '?')
return hkhelp();
if (!e.shiftKey && ctrl(e)) {
var sel = window.getSelection && window.getSelection() || {};
sel = sel && !sel.isCollapsed && sel.direction != 'none';
if (k == 'KeyX' || k == 'x') if (k == 'KeyX' || k == 'x')
return fileman.cut(); return fileman.cut(e);
if ((k == 'KeyC' || k == 'c') && !sel)
return fileman.cpy(e);
if (k == 'KeyV' || k == 'v') if (k == 'KeyV' || k == 'v')
return fileman.d_paste(); return fileman.d_paste(e);
if (k == 'KeyK' || k == 'k') if (k == 'KeyK' || k == 'k')
return fileman.delete(); return fileman.delete(e);
return; return;
} }
@@ -7232,6 +7455,7 @@ var treectl = (function () {
r.reqls(href, true); r.reqls(href, true);
r.dir_cb = tree_scrollto; r.dir_cb = tree_scrollto;
thegrid.setvis(true); thegrid.setvis(true);
clmod(this, 'ld', 1);
} }
r.reqls = function (url, hpush, back, hydrate) { r.reqls = function (url, hpush, back, hydrate) {

BIN
copyparty/web/iiam.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

View File

@@ -90,6 +90,10 @@ table {
text-align: left; text-align: left;
white-space: nowrap; white-space: nowrap;
} }
.vols td:empty,
.vols th:empty {
padding: 0;
}
.num { .num {
border-right: 1px solid #bbb; border-right: 1px solid #bbb;
} }
@@ -222,3 +226,6 @@ html.bz {
color: #bbd; color: #bbd;
background: #11121d; background: #11121d;
} }
html.bz .vols img {
filter: sepia(0.8) hue-rotate(180deg);
}

View File

@@ -44,6 +44,18 @@
</table> </table>
{%- endif %} {%- endif %}
{%- if dls %}
<h1 id="ae">active downloads:</h1>
<table class="vols">
<thead><tr><th>%</th><th>sent</th><th>speed</th><th>eta</th><th>idle</th><th></th><th>dir</th><th>file</th></tr></thead>
<tbody>
{% for u in dls %}
<tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td>{{ u[4] }}</td><td>{{ u[5] }}</td><td><a href="{{ u[6] }}">{{ u[7]|e }}</a></td><td>{{ u[8] }}</td></tr>
{% endfor %}
</tbody>
</table>
{%- endif %}
{%- if avol %} {%- if avol %}
<h1>admin panel:</h1> <h1>admin panel:</h1>
<table><tr><td> <!-- hehehe --> <table><tr><td> <!-- hehehe -->

View File

@@ -37,6 +37,7 @@ var Ls = {
"ab1": "skru av no304", "ab1": "skru av no304",
"ac1": "skru på no304", "ac1": "skru på no304",
"ad1": "no304 stopper all bruk av cache. Hvis ikke k304 var nok, prøv denne. Vil mangedoble dataforbruk!", "ad1": "no304 stopper all bruk av cache. Hvis ikke k304 var nok, prøv denne. Vil mangedoble dataforbruk!",
"ae1": "utgående:",
}, },
"eng": { "eng": {
"d2": "shows the state of all active threads", "d2": "shows the state of all active threads",
@@ -86,6 +87,7 @@ var Ls = {
"ab1": "关闭 k304", "ab1": "关闭 k304",
"ac1": "开启 k304", "ac1": "开启 k304",
"ad1": "启用 no304 将禁用所有缓存;如果 k304 不够,可以尝试此选项。这将消耗大量的网络流量!", //m "ad1": "启用 no304 将禁用所有缓存;如果 k304 不够,可以尝试此选项。这将消耗大量的网络流量!", //m
"ae1": "正在下载:", //m
} }
}; };

View File

@@ -75,7 +75,7 @@ html {
top: 1px; top: 1px;
right: 1px; right: 1px;
left: 1px; left: 1px;
animation: toastt var(--tmtime) steps(var(--tmstep)) forwards; animation: toastt var(--tmtime) 0.07s steps(var(--tmstep)) forwards;
transform-origin: right; transform-origin: right;
} }
@keyframes toastt { @keyframes toastt {

View File

@@ -1542,8 +1542,8 @@ var toast = (function () {
var html = ''; var html = '';
if (sec) { if (sec) {
setcvar('--tmtime', sec + 's'); setcvar('--tmtime', (sec - 0.15) + 's');
setcvar('--tmstep', sec * 15); setcvar('--tmstep', Math.floor(sec * 20));
html += '<div id="toastt"></div>'; html += '<div id="toastt"></div>';
} }
obj.innerHTML = html + '<a href="#" id="toastc">x</a><div id="toastb">' + lf2br(txt) + '</div>'; obj.innerHTML = html + '<a href="#" id="toastc">x</a><div id="toastb">' + lf2br(txt) + '</div>';

View File

@@ -1,3 +1,75 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2024-1110-1932 `v1.16.0` COPYparty
## 🧪 new features
* #46 #115 copy/paste files and folders cacec9c1
* cut/paste still exists, but now you can copy too
* with a UI to rename files in case of filename collisions 56317b00
* files are created according to the dedup settings in the target volume (either full copies or symlinks/hardlinks)
* show currently active downloads in the controlpanel 8aba5aed
* can be made admin-only with `--dl-list=1` or disabled with `--dl-list=0`
* hides filenames of hidden files, and files from volumes where the viewer doesn't have access
* #114 async reinit on new [IdP users](https://github.com/9001/copyparty#identity-providers) 44ee07f0
* new IdP users can now always auth, even while a filesystem reindex is running
* ux:
* remember batch-rename settings from last time 6a8d5e17
* URL parameters to force grid/thumbs on/off 5718caa9
## 🩹 bugfixes
* folders that fail to list due to a corrupt HDD/filesystem will now return a 404 instead of an empty listing 119e88d8
* also fixes similar issues in u2c and partyfuse
* u2c (commandline uploader): detect and adapt to proxies with short connection keepalives c784e528
* ui/ux:
* show the "switch-to-https" button in 404-messages too efd8a32e
* the folder-loading indicator could steal keyboard focus d9962f65
* hotkey-help was very trigger-happy 71d9e010
## 🔧 other changes
* choose more conservative defaults when server has less than 1 GiB RAM 2bf9055c
* runs okay down to 128 MiB, but thumbnails die below 256 MiB
* update the [comparison to similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) after years of optimizations on both sides 0ce7cf5e
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2024-1027-0751 `v1.15.10` temporary upload links
## 🧪 new features
* [shares](https://github.com/9001/copyparty#shares) can now be uploaded into, and unpost works too 4bdcbc1c
* useful to create temporary URLs for other people to upload to
* shares can be write-only, so visitors can't browse or see any files
* #110 HTTP 304 (caching):
* support `If-Range` for HTTP 206 159f51b1
* add server-side and client-side options to force-disable cache dd6dbdd9
* `--no304=1` shows a button in the controlpanel to disable caching
* `--no304=2` makes that button auto-enabled
* even when `--no304` is not specified, accessing the URL `/?setck=no304=y` force-disables cache
* when cache is force-disabled, browsers will waste a lot of network traffic / data usage
* might help to avoid bugs in browsers or proxies, for example if media files suddenly stop loading
* but such bugs should be exceedingly rare, so do not enable this unless actually necessary
## 🩹 bugfixes
* #110 HTTP 304 (caching):
* remove `Content-Length` and `Content-Type` response headers from 304 replies 91240236
* browsers don't need these, and some middlewares might get confused if they're present
* #113 fix crash on startup if `-j0` was combined with `--ipa` or `--ipu` 3a0d882c
* #111 fix javascript crash if `--u2sz` was set to an invalid value b13899c6
## 🔧 other changes
* #110 HTTP 304 (caching):
* never automatically enable k304 because the `Vary` header killed support for caching in msie anyways 63013cc5
* change time comparison for `If-Modified-Since` to require an exact timestamp match, instead of the intended "modified since". This technically violates the http-spec, but should be safer for backdating file mtimes 159f51b1
* new option `--ohead` to log response headers 7678a91b
* added [nintendo 3ds](https://github.com/user-attachments/assets/88deab3d-6cad-4017-8841-2f041472b853) to the [list of supported browsers](https://github.com/9001/copyparty#browser-support) cb81f0ad
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2024-1018-2342 `v1.15.9` rss server # 2024-1018-2342 `v1.15.9` rss server

View File

@@ -140,6 +140,7 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
| GET | `?tar&j` | pregenerate jpg thumbnails | | GET | `?tar&j` | pregenerate jpg thumbnails |
| GET | `?tar&p` | pregenerate audio waveforms | | GET | `?tar&p` | pregenerate audio waveforms |
| GET | `?shares` | list your shared files/folders | | GET | `?shares` | list your shared files/folders |
| GET | `?dls` | show active downloads (do this as admin) |
| GET | `?ups` | show recent uploads from your IP | | GET | `?ups` | show recent uploads from your IP |
| GET | `?ups&filter=f` | ...where URL contains `f` | | GET | `?ups&filter=f` | ...where URL contains `f` |
| GET | `?mime=foo` | specify return mimetype `foo` | | GET | `?mime=foo` | specify return mimetype `foo` |
@@ -163,6 +164,7 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
| method | params | result | | method | params | result |
|--|--|--| |--|--|--|
| POST | `?copy=/foo/bar` | copy the file/folder at URL to /foo/bar |
| POST | `?move=/foo/bar` | move/rename the file/folder at URL to /foo/bar | | POST | `?move=/foo/bar` | move/rename the file/folder at URL to /foo/bar |
| method | params | body | result | | method | params | body | result |
@@ -208,6 +210,12 @@ upload modifiers:
| method | params | result | | method | params | result |
|--|--|--| |--|--|--|
| GET | `?pw=x` | logout | | GET | `?pw=x` | logout |
| GET | `?grid` | ui: show grid-view |
| GET | `?imgs` | ui: show grid-view with thumbnails |
| GET | `?grid=0` | ui: show list-view |
| GET | `?imgs=0` | ui: show list-view |
| GET | `?thumb` | ui, grid-mode: show thumbnails |
| GET | `?thumb=0` | ui, grid-mode: show icons |
# event hooks # event hooks

View File

@@ -465,11 +465,13 @@ symbol legend,
## [hfs3](https://rejetto.com/hfs/) ## [hfs3](https://rejetto.com/hfs/)
* nodejs; cross-platform * nodejs; cross-platform
* vfs with gui config, per-volume permissions * vfs with gui config, per-volume permissions
* tested locally, v0.53.2 on archlinux
* 🔵 uploads are resumable * 🔵 uploads are resumable
* ⚠️ uploads are not segmented; max upload size 100 MiB on cloudflare * ⚠️ uploads are not segmented; max upload size 100 MiB on cloudflare
* ⚠️ uploads are not accelerated (copyparty is 3x faster across the atlantic) * ⚠️ uploads are not accelerated (copyparty is 3x faster across the atlantic)
* ⚠️ uploads are not integrity-checked * ⚠️ uploads are not integrity-checked
* ⚠️ copies the file after upload; need twice filesize free disk space * ⚠️ copies the file after upload; need twice filesize free disk space
* ⚠️ uploading small files is decent; `107` files per sec (copyparty does `670`/sec, 6x faster)
* ⚠️ doesn't support crazy filenames * ⚠️ doesn't support crazy filenames
* ✅ config GUI * ✅ config GUI
* ✅ download counter * ✅ download counter
@@ -478,11 +480,12 @@ symbol legend,
## [nextcloud](https://github.com/nextcloud/server) ## [nextcloud](https://github.com/nextcloud/server)
* php, mariadb * php, mariadb
* tested locally, [linuxserver/nextcloud](https://hub.docker.com/r/linuxserver/nextcloud) v30.0.2 (sqlite)
* ⚠️ [isolated on-disk file hierarchy] in per-user folders * ⚠️ [isolated on-disk file hierarchy] in per-user folders
* not that bad, can probably be remedied with bindmounts or maybe symlinks * not that bad, can probably be remedied with bindmounts or maybe symlinks
* ⚠️ uploads not resumable / accelerated / integrity-checked * ⚠️ uploads not resumable / accelerated / integrity-checked
* ⚠️ on cloudflare: max upload size 100 MiB * ⚠️ on cloudflare: max upload size 100 MiB
* ⚠️ uploading small files is slow; `2.2` files per sec (copyparty does `87`/sec), tested locally with [linuxserver/nextcloud](https://hub.docker.com/r/linuxserver/nextcloud) (sqlite) * ⚠️ uploading small files is slow; `4` files per sec (copyparty does `670`/sec, 160x faster)
* ⚠️ no write-only / upload-only folders * ⚠️ no write-only / upload-only folders
* ⚠️ http/webdav only; no ftp, zeroconf * ⚠️ http/webdav only; no ftp, zeroconf
* ⚠️ less awesome music player * ⚠️ less awesome music player
@@ -498,11 +501,12 @@ symbol legend,
## [seafile](https://github.com/haiwen/seafile) ## [seafile](https://github.com/haiwen/seafile)
* c, mariadb * c, mariadb
* tested locally, [official container](https://manual.seafile.com/latest/docker/deploy_seafile_with_docker/) v11.0.13
* ⚠️ [isolated on-disk file hierarchy](https://manual.seafile.com/maintain/seafile_fsck/), incompatible with other software * ⚠️ [isolated on-disk file hierarchy](https://manual.seafile.com/maintain/seafile_fsck/), incompatible with other software
* *much worse than nextcloud* in that regard * *much worse than nextcloud* in that regard
* ⚠️ uploads not resumable / accelerated / integrity-checked * ⚠️ uploads not resumable / accelerated / integrity-checked
* ⚠️ on cloudflare: max upload size 100 MiB * ⚠️ on cloudflare: max upload size 100 MiB
* ⚠️ uploading small files is slow; `2.7` files per sec (copyparty does `87`/sec), tested locally with [official container](https://manual.seafile.com/docker/deploy_seafile_with_docker/) * ⚠️ uploading small files is slow; `4.7` files per sec (copyparty does `670`/sec, 140x faster)
* ⚠️ no write-only / upload-only folders * ⚠️ no write-only / upload-only folders
* ⚠️ big folders cannot be zip-downloaded * ⚠️ big folders cannot be zip-downloaded
* ⚠️ http/webdav only; no ftp, zeroconf * ⚠️ http/webdav only; no ftp, zeroconf
@@ -526,9 +530,11 @@ symbol legend,
## [dufs](https://github.com/sigoden/dufs) ## [dufs](https://github.com/sigoden/dufs)
* rust; cross-platform (windows, linux, macos) * rust; cross-platform (windows, linux, macos)
* tested locally, v0.43.0 on archlinux (plain binary)
* ⚠️ uploads not resumable / accelerated / integrity-checked * ⚠️ uploads not resumable / accelerated / integrity-checked
* ⚠️ on cloudflare: max upload size 100 MiB * ⚠️ on cloudflare: max upload size 100 MiB
* ⚠️ across the atlantic, copyparty is 3x faster * ⚠️ across the atlantic, copyparty is 3x faster
* ⚠️ uploading small files is decent; `97` files per sec (copyparty does `670`/sec, 7x faster)
* ⚠️ doesn't support crazy filenames * ⚠️ doesn't support crazy filenames
* ✅ per-url access control (copyparty is per-volume) * ✅ per-url access control (copyparty is per-volume)
* 🔵 basic but really snappy ui * 🔵 basic but really snappy ui
@@ -571,10 +577,12 @@ symbol legend,
## [filebrowser](https://github.com/filebrowser/filebrowser) ## [filebrowser](https://github.com/filebrowser/filebrowser)
* go; cross-platform (windows, linux, mac) * go; cross-platform (windows, linux, mac)
* tested locally, v2.31.2 on archlinux (plain binary)
* 🔵 uploads are resumable and segmented * 🔵 uploads are resumable and segmented
* 🔵 multiple files are uploaded in parallel, but... * 🔵 multiple files are uploaded in parallel, but...
* ⚠️ big files are not accelerated (copyparty is 5x faster across the atlantic) * ⚠️ big files are not accelerated (copyparty is 5x faster across the atlantic)
* ⚠️ uploads are not integrity-checked * ⚠️ uploads are not integrity-checked
* ⚠️ uploading small files is decent; `69` files per sec (copyparty does `670`/sec, 9x faster)
* ⚠️ http only; no webdav / ftp / zeroconf * ⚠️ http only; no webdav / ftp / zeroconf
* ⚠️ doesn't support crazy filenames * ⚠️ doesn't support crazy filenames
* ⚠️ no directory tree nav * ⚠️ no directory tree nav
@@ -612,6 +620,7 @@ symbol legend,
* ⚠️ no zeroconf (mdns/ssdp) * ⚠️ no zeroconf (mdns/ssdp)
* ⚠️ impractical directory URLs * ⚠️ impractical directory URLs
* ⚠️ AGPL licensed * ⚠️ AGPL licensed
* 🔵 uploading small files is fast; `340` files per sec (copyparty does `670`/sec)
* 🔵 ftp, ftps, webdav * 🔵 ftp, ftps, webdav
* ✅ sftp server * ✅ sftp server
* ✅ settings gui * ✅ settings gui

View File

@@ -94,6 +94,7 @@ copyparty/web/deps/prismd.css,
copyparty/web/deps/scp.woff2, copyparty/web/deps/scp.woff2,
copyparty/web/deps/sha512.ac.js, copyparty/web/deps/sha512.ac.js,
copyparty/web/deps/sha512.hw.js, copyparty/web/deps/sha512.hw.js,
copyparty/web/iiam.gif,
copyparty/web/md.css, copyparty/web/md.css,
copyparty/web/md.html, copyparty/web/md.html,
copyparty/web/md.js, copyparty/web/md.js,

View File

@@ -79,6 +79,7 @@ var tl_cpanel = {
"ab1": "disable no304", "ab1": "disable no304",
"ac1": "enable no304", "ac1": "enable no304",
"ad1": "enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!", "ad1": "enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!",
"ae1": "active downloads:",
}, },
}; };
@@ -121,8 +122,9 @@ var tl_browser = {
["T", "toggle thumbnails / icons"], ["T", "toggle thumbnails / icons"],
["🡅 A/D", "thumbnail size"], ["🡅 A/D", "thumbnail size"],
["ctrl-K", "delete selected"], ["ctrl-K", "delete selected"],
["ctrl-X", "cut selected"], ["ctrl-X", "cut selection to clipboard"],
["ctrl-V", "paste into folder"], ["ctrl-C", "copy selection to clipboard"],
["ctrl-V", "paste (move/copy) here"],
["Y", "download selected"], ["Y", "download selected"],
["F2", "rename selected"], ["F2", "rename selected"],
@@ -167,7 +169,7 @@ var tl_browser = {
["I/K", "prev/next file"], ["I/K", "prev/next file"],
["M", "close textfile"], ["M", "close textfile"],
["E", "edit textfile"], ["E", "edit textfile"],
["S", "select file (for cut/rename)"], ["S", "select file (for cut/copy/rename)"],
] ]
], ],
@@ -217,6 +219,7 @@ var tl_browser = {
"wt_ren": "rename selected items$NHotkey: F2", "wt_ren": "rename selected items$NHotkey: F2",
"wt_del": "delete selected items$NHotkey: ctrl-K", "wt_del": "delete selected items$NHotkey: ctrl-K",
"wt_cut": "cut selected items &lt;small&gt;(then paste somewhere else)&lt;/small&gt;$NHotkey: ctrl-X", "wt_cut": "cut selected items &lt;small&gt;(then paste somewhere else)&lt;/small&gt;$NHotkey: ctrl-X",
"wt_cpy": "copy selected items to clipboard$N(to paste them somewhere else)$NHotkey: ctrl-C",
"wt_pst": "paste a previously cut / copied selection$NHotkey: ctrl-V", "wt_pst": "paste a previously cut / copied selection$NHotkey: ctrl-V",
"wt_selall": "select all files$NHotkey: ctrl-A (when file focused)", "wt_selall": "select all files$NHotkey: ctrl-A (when file focused)",
"wt_selinv": "invert selection", "wt_selinv": "invert selection",
@@ -411,6 +414,7 @@ var tl_browser = {
"fr_emore": "select at least one item to rename", "fr_emore": "select at least one item to rename",
"fd_emore": "select at least one item to delete", "fd_emore": "select at least one item to delete",
"fc_emore": "select at least one item to cut", "fc_emore": "select at least one item to cut",
"fcp_emore": "select at least one item to copy",
"fs_sc": "share the folder you're in", "fs_sc": "share the folder you're in",
"fs_ss": "share the selected files", "fs_ss": "share the selected files",
@@ -463,16 +467,26 @@ var tl_browser = {
"fc_ok": "cut {0} items", "fc_ok": "cut {0} items",
"fc_warn": 'cut {0} items\n\nbut: only <b>this</b> browser-tab can paste them\n(since the selection is so absolutely massive)', "fc_warn": 'cut {0} items\n\nbut: only <b>this</b> browser-tab can paste them\n(since the selection is so absolutely massive)',
"fp_ecut": "first cut some files / folders to paste / move\n\nnote: you can cut / paste across different browser tabs", "fcc_ok": "copied {0} items to clipboard",
"fcc_warn": 'copied {0} items to clipboard\n\nbut: only <b>this</b> browser-tab can paste them\n(since the selection is so absolutely massive)',
"fp_ecut": "first cut or copy some files / folders to paste / move\n\nnote: you can cut / paste across different browser tabs",
"fp_ename": "these {0} items cannot be moved here (names already exist):", "fp_ename": "these {0} items cannot be moved here (names already exist):",
"fcp_ename": "these {0} items cannot be copied here (names already exist):",
"fp_ok": "move OK", "fp_ok": "move OK",
"fcp_ok": "copy OK",
"fp_busy": "moving {0} items...\n\n{1}", "fp_busy": "moving {0} items...\n\n{1}",
"fcp_busy": "copying {0} items...\n\n{1}",
"fp_err": "move failed:\n", "fp_err": "move failed:\n",
"fcp_err": "copy failed:\n",
"fp_confirm": "move these {0} items here?", "fp_confirm": "move these {0} items here?",
"fcp_confirm": "copy these {0} items here?",
"fp_etab": 'failed to read clipboard from other browser tab', "fp_etab": 'failed to read clipboard from other browser tab',
"fp_name": "uploading a file from your device. Give it a name:", "fp_name": "uploading a file from your device. Give it a name:",
"fp_both_m": '<h6>choose what to paste</h6><code>Enter</code> = Move {0} files from «{1}»\n<code>ESC</code> = Upload {2} files from your device', "fp_both_m": '<h6>choose what to paste</h6><code>Enter</code> = Move {0} files from «{1}»\n<code>ESC</code> = Upload {2} files from your device',
"fcp_both_m": '<h6>choose what to paste</h6><code>Enter</code> = Copy {0} files from «{1}»\n<code>ESC</code> = Upload {2} files from your device',
"fp_both_b": '<a href="#" id="modal-ok">Move</a><a href="#" id="modal-ng">Upload</a>', "fp_both_b": '<a href="#" id="modal-ok">Move</a><a href="#" id="modal-ng">Upload</a>',
"fcp_both_b": '<a href="#" id="modal-ok">Copy</a><a href="#" id="modal-ng">Upload</a>',
"mk_noname": "type a name into the text field on the left before you do that :p", "mk_noname": "type a name into the text field on the left before you do that :p",
@@ -484,7 +498,7 @@ var tl_browser = {
"tvt_dl": "download this file$NHotkey: Y\">💾 download", "tvt_dl": "download this file$NHotkey: Y\">💾 download",
"tvt_prev": "show previous document$NHotkey: i\">⬆ prev", "tvt_prev": "show previous document$NHotkey: i\">⬆ prev",
"tvt_next": "show next document$NHotkey: K\">⬇ next", "tvt_next": "show next document$NHotkey: K\">⬇ next",
"tvt_sel": "select file &nbsp; ( for cut / delete / ... )$NHotkey: S\">sel", "tvt_sel": "select file &nbsp; ( for cut / copy / delete / ... )$NHotkey: S\">sel",
"tvt_edit": "open file in text editor$NHotkey: E\">✏️ edit", "tvt_edit": "open file in text editor$NHotkey: E\">✏️ edit",
"gt_vau": "don't show videos, just play the audio\">🎧", "gt_vau": "don't show videos, just play the audio\">🎧",

109
tests/test_cp.py Normal file
View File

@@ -0,0 +1,109 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
import os
import shutil
import tempfile
import unittest
from itertools import product
from copyparty.authsrv import AuthSrv
from copyparty.httpcli import HttpCli
from tests import util as tu
from tests.util import Cfg
class TestDedup(unittest.TestCase):
def setUp(self):
self.td = tu.get_ramdisk()
def tearDown(self):
os.chdir(tempfile.gettempdir())
shutil.rmtree(self.td)
def reset(self):
td = os.path.join(self.td, "vfs")
if os.path.exists(td):
shutil.rmtree(td)
os.mkdir(td)
os.chdir(td)
for a in "abc":
os.mkdir(a)
for b in "fg":
d = "%s/%s%s" % (a, a, b)
os.mkdir(d)
for fn in "x":
fp = "%s/%s%s%s" % (d, a, b, fn)
with open(fp, "wb") as f:
f.write(fp.encode("utf-8"))
return td
def cinit(self):
if self.conn:
self.fstab = self.conn.hsrv.hub.up2k.fstab
self.conn.hsrv.hub.up2k.shutdown()
self.asrv = AuthSrv(self.args, self.log)
self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b"", True)
if self.fstab:
self.conn.hsrv.hub.up2k.fstab = self.fstab
def test(self):
tc_dedup = ["sym", "no"]
vols = [".::A", "a/af:a/af:r", "b:a/b:r"]
tcs = [
"/a?copy=/c/a /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /b/bf/bfx /b/bg/bgx /c/a/af/afx /c/a/ag/agx /c/a/b/bf/bfx /c/a/b/bg/bgx /c/cf/cfx /c/cg/cgx",
"/b?copy=/d /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /b/bf/bfx /b/bg/bgx /c/cf/cfx /c/cg/cgx /d/bf/bfx /d/bg/bgx",
"/b/bf?copy=/d /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /b/bf/bfx /b/bg/bgx /c/cf/cfx /c/cg/cgx /d/bfx",
"/a/af?copy=/d /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /b/bf/bfx /b/bg/bgx /c/cf/cfx /c/cg/cgx /d/afx",
"/a/af?copy=/ /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /afx /b/bf/bfx /b/bg/bgx /c/cf/cfx /c/cg/cgx",
"/a/af/afx?copy=/afx /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /afx /b/bf/bfx /b/bg/bgx /c/cf/cfx /c/cg/cgx",
]
self.conn = None
self.fstab = None
self.ctr = 0 # 2304
for dedup, act_exp in product(tc_dedup, tcs):
action, expect = act_exp.split(" ", 1)
t = "dedup:%s action:%s" % (dedup, action)
print("\n\n\033[0;7m# ", t, "\033[0m")
ka = {"dav_inf": True}
if dedup == "hard":
ka["hardlink"] = True
elif dedup == "no":
ka["no_dedup"] = True
self.args = Cfg(v=vols, a=[], **ka)
self.reset()
self.cinit()
self.do_cp(action)
zs = self.propfind()
fns = " ".join(zs[1])
self.assertEqual(expect, fns)
def do_cp(self, action):
hdr = "POST %s HTTP/1.1\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
buf = (hdr % (action,)).encode("utf-8")
print("CP [%s]" % (action,))
HttpCli(self.conn.setbuf(buf)).run()
ret = self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
print("CP <-- ", ret)
self.assertIn(" 201 Created", ret[0])
self.assertEqual("k\r\n", ret[1])
return ret
def propfind(self):
h = "PROPFIND / HTTP/1.1\r\nConnection: close\r\n\r\n"
HttpCli(self.conn.setbuf(h.encode("utf-8"))).run()
h, t = self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
fns = t.split("<D:response><D:href>")[1:]
fns = [x.split("</D", 1)[0] for x in fns]
fns = [x for x in fns if not x.endswith("/")]
fns.sort()
return h, fns
def log(self, src, msg, c=0):
print(msg)

View File

@@ -57,6 +57,7 @@ class TestMetrics(unittest.TestCase):
ptns = r""" ptns = r"""
cpp_uptime_seconds [0-9]\.[0-9]{3}$ cpp_uptime_seconds [0-9]\.[0-9]{3}$
cpp_boot_unixtime_seconds [0-9]{7,10}\.[0-9]{3}$ cpp_boot_unixtime_seconds [0-9]{7,10}\.[0-9]{3}$
cpp_active_dl 0$
cpp_http_reqs_created [0-9]{7,10}$ cpp_http_reqs_created [0-9]{7,10}$
cpp_http_reqs_total -1$ cpp_http_reqs_total -1$
cpp_http_conns 9$ cpp_http_conns 9$

View File

@@ -122,7 +122,7 @@ class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None, **ka0): def __init__(self, a=None, v=None, c=None, **ka0):
ka = {} ka = {}
ex = "chpw daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink ih ihead magic hardlink_only nid nih no_acode no_athumb no_clone no_dav no_db_ip no_del no_dirsz no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw og og_no_head og_s_title ohead q rand re_dirsz rss smb srch_dbg stats uqe vague_403 vc ver write_uplog xdev xlink xvol zs" ex = "chpw daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink ih ihead magic hardlink_only nid nih no_acode no_athumb no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw og og_no_head og_s_title ohead q rand re_dirsz rss smb srch_dbg stats uqe vague_403 vc ver write_uplog xdev xlink xvol zs"
ka.update(**{k: False for k in ex.split()}) ka.update(**{k: False for k in ex.split()})
ex = "dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash plain_ip" ex = "dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash plain_ip"
@@ -134,7 +134,7 @@ class Cfg(Namespace):
ex = "hash_mt safe_dedup srch_time u2abort u2j u2sz" ex = "hash_mt safe_dedup srch_time u2abort u2j u2sz"
ka.update(**{k: 1 for k in ex.split()}) ka.update(**{k: 1 for k in ex.split()})
ex = "au_vol mtab_age reg_cap s_thead s_tbody th_convt" ex = "au_vol dl_list mtab_age reg_cap s_thead s_tbody th_convt"
ka.update(**{k: 9 for k in ex.split()}) ka.update(**{k: 9 for k in ex.split()})
ex = "db_act k304 loris no304 re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo" ex = "db_act k304 loris no304 re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo"
@@ -146,7 +146,7 @@ class Cfg(Namespace):
ex = "ban_403 ban_404 ban_422 ban_pw ban_url" ex = "ban_403 ban_404 ban_422 ban_pw ban_url"
ka.update(**{k: "no" for k in ex.split()}) ka.update(**{k: "no" for k in ex.split()})
ex = "grp on403 on404 xad xar xau xban xbd xbr xbu xiu xm" ex = "grp on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm"
ka.update(**{k: [] for k in ex.split()}) ka.update(**{k: [] for k in ex.split()})
ex = "exp_lg exp_md" ex = "exp_lg exp_md"
@@ -254,6 +254,8 @@ class VHttpSrv(object):
self.broker = NullBroker(args, asrv) self.broker = NullBroker(args, asrv)
self.prism = None self.prism = None
self.bans = {} self.bans = {}
self.tdls = self.dls = {}
self.tdli = self.dli = {}
self.nreq = 0 self.nreq = 0
self.nsus = 0 self.nsus = 0
@@ -292,6 +294,8 @@ class VHttpConn(object):
self.args = args self.args = args
self.asrv = asrv self.asrv = asrv
self.bans = {} self.bans = {}
self.tdls = self.dls = {}
self.tdli = self.dli = {}
self.freshen_pwd = 0.0 self.freshen_pwd = 0.0
Ctor = VHttpSrvUp2k if use_up2k else VHttpSrv Ctor = VHttpSrvUp2k if use_up2k else VHttpSrv