diff --git a/README.md b/README.md index 3c020c19..4fa5ff36 100644 --- a/README.md +++ b/README.md @@ -428,7 +428,7 @@ configuring accounts/volumes with arguments: permissions: * `r` (read): browse folder contents, download files, download as zip/tar, see filekeys/dirkeys -* `w` (write): upload files, move files *into* this folder +* `w` (write): upload files, move/copy files *into* this folder * `m` (move): move files/folders *from* this folder * `d` (delete): delete files/folders * `.` (dots): user can ask to show dotfiles in directory listings @@ -508,7 +508,8 @@ the browser has the following hotkeys (always qwerty) * `ESC` close various things * `ctrl-K` delete selected files/folders * `ctrl-X` cut selected files/folders -* `ctrl-V` paste +* `ctrl-C` copy selected files/folders to clipboard +* `ctrl-V` paste (move/copy) * `Y` download selected files * `F2` [rename](#batch-rename) selected file/folder * when a file/folder is selected (in not-grid-view): @@ -757,10 +758,11 @@ file selection: click somewhere on the line (not the link itself), then: * shift-click another line for range-select * cut: select some files and `ctrl-x` +* copy: select some files and `ctrl-c` * paste: `ctrl-v` in another folder * rename: `F2` -you can move files across browser tabs (cut in one tab, paste in another) +you can copy/move files across browser tabs (cut/copy in one tab, paste in another) ## shares diff --git a/bin/hooks/README.md b/bin/hooks/README.md index f79de79e..82ef3d7e 100644 --- a/bin/hooks/README.md +++ b/bin/hooks/README.md @@ -2,7 +2,7 @@ standalone programs which are executed by copyparty when an event happens (uploa these programs either take zero arguments, or a filepath (the affected file), or a json message with filepath + additional info -run copyparty with `--help-hooks` for usage details / hook type explanations (xm/xbu/xau/xiu/xbr/xar/xbd/xad/xban) +run copyparty with `--help-hooks` for usage details / hook type explanations (xm/xbu/xau/xiu/xbc/xac/xbr/xar/xbd/xad/xban) > **note:** in addition to event hooks (the stuff described here), copyparty has another api to run your programs/scripts while providing way more information such as audio tags / video codecs / etc and optionally daisychaining data between scripts in a processing pipeline; if that's what you want then see [mtp plugins](../mtag/) instead diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 05336755..19ba327c 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -684,6 +684,8 @@ def get_sects(): \033[36mxbu\033[35m executes CMD before a file upload starts \033[36mxau\033[35m executes CMD after a file upload finishes \033[36mxiu\033[35m executes CMD after all uploads finish and volume is idle + \033[36mxbc\033[35m executes CMD before a file copy + \033[36mxac\033[35m executes CMD after a file copy \033[36mxbr\033[35m executes CMD before a file rename/move \033[36mxar\033[35m executes CMD after a file rename/move \033[36mxbd\033[35m executes CMD before a file delete @@ -1201,6 +1203,8 @@ def add_hooks(ap): ap2.add_argument("--xbu", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file upload starts") ap2.add_argument("--xau", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file upload finishes") ap2.add_argument("--xiu", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after all uploads finish and volume is idle") + ap2.add_argument("--xbc", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file copy") + ap2.add_argument("--xac", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file copy") ap2.add_argument("--xbr", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file move/rename") ap2.add_argument("--xar", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file move/rename") ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file delete") @@ -1233,6 +1237,7 @@ def add_optouts(ap): ap2.add_argument("--no-dav", action="store_true", help="disable webdav support") 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("-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/authsrv.py b/copyparty/authsrv.py index 86a42206..84bd19ec 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -673,6 +673,10 @@ class VFS(object): """ recursively yields from ./rem; rel is a unix-style user-defined vpath (not vfs-related) + + NOTE: don't invoke this function from a dbv; subvols are only + descended into if rem is blank due to the _ls `if not rem:` + which intention is to prevent unintended access to subvols """ fsroot, vfs_ls, vfs_virt = self.ls(rem, uname, scandir, permsets, lstat=lstat) @@ -1383,7 +1387,7 @@ class AuthSrv(object): flags[name] = True return - zs = "mtp on403 on404 xbu xau xiu xbr xar xbd xad xm xban" + zs = "mtp on403 on404 xbu xau xiu xbc xac xbr xar xbd xad xm xban" if name not in zs.split(): if value is True: t = "└─add volflag [{}] = {} ({})" @@ -1938,7 +1942,7 @@ class AuthSrv(object): vol.flags[k] = odfusion(getattr(self.args, k), vol.flags[k]) # append additive args from argv to volflags - hooks = "xbu xau xiu xbr xar xbd xad xm xban".split() + hooks = "xbu xau xiu xbc xac xbr xar xbd xad xm xban".split() for name in "mtp on404 on403".split() + hooks: self._read_volflag(vol.flags, name, getattr(self.args, name), True) @@ -2641,7 +2645,7 @@ class AuthSrv(object): ] csv = set("i p th_covers zm_on zm_off zs_on zs_off".split()) - zs = "c ihead ohead mtm mtp on403 on404 xad xar xau xiu xban xbd xbr xbu xm" + zs = "c ihead ohead mtm mtp on403 on404 xac xad xar xau xiu xban xbc xbd xbr xbu xm" lst = set(zs.split()) askip = set("a v c vc cgen exp_lg exp_md theme".split()) fskip = set("exp_lg exp_md mv_re_r mv_re_t rm_re_r rm_re_t".split()) diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 3ad2d792..56d533e7 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -103,10 +103,12 @@ def vf_cmap() -> dict[str, str]: "mte", "mth", "mtp", + "xac", "xad", "xar", "xau", "xban", + "xbc", "xbd", "xbr", "xbu", @@ -212,6 +214,8 @@ flagcats = { "xbu=CMD": "execute CMD before a file upload starts", "xau=CMD": "execute CMD after a file upload finishes", "xiu=CMD": "execute CMD after all uploads finish and volume is idle", + "xbc=CMD": "execute CMD before a file copy", + "xac=CMD": "execute CMD after a file copy", "xbr=CMD": "execute CMD before a file rename/move", "xar=CMD": "execute CMD after a file rename/move", "xbd=CMD": "execute CMD before a file delete", diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 075dba1b..43eb3d68 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -637,7 +637,7 @@ class HttpCli(object): avn.can_access("", self.uname) if avn else [False] * 8 ) self.avn = avn - self.vn = vn + self.vn = vn # note: do not dbv due to walk/zipgen self.rem = rem self.s.settimeout(self.args.s_tbody or None) @@ -1196,6 +1196,9 @@ class HttpCli(object): if "move" in self.uparam: return self.handle_mv() + if "copy" in self.uparam: + return self.handle_cp() + if not self.vpath and self.ouparam: if "reload" in self.uparam: return self.handle_reload() @@ -1791,6 +1794,9 @@ class HttpCli(object): if "move" in self.uparam: return self.handle_mv() + if "copy" in self.uparam: + return self.handle_cp() + if "delete" in self.uparam: return self.handle_rm([]) @@ -5021,16 +5027,39 @@ class HttpCli(object): return self._mv(self.vpath, dst.lstrip("/")) def _mv(self, vsrc: str, vdst: str) -> bool: - if not self.can_move: - raise Pebkac(403, "not allowed for user " + self.uname) - if self.args.no_mv: raise Pebkac(403, "the rename/move feature is disabled in server config") + self.asrv.vfs.get(vsrc, self.uname, True, False, True) + self.asrv.vfs.get(vdst, self.uname, False, True) + x = self.conn.hsrv.broker.ask("up2k.handle_mv", self.uname, self.ip, vsrc, vdst) self.loud_reply(x.get(), status=201) return True + def handle_cp(self) -> bool: + # full path of new loc (incl filename) + dst = self.uparam.get("copy") + + if self.is_vproxied and dst and dst.startswith(self.args.SR): + dst = dst[len(self.args.RS) :] + + if not dst: + raise Pebkac(400, "need dst vpath") + + return self._cp(self.vpath, dst.lstrip("/")) + + def _cp(self, vsrc: str, vdst: str) -> bool: + if self.args.no_cp: + raise Pebkac(403, "the copy feature is disabled in server config") + + self.asrv.vfs.get(vsrc, self.uname, True, False) + self.asrv.vfs.get(vdst, self.uname, False, True) + + x = self.conn.hsrv.broker.ask("up2k.handle_cp", self.uname, self.ip, vsrc, vdst) + self.loud_reply(x.get(), status=201) + return True + def tx_ls(self, ls: dict[str, Any]) -> bool: dirs = ls["dirs"] files = ls["files"] diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 28ed66f2..e9056846 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -1464,7 +1464,7 @@ class Up2k(object): t = "failed to index subdir [{}]:\n{}" self.log(t.format(abspath, min_ex()), c=1) elif not stat.S_ISREG(inf.st_mode): - self.log("skip type-{:x} file [{}]".format(inf.st_mode, abspath)) + self.log("skip type-0%o file [%s]" % (inf.st_mode, abspath)) else: # self.log("file: {}".format(abspath)) if rp.endswith(".PARTIAL") and time.time() - lmod < 60: @@ -3896,13 +3896,13 @@ class Up2k(object): partial = "" if not unpost: permsets = [[True, False, False, True]] - vn, rem = self.vfs.get(vpath, uname, *permsets[0]) - vn, rem = vn.get_dbv(rem) + vn0, rem0 = self.vfs.get(vpath, uname, *permsets[0]) + vn, rem = vn0.get_dbv(rem0) else: # unpost with missing permissions? verify with db permsets = [[False, True]] - vn, rem = self.vfs.get(vpath, uname, *permsets[0]) - vn, rem = vn.get_dbv(rem) + vn0, rem0 = self.vfs.get(vpath, uname, *permsets[0]) + vn, rem = vn0.get_dbv(rem0) ptop = vn.realpath with self.mutex, self.reg_mutex: abrt_cfg = self.flags.get(ptop, {}).get("u2abort", 1) @@ -3958,7 +3958,9 @@ class Up2k(object): scandir = not self.args.no_scandir if is_dir: - g = vn.walk("", rem, [], uname, permsets, True, scandir, True) + # note: deletion inside shares would require a rewrite here; + # shares necessitate get_dbv which is incompatible with walk + g = vn0.walk("", rem0, [], uname, permsets, True, scandir, True) if unpost: raise Pebkac(400, "cannot unpost folders") elif stat.S_ISLNK(st.st_mode) or stat.S_ISREG(st.st_mode): @@ -3966,7 +3968,7 @@ class Up2k(object): vpath_dir = vsplit(vpath)[0] g = [(vn, voldir, vpath_dir, adir, [(fn, 0)], [], {})] # type: ignore else: - self.log("rm: skip type-{:x} file [{}]".format(st.st_mode, atop)) + self.log("rm: skip type-0%o file [%s]" % (st.st_mode, atop)) return 0, [], [] xbd = vn.flags.get("xbd") @@ -4066,17 +4068,226 @@ class Up2k(object): return n_files, ok + ok2, ng + ng2 + def handle_cp(self, 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") + + svn, srem = self.vfs.get(svp, uname, True, False) + svn_dbv, _ = svn.get_dbv(srem) + sabs = svn.canonical(srem, False) + curs: set["sqlite3.Cursor"] = set() + self.db_act = self.vol_act[svn_dbv.realpath] = time.time() + + st = bos.stat(sabs) + if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode): + with self.mutex: + try: + ret = self._cp_file(uname, ip, svp, dvp, curs) + finally: + for v in curs: + v.connection.commit() + + return ret + + if not stat.S_ISDIR(st.st_mode): + raise Pebkac(400, "cannot copy type-0%o file" % (st.st_mode,)) + + permsets = [[True, False]] + scandir = not self.args.no_scandir + + # don't use svn_dbv; would skip subvols due to _ls `if not rem:` + g = svn.walk("", srem, [], uname, permsets, True, scandir, True) + with self.mutex: + try: + for dbv, vrem, _, atop, files, rd, vd in g: + for fn in files: + self.db_act = self.vol_act[dbv.realpath] = time.time() + svpf = "/".join(x for x in [dbv.vpath, vrem, fn[0]] if x) + if not svpf.startswith(svp + "/"): # assert + self.log(min_ex(), 1) + t = "cp: bug at %s, top %s%s" + raise Pebkac(500, t % (svpf, svp, SEESLOG)) + + dvpf = dvp + svpf[len(svp) :] + self._cp_file(uname, ip, svpf, dvpf, curs) + + for v in curs: + v.connection.commit() + curs.clear() + finally: + for v in curs: + v.connection.commit() + + return "k" + + def _cp_file( + self, uname: str, ip: str, svp: str, dvp: str, curs: set["sqlite3.Cursor"] + ) -> str: + """mutex(main) me; will mutex(reg)""" + svn, srem = self.vfs.get(svp, uname, True, False) + svn_dbv, srem_dbv = svn.get_dbv(srem) + + dvn, drem = self.vfs.get(dvp, uname, False, True) + dvn, drem = dvn.get_dbv(drem) + + sabs = svn.canonical(srem, False) + dabs = dvn.canonical(drem) + drd, dfn = vsplit(drem) + + if bos.path.exists(dabs): + raise Pebkac(400, "cp2: target file exists") + + st = stl = bos.lstat(sabs) + if stat.S_ISLNK(stl.st_mode): + is_link = True + try: + st = bos.stat(sabs) + except: + pass # broken symlink; keep as-is + elif not stat.S_ISREG(st.st_mode): + self.log("skipping type-0%o file [%s]" % (st.st_mode, sabs)) + return "" + else: + is_link = False + + ftime = stl.st_mtime + fsize = st.st_size + + xbc = svn.flags.get("xbc") + xac = dvn.flags.get("xac") + if xbc: + if not runhook( + self.log, + None, + self, + "xbc", + xbc, + sabs, + svp, + "", + uname, + self.vfs.get_perms(svp, uname), + ftime, + fsize, + ip, + time.time(), + "", + ): + t = "copy blocked by xbr server config: {}".format(svp) + self.log(t, 1) + raise Pebkac(405, t) + + bos.makedirs(os.path.dirname(dabs)) + + c1, w, ftime_, fsize_, ip, at = self._find_from_vpath( + svn_dbv.realpath, srem_dbv + ) + c2 = self.cur.get(dvn.realpath) + + if w: + assert c1 # !rm + if c2 and c2 != c1: + self._copy_tags(c1, c2, w) + + curs.add(c1) + + if c2: + self.db_add( + c2, + {}, # skip upload hooks + drd, + dfn, + ftime, + fsize, + dvn.realpath, + dvn.vpath, + w, + w, + "", + "", + ip or "", + at or 0, + ) + curs.add(c2) + else: + self.log("not found in src db: [{}]".format(svp)) + + try: + if is_link and st != stl: + # relink non-broken symlinks to still work after the move, + # but only resolve 1st level to maintain relativity + dlink = bos.readlink(sabs) + dlink = os.path.join(os.path.dirname(sabs), dlink) + dlink = bos.path.abspath(dlink) + self._symlink(dlink, dabs, dvn.flags, lmod=ftime) + else: + self._symlink(sabs, dabs, dvn.flags, lmod=ftime) + + except OSError as ex: + if ex.errno != errno.EXDEV: + raise + + self.log("using plain copy (%s):\n %s\n %s" % (ex.strerror, sabs, dabs)) + b1, b2 = fsenc(sabs), fsenc(dabs) + is_link = os.path.islink(b1) # due to _relink + try: + shutil.copy2(b1, b2) + except: + try: + wunlink(self.log, dabs, dvn.flags) + except: + pass + + if not is_link: + raise + + # broken symlink? keep it as-is + try: + zb = os.readlink(b1) + os.symlink(zb, b2) + except: + wunlink(self.log, dabs, dvn.flags) + raise + + if is_link: + try: + times = (int(time.time()), int(ftime)) + bos.utime(dabs, times, False) + except: + pass + + if xac: + runhook( + self.log, + None, + self, + "xac", + xac, + dabs, + dvp, + "", + uname, + self.vfs.get_perms(dvp, uname), + ftime, + fsize, + ip, + time.time(), + "", + ) + + return "k" + def handle_mv(self, 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") svn, srem = self.vfs.get(svp, uname, True, False, True) - svn, srem = svn.get_dbv(srem) + jail, jail_rem = svn.get_dbv(srem) sabs = svn.canonical(srem, False) curs: set["sqlite3.Cursor"] = set() - self.db_act = self.vol_act[svn.realpath] = time.time() + self.db_act = self.vol_act[jail.realpath] = time.time() - if not srem: + if not jail_rem: raise Pebkac(400, "mv: cannot move a mountpoint") st = bos.lstat(sabs) @@ -4090,7 +4301,9 @@ class Up2k(object): return ret - jail = svn.get_dbv(srem)[0] + if not stat.S_ISDIR(st.st_mode): + raise Pebkac(400, "cannot move type-0%o file" % (st.st_mode,)) + permsets = [[True, False, True]] scandir = not self.args.no_scandir @@ -4102,13 +4315,13 @@ class Up2k(object): raise Pebkac(400, "mv: source folder contains other volumes") g = svn.walk("", srem, [], uname, permsets, True, scandir, True) - for dbv, vrem, _, atop, files, rd, vd in g: - if dbv != jail: - # the actual check (avoid toctou) - raise Pebkac(400, "mv: source folder contains other volumes") + with self.mutex: + try: + for dbv, vrem, _, atop, files, rd, vd in g: + if dbv != jail: + # the actual check (avoid toctou) + raise Pebkac(400, "mv: source folder contains other volumes") - with self.mutex: - try: for fn in files: self.db_act = self.vol_act[dbv.realpath] = time.time() svpf = "/".join(x for x in [dbv.vpath, vrem, fn[0]] if x) @@ -4119,11 +4332,13 @@ class Up2k(object): dvpf = dvp + svpf[len(svp) :] self._mv_file(uname, ip, svpf, dvpf, curs) - finally: + for v in curs: v.connection.commit() - - curs.clear() + curs.clear() + finally: + for v in curs: + v.connection.commit() rm_ok, rm_ng = rmdirs(self.log_func, scandir, True, sabs, 1) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 75c9e451..15818625 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -37,8 +37,9 @@ var Ls = { ["T", "toggle thumbnails / icons"], ["🡅 A/D", "thumbnail size"], ["ctrl-K", "delete selected"], - ["ctrl-X", "cut selected"], - ["ctrl-V", "paste into folder"], + ["ctrl-X", "cut selection to clipboard"], + ["ctrl-C", "copy selection to clipboard"], + ["ctrl-V", "paste (move/copy) here"], ["Y", "download selected"], ["F2", "rename selected"], @@ -83,7 +84,7 @@ var Ls = { ["I/K", "prev/next file"], ["M", "close textfile"], ["E", "edit textfile"], - ["S", "select file (for cut/rename)"], + ["S", "select file (for cut/copy/rename)"], ] ], @@ -133,6 +134,7 @@ var Ls = { "wt_ren": "rename selected items$NHotkey: F2", "wt_del": "delete selected items$NHotkey: ctrl-K", "wt_cut": "cut selected items <small>(then paste somewhere else)</small>$NHotkey: ctrl-X", + "wt_cpy": "copy selected items to clipboard$N(to paste them somewhere else)$NHotkey: ctrl-C", "wt_pst": "paste a previously cut / copied selection$NHotkey: ctrl-V", "wt_selall": "select all files$NHotkey: ctrl-A (when file focused)", "wt_selinv": "invert selection", @@ -327,6 +329,7 @@ var Ls = { "fr_emore": "select at least one item to rename", "fd_emore": "select at least one item to delete", "fc_emore": "select at least one item to cut", + "fcp_emore": "select at least one item to copy to clipboard", "fs_sc": "share the folder you're in", "fs_ss": "share the selected files", @@ -379,16 +382,26 @@ var Ls = { "fc_ok": "cut {0} items", "fc_warn": 'cut {0} items\n\nbut: only <b>this</b> browser-tab can paste them\n(since the selection is so absolutely massive)', - "fp_ecut": "first cut some files / folders to paste / move\n\nnote: you can cut / paste across different browser tabs", + "fcc_ok": "copied {0} items to clipboard", + "fcc_warn": 'copied {0} items to clipboard\n\nbut: only <b>this</b> browser-tab can paste them\n(since the selection is so absolutely massive)', + + "fp_ecut": "first cut or copy some files / folders to paste / move\n\nnote: you can cut / paste across different browser tabs", "fp_ename": "these {0} items cannot be moved here (names already exist):", + "fcp_ename": "these {0} items cannot be copied here (names already exist):", "fp_ok": "move OK", + "fcp_ok": "copy OK", "fp_busy": "moving {0} items...\n\n{1}", + "fcp_busy": "copying {0} items...\n\n{1}", "fp_err": "move failed:\n", + "fcp_err": "copy failed:\n", "fp_confirm": "move these {0} items here?", + "fcp_confirm": "copy these {0} items here?", "fp_etab": 'failed to read clipboard from other browser tab', "fp_name": "uploading a file from your device. Give it a name:", "fp_both_m": '<h6>choose what to paste</h6><code>Enter</code> = Move {0} files from «{1}»\n<code>ESC</code> = Upload {2} files from your device', + "fcp_both_m": '<h6>choose what to paste</h6><code>Enter</code> = Copy {0} files from «{1}»\n<code>ESC</code> = Upload {2} files from your device', "fp_both_b": '<a href="#" id="modal-ok">Move</a><a href="#" id="modal-ng">Upload</a>', + "fcp_both_b": '<a href="#" id="modal-ok">Copy</a><a href="#" id="modal-ng">Upload</a>', "mk_noname": "type a name into the text field on the left before you do that :p", @@ -400,7 +413,7 @@ var Ls = { "tvt_dl": "download this file$NHotkey: Y\">💾 download", "tvt_prev": "show previous document$NHotkey: i\">⬆ prev", "tvt_next": "show next document$NHotkey: K\">⬇ next", - "tvt_sel": "select file   ( for cut / delete / ... )$NHotkey: S\">sel", + "tvt_sel": "select file   ( for cut / copy / delete / ... )$NHotkey: S\">sel", "tvt_edit": "open file in text editor$NHotkey: E\">✏️ edit", "gt_vau": "don't show videos, just play the audio\">🎧", @@ -605,8 +618,9 @@ var Ls = { ["T", "miniatyrbilder på/av"], ["🡅 A/D", "ikonstørrelse"], ["ctrl-K", "slett valgte"], - ["ctrl-X", "klipp ut"], - ["ctrl-V", "lim inn"], + ["ctrl-X", "klipp ut valgte"], + ["ctrl-C", "kopiér til utklippstavle"], + ["ctrl-V", "lim inn (flytt/kopiér)"], ["Y", "last ned valgte"], ["F2", "endre navn på valgte"], @@ -702,7 +716,8 @@ var Ls = { "wt_ren": "gi nye navn til de valgte filene$NSnarvei: F2", "wt_del": "slett de valgte filene$NSnarvei: ctrl-K", "wt_cut": "klipp ut de valgte filene <small>(for å lime inn et annet sted)</small>$NSnarvei: ctrl-X", - "wt_pst": "lim inn filer (som tidligere ble klippet ut et annet sted)$NSnarvei: ctrl-V", + "wt_cpy": "kopiér de valgte filene til utklippstavlen$N(for å lime inn et annet sted)$NSnarvei: ctrl-C", + "wt_pst": "lim inn filer (som tidligere ble klippet ut / kopiert et annet sted)$NSnarvei: ctrl-V", "wt_selall": "velg alle filer$NSnarvei: ctrl-A (mens fokus er på en fil)", "wt_selinv": "inverter utvalg", "wt_selzip": "last ned de valgte filene som et arkiv", @@ -845,7 +860,7 @@ var Ls = { "mt_oscv": "vis album-cover på infoskjermen\">bilde", "mt_follow": "bla slik at sangen som spilles alltid er synlig\">🎯", "mt_compact": "tettpakket avspillerpanel\">⟎", - "mt_uncache": "prøv denne hvis en sang ikke spiller riktig\">uncache", + "mt_uncache": "prøv denne hvis en sang ikke spiller riktig\">oppfrisk", "mt_mloop": "repeter hele mappen\">🔁 gjenta", "mt_mnext": "hopp til neste mappe og fortsett\">📂 neste", "mt_cflac": "konverter flac / wav-filer til opus\">flac", @@ -896,6 +911,7 @@ var Ls = { "fr_emore": "velg minst én fil som skal få nytt navn", "fd_emore": "velg minst én fil som skal slettes", "fc_emore": "velg minst én fil som skal klippes ut", + "fcp_emore": "velg minst én fil som skal kopieres til utklippstavlen", "fs_sc": "del mappen du er i nå", "fs_ss": "del de valgte filene", @@ -948,16 +964,26 @@ var Ls = { "fc_ok": "klippet ut {0} filer", "fc_warn": 'klippet ut {0} filer\n\nmen: kun <b>denne</b> nettleserfanen har mulighet til å lime dem inn et annet sted, siden antallet filer er helt hinsides', - "fp_ecut": "du må klippe ut noen filer / mapper først\n\nmerk: du kan gjerne jobbe på kryss av nettleserfaner; klippe ut i én fane, lime inn i en annen", + "fcc_ok": "kopierte {0} filer til utklippstavlen", + "fcc_warn": 'kopierte {0} filer til utklippstavlen\n\nmen: kun <b>denne</b> nettleserfanen har mulighet til å lime dem inn et annet sted, siden antallet filer er helt hinsides', + + "fp_ecut": "du må klippe ut eller kopiere noen filer / mapper først\n\nmerk: du kan gjerne jobbe på kryss av nettleserfaner; klippe ut i én fane, lime inn i en annen", "fp_ename": "disse {0} filene kan ikke flyttes til målmappen fordi det allerede finnes filer med samme navn:", + "fcp_ename": "disse {0} filene kan ikke kopieres til målmappen fordi det allerede finnes filer med samme navn:", "fp_ok": "flytting OK", + "fcp_ok": "kopiering OK", "fp_busy": "flytter {0} filer...\n\n{1}", + "fcp_busy": "kopierer {0} filer...\n\n{1}", "fp_err": "flytting feilet:\n", + "fcp_err": "kopiering feilet:\n", "fp_confirm": "flytt disse {0} filene hit?", + "fcp_confirm": "kopiér disse {0} filene hit?", "fp_etab": 'kunne ikke lese listen med filer ifra den andre nettleserfanen', "fp_name": "Laster opp én fil fra enheten din. Velg filnavn:", "fp_both_m": '<h6>hva skal limes inn her?</h6><code>Enter</code> = Flytt {0} filer fra «{1}»\n<code>ESC</code> = Last opp {2} filer fra enheten din', + "fcp_both_m": '<h6>hva skal limes inn her?</h6><code>Enter</code> = Kopiér {0} filer fra «{1}»\n<code>ESC</code> = Last opp {2} filer fra enheten din', "fp_both_b": '<a href="#" id="modal-ok">Flytt</a><a href="#" id="modal-ng">Last opp</a>', + "fcp_both_b": '<a href="#" id="modal-ok">Kopiér</a><a href="#" id="modal-ng">Last opp</a>', "mk_noname": "skriv inn et navn i tekstboksen til venstre først :p", @@ -1176,6 +1202,7 @@ var Ls = { ["🡅 A/D", "缩略图大小"], ["ctrl-K", "删除选中项"], ["ctrl-X", "剪切选中项"], + ["ctrl-C", "复制选中项"], //m ["ctrl-V", "粘贴到文件夹"], ["Y", "下载选中项"], ["F2", "重命名选中项"], @@ -1271,6 +1298,7 @@ var Ls = { "wt_ren": "重命名选中的项目$N快捷键: F2", "wt_del": "删除选中的项目$N快捷键: ctrl-K", "wt_cut": "剪切选中的项目<small>(然后粘贴到其他地方)</small>$N快捷键: ctrl-X", + "wt_cpy": "将选中的项目复制到剪贴板<small>(然后粘贴到其他地方)</small>$N快捷键: ctrl-C", //m "wt_pst": "粘贴之前剪切/复制的选择$N快捷键: ctrl-V", "wt_selall": "选择所有文件$N快捷键: ctrl-A(当文件被聚焦时)", "wt_selinv": "反转选择", @@ -1465,6 +1493,7 @@ var Ls = { "fr_emore": "选择至少一个项目以重命名", "fd_emore": "选择至少一个项目以删除", "fc_emore": "选择至少一个项目以剪切", + "fcp_emore": "选择至少一个要复制到剪贴板的项目", //m "fs_sc": "分享你所在的文件夹", "fs_ss": "分享选定的文件", @@ -1517,16 +1546,26 @@ var Ls = { "fc_ok": "剪切 {0} 项", "fc_warn": '剪切 {0} 项\n\n但:只有 <b>这个</b> 浏览器标签页可以粘贴它们\n(因为选择非常庞大)', - "fp_ecut": "首先剪切一些文件/文件夹以粘贴/移动\n\n注意:你可以在不同的浏览器标签页之间剪切/粘贴", + "fcc_ok": "已将 {0} 项复制到剪贴板", //m + "fcc_warn": '已将 {0} 项复制到剪贴板\n\n但:只有 <b>这个</b> 浏览器标签页可以粘贴它们\n(因为选择非常庞大)', //m + + "fp_ecut": "首先剪切或复制一些文件/文件夹以粘贴/移动\n\n注意:你可以在不同的浏览器标签页之间剪切/粘贴", //m "fp_ename": "这些 {0} 项不能移动到这里(名称已存在):", + "fcp_ename": "这些 {0} 项不能复制到这里(名称已存在):", //m "fp_ok": "移动成功", + "fcp_ok": "复制成功", //m "fp_busy": "正在移动 {0} 项...\n\n{1}", + "fcp_busy": "正在复制 {0} 项...\n\n{1}", //m "fp_err": "移动失败:\n", + "fcp_err": "复制失败:\n", //m "fp_confirm": "将这些 {0} 项移动到这里?", + "fcp_confirm": "将这些 {0} 项复制到这里?", //m "fp_etab": '无法从其他浏览器标签页读取剪贴板', "fp_name": "从你的设备上传一个文件。给它一个名字:", "fp_both_m": '<h6>选择粘贴内容</h6><code>Enter</code> = 从 «{1}» 移动 {0} 个文件\n<code>ESC</code> = 从你的设备上传 {2} 个文件', + "fcp_both_m": '<h6>选择粘贴内容</h6><code>Enter</code> = 从 «{1}» 复制 {0} 个文件\n<code>ESC</code> = 从你的设备上传 {2} 个文件', //m "fp_both_b": '<a href="#" id="modal-ok">移动</a><a href="#" id="modal-ng">上传</a>', + "fcp_both_b": '<a href="#" id="modal-ok">复制</a><a href="#" id="modal-ng">上传</a>', //m "mk_noname": "在左侧文本框中输入名称,然后再执行此操作 :p", @@ -1771,6 +1810,7 @@ ebi('widget').innerHTML = ( ' href="#" id="fren" tt="' + L.wt_ren + '">✎<span>name</span></a><a' + ' href="#" id="fdel" tt="' + L.wt_del + '">⌫<span>del.</span></a><a' + ' href="#" id="fcut" tt="' + L.wt_cut + '">✂<span>cut</span></a><a' + + ' href="#" id="fcpy" tt="' + L.wt_cpy + '">⧉<span>copy</span></a><a' + ' href="#" id="fpst" tt="' + L.wt_pst + '">📋<span>paste</span></a>' + '</span><span id="wzip"><a' + ' href="#" id="selall" tt="' + L.wt_selall + '">sel.<br />all</a><a' + @@ -4377,6 +4417,7 @@ var fileman = (function () { var bren = ebi('fren'), bdel = ebi('fdel'), bcut = ebi('fcut'), + bcpy = ebi('fcpy'), bpst = ebi('fpst'), bshr = ebi('fshr'), t_paste, @@ -4389,14 +4430,19 @@ var fileman = (function () { catch (ex) { } r.render = function () { - if (r.clip === null) + if (r.clip === null) { r.clip = jread('fman_clip', []).slice(1); + r.ccp = r.clip.length && r.clip[0] == '//c'; + if (r.ccp) + r.clip.shift(); + } var sel = msel.getsel(), nsel = sel.length, enren = nsel, endel = nsel, encut = nsel, + encpy = nsel, enpst = r.clip && r.clip.length, hren = !(have_mv && has(perms, 'write') && has(perms, 'move')), hdel = !(have_del && has(perms, 'delete')), @@ -4410,6 +4456,7 @@ var fileman = (function () { clmod(bren, 'en', enren); clmod(bdel, 'en', endel); clmod(bcut, 'en', encut); + clmod(bcpy, 'en', encpy); clmod(bpst, 'en', enpst); clmod(bshr, 'en', 1); @@ -5015,7 +5062,8 @@ var fileman = (function () { r.cut = function (e) { ev(e); var sel = msel.getsel(), - vps = []; + stamp = Date.now(), + vps = [stamp]; if (!sel.length) return toast.err(3, L.fc_emore); @@ -5046,9 +5094,11 @@ var fileman = (function () { catch (ex) { } }, 1); + r.ccp = false; + r.clip = vps.slice(1); + try { - var stamp = Date.now(); - vps = JSON.stringify([stamp].concat(vps)); + vps = JSON.stringify(vps); if (vps.length > 1024 * 1024) throw 'a'; @@ -5062,6 +5112,59 @@ var fileman = (function () { } }; + r.cpy = function (e) { + ev(e); + var sel = msel.getsel(), + stamp = Date.now(), + vps = [stamp, '//c']; + + if (!sel.length) + return toast.err(3, L.fcp_emore); + + var els = [], griden = thegrid.en; + for (var a = 0; a < sel.length; a++) { + vps.push(sel[a].vp); + if (sel.length < 100) + try { + if (griden) + els.push(QS('#ggrid>a[ref="' + sel[a].id + '"]')); + else + els.push(ebi(sel[a].id).closest('tr')); + + clmod(els[a], 'fcut'); + } + catch (ex) { } + } + + setTimeout(function () { + try { + for (var a = 0; a < els.length; a++) + clmod(els[a], 'fcut', 1); + } + catch (ex) { } + }, 1); + + if (vps.length < 3) + vps.pop(); + + r.ccp = true; + r.clip = vps.slice(2); + + try { + vps = JSON.stringify(vps); + if (vps.length > 1024 * 1024) + throw 'a'; + + swrite('fman_clip', vps); + r.tx(stamp); + if (sel.length) + toast.inf(1.5, L.fcc_ok.format(sel.length)); + } + catch (ex) { + toast.warn(30, L.fcc_warn.format(sel.length)); + } + }; + document.onpaste = function (e) { var xfer = e.clipboardData || window.clipboardData; if (!xfer || !xfer.files || !xfer.files.length) @@ -5077,9 +5180,9 @@ var fileman = (function () { return r.clip_up(files); var src = r.clip.length == 1 ? r.clip[0] : vsplit(r.clip[0])[0], - msg = L.fp_both_m.format(r.clip.length, src, files.length); + msg = (r.ccp ? L.fcp_both_m : L.fp_both_m).format(r.clip.length, src, files.length); - modal.confirm(msg, r.paste, function () { r.clip_up(files); }, null, L.fp_both_b); + modal.confirm(msg, r.paste, function () { r.clip_up(files); }, null, (r.ccp ? L.fcp_both_b : L.fp_both_b)); }; r.clip_up = function (files) { @@ -5157,7 +5260,7 @@ var fileman = (function () { } if (exists.length) - toast.warn(30, L.fp_ename.format(exists.length) + '<ul>' + uricom_adec(exists, true).join('') + '</ul>'); + toast.warn(30, (r.ccp ? L.fcp_ename : L.fp_ename).format(exists.length) + '<ul>' + uricom_adec(exists, true).join('') + '</ul>'); if (!req.length) return; @@ -5167,29 +5270,30 @@ var fileman = (function () { vp = req.shift(); if (!vp) { - toast.ok(2, L.fp_ok); + toast.ok(2, r.ccp ? L.fcp_ok : L.fp_ok); treectl.goto(); r.tx(srcdir); return; } - toast.show('inf r', 0, esc(L.fp_busy.format(req.length + 1, uricom_dec(vp)))); + toast.show('inf r', 0, esc((r.ccp ? L.fcp_busy : L.fp_busy).format(req.length + 1, uricom_dec(vp)))); - var dst = get_evpath() + vp.split('/').pop(); + var act = r.ccp ? '?copy=' : '?move=', + dst = get_evpath() + vp.split('/').pop(); - xhr.open('POST', vp + '?move=' + dst, true); + xhr.open('POST', vp + act + dst, true); xhr.onload = xhr.onerror = paste_cb; xhr.send(); } function paste_cb() { if (this.status !== 201) { var msg = unpre(this.responseText); - toast.err(9, L.fp_err + msg); + toast.err(9, (r.ccp ? L.fcp_err : L.fp_err) + msg); return; } paster(); } - modal.confirm(L.fp_confirm.format(req.length) + '<ul>' + uricom_adec(req, true).join('') + '</ul>', function () { + modal.confirm((r.ccp ? L.fcp_confirm : L.fp_confirm).format(req.length) + '<ul>' + uricom_adec(req, true).join('') + '</ul>', function () { paster(); jwrite('fman_clip', [Date.now()]); }, null); @@ -5231,6 +5335,7 @@ var fileman = (function () { bren.onclick = r.rename; bdel.onclick = r.delete; bcut.onclick = r.cut; + bcpy.onclick = r.cpy; bpst.onclick = r.paste; bshr.onclick = r.share; @@ -6326,9 +6431,15 @@ var ahotkeys = function (e) { return hkhelp(); if (ctrl(e)) { + var sel = window.getSelection && window.getSelection() || {}; + sel = sel && !sel.isCollapsed && sel.direction != 'none'; + if (k == 'KeyX' || k == 'x') return fileman.cut(); + if ((k == 'KeyC' || k == 'c') && !sel) + return fileman.cpy(); + if (k == 'KeyV' || k == 'v') return fileman.d_paste(); diff --git a/docs/devnotes.md b/docs/devnotes.md index 71f3bba8..6d39704a 100644 --- a/docs/devnotes.md +++ b/docs/devnotes.md @@ -163,6 +163,7 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo` | method | params | result | |--|--|--| +| POST | `?copy=/foo/bar` | copy the file/folder at URL to /foo/bar | | POST | `?move=/foo/bar` | move/rename the file/folder at URL to /foo/bar | | method | params | body | result | diff --git a/scripts/tl.js b/scripts/tl.js index aa1f5bd1..22cde3fa 100644 --- a/scripts/tl.js +++ b/scripts/tl.js @@ -121,8 +121,9 @@ var tl_browser = { ["T", "toggle thumbnails / icons"], ["🡅 A/D", "thumbnail size"], ["ctrl-K", "delete selected"], - ["ctrl-X", "cut selected"], - ["ctrl-V", "paste into folder"], + ["ctrl-X", "cut selection to clipboard"], + ["ctrl-C", "copy selection to clipboard"], + ["ctrl-V", "paste (move/copy) here"], ["Y", "download selected"], ["F2", "rename selected"], @@ -167,7 +168,7 @@ var tl_browser = { ["I/K", "prev/next file"], ["M", "close textfile"], ["E", "edit textfile"], - ["S", "select file (for cut/rename)"], + ["S", "select file (for cut/copy/rename)"], ] ], @@ -217,6 +218,7 @@ var tl_browser = { "wt_ren": "rename selected items$NHotkey: F2", "wt_del": "delete selected items$NHotkey: ctrl-K", "wt_cut": "cut selected items <small>(then paste somewhere else)</small>$NHotkey: ctrl-X", + "wt_cpy": "copy selected items to clipboard$N(to paste them somewhere else)$NHotkey: ctrl-C", "wt_pst": "paste a previously cut / copied selection$NHotkey: ctrl-V", "wt_selall": "select all files$NHotkey: ctrl-A (when file focused)", "wt_selinv": "invert selection", @@ -411,6 +413,7 @@ var tl_browser = { "fr_emore": "select at least one item to rename", "fd_emore": "select at least one item to delete", "fc_emore": "select at least one item to cut", + "fcp_emore": "select at least one item to copy", "fs_sc": "share the folder you're in", "fs_ss": "share the selected files", @@ -463,16 +466,26 @@ var tl_browser = { "fc_ok": "cut {0} items", "fc_warn": 'cut {0} items\n\nbut: only <b>this</b> browser-tab can paste them\n(since the selection is so absolutely massive)', - "fp_ecut": "first cut some files / folders to paste / move\n\nnote: you can cut / paste across different browser tabs", + "fcc_ok": "copied {0} items to clipboard", + "fcc_warn": 'copied {0} items to clipboard\n\nbut: only <b>this</b> browser-tab can paste them\n(since the selection is so absolutely massive)', + + "fp_ecut": "first cut or copy some files / folders to paste / move\n\nnote: you can cut / paste across different browser tabs", "fp_ename": "these {0} items cannot be moved here (names already exist):", + "fcp_ename": "these {0} items cannot be copied here (names already exist):", "fp_ok": "move OK", + "fcp_ok": "copy OK", "fp_busy": "moving {0} items...\n\n{1}", + "fcp_busy": "copying {0} items...\n\n{1}", "fp_err": "move failed:\n", + "fcp_err": "copy failed:\n", "fp_confirm": "move these {0} items here?", + "fcp_confirm": "copy these {0} items here?", "fp_etab": 'failed to read clipboard from other browser tab', "fp_name": "uploading a file from your device. Give it a name:", "fp_both_m": '<h6>choose what to paste</h6><code>Enter</code> = Move {0} files from «{1}»\n<code>ESC</code> = Upload {2} files from your device', + "fcp_both_m": '<h6>choose what to paste</h6><code>Enter</code> = Copy {0} files from «{1}»\n<code>ESC</code> = Upload {2} files from your device', "fp_both_b": '<a href="#" id="modal-ok">Move</a><a href="#" id="modal-ng">Upload</a>', + "fcp_both_b": '<a href="#" id="modal-ok">Copy</a><a href="#" id="modal-ng">Upload</a>', "mk_noname": "type a name into the text field on the left before you do that :p", @@ -484,7 +497,7 @@ var tl_browser = { "tvt_dl": "download this file$NHotkey: Y\">💾 download", "tvt_prev": "show previous document$NHotkey: i\">⬆ prev", "tvt_next": "show next document$NHotkey: K\">⬇ next", - "tvt_sel": "select file   ( for cut / delete / ... )$NHotkey: S\">sel", + "tvt_sel": "select file   ( for cut / copy / delete / ... )$NHotkey: S\">sel", "tvt_edit": "open file in text editor$NHotkey: E\">✏️ edit", "gt_vau": "don't show videos, just play the audio\">🎧", diff --git a/tests/test_cp.py b/tests/test_cp.py new file mode 100644 index 00000000..7e0f2010 --- /dev/null +++ b/tests/test_cp.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# coding: utf-8 +from __future__ import print_function, unicode_literals + +import os +import shutil +import tempfile +import unittest +from itertools import product + +from copyparty.authsrv import AuthSrv +from copyparty.httpcli import HttpCli +from tests import util as tu +from tests.util import Cfg + + +class TestDedup(unittest.TestCase): + def setUp(self): + self.td = tu.get_ramdisk() + + def tearDown(self): + os.chdir(tempfile.gettempdir()) + shutil.rmtree(self.td) + + def reset(self): + td = os.path.join(self.td, "vfs") + if os.path.exists(td): + shutil.rmtree(td) + os.mkdir(td) + os.chdir(td) + for a in "abc": + os.mkdir(a) + for b in "fg": + d = "%s/%s%s" % (a, a, b) + os.mkdir(d) + for fn in "x": + fp = "%s/%s%s%s" % (d, a, b, fn) + with open(fp, "wb") as f: + f.write(fp.encode("utf-8")) + return td + + def cinit(self): + if self.conn: + self.fstab = self.conn.hsrv.hub.up2k.fstab + self.conn.hsrv.hub.up2k.shutdown() + self.asrv = AuthSrv(self.args, self.log) + self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b"", True) + if self.fstab: + self.conn.hsrv.hub.up2k.fstab = self.fstab + + def test(self): + tc_dedup = ["sym", "no"] + vols = [".::A", "a/af:a/af:r", "b:a/b:r"] + tcs = [ + "/a?copy=/c/a /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /b/bf/bfx /b/bg/bgx /c/a/af/afx /c/a/ag/agx /c/a/b/bf/bfx /c/a/b/bg/bgx /c/cf/cfx /c/cg/cgx", + "/b?copy=/d /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /b/bf/bfx /b/bg/bgx /c/cf/cfx /c/cg/cgx /d/bf/bfx /d/bg/bgx", + "/b/bf?copy=/d /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /b/bf/bfx /b/bg/bgx /c/cf/cfx /c/cg/cgx /d/bfx", + "/a/af?copy=/d /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /b/bf/bfx /b/bg/bgx /c/cf/cfx /c/cg/cgx /d/afx", + "/a/af?copy=/ /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /afx /b/bf/bfx /b/bg/bgx /c/cf/cfx /c/cg/cgx", + "/a/af/afx?copy=/afx /a/af/afx /a/ag/agx /a/b/bf/bfx /a/b/bg/bgx /afx /b/bf/bfx /b/bg/bgx /c/cf/cfx /c/cg/cgx", + ] + + self.conn = None + self.fstab = None + self.ctr = 0 # 2304 + for dedup, act_exp in product(tc_dedup, tcs): + action, expect = act_exp.split(" ", 1) + t = "dedup:%s action:%s" % (dedup, action) + print("\n\n\033[0;7m# ", t, "\033[0m") + + ka = {"dav_inf": True} + if dedup == "hard": + ka["hardlink"] = True + elif dedup == "no": + ka["no_dedup"] = True + + self.args = Cfg(v=vols, a=[], **ka) + self.reset() + self.cinit() + + self.do_cp(action) + zs = self.propfind() + + fns = " ".join(zs[1]) + self.assertEqual(expect, fns) + + def do_cp(self, action): + hdr = "POST %s HTTP/1.1\r\nConnection: close\r\nContent-Length: 0\r\n\r\n" + buf = (hdr % (action,)).encode("utf-8") + print("CP [%s]" % (action,)) + HttpCli(self.conn.setbuf(buf)).run() + ret = self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1) + print("CP <-- ", ret) + self.assertIn(" 201 Created", ret[0]) + self.assertEqual("k\r\n", ret[1]) + return ret + + def propfind(self): + h = "PROPFIND / HTTP/1.1\r\nConnection: close\r\n\r\n" + HttpCli(self.conn.setbuf(h.encode("utf-8"))).run() + h, t = self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1) + fns = t.split("<D:response><D:href>")[1:] + fns = [x.split("</D", 1)[0] for x in fns] + fns = [x for x in fns if not x.endswith("/")] + fns.sort() + return h, fns + + def log(self, src, msg, c=0): + print(msg) diff --git a/tests/util.py b/tests/util.py index 17d80f54..4b72c65d 100644 --- a/tests/util.py +++ b/tests/util.py @@ -122,7 +122,7 @@ class Cfg(Namespace): def __init__(self, a=None, v=None, c=None, **ka0): ka = {} - 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_clone no_dav no_db_ip no_del no_dirsz 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 ohead q rand re_dirsz rss 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_clone no_cp no_dav no_db_ip no_del no_dirsz 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 ohead q rand re_dirsz rss smb srch_dbg stats uqe vague_403 vc ver write_uplog xdev xlink xvol zs" 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_up_list no_voldump re_dhash plain_ip" @@ -146,7 +146,7 @@ class Cfg(Namespace): ex = "ban_403 ban_404 ban_422 ban_pw ban_url" ka.update(**{k: "no" for k in ex.split()}) - ex = "grp on403 on404 xad xar xau xban xbd xbr xbu xiu xm" + ex = "grp on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm" ka.update(**{k: [] for k in ex.split()}) ex = "exp_lg exp_md"