list incoming files + ETA in controlpanel

This commit is contained in:
ed 2024-09-10 21:24:05 +00:00
parent c79eaa089a
commit 609c5921d4
7 changed files with 104 additions and 24 deletions

View file

@ -43,6 +43,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [unpost](#unpost) - undo/delete accidental uploads * [unpost](#unpost) - undo/delete accidental uploads
* [self-destruct](#self-destruct) - uploads can be given a lifetime * [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)) * [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) * [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 * [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 * [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 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 ## file manager
cut/paste, rename, and delete files/folders (if you have permission) cut/paste, rename, and delete files/folders (if you have permission)

View file

@ -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-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-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-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-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") ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader IPs into the database")

View file

@ -87,6 +87,7 @@ from .util import (
relchk, relchk,
ren_open, ren_open,
runhook, runhook,
s2hms,
s3enc, s3enc,
sanitize_fn, sanitize_fn,
sanitize_vpath, sanitize_vpath,
@ -3939,11 +3940,30 @@ class HttpCli(object):
for y in [self.rvol, self.wvol, self.avol] for y in [self.rvol, self.wvol, self.avol]
] ]
if self.avol and not self.args.no_rescan: ups = []
x = self.conn.hsrv.broker.ask("up2k.get_state") 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()) vs = json.loads(x.get())
vstate = {("/" + k).rstrip("/") + "/": v for k, v in vs["volstate"].items()} 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 = {} vstate = {}
vs = { vs = {
"scanning": None, "scanning": None,
@ -3968,6 +3988,12 @@ class HttpCli(object):
for k in ["scanning", "hashq", "tagq", "mtpq", "dbwt"]: for k in ["scanning", "hashq", "tagq", "mtpq", "dbwt"]:
txt += " {}({})".format(k, vs[k]) 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: if rvol:
txt += "\nyou can browse:" txt += "\nyou can browse:"
for v in rvol: for v in rvol:
@ -3991,6 +4017,7 @@ class HttpCli(object):
avol=avol, avol=avol,
in_shr=self.args.shr and self.vpath.startswith(self.args.shr[1:]), in_shr=self.args.shr and self.vpath.startswith(self.args.shr[1:]),
vstate=vstate, vstate=vstate,
ups=ups,
scanning=vs["scanning"], scanning=vs["scanning"],
hashq=vs["hashq"], hashq=vs["hashq"],
tagq=vs["tagq"], tagq=vs["tagq"],

View file

@ -268,19 +268,29 @@ class Up2k(object):
if not self.stop: if not self.stop:
self.log("uploads are now possible", 2) 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 mtpq: Union[int, str] = 0
ups = []
up_en = not self.args.no_up_list
q = "select count(w) from mt where k = 't:mtp'" q = "select count(w) from mt where k = 't:mtp'"
got_lock = False if PY2 else self.mutex.acquire(timeout=0.5) got_lock = False if PY2 else self.mutex.acquire(timeout=0.5)
if got_lock: if got_lock:
for cur in self.cur.values(): try:
try: for cur in self.cur.values() if get_q else []:
mtpq += cur.execute(q).fetchone()[0] try:
except: mtpq += cur.execute(q).fetchone()[0]
pass except:
self.mutex.release() pass
if uname and up_en:
ups = self._active_uploads(uname)
finally:
self.mutex.release()
else: else:
mtpq = "(?)" mtpq = "(?)"
if up_en:
ups = [(0, 0, time.time(), "cannot show list (server too busy)")]
ups.sort(reverse=True)
ret = { ret = {
"volstate": self.volstate, "volstate": self.volstate,
@ -288,6 +298,7 @@ class Up2k(object):
"hashq": self.n_hashq, "hashq": self.n_hashq,
"tagq": self.n_tagq, "tagq": self.n_tagq,
"mtpq": mtpq, "mtpq": mtpq,
"ups": ups,
"dbwu": "{:.2f}".format(self.db_act), "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)
@ -295,6 +306,32 @@ class Up2k(object):
} }
return json.dumps(ret, separators=(",\n", ": ")) 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: def find_job_by_ap(self, ptop: str, ap: str) -> str:
try: try:
if ANYWIN: if ANYWIN:
@ -2910,9 +2947,12 @@ class Up2k(object):
job = deepcopy(job) job = deepcopy(job)
job["wark"] = wark job["wark"] = wark
job["at"] = cj.get("at") or time.time() 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(): for k in zs.split():
job[k] = cj.get(k) or "" 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"]) pdir = djoin(cj["ptop"], cj["prel"])
if rand: if rand:
@ -3013,18 +3053,8 @@ class Up2k(object):
"busy": {}, "busy": {},
} }
# client-provided, sanitized by _get_wark: name, size, lmod # client-provided, sanitized by _get_wark: name, size, lmod
for k in [ zs = "vtop ptop prel name size lmod host user addr poke"
"host", for k in zs.split():
"user",
"addr",
"vtop",
"ptop",
"prel",
"name",
"size",
"lmod",
"poke",
]:
job[k] = cj[k] job[k] = cj[k]
for k in ["life", "replace"]: for k in ["life", "replace"]:

