-
- {t("Bucket")} {index + 1}
+ {s3Configs.map((config, index) => {
+ const updateBucket = (
+ bucketIndex: number,
+ updates: Partial,
+ ) => {
+ const newBuckets = [...config.buckets];
+ newBuckets[bucketIndex] = {
+ ...newBuckets[bucketIndex],
+ ...updates,
+ };
+ setS3Configs(
+ s3Configs.map((c, i) => {
+ if (i === index) {
+ return {
+ ...c,
+ buckets: newBuckets,
+ };
+ }
+ return c;
+ }),
+ );
+ };
+ return (
+
+
+
+ {config.provider_name}
-
- {index > 0 && (
-
{
- const newBuckets = [...r2Credentials.buckets];
- newBuckets.splice(index, 1);
- newBuckets.splice(index - 1, 0, bucket);
- setR2Credentials({
- ...r2Credentials,
- buckets: newBuckets,
- });
+
+ {t("{length} Buckets", {
+ length: config.buckets.length,
+ })}
+
+ {
+ setS3Configs(s3Configs.filter((_, i) => i !== index));
+ }}
+ />
+
+
+
+ {/* Base */}
+
+
+ {t("Provider")}*
+ {
+ const provider = S3_PRVIDERS.find(
+ (p) => `${p.platform} (${p.channel})` === v,
+ );
+ setS3Configs(
+ s3Configs.map((c, i) => {
+ if (i === index) {
+ return {
+ ...c,
+ provider_name: `${provider?.value} (${index + 1})`,
+ channel: provider?.channel || "",
+ platform: provider?.platform || "",
+ };
+ }
+ return c;
+ }),
+ );
}}
>
- {" "}
-
- )}
- {index < r2Credentials.buckets.length - 1 && (
- {
- const newBuckets = [...r2Credentials.buckets];
- newBuckets.splice(index, 1);
- newBuckets.splice(index + 1, 0, bucket);
- setR2Credentials({
- ...r2Credentials,
- buckets: newBuckets,
- });
- }}
- >
- {" "}
-
- )}
- {
- const newBuckets = [...r2Credentials.buckets];
- newBuckets.splice(index + 1, 0, {
- bucket: "",
- prefix: "",
- file_types: "",
- region: "auto",
- custom_domain: "",
- file_size: "26214400",
- public: true,
- });
- setR2Credentials({
- ...r2Credentials,
- buckets: newBuckets,
- });
+
+
+
+
+ {S3_PRVIDERS.map((provider) => (
+
+ {provider.platform} ({provider.channel})
+
+ ))}
+
+
+
+
+
+ {t("Provider Unique Name")}* ({t("Unique")})
+
+
+ setS3Configs(
+ s3Configs.map((c, i) => {
+ if (i === index) {
+ return {
+ ...c,
+ provider_name: e.target.value,
+ };
+ }
+ return c;
+ }),
+ )
+ }
+ />
+
+
+ {t("Endpoint")}*
+
+ setS3Configs(
+ s3Configs.map((c, i) => {
+ if (i === index) {
+ return {
+ ...c,
+ endpoint: e.target.value,
+ };
+ }
+ return c;
+ }),
+ )
+ }
+ />
+
+
+ {t("Access Key ID")}*
+
+ setS3Configs(
+ s3Configs.map((c, i) => {
+ if (i === index) {
+ return {
+ ...c,
+ access_key_id: e.target.value,
+ };
+ }
+ return c;
+ }),
+ )
+ }
+ />
+
+
+ {t("Secret Access Key")}*
+
+ setS3Configs(
+ s3Configs.map((c, i) => {
+ if (i === index) {
+ return {
+ ...c,
+ secret_access_key: e.target.value,
+ };
+ }
+ return c;
+ }),
+ )
+ }
+ />
+
+
+ {t("Enabled")}*
+
+ setS3Configs(
+ s3Configs.map((c, i) => {
+ if (i === index) {
+ return {
+ ...c,
+ enabled: e,
+ };
+ }
+ return c;
+ }),
+ )
+ }
+ />
+
+
+ {/* buckets */}
+ {config.buckets.map((bucket, index2) => (
+
- {" "}
-
- {index !== 0 && (
-
- {
- const newBuckets = [...r2Credentials.buckets];
- newBuckets.splice(index, 1);
- setR2Credentials({
- ...r2Credentials,
- buckets: newBuckets,
- });
- }}
- />
-
- )}
-
+
+ {t("Bucket")} {index2 + 1}
+
-
-
{t("Bucket Name")}*
-
{
- const newBuckets = [...r2Credentials.buckets];
- newBuckets[index] = {
- ...bucket,
- bucket: e.target.value,
- };
- setR2Credentials({
- ...r2Credentials,
- buckets: newBuckets,
- });
+ {/* 按钮部分 */}
+
+ {index2 > 0 && (
+ {
+ const newBuckets = [...config.buckets];
+ newBuckets.splice(index2, 1);
+ newBuckets.splice(index2 - 1, 0, bucket);
+ setS3Configs(
+ s3Configs.map((c, i) => {
+ if (i === index) {
+ return {
+ ...c,
+ buckets: newBuckets,
+ };
+ }
+ return c;
+ }),
+ );
+ }}
+ >
+
+
+ )}
+ {index2 < config.buckets.length - 1 && (
+ {
+ const newBuckets = [...config.buckets];
+ newBuckets.splice(index2, 1);
+ newBuckets.splice(index2 + 1, 0, bucket);
+ setS3Configs(
+ s3Configs.map((c, i) => {
+ if (i === index) {
+ return {
+ ...c,
+ buckets: newBuckets,
+ };
+ }
+ return c;
+ }),
+ );
+ }}
+ >
+
+
+ )}
+ {
+ const newBuckets = [...config.buckets];
+ newBuckets.splice(index2 + 1, 0, {
+ bucket: "",
+ prefix: "",
+ file_types: "",
+ region: "auto",
+ custom_domain: "",
+ file_size: "26214400",
+ public: true,
+ });
+ setS3Configs(
+ s3Configs.map((c, i) => {
+ if (i === index) {
+ return {
+ ...c,
+ buckets: newBuckets,
+ };
+ }
+ return c;
+ }),
+ );
+ }}
+ >
+
+
+ {index2 !== 0 && (
+ {
+ const newBuckets = [...config.buckets];
+ newBuckets.splice(index2, 1);
+ setS3Configs(
+ s3Configs.map((c, i) => {
+ if (i === index) {
+ return {
+ ...c,
+ buckets: newBuckets,
+ };
+ }
+ return c;
+ }),
+ );
+ }}
+ >
+
+
+ )}
+
+
+ {/* 使用 updateBucket 函数的输入字段 */}
+
+ {t("Bucket Name")}*
+
+ updateBucket(index2, { bucket: e.target.value })
+ }
+ />
+
+
+
+ {t("Public Domain")}*
+
+ updateBucket(index2, {
+ custom_domain: e.target.value,
+ })
+ }
+ />
+
+
+
+ {t("Region")}
+
+ updateBucket(index2, { region: e.target.value })
+ }
+ />
+
+
+
+
+ {t("Prefix")} ({t("Optional")})
+
+
+ updateBucket(index2, { prefix: e.target.value })
+ }
+ />
+
+
+
+
+ {t("Public")}
+
+
+
+
+
+
+ {t(
+ "Publicize this storage bucket, all registered users can upload files to this storage bucket; If not public, only administrators can upload files to this storage bucket",
+ )}
+
+
+
+
+
+ updateBucket(index2, { public: e })
+ }
+ />
+
+
+ ))}
+ {/* actions */}
+
+
+ {t("How to get the S3 credentials?")}
+
+ {/* {
+ setS3Configs([
+ {
+ platform: "cloudflare",
+ channel: "r2",
+ provider_name: "Cloudflare R2",
+ endpoint: "",
+ access_key_id: "",
+ secret_access_key: "",
+ buckets: [
+ {
+ bucket: "",
+ prefix: "",
+ file_types: "",
+ region: "auto",
+ custom_domain: "",
+ file_size: "26214400",
+ public: true,
+ },
+ ],
+ account_id: "",
+ enabled: false,
+ },
+ ]);
}}
- />
-
-
- {t("Public Domain")}*
- {
- const newBuckets = [...r2Credentials.buckets];
- newBuckets[index] = {
- ...bucket,
- custom_domain: e.target.value,
- };
- setR2Credentials({
- ...r2Credentials,
- buckets: newBuckets,
- });
+ >
+ {t("Clear")}
+ */}
+ {
+ handleSaveConfigs(
+ s3Configs,
+ "s3_config_list",
+ "OBJECT",
+ );
}}
- />
+ >
+ {isPending ? (
+
+ ) : null}
+ {t("Save")}
+
-
- {t("Region")}
- {
- const newBuckets = [...r2Credentials.buckets];
- newBuckets[index] = {
- ...bucket,
- region: e.target.value,
- };
- setR2Credentials({
- ...r2Credentials,
- buckets: newBuckets,
- });
- }}
- />
-
-
-
- {t("Prefix")} ({t("Optional")})
-
- {
- const newBuckets = [...r2Credentials.buckets];
- newBuckets[index] = {
- ...bucket,
- prefix: e.target.value,
- };
- setR2Credentials({
- ...r2Credentials,
- buckets: newBuckets,
- });
- }}
- />
-
-
-
- {t("Public")}
-
-
-
-
-
-
- {t(
- "Publicize this storage bucket, all registered users can upload files to this storage bucket; If not public, only administrators can upload files to this storage bucket",
- )}
-
-
-
-
-
- setR2Credentials({
- ...r2Credentials,
- buckets: r2Credentials.buckets.map((b, i) => {
- if (i === index) {
- return {
- ...b,
- public: e,
- };
- }
- return b;
- }),
- })
- }
- />
-
- {/*
-
- {t("Allowed File Types")} ({t("Optional")})
-
- {
- const newBuckets = [...r2Credentials.buckets];
- newBuckets[index] = {
- ...bucket,
- file_types: e.target.value,
- };
- setR2Credentials({
- ...r2Credentials,
- buckets: newBuckets,
- });
- }}
- />
-
*/}
-
- ))}
-
-
- {t("How to get the R2 credentials?")}
-
- {
- setR2Credentials({
- platform: "cloudflare",
- channel: "r2",
- provider_name: "cloudflare",
- endpoint: "",
- access_key_id: "",
- secret_access_key: "",
- buckets: [],
- account_id: "",
- enabled: false,
- });
- }}
- >
- {t("Clear")}
-
- {
- handleSaveConfigs(r2Credentials, "s3_config_01", "OBJECT");
- }}
- >
- {isPending ? (
-
- ) : null}
- {t("Save")}
-
-
-
-
+
+
+ );
+ })}
diff --git a/app/api/admin/s3/route.ts b/app/api/admin/s3/route.ts
index 92b0f71..044e747 100644
--- a/app/api/admin/s3/route.ts
+++ b/app/api/admin/s3/route.ts
@@ -15,14 +15,7 @@ export async function GET(req: NextRequest) {
return Response.json("Unauthorized", { status: 401 });
}
- const s3ConfigKeys = [
- "s3_config_01",
- "s3_config_02",
- "s3_config_03",
- "s3_config_04",
- ];
-
- const configs = await getMultipleConfigs(s3ConfigKeys);
+ const configs = await getMultipleConfigs(["s3_config_list"]);
return Response.json(configs, { status: 200 });
} catch (error) {
diff --git a/app/api/storage/admin/r2/files/configs/route.ts b/app/api/storage/admin/r2/files/configs/route.ts
deleted file mode 100644
index b33b924..0000000
--- a/app/api/storage/admin/r2/files/configs/route.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { NextRequest, NextResponse } from "next/server";
-
-import { getMultipleConfigs } from "@/lib/dto/system-config";
-import { checkUserStatus } from "@/lib/dto/user";
-import { getCurrentUser } from "@/lib/session";
-
-export async function GET(req: NextRequest) {
- try {
- const user = checkUserStatus(await getCurrentUser());
- if (user instanceof Response) return user;
- if (user.role !== "ADMIN") {
- return Response.json("Unauthorized", {
- status: 401,
- statusText: "Unauthorized",
- });
- }
-
- const configs = await getMultipleConfigs(["s3_config_01"]);
-
- if (!configs.s3_config_01 || !configs.s3_config_01.enabled) {
- return NextResponse.json({ error: "Invalid S3 config" }, { status: 400 });
- }
-
- return NextResponse.json({
- buckets: configs.s3_config_01.buckets,
- enabled: configs.s3_config_01.enabled,
- provider_name: configs.s3_config_01.provider_name,
- platform: configs.s3_config_01.platform,
- channel: configs.s3_config_01.channel,
- });
- } catch (error) {
- console.error("[Error]", error);
- return NextResponse.json("Error listing buckets", { status: 500 });
- }
-}
diff --git a/app/api/storage/admin/s3/files/configs/route.ts b/app/api/storage/admin/s3/files/configs/route.ts
new file mode 100644
index 0000000..84e1353
--- /dev/null
+++ b/app/api/storage/admin/s3/files/configs/route.ts
@@ -0,0 +1,47 @@
+import { NextRequest, NextResponse } from "next/server";
+
+import { getMultipleConfigs } from "@/lib/dto/system-config";
+import { checkUserStatus } from "@/lib/dto/user";
+import { getCurrentUser } from "@/lib/session";
+
+export async function GET(req: NextRequest) {
+ try {
+ const user = checkUserStatus(await getCurrentUser());
+ if (user instanceof Response) return user;
+ if (user.role !== "ADMIN") {
+ return Response.json("Unauthorized", {
+ status: 401,
+ statusText: "Unauthorized",
+ });
+ }
+
+ const configs = await getMultipleConfigs(["s3_config_list"]);
+ if (!configs || !configs.s3_config_list) {
+ return NextResponse.json("Invalid S3 configs", {
+ status: 400,
+ });
+ }
+
+ const processedList = configs.s3_config_list
+ .filter((c) => c.enabled && c.buckets?.length > 0)
+ .map((c) => ({
+ provider_name: c.provider_name,
+ platform: c.platform,
+ channel: c.channel,
+ enabled: c.enabled,
+ buckets: c.buckets.filter((b) => b?.bucket),
+ }))
+ .filter((c) => c.buckets.length > 0);
+
+ if (processedList.length === 0) {
+ return NextResponse.json("No buckets found", {
+ status: 404,
+ });
+ }
+
+ return NextResponse.json(processedList);
+ } catch (error) {
+ console.error("[Error]", error);
+ return NextResponse.json("Error listing buckets", { status: 500 });
+ }
+}
diff --git a/app/api/storage/admin/r2/files/route.ts b/app/api/storage/admin/s3/files/route.ts
similarity index 58%
rename from app/api/storage/admin/r2/files/route.ts
rename to app/api/storage/admin/s3/files/route.ts
index a286d9f..003bafb 100644
--- a/app/api/storage/admin/r2/files/route.ts
+++ b/app/api/storage/admin/s3/files/route.ts
@@ -21,31 +21,32 @@ export async function GET(req: NextRequest) {
const page = url.searchParams.get("page");
const pageSize = url.searchParams.get("pageSize");
const bucket = url.searchParams.get("bucket") || "";
+ const provider = url.searchParams.get("provider") || "";
const name = url.searchParams.get("name") || "";
const fileSize = url.searchParams.get("fileSize") || "";
const mimeType = url.searchParams.get("mimeType") || "";
const status = url.searchParams.get("status") || "";
- const configs = await getMultipleConfigs(["s3_config_01"]);
- if (!configs.s3_config_01.enabled) {
- return NextResponse.json("S3 is not enabled", {
- status: 403,
+ const configs = await getMultipleConfigs(["s3_config_list"]);
+ if (!configs || !configs.s3_config_list) {
+ return NextResponse.json("Invalid S3 configs", {
+ status: 400,
});
}
- if (
- !configs.s3_config_01 ||
- !configs.s3_config_01.access_key_id ||
- !configs.s3_config_01.secret_access_key ||
- !configs.s3_config_01.endpoint
- ) {
- return NextResponse.json("Invalid S3 config", {
- status: 403,
+
+ const providerChannel = configs.s3_config_list.find(
+ (c) => c.provider_name === provider,
+ );
+ if (!providerChannel) {
+ return NextResponse.json("Provider does not exist", {
+ status: 400,
});
}
- const buckets = configs.s3_config_01.buckets || [];
+
+ const buckets = providerChannel.buckets || [];
if (!buckets.find((b) => b.bucket === bucket)) {
return NextResponse.json("Bucket does not exist", {
- status: 403,
+ status: 400,
});
}
@@ -53,18 +54,19 @@ export async function GET(req: NextRequest) {
page: Number(page) || 1,
limit: Number(pageSize) || 20,
bucket,
- channel: configs.s3_config_01.channel,
- platform: configs.s3_config_01.platform,
name,
size: Number(fileSize || 0),
status: Number(status === "0" ? 0 : 1),
mimeType,
+ channel: providerChannel.channel,
+ platform: providerChannel.platform,
+ providerName: providerChannel.provider_name,
});
return NextResponse.json(res);
} catch (error) {
console.error("Error listing files:", error);
- return NextResponse.json({ error: "Error listing files" }, { status: 500 });
+ return NextResponse.json("Error listing files", { status: 500 });
}
}
@@ -79,51 +81,48 @@ export async function POST(request: NextRequest) {
});
}
- const { key, bucket } = await request.json();
- if (!key || !bucket) {
+ const { key, bucket, provider } = await request.json();
+ if (!key || !bucket || !provider) {
return NextResponse.json("key and bucket is required", {
status: 400,
});
}
- const configs = await getMultipleConfigs(["s3_config_01"]);
- if (!configs.s3_config_01.enabled) {
- return NextResponse.json("S3 is not enabled", {
- status: 403,
+ const configs = await getMultipleConfigs(["s3_config_list"]);
+ if (!configs || !configs.s3_config_list) {
+ return NextResponse.json("Invalid S3 configs", {
+ status: 400,
});
}
- if (
- !configs.s3_config_01 ||
- !configs.s3_config_01.access_key_id ||
- !configs.s3_config_01.secret_access_key ||
- !configs.s3_config_01.endpoint
- ) {
- return NextResponse.json("Invalid S3 config", {
- status: 403,
+
+ const providerChannel = configs.s3_config_list.find(
+ (c) => c.provider_name === provider,
+ );
+ if (!providerChannel) {
+ return NextResponse.json("Provider does not exist", {
+ status: 400,
});
}
- const buckets = configs.s3_config_01.buckets || [];
+
+ const buckets = providerChannel.buckets || [];
if (!buckets.find((b) => b.bucket === bucket)) {
return NextResponse.json("Bucket does not exist", {
- status: 403,
+ status: 400,
});
}
const signedUrl = await getSignedUrlForDownload(
key,
createS3Client(
- configs.s3_config_01.endpoint,
- configs.s3_config_01.access_key_id,
- configs.s3_config_01.secret_access_key,
+ providerChannel.endpoint,
+ providerChannel.access_key_id,
+ providerChannel.secret_access_key,
),
bucket,
);
return NextResponse.json({ signedUrl });
} catch (error) {
- return NextResponse.json(
- { error: "Error generating download URL" },
- { status: 500 },
- );
+ return NextResponse.json("Error generating download URL", { status: 500 });
}
}
@@ -138,41 +137,41 @@ export async function DELETE(request: NextRequest) {
});
}
- const { keys, ids, bucket } = await request.json();
+ const { keys, ids, bucket, provider } = await request.json();
- if (!keys || !ids || !bucket) {
+ if (!keys || !ids || !bucket || !provider) {
return NextResponse.json("key and bucket is required", {
status: 400,
});
}
- const configs = await getMultipleConfigs(["s3_config_01"]);
- if (!configs.s3_config_01.enabled) {
- return NextResponse.json("S3 is not enabled", {
- status: 403,
+ const configs = await getMultipleConfigs(["s3_config_list"]);
+ if (!configs || !configs.s3_config_list) {
+ return NextResponse.json("Invalid S3 configs", {
+ status: 400,
});
}
- if (
- !configs.s3_config_01 ||
- !configs.s3_config_01.access_key_id ||
- !configs.s3_config_01.secret_access_key ||
- !configs.s3_config_01.endpoint
- ) {
- return NextResponse.json("Invalid S3 config", {
- status: 403,
+
+ const providerChannel = configs.s3_config_list.find(
+ (c) => c.provider_name === provider,
+ );
+ if (!providerChannel) {
+ return NextResponse.json("Provider does not exist", {
+ status: 400,
});
}
- const buckets = configs.s3_config_01.buckets || [];
+
+ const buckets = providerChannel.buckets || [];
if (!buckets.find((b) => b.bucket === bucket)) {
return NextResponse.json("Bucket does not exist", {
- status: 403,
+ status: 400,
});
}
const R2 = createS3Client(
- configs.s3_config_01.endpoint,
- configs.s3_config_01.access_key_id,
- configs.s3_config_01.secret_access_key,
+ providerChannel.endpoint,
+ providerChannel.access_key_id,
+ providerChannel.secret_access_key,
);
for (const key of keys) {
@@ -181,6 +180,6 @@ export async function DELETE(request: NextRequest) {
await softDeleteUserFiles(ids);
return NextResponse.json({ message: "File deleted successfully" });
} catch (error) {
- return NextResponse.json({ error: "Error deleting file" }, { status: 500 });
+ return NextResponse.json("Error deleting file", { status: 500 });
}
}
diff --git a/app/api/storage/admin/r2/files/short/route.ts b/app/api/storage/admin/s3/files/short/route.ts
similarity index 89%
rename from app/api/storage/admin/r2/files/short/route.ts
rename to app/api/storage/admin/s3/files/short/route.ts
index c4939c9..6997951 100644
--- a/app/api/storage/admin/r2/files/short/route.ts
+++ b/app/api/storage/admin/s3/files/short/route.ts
@@ -23,7 +23,6 @@ export async function POST(request: NextRequest) {
const data = await getUserShortLinksByIds(ids, user.id);
- // ids:["cmcrqtql10001zbhynvwfuqza", "", "", "", ""],则返回的短链位置也要一一对应
const dataMap = new Map(data.map((item) => [item.id, item]));
const orderedResults = ids.map((id) => {
@@ -35,10 +34,7 @@ export async function POST(request: NextRequest) {
urls: orderedResults,
});
} catch (error) {
- return NextResponse.json(
- { error: "Error generating download URL" },
- { status: 500 },
- );
+ return NextResponse.json("Error generating download URL", { status: 500 });
}
}
diff --git a/app/api/storage/r2/files/configs/route.ts b/app/api/storage/r2/files/configs/route.ts
deleted file mode 100644
index 3c09f49..0000000
--- a/app/api/storage/r2/files/configs/route.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { NextRequest, NextResponse } from "next/server";
-
-import { getMultipleConfigs } from "@/lib/dto/system-config";
-import { checkUserStatus } from "@/lib/dto/user";
-import { getCurrentUser } from "@/lib/session";
-
-export async function GET(req: NextRequest) {
- try {
- const user = checkUserStatus(await getCurrentUser());
- if (user instanceof Response) return user;
-
- const configs = await getMultipleConfigs(["s3_config_01"]);
-
- if (!configs.s3_config_01 || !configs.s3_config_01.enabled) {
- return NextResponse.json({ error: "Invalid S3 config" }, { status: 400 });
- }
-
- return NextResponse.json({
- buckets: configs.s3_config_01.buckets.filter((b) => b.public), // public
- enabled: configs.s3_config_01.enabled,
- provider_name: configs.s3_config_01.provider_name,
- platform: configs.s3_config_01.platform,
- channel: configs.s3_config_01.channel,
- });
- } catch (error) {
- return NextResponse.json(
- { error: "Error listing buckets" },
- { status: 500 },
- );
- }
-}
diff --git a/app/api/storage/s3/files/configs/route.ts b/app/api/storage/s3/files/configs/route.ts
new file mode 100644
index 0000000..2edd7fd
--- /dev/null
+++ b/app/api/storage/s3/files/configs/route.ts
@@ -0,0 +1,40 @@
+import { NextRequest, NextResponse } from "next/server";
+
+import { getMultipleConfigs } from "@/lib/dto/system-config";
+import { checkUserStatus } from "@/lib/dto/user";
+import { getCurrentUser } from "@/lib/session";
+
+export async function GET(req: NextRequest) {
+ try {
+ const user = checkUserStatus(await getCurrentUser());
+ if (user instanceof Response) return user;
+
+ const configs = await getMultipleConfigs(["s3_config_list"]);
+ if (!configs || !configs.s3_config_list) {
+ return NextResponse.json("Invalid S3 configs", {
+ status: 400,
+ });
+ }
+
+ const processedList = configs.s3_config_list
+ .filter((c) => c.enabled && c.buckets?.length > 0)
+ .map((c) => ({
+ provider_name: c.provider_name,
+ platform: c.platform,
+ channel: c.channel,
+ enabled: c.enabled,
+ buckets: c.buckets.filter((b) => b?.bucket && b?.public),
+ }))
+ .filter((c) => c.buckets.length > 0);
+
+ if (processedList.length === 0) {
+ return NextResponse.json("No buckets found", {
+ status: 404,
+ });
+ }
+
+ return NextResponse.json(processedList);
+ } catch (error) {
+ return NextResponse.json("Error listing buckets", { status: 500 });
+ }
+}
diff --git a/app/api/storage/r2/files/route.ts b/app/api/storage/s3/files/route.ts
similarity index 55%
rename from app/api/storage/r2/files/route.ts
rename to app/api/storage/s3/files/route.ts
index 7242c29..6408b67 100644
--- a/app/api/storage/r2/files/route.ts
+++ b/app/api/storage/s3/files/route.ts
@@ -15,30 +15,31 @@ export async function GET(req: NextRequest) {
const page = url.searchParams.get("page");
const pageSize = url.searchParams.get("pageSize");
const bucket = url.searchParams.get("bucket") || "";
+ const provider = url.searchParams.get("provider") || "";
const name = url.searchParams.get("name") || "";
const fileSize = url.searchParams.get("fileSize") || "";
const mimeType = url.searchParams.get("mimeType") || "";
- const configs = await getMultipleConfigs(["s3_config_01"]);
- if (!configs.s3_config_01.enabled) {
- return NextResponse.json("S3 is not enabled", {
- status: 403,
+ const configs = await getMultipleConfigs(["s3_config_list"]);
+ if (!configs || !configs.s3_config_list) {
+ return NextResponse.json("Invalid S3 configs", {
+ status: 400,
});
}
- if (
- !configs.s3_config_01 ||
- !configs.s3_config_01.access_key_id ||
- !configs.s3_config_01.secret_access_key ||
- !configs.s3_config_01.endpoint
- ) {
- return NextResponse.json("Invalid S3 config", {
- status: 403,
+
+ const providerChannel = configs.s3_config_list.find(
+ (c) => c.provider_name === provider,
+ );
+ if (!providerChannel) {
+ return NextResponse.json("Provider does not exist", {
+ status: 400,
});
}
- const buckets = configs.s3_config_01.buckets || [];
+
+ const buckets = providerChannel.buckets || [];
if (!buckets.find((b) => b.bucket === bucket)) {
return NextResponse.json("Bucket does not exist", {
- status: 403,
+ status: 400,
});
}
@@ -48,8 +49,9 @@ export async function GET(req: NextRequest) {
bucket,
userId: user.id,
status: 1,
- channel: configs.s3_config_01.channel,
- platform: configs.s3_config_01.platform,
+ channel: providerChannel.channel,
+ platform: providerChannel.platform,
+ providerName: providerChannel.provider_name,
name,
size: Number(fileSize || 0),
mimeType,
@@ -67,51 +69,48 @@ export async function POST(request: NextRequest) {
const user = checkUserStatus(await getCurrentUser());
if (user instanceof Response) return user;
- const { key, bucket } = await request.json();
- if (!key || !bucket) {
+ const { key, bucket, provider } = await request.json();
+ if (!key || !bucket || !provider) {
return NextResponse.json("key and bucket is required", {
status: 400,
});
}
- const configs = await getMultipleConfigs(["s3_config_01"]);
- if (!configs.s3_config_01.enabled) {
- return NextResponse.json("S3 is not enabled", {
- status: 403,
+ const configs = await getMultipleConfigs(["s3_config_list"]);
+ if (!configs || !configs.s3_config_list) {
+ return NextResponse.json("Invalid S3 configs", {
+ status: 400,
});
}
- if (
- !configs.s3_config_01 ||
- !configs.s3_config_01.access_key_id ||
- !configs.s3_config_01.secret_access_key ||
- !configs.s3_config_01.endpoint
- ) {
- return NextResponse.json("Invalid S3 config", {
- status: 403,
+
+ const providerChannel = configs.s3_config_list.find(
+ (c) => c.provider_name === provider,
+ );
+ if (!providerChannel) {
+ return NextResponse.json("Provider does not exist", {
+ status: 400,
});
}
- const buckets = configs.s3_config_01.buckets || [];
+
+ const buckets = providerChannel.buckets || [];
if (!buckets.find((b) => b.bucket === bucket)) {
return NextResponse.json("Bucket does not exist", {
- status: 403,
+ status: 400,
});
}
const signedUrl = await getSignedUrlForDownload(
key,
createS3Client(
- configs.s3_config_01.endpoint,
- configs.s3_config_01.access_key_id,
- configs.s3_config_01.secret_access_key,
+ providerChannel.endpoint,
+ providerChannel.access_key_id,
+ providerChannel.secret_access_key,
),
bucket,
);
return NextResponse.json({ signedUrl });
} catch (error) {
- return NextResponse.json(
- { error: "Error generating download URL" },
- { status: 500 },
- );
+ return NextResponse.json("Error generating download URL", { status: 500 });
}
}
@@ -120,41 +119,41 @@ export async function DELETE(request: NextRequest) {
const user = checkUserStatus(await getCurrentUser());
if (user instanceof Response) return user;
- const { keys, ids, bucket } = await request.json();
+ const { keys, ids, bucket, provider } = await request.json();
- if (!keys || !ids || !bucket) {
+ if (!keys || !ids || !bucket || !provider) {
return NextResponse.json("key and bucket is required", {
status: 400,
});
}
- const configs = await getMultipleConfigs(["s3_config_01"]);
- if (!configs.s3_config_01.enabled) {
- return NextResponse.json("S3 is not enabled", {
- status: 403,
+ const configs = await getMultipleConfigs(["s3_config_list"]);
+ if (!configs || !configs.s3_config_list) {
+ return NextResponse.json("Invalid S3 configs", {
+ status: 400,
});
}
- if (
- !configs.s3_config_01 ||
- !configs.s3_config_01.access_key_id ||
- !configs.s3_config_01.secret_access_key ||
- !configs.s3_config_01.endpoint
- ) {
- return NextResponse.json("Invalid S3 config", {
- status: 403,
+
+ const providerChannel = configs.s3_config_list.find(
+ (c) => c.provider_name === provider,
+ );
+ if (!providerChannel) {
+ return NextResponse.json("Provider does not exist", {
+ status: 400,
});
}
- const buckets = configs.s3_config_01.buckets || [];
+
+ const buckets = providerChannel.buckets || [];
if (!buckets.find((b) => b.bucket === bucket)) {
return NextResponse.json("Bucket does not exist", {
- status: 403,
+ status: 400,
});
}
const R2 = createS3Client(
- configs.s3_config_01.endpoint,
- configs.s3_config_01.access_key_id,
- configs.s3_config_01.secret_access_key,
+ providerChannel.endpoint,
+ providerChannel.access_key_id,
+ providerChannel.secret_access_key,
);
for (const key of keys) {
@@ -163,6 +162,6 @@ export async function DELETE(request: NextRequest) {
await softDeleteUserFiles(ids);
return NextResponse.json({ message: "File deleted successfully" });
} catch (error) {
- return NextResponse.json({ error: "Error deleting file" }, { status: 500 });
+ return NextResponse.json("Error deleting file", { status: 500 });
}
}
diff --git a/app/api/storage/r2/files/short/route.ts b/app/api/storage/s3/files/short/route.ts
similarity index 88%
rename from app/api/storage/r2/files/short/route.ts
rename to app/api/storage/s3/files/short/route.ts
index 2ed825d..d122f55 100644
--- a/app/api/storage/r2/files/short/route.ts
+++ b/app/api/storage/s3/files/short/route.ts
@@ -17,7 +17,6 @@ export async function POST(request: NextRequest) {
const data = await getUserShortLinksByIds(ids, user.id);
- // ids:["cmcrqtql10001zbhynvwfuqza", "", "", "", ""],则返回的短链位置也要一一对应
const dataMap = new Map(data.map((item) => [item.id, item]));
const orderedResults = ids.map((id) => {
@@ -29,10 +28,7 @@ export async function POST(request: NextRequest) {
urls: orderedResults,
});
} catch (error) {
- return NextResponse.json(
- { error: "Error generating download URL" },
- { status: 500 },
- );
+ return NextResponse.json("Error generating download URL", { status: 500 });
}
}
diff --git a/app/api/storage/r2/upload/add/route.ts b/app/api/storage/s3/upload/add/route.ts
similarity index 99%
rename from app/api/storage/r2/upload/add/route.ts
rename to app/api/storage/s3/upload/add/route.ts
index 06a0f4d..90bc331 100644
--- a/app/api/storage/r2/upload/add/route.ts
+++ b/app/api/storage/s3/upload/add/route.ts
@@ -34,6 +34,8 @@ export async function POST(request: NextRequest) {
);
}
+ console.log(body);
+
const userFile = await createUserFile({
userId: body.userId,
name: body.name,
diff --git a/app/api/storage/r2/upload/route.ts b/app/api/storage/s3/upload/route.ts
similarity index 85%
rename from app/api/storage/r2/upload/route.ts
rename to app/api/storage/s3/upload/route.ts
index d06faba..7846743 100644
--- a/app/api/storage/r2/upload/route.ts
+++ b/app/api/storage/s3/upload/route.ts
@@ -15,32 +15,32 @@ export async function POST(request: NextRequest) {
const user = checkUserStatus(await getCurrentUser());
if (user instanceof Response) return user;
- const { files, bucket, prefix } = await request.json();
+ const { provider, bucket, files, prefix } = await request.json();
if (!bucket || !files || !Array.isArray(files)) {
return NextResponse.json("Invalid request parameters", { status: 400 });
}
- const configs = await getMultipleConfigs(["s3_config_01"]);
- if (!configs.s3_config_01.enabled) {
- return NextResponse.json("S3 is not enabled", {
- status: 403,
+ const configs = await getMultipleConfigs(["s3_config_list"]);
+ if (!configs || !configs.s3_config_list) {
+ return NextResponse.json("Invalid S3 configs", {
+ status: 400,
});
}
- if (
- !configs.s3_config_01 ||
- !configs.s3_config_01.access_key_id ||
- !configs.s3_config_01.secret_access_key ||
- !configs.s3_config_01.endpoint
- ) {
- return NextResponse.json("Invalid S3 config", {
- status: 403,
+
+ const providerChannel = configs.s3_config_list.find(
+ (c) => c.provider_name === provider,
+ );
+ if (!providerChannel) {
+ return NextResponse.json("Provider does not exist", {
+ status: 400,
});
}
- const buckets = configs.s3_config_01.buckets || [];
+
+ const buckets = providerChannel.buckets || [];
if (!buckets.find((b) => b.bucket === bucket)) {
return NextResponse.json("Bucket does not exist", {
- status: 403,
+ status: 400,
});
}
@@ -61,9 +61,9 @@ export async function POST(request: NextRequest) {
if (limit) return Response.json(limit.statusText, { status: limit.status });
const R2 = createS3Client(
- configs.s3_config_01.endpoint,
- configs.s3_config_01.access_key_id,
- configs.s3_config_01.secret_access_key,
+ providerChannel.endpoint,
+ providerChannel.access_key_id,
+ providerChannel.secret_access_key,
);
const signedUrls = await Promise.all(
diff --git a/components/file/file-list.tsx b/components/file/file-list.tsx
index abc5742..ea61ff0 100644
--- a/components/file/file-list.tsx
+++ b/components/file/file-list.tsx
@@ -108,10 +108,14 @@ export default function UserFileList({
const handlePreviewRawFile = async (key: string) => {
try {
- const response = await fetch(`${action}/r2/files`, {
+ const response = await fetch(`${action}/s3/files`, {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ key, bucket: bucketInfo.bucket }),
+ body: JSON.stringify({
+ key,
+ bucket: bucketInfo.bucket,
+ provider: bucketInfo.provider_name,
+ }),
});
const { signedUrl } = await response.json();
window.open(signedUrl, "_blank");
@@ -126,13 +130,14 @@ export default function UserFileList({
try {
toast.promise(
- fetch(`${action}/r2/files`, {
+ fetch(`${action}/s3/files`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
keys: [file.path],
ids: [file.id],
bucket: bucketInfo.bucket,
+ provider: bucketInfo.provider_name,
}),
}),
{
@@ -151,7 +156,7 @@ export default function UserFileList({
const handleGenerateShortLink = async (urlId: string) => {
if (!shortTarget) return;
try {
- const response = await fetch(`${action}/r2/files/short`, {
+ const response = await fetch(`${action}/s3/files/short`, {
method: "PUT",
body: JSON.stringify({ urlId, fileId: shortTarget?.id }),
});
@@ -159,7 +164,6 @@ export default function UserFileList({
toast.error("Error generating short link");
} else {
onRefresh();
- // handleGetFileShortLinkByIds();
}
} catch (error) {
console.error("Error generating short link:", error);
@@ -172,7 +176,7 @@ export default function UserFileList({
try {
const ids = files.list.map((f) => f.shortUrlId || "");
if (!ids?.some((id) => id !== "")) return;
- const response = await fetch(`${action}/r2/files/short`, {
+ const response = await fetch(`${action}/s3/files/short`, {
method: "POST",
body: JSON.stringify({ ids }),
});
@@ -251,11 +255,11 @@ export default function UserFileList({
- {`[${file.name}](${getFileUrl(file.path)})`}
+ {`})`}
>
@@ -325,7 +329,7 @@ export default function UserFileList({
width={300}
height={300}
src={getFileUrl(file.path)}
- alt={`${file.path}`}
+ alt={`${file.name}`}
/>
)}
{renderFileLinks(file, index)}
@@ -491,7 +495,7 @@ export default function UserFileList({
{React.cloneElement(getFileIcon(file, bucketInfo), { size: 40 })}
{file.mimeType.startsWith("image/") &&
@@ -501,7 +505,7 @@ export default function UserFileList({
width={300}
height={300}
src={getFileUrl(file.path)}
- alt={`${file.path}`}
+ alt={`${file.name}`}
/>
)}
diff --git a/components/file/index.tsx b/components/file/index.tsx
index d553463..d5e0521 100644
--- a/components/file/index.tsx
+++ b/components/file/index.tsx
@@ -16,17 +16,13 @@ import {
SelectGroup,
SelectItem,
SelectLabel,
+ SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from "@/components/ui/tooltip";
+import { ClickableTooltip } from "@/components/ui/tooltip";
import UserFileList from "@/components/file/file-list";
import { Icons } from "@/components/shared/icons";
@@ -84,6 +80,8 @@ export default function UserFileManager({ user, action }: FileListProps) {
provider_name: "",
public: true,
});
+ const [currentProvider, setCurrentProvider] =
+ useState(null);
const [selectedFiles, setSelectedFiles] = useState([]);
const [isDeleting, startDeleteTransition] = useTransition();
@@ -100,8 +98,8 @@ export default function UserFileManager({ user, action }: FileListProps) {
const { mutate } = useSWRConfig();
- const { data: r2Configs, isLoading } = useSWR(
- `${action}/r2/files/configs`,
+ const { data: s3Configs, isLoading } = useSWR(
+ `${action}/s3/files/configs`,
fetcher,
{ revalidateOnFocus: false },
);
@@ -112,7 +110,7 @@ export default function UserFileManager({ user, action }: FileListProps) {
error,
} = useSWR(
currentBucketInfo.bucket
- ? `${action}/r2/files?bucket=${currentBucketInfo.bucket}&page=${currentPage}&pageSize=${pageSize}&name=${searchParams.name}&fileSize=${searchParams.fileSize}&mimeType=${searchParams.mimeType}&status=${searchParams.status}`
+ ? `${action}/s3/files?provider=${currentBucketInfo.provider_name}&bucket=${currentBucketInfo.bucket}&page=${currentPage}&pageSize=${pageSize}&name=${searchParams.name}&fileSize=${searchParams.fileSize}&mimeType=${searchParams.mimeType}&status=${searchParams.status}`
: null,
fetcher,
{
@@ -127,36 +125,43 @@ export default function UserFileManager({ user, action }: FileListProps) {
);
useEffect(() => {
- if (
- r2Configs &&
- r2Configs.buckets &&
- r2Configs.buckets.length > 0 &&
- r2Configs.buckets[0].bucket
- ) {
+ if (s3Configs && s3Configs.length > 0) {
+ setCurrentProvider(s3Configs[0]);
setCurrentBucketInfo({
- ...r2Configs.buckets[0],
- platform: r2Configs.platform,
- channel: r2Configs.channel,
- provider_name: r2Configs.provider_name,
+ bucket: s3Configs[0].buckets[0].bucket,
+ custom_domain: s3Configs[0].buckets[0].custom_domain,
+ prefix: s3Configs[0].buckets[0].prefix,
+ platform: s3Configs[0].platform,
+ channel: s3Configs[0].channel,
+ provider_name: s3Configs[0].provider_name,
+ public: s3Configs[0].buckets[0].public,
});
}
- }, [r2Configs]);
+ }, [s3Configs]);
const handleRefresh = () => {
setSelectedFiles([]);
mutate(
- `${action}/r2/files?bucket=${currentBucketInfo.bucket}&page=${currentPage}&pageSize=${pageSize}&name=${searchParams.name}&fileSize=${searchParams.fileSize}&mimeType=${searchParams.mimeType}&status=${searchParams.status}`,
+ `${action}/s3/files?provider=${currentBucketInfo.provider_name}&bucket=${currentBucketInfo.bucket}&page=${currentPage}&pageSize=${pageSize}&name=${searchParams.name}&fileSize=${searchParams.fileSize}&mimeType=${searchParams.mimeType}&status=${searchParams.status}`,
undefined,
);
};
- const handleChangeBucket = (bucket: string) => {
- const newBucketInfo = r2Configs?.buckets?.find(
- (item) => item.bucket === bucket,
- );
+ const handleChangeBucket = (
+ provider: ClientStorageCredentials,
+ bucket: string,
+ ) => {
+ console.log(provider, bucket);
+
setCurrentBucketInfo({
- ...currentBucketInfo,
- ...newBucketInfo,
+ bucket: bucket,
+ custom_domain: provider.buckets.find((b) => b.bucket === bucket)
+ ?.custom_domain,
+ prefix: provider.buckets.find((b) => b.bucket === bucket)?.prefix,
+ platform: provider.platform,
+ channel: provider.channel,
+ provider_name: provider.provider_name,
+ public: true,
});
};
@@ -172,13 +177,14 @@ export default function UserFileManager({ user, action }: FileListProps) {
startDeleteTransition(async () => {
try {
toast.promise(
- fetch(`${action}/r2/files`, {
+ fetch(`${action}/s3/files`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
keys: selectedFiles.map((file) => file.path),
ids: selectedFiles.map((file) => file.id),
bucket: currentBucketInfo.bucket,
+ provider: currentBucketInfo.provider_name,
}),
}),
{
@@ -252,58 +258,59 @@ export default function UserFileManager({ user, action }: FileListProps) {
{/* Storage */}
{files && files.totalSize > 0 && plan && (
-
-
-
-
-
-
+
-
-
-
+
+ }
+ >
+
+
)}
{/* Bucket Select */}
{isLoading ? (
) : (
- r2Configs &&
- r2Configs.buckets &&
- r2Configs.buckets.length > 0 &&
- r2Configs.buckets[0].bucket && (
+ s3Configs &&
+ s3Configs.length > 0 && (
{
+ const [providerName, bucketName] = value.split("|");
+ const provider = s3Configs.find(
+ (p) => p.provider_name === providerName,
+ );
+ provider && handleChangeBucket(provider, bucketName);
+ }}
>
-
+
-
-
- {r2Configs?.provider_name}
-
- {r2Configs?.buckets?.map((item) => (
- handleChangeBucket(item.bucket)}
- >
- {item.bucket}
-
- ))}
-
- {/* */}
+ {s3Configs.map((provider) => (
+
+ {provider.provider_name}
+ {provider.buckets?.map((item) => (
+
+ {item.bucket}
+
+ ))}
+
+
+ ))}
)
)}
{/* Uploader */}
{!isLoading &&
- r2Configs &&
- r2Configs.buckets &&
- r2Configs.buckets.length > 0 &&
- r2Configs.buckets[0].bucket && (
+ s3Configs &&
+ s3Configs.length > 0 &&
+ currentBucketInfo && (
)}
- {!isLoading &&
- !error &&
- (!r2Configs?.buckets?.length || !r2Configs?.buckets?.[0].bucket) && (
-
-
-
- {t("No buckets found")}
-
-
- {t(
- "The administrator has not configured the storage bucket, no file can be uploaded",
- )}
-
-
- )}
+ {!isLoading && !error && !s3Configs && !currentBucketInfo && (
+
+
+
+ {t("No buckets found")}
+
+
+ {t(
+ "The administrator has not configured the storage bucket, no file can be uploaded",
+ )}
+
+
+ )}
{!isLoading &&
!error &&
- r2Configs?.buckets &&
- r2Configs.buckets.length > 0 &&
- r2Configs?.buckets[0].bucket && (
+ s3Configs &&
+ s3Configs.length > 0 &&
+ currentBucketInfo && (
- {formatFileSize(totalSize, { precision: 0 })}
+ {formatFileSize(totalSize, { precision: 2 })}
@@ -70,7 +70,7 @@ export function FileSizeDisplay({ files, plan, t }) {
{t("totalCapacity")}:
- {formatFileSize(maxSize, { precision: 0 })}
+ {formatFileSize(maxSize, { precision: 2 })}
@@ -78,7 +78,7 @@ export function FileSizeDisplay({ files, plan, t }) {
{t("availableSpace")}:
- {formatFileSize(maxSize - totalSize, { precision: 0 })}
+ {formatFileSize(maxSize - totalSize, { precision: 2 })}
@@ -121,7 +121,7 @@ export function CircularStorageIndicator({ files, plan, size = 32 }) {
return (
diff --git a/components/file/upload.tsx b/components/file/upload.tsx
index e325b4b..ac977a6 100644
--- a/components/file/upload.tsx
+++ b/components/file/upload.tsx
@@ -46,7 +46,7 @@ export const FileUploader = ({
retryUpload,
startUpload,
clearAll,
- } = useFileUpload({ api: `${action}/r2/upload`, bucketInfo, userId });
+ } = useFileUpload({ api: `${action}/s3/upload`, bucketInfo, userId });
useEffect(() => {
if (selectedFile) {
diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx
index 525c40e..1c57881 100644
--- a/components/ui/tooltip.tsx
+++ b/components/ui/tooltip.tsx
@@ -5,6 +5,7 @@ import { useState } from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
+import { useMediaQuery } from "@/hooks/use-media-query";
const TooltipProvider = TooltipPrimitive.Provider;
@@ -43,6 +44,7 @@ export {
export const ClickableTooltip = ({ children, content, className = "" }) => {
const [open, setOpen] = useState(false);
+ const { isMobile } = useMediaQuery();
const handleClick = (e) => {
e.preventDefault();
@@ -65,12 +67,14 @@ export const ClickableTooltip = ({ children, content, className = "" }) => {
onFocus={(e) => e.preventDefault()} // 阻止焦点事件
onBlur={(e) => e.preventDefault()}
>
-
+
{children}
- {content}
+
+ {content}
+
diff --git a/hooks/use-file-upload.ts b/hooks/use-file-upload.ts
index 204f7b2..c0302fd 100644
--- a/hooks/use-file-upload.ts
+++ b/hooks/use-file-upload.ts
@@ -139,8 +139,9 @@ export function useFileUpload({ bucketInfo, userId, api }: Props) {
},
body: JSON.stringify({
files: filesData,
- bucket: bucketInfo.bucket,
prefix: bucketInfo.prefix,
+ bucket: bucketInfo.bucket,
+ provider: bucketInfo.provider_name,
}),
});
diff --git a/locales/en.json b/locales/en.json
index fa4d7c4..73ec486 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -625,6 +625,13 @@
"Optional": "Optional",
"Allowed File Types": "Allowed File Types",
"Public": "Public",
- "Publicize this storage bucket, all registered users can upload files to this storage bucket; If not public, only administrators can upload files to this storage bucket": "Publicize this storage bucket, all registered users can upload files to this storage bucket; If not public, only administrators can upload files to this storage bucket"
+ "Publicize this storage bucket, all registered users can upload files to this storage bucket; If not public, only administrators can upload files to this storage bucket": "Publicize this storage bucket, all registered users can upload files to this storage bucket; If not public, only administrators can upload files to this storage bucket",
+ "Provider": "Provider",
+ "How to get the S3 credentials?": "How to get the S3 credentials?",
+ "Provider Unique Name": "Provider Unique Name",
+ "Unique": "Unique",
+ "Add Provider": "Add Provider",
+ "{length} Buckets": "{length} Buckets",
+ "Save Modifications": "Save Modifications"
}
}
diff --git a/locales/zh.json b/locales/zh.json
index cd6bdf9..237aa1e 100644
--- a/locales/zh.json
+++ b/locales/zh.json
@@ -625,6 +625,13 @@
"Optional": "可选",
"Allowed File Types": "允许的文件类型",
"Public": "公开",
- "Publicize this storage bucket, all registered users can upload files to this storage bucket; If not public, only administrators can upload files to this storage bucket": "公开此存储桶,所有注册用户都可以上传文件到此存储桶; 若不公开,只有管理员可以上传文件到此存储桶"
+ "Publicize this storage bucket, all registered users can upload files to this storage bucket; If not public, only administrators can upload files to this storage bucket": "公开此存储桶,所有注册用户都可以上传文件到此存储桶; 若不公开,只有管理员可以上传文件到此存储桶",
+ "Provider": "提供渠道",
+ "How to get the S3 credentials?": "如何获取 S3 授权配置?",
+ "Provider Unique Name": "渠道名称",
+ "Unique": "唯一",
+ "Add Provider": "添加渠道",
+ "{length} Buckets": "{length}个存储桶",
+ "Save Modifications": "保存修改"
}
}
diff --git a/prisma/migrations/20250714192022/migration.sql b/prisma/migrations/20250714192022/migration.sql
new file mode 100644
index 0000000..a4e29db
--- /dev/null
+++ b/prisma/migrations/20250714192022/migration.sql
@@ -0,0 +1,15 @@
+-- S3 配置
+INSERT INTO "system_configs"
+ (
+ "key",
+ "value",
+ "type",
+ "description"
+ )
+VALUES
+ (
+ 's3_config_list',
+ '[{"enabled":true,"platform":"cloudflare","channel":"r2","provider_name":"Cloudflare R2","account_id":"","access_key_id":"","secret_access_key":"","endpoint":"https://
.r2.cloudflarestorage.com","buckets":[{"bucket":"","prefix":"","file_types":"","region":"auto","custom_domain":"","file_size":"26214400","public":true}]},{"enabled":false,"platform":"tencent","channel":"cos","provider_name":"腾讯云 COS","endpoint":"","account_id":"","access_key_id":"","secret_access_key":"","buckets":[{"custom_domain":"","prefix":"","bucket":"","file_types":"","file_size":"26214400","region":"","public":true}]}]',
+ 'OBJECT',
+ 'R2 存储桶配置'
+);
\ No newline at end of file
diff --git a/public/sw.js.map b/public/sw.js.map
index 47b263b..b20412d 100644
--- a/public/sw.js.map
+++ b/public/sw.js.map
@@ -1 +1 @@
-{"version":3,"file":"sw.js","sources":["../../../../../../private/var/folders/9b/3qmyp8zd2xvdspdrp149fyg00000gn/T/61bb94bdeccd564ae30240a589143f64/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-routing@6.6.0/node_modules/workbox-routing/registerRoute.mjs';\nimport {NetworkFirst as workbox_strategies_NetworkFirst} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkFirst.mjs';\nimport {NetworkOnly as workbox_strategies_NetworkOnly} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkOnly.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-core@6.6.0/node_modules/workbox-core/clientsClaim.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\nimportScripts(\n \n);\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n\nworkbox_routing_registerRoute(\"/\", new workbox_strategies_NetworkFirst({ \"cacheName\":\"start-url\", plugins: [{ cacheWillUpdate: async ({ request, response, event, state }) => { if (response && response.type === 'opaqueredirect') { return new Response(response.body, { status: 200, statusText: 'OK', headers: response.headers }) } return response } }] }), 'GET');\nworkbox_routing_registerRoute(/.*/i, new workbox_strategies_NetworkOnly({ \"cacheName\":\"dev\", plugins: [] }), 'GET');\n\n\n\n\n"],"names":["importScripts","self","skipWaiting","workbox_core_clientsClaim","workbox_routing_registerRoute","workbox_strategies_NetworkFirst","plugins","cacheWillUpdate","request","response","event","state","type","Response","body","status","statusText","headers","workbox_strategies_NetworkOnly"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"sw.js","sources":["../../../../../../private/var/folders/9b/3qmyp8zd2xvdspdrp149fyg00000gn/T/666caf855e5c767f440b311388b39571/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-routing@6.6.0/node_modules/workbox-routing/registerRoute.mjs';\nimport {NetworkFirst as workbox_strategies_NetworkFirst} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkFirst.mjs';\nimport {NetworkOnly as workbox_strategies_NetworkOnly} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkOnly.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-core@6.6.0/node_modules/workbox-core/clientsClaim.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\nimportScripts(\n \n);\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n\nworkbox_routing_registerRoute(\"/\", new workbox_strategies_NetworkFirst({ \"cacheName\":\"start-url\", plugins: [{ cacheWillUpdate: async ({ request, response, event, state }) => { if (response && response.type === 'opaqueredirect') { return new Response(response.body, { status: 200, statusText: 'OK', headers: response.headers }) } return response } }] }), 'GET');\nworkbox_routing_registerRoute(/.*/i, new workbox_strategies_NetworkOnly({ \"cacheName\":\"dev\", plugins: [] }), 'GET');\n\n\n\n\n"],"names":["importScripts","self","skipWaiting","workbox_core_clientsClaim","workbox_routing_registerRoute","workbox_strategies_NetworkFirst","plugins","cacheWillUpdate","request","response","event","state","type","Response","body","status","statusText","headers","workbox_strategies_NetworkOnly"],"mappings":"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"}
\ No newline at end of file