mirror of
https://github.com/marcogll/TaxHacker_s23.git
synced 2026-01-13 21:35:19 +00:00
feat: calculate used storage
This commit is contained in:
@@ -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 }
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
20
lib/files.ts
20
lib/files.ts
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
12
lib/utils.ts
12
lib/utils.ts
@@ -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: "_",
|
||||||
|
|||||||
10
prisma/migrations/20250410130313_add_storage/migration.sql
Normal file
10
prisma/migrations/20250410130313_add_storage/migration.sql
Normal 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;
|
||||||
@@ -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")
|
||||||
|
emailVerified Boolean @default(false) @map("is_email_verified")
|
||||||
sessions Session[]
|
storageUsed Int? @default(0) @map("storage_used")
|
||||||
|
tokenBalance Int? @default(0) @map("token_balance")
|
||||||
emailVerified Boolean @default(false) @map("is_email_verified")
|
accounts Account[]
|
||||||
image String?
|
sessions Session[]
|
||||||
accounts Account[]
|
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user