diff --git a/lib/browser/app.js b/lib/browser/app.js
index 897345c2..6e9cef30 100644
--- a/lib/browser/app.js
+++ b/lib/browser/app.js
@@ -128,6 +128,13 @@ app.controller('AppController', function(
this.selectImage = function() {
return $q.when(dialog.selectImage()).then(function(image) {
+
+ // Avoid analytics and selection state changes
+ // if no file was resolved from the dialog.
+ if (!image) {
+ return;
+ }
+
self.selection.setImage(image);
AnalyticsService.logEvent('Select image', {
image: image
diff --git a/lib/partials/main.html b/lib/partials/main.html
index 1c49c21c..580cd8ea 100644
--- a/lib/partials/main.html
+++ b/lib/partials/main.html
@@ -7,7 +7,7 @@
Select image
-
+
diff --git a/lib/src/dialog.js b/lib/src/dialog.js
index 9dc088c0..4657cb1d 100644
--- a/lib/src/dialog.js
+++ b/lib/src/dialog.js
@@ -18,6 +18,7 @@
const electron = require('electron');
const Bluebird = require('bluebird');
+const zipImage = require('resin-zip-image');
/**
* @summary Open an image selection dialog
@@ -25,7 +26,10 @@ const Bluebird = require('bluebird');
* @public
*
* @description
- * Notice that by image, we mean *.img/*.iso files.
+ * Notice that by image, we mean *.img/*.iso/*.zip files.
+ *
+ * If the user selects an invalid zip image, an error alert
+ * is shown, and the promise resolves `undefined`.
*
* @fulfil {String} - selected image
* @returns {Promise};
@@ -41,15 +45,27 @@ exports.selectImage = function() {
properties: [ 'openFile' ],
filters: [
{
- name: 'IMG/ISO',
+ name: 'IMG/ISO/ZIP',
extensions: [
+ 'zip',
'img',
'iso'
]
}
]
}, resolve);
- }).get(0);
+ }).get(0).then(function(file) {
+ if (zipImage.isZip(file) && !zipImage.isValidZipImage(file)) {
+ electron.dialog.showErrorBox(
+ 'Invalid zip image',
+ 'Etcher can only open Zip archives that contain exactly one image file inside.'
+ );
+
+ return;
+ }
+
+ return file;
+ });
};
/**
diff --git a/lib/src/writer.js b/lib/src/writer.js
index ac24fda1..fb9b2b49 100644
--- a/lib/src/writer.js
+++ b/lib/src/writer.js
@@ -17,6 +17,7 @@
'use strict';
const imageWrite = require('resin-image-write');
+const zipImage = require('resin-zip-image');
const Bluebird = require('bluebird');
const umount = Bluebird.promisifyAll(require('umount'));
const fs = require('fs');
@@ -43,9 +44,17 @@ if (isWindows) {
* const stream = writer.getImageStream('foo/bar/baz.img');
*/
exports.getImageStream = function(image) {
+ if (zipImage.isZip(image)) {
+ if (!zipImage.isValidZipImage(image)) {
+ return Bluebird.reject(new Error('Invalid zip image'));
+ }
+
+ return zipImage.extractImage(image);
+ }
+
let stream = fs.createReadStream(image);
stream.length = fs.statSync(image).size;
- return stream;
+ return Bluebird.resolve(stream);
};
/**
@@ -78,7 +87,8 @@ exports.getImageStream = function(image) {
*/
exports.writeImage = function(image, drive, options, onProgress) {
return umount.umountAsync(drive.device).then(function() {
- let stream = exports.getImageStream(image);
+ return exports.getImageStream(image);
+ }).then(function(stream) {
return imageWrite.write(drive.device, stream);
}).then(function(writer) {
return new Bluebird(function(resolve, reject) {
diff --git a/package.ignore b/package.ignore
index 15ee8571..ce775dac 100644
--- a/package.ignore
+++ b/package.ignore
@@ -17,3 +17,5 @@ node_modules/gulp*
node_modules/jshint-stylish
node_modules/mochainon
node_modules/vinyl-*
+node_modules/rindle
+node_modules/tmp
diff --git a/package.json b/package.json
index 11317e5f..efca094a 100644
--- a/package.json
+++ b/package.json
@@ -61,6 +61,7 @@
"lodash": "^4.5.1",
"ngstorage": "^0.3.10",
"resin-image-write": "^2.0.5",
+ "resin-zip-image": "^1.1.1",
"sudo-prompt": "^2.2.0",
"trackjs": "^2.1.16",
"umount": "^1.1.1",
@@ -78,6 +79,8 @@
"gulp-sass": "^2.0.4",
"jshint": "^2.9.1",
"jshint-stylish": "^2.0.1",
- "mochainon": "^1.0.0"
+ "mochainon": "^1.0.0",
+ "rindle": "^1.3.0",
+ "tmp": "0.0.28"
}
}
diff --git a/tests/src/writer.spec.js b/tests/src/writer.spec.js
index 0feea72f..482bc382 100644
--- a/tests/src/writer.spec.js
+++ b/tests/src/writer.spec.js
@@ -2,27 +2,84 @@
const m = require('mochainon');
const ReadableStream = require('stream').Readable;
+const Bluebird = require('bluebird');
+const fs = Bluebird.promisifyAll(require('fs'));
const path = require('path');
+const tmp = require('tmp');
+const rindle = require('rindle');
const writer = require('../../lib/src/writer');
describe('Writer:', function() {
describe('.getImageStream()', function() {
- describe('given a valid image', function() {
+ describe('given a valid image file', function() {
beforeEach(function() {
this.image = path.join(__dirname, '..', 'utils', 'data.random');
});
- it('should return a readable stream', function() {
- const stream = writer.getImageStream(this.image);
- m.chai.expect(stream).to.be.an.instanceof(ReadableStream);
+ it('should return a readable stream', function(done) {
+ writer.getImageStream(this.image).then(function(stream) {
+ m.chai.expect(stream).to.be.an.instanceof(ReadableStream);
+ }).nodeify(done);
});
- it('should append a .length property with the correct size', function() {
- const stream = writer.getImageStream(this.image);
- m.chai.expect(stream.length).to.equal(2097152);
+ it('should append a .length property with the correct size', function(done) {
+ writer.getImageStream(this.image).then(function(stream) {
+ m.chai.expect(stream.length).to.equal(2097152);
+ }).nodeify(done);
+ });
+
+ });
+
+ describe('given a valid image zip', function() {
+
+ beforeEach(function() {
+ this.image = path.join(__dirname, '..', 'utils', 'data.zip');
+ });
+
+ it('should return a readable stream', function(done) {
+ writer.getImageStream(this.image).then(function(stream) {
+ m.chai.expect(stream).to.be.an.instanceof(ReadableStream);
+ }).nodeify(done);
+ });
+
+ it('should append a .length property with the correct size', function(done) {
+ writer.getImageStream(this.image).then(function(stream) {
+ m.chai.expect(stream.length).to.equal(2097152);
+ }).nodeify(done);
+ });
+
+ it('should pipe the image from the zip', function(done) {
+ const tmpFile = tmp.tmpNameSync();
+ const image = path.join(__dirname, '..', 'utils', 'data.random');
+ const output = fs.createWriteStream(tmpFile);
+
+ writer.getImageStream(this.image).then(function(stream) {
+ return stream.pipe(output);
+ }).then(rindle.wait).then(function() {
+ return Bluebird.props({
+ output: fs.readFileAsync(tmpFile),
+ data: fs.readFileAsync(image)
+ });
+ }).then(function(results) {
+ m.chai.expect(results.output).to.deep.equal(results.data);
+ return fs.unlinkAsync(tmpFile);
+ }).nodeify(done);
+ });
+
+ });
+
+ describe('given an invalid image zip', function() {
+
+ beforeEach(function() {
+ this.image = path.join(__dirname, '..', 'utils', 'invalid.zip');
+ });
+
+ it('should be rejected with an error', function() {
+ const promise = writer.getImageStream(this.image);
+ m.chai.expect(promise).to.be.rejectedWith('Invalid zip image');
});
});
diff --git a/tests/utils/data.zip b/tests/utils/data.zip
new file mode 100644
index 00000000..617a734d
Binary files /dev/null and b/tests/utils/data.zip differ
diff --git a/tests/utils/invalid.zip b/tests/utils/invalid.zip
new file mode 100644
index 00000000..3e4181c9
Binary files /dev/null and b/tests/utils/invalid.zip differ