From 62bad46e58bcabc66d6dde961213599347d4f2ce Mon Sep 17 00:00:00 2001 From: Vasily Zubarev Date: Thu, 10 Apr 2025 15:14:54 +0200 Subject: [PATCH] feat: calculate used storage --- ai/analyze.ts | 1 + app/(app)/files/actions.ts | 7 ++++++- app/(app)/layout.tsx | 15 ++++++++------ app/(app)/transactions/actions.ts | 12 ++++++++++- app/layout.tsx | 4 ---- components/files/preview.tsx | 7 +++---- components/sidebar/sidebar-user.tsx | 10 +++++++++- lib/auth.ts | 1 + lib/files.ts | 20 ++++++++++++++++++- lib/utils.ts | 12 +++++++++++ .../20250410130313_add_storage/migration.sql | 10 ++++++++++ prisma/migrations/migration_lock.toml | 2 +- prisma/schema.prisma | 11 +++++----- 13 files changed, 87 insertions(+), 25 deletions(-) create mode 100644 prisma/migrations/20250410130313_add_storage/migration.sql diff --git a/ai/analyze.ts b/ai/analyze.ts index 7b97f19..b64b93b 100644 --- a/ai/analyze.ts +++ b/ai/analyze.ts @@ -45,6 +45,7 @@ export async function analyzeTransaction( }) console.log("ChatGPT response:", response.output_text) + console.log("ChatGPT tokens used:", response.usage) const result = JSON.parse(response.output_text) return { success: true, data: result } diff --git a/app/(app)/files/actions.ts b/app/(app)/files/actions.ts index 355e62e..1d8d2e8 100644 --- a/app/(app)/files/actions.ts +++ b/app/(app)/files/actions.ts @@ -2,8 +2,9 @@ import { ActionState } from "@/lib/actions" import { getCurrentUser } from "@/lib/auth" -import { getUserUploadsDirectory, unsortedFilePath } from "@/lib/files" +import { getDirectorySize, getUserUploadsDirectory, unsortedFilePath } from "@/lib/files" import { createFile } from "@/models/files" +import { updateUser } from "@/models/users" import { randomUUID } from "crypto" import { mkdir, writeFile } from "fs/promises" import { revalidatePath } from "next/cache" @@ -50,6 +51,10 @@ export async function uploadFilesAction(formData: FormData): Promise diff --git a/app/(app)/transactions/actions.ts b/app/(app)/transactions/actions.ts index 328ac6c..4b71cba 100644 --- a/app/(app)/transactions/actions.ts +++ b/app/(app)/transactions/actions.ts @@ -3,7 +3,7 @@ import { transactionFormSchema } from "@/forms/transactions" import { ActionState } from "@/lib/actions" import { getCurrentUser } from "@/lib/auth" -import { getTransactionFileUploadPath, getUserUploadsDirectory } from "@/lib/files" +import { getDirectorySize, getTransactionFileUploadPath, getUserUploadsDirectory } from "@/lib/files" import { updateField } from "@/models/fields" import { createFile, deleteFile } from "@/models/files" import { @@ -14,6 +14,7 @@ import { updateTransaction, updateTransactionFiles, } from "@/models/transactions" +import { updateUser } from "@/models/users" import { Transaction } from "@prisma/client" import { randomUUID } from "crypto" import { mkdir, writeFile } from "fs/promises" @@ -106,6 +107,11 @@ export async function deleteTransactionFileAction( ) await deleteFile(fileId, user.id) + + // Update user storage used + const storageUsed = await getDirectorySize(await getUserUploadsDirectory(user)) + await updateUser(user.id, { storageUsed }) + revalidatePath(`/transactions/${transactionId}`) return { success: true, data: transaction } } @@ -169,6 +175,10 @@ export async function uploadTransactionFilesAction(formData: FormData): Promise< : fileRecords.map((file) => file.id) ) + // Update user storage used + const storageUsed = await getDirectorySize(await getUserUploadsDirectory(user)) + await updateUser(user.id, { storageUsed }) + revalidatePath(`/transactions/${transactionId}`) return { success: true } } catch (error) { diff --git a/app/layout.tsx b/app/layout.tsx index cf83a54..8bd34f5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -45,10 +45,6 @@ export const viewport: Viewport = { export default async function RootLayout({ children }: { children: React.ReactNode }) { return ( - - - - {children} ) diff --git a/components/files/preview.tsx b/components/files/preview.tsx index c52bd4c..d387ee0 100644 --- a/components/files/preview.tsx +++ b/components/files/preview.tsx @@ -1,5 +1,6 @@ "use client" +import { formatBytes } from "@/lib/utils" import { File } from "@prisma/client" import Image from "next/image" import Link from "next/link" @@ -9,9 +10,7 @@ export function FilePreview({ file }: { file: File }) { const [isEnlarged, setIsEnlarged] = useState(false) const fileSize = - file.metadata && typeof file.metadata === "object" && "size" in file.metadata - ? Number(file.metadata.size) / 1024 / 1024 - : 0 + file.metadata && typeof file.metadata === "object" && "size" in file.metadata ? Number(file.metadata.size) : 0 return ( <> @@ -45,7 +44,7 @@ export function FilePreview({ file }: { file: File }) { Uploaded: {format(file.createdAt, "MMM d, yyyy")}

*/}

