etcher-sdk
Version:
430 lines • 14.3 kB
JavaScript
"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