diff --git a/bin/hooks/README.md b/bin/hooks/README.md index 82ef3d7e..31ee3bff 100644 --- a/bin/hooks/README.md +++ b/bin/hooks/README.md @@ -30,4 +30,5 @@ these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every sin # on message * [wget.py](wget.py) lets you download files by POSTing URLs to copyparty * [qbittorrent-magnet.py](qbittorrent-magnet.py) starts downloading a torrent if you post a magnet url +* [usb-eject.py](usb-eject.py) adds web-UI buttons to safe-remove usb flashdrives shared through copyparty * [msg-log.py](msg-log.py) is a guestbook; logs messages to a doc in the same folder diff --git a/bin/hooks/usb-eject.js b/bin/hooks/usb-eject.js new file mode 100644 index 00000000..ad60e527 --- /dev/null +++ b/bin/hooks/usb-eject.js @@ -0,0 +1,54 @@ +// see usb-eject.py for usage + +function usbclick() { + QS('#treeul a[href="/usb/"]').click(); +} + +function eject_cb() { + var t = this.responseText; + if (t.indexOf('can be safely unplugged') < 0 && t.indexOf('Device can be removed') < 0) + return toast.err(30, 'usb eject failed:\n\n' + t); + + toast.ok(5, esc(t.replace(/ - /g, '\n\n'))); + usbclick(); setTimeout(usbclick, 10); +}; + +function add_eject_2(a) { + var aw = a.getAttribute('href').split(/\//g); + if (aw.length != 4 || aw[3]) + return; + + var v = aw[2], + k = 'umount_' + v; + + qsr('#' + k); + a.appendChild(mknod('span', k, '⏏'), a); + + var o = ebi(k); + o.style.cssText = 'position:absolute; right:1em; margin-top:-.2em; font-size:1.3em'; + o.onclick = function (e) { + ev(e); + var xhr = new XHR(); + xhr.open('POST', get_evpath(), true); + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8'); + xhr.send('msg=' + uricom_enc(':usb-eject:' + v + ':')); + xhr.onload = xhr.onerror = eject_cb; + toast.inf(10, "ejecting " + v + "..."); + }; +}; + +function add_eject() { + for (var a of QSA('#treeul a[href^="/usb/"]')) + add_eject_2(a); +}; + +(function() { + var f0 = treectl.rendertree; + treectl.rendertree = function (res, ts, top0, dst, rst) { + var ret = f0(res, ts, top0, dst, rst); + add_eject(); + return ret; + }; +})(); + +setTimeout(add_eject, 50); diff --git a/bin/hooks/usb-eject.py b/bin/hooks/usb-eject.py new file mode 100644 index 00000000..8ccb3b3c --- /dev/null +++ b/bin/hooks/usb-eject.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +import os +import stat +import subprocess as sp +import sys + + +""" +if you've found yourself using copyparty to serve flashdrives on a LAN +and your only wish is that the web-UI had a button to unmount / safely +remove those flashdrives, then boy howdy are you in the right place :D + +put usb-eject.js in the webroot (or somewhere else http-accessible) +then run copyparty with these args: + + -v /run/media/ed:/usb:A:c,hist=/tmp/junk + --xm=c1,bin/hooks/usb-eject.py + --js-browser=/usb-eject.js + +which does the following respectively, + + * share all of /run/media/ed as /usb with admin for everyone + and put the histpath somewhere it won't cause trouble + * run the usb-eject hook with stdout redirect to the web-ui + * add the complementary usb-eject.js to the browser + +""" + + +def main(): + try: + label = sys.argv[1].split(":usb-eject:")[1].split(":")[0] + mp = "/run/media/ed/" + label + # print("ejecting [%s]... " % (mp,), end="") + mp = os.path.abspath(os.path.realpath(mp.encode("utf-8"))) + st = os.lstat(mp) + if not stat.S_ISDIR(st.st_mode): + raise Exception("not a regular directory") + + cmd = [b"gio", b"mount", b"-e", mp] + print(sp.check_output(cmd).decode("utf-8", "replace").strip()) + + except Exception as ex: + print("unmount failed: %r" % (ex,)) + + +if __name__ == "__main__": + main() diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index f981236f..89e38338 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -7460,12 +7460,12 @@ var treectl = (function () { xhr.rst = rst; xhr.ts = Date.now(); xhr.open('GET', addq(dst, 'tree=' + top + (r.dots ? '&dots' : '') + k), true); - xhr.onload = xhr.onerror = recvtree; + xhr.onload = xhr.onerror = r.recvtree; xhr.send(); enspin('#tree'); } - function recvtree() { + r.recvtree = function () { if (!xhrchk(this, L.tl_xe1, L.tl_xe2)) return; @@ -7475,10 +7475,10 @@ var treectl = (function () { catch (ex) { return toast.err(30, "bad ?tree reply;\nexpected json, got this:\n\n" + esc(this.responseText + '')); } - rendertree(res, this.ts, this.top, this.dst, this.rst); - } + r.rendertree(res, this.ts, this.top, this.dst, this.rst); + }; - function rendertree(res, ts, top0, dst, rst) { + r.rendertree = function (res, ts, top0, dst, rst) { var cur = ebi('treeul').getAttribute('ts'); if (cur && parseInt(cur) > ts + 20 && QS('#treeul>li>a+a')) { console.log("reject tree; " + cur + " / " + (ts - cur)); @@ -7532,7 +7532,7 @@ var treectl = (function () { console.log("dir_cb failed", ex); } } - } + }; function reload_tree() { var cdir = r.nextdir || get_vpath(), @@ -7754,7 +7754,7 @@ var treectl = (function () { dirs.push(dn); } - rendertree({ "a": dirs }, this.ts, ".", get_evpath() + (dk ? '?k=' + dk : '')); + r.rendertree({ "a": dirs }, this.ts, ".", get_evpath() + (dk ? '?k=' + dk : '')); } r.gentab(this.top, res);