refactor: integrate etcher-image-stream into the etcher repository (#1040)

This is a long lasting task. The `etcher-image-stream` project takes
care of converting any kind of image input into a NodeJS readable
stream, handling things like decompression in betwee, however its a
module that, except for weird cases, there is no benefit on having
separate from the main repository.

In order to validate the assumption above, we've left the module
separate for almost a year, and no use case has emerged to keep it
like that.

This commit joins the code and tests of that module in the main
repository.

Signed-off-by: Juan Cruz Viotti <jviotti@openmailbox.org>
This commit is contained in:
Juan Cruz Viotti 2017-01-25 10:32:37 -04:00 committed by GitHub
parent 87a782f6ff
commit 0e1f50422e
36 changed files with 1523 additions and 48 deletions

View File

@ -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

View File

@ -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, []);

View File

@ -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) {

View File

@ -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);
});
};

198
lib/image-stream/archive.js Normal file
View File

@ -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;
});
});
};

View File

@ -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())
});
}
};

120
lib/image-stream/index.js Normal file
View File

@ -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;

View File

@ -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'
}
];

41
lib/image-stream/utils.js Normal file
View File

@ -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');
};

62
npm-shrinkwrap.json generated
View File

@ -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"
}
}
}

View File

@ -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": {

View File

@ -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();
});
});
});
});

View File

@ -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();
});
});
});
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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();
});
});
});
});

View File

@ -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();
});
});
});
});

View File

@ -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;
});
});
});

View File

@ -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 = [
'<svg xmlns="http://www.w3.org/2000/svg">',
' <text>Hello World</text>',
'</svg>',
''
].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 = [
'<?xml version="1.0" ?>',
'<bmap version="1.3">',
' <ImageSize> 36864 </ImageSize>',
' <BlockSize> 4096 </BlockSize>',
' <BlocksCount> 9 </BlocksCount>',
' <MappedBlocksCount> 4 </MappedBlocksCount>',
' <BmapFileSHA1> d90f372215cbbef8801caca7b1dd7e587b2142cc </BmapFileSHA1>',
' <BlockMap>',
' <Range sha1="193edb53bde599f58369f4e83a6c5d54b96819ce"> 0-1 </Range>',
' <Range sha1="193edb53bde599f58369f4e83a6c5d54b96819ce"> 7-8 </Range>',
' </BlockMap>',
'</bmap>',
''
].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);
});
});
});

View File

@ -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);
});
};

View File

@ -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');
});
});
});

View File

@ -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();
});
});
});
});

View File

@ -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();
});
});
});
});