diff --git a/lib/cli/writer.js b/lib/cli/writer.js
index ccc6bff4..cfc13c83 100644
--- a/lib/cli/writer.js
+++ b/lib/cli/writer.js
@@ -17,11 +17,11 @@
'use strict';
const imageWrite = require('etcher-image-write');
-const imageStream = require('etcher-image-stream');
const Bluebird = require('bluebird');
const fs = Bluebird.promisifyAll(require('fs'));
const os = require('os');
const unmount = require('./unmount');
+const imageStream = require('../image-stream');
/**
* @summary Write an image to a disk drive
diff --git a/lib/gui/models/supported-formats.js b/lib/gui/models/supported-formats.js
index c096acb5..e1d7bd41 100644
--- a/lib/gui/models/supported-formats.js
+++ b/lib/gui/models/supported-formats.js
@@ -23,7 +23,7 @@
const angular = require('angular');
const _ = require('lodash');
const path = require('path');
-const imageStream = require('etcher-image-stream');
+const imageStream = require('../../image-stream');
const MODULE_NAME = 'Etcher.Models.SupportedFormats';
const SupportedFormats = angular.module(MODULE_NAME, []);
diff --git a/lib/gui/os/dialog/services/dialog.js b/lib/gui/os/dialog/services/dialog.js
index d9db02fa..663b3bbe 100644
--- a/lib/gui/os/dialog/services/dialog.js
+++ b/lib/gui/os/dialog/services/dialog.js
@@ -17,8 +17,8 @@
'use strict';
const _ = require('lodash');
-const imageStream = require('etcher-image-stream');
const electron = require('electron');
+const imageStream = require('../../../../image-stream');
module.exports = function($q, SupportedFormatsModel) {
diff --git a/lib/image-stream/archive-hooks/zip.js b/lib/image-stream/archive-hooks/zip.js
new file mode 100644
index 00000000..9598254c
--- /dev/null
+++ b/lib/image-stream/archive-hooks/zip.js
@@ -0,0 +1,112 @@
+/*
+ * 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 Bluebird = require('bluebird');
+const _ = require('lodash');
+const StreamZip = require('node-stream-zip');
+const yauzl = Bluebird.promisifyAll(require('yauzl'));
+
+/**
+ * @summary Get all archive entries
+ * @function
+ * @public
+ *
+ * @param {String} archive - archive path
+ * @fulfil {Object[]} - archive entries
+ * @returns {Promise}
+ *
+ * @example
+ * zip.getEntries('path/to/my.zip').then((entries) => {
+ * entries.forEach((entry) => {
+ * console.log(entry.name);
+ * console.log(entry.size);
+ * });
+ * });
+ */
+exports.getEntries = (archive) => {
+ return new Bluebird((resolve, reject) => {
+ const zip = new StreamZip({
+ file: archive,
+ storeEntries: true
+ });
+
+ zip.on('error', reject);
+
+ zip.on('ready', () => {
+ return resolve(_.chain(zip.entries())
+ .omitBy((entry) => {
+ return entry.size === 0;
+ })
+ .map((metadata) => {
+ return {
+ name: metadata.name,
+ size: metadata.size
+ };
+ })
+ .value());
+ });
+ });
+};
+
+/**
+ * @summary Extract a file from an archive
+ * @function
+ * @public
+ *
+ * @param {String} archive - archive path
+ * @param {String[]} entries - archive entries
+ * @param {String} file - archive file
+ * @fulfil {ReadableStream} file
+ * @returns {Promise}
+ *
+ * @example
+ * zip.getEntries('path/to/my.zip').then((entries) => {
+ * return zip.extractFile('path/to/my.zip', entries, 'my/file');
+ * }).then((stream) => {
+ * stream.pipe('...');
+ * });
+ */
+exports.extractFile = (archive, entries, file) => {
+ return new Bluebird((resolve, reject) => {
+ if (!_.find(entries, {
+ name: file
+ })) {
+ throw new Error(`Invalid entry: ${file}`);
+ }
+
+ yauzl.openAsync(archive, {
+ lazyEntries: true
+ }).then((zipfile) => {
+ zipfile.readEntry();
+
+ zipfile.on('entry', (entry) => {
+ if (entry.fileName !== file) {
+ return zipfile.readEntry();
+ }
+
+ zipfile.openReadStream(entry, (error, readStream) => {
+ if (error) {
+ return reject(error);
+ }
+
+ return resolve(readStream);
+ });
+ });
+ }).catch(reject);
+ });
+};
diff --git a/lib/image-stream/archive.js b/lib/image-stream/archive.js
new file mode 100644
index 00000000..1f48a320
--- /dev/null
+++ b/lib/image-stream/archive.js
@@ -0,0 +1,198 @@
+/*
+ * 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 path = require('path');
+const Bluebird = require('bluebird');
+const rindle = require('rindle');
+const _ = require('lodash');
+const PassThroughStream = require('stream').PassThrough;
+const supportedFileTypes = require('./supported');
+
+/**
+ * @summary Archive metadata base path
+ * @constant
+ * @private
+ * @type {String}
+ */
+const ARCHIVE_METADATA_BASE_PATH = '.meta';
+
+/**
+ * @summary Image extensions
+ * @constant
+ * @private
+ * @type {String[]}
+ */
+const IMAGE_EXTENSIONS = _.reduce(supportedFileTypes, (accumulator, file) => {
+ if (file.type === 'image') {
+ accumulator.push(file.extension);
+ }
+
+ return accumulator;
+}, []);
+
+/**
+ * @summary Extract entry by path
+ * @function
+ * @private
+ *
+ * @param {String} archive - archive
+ * @param {String} filePath - entry file path
+ * @param {Object} options - options
+ * @param {Object} options.hooks - archive hooks
+ * @param {Object[]} options.entries - archive entries
+ * @param {*} [options.default] - entry default value
+ * @fulfil {*} contents
+ * @returns {Promise}
+ *
+ * extractEntryByPath('my/archive.zip', '_info/logo.svg', {
+ * hooks: { ... },
+ * entries: [ ... ],
+ * default: ''
+ * }).then((contents) => {
+ * console.log(contents);
+ * });
+ */
+const extractEntryByPath = (archive, filePath, options) => {
+ const fileEntry = _.find(options.entries, (entry) => {
+ return _.chain(entry.name)
+ .split('/')
+ .tail()
+ .join('/')
+ .value() === filePath;
+ });
+
+ if (!fileEntry) {
+ return Bluebird.resolve(options.default);
+ }
+
+ return options.hooks.extractFile(archive, options.entries, fileEntry.name)
+ .then(rindle.extract);
+};
+
+/**
+ * @summary Extract archive metadata
+ * @function
+ * @private
+ *
+ * @param {String} archive - archive
+ * @param {String} basePath - metadata base path
+ * @param {Object} options - options
+ * @param {Object[]} options.entries - archive entries
+ * @param {Object} options.hooks - archive hooks
+ * @fulfil {Object} - metadata
+ * @returns {Promise}
+ *
+ * extractArchiveMetadata('my/archive.zip', '.meta', {
+ * hooks: { ... },
+ * entries: [ ... ]
+ * }).then((metadata) => {
+ * console.log(metadata);
+ * });
+ */
+const extractArchiveMetadata = (archive, basePath, options) => {
+ return Bluebird.props({
+ logo: extractEntryByPath(archive, `${basePath}/logo.svg`, options),
+ instructions: extractEntryByPath(archive, `${basePath}/instructions.markdown`, options),
+ bmap: extractEntryByPath(archive, `${basePath}/image.bmap`, options),
+ manifest: _.attempt(() => {
+ return extractEntryByPath(archive, `${basePath}/manifest.json`, {
+ hooks: options.hooks,
+ entries: options.entries,
+ default: '{}'
+ }).then((manifest) => {
+ try {
+ return JSON.parse(manifest);
+ } catch (parseError) {
+ const error = new Error('Invalid archive manifest.json');
+ error.description = 'The archive manifest.json file is not valid JSON.';
+ throw error;
+ }
+ });
+ })
+ }).then((results) => {
+ return {
+ name: results.manifest.name,
+ version: results.manifest.version,
+ url: results.manifest.url,
+ supportUrl: results.manifest.supportUrl,
+ releaseNotesUrl: results.manifest.releaseNotesUrl,
+ checksumType: results.manifest.checksumType,
+ checksum: results.manifest.checksum,
+ bytesToZeroOutFromTheBeginning: results.manifest.bytesToZeroOutFromTheBeginning,
+ recommendedDriveSize: results.manifest.recommendedDriveSize,
+ logo: results.logo,
+ bmap: results.bmap,
+ instructions: results.instructions
+ };
+ });
+};
+
+/**
+ * @summary Extract image from archive
+ * @function
+ * @public
+ *
+ * @param {String} archive - archive path
+ * @param {Object} hooks - archive hooks
+ * @param {Function} hooks.getEntries - get entries hook
+ * @param {Function} hooks.extractFile - extract file hook
+ * @fulfil {Object} image metadata
+ * @returns {Promise}
+ *
+ * @example
+ * archive.extractImage('path/to/my/archive.zip', {
+ * getEntries: (archive) => {
+ * return [ ..., ..., ... ];
+ * },
+ * extractFile: (archive, entries, file) => {
+ * ...
+ * }
+ * }).then((image) => {
+ * image.stream.pipe(image.transform).pipe(...);
+ * });
+ */
+exports.extractImage = (archive, hooks) => {
+ return hooks.getEntries(archive).then((entries) => {
+
+ const imageEntries = _.filter(entries, (entry) => {
+ const extension = path.extname(entry.name).slice(1);
+ return _.includes(IMAGE_EXTENSIONS, extension);
+ });
+
+ if (imageEntries.length !== 1) {
+ const error = new Error('Invalid archive image');
+ error.description = 'The archive image should contain one and only one top image file.';
+ throw error;
+ }
+
+ const imageEntry = _.first(imageEntries);
+
+ return Bluebird.props({
+ imageStream: hooks.extractFile(archive, entries, imageEntry.name),
+ metadata: extractArchiveMetadata(archive, ARCHIVE_METADATA_BASE_PATH, {
+ entries,
+ hooks
+ })
+ }).then((results) => {
+ results.metadata.stream = results.imageStream;
+ results.metadata.size = imageEntry.size;
+ results.metadata.transform = new PassThroughStream();
+ return results.metadata;
+ });
+ });
+};
diff --git a/lib/image-stream/handlers.js b/lib/image-stream/handlers.js
new file mode 100644
index 00000000..e64e0149
--- /dev/null
+++ b/lib/image-stream/handlers.js
@@ -0,0 +1,130 @@
+/*
+ * 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 Bluebird = require('bluebird');
+const fs = Bluebird.promisifyAll(require('fs'));
+const PassThroughStream = require('stream').PassThrough;
+const lzma = Bluebird.promisifyAll(require('lzma-native'));
+const zlib = require('zlib');
+const unbzip2Stream = require('unbzip2-stream');
+const gzipUncompressedSize = Bluebird.promisifyAll(require('gzip-uncompressed-size'));
+const archive = require('./archive');
+const zipArchiveHooks = require('./archive-hooks/zip');
+
+/**
+ * @summary Image handlers
+ * @namespace handlers
+ * @public
+ */
+module.exports = {
+
+ /**
+ * @summary Handle BZ2 compressed images
+ * @function
+ * @public
+ * @memberof handlers
+ *
+ * @param {String} file - file path
+ * @fulfil {Object} - image metadata
+ * @returns {Promise}
+ */
+ 'application/x-bzip2': (file) => {
+ return Bluebird.props({
+ stream: fs.createReadStream(file),
+ size: fs.statAsync(file).get('size'),
+ transform: Bluebird.resolve(unbzip2Stream())
+ });
+ },
+
+ /**
+ * @summary Handle GZ compressed images
+ * @function
+ * @public
+ * @memberof handlers
+ *
+ * @param {String} file - file path
+ * @fulfil {Object} - image metadata
+ * @returns {Promise}
+ */
+ 'application/gzip': (file) => {
+ return Bluebird.props({
+ stream: fs.createReadStream(file),
+ size: fs.statAsync(file).get('size'),
+ estimatedUncompressedSize: gzipUncompressedSize.fromFileAsync(file),
+ transform: Bluebird.resolve(zlib.createGunzip())
+ });
+ },
+
+ /**
+ * @summary Handle XZ compressed images
+ * @function
+ * @public
+ * @memberof handlers
+ *
+ * @param {String} file - file path
+ * @fulfil {Object} - image metadata
+ * @returns {Promise}
+ */
+ 'application/x-xz': (file) => {
+ return fs.openAsync(file, 'r').then((fileDescriptor) => {
+ return lzma.parseFileIndexFDAsync(fileDescriptor).tap(() => {
+ return fs.closeAsync(fileDescriptor);
+ });
+ }).then((metadata) => {
+ return {
+ stream: fs.createReadStream(file)
+ .pipe(lzma.createDecompressor()),
+ size: metadata.uncompressedSize,
+ transform: new PassThroughStream()
+ };
+ });
+ },
+
+ /**
+ * @summary Handle ZIP compressed images
+ * @function
+ * @public
+ * @memberof handlers
+ *
+ * @param {String} file - file path
+ * @fulfil {Object} - image metadata
+ * @returns {Promise}
+ */
+ 'application/zip': (file) => {
+ return archive.extractImage(file, zipArchiveHooks);
+ },
+
+ /**
+ * @summary Handle plain uncompressed images
+ * @function
+ * @public
+ * @memberof handlers
+ *
+ * @param {String} file - file path
+ * @fulfil {Object} - image metadata
+ * @returns {Promise}
+ */
+ 'application/octet-stream': (file) => {
+ return Bluebird.props({
+ stream: fs.createReadStream(file),
+ size: fs.statAsync(file).get('size'),
+ transform: Bluebird.resolve(new PassThroughStream())
+ });
+ }
+
+};
diff --git a/lib/image-stream/index.js b/lib/image-stream/index.js
new file mode 100644
index 00000000..faa60a93
--- /dev/null
+++ b/lib/image-stream/index.js
@@ -0,0 +1,120 @@
+/*
+ * 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 _ = require('lodash');
+const Bluebird = require('bluebird');
+const utils = require('./utils');
+const handlers = require('./handlers');
+const supportedFileTypes = require('./supported');
+
+/**
+ * @summary Get an image stream from a file
+ * @function
+ * @public
+ *
+ * @description
+ * This function resolves an object containing the following properties:
+ *
+ * - `Number size`: The input file size.
+ *
+ * - `ReadableStream stream`: The input file stream.
+ *
+ * - `TransformStream transform`: A transform stream that performs any
+ * needed transformation to get the image out of the source input file
+ * (for example, decompression).
+ *
+ * The purpose of separating the above components is to handle cases like
+ * showing a progress bar when you can't know the final uncompressed size.
+ *
+ * In such case, you can pipe the `stream` through a progress stream using
+ * the input file `size`, and apply the `transform` after the progress stream.
+ *
+ * @param {String} file - file path
+ * @fulfil {Object} - image stream details
+ * @returns {Promise}
+ *
+ * @example
+ * const imageStream = require('./lib/image-stream');
+ *
+ * imageStream.getFromFilePath('path/to/rpi.img.xz').then((image) => {
+ * image.stream
+ * .pipe(image.transform)
+ * .pipe(fs.createWriteStream('/dev/disk2'));
+ * });
+ */
+exports.getFromFilePath = (file) => {
+ return Bluebird.try(() => {
+ const type = utils.getArchiveMimeType(file);
+
+ if (!handlers[type]) {
+ throw new Error('Invalid image');
+ }
+
+ return handlers[type](file);
+ }).then((image) => {
+ return _.omitBy(image, _.isUndefined);
+ });
+};
+
+/**
+ * @summary Get image metadata
+ * @function
+ * @public
+ *
+ * @description
+ * This function is useful to determine the final size of an image
+ * after decompression or any other needed transformation, as well as
+ * other relevent metadata, if any.
+ *
+ * **NOTE:** This function is known to output incorrect size results for
+ * `bzip2`. For this compression format, this function will simply
+ * return the size of the compressed file.
+ *
+ * @param {String} file - file path
+ * @fulfil {Object} - image metadata
+ * @returns {Promise}
+ *
+ * @example
+ * const imageStream = require('./lib/image-stream');
+ *
+ * imageStream.getImageMetadata('path/to/rpi.img.xz').then((metadata) => {
+ * console.log(`The image display name is: ${metada.name}`);
+ * console.log(`The image url is: ${metada.url}`);
+ * console.log(`The image support url is: ${metada.supportUrl}`);
+ * console.log(`The image logo is: ${metada.logo}`);
+ * });
+ */
+exports.getImageMetadata = (file) => {
+ return exports.getFromFilePath(file).then((image) => {
+ return _.omitBy(image, _.isObject);
+ });
+};
+
+/**
+ * @summary Supported file types
+ * @type {String[]}
+ * @public
+ *
+ * @example
+ * const imageStream = require('./lib/image-stream');
+ *
+ * imageStream.supportedFileTypes.forEach((fileType) => {
+ * console.log('Supported file type: ' + fileType.extension);
+ * });
+ */
+exports.supportedFileTypes = supportedFileTypes;
diff --git a/lib/image-stream/supported.js b/lib/image-stream/supported.js
new file mode 100644
index 00000000..18d66c36
--- /dev/null
+++ b/lib/image-stream/supported.js
@@ -0,0 +1,60 @@
+/*
+ * 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';
+
+module.exports = [
+ {
+ extension: 'zip',
+ type: 'archive'
+ },
+ {
+ extension: 'etch',
+ type: 'archive'
+ },
+ {
+ extension: 'gz',
+ type: 'compressed'
+ },
+ {
+ extension: 'bz2',
+ type: 'compressed'
+ },
+ {
+ extension: 'xz',
+ type: 'compressed'
+ },
+ {
+ extension: 'img',
+ type: 'image'
+ },
+ {
+ extension: 'iso',
+ type: 'image'
+ },
+ {
+ extension: 'dsk',
+ type: 'image'
+ },
+ {
+ extension: 'hddimg',
+ type: 'image'
+ },
+ {
+ extension: 'raw',
+ type: 'image'
+ }
+];
diff --git a/lib/image-stream/utils.js b/lib/image-stream/utils.js
new file mode 100644
index 00000000..fef1d62d
--- /dev/null
+++ b/lib/image-stream/utils.js
@@ -0,0 +1,41 @@
+/*
+ * 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 _ = require('lodash');
+const readChunk = require('read-chunk');
+const archiveType = require('archive-type');
+
+/**
+ * @summary Get archive mime type
+ * @function
+ * @public
+ *
+ * @param {String} file - file path
+ * @returns {String} mime type
+ *
+ * @example
+ * utils.getArchiveMimeType('path/to/raspberrypi.img.gz');
+ */
+exports.getArchiveMimeType = (file) => {
+
+ // archive-type only needs the first 261 bytes
+ // See https://github.com/kevva/archive-type
+ const chunk = readChunk.sync(file, 0, 261);
+
+ return _.get(archiveType(chunk), 'mime', 'application/octet-stream');
+};
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index bf1e5a98..7de6e48a 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -309,18 +309,6 @@
"from": "esprima@>=2.6.0 <3.0.0",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.2.tgz"
},
- "etcher-image-stream": {
- "version": "5.1.0",
- "from": "etcher-image-stream@5.1.0",
- "resolved": "https://registry.npmjs.org/etcher-image-stream/-/etcher-image-stream-5.1.0.tgz",
- "dependencies": {
- "yauzl": {
- "version": "2.6.0",
- "from": "yauzl@>=2.6.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.6.0.tgz"
- }
- }
- },
"etcher-image-write": {
"version": "9.0.0",
"from": "etcher-image-write@9.0.0",
@@ -396,9 +384,9 @@
"resolved": "https://registry.npmjs.org/file-tail/-/file-tail-0.3.0.tgz"
},
"file-type": {
- "version": "3.8.0",
+ "version": "3.9.0",
"from": "file-type@>=3.1.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.8.0.tgz"
+ "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz"
},
"find-up": {
"version": "1.1.2",
@@ -611,11 +599,6 @@
"from": "commander@>=2.9.0 <3.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz"
},
- "isarray": {
- "version": "1.0.0",
- "from": "isarray@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
- },
"node-pre-gyp": {
"version": "0.6.29",
"from": "node-pre-gyp@0.6.29",
@@ -1217,11 +1200,6 @@
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz"
}
}
- },
- "readable-stream": {
- "version": "2.1.5",
- "from": "readable-stream@>=2.0.5 <3.0.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz"
}
}
},
@@ -1392,6 +1370,18 @@
"from": "read-pkg-up@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz"
},
+ "readable-stream": {
+ "version": "2.2.2",
+ "from": "readable-stream@>=2.0.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.2.tgz",
+ "dependencies": {
+ "isarray": {
+ "version": "1.0.0",
+ "from": "isarray@>=1.0.0 <1.1.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
+ }
+ }
+ },
"readline2": {
"version": "1.0.1",
"from": "readline2@>=1.0.1 <2.0.0",
@@ -1412,11 +1402,6 @@
"from": "require-main-filename@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz"
},
- "require-uncached": {
- "version": "1.0.2",
- "from": "require-uncached@>=1.0.2 <2.0.0",
- "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.2.tgz"
- },
"resin-cli-form": {
"version": "1.4.1",
"from": "resin-cli-form@>=1.4.1 <2.0.0",
@@ -1605,19 +1590,7 @@
"string-to-stream": {
"version": "1.1.0",
"from": "string-to-stream@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/string-to-stream/-/string-to-stream-1.1.0.tgz",
- "dependencies": {
- "isarray": {
- "version": "1.0.0",
- "from": "isarray@~1.0.0",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
- },
- "readable-stream": {
- "version": "2.1.5",
- "from": "readable-stream@^2.1.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz"
- }
- }
+ "resolved": "https://registry.npmjs.org/string-to-stream/-/string-to-stream-1.1.0.tgz"
},
"string-width": {
"version": "1.0.1",
@@ -1787,6 +1760,11 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz"
}
}
+ },
+ "yauzl": {
+ "version": "2.6.0",
+ "from": "yauzl@2.6.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.6.0.tgz"
}
}
}
diff --git a/package.json b/package.json
index c93655f4..48935fda 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"url": "git@github.com:resin-io/etcher.git"
},
"scripts": {
- "test": "npm run lint && electron-mocha --recursive --renderer tests/gui -R spec && electron-mocha --recursive tests/cli tests/shared tests/child-writer -R spec",
+ "test": "npm run lint && electron-mocha --recursive --renderer tests/gui -R spec && electron-mocha --recursive tests/cli tests/shared tests/child-writer tests/image-stream -R spec",
"sass": "node-sass ./lib/gui/scss/main.scss > ./lib/gui/css/main.css",
"jslint": "eslint lib tests scripts bin versionist.conf.js",
"scsslint": "scss-lint lib/gui/scss",
@@ -67,31 +67,38 @@
"angular-seconds-to-date": "^1.0.0",
"angular-ui-bootstrap": "^1.3.2",
"angular-ui-router": "^0.2.18",
+ "archive-type": "^3.2.0",
"bluebird": "^3.0.5",
"bootstrap-sass": "^3.3.5",
"chalk": "^1.1.3",
"drivelist": "^5.0.6",
"electron-is-running-in-asar": "^1.0.0",
- "etcher-image-stream": "^5.1.0",
"etcher-image-write": "^9.0.0",
"etcher-latest-version": "^1.0.0",
"file-tail": "^0.3.0",
"flexboxgrid": "^6.3.0",
+ "gzip-uncompressed-size": "^1.0.0",
"immutable": "^3.8.1",
"is-elevated": "^1.0.0",
"lodash": "^4.5.1",
+ "lzma-native": "^1.5.2",
"node-ipc": "^8.9.2",
+ "node-stream-zip": "^1.3.4",
+ "read-chunk": "^2.0.0",
"redux": "^3.5.2",
"redux-localstorage": "^0.4.1",
"resin-cli-form": "^1.4.1",
"resin-cli-visuals": "^1.2.8",
+ "rindle": "^1.3.0",
"rx": "^4.1.0",
"semver": "^5.1.0",
"sudo-prompt": "^6.1.0",
"tail": "^1.1.0",
"trackjs": "^2.1.16",
+ "unbzip2-stream": "^1.0.10",
"username": "^2.1.0",
- "yargs": "^4.6.0"
+ "yargs": "^4.6.0",
+ "yauzl": "^2.6.0"
},
"devDependencies": {
"angular-mocks": "^1.4.7",
@@ -102,9 +109,11 @@
"electron-packager": "^7.0.1",
"electron-prebuilt": "1.4.4",
"eslint": "^2.13.1",
+ "file-exists": "^1.0.0",
"jsonfile": "^2.3.1",
"mochainon": "^1.0.0",
"node-sass": "^3.8.0",
+ "tmp": "0.0.31",
"versionist": "^2.1.0"
},
"config": {
diff --git a/tests/image-stream/archive-hooks/zip.spec.js b/tests/image-stream/archive-hooks/zip.spec.js
new file mode 100644
index 00000000..f6242d1f
--- /dev/null
+++ b/tests/image-stream/archive-hooks/zip.spec.js
@@ -0,0 +1,149 @@
+/*
+ * 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 path = require('path');
+const rindle = require('rindle');
+const zipHooks = require('../../../lib/image-stream/archive-hooks/zip');
+const ZIP_PATH = path.join(__dirname, '..', 'data', 'zip');
+
+describe('ImageStream: Archive hooks: ZIP', function() {
+
+ this.timeout(20000);
+
+ describe('.getEntries()', function() {
+
+ describe('given an empty zip', function() {
+
+ beforeEach(function() {
+ this.zip = path.join(ZIP_PATH, 'zip-directory-empty.zip');
+ });
+
+ it('should become an empty array', function(done) {
+ zipHooks.getEntries(this.zip).then((entries) => {
+ m.chai.expect(entries).to.deep.equal([]);
+ }).asCallback(done);
+ });
+
+ });
+
+ describe('given a zip with multiple files in it', function() {
+
+ beforeEach(function() {
+ this.zip = path.join(ZIP_PATH, 'zip-directory-multiple-images.zip');
+ });
+
+ it('should become all entries', function(done) {
+ zipHooks.getEntries(this.zip).then((entries) => {
+ m.chai.expect(entries).to.deep.equal([
+ {
+ name: 'multiple-images/edison-config.img',
+ size: 16777216
+ },
+ {
+ name: 'multiple-images/raspberrypi.img',
+ size: 33554432
+ }
+ ]);
+ }).asCallback(done);
+ });
+
+ });
+
+ describe('given a zip with nested files in it', function() {
+
+ beforeEach(function() {
+ this.zip = path.join(ZIP_PATH, 'zip-directory-nested-misc.zip');
+ });
+
+ it('should become all entries', function(done) {
+ zipHooks.getEntries(this.zip).then((entries) => {
+ m.chai.expect(entries).to.deep.equal([
+ {
+ name: 'zip-directory-nested-misc/foo',
+ size: 4
+ },
+ {
+ name: 'zip-directory-nested-misc/hello/there/bar',
+ size: 4
+ }
+ ]);
+ }).asCallback(done);
+ });
+
+ });
+
+ });
+
+ describe('.extractFile()', function() {
+
+ beforeEach(function() {
+ this.zip = path.join(ZIP_PATH, 'zip-directory-nested-misc.zip');
+ });
+
+ it('should be able to extract a top-level file', function(done) {
+ const fileName = 'zip-directory-nested-misc/foo';
+ zipHooks.getEntries(this.zip).then((entries) => {
+ return zipHooks.extractFile(this.zip, entries, fileName);
+ }).then((stream) => {
+ rindle.extract(stream, function(error, data) {
+ m.chai.expect(error).to.not.exist;
+ m.chai.expect(data).to.equal('foo\n');
+ done();
+ });
+ });
+ });
+
+ it('should be able to extract a nested file', function(done) {
+ const fileName = 'zip-directory-nested-misc/hello/there/bar';
+ zipHooks.getEntries(this.zip).then((entries) => {
+ return zipHooks.extractFile(this.zip, entries, fileName);
+ }).then((stream) => {
+ rindle.extract(stream, function(error, data) {
+ m.chai.expect(error).to.not.exist;
+ m.chai.expect(data).to.equal('bar\n');
+ done();
+ });
+ });
+ });
+
+ it('should throw if the entry does not exist', function(done) {
+ const fileName = 'zip-directory-nested-misc/xxxxxxxxxxxxxxxx';
+ zipHooks.getEntries(this.zip).then((entries) => {
+ return zipHooks.extractFile(this.zip, entries, fileName);
+ }).catch((error) => {
+ m.chai.expect(error).to.be.an.instanceof(Error);
+ m.chai.expect(error.message).to.equal(`Invalid entry: ${fileName}`);
+ done();
+ });
+ });
+
+ it('should throw if the entry is a directory', function(done) {
+ const fileName = 'zip-directory-nested-misc/hello';
+ zipHooks.getEntries(this.zip).then((entries) => {
+ return zipHooks.extractFile(this.zip, entries, fileName);
+ }).catch((error) => {
+ m.chai.expect(error).to.be.an.instanceof(Error);
+ m.chai.expect(error.message).to.equal(`Invalid entry: ${fileName}`);
+ done();
+ });
+ });
+
+ });
+
+});
diff --git a/tests/image-stream/bz2.spec.js b/tests/image-stream/bz2.spec.js
new file mode 100644
index 00000000..34da9f7d
--- /dev/null
+++ b/tests/image-stream/bz2.spec.js
@@ -0,0 +1,58 @@
+/*
+ * 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 BZ2_PATH = path.join(DATA_PATH, 'bz2');
+const imageStream = require('../../lib/image-stream/index');
+const tester = require('./tester');
+
+describe('ImageStream: BZ2', function() {
+
+ this.timeout(20000);
+
+ describe('.getFromFilePath()', function() {
+
+ describe('given a bz2 image', function() {
+ tester.extractFromFilePath(
+ path.join(BZ2_PATH, 'raspberrypi.img.bz2'),
+ path.join(IMAGES_PATH, 'raspberrypi.img'));
+ });
+
+ });
+
+ describe('.getImageMetadata()', function() {
+
+ it('should return the correct metadata', function(done) {
+ const image = path.join(BZ2_PATH, 'raspberrypi.img.bz2');
+ const expectedSize = fs.statSync(image).size;
+
+ imageStream.getImageMetadata(image).then((metadata) => {
+ m.chai.expect(metadata).to.deep.equal({
+ size: expectedSize
+ });
+ done();
+ });
+ });
+
+ });
+
+});
diff --git a/tests/image-stream/data/bz2/raspberrypi.img.bz2 b/tests/image-stream/data/bz2/raspberrypi.img.bz2
new file mode 100644
index 00000000..1b419230
Binary files /dev/null and b/tests/image-stream/data/bz2/raspberrypi.img.bz2 differ
diff --git a/tests/image-stream/data/gz/raspberrypi.img.gz b/tests/image-stream/data/gz/raspberrypi.img.gz
new file mode 100644
index 00000000..7f9d26f3
Binary files /dev/null and b/tests/image-stream/data/gz/raspberrypi.img.gz differ
diff --git a/tests/image-stream/data/images/raspberrypi.img b/tests/image-stream/data/images/raspberrypi.img
new file mode 100644
index 00000000..1cf96222
Binary files /dev/null and b/tests/image-stream/data/images/raspberrypi.img differ
diff --git a/tests/image-stream/data/metadata/zip/rpi-invalid-manifest.zip b/tests/image-stream/data/metadata/zip/rpi-invalid-manifest.zip
new file mode 100644
index 00000000..21a0bbc8
Binary files /dev/null and b/tests/image-stream/data/metadata/zip/rpi-invalid-manifest.zip differ
diff --git a/tests/image-stream/data/metadata/zip/rpi-with-bmap.zip b/tests/image-stream/data/metadata/zip/rpi-with-bmap.zip
new file mode 100644
index 00000000..14cc086b
Binary files /dev/null and b/tests/image-stream/data/metadata/zip/rpi-with-bmap.zip differ
diff --git a/tests/image-stream/data/metadata/zip/rpi-with-instructions.zip b/tests/image-stream/data/metadata/zip/rpi-with-instructions.zip
new file mode 100644
index 00000000..19c75692
Binary files /dev/null and b/tests/image-stream/data/metadata/zip/rpi-with-instructions.zip differ
diff --git a/tests/image-stream/data/metadata/zip/rpi-with-logo.zip b/tests/image-stream/data/metadata/zip/rpi-with-logo.zip
new file mode 100644
index 00000000..5b0158f6
Binary files /dev/null and b/tests/image-stream/data/metadata/zip/rpi-with-logo.zip differ
diff --git a/tests/image-stream/data/metadata/zip/rpi-with-manifest.zip b/tests/image-stream/data/metadata/zip/rpi-with-manifest.zip
new file mode 100644
index 00000000..15289b19
Binary files /dev/null and b/tests/image-stream/data/metadata/zip/rpi-with-manifest.zip differ
diff --git a/tests/image-stream/data/xz/raspberrypi.img.xz b/tests/image-stream/data/xz/raspberrypi.img.xz
new file mode 100644
index 00000000..5e33ece2
Binary files /dev/null and b/tests/image-stream/data/xz/raspberrypi.img.xz differ
diff --git a/tests/image-stream/data/zip/zip-directory-empty.zip b/tests/image-stream/data/zip/zip-directory-empty.zip
new file mode 100644
index 00000000..944763ab
Binary files /dev/null and b/tests/image-stream/data/zip/zip-directory-empty.zip differ
diff --git a/tests/image-stream/data/zip/zip-directory-multiple-images.zip b/tests/image-stream/data/zip/zip-directory-multiple-images.zip
new file mode 100644
index 00000000..f71a56e6
Binary files /dev/null and b/tests/image-stream/data/zip/zip-directory-multiple-images.zip differ
diff --git a/tests/image-stream/data/zip/zip-directory-nested-misc.zip b/tests/image-stream/data/zip/zip-directory-nested-misc.zip
new file mode 100644
index 00000000..126e2b47
Binary files /dev/null and b/tests/image-stream/data/zip/zip-directory-nested-misc.zip differ
diff --git a/tests/image-stream/data/zip/zip-directory-no-image-only-misc.zip b/tests/image-stream/data/zip/zip-directory-no-image-only-misc.zip
new file mode 100644
index 00000000..f796dc1f
Binary files /dev/null and b/tests/image-stream/data/zip/zip-directory-no-image-only-misc.zip differ
diff --git a/tests/image-stream/data/zip/zip-directory-rpi-and-misc.zip b/tests/image-stream/data/zip/zip-directory-rpi-and-misc.zip
new file mode 100644
index 00000000..ea202cd6
Binary files /dev/null and b/tests/image-stream/data/zip/zip-directory-rpi-and-misc.zip differ
diff --git a/tests/image-stream/data/zip/zip-directory-rpi-only.zip b/tests/image-stream/data/zip/zip-directory-rpi-only.zip
new file mode 100644
index 00000000..50ed1539
Binary files /dev/null and b/tests/image-stream/data/zip/zip-directory-rpi-only.zip differ
diff --git a/tests/image-stream/gz.spec.js b/tests/image-stream/gz.spec.js
new file mode 100644
index 00000000..46907f5a
--- /dev/null
+++ b/tests/image-stream/gz.spec.js
@@ -0,0 +1,60 @@
+/*
+ * 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 GZ_PATH = path.join(DATA_PATH, 'gz');
+const imageStream = require('../../lib/image-stream/index');
+const tester = require('./tester');
+
+describe('ImageStream: GZ', function() {
+
+ this.timeout(20000);
+
+ describe('.getFromFilePath()', function() {
+
+ describe('given a gz image', function() {
+ tester.extractFromFilePath(
+ path.join(GZ_PATH, 'raspberrypi.img.gz'),
+ path.join(IMAGES_PATH, 'raspberrypi.img'));
+ });
+
+ });
+
+ describe('.getImageMetadata()', function() {
+
+ it('should return the correct metadata', function(done) {
+ const image = path.join(GZ_PATH, 'raspberrypi.img.gz');
+ const expectedSize = fs.statSync(path.join(IMAGES_PATH, 'raspberrypi.img')).size;
+ const size = fs.statSync(path.join(GZ_PATH, 'raspberrypi.img.gz')).size;
+
+ imageStream.getImageMetadata(image).then((metadata) => {
+ m.chai.expect(metadata).to.deep.equal({
+ estimatedUncompressedSize: expectedSize,
+ size: size
+ });
+ done();
+ });
+ });
+
+ });
+
+});
diff --git a/tests/image-stream/img.spec.js b/tests/image-stream/img.spec.js
new file mode 100644
index 00000000..666788d1
--- /dev/null
+++ b/tests/image-stream/img.spec.js
@@ -0,0 +1,57 @@
+/*
+ * 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 imageStream = require('../../lib/image-stream/index');
+const tester = require('./tester');
+
+describe('ImageStream: IMG', function() {
+
+ this.timeout(20000);
+
+ describe('.getFromFilePath()', function() {
+
+ describe('given an img image', function() {
+ tester.extractFromFilePath(
+ path.join(IMAGES_PATH, 'raspberrypi.img'),
+ path.join(IMAGES_PATH, 'raspberrypi.img'));
+ });
+
+ });
+
+ describe('.getImageMetadata()', function() {
+
+ it('should return the correct metadata', function(done) {
+ const image = path.join(IMAGES_PATH, 'raspberrypi.img');
+ const expectedSize = fs.statSync(image).size;
+
+ imageStream.getImageMetadata(image).then((metadata) => {
+ m.chai.expect(metadata).to.deep.equal({
+ size: expectedSize
+ });
+ done();
+ });
+ });
+
+ });
+
+});
diff --git a/tests/image-stream/index.spec.js b/tests/image-stream/index.spec.js
new file mode 100644
index 00000000..3f5f02ab
--- /dev/null
+++ b/tests/image-stream/index.spec.js
@@ -0,0 +1,55 @@
+/*
+ * 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 _ = require('lodash');
+const imageStream = require('../../lib/image-stream/index');
+
+describe('ImageStream', function() {
+
+ describe('.supportedFileTypes', function() {
+
+ it('should be an array', function() {
+ m.chai.expect(_.isArray(imageStream.supportedFileTypes)).to.be.true;
+ });
+
+ it('should not be empty', function() {
+ m.chai.expect(_.isEmpty(imageStream.supportedFileTypes)).to.be.false;
+ });
+
+ it('should contain only strings', function() {
+ m.chai.expect(_.every(_.map(imageStream.supportedFileTypes, function(fileType) {
+ return _.isString(fileType.extension) && _.isString(fileType.type);
+ }))).to.be.true;
+ });
+
+ it('should not contain empty strings', function() {
+ m.chai.expect(_.every(_.map(imageStream.supportedFileTypes, function(fileType) {
+ return !_.isEmpty(fileType.extension) && !_.isEmpty(fileType.type);
+ }))).to.be.true;
+ });
+
+ it('should not contain a leading period in any file type extension', function() {
+ m.chai.expect(_.every(_.map(imageStream.supportedFileTypes, function(fileType) {
+ return _.first(fileType.extension) !== '.';
+ }))).to.be.true;
+ });
+
+ });
+
+});
diff --git a/tests/image-stream/metadata/zip.spec.js b/tests/image-stream/metadata/zip.spec.js
new file mode 100644
index 00000000..2a07ae0c
--- /dev/null
+++ b/tests/image-stream/metadata/zip.spec.js
@@ -0,0 +1,171 @@
+/*
+ * 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 path = require('path');
+const DATA_PATH = path.join(__dirname, '..', 'data');
+const IMAGES_PATH = path.join(DATA_PATH, 'images');
+const ZIP_PATH = path.join(DATA_PATH, 'metadata', 'zip');
+const tester = require('../tester');
+const imageStream = require('../../../lib/image-stream/index');
+
+const testMetadataProperty = (archivePath, propertyName, expectedValue) => {
+ return imageStream.getFromFilePath(archivePath).then((image) => {
+ m.chai.expect(image[propertyName]).to.deep.equal(expectedValue);
+
+ return imageStream.getImageMetadata(archivePath).then((metadata) => {
+ m.chai.expect(metadata[propertyName]).to.deep.equal(expectedValue);
+ });
+ });
+};
+
+describe('ImageStream: Metadata ZIP', function() {
+
+ this.timeout(10000);
+
+ describe('given an archive with an invalid `manifest.json`', function() {
+
+ tester.expectError(
+ path.join(ZIP_PATH, 'rpi-invalid-manifest.zip'),
+ 'Invalid archive manifest.json');
+
+ describe('.getImageMetadata()', function() {
+
+ it('should be rejected with an error', function(done) {
+ const image = path.join(ZIP_PATH, 'rpi-invalid-manifest.zip');
+
+ imageStream.getImageMetadata(image).catch((error) => {
+ m.chai.expect(error).to.be.an.instanceof(Error);
+ m.chai.expect(error.message).to.equal('Invalid archive manifest.json');
+ done();
+ });
+ });
+
+ });
+
+ });
+
+ describe('given an archive with a `manifest.json`', function() {
+
+ const archive = path.join(ZIP_PATH, 'rpi-with-manifest.zip');
+
+ tester.extractFromFilePath(
+ archive,
+ path.join(IMAGES_PATH, 'raspberrypi.img'));
+
+ it('should read the manifest name property', function(done) {
+ testMetadataProperty(archive, 'name', 'Raspberry Pi').asCallback(done);
+ });
+
+ it('should read the manifest version property', function(done) {
+ testMetadataProperty(archive, 'version', '1.0.0').asCallback(done);
+ });
+
+ it('should read the manifest url property', function(done) {
+ testMetadataProperty(archive, 'url', 'https://www.raspberrypi.org').asCallback(done);
+ });
+
+ it('should read the manifest supportUrl property', function(done) {
+ const expectedValue = 'https://www.raspberrypi.org/forums/';
+ testMetadataProperty(archive, 'supportUrl', expectedValue).asCallback(done);
+ });
+
+ it('should read the manifest releaseNotesUrl property', function(done) {
+ const expectedValue = 'http://downloads.raspberrypi.org/raspbian/release_notes.txt';
+ testMetadataProperty(archive, 'releaseNotesUrl', expectedValue).asCallback(done);
+ });
+
+ it('should read the manifest checksumType property', function(done) {
+ testMetadataProperty(archive, 'checksumType', 'md5').asCallback(done);
+ });
+
+ it('should read the manifest checksum property', function(done) {
+ testMetadataProperty(archive, 'checksum', 'add060b285d512f56c175b76b7ef1bee').asCallback(done);
+ });
+
+ it('should read the manifest bytesToZeroOutFromTheBeginning property', function(done) {
+ testMetadataProperty(archive, 'bytesToZeroOutFromTheBeginning', 512).asCallback(done);
+ });
+
+ it('should read the manifest recommendedDriveSize property', function(done) {
+ testMetadataProperty(archive, 'recommendedDriveSize', 4294967296).asCallback(done);
+ });
+
+ });
+
+ describe('given an archive with a `logo.svg`', function() {
+
+ const archive = path.join(ZIP_PATH, 'rpi-with-logo.zip');
+
+ const logo = [
+ '',
+ ''
+ ].join('\n');
+
+ it('should read the logo contents', function(done) {
+ testMetadataProperty(archive, 'logo', logo).asCallback(done);
+ });
+
+ });
+
+ describe('given an archive with a bmap file', function() {
+
+ const archive = path.join(ZIP_PATH, 'rpi-with-bmap.zip');
+
+ const bmap = [
+ '',
+ '',
+ ' 36864 ',
+ ' 4096 ',
+ ' 9 ',
+ ' 4 ',
+ ' d90f372215cbbef8801caca7b1dd7e587b2142cc ',
+ ' ',
+ ' 0-1 ',
+ ' 7-8 ',
+ ' ',
+ '',
+ ''
+ ].join('\n');
+
+ it('should read the bmap contents', function(done) {
+ testMetadataProperty(archive, 'bmap', bmap).asCallback(done);
+ });
+
+ });
+
+ describe('given an archive with instructions', function() {
+
+ const archive = path.join(ZIP_PATH, 'rpi-with-instructions.zip');
+
+ const instructions = [
+ '# Raspberry Pi Next Steps',
+ '',
+ 'Lorem ipsum dolor sit amet.',
+ ''
+ ].join('\n');
+
+ it('should read the instruction contents', function(done) {
+ testMetadataProperty(archive, 'instructions', instructions).asCallback(done);
+ });
+
+ });
+
+});
diff --git a/tests/image-stream/tester.js b/tests/image-stream/tester.js
new file mode 100644
index 00000000..16d04732
--- /dev/null
+++ b/tests/image-stream/tester.js
@@ -0,0 +1,82 @@
+/*
+ * 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 _ = require('lodash');
+const Bluebird = require('bluebird');
+const fileExists = require('file-exists');
+const fs = Bluebird.promisifyAll(require('fs'));
+const tmp = require('tmp');
+const rindle = require('rindle');
+const imageStream = require('../../lib/image-stream/index');
+
+const doFilesContainTheSameData = (file1, file2) => {
+ return Bluebird.props({
+ file1: fs.readFileAsync(file1),
+ file2: fs.readFileAsync(file2)
+ }).then(function(data) {
+ return _.isEqual(data.file1, data.file2);
+ });
+};
+
+const deleteIfExists = (file) => {
+ return Bluebird.try(function() {
+ if (fileExists(file)) {
+ return fs.unlinkAsync(file);
+ }
+ });
+};
+
+exports.expectError = function(file, errorMessage) {
+ it('should be rejected with an error', function(done) {
+ imageStream.getFromFilePath(file).catch((error) => {
+ m.chai.expect(error).to.be.an.instanceof(Error);
+ m.chai.expect(error.message).to.equal(errorMessage);
+ m.chai.expect(error.description).to.be.a.string;
+ m.chai.expect(error.description.length > 0).to.be.true;
+ done();
+ });
+ });
+};
+
+exports.extractFromFilePath = function(file, image) {
+ it('should be able to extract the image', function(done) {
+ const output = tmp.tmpNameSync();
+
+ imageStream.getFromFilePath(file).then(function(results) {
+ if (!_.some([
+ results.size === fs.statSync(file).size,
+ results.size === fs.statSync(image).size
+ ])) {
+ throw new Error('Invalid size: ' + results.size);
+ }
+
+ const stream = results.stream
+ .pipe(results.transform)
+ .pipe(fs.createWriteStream(output));
+
+ return rindle.wait(stream);
+ }).then(function() {
+ return doFilesContainTheSameData(image, output);
+ }).then(function(areEqual) {
+ m.chai.expect(areEqual).to.be.true;
+ }).finally(function() {
+ return deleteIfExists(output);
+ }).nodeify(done);
+ });
+};
diff --git a/tests/image-stream/utils.spec.js b/tests/image-stream/utils.spec.js
new file mode 100644
index 00000000..54a312cf
--- /dev/null
+++ b/tests/image-stream/utils.spec.js
@@ -0,0 +1,55 @@
+/*
+ * 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 path = require('path');
+const DATA_PATH = path.join(__dirname, 'data');
+const utils = require('../../lib/image-stream/utils');
+
+describe('ImageStream: Utils', function() {
+
+ describe('.getArchiveMimeType()', function() {
+
+ it('should return application/x-bzip2 for a bz2 archive', function() {
+ const file = path.join(DATA_PATH, 'bz2', 'raspberrypi.img.bz2');
+ m.chai.expect(utils.getArchiveMimeType(file)).to.equal('application/x-bzip2');
+ });
+
+ it('should return application/x-xz for a xz archive', function() {
+ const file = path.join(DATA_PATH, 'xz', 'raspberrypi.img.xz');
+ m.chai.expect(utils.getArchiveMimeType(file)).to.equal('application/x-xz');
+ });
+
+ it('should return application/gzip for a gz archive', function() {
+ const file = path.join(DATA_PATH, 'gz', 'raspberrypi.img.gz');
+ m.chai.expect(utils.getArchiveMimeType(file)).to.equal('application/gzip');
+ });
+
+ it('should return application/zip for a zip archive', function() {
+ const file = path.join(DATA_PATH, 'zip', 'zip-directory-rpi-only.zip');
+ m.chai.expect(utils.getArchiveMimeType(file)).to.equal('application/zip');
+ });
+
+ it('should return application/octet-stream for an uncompress image', function() {
+ const file = path.join(DATA_PATH, 'images', 'raspberrypi.img');
+ m.chai.expect(utils.getArchiveMimeType(file)).to.equal('application/octet-stream');
+ });
+
+ });
+
+});
diff --git a/tests/image-stream/xz.spec.js b/tests/image-stream/xz.spec.js
new file mode 100644
index 00000000..28dda9f1
--- /dev/null
+++ b/tests/image-stream/xz.spec.js
@@ -0,0 +1,58 @@
+/*
+ * 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 XZ_PATH = path.join(DATA_PATH, 'xz');
+const imageStream = require('../../lib/image-stream/index');
+const tester = require('./tester');
+
+describe('ImageStream: XZ', function() {
+
+ this.timeout(20000);
+
+ describe('.getFromFilePath()', function() {
+
+ describe('given a xz image', function() {
+ tester.extractFromFilePath(
+ path.join(XZ_PATH, 'raspberrypi.img.xz'),
+ path.join(IMAGES_PATH, 'raspberrypi.img'));
+ });
+
+ });
+
+ describe('.getImageMetadata()', function() {
+
+ it('should return the correct metadata', function(done) {
+ const image = path.join(XZ_PATH, 'raspberrypi.img.xz');
+ const expectedSize = fs.statSync(path.join(IMAGES_PATH, 'raspberrypi.img')).size;
+
+ imageStream.getImageMetadata(image).then((metadata) => {
+ m.chai.expect(metadata).to.deep.equal({
+ size: expectedSize
+ });
+ done();
+ });
+ });
+
+ });
+
+});
diff --git a/tests/image-stream/zip.spec.js b/tests/image-stream/zip.spec.js
new file mode 100644
index 00000000..b380aab7
--- /dev/null
+++ b/tests/image-stream/zip.spec.js
@@ -0,0 +1,82 @@
+/*
+ * 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 ZIP_PATH = path.join(DATA_PATH, 'zip');
+const imageStream = require('../../lib/image-stream/index');
+const tester = require('./tester');
+
+describe('ImageStream: ZIP', function() {
+
+ this.timeout(20000);
+
+ describe('.getFromFilePath()', function() {
+
+ describe('given an empty zip directory', function() {
+ tester.expectError(
+ path.join(ZIP_PATH, 'zip-directory-empty.zip'),
+ 'Invalid archive image');
+ });
+
+ describe('given a zip directory containing only misc files', function() {
+ tester.expectError(
+ path.join(ZIP_PATH, 'zip-directory-no-image-only-misc.zip'),
+ 'Invalid archive image');
+ });
+
+ describe('given a zip directory containing multiple images', function() {
+ tester.expectError(
+ path.join(ZIP_PATH, 'zip-directory-multiple-images.zip'),
+ 'Invalid archive image');
+ });
+
+ describe('given a zip directory containing only an image', function() {
+ tester.extractFromFilePath(
+ path.join(ZIP_PATH, 'zip-directory-rpi-only.zip'),
+ path.join(IMAGES_PATH, 'raspberrypi.img'));
+ });
+
+ describe('given a zip directory containing an image and other misc files', function() {
+ tester.extractFromFilePath(
+ path.join(ZIP_PATH, 'zip-directory-rpi-and-misc.zip'),
+ path.join(IMAGES_PATH, 'raspberrypi.img'));
+ });
+
+ });
+
+ describe('.getImageMetadata()', function() {
+
+ it('should return the correct metadata', function(done) {
+ const image = path.join(ZIP_PATH, 'zip-directory-rpi-only.zip');
+ const expectedSize = fs.statSync(path.join(IMAGES_PATH, 'raspberrypi.img')).size;
+
+ imageStream.getImageMetadata(image).then((metadata) => {
+ m.chai.expect(metadata).to.deep.equal({
+ size: expectedSize
+ });
+ done();
+ });
+ });
+
+ });
+
+});