From e45420646fb6a58148ae03d6d2465c57a47963c5 Mon Sep 17 00:00:00 2001 From: ed Date: Thu, 3 Oct 2024 23:14:06 +0000 Subject: [PATCH] share folders as qr-codes --- copyparty/httpcli.py | 36 +++++++++++++++++++++++++++++++---- copyparty/stolen/qrcodegen.py | 17 +++++++++++++++++ copyparty/web/browser.js | 13 +++++++------ copyparty/web/shares.css | 7 ++++--- copyparty/web/shares.html | 11 +++++++---- copyparty/web/shares.js | 26 +++++++++++++++++++++++-- copyparty/web/ui.css | 6 ++++++ scripts/tl.js | 2 +- 8 files changed, 98 insertions(+), 20 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index bc21e036..75503abc 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -37,6 +37,7 @@ from .__version__ import S_VERSION from .authsrv import VFS # typechk from .bos import bos from .star import StreamTar +from .stolen.qrcodegen import QrCode, qr2svg from .sutil import StreamArc, gfilter from .szip import StreamZip from .up2k import up2k_chunksize @@ -495,6 +496,9 @@ class HttpCli(object): self.vpath + "/" if self.trailing_slash and self.vpath else self.vpath ) + if "qr" in uparam: + return self.tx_qr() + if relchk(self.vpath) and (self.vpath != "*" or self.mode != "OPTIONS"): self.log("invalid relpath [{}]".format(self.vpath)) self.cbonk(self.conn.hsrv.gmal, self.req, "bad_vp", "invalid relpaths") @@ -3934,6 +3938,33 @@ class HttpCli(object): self.reply(ico, mime=mime, headers={"Last-Modified": lm}) return True + def tx_qr(self): + url = "%s://%s%s%s" % ( + "https" if self.is_https else "http", + self.host, + self.args.SRS, + self.vpaths, + ) + uhash = "" + uparams = [] + if self.ouparam: + for k, v in self.ouparam.items(): + if k == "qr": + continue + if k == "uhash": + uhash = v + continue + uparams.append(k if v is "" else "%s=%s" % (k, v)) + if uparams: + url += "?" + "&".join(uparams) + if uhash: + url += "#" + uhash + + self.log("qrcode(%r)" % (url,)) + ret = qr2svg(QrCode.encode_binary(url.encode("utf-8")), 2) + self.reply(ret.encode("utf-8"), mime="image/svg+xml") + return True + def tx_md(self, vn: VFS, fs_path: str) -> bool: logmsg = " %s @%s " % (self.req, self.uname) @@ -4505,9 +4536,6 @@ class HttpCli(object): 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()) ) @@ -4585,7 +4613,7 @@ class HttpCli(object): else: for zs in vps: if zs.endswith("/"): - t = "you cannot select more than one folder, or mix flies and folders in one selection" + t = "you cannot select more than one folder, or mix files and folders in one selection" raise Pebkac(400, t) vp = vps[0].rsplit("/", 1)[0] for zs in vps: diff --git a/copyparty/stolen/qrcodegen.py b/copyparty/stolen/qrcodegen.py index 3bee4dc9..38296359 100644 --- a/copyparty/stolen/qrcodegen.py +++ b/copyparty/stolen/qrcodegen.py @@ -594,3 +594,20 @@ def _get_bit(x: int, i: int) -> bool: class DataTooLongError(ValueError): pass + + +def qr2svg(qr: QrCode, border: int) -> str: + parts: list[str] = [] + for y in range(qr.size): + sy = border + y + for x in range(qr.size): + if qr.modules[y][x]: + parts.append("M%d,%dh1v1h-1z" % (border + x, sy)) + t = """\ + + + + + +""" + return t.format(qr.size + border * 2, " ".join(parts)) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index c35f750d..7cde5c63 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -345,7 +345,7 @@ var Ls = { "fs_tsrc": "the file or folder to share", "fs_ppwd": "optional password", "fs_w8": "creating share...", - "fs_ok": "
share-URL created
\npress Enter/OK to Clipboard\npress ESC/Cancel to Close\n\n", + "fs_ok": "press Enter/OK to Clipboard\npress ESC/Cancel to Close", "frt_dec": "may fix some cases of broken filenames\">url-decode", "frt_rst": "reset modified filenames back to the original ones\">↺ reset", @@ -913,7 +913,7 @@ var Ls = { "fs_tsrc": "fil/mappe som skal deles", "fs_ppwd": "frivillig passord", "fs_w8": "oppretter deling...", - "fs_ok": "
URL opprettet
\ntrykk Enter/OK for å kopiere linken (for CTRL-V)\ntrykk ESC/Avbryt for å bare bekrefte\n\n", + "fs_ok": "trykk Enter/OK for å kopiere linken (for CTRL-V)\ntrykk ESC/Avbryt for å bare bekrefte", "frt_dec": "kan korrigere visse ødelagte filnavn\">url-decode", "frt_rst": "nullstiller endringer (tilbake til de originale filnavnene)\">↺ reset", @@ -1481,7 +1481,7 @@ var Ls = { "fs_tsrc": "共享的文件或文件夹", "fs_ppwd": "密码可选", "fs_w8": "正在创建文件共享...", - "fs_ok": "
分享链接已创建
\n按 Enter/OK 复制到剪贴板\n按 ESC/Cancel 关闭\n\n", + "fs_ok": "按 Enter/OK 复制到剪贴板\n按 ESC/Cancel 关闭", "frt_dec": "可能修复一些损坏的文件名\">url-decode", "frt_rst": "将修改后的文件名重置为原始文件名\">↺ 重置", @@ -1788,7 +1788,7 @@ ebi('widget').innerHTML = ( ' ' + '' + '
' + - ' ' + + ' ' + ' ' + ' ' + ' ' + @@ -4596,11 +4596,12 @@ var fileman = (function () { return; } surl = surl.slice(15); - modal.confirm(L.fs_ok + esc(surl), function() { + var txt = esc(surl) + ''; + modal.confirm(txt + L.fs_ok, function() { cliptxt(surl, function () { toast.ok(2, L.clipped); }); - }); + }, null); } sh_apply.onclick = function () { diff --git a/copyparty/web/shares.css b/copyparty/web/shares.css index 9c48ac0a..4bf41a07 100644 --- a/copyparty/web/shares.css +++ b/copyparty/web/shares.css @@ -27,7 +27,7 @@ a { padding: .2em .6em; margin: 0 .3em; } -td a { +#wrap td a { margin: 0; } #w { @@ -53,12 +53,13 @@ th { position: sticky; background: #f7f7f7; } -td, th { +#wrap td, +#wrap th { padding: .3em .6em; text-align: left; white-space: nowrap; } -td+td+td+td+td+td+td+td { +#wrap td+td+td+td+td+td+td+td { font-family: var(--font-mono), monospace, monospace; } diff --git a/copyparty/web/shares.html b/copyparty/web/shares.html index 1ce0b1e8..caf1add8 100644 --- a/copyparty/web/shares.html +++ b/copyparty/web/shares.html @@ -22,8 +22,8 @@ min/hrs = time left - + @@ -37,10 +37,13 @@ {% for k, pw, vp, pr, st, un, t0, t1 in rows %} + - - - + + diff --git a/copyparty/web/shares.js b/copyparty/web/shares.js index 10e401f7..203042bd 100644 --- a/copyparty/web/shares.js +++ b/copyparty/web/shares.js @@ -12,7 +12,7 @@ function rm() { } function bump() { - var k = this.closest('tr').getElementsByTagName('a')[0].getAttribute('k'), + var k = this.closest('tr').getElementsByTagName('a')[2].getAttribute('k'), u = SR + shr + uricom_enc(k) + '?eshare=' + this.value, xhr = new XHR(); @@ -28,14 +28,36 @@ function cb() { document.location = '?shares'; } +function qr(e) { + ev(e); + var href = this.href, + pw = this.closest('tr').cells[2].textContent; + + if (pw.indexOf('yes') < 0) + return showqr(href); + + modal.prompt("if you want to bypass the password protection by\nembedding the password into the qr-code, then\ntype the password now, otherwise leave this empty", "", function (v) { + if (v) + href += "&pw=" + v; + showqr(href); + }); +} + +function showqr(href) { + var vhref = href.replace('?qr&', '?').replace('?qr', ''); + modal.alert(esc(vhref) + ''); +} + (function() { var tab = ebi('tab').tBodies[0], tr = Array.prototype.slice.call(tab.rows, 0); var buf = []; - for (var a = 0; a < tr.length; a++) + for (var a = 0; a < tr.length; a++) { + tr[a].cells[0].getElementsByTagName('a')[0].onclick = qr; for (var b = 7; b < 9; b++) buf.push(parseInt(tr[a].cells[b].innerHTML)); + } var ibuf = 0; for (var a = 0; a < tr.length; a++) diff --git a/copyparty/web/ui.css b/copyparty/web/ui.css index b7c25557..e1e77fcd 100644 --- a/copyparty/web/ui.css +++ b/copyparty/web/ui.css @@ -293,6 +293,12 @@ html.y #tth { #modalc a { color: #07b; } +#modalc .b64 { + display: block; + margin: .1em auto; + width: 60%; + height: 60%; +} #modalb { position: sticky; text-align: right; diff --git a/scripts/tl.js b/scripts/tl.js index cd0f7ee0..3a17a13e 100644 --- a/scripts/tl.js +++ b/scripts/tl.js @@ -426,7 +426,7 @@ var tl_browser = { "fs_tsrc": "the file or folder to share", "fs_ppwd": "optional password", "fs_w8": "creating share...", - "fs_ok": "
share-URL created
\npress Enter/OK to Clipboard\npress ESC/Cancel to Close\n\n", + "fs_ok": "press Enter/OK to Clipboard\npress ESC/Cancel to Close", "frt_dec": "may fix some cases of broken filenames\">url-decode", "frt_rst": "reset modified filenames back to the original ones\">↺ reset",
delete sharekeydelete pw source axs
+ qr + {{ k }} + delete{{ k }}{{ pw }}{{ vp|e }}{{ "yes" if pw else "--" }}/{{ vp|e }} {{ pr }} {{ st }} {{ un|e }}