diff --git a/README.md b/README.md index 8a52d56a..5b9c04ac 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,7 @@ permissions: * `d` (delete): delete files/folders * `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 (see `fk` in examples below) +* `a` (admin): can see uploader IPs examples: * add accounts named u1, u2, u3 with passwords p1, p2, p3: `-a u1:p1 -a u2:p2 -a u3:p3` diff --git a/contrib/nixos/modules/copyparty.nix b/contrib/nixos/modules/copyparty.nix index 5c7d65c2..1e64ecdf 100644 --- a/contrib/nixos/modules/copyparty.nix +++ b/contrib/nixos/modules/copyparty.nix @@ -138,6 +138,7 @@ in { "d" (delete): permanently delete files and folders "g" (get): download files, but cannot see folder contents "G" (upget): "get", but can see filekeys of their own uploads + "a" (upget): can see uploader IPs For example: "rwmd" diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 032bcbb8..2a76aea5 100755 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -492,6 +492,7 @@ def get_sects(): "d" (delete): permanently delete files and folders "g" (get): download files, but cannot see folder contents "G" (upget): "get", but can see filekeys of their own uploads + "a" (admin): can see uploader IPs too many volflags to list here, see --help-flags @@ -1073,7 +1074,7 @@ def add_db_metadata(ap): ap2.add_argument("--mtag-vv", action="store_true", help="debug mtp settings and mutagen/ffprobe parsers") ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="add/replace metadata mapping") ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.)", - default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash") + default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash,up_ip,.up_at") ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.)", default=".vq,.aq,vc,ac,fmt,res,.fps") ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="read tag M using program BIN to parse the file") @@ -1337,11 +1338,9 @@ def main(argv: Optional[list[str]] = None) -> None: if re.match("c[^,]", opt): mod = True na.append("c," + opt[1:]) - elif re.sub("^[rwmdgG]*", "", opt) and "," not in opt: + elif re.sub("^[rwmdgGa]*", "", opt) and "," not in opt: mod = True perm = opt[0] - if perm == "a": - perm = "rw" na.append(perm + "," + opt[1:]) else: na.append(opt) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 60b506bd..ca42ba40 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -62,6 +62,7 @@ def __init__( udel: Optional[Union[list[str], set[str]]] = None, uget: Optional[Union[list[str], set[str]]] = None, upget: Optional[Union[list[str], set[str]]] = None, + uadmin: Optional[Union[list[str], set[str]]] = None, ) -> None: self.uread: set[str] = set(uread or []) self.uwrite: set[str] = set(uwrite or []) @@ -69,14 +70,11 @@ def __init__( self.udel: set[str] = set(udel or []) self.uget: set[str] = set(uget or []) self.upget: set[str] = set(upget or []) + self.uadmin: set[str] = set(uadmin or []) def __repr__(self) -> str: - return "AXS(%s)" % ( - ", ".join( - "%s=%r" % (k, self.__dict__[k]) - for k in "uread uwrite umove udel uget upget".split() - ) - ) + ks = "uread uwrite umove udel uget upget uadmin".split() + return "AXS(%s)" % (", ".join("%s=%r" % (k, self.__dict__[k]) for k in ks),) class Lim(object): @@ -435,8 +433,8 @@ def _find(self, vpath: str) -> tuple["VFS", str]: def can_access( self, vpath: str, uname: str - ) -> tuple[bool, bool, bool, bool, bool, bool]: - """can Read,Write,Move,Delete,Get,Upget""" + ) -> tuple[bool, bool, bool, bool, bool, bool, bool]: + """can Read,Write,Move,Delete,Get,Upget,Admin""" if vpath: vn, _ = self._find(undot(vpath)) else: @@ -450,6 +448,7 @@ def can_access( uname in c.udel or "*" in c.udel, uname in c.uget or "*" in c.uget, uname in c.upget or "*" in c.upget, + uname in c.uadmin or "*" in c.uadmin, ) def get( @@ -944,7 +943,7 @@ def _parse_config_file( try: self._l(ln, 5, "volume access config:") sk, sv = ln.split(":") - if re.sub("[rwmdgG]", "", sk) or not sk: + if re.sub("[rwmdgGa]", "", sk) or not sk: err = "invalid accs permissions list; " raise Exception(err) if " " in re.sub(", *", "", sv).strip(): @@ -953,7 +952,7 @@ def _parse_config_file( self._read_vol_str(sk, sv.replace(" ", ""), daxs[vp], mflags[vp]) continue except: - err += "accs entries must be 'rwmdgG: user1, user2, ...'" + err += "accs entries must be 'rwmdgGa: user1, user2, ...'" raise Exception(err) if cat == catf: @@ -989,7 +988,7 @@ def _parse_config_file( def _read_vol_str( self, lvl: str, uname: str, axs: AXS, flags: dict[str, Any] ) -> None: - if lvl.strip("crwmdgG"): + if lvl.strip("crwmdgGa"): raise Exception("invalid volflag: {},{}".format(lvl, uname)) if lvl == "c": @@ -1021,6 +1020,7 @@ def _read_vol_str( ("g", axs.uget), ("G", axs.uget), ("G", axs.upget), + ("a", axs.uadmin), ]: # b bb bbb if ch in lvl: if un == "*": @@ -1092,7 +1092,7 @@ def reload(self) -> None: if self.args.v: # list of src:dst:permset:permset:... - # permset is [,username][,username] or ,[=args] + # permset is [,username][,username] or ,[=args] for v_str in self.args.v: m = re_vol.match(v_str) if not m: @@ -1196,7 +1196,15 @@ def reload(self) -> None: all_users = {} missing_users = {} for axs in daxs.values(): - for d in [axs.uread, axs.uwrite, axs.umove, axs.udel, axs.uget, axs.upget]: + for d in [ + axs.uread, + axs.uwrite, + axs.umove, + axs.udel, + axs.uget, + axs.upget, + axs.uadmin, + ]: for usr in d: all_users[usr] = 1 if usr != "*" and usr not in acct: @@ -1611,6 +1619,7 @@ def reload(self) -> None: ["delete", "udel"], [" get", "uget"], [" upget", "upget"], + ["uadmin", "uadmin"], ]: u = list(sorted(getattr(zv.axs, attr))) u = ", ".join("\033[35meverybody\033[0m" if x == "*" else x for x in u) @@ -1756,10 +1765,19 @@ def dbg_ls(self) -> None: raise Exception("volume not found: " + zs) self.log(str({"users": users, "vols": vols, "flags": flags})) - t = "/{}: read({}) write({}) move({}) del({}) get({}) upget({})" + t = "/{}: read({}) write({}) move({}) del({}) get({}) upget({}) uadmin({})" for k, zv in self.vfs.all_vols.items(): vc = zv.axs - vs = [k, vc.uread, vc.uwrite, vc.umove, vc.udel, vc.uget, vc.upget] + vs = [ + k, + vc.uread, + vc.uwrite, + vc.umove, + vc.udel, + vc.uget, + vc.upget, + vc.uadmin, + ] self.log(t.format(*vs)) flag_v = "v" in flags @@ -1898,6 +1916,7 @@ def cgen(self) -> None: "d": "udel", "g": "uget", "G": "upget", + "a": "uadmin", } users = {} for pkey in perms.values(): @@ -2094,7 +2113,7 @@ def upgrade_cfg_fmt( else: sn = sn.replace(",", ", ") ret.append(" " + sn) - elif sn[:1] in "rwmdgG": + elif sn[:1] in "rwmdgGa": if cat != catx: cat = catx ret.append(cat) diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index 368c787d..d5ae8ea9 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -134,6 +134,7 @@ def __init__( self.can_read = self.can_write = self.can_move = False self.can_delete = self.can_get = self.can_upget = False + self.can_admin = False self.listdirinfo = self.listdir self.chdir(".") @@ -168,7 +169,7 @@ def v2a( if not avfs: 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: raise FSE(t.format(vpath), 1) @@ -243,6 +244,7 @@ def chdir(self, path: str) -> None: self.can_delete, self.can_get, self.can_upget, + self.can_admin, ) = avfs.can_access("", self.h.uname) def mkdir(self, path: str) -> None: diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 9e4703e7..c7e5689b 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -153,6 +153,7 @@ def __init__(self, conn: "HttpConn") -> None: self.can_delete = False self.can_get = False self.can_upget = False + self.can_admin = False # post self.parser: Optional[MultipartParser] = None # end placeholders @@ -431,6 +432,7 @@ def run(self) -> bool: self.can_delete, self.can_get, self.can_upget, + self.can_admin, ) = ( avn.can_access("", self.uname) if avn else [False] * 6 ) @@ -782,6 +784,7 @@ def handle_get(self) -> bool: self.log("plugin override; access permitted") self.can_read = self.can_write = self.can_move = True self.can_delete = self.can_get = self.can_upget = True + self.can_admin = True else: return self.tx_404(True) else: @@ -3535,6 +3538,8 @@ def tx_browser(self) -> bool: perms.append("get") if self.can_upget: perms.append("upget") + if self.can_admin: + perms.append("admin") url_suf = self.urlq({}, ["k"]) is_ls = "ls" in self.uparam @@ -3786,22 +3791,33 @@ def tx_browser(self) -> bool: if vn != dbv: _, rd = vn.get_dbv(rd) + erd_efn = (rd, fn) q = "select mt.k, mt.v from up inner join mt on mt.w = substr(up.w,1,16) where up.rd = ? and up.fn = ? and +mt.k != 'x'" try: - r = icur.execute(q, (rd, fn)) + r = icur.execute(q, erd_efn) except Exception as ex: if "database is locked" in str(ex): break try: - args = s3enc(idx.mem_cur, rd, fn) - r = icur.execute(q, args) + erd_efn = s3enc(idx.mem_cur, rd, fn) + r = icur.execute(q, erd_efn) except: t = "tag read error, {}/{}\n{}" self.log(t.format(rd, fn, min_ex())) break fe["tags"] = {k: v for k, v in r} + + if self.can_admin: + q = "select ip, at from up where rd=? and fn=?" + try: + zs1, zs2 = icur.execute(q, erd_efn).fetchone() + fe["tags"]["up_ip"] = zs1 + fe["tags"][".up_at"] = zs2 + except: + pass + _ = [tagset.add(k) for k in fe["tags"]] if icur: diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 15c2ed00..33cb799a 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -5780,14 +5780,18 @@ var treectl = (function () { for (var b = 0; b < res.taglist.length; b++) { var k = res.taglist[b], - v = (tn.tags || {})[k] || ""; + v = (tn.tags || {})[k] || "", + sv = null; - if (k == ".dur") { - var sv = v ? s2ms(v) : ""; - ln[ln.length - 1] += '' + sv; + if (k == ".dur") + sv = v ? s2ms(v) : ""; + else if (k == ".up_at") + sv = v ? unix2iso(v) : ""; + else { + ln.push(v); continue; } - ln.push(v); + ln[ln.length - 1] += '' + sv; } ln = ln.concat([tn.ext, unix2iso(tn.ts)]).join(''); html.push(ln + ''); @@ -6066,7 +6070,7 @@ function apply_perms(res) { var axs = [], aclass = '>', - chk = ['read', 'write', 'move', 'delete', 'get']; + chk = ['read', 'write', 'move', 'delete', 'get', 'admin']; for (var a = 0; a < chk.length; a++) if (has(perms, chk[a])) diff --git a/tests/test_vfs.py b/tests/test_vfs.py index 27f1436a..bd26f8dd 100644 --- a/tests/test_vfs.py +++ b/tests/test_vfs.py @@ -178,9 +178,9 @@ def test(self): self.assertEqual(n.realpath, os.path.join(td, "a")) self.assertAxs(n.axs.uread, ["*"]) self.assertAxs(n.axs.uwrite, []) - perm_na = (False, False, False, False, False, False) - perm_rw = (True, True, False, False, False, False) - perm_ro = (True, False, False, False, False, False) + perm_na = (False, False, False, False, False, False, False) + perm_rw = (True, True, False, False, False, False, False) + perm_ro = (True, False, False, False, False, False, False) self.assertEqual(vfs.can_access("/", "*"), perm_na) self.assertEqual(vfs.can_access("/", "k"), perm_rw) self.assertEqual(vfs.can_access("/a", "*"), perm_ro)