etcher-image-write
Version:
The cross-platform way to stream an OS image to a device
220 lines (207 loc) • 6.52 kB
JavaScript
/*
* 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 bmapflash = require('bmapflash');
const CRC32Stream = require('crc32-stream');
const streamChunker = require('stream-chunker');
const progressStream = require('progress-stream');
const PassThroughStream = require('stream').PassThrough;
const through2 = require('through2');
const ImageWriteStream = require('./image-write-stream');
const utils = require('./utils');
/**
* @summary Write an image using bmap
* @function
* @public
*
* @param {Number} deviceFileDescriptor - device file descriptor
* @param {Object} options - options
* @param {Number} options.imageSize - image size
* @param {ReadableStream} options.imageStream - image stream
* @param {String} options.bmapContents - bmap xml contents
* @param {Number} [options.bytesToZeroOutFromTheBeginning] - bytes to zero out from the beginning
* @param {Function} options.progress - progress callback (state)
* @fulfil {Object} - results
* @returns {Promise}
*
* @example
* const fd = fs.openSync('/dev/rdisk2', 'rs+');
* write.usingBmap(fd, {
* imageStream: fs.createReadStream('path/to/image'),
* imageSize: 65536 * 128,
* bmapContents: fs.readFileSync('path/to/image.bmap', {
* encoding: 'utf8'
* }),
* progress: (state) => {
* console.log(state);
* }
* }).then(() => {
* console.log('Done!');
* });
*/
exports.usingBmap = (deviceFileDescriptor, options) => {
return new Bluebird((resolve, reject) => {
const flasher = bmapflash.flashImageToFileDescriptor(
options.imageStream,
deviceFileDescriptor,
options.bmapContents,
{
bytesToZeroOutFromTheBeginning: options.bytesToZeroOutFromTheBeginning
}
);
flasher.on('progress', options.progress);
flasher.on('error', reject);
flasher.on('done', () => {
return resolve({
transferredBytes: options.imageSize
});
});
});
};
/**
* @summary Write an image by streaming to the drive
* @function
* @public
*
* @param {Number} deviceFileDescriptor - device file descriptor
* @param {Object} options - options
* @param {ReadableStream} options.imageStream - image stream
* @param {TransformStream} [options.transformStream] - transform stream
* @param {Number} options.imageSize - image size
* @param {Number} options.chunkSize - chunk size
* @param {Number} options.minimumChunkSize - minimum chunk size
* @param {Number} options.driveSize - drive size
* @param {Function} options.progress - progress callback (state)
* @fulfil {Object} - results
* @returns {Promise}
*
* @example
* const fd = fs.openSync('/dev/rdisk2', 'rs+');
* write.usingStreaming(fd, {
* imageStream: fs.createReadStream('path/to/image'),
* imageSize: 65536 * 128,
* chunkSize: 65536,
* minimumChunkSize: 32768,
* driveSize: 65536 * 256,
* progress: (state) => {
* console.log(state);
* }
* }).then(() => {
* console.log('Done!');
* });
*/
exports.usingStreaming = (deviceFileDescriptor, options) => {
return new Bluebird((resolve, reject) => {
const checksumStream = new CRC32Stream();
let transferredBytes = 0;
let paddingBytes = 0;
return utils.safePipe([
{
stream: options.imageStream
},
{
stream: progressStream({
length: _.parseInt(options.imageSize),
time: 500
}),
events: {
progress: options.progress
}
},
{
stream: options.transformStream || new PassThroughStream()
},
{
stream: checksumStream
},
{
stream: streamChunker(options.chunkSize, {
flush: true,
align: false
})
},
{
stream: through2((chunk, encoding, callback) => {
transferredBytes += chunk.length;
if (chunk.length < options.chunkSize) {
const finalChunkSize = Math.ceil(chunk.length / options.minimumChunkSize) * options.minimumChunkSize;
paddingBytes += finalChunkSize - chunk.length;
chunk = utils.padChunk(chunk, finalChunkSize);
}
if (transferredBytes + paddingBytes > options.driveSize) {
const error = new Error('Not enough space on the drive');
error.code = 'ENOSPC';
return callback(error);
}
return callback(null, chunk);
})
},
{
stream: new ImageWriteStream(deviceFileDescriptor)
}
], (error) => {
if (error) {
return reject(error);
}
return resolve({
transferredBytes: transferredBytes,
checksum: checksumStream.hex().toLowerCase()
});
});
});
};
/**
* @summary Infer writing strategy from options
* @function
* @public
*
* @description
* This is a facade for the other `.using*()` functions
* implemented on this file.
*
* @param {Number} deviceFileDescriptor - device file descriptor
* @param {Object} options - options
* @fulfil {Object} - results
* @returns {Promise}
*
* @example
* const fd = fs.openSync('/dev/rdisk2', 'rs+');
* write.inferFromOptions(fd, { ... }).then(() => {
* console.log('Done!');
* });
*/
exports.inferFromOptions = (deviceFileDescriptor, options) => {
if (options.bmapContents) {
return exports.usingBmap(deviceFileDescriptor, {
imageStream: options.imageStream,
imageSize: options.imageSize,
bmapContents: options.bmapContents,
bytesToZeroOutFromTheBeginning: options.bytesToZeroOutFromTheBeginning,
progress: options.progress
});
}
return exports.usingStreaming(deviceFileDescriptor, {
imageStream: options.imageStream,
imageSize: options.imageSize,
transformStream: options.transformStream,
chunkSize: options.chunkSize,
minimumChunkSize: options.minimumChunkSize,
driveSize: options.driveSize,
progress: options.progress
});
};