support copying files/folders; closes #115

behaves according to the target volume's deduplication config;
will create symlinks / hardlinks instead if dedup is enabled
This commit is contained in:
ed 2024-11-07 21:41:53 +00:00
parent 44ee07f0b2
commit cacec9c1f3
12 changed files with 555 additions and 62 deletions

View file

@ -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

View file

@ -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

View file

@ -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 <title>")
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")

View file

@ -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())

View file

@ -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",

View file

@ -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"]

View file

@ -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)
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,12 +4332,14 @@ class Up2k(object):
dvpf = dvp + svpf[len(svp) :]
self._mv_file(uname, ip, svpf, dvpf, curs)
for v in curs:
v.connection.commit()
curs.clear()
finally:
for v in curs:
v.connection.commit()
curs.clear()
rm_ok, rm_ng = rmdirs(self.log_func, scandir, True, sabs, 1)
for zsl in (rm_ok, rm_ng):

View file

@ -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 &lt;small&gt;(then paste somewhere else)&lt;/small&gt;$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 &nbsp; ( for cut / delete / ... )$NHotkey: S\">sel",
"tvt_sel": "select file &nbsp; ( 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 &lt;small&gt;(for å lime inn et annet sted)&lt;/small&gt;$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": "剪切选中的项目&lt;small&gt;(然后粘贴到其他地方)&lt;/small&gt;$N快捷键: ctrl-X",
"wt_cpy": "将选中的项目复制到剪贴板&lt;small&gt;(然后粘贴到其他地方)&lt;/small&gt;$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();

View file

@ -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 |

View file

@ -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 &lt;small&gt;(then paste somewhere else)&lt;/small&gt;$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 &nbsp; ( for cut / delete / ... )$NHotkey: S\">sel",
"tvt_sel": "select file &nbsp; ( 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\">🎧",

109
tests/test_cp.py Normal file
View file

@ -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)

View file

@ -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"