199 lines
5.5 KiB
TypeScript
199 lines
5.5 KiB
TypeScript
import "server-only";
|
||
|
||
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
||
import { put } from "@vercel/blob";
|
||
import { getDefaultStorageConfig } from "@/lib/db/queries";
|
||
|
||
// 获取文件的 Content-Type
|
||
function getContentType(filename: string): string {
|
||
const ext = filename.toLowerCase().split(".").pop();
|
||
const types: Record<string, string> = {
|
||
jpg: "image/jpeg",
|
||
jpeg: "image/jpeg",
|
||
png: "image/png",
|
||
gif: "image/gif",
|
||
webp: "image/webp",
|
||
svg: "image/svg+xml",
|
||
pdf: "application/pdf",
|
||
json: "application/json",
|
||
txt: "text/plain",
|
||
html: "text/html",
|
||
css: "text/css",
|
||
js: "application/javascript",
|
||
};
|
||
return types[ext ?? ""] ?? "application/octet-stream";
|
||
}
|
||
|
||
// S3/MinIO 上传函数(使用 AWS SDK)
|
||
async function uploadToS3(
|
||
filename: string,
|
||
buffer: ArrayBuffer,
|
||
config: {
|
||
endpoint: string;
|
||
region: string;
|
||
bucket: string;
|
||
accessKeyId: string;
|
||
secretAccessKey: string;
|
||
publicUrl?: string;
|
||
}
|
||
) {
|
||
const { endpoint, region, bucket, accessKeyId, secretAccessKey, publicUrl } = config;
|
||
|
||
// 生成唯一的文件名
|
||
const timestamp = Date.now();
|
||
const randomString = Math.random().toString(36).slice(2, 8);
|
||
const ext = filename.split(".").pop();
|
||
const key = `uploads/${timestamp}-${randomString}.${ext}`;
|
||
|
||
const contentType = getContentType(filename);
|
||
|
||
try {
|
||
// 创建 S3 客户端(支持 MinIO)
|
||
const s3Client = new S3Client({
|
||
endpoint: endpoint,
|
||
region: region || "auto",
|
||
credentials: {
|
||
accessKeyId: accessKeyId,
|
||
secretAccessKey: secretAccessKey,
|
||
},
|
||
// 关键:MinIO 需要使用 path-style URL
|
||
forcePathStyle: true,
|
||
});
|
||
|
||
console.log(`[S3 Upload] Uploading to: ${endpoint}/${bucket}/${key}`);
|
||
|
||
// 上传文件
|
||
const command = new PutObjectCommand({
|
||
Bucket: bucket,
|
||
Key: key,
|
||
Body: Buffer.from(buffer),
|
||
ContentType: contentType,
|
||
// 设置为公开可读(如果您的 MinIO 桶支持)
|
||
ACL: "public-read",
|
||
});
|
||
|
||
await s3Client.send(command);
|
||
|
||
console.log(`[S3 Upload] Upload successful: ${key}`);
|
||
|
||
// 返回文件的公开 URL
|
||
// MinIO 的公开 URL 格式通常是: http://endpoint:port/bucket/key
|
||
let fileUrl: string;
|
||
if (publicUrl) {
|
||
// 如果用户配置了自定义公开 URL,使用它
|
||
fileUrl = `${publicUrl}/${key}`;
|
||
} else {
|
||
// 否则使用 endpoint + bucket + key
|
||
const cleanEndpoint = endpoint.replace(/\/$/, ""); // 移除末尾的斜杠
|
||
fileUrl = `${cleanEndpoint}/${bucket}/${key}`;
|
||
}
|
||
|
||
console.log(`[S3 Upload] File URL: ${fileUrl}`);
|
||
|
||
return {
|
||
url: fileUrl,
|
||
pathname: key,
|
||
contentType,
|
||
};
|
||
} catch (error) {
|
||
console.error("[S3 Upload] Error:", error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 上传文件到存储服务
|
||
export async function uploadFile(
|
||
filename: string,
|
||
buffer: ArrayBuffer
|
||
): Promise<{ url: string; pathname: string; contentType: string }> {
|
||
console.log(`[Upload] Starting upload for file: ${filename}`);
|
||
|
||
try {
|
||
// 尝试从数据库获取存储配置
|
||
const storageConfig = await getDefaultStorageConfig();
|
||
|
||
if (
|
||
storageConfig &&
|
||
storageConfig.type === "s3" &&
|
||
storageConfig.endpoint &&
|
||
storageConfig.bucket &&
|
||
storageConfig.accessKeyId &&
|
||
storageConfig.secretAccessKey &&
|
||
storageConfig.isEnabled
|
||
) {
|
||
console.log(`[Upload] Using database S3 config: ${storageConfig.name}`);
|
||
console.log(`[Upload] Endpoint: ${storageConfig.endpoint}`);
|
||
console.log(`[Upload] Bucket: ${storageConfig.bucket}`);
|
||
|
||
// 使用数据库配置的 S3/MinIO 存储
|
||
return await uploadToS3(filename, buffer, {
|
||
endpoint: storageConfig.endpoint,
|
||
region: storageConfig.region ?? "auto",
|
||
bucket: storageConfig.bucket,
|
||
accessKeyId: storageConfig.accessKeyId,
|
||
secretAccessKey: storageConfig.secretAccessKey,
|
||
publicUrl: storageConfig.publicUrl ?? undefined,
|
||
});
|
||
} else {
|
||
console.warn("[Upload] No valid S3 config in database");
|
||
}
|
||
} catch (error) {
|
||
console.error("[Upload] Failed to get storage config from database:", error);
|
||
}
|
||
|
||
// 回退到环境变量配置的 S3
|
||
if (
|
||
process.env.S3_ENDPOINT &&
|
||
process.env.S3_BUCKET &&
|
||
process.env.S3_ACCESS_KEY_ID &&
|
||
process.env.S3_SECRET_ACCESS_KEY
|
||
) {
|
||
console.log("[Upload] Using environment variable S3 config");
|
||
return await uploadToS3(filename, buffer, {
|
||
endpoint: process.env.S3_ENDPOINT,
|
||
region: process.env.S3_REGION ?? "auto",
|
||
bucket: process.env.S3_BUCKET,
|
||
accessKeyId: process.env.S3_ACCESS_KEY_ID,
|
||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||
publicUrl: process.env.S3_PUBLIC_URL,
|
||
});
|
||
}
|
||
|
||
// 最终回退到 Vercel Blob
|
||
console.log("[Upload] Falling back to Vercel Blob");
|
||
const data = await put(filename, buffer, {
|
||
access: "public",
|
||
});
|
||
|
||
return {
|
||
url: data.url,
|
||
pathname: data.pathname,
|
||
contentType: getContentType(filename),
|
||
};
|
||
}
|
||
|
||
// 获取当前使用的存储类型
|
||
export async function getStorageType(): Promise<
|
||
"database-s3" | "env-s3" | "vercel-blob"
|
||
> {
|
||
try {
|
||
const storageConfig = await getDefaultStorageConfig();
|
||
if (
|
||
storageConfig &&
|
||
storageConfig.type === "s3" &&
|
||
storageConfig.endpoint &&
|
||
storageConfig.bucket
|
||
) {
|
||
return "database-s3";
|
||
}
|
||
} catch (error) {
|
||
// ignore
|
||
}
|
||
|
||
if (process.env.S3_ENDPOINT && process.env.S3_BUCKET) {
|
||
return "env-s3";
|
||
}
|
||
|
||
return "vercel-blob";
|
||
}
|