diff --git a/copyparty/__main__.py b/copyparty/__main__.py index dcd78d8c..01216722 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1120,6 +1120,7 @@ def add_upload(ap): ap2.add_argument("--put-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for PUT/WebDAV uploads: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=put_ck)") ap2.add_argument("--bup-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for bup/basic-uploader: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=bup_ck)") ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h") + ap2.add_argument("--unp-who", metavar="NUM", type=int, default=1, help="clients can undo recent uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=unp_who)") ap2.add_argument("--u2abort", metavar="NUM", type=int, default=1, help="clients can abort incomplete uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=u2abort)") ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)") ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600") diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 5b1651c4..3a7a205b 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -118,6 +118,7 @@ def vf_vmap() -> dict[str, str]: "u2ts", "uid", "gid", + "unp_who", "ups_who", "zip_who", "zipmaxn", @@ -343,6 +344,7 @@ flagcats = { "dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so', "rss": "allow '?rss' URL suffix (experimental)", "rmagic": "expensive analysis for mimetype accuracy", + "unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name", "ups_who=2": "restrict viewing the list of recent uploads", "zip_who=2": "restrict access to download-as-zip/tar", "zipmaxn=9k": "reject download-as-zip if more than 9000 files", diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 4710feaf..de3e6ede 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -5501,6 +5501,10 @@ class HttpCli(object): and ("*" in x.axs.uwrite or self.uname in x.axs.uwrite or x == shr_dbv) ] + q = "" + qp = (0,) + q_c = -1 + for vol in allvols: cur = idx.get_cur(vol) if not cur: @@ -5508,9 +5512,23 @@ class HttpCli(object): nfk, fk_alg = fk_vols.get(vol) or (0, 0) + zi = vol.flags["unp_who"] + if q_c != zi: + q_c = zi + q = "select sz, rd, fn, at from up where " + if zi == 1: + q += "ip=? and un=?" + qp = (self.ip, self.uname, lim) + elif zi == 2: + q += "ip=?" + qp = (self.ip, lim) + if zi == 3: + q += "un=?" + qp = (self.uname, lim) + q += " and at>? order by at desc" + n = 2000 - q = "select sz, rd, fn, at from up where ip=? and at>? order by at desc" - for sz, rd, fn, at in cur.execute(q, (self.ip, lim)): + for sz, rd, fn, at in cur.execute(q, qp): vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x) if nfi == 0 or (nfi == 1 and vfi in vp.lower()): pass @@ -5635,8 +5653,8 @@ class HttpCli(object): continue n = 1000 - q = "select sz, rd, fn, ip, at from up where at>0 order by at desc" - for sz, rd, fn, ip, at in cur.execute(q): + q = "select sz, rd, fn, ip, at, un from up where at>0 order by at desc" + for sz, rd, fn, ip, at, un in cur.execute(q): vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x) if nfi == 0 or (nfi == 1 and vfi in vp.lower()): pass @@ -5657,6 +5675,7 @@ class HttpCli(object): "sz": sz, "ip": ip, "at": at, + "un": un, "nfk": nfk, "adm": adm, } @@ -5701,12 +5720,16 @@ class HttpCli(object): adm = rv.pop("adm") if not adm: rv["ip"] = "(You)" if rv["ip"] == self.ip else "(?)" + if rv["un"] not in ("*", self.uname): + rv["un"] = "(?)" else: for rv in ret: adm = rv.pop("adm") if not adm: rv["ip"] = "(You)" if rv["ip"] == self.ip else "(?)" rv["at"] = 0 + if rv["un"] not in ("*", self.uname): + rv["un"] = "(?)" if self.is_vproxied: for v in ret: @@ -6628,13 +6651,15 @@ class HttpCli(object): tags = {k: v for k, v in r} if is_admin: - q = "select ip, at from up where rd=? and fn=?" + q = "select ip, at, un from up where rd=? and fn=?" try: - zs1, zs2 = icur.execute(q, erd_efn).fetchone() + zs1, zs2, zs3 = icur.execute(q, erd_efn).fetchone() if zs1: tags["up_ip"] = zs1 if zs2: tags[".up_at"] = zs2 + if zs3: + tags["up_by"] = zs3 except: pass elif add_up_at: @@ -6655,7 +6680,7 @@ class HttpCli(object): lmte = list(mte) if self.can_admin: - lmte.extend(("up_ip", ".up_at")) + lmte.extend(("up_by", "up_ip", ".up_at")) if "nodirsz" not in vf: tagset.add(".files") diff --git a/copyparty/u2idx.py b/copyparty/u2idx.py index ff0e329f..45b479c5 100644 --- a/copyparty/u2idx.py +++ b/copyparty/u2idx.py @@ -391,7 +391,7 @@ class U2idx(object): fk_alg = 2 if "fka" in flags else 1 c = cur.execute(uq, tuple(vuv)) for hit in c: - w, ts, sz, rd, fn, ip, at = hit[:7] + w, ts, sz, rd, fn = hit[:5] if rd.startswith("//") or fn.startswith("//"): rd, fn = s3dec(rd, fn) diff --git a/copyparty/up2k.py b/copyparty/up2k.py index f4d6d66f..7ae6441b 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -77,7 +77,7 @@ except: if HAVE_SQLITE3: import sqlite3 -DB_VER = 5 +DB_VER = 6 if True: # pylint: disable=using-constant-test from typing import Any, Optional, Pattern, Union @@ -1655,7 +1655,7 @@ class Up2k(object): abspath = cdirs + fn nohash = reh.search(abspath) if reh else False - sql = "select w, mt, sz, ip, at from up where rd = ? and fn = ?" + sql = "select w, mt, sz, ip, at, un from up where rd = ? and fn = ?" try: c = db.c.execute(sql, (rd, fn)) except: @@ -1664,7 +1664,7 @@ class Up2k(object): in_db = list(c.fetchall()) if in_db: self.pp.n -= 1 - dw, dts, dsz, ip, at = in_db[0] + dw, dts, dsz, ip, at, un = in_db[0] if len(in_db) > 1: t = "WARN: multiple entries: %r => %r |%d|\n%r" rep_db = "\n".join([repr(x) for x in in_db]) @@ -1677,6 +1677,9 @@ class Up2k(object): if dts == lmod and dsz == sz and (nohash or dw[0] != "#" or not sz): continue + if un is None: + un = "" + t = "reindex %r => %r mtime(%s/%s) size(%s/%s)" self.log(t % (top, rp, dts, lmod, dsz, sz)) self.db_rm(db.c, rd, fn, 0) @@ -1687,6 +1690,7 @@ class Up2k(object): dw = "" ip = "" at = 0 + un = "" self.pp.msg = "a%d %s" % (self.pp.n, abspath) @@ -1712,9 +1716,10 @@ class Up2k(object): if dw and dw != wark: ip = "" at = 0 + un = "" # skip upload hooks by not providing vflags - self.db_add(db.c, {}, rd, fn, lmod, sz, "", "", wark, wark, "", "", ip, at) + self.db_add(db.c, {}, rd, fn, lmod, sz, "", "", wark, wark, "", un, ip, at) db.n += 1 db.nf += 1 tfa += 1 @@ -2151,8 +2156,8 @@ class Up2k(object): with self.mutex: try: - q = "select rd, fn, ip, at from up where substr(w,1,16)=? and +w=?" - rd, fn, ip, at = cur.execute(q, (w16, w)).fetchone() + q = "select rd, fn, ip, at, un from up where substr(w,1,16)=? and +w=?" + rd, fn, ip, at, un = cur.execute(q, (w16, w)).fetchone() except: # file modified/deleted since spooling continue @@ -2171,12 +2176,15 @@ class Up2k(object): abspath = djoin(ptop, rd, fn) self.pp.msg = "c%d %s" % (nq, abspath) if not mpool: - n_tags = self._tagscan_file(cur, entags, w, abspath, ip, at) + n_tags = self._tagscan_file(cur, entags, w, abspath, ip, at, un) else: + oth_tags = {} if ip: - oth_tags = {"up_ip": ip, "up_at": at} - else: - oth_tags = {} + oth_tags["up_ip"] = ip + if at: + oth_tags["up_at"] = at + if un: + oth_tags["up_by"] = un mpool.put(Mpqe({}, entags, w, abspath, oth_tags)) with self.mutex: @@ -2332,8 +2340,8 @@ class Up2k(object): if w in in_progress: continue - q = "select rd, fn, ip, at from up where substr(w,1,16)=? limit 1" - rd, fn, ip, at = cur.execute(q, (w,)).fetchone() + q = "select rd, fn, ip, at, un from up where substr(w,1,16)=? limit 1" + rd, fn, ip, at, un = cur.execute(q, (w,)).fetchone() rd, fn = s3dec(rd, fn) abspath = djoin(ptop, rd, fn) @@ -2357,7 +2365,10 @@ class Up2k(object): if ip: oth_tags["up_ip"] = ip + if at: oth_tags["up_at"] = at + if un: + oth_tags["up_by"] = un jobs.append(Mpqe(parsers, set(), w, abspath, oth_tags)) in_progress[w] = True @@ -2546,6 +2557,7 @@ class Up2k(object): abspath: str, ip: str, at: float, + un: Optional[str], ) -> int: """will mutex(main)""" assert self.mtag # !rm @@ -2566,7 +2578,10 @@ class Up2k(object): if ip: tags["up_ip"] = ip + if at: tags["up_at"] = at + if un: + tags["up_by"] = un with self.mutex: return self._tag_file(write_cur, entags, wark, abspath, tags) @@ -2670,16 +2685,19 @@ class Up2k(object): if not existed and ver is None: return self._try_create_db(db_path, cur) - if ver == 4: + for upver in (4, 5): + if ver != upver: + continue try: t = "creating backup before upgrade: " cur = self._backup_db(db_path, cur, ver, t) - self._upgrade_v4(cur) - ver = 5 + getattr(self, "_upgrade_v%d" % (upver,))(cur) + ver += 1 # type: ignore except: - self.log("WARN: failed to upgrade from v4", 3) + self.log("WARN: failed to upgrade from v%d" % (ver,), 3) if ver == DB_VER: + # these no longer serve their intended purpose but they're great as additional sanchks self._add_dhash_tab(cur) self._add_xiu_tab(cur) self._add_cv_tab(cur) @@ -2781,7 +2799,7 @@ class Up2k(object): idx = r"create index up_w on up(w)" for cmd in [ - r"create table up (w text, mt int, sz int, rd text, fn text, ip text, at int)", + r"create table up (w text, mt int, sz int, rd text, fn text, ip text, at int, un text)", r"create index up_vp on up(rd, fn)", r"create index up_fn on up(fn)", r"create index up_ip on up(ip)", @@ -2814,6 +2832,15 @@ class Up2k(object): cur.connection.commit() + def _upgrade_v5(self, cur: "sqlite3.Cursor") -> None: + for cmd in [ + r"alter table up add column un text", + r"update kv set v=6 where k='sver'", + ]: + cur.execute(cmd) + + cur.connection.commit() + def _add_dhash_tab(self, cur: "sqlite3.Cursor") -> None: # v5 -> v5a try: @@ -3011,7 +3038,7 @@ class Up2k(object): argv = [dwark[:16], dwark] c2 = cur.execute(q, tuple(argv)) - for _, dtime, dsize, dp_dir, dp_fn, ip, at in c2: + for _, dtime, dsize, dp_dir, dp_fn, ip, at, _ in c2: if dp_dir.startswith("//") or dp_fn.startswith("//"): dp_dir, dp_fn = s3dec(dp_dir, dp_fn) @@ -3433,7 +3460,7 @@ class Up2k(object): try: vrel = vjoin(job["prel"], fname) xlink = bool(vf.get("xlink")) - cur, wark, _, _, _, _ = self._find_from_vpath(ptop, vrel) + cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, vrel) self._forget_file(ptop, vrel, cur, wark, True, st.st_size, xlink) except Exception as ex: self.log("skipping replace-relink: %r" % (ex,)) @@ -3890,14 +3917,14 @@ class Up2k(object): # plugins may expect this to look like an actual IP db_ip = "1.1.1.1" if "no_db_ip" in vflags else ip - sql = "insert into up values (?,?,?,?,?,?,?)" - v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0)) + sql = "insert into up values (?,?,?,?,?,?,?,?)" + v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0), usr) try: db.execute(sql, v) except: assert self.mem_cur # !rm rd, fn = s3enc(self.mem_cur, rd, fn) - v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0)) + v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0), usr) db.execute(sql, v) self.volsize[db] += sz @@ -4038,7 +4065,7 @@ class Up2k(object): vn, rem = vn0.get_dbv(rem0) ptop = vn.realpath with self.mutex, self.reg_mutex: - abrt_cfg = self.flags.get(ptop, {}).get("u2abort", 1) + abrt_cfg = vn.flags.get("u2abort", 1) addr = (ip or "\n") if abrt_cfg in (1, 2) else "" user = ((uname or "\n"), "*") if abrt_cfg in (1, 3) else None reg = self.registry.get(ptop, {}) if abrt_cfg else {} @@ -4059,17 +4086,22 @@ class Up2k(object): if partial: dip = ip dat = time.time() + dun = uname + un_cfg = 1 else: - if not self.args.unpost: + un_cfg = vn.flags["unp_who"] + if not self.args.unpost or not un_cfg: t = "the unpost feature is disabled in server config" raise Pebkac(400, t) - _, _, _, _, dip, dat = self._find_from_vpath(ptop, rem) + _, _, _, _, dip, dat, dun = self._find_from_vpath(ptop, rem) t = "you cannot delete this: " if not dip: t += "file not found" - elif dip != ip: + elif dip != ip and un_cfg in (1, 2): + t += "not uploaded by (You)" + elif dun != uname and un_cfg in (1, 3): t += "not uploaded by (You)" elif dat < time.time() - self.args.unpost: t += "uploaded too long ago" @@ -4158,7 +4190,7 @@ class Up2k(object): try: ptop = dbv.realpath xlink = bool(dbv.flags.get("xlink")) - cur, wark, _, _, _, _ = self._find_from_vpath(ptop, volpath) + cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, volpath) self._forget_file( ptop, volpath, cur, wark, True, st.st_size, xlink ) @@ -4319,7 +4351,7 @@ class Up2k(object): bos.makedirs(os.path.dirname(dabs), vf=dvn.flags) - c1, w, ftime_, fsize_, ip, at = self._find_from_vpath( + c1, w, ftime_, fsize_, ip, at, un = self._find_from_vpath( svn_dbv.realpath, srem_dbv ) c2 = self.cur.get(dvn.realpath) @@ -4344,7 +4376,7 @@ class Up2k(object): w, w, "", - "", + un or "", ip or "", at or 0, ) @@ -4605,7 +4637,7 @@ class Up2k(object): return "k" - c1, w, ftime_, fsize_, ip, at = self._find_from_vpath(svn.realpath, srem) + c1, w, ftime_, fsize_, ip, at, un = self._find_from_vpath(svn.realpath, srem) c2 = self.cur.get(dvn.realpath) has_dupes = False @@ -4639,7 +4671,7 @@ class Up2k(object): w, w, "", - "", + un or "", ip or "", at or 0, ) @@ -4739,13 +4771,14 @@ class Up2k(object): Optional[int], str, Optional[int], + str, ]: cur = self.cur.get(ptop) if not cur: - return None, None, None, None, "", None + return None, None, None, None, "", None, "" rd, fn = vsplit(vrem) - q = "select w, mt, sz, ip, at from up where rd=? and fn=? limit 1" + q = "select w, mt, sz, ip, at, un from up where rd=? and fn=? limit 1" try: c = cur.execute(q, (rd, fn)) except: @@ -4754,9 +4787,9 @@ class Up2k(object): hit = c.fetchone() if hit: - wark, ftime, fsize, ip, at = hit - return cur, wark, ftime, fsize, ip, at - return cur, None, None, None, "", None + wark, ftime, fsize, ip, at, un = hit + return cur, wark, ftime, fsize, ip, at, un + return cur, None, None, None, "", None, "" def _forget_file( self, diff --git a/copyparty/util.py b/copyparty/util.py index 03517785..7e9d4bb1 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -3602,7 +3602,7 @@ def runihook( verbose: bool, cmd: str, vol: "VFS", - ups: list[tuple[str, int, int, str, str, str, int]], + ups: list[tuple[str, int, int, str, str, str, int, str]], ) -> bool: _, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd) bcmd = [sfsenc(x) for x in acmd] diff --git a/copyparty/web/rups.js b/copyparty/web/rups.js index 17a44556..e4e8b3ce 100644 --- a/copyparty/web/rups.js +++ b/copyparty/web/rups.js @@ -1,5 +1,5 @@ function render() { - var html = ['']; + var html = ['
sizewhowhenagedirfile
']; var ups = V.ups, now = V.now; ebi('filter').value = V.filter; ebi('hits').innerHTML = 'showing ' + ups.length + ' files'; @@ -16,6 +16,7 @@ function render() { sz = ('' + f.sz).replace(/\B(?=(\d{3})+(?!\d))/g, " "); html.push('
sizewhoipwhenagedirfile
' + sz + + '' + (f.un || '') + '' + f.ip + '' + ts + '' + sa +