Compare commits

...

19 Commits

Author SHA1 Message Date
ed
dabdaaee33 v1.9.16 2023-11-04 21:58:01 +00:00
ed
65e4d67c3e mkdir with leading slash works as expected 2023-11-04 22:21:56 +00:00
ed
4b720f4150 add more prometheus metrics; breaking changes:
* cpp_uptime is now a gauge
* cpp_bans is now cpp_active_bans (and also a gauge)

and other related fixes:
* stop emitting invalid cpp_disk_size/free for offline volumes
* support overriding the spec-mandatory mimetype with ?mime=foo
2023-11-04 20:32:34 +00:00
ed
2e85a25614 improve service listing 2023-11-04 10:23:37 +00:00
ed
713fffcb8e also mkdir missing intermediates,
unless requester is a webdav client (those expect a 409)
2023-11-03 23:23:49 +00:00
ed
8020b11ea0 improve/simplify validation/errorhandling:
* some malicious requests are now answered with HTTP 422,
   so that they count against --ban-422
* do not include request headers when replying to invalid requests,
   in case there is a reverse-proxy inserting something interesting
2023-11-03 23:07:16 +00:00
ed
2523d76756 windows: fix symlinks 2023-11-03 17:16:12 +00:00
ed
7ede509973 nginx: reduce cost of spurious connectivity loss;
default value of fail_timeout (10sec) makes server unavailable for that
amount of time, even if the server is just down for a quick restart
2023-11-03 17:13:11 +00:00
ed
7c1d97af3b slightly better pyinstaller loader 2023-11-03 17:09:34 +00:00
ed
95566e8388 cosmetics:
* fix toast/tooltip colors on splashpage
* properly warn if --ah-cli or --ah-gen is used without --ah-alg
* support ^D during --ah-cli
* improve flavor texts
2023-11-03 16:52:43 +00:00
ed
76afb62b7b make each segment of links separately selectable 2023-10-25 12:21:39 +00:00
ed
7dec922c70 update pkgs to 1.9.15 2023-10-24 16:56:57 +00:00
ed
c07e0110f8 v1.9.15 2023-10-24 16:43:26 +00:00
ed
2808734047 drc: further reduce volume skip between songs 2023-10-24 16:38:29 +00:00
ed
1f75314463 placeholder expansion in readme and logues; closes #56
also fixes the "scan" volflag which broke in v1.9.14
2023-10-24 16:37:32 +00:00
ed
063fa3efde drc: fix volume jump on song change
(in exchange for a chance of clipping, which should be fine because
all browsers appear to have a limiter on the output anyways)
2023-10-23 09:05:31 +00:00
ed
44693d79ec update pkgs to 1.9.14 2023-10-21 14:52:22 +00:00
ed
cea746377e v1.9.14 2023-10-21 14:43:11 +00:00
ed
59a98bd2b5 update pkgs to 1.9.13 2023-10-21 13:34:50 +00:00
26 changed files with 577 additions and 177 deletions

View File

