minio.service.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import { Client } from 'minio';
  2. import { logger } from '@/server/utils/logger';
  3. import * as process from 'node:process';
  4. export class MinioService {
  5. private readonly client: Client;
  6. public readonly bucketName: string;
  7. constructor() {
  8. this.client = new Client({
  9. endPoint: process.env.MINIO_HOST || 'localhost',
  10. port: parseInt(process.env.MINIO_PORT || '443'),
  11. useSSL: process.env.MINIO_USE_SSL !== 'false',
  12. accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
  13. secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin'
  14. });
  15. this.bucketName = process.env.MINIO_BUCKET_NAME || 'd8dai';
  16. }
  17. // 设置桶策略为"公读私写"
  18. async setPublicReadPolicy(bucketName: string = this.bucketName) {
  19. const policy = {
  20. "Version": "2012-10-17",
  21. "Statement": [
  22. {
  23. "Effect": "Allow",
  24. "Principal": {"AWS": "*"},
  25. "Action": ["s3:GetObject"],
  26. "Resource": [`arn:aws:s3:::${bucketName}/*`]
  27. },
  28. {
  29. "Effect": "Allow",
  30. "Principal": {"AWS": "*"},
  31. "Action": ["s3:ListBucket"],
  32. "Resource": [`arn:aws:s3:::${bucketName}`]
  33. }
  34. ]
  35. };
  36. try {
  37. await this.client.setBucketPolicy(bucketName, JSON.stringify(policy));
  38. logger.db(`Bucket policy set to public read for: ${bucketName}`);
  39. } catch (error) {
  40. logger.error(`Failed to set bucket policy for ${bucketName}:`, error);
  41. throw error;
  42. }
  43. }
  44. // 确保存储桶存在
  45. async ensureBucketExists(bucketName: string = this.bucketName) {
  46. try {
  47. const exists = await this.client.bucketExists(bucketName);
  48. if (!exists) {
  49. await this.client.makeBucket(bucketName);
  50. await this.setPublicReadPolicy(bucketName);
  51. logger.db(`Created new bucket: ${bucketName}`);
  52. }
  53. return true;
  54. } catch (error) {
  55. logger.error(`Failed to ensure bucket exists: ${bucketName}`, error);
  56. throw error;
  57. }
  58. }
  59. // 生成上传策略
  60. async generateUploadPolicy(fileKey: string) {
  61. await this.ensureBucketExists();
  62. const expiresAt = new Date(Date.now() + 3600 * 1000);
  63. const policy = this.client.newPostPolicy();
  64. policy.setBucket(this.bucketName);
  65. policy.setKey(fileKey);
  66. policy.setExpires(expiresAt);
  67. const { postURL, formData } = await this.client.presignedPostPolicy(policy);
  68. return {
  69. 'x-amz-algorithm': formData['x-amz-algorithm'],
  70. 'x-amz-credential': formData['x-amz-credential'],
  71. 'x-amz-date': formData['x-amz-date'],
  72. 'x-amz-security-token': formData['x-amz-security-token'] || undefined,
  73. policy: formData['policy'],
  74. 'x-amz-signature': formData['x-amz-signature'],
  75. host: postURL,
  76. key: fileKey,
  77. bucket: this.bucketName,
  78. };
  79. }
  80. // 生成文件访问URL
  81. getFileUrl(bucketName: string, fileKey: string) {
  82. const protocol = process.env.MINIO_USE_SSL ? 'https' : 'http';
  83. const port = process.env.MINIO_PORT ? `:${process.env.MINIO_PORT}` : '';
  84. return `${protocol}://${process.env.MINIO_ENDPOINT}${port}/${bucketName}/${fileKey}`;
  85. }
  86. // 创建分段上传会话
  87. async createMultipartUpload(bucketName: string, objectName: string) {
  88. try {
  89. const uploadId = await this.client.initiateNewMultipartUpload(bucketName, objectName, {});
  90. logger.db(`Created multipart upload for ${objectName} with ID: ${uploadId}`);
  91. return uploadId;
  92. } catch (error) {
  93. logger.error(`Failed to create multipart upload for ${objectName}:`, error);
  94. throw error;
  95. }
  96. }
  97. // 生成分段上传预签名URL
  98. async generateMultipartUploadUrls(
  99. bucketName: string,
  100. objectName: string,
  101. uploadId: string,
  102. partCount: number,
  103. expiresInSeconds = 3600
  104. ) {
  105. try {
  106. const partUrls = [];
  107. for (let partNumber = 1; partNumber <= partCount; partNumber++) {
  108. const url = await this.client.presignedUrl(
  109. 'put',
  110. bucketName,
  111. objectName,
  112. expiresInSeconds,
  113. {
  114. uploadId,
  115. partNumber: partNumber.toString()
  116. }
  117. );
  118. partUrls.push(url);
  119. }
  120. return partUrls;
  121. } catch (error) {
  122. logger.error(`Failed to generate multipart upload URLs for ${objectName}:`, error);
  123. throw error;
  124. }
  125. }
  126. // 完成分段上传
  127. async completeMultipartUpload(
  128. bucketName: string,
  129. objectName: string,
  130. uploadId: string,
  131. parts: { ETag: string; PartNumber: number }[]
  132. ): Promise<{ size: number }> {
  133. try {
  134. await this.client.completeMultipartUpload(
  135. bucketName,
  136. objectName,
  137. uploadId,
  138. parts.map(p => ({ part: p.PartNumber, etag: p.ETag }))
  139. );
  140. logger.db(`Completed multipart upload for ${objectName} with ID: ${uploadId}`);
  141. // 获取对象信息以获取文件大小
  142. const stat = await this.client.statObject(bucketName, objectName);
  143. return { size: stat.size };
  144. } catch (error) {
  145. logger.error(`Failed to complete multipart upload for ${objectName}:`, error);
  146. throw error;
  147. }
  148. }
  149. // 上传文件
  150. async createObject(bucketName: string, objectName: string, fileContent: Buffer, contentType: string = 'application/octet-stream') {
  151. try {
  152. await this.ensureBucketExists(bucketName);
  153. await this.client.putObject(bucketName, objectName, fileContent, fileContent.length, {
  154. 'Content-Type': contentType
  155. });
  156. logger.db(`Created object: ${bucketName}/${objectName}`);
  157. return this.getFileUrl(bucketName, objectName);
  158. } catch (error) {
  159. logger.error(`Failed to create object ${bucketName}/${objectName}:`, error);
  160. throw error;
  161. }
  162. }
  163. // 检查文件是否存在
  164. async objectExists(bucketName: string, objectName: string): Promise<boolean> {
  165. try {
  166. await this.client.statObject(bucketName, objectName);
  167. return true;
  168. } catch (error) {
  169. if ((error as Error).message.includes('not found')) {
  170. return false;
  171. }
  172. logger.error(`Error checking existence of object ${bucketName}/${objectName}:`, error);
  173. throw error;
  174. }
  175. }
  176. // 删除文件
  177. async deleteObject(bucketName: string, objectName: string) {
  178. try {
  179. await this.client.removeObject(bucketName, objectName);
  180. logger.db(`Deleted object: ${bucketName}/${objectName}`);
  181. } catch (error) {
  182. logger.error(`Failed to delete object ${bucketName}/${objectName}:`, error);
  183. throw error;
  184. }
  185. }
  186. }