feat(image-stream): Support Apple Disk Images (#1257)

This adds support for flashing Apple Disk Images (.dmg)

Change-Type: minor
This commit is contained in:
Jonas Hermsmeier 2017-04-07 21:34:18 +02:00 committed by GitHub
parent 2411a7677d
commit b3b928ae4f
13 changed files with 268 additions and 58 deletions

1
.gitattributes vendored
View File

@ -30,3 +30,4 @@ Makefile text
*.png binary
*.xz binary
*.zip binary
*.dmg binary

View File

@ -23,6 +23,7 @@ const lzma = Bluebird.promisifyAll(require('lzma-native'));
const zlib = require('zlib');
const unbzip2Stream = require('unbzip2-stream');
const gzip = require('./gzip');
const udif = Bluebird.promisifyAll(require('udif'));
const archive = require('./archive');
const zipArchiveHooks = require('./archive-hooks/zip');
@ -125,6 +126,35 @@ module.exports = {
});
},
/**
* @summary Handle Apple disk images (.dmg)
* @function
* @public
* @memberof handlers
*
* @param {String} file - file path
* @param {Object} options - options
* @param {Number} [options.size] - file size
*
* @fulfil {Object} - image metadata
* @returns {Promise}
*/
'application/x-apple-diskimage': (file, options) => {
return udif.getUncompressedSizeAsync(file).then((size) => {
return {
stream: udif.createReadStream(file),
size: {
original: options.size,
final: {
estimation: false,
value: size
}
},
transform: new PassThroughStream()
};
});
},
/**
* @summary Handle ZIP compressed images
* @function

View File

@ -37,6 +37,10 @@ module.exports = [
extension: 'xz',
type: 'compressed'
},
{
extension: 'dmg',
type: 'compressed'
},
{
extension: 'img',
type: 'image'

View File

@ -18,15 +18,16 @@
const _ = require('lodash');
const Bluebird = require('bluebird');
const fs = Bluebird.promisifyAll(require('fs'));
const archiveType = require('archive-type');
const fileType = require('file-type');
const mime = require('mime-types');
const fs = require('fs');
/**
* @summary Get archive mime type
* @function
* @public
*
* @param {String} file - file path
* @param {String} filename - file path
* @fulfil {String} - mime type
* @returns {Promise}
*
@ -35,28 +36,27 @@ const archiveType = require('archive-type');
* console.log(mimeType);
* });
*/
exports.getArchiveMimeType = (file) => {
exports.getArchiveMimeType = (filename) => {
// `archive-type` only needs the first 261 bytes
// See https://github.com/kevva/archive-type
const ARCHIVE_TYPE_IDENTIFICATION_BYTES_LENGTH = 261;
const mimeType = mime.lookup(filename);
return Bluebird.using(fs.openAsync(file, 'r').disposer((fileDescriptor) => {
if (mimeType) {
return Bluebird.resolve(mimeType);
}
const FILE_TYPE_ID_BYTES = 262;
return Bluebird.using(fs.openAsync(filename, 'r').disposer((fileDescriptor) => {
return fs.closeAsync(fileDescriptor);
}), (fileDescriptor) => {
const BUFFER_START = 0;
const chunk = new Buffer(ARCHIVE_TYPE_IDENTIFICATION_BYTES_LENGTH);
const buffer = Buffer.alloc(FILE_TYPE_ID_BYTES);
return fs.readAsync(
fileDescriptor,
chunk,
BUFFER_START,
ARCHIVE_TYPE_IDENTIFICATION_BYTES_LENGTH,
null
).then(() => {
return _.get(archiveType(chunk), [ 'mime' ], 'application/octet-stream');
return fs.readAsync(fileDescriptor, buffer, BUFFER_START, FILE_TYPE_ID_BYTES, null).then(() => {
return _.get(fileType(buffer), [ 'mime' ], 'application/octet-stream');
});
});
};
/**

98
npm-shrinkwrap.json generated
View File

@ -146,6 +146,11 @@
"from": "any-promise@>=1.1.0 <2.0.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz"
},
"apple-data-compression": {
"version": "0.1.0",
"from": "apple-data-compression@>=0.1.0 <0.2.0",
"resolved": "https://registry.npmjs.org/apple-data-compression/-/apple-data-compression-0.1.0.tgz"
},
"aproba": {
"version": "1.1.1",
"from": "aproba@>=1.0.3 <2.0.0",
@ -157,11 +162,6 @@
"from": "arch@2.1.0",
"resolved": "https://registry.npmjs.org/arch/-/arch-2.1.0.tgz"
},
"archive-type": {
"version": "3.2.0",
"from": "archive-type@>=3.2.0 <4.0.0",
"resolved": "https://registry.npmjs.org/archive-type/-/archive-type-3.2.0.tgz"
},
"archiver": {
"version": "1.3.0",
"from": "archiver@>=1.0.0 <2.0.0",
@ -470,6 +470,11 @@
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
"dev": true
},
"bloodline": {
"version": "1.0.1",
"from": "bloodline@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/bloodline/-/bloodline-1.0.1.tgz"
},
"bluebird": {
"version": "3.4.1",
"from": "bluebird@>=3.0.5 <4.0.0",
@ -940,6 +945,11 @@
"resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-2.0.5.tgz",
"dev": true
},
"commander": {
"version": "2.8.1",
"from": "commander@>=2.8.1 <2.9.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz"
},
"commoner": {
"version": "0.10.8",
"from": "commoner@>=0.10.3 <0.11.0",
@ -2439,9 +2449,9 @@
}
},
"file-type": {
"version": "3.9.0",
"from": "file-type@>=3.1.0 <4.0.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz"
"version": "4.1.0",
"from": "file-type@latest",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-4.1.0.tgz"
},
"file-uri-to-path": {
"version": "0.0.2",
@ -2565,6 +2575,18 @@
"from": "async@>=0.9.0 <0.10.0",
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
"dev": true
},
"mime-db": {
"version": "1.12.0",
"from": "mime-db@>=1.12.0 <1.13.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz",
"dev": true
},
"mime-types": {
"version": "2.0.14",
"from": "mime-types@>=2.0.3 <2.1.0",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz",
"dev": true
}
}
},
@ -4634,16 +4656,14 @@
"dev": true
},
"mime-db": {
"version": "1.12.0",
"from": "mime-db@>=1.12.0 <1.13.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz",
"dev": true
"version": "1.27.0",
"from": "mime-db@>=1.27.0 <1.28.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz"
},
"mime-types": {
"version": "2.0.14",
"from": "mime-types@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz",
"dev": true
"version": "2.1.15",
"from": "mime-types@latest",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz"
},
"minimalistic-assert": {
"version": "1.0.0",
@ -5746,7 +5766,21 @@
"version": "2.55.0",
"from": "request@2.55.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.55.0.tgz",
"dev": true
"dev": true,
"dependencies": {
"mime-db": {
"version": "1.12.0",
"from": "mime-db@>=1.12.0 <1.13.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz",
"dev": true
},
"mime-types": {
"version": "2.0.14",
"from": "mime-types@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz",
"dev": true
}
}
},
"require-directory": {
"version": "2.1.1",
@ -5983,6 +6017,11 @@
"from": "sax@>=0.6.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.2.tgz"
},
"seek-bzip": {
"version": "1.0.5",
"from": "seek-bzip@>=1.0.5 <2.0.0",
"resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz"
},
"semver": {
"version": "5.1.1",
"from": "semver@>=5.1.0 <6.0.0",
@ -6728,6 +6767,28 @@
"resolved": "https://registry.npmjs.org/typical/-/typical-2.6.0.tgz",
"dev": true
},
"udif": {
"version": "0.7.0",
"from": "udif@latest",
"resolved": "https://registry.npmjs.org/udif/-/udif-0.7.0.tgz",
"dependencies": {
"base64-js": {
"version": "1.1.2",
"from": "base64-js@1.1.2",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.1.2.tgz"
},
"plist": {
"version": "2.0.1",
"from": "plist@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/plist/-/plist-2.0.1.tgz"
},
"xmlbuilder": {
"version": "8.2.2",
"from": "xmlbuilder@8.2.2",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz"
}
}
},
"uglify-js": {
"version": "2.8.13",
"from": "uglify-js@>=2.6.0 <3.0.0",
@ -7046,8 +7107,7 @@
"xmldom": {
"version": "0.1.27",
"from": "xmldom@>=0.1.0 <0.2.0",
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz"
},
"xregexp": {
"version": "2.0.0",

View File

@ -69,7 +69,6 @@
"angular-ui-bootstrap": "^2.5.0",
"angular-ui-router": "^0.4.2",
"arch": "^2.1.0",
"archive-type": "^3.2.0",
"bluebird": "^3.0.5",
"bootstrap-sass": "^3.3.5",
"chalk": "^1.1.3",
@ -79,6 +78,7 @@
"electron-is-running-in-asar": "^1.0.0",
"etcher-image-write": "^9.0.1",
"etcher-latest-version": "^1.0.0",
"file-type": "^4.1.0",
"flat": "^2.0.1",
"flexboxgrid": "^6.3.0",
"immutable": "^3.8.1",
@ -86,6 +86,7 @@
"lodash": "^4.5.1",
"lodash-deep": "^2.0.0",
"lzma-native": "^1.5.2",
"mime-types": "^2.1.15",
"mountutils": "^1.0.3",
"node-ipc": "^8.9.2",
"node-stream-zip": "^1.3.4",
@ -98,6 +99,7 @@
"semver": "^5.1.0",
"sudo-prompt": "^6.1.0",
"trackjs": "^2.1.16",
"udif": "^0.7.0",
"unbzip2-stream": "^1.0.11",
"yargs": "^4.6.0",
"yauzl": "^2.6.0"

View File

@ -23,7 +23,7 @@ describe('Browser: SupportedFormats', function() {
it('should return the supported compressed extensions', function() {
const extensions = SupportedFormatsModel.getCompressedExtensions();
m.chai.expect(extensions).to.deep.equal([ 'gz', 'bz2', 'xz' ]);
m.chai.expect(extensions).to.deep.equal([ 'gz', 'bz2', 'xz', 'dmg' ]);
});
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,104 @@
/*
* Copyright 2016 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const m = require('mochainon');
const fs = require('fs');
const path = require('path');
const DATA_PATH = path.join(__dirname, 'data');
const IMAGES_PATH = path.join(DATA_PATH, 'images');
const DMG_PATH = path.join(DATA_PATH, 'dmg');
const imageStream = require('../../lib/image-stream/index');
const tester = require('./tester');
describe('ImageStream: DMG', function() {
this.timeout(20000);
context('compressed', function() {
describe('.getFromFilePath()', function() {
describe('given an dmg image', function() {
tester.extractFromFilePath(
path.join(DMG_PATH, 'zlib-compressed.dmg'),
path.join(IMAGES_PATH, 'zlib-compressed.img'));
});
});
describe('.getImageMetadata()', function() {
it('should return the correct metadata', function() {
const image = path.join(DMG_PATH, 'zlib-compressed.dmg');
const uncompressedSize = fs.statSync(path.join(IMAGES_PATH, 'zlib-compressed.img')).size;
const compressedSize = fs.statSync(image).size;
return imageStream.getImageMetadata(image).then((metadata) => {
m.chai.expect(metadata).to.deep.equal({
size: {
original: compressedSize,
final: {
estimation: false,
value: uncompressedSize
}
}
});
});
});
});
});
context('uncompressed', function() {
describe('.getFromFilePath()', function() {
describe('given an dmg image', function() {
tester.extractFromFilePath(
path.join(DMG_PATH, 'raw.dmg'),
path.join(IMAGES_PATH, 'raw.img'));
});
});
describe('.getImageMetadata()', function() {
it('should return the correct metadata', function() {
const image = path.join(DMG_PATH, 'raw.dmg');
const uncompressedSize = fs.statSync(path.join(IMAGES_PATH, 'raw.img')).size;
const compressedSize = fs.statSync(image).size;
return imageStream.getImageMetadata(image).then((metadata) => {
m.chai.expect(metadata).to.deep.equal({
size: {
original: compressedSize,
final: {
estimation: false,
value: uncompressedSize
}
}
});
});
});
});
});
});

View File

@ -26,44 +26,53 @@ describe('ImageStream: Utils', function() {
describe('.getArchiveMimeType()', function() {
it('should resolve application/x-bzip2 for a bz2 archive', function(done) {
it('should resolve application/x-bzip2 for a bz2 archive', function() {
const file = path.join(DATA_PATH, 'bz2', 'raspberrypi.img.bz2');
utils.getArchiveMimeType(file).then((type) => {
return utils.getArchiveMimeType(file).then((type) => {
m.chai.expect(type).to.equal('application/x-bzip2');
done();
}).catch(done);
});
});
it('should resolve application/x-xz for a xz archive', function(done) {
it('should resolve application/x-xz for a xz archive', function() {
const file = path.join(DATA_PATH, 'xz', 'raspberrypi.img.xz');
utils.getArchiveMimeType(file).then((type) => {
return utils.getArchiveMimeType(file).then((type) => {
m.chai.expect(type).to.equal('application/x-xz');
done();
}).catch(done);
});
});
it('should resolve application/gzip for a gz archive', function(done) {
it('should resolve application/gzip for a gz archive', function() {
const file = path.join(DATA_PATH, 'gz', 'raspberrypi.img.gz');
utils.getArchiveMimeType(file).then((type) => {
return utils.getArchiveMimeType(file).then((type) => {
m.chai.expect(type).to.equal('application/gzip');
done();
}).catch(done);
});
});
it('should resolve application/zip for a zip archive', function(done) {
it('should resolve application/zip for a zip archive', function() {
const file = path.join(DATA_PATH, 'zip', 'zip-directory-rpi-only.zip');
utils.getArchiveMimeType(file).then((type) => {
return utils.getArchiveMimeType(file).then((type) => {
m.chai.expect(type).to.equal('application/zip');
done();
}).catch(done);
});
});
it('should resolve application/octet-stream for an uncompress image', function(done) {
it('should resolve application/octet-stream for an uncompressed image', function() {
const file = path.join(DATA_PATH, 'images', 'raspberrypi.img');
utils.getArchiveMimeType(file).then((type) => {
return utils.getArchiveMimeType(file).then((type) => {
m.chai.expect(type).to.equal('application/octet-stream');
done();
}).catch(done);
});
});
it('should resolve application/x-apple-diskimage for a compressed Apple disk image', function() {
const file = path.join(DATA_PATH, 'dmg', 'zlib-compressed.dmg');
return utils.getArchiveMimeType(file).then((type) => {
m.chai.expect(type).to.equal('application/x-apple-diskimage');
});
});
it('should resolve application/x-apple-diskimage for an uncompressed Apple disk image', function() {
const file = path.join(DATA_PATH, 'dmg', 'raw.dmg');
return utils.getArchiveMimeType(file).then((type) => {
m.chai.expect(type).to.equal('application/x-apple-diskimage');
});
});
});