"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