Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce3cab0295 | ||
|
|
c784e5285e | ||
|
|
2bf9055cae | ||
|
|
8aba5aed4f | ||
|
|
0ce7cf5e10 | ||
|
|
96edcbccd7 | ||
|
|
4603afb6de | ||
|
|
56317b00af | ||
|
|
cacec9c1f3 | ||
|
|
44ee07f0b2 | ||
|
|
6a8d5e1731 | ||
|
|
d9962f65b3 | ||
|
|
119e88d87b | ||
|
|
71d9e010d9 | ||
|
|
5718caa957 | ||
|
|
efd8a32ed6 | ||
|
|
b22d700e16 | ||
|
|
ccdacea0c4 | ||
|
|
4bdcbc1cb5 | ||
|
|
833c6cf2ec | ||
|
|
dd6dbdd90a | ||
|
|
63013cc565 | ||
|
|
912402364a | ||
|
|
159f51b12b | ||
|
|
7678a91b0e | ||
|
|
b13899c63d | ||
|
|
3a0d882c5e | ||
|
|
cb81f0ad6d | ||
|
|
518bacf628 | ||
|
|
ca63b03e55 |
13
README.md
13
README.md
@@ -428,7 +428,7 @@ configuring accounts/volumes with arguments:
|
||||
|
||||
permissions:
|
||||
* `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
|
||||
* `d` (delete): delete files/folders
|
||||
* `.` (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
|
||||
* `ctrl-K` delete 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
|
||||
* `F2` [rename](#batch-rename) selected file/folder
|
||||
* 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
|
||||
* 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`
|
||||
|
||||

