"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Downloader = void 0;
const fs_1 = require("fs");
const fs_2 = require("fs");
const stream_throttle_1 = require("stream-throttle");
const DEFAULT_RETRIES_ON_SAME_OFFSET = 10;
class Downloader {
    constructor(adapter) {
        this.adapter = adapter;
        this.aborted = false;
    }
    getObjectToFile(region, object, filePath, domain, getFileOption) {
        this.aborted = false;
        return new Promise((resolve, reject) => {
            this.adapter.getObjectHeader(region, object, domain).then((header) => {
                var _a;
                if ((_a = getFileOption === null || getFileOption === void 0 ? void 0 : getFileOption.getCallback) === null || _a === void 0 ? void 0 : _a.headerCallback) {
                    try {
                        getFileOption.getCallback.headerCallback(header);
                    }
                    catch (err) {
                        reject(err);
                        return;
                    }
                }
                if (getFileOption === null || getFileOption === void 0 ? void 0 : getFileOption.recoveredFrom) {
                    fs_2.promises.stat(filePath).then((stat) => {
                        let recoveredFrom = stat.size;
                        if (typeof (getFileOption.recoveredFrom) === 'number') {
                            recoveredFrom = getFileOption.recoveredFrom > stat.size ? stat.size : getFileOption.recoveredFrom;
                        }
                        this.getObjectToFilePath(region, object, filePath, recoveredFrom, header.size, 0, domain, getFileOption).then(resolve).catch(reject);
                    }).catch(reject);
                }
                else {
                    this.getObjectToFilePath(region, object, filePath, 0, header.size, 0, domain, getFileOption).then(resolve).catch(reject);
                }
            }).catch(reject);
        });
    }
    getObjectToFilePath(region, object, filePath, offset, totalObjectSize, retriedOnThisOffset, domain, getFileOption) {
        return new Promise((resolve, reject) => {
            const fileWriteStream = fs_1.createWriteStream(filePath, {
                flags: (fs_1.constants.O_CREAT | fs_1.constants.O_WRONLY | fs_1.constants.O_NONBLOCK),
                encoding: 'binary',
                start: offset,
            });
            const retries = (getResult) => {
                var _a;
                const receivedDataBytes = getResult.downloaded;
                const err = getResult.error;
                if (this.aborted) {
                    reject(err !== null && err !== void 0 ? err : Downloader.userCanceledError);
                }
                else if (receivedDataBytes === totalObjectSize) {
                    resolve();
                }
                else if (receivedDataBytes > offset) {
                    this.getObjectToFilePath(region, object, filePath, receivedDataBytes, totalObjectSize, 0, domain, getFileOption).then(resolve).catch(reject);
                }
                else if (retriedOnThisOffset < ((_a = getFileOption === null || getFileOption === void 0 ? void 0 : getFileOption.retriesOnSameOffset) !== null && _a !== void 0 ? _a : DEFAULT_RETRIES_ON_SAME_OFFSET)) {
                    this.getObjectToFilePath(region, object, filePath, receivedDataBytes, totalObjectSize, retriedOnThisOffset + 1, domain, getFileOption).then(resolve).catch(reject);
                }
                else if (err) {
                    reject(err);
                }
                else {
                    reject(new Error(`File content size mismatch, got ${receivedDataBytes}, expected ${totalObjectSize}`));
                }
            };
            const destroyFileWriteStream = () => {
                if (!fileWriteStream.destroyed) {
                    fileWriteStream.destroy();
                }
            };
            this.getObjectToFileWriteStream(region, object, fileWriteStream, offset, totalObjectSize, domain, getFileOption).then((getResult) => {
                destroyFileWriteStream();
                retries(getResult);
            }).catch((err) => {
                destroyFileWriteStream();
                reject(err);
            });
        });
    }
    getObjectToFileWriteStream(region, object, fileWriteStream, offset, totalObjectSize, domain, getFileOption) {
        return new Promise((resolve, reject) => {
            this.adapter.getObjectStream(region, object, domain, { rangeStart: offset }).then((reader) => {
                var _a;
                let receivedDataBytes = offset;
                let thisPartSize = 0;
                let tid = undefined;
                let chain = reader.on('data', (chunk) => {
                    var _a, _b;
                    if (this.aborted) {
                        if (!reader.destroyed) {
                            reader.destroy(Downloader.userCanceledError);
                        }
                        reject(Downloader.userCanceledError);
                        this.abort();
                        return;
                    }
                    receivedDataBytes += chunk.length;
                    if (getFileOption === null || getFileOption === void 0 ? void 0 : getFileOption.chunkTimeout) {
                        if (tid) {
                            clearTimeout(tid);
                            tid = undefined;
                        }
                        tid = setTimeout(() => {
                            const timeoutErr = new Error('Timeout');
                            if (!reader.destroyed) {
                                reader.destroy(timeoutErr);
                            }
                        }, getFileOption.chunkTimeout);
                    }
                    if ((_a = getFileOption === null || getFileOption === void 0 ? void 0 : getFileOption.getCallback) === null || _a === void 0 ? void 0 : _a.progressCallback) {
                        try {
                            getFileOption.getCallback.progressCallback(receivedDataBytes, totalObjectSize);
                        }
                        catch (err) {
                            if (!reader.destroyed) {
                                reader.destroy(err);
                            }
                            if (!this.aborted) {
                                this.abort();
                                reject(err);
                            }
                            return;
                        }
                    }
                    if ((getFileOption === null || getFileOption === void 0 ? void 0 : getFileOption.partSize) && ((_b = getFileOption === null || getFileOption === void 0 ? void 0 : getFileOption.getCallback) === null || _b === void 0 ? void 0 : _b.partGetCallback)) {
                        thisPartSize += chunk.length;
                        if (thisPartSize > getFileOption.partSize) {
                            try {
                                getFileOption.getCallback.partGetCallback(thisPartSize);
                            }
                            catch (err) {
                                if (!reader.destroyed) {
                                    reader.destroy(err);
                                }
                                if (!this.aborted) {
                                    this.abort();
                                    reject(err);
                                }
                                return;
                            }
                            thisPartSize = 0;
                        }
                    }
                }).on('error', (err) => {
                    if (this.aborted) {
                        reject(err);
                        return;
                    }
                    resolve({ downloaded: receivedDataBytes, error: err });
                });
                if (getFileOption === null || getFileOption === void 0 ? void 0 : getFileOption.downloadThrottleOption) {
                    const throttleGroup = (_a = getFileOption === null || getFileOption === void 0 ? void 0 : getFileOption.downloadThrottleGroup) !== null && _a !== void 0 ? _a : new stream_throttle_1.ThrottleGroup(getFileOption.downloadThrottleOption);
                    chain = chain.pipe(throttleGroup.throttle(getFileOption.downloadThrottleOption));
                }
                chain.pipe(fileWriteStream).on('error', (err) => {
                    if (this.aborted) {
                        reject(err);
                        return;
                    }
                    resolve({ downloaded: receivedDataBytes, error: err });
                }).on('finish', () => {
                    if (this.aborted) {
                        reject(Downloader.userCanceledError);
                        return;
                    }
                    resolve({ downloaded: receivedDataBytes });
                });
            }).catch((err) => {
                resolve({ downloaded: offset, error: err });
            });
        });
    }
    abort() {
        this.aborted = true;
    }
}
exports.Downloader = Downloader;
Downloader.userCanceledError = new Error('User Canceled');
//# sourceMappingURL=downloader.js.map