"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TarballIntegrityError = void 0;
const core_loggers_1 = require("@pnpm/core-loggers");
const error_1 = __importStar(require("@pnpm/error"));
const retry = __importStar(require("@zkochan/retry"));
const errorTypes_1 = require("./errorTypes");
const urlLib = require("url");
const ssri = require("ssri");
const BIG_TARBALL_SIZE = 1024 * 1024 * 5; // 5 MB
class TarballIntegrityError extends error_1.default {
    constructor(opts) {
        super('TARBALL_INTEGRITY', `Got unexpected checksum for "${opts.url}". Wanted "${opts.expected}". Got "${opts.found}".`, { attempts: opts.attempts });
        this.found = opts.found;
        this.expected = opts.expected;
        this.algorithm = opts.algorithm;
        this.sri = opts.sri;
        this.url = opts.url;
    }
}
exports.TarballIntegrityError = TarballIntegrityError;
exports.default = (fetchFromRegistry, gotOpts) => {
    const retryOpts = {
        factor: 10,
        maxTimeout: 6e4,
        minTimeout: 1e4,
        retries: 2,
        ...gotOpts.retry,
    };
    return function download(url, opts) {
        // If a tarball is hosted on a different place than the manifest, only send
        // credentials on `alwaysAuth`
        const shouldAuth = opts.auth && (opts.auth.alwaysAuth === true ||
            !opts.registry ||
            urlLib.parse(url).host === urlLib.parse(opts.registry).host);
        const op = retry.operation(retryOpts);
        return new Promise((resolve, reject) => {
            op.attempt(async (attempt) => {
                var _a, _b;
                try {
                    resolve(await fetch(attempt));
                }
                catch (error) {
                    if (((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 401 || ((_b = error.response) === null || _b === void 0 ? void 0 : _b.status) === 403) {
                        reject(error);
                    }
                    const timeout = op.retry(error);
                    if (timeout === false) {
                        reject(op.mainError());
                        return;
                    }
                    core_loggers_1.requestRetryLogger.debug({
                        attempt,
                        error,
                        maxRetries: retryOpts.retries,
                        method: 'GET',
                        timeout,
                        url,
                    });
                }
            });
        });
        async function fetch(currentAttempt) {
            var _a;
            try {
                const authHeaderValue = shouldAuth ? (_a = opts.auth) === null || _a === void 0 ? void 0 : _a.authHeaderValue : undefined;
                const res = await fetchFromRegistry(url, {
                    authHeaderValue,
                    // The fetch library can retry requests on bad HTTP responses.
                    // However, it is not enough to retry on bad HTTP responses only.
                    // Requests should also be retried when the tarball's integrity check fails.
                    // Hence, we tell fetch to not retry,
                    // and we perform the retries from this function instead.
                    retry: { retries: 0 },
                });
                if (res.status !== 200) {
                    throw new error_1.FetchError({ url, authHeaderValue }, res);
                }
                const contentLength = res.headers.has('content-length') && res.headers.get('content-length');
                const size = typeof contentLength === 'string'
                    ? parseInt(contentLength, 10)
                    : null;
                if (opts.onStart) {
                    opts.onStart(size, currentAttempt);
                }
                // In order to reduce the amount of logs, we only report the download progress of big tarballs
                const onProgress = size != null && size >= BIG_TARBALL_SIZE
                    ? opts.onProgress : undefined;
                let downloaded = 0;
                res.body.on('data', (chunk) => {
                    downloaded += chunk.length;
                    if (onProgress)
                        onProgress(downloaded);
                });
                // eslint-disable-next-line no-async-promise-executor
                return await new Promise(async (resolve, reject) => {
                    const stream = res.body
                        .on('error', reject);
                    try {
                        const [integrityCheckResult, filesIndex] = await Promise.all([
                            opts.integrity ? safeCheckStream(res.body, opts.integrity, url) : true,
                            opts.cafs.addFilesFromTarball(res.body, opts.manifest),
                            waitTillClosed({ stream, size, getDownloaded: () => downloaded, url }),
                        ]);
                        if (integrityCheckResult !== true) {
                            throw integrityCheckResult;
                        }
                        resolve({ filesIndex: filesIndex });
                    }
                    catch (err) {
                        reject(err);
                    }
                });
            }
            catch (err) {
                err.attempts = currentAttempt;
                err.resource = url;
                throw err;
            }
        }
    };
};
async function safeCheckStream(stream, integrity, url) {
    try {
        await ssri.checkStream(stream, integrity);
        return true;
    }
    catch (err) {
        return new TarballIntegrityError({
            algorithm: err['algorithm'],
            expected: err['expected'],
            found: err['found'],
            sri: err['sri'],
            url,
        });
    }
}
function waitTillClosed(opts) {
    return new Promise((resolve, reject) => {
        opts.stream.on('end', () => {
            const downloaded = opts.getDownloaded();
            if (opts.size !== null && opts.size !== downloaded) {
                const err = new errorTypes_1.BadTarballError({
                    expectedSize: opts.size,
                    receivedSize: downloaded,
                    tarballUrl: opts.url,
                });
                reject(err);
                return;
            }
            resolve();
        });
    });
}
//# sourceMappingURL=createDownloader.js.map