"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.S3 = exports.USER_AGENT = void 0;
const async_lock_1 = __importDefault(require("async-lock"));
const aws_sdk_1 = __importDefault(require("aws-sdk"));
const os_1 = __importDefault(require("os"));
const package_json_1 = __importDefault(require("./package.json"));
const js_md5_1 = __importDefault(require("js-md5"));
const url_1 = require("url");
const semaphore_promise_1 = require("semaphore-promise");
const region_service_1 = require("./region_service");
const kodo_1 = require("./kodo");
const stream_buffers_1 = require("stream-buffers");
exports.USER_AGENT = `Qiniu-Kodo-S3-Adapter-NodeJS-SDK/${package_json_1.default.version} (${os_1.default.type()}; ${os_1.default.platform()}; ${os_1.default.arch()}; )/s3`;
class S3 {
    constructor(adapterOption) {
        this.adapterOption = adapterOption;
        this.bucketNameToIdCache = {};
        this.bucketIdToNameCache = {};
        this.clients = {};
        this.bucketNameToIdCacheLock = new async_lock_1.default();
        this.clientsLock = new async_lock_1.default();
        this.kodo = new kodo_1.Kodo(adapterOption);
        this.regionService = new region_service_1.RegionService(adapterOption);
    }
    getClient(s3RegionId) {
        return new Promise((resolve, reject) => {
            const cacheKey = s3RegionId !== null && s3RegionId !== void 0 ? s3RegionId : '';
            if (this.clients[cacheKey]) {
                resolve(this.clients[cacheKey]);
                return;
            }
            this.clientsLock.acquire(cacheKey, () => {
                return new Promise((resolve, reject) => {
                    let userAgent = exports.USER_AGENT;
                    if (this.adapterOption.appendedUserAgent) {
                        userAgent += `/${this.adapterOption.appendedUserAgent}`;
                    }
                    this.regionService.getS3Endpoint(s3RegionId).then((s3IdEndpoint) => {
                        resolve(new aws_sdk_1.default.S3({
                            apiVersion: "2006-03-01",
                            customUserAgent: userAgent,
                            computeChecksums: true,
                            region: s3IdEndpoint.s3Id,
                            endpoint: s3IdEndpoint.s3Endpoint,
                            accessKeyId: this.adapterOption.accessKey,
                            secretAccessKey: this.adapterOption.secretKey,
                            // logger: console, TODO: Use Adapter Option here
                            maxRetries: 10,
                            s3ForcePathStyle: true,
                            signatureVersion: "v4",
                            useDualstack: true,
                            httpOptions: {
                                connectTimeout: 30000,
                                timeout: 300000,
                            }
                        }));
                    }).catch(reject);
                });
            }).then((client) => {
                this.clients[cacheKey] = client;
                resolve(client);
            }).catch(reject);
        });
    }
    fromKodoBucketNameToS3BucketId(bucketName) {
        return new Promise((resolve, reject) => {
            if (this.bucketNameToIdCache[bucketName]) {
                resolve(this.bucketNameToIdCache[bucketName]);
                return;
            }
            this.bucketNameToIdCacheLock.acquire('all', () => {
                return new Promise((resolve, reject) => {
                    if (this.bucketNameToIdCache[bucketName]) {
                        resolve();
                        return;
                    }
                    this.kodo.listBucketIdNames().then((buckets) => {
                        buckets.forEach((bucket) => {
                            this.bucketNameToIdCache[bucket.name] = bucket.id;
                            this.bucketIdToNameCache[bucket.id] = bucket.name;
                        });
                        resolve();
                    }).catch(reject);
                });
            }).then(() => {
                if (this.bucketNameToIdCache[bucketName]) {
                    resolve(this.bucketNameToIdCache[bucketName]);
                }
                else {
                    resolve(bucketName);
                }
            }).catch(reject);
        });
    }
    fromS3BucketIdToKodoBucketName(bucketId) {
        return new Promise((resolve, reject) => {
            if (this.bucketIdToNameCache[bucketId]) {
                resolve(this.bucketIdToNameCache[bucketId]);
                return;
            }
            this.bucketNameToIdCacheLock.acquire('all', () => {
                return new Promise((resolve, reject) => {
                    if (this.bucketIdToNameCache[bucketId]) {
                        resolve();
                        return;
                    }
                    this.kodo.listBucketIdNames().then((buckets) => {
                        buckets.forEach((bucket) => {
                            this.bucketNameToIdCache[bucket.name] = bucket.id;
                            this.bucketIdToNameCache[bucket.id] = bucket.name;
                        });
                        resolve();
                    }).catch(reject);
                });
            }).then(() => {
                if (this.bucketIdToNameCache[bucketId]) {
                    resolve(this.bucketIdToNameCache[bucketId]);
                }
                else {
                    reject(new Error(`Cannot find bucket name of bucket ${bucketId}`));
                }
            }).catch(reject);
        });
    }
    sendS3Request(request, resolve, reject) {
        let requestInfo = undefined;
        const beginTime = new Date().getTime();
        request.on('sign', (request) => {
            let url = request.httpRequest.endpoint.href;
            if (url.endsWith('/') && request.httpRequest.path.startsWith('/')) {
                url += request.httpRequest.path.substring(1);
            }
            else {
                url += request.httpRequest.path;
            }
            requestInfo = {
                url: url,
                method: request.httpRequest.method,
                headers: request.httpRequest.headers,
            };
            if (this.adapterOption.requestCallback) {
                this.adapterOption.requestCallback(requestInfo);
            }
        });
        request.on('complete', (response) => {
            const responseInfo = {
                request: requestInfo,
                statusCode: response.httpResponse.statusCode,
                headers: response.httpResponse.headers,
                data: response.data,
                error: response.error,
                interval: new Date().getTime() - beginTime,
            };
            if (this.adapterOption.responseCallback) {
                this.adapterOption.responseCallback(responseInfo);
            }
        });
        request.send((err, data) => {
            if (err) {
                reject(err);
            }
            else {
                resolve(data);
            }
        });
    }
    createBucket(s3RegionId, bucket) {
        return new Promise((resolve, reject) => {
            this.getClient(s3RegionId).then((s3) => {
                this.sendS3Request(s3.createBucket({
                    Bucket: bucket,
                    CreateBucketConfiguration: {
                        LocationConstraint: s3RegionId,
                    },
                }), resolve, reject);
            }, reject).catch(reject);
        });
    }
    deleteBucket(s3RegionId, bucket) {
        return new Promise((resolve, reject) => {
            Promise.all([this.getClient(s3RegionId), this.fromKodoBucketNameToS3BucketId(bucket)]).then(([s3, bucketId]) => {
                this.sendS3Request(s3.deleteBucket({ Bucket: bucketId }), resolve, reject);
            }).catch(reject);
        });
    }
    getBucketLocation(bucket) {
        return new Promise((resolve, reject) => {
            Promise.all([this.getClient(), this.fromKodoBucketNameToS3BucketId(bucket)]).then(([s3, bucketId]) => {
                this._getBucketLocation(s3, bucketId, resolve, reject);
            }).catch(reject);
        });
    }
    _getBucketLocation(s3, bucketId, resolve, reject) {
        this.sendS3Request(s3.getBucketLocation({ Bucket: bucketId }), (data) => {
            const s3RegionId = data.LocationConstraint;
            resolve(s3RegionId);
        }, reject);
    }
    listBuckets() {
        return new Promise((resolve, reject) => {
            this.getClient().then((s3) => {
                this.sendS3Request(s3.listBuckets(), (data) => {
                    const bucketNamePromises = data.Buckets.map((info) => {
                        return this.fromS3BucketIdToKodoBucketName(info.Name);
                    });
                    const bucketLocationPromises = data.Buckets.map((info) => {
                        return new Promise((resolve) => {
                            this.sendS3Request(s3.getBucketLocation({ Bucket: info.Name }), (data) => {
                                resolve(data.LocationConstraint);
                            }, () => {
                                resolve(undefined);
                            });
                        });
                    });
                    Promise.all([Promise.all(bucketNamePromises), Promise.all(bucketLocationPromises)])
                        .then(([bucketNames, bucketLocations]) => {
                        const bucketInfos = data.Buckets.map((info, index) => {
                            return {
                                id: info.Name, name: bucketNames[index],
                                createDate: info.CreationDate,
                                regionId: bucketLocations[index],
                            };
                        });
                        resolve(bucketInfos);
                    }).catch(reject);
                }, reject);
            }).catch(reject);
        });
    }
    listDomains(_s3RegionId, _bucket) {
        return Promise.resolve([]);
    }
    isExists(s3RegionId, object) {
        return new Promise((resolve, reject) => {
            this.getObjectInfo(s3RegionId, object).then(() => {
                resolve(true);
            }).catch((error) => {
                if (error.message === 'no such file or directory') {
                    resolve(false);
                }
                else {
                    reject(error);
                }
            });
        });
    }
    deleteObject(s3RegionId, object) {
        return new Promise((resolve, reject) => {
            Promise.all([this.getClient(s3RegionId), this.fromKodoBucketNameToS3BucketId(object.bucket)]).then(([s3, bucketId]) => {
                this.sendS3Request(s3.deleteObject({ Bucket: bucketId, Key: object.key }), resolve, reject);
            }).catch(reject);
        });
    }
    putObject(s3RegionId, object, data, originalFileName, header, option) {
        return new Promise((resolve, reject) => {
            Promise.all([this.getClient(s3RegionId), this.fromKodoBucketNameToS3BucketId(object.bucket)]).then(([s3, bucketId]) => {
                var _a, _b;
                let dataSource;
                if ((_b = (_a = this.adapterOption.ucUrl) === null || _a === void 0 ? void 0 : _a.startsWith("https://")) !== null && _b !== void 0 ? _b : true) {
                    const reader = new stream_buffers_1.ReadableStreamBuffer({ initialSize: data.length, chunkSize: 1 << 20 });
                    reader.put(data);
                    reader.stop();
                    if (option === null || option === void 0 ? void 0 : option.throttle) {
                        dataSource = reader.pipe(option.throttle);
                    }
                    else {
                        dataSource = reader;
                    }
                }
                else {
                    dataSource = data;
                }
                const params = {
                    Bucket: bucketId,
                    Key: object.key,
                    Body: dataSource,
                    ContentLength: data.length,
                    Metadata: header === null || header === void 0 ? void 0 : header.metadata,
                    ContentDisposition: makeContentDisposition(originalFileName),
                };
                if (header === null || header === void 0 ? void 0 : header.contentType) {
                    params.ContentType = header.contentType;
                }
                const uploader = s3.putObject(params);
                if (option === null || option === void 0 ? void 0 : option.progressCallback) {
                    uploader.on('httpUploadProgress', (progress) => {
                        option.progressCallback(progress.loaded, progress.total);
                    });
                }
                this.sendS3Request(uploader, resolve, reject);
            }).catch(reject);
        });
    }
    getObject(s3RegionId, object, _domain) {
        return new Promise((resolve, reject) => {
            Promise.all([this.getClient(s3RegionId), this.fromKodoBucketNameToS3BucketId(object.bucket)]).then(([s3, bucketId]) => {
                this.sendS3Request(s3.getObject({ Bucket: bucketId, Key: object.key }), (data) => {
                    resolve({
                        data: Buffer.from(data.Body),
                        header: { size: data.ContentLength, contentType: data.ContentType, lastModified: data.LastModified, metadata: data.Metadata },
                    });
                }, reject);
            }).catch(reject);
        });
    }
    getObjectStream(s3RegionId, object, _domain, option) {
        return new Promise((resolve, reject) => {
            Promise.all([this.getClient(s3RegionId), this.fromKodoBucketNameToS3BucketId(object.bucket)]).then(([s3, bucketId]) => {
                var _a, _b;
                let range = undefined;
                if ((option === null || option === void 0 ? void 0 : option.rangeStart) || (option === null || option === void 0 ? void 0 : option.rangeEnd)) {
                    range = `bytes=${(_a = option === null || option === void 0 ? void 0 : option.rangeStart) !== null && _a !== void 0 ? _a : ''}-${(_b = option === null || option === void 0 ? void 0 : option.rangeEnd) !== null && _b !== void 0 ? _b : ''}`;
                }
                resolve(s3.getObject({ Bucket: bucketId, Key: object.key, Range: range }).createReadStream());
            }).catch(reject);
        });
    }
    getObjectURL(s3RegionId, object, _domain, deadline) {
        return new Promise((resolve, reject) => {
            Promise.all([this.getClient(s3RegionId), this.fromKodoBucketNameToS3BucketId(object.bucket)]).then(([s3, bucketId]) => {
                let expires;
                if (deadline) {
                    expires = ~~((deadline.getTime() - Date.now()) / 1000);
                }
                else {
                    expires = 7 * 24 * 60 * 60;
                }
                const url = s3.getSignedUrl('getObject', { Bucket: bucketId, Key: object.key, Expires: expires });
                resolve(new url_1.URL(url));
            }).catch(reject);
        });
    }
    getObjectInfo(s3RegionId, object) {
        return new Promise((resolve, reject) => {
            Promise.all([this.getClient(s3RegionId), this.fromKodoBucketNameToS3BucketId(object.bucket)]).then(([s3, bucketId]) => {
                this.sendS3Request(s3.listObjects({ Bucket: bucketId, MaxKeys: 1, Prefix: object.key }), (data) => {
                    if (data.Contents && data.Contents.length > 0) {
                        if (data.Contents[0].Key === object.key) {
                            resolve({
                                bucket: object.bucket,
                                key: data.Contents[0].Key, size: data.Contents[0].Size,
                                lastModified: data.Contents[0].LastModified,
                                storageClass: toStorageClass(data.Contents[0].StorageClass),
                            });
                            return;
                        }
                    }
                    reject(new Error('no such file or directory'));
                }, reject);
            }).catch(reject);
        });
    }
    getObjectHeader(s3RegionId, object, _domain) {
        return new Promise((resolve, reject) => {
            Promise.all([this.getClient(s3RegionId), this.fromKodoBucketNameToS3BucketId(object.bucket)]).then(([s3, bucketId]) => {
                this.sendS3Request(s3.headObject({ Bucket: bucketId, Key: object.key }), (data) => {
                    resolve({ size: data.ContentLength, contentType: data.ContentType, lastModified: data.LastModified, metadata: data.Metadata });
                }, reject);
            }).catch(reject);
        });
    }
    moveObject(s3RegionId, transferObject) {
        return new Promise((resolve, reject) => {
            this.copyObject(s3RegionId, transferObject).then(() => {
                this.deleteObject(s3RegionId, transferObject.from).then(resolve).catch((err) => {
                    err.stage = 'delete';
                    reject(err);
                });
            }).catch((err) => {
                err.stage = 'copy';
                reject(err);
            });
        });
    }
    copyObject(s3RegionId, transferObject) {
        return new Promise((resolve, reject) => {
            Promise.all([
                this.getClient(s3RegionId),
                this.getObjectStorageClass(s3RegionId, transferObject.from),
                this.fromKodoBucketNameToS3BucketId(transferObject.from.bucket),
                this.fromKodoBucketNameToS3BucketId(transferObject.to.bucket),
            ]).then(([s3, storageClass, fromBucketId, toBucketId]) => {
                const params = {
                    Bucket: toBucketId, Key: transferObject.to.key,
                    CopySource: `/${fromBucketId}/${encodeURIComponent(transferObject.from.key)}`,
                    MetadataDirective: 'COPY',
                    StorageClass: storageClass,
                };
                this.sendS3Request(s3.copyObject(params), resolve, reject);
            }).catch(reject);
        });
    }
    getObjectStorageClass(s3RegionId, object) {
        return new Promise((resolve, reject) => {
            Promise.all([this.getClient(s3RegionId), this.fromKodoBucketNameToS3BucketId(object.bucket)]).then(([s3, bucketId]) => {
                this.sendS3Request(s3.headObject({ Bucket: bucketId, Key: object.key }), (data) => {
                    resolve(data.StorageClass);
                }, reject);
            }).catch(reject);
        });
    }
    moveObjects(s3RegionId, transferObjects, callback) {
        return new Promise((resolve, reject) => {
            const semaphore = new semaphore_promise_1.Semaphore(5);
            const promises = transferObjects.map((transferObject, index) => {
                return new Promise((resolve, reject) => {
                    semaphore.acquire().then((release) => {
                        this.moveObject(s3RegionId, transferObject).then(() => {
                            if (callback && callback(index) === false) {
                                reject(new Error('aborted'));
                                return;
                            }
                            resolve({ bucket: transferObject.from.bucket, key: transferObject.from.key });
                        }).catch((err) => {
                            if (callback && callback(index, err) === false) {
                                reject(new Error('aborted'));
                                return;
                            }
                            resolve({ bucket: transferObject.from.bucket, key: transferObject.from.key, error: err });
                        }).finally(() => {
                            release();
                        });
                    });
                });
            });
            Promise.all(promises).then(resolve).catch(reject);
        });
    }
    copyObjects(s3RegionId, transferObjects, callback) {
        return new Promise((resolve, reject) => {
            const semaphore = new semaphore_promise_1.Semaphore(5);
            const promises = transferObjects.map((transferObject, index) => {
                return new Promise((resolve, reject) => {
                    semaphore.acquire().then((release) => {
                        this.copyObject(s3RegionId, transferObject).then(() => {
                            if (callback && callback(index) === false) {
                                reject(new Error('aborted'));
                                return;
                            }
                            resolve({ bucket: transferObject.from.bucket, key: transferObject.from.key });
                        }).catch((err) => {
                            if (callback && callback(index, err) === false) {
                                reject(new Error('aborted'));
                                return;
                            }
                            resolve({ bucket: transferObject.from.bucket, key: transferObject.from.key, error: err });
                        }).finally(() => {
                            release();
                        });
                    });
                });
            });
            Promise.all(promises).then(resolve).catch(reject);
        });
    }
    deleteObjects(s3RegionId, bucket, keys, callback) {
        return new Promise((resolve, reject) => {
            Promise.all([this.getClient(s3RegionId), this.fromKodoBucketNameToS3BucketId(bucket)]).then(([s3, bucketId]) => {
                const semaphore = new semaphore_promise_1.Semaphore(5);
                const batchCount = 100;
                const batches = [];
                while (keys.length >= batchCount) {
                    batches.push(keys.splice(0, batchCount));
                }
                if (keys.length > 0) {
                    batches.push(keys);
                }
                let counter = 0;
                const promises = batches.map((batch) => {
                    const firstIndexInCurrentBatch = counter;
                    const partialObjectErrors = new Array(batch.length);
                    counter += batch.length;
                    return new Promise((resolve, reject) => {
                        semaphore.acquire().then((release) => {
                            this.sendS3Request(s3.deleteObjects({
                                Bucket: bucketId,
                                Delete: {
                                    Objects: batch.map((key) => { return { Key: key }; }),
                                },
                            }), (results) => {
                                let aborted = false;
                                if (results.Deleted) {
                                    results.Deleted.forEach((deletedObject) => {
                                        const index = batch.findIndex((key) => key === deletedObject.Key);
                                        if (index < 0) {
                                            throw new Error('s3.deleteObjects deleted key which is not given');
                                        }
                                        if (callback && callback(index + firstIndexInCurrentBatch) === false) {
                                            aborted = true;
                                        }
                                        partialObjectErrors[index] = { bucket: bucket, key: deletedObject.Key };
                                    });
                                }
                                if (results.Errors) {
                                    results.Errors.forEach((deletedObject) => {
                                        const error = new Error(deletedObject.Message);
                                        const index = batch.findIndex((key) => key === deletedObject.Key);
                                        if (index < 0) {
                                            throw new Error('s3.deleteObjects deleted key which is not given');
                                        }
                                        if (callback && callback(index + firstIndexInCurrentBatch, error) === false) {
                                            aborted = true;
                                        }
                                        partialObjectErrors[index] = { bucket: bucket, key: deletedObject.Key, error: error };
                                    });
                                }
                                if (aborted) {
                                    reject(new Error('aborted'));
                                }
                                else {
                                    resolve(partialObjectErrors);
                                }
                                release();
                            }, (err) => {
                                let aborted = false;
                                if (err) {
                                    batch.forEach((key, index) => {
                                        if (callback && callback(index + firstIndexInCurrentBatch, err) === false) {
                                            aborted = true;
                                        }
                                        partialObjectErrors[index] = { bucket: bucket, key: key, error: err };
                                    });
                                }
                                if (aborted) {
                                    reject(new Error('aborted'));
                                }
                                else {
                                    resolve(partialObjectErrors);
                                }
                                release();
                            });
                        });
                    });
                });
                Promise.all(promises).then((batches) => {
                    let results = [];
                    for (const batch of batches) {
                        results = results.concat(batch);
                    }
                    resolve(results);
                }).catch(reject);
            }).catch(reject);
        });
    }
    getFrozenInfo(s3RegionId, object) {
        return new Promise((resolve, reject) => {
            Promise.all([this.getClient(s3RegionId), this.fromKodoBucketNameToS3BucketId(object.bucket)]).then(([s3, bucketId]) => {
                this.sendS3Request(s3.headObject({ Bucket: bucketId, Key: object.key }), (data) => {
                    var _a;
                    if (((_a = data.StorageClass) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'glacier') {
                        if (data.Restore) {
                            const restoreInfo = parseRestoreInfo(data.Restore);
                            if (restoreInfo.get('ongoing-request') === 'true') {
                                resolve({ status: 'Unfreezing' });
                            }
                            else {
                                const frozenInfo = { status: 'Unfrozen' };
                                const expiryDate = restoreInfo.get('expiry-date');
                                if (expiryDate) {
                                    frozenInfo.expiryDate = new Date(expiryDate);
                                }
                                resolve(frozenInfo);
                            }
                        }
                        else {
                            resolve({ status: 'Frozen' });
                        }
                    }
                    else {
                        resolve({ status: 'Normal' });
                    }
                }, reject);
            }).catch(reject);
        });
    }
    unfreeze(s3RegionId, object, days) {
        return new Promise((resolve, reject) => {
            Promise.all([this.getClient(s3RegionId), this.fromKodoBucketNameToS3BucketId(object.bucket)]).then(([s3, bucketId]) => {
                const params = {
                    Bucket: bucketId, Key: object.key,
                    RestoreRequest: {
                        Days: days,
                        GlacierJobParameters: { Tier: 'Standard' },
                    },
                };
                this.sendS3Request(s3.restoreObject(params), resolve, reject);
            }).catch(reject);
        });
    }
    listObjects(s3RegionId, bucket, prefix, option) {
        return new Promise((resolve, reject) => {
            Promise.all([this.getClient(s3RegionId), this.fromKodoBucketNameToS3BucketId(bucket)]).then(([s3, bucketId]) => {
                const results = { objects: [] };
                this._listObjects(s3RegionId, s3, bucket, bucketId, prefix, resolve, reject, results, option);
            }).catch(reject);
        });
    }
    _listObjects(s3RegionId, s3, bucket, bucketId, prefix, resolve, reject, results, option) {
        const params = {
            Bucket: bucketId,
            Delimiter: option === null || option === void 0 ? void 0 : option.delimiter, Marker: option === null || option === void 0 ? void 0 : option.nextContinuationToken, MaxKeys: option === null || option === void 0 ? void 0 : option.maxKeys,
            Prefix: prefix,
        };
        const newOption = {
            delimiter: option === null || option === void 0 ? void 0 : option.delimiter,
        };
        this.sendS3Request(s3.listObjects(params), (data) => {
            delete results.nextContinuationToken;
            if (data.Contents && data.Contents.length > 0) {
                results.objects = results.objects.concat(data.Contents.map((object) => {
                    return {
                        bucket: bucket,
                        key: object.Key, size: object.Size,
                        lastModified: object.LastModified,
                        storageClass: toStorageClass(object.StorageClass),
                    };
                }));
            }
            if (data.CommonPrefixes && data.CommonPrefixes.length > 0) {
                if (!results.commonPrefixes) {
                    results.commonPrefixes = [];
                }
                results.commonPrefixes = results.commonPrefixes.concat(data.CommonPrefixes.map((commonPrefix) => {
                    return { bucket: bucket, key: commonPrefix.Prefix };
                }));
            }
            results.nextContinuationToken = data.NextMarker;
            if (data.NextMarker) {
                newOption.nextContinuationToken = data.NextMarker;
                if (option === null || option === void 0 ? void 0 : option.minKeys) {
                    let resultsSize = results.objects.length;
                    if (results.commonPrefixes) {
                        resultsSize += results.commonPrefixes.length;
                    }
                    if (resultsSize < option.minKeys) {
                        newOption.minKeys = option.minKeys;
                        newOption.maxKeys = option.minKeys - resultsSize;
                        this._listObjects(s3RegionId, s3, bucket, bucketId, prefix, resolve, reject, results, newOption);
                        return;
                    }
                }
            }
            resolve(results);
        }, reject);
    }
    createMultipartUpload(s3RegionId, object, originalFileName, header) {
        return new Promise((resolve, reject) => {
            Promise.all([this.getClient(s3RegionId), this.fromKodoBucketNameToS3BucketId(object.bucket)]).then(([s3, bucketId]) => {
                const params = {
                    Bucket: bucketId, Key: object.key,
                    Metadata: header === null || header === void 0 ? void 0 : header.metadata,
                    ContentDisposition: makeContentDisposition(originalFileName),
                };
                if (header === null || header === void 0 ? void 0 : header.contentType) {
                    params.ContentType = header.contentType;
                }
                this.sendS3Request(s3.createMultipartUpload(params), (data) => {
                    resolve({ uploadId: data.UploadId });
                }, reject);
            }).catch(reject);
        });
    }
    uploadPart(s3RegionId, object, uploadId, partNumber, data, option) {
        return new Promise((resolve, reject) => {
            Promise.all([this.getClient(s3RegionId), this.fromKodoBucketNameToS3BucketId(object.bucket)]).then(([s3, bucketId]) => {
                var _a, _b;
                let dataSource;
                if ((_b = (_a = this.adapterOption.ucUrl) === null || _a === void 0 ? void 0 : _a.startsWith("https://")) !== null && _b !== void 0 ? _b : true) {
                    const reader = new stream_buffers_1.ReadableStreamBuffer({ initialSize: data.length, chunkSize: 1 << 20 });
                    reader.put(data);
                    reader.stop();
                    if (option === null || option === void 0 ? void 0 : option.throttle) {
                        dataSource = reader.pipe(option.throttle);
                    }
                    else {
                        dataSource = reader;
                    }
                }
                else {
                    dataSource = data;
                }
                const params = {
                    Bucket: bucketId, Key: object.key, Body: dataSource, ContentLength: data.length,
                    ContentMD5: js_md5_1.default.hex(data), PartNumber: partNumber, UploadId: uploadId,
                };
                const uploader = s3.uploadPart(params);
                if (option === null || option === void 0 ? void 0 : option.progressCallback) {
                    uploader.on('httpUploadProgress', (progress) => {
                        option.progressCallback(progress.loaded, progress.total);
                    });
                }
                this.sendS3Request(uploader, (data) => {
                    resolve({ etag: data.ETag });
                }, reject);
            }).catch(reject);
        });
    }
    completeMultipartUpload(s3RegionId, object, uploadId, parts, _originalFileName, _header) {
        return new Promise((resolve, reject) => {
            Promise.all([this.getClient(s3RegionId), this.fromKodoBucketNameToS3BucketId(object.bucket)]).then(([s3, bucketId]) => {
                const params = {
                    Bucket: bucketId, Key: object.key, UploadId: uploadId,
                    MultipartUpload: {
                        Parts: parts.map((part) => {
                            return { PartNumber: part.partNumber, ETag: part.etag };
                        }),
                    },
                };
                this.sendS3Request(s3.completeMultipartUpload(params), resolve, reject);
            }).catch(reject);
        });
    }
    clearCache() {
        Object.keys(this.bucketNameToIdCache).forEach((key) => { delete this.bucketNameToIdCache[key]; });
        Object.keys(this.bucketIdToNameCache).forEach((key) => { delete this.bucketIdToNameCache[key]; });
        Object.keys(this.clients).forEach((key) => { delete this.clients[key]; });
        this.kodo.clearCache();
        this.regionService.clearCache();
    }
}
exports.S3 = S3;
function toStorageClass(storageClass) {
    const s = (storageClass !== null && storageClass !== void 0 ? storageClass : 'standard').toLowerCase();
    if (s === 'standard') {
        return 'Standard';
    }
    else if (s.includes('_ia') || s === 'line') {
        return 'InfrequentAccess';
    }
    else if (s === 'glacier') {
        return 'Glacier';
    }
    throw new Error(`Unknown file type: ${storageClass}`);
}
function parseRestoreInfo(s) {
    const matches = s.match(/([\w\-]+)=\"([^\"]+)\"/g);
    const result = new Map();
    if (matches) {
        matches.forEach((s) => {
            const pair = s.match(/([\w\-]+)=\"([^\"]+)\"/);
            if (pair && pair.length >= 3) {
                result.set(pair[1], pair[2]);
            }
        });
    }
    return result;
}
function makeContentDisposition(originalFileName) {
    return `attachment; filename*=utf-8''${encodeURIComponent(originalFileName)}`;
}
//# sourceMappingURL=s3.js.map