Fix #306 - Support importing .zip archives

This commit is contained in:
David Humphrey (:humph) david.humphrey@senecacollege.ca 2015-06-26 14:51:30 -04:00
Родитель 079e94eb57
Коммит 486ba1d7de
5 изменённых файлов: 373 добавлений и 10 удалений

3
.gitmodules поставляемый
Просмотреть файл

@ -43,3 +43,6 @@
[submodule "src/bramble/thirdparty/EventEmitter"]
path = src/bramble/thirdparty/EventEmitter
url = https://github.com/Wolfy87/EventEmitter.git
[submodule "src/thirdparty/jszip"]
path = src/thirdparty/jszip
url = https://github.com/Stuk/jszip.git

Просмотреть файл

@ -21,6 +21,70 @@ define(function (require, exports, module) {
mkdir: function(path, callback) {
proxyCall("mkdir", {args: [path]}, callback);
},
/**
* Recursively creates the directory at `path`. If the parent
* of `path` does not exist, it will be created. From:
* https://github.com/filerjs/filer/blob/develop/src/shell/shell.js
*/
mkdirp: function(path, callback) {
callback = callback || function(){};
// We don't have direct access to Filer.Errors, fake it.
function filerError(code, msg) {
var err = new Error(msg);
err.code = code;
return err;
}
if(!path) {
return callback(filerError("EINVAL", "missing path argument"));
} else if (path === "/") {
return callback();
}
function _mkdirp(path, callback) {
var parent;
proxyFS.stat(path, function(err, stat) {
if(stat) {
if(stat.type === "DIRECTORY") {
callback();
} else if (stat.type === "FILE") {
callback(filerError("ENOTDIR", path));
}
} else if (err && err.code !== "ENOENT") {
callback(err);
} else {
parent = Path.dirname(path);
if(parent === "/") {
proxyFS.mkdir(path, function (err) {
if (err && err.code !== "EEXIST") {
return callback(err);
}
callback();
});
} else {
_mkdirp(parent, function (err) {
if (err) {
return callback(err);
}
proxyFS.mkdir(path, function (err) {
if (err && err.code !== "EEXIST") {
return callback(err);
}
callback();
});
});
}
}
});
}
_mkdirp(path, callback);
},
rmdir: function(path, callback) {
proxyCall("rmdir", {args: [path]}, callback);
},
@ -31,6 +95,11 @@ define(function (require, exports, module) {
proxyCall("rename", {args: [oldPath, newPath]}, callback);
},
readFile: function(path, options, callback) {
if(typeof options === "function") {
callback = options;
options = {};
}
// Always do binary reads, and decode in callback if necessary
proxyCall("readFile", {args: [path, {encoding: null}]}, function(err, data) {
if(err) {
@ -47,6 +116,11 @@ define(function (require, exports, module) {
});
},
writeFile: function(path, data, encoding, callback) {
if(typeof encoding === "function") {
callback = encoding;
encoding = null;
}
// Always do binary write, and send ArrayBuffer over transport
if (typeof(data) === "string") {
data = new FilerBuffer(data, "utf8");

Просмотреть файл

@ -0,0 +1,217 @@
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define */
define(function (require, exports, module) {
"use strict";
var CommandManager = require("command/CommandManager");
var Commands = require("command/Commands");
var async = require("filesystem/impls/filer/lib/async");
var StartupState = require("bramble/StartupState");
var JSZip = require("thirdparty/jszip/dist/jszip.min");
var BlobUtils = require("filesystem/impls/filer/BlobUtils");
var Filer = require("filesystem/impls/filer/BracketsFiler");
var Buffer = Filer.Buffer;
var Path = Filer.Path;
var fs = Filer.fs();
// zipfile can be a path (string) to a zipfile, or raw binary data.
function unzip(zipfile, options, callback) {
if(typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
callback = callback || function(){};
if(!zipfile) {
callback(new Error("missing zipfile argument"));
return;
}
var root = StartupState.project("root");
var destination = Path.resolve(options.destination || root);
// Mac and Windows clutter zip files with extra files/folders we don't need
function skipFile(filename) {
var basename = Path.basename(filename);
// Skip OS X additions we don't care about in the browser fs
if(/^__MACOSX\//.test(filename)) {
// http://superuser.com/questions/104500/what-is-macosx-folder
return true;
}
if(basename === ".DS_Store") {
// https://en.wikipedia.org/wiki/.DS_Store
return true;
}
if(/^\._/.test(basename)) {
// http://apple.stackexchange.com/questions/14980/why-are-dot-underscore-files-created-and-how-can-i-avoid-them
return true;
}
// Skip Windows additiosn we don't care about in the browser fs
if(/(Tt)humbs\.db/.test(basename)) {
// https://en.wikipedia.org/wiki/Windows_thumbnail_cache
return true;
}
if(basename === "desktop.ini") {
// http://www.computerhope.com/issues/ch001060.htm
return true;
}
// Include this file, don't skip.
return false;
}
function _unzip(data){
// TODO: it would be great to move this bit to a worker.
var archive = new JSZip(data);
var filenames = [];
archive.filter(function(relPath, file) {
if(skipFile(file.name)) {
return;
}
var isDir = file.options.dir;
filenames.push({
absPath: Path.join(destination, file.name),
isDirectory: isDir,
data: isDir ? null : new Buffer(file.asArrayBuffer())
});
});
function decompress(path, callback) {
if(path.isDirectory) {
fs.mkdirp(path.absPath, callback);
} else {
fs.writeFile(path.absPath, path.data, callback);
}
}
async.eachSeries(filenames, decompress, function(err) {
if(err) {
return callback(err);
}
// Generate Blob URLs for all the files we imported so they work
// in the preview.
BlobUtils.preload(root, function(err) {
if(err) {
return callback(err);
}
// Update the file tree to show the new files
CommandManager.execute(Commands.FILE_REFRESH);
callback();
});
});
}
if(typeof zipfile === "string") {
fs.readFile(Path.resolve(root, zipfile), function(err, data) {
if(err) {
return callback(err);
}
_unzip(data);
});
} else {
// zipfile is raw zip data, process it directly
_unzip(zipfile);
}
}
function zip(zipfile, paths, options, callback) {
if(typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
callback = callback || function(){};
if(!zipfile) {
return callback(new Error("missing zipfile argument"));
}
if(!paths) {
return callback(new Error("missing paths argument"));
}
if(typeof paths === "string") {
paths = [paths];
}
var root = StartupState.project("root");
zipfile = Path.resolve(root, zipfile);
function toRelPath(path) {
// Make path relative within the zip
return path.replace(/^\//, '');
}
function addFile(path, callback) {
fs.readFile(path, function(err, data) {
if(err) {
return callback(err);
}
archive.file(toRelPath(path), data.buffer, {binary: true});
callback();
});
}
function addDir(path, callback) {
fs.readdir(path, function(err, list) {
// Add the directory itself
archive.folder(toRelPath(path));
if(!options.recursive) {
return callback();
}
// Add all children of this dir, too
async.eachSeries(list, function(entry, callback) {
add(Path.join(path, entry), callback);
}, callback);
});
}
function add(path, callback) {
path = Path.resolve(root, path);
fs.stat(path, function(err, stats) {
if(err) {
return callback(err);
}
if(stats.type === "DIRECTORY") {
addDir(path, callback);
} else {
addFile(path, callback);
}
});
}
var archive = new JSZip();
// Make sure the zipfile doesn't already exist.
fs.exists(zipfile, function(exists) {
if(exists) {
return callback(new Error('zipfile already exists', zipfile));
}
async.eachSeries(paths, add, function(err) {
if(err) {
return callback(err);
}
var compressed = archive.generate({type: 'arraybuffer'});
fs.writeFile(zipfile, compressed, callback);
});
});
}
exports.zip = zip;
exports.unzip = unzip;
});

1
src/thirdparty/jszip поставляемый Submodule

@ -0,0 +1 @@
Subproject commit f8534d472f2348aa9d186af59301bc01336ef7a5

Просмотреть файл

@ -46,7 +46,8 @@ define(function (require, exports, module) {
Path = Filer.Path,
Content = require("filesystem/impls/filer/lib/content"),
LanguageManager = require("language/LanguageManager"),
StartupState = require("bramble/StartupState");
StartupState = require("bramble/StartupState"),
unzip = require("filesystem/impls/filer/ZipUtils").unzip;
// 3M size limit for imported files
var byteLimit = 3 * 1024 * 1000;
@ -181,6 +182,76 @@ define(function (require, exports, module) {
var pathList = [];
var errorList = [];
function shouldOpenFile(filename, encoding) {
var ext = Path.extname(filename).replace(/^\./, "");
var language = LanguageManager.getLanguageForExtension(ext);
var id = language && language.getId();
var isImage = id === "image" || id === "svg";
return isImage || encoding === "utf8";
}
function handleRegularFile(deferred, file, filename, buffer, encoding) {
file.write(buffer, {encoding: encoding}, function(err) {
if (err) {
deferred.reject(err);
return;
}
// See if this file is worth trying to open in the editor or not
if(shouldOpenFile(filename, encoding)) {
pathList.push(filename);
}
deferred.resolve();
});
}
function handleZipFile(deferred, file, filename, buffer, encoding) {
var basename = Path.basename(filename);
var message = "<p>Do you want to extract the contents of the zip file: <b>" +
basename + "</b>?</p>" +
"<p><em>NOTE: This can take some time.</em><p>";
// TODO: l10n, UX audit
Dialogs.showModalDialog(
DefaultDialogs.DIALOG_ID_INFO,
"Unzip file",
message,
[
{
className : Dialogs.DIALOG_BTN_CLASS_NORMAL,
id : Dialogs.DIALOG_BTN_CANCEL,
text : "No"
},
{
className : Dialogs.DIALOG_BTN_CLASS_PRIMARY,
id : Dialogs.DIALOG_BTN_OK,
text : "Yes"
}
]
)
.done(function(id) {
if (id === Dialogs.DIALOG_BTN_OK) {
// TODO: we need to give a spinner or something to indicate we're working
unzip(buffer, function(err) {
if (err) {
deferred.reject(err);
return;
}
Dialogs.showModalDialog(
DefaultDialogs.DIALOG_ID_INFO,
"Unzip Completed Successfully",
"Successfully unzipped <b>" + basename + "</b>."
).then(deferred.resolve, deferred.reject);
});
} else {
handleRegularFile(deferred, file, filename, buffer, encoding);
}
});
}
function prepareDropPaths(fileList) {
// Convert FileList object to an Array with all image files first, then CSS
// followed by HTML files at the end, since we need to write any .css, .js, etc.
@ -267,15 +338,12 @@ define(function (require, exports, module) {
buffer = buffer.toString();
}
file.write(buffer, {encoding: encoding}, function(err) {
if (err) {
deferred.reject(err);
return;
}
pathList.push(filename);
deferred.resolve();
});
// Special-case .zip files, so we can offer to extract the contents
if(Path.extname(filename) === ".zip") {
handleZipFile(deferred, file, filename, buffer, encoding);
} else {
handleRegularFile(deferred, file, filename, buffer, encoding);
}
};
// Deal with error cases, for example, trying to drop a folder vs. file