diff --git a/README.md b/README.md index 6f7951a1..4ba6b60b 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ turn almost any device into a file server with resumable uploads/downloads using * [sfx](#sfx) - the self-contained "binary" * [copyparty.exe](#copypartyexe) - download [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) (win8+) or [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) (win7+) * [install on android](#install-on-android) -* [reporting bugs](#reporting-bugs) - ideas for context to include in bug reports +* [reporting bugs](#reporting-bugs) - ideas for context to include, and where to submit them * [devnotes](#devnotes) - for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md) @@ -286,6 +286,9 @@ roughly sorted by chance of encounter * cannot index non-ascii filenames with `-e2d` * cannot handle filenames with mojibake +if you have a new exciting bug to share, see [reporting bugs](#reporting-bugs) + + ## not my bugs same order here too @@ -341,9 +344,18 @@ upgrade notes * yes, using the [`g` permission](#accounts-and-volumes), see the examples there * you can also do this with linux filesystem permissions; `chmod 111 music` will make it possible to access files and folders inside the `music` folder but not list the immediate contents -- also works with other software, not just copyparty +* can I link someone to a password-protected volume/file by including the password in the URL? + * yes, by adding `?pw=hunter2` to the end; replace `?` with `&` if there are parameters in the URL already, meaning it contains a `?` near the end + +* how do I stop `.hist` folders from appearing everywhere on my HDD? + * by default, a `.hist` folder is created inside each volume for the filesystem index, thumbnails, audio transcodes, and markdown document history. Use the `--hist` global-option or the `hist` volflag to move it somewhere else; see [database location](#database-location) + * can I make copyparty download a file to my server if I give it a URL? * yes, using [hooks](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/wget.py) +* firefox refuses to connect over https, saying "Secure Connection Failed" or "SEC_ERROR_BAD_SIGNATURE", but the usual button to "Accept the Risk and Continue" is not shown + * firefox has corrupted its certstore; fix this by exiting firefox, then find and delete the file named `cert9.db` somewhere in your firefox profile folder + * i want to learn python and/or programming and am considering looking at the copyparty source code in that occasion * ```bash _| _ __ _ _|_ @@ -1292,6 +1304,8 @@ the classname of the HTML tag is set according to the selected theme, which is u see the top of [./copyparty/web/browser.css](./copyparty/web/browser.css) where the color variables are set, and there's layout-specific stuff near the bottom +if you want to change the fonts, see [./docs/rice/](./docs/rice/) + ## complete examples @@ -1943,7 +1957,12 @@ if you want thumbnails (photos+videos) and you're okay with spending another 132 # reporting bugs -ideas for context to include in bug reports +ideas for context to include, and where to submit them + +please get in touch using any of the following URLs: +* https://github.com/9001/copyparty/ **(primary)** +* https://gitlab.com/9001/copyparty/ *(mirror)* +* https://codeberg.org/9001/copyparty *(mirror)* in general, commandline arguments (and config file if any) @@ -1958,3 +1977,6 @@ if there's a wall of base64 in the log (thread stacks) then please include that, # devnotes for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md) + +see [./docs/TODO.md](./docs/TODO.md) for planned features / fixes / changes + diff --git a/contrib/README.md b/contrib/README.md index 1dab4824..01a175f2 100644 --- a/contrib/README.md +++ b/contrib/README.md @@ -16,6 +16,8 @@ * sharex config file to upload screenshots and grab the URL * `RequestURL`: full URL to the target folder * `pw`: password (remove the `pw` line if anon-write) +* the `act:bput` thing is optional since copyparty v1.9.29 +* using an older sharex version, maybe sharex v12.1.1 for example? dw fam i got your back 👉😎👉 [`sharex12.sxcu`](sharex12.sxcu) ### [`send-to-cpp.contextlet.json`](send-to-cpp.contextlet.json) * browser integration, kind of? custom rightclick actions and stuff diff --git a/contrib/sharex12.sxcu b/contrib/sharex12.sxcu new file mode 100644 index 00000000..297eed50 --- /dev/null +++ b/contrib/sharex12.sxcu @@ -0,0 +1,13 @@ +{ + "Name": "copyparty", + "DestinationType": "ImageUploader, TextUploader, FileUploader", + "RequestURL": "http://127.0.0.1:3923/sharex", + "FileFormName": "f", + "Arguments": { + "act": "bput" + }, + "Headers": { + "accept": "url", + "pw": "PUT_YOUR_PASSWORD_HERE_MY_DUDE" + } +} diff --git a/copyparty/__main__.py b/copyparty/__main__.py index f11672c1..64a36145 100755 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -395,7 +395,7 @@ def configure_ssl_ciphers(al: argparse.Namespace) -> None: def args_from_cfg(cfg_path: str) -> list[str]: lines: list[str] = [] - expand_config_file(lines, cfg_path, "") + expand_config_file(None, lines, cfg_path, "") lines = upgrade_cfg_fmt(None, argparse.Namespace(vc=False), lines, "") ret: list[str] = [] @@ -876,6 +876,7 @@ def add_upload(ap): ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless \033[33m-ed\033[0m") ap2.add_argument("--plain-ip", action="store_true", help="when avoiding filename collisions by appending the uploader's ip to the filename: append the plaintext ip instead of salting and hashing the ip") ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h") + ap2.add_argument("--u2abort", metavar="NUM", type=int, default=1, help="clients can abort incomplete uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=u2abort)") ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)") ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600") ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (bad idea to enable this on windows and/or cow filesystems)") @@ -1274,6 +1275,7 @@ def add_ui(ap, retry): ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)") 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("--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.md documents (volflags: no_sb_md | sb_md)") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index eba072f4..612d661c 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -991,7 +991,7 @@ class AuthSrv(object): ) -> None: self.line_ctr = 0 - expand_config_file(cfg_lines, fp, "") + expand_config_file(self.log, cfg_lines, fp, "") if self.args.vc: lns = ["{:4}: {}".format(n, s) for n, s in enumerate(cfg_lines, 1)] self.log("expanded config file (unprocessed):\n" + "\n".join(lns)) @@ -1745,7 +1745,7 @@ class AuthSrv(object): if k not in vol.flags: vol.flags[k] = getattr(self.args, k) - for k in ("nrand",): + for k in ("nrand", "u2abort"): if k in vol.flags: vol.flags[k] = int(vol.flags[k]) @@ -2367,27 +2367,50 @@ def split_cfg_ln(ln: str) -> dict[str, Any]: return ret -def expand_config_file(ret: list[str], fp: str, ipath: str) -> None: +def expand_config_file( + log: Optional["NamedLogger"], ret: list[str], fp: str, ipath: str +) -> None: """expand all % file includes""" fp = absreal(fp) if len(ipath.split(" -> ")) > 64: raise Exception("hit max depth of 64 includes") if os.path.isdir(fp): - names = os.listdir(fp) - crumb = "#\033[36m cfg files in {} => {}\033[0m".format(fp, names) - ret.append(crumb) - for fn in sorted(names): + names = list(sorted(os.listdir(fp))) + cnames = [x for x in names if x.lower().endswith(".conf")] + if not cnames: + t = "warning: tried to read config-files from folder '%s' but it does not contain any " + if names: + t += ".conf files; the following files were ignored: %s" + t = t % (fp, ", ".join(names[:8])) + else: + t += "files at all" + t = t % (fp,) + + if log: + log(t, 3) + + ret.append("#\033[33m %s\033[0m" % (t,)) + else: + zs = "#\033[36m cfg files in %s => %s\033[0m" % (fp, cnames) + ret.append(zs) + + for fn in cnames: fp2 = os.path.join(fp, fn) - if not fp2.endswith(".conf") or fp2 in ipath: + if fp2 in ipath: continue - expand_config_file(ret, fp2, ipath) + expand_config_file(log, ret, fp2, ipath) - if ret[-1] == crumb: - # no config files below; remove breadcrumb - ret.pop() + return + if not os.path.exists(fp): + t = "warning: tried to read config from '%s' but the file/folder does not exist" + t = t % (fp,) + if log: + log(t, 3) + + ret.append("#\033[31m %s\033[0m" % (t,)) return ipath += " -> " + fp @@ -2401,7 +2424,7 @@ def expand_config_file(ret: list[str], fp: str, ipath: str) -> None: fp2 = ln[1:].strip() fp2 = os.path.join(os.path.dirname(fp), fp2) ofs = len(ret) - expand_config_file(ret, fp2, ipath) + expand_config_file(log, ret, fp2, ipath) for n in range(ofs, len(ret)): ret[n] = pad + ret[n] continue diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 10e921e8..9781979f 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -66,6 +66,7 @@ def vf_vmap() -> dict[str, str]: "rm_retry", "sort", "unlist", + "u2abort", "u2ts", ): ret[k] = k @@ -116,6 +117,7 @@ flagcats = { "hardlink": "does dedup with hardlinks instead of symlinks", "neversymlink": "disables symlink fallback; full copy instead", "copydupes": "disables dedup, always saves full copies of dupes", + "sparse": "force use of sparse files, mainly for s3-backed storage", "daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files", "nosub": "forces all uploads into the top folder of the vfs", "magic": "enables filetype detection for nameless uploads", @@ -130,6 +132,7 @@ flagcats = { "rand": "force randomized filenames, 9 chars long by default", "nrand=N": "randomized filenames are N chars long", "u2ts=fc": "[f]orce [c]lient-last-modified or [u]pload-time", + "u2abort=1": "allow aborting unfinished uploads? 0=no 1=strict 2=ip-chk 3=acct-chk", "sz=1k-3m": "allow filesizes between 1 KiB and 3MiB", "df=1g": "ensure 1 GiB free disk space", }, diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index 4d72c4b1..4d9c4879 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -300,7 +300,7 @@ class FtpFs(AbstractedFS): vp = join(self.cwd, path).lstrip("/") try: - self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [], False) + self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [], False, False) except Exception as ex: raise FSE(str(ex)) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index bc18a3db..0c0c3a0a 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -319,7 +319,9 @@ class HttpCli(object): if self.args.xff_re and not self.args.xff_re.match(pip): t = 'got header "%s" from untrusted source "%s" claiming the true client ip is "%s" (raw value: "%s"); if you trust this, you must allowlist this proxy with "--xff-src=%s"' if self.headers.get("cf-connecting-ip"): - t += " Alternatively, if you are behind cloudflare, it is better to specify these two instead: --xff-hdr=cf-connecting-ip --xff-src=any" + t += ' Note: if you are behind cloudflare, then this default header is not a good choice; please first make sure your local reverse-proxy (if any) does not allow non-cloudflare IPs from providing cf-* headers, and then add this additional global setting: "--xff-hdr=cf-connecting-ip"' + else: + t += ' Note: depending on your reverse-proxy, and/or WAF, and/or other intermediates, you may want to read the true client IP from another header by also specifying "--xff-hdr=SomeOtherHeader"' zs = ( ".".join(pip.split(".")[:2]) + "." if "." in pip @@ -529,9 +531,13 @@ class HttpCli(object): return self.handle_options() and self.keepalive if not cors_k: + host = self.headers.get("host", "") origin = self.headers.get("origin", "") - self.log("cors-reject {} from {}".format(self.mode, origin), 3) - raise Pebkac(403, "no surfing") + proto = "https://" if self.is_https else "http://" + guess = "modifying" if (origin and host) else "stripping" + t = "cors-reject %s because request-header Origin='%s' does not match request-protocol '%s' and host '%s' based on request-header Host='%s' (note: if this request is not malicious, check if your reverse-proxy is accidentally %s request headers, in particular 'Origin', for example by running copyparty with --ihead='*' to show all request headers)" + self.log(t % (self.mode, origin, proto, self.host, host, guess), 3) + raise Pebkac(403, "rejected by cors-check") # getattr(self.mode) is not yet faster than this if self.mode == "POST": @@ -662,7 +668,11 @@ class HttpCli(object): def k304(self) -> bool: k304 = self.cookies.get("k304") - return k304 == "y" or ("; Trident/" in self.ua and not k304) + return ( + k304 == "y" + or (self.args.k304 == 2 and k304 != "n") + or ("; Trident/" in self.ua and not k304) + ) def send_headers( self, @@ -2838,11 +2848,11 @@ class HttpCli(object): logtail = "" # - # if request is for foo.js, check if we have foo.js.{gz,br} + # if request is for foo.js, check if we have foo.js.gz file_ts = 0.0 editions: dict[str, tuple[str, int]] = {} - for ext in ["", ".gz", ".br"]: + for ext in ("", ".gz"): try: fs_path = req_path + ext st = bos.stat(fs_path) @@ -2887,12 +2897,7 @@ class HttpCli(object): x.strip() for x in self.headers.get("accept-encoding", "").lower().split(",") ] - if ".br" in editions and "br" in supported_editions: - is_compressed = True - selected_edition = ".br" - fs_path, file_sz = editions[".br"] - self.out_headers["Content-Encoding"] = "br" - elif ".gz" in editions: + if ".gz" in editions: is_compressed = True selected_edition = ".gz" fs_path, file_sz = editions[".gz"] @@ -2908,13 +2913,8 @@ class HttpCli(object): is_compressed = False selected_edition = "plain" - try: - fs_path, file_sz = editions[selected_edition] - logmsg += "{} ".format(selected_edition.lstrip(".")) - except: - # client is old and we only have .br - # (could make brotli a dep to fix this but it's not worth) - raise Pebkac(404) + fs_path, file_sz = editions[selected_edition] + logmsg += "{} ".format(selected_edition.lstrip(".")) # # partial @@ -3369,6 +3369,7 @@ class HttpCli(object): dbwt=vs["dbwt"], url_suf=suf, k304=self.k304(), + k304vis=self.args.k304 > 0, ver=S_VERSION if self.args.ver else "", ahttps="" if self.is_https else "https://" + self.host + self.req, ) @@ -3377,7 +3378,7 @@ class HttpCli(object): def set_k304(self) -> bool: v = self.uparam["k304"].lower() - if v == "y": + if v in "yn": dur = 86400 * 299 else: dur = 0 @@ -3560,8 +3561,7 @@ class HttpCli(object): return ret def tx_ups(self) -> bool: - if not self.args.unpost: - raise Pebkac(403, "the unpost feature is disabled in server config") + have_unpost = self.args.unpost and "e2d" in self.vn.flags idx = self.conn.get_u2idx() if not idx or not hasattr(idx, "p_end"): @@ -3580,7 +3580,14 @@ class HttpCli(object): if "fk" in vol.flags and (self.uname in vol.axs.uread or self.uname in vol.axs.upget) } - for vol in self.asrv.vfs.all_vols.values(): + + x = self.conn.hsrv.broker.ask( + "up2k.get_unfinished_by_user", self.uname, self.ip + ) + uret = x.get() + + allvols = self.asrv.vfs.all_vols if have_unpost else {} + for vol in allvols.values(): cur = idx.get_cur(vol.realpath) if not cur: continue @@ -3632,9 +3639,13 @@ class HttpCli(object): for v in ret: v["vp"] = self.args.SR + v["vp"] - jtxt = json.dumps(ret, indent=2, sort_keys=True).encode("utf-8", "replace") - self.log("{} #{} {:.2f}sec".format(lm, len(ret), time.time() - t0)) - self.reply(jtxt, mime="application/json") + if not have_unpost: + ret = [{"kinshi": 1}] + + jtxt = '{"u":%s,"c":%s}' % (uret, json.dumps(ret, indent=0)) + zi = len(uret.split('\n"pd":')) - 1 + self.log("%s #%d+%d %.2fsec" % (lm, zi, len(ret), time.time() - t0)) + self.reply(jtxt.encode("utf-8", "replace"), mime="application/json") return True def handle_rm(self, req: list[str]) -> bool: @@ -3649,11 +3660,12 @@ class HttpCli(object): elif self.is_vproxied: req = [x[len(self.args.SR) :] for x in req] + unpost = "unpost" in self.uparam nlim = int(self.uparam.get("lim") or 0) lim = [nlim, nlim] if nlim else [] x = self.conn.hsrv.broker.ask( - "up2k.handle_rm", self.uname, self.ip, req, lim, False + "up2k.handle_rm", self.uname, self.ip, req, lim, False, unpost ) self.loud_reply(x.get()) return True diff --git a/copyparty/httpsrv.py b/copyparty/httpsrv.py index 40bd108f..cade2df7 100644 --- a/copyparty/httpsrv.py +++ b/copyparty/httpsrv.py @@ -191,7 +191,7 @@ class HttpSrv(object): for fn in df: ap = absreal(os.path.join(dp, fn)) self.statics.add(ap) - if ap.endswith(".gz") or ap.endswith(".br"): + if ap.endswith(".gz"): self.statics.add(ap[:-3]) def set_netdevs(self, netdevs: dict[str, Netdev]) -> None: diff --git a/copyparty/metrics.py b/copyparty/metrics.py index 72e86fdb..3af8be9d 100644 --- a/copyparty/metrics.py +++ b/copyparty/metrics.py @@ -206,6 +206,9 @@ class Metrics(object): try: x = self.hsrv.broker.ask("up2k.get_unfinished") xs = x.get() + if not xs: + raise Exception("up2k mutex acquisition timed out") + xj = json.loads(xs) for ptop, (nbytes, nfiles) in xj.items(): tnbytes += nbytes diff --git a/copyparty/smbd.py b/copyparty/smbd.py index 1e97386d..979c11df 100644 --- a/copyparty/smbd.py +++ b/copyparty/smbd.py @@ -340,7 +340,7 @@ class SMB(object): yeet("blocked delete (no-del-acc): " + vpath) vpath = vpath.replace("\\", "/").lstrip("/") - self.hub.up2k.handle_rm(uname, "1.7.6.2", [vpath], [], False) + self.hub.up2k.handle_rm(uname, "1.7.6.2", [vpath], [], False, False) def _utime(self, vpath: str, times: tuple[float, float]) -> None: if not self.args.smbw: diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 08335ad2..80ead14c 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -28,7 +28,7 @@ if True: # pylint: disable=using-constant-test import typing from typing import Any, Optional, Union -from .__init__ import ANYWIN, EXE, MACOS, TYPE_CHECKING, EnvParams, unicode +from .__init__ import ANYWIN, E, EXE, MACOS, TYPE_CHECKING, EnvParams, unicode from .authsrv import BAD_CFG, AuthSrv from .cert import ensure_cert from .mtag import HAVE_FFMPEG, HAVE_FFPROBE @@ -94,7 +94,7 @@ class SvcHub(object): self.stopping = False self.stopped = False self.reload_req = False - self.reloading = False + self.reloading = 0 self.stop_cond = threading.Condition() self.nsigs = 3 self.retcode = 0 @@ -154,6 +154,8 @@ class SvcHub(object): lg.handlers = [lh] lg.setLevel(logging.DEBUG) + self._check_env() + if args.stackmon: start_stackmon(args.stackmon, 0) @@ -385,6 +387,17 @@ class SvcHub(object): Daemon(self.sd_notify, "sd-notify") + def _check_env(self) -> None: + try: + files = os.listdir(E.cfg) + except: + files = [] + + hits = [x for x in files if x.lower().endswith(".conf")] + if hits: + t = "WARNING: found config files in [%s]: %s\n config files are not expected here, and will NOT be loaded (unless your setup is intentionally hella funky)" + self.log("root", t % (E.cfg, ", ".join(hits)), 3) + def _process_config(self) -> bool: al = self.args @@ -674,23 +687,24 @@ class SvcHub(object): self.log("root", "ssdp startup failed;\n" + min_ex(), 3) def reload(self) -> str: - if self.reloading: - return "cannot reload; already in progress" + with self.up2k.mutex: + if self.reloading: + return "cannot reload; already in progress" + self.reloading = 1 - self.reloading = True Daemon(self._reload, "reloading") return "reload initiated" def _reload(self, rescan_all_vols: bool = True) -> None: - self.reloading = True - self.log("root", "reload scheduled") with self.up2k.mutex: - self.reloading = True + if self.reloading != 1: + return + self.reloading = 2 + self.log("root", "reloading config") self.asrv.reload() self.up2k.reload(rescan_all_vols) self.broker.reload() - - self.reloading = False + self.reloading = 0 def stop_thr(self) -> None: while not self.stop_req: diff --git a/copyparty/tftpd.py b/copyparty/tftpd.py index 0020e96a..7b09533a 100644 --- a/copyparty/tftpd.py +++ b/copyparty/tftpd.py @@ -360,7 +360,7 @@ class Tftpd(object): yeet("attempted delete of non-empty file") vpath = vpath.replace("\\", "/").lstrip("/") - self.hub.up2k.handle_rm("*", "8.3.8.7", [vpath], [], False) + self.hub.up2k.handle_rm("*", "8.3.8.7", [vpath], [], False, False) def _access(self, *a: Any) -> bool: return True diff --git a/copyparty/up2k.py b/copyparty/up2k.py index c154431c..1418e36d 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -200,15 +200,15 @@ class Up2k(object): Daemon(self.deferred_init, "up2k-deferred-init") def reload(self, rescan_all_vols: bool) -> None: - self.gid += 1 - self.log("reload #{} initiated".format(self.gid)) + """mutex me""" + self.log("reload #{} scheduled".format(self.gid + 1)) all_vols = self.asrv.vfs.all_vols scan_vols = [k for k, v in all_vols.items() if v.realpath not in self.registry] if rescan_all_vols: scan_vols = list(all_vols.keys()) - self.rescan(all_vols, scan_vols, True, False) + self._rescan(all_vols, scan_vols, True, False) def deferred_init(self) -> None: all_vols = self.asrv.vfs.all_vols @@ -237,7 +237,7 @@ class Up2k(object): for n in range(max(1, self.args.mtag_mt)): Daemon(self._tagger, "tagger-{}".format(n)) - Daemon(self._run_all_mtp, "up2k-mtp-init") + Daemon(self._run_all_mtp, "up2k-mtp-init", (self.gid,)) def log(self, msg: str, c: Union[int, str] = 0) -> None: if self.pp: @@ -287,9 +287,48 @@ class Up2k(object): } return json.dumps(ret, indent=4) + def get_unfinished_by_user(self, uname, ip) -> str: + if PY2 or not self.mutex.acquire(timeout=2): + return '[{"timeout":1}]' + + ret: list[tuple[int, str, int, int, int]] = [] + try: + for ptop, tab2 in self.registry.items(): + cfg = self.flags.get(ptop, {}).get("u2abort", 1) + if not cfg: + continue + addr = (ip or "\n") if cfg in (1, 2) else "" + user = (uname or "\n") if cfg in (1, 3) else "" + drp = self.droppable.get(ptop, {}) + for wark, job in tab2.items(): + if ( + wark in drp + or (user and user != job["user"]) + or (addr and addr != job["addr"]) + ): + continue + + zt5 = ( + int(job["t0"]), + djoin(job["vtop"], job["prel"], job["name"]), + job["size"], + len(job["need"]), + len(job["hash"]), + ) + ret.append(zt5) + finally: + self.mutex.release() + + ret.sort(reverse=True) + ret2 = [ + {"at": at, "vp": "/" + vp, "pd": 100 - ((nn * 100) // (nh or 1)), "sz": sz} + for (at, vp, sz, nn, nh) in ret + ] + return json.dumps(ret2, indent=0) + def get_unfinished(self) -> str: if PY2 or not self.mutex.acquire(timeout=0.5): - return "{}" + return "" ret: dict[str, tuple[int, int]] = {} try: @@ -342,14 +381,21 @@ class Up2k(object): def rescan( self, all_vols: dict[str, VFS], scan_vols: list[str], wait: bool, fscan: bool ) -> str: + with self.mutex: + return self._rescan(all_vols, scan_vols, wait, fscan) + + def _rescan( + self, all_vols: dict[str, VFS], scan_vols: list[str], wait: bool, fscan: bool + ) -> str: + """mutex me""" if not wait and self.pp: return "cannot initiate; scan is already in progress" - args = (all_vols, scan_vols, fscan) + self.gid += 1 Daemon( self.init_indexes, "up2k-rescan-{}".format(scan_vols[0] if scan_vols else "all"), - args, + (all_vols, scan_vols, fscan, self.gid), ) return "" @@ -461,7 +507,7 @@ class Up2k(object): if vp: fvp = "%s/%s" % (vp, fvp) - self._handle_rm(LEELOO_DALLAS, "", fvp, [], True) + self._handle_rm(LEELOO_DALLAS, "", fvp, [], True, False) nrm += 1 if nrm: @@ -580,19 +626,32 @@ class Up2k(object): return True, ret def init_indexes( - self, all_vols: dict[str, VFS], scan_vols: list[str], fscan: bool + self, all_vols: dict[str, VFS], scan_vols: list[str], fscan: bool, gid: int = 0 ) -> bool: - gid = self.gid - while self.pp and gid == self.gid: - time.sleep(0.1) + if not gid: + with self.mutex: + gid = self.gid - if gid != self.gid: - return False + nspin = 0 + while True: + nspin += 1 + if nspin > 1: + time.sleep(0.1) + + with self.mutex: + if gid != self.gid: + return False + + if self.pp: + continue + + self.pp = ProgressPrinter(self.log, self.args) + + break if gid: - self.log("reload #{} running".format(self.gid)) + self.log("reload #%d running" % (gid,)) - self.pp = ProgressPrinter(self.log, self.args) vols = list(all_vols.values()) t0 = time.time() have_e2d = False @@ -780,7 +839,7 @@ class Up2k(object): if self.mtag: t = "online (running mtp)" if scan_vols: - thr = Daemon(self._run_all_mtp, "up2k-mtp-scan", r=False) + thr = Daemon(self._run_all_mtp, "up2k-mtp-scan", (gid,), r=False) else: self.pp = None t = "online, idle" @@ -1814,8 +1873,7 @@ class Up2k(object): self.pending_tags = [] return ret - def _run_all_mtp(self) -> None: - gid = self.gid + def _run_all_mtp(self, gid: int) -> None: t0 = time.time() for ptop, flags in self.flags.items(): if "mtp" in flags: @@ -2676,6 +2734,9 @@ class Up2k(object): a = [job[x] for x in zs.split()] self.db_add(cur, vfs.flags, *a) cur.connection.commit() + elif wark in reg: + # checks out, but client may have hopped IPs + job["addr"] = cj["addr"] if not job: ap1 = djoin(cj["ptop"], cj["prel"]) @@ -3212,7 +3273,13 @@ class Up2k(object): pass def handle_rm( - self, uname: str, ip: str, vpaths: list[str], lim: list[int], rm_up: bool + self, + uname: str, + ip: str, + vpaths: list[str], + lim: list[int], + rm_up: bool, + unpost: bool, ) -> str: n_files = 0 ok = {} @@ -3222,7 +3289,7 @@ class Up2k(object): self.log("hit delete limit of {} files".format(lim[1]), 3) break - a, b, c = self._handle_rm(uname, ip, vp, lim, rm_up) + a, b, c = self._handle_rm(uname, ip, vp, lim, rm_up, unpost) n_files += a for k in b: ok[k] = 1 @@ -3236,25 +3303,43 @@ class Up2k(object): return "deleted {} files (and {}/{} folders)".format(n_files, iok, iok + ing) def _handle_rm( - self, uname: str, ip: str, vpath: str, lim: list[int], rm_up: bool + self, uname: str, ip: str, vpath: str, lim: list[int], rm_up: bool, unpost: bool ) -> tuple[int, list[str], list[str]]: self.db_act = time.time() - try: + 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) - unpost = False - except: + else: # unpost with missing permissions? verify with db - if not self.args.unpost: - raise Pebkac(400, "the unpost feature is disabled in server config") - - unpost = True permsets = [[False, True]] vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0]) vn, rem = vn.get_dbv(rem) + ptop = vn.realpath with self.mutex: - _, _, _, _, dip, dat = self._find_from_vpath(vn.realpath, rem) + abrt_cfg = self.flags.get(ptop, {}).get("u2abort", 1) + addr = (ip or "\n") if abrt_cfg in (1, 2) else "" + user = (uname or "\n") if abrt_cfg in (1, 3) else "" + reg = self.registry.get(ptop, {}) if abrt_cfg else {} + for wark, job in reg.items(): + if (user and user != job["user"]) or (addr and addr != job["addr"]): + continue + if djoin(job["prel"], job["name"]) == rem: + if job["ptop"] != ptop: + t = "job.ptop [%s] != vol.ptop [%s] ??" + raise Exception(t % (job["ptop"] != ptop)) + partial = vn.canonical(vjoin(job["prel"], job["tnam"])) + break + if partial: + dip = ip + dat = time.time() + else: + if not self.args.unpost: + t = "the unpost feature is disabled in server config" + raise Pebkac(400, t) + + _, _, _, _, dip, dat = self._find_from_vpath(ptop, rem) t = "you cannot delete this: " if not dip: @@ -3347,6 +3432,9 @@ class Up2k(object): cur.connection.commit() wunlink(self.log, abspath, dbv.flags) + if partial: + wunlink(self.log, partial, dbv.flags) + partial = "" if xad: runhook( self.log, @@ -3942,7 +4030,13 @@ class Up2k(object): if not ANYWIN and sprs and sz > 1024 * 1024: fs = self.fstab.get(pdir) - if fs != "ok": + if fs == "ok": + pass + elif "sparse" in self.flags[job["ptop"]]: + t = "volflag 'sparse' is forcing use of sparse files for uploads to [%s]" + self.log(t % (job["ptop"],)) + relabel = True + else: relabel = True f.seek(1024 * 1024 - 1) f.write(b"e") diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index ca31c7a3..c1e8595f 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -494,6 +494,7 @@ html.dz { text-shadow: none; font-family: 'scp', monospace, monospace; + font-family: var(--font-mono), 'scp', monospace, monospace; } html.dy { --fg: #000; @@ -603,6 +604,7 @@ html { color: var(--fg); background: var(--bgg); font-family: sans-serif; + font-family: var(--font-main), sans-serif; text-shadow: 1px 1px 0px var(--bg-max); } html, body { @@ -611,6 +613,7 @@ html, body { } pre, code, tt, #doc, #doc>code { font-family: 'scp', monospace, monospace; + font-family: var(--font-mono), 'scp', monospace, monospace; } .ayjump { position: fixed; @@ -759,6 +762,7 @@ html #files.hhpick thead th { } #files tbody td:nth-child(3) { font-family: 'scp', monospace, monospace; + font-family: var(--font-mono), 'scp', monospace, monospace; text-align: right; padding-right: 1em; white-space: nowrap; @@ -821,6 +825,7 @@ html.y #path a:hover { .logue.raw { white-space: pre; font-family: 'scp', 'consolas', monospace; + font-family: var(--font-mono), 'scp', 'consolas', monospace; } #doc>iframe, .logue>iframe { @@ -1417,6 +1422,7 @@ input[type="checkbox"]:checked+label { } html.dz input { font-family: 'scp', monospace, monospace; + font-family: var(--font-mono), 'scp', monospace, monospace; } .opwide div>span>input+label { padding: .3em 0 .3em .3em; @@ -1702,6 +1708,7 @@ html.y #tree.nowrap .ntree a+a:hover { } .ntree a:first-child { font-family: 'scp', monospace, monospace; + font-family: var(--font-mono), 'scp', monospace, monospace; font-size: 1.2em; line-height: 0; } @@ -1832,6 +1839,10 @@ html.y #tree.nowrap .ntree a+a:hover { margin: 0; padding: 0; } +#unpost td:nth-child(3), +#unpost td:nth-child(4) { + text-align: right; +} #rui { background: #fff; background: var(--bg); @@ -1859,6 +1870,7 @@ html.y #tree.nowrap .ntree a+a:hover { } #rn_vadv input { font-family: 'scp', monospace, monospace; + font-family: var(--font-mono), 'scp', monospace, monospace; } #rui td+td, #rui td input[type="text"] { @@ -1922,6 +1934,7 @@ html.y #doc { #doc.mdo { white-space: normal; font-family: sans-serif; + font-family: var(--font-main), sans-serif; } #doc.prism * { line-height: 1.5em; @@ -1981,6 +1994,7 @@ a.btn, } #hkhelp td:first-child { font-family: 'scp', monospace, monospace; + font-family: var(--font-mono), 'scp', monospace, monospace; } html.noscroll, html.noscroll .sbar { @@ -2490,6 +2504,7 @@ html.y #bbox-overlay figcaption a { } #op_up2k.srch td.prog { font-family: sans-serif; + font-family: var(--font-main), sans-serif; font-size: 1em; width: auto; } @@ -2504,6 +2519,7 @@ html.y #bbox-overlay figcaption a { white-space: nowrap; display: inline-block; font-family: 'scp', monospace, monospace; + font-family: var(--font-mono), 'scp', monospace, monospace; } #u2etas.o { width: 20em; @@ -2573,6 +2589,7 @@ html.y #bbox-overlay figcaption a { #u2cards span { color: var(--fg-max); font-family: 'scp', monospace; + font-family: var(--font-mono), 'scp', monospace; } #u2cards > a:nth-child(4) > span { display: inline-block; @@ -2738,6 +2755,7 @@ html.b #u2conf a.b:hover { } .prog { font-family: 'scp', monospace, monospace; + font-family: var(--font-mono), 'scp', monospace, monospace; } #u2tab span.inf, #u2tab span.ok, diff --git a/copyparty/web/browser.html b/copyparty/web/browser.html index 0f48b211..76f6fcca 100644 --- a/copyparty/web/browser.html +++ b/copyparty/web/browser.html @@ -7,9 +7,9 @@ -{{ html_head }} +{{ html_head }} {%- if css %} {%- endif %} diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 03fb2339..5c9ec3bc 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -102,7 +102,7 @@ var Ls = { "access": " access", "ot_close": "close submenu", "ot_search": "search for files by attributes, path / name, music tags, or any combination of those$N$N<code>foo bar</code> = must contain both «foo» and «bar»,$N<code>foo -bar</code> = must contain «foo» but not «bar»,$N<code>^yana .opus$</code> = start with «yana» and be an «opus» file$N<code>"try unite"</code> = contain exactly «try unite»$N$Nthe date format is iso-8601, like$N<code>2009-12-31</code> or <code>2020-09-12 23:30:00</code>", - "ot_unpost": "unpost: delete your recent uploads", + "ot_unpost": "unpost: delete your recent uploads, or abort unfinished ones", "ot_bup": "bup: basic uploader, even supports netscape 4.0", "ot_mkdir": "mkdir: create a new directory", "ot_md": "new-md: create a new markdown document", @@ -240,13 +240,14 @@ var Ls = { "ml_drc": "dynamic range compressor", "mt_shuf": "shuffle the songs in each folder\">🔀", + "mt_aplay": "autoplay if there is a song-ID in the link you clicked to access the server$N$Ndisabling this will also stop the page URL from being updated with song-IDs when playing music, to prevent autoplay if these settings are lost but the URL remains\">a▶", "mt_preload": "start loading the next song near the end for gapless playback\">preload", "mt_prescan": "go to the next folder before the last song$Nends, keeping the webbrowser happy$Nso it doesn't stop the playback\">nav", "mt_fullpre": "try to preload the entire song;$N✅ enable on unreliable connections,$N❌ disable on slow connections probably\">full", "mt_waves": "waveform seekbar:$Nshow audio amplitude in the scrubber\">~s", "mt_npclip": "show buttons for clipboarding the currently playing song\">/np", "mt_octl": "os integration (media hotkeys / osd)\">os-ctl", - "mt_oseek": "allow seeking through os integration\">seek", + "mt_oseek": "allow seeking through os integration$N$Nnote: on some devices (iPhones),$Nthis replaces the next-song button\">seek", "mt_oscv": "show album cover in osd\">art", "mt_follow": "keep the playing track scrolled into view\">🎯", "mt_compact": "compact controls\">⟎", @@ -388,6 +389,8 @@ var Ls = { "md_eshow": "cannot render ", "md_off": "[📜readme] disabled in [⚙] -- document hidden", + "badreply": "Failed to parse reply from server", + "xhr403": "403: Access denied\n\ntry pressing F5, maybe you got logged out", "cf_ok": "sorry about that -- DD" + wah + "oS protection kicked in\n\nthings should resume in about 30 sec\n\nif nothing happens, hit F5 to reload the page", "tl_xe1": "could not list subfolders:\n\nerror ", @@ -409,7 +412,7 @@ var Ls = { "fz_zipd": "zip with traditional cp437 filenames, for really old software", "fz_zipc": "cp437 with crc32 computed early,$Nfor MS-DOS PKZIP v2.04g (october 1993)$N(takes longer to process before download can start)", - "un_m1": "you can delete your recent uploads below", + "un_m1": "you can delete your recent uploads (or abort unfinished ones) below", "un_upd": "refresh", "un_m4": "or share the files visible below:", "un_ulist": "show", @@ -418,12 +421,15 @@ var Ls = { "un_fclr": "clear filter", "un_derr": 'unpost-delete failed:\n', "un_f5": 'something broke, please try a refresh or hit F5', + "un_nou": 'warning: server too busy to show unfinished uploads; click the "refresh" link in a bit', + "un_noc": 'warning: unpost of fully uploaded files is not enabled/permitted in server config', "un_max": "showing first 2000 files (use the filter)", - "un_avail": "{0} uploads can be deleted", - "un_m2": "sorted by upload time – most recent first:", + "un_avail": "{0} recent uploads can be deleted
{1} unfinished ones can be aborted", + "un_m2": "sorted by upload time; most recent first:", "un_no1": "sike! no uploads are sufficiently recent", "un_no2": "sike! no uploads matching that filter are sufficiently recent", "un_next": "delete the next {0} files below", + "un_abrt": "abort", "un_del": "delete", "un_m3": "loading your recent uploads...", "un_busy": "deleting {0} files...", @@ -737,13 +743,14 @@ var Ls = { "ml_drc": "compressor (volum-utjevning)", "mt_shuf": "sangene i hver mappe$Nspilles i tilfeldig rekkefĂžlge\">🔀", + "mt_aplay": "forsĂžk Ă„ starte avspilling hvis linken du klikket pĂ„ for Ă„ Ă„pne nettsiden inneholder en sang-ID$N$Nhvis denne deaktiveres sĂ„ vil heller ikke nettside-URLen bli oppdatert med sang-ID'er nĂ„r musikk spilles, i tilfelle innstillingene skulle gĂ„ tapt og nettsiden lastes pĂ„ ny\">a▶", "mt_preload": "hent ned litt av neste sang i forkant,$Nslik at pausen i overgangen blir mindre\">forles", "mt_prescan": "ved behov, bla til neste mappe$Nslik at nettleseren lar oss$Nfortsette Ă„ spille musikk\">bla", "mt_fullpre": "hent ned hele neste sang, ikke bare litt:$N✅ skru pĂ„ hvis nettet ditt er ustabilt,$N❌ skru av hvis nettet ditt er tregt\">full", "mt_waves": "waveform seekbar:$Nvis volumkurve i avspillingsfeltet\">~s", "mt_npclip": "vis knapper for Ă„ kopiere info om sangen du hĂžrer pĂ„\">/np", "mt_octl": "integrering med operativsystemet (fjernkontroll, info-skjerm)\">os-ctl", - "mt_oseek": "tillat spoling med fjernkontroll\">spoling", + "mt_oseek": "tillat spoling med fjernkontroll$N$Nmerk: pĂ„ noen enheter (iPhones) sĂ„ vil$Ndette erstatte knappen for neste sang\">spoling", "mt_oscv": "vis album-cover pĂ„ infoskjermen\">bilde", "mt_follow": "bla slik at sangen som spilles alltid er synlig\">🎯", "mt_compact": "tettpakket avspillerpanel\">⟎", @@ -885,6 +892,8 @@ var Ls = { "md_eshow": "viser forenklet ", "md_off": "[📜readme] er avskrudd i [⚙] -- dokument skjult", + "badreply": "Ugyldig svar ifra serveren", + "xhr403": "403: Tilgang nektet\n\nkanskje du ble logget ut? prĂžv Ă„ trykk F5", "cf_ok": "beklager -- liten tilfeldig kontroll, alt OK\n\nting skal fortsette om ca. 30 sekunder\n\nhvis ikkeno skjer, trykk F5 for Ă„ laste siden pĂ„ nytt", "tl_xe1": "kunne ikke hente undermapper:\n\nfeil ", @@ -906,7 +915,7 @@ var Ls = { "fz_zipd": "zip med filnavn i cp437, for hĂžggamle maskiner", "fz_zipc": "cp437 med tidlig crc32,$Nfor MS-DOS PKZIP v2.04g (oktober 1993)$N(Ăžker behandlingstid pĂ„ server)", - "un_m1": "nedenfor kan du angre / slette filer som du nylig har lastet opp", + "un_m1": "nedenfor kan du angre / slette filer som du nylig har lastet opp, eller avbryte ufullstendige opplastninger", "un_upd": "oppdater", "un_m4": "eller hvis du vil dele nedlastnings-lenkene:", "un_ulist": "vis", @@ -915,12 +924,15 @@ var Ls = { "un_fclr": "nullstill filter", "un_derr": 'unpost-sletting feilet:\n', "un_f5": 'noe gikk galt, prĂžv Ă„ oppdatere listen eller trykk F5', + "un_nou": 'advarsel: kan ikke vise ufullstendige opplastninger akkurat nĂ„; klikk pĂ„ oppdater-linken om litt', + "un_noc": 'advarsel: angring av fullfĂžrte opplastninger er deaktivert i serverkonfigurasjonen', "un_max": "viser de fĂžrste 2000 filene (bruk filteret for Ă„ innsnevre)", - "un_avail": "{0} filer kan slettes", - "un_m2": "sortert etter opplastningstid – nyeste fĂžrst:", + "un_avail": "{0} nylig opplastede filer kan slettes
{1} ufullstendige opplastninger kan avbrytes", + "un_m2": "sortert etter opplastningstid; nyeste fÞrst:", "un_no1": "men nei, her var det jaggu ikkeno som slettes kan", "un_no2": "men nei, her var det jaggu ingenting som passet overens med filteret", "un_next": "slett de neste {0} filene nedenfor", + "un_abrt": "avbryt", "un_del": "slett", "un_m3": "henter listen med nylig opplastede filer...", "un_busy": "sletter {0} filer...", @@ -967,7 +979,7 @@ var Ls = { "u_emtleakf": 'prÞver fÞlgende:\n\nPS: Firefox fikser forhÄpentligvis feilen en eller annen gang', "u_s404": "ikke funnet pÄ serveren", "u_expl": "forklar", - "u_maxconn": "de fleste nettlesere tillater ikke mer enn 6, men firefox lar deg Þke grensen med connections-per-server in about:config", + "u_maxconn": "de fleste nettlesere tillater ikke mer enn 6, men firefox lar deg Þke grensen med connections-per-server i about:config", "u_tu": '

ADVARSEL: turbo er pĂ„,  avbrutte opplastninger vil muligens ikke oppdages og gjenopptas; hold musepekeren over turbo-knappen for mer info

', "u_ts": '

ADVARSEL: turbo er pĂ„,  sĂžkeresultater kan vĂŠre feil; hold musepekeren over turbo-knappen for mer info

', "u_turbo_c": "turbo er deaktivert i serverkonfigurasjonen", @@ -1024,7 +1036,7 @@ modal.load(); ebi('ops').innerHTML = ( '--' + '🔎' + - (have_del && have_unpost ? '🧯' : '') + + (have_del ? '🧯' : '') + '🚀' + '🎈' + '📂' + @@ -1401,6 +1413,7 @@ var mpl = (function () { ebi('op_player').innerHTML = ( '

' + L.cl_opts + '

' + ' 201) { - sf.textContent = 'error: ' + unpre(this.responseText); + sf.textContent = 'error: ' + hunpre(this.responseText); return; } @@ -7569,12 +7586,25 @@ var globalcss = (function () { var css = ds[b].cssText.split(/\burl\(/g); ret += css[0]; for (var c = 1; c < css.length; c++) { - var delim = (/^["']/.exec(css[c])) ? css[c].slice(0, 1) : ''; - ret += 'url(' + delim + ((css[c].slice(0, 8).indexOf('://') + 1 || css[c].startsWith('/')) ? '' : base) + - css[c].slice(delim ? 1 : 0); + var m = /(^ *["']?)(.*)/.exec(css[c]), + delim = m[1], + ctxt = m[2], + is_abs = /^\/|[^)/:]+:\/\//.exec(ctxt); + + ret += 'url(' + delim + (is_abs ? '' : base) + ctxt; } ret += '\n'; } + if (ret.indexOf('\n@import') + 1) { + var c0 = ret.split('\n'), + c1 = [], + c2 = []; + + for (var a = 0; a < c0.length; a++) + (c0[a].startsWith('@import') ? c1 : c2).push(c0[a]); + + ret = c1.concat(c2).join('\n'); + } } catch (ex) { console.log('could not read css', a, base); @@ -7858,15 +7888,39 @@ var unpost = (function () { if (!xhrchk(this, L.fu_xe1, L.fu_xe2)) return ebi('op_unpost').innerHTML = L.fu_xe1; - var res = JSON.parse(this.responseText); + try { + var ores = JSON.parse(this.responseText); + } + catch (ex) { + return ebi('op_unpost').innerHTML = '

' + L.badreply + ':

' + unpre(this.responseText); + } + + if (ores.u.length == 1 && ores.u[0].timeout) { + html.push('

' + L.un_nou + '

'); + ores.u = []; + } + + if (ores.c.length == 1 && ores.c[0].kinshi) { + html.push('

' + L.un_noc + '

'); + ores.c = []; + } + + for (var a = 0; a < ores.u.length; a++) + ores.u[a].k = 'u'; + + for (var a = 0; a < ores.c.length; a++) + ores.c[a].k = 'c'; + + var res = ores.u.concat(ores.c); + if (res.length) { if (res.length == 2000) html.push("

" + L.un_max); else - html.push("

" + L.un_avail.format(res.length)); + html.push("

" + L.un_avail.format(ores.c.length, ores.u.length)); - html.push(" – " + L.un_m2 + "

"); - html.push(""); + html.push("
" + L.un_m2 + "

"); + html.push("
timesizefile
"); } else html.push('-- ' + (filt.value ? L.un_no2 : L.un_no1) + ''); @@ -7879,10 +7933,13 @@ var unpost = (function () { ''); + + var done = res[a].k == 'c'; html.push( - '' + + '' + '' + - '' + + '' + + (done ? '' : '') + ''); } @@ -7968,7 +8025,7 @@ var unpost = (function () { var xhr = new XHR(); xhr.n = n; xhr.n2 = n2; - xhr.open('POST', SR + '/?delete&lim=' + req.length, true); + xhr.open('POST', SR + '/?delete&unpost&lim=' + req.length, true); xhr.onload = xhr.onerror = unpost_delete_cb; xhr.send(JSON.stringify(req)); }; diff --git a/copyparty/web/browser2.html b/copyparty/web/browser2.html index 66d4f0fc..03195b4e 100644 --- a/copyparty/web/browser2.html +++ b/copyparty/web/browser2.html @@ -6,12 +6,12 @@ {{ title }} -{{ html_head }} +{{ html_head }} diff --git a/copyparty/web/md.css b/copyparty/web/md.css index 0b1cd49c..e964865f 100644 --- a/copyparty/web/md.css +++ b/copyparty/web/md.css @@ -2,6 +2,7 @@ html, body { color: #333; background: #eee; font-family: sans-serif; + font-family: var(--font-main), sans-serif; line-height: 1.5em; } html.y #helpbox a { @@ -67,6 +68,7 @@ a { position: relative; display: inline-block; font-family: 'scp', monospace, monospace; + font-family: var(--font-mono), 'scp', monospace, monospace; font-weight: bold; font-size: 1.3em; line-height: .1em; diff --git a/copyparty/web/md.html b/copyparty/web/md.html index 07bd5634..1ea1909f 100644 --- a/copyparty/web/md.html +++ b/copyparty/web/md.html @@ -4,12 +4,12 @@ -{{ html_head }} {%- if edit %} {%- endif %} +{{ html_head }}
diff --git a/copyparty/web/md.js b/copyparty/web/md.js index 4dd29a37..d255b2c4 100644 --- a/copyparty/web/md.js +++ b/copyparty/web/md.js @@ -512,13 +512,6 @@ dom_navtgl.onclick = function () { redraw(); }; -if (!HTTPS && location.hostname != '127.0.0.1') try { - ebi('edit2').onclick = function (e) { - toast.err(0, "the fancy editor is only available over https"); - return ev(e); - } -} catch (ex) { } - if (sread('hidenav') == 1) dom_navtgl.onclick(); diff --git a/copyparty/web/md2.css b/copyparty/web/md2.css index f06d0bb8..ec4a7b29 100644 --- a/copyparty/web/md2.css +++ b/copyparty/web/md2.css @@ -9,7 +9,7 @@ width: calc(100% - 56em); } #mw { - left: calc(100% - 55em); + left: max(0em, calc(100% - 55em)); overflow-y: auto; position: fixed; bottom: 0; @@ -56,6 +56,7 @@ padding: 0; margin: 0; font-family: 'scp', monospace, monospace; + font-family: var(--font-mono), 'scp', monospace, monospace; white-space: pre-wrap; word-break: break-word; overflow-wrap: break-word; diff --git a/copyparty/web/md2.js b/copyparty/web/md2.js index dc2f702b..e54af570 100644 --- a/copyparty/web/md2.js +++ b/copyparty/web/md2.js @@ -368,14 +368,14 @@ function save(e) { function save_cb() { if (this.status !== 200) - return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^
/, ""));
+        return toast.err(0, 'Error!  The file was NOT saved.\n\nError ' + this.status + ":\n" + unpre(this.responseText));
 
     var r;
     try {
         r = JSON.parse(this.responseText);
     }
     catch (ex) {
-        return toast.err(0, 'Failed to parse reply from server:\n\n' + this.responseText);
+        return toast.err(0, 'Error!  The file was likely NOT saved.\n\nFailed to parse reply from server:\n\n' + unpre(this.responseText));
     }
 
     if (!r.ok) {
@@ -418,7 +418,7 @@ function run_savechk(lastmod, txt, btn, ntry) {
 
 function savechk_cb() {
     if (this.status !== 200)
-        return toast.err(0, 'Error!  The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^
/, ""));
+        return toast.err(0, 'Error!  The file was NOT saved.\n\nError ' + this.status + ":\n" + unpre(this.responseText));
 
     var doc1 = this.txt.replace(/\r\n/g, "\n");
     var doc2 = this.responseText.replace(/\r\n/g, "\n");
diff --git a/copyparty/web/mde.css b/copyparty/web/mde.css
index 7b1d2618..0a8d8ec5 100644
--- a/copyparty/web/mde.css
+++ b/copyparty/web/mde.css
@@ -17,6 +17,7 @@ html, body {
 	padding: 0;
 	min-height: 100%;
 	font-family: sans-serif;
+	font-family: var(--font-main), sans-serif;
 	background: #f7f7f7;
 	color: #333;
 }
diff --git a/copyparty/web/mde.html b/copyparty/web/mde.html
index d9b3c056..31618851 100644
--- a/copyparty/web/mde.html
+++ b/copyparty/web/mde.html
@@ -4,11 +4,11 @@
 	
 	
 	
-{{ html_head }}
 	
 	
 	
 	
+{{ html_head }}
 
 
 	
diff --git a/copyparty/web/mde.js b/copyparty/web/mde.js index f8de0feb..5c2872df 100644 --- a/copyparty/web/mde.js +++ b/copyparty/web/mde.js @@ -134,14 +134,14 @@ function save(mde) { function save_cb() { if (this.status !== 200) - return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^
/, ""));
+        return toast.err(0, 'Error!  The file was NOT saved.\n\nError ' + this.status + ":\n" + unpre(this.responseText));
 
     var r;
     try {
         r = JSON.parse(this.responseText);
     }
     catch (ex) {
-        return toast.err(0, 'Failed to parse reply from server:\n\n' + this.responseText);
+        return toast.err(0, 'Error!  The file was likely NOT saved.\n\nFailed to parse reply from server:\n\n' + unpre(this.responseText));
     }
 
     if (!r.ok) {
@@ -180,7 +180,7 @@ function save_cb() {
 
 function save_chk() {
     if (this.status !== 200)
-        return toast.err(0, 'Error!  The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^
/, ""));
+        return toast.err(0, 'Error!  The file was NOT saved.\n\nError ' + this.status + ":\n" + unpre(this.responseText));
 
     var doc1 = this.txt.replace(/\r\n/g, "\n");
     var doc2 = this.responseText.replace(/\r\n/g, "\n");
diff --git a/copyparty/web/msg.css b/copyparty/web/msg.css
index 764cee5d..ab8fa4d1 100644
--- a/copyparty/web/msg.css
+++ b/copyparty/web/msg.css
@@ -1,3 +1,8 @@
+:root {
+	--font-main: sans-serif;
+	--font-serif: serif;
+	--font-mono: 'scp';
+}
 html,body,tr,th,td,#files,a {
 	color: inherit;
 	background: none;
@@ -10,6 +15,7 @@ html {
 	color: #ccc;
 	background: #333;
 	font-family: sans-serif;
+	font-family: var(--font-main), sans-serif;
 	text-shadow: 1px 1px 0px #000;
 	touch-action: manipulation;
 }
@@ -23,6 +29,7 @@ html, body {
 }
 pre {
 	font-family: monospace, monospace;
+	font-family: var(--font-mono), monospace, monospace;
 }
 a {
 	color: #fc5;
diff --git a/copyparty/web/msg.html b/copyparty/web/msg.html
index 4ef5973a..910bc3ae 100644
--- a/copyparty/web/msg.html
+++ b/copyparty/web/msg.html
@@ -7,8 +7,8 @@
 	
 	
 	
-{{ html_head }}
 	
+{{ html_head }}
 
 
 
diff --git a/copyparty/web/splash.css b/copyparty/web/splash.css
index c6b8b5b7..5ca37f9c 100644
--- a/copyparty/web/splash.css
+++ b/copyparty/web/splash.css
@@ -2,6 +2,7 @@ html {
 	color: #333;
 	background: #f7f7f7;
 	font-family: sans-serif;
+	font-family: var(--font-main), sans-serif;
 	touch-action: manipulation;
 }
 #wrap {
@@ -127,6 +128,7 @@ pre, code {
 	color: #480;
 	background: #fff;
 	font-family: 'scp', monospace, monospace;
+	font-family: var(--font-mono), 'scp', monospace, monospace;
 	border: 1px solid rgba(128,128,128,0.3);
 	border-radius: .2em;
 	padding: .15em .2em;
diff --git a/copyparty/web/splash.html b/copyparty/web/splash.html
index 79391610..8e3fe668 100644
--- a/copyparty/web/splash.html
+++ b/copyparty/web/splash.html
@@ -7,9 +7,9 @@
 	
 	
 	
-{{ html_head }}
 	
 	
+{{ html_head }}
 
 
 
@@ -78,13 +78,15 @@
 
 		

client config:

    + {% if k304 or k304vis %} {% if k304 %}
  • disable k304 (currently enabled) {%- else %}
  • enable k304 (currently disabled) {% endif %}
    enabling this will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), but it will also make things slower in general
  • - + {% endif %} +
  • reset client settings
diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index 63fb4a25..d4195833 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -6,7 +6,7 @@ var Ls = { "d1": "tilstand", "d2": "vis tilstanden til alle trÄder", "e1": "last innst.", - "e2": "leser inn konfigurasjonsfiler pÄ nytt$N(kontoer, volumer, volumbrytere)$Nog kartlegger alle e2ds-volumer", + "e2": "leser inn konfigurasjonsfiler pÄ nytt$N(kontoer, volumer, volumbrytere)$Nog kartlegger alle e2ds-volumer$N$Nmerk: endringer i globale parametere$Nkrever en full restart for Ä ta gjenge", "f1": "du kan betrakte:", "g1": "du kan laste opp til:", "cc1": "klient-konfigurasjon", @@ -30,7 +30,7 @@ var Ls = { }, "eng": { "d2": "shows the state of all active threads", - "e2": "reload config files (accounts/volumes/volflags),$Nand rescan all e2ds volumes", + "e2": "reload config files (accounts/volumes/volflags),$Nand rescan all e2ds volumes$N$Nnote: any changes to global settings$Nrequire a full restart to take effect", "u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds", "v2": "use this server as a local HDD$N$NWARNING: this will show your password!", } diff --git a/copyparty/web/svcs.html b/copyparty/web/svcs.html index 49ca9a02..b560b379 100644 --- a/copyparty/web/svcs.html +++ b/copyparty/web/svcs.html @@ -7,10 +7,10 @@ -{{ html_head }} +{{ html_head }} diff --git a/copyparty/web/ui.css b/copyparty/web/ui.css index ffed63e3..eecfc7cf 100644 --- a/copyparty/web/ui.css +++ b/copyparty/web/ui.css @@ -1,4 +1,8 @@ :root { + --font-main: sans-serif; + --font-serif: serif; + --font-mono: 'scp'; + --fg: #ccc; --fg-max: #fff; --bg-u2: #2b2b2b; @@ -378,6 +382,7 @@ html.y textarea:focus { .mdo code, .mdo tt { font-family: 'scp', monospace, monospace; + font-family: var(--font-mono), 'scp', monospace, monospace; white-space: pre-wrap; word-break: break-all; } @@ -447,6 +452,7 @@ html.y textarea:focus { } .mdo blockquote { font-family: serif; + font-family: var(--font-serif), serif; background: #f7f7f7; border: .07em dashed #ccc; padding: 0 2em; diff --git a/copyparty/web/up2k.js b/copyparty/web/up2k.js index 5386d5eb..38737116 100644 --- a/copyparty/web/up2k.js +++ b/copyparty/web/up2k.js @@ -1722,8 +1722,6 @@ function up2k_init(subtle) { ebi('u2etas').style.textAlign = 'left'; } etafun(); - if (pvis.act == 'bz') - pvis.changecard('bz'); } if (flag) { @@ -1859,6 +1857,9 @@ function up2k_init(subtle) { timer.rm(donut.do); ebi('u2tabw').style.minHeight = '0px'; utw_minh = 0; + + if (pvis.act == 'bz') + pvis.changecard('bz'); } function chill(t) { @@ -2256,6 +2257,7 @@ function up2k_init(subtle) { console.log('handshake onerror, retrying', t.name, t); apop(st.busy.handshake, t); st.todo.handshake.unshift(t); + t.cooldown = Date.now() + 5000 + Math.floor(Math.random() * 3000); t.keepalive = keepalive; }; var orz = function (e) { @@ -2263,16 +2265,26 @@ function up2k_init(subtle) { return console.log('zombie handshake onload', t.name, t); if (xhr.status == 200) { + try { + var response = JSON.parse(xhr.responseText); + } + catch (ex) { + apop(st.busy.handshake, t); + st.todo.handshake.unshift(t); + t.cooldown = Date.now() + 5000 + Math.floor(Math.random() * 3000); + return toast.err(0, 'Handshake error; will retry...\n\n' + L.badreply + ':\n\n' + unpre(xhr.responseText)); + } + t.t_handshake = Date.now(); if (keepalive) { apop(st.busy.handshake, t); + tasker(); return; } if (toast.tag === t) toast.ok(5, L.u_fixed); - var response = JSON.parse(xhr.responseText); if (!response.name) { var msg = '', smsg = ''; @@ -2856,6 +2868,8 @@ function up2k_init(subtle) { new_state = false; fixed = true; } + if (new_state === undefined) + new_state = can_write ? false : have_up2k_idx ? true : undefined; } if (new_state === undefined) diff --git a/copyparty/web/util.js b/copyparty/web/util.js index 1faf73be..c7e5e44f 100644 --- a/copyparty/web/util.js +++ b/copyparty/web/util.js @@ -1417,9 +1417,12 @@ function lf2br(txt) { } -function unpre(txt) { +function hunpre(txt) { return ('' + txt).replace(/^
/, '');
 }
+function unpre(txt) {
+    return esc(hunpre(txt));
+}
 
 
 var toast = (function () {
@@ -1995,15 +1998,21 @@ function xhrchk(xhr, prefix, e404, lvl, tag) {
     if (tag === undefined)
         tag = prefix;
 
-    var errtxt = (xhr.response && xhr.response.err) || xhr.responseText,
+    var errtxt = ((xhr.response && xhr.response.err) || xhr.responseText) || '',
+        suf = '',
         fun = toast[lvl || 'err'],
         is_cf = /[Cc]loud[f]lare|>Just a mo[m]ent|#cf-b[u]bbles|Chec[k]ing your br[o]wser|\/chall[e]nge-platform|"chall[e]nge-error|nable Ja[v]aScript and cook/.test(errtxt);
 
+    if (errtxt.startsWith('
'))
+        suf = '\n\nerror-details: «' + unpre(errtxt).split('\n')[0].trim() + '»';
+    else
+        errtxt = esc(errtxt).slice(0, 32768);
+
     if (xhr.status == 403 && !is_cf)
-        return toast.err(0, prefix + (L && L.xhr403 || "403: access denied\n\ntry pressing F5, maybe you got logged out"), tag);
+        return toast.err(0, prefix + (L && L.xhr403 || "403: access denied\n\ntry pressing F5, maybe you got logged out") + suf, tag);
 
     if (xhr.status == 404)
-        return toast.err(0, prefix + e404, tag);
+        return toast.err(0, prefix + e404 + suf, tag);
 
     if (is_cf && (xhr.status == 403 || xhr.status == 503)) {
         var now = Date.now(), td = now - cf_cha_t;
diff --git a/docs/README.md b/docs/README.md
index 4f3339e5..3086e534 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -13,6 +13,9 @@
 
 # other stuff
 
+## [`TODO.md`](TODO.md)
+* planned features / fixes / changes
+
 ## [`example.conf`](example.conf)
 * example config file for `-c`
 
diff --git a/docs/TODO.md b/docs/TODO.md
new file mode 100644
index 00000000..9227ace5
--- /dev/null
+++ b/docs/TODO.md
@@ -0,0 +1,31 @@
+a living list of upcoming features / fixes / changes, very roughly in order of priority
+
+* readme / docs
+  * docker ftp config
+  * custom-fonts (copy from issue)
+  * s3 speedfix
+  * reverseproxy/cloudflare: ensure cloudflare does not terminate https
+  * docker: suggest putting hists in /cfg/hists/
+
+* [github issue #62](https://github.com/9001/copyparty/issues/62) - IdP / single-sign-on powered by a local identity provider service which is possibly hooked up to ldap or an oauth service
+  * secret token header between reverse-proxy and copyparty to confirm the headers are legit
+  * persist autogenerated volumes for db-init + nullmapping on next startup (`_map_volume` += `only_if_exist`)
+  * sanchk that autogenerated volumes below inaccessible parent
+  * disable logout links if idp detected
+
+* download accelerator
+  * definitely download chunks in parallel
+  * maybe resumable downloads (chrome-only, jank api)
+  * maybe checksum validation (return sha512 of requested range in responses, and probably also warks)
+
+* [github issue #64](https://github.com/9001/copyparty/issues/64) - dirkeys 2nd season
+  * popular feature request, finally time to refactor browser.js i suppose...
+
+* [github issue #37](https://github.com/9001/copyparty/issues/37) - upload PWA
+  * or [maybe not](https://arstechnica.com/tech-policy/2024/02/apple-under-fire-for-disabling-iphone-web-apps-eu-asks-developers-to-weigh-in/), or [maybe](https://arstechnica.com/gadgets/2024/03/apple-changes-course-will-keep-iphone-eu-web-apps-how-they-are-in-ios-17-4/)
+
+* [github issue #57](https://github.com/9001/copyparty/issues/57) - config GUI
+  * configs given to -c can be ordered with numerical prefix
+  * autorevert settings if it fails to apply
+  * countdown until session invalidates in settings gui, with refresh-button
+
diff --git a/docs/devnotes.md b/docs/devnotes.md
index 49023b81..72f331c0 100644
--- a/docs/devnotes.md
+++ b/docs/devnotes.md
@@ -218,7 +218,7 @@ if you don't need all the features, you can repack the sfx and save a bunch of s
 * `269k` after `./scripts/make-sfx.sh re no-cm no-hl`
 
 the features you can opt to drop are
-* `cm`/easymde, the "fancy" markdown editor, saves ~82k
+* `cm`/easymde, the "fancy" markdown editor, saves ~89k
 * `hl`, prism, the syntax hilighter, saves ~41k
 * `fnt`, source-code-pro, the monospace font, saves ~9k
 * `dd`, the custom mouse cursor for the media player tray tab, saves ~2k
diff --git a/docs/rice/README.md b/docs/rice/README.md
new file mode 100644
index 00000000..385e3ee3
--- /dev/null
+++ b/docs/rice/README.md
@@ -0,0 +1,34 @@
+# custom fonts
+
+to change the fonts in the web-UI,  first save the following text (the default font-config) to a new css file, for example named `customfonts.css` in your webroot:
+
+```css
+:root {
+	--font-main: sans-serif;
+	--font-serif: serif;
+	--font-mono: 'scp';
+}
+```
+
+add this to your copyparty config so the css file gets loaded: `--html-head=''`
+
+alternatively, if you are using a config file instead of commandline args:
+
+```yaml
+[global]
+  html-head: 
+```
+
+restart copyparty for the config change to take effect
+
+edit the css file you made and press `ctrl`-`shift`-`R` in the browser to see the changes as you go (no need to restart copyparty for each change)
+
+if you are introducing a new ttf/woff font, don't forget to declare the font itself in the css file; here's one of the default fonts from `ui.css`:
+
+```css
+@font-face {
+	font-family: 'scp';
+	font-display: swap;
+	src: local('Source Code Pro Regular'), local('SourceCodePro-Regular'), url(deps/scp.woff2) format('woff2');
+}
+```
diff --git a/scripts/deps-docker/Dockerfile b/scripts/deps-docker/Dockerfile
index 96c08e88..20e86a9e 100644
--- a/scripts/deps-docker/Dockerfile
+++ b/scripts/deps-docker/Dockerfile
@@ -24,7 +24,7 @@ ENV     ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \
 # the scp url is regular latin from https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap
 RUN     mkdir -p /z/dist/no-pk \
         && wget https://fonts.gstatic.com/s/sourcecodepro/v11/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2 -O scp.woff2 \
-        && apk add cmake make g++ git bash npm patch wget tar pigz brotli gzip unzip python3 python3-dev brotli py3-brotli \
+        && apk add cmake make g++ git bash npm patch wget tar pigz brotli gzip unzip python3 python3-dev py3-brotli \
         && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \
         && wget https://github.com/openpgpjs/asmcrypto.js/archive/$ver_asmcrypto.tar.gz -O asmcrypto.tgz \
         && wget https://github.com/markedjs/marked/archive/v$ver_marked.tar.gz -O marked.tgz \
@@ -143,9 +143,8 @@ RUN     ./genprism.sh $ver_prism
 
 
 # compress
-COPY    brotli.makefile zopfli.makefile /z/dist/
+COPY    zopfli.makefile /z/dist/
 RUN     cd /z/dist \
-        && make -j$(nproc) -f brotli.makefile \
         && make -j$(nproc) -f zopfli.makefile \
         && rm *.makefile \
         && mv no-pk/* . \
diff --git a/scripts/deps-docker/brotli.makefile b/scripts/deps-docker/brotli.makefile
deleted file mode 100644
index 3860224f..00000000
--- a/scripts/deps-docker/brotli.makefile
+++ /dev/null
@@ -1,4 +0,0 @@
-all: $(addsuffix .br, $(wildcard easymde*))
-
-%.br: %
-	brotli -jZ $<
diff --git a/scripts/make-sfx.sh b/scripts/make-sfx.sh
index 0c77f5f2..53c6d2d0 100755
--- a/scripts/make-sfx.sh
+++ b/scripts/make-sfx.sh
@@ -37,7 +37,7 @@ help() { exec cat <<'EOF'
 # _____________________________________________________________________
 # web features:
 #
-# `no-cm` saves ~82k by removing easymde/codemirror
+# `no-cm` saves ~89k by removing easymde/codemirror
 #   (the fancy markdown editor)
 #
 # `no-hl` saves ~41k by removing syntax hilighting in the text viewer
@@ -406,7 +406,7 @@ find -type f -name ._\* | while IFS= read -r f; do cmp <(printf '\x00\x05\x16')
 
 rm -f copyparty/web/deps/*.full.* copyparty/web/dbg-* copyparty/web/Makefile
 
-find copyparty | LC_ALL=C sort | sed -r 's/\.(gz|br)$//;s/$/,/' > have
+find copyparty | LC_ALL=C sort | sed -r 's/\.gz$//;s/$/,/' > have
 cat have | while IFS= read -r x; do
 	grep -qF -- "$x" ../scripts/sfx.ls || {
 		echo "unexpected file: $x"
@@ -603,7 +603,7 @@ sed -r 's/(.*)\.(.*)/\2 \1/' | LC_ALL=C sort |
 sed -r 's/([^ ]*) (.*)/\2.\1/' | grep -vE '/list1?$' > list1
 
 for n in {1..50}; do
-	(grep -vE '\.(gz|br)$' list1; grep -E '\.(gz|br)$' list1 | (shuf||gshuf) ) >list || true
+	(grep -vE '\.gz$' list1; grep -E '\.gz$' list1 | (shuf||gshuf) ) >list || true
 	s=$( (sha1sum||shasum) < list | cut -c-16)
 	grep -q $s "$zdir/h" 2>/dev/null && continue
 	echo $s >> "$zdir/h"
diff --git a/tests/util.py b/tests/util.py
index 66eae05c..0d243146 100644
--- a/tests/util.py
+++ b/tests/util.py
@@ -119,13 +119,13 @@ class Cfg(Namespace):
         ex = "ah_cli ah_gen css_browser hist ipa_re js_browser no_forget no_hash no_idx nonsus_urls"
         ka.update(**{k: None for k in ex.split()})
 
-        ex = "hash_mt srch_time u2j"
+        ex = "hash_mt srch_time u2abort u2j"
         ka.update(**{k: 1 for k in ex.split()})
 
         ex = "reg_cap s_thead s_tbody th_convt"
         ka.update(**{k: 9 for k in ex.split()})
 
-        ex = "db_act df loris re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo"
+        ex = "db_act df k304 loris 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 doctitle exit favico idp_h_usr html_head lg_sbf log_fk md_sbf name textfiles unlist vname R RS SR"
timesizedonefile
' + '' + L.un_next.format(Math.min(mods[b], res.length - a)) + '
' + L.un_del + '
' + (done ? L.un_del : L.un_abrt) + '' + unix2iso(res[a].at) + '' + res[a].sz + '' + ('' + res[a].sz).replace(/\B(?=(\d{3})+(?!\d))/g, " ") + '100%' + res[a].pd + '%' + linksplit(res[a].vp).join(' / ') + '