diff --git a/www/common/common-file.js b/www/common/common-file.js new file mode 100644 index 000000000..9781df3ef --- /dev/null +++ b/www/common/common-file.js @@ -0,0 +1,260 @@ +define([ + 'jquery', + '/file/file-crypto.js', + '/bower_components/tweetnacl/nacl-fast.min.js', +], function ($, FileCrypto) { + var Nacl = window.nacl; + var module = {}; + + module.create = function (common) { + var File = {}; + + var Messages = common.Messages; + + var queue = File.queue = { + queue: [], + inProgress: false + }; + + var uid = function () { + return 'file-' + String(Math.random()).substring(2); + }; + + var $table = File.$table = $('', { id: 'uploadStatus' }); + var $thead = $('').appendTo($table); + $('', {id: id}).appendTo($table); + + var $cancel = $('', {'class': 'cancel fa fa-times'}).click(function () { + queue.queue = queue.queue.filter(function (el) { return el.id !== id; }); + $cancel.remove(); + $tr.find('.upCancel').text('-'); + $tr.find('.progressValue').text(Messages.upload_cancelled); + }); + + var $link = $('', { + 'class': 'upLink', + 'rel': 'noopener noreferrer' + }).text(obj.metadata.name); + + $('
').text(Messages.upload_name).appendTo($thead); + $('').text(Messages.upload_size).appendTo($thead); + $('').text(Messages.upload_progress).appendTo($thead); + $('').text(Messages.cancel).appendTo($thead); + + var createTableContainer = function ($body) { + File.$container = $('
', { id: 'uploadStatusContainer' }).append($table).appendTo($body); + return File.$container; + }; + + var upload = function (blob, metadata, id) { + if (queue.inProgress) { return; } + queue.inProgress = true; + + var $row = $table.find('tr[id="'+id+'"]'); + + $row.find('.upCancel').html('-'); + var $pv = $row.find('.progressValue'); + var $pb = $row.find('.progressContainer'); + var $link = $row.find('.upLink'); + + var updateProgress = function (progressValue) { + $pv.text(Math.round(progressValue*100)/100 + '%'); + $pb.css({ + width: (progressValue/100)*188+'px' + }); + }; + + var u8 = new Uint8Array(blob); + + var key = Nacl.randomBytes(32); + var next = FileCrypto.encrypt(u8, metadata, key); + + var estimate = FileCrypto.computeEncryptedSize(blob.byteLength, metadata); + + var sendChunk = function (box, cb) { + var enc = Nacl.util.encodeBase64(box); + Cryptpad.rpc.send.unauthenticated('UPLOAD', enc, function (e, msg) { + console.log(box); + cb(e, msg); + }); + }; + + var actual = 0; + var again = function (err, box) { + if (err) { throw new Error(err); } + if (box) { + actual += box.length; + var progressValue = (actual / estimate * 100); + updateProgress(progressValue); + + return void sendChunk(box, function (e) { + if (e) { return console.error(e); } + next(again); + }); + } + + if (actual !== estimate) { + console.error('Estimated size does not match actual size'); + } + + // if not box then done + Cryptpad.uploadComplete(function (e, id) { + if (e) { return void console.error(e); } + var uri = ['', 'blob', id.slice(0,2), id].join('/'); + console.log("encrypted blob is now available as %s", uri); + + var b64Key = Nacl.util.encodeBase64(key); + + var hash = Cryptpad.getFileHashFromKeys(id, b64Key); + var href = '/file/#' + hash; + $link.attr('href', href) + .click(function (e) { + e.preventDefault(); + window.open($link.attr('href'), '_blank'); + }); + + // TODO add button to table which copies link to clipboard? + //APP.toolbar.addElement(['fileshare'], {}); + + var title = document.title = metadata.name; + myFile = blob; + myDataType = metadata.type; + + Cryptpad.renamePad(title || "", href, function (err) { + if (err) { console.error(err); } // TODO + console.log(title); + Cryptpad.log(Messages._getKey('upload_success', [title])); + queue.inProgress = false; + queue.next(); + }) + //Title.updateTitle(title || "", href); + //APP.toolbar.title.show(); + }); + }; + + Cryptpad.uploadStatus(estimate, function (e, pending) { + if (e) { + queue.inProgress = false; + queue.next(); + if (e === 'TOO_LARGE') { + // TODO update table to say too big? + return void Cryptpad.alert(Messages.upload_tooLarge); + } + if (e === 'NOT_ENOUGH_SPACE') { + // TODO update table to say not enough space? + return void Cryptpad.alert(Messages.upload_notEnoughSpace); + } + console.error(e); + return void Cryptpad.alert(Messages.upload_serverError); + } + + if (pending) { + // TODO keep this message in case of pending files in another window? + return void Cryptpad.confirm(Messages.upload_uploadPending, function (yes) { + if (!yes) { return; } + Cryptpad.uploadCancel(function (e, res) { + if (e) { + return void console.error(e); + } + console.log(res); + next(again); + }); + }); + } + next(again); + }); + }; + + var prettySize = function (bytes) { + var kB = Cryptpad.bytesToKilobytes(bytes); + if (kB < 1024) { return kB + Messages.KB; } + var mB = Cryptpad.bytesToMegabytes(bytes); + return mB + Messages.MB; + }; + + queue.next = function () { + if (queue.queue.length === 0) { return; } + if (queue.inProgress) { return; } + var file = queue.queue.shift(); + upload(file.blob, file.metadata, file.id); + }; + queue.push = function (obj) { + var id = uid(); + obj.id = id; + queue.queue.push(obj); + + $table.show(); + var estimate = FileCrypto.computeEncryptedSize(obj.blob.byteLength, obj.metadata); + + var $progressBar = $('
', {'class':'progressContainer'}); + var $progressValue = $('', {'class':'progressValue'}).text(Messages.upload_pending); + + var $tr = $('
').append($link).appendTo($tr); + $('').text(prettySize(estimate)).appendTo($tr); + $('', {'class': 'upProgress'}).append($progressBar).append($progressValue).appendTo($tr); + $('', {'class': 'upCancel'}).append($cancel).appendTo($tr); + + queue.next(); + }; + + var handleFile = File.handleFile = function (file) { + console.log(file); + var reader = new FileReader(); + reader.onloadend = function () { + queue.push({ + blob: this.result, + metadata: { + name: file.name, + type: file.type, + } + }); + }; + reader.readAsArrayBuffer(file); + }; + + var createAreaHandlers = File.createDropArea = function ($area, $hoverArea, todo) { + var counter = 0; + if (!$hoverArea) { $hoverArea = $area; } + $hoverArea + .on('dragenter', function (e) { + e.preventDefault(); + e.stopPropagation(); + counter++; + $label.addClass('hovering'); + }) + .on('dragleave', function (e) { + e.preventDefault(); + e.stopPropagation(); + counter--; + if (counter <= 0) { + $label.removeClass('hovering'); + } + }); + + $area + .on('drag dragstart dragend dragover drop dragenter dragleave', function (e) { + e.preventDefault(); + e.stopPropagation(); + }) + .on('drop', function (e) { + e.stopPropagation(); + var dropped = e.originalEvent.dataTransfer.files; + counter = 0; + $label.removeClass('hovering'); + + Array.prototype.slice.call(dropped).forEach(function (d) { + todo(d); + }); + }); + }; + + File.createUploader = function ($area, $hover, $body) { + createAreaHandlers($area, null, handleFile); + var $c = createTableContainer($body); + }; + + return File; + }; + + return module; +}); diff --git a/www/common/common-title.js b/www/common/common-title.js index 226dc1b34..f5f793f6e 100644 --- a/www/common/common-title.js +++ b/www/common/common-title.js @@ -43,11 +43,12 @@ define(function () { onLocal(); }; - exp.updateTitle = function (newTitle) { + // update title: href is optional; if not specified, we use window.location.href + exp.updateTitle = function (newTitle, href) { if (newTitle === exp.title) { return; } // Change the title now, and set it back to the old value if there is an error var oldTitle = exp.title; - Cryptpad.renamePad(newTitle, function (err, data) { + Cryptpad.renamePad(newTitle, href, function (err, data) { if (err) { console.log("Couldn't set pad title"); console.error(err); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index fcefbd0fe..af8a600ca 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -11,11 +11,13 @@ define([ '/common/common-title.js', '/common/common-metadata.js', '/common/common-codemirror.js', + '/common/common-file.js', '/common/clipboard.js', '/common/pinpad.js', '/customize/application_config.js' -], function ($, Config, Messages, Store, Util, Hash, UI, History, UserList, Title, Metadata, CodeMirror, Clipboard, Pinpad, AppConfig) { +], function ($, Config, Messages, Store, Util, Hash, UI, History, UserList, Title, Metadata, + CodeMirror, Files, Clipboard, Pinpad, AppConfig) { /* This file exposes functionality which is specific to Cryptpad, but not to any particular pad type. This includes functions for committing metadata @@ -114,6 +116,9 @@ define([ // CodeMirror common.createCodemirror = CodeMirror.create; + // Files + common.createFileManager = function () { return Files.create(common); }; + // History common.getHistory = function (config) { return History.create(common, config); }; @@ -520,8 +525,8 @@ define([ cb ("store.forgetPad is not a function"); }; - common.setPadTitle = function (name, cb) { - var href = window.location.href; + common.setPadTitle = function (name, padHref, cb) { + var href = padHref || window.location.href; var parsed = parsePadUrl(href); if (!parsed.hash) { return; } href = getRelativeHref(href); @@ -621,15 +626,15 @@ define([ /* * Buttons */ - common.renamePad = function (title, callback) { + common.renamePad = function (title, href, callback) { if (title === null) { return; } if (title.trim() === "") { - var parsed = parsePadUrl(window.location.href); + var parsed = parsePadUrl(href || window.location.href); title = getDefaultName(parsed); } - common.setPadTitle(title, function (err) { + common.setPadTitle(title, href, function (err) { if (err) { console.log("unable to set pad title"); console.log(err); diff --git a/www/common/toolbar2.js b/www/common/toolbar2.js index 813cef23c..39bfc49e8 100644 --- a/www/common/toolbar2.js +++ b/www/common/toolbar2.js @@ -438,7 +438,7 @@ define([ if (name === "") { name = $input.attr('placeholder'); } - Cryptpad.renamePad(name, function (err, newtitle) { + Cryptpad.renamePad(name, null, function (err, newtitle) { if (err) { return; } $text.text(newtitle); callback(null, newtitle); diff --git a/www/file/file.css b/www/file/file.css index f1e78d72a..eade31db1 100644 --- a/www/file/file.css +++ b/www/file/file.css @@ -77,7 +77,7 @@ body { z-index: 10000; display: block; } -#status { +#uploadStatus { display: none; width: 80vw; margin-top: 50px; @@ -85,24 +85,24 @@ body { border: 1px solid black; border-collapse: collapse; } -#status tr:nth-child(1) { +#uploadStatus tr:nth-child(1) { background-color: #ccc; border: 1px solid #999; } -#status tr:nth-child(1) td { +#uploadStatus tr:nth-child(1) td { text-align: center; } -#status td { +#uploadStatus td { border-left: 1px solid #BBB; border-right: 1px solid #BBB; padding: 0 10px; } -#status .upProgress { +#uploadStatus .upProgress { width: 200px; position: relative; text-align: center; } -#status .progressContainer { +#uploadStatus .progressContainer { position: absolute; width: 0px; left: 5px; @@ -110,9 +110,9 @@ body { bottom: 1px; background-color: rgba(0, 0, 255, 0.3); } -#status .upCancel { +#uploadStatus .upCancel { text-align: center; } -#status .fa.cancel { +#uploadStatus .fa.cancel { color: #ff0073; } diff --git a/www/file/file.less b/www/file/file.less index 04407f8e5..2347889f7 100644 --- a/www/file/file.less +++ b/www/file/file.less @@ -87,7 +87,7 @@ html, body { display: block; } -#status { +#uploadStatus { display: none; width: 80vw; margin-top: 50px; diff --git a/www/file/inner.html b/www/file/inner.html index f5946c099..0005241a5 100644 --- a/www/file/inner.html +++ b/www/file/inner.html @@ -21,14 +21,14 @@ data-localization="download_button"> - + diff --git a/www/file/main.js b/www/file/main.js index a07a7d27e..3e7c7faa3 100644 --- a/www/file/main.js +++ b/www/file/main.js @@ -17,7 +17,7 @@ define([ var APP = {}; $(function () { - +// TODO race condition with contents() here var ifrw = $('#pad-iframe')[0].contentWindow; var $iframe = $('#pad-iframe').contents(); var $form = $iframe.find('#upload-form'); @@ -25,9 +25,10 @@ define([ var $label = $form.find('label'); var $table = $iframe.find('#status'); var $progress = $iframe.find('#progress'); + var $body = $iframe.find('body'); - $iframe.find('body').on('dragover', function (e) { e.preventDefault(); }); - $iframe.find('body').on('drop', function (e) { e.preventDefault(); }); + $body.on('dragover', function (e) { e.preventDefault(); }); + $body.on('drop', function (e) { e.preventDefault(); }); Cryptpad.addLoadingScreen(); @@ -35,7 +36,7 @@ define([ var myFile; var myDataType; - +/* var queue = { queue: [], inProgress: false @@ -206,7 +207,7 @@ define([ queue.next(); }; - +*/ var uploadMode = false; var andThen = function () { @@ -329,7 +330,7 @@ define([ display: 'block', }); - var handleFile = function (file) { + /*var handleFile = function (file) { console.log(file); var reader = new FileReader(); reader.onloadend = function () { @@ -342,45 +343,17 @@ define([ }); }; reader.readAsArrayBuffer(file); - }; + };*/ + + var FM = Cryptpad.createFileManager(); $form.find("#file").on('change', function (e) { var file = e.target.files[0]; - handleFile(file); + FM.handleFile(file); }); - var counter = 0; - $label - .on('dragenter', function (e) { - e.preventDefault(); - e.stopPropagation(); - counter++; - $label.addClass('hovering'); - }) - .on('dragleave', function (e) { - e.preventDefault(); - e.stopPropagation(); - counter--; - if (counter <= 0) { - $label.removeClass('hovering'); - } - }); - - $form - .on('drag dragstart dragend dragover drop dragenter dragleave', function (e) { - e.preventDefault(); - e.stopPropagation(); - }) - .on('drop', function (e) { - e.stopPropagation(); - var dropped = e.originalEvent.dataTransfer.files; - counter = 0; - $label.removeClass('hovering'); - - Array.prototype.slice.call(dropped).forEach(function (d) { - handleFile(d); - }); - }); + //FM.createDropArea($form, $label, handleFile); + FM.createUploader($form, $label, $body); // we're in upload mode Cryptpad.removeLoadingScreen();