mirror of
https://github.com/9001/copyparty.git
synced 2025-08-17 00:52:16 -06:00
add file/folder sharing; closes #84
This commit is contained in:
parent
7d8d94388b
commit
7c2beba555
28
README.md
28
README.md
|
@ -42,6 +42,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
|||
* [self-destruct](#self-destruct) - uploads can be given a lifetime
|
||||
* [race the beam](#race-the-beam) - download files while they're still uploading ([demo video](http://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm))
|
||||
* [file manager](#file-manager) - cut/paste, rename, and delete files/folders (if you have permission)
|
||||
* [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
|
||||
* [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)
|
||||
|
@ -745,6 +746,33 @@ file selection: click somewhere on the line (not the link itsef), then:
|
|||
you can move files across browser tabs (cut in one tab, paste in another)
|
||||
|
||||
|
||||
## shares
|
||||
|
||||
share a file or folder by creating a temporary link
|
||||
|
||||
when enabled in the server settings (`--shr`), click the bottom-right `share` button to share the folder you're currently in, or select a file first to share only that file
|
||||
|
||||
this feature was made with [identity providers](#identity-providers) in mind -- configure your reverseproxy to skip the IdP's access-control for a given URL prefix and use that to safely share specific files/folders sans the usual auth checks
|
||||
|
||||
when creating a share, the creator can choose any of the following options:
|
||||
|
||||
* password-protection
|
||||
* expire after a certain time
|
||||
* allow visitors to upload (if the user who creates the share has write-access)
|
||||
|
||||
semi-intentional limitations:
|
||||
|
||||
* cleanup of expired shares only works when global option `e2d` is set, and/or at least one volume on the server has volflag `e2d`
|
||||
* only folders from the same volume are shared; if you are sharing a folder which contains other volumes, then the contents of those volumes will not be available
|
||||
* no option to "delete after first access" because tricky
|
||||
* when linking something to discord (for example) it'll get accessed by their scraper and that would count as a hit
|
||||
* browsers wouldn't be able to resume a broken download unless the requester's IP gets allowlisted for X minutes (ref. tricky)
|
||||
|
||||
the links are created inside a specific toplevel folder which must be specified with server-config `--shr`, for example `--shr /share/` (this also enables the feature)
|
||||
|
||||
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
|
||||
|
||||
|
||||
## batch rename
|
||||
|
||||
select some files and press `F2` to bring up the rename UI
|
||||
|
|
|
@ -972,6 +972,15 @@ def add_fs(ap):
|
|||
ap2.add_argument("--mtab-age", metavar="SEC", type=int, default=60, help="rebuild mountpoint cache every \033[33mSEC\033[0m to keep track of sparse-files support; keep low on servers with removable media")
|
||||
|
||||
|
||||
def add_share(ap):
|
||||
db_path = os.path.join(E.cfg, "shares.db")
|
||||
ap2 = ap.add_argument_group('share-url options')
|
||||
ap2.add_argument("--shr", metavar="URL", default="", help="base url for shared files, for example [\033[32m/share\033[0m] (must be a toplevel subfolder)")
|
||||
ap2.add_argument("--shr-db", metavar="PATH", 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-v", action="store_true", help="debug")
|
||||
|
||||
|
||||
def add_upload(ap):
|
||||
ap2 = ap.add_argument_group('upload options')
|
||||
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless \033[33m-ed\033[0m")
|
||||
|
@ -1489,6 +1498,7 @@ def run_argparse(
|
|||
add_zc_mdns(ap)
|
||||
add_zc_ssdp(ap)
|
||||
add_fs(ap)
|
||||
add_share(ap)
|
||||
add_upload(ap)
|
||||
add_db_general(ap, hcores)
|
||||
add_db_metadata(ap)
|
||||
|
|
|
@ -38,6 +38,7 @@ from .util import (
|
|||
uncyg,
|
||||
undot,
|
||||
unhumanize,
|
||||
vjoin,
|
||||
vsplit,
|
||||
)
|
||||
|
||||
|
@ -342,6 +343,7 @@ class VFS(object):
|
|||
self.histtab: dict[str, str] = {} # all realpath->histpath
|
||||
self.dbv: Optional[VFS] = None # closest full/non-jump parent
|
||||
self.lim: Optional[Lim] = None # upload limits; only set for dbv
|
||||
self.shr_src: Optional[tuple[VFS, str]] = None # source vfs+rem of a share
|
||||
self.aread: dict[str, list[str]] = {}
|
||||
self.awrite: dict[str, list[str]] = {}
|
||||
self.amove: dict[str, list[str]] = {}
|
||||
|
@ -366,6 +368,8 @@ class VFS(object):
|
|||
self.all_aps = []
|
||||
self.all_vps = []
|
||||
|
||||
self.get_dbv = self._get_dbv
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "VFS(%s)" % (
|
||||
", ".join(
|
||||
|
@ -527,7 +531,15 @@ class VFS(object):
|
|||
|
||||
return vn, rem
|
||||
|
||||
def get_dbv(self, vrem: str) -> tuple["VFS", str]:
|
||||
def _get_share_src(self, vrem: str) -> tuple["VFS", str]:
|
||||
src = self.shr_src
|
||||
if not src:
|
||||
return self._get_dbv(vrem)
|
||||
|
||||
shv, srem = src
|
||||
return shv, vjoin(srem, vrem)
|
||||
|
||||
def _get_dbv(self, vrem: str) -> tuple["VFS", str]:
|
||||
dbv = self.dbv
|
||||
if not dbv:
|
||||
return self, vrem
|
||||
|
@ -1354,7 +1366,7 @@ class AuthSrv(object):
|
|||
flags[name] = vals
|
||||
self._e("volflag [{}] += {} ({})".format(name, vals, desc))
|
||||
|
||||
def reload(self) -> None:
|
||||
def reload(self, verbosity: int = 9) -> None:
|
||||
"""
|
||||
construct a flat list of mountpoints and usernames
|
||||
first from the commandline arguments
|
||||
|
@ -1362,9 +1374,9 @@ class AuthSrv(object):
|
|||
before finally building the VFS
|
||||
"""
|
||||
with self.mutex:
|
||||
self._reload()
|
||||
self._reload(verbosity)
|
||||
|
||||
def _reload(self) -> None:
|
||||
def _reload(self, verbosity: int = 9) -> None:
|
||||
acct: dict[str, str] = {} # username:password
|
||||
grps: dict[str, list[str]] = {} # groupname:usernames
|
||||
daxs: dict[str, AXS] = {}
|
||||
|
@ -1459,9 +1471,8 @@ class AuthSrv(object):
|
|||
vfs = VFS(self.log_func, absreal("."), "", axs, {})
|
||||
elif "" not in mount:
|
||||
# there's volumes but no root; make root inaccessible
|
||||
vfs = VFS(self.log_func, "", "", AXS(), {})
|
||||
vfs.flags["tcolor"] = self.args.tcolor
|
||||
vfs.flags["d2d"] = True
|
||||
zsd = {"d2d": True, "tcolor": self.args.tcolor}
|
||||
vfs = VFS(self.log_func, "", "", AXS(), zsd)
|
||||
|
||||
maxdepth = 0
|
||||
for dst in sorted(mount.keys(), key=lambda x: (x.count("/"), len(x))):
|
||||
|
@ -1490,6 +1501,52 @@ class AuthSrv(object):
|
|||
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
|
||||
vol.root = vfs
|
||||
|
||||
enshare = self.args.shr
|
||||
shr = enshare[1:-1]
|
||||
shrs = enshare[1:]
|
||||
if enshare:
|
||||
import sqlite3
|
||||
|
||||
shv = VFS(self.log_func, "", shr, AXS(), {"d2d": True})
|
||||
par = vfs.all_vols[""]
|
||||
|
||||
db_path = self.args.shr_db
|
||||
db = sqlite3.connect(db_path)
|
||||
cur = db.cursor()
|
||||
now = time.time()
|
||||
for row in cur.execute("select * from sh"):
|
||||
s_k, s_pw, s_vp, s_pr, s_st, s_un, s_t0, s_t1 = row
|
||||
if s_t1 and s_t1 < now:
|
||||
continue
|
||||
|
||||
if self.args.shr_v:
|
||||
t = "loading %s share [%s] by [%s] => [%s]"
|
||||
self.log(t % (s_pr, s_k, s_un, s_vp))
|
||||
|
||||
if s_pw:
|
||||
sun = "s_%s" % (s_k,)
|
||||
acct[sun] = s_pw
|
||||
else:
|
||||
sun = "*"
|
||||
|
||||
s_axs = AXS(
|
||||
[sun] if "r" in s_pr else [],
|
||||
[sun] if "w" in s_pr else [],
|
||||
[sun] if "m" in s_pr else [],
|
||||
[sun] if "d" in s_pr else [],
|
||||
)
|
||||
|
||||
# don't know the abspath yet + wanna ensure the user
|
||||
# still has the privs they granted, so nullmap it
|
||||
shv.nodes[s_k] = VFS(
|
||||
self.log_func, "", "%s/%s" % (shr, s_k), s_axs, par.flags.copy()
|
||||
)
|
||||
|
||||
vfs.nodes[shr] = vfs.all_vols[shr] = shv
|
||||
for vol in shv.nodes.values():
|
||||
vfs.all_vols[vol.vpath] = vol
|
||||
vol.get_dbv = vol._get_share_src
|
||||
|
||||
zss = set(acct)
|
||||
zss.update(self.idp_accs)
|
||||
zss.discard("*")
|
||||
|
@ -1508,7 +1565,7 @@ class AuthSrv(object):
|
|||
for usr in unames:
|
||||
for vp, vol in vfs.all_vols.items():
|
||||
zx = getattr(vol.axs, axs_key)
|
||||
if usr in zx:
|
||||
if usr in zx and (not enshare or not vp.startswith(shrs)):
|
||||
umap[usr].append(vp)
|
||||
umap[usr].sort()
|
||||
setattr(vfs, "a" + perm, umap)
|
||||
|
@ -1558,6 +1615,8 @@ class AuthSrv(object):
|
|||
|
||||
for usr in acct:
|
||||
if usr not in associated_users:
|
||||
if enshare and usr.startswith("s_"):
|
||||
continue
|
||||
if len(vfs.all_vols) > 1:
|
||||
# user probably familiar enough that the verbose message is not necessary
|
||||
t = "account [%s] is not mentioned in any volume definitions; see --help-accounts"
|
||||
|
@ -1993,7 +2052,7 @@ class AuthSrv(object):
|
|||
have_e2t = False
|
||||
t = "volumes and permissions:\n"
|
||||
for zv in vfs.all_vols.values():
|
||||
if not self.warn_anonwrite:
|
||||
if not self.warn_anonwrite or verbosity < 5:
|
||||
break
|
||||
|
||||
t += '\n\033[36m"/{}" \033[33m{}\033[0m'.format(zv.vpath, zv.realpath)
|
||||
|
@ -2022,7 +2081,7 @@ class AuthSrv(object):
|
|||
|
||||
t += "\n"
|
||||
|
||||
if self.warn_anonwrite:
|
||||
if self.warn_anonwrite and verbosity > 4:
|
||||
if not self.args.no_voldump:
|
||||
self.log(t)
|
||||
|
||||
|
@ -2046,7 +2105,7 @@ class AuthSrv(object):
|
|||
|
||||
try:
|
||||
zv, _ = vfs.get("", "*", False, True, err=999)
|
||||
if self.warn_anonwrite and os.getcwd() == zv.realpath:
|
||||
if self.warn_anonwrite and verbosity > 4 and os.getcwd() == zv.realpath:
|
||||
t = "anyone can write to the current directory: {}\n"
|
||||
self.log(t.format(zv.realpath), c=1)
|
||||
|
||||
|
@ -2094,6 +2153,51 @@ class AuthSrv(object):
|
|||
MIMES[ext] = mime
|
||||
EXTS.update({v: k for k, v in MIMES.items()})
|
||||
|
||||
if enshare:
|
||||
# hide shares from controlpanel
|
||||
vfs.all_vols = {
|
||||
x: y
|
||||
for x, y in vfs.all_vols.items()
|
||||
if x != shr and not x.startswith(shrs)
|
||||
}
|
||||
|
||||
assert cur # type: ignore
|
||||
assert shv # type: ignore
|
||||
for row in cur.execute("select * from sh"):
|
||||
s_k, s_pw, s_vp, s_pr, s_st, s_un, s_t0, s_t1 = row
|
||||
shn = shv.nodes.get(s_k, None)
|
||||
if not shn:
|
||||
continue
|
||||
|
||||
try:
|
||||
s_vfs, s_rem = vfs.get(
|
||||
s_vp, s_un, "r" in s_pr, "w" in s_pr, "m" in s_pr, "d" in s_pr
|
||||
)
|
||||
except Exception as ex:
|
||||
t = "removing share [%s] by [%s] to [%s] due to %r"
|
||||
self.log(t % (s_k, s_un, s_vp, ex), 3)
|
||||
shv.nodes.pop(s_k)
|
||||
continue
|
||||
|
||||
shn.shr_src = (s_vfs, s_rem)
|
||||
shn.realpath = s_vfs.canonical(s_rem)
|
||||
|
||||
if self.args.shr_v:
|
||||
t = "mapped %s share [%s] by [%s] => [%s] => [%s]"
|
||||
self.log(t % (s_pr, s_k, s_un, s_vp, shn.realpath))
|
||||
|
||||
# transplant shadowing into shares
|
||||
for vn in shv.nodes.values():
|
||||
svn, srem = vn.shr_src # type: ignore
|
||||
if srem:
|
||||
continue # free branch, safe
|
||||
ap = svn.canonical(srem)
|
||||
if bos.path.isfile(ap):
|
||||
continue # also fine
|
||||
for zs in svn.nodes.keys():
|
||||
# hide subvolume
|
||||
vn.nodes[zs] = VFS(self.log_func, "", "", AXS(), {})
|
||||
|
||||
def chpw(self, broker: Optional["BrokerCli"], uname, pw) -> tuple[bool, str]:
|
||||
if not self.args.chpw:
|
||||
return False, "feature disabled in server config"
|
||||
|
|
|
@ -45,6 +45,7 @@ from .util import unquote # type: ignore
|
|||
from .util import (
|
||||
APPLESAN_RE,
|
||||
BITNESS,
|
||||
HAVE_SQLITE3,
|
||||
HTTPCODE,
|
||||
META_NOBOTS,
|
||||
UTC,
|
||||
|
@ -454,7 +455,7 @@ class HttpCli(object):
|
|||
t = "incorrect --rp-loc or webserver config; expected vpath starting with [{}] but got [{}]"
|
||||
self.log(t.format(self.args.R, vpath), 1)
|
||||
|
||||
self.ouparam = {k: zs for k, zs in uparam.items()}
|
||||
self.ouparam = uparam.copy()
|
||||
|
||||
if self.args.rsp_slp:
|
||||
time.sleep(self.args.rsp_slp)
|
||||
|
@ -971,7 +972,7 @@ class HttpCli(object):
|
|||
vp = self.args.SRS + vpath
|
||||
html = self.j2s(
|
||||
"msg",
|
||||
h2='<a href="%s">%s %s</a>' % (
|
||||
h2='<a href="{}">{} {}</a>'.format(
|
||||
quotep(vp) + suf, flavor, html_escape(vp, crlf=True) + suf
|
||||
),
|
||||
pre=msg,
|
||||
|
@ -1141,7 +1142,7 @@ class HttpCli(object):
|
|||
if "move" in self.uparam:
|
||||
return self.handle_mv()
|
||||
|
||||
if not self.vpath:
|
||||
if not self.vpath and self.ouparam:
|
||||
if "reload" in self.uparam:
|
||||
return self.handle_reload()
|
||||
|
||||
|
@ -1163,23 +1164,12 @@ class HttpCli(object):
|
|||
if "hc" in self.uparam:
|
||||
return self.tx_svcs()
|
||||
|
||||
if "shares" in self.uparam:
|
||||
return self.tx_shares()
|
||||
|
||||
if "h" in self.uparam:
|
||||
return self.tx_mounts()
|
||||
|
||||
# conditional redirect to single volumes
|
||||
if not self.vpath and not self.ouparam:
|
||||
nread = len(self.rvol)
|
||||
nwrite = len(self.wvol)
|
||||
if nread + nwrite == 1 or (self.rvol == self.wvol and nread == 1):
|
||||
if nread == 1:
|
||||
vpath = self.rvol[0]
|
||||
else:
|
||||
vpath = self.wvol[0]
|
||||
|
||||
if self.vpath != vpath:
|
||||
self.redirect(vpath, flavor="redirecting to", use302=True)
|
||||
return True
|
||||
|
||||
return self.tx_browser()
|
||||
|
||||
def handle_propfind(self) -> bool:
|
||||
|
@ -1618,6 +1608,9 @@ class HttpCli(object):
|
|||
if "delete" in self.uparam:
|
||||
return self.handle_rm([])
|
||||
|
||||
if "unshare" in self.uparam:
|
||||
return self.handle_unshare()
|
||||
|
||||
if "application/octet-stream" in ctype:
|
||||
return self.handle_post_binary()
|
||||
|
||||
|
@ -2150,6 +2143,9 @@ class HttpCli(object):
|
|||
if "srch" in self.uparam or "srch" in body:
|
||||
return self.handle_search(body)
|
||||
|
||||
if "share" in self.uparam:
|
||||
return self.handle_share(body)
|
||||
|
||||
if "delete" in self.uparam:
|
||||
return self.handle_rm(body)
|
||||
|
||||
|
@ -2206,7 +2202,9 @@ class HttpCli(object):
|
|||
def handle_search(self, body: dict[str, Any]) -> bool:
|
||||
idx = self.conn.get_u2idx()
|
||||
if not idx or not hasattr(idx, "p_end"):
|
||||
raise Pebkac(500, "server busy, or sqlite3 not available; cannot search")
|
||||
if not HAVE_SQLITE3:
|
||||
raise Pebkac(500, "sqlite3 not found on server; search is disabled")
|
||||
raise Pebkac(500, "server busy, cannot search; please retry in a bit")
|
||||
|
||||
vols: list[VFS] = []
|
||||
seen: dict[VFS, bool] = {}
|
||||
|
@ -4179,7 +4177,9 @@ class HttpCli(object):
|
|||
def tx_ups(self) -> bool:
|
||||
idx = self.conn.get_u2idx()
|
||||
if not idx or not hasattr(idx, "p_end"):
|
||||
raise Pebkac(500, "sqlite3 is not available on the server; cannot unpost")
|
||||
if not HAVE_SQLITE3:
|
||||
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 [{}]".format(filt)
|
||||
|
@ -4268,6 +4268,137 @@ class HttpCli(object):
|
|||
self.reply(jtxt.encode("utf-8", "replace"), mime="application/json")
|
||||
return True
|
||||
|
||||
def tx_shares(self) -> bool:
|
||||
if self.uname == "*":
|
||||
self.loud_reply("you're not logged in")
|
||||
return True
|
||||
|
||||
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; sharing is disabled")
|
||||
raise Pebkac(500, "server busy, cannot list shares; please retry in a bit")
|
||||
|
||||
cur = idx.get_shr()
|
||||
if not cur:
|
||||
raise Pebkac(400, "huh, sharing must be disabled in the server config...")
|
||||
|
||||
rows = cur.execute("select * from sh").fetchall()
|
||||
rows = [list(x) for x in rows]
|
||||
|
||||
if self.uname != self.args.shr_adm:
|
||||
rows = [x for x in rows if x[5] == self.uname]
|
||||
|
||||
for x in rows:
|
||||
x[1] = "yes" if x[1] else ""
|
||||
|
||||
html = self.j2s(
|
||||
"shares", this=self, shr=self.args.shr, rows=rows, now=int(time.time())
|
||||
)
|
||||
self.reply(html.encode("utf-8"), status=200)
|
||||
return True
|
||||
|
||||
def handle_unshare(self) -> bool:
|
||||
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; sharing is disabled")
|
||||
raise Pebkac(500, "server busy, cannot create share; please retry in a bit")
|
||||
|
||||
if self.args.shr_v:
|
||||
self.log("handle_unshare: " + self.req)
|
||||
|
||||
cur = idx.get_shr()
|
||||
if not cur:
|
||||
raise Pebkac(400, "huh, sharing must be disabled in the server config...")
|
||||
|
||||
skey = self.vpath.split("/")[-1]
|
||||
|
||||
uns = cur.execute("select un from sh where k = ?", (skey,)).fetchall()
|
||||
un = uns[0][0] if uns and uns[0] else ""
|
||||
|
||||
if not un:
|
||||
raise Pebkac(400, "that sharekey didn't match anything")
|
||||
|
||||
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"
|
||||
raise Pebkac(400, t % (self.uname, un))
|
||||
|
||||
cur.execute("delete from sh where k = ?", (skey,))
|
||||
cur.connection.commit()
|
||||
|
||||
self.redirect(self.args.SRS + "?shares")
|
||||
return True
|
||||
|
||||
def handle_share(self, req: dict[str, str]) -> bool:
|
||||
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; sharing is disabled")
|
||||
raise Pebkac(500, "server busy, cannot create share; please retry in a bit")
|
||||
|
||||
if self.args.shr_v:
|
||||
self.log("handle_share: " + json.dumps(req, indent=4))
|
||||
|
||||
skey = req["k"]
|
||||
vp = req["vp"].strip("/")
|
||||
if self.is_vproxied and (vp == self.args.R or vp.startswith(self.args.RS)):
|
||||
vp = vp[len(self.args.RS) :]
|
||||
|
||||
m = re.search(r"([^0-9a-zA-Z_\.-]|\.\.|^\.)", skey)
|
||||
if m:
|
||||
raise Pebkac(400, "sharekey has illegal character [%s]" % (m[1],))
|
||||
|
||||
if vp.startswith(self.args.shr[1:]):
|
||||
raise Pebkac(400, "yo dawg...")
|
||||
|
||||
cur = idx.get_shr()
|
||||
if not cur:
|
||||
raise Pebkac(400, "huh, sharing must be disabled in the server config...")
|
||||
|
||||
q = "select * from sh where k = ?"
|
||||
qr = cur.execute(q, (skey,)).fetchall()
|
||||
if qr and qr[0]:
|
||||
self.log("sharekey taken by %r" % (qr,))
|
||||
raise Pebkac(400, "sharekey [%s] is already in use" % (skey,))
|
||||
|
||||
# ensure user has requested perms
|
||||
s_rd = "read" in req["perms"]
|
||||
s_wr = "write" in req["perms"]
|
||||
s_mv = "move" in req["perms"]
|
||||
s_del = "delete" in req["perms"]
|
||||
try:
|
||||
vfs, rem = self.asrv.vfs.get(vp, self.uname, s_rd, s_wr, s_mv, s_del)
|
||||
except:
|
||||
raise Pebkac(400, "you dont have all the perms you tried to grant")
|
||||
|
||||
ap = vfs.canonical(rem)
|
||||
st = bos.stat(ap)
|
||||
ist = 2 if stat.S_ISDIR(st.st_mode) else 1
|
||||
|
||||
pw = req.get("pw") or ""
|
||||
now = int(time.time())
|
||||
sexp = req["exp"]
|
||||
exp = now + int(sexp) * 60 if sexp else 0
|
||||
pr = "".join(zc for zc, zb in zip("rwmd", (s_rd, s_wr, s_mv, s_del)) if zb)
|
||||
|
||||
q = "insert into sh values (?,?,?,?,?,?,?,?)"
|
||||
cur.execute(q, (skey, pw, vp, pr, ist, self.uname, now, exp))
|
||||
cur.connection.commit()
|
||||
|
||||
self.conn.hsrv.broker.ask("_reload_blocking", False, False).get()
|
||||
self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
|
||||
|
||||
surl = "%s://%s%s%s%s" % (
|
||||
"https" if self.is_https else "http",
|
||||
self.host,
|
||||
self.args.SR,
|
||||
self.args.shr,
|
||||
skey,
|
||||
)
|
||||
self.loud_reply(surl, status=201)
|
||||
return True
|
||||
|
||||
def handle_rm(self, req: list[str]) -> bool:
|
||||
if not req and not self.can_delete:
|
||||
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||
|
@ -4666,6 +4797,7 @@ class HttpCli(object):
|
|||
"have_mv": (not self.args.no_mv),
|
||||
"have_del": (not self.args.no_del),
|
||||
"have_zip": (not self.args.no_zip),
|
||||
"have_shr": self.args.shr,
|
||||
"have_unpost": int(self.args.unpost),
|
||||
"sb_md": "" if "no_sb_md" in vf else (vf.get("md_sbf") or "y"),
|
||||
"dgrid": "grid" in vf,
|
||||
|
|
|
@ -154,7 +154,17 @@ class HttpSrv(object):
|
|||
|
||||
env = jinja2.Environment()
|
||||
env.loader = jinja2.FileSystemLoader(os.path.join(self.E.mod, "web"))
|
||||
jn = ["splash", "svcs", "browser", "browser2", "msg", "md", "mde", "cf"]
|
||||
jn = [
|
||||
"splash",
|
||||
"shares",
|
||||
"svcs",
|
||||
"browser",
|
||||
"browser2",
|
||||
"msg",
|
||||
"md",
|
||||
"mde",
|
||||
"cf",
|
||||
]
|
||||
self.j2 = {x: env.get_template(x + ".html") for x in jn}
|
||||
zs = os.path.join(self.E.mod, "web", "deps", "prism.js.gz")
|
||||
self.prism = os.path.exists(zs)
|
||||
|
|
|
@ -219,6 +219,9 @@ class SvcHub(object):
|
|||
noch.update([x for x in zsl if x])
|
||||
args.chpw_no = noch
|
||||
|
||||
if args.shr:
|
||||
self.setup_share_db()
|
||||
|
||||
bri = "zy"[args.theme % 2 :][:1]
|
||||
ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
|
||||
args.theme = "{0}{1} {0} {1}".format(ch, bri)
|
||||
|
@ -364,6 +367,61 @@ class SvcHub(object):
|
|||
|
||||
self.broker = Broker(self)
|
||||
|
||||
def setup_share_db(self) -> None:
|
||||
al = self.args
|
||||
if not HAVE_SQLITE3:
|
||||
self.log("root", "sqlite3 not available; disabling --shr", 1)
|
||||
al.shr = ""
|
||||
return
|
||||
|
||||
import sqlite3
|
||||
|
||||
al.shr = "/%s/" % (al.shr.strip("/"))
|
||||
|
||||
create = True
|
||||
db_path = self.args.shr_db
|
||||
self.log("root", "initializing shares-db %s" % (db_path,))
|
||||
for n in range(2):
|
||||
try:
|
||||
db = sqlite3.connect(db_path)
|
||||
cur = db.cursor()
|
||||
try:
|
||||
cur.execute("select count(*) from sh").fetchone()
|
||||
create = False
|
||||
break
|
||||
except:
|
||||
pass
|
||||
except Exception as ex:
|
||||
if n:
|
||||
raise
|
||||
t = "shares-db corrupt; deleting and recreating: %r"
|
||||
self.log("root", t % (ex,), 3)
|
||||
try:
|
||||
cur.close() # type: ignore
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
db.close() # type: ignore
|
||||
except:
|
||||
pass
|
||||
os.unlink(db_path)
|
||||
|
||||
assert db # type: ignore
|
||||
assert cur # type: ignore
|
||||
if create:
|
||||
for cmd in [
|
||||
# sharekey, password, src, perms, type, owner, created, expires
|
||||
r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)",
|
||||
r"create table kv (k text, v int)",
|
||||
r"insert into kv values ('sver', {})".format(1),
|
||||
]:
|
||||
cur.execute(cmd)
|
||||
db.commit()
|
||||
self.log("root", "created new shares-db")
|
||||
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
def start_ftpd(self) -> None:
|
||||
time.sleep(30)
|
||||
|
||||
|
@ -832,7 +890,7 @@ class SvcHub(object):
|
|||
return
|
||||
self.reloading = 2
|
||||
self.log("root", "reloading config")
|
||||
self.asrv.reload()
|
||||
self.asrv.reload(9 if up2k else 4)
|
||||
if up2k:
|
||||
self.up2k.reload(rescan_all_vols)
|
||||
else:
|
||||
|
|
|
@ -59,6 +59,8 @@ class U2idx(object):
|
|||
self.mem_cur = sqlite3.connect(":memory:", check_same_thread=False).cursor()
|
||||
self.mem_cur.execute(r"create table a (b text)")
|
||||
|
||||
self.sh_cur: Optional["sqlite3.Cursor"] = None
|
||||
|
||||
self.p_end = 0.0
|
||||
self.p_dur = 0.0
|
||||
|
||||
|
@ -95,17 +97,31 @@ class U2idx(object):
|
|||
except:
|
||||
raise Pebkac(500, min_ex())
|
||||
|
||||
def get_cur(self, vn: VFS) -> Optional["sqlite3.Cursor"]:
|
||||
if not HAVE_SQLITE3:
|
||||
def get_shr(self) -> Optional["sqlite3.Cursor"]:
|
||||
if self.sh_cur:
|
||||
return self.sh_cur
|
||||
|
||||
if not HAVE_SQLITE3 or not self.args.shr:
|
||||
return None
|
||||
|
||||
assert sqlite3 # type: ignore
|
||||
|
||||
db = sqlite3.connect(self.args.shr_db, timeout=2, check_same_thread=False)
|
||||
cur = db.cursor()
|
||||
cur.execute('pragma table_info("sh")').fetchall()
|
||||
self.sh_cur = cur
|
||||
return cur
|
||||
|
||||
def get_cur(self, vn: VFS) -> Optional["sqlite3.Cursor"]:
|
||||
cur = self.cur.get(vn.realpath)
|
||||
if cur:
|
||||
return cur
|
||||
|
||||
if "e2d" not in vn.flags:
|
||||
if not HAVE_SQLITE3 or "e2d" not in vn.flags:
|
||||
return None
|
||||
|
||||
assert sqlite3 # type: ignore
|
||||
|
||||
ptop = vn.realpath
|
||||
histpath = self.asrv.vfs.histtab.get(ptop)
|
||||
if not histpath:
|
||||
|
|
|
@ -454,11 +454,16 @@ class Up2k(object):
|
|||
cooldown = now + 3
|
||||
# self.log("SR", 5)
|
||||
|
||||
if self.args.no_lifetime:
|
||||
if self.args.no_lifetime and not self.args.shr:
|
||||
timeout = now + 9001
|
||||
else:
|
||||
# important; not deferred by db_act
|
||||
timeout = self._check_lifetimes()
|
||||
try:
|
||||
timeout = min(self._check_shares(), timeout)
|
||||
except Exception as ex:
|
||||
t = "could not check for expiring shares: %r"
|
||||
self.log(t % (ex,), 1)
|
||||
|
||||
try:
|
||||
timeout = min(timeout, now + self._check_xiu())
|
||||
|
@ -561,6 +566,34 @@ class Up2k(object):
|
|||
|
||||
return timeout
|
||||
|
||||
def _check_shares(self) -> float:
|
||||
assert sqlite3 # type: ignore
|
||||
|
||||
now = time.time()
|
||||
timeout = now + 9001
|
||||
|
||||
db = sqlite3.connect(self.args.shr_db, timeout=2)
|
||||
cur = db.cursor()
|
||||
|
||||
q = "select k from sh where t1 and t1 <= ?"
|
||||
rm = [x[0] for x in cur.execute(q, (now,))]
|
||||
if rm:
|
||||
self.log("forgetting expired shares %s" % (rm,))
|
||||
q = "delete from sh where k=?"
|
||||
cur.executemany(q, [(x,) for x in rm])
|
||||
db.commit()
|
||||
Daemon(self.hub._reload_blocking, "sharedrop", (False, False))
|
||||
|
||||
q = "select min(t1) from sh where t1 > 1"
|
||||
(earliest,) = cur.execute(q).fetchone()
|
||||
if earliest:
|
||||
timeout = earliest - now
|
||||
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
return timeout
|
||||
|
||||
def _check_xiu(self) -> float:
|
||||
if self.xiu_busy:
|
||||
return 2
|
||||
|
@ -2535,6 +2568,10 @@ class Up2k(object):
|
|||
|
||||
cur.connection.commit()
|
||||
|
||||
def wake_rescanner(self):
|
||||
with self.rescan_cond:
|
||||
self.rescan_cond.notify_all()
|
||||
|
||||
def handle_json(
|
||||
self, cj: dict[str, Any], busy_aps: dict[str, int]
|
||||
) -> dict[str, Any]:
|
||||
|
|
|
@ -1147,6 +1147,7 @@ html.y #widget.open {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#fshr,
|
||||
#wtgrid,
|
||||
#wtico {
|
||||
position: relative;
|
||||
|
@ -1333,6 +1334,7 @@ html.y #widget.open {
|
|||
#widget.cmp #wtoggle {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
#widget.cmp #fshr,
|
||||
#widget.cmp #wtgrid {
|
||||
display: none;
|
||||
}
|
||||
|
@ -1857,6 +1859,7 @@ html.y #tree.nowrap .ntree a+a:hover {
|
|||
#unpost td:nth-child(4) {
|
||||
text-align: right;
|
||||
}
|
||||
#shui,
|
||||
#rui {
|
||||
background: #fff;
|
||||
background: var(--bg);
|
||||
|
@ -1872,13 +1875,25 @@ html.y #tree.nowrap .ntree a+a:hover {
|
|||
padding: 1em;
|
||||
z-index: 765;
|
||||
}
|
||||
#shui div+div,
|
||||
#rui div+div {
|
||||
margin-top: 1em;
|
||||
}
|
||||
#shui table,
|
||||
#rui table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
#shui button {
|
||||
margin: 0 1em 0 0;
|
||||
}
|
||||
#shui .btn {
|
||||
font-size: 1em;
|
||||
}
|
||||
#shui td {
|
||||
padding: .8em 0;
|
||||
}
|
||||
#shui td+td,
|
||||
#rui td+td {
|
||||
padding: .2em 0 .2em .5em;
|
||||
}
|
||||
|
@ -1886,10 +1901,15 @@ html.y #tree.nowrap .ntree a+a:hover {
|
|||
font-family: 'scp', monospace, monospace;
|
||||
font-family: var(--font-mono), 'scp', monospace, monospace;
|
||||
}
|
||||
#shui td+td,
|
||||
#rui td+td,
|
||||
#shui td input[type="text"],
|
||||
#rui td input[type="text"] {
|
||||
width: 100%;
|
||||
}
|
||||
#shui td.exs input[type="text"] {
|
||||
width: 3em;
|
||||
}
|
||||
#rn_f.m td:first-child {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -309,6 +309,11 @@ var Ls = {
|
|||
"fd_emore": "select at least one item to delete",
|
||||
"fc_emore": "select at least one item to cut",
|
||||
|
||||
"fs_sc": "share the folder you're in",
|
||||
"fs_ss": "share the selected file/folder",
|
||||
"fs_just1": "select one or zero things to share",
|
||||
"fs_ok": "<h6>share-URL created</h6>\npress <code>Enter/OK</code> to Clipboard\npress <code>ESC/Cancel</code> to Close\n\n",
|
||||
|
||||
"frt_dec": "may fix some cases of broken filenames\">url-decode",
|
||||
"frt_rst": "reset modified filenames back to the original ones\">↺ reset",
|
||||
"frt_abrt": "abort and close this window\">❌ cancel",
|
||||
|
@ -826,6 +831,11 @@ var Ls = {
|
|||
"fd_emore": "velg minst én fil som skal slettes",
|
||||
"fc_emore": "velg minst én fil som skal klippes ut",
|
||||
|
||||
"fs_sc": "del mappen du er i nå",
|
||||
"fs_ss": "del den valgte filen/mappen",
|
||||
"fs_just1": "velg 1 eller 0 ting å dele",
|
||||
"fs_ok": "<h6>URL opprettet</h6>\ntrykk <code>Enter/OK</code> for å kopiere linken (for CTRL-V)\ntrykk <code>ESC/Avbryt</code> for å bare bekrefte\n\n",
|
||||
|
||||
"frt_dec": "kan korrigere visse ødelagte filnavn\">url-decode",
|
||||
"frt_rst": "nullstiller endringer (tilbake til de originale filnavnene)\">↺ reset",
|
||||
"frt_abrt": "avbryt og lukk dette vinduet\">❌ avbryt",
|
||||
|
@ -1089,6 +1099,7 @@ ebi('widget').innerHTML = (
|
|||
'<div id="wtoggle">' +
|
||||
'<span id="wfs"></span>' +
|
||||
'<span id="wfm"><a' +
|
||||
' href="#" id="fshr" tt="' + L.wt_shr + '">📨<span>share</span></a><a' +
|
||||
' href="#" id="fren" tt="' + L.wt_ren + '">✎<span>name</span></a><a' +
|
||||
' href="#" id="fdel" tt="' + L.wt_del + '">⌫<span>del.</span></a><a' +
|
||||
' href="#" id="fcut" tt="' + L.wt_cut + '">✂<span>cut</span></a><a' +
|
||||
|
@ -3691,6 +3702,7 @@ var fileman = (function () {
|
|||
bdel = ebi('fdel'),
|
||||
bcut = ebi('fcut'),
|
||||
bpst = ebi('fpst'),
|
||||
bshr = ebi('fshr'),
|
||||
t_paste,
|
||||
r = {};
|
||||
|
||||
|
@ -3705,17 +3717,33 @@ var fileman = (function () {
|
|||
r.clip = jread('fman_clip', []).slice(1);
|
||||
|
||||
var sel = msel.getsel(),
|
||||
nsel = sel.length;
|
||||
nsel = sel.length,
|
||||
enren = nsel,
|
||||
endel = nsel,
|
||||
encut = nsel,
|
||||
enpst = r.clip && r.clip.length,
|
||||
enshr = nsel < 2,
|
||||
hren = !(have_mv && has(perms, 'write') && has(perms, 'move')),
|
||||
hdel = !(have_del && has(perms, 'delete')),
|
||||
hcut = !(have_mv && has(perms, 'move')),
|
||||
hpst = !(have_mv && has(perms, 'write')),
|
||||
hshr = !(have_shr && acct != '*' && (has(perms, 'read') || has(perms, 'write')));
|
||||
|
||||
clmod(bren, 'en', nsel);
|
||||
clmod(bdel, 'en', nsel);
|
||||
clmod(bcut, 'en', nsel);
|
||||
clmod(bpst, 'en', r.clip && r.clip.length);
|
||||
if (!(enren || endel || encut || enpst))
|
||||
hren = hdel = hcut = hpst = true;
|
||||
|
||||
clmod(bren, 'en', enren);
|
||||
clmod(bdel, 'en', endel);
|
||||
clmod(bcut, 'en', encut);
|
||||
clmod(bpst, 'en', enpst);
|
||||
clmod(bshr, 'en', enshr);
|
||||
|
||||
clmod(bren, 'hide', hren);
|
||||
clmod(bdel, 'hide', hdel);
|
||||
clmod(bcut, 'hide', hcut);
|
||||
clmod(bpst, 'hide', hpst);
|
||||
clmod(bshr, 'hide', hshr);
|
||||
|
||||
clmod(bren, 'hide', !(have_mv && has(perms, 'write') && has(perms, 'move')));
|
||||
clmod(bdel, 'hide', !(have_del && has(perms, 'delete')));
|
||||
clmod(bcut, 'hide', !(have_mv && has(perms, 'move')));
|
||||
clmod(bpst, 'hide', !(have_mv && has(perms, 'write')));
|
||||
clmod(ebi('wfm'), 'act', QS('#wfm a.en:not(.hide)'));
|
||||
|
||||
var wfs = ebi('wfs'), h = '';
|
||||
|
@ -3726,6 +3754,7 @@ var fileman = (function () {
|
|||
clmod(wfs, 'act', h);
|
||||
|
||||
bpst.setAttribute('tt', L.ft_paste.format(r.clip.length));
|
||||
bshr.setAttribute('tt', nsel ? L.fs_ss : L.fs_sc);
|
||||
};
|
||||
|
||||
r.fsi = function (sel) {
|
||||
|
@ -3763,6 +3792,163 @@ var fileman = (function () {
|
|||
return ret;
|
||||
};
|
||||
|
||||
r.share = function (e) {
|
||||
ev(e);
|
||||
|
||||
var sel = msel.getsel();
|
||||
if (sel.length > 1)
|
||||
return toast.err(3, L.fs_just1);
|
||||
|
||||
var vp = get_evpath();
|
||||
if (sel.length)
|
||||
vp = sel[0].vp;
|
||||
|
||||
vp = uricom_dec(vp.split('?')[0]);
|
||||
|
||||
var shui = ebi('shui');
|
||||
if (!shui) {
|
||||
shui = mknod('div', 'shui');
|
||||
document.body.appendChild(shui);
|
||||
}
|
||||
shui.style.display = 'block';
|
||||
|
||||
var html = [
|
||||
'<div>',
|
||||
'<table>',
|
||||
'<tr><td colspan="2">',
|
||||
'<button id="sh_abrt">❌ abort</button>',
|
||||
'<button id="sh_rand">🎲 random</button>',
|
||||
'<button id="sh_apply">✅ create share</button>',
|
||||
'</td></tr>',
|
||||
'<tr><td>name</td><td><input type="text" id="sh_k" ' + NOAC + ' tt="name your link" /></td></tr>',
|
||||
'<tr><td>source</td><td><input type="text" id="sh_vp" ' + NOAC + ' readonly tt="the file or folder to share" /></td></tr>',
|
||||
'<tr><td>passwd</td><td><input type="text" id="sh_pw" ' + NOAC + ' tt="optional password" /></td></tr>',
|
||||
'<tr><td>expiry</td><td class="exs">',
|
||||
'<input type="text" id="sh_exm" ' + NOAC + ' /> min / ',
|
||||
'<input type="text" id="sh_exh" ' + NOAC + ' /> hours / ',
|
||||
'<input type="text" id="sh_exd" ' + NOAC + ' /> days',
|
||||
'</td></tr>',
|
||||
'<tr><td>perms</td><td class="sh_axs">',
|
||||
];
|
||||
for (var a = 0; a < perms.length; a++)
|
||||
if (perms[a] != 'admin')
|
||||
html.push('<a href="#" class="tgl btn">' + perms[a] + '</a>');
|
||||
|
||||
html.push('</td></tr></div');
|
||||
shui.innerHTML = html.join('\n');
|
||||
|
||||
var sh_rand = ebi('sh_rand'),
|
||||
sh_abrt = ebi('sh_abrt'),
|
||||
sh_apply = ebi('sh_apply'),
|
||||
exm = ebi('sh_exm'),
|
||||
exh = ebi('sh_exh'),
|
||||
exd = ebi('sh_exd'),
|
||||
sh_k = ebi('sh_k'),
|
||||
sh_vp = ebi('sh_vp');
|
||||
sh_pw = ebi('sh_pw');
|
||||
|
||||
function setexp(a, b) {
|
||||
a = parseFloat(a);
|
||||
if (!isNum(a))
|
||||
return;
|
||||
|
||||
var v = a * b;
|
||||
swrite('fsh_exp', v);
|
||||
|
||||
if (exm.value != v) exm.value = Math.round(v * 10) / 10; v /= 60;
|
||||
if (exh.value != v) exh.value = Math.round(v * 10) / 10; v /= 24;
|
||||
if (exd.value != v) exd.value = Math.round(v * 10) / 10;
|
||||
}
|
||||
function setdef() {
|
||||
setexp(icfg_get('fsh_exp', 60 * 24), 1);
|
||||
}
|
||||
setdef();
|
||||
|
||||
exm.oninput = function () { setexp(this.value, 1); };
|
||||
exh.oninput = function () { setexp(this.value, 60); };
|
||||
exd.oninput = function () { setexp(this.value, 60 * 24); };
|
||||
exm.onfocus = exh.onfocus = exd.onfocus = function () {
|
||||
this.value = '';
|
||||
};
|
||||
exm.onblur = exh.onblur = exd.onblur = setdef;
|
||||
|
||||
exm.onkeydown = exh.onkeydown = exd.onkeydown =
|
||||
sh_k.onkeydown = sh_pw.onkeydown = function (e) {
|
||||
var kc = (e.code || e.key) + '';
|
||||
if (kc.endsWith('Enter'))
|
||||
sh_apply.click();
|
||||
};
|
||||
|
||||
sh_abrt.onclick = function () {
|
||||
shui.parentNode.removeChild(shui);
|
||||
};
|
||||
sh_rand.onclick = function () {
|
||||
var v = randstr(12).replace(/l/g, 'n');
|
||||
if (sel.length && !noq_href(ebi(sel[0].id)).endsWith('/'))
|
||||
v += '.' + vp.split('.').pop();
|
||||
sh_k.value = v;
|
||||
};
|
||||
tt.att(shui);
|
||||
|
||||
var pbtns = QSA('#shui .sh_axs a');
|
||||
for (var a = 0; a < pbtns.length; a++)
|
||||
pbtns[a].onclick = shspf;
|
||||
|
||||
function shspf() {
|
||||
clmod(this, 'on', 't');
|
||||
}
|
||||
clmod(pbtns[0], 'on', 1);
|
||||
|
||||
sh_vp.value = vp;
|
||||
|
||||
sh_k.oninput = function (e) {
|
||||
var v = this.value,
|
||||
v2 = v.replace(/[^0-9a-zA-Z\.-]/g, '_');
|
||||
|
||||
if (v != v2)
|
||||
this.value = v2;
|
||||
};
|
||||
|
||||
function shr_cb() {
|
||||
if (this.status !== 201) {
|
||||
shui.style.display = 'block';
|
||||
var msg = unpre(this.responseText);
|
||||
toast.err(9, msg);
|
||||
return;
|
||||
}
|
||||
var surl = this.responseText;
|
||||
modal.confirm(L.fs_ok + esc(surl), function() {
|
||||
cliptxt(surl, function () {
|
||||
toast.ok(1, 'copied to clipboard');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
sh_apply.onclick = function () {
|
||||
if (!sh_k.value)
|
||||
sh_rand.click();
|
||||
|
||||
var plist = [];
|
||||
for (var a = 0; a < pbtns.length; a++)
|
||||
if (clgot(pbtns[a], 'on'))
|
||||
plist.push(pbtns[a].textContent);
|
||||
|
||||
shui.style.display = 'none';
|
||||
var body = {
|
||||
"k": sh_k.value,
|
||||
"vp": sh_vp.value,
|
||||
"pw": sh_pw.value,
|
||||
"exp": exm.value,
|
||||
"perms": plist,
|
||||
};
|
||||
var xhr = new XHR();
|
||||
xhr.open('POST', SR + '/?share', true);
|
||||
xhr.setRequestHeader('Content-Type', 'text/plain');
|
||||
xhr.onload = xhr.onerror = shr_cb;
|
||||
xhr.send(JSON.stringify(body));
|
||||
};
|
||||
};
|
||||
|
||||
r.rename = function (e) {
|
||||
ev(e);
|
||||
if (clgot(bren, 'hide'))
|
||||
|
@ -4339,6 +4525,7 @@ var fileman = (function () {
|
|||
bdel.onclick = r.delete;
|
||||
bcut.onclick = r.cut;
|
||||
bpst.onclick = r.paste;
|
||||
bshr.onclick = r.share;
|
||||
|
||||
return r;
|
||||
})();
|
||||
|
@ -5349,6 +5536,9 @@ var ahotkeys = function (e) {
|
|||
if (ebi('rn_cancel'))
|
||||
return ebi('rn_cancel').click();
|
||||
|
||||
if (ebi('sh_abrt'))
|
||||
return ebi('sh_abrt').click();
|
||||
|
||||
if (QS('.opview.act'))
|
||||
return QS('#ops>a').click();
|
||||
|
||||
|
|
79
copyparty/web/shares.css
Normal file
79
copyparty/web/shares.css
Normal file
|
@ -0,0 +1,79 @@
|
|||
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;
|
||||
}
|
||||
#wrap>span {
|
||||
margin: 0 0 0 1em;
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
||||
li {
|
||||
margin: 1em 0;
|
||||
}
|
||||
a {
|
||||
color: #047;
|
||||
background: #fff;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
border-bottom: 1px solid #8ab;
|
||||
border-radius: .2em;
|
||||
padding: .2em .6em;
|
||||
margin: 0 .3em;
|
||||
}
|
||||
td a {
|
||||
margin: 0;
|
||||
}
|
||||
#w {
|
||||
color: #fff;
|
||||
background: #940;
|
||||
border-color: #b70;
|
||||
}
|
||||
#repl {
|
||||
border: none;
|
||||
background: none;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
position: fixed;
|
||||
bottom: .25em;
|
||||
left: .2em;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
position: relative;
|
||||
}
|
||||
th {
|
||||
top: -1px;
|
||||
position: sticky;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
td, th {
|
||||
padding: .3em .6em;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
html.z {
|
||||
background: #222;
|
||||
color: #ccc;
|
||||
}
|
||||
html.z a {
|
||||
color: #fff;
|
||||
background: #057;
|
||||
border-color: #37a;
|
||||
}
|
||||
html.z th {
|
||||
background: #222;
|
||||
}
|
||||
html.bz {
|
||||
color: #bbd;
|
||||
background: #11121d;
|
||||
}
|
74
copyparty/web/shares.html
Normal file
74
copyparty/web/shares.html
Normal file
|
@ -0,0 +1,74 @@
|
|||
<!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/shares.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 }}/?shares" class="af">refresh</a>
|
||||
<a id="a" href="{{ r }}/?h" class="af">controlpanel</a>
|
||||
|
||||
<span>axs = perms (read,write,move,delet)</span>
|
||||
<span>st 1=file 2=dir</span>
|
||||
<span>min/hrs = time left</span>
|
||||
|
||||
<table><tr>
|
||||
<th>delete</th>
|
||||
<th>sharekey</th>
|
||||
<th>pw</th>
|
||||
<th>source</th>
|
||||
<th>axs</th>
|
||||
<th>st</th>
|
||||
<th>user</th>
|
||||
<th>created</th>
|
||||
<th>expires</th>
|
||||
<th>min</th>
|
||||
<th>hrs</th>
|
||||
</tr>
|
||||
{% for k, pw, vp, pr, st, un, t0, t1 in rows %}
|
||||
<tr>
|
||||
<td><a href="#" k="{{ k }}">delete</a></td>
|
||||
<td><a href="{{ r }}{{ shr }}{{ k }}">{{ k }}</a></td>
|
||||
<td>{{ pw }}</td>
|
||||
<td><a href="{{ r }}/{{ vp|e }}">{{ vp|e }}</a></td>
|
||||
<td>{{ pr }}</td>
|
||||
<td>{{ st }}</td>
|
||||
<td>{{ un|e }}</td>
|
||||
<td>{{ t0 }}</td>
|
||||
<td>{{ t1 }}</td>
|
||||
<td>{{ (t1 - now) // 60 if t1 else "never" }}</td>
|
||||
<td>{{ (t1 - now) // 3600 if t1 else "never" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if not rows %}
|
||||
(you don't have any active shares btw)
|
||||
{% endif %}
|
||||
<script>
|
||||
|
||||
var SR = {{ r|tojson }},
|
||||
shr="{{ shr }}",
|
||||
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/shares.js?_={{ ts }}"></script>
|
||||
{%- if js %}
|
||||
<script src="{{ js }}_={{ ts }}"></script>
|
||||
{%- endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
19
copyparty/web/shares.js
Normal file
19
copyparty/web/shares.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
var t = QSA('a[k]');
|
||||
for (var a = 0; a < t.length; a++)
|
||||
t[a].onclick = rm;
|
||||
|
||||
function rm() {
|
||||
var u = SR + shr + uricom_enc(this.getAttribute('k')) + '?unshare',
|
||||
xhr = new XHR();
|
||||
|
||||
xhr.open('POST', u, true);
|
||||
xhr.onload = xhr.onerror = cb;
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function cb() {
|
||||
if (this.status !== 200)
|
||||
return modal.alert('<h6>server error</h6>' + esc(unpre(this.responseText)));
|
||||
|
||||
document.location = '?shares';
|
||||
}
|
|
@ -76,8 +76,12 @@
|
|||
</ul>
|
||||
{%- endif %}
|
||||
|
||||
<h1 id="cc">client config:</h1>
|
||||
<h1 id="cc">other stuff:</h1>
|
||||
<ul>
|
||||
{%- if this.uname != '*' and this.args.shr %}
|
||||
<li><a id="y" href="{{ r }}/?shares">edit shares</a></li>
|
||||
{% endif %}
|
||||
|
||||
{% if k304 or k304vis %}
|
||||
{% if k304 %}
|
||||
<li><a id="h" href="{{ r }}/?k304=n">disable k304</a> (currently enabled)
|
||||
|
|
|
@ -9,7 +9,7 @@ var Ls = {
|
|||
"e2": "leser inn konfigurasjonsfiler på nytt$N(kontoer, volumer, volumbrytere)$Nog kartlegger alle e2ds-volumer$N$Nmerk: endringer i globale parametere$Nkrever en full restart for å ta gjenge",
|
||||
"f1": "du kan betrakte:",
|
||||
"g1": "du kan laste opp til:",
|
||||
"cc1": "klient-konfigurasjon",
|
||||
"cc1": "brytere og sånt",
|
||||
"h1": "skru av k304",
|
||||
"i1": "skru på k304",
|
||||
"j1": "k304 bryter tilkoplingen for hver HTTP 304. Dette hjelper mot visse mellomtjenere som kan sette seg fast / plutselig slutter å laste sider, men det reduserer også ytelsen betydelig",
|
||||
|
@ -28,6 +28,7 @@ var Ls = {
|
|||
"v2": "bruk denne serveren som en lokal harddisk$N$NADVARSEL: kommer til å vise passordet ditt!",
|
||||
"w1": "bytt til https",
|
||||
"x1": "bytt passord",
|
||||
"y1": "dine delinger",
|
||||
"ta1": "du må skrive et nytt passord først",
|
||||
"ta2": "gjenta for å bekrefte nytt passord:",
|
||||
"ta3": "fant en skrivefeil; vennligst prøv igjen",
|
||||
|
|
|
@ -473,6 +473,24 @@ function crc32(str) {
|
|||
}
|
||||
|
||||
|
||||
function randstr(len) {
|
||||
var ret = '';
|
||||
try {
|
||||
var ar = new Uint32Array(Math.floor((len + 3) / 4));
|
||||
crypto.getRandomValues(ar);
|
||||
for (var a = 0; a < ar.length; a++)
|
||||
ret += ('000' + ar[a].toString(36)).slice(-4);
|
||||
return ret.slice(0, len);
|
||||
}
|
||||
catch (ex) {
|
||||
console.log('using unsafe randstr because ' + ex);
|
||||
while (ret.length < len)
|
||||
ret += ('000' + Math.floor(Math.random() * 1679616).toString(36)).slice(-4);
|
||||
return ret.slice(0, len);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function clmod(el, cls, add) {
|
||||
if (!el)
|
||||
return false;
|
||||
|
|
|
@ -139,6 +139,7 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
|
|||
| GET | `?tar&w` | pregenerate webp thumbnails |
|
||||
| GET | `?tar&j` | pregenerate jpg thumbnails |
|
||||
| GET | `?tar&p` | pregenerate audio waveforms |
|
||||
| GET | `?shares` | list your shared files/folders |
|
||||
| GET | `?ups` | show recent uploads from your IP |
|
||||
| GET | `?ups&filter=f` | ...where URL contains `f` |
|
||||
| GET | `?mime=foo` | specify return mimetype `foo` |
|
||||
|
@ -175,6 +176,8 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
|
|||
| mPOST | `?media` | `f=FILE` | ...and return medialink (not hotlink) |
|
||||
| mPOST | | `act=mkdir`, `name=foo` | create directory `foo` at URL |
|
||||
| POST | `?delete` | | delete URL recursively |
|
||||
| POST | `?unshare` | | stop sharing a file/folder |
|
||||
| jPOST | `?share` | (complicated) | create temp URL for file/folder |
|
||||
| jPOST | `?delete` | `["/foo","/bar"]` | delete `/foo` and `/bar` recursively |
|
||||
| uPOST | | `msg=foo` | send message `foo` into server log |
|
||||
| mPOST | | `act=tput`, `body=TEXT` | overwrite markdown document at URL |
|
||||
|
|
|
@ -103,6 +103,9 @@ copyparty/web/mde.html,
|
|||
copyparty/web/mde.js,
|
||||
copyparty/web/msg.css,
|
||||
copyparty/web/msg.html,
|
||||
copyparty/web/shares.css,
|
||||
copyparty/web/shares.html,
|
||||
copyparty/web/shares.js,
|
||||
copyparty/web/splash.css,
|
||||
copyparty/web/splash.html,
|
||||
copyparty/web/splash.js,
|
||||
|
|
|
@ -137,7 +137,7 @@ class Cfg(Namespace):
|
|||
ex = "db_act k304 loris re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo"
|
||||
ka.update(**{k: 0 for k in ex.split()})
|
||||
|
||||
ex = "ah_alg bname chpw_db doctitle df exit favico idp_h_usr html_head lg_sbf log_fk md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i tcolor textfiles unlist vname R RS SR"
|
||||
ex = "ah_alg bname chpw_db doctitle df exit favico idp_h_usr html_head lg_sbf log_fk md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles unlist vname R RS SR"
|
||||
ka.update(**{k: "" for k in ex.split()})
|
||||
|
||||
ex = "grp on403 on404 xad xar xau xban xbd xbr xbu xiu xm"
|
||||
|
@ -233,7 +233,7 @@ class VHttpSrv(object):
|
|||
self.nreq = 0
|
||||
self.nsus = 0
|
||||
|
||||
aliases = ["splash", "browser", "browser2", "msg", "md", "mde"]
|
||||
aliases = ["splash", "shares", "browser", "browser2", "msg", "md", "mde"]
|
||||
self.j2 = {x: J2_FILES for x in aliases}
|
||||
|
||||
self.gpwd = Garda("")
|
||||
|
|
Loading…
Reference in a new issue