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")
}