chore: add public filed for bucket

This commit is contained in:
oiov
2025-07-09 16:24:06 +08:00
parent 709a56609e
commit feb0e31eb2
9 changed files with 129 additions and 98 deletions
+45 -31
View File
@@ -2,14 +2,14 @@
import { useEffect, useMemo, useState, useTransition } from "react";
import Link from "next/link";
import { AnimatePresence, motion } from "framer-motion";
import { motion } from "framer-motion";
import { CloudCog } from "lucide-react";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
import useSWR from "swr";
import { CloudStorageCredentials } from "@/lib/r2";
import { cn, fetcher, formatFileSize } from "@/lib/utils";
import { cn, fetcher } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
@@ -22,6 +22,12 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Icons } from "@/components/shared/icons";
export default function S3Configs({}: {}) {
@@ -47,6 +53,7 @@ export default function S3Configs({}: {}) {
prefix: "",
file_types: "",
region: "auto",
public: true,
},
],
});
@@ -191,7 +198,7 @@ export default function S3Configs({}: {}) {
</div>
{r2Credentials.buckets.map((bucket, index) => (
<motion.div
className="relative grid grid-cols-1 gap-4 rounded-lg border border-dashed border-muted-foreground px-3 pb-3 pt-10 text-neutral-600 dark:text-neutral-400 sm:grid-cols-4"
className="relative grid grid-cols-1 gap-4 rounded-lg border border-dashed border-muted-foreground px-3 pb-3 pt-10 text-neutral-600 dark:text-neutral-400 sm:grid-cols-3"
key={`bucket-${index}`}
layout
initial={{ opacity: 0, scale: 0.9 }}
@@ -225,7 +232,6 @@ export default function S3Configs({}: {}) {
<Icons.arrowUp className="size-4" />{" "}
</Button>
)}
{index < r2Credentials.buckets.length - 1 && (
<Button
className="h-[30px] px-1.5"
@@ -244,7 +250,6 @@ export default function S3Configs({}: {}) {
<Icons.arrowDown className="size-4" />{" "}
</Button>
)}
<Button
className="ml-auto h-[30px] px-1.5"
size={"sm"}
@@ -258,6 +263,7 @@ export default function S3Configs({}: {}) {
region: "auto",
custom_domain: "",
file_size: "26214400",
public: true,
});
setR2Credentials({
...r2Credentials,
@@ -324,32 +330,6 @@ export default function S3Configs({}: {}) {
}}
/>
</div>
{/* <div className="space-y-1">
<Label>{t("Max File Size")} (Bytes)</Label>
<div className="relative">
<Input
value={bucket.file_size}
placeholder=""
onChange={(e) => {
const newBuckets = [...r2Credentials.buckets];
newBuckets[index] = {
...bucket,
file_size: e.target.value,
};
setR2Credentials({
...r2Credentials,
buckets: newBuckets,
});
}}
/>
<span className="absolute right-2 top-[11px] text-xs text-muted-foreground">
=
{formatFileSize(Number(bucket.file_size || "0"), {
precision: 0,
})}
</span>
</div>
</div> */}
<div className="space-y-1">
<Label>{t("Region")}</Label>
<Input
@@ -388,6 +368,40 @@ export default function S3Configs({}: {}) {
}}
/>
</div>
<div className="flex flex-col justify-center space-y-3">
<div className="flex items-center gap-1">
<Label>{t("Public")}</Label>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<Icons.help className="size-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent className="max-w-56 text-wrap">
{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",
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Switch
checked={bucket.public}
onCheckedChange={(e) =>
setR2Credentials({
...r2Credentials,
buckets: r2Credentials.buckets.map((b, i) => {
if (i === index) {
return {
...b,
public: e,
};
}
return b;
}),
})
}
/>
</div>
{/* <div className="space-y-1">
<Label>
{t("Allowed File Types")} ({t("Optional")})
+1 -1
View File
@@ -16,7 +16,7 @@ export async function GET(req: NextRequest) {
}
return NextResponse.json({
buckets: configs.s3_config_01.buckets,
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,
+70 -59
View File
@@ -12,6 +12,7 @@ import {
FileType2,
Folder,
Image,
ImageOff,
Trash2,
} from "lucide-react";
import { useTranslations } from "next-intl";
@@ -269,7 +270,7 @@ export default function UserFileList({
const renderListView = () => (
<div className="overflow-hidden rounded-lg border bg-primary-foreground">
<div className="text-mute-foreground grid grid-cols-6 gap-4 bg-neutral-100 px-6 py-3 text-sm font-medium dark:bg-neutral-800 sm:grid-cols-9">
<div className="text-mute-foreground grid grid-cols-6 gap-4 bg-neutral-100 px-6 py-3 text-sm font-medium dark:bg-neutral-800 sm:grid-cols-10">
{showMutiCheckBox && (
<div className="col-span-1 flex">
<Checkbox
@@ -282,8 +283,8 @@ export default function UserFileList({
<div className={cn(showMutiCheckBox ? "col-span-2" : "col-span-3")}>
{t("Name")}
</div>
<div className="col-span-2 hidden sm:flex">{t("Type")}</div>
<div className="col-span-1">{t("Size")}</div>
<div className="col-span-1 hidden sm:flex">{t("Type")}</div>
<div className="col-span-1 hidden sm:flex">{t("User")}</div>
<div className="col-span-1 hidden sm:flex">{t("Date")}</div>
<div className="col-span-1 hidden sm:flex">{t("Active")}</div>
@@ -302,7 +303,7 @@ export default function UserFileList({
{files?.list.map((file, index) => (
<div
key={file.id}
className="text-mute-foreground grid grid-cols-6 gap-4 px-6 py-4 transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-600 sm:grid-cols-9"
className="text-mute-foreground grid grid-cols-6 gap-4 px-6 py-4 transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-600 sm:grid-cols-10"
>
{showMutiCheckBox && (
<div
@@ -326,12 +327,19 @@ export default function UserFileList({
>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger className="flex items-center justify-start gap-1 break-all text-start">
<TooltipTrigger
className={cn(
"flex items-center justify-start gap-1 break-all text-start",
file.status !== 1 && "text-muted-foreground",
)}
>
{truncateMiddle(file.path)}
<CopyButton
className="size-6"
value={getFileUrl(file.path)}
/>
{file.status === 1 && (
<CopyButton
className="size-6"
value={getFileUrl(file.path)}
/>
)}
</TooltipTrigger>
<TooltipContent
side="right"
@@ -351,14 +359,14 @@ export default function UserFileList({
</Tooltip>
</TooltipProvider>
</div>
<div className="col-span-1 flex items-center text-nowrap text-xs">
{formatFileSize(file.size || 0)}
</div>
<div className="col-span-1 hidden items-center text-xs sm:flex">
<div className="col-span-2 hidden items-center text-xs sm:flex">
<Badge className="truncate" variant="outline">
{file.mimeType || "-"}
</Badge>
</div>
<div className="col-span-1 flex items-center text-nowrap text-xs">
{formatFileSize(file.size || 0)}
</div>
<div className="col-span-1 hidden items-center text-xs sm:flex">
<TooltipProvider>
<Tooltip delayDuration={200}>
@@ -376,7 +384,7 @@ export default function UserFileList({
<TimeAgoIntl date={file.updatedAt as Date} />
</div>
<div className="col-span-1 hidden items-center text-xs sm:flex">
<Switch checked={file.status === 1} />
<Switch checked={file.status === 1} disabled />
</div>
<div className="col-span-1 flex items-center">
<DropdownMenu>
@@ -488,7 +496,7 @@ export default function UserFileList({
)}
onClick={() => handleSelectFile(file)}
>
<div className="flex flex-col items-center justify-center space-y-2">
<div className="flex flex-col items-center justify-center space-y-1 py-1">
{showMutiCheckBox && (
<Checkbox
checked={
@@ -498,12 +506,7 @@ export default function UserFileList({
className="absolute left-1 top-1 size-4 border-neutral-300 bg-neutral-100 data-[state=checked]:border-neutral-900 data-[state=checked]:bg-neutral-600 data-[state=checked]:text-neutral-100 dark:border-neutral-700 dark:bg-neutral-800 dark:data-[state=checked]:border-neutral-300 dark:data-[state=checked]:bg-neutral-300"
/>
)}
{React.cloneElement(
getFileIcon(file.path, file.mimeType, bucketInfo),
{
size: 40,
},
)}
{React.cloneElement(getFileIcon(file, bucketInfo), { size: 40 })}
<div className="w-full text-center">
<TooltipProvider>
<Tooltip delayDuration={0}>
@@ -514,15 +517,16 @@ export default function UserFileList({
side="right"
className="max-w-[300px] space-y-1 p-3 text-start"
>
{file.mimeType.startsWith("image/") && (
<img
className="mb-2 max-h-[70vh] w-fit rounded shadow"
width={300}
height={300}
src={getFileUrl(file.path)}
alt={`${file.path}`}
/>
)}
{file.mimeType.startsWith("image/") &&
file.status === 1 && (
<img
className="mb-2 max-h-[70vh] w-fit rounded shadow"
width={300}
height={300}
src={getFileUrl(file.path)}
alt={`${file.path}`}
/>
)}
<p className="mt-1 text-sm font-semibold text-muted-foreground">
{file.path}
</p>
@@ -546,6 +550,7 @@ export default function UserFileList({
size="sm"
variant="outline"
onClick={() => handlePreviewRawFile(file.path)}
disabled={file.status !== 1}
>
<Icons.eye className="size-4" />
{t("Raw Data")}
@@ -554,6 +559,7 @@ export default function UserFileList({
className="h-7 px-1.5 text-xs hover:bg-slate-100 dark:hover:text-primary-foreground"
size="sm"
variant={"outline"}
disabled={file.status !== 1}
onClick={() => {
setCurrentSelectFile(file);
setShowQrcode(!isShowQrcode);
@@ -567,18 +573,22 @@ export default function UserFileList({
title="下载"
size="sm"
variant={"blue"}
disabled={file.status !== 1}
>
<Download className="size-4" />
</Button>
<Button
onClick={() => handleDeleteSingle(file)}
className="h-7 px-1.5"
title="删除"
size="sm"
variant={"destructive"}
>
<Trash2 className="size-4" />
</Button>
{file.status === 1 && (
<Button
onClick={() => handleDeleteSingle(file)}
className="h-7 px-1.5"
title="删除"
size="sm"
variant={"destructive"}
disabled={file.status !== 1}
>
<Trash2 className="size-4" />
</Button>
)}
</div>
</TooltipContent>
</Tooltip>
@@ -678,11 +688,10 @@ function TableColumnSekleton() {
);
}
const getFileIcon = (
filename: string,
mimeType: string | null,
bucketInfo: BucketInfo,
) => {
const getFileIcon = (file: UserFileData, bucketInfo: BucketInfo) => {
const filename = file.path;
const mimeType = file.mimeType;
const status = file.status;
const iconProps = { size: 24, className: "text-gray-600" };
// 如果没有 mimeType,回退到文件夹判断
@@ -693,25 +702,27 @@ const getFileIcon = (
return <FileText {...iconProps} className="text-gray-500" />;
}
// 图片类型 - 直接显示图片
if (mimeType.startsWith("image/")) {
if (mimeType === "image/svg+xml") {
return <Image {...iconProps} className="text-blue-500" />;
return <FileCode {...iconProps} className="text-blue-500" />;
}
if (status === 1) {
return (
<img
className="max-h-12 w-fit max-w-24 rounded shadow"
height={60}
width={60}
src={
bucketInfo.custom_domain
? `${bucketInfo.custom_domain}/${filename}`
: filename
}
alt={filename}
/>
);
} else {
return <ImageOff {...iconProps} className="text-muted-foreground" />;
}
// 其他图片格式显示缩略图
return (
<img
className="max-h-12 w-fit max-w-24 rounded shadow"
height={60}
width={60}
src={
bucketInfo.custom_domain
? `${bucketInfo.custom_domain}/${filename}`
: filename
}
alt={filename}
/>
);
}
// 压缩文件
+1
View File
@@ -78,6 +78,7 @@ export default function UserFileManager({ user, action }: FileListProps) {
platform: "",
channel: "",
provider_name: "",
public: true,
});
const [selectedFiles, setSelectedFiles] = useState<UserFileData[]>([]);
+1
View File
@@ -35,6 +35,7 @@ export interface BucketItem {
file_types?: string;
file_size?: string;
region?: string;
public: boolean;
}
export interface FileObject {
+3 -1
View File
@@ -615,6 +615,8 @@
"Region": "Region",
"Prefix": "Prefix",
"Optional": "Optional",
"Allowed File Types": "Allowed File Types"
"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"
}
}
+3 -1
View File
@@ -615,6 +615,8 @@
"Region": "存储桶区域",
"Prefix": "前缀",
"Optional": "可选",
"Allowed File Types": "允许的文件类型"
"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": "公开此存储桶,所有注册用户都可以上传文件到此存储桶; 若不公开,只有管理员可以上传文件到此存储桶"
}
}
@@ -9,7 +9,7 @@ INSERT INTO "system_configs"
VALUES
(
's3_config_01',
'{"enabled":true,"platform":"cloudflare","channel":"r2","provider_name":"Cloudflare R2","account_id":"","access_key_id":"","secret_access_key":"","endpoint":"https://<account_id>.r2.cloudflarestorage.com","buckets":[{"custom_domain":"","prefix":"","bucket":"","file_types":"","file_size":"26214400","region":"auto"}]}',
'{"enabled":true,"platform":"cloudflare","channel":"r2","provider_name":"Cloudflare R2","account_id":"","access_key_id":"","secret_access_key":"","endpoint":"https://<account_id>.r2.cloudflarestorage.com","buckets":[{"custom_domain":"","prefix":"","bucket":"","file_types":"","file_size":"26214400","region":"auto","public":true}]}',
'OBJECT',
'R2 存储桶配置'
);
@@ -25,7 +25,7 @@ INSERT INTO "system_configs"
VALUES
(
's3_config_02',
'{"enabled":true,"platform":"aws","channel":"s3","provider_name":"Amazon S3","endpoint":"https://s3.<region>.amazonaws.com","account_id":"","access_key_id":"","secret_access_key":"","buckets":[{"custom_domain":"","prefix":"","bucket":"","file_types":"","file_size":"26214400","region":"us-east-1"}]}',
'{"enabled":true,"platform":"aws","channel":"s3","provider_name":"Amazon S3","endpoint":"https://s3.<region>.amazonaws.com","account_id":"","access_key_id":"","secret_access_key":"","buckets":[{"custom_domain":"","prefix":"","bucket":"","file_types":"","file_size":"26214400","region":"us-east-1","public":true}]}',
'OBJECT',
'Amazon S3 存储桶配置'
);
@@ -41,7 +41,7 @@ INSERT INTO "system_configs"
VALUES
(
's3_config_03',
'{"enabled":true,"platform":"ali","channel":"oss","provider_name":"阿里云 OSS","endpoint":"","account_id":"","access_key_id":"","secret_access_key":"","buckets":[{"custom_domain":"","prefix":"","bucket":"","file_types":"","file_size":"26214400","region":""}]}',
'{"enabled":true,"platform":"ali","channel":"oss","provider_name":"阿里云 OSS","endpoint":"","account_id":"","access_key_id":"","secret_access_key":"","buckets":[{"custom_domain":"","prefix":"","bucket":"","file_types":"","file_size":"26214400","region":"","public":true}]}',
'OBJECT',
'阿里云 OSS 存储桶配置'
);
@@ -57,7 +57,7 @@ INSERT INTO "system_configs"
VALUES
(
's3_config_04',
'{"enabled":true,"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":""}]}',
'{"enabled":true,"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',
'腾讯云 COS 存储桶配置'
);
+1 -1
View File
File diff suppressed because one or more lines are too long