Merge pull request #427 from xwiki-labs/exportFolder

Export folder
This commit is contained in:
ansuz
2019-08-16 15:02:02 +02:00
committed by GitHub
7 changed files with 308 additions and 154 deletions

View File

@@ -21,9 +21,9 @@ define([
S.leave();
} catch (e) { console.log(e); }
}
if (S.session && S.session.stop) {
if (S.realtime && S.realtime.stop) {
try {
S.session.stop();
S.realtime.stop();
} catch (e) { console.error(e); }
}
var abort = Util.find(S, ['session', 'realtime', 'abort']);

340
www/common/make-backup.js Normal file
View File

@@ -0,0 +1,340 @@
define([
'/common/cryptget.js',
'/file/file-crypto.js',
'/common/common-hash.js',
'/common/common-util.js',
'/bower_components/nthen/index.js',
'/bower_components/saferphore/index.js',
'/bower_components/jszip/dist/jszip.min.js',
], function (Crypt, FileCrypto, Hash, Util, nThen, Saferphore, JsZip) {
var saveAs = window.saveAs;
var sanitize = function (str) {
return str.replace(/[\\/?%*:|"<>]/gi, '_')/*.toLowerCase()*/;
};
var getUnique = function (name, ext, existing) {
var n = name + ext;
var i = 1;
while (existing.indexOf(n.toLowerCase()) !== -1) {
n = name + ' ('+ i++ + ')' + ext;
}
return n;
};
var transform = function (ctx, type, sjson, cb) {
var result = {
data: sjson,
ext: '.json',
};
var json;
try {
json = JSON.parse(sjson);
} catch (e) {
return void cb(result);
}
var path = '/' + type + '/export.js';
require([path], function (Exporter) {
Exporter.main(json, function (data) {
result.ext = Exporter.ext || '';
result.data = data;
cb(result);
});
}, function () {
cb(result);
});
};
var _downloadFile = function (ctx, fData, cb, updateProgress) {
var cancelled = false;
var cancel = function () {
cancelled = true;
};
var parsed = Hash.parsePadUrl(fData.href || fData.roHref);
var hash = parsed.hash;
var name = fData.filename || fData.title;
var secret = Hash.getSecrets('file', hash, fData.password);
var src = Hash.getBlobPathFromHex(secret.channel);
var key = secret.keys && secret.keys.cryptKey;
Util.fetch(src, function (err, u8) {
if (cancelled) { return; }
if (err) { return void cb('E404'); }
FileCrypto.decrypt(u8, key, function (err, res) {
if (cancelled) { return; }
if (err) { return void cb(err); }
if (!res.content) { return void cb('EEMPTY'); }
var dl = function () {
saveAs(res.content, name || res.metadata.name);
};
cb(null, {
metadata: res.metadata,
content: res.content,
download: dl
});
}, updateProgress && updateProgress.progress2);
}, updateProgress && updateProgress.progress);
return {
cancel: cancel
};
};
var _downloadPad = function (ctx, pData, cb, updateProgress) {
var cancelled = false;
var cancel = function () {
cancelled = true;
};
var parsed = Hash.parsePadUrl(pData.href || pData.roHref);
var name = pData.filename || pData.title;
var opts = {
password: pData.password
};
var done = false;
ctx.sframeChan.on("EV_CRYPTGET_PROGRESS", function (data) {
if (done || data.hash !== parsed.hash) { return; }
updateProgress.progress(data.progress);
if (data.progress === 1) {
done = true;
updateProgress.progress2(1);
}
});
ctx.get({
hash: parsed.hash,
opts: opts
}, function (err, val) {
if (cancelled) { return; }
if (err) { return; }
if (!val) { return; }
transform(ctx, parsed.type, val, function (res) {
if (cancelled) { return; }
if (!res.data) { return; }
var dl = function () {
saveAs(res.data, Util.fixFileName(name));
};
cb(null, {
metadata: res.metadata,
content: res.data,
download: dl
});
});
});
return {
cancel: cancel
};
};
// Add a file to the zip. We have to cryptget&transform it if it's a pad
// or fetch&decrypt it if it's a file.
var addFile = function (ctx, zip, fData, existingNames) {
if (!fData.href && !fData.roHref) {
return void ctx.errors.push({
error: 'EINVAL',
data: fData
});
}
var parsed = Hash.parsePadUrl(fData.href || fData.roHref);
if (['pad', 'file'].indexOf(parsed.hashData.type) === -1) { return; }
// 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 g = give();
if (ctx.stop) { return; }
var to;
var done = function () {
if (ctx.stop) { return; }
if (to) { clearTimeout(to); }
//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
});
};
to = setTimeout(function () {
error('TIMEOUT');
}, 60000);
setTimeout(function () {
if (ctx.stop) { return; }
var opts = {
password: fData.password
};
var rawName = fData.filename || fData.title || 'File';
console.log(rawName);
// 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,
};
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);
});
});
};
// Files (mediatags...)
var todoFile = function () {
var it;
var dl = _downloadFile(ctx, fData, function (err, res) {
if (it) { clearInterval(it); }
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);
});
it = setInterval(function () {
if (ctx.stop) {
clearInterval(it);
dl.cancel();
}
}, 50);
};
if (parsed.hashData.type === 'file') {
return void todoFile();
}
todoPad();
});
});
// cb(err, blob);
};
// Add folders and their content recursively in the zip
var makeFolder = function (ctx, root, zip, fd) {
if (typeof (root) !== "object") { return; }
var existingNames = [];
Object.keys(root).forEach(function (k) {
var el = root[k];
if (typeof el === "object" && el.metadata !== true) { // if folder
var fName = getUnique(sanitize(k), '', existingNames);
existingNames.push(fName.toLowerCase());
return void makeFolder(ctx, el, zip.folder(fName), fd);
}
if (ctx.data.sharedFolders[el]) { // if shared folder
var sfData = ctx.sf[el].metadata;
var sfName = getUnique(sanitize(sfData.title || 'Folder'), '', existingNames);
existingNames.push(sfName.toLowerCase());
return void makeFolder(ctx, ctx.sf[el].root, zip.folder(sfName), ctx.sf[el].filesData);
}
var fData = fd[el];
if (fData) {
addFile(ctx, zip, fData, existingNames);
return;
}
});
};
// Main function. Create the empty zip and fill it starting from drive.root
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 = {
get: getPad,
data: data.uo.drive,
folder: data.folder || ctx.data.root,
sf: data.sf,
zip: new JsZip(),
errors: [],
sem: sem,
updateProgress: progress,
max: 0,
done: 0
};
var filesData = data.sharedFolderId && ctx.sf[data.sharedFolderId] ? ctx.sf[data.sharedFolderId].filesData : ctx.data.filesData;
progress('reading', -1);
nThen(function (waitFor) {
ctx.waitFor = waitFor;
var zipRoot = ctx.zip.folder('Root');
makeFolder(ctx, ctx.folder, zipRoot, filesData);
progress('download', {});
}).nThen(function () {
console.log(ctx.zip);
console.log(ctx.errors);
progress('compressing', -1);
ctx.zip.generateAsync({type: 'blob'}).then(function (content) {
progress('done', -1);
cb(content, ctx.errors);
});
});
var stop = function () {
ctx.stop = true;
delete ctx.zip;
};
return {
stop: stop
};
};
var _downloadFolder = function (ctx, data, cb, updateProgress) {
create(data, ctx.get, function (blob, errors) {
console.error(errors); // TODO show user errors
var dl = function () {
saveAs(blob, data.folderName);
};
cb(null, {download: dl});
}, function (state, progress) {
if (state === "reading") {
updateProgress.folderProgress(0);
}
if (state === "download") {
if (typeof progress.current !== "number") { return; }
updateProgress.folderProgress(progress.current / progress.max);
}
else if (state === "done") {
updateProgress.folderProgress(1);
}
});
};
return {
create: create,
downloadFile: _downloadFile,
downloadPad: _downloadPad,
downloadFolder: _downloadFolder,
};
});

