139 lines
3.9 KiB
TypeScript
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)
|
|
};
|
|
}
|