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 *.png binary
*.xz binary *.xz binary
*.zip binary *.zip binary
*.dmg binary

View File

@ -23,6 +23,7 @@ const lzma = Bluebird.promisifyAll(require('lzma-native'));
const zlib = require('zlib'); const zlib = require('zlib');
const unbzip2Stream = require('unbzip2-stream'); const unbzip2Stream = require('unbzip2-stream');
const gzip = require('./gzip'); const gzip = require('./gzip');
const udif = Bluebird.promisifyAll(require('udif'));
const archive = require('./archive'); const archive = require('./archive');
const zipArchiveHooks = require('./archive-hooks/zip'); 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 * @summary Handle ZIP compressed images
* @function * @function

View File

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

View File

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

96
npm-shrinkwrap.json generated
View File

@ -146,6 +146,11 @@
"from": "any-promise@>=1.1.0 <2.0.0", "from": "any-promise@>=1.1.0 <2.0.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz" "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": { "aproba": {
"version": "1.1.1", "version": "1.1.1",
"from": "aproba@>=1.0.3 <2.0.0", "from": "aproba@>=1.0.3 <2.0.0",
@ -157,11 +162,6 @@
"from": "arch@2.1.0", "from": "arch@2.1.0",
"resolved": "https://registry.npmjs.org/arch/-/arch-2.1.0.tgz" "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": { "archiver": {
"version": "1.3.0", "version": "1.3.0",
"from": "archiver@>=1.0.0 <2.0.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", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
"dev": true "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": { "bluebird": {
"version": "3.4.1", "version": "3.4.1",
"from": "bluebird@>=3.0.5 <4.0.0", "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", "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-2.0.5.tgz",
"dev": true "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": { "commoner": {
"version": "0.10.8", "version": "0.10.8",
"from": "commoner@>=0.10.3 <0.11.0", "from": "commoner@>=0.10.3 <0.11.0",
@ -2439,9 +2449,9 @@
} }
}, },
"file-type": { "file-type": {
"version": "3.9.0", "version": "4.1.0",
"from": "file-type@>=3.1.0 <4.0.0", "from": "file-type@latest",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz" "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.1.0.tgz"
}, },
"file-uri-to-path": { "file-uri-to-path": {
"version": "0.0.2", "version": "0.0.2",
@ -2565,6 +2575,18 @@
"from": "async@>=0.9.0 <0.10.0", "from": "async@>=0.9.0 <0.10.0",
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
"dev": true "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 "dev": true
}, },
"mime-db": { "mime-db": {
"version": "1.12.0", "version": "1.27.0",
"from": "mime-db@>=1.12.0 <1.13.0", "from": "mime-db@>=1.27.0 <1.28.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz"
"dev": true
}, },
"mime-types": { "mime-types": {
"version": "2.0.14", "version": "2.1.15",
"from": "mime-types@>=2.0.1 <2.1.0", "from": "mime-types@latest",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz"
"dev": true
}, },
"minimalistic-assert": { "minimalistic-assert": {
"version": "1.0.0", "version": "1.0.0",
@ -5746,8 +5766,22 @@
"version": "2.55.0", "version": "2.55.0",
"from": "request@2.55.0", "from": "request@2.55.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.55.0.tgz", "resolved": "https://registry.npmjs.org/request/-/request-2.55.0.tgz",
"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 "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": { "require-directory": {
"version": "2.1.1", "version": "2.1.1",
"from": "require-directory@>=2.1.1 <3.0.0", "from": "require-directory@>=2.1.1 <3.0.0",
@ -5983,6 +6017,11 @@
"from": "sax@>=0.6.0", "from": "sax@>=0.6.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.2.tgz" "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": { "semver": {
"version": "5.1.1", "version": "5.1.1",
"from": "semver@>=5.1.0 <6.0.0", "from": "semver@>=5.1.0 <6.0.0",
@ -6728,6 +6767,28 @@
"resolved": "https://registry.npmjs.org/typical/-/typical-2.6.0.tgz", "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.0.tgz",
"dev": true "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": { "uglify-js": {
"version": "2.8.13", "version": "2.8.13",
"from": "uglify-js@>=2.6.0 <3.0.0", "from": "uglify-js@>=2.6.0 <3.0.0",
@ -7046,8 +7107,7 @@
"xmldom": { "xmldom": {
"version": "0.1.27", "version": "0.1.27",
"from": "xmldom@>=0.1.0 <0.2.0", "from": "xmldom@>=0.1.0 <0.2.0",
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz"
"dev": true
}, },
"xregexp": { "xregexp": {
"version": "2.0.0", "version": "2.0.0",

View File

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

View File

@ -23,7 +23,7 @@ describe('Browser: SupportedFormats', function() {
it('should return the supported compressed extensions', function() { it('should return the supported compressed extensions', function() {
const extensions = SupportedFormatsModel.getCompressedExtensions(); 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() { 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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');
});
}); });
}); });