From 076bf52c13f9fad68d63000ba0bc8652f8ab1d9f Mon Sep 17 00:00:00 2001 From: AppleTheGolden Date: Sat, 4 Oct 2025 19:25:03 +0200 Subject: [PATCH] WIP cbz browsing --- copyparty/__main__.py | 1 + copyparty/httpcli.py | 74 ++++++++++++++++++++++++++++++++++++ copyparty/web/baguettebox.js | 62 +++++++++++++++++++++++++++++- copyparty/web/browser.html | 3 +- copyparty/web/browser.js | 4 +- 5 files changed, 141 insertions(+), 3 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 68ae5c0d..5bed2510 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1516,6 +1516,7 @@ def add_optouts(ap): 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-tail", action="store_true", help="disable streaming a growing files with ?tail (volflag=notail)") ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader-IP into the database; will also disable unpost, you may want \033[32m--forget-ip\033[0m instead (volflag=no_db_ip)") + ap2.add_argument("--no-zls", action="store_true", help="disable browsing the contents of zip/cbz files, does not affect thumbnails") def add_safety(ap): diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 77778547..a8dea451 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1519,6 +1519,74 @@ class HttpCli(object): self.log("rss: %d hits, %d bytes" % (len(hits), len(bret))) return True + def tx_zls(self, abspath) -> bool: + if self.do_log: + self.log("zls %s @%s" % (self.req, self.uname)) + if self.args.no_zls: + raise Pebkac(405, "zip browsing is disabled in server config") + + import zipfile + + try: + with zipfile.ZipFile(abspath, "r") as zf: + filelist = [f.filename for f in zf.infolist()] + ret = json.dumps(filelist).encode("utf-8", "replace") + self.reply(ret, mime="application/json") + return True + except (zipfile.BadZipfile, RuntimeError): + raise Pebkac(404, "requested file is not a valid zip file") + + def tx_zget(self, abspath) -> bool: + maxsz = 1024 * 1024 * 64 + + inner_path = self.uparam.get("zget") + if not inner_path: + raise Pebkac(405, "inner path is required") + if self.do_log: + self.log( + "zget %s \033[35m%s\033[0m @%s" % (self.req, inner_path, self.uname) + ) + if self.args.no_zls: + raise Pebkac(405, "zip browsing is disabled in server config") + + import zipfile + + try: + with zipfile.ZipFile(abspath, "r") as zf: + zi = zf.getinfo(inner_path) + with zf.open(zi, "r") as fi: + self.send_headers(length=zi.file_size, mime=guess_mime(inner_path)) + + remains = sendfile_py( + self.log, 0, zi.file_size, + fi, + self.s, + self.args.s_wr_sz, + self.args.s_wr_slp, + not self.args.no_poll, + {}, + "", + ) + # fd, ret = tempfile.mkstemp("." + inner_path.rsplit(".", 1)[0]) + # fsz = 0 + # with os.fdopen(fd, "wb") as fo: + # + # while True: + # buf = fi.read(32768) + # if not buf: + # break + # + # fsz += len(buf) + # if fsz > maxsz: + # raise Exception("zipbomb defused") + # + # fo.write(buf) + except KeyError: + raise Pebkac(404, "no such file in archive") + except (zipfile.BadZipfile, RuntimeError): + raise Pebkac(404, "requested file is not a valid zip file") + + def handle_propfind(self) -> bool: if self.do_log: self.log("PFIND %s @%s" % (self.req, self.uname)) @@ -6478,6 +6546,11 @@ class HttpCli(object): ): return self.tx_md(vn, abspath) + if "zls" in self.uparam: + return self.tx_zls(abspath) + if "zget" in self.uparam: + return self.tx_zget(abspath) + if not add_og or not og_fn: return self.tx_file( abspath, None if st.st_size or "nopipe" in vn.flags else vn.realpath @@ -6576,6 +6649,7 @@ class HttpCli(object): "taglist": [], "have_tags_idx": int(e2t), "have_b_u": (self.can_write and self.uparam.get("b") == "u"), + "have_zls": int(not self.args.no_zls), "sb_lg": vn.js_ls["sb_lg"], "url_suf": url_suf, "title": html_escape("%s %s" % (self.args.bname, self.vpath), crlf=True), diff --git a/copyparty/web/baguettebox.js b/copyparty/web/baguettebox.js index 08857282..712e487e 100644 --- a/copyparty/web/baguettebox.js +++ b/copyparty/web/baguettebox.js @@ -34,6 +34,8 @@ window.baguetteBox = (function () { scrollTimer = 0, re_i = /^[^?]+\.(a?png|avif|bmp|gif|heif|jpe?g|jfif|svg|webp)(\?|$)/i, re_v = /^[^?]+\.(webm|mkv|mp4|m4v|mov)(\?|$)/i, + re_cbz = /^[^?]+\.(cbz)(\?|$)/i, + cbz_pics = ["png", "jpg", "jpeg", "gif", "bmp", "tga", "tif", "tiff", "webp", "avif"], anims = ['slideIn', 'fadeIn', 'none'], data = {}, // all galleries imagesElements = [], @@ -147,6 +149,8 @@ window.baguetteBox = (function () { tagsNodeList = [galleryElement]; else tagsNodeList = galleryElement.getElementsByTagName('a'); + if (have_zls) + bindCbzClickListeners(tagsNodeList, userOptions); tagsNodeList = [].filter.call(tagsNodeList, function (element) { if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1) @@ -178,6 +182,62 @@ window.baguetteBox = (function () { return [selectorData.galleries, options]; } + function bindCbzClickListeners(tagsNodeList, userOptions) { + var cbzNodes = [].filter.call(tagsNodeList, function (element) { + return re_cbz.test(element.href); + }); + if (!tagsNodeList.length) { + return; + } + + [].forEach.call(cbzNodes, function (cbzElement, index) { + var gallery = []; + var eventHandler = function (e) { + if (ctrl(e) || e && e.shiftKey) + return true; + + e.preventDefault ? e.preventDefault() : e.returnValue = false; + fillCbzGallery(gallery, cbzElement, eventHandler).then(() => { + console.log("starting prepare") + prepareOverlay(gallery, userOptions); + console.log("prepare done, starting show") + showOverlay(0); + }); + } + + bind(cbzElement, "click", eventHandler); + }) + } + + function fillCbzGallery(gallery, cbzElement, eventHandler) { + var href = cbzElement.href + var zlsHref = href + (href.indexOf("?") === -1 ? "?" : "&") + "zls"; + console.log("pre-fetch") + return fetch(zlsHref).then(response => response.json()) + .then((fileList) => { + console.log("fetched") + var imagesList = fileList.filter((name) => + name.indexOf(".") !== -1 + && cbz_pics.indexOf(name.split(".").pop()) !== -1 + ).sort(); + + imagesList.forEach((imageName) => { + var imageHref = href + + (href.indexOf("?") === -1 ? "?" : "&") + + "zget=" + + encodeURIComponent(imageName); + + var galleryItem = { + href: imageHref, + imageElement: cbzElement, + eventHandler: eventHandler, + }; + gallery.push(galleryItem); + }); + console.log(gallery); + }); + } + function clearCachedData() { for (var selector in data) if (data.hasOwnProperty(selector)) @@ -786,7 +846,7 @@ window.baguetteBox = (function () { imageContainer.removeChild(imageContainer.firstChild); var imageElement = galleryItem.imageElement, - imageSrc = imageElement.href, + imageSrc = galleryItem.href || imageElement.href, is_vid = re_v.test(imageSrc), thumbnailElement = imageElement.querySelector('img, video'), imageCaption = typeof options.captions === 'function' ? diff --git a/copyparty/web/browser.html b/copyparty/web/browser.html index 2201ab58..d54fedc1 100644 --- a/copyparty/web/browser.html +++ b/copyparty/web/browser.html @@ -139,7 +139,8 @@ have_tags_idx = {{ have_tags_idx }}, sb_lg = "{{ sb_lg }}", logues = {{ logues|tojson if sb_lg else "[]" }}, - ls0 = {{ ls0|tojson }}; + ls0 = {{ ls0|tojson }}, + have_zls = {{ have_zls }}; var STG = window.localStorage; document.documentElement.className = (STG && STG.cpp_thm) || dtheme; diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index e64d181c..319dc889 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -17565,7 +17565,9 @@ var thegrid = (function () { esc(uricom_dec(h.split('/').pop())) + ''; }, onChange: function (i) { - sethash('g' + r.bbox[i].imageElement.getAttribute('ref') + getsort()); + if (r.bbox[i]) { + sethash('g' + r.bbox[i].imageElement.getAttribute('ref') + getsort()); + } } }); r.bbox = br[0][0];