View File

@@ -1,6 +1,7 @@
define([
'jquery',
'/file/file-crypto.js',
'/common/make-backup.js',
'/common/common-thumbnail.js',
'/common/common-interface.js',
'/common/common-ui-elements.js',
@@ -11,9 +12,8 @@ define([
'/bower_components/file-saver/FileSaver.min.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
], function ($, FileCrypto, Thumb, UI, UIElements, Util, Hash, h, Messages) {
], function ($, FileCrypto, MakeBackup, Thumb, UI, UIElements, Util, Hash, h, Messages) {
var Nacl = window.nacl;
var saveAs = window.saveAs;
var module = {};
var blobToArrayBuffer = function (blob, cb) {
@@ -446,124 +446,108 @@ define([
createUploader(config.dropArea, config.hoverArea, config.body);
var updateProgressbar = function (file, data, downloadFunction, cb) {
if (queue.inProgress) { return; }
queue.inProgress = true;
var id = file.id;
var $row = $table.find('tr[id="'+id+'"]');
var $pv = $row.find('.cp-fileupload-table-progress-value');
var $pb = $row.find('.cp-fileupload-table-progress-container');
var $pc = $row.find('.cp-fileupload-table-progress');
var $link = $row.find('.cp-fileupload-table-link');
var done = function () {
$row.find('.cp-fileupload-table-cancel').text('-');
queue.inProgress = false;
queue.next();
};
var updateDLProgress = function (progressValue) {
var text = Math.round(progressValue * 100) + '%';
text += ' (' + Messages.download_step1 + '...)';
$pv.text(text);
$pb.css({
width: progressValue * $pc.width() + 'px'
});
};
var updateDecryptProgress = function (progressValue) {
var text = Math.round(progressValue*100) + '%';
text += progressValue === 1 ? '' : ' (' + Messages.download_step2 + '...)';
$pv.text(text);
$pb.css({
width: progressValue * $pc.width()+'px'
});
};
var updateProgress = function (progressValue) {
var text = Math.round(progressValue*100) + '%';
$pv.text(text);
$pb.css({
width: progressValue * $pc.width()+'px'
});
};
var ctx = {
get: common.getPad,
sframeChan: sframeChan,
};
downloadFunction(ctx, data, function (err, obj) {
$link.prepend($('<span>', {'class': 'fa fa-external-link'}))
.attr('href', '#')
.click(function (e) {
e.preventDefault();
obj.download();
});
done();
if (obj) { obj.download(); }
cb(err, obj);
}, {
progress: updateDLProgress,
progress2: updateDecryptProgress,
folderProgress: updateProgress,
});
// var $cancel = $('<span>', {'class': 'cp-fileupload-table-cancel-button fa fa-times'}).click(function () {
// dl.cancel();
// $cancel.remove();
// $row.find('.cp-fileupload-table-progress-value').text(Messages.upload_cancelled);
// done();
// });
// $row.find('.cp-fileupload-table-cancel').html('').append($cancel);
$row.find('.cp-fileupload-table-cancel').html('');
};
File.downloadFile = function (fData, cb) {
var parsed = Hash.parsePadUrl(fData.href || fData.roHref);
var hash = parsed.hash;
var name = fData.filename || fData.title;
var secret = Hash.getSecrets('file', hash, fData.password);
var src = Hash.getBlobPathFromHex(secret.channel);
var key = secret.keys && secret.keys.cryptKey;
common.getFileSize(secret.channel, function (e, data) {
var todo = function (file) {
if (queue.inProgress) { return; }
queue.inProgress = true;
var id = file.id;
var $row = $table.find('tr[id="'+id+'"]');
var $pv = $row.find('.cp-fileupload-table-progress-value');
var $pb = $row.find('.cp-fileupload-table-progress-container');
var $pc = $row.find('.cp-fileupload-table-progress');
var $link = $row.find('.cp-fileupload-table-link');
var done = function () {
$row.find('.cp-fileupload-table-cancel').text('-');
queue.inProgress = false;
queue.next();
};
var updateDLProgress = function (progressValue) {
var text = Math.round(progressValue*100) + '%';
text += ' ('+ Messages.download_step1 +'...)';
$pv.text(text);
$pb.css({
width: progressValue * $pc.width()+'px'
});
};
var updateProgress = function (progressValue) {
var text = Math.round(progressValue*100) + '%';
text += progressValue === 1 ? '' : ' ('+ Messages.download_step2 +'...)';
$pv.text(text);
$pb.css({
width: progressValue * $pc.width()+'px'
});
};
var dl = module.downloadFile(fData, function (err, obj) {
$link.prepend($('<span>', {'class': 'fa fa-external-link'}))
.attr('href', '#')
.click(function (e) {
e.preventDefault();
obj.download();
});
done();
if (obj) { obj.download(); }
cb(err, obj);
}, {
src: src,
key: key,
name: name,
progress: updateDLProgress,
progress2: updateProgress,
});
var $cancel = $('<span>', {'class': 'cp-fileupload-table-cancel-button fa fa-times'}).click(function () {
dl.cancel();
$cancel.remove();
$row.find('.cp-fileupload-table-progress-value').text(Messages.upload_cancelled);
done();
});
$row.find('.cp-fileupload-table-cancel').html('').append($cancel);
};
common.getFileSize(fData.channel, function (e, data) {
queue.push({
dl: todo,
dl: function (file) { updateProgressbar(file, fData, MakeBackup.downloadFile, cb); },
size: data,
name: name
});
});
};
File.downloadPad = function (pData, cb) {
queue.push({
dl: function (file) { updateProgressbar(file, pData, MakeBackup.downloadPad, cb); },
size: 0,
name: pData.title,
});
};
File.downloadFolder = function (data, cb) {
queue.push({
dl: function (file) { updateProgressbar(file, data, MakeBackup.downloadFolder, cb); },
size: 0,
name: data.folderName,
});
};
return File;
};
module.downloadFile = function (fData, cb, obj) {
var cancelled = false;
var cancel = function () {
cancelled = true;
};
var src, key, name;
if (obj && obj.src && obj.key && obj.name) {
src = obj.src;
key = obj.key;
name = obj.name;
} else {
var parsed = Hash.parsePadUrl(fData.href || fData.roHref);
var hash = parsed.hash;
name = fData.filename || fData.title;
var secret = Hash.getSecrets('file', hash, fData.password);
src = Hash.getBlobPathFromHex(secret.channel);
key = secret.keys && secret.keys.cryptKey;
}
Util.fetch(src, function (err, u8) {
if (cancelled) { return; }
if (err) { return void cb('E404'); }
FileCrypto.decrypt(u8, key, function (err, res) {
if (cancelled) { return; }
if (err) { return void cb(err); }
if (!res.content) { return void cb('EEMPTY'); }
var dl = function () {
saveAs(res.content, name || res.metadata.name);
};
cb(null, {
metadata: res.metadata,
content: res.content,
download: dl
});
}, obj && obj.progress2);
}, obj && obj.progress);
return {
cancel: cancel
};
};
return module;
});

View File

@@ -883,7 +883,12 @@ define([
error: err,
data: val
});
}, data.opts);
}, data.opts, function (progress) {
sframeChan.event("EV_CRYPTGET_PROGRESS", {
hash: data.hash,
progress: progress,
});
});
};
//return void todo();
if (i > 30) {

View File

@@ -459,6 +459,14 @@ define([
});
}; */
funcs.getPad = function (data, cb) {
ctx.sframeChan.query("Q_CRYPTGET", data, function (err, obj) {
if (err) { return void cb(err); }
if (obj.error) { return void cb(obj.error); }
cb(null, obj.data);
}, { timeout: 60000 });
};
funcs.gotoURL = function (url) { ctx.sframeChan.event('EV_GOTO_URL', url); };
funcs.openURL = function (url) { ctx.sframeChan.event('EV_OPEN_URL', url); };
funcs.openUnsafeURL = function (url) {