diff --git a/.gitattributes b/.gitattributes index 69f141f2..8d2169d9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -30,3 +30,4 @@ Makefile text *.png binary *.xz binary *.zip binary +*.dmg binary diff --git a/lib/image-stream/handlers.js b/lib/image-stream/handlers.js index d46673fb..fbe2b9c3 100644 --- a/lib/image-stream/handlers.js +++ b/lib/image-stream/handlers.js @@ -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 diff --git a/lib/image-stream/supported.js b/lib/image-stream/supported.js index 18d66c36..144f196f 100644 --- a/lib/image-stream/supported.js +++ b/lib/image-stream/supported.js @@ -37,6 +37,10 @@ module.exports = [ extension: 'xz', type: 'compressed' }, + { + extension: 'dmg', + type: 'compressed' + }, { extension: 'img', type: 'image' diff --git a/lib/image-stream/utils.js b/lib/image-stream/utils.js index 5090c5a3..81bfc06c 100644 --- a/lib/image-stream/utils.js +++ b/lib/image-stream/utils.js @@ -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'); }); }); + }; /** diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index f2e3c2dc..d064970e 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -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", diff --git a/package.json b/package.json index e737917c..cd827d61 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/tests/gui/models/supported-formats.spec.js b/tests/gui/models/supported-formats.spec.js index 89865643..5ba2f63d 100644 --- a/tests/gui/models/supported-formats.spec.js +++ b/tests/gui/models/supported-formats.spec.js @@ -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' ]); }); }); diff --git a/tests/image-stream/data/dmg/raw.dmg b/tests/image-stream/data/dmg/raw.dmg new file mode 100644 index 00000000..560d1943 Binary files /dev/null and b/tests/image-stream/data/dmg/raw.dmg differ diff --git a/tests/image-stream/data/dmg/zlib-compressed.dmg b/tests/image-stream/data/dmg/zlib-compressed.dmg new file mode 100644 index 00000000..cac90e9e Binary files /dev/null and b/tests/image-stream/data/dmg/zlib-compressed.dmg differ diff --git a/tests/image-stream/data/images/raw.img b/tests/image-stream/data/images/raw.img new file mode 100644 index 00000000..347f5e13 Binary files /dev/null and b/tests/image-stream/data/images/raw.img differ diff --git a/tests/image-stream/data/images/zlib-compressed.img b/tests/image-stream/data/images/zlib-compressed.img new file mode 100644 index 00000000..ea2b8893 Binary files /dev/null and b/tests/image-stream/data/images/zlib-compressed.img differ diff --git a/tests/image-stream/dmg.spec.js b/tests/image-stream/dmg.spec.js new file mode 100644 index 00000000..1d7c19fa --- /dev/null +++ b/tests/image-stream/dmg.spec.js @@ -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 + } + } + }); + }); + }); + + }); + + }); + +}); diff --git a/tests/image-stream/utils.spec.js b/tests/image-stream/utils.spec.js index c7ea3199..476da520 100644 --- a/tests/image-stream/utils.spec.js +++ b/tests/image-stream/utils.spec.js @@ -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'); + }); }); });