379 lines
13 KiB
JavaScript
379 lines
13 KiB
JavaScript
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', {});
|
||
}
|
||
}
|
||
}
|