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