View file

@ -60,6 +60,18 @@
</div> </div>
{%- endif %} {%- endif %}
{%- if ups %}
<h1 id="aa">incoming files:</h1>
<table class="vols">
<thead><tr><th>%</th><th>speed</th><th>eta</th><th>idle</th><th>dir</th><th>file</th></tr></thead>
<tbody>
{% for u in ups %}
<tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td><a href="{{ u[4] }}">{{ u[5]|e }}</a></td><td>{{ u[6]|e }}</td></tr>
{% endfor %}
</tbody>
</table>
{%- endif %}
{%- if rvol %} {%- if rvol %}
<h1 id="f">you can browse:</h1> <h1 id="f">you can browse:</h1>
<ul> <ul>

View file

@ -33,6 +33,7 @@ var Ls = {
"ta1": "du må skrive et nytt passord først", "ta1": "du må skrive et nytt passord først",
"ta2": "gjenta for å bekrefte nytt passord:", "ta2": "gjenta for å bekrefte nytt passord:",
"ta3": "fant en skrivefeil; vennligst prøv igjen", "ta3": "fant en skrivefeil; vennligst prøv igjen",
"aa1": "innkommende:",
}, },
"eng": { "eng": {
"d2": "shows the state of all active threads", "d2": "shows the state of all active threads",
@ -78,6 +79,7 @@ var Ls = {
"ta1": "请先输入新密码", "ta1": "请先输入新密码",
"ta2": "重复以确认新密码:", "ta2": "重复以确认新密码:",
"ta3": "发现拼写错误;请重试", "ta3": "发现拼写错误;请重试",
"aa1": "正在接收的文件:", //m
} }
}; };

View file

@ -120,7 +120,7 @@ class Cfg(Namespace):
ex = "chpw daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink ih ihead magic hardlink_only nid nih no_acode no_athumb no_dav no_db_ip no_del no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw og og_no_head og_s_title q rand smb srch_dbg stats uqe vague_403 vc ver write_uplog xdev xlink xvol zs" ex = "chpw daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink ih ihead magic hardlink_only nid nih no_acode no_athumb no_dav no_db_ip no_del no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw og og_no_head og_s_title q rand smb srch_dbg stats uqe vague_403 vc ver write_uplog xdev xlink xvol zs"
ka.update(**{k: False for k in ex.split()}) ka.update(**{k: False for k in ex.split()})
ex = "dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_voldump re_dhash plain_ip" ex = "dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash plain_ip"
ka.update(**{k: True for k in ex.split()}) ka.update(**{k: True for k in ex.split()})
ex = "ah_cli ah_gen css_browser hist js_browser js_other mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua" ex = "ah_cli ah_gen css_browser hist js_browser js_other mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua"