oak-external-sdk/es/service/s3/S3.js

379 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { S3Client, PutObjectCommand, DeleteObjectCommand, HeadObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, ListPartsCommand, GetObjectCommand, } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { OakExternalException } from 'oak-domain/lib/types';
export 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 S3Client(config);
}
/**
* 获取上传信息(预签名 URL)
*/
async getUploadInfo(bucket, key, endpoint, pathStyle = false) {
try {
const client = endpoint
? this.createClient(endpoint, this.defaultRegion)
: this.client;
const command = new PutObjectCommand({
Bucket: bucket,
Key: key,
});
// 生成预签名 URL有效期 1 小时
const uploadUrl = await getSignedUrl(client, command, {
expiresIn: 3600,
});
return {
key,
uploadUrl,
bucket,
accessKey: this.accessKey,
};
}
catch (err) {
throw new 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 DeleteObjectCommand({
Bucket: bucket,
Key: key,
});
await client.send(command);
}
catch (err) {
throw new 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 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 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 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 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 UploadPartCommand({
Bucket: bucket,
Key: key,
UploadId: uploadId,
PartNumber: i,
});
const uploadUrl = await 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 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 UploadPartCommand({
Bucket: bucket,
Key: key,
UploadId: uploadId,
PartNumber: i,
});
const uploadUrl = await getSignedUrl(client, uploadCommand, {
expiresIn: expiresIn,
});
parts.push({
partNumber: i,
uploadUrl: uploadUrl,
});
}
return {
uploadId: uploadId,
parts: parts,
};
}
catch (err) {
throw new 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 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 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 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 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 AbortMultipartUploadCommand({
Bucket: bucket,
Key: key,
UploadId: uploadId,
});
await client.send(command);
}
catch (err) {
throw new 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 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 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 PutObjectCommand({ Bucket: bucket, Key: key })
: new GetObjectCommand({ Bucket: bucket, Key: key });
const url = await getSignedUrl(client, command, { expiresIn });
return url;
}
catch (err) {
throw new 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 GetObjectCommand({ Bucket: bucket, Key: key });
break;
case 'PUT':
req = new PutObjectCommand({ Bucket: bucket, Key: key });
break;
case 'DELETE':
req = new DeleteObjectCommand({ Bucket: bucket, Key: key });
break;
case 'POST':
throw new Error('S3 不支持 POST 方法的预签名 URL');
}
const url = await getSignedUrl(client, req, {
expiresIn: options?.expires || 3600,
});
return { url };
}
catch (error) {
throw new OakExternalException('s3', error.code, error.message, error, 'oak-external-sdk', {});
}
}
}