[DB-V6]: store usernames; closes #530

This commit is contained in:
ed 2025-08-15 21:33:13 +00:00
parent 1228b5510b
commit 4df033ecc3
7 changed files with 108 additions and 46 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
function render() {
var html = ['<table id="tab"><thead><tr><th>size</th><th>who</th><th>when</th><th>age</th><th>dir</th><th>file</th></tr></thead><tbody>'];
var html = ['<table id="tab"><thead><tr><th>size</th><th>who</th><th>ip</th><th>when</th><th>age</th><th>dir</th><th>file</th></tr></thead><tbody>'];
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('<tr><td>' + sz +
'</td><td>' + (f.un || '') +
'</td><td>' + f.ip +
'</td><td>' + ts +
'</td><td>' + sa +