@@ -53,6 +53,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [webdav server](#webdav-server) - with read-write support * [webdav server](#webdav-server) - with read-write support
* [connecting to webdav from windows](#connecting-to-webdav-from-windows) - using the GUI * [connecting to webdav from windows](#connecting-to-webdav-from-windows) - using the GUI
* [smb server](#smb-server) - unsafe, slow, not recommended for wan * [smb server](#smb-server) - unsafe, slow, not recommended for wan
* [browser ux](#browser-ux) - tweaking the ui
* [file indexing](#file-indexing) - enables dedup and music search ++ * [file indexing](#file-indexing) - enables dedup and music search ++
* [exclude-patterns](#exclude-patterns) - to save some time * [exclude-patterns](#exclude-patterns) - to save some time
* [filesystem guards](#filesystem-guards) - avoid traversing into other filesystems * [filesystem guards](#filesystem-guards) - avoid traversing into other filesystems
@@ -317,6 +318,8 @@ same order here too
upgrade notes upgrade notes
* `1.9.16` (2023-11-04):
* `--stats`/prometheus: `cpp_bans` renamed to `cpp_active_bans`, and that + `cpp_uptime` are gauges
* `1.6.0` (2023-01-29): * `1.6.0` (2023-01-29):
* http-api: delete/move is now `POST` instead of `GET` * http-api: delete/move is now `POST` instead of `GET`
* everything other than `GET` and `HEAD` must pass [cors validation](#cors) * everything other than `GET` and `HEAD` must pass [cors validation](#cors)
@@ -789,6 +792,8 @@ other notes,
* files named `README.md` / `readme.md` will be rendered after directory listings unless `--no-readme` (but `.epilogue.html` takes precedence) * files named `README.md` / `readme.md` will be rendered after directory listings unless `--no-readme` (but `.epilogue.html` takes precedence)
* `README.md` and `*logue.html` can contain placeholder values which are replaced server-side before embedding into directory listings; see `--help-exp`
## searching ## searching
@@ -954,6 +959,16 @@ authenticate with one of the following:
* username `$password`, password `k` * username `$password`, password `k`
## browser ux
tweaking the ui
* set default sort order globally with `--sort` or per-volume with the `sort` volflag; specify one or more comma-separated columns to sort by, and prefix the column name with `-` for reverse sort
* the column names you can use are visible as tooltips when hovering over the column headers in the directory listing, for example `href ext sz ts tags/.up_at tags/Cirle tags/.tn tags/Artist tags/Title`
* to sort in music order (album, track, artist, title) with filename as fallback, you could `--sort tags/Cirle,tags/.tn,tags/Artist,tags/Title,href`
* to sort by upload date, first enable showing the upload date in the listing with `-e2d -mte +.up_at` and then `--sort tags/.up_at`
## file indexing ## file indexing
enables dedup and music search ++ enables dedup and music search ++
@@ -1292,8 +1307,23 @@ scrape_configs:
``` ```
currently the following metrics are available, currently the following metrics are available,
* `cpp_uptime_seconds` * `cpp_uptime_seconds` time since last copyparty restart
* `cpp_bans` number of banned IPs * `cpp_boot_unixtime_seconds` same but as an absolute timestamp
* `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
* `cpp_active_bans` number of currently banned IPs
* `cpp_total_bans` number of IPs banned since last restart
these are available unless `--nos-vst` is specified:
* `cpp_db_idle_seconds` time since last database activity (upload/rename/delete)
* `cpp_db_act_seconds` same but as an absolute timestamp
* `cpp_idle_vols` number of volumes which are idle / ready
* `cpp_busy_vols` number of volumes which are busy / indexing
* `cpp_offline_vols` number of volumes which are offline / unavailable
* `cpp_hashing_files` number of files queued for hashing / indexing
* `cpp_tagq_files` number of files queued for metadata scanning
* `cpp_mtpq_files` number of files queued for plugin-based analysis
and these are available per-volume only: and these are available per-volume only:
* `cpp_disk_size_bytes` total HDD size * `cpp_disk_size_bytes` total HDD size
@@ -1312,9 +1342,12 @@ some of the metrics have additional requirements to function correctly,
the following options are available to disable some of the metrics: the following options are available to disable some of the metrics:
* `--nos-hdd` disables `cpp_disk_*` which can prevent spinning up HDDs * `--nos-hdd` disables `cpp_disk_*` which can prevent spinning up HDDs
* `--nos-vol` disables `cpp_vol_*` which reduces server startup time * `--nos-vol` disables `cpp_vol_*` which reduces server startup time
* `--nos-vst` disables volume state, reducing the worst-case prometheus query time by 0.5 sec
* `--nos-dup` disables `cpp_dupe_*` which reduces the server load caused by prometheus queries * `--nos-dup` disables `cpp_dupe_*` which reduces the server load caused by prometheus queries
* `--nos-unf` disables `cpp_unf_*` for no particular purpose * `--nos-unf` disables `cpp_unf_*` for no particular purpose
note: the following metrics are counted incorrectly if multiprocessing is enabled with `-j`: `cpp_http_conns`, `cpp_http_reqs`, `cpp_sus_reqs`, `cpp_active_bans`, `cpp_total_bans`
# packages # packages

View File

@@ -13,7 +13,7 @@
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1 # on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
upstream cpp { upstream cpp {
server 127.0.0.1:3923; server 127.0.0.1:3923 fail_timeout=1s;
keepalive 1; keepalive 1;
} }
server { server {

View File

@@ -1,8 +1,8 @@
# Maintainer: icxes <dev.null@need.moe> # Maintainer: icxes <dev.null@need.moe>
pkgname=copyparty pkgname=copyparty
pkgver="1.9.12" pkgver="1.9.15"
pkgrel=1 pkgrel=1
pkgdesc="Portable file sharing hub" pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, zeroconf, media indexer, thumbnails++"
arch=("any") arch=("any")
url="https://github.com/9001/${pkgname}" url="https://github.com/9001/${pkgname}"
license=('MIT') license=('MIT')
@@ -20,7 +20,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
) )
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}.d/init" ) backup=("etc/${pkgname}.d/init" )
sha256sums=("bf285725a70b3b201fa8927dd93b294dc9c8c29e00d6826accac8977fc72e1d4") sha256sums=("ee569d664b22cb59ac0eb11850380648d9f8d42d1c26283d43dab350745c102e")
build() { build() {
cd "${srcdir}/${pkgname}-${pkgver}" cd "${srcdir}/${pkgname}-${pkgver}"

View File

@@ -1,5 +1,5 @@
{ {
"url": "https://github.com/9001/copyparty/releases/download/v1.9.12/copyparty-sfx.py", "url": "https://github.com/9001/copyparty/releases/download/v1.9.15/copyparty-sfx.py",
"version": "1.9.12", "version": "1.9.15",
"hash": "sha256-/ih867kYtyYcwM+jf5ciHmgTg8BVC+Ve6U8BnamN0kw=" "hash": "sha256-EUenh567NYj1klMpjVOWKqiBSqZbdEA0ZGidzzzpnsY="
} }

View File

@@ -27,6 +27,7 @@ from .authsrv import expand_config_file, re_vol, split_cfg_ln, upgrade_cfg_fmt
from .cfg import flagcats, onedash from .cfg import flagcats, onedash
from .svchub import SvcHub from .svchub import SvcHub
from .util import ( from .util import (
DEF_EXP,
DEF_MTE, DEF_MTE,
DEF_MTH, DEF_MTH,
IMPLICATIONS, IMPLICATIONS,
@@ -646,6 +647,47 @@ def get_sects():
""" """
), ),
], ],
[
"exp",
"text expansion",
dedent(
"""
specify --exp or the "exp" volflag to enable placeholder expansions
in README.md / .prologue.html / .epilogue.html
--exp-md (volflag exp_md) holds the list of placeholders which can be
expanded in READMEs, and --exp-lg (volflag exp_lg) likewise for logues;
any placeholder not given in those lists will be ignored and shown as-is
the default list will expand the following placeholders:
\033[36m{{self.ip}} \033[35mclient ip
\033[36m{{self.ua}} \033[35mclient user-agent
\033[36m{{self.uname}} \033[35mclient username
\033[36m{{self.host}} \033[35mthe "Host" header, or the server's external IP otherwise
\033[36m{{cfg.name}} \033[35mthe --name global-config
\033[36m{{cfg.logout}} \033[35mthe --logout global-config
\033[36m{{vf.scan}} \033[35mthe "scan" volflag
\033[36m{{vf.thsize}} \033[35mthumbnail size
\033[36m{{srv.itime}} \033[35mserver time in seconds
\033[36m{{srv.htime}} \033[35mserver time as YY-mm-dd, HH:MM:SS (UTC)
\033[36m{{hdr.cf_ipcountry}} \033[35mthe "CF-IPCountry" client header (probably blank)
\033[0m
so the following types of placeholders can be added to the lists:
* any client header can be accessed through {{hdr.*}}
* any variable in httpcli.py can be accessed through {{self.*}}
* any global server setting can be accessed through {{cfg.*}}
* any volflag can be accessed through {{vf.*}}
remove vf.scan from default list using --exp-md /vf.scan
add "accept" header to def. list using --exp-md +hdr.accept
for performance reasons, expansion only happens while embedding
documents into directory listings, and when accessing a ?doc=...
link, but never otherwise, so if you click a -txt- link you'll
have to refresh the page to apply expansion
"""
),
],
[ [
"ls", "ls",
"volume inspection", "volume inspection",
@@ -776,8 +818,6 @@ def add_general(ap, nc, srvname):
ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="add account, \033[33mUSER\033[0m:\033[33mPASS\033[0m; example [\033[32med:wark\033[0m]") ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="add account, \033[33mUSER\033[0m:\033[33mPASS\033[0m; example [\033[32med:wark\033[0m]")
ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m]") ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m]")
ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files") ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files")
ap2.add_argument("-emp", action="store_true", help="enable markdown plugins -- neat but dangerous, big XSS risk")
ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate")
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,get", help="how to handle url-form POSTs; see --help-urlform") ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,get", help="how to handle url-form POSTs; see --help-urlform")
ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="window title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]") ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="window title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]")
ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)") ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)")
@@ -974,6 +1014,7 @@ def add_stats(ap):
ap2.add_argument("--stats", action="store_true", help="enable openmetrics at /.cpr/metrics for admin accounts") ap2.add_argument("--stats", action="store_true", help="enable openmetrics at /.cpr/metrics for admin accounts")
ap2.add_argument("--nos-hdd", action="store_true", help="disable disk-space metrics (used/free space)") ap2.add_argument("--nos-hdd", action="store_true", help="disable disk-space metrics (used/free space)")
ap2.add_argument("--nos-vol", action="store_true", help="disable volume size metrics (num files, total bytes, vmaxb/vmaxn)") ap2.add_argument("--nos-vol", action="store_true", help="disable volume size metrics (num files, total bytes, vmaxb/vmaxn)")
ap2.add_argument("--nos-vst", action="store_true", help="disable volume state metrics (indexing, analyzing, activity)")
ap2.add_argument("--nos-dup", action="store_true", help="disable dupe-files metrics (good idea; very slow)") ap2.add_argument("--nos-dup", action="store_true", help="disable dupe-files metrics (good idea; very slow)")
ap2.add_argument("--nos-unf", action="store_true", help="disable unfinished-uploads metrics") ap2.add_argument("--nos-unf", action="store_true", help="disable unfinished-uploads metrics")
@@ -1054,7 +1095,7 @@ def add_logging(ap):
ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR") ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup") ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup")
ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals") ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals")
ap2.add_argument("--log-badpwd", metavar="N", type=int, default=1, help="log passphrase of failed login attempts: 0=terse, 1=plaintext, 2=hashed") ap2.add_argument("--log-badpwd", metavar="N", type=int, default=1, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed")
ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs") 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("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="dump incoming header") ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="dump incoming header")
@@ -1144,6 +1185,15 @@ def add_db_metadata(ap):
ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="read tag M using program BIN to parse the file") ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="read tag M using program BIN to parse the file")
def add_txt(ap):
ap2 = ap.add_argument_group('textfile options')
ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="textfile editor checks for serverside changes every SEC seconds")
ap2.add_argument("-emp", action="store_true", help="enable markdown plugins -- neat but dangerous, big XSS risk")
ap2.add_argument("--exp", action="store_true", help="enable textfile expansion -- replace {{self.ip}} and such; see --help-exp (volflag=exp)")
ap2.add_argument("--exp-md", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in markdown files; add/remove stuff on the default list with +hdr_foo or /vf.scan (volflag=exp_md)")
ap2.add_argument("--exp-lg", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in prologue/epilogue files (volflag=exp_lg)")
def add_ui(ap, retry): def add_ui(ap, retry):
ap2 = ap.add_argument_group('ui options') ap2 = ap.add_argument_group('ui options')
ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)") ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
@@ -1237,6 +1287,7 @@ def run_argparse(
add_handlers(ap) add_handlers(ap)
add_hooks(ap) add_hooks(ap)
add_stats(ap) add_stats(ap)
add_txt(ap)
add_ui(ap, retry) add_ui(ap, retry)
add_admin(ap) add_admin(ap)
add_logging(ap) add_logging(ap)
@@ -1264,7 +1315,7 @@ def run_argparse(
for k, h, t in sects: for k, h, t in sects:
k2 = "help_" + k.replace("-", "_") k2 = "help_" + k.replace("-", "_")
if vars(ret)[k2]: if vars(ret)[k2]:
lprint("# {} help page".format(k)) lprint("# %s help page (%s)" % (k, h))
lprint(t + "\033[0m") lprint(t + "\033[0m")
sys.exit(0) sys.exit(0)

View File

@@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (1, 9, 13) VERSION = (1, 9, 16)
CODENAME = "prometheable" CODENAME = "prometheable"
BUILD_DT = (2023, 10, 21) BUILD_DT = (2023, 11, 4)
S_VERSION = ".".join(map(str, VERSION)) S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -476,12 +476,10 @@ class VFS(object):
err: int = 403, err: int = 403,
) -> tuple["VFS", str]: ) -> tuple["VFS", str]:
"""returns [vfsnode,fs_remainder] if user has the requested permissions""" """returns [vfsnode,fs_remainder] if user has the requested permissions"""
if ANYWIN: if relchk(vpath):
mod = relchk(vpath) if self.log:
if mod: self.log("vfs", "invalid relpath [{}]".format(vpath))
if self.log: raise Pebkac(422)
self.log("vfs", "invalid relpath [{}]".format(vpath))
raise Pebkac(404)
cvpath = undot(vpath) cvpath = undot(vpath)
vn, rem = self._find(cvpath) vn, rem = self._find(cvpath)
@@ -500,8 +498,8 @@ class VFS(object):
t = "{} has no {} in [{}] => [{}] => [{}]" t = "{} has no {} in [{}] => [{}] => [{}]"
self.log("vfs", t.format(uname, msg, vpath, cvpath, ap), 6) self.log("vfs", t.format(uname, msg, vpath, cvpath, ap), 6)
t = "you don't have {}-access for this location" t = 'you don\'t have %s-access in "/%s"'
raise Pebkac(err, t.format(msg)) raise Pebkac(err, t % (msg, cvpath))
return vn, rem return vn, rem
@@ -1479,15 +1477,11 @@ class AuthSrv(object):
raise Exception(t.format(dbd, dbds)) raise Exception(t.format(dbd, dbds))
# default tag cfgs if unset # default tag cfgs if unset
if "mte" not in vol.flags: for k in ("mte", "mth", "exp_md", "exp_lg"):
vol.flags["mte"] = self.args.mte.copy() if k not in vol.flags:
else: vol.flags[k] = getattr(self.args, k).copy()
vol.flags["mte"] = odfusion(self.args.mte, vol.flags["mte"]) else:
vol.flags[k] = odfusion(getattr(self.args, k), vol.flags[k])
if "mth" not in vol.flags:
vol.flags["mth"] = self.args.mth.copy()
else:
vol.flags["mth"] = odfusion(self.args.mth, vol.flags["mth"])
# append additive args from argv to volflags # append additive args from argv to volflags
hooks = "xbu xau xiu xbr xar xbd xad xm xban".split() hooks = "xbu xau xiu xbr xar xbd xad xm xban".split()
@@ -1727,6 +1721,9 @@ class AuthSrv(object):
def setup_pwhash(self, acct: dict[str, str]) -> None: def setup_pwhash(self, acct: dict[str, str]) -> None:
self.ah = PWHash(self.args) self.ah = PWHash(self.args)
if not self.ah.on: if not self.ah.on:
if self.args.ah_cli or self.args.ah_gen:
t = "\n BAD CONFIG:\n cannot --ah-cli or --ah-gen without --ah-alg"
raise Exception(t)
return return
if self.args.ah_cli: if self.args.ah_cli:

View File

@@ -17,7 +17,6 @@ def vf_bmap() -> dict[str, str]:
"no_thumb": "dthumb", "no_thumb": "dthumb",
"no_vthumb": "dvthumb", "no_vthumb": "dvthumb",
"no_athumb": "dathumb", "no_athumb": "dathumb",
"re_maxage": "scan",
"th_no_crop": "nocrop", "th_no_crop": "nocrop",
"dav_auth": "davauth", "dav_auth": "davauth",
"dav_rt": "davrt", "dav_rt": "davrt",
@@ -33,6 +32,7 @@ def vf_bmap() -> dict[str, str]:
"e2v", "e2v",
"e2vu", "e2vu",
"e2vp", "e2vp",
"exp",
"grid", "grid",
"hardlink", "hardlink",
"magic", "magic",
@@ -52,10 +52,19 @@ def vf_vmap() -> dict[str, str]:
ret = { ret = {
"no_hash": "nohash", "no_hash": "nohash",
"no_idx": "noidx", "no_idx": "noidx",
"re_maxage": "scan",
"th_convt": "convt", "th_convt": "convt",
"th_size": "thsize", "th_size": "thsize",
} }
for k in ("dbd", "lg_sbf", "md_sbf", "nrand", "sort", "unlist", "u2ts"): for k in (
"dbd",
"lg_sbf",
"md_sbf",
"nrand",
"sort",
"unlist",
"u2ts",
):
ret[k] = k ret[k] = k
return ret return ret
@@ -64,6 +73,8 @@ def vf_cmap() -> dict[str, str]:
"""argv-to-volflag: complex/lists""" """argv-to-volflag: complex/lists"""
ret = {} ret = {}
for k in ( for k in (
"exp_lg",
"exp_md",
"html_head", "html_head",
"mte", "mte",
"mth", "mth",

View File

@@ -92,6 +92,12 @@ class FtpAuth(DummyAuthorizer):
if bonk: if bonk:
logging.warning("client banned: invalid passwords") logging.warning("client banned: invalid passwords")
bans[ip] = bonk bans[ip] = bonk
try:
# only possible if multiprocessing disabled
self.hub.broker.httpsrv.bans[ip] = bonk
self.hub.broker.httpsrv.nban += 1
except:
pass
raise AuthenticationFailed("Authentication failed.") raise AuthenticationFailed("Authentication failed.")
@@ -148,7 +154,7 @@ class FtpFs(AbstractedFS):
try: try:
vpath = vpath.replace("\\", "/").strip("/") vpath = vpath.replace("\\", "/").strip("/")
rd, fn = os.path.split(vpath) rd, fn = os.path.split(vpath)
if ANYWIN and relchk(rd): if relchk(rd):
logging.warning("malicious vpath: %s", vpath) logging.warning("malicious vpath: %s", vpath)
t = "Unsupported characters in [{}]" t = "Unsupported characters in [{}]"
raise FSE(t.format(vpath), 1) raise FSE(t.format(vpath), 1)

View File

@@ -37,6 +37,7 @@ from .star import StreamTar
from .sutil import StreamArc, gfilter from .sutil import StreamArc, gfilter
from .szip import StreamZip from .szip import StreamZip
from .util import ( from .util import (
Garda,
HTTPCODE, HTTPCODE,
META_NOBOTS, META_NOBOTS,
MultipartParser, MultipartParser,
@@ -75,6 +76,7 @@ from .util import (
runhook, runhook,
s3enc, s3enc,
sanitize_fn, sanitize_fn,
sanitize_vpath,
sendfile_kern, sendfile_kern,
sendfile_py, sendfile_py,
undot, undot,
@@ -146,6 +148,7 @@ class HttpCli(object):
self.rem = " " self.rem = " "
self.vpath = " " self.vpath = " "
self.vpaths = " " self.vpaths = " "
self.gctx = " " # additional context for garda
self.trailing_slash = True self.trailing_slash = True
self.uname = " " self.uname = " "
self.pw = " " self.pw = " "
@@ -254,8 +257,8 @@ class HttpCli(object):
k, zs = header_line.split(":", 1) k, zs = header_line.split(":", 1)
self.headers[k.lower()] = zs.strip() self.headers[k.lower()] = zs.strip()
except: except:
msg = " ]\n#[ ".join(headerlines) msg = "#[ " + " ]\n#[ ".join(headerlines) + " ]"
raise Pebkac(400, "bad headers:\n#[ " + msg + " ]") raise Pebkac(400, "bad headers", log=msg)
except Pebkac as ex: except Pebkac as ex:
self.mode = "GET" self.mode = "GET"
@@ -268,8 +271,14 @@ class HttpCli(object):
self.loud_reply(unicode(ex), status=ex.code, headers=h, volsan=True) self.loud_reply(unicode(ex), status=ex.code, headers=h, volsan=True)
except: except:
pass pass
if ex.log:
self.log("additional error context:\n" + ex.log, 6)
return False return False
self.conn.hsrv.nreq += 1
self.ua = self.headers.get("user-agent", "") self.ua = self.headers.get("user-agent", "")
self.is_rclone = self.ua.startswith("rclone/") self.is_rclone = self.ua.startswith("rclone/")
@@ -411,12 +420,9 @@ class HttpCli(object):
self.vpath + "/" if self.trailing_slash and self.vpath else self.vpath self.vpath + "/" if self.trailing_slash and self.vpath else self.vpath
) )
ok = "\x00" not in self.vpath if relchk(self.vpath) and (self.vpath != "*" or self.mode != "OPTIONS"):
if ANYWIN:
ok = ok and not relchk(self.vpath)
if not ok and (self.vpath != "*" or self.mode != "OPTIONS"):
self.log("invalid relpath [{}]".format(self.vpath)) self.log("invalid relpath [{}]".format(self.vpath))
self.cbonk(self.conn.hsrv.g422, self.vpath, "bad_vp", "invalid relpaths")
return self.tx_404() and self.keepalive return self.tx_404() and self.keepalive
zso = self.headers.get("authorization") zso = self.headers.get("authorization")
@@ -549,6 +555,9 @@ class HttpCli(object):
zb = b"<pre>" + html_escape(msg).encode("utf-8", "replace") zb = b"<pre>" + html_escape(msg).encode("utf-8", "replace")
h = {"WWW-Authenticate": 'Basic realm="a"'} if pex.code == 401 else {} h = {"WWW-Authenticate": 'Basic realm="a"'} if pex.code == 401 else {}
self.reply(zb, status=pex.code, headers=h, volsan=True) self.reply(zb, status=pex.code, headers=h, volsan=True)
if pex.log:
self.log("additional error context:\n" + pex.log, 6)
return self.keepalive return self.keepalive
except Pebkac: except Pebkac:
return False return False
@@ -559,6 +568,36 @@ class HttpCli(object):
else: else:
return self.conn.iphash.s(self.ip) return self.conn.iphash.s(self.ip)
def cbonk(self, g: Garda, v: str, reason: str, descr: str) -> bool:
self.conn.hsrv.nsus += 1
if not g.lim:
return False
bonk, ip = g.bonk(self.ip, v + self.gctx)
if not bonk:
return False
xban = self.vn.flags.get("xban")
if not xban or not runhook(
self.log,
xban,
self.vn.canonical(self.rem),
self.vpath,
self.host,
self.uname,
time.time(),
0,
self.ip,
time.time(),
reason,
):
self.log("client banned: %s" % (descr,), 1)
self.conn.hsrv.bans[ip] = bonk
self.conn.hsrv.nban += 1
return True
return False
def is_banned(self) -> bool: def is_banned(self) -> bool:
if not self.conn.bans: if not self.conn.bans:
return False return False
@@ -678,24 +717,7 @@ class HttpCli(object):
or not self.args.nonsus_urls or not self.args.nonsus_urls
or not self.args.nonsus_urls.search(self.vpath) or not self.args.nonsus_urls.search(self.vpath)
): ):
bonk, ip = g.bonk(self.ip, self.vpath) self.cbonk(g, self.vpath, str(status), "%ss" % (status,))
if bonk:
xban = self.vn.flags.get("xban")
if not xban or not runhook(
self.log,
xban,
self.vn.canonical(self.rem),
self.vpath,
self.host,
self.uname,
time.time(),
0,
self.ip,
time.time(),
str(status),
):
self.log("client banned: %ss" % (status,), 1)
self.conn.hsrv.bans[ip] = bonk
if volsan: if volsan:
vols = list(self.asrv.vfs.all_vols.values()) vols = list(self.asrv.vfs.all_vols.values())
@@ -2121,8 +2143,10 @@ class HttpCli(object):
return True return True
def get_pwd_cookie(self, pwd: str) -> str: def get_pwd_cookie(self, pwd: str) -> str:
if self.asrv.ah.hash(pwd) in self.asrv.iacct: hpwd = self.asrv.ah.hash(pwd)
msg = "login ok" uname = self.asrv.iacct.get(hpwd)
if uname:
msg = "hi " + uname
dur = int(60 * 60 * self.args.logout) dur = int(60 * 60 * self.args.logout)
else: else:
logpwd = pwd logpwd = pwd
@@ -2133,27 +2157,7 @@ class HttpCli(object):
logpwd = "%" + base64.b64encode(zb[:12]).decode("utf-8") logpwd = "%" + base64.b64encode(zb[:12]).decode("utf-8")
self.log("invalid password: {}".format(logpwd), 3) self.log("invalid password: {}".format(logpwd), 3)
self.cbonk(self.conn.hsrv.gpwd, pwd, "pw", "invalid passwords")
g = self.conn.hsrv.gpwd
if g.lim:
bonk, ip = g.bonk(self.ip, pwd)
if bonk:
xban = self.vn.flags.get("xban")
if not xban or not runhook(
self.log,
xban,
self.vn.canonical(self.rem),
self.vpath,
self.host,
self.uname,
time.time(),
0,
self.ip,
time.time(),
"pw",
):
self.log("client banned: invalid passwords", 1)
self.conn.hsrv.bans[ip] = bonk
msg = "naw dude" msg = "naw dude"
pwd = "x" # nosec pwd = "x" # nosec
@@ -2177,26 +2181,30 @@ class HttpCli(object):
new_dir = self.parser.require("name", 512) new_dir = self.parser.require("name", 512)
self.parser.drop() self.parser.drop()
sanitized = sanitize_fn(new_dir, "", []) return self._mkdir(vjoin(self.vpath, new_dir))
return self._mkdir(vjoin(self.vpath, sanitized))
def _mkdir(self, vpath: str, dav: bool = False) -> bool: def _mkdir(self, vpath: str, dav: bool = False) -> bool:
nullwrite = self.args.nw nullwrite = self.args.nw
self.gctx = vpath
vpath = undot(vpath)
vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True) vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True)
self._assert_safe_rem(rem) rem = sanitize_vpath(rem, "/", [])
fn = vfs.canonical(rem) fn = vfs.canonical(rem)
if not fn.startswith(vfs.realpath):
self.log("invalid mkdir [%s] [%s]" % (self.gctx, vpath), 1)
raise Pebkac(422)
if not nullwrite: if not nullwrite:
fdir = os.path.dirname(fn) fdir = os.path.dirname(fn)
if not bos.path.isdir(fdir): if dav and not bos.path.isdir(fdir):
raise Pebkac(409, "parent folder does not exist") raise Pebkac(409, "parent folder does not exist")
if bos.path.isdir(fn): if bos.path.isdir(fn):
raise Pebkac(405, "that folder exists already") raise Pebkac(405, 'folder "/%s" already exists' % (vpath,))
try: try:
bos.mkdir(fn) bos.makedirs(fn)
except OSError as ex: except OSError as ex:
if ex.errno == errno.EACCES: if ex.errno == errno.EACCES:
raise Pebkac(500, "the server OS denied write-access") raise Pebkac(500, "the server OS denied write-access")
@@ -2205,7 +2213,7 @@ class HttpCli(object):
except: except:
raise Pebkac(500, min_ex()) raise Pebkac(500, min_ex())
self.out_headers["X-New-Dir"] = quotep(vpath.split("/")[-1]) self.out_headers["X-New-Dir"] = quotep(vpath)
if dav: if dav:
self.reply(b"", 201) self.reply(b"", 201)
@@ -2726,6 +2734,29 @@ class HttpCli(object):
return file_lastmod, True return file_lastmod, True
def _expand(self, txt: str, phs: list[str]) -> str:
for ph in phs:
if ph.startswith("hdr."):
sv = str(self.headers.get(ph[4:], ""))
elif ph.startswith("self."):
sv = str(getattr(self, ph[5:], ""))
elif ph.startswith("cfg."):
sv = str(getattr(self.args, ph[4:], ""))
elif ph.startswith("vf."):
sv = str(self.vn.flags.get(ph[3:]) or "")
elif ph == "srv.itime":
sv = str(int(time.time()))
elif ph == "srv.htime":
sv = datetime.now(UTC).strftime("%Y-%m-%d, %H:%M:%S")
else:
self.log("unknown placeholder in server config: [%s]" % (ph), 3)
continue
sv = self.conn.hsrv.ptn_hsafe.sub("_", sv)
txt = txt.replace("{{%s}}" % (ph,), sv)
return txt
def tx_file(self, req_path: str) -> bool: def tx_file(self, req_path: str) -> bool:
status = 200 status = 200
logmsg = "{:4} {} ".format("", self.req) logmsg = "{:4} {} ".format("", self.req)
@@ -3052,7 +3083,7 @@ class HttpCli(object):
self.reply(ico, mime=mime, headers={"Last-Modified": lm}) self.reply(ico, mime=mime, headers={"Last-Modified": lm})
return True return True
def tx_md(self, fs_path: str) -> bool: def tx_md(self, vn: VFS, fs_path: str) -> bool:
logmsg = " %s @%s " % (self.req, self.uname) logmsg = " %s @%s " % (self.req, self.uname)
if not self.can_write: if not self.can_write:
@@ -3069,9 +3100,16 @@ class HttpCli(object):
st = bos.stat(html_path) st = bos.stat(html_path)
ts_html = st.st_mtime ts_html = st.st_mtime
max_sz = 1024 * self.args.txt_max
sz_md = 0 sz_md = 0
lead = b"" lead = b""
fullfile = b""
for buf in yieldfile(fs_path): for buf in yieldfile(fs_path):
if sz_md < max_sz:
fullfile += buf
else:
fullfile = b""
if not sz_md and b"\n" in buf[:2]: if not sz_md and b"\n" in buf[:2]:
lead = buf[: buf.find(b"\n") + 1] lead = buf[: buf.find(b"\n") + 1]
sz_md += len(lead) sz_md += len(lead)
@@ -3080,6 +3118,21 @@ class HttpCli(object):
for c, v in [(b"&", 4), (b"<", 3), (b">", 3)]: for c, v in [(b"&", 4), (b"<", 3), (b">", 3)]:
sz_md += (len(buf) - len(buf.replace(c, b""))) * v sz_md += (len(buf) - len(buf.replace(c, b""))) * v
if (
fullfile
and "exp" in vn.flags
and "edit" not in self.uparam
and "edit2" not in self.uparam
and vn.flags.get("exp_md")
):
fulltxt = fullfile.decode("utf-8", "replace")
fulltxt = self._expand(fulltxt, vn.flags.get("exp_md") or [])
fullfile = fulltxt.encode("utf-8", "replace")
if fullfile:
fullfile = html_bescape(fullfile)
sz_md = len(lead) + len(fullfile)
file_ts = int(max(ts_md, ts_html, self.E.t0)) file_ts = int(max(ts_md, ts_html, 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["Last-Modified"] = file_lastmod
@@ -3121,8 +3174,11 @@ class HttpCli(object):
try: try:
self.s.sendall(html[0] + lead) self.s.sendall(html[0] + lead)
for buf in yieldfile(fs_path): if fullfile:
self.s.sendall(html_bescape(buf)) self.s.sendall(fullfile)
else:
for buf in yieldfile(fs_path):
self.s.sendall(html_bescape(buf))
self.s.sendall(html[1]) self.s.sendall(html[1])
@@ -3753,7 +3809,7 @@ class HttpCli(object):
or "edit2" in self.uparam or "edit2" in self.uparam
) )
): ):
return self.tx_md(abspath) return self.tx_md(vn, abspath)
return self.tx_file(abspath) return self.tx_file(abspath)
@@ -3815,6 +3871,10 @@ class HttpCli(object):
if bos.path.exists(fn): if bos.path.exists(fn):
with open(fsenc(fn), "rb") as f: with open(fsenc(fn), "rb") as f:
logues[n] = f.read().decode("utf-8") logues[n] = f.read().decode("utf-8")
if "exp" in vn.flags:
logues[n] = self._expand(
logues[n], vn.flags.get("exp_lg") or []
)
readme = "" readme = ""
if not self.args.no_readme and not logues[1]: if not self.args.no_readme and not logues[1]:
@@ -3824,6 +3884,8 @@ class HttpCli(object):
with open(fsenc(fn), "rb") as f: with open(fsenc(fn), "rb") as f:
readme = f.read().decode("utf-8") readme = f.read().decode("utf-8")
break break
if readme and "exp" in vn.flags:
readme = self._expand(readme, vn.flags.get("exp_md") or [])
vf = vn.flags vf = vn.flags
unlist = vf.get("unlist", "") unlist = vf.get("unlist", "")
@@ -4134,6 +4196,12 @@ class HttpCli(object):
if sz < 1024 * self.args.txt_max: if sz < 1024 * self.args.txt_max:
with open(fsenc(docpath), "rb") as f: with open(fsenc(docpath), "rb") as f:
doctxt = f.read().decode("utf-8", "replace") doctxt = f.read().decode("utf-8", "replace")
if doc.lower().endswith(".md") and "exp" in vn.flags:
doctxt = self._expand(doctxt, vn.flags.get("exp_md") or [])
else:
self.log("doc 2big: [{}]".format(doc), c=6)
doctxt = "( size of textfile exceeds serverside limit )"
else: else:
self.log("doc 404: [{}]".format(doc), c=6) self.log("doc 404: [{}]".format(doc), c=6)
doctxt = "( textfile not found )" doctxt = "( textfile not found )"

View File

@@ -128,6 +128,9 @@ class HttpSrv(object):
self.u2fh = FHC() self.u2fh = FHC()
self.metrics = Metrics(self) self.metrics = Metrics(self)
self.nreq = 0
self.nsus = 0
self.nban = 0
self.srvs: list[socket.socket] = [] self.srvs: list[socket.socket] = []
self.ncli = 0 # exact self.ncli = 0 # exact
self.clients: set[HttpConn] = set() # laggy self.clients: set[HttpConn] = set() # laggy
@@ -149,6 +152,7 @@ class HttpSrv(object):
self._build_statics() self._build_statics()
self.ptn_cc = re.compile(r"[\x00-\x1f]") self.ptn_cc = re.compile(r"[\x00-\x1f]")
self.ptn_hsafe = re.compile(r"[\x00-\x1f<>\"'&]")
self.mallow = "GET HEAD POST PUT DELETE OPTIONS".split() self.mallow = "GET HEAD POST PUT DELETE OPTIONS".split()
if not self.args.no_dav: if not self.args.no_dav:

View File

@@ -34,14 +34,23 @@ class Metrics(object):
ret: list[str] = [] ret: list[str] = []
def addc(k: str, unit: str, v: str, desc: str) -> None: def addc(k: str, v: str, desc: str) -> None:
if unit: zs = "# TYPE %s counter\n# HELP %s %s\n%s_created %s\n%s_total %s"
k += "_" + unit ret.append(zs % (k, k, desc, k, int(self.hsrv.t0), k, v))
zs = "# TYPE %s counter\n# UNIT %s %s\n# HELP %s %s\n%s_created %s\n%s_total %s"
ret.append(zs % (k, k, unit, k, desc, k, int(self.hsrv.t0), k, v)) def adduc(k: str, unit: str, v: str, desc: str) -> None:
else: k += "_" + unit
zs = "# TYPE %s counter\n# HELP %s %s\n%s_created %s\n%s_total %s" zs = "# TYPE %s counter\n# UNIT %s %s\n# HELP %s %s\n%s_created %s\n%s_total %s"
ret.append(zs % (k, k, desc, k, int(self.hsrv.t0), k, v)) ret.append(zs % (k, k, unit, k, desc, k, int(self.hsrv.t0), k, v))
def addg(k: str, v: str, desc: str) -> None:
zs = "# TYPE %s gauge\n# HELP %s %s\n%s %s"
ret.append(zs % (k, k, desc, k, v))
def addug(k: str, unit: str, v: str, desc: str) -> None:
k += "_" + unit
zs = "# TYPE %s gauge\n# UNIT %s %s\n# HELP %s %s\n%s %s"
ret.append(zs % (k, k, unit, k, desc, k, v))
def addh(k: str, typ: str, desc: str) -> None: def addh(k: str, typ: str, desc: str) -> None:
zs = "# TYPE %s %s\n# HELP %s %s" zs = "# TYPE %s %s\n# HELP %s %s"
@@ -54,17 +63,75 @@ class Metrics(object):
def addv(k: str, v: str) -> None: def addv(k: str, v: str) -> None:
ret.append("%s %s" % (k, v)) ret.append("%s %s" % (k, v))
t = "time since last copyparty restart"
v = "{:.3f}".format(time.time() - self.hsrv.t0) v = "{:.3f}".format(time.time() - self.hsrv.t0)
addc("cpp_uptime", "seconds", v, "time since last server restart") addug("cpp_uptime", "seconds", v, t)
# timestamps are gauges because initial value is not zero
t = "unixtime of last copyparty restart"
v = "{:.3f}".format(self.hsrv.t0)
addug("cpp_boot_unixtime", "seconds", v, t)
t = "number of open http(s) client connections"
addg("cpp_http_conns", str(self.hsrv.ncli), t)
t = "number of http(s) requests since last restart"
addc("cpp_http_reqs", str(self.hsrv.nreq), t)
t = "number of 403/422/malicious reqs since restart"
addc("cpp_sus_reqs", str(self.hsrv.nsus), t)
v = str(len(conn.bans or [])) v = str(len(conn.bans or []))
addc("cpp_bans", "", v, "number of banned IPs") addg("cpp_active_bans", v, "number of currently banned IPs")
t = "number of IPs banned since last restart"
addg("cpp_total_bans", str(self.hsrv.nban), t)
if not args.nos_vst:
x = self.hsrv.broker.ask("up2k.get_state")
vs = json.loads(x.get())
nvidle = 0
nvbusy = 0
nvoffline = 0
for v in vs["volstate"].values():
if v == "online, idle":
nvidle += 1
elif "OFFLINE" in v:
nvoffline += 1
else:
nvbusy += 1
addg("cpp_idle_vols", str(nvidle), "number of idle/ready volumes")
addg("cpp_busy_vols", str(nvbusy), "number of busy/indexing volumes")
addg("cpp_offline_vols", str(nvoffline), "number of offline volumes")
t = "time since last database activity (upload/rename/delete)"
addug("cpp_db_idle", "seconds", str(vs["dbwt"]), t)
t = "unixtime of last database activity (upload/rename/delete)"
addug("cpp_db_act", "seconds", str(vs["dbwu"]), t)
t = "number of files queued for hashing/indexing"
addg("cpp_hashing_files", str(vs["hashq"]), t)
t = "number of files queued for metadata scanning"
addg("cpp_tagq_files", str(vs["tagq"]), t)
try:
t = "number of files queued for plugin-based analysis"
addg("cpp_mtpq_files", str(int(vs["mtpq"])), t)
except:
pass
if not args.nos_hdd: if not args.nos_hdd:
addbh("cpp_disk_size_bytes", "total HDD size of volume") addbh("cpp_disk_size_bytes", "total HDD size of volume")
addbh("cpp_disk_free_bytes", "free HDD space in volume") addbh("cpp_disk_free_bytes", "free HDD space in volume")
for vpath, vol in allvols: for vpath, vol in allvols:
free, total = get_df(vol.realpath) free, total = get_df(vol.realpath)
if free is None or total is None:
continue
addv('cpp_disk_size_bytes{vol="/%s"}' % (vpath), str(total)) addv('cpp_disk_size_bytes{vol="/%s"}' % (vpath), str(total))
addv('cpp_disk_free_bytes{vol="/%s"}' % (vpath), str(free)) addv('cpp_disk_free_bytes{vol="/%s"}' % (vpath), str(free))
@@ -161,5 +228,6 @@ class Metrics(object):
ret.append("# EOF") ret.append("# EOF")
mime = "application/openmetrics-text; version=1.0.0; charset=utf-8" mime = "application/openmetrics-text; version=1.0.0; charset=utf-8"
mime = cli.uparam.get("mime") or mime
cli.reply("\n".join(ret).encode("utf-8"), mime=mime) cli.reply("\n".join(ret).encode("utf-8"), mime=mime)
return True return True

View File

@@ -136,8 +136,12 @@ class PWHash(object):
import getpass import getpass
while True: while True:
p1 = getpass.getpass("password> ") try:
p2 = getpass.getpass("again or just hit ENTER> ") p1 = getpass.getpass("password> ")
p2 = getpass.getpass("again or just hit ENTER> ")
except EOFError:
return
if p2 and p1 != p2: if p2 and p1 != p2:
print("\033[31minputs don't match; try again\033[0m", file=sys.stderr) print("\033[31minputs don't match; try again\033[0m", file=sys.stderr)
continue continue

View File

@@ -39,6 +39,7 @@ from .util import (
FFMPEG_URL, FFMPEG_URL,
VERSIONS, VERSIONS,
Daemon, Daemon,
DEF_EXP,
DEF_MTE, DEF_MTE,
DEF_MTH, DEF_MTH,
Garda, Garda,
@@ -442,6 +443,10 @@ class SvcHub(object):
mth = ODict.fromkeys(DEF_MTH.split(","), True) mth = ODict.fromkeys(DEF_MTH.split(","), True)
al.mth = odfusion(mth, al.mth) al.mth = odfusion(mth, al.mth)
exp = ODict.fromkeys(DEF_EXP.split(" "), True)
al.exp_md = odfusion(exp, al.exp_md.replace(" ", ","))
al.exp_lg = odfusion(exp, al.exp_lg.replace(" ", ","))
for k in ["no_hash", "no_idx"]: for k in ["no_hash", "no_idx"]:
ptn = getattr(self.args, k) ptn = getattr(self.args, k)
if ptn: if ptn:

View File

@@ -65,6 +65,11 @@ from .util import (
w8b64enc, w8b64enc,
) )
try:
from pathlib import Path
except:
pass
if HAVE_SQLITE3: if HAVE_SQLITE3:
import sqlite3 import sqlite3
@@ -261,6 +266,7 @@ class Up2k(object):
"hashq": self.n_hashq, "hashq": self.n_hashq,
"tagq": self.n_tagq, "tagq": self.n_tagq,
"mtpq": mtpq, "mtpq": mtpq,
"dbwu": "{:.2f}".format(self.db_act),
"dbwt": "{:.2f}".format( "dbwt": "{:.2f}".format(
min(1000 * 24 * 60 * 60 - 1, time.time() - self.db_act) min(1000 * 24 * 60 * 60 - 1, time.time() - self.db_act)
), ),
@@ -814,7 +820,7 @@ class Up2k(object):
if str(fl[k1]) == str(getattr(self.args, k2)): if str(fl[k1]) == str(getattr(self.args, k2)):
del fl[k1] del fl[k1]
else: else:
fl[k1] = ",".join(x for x in fl) fl[k1] = ",".join(x for x in fl[k1])
a = [ a = [
(ft if v is True else ff if v is False else fv).format(k, str(v)) (ft if v is True else ff if v is False else fv).format(k, str(v))
for k, v in fl.items() for k, v in fl.items()
@@ -2723,7 +2729,18 @@ class Up2k(object):
raise Exception("symlink-fallback disabled in cfg") raise Exception("symlink-fallback disabled in cfg")
if not linked: if not linked:
os.symlink(fsenc(lsrc), fsenc(ldst)) if ANYWIN:
Path(ldst).symlink_to(lsrc)
if not bos.path.exists(dst):
try:
bos.unlink(dst)
except:
pass
t = "the created symlink [%s] did not resolve to [%s]"
raise Exception(t % (ldst, lsrc))
else:
os.symlink(fsenc(lsrc), fsenc(ldst))
linked = True linked = True
except Exception as ex: except Exception as ex:
self.log("cannot link; creating copy: " + repr(ex)) self.log("cannot link; creating copy: " + repr(ex))

View File

@@ -289,6 +289,8 @@ EXTS["vnd.mozilla.apng"] = "png"
MAGIC_MAP = {"jpeg": "jpg"} MAGIC_MAP = {"jpeg": "jpg"}
DEF_EXP = "self.ip self.ua self.uname self.host cfg.name cfg.logout vf.scan vf.thsize hdr.cf_ipcountry srv.itime srv.htime"
DEF_MTE = "circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash" DEF_MTE = "circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash"
DEF_MTH = ".vq,.aq,vc,ac,fmt,res,.fps" DEF_MTH = ".vq,.aq,vc,ac,fmt,res,.fps"
@@ -1561,8 +1563,8 @@ def read_header(sr: Unrecv, t_idle: int, t_tot: int) -> list[str]:
raise Pebkac( raise Pebkac(
400, 400,
"protocol error while reading headers:\n" "protocol error while reading headers",
+ ret.decode("utf-8", "replace"), log=ret.decode("utf-8", "replace"),
) )
ofs = ret.find(b"\r\n\r\n") ofs = ret.find(b"\r\n\r\n")
@@ -1771,7 +1773,16 @@ def sanitize_fn(fn: str, ok: str, bad: list[str]) -> str:
return fn.strip() return fn.strip()
def sanitize_vpath(vp: str, ok: str, bad: list[str]) -> str:
parts = vp.replace(os.sep, "/").split("/")
ret = [sanitize_fn(x, ok, bad) for x in parts]
return "/".join(ret)
def relchk(rp: str) -> str: def relchk(rp: str) -> str:
if "\x00" in rp:
return "[nul]"
if ANYWIN: if ANYWIN:
if "\n" in rp or "\r" in rp: if "\n" in rp or "\r" in rp:
return "x\nx" return "x\nx"
@@ -1809,15 +1820,18 @@ def exclude_dotfiles(filepaths: list[str]) -> list[str]:
def odfusion(base: ODict[str, bool], oth: str) -> ODict[str, bool]: def odfusion(base: ODict[str, bool], oth: str) -> ODict[str, bool]:
# merge an "ordered set" (just a dict really) with another list of keys # merge an "ordered set" (just a dict really) with another list of keys
words0 = [x for x in oth.split(",") if x]
words1 = [x for x in oth[1:].split(",") if x]
ret = base.copy() ret = base.copy()
if oth.startswith("+"): if oth.startswith("+"):
for k in oth[1:].split(","): for k in words1:
ret[k] = True ret[k] = True
elif oth[:1] in ("-", "/"): elif oth[:1] in ("-", "/"):
for k in oth[1:].split(","): for k in words1:
ret.pop(k, None) ret.pop(k, None)
else: else:
ret = ODict.fromkeys(oth.split(","), True) ret = ODict.fromkeys(words0, True)
return ret return ret
@@ -2971,9 +2985,12 @@ def hidedir(dp) -> None:
class Pebkac(Exception): class Pebkac(Exception):
def __init__(self, code: int, msg: Optional[str] = None) -> None: def __init__(
self, code: int, msg: Optional[str] = None, log: Optional[str] = None
) -> None:
super(Pebkac, self).__init__(msg or HTTPCODE[code]) super(Pebkac, self).__init__(msg or HTTPCODE[code])
self.code = code self.code = code
self.log = log
def __repr__(self) -> str: def __repr__(self) -> str:
return "Pebkac({}, {})".format(self.code, repr(self.args)) return "Pebkac({}, {})".format(self.code, repr(self.args))

View File

@@ -1891,6 +1891,10 @@ html.y #doc {
text-align: center; text-align: center;
padding: .5em; padding: .5em;
} }
#docul li.bn span {
font-weight: bold;
color: var(--fg-max);
}
#doc.prism { #doc.prism {
padding-left: 3em; padding-left: 3em;
} }

View File

@@ -251,7 +251,7 @@ var Ls = {
"mt_coth": "convert all others (not mp3) to opus\">oth", "mt_coth": "convert all others (not mp3) to opus\">oth",
"mt_tint": "background level (0-100) on the seekbar$Nto make buffering less distracting", "mt_tint": "background level (0-100) on the seekbar$Nto make buffering less distracting",
"mt_eq": "enables the equalizer and gain control;$N$Nboost &lt;code&gt;0&lt;/code&gt; = standard 100% volume (unmodified)$N$Nwidth &lt;code&gt;1 &nbsp;&lt;/code&gt; = standard stereo (unmodified)$Nwidth &lt;code&gt;0.5&lt;/code&gt; = 50% left-right crossfeed$Nwidth &lt;code&gt;0 &nbsp;&lt;/code&gt; = mono$N$Nboost &lt;code&gt;-0.8&lt;/code&gt; &amp; width &lt;code&gt;10&lt;/code&gt; = vocal removal :^)$N$Nenabling the equalizer makes gapless albums fully gapless, so leave it on with all the values at zero (except width = 1) if you care about that", "mt_eq": "enables the equalizer and gain control;$N$Nboost &lt;code&gt;0&lt;/code&gt; = standard 100% volume (unmodified)$N$Nwidth &lt;code&gt;1 &nbsp;&lt;/code&gt; = standard stereo (unmodified)$Nwidth &lt;code&gt;0.5&lt;/code&gt; = 50% left-right crossfeed$Nwidth &lt;code&gt;0 &nbsp;&lt;/code&gt; = mono$N$Nboost &lt;code&gt;-0.8&lt;/code&gt; &amp; width &lt;code&gt;10&lt;/code&gt; = vocal removal :^)$N$Nenabling the equalizer makes gapless albums fully gapless, so leave it on with all the values at zero (except width = 1) if you care about that",
"mt_drc": "enables the dynamic range compressor (volume flattener / brickwaller); will also enable EQ to balance the spaghetti, so set all EQ fields except for 'width' to 0 if you don't want it$N$Nlowers the volume of audio above THRESHOLD dB; for every RATIO dB past THRESHOLD there is 1 dB of output, so default values of tresh -24 and ratio 12 means it should never get louder than -22 dB and it is safe to increase the equalizer boost to 0.8, or even 1.8 with ATK 0 and a huge RLS like 90$N$Nplease see wikipedia instead, this is probably wrong", "mt_drc": "enables the dynamic range compressor (volume flattener / brickwaller); will also enable EQ to balance the spaghetti, so set all EQ fields except for 'width' to 0 if you don't want it$N$Nlowers the volume of audio above THRESHOLD dB; for every RATIO dB past THRESHOLD there is 1 dB of output, so default values of tresh -24 and ratio 12 means it should never get louder than -22 dB and it is safe to increase the equalizer boost to 0.8, or even 1.8 with ATK 0 and a huge RLS like 90 (only works in firefox; RLS is max 1 in other browsers)$N$N(see wikipedia, they explain it much better)",
"mb_play": "play", "mb_play": "play",
"mm_hashplay": "play this audio file?", "mm_hashplay": "play this audio file?",
@@ -730,7 +730,7 @@ var Ls = {
"mt_coth": "konverter alt annet (men ikke mp3) til opus\">andre", "mt_coth": "konverter alt annet (men ikke mp3) til opus\">andre",
"mt_tint": "nivå av bakgrunnsfarge på søkestripa (0-100),$Ngjør oppdateringer mindre distraherende", "mt_tint": "nivå av bakgrunnsfarge på søkestripa (0-100),$Ngjør oppdateringer mindre distraherende",
"mt_eq": "aktiver tonekontroll og forsterker;$N$Nboost &lt;code&gt;0&lt;/code&gt; = normal volumskala$N$Nwidth &lt;code&gt;1 &nbsp;&lt;/code&gt; = normal stereo$Nwidth &lt;code&gt;0.5&lt;/code&gt; = 50% blanding venstre-høyre$Nwidth &lt;code&gt;0 &nbsp;&lt;/code&gt; = mono$N$Nboost &lt;code&gt;-0.8&lt;/code&gt; &amp; width &lt;code&gt;10&lt;/code&gt; = instrumental :^)$N$Nreduserer også dødtid imellom sangfiler", "mt_eq": "aktiver tonekontroll og forsterker;$N$Nboost &lt;code&gt;0&lt;/code&gt; = normal volumskala$N$Nwidth &lt;code&gt;1 &nbsp;&lt;/code&gt; = normal stereo$Nwidth &lt;code&gt;0.5&lt;/code&gt; = 50% blanding venstre-høyre$Nwidth &lt;code&gt;0 &nbsp;&lt;/code&gt; = mono$N$Nboost &lt;code&gt;-0.8&lt;/code&gt; &amp; width &lt;code&gt;10&lt;/code&gt; = instrumental :^)$N$Nreduserer også dødtid imellom sangfiler",
"mt_drc": "aktiver volum-utjevning (dynamic range compressor); vil også aktivere tonejustering, så sett alle EQ-feltene bortsett fra 'width' til 0 hvis du ikke vil ha noe EQ$N$Nfilteret vil dempe volumet på alt som er høyere enn TRESH dB; for hver RATIO dB over grensen er det 1dB som treffer høyttalerne, så standardverdiene tresh -24 og ratio 12 skal bety at volumet ikke går høyere enn -22 dB, slik at man trygt kan øke boost-verdien i equalizer'n til rundt 0.8, eller 1.8 kombinert med ATK 0 og RLS 90$N$Ngodt mulig jeg har misforstått litt, så wikipedia forklarer nok bedre", "mt_drc": "aktiver volum-utjevning (dynamic range compressor); vil også aktivere tonejustering, så sett alle EQ-feltene bortsett fra 'width' til 0 hvis du ikke vil ha noe EQ$N$Nfilteret vil dempe volumet på alt som er høyere enn TRESH dB; for hver RATIO dB over grensen er det 1dB som treffer høyttalerne, så standardverdiene tresh -24 og ratio 12 skal bety at volumet ikke går høyere enn -22 dB, slik at man trygt kan øke boost-verdien i equalizer'n til rundt 0.8, eller 1.8 kombinert med ATK 0 og RLS 90 (bare mulig i firefox; andre nettlesere tar ikke høyere RLS enn 1)$N$Nwikipedia forklarer dette mye bedre forresten",
"mb_play": "lytt", "mb_play": "lytt",
"mm_hashplay": "spill denne sangen?", "mm_hashplay": "spill denne sangen?",
@@ -2501,7 +2501,7 @@ var afilt = (function () {
"drcen": false, "drcen": false,
"bands": [31.25, 62.5, 125, 250, 500, 1000, 2000, 4000, 8000, 16000], "bands": [31.25, 62.5, 125, 250, 500, 1000, 2000, 4000, 8000, 16000],
"gains": [4, 3, 2, 1, 0, 0, 1, 2, 3, 4], "gains": [4, 3, 2, 1, 0, 0, 1, 2, 3, 4],
"drcv": [-24, 30, 12, 0.003, 0.25], "drcv": [-24, 30, 12, 0.01, 0.25],
"drch": ['tresh', 'knee', 'ratio', 'atk', 'rls'], "drch": ['tresh', 'knee', 'ratio', 'atk', 'rls'],
"drck": ['threshold', 'knee', 'ratio', 'attack', 'release'], "drck": ['threshold', 'knee', 'ratio', 'attack', 'release'],
"drcn": null, "drcn": null,
@@ -2576,6 +2576,8 @@ var afilt = (function () {
var gains = jread('au_eq_gain', r.gains); var gains = jread('au_eq_gain', r.gains);
if (r.gains.length == gains.length) if (r.gains.length == gains.length)
r.gains = gains; r.gains = gains;
r.drcv = jread('au_drcv', r.drcv);
} }
catch (ex) { } catch (ex) { }
@@ -2692,6 +2694,17 @@ var afilt = (function () {
for (var a = 0; a < r.drcv.length; a++) for (var a = 0; a < r.drcv.length; a++)
fi[r.drck[a]].value = r.drcv[a]; fi[r.drck[a]].value = r.drcv[a];
if (r.drcv[3] < 0.02) {
// avoid static at decode start
fi.attack.value = 0.02;
setTimeout(function () {
try {
fi.attack.value = r.drcv[3];
}
catch (ex) { }
}, 200);
}
r.filters.push(fi); r.filters.push(fi);
timer.add(showdrc); timer.add(showdrc);
} }
@@ -2783,7 +2796,7 @@ var afilt = (function () {
return; return;
r.drcv[n] = v; r.drcv[n] = v;
jwrite('au_drc', r.drcv); jwrite('au_drcv', r.drcv);
if (r.drcn) if (r.drcn)
r.drcn[r.drck[n]].value = v; r.drcn[r.drck[n]].value = v;
} }
@@ -3784,7 +3797,7 @@ var fileman = (function () {
function rename_cb() { function rename_cb() {
if (this.status !== 201) { if (this.status !== 201) {
var msg = this.responseText; var msg = unpre(this.responseText);
toast.err(9, L.fr_efail + msg); toast.err(9, L.fr_efail + msg);
return; return;
} }
@@ -3833,7 +3846,7 @@ var fileman = (function () {
} }
function delete_cb() { function delete_cb() {
if (this.status !== 200) { if (this.status !== 200) {
var msg = this.responseText; var msg = unpre(this.responseText);
toast.err(9, L.fd_err + msg); toast.err(9, L.fd_err + msg);
return; return;
} }
@@ -3954,7 +3967,7 @@ var fileman = (function () {
} }
function paste_cb() { function paste_cb() {
if (this.status !== 201) { if (this.status !== 201) {
var msg = this.responseText; var msg = unpre(this.responseText);
toast.err(9, L.fp_err + msg); toast.err(9, L.fp_err + msg);
return; return;
} }
@@ -4287,7 +4300,7 @@ var showfile = (function () {
}; };
r.mktree = function () { r.mktree = function () {
var html = ['<li class="bn">' + L.tv_lst + '<br />' + linksplit(get_vpath()).join('') + '</li>']; var html = ['<li class="bn">' + L.tv_lst + '<br />' + linksplit(get_vpath()).join('<span>/</span>') + '</li>'];
for (var a = 0; a < r.files.length; a++) { for (var a = 0; a < r.files.length; a++) {
var file = r.files[a]; var file = r.files[a];
html.push('<li><a href="?doc=' + html.push('<li><a href="?doc=' +
@@ -5287,10 +5300,7 @@ document.onkeydown = function (e) {
function xhr_search_results() { function xhr_search_results() {
if (this.status !== 200) { if (this.status !== 200) {
var msg = this.responseText; var msg = unpre(this.responseText);
if (msg.indexOf('<pre>') === 0)
msg = msg.slice(5);
srch_msg(true, "http " + this.status + ": " + msg); srch_msg(true, "http " + this.status + ": " + msg);
search_in_progress = 0; search_in_progress = 0;
return; return;
@@ -5329,7 +5339,7 @@ document.onkeydown = function (e) {
if (ext.length > 8) if (ext.length > 8)
ext = '%'; ext = '%';
var links = linksplit(r.rp + '', id).join(''), var links = linksplit(r.rp + '', id).join('<span>/</span>'),
nodes = ['<tr><td>-</td><td><div>' + links + '</div>', sz]; nodes = ['<tr><td>-</td><td><div>' + links + '</div>', sz];
for (var b = 0; b < tagord.length; b++) { for (var b = 0; b < tagord.length; b++) {
@@ -7155,16 +7165,17 @@ var msel = (function () {
form.onsubmit = function (e) { form.onsubmit = function (e) {
ev(e); ev(e);
clmod(sf, 'vis', 1); clmod(sf, 'vis', 1);
sf.textContent = 'creating "' + tb.value + '"...'; var dn = tb.value;
sf.textContent = 'creating "' + dn + '"...';
var fd = new FormData(); var fd = new FormData();
fd.append("act", "mkdir"); fd.append("act", "mkdir");
fd.append("name", tb.value); fd.append("name", dn);
var xhr = new XHR(); var xhr = new XHR();
xhr.vp = get_evpath(); xhr.vp = get_evpath();
xhr.dn = tb.value; xhr.dn = dn;
xhr.open('POST', xhr.vp, true); xhr.open('POST', dn.startsWith('/') ? (SR || '/') : xhr.vp, true);
xhr.onload = xhr.onerror = cb; xhr.onload = xhr.onerror = cb;
xhr.responseType = 'text'; xhr.responseType = 'text';
xhr.send(fd); xhr.send(fd);
@@ -7181,7 +7192,7 @@ var msel = (function () {
xhrchk(this, L.fd_xe1, L.fd_xe2); xhrchk(this, L.fd_xe1, L.fd_xe2);
if (this.status !== 201) { if (this.status !== 201) {
sf.textContent = 'error: ' + this.responseText; sf.textContent = 'error: ' + unpre(this.responseText);
return; return;
} }
@@ -7190,8 +7201,9 @@ var msel = (function () {
sf.textContent = ''; sf.textContent = '';
var dn = this.getResponseHeader('X-New-Dir'); var dn = this.getResponseHeader('X-New-Dir');
dn = dn || uricom_enc(this.dn); dn = dn ? '/' + dn + '/' : uricom_enc(this.dn);
treectl.goto(this.vp + dn + '/', true); treectl.goto(dn, true);
tree_scrollto();
} }
})(); })();
@@ -7228,7 +7240,7 @@ var msel = (function () {
xhrchk(this, L.fsm_xe1, L.fsm_xe2); xhrchk(this, L.fsm_xe1, L.fsm_xe2);
if (this.status < 200 || this.status > 201) { if (this.status < 200 || this.status > 201) {
sf.textContent = 'error: ' + this.responseText; sf.textContent = 'error: ' + unpre(this.responseText);
return; return;
} }
@@ -7573,7 +7585,7 @@ var unpost = (function () {
'<tr><td><a me="' + me + '" class="n' + a + '" href="#">' + L.un_del + '</a></td>' + '<tr><td><a me="' + me + '" class="n' + a + '" href="#">' + L.un_del + '</a></td>' +
'<td>' + unix2iso(res[a].at) + '</td>' + '<td>' + unix2iso(res[a].at) + '</td>' +
'<td>' + res[a].sz + '</td>' + '<td>' + res[a].sz + '</td>' +
'<td>' + linksplit(res[a].vp).join(' ') + '</td></tr>'); '<td>' + linksplit(res[a].vp).join('<span> / </span>') + '</td></tr>');
} }
html.push("</tbody></table>"); html.push("</tbody></table>");
@@ -7606,7 +7618,7 @@ var unpost = (function () {
function unpost_delete_cb() { function unpost_delete_cb() {
if (this.status !== 200) { if (this.status !== 200) {
var msg = this.responseText; var msg = unpre(this.responseText);
toast.err(9, L.un_derr + msg); toast.err(9, L.un_derr + msg);
return; return;
} }

View File

@@ -10,6 +10,7 @@
{{ html_head }} {{ html_head }}
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}"> <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}"> <link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
<style>ul{padding-left:1.3em}li{margin:.4em 0}</style>
</head> </head>
<body> <body>
@@ -48,9 +49,13 @@
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %} rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>W:</b> rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>W:</b>
</pre> </pre>
{% if s %} <ul>
<p><em>note: if you are on LAN (or just dont have valid certificates), add <code>--no-check-certificate</code> to the mount command</em><br />---</p> {% if s %}
{% endif %} <li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>
{% endif %}
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
<li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>
</ul>
<p>if you want to use the native WebDAV client in windows instead (slow and buggy), first run <a href="{{ r }}/.cpr/a/webdav-cfg.bat">webdav-cfg.bat</a> to remove the 47 MiB filesize limit (also fixes latency and password login), then connect:</p> <p>if you want to use the native WebDAV client in windows instead (slow and buggy), first run <a href="{{ r }}/.cpr/a/webdav-cfg.bat">webdav-cfg.bat</a> to remove the 47 MiB filesize limit (also fixes latency and password login), then connect:</p>
<pre> <pre>
@@ -73,10 +78,13 @@
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %} rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b> rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b>
</pre> </pre>
{% if s %} <ul>
<p><em>note: if you are on LAN (or just dont have valid certificates), add <code>--no-check-certificate</code> to the mount command</em><br />---</p> {% if s %}
{% endif %} <li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>
{% endif %}
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
<li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>
</ul>
<p>or the emergency alternative (gnome/gui-only):</p> <p>or the emergency alternative (gnome/gui-only):</p>
<!-- gnome-bug: ignores vp --> <!-- gnome-bug: ignores vp -->
<pre> <pre>
@@ -123,8 +131,14 @@
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>W:</b> rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>W:</b>
</pre> </pre>
<p><em>note: if you are on LAN (or just dont have valid certificates), add <code>no_check_certificate=true</code> to the config command</em><br />---</p>
{% endif %} {% endif %}
<ul>
{% if args.ftps %}
<li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>
{% endif %}
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
<li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>
</ul>
<p>if you want to use the native FTP client in windows instead (please dont), press <code>win+R</code> and run this command:</p> <p>if you want to use the native FTP client in windows instead (please dont), press <code>win+R</code> and run this command:</p>
<pre> <pre>
explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}<b>{{ pw }}</b>:k@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }} explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}<b>{{ pw }}</b>:k@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
@@ -145,8 +159,14 @@
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>mp</b> rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>mp</b>
</pre> </pre>
<p><em>note: if you are on LAN (or just dont have valid certificates), add <code>no_check_certificate=true</code> to the config command</em><br />---</p>
{% endif %} {% endif %}
<ul>
{% if args.ftps %}
<li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>
{% endif %}
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
<li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>
</ul>
<p>emergency alternative (gnome/gui-only):</p> <p>emergency alternative (gnome/gui-only):</p>
<!-- gnome-bug: ignores vp --> <!-- gnome-bug: ignores vp -->
<pre> <pre>
@@ -178,7 +198,7 @@
partyfuse.py{% if accs %} -a <b>{{ pw }}</b>{% endif %} http{{ s }}://{{ ep }}/{{ rvp }} <b><span class="os win">W:</span><span class="os lin mac">mp</span></b> partyfuse.py{% if accs %} -a <b>{{ pw }}</b>{% endif %} http{{ s }}://{{ ep }}/{{ rvp }} <b><span class="os win">W:</span><span class="os lin mac">mp</span></b>
</pre> </pre>
{% if s %} {% if s %}
<p><em>note: if you are on LAN (or just dont have valid certificates), add <code>-td</code></em></p> <ul><li>if you are on LAN (or just dont have valid certificates), add <code>-td</code></li></ul>
{% endif %} {% endif %}
<p> <p>
you can use <a href="{{ r }}/.cpr/a/u2c.py">u2c.py</a> to upload (sometimes faster than web-browsers) you can use <a href="{{ r }}/.cpr/a/u2c.py">u2c.py</a> to upload (sometimes faster than web-browsers)

View File

@@ -1,3 +1,18 @@
:root {
--fg: #ccc;
--fg-max: #fff;
--bg-u2: #2b2b2b;
--bg-u5: #444;
}
html.y {
--fg: #222;
--fg-max: #000;
--bg-u2: #f7f7f7;
--bg-u5: #ccc;
}
html.bz {
--bg-u2: #202231;
}
@font-face { @font-face {
font-family: 'scp'; font-family: 'scp';
font-display: swap; font-display: swap;
@@ -14,6 +29,7 @@ html {
max-width: min(34em, 90%); max-width: min(34em, 90%);
max-width: min(34em, calc(100% - 7em)); max-width: min(34em, calc(100% - 7em));
color: #ddd; color: #ddd;
color: var(--fg);
background: #333; background: #333;
background: var(--bg-u2); background: var(--bg-u2);
border: 0 solid #777; border: 0 solid #777;
@@ -171,24 +187,15 @@ html {
color: #f6a; color: #f6a;
} }
html.y #tt { html.y #tt {
color: #333;
background: #fff;
border-color: #888 #000 #777 #000; border-color: #888 #000 #777 #000;
} }
html.bz #tt { html.bz #tt {
background: #202231;
border-color: #3b3f58; border-color: #3b3f58;
} }
html.y #tt, html.y #tt,
html.y #toast { html.y #toast {
box-shadow: 0 .3em 1em rgba(0,0,0,0.4); box-shadow: 0 .3em 1em rgba(0,0,0,0.4);
} }
html.y #tt code {
color: #fff;
color: var(--fg-max);
background: #060;
background: var(--bg-u5);
}
#modalc code { #modalc code {
color: #060; color: #060;
background: transparent; background: transparent;
@@ -326,6 +333,9 @@ html.y .btn:focus {
box-shadow: 0 .1em .2em #037 inset; box-shadow: 0 .1em .2em #037 inset;
outline: #037 solid .1em; outline: #037 solid .1em;
} }
input[type="submit"] {
cursor: pointer;
}
input[type="text"]:focus, input[type="text"]:focus,
input:not([type]):focus, input:not([type]):focus,
textarea:focus { textarea:focus {

View File

@@ -1407,7 +1407,7 @@ function up2k_init(subtle) {
pvis.addfile([ pvis.addfile([
uc.fsearch ? esc(entry.name) : linksplit( uc.fsearch ? esc(entry.name) : linksplit(
entry.purl + uricom_enc(entry.name)).join(' '), entry.purl + uricom_enc(entry.name)).join(' / '),
'📐 ' + L.u_hashing, '📐 ' + L.u_hashing,
'' ''
], entry.size, draw_each); ], entry.size, draw_each);
@@ -2284,7 +2284,7 @@ function up2k_init(subtle) {
cdiff = (Math.abs(diff) <= 2) ? '3c0' : 'f0b', cdiff = (Math.abs(diff) <= 2) ? '3c0' : 'f0b',
sdiff = '<span style="color:#' + cdiff + '">diff ' + diff; sdiff = '<span style="color:#' + cdiff + '">diff ' + diff;
msg.push(linksplit(hit.rp).join('') + '<br /><small>' + tr + ' (srv), ' + tu + ' (You), ' + sdiff + '</small></span>'); msg.push(linksplit(hit.rp).join(' / ') + '<br /><small>' + tr + ' (srv), ' + tu + ' (You), ' + sdiff + '</small></span>');
} }
msg = msg.join('<br />\n'); msg = msg.join('<br />\n');
} }
@@ -2318,7 +2318,7 @@ function up2k_init(subtle) {
url += '?k=' + fk; url += '?k=' + fk;
} }
pvis.seth(t.n, 0, linksplit(url).join(' ')); pvis.seth(t.n, 0, linksplit(url).join(' / '));
} }
var chunksize = get_chunksize(t.size), var chunksize = get_chunksize(t.size),
@@ -2402,15 +2402,12 @@ function up2k_init(subtle) {
pvis.seth(t.n, 2, L.u_ehstmp, t); pvis.seth(t.n, 2, L.u_ehstmp, t);
var err = "", var err = "",
rsp = (xhr.responseText + ''), rsp = unpre(this.responseText),
ofs = rsp.lastIndexOf('\nURL: '); ofs = rsp.lastIndexOf('\nURL: ');
if (ofs !== -1) if (ofs !== -1)
rsp = rsp.slice(0, ofs); rsp = rsp.slice(0, ofs);
if (rsp.indexOf('<pre>') === 0)
rsp = rsp.slice(5);
if (rsp.indexOf('rate-limit ') !== -1) { if (rsp.indexOf('rate-limit ') !== -1) {
var penalty = rsp.replace(/.*rate-limit /, "").split(' ')[0]; var penalty = rsp.replace(/.*rate-limit /, "").split(' ')[0];
console.log("rate-limit: " + penalty); console.log("rate-limit: " + penalty);
@@ -2429,7 +2426,7 @@ function up2k_init(subtle) {
err = rsp; err = rsp;
ofs = err.indexOf('\n/'); ofs = err.indexOf('\n/');
if (ofs !== -1) { if (ofs !== -1) {
err = err.slice(0, ofs + 1) + linksplit(err.slice(ofs + 2).trimEnd()).join(' '); err = err.slice(0, ofs + 1) + linksplit(err.slice(ofs + 2).trimEnd()).join(' / ');
} }
if (!t.rechecks && (err_pend || err_srcb)) { if (!t.rechecks && (err_pend || err_srcb)) {
t.rechecks = 0; t.rechecks = 0;
@@ -2536,7 +2533,7 @@ function up2k_init(subtle) {
cdr = t.size; cdr = t.size;
var orz = function (xhr) { var orz = function (xhr) {
var txt = ((xhr.response && xhr.response.err) || xhr.responseText) + ''; var txt = unpre((xhr.response && xhr.response.err) || xhr.responseText);
if (txt.indexOf('upload blocked by x') + 1) { if (txt.indexOf('upload blocked by x') + 1) {
apop(st.busy.upload, upt); apop(st.busy.upload, upt);
apop(t.postlist, npart); apop(t.postlist, npart);

View File

@@ -622,9 +622,8 @@ function linksplit(rp, id) {
} }
var vlink = esc(uricom_dec(link)); var vlink = esc(uricom_dec(link));
if (link.indexOf('/') !== -1) { if (link.indexOf('/') !== -1)
vlink = vlink.slice(0, -1) + '<span>/</span>'; vlink = vlink.slice(0, -1);
}
if (!rp) { if (!rp) {
if (q) if (q)
@@ -1357,6 +1356,11 @@ function lf2br(txt) {
} }
function unpre(txt) {
return ('' + txt).replace(/^<pre>/, '');
}
var toast = (function () { var toast = (function () {
var r = {}, var r = {},
te = null, te = null,

View File

@@ -1,3 +1,47 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-1024-1643 `v1.9.15` expand placeholder
[made it just in time!](https://a.ocv.me/pub/g/nerd-stuff/PXL_20231024_170348367.jpg) (EDIT: nevermind, three of the containers didn't finish uploading to ghcr before takeoff ;_; all up now)
## new features
* #56 placeholder variables in markdown documents and prologue/epilogue html files
* default-disabled; must be enabled globally with `--exp` or per-volume with volflag `exp`
* `{{self.ip}}` becomes the client IP; see [/srv/expand/README.md](https://github.com/9001/copyparty/blob/hovudstraum/srv/expand/README.md) for more examples
* dynamic-range-compressor: reduced volume jumps between songs when enabled
## bugfixes
* v1.9.14 broke the `scan` volflag, causing volume rescans to happen every 10sec if enabled
* its global counterpart `--re-maxage` was not affected
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-1021-1443 `v1.9.14` uptime
## new features
* search for files by upload time
* option to display upload time in directory listings
* enable globally with `-e2d -mte +.up_at` or per-volume with volflags `e2d,mte=+.up_at`
* has a ~17% performance impact on directory listings
* [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression) in the audioplayer settings
* `--ban-404` is now default-enabled
* the turbo-uploader will now un-turbo when necessary to avoid banning itself
* this only affects accounts with permissions `g`, `G`, or `h`
* accounts with read-access (which are able to see directory listings anyways) and accounts with write-only access are no longer affected by `--ban-404` or `--ban-url`
## bugfixes
* #55 clients could hit the `--url-ban` filter when uploading over webdav
* fixed by limiting `--ban-404` and `--ban-url` to accounts with permission `g`, `G`, or `h`
* fixed 20% performance drop in python 3.12 due to utcfromtimestamp deprecation
* but 3.12.0 is still 5% slower than 3.11.6 for some reason
* volume listing on startup would display some redundant info
## other changes
* timeout for unfinished uploads increased from 6 to 24 hours
* and is now configurable with `--snap-drop`
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-1015-2006 `v1.9.12` more buttons # 2023-1015-2006 `v1.9.12` more buttons

View File

@@ -106,20 +106,19 @@ def meichk():
if filt not in sys.executable: if filt not in sys.executable:
filt = os.path.basename(sys.executable) filt = os.path.basename(sys.executable)
pids = [] hits = []
ptn = re.compile(r"^([^\s]+)\s+([0-9]+)")
try: try:
procs = sp.check_output("tasklist").decode("utf-8", "replace") cmd = "tasklist /fo csv".split(" ")
procs = sp.check_output(cmd).decode("utf-8", "replace")
except: except:
procs = "" # winpe procs = "" # winpe
for ln in procs.splitlines(): for ln in procs.split("\n"):
m = ptn.match(ln) if filt in ln.split('"')[:2][-1]:
if m and filt in m.group(1).lower(): hits.append(ln)
pids.append(int(m.group(2)))
mod = os.path.dirname(os.path.realpath(__file__)) mod = os.path.dirname(os.path.realpath(__file__))
if os.path.basename(mod).startswith("_MEI") and len(pids) == 2: if os.path.basename(mod).startswith("_MEI") and len(hits) == 2:
meicln(mod) meicln(mod)

26
srv/expand/README.md Normal file
View File

@@ -0,0 +1,26 @@
## text expansion
enable expansion of placeholder variables in `README.md` and prologue/epilogue files with `--exp` and customize the list of allowed placeholders to expand using `--exp-md` and `--exp-lg`
| explanation | placeholder |
| -------------------- | -------------------- |
| your ip address | {{self.ip}} |
| your user-agent | {{self.ua}} |
| your username | {{self.uname}} |
| the `Host` you see | {{self.host}} |
| server unix time | {{srv.itime}} |
| server datetime | {{srv.htime}} |
| server name | {{cfg.name}} |
| logout after | {{cfg.logout}} hours |
| vol reindex interval | {{vf.scan}} |
| thumbnail size | {{vf.thsize}} |
| your country | {{hdr.cf_ipcountry}} |
placeholders starting with...
* `self.` are grabbed from copyparty's internal state; anything in `httpcli.py` is fair game
* `cfg.` are the global server settings
* `vf.` are the volflags of the current volume
* `hdr.` are grabbed from the client headers; any header is supported, just add it (in lowercase) to the allowlist
* `srv.` are processed inside the `_expand` function in httpcli
for example (bad example), `hdr_cf_ipcountry` maps to the header `CF-IPCountry` (which is generated by cloudflare before the request is passed on to your server / copyparty)

View File

@@ -109,7 +109,7 @@ class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None): def __init__(self, a=None, v=None, c=None):
ka = {} ka = {}
ex = "daw dav_auth dav_inf dav_mac dav_rt dotsrch e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw rand smb th_no_crop vague_403 vc ver xdev xlink xvol" ex = "daw dav_auth dav_inf dav_mac dav_rt dotsrch e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp exp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw rand smb th_no_crop vague_403 vc ver xdev xlink xvol"
ka.update(**{k: False for k in ex.split()}) ka.update(**{k: False for k in ex.split()})
ex = "dotpart no_rescan no_sendfile no_voldump plain_ip" ex = "dotpart no_rescan no_sendfile no_voldump plain_ip"
@@ -130,6 +130,9 @@ class Cfg(Namespace):
ex = "on403 on404 xad xar xau xban xbd xbr xbu xiu xm" ex = "on403 on404 xad xar xau xban xbd xbr xbu xiu xm"
ka.update(**{k: [] for k in ex.split()}) ka.update(**{k: [] for k in ex.split()})
ex = "exp_lg exp_md"
ka.update(**{k: {} for k in ex.split()})
super(Cfg, self).__init__( super(Cfg, self).__init__(
a=a or [], a=a or [],
v=v or [], v=v or [],