@@ -2,10 +2,12 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
|
||||
import { getPlanQuota } from "@/lib/dto/plan";
|
||||
import { getMultipleConfigs } from "@/lib/dto/system-config";
|
||||
import { checkUserStatus } from "@/lib/dto/user";
|
||||
import { createS3Client } from "@/lib/r2";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { restrictByTimeRange } from "@/lib/team";
|
||||
import { generateFileKey } from "@/lib/utils";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
@@ -42,6 +44,22 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
const plan = await getPlanQuota(user.team!);
|
||||
for (const file of files) {
|
||||
if (Number(file.size) > Number(plan.stMaxFileSize)) {
|
||||
return Response.json(`File (${file.name}) size limit exceeded`, {
|
||||
status: 403,
|
||||
});
|
||||
}
|
||||
}
|
||||
const limit = await restrictByTimeRange({
|
||||
model: "userFile",
|
||||
userId: user.id,
|
||||
limit: Number(plan.stMaxFileCount),
|
||||
rangeType: "month",
|
||||
});
|
||||
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,
|
||||
|
||||
@@ -6,10 +6,12 @@ import { User } from "@prisma/client";
|
||||
import {
|
||||
Archive,
|
||||
Download,
|
||||
FileAudio,
|
||||
FileCode,
|
||||
FileSpreadsheet,
|
||||
FileText,
|
||||
FileType2,
|
||||
FileVideo,
|
||||
Folder,
|
||||
ImageOff,
|
||||
Trash2,
|
||||
@@ -25,20 +27,13 @@ import {
|
||||
formatFileSize,
|
||||
truncateMiddle,
|
||||
} from "@/lib/utils";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { ClickableTooltip } from "@/components/ui/tooltip";
|
||||
import { BucketInfo, DisplayType, FileListData } from "@/components/file";
|
||||
|
||||
import { UrlForm } from "../forms/url-form";
|
||||
import { CopyButton } from "../shared/copy-button";
|
||||
import { EmptyPlaceholder } from "../shared/empty-placeholder";
|
||||
import { Icons } from "../shared/icons";
|
||||
import { PaginationWrapper } from "../shared/pagination";
|
||||
import QRCodeEditor from "../shared/qr";
|
||||
import { TimeAgoIntl } from "../shared/time-ago";
|
||||
import { Badge } from "../ui/badge";
|
||||
@@ -52,6 +47,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
import { Modal } from "../ui/modal";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
import { Switch } from "../ui/switch";
|
||||
import { TableCell, TableRow } from "../ui/table";
|
||||
@@ -92,7 +88,7 @@ export default function UserFileList({
|
||||
const [currentSelectFile, setCurrentSelectFile] =
|
||||
useState<UserFileData | null>();
|
||||
|
||||
const isAdmin = action.includes("/admin");
|
||||
// const isAdmin = action.includes("/admin");
|
||||
|
||||
const getFileUrl = (key: string) => {
|
||||
return `${bucketInfo.custom_domain}/${key}`;
|
||||
@@ -155,7 +151,7 @@ export default function UserFileList({
|
||||
const handleGenerateShortLink = async (urlId: string) => {
|
||||
if (!shortTarget) return;
|
||||
try {
|
||||
const response = await fetch(`${action}/r2/short`, {
|
||||
const response = await fetch(`${action}/r2/files/short`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ urlId, fileId: shortTarget?.id }),
|
||||
});
|
||||
@@ -208,7 +204,14 @@ export default function UserFileList({
|
||||
|
||||
const renderFileLinks = (file: UserFileData, index: number) => (
|
||||
<>
|
||||
{!isAdmin && file.shortUrlId && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Icons.fileText className="size-3 flex-shrink-0" />
|
||||
<p className="line-clamp-1 truncate rounded-md bg-neutral-100 p-1.5 text-xs dark:bg-neutral-800">
|
||||
{file.path}
|
||||
</p>
|
||||
<CopyButton className="size-6" value={file.path} />
|
||||
</div>
|
||||
{file.shortUrlId && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Icons.unLink className="size-3 flex-shrink-0 text-blue-500" />
|
||||
<Link
|
||||
@@ -308,30 +311,13 @@ export default function UserFileList({
|
||||
</div>
|
||||
)}
|
||||
<div className={cn("col-span-3 items-center space-x-3 text-sm")}>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger
|
||||
className={cn(
|
||||
"flex items-center justify-start gap-1 break-all text-start",
|
||||
file.status !== 1 && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{truncateMiddle(file.path)}
|
||||
{file.status === 1 && (
|
||||
<CopyButton
|
||||
className="size-6"
|
||||
value={getFileUrl(file.path)}
|
||||
/>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
className="w-72 space-y-1 text-wrap p-3 text-start"
|
||||
>
|
||||
<ClickableTooltip
|
||||
content={
|
||||
<div className="w-72 space-y-1 text-wrap p-3 text-start">
|
||||
{file.mimeType.startsWith("image/") &&
|
||||
file.status === 1 && (
|
||||
<img
|
||||
className="mb-2 max-h-[70vh] w-72 rounded shadow"
|
||||
className="mb-2 max-h-[300px] w-fit rounded shadow"
|
||||
width={300}
|
||||
height={300}
|
||||
src={getFileUrl(file.path)}
|
||||
@@ -339,9 +325,24 @@ export default function UserFileList({
|
||||
/>
|
||||
)}
|
||||
{renderFileLinks(file, index)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-start gap-1 break-all text-start",
|
||||
file.status !== 1 && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{truncateMiddle(file.path)}
|
||||
{file.status === 1 && (
|
||||
<CopyButton
|
||||
className="size-6"
|
||||
value={getFileUrl(file.path)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ClickableTooltip>
|
||||
</div>
|
||||
<div className="col-span-2 hidden items-center text-xs sm:flex">
|
||||
<Badge className="truncate" variant="outline">
|
||||
@@ -352,17 +353,18 @@ export default function UserFileList({
|
||||
{formatFileSize(file.size || 0)}
|
||||
</div>
|
||||
<div className="col-span-1 hidden items-center text-xs sm:flex">
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger className="truncate">
|
||||
{file.user.name ?? file.user.email}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<ClickableTooltip
|
||||
content={
|
||||
<>
|
||||
<p>{file.user.name}</p>
|
||||
<p>{file.user.email}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="truncate">
|
||||
{file.user.name ?? file.user.email}
|
||||
</div>
|
||||
</ClickableTooltip>
|
||||
</div>
|
||||
<div className="col-span-1 hidden items-center text-nowrap text-xs sm:flex">
|
||||
<TimeAgoIntl date={file.updatedAt as Date} />
|
||||
@@ -492,19 +494,13 @@ export default function UserFileList({
|
||||
)}
|
||||
{React.cloneElement(getFileIcon(file, bucketInfo), { size: 40 })}
|
||||
<div className="w-full text-center">
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger className="mx-auto line-clamp-2 max-w-[60px] break-all px-2 pb-1 text-left text-xs font-medium text-muted-foreground group-hover:text-blue-500 sm:max-w-[100px]">
|
||||
{truncateMiddle(file.path || "")}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
className="max-w-[300px] space-y-1 p-3 text-start"
|
||||
>
|
||||
<ClickableTooltip
|
||||
content={
|
||||
<div className="max-w-[300px] space-y-1 p-3 text-start">
|
||||
{file.mimeType.startsWith("image/") &&
|
||||
file.status === 1 && (
|
||||
<img
|
||||
className="mb-2 max-h-[70vh] w-fit rounded shadow"
|
||||
className="mb-2 max-h-[300px] w-fit rounded shadow"
|
||||
width={300}
|
||||
height={300}
|
||||
src={getFileUrl(file.path)}
|
||||
@@ -574,9 +570,13 @@ export default function UserFileList({
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="mx-auto line-clamp-2 max-w-[60px] break-all px-2 pb-1 text-left text-xs font-medium text-muted-foreground group-hover:text-blue-500 sm:max-w-[100px]">
|
||||
{truncateMiddle(file.path || "")}
|
||||
</div>
|
||||
</ClickableTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -768,12 +768,12 @@ const getFileIcon = (file: UserFileData, bucketInfo: BucketInfo) => {
|
||||
|
||||
// 音频文件
|
||||
if (mimeType.startsWith("audio/")) {
|
||||
return <FileText {...iconProps} className="text-purple-500" />;
|
||||
return <FileAudio {...iconProps} className="text-purple-500" />;
|
||||
}
|
||||
|
||||
// 视频文件
|
||||
if (mimeType.startsWith("video/")) {
|
||||
return <FileText {...iconProps} className="text-pink-500" />;
|
||||
return <FileVideo {...iconProps} className="text-pink-500" />;
|
||||
}
|
||||
|
||||
// 默认文件图标
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { cn, formatFileSize } from "@/lib/utils";
|
||||
import { useFileUpload } from "@/hooks/use-file-upload";
|
||||
@@ -62,7 +63,20 @@ export const FileUploader = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedFile) {
|
||||
addFiles(selectedFile);
|
||||
const outOfLimitSizeFiles = selectedFile.some(
|
||||
(file) => file.size > Number(plan?.stMaxFileSize ?? 0),
|
||||
);
|
||||
const notOutOfLimitSizeFiles = selectedFile.filter(
|
||||
(file) => file.size <= Number(plan?.stMaxFileSize ?? 0),
|
||||
);
|
||||
addFiles(notOutOfLimitSizeFiles);
|
||||
if (outOfLimitSizeFiles) {
|
||||
toast.warning(
|
||||
`File size exceeds the limit of ${formatFileSize(
|
||||
Number(plan?.stMaxFileSize ?? 0),
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [selectedFile]);
|
||||
|
||||
@@ -99,6 +113,7 @@ export const FileUploader = ({
|
||||
</div>
|
||||
</div>
|
||||
<Badge className="text-xs">
|
||||
{t("Limit")}:{" "}
|
||||
{formatFileSize(Number(plan?.stMaxFileSize || "0"), {
|
||||
precision: 0,
|
||||
})}{" "}
|
||||
@@ -327,7 +342,7 @@ export const FileUploader = ({
|
||||
className="flex items-center gap-2 rounded-md bg-green-500 px-4 py-2 text-white transition-colors hover:bg-green-600 disabled:cursor-not-allowed disabled:bg-gray-400"
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
Start Upload
|
||||
{t("Start Upload")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -39,3 +40,39 @@ export {
|
||||
TooltipProvider,
|
||||
TooltipArrow,
|
||||
};
|
||||
|
||||
export const ClickableTooltip = ({ children, content }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip
|
||||
delayDuration={0}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
disableHoverableContent
|
||||
>
|
||||
<TooltipTrigger
|
||||
asChild
|
||||
onPointerEnter={(e) => e.preventDefault()} // 阻止指针进入事件
|
||||
onPointerLeave={(e) => e.preventDefault()} // 阻止指针离开事件
|
||||
onPointerMove={(e) => e.preventDefault()} // 阻止指针移动事件
|
||||
onFocus={(e) => e.preventDefault()} // 阻止焦点事件
|
||||
onBlur={(e) => e.preventDefault()}
|
||||
>
|
||||
<div onClick={handleClick} className="cursor-pointer truncate">
|
||||
{children}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent className="p-1">{content}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-1
@@ -370,7 +370,9 @@
|
||||
"Upload Files": "Upload Files",
|
||||
"Uploud channel": "Uploud channel",
|
||||
"Do not close the window until the upload is complete": "Do not close the window until the upload is complete",
|
||||
"Pending Upload": "Pending Upload"
|
||||
"Pending Upload": "Pending Upload",
|
||||
"Start Upload": "Start Upload",
|
||||
"Limit": "Limit"
|
||||
},
|
||||
"Landing": {
|
||||
"settings": "Settings",
|
||||
|
||||
+3
-1
@@ -370,7 +370,9 @@
|
||||
"Upload Files": "上传文件",
|
||||
"Uploud channel": "渠道",
|
||||
"Do not close the window until the upload is complete": "在上传完成之前不要浏览器关闭窗口",
|
||||
"Pending Upload": "等待上传"
|
||||
"Pending Upload": "等待上传",
|
||||
"Start Upload": "开始上传",
|
||||
"Limit": "限制"
|
||||
},
|
||||
"Landing": {
|
||||
"settings": "设置",
|
||||
|
||||
@@ -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","public":true}]}',
|
||||
'{"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":[{"bucket":"","prefix":"","file_types":"","region":"auto","custom_domain":"","file_size":"26214400","public":true}]}',
|
||||
'OBJECT',
|
||||
'R2 存储桶配置'
|
||||
);
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user