UNPKG

etcher-sdk

Version:
351 lines • 14.5 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.pipeSourceToDestinations = exports.decompressThenFlash = exports.DECOMPRESSED_IMAGE_PREFIX = void 0; const fs_1 = require("fs"); const path_1 = require("path"); const block_read_stream_1 = require("./block-read-stream"); const block_transform_stream_1 = require("./block-transform-stream"); const constants_1 = require("./constants"); const compressed_source_1 = require("./source-destination/compressed-source"); const configured_source_1 = require("./source-destination/configured-source/configured-source"); const file_1 = require("./source-destination/file"); const multi_destination_1 = require("./source-destination/multi-destination"); const source_destination_1 = require("./source-destination/source-destination"); const tmp_1 = require("./tmp"); exports.DECOMPRESSED_IMAGE_PREFIX = 'decompressed-image-'; function getEta(current, total, speed) { return speed === 0 ? undefined : (total - current) / speed; } function isWorthDecompressing(filename = '') { return [ '.img', '.bin', '.hddimg', '.raw', '.sdcard', '.rpi-sdimg', '.wic', ].includes((0, path_1.extname)(filename)); } function defaultEnoughSpaceForDecompression(free, imageSize) { return imageSize === undefined ? false : imageSize < Math.min(free / 2, 5 * 1024 ** 3); } async function decompressThenFlash({ source, destinations, onFail, onProgress, verify = false, numBuffers = 16, decompressFirst = false, trim = false, configure, enoughSpaceForDecompression = defaultEnoughSpaceForDecompression, asItIs = false, }) { await source.open(); if (!asItIs) { source = await source.getInnerSource(); } const sourceMetadata = await source.getMetadata(); const enoughDiskSpaceAvailable = enoughSpaceForDecompression(await (0, tmp_1.freeSpace)(), sourceMetadata.size); let decompressedFilePath; try { if (decompressFirst && sourceMetadata.isCompressed && isWorthDecompressing(sourceMetadata.name) && enoughDiskSpaceAvailable) { ({ path: decompressedFilePath } = await (0, tmp_1.tmpFile)({ keepOpen: false, prefix: exports.DECOMPRESSED_IMAGE_PREFIX, })); const decompressedSource = new file_1.File({ path: decompressedFilePath, write: true, }); await decompressedSource.open(); const inputStream = await source.createReadStream(); const outputStream = await decompressedSource.createWriteStream(); await new Promise((resolve, reject) => { outputStream.on('done', resolve); outputStream.on('error', reject); inputStream.on('error', reject); const state = { active: 0, failed: 0, type: 'decompressing', }; const [$onProgress, onRootStreamProgress] = createCompleteOnProgress(onProgress, sourceMetadata, state, false); (0, compressed_source_1.getRootStream)(inputStream).on('progress', onRootStreamProgress); outputStream.on('progress', $onProgress); inputStream.pipe(outputStream); }); source = decompressedSource; } if ((trim || configure !== undefined) && !(source instanceof configured_source_1.ConfiguredSource)) { if (!(await source.canRead())) { console.warn("Can't configure or trim a source that is not randomly readable, skipping"); } else { source = new configured_source_1.ConfiguredSource({ source, shouldTrimPartitions: trim, createStreamFromDisk: decompressFirst || !sourceMetadata.isCompressed, configure, }); await source.open(); } } return await pipeSourceToDestinations({ source, destinations, onFail, onProgress, verify, numBuffers, }); } finally { // don't be fooled by the await, `decompressThenFlash` promise is already resolved; // if you cleandup tmp yourself you might get a race condition if (decompressedFilePath) { try { await fs_1.promises.unlink(decompressedFilePath); } catch (error) { if (error.code === 'ENOENT') { console.warn(`File ${decompressedFilePath} was already deleted`); } else { throw error; // eslint-disable-line no-unsafe-finally } } } } } exports.decompressThenFlash = decompressThenFlash; function createCompleteOnProgress(onProgress, sourceMetadata, state, sparse) { function $onProgress(progress) { // sourceMetadata will be updated by pipeRegularSourceToDestination if (sourceMetadata.size !== undefined && state.size === undefined) { state.size = sourceMetadata.size; } let size; let percentage; let eta; if (sparse) { size = state.blockmappedSize; state.bytesWritten = progress.bytes; } else { size = state.size; state.bytesWritten = progress.position; } if (size !== undefined && state.bytesWritten !== undefined && state.bytesWritten <= size) { percentage = (state.bytesWritten / size) * 100; eta = getEta(state.bytesWritten, size, progress.speed); } else if (state.rootStreamSpeed !== undefined && state.rootStreamPosition !== undefined && state.compressedSize !== undefined) { percentage = (state.rootStreamPosition / state.compressedSize) * 100; eta = getEta(state.rootStreamPosition, state.compressedSize, state.rootStreamSpeed); } const result = { ...progress, ...state, percentage, eta, }; onProgress(result); } function onRootStreamProgress(progress) { state.rootStreamPosition = progress.position; state.rootStreamSpeed = progress.speed; state.rootStreamAverageSpeed = progress.averageSpeed; } return [$onProgress, onRootStreamProgress]; } // This function is the most common use case of the SDK. // Added it here to avoid duplicating it in other projects. async function pipeSourceToDestinations({ source, destinations, onFail, onProgress, verify = false, numBuffers = 16, }) { if (numBuffers < 2) { numBuffers = 2; } const destination = new multi_destination_1.MultiDestination(destinations); const failures = new Map(); const state = { active: destination.destinations.size, failed: 0, type: 'flashing', }; destination.on('fail', _onFail); await Promise.all([source.open(), destination.open()]); const [sourceMetadata, sparseSource, sparseDestination] = await Promise.all([ source.getMetadata(), source.canCreateSparseReadStream(), destination.canCreateSparseWriteStream(), ]); const sparse = sparseSource && sparseDestination; state.sparse = sparse; state.size = sourceMetadata.size; state.compressedSize = sourceMetadata.compressedSize; state.blockmappedSize = sourceMetadata.blockmappedSize; function updateState(step) { if (step !== undefined) { state.type = step; } state.failed = failures.size; state.active = destination.destinations.size - state.failed; } function _onFail(error) { failures.set(error.destination, error.error); updateState(); onFail(error.destination, error.error); } const [$onProgress, onRootStreamProgress] = createCompleteOnProgress(onProgress, sourceMetadata, state, sparse); if (sparse) { await pipeSparseSourceToDestination(source, destination, verify, numBuffers, updateState, _onFail, $onProgress, onRootStreamProgress); } else { await pipeRegularSourceToDestination(source, sourceMetadata, destination, verify, numBuffers, updateState, _onFail, $onProgress, onRootStreamProgress); } updateState('finished'); await Promise.all([source.close(), destination.close()]); return { sourceMetadata, failures, bytesWritten: state.bytesWritten || 0 }; } exports.pipeSourceToDestinations = pipeSourceToDestinations; function notUndefined(x) { return x !== undefined; } function getAlignment(...devices) { const alignments = devices.map((d) => d.getAlignment()).filter(notUndefined); if (alignments.length) { return Math.max(...alignments); } } async function pipeRegularSourceToDestination(source, sourceMetadata, destination, verify, numBuffers, updateState, onFail, onProgress, onRootStreamProgress) { let lastPosition = 0; const emitSourceProgress = sourceMetadata.size === undefined || sourceMetadata.isSizeEstimated; const alignment = getAlignment(source, destination); const highWaterMark = alignment === undefined ? undefined : numBuffers - 1; const [sourceStream, destinationStream] = await Promise.all([ source.createReadStream({ emitProgress: emitSourceProgress, alignment, numBuffers, }), destination.createWriteStream({ highWaterMark }), ]); (0, compressed_source_1.getRootStream)(sourceStream).on('progress', (progress) => { onRootStreamProgress(progress); }); const checksum = await new Promise((resolve, reject) => { let result; let done = false; let hasher; function maybeDone(maybeChecksum) { if (maybeChecksum !== undefined) { result = maybeChecksum; } else { done = true; } if (done && (!verify || destination.activeDestinations.size === 0 || result !== undefined)) { if (hasher !== undefined) { sourceStream.unpipe(hasher); hasher.end(); } resolve(result); } } sourceStream.once('error', reject); destinationStream.on('fail', onFail); // This is emitted by MultiDestination when one of its destinations fails destinationStream.once('error', reject); if (verify) { hasher = (0, source_destination_1.createHasher)(); hasher.once('checksum', maybeDone); sourceStream.pipe(hasher); } destinationStream.once('done', maybeDone); destinationStream.on('progress', (progress) => { lastPosition = progress.position; onProgress(progress); }); if (alignment !== undefined && !(sourceStream instanceof block_read_stream_1.BlockReadStream || sourceStream instanceof block_transform_stream_1.BlockTransformStream)) { // The destination needs data to be aligned and it isn't. // Pass it through a BlockTransformStream to align it. sourceStream .pipe(new block_transform_stream_1.BlockTransformStream({ chunkSize: constants_1.CHUNK_SIZE, alignment, numBuffers, })) .pipe(destinationStream); } else { sourceStream.pipe(destinationStream); } }); if (sourceMetadata.size === undefined || sourceMetadata.isSizeEstimated === true) { sourceMetadata.size = lastPosition; sourceMetadata.isSizeEstimated = false; } if (verify && checksum) { updateState('verifying'); const verifier = destination.createVerifier(checksum, lastPosition); await runVerifier(verifier, onFail, onProgress); } } async function pipeSparseSourceToDestination(source, destination, verify, numBuffers, updateState, onFail, onProgress, onRootStreamProgress) { const alignment = getAlignment(source, destination); const highWaterMark = alignment === undefined ? undefined : numBuffers - 1; const [sourceStream, destinationStream] = await Promise.all([ source.createSparseReadStream({ generateChecksums: verify, alignment, numBuffers, }), destination.createSparseWriteStream({ highWaterMark }), ]); (0, compressed_source_1.getRootStream)(sourceStream).on('progress', (progress) => { onRootStreamProgress(progress); }); await new Promise((resolve, reject) => { sourceStream.once('error', reject); destinationStream.once('error', reject); destinationStream.once('done', resolve); destinationStream.on('fail', onFail); // This is emitted by MultiDestination when one of its destinations fails destinationStream.on('progress', onProgress); sourceStream.pipe(destinationStream); }); if (verify) { updateState('verifying'); const verifier = destination.createVerifier(sourceStream.blocks); await runVerifier(verifier, onFail, onProgress); } } async function runVerifier(verifier, onFail, onProgress) { await new Promise((resolve, reject) => { verifier.once('error', reject); verifier.once('finish', resolve); verifier.on('fail', onFail); verifier.on('progress', onProgress); verifier.run(); }); } //# sourceMappingURL=multi-write.js.map