mirror of
https://github.com/9001/copyparty.git
synced 2025-08-17 09:02:15 -06:00
shares: add revival and expiration extension
This commit is contained in:
parent
c4e2b0f95f
commit
ad2371f810
|
@ -779,6 +779,8 @@ specify `--shr /foobar` to enable this feature; a toplevel virtual folder named
|
||||||
|
|
||||||
users can delete their own shares in the controlpanel, and a list of privileged users (`--shr-adm`) are allowed to see and/or delet any share on the server
|
users can delete their own shares in the controlpanel, and a list of privileged users (`--shr-adm`) are allowed to see and/or delet any share on the server
|
||||||
|
|
||||||
|
after a share has expired, it remains visible in the controlpanel for `--shr-rt` minutes (default is 1 day), and the owner can revive it by extending the expiration time there
|
||||||
|
|
||||||
**security note:** using this feature does not mean that you can skip the [accounts and volumes](#accounts-and-volumes) section -- you still need to restrict access to volumes that you do not intend to share with unauthenticated users! it is not sufficient to use rules in the reverseproxy to restrict access to just the `/share` folder.
|
**security note:** using this feature does not mean that you can skip the [accounts and volumes](#accounts-and-volumes) section -- you still need to restrict access to volumes that you do not intend to share with unauthenticated users! it is not sufficient to use rules in the reverseproxy to restrict access to just the `/share` folder.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -975,9 +975,10 @@ def add_fs(ap):
|
||||||
def add_share(ap):
|
def add_share(ap):
|
||||||
db_path = os.path.join(E.cfg, "shares.db")
|
db_path = os.path.join(E.cfg, "shares.db")
|
||||||
ap2 = ap.add_argument_group('share-url options')
|
ap2 = ap.add_argument_group('share-url options')
|
||||||
ap2.add_argument("--shr", metavar="DIR", default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]")
|
ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]")
|
||||||
ap2.add_argument("--shr-db", metavar="FILE", default=db_path, help="database to store shares in")
|
ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in")
|
||||||
ap2.add_argument("--shr-adm", metavar="U,U", default="", help="comma-separated list of users allowed to view/delete any share")
|
ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share")
|
||||||
|
ap2.add_argument("--shr-rt", metavar="MIN", type=int, default=1440, help="shares can be revived by their owner if they expired less than MIN minutes ago; [\033[32m60\033[0m]=hour, [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week")
|
||||||
ap2.add_argument("--shr-v", action="store_true", help="debug")
|
ap2.add_argument("--shr-v", action="store_true", help="debug")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1611,8 +1611,8 @@ class HttpCli(object):
|
||||||
if "delete" in self.uparam:
|
if "delete" in self.uparam:
|
||||||
return self.handle_rm([])
|
return self.handle_rm([])
|
||||||
|
|
||||||
if "unshare" in self.uparam:
|
if "eshare" in self.uparam:
|
||||||
return self.handle_unshare()
|
return self.handle_eshare()
|
||||||
|
|
||||||
if "application/octet-stream" in ctype:
|
if "application/octet-stream" in ctype:
|
||||||
return self.handle_post_binary()
|
return self.handle_post_binary()
|
||||||
|
@ -4304,7 +4304,7 @@ class HttpCli(object):
|
||||||
self.reply(html.encode("utf-8"), status=200)
|
self.reply(html.encode("utf-8"), status=200)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def handle_unshare(self) -> bool:
|
def handle_eshare(self) -> bool:
|
||||||
idx = self.conn.get_u2idx()
|
idx = self.conn.get_u2idx()
|
||||||
if not idx or not hasattr(idx, "p_end"):
|
if not idx or not hasattr(idx, "p_end"):
|
||||||
if not HAVE_SQLITE3:
|
if not HAVE_SQLITE3:
|
||||||
|
@ -4312,7 +4312,7 @@ class HttpCli(object):
|
||||||
raise Pebkac(500, "server busy, cannot create share; please retry in a bit")
|
raise Pebkac(500, "server busy, cannot create share; please retry in a bit")
|
||||||
|
|
||||||
if self.args.shr_v:
|
if self.args.shr_v:
|
||||||
self.log("handle_unshare: " + self.req)
|
self.log("handle_eshare: " + self.req)
|
||||||
|
|
||||||
cur = idx.get_shr()
|
cur = idx.get_shr()
|
||||||
if not cur:
|
if not cur:
|
||||||
|
@ -4320,18 +4320,36 @@ class HttpCli(object):
|
||||||
|
|
||||||
skey = self.vpath.split("/")[-1]
|
skey = self.vpath.split("/")[-1]
|
||||||
|
|
||||||
uns = cur.execute("select un from sh where k = ?", (skey,)).fetchall()
|
rows = cur.execute("select un, t1 from sh where k = ?", (skey,)).fetchall()
|
||||||
un = uns[0][0] if uns and uns[0] else ""
|
un = rows[0][0] if rows and rows[0] else ""
|
||||||
|
|
||||||
if not un:
|
if not un:
|
||||||
raise Pebkac(400, "that sharekey didn't match anything")
|
raise Pebkac(400, "that sharekey didn't match anything")
|
||||||
|
|
||||||
|
expiry = rows[0][1]
|
||||||
|
|
||||||
if un != self.uname and self.uname != self.args.shr_adm:
|
if un != self.uname and self.uname != self.args.shr_adm:
|
||||||
t = "your username (%r) does not match the sharekey's owner (%r) and you're not admin"
|
t = "your username (%r) does not match the sharekey's owner (%r) and you're not admin"
|
||||||
raise Pebkac(400, t % (self.uname, un))
|
raise Pebkac(400, t % (self.uname, un))
|
||||||
|
|
||||||
|
reload = False
|
||||||
|
act = self.uparam["eshare"]
|
||||||
|
if act == "rm":
|
||||||
cur.execute("delete from sh where k = ?", (skey,))
|
cur.execute("delete from sh where k = ?", (skey,))
|
||||||
|
if skey in self.asrv.vfs.nodes[self.args.shr.strip("/")].nodes:
|
||||||
|
reload = True
|
||||||
|
else:
|
||||||
|
now = time.time()
|
||||||
|
if expiry < now:
|
||||||
|
expiry = now
|
||||||
|
reload = True
|
||||||
|
expiry += int(act) * 60
|
||||||
|
cur.execute("update sh set t1 = ? where k = ?", (expiry, skey))
|
||||||
|
|
||||||
cur.connection.commit()
|
cur.connection.commit()
|
||||||
|
if reload:
|
||||||
|
self.conn.hsrv.broker.ask("_reload_blocking", False, False).get()
|
||||||
|
self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
|
||||||
|
|
||||||
self.redirect(self.args.SRS + "?shares")
|
self.redirect(self.args.SRS + "?shares")
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -435,7 +435,7 @@ class Up2k(object):
|
||||||
def _sched_rescan(self) -> None:
|
def _sched_rescan(self) -> None:
|
||||||
volage = {}
|
volage = {}
|
||||||
cooldown = timeout = time.time() + 3.0
|
cooldown = timeout = time.time() + 3.0
|
||||||
while True:
|
while not self.stop:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
timeout = max(timeout, cooldown)
|
timeout = max(timeout, cooldown)
|
||||||
wait = timeout - time.time()
|
wait = timeout - time.time()
|
||||||
|
@ -443,6 +443,9 @@ class Up2k(object):
|
||||||
with self.rescan_cond:
|
with self.rescan_cond:
|
||||||
self.rescan_cond.wait(wait)
|
self.rescan_cond.wait(wait)
|
||||||
|
|
||||||
|
if self.stop:
|
||||||
|
return
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now < cooldown:
|
if now < cooldown:
|
||||||
# self.log("SR: cd - now = {:.2f}".format(cooldown - now), 5)
|
# self.log("SR: cd - now = {:.2f}".format(cooldown - now), 5)
|
||||||
|
@ -466,6 +469,7 @@ class Up2k(object):
|
||||||
if self.args.shr:
|
if self.args.shr:
|
||||||
timeout = min(self._check_shares(), timeout)
|
timeout = min(self._check_shares(), timeout)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
timeout = min(timeout, now + 60)
|
||||||
t = "could not check for expiring shares: %r"
|
t = "could not check for expiring shares: %r"
|
||||||
self.log(t % (ex,), 1)
|
self.log(t % (ex,), 1)
|
||||||
|
|
||||||
|
@ -575,27 +579,53 @@ class Up2k(object):
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
timeout = now + 9001
|
timeout = now + 9001
|
||||||
|
maxage = self.args.shr_rt * 60
|
||||||
|
low = now - maxage
|
||||||
|
|
||||||
|
vn = self.asrv.vfs.nodes.get(self.args.shr.strip("/"))
|
||||||
|
active = vn and vn.nodes
|
||||||
|
|
||||||
db = sqlite3.connect(self.args.shr_db, timeout=2)
|
db = sqlite3.connect(self.args.shr_db, timeout=2)
|
||||||
cur = db.cursor()
|
cur = db.cursor()
|
||||||
|
|
||||||
q = "select k from sh where t1 and t1 <= ?"
|
q = "select k from sh where t1 and t1 <= ?"
|
||||||
rm = [x[0] for x in cur.execute(q, (now,))]
|
rm = [x[0] for x in cur.execute(q, (now,))] if active else []
|
||||||
|
if rm:
|
||||||
|
assert vn and vn.nodes # type: ignore
|
||||||
|
# self.log("chk_shr: %d" % (len(rm),))
|
||||||
|
zss = set(rm)
|
||||||
|
rm = [zs for zs in vn.nodes if zs in zss]
|
||||||
|
reload = bool(rm)
|
||||||
|
if reload:
|
||||||
|
self.log("disabling expired shares %s" % (rm,))
|
||||||
|
|
||||||
|
rm = [x[0] for x in cur.execute(q, (low,))]
|
||||||
if rm:
|
if rm:
|
||||||
self.log("forgetting expired shares %s" % (rm,))
|
self.log("forgetting expired shares %s" % (rm,))
|
||||||
cur.executemany("delete from sh where k=?", [(x,) for x in rm])
|
cur.executemany("delete from sh where k=?", [(x,) for x in rm])
|
||||||
cur.executemany("delete from sf where k=?", [(x,) for x in rm])
|
cur.executemany("delete from sf where k=?", [(x,) for x in rm])
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
if reload:
|
||||||
Daemon(self.hub._reload_blocking, "sharedrop", (False, False))
|
Daemon(self.hub._reload_blocking, "sharedrop", (False, False))
|
||||||
|
|
||||||
q = "select min(t1) from sh where t1 > 1"
|
q = "select min(t1) from sh where t1 > ?"
|
||||||
(earliest,) = cur.execute(q).fetchone()
|
(earliest,) = cur.execute(q, (1,)).fetchone()
|
||||||
if earliest:
|
if earliest:
|
||||||
timeout = earliest - now
|
# deadline for revoking regular access
|
||||||
|
timeout = min(timeout, earliest + maxage)
|
||||||
|
|
||||||
|
(earliest,) = cur.execute(q, (now - 2,)).fetchone()
|
||||||
|
if earliest:
|
||||||
|
# deadline for revival; drop entirely
|
||||||
|
timeout = min(timeout, earliest)
|
||||||
|
|
||||||
cur.close()
|
cur.close()
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
if self.args.shr_v:
|
||||||
|
self.log("next shr_chk = %d (%d)" % (timeout, timeout - time.time()))
|
||||||
|
|
||||||
return timeout
|
return timeout
|
||||||
|
|
||||||
def _check_xiu(self) -> float:
|
def _check_xiu(self) -> float:
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
<th>expires</th>
|
<th>expires</th>
|
||||||
<th>min</th>
|
<th>min</th>
|
||||||
<th>hrs</th>
|
<th>hrs</th>
|
||||||
|
<th>add time</th>
|
||||||
</tr></thead><tbody>
|
</tr></thead><tbody>
|
||||||
{% for k, pw, vp, pr, st, un, t0, t1 in rows %}
|
{% for k, pw, vp, pr, st, un, t0, t1 in rows %}
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -45,8 +46,9 @@
|
||||||
<td>{{ un|e }}</td>
|
<td>{{ un|e }}</td>
|
||||||
<td>{{ t0 }}</td>
|
<td>{{ t0 }}</td>
|
||||||
<td>{{ t1 }}</td>
|
<td>{{ t1 }}</td>
|
||||||
<td>{{ ((t1 - now) / 60) | round(1) if t1 else "inf" }}</td>
|
<td>{{ "inf" if not t1 else "dead" if t1 < now else ((t1 - now) / 60) | round(1) }}</td>
|
||||||
<td>{{ ((t1 - now) / 3600) | round(1) if t1 else "inf" }}</td>
|
<td>{{ "inf" if not t1 else "dead" if t1 < now else ((t1 - now) / 3600) | round(1) }}</td>
|
||||||
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody></table>
|
</tbody></table>
|
||||||
|
|
|
@ -3,7 +3,17 @@ for (var a = 0; a < t.length; a++)
|
||||||
t[a].onclick = rm;
|
t[a].onclick = rm;
|
||||||
|
|
||||||
function rm() {
|
function rm() {
|
||||||
var u = SR + shr + uricom_enc(this.getAttribute('k')) + '?unshare',
|
var u = SR + shr + uricom_enc(this.getAttribute('k')) + '?eshare=rm',
|
||||||
|
xhr = new XHR();
|
||||||
|
|
||||||
|
xhr.open('POST', u, true);
|
||||||
|
xhr.onload = xhr.onerror = cb;
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bump() {
|
||||||
|
var k = this.closest('tr').getElementsByTagName('a')[0].getAttribute('k'),
|
||||||
|
u = SR + shr + uricom_enc(k) + '?eshare=' + this.value,
|
||||||
xhr = new XHR();
|
xhr = new XHR();
|
||||||
|
|
||||||
xhr.open('POST', u, true);
|
xhr.open('POST', u, true);
|
||||||
|
@ -34,4 +44,13 @@ function cb() {
|
||||||
tr[a].cells[b].innerHTML =
|
tr[a].cells[b].innerHTML =
|
||||||
v ? unix2iso(v).replace(' ', ', ') : 'never';
|
v ? unix2iso(v).replace(' ', ', ') : 'never';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (var a = 0; a < tr.length; a++)
|
||||||
|
tr[a].cells[11].innerHTML =
|
||||||
|
'<button value="1">1min</button> ' +
|
||||||
|
'<button value="60">1h</button>';
|
||||||
|
|
||||||
|
var btns = QSA('td button'), aa = btns.length;
|
||||||
|
for (var a = 0; a < aa; a++)
|
||||||
|
btns[a].onclick = bump;
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -176,7 +176,8 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
|
||||||
| mPOST | `?media` | `f=FILE` | ...and return medialink (not hotlink) |
|
| mPOST | `?media` | `f=FILE` | ...and return medialink (not hotlink) |
|
||||||
| mPOST | | `act=mkdir`, `name=foo` | create directory `foo` at URL |
|
| mPOST | | `act=mkdir`, `name=foo` | create directory `foo` at URL |
|
||||||
| POST | `?delete` | | delete URL recursively |
|
| POST | `?delete` | | delete URL recursively |
|
||||||
| POST | `?unshare` | | stop sharing a file/folder |
|
| POST | `?eshare=rm` | | stop sharing a file/folder |
|
||||||
|
| POST | `?eshare=3` | | set expiration to 3 minutes |
|
||||||
| jPOST | `?share` | (complicated) | create temp URL for file/folder |
|
| jPOST | `?share` | (complicated) | create temp URL for file/folder |
|
||||||
| jPOST | `?delete` | `["/foo","/bar"]` | delete `/foo` and `/bar` recursively |
|
| jPOST | `?delete` | `["/foo","/bar"]` | delete `/foo` and `/bar` recursively |
|
||||||
| uPOST | | `msg=foo` | send message `foo` into server log |
|
| uPOST | | `msg=foo` | send message `foo` into server log |
|
||||||
|
|
Loading…
Reference in a new issue