Files
dmt-modelLibrary/server/src/storage.ts
2026-05-23 09:11:46 +08:00

139 lines
3.9 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import COS from "cos-nodejs-sdk-v5";
import { config } from "./config.js";
import { ensureDir, removeIfExists, safeStoragePath } from "./utils.js";
export type StorageProvider = "local" | "cos";
export type StoredObject = {
key: string;
provider: StorageProvider;
};
type StorageService = {
provider: StorageProvider;
putObject(key: string, buffer: Buffer): Promise<StoredObject>;
deleteObject(key: string): Promise<void>;
publicUrl(key: string): string;
};
function normalizeKey(key: string) {
return key.replace(/\\/g, "/").replace(/^\/+/, "");
}
function joinUrl(base: string, key: string) {
return `${base.replace(/\/+$/g, "")}/${normalizeKey(key).split("/").map(encodeURIComponent).join("/")}`;
}
class LocalStorageService implements StorageService {
provider: StorageProvider = "local";
async putObject(key: string, buffer: Buffer) {
const normalized = normalizeKey(key);
const fullPath = safeStoragePath(normalized);
await ensureDir(path.dirname(fullPath));
await fs.writeFile(fullPath, buffer);
return { key: normalized, provider: this.provider };
}
async deleteObject(key: string) {
await removeIfExists(safeStoragePath(normalizeKey(key)));
}
publicUrl(key: string) {
const normalized = normalizeKey(key);
if (config.publicBaseUrl) {
return joinUrl(`${config.publicBaseUrl}/storage`, normalized);
}
return `/storage/${normalized}`;
}
}
class CosStorageService implements StorageService {
provider: StorageProvider = "cos";
private cos: COS;
constructor() {
const missing = [
["COS_SECRET_ID", config.cos.secretId],
["COS_SECRET_KEY", config.cos.secretKey],
["COS_BUCKET", config.cos.bucket],
["COS_REGION", config.cos.region],
["CDN_BASE_URL", config.cdnBaseUrl]
].filter(([, value]) => !value).map(([key]) => key);
if (missing.length > 0) {
throw new Error(`COS 存储配置缺失:${missing.join(", ")}`);
}
this.cos = new COS({
SecretId: config.cos.secretId,
SecretKey: config.cos.secretKey
});
}
async putObject(key: string, buffer: Buffer) {
const objectKey = normalizeKey(config.cos.prefix ? `${config.cos.prefix}/${key}` : key);
await new Promise<void>((resolve, reject) => {
this.cos.putObject({
Bucket: config.cos.bucket,
Region: config.cos.region,
Key: objectKey,
Body: buffer
}, (error) => {
if (error) reject(error);
else resolve();
});
});
return { key: objectKey, provider: this.provider };
}
async deleteObject(key: string) {
await new Promise<void>((resolve, reject) => {
this.cos.deleteObject({
Bucket: config.cos.bucket,
Region: config.cos.region,
Key: normalizeKey(key)
}, (error) => {
if (error) reject(error);
else resolve();
});
});
}
publicUrl(key: string) {
return joinUrl(config.cdnBaseUrl, key);
}
}
export const storage = config.storageDriver === "cos"
? new CosStorageService()
: new LocalStorageService();
const localStorage = storage.provider === "local" ? storage : new LocalStorageService();
export function publicObjectUrl(key: string, provider: StorageProvider) {
if (provider === "cos") {
if (!config.cdnBaseUrl) return normalizeKey(key);
return joinUrl(config.cdnBaseUrl, key);
}
return localStorage.publicUrl(key);
}
export async function deleteStoredObject(key: string, provider: StorageProvider) {
if (provider === storage.provider) {
await storage.deleteObject(key);
return;
}
if (provider === "local") {
await localStorage.deleteObject(key);
}
}
export function storageStatus() {
return {
driver: storage.provider,
cdnEnabled: Boolean(config.cdnBaseUrl),
cosConfigured: Boolean(config.cos.bucket && config.cos.region && config.cos.secretId && config.cos.secretKey)
};
}