mirror of
https://github.com/9001/copyparty.git
synced 2025-10-12 11:32:20 -06:00
view .cbz in browser (#916)
adds functionality to allow browsing .cbz directly in the browser, without downloading them and using a separate program. meant for quickly inspecting the contents, less so for reading. adds two new api calls, ?zls and ?zget, which return a file listing of a zip file and a specific file in the archive, respectively. uses the zipfile module, so no support for .cbr etc
This commit is contained in:
parent
46c205dd60
commit
8ef6dda74b
|
@ -1528,6 +1528,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-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-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-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):
|
def add_safety(ap):
|
||||||
|
|
|
@ -3027,6 +3027,7 @@ class AuthSrv(object):
|
||||||
"have_shr": self.args.shr,
|
"have_shr": self.args.shr,
|
||||||
"shr_who": vf["shr_who"],
|
"shr_who": vf["shr_who"],
|
||||||
"have_zip": not self.args.no_zip,
|
"have_zip": not self.args.no_zip,
|
||||||
|
"have_zls": not self.args.no_zls,
|
||||||
"have_mv": not self.args.no_mv,
|
"have_mv": not self.args.no_mv,
|
||||||
"have_del": not self.args.no_del,
|
"have_del": not self.args.no_del,
|
||||||
"have_unpost": int(self.args.unpost),
|
"have_unpost": int(self.args.unpost),
|
||||||
|
|
|
@ -1521,6 +1521,63 @@ class HttpCli(object):
|
||||||
self.log("rss: %d hits, %d bytes" % (len(hits), len(bret)))
|
self.log("rss: %d hits, %d bytes" % (len(hits), len(bret)))
|
||||||
return True
|
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 = [{"fn": 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)
|
||||||
|
if zi.file_size >= maxsz:
|
||||||
|
raise Pebkac(404, "zip bomb defused")
|
||||||
|
with zf.open(zi, "r") as fi:
|
||||||
|
self.send_headers(length=zi.file_size, mime=guess_mime(inner_path))
|
||||||
|
|
||||||
|
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,
|
||||||
|
{},
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
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:
|
def handle_propfind(self) -> bool:
|
||||||
if self.do_log:
|
if self.do_log:
|
||||||
self.log("PFIND %s @%s" % (self.req, self.uname))
|
self.log("PFIND %s @%s" % (self.req, self.uname))
|
||||||
|
@ -6480,6 +6537,11 @@ class HttpCli(object):
|
||||||
):
|
):
|
||||||
return self.tx_md(vn, abspath)
|
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:
|
if not add_og or not og_fn:
|
||||||
return self.tx_file(
|
return self.tx_file(
|
||||||
abspath, None if st.st_size or "nopipe" in vn.flags else vn.realpath
|
abspath, None if st.st_size or "nopipe" in vn.flags else vn.realpath
|
||||||
|
@ -6569,6 +6631,7 @@ class HttpCli(object):
|
||||||
"acct": self.uname,
|
"acct": self.uname,
|
||||||
"perms": perms,
|
"perms": perms,
|
||||||
}
|
}
|
||||||
|
# also see `js_htm` in authsrv.py
|
||||||
j2a = {
|
j2a = {
|
||||||
"cgv1": vn.js_htm,
|
"cgv1": vn.js_htm,
|
||||||
"cgv": cgv,
|
"cgv": cgv,
|
||||||
|
|
|
@ -168,12 +168,12 @@ def au_unpk(
|
||||||
znil = [x for x in znil if "cover" in x[0]] or znil
|
znil = [x for x in znil if "cover" in x[0]] or znil
|
||||||
znil = [x for x in znil if CBZ_01.search(x[0])] or znil
|
znil = [x for x in znil if CBZ_01.search(x[0])] or znil
|
||||||
t = "cbz: %d files, %d hits" % (nf, len(znil))
|
t = "cbz: %d files, %d hits" % (nf, len(znil))
|
||||||
|
if not znil:
|
||||||
|
raise Exception("no images inside cbz")
|
||||||
using = sorted(znil)[0][1].filename
|
using = sorted(znil)[0][1].filename
|
||||||
if znil:
|
if znil:
|
||||||
t += ", using " + using
|
t += ", using " + using
|
||||||
log(t)
|
log(t)
|
||||||
if not znil:
|
|
||||||
raise Exception("no images inside cbz")
|
|
||||||
fi = zf.open(using)
|
fi = zf.open(using)
|
||||||
|
|
||||||
elif pk == "epub":
|
elif pk == "epub":
|
||||||
|
|
|
@ -34,6 +34,8 @@ window.baguetteBox = (function () {
|
||||||
scrollTimer = 0,
|
scrollTimer = 0,
|
||||||
re_i = /^[^?]+\.(a?png|avif|bmp|gif|heif|jpe?g|jfif|svg|webp)(\?|$)/i,
|
re_i = /^[^?]+\.(a?png|avif|bmp|gif|heif|jpe?g|jfif|svg|webp)(\?|$)/i,
|
||||||
re_v = /^[^?]+\.(webm|mkv|mp4|m4v|mov)(\?|$)/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'],
|
anims = ['slideIn', 'fadeIn', 'none'],
|
||||||
data = {}, // all galleries
|
data = {}, // all galleries
|
||||||
imagesElements = [],
|
imagesElements = [],
|
||||||
|
@ -147,6 +149,8 @@ window.baguetteBox = (function () {
|
||||||
tagsNodeList = [galleryElement];
|
tagsNodeList = [galleryElement];
|
||||||
else
|
else
|
||||||
tagsNodeList = galleryElement.getElementsByTagName('a');
|
tagsNodeList = galleryElement.getElementsByTagName('a');
|
||||||
|
if (have_zls)
|
||||||
|
bindCbzClickListeners(tagsNodeList, userOptions);
|
||||||
|
|
||||||
tagsNodeList = [].filter.call(tagsNodeList, function (element) {
|
tagsNodeList = [].filter.call(tagsNodeList, function (element) {
|
||||||
if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1)
|
if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1)
|
||||||
|
@ -167,7 +171,7 @@ window.baguetteBox = (function () {
|
||||||
};
|
};
|
||||||
var imageItem = {
|
var imageItem = {
|
||||||
eventHandler: imageElementClickHandler,
|
eventHandler: imageElementClickHandler,
|
||||||
imageElement: imageElement
|
imageElement: imageElement,
|
||||||
};
|
};
|
||||||
bind(imageElement, 'click', imageElementClickHandler);
|
bind(imageElement, 'click', imageElementClickHandler);
|
||||||
gallery.push(imageItem);
|
gallery.push(imageItem);
|
||||||
|
@ -178,6 +182,86 @@ window.baguetteBox = (function () {
|
||||||
return [selectorData.galleries, options];
|
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(function () {
|
||||||
|
prepareOverlay(gallery, userOptions);
|
||||||
|
showOverlay(0);
|
||||||
|
}
|
||||||
|
).catch(function (reason) {
|
||||||
|
console.error("cbz-ded", reason);
|
||||||
|
var t;
|
||||||
|
try {
|
||||||
|
t = uricom_dec(cbzElement.href.split('/').pop());
|
||||||
|
} catch (ex) { }
|
||||||
|
|
||||||
|
var msg = "Could not browse " + (t ? t : 'archive');
|
||||||
|
try {
|
||||||
|
msg += "\n\n" + reason.message;
|
||||||
|
} catch (ex) { }
|
||||||
|
toast.err(20, msg, 'cbz-ded');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bind(cbzElement, "click", eventHandler);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillCbzGallery(gallery, cbzElement, eventHandler) {
|
||||||
|
if (gallery.length !== 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
var href = cbzElement.href;
|
||||||
|
var zlsHref = href + (href.indexOf("?") === -1 ? "?" : "&") + "zls";
|
||||||
|
return fetch(zlsHref)
|
||||||
|
.then(function (response) {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
} else {
|
||||||
|
throw new Error("Archive is invalid");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(function (fileList) {
|
||||||
|
var imagesList = fileList.map(function (file) {
|
||||||
|
return file["fn"];
|
||||||
|
}).filter(function (file) {
|
||||||
|
return file.indexOf(".") !== -1
|
||||||
|
&& cbz_pics.indexOf(file.split(".").pop()) !== -1;
|
||||||
|
}).sort();
|
||||||
|
|
||||||
|
if (imagesList.length === 0) {
|
||||||
|
throw new Error("Archive does not contain any images");
|
||||||
|
}
|
||||||
|
|
||||||
|
imagesList.forEach(function (imageName, index) {
|
||||||
|
var imageHref = href
|
||||||
|
+ (href.indexOf("?") === -1 ? "?" : "&")
|
||||||
|
+ "zget="
|
||||||
|
+ encodeURIComponent(imageName);
|
||||||
|
|
||||||
|
var galleryItem = {
|
||||||
|
href: imageHref,
|
||||||
|
imageElement: cbzElement,
|
||||||
|
eventHandler: eventHandler,
|
||||||
|
};
|
||||||
|
gallery.push(galleryItem);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function clearCachedData() {
|
function clearCachedData() {
|
||||||
for (var selector in data)
|
for (var selector in data)
|
||||||
if (data.hasOwnProperty(selector))
|
if (data.hasOwnProperty(selector))
|
||||||
|
@ -658,7 +742,7 @@ window.baguetteBox = (function () {
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
if (options.onChange && !url_ts)
|
if (options.onChange && !url_ts)
|
||||||
options.onChange(currentIndex, imagesElements.length);
|
options.onChange.call(currentGallery, currentIndex, imagesElements.length);
|
||||||
|
|
||||||
url_ts = null;
|
url_ts = null;
|
||||||
documentLastFocus = document.activeElement;
|
documentLastFocus = document.activeElement;
|
||||||
|
@ -786,11 +870,11 @@ window.baguetteBox = (function () {
|
||||||
imageContainer.removeChild(imageContainer.firstChild);
|
imageContainer.removeChild(imageContainer.firstChild);
|
||||||
|
|
||||||
var imageElement = galleryItem.imageElement,
|
var imageElement = galleryItem.imageElement,
|
||||||
imageSrc = imageElement.href,
|
imageSrc = galleryItem.href || imageElement.href,
|
||||||
is_vid = re_v.test(imageSrc),
|
is_vid = re_v.test(imageSrc),
|
||||||
thumbnailElement = imageElement.querySelector('img, video'),
|
thumbnailElement = imageElement.querySelector('img, video'),
|
||||||
imageCaption = typeof options.captions === 'function' ?
|
imageCaption = typeof options.captions === 'function' ?
|
||||||
options.captions.call(currentGallery, imageElement) :
|
options.captions.call(currentGallery, imageElement, index) :
|
||||||
imageElement.getAttribute('data-caption') || imageElement.title;
|
imageElement.getAttribute('data-caption') || imageElement.title;
|
||||||
|
|
||||||
imageSrc = addq(imageSrc, 'cache');
|
imageSrc = addq(imageSrc, 'cache');
|
||||||
|
@ -930,7 +1014,7 @@ window.baguetteBox = (function () {
|
||||||
unfig(index);
|
unfig(index);
|
||||||
|
|
||||||
if (options.onChange)
|
if (options.onChange)
|
||||||
options.onChange(currentIndex, imagesElements.length);
|
options.onChange.call(currentGallery, currentIndex, imagesElements.length);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17576,23 +17576,20 @@ var thegrid = (function () {
|
||||||
afterShow: function () {
|
afterShow: function () {
|
||||||
r.bbox_opts.refocus = true;
|
r.bbox_opts.refocus = true;
|
||||||
},
|
},
|
||||||
captions: function (g) {
|
captions: function (g, idx) {
|
||||||
var idx = -1,
|
var h = '' + g;
|
||||||
h = '' + g;
|
|
||||||
|
|
||||||
for (var a = 0; a < r.bbox.length; a++)
|
|
||||||
if (r.bbox[a].imageElement == g)
|
|
||||||
idx = a;
|
|
||||||
|
|
||||||
return '<a download href="' + h +
|
return '<a download href="' + h +
|
||||||
'">' + (idx + 1) + ' / ' + r.bbox.length + ' -- ' +
|
'">' + (idx + 1) + ' / ' + this.length + ' -- ' +
|
||||||
esc(uricom_dec(h.split('/').pop())) + '</a>';
|
esc(uricom_dec(h.split('/').pop())) + '</a>';
|
||||||
},
|
},
|
||||||
onChange: function (i) {
|
onChange: function (i, maxIdx) {
|
||||||
sethash('g' + r.bbox[i].imageElement.getAttribute('ref') + getsort());
|
if (this[i].imageElement) {
|
||||||
|
sethash('g' + this[i].imageElement.getAttribute('ref') + getsort());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
r.bbox = br[0][0];
|
r.bbox = true;
|
||||||
r.bbox_opts = br[1];
|
r.bbox_opts = br[1];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -200,6 +200,8 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
|
||||||
| GET | `?th` | get image/video at URL as thumbnail |
|
| GET | `?th` | get image/video at URL as thumbnail |
|
||||||
| GET | `?th=opus` | convert audio file to 128kbps opus |
|
| GET | `?th=opus` | convert audio file to 128kbps opus |
|
||||||
| GET | `?th=caf` | ...in the iOS-proprietary container |
|
| GET | `?th=caf` | ...in the iOS-proprietary container |
|
||||||
|
| GET | `?zls` | get listing of filepaths in zip file at URL |
|
||||||
|
| GET | `?zget=path` | get specific file from inside a zip file at URL |
|
||||||
|
|
||||||
| method | body | result |
|
| method | body | result |
|
||||||
|--|--|--|
|
|--|--|--|
|
||||||
|
|
Loading…
Reference in a new issue