ai-chatbot/lib/storage/service.ts
Your Name 53ddb7e585
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
Initial commit - clean repository
2025-12-17 20:15:18 +08:00

199 lines
5.5 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 "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";
}