list recent uploads

also makes the unpost lister 5x faster
This commit is contained in:
ed 2024-12-18 22:17:30 +01:00
parent 3051b13108
commit eaa4b04a22
16 changed files with 405 additions and 23 deletions

View file

@ -48,6 +48,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [shares](#shares) - share a file or folder by creating a temporary link
* [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI
* [rss feeds](#rss-feeds) - monitor a folder with your RSS reader
* [recent uploads](#recent-uploads) - list all recent uploads
* [media player](#media-player) - plays almost every audio format there is
* [audio equalizer](#audio-equalizer) - and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression)
* [fix unreliable playback on android](#fix-unreliable-playback-on-android) - due to phone / app settings
@ -717,7 +718,7 @@ files go into `[ok]` if they exist (and you get a link to where it is), otherwis
### unpost
undo/delete accidental uploads
undo/delete accidental uploads using the `[🧯]` tab in the UI
![copyparty-unpost-fs8](https://user-images.githubusercontent.com/241032/129635368-3afa6634-c20f-418c-90dc-ec411f3b3897.png)
@ -876,6 +877,17 @@ url parameters:
* uppercase = reverse-sort; `M` = oldest file first
## recent uploads
list all recent uploads by clicking "show recent uploads" in the controlpanel
will show uploader IP and upload-time if the visitor has the admin permission
* global-option `--ups-when` makes upload-time visible to all users, and not just admins
note that the [🧯 unpost](#unpost) feature is better suited for viewing *your own* recent uploads, as it includes the option to undo/delete them
## media player
plays almost every audio format there is (if the server has FFmpeg installed for on-demand transcoding)

View file

@ -91,6 +91,9 @@ web/mde.html
web/mde.js
web/msg.css
web/msg.html
web/rups.css
web/rups.html
web/rups.js
web/shares.css
web/shares.html
web/shares.js

View file

@ -1250,7 +1250,6 @@ def add_optouts(ap):
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)")
ap2.add_argument("--no-lifetime", action="store_true", help="do not allow clients (or server config) to schedule an upload to be deleted after a given time")
ap2.add_argument("--no-up-list", action="store_true", help="don't show list of incoming files in controlpanel")
ap2.add_argument("--no-pipe", action="store_true", help="disable race-the-beam (lockstep download of files which are currently being uploaded) (volflag=nopipe)")
ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader IPs into the database")
@ -1326,7 +1325,10 @@ def add_admin(ap):
ap2.add_argument("--no-reload", action="store_true", help="disable ?reload=cfg (reload users/volumes/volflags from config file)")
ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)")
ap2.add_argument("--no-stack", action="store_true", help="disable ?stack (list all stacks)")
ap2.add_argument("--no-ups-page", action="store_true", help="disable ?ru (list of recent uploads)")
ap2.add_argument("--no-up-list", action="store_true", help="don't show list of incoming files in controlpanel")
ap2.add_argument("--dl-list", metavar="LVL", type=int, default=2, help="who can see active downloads in the controlpanel? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=everyone")
ap2.add_argument("--ups-when", action="store_true", help="let everyone see upload timestamps on the ?ru page, not just admins")
def add_thumbnail(ap):

View file

@ -1233,6 +1233,9 @@ class HttpCli(object):
if "dls" in self.uparam:
return self.tx_dls()
if "ru" in self.uparam:
return self.tx_rups()
if "h" in self.uparam:
return self.tx_mounts()
@ -4919,9 +4922,9 @@ class HttpCli(object):
raise Pebkac(500, "sqlite3 not found on server; unpost is disabled")
raise Pebkac(500, "server busy, cannot unpost; please retry in a bit")
filt = self.uparam.get("filter") or ""
lm = "ups %r" % (filt,)
self.log(lm)
zs = self.uparam.get("filter") or ""
filt = re.compile(zs, re.I) if zs else None
lm = "ups %r" % (zs,)
if self.args.shr and self.vpath.startswith(self.args.shr1):
shr_dbv, shr_vrem = self.vn.get_dbv(self.rem)
@ -4962,13 +4965,18 @@ class HttpCli(object):
nfk, fk_alg = fk_vols.get(vol) or (0, 0)
q = "select sz, rd, fn, at from up where ip=? and at>?"
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)):
vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x)
if filt and filt not in vp:
if filt and not filt.search(vp):
continue
rv = {"vp": quotep(vp), "sz": sz, "at": at, "nfk": nfk}
n -= 1
if not n:
break
rv = {"vp": vp, "sz": sz, "at": at, "nfk": nfk}
if nfk:
rv["ap"] = vol.canonical(vjoin(rd, fn))
rv["fk_alg"] = fk_alg
@ -4978,9 +4986,13 @@ class HttpCli(object):
ret.sort(key=lambda x: x["at"], reverse=True) # type: ignore
ret = ret[:2000]
if len(ret) > 2000:
ret = ret[:2000]
ret.sort(key=lambda x: x["at"], reverse=True) # type: ignore
n = 0
for rv in ret[:11000]:
for rv in ret:
rv["vp"] = quotep(rv["vp"])
nfk = rv.pop("nfk")
if not nfk:
continue
@ -4997,12 +5009,6 @@ class HttpCli(object):
)
rv["vp"] += "?k=" + fk[:nfk]
n += 1
if n > 2000:
break
ret = ret[:2000]
if shr_dbv:
# translate vpaths from share-target to share-url
# to satisfy access checks
@ -5026,6 +5032,125 @@ class HttpCli(object):
self.reply(jtxt.encode("utf-8", "replace"), mime="application/json")
return True
def tx_rups(self) -> bool:
if self.args.no_ups_page:
raise Pebkac(500, "listing of recent uploads is disabled in server config")
idx = self.conn.get_u2idx()
if not idx or not hasattr(idx, "p_end"):
if not HAVE_SQLITE3:
raise Pebkac(500, "sqlite3 not found on server; recent-uploads n/a")
raise Pebkac(500, "server busy, cannot list recent uploads; please retry")
sfilt = self.uparam.get("filter") or ""
filt = re.compile(sfilt, re.I) if sfilt else None
lm = "ru %r" % (sfilt,)
self.log(lm)
ret: list[dict[str, Any]] = []
t0 = time.time()
allvols = [
x
for x in self.asrv.vfs.all_vols.values()
if "e2d" in x.flags and ("*" in x.axs.uread or self.uname in x.axs.uread)
]
fk_vols = {
vol: (vol.flags["fk"], 2 if "fka" in vol.flags else 1)
for vol in allvols
if "fk" in vol.flags and "*" not in vol.axs.uread
}
for vol in allvols:
cur = idx.get_cur(vol)
if not cur:
continue
nfk, fk_alg = fk_vols.get(vol) or (0, 0)
adm = "*" in vol.axs.uadmin or self.uname in vol.axs.uadmin
dots = "*" in vol.axs.udot or self.uname in vol.axs.udot
n = 2000
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):
vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x)
if filt and not filt.search(vp):
continue
if not dots and "/." in vp:
continue
n -= 1
if not n:
break
rv = {
"vp": vp,
"sz": sz,
"ip": ip,
"at": at,
"nfk": nfk,
"adm": adm,
}
if nfk:
rv["ap"] = vol.canonical(vjoin(rd, fn))
rv["fk_alg"] = fk_alg
ret.append(rv)
if len(ret) > 3000:
ret.sort(key=lambda x: x["at"], reverse=True) # type: ignore
ret = ret[:2000]
if len(ret) > 2000:
ret = ret[:2000]
ret.sort(key=lambda x: x["at"], reverse=True) # type: ignore
for rv in ret:
rv["evp"] = quotep(rv["vp"])
nfk = rv.pop("nfk")
if not nfk:
continue
alg = rv.pop("fk_alg")
ap = rv.pop("ap")
try:
st = bos.stat(ap)
except:
continue
fk = self.gen_fk(
alg, self.args.fk_salt, ap, st.st_size, 0 if ANYWIN else st.st_ino
)
rv["vp"] += "?k=" + fk[:nfk]
if self.args.ups_when:
for rv in ret:
adm = rv.pop("adm")
if not adm:
rv["ip"] = "(You)" if rv["ip"] == self.ip else "(?)"
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 self.is_vproxied:
for v in ret:
v["vp"] = self.args.SR + v["vp"]
self.log("%s #%d %.2fsec" % (lm, len(ret), time.time() - t0))
if "j" in self.ouparam:
jtxt = json.dumps(ret, separators=(",\n", ": "))
self.reply(jtxt.encode("utf-8", "replace"), mime="application/json")
return True
rows = [[x["vp"], x["evp"], x["sz"], x["ip"], x["at"]] for x in ret]
html = self.j2s("rups", this=self, rows=rows, filt=sfilt, now=int(time.time()))
self.reply(html.encode("utf-8"), status=200)
return True
def tx_shares(self) -> bool:
if self.uname == "*":
self.loud_reply("you're not logged in")

View file

@ -172,15 +172,16 @@ class HttpSrv(object):
env = jinja2.Environment()
env.loader = jinja2.FunctionLoader(lambda f: load_jinja2_resource(self.E, f))
jn = [
"splash",
"shares",
"svcs",
"browser",
"browser2",
"msg",
"cf",
"md",
"mde",
"cf",
"msg",
"rups",
"shares",
"splash",
"svcs",
]
self.j2 = {x: env.get_template(x + ".html") for x in jn}
self.prism = has_resource(self.E, "web/deps/prism.js.gz")

113
copyparty/web/rups.css Normal file
View file

@ -0,0 +1,113 @@
html {
color: #333;
background: #f7f7f7;
font-family: sans-serif;
font-family: var(--font-main), sans-serif;
touch-action: manipulation;
}
#wrap {
margin: 2em auto;
padding: 0 1em 3em 1em;
line-height: 2.3em;
}
form {
display: inline;
padding-left: 1em;
}
input[type=submit],
a {
color: #047;
background: #fff;
text-decoration: none;
border: none;
border-bottom: 1px solid #8ab;
border-radius: .2em;
padding: .2em .6em;
margin: 0 .3em;
}
#wrap td a {
margin: 0;
line-height: 1em;
display: inline-block;
white-space: initial;
font-family: var(--font-main), sans-serif;
}
#repl {
border: none;
background: none;
color: inherit;
padding: 0;
position: fixed;
bottom: .25em;
left: .2em;
}
#wrap table {
border-collapse: collapse;
position: relative;
margin-top: 2em;
}
#wrap th {
top: -1px;
position: sticky;
background: #f7f7f7;
}
#wrap td {
font-family: var(--font-mono), monospace, monospace;
white-space: pre;
}
#wrap th:first-child,
#wrap td:first-child {
text-align: right;
}
#wrap td,
#wrap th {
text-align: left;
padding: .3em .6em;
max-width: 30vw;
}
#wrap tr:hover td {
background: #ddd;
box-shadow: 0 -1px 0 rgba(128, 128, 128, 0.5) inset;
}
#wrap th:first-child,
#wrap td:first-child {
border-radius: .5em 0 0 .5em;
}
#wrap th:last-child,
#wrap td:last-child {
border-radius: 0 .5em .5em 0;
}
html.z {
background: #222;
color: #ccc;
}
html.bz {
background: #11121d;
color: #bbd;
}
html.z input[type=submit],
html.z a {
color: #fff;
background: #057;
border-color: #37a;
}
html.z input[type=text] {
color: #ddd;
background: #223;
border: none;
border-bottom: 1px solid #fc5;
border-radius: .2em;
padding: .2em .3em;
}
html.z #wrap th {
background: #222;
}
html.bz #wrap th {
background: #223;
}
html.z #wrap tr:hover td {
background: #000;
}

