UNPKG

blockmap

Version:
193 lines 7.44 kB
"use strict"; /** * @license * Copyright 2019 Balena Ltd. * * 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.ReadStream = void 0; const tslib_1 = require("tslib"); const crypto_1 = require("crypto"); const debug_1 = require("debug"); const fs_1 = require("fs"); const stream_1 = require("stream"); const chunk_1 = require("./chunk"); const read_range_1 = require("./read-range"); const debug = debug_1.debug('blockmap:readstream'); class ReadStream extends stream_1.Readable { constructor(fdOrReadFn, blockMap, verify = true, generateChecksums = false, start = 0, end = Infinity, chunkSize = 64 * 1024) { super({ objectMode: true }); this.blockMap = blockMap; this.verify = verify; this.generateChecksums = generateChecksums; this.start = start; this.end = end; this.chunkSize = chunkSize; /** Number of block map ranges read */ this.rangesRead = 0; /** Number of block map ranges verified */ this.rangesVerified = 0; /** Number of blocks read */ this.blocksRead = 0; /** Number of bytes read */ this.bytesRead = 0; /** Current offset in bytes */ this.position = 0; if (verify && generateChecksums) { throw new Error('verify and generateChecksums options are mutually exclusive'); } if (start < 0) { throw new Error('Start must not be negative'); } if (start > end) { throw new Error('Start must be less or equal to end'); } if (verify || generateChecksums) { this._hash = crypto_1.createHash(blockMap.checksumType); } this.ranges = this._prepareRanges(); if (typeof fdOrReadFn === 'number') { this.readFn = (buf, offset, length, position) => { return new Promise((resolve, reject) => { fs_1.read(fdOrReadFn, buf, offset, length, position, (error, bytesRead, buffer) => { if (error) { reject(error); } else { resolve({ bytesRead, buffer }); } }); }); }; } else { this.readFn = fdOrReadFn; } } /** * Preprocess the `blockMap`'s ranges into byte-ranges * with respect to the `start` offset, and an `offset` * for tracking chunked range reading */ _prepareRanges() { return this.blockMap.ranges .map((range) => { const readRange = new read_range_1.ReadRange(range, this.blockMap.blockSize); // Account for readstream's start offset readRange.start += this.start; readRange.end += this.start; return readRange; }) .filter((readRange) => { return readRange.end <= this.end + this.start; }); } /** * Verify a fully read range's checksum against * the range's checksum from the blockmap */ _verifyRange() { if (this.currentRange === undefined || this._hash === undefined) { return; } const digest = this._hash.digest('hex'); debug('verify:checksum', this.currentRange.checksum); debug('verify:digest ', digest); this._hash = crypto_1.createHash(this.blockMap.checksumType); if (this.verify && this.currentRange.checksum !== digest) { const error = new read_range_1.ReadRangeError(`Invalid checksum for range [${this.currentRange.startLBA},${this.currentRange.endLBA}], bytes ${this.currentRange.start}-${this.currentRange.end}`, this.currentRange, digest); this.emit('error', error); } if (this.generateChecksums) { this.currentRange.range.checksum = digest; } else { this.rangesVerified++; } } /** * Read the current range (or a chunk thereof), * update state and emit the read block */ _readBlock() { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (this.currentRange === undefined) { return; } const length = Math.min(this.currentRange.length - this.currentRange.offset, this.chunkSize); const position = this.currentRange.start + this.currentRange.offset; const chunk = new chunk_1.Chunk(Buffer.allocUnsafe(length), position); debug('read-block:position', position); debug('read-block:length', length); try { const { bytesRead } = yield this.readFn(chunk.buffer, 0, length, position); if (bytesRead !== length) { throw new Error(`Bytes read mismatch: ${bytesRead} != ${length}`); } this.currentRange.offset += bytesRead; this.blocksRead += bytesRead / this.blockMap.blockSize; this.bytesRead += bytesRead; this.position += bytesRead; debug('read-block:blocksRead', this.blocksRead); // Feed the hash if we're verifying if (this._hash !== undefined) { this._hash.update(chunk.buffer); } this.push(chunk); } catch (error) { this.emit('error', error); } }); } /** * Advance to next the Range if there is one then read a block; * else end the stream; * @see https://nodejs.org/api/stream.html#stream_implementing_a_readable_stream */ _advanceRange() { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (this.ranges.length > 0) { this.currentRange = this.ranges.shift(); this.rangesRead++; debug('read:range %O', this.currentRange); yield this._readBlock(); } else { this.push(null); } }); } /** * Initiate a new read, advancing the range if necessary, * and verifying checksums, if enabled * @see https://nodejs.org/api/stream.html#stream_implementing_a_readable_stream */ _read() { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (this.currentRange === undefined) { yield this._advanceRange(); } else if (this.currentRange.offset === this.currentRange.length) { this._verifyRange(); yield this._advanceRange(); } else { yield this._readBlock(); } }); } } exports.ReadStream = ReadStream; //# sourceMappingURL=read-stream.js.map