@@ -29,6 +29,9 @@ export async function GET(req: NextRequest) {
|
||||
channel: configs.s3_config_01.channel,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Error listing files" }, { status: 500 });
|
||||
return NextResponse.json(
|
||||
{ error: "Error listing buckets" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function GET(req: NextRequest) {
|
||||
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) {
|
||||
@@ -56,6 +57,7 @@ export async function GET(req: NextRequest) {
|
||||
platform: configs.s3_config_01.platform,
|
||||
name,
|
||||
size: Number(fileSize || 0),
|
||||
status: Number(status === "0" ? 0 : 1),
|
||||
mimeType,
|
||||
});
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ export async function GET(req: NextRequest) {
|
||||
channel: configs.s3_config_01.channel,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Error listing files" }, { status: 500 });
|
||||
return NextResponse.json(
|
||||
{ error: "Error listing buckets" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { createUserFile } from "@/lib/dto/files";
|
||||
import { checkUserStatus } from "@/lib/dto/user";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { bytesToStorageValue } from "@/lib/utils";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -44,7 +45,7 @@ export async function POST(request: NextRequest) {
|
||||
channel: body.channel || "",
|
||||
platform: body.platform || "",
|
||||
providerName: body.providerName || "",
|
||||
size: body.size,
|
||||
size: bytesToStorageValue(body.size),
|
||||
bucket: body.bucket,
|
||||
lastModified: body.lastModified
|
||||
? new Date(body.lastModified)
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
downloadFileFromUrl,
|
||||
formatDate,
|
||||
formatFileSize,
|
||||
storageValueToBytes,
|
||||
truncateMiddle,
|
||||
} from "@/lib/utils";
|
||||
import { ClickableTooltip } from "@/components/ui/tooltip";
|
||||
@@ -47,7 +48,6 @@ 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";
|
||||
@@ -312,6 +312,10 @@ export default function UserFileList({
|
||||
)}
|
||||
<div className={cn("col-span-3 items-center space-x-3 text-sm")}>
|
||||
<ClickableTooltip
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center justify-start gap-1 break-all text-start",
|
||||
file.status !== 1 && "text-muted-foreground",
|
||||
)}
|
||||
content={
|
||||
<div className="w-72 space-y-1 text-wrap p-3 text-start">
|
||||
{file.mimeType.startsWith("image/") &&
|
||||
@@ -328,20 +332,13 @@ export default function UserFileList({
|
||||
</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>
|
||||
{truncateMiddle(file.path)}
|
||||
{file.status === 1 && (
|
||||
<CopyButton
|
||||
className="size-6"
|
||||
value={getFileUrl(file.path)}
|
||||
/>
|
||||
)}
|
||||
</ClickableTooltip>
|
||||
</div>
|
||||
<div className="col-span-2 hidden items-center text-xs sm:flex">
|
||||
@@ -350,10 +347,11 @@ export default function UserFileList({
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center text-nowrap text-xs">
|
||||
{formatFileSize(file.size || 0)}
|
||||
{formatFileSize(storageValueToBytes(file.size) || 0)}
|
||||
</div>
|
||||
<div className="col-span-1 hidden items-center text-xs sm:flex">
|
||||
<ClickableTooltip
|
||||
className="cursor-pointer truncate"
|
||||
content={
|
||||
<>
|
||||
<p>{file.user.name}</p>
|
||||
@@ -361,9 +359,7 @@ export default function UserFileList({
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="truncate">
|
||||
{file.user.name ?? file.user.email}
|
||||
</div>
|
||||
{file.user.name ?? file.user.email}
|
||||
</ClickableTooltip>
|
||||
</div>
|
||||
<div className="col-span-1 hidden items-center text-nowrap text-xs sm:flex">
|
||||
@@ -495,6 +491,7 @@ export default function UserFileList({
|
||||
{React.cloneElement(getFileIcon(file, bucketInfo), { size: 40 })}
|
||||
<div className="w-full text-center">
|
||||
<ClickableTooltip
|
||||
className="mx-auto line-clamp-2 max-w-[60px] cursor-pointer break-all px-2 pb-1 text-left text-xs font-medium text-muted-foreground group-hover:text-blue-500 sm:max-w-[100px]"
|
||||
content={
|
||||
<div className="max-w-[300px] space-y-1 p-3 text-start">
|
||||
{file.mimeType.startsWith("image/") &&
|
||||
@@ -511,7 +508,8 @@ export default function UserFileList({
|
||||
{file.path}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
<strong>Size:</strong> {formatFileSize(file.size || 0)}
|
||||
<strong>Size:</strong>{" "}
|
||||
{formatFileSize(storageValueToBytes(file.size) || 0)}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
<strong>Type:</strong> {file.mimeType || "-"}
|
||||
@@ -573,9 +571,7 @@ export default function UserFileList({
|
||||
</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>
|
||||
{truncateMiddle(file.path || "")}
|
||||
</ClickableTooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,6 +93,7 @@ export default function UserFileManager({ user, action }: FileListProps) {
|
||||
name: "",
|
||||
fileSize: "",
|
||||
mimeType: "",
|
||||
status: "1",
|
||||
});
|
||||
|
||||
// const isAdmin = action.includes("/admin");
|
||||
@@ -107,7 +108,7 @@ export default function UserFileManager({ user, action }: FileListProps) {
|
||||
|
||||
const { data: files, isLoading: isLoadingFiles } = useSWR<FileListData>(
|
||||
bucketInfo.bucket
|
||||
? `${action}/r2/files?bucket=${bucketInfo.bucket}&page=${currentPage}&pageSize=${pageSize}&name=${searchParams.name}&fileSize=${searchParams.fileSize}&mimeType=${searchParams.mimeType}`
|
||||
? `${action}/r2/files?bucket=${bucketInfo.bucket}&page=${currentPage}&pageSize=${pageSize}&name=${searchParams.name}&fileSize=${searchParams.fileSize}&mimeType=${searchParams.mimeType}&status=${searchParams.status}`
|
||||
: null,
|
||||
fetcher,
|
||||
{
|
||||
@@ -133,8 +134,9 @@ export default function UserFileManager({ user, action }: FileListProps) {
|
||||
}, [r2Configs]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
setSelectedFiles([]);
|
||||
mutate(
|
||||
`${action}/r2/files?bucket=${bucketInfo.bucket}&page=${currentPage}&pageSize=${pageSize}&name=${searchParams.name}&fileSize=${searchParams.fileSize}&mimeType=${searchParams.mimeType}`,
|
||||
`${action}/r2/files?bucket=${bucketInfo.bucket}&page=${currentPage}&pageSize=${pageSize}&name=${searchParams.name}&fileSize=${searchParams.fileSize}&mimeType=${searchParams.mimeType}&status=${searchParams.status}`,
|
||||
undefined,
|
||||
);
|
||||
};
|
||||
@@ -205,11 +207,12 @@ export default function UserFileManager({ user, action }: FileListProps) {
|
||||
name: "",
|
||||
fileSize: "",
|
||||
mimeType: "",
|
||||
status: "1",
|
||||
});
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[80px] rounded-r-none">
|
||||
<SelectTrigger className="w-[80px] rounded-r-none text-sm">
|
||||
<SelectValue placeholder="Select a type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -217,6 +220,7 @@ export default function UserFileManager({ user, action }: FileListProps) {
|
||||
{ lebal: "Name", value: "name" },
|
||||
{ lebal: "Size", value: "fileSize" },
|
||||
{ lebal: "Type", value: "mimeType" },
|
||||
{ lebal: "Status", value: "status" },
|
||||
].map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{t(item.lebal)}
|
||||
@@ -225,7 +229,7 @@ export default function UserFileManager({ user, action }: FileListProps) {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
className="min-w-28 rounded-l-none border-l-0 sm:w-48 sm:flex-none"
|
||||
className="min-w-28 rounded-l-none border-l-0 placeholder:text-xs sm:w-48 sm:flex-none"
|
||||
placeholder={`Search by ${currentSearchType}...`}
|
||||
value={searchParams[currentSearchType] || ""}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -41,7 +41,7 @@ export {
|
||||
TooltipArrow,
|
||||
};
|
||||
|
||||
export const ClickableTooltip = ({ children, content }) => {
|
||||
export const ClickableTooltip = ({ children, content, className = "" }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleClick = (e) => {
|
||||
@@ -65,7 +65,7 @@ export const ClickableTooltip = ({ children, content }) => {
|
||||
onFocus={(e) => e.preventDefault()} // 阻止焦点事件
|
||||
onBlur={(e) => e.preventDefault()}
|
||||
>
|
||||
<div onClick={handleClick} className="cursor-pointer truncate">
|
||||
<div onClick={handleClick} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -97,7 +97,7 @@ export function useFileUpload({ bucketInfo, userId, api }: Props) {
|
||||
userId,
|
||||
name: extractKey.fileName,
|
||||
originalName: extractKey.nameWithoutExtension,
|
||||
mimeType: item.file.type,
|
||||
mimeType: item.file.type || "-",
|
||||
path: item.fileName,
|
||||
etag,
|
||||
storageClass: "",
|
||||
|
||||
+4
-3
@@ -1,6 +1,7 @@
|
||||
import { Prisma, UserFile } from "@prisma/client";
|
||||
|
||||
import { prisma } from "../db";
|
||||
import { bytesToStorageValue, storageValueToBytes } from "../utils";
|
||||
|
||||
export interface UserFileData extends UserFile {
|
||||
user: {
|
||||
@@ -127,14 +128,14 @@ export async function getUserFiles(options: QueryUserFileOptions = {}) {
|
||||
|
||||
const where: Prisma.UserFileWhereInput = {
|
||||
bucket,
|
||||
...(status && { status }),
|
||||
...(status !== undefined && { status }),
|
||||
...(userId && { userId }),
|
||||
...(providerName && { providerName }),
|
||||
...(channel && { channel }),
|
||||
...(platform && { platform }),
|
||||
...(shortUrlId && { shortUrlId }),
|
||||
...(name && { name: { contains: name, mode: "insensitive" } }),
|
||||
...(size && { size: { gte: size } }),
|
||||
...(size && { size: { gte: bytesToStorageValue(size) } }),
|
||||
...(mimeType && {
|
||||
mimeType: { contains: mimeType, mode: "insensitive" },
|
||||
}),
|
||||
@@ -164,7 +165,7 @@ export async function getUserFiles(options: QueryUserFileOptions = {}) {
|
||||
|
||||
return {
|
||||
total,
|
||||
totalSize: totalSize._sum.size || 0,
|
||||
totalSize: storageValueToBytes(totalSize._sum.size || 0),
|
||||
list: files,
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -571,3 +571,23 @@ export function generateFileKey(fileName: string, prefix?: string): string {
|
||||
|
||||
return `${year}/${month}/${day}/${fileName}`;
|
||||
}
|
||||
|
||||
const SIZE_THRESHOLD = 1000;
|
||||
export function bytesToStorageValue(bytes: number): number {
|
||||
if (bytes < SIZE_THRESHOLD) {
|
||||
return bytes;
|
||||
} else {
|
||||
return Math.ceil(bytes / SIZE_THRESHOLD);
|
||||
}
|
||||
}
|
||||
|
||||
export function storageValueToBytes(
|
||||
storageValue: number,
|
||||
originalBytes?: number,
|
||||
): number {
|
||||
if (storageValue < SIZE_THRESHOLD) {
|
||||
return storageValue;
|
||||
} else {
|
||||
return storageValue * SIZE_THRESHOLD;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ ON "user_files"("userId", "providerName", "status", "lastModified", "createdAt")
|
||||
ALTER TABLE "user_files" ADD CONSTRAINT "user_files_userId_fkey"
|
||||
FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- BigInt
|
||||
ALTER TABLE "plans" ADD COLUMN "stMaxFileSize" TEXT NOT NULL DEFAULT '26214400';
|
||||
ALTER TABLE "plans" ADD COLUMN "stMaxTotalSize" TEXT NOT NULL DEFAULT '524288000';
|
||||
ALTER TABLE "plans" ADD COLUMN "stMaxFileCount" INTEGER NOT NULL DEFAULT 1000;
|
||||
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user