From 5e779581061eb7a3310a1f4f0d85505a39d04b97 Mon Sep 17 00:00:00 2001 From: Jonas Hermsmeier Date: Tue, 14 Nov 2017 19:54:10 +0100 Subject: [PATCH] feat(writer): Implement streaming pipelines (#1671) This implements a new way of image write streaming under use of pipage and blockmap, which paves the way for a few things like using network locations as sources, and imaging of storage devices (aka backups). As it allows for mutation of the streaming pipeline while it's writing, it also facilitates the development of dynamic block-mapping. Change-Type: minor --- lib/cli/diskpart.js | 124 ++++++++++++ lib/cli/writer.js | 25 ++- lib/image-stream/handlers.js | 2 +- lib/writer/.eslintrc.yml | 6 + lib/writer/block-read-stream.js | 212 +++++++++++++++++++++ lib/writer/block-stream.js | 136 ++++++++++++++ lib/writer/block-write-stream.js | 261 ++++++++++++++++++++++++++ lib/writer/checksum-stream.js | 142 ++++++++++++++ lib/writer/index.js | 311 +++++++++++++++++++++++++++++++ lib/writer/progress-stream.js | 113 +++++++++++ npm-shrinkwrap.json | 284 ++++++++++++++-------------- package.json | 9 +- tests/image-stream/dmg.spec.js | 6 +- 13 files changed, 1466 insertions(+), 165 deletions(-) create mode 100644 lib/cli/diskpart.js create mode 100644 lib/writer/.eslintrc.yml create mode 100644 lib/writer/block-read-stream.js create mode 100644 lib/writer/block-stream.js create mode 100644 lib/writer/block-write-stream.js create mode 100644 lib/writer/checksum-stream.js create mode 100644 lib/writer/index.js create mode 100644 lib/writer/progress-stream.js diff --git a/lib/cli/diskpart.js b/lib/cli/diskpart.js new file mode 100644 index 00000000..731c4015 --- /dev/null +++ b/lib/cli/diskpart.js @@ -0,0 +1,124 @@ +/* + * Copyright 2017 resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict' + +const os = require('os') +const fs = require('fs') +const path = require('path') +const crypto = require('crypto') +const childProcess = require('child_process') +const debug = require('debug')('etcher:cli:diskpart') +const Promise = require('bluebird') +const retry = require('bluebird-retry') + +const TMP_RANDOM_BYTES = 6 +const DISKPART_DELAY = 2000 +const DISKPART_RETRIES = 5 + +/** + * @summary Generate a tmp filename with full path of OS' tmp dir + * @function + * @private + * + * @param {String} extension - temporary file extension + * @returns {String} filename + * + * @example + * const filename = tmpFilename('.sh'); + */ +const tmpFilename = (extension) => { + const random = crypto.randomBytes(TMP_RANDOM_BYTES).toString('hex') + const filename = `etcher-diskpart-${random}${extension}` + return path.join(os.tmpdir(), filename) +} + +/** + * @summary Run a diskpart script + * @param {Array} commands - list of commands to run + * @param {Function} callback - callback(error) + * @example + * runDiskpart(['rescan'], (error) => { + * ... + * }) + */ +const runDiskpart = (commands, callback) => { + if (os.platform() !== 'win32') { + callback() + return + } + + const filename = tmpFilename('') + const script = commands.join('\r\n') + + fs.writeFile(filename, script, { + mode: 0o755 + }, (writeError) => { + debug('write %s:', filename, writeError || 'OK') + + childProcess.exec(`diskpart /s ${filename}`, (execError, stdout, stderr) => { + debug('stdout:', stdout) + debug('stderr:', stderr) + + fs.unlink(filename, (unlinkError) => { + debug('unlink %s:', filename, unlinkError || 'OK') + callback(execError) + }) + }) + }) +} + +module.exports = { + + /** + * @summary Clean a device's partition tables + * @param {String} device - device path + * @example + * diskpart.clean('\\\\.\\PhysicalDrive2') + * .then(...) + * .catch(...) + * @returns {Promise} + */ + clean (device) { + if (os.platform() !== 'win32') { + return Promise.resolve() + } + + debug('clean', device) + + const pattern = /PHYSICALDRIVE(\d+)/i + + if (pattern.test(device)) { + const deviceId = device.match(pattern).pop() + return retry(() => { + return new Promise((resolve, reject) => { + runDiskpart([ `select disk ${deviceId}`, 'clean', 'rescan' ], (error) => { + return error ? reject(error) : resolve() + }) + }).delay(DISKPART_DELAY) + }, { + /* eslint-disable camelcase */ + max_tries: DISKPART_RETRIES + /* eslint-enable camelcase */ + }).catch((error) => { + throw new Error(`Couldn't clean the drive, ${error.failure.message} (code ${error.failure.code})`) + }) + } + + return Promise.reject(new Error(`Invalid device: "${device}"`)) + } + +} diff --git a/lib/cli/writer.js b/lib/cli/writer.js index a5965822..103c0d43 100644 --- a/lib/cli/writer.js +++ b/lib/cli/writer.js @@ -16,7 +16,7 @@ 'use strict' -const imageWrite = require('etcher-image-write') +const ImageWriter = require('../writer') const Bluebird = require('bluebird') const fs = Bluebird.promisifyAll(require('fs')) const mountutils = Bluebird.promisifyAll(require('mountutils')) @@ -24,6 +24,7 @@ const os = require('os') const imageStream = require('../image-stream') const errors = require('../shared/errors') const constraints = require('../shared/drive-constraints') +const diskpart = require('./diskpart') /** * @summary Timeout, in milliseconds, to wait before unmounting on success @@ -71,6 +72,8 @@ exports.writeImage = (imagePath, drive, options, onProgress) => { } return mountutils.unmountDiskAsync(drive.device) + }).then(() => { + return diskpart.clean(drive.device) }).then(() => { return fs.openAsync(drive.raw, 'rs+') }).then((driveFileDescriptor) => { @@ -82,24 +85,20 @@ exports.writeImage = (imagePath, drive, options, onProgress) => { }) } - return imageWrite.write({ + const writer = new ImageWriter({ + image, fd: driveFileDescriptor, - device: drive.raw, - size: drive.size - }, { - stream: image.stream, - size: image.size.original - }, { - check: options.validateWriteOnSuccess, - transform: image.transform, - bmap: image.bmap, - bytesToZeroOutFromTheBeginning: image.bytesToZeroOutFromTheBeginning + path: drive.raw, + verify: options.validateWriteOnSuccess, + checksumAlgorithms: [ 'crc32' ] }) + + return writer.write() }).then((writer) => { return new Bluebird((resolve, reject) => { writer.on('progress', onProgress) writer.on('error', reject) - writer.on('done', resolve) + writer.on('finish', resolve) }) }).tap(() => { // Make sure the device stream file descriptor is closed diff --git a/lib/image-stream/handlers.js b/lib/image-stream/handlers.js index 6c32d50a..ac8eba47 100644 --- a/lib/image-stream/handlers.js +++ b/lib/image-stream/handlers.js @@ -181,7 +181,7 @@ module.exports = { size: { // FIXME(jhermsmeier): Originally `options.size`, // See discussion in https://github.com/resin-io/etcher/pull/1587 - original: size || options.size, + original: options.size, final: { estimation: false, value: size diff --git a/lib/writer/.eslintrc.yml b/lib/writer/.eslintrc.yml new file mode 100644 index 00000000..83b047c7 --- /dev/null +++ b/lib/writer/.eslintrc.yml @@ -0,0 +1,6 @@ +rules: + no-eq-null: off + no-magic-numbers: off + no-param-reassign: off + no-underscore-dangle: off + lodash/prefer-lodash-method: off diff --git a/lib/writer/block-read-stream.js b/lib/writer/block-read-stream.js new file mode 100644 index 00000000..2183084c --- /dev/null +++ b/lib/writer/block-read-stream.js @@ -0,0 +1,212 @@ +/* + * Copyright 2017 resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict' + +const stream = require('readable-stream') +const fs = require('fs') +const debug = require('debug')('block-read-stream') + +const CHUNK_SIZE = 64 * 1024 +const MIN_CHUNK_SIZE = 512 + +/** + * @summary BlockReadStream + * @class + */ +class BlockReadStream extends stream.Readable { + /** + * @summary BlockReadStream constructor + * @param {Object} [options] - options + * @param {Number} [options.fd] - file descriptor + * @param {String} [options.path] - file path + * @param {String} [options.flags] - file open flags + * @param {Number} [options.mode] - file mode + * @param {Number} [options.start] - start offset in bytes + * @param {Number} [options.end] - end offset in bytes + * @param {Boolean} [options.autoClose] - automatically close the stream on end + * @example + * new BlockReadStream() + */ + constructor (options) { + options = Object.assign({}, BlockReadStream.defaults, options) + options.objectMode = true + + debug('block-read-stream %j', options) + + super(options) + + this.fs = options.fs + this.fd = options.fd + this.path = options.path + this.flags = options.flags + this.mode = options.mode + this.end = options.end || Infinity + this.autoClose = options.autoClose + + this.position = options.start || 0 + this.bytesRead = 0 + + this.closed = false + this.destroyed = false + + this.once('end', function () { + if (this.autoClose) { + this.close() + } + }) + + /** + * @summary onRead handler + * @param {Error} error - error + * @param {Number} bytesRead - bytes read + * @param {Buffer} buffer - resulting buffer + * @example + * fs.read(fd, buffer, 0, length, position, onRead) + */ + this._onRead = (error, bytesRead, buffer) => { + if (!error && bytesRead !== buffer.length) { + error = new Error(`Bytes read mismatch: ${bytesRead} != ${buffer.length}`) + } + + if (error) { + if (this.autoClose) { + this.destroy() + } + this.emit('error', error) + return + } + + this.bytesRead += bytesRead + this.push(buffer) + } + + this.open() + } + + /** + * @summary Read a chunk from the source + * @private + * @example + * // not to be called directly + */ + _read () { + // Wait for file handle to be open + if (this.fd == null) { + this.once('open', () => { + this._read() + }) + return + } + + const toRead = this.end - this.position + + if (toRead <= 0) { + this.push(null) + return + } + + const length = Math.min(CHUNK_SIZE, Math.max(MIN_CHUNK_SIZE, toRead)) + const buffer = Buffer.alloc(length) + + this.fs.read(this.fd, buffer, 0, length, this.position, this._onRead) + this.position += length + } + + /** + * @summary Open a handle to the file + * @private + * @example + * this.open() + */ + open () { + debug('open') + + if (this.fd != null) { + this.emit('open', this.fd) + return + } + + this.fs.open(this.path, this.flags, this.mode, (error, fd) => { + if (error) { + if (this.autoClose) { + this.destroy() + } + this.emit('error', error) + } else { + this.fd = fd + this.emit('open', fd) + } + }) + } + + /** + * @summary Close the underlying resource + * @param {Function} callback - callback(error) + * @example + * blockStream.close((error) => { + * // ... + * }) + */ + close (callback) { + debug('close') + + if (callback) { + this.once('close', callback) + } + + if (this.closed || this.fd == null) { + if (this.fd == null) { + this.once('open', () => { + this.close() + }) + } else { + process.nextTick(() => { + this.emit('close') + }) + } + return + } + + this.closed = true + + this.fs.close(this.fd, (error) => { + if (error) { + this.emit('error', error) + } else { + this.emit('close') + } + }) + + this.fd = null + } +} + +/** + * @summary Default options + * @type {Object} + * @constant + */ +BlockReadStream.defaults = { + fs, + fd: null, + path: null, + flags: 'r', + mode: 0o666, + autoClose: true +} + +module.exports = BlockReadStream diff --git a/lib/writer/block-stream.js b/lib/writer/block-stream.js new file mode 100644 index 00000000..b67e9602 --- /dev/null +++ b/lib/writer/block-stream.js @@ -0,0 +1,136 @@ +/* + * Copyright 2017 resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict' + +const stream = require('readable-stream') +const debug = require('debug')('block-stream') + +const MIN_BLOCK_SIZE = 512 +const CHUNK_SIZE = 64 * 1024 + +/** + * @summary BlockStream class + * @class + */ +class BlockStream extends stream.Transform { + /** + * @summary BlockStream constructor + * @param {Object} [options] - options + * @param {Number} [options.blockSize] - block size in bytes + * @param {Number} [options.chunkSize] - chunk size in bytes + * @example + * new BlockStream(options) + */ + constructor (options) { + options = Object.assign({}, BlockStream.defaults, options) + options.readableObjectMode = true + + super(options) + + this.blockSize = options.blockSize + this.chunkSize = options.chunkSize + this.bytesRead = 0 + this.bytesWritten = 0 + + this._buffers = [] + this._bytes = 0 + + debug('new %j', options) + } + + /** + * @summary Internal write handler + * @private + * @param {Buffer} chunk - chunk buffer + * @param {String} encoding - chunk encoding + * @param {Function} next - callback(error, value) + * @example + * // Not to be called directly + */ + _transform (chunk, encoding, next) { + this.bytesRead += chunk.length + + if (this._bytes === 0 && chunk.length >= this.chunkSize) { + if (chunk.length % this.blockSize === 0) { + this.bytesWritten += chunk.length + this.push(chunk) + next() + return + } + } + + this._buffers.push(chunk) + this._bytes += chunk.length + + if (this._bytes >= this.chunkSize) { + let block = Buffer.concat(this._buffers) + const length = Math.floor(block.length / this.blockSize) * this.blockSize + + this._buffers.length = 0 + this._bytes = 0 + + if (block.length !== length) { + this._buffers.push(block.slice(length)) + this._bytes += block.length - length + block = block.slice(0, length) + } + + this.bytesWritten += block.length + this.push(block) + } + + next() + } + + /** + * @summary Internal stream end handler + * @private + * @param {Function} done - callback(error, value) + * @example + * // Not to be called directly + */ + _flush (done) { + if (!this._bytes) { + done() + return + } + + const length = Math.ceil(this._bytes / this.blockSize) * this.blockSize + const block = Buffer.alloc(length) + let offset = 0 + + for (let index = 0; index < this._buffers.length; index += 1) { + this._buffers[index].copy(block, offset) + offset += this._buffers[index].length + } + + this.push(block) + done() + } +} + +/** + * @summary Default options + * @type {Object} + * @constant + */ +BlockStream.defaults = { + blockSize: MIN_BLOCK_SIZE, + chunkSize: CHUNK_SIZE +} + +module.exports = BlockStream diff --git a/lib/writer/block-write-stream.js b/lib/writer/block-write-stream.js new file mode 100644 index 00000000..18ced4ef --- /dev/null +++ b/lib/writer/block-write-stream.js @@ -0,0 +1,261 @@ +/* + * Copyright 2017 resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict' + +const stream = require('readable-stream') +const fs = require('fs') +const debug = require('debug')('block-write-stream') + +const CHUNK_SIZE = 64 * 1024 + +/** + * @summary BlockWriteStream + * @class + */ +class BlockWriteStream extends stream.Writable { + /** + * @summary BlockReadStream constructor + * @param {Object} [options] - options + * @param {Number} [options.fd] - file descriptor + * @param {String} [options.path] - file path + * @param {String} [options.flags] - file open flags + * @param {Number} [options.mode] - file mode + * @param {Boolean} [options.autoClose] - automatically close the stream on end + * @example + * new BlockWriteStream(options) + */ + constructor (options) { + options = Object.assign({}, BlockWriteStream.defaults, options) + options.objectMode = true + + debug('block-write-stream %j', options) + + super(options) + + this._writableState.highWaterMark = 1 + + this.fs = options.fs + this.fd = options.fd + this.path = options.path + this.flags = options.flags + this.mode = options.mode + this.autoClose = options.autoClose + + this.position = 0 + this.bytesRead = 0 + this.blocksRead = 0 + this.bytesWritten = 0 + this.blocksWritten = 0 + + this.closed = false + this.destroyed = false + + this.once('finish', function () { + if (this.autoClose) { + this.close() + } + }) + + this._flushing = false + this._firstBlocks = [] + + this.open() + } + + /** + * @summary Internal write handler + * @private + * @param {Buffer} chunk - chunk buffer + * @param {String} encoding - chunk encoding + * @param {Function} next - callback(error, value) + * @example + * // Not to be called directly + */ + _write (chunk, encoding, next) { + debug('_write', chunk.length, chunk.position, chunk.address) + + // Wait for file handle to be open + if (this.fd == null) { + this.once('open', () => { + this._write(chunk, encoding, next) + }) + return + } + + this.bytesRead += chunk.length + this.blocksRead += 1 + + if (chunk.position == null) { + chunk.position = this.position + } + + if (!this._flushing && (chunk.position < CHUNK_SIZE)) { + this._firstBlocks.push(chunk) + this.position = chunk.position + chunk.length + process.nextTick(next) + return + } + + if (chunk.position !== this.position) { + this.position = chunk.position + } + + fs.write(this.fd, chunk, 0, chunk.length, chunk.position, (error, bytesWritten) => { + this.bytesWritten += bytesWritten + this.blocksWritten += 1 + this.position += bytesWritten + next(error) + }) + } + + /** + * @summary Write buffered data before a stream ends + * @private + * @param {Function} done - callback + * @example + * // Called by stream internals + */ + _final (done) { + debug('_final') + + /** + * @summary Write the next chunk of the buffered `_firstBlocks` + * @param {Error} [error] - error + * @example + * writeNext() + */ + const writeNext = (error) => { + if (error) { + this.destroy(error) + return + } + const chunk = this._firstBlocks.pop() + if (!chunk) { + done() + return + } + this._write(chunk, null, writeNext) + } + + this._flushing = true + writeNext() + } + + /** + * @summary Destroy the stream, and emit the passed error + * @private + * @param {Error} [error] - error + * @param {Function} done - callback + * @example + * stream.destroy() + */ + _destroy (error, done) { + debug('_destroy', error) + + if (this.autoClose) { + this.close((closeError) => { + done(error || closeError) + }) + } else { + done(error) + } + } + + /** + * @summary Open a handle to the file + * @private + * @example + * this.open() + */ + open () { + debug('open') + + if (this.fd != null) { + this.emit('open', this.fd) + return + } + + this.fs.open(this.path, this.flags, this.mode, (error, fd) => { + if (error) { + if (this.autoClose) { + this.destroy() + } + this.emit('error', error) + } else { + this.fd = fd + this.emit('open', fd) + } + }) + } + + /** + * @summary Close the underlying resource + * @param {Function} callback - callback(error) + * @example + * blockStream.close((error) => { + * // ... + * }) + */ + close (callback) { + debug('close') + + if (callback) { + this.once('close', callback) + } + + if (this.closed || this.fd == null) { + if (this.fd == null) { + this.once('open', () => { + this.close() + }) + } else { + process.nextTick(() => { + this.emit('close') + }) + } + return + } + + this.closed = true + + this.fs.close(this.fd, (error) => { + if (error) { + this.emit('error', error) + } else { + this.emit('close') + } + }) + + this.fd = null + } +} + +/** + * @summary Default options + * @type {Object} + * @constant + */ +BlockWriteStream.defaults = { + fs, + fd: null, + path: null, + flags: 'w', + mode: 0o666, + autoClose: true +} + +module.exports = BlockWriteStream diff --git a/lib/writer/checksum-stream.js b/lib/writer/checksum-stream.js new file mode 100644 index 00000000..7aa05c22 --- /dev/null +++ b/lib/writer/checksum-stream.js @@ -0,0 +1,142 @@ +/* + * Copyright 2017 resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict' + +const stream = require('readable-stream') +const crypto = require('crypto') +const CRC32Stream = require('crc32-stream') +const _ = require('lodash') + +/** + * @summary Get a hash stream + * @function + * @private + * @example + * var md5Stream = getHashStream('md5') + * + * @param {String} algorithm - either `crc32` or anything supported by `crypto.Hash` + * @returns {Stream.Transform} + */ +const getHashStream = (algorithm) => { + if (algorithm === 'crc32') { + return new CRC32Stream() + } + return crypto.createHash(algorithm) +} + +/** + * @summary Create an instance of ChecksumStream + * @name ChecksumStream + * @class + */ +class ChecksumStream extends stream.Transform { + /** + * @summary Create an instance of ChecksumStream + * @name ChecksumStream + * @class + * @param {Object} options - options + * @param {String[]} options.algorithms - hash algorithms + * @example + * var checksum = new ChecksumStream({ + * algorithms: [ 'crc32', 'md5' ] + * }) + * + * checksum.once('checksum', (checksum) => { + * // checksum: { + * // crc32: 'EF28AF1C', + * // md5: '' + * // } + * }) + * + * fs.createReadStream( 'os-image.img' ) + * .pipe( checksum ) + * .pipe( fs.createWriteStream( '/dev/rdisk2' ) ) + * .once( 'finish', () => { ... }) + */ + constructor (options = {}) { + super(options) + this.results = {} + this.algorithms = options.algorithms || [] + this.hashes = _.map(this.algorithms, (algorithm) => { + return this._createHash(algorithm) + }) + } + + /** + * @summary Create & pipe to the Hash streams + * @private + * @param {String[]} algorithm - hash algorithm + * @returns {Stream} + * @example + * const hash = this._createHash(algorithm) + */ + _createHash (algorithm) { + const hash = _.attempt(getHashStream, algorithm) + + if (_.isError(hash)) { + hash.message += ` "${algorithm}"` + throw hash + } + + /** + * @summary Check for all checksums to have been calculated + * @private + * @example + * hash.once('end', check) + */ + const check = () => { + if (_.keys(this.results).length === this.algorithms.length) { + this.emit('checksum', _.clone(this.results)) + } + } + + hash.once('error', (error) => { + return this.emit('error', error) + }) + + if (algorithm === 'crc32') { + hash.once('end', () => { + this.results[algorithm] = hash.digest('hex') + check() + }) + hash.resume() + } else { + hash.once('readable', () => { + this.results[algorithm] = hash.read().toString('hex') + check() + }) + } + + return this.pipe(hash) + } + + /** + * @summary Pass through chunks + * @private + * @param {Buffer} chunk - chunk + * @param {String} encoding - encoding + * @param {Function} next - callback + * @example + * checksumStream.write(buffer) + */ + _transform (chunk, encoding, next) { + this.push(chunk) + next() + } +} + +module.exports = ChecksumStream diff --git a/lib/writer/index.js b/lib/writer/index.js new file mode 100644 index 00000000..dba5aa6b --- /dev/null +++ b/lib/writer/index.js @@ -0,0 +1,311 @@ +/* + * Copyright 2017 resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict' + +const stream = require('readable-stream') +const Pipage = require('pipage') +const BlockMap = require('blockmap') +const BlockStream = require('./block-stream') +const BlockWriteStream = require('./block-write-stream') +const BlockReadStream = require('./block-read-stream') +const ChecksumStream = require('./checksum-stream') +const ProgressStream = require('./progress-stream') +const debug = require('debug')('image-writer') +const EventEmitter = require('events').EventEmitter +const _ = require('lodash') + +/** + * @summary ImageWriter class + * @class + */ +class ImageWriter extends EventEmitter { + /** + * @summary ImageWriter constructor + * @param {Object} options - options + * @example + * new ImageWriter(options) + */ + constructor (options) { + super() + + this.options = options + + this.source = null + this.pipeline = null + this.target = null + + this.hadError = false + + this.bytesRead = 0 + this.bytesWritten = 0 + this.checksum = {} + } + + /** + * @summary Start the writing process + * @returns {ImageWriter} imageWriter + * @example + * imageWriter.write() + */ + write () { + this.hadError = false + + this._createWritePipeline(this.options) + .on('checksum', (checksum) => { + debug('write:checksum', checksum) + this.checksum = checksum + }) + .on('error', (error) => { + this.hadError = true + this.emit('error', error) + }) + + this.target.on('finish', () => { + this.bytesRead = this.source.bytesRead + this.bytesWritten = this.target.bytesWritten + if (this.options.verify) { + this.verify() + } else { + this._emitFinish() + } + }) + + return this + } + + /** + * @summary Start the writing process + * @returns {ImageWriter} imageWriter + * @example + * imageWriter.verify() + */ + verify () { + this._createVerifyPipeline(this.options) + .on('error', (error) => { + this.hadError = true + this.emit('error', error) + }) + .on('checksum', (checksum) => { + debug('verify:checksum', this.checksum, '==', checksum) + if (!_.isEqual(this.checksum, checksum)) { + const error = new Error(`Verification failed: ${JSON.stringify(this.checksum)} != ${JSON.stringify(checksum)}`) + error.code = 'EVALIDATION' + this.emit('error', error) + } + this._emitFinish() + }) + .on('finish', () => { + debug('verify:end') + + // NOTE: As the 'checksum' event only happens after + // the 'finish' event, we `._emitFinish()` there instead of here + }) + + return this + } + + /** + * @summary Abort the flashing process + * @example + * imageWriter.abort() + */ + abort () { + if (this.source) { + this.emit('abort') + this.source.destroy() + } + } + + /** + * @summary Emits the `finish` event with state metadata + * @private + * @example + * this._emitFinish() + */ + _emitFinish () { + this.emit('finish', { + bytesRead: this.bytesRead, + bytesWritten: this.bytesWritten, + checksum: this.checksum + }) + } + + /** + * @summary Creates a write pipeline from given options + * @private + * @param {Object} options - options + * @param {Object} options.image - source image + * @param {Number} [options.fd] - destination file descriptor + * @param {String} [options.path] - destination file path + * @param {String} [options.flags] - destination file open flags + * @param {String} [options.mode] - destination file mode + * @returns {Pipage} pipeline + * @example + * this._createWritePipeline({ + * image: sourceImage, + * path: '/dev/rdisk2' + * }) + */ + _createWritePipeline (options) { + const pipeline = new Pipage({ + readableObjectMode: true + }) + + const image = options.image + const source = image.stream + const progressOptions = { + length: image.size.original, + time: 500 + } + + let progressStream = null + + // If the final size is an estimation, + // use the original source size for progress metering + if (image.size.final.estimation) { + progressStream = new ProgressStream(progressOptions) + pipeline.append(progressStream) + } + + const isPassThrough = image.transform instanceof stream.PassThrough + + // If the image transform is a pass-through, + // ignore it to save on the overhead + if (image.transform && !isPassThrough) { + pipeline.append(image.transform) + } + + // If the final size is known precisely and we're not + // using block maps, then use the final size for progress + if (!image.size.final.estimation && !image.bmap) { + progressOptions.length = image.size.final.value + progressStream = new ProgressStream(progressOptions) + pipeline.append(progressStream) + } + + if (image.bmap) { + const blockMap = BlockMap.parse(image.bmap) + debug('write:bmap', blockMap) + progressStream = new ProgressStream(progressOptions) + pipeline.append(progressStream) + pipeline.append(new BlockMap.FilterStream(blockMap)) + } else { + debug('write:blockstream') + const checksumStream = new ChecksumStream({ + algorithms: options.checksumAlgorithms + }) + pipeline.append(checksumStream) + pipeline.bind(checksumStream, 'checksum') + pipeline.append(new BlockStream()) + } + + const target = new BlockWriteStream({ + fd: options.fd, + path: options.path, + flags: options.flags, + mode: options.mode, + autoClose: false + }) + + // Pipeline.bind(progressStream, 'progress'); + progressStream.on('progress', (state) => { + state.device = options.path + state.type = 'write' + this.emit('progress', state) + }) + + pipeline.bind(source, 'error') + pipeline.bind(target, 'error') + + source.pipe(pipeline) + .pipe(target) + + this.source = source + this.pipeline = pipeline + this.target = target + + return pipeline + } + + /** + * @summary Creates a verification pipeline from given options + * @private + * @param {Object} options - options + * @param {Object} options.image - image + * @param {Number} [options.fd] - file descriptor + * @param {String} [options.path] - file path + * @param {String} [options.flags] - file open flags + * @param {String} [options.mode] - file mode + * @returns {Pipage} pipeline + * @example + * this._createVerifyPipeline({ + * path: '/dev/rdisk2' + * }) + */ + _createVerifyPipeline (options) { + const pipeline = new Pipage() + + const size = options.image.size.final.estimation ? this.bytesWritten : options.image.size.final.value + + const progressStream = new ProgressStream({ + length: size, + time: 500 + }) + + pipeline.append(progressStream) + + if (options.image.bmap) { + debug('verify:bmap') + const blockMap = BlockMap.parse(options.image.bmap) + pipeline.append(new BlockMap.FilterStream(blockMap)) + } else { + const checksumStream = new ChecksumStream({ + algorithms: options.checksumAlgorithms + }) + pipeline.append(checksumStream) + pipeline.bind(checksumStream, 'checksum') + } + + const source = new BlockReadStream({ + fd: options.fd, + path: options.path, + flags: options.flags, + mode: options.mode, + autoClose: false, + start: 0, + end: size + }) + + pipeline.bind(source, 'error') + + progressStream.on('progress', (state) => { + state.device = options.path + state.type = 'check' + this.emit('progress', state) + }) + + this.target = null + this.source = source + this.pipeline = pipeline + + source.pipe(pipeline).resume() + + return pipeline + } +} + +module.exports = ImageWriter diff --git a/lib/writer/progress-stream.js b/lib/writer/progress-stream.js new file mode 100644 index 00000000..cb95400a --- /dev/null +++ b/lib/writer/progress-stream.js @@ -0,0 +1,113 @@ +/* + * Copyright 2017 resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict' + +const Stream = require('stream') +const speedometer = require('speedometer') + +const PERCENT = 100 +const DEFAULT_TIME_MS = 500 + +/** + * @class ProgressStream + * @public + */ +class ProgressStream extends Stream.Transform { + /** + * @summary ProgressStream constructor + * @param {Object} options - options + * @param {Number} options.length - expected total + * @param {Number} [options.time] - time interval to report progress + * @example + * new ProgressStream({ length: 1024 * 1024 }) + * .on('progress', (state) => { + * console.log( state.percentage.toFixed(0) + '%' ) + * }) + */ + constructor (options) { + super(options) + + this.start = 0 + this.interval = options.time || DEFAULT_TIME_MS + this.timer = null + this.meter = speedometer() + + this.delta = 0 + + this.state = { + delta: 0, + eta: 0, + length: options.length, + percentage: 0, + remaining: 0, + runtime: 0, + speed: 0, + transferred: 0 + } + + this.clear = () => { + clearInterval(this.timer) + } + + this.update = () => { + this.state.delta = this.delta + this.state.transferred += this.delta + this.state.percentage = this.state.transferred / this.state.length * PERCENT + this.state.remaining = this.state.length - this.state.transferred + this.state.runtime = Date.now() - this.start + this.state.speed = this.meter(this.state.delta) + this.state.eta = this.state.remaining / this.state.speed + this.delta = 0 + this.emit('progress', this.state) + } + + this.once('end', this.clear) + this.once('end', this.update) + this.once('error', this.clear) + + this.timer = setInterval(this.update, this.interval) + } + + /** + * @summary Transform function + * @private + * @param {Buffer} chunk - chunk + * @param {String} _ - encoding + * @param {Function} next - callback + * @example + * progressStream.write(buffer) + */ + _transform (chunk, _, next) { + this.start = this.start || Date.now() + this.delta += chunk.length + next(null, chunk) + } + + /** + * @summary Destroy handler + * @param {Error} [error] - error + * @param {Function} done - callback + * @example + * progressStream.destroy() + */ + _destroy (error, done) { + this.clear() + done(error) + } +} + +module.exports = ProgressStream diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 866752c3..fe9969be 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -493,6 +493,23 @@ "from": "block-stream@*", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz" }, + "blockmap": { + "version": "2.0.2", + "from": "blockmap@2.0.2", + "resolved": "https://registry.npmjs.org/blockmap/-/blockmap-2.0.2.tgz", + "dependencies": { + "debug": { + "version": "3.1.0", + "from": "debug@>=3.1.0 <4.0.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz" + }, + "ms": { + "version": "2.0.0", + "from": "ms@2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + } + } + }, "bloodline": { "version": "1.0.1", "from": "bloodline@>=1.0.1 <2.0.0", @@ -518,36 +535,9 @@ } }, "bluebird-retry": { - "version": "0.10.1", - "from": "bluebird-retry@>=0.10.1 <0.11.0", - "resolved": "https://registry.npmjs.org/bluebird-retry/-/bluebird-retry-0.10.1.tgz" - }, - "bmapflash": { - "version": "1.2.1", - "from": "bmapflash@>=1.2.1 <2.0.0", - "resolved": "https://registry.npmjs.org/bmapflash/-/bmapflash-1.2.1.tgz", - "dependencies": { - "lodash": { - "version": "4.17.4", - "from": "lodash@>=4.14.2 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz" - }, - "through2": { - "version": "2.0.3", - "from": "through2@^2.0.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz" - }, - "xml2js": { - "version": "0.4.17", - "from": "xml2js@>=0.4.17 <0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.17.tgz" - }, - "xtend": { - "version": "4.0.1", - "from": "xtend@~4.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" - } - } + "version": "0.11.0", + "from": "bluebird-retry@0.11.0", + "resolved": "https://registry.npmjs.org/bluebird-retry/-/bluebird-retry-0.11.0.tgz" }, "boom": { "version": "2.10.1", @@ -637,11 +627,6 @@ "from": "buffer-crc32@>=0.2.1 <0.3.0", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.5.tgz" }, - "buffer-shims": { - "version": "1.0.0", - "from": "buffer-shims@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz" - }, "buffers": { "version": "0.1.1", "from": "buffers@>=0.1.1 <0.2.0", @@ -991,9 +976,9 @@ "resolved": "https://registry.npmjs.org/crc/-/crc-3.4.4.tgz" }, "crc32-stream": { - "version": "1.0.1", - "from": "crc32-stream@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-1.0.1.tgz" + "version": "2.0.0", + "from": "crc32-stream@latest", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz" }, "create-error-class": { "version": "3.0.2", @@ -1099,9 +1084,16 @@ } }, "debug": { - "version": "2.6.0", - "from": "debug@>=2.2.0 <3.0.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.0.tgz" + "version": "2.6.8", + "from": "debug@2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "dependencies": { + "ms": { + "version": "2.0.0", + "from": "ms@2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + } + } }, "decamelize": { "version": "1.2.0", @@ -1274,11 +1266,6 @@ "resolved": "https://registry.npmjs.org/detective/-/detective-4.5.0.tgz", "dev": true }, - "dev-null-stream": { - "version": "0.0.1", - "from": "dev-null-stream@0.0.1", - "resolved": "https://registry.npmjs.org/dev-null-stream/-/dev-null-stream-0.0.1.tgz" - }, "diff": { "version": "1.4.0", "from": "diff@1.4.0", @@ -1299,6 +1286,33 @@ } } }, + "dom-serializer": { + "version": "0.1.0", + "from": "dom-serializer@>=0.0.0 <1.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "from": "domelementtype@>=1.1.1 <1.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz" + } + } + }, + "domelementtype": { + "version": "1.3.0", + "from": "domelementtype@^1.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz" + }, + "domhandler": { + "version": "2.4.1", + "from": "domhandler@^2.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz" + }, + "domutils": { + "version": "1.6.2", + "from": "domutils@^1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.6.2.tgz" + }, "dot-prop": { "version": "4.1.1", "from": "dot-prop@>=4.1.0 <5.0.0", @@ -1767,24 +1781,17 @@ "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", "dev": true }, + "entities": { + "version": "1.1.1", + "from": "entities@^1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz" + }, "env-paths": { "version": "1.0.0", "from": "env-paths@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz", "dev": true }, - "error": { - "version": "7.0.2", - "from": "error@>=7.0.2 <8.0.0", - "resolved": "https://registry.npmjs.org/error/-/error-7.0.2.tgz", - "dependencies": { - "xtend": { - "version": "4.0.1", - "from": "xtend@~4.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" - } - } - }, "error-ex": { "version": "1.3.0", "from": "error-ex@>=1.2.0 <2.0.0", @@ -2166,43 +2173,6 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", "dev": true }, - "etcher-image-write": { - "version": "9.1.3", - "from": "etcher-image-write@9.1.3", - "resolved": "https://registry.npmjs.org/etcher-image-write/-/etcher-image-write-9.1.3.tgz", - "dependencies": { - "bluebird": { - "version": "3.5.0", - "from": "bluebird@>=3.4.7 <4.0.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz" - }, - "debug": { - "version": "2.6.6", - "from": "debug@>=2.6.6 <3.0.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.6.tgz" - }, - "lodash": { - "version": "4.17.4", - "from": "lodash@>=4.17.4 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz" - }, - "ms": { - "version": "0.7.3", - "from": "ms@0.7.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.3.tgz" - }, - "through2": { - "version": "2.0.3", - "from": "through2@>=2.0.1 <3.0.0", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz" - }, - "xtend": { - "version": "4.0.1", - "from": "xtend@~4.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" - } - } - }, "event-emitter": { "version": "0.3.5", "from": "event-emitter@>=0.3.5 <0.4.0", @@ -3046,6 +3016,11 @@ "resolved": "https://registry.npmjs.org/html-tag/-/html-tag-0.2.1.tgz", "dev": true }, + "htmlparser2": { + "version": "3.9.2", + "from": "htmlparser2@>=3.9.2 <4.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz" + }, "http-proxy-agent": { "version": "0.2.7", "from": "http-proxy-agent@>=0.0.0 <1.0.0", @@ -3450,7 +3425,8 @@ "isarray": { "version": "0.0.1", "from": "isarray@0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "dev": true }, "isbinaryfile": { "version": "3.0.2", @@ -4823,7 +4799,8 @@ "ms": { "version": "0.7.2", "from": "ms@0.7.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz" + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "dev": true }, "multistream": { "version": "2.1.0", @@ -8204,7 +8181,8 @@ "object-keys": { "version": "0.4.0", "from": "object-keys@>=0.4.0 <0.5.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz" + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "dev": true }, "object.omit": { "version": "2.0.1", @@ -8442,6 +8420,23 @@ "from": "pinkie-promise@>=2.0.0 <3.0.0", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz" }, + "pipage": { + "version": "1.0.2", + "from": "pipage@latest", + "resolved": "https://registry.npmjs.org/pipage/-/pipage-1.0.2.tgz", + "dependencies": { + "debug": { + "version": "3.1.0", + "from": "debug@>=3.1.0 <3.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz" + }, + "ms": { + "version": "2.0.0", + "from": "ms@2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + } + } + }, "pkg": { "version": "4.1.1", "from": "pkg@4.1.1", @@ -8585,7 +8580,16 @@ "progress-stream": { "version": "1.2.0", "from": "progress-stream@>=1.1.1 <2.0.0", - "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-1.2.0.tgz" + "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-1.2.0.tgz", + "dev": true, + "dependencies": { + "speedometer": { + "version": "0.1.4", + "from": "speedometer@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-0.1.4.tgz", + "dev": true + } + } }, "project-name": { "version": "0.2.6", @@ -8715,14 +8719,29 @@ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz" }, "readable-stream": { - "version": "2.2.2", + "version": "2.3.3", "from": "readable-stream@>=2.0.2 <3.0.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.2.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", "dependencies": { + "inherits": { + "version": "2.0.3", + "from": "inherits@>=2.0.3 <2.1.0", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.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" + }, + "safe-buffer": { + "version": "5.1.1", + "from": "safe-buffer@>=5.1.1 <5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz" + }, + "string_decoder": { + "version": "1.0.3", + "from": "string_decoder@>=1.0.3 <1.1.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz" } } }, @@ -9343,23 +9362,6 @@ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", "dev": true }, - "slice-stream2": { - "version": "2.0.1", - "from": "slice-stream2@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/slice-stream2/-/slice-stream2-2.0.1.tgz", - "dependencies": { - "through2": { - "version": "2.0.3", - "from": "through2@^2.0.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz" - }, - "xtend": { - "version": "4.0.1", - "from": "xtend@>=4.0.0 <4.1.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" - } - } - }, "slide": { "version": "1.1.6", "from": "slide@>=1.1.5 <2.0.0", @@ -9430,9 +9432,9 @@ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.1.tgz" }, "speedometer": { - "version": "0.1.4", - "from": "speedometer@>=0.1.2 <0.2.0", - "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-0.1.4.tgz" + "version": "1.0.0", + "from": "speedometer@1.0.0", + "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-1.0.0.tgz" }, "sprintf-js": { "version": "1.0.3", @@ -9479,23 +9481,6 @@ "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz", "dev": true }, - "stream-chunker": { - "version": "1.2.8", - "from": "stream-chunker@>=1.2.8 <2.0.0", - "resolved": "https://registry.npmjs.org/stream-chunker/-/stream-chunker-1.2.8.tgz", - "dependencies": { - "through2": { - "version": "2.0.3", - "from": "through2@~2.0.0", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz" - }, - "xtend": { - "version": "4.0.1", - "from": "xtend@>=4.0.0 <4.1.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" - } - } - }, "stream-meter": { "version": "1.0.4", "from": "stream-meter@1.0.4", @@ -9511,12 +9496,8 @@ "string_decoder": { "version": "0.10.31", "from": "string_decoder@>=0.10.0 <0.11.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" - }, - "string-template": { - "version": "0.2.1", - "from": "string-template@>=0.2.1 <0.3.0", - "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz" + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "dev": true }, "string-width": { "version": "1.0.1", @@ -9772,11 +9753,13 @@ "version": "0.2.3", "from": "through2@>=0.2.3 <0.3.0", "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", + "dev": true, "dependencies": { "readable-stream": { "version": "1.1.14", "from": "readable-stream@>=1.1.9 <1.2.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz" + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "dev": true } } }, @@ -9795,7 +9778,8 @@ "tmp": { "version": "0.0.31", "from": "tmp@0.0.31", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz" + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", + "dev": true }, "to-file": { "version": "0.2.0", @@ -10268,6 +10252,11 @@ "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", "dev": true }, + "xml": { + "version": "1.0.1", + "from": "xml@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz" + }, "xml2js": { "version": "0.4.17", "from": "xml2js@0.4.17", @@ -10292,7 +10281,8 @@ "xtend": { "version": "2.1.2", "from": "xtend@>=2.1.1 <2.2.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz" + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "dev": true }, "y18n": { "version": "3.2.1", diff --git a/package.json b/package.json index 8def4615..1036876f 100644 --- a/package.json +++ b/package.json @@ -47,14 +47,16 @@ "angular-ui-bootstrap": "2.5.0", "angular-ui-router": "0.4.2", "bindings": "1.2.1", + "blockmap": "2.0.2", "bluebird": "3.4.1", + "bluebird-retry": "0.11.0", "bootstrap-sass": "3.3.6", "chalk": "1.1.3", "command-join": "2.0.0", - "debug": "2.6.0", + "crc32-stream": "2.0.0", + "debug": "2.6.8", "drivelist": "5.2.4", "electron-is-running-in-asar": "1.0.0", - "etcher-image-write": "9.1.3", "file-type": "4.1.0", "flexboxgrid": "6.3.0", "gpt": "1.0.0", @@ -68,17 +70,20 @@ "node-ipc": "8.9.2", "node-stream-zip": "1.3.7", "path-is-inside": "1.0.2", + "pipage": "1.0.2", "pretty-bytes": "1.0.4", "prop-types": "15.5.9", "react": "15.5.4", "react-dom": "15.5.4", "react2angular": "1.1.3", + "readable-stream": "2.3.3", "redux": "3.5.2", "request": "2.81.0", "resin-cli-form": "1.4.1", "resin-cli-visuals": "1.3.1", "resin-corvus": "1.0.0-beta.30", "semver": "5.1.1", + "speedometer": "1.0.0", "sudo-prompt": "6.1.0", "trackjs": "2.3.1", "udif": "0.10.0", diff --git a/tests/image-stream/dmg.spec.js b/tests/image-stream/dmg.spec.js index d0425954..bd305970 100644 --- a/tests/image-stream/dmg.spec.js +++ b/tests/image-stream/dmg.spec.js @@ -73,6 +73,7 @@ describe('ImageStream: DMG', function () { describe('.getImageMetadata()', function () { it('should return the correct metadata', function () { const image = path.join(DMG_PATH, 'etcher-test-zlib.dmg') + const compressedSize = fs.statSync(path.join(DMG_PATH, 'etcher-test-zlib.dmg')).size const uncompressedSize = fs.statSync(path.join(IMAGES_PATH, 'etcher-test.img')).size return imageStream.getImageMetadata(image).then((metadata) => { @@ -80,7 +81,7 @@ describe('ImageStream: DMG', function () { path: image, extension: 'dmg', size: { - original: uncompressedSize, + original: compressedSize, final: { estimation: false, value: uncompressedSize @@ -107,6 +108,7 @@ describe('ImageStream: DMG', function () { describe('.getImageMetadata()', function () { it('should return the correct metadata', function () { const image = path.join(DMG_PATH, 'etcher-test-raw.dmg') + const compressedSize = fs.statSync(path.join(DMG_PATH, 'etcher-test-raw.dmg')).size const uncompressedSize = fs.statSync(path.join(IMAGES_PATH, 'etcher-test.img')).size return imageStream.getImageMetadata(image).then((metadata) => { @@ -114,7 +116,7 @@ describe('ImageStream: DMG', function () { path: image, extension: 'dmg', size: { - original: uncompressedSize, + original: compressedSize, final: { estimation: false, value: uncompressedSize