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 = ['
size | who | when | age | dir | file |
'];
+ var html = ['size | who | ip | when | age | dir | file |
'];
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('' + sz +
+ ' | ' + (f.un || '') +
' | ' + f.ip +
' | ' + ts +
' | ' + sa +
|