hackpro-sdk
Version:
372 lines • 15.4 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.
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const blockmap_1 = require("blockmap");
const bluebird_1 = require("bluebird");
const lodash_1 = require("lodash");
const path_1 = require("path");
const readable_stream_1 = require("readable-stream");
const yauzl_1 = require("yauzl");
const constants_1 = require("../constants");
const zip_1 = require("../zip");
const source_destination_1 = require("./source-destination");
const source_source_1 = require("./source-source");
const errors_1 = require("../errors");
const shared_1 = require("../sparse-stream/shared");
const sparse_filter_stream_1 = require("../sparse-stream/sparse-filter-stream");
const stream_limiter_1 = require("../stream-limiter");
const utils_1 = require("../utils");
function blockmapToBlocks(blockmap) {
return blockmap.ranges.map((range) => {
const offset = range.start * blockmap.blockSize;
const length = (range.end - range.start + 1) * blockmap.blockSize;
const checksum = range.checksum;
const checksumType = blockmap.checksumType === 'sha1' || blockmap.checksumType === 'sha256'
? blockmap.checksumType
: undefined;
return { checksum, checksumType, blocks: [{ offset, length }] };
});
}
function matchSupportedExtensions(filename) {
const extension = path_1.posix.extname(filename);
return (extension.length > 1 &&
source_destination_1.SourceDestination.imageExtensions.includes(extension.slice(1)));
}
exports.matchSupportedExtensions = matchSupportedExtensions;
class StreamZipSource extends source_source_1.SourceSource {
constructor(source, match = matchSupportedExtensions) {
super(source);
this.match = match;
}
canCreateReadStream() {
return __awaiter(this, void 0, void 0, function* () {
return true;
});
}
getEntry() {
return __awaiter(this, void 0, void 0, function* () {
if (this.entry === undefined) {
const entry = yield zip_1.getFileStreamFromZipStream(yield this.source.createReadStream(false), this.match);
this.entry = entry;
const onData = () => {
// We need to reset the entry if any read happens on this stream
entry.removeListener('data', onData);
this.entry = undefined;
};
entry.on('data', onData);
entry.pause();
}
return this.entry;
});
}
createReadStream(_emitProgress = false, start = 0, end) {
return __awaiter(this, void 0, void 0, function* () {
if (start !== 0) {
throw new errors_1.NotCapable();
}
const stream = yield this.getEntry();
if (end !== undefined) {
// TODO: handle errors on stream after transform finsh event
const transform = new stream_limiter_1.StreamLimiter(stream, end + 1);
return transform;
}
return stream;
});
}
_getMetadata() {
return __awaiter(this, void 0, void 0, function* () {
const entry = yield this.getEntry();
return {
size: entry.size,
compressedSize: entry.compressedSize,
name: path_1.posix.basename(entry.path),
};
});
}
}
exports.StreamZipSource = StreamZipSource;
class SourceRandomAccessReader extends yauzl_1.RandomAccessReader {
constructor(source) {
super();
this.source = source;
}
_readStreamForRange(start, end) {
// _readStreamForRange end is exclusive
// this.source.createReadStream end is inclusive
// Workaround this method not being async with a passthrough stream
const passthrough = new readable_stream_1.PassThrough();
this.source
.createReadStream(false, start, end - 1)
.then(stream => {
stream.on('error', passthrough.emit.bind(passthrough, 'error'));
stream.pipe(passthrough);
})
.catch(passthrough.emit.bind(passthrough, 'error'));
return passthrough;
}
}
class RandomAccessZipSource extends source_source_1.SourceSource {
constructor(source, match = matchSupportedExtensions) {
super(source);
this.match = match;
this.entries = [];
this.ready = this.init();
}
init() {
return __awaiter(this, void 0, void 0, function* () {
yield this.source.open();
const sourceMetadata = yield this.source.getMetadata();
const reader = new SourceRandomAccessReader(this.source);
this.zip = yield bluebird_1.fromCallback((callback) => {
if (sourceMetadata.size === undefined) {
throw new errors_1.NotCapable();
}
yauzl_1.fromRandomAccessReader(reader, sourceMetadata.size, { autoClose: false }, callback);
});
this.zip.on('entry', (entry) => {
this.entries.push(entry);
});
yield new Promise((resolve, reject) => {
this.zip.on('end', resolve);
this.zip.on('error', reject);
});
});
}
canCreateReadStream() {
return __awaiter(this, void 0, void 0, function* () {
return true;
});
}
canCreateSparseReadStream() {
return __awaiter(this, void 0, void 0, function* () {
const metadata = yield this.getMetadata();
return metadata.blockMap !== undefined;
});
}
getEntries() {
return __awaiter(this, void 0, void 0, function* () {
yield this.ready;
return this.entries;
});
}
getImageEntry() {
return __awaiter(this, void 0, void 0, function* () {
let entries = (yield this.getEntries()).filter(e => this.match(e.fileName));
if (entries.length === 0) {
throw new Error(constants_1.NO_MATCHING_FILE_MSG);
}
entries = lodash_1.sortBy(entries, 'uncompressedSize');
const entry = entries[entries.length - 1];
if (entry.compressionMethod !== 0 && entry.compressionMethod !== 8) {
throw new Error(`unsupported compression method: ${entry.compressionMethod}`);
}
return entry;
});
}
_open() {
return __awaiter(this, void 0, void 0, function* () {
yield this.ready;
// We only want to run this for the error it may throw if there is no disk image in the zip
yield this.getImageEntry();
});
}
getEntryByName(name) {
return __awaiter(this, void 0, void 0, function* () {
const entries = yield this.getEntries();
for (const entry of entries) {
if (entry.fileName === name) {
return entry;
}
}
});
}
getStream(name) {
return __awaiter(this, void 0, void 0, function* () {
const entry = yield this.getEntryByName(name);
if (entry !== undefined) {
return yield bluebird_1.fromCallback((callback) => {
// yauzl does not support start / end for compressed entries
this.zip.openReadStream(entry, callback);
});
}
});
}
getString(name) {
return __awaiter(this, void 0, void 0, function* () {
const stream = yield this.getStream(name);
if (stream !== undefined) {
const buffer = yield utils_1.streamToBuffer(stream);
return buffer.toString();
}
});
}
getJson(name) {
return __awaiter(this, void 0, void 0, function* () {
const data = yield this.getString(name);
if (data !== undefined) {
return JSON.parse(data);
}
});
}
createReadStream(_emitProgress = false, start = 0, end) {
return __awaiter(this, void 0, void 0, function* () {
if (start !== 0) {
throw new errors_1.NotCapable();
}
const entry = yield this.getImageEntry();
const stream = yield this.getStream(entry.fileName);
if (stream === undefined) {
throw new errors_1.NotCapable();
}
if (end !== undefined) {
// TODO: handle errors on stream after transform finish event
const transform = new stream_limiter_1.StreamLimiter(stream, end + 1);
return transform;
}
return stream;
});
}
createSparseReadStream(generateChecksums = false) {
return __awaiter(this, void 0, void 0, function* () {
const metadata = yield this.getMetadata();
if (metadata.blocks === undefined) {
throw new errors_1.NotCapable();
}
// Verifying and generating checksums makes no sense, so we only verify if generateChecksums is false.
const transform = new sparse_filter_stream_1.SparseFilterStream(metadata.blocks, !generateChecksums, generateChecksums);
const stream = yield this.createReadStream(false);
stream.pipe(transform);
return transform;
});
}
_getMetadata() {
return __awaiter(this, void 0, void 0, function* () {
const entry = yield this.getImageEntry();
const result = {
size: entry.uncompressedSize,
compressedSize: entry.compressedSize,
};
const prefix = path_1.posix.join(path_1.posix.dirname(entry.fileName), '.meta');
result.logo = yield this.getString(path_1.posix.join(prefix, 'logo.svg'));
result.instructions = yield this.getString(path_1.posix.join(prefix, 'instructions.markdown'));
const blockMap = yield this.getString(path_1.posix.join(prefix, 'image.bmap'));
if (blockMap !== undefined) {
result.blockMap = blockmap_1.BlockMap.parse(blockMap);
result.blocks = blockmapToBlocks(result.blockMap);
result.blockmappedSize = shared_1.blocksLength(result.blocks);
}
let manifest;
try {
manifest = yield this.getJson(path_1.posix.join(prefix, 'manifest.json'));
}
catch (error) {
throw new Error('Invalid archive manifest.json');
}
let name;
if (manifest !== undefined) {
name = manifest.name;
for (const key of RandomAccessZipSource.manifestFields) {
result[key] = manifest[key];
}
}
result.name = name || path_1.posix.basename(entry.fileName);
if (result.logo || result.instructions || result.blockMap || manifest) {
result.isEtch = true;
}
return result;
});
}
}
exports.RandomAccessZipSource = RandomAccessZipSource;
RandomAccessZipSource.manifestFields = [
'bytesToZeroOutFromTheBeginning',
'checksum',
'checksumType',
'recommendedDriveSize',
'releaseNotesUrl',
'supportUrl',
'url',
'version',
];
class ZipSource extends source_source_1.SourceSource {
constructor(source, preferStreamSource = false, match = matchSupportedExtensions) {
super(source);
this.preferStreamSource = preferStreamSource;
this.match = match;
}
prepare() {
return __awaiter(this, void 0, void 0, function* () {
if (this.implementation === undefined) {
if (!this.preferStreamSource && (yield this.source.canRead())) {
this.implementation = new RandomAccessZipSource(this.source, this.match);
}
else {
this.implementation = new StreamZipSource(this.source, this.match);
}
}
});
}
canCreateReadStream() {
return __awaiter(this, void 0, void 0, function* () {
yield this.prepare();
return yield this.implementation.canCreateReadStream();
});
}
open() {
return __awaiter(this, void 0, void 0, function* () {
yield this.prepare();
return yield this.implementation.open();
});
}
canCreateSparseReadStream() {
return __awaiter(this, void 0, void 0, function* () {
yield this.prepare();
return yield this.implementation.canCreateSparseReadStream();
});
}
createReadStream(emitProgress = false, start = 0, end) {
return __awaiter(this, void 0, void 0, function* () {
yield this.prepare();
return yield this.implementation.createReadStream(emitProgress, start, end);
});
}
createSparseReadStream(generateChecksums = false) {
return __awaiter(this, void 0, void 0, function* () {
yield this.prepare();
return yield this.implementation.createSparseReadStream(generateChecksums);
});
}
_getMetadata() {
return __awaiter(this, void 0, void 0, function* () {
yield this.prepare();
return yield this.implementation.getMetadata();
});
}
}
exports.ZipSource = ZipSource;
ZipSource.mimetype = 'application/zip';
source_destination_1.SourceDestination.register(ZipSource);
//# sourceMappingURL=zip.js.map