diff --git a/.gitattributes b/.gitattributes index d590a514..0d117874 100644 --- a/.gitattributes +++ b/.gitattributes @@ -39,3 +39,5 @@ Makefile text *.zip binary diff=hex *.dmg binary diff=hex *.rpi-sdcard binary diff=hex +*.foo binary diff=hex +xz-without-extension binary diff=hex diff --git a/lib/image-stream/index.js b/lib/image-stream/index.js index 402f1d68..9aa36d96 100644 --- a/lib/image-stream/index.js +++ b/lib/image-stream/index.js @@ -20,7 +20,7 @@ const _ = require('lodash'); const Bluebird = require('bluebird'); const fs = Bluebird.promisifyAll(require('fs')); const stream = require('stream'); -const utils = require('./utils'); +const mime = require('./mime'); const handlers = require('./handlers'); const supportedFileTypes = require('./supported'); const errors = require('../shared/errors'); @@ -76,9 +76,8 @@ exports.getFromFilePath = (file) => { }); } - return utils.getArchiveMimeType(file).then((type) => { - const MIME_TYPE_RAW_IMAGE = 'application/octet-stream'; - const mimeType = _.has(handlers, type) ? type : MIME_TYPE_RAW_IMAGE; + return mime.getMimeTypeFromFileName(file).then((type) => { + const mimeType = _.has(handlers, type) ? type : mime.DEFAULT_MIME_TYPE; return _.invoke(handlers, mimeType, file, { size: fileStats.size }); diff --git a/lib/image-stream/mime.js b/lib/image-stream/mime.js new file mode 100644 index 00000000..bc7d2e6f --- /dev/null +++ b/lib/image-stream/mime.js @@ -0,0 +1,65 @@ +/* + * Copyright 2017 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 _ = require('lodash'); +const Bluebird = require('bluebird'); +const fileType = require('file-type'); +const mime = require('mime-types'); +const fs = require('fs'); + +/** + * @summary The default MIME type + * @type {String} + * @constant + */ +exports.DEFAULT_MIME_TYPE = 'application/octet-stream'; + +/** + * @summary Get file's mime type, by reading the initial 262 bytes if necessary + * @function + * @public + * + * @param {String} filename - file path + * @fulfil {String} - mime type + * @returns {Promise} + * + * @example + * mime.getMimeTypeFromFileName('path/to/raspberrypi.img.gz').then((mimeType) => { + * console.log(mimeType); + * }); + */ +exports.getMimeTypeFromFileName = (filename) => { + const mimeType = mime.lookup(filename); + + 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 buffer = Buffer.alloc(FILE_TYPE_ID_BYTES); + + return fs.readAsync(fileDescriptor, buffer, BUFFER_START, FILE_TYPE_ID_BYTES, null).then(() => { + return _.get(fileType(buffer), [ 'mime' ], exports.DEFAULT_MIME_TYPE); + }); + }); +}; diff --git a/lib/image-stream/utils.js b/lib/image-stream/utils.js index f43d7a8c..fa896a11 100644 --- a/lib/image-stream/utils.js +++ b/lib/image-stream/utils.js @@ -16,48 +16,7 @@ 'use strict'; -const _ = require('lodash'); const Bluebird = require('bluebird'); -const fileType = require('file-type'); -const mime = require('mime-types'); -const fs = require('fs'); - -/** - * @summary Get archive mime type - * @function - * @public - * - * @param {String} filename - file path - * @fulfil {String} - mime type - * @returns {Promise} - * - * @example - * utils.getArchiveMimeType('path/to/raspberrypi.img.gz').then((mimeType) => { - * console.log(mimeType); - * }); - */ -exports.getArchiveMimeType = (filename) => { - const MIME_TYPE_RAW_IMAGE = 'application/octet-stream'; - const mimeType = mime.lookup(filename); - - 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 buffer = Buffer.alloc(FILE_TYPE_ID_BYTES); - - return fs.readAsync(fileDescriptor, buffer, BUFFER_START, FILE_TYPE_ID_BYTES, null).then(() => { - return _.get(fileType(buffer), [ 'mime' ], MIME_TYPE_RAW_IMAGE); - }); - }); - -}; /** * @summary Extract the data of a readable stream diff --git a/tests/image-stream/data/unrecognized/xz-with-invalid-extension.foo b/tests/image-stream/data/unrecognized/xz-with-invalid-extension.foo new file mode 100644 index 00000000..bf5ff41b Binary files /dev/null and b/tests/image-stream/data/unrecognized/xz-with-invalid-extension.foo differ diff --git a/tests/image-stream/data/unrecognized/xz-without-extension b/tests/image-stream/data/unrecognized/xz-without-extension new file mode 100644 index 00000000..bf5ff41b Binary files /dev/null and b/tests/image-stream/data/unrecognized/xz-without-extension differ diff --git a/tests/image-stream/mime.spec.js b/tests/image-stream/mime.spec.js new file mode 100644 index 00000000..46f0cfe0 --- /dev/null +++ b/tests/image-stream/mime.spec.js @@ -0,0 +1,107 @@ +/* + * Copyright 2017 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 path = require('path'); +const DATA_PATH = path.join(__dirname, 'data'); +const mime = require('../../lib/image-stream/mime'); + +describe('ImageStream: MIME', function() { + + describe('.getMimeTypeFromFileName()', function() { + + it('should resolve application/x-bzip2 for a bz2 archive', function() { + const file = path.join(DATA_PATH, 'bz2', 'etcher-test.img.bz2'); + return mime.getMimeTypeFromFileName(file).then((type) => { + m.chai.expect(type).to.equal('application/x-bzip2'); + }); + }); + + it('should resolve application/x-xz for a xz archive', function() { + const file = path.join(DATA_PATH, 'xz', 'etcher-test.img.xz'); + return mime.getMimeTypeFromFileName(file).then((type) => { + m.chai.expect(type).to.equal('application/x-xz'); + }); + }); + + it('should resolve application/gzip for a gz archive', function() { + const file = path.join(DATA_PATH, 'gz', 'etcher-test.img.gz'); + return mime.getMimeTypeFromFileName(file).then((type) => { + m.chai.expect(type).to.equal('application/gzip'); + }); + }); + + it('should resolve application/zip for a zip archive', function() { + const file = path.join(DATA_PATH, 'zip', 'zip-directory-etcher-only.zip'); + return mime.getMimeTypeFromFileName(file).then((type) => { + m.chai.expect(type).to.equal('application/zip'); + }); + }); + + it('should resolve application/octet-stream for an uncompressed image', function() { + const file = path.join(DATA_PATH, 'images', 'etcher-test.img'); + return mime.getMimeTypeFromFileName(file).then((type) => { + m.chai.expect(type).to.equal('application/octet-stream'); + }); + }); + + it('should resolve application/x-iso9660-image for an uncompressed iso', function() { + const file = path.join(DATA_PATH, 'images', 'etcher-test.iso'); + return mime.getMimeTypeFromFileName(file).then((type) => { + m.chai.expect(type).to.equal('application/x-iso9660-image'); + }); + }); + + it('should resolve application/x-apple-diskimage for a compressed Apple disk image', function() { + const file = path.join(DATA_PATH, 'dmg', 'etcher-test-zlib.dmg'); + return mime.getMimeTypeFromFileName(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', 'etcher-test-raw.dmg'); + return mime.getMimeTypeFromFileName(file).then((type) => { + m.chai.expect(type).to.equal('application/x-apple-diskimage'); + }); + }); + + it('should resolve application/octet-stream for an unrecognized file type', function() { + const file = path.join(DATA_PATH, 'unrecognized', 'random.rpi-sdcard'); + return mime.getMimeTypeFromFileName(file).then((type) => { + m.chai.expect(type).to.equal('application/octet-stream'); + }); + }); + + it('should resolve the correct MIME type given an invalid extension', function() { + const file = path.join(DATA_PATH, 'unrecognized', 'xz-with-invalid-extension.foo'); + return mime.getMimeTypeFromFileName(file).then((type) => { + m.chai.expect(type).to.equal('application/x-xz'); + }); + }); + + it('should resolve the correct MIME type given no extension', function() { + const file = path.join(DATA_PATH, 'unrecognized', 'xz-without-extension'); + return mime.getMimeTypeFromFileName(file).then((type) => { + m.chai.expect(type).to.equal('application/x-xz'); + }); + }); + + }); + +}); diff --git a/tests/image-stream/utils.spec.js b/tests/image-stream/utils.spec.js index 87afeb68..b4a9b800 100644 --- a/tests/image-stream/utils.spec.js +++ b/tests/image-stream/utils.spec.js @@ -17,80 +17,11 @@ 'use strict'; const m = require('mochainon'); -const path = require('path'); const StreamReadable = require('stream').Readable; -const DATA_PATH = path.join(__dirname, 'data'); const utils = require('../../lib/image-stream/utils'); describe('ImageStream: Utils', function() { - describe('.getArchiveMimeType()', function() { - - it('should resolve application/x-bzip2 for a bz2 archive', function() { - const file = path.join(DATA_PATH, 'bz2', 'etcher-test.img.bz2'); - return utils.getArchiveMimeType(file).then((type) => { - m.chai.expect(type).to.equal('application/x-bzip2'); - }); - }); - - it('should resolve application/x-xz for a xz archive', function() { - const file = path.join(DATA_PATH, 'xz', 'etcher-test.img.xz'); - return utils.getArchiveMimeType(file).then((type) => { - m.chai.expect(type).to.equal('application/x-xz'); - }); - }); - - it('should resolve application/gzip for a gz archive', function() { - const file = path.join(DATA_PATH, 'gz', 'etcher-test.img.gz'); - return utils.getArchiveMimeType(file).then((type) => { - m.chai.expect(type).to.equal('application/gzip'); - }); - }); - - it('should resolve application/zip for a zip archive', function() { - const file = path.join(DATA_PATH, 'zip', 'zip-directory-etcher-only.zip'); - return utils.getArchiveMimeType(file).then((type) => { - m.chai.expect(type).to.equal('application/zip'); - }); - }); - - it('should resolve application/octet-stream for an uncompressed image', function() { - const file = path.join(DATA_PATH, 'images', 'etcher-test.img'); - return utils.getArchiveMimeType(file).then((type) => { - m.chai.expect(type).to.equal('application/octet-stream'); - }); - }); - - it('should resolve application/x-iso9660-image for an uncompressed iso', function() { - const file = path.join(DATA_PATH, 'images', 'etcher-test.iso'); - return utils.getArchiveMimeType(file).then((type) => { - m.chai.expect(type).to.equal('application/x-iso9660-image'); - }); - }); - - it('should resolve application/x-apple-diskimage for a compressed Apple disk image', function() { - const file = path.join(DATA_PATH, 'dmg', 'etcher-test-zlib.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', 'etcher-test-raw.dmg'); - return utils.getArchiveMimeType(file).then((type) => { - m.chai.expect(type).to.equal('application/x-apple-diskimage'); - }); - }); - - it('should resolve application/octet-stream for an unrecognized file type', function() { - const file = path.join(DATA_PATH, 'unrecognized', 'random.rpi-sdcard'); - return utils.getArchiveMimeType(file).then((type) => { - m.chai.expect(type).to.equal('application/octet-stream'); - }); - }); - - }); - describe('.extractStream()', function() { describe('given a stream that emits data', function() {