From 609c5921d44c73588a22c3eb1620bbaab7601772 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 10 Sep 2024 21:24:05 +0000 Subject: [PATCH] list incoming files + ETA in controlpanel --- README.md | 8 +++++ copyparty/__main__.py | 1 + copyparty/httpcli.py | 33 ++++++++++++++++-- copyparty/up2k.py | 70 ++++++++++++++++++++++++++++----------- copyparty/web/splash.html | 12 +++++++ copyparty/web/splash.js | 2 ++ tests/util.py | 2 +- 7 files changed, 104 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 43fad539..cb62b8bb 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ turn almost any device into a file server with resumable uploads/downloads using * [unpost](#unpost) - undo/delete accidental uploads * [self-destruct](#self-destruct) - uploads can be given a lifetime * [race the beam](#race-the-beam) - download files while they're still uploading ([demo video](http://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm)) + * [incoming files](#incoming-files) - the control-panel shows the ETA for all incoming files * [file manager](#file-manager) - cut/paste, rename, and delete files/folders (if you have permission) * [shares](#shares) - share a file or folder by creating a temporary link * [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI @@ -731,6 +732,13 @@ download files while they're still uploading ([demo video](http://a.ocv.me/pub/g requires the file to be uploaded using up2k (which is the default drag-and-drop uploader), alternatively the command-line program +### incoming files + +the control-panel shows the ETA for all incoming files , but only for files being uploaded into volumes where you have read-access + +![copyparty-cpanel-upload-eta-or8](https://github.com/user-attachments/assets/fd275ffa-698c-4fca-a307-4d2181269a6a) + + ## file manager cut/paste, rename, and delete files/folders (if you have permission) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 1aeafe0f..ab50dfb3 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1230,6 +1230,7 @@ def add_optouts(ap): ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar") ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)") ap2.add_argument("--no-lifetime", action="store_true", help="do not allow clients (or server config) to schedule an upload to be deleted after a given time") + ap2.add_argument("--no-up-list", action="store_true", help="don't show list of incoming files in controlpanel") ap2.add_argument("--no-pipe", action="store_true", help="disable race-the-beam (lockstep download of files which are currently being uploaded) (volflag=nopipe)") ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader IPs into the database") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index ca1b5a58..36bf858e 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -87,6 +87,7 @@ from .util import ( relchk, ren_open, runhook, + s2hms, s3enc, sanitize_fn, sanitize_vpath, @@ -3939,11 +3940,30 @@ class HttpCli(object): for y in [self.rvol, self.wvol, self.avol] ] - if self.avol and not self.args.no_rescan: - x = self.conn.hsrv.broker.ask("up2k.get_state") + ups = [] + now = time.time() + get_vst = self.avol and not self.args.no_rescan + get_ups = self.rvol and not self.args.no_up_list and self.uname or "" + if get_vst or get_ups: + x = self.conn.hsrv.broker.ask("up2k.get_state", get_vst, get_ups) vs = json.loads(x.get()) vstate = {("/" + k).rstrip("/") + "/": v for k, v in vs["volstate"].items()} - else: + try: + for rem, sz, t0, poke, vp in vs["ups"]: + fdone = max(0.001, 1 - rem) + td = max(0.1, now - t0) + rd, fn = vsplit(vp.replace(os.sep, "/")) + if not rd: + rd = "/" + erd = quotep(rd) + rds = rd.replace("/", " / ") + spd = humansize(sz * fdone / td, True) + "/s" + eta = s2hms((td / fdone) - td, True) + idle = s2hms(now - poke, True) + ups.append((int(100 * fdone), spd, eta, idle, erd, rds, fn)) + except Exception as ex: + self.log("failed to list upload progress: %r" % (ex,), 1) + if not get_vst: vstate = {} vs = { "scanning": None, @@ -3968,6 +3988,12 @@ class HttpCli(object): for k in ["scanning", "hashq", "tagq", "mtpq", "dbwt"]: txt += " {}({})".format(k, vs[k]) + if ups: + txt += "\n\nincoming files:" + for zt in ups: + txt += "\n%s" % (", ".join((str(x) for x in zt)),) + txt += "\n" + if rvol: txt += "\nyou can browse:" for v in rvol: @@ -3991,6 +4017,7 @@ class HttpCli(object): avol=avol, in_shr=self.args.shr and self.vpath.startswith(self.args.shr[1:]), vstate=vstate, + ups=ups, scanning=vs["scanning"], hashq=vs["hashq"], tagq=vs["tagq"], diff --git a/copyparty/up2k.py b/copyparty/up2k.py index b53e9e1d..dfaf95f7 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -268,19 +268,29 @@ class Up2k(object): if not self.stop: self.log("uploads are now possible", 2) - def get_state(self) -> str: + def get_state(self, get_q: bool, uname: str) -> str: mtpq: Union[int, str] = 0 + ups = [] + up_en = not self.args.no_up_list q = "select count(w) from mt where k = 't:mtp'" got_lock = False if PY2 else self.mutex.acquire(timeout=0.5) if got_lock: - for cur in self.cur.values(): - try: - mtpq += cur.execute(q).fetchone()[0] - except: - pass - self.mutex.release() + try: + for cur in self.cur.values() if get_q else []: + try: + mtpq += cur.execute(q).fetchone()[0] + except: + pass + if uname and up_en: + ups = self._active_uploads(uname) + finally: + self.mutex.release() else: mtpq = "(?)" + if up_en: + ups = [(0, 0, time.time(), "cannot show list (server too busy)")] + + ups.sort(reverse=True) ret = { "volstate": self.volstate, @@ -288,6 +298,7 @@ class Up2k(object): "hashq": self.n_hashq, "tagq": self.n_tagq, "mtpq": mtpq, + "ups": ups, "dbwu": "{:.2f}".format(self.db_act), "dbwt": "{:.2f}".format( min(1000 * 24 * 60 * 60 - 1, time.time() - self.db_act) @@ -295,6 +306,32 @@ class Up2k(object): } return json.dumps(ret, separators=(",\n", ": ")) + def _active_uploads(self, uname: str) -> list[tuple[float, int, int, str]]: + ret = [] + for vtop in self.asrv.vfs.aread[uname]: + vfs = self.asrv.vfs.all_vols.get(vtop) + if not vfs: # dbv only + continue + ptop = vfs.realpath + tab = self.registry.get(ptop) + if not tab: + continue + for job in tab.values(): + ineed = len(job["need"]) + ihash = len(job["hash"]) + if ineed == ihash or not ineed: + continue + + zt = ( + ineed / ihash, + job["size"], + int(job["t0"]), + int(job["poke"]), + djoin(vtop, job["prel"], job["name"]), + ) + ret.append(zt) + return ret + def find_job_by_ap(self, ptop: str, ap: str) -> str: try: if ANYWIN: @@ -2910,9 +2947,12 @@ class Up2k(object): job = deepcopy(job) job["wark"] = wark job["at"] = cj.get("at") or time.time() - zs = "lmod ptop vtop prel name host user addr poke" + zs = "vtop ptop prel name lmod host user addr poke" for k in zs.split(): job[k] = cj.get(k) or "" + for k in ("life", "replace"): + if k in cj: + job[k] = cj[k] pdir = djoin(cj["ptop"], cj["prel"]) if rand: @@ -3013,18 +3053,8 @@ class Up2k(object): "busy": {}, } # client-provided, sanitized by _get_wark: name, size, lmod - for k in [ - "host", - "user", - "addr", - "vtop", - "ptop", - "prel", - "name", - "size", - "lmod", - "poke", - ]: + zs = "vtop ptop prel name size lmod host user addr poke" + for k in zs.split(): job[k] = cj[k] for k in ["life", "replace"]: diff --git a/copyparty/web/splash.html b/copyparty/web/splash.html index de6dac6c..9ac159f7 100644 --- a/copyparty/web/splash.html +++ b/copyparty/web/splash.html @@ -60,6 +60,18 @@ {%- endif %} + {%- if ups %} +

incoming files:

+ + + + {% for u in ups %} + + {% endfor %} + +
%speedetaidledirfile
{{ u[0] }}{{ u[1] }}{{ u[2] }}{{ u[3] }}{{ u[5]|e }}{{ u[6]|e }}
+ {%- endif %} + {%- if rvol %}

you can browse: