import * as Minio from 'minio'; // MinIO 客户端配置 const useSSL = process.env.MINIO_USE_SSL === 'true'; const port = Number(process.env.MINIO_PORT) || 9000; // 当使用标准HTTPS端口(443)或HTTP端口(80)时,MinIO客户端不需要指定端口 const shouldOmitPort = (useSSL && port === 443) || (!useSSL && port === 80); const minioClient = new Minio.Client({ endPoint: process.env.MINIO_ENDPOINT || 'localhost', ...(shouldOmitPort ? {} : { port }), useSSL, accessKey: process.env.MINIO_ACCESS_KEY || '', secretKey: process.env.MINIO_SECRET_KEY || '', pathStyle: true, // 使用路径风格,对反向代理更友好 }); const BUCKET_NAME = process.env.MINIO_BUCKET_NAME || 'home-page'; /** * 初始化 MinIO Bucket(确保 bucket 存在) */ export async function initBucket(): Promise { try { const exists = await minioClient.bucketExists(BUCKET_NAME); if (!exists) { await minioClient.makeBucket(BUCKET_NAME, 'us-east-1'); console.log(`Bucket ${BUCKET_NAME} created successfully`); } // 设置公共读取策略(可选) const policy = { Version: '2012-10-17', Statement: [ { Effect: 'Allow', Principal: { AWS: ['*'] }, Action: ['s3:GetObject'], Resource: [`arn:aws:s3:::${BUCKET_NAME}/*`], }, ], }; await minioClient.setBucketPolicy(BUCKET_NAME, JSON.stringify(policy)); } catch (error) { console.error('Error initializing bucket:', error); throw error; } } /** * 上传文件到 MinIO * @param file - File 对象或 Buffer * @param path - 文件存储路径(如: 'avatars/user123.jpg' 或 'posts/2024/image.png') * @param metadata - 可选的元数据 * @returns 文件的访问URL */ export async function uploadFile( file: File | Buffer, path: string, metadata?: Record ): Promise { try { await initBucket(); let buffer: Buffer; let contentType: string; if (file instanceof File) { buffer = Buffer.from(await file.arrayBuffer()); contentType = file.type || 'application/octet-stream'; } else { buffer = file; contentType = metadata?.['Content-Type'] || 'application/octet-stream'; } const metaData = { 'Content-Type': contentType, ...metadata, }; await minioClient.putObject(BUCKET_NAME, path, buffer, buffer.length, metaData); return getFileUrl(path); } catch (error) { console.error('Error uploading file:', error); throw error; } } /** * 获取文件的公共访问URL * @param path - 文件路径 * @returns 文件的访问URL */ export function getFileUrl(path: string): string { const endpoint = process.env.MINIO_REAL_DOMAIN; return `${endpoint}/${BUCKET_NAME}/${path}`; } /** * 获取文件的临时访问URL(带签名,用于私有文件) * @param path - 文件路径 * @param expirySeconds - 过期时间(秒),默认7天 * @returns 临时访问URL */ export async function getPresignedUrl( path: string, expirySeconds: number = 7 * 24 * 60 * 60 ): Promise { try { return await minioClient.presignedGetObject(BUCKET_NAME, path, expirySeconds); } catch (error) { console.error('Error generating presigned URL:', error); throw error; } } /** * 下载文件 * @param path - 文件路径 * @returns 文件的 Buffer */ export async function downloadFile(path: string): Promise { try { const chunks: Buffer[] = []; const stream = await minioClient.getObject(BUCKET_NAME, path); return new Promise((resolve, reject) => { stream.on('data', (chunk) => chunks.push(chunk)); stream.on('end', () => resolve(Buffer.concat(chunks))); stream.on('error', reject); }); } catch (error) { console.error('Error downloading file:', error); throw error; } } /** * 获取文件流 * @param path - 文件路径 * @returns 文件流 */ export async function getFileStream(path: string): Promise { try { return await minioClient.getObject(BUCKET_NAME, path); } catch (error) { console.error('Error getting file stream:', error); throw error; } } /** * 删除文件 * @param path - 文件路径 */ export async function deleteFile(path: string): Promise { try { await minioClient.removeObject(BUCKET_NAME, path); } catch (error) { console.error('Error deleting file:', error); throw error; } } /** * 批量删除文件 * @param paths - 文件路径数组 */ export async function deleteFiles(paths: string[]): Promise { try { await minioClient.removeObjects(BUCKET_NAME, paths); } catch (error) { console.error('Error deleting files:', error); throw error; } } /** * 检查文件是否存在 * @param path - 文件路径 * @returns 文件是否存在 */ export async function fileExists(path: string): Promise { try { await minioClient.statObject(BUCKET_NAME, path); return true; } catch (error) { return false; } } /** * 获取文件信息 * @param path - 文件路径 * @returns 文件元数据 */ export async function getFileInfo(path: string): Promise { try { return await minioClient.statObject(BUCKET_NAME, path); } catch (error) { console.error('Error getting file info:', error); throw error; } } /** * 列出指定路径下的所有文件 * @param prefix - 路径前缀(如: 'avatars/' 或 'posts/2024/') * @param recursive - 是否递归列出子目录 * @returns 文件列表 */ export async function listFiles( prefix: string = '', recursive: boolean = false ): Promise<(Minio.BucketItem & { endpoint: string })[]> { try { const files: (Minio.BucketItem & { endpoint: string })[] = []; const stream = minioClient.listObjects(BUCKET_NAME, prefix, recursive); return new Promise((resolve, reject) => { stream.on('data', (obj) => { if (obj.name) { files.push({ endpoint: `${process.env.MINIO_REAL_DOMAIN}/${BUCKET_NAME}`, ...obj, } as Minio.BucketItem & { endpoint: string }); } }); stream.on('end', () => resolve(files)); stream.on('error', reject); }); } catch (error) { console.error('Error listing files:', error); throw error; } } /** * 复制文件 * @param sourcePath - 源文件路径 * @param destPath - 目标文件路径 */ export async function copyFile(sourcePath: string, destPath: string): Promise { try { const conds = new Minio.CopyConditions(); await minioClient.copyObject( BUCKET_NAME, destPath, `/${BUCKET_NAME}/${sourcePath}`, conds ); } catch (error) { console.error('Error copying file:', error); throw error; } } /** * 生成唯一文件名 * @param originalName - 原始文件名 * @param prefix - 路径前缀(如: 'avatars/' 或 'posts/2024/') * @returns 带路径的唯一文件名 */ export function generateUniqueFileName(originalName: string, prefix: string = ''): string { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); const ext = originalName.split('.').pop(); const nameWithoutExt = originalName.replace(`.${ext}`, '').replace(/[^a-zA-Z0-9]/g, '_'); const fileName = `${nameWithoutExt}_${timestamp}_${random}.${ext}`; return prefix ? `${prefix.replace(/\/$/, '')}/${fileName}` : fileName; } /** * 获取文件扩展名 * @param filename - 文件名 * @returns 扩展名(不含点) */ export function getFileExtension(filename: string): string { return filename.split('.').pop() || ''; } /** * 验证文件类型 * @param filename - 文件名 * @param allowedTypes - 允许的扩展名数组(如: ['jpg', 'png', 'gif']) * @returns 是否为允许的类型 */ export function validateFileType(filename: string, allowedTypes: string[]): boolean { const ext = getFileExtension(filename).toLowerCase(); return allowedTypes.map(t => t.toLowerCase()).includes(ext); } /** * 验证文件大小 * @param size - 文件大小(字节) * @param maxSize - 最大大小(字节) * @returns 是否在允许的大小范围内 */ export function validateFileSize(size: number, maxSize: number): boolean { return size <= maxSize; } /** * 格式化文件大小 * @param bytes - 字节数 * @returns 格式化后的文件大小 */ export function formatFileSize(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; } export default { initBucket, uploadFile, getFileUrl, getPresignedUrl, downloadFile, getFileStream, deleteFile, deleteFiles, fileExists, getFileInfo, listFiles, copyFile, generateUniqueFileName, getFileExtension, validateFileType, validateFileSize, formatFileSize, };