优化图文页

This commit is contained in:
2026-04-28 17:11:42 +08:00
parent 674c202264
commit 040273586b
69 changed files with 3939 additions and 2302 deletions
+69 -50
View File
@@ -1,22 +1,22 @@
import * as Minio from 'minio';
import * as Minio from "minio";
// MinIO 客户端配置
const useSSL = process.env.MINIO_USE_SSL === 'true';
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',
endPoint: process.env.MINIO_ENDPOINT || "localhost",
...(shouldOmitPort ? {} : { port }),
useSSL,
accessKey: process.env.MINIO_ACCESS_KEY || '',
secretKey: process.env.MINIO_SECRET_KEY || '',
accessKey: process.env.MINIO_ACCESS_KEY || "",
secretKey: process.env.MINIO_SECRET_KEY || "",
pathStyle: true, // 使用路径风格,对反向代理更友好
});
const BUCKET_NAME = process.env.MINIO_BUCKET_NAME || 'home-page';
const BUCKET_NAME = process.env.MINIO_BUCKET_NAME || "home-page";
/**
* 初始化 MinIO Bucket(确保 bucket 存在)
@@ -25,41 +25,39 @@ export async function initBucket(): Promise<void> {
try {
const exists = await minioClient.bucketExists(BUCKET_NAME);
if (!exists) {
await minioClient.makeBucket(BUCKET_NAME, 'us-east-1');
await minioClient.makeBucket(BUCKET_NAME, "us-east-1");
console.log(`Bucket ${BUCKET_NAME} created successfully`);
}
// 设置公共读取策略(可选)
const policy = {
Version: '2012-10-17',
Version: "2012-10-17",
Statement: [
{
Effect: 'Allow',
Principal: { AWS: ['*'] },
Action: ['s3:GetObject'],
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);
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 文件 path = args.path
* @returns 文件 path = args.path
*/
export async function uploadFile(
file: File | Buffer,
path: string,
metadata?: Record<string, string>
metadata?: Record<string, string>,
): Promise<string> {
try {
await initBucket();
@@ -69,27 +67,32 @@ export async function uploadFile(
if (file instanceof File) {
buffer = Buffer.from(await file.arrayBuffer());
contentType = file.type || 'application/octet-stream';
contentType = file.type || "application/octet-stream";
} else {
buffer = file;
contentType = metadata?.['Content-Type'] || 'application/octet-stream';
contentType = metadata?.["Content-Type"] || "application/octet-stream";
}
const metaData = {
'Content-Type': contentType,
"Content-Type": contentType,
...metadata,
};
await minioClient.putObject(BUCKET_NAME, path, buffer, buffer.length, metaData);
await minioClient.putObject(
BUCKET_NAME,
path,
buffer,
buffer.length,
metaData,
);
return path;
} catch (error) {
console.error('Error uploading file:', error);
console.error("Error uploading file:", error);
throw error;
}
}
/**
* 获取文件的公共访问URL
* @param path - 文件路径
@@ -112,12 +115,12 @@ export async function downloadFile(path: string): Promise<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);
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);
console.error("Error downloading file:", error);
throw error;
}
}
@@ -127,11 +130,13 @@ export async function downloadFile(path: string): Promise<Buffer> {
* @param path - 文件路径
* @returns 文件流
*/
export async function getFileStream(path: string): Promise<NodeJS.ReadableStream> {
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);
console.error("Error getting file stream:", error);
throw error;
}
}
@@ -144,7 +149,7 @@ export async function deleteFile(path: string): Promise<void> {
try {
await minioClient.removeObject(BUCKET_NAME, path);
} catch (error) {
console.error('Error deleting file:', error);
console.error("Error deleting file:", error);
throw error;
}
}
@@ -157,7 +162,7 @@ export async function deleteFiles(paths: string[]): Promise<void> {
try {
await minioClient.removeObjects(BUCKET_NAME, paths);
} catch (error) {
console.error('Error deleting files:', error);
console.error("Error deleting files:", error);
throw error;
}
}
@@ -185,7 +190,7 @@ 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);
console.error("Error getting file info:", error);
throw error;
}
}
@@ -197,24 +202,27 @@ export async function getFileInfo(path: string): Promise<Minio.BucketItemStat> {
* @returns 文件列表
*/
export async function listFiles(
prefix: string = '',
recursive: boolean = false
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) => {
stream.on("data", (obj) => {
if (obj.name) {
files.push({ endpoint: `${process.env.MINIO_PUBLIC_DOMAIN}/${BUCKET_NAME}`, ...obj, } as Minio.BucketItem & { endpoint: string });
files.push({
endpoint: `${process.env.MINIO_PUBLIC_DOMAIN}/${BUCKET_NAME}`,
...obj,
} as Minio.BucketItem & { endpoint: string });
}
});
stream.on('end', () => resolve(files));
stream.on('error', reject);
stream.on("end", () => resolve(files));
stream.on("error", reject);
});
} catch (error) {
console.error('Error listing files:', error);
console.error("Error listing files:", error);
throw error;
}
}
@@ -224,17 +232,20 @@ export async function listFiles(
* @param sourcePath - 源文件路径
* @param destPath - 目标文件路径
*/
export async function copyFile(sourcePath: string, destPath: string): Promise<void> {
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
conds,
);
} catch (error) {
console.error('Error copying file:', error);
console.error("Error copying file:", error);
throw error;
}
}
@@ -245,15 +256,20 @@ export async function copyFile(sourcePath: string, destPath: string): Promise<vo
* @param prefix - 路径前缀(如: 'avatars/' 或 'posts/2024/')
* @returns 带路径的唯一文件名
*/
export function generateUniqueFileName(originalName: string, prefix: string = ''): string {
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 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;
return prefix ? `${prefix.replace(/\/$/, "")}/${fileName}` : fileName;
}
/**
@@ -262,7 +278,7 @@ export function generateUniqueFileName(originalName: string, prefix: string = ''
* @returns 扩展名(不含点)
*/
export function getFileExtension(filename: string): string {
return filename.split('.').pop() || '';
return filename.split(".").pop() || "";
}
/**
@@ -271,9 +287,12 @@ export function getFileExtension(filename: string): string {
* @param allowedTypes - 允许的扩展名数组(如: ['jpg', 'png', 'gif'])
* @returns 是否为允许的类型
*/
export function validateFileType(filename: string, allowedTypes: string[]): boolean {
export function validateFileType(
filename: string,
allowedTypes: string[],
): boolean {
const ext = getFileExtension(filename).toLowerCase();
return allowedTypes.map(t => t.toLowerCase()).includes(ext);
return allowedTypes.map((t) => t.toLowerCase()).includes(ext);
}
/**
@@ -292,10 +311,10 @@ export function validateFileSize(size: number, maxSize: number): boolean {
* @returns 格式化后的文件大小
*/
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
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]}`;