diff --git a/README.md b/README.md index a1c0a3d..97d769d 100644 --- a/README.md +++ b/README.md @@ -195,4 +195,3 @@ See [How to Trigger Sync](https://wr.do/docs/developer/sync) for details. Star History Chart - diff --git a/app/(protected)/admin/system/app-configs.tsx b/app/(protected)/admin/system/app-configs.tsx index 8af4384..c208b46 100644 --- a/app/(protected)/admin/system/app-configs.tsx +++ b/app/(protected)/admin/system/app-configs.tsx @@ -88,7 +88,7 @@ export default function AppConfigs({}: {}) { return ( - +
{t("App Configs")}
diff --git a/app/(protected)/admin/system/domain-list.tsx b/app/(protected)/admin/system/domain-list.tsx index 06ac3c4..884709c 100644 --- a/app/(protected)/admin/system/domain-list.tsx +++ b/app/(protected)/admin/system/domain-list.tsx @@ -123,7 +123,7 @@ export default function DomainList({ user, action }: DomainListProps) { if (res.ok) { const data = await res.json(); if (data) { - toast.success("Successed!"); + toast.success("Saved"); handleRefresh(); } } else { diff --git a/app/(protected)/admin/system/s3-list.tsx b/app/(protected)/admin/system/s3-list.tsx index 8dd4962..f78f408 100644 --- a/app/(protected)/admin/system/s3-list.tsx +++ b/app/(protected)/admin/system/s3-list.tsx @@ -8,7 +8,7 @@ import { useTranslations } from "next-intl"; import { toast } from "sonner"; import useSWR from "swr"; -import { CloudStorageCredentials } from "@/lib/r2"; +import { BucketItem, CloudStorageCredentials } from "@/lib/r2"; import { cn, fetcher } from "@/lib/utils"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -20,6 +20,13 @@ import { } from "@/components/ui/collapsible"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; import { Switch } from "@/components/ui/switch"; import { @@ -33,30 +40,7 @@ import { Icons } from "@/components/shared/icons"; export default function S3Configs({}: {}) { const t = useTranslations("Setting"); const [isPending, startTransition] = useTransition(); - const [isCheckingR2Config, startCheckR2Transition] = useTransition(); - const [isCheckingCOSConfig, startCheckCOSTransition] = useTransition(); - const [isCheckedR2Config, setIsCheckedR2Config] = useState(false); - const [isCheckedCOSConfig, setIsCheckedCOSConfig] = useState(false); - const [r2Credentials, setR2Credentials] = useState({ - platform: "cloudflare", - channel: "r2", - provider_name: "Cloudflare R2", - account_id: "", - access_key_id: "", - secret_access_key: "", - endpoint: "", - enabled: true, - buckets: [ - { - bucket: "", - custom_domain: "", - prefix: "", - file_types: "", - region: "auto", - public: true, - }, - ], - }); + const [s3Configs, setS3Configs] = useState([]); const { data: configs, @@ -64,13 +48,40 @@ export default function S3Configs({}: {}) { mutate, } = useSWR>("/api/admin/s3", fetcher); + const S3_PRVIDERS = [ + { + label: "Cloudflare R2", + value: "Cloudflare R2", + platform: "cloudflare", + channel: "r2", + }, + { label: "AWS S3", value: "AWS S3", platform: "aws", channel: "s3" }, + { + label: "Tencent COS", + value: "Tencent COS", + platform: "tencent", + channel: "cos", + }, + { label: "Ali OSS", value: "Ali OSS", platform: "ali", channel: "oss" }, + { label: "Minio", value: "Minio", platform: "minio", channel: "s3" }, + ]; + useEffect(() => { - if (configs) { - setR2Credentials(configs.s3_config_01); + if (configs && configs?.s3_config_list) { + setS3Configs(configs.s3_config_list); } }, [configs]); + function isProviderNameUnique(array: CloudStorageCredentials[]): boolean { + const names = array.map((item) => item.provider_name); + return new Set(names).size === names.length; + } + const handleSaveConfigs = (value: any, key: string, type: string) => { + if (!isProviderNameUnique(s3Configs)) { + toast.error("Provider name must be unique"); + return; + } startTransition(async () => { const res = await fetch("/api/admin/s3", { method: "POST", @@ -87,43 +98,15 @@ export default function S3Configs({}: {}) { }); }; - const handleR2CheckAccess = async (event) => { - event.preventDefault(); - event.stopPropagation(); - toast.success("Checking"); - }; - - const handleCOSCheckAccess = async (event) => { - event.preventDefault(); - event.stopPropagation(); - toast.success("Checking"); - }; - const canSaveR2Credentials = useMemo(() => { if (!configs) return true; - return Object.keys(r2Credentials).some( - (key) => r2Credentials[key] !== configs.s3_config_01[key], + return ( + Object.keys(s3Configs).some( + (key) => s3Configs[key] !== configs.s3_config_list[key], + ) || configs.s3_config_list.length !== s3Configs.length ); - }, [r2Credentials, configs]); - - const ReadyBadge = ( - isChecked: boolean, - isChecking: boolean, - type: string, - ) => ( - - type === "r2" ? handleR2CheckAccess(event) : handleCOSCheckAccess(event) - } - > - {isChecking && } - {isChecked && !isChecking && } - {isChecked ? t("Verified") : t("Verify Configuration")} - - ); + }, [s3Configs, configs]); if (isLoading) { return ; @@ -131,341 +114,511 @@ export default function S3Configs({}: {}) { return ( - - -
{t("Cloud Storage Configs")}
- - + + +

+ {t("Cloud Storage Configs")} +

+ {canSaveR2Credentials && ( + + )} +

{ + e.preventDefault(); + setS3Configs([ + ...s3Configs, + { + platform: "cloudflare", + channel: "s3", + provider_name: `Cloudflare R2 (${s3Configs.length + 1})`, + account_id: "", + access_key_id: "", + secret_access_key: "", + endpoint: "", + enabled: true, + buckets: [ + { + bucket: "", + custom_domain: "", + prefix: "", + file_types: "", + region: "auto", + public: true, + }, + ], + }, + ]); + }} + > + + {t("Add Provider")} +

+
- - -
Cloudflare R2
- {ReadyBadge(isCheckedR2Config, isCheckingR2Config, "r2")} - -
- -
-
- - - setR2Credentials({ - ...r2Credentials, - endpoint: e.target.value, - }) - } - /> -
-
- - - setR2Credentials({ - ...r2Credentials, - access_key_id: e.target.value, - }) - } - /> -
-
- - - setR2Credentials({ - ...r2Credentials, - secret_access_key: e.target.value, - }) - } - /> -
-
- - - setR2Credentials({ - ...r2Credentials, - enabled: e, - }) - } - /> -
-
- {r2Credentials.buckets.map((bucket, index) => ( - -

- {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 && ( - - {index !== 0 && ( - - )} -
+

+ {t("Bucket")} {index2 + 1} +

-
- - { - const newBuckets = [...r2Credentials.buckets]; - newBuckets[index] = { - ...bucket, - bucket: e.target.value, - }; - setR2Credentials({ - ...r2Credentials, - buckets: newBuckets, - }); + {/* 按钮部分 */} +
+ {index2 > 0 && ( + + )} + {index2 < config.buckets.length - 1 && ( + + )} + + {index2 !== 0 && ( + + )} +
+ + {/* 使用 updateBucket 函数的输入字段 */} +
+ + + updateBucket(index2, { bucket: e.target.value }) + } + /> +
+ +
+ + + updateBucket(index2, { + custom_domain: e.target.value, + }) + } + /> +
+ +
+ + + updateBucket(index2, { region: e.target.value }) + } + /> +
+ +
+ + + updateBucket(index2, { prefix: e.target.value }) + } + /> +
+ +
+
+ + + + + + + + {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?")} + + {/*
-
- - { - const newBuckets = [...r2Credentials.buckets]; - newBuckets[index] = { - ...bucket, - custom_domain: e.target.value, - }; - setR2Credentials({ - ...r2Credentials, - buckets: newBuckets, - }); + > + {t("Clear")} + */} +
-
- - { - const newBuckets = [...r2Credentials.buckets]; - newBuckets[index] = { - ...bucket, - region: e.target.value, - }; - setR2Credentials({ - ...r2Credentials, - buckets: newBuckets, - }); - }} - /> -
-
- - { - const newBuckets = [...r2Credentials.buckets]; - newBuckets[index] = { - ...bucket, - prefix: e.target.value, - }; - setR2Credentials({ - ...r2Credentials, - buckets: newBuckets, - }); - }} - /> -
-
-
- - - - - - - - {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; - }), - }) - } - /> -
- {/*
- - { - const newBuckets = [...r2Credentials.buckets]; - newBuckets[index] = { - ...bucket, - file_types: e.target.value, - }; - setR2Credentials({ - ...r2Credentials, - buckets: newBuckets, - }); - }} - /> -
*/} - - ))} -
- - {t("How to get the R2 credentials?")} - - - -
- - + + + ); + })} 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)})`} + {`![${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 && ( ) )} {/* 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgBAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAa,EAEZ,CAAA;EAQDC,CAAI,CAAA,CAAA,CAAA,CAACC,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAA;AAElBC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAyB,EAAE,CAAA;AAI3BC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAA6B,CAAC,CAAA,CAAA,CAAG,CAAE,CAAA,CAAA,CAAA,CAAA,CAAIC,oBAA+B,CAAC,CAAA;EAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,EAAC,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;EAAEC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAO,EAAE,CAAC,CAAA;GAAEC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAe,EAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;QAAEC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;QAAEC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;QAAEC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AAAEC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA;AAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;EAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAIF,QAAQ,CAAIA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAQ,CAACG,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,gBAAgB,CAAE,CAAA,CAAA;AAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,OAAO,CAAIC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAQ,CAACJ,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAACK,IAAI,CAAE,CAAA,CAAA;EAAEC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAM,EAAE,CAAG,CAAA,CAAA,CAAA;EAAEC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAU,EAAE,CAAI,CAAA,CAAA,CAAA,CAAA;YAAEC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAER,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAACQ,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA;AAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAA;EAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;EAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAOR,QAAQ,CAAA;EAAC,CAAA,CAAA,CAAA,CAAA,CAAA;KAAG,CAAA;AAAE,CAAA,CAAA,CAAC,CAAC,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAA;AACxWL,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAA6B,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAIc,mBAA8B,CAAC,CAAA;EAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,EAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;EAAEZ,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAO,EAAE,CAAA,CAAA;EAAG,CAAC,CAAC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA;;"} \ No newline at end of file