From cc5420a324da9e7f0024c497189e15a7574cfb06 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 25 May 2026 18:32:42 +0000 Subject: [PATCH] download-as-zip: toplevel optional --- README.md | 1 + copyparty/authsrv.py | 8 +------- copyparty/httpcli.py | 15 ++++++++++++--- copyparty/httpsrv.py | 2 +- copyparty/sutil.py | 12 ++++++++---- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 31d0fdc9..85802fa1 100644 --- a/README.md +++ b/README.md @@ -836,6 +836,7 @@ you can also zip a selection of files or folders by clicking them in the browser cool trick: download a folder by appending url-params `?tar&opus` or `?tar&mp3` to transcode all audio files (except aac|m4a|mp3|ogg|opus|wma) to opus/mp3 before they're added to the archive * super useful if you're 5 minutes away from takeoff and realize you don't have any music on your phone but your server only has flac files and downloading those will burn through all your data + there wouldn't be enough time anyways +* and url-param `&name=foo` changes the name of the toplevel folder in the archive to `foo`, and just `&name` removes the folder entirely * and url-param `&nodot` skips dotfiles/dotfolders; they are included by default if your account has permission to see them * and url-params `&j` / `&w` produce jpeg/webm thumbnails/spectrograms instead of the original audio/video/images (`&p` for audio waveforms) * can also be used to pregenerate thumbnails; combine with `--th-maxage=9999999` or `--th-clean=0` diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 2f284920..9eb0e259 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -900,20 +900,14 @@ class VFS(object): def zipgen( self, - vpath: str, + folder: str, vrem: str, flt: set[str], uname: str, dirs: bool, dots: int, scandir: bool, - wrap: bool = True, ) -> Generator[dict[str, Any], None, None]: - - # if multiselect: add all items to archive root - # if single folder: the folder itself is the top-level item - folder = "" if flt or not wrap else (vpath.split("/")[-1].lstrip(".") or "top") - g = self.walk(folder, vrem, [], uname, [[True, False]], dots, scandir, False) for _, _, vpath, apath, files, rd, vd in g: if flt: diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 91a5803b..246545fc 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1823,14 +1823,13 @@ class HttpCli(object): # because lstat=true would not recurse into subfolders # and this is a rare case where we actually want that fgen = vn.zipgen( - rem, + "", rem, set(), self.uname, True, 1, not self.args.no_scandir, - wrap=False, ) elif depth == "0": @@ -5163,6 +5162,16 @@ class HttpCli(object): if items: fn = "sel-" + fn + if "name" in self.ouparam: + # user-selected name for toplevel folder, or blank for none + vpath = undot(self.ouparam["name"]) + elif items: + # multiselect; add all items to archive root + vpath = "" + else: + # single folder; the folder itself is the top-level item + vpath = vpath.split("/")[-1].lstrip(".") or "top" + if vn.flags.get("zipmax") and not ( vn.flags.get("zipmaxu") and self.uname != "*" ): @@ -5209,7 +5218,7 @@ class HttpCli(object): if cfmt: self.log("transcoding to [{}]".format(cfmt)) - fgen = gfilter(fgen, self.thumbcli, self.uname, vpath, cfmt) + fgen = gfilter(fgen, self.thumbcli, self.uname, self.vpath, vpath, cfmt) now = time.time() self.dl_id = "%s:%s" % (self.ip, self.addr[1]) diff --git a/copyparty/httpsrv.py b/copyparty/httpsrv.py index f6ec1e56..db81725b 100644 --- a/copyparty/httpsrv.py +++ b/copyparty/httpsrv.py @@ -700,7 +700,7 @@ class HttpSrv(object): if not fmts: continue log("starting for volume /%s" % (vn.vpath,), 6) - g = vn.walk("x", "/", [], LEELOO_DALLAS, [[True]], 2, scandir, False, False) + g = vn.walk("", "/", [], LEELOO_DALLAS, [[True]], 2, scandir, False, False) g = gfilter2(g, self, vn.vpath, fmts.split(",")) for f in g: nfiles += 1 diff --git a/copyparty/sutil.py b/copyparty/sutil.py index 7fc03bab..97c10086 100644 --- a/copyparty/sutil.py +++ b/copyparty/sutil.py @@ -61,6 +61,7 @@ def gfilter( thumbcli: ThumbCli, uname: str, vtop: str, + vname: str, fmt: str, ) -> Generator[dict[str, Any], None, None]: from concurrent.futures import ThreadPoolExecutor @@ -70,7 +71,7 @@ def gfilter( _pools[tp] = 1 try: for f in fgen: - task = tp.submit(enthumb, thumbcli, uname, vtop, f, fmt) + task = tp.submit(enthumb, thumbcli, uname, vtop, vname, f, fmt) pend.append((task, f)) if pend[0][0].done() or len(pend) > CORES * 4: task, f = pend.pop(0) @@ -130,7 +131,7 @@ def gfilter2( try: f = {"vp": vp, "st": fi[1]} task = tp.submit( - enthumb, hsrv.thumbcli, LEELOO_DALLAS, vtop, f, fmt + enthumb, hsrv.thumbcli, LEELOO_DALLAS, vtop, "", f, fmt ) pend.append((task, f)) if pend[0][0].done() or len(pend) > CORES * 4: @@ -152,14 +153,17 @@ def gfilter2( def enthumb( - thumbcli: ThumbCli, uname: str, vtop: str, f: dict[str, Any], fmt: str + thumbcli: ThumbCli, uname: str, vtop: str, vname: str, f: dict[str, Any], fmt: str ) -> dict[str, Any]: rem = f["vp"] ext = rem.rsplit(".", 1)[-1].lower() if (fmt == "mp3" and ext == "mp3") or (fmt == "opus" and ext in TAR_NO_OPUS): raise Exception() - vp = vjoin(vtop, rem.split("/", 1)[1]) + if vname: + vp = vjoin(vtop, rem.split("/", 1)[1]) + else: + vp = vjoin(vtop, rem) vn, rem = thumbcli.asrv.vfs.get(vp, uname, True, False) dbv, vrem = vn.get_dbv(rem) thp = thumbcli.get(dbv, vrem, f["st"].st_mtime, fmt)