etcher-sdk
Version:
283 lines • 12 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.URLCompressedSource = void 0;
const CombinedStream = require("combined-stream");
const file_disk_1 = require("file-disk");
const gzip_stream_1 = require("gzip-stream");
const compressed_source_utils_1 = require("./compressed-source-utils");
const node_stream_1 = require("node:stream");
const raw_deflate_zip_stream_1 = require("./raw-deflate-zip-stream");
const zlib_1 = require("zlib");
const axios_1 = require("axios");
const configure_1 = require("./configured-source/configure");
const configure_2 = require("./configured-source/operations/configure");
const copy_1 = require("./configured-source/operations/copy");
const source_destination_1 = require("./source-destination");
const errors_1 = require("../errors");
const stream_limiter_1 = require("../stream-limiter");
const utils_1 = require("../utils");
/**
* URLCompressedSource - Downloads and streams compressed images from direct URLs
* Similar to BalenaS3CompressedSource but uses pre-configured URLs instead of S3 paths
*
* Supports regular disk images (zipped or gzipped) and edison zip archives (zip only as it contains many files).
* No random reads, you can't use this with ConfiguredSource.
* Instead it handles the configuration on its own.
* Partial compressed files are streamed from URLs.
* If a partition (or disk image in the zip archive for edisons) needs configuration, it is downloaded, decompressed and recompressed.
* The complete compressed stream is created from the partial compressed files from URLs and the configured parts described above.
*/
class URLCompressedSource extends source_destination_1.SourceDestination {
constructor({ urls, format, filenamePrefix, configuration, buildId, deviceType, }) {
super();
this.configuredParts = new Map();
this.urls = urls;
this.format = format;
this.filenamePrefix = filenamePrefix;
this.configuration = configuration;
this.buildId = buildId;
this.deviceType = deviceType;
}
getFilename() {
return [
this.filenamePrefix,
this.deviceType,
this.osVersion,
this.buildId.endsWith('.dev') ? 'dev' : undefined,
this.supervisorVersion,
]
.filter((p) => p !== undefined)
.join('-');
}
async _getMetadata() {
var _a;
return {
supervisorVersion: this.supervisorVersion,
osVersion: this.osVersion,
lastModified: this.lastModified,
size: this.size,
version: this.buildId,
name: this.filename,
format: this.format,
arch: (_a = this.deviceTypeJSON) === null || _a === void 0 ? void 0 : _a.arch,
};
}
async getSupervisorVersion() {
const response = await this.download('VERSION');
const lastModified = new Date(response.headers['last-modified']);
const supervisorVersion = response.data.trim();
return { supervisorVersion, lastModified };
}
async getOsVersion() {
const response = await this.download('VERSION_HOSTOS');
return response.data.trim();
}
async getImageJSON() {
return (await this.download('image.json')).data;
}
async getDeviceTypeJSON() {
return (await this.download('device-type.json')).data;
}
async getPartStream(filename) {
const url = this.urls.parts[filename];
if (!url) {
throw new Error(`URL not found for part: ${filename}`);
}
const response = await axios_1.default.get(url, {
responseType: 'stream',
});
// Confirm that the response is of the expected type, until axios no longer types it as any.
if (!(response.data instanceof node_stream_1.Readable)) {
// This should never happen
throw new Error(`Expected response.data to be a Readable stream, but got: ${typeof response.data}`);
}
return response.data;
}
async download(key) {
const url = this.urls[key];
if (url == null) {
throw new Error(`URL not found for key: ${key}`);
}
return await axios_1.default.get(url);
}
findPartitionPart(imageJSON, partition) {
for (const { parts } of Object.values(imageJSON)) {
for (const part of parts) {
if (part.partitionIndex === `(${partition})`) {
return part;
}
}
}
throw new Error(`Couldn't find compressed image part for partition ${partition}`);
}
findImagePart(imageJSON, image) {
var _a, _b;
const [part] = (_b = (_a = imageJSON[image]) === null || _a === void 0 ? void 0 : _a.parts) !== null && _b !== void 0 ? _b : [];
if (part === undefined) {
throw new Error(`Couldn't find compressed part for image ${image}`);
}
return part;
}
findPart(definition) {
if (definition.partition !== undefined) {
const partition = (0, configure_1.normalizePartition)(definition.partition);
return this.findPartitionPart(this.imageJSON, partition);
}
else if (definition.image !== undefined) {
return this.findImagePart(this.imageJSON, definition.image);
}
else {
throw new Error('No partition or image to configure found');
}
}
async extractDeflateToDisk(filename) {
const stream = await this.getPartStream(filename);
const combined = CombinedStream.create();
combined.append(stream);
combined.append(gzip_stream_1.DEFLATE_END);
const inflate = (0, zlib_1.createInflateRaw)();
(0, node_stream_1.pipeline)(combined, inflate, utils_1.noop);
return new file_disk_1.BufferDisk(await (0, utils_1.streamToBuffer)(inflate));
}
async configure() {
var _a, _b;
if (this.configuration === undefined) {
return;
}
const disks = new Map();
const self = this;
async function getDisk(definition) {
const filename = self.findPart(definition).filename;
const d = disks.get(filename);
if (d !== undefined) {
return d;
}
const d2 = await self.extractDeflateToDisk(filename);
disks.set(filename, d2);
return d2;
}
// configure
await (0, configure_2.configure)(await getDisk(this.deviceTypeJSON.configuration.config), undefined, this.configuration);
// copy operations
for (const cpy of (_a = this.deviceTypeJSON.configuration.operations) !== null && _a !== void 0 ? _a : []) {
if ((0, configure_1.shouldRunOperation)((_b = this.configuration) !== null && _b !== void 0 ? _b : {}, cpy)) {
await (0, copy_1.copy)(await getDisk(cpy.from), undefined, cpy.from.path, await getDisk(cpy.to), undefined, cpy.to.path);
}
}
// compress
await Promise.all(Array.from(disks.entries()).map(async ([filename, disk]) => {
const stream = (0, node_stream_1.pipeline)(await disk.getStream(), (0, gzip_stream_1.createDeflatePart)(), utils_1.noop);
const buffer = await (0, utils_1.streamToBuffer)(stream);
const { crc, zLen } = stream.metadata();
this.configuredParts.set(filename, { crc, zLen, buffer });
}));
}
async _open() {
// Validate that all required URLs are present
if (!this.urls.VERSION || !this.urls['image.json']) {
throw new Error('Required URLs (VERSION, image.json) must be provided');
}
const [{ supervisorVersion, lastModified }, osVersion, imageJSON, deviceTypeJSON,] = await Promise.all([
this.getSupervisorVersion(),
this.getOsVersion(),
this.getImageJSON(),
this.getDeviceTypeJSON(),
]);
if (deviceTypeJSON.yocto.archive) {
// Only zip works for yocto archives (intel-edison)
this.format = 'zip';
}
this.supervisorVersion = supervisorVersion;
this.lastModified = lastModified;
this.osVersion = osVersion;
this.deviceTypeJSON = deviceTypeJSON;
// The order is important, getFilename() expects osVersion and supervisorVersion to be set
this.filename = this.getFilename();
// replace resin.img with the requested filename if needed
const keys = Object.keys(imageJSON);
if (keys.length === 1 && keys[0] === 'resin.img') {
this.filename += '.img';
this.imageJSON = {
[this.filename]: imageJSON['resin.img'],
};
}
else {
this.imageJSON = imageJSON;
}
// Validate that all parts referenced in image.json have corresponding URLs
for (const [imageName, { parts }] of Object.entries(this.imageJSON)) {
for (const part of parts) {
if (!this.urls.parts[part.filename]) {
throw new Error(`URL not found for part ${part.filename} referenced in image.json for ${imageName}`);
}
}
}
await this.configure();
// The order is important, getSize() expects imageJSON and filename to be set and the image to be configured
this.size = this.getSize();
}
getSize() {
const partInfos = Object.entries(this.imageJSON).map(([filename, { parts }]) => ({
filename,
parts: parts.map((p) => {
const configuredPart = this.configuredParts.get(p.filename);
return {
len: p.len,
zLen: (configuredPart !== null && configuredPart !== void 0 ? configuredPart : p).zLen,
};
}),
}));
if (this.format === 'zip') {
return (0, raw_deflate_zip_stream_1.getZipSizeFromParts)(partInfos);
}
return (0, compressed_source_utils_1.getGzipSizeFromParts)(partInfos);
}
async getParts() {
return Promise.all(Object.entries(this.imageJSON).map(async ([filename, { parts }]) => ({
filename,
parts: await Promise.all(parts.map(async (p) => {
let stream;
let { crc, zLen } = p;
const configuredPart = this.configuredParts.get(p.filename);
if (configuredPart !== undefined) {
({ buffer: stream, crc, zLen } = configuredPart);
}
else {
stream = await this.getPartStream(p.filename);
}
return { ...p, crc, zLen, stream };
})),
})));
}
async createStream() {
const parts = await this.getParts();
const stream = this.format === 'zip'
? (0, raw_deflate_zip_stream_1.createZipStreamFromParts)(parts)
: (0, compressed_source_utils_1.createGzipStreamFromParts)(parts);
const cleanupFinishedListeners = (0, node_stream_1.finished)(stream, (err) => {
if (err != null) {
(0, compressed_source_utils_1.cleanupParts)(parts);
}
cleanupFinishedListeners();
});
return stream;
}
async createReadStream(
// TODO: Drop in the next major
/* @deprecated */
options = {}) {
if (options.start !== undefined) {
throw new errors_1.NotCapable();
}
const stream = await this.createStream();
if (options.end !== undefined) {
return new stream_limiter_1.StreamLimiter(stream, options.end + 1);
}
return stream;
}
async canCreateReadStream() {
return true;
}
}
exports.URLCompressedSource = URLCompressedSource;
//# sourceMappingURL=url-compressed-source.js.map