chore: add public filed for bucket
This commit is contained in:
@@ -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")})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 压缩文件
|
||||
|
||||
@@ -78,6 +78,7 @@ export default function UserFileManager({ user, action }: FileListProps) {
|
||||
platform: "",
|
||||
channel: "",
|
||||
provider_name: "",
|
||||
public: true,
|
||||
});
|
||||
|
||||
const [selectedFiles, setSelectedFiles] = useState<UserFileData[]>([]);
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface BucketItem {
|
||||
file_types?: string;
|
||||
file_size?: string;
|
||||
region?: string;
|
||||
public: boolean;
|
||||
}
|
||||
|
||||
export interface FileObject {
|
||||
|
||||
+3
-1
@@ -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
@@ -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
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user