winupdate-neo/lib/fileStorage.ts
2025-06-28 15:16:06 +08:00

217 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<void> {
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<StoredFile> {
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<boolean> {
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<void>((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 }
}
}