Add UI for the CryptDrive export

This commit is contained in:
yflory 2018-10-19 18:38:35 +02:00
parent 6397528f0f
commit ef37bac2e7
6 changed files with 353 additions and 112 deletions

View File

@ -769,7 +769,7 @@ define([
i = 0;
cgNetwork = undefined;
}
i++
i++;
if (!cgNetwork) {
cgNetwork = true;
return void Cryptpad.makeNetwork(function (err, nw) {

View File

@ -483,7 +483,7 @@ define([
});
ctx.sframeChan.on('EV_LOADING_INFO', function (data) {
UI.updateLoadingProgress(data, true);
UI.updateLoadingProgress(data, 'drive');
});
ctx.sframeChan.on('EV_NEW_VERSION', function () {

View File

@ -566,6 +566,22 @@ define(function () {
out.settings_backup2 = "Download my CryptDrive";
out.settings_restore = "Restore";
// XXX
out.settings_backup2Confirm = "This will download all the pads and files from your CryptDrive. If you want to continue, pick a name and press OK";
out.settings_exportTitle = "Export your CryptDrive";
out.settings_exportDescription = "Please wait while we're downloading and decrypting your documents, this may take a few minutes. Closing the tab will interrupt the process.";
out.settings_exportWarning = "For better performances, it is recommended to leave this tab focused.";
out.settings_exportCancel = "Are you sure you want to cancel the export? You will have to start again from the beginning for your next export.";
out.settings_export_reading = "Reading your CryptDrive...";
out.settings_export_download = "Decrypting and downloading your documents...";
out.settings_export_compressing = "Compressing the data...";
out.settings_export_done = "Export is ready!";
out.settings_exportError = "View errors";
out.settings_exportErrorDescription = "We weren't able to add the following documents into the export:";
out.settings_exportErrorEmpty = "The document can't be exported (empty or invalid content).";
out.settings_exportErrorMissing = "The document is missing from our servers (expired or deleted by its owner)";
out.settings_exportErrorOther = "An error occured while trying to export the document: {0}";
out.settings_resetNewTitle = "Clean CryptDrive";
out.settings_resetButton = "Remove";
out.settings_reset = "Remove all the files and folders from your CryptDrive";

View File

@ -16,6 +16,95 @@
display: flex;
flex-flow: column;
font: @colortheme_app-font;
#cp-export-container {
font-size: 16px;
display: flex;
flex: 1;
min-height: 0;
align-items: center;
justify-content: center;
.cp-export-block {
width: 800px;
max-width: 90vw;
.cp-export-progress-bar-container {
height: 24px;
background: white;
border: 1px solid #DDD;
width: 80%;
margin: auto;
position: relative;
.cp-export-progress-bar {
height: 100%;
background: #5cb85c; // Same color as loading screen bar
width: 0%;
display: inline-block;
}
.cp-export-progress-text {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
text-align: center;
font-weight: bold;
}
}
& > p {
color: #777;
}
.cp-export-progress {
margin-bottom: 1rem;
p {
margin-bottom: 0;
display: flex;
align-items: center;
padding: 5px 0;
.fa {
width: 25px;
}
}
}
.cp-export-actions {
display: flex;
flex-flow: row-reverse;
.btn-default, .btn-primary {
margin-left: 20px;
}
}
.cp-export-errors {
display: none;
overflow-x: auto;
max-height: 300px;
background: #ededed;
border: 1px solid #777;
padding: 5px 20px;
margin-top: 1rem;
& > p {
margin: 0;
}
.cp-export-errors-list {
& > div {
padding: 5px 10px;
margin: 5px 0;
background: #dedede;
.title {
font-weight: bold;
}
.link {
padding: 0 20px;
font-size: 14px;
}
.reason {
padding: 0 20px;
color: #777;
}
}
}
}
}
}
#cp-sidebarlayout-container {
#cp-sidebarlayout-rightside {
input[type="checkbox"] {

View File

@ -32,7 +32,7 @@ define([
Cred,
AppConfig,
ApiConfig,
Backup,
Backup
)
{
var saveAs = window.saveAs;
@ -303,45 +303,6 @@ define([
return $div;
};
create['backup'] = function () {
if (!common.isLoggedIn()) { return; }
var $div = $('<div>', { 'class': 'cp-settings-backup cp-sidebarlayout-element'});
$('<span>', {'class': 'label'}).text(Messages.settings_backupTitle || 'TODO BACKUP').appendTo($div); // XXX
$('<span>', {'class': 'cp-sidebarlayout-description'})
.append(Messages.settings_backupHint || 'TODO').appendTo($div); // XXX
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved});
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'});
var $button = $('<button>', {'id': 'cp-settings-delete', 'class': 'btn btn-danger'})
.text(Messages.settings_backupButton || 'BACKUP').appendTo($div); // XXX
$button.click(function () {
$spinner.show();
UI.confirm(Messages.settings_backupConfirm || 'TODO Are you sure?', function (yes) { // XXX
if (!yes) { return; }
});
// TODO
/*
UI.confirm("Are you sure?", function (yes) {
// Logout everywhere
// Disconnect other tabs
// Remove owned pads
// Remove owned drive
// Remove pinstore
// Alert: "Account deleted", press OK to be redirected to the home page
$spinner.hide();
});*/
});
$spinner.hide().appendTo($div);
$ok.hide().appendTo($div);
return $div;
};
create['delete'] = function () {
if (!common.isLoggedIn()) { return; }
var $div = $('<div>', { 'class': 'cp-settings-delete cp-sidebarlayout-element'});
@ -857,6 +818,143 @@ define([
return $div;
};
var createExportUI = function () {
var progress = h('div.cp-export-progress');
var actions = h('div.cp-export-actions');
var errors = h('div.cp-export-errors', [
h('p', Messages.settings_exportErrorDescription)
]);
var exportUI = h('div#cp-export-container', [
h('div.cp-export-block', [
h('h3', Messages.settings_exportTitle),
h('p', [
Messages.settings_exportDescription,
h('br'),
Messages.settings_exportWarning
]),
progress,
actions,
errors
])
]);
$('body').append(exportUI);
$('#cp-sidebarlayout-container').hide();
var close = function () {
$(exportUI).remove();
$('#cp-sidebarlayout-container').show();
};
var _onCancel = [];
var onCancel = function (h) {
if (typeof (h) !== "function") { return; }
_onCancel.push(h);
};
var cancel = h('button.btn.btn-default', Messages.cancel);
$(cancel).click(function () {
UI.confirm(Messages.settings_exportCancel, function (yes) {
if (!yes) { return; }
_onCancel.forEach(function (h) { h(); });
});
}).appendTo(actions);
var error = h('button.btn.btn-danger', Messages.settings_exportError);
var translateErrors = function (err) {
if (err === 'EEMPTY') {
return Messages.settings_exportErrorEmpty;
}
if (['E404', 'EEXPIRED', 'EDELETED'].indexOf(err) !== -1) {
return Messages.settings_exportErrorMissing;
}
return Messages._getKey('settings_exportErrorOther', [err]);
};
var addErrors = function (errs) {
if (!errs.length) { return; }
var onClick = function () {
console.error('clicked?');
$(errors).toggle();
};
$(error).click(onClick).appendTo(actions);
var list = h('div.cp-export-errors-list');
$(list).appendTo(errors);
errs.forEach(function (err) {
if (!err.data) { return; }
var href = err.data.href || err.data.roHref;
$(h('div', [
h('div.title', err.data.filename || err.data.title),
h('div.link', [
h('a', {
href: err.data.href || err.data.roHref,
target: '_blank'
}, privateData.origin + href)
]),
h('div.reason', translateErrors(err.error))
])).appendTo(list);
});
};
var download = h('button.btn.btn-primary', Messages.download_mt_button);
var completed = false;
var complete = function (h, err) {
if (completed) { return; }
completed = true;
$(progress).find('.fa-square-o').removeClass('fa-square-o')
.addClass('fa-check-square-o');
$(cancel).text(Messages.filePicker_close).off('click').click(function () {
_onCancel.forEach(function (h) { h(); });
});
$(download).click(h).appendTo(actions);
addErrors(err);
};
var done = {};
var update = function (step, state) {
console.log(step, state);
console.log(done[step]);
if (done[step] && done[step] === -1) { return; }
// New step
if (!done[step]) {
$(progress).find('.fa-square-o').removeClass('fa-square-o')
.addClass('fa-check-square-o');
$(progress).append(h('p', [
h('span.fa.fa-square-o'),
h('span.text', Messages['settings_export_'+step] || step)
]));
done[step] = state; // -1 if no bar, object otherwise
if (state !== -1) {
var bar = h('div.cp-export-progress-bar');
$(progress).append(h('div.cp-export-progress-bar-container', [
bar
]));
done[step] = { bar: bar };
}
return;
}
// Updating existing step
if (typeof state !== "object") { return; }
var b = done[step].bar;
var w = (state.current/state.max) * 100;
$(b).css('width', w + '%');
if (!done[step].text) {
done[step].text = h('div.cp-export-progress-text');
$(done[step].text).appendTo(b);
}
$(done[step].text).text(state.current + ' / ' + state.max);
if (state.current === state.max) { done[step] = -1; }
};
return {
close: close,
update: update,
complete: complete,
onCancel: onCancel
};
};
create['drive-backup'] = function () {
var $div = $('<div>', {'class': 'cp-settings-drive-backup cp-sidebarlayout-element'});
@ -911,18 +1009,28 @@ define([
if (err) { return void cb(err); }
if (obj.error) { return void cb(obj.error); }
cb(null, obj.data);
});
}, { timeout: 5 * 60 * 1000 });
};
Backup.create(data, getPad, function (blob) {
var ui = createExportUI();
var bu = Backup.create(data, getPad, function (blob, errors) {
console.log(blob);
saveAs(blob, filename);
sframeChan.event('EV_CRYPTGET_DISCONNECT');
ui.complete(function () {
saveAs(blob, filename);
}, errors);
}, ui.update);
ui.onCancel(function () {
ui.close();
bu.stop();
});
};
sframeChan.query("Q_SETTINGS_DRIVE_GET", "full", function (err, data) {
if (err) { return void console.error(err); }
if (data.error) { return void console.error(data.error); }
UI.prompt('TODO are you sure? if yes, pick a name...', // XXX
UI.prompt(Messages.settings_backup2Confirm,
Util.fixFileName(suggestion) + '.zip', function (filename) {
if (!(typeof(filename) === 'string' && filename)) { return; }
todo(data, filename);
@ -931,9 +1039,9 @@ define([
};
$('<span>', {'class': 'cp-sidebarlayout-description'})
.text(Messages.settings_backupHint2).appendTo($div);
var $export = common.createButton('export', true, {}, exportDrive);
$export.attr('class', 'btn btn-success').text(Messages.settings_backup2);
$div.append($export);
var $export2 = common.createButton('export', true, {}, exportDrive);
$export2.attr('class', 'btn btn-success').text(Messages.settings_backup2);
$div.append($export2);
return $div;
};

View File

@ -60,79 +60,92 @@ define([
// waitFor is used to make sure all the pads and files are process before downloading the zip.
var w = ctx.waitFor();
ctx.max++;
// Work with only 10 pad/files at a time
ctx.sem.take(function (give) {
var opts = {
password: fData.password
};
var rawName = fData.filename || fData.title || 'File';
console.log(rawName);
var g = give();
if (ctx.stop) { return; }
setTimeout(function () {
if (ctx.stop) { return; }
var opts = {
password: fData.password
};
var rawName = fData.filename || fData.title || 'File';
console.log(rawName);
var done = function () {
//setTimeout(g, 2000);
g();
w();
};
var error = function (err) {
done();
return void ctx.errors.push({
error: err,
data: fData
});
};
// Pads (pad,code,slide,kanban,poll,...)
var todoPad = function () {
ctx.get({
hash: parsed.hash,
opts: opts
}, function (err, val) {
if (err) { return void error(err); }
if (!val) { return void error('EEMPTY'); }
var opts = {
binary: true,
};
transform(ctx, parsed.type, val, function (res) {
if (!res.data) { return void error('EEMPTY'); }
var fileName = getUnique(sanitize(rawName), res.ext, existingNames);
existingNames.push(fileName.toLowerCase());
zip.file(fileName, res.data, opts);
console.log('DONE ---- ' + fileName);
setTimeout(done, 1000);
var done = function () {
if (ctx.stop) { return; }
//setTimeout(g, 2000);
g();
w();
ctx.done++;
ctx.updateProgress('download', {max: ctx.max, current: ctx.done});
};
var error = function (err) {
if (ctx.stop) { return; }
done();
return void ctx.errors.push({
error: err,
data: fData
});
});
};
};
// Files (mediatags...)
var todoFile = function () {
var secret = Hash.getSecrets('file', parsed.hash, fData.password);
var hexFileName = secret.channel;
var src = Hash.getBlobPathFromHex(hexFileName);
var key = secret.keys && secret.keys.cryptKey;
Util.fetch(src, function (err, u8) {
if (err) { return void error('E404'); }
FileCrypto.decrypt(u8, key, function (err, res) {
// Pads (pad,code,slide,kanban,poll,...)
var todoPad = function () {
ctx.get({
hash: parsed.hash,
opts: opts
}, function (err, val) {
if (ctx.stop) { return; }
if (err) { return void error(err); }
if (!val) { return void error('EEMPTY'); }
var opts = {
binary: true,
};
var extIdx = rawName.lastIndexOf('.');
var name = extIdx !== -1 ? rawName.slice(0,extIdx) : rawName;
var ext = extIdx !== -1 ? rawName.slice(extIdx) : "";
var fileName = getUnique(sanitize(name), ext, existingNames);
existingNames.push(fileName.toLowerCase());
zip.file(fileName, res.content, opts);
console.log('DONE ---- ' + fileName);
setTimeout(done, 1000);
transform(ctx, parsed.type, val, function (res) {
if (ctx.stop) { return; }
if (!res.data) { return void error('EEMPTY'); }
var fileName = getUnique(sanitize(rawName), res.ext, existingNames);
existingNames.push(fileName.toLowerCase());
zip.file(fileName, res.data, opts);
console.log('DONE ---- ' + fileName);
setTimeout(done, 500);
});
});
});
};
if (parsed.hashData.type === 'file') {
return void todoFile();
}
todoPad();
};
// Files (mediatags...)
var todoFile = function () {
var secret = Hash.getSecrets('file', parsed.hash, fData.password);
var hexFileName = secret.channel;
var src = Hash.getBlobPathFromHex(hexFileName);
var key = secret.keys && secret.keys.cryptKey;
Util.fetch(src, function (err, u8) {
if (ctx.stop) { return; }
if (err) { return void error('E404'); }
FileCrypto.decrypt(u8, key, function (err, res) {
if (ctx.stop) { return; }
if (err) { return void error(err); }
var opts = {
binary: true,
};
var extIdx = rawName.lastIndexOf('.');
var name = extIdx !== -1 ? rawName.slice(0,extIdx) : rawName;
var ext = extIdx !== -1 ? rawName.slice(extIdx) : "";
var fileName = getUnique(sanitize(name), ext, existingNames);
existingNames.push(fileName.toLowerCase());
zip.file(fileName, res.content, opts);
console.log('DONE ---- ' + fileName);
setTimeout(done, 1000);
});
});
};
if (parsed.hashData.type === 'file') {
return void todoFile();
}
todoPad();
});
});
// cb(err, blob);
};
@ -163,7 +176,7 @@ define([
};
// Main function. Create the empty zip and fill it starting from drive.root
var create = function (data, getPad, cb) {
var create = function (data, getPad, cb, progress) {
if (!data || !data.uo || !data.uo.drive) { return void cb('EEMPTY'); }
var sem = Saferphore.create(5);
var ctx = {
@ -173,18 +186,33 @@ define([
zip: new JsZip(),
errors: [],
sem: sem,
updateProgress: progress,
max: 0,
done: 0
};
progress('reading', -1);
nThen(function (waitFor) {
ctx.waitFor = waitFor;
var zipRoot = ctx.zip.folder('Root');
makeFolder(ctx, ctx.data.root, zipRoot, ctx.data.filesData);
progress('download', {});
}).nThen(function () {
console.log(ctx.zip);
console.log(ctx.errors);
progress('compressing', -1);
ctx.zip.generateAsync({type: 'blob'}).then(function (content) {
cb(content);
progress('done', -1);
cb(content, ctx.errors);
});
});
var stop = function () {
ctx.stop = true;
delete ctx.zip;
};
return {
stop: stop
};
};
return {