| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204 |
- 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;
- }
- }
- }
|