UNPKG

etcher-sdk

Version:
430 lines • 14.3 kB
"use strict"; /* * Copyright 2018 balena.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. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.SourceDestination = exports.SparseStreamVerifier = exports.StreamVerifier = exports.Verifier = exports.SourceDestinationFs = exports.createHasher = exports.ProgressHashStream = exports.CountingHashStream = void 0; const stream_1 = require("stream"); const events_1 = require("events"); const file_type_1 = require("file-type"); const partitioninfo_1 = require("partitioninfo"); const path_1 = require("path"); const xxhash_addon_1 = require("xxhash-addon"); const aligned_lockable_buffer_1 = require("../aligned-lockable-buffer"); const constants_1 = require("../constants"); const errors_1 = require("../errors"); const sparse_filter_stream_1 = require("../sparse-stream/sparse-filter-stream"); const sparse_read_stream_1 = require("../sparse-stream/sparse-read-stream"); const utils_1 = require("../utils"); const progress_1 = require("./progress"); class HashStream extends stream_1.Transform { constructor(seed, outEnc) { super(); if (outEnc && typeof outEnc !== 'string' && !Buffer.isBuffer(outEnc)) { outEnc = 'buffer'; } this._hash = new xxhash_addon_1.XXHash3(seed); } _transform(chunk, _encoding, callback) { this._hash.update(chunk); callback(); } _flush(callback) { this.push(this._hash.digest()); callback(); } } class CountingHashStream extends HashStream { constructor() { super(...arguments); this.bytesWritten = 0; } async __transform(chunk, encoding) { const unlock = (0, aligned_lockable_buffer_1.isAlignedLockableBuffer)(chunk) ? await chunk.rlock() : undefined; try { await (0, utils_1.fromCallback)((callback) => { super._transform(chunk, encoding, callback); }); } finally { unlock === null || unlock === void 0 ? void 0 : unlock(); } this.bytesWritten += chunk.length; } _transform(chunk, encoding, callback) { void (0, utils_1.asCallback)(this.__transform(chunk, encoding), callback); } } exports.CountingHashStream = CountingHashStream; exports.ProgressHashStream = (0, progress_1.makeClassEmitProgressEvents)(CountingHashStream, 'bytesWritten', 'bytesWritten'); function createHasher() { const hasher = new exports.ProgressHashStream(constants_1.XXHASH_SEED, 'buffer'); hasher.on('finish', async () => { const checksum = (await (0, utils_1.streamToBuffer)(hasher)).toString('hex'); hasher.emit('checksum', checksum); }); return hasher; } exports.createHasher = createHasher; class SourceDestinationFs { // Adapts a SourceDestination to an fs like interface (so it can be used in udif for example) constructor(source) { this.source = source; } open(_path, _options, callback) { callback(null, 1); } close(_fd, callback) { callback(null); } fstat(_fd, callback) { this.source .getMetadata() .then((metadata) => { if (metadata.size === undefined) { callback(new Error('No size')); return; } callback(null, { size: metadata.size }); }) .catch(callback); } read(_fd, buffer, bufferOffset, length, sourceOffset, callback) { this.source .read(buffer, bufferOffset, length, sourceOffset) .then((res) => { callback(null, res.bytesRead, res.buffer); }) .catch(callback); } } exports.SourceDestinationFs = SourceDestinationFs; class Verifier extends events_1.EventEmitter { constructor() { super(...arguments); this.progress = { bytes: 0, position: 0, speed: 0, averageSpeed: 0, }; } handleEventsAndPipe(stream, meter) { meter.on('progress', (progress) => { this.progress = progress; this.emit('progress', progress); }); stream.on('end', this.emit.bind(this, 'end')); meter.on('finish', this.emit.bind(this, 'finish')); stream.once('error', () => { stream.unpipe(meter); meter.end(); }); stream.pipe(meter); } } exports.Verifier = Verifier; class StreamVerifier extends Verifier { constructor(source, checksum, size) { super(); this.source = source; this.checksum = checksum; this.size = size; } async run() { const stream = await this.source.createReadStream({ end: this.size - 1, alignment: this.source.getAlignment(), }); stream.on('error', this.emit.bind(this, 'error')); const hasher = createHasher(); hasher.on('error', this.emit.bind(this, 'error')); hasher.on('checksum', (streamChecksum) => { if (streamChecksum !== this.checksum) { this.emit('error', new errors_1.ChecksumVerificationError(`Source and destination checksums do not match: ${this.checksum} !== ${streamChecksum}`, streamChecksum, this.checksum)); } }); this.handleEventsAndPipe(stream, hasher); } } exports.StreamVerifier = StreamVerifier; class SparseStreamVerifier extends Verifier { constructor(source, blocks) { super(); this.source = source; this.blocks = blocks; } async run() { const alignment = this.source.getAlignment(); let stream; if (await this.source.canRead()) { stream = new sparse_read_stream_1.SparseReadStream({ source: this.source, blocks: this.blocks, chunkSize: constants_1.CHUNK_SIZE, verify: true, generateChecksums: false, alignment, numBuffers: 2, }); stream.on('error', this.emit.bind(this, 'error')); } else if (await this.source.canCreateReadStream()) { const originalStream = await this.source.createReadStream(); originalStream.once('error', (error) => { originalStream.unpipe(transform); this.emit('error', error); }); const transform = new sparse_filter_stream_1.SparseFilterStream({ blocks: this.blocks, verify: true, generateChecksums: false, }); transform.once('error', (error) => { originalStream.unpipe(transform); // @ts-expect-error destroy is not in the Node.js typings if (typeof originalStream.destroy === 'function') { // @ts-expect-error destroy is not in the Node.js typings originalStream.destroy(); } this.emit('error', error); }); originalStream.pipe(transform); stream = transform; } else { throw new errors_1.NotCapable(); } const meter = new progress_1.ProgressWritable({ objectMode: true }); this.handleEventsAndPipe(stream, meter); } } exports.SparseStreamVerifier = SparseStreamVerifier; class SourceDestination extends events_1.EventEmitter { constructor() { super(...arguments); this.isOpen = false; } static register(Cls) { if (Cls.mimetype !== undefined) { SourceDestination.mimetypes.set(Cls.mimetype, Cls); } } getAlignment() { return undefined; } async canRead() { return false; } async canWrite() { return false; } async canCreateReadStream() { return false; } async canCreateSparseReadStream() { return false; } async canCreateWriteStream() { return false; } async canCreateSparseWriteStream() { return false; } async getMetadata() { if (this.metadata === undefined) { this.metadata = await this._getMetadata(); } return this.metadata; } async _getMetadata() { return {}; } async read(_buffer, // eslint-disable-line @typescript-eslint/no-unused-vars _bufferOffset, // eslint-disable-line @typescript-eslint/no-unused-vars _length, // eslint-disable-line @typescript-eslint/no-unused-vars _sourceOffset) { throw new errors_1.NotCapable(); } async write(_buffer, // eslint-disable-line @typescript-eslint/no-unused-vars _bufferOffset, // eslint-disable-line @typescript-eslint/no-unused-vars _length, // eslint-disable-line @typescript-eslint/no-unused-vars _fileOffset) { throw new errors_1.NotCapable(); } async createReadStream(_options) { throw new errors_1.NotCapable(); } async createSparseReadStream(_options) { throw new errors_1.NotCapable(); } async getBlocks() { throw new errors_1.NotCapable(); } async createWriteStream(_options) { throw new errors_1.NotCapable(); } async createSparseWriteStream(_options) { throw new errors_1.NotCapable(); } async open() { if (!this.isOpen) { await this._open(); this.isOpen = true; } } async close() { if (this.isOpen) { await this._close(); this.isOpen = false; } } async _open() { // noop } async _close() { // noop } createVerifier(checksumOrBlocks, size) { if (Array.isArray(checksumOrBlocks)) { for (const block of checksumOrBlocks) { if (block.checksumType === undefined || block.checksum === undefined) { throw new Error('Block is missing checksum or checksumType attributes, can not create verifier'); } } return new SparseStreamVerifier(this, checksumOrBlocks); } else { if (size === undefined) { throw new Error('A size argument is required for creating a stream checksum verifier'); } return new StreamVerifier(this, checksumOrBlocks, size); } } async getMimeTypeFromName() { const metadata = await this.getMetadata(); if (metadata.name === undefined) { return; } const extension = (0, path_1.extname)(metadata.name).toLowerCase(); if (extension === '.dmg') { return 'application/x-apple-diskimage'; } } async getMimeTypeFromContent() { let stream; try { stream = await this.createReadStream({ end: 263, alignment: this.getAlignment(), }); } catch (error) { if (error instanceof errors_1.NotCapable) { return; } throw error; } try { const ft = await (0, file_type_1.fromStream)(stream); if (ft !== undefined && ft !== null) { return ft.mime; } } catch (error) { console.log("Can't read stream to buffer"); throw error; } } async getInnerSourceHelper(mimetype) { if (mimetype === undefined) { return this; } const Cls = SourceDestination.mimetypes.get(mimetype); if (Cls === undefined) { return this; } if (Cls.requiresRandomReadableSource && !(await this.canRead())) { throw new errors_1.NotCapable(`Can not read a ${Cls.name} from a ${this.constructor.name}.`); } const innerSource = new Cls(this); return innerSource.getInnerSource(); } async getInnerSource() { await this.open(); const metadata = await this.getMetadata(); if (metadata.isEtch === true) { return this; } let mimetype = await this.getMimeTypeFromName(); if (mimetype !== undefined) { try { return await this.getInnerSourceHelper(mimetype); } catch (error) { if (error instanceof errors_1.NotCapable) { throw error; } // File extension may be wrong, try content. } } try { mimetype = await this.getMimeTypeFromContent(); } catch (e) { if (e.code === 'EISDIR') { throw e; } // expected to die on directories console.log("Can't get mimetype from content", e.code); } return this.getInnerSourceHelper(mimetype); } async getPartitionTable() { const stream = await this.createReadStream({ end: 34 * 512, alignment: this.getAlignment(), }); try { const buffer = await (0, utils_1.streamToBuffer)(stream); try { return await (0, partitioninfo_1.getPartitions)(buffer, { getLogical: false }); } catch (_a) { // no partitions } } catch (error) { console.log("Can't read to buffer to get partitions"); throw error; } } } exports.SourceDestination = SourceDestination; SourceDestination.imageExtensions = [ 'img', 'iso', 'bin', 'dsk', 'hddimg', 'raw', 'dmg', 'sdcard', 'rpi-sdimg', 'wic', ]; SourceDestination.mimetypes = new Map(); //# sourceMappingURL=source-destination.js.map