list dotfiles only for specific volumes or users (#66):

* permission `.` grants dotfile visibility if user has `r` too
* `-ed` will grant dotfiles to all `r` accounts (same as before)
* volflag `dots` likewise

also drops compatibility for pre-0.12.0 `-v` syntax
(`-v .::red` will no longer translate to `-v .::r,ed`)
This commit is contained in:
ed 2023-12-16 15:38:48 +00:00
parent c057c5e8e8
commit 0c50ea1757
12 changed files with 306 additions and 167 deletions

View file

@ -26,6 +26,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [FAQ](#FAQ) - "frequently" asked questions * [FAQ](#FAQ) - "frequently" asked questions
* [accounts and volumes](#accounts-and-volumes) - per-folder, per-user permissions * [accounts and volumes](#accounts-and-volumes) - per-folder, per-user permissions
* [shadowing](#shadowing) - hiding specific subfolders * [shadowing](#shadowing) - hiding specific subfolders
* [dotfiles](#dotfiles) - unix-style hidden files/folders
* [the browser](#the-browser) - accessing a copyparty server using a web-browser * [the browser](#the-browser) - accessing a copyparty server using a web-browser
* [tabs](#tabs) - the main tabs in the ui * [tabs](#tabs) - the main tabs in the ui
* [hotkeys](#hotkeys) - the browser has the following hotkeys * [hotkeys](#hotkeys) - the browser has the following hotkeys
@ -368,6 +369,7 @@ permissions:
* `w` (write): upload files, move files *into* this folder * `w` (write): upload files, move files *into* this folder
* `m` (move): move files/folders *from* this folder * `m` (move): move files/folders *from* this folder
* `d` (delete): delete files/folders * `d` (delete): delete files/folders
* `.` (dots): user can ask to show dotfiles in directory listings
* `g` (get): only download files, cannot see folder contents or zip/tar * `g` (get): only download files, cannot see folder contents or zip/tar
* `G` (upget): same as `g` except uploaders get to see their own [filekeys](#filekeys) (see `fk` in examples below) * `G` (upget): same as `g` except uploaders get to see their own [filekeys](#filekeys) (see `fk` in examples below)
* `h` (html): same as `g` except folders return their index.html, and filekeys are not necessary for index.html * `h` (html): same as `g` except folders return their index.html, and filekeys are not necessary for index.html
@ -399,6 +401,17 @@ hiding specific subfolders by mounting another volume on top of them
for example `-v /mnt::r -v /var/empty:web/certs:r` mounts the server folder `/mnt` as the webroot, but another volume is mounted at `/web/certs` -- so visitors can only see the contents of `/mnt` and `/mnt/web` (at URLs `/` and `/web`), but not `/mnt/web/certs` because URL `/web/certs` is mapped to `/var/empty` for example `-v /mnt::r -v /var/empty:web/certs:r` mounts the server folder `/mnt` as the webroot, but another volume is mounted at `/web/certs` -- so visitors can only see the contents of `/mnt` and `/mnt/web` (at URLs `/` and `/web`), but not `/mnt/web/certs` because URL `/web/certs` is mapped to `/var/empty`
## dotfiles
unix-style hidden files/folders by starting the name with a dot
anyone can access these if they know the name, but they normally don't appear in directory listings
a client can request to see dotfiles in directory listings if global option `-ed` is specified, or the volume has volflag `dots`, or the user has permission `.`
dotfiles do not appear in search results unless one of the above is true, **and** the global option / volflag `dotsrch` is set
# the browser # the browser
accessing a copyparty server using a web-browser accessing a copyparty server using a web-browser
@ -539,7 +552,7 @@ select which type of archive you want in the `[⚙️] config` tab:
* gzip default level is `3` (0=fast, 9=best), change with `?tar=gz:9` * gzip default level is `3` (0=fast, 9=best), change with `?tar=gz:9`
* xz default level is `1` (0=fast, 9=best), change with `?tar=xz:9` * xz default level is `1` (0=fast, 9=best), change with `?tar=xz:9`
* bz2 default level is `2` (1=fast, 9=best), change with `?tar=bz2:9` * bz2 default level is `2` (1=fast, 9=best), change with `?tar=bz2:9`
* hidden files (dotfiles) are excluded unless `-ed` * hidden files ([dotfiles](#dotfiles)) are excluded unless account is allowed to list them
* `up2k.db` and `dir.txt` is always excluded * `up2k.db` and `dir.txt` is always excluded
* bsdtar supports streaming unzipping: `curl foo?zip=utf8 | bsdtar -xv` * bsdtar supports streaming unzipping: `curl foo?zip=utf8 | bsdtar -xv`
* good, because copyparty's zip is faster than tar on small files * good, because copyparty's zip is faster than tar on small files

View file

@ -19,11 +19,10 @@ import threading
import time import time
import traceback import traceback
import uuid import uuid
from textwrap import dedent
from .__init__ import ANYWIN, CORES, EXE, PY2, VT100, WINDOWS, E, EnvParams, unicode from .__init__ import ANYWIN, CORES, EXE, PY2, VT100, WINDOWS, E, EnvParams, unicode
from .__version__ import CODENAME, S_BUILD_DT, S_VERSION from .__version__ import CODENAME, S_BUILD_DT, S_VERSION
from .authsrv import expand_config_file, re_vol, split_cfg_ln, upgrade_cfg_fmt from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt
from .cfg import flagcats, onedash from .cfg import flagcats, onedash
from .svchub import SvcHub from .svchub import SvcHub
from .util import ( from .util import (
@ -37,6 +36,7 @@ from .util import (
UNPLICATIONS, UNPLICATIONS,
align_tab, align_tab,
ansi_re, ansi_re,
dedent,
min_ex, min_ex,
py_desc, py_desc,
pybin, pybin,
@ -498,6 +498,7 @@ def get_sects():
"g" (get): download files, but cannot see folder contents "g" (get): download files, but cannot see folder contents
"G" (upget): "get", but can see filekeys of their own uploads "G" (upget): "get", but can see filekeys of their own uploads
"h" (html): "get", but folders return their index.html "h" (html): "get", but folders return their index.html
"." (dots): user can ask to show dotfiles in listings
"a" (admin): can see uploader IPs, config-reload "a" (admin): can see uploader IPs, config-reload
too many volflags to list here, see --help-flags too many volflags to list here, see --help-flags
@ -705,6 +706,7 @@ def get_sects():
\033[36mln\033[0m only prints symlinks leaving the volume mountpoint \033[36mln\033[0m only prints symlinks leaving the volume mountpoint
\033[36mp\033[0m exits 1 if any such symlinks are found \033[36mp\033[0m exits 1 if any such symlinks are found
\033[36mr\033[0m resumes startup after the listing \033[36mr\033[0m resumes startup after the listing
examples: examples:
--ls '**' # list all files which are possible to read --ls '**' # list all files which are possible to read
--ls '**,*,ln' # check for dangerous symlinks --ls '**,*,ln' # check for dangerous symlinks
@ -738,9 +740,12 @@ def get_sects():
""" """
when \033[36m--ah-alg\033[0m is not the default [\033[32mnone\033[0m], all account passwords must be hashed when \033[36m--ah-alg\033[0m is not the default [\033[32mnone\033[0m], all account passwords must be hashed
passwords can be hashed on the commandline with \033[36m--ah-gen\033[0m, but copyparty will also hash and print any passwords that are non-hashed (password which do not start with '+') and then terminate afterwards passwords can be hashed on the commandline with \033[36m--ah-gen\033[0m, but
copyparty will also hash and print any passwords that are non-hashed
(password which do not start with '+') and then terminate afterwards
\033[36m--ah-alg\033[0m specifies the hashing algorithm and a list of optional comma-separated arguments: \033[36m--ah-alg\033[0m specifies the hashing algorithm and a
list of optional comma-separated arguments:
\033[36m--ah-alg argon2\033[0m # which is the same as: \033[36m--ah-alg argon2\033[0m # which is the same as:
\033[36m--ah-alg argon2,3,256,4,19\033[0m \033[36m--ah-alg argon2,3,256,4,19\033[0m
@ -821,7 +826,7 @@ def add_general(ap, nc, srvname):
ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores, 0=all") ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores, 0=all")
ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="add account, \033[33mUSER\033[0m:\033[33mPASS\033[0m; example [\033[32med:wark\033[0m]") ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="add account, \033[33mUSER\033[0m:\033[33mPASS\033[0m; example [\033[32med:wark\033[0m]")
ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m]") ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m]")
ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files") ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files (volflag=dots)")
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,get", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m") ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,get", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m")
ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="server terminal title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]") ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="server terminal title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]")
ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)") ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)")
@ -1457,40 +1462,6 @@ def main(argv: Optional[list[str]] = None) -> None:
if al.ansi: if al.ansi:
al.wintitle = "" al.wintitle = ""
nstrs: list[str] = []
anymod = False
for ostr in al.v or []:
m = re_vol.match(ostr)
if not m:
# not our problem
nstrs.append(ostr)
continue
src, dst, perms = m.groups()
na = [src, dst]
mod = False
for opt in perms.split(":"):
if re.match("c[^,]", opt):
mod = True
na.append("c," + opt[1:])
elif re.sub("^[rwmdgGha]*", "", opt) and "," not in opt:
mod = True
perm = opt[0]
na.append(perm + "," + opt[1:])
else:
na.append(opt)
nstr = ":".join(na)
nstrs.append(nstr if mod else ostr)
if mod:
msg = "\033[1;31mWARNING:\033[0;1m\n -v {} \033[0;33mwas replaced with\033[0;1m\n -v {} \n\033[0m"
lprint(msg.format(ostr, nstr))
anymod = True
if anymod:
al.v = nstrs
time.sleep(2)
# propagate implications # propagate implications
for k1, k2 in IMPLICATIONS: for k1, k2 in IMPLICATIONS:
if getattr(al, k1): if getattr(al, k1):

View file

@ -72,6 +72,7 @@ class AXS(object):
upget: Optional[Union[list[str], set[str]]] = None, upget: Optional[Union[list[str], set[str]]] = None,
uhtml: Optional[Union[list[str], set[str]]] = None, uhtml: Optional[Union[list[str], set[str]]] = None,
uadmin: Optional[Union[list[str], set[str]]] = None, uadmin: Optional[Union[list[str], set[str]]] = None,
udot: Optional[Union[list[str], set[str]]] = None,
) -> None: ) -> None:
self.uread: set[str] = set(uread or []) self.uread: set[str] = set(uread or [])
self.uwrite: set[str] = set(uwrite or []) self.uwrite: set[str] = set(uwrite or [])
@ -81,9 +82,10 @@ class AXS(object):
self.upget: set[str] = set(upget or []) self.upget: set[str] = set(upget or [])
self.uhtml: set[str] = set(uhtml or []) self.uhtml: set[str] = set(uhtml or [])
self.uadmin: set[str] = set(uadmin or []) self.uadmin: set[str] = set(uadmin or [])
self.udot: set[str] = set(udot or [])
def __repr__(self) -> str: def __repr__(self) -> str:
ks = "uread uwrite umove udel uget upget uhtml uadmin".split() ks = "uread uwrite umove udel uget upget uhtml uadmin udot".split()
return "AXS(%s)" % (", ".join("%s=%r" % (k, self.__dict__[k]) for k in ks),) return "AXS(%s)" % (", ".join("%s=%r" % (k, self.__dict__[k]) for k in ks),)
@ -336,6 +338,8 @@ class VFS(object):
self.apget: dict[str, list[str]] = {} self.apget: dict[str, list[str]] = {}
self.ahtml: dict[str, list[str]] = {} self.ahtml: dict[str, list[str]] = {}
self.aadmin: dict[str, list[str]] = {} self.aadmin: dict[str, list[str]] = {}
self.adot: dict[str, list[str]] = {}
self.all_vols: dict[str, VFS] = {}
if realpath: if realpath:
rp = realpath + ("" if realpath.endswith(os.sep) else os.sep) rp = realpath + ("" if realpath.endswith(os.sep) else os.sep)
@ -445,8 +449,8 @@ class VFS(object):
def can_access( def can_access(
self, vpath: str, uname: str self, vpath: str, uname: str
) -> tuple[bool, bool, bool, bool, bool, bool, bool]: ) -> tuple[bool, bool, bool, bool, bool, bool, bool, bool]:
"""can Read,Write,Move,Delete,Get,Upget,Admin""" """can Read,Write,Move,Delete,Get,Upget,Admin,Dot"""
if vpath: if vpath:
vn, _ = self._find(undot(vpath)) vn, _ = self._find(undot(vpath))
else: else:
@ -454,13 +458,14 @@ class VFS(object):
c = vn.axs c = vn.axs
return ( return (
uname in c.uread or "*" in c.uread, uname in c.uread,
uname in c.uwrite or "*" in c.uwrite, uname in c.uwrite,
uname in c.umove or "*" in c.umove, uname in c.umove,
uname in c.udel or "*" in c.udel, uname in c.udel,
uname in c.uget or "*" in c.uget, uname in c.uget,
uname in c.upget or "*" in c.upget, uname in c.upget,
uname in c.uadmin or "*" in c.uadmin, uname in c.uadmin,
uname in c.udot,
) )
# skip uhtml because it's rarely needed # skip uhtml because it's rarely needed
@ -492,7 +497,7 @@ class VFS(object):
(will_del, c.udel, "delete"), (will_del, c.udel, "delete"),
(will_get, c.uget, "get"), (will_get, c.uget, "get"),
]: ]:
if req and (uname not in d and "*" not in d) and uname != LEELOO_DALLAS: if req and uname not in d and uname != LEELOO_DALLAS:
if vpath != cvpath and vpath != "." and self.log: if vpath != cvpath and vpath != "." and self.log:
ap = vn.canonical(rem) ap = vn.canonical(rem)
t = "{} has no {} in [{}] => [{}] => [{}]" t = "{} has no {} in [{}] => [{}] => [{}]"
@ -553,7 +558,7 @@ class VFS(object):
for pset in permsets: for pset in permsets:
ok = True ok = True
for req, lst in zip(pset, axs): for req, lst in zip(pset, axs):
if req and uname not in lst and "*" not in lst: if req and uname not in lst:
ok = False ok = False
if ok: if ok:
break break
@ -577,7 +582,7 @@ class VFS(object):
seen: list[str], seen: list[str],
uname: str, uname: str,
permsets: list[list[bool]], permsets: list[list[bool]],
dots: bool, wantdots: bool,
scandir: bool, scandir: bool,
lstat: bool, lstat: bool,
subvols: bool = True, subvols: bool = True,
@ -621,6 +626,10 @@ class VFS(object):
rm1.append(le) rm1.append(le)
_ = [vfs_ls.remove(x) for x in rm1] # type: ignore _ = [vfs_ls.remove(x) for x in rm1] # type: ignore
dots_ok = wantdots and uname in dbv.axs.udot
if not dots_ok:
vfs_ls = [x for x in vfs_ls if "/." not in "/" + x[0]]
seen = seen[:] + [fsroot] seen = seen[:] + [fsroot]
rfiles = [x for x in vfs_ls if not stat.S_ISDIR(x[1].st_mode)] rfiles = [x for x in vfs_ls if not stat.S_ISDIR(x[1].st_mode)]
rdirs = [x for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)] rdirs = [x for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]
@ -633,13 +642,13 @@ class VFS(object):
yield dbv, vrem, rel, fsroot, rfiles, rdirs, vfs_virt yield dbv, vrem, rel, fsroot, rfiles, rdirs, vfs_virt
for rdir, _ in rdirs: for rdir, _ in rdirs:
if not dots and rdir.startswith("."): if not dots_ok and rdir.startswith("."):
continue continue
wrel = (rel + "/" + rdir).lstrip("/") wrel = (rel + "/" + rdir).lstrip("/")
wrem = (rem + "/" + rdir).lstrip("/") wrem = (rem + "/" + rdir).lstrip("/")
for x in self.walk( for x in self.walk(
wrel, wrem, seen, uname, permsets, dots, scandir, lstat, subvols wrel, wrem, seen, uname, permsets, wantdots, scandir, lstat, subvols
): ):
yield x yield x
@ -647,11 +656,13 @@ class VFS(object):
return return
for n, vfs in sorted(vfs_virt.items()): for n, vfs in sorted(vfs_virt.items()):
if not dots and n.startswith("."): if not dots_ok and n.startswith("."):
continue continue
wrel = (rel + "/" + n).lstrip("/") wrel = (rel + "/" + n).lstrip("/")
for x in vfs.walk(wrel, "", seen, uname, permsets, dots, scandir, lstat): for x in vfs.walk(
wrel, "", seen, uname, permsets, wantdots, scandir, lstat
):
yield x yield x
def zipgen( def zipgen(
@ -660,7 +671,6 @@ class VFS(object):
vrem: str, vrem: str,
flt: set[str], flt: set[str],
uname: str, uname: str,
dots: bool,
dirs: bool, dirs: bool,
scandir: bool, scandir: bool,
wrap: bool = True, wrap: bool = True,
@ -670,7 +680,7 @@ class VFS(object):
# if single folder: the folder itself is the top-level item # if single folder: the folder itself is the top-level item
folder = "" if flt or not wrap else (vpath.split("/")[-1].lstrip(".") or "top") folder = "" if flt or not wrap else (vpath.split("/")[-1].lstrip(".") or "top")
g = self.walk(folder, vrem, [], uname, [[True, False]], dots, scandir, False) g = self.walk(folder, vrem, [], uname, [[True, False]], True, scandir, False)
for _, _, vpath, apath, files, rd, vd in g: for _, _, vpath, apath, files, rd, vd in g:
if flt: if flt:
files = [x for x in files if x[0] in flt] files = [x for x in files if x[0] in flt]
@ -689,18 +699,6 @@ class VFS(object):
apaths = [os.path.join(apath, n) for n in fnames] apaths = [os.path.join(apath, n) for n in fnames]
ret = list(zip(vpaths, apaths, files)) ret = list(zip(vpaths, apaths, files))
if not dots:
# dotfile filtering based on vpath (intended visibility)
ret = [x for x in ret if "/." not in "/" + x[0]]
zel = [ze for ze in rd if ze[0].startswith(".")]
for ze in zel:
rd.remove(ze)
zsl = [zs for zs in vd.keys() if zs.startswith(".")]
for zs in zsl:
del vd[zs]
for f in [{"vp": v, "ap": a, "st": n[1]} for v, a, n in ret]: for f in [{"vp": v, "ap": a, "st": n[1]} for v, a, n in ret]:
yield f yield f
@ -958,16 +956,17 @@ class AuthSrv(object):
try: try:
self._l(ln, 5, "volume access config:") self._l(ln, 5, "volume access config:")
sk, sv = ln.split(":") sk, sv = ln.split(":")
if re.sub("[rwmdgGha]", "", sk) or not sk: if re.sub("[rwmdgGha.]", "", sk) or not sk:
err = "invalid accs permissions list; " err = "invalid accs permissions list; "
raise Exception(err) raise Exception(err)
if " " in re.sub(", *", "", sv).strip(): if " " in re.sub(", *", "", sv).strip():
err = "list of users is not comma-separated; " err = "list of users is not comma-separated; "
raise Exception(err) raise Exception(err)
assert vp is not None
self._read_vol_str(sk, sv.replace(" ", ""), daxs[vp], mflags[vp]) self._read_vol_str(sk, sv.replace(" ", ""), daxs[vp], mflags[vp])
continue continue
except: except:
err += "accs entries must be 'rwmdgGha: user1, user2, ...'" err += "accs entries must be 'rwmdgGha.: user1, user2, ...'"
raise Exception(err + SBADCFG) raise Exception(err + SBADCFG)
if cat == catf: if cat == catf:
@ -986,9 +985,11 @@ class AuthSrv(object):
fstr += "," + sk fstr += "," + sk
else: else:
fstr += ",{}={}".format(sk, sv) fstr += ",{}={}".format(sk, sv)
assert vp is not None
self._read_vol_str("c", fstr[1:], daxs[vp], mflags[vp]) self._read_vol_str("c", fstr[1:], daxs[vp], mflags[vp])
fstr = "" fstr = ""
if fstr: if fstr:
assert vp is not None
self._read_vol_str("c", fstr[1:], daxs[vp], mflags[vp]) self._read_vol_str("c", fstr[1:], daxs[vp], mflags[vp])
continue continue
except: except:
@ -1003,8 +1004,9 @@ class AuthSrv(object):
def _read_vol_str( def _read_vol_str(
self, lvl: str, uname: str, axs: AXS, flags: dict[str, Any] self, lvl: str, uname: str, axs: AXS, flags: dict[str, Any]
) -> None: ) -> None:
if lvl.strip("crwmdgGha"): if lvl.strip("crwmdgGha."):
raise Exception("invalid volflag: {},{}".format(lvl, uname)) t = "%s,%s" % (lvl, uname) if uname else lvl
raise Exception("invalid config value (volume or volflag): %s" % (t,))
if lvl == "c": if lvl == "c":
cval: Union[bool, str] = True cval: Union[bool, str] = True
@ -1032,6 +1034,7 @@ class AuthSrv(object):
("w", axs.uwrite), ("w", axs.uwrite),
("m", axs.umove), ("m", axs.umove),
("d", axs.udel), ("d", axs.udel),
(".", axs.udot),
("a", axs.uadmin), ("a", axs.uadmin),
("h", axs.uhtml), ("h", axs.uhtml),
("h", axs.uget), ("h", axs.uget),
@ -1110,7 +1113,7 @@ class AuthSrv(object):
if self.args.v: if self.args.v:
# list of src:dst:permset:permset:... # list of src:dst:permset:permset:...
# permset is <rwmdgGha>[,username][,username] or <c>,<flag>[=args] # permset is <rwmdgGha.>[,username][,username] or <c>,<flag>[=args]
for v_str in self.args.v: for v_str in self.args.v:
m = re_vol.match(v_str) m = re_vol.match(v_str)
if not m: if not m:
@ -1200,14 +1203,21 @@ class AuthSrv(object):
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True) vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
vol.root = vfs vol.root = vfs
for perm in "read write move del get pget html admin".split(): for perm in "read write move del get pget html admin dot".split():
axs_key = "u" + perm axs_key = "u" + perm
unames = ["*"] + list(acct.keys()) unames = ["*"] + list(acct.keys())
for vp, vol in vfs.all_vols.items():
zx = getattr(vol.axs, axs_key)
if "*" in zx:
for usr in unames:
zx.add(usr)
# aread,... = dict[uname, list[volnames] or []]
umap: dict[str, list[str]] = {x: [] for x in unames} umap: dict[str, list[str]] = {x: [] for x in unames}
for usr in unames: for usr in unames:
for vp, vol in vfs.all_vols.items(): for vp, vol in vfs.all_vols.items():
zx = getattr(vol.axs, axs_key) zx = getattr(vol.axs, axs_key)
if usr in zx or "*" in zx: if usr in zx:
umap[usr].append(vp) umap[usr].append(vp)
umap[usr].sort() umap[usr].sort()
setattr(vfs, "a" + perm, umap) setattr(vfs, "a" + perm, umap)
@ -1224,6 +1234,7 @@ class AuthSrv(object):
axs.upget, axs.upget,
axs.uhtml, axs.uhtml,
axs.uadmin, axs.uadmin,
axs.udot,
]: ]:
for usr in d: for usr in d:
all_users[usr] = 1 all_users[usr] = 1
@ -1632,6 +1643,11 @@ class AuthSrv(object):
vol.flags.pop(k[1:], None) vol.flags.pop(k[1:], None)
vol.flags.pop(k) vol.flags.pop(k)
for vol in vfs.all_vols.values():
if vol.flags.get("dots"):
for name in vol.axs.uread:
vol.axs.udot.add(name)
if errors: if errors:
sys.exit(1) sys.exit(1)
@ -1650,12 +1666,14 @@ class AuthSrv(object):
[" write", "uwrite"], [" write", "uwrite"],
[" move", "umove"], [" move", "umove"],
["delete", "udel"], ["delete", "udel"],
[" dots", "udot"],
[" get", "uget"], [" get", "uget"],
[" upget", "upget"], [" upGet", "upget"],
[" html", "uhtml"], [" html", "uhtml"],
["uadmin", "uadmin"], ["uadmin", "uadmin"],
]: ]:
u = list(sorted(getattr(zv.axs, attr))) u = list(sorted(getattr(zv.axs, attr)))
u = ["*"] if "*" in u else u
u = ", ".join("\033[35meverybody\033[0m" if x == "*" else x for x in u) u = ", ".join("\033[35meverybody\033[0m" if x == "*" else x for x in u)
u = u if u else "\033[36m--none--\033[0m" u = u if u else "\033[36m--none--\033[0m"
t += "\n| {}: {}".format(txt, u) t += "\n| {}: {}".format(txt, u)
@ -1812,7 +1830,7 @@ class AuthSrv(object):
raise Exception("volume not found: " + zs) raise Exception("volume not found: " + zs)
self.log(str({"users": users, "vols": vols, "flags": flags})) self.log(str({"users": users, "vols": vols, "flags": flags}))
t = "/{}: read({}) write({}) move({}) del({}) get({}) upget({}) uadmin({})" t = "/{}: read({}) write({}) move({}) del({}) dots({}) get({}) upGet({}) uadmin({})"
for k, zv in self.vfs.all_vols.items(): for k, zv in self.vfs.all_vols.items():
vc = zv.axs vc = zv.axs
vs = [ vs = [
@ -1821,6 +1839,7 @@ class AuthSrv(object):
vc.uwrite, vc.uwrite,
vc.umove, vc.umove,
vc.udel, vc.udel,
vc.udot,
vc.uget, vc.uget,
vc.upget, vc.upget,
vc.uhtml, vc.uhtml,
@ -1963,6 +1982,7 @@ class AuthSrv(object):
"w": "uwrite", "w": "uwrite",
"m": "umove", "m": "umove",
"d": "udel", "d": "udel",
".": "udot",
"g": "uget", "g": "uget",
"G": "upget", "G": "upget",
"h": "uhtml", "h": "uhtml",
@ -2169,7 +2189,7 @@ def upgrade_cfg_fmt(
else: else:
sn = sn.replace(",", ", ") sn = sn.replace(",", ", ")
ret.append(" " + sn) ret.append(" " + sn)
elif sn[:1] in "rwmdgGha": elif sn[:1] in "rwmdgGha.":
if cat != catx: if cat != catx:
cat = catx cat = catx
ret.append(cat) ret.append(cat)

View file

@ -9,6 +9,9 @@ onedash = set(zs.split())
def vf_bmap() -> dict[str, str]: def vf_bmap() -> dict[str, str]:
"""argv-to-volflag: simple bools""" """argv-to-volflag: simple bools"""
ret = { ret = {
"dav_auth": "davauth",
"dav_rt": "davrt",
"ed": "dots",
"never_symlink": "neversymlink", "never_symlink": "neversymlink",
"no_dedup": "copydupes", "no_dedup": "copydupes",
"no_dupe": "nodupe", "no_dupe": "nodupe",
@ -18,8 +21,6 @@ def vf_bmap() -> dict[str, str]:
"no_vthumb": "dvthumb", "no_vthumb": "dvthumb",
"no_athumb": "dathumb", "no_athumb": "dathumb",
"th_no_crop": "nocrop", "th_no_crop": "nocrop",
"dav_auth": "davauth",
"dav_rt": "davrt",
} }
for k in ( for k in (
"dotsrch", "dotsrch",
@ -98,6 +99,7 @@ permdescs = {
"w": 'write; upload files; need "r" to see the uploads', "w": 'write; upload files; need "r" to see the uploads',
"m": 'move; move files and folders; need "w" at destination', "m": 'move; move files and folders; need "w" at destination',
"d": "delete; permanently delete files and folders", "d": "delete; permanently delete files and folders",
".": "dots; user can ask to show dotfiles in listings",
"g": "get; download files, but cannot see folder contents", "g": "get; download files, but cannot see folder contents",
"G": 'upget; same as "g" but can see filekeys of their own uploads', "G": 'upget; same as "g" but can see filekeys of their own uploads',
"h": 'html; same as "g" but folders return their index.html', "h": 'html; same as "g" but folders return their index.html',
@ -202,6 +204,7 @@ flagcats = {
"nohtml": "return html and markdown as text/html", "nohtml": "return html and markdown as text/html",
}, },
"others": { "others": {
"dots": "allow all users with read-access to\nenable the option to show dotfiles in listings",
"fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes', "fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
"fka=8": 'generates slightly weaker per-file accesskeys,\nwhich are then required at the "g" permission;\nnot affected by filesize or inode numbers', "fka=8": 'generates slightly weaker per-file accesskeys,\nwhich are then required at the "g" permission;\nnot affected by filesize or inode numbers',
"davauth": "ask webdav clients to login for all folders", "davauth": "ask webdav clients to login for all folders",

View file

@ -73,6 +73,7 @@ class FtpAuth(DummyAuthorizer):
asrv = self.hub.asrv asrv = self.hub.asrv
uname = "*" uname = "*"
if username != "anonymous": if username != "anonymous":
uname = ""
for zs in (password, username): for zs in (password, username):
zs = asrv.iacct.get(asrv.ah.hash(zs), "") zs = asrv.iacct.get(asrv.ah.hash(zs), "")
if zs: if zs:
@ -132,7 +133,7 @@ class FtpFs(AbstractedFS):
self.can_read = self.can_write = self.can_move = False self.can_read = self.can_write = self.can_move = False
self.can_delete = self.can_get = self.can_upget = False self.can_delete = self.can_get = self.can_upget = False
self.can_admin = False self.can_admin = self.can_dot = False
self.listdirinfo = self.listdir self.listdirinfo = self.listdir
self.chdir(".") self.chdir(".")
@ -167,7 +168,7 @@ class FtpFs(AbstractedFS):
if not avfs: if not avfs:
raise FSE(t.format(vpath), 1) raise FSE(t.format(vpath), 1)
cr, cw, cm, cd, _, _, _ = avfs.can_access("", self.h.uname) cr, cw, cm, cd, _, _, _, _ = avfs.can_access("", self.h.uname)
if r and not cr or w and not cw or m and not cm or d and not cd: if r and not cr or w and not cw or m and not cm or d and not cd:
raise FSE(t.format(vpath), 1) raise FSE(t.format(vpath), 1)
@ -243,6 +244,7 @@ class FtpFs(AbstractedFS):
self.can_get, self.can_get,
self.can_upget, self.can_upget,
self.can_admin, self.can_admin,
self.can_dot,
) = avfs.can_access("", self.h.uname) ) = avfs.can_access("", self.h.uname)
def mkdir(self, path: str) -> None: def mkdir(self, path: str) -> None:
@ -265,7 +267,7 @@ class FtpFs(AbstractedFS):
vfs_ls = [x[0] for x in vfs_ls1] vfs_ls = [x[0] for x in vfs_ls1]
vfs_ls.extend(vfs_virt.keys()) vfs_ls.extend(vfs_virt.keys())
if not self.args.ed: if not self.can_dot:
vfs_ls = exclude_dotfiles(vfs_ls) vfs_ls = exclude_dotfiles(vfs_ls)
vfs_ls.sort() vfs_ls.sort()

View file

@ -154,10 +154,6 @@ class HttpCli(object):
self.pw = " " self.pw = " "
self.rvol = [" "] self.rvol = [" "]
self.wvol = [" "] self.wvol = [" "]
self.mvol = [" "]
self.dvol = [" "]
self.gvol = [" "]
self.upvol = [" "]
self.avol = [" "] self.avol = [" "]
self.do_log = True self.do_log = True
self.can_read = False self.can_read = False
@ -167,6 +163,7 @@ class HttpCli(object):
self.can_get = False self.can_get = False
self.can_upget = False self.can_upget = False
self.can_admin = False self.can_admin = False
self.can_dot = False
self.out_headerlist: list[tuple[str, str]] = [] self.out_headerlist: list[tuple[str, str]] = []
self.out_headers: dict[str, str] = {} self.out_headers: dict[str, str] = {}
self.html_head = " " self.html_head = " "
@ -467,10 +464,6 @@ class HttpCli(object):
self.rvol = self.asrv.vfs.aread[self.uname] self.rvol = self.asrv.vfs.aread[self.uname]
self.wvol = self.asrv.vfs.awrite[self.uname] self.wvol = self.asrv.vfs.awrite[self.uname]
self.mvol = self.asrv.vfs.amove[self.uname]
self.dvol = self.asrv.vfs.adel[self.uname]
self.gvol = self.asrv.vfs.aget[self.uname]
self.upvol = self.asrv.vfs.apget[self.uname]
self.avol = self.asrv.vfs.aadmin[self.uname] self.avol = self.asrv.vfs.aadmin[self.uname]
if self.pw and ( if self.pw and (
@ -503,6 +496,7 @@ class HttpCli(object):
self.can_get, self.can_get,
self.can_upget, self.can_upget,
self.can_admin, self.can_admin,
self.can_dot,
) = ( ) = (
avn.can_access("", self.uname) if avn else [False] * 7 avn.can_access("", self.uname) if avn else [False] * 7
) )
@ -1131,7 +1125,6 @@ class HttpCli(object):
rem, rem,
set(), set(),
self.uname, self.uname,
self.args.ed,
True, True,
not self.args.no_scandir, not self.args.no_scandir,
wrap=False, wrap=False,
@ -1145,7 +1138,7 @@ class HttpCli(object):
[[True, False]], [[True, False]],
lstat="davrt" not in vn.flags, lstat="davrt" not in vn.flags,
) )
if not self.args.ed: if not self.can_dot:
names = set(exclude_dotfiles([x[0] for x in vfs_ls])) names = set(exclude_dotfiles([x[0] for x in vfs_ls]))
vfs_ls = [x for x in vfs_ls if x[0] in names] vfs_ls = [x for x in vfs_ls if x[0] in names]
@ -1910,7 +1903,7 @@ class HttpCli(object):
items = [unquotep(x) for x in items if items] items = [unquotep(x) for x in items if items]
self.parser.drop() self.parser.drop()
return self.tx_zip(k, v, "", vn, rem, items, self.args.ed) return self.tx_zip(k, v, "", vn, rem, items)
def handle_post_json(self) -> bool: def handle_post_json(self) -> bool:
try: try:
@ -1996,10 +1989,10 @@ class HttpCli(object):
def handle_search(self, body: dict[str, Any]) -> bool: def handle_search(self, body: dict[str, Any]) -> bool:
idx = self.conn.get_u2idx() idx = self.conn.get_u2idx()
if not idx or not hasattr(idx, "p_end"): if not idx or not hasattr(idx, "p_end"):
raise Pebkac(500, "sqlite3 is not available on the server; cannot search") raise Pebkac(500, "server busy, or sqlite3 not available; cannot search")
vols = [] vols: list[VFS] = []
seen = {} seen: dict[VFS, bool] = {}
for vtop in self.rvol: for vtop in self.rvol:
vfs, _ = self.asrv.vfs.get(vtop, self.uname, True, False) vfs, _ = self.asrv.vfs.get(vtop, self.uname, True, False)
vfs = vfs.dbv or vfs vfs = vfs.dbv or vfs
@ -2007,7 +2000,7 @@ class HttpCli(object):
continue continue
seen[vfs] = True seen[vfs] = True
vols.append((vfs.vpath, vfs.realpath, vfs.flags)) vols.append(vfs)
t0 = time.time() t0 = time.time()
if idx.p_end: if idx.p_end:
@ -2022,7 +2015,7 @@ class HttpCli(object):
vbody = copy.deepcopy(body) vbody = copy.deepcopy(body)
vbody["hash"] = len(vbody["hash"]) vbody["hash"] = len(vbody["hash"])
self.log("qj: " + repr(vbody)) self.log("qj: " + repr(vbody))
hits = idx.fsearch(vols, body) hits = idx.fsearch(self.uname, vols, body)
msg: Any = repr(hits) msg: Any = repr(hits)
taglist: list[str] = [] taglist: list[str] = []
trunc = False trunc = False
@ -2031,7 +2024,7 @@ class HttpCli(object):
q = body["q"] q = body["q"]
n = body.get("n", self.args.srch_hits) n = body.get("n", self.args.srch_hits)
self.log("qj: {} |{}|".format(q, n)) self.log("qj: {} |{}|".format(q, n))
hits, taglist, trunc = idx.search(vols, q, n) hits, taglist, trunc = idx.search(self.uname, vols, q, n)
msg = len(hits) msg = len(hits)
idx.p_end = time.time() idx.p_end = time.time()
@ -3002,7 +2995,6 @@ class HttpCli(object):
vn: VFS, vn: VFS,
rem: str, rem: str,
items: list[str], items: list[str],
dots: bool,
) -> bool: ) -> bool:
if self.args.no_zip: if self.args.no_zip:
raise Pebkac(400, "not enabled") raise Pebkac(400, "not enabled")
@ -3059,7 +3051,7 @@ class HttpCli(object):
self.send_headers(None, mime=mime, headers={"Content-Disposition": cdis}) self.send_headers(None, mime=mime, headers={"Content-Disposition": cdis})
fgen = vn.zipgen( fgen = vn.zipgen(
vpath, rem, set(items), self.uname, dots, False, not self.args.no_scandir vpath, rem, set(items), self.uname, False, not self.args.no_scandir
) )
# for f in fgen: print(repr({k: f[k] for k in ["vp", "ap"]})) # for f in fgen: print(repr({k: f[k] for k in ["vp", "ap"]}))
cfmt = "" cfmt = ""
@ -3473,6 +3465,7 @@ class HttpCli(object):
ret["k" + quotep(excl)] = sub ret["k" + quotep(excl)] = sub
vfs = self.asrv.vfs vfs = self.asrv.vfs
dots = False
try: try:
vn, rem = vfs.get(top, self.uname, True, False) vn, rem = vfs.get(top, self.uname, True, False)
fsroot, vfs_ls, vfs_virt = vn.ls( fsroot, vfs_ls, vfs_virt = vn.ls(
@ -3481,6 +3474,7 @@ class HttpCli(object):
not self.args.no_scandir, not self.args.no_scandir,
[[True, False], [False, True]], [[True, False], [False, True]],
) )
dots = self.uname in vn.axs.udot
except: except:
vfs_ls = [] vfs_ls = []
vfs_virt = {} vfs_virt = {}
@ -3493,7 +3487,7 @@ class HttpCli(object):
dirnames = [x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)] dirnames = [x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]
if not self.args.ed or "dots" not in self.uparam: if not dots or "dots" not in self.uparam:
dirnames = exclude_dotfiles(dirnames) dirnames = exclude_dotfiles(dirnames)
for fn in [x for x in dirnames if x != excl]: for fn in [x for x in dirnames if x != excl]:
@ -3529,7 +3523,8 @@ class HttpCli(object):
fk_vols = { fk_vols = {
vol: (vol.flags["fk"], 2 if "fka" in vol.flags else 1) vol: (vol.flags["fk"], 2 if "fka" in vol.flags else 1)
for vp, vol in self.asrv.vfs.all_vols.items() for vp, vol in self.asrv.vfs.all_vols.items()
if "fk" in vol.flags and (vp in self.rvol or vp in self.upvol) if "fk" in vol.flags
and (self.uname in vol.axs.uread or self.uname in vol.axs.upget)
} }
for vol in self.asrv.vfs.all_vols.values(): for vol in self.asrv.vfs.all_vols.values():
cur = idx.get_cur(vol.realpath) cur = idx.get_cur(vol.realpath)
@ -3800,7 +3795,7 @@ class HttpCli(object):
elif self.can_get and self.avn: elif self.can_get and self.avn:
axs = self.avn.axs axs = self.avn.axs
if self.uname not in axs.uhtml and "*" not in axs.uhtml: if self.uname not in axs.uhtml:
pass pass
elif is_dir: elif is_dir:
for fn in ("index.htm", "index.html"): for fn in ("index.htm", "index.html"):
@ -4021,7 +4016,7 @@ class HttpCli(object):
for k in ["zip", "tar"]: for k in ["zip", "tar"]:
v = self.uparam.get(k) v = self.uparam.get(k)
if v is not None: if v is not None:
return self.tx_zip(k, v, self.vpath, vn, rem, [], self.args.ed) return self.tx_zip(k, v, self.vpath, vn, rem, [])
fsroot, vfs_ls, vfs_virt = vn.ls( fsroot, vfs_ls, vfs_virt = vn.ls(
rem, rem,
@ -4052,13 +4047,13 @@ class HttpCli(object):
pass pass
# show dotfiles if permitted and requested # show dotfiles if permitted and requested
if not self.args.ed or ( if not self.can_dot or (
"dots" not in self.uparam and (is_ls or "dots" not in self.cookies) "dots" not in self.uparam and (is_ls or "dots" not in self.cookies)
): ):
ls_names = exclude_dotfiles(ls_names) ls_names = exclude_dotfiles(ls_names)
add_fk = vn.flags.get("fk") add_fk = vf.get("fk")
fk_alg = 2 if "fka" in vn.flags else 1 fk_alg = 2 if "fka" in vf else 1
dirs = [] dirs = []
files = [] files = []

View file

@ -9,7 +9,7 @@ import time
from operator import itemgetter from operator import itemgetter
from .__init__ import ANYWIN, TYPE_CHECKING, unicode from .__init__ import ANYWIN, TYPE_CHECKING, unicode
from .authsrv import LEELOO_DALLAS from .authsrv import LEELOO_DALLAS, VFS
from .bos import bos from .bos import bos
from .up2k import up2k_wark_from_hashlist from .up2k import up2k_wark_from_hashlist
from .util import ( from .util import (
@ -63,7 +63,7 @@ class U2idx(object):
self.log_func("u2idx", msg, c) self.log_func("u2idx", msg, c)
def fsearch( def fsearch(
self, vols: list[tuple[str, str, dict[str, Any]]], body: dict[str, Any] self, uname: str, vols: list[VFS], body: dict[str, Any]
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
"""search by up2k hashlist""" """search by up2k hashlist"""
if not HAVE_SQLITE3: if not HAVE_SQLITE3:
@ -77,7 +77,7 @@ class U2idx(object):
uv: list[Union[str, int]] = [wark[:16], wark] uv: list[Union[str, int]] = [wark[:16], wark]
try: try:
return self.run_query(vols, uq, uv, True, False, 99999)[0] return self.run_query(uname, vols, uq, uv, False, 99999)[0]
except: except:
raise Pebkac(500, min_ex()) raise Pebkac(500, min_ex())
@ -122,7 +122,7 @@ class U2idx(object):
return cur return cur
def search( def search(
self, vols: list[tuple[str, str, dict[str, Any]]], uq: str, lim: int self, uname: str, vols: list[VFS], uq: str, lim: int
) -> tuple[list[dict[str, Any]], list[str], bool]: ) -> tuple[list[dict[str, Any]], list[str], bool]:
"""search by query params""" """search by query params"""
if not HAVE_SQLITE3: if not HAVE_SQLITE3:
@ -131,7 +131,6 @@ class U2idx(object):
q = "" q = ""
v: Union[str, int] = "" v: Union[str, int] = ""
va: list[Union[str, int]] = [] va: list[Union[str, int]] = []
have_up = False # query has up.* operands
have_mt = False have_mt = False
is_key = True is_key = True
is_size = False is_size = False
@ -176,26 +175,21 @@ class U2idx(object):
if v == "size": if v == "size":
v = "up.sz" v = "up.sz"
is_size = True is_size = True
have_up = True
elif v == "date": elif v == "date":
v = "up.mt" v = "up.mt"
is_date = True is_date = True
have_up = True
elif v == "up_at": elif v == "up_at":
v = "up.at" v = "up.at"
is_date = True is_date = True
have_up = True
elif v == "path": elif v == "path":
v = "trim(?||up.rd,'/')" v = "trim(?||up.rd,'/')"
va.append("\nrd") va.append("\nrd")
have_up = True
elif v == "name": elif v == "name":
v = "up.fn" v = "up.fn"
have_up = True
elif v == "tags" or ptn_mt.match(v): elif v == "tags" or ptn_mt.match(v):
have_mt = True have_mt = True
@ -271,22 +265,22 @@ class U2idx(object):
q += " lower({}) {} ? ) ".format(field, oper) q += " lower({}) {} ? ) ".format(field, oper)
try: try:
return self.run_query(vols, q, va, have_up, have_mt, lim) return self.run_query(uname, vols, q, va, have_mt, lim)
except Exception as ex: except Exception as ex:
raise Pebkac(500, repr(ex)) raise Pebkac(500, repr(ex))
def run_query( def run_query(
self, self,
vols: list[tuple[str, str, dict[str, Any]]], uname: str,
vols: list[VFS],
uq: str, uq: str,
uv: list[Union[str, int]], uv: list[Union[str, int]],
have_up: bool,
have_mt: bool, have_mt: bool,
lim: int, lim: int,
) -> tuple[list[dict[str, Any]], list[str], bool]: ) -> tuple[list[dict[str, Any]], list[str], bool]:
if self.args.srch_dbg: if self.args.srch_dbg:
t = "searching across all %s volumes in which the user has 'r' (full read access):\n %s" t = "searching across all %s volumes in which the user has 'r' (full read access):\n %s"
zs = "\n ".join(["/%s = %s" % (x[0], x[1]) for x in vols]) zs = "\n ".join(["/%s = %s" % (x.vpath, x.realpath) for x in vols])
self.log(t % (len(vols), zs), 5) self.log(t % (len(vols), zs), 5)
done_flag: list[bool] = [] done_flag: list[bool] = []
@ -315,10 +309,14 @@ class U2idx(object):
clamped = False clamped = False
taglist = {} taglist = {}
for (vtop, ptop, flags) in vols: for vol in vols:
if lim < 0: if lim < 0:
break break
vtop = vol.vpath
ptop = vol.realpath
flags = vol.flags
cur = self.get_cur(ptop) cur = self.get_cur(ptop)
if not cur: if not cur:
continue continue
@ -343,7 +341,7 @@ class U2idx(object):
sret = [] sret = []
fk = flags.get("fk") fk = flags.get("fk")
dots = flags.get("dotsrch") dots = flags.get("dotsrch") and uname in vol.axs.udot
fk_alg = 2 if "fka" in flags else 1 fk_alg = 2 if "fka" in flags else 1
c = cur.execute(uq, tuple(vuv)) c = cur.execute(uq, tuple(vuv))
for hit in c: for hit in c:

View file

@ -2664,12 +2664,7 @@ class Up2k(object):
not ret["hash"] not ret["hash"]
and "fk" in vfs.flags and "fk" in vfs.flags
and not self.args.nw and not self.args.nw
and ( and (cj["user"] in vfs.axs.uread or cj["user"] in vfs.axs.upget)
cj["user"] in vfs.axs.uread
or cj["user"] in vfs.axs.upget
or "*" in vfs.axs.uread
or "*" in vfs.axs.upget
)
): ):
alg = 2 if "fka" in vfs.flags else 1 alg = 2 if "fka" in vfs.flags else 1
ap = absreal(djoin(job["ptop"], job["prel"], job["name"])) ap = absreal(djoin(job["ptop"], job["prel"], job["name"]))

View file

@ -1074,6 +1074,17 @@ def nuprint(msg: str) -> None:
uprint("{}\n".format(msg)) uprint("{}\n".format(msg))
def dedent(txt: str) -> str:
pad = 64
lns = txt.replace("\r", "").split("\n")
for ln in lns:
zs = ln.lstrip()
pad2 = len(ln) - len(zs)
if zs and pad > pad2:
pad = pad2
return "\n".join([ln[pad:] for ln in lns])
def rice_tid() -> str: def rice_tid() -> str:
tid = threading.current_thread().ident tid = threading.current_thread().ident
c = sunpack(b"B" * 5, spack(b">Q", tid)[-5:]) c = sunpack(b"B" * 5, spack(b">Q", tid)[-5:])

111
tests/test_dots.py Normal file
View file

@ -0,0 +1,111 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
import io
import os
import shutil
import tarfile
import tempfile
import unittest
from copyparty.authsrv import AuthSrv
from copyparty.httpcli import HttpCli
from copyparty.up2k import Up2k
from copyparty.u2idx import U2idx
from tests import util as tu
from tests.util import Cfg
def hdr(query, uname):
h = "GET /%s HTTP/1.1\r\nPW: %s\r\nConnection: close\r\n\r\n"
return (h % (query, uname)).encode("utf-8")
class TestHttpCli(unittest.TestCase):
def setUp(self):
self.td = tu.get_ramdisk()
def tearDown(self):
os.chdir(tempfile.gettempdir())
shutil.rmtree(self.td)
def test(self):
td = os.path.join(self.td, "vfs")
os.mkdir(td)
os.chdir(td)
# topDir volA volA/*dirA .volB .volB/*dirB
spaths = " t .t a a/da a/.da .b .b/db .b/.db"
for n, dirpath in enumerate(spaths.split(" ")):
if dirpath:
os.makedirs(dirpath)
for pfx in "f", ".f":
filepath = pfx + str(n)
if dirpath:
filepath = os.path.join(dirpath, filepath)
with open(filepath, "wb") as f:
f.write(filepath.encode("utf-8"))
vcfg = [
".::r,u1:r.,u2",
"a:a:r,u1:r,u2",
".b:.b:r.,u1:r,u2"
]
self.args = Cfg(v=vcfg, a=["u1:u1", "u2:u2"], e2dsa=True)
self.asrv = AuthSrv(self.args, self.log)
self.assertEqual(self.tardir("", "u1"), "f0 t/f1 a/f3 a/da/f4")
self.assertEqual(self.tardir(".t", "u1"), "f2")
self.assertEqual(self.tardir(".b", "u1"), ".f6 f6 .db/.f8 .db/f8 db/.f7 db/f7")
zs = ".f0 f0 .t/.f2 .t/f2 t/.f1 t/f1 .b/f6 .b/db/f7 a/f3 a/da/f4"
self.assertEqual(self.tardir("", "u2"), zs)
self.assertEqual(self.curl("?tar", "x")[1][:17], "\nJ2EOT")
# search
up2k = Up2k(self)
u2idx = U2idx(self)
allvols = list(self.asrv.vfs.all_vols.values())
x = u2idx.search("u1", allvols, "", 999)
x = " ".join(sorted([x["rp"] for x in x[0]]))
# u1 can see dotfiles in volB so they should be included
xe = ".b/.db/.f8 .b/.db/f8 .b/.f6 .b/db/.f7 .b/db/f7 .b/f6 a/da/f4 a/f3 f0 t/f1"
self.assertEqual(x, xe)
x = u2idx.search("u2", allvols, "", 999)
x = " ".join(sorted([x["rp"] for x in x[0]]))
self.assertEqual(x, ".f0 .t/.f2 .t/f2 a/da/f4 a/f3 f0 t/.f1 t/f1")
self.args = Cfg(v=vcfg, a=["u1:u1", "u2:u2"], dotsrch=False)
self.asrv = AuthSrv(self.args, self.log)
u2idx = U2idx(self)
x = u2idx.search("u1", self.asrv.vfs.all_vols.values(), "", 999)
x = " ".join(sorted([x["rp"] for x in x[0]]))
# u1 can see dotfiles in volB so they should be included
xe = "a/da/f4 a/f3 f0 t/f1"
self.assertEqual(x, xe)
def tardir(self, url, uname):
h, b = self.curl("/" + url + "?tar", uname, True)
tar = tarfile.open(fileobj=io.BytesIO(b), mode="r|").getnames()
top = ("top" if not url else url.lstrip(".").split("/")[0]) + "/"
assert len(tar) == len([x for x in tar if x.startswith(top)])
return " ".join([x[len(top):] for x in tar])
def curl(self, url, uname, binary=False):
conn = tu.VHttpConn(self.args, self.asrv, self.log, hdr(url, uname))
HttpCli(conn).run()
if binary:
h, b = conn.s._reply.split(b"\r\n\r\n", 1)
return [h.decode("utf-8"), b]
return conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
def log(self, src, msg, c=0):
print(msg)

View file

@ -7,7 +7,6 @@ import os
import shutil import shutil
import tempfile import tempfile
import unittest import unittest
from textwrap import dedent
from copyparty import util from copyparty import util
from copyparty.authsrv import VFS, AuthSrv from copyparty.authsrv import VFS, AuthSrv
@ -175,11 +174,11 @@ class TestVFS(unittest.TestCase):
self.assertEqual(len(vfs.nodes), 1) self.assertEqual(len(vfs.nodes), 1)
self.assertEqual(n.vpath, "a") self.assertEqual(n.vpath, "a")
self.assertEqual(n.realpath, os.path.join(td, "a")) self.assertEqual(n.realpath, os.path.join(td, "a"))
self.assertAxs(n.axs.uread, ["*"]) self.assertAxs(n.axs.uread, ["*", "k"])
self.assertAxs(n.axs.uwrite, []) self.assertAxs(n.axs.uwrite, [])
perm_na = (False, False, False, False, False, False, False) perm_na = (False, False, False, False, False, False, False, False)
perm_rw = (True, True, False, False, False, False, False) perm_rw = (True, True, False, False, False, False, False, False)
perm_ro = (True, False, False, False, False, False, False) perm_ro = (True, False, False, False, False, False, False, False)
self.assertEqual(vfs.can_access("/", "*"), perm_na) self.assertEqual(vfs.can_access("/", "*"), perm_na)
self.assertEqual(vfs.can_access("/", "k"), perm_rw) self.assertEqual(vfs.can_access("/", "k"), perm_rw)
self.assertEqual(vfs.can_access("/a", "*"), perm_ro) self.assertEqual(vfs.can_access("/a", "*"), perm_ro)
@ -232,7 +231,7 @@ class TestVFS(unittest.TestCase):
cfg_path = os.path.join(self.td, "test.cfg") cfg_path = os.path.join(self.td, "test.cfg")
with open(cfg_path, "wb") as f: with open(cfg_path, "wb") as f:
f.write( f.write(
dedent( util.dedent(
""" """
u a:123 u a:123
u asd:fgh:jkl u asd:fgh:jkl

View file

@ -44,6 +44,7 @@ if MACOS:
from copyparty.__init__ import E from copyparty.__init__ import E
from copyparty.__main__ import init_E from copyparty.__main__ import init_E
from copyparty.util import FHC, Garda, Unrecv from copyparty.util import FHC, Garda, Unrecv
from copyparty.u2idx import U2idx
init_E(E) init_E(E)
@ -106,51 +107,59 @@ def get_ramdisk():
class Cfg(Namespace): class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None): def __init__(self, a=None, v=None, c=None, **ka0):
ka = {} ka = {}
ex = "daw dav_auth dav_inf dav_mac dav_rt dotsrch e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp exp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw rand smb th_no_crop vague_403 vc ver xdev xlink xvol" ex = "daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp exp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_lifetime no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw rand smb srch_dbg stats th_no_crop vague_403 vc ver xdev xlink xvol"
ka.update(**{k: False for k in ex.split()}) ka.update(**{k: False for k in ex.split()})
ex = "dotpart no_rescan no_sendfile no_voldump plain_ip" ex = "dotpart dotsrch no_dhash no_fastboot no_rescan no_sendfile no_voldump re_dhash plain_ip"
ka.update(**{k: True for k in ex.split()}) ka.update(**{k: True for k in ex.split()})
ex = "ah_cli ah_gen css_browser hist ipa_re js_browser no_forget no_hash no_idx nonsus_urls" ex = "ah_cli ah_gen css_browser hist ipa_re js_browser no_forget no_hash no_idx nonsus_urls"
ka.update(**{k: None for k in ex.split()}) ka.update(**{k: None for k in ex.split()})
ex = "s_thead s_tbody th_convt" ex = "hash_mt srch_time"
ka.update(**{k: 1 for k in ex.split()})
ex = "reg_cap s_thead s_tbody th_convt"
ka.update(**{k: 9 for k in ex.split()}) ka.update(**{k: 9 for k in ex.split()})
ex = "df loris re_maxage rproxy rsp_jtr rsp_slp s_wr_slp theme themes turbo" ex = "db_act df loris re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo"
ka.update(**{k: 0 for k in ex.split()}) ka.update(**{k: 0 for k in ex.split()})
ex = "ah_alg bname doctitle favico hdr_au_usr html_head lg_sbf log_fk md_sbf name textfiles unlist vname R RS SR" ex = "ah_alg bname doctitle exit favico hdr_au_usr html_head lg_sbf log_fk md_sbf name textfiles unlist vname R RS SR"
ka.update(**{k: "" for k in ex.split()}) ka.update(**{k: "" for k in ex.split()})
ex = "on403 on404 xad xar xau xban xbd xbr xbu xiu xm" ex = "on403 on404 xad xar xau xban xbd xbr xbu xiu xm"
ka.update(**{k: [] for k in ex.split()}) ka.update(**{k: [] for k in ex.split()})
ex = "exp_lg exp_md" ex = "exp_lg exp_md th_coversd"
ka.update(**{k: {} for k in ex.split()}) ka.update(**{k: {} for k in ex.split()})
ka.update(ka0)
super(Cfg, self).__init__( super(Cfg, self).__init__(
a=a or [], a=a or [],
v=v or [], v=v or [],
c=c, c=c,
E=E, E=E,
dbd="wal", dbd="wal",
s_wr_sz=512 * 1024,
th_size="320x256",
fk_salt="a" * 16, fk_salt="a" * 16,
unpost=600, lang="eng",
u2sort="s", log_badpwd=1,
u2ts="c", logout=573,
sort="href",
mtp=[],
mte={"a": True}, mte={"a": True},
mth={}, mth={},
lang="eng", mtp=[],
logout=573, s_wr_sz=512 * 1024,
sort="href",
srch_hits=99999,
th_size="320x256",
u2sort="s",
u2ts="c",
unpost=600,
warksalt="hunter2",
**ka **ka
) )
@ -186,11 +195,16 @@ class VSock(object):
class VHttpSrv(object): class VHttpSrv(object):
def __init__(self): def __init__(self, args, asrv, log):
self.args = args
self.asrv = asrv
self.log = log
self.broker = NullBroker() self.broker = NullBroker()
self.prism = None self.prism = None
self.bans = {} self.bans = {}
self.nreq = 0 self.nreq = 0
self.nsus = 0
aliases = ["splash", "browser", "browser2", "msg", "md", "mde"] aliases = ["splash", "browser", "browser2", "msg", "md", "mde"]
self.j2 = {x: J2_FILES for x in aliases} self.j2 = {x: J2_FILES for x in aliases}
@ -200,31 +214,38 @@ class VHttpSrv(object):
self.g403 = Garda("") self.g403 = Garda("")
self.gurl = Garda("") self.gurl = Garda("")
self.u2idx = None
self.ptn_cc = re.compile(r"[\x00-\x1f]") self.ptn_cc = re.compile(r"[\x00-\x1f]")
def cachebuster(self): def cachebuster(self):
return "a" return "a"
def get_u2idx(self):
self.u2idx = self.u2idx or U2idx(self)
return self.u2idx
class VHttpConn(object): class VHttpConn(object):
def __init__(self, args, asrv, log, buf): def __init__(self, args, asrv, log, buf):
self.t0 = time.time()
self.s = VSock(buf) self.s = VSock(buf)
self.sr = Unrecv(self.s, None) # type: ignore self.sr = Unrecv(self.s, None) # type: ignore
self.aclose = {}
self.addr = ("127.0.0.1", "42069") self.addr = ("127.0.0.1", "42069")
self.args = args self.args = args
self.asrv = asrv self.asrv = asrv
self.nid = None self.bans = {}
self.freshen_pwd = 0.0
self.hsrv = VHttpSrv(args, asrv, log)
self.ico = None
self.lf_url = None
self.log_func = log self.log_func = log
self.log_src = "a" self.log_src = "a"
self.lf_url = None
self.hsrv = VHttpSrv()
self.bans = {}
self.aclose = {}
self.u2fh = FHC()
self.mutex = threading.Lock() self.mutex = threading.Lock()
self.nreq = -1
self.nbyte = 0 self.nbyte = 0
self.ico = None self.nid = None
self.nreq = -1
self.thumbcli = None self.thumbcli = None
self.freshen_pwd = 0.0 self.u2fh = FHC()
self.t0 = time.time()
self.get_u2idx = self.hsrv.get_u2idx