"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.S3Instance = void 0; const client_s3_1 = require("@aws-sdk/client-s3"); const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner"); const types_1 = require("oak-domain/lib/types"); class S3Instance { accessKey; secretKey; client; defaultEndpoint; defaultRegion; constructor(accessKey, secretKey, endpoint, region = 'us-east-1') { this.accessKey = accessKey; this.secretKey = secretKey; this.defaultEndpoint = endpoint; this.defaultRegion = region; // 创建默认客户端 this.client = this.createClient(endpoint, region); } createClient(endpoint, region = this.defaultRegion) { const config = { region, credentials: { accessKeyId: this.accessKey, secretAccessKey: this.secretKey, }, }; // 如果指定了 endpoint (Minio 场景) if (endpoint) { let url = endpoint; if (!/^https?:\/\//.test(url)) { url = `http://${url}`; // 自动补协议 } if (!url.endsWith('/')) { url += '/'; // 自动补尾斜杠 } config.endpoint = url; config.tls = url.startsWith('https'); config.forcePathStyle = true; // Minio 通常需要路径风格 } return new client_s3_1.S3Client(config); } /** * 获取上传信息(预签名 URL) */ async getUploadInfo(bucket, key, endpoint, pathStyle = false) { try { const client = endpoint ? this.createClient(endpoint, this.defaultRegion) : this.client; const command = new client_s3_1.PutObjectCommand({ Bucket: bucket, Key: key, }); // 生成预签名 URL,有效期 1 小时 const uploadUrl = await (0, s3_request_presigner_1.getSignedUrl)(client, command, { expiresIn: 3600, }); return { key, uploadUrl, bucket, accessKey: this.accessKey, }; } catch (err) { throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {}); } } /** * 删除文件 */ async removeFile(bucket, key, endpoint, pathStyle = false) { try { const client = endpoint ? this.createClient(endpoint, this.defaultRegion) : this.client; const command = new client_s3_1.DeleteObjectCommand({ Bucket: bucket, Key: key, }); await client.send(command); } catch (err) { throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {}); } } /** * 检查对象是否存在 */ async isExistObject(bucket, key, endpoint, pathStyle = false) { try { const client = endpoint ? this.createClient(endpoint, this.defaultRegion) : this.client; const command = new client_s3_1.HeadObjectCommand({ Bucket: bucket, Key: key, }); await client.send(command); return true; } catch (err) { if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) { return false; } throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {}); } } /** * 获取文件访问 URL */ getFileUrl(bucket, key, endpoint, pathStyle = false) { if (endpoint) { // Minio 或自定义 endpoint if (pathStyle) { return `${endpoint}/${bucket}/${key}`; } return `${endpoint.replace(/https?:\/\//, `$&${bucket}.`)}/${key}`; } // AWS S3 默认 return `https://${bucket}.s3.${this.defaultRegion}.amazonaws.com/${key}`; } /** * 创建分片上传 */ async createMultipartUpload(bucket, key, endpoint, contentType) { try { const client = endpoint ? this.createClient(endpoint, this.defaultRegion) : this.client; const command = new client_s3_1.CreateMultipartUploadCommand({ Bucket: bucket, Key: key, ContentType: contentType, }); const response = await client.send(command); return { uploadId: response.UploadId, bucket: response.Bucket, key: response.Key, }; } catch (err) { throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {}); } } /** * 为分片上传生成预签名 URL * @param bucket 桶 * @param key 对象键 * @param uploadId 上传 ID * @param from 起始分片号 * @param to 结束分片号 * @param options 配置项 * @returns 分片预签名 URL 列表 */ async presignMulti(bucket, key, uploadId, from, to, options) { const client = options?.endpoint ? this.createClient(options.endpoint, this.defaultRegion) : this.client; // 2. 为每个分片生成预签名 URL const parts = []; const expiresIn = options?.expiresIn || 3600; for (let i = from; i <= to; i++) { const uploadCommand = new client_s3_1.UploadPartCommand({ Bucket: bucket, Key: key, UploadId: uploadId, PartNumber: i, }); const uploadUrl = await (0, s3_request_presigner_1.getSignedUrl)(client, uploadCommand, { expiresIn: expiresIn, }); parts.push({ partNumber: i, uploadUrl: uploadUrl, }); } return parts; } /** * 准备分片上传(创建分片上传并生成所有分片的预签名URL) * 用于前端直传场景,返回 uploadId 和每个分片的上传 URL */ async prepareMultipartUpload(bucket, key, partCount, options) { try { const client = options?.endpoint ? this.createClient(options.endpoint, this.defaultRegion) : this.client; // 1. 创建分片上传 const createCommand = new client_s3_1.CreateMultipartUploadCommand({ Bucket: bucket, Key: key, ContentType: options?.contentType, }); const createResponse = await client.send(createCommand); const uploadId = createResponse.UploadId; // 2. 为每个分片生成预签名 URL const parts = []; const expiresIn = options?.expiresIn || 3600; for (let i = 1; i <= partCount; i++) { const uploadCommand = new client_s3_1.UploadPartCommand({ Bucket: bucket, Key: key, UploadId: uploadId, PartNumber: i, }); const uploadUrl = await (0, s3_request_presigner_1.getSignedUrl)(client, uploadCommand, { expiresIn: expiresIn, }); parts.push({ partNumber: i, uploadUrl: uploadUrl, }); } return { uploadId: uploadId, parts: parts, }; } catch (err) { throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {}); } } /** * 上传分片 */ async uploadPart(bucket, key, uploadId, partNumber, body, endpoint) { try { const client = endpoint ? this.createClient(endpoint, this.defaultRegion) : this.client; const command = new client_s3_1.UploadPartCommand({ Bucket: bucket, Key: key, UploadId: uploadId, PartNumber: partNumber, Body: body, }); const response = await client.send(command); return { partNumber, eTag: response.ETag, }; } catch (err) { throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {}); } } /** * 完成分片上传 */ async completeMultipartUpload(bucket, key, uploadId, parts, endpoint) { try { const client = endpoint ? this.createClient(endpoint, this.defaultRegion) : this.client; const command = new client_s3_1.CompleteMultipartUploadCommand({ Bucket: bucket, Key: key, UploadId: uploadId, MultipartUpload: { Parts: parts.map(part => ({ PartNumber: part.partNumber, ETag: part.eTag, })), }, }); const response = await client.send(command); return { location: response.Location, bucket: response.Bucket, key: response.Key, eTag: response.ETag, }; } catch (err) { throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {}); } } /** * 中止分片上传 */ async abortMultipartUpload(bucket, key, uploadId, endpoint) { try { const client = endpoint ? this.createClient(endpoint, this.defaultRegion) : this.client; const command = new client_s3_1.AbortMultipartUploadCommand({ Bucket: bucket, Key: key, UploadId: uploadId, }); await client.send(command); } catch (err) { throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {}); } } /** * 列出已上传的分片 */ async listParts(bucket, key, uploadId, endpoint, maxParts) { try { const client = endpoint ? this.createClient(endpoint, this.defaultRegion) : this.client; const command = new client_s3_1.ListPartsCommand({ Bucket: bucket, Key: key, UploadId: uploadId, MaxParts: maxParts, }); const response = await client.send(command); return { parts: response.Parts?.map(part => ({ partNumber: part.PartNumber, eTag: part.ETag, size: part.Size, lastModified: part.LastModified, })) || [], isTruncated: response.IsTruncated, nextPartNumberMarker: response.NextPartNumberMarker, }; } catch (err) { throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {}); } } /** * 获取预签名 URL(通用方法) */ async getSignedUrl(bucket, key, operation = 'get', expiresIn = 3600, endpoint) { try { const client = endpoint ? this.createClient(endpoint, this.defaultRegion) : this.client; const command = operation === 'put' ? new client_s3_1.PutObjectCommand({ Bucket: bucket, Key: key }) : new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: key }); const url = await (0, s3_request_presigner_1.getSignedUrl)(client, command, { expiresIn }); return url; } catch (err) { throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {}); } } /** * 获取预签名对象URL(统一接口) */ async presignObjectUrl(method, bucket, zone = '', key, options) { try { const client = options?.endpoint ? this.createClient(options.endpoint, this.defaultRegion) : this.client; let req; switch (method) { case 'GET': req = new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: key }); break; case 'PUT': req = new client_s3_1.PutObjectCommand({ Bucket: bucket, Key: key }); break; case 'DELETE': req = new client_s3_1.DeleteObjectCommand({ Bucket: bucket, Key: key }); break; case 'POST': throw new Error('S3 不支持 POST 方法的预签名 URL'); } const url = await (0, s3_request_presigner_1.getSignedUrl)(client, req, { expiresIn: options?.expires || 3600, }); return { url }; } catch (error) { throw new types_1.OakExternalException('s3', error.code, error.message, error, 'oak-external-sdk', {}); } } } exports.S3Instance = S3Instance;