download-as-zip: toplevel optional

This commit is contained in:
ed 2026-05-25 18:32:42 +00:00
parent c28aa08b35
commit cc5420a324
5 changed files with 23 additions and 15 deletions

View file

@ -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 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 * 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-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) * 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` * can also be used to pregenerate thumbnails; combine with `--th-maxage=9999999` or `--th-clean=0`

View file

@ -900,20 +900,14 @@ class VFS(object):
def zipgen( def zipgen(
self, self,
vpath: str, folder: str,
vrem: str, vrem: str,
flt: set[str], flt: set[str],
uname: str, uname: str,
dirs: bool, dirs: bool,
dots: int, dots: int,
scandir: bool, scandir: bool,
wrap: bool = True,
) -> Generator[dict[str, Any], None, None]: ) -> 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) g = self.walk(folder, vrem, [], uname, [[True, False]], dots, scandir, False)
for _, _, vpath, apath, files, rd, vd in g: for _, _, vpath, apath, files, rd, vd in g:
if flt: if flt:

View file

@ -1823,14 +1823,13 @@ class HttpCli(object):
# because lstat=true would not recurse into subfolders # because lstat=true would not recurse into subfolders
# and this is a rare case where we actually want that # and this is a rare case where we actually want that
fgen = vn.zipgen( fgen = vn.zipgen(
rem, "",
rem, rem,
set(), set(),
self.uname, self.uname,
True, True,
1, 1,
not self.args.no_scandir, not self.args.no_scandir,
wrap=False,
) )
elif depth == "0": elif depth == "0":
@ -5163,6 +5162,16 @@ class HttpCli(object):
if items: if items:
fn = "sel-" + fn 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 ( if vn.flags.get("zipmax") and not (
vn.flags.get("zipmaxu") and self.uname != "*" vn.flags.get("zipmaxu") and self.uname != "*"
): ):
@ -5209,7 +5218,7 @@ class HttpCli(object):
if cfmt: if cfmt:
self.log("transcoding to [{}]".format(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() now = time.time()
self.dl_id = "%s:%s" % (self.ip, self.addr[1]) self.dl_id = "%s:%s" % (self.ip, self.addr[1])

View file

@ -700,7 +700,7 @@ class HttpSrv(object):
if not fmts: if not fmts:
continue continue
log("starting for volume /%s" % (vn.vpath,), 6) 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(",")) g = gfilter2(g, self, vn.vpath, fmts.split(","))
for f in g: for f in g:
nfiles += 1 nfiles += 1

View file

@ -61,6 +61,7 @@ def gfilter(
thumbcli: ThumbCli, thumbcli: ThumbCli,
uname: str, uname: str,
vtop: str, vtop: str,
vname: str,
fmt: str, fmt: str,
) -> Generator[dict[str, Any], None, None]: ) -> Generator[dict[str, Any], None, None]:
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
@ -70,7 +71,7 @@ def gfilter(
_pools[tp] = 1 _pools[tp] = 1
try: try:
for f in fgen: 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)) pend.append((task, f))
if pend[0][0].done() or len(pend) > CORES * 4: if pend[0][0].done() or len(pend) > CORES * 4:
task, f = pend.pop(0) task, f = pend.pop(0)
@ -130,7 +131,7 @@ def gfilter2(
try: try:
f = {"vp": vp, "st": fi[1]} f = {"vp": vp, "st": fi[1]}
task = tp.submit( task = tp.submit(
enthumb, hsrv.thumbcli, LEELOO_DALLAS, vtop, f, fmt enthumb, hsrv.thumbcli, LEELOO_DALLAS, vtop, "", f, fmt
) )
pend.append((task, f)) pend.append((task, f))
if pend[0][0].done() or len(pend) > CORES * 4: if pend[0][0].done() or len(pend) > CORES * 4:
@ -152,14 +153,17 @@ def gfilter2(
def enthumb( 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]: ) -> dict[str, Any]:
rem = f["vp"] rem = f["vp"]
ext = rem.rsplit(".", 1)[-1].lower() ext = rem.rsplit(".", 1)[-1].lower()
if (fmt == "mp3" and ext == "mp3") or (fmt == "opus" and ext in TAR_NO_OPUS): if (fmt == "mp3" and ext == "mp3") or (fmt == "opus" and ext in TAR_NO_OPUS):
raise Exception() 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) vn, rem = thumbcli.asrv.vfs.get(vp, uname, True, False)
dbv, vrem = vn.get_dbv(rem) dbv, vrem = vn.get_dbv(rem)
thp = thumbcli.get(dbv, vrem, f["st"].st_mtime, fmt) thp = thumbcli.get(dbv, vrem, f["st"].st_mtime, fmt)