From 715d374ee4d849ef4e7da2aec84f68f5dad87458 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 12 Aug 2025 21:46:42 +0000 Subject: [PATCH] button to abort copy/move; closes #572 --- copyparty/__main__.py | 1 + copyparty/httpcli.py | 20 +++++++++++++++-- copyparty/svchub.py | 2 +- copyparty/up2k.py | 12 ++++++++-- copyparty/web/browser.css | 5 +++++ copyparty/web/browser.js | 46 +++++++++++++++++++++++++++++++++++---- tests/util.py | 2 +- 7 files changed, 78 insertions(+), 10 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index ddfdd8a2..86d8cdd5 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1319,6 +1319,7 @@ def add_optouts(ap): ap2.add_argument("--no-del", action="store_true", help="disable delete operations") ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations") ap2.add_argument("--no-cp", action="store_true", help="disable copy operations") + ap2.add_argument("--no-fs-abrt", action="store_true", help="disable ability to abort ongoing copy/move") ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m in ") ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI") ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 72bff45e..a7ff303d 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1987,6 +1987,9 @@ class HttpCli(object): if "eshare" in self.uparam: return self.handle_eshare() + if "fs_abrt" in self.uparam: + return self.handle_fs_abrt() + if "application/octet-stream" in ctype: return self.handle_post_binary() @@ -5958,7 +5961,9 @@ class HttpCli(object): self.asrv.vfs.get(vdst, self.uname, False, True, False, True) wunlink(self.log, dabs, dvn.flags) - x = self.conn.hsrv.broker.ask("up2k.handle_mv", self.uname, self.ip, vsrc, vdst) + x = self.conn.hsrv.broker.ask( + "up2k.handle_mv", self.ouparam.get("akey"), self.uname, self.ip, vsrc, vdst + ) self.loud_reply(x.get(), status=201) return True @@ -5988,10 +5993,21 @@ class HttpCli(object): self.asrv.vfs.get(vdst, self.uname, False, True, False, True) wunlink(self.log, dabs, dvn.flags) - x = self.conn.hsrv.broker.ask("up2k.handle_cp", self.uname, self.ip, vsrc, vdst) + x = self.conn.hsrv.broker.ask( + "up2k.handle_cp", self.ouparam.get("akey"), self.uname, self.ip, vsrc, vdst + ) self.loud_reply(x.get(), status=201) return True + def handle_fs_abrt(self): + if self.args.no_fs_abrt: + t = "aborting an ongoing copy/move is disabled in server config" + raise Pebkac(403, t) + + self.conn.hsrv.broker.say("up2k.handle_fs_abrt", self.uparam["fs_abrt"]) + self.loud_reply("aborting", status=200) + return True + def tx_ls(self, ls: dict[str, Any]) -> bool: dirs = ls["dirs"] files = ls["files"] diff --git a/copyparty/svchub.py b/copyparty/svchub.py index a6f87a85..950122d0 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -39,9 +39,9 @@ from .th_srv import ( HAVE_FFPROBE, HAVE_HEIF, HAVE_PIL, + HAVE_RAW, HAVE_VIPS, HAVE_WEBP, - HAVE_RAW, ThumbSrv, ) from .up2k import Up2k diff --git a/copyparty/up2k.py b/copyparty/up2k.py index b233cecb..993d5b82 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -144,6 +144,7 @@ class Up2k(object): self.salt = self.args.warksalt self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$") + self.abrt_key = "" self.gid = 0 self.gt0 = 0 @@ -3988,6 +3989,9 @@ class Up2k(object): except: pass + def handle_fs_abrt(self, akey: str) -> None: + self.abrt_key = akey + def handle_rm( self, uname: str, @@ -4197,7 +4201,7 @@ class Up2k(object): return n_files, ok + ok2, ng + ng2 - def handle_cp(self, uname: str, ip: str, svp: str, dvp: str) -> str: + def handle_cp(self, abrt: str, uname: str, ip: str, svp: str, dvp: str) -> str: if svp == dvp or dvp.startswith(svp + "/"): raise Pebkac(400, "cp: cannot copy parent into subfolder") @@ -4244,6 +4248,8 @@ class Up2k(object): dvpf = dvp + svpf[len(svp) :] self._cp_file(uname, ip, svpf, dvpf, curs) + if abrt and abrt == self.abrt_key: + raise Pebkac(400, "filecopy aborted by http-api") for v in curs: v.connection.commit() @@ -4411,7 +4417,7 @@ class Up2k(object): return "k" - def handle_mv(self, uname: str, ip: str, svp: str, dvp: str) -> str: + def handle_mv(self, abrt: str, uname: str, ip: str, svp: str, dvp: str) -> str: if svp == dvp or dvp.startswith(svp + "/"): raise Pebkac(400, "mv: cannot move parent into subfolder") @@ -4466,6 +4472,8 @@ class Up2k(object): dvpf = dvp + svpf[len(svp) :] self._mv_file(uname, ip, svpf, dvpf, curs) + if abrt and abrt == self.abrt_key: + raise Pebkac(400, "filemove aborted by http-api") for v in curs: v.connection.commit() diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index f2dc8cde..00e42cae 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -1930,6 +1930,11 @@ html.y #tree.nowrap .ntree a+a:hover { padding: 0; font-size: 1.5em; } +#fs_abrt { + margin-top: 1em; + text-shadow: 0; + box-shadow: 1px 1px 0 var(--bg-d3); +} #doc { overflow: visible; background: #fff; diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index a0a1a21c..112e7da0 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -427,6 +427,7 @@ var Ls = { "fcp_ok": "copy OK", "fp_busy": "moving {0} items...\n\n{1}", "fcp_busy": "copying {0} items...\n\n{1}", + "fp_abrt": "aborting...", "fp_err": "move failed:\n", "fcp_err": "copy failed:\n", "fp_confirm": "move these {0} items here?", @@ -1057,6 +1058,7 @@ var Ls = { "fcp_ok": "kopiering OK", "fp_busy": "flytter {0} filer...\n\n{1}", "fcp_busy": "kopierer {0} filer...\n\n{1}", + "fp_abrt": "avbryter...", "fp_err": "flytting feilet:\n", "fcp_err": "kopiering feilet:\n", "fp_confirm": "flytt disse {0} filene hit?", @@ -1686,6 +1688,7 @@ var Ls = { "fcp_ok": "复制成功", //m "fp_busy": "正在移动 {0} 项...\n\n{1}", "fcp_busy": "正在复制 {0} 项...\n\n{1}", //m + "fp_abrt": "正在中止...", //m "fp_err": "移动失败:\n", "fcp_err": "复制失败:\n", //m "fp_confirm": "将这些 {0} 项移动到这里?", @@ -2319,6 +2322,7 @@ var Ls = { "fcp_ok": "kopírování OK", "fp_busy": "přesouvám {0} položek...\n\n{1}", "fcp_busy": "kopíruji {0} položek...\n\n{1}", + "fp_abrt": "přerušuji...", //m "fp_err": "přesun selhal:\n", "fcp_err": "kopírování selhalo:\n", "fp_confirm": "přesunout těchto {0} položek sem?", @@ -2948,6 +2952,7 @@ var Ls = { "fcp_ok": "Kopieren OK", "fp_busy": "Verschiebe {0} Elemente...\n\n{1}", "fcp_busy": "Kopiere {0} Elemente...\n\n{1}", + "fp_abrt": "Abbrechen...", //m "fp_err": "Verschieben fehlgeschlagen:\n", "fcp_err": "Kopieren fehlgeschlagen:\n", "fp_confirm": "Diese {0} Elemente hierher verschieben?", @@ -3577,6 +3582,7 @@ var Ls = { "fcp_ok": "kopiointi OK", "fp_busy": "siirretään {0} kohdetta...\n\n{1}", "fcp_busy": "kopioidaan {0} kohdetta...\n\n{1}", + "fp_abrt": "keskeytetään...", //m "fp_err": "siirto epäonnistui:\n", "fcp_err": "kopiointi epäonnistui:\n", "fp_confirm": "siirrä nämä {0} kohdetta tänne?", @@ -4206,6 +4212,7 @@ var Ls = { "fcp_ok": "copie OK", "fp_busy": "déplacement de {0} éléments…\n\n{1}", "fcp_busy": "copie de {0} éléments…\n\n{1}", + "fp_abrt": "abandon en cours...", //m "fp_err": "deplacement échoué:\n", "fcp_err": "copie échouée:\n", "fp_confirm": "déplacer ces {0} éléments ici ?", @@ -4835,6 +4842,7 @@ var Ls = { "fcp_ok": "αντιγραφή OK", "fp_busy": "μετακίνηση {0} αντικειμένων...\n\n{1}", "fcp_busy": "αντιγραφή {0} αντικειμένων...\n\n{1}", + "fp_abrt": "γίνεται ακύρωση...", //m "fp_err": "αποτυχία μετακίνησης:\n", "fcp_err": "αποτυχία αντιγραφής:\n", "fp_confirm": "να μετακινηθούν αυτά τα {0} αντικείμενα εδώ;", @@ -5464,6 +5472,7 @@ var Ls = { "fcp_ok": "copia OK", "fp_busy": "spostando {0} elementi...\n\n{1}", "fcp_busy": "copiando {0} elementi...\n\n{1}", + "fp_abrt": "annullamento in corso...", //m "fp_err": "spostamento fallito:\n", "fcp_err": "copia fallita:\n", "fp_confirm": "spostare questi {0} elementi qui?", @@ -6093,6 +6102,7 @@ var Ls = { "fcp_ok": "Kopiëren OK", "fp_busy": "{0} items verplaatsen...\n\n{1}", "fcp_busy": "{0} items kopiëren...\n\n{1}", + "fp_abrt": "afbreken...", //m "fp_err": "Verplaatsen mislukt:\n", "fcp_err": "Kopieëren mislukt:\n", "fp_confirm": "Verplaats deze {0} items hierheen?", @@ -6723,6 +6733,7 @@ var Ls = { "fcp_ok": "kopiering OK", "fp_busy": "flyttar {0} filer...\n\n{1}", "fcp_busy": "kopierar {0} filer...\n\n{1}", + "fp_abrt": "avbryt...", "fp_err": "flytting feila:\n", "fcp_err": "kopiering feila:\n", "fp_confirm": "flytt disse {0} filene hit?", @@ -7349,6 +7360,7 @@ var Ls = { "fcp_ok": "przekopiowano", "fp_busy": "przenoszenie {0} elementów...\n\n{1}", "fcp_busy": "kopiowanie {0} elementów...\n\n{1}", + "fp_abrt": "przerywanie...", //m "fp_err": "nie udało się przenieść:\n", "fcp_err": "nie udało się skopiować:\n", "fp_confirm": "przenieść tutaj {0} elementy(ów)?", @@ -7978,6 +7990,7 @@ var Ls = { "fcp_ok": "успешно скопировано", "fp_busy": "перемещаю {0} файлов...\n\n{1}", "fcp_busy": "копирую {0} файлов...\n\n{1}", + "fp_abrt": "прерывание...", //m "fp_err": "ошибка перемещения:\n", "fcp_err": "ошибка копирования:\n", "fp_confirm": "переместить эти {0} файлов сюда?", @@ -8606,6 +8619,7 @@ var Ls = { "fcp_ok": "copia correcta", "fp_busy": "moviendo {0} elementos...\n\n{1}", "fcp_busy": "copiando {0} elementos...\n\n{1}", + "fp_abrt": "cancelando...", //m "fp_err": "fallo al mover:\n", "fcp_err": "fallo al copiar:\n", "fp_confirm": "¿mover estos {0} elementos aquí?", @@ -9235,6 +9249,7 @@ var Ls = { "fcp_ok": "копіювання OK", "fp_busy": "переміщення {0} елементів...\n\n{1}", "fcp_busy": "копіювання {0} елементів...\n\n{1}", + "fp_abrt": "переривання...", //m "fp_err": "переміщення невдале:\n", "fcp_err": "копіювання невдале:\n", "fp_confirm": "перемістити ці {0} елементи сюди?", @@ -9882,6 +9897,7 @@ var ACtx = !IPHONE && (window.AudioContext || window.webkitAudioContext), hash0 = location.hash, sloc0 = '' + location, noih = /[?&]v\b/.exec(sloc0), + abrt_key = "", rtt = null, ldks = [], dks = {}, @@ -12414,6 +12430,15 @@ function fmt_ren(re, md, fmt) { } +function fs_abrt() { + toast.inf(30, L.fp_abrt); + fileman.f.length = 0; + var xhr = new XHR(); + xhr.open('POST', '/?fs_abrt=' + abrt_key, true); + xhr.send(); +} + + var fileman = (function () { var bren = ebi('fren'), bdel = ebi('fdel'), @@ -12424,6 +12449,7 @@ var fileman = (function () { t_paste, r = {}; + r.f = []; r.clip = null; try { r.bus = new BroadcastChannel("fileman_bus"); @@ -12711,6 +12737,8 @@ var fileman = (function () { base = vsplit(sel[0].vp)[0], mkeys; + r.f = f; + for (var a = 0; a < sel.length; a++) { var vp = sel[a].vp; if (vp.endsWith('/')) @@ -12990,7 +13018,9 @@ var fileman = (function () { return rn_cancel(); } - toast.show('inf r', 0, esc(L.fr_busy.format(f.length, f[0].ofn))); + var msg = esc(L.fr_busy.format(f.length, f[0].ofn)); + msg += '\n<a id="fs_abrt" class="btn" href="#" onclick="fs_abrt()">' + L.fs_abrt + '</a>'; + toast.show('inf r', 0, msg); var dst = base + uricom_enc(f[0].inew.value, false); function rename_cb() { @@ -13004,8 +13034,10 @@ var fileman = (function () { return rn_apply_loop(); } + abrt_key = randstr(9); + var xhr = new XHR(); - xhr.open('POST', f[0].src + '?move=' + dst, true); + xhr.open('POST', f[0].src + '?move=' + dst + '&akey=' + abrt_key, true); xhr.onload = xhr.onerror = rename_cb; xhr.send(); } @@ -13267,6 +13299,8 @@ var fileman = (function () { srcdir = vsplit(r.clip[0])[0], links = QSA('#files tbody td:nth-child(2) a'); + r.f = f; + for (var a = 0, aa = links.length; a < aa; a++) indir.push(uricom_dec(vsplit(noq_href(links[a]))[1])); @@ -13298,13 +13332,17 @@ var fileman = (function () { if (!t.dst) return paster(); - toast.show('inf r', 0, esc((r.ccp ? L.fcp_busy : L.fp_busy).format(f.length + 1, uricom_dec(t.src)))); + var msg = esc((r.ccp ? L.fcp_busy : L.fp_busy).format(f.length + 1, uricom_dec(t.src))); + msg += '\n<a id="fs_abrt" class="btn" href="#" onclick="fs_abrt()">' + L.fs_abrt + '</a>'; + toast.show('inf r', 0, msg); var xhr = new XHR(), act = r.ccp ? '?copy=' : '?move=', dst = get_evpath() + uricom_enc(t.dst); - xhr.open('POST', t.src + act + dst, true); + abrt_key = randstr(9); + + xhr.open('POST', t.src + act + dst + '&akey=' + abrt_key, true); xhr.onload = xhr.onerror = paste_cb; xhr.send(); } diff --git a/tests/util.py b/tests/util.py index f02f81ba..bbd0b215 100644 --- a/tests/util.py +++ b/tests/util.py @@ -143,7 +143,7 @@ class Cfg(Namespace): def __init__(self, a=None, v=None, c=None, **ka0): ka = {} - ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only ih ihead localtime magic nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_fnugg no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz reflink rmagic rss smb srch_dbg srch_excl stats uqe usernames vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs" + ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only ih ihead localtime magic nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_fnugg no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz reflink rmagic rss smb srch_dbg srch_excl stats uqe usernames vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs" ka.update(**{k: False for k in ex.split()}) ex = "dav_inf 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 see_dots plain_ip"