UNPKG

etcher-sdk

Version:
264 lines • 10.9 kB
"use strict"; 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 stream_1 = require("stream"); const ZipPartStream = require("zip-part-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; } async getSize() { return (await this.createStream(true)).zLen; } 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', }); 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)(); combined.pipe(inflate); 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 = (await disk.getStream()).pipe((0, gzip_stream_1.createDeflatePart)()); 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 = await this.getSize(); } async getParts(fake) { 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 if (fake) { stream = new stream_1.Readable(); } else { stream = await this.getPartStream(p.filename); } return { ...p, crc, zLen, stream }; })), }))); } async createZipStream(fake) { const entries = (await this.getParts(fake)).map(({ filename, parts }) => ZipPartStream.createEntry(filename, parts)); return ZipPartStream.create(entries); } async createGzipStream(fake) { const [{ parts }] = await this.getParts(fake); return (0, gzip_stream_1.createGzipFromParts)(parts); } async createStream(fake = false) { return await (this.format === 'zip' ? this.createZipStream(fake) : this.createGzipStream(fake)); } async createReadStream(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