From 11ad8a5c3b8905eee6cfa30f671897dd0aa2cbaf Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 22 Jun 2019 12:54:24 +0000 Subject: [PATCH] add up2k client code --- copyparty/web/browser.js | 32 +-- copyparty/web/splash.html | 2 +- copyparty/web/up2k.js | 545 +++++++++++++++++++++++++++++++++++++- copyparty/web/upload.css | 16 +- copyparty/web/upload.html | 15 +- 5 files changed, 566 insertions(+), 44 deletions(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 5b703262..95642118 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -1,3 +1,5 @@ +"use strict"; + // error handler for mobile devices function hcroak(msg) { document.body.innerHTML = msg; @@ -61,12 +63,12 @@ function import_js(url, cb) { } -function ebi(id) { +function o(id) { return document.getElementById(id); } function dbg(msg) { - ebi('path').innerHTML = msg; + o('path').innerHTML = msg; } @@ -99,7 +101,7 @@ var mp = (function () { } for (var a = 0, aa = tracks.length; a < aa; a++) - ebi('trk' + a).onclick = ev_play; + o('trk' + a).onclick = ev_play; ret.vol = localStorage.getItem('vol'); if (ret.vol !== null) @@ -156,7 +158,7 @@ var widget = (function () { ret.paused = function (paused) { if (was_paused != paused) { was_paused = paused; - ebi('bplay').innerHTML = paused ? '▶' : '⏸'; + o('bplay').innerHTML = paused ? '▶' : '⏸'; } }; var click_handler = function (e) { @@ -180,8 +182,8 @@ var widget = (function () { // buffer/position bar var pbar = (function () { var r = {}; - r.bcan = ebi('barbuf'); - r.pcan = ebi('barpos'); + r.bcan = o('barbuf'); + r.pcan = o('barpos'); r.bctx = r.bcan.getContext('2d'); r.pctx = r.pcan.getContext('2d'); @@ -246,7 +248,7 @@ var pbar = (function () { // volume bar var vbar = (function () { var r = {}; - r.can = ebi('pvol'); + r.can = o('pvol'); r.ctx = r.can.getContext('2d'); var bctx = r.ctx; @@ -343,7 +345,7 @@ var vbar = (function () { else play(0); }; - ebi('bplay').onclick = function (e) { + o('bplay').onclick = function (e) { e.preventDefault(); if (mp.au) { if (mp.au.paused) @@ -354,15 +356,15 @@ var vbar = (function () { else play(0); }; - ebi('bprev').onclick = function (e) { + o('bprev').onclick = function (e) { e.preventDefault(); bskip(-1); }; - ebi('bnext').onclick = function (e) { + o('bnext').onclick = function (e) { e.preventDefault(); bskip(1); }; - ebi('barpos').onclick = function (e) { + o('barpos').onclick = function (e) { if (!mp.au) { //dbg((new Date()).getTime()); return play(0); @@ -437,7 +439,7 @@ function ev_play(e) { function setclass(id, clas) { - ebi(id).setAttribute('class', clas); + o(id).setAttribute('class', clas); } @@ -569,7 +571,7 @@ function show_modal(html) { // hide fullscreen message function unblocked() { - var dom = ebi('blocked'); + var dom = o('blocked'); if (dom) dom.remove(); } @@ -585,8 +587,8 @@ function autoplay_blocked(tid) { Cancel

(show file list)
`); - var go = ebi('blk_go'); - var na = ebi('blk_na'); + var go = o('blk_go'); + var na = o('blk_na'); var fn = mp.tracks[mp.au.tid].split(/\//).pop(); fn = decodeURIComponent(fn.replace(/\+/g, ' ')); diff --git a/copyparty/web/splash.html b/copyparty/web/splash.html index 58f51176..b03929b6 100644 --- a/copyparty/web/splash.html +++ b/copyparty/web/splash.html @@ -43,7 +43,7 @@
- + \ No newline at end of file diff --git a/copyparty/web/up2k.js b/copyparty/web/up2k.js index 2867089b..8229df5f 100644 --- a/copyparty/web/up2k.js +++ b/copyparty/web/up2k.js @@ -1,3 +1,5 @@ +"use strict"; + // error handler for mobile devices function hcroak(msg) { document.body.innerHTML = msg; @@ -35,21 +37,538 @@ window.onerror = function (msg, url, lineNo, columnNo, error) { hcroak(html.join('\n')); }; -function ebi(id) { +function o(id) { return document.getElementById(id); } -ebi('up2k').style.display = 'block'; -ebi('bup').style.display = 'none'; +(function () { + // hide basic uploader + o('up2k').style.display = 'block'; + o('bup').style.display = 'none'; -ebi('u2tgl').onclick = function (e) { - e.preventDefault(); - ebi('u2tgl').style.display = 'none'; - ebi('u2body').style.display = 'block'; -} + // upload ui hidden by default, clicking the header shows it + o('u2tgl').onclick = function (e) { + e.preventDefault(); + o('u2tgl').style.display = 'none'; + o('u2body').style.display = 'block'; + }; -ebi('u2nope').onclick = function (e) { - e.preventDefault(); - ebi('up2k').style.display = 'none'; - ebi('bup').style.display = 'block'; -} + // shows or clears an error message in the basic uploader ui + function setmsg(msg) { + if (msg !== undefined) { + o('u2err').setAttribute('class', 'err'); + o('u2err').innerHTML = msg; + } + else { + o('u2err').setAttribute('class', ''); + o('u2err').innerHTML = ''; + } + } + + // switches to the basic uploader with msg as error message + function un2k(msg) { + o('up2k').style.display = 'none'; + o('bup').style.display = 'block'; + setmsg(msg); + } + + // handle user intent to use the basic uploader instead + o('u2nope').onclick = function (e) { + e.preventDefault(); + un2k(); + }; + + if (!String.prototype.format) { + String.prototype.format = function () { + var args = arguments; + return this.replace(/{(\d+)}/g, function (match, number) { + return typeof args[number] != 'undefined' ? + args[number] : match; + }); + }; + } + + function cfg(name) { + var val = localStorage.getItem(name); + if (val === null) + return parseInt(o(name).value); + + o(name).value = val + return val; + } + + var parallel_uploads = cfg('nthread'); + var chunksize_mb = cfg('chunksz'); + var chunksize = chunksize_mb * 1024 * 1024; + + var col_hashing = '#0099ff'; //'#d7d7d7'; + //var col_hashed = '#e8a6df'; //'#decb7f'; + //var col_hashed = '#0099ff'; + var col_hashed = '#eeeeee'; + var col_uploading = '#ffcc44'; + var col_uploaded = '#00cc00'; + var fdom_ctr = 0; + var st = { + "files": [], + "todo": { + "hash": [], + "handshake": [], + "upload": [] + }, + "busy": { + "hash": [], + "handshake": [], + "upload": [] + } + }; + + var bobslice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice; + var have_crypto = window.crypto && crypto.subtle && crypto.subtle.digest; + + if (!bobslice || !window.FileReader || !window.FileList || !have_crypto) + return un2k("this is the basic uploader; the good one needs at least
chrome 37 // firefox 34 // edge 12 // opera 24 // safari 7"); + + function nav() { + o('file' + fdom_ctr).click(); + } + o('u2btn').addEventListener('click', nav, false); + + function ondrag(ev) { + ev.stopPropagation(); + ev.preventDefault(); + ev.dataTransfer.dropEffect = 'copy'; + ev.dataTransfer.effectAllowed = 'copy'; + } + o('u2btn').addEventListener('dragover', ondrag, false); + o('u2btn').addEventListener('dragenter', ondrag, false); + + function gotfile(ev) { + ev.stopPropagation(); + ev.preventDefault(); + + var files = ev.dataTransfer ? + ev.dataTransfer.files : ev.target.files; + + if (files.length == 0) + return alert('no files selected??'); + + more_one_file(); + for (var a = 0; a < files.length; a++) { + var fobj = files[a]; + var entry = { + "n": parseInt(st.files.length.toString()), + "fobj": fobj, + "name": fobj.name, + "size": fobj.size, + "hash": [] + }; + + var skip = false; + for (var b = 0; b < st.files.length; b++) + if (entry.name == st.files[b].name && + entry.size == st.files[b].size) + skip = true; + + if (skip) + continue; + + var tr = document.createElement('tr'); + tr.innerHTML = 'hashing'.format(st.files.length); + tr.getElementsByTagName('td')[0].textContent = entry.name; + o('u2tab').appendChild(tr); + + st.files.push(entry); + st.todo.hash.push(entry); + } + } + o('u2btn').addEventListener('drop', gotfile, false); + + function more_one_file() { + fdom_ctr++; + var elm = document.createElement('div') + elm.innerHTML = ''.format(fdom_ctr); + o('u2form').appendChild(elm); + o('file' + fdom_ctr).addEventListener('change', gotfile, false); + } + more_one_file(); + + ///// + //// + /// actuator + // + + function boss() { + if (st.todo.hash.length > 0 && + st.busy.hash.length == 0) + exec_hash(); + + if (st.todo.handshake.length > 0 && + st.busy.handshake.length == 0 && + st.busy.upload.length < parallel_uploads) + exec_handshake(); + + if (st.todo.upload.length > 0 && + st.busy.upload.length < parallel_uploads) + exec_upload(); + + setTimeout(boss, 100); + } + boss(); + + ///// + //// + /// hashing + // + + // https://gist.github.com/jonleighton/958841 + function buf2b64_maybe_fucky(buffer) { + var ret = ''; + var view = new DataView(buffer); + for (var i = 0; i < view.byteLength; i++) { + ret += String.fromCharCode(view.getUint8(i)); + } + return window.btoa(ret).replace( + /\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + } + + // https://gist.github.com/jonleighton/958841 + function buf2b64(arrayBuffer) { + var base64 = ''; + var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; + var bytes = new Uint8Array(arrayBuffer); + var byteLength = bytes.byteLength; + var byteRemainder = byteLength % 3; + var mainLength = byteLength - byteRemainder; + var a, b, c, d; + var chunk; + + for (var i = 0; i < mainLength; i = i + 3) { + chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; + // create 8*3=24bit segment then split into 6bit segments + a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18 + b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12 + c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6 + d = chunk & 63; // 63 = 2^6 - 1 + + // Convert the raw binary segments to the appropriate ASCII encoding + base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]; + } + + if (byteRemainder == 1) { + chunk = bytes[mainLength]; + a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2 + b = (chunk & 3) << 4; // 3 = 2^2 - 1 (zero 4 LSB) + base64 += encodings[a] + encodings[b];//+ '=='; + } + else if (byteRemainder == 2) { + chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]; + a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10 + b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4 + c = (chunk & 15) << 2; // 15 = 2^4 - 1 (zero 2 LSB) + base64 += encodings[a] + encodings[b] + encodings[c];//+ '='; + } + + return base64; + } + + // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest + function buf2hex(buffer) { + var hexCodes = []; + var view = new DataView(buffer); + for (var i = 0; i < view.byteLength; i += 4) { + var value = view.getUint32(i) // 4 bytes per iter + var stringValue = value.toString(16) // doesn't pad + var padding = '00000000' + var paddedValue = (padding + stringValue).slice(-padding.length) + hexCodes.push(paddedValue); + } + return hexCodes.join(""); + } + + function exec_hash() { + var t = st.todo.hash.shift(); + st.busy.hash.push(t); + + var nchunks = Math.ceil(t.size / chunksize); + var nchunk = 0; + + var pb_html = ''; + var pb_perc = 99.9 / nchunks; + for (var a = 0; a < nchunks; a++) + pb_html += '
'.format( + t.n, a, pb_perc); + + o('f{0}p'.format(t.n)).innerHTML = pb_html; + + var segm_next = function () { + var reader = new FileReader(); + reader.onload = segm_load; + reader.onerror = segm_err; + + var car = nchunk * chunksize; + var cdr = car + chunksize; + if (cdr >= t.size) + cdr = t.size; + + reader.readAsArrayBuffer( + bobslice.call(t.fobj, car, cdr)); + + prog(t.n, nchunk, col_hashing); + }; + + var segm_load = async function (ev) { + const hashbuf = await crypto.subtle.digest('SHA-256', ev.target.result); + t.hash.push(buf2b64(hashbuf)); + + prog(t.n, nchunk, col_hashed); + if (++nchunk < nchunks) { + prog(t.n, nchunk, col_hashing); + return segm_next(); + } + + o('f{0}t'.format(t.n)).innerHTML = 'connecting'; + st.busy.hash.splice(st.busy.hash.indexOf(t), 1); + st.todo.handshake.push(t); + }; + + var segm_err = function () { + alert('y o u b r o k e i t\n\n(was that a folder? just files please)'); + }; + + segm_next(); + } + + ///// + //// + /// handshake + // + + function exec_handshake() { + var t = st.todo.handshake.shift(); + st.busy.handshake.push(t); + + var xhr = new XMLHttpRequest(); + xhr.onload = function (ev) { + if (xhr.status == 200) { + t.postlist = []; + t.wark = xhr.response.wark; + var missing = xhr.response.hash; + for (var a = 0; a < missing.length; a++) { + var idx = t.hash.indexOf(missing[a]); + if (idx < 0) + return alert('wtf negative index for hash "{0}" in task:\n{1}'.format( + missing[a], JSON.stringify(t))); + + t.postlist.push(idx); + } + for (var a = 0; a < t.hash.length; a++) + prog(t.n, a, (t.postlist.indexOf(a) == -1) + ? col_uploaded : col_hashed); + + var msg = 'completed'; + if (t.postlist.length > 0) { + for (var a = 0; a < t.postlist.length; a++) + st.todo.upload.push({ + 'nfile': t.n, + 'npart': t.postlist[a] + }); + + msg = 'uploading'; + } + o('f{0}t'.format(t.n)).innerHTML = msg; + st.busy.handshake.splice(st.busy.handshake.indexOf(t), 1); + } + else + alert("server broke (error {0}):\n\"{1}\"\n".format( + xhr.status, (xhr.response && xhr.response.err) || + "no further information")); + }; + xhr.open('POST', 'handshake.php', true); + xhr.responseType = 'json'; + xhr.send(JSON.stringify({ + "name": t.name, + "size": t.size, + "hash": t.hash + })); + } + + ///// + //// + /// upload + // + + function exec_upload() { + var upt = st.todo.upload.shift(); + st.busy.upload.push(upt); + + var npart = upt.npart; + var t = st.files[upt.nfile]; + + prog(t.n, npart, col_uploading); + + var car = npart * chunksize; + var cdr = car + chunksize; + if (cdr >= t.size) + cdr = t.size; + + var reader = new FileReader(); + + reader.onerror = function () { + alert('y o u b r o k e i t\n\n(was that a folder? just files please)'); + }; + + reader.onload = function (ev) { + var xhr = new XMLHttpRequest(); + xhr.upload.onprogress = function (xev) { + var perc = xev.loaded / (cdr - car) * 100; + prog(t.n, npart, '', perc); + }; + xhr.onload = function (xev) { + if (xhr.status == 200) { + prog(t.n, npart, col_uploaded); + st.busy.upload.splice(st.busy.upload.indexOf(upt), 1); + t.postlist.splice(t.postlist.indexOf(npart), 1); + if (t.postlist.length == 0) { + o('f{0}t'.format(t.n)).innerHTML = 'verifying'; + st.todo.handshake.push(t); + } + } + else + alert("server broke (error {0}):\n\"{1}\"\n".format( + xhr.status, (xhr.response && xhr.response.err) || + "no further information")); + }; + xhr.open('POST', 'chunkpit.php', true); + //xhr.setRequestHeader("X-Up2k-Hash", t.hash[npart].substr(1) + "x"); + xhr.setRequestHeader("X-Up2k-Hash", t.hash[npart]); + xhr.setRequestHeader("X-Up2k-Wark", t.wark); + xhr.setRequestHeader('Content-Type', 'application/octet-stream'); + xhr.overrideMimeType('Content-Type', 'application/octet-stream'); + xhr.responseType = 'json'; + xhr.send(ev.target.result); + }; + + reader.readAsArrayBuffer(bobslice.call(t.fobj, car, cdr)); + } + + ///// + //// + /// progress bar + // + + function prog(nfile, nchunk, color, percent) { + var n1 = o('f{0}p{1}'.format(nfile, nchunk)); + var n2 = n1.getElementsByTagName('div')[0]; + if (percent === undefined) { + n1.style.background = color; + n2.style.display = 'none'; + } + else { + n2.style.width = percent + '%'; + n2.style.display = 'block'; + } + } + + ///// + //// + /// config ui + // + + function bumpchunk(dir) { + try { + dir.stopPropagation(); + dir.preventDefault(); + } catch (ex) { } + + if (st.files.length > 0) + return alert('only possible before you start uploading\n\n(refresh and try again)') + + var obj = o('chunksz'); + if (dir.target) { + obj.style.background = '#922'; + var v = Math.floor(parseInt(obj.value)); + if (v < 1 || v > 1024 || v !== v) + return; + + chunksize_mb = v; + chunksize = chunksize_mb * 1024 * 1024; + localStorage.setItem('chunksz', v); + obj.style.background = '#444'; + return; + } + + chunksize_mb = Math.floor(chunksize_mb * dir); + + if (chunksize_mb < 1) + chunksize_mb = 1; + + if (chunksize_mb > 1024) + chunksize_mb = 1024; + + obj.value = chunksize_mb; + bumpchunk({ "target": 1 }) + } + + function bumpthread(dir) { + try { + dir.stopPropagation(); + dir.preventDefault(); + } catch (ex) { } + + var obj = o('nthread'); + if (dir.target) { + obj.style.background = '#922'; + var v = Math.floor(parseInt(obj.value)); + if (v < 1 || v > 8 || v !== v) + return; + + parallel_uploads = v; + localStorage.setItem('nthread', v); + obj.style.background = '#444'; + return; + } + + parallel_uploads += dir; + + if (parallel_uploads < 1) + parallel_uploads = 1; + + if (parallel_uploads > 8) + parallel_uploads = 8; + + obj.value = parallel_uploads; + bumpthread({ "target": 1 }) + } + + function nop(ev) { + ev.preventDefault(); + this.click(); + } + + o('chunksz_add').onclick = function (ev) { + ev.preventDefault(); + bumpchunk(2); + }; + o('chunksz_sub').onclick = function (ev) { + ev.preventDefault(); + bumpchunk(0.5); + }; + o('nthread_add').onclick = function (ev) { + ev.preventDefault(); + bumpthread(1); + }; + o('nthread_sub').onclick = function (ev) { + ev.preventDefault(); + bumpthread(-1); + }; + + o('chunksz').addEventListener('input', bumpchunk, false); + o('nthread').addEventListener('input', bumpthread, false); + + var nodes = o('u2conf').getElementsByTagName('a'); + for (var a = nodes.length - 1; a >= 0; a--) + nodes[a].addEventListener('touchend', nop, false); + + bumpchunk({ "target": 1 }) + bumpthread({ "target": 1 }) +})(); diff --git a/copyparty/web/upload.css b/copyparty/web/upload.css index 2dbe99a4..6751df21 100644 --- a/copyparty/web/upload.css +++ b/copyparty/web/upload.css @@ -2,7 +2,9 @@ padding: .5em .5em .5em .3em; background: #2d2d2d; border-radius: 0 0 1em 0; - border-right: .3em solid #3a3a3a; + border: 1px solid #3a3a3a; + border-width: 0 .3em .3em 0; + box-shadow: 0 0 1em #222 inset; max-width: 40em; } #bup input { @@ -10,19 +12,19 @@ } #up2k { display: none; + padding: 1.5em 1em 0 1em; +} +#u2err.err { + color: #f87; + padding: .5em; } #u2tgl { - display: block; color: #fc5; - background: #444; - width: 14em; font-size: 1.5em; - padding: 1em 1em .25em 1em; - border-bottom: 2px solid #2c2c2c; - border-radius: 0 0 .3em 0; } #u2body { display: none; + padding-bottom: 2em; } #u2form { width: 2px; diff --git a/copyparty/web/upload.html b/copyparty/web/upload.html index 37f85e84..4c9a33b1 100644 --- a/copyparty/web/upload.html +++ b/copyparty/web/upload.html @@ -1,4 +1,5 @@
+

@@ -7,9 +8,7 @@
- - upload - + you can upload here
@@ -20,14 +19,14 @@ - + - + + + - + - + + + @@ -45,6 +44,6 @@ - switch to the basic uploader + switch to the basic uploader if you don't need resumable uploads and progress bars