|
|
@@ -1,204 +0,0 @@
|
|
|
-import { Client } from 'minio';
|
|
|
-import { logger } from '@/server/utils/logger';
|
|
|
-import * as process from 'node:process';
|
|
|
-
|
|
|
-export class MinioService {
|
|
|
- private readonly client: Client;
|
|
|
- public readonly bucketName: string;
|
|
|
-
|
|
|
- constructor() {
|
|
|
- this.client = new Client({
|
|
|
- endPoint: process.env.MINIO_HOST || 'localhost',
|
|
|
- port: parseInt(process.env.MINIO_PORT || '443'),
|
|
|
- useSSL: process.env.MINIO_USE_SSL !== 'false',
|
|
|
- accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
|
|
|
- secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin'
|
|
|
- });
|
|
|
- this.bucketName = process.env.MINIO_BUCKET_NAME || 'd8dai';
|
|
|
- }
|
|
|
-
|
|
|
- // 设置桶策略为"公读私写"
|
|
|
- async setPublicReadPolicy(bucketName: string = this.bucketName) {
|
|
|
- const policy = {
|
|
|
- "Version": "2012-10-17",
|
|
|
- "Statement": [
|
|
|
- {
|
|
|
- "Effect": "Allow",
|
|
|
- "Principal": {"AWS": "*"},
|
|
|
- "Action": ["s3:GetObject"],
|
|
|
- "Resource": [`arn:aws:s3:::${bucketName}/*`]
|
|
|
- },
|
|
|
- {
|
|
|
- "Effect": "Allow",
|
|
|
- "Principal": {"AWS": "*"},
|
|
|
- "Action": ["s3:ListBucket"],
|
|
|
- "Resource": [`arn:aws:s3:::${bucketName}`]
|
|
|
- }
|
|
|
- ]
|
|
|
- };
|
|
|
-
|
|
|
- try {
|
|
|
- await this.client.setBucketPolicy(bucketName, JSON.stringify(policy));
|
|
|
- logger.db(`Bucket policy set to public read for: ${bucketName}`);
|
|
|
- } catch (error) {
|
|
|
- logger.error(`Failed to set bucket policy for ${bucketName}:`, error);
|
|
|
- throw error;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 确保存储桶存在
|
|
|
- async ensureBucketExists(bucketName: string = this.bucketName) {
|
|
|
- try {
|
|
|
- const exists = await this.client.bucketExists(bucketName);
|
|
|
- if (!exists) {
|
|
|
- await this.client.makeBucket(bucketName);
|
|
|
- await this.setPublicReadPolicy(bucketName);
|
|
|
- logger.db(`Created new bucket: ${bucketName}`);
|
|
|
- }
|
|
|
- return true;
|
|
|
- } catch (error) {
|
|
|
- logger.error(`Failed to ensure bucket exists: ${bucketName}`, error);
|
|
|
- throw error;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 生成上传策略
|
|
|
- async generateUploadPolicy(fileKey: string) {
|
|
|
- await this.ensureBucketExists();
|
|
|
-
|
|
|
- const expiresAt = new Date(Date.now() + 3600 * 1000);
|
|
|
- const policy = this.client.newPostPolicy();
|
|
|
- policy.setBucket(this.bucketName);
|
|
|
-
|
|
|
- policy.setKey(fileKey);
|
|
|
- policy.setExpires(expiresAt);
|
|
|
-
|
|
|
- const { postURL, formData } = await this.client.presignedPostPolicy(policy);
|
|
|
-
|
|
|
- return {
|
|
|
- 'x-amz-algorithm': formData['x-amz-algorithm'],
|
|
|
- 'x-amz-credential': formData['x-amz-credential'],
|
|
|
- 'x-amz-date': formData['x-amz-date'],
|
|
|
- 'x-amz-security-token': formData['x-amz-security-token'] || undefined,
|
|
|
- policy: formData['policy'],
|
|
|
- 'x-amz-signature': formData['x-amz-signature'],
|
|
|
- host: postURL,
|
|
|
- key: fileKey,
|
|
|
- bucket: this.bucketName,
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- // 生成文件访问URL
|
|
|
- getFileUrl(bucketName: string, fileKey: string) {
|
|
|
- const protocol = process.env.MINIO_USE_SSL ? 'https' : 'http';
|
|
|
- const port = process.env.MINIO_PORT ? `:${process.env.MINIO_PORT}` : '';
|
|
|
- return `${protocol}://${process.env.MINIO_ENDPOINT}${port}/${bucketName}/${fileKey}`;
|
|
|
- }
|
|
|
-
|
|
|
- // 创建分段上传会话
|
|
|
- async createMultipartUpload(bucketName: string, objectName: string) {
|
|
|
- try {
|
|
|
- const uploadId = await this.client.initiateNewMultipartUpload(bucketName, objectName, {});
|
|
|
- logger.db(`Created multipart upload for ${objectName} with ID: ${uploadId}`);
|
|
|
- return uploadId;
|
|
|
- } catch (error) {
|
|
|
- logger.error(`Failed to create multipart upload for ${objectName}:`, error);
|
|
|
- throw error;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 生成分段上传预签名URL
|
|
|
- async generateMultipartUploadUrls(
|
|
|
- bucketName: string,
|
|
|
- objectName: string,
|
|
|
- uploadId: string,
|
|
|
- partCount: number,
|
|
|
- expiresInSeconds = 3600
|
|
|
- ) {
|
|
|
- try {
|
|
|
- const partUrls = [];
|
|
|
- for (let partNumber = 1; partNumber <= partCount; partNumber++) {
|
|
|
- const url = await this.client.presignedUrl(
|
|
|
- 'put',
|
|
|
- bucketName,
|
|
|
- objectName,
|
|
|
- expiresInSeconds,
|
|
|
- {
|
|
|
- uploadId,
|
|
|
- partNumber: partNumber.toString()
|
|
|
- }
|
|
|
- );
|
|
|
- partUrls.push(url);
|
|
|
- }
|
|
|
- return partUrls;
|
|
|
- } catch (error) {
|
|
|
- logger.error(`Failed to generate multipart upload URLs for ${objectName}:`, error);
|
|
|
- throw error;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 完成分段上传
|
|
|
- async completeMultipartUpload(
|
|
|
- bucketName: string,
|
|
|
- objectName: string,
|
|
|
- uploadId: string,
|
|
|
- parts: { ETag: string; PartNumber: number }[]
|
|
|
- ): Promise<{ size: number }> {
|
|
|
- try {
|
|
|
- await this.client.completeMultipartUpload(
|
|
|
- bucketName,
|
|
|
- objectName,
|
|
|
- uploadId,
|
|
|
- parts.map(p => ({ part: p.PartNumber, etag: p.ETag }))
|
|
|
- );
|
|
|
- logger.db(`Completed multipart upload for ${objectName} with ID: ${uploadId}`);
|
|
|
-
|
|
|
- // 获取对象信息以获取文件大小
|
|
|
- const stat = await this.client.statObject(bucketName, objectName);
|
|
|
- return { size: stat.size };
|
|
|
- } catch (error) {
|
|
|
- logger.error(`Failed to complete multipart upload for ${objectName}:`, error);
|
|
|
- throw error;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 上传文件
|
|
|
- async createObject(bucketName: string, objectName: string, fileContent: Buffer, contentType: string = 'application/octet-stream') {
|
|
|
- try {
|
|
|
- await this.ensureBucketExists(bucketName);
|
|
|
- await this.client.putObject(bucketName, objectName, fileContent, fileContent.length, {
|
|
|
- 'Content-Type': contentType
|
|
|
- });
|
|
|
- logger.db(`Created object: ${bucketName}/${objectName}`);
|
|
|
- return this.getFileUrl(bucketName, objectName);
|
|
|
- } catch (error) {
|
|
|
- logger.error(`Failed to create object ${bucketName}/${objectName}:`, error);
|
|
|
- throw error;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 检查文件是否存在
|
|
|
- async objectExists(bucketName: string, objectName: string): Promise<boolean> {
|
|
|
- try {
|
|
|
- await this.client.statObject(bucketName, objectName);
|
|
|
- return true;
|
|
|
- } catch (error) {
|
|
|
- if ((error as Error).message.includes('not found')) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- logger.error(`Error checking existence of object ${bucketName}/${objectName}:`, error);
|
|
|
- throw error;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 删除文件
|
|
|
- async deleteObject(bucketName: string, objectName: string) {
|
|
|
- try {
|
|
|
- await this.client.removeObject(bucketName, objectName);
|
|
|
- logger.db(`Deleted object: ${bucketName}/${objectName}`);
|
|
|
- } catch (error) {
|
|
|
- logger.error(`Failed to delete object ${bucketName}/${objectName}:`, error);
|
|
|
- throw error;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|