mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 07:17:18 +00:00
refactor(image-stream): stream original/final sizes (#1050)
The `image-stream` module currently returns a readable stream, a transform stream, a "size", and an optional "estimatedUncompressedSize". Based on this information, its hard to say what "size" represents. Some format handlers return the compressed size and provide a decompression transform stream while others return the decompressed stream directly and put the decompressed size in "size". As a way to simplify this, every format handler now returns a "size" object with the following properties: - `original`: The original compressed size - `final.estimated`: Whether the final size is an estimation or not - `final.value`: The final uncompressed size As a bonus, we extract the file size retrieval logic to `imageStream.getFromFilePath()`, which is the onlt part where the concept of a file should be referred to. Signed-off-by: Juan Cruz Viotti <jviotti@openmailbox.org>
This commit is contained in:
parent
b25b2d1179
commit
1bfcee06e2
@ -71,7 +71,10 @@ exports.writeImage = (imagePath, drive, options, onProgress) => {
|
||||
fd: driveFileDescriptor,
|
||||
device: drive.raw,
|
||||
size: drive.size
|
||||
}, image, {
|
||||
}, {
|
||||
stream: image.stream,
|
||||
size: image.size.original
|
||||
}, {
|
||||
check: options.validateWriteOnSuccess,
|
||||
transform: image.transform,
|
||||
bmap: image.bmap,
|
||||
|
@ -74,7 +74,7 @@ module.exports = function($q, SupportedFormatsModel) {
|
||||
|
||||
imageStream.getImageMetadata(imagePath).then((metadata) => {
|
||||
metadata.path = imagePath;
|
||||
metadata.size = metadata.estimatedUncompressedSize || metadata.size;
|
||||
metadata.size = metadata.size.final.value;
|
||||
return resolve(metadata);
|
||||
}).catch(reject);
|
||||
});
|
||||
|
@ -190,8 +190,16 @@ exports.extractImage = (archive, hooks) => {
|
||||
})
|
||||
}).then((results) => {
|
||||
results.metadata.stream = results.imageStream;
|
||||
results.metadata.size = imageEntry.size;
|
||||
results.metadata.transform = new PassThroughStream();
|
||||
|
||||
results.metadata.size = {
|
||||
original: imageEntry.size,
|
||||
final: {
|
||||
estimation: false,
|
||||
value: imageEntry.size
|
||||
}
|
||||
};
|
||||
|
||||
return results.metadata;
|
||||
});
|
||||
});
|
||||
|
@ -40,13 +40,22 @@ module.exports = {
|
||||
* @memberof handlers
|
||||
*
|
||||
* @param {String} file - file path
|
||||
* @param {Object} options - options
|
||||
* @param {Number} [options.size] - file size
|
||||
*
|
||||
* @fulfil {Object} - image metadata
|
||||
* @returns {Promise}
|
||||
*/
|
||||
'application/x-bzip2': (file) => {
|
||||
'application/x-bzip2': (file, options) => {
|
||||
return Bluebird.props({
|
||||
stream: fs.createReadStream(file),
|
||||
size: fs.statAsync(file).get('size'),
|
||||
size: {
|
||||
original: options.size,
|
||||
final: {
|
||||
estimation: true,
|
||||
value: options.size
|
||||
}
|
||||
},
|
||||
transform: Bluebird.resolve(unbzip2Stream())
|
||||
});
|
||||
},
|
||||
@ -58,15 +67,25 @@ module.exports = {
|
||||
* @memberof handlers
|
||||
*
|
||||
* @param {String} file - file path
|
||||
* @param {Object} options - options
|
||||
* @param {Number} [options.size] - file size
|
||||
*
|
||||
* @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())
|
||||
'application/gzip': (file, options) => {
|
||||
return gzipUncompressedSize.fromFileAsync(file).then((uncompressedSize) => {
|
||||
return Bluebird.props({
|
||||
stream: fs.createReadStream(file),
|
||||
size: {
|
||||
original: options.size,
|
||||
final: {
|
||||
estimation: true,
|
||||
value: uncompressedSize
|
||||
}
|
||||
},
|
||||
transform: Bluebird.resolve(zlib.createGunzip())
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@ -77,20 +96,28 @@ module.exports = {
|
||||
* @memberof handlers
|
||||
*
|
||||
* @param {String} file - file path
|
||||
* @param {Object} options - options
|
||||
* @param {Number} [options.size] - file size
|
||||
*
|
||||
* @fulfil {Object} - image metadata
|
||||
* @returns {Promise}
|
||||
*/
|
||||
'application/x-xz': (file) => {
|
||||
'application/x-xz': (file, options) => {
|
||||
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()
|
||||
stream: fs.createReadStream(file),
|
||||
size: {
|
||||
original: options.size,
|
||||
final: {
|
||||
estimation: false,
|
||||
value: metadata.uncompressedSize
|
||||
}
|
||||
},
|
||||
transform: lzma.createDecompressor()
|
||||
};
|
||||
});
|
||||
},
|
||||
@ -116,13 +143,22 @@ module.exports = {
|
||||
* @memberof handlers
|
||||
*
|
||||
* @param {String} file - file path
|
||||
* @param {Object} options - options
|
||||
* @param {Number} [options.size] - file size
|
||||
*
|
||||
* @fulfil {Object} - image metadata
|
||||
* @returns {Promise}
|
||||
*/
|
||||
'application/octet-stream': (file) => {
|
||||
'application/octet-stream': (file, options) => {
|
||||
return Bluebird.props({
|
||||
stream: fs.createReadStream(file),
|
||||
size: fs.statAsync(file).get('size'),
|
||||
size: {
|
||||
original: options.size,
|
||||
final: {
|
||||
estimation: false,
|
||||
value: options.size
|
||||
}
|
||||
},
|
||||
transform: Bluebird.resolve(new PassThroughStream())
|
||||
});
|
||||
}
|
||||
|
@ -18,6 +18,8 @@
|
||||
|
||||
const _ = require('lodash');
|
||||
const Bluebird = require('bluebird');
|
||||
const fs = Bluebird.promisifyAll(require('fs'));
|
||||
const isStream = require('isstream');
|
||||
const utils = require('./utils');
|
||||
const handlers = require('./handlers');
|
||||
const supportedFileTypes = require('./supported');
|
||||
@ -61,11 +63,15 @@ exports.getFromFilePath = (file) => {
|
||||
return Bluebird.try(() => {
|
||||
const type = utils.getArchiveMimeType(file);
|
||||
|
||||
if (!handlers[type]) {
|
||||
if (!_.has(handlers, type)) {
|
||||
throw new Error('Invalid image');
|
||||
}
|
||||
|
||||
return handlers[type](file);
|
||||
return fs.statAsync(file).then((fileStats) => {
|
||||
return _.invoke(handlers, type, file, {
|
||||
size: fileStats.size
|
||||
});
|
||||
});
|
||||
}).then((image) => {
|
||||
return _.omitBy(image, _.isUndefined);
|
||||
});
|
||||
@ -101,7 +107,7 @@ exports.getFromFilePath = (file) => {
|
||||
*/
|
||||
exports.getImageMetadata = (file) => {
|
||||
return exports.getFromFilePath(file).then((image) => {
|
||||
return _.omitBy(image, _.isObject);
|
||||
return _.omitBy(image, isStream);
|
||||
});
|
||||
};
|
||||
|
||||
|
5
npm-shrinkwrap.json
generated
5
npm-shrinkwrap.json
generated
@ -512,6 +512,11 @@
|
||||
"from": "isarray@0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
|
||||
},
|
||||
"isstream": {
|
||||
"version": "0.1.2",
|
||||
"from": "isstream@>=0.1.1 <0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz"
|
||||
},
|
||||
"isexe": {
|
||||
"version": "1.1.2",
|
||||
"from": "isexe@>=1.1.1 <2.0.0",
|
||||
|
@ -80,6 +80,7 @@
|
||||
"gzip-uncompressed-size": "^1.0.0",
|
||||
"immutable": "^3.8.1",
|
||||
"is-elevated": "^1.0.0",
|
||||
"isstream": "^0.1.2",
|
||||
"lodash": "^4.5.1",
|
||||
"lzma-native": "^1.5.2",
|
||||
"node-ipc": "^8.9.2",
|
||||
|
@ -47,7 +47,13 @@ describe('ImageStream: BZ2', function() {
|
||||
|
||||
imageStream.getImageMetadata(image).then((metadata) => {
|
||||
m.chai.expect(metadata).to.deep.equal({
|
||||
size: expectedSize
|
||||
size: {
|
||||
original: expectedSize,
|
||||
final: {
|
||||
estimation: true,
|
||||
value: expectedSize
|
||||
}
|
||||
}
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
@ -43,13 +43,18 @@ describe('ImageStream: GZ', 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;
|
||||
const uncompressedSize = fs.statSync(path.join(IMAGES_PATH, 'raspberrypi.img')).size;
|
||||
const compressedSize = 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
|
||||
size: {
|
||||
original: compressedSize,
|
||||
final: {
|
||||
estimation: true,
|
||||
value: uncompressedSize
|
||||
}
|
||||
}
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
@ -46,7 +46,13 @@ describe('ImageStream: IMG', function() {
|
||||
|
||||
imageStream.getImageMetadata(image).then((metadata) => {
|
||||
m.chai.expect(metadata).to.deep.equal({
|
||||
size: expectedSize
|
||||
size: {
|
||||
original: expectedSize,
|
||||
final: {
|
||||
estimation: false,
|
||||
value: expectedSize
|
||||
}
|
||||
}
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
@ -60,10 +60,10 @@ exports.extractFromFilePath = function(file, image) {
|
||||
|
||||
imageStream.getFromFilePath(file).then(function(results) {
|
||||
if (!_.some([
|
||||
results.size === fs.statSync(file).size,
|
||||
results.size === fs.statSync(image).size
|
||||
results.size.original === fs.statSync(file).size,
|
||||
results.size.original === fs.statSync(image).size
|
||||
])) {
|
||||
throw new Error('Invalid size: ' + results.size);
|
||||
throw new Error('Invalid size: ' + results.size.original);
|
||||
}
|
||||
|
||||
const stream = results.stream
|
||||
|
@ -43,11 +43,18 @@ describe('ImageStream: XZ', 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;
|
||||
const compressedSize = fs.statSync(image).size;
|
||||
const uncompressedSize = fs.statSync(path.join(IMAGES_PATH, 'raspberrypi.img')).size;
|
||||
|
||||
imageStream.getImageMetadata(image).then((metadata) => {
|
||||
m.chai.expect(metadata).to.deep.equal({
|
||||
size: expectedSize
|
||||
size: {
|
||||
original: compressedSize,
|
||||
final: {
|
||||
estimation: false,
|
||||
value: uncompressedSize
|
||||
}
|
||||
}
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
@ -71,7 +71,13 @@ describe('ImageStream: ZIP', function() {
|
||||
|
||||
imageStream.getImageMetadata(image).then((metadata) => {
|
||||
m.chai.expect(metadata).to.deep.equal({
|
||||
size: expectedSize
|
||||
size: {
|
||||
original: expectedSize,
|
||||
final: {
|
||||
estimation: false,
|
||||
value: expectedSize
|
||||
}
|
||||
}
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user