67
copyparty/web/rups.html Normal file
View file

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ s_doctitle }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.8">
<meta name="theme-color" content="#{{ tcolor }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/rups.css?_={{ ts }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
{{ html_head }}
</head>
<body>
<div id="wrap">
<a id="a" href="{{ r }}/?ru" class="af">refresh</a>
<a id="a" href="{{ r }}/?h" class="af">control-panel</a>
<form method="get" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" action="{{ r }}">
<input type="hidden" name="ru" value="a" />
Filter: <input type="text" name="filter" size="20" placeholder="documents/passwords" value="{{ filt }}" />
<input type="submit" />
</form>
<span id="hits"></span>
<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>
{% for vp, evp, sz, ip, at in rows %}
<tr>
<td>{{ sz }}</td>
<td>{{ ip }}</td>
<td>{{ at }}</td>
<td>{{ (now-at) }}</td>
<td></td>
<td><a href="{{ r }}{{ evp }}">{{ vp|e }}</a></td>
</tr>
{% endfor %}
</tbody></table>
{% if not rows %}
(the database is not aware of any uploads)
{% endif %}
</div>
<a href="#" id="repl">π</a>
<script>
var SR = {{ r|tojson }},
NOW = {{ now }},
lang="{{ lang }}",
dfavico="{{ favico }}";
var STG = window.localStorage;
document.documentElement.className = (STG && STG.cpp_thm) || "{{ this.args.theme }}";
</script>
<script src="{{ r }}/.cpr/util.js?_={{ ts }}"></script>
<script src="{{ r }}/.cpr/rups.js?_={{ ts }}"></script>
{%- if js %}
<script src="{{ js }}_={{ ts }}"></script>
{%- endif %}
</body>
</html>

