diff --git a/app/(protected)/admin/storage/loading.tsx b/app/(protected)/admin/storage/loading.tsx index 1eab0dd..835c8cf 100644 --- a/app/(protected)/admin/storage/loading.tsx +++ b/app/(protected)/admin/storage/loading.tsx @@ -5,8 +5,8 @@ export default function DashboardRecordsLoading() { return ( <> diff --git a/app/(protected)/admin/system/s3-list.tsx b/app/(protected)/admin/system/s3-list.tsx index f78f408..3d14090 100644 --- a/app/(protected)/admin/system/s3-list.tsx +++ b/app/(protected)/admin/system/s3-list.tsx @@ -3,7 +3,6 @@ import { useEffect, useMemo, useState, useTransition } from "react"; import Link from "next/link"; import { motion } from "framer-motion"; -import { CloudCog } from "lucide-react"; import { useTranslations } from "next-intl"; import { toast } from "sonner"; import useSWR from "swr"; @@ -63,7 +62,12 @@ export default function S3Configs({}: {}) { channel: "cos", }, { label: "Ali OSS", value: "Ali OSS", platform: "ali", channel: "oss" }, - { label: "Minio", value: "Minio", platform: "minio", channel: "s3" }, + { + label: "Custom Provider", + value: "Custom Provider", + platform: "custom", + channel: "cp", + }, ]; useEffect(() => { @@ -442,6 +446,7 @@ export default function S3Configs({}: {}) { region: "auto", custom_domain: "", file_size: "26214400", + max_storage: "", public: true, }); setS3Configs( @@ -534,6 +539,38 @@ export default function S3Configs({}: {}) { /> +
+
+ + + + + + + + {t("maxStorageTooltip")} + + + +
+
+ + updateBucket(index2, { max_storage: e.target.value }) + } + /> + {bucket.max_storage && ( + + ≈{(Number(bucket.max_storage) / (1024 * 1024 * 1024)).toFixed(1)}GB + + )} +
+
+
@@ -588,6 +625,7 @@ export default function S3Configs({}: {}) { region: "auto", custom_domain: "", file_size: "26214400", + max_storage: "", public: true, }, ], diff --git a/app/(protected)/dashboard/storage/loading.tsx b/app/(protected)/dashboard/storage/loading.tsx index 1eab0dd..835c8cf 100644 --- a/app/(protected)/dashboard/storage/loading.tsx +++ b/app/(protected)/dashboard/storage/loading.tsx @@ -5,8 +5,8 @@ export default function DashboardRecordsLoading() { return ( <> diff --git a/app/api/storage/admin/s3/files/configs/route.ts b/app/api/storage/admin/s3/files/configs/route.ts index 84e1353..5ac2bed 100644 --- a/app/api/storage/admin/s3/files/configs/route.ts +++ b/app/api/storage/admin/s3/files/configs/route.ts @@ -33,12 +33,6 @@ export async function GET(req: NextRequest) { })) .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); diff --git a/app/api/storage/bucket-usage/route.ts b/app/api/storage/bucket-usage/route.ts new file mode 100644 index 0000000..2996f6c --- /dev/null +++ b/app/api/storage/bucket-usage/route.ts @@ -0,0 +1,72 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { getBucketStorageUsage } from "@/lib/dto/files"; +import { getMultipleConfigs } from "@/lib/dto/system-config"; +import { checkUserStatus } from "@/lib/dto/user"; +import { getCurrentUser } from "@/lib/session"; + +export async function GET(request: NextRequest) { + try { + const user = checkUserStatus(await getCurrentUser()); + if (user instanceof Response) return user; + + const url = new URL(request.url); + const bucket = url.searchParams.get("bucket"); + const provider = url.searchParams.get("provider"); + + if (!bucket || !provider) { + return NextResponse.json("Missing bucket or provider parameters", { + status: 400, + }); + } + + // 获取存储桶配置 + const configs = await getMultipleConfigs(["s3_config_list"]); + if (!configs || !configs.s3_config_list) { + return NextResponse.json("Invalid S3 configs", { + status: 400, + }); + } + + const providerConfig = configs.s3_config_list.find( + (c) => c.provider_name === provider, + ); + if (!providerConfig) { + return NextResponse.json("Provider does not exist", { + status: 400, + }); + } + + const bucketConfig = providerConfig.buckets?.find( + (b) => b.bucket === bucket, + ); + if (!bucketConfig) { + return NextResponse.json("Bucket does not exist", { + status: 400, + }); + } + + // 获取存储桶使用情况 + const bucketUsage = await getBucketStorageUsage(bucket, provider); + if (!bucketUsage.success) { + return NextResponse.json("Failed to get bucket usage", { + status: 500, + }); + } + + return NextResponse.json({ + bucket: bucket, + provider: provider, + usage: { + totalSize: bucketUsage.data.totalSize, + totalFiles: bucketUsage.data.totalFiles, + }, + limits: { + maxStorage: bucketConfig.max_storage ? Number(bucketConfig.max_storage) : null, + }, + }); + } catch (error) { + console.error("Error getting bucket usage:", error); + return NextResponse.json({ error: "Server Error" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/storage/s3/files/route.ts b/app/api/storage/s3/files/route.ts index 6408b67..0c4022d 100644 --- a/app/api/storage/s3/files/route.ts +++ b/app/api/storage/s3/files/route.ts @@ -162,6 +162,7 @@ export async function DELETE(request: NextRequest) { await softDeleteUserFiles(ids); return NextResponse.json({ message: "File deleted successfully" }); } catch (error) { + console.error("Error deleting file:", error); return NextResponse.json("Error deleting file", { status: 500 }); } } diff --git a/app/api/storage/s3/upload/route.ts b/app/api/storage/s3/upload/route.ts index 7846743..6525479 100644 --- a/app/api/storage/s3/upload/route.ts +++ b/app/api/storage/s3/upload/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { getBucketStorageUsage } from "@/lib/dto/files"; import { getPlanQuota } from "@/lib/dto/plan"; import { getMultipleConfigs } from "@/lib/dto/system-config"; import { checkUserStatus } from "@/lib/dto/user"; @@ -60,6 +61,27 @@ export async function POST(request: NextRequest) { }); if (limit) return Response.json(limit.statusText, { status: limit.status }); + // 检查存储桶容量限制 + const bucketConfig = buckets.find((b) => b.bucket === bucket); + const totalUploadSize = files.reduce((sum, file) => sum + Number(file.size), 0); + + if (bucketConfig?.max_storage) { + const bucketUsage = await getBucketStorageUsage(bucket, provider); + if (bucketUsage.success && bucketUsage.data) { + const currentUsage = bucketUsage.data.totalSize; + const maxStorage = Number(bucketConfig.max_storage); + + if (currentUsage + totalUploadSize > maxStorage) { + const remainingSpace = maxStorage - currentUsage; + const remainingSpaceGB = (remainingSpace / (1024 * 1024 * 1024)).toFixed(2); + return Response.json( + `存储桶容量不足!剩余 ${remainingSpaceGB}GB,请更换存储桶`, + { status: 403 } + ); + } + } + } + const R2 = createS3Client( providerChannel.endpoint, providerChannel.access_key_id, diff --git a/app/layout.tsx b/app/layout.tsx index 5e3e093..d2a2fcc 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -43,7 +43,7 @@ export default async function RootLayout({ children }: RootLayoutProps) { disableTransitionOnChange > {children} - + diff --git a/components/file/file-list.tsx b/components/file/file-list.tsx index 643ca1d..d537a72 100644 --- a/components/file/file-list.tsx +++ b/components/file/file-list.tsx @@ -240,26 +240,28 @@ export default function UserFileList({
-
- -

- {`${file.name}${getFileUrl(file.path)}`} -

- ${getFileUrl(file.path)}`} - /> -

- {`![${file.name}](${getFileUrl(file.path)})`} + {`[${file.name}](${getFileUrl(file.path)})`}

+ {file.mimeType.startsWith("image/") && ( +
+ +

+ {`${file.name}${getFileUrl(file.path)}`} +

+ ${getFileUrl(file.path)}`} + /> +
+ )} ); @@ -355,10 +357,10 @@ export default function UserFileList({ +

{file.user.name}

{file.user.email}

- +
} > {file.user.name ?? file.user.email} diff --git a/components/file/index.tsx b/components/file/index.tsx index d5e0521..8702f39 100644 --- a/components/file/index.tsx +++ b/components/file/index.tsx @@ -124,6 +124,17 @@ export default function UserFileManager({ user, action }: FileListProps) { fetcher, ); + const { data: bucketUsage } = useSWR( + currentBucketInfo.bucket && currentBucketInfo.provider_name + ? `/api/storage/bucket-usage?bucket=${currentBucketInfo.bucket}&provider=${currentBucketInfo.provider_name}` + : null, + fetcher, + { + revalidateOnFocus: false, + refreshInterval: 30000, // 每30秒更新一次 + }, + ); + useEffect(() => { if (s3Configs && s3Configs.length > 0) { setCurrentProvider(s3Configs[0]); @@ -261,11 +272,22 @@ export default function UserFileManager({ user, action }: FileListProps) { - +
} > - + )} {/* Bucket Select */} @@ -288,7 +310,7 @@ export default function UserFileManager({ user, action }: FileListProps) { - {s3Configs.map((provider) => ( + {s3Configs.map((provider, index) => ( {provider.provider_name} {provider.buckets?.map((item) => ( @@ -299,7 +321,7 @@ export default function UserFileManager({ user, action }: FileListProps) { {item.bucket} ))} - + {index !== s3Configs.length - 1 && } ))} @@ -415,25 +437,28 @@ export default function UserFileManager({ user, action }: FileListProps) { )} - {!isLoading && !error && !s3Configs && !currentBucketInfo && ( - - - - {t("No buckets found")} - - - {t( - "The administrator has not configured the storage bucket, no file can be uploaded", - )} - - - )} + {!isLoading && + !error && + !s3Configs?.length && + !currentBucketInfo.bucket && ( + + + + {t("No buckets found")} + + + {t( + "The administrator has not configured the storage bucket, no file can be uploaded", + )} + + + )} {!isLoading && !error && s3Configs && s3Configs.length > 0 && - currentBucketInfo && ( + currentBucketInfo.bucket && ( 0 ? Math.min((totalSize / maxSize) * 100, 100) : 0; + // 存储桶级别的配额信息 + const bucketTotalSize = bucketUsage?.usage?.totalSize || 0; + const bucketMaxSize = bucketUsage?.limits?.maxStorage || 0; + const bucketUsagePercentage = + bucketMaxSize > 0 ? Math.min((bucketTotalSize / bucketMaxSize) * 100, 100) : 0; + const hasBucketLimit = bucketMaxSize > 0; + const getStatusColor = (percentage) => { if (percentage >= 90) return "text-red-600"; if (percentage >= 70) return "text-yellow-600"; @@ -55,8 +62,11 @@ export function FileSizeDisplay({ files, plan, t }) { - {/* 详细信息 */} + {/* Plan级别详细信息 */}
+
+ {t("planQuota")} +
{t("usedSpace")}: @@ -83,34 +93,103 @@ export function FileSizeDisplay({ files, plan, t }) {
+ {/* 存储桶级别信息 */} + {hasBucketLimit && ( + <> +
+
+ + + {t("bucketQuota")} - {bucketInfo?.bucket} + +
+
+ + {t("used")} + + + {bucketUsagePercentage.toFixed(1)}% + +
+
+
+
+
+
+ + {t("usedSpace")}: + + + {formatFileSize(bucketTotalSize, { precision: 2 })} + +
+
+ + {t("bucketCapacity")}: + + + {formatFileSize(bucketMaxSize, { precision: 2 })} + +
+
+ + {t("availableSpace")}: + + + {formatFileSize(bucketMaxSize - bucketTotalSize, { precision: 2 })} + +
+
+
+ + )} + {/* 状态提示 */}
usagePercentage ? bucketUsagePercentage : usagePercentage)}`} > - {getStatusIcon(usagePercentage)} + {getStatusIcon(hasBucketLimit && bucketUsagePercentage > usagePercentage ? bucketUsagePercentage : usagePercentage)} - {usagePercentage >= 90 - ? t("storageFull") - : usagePercentage >= 70 - ? t("storageHigh") - : t("storageGood")} + {(() => { + const criticalPercentage = hasBucketLimit && bucketUsagePercentage > usagePercentage ? bucketUsagePercentage : usagePercentage; + if (criticalPercentage >= 90) { + return hasBucketLimit && bucketUsagePercentage >= 90 ? t("bucketStorageFull") : t("storageFull"); + } else if (criticalPercentage >= 70) { + return hasBucketLimit && bucketUsagePercentage >= 70 ? t("bucketStorageHigh") : t("storageHigh"); + } else { + return t("storageGood"); + } + })()}
); } -export function CircularStorageIndicator({ files, plan, size = 32 }) { +export function CircularStorageIndicator({ files, plan, bucketUsage, size = 32 }) { const totalSize = files?.totalSize || 0; const maxSize = Number(plan?.stMaxTotalSize || 0); const usagePercentage = maxSize > 0 ? Math.min((totalSize / maxSize) * 100, 100) : 0; + // 存储桶级别的配额信息 + const bucketTotalSize = bucketUsage?.usage?.totalSize || 0; + const bucketMaxSize = bucketUsage?.limits?.maxStorage || 0; + const bucketUsagePercentage = + bucketMaxSize > 0 ? Math.min((bucketTotalSize / bucketMaxSize) * 100, 100) : 0; + const hasBucketLimit = bucketMaxSize > 0; + + // 使用更严格的限制来显示 + const displayPercentage = hasBucketLimit && bucketUsagePercentage > usagePercentage ? bucketUsagePercentage : usagePercentage; + // 圆形参数 const radius = (size - 6) / 2; const circumference = 2 * Math.PI * radius; const strokeDashoffset = - circumference - (usagePercentage / 100) * circumference; + circumference - (displayPercentage / 100) * circumference; // 根据使用率确定颜色 const getColor = (percentage) => { @@ -139,7 +218,7 @@ export function CircularStorageIndicator({ files, plan, size = 32 }) { cx={size / 2} cy={size / 2} r={radius} - stroke={getColor(usagePercentage)} + stroke={getColor(displayPercentage)} strokeWidth="3" fill="none" strokeLinecap="round" @@ -150,9 +229,9 @@ export function CircularStorageIndicator({ files, plan, size = 32 }) {
- {Math.round(usagePercentage)}% + {Math.round(displayPercentage)}%
); diff --git a/components/file/upload.tsx b/components/file/upload.tsx index ac977a6..77f355f 100644 --- a/components/file/upload.tsx +++ b/components/file/upload.tsx @@ -257,27 +257,34 @@ export const FileUploader = ({ ) : ( (file.status === "error" || file.status === "cancelled") && ( -
-
-
- {t("Aborted")} +
+
+
+
+ {file.status === "cancelled" ? t("Aborted") : t("Failed")} +
+ +
- - + {file.status === "error" && file.error && ( +
+ {file.error} +
+ )}
) )} diff --git a/content/docs/developer/cloud-storag-zh.mdx b/content/docs/developer/cloud-storag-zh.mdx new file mode 100644 index 0000000..625bbe1 --- /dev/null +++ b/content/docs/developer/cloud-storag-zh.mdx @@ -0,0 +1,259 @@ +--- +title: 云存储配置 +description: 如何配置多平台云存储 +--- + + + +## 概览 + +管理员可以在 `/admin/system` 管理 S3 配置,包括添加、删除和修改云存储的 S3 配置。 + +WR.DO 目前支持多种云存储提供商: + +- Cloudflare R2 +- AWS S3 +- 腾讯云 COS +- 阿里云 OSS +- 自定义提供商 (兼容 S3 API) + +> 一个提供商可以添加多个存储桶 + +## Cloudflare R2 + +### 1. 创建 R2 存储桶 + +1. 登录您的 [Cloudflare 仪表板](https://dash.cloudflare.com/) +2. 从左侧边栏导航到 **R2 对象存储** +3. 点击 **创建存储桶** +4. 输入您的存储桶名称(例如:`wrdo`) +5. 选择位置(建议选择自动) +6. 点击 **创建存储桶** + +### 2. 获取 API 凭证 + +1. 在您的 Cloudflare 仪表板中,前往 **我的个人资料** > **API 令牌** +2. 点击 **创建令牌** +3. 使用 **R2 令牌** 模板或创建自定义令牌,包含: + - **权限**: `R2:Edit` + - **账户资源**: 包含您的账户 + - **区域资源**: 包含所有区域(如果需要) +4. 点击 **继续摘要**,然后 **创建令牌** +5. 复制并保存令牌(这是您的 **访问密钥 ID** 和 **秘密访问密钥**) + +### 3. 获取账户 ID + +1. 在您的 Cloudflare 仪表板中,前往右侧边栏 +2. 复制您的 **账户 ID** + +### 4. 获取公共 URL + +1. 在您的 Cloudflare 仪表板中,前往 **R2 对象存储** > **存储桶详情** > **公共开发 URL** + +如果您已配置自定义域名,请使用该域名。 + +### 5. 配置 CORS + +1. 在您的 Cloudflare 仪表板中,前往 **R2 对象存储** > **存储桶设置** -> **CORS 策略** + +填入以下内容: + +```json +[ + { + "AllowedOrigins": [ + "http://localhost:3000", + "https://wr.do" // 替换为您的域名 + ], + "AllowedMethods": [ + "GET", + "PUT", + "POST", + "DELETE", + "HEAD" + ], + "AllowedHeaders": [ + "*" + ], + "ExposeHeaders": [ + "ETag" + ], + "MaxAgeSeconds": 3600 + } +] +``` + +### 6. 在 WR.DO 中配置 + +访问 `localhost:3000/admin/system`,填写配置表单: + +- **提供渠道**: cloudflare (r2) +- **渠道名称**: Cloudflare R2(或任何自定义名称) +- **S3 端点**: `https://<账户ID>.r2.cloudflarestorage.com`(替换为您的账户端点) +- **访问密钥 ID**: 第 2 步中的 API 令牌 +- **机密访问密钥**: 第 2 步中的 API 令牌 +- **启用**: 开启 +- **存储桶名称**: 您的存储桶名称 +- **公开域名或自定义域名**: 参考第 4 步 +- **存储桶区域**: auto +- **前缀**: 可选 +- **公开**: 如果您想要公共访问,请启用 + +## 腾讯云 COS + +### 1. 创建 COS 存储桶 + +1. 登录 [腾讯云控制台](https://console.cloud.tencent.com/cos) +2. 点击 **存储桶列表** > **创建存储桶** +3. 输入存储桶名称(例如:`wrdo-1303456836`) +4. 选择地域(例如:`ap-chengdu`) +5. 配置访问权限 +6. 点击 **创建** + +### 2. 获取 API 密钥 + +1. 前往 [CAM 控制台](https://console.cloud.tencent.com/cam/capi) +2. 点击 **新建密钥** 或使用现有密钥 +3. 保存您的 **SecretId** 和 **SecretKey** + +### 3. CORS 设置 + +1. 前往 [COS 控制台](https://console.cloud.tencent.com/cos) +2. 点击 **存储桶列表** > **选择存储桶** -> 安全管理 -> 跨域访问 CORS 设置 +3. 填入下列规则: + +![](/_static/docs/cos-cors.png) + +### 4. 在 WR.DO 中配置 + +填写配置表单: + +- **提供渠道**: tencent (cos) +- **渠道名称**: 腾讯云 COS(或任何自定义名称) +- **S3 端点**: `https://cos.ap-chengdu.myqcloud.com`(替换为您的地域) +- **访问密钥 ID**: 您的 SecretId +- **机密访问密钥**: 您的 SecretKey +- **启用**: 开启 +- **存储桶名称**: 您的存储桶名称(例如:`wrdo-1303456836`) +- **公开域名或自定义域名**: `https://wrdo-1303456836.cos.ap-chengdu.myqcloud.com`(您的存储桶公共 URL) +- **存储桶区域**: 您的 COS 地域(例如:`ap-chengdu`) +- **前缀**: 可选日期前缀 +- **公开**: 如果您想要公共访问,请启用 + +## 阿里云 OSS + +### 1. 创建 OSS 存储桶 + +1. 登录 [阿里云控制台](https://oss.console.aliyun.com/) +2. 点击 **创建 Bucket** +3. 输入存储桶名称 +4. 选择地域(例如:`oss-cn-hangzhou`) +5. 配置 ACL 和其他设置 +6. 点击 **确定** + +### 2. 获取 AccessKey + +1. 前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) +2. 点击 **创建 AccessKey** 或使用现有密钥 +3. 保存您的 **AccessKeyId** 和 **AccessKeySecret** + +### 3. 在 WR.DO 中配置 + +填写配置表单: + +- **提供渠道**: ali (oss) +- **渠道名称**: 阿里云 OSS(或任何自定义名称) +- **S3 端点**: `https://oss-cn-hangzhou.aliyuncs.com`(替换为您的地域) +- **访问密钥 ID**: 您的 AccessKeyId +- **机密访问密钥**: 您的 AccessKeySecret +- **启用**: 开启 +- **存储桶名称**: 您的存储桶名称 +- **公开域名或自定义域名**: `https://your-bucket.oss-cn-hangzhou.aliyuncs.com`(您的存储桶公共 URL) +- **存储桶区域**: 您的 OSS 地域(例如:`oss-cn-hangzhou`) +- **前缀**: 可选日期前缀 +- **公开**: 如果您想要公共访问,请启用 + +## AWS S3 + +### 1. 创建 S3 存储桶 + +1. 登录 [AWS 控制台](https://console.aws.amazon.com/s3/) +2. 点击 **创建存储桶** +3. 输入存储桶名称(全球唯一) +4. 选择 AWS 区域 +5. 根据需要配置存储桶设置 +6. 点击 **创建存储桶** + +### 2. 创建 IAM 用户 + +1. 前往 [IAM 控制台](https://console.aws.amazon.com/iam/) +2. 点击 **用户** > **添加用户** +3. 输入用户名并选择 **编程访问** +4. 附加现有策略或创建具有 S3 权限的自定义策略: + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject", + "s3:ListBucket" + ], + "Resource": [ + "arn:aws:s3:::your-bucket-name", + "arn:aws:s3:::your-bucket-name/*" + ] + } + ] + } + ``` +5. 完成用户创建并保存 **访问密钥 ID** 和 **秘密访问密钥** + +### 3. 在 WR.DO 中配置 + +填写配置表单: + +- **提供渠道**: aws (s3) +- **渠道名称**: AWS S3(或任何自定义名称) +- **S3 端点**: `https://s3.amazonaws.com`(或特定区域端点) +- **访问密钥 ID**: 您的 IAM 用户访问密钥 ID +- **机密访问密钥**: 您的 IAM 用户秘密访问密钥 +- **启用**: 开启 +- **存储桶名称**: 您的 S3 存储桶名称 +- **公开域名或自定义域名**: 您的存储桶公共 URL 或 CloudFront 分发 +- **存储桶区域**: 您的 S3 区域(例如:`us-east-1`) +- **前缀**: 可选日期前缀 +- **公开**: 如果您想要公共访问,请启用 + +## 通用配置选项 + +### 前缀设置 +- 使用基于日期的前缀(例如:`2025/08/08`)按日期组织文件 +- 如果您喜欢扁平文件结构,请留空 + +### 公共访问 +- 如果您希望文件可以通过直接 URL 访问,请启用 **公开** +- 对于私有文件存储,请禁用 + +### 自定义域名 +- 配置自定义域名以获得更好的品牌体验 +- 确保为您的域名进行正确的 DNS 配置 + +## 故障排除 + +### 常见问题 + +1. **访问被拒绝**: 检查您的 API 凭证和权限 +2. **存储桶未找到**: 验证存储桶名称和区域设置 +3. **CORS 问题**: 如果需要,在您的存储桶中配置 CORS 设置 +4. **端点错误**: 确保您的提供商端点格式正确 + +### 测试配置 + +保存配置后,您可以通过以下方式测试: +1. 通过管理界面上传测试文件 +2. 检查文件是否出现在您的云存储桶中 +3. 验证公共访问(如果启用)是否可以通过文件 URL 访问 \ No newline at end of file diff --git a/content/docs/developer/cloud-storage.mdx b/content/docs/developer/cloud-storage.mdx new file mode 100644 index 0000000..0f579b9 --- /dev/null +++ b/content/docs/developer/cloud-storage.mdx @@ -0,0 +1,262 @@ +--- +title: Cloud Storage +description: How to config the cloud storage api +--- + + + +## Overview + +Administrators can manage s3 configurations at `/admin/system`, +including adding, deleting, and modifying s3 configurations for cloud storage. + +WR.DO now supports multiple cloud storage providers: + +- Cloudflare R2 +- AWS S3 +- Tencent COS +- Ali OSS +- Custom Provider (Support any S3 compatible provider) + +> One provider can configure multiple buckets. + +## Cloudflare R2 + +### 1. Create R2 Bucket + +1. Log in to your [Cloudflare dashboard](https://dash.cloudflare.com/) +2. Navigate to **R2 Object Storage** from the left sidebar +3. Click **Create bucket** +4. Enter your bucket name (e.g., `wrdo`) +5. Select the location (auto is recommended) +6. Click **Create bucket** + +### 2. Get API Credentials + +1. In your Cloudflare dashboard, go to **My Profile** > **API Tokens** +2. Click **Create Token** +3. Use the **R2 Token** template or create a custom token with: + - **Permissions**: `R2:Edit` + - **Account Resources**: Include your account + - **Zone Resources**: Include all zones (if needed) +4. Click **Continue to summary** and then **Create Token** +5. Copy and save the token (this is your **Access Key ID** and **Secret Access Key**) + +### 3. Get Account ID + +1. In your Cloudflare dashboard, go to the right sidebar +2. Copy your **Account ID** + +### 4. Get Public URL + +1. In your Cloudflare dashboard, go to **R2 Object Storage** > **Bucket Details** > **Public Development URL** + +if you have configured a custom domain, use that instead. + +### 5. Config CORS + +1. In your Cloudflare dashboard, go to **R2 Object Storage** > **Bucket Settings** -> **CORS Policy** + +Fill in the following: + +```json +[ + { + "AllowedOrigins": [ + "http://localhost:3000", + "https://wr.do" // Replace with your domain + ], + "AllowedMethods": [ + "GET", + "PUT", + "POST", + "DELETE", + "HEAD" + ], + "AllowedHeaders": [ + "*" + ], + "ExposeHeaders": [ + "ETag" + ], + "MaxAgeSeconds": 3600 + } +] +``` + +### 6. Configuration in WR.DO + +Follow `localhost:3000/admin/system`, fill in the configuration form with: + +- **Provider**: cloudflare (r2) +- **Channel Name**: Cloudflare R2 (or any custom name) +- **S3 Endpoint**: `https://.r2.cloudflarestorage.com` (replace with your account's endpoint) +- **Access Key ID**: Your API token from step 2 +- **Secret Access Key**: Your API token from step 2 +- **Enable**: Toggle ON +- **Bucket Name**: Your bucket name +- **Public Domain**: follow step 4 +- **Storage Region**: auto +- **Prefix**: Optional +- **Public**: Enable if you want public access + + +## Tencent COS + +### 1. Create COS Bucket + +1. Log in to [Tencent Cloud Console](https://console.cloud.tencent.com/cos) +2. Click **Bucket List** > **Create Bucket** +3. Enter bucket name (e.g., `wrdo-1303456836`) +4. Select region (e.g., `ap-chengdu`) +5. Configure access permissions +6. Click **Create** + +### 2. Get API Keys + +1. Go to [CAM Console](https://console.cloud.tencent.com/cam/capi) +2. Click **Create Key** or use existing keys +3. Save your **SecretId** and **SecretKey** + +### 3. CORS 设置 + +1. Follow [COS Console](https://console.cloud.tencent.com/cos) +2. Click **Bucket List** > **Select a Bucket** -> 安全管理 -> 跨域访问 CORS 设置 +3. Fill in the following rules: + +![](/_static/docs/cos-cors.png) + +### 4. Configuration in WR.DO + +Fill in the configuration form with: + +- **Provider**: tencent (cos) +- **Channel Name**: 腾讯云 COS (or any custom name) +- **S3 Endpoint**: `https://cos.ap-chengdu.myqcloud.com` (replace with your region) +- **Access Key ID**: Your SecretId +- **Secret Access Key**: Your SecretKey +- **Enable**: Toggle ON +- **Bucket Name**: Your bucket name (e.g., `wrdo-1303456836`) +- **Public Domain**: `https://wrdo-1303456836.cos.ap-chengdu.myqcloud.com` (your bucket's public URL) +- **Storage Region**: Your COS region (e.g., `ap-chengdu`) +- **Prefix**: Optional date prefix +- **Public**: Enable if you want public access + +## Ali OSS + +### 1. Create OSS Bucket + +1. Log in to [Alibaba Cloud Console](https://oss.console.aliyun.com/) +2. Click **Create Bucket** +3. Enter bucket name +4. Select region (e.g., `oss-cn-hangzhou`) +5. Configure ACL and other settings +6. Click **OK** + +### 2. Get AccessKey + +1. Go to [RAM Console](https://ram.console.aliyun.com/manage/ak) +2. Click **Create AccessKey** or use existing keys +3. Save your **AccessKeyId** and **AccessKeySecret** + +### 3. Configuration in WR.DO + +Fill in the configuration form with: + +- **Provider**: ali (oss) +- **Channel Name**: 阿里云 OSS (or any custom name) +- **S3 Endpoint**: `https://oss-cn-hangzhou.aliyuncs.com` (replace with your region) +- **Access Key ID**: Your AccessKeyId +- **Secret Access Key**: Your AccessKeySecret +- **Enable**: Toggle ON +- **Bucket Name**: Your bucket name +- **Public Domain**: `https://your-bucket.oss-cn-hangzhou.aliyuncs.com` (your bucket's public URL) +- **Storage Region**: Your OSS region (e.g., `oss-cn-hangzhou`) +- **Prefix**: Optional date prefix +- **Public**: Enable if you want public access + +## AWS S3 + +### 1. Create S3 Bucket + +1. Log in to [AWS Console](https://console.aws.amazon.com/s3/) +2. Click **Create bucket** +3. Enter bucket name (globally unique) +4. Select AWS region +5. Configure bucket settings as needed +6. Click **Create bucket** + +### 2. Create IAM User + +1. Go to [IAM Console](https://console.aws.amazon.com/iam/) +2. Click **Users** > **Add user** +3. Enter username and select **Programmatic access** +4. Attach existing policies or create custom policy with S3 permissions: + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject", + "s3:ListBucket" + ], + "Resource": [ + "arn:aws:s3:::your-bucket-name", + "arn:aws:s3:::your-bucket-name/*" + ] + } + ] + } + ``` +5. Complete user creation and save **Access Key ID** and **Secret Access Key** + +### 3. Configuration in WR.DO + +Fill in the configuration form with: + +- **Provider**: aws (s3) +- **Channel Name**: AWS S3 (or any custom name) +- **S3 Endpoint**: `https://s3.amazonaws.com` (or region-specific endpoint) +- **Access Key ID**: Your IAM user's Access Key ID +- **Secret Access Key**: Your IAM user's Secret Access Key +- **Enable**: Toggle ON +- **Bucket Name**: Your S3 bucket name +- **Public Domain**: Your bucket's public URL or CloudFront distribution +- **Storage Region**: Your S3 region (e.g., `us-east-1`) +- **Prefix**: Optional date prefix +- **Public**: Enable if you want public access + + +## Common Configuration Options + +### Prefix Settings +- Use date-based prefixes (e.g., `2025/08/08`) to organize files by date +- Leave empty if you prefer flat file structure + +### Public Access +- Enable **Public** if you want files to be accessible via direct URLs +- Disable for private file storage + +### Custom Domains +- Configure custom domains for better branding +- Ensure proper DNS configuration for your domain + +## Troubleshooting + +### Common Issues + +1. **Access Denied**: Check your API credentials and permissions +2. **Bucket Not Found**: Verify bucket name and region settings +3. **CORS Issues**: Configure CORS settings in your bucket if needed +4. **Endpoint Errors**: Ensure correct endpoint format for your provider + +### Testing Configuration + +After saving your configuration, you can test it by: +1. Uploading a test file through the admin interface +2. Checking if the file appears in your cloud storage bucket +3. Verifying public access (if enabled) by accessing the file URL diff --git a/content/docs/developer/s3.mdx b/content/docs/developer/s3.mdx deleted file mode 100644 index 3b08cc4..0000000 --- a/content/docs/developer/s3.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: S3 Configs -description: How to config the s3 api. ---- - -## Overview - -Administrators can manage s3 configurations at `/admin/system`, including adding, deleting, and modifying s3 configurations. - -## Cloudflare R2 \ No newline at end of file diff --git a/hooks/use-file-upload.ts b/hooks/use-file-upload.ts index c0302fd..f938484 100644 --- a/hooks/use-file-upload.ts +++ b/hooks/use-file-upload.ts @@ -146,7 +146,25 @@ export function useFileUpload({ bucketInfo, userId, api }: Props) { }); if (!response.ok) { - throw new Error("获取预签名 URL 失败"); + // 尝试获取后端返回的具体错误信息 + let errorMessage = "获取预签名 URL 失败"; + try { + const errorText = await response.text(); + if (errorText) { + // 如果返回的是JSON格式的错误信息,尝试解析 + try { + const errorData = JSON.parse(errorText); + errorMessage = errorData.message || errorData.error || errorText; + } catch { + // 如果不是JSON,直接使用文本内容 + errorMessage = errorText; + } + } + } catch { + // 如果无法读取响应内容,使用默认错误信息 + errorMessage = `上传失败 (${response.status})`; + } + throw new Error(errorMessage); } const data = await response.json(); @@ -331,14 +349,15 @@ export function useFileUpload({ bucketInfo, userId, api }: Props) { await Promise.allSettled(uploadPromises); } catch (error) { console.error("上传失败:", error); - // 将所有 pending 状态的文件设置为错误状态 + // 将所有 pending 状态的文件设置为错误状态,并显示具体错误信息 + const errorMessage = error instanceof Error ? error.message : "上传失败"; setFiles((prev) => prev.map((file) => file.status === "pending" ? { ...file, status: "error", - error: "上传失败", + error: errorMessage, } : file, ), diff --git a/lib/dto/files.ts b/lib/dto/files.ts index cc20470..0941ac7 100644 --- a/lib/dto/files.ts +++ b/lib/dto/files.ts @@ -158,9 +158,8 @@ export async function getUserFiles(options: QueryUserFileOptions = {}) { }), prisma.userFile.count({ where }), prisma.userFile.aggregate({ - // All provider's file size where: { - userId, + ...(userId && { userId }), status: 1, }, _sum: { size: true }, @@ -275,7 +274,7 @@ export async function getUserFileStats(userId: string) { success: true, data: { totalFiles, - totalSize: totalSize._sum.size || 0, + totalSize: storageValueToBytes(totalSize._sum.size || 0), filesByProvider, }, }; @@ -358,3 +357,39 @@ export async function cleanupExpiredFiles(days: number = 30) { return { success: false, error: "Failed to clean up expired files" }; } } + +// 获取特定存储桶的使用量统计 +export async function getBucketStorageUsage( + bucket: string, + providerName: string, +): Promise< + | { success: true; data: { totalSize: number; totalFiles: number } } + | { success: false; error: string } +> { + try { + const result = await prisma.userFile.aggregate({ + where: { + bucket, + providerName, + status: 1, + }, + _sum: { + size: true, + }, + _count: { + id: true, + }, + }); + + return { + success: true, + data: { + totalSize: storageValueToBytes(result._sum.size || 0), + totalFiles: result._count.id || 0, + }, + }; + } catch (error) { + console.error("Failed to get bucket storage usage:", error); + return { success: false, error: "Failed to get bucket storage usage" }; + } +} diff --git a/lib/r2.ts b/lib/r2.ts index 3aa3b98..dbf54ce 100644 --- a/lib/r2.ts +++ b/lib/r2.ts @@ -34,6 +34,7 @@ export interface BucketItem { prefix?: string; file_types?: string; file_size?: string; + max_storage?: string; // 存储桶最大存储容量(字节) region?: string; public: boolean; } diff --git a/locales/en.json b/locales/en.json index 73ec486..4bbf049 100644 --- a/locales/en.json +++ b/locales/en.json @@ -214,6 +214,11 @@ "storageFull": "Storage space is almost full", "storageHigh": "Storage space usage is high", "storageGood": "Storage space is sufficient", + "planQuota": "Plan Quota", + "bucketQuota": "Bucket Quota", + "bucketCapacity": "Bucket Capacity", + "bucketStorageFull": "Bucket storage is almost full", + "bucketStorageHigh": "Bucket storage usage is high", "items": "items", "Total": "Total", "Configuration Error": "Configuration Error" @@ -360,6 +365,7 @@ "Uploading": "Uploading", "Completed": "Completed", "Aborted": "Aborted", + "Failed": "Failed", "Drop files to upload them to": "Drop files to upload them to", "Drag and drop file(s) here": "Drag and drop file(s) here", "or": "or", @@ -623,6 +629,8 @@ "Region": "Region", "Prefix": "Prefix", "Optional": "Optional", + "Max Storage": "Max Storage", + "maxStorageTooltip": "Set maximum storage capacity for this bucket (in bytes). If not set, uses plan quota global limit.", "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", diff --git a/locales/zh.json b/locales/zh.json index 237aa1e..5c87543 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -214,6 +214,11 @@ "storageFull": "存储空间即将用完", "storageHigh": "存储空间使用较多", "storageGood": "存储空间充足", + "planQuota": "计划配额", + "bucketQuota": "存储桶配额", + "bucketCapacity": "存储桶容量", + "bucketStorageFull": "存储桶空间即将用完", + "bucketStorageHigh": "存储桶空间使用较多", "items": "条", "Total": "共", "Configuration Error": "配置错误" @@ -360,6 +365,7 @@ "Uploading": "上传中", "Completed": "已完成", "Aborted": "已中止", + "Failed": "失败", "Drop files to upload them to": "将文件上传到", "Drag and drop file(s) here": "将文件拖到此处上传", "or": "或", @@ -623,6 +629,8 @@ "Region": "存储桶区域", "Prefix": "前缀", "Optional": "可选", + "Max Storage": "最大存储容量", + "maxStorageTooltip": "设置此存储桶的最大存储容量(字节)。如果不设置,默认使用 Plan 配额的全局限制。", "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": "公开此存储桶,所有注册用户都可以上传文件到此存储桶; 若不公开,只有管理员可以上传文件到此存储桶", diff --git a/public/_static/docs/cos-cors.png b/public/_static/docs/cos-cors.png new file mode 100644 index 0000000..2daf819 Binary files /dev/null and b/public/_static/docs/cos-cors.png differ diff --git a/public/sw.js.map b/public/sw.js.map index 6110b66..990ae64 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/bf4c8ec428159b5c3ebe0be7863842cb/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/da3081931a4b4b8e0299a7aed11c8d3e/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