const { createMinioClient, minioConfig, getPublicUrl } = require('../config/minio'); const path = require('path'); const crypto = require('crypto'); /** * MinIO 文件服务 * 提供文件上传、删除、获取等功能 */ class MinioService { constructor() { this.client = createMinioClient(); } /** * 生成唯一文件名 * @param {string} originalName - 原始文件名 * @returns {string} 唯一文件名 */ generateUniqueFileName(originalName) { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const timestamp = Date.now(); const randomString = crypto.randomBytes(8).toString('hex'); const ext = path.extname(originalName); return `${year}/${month}/${day}/${timestamp}_${randomString}${ext}`; } /** * 根据文件类型获取存储桶名称 * @param {string} type - 文件类型 (avatar, product, document) * @returns {string} 存储桶名称 */ getBucketName(type = 'document') { const bucketMap = { 'avatar': minioConfig.buckets.avatars, 'product': minioConfig.buckets.products, 'document': minioConfig.buckets.documents }; return bucketMap[type] || minioConfig.buckets.documents; } /** * 上传单个文件 * @param {Buffer} fileBuffer - 文件缓冲区 * @param {string} originalName - 原始文件名 * @param {string} mimeType - 文件MIME类型 * @param {string} type - 文件类型 * @returns {Promise} 上传结果 */ async uploadFile(fileBuffer, originalName, mimeType, type = 'document') { try { const bucketName = this.getBucketName(type); const fileName = this.generateUniqueFileName(originalName); // 设置文件元数据 const metaData = { 'Content-Type': mimeType, 'Original-Name': encodeURIComponent(originalName), 'Upload-Time': new Date().toISOString() }; // 上传文件到MinIO await this.client.putObject(bucketName, fileName, fileBuffer, fileBuffer.length, metaData); // 生成访问URL const url = getPublicUrl(bucketName, fileName); return { success: true, data: { filename: fileName, originalname: originalName, mimetype: mimeType, size: fileBuffer.length, bucket: bucketName, path: `${bucketName}/${fileName}`, url: url } }; } catch (error) { console.error('MinIO文件上传失败:', error); throw new Error(`文件上传失败: ${error.message}`); } } /** * 迁移专用:上传文件到指定存储桶和路径 * @param {string} bucketName - 存储桶名称 * @param {string} filePath - 文件路径 * @param {Buffer} fileBuffer - 文件缓冲区 * @param {string} mimeType - 文件MIME类型 * @returns {Promise} 上传结果 */ async uploadFileForMigration(bucketName, filePath, fileBuffer, mimeType) { try { // 设置文件元数据 const metaData = { 'Content-Type': mimeType, 'Upload-Time': new Date().toISOString() }; // 上传文件到MinIO await this.client.putObject(bucketName, filePath, fileBuffer, fileBuffer.length, metaData); // 生成访问URL const url = getPublicUrl(bucketName, filePath); return { success: true, data: { filename: filePath, mimetype: mimeType, size: fileBuffer.length, bucket: bucketName, path: `${bucketName}/${filePath}`, url: url } }; } catch (error) { console.error('MinIO文件迁移上传失败:', error); throw new Error(`文件迁移上传失败: ${error.message}`); } } /** * 上传多个文件 * @param {Array} files - 文件数组,每个文件包含 {buffer, originalName, mimeType} * @param {string} type - 文件类型 * @returns {Promise} 上传结果数组 */ async uploadMultipleFiles(files, type = 'document') { try { const uploadPromises = files.map(file => this.uploadFile(file.buffer, file.originalName, file.mimeType, type) ); const results = await Promise.all(uploadPromises); const uploadedFiles = results.map(result => result.data); return { success: true, data: { files: uploadedFiles, urls: uploadedFiles.map(file => file.url), count: uploadedFiles.length } }; } catch (error) { console.error('MinIO多文件上传失败:', error); throw new Error(`多文件上传失败: ${error.message}`); } } /** * 删除文件 * @param {string} bucketName - 存储桶名称 * @param {string} fileName - 文件名 * @returns {Promise} 删除结果 */ async deleteFile(bucketName, fileName) { try { await this.client.removeObject(bucketName, fileName); console.log(`✅ 文件删除成功: ${bucketName}/${fileName}`); return true; } catch (error) { console.error('MinIO文件删除失败:', error); throw new Error(`文件删除失败: ${error.message}`); } } /** * 批量删除文件 * @param {string} bucketName - 存储桶名称 * @param {Array} fileNames - 文件名数组 * @returns {Promise} 删除结果 */ async deleteMultipleFiles(bucketName, fileNames) { try { const deletePromises = fileNames.map(fileName => this.deleteFile(bucketName, fileName) ); await Promise.all(deletePromises); return { success: true, deletedCount: fileNames.length, message: `成功删除${fileNames.length}个文件` }; } catch (error) { console.error('MinIO批量删除失败:', error); throw new Error(`批量删除失败: ${error.message}`); } } /** * 检查文件是否存在 * @param {string} bucketName - 存储桶名称 * @param {string} fileName - 文件名 * @returns {Promise} 文件是否存在 */ async fileExists(bucketName, fileName) { try { await this.client.statObject(bucketName, fileName); return true; } catch (error) { if (error.code === 'NotFound') { return false; } throw error; } } /** * 获取文件信息 * @param {string} bucketName - 存储桶名称 * @param {string} fileName - 文件名 * @returns {Promise} 文件信息 */ async getFileInfo(bucketName, fileName) { try { const stat = await this.client.statObject(bucketName, fileName); return { size: stat.size, lastModified: stat.lastModified, etag: stat.etag, contentType: stat.metaData['content-type'], originalName: decodeURIComponent(stat.metaData['original-name'] || fileName) }; } catch (error) { console.error('获取文件信息失败:', error); throw new Error(`获取文件信息失败: ${error.message}`); } } /** * 生成预签名URL(用于临时访问) * @param {string} bucketName - 存储桶名称 * @param {string} fileName - 文件名 * @param {number} expiry - 过期时间(秒),默认7天 * @returns {Promise} 预签名URL */ async getPresignedUrl(bucketName, fileName, expiry = 7 * 24 * 60 * 60) { try { const url = await this.client.presignedGetObject(bucketName, fileName, expiry); return url; } catch (error) { console.error('生成预签名URL失败:', error); throw new Error(`生成预签名URL失败: ${error.message}`); } } /** * 列出存储桶中的文件 * @param {string} bucketName - 存储桶名称 * @param {string} prefix - 文件前缀 * @param {number} limit - 限制数量 * @returns {Promise} 文件列表 */ async listFiles(bucketName, prefix = '', limit = 100) { try { const files = []; const stream = this.client.listObjects(bucketName, prefix, true); return new Promise((resolve, reject) => { stream.on('data', (obj) => { if (files.length < limit) { files.push({ name: obj.name, size: obj.size, lastModified: obj.lastModified, etag: obj.etag, url: getPublicUrl(bucketName, obj.name) }); } }); stream.on('end', () => resolve(files)); stream.on('error', reject); }); } catch (error) { console.error('列出文件失败:', error); throw new Error(`列出文件失败: ${error.message}`); } } } // 创建单例实例 const minioService = new MinioService(); module.exports = minioService;