douyin-archive/lib/minio.ts
2025-10-20 13:06:06 +08:00

341 lines
8.8 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';
// MinIO 客户端配置
const useSSL = process.env.MINIO_USE_SSL === 'true';
const port = Number(process.env.MINIO_PORT) || 9000;
// 当使用标准HTTPS端口443或HTTP端口80MinIO客户端不需要指定端口
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<void> {
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<string, string>
): Promise<string> {
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<string> {
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<Buffer> {
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<NodeJS.ReadableStream> {
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<void> {
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<void> {
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<boolean> {
try {
await minioClient.statObject(BUCKET_NAME, path);
return true;
} catch (error) {
return false;
}
}
/**
* 获取文件信息
* @param path - 文件路径
* @returns 文件元数据
*/
export async function getFileInfo(path: string): Promise<Minio.BucketItemStat> {
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<void> {
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,
};