|
||||
|
||||
@@ -756,10 +758,11 @@ file selection: click somewhere on the line (not the link itself), then:
|
||||
* shift-click another line for range-select
|
||||
|
||||
* cut: select some files and `ctrl-x`
|
||||
* copy: select some files and `ctrl-c`
|
||||
* paste: `ctrl-v` in another folder
|
||||
* 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
|
||||
@@ -1684,6 +1687,7 @@ scrape_configs:
|
||||
currently the following metrics are available,
|
||||
* `cpp_uptime_seconds` time since last copyparty restart
|
||||
* `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_reqs` number of http(s) requests handled
|
||||
* `cpp_sus_reqs` number of 403/422/malicious requests
|
||||
@@ -1933,6 +1937,9 @@ quick summary of more eccentric web-browsers trying to view a directory index:
|
||||
| **ie4** and **netscape** 4.0 | can browse, upload with `?b=u`, auth with `&pw=wark` |
|
||||
| **ncsa mosaic** 2.7 | does not get a pass, [pic1](https://user-images.githubusercontent.com/241032/174189227-ae816026-cf6f-4be5-a26e-1b3b072c1b2f.png) - [pic2](https://user-images.githubusercontent.com/241032/174189225-5651c059-5152-46e9-ac26-7e98e497901b.png) |
|
||||
| **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl |
|
||||
| **nintendo 3ds** | can browse, upload, view thumbnails (thx bnjmn) |
|
||||
|
||||
<p align="center"><img src="https://github.com/user-attachments/assets/88deab3d-6cad-4017-8841-2f041472b853" /></p>
|
||||
|
||||
|
||||
# client examples
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -393,7 +393,8 @@ class Gateway(object):
|
||||
if r.status != 200:
|
||||
self.closeconn()
|
||||
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", "")
|
||||
if ctype == "application/json":
|
||||
@@ -1128,7 +1129,7 @@ def main():
|
||||
|
||||
# dircache is always a boost,
|
||||
# only want to disable it for tests etc,
|
||||
cdn = 9 # max num dirs; 0=disable
|
||||
cdn = 24 # max num dirs; keep larger than max dir depth; 0=disable
|
||||
cds = 1 # numsec until an entry goes stale
|
||||
|
||||
where = "local directory"
|
||||
|
||||
25
bin/u2c.py
25
bin/u2c.py
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
S_VERSION = "2.5"
|
||||
S_BUILD_DT = "2024-10-18"
|
||||
S_VERSION = "2.6"
|
||||
S_BUILD_DT = "2024-11-10"
|
||||
|
||||
"""
|
||||
u2c.py: upload to copyparty
|
||||
@@ -189,6 +189,8 @@ class HCli(object):
|
||||
return C(self.addr, self.port, timeout=timeout, **args)
|
||||
|
||||
def req(self, meth, vpath, hdrs, body=None, ctype=None):
|
||||
now = time.time()
|
||||
|
||||
hdrs.update(self.base_hdrs)
|
||||
if self.ar.a:
|
||||
hdrs["PW"] = self.ar.a
|
||||
@@ -201,7 +203,9 @@ class HCli(object):
|
||||
|
||||
# large timeout for handshakes (safededup)
|
||||
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:
|
||||
c.request(meth, vpath, body, hdrs)
|
||||
if PY27:
|
||||
@@ -210,8 +214,15 @@ class HCli(object):
|
||||
rsp = c.getresponse()
|
||||
|
||||
data = rsp.read()
|
||||
conns.append(c)
|
||||
conns.append((time.time(), c))
|
||||
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:
|
||||
c.close()
|
||||
raise
|
||||
@@ -1142,7 +1153,10 @@ class Ctl(object):
|
||||
|
||||
if self.ar.drd:
|
||||
dp = os.path.join(top, rd)
|
||||
lnodes = set(os.listdir(dp))
|
||||
try:
|
||||
lnodes = set(os.listdir(dp))
|
||||
except:
|
||||
lnodes = list(ls) # fs eio; don't delete
|
||||
if ptn:
|
||||
zs = dp.replace(sep, b"/").rstrip(b"/") + b"/"
|
||||
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("-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("--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("--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)")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: icxes <dev.null@need.moe>
|
||||
pkgname=copyparty
|
||||
pkgver="1.15.8"
|
||||
pkgver="1.15.10"
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
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")
|
||||
backup=("etc/${pkgname}.d/init" )
|
||||
sha256sums=("ce2870eca76c554d36392d649e9371ec77b85b84e4899926889e7428922c83ab")
|
||||
sha256sums=("070d5bdebe57c247427ceea6b9029f93097e4b8996cabfc59d0ec248d063b993")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.15.8/copyparty-sfx.py",
|
||||
"version": "1.15.8",
|
||||
"hash": "sha256-J+E6W4q0lsPlsO8S0nglWwGBeu98SE9w/zgIHSNg6Ic="
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.15.10/copyparty-sfx.py",
|
||||
"version": "1.15.10",
|
||||
"hash": "sha256-9InxXpCfgnsvcNdRwWhQ74TpI24Osdr0lN0IwIULL3I="
|
||||
}
|
||||
@@ -80,6 +80,7 @@ web/deps/prismd.css
|
||||
web/deps/scp.woff2
|
||||
web/deps/sha512.ac.js
|
||||
web/deps/sha512.hw.js
|
||||
web/iiam.gif
|
||||
web/md.css
|
||||
web/md.html
|
||||
web/md.js
|
||||
|
||||
@@ -50,6 +50,8 @@ from .util import (
|
||||
PARTFTPY_VER,
|
||||
PY_DESC,
|
||||
PYFTPD_VER,
|
||||
RAM_AVAIL,
|
||||
RAM_TOTAL,
|
||||
SQLITE_VER,
|
||||
UNPLICATIONS,
|
||||
Daemon,
|
||||
@@ -684,6 +686,8 @@ def get_sects():
|
||||
\033[36mxbu\033[35m executes CMD before a file upload starts
|
||||
\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[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[36mxar\033[35m executes CMD after a file rename/move
|
||||
\033[36mxbd\033[35m executes CMD before a file delete
|
||||
@@ -1201,6 +1205,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("--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("--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("--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")
|
||||
@@ -1233,6 +1239,7 @@ def add_optouts(ap):
|
||||
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-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("-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")
|
||||
@@ -1307,6 +1314,7 @@ def add_logging(ap):
|
||||
ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
|
||||
ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
|
||||
ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
|
||||
ap2.add_argument("--ohead", metavar="HEADER", type=u, action='append', help="print response \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
|
||||
ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|\?th=[wj]$|/\.(_|ql_|DS_Store$|localized$)", help="dont log URLs matching regex \033[33mRE\033[0m")
|
||||
|
||||
|
||||
@@ -1315,9 +1323,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-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("--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):
|
||||
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.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)")
|
||||
@@ -1325,7 +1336,7 @@ def add_thumbnail(ap):
|
||||
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-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-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")
|
||||
@@ -1460,6 +1471,7 @@ def add_ui(ap, retry):
|
||||
ap2.add_argument("--pb-url", metavar="URL", type=u, default="https://github.com/9001/copyparty", help="powered-by link; disable with \033[33m-np\033[0m")
|
||||
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)")
|
||||
ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
|
||||
ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
|
||||
ap2.add_argument("--md-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for README.md docs (volflag=md_sbf); see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox")
|
||||
ap2.add_argument("--lg-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for prologue/epilogue docs (volflag=lg_sbf)")
|
||||
ap2.add_argument("--no-sb-md", action="store_true", help="don't sandbox README/PREADME.md documents (volflags: no_sb_md | sb_md)")
|
||||
@@ -1757,6 +1769,9 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
if al.ihead:
|
||||
al.ihead = [x.lower() for x in al.ihead]
|
||||
|
||||
if al.ohead:
|
||||
al.ohead = [x.lower() for x in al.ohead]
|
||||
|
||||
if HAVE_SSL:
|
||||
if al.ssl_ver:
|
||||
configure_ssl_ver(al)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 15, 9)
|
||||
CODENAME = "fill the drives"
|
||||
BUILD_DT = (2024, 10, 18)
|
||||
VERSION = (1, 16, 0)
|
||||
CODENAME = "COPYparty"
|
||||
BUILD_DT = (2024, 11, 10)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
@@ -552,15 +552,14 @@ class VFS(object):
|
||||
return self._get_dbv(vrem)
|
||||
|
||||
shv, srem = src
|
||||
return shv, vjoin(srem, vrem)
|
||||
return shv._get_dbv(vjoin(srem, vrem))
|
||||
|
||||
def _get_dbv(self, vrem: str) -> tuple["VFS", str]:
|
||||
dbv = self.dbv
|
||||
if not dbv:
|
||||
return self, vrem
|
||||
|
||||
tv = [self.vpath[len(dbv.vpath) :].lstrip("/"), vrem]
|
||||
vrem = "/".join([x for x in tv if x])
|
||||
vrem = vjoin(self.vpath[len(dbv.vpath) :].lstrip("/"), vrem)
|
||||
return dbv, vrem
|
||||
|
||||
def canonical(self, rem: str, resolve: bool = True) -> str:
|
||||
@@ -592,10 +591,11 @@ class VFS(object):
|
||||
scandir: bool,
|
||||
permsets: list[list[bool]],
|
||||
lstat: bool = False,
|
||||
throw: bool = False,
|
||||
) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]:
|
||||
"""replaces _ls for certain shares (single-file, or file selection)"""
|
||||
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]
|
||||
return abspath, real, {}
|
||||
|
||||
@@ -606,11 +606,12 @@ class VFS(object):
|
||||
scandir: bool,
|
||||
permsets: list[list[bool]],
|
||||
lstat: bool = False,
|
||||
throw: bool = False,
|
||||
) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]:
|
||||
"""return user-readable [fsdir,real,virt] items at vpath"""
|
||||
virt_vis = {} # nodes readable by user
|
||||
abspath = self.canonical(rem)
|
||||
real = list(statdir(self.log, scandir, lstat, abspath))
|
||||
real = list(statdir(self.log, scandir, lstat, abspath, throw))
|
||||
real.sort()
|
||||
if not rem:
|
||||
# no vfs nodes in the list of real inodes
|
||||
@@ -672,6 +673,10 @@ class VFS(object):
|
||||
"""
|
||||
recursively yields from ./rem;
|
||||
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)
|
||||
@@ -912,7 +917,7 @@ class AuthSrv(object):
|
||||
self._reload()
|
||||
return True
|
||||
|
||||
broker.ask("_reload_blocking", False).get()
|
||||
broker.ask("reload", False, True).get()
|
||||
return True
|
||||
|
||||
def _map_volume_idp(
|
||||
@@ -1382,7 +1387,7 @@ class AuthSrv(object):
|
||||
flags[name] = True
|
||||
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 value is True:
|
||||
t = "└─add volflag [{}] = {} ({})"
|
||||
@@ -1937,7 +1942,7 @@ class AuthSrv(object):
|
||||
vol.flags[k] = odfusion(getattr(self.args, k), vol.flags[k])
|
||||
|
||||
# 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:
|
||||
self._read_volflag(vol.flags, name, getattr(self.args, name), True)
|
||||
|
||||
@@ -2388,7 +2393,7 @@ class AuthSrv(object):
|
||||
self._reload()
|
||||
return True, "new password OK"
|
||||
|
||||
broker.ask("_reload_blocking", False, False).get()
|
||||
broker.ask("reload", False, False).get()
|
||||
return True, "new password OK"
|
||||
|
||||
def setup_chpw(self, acct: dict[str, str]) -> None:
|
||||
@@ -2640,7 +2645,7 @@ class AuthSrv(object):
|
||||
]
|
||||
|
||||
csv = set("i p th_covers zm_on zm_off zs_on zs_off".split())
|
||||
zs = "c ihead 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())
|
||||
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())
|
||||
|
||||
@@ -43,6 +43,9 @@ class BrokerMp(object):
|
||||
self.procs = []
|
||||
self.mutex = threading.Lock()
|
||||
|
||||
self.retpend: dict[int, Any] = {}
|
||||
self.retpend_mutex = threading.Lock()
|
||||
|
||||
self.num_workers = self.args.j or CORES
|
||||
self.log("broker", "booting {} subprocesses".format(self.num_workers))
|
||||
for n in range(1, self.num_workers + 1):
|
||||
@@ -54,6 +57,8 @@ class BrokerMp(object):
|
||||
self.procs.append(proc)
|
||||
proc.start()
|
||||
|
||||
Daemon(self.periodic, "mp-periodic")
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self.log("broker", "shutting down")
|
||||
for n, proc in enumerate(self.procs):
|
||||
@@ -90,8 +95,10 @@ class BrokerMp(object):
|
||||
self.log(*args)
|
||||
|
||||
elif dest == "retq":
|
||||
# response from previous ipc call
|
||||
raise Exception("invalid broker_mp usage")
|
||||
with self.retpend_mutex:
|
||||
retq = self.retpend.pop(retq_id)
|
||||
|
||||
retq.put(args[0])
|
||||
|
||||
else:
|
||||
# new ipc invoking managed service in hub
|
||||
@@ -109,7 +116,6 @@ class BrokerMp(object):
|
||||
proc.q_pend.put((retq_id, "retq", rv))
|
||||
|
||||
def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]:
|
||||
|
||||
# new non-ipc invoking managed service in hub
|
||||
obj = self.hub
|
||||
for node in dest.split("."):
|
||||
@@ -121,17 +127,30 @@ class BrokerMp(object):
|
||||
retq.put(rv)
|
||||
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:
|
||||
"""
|
||||
send message to non-hub component in other process,
|
||||
returns a Queue object which eventually contains the response if want_retval
|
||||
(not-impl here since nothing uses it yet)
|
||||
"""
|
||||
if dest == "listen":
|
||||
if dest == "httpsrv.listen":
|
||||
for p in 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:
|
||||
p.q_pend.put((0, dest, list(args)))
|
||||
|
||||
@@ -140,3 +159,19 @@ class BrokerMp(object):
|
||||
|
||||
else:
|
||||
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))
|
||||
|
||||
@@ -82,37 +82,38 @@ class MpWorker(BrokerCli):
|
||||
while True:
|
||||
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":
|
||||
self.httpsrv.shutdown()
|
||||
self.logw("ok bye")
|
||||
sys.exit(0)
|
||||
return
|
||||
|
||||
elif dest == "reload":
|
||||
if dest == "reload":
|
||||
self.logw("mpw.asrv reloading")
|
||||
self.asrv.reload()
|
||||
self.logw("mpw.asrv reloaded")
|
||||
continue
|
||||
|
||||
elif dest == "reload_sessions":
|
||||
if dest == "reload_sessions":
|
||||
with self.asrv.mutex:
|
||||
self.asrv.load_sessions()
|
||||
continue
|
||||
|
||||
elif dest == "listen":
|
||||
self.httpsrv.listen(args[0], args[1])
|
||||
obj = self
|
||||
for node in dest.split("."):
|
||||
obj = getattr(obj, node)
|
||||
|
||||
elif dest == "set_netdevs":
|
||||
self.httpsrv.set_netdevs(args[0])
|
||||
|
||||
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))
|
||||
rv = obj(*args) # type: ignore
|
||||
if retq_id:
|
||||
self.say("retq", rv, retq_id=retq_id)
|
||||
|
||||
def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]:
|
||||
retq = ExceptionalQueue(1)
|
||||
@@ -123,5 +124,5 @@ class MpWorker(BrokerCli):
|
||||
self.q_yield.put((retq_id, dest, list(args)))
|
||||
return retq
|
||||
|
||||
def say(self, dest: str, *args: Any) -> None:
|
||||
self.q_yield.put((0, dest, list(args)))
|
||||
def say(self, dest: str, *args: Any, retq_id=0) -> None:
|
||||
self.q_yield.put((retq_id, dest, list(args)))
|
||||
|
||||
@@ -53,11 +53,11 @@ class BrokerThr(BrokerCli):
|
||||
return NotExQueue(obj(*args)) # type: ignore
|
||||
|
||||
def say(self, dest: str, *args: Any) -> None:
|
||||
if dest == "listen":
|
||||
if dest == "httpsrv.listen":
|
||||
self.httpsrv.listen(args[0], 1)
|
||||
return
|
||||
|
||||
if dest == "set_netdevs":
|
||||
if dest == "httpsrv.set_netdevs":
|
||||
self.httpsrv.set_netdevs(args[0])
|
||||
return
|
||||
|
||||
|
||||
@@ -103,10 +103,12 @@ def vf_cmap() -> dict[str, str]:
|
||||
"mte",
|
||||
"mth",
|
||||
"mtp",
|
||||
"xac",
|
||||
"xad",
|
||||
"xar",
|
||||
"xau",
|
||||
"xban",
|
||||
"xbc",
|
||||
"xbd",
|
||||
"xbr",
|
||||
"xbu",
|
||||
@@ -212,6 +214,8 @@ flagcats = {
|
||||
"xbu=CMD": "execute CMD before a file upload starts",
|
||||
"xau=CMD": "execute CMD after a file upload finishes",
|
||||
"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",
|
||||
"xar=CMD": "execute CMD after a file rename/move",
|
||||
"xbd=CMD": "execute CMD before a file delete",
|
||||
|
||||
@@ -296,6 +296,7 @@ class FtpFs(AbstractedFS):
|
||||
self.uname,
|
||||
not self.args.no_scandir,
|
||||
[[True, False], [False, True]],
|
||||
throw=True,
|
||||
)
|
||||
vfs_ls = [x[0] for x in vfs_ls1]
|
||||
vfs_ls.extend(vfs_virt.keys())
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse # typechk
|
||||
import calendar
|
||||
import copy
|
||||
import errno
|
||||
import gzip
|
||||
@@ -19,7 +18,6 @@ import threading # typechk
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from email.utils import parsedate
|
||||
from operator import itemgetter
|
||||
|
||||
import jinja2 # typechk
|
||||
@@ -107,6 +105,7 @@ from .util import (
|
||||
unquotep,
|
||||
vjoin,
|
||||
vol_san,
|
||||
vroots,
|
||||
vsplit,
|
||||
wrename,
|
||||
wunlink,
|
||||
@@ -127,6 +126,11 @@ _ = (argparse, threading)
|
||||
|
||||
NO_CACHE = {"Cache-Control": "no-cache"}
|
||||
|
||||
ALL_COOKIES = "k304 no304 js idxh dots cppwd cppws".split()
|
||||
|
||||
H_CONN_KEEPALIVE = "Connection: Keep-Alive"
|
||||
H_CONN_CLOSE = "Connection: Close"
|
||||
|
||||
LOGUES = [[0, ".prologue.html"], [1, ".epilogue.html"]]
|
||||
|
||||
READMES = [[0, ["preadme.md", "PREADME.md"]], [1, ["readme.md", "README.md"]]]
|
||||
@@ -182,6 +186,7 @@ class HttpCli(object):
|
||||
self.rem = " "
|
||||
self.vpath = " "
|
||||
self.vpaths = " "
|
||||
self.dl_id = ""
|
||||
self.gctx = " " # additional context for garda
|
||||
self.trailing_slash = True
|
||||
self.uname = " "
|
||||
@@ -633,7 +638,7 @@ class HttpCli(object):
|
||||
avn.can_access("", self.uname) if avn else [False] * 8
|
||||
)
|
||||
self.avn = avn
|
||||
self.vn = vn
|
||||
self.vn = vn # note: do not dbv due to walk/zipgen
|
||||
self.rem = rem
|
||||
|
||||
self.s.settimeout(self.args.s_tbody or None)
|
||||
@@ -722,6 +727,11 @@ class HttpCli(object):
|
||||
except Pebkac:
|
||||
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:
|
||||
if self.args.plain_ip:
|
||||
return self.ip.replace(":", ".")
|
||||
@@ -792,11 +802,11 @@ class HttpCli(object):
|
||||
|
||||
def k304(self) -> bool:
|
||||
k304 = self.cookies.get("k304")
|
||||
return (
|
||||
k304 == "y"
|
||||
or (self.args.k304 == 2 and k304 != "n")
|
||||
or ("; Trident/" in self.ua and not k304)
|
||||
)
|
||||
return k304 == "y" or (self.args.k304 == 2 and k304 != "n")
|
||||
|
||||
def no304(self) -> bool:
|
||||
no304 = self.cookies.get("no304")
|
||||
return no304 == "y" or (self.args.no304 == 2 and no304 != "n")
|
||||
|
||||
def _build_html_head(self, maybe_html: Any, kv: dict[str, Any]) -> None:
|
||||
html = str(maybe_html)
|
||||
@@ -831,25 +841,28 @@ class HttpCli(object):
|
||||
) -> None:
|
||||
response = ["%s %s %s" % (self.http_ver, status, HTTPCODE[status])]
|
||||
|
||||
if length is not None:
|
||||
response.append("Content-Length: " + unicode(length))
|
||||
|
||||
if status == 304 and self.k304():
|
||||
self.keepalive = False
|
||||
|
||||
# close if unknown length, otherwise take client's preference
|
||||
response.append("Connection: " + ("Keep-Alive" if self.keepalive else "Close"))
|
||||
response.append("Date: " + formatdate())
|
||||
|
||||
# headers{} overrides anything set previously
|
||||
if headers:
|
||||
self.out_headers.update(headers)
|
||||
|
||||
# default to utf8 html if no content-type is set
|
||||
if not mime:
|
||||
mime = self.out_headers.get("Content-Type") or "text/html; charset=utf-8"
|
||||
if status == 304:
|
||||
self.out_headers.pop("Content-Length", None)
|
||||
self.out_headers.pop("Content-Type", None)
|
||||
self.out_headerlist.clear()
|
||||
if self.k304():
|
||||
self.keepalive = False
|
||||
else:
|
||||
if length is not None:
|
||||
response.append("Content-Length: " + unicode(length))
|
||||
|
||||
self.out_headers["Content-Type"] = mime
|
||||
if mime:
|
||||
self.out_headers["Content-Type"] = mime
|
||||
elif "Content-Type" not in self.out_headers:
|
||||
self.out_headers["Content-Type"] = "text/html; charset=utf-8"
|
||||
|
||||
# close if unknown length, otherwise take client's preference
|
||||
response.append(H_CONN_KEEPALIVE if self.keepalive else H_CONN_CLOSE)
|
||||
response.append("Date: " + formatdate())
|
||||
|
||||
for k, zs in list(self.out_headers.items()) + self.out_headerlist:
|
||||
response.append("%s: %s" % (k, zs))
|
||||
@@ -863,6 +876,19 @@ class HttpCli(object):
|
||||
self.cbonk(self.conn.hsrv.gmal, zs, "cc_hdr", "Cc in out-hdr")
|
||||
raise Pebkac(999)
|
||||
|
||||
if self.args.ohead and self.do_log:
|
||||
keys = self.args.ohead
|
||||
if "*" in keys:
|
||||
lines = response[1:]
|
||||
else:
|
||||
lines = []
|
||||
for zs in response[1:]:
|
||||
if zs.split(":")[0].lower() in keys:
|
||||
lines.append(zs)
|
||||
for zs in lines:
|
||||
hk, hv = zs.split(": ")
|
||||
self.log("[O] {}: \033[33m[{}]".format(hk, hv), 5)
|
||||
|
||||
response.append("\r\n")
|
||||
try:
|
||||
self.s.sendall("\r\n".join(response).encode("utf-8"))
|
||||
@@ -942,13 +968,14 @@ class HttpCli(object):
|
||||
|
||||
lines = [
|
||||
"%s %s %s" % (self.http_ver or "HTTP/1.1", status, HTTPCODE[status]),
|
||||
"Connection: Close",
|
||||
H_CONN_CLOSE,
|
||||
]
|
||||
|
||||
if body:
|
||||
lines.append("Content-Length: " + unicode(len(body)))
|
||||
|
||||
self.s.sendall("\r\n".join(lines).encode("utf-8") + b"\r\n\r\n" + body)
|
||||
lines.append("\r\n")
|
||||
self.s.sendall("\r\n".join(lines).encode("utf-8") + body)
|
||||
|
||||
def urlq(self, add: dict[str, str], rm: list[str]) -> str:
|
||||
"""
|
||||
@@ -1175,6 +1202,9 @@ class HttpCli(object):
|
||||
if "move" in self.uparam:
|
||||
return self.handle_mv()
|
||||
|
||||
if "copy" in self.uparam:
|
||||
return self.handle_cp()
|
||||
|
||||
if not self.vpath and self.ouparam:
|
||||
if "reload" in self.uparam:
|
||||
return self.handle_reload()
|
||||
@@ -1182,12 +1212,6 @@ class HttpCli(object):
|
||||
if "stack" in self.uparam:
|
||||
return self.tx_stack()
|
||||
|
||||
if "ups" in self.uparam:
|
||||
return self.tx_ups()
|
||||
|
||||
if "k304" in self.uparam:
|
||||
return self.set_k304()
|
||||
|
||||
if "setck" in self.uparam:
|
||||
return self.setck()
|
||||
|
||||
@@ -1200,9 +1224,16 @@ class HttpCli(object):
|
||||
if "shares" in self.uparam:
|
||||
return self.tx_shares()
|
||||
|
||||
if "dls" in self.uparam:
|
||||
return self.tx_dls()
|
||||
|
||||
if "h" in self.uparam:
|
||||
return self.tx_mounts()
|
||||
|
||||
if "ups" in self.uparam:
|
||||
# vpath is used for share translation
|
||||
return self.tx_ups()
|
||||
|
||||
if "rss" in self.uparam:
|
||||
return self.tx_rss()
|
||||
|
||||
@@ -1431,6 +1462,7 @@ class HttpCli(object):
|
||||
not self.args.no_scandir,
|
||||
[[True, False]],
|
||||
lstat="davrt" not in vn.flags,
|
||||
throw=True,
|
||||
)
|
||||
if not self.can_read:
|
||||
vfs_ls = []
|
||||
@@ -1771,6 +1803,9 @@ class HttpCli(object):
|
||||
if "move" in self.uparam:
|
||||
return self.handle_mv()
|
||||
|
||||
if "copy" in self.uparam:
|
||||
return self.handle_cp()
|
||||
|
||||
if "delete" in self.uparam:
|
||||
return self.handle_rm([])
|
||||
|
||||
@@ -2391,6 +2426,15 @@ class HttpCli(object):
|
||||
if "purl" in ret:
|
||||
ret["purl"] = self.args.SR + ret["purl"]
|
||||
|
||||
if self.args.shr and self.vpath.startswith(self.args.shr1):
|
||||
# strip common suffix (uploader's folder structure)
|
||||
vp_req, vp_vfs = vroots(self.vpath, vjoin(dbv.vpath, vrem))
|
||||
if not ret["purl"].startswith(vp_vfs):
|
||||
t = "share-mapping failed; req=[%s] dbv=[%s] vrem=[%s] n1=[%s] n2=[%s] purl=[%s]"
|
||||
zt = (self.vpath, dbv.vpath, vrem, vp_req, vp_vfs, ret["purl"])
|
||||
raise Pebkac(500, t % zt)
|
||||
ret["purl"] = vp_req + ret["purl"][len(vp_vfs) :]
|
||||
|
||||
ret = json.dumps(ret)
|
||||
self.log(ret)
|
||||
self.reply(ret.encode("utf-8"), mime="application/json")
|
||||
@@ -2483,7 +2527,11 @@ class HttpCli(object):
|
||||
chashes.append(siblings[n : n + clen])
|
||||
|
||||
vfs, _ = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||
ptop = (vfs.dbv or vfs).realpath
|
||||
ptop = vfs.get_dbv("")[0].realpath
|
||||
# if this is a share, then get_dbv has been overridden to return
|
||||
# the dbv (which does not exist as a property). And its realpath
|
||||
# could point into the middle of its origin vfs node, meaning it
|
||||
# is not necessarily registered with up2k, so get_dbv is crucial
|
||||
|
||||
broker = self.conn.hsrv.broker
|
||||
x = broker.ask("up2k.handle_chunks", ptop, wark, chashes)
|
||||
@@ -3396,26 +3444,29 @@ class HttpCli(object):
|
||||
self.reply(response.encode("utf-8"))
|
||||
return True
|
||||
|
||||
def _chk_lastmod(self, file_ts: int) -> tuple[str, bool]:
|
||||
def _chk_lastmod(self, file_ts: int) -> tuple[str, bool, bool]:
|
||||
# ret: lastmod, do_send, can_range
|
||||
file_lastmod = formatdate(file_ts)
|
||||
cli_lastmod = self.headers.get("if-modified-since")
|
||||
if cli_lastmod:
|
||||
try:
|
||||
# some browser append "; length=573"
|
||||
cli_lastmod = cli_lastmod.split(";")[0].strip()
|
||||
cli_dt = parsedate(cli_lastmod)
|
||||
assert cli_dt # !rm
|
||||
cli_ts = calendar.timegm(cli_dt)
|
||||
return file_lastmod, int(file_ts) > int(cli_ts)
|
||||
except Exception as ex:
|
||||
self.log(
|
||||
"lastmod {}\nremote: [{}]\n local: [{}]".format(
|
||||
repr(ex), cli_lastmod, file_lastmod
|
||||
)
|
||||
)
|
||||
return file_lastmod, file_lastmod != cli_lastmod
|
||||
c_ifrange = self.headers.get("if-range")
|
||||
c_lastmod = self.headers.get("if-modified-since")
|
||||
|
||||
return file_lastmod, True
|
||||
if not c_ifrange and not c_lastmod:
|
||||
return file_lastmod, True, True
|
||||
|
||||
if c_ifrange and c_ifrange != file_lastmod:
|
||||
t = "sending entire file due to If-Range; cli(%s) file(%s)"
|
||||
self.log(t % (c_ifrange, file_lastmod), 6)
|
||||
return file_lastmod, True, False
|
||||
|
||||
do_send = c_lastmod != file_lastmod
|
||||
if do_send and c_lastmod:
|
||||
t = "sending body due to If-Modified-Since cli(%s) file(%s)"
|
||||
self.log(t % (c_lastmod, file_lastmod), 6)
|
||||
elif not do_send and self.no304():
|
||||
do_send = True
|
||||
self.log("sending body due to no304")
|
||||
|
||||
return file_lastmod, do_send, True
|
||||
|
||||
def _use_dirkey(self, vn: VFS, ap: str) -> bool:
|
||||
if self.can_read or not self.can_get:
|
||||
@@ -3565,7 +3616,7 @@ class HttpCli(object):
|
||||
# if-modified
|
||||
|
||||
if file_ts > 0:
|
||||
file_lastmod, do_send = self._chk_lastmod(int(file_ts))
|
||||
file_lastmod, do_send, _ = self._chk_lastmod(int(file_ts))
|
||||
self.out_headers["Last-Modified"] = file_lastmod
|
||||
if not do_send:
|
||||
status = 304
|
||||
@@ -3648,6 +3699,8 @@ class HttpCli(object):
|
||||
self.args.s_wr_sz,
|
||||
self.args.s_wr_slp,
|
||||
not self.args.no_poll,
|
||||
{},
|
||||
"",
|
||||
)
|
||||
res.close()
|
||||
|
||||
@@ -3694,6 +3747,7 @@ class HttpCli(object):
|
||||
editions: dict[str, tuple[str, int]] = {}
|
||||
for ext in ("", ".gz"):
|
||||
if ptop is not None:
|
||||
assert job and ap_data # type: ignore # !rm
|
||||
sz = job["size"]
|
||||
file_ts = job["lmod"]
|
||||
editions["plain"] = (ap_data, sz)
|
||||
@@ -3727,7 +3781,7 @@ class HttpCli(object):
|
||||
#
|
||||
# if-modified
|
||||
|
||||
file_lastmod, do_send = self._chk_lastmod(int(file_ts))
|
||||
file_lastmod, do_send, can_range = self._chk_lastmod(int(file_ts))
|
||||
self.out_headers["Last-Modified"] = file_lastmod
|
||||
if not do_send:
|
||||
status = 304
|
||||
@@ -3771,7 +3825,14 @@ class HttpCli(object):
|
||||
|
||||
# let's not support 206 with compression
|
||||
# and multirange / multipart is also not-impl (mostly because calculating contentlength is a pain)
|
||||
if do_send and not is_compressed and hrange and file_sz and "," not in hrange:
|
||||
if (
|
||||
do_send
|
||||
and not is_compressed
|
||||
and hrange
|
||||
and can_range
|
||||
and file_sz
|
||||
and "," not in hrange
|
||||
):
|
||||
try:
|
||||
if not hrange.lower().startswith("bytes"):
|
||||
raise Exception()
|
||||
@@ -3855,7 +3916,21 @@ class HttpCli(object):
|
||||
self.send_headers(length=upper - lower, status=status, mime=mime)
|
||||
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:
|
||||
assert job and ap_data # type: ignore # !rm
|
||||
return self.tx_pipe(
|
||||
ptop, req_path, ap_data, job, lower, upper, status, mime, logmsg
|
||||
)
|
||||
@@ -3874,6 +3949,8 @@ class HttpCli(object):
|
||||
self.args.s_wr_sz,
|
||||
self.args.s_wr_slp,
|
||||
not self.args.no_poll,
|
||||
dls,
|
||||
self.dl_id,
|
||||
)
|
||||
|
||||
if remains > 0:
|
||||
@@ -4024,6 +4101,8 @@ class HttpCli(object):
|
||||
wr_sz,
|
||||
wr_slp,
|
||||
not self.args.no_poll,
|
||||
self.conn.hsrv.dls,
|
||||
self.dl_id,
|
||||
)
|
||||
|
||||
spd = self._spd((upper - lower) - remains)
|
||||
@@ -4109,6 +4188,18 @@ class HttpCli(object):
|
||||
self.log("transcoding to [{}]".format(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(
|
||||
self.log,
|
||||
self.asrv,
|
||||
@@ -4117,6 +4208,7 @@ class HttpCli(object):
|
||||
pre_crc="crc" in uarg,
|
||||
cmp=uarg if cancmp or uarg == "pax" else "",
|
||||
)
|
||||
n = 0
|
||||
bsent = 0
|
||||
for buf in bgen.gen():
|
||||
if not buf:
|
||||
@@ -4130,6 +4222,11 @@ class HttpCli(object):
|
||||
bgen.stop()
|
||||
break
|
||||
|
||||
n += 1
|
||||
if n >= 4:
|
||||
n = 0
|
||||
dls[self.dl_id] = (time.time(), bsent)
|
||||
|
||||
spd = self._spd(bsent)
|
||||
self.log("{}, {}".format(logmsg, spd))
|
||||
return True
|
||||
@@ -4242,7 +4339,7 @@ class HttpCli(object):
|
||||
sz_md = len(lead) + len(fullfile)
|
||||
|
||||
file_ts = int(max(ts_md, self.E.t0))
|
||||
file_lastmod, do_send = self._chk_lastmod(file_ts)
|
||||
file_lastmod, do_send, _ = self._chk_lastmod(file_ts)
|
||||
self.out_headers["Last-Modified"] = file_lastmod
|
||||
self.out_headers.update(NO_CACHE)
|
||||
status = 200 if do_send else 304
|
||||
@@ -4387,6 +4484,32 @@ class HttpCli(object):
|
||||
|
||||
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", "")
|
||||
if not fmt and (self.ua.startswith("curl/") or self.ua.startswith("fetch")):
|
||||
fmt = "v"
|
||||
@@ -4408,6 +4531,12 @@ class HttpCli(object):
|
||||
txt += "\n%s" % (", ".join((str(x) for x in zt)),)
|
||||
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:
|
||||
txt += "\nyou can browse:"
|
||||
for v in rvol:
|
||||
@@ -4429,8 +4558,9 @@ class HttpCli(object):
|
||||
rvol=rvol,
|
||||
wvol=wvol,
|
||||
avol=avol,
|
||||
in_shr=self.args.shr and self.vpath.startswith(self.args.shr[1:]),
|
||||
in_shr=self.args.shr and self.vpath.startswith(self.args.shr1),
|
||||
vstate=vstate,
|
||||
dls=dls,
|
||||
ups=ups,
|
||||
scanning=vs["scanning"],
|
||||
hashq=vs["hashq"],
|
||||
@@ -4439,7 +4569,9 @@ class HttpCli(object):
|
||||
dbwt=vs["dbwt"],
|
||||
url_suf=suf,
|
||||
k304=self.k304(),
|
||||
no304=self.no304(),
|
||||
k304vis=self.args.k304 > 0,
|
||||
no304vis=self.args.no304 > 0,
|
||||
ver=S_VERSION if self.args.ver else "",
|
||||
chpw=self.args.chpw and self.uname != "*",
|
||||
ahttps="" if self.is_https else "https://" + self.host + self.req,
|
||||
@@ -4447,29 +4579,21 @@ class HttpCli(object):
|
||||
self.reply(html.encode("utf-8"))
|
||||
return True
|
||||
|
||||
def set_k304(self) -> bool:
|
||||
v = self.uparam["k304"].lower()
|
||||
if v in "yn":
|
||||
dur = 86400 * 299
|
||||
else:
|
||||
dur = 0
|
||||
v = "x"
|
||||
|
||||
ck = gencookie("k304", v, self.args.R, False, dur)
|
||||
self.out_headerlist.append(("Set-Cookie", ck))
|
||||
self.redirect("", "?h#cc")
|
||||
return True
|
||||
|
||||
def setck(self) -> bool:
|
||||
k, v = self.uparam["setck"].split("=", 1)
|
||||
t = 0 if v == "" else 86400 * 299
|
||||
t = 0 if v in ("", "x") else 86400 * 299
|
||||
ck = gencookie(k, v, self.args.R, False, t)
|
||||
self.out_headerlist.append(("Set-Cookie", ck))
|
||||
self.reply(b"o7\n")
|
||||
if "cc" in self.ouparam:
|
||||
self.redirect("", "?h#cc")
|
||||
else:
|
||||
self.reply(b"o7\n")
|
||||
return True
|
||||
|
||||
def set_cfg_reset(self) -> bool:
|
||||
for k in ("k304", "js", "idxh", "dots", "cppwd", "cppws"):
|
||||
for k in ALL_COOKIES:
|
||||
if k not in self.cookies:
|
||||
continue
|
||||
cookie = gencookie(k, "x", self.args.R, False)
|
||||
self.out_headerlist.append(("Set-Cookie", cookie))
|
||||
|
||||
@@ -4499,8 +4623,14 @@ class HttpCli(object):
|
||||
|
||||
t = t.format(self.args.SR)
|
||||
qv = quotep(self.vpaths) + self.ourlq()
|
||||
in_shr = self.args.shr and self.vpath.startswith(self.args.shr[1:])
|
||||
html = self.j2s("splash", this=self, qvpath=qv, in_shr=in_shr, msg=t)
|
||||
html = self.j2s(
|
||||
"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)
|
||||
return True
|
||||
|
||||
@@ -4548,7 +4678,7 @@ class HttpCli(object):
|
||||
if self.args.no_reload:
|
||||
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)
|
||||
|
||||
def tx_stack(self) -> bool:
|
||||
@@ -4651,6 +4781,40 @@ class HttpCli(object):
|
||||
ret["a"] = dirs
|
||||
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:
|
||||
idx = self.conn.get_u2idx()
|
||||
if not idx or not hasattr(idx, "p_end"):
|
||||
@@ -4662,6 +4826,11 @@ class HttpCli(object):
|
||||
lm = "ups [{}]".format(filt)
|
||||
self.log(lm)
|
||||
|
||||
if self.args.shr and self.vpath.startswith(self.args.shr1):
|
||||
shr_dbv, shr_vrem = self.vn.get_dbv(self.rem)
|
||||
else:
|
||||
shr_dbv = None
|
||||
|
||||
ret: list[dict[str, Any]] = []
|
||||
t0 = time.time()
|
||||
lim = time.time() - self.args.unpost
|
||||
@@ -4682,7 +4851,12 @@ class HttpCli(object):
|
||||
else:
|
||||
allvols = list(self.asrv.vfs.all_vols.values())
|
||||
|
||||
allvols = [x for x in allvols if "e2d" in x.flags]
|
||||
allvols = [
|
||||
x
|
||||
for x in allvols
|
||||
if "e2d" in x.flags
|
||||
and ("*" in x.axs.uwrite or self.uname in x.axs.uwrite or x == shr_dbv)
|
||||
]
|
||||
|
||||
for vol in allvols:
|
||||
cur = idx.get_cur(vol)
|
||||
@@ -4732,6 +4906,16 @@ class HttpCli(object):
|
||||
|
||||
ret = ret[:2000]
|
||||
|
||||
if shr_dbv:
|
||||
# translate vpaths from share-target to share-url
|
||||
# to satisfy access checks
|
||||
assert shr_vrem.split # type: ignore # !rm
|
||||
vp_shr, vp_vfs = vroots(self.vpath, vjoin(shr_dbv.vpath, shr_vrem))
|
||||
for v in ret:
|
||||
vp = v["vp"]
|
||||
if vp.startswith(vp_vfs):
|
||||
v["vp"] = vp_shr + vp[len(vp_vfs) :]
|
||||
|
||||
if self.is_vproxied:
|
||||
for v in ret:
|
||||
v["vp"] = self.args.SR + v["vp"]
|
||||
@@ -4816,7 +5000,7 @@ class HttpCli(object):
|
||||
|
||||
cur.connection.commit()
|
||||
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.redirect(self.args.SRS + "?shares")
|
||||
@@ -4861,7 +5045,7 @@ class HttpCli(object):
|
||||
if m:
|
||||
raise Pebkac(400, "sharekey has illegal character [%s]" % (m[1],))
|
||||
|
||||
if vp.startswith(self.args.shr[1:]):
|
||||
if vp.startswith(self.args.shr1):
|
||||
raise Pebkac(400, "yo dawg...")
|
||||
|
||||
cur = idx.get_shr()
|
||||
@@ -4907,7 +5091,7 @@ class HttpCli(object):
|
||||
cur.execute(q, (skey, fn))
|
||||
|
||||
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()
|
||||
|
||||
fn = quotep(fns[0]) if len(fns) == 1 else ""
|
||||
@@ -4958,16 +5142,39 @@ class HttpCli(object):
|
||||
return self._mv(self.vpath, dst.lstrip("/"))
|
||||
|
||||
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:
|
||||
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)
|
||||
self.loud_reply(x.get(), status=201)
|
||||
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:
|
||||
dirs = ls["dirs"]
|
||||
files = ls["files"]
|
||||
@@ -5397,6 +5604,7 @@ class HttpCli(object):
|
||||
not self.args.no_scandir,
|
||||
[[True, False], [False, True]],
|
||||
lstat="lt" in self.uparam,
|
||||
throw=True,
|
||||
)
|
||||
stats = {k: v for k, v in vfs_ls}
|
||||
ls_names = [x[0] for x in vfs_ls]
|
||||
|
||||
@@ -81,6 +81,7 @@ from .util import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .authsrv import VFS
|
||||
from .broker_util import BrokerCli
|
||||
from .ssdp import SSDPr
|
||||
|
||||
@@ -130,6 +131,12 @@ class HttpSrv(object):
|
||||
self.bans: 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.name = "hsrv" + nsuf
|
||||
self.mutex = threading.Lock()
|
||||
@@ -205,6 +212,9 @@ class HttpSrv(object):
|
||||
self.start_threads(4)
|
||||
|
||||
if nid:
|
||||
self.tdli = {}
|
||||
self.tdls = {}
|
||||
|
||||
if self.args.stackmon:
|
||||
start_stackmon(self.args.stackmon, nid)
|
||||
|
||||
@@ -579,3 +589,32 @@ class HttpSrv(object):
|
||||
ident += "a"
|
||||
|
||||
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_vols[c]
|
||||
dli[k] = (a, b, vn, d, e)
|
||||
|
||||
self.tdli = dli
|
||||
self.tdls = dls
|
||||
|
||||
@@ -72,6 +72,9 @@ class Metrics(object):
|
||||
v = "{:.3f}".format(self.hsrv.t0)
|
||||
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"
|
||||
addg("cpp_http_conns", str(self.hsrv.ncli), t)
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ class SvcHub(object):
|
||||
self.stopping = False
|
||||
self.stopped = False
|
||||
self.reload_req = False
|
||||
self.reloading = 0
|
||||
self.reload_mutex = threading.Lock()
|
||||
self.stop_cond = threading.Condition()
|
||||
self.nsigs = 3
|
||||
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"
|
||||
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:
|
||||
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)
|
||||
@@ -223,13 +232,14 @@ class SvcHub(object):
|
||||
args.chpw_no = noch
|
||||
|
||||
if args.ipu:
|
||||
iu, nm = load_ipu(self.log, args.ipu)
|
||||
iu, nm = load_ipu(self.log, args.ipu, True)
|
||||
setattr(args, "ipu_iu", iu)
|
||||
setattr(args, "ipu_nm", nm)
|
||||
|
||||
if not self.args.no_ses:
|
||||
self.setup_session_db()
|
||||
|
||||
args.shr1 = ""
|
||||
if args.shr:
|
||||
self.setup_share_db()
|
||||
|
||||
@@ -378,6 +388,14 @@ class SvcHub(object):
|
||||
|
||||
self.broker = Broker(self)
|
||||
|
||||
# create netmaps early to avoid firewall gaps,
|
||||
# but the mutex blocks multiprocessing startup
|
||||
for zs in "ipu_iu ftp_ipa_nm tftp_ipa_nm".split():
|
||||
try:
|
||||
getattr(args, zs).mutex = threading.Lock()
|
||||
except:
|
||||
pass
|
||||
|
||||
def setup_session_db(self) -> None:
|
||||
if not HAVE_SQLITE3:
|
||||
self.args.no_ses = True
|
||||
@@ -452,6 +470,7 @@ class SvcHub(object):
|
||||
raise Exception(t)
|
||||
|
||||
al.shr = "/%s/" % (al.shr,)
|
||||
al.shr1 = al.shr[1:]
|
||||
|
||||
create = True
|
||||
modified = False
|
||||
@@ -761,8 +780,8 @@ class SvcHub(object):
|
||||
al.idp_h_grp = al.idp_h_grp.lower()
|
||||
al.idp_h_key = al.idp_h_key.lower()
|
||||
|
||||
al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa)
|
||||
al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa)
|
||||
al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa, True)
|
||||
al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa, True)
|
||||
|
||||
mte = ODict.fromkeys(DEF_MTE.split(","), True)
|
||||
al.mte = odfusion(mte, al.mte)
|
||||
@@ -809,6 +828,24 @@ class SvcHub(object):
|
||||
if len(al.tcolor) == 3: # fc5 => ffcc55
|
||||
al.tcolor = "".join([x * 2 for x in al.tcolor])
|
||||
|
||||
zs = al.u2sz
|
||||
zsl = zs.split(",")
|
||||
if len(zsl) not in (1, 3):
|
||||
t = "invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)"
|
||||
raise Exception(t)
|
||||
if len(zsl) < 3:
|
||||
zsl = ["1", zs, zs]
|
||||
zi2 = 1
|
||||
for zs in zsl:
|
||||
zi = int(zs)
|
||||
# arbitrary constraint (anything above 2 GiB is probably unintended)
|
||||
if zi < 1 or zi > 2047:
|
||||
raise Exception("invalid --u2sz; minimum is 1, max is 2047")
|
||||
if zi < zi2:
|
||||
raise Exception("invalid --u2sz; values must be equal or ascending")
|
||||
zi2 = zi
|
||||
al.u2sz = ",".join(zsl)
|
||||
|
||||
return True
|
||||
|
||||
def _ipa2re(self, txt) -> Optional[re.Pattern]:
|
||||
@@ -976,41 +1013,18 @@ class SvcHub(object):
|
||||
except:
|
||||
self.log("root", "ssdp startup failed;\n" + min_ex(), 3)
|
||||
|
||||
def reload(self) -> str:
|
||||
with self.up2k.mutex:
|
||||
if self.reloading:
|
||||
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
|
||||
def reload(self, rescan_all_vols: bool, up2k: bool) -> str:
|
||||
t = "config has been reloaded"
|
||||
with self.reload_mutex:
|
||||
self.log("root", "reloading config")
|
||||
self.asrv.reload(9 if up2k else 4)
|
||||
if up2k:
|
||||
self.up2k.reload(rescan_all_vols)
|
||||
t += "; volumes are now reinitializing"
|
||||
else:
|
||||
self.log("root", "reload done")
|
||||
self.broker.reload()
|
||||
self.reloading = 0
|
||||
|
||||
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)
|
||||
return t
|
||||
|
||||
def _reload_sessions(self) -> None:
|
||||
with self.asrv.mutex:
|
||||
@@ -1024,7 +1038,7 @@ class SvcHub(object):
|
||||
|
||||
if self.reload_req:
|
||||
self.reload_req = False
|
||||
self.reload()
|
||||
self.reload(True, True)
|
||||
|
||||
self.shutdown()
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ def gen_hdr(
|
||||
|
||||
# spec says to put zeros when !crc if bit3 (streaming)
|
||||
# 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
|
||||
ret += spack(b"<LL", vsz, vsz)
|
||||
|
||||
|
||||
@@ -371,7 +371,7 @@ class TcpSrv(object):
|
||||
if self.args.q:
|
||||
print(msg)
|
||||
|
||||
self.hub.broker.say("listen", srv)
|
||||
self.hub.broker.say("httpsrv.listen", srv)
|
||||
|
||||
self.srv = srvs
|
||||
self.bound = bound
|
||||
@@ -379,7 +379,7 @@ class TcpSrv(object):
|
||||
self._distribute_netdevs()
|
||||
|
||||
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()
|
||||
gencert(self.log, self.args, self.netdevs)
|
||||
self.hub.restart_ftpd()
|
||||
|
||||
@@ -269,6 +269,7 @@ class Tftpd(object):
|
||||
"*",
|
||||
not self.args.no_scandir,
|
||||
[[True, False]],
|
||||
throw=True,
|
||||
)
|
||||
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]
|
||||
|
||||
@@ -20,7 +20,6 @@ from .util import (
|
||||
FFMPEG_URL,
|
||||
Cooldown,
|
||||
Daemon,
|
||||
Pebkac,
|
||||
afsenc,
|
||||
fsenc,
|
||||
min_ex,
|
||||
@@ -164,6 +163,7 @@ class ThumbSrv(object):
|
||||
self.ram: dict[str, float] = {}
|
||||
self.memcond = threading.Condition(self.mutex)
|
||||
self.stopping = False
|
||||
self.rm_nullthumbs = True # forget failed conversions on startup
|
||||
self.nthr = max(1, self.args.th_mt)
|
||||
|
||||
self.q: Queue[Optional[tuple[str, str, str, VFS]]] = Queue(self.nthr * 4)
|
||||
@@ -862,7 +862,6 @@ class ThumbSrv(object):
|
||||
def cleaner(self) -> None:
|
||||
interval = self.args.th_clean
|
||||
while True:
|
||||
time.sleep(interval)
|
||||
ndirs = 0
|
||||
for vol, histpath in self.asrv.vfs.histtab.items():
|
||||
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 ok; rm {} dirs".format(ndirs))
|
||||
self.rm_nullthumbs = False
|
||||
time.sleep(interval)
|
||||
|
||||
def clean(self, histpath: str) -> int:
|
||||
ret = 0
|
||||
@@ -896,7 +897,9 @@ class ThumbSrv(object):
|
||||
prev_b64 = None
|
||||
prev_fp = ""
|
||||
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))
|
||||
except:
|
||||
return 0
|
||||
@@ -937,6 +940,10 @@ class ThumbSrv(object):
|
||||
|
||||
continue
|
||||
|
||||
if self.rm_nullthumbs and not inf.st_size:
|
||||
bos.unlink(fp)
|
||||
continue
|
||||
|
||||
if b64 == prev_b64:
|
||||
self.log("rm replaced [{}]".format(fp))
|
||||
bos.unlink(prev_fp)
|
||||
|
||||
@@ -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(","))
|
||||
|
||||
|
||||
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)"
|
||||
|
||||
|
||||
@@ -125,12 +127,22 @@ class Up2k(object):
|
||||
self.args = hub.args
|
||||
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.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$")
|
||||
|
||||
self.gid = 0
|
||||
self.gt0 = 0
|
||||
self.gt1 = 0
|
||||
self.stop = False
|
||||
self.mutex = threading.Lock()
|
||||
self.reload_mutex = threading.Lock()
|
||||
self.reload_flag = 0
|
||||
self.reloading = False
|
||||
self.blocked: Optional[str] = None
|
||||
self.pp: Optional[ProgressPrinter] = None
|
||||
self.rescan_cond = threading.Condition()
|
||||
@@ -203,7 +215,38 @@ class Up2k(object):
|
||||
|
||||
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:
|
||||
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"""
|
||||
self.log("reload #{} scheduled".format(self.gid + 1))
|
||||
all_vols = self.asrv.vfs.all_vols
|
||||
@@ -228,10 +271,7 @@ class Up2k(object):
|
||||
with self.mutex, self.reg_mutex:
|
||||
self._drop_caches()
|
||||
|
||||
if self.pp:
|
||||
self.pp.end = True
|
||||
self.pp = None
|
||||
|
||||
self.unpp()
|
||||
return
|
||||
|
||||
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]]:
|
||||
ret = []
|
||||
for vtop in self.asrv.vfs.aread[uname]:
|
||||
vfs = self.asrv.vfs.all_vols.get(vtop)
|
||||
for vtop in self.vfs.aread.get(uname) or []:
|
||||
vfs = self.vfs.all_vols.get(vtop)
|
||||
if not vfs: # dbv only
|
||||
continue
|
||||
ptop = vfs.realpath
|
||||
@@ -485,6 +525,12 @@ class Up2k(object):
|
||||
if self.stop:
|
||||
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()
|
||||
if now < cooldown:
|
||||
# self.log("SR: cd - now = {:.2f}".format(cooldown - now), 5)
|
||||
@@ -521,7 +567,7 @@ class Up2k(object):
|
||||
raise
|
||||
|
||||
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")
|
||||
if not maxage:
|
||||
continue
|
||||
@@ -554,7 +600,7 @@ class Up2k(object):
|
||||
|
||||
if vols:
|
||||
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:
|
||||
for v in vols:
|
||||
self.need_rescan.add(v)
|
||||
@@ -567,7 +613,7 @@ class Up2k(object):
|
||||
def _check_lifetimes(self) -> float:
|
||||
now = time.time()
|
||||
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")
|
||||
if not lifetime:
|
||||
continue
|
||||
@@ -621,7 +667,7 @@ class Up2k(object):
|
||||
maxage = self.args.shr_rt * 60
|
||||
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
|
||||
|
||||
db = sqlite3.connect(self.args.shr_db, timeout=2)
|
||||
@@ -646,7 +692,7 @@ class Up2k(object):
|
||||
db.commit()
|
||||
|
||||
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 > ?"
|
||||
(earliest,) = cur.execute(q, (1,)).fetchone()
|
||||
@@ -672,7 +718,7 @@ class Up2k(object):
|
||||
return 2
|
||||
|
||||
ret = 9001
|
||||
for _, vol in sorted(self.asrv.vfs.all_vols.items()):
|
||||
for _, vol in sorted(self.vfs.all_vols.items()):
|
||||
rp = vol.realpath
|
||||
cur = self.cur.get(rp)
|
||||
if not cur:
|
||||
@@ -774,6 +820,8 @@ class Up2k(object):
|
||||
with self.mutex:
|
||||
gid = self.gid
|
||||
|
||||
self.gt0 = time.time()
|
||||
|
||||
nspin = 0
|
||||
while True:
|
||||
nspin += 1
|
||||
@@ -796,6 +844,11 @@ class Up2k(object):
|
||||
if 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())
|
||||
t0 = time.time()
|
||||
have_e2d = False
|
||||
@@ -859,7 +912,7 @@ class Up2k(object):
|
||||
self._drop_caches()
|
||||
|
||||
for vol in vols:
|
||||
if self.stop:
|
||||
if self.stop or gid != self.gid:
|
||||
break
|
||||
|
||||
en = set(vol.flags.get("mte", {}))
|
||||
@@ -990,7 +1043,7 @@ class Up2k(object):
|
||||
if self.mtag:
|
||||
Daemon(self._run_all_mtp, "up2k-mtp-scan", (gid,))
|
||||
else:
|
||||
self.pp = None
|
||||
self.unpp()
|
||||
|
||||
return have_e2d
|
||||
|
||||
@@ -998,7 +1051,7 @@ class Up2k(object):
|
||||
self, ptop: str, flags: dict[str, Any]
|
||||
) -> Optional[tuple["sqlite3.Cursor", str]]:
|
||||
"""mutex(main,reg) me"""
|
||||
histpath = self.asrv.vfs.histtab.get(ptop)
|
||||
histpath = self.vfs.histtab.get(ptop)
|
||||
if not histpath:
|
||||
self.log("no histpath for [{}]".format(ptop))
|
||||
return None
|
||||
@@ -1011,7 +1064,7 @@ class Up2k(object):
|
||||
return None
|
||||
|
||||
vpath = "?"
|
||||
for k, v in self.asrv.vfs.all_vols.items():
|
||||
for k, v in self.vfs.all_vols.items():
|
||||
if v.realpath == ptop:
|
||||
vpath = k
|
||||
|
||||
@@ -1178,7 +1231,7 @@ class Up2k(object):
|
||||
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
|
||||
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.sort()
|
||||
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)
|
||||
]
|
||||
excl += [absreal(x) for x in excl]
|
||||
excl += list(self.asrv.vfs.histtab.values())
|
||||
excl += list(self.vfs.histtab.values())
|
||||
if WINDOWS:
|
||||
excl = [x.replace("/", "\\") for x in excl]
|
||||
else:
|
||||
@@ -1347,7 +1400,7 @@ class Up2k(object):
|
||||
rds = rd + "/" if rd else ""
|
||||
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)
|
||||
partials = set([x[0] for x in gl if "PARTIAL" in x[0]])
|
||||
for iname, inf in gl:
|
||||
@@ -1411,7 +1464,7 @@ class Up2k(object):
|
||||
t = "failed to index subdir [{}]:\n{}"
|
||||
self.log(t.format(abspath, min_ex()), c=1)
|
||||
elif not stat.S_ISREG(inf.st_mode):
|
||||
self.log("skip type-{:x} file [{}]".format(inf.st_mode, abspath))
|
||||
self.log("skip type-0%o file [%s]" % (inf.st_mode, abspath))
|
||||
else:
|
||||
# self.log("file: {}".format(abspath))
|
||||
if rp.endswith(".PARTIAL") and time.time() - lmod < 60:
|
||||
@@ -1733,7 +1786,7 @@ class Up2k(object):
|
||||
|
||||
excl = [
|
||||
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)
|
||||
]
|
||||
qexa: list[str] = []
|
||||
@@ -1885,7 +1938,7 @@ class Up2k(object):
|
||||
def _drop_caches(self) -> None:
|
||||
"""mutex(main,reg) me"""
|
||||
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)
|
||||
if not reg:
|
||||
continue
|
||||
@@ -2113,7 +2166,7 @@ class Up2k(object):
|
||||
self._run_one_mtp(ptop, gid)
|
||||
|
||||
vtop = "\n"
|
||||
for vol in self.asrv.vfs.all_vols.values():
|
||||
for vol in self.vfs.all_vols.values():
|
||||
if vol.realpath == ptop:
|
||||
vtop = vol.vpath
|
||||
if "running mtp" in self.volstate.get(vtop, ""):
|
||||
@@ -2123,7 +2176,7 @@ class Up2k(object):
|
||||
msg = "mtp finished in {:.2f} sec ({})"
|
||||
self.log(msg.format(td, s2hms(td, True)))
|
||||
|
||||
self.pp = None
|
||||
self.unpp()
|
||||
if self.args.exit == "idx":
|
||||
self.hub.sigterm()
|
||||
|
||||
@@ -2765,6 +2818,9 @@ class Up2k(object):
|
||||
) -> dict[str, Any]:
|
||||
# busy_aps is u2fh (always undefined if -j0) so this is safe
|
||||
self.busy_aps = busy_aps
|
||||
if self.reload_flag or self.reloading:
|
||||
raise Pebkac(503, SBUSY % ("fs-reload",))
|
||||
|
||||
got_lock = False
|
||||
try:
|
||||
# bit expensive; 3.9=10x 3.11=2x
|
||||
@@ -2773,8 +2829,7 @@ class Up2k(object):
|
||||
with self.reg_mutex:
|
||||
ret = self._handle_json(cj)
|
||||
else:
|
||||
t = "cannot receive uploads right now;\nserver busy with {}.\nPlease wait; the client will retry..."
|
||||
raise Pebkac(503, t.format(self.blocked or "[unknown]"))
|
||||
raise Pebkac(503, SBUSY % (self.blocked or "[unknown]",))
|
||||
except TypeError:
|
||||
if not PY2:
|
||||
raise
|
||||
@@ -2816,7 +2871,7 @@ class Up2k(object):
|
||||
if True:
|
||||
jcur = self.cur.get(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"))
|
||||
noclone = bool(vfs.flags.get("noclone"))
|
||||
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]] = []
|
||||
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)
|
||||
vtop = cj["vtop"] if cur == jcur else cvfs.vpath
|
||||
|
||||
@@ -3083,7 +3138,7 @@ class Up2k(object):
|
||||
vp,
|
||||
job["host"],
|
||||
job["user"],
|
||||
self.asrv.vfs.get_perms(job["vtop"], job["user"]),
|
||||
self.vfs.get_perms(job["vtop"], job["user"]),
|
||||
job["lmod"],
|
||||
job["size"],
|
||||
job["addr"],
|
||||
@@ -3095,7 +3150,7 @@ class Up2k(object):
|
||||
self.log(t, 1)
|
||||
raise Pebkac(403, t)
|
||||
if hr.get("reloc"):
|
||||
x = pathmod(self.asrv.vfs, dst, vp, hr["reloc"])
|
||||
x = pathmod(self.vfs, dst, vp, hr["reloc"])
|
||||
if x:
|
||||
zvfs = vfs
|
||||
pdir, _, job["name"], (vfs, rem) = x
|
||||
@@ -3555,7 +3610,7 @@ class Up2k(object):
|
||||
wake_sr = False
|
||||
try:
|
||||
flt = job["life"]
|
||||
vfs = self.asrv.vfs.all_vols[job["vtop"]]
|
||||
vfs = self.vfs.all_vols[job["vtop"]]
|
||||
vlt = vfs.flags["lifetime"]
|
||||
if vlt and flt > 1 and flt < vlt:
|
||||
upt -= vlt - flt
|
||||
@@ -3731,7 +3786,7 @@ class Up2k(object):
|
||||
djoin(vtop, rd, fn),
|
||||
host,
|
||||
usr,
|
||||
self.asrv.vfs.get_perms(djoin(vtop, rd, fn), usr),
|
||||
self.vfs.get_perms(djoin(vtop, rd, fn), usr),
|
||||
ts,
|
||||
sz,
|
||||
ip,
|
||||
@@ -3841,13 +3896,13 @@ class Up2k(object):
|
||||
partial = ""
|
||||
if not unpost:
|
||||
permsets = [[True, False, False, True]]
|
||||
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0])
|
||||
vn, rem = vn.get_dbv(rem)
|
||||
vn0, rem0 = self.vfs.get(vpath, uname, *permsets[0])
|
||||
vn, rem = vn0.get_dbv(rem0)
|
||||
else:
|
||||
# unpost with missing permissions? verify with db
|
||||
permsets = [[False, True]]
|
||||
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0])
|
||||
vn, rem = vn.get_dbv(rem)
|
||||
vn0, rem0 = self.vfs.get(vpath, uname, *permsets[0])
|
||||
vn, rem = vn0.get_dbv(rem0)
|
||||
ptop = vn.realpath
|
||||
with self.mutex, self.reg_mutex:
|
||||
abrt_cfg = self.flags.get(ptop, {}).get("u2abort", 1)
|
||||
@@ -3903,17 +3958,17 @@ class Up2k(object):
|
||||
|
||||
scandir = not self.args.no_scandir
|
||||
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:
|
||||
raise Pebkac(400, "cannot unpost folders")
|
||||
elif stat.S_ISLNK(st.st_mode) or stat.S_ISREG(st.st_mode):
|
||||
dbv, vrem = self.asrv.vfs.get(vpath, uname, *permsets[0])
|
||||
dbv, vrem = dbv.get_dbv(vrem)
|
||||
voldir = vsplit(vrem)[0]
|
||||
voldir = vsplit(rem)[0]
|
||||
vpath_dir = vsplit(vpath)[0]
|
||||
g = [(dbv, voldir, vpath_dir, adir, [(fn, 0)], [], {})] # type: ignore
|
||||
g = [(vn, voldir, vpath_dir, adir, [(fn, 0)], [], {})] # type: ignore
|
||||
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, [], []
|
||||
|
||||
xbd = vn.flags.get("xbd")
|
||||
@@ -3938,7 +3993,10 @@ class Up2k(object):
|
||||
volpath = ("%s/%s" % (vrem, fn)).strip("/")
|
||||
vpath = ("%s/%s" % (dbv.vpath, volpath)).strip("/")
|
||||
self.log("rm %s\n %s" % (vpath, abspath))
|
||||
_ = dbv.get(volpath, uname, *permsets[0])
|
||||
if not unpost:
|
||||
# recursion-only sanchk
|
||||
_ = dbv.get(volpath, uname, *permsets[0])
|
||||
|
||||
if xbd:
|
||||
if not runhook(
|
||||
self.log,
|
||||
@@ -3950,7 +4008,7 @@ class Up2k(object):
|
||||
vpath,
|
||||
"",
|
||||
uname,
|
||||
self.asrv.vfs.get_perms(vpath, uname),
|
||||
self.vfs.get_perms(vpath, uname),
|
||||
stl.st_mtime,
|
||||
st.st_size,
|
||||
ip,
|
||||
@@ -3990,7 +4048,7 @@ class Up2k(object):
|
||||
vpath,
|
||||
"",
|
||||
uname,
|
||||
self.asrv.vfs.get_perms(vpath, uname),
|
||||
self.vfs.get_perms(vpath, uname),
|
||||
stl.st_mtime,
|
||||
st.st_size,
|
||||
ip,
|
||||
@@ -4010,17 +4068,226 @@ class Up2k(object):
|
||||
|
||||
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:
|
||||
if svp == dvp or dvp.startswith(svp + "/"):
|
||||
raise Pebkac(400, "mv: cannot move parent into subfolder")
|
||||
|
||||
svn, srem = self.asrv.vfs.get(svp, uname, True, False, True)
|
||||
svn, srem = svn.get_dbv(srem)
|
||||
svn, srem = self.vfs.get(svp, uname, True, False, True)
|
||||
jail, jail_rem = svn.get_dbv(srem)
|
||||
sabs = svn.canonical(srem, False)
|
||||
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")
|
||||
|
||||
st = bos.lstat(sabs)
|
||||
@@ -4034,7 +4301,9 @@ class Up2k(object):
|
||||
|
||||
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]]
|
||||
scandir = not self.args.no_scandir
|
||||
|
||||
@@ -4046,13 +4315,13 @@ class Up2k(object):
|
||||
raise Pebkac(400, "mv: source folder contains other volumes")
|
||||
|
||||
g = svn.walk("", srem, [], uname, permsets, True, scandir, True)
|
||||
for dbv, vrem, _, atop, files, rd, vd in g:
|
||||
if dbv != jail:
|
||||
# the actual check (avoid toctou)
|
||||
raise Pebkac(400, "mv: source folder contains other volumes")
|
||||
with self.mutex:
|
||||
try:
|
||||
for dbv, vrem, _, atop, files, rd, vd in g:
|
||||
if dbv != jail:
|
||||
# the actual check (avoid toctou)
|
||||
raise Pebkac(400, "mv: source folder contains other volumes")
|
||||
|
||||
with self.mutex:
|
||||
try:
|
||||
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)
|
||||
@@ -4063,11 +4332,13 @@ class Up2k(object):
|
||||
|
||||
dvpf = dvp + svpf[len(svp) :]
|
||||
self._mv_file(uname, ip, svpf, dvpf, curs)
|
||||
finally:
|
||||
|
||||
for v in curs:
|
||||
v.connection.commit()
|
||||
|
||||
curs.clear()
|
||||
curs.clear()
|
||||
finally:
|
||||
for v in curs:
|
||||
v.connection.commit()
|
||||
|
||||
rm_ok, rm_ng = rmdirs(self.log_func, scandir, True, sabs, 1)
|
||||
|
||||
@@ -4081,7 +4352,7 @@ class Up2k(object):
|
||||
rem = ap[len(sabs) :].replace(os.sep, "/").lstrip("/")
|
||||
vp = vjoin(dvp, rem)
|
||||
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))
|
||||
except:
|
||||
pass
|
||||
@@ -4092,10 +4363,10 @@ class Up2k(object):
|
||||
self, uname: str, ip: str, svp: str, dvp: str, curs: set["sqlite3.Cursor"]
|
||||
) -> str:
|
||||
"""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)
|
||||
|
||||
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)
|
||||
|
||||
sabs = svn.canonical(srem, False)
|
||||
@@ -4139,7 +4410,7 @@ class Up2k(object):
|
||||
svp,
|
||||
"",
|
||||
uname,
|
||||
self.asrv.vfs.get_perms(svp, uname),
|
||||
self.vfs.get_perms(svp, uname),
|
||||
ftime,
|
||||
fsize,
|
||||
ip,
|
||||
@@ -4179,7 +4450,7 @@ class Up2k(object):
|
||||
dvp,
|
||||
"",
|
||||
uname,
|
||||
self.asrv.vfs.get_perms(dvp, uname),
|
||||
self.vfs.get_perms(dvp, uname),
|
||||
ftime,
|
||||
fsize,
|
||||
ip,
|
||||
@@ -4292,7 +4563,7 @@ class Up2k(object):
|
||||
dvp,
|
||||
"",
|
||||
uname,
|
||||
self.asrv.vfs.get_perms(dvp, uname),
|
||||
self.vfs.get_perms(dvp, uname),
|
||||
ftime,
|
||||
fsize,
|
||||
ip,
|
||||
@@ -4605,7 +4876,7 @@ class Up2k(object):
|
||||
vp_chk,
|
||||
job["host"],
|
||||
job["user"],
|
||||
self.asrv.vfs.get_perms(vp_chk, job["user"]),
|
||||
self.vfs.get_perms(vp_chk, job["user"]),
|
||||
job["lmod"],
|
||||
job["size"],
|
||||
job["addr"],
|
||||
@@ -4617,7 +4888,7 @@ class Up2k(object):
|
||||
self.log(t, 1)
|
||||
raise Pebkac(403, t)
|
||||
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:
|
||||
zvfs = vfs
|
||||
pdir, _, job["name"], (vfs, rem) = x
|
||||
@@ -4724,7 +4995,7 @@ class Up2k(object):
|
||||
|
||||
def _snap_reg(self, ptop: str, reg: dict[str, dict[str, Any]]) -> None:
|
||||
now = time.time()
|
||||
histpath = self.asrv.vfs.histtab.get(ptop)
|
||||
histpath = self.vfs.histtab.get(ptop)
|
||||
if not histpath:
|
||||
return
|
||||
|
||||
@@ -4972,7 +5243,7 @@ class Up2k(object):
|
||||
else:
|
||||
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:
|
||||
t = "hook_fx(%s): failed to resolve %s based on %s"
|
||||
self.log(t % (act, fvp, req_vp))
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
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 ""
|
||||
if EXE:
|
||||
pybin = ""
|
||||
@@ -669,13 +690,20 @@ class HLog(logging.Handler):
|
||||
|
||||
class NetMap(object):
|
||||
def __init__(
|
||||
self, ips: list[str], cidrs: list[str], keep_lo=False, strict_cidr=False
|
||||
self,
|
||||
ips: list[str],
|
||||
cidrs: list[str],
|
||||
keep_lo=False,
|
||||
strict_cidr=False,
|
||||
defer_mutex=False,
|
||||
) -> None:
|
||||
"""
|
||||
ips: list of plain ipv4/ipv6 IPs, not cidr
|
||||
cidrs: list of cidr-notation IPs (ip/prefix)
|
||||
"""
|
||||
self.mutex = threading.Lock()
|
||||
|
||||
# fails multiprocessing; defer assignment
|
||||
self.mutex: Optional[threading.Lock] = None if defer_mutex else threading.Lock()
|
||||
|
||||
if "::" in ips:
|
||||
ips = [x for x in ips if x != "::"] + list(
|
||||
@@ -714,6 +742,9 @@ class NetMap(object):
|
||||
try:
|
||||
return self.cache[ip]
|
||||
except:
|
||||
# intentionally crash the calling thread if unset:
|
||||
assert self.mutex # type: ignore # !rm
|
||||
|
||||
with self.mutex:
|
||||
return self._map(ip)
|
||||
|
||||
@@ -1020,6 +1051,7 @@ class MTHash(object):
|
||||
self.sz = 0
|
||||
self.csz = 0
|
||||
self.stop = False
|
||||
self.readsz = 1024 * 1024 * (2 if (RAM_AVAIL or 2) < 1 else 12)
|
||||
self.omutex = threading.Lock()
|
||||
self.imutex = threading.Lock()
|
||||
self.work_q: Queue[int] = Queue()
|
||||
@@ -1095,7 +1127,7 @@ class MTHash(object):
|
||||
while chunk_rem > 0:
|
||||
with self.imutex:
|
||||
f.seek(ofs)
|
||||
buf = f.read(min(chunk_rem, 1024 * 1024 * 12))
|
||||
buf = f.read(min(chunk_rem, self.readsz))
|
||||
|
||||
if not buf:
|
||||
raise Exception("EOF at " + str(ofs))
|
||||
@@ -2194,6 +2226,23 @@ def unquotep(txt: str) -> str:
|
||||
return w8dec(unq2)
|
||||
|
||||
|
||||
def vroots(vp1: str, vp2: str) -> tuple[str, str]:
|
||||
"""
|
||||
input("q/w/e/r","a/s/d/e/r") output("/q/w/","/a/s/d/")
|
||||
"""
|
||||
while vp1 and vp2:
|
||||
zt1 = vp1.rsplit("/", 1) if "/" in vp1 else ("", vp1)
|
||||
zt2 = vp2.rsplit("/", 1) if "/" in vp2 else ("", vp2)
|
||||
if zt1[1] != zt2[1]:
|
||||
break
|
||||
vp1 = zt1[0]
|
||||
vp2 = zt2[0]
|
||||
return (
|
||||
"/%s/" % (vp1,) if vp1 else "/",
|
||||
"/%s/" % (vp2,) if vp2 else "/",
|
||||
)
|
||||
|
||||
|
||||
def vsplit(vpath: str) -> tuple[str, str]:
|
||||
if "/" not in vpath:
|
||||
return "", vpath
|
||||
@@ -2654,7 +2703,7 @@ def list_ips() -> list[str]:
|
||||
return list(ret)
|
||||
|
||||
|
||||
def build_netmap(csv: str):
|
||||
def build_netmap(csv: str, defer_mutex: bool = False):
|
||||
csv = csv.lower().strip()
|
||||
|
||||
if csv in ("any", "all", "no", ",", ""):
|
||||
@@ -2689,10 +2738,12 @@ def build_netmap(csv: str):
|
||||
cidrs.append(zs)
|
||||
|
||||
ips = [x.split("/")[0] for x in cidrs]
|
||||
return NetMap(ips, cidrs, True)
|
||||
return NetMap(ips, cidrs, True, False, defer_mutex)
|
||||
|
||||
|
||||
def load_ipu(log: "RootLogger", ipus: list[str]) -> tuple[dict[str, str], NetMap]:
|
||||
def load_ipu(
|
||||
log: "RootLogger", ipus: list[str], defer_mutex: bool = False
|
||||
) -> tuple[dict[str, str], NetMap]:
|
||||
ip_u = {"": "*"}
|
||||
cidr_u = {}
|
||||
for ipu in ipus:
|
||||
@@ -2709,7 +2760,7 @@ def load_ipu(log: "RootLogger", ipus: list[str]) -> tuple[dict[str, str], NetMap
|
||||
cidr_u[cidr] = uname
|
||||
ip_u[cip] = uname
|
||||
try:
|
||||
nm = NetMap(["::"], list(cidr_u.keys()), True, True)
|
||||
nm = NetMap(["::"], list(cidr_u.keys()), True, True, defer_mutex)
|
||||
except Exception as ex:
|
||||
t = "failed to translate --ipu into netmap, probably due to invalid config: %r"
|
||||
log("root", t % (ex,), 1)
|
||||
@@ -2762,7 +2813,10 @@ def sendfile_py(
|
||||
bufsz: int,
|
||||
slp: float,
|
||||
use_poll: bool,
|
||||
dls: dict[str, tuple[float, int]],
|
||||
dl_id: str,
|
||||
) -> int:
|
||||
sent = 0
|
||||
remains = upper - lower
|
||||
f.seek(lower)
|
||||
while remains > 0:
|
||||
@@ -2779,6 +2833,10 @@ def sendfile_py(
|
||||
except:
|
||||
return remains
|
||||
|
||||
if dl_id:
|
||||
sent += len(buf)
|
||||
dls[dl_id] = (time.time(), sent)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -2791,6 +2849,8 @@ def sendfile_kern(
|
||||
bufsz: int,
|
||||
slp: float,
|
||||
use_poll: bool,
|
||||
dls: dict[str, tuple[float, int]],
|
||||
dl_id: str,
|
||||
) -> int:
|
||||
out_fd = s.fileno()
|
||||
in_fd = f.fileno()
|
||||
@@ -2803,7 +2863,7 @@ def sendfile_kern(
|
||||
while ofs < upper:
|
||||
stuck = stuck or time.time()
|
||||
try:
|
||||
req = min(2 ** 30, upper - ofs)
|
||||
req = min(0x2000000, upper - ofs) # 32 MiB
|
||||
if use_poll:
|
||||
poll.poll(10000)
|
||||
else:
|
||||
@@ -2827,13 +2887,16 @@ def sendfile_kern(
|
||||
return upper - ofs
|
||||
|
||||
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))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
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]:
|
||||
if lstat and ANYWIN:
|
||||
lstat = False
|
||||
@@ -2869,6 +2932,12 @@ def statdir(
|
||||
logger(src, "[s] {} @ {}".format(repr(ex), fsdec(abspath)), 6)
|
||||
|
||||
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)
|
||||
if logger:
|
||||
logger(src, t, 1)
|
||||
@@ -2877,7 +2946,7 @@ def statdir(
|
||||
|
||||
|
||||
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 True
|
||||
|
||||
@@ -2890,7 +2959,7 @@ def rmdirs(
|
||||
top = os.path.dirname(top)
|
||||
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 = [os.path.join(top, x) for x in dirs]
|
||||
ok = []
|
||||
|
||||
@@ -1710,6 +1710,18 @@ html.dz .btn {
|
||||
background: var(--btn-1-bg);
|
||||
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 {
|
||||
color: var(--fg-max);
|
||||
}
|
||||
@@ -1931,11 +1943,10 @@ html.y #tree.nowrap .ntree a+a:hover {
|
||||
#rn_f.m td+td {
|
||||
width: 50%;
|
||||
}
|
||||
#rn_f .err td {
|
||||
background: var(--err-bg);
|
||||
color: var(--fg-max);
|
||||
}
|
||||
#rn_f .err input[readonly] {
|
||||
#rn_f .err td,
|
||||
#rn_f .err input[readonly],
|
||||
#rui .ng input[readonly] {
|
||||
color: var(--err-fg);
|
||||
background: var(--err-bg);
|
||||
}
|
||||
#rui input[readonly] {
|
||||
|
||||
@@ -37,8 +37,9 @@ var Ls = {
|
||||
["T", "toggle thumbnails / icons"],
|
||||
["🡅 A/D", "thumbnail size"],
|
||||
["ctrl-K", "delete selected"],
|
||||
["ctrl-X", "cut selected"],
|
||||
["ctrl-V", "paste into folder"],
|
||||
["ctrl-X", "cut selection to clipboard"],
|
||||
["ctrl-C", "copy selection to clipboard"],
|
||||
["ctrl-V", "paste (move/copy) here"],
|
||||
["Y", "download selected"],
|
||||
["F2", "rename selected"],
|
||||
|
||||
@@ -83,7 +84,7 @@ var Ls = {
|
||||
["I/K", "prev/next file"],
|
||||
["M", "close 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_del": "delete selected items$NHotkey: ctrl-K",
|
||||
"wt_cut": "cut selected items <small>(then paste somewhere else)</small>$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_selall": "select all files$NHotkey: ctrl-A (when file focused)",
|
||||
"wt_selinv": "invert selection",
|
||||
@@ -327,6 +329,7 @@ var Ls = {
|
||||
"fr_emore": "select at least one item to rename",
|
||||
"fd_emore": "select at least one item to delete",
|
||||
"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_ss": "share the selected files",
|
||||
@@ -379,16 +382,28 @@ var Ls = {
|
||||
"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)',
|
||||
|
||||
"fp_ecut": "first cut 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):",
|
||||
"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_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",
|
||||
"fcp_ok": "copy OK",
|
||||
"fp_busy": "moving {0} items...\n\n{1}",
|
||||
"fcp_busy": "copying {0} items...\n\n{1}",
|
||||
"fp_err": "move failed:\n",
|
||||
"fcp_err": "copy failed:\n",
|
||||
"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_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',
|
||||
"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>',
|
||||
"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",
|
||||
|
||||
@@ -400,7 +415,7 @@ var Ls = {
|
||||
"tvt_dl": "download this file$NHotkey: Y\">💾 download",
|
||||
"tvt_prev": "show previous document$NHotkey: i\">⬆ prev",
|
||||
"tvt_next": "show next document$NHotkey: K\">⬇ next",
|
||||
"tvt_sel": "select file ( for cut / delete / ... )$NHotkey: S\">sel",
|
||||
"tvt_sel": "select file ( for cut / copy / delete / ... )$NHotkey: S\">sel",
|
||||
"tvt_edit": "open file in text editor$NHotkey: E\">✏️ edit",
|
||||
|
||||
"gt_vau": "don't show videos, just play the audio\">🎧",
|
||||
@@ -605,8 +620,9 @@ var Ls = {
|
||||
["T", "miniatyrbilder på/av"],
|
||||
["🡅 A/D", "ikonstørrelse"],
|
||||
["ctrl-K", "slett valgte"],
|
||||
["ctrl-X", "klipp ut"],
|
||||
["ctrl-V", "lim inn"],
|
||||
["ctrl-X", "klipp ut valgte"],
|
||||
["ctrl-C", "kopiér til utklippstavle"],
|
||||
["ctrl-V", "lim inn (flytt/kopiér)"],
|
||||
["Y", "last ned valgte"],
|
||||
["F2", "endre navn på valgte"],
|
||||
|
||||
@@ -702,7 +718,8 @@ var Ls = {
|
||||
"wt_ren": "gi nye navn til de valgte filene$NSnarvei: F2",
|
||||
"wt_del": "slett de valgte filene$NSnarvei: ctrl-K",
|
||||
"wt_cut": "klipp ut de valgte filene <small>(for å lime inn et annet sted)</small>$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_selinv": "inverter utvalg",
|
||||
"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_follow": "bla slik at sangen som spilles alltid er synlig\">🎯",
|
||||
"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_mnext": "hopp til neste mappe og fortsett\">📂 neste",
|
||||
"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",
|
||||
"fd_emore": "velg minst én fil som skal slettes",
|
||||
"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_ss": "del de valgte filene",
|
||||
@@ -948,16 +966,28 @@ var Ls = {
|
||||
"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',
|
||||
|
||||
"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",
|
||||
"fp_ename": "disse {0} filene kan ikke flyttes til målmappen fordi det allerede finnes filer med samme navn:",
|
||||
"fcc_ok": "kopierte {0} filer til utklippstavlen",
|
||||
"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",
|
||||
"fcp_ok": "kopiering OK",
|
||||
"fp_busy": "flytter {0} filer...\n\n{1}",
|
||||
"fcp_busy": "kopierer {0} filer...\n\n{1}",
|
||||
"fp_err": "flytting feilet:\n",
|
||||
"fcp_err": "kopiering feilet:\n",
|
||||
"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_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',
|
||||
"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>',
|
||||
"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",
|
||||
|
||||
@@ -1176,6 +1206,7 @@ var Ls = {
|
||||
["🡅 A/D", "缩略图大小"],
|
||||
["ctrl-K", "删除选中项"],
|
||||
["ctrl-X", "剪切选中项"],
|
||||
["ctrl-C", "复制选中项"], //m
|
||||
["ctrl-V", "粘贴到文件夹"],
|
||||
["Y", "下载选中项"],
|
||||
["F2", "重命名选中项"],
|
||||
@@ -1271,6 +1302,7 @@ var Ls = {
|
||||
"wt_ren": "重命名选中的项目$N快捷键: F2",
|
||||
"wt_del": "删除选中的项目$N快捷键: ctrl-K",
|
||||
"wt_cut": "剪切选中的项目<small>(然后粘贴到其他地方)</small>$N快捷键: ctrl-X",
|
||||
"wt_cpy": "将选中的项目复制到剪贴板<small>(然后粘贴到其他地方)</small>$N快捷键: ctrl-C", //m
|
||||
"wt_pst": "粘贴之前剪切/复制的选择$N快捷键: ctrl-V",
|
||||
"wt_selall": "选择所有文件$N快捷键: ctrl-A(当文件被聚焦时)",
|
||||
"wt_selinv": "反转选择",
|
||||
@@ -1465,6 +1497,7 @@ var Ls = {
|
||||
"fr_emore": "选择至少一个项目以重命名",
|
||||
"fd_emore": "选择至少一个项目以删除",
|
||||
"fc_emore": "选择至少一个项目以剪切",
|
||||
"fcp_emore": "选择至少一个要复制到剪贴板的项目", //m
|
||||
|
||||
"fs_sc": "分享你所在的文件夹",
|
||||
"fs_ss": "分享选定的文件",
|
||||
@@ -1517,16 +1550,28 @@ var Ls = {
|
||||
"fc_ok": "剪切 {0} 项",
|
||||
"fc_warn": '剪切 {0} 项\n\n但:只有 <b>这个</b> 浏览器标签页可以粘贴它们\n(因为选择非常庞大)',
|
||||
|
||||
"fp_ecut": "首先剪切一些文件/文件夹以粘贴/移动\n\n注意:你可以在不同的浏览器标签页之间剪切/粘贴",
|
||||
"fp_ename": "这些 {0} 项不能移动到这里(名称已存在):",
|
||||
"fcc_ok": "已将 {0} 项复制到剪贴板", //m
|
||||
"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": "移动成功",
|
||||
"fcp_ok": "复制成功", //m
|
||||
"fp_busy": "正在移动 {0} 项...\n\n{1}",
|
||||
"fcp_busy": "正在复制 {0} 项...\n\n{1}", //m
|
||||
"fp_err": "移动失败:\n",
|
||||
"fcp_err": "复制失败:\n", //m
|
||||
"fp_confirm": "将这些 {0} 项移动到这里?",
|
||||
"fcp_confirm": "将这些 {0} 项复制到这里?", //m
|
||||
"fp_etab": '无法从其他浏览器标签页读取剪贴板',
|
||||
"fp_name": "从你的设备上传一个文件。给它一个名字:",
|
||||
"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>',
|
||||
"fcp_both_b": '<a href="#" id="modal-ok">复制</a><a href="#" id="modal-ng">上传</a>', //m
|
||||
|
||||
"mk_noname": "在左侧文本框中输入名称,然后再执行此操作 :p",
|
||||
|
||||
@@ -1771,6 +1816,7 @@ ebi('widget').innerHTML = (
|
||||
' 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="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>' +
|
||||
'</span><span id="wzip"><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
|
||||
var ACtx = !IPHONE && (window.AudioContext || window.webkitAudioContext),
|
||||
ACB = sread('au_cbv') || 1,
|
||||
noih = /[?&]v\b/.exec('' + location),
|
||||
hash0 = location.hash,
|
||||
sloc0 = '' + location,
|
||||
noih = /[?&]v\b/.exec(sloc0),
|
||||
ldks = [],
|
||||
dks = {},
|
||||
dk, mp;
|
||||
@@ -4376,6 +4423,7 @@ var fileman = (function () {
|
||||
var bren = ebi('fren'),
|
||||
bdel = ebi('fdel'),
|
||||
bcut = ebi('fcut'),
|
||||
bcpy = ebi('fcpy'),
|
||||
bpst = ebi('fpst'),
|
||||
bshr = ebi('fshr'),
|
||||
t_paste,
|
||||
@@ -4388,14 +4436,19 @@ var fileman = (function () {
|
||||
catch (ex) { }
|
||||
|
||||
r.render = function () {
|
||||
if (r.clip === null)
|
||||
if (r.clip === null) {
|
||||
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(),
|
||||
nsel = sel.length,
|
||||
enren = nsel,
|
||||
endel = nsel,
|
||||
encut = nsel,
|
||||
encpy = nsel,
|
||||
enpst = r.clip && r.clip.length,
|
||||
hren = !(have_mv && has(perms, 'write') && has(perms, 'move')),
|
||||
hdel = !(have_del && has(perms, 'delete')),
|
||||
@@ -4409,6 +4462,7 @@ var fileman = (function () {
|
||||
clmod(bren, 'en', enren);
|
||||
clmod(bdel, 'en', endel);
|
||||
clmod(bcut, 'en', encut);
|
||||
clmod(bcpy, 'en', encpy);
|
||||
clmod(bpst, 'en', enpst);
|
||||
clmod(bshr, 'en', 1);
|
||||
|
||||
@@ -4510,9 +4564,12 @@ var fileman = (function () {
|
||||
'<tr><td>perms</td><td class="sh_axs">',
|
||||
];
|
||||
for (var a = 0; a < perms.length; a++)
|
||||
if (perms[a] != 'admin')
|
||||
if (!has(['admin', 'move'], perms[a]))
|
||||
html.push('<a href="#" class="tgl btn">' + perms[a] + '</a>');
|
||||
|
||||
if (has(perms, 'write'))
|
||||
html.push('<a href="#" class="btn">write-only</a>');
|
||||
|
||||
html.push('</td></tr></div');
|
||||
shui.innerHTML = html.join('\n');
|
||||
|
||||
@@ -4576,6 +4633,9 @@ var fileman = (function () {
|
||||
|
||||
function shspf() {
|
||||
clmod(this, 'on', 't');
|
||||
if (this.textContent == 'write-only')
|
||||
for (var a = 0; a < pbtns.length; a++)
|
||||
clmod(pbtns[a], 'on', pbtns[a].textContent == 'write');
|
||||
}
|
||||
clmod(pbtns[0], 'on', 1);
|
||||
|
||||
@@ -4697,9 +4757,9 @@ var fileman = (function () {
|
||||
|
||||
var html = sel.length > 1 ? ['<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([
|
||||
@@ -4726,8 +4786,8 @@ var fileman = (function () {
|
||||
if (sel.length == 1)
|
||||
html.push(
|
||||
'<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>new:</td><td><input type="text" id="rn_new" n="0" /></td></tr>');
|
||||
'<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_0" /></td></tr>');
|
||||
else {
|
||||
html.push(
|
||||
'<div><table id="rn_f" class="m">' +
|
||||
@@ -4736,10 +4796,10 @@ var fileman = (function () {
|
||||
html.push(
|
||||
'<tr><td>' +
|
||||
(cheap ? '</td>' :
|
||||
'<button class="rn_dec" n="' + a + '">decode</button>' +
|
||||
'<button class="rn_reset" n="' + a + '">' + t_rst + '</button></td>') +
|
||||
'<td><input type="text" id="rn_new" n="' + a + '" /></td>' +
|
||||
'<td><input type="text" id="rn_old" n="' + a + '" readonly /></td></tr>');
|
||||
'<button class="rn_dec" id="rn_dec_' + a + '">decode</button>' +
|
||||
'<button class="rn_reset" id="rn_reset_' + a + '">' + t_rst + '</button></td>') +
|
||||
'<td><input type="text" id="rn_new_' + a + '" /></td>' +
|
||||
'<td><input type="text" id="rn_old_' + a + '" readonly /></td></tr>');
|
||||
}
|
||||
html.push('</table></div>');
|
||||
|
||||
@@ -4753,9 +4813,8 @@ var fileman = (function () {
|
||||
|
||||
rui.innerHTML = html.join('\n');
|
||||
for (var a = 0; a < f.length; a++) {
|
||||
var k = '[n="' + a + '"]';
|
||||
f[a].iold = QS('#rn_old' + k);
|
||||
f[a].inew = QS('#rn_new' + k);
|
||||
f[a].iold = ebi('rn_old_' + a);
|
||||
f[a].inew = ebi('rn_new_' + a);
|
||||
f[a].inew.value = f[a].iold.value = f[a].ofn;
|
||||
|
||||
if (!cheap)
|
||||
@@ -4766,11 +4825,11 @@ var fileman = (function () {
|
||||
if (kc.endsWith('Enter'))
|
||||
return rn_apply();
|
||||
};
|
||||
QS('.rn_dec' + k).onclick = function (e) {
|
||||
ebi('rn_dec_' + a).onclick = function (e) {
|
||||
ev(e);
|
||||
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);
|
||||
rn_reset(a);
|
||||
};
|
||||
@@ -4813,6 +4872,9 @@ var fileman = (function () {
|
||||
inew = ebi('rn_pnew'),
|
||||
defp = '$lpad((tn),2,0). [(artist) - ](title).(ext)';
|
||||
|
||||
ire.value = sread('cpp_rn_re') || '';
|
||||
ifmt.value = sread('cpp_rn_fmt') || '';
|
||||
|
||||
var presets = {};
|
||||
presets[defp] = ['', defp];
|
||||
presets = jread("rn_pre", presets);
|
||||
@@ -4903,6 +4965,8 @@ var fileman = (function () {
|
||||
|
||||
function rn_apply(e) {
|
||||
ev(e);
|
||||
swrite('cpp_rn_re', ire.value);
|
||||
swrite('cpp_rn_fmt', ifmt.value);
|
||||
if (r.win || r.slash) {
|
||||
var changed = 0;
|
||||
for (var a = 0; a < f.length; a++) {
|
||||
@@ -4952,7 +5016,6 @@ var fileman = (function () {
|
||||
};
|
||||
|
||||
r.delete = function (e) {
|
||||
ev(e);
|
||||
var sel = msel.getsel(),
|
||||
vps = [];
|
||||
|
||||
@@ -4962,6 +5025,8 @@ var fileman = (function () {
|
||||
if (!sel.length)
|
||||
return toast.err(3, L.fd_emore);
|
||||
|
||||
ev(e);
|
||||
|
||||
if (clgot(bdel, 'hide'))
|
||||
return toast.err(3, L.fd_eperm);
|
||||
|
||||
@@ -5001,13 +5066,15 @@ var fileman = (function () {
|
||||
};
|
||||
|
||||
r.cut = function (e) {
|
||||
ev(e);
|
||||
var sel = msel.getsel(),
|
||||
vps = [];
|
||||
stamp = Date.now(),
|
||||
vps = [stamp];
|
||||
|
||||
if (!sel.length)
|
||||
return toast.err(3, L.fc_emore);
|
||||
|
||||
ev(e);
|
||||
|
||||
if (clgot(bcut, 'hide'))
|
||||
return toast.err(3, L.fc_eperm);
|
||||
|
||||
@@ -5034,9 +5101,11 @@ var fileman = (function () {
|
||||
catch (ex) { }
|
||||
}, 1);
|
||||
|
||||
r.ccp = false;
|
||||
r.clip = vps.slice(1);
|
||||
|
||||
try {
|
||||
var stamp = Date.now();
|
||||
vps = JSON.stringify([stamp].concat(vps));
|
||||
vps = JSON.stringify(vps);
|
||||
if (vps.length > 1024 * 1024)
|
||||
throw 'a';
|
||||
|
||||
@@ -5050,6 +5119,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) {
|
||||
var xfer = e.clipboardData || window.clipboardData;
|
||||
if (!xfer || !xfer.files || !xfer.files.length)
|
||||
@@ -5065,9 +5188,9 @@ var fileman = (function () {
|
||||
return r.clip_up(files);
|
||||
|
||||
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) {
|
||||
@@ -5123,65 +5246,147 @@ var fileman = (function () {
|
||||
if (clgot(bpst, 'hide'))
|
||||
return toast.err(3, L.fp_eperm);
|
||||
|
||||
var req = [],
|
||||
exists = [],
|
||||
var html = [
|
||||
'<div>',
|
||||
'<button id="rn_cancel" tt="' + L.frt_abrt + '</button>',
|
||||
'<button id="rn_apply">✅ ' + L.fp_apply + '</button>',
|
||||
' 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 = [],
|
||||
srcdir = vsplit(r.clip[0])[0],
|
||||
links = QSA('#files tbody td:nth-child(2) 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++) {
|
||||
var found = false;
|
||||
for (var b = 0; b < indir.length; b++) {
|
||||
if (r.clip[a].endsWith('/' + indir[b])) {
|
||||
exists.push(r.clip[a]);
|
||||
found = true;
|
||||
var t = {
|
||||
'ok': true,
|
||||
'src': r.clip[a],
|
||||
'dst': uricom_dec(r.clip[a].split('/').pop()),
|
||||
};
|
||||
f.push(t);
|
||||
|
||||
for (var b = 0; b < indir.length; b++)
|
||||
if (t.dst == indir[b]) {
|
||||
t.ok = false;
|
||||
ui = true;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
req.push(r.clip[a]);
|
||||
|
||||
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>');
|
||||
}
|
||||
|
||||
if (exists.length)
|
||||
toast.warn(30, L.fp_ename.format(exists.length) + '<ul>' + uricom_adec(exists, true).join('') + '</ul>');
|
||||
|
||||
if (!req.length)
|
||||
return;
|
||||
|
||||
function paster() {
|
||||
var xhr = new XHR(),
|
||||
vp = req.shift();
|
||||
|
||||
if (!vp) {
|
||||
toast.ok(2, L.fp_ok);
|
||||
var t = f.shift();
|
||||
if (!t) {
|
||||
toast.ok(2, r.ccp ? L.fcp_ok : L.fp_ok);
|
||||
treectl.goto();
|
||||
r.tx(srcdir);
|
||||
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(f.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.send();
|
||||
}
|
||||
function paste_cb() {
|
||||
if (this.status !== 201) {
|
||||
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;
|
||||
}
|
||||
paster();
|
||||
}
|
||||
|
||||
modal.confirm(L.fp_confirm.format(req.length) + '<ul>' + uricom_adec(req, true).join('') + '</ul>', function () {
|
||||
function okgo() {
|
||||
paster();
|
||||
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) {
|
||||
r.clip = null;
|
||||
@@ -5219,6 +5424,7 @@ var fileman = (function () {
|
||||
bren.onclick = r.rename;
|
||||
bdel.onclick = r.delete;
|
||||
bcut.onclick = r.cut;
|
||||
bcpy.onclick = r.cpy;
|
||||
bpst.onclick = r.paste;
|
||||
bshr.onclick = r.share;
|
||||
|
||||
@@ -5328,7 +5534,7 @@ var showfile = (function () {
|
||||
|
||||
if (ah.textContent.endsWith('/'))
|
||||
continue;
|
||||
|
||||
|
||||
if (lang == 'ts' || (lang == 'md' && td.textContent != '-'))
|
||||
continue;
|
||||
|
||||
@@ -6041,6 +6247,19 @@ var thegrid = (function () {
|
||||
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, 'ihop', 'ihop', true);
|
||||
bcfg_bind(r, 'vau', 'gridvau', false);
|
||||
@@ -6120,8 +6339,6 @@ function tree_neigh(n) {
|
||||
links[act].click();
|
||||
else
|
||||
treectl.treego.call(links[act]);
|
||||
|
||||
links[act].focus();
|
||||
}
|
||||
|
||||
|
||||
@@ -6219,9 +6436,6 @@ var ahotkeys = function (e) {
|
||||
ae = document.activeElement,
|
||||
aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : '';
|
||||
|
||||
if (e.key == '?')
|
||||
return hkhelp();
|
||||
|
||||
if (k == 'Escape' || k == 'Esc') {
|
||||
ae && ae.blur();
|
||||
tt.hide();
|
||||
@@ -6302,15 +6516,24 @@ var ahotkeys = function (e) {
|
||||
if (aet && aet != 'a' && aet != 'tr' && aet != 'td' && aet != 'div' && aet != 'pre')
|
||||
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')
|
||||
return fileman.cut();
|
||||
return fileman.cut(e);
|
||||
|
||||
if ((k == 'KeyC' || k == 'c') && !sel)
|
||||
return fileman.cpy(e);
|
||||
|
||||
if (k == 'KeyV' || k == 'v')
|
||||
return fileman.d_paste();
|
||||
return fileman.d_paste(e);
|
||||
|
||||
if (k == 'KeyK' || k == 'k')
|
||||
return fileman.delete();
|
||||
return fileman.delete(e);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -7226,6 +7449,7 @@ var treectl = (function () {
|
||||
r.reqls(href, true);
|
||||
r.dir_cb = tree_scrollto;
|
||||
thegrid.setvis(true);
|
||||
clmod(this, 'ld', 1);
|
||||
}
|
||||
|
||||
r.reqls = function (url, hpush, back, hydrate) {
|
||||
@@ -7380,6 +7604,9 @@ var treectl = (function () {
|
||||
r.ls_cb = null;
|
||||
fun();
|
||||
}
|
||||
|
||||
if (window.have_shr && QS('#op_unpost.act') && (cdir.startsWith(SR + have_shr) || get_evpath().startsWith(SR + have_shr)))
|
||||
goto('unpost');
|
||||
}
|
||||
|
||||
r.chk_index_html = function (top, res) {
|
||||
@@ -9115,7 +9342,7 @@ var unpost = (function () {
|
||||
r.me = me;
|
||||
}
|
||||
|
||||
var q = SR + '/?ups';
|
||||
var q = get_evpath() + '?ups';
|
||||
if (filt.value)
|
||||
q += '&filter=' + uricom_enc(filt.value, true);
|
||||
|
||||
|
||||
BIN
copyparty/web/iiam.gif
Normal file
BIN
copyparty/web/iiam.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 230 B |
@@ -71,7 +71,7 @@ function showqr(href) {
|
||||
tr[a].cells[11].innerHTML =
|
||||
'<button value="1">1min</button> ' +
|
||||
'<button value="60">1h</button>';
|
||||
|
||||
|
||||
var btns = QSA('td button'), aa = btns.length;
|
||||
for (var a = 0; a < aa; a++)
|
||||
btns[a].onclick = bump;
|
||||
|
||||
@@ -90,6 +90,10 @@ table {
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.vols td:empty,
|
||||
.vols th:empty {
|
||||
padding: 0;
|
||||
}
|
||||
.num {
|
||||
border-right: 1px solid #bbb;
|
||||
}
|
||||
@@ -222,3 +226,6 @@ html.bz {
|
||||
color: #bbd;
|
||||
background: #11121d;
|
||||
}
|
||||
html.bz .vols img {
|
||||
filter: sepia(0.8) hue-rotate(180deg);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,18 @@
|
||||
</table>
|
||||
{%- 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 %}
|
||||
<h1>admin panel:</h1>
|
||||
<table><tr><td> <!-- hehehe -->
|
||||
@@ -129,11 +141,20 @@
|
||||
|
||||
{% if k304 or k304vis %}
|
||||
{% if k304 %}
|
||||
<li><a id="h" href="{{ r }}/?k304=n">disable k304</a> (currently enabled)
|
||||
<li><a id="h" href="{{ r }}/?cc&setck=k304=n">disable k304</a> (currently enabled)
|
||||
{%- else %}
|
||||
<li><a id="i" href="{{ r }}/?k304=y" class="r">enable k304</a> (currently disabled)
|
||||
<li><a id="i" href="{{ r }}/?cc&setck=k304=y" class="r">enable k304</a> (currently disabled)
|
||||
{% endif %}
|
||||
<blockquote id="j">enabling this will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general</blockquote></li>
|
||||
<blockquote id="j">enabling k304 will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general</blockquote></li>
|
||||
{% endif %}
|
||||
|
||||
{% if no304 or no304vis %}
|
||||
{% if no304 %}
|
||||
<li><a id="ab" href="{{ r }}/?cc&setck=no304=n">disable no304</a> (currently enabled)
|
||||
{%- else %}
|
||||
<li><a id="ac" href="{{ r }}/?cc&setck=no304=y" class="r">enable no304</a> (currently disabled)
|
||||
{% endif %}
|
||||
<blockquote id="ad">enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!</blockquote></li>
|
||||
{% endif %}
|
||||
|
||||
<li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
|
||||
|
||||
@@ -34,6 +34,10 @@ var Ls = {
|
||||
"ta2": "gjenta for å bekrefte nytt passord:",
|
||||
"ta3": "fant en skrivefeil; vennligst prøv igjen",
|
||||
"aa1": "innkommende:",
|
||||
"ab1": "skru av no304",
|
||||
"ac1": "skru på no304",
|
||||
"ad1": "no304 stopper all bruk av cache. Hvis ikke k304 var nok, prøv denne. Vil mangedoble dataforbruk!",
|
||||
"ae1": "utgående:",
|
||||
},
|
||||
"eng": {
|
||||
"d2": "shows the state of all active threads",
|
||||
@@ -80,6 +84,10 @@ var Ls = {
|
||||
"ta2": "重复以确认新密码:",
|
||||
"ta3": "发现拼写错误;请重试",
|
||||
"aa1": "正在接收的文件:", //m
|
||||
"ab1": "关闭 k304",
|
||||
"ac1": "开启 k304",
|
||||
"ad1": "启用 no304 将禁用所有缓存;如果 k304 不够,可以尝试此选项。这将消耗大量的网络流量!", //m
|
||||
"ae1": "正在下载:", //m
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ html {
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
left: 1px;
|
||||
animation: toastt var(--tmtime) steps(var(--tmstep)) forwards;
|
||||
animation: toastt var(--tmtime) 0.07s steps(var(--tmstep)) forwards;
|
||||
transform-origin: right;
|
||||
}
|
||||
@keyframes toastt {
|
||||
|
||||
@@ -1542,8 +1542,8 @@ var toast = (function () {
|
||||
|
||||
var html = '';
|
||||
if (sec) {
|
||||
setcvar('--tmtime', sec + 's');
|
||||
setcvar('--tmstep', sec * 15);
|
||||
setcvar('--tmtime', (sec - 0.15) + 's');
|
||||
setcvar('--tmstep', Math.floor(sec * 20));
|
||||
html += '<div id="toastt"></div>';
|
||||
}
|
||||
obj.innerHTML = html + '<a href="#" id="toastc">x</a><div id="toastb">' + lf2br(txt) + '</div>';
|
||||
|
||||
@@ -1,3 +1,63 @@
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 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
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #109 [rss feed generator](https://github.com/9001/copyparty#rss-feeds) 7ffd805a
|
||||
* monitor folders recursively with RSS readers
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* #107 `--df` diskspace limits was incompatible with webdav 2a570bb4
|
||||
* #108 up2k javascript crash (only affected the Chinese translation) a7e2a0c9
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* up2k: detect buggy webworkers 5ca8f070
|
||||
* up2k: improve upload retry/timeout logic a9b4436c
|
||||
* js: make handshake retries more aggressive
|
||||
* u2c: reduce chunks timeout + ^
|
||||
* main: reduce tcp timeout to 128sec (js is 42s)
|
||||
* httpcli: less confusing log messages
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-1016-2153 `v1.15.8` the sky is the limit
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
|
||||
| GET | `?tar&j` | pregenerate jpg thumbnails |
|
||||
| GET | `?tar&p` | pregenerate audio waveforms |
|
||||
| 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&filter=f` | ...where URL contains `f` |
|
||||
| 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 |
|
||||
|--|--|--|
|
||||
| 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 |
|
||||
|
||||
| method | params | body | result |
|
||||
@@ -208,6 +210,12 @@ upload modifiers:
|
||||
| method | params | result |
|
||||
|--|--|--|
|
||||
| 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
|
||||
|
||||
@@ -58,7 +58,9 @@ currently up to date with [awesome-selfhosted](https://github.com/awesome-selfho
|
||||
* [h5ai](#h5ai)
|
||||
* [autoindex](#autoindex)
|
||||
* [miniserve](#miniserve)
|
||||
* [pingvin-share](#pingvin-share)
|
||||
* [briefly considered](#briefly-considered)
|
||||
* [notes](#notes)
|
||||
|
||||
|
||||
# recommendations
|
||||
@@ -106,6 +108,7 @@ some softwares not in the matrixes,
|
||||
* [h5ai](#h5ai)
|
||||
* [autoindex](#autoindex)
|
||||
* [miniserve](#miniserve)
|
||||
* [pingvin-share](#pingvin-share)
|
||||
|
||||
symbol legend,
|
||||
* `█` = absolutely
|
||||
@@ -426,6 +429,10 @@ symbol legend,
|
||||
| gimme-that | python | █ mit | 4.8 MB |
|
||||
| ass | ts | █ isc | • |
|
||||
| linx | go | ░ gpl3 | 20 MB |
|
||||
| h5ai | php | █ mit | • |
|
||||
| autoindex | go | █ mpl2 | 11 MB |
|
||||
| miniserve | rust | █ mit | 2 MB |
|
||||
| pingvin-share | go | █ bsd2 | 487 MB |
|
||||
|
||||
* `size` = binary (if available) or installed size of program and its dependencies
|
||||
* copyparty size is for the [standalone python](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) file; the [windows exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) is **6 MiB**
|
||||
@@ -458,11 +465,13 @@ symbol legend,
|
||||
## [hfs3](https://rejetto.com/hfs/)
|
||||
* nodejs; cross-platform
|
||||
* vfs with gui config, per-volume permissions
|
||||
* tested locally, v0.53.2 on archlinux
|
||||
* 🔵 uploads are resumable
|
||||
* ⚠️ 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 integrity-checked
|
||||
* ⚠️ 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
|
||||
* ✅ config GUI
|
||||
* ✅ download counter
|
||||
@@ -471,11 +480,12 @@ symbol legend,
|
||||
|
||||
## [nextcloud](https://github.com/nextcloud/server)
|
||||
* 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
|
||||
* not that bad, can probably be remedied with bindmounts or maybe symlinks
|
||||
* ⚠️ uploads not resumable / accelerated / integrity-checked
|
||||
* ⚠️ 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
|
||||
* ⚠️ http/webdav only; no ftp, zeroconf
|
||||
* ⚠️ less awesome music player
|
||||
@@ -491,11 +501,12 @@ symbol legend,
|
||||
|
||||
## [seafile](https://github.com/haiwen/seafile)
|
||||
* 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
|
||||
* *much worse than nextcloud* in that regard
|
||||
* ⚠️ uploads not resumable / accelerated / integrity-checked
|
||||
* ⚠️ 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
|
||||
* ⚠️ big folders cannot be zip-downloaded
|
||||
* ⚠️ http/webdav only; no ftp, zeroconf
|
||||
@@ -519,9 +530,11 @@ symbol legend,
|
||||
|
||||
## [dufs](https://github.com/sigoden/dufs)
|
||||
* rust; cross-platform (windows, linux, macos)
|
||||
* tested locally, v0.43.0 on archlinux (plain binary)
|
||||
* ⚠️ uploads not resumable / accelerated / integrity-checked
|
||||
* ⚠️ on cloudflare: max upload size 100 MiB
|
||||
* ⚠️ 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
|
||||
* ✅ per-url access control (copyparty is per-volume)
|
||||
* 🔵 basic but really snappy ui
|
||||
@@ -564,10 +577,12 @@ symbol legend,
|
||||
|
||||
## [filebrowser](https://github.com/filebrowser/filebrowser)
|
||||
* go; cross-platform (windows, linux, mac)
|
||||
* tested locally, v2.31.2 on archlinux (plain binary)
|
||||
* 🔵 uploads are resumable and segmented
|
||||
* 🔵 multiple files are uploaded in parallel, but...
|
||||
* ⚠️ big files are not accelerated (copyparty is 5x faster across the atlantic)
|
||||
* ⚠️ 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
|
||||
* ⚠️ doesn't support crazy filenames
|
||||
* ⚠️ no directory tree nav
|
||||
@@ -605,6 +620,7 @@ symbol legend,
|
||||
* ⚠️ no zeroconf (mdns/ssdp)
|
||||
* ⚠️ impractical directory URLs
|
||||
* ⚠️ AGPL licensed
|
||||
* 🔵 uploading small files is fast; `340` files per sec (copyparty does `670`/sec)
|
||||
* 🔵 ftp, ftps, webdav
|
||||
* ✅ sftp server
|
||||
* ✅ settings gui
|
||||
@@ -719,7 +735,31 @@ symbol legend,
|
||||
* 🔵 upload, tar/zip download, qr-code
|
||||
* ✅ faster at loading huge folders
|
||||
|
||||
## [pingvin-share](https://github.com/stonith404/pingvin-share)
|
||||
* node; linux (docker)
|
||||
* mainly for uploads, not a general file server
|
||||
* 🔵 uploads are segmented (avoids cloudflare size limit)
|
||||
* 🔵 segments are written directly to target file (HDD-friendly)
|
||||
* ⚠️ uploads not resumable after a browser or laptop crash
|
||||
* ⚠️ uploads are not accelerated / integrity-checked
|
||||
* ⚠️ across the atlantic, copyparty is 3x faster
|
||||
* measured with chunksize 96 MiB; pingvin's default 10 MiB is much slower
|
||||
* ⚠️ can't upload folders with subfolders
|
||||
* ⚠️ no upload ETA
|
||||
* 🔵 expiration times, shares, upload-undo
|
||||
* ✅ config + user-registration gui
|
||||
* ✅ built-in OpenID and LDAP support
|
||||
* 💾 [IdP middleware](https://github.com/9001/copyparty#identity-providers) and config-files
|
||||
* ✅ probably more than one person who understands the code
|
||||
|
||||
|
||||
|
||||
# briefly considered
|
||||
* [pydio](https://github.com/pydio/cells): python/agpl3, looks great, fantastic ux -- but needs mariadb, systemwide install
|
||||
* [gossa](https://github.com/pldubouilh/gossa): go/mit, minimalistic, basic file upload, text editor, mkdir and rename (no delete/move)
|
||||
|
||||
|
||||
|
||||
# notes
|
||||
|
||||
* high-latency connections (cross-atlantic uploads) can be accurately simulated with `tc qdisc add dev eth0 root netem delay 100ms`
|
||||
|
||||
@@ -25,6 +25,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: Jython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
|
||||
@@ -94,6 +94,7 @@ copyparty/web/deps/prismd.css,
|
||||
copyparty/web/deps/scp.woff2,
|
||||
copyparty/web/deps/sha512.ac.js,
|
||||
copyparty/web/deps/sha512.hw.js,
|
||||
copyparty/web/iiam.gif,
|
||||
copyparty/web/md.css,
|
||||
copyparty/web/md.html,
|
||||
copyparty/web/md.js,
|
||||
|
||||
@@ -54,7 +54,7 @@ var tl_cpanel = {
|
||||
"cc1": "other stuff:",
|
||||
"h1": "disable k304", // TLNote: "j1" explains what k304 is
|
||||
"i1": "enable k304",
|
||||
"j1": "enabling this will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general",
|
||||
"j1": "enabling k304 will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general",
|
||||
"k1": "reset client settings",
|
||||
"l1": "login for more:",
|
||||
"m1": "welcome back,", // TLNote: "welcome back, USERNAME"
|
||||
@@ -76,6 +76,10 @@ var tl_cpanel = {
|
||||
"ta2": "repeat to confirm new password:",
|
||||
"ta3": "found a typo; please try again",
|
||||
"aa1": "incoming files:",
|
||||
"ab1": "disable 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!",
|
||||
"ae1": "active downloads:",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -118,8 +122,9 @@ var tl_browser = {
|
||||
["T", "toggle thumbnails / icons"],
|
||||
["🡅 A/D", "thumbnail size"],
|
||||
["ctrl-K", "delete selected"],
|
||||
["ctrl-X", "cut selected"],
|
||||
["ctrl-V", "paste into folder"],
|
||||
["ctrl-X", "cut selection to clipboard"],
|
||||
["ctrl-C", "copy selection to clipboard"],
|
||||
["ctrl-V", "paste (move/copy) here"],
|
||||
["Y", "download selected"],
|
||||
["F2", "rename selected"],
|
||||
|
||||
@@ -164,7 +169,7 @@ var tl_browser = {
|
||||
["I/K", "prev/next file"],
|
||||
["M", "close textfile"],
|
||||
["E", "edit textfile"],
|
||||
["S", "select file (for cut/rename)"],
|
||||
["S", "select file (for cut/copy/rename)"],
|
||||
]
|
||||
],
|
||||
|
||||
@@ -214,6 +219,7 @@ var tl_browser = {
|
||||
"wt_ren": "rename selected items$NHotkey: F2",
|
||||
"wt_del": "delete selected items$NHotkey: ctrl-K",
|
||||
"wt_cut": "cut selected items <small>(then paste somewhere else)</small>$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_selall": "select all files$NHotkey: ctrl-A (when file focused)",
|
||||
"wt_selinv": "invert selection",
|
||||
@@ -408,6 +414,7 @@ var tl_browser = {
|
||||
"fr_emore": "select at least one item to rename",
|
||||
"fd_emore": "select at least one item to delete",
|
||||
"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_ss": "share the selected files",
|
||||
@@ -460,16 +467,26 @@ var tl_browser = {
|
||||
"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)',
|
||||
|
||||
"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):",
|
||||
"fcp_ename": "these {0} items cannot be copied here (names already exist):",
|
||||
"fp_ok": "move OK",
|
||||
"fcp_ok": "copy OK",
|
||||
"fp_busy": "moving {0} items...\n\n{1}",
|
||||
"fcp_busy": "copying {0} items...\n\n{1}",
|
||||
"fp_err": "move failed:\n",
|
||||
"fcp_err": "copy failed:\n",
|
||||
"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_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',
|
||||
"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>',
|
||||
"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",
|
||||
|
||||
@@ -481,7 +498,7 @@ var tl_browser = {
|
||||
"tvt_dl": "download this file$NHotkey: Y\">💾 download",
|
||||
"tvt_prev": "show previous document$NHotkey: i\">⬆ prev",
|
||||
"tvt_next": "show next document$NHotkey: K\">⬇ next",
|
||||
"tvt_sel": "select file ( for cut / delete / ... )$NHotkey: S\">sel",
|
||||
"tvt_sel": "select file ( for cut / copy / delete / ... )$NHotkey: S\">sel",
|
||||
"tvt_edit": "open file in text editor$NHotkey: E\">✏️ edit",
|
||||
|
||||
"gt_vau": "don't show videos, just play the audio\">🎧",
|
||||
|
||||
@@ -89,7 +89,7 @@ var tl_cpanel = {{
|
||||
"cc1": "other stuff:",
|
||||
"h1": "disable k304", // TLNote: "j1" explains what k304 is
|
||||
"i1": "enable k304",
|
||||
"j1": "enabling this will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general",
|
||||
"j1": "enabling k304 will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general",
|
||||
"k1": "reset client settings",
|
||||
"l1": "login for more:",
|
||||
"m1": "welcome back,", // TLNote: "welcome back, USERNAME"
|
||||
@@ -111,6 +111,9 @@ var tl_cpanel = {{
|
||||
"ta2": "repeat to confirm new password:",
|
||||
"ta3": "found a typo; please try again",
|
||||
"aa1": "incoming files:",
|
||||
"ab1": "disable 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!",
|
||||
}},
|
||||
}};
|
||||
|
||||
|
||||
1
setup.py
1
setup.py
@@ -108,6 +108,7 @@ args = {
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: Jython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
|
||||
109
tests/test_cp.py
Normal file
109
tests/test_cp.py
Normal 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)
|
||||
@@ -57,6 +57,7 @@ class TestMetrics(unittest.TestCase):
|
||||
ptns = r"""
|
||||
cpp_uptime_seconds [0-9]\.[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_total -1$
|
||||
cpp_http_conns 9$
|
||||
|
||||
@@ -122,7 +122,7 @@ class Cfg(Namespace):
|
||||
def __init__(self, a=None, v=None, c=None, **ka0):
|
||||
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 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()})
|
||||
|
||||
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,10 +134,10 @@ class Cfg(Namespace):
|
||||
ex = "hash_mt safe_dedup srch_time u2abort u2j u2sz"
|
||||
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()})
|
||||
|
||||
ex = "db_act k304 loris 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"
|
||||
ka.update(**{k: 0 for k in ex.split()})
|
||||
|
||||
ex = "ah_alg bname chpw_db doctitle df exit favico idp_h_usr ipa html_head lg_sbf log_fk md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles unlist vname xff_src R RS SR"
|
||||
@@ -146,7 +146,7 @@ class Cfg(Namespace):
|
||||
ex = "ban_403 ban_404 ban_422 ban_pw ban_url"
|
||||
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()})
|
||||
|
||||
ex = "exp_lg exp_md"
|
||||
@@ -254,6 +254,8 @@ class VHttpSrv(object):
|
||||
self.broker = NullBroker(args, asrv)
|
||||
self.prism = None
|
||||
self.bans = {}
|
||||
self.tdls = self.dls = {}
|
||||
self.tdli = self.dli = {}
|
||||
self.nreq = 0
|
||||
self.nsus = 0
|
||||
|
||||
@@ -292,6 +294,8 @@ class VHttpConn(object):
|
||||
self.args = args
|
||||
self.asrv = asrv
|
||||
self.bans = {}
|
||||
self.tdls = self.dls = {}
|
||||
self.tdli = self.dli = {}
|
||||
self.freshen_pwd = 0.0
|
||||
|
||||
Ctor = VHttpSrvUp2k if use_up2k else VHttpSrv
|
||||
|
||||
Reference in New Issue
Block a user