34
copyparty/web/rups.js Normal file
View file

@ -0,0 +1,34 @@
(function() {
var tab = ebi('tab').tBodies[0],
tr = Array.prototype.slice.call(tab.rows, 0),
rows = [];
for (var a = 0; a < tr.length; a++) {
var td = tr[a].cells,
an = td[5].children[0];
rows.push([
td[0].textContent,
td[2].textContent,
td[3].textContent,
an.textContent,
an.getAttribute('href'),
]);
}
for (var a = 0; a < rows.length; a++) {
var t = rows[a],
sz = t[0],
at = parseInt(t[1]),
nam = vsplit(t[3]),
dh = vsplit(t[4])[0];
tr[a].cells[0].innerHTML = sz.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
tr[a].cells[2].innerHTML = at ? unix2iso(at) : '(?)';
tr[a].cells[3].innerHTML = at ? shumantime(t[2]) : '(?)';
tr[a].cells[4].innerHTML = '<a href="' + dh + '">' + nam[0] + '</a>';
tr[a].cells[5].children[0].innerHTML = nam[1].split('?')[0];
}
ebi('hits').innerHTML = '-- showing ' + rows.length + ' files';
})();

View file

@ -44,9 +44,10 @@ a {
bottom: .25em;
left: .2em;
}
table {
#wrap table {
border-collapse: collapse;
position: relative;
margin-top: 2em;
}
th {
top: -1px;
@ -62,6 +63,14 @@ th {
#wrap td+td+td+td+td+td+td+td {
font-family: var(--font-mono), monospace, monospace;
}
#wrap th:first-child,
#wrap td:first-child {
border-radius: .5em 0 0 .5em;
}
#wrap th:last-child,
#wrap td:last-child {
border-radius: 0 .5em .5em 0;
}
@ -81,3 +90,6 @@ html.bz {
color: #bbd;
background: #11121d;
}
html.bz th {
background: #223;
}

