mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-22 02:36:32 +00:00
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:
parent
2411a7677d
commit
b3b928ae4f
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -30,3 +30,4 @@ Makefile text
|
||||
*.png binary
|
||||
*.xz binary
|
||||
*.zip binary
|
||||
*.dmg binary
|
||||
|
@ -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
|
||||
|
@ -37,6 +37,10 @@ module.exports = [
|
||||
extension: 'xz',
|
||||
type: 'compressed'
|
||||
},
|
||||
{
|
||||
extension: 'dmg',
|
||||
type: 'compressed'
|
||||
},
|
||||
{
|
||||
extension: 'img',
|
||||
type: 'image'
|
||||
|
@ -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
98
npm-shrinkwrap.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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' ]);
|
||||
});
|
||||
|
||||
});
|
||||
|
BIN
tests/image-stream/data/dmg/raw.dmg
Normal file
BIN
tests/image-stream/data/dmg/raw.dmg
Normal file
Binary file not shown.
BIN
tests/image-stream/data/dmg/zlib-compressed.dmg
Normal file
BIN
tests/image-stream/data/dmg/zlib-compressed.dmg
Normal file
Binary file not shown.
BIN
tests/image-stream/data/images/raw.img
Normal file
BIN
tests/image-stream/data/images/raw.img
Normal file
Binary file not shown.
BIN
tests/image-stream/data/images/zlib-compressed.img
Normal file
BIN
tests/image-stream/data/images/zlib-compressed.img
Normal file
Binary file not shown.
104
tests/image-stream/dmg.spec.js
Normal file
104
tests/image-stream/dmg.spec.js
Normal 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
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user