diff options
| -rw-r--r-- | frontend/app/api/storage/route.ts | 80 | ||||
| -rw-r--r-- | frontend/app/drive/[...path]/page.tsx | 29 | ||||
| -rw-r--r-- | frontend/app/drive/page.tsx | 29 | ||||
| -rw-r--r-- | frontend/components/drive/DriveDirectoryClient.tsx | 30 | ||||
| -rw-r--r-- | frontend/components/drive/DriveDirectoryView.tsx | 11 | ||||
| -rw-r--r-- | frontend/components/drive/StorageUsage.tsx | 78 |
6 files changed, 233 insertions, 24 deletions
diff --git a/frontend/app/api/storage/route.ts b/frontend/app/api/storage/route.ts new file mode 100644 index 0000000..66f2817 --- /dev/null +++ b/frontend/app/api/storage/route.ts | |||
| @@ -0,0 +1,80 @@ | |||
| 1 | import { NextResponse } from 'next/server' | ||
| 2 | import { exec } from 'child_process' | ||
| 3 | import { promisify } from 'util' | ||
| 4 | |||
| 5 | const execAsync = promisify(exec) | ||
| 6 | |||
| 7 | // Cache for 10 seconds | ||
| 8 | let cacheTimestamp = 0 | ||
| 9 | let cachedData: StorageData | null = null | ||
| 10 | |||
| 11 | interface StorageData { | ||
| 12 | activeDriveUsage: number | ||
| 13 | totalDiskCapacity: number | ||
| 14 | totalDiskUsed: number | ||
| 15 | availableDisk: number | ||
| 16 | } | ||
| 17 | |||
| 18 | async function fetchStorageData(): Promise<StorageData> { | ||
| 19 | const now = Date.now() | ||
| 20 | |||
| 21 | // Return cached data if less than 10 seconds old | ||
| 22 | if (cachedData && now - cacheTimestamp < 10000) { | ||
| 23 | return cachedData | ||
| 24 | } | ||
| 25 | |||
| 26 | try { | ||
| 27 | // Get active drive usage in bytes | ||
| 28 | const { stdout: driveSize } = await execAsync('fctdrive drive-size') | ||
| 29 | const activeDriveUsage = parseInt(driveSize.trim()) || 0 | ||
| 30 | |||
| 31 | // Get disk usage in bytes | ||
| 32 | const fctdrivePath = process.env.FCTDRIVE_PATH || '/home/diogo464/dev/fctdrive' | ||
| 33 | const { stdout: dfOutput } = await execAsync(`df -B1 "${fctdrivePath}"`) | ||
| 34 | |||
| 35 | // Parse df output - second line contains the data | ||
| 36 | const lines = dfOutput.trim().split('\n') | ||
| 37 | const dataLine = lines[1] // Skip header line | ||
| 38 | const columns = dataLine.split(/\s+/) | ||
| 39 | |||
| 40 | const totalDiskCapacity = parseInt(columns[1]) || 0 | ||
| 41 | const totalDiskUsed = parseInt(columns[2]) || 0 | ||
| 42 | const availableDisk = parseInt(columns[3]) || 0 | ||
| 43 | |||
| 44 | const data: StorageData = { | ||
| 45 | activeDriveUsage, | ||
| 46 | totalDiskCapacity, | ||
| 47 | totalDiskUsed, | ||
| 48 | availableDisk | ||
| 49 | } | ||
| 50 | |||
| 51 | // Update cache | ||
| 52 | cachedData = data | ||
| 53 | cacheTimestamp = now | ||
| 54 | |||
| 55 | return data | ||
| 56 | } catch (error) { | ||
| 57 | console.error('Failed to fetch storage data:', error) | ||
| 58 | |||
| 59 | // Return zeros on error as requested | ||
| 60 | return { | ||
| 61 | activeDriveUsage: 0, | ||
| 62 | totalDiskCapacity: 0, | ||
| 63 | totalDiskUsed: 0, | ||
| 64 | availableDisk: 0 | ||
| 65 | } | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | export async function GET() { | ||
| 70 | try { | ||
| 71 | const data = await fetchStorageData() | ||
| 72 | return NextResponse.json(data) | ||
| 73 | } catch (error) { | ||
| 74 | console.error('Storage API error:', error) | ||
| 75 | return NextResponse.json( | ||
| 76 | { error: 'Failed to fetch storage data' }, | ||
| 77 | { status: 500 } | ||
| 78 | ) | ||
| 79 | } | ||
| 80 | } \ No newline at end of file | ||
diff --git a/frontend/app/drive/[...path]/page.tsx b/frontend/app/drive/[...path]/page.tsx index 8380a30..bfb65ad 100644 --- a/frontend/app/drive/[...path]/page.tsx +++ b/frontend/app/drive/[...path]/page.tsx | |||
| @@ -1,6 +1,27 @@ | |||
| 1 | import { DriveDirectoryView } from "@/components/drive/DriveDirectoryView" | 1 | import { DriveDirectoryView } from "@/components/drive/DriveDirectoryView" |
| 2 | import { Drive_ls } from "@/lib/drive_server" | 2 | import { Drive_ls } from "@/lib/drive_server" |
| 3 | 3 | ||
| 4 | async function fetchStorageData() { | ||
| 5 | try { | ||
| 6 | const response = await fetch(`${process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:3000' : ''}/api/storage`, { | ||
| 7 | cache: 'no-store' | ||
| 8 | }) | ||
| 9 | if (!response.ok) { | ||
| 10 | throw new Error('Failed to fetch storage data') | ||
| 11 | } | ||
| 12 | return await response.json() | ||
| 13 | } catch (error) { | ||
| 14 | console.error('Failed to fetch storage data:', error) | ||
| 15 | // Return zeros on error as requested | ||
| 16 | return { | ||
| 17 | activeDriveUsage: 0, | ||
| 18 | totalDiskCapacity: 0, | ||
| 19 | totalDiskUsed: 0, | ||
| 20 | availableDisk: 0 | ||
| 21 | } | ||
| 22 | } | ||
| 23 | } | ||
| 24 | |||
| 4 | export default async function DriveDirectoryPage({ | 25 | export default async function DriveDirectoryPage({ |
| 5 | params, | 26 | params, |
| 6 | }: { | 27 | }: { |
| @@ -11,6 +32,10 @@ export default async function DriveDirectoryPage({ | |||
| 11 | const decodedSegments = pathSegments?.map(segment => decodeURIComponent(segment)) || [] | 32 | const decodedSegments = pathSegments?.map(segment => decodeURIComponent(segment)) || [] |
| 12 | const currentPath = '/' + decodedSegments.join('/') | 33 | const currentPath = '/' + decodedSegments.join('/') |
| 13 | 34 | ||
| 14 | const files = await Drive_ls(currentPath, false) | 35 | const [files, storageData] = await Promise.all([ |
| 15 | return <DriveDirectoryView path={currentPath} files={files} /> | 36 | Drive_ls(currentPath, false), |
| 37 | fetchStorageData() | ||
| 38 | ]) | ||
| 39 | |||
| 40 | return <DriveDirectoryView path={currentPath} files={files} storageData={storageData} /> | ||
| 16 | } \ No newline at end of file | 41 | } \ No newline at end of file |
diff --git a/frontend/app/drive/page.tsx b/frontend/app/drive/page.tsx index 0e3fd0c..ea5846e 100644 --- a/frontend/app/drive/page.tsx +++ b/frontend/app/drive/page.tsx | |||
| @@ -1,7 +1,32 @@ | |||
| 1 | import { DriveDirectoryView } from "@/components/drive/DriveDirectoryView" | 1 | import { DriveDirectoryView } from "@/components/drive/DriveDirectoryView" |
| 2 | import { Drive_ls } from "@/lib/drive_server" | 2 | import { Drive_ls } from "@/lib/drive_server" |
| 3 | 3 | ||
| 4 | async function fetchStorageData() { | ||
| 5 | try { | ||
| 6 | const response = await fetch(`${process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:3000' : ''}/api/storage`, { | ||
| 7 | cache: 'no-store' | ||
| 8 | }) | ||
| 9 | if (!response.ok) { | ||
| 10 | throw new Error('Failed to fetch storage data') | ||
| 11 | } | ||
| 12 | return await response.json() | ||
| 13 | } catch (error) { | ||
| 14 | console.error('Failed to fetch storage data:', error) | ||
| 15 | // Return zeros on error as requested | ||
| 16 | return { | ||
| 17 | activeDriveUsage: 0, | ||
| 18 | totalDiskCapacity: 0, | ||
| 19 | totalDiskUsed: 0, | ||
| 20 | availableDisk: 0 | ||
| 21 | } | ||
| 22 | } | ||
| 23 | } | ||
| 24 | |||
| 4 | export default async function DriveRootPage() { | 25 | export default async function DriveRootPage() { |
| 5 | const files = await Drive_ls("/", false) | 26 | const [files, storageData] = await Promise.all([ |
| 6 | return <DriveDirectoryView path="/" files={files} /> | 27 | Drive_ls("/", false), |
| 28 | fetchStorageData() | ||
| 29 | ]) | ||
| 30 | |||
| 31 | return <DriveDirectoryView path="/" files={files} storageData={storageData} /> | ||
| 7 | } \ No newline at end of file | 32 | } \ No newline at end of file |
diff --git a/frontend/components/drive/DriveDirectoryClient.tsx b/frontend/components/drive/DriveDirectoryClient.tsx index c405341..6877b9b 100644 --- a/frontend/components/drive/DriveDirectoryClient.tsx +++ b/frontend/components/drive/DriveDirectoryClient.tsx | |||
| @@ -21,7 +21,6 @@ import { | |||
| 21 | } from "lucide-react" | 21 | } from "lucide-react" |
| 22 | import { Button } from "@/components/ui/button" | 22 | import { Button } from "@/components/ui/button" |
| 23 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" | 23 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" |
| 24 | import { Progress } from "@/components/ui/progress" | ||
| 25 | import { | 24 | import { |
| 26 | DropdownMenu, | 25 | DropdownMenu, |
| 27 | DropdownMenuContent, | 26 | DropdownMenuContent, |
| @@ -37,6 +36,7 @@ import { toast } from "@/hooks/use-toast" | |||
| 37 | import { DriveLsEntry } from "@/lib/drive_types" | 36 | import { DriveLsEntry } from "@/lib/drive_types" |
| 38 | import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from "@/lib/constants" | 37 | import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from "@/lib/constants" |
| 39 | import { DriveMoveDialog } from "./DriveMoveDialog" | 38 | import { DriveMoveDialog } from "./DriveMoveDialog" |
| 39 | import { StorageUsage } from "./StorageUsage" | ||
| 40 | 40 | ||
| 41 | function formatFileSize(bytes: number): string { | 41 | function formatFileSize(bytes: number): string { |
| 42 | if (bytes === 0) return "0 Bytes" | 42 | if (bytes === 0) return "0 Bytes" |
| @@ -67,13 +67,21 @@ interface Breadcrumb { | |||
| 67 | path: string | 67 | path: string |
| 68 | } | 68 | } |
| 69 | 69 | ||
| 70 | interface StorageData { | ||
| 71 | activeDriveUsage: number | ||
| 72 | totalDiskCapacity: number | ||
| 73 | totalDiskUsed: number | ||
| 74 | availableDisk: number | ||
| 75 | } | ||
| 76 | |||
| 70 | interface DriveDirectoryClientProps { | 77 | interface DriveDirectoryClientProps { |
| 71 | path: string | 78 | path: string |
| 72 | files: DriveLsEntry[] | 79 | files: DriveLsEntry[] |
| 73 | breadcrumbs: Breadcrumb[] | 80 | breadcrumbs: Breadcrumb[] |
| 81 | storageData: StorageData | ||
| 74 | } | 82 | } |
| 75 | 83 | ||
| 76 | export function DriveDirectoryClient({ path, files, breadcrumbs }: DriveDirectoryClientProps) { | 84 | export function DriveDirectoryClient({ path, files, breadcrumbs, storageData }: DriveDirectoryClientProps) { |
| 77 | const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set()) | 85 | const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set()) |
| 78 | const [renameDialogOpen, setRenameDialogOpen] = useState(false) | 86 | const [renameDialogOpen, setRenameDialogOpen] = useState(false) |
| 79 | const [infoDialogOpen, setInfoDialogOpen] = useState(false) | 87 | const [infoDialogOpen, setInfoDialogOpen] = useState(false) |
| @@ -87,10 +95,6 @@ export function DriveDirectoryClient({ path, files, breadcrumbs }: DriveDirector | |||
| 87 | 95 | ||
| 88 | const [isLoggedIn, setIsLoggedIn] = useState(true) // Mock logged in state | 96 | const [isLoggedIn, setIsLoggedIn] = useState(true) // Mock logged in state |
| 89 | 97 | ||
| 90 | const maxStorage = 1073741824 // 1GB | ||
| 91 | const usedStorage = 0 // TODO: Calculate from files if needed | ||
| 92 | const storagePercentage = (usedStorage / maxStorage) * 100 | ||
| 93 | |||
| 94 | const toggleFileSelection = (filePath: string) => { | 98 | const toggleFileSelection = (filePath: string) => { |
| 95 | const newSelected = new Set(selectedFiles) | 99 | const newSelected = new Set(selectedFiles) |
| 96 | if (newSelected.has(filePath)) { | 100 | if (newSelected.has(filePath)) { |
| @@ -438,19 +442,7 @@ export function DriveDirectoryClient({ path, files, breadcrumbs }: DriveDirector | |||
| 438 | </div> | 442 | </div> |
| 439 | 443 | ||
| 440 | {/* Storage Info */} | 444 | {/* Storage Info */} |
| 441 | <div className="bg-card rounded-lg border p-4"> | 445 | <StorageUsage data={storageData} /> |
| 442 | <div className="flex items-center justify-between mb-2"> | ||
| 443 | <span className="text-sm font-medium">Storage Usage</span> | ||
| 444 | <span className="text-sm text-muted-foreground"> | ||
| 445 | {formatFileSize(usedStorage)} of {formatFileSize(maxStorage)} used | ||
| 446 | </span> | ||
| 447 | </div> | ||
| 448 | <Progress value={storagePercentage} className="h-2" /> | ||
| 449 | <div className="flex justify-between text-xs text-muted-foreground mt-1"> | ||
| 450 | <span>{storagePercentage.toFixed(1)}% used</span> | ||
| 451 | <span>{formatFileSize(maxStorage - usedStorage)} available</span> | ||
| 452 | </div> | ||
| 453 | </div> | ||
| 454 | 446 | ||
| 455 | {/* Bulk Actions */} | 447 | {/* Bulk Actions */} |
| 456 | {selectedFiles.size > 0 && ( | 448 | {selectedFiles.size > 0 && ( |
diff --git a/frontend/components/drive/DriveDirectoryView.tsx b/frontend/components/drive/DriveDirectoryView.tsx index 20ed9d8..d93dce8 100644 --- a/frontend/components/drive/DriveDirectoryView.tsx +++ b/frontend/components/drive/DriveDirectoryView.tsx | |||
| @@ -1,9 +1,17 @@ | |||
| 1 | import { DriveLsEntry } from "@/lib/drive_types" | 1 | import { DriveLsEntry } from "@/lib/drive_types" |
| 2 | import { DriveDirectoryClient } from "./DriveDirectoryClient" | 2 | import { DriveDirectoryClient } from "./DriveDirectoryClient" |
| 3 | 3 | ||
| 4 | interface StorageData { | ||
| 5 | activeDriveUsage: number | ||
| 6 | totalDiskCapacity: number | ||
| 7 | totalDiskUsed: number | ||
| 8 | availableDisk: number | ||
| 9 | } | ||
| 10 | |||
| 4 | interface DriveDirectoryViewProps { | 11 | interface DriveDirectoryViewProps { |
| 5 | path: string | 12 | path: string |
| 6 | files: DriveLsEntry[] | 13 | files: DriveLsEntry[] |
| 14 | storageData: StorageData | ||
| 7 | } | 15 | } |
| 8 | 16 | ||
| 9 | // Generate breadcrumbs from path | 17 | // Generate breadcrumbs from path |
| @@ -41,7 +49,7 @@ function sortFiles(files: DriveLsEntry[]): DriveLsEntry[] { | |||
| 41 | }); | 49 | }); |
| 42 | } | 50 | } |
| 43 | 51 | ||
| 44 | export function DriveDirectoryView({ path, files }: DriveDirectoryViewProps) { | 52 | export function DriveDirectoryView({ path, files, storageData }: DriveDirectoryViewProps) { |
| 45 | const sortedFiles = sortFiles(files) | 53 | const sortedFiles = sortFiles(files) |
| 46 | const breadcrumbs = generateBreadcrumbs(path) | 54 | const breadcrumbs = generateBreadcrumbs(path) |
| 47 | 55 | ||
| @@ -50,6 +58,7 @@ export function DriveDirectoryView({ path, files }: DriveDirectoryViewProps) { | |||
| 50 | path={path} | 58 | path={path} |
| 51 | files={sortedFiles} | 59 | files={sortedFiles} |
| 52 | breadcrumbs={breadcrumbs} | 60 | breadcrumbs={breadcrumbs} |
| 61 | storageData={storageData} | ||
| 53 | /> | 62 | /> |
| 54 | ) | 63 | ) |
| 55 | } \ No newline at end of file | 64 | } \ No newline at end of file |
diff --git a/frontend/components/drive/StorageUsage.tsx b/frontend/components/drive/StorageUsage.tsx new file mode 100644 index 0000000..a5a1d68 --- /dev/null +++ b/frontend/components/drive/StorageUsage.tsx | |||
| @@ -0,0 +1,78 @@ | |||
| 1 | interface StorageData { | ||
| 2 | activeDriveUsage: number | ||
| 3 | totalDiskCapacity: number | ||
| 4 | totalDiskUsed: number | ||
| 5 | availableDisk: number | ||
| 6 | } | ||
| 7 | |||
| 8 | interface StorageUsageProps { | ||
| 9 | data: StorageData | ||
| 10 | } | ||
| 11 | |||
| 12 | function formatFileSize(bytes: number): string { | ||
| 13 | if (bytes === 0) return "0 Bytes" | ||
| 14 | const k = 1024 | ||
| 15 | const sizes = ["Bytes", "KB", "MB", "GB", "TB"] | ||
| 16 | const i = Math.floor(Math.log(bytes) / Math.log(k)) | ||
| 17 | return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] | ||
| 18 | } | ||
| 19 | |||
| 20 | export function StorageUsage({ data }: StorageUsageProps) { | ||
| 21 | // Safety check for undefined data | ||
| 22 | if (!data) { | ||
| 23 | return ( | ||
| 24 | <div className="bg-card rounded-lg border p-4"> | ||
| 25 | <div className="flex items-center justify-between mb-2"> | ||
| 26 | <span className="text-sm font-medium">Storage Usage</span> | ||
| 27 | <span className="text-sm text-muted-foreground">Loading...</span> | ||
| 28 | </div> | ||
| 29 | <div className="w-full h-2 bg-gray-200 rounded-full" /> | ||
| 30 | </div> | ||
| 31 | ) | ||
| 32 | } | ||
| 33 | |||
| 34 | const { activeDriveUsage, totalDiskCapacity, totalDiskUsed } = data | ||
| 35 | |||
| 36 | // Calculate percentages based on total disk capacity | ||
| 37 | const activeDrivePercentage = totalDiskCapacity > 0 ? (activeDriveUsage / totalDiskCapacity) * 100 : 0 | ||
| 38 | const totalUsedPercentage = totalDiskCapacity > 0 ? (totalDiskUsed / totalDiskCapacity) * 100 : 0 | ||
| 39 | |||
| 40 | return ( | ||
| 41 | <div className="bg-card rounded-lg border p-4"> | ||
| 42 | <div className="flex items-center justify-between mb-2"> | ||
| 43 | <span className="text-sm font-medium">Storage Usage</span> | ||
| 44 | <span className="text-sm text-muted-foreground"> | ||
| 45 | {formatFileSize(activeDriveUsage)} active / {formatFileSize(totalDiskUsed)} total of {formatFileSize(totalDiskCapacity)} | ||
| 46 | </span> | ||
| 47 | </div> | ||
| 48 | |||
| 49 | {/* Custom two-color progress bar */} | ||
| 50 | <div className="relative w-full h-2 bg-gray-200 rounded-full overflow-hidden"> | ||
| 51 | {/* Total disk usage (gray background layer) */} | ||
| 52 | <div | ||
| 53 | className="absolute top-0 left-0 h-full bg-gray-400 rounded-full transition-all duration-300" | ||
| 54 | style={{ width: `${Math.min(totalUsedPercentage, 100)}%` }} | ||
| 55 | /> | ||
| 56 | {/* Active drive usage (blue foreground layer) */} | ||
| 57 | <div | ||
| 58 | className="absolute top-0 left-0 h-full bg-blue-600 rounded-full transition-all duration-300" | ||
| 59 | style={{ width: `${Math.min(activeDrivePercentage, 100)}%` }} | ||
| 60 | /> | ||
| 61 | </div> | ||
| 62 | |||
| 63 | <div className="flex justify-between text-xs text-muted-foreground mt-2"> | ||
| 64 | <div className="flex items-center gap-4"> | ||
| 65 | <div className="flex items-center gap-1"> | ||
| 66 | <div className="w-2 h-2 bg-blue-600 rounded-full" /> | ||
| 67 | <span>Active Drive: {formatFileSize(activeDriveUsage)}</span> | ||
| 68 | </div> | ||
| 69 | <div className="flex items-center gap-1"> | ||
| 70 | <div className="w-2 h-2 bg-gray-400 rounded-full" /> | ||
| 71 | <span>Other Usage: {formatFileSize(totalDiskUsed - activeDriveUsage)}</span> | ||
| 72 | </div> | ||
| 73 | </div> | ||
| 74 | <span>{formatFileSize(totalDiskCapacity - totalDiskUsed)} available</span> | ||
| 75 | </div> | ||
| 76 | </div> | ||
| 77 | ) | ||
| 78 | } \ No newline at end of file | ||
