diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 43ca2b36..7d6841e7 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -24,6 +24,7 @@ class HttpCli(object): """ def __init__(self, conn): + self.t0 = time.time() self.conn = conn self.s = conn.s self.sr = conn.sr @@ -86,7 +87,7 @@ class HttpCli(object): if "cookie" in self.headers: cookies = self.headers["cookie"].split(";") for k, v in [x.split("=", 1) for x in cookies]: - if k != "cppwd": + if k.strip() != "cppwd": continue v = unescape_cookie(v) @@ -307,10 +308,19 @@ class HttpCli(object): with open(path, "wb", 512 * 1024) as f: post_sz, _, sha_b64 = hashcopy(self.conn, reader, f) - self.log("wrote {}/{} bytes to {}".format(post_sz, remains, path)) + spd = self._spd(post_sz) + self.log("{} wrote {}/{} bytes to {}".format(spd, post_sz, remains, path)) self.reply("{}\n{}\n".format(post_sz, sha_b64).encode("utf-8")) return True + def _spd(self, nbytes, add=True): + if add: + self.conn.nbyte += nbytes + + spd1 = get_spd(nbytes, self.t0) + spd2 = get_spd(self.conn.nbyte, self.conn.t0) + return spd1 + " " + spd2 + def handle_post_multipart(self): self.parser = MultipartParser(self.log, self.sr, self.headers) self.parser.parse() @@ -450,7 +460,9 @@ class HttpCli(object): except: self.log("failed to utime ({}, {})".format(path, times)) - self.loud_reply("thank") + spd = self._spd(post_sz) + self.log("{} thank".format(spd)) + self.reply("thank") return True def handle_login(self): @@ -463,7 +475,7 @@ class HttpCli(object): msg = "naw dude" pwd = "x" # nosec - h = {"Set-Cookie": "cppwd={}; Path=/".format(pwd)} + h = {"Set-Cookie": "cppwd={}; Path=/; SameSite=Lax".format(pwd)} html = self.conn.tpl_msg.render(h1=msg, h2='ack', redir="/") self.reply(html.encode("utf-8"), headers=h) return True @@ -575,6 +587,7 @@ class HttpCli(object): raise Pebkac(400, "empty files in post") files.append([sz, sha512_hex]) + self.conn.nbyte += sz except Pebkac: if fn != os.devnull: @@ -602,7 +615,9 @@ class HttpCli(object): # truncated SHA-512 prevents length extension attacks; # using SHA-512/224, optionally SHA-512/256 = :64 - self.log(msg) + vspd = self._spd(sz_total, False) + self.log("{} {}".format(vspd, msg)) + if not nullwrite: # TODO this is bad log_fn = "up.{:.6f}.txt".format(t0) @@ -885,6 +900,7 @@ class HttpCli(object): self.log(logmsg) return True + ret = True with open_func(*open_args) as f: remains = upper - lower f.seek(lower) @@ -897,17 +913,17 @@ class HttpCli(object): if remains < len(buf): buf = buf[:remains] - remains -= len(buf) - try: self.s.sendall(buf) + remains -= len(buf) except: logmsg += " \033[31m" + str(upper - remains) + "\033[0m" - self.log(logmsg) - return False + ret = False + break - self.log(logmsg) - return True + spd = self._spd((upper - lower) - remains) + self.log("{}, {}".format(logmsg, spd)) + return ret def tx_md(self, fs_path): logmsg = "{:4} {} ".format("", self.req) diff --git a/copyparty/httpconn.py b/copyparty/httpconn.py index 8be3c49a..79be834e 100644 --- a/copyparty/httpconn.py +++ b/copyparty/httpconn.py @@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals import os import sys import ssl +import time import socket try: @@ -41,6 +42,8 @@ class HttpConn(object): self.auth = hsrv.auth self.cert_path = hsrv.cert_path + self.t0 = time.time() + self.nbyte = 0 self.workload = 0 self.log_func = hsrv.log self.log_src = "{} \033[36m{}".format(addr[0], addr[1]).ljust(26) diff --git a/copyparty/util.py b/copyparty/util.py index a3a4ae16..b1cab228 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -3,6 +3,7 @@ from __future__ import print_function, unicode_literals import re import sys +import time import base64 import struct import hashlib @@ -349,6 +350,16 @@ def humansize(sz, terse=False): return ret.replace("iB", "").replace(" ", "") +def get_spd(nbyte, t0, t=None): + if t is None: + t = time.time() + + bps = nbyte / ((t - t0) + 0.001) + s1 = humansize(nbyte).replace(" ", "\033[33m").replace("iB", "") + s2 = humansize(bps).replace(" ", "\033[35m").replace("iB", "") + return "{} \033[0m{}/s\033[0m".format(s1, s2) + + def undot(path): ret = [] for node in path.split("/"): diff --git a/copyparty/web/md.css b/copyparty/web/md.css index e32b45c3..b6c2837a 100644 --- a/copyparty/web/md.css +++ b/copyparty/web/md.css @@ -109,8 +109,12 @@ h2 a, h4 a, h6 a { #mp ol>li { margin: .7em 0; } +strong { + color: #000; +} p>em, -li>em { +li>em, +td>em { color: #c50; padding: .1em; border-bottom: .1em solid #bbb; @@ -289,6 +293,32 @@ blink { text-decoration: underline; border: none; } + #mh a:hover { + color: #000; + background: #ddd; + } + #toolsbox { + overflow: hidden; + display: inline-block; + background: #eee; + height: 1.5em; + padding: 0 .2em; + margin: 0 .2em; + position: absolute; + } + #toolsbox.open { + height: auto; + overflow: visible; + background: #eee; + box-shadow: 0 .2em .2em #ccc; + padding-bottom: .2em; + } + #toolsbox a { + display: block; + } + #toolsbox a+a { + text-decoration: none; + } @@ -332,8 +362,12 @@ blink { html.dark #m>ol { border-color: #555; } + html.dark strong { + color: #fff; + } html.dark p>em, - html.dark li>em { + html.dark li>em, + html.dark td>em { color: #f94; border-color: #666; } @@ -371,6 +405,17 @@ blink { color: #ccc; background: none; } + html.dark #mh a:hover { + background: #333; + color: #fff; + } + html.dark #toolsbox { + background: #222; + } + html.dark #toolsbox.open { + box-shadow: 0 .2em .2em #069; + border-radius: 0 0 .4em .4em; + } } @media screen and (min-width: 66em) { @@ -541,7 +586,8 @@ blink { color: #240; } html.dark p>em, - html.dark li>em { + html.dark li>em, + html.dark td>em { color: #940; } } diff --git a/copyparty/web/md.html b/copyparty/web/md.html index 1e93cbc9..b9545cad 100644 --- a/copyparty/web/md.html +++ b/copyparty/web/md.html @@ -17,7 +17,14 @@ save sbs editor - help +
+ tools + prettify table (ctrl-k) + non-ascii: iterate (ctrl-u) + non-ascii: markup + non-ascii: whitelist + help +
{%- else %} edit (basic) edit (fancy) @@ -46,6 +53,9 @@ write markdown (most html is πŸ™† too) ## hotkey list * `Ctrl-S` to save +* `Ctrl-E` to toggle mode +* `Ctrl-K` to prettyprint a table +* `Ctrl-U` to iterate non-ascii chars * `Ctrl-H` / `Ctrl-Shift-H` to create a header * `TAB` / `Shift-TAB` to indent/dedent a selection diff --git a/copyparty/web/md2.js b/copyparty/web/md2.js index 318ba9fd..6b74c85c 100644 --- a/copyparty/web/md2.js +++ b/copyparty/web/md2.js @@ -2,10 +2,16 @@ var server_md = dom_src.value; +// the non-ascii whitelist +var esc_uni_whitelist = '\\n\\t\\x20-\\x7eΓ†Γ˜Γ…Γ¦ΓΈΓ₯'; +var js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\''); + + // dom nodes var dom_swrap = document.getElementById('mtw'); var dom_sbs = document.getElementById('sbs'); var dom_nsbs = document.getElementById('nsbs'); +var dom_tbox = document.getElementById('toolsbox'); var dom_ref = (function () { var d = document.createElement('div'); d.setAttribute('id', 'mtr'); @@ -339,7 +345,7 @@ function savechk_cb() { server_md = this.txt; draw_md(); toast('font-size:6em;font-family:serif;color:#cf6;width:4em;', - 'OKβœ”οΈ' + this.ntry + ''); + 'OKβœ”οΈ' + this.ntry + ''); } function toast(style, msg) { @@ -427,6 +433,9 @@ function setsel(s) { dom_src.value = [s.pre, s.sel, s.post].join(''); dom_src.setSelectionRange(s.car, s.cdr, dom_src.selectionDirection); dom_src.oninput(); + // support chrome: + dom_src.blur(); + dom_src.focus(); } @@ -500,7 +509,8 @@ function md_newline() { var s = linebounds(true), ln = s.md.substring(s.n1, s.n2), m1 = /^( *)([0-9]+)(\. +)/.exec(ln), - m2 = /^[ \t>+-]*(\* )?/.exec(ln); + m2 = /^[ \t>+-]*(\* )?/.exec(ln), + drop = dom_src.selectionEnd - dom_src.selectionStart; var pre = m2[0]; if (m1 !== null) @@ -512,7 +522,7 @@ function md_newline() { s.pre = s.md.substring(0, s.car) + '\n' + pre; s.sel = ''; - s.post = s.md.substring(s.car); + s.post = s.md.substring(s.car + drop); s.car = s.cdr = s.pre.length; setsel(s); return false; @@ -522,11 +532,17 @@ function md_newline() { // backspace function md_backspace() { var s = linebounds(true), - ln = s.md.substring(s.n1, s.n2), - m = /^[ \t>+-]*(\* )?([0-9]+\. +)?/.exec(ln); + o0 = dom_src.selectionStart, + left = s.md.slice(s.n1, o0), + m = /^[ \t>+-]*(\* )?([0-9]+\. +)?/.exec(left); + // if car is in whitespace area, do nothing + if (/^\s*$/.test(left)) + return true; + + // same if line is all-whitespace or non-markup var v = m[0].replace(/[^ ]/g, " "); - if (v === m[0] || v.length !== ln.length) + if (v === m[0] || v.length !== left.length) return true; s.pre = s.md.substring(0, s.n1) + v; @@ -540,8 +556,8 @@ function md_backspace() { // paragraph jump function md_p_jump(down) { - var ofs = dom_src.selectionStart; - var txt = dom_src.value; + var txt = dom_src.value, + ofs = dom_src.selectionStart; if (down) { while (txt[ofs] == '\n' && --ofs > 0); @@ -562,6 +578,202 @@ function md_p_jump(down) { } +function reLastIndexOf(txt, ptn, end) { + var ofs = (typeof end !== 'undefined') ? end : txt.length; + end = ofs; + while (ofs >= 0) { + var sub = txt.slice(ofs, end); + if (ptn.test(sub)) + return ofs; + + ofs--; + } + return -1; +} + + +// table formatter +function fmt_table(e) { + if (e) e.preventDefault(); + //dom_tbox.setAttribute('class', ''); + + var txt = dom_src.value, + ofs = dom_src.selectionStart, + //o0 = txt.lastIndexOf('\n\n', ofs), + //o1 = txt.indexOf('\n\n', ofs); + o0 = reLastIndexOf(txt, /\n\s*\n/m, ofs), + o1 = txt.slice(ofs).search(/\n\s*\n/m); + // note \s contains \n but its fine + + if (o0 < 0) + o0 = 0; + else { + // seek past the hit + var m = /\n\s*\n/m.exec(txt.slice(o0)); + o0 += m[0].length; + } + + o1 = o1 < 0 ? txt.length : o1 + ofs; + + var err = 'cannot format table due to ', + tab = txt.slice(o0, o1).split(/\s*\n/), + re_ind = /^\s*/, + ind = tab[1].match(re_ind)[0], + r0_ind = tab[0].slice(0, ind.length), + lpipe = tab[1].indexOf('|') < tab[1].indexOf('-'), + rpipe = tab[1].lastIndexOf('|') > tab[1].lastIndexOf('-'), + re_lpipe = lpipe ? /^\s*\|\s*/ : /^\s*/, + re_rpipe = rpipe ? /\s*\|\s*$/ : /\s*$/; + + for (var a = 0; a < tab.length; a++) { + var ind2 = tab[a].match(re_ind)[0]; + if (ind != ind2 && a > 0) // the table can be a list entry or something, ignore [0] + return alert(err + 'indentation mismatch on row 2 and ' + (a + 1) + ',\n' + tab[a]); + + var t = tab[a].slice(ind.length); + t = t.replace(re_lpipe, ""); + t = t.replace(re_rpipe, ""); + tab[a] = t.split(/\s*\|\s*/g); + + if (a == 0) + ncols = tab[a].length; + + if (ncols != tab[a].length) + return alert(err + 'num.columns mismatch on row 2 and ' + (a + 1) + '; ' + ncols + ' != ' + tab[a].length); + } + + var re_align = /^ *(:?)-+(:?) *$/; + var align = []; + for (var col = 0; col < tab[1].length; col++) { + var m = tab[1][col].match(re_align); + if (!m) + return alert(err + 'invalid column specification, row 2, col ' + (col + 1) + ', [' + tab[1][col] + ']'); + + if (m[2]) { + if (m[1]) + align.push('c'); + else + align.push('r'); + } + else + align.push('l'); + } + + var pad = []; + var tmax = 0; + for (var col = 0; col < ncols; col++) { + var max = 0; + for (var row = 0; row < tab.length; row++) + max = Math.max(max, tab[row][col].length); + + var s = ''; + for (var n = 0; n < max; n++) + s += ' '; + + pad.push(s); + tmax = Math.max(max, tmax); + } + + var dashes = ''; + for (var a = 0; a < tmax; a++) + dashes += '-'; + + var ret = []; + for (var row = 0; row < tab.length; row++) { + var ln = []; + for (var col = 0; col < tab[row].length; col++) { + var p = pad[col]; + var s = tab[row][col]; + + if (align[col] == 'l') { + s = (s + p).slice(0, p.length); + } + else if (align[col] == 'r') { + s = (p + s).slice(-p.length); + } + else { + var pt = p.length - s.length; + var pl = p.slice(0, Math.floor(pt / 2)); + var pr = p.slice(0, pt - pl.length); + s = pl + s + pr; + } + + if (row == 1) { + if (align[col] == 'l') + s = dashes.slice(0, p.length); + else if (align[col] == 'r') + s = dashes.slice(0, p.length - 1) + ':'; + else + s = ':' + dashes.slice(0, p.length - 2) + ':'; + } + ln.push(s); + } + ret.push(ind + '| ' + ln.join(' | ') + ' |'); + } + + // restore any markup in the row0 gutter + ret[0] = r0_ind + ret[0].slice(ind.length); + + ret = { + "pre": txt.slice(0, o0), + "sel": ret.join('\n'), + "post": txt.slice(o1), + "car": o0, + "cdr": o0 + }; + setsel(ret); +} + + +// show unicode +function mark_uni(e) { + if (e) e.preventDefault(); + dom_tbox.setAttribute('class', ''); + + var txt = dom_src.value, + ptn = new RegExp('([^' + js_uni_whitelist + ']+)', 'g'); + + mod = txt.replace(/\r/g, "").replace(ptn, "\u2588\u2770$1\u2771"); + + if (txt == mod) { + alert('no results; no modifications were made'); + return; + } + dom_src.value = mod; +} + + +// iterate unicode +function iter_uni(e) { + if (e) e.preventDefault(); + + var txt = dom_src.value, + ofs = dom_src.selectionDirection == "forward" ? dom_src.selectionEnd : dom_src.selectionStart, + re = new RegExp('([^' + js_uni_whitelist + ']+)'), + m = re.exec(txt.slice(ofs)); + + if (!m) { + alert('no more hits from cursor onwards'); + return; + } + ofs += m.index; + + dom_src.setSelectionRange(ofs, ofs + m[0].length, "forward"); + dom_src.oninput(); + // support chrome: + dom_src.blur(); + dom_src.focus(); +} + + +// configure whitelist +function cfg_uni(e) { + if (e) e.preventDefault(); + esc_uni_whitelist = prompt("unicode whitelist", esc_uni_whitelist); + js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\''); +} + + // hotkeys / toolbar (function () { function keydown(ev) { @@ -609,6 +821,19 @@ function md_p_jump(down) { if (!ctrl && !ev.shiftKey && kc == 8) { return md_backspace(); } + if (ctrl && (ev.code == "KeyK")) { + fmt_table(); + return false; + } + if (ctrl && (ev.code == "KeyU")) { + iter_uni(); + return false; + } + if (ctrl && (ev.code == "KeyE")) { + dom_nsbs.click(); + //fmt_table(); + return false; + } var up = ev.code == "ArrowUp" || kc == 38; var dn = ev.code == "ArrowDown" || kc == 40; if (ctrl && (up || dn)) { @@ -622,8 +847,17 @@ function md_p_jump(down) { })(); +document.getElementById('tools').onclick = function (e) { + if (e) e.preventDefault(); + var is_open = dom_tbox.getAttribute('class') != 'open'; + dom_tbox.setAttribute('class', is_open ? 'open' : ''); +}; + + document.getElementById('help').onclick = function (e) { if (e) e.preventDefault(); + dom_tbox.setAttribute('class', ''); + var dom = document.getElementById('helpbox'); var dtxt = dom.getElementsByTagName('textarea'); if (dtxt.length > 0) { @@ -638,6 +872,12 @@ document.getElementById('help').onclick = function (e) { }; +document.getElementById('fmt_table').onclick = fmt_table; +document.getElementById('mark_uni').onclick = mark_uni; +document.getElementById('iter_uni').onclick = iter_uni; +document.getElementById('cfg_uni').onclick = cfg_uni; + + // blame steen action_stack = (function () { var hist = { diff --git a/copyparty/web/mde.css b/copyparty/web/mde.css index 6e73c9cd..ac59593a 100644 --- a/copyparty/web/mde.css +++ b/copyparty/web/mde.css @@ -160,8 +160,12 @@ h2 { .mdo ol>li { margin: .7em 0; } +strong { + color: #000; +} p>em, -li>em { +li>em, +td>em { color: #c50; padding: .1em; border-bottom: .1em solid #bbb; @@ -253,8 +257,12 @@ html.dark .mdo>ul, html.dark .mdo>ol { border-color: #555; } +html.dark strong { + color: #fff; +} html.dark p>em, -html.dark li>em { +html.dark li>em, +html.dark td>em { color: #f94; border-color: #666; } diff --git a/copyparty/web/mde.js b/copyparty/web/mde.js index 2a8d7906..fd16cba0 100644 --- a/copyparty/web/mde.js +++ b/copyparty/web/mde.js @@ -121,7 +121,7 @@ function save(mde) { fd.append("lastmod", (force ? -1 : last_modified)); fd.append("body", txt); - var url = (document.location + '').split('?')[0] + '?raw'; + var url = (document.location + '').split('?')[0]; var xhr = new XMLHttpRequest(); xhr.open('POST', url, true); xhr.responseType = 'text'; diff --git a/scripts/sfx.sh b/scripts/sfx.sh index c7f7bbde..acaaa0ed 100644 --- a/scripts/sfx.sh +++ b/scripts/sfx.sh @@ -32,8 +32,12 @@ dir="$( # detect available pythons (IFS=:; for d in $PATH; do - printf '%s\n' "$d"/python* "$d"/pypy* | tac; -done) | grep -E '(python|pypy)[0-9\.-]*$' > $dir/pys || true + printf '%s\n' "$d"/python* "$d"/pypy*; +done) | +(sed -E 's/(.*\/[^/0-9]+)([0-9]?[^/]*)$/\2 \1/' || cat) | +(sort -nr || cat) | +(sed -E 's/([^ ]*) (.*)/\2\1/' || cat) | +grep -E '/(python|pypy)[0-9\.-]*$' >$dir/pys || true # see if we made a choice before [ -z "$pybin" ] && pybin="$(cat $dir/py 2>/dev/null || true)" diff --git a/srv/test.md b/srv/test.md index 3025cc2d..8572fc48 100644 --- a/srv/test.md +++ b/srv/test.md @@ -1,5 +1,16 @@ ### hello world +* qwe +* asd + * zxc + * 573 + * one + * two + + * ||| + |--|--| + |listed|table| + ``` [72....................................................................] [80............................................................................] @@ -21,6 +32,8 @@ l[i]=1I;(){}o0O var foo = "$(`bar`)"; a's'd ``` +πŸ”πŸŒ½.πŸ“•.πŸ™πŸ”Ž + [](#s1) [s1](#s1) [#s1](#s1) @@ -121,6 +134,11 @@ a newline toplevel | a table | on the right | | second row | foo bar | +|| +--|:-:|-: +a table | big text in this | aaakbfddd +second row | centred | bbb + * list entry * [x] yes * [ ] no