View file

@ -58,6 +58,8 @@
{% if not rows %}
(you don't have any active shares btw)
{% endif %}
</div>
<a href="#" id="repl">π</a>
<script>
var SR = {{ r|tojson }},

View file

@ -157,6 +157,7 @@
<blockquote id="ad">enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!</blockquote></li>
{% endif %}
<li><a id="af" href="{{ r }}/?ru">show recent uploads</a></li>
<li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
</ul>

View file

@ -38,6 +38,7 @@ var Ls = {
"ac1": "skru på no304",
"ad1": "no304 stopper all bruk av cache. Hvis ikke k304 var nok, prøv denne. Vil mangedoble dataforbruk!",
"ae1": "utgående:",
"af1": "vis nylig opplastede filer",
},
"eng": {
"d2": "shows the state of all active threads",
@ -88,6 +89,7 @@ var Ls = {
"ac1": "开启 k304",
"ad1": "启用 no304 将禁用所有缓存;如果 k304 不够,可以尝试此选项。这将消耗大量的网络流量!", //m
"ae1": "正在下载:", //m
"af1": "显示最近上传的文件", //m
}
};

View file

@ -143,6 +143,9 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
| GET | `?dls` | show active downloads (do this as admin) |
| GET | `?ups` | show recent uploads from your IP |
| GET | `?ups&filter=f` | ...where URL contains `f` |
| GET | `?ru` | show all recent uploads |
| GET | `?ru&filter=f` | ...where URL contains `f` |
| GET | `?ru&j` | ...as json |
| GET | `?mime=foo` | specify return mimetype `foo` |
| GET | `?v` | render markdown file at URL |
| GET | `?v` | open image/video/audio in mediaplayer |

View file

@ -105,6 +105,9 @@ copyparty/web/mde.html,
copyparty/web/mde.js,
copyparty/web/msg.css,
copyparty/web/msg.html,
copyparty/web/rups.css,
copyparty/web/rups.html,
copyparty/web/rups.js,
copyparty/web/shares.css,
copyparty/web/shares.html,
copyparty/web/shares.js,

View file

@ -80,6 +80,7 @@ var tl_cpanel = {
"ac1": "enable no304",
"ad1": "enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!",
"ae1": "active downloads:",
"af1": "show recent uploads",
},
};

View file

@ -115,6 +115,7 @@ var tl_cpanel = {{
"ac1": "enable no304",
"ad1": "enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!",
"ae1": "active downloads:",
"af1": "show recent uploads",
}},
}};