feat: calculate used storage

This commit is contained in:
Vasily Zubarev
2025-04-10 15:14:54 +02:00
parent ea8a3295e9
commit 62bad46e58
13 changed files with 87 additions and 25 deletions

View File

@@ -45,6 +45,7 @@ export async function analyzeTransaction(
}) })
console.log("ChatGPT response:", response.output_text) console.log("ChatGPT response:", response.output_text)
console.log("ChatGPT tokens used:", response.usage)
const result = JSON.parse(response.output_text) const result = JSON.parse(response.output_text)
return { success: true, data: result } return { success: true, data: result }

View File

@@ -2,8 +2,9 @@
import { ActionState } from "@/lib/actions" import { ActionState } from "@/lib/actions"
import { getCurrentUser } from "@/lib/auth" import { getCurrentUser } from "@/lib/auth"
import { getUserUploadsDirectory, unsortedFilePath } from "@/lib/files" import { getDirectorySize, getUserUploadsDirectory, unsortedFilePath } from "@/lib/files"
import { createFile } from "@/models/files" import { createFile } from "@/models/files"
import { updateUser } from "@/models/users"
import { randomUUID } from "crypto" import { randomUUID } from "crypto"
import { mkdir, writeFile } from "fs/promises" import { mkdir, writeFile } from "fs/promises"
import { revalidatePath } from "next/cache" import { revalidatePath } from "next/cache"
@@ -50,6 +51,10 @@ export async function uploadFilesAction(formData: FormData): Promise<ActionState
}) })
) )
// Update user storage used
const storageUsed = await getDirectorySize(await getUserUploadsDirectory(user))
await updateUser(user.id, { storageUsed })
console.log("uploadedFiles", uploadedFiles) console.log("uploadedFiles", uploadedFiles)
revalidatePath("/unsorted") revalidatePath("/unsorted")

View File

@@ -32,18 +32,21 @@ export default async function RootLayout({ children }: { children: React.ReactNo
const user = await getCurrentUser() const user = await getCurrentUser()
const unsortedFilesCount = await getUnsortedFilesCount(user.id) const unsortedFilesCount = await getUnsortedFilesCount(user.id)
const userProfile = {
id: user.id,
name: user.name || "",
email: user.email,
avatar: user.avatar || undefined,
storageUsed: user.storageUsed || 0,
}
return ( return (
<NotificationProvider> <NotificationProvider>
<ScreenDropArea> <ScreenDropArea>
<SidebarProvider> <SidebarProvider>
<MobileMenu unsortedFilesCount={unsortedFilesCount} /> <MobileMenu unsortedFilesCount={unsortedFilesCount} />
<AppSidebar <AppSidebar
profile={{ profile={userProfile}
id: user.id,
name: user.name || "",
email: user.email,
avatar: user.avatar || undefined,
}}
unsortedFilesCount={unsortedFilesCount} unsortedFilesCount={unsortedFilesCount}
isSelfHosted={config.selfHosted.isEnabled} isSelfHosted={config.selfHosted.isEnabled}
/> />

View File

@@ -3,7 +3,7 @@
import { transactionFormSchema } from "@/forms/transactions" import { transactionFormSchema } from "@/forms/transactions"
import { ActionState } from "@/lib/actions" import { ActionState } from "@/lib/actions"
import { getCurrentUser } from "@/lib/auth" import { getCurrentUser } from "@/lib/auth"
import { getTransactionFileUploadPath, getUserUploadsDirectory } from "@/lib/files" import { getDirectorySize, getTransactionFileUploadPath, getUserUploadsDirectory } from "@/lib/files"
import { updateField } from "@/models/fields" import { updateField } from "@/models/fields"
import { createFile, deleteFile } from "@/models/files" import { createFile, deleteFile } from "@/models/files"
import { import {
@@ -14,6 +14,7 @@ import {
updateTransaction, updateTransaction,
updateTransactionFiles, updateTransactionFiles,
} from "@/models/transactions" } from "@/models/transactions"
import { updateUser } from "@/models/users"
import { Transaction } from "@prisma/client" import { Transaction } from "@prisma/client"
import { randomUUID } from "crypto" import { randomUUID } from "crypto"
import { mkdir, writeFile } from "fs/promises" import { mkdir, writeFile } from "fs/promises"
@@ -106,6 +107,11 @@ export async function deleteTransactionFileAction(
) )
await deleteFile(fileId, user.id) await deleteFile(fileId, user.id)
// Update user storage used
const storageUsed = await getDirectorySize(await getUserUploadsDirectory(user))
await updateUser(user.id, { storageUsed })
revalidatePath(`/transactions/${transactionId}`) revalidatePath(`/transactions/${transactionId}`)
return { success: true, data: transaction } return { success: true, data: transaction }
} }
@@ -169,6 +175,10 @@ export async function uploadTransactionFilesAction(formData: FormData): Promise<
: fileRecords.map((file) => file.id) : fileRecords.map((file) => file.id)
) )
// Update user storage used
const storageUsed = await getDirectorySize(await getUserUploadsDirectory(user))
await updateUser(user.id, { storageUsed })
revalidatePath(`/transactions/${transactionId}`) revalidatePath(`/transactions/${transactionId}`)
return { success: true } return { success: true }
} catch (error) { } catch (error) {

View File

@@ -45,10 +45,6 @@ export const viewport: Viewport = {
export default async function RootLayout({ children }: { children: React.ReactNode }) { export default async function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en"> <html lang="en">
<head>
<meta name="theme-color" content="#ffffff" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
</head>
<body className="min-h-screen bg-white antialiased">{children}</body> <body className="min-h-screen bg-white antialiased">{children}</body>
</html> </html>
) )

View File

@@ -1,5 +1,6 @@
"use client" "use client"
import { formatBytes } from "@/lib/utils"
import { File } from "@prisma/client" import { File } from "@prisma/client"
import Image from "next/image" import Image from "next/image"
import Link from "next/link" import Link from "next/link"
@@ -9,9 +10,7 @@ export function FilePreview({ file }: { file: File }) {
const [isEnlarged, setIsEnlarged] = useState(false) const [isEnlarged, setIsEnlarged] = useState(false)
const fileSize = const fileSize =
file.metadata && typeof file.metadata === "object" && "size" in file.metadata file.metadata && typeof file.metadata === "object" && "size" in file.metadata ? Number(file.metadata.size) : 0
? Number(file.metadata.size) / 1024 / 1024
: 0
return ( return (
<> <>
@@ -45,7 +44,7 @@ export function FilePreview({ file }: { file: File }) {
<strong>Uploaded:</strong> {format(file.createdAt, "MMM d, yyyy")} <strong>Uploaded:</strong> {format(file.createdAt, "MMM d, yyyy")}
</p> */} </p> */}
<p className="text-sm"> <p className="text-sm">
<strong>Size:</strong> {fileSize < 1 ? (fileSize * 1024).toFixed(2) + " KB" : fileSize.toFixed(2) + " MB"} <strong>Size:</strong> {formatBytes(fileSize)}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -10,7 +10,8 @@ import {
import { SidebarMenuButton } from "@/components/ui/sidebar" import { SidebarMenuButton } from "@/components/ui/sidebar"
import { UserProfile } from "@/lib/auth" import { UserProfile } from "@/lib/auth"
import { authClient } from "@/lib/auth-client" import { authClient } from "@/lib/auth-client"
import { LogOut, MoreVertical, User } from "lucide-react" import { formatBytes } from "@/lib/utils"
import { HardDrive, LogOut, MoreVertical, User } from "lucide-react"
import Link from "next/link" import Link from "next/link"
import { redirect } from "next/navigation" import { redirect } from "next/navigation"
@@ -59,6 +60,13 @@ export default function SidebarUser({ profile, isSelfHosted }: { profile: UserPr
Your Subscription Your Subscription
</Link> </Link>
</DropdownMenuItem> */} </DropdownMenuItem> */}
<DropdownMenuItem asChild>
<Link href="/settings/profile" className="flex items-center gap-2">
<HardDrive className="h-4 w-4" />
Storage: {profile.storageUsed ? formatBytes(profile.storageUsed) : "N/A"}
</Link>
</DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
{!isSelfHosted && ( {!isSelfHosted && (
<> <>

View File

@@ -17,6 +17,7 @@ export type UserProfile = {
name: string name: string
email: string email: string
avatar?: string avatar?: string
storageUsed?: number
} }
export const auth = betterAuth({ export const auth = betterAuth({

View File

@@ -1,5 +1,5 @@
import { File, Transaction, User } from "@prisma/client" import { File, Transaction, User } from "@prisma/client"
import { access, constants } from "fs/promises" import { access, constants, readdir, stat } from "fs/promises"
import path from "path" import path from "path"
export const FILE_UPLOAD_PATH = path.resolve(process.env.UPLOAD_PATH || "./uploads") export const FILE_UPLOAD_PATH = path.resolve(process.env.UPLOAD_PATH || "./uploads")
@@ -52,3 +52,21 @@ export async function fileExists(filePath: string) {
return false return false
} }
} }
export async function getDirectorySize(directoryPath: string) {
let totalSize = 0
async function calculateSize(dir: string) {
const files = await readdir(dir, { withFileTypes: true })
for (const file of files) {
const fullPath = path.join(dir, file.name)
if (file.isDirectory()) {
await calculateSize(fullPath)
} else if (file.isFile()) {
const stats = await stat(fullPath)
totalSize += stats.size
}
}
}
await calculateSize(directoryPath)
return totalSize
}

View File

@@ -16,6 +16,18 @@ export function formatCurrency(total: number, currency: string) {
}).format(total / 100) }).format(total / 100)
} }
export function formatBytes(bytes: number) {
if (bytes === 0) return "0 Bytes"
const sizes = ["Bytes", "KB", "MB", "GB"]
const maxIndex = sizes.length - 1
const i = Math.min(Math.floor(Math.log10(bytes) / Math.log10(1024)), maxIndex)
const value = bytes / Math.pow(1024, i)
return `${parseFloat(value.toFixed(2))} ${sizes[i]}`
}
export function codeFromName(name: string, maxLength: number = 16) { export function codeFromName(name: string, maxLength: number = 16) {
const code = slugify(name, { const code = slugify(name, {
replacement: "_", replacement: "_",

View File

@@ -0,0 +1,10 @@
/*
Warnings:
- You are about to drop the column `image` on the `users` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "users" DROP COLUMN "image",
ADD COLUMN "storage_used" INTEGER DEFAULT 0,
ADD COLUMN "token_balance" INTEGER DEFAULT 0;

View File

@@ -26,12 +26,11 @@ model User {
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
membershipPlan String? @map("membership_plan") membershipPlan String? @map("membership_plan")
membershipExpiresAt DateTime? @map("membership_expires_at") membershipExpiresAt DateTime? @map("membership_expires_at")
sessions Session[]
emailVerified Boolean @default(false) @map("is_email_verified") emailVerified Boolean @default(false) @map("is_email_verified")
image String? storageUsed Int? @default(0) @map("storage_used")
tokenBalance Int? @default(0) @map("token_balance")
accounts Account[] accounts Account[]
sessions Session[]
@@map("users") @@map("users")
} }