feat: add aliyun oss storage config
This commit is contained in:
1194
package-lock.json
generated
1194
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -19,13 +19,16 @@
|
||||
"@capacitor/cli": "^8.2.0",
|
||||
"@capacitor/core": "^8.2.0",
|
||||
"@capacitor/preferences": "^8.0.1",
|
||||
"ali-oss": "^6.23.0",
|
||||
"clsx": "^2.1.1",
|
||||
"next": "16.2.1",
|
||||
"proxy-agent": "^5.0.0",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/ali-oss": "^6.23.3",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
|
||||
92
scripts/verify-storage-config.mjs
Normal file
92
scripts/verify-storage-config.mjs
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env node
|
||||
const baseUrl = process.env.BOSS_TEST_BASE_URL || "http://127.0.0.1:3000";
|
||||
|
||||
const loginResponse = await fetch(`${baseUrl}/api/auth/login`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
if (!loginResponse.ok) {
|
||||
throw new Error(`LOGIN_FAILED:${loginResponse.status}`);
|
||||
}
|
||||
|
||||
const cookie = (loginResponse.headers.get("set-cookie") || "").split(";")[0];
|
||||
if (!cookie) {
|
||||
throw new Error("COOKIE_MISSING");
|
||||
}
|
||||
|
||||
const getResponse = await fetch(`${baseUrl}/api/v1/storage/config`, {
|
||||
headers: {
|
||||
cookie,
|
||||
},
|
||||
});
|
||||
|
||||
if (!getResponse.ok) {
|
||||
throw new Error(`GET_CONFIG_FAILED:${getResponse.status}`);
|
||||
}
|
||||
|
||||
const getJson = await getResponse.json();
|
||||
if (!getJson.ok || getJson.config?.mode !== "server_file") {
|
||||
throw new Error("DEFAULT_STORAGE_MODE_INVALID");
|
||||
}
|
||||
|
||||
const patchPayload = {
|
||||
mode: "server_file",
|
||||
ossProvider: "aliyun_oss",
|
||||
aliyunOss: {
|
||||
enabled: false,
|
||||
accessKeyId: "ak-test",
|
||||
accessKeySecret: "oss-secret-test",
|
||||
bucket: "boss-private-bucket",
|
||||
endpoint: "oss-cn-shanghai.aliyuncs.com",
|
||||
region: "oss-cn-shanghai",
|
||||
prefix: "boss/custom/",
|
||||
},
|
||||
};
|
||||
|
||||
const patchResponse = await fetch(`${baseUrl}/api/v1/storage/config`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
cookie,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(patchPayload),
|
||||
});
|
||||
|
||||
if (!patchResponse.ok) {
|
||||
throw new Error(`PATCH_CONFIG_FAILED:${patchResponse.status}`);
|
||||
}
|
||||
|
||||
const patchJson = await patchResponse.json();
|
||||
if (!patchJson.ok) {
|
||||
throw new Error("PATCH_CONFIG_NOT_OK");
|
||||
}
|
||||
if (patchJson.config?.aliyunOss?.accessKeySecretConfigured !== true) {
|
||||
throw new Error("SECRET_SANITIZE_FLAG_MISSING");
|
||||
}
|
||||
if ("accessKeySecretEncrypted" in (patchJson.config?.aliyunOss ?? {})) {
|
||||
throw new Error("ENCRYPTED_SECRET_LEAKED");
|
||||
}
|
||||
|
||||
const rereadResponse = await fetch(`${baseUrl}/api/v1/storage/config`, {
|
||||
headers: {
|
||||
cookie,
|
||||
},
|
||||
});
|
||||
|
||||
if (!rereadResponse.ok) {
|
||||
throw new Error(`GET_CONFIG_REREAD_FAILED:${rereadResponse.status}`);
|
||||
}
|
||||
|
||||
const rereadJson = await rereadResponse.json();
|
||||
if (rereadJson.config?.aliyunOss?.accessKeyId !== "ak-test") {
|
||||
throw new Error("PATCHED_CONFIG_NOT_PERSISTED");
|
||||
}
|
||||
if (rereadJson.config?.aliyunOss?.accessKeySecretConfigured !== true) {
|
||||
throw new Error("SECRET_FLAG_NOT_PERSISTED");
|
||||
}
|
||||
|
||||
console.log("OK");
|
||||
53
src/app/api/v1/storage/config/route.ts
Normal file
53
src/app/api/v1/storage/config/route.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import {
|
||||
getAttachmentStorageConfig,
|
||||
upsertAttachmentStorageConfig,
|
||||
} from "@/lib/boss-data";
|
||||
import {
|
||||
type AttachmentStorageConfigPatch,
|
||||
applyAttachmentStorageConfigPatch,
|
||||
} from "@/lib/boss-storage-config";
|
||||
import {
|
||||
normalizeStorageError,
|
||||
sanitizeAttachmentStorageConfig,
|
||||
} from "@/lib/boss-storage";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
|
||||
const config = await getAttachmentStorageConfig(session.account);
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
config: sanitizeAttachmentStorageConfig(config),
|
||||
});
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = (await request.json()) as AttachmentStorageConfigPatch;
|
||||
|
||||
try {
|
||||
const existing = await getAttachmentStorageConfig(session.account);
|
||||
const nextConfig = await applyAttachmentStorageConfigPatch(existing, body);
|
||||
const saved = await upsertAttachmentStorageConfig(nextConfig);
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
config: sanitizeAttachmentStorageConfig(saved),
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: normalizeStorageError(error) },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
49
src/app/api/v1/storage/config/validate/route.ts
Normal file
49
src/app/api/v1/storage/config/validate/route.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import {
|
||||
getAttachmentStorageConfig,
|
||||
upsertAttachmentStorageConfig,
|
||||
} from "@/lib/boss-data";
|
||||
import {
|
||||
type AttachmentStorageConfigPatch,
|
||||
applyAttachmentStorageConfigPatch,
|
||||
} from "@/lib/boss-storage-config";
|
||||
import {
|
||||
normalizeStorageError,
|
||||
sanitizeAttachmentStorageConfig,
|
||||
validateAttachmentStorageConfig,
|
||||
} from "@/lib/boss-storage";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = (await request.json().catch(() => ({}))) as AttachmentStorageConfigPatch;
|
||||
|
||||
try {
|
||||
const existing = await getAttachmentStorageConfig(session.account);
|
||||
const draft = await applyAttachmentStorageConfigPatch(existing, body);
|
||||
const result = await validateAttachmentStorageConfig(draft);
|
||||
const saved = await upsertAttachmentStorageConfig({
|
||||
...draft,
|
||||
validatedAt: new Date().toISOString(),
|
||||
});
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
provider: result.provider,
|
||||
bucket: result.bucket,
|
||||
endpoint: result.endpoint,
|
||||
region: result.region,
|
||||
config: sanitizeAttachmentStorageConfig(saved),
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: normalizeStorageError(error) },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
86
src/lib/boss-storage-aliyun-oss.ts
Normal file
86
src/lib/boss-storage-aliyun-oss.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import path from "node:path";
|
||||
import OSS from "ali-oss";
|
||||
import type { UserAttachmentStorageConfig } from "@/lib/boss-data";
|
||||
import type { AttachmentStorageProvider, StoreAttachmentParams, StoredAttachmentRecord } from "@/lib/boss-storage";
|
||||
import { sanitizeFileName } from "@/lib/boss-attachments";
|
||||
import { decryptStorageSecret } from "@/lib/boss-storage-secrets";
|
||||
|
||||
type AliyunOssConfig = NonNullable<UserAttachmentStorageConfig["aliyunOss"]>;
|
||||
|
||||
function accountStorageSegment(account: string) {
|
||||
const normalized = account.normalize("NFKC").trim();
|
||||
const digest = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
||||
return `acct-${digest}`;
|
||||
}
|
||||
|
||||
function normalizePrefix(prefix?: string) {
|
||||
const trimmed = (prefix ?? "boss/").trim();
|
||||
const normalized = trimmed.replace(/^\/+|\/+$/g, "");
|
||||
return normalized || "boss";
|
||||
}
|
||||
|
||||
function buildObjectKey(params: StoreAttachmentParams, prefix?: string) {
|
||||
const now = new Date();
|
||||
return path.posix.join(
|
||||
normalizePrefix(prefix),
|
||||
accountStorageSegment(params.account),
|
||||
String(now.getUTCFullYear()),
|
||||
String(now.getUTCMonth() + 1).padStart(2, "0"),
|
||||
`${params.messageId}-${sanitizeFileName(params.fileName)}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createAliyunOssClient(config: AliyunOssConfig) {
|
||||
if (!config.enabled) {
|
||||
throw new Error("ALIYUN_OSS_NOT_ENABLED");
|
||||
}
|
||||
if (
|
||||
!config.accessKeyId.trim() ||
|
||||
!config.accessKeySecretEncrypted.trim() ||
|
||||
!config.bucket.trim() ||
|
||||
!config.endpoint.trim() ||
|
||||
!config.region.trim()
|
||||
) {
|
||||
throw new Error("ALIYUN_OSS_CONFIG_INCOMPLETE");
|
||||
}
|
||||
|
||||
const accessKeySecret = await decryptStorageSecret(config.accessKeySecretEncrypted);
|
||||
return new OSS({
|
||||
accessKeyId: config.accessKeyId,
|
||||
accessKeySecret,
|
||||
bucket: config.bucket,
|
||||
endpoint: config.endpoint,
|
||||
region: config.region,
|
||||
});
|
||||
}
|
||||
|
||||
export function createAliyunOssStorageProvider(config: AliyunOssConfig): AttachmentStorageProvider {
|
||||
return {
|
||||
backend: "aliyun_oss",
|
||||
async storeAttachment(params: StoreAttachmentParams): Promise<StoredAttachmentRecord> {
|
||||
const client = await createAliyunOssClient(config);
|
||||
const objectKey = buildObjectKey(params, config.prefix);
|
||||
await client.put(objectKey, params.buffer, {
|
||||
headers: {
|
||||
"Content-Type": params.mimeType || "application/octet-stream",
|
||||
},
|
||||
});
|
||||
return {
|
||||
storageBackend: "aliyun_oss",
|
||||
storagePath: objectKey,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function validateAliyunOssConfig(config: AliyunOssConfig) {
|
||||
const client = await createAliyunOssClient(config);
|
||||
await client.getBucketInfo(config.bucket);
|
||||
return {
|
||||
provider: "aliyun_oss" as const,
|
||||
bucket: config.bucket,
|
||||
endpoint: config.endpoint,
|
||||
region: config.region,
|
||||
};
|
||||
}
|
||||
100
src/lib/boss-storage-config.ts
Normal file
100
src/lib/boss-storage-config.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { UserAttachmentStorageConfig } from "@/lib/boss-data";
|
||||
import { encryptStorageSecret } from "@/lib/boss-storage-secrets";
|
||||
|
||||
export interface AttachmentStorageConfigPatch {
|
||||
mode?: UserAttachmentStorageConfig["mode"];
|
||||
ossProvider?: UserAttachmentStorageConfig["ossProvider"];
|
||||
aliyunOss?: {
|
||||
enabled?: boolean;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
bucket?: string;
|
||||
endpoint?: string;
|
||||
region?: string;
|
||||
prefix?: string;
|
||||
};
|
||||
}
|
||||
|
||||
function trimText(value: string | undefined) {
|
||||
return value?.trim() ?? "";
|
||||
}
|
||||
|
||||
function normalizePrefix(prefix: string | undefined, fallback?: string) {
|
||||
const candidate = prefix?.trim() || fallback?.trim() || "boss/";
|
||||
return candidate || "boss/";
|
||||
}
|
||||
|
||||
function buildAliyunOssBaseConfig(existing?: UserAttachmentStorageConfig["aliyunOss"]) {
|
||||
return {
|
||||
enabled: existing?.enabled ?? false,
|
||||
accessKeyId: existing?.accessKeyId ?? "",
|
||||
accessKeySecretEncrypted: existing?.accessKeySecretEncrypted ?? "",
|
||||
bucket: existing?.bucket ?? "",
|
||||
endpoint: existing?.endpoint ?? "",
|
||||
region: existing?.region ?? "",
|
||||
prefix: existing?.prefix ?? "boss/",
|
||||
};
|
||||
}
|
||||
|
||||
export async function applyAttachmentStorageConfigPatch(
|
||||
existing: UserAttachmentStorageConfig,
|
||||
patch: AttachmentStorageConfigPatch,
|
||||
): Promise<UserAttachmentStorageConfig> {
|
||||
const mode = patch.mode ?? existing.mode;
|
||||
const effectiveOssProvider =
|
||||
mode === "oss" ? patch.ossProvider ?? existing.ossProvider ?? "aliyun_oss" : existing.ossProvider;
|
||||
|
||||
const nextAliyun = buildAliyunOssBaseConfig(existing.aliyunOss);
|
||||
if (patch.aliyunOss) {
|
||||
if (patch.aliyunOss.enabled !== undefined) {
|
||||
nextAliyun.enabled = patch.aliyunOss.enabled;
|
||||
}
|
||||
if (patch.aliyunOss.accessKeyId !== undefined) {
|
||||
nextAliyun.accessKeyId = trimText(patch.aliyunOss.accessKeyId);
|
||||
}
|
||||
if (patch.aliyunOss.bucket !== undefined) {
|
||||
nextAliyun.bucket = trimText(patch.aliyunOss.bucket);
|
||||
}
|
||||
if (patch.aliyunOss.endpoint !== undefined) {
|
||||
nextAliyun.endpoint = trimText(patch.aliyunOss.endpoint);
|
||||
}
|
||||
if (patch.aliyunOss.region !== undefined) {
|
||||
nextAliyun.region = trimText(patch.aliyunOss.region);
|
||||
}
|
||||
if (patch.aliyunOss.prefix !== undefined) {
|
||||
nextAliyun.prefix = normalizePrefix(patch.aliyunOss.prefix, nextAliyun.prefix);
|
||||
}
|
||||
if (patch.aliyunOss.accessKeySecret !== undefined) {
|
||||
nextAliyun.accessKeySecretEncrypted = await encryptStorageSecret(
|
||||
patch.aliyunOss.accessKeySecret,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === "oss") {
|
||||
if (effectiveOssProvider !== "aliyun_oss") {
|
||||
throw new Error("ALIYUN_OSS_PROVIDER_REQUIRED");
|
||||
}
|
||||
if (
|
||||
!nextAliyun.accessKeyId ||
|
||||
!nextAliyun.accessKeySecretEncrypted ||
|
||||
!nextAliyun.bucket ||
|
||||
!nextAliyun.endpoint ||
|
||||
!nextAliyun.region
|
||||
) {
|
||||
throw new Error("ALIYUN_OSS_CONFIG_INCOMPLETE");
|
||||
}
|
||||
nextAliyun.enabled = true;
|
||||
}
|
||||
|
||||
return {
|
||||
account: existing.account,
|
||||
mode,
|
||||
ossProvider: effectiveOssProvider,
|
||||
aliyunOss: nextAliyun,
|
||||
updatedAt: new Date().toISOString(),
|
||||
validatedAt: patch.mode !== undefined || patch.ossProvider !== undefined || patch.aliyunOss
|
||||
? undefined
|
||||
: existing.validatedAt,
|
||||
};
|
||||
}
|
||||
96
src/lib/boss-storage-secrets.ts
Normal file
96
src/lib/boss-storage-secrets.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const STORAGE_SECRET_KEY_FILE = "storage-secret.key";
|
||||
const STORAGE_SECRET_VERSION = "v1";
|
||||
const STORAGE_SECRET_ALGORITHM = "aes-256-gcm";
|
||||
|
||||
function detectRuntimeRoot(startDir: string) {
|
||||
let current = startDir;
|
||||
while (true) {
|
||||
if (
|
||||
path.basename(current) === "boss" &&
|
||||
path.basename(path.dirname(current)) === "code"
|
||||
) {
|
||||
return current;
|
||||
}
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) {
|
||||
return startDir;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveRuntimeRoot() {
|
||||
if (process.env.BOSS_RUNTIME_ROOT?.trim()) {
|
||||
return path.resolve(process.env.BOSS_RUNTIME_ROOT);
|
||||
}
|
||||
if (process.env.BOSS_STATE_FILE?.trim()) {
|
||||
return path.dirname(path.dirname(path.resolve(process.env.BOSS_STATE_FILE)));
|
||||
}
|
||||
return detectRuntimeRoot(process.cwd());
|
||||
}
|
||||
|
||||
function resolveStorageSecretKeyPath() {
|
||||
return path.join(resolveRuntimeRoot(), "data", STORAGE_SECRET_KEY_FILE);
|
||||
}
|
||||
|
||||
function deriveKeyFromSource(source: string) {
|
||||
return createHash("sha256").update(source.normalize("NFKC")).digest();
|
||||
}
|
||||
|
||||
async function getStorageSecretKey() {
|
||||
if (process.env.BOSS_STORAGE_SECRET_KEY?.trim()) {
|
||||
return deriveKeyFromSource(process.env.BOSS_STORAGE_SECRET_KEY);
|
||||
}
|
||||
|
||||
const keyPath = resolveStorageSecretKeyPath();
|
||||
try {
|
||||
const existing = await readFile(keyPath, "utf8");
|
||||
return deriveKeyFromSource(existing.trim());
|
||||
} catch {
|
||||
const generated = randomBytes(32).toString("hex");
|
||||
await mkdir(path.dirname(keyPath), { recursive: true });
|
||||
await writeFile(keyPath, `${generated}\n`, { encoding: "utf8", mode: 0o600 });
|
||||
return deriveKeyFromSource(generated);
|
||||
}
|
||||
}
|
||||
|
||||
export async function encryptStorageSecret(plainText: string) {
|
||||
const normalized = plainText.trim();
|
||||
if (!normalized) {
|
||||
throw new Error("ALIYUN_OSS_SECRET_REQUIRED");
|
||||
}
|
||||
const key = await getStorageSecretKey();
|
||||
const iv = randomBytes(12);
|
||||
const cipher = createCipheriv(STORAGE_SECRET_ALGORITHM, key, iv);
|
||||
const encrypted = Buffer.concat([cipher.update(normalized, "utf8"), cipher.final()]);
|
||||
const tag = cipher.getAuthTag();
|
||||
return [
|
||||
STORAGE_SECRET_VERSION,
|
||||
iv.toString("hex"),
|
||||
tag.toString("hex"),
|
||||
encrypted.toString("hex"),
|
||||
].join(":");
|
||||
}
|
||||
|
||||
export async function decryptStorageSecret(cipherText: string) {
|
||||
const [version, ivHex, tagHex, payloadHex] = cipherText.split(":");
|
||||
if (version !== STORAGE_SECRET_VERSION || !ivHex || !tagHex || !payloadHex) {
|
||||
throw new Error("INVALID_STORAGE_SECRET_FORMAT");
|
||||
}
|
||||
const key = await getStorageSecretKey();
|
||||
const decipher = createDecipheriv(
|
||||
STORAGE_SECRET_ALGORITHM,
|
||||
key,
|
||||
Buffer.from(ivHex, "hex"),
|
||||
);
|
||||
decipher.setAuthTag(Buffer.from(tagHex, "hex"));
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(Buffer.from(payloadHex, "hex")),
|
||||
decipher.final(),
|
||||
]);
|
||||
return decrypted.toString("utf8");
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AttachmentStorageBackend, UserAttachmentStorageConfig } from "@/lib/boss-data";
|
||||
import { createAliyunOssStorageProvider, validateAliyunOssConfig } from "@/lib/boss-storage-aliyun-oss";
|
||||
import { storeServerFileAttachment } from "@/lib/boss-storage-server-file";
|
||||
|
||||
export interface StoreAttachmentParams {
|
||||
@@ -20,6 +21,23 @@ export interface AttachmentStorageProvider {
|
||||
storeAttachment(params: StoreAttachmentParams): Promise<StoredAttachmentRecord>;
|
||||
}
|
||||
|
||||
export interface SanitizedUserAttachmentStorageConfig {
|
||||
account: string;
|
||||
mode: UserAttachmentStorageConfig["mode"];
|
||||
ossProvider?: UserAttachmentStorageConfig["ossProvider"];
|
||||
aliyunOss?: {
|
||||
enabled: boolean;
|
||||
accessKeyId: string;
|
||||
accessKeySecretConfigured: boolean;
|
||||
bucket: string;
|
||||
endpoint: string;
|
||||
region: string;
|
||||
prefix?: string;
|
||||
};
|
||||
updatedAt: string;
|
||||
validatedAt?: string;
|
||||
}
|
||||
|
||||
const serverFileProvider: AttachmentStorageProvider = {
|
||||
backend: "server_file",
|
||||
async storeAttachment(params) {
|
||||
@@ -28,11 +46,63 @@ const serverFileProvider: AttachmentStorageProvider = {
|
||||
};
|
||||
|
||||
export function getAttachmentStorageProvider(
|
||||
config: Pick<UserAttachmentStorageConfig, "mode" | "ossProvider">,
|
||||
config: UserAttachmentStorageConfig,
|
||||
) {
|
||||
if (config.mode === "server_file") {
|
||||
return serverFileProvider;
|
||||
}
|
||||
|
||||
if (config.mode === "oss" && config.ossProvider === "aliyun_oss" && config.aliyunOss) {
|
||||
return createAliyunOssStorageProvider(config.aliyunOss);
|
||||
}
|
||||
|
||||
throw new Error("ATTACHMENT_STORAGE_MODE_NOT_SUPPORTED");
|
||||
}
|
||||
|
||||
export function sanitizeAttachmentStorageConfig(
|
||||
config: UserAttachmentStorageConfig,
|
||||
): SanitizedUserAttachmentStorageConfig {
|
||||
return {
|
||||
account: config.account,
|
||||
mode: config.mode,
|
||||
ossProvider: config.ossProvider,
|
||||
aliyunOss: config.aliyunOss
|
||||
? {
|
||||
enabled: config.aliyunOss.enabled,
|
||||
accessKeyId: config.aliyunOss.accessKeyId,
|
||||
accessKeySecretConfigured: Boolean(config.aliyunOss.accessKeySecretEncrypted),
|
||||
bucket: config.aliyunOss.bucket,
|
||||
endpoint: config.aliyunOss.endpoint,
|
||||
region: config.aliyunOss.region,
|
||||
prefix: config.aliyunOss.prefix,
|
||||
}
|
||||
: undefined,
|
||||
updatedAt: config.updatedAt,
|
||||
validatedAt: config.validatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeStorageError(error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "UNKNOWN_STORAGE_ERROR";
|
||||
switch (message) {
|
||||
case "ALIYUN_OSS_NOT_ENABLED":
|
||||
return "阿里 OSS 尚未启用。";
|
||||
case "ALIYUN_OSS_CONFIG_INCOMPLETE":
|
||||
return "阿里 OSS 配置不完整,请补齐 AccessKey、Bucket、Endpoint 和 Region。";
|
||||
case "ALIYUN_OSS_SECRET_REQUIRED":
|
||||
return "请填写 AccessKey Secret。";
|
||||
case "ALIYUN_OSS_PROVIDER_REQUIRED":
|
||||
return "当前只支持阿里 OSS,请先选择阿里 OSS。";
|
||||
case "INVALID_STORAGE_SECRET_FORMAT":
|
||||
return "当前 OSS 密钥格式无效,请重新填写 AccessKey Secret。";
|
||||
default:
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateAttachmentStorageConfig(config: UserAttachmentStorageConfig) {
|
||||
if (config.mode !== "oss" || config.ossProvider !== "aliyun_oss" || !config.aliyunOss) {
|
||||
throw new Error("ALIYUN_OSS_NOT_ENABLED");
|
||||
}
|
||||
return validateAliyunOssConfig(config.aliyunOss);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user