Merge pull request #182 from resin-io/feat/zip

Add support for zip images in select image dialog
This commit is contained in:
Juan Cruz Viotti 2016-03-07 14:05:18 -04:00
commit b0129eba3e
9 changed files with 109 additions and 14 deletions

View File

@ -128,6 +128,13 @@ app.controller('AppController', function(
this.selectImage = function() { this.selectImage = function() {
return $q.when(dialog.selectImage()).then(function(image) { 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); self.selection.setImage(image);
AnalyticsService.logEvent('Select image', { AnalyticsService.logEvent('Select image', {
image: image image: image

View File

@ -7,7 +7,7 @@
<div class="space-vertical-large"> <div class="space-vertical-large">
<div ng-hide="app.selection.hasImage()"> <div ng-hide="app.selection.hasImage()">
<hero-button ng-click="app.selectImage()">Select image</hero-button> <hero-button ng-click="app.selectImage()">Select image</hero-button>
<p class="step-footer tiny">*supported files: .img, .iso</p> <p class="step-footer tiny">*supported files: .img, .iso, .zip</p>
</div> </div>
<div ng-show="app.selection.hasImage()"> <div ng-show="app.selection.hasImage()">
<span ng-bind="app.selection.getImage() | basename" ng-click="app.reselectImage()"></span> <span ng-bind="app.selection.getImage() | basename" ng-click="app.reselectImage()"></span>

View File

@ -18,6 +18,7 @@
const electron = require('electron'); const electron = require('electron');
const Bluebird = require('bluebird'); const Bluebird = require('bluebird');
const zipImage = require('resin-zip-image');
/** /**
* @summary Open an image selection dialog * @summary Open an image selection dialog
@ -25,7 +26,10 @@ const Bluebird = require('bluebird');
* @public * @public
* *
* @description * @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 * @fulfil {String} - selected image
* @returns {Promise}; * @returns {Promise};
@ -41,15 +45,27 @@ exports.selectImage = function() {
properties: [ 'openFile' ], properties: [ 'openFile' ],
filters: [ filters: [
{ {
name: 'IMG/ISO', name: 'IMG/ISO/ZIP',
extensions: [ extensions: [
'zip',
'img', 'img',
'iso' 'iso'
] ]
} }
] ]
}, resolve); }, 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;
});
}; };
/** /**

View File

@ -17,6 +17,7 @@
'use strict'; 'use strict';
const imageWrite = require('resin-image-write'); const imageWrite = require('resin-image-write');
const zipImage = require('resin-zip-image');
const Bluebird = require('bluebird'); const Bluebird = require('bluebird');
const umount = Bluebird.promisifyAll(require('umount')); const umount = Bluebird.promisifyAll(require('umount'));
const fs = require('fs'); const fs = require('fs');
@ -43,9 +44,17 @@ if (isWindows) {
* const stream = writer.getImageStream('foo/bar/baz.img'); * const stream = writer.getImageStream('foo/bar/baz.img');
*/ */
exports.getImageStream = function(image) { 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); let stream = fs.createReadStream(image);
stream.length = fs.statSync(image).size; 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) { exports.writeImage = function(image, drive, options, onProgress) {
return umount.umountAsync(drive.device).then(function() { 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); return imageWrite.write(drive.device, stream);
}).then(function(writer) { }).then(function(writer) {
return new Bluebird(function(resolve, reject) { return new Bluebird(function(resolve, reject) {

View File

@ -17,3 +17,5 @@ node_modules/gulp*
node_modules/jshint-stylish node_modules/jshint-stylish
node_modules/mochainon node_modules/mochainon
node_modules/vinyl-* node_modules/vinyl-*
node_modules/rindle
node_modules/tmp

View File

@ -61,6 +61,7 @@
"lodash": "^4.5.1", "lodash": "^4.5.1",
"ngstorage": "^0.3.10", "ngstorage": "^0.3.10",
"resin-image-write": "^2.0.5", "resin-image-write": "^2.0.5",
"resin-zip-image": "^1.1.1",
"sudo-prompt": "^2.2.0", "sudo-prompt": "^2.2.0",
"trackjs": "^2.1.16", "trackjs": "^2.1.16",
"umount": "^1.1.1", "umount": "^1.1.1",
@ -78,6 +79,8 @@
"gulp-sass": "^2.0.4", "gulp-sass": "^2.0.4",
"jshint": "^2.9.1", "jshint": "^2.9.1",
"jshint-stylish": "^2.0.1", "jshint-stylish": "^2.0.1",
"mochainon": "^1.0.0" "mochainon": "^1.0.0",
"rindle": "^1.3.0",
"tmp": "0.0.28"
} }
} }

View File

@ -2,27 +2,84 @@
const m = require('mochainon'); const m = require('mochainon');
const ReadableStream = require('stream').Readable; const ReadableStream = require('stream').Readable;
const Bluebird = require('bluebird');
const fs = Bluebird.promisifyAll(require('fs'));
const path = require('path'); const path = require('path');
const tmp = require('tmp');
const rindle = require('rindle');
const writer = require('../../lib/src/writer'); const writer = require('../../lib/src/writer');
describe('Writer:', function() { describe('Writer:', function() {
describe('.getImageStream()', function() { describe('.getImageStream()', function() {
describe('given a valid image', function() { describe('given a valid image file', function() {
beforeEach(function() { beforeEach(function() {
this.image = path.join(__dirname, '..', 'utils', 'data.random'); this.image = path.join(__dirname, '..', 'utils', 'data.random');
}); });
it('should return a readable stream', function() { it('should return a readable stream', function(done) {
const stream = writer.getImageStream(this.image); writer.getImageStream(this.image).then(function(stream) {
m.chai.expect(stream).to.be.an.instanceof(ReadableStream); m.chai.expect(stream).to.be.an.instanceof(ReadableStream);
}).nodeify(done);
}); });
it('should append a .length property with the correct size', function() { it('should append a .length property with the correct size', function(done) {
const stream = writer.getImageStream(this.image); writer.getImageStream(this.image).then(function(stream) {
m.chai.expect(stream.length).to.equal(2097152); 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');
}); });
}); });

BIN
tests/utils/data.zip Normal file

Binary file not shown.

BIN
tests/utils/invalid.zip Normal file

Binary file not shown.