import * as Minio from 'minio' import { randomUUID } from 'crypto' import { minioClient, BUCKET_NAME, initializeMinIO } from './minioClient' export interface StoredFile { id: string filename: string contentType: string objectName: string // MinIO 对象名称 size: number uploadTime: Date } export interface FileMetadata { id: string originalFilename: string contentType: string size: number uploadTime: Date objectName: string } // 生成对象名称,使用分层结构优化性能 function generateObjectName(type: 'screenshot' | 'version' | 'other', hostname?: string): string { 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 uuid = randomUUID() if (type === 'screenshot' && hostname) { return `screenshots/${year}/${month}/${day}/${hostname}/${uuid}` } else if (type === 'version') { return `versions/${year}/${month}/${uuid}` } else { return `files/${year}/${month}/${day}/${uuid}` } } // 确保 MinIO 已初始化 let minioInitialized = false export async function ensureMinIOReady(): Promise { if (!minioInitialized) { const success = await initializeMinIO() if (!success) { throw new Error('MinIO 初始化失败') } minioInitialized = true } } export async function storeFile( buffer: Buffer, filename: string, contentType: string = 'application/octet-stream', type: 'screenshot' | 'version' | 'other' = 'other', hostname?: string ): Promise { await ensureMinIOReady() const id = randomUUID() const ext = filename.split('.').pop() || '' const objectName = `${generateObjectName(type, hostname)}.${ext}` // 设置元数据 const metadata = { 'Content-Type': contentType, 'X-Original-Filename': filename, 'X-File-ID': id, 'X-Upload-Time': new Date().toISOString(), 'X-File-Type': type, ...(hostname && { 'X-Hostname': hostname }) } try { // 上传到 MinIO await minioClient.putObject(BUCKET_NAME, objectName, buffer, buffer.length, metadata) console.log(`✅ 文件上传成功: ${objectName} (${buffer.length} bytes)`) return { id, filename, contentType, objectName, size: buffer.length, uploadTime: new Date() } } catch (error) { console.error('❌ 文件上传失败:', error) throw new Error(`文件上传失败: ${error}`) } } export async function getFile(fileIdOrObjectName: string): Promise<{ buffer: Buffer; contentType: string; filename: string } | null> { try { await ensureMinIOReady() let objectName = fileIdOrObjectName // 如果看起来是 fileId (UUID格式),我们需要从数据库查找 objectName // 这里简化处理:如果包含 '/' 就认为是 objectName,否则当作 fileId if (!fileIdOrObjectName.includes('/')) { // 这是一个 fileId,需要从数据库查找对应的 objectName // 注意:这里需要在 API 层面处理,因为不同的模型存储在不同表中 // 暂时直接使用传入的值,让 API 层传递正确的 objectName console.log(`⚠️ 警告: getFile 收到的可能是 fileId 而不是 objectName: ${fileIdOrObjectName}`) } // 获取对象信息 const stat = await minioClient.statObject(BUCKET_NAME, objectName) // 下载文件 const stream = await minioClient.getObject(BUCKET_NAME, objectName) const chunks: Buffer[] = [] return new Promise((resolve, reject) => { stream.on('data', (chunk) => chunks.push(chunk)) stream.on('end', () => { const buffer = Buffer.concat(chunks) resolve({ buffer, contentType: stat.metaData?.['content-type'] || 'application/octet-stream', filename: stat.metaData?.['x-original-filename'] || objectName.split('/').pop() || 'unknown' }) }) stream.on('error', (error) => { console.error('❌ 文件下载失败:', error) reject(error) }) }) } catch (error) { console.error('❌ 获取文件失败:', error) return null } } // 优化版本:直接通过 objectName 获取文件(用于已知对象名的情况) export async function getFileByObjectName(objectName: string): Promise<{ buffer: Buffer; contentType: string; filename: string } | null> { try { await ensureMinIOReady() // 获取对象信息 const stat = await minioClient.statObject(BUCKET_NAME, objectName) // 下载文件 const stream = await minioClient.getObject(BUCKET_NAME, objectName) const chunks: Buffer[] = [] return new Promise((resolve, reject) => { stream.on('data', (chunk) => chunks.push(chunk)) stream.on('end', () => { const buffer = Buffer.concat(chunks) resolve({ buffer, contentType: stat.metaData?.['content-type'] || 'application/octet-stream', filename: stat.metaData?.['x-original-filename'] || objectName.split('/').pop() || 'unknown' }) }) stream.on('error', (error) => { console.error('❌ 文件下载失败:', error) reject(error) }) }) } catch (error) { console.error('❌ 获取文件失败:', error) return null } } export async function deleteFile(objectName: string): Promise { try { await ensureMinIOReady() await minioClient.removeObject(BUCKET_NAME, objectName) console.log(`✅ 文件删除成功: ${objectName}`) return true } catch (error) { console.error('❌ 文件删除失败:', error) return false } } // 获取文件统计信息 export async function getStorageStats(): Promise<{ totalObjects: number; totalSize: number; bucketName: string; }> { try { await ensureMinIOReady() let totalObjects = 0 let totalSize = 0 const objectsStream = minioClient.listObjects(BUCKET_NAME, '', true) await new Promise((resolve, reject) => { objectsStream.on('data', (obj) => { totalObjects++ totalSize += obj.size || 0 }) objectsStream.on('end', resolve) objectsStream.on('error', reject) }) return { totalObjects, totalSize, bucketName: BUCKET_NAME } } catch (error) { console.error('❌ 获取存储统计失败:', error) return { totalObjects: 0, totalSize: 0, bucketName: BUCKET_NAME } } }