initial commit
This commit is contained in:
138
server/src/storage.ts
Normal file
138
server/src/storage.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
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)
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user