- Size: {fileSize < 1 ? (fileSize * 1024).toFixed(2) + " KB" : fileSize.toFixed(2) + " MB"} + Size: {formatBytes(fileSize)}

diff --git a/components/sidebar/sidebar-user.tsx b/components/sidebar/sidebar-user.tsx index 97b63b7..b4ef6b7 100644 --- a/components/sidebar/sidebar-user.tsx +++ b/components/sidebar/sidebar-user.tsx @@ -10,7 +10,8 @@ import { import { SidebarMenuButton } from "@/components/ui/sidebar" import { UserProfile } from "@/lib/auth" 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 { redirect } from "next/navigation" @@ -59,6 +60,13 @@ export default function SidebarUser({ profile, isSelfHosted }: { profile: UserPr Your Subscription */} + + + + + Storage: {profile.storageUsed ? formatBytes(profile.storageUsed) : "N/A"} + + {!isSelfHosted && ( <> diff --git a/lib/auth.ts b/lib/auth.ts index fe753d1..400089e 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -17,6 +17,7 @@ export type UserProfile = { name: string email: string avatar?: string + storageUsed?: number } export const auth = betterAuth({ diff --git a/lib/files.ts b/lib/files.ts index af34932..0a6e235 100644 --- a/lib/files.ts +++ b/lib/files.ts @@ -1,5 +1,5 @@ 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" export const FILE_UPLOAD_PATH = path.resolve(process.env.UPLOAD_PATH || "./uploads") @@ -52,3 +52,21 @@ export async function fileExists(filePath: string) { 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 +} diff --git a/lib/utils.ts b/lib/utils.ts index 5804ca7..346e6a4 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -16,6 +16,18 @@ export function formatCurrency(total: number, currency: string) { }).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) { const code = slugify(name, { replacement: "_", diff --git a/prisma/migrations/20250410130313_add_storage/migration.sql b/prisma/migrations/20250410130313_add_storage/migration.sql new file mode 100644 index 0000000..b821f39 --- /dev/null +++ b/prisma/migrations/20250410130313_add_storage/migration.sql @@ -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; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml index 648c57f..044d57c 100644 --- a/prisma/migrations/migration_lock.toml +++ b/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually # It should be added in your version-control system (e.g., Git) -provider = "postgresql" \ No newline at end of file +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 238e7ee..0531aac 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -26,12 +26,11 @@ model User { updatedAt DateTime @updatedAt @map("updated_at") membershipPlan String? @map("membership_plan") membershipExpiresAt DateTime? @map("membership_expires_at") - - sessions Session[] - - emailVerified Boolean @default(false) @map("is_email_verified") - image String? - accounts Account[] + emailVerified Boolean @default(false) @map("is_email_verified") + storageUsed Int? @default(0) @map("storage_used") + tokenBalance Int? @default(0) @map("token_balance") + accounts Account[] + sessions Session[] @@map("users") }