diff options
| author | diogo464 <[email protected]> | 2025-08-14 21:11:48 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-08-14 21:13:57 +0100 |
| commit | e4917874be67de24f934e069b53e1726599c6cc5 (patch) | |
| tree | 92079699e1f17e0a088d110f6ab19fb093ba3adf | |
| parent | f1edb3ce07ce5569718f6a899679e24f45349ce2 (diff) | |
frontend: improvements
| -rw-r--r-- | frontend/app/api/logout/route.ts | 4 | ||||
| -rw-r--r-- | frontend/app/api/upload/route.ts | 4 | ||||
| -rw-r--r-- | frontend/components/drive/DriveDirectoryClient.tsx | 88 | ||||
| -rw-r--r-- | frontend/components/drive/DriveDirectoryView.tsx | 7 | ||||
| -rw-r--r-- | frontend/components/drive/StorageUsage.tsx | 9 | ||||
| -rw-r--r-- | frontend/components/history/HistoryView.tsx | 9 | ||||
| -rw-r--r-- | frontend/lib/constants.ts | 37 | ||||
| -rw-r--r-- | frontend/lib/drive_server.ts | 83 | ||||
| -rw-r--r-- | frontend/package.json | 5 |
9 files changed, 67 insertions, 179 deletions
diff --git a/frontend/app/api/logout/route.ts b/frontend/app/api/logout/route.ts index 51de324..202a8b7 100644 --- a/frontend/app/api/logout/route.ts +++ b/frontend/app/api/logout/route.ts | |||
| @@ -10,7 +10,7 @@ export async function POST(request: NextRequest) { | |||
| 10 | 10 | ||
| 11 | if (sessionCookie) { | 11 | if (sessionCookie) { |
| 12 | // Call tinyauth logout endpoint to invalidate the session | 12 | // Call tinyauth logout endpoint to invalidate the session |
| 13 | const logoutResponse = await fetch(`${Auth_tinyauth_endpoint()}/auth/logout`, { | 13 | await fetch(`${Auth_tinyauth_endpoint()}/auth/logout`, { |
| 14 | method: 'POST', | 14 | method: 'POST', |
| 15 | headers: { | 15 | headers: { |
| 16 | 'Cookie': `${sessionCookie.name}=${sessionCookie.value}` | 16 | 'Cookie': `${sessionCookie.name}=${sessionCookie.value}` |
| @@ -29,4 +29,4 @@ export async function POST(request: NextRequest) { | |||
| 29 | // Even if logout fails, redirect to home | 29 | // Even if logout fails, redirect to home |
| 30 | return redirect('/'); | 30 | return redirect('/'); |
| 31 | } | 31 | } |
| 32 | } \ No newline at end of file | 32 | } |
diff --git a/frontend/app/api/upload/route.ts b/frontend/app/api/upload/route.ts index 5ce0640..ba2d151 100644 --- a/frontend/app/api/upload/route.ts +++ b/frontend/app/api/upload/route.ts | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | import { NextRequest, NextResponse } from 'next/server' | 1 | import { NextRequest, NextResponse } from 'next/server' |
| 2 | import { writeFile, unlink } from 'fs/promises' | 2 | import { writeFile } from 'fs/promises' |
| 3 | import { tmpdir } from 'os' | 3 | import { tmpdir } from 'os' |
| 4 | import { join } from 'path' | 4 | import { join } from 'path' |
| 5 | import { randomUUID } from 'crypto' | 5 | import { randomUUID } from 'crypto' |
| @@ -25,7 +25,6 @@ export async function POST(request: NextRequest) { | |||
| 25 | const url = new URL(request.url) | 25 | const url = new URL(request.url) |
| 26 | const rawPath = url.searchParams.get('path') || '/' | 26 | const rawPath = url.searchParams.get('path') || '/' |
| 27 | const targetPath = decodeURIComponent(rawPath) | 27 | const targetPath = decodeURIComponent(rawPath) |
| 28 | console.log(`targetPath = ${targetPath}`) | ||
| 29 | 28 | ||
| 30 | // Handle multipart form data | 29 | // Handle multipart form data |
| 31 | const formData = await request.formData() | 30 | const formData = await request.formData() |
| @@ -53,7 +52,6 @@ export async function POST(request: NextRequest) { | |||
| 53 | 52 | ||
| 54 | // Construct the final drive path | 53 | // Construct the final drive path |
| 55 | const finalPath = `${targetPath}/${file.name}` | 54 | const finalPath = `${targetPath}/${file.name}` |
| 56 | console.log(`finalPath = ${finalPath}`) | ||
| 57 | 55 | ||
| 58 | // Import file using Drive_import (uses --mode move, so temp file is automatically deleted) | 56 | // Import file using Drive_import (uses --mode move, so temp file is automatically deleted) |
| 59 | await Drive_import(tempFilePath, finalPath, user.email) | 57 | await Drive_import(tempFilePath, finalPath, user.email) |
diff --git a/frontend/components/drive/DriveDirectoryClient.tsx b/frontend/components/drive/DriveDirectoryClient.tsx index f523e4f..d238065 100644 --- a/frontend/components/drive/DriveDirectoryClient.tsx +++ b/frontend/components/drive/DriveDirectoryClient.tsx | |||
| @@ -32,18 +32,10 @@ import { Checkbox } from "@/components/ui/checkbox" | |||
| 32 | import { toast } from "@/hooks/use-toast" | 32 | import { toast } from "@/hooks/use-toast" |
| 33 | import { DriveLsEntry } from "@/lib/drive_types" | 33 | import { DriveLsEntry } from "@/lib/drive_types" |
| 34 | import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from "@/lib/constants" | 34 | import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from "@/lib/constants" |
| 35 | import { formatFileSize } from "@/lib/utils" | ||
| 35 | import { DriveMoveDialog } from "./DriveMoveDialog" | 36 | import { DriveMoveDialog } from "./DriveMoveDialog" |
| 36 | import { StorageUsage } from "./StorageUsage" | 37 | import { StorageUsage } from "./StorageUsage" |
| 37 | import type { StorageData } from "@/lib/storage" | 38 | import type { StorageData } from "@/lib/storage" |
| 38 | import type { UserAuth } from "@/lib/auth_types" | ||
| 39 | |||
| 40 | function formatFileSize(bytes: number): string { | ||
| 41 | if (bytes === 0) return "0 Bytes" | ||
| 42 | const k = 1024 | ||
| 43 | const sizes = ["Bytes", "KB", "MB", "GB"] | ||
| 44 | const i = Math.floor(Math.log(bytes) / Math.log(k)) | ||
| 45 | return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] | ||
| 46 | } | ||
| 47 | 39 | ||
| 48 | function formatDate(timestamp: number): string { | 40 | function formatDate(timestamp: number): string { |
| 49 | return new Date(timestamp * 1000).toISOString().split('T')[0] | 41 | return new Date(timestamp * 1000).toISOString().split('T')[0] |
| @@ -52,9 +44,9 @@ function formatDate(timestamp: number): string { | |||
| 52 | function formatDateTime(timestamp: number): string { | 44 | function formatDateTime(timestamp: number): string { |
| 53 | const date = new Date(timestamp * 1000) | 45 | const date = new Date(timestamp * 1000) |
| 54 | const dateStr = date.toISOString().split('T')[0] | 46 | const dateStr = date.toISOString().split('T')[0] |
| 55 | const timeStr = date.toLocaleTimeString('en-US', { | 47 | const timeStr = date.toLocaleTimeString('en-US', { |
| 56 | hour12: false, | 48 | hour12: false, |
| 57 | hour: '2-digit', | 49 | hour: '2-digit', |
| 58 | minute: '2-digit', | 50 | minute: '2-digit', |
| 59 | second: '2-digit' | 51 | second: '2-digit' |
| 60 | }) | 52 | }) |
| @@ -72,10 +64,9 @@ interface DriveDirectoryClientProps { | |||
| 72 | files: DriveLsEntry[] | 64 | files: DriveLsEntry[] |
| 73 | breadcrumbs: Breadcrumb[] | 65 | breadcrumbs: Breadcrumb[] |
| 74 | storageData: StorageData | 66 | storageData: StorageData |
| 75 | user: UserAuth | ||
| 76 | } | 67 | } |
| 77 | 68 | ||
| 78 | export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, user }: DriveDirectoryClientProps) { | 69 | export function DriveDirectoryClient({ path, files, breadcrumbs, storageData }: DriveDirectoryClientProps) { |
| 79 | const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set()) | 70 | const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set()) |
| 80 | const [renameDialogOpen, setRenameDialogOpen] = useState(false) | 71 | const [renameDialogOpen, setRenameDialogOpen] = useState(false) |
| 81 | const [infoDialogOpen, setInfoDialogOpen] = useState(false) | 72 | const [infoDialogOpen, setInfoDialogOpen] = useState(false) |
| @@ -98,10 +89,6 @@ export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, us | |||
| 98 | setSelectedFiles(newSelected) | 89 | setSelectedFiles(newSelected) |
| 99 | } | 90 | } |
| 100 | 91 | ||
| 101 | const selectAll = () => { | ||
| 102 | setSelectedFiles(new Set(files.map(file => file.path))) | ||
| 103 | } | ||
| 104 | |||
| 105 | const deselectAll = () => { | 92 | const deselectAll = () => { |
| 106 | setSelectedFiles(new Set()) | 93 | setSelectedFiles(new Set()) |
| 107 | } | 94 | } |
| @@ -130,7 +117,7 @@ export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, us | |||
| 130 | }) | 117 | }) |
| 131 | return | 118 | return |
| 132 | } | 119 | } |
| 133 | 120 | ||
| 134 | const filename = item.path.split('/').pop() || 'download' | 121 | const filename = item.path.split('/').pop() || 'download' |
| 135 | const permalink = `${window.location.origin}/blob/${item.blob}?filename=${encodeURIComponent(filename)}` | 122 | const permalink = `${window.location.origin}/blob/${item.blob}?filename=${encodeURIComponent(filename)}` |
| 136 | navigator.clipboard.writeText(permalink).then(() => { | 123 | navigator.clipboard.writeText(permalink).then(() => { |
| @@ -171,8 +158,7 @@ export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, us | |||
| 171 | title: "Renamed successfully", | 158 | title: "Renamed successfully", |
| 172 | description: result.message, | 159 | description: result.message, |
| 173 | }) | 160 | }) |
| 174 | 161 | ||
| 175 | // Refresh page to show changes | ||
| 176 | window.location.reload() | 162 | window.location.reload() |
| 177 | } else { | 163 | } else { |
| 178 | throw new Error(result.error || `Rename failed with status ${response.status}`) | 164 | throw new Error(result.error || `Rename failed with status ${response.status}`) |
| @@ -222,7 +208,7 @@ export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, us | |||
| 222 | try { | 208 | try { |
| 223 | const formData = new FormData() | 209 | const formData = new FormData() |
| 224 | formData.append('file', file) | 210 | formData.append('file', file) |
| 225 | 211 | ||
| 226 | // Use the new simple upload endpoint with path as query parameter | 212 | // Use the new simple upload endpoint with path as query parameter |
| 227 | const response = await fetch(`/api/upload?path=${encodeURIComponent(path)}`, { | 213 | const response = await fetch(`/api/upload?path=${encodeURIComponent(path)}`, { |
| 228 | method: 'POST', | 214 | method: 'POST', |
| @@ -247,8 +233,7 @@ export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, us | |||
| 247 | title: "Upload successful", | 233 | title: "Upload successful", |
| 248 | description: `${successCount} file(s) uploaded successfully${errorCount > 0 ? `, ${errorCount} failed` : ''}` | 234 | description: `${successCount} file(s) uploaded successfully${errorCount > 0 ? `, ${errorCount} failed` : ''}` |
| 249 | }) | 235 | }) |
| 250 | 236 | ||
| 251 | // Refresh page to show changes | ||
| 252 | window.location.reload() | 237 | window.location.reload() |
| 253 | } | 238 | } |
| 254 | 239 | ||
| @@ -292,8 +277,7 @@ export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, us | |||
| 292 | title: "Deleted successfully", | 277 | title: "Deleted successfully", |
| 293 | description: result.message, | 278 | description: result.message, |
| 294 | }) | 279 | }) |
| 295 | 280 | ||
| 296 | // Refresh page to show changes | ||
| 297 | window.location.reload() | 281 | window.location.reload() |
| 298 | } else { | 282 | } else { |
| 299 | throw new Error(result.error || `Delete failed with status ${response.status}`) | 283 | throw new Error(result.error || `Delete failed with status ${response.status}`) |
| @@ -322,7 +306,7 @@ export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, us | |||
| 322 | // Extract filename from the full path | 306 | // Extract filename from the full path |
| 323 | const fileName = filePath.split('/').pop() || '' | 307 | const fileName = filePath.split('/').pop() || '' |
| 324 | // Construct new path: destination + filename | 308 | // Construct new path: destination + filename |
| 325 | const newPath = destinationPath.endsWith('/') | 309 | const newPath = destinationPath.endsWith('/') |
| 326 | ? `${destinationPath}${fileName}` | 310 | ? `${destinationPath}${fileName}` |
| 327 | : `${destinationPath}/${fileName}` | 311 | : `${destinationPath}/${fileName}` |
| 328 | 312 | ||
| @@ -360,14 +344,13 @@ export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, us | |||
| 360 | title: "Move completed", | 344 | title: "Move completed", |
| 361 | description: `${successCount} item(s) moved successfully${errorCount > 0 ? `, ${errorCount} failed` : ''}`, | 345 | description: `${successCount} item(s) moved successfully${errorCount > 0 ? `, ${errorCount} failed` : ''}`, |
| 362 | }) | 346 | }) |
| 363 | 347 | ||
| 364 | // Refresh page to show changes | ||
| 365 | window.location.reload() | 348 | window.location.reload() |
| 366 | } | 349 | } |
| 367 | 350 | ||
| 368 | if (errorCount > 0 && successCount === 0) { | 351 | if (errorCount > 0 && successCount === 0) { |
| 369 | toast({ | 352 | toast({ |
| 370 | title: "Move failed", | 353 | title: "Move failed", |
| 371 | description: `All ${errorCount} item(s) failed to move. ${errors[0] || ''}`, | 354 | description: `All ${errorCount} item(s) failed to move. ${errors[0] || ''}`, |
| 372 | variant: "destructive" | 355 | variant: "destructive" |
| 373 | }) | 356 | }) |
| @@ -416,8 +399,7 @@ export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, us | |||
| 416 | title: "Folder created", | 399 | title: "Folder created", |
| 417 | description: result.message, | 400 | description: result.message, |
| 418 | }) | 401 | }) |
| 419 | 402 | ||
| 420 | // Refresh page to show changes | ||
| 421 | window.location.reload() | 403 | window.location.reload() |
| 422 | } else { | 404 | } else { |
| 423 | throw new Error(result.error || `Failed to create folder`) | 405 | throw new Error(result.error || `Failed to create folder`) |
| @@ -447,7 +429,7 @@ export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, us | |||
| 447 | {index === breadcrumbs.length - 1 ? ( | 429 | {index === breadcrumbs.length - 1 ? ( |
| 448 | <span className="text-foreground font-medium">{crumb.name}</span> | 430 | <span className="text-foreground font-medium">{crumb.name}</span> |
| 449 | ) : ( | 431 | ) : ( |
| 450 | <Link | 432 | <Link |
| 451 | href={crumb.path} | 433 | href={crumb.path} |
| 452 | className="hover:text-foreground transition-colors" | 434 | className="hover:text-foreground transition-colors" |
| 453 | > | 435 | > |
| @@ -457,9 +439,9 @@ export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, us | |||
| 457 | </div> | 439 | </div> |
| 458 | ))} | 440 | ))} |
| 459 | </nav> | 441 | </nav> |
| 460 | 442 | ||
| 461 | <div className="flex items-center gap-2 sm:gap-2"> | 443 | <div className="flex items-center gap-2 sm:gap-2"> |
| 462 | <Button | 444 | <Button |
| 463 | variant="secondary" | 445 | variant="secondary" |
| 464 | onClick={() => setCreateFolderDialogOpen(true)} | 446 | onClick={() => setCreateFolderDialogOpen(true)} |
| 465 | className="flex-1 sm:flex-none" | 447 | className="flex-1 sm:flex-none" |
| @@ -467,7 +449,7 @@ export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, us | |||
| 467 | <FolderPlus className="mr-2 h-4 w-4" /> | 449 | <FolderPlus className="mr-2 h-4 w-4" /> |
| 468 | Create Folder | 450 | Create Folder |
| 469 | </Button> | 451 | </Button> |
| 470 | <Button | 452 | <Button |
| 471 | onClick={() => fileInputRef.current?.click()} | 453 | onClick={() => fileInputRef.current?.click()} |
| 472 | disabled={uploading} | 454 | disabled={uploading} |
| 473 | className="flex-1 sm:flex-none" | 455 | className="flex-1 sm:flex-none" |
| @@ -534,16 +516,16 @@ export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, us | |||
| 534 | files.map((file) => { | 516 | files.map((file) => { |
| 535 | const isSelected = selectedFiles.has(file.path) | 517 | const isSelected = selectedFiles.has(file.path) |
| 536 | const fileName = file.path.split('/').pop() || file.path | 518 | const fileName = file.path.split('/').pop() || file.path |
| 537 | 519 | ||
| 538 | return ( | 520 | return ( |
| 539 | <TableRow | 521 | <TableRow |
| 540 | key={file.path} | 522 | key={file.path} |
| 541 | className={`hover:bg-muted/50 ${isSelected ? "bg-muted/30" : ""}`} | 523 | className={`hover:bg-muted/50 ${isSelected ? "bg-muted/30" : ""}`} |
| 542 | > | 524 | > |
| 543 | <TableCell className="w-[40px]" onClick={(e) => e.stopPropagation()}> | 525 | <TableCell className="w-[40px]" onClick={(e) => e.stopPropagation()}> |
| 544 | <Checkbox | 526 | <Checkbox |
| 545 | checked={isSelected} | 527 | checked={isSelected} |
| 546 | onCheckedChange={() => toggleFileSelection(file.path)} | 528 | onCheckedChange={() => toggleFileSelection(file.path)} |
| 547 | /> | 529 | /> |
| 548 | </TableCell> | 530 | </TableCell> |
| 549 | <TableCell className="font-medium"> | 531 | <TableCell className="font-medium"> |
| @@ -552,7 +534,7 @@ export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, us | |||
| 552 | <> | 534 | <> |
| 553 | <Folder className="h-4 w-4 text-blue-500 flex-shrink-0" /> | 535 | <Folder className="h-4 w-4 text-blue-500 flex-shrink-0" /> |
| 554 | <div className="min-w-0 max-w-[60vw]"> | 536 | <div className="min-w-0 max-w-[60vw]"> |
| 555 | <Link | 537 | <Link |
| 556 | href={`/drive${file.path}`} | 538 | href={`/drive${file.path}`} |
| 557 | className="text-blue-600 hover:text-blue-800 hover:underline cursor-pointer block truncate" | 539 | className="text-blue-600 hover:text-blue-800 hover:underline cursor-pointer block truncate" |
| 558 | title={fileName} | 540 | title={fileName} |
| @@ -606,8 +588,8 @@ export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, us | |||
| 606 | Info | 588 | Info |
| 607 | </DropdownMenuItem> | 589 | </DropdownMenuItem> |
| 608 | <DropdownMenuSeparator /> | 590 | <DropdownMenuSeparator /> |
| 609 | <DropdownMenuItem | 591 | <DropdownMenuItem |
| 610 | onClick={() => handleDelete([file.path])} | 592 | onClick={() => handleDelete([file.path])} |
| 611 | className="text-red-600" | 593 | className="text-red-600" |
| 612 | > | 594 | > |
| 613 | <Trash2 className="mr-2 h-4 w-4" /> | 595 | <Trash2 className="mr-2 h-4 w-4" /> |
| @@ -695,7 +677,7 @@ export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, us | |||
| 695 | </div> | 677 | </div> |
| 696 | <div> | 678 | <div> |
| 697 | <Label className="text-sm font-medium text-muted-foreground">Path</Label> | 679 | <Label className="text-sm font-medium text-muted-foreground">Path</Label> |
| 698 | <p className="text-sm font-mono text-xs">{currentItem.path}</p> | 680 | <p className="font-mono text-xs">{currentItem.path}</p> |
| 699 | </div> | 681 | </div> |
| 700 | </div> | 682 | </div> |
| 701 | <div className="flex justify-end"> | 683 | <div className="flex justify-end"> |
| @@ -749,13 +731,13 @@ export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, us | |||
| 749 | </DialogContent> | 731 | </DialogContent> |
| 750 | </Dialog> | 732 | </Dialog> |
| 751 | 733 | ||
| 752 | <input | 734 | <input |
| 753 | ref={fileInputRef} | 735 | ref={fileInputRef} |
| 754 | type="file" | 736 | type="file" |
| 755 | multiple | 737 | multiple |
| 756 | className="hidden" | 738 | className="hidden" |
| 757 | onChange={handleFileUpload} | 739 | onChange={handleFileUpload} |
| 758 | /> | 740 | /> |
| 759 | </div> | 741 | </div> |
| 760 | ) | 742 | ) |
| 761 | } \ No newline at end of file | 743 | } |
diff --git a/frontend/components/drive/DriveDirectoryView.tsx b/frontend/components/drive/DriveDirectoryView.tsx index d2f30ee..de9d70a 100644 --- a/frontend/components/drive/DriveDirectoryView.tsx +++ b/frontend/components/drive/DriveDirectoryView.tsx | |||
| @@ -2,7 +2,6 @@ import { DriveLsEntry } from "@/lib/drive_types" | |||
| 2 | import { DriveDirectoryClient } from "./DriveDirectoryClient" | 2 | import { DriveDirectoryClient } from "./DriveDirectoryClient" |
| 3 | import { DriveHeader } from "./DriveHeader" | 3 | import { DriveHeader } from "./DriveHeader" |
| 4 | import type { StorageData } from "@/lib/storage" | 4 | import type { StorageData } from "@/lib/storage" |
| 5 | import { Auth_get_user } from "@/lib/auth" | ||
| 6 | 5 | ||
| 7 | interface DriveDirectoryViewProps { | 6 | interface DriveDirectoryViewProps { |
| 8 | path: string | 7 | path: string |
| @@ -20,7 +19,7 @@ function generateBreadcrumbs(currentPath: string) { | |||
| 20 | const breadcrumbs = [{ name: 'Root', path: '/drive' }] | 19 | const breadcrumbs = [{ name: 'Root', path: '/drive' }] |
| 21 | 20 | ||
| 22 | let accumulatedPath = '' | 21 | let accumulatedPath = '' |
| 23 | parts.forEach((part, index) => { | 22 | parts.forEach((part, _index) => { |
| 24 | accumulatedPath += '/' + part | 23 | accumulatedPath += '/' + part |
| 25 | breadcrumbs.push({ | 24 | breadcrumbs.push({ |
| 26 | name: decodeURIComponent(part), // Decode URL encoded characters | 25 | name: decodeURIComponent(part), // Decode URL encoded characters |
| @@ -48,7 +47,6 @@ function sortFiles(files: DriveLsEntry[]): DriveLsEntry[] { | |||
| 48 | export async function DriveDirectoryView({ path, files, storageData }: DriveDirectoryViewProps) { | 47 | export async function DriveDirectoryView({ path, files, storageData }: DriveDirectoryViewProps) { |
| 49 | const sortedFiles = sortFiles(files) | 48 | const sortedFiles = sortFiles(files) |
| 50 | const breadcrumbs = generateBreadcrumbs(path) | 49 | const breadcrumbs = generateBreadcrumbs(path) |
| 51 | const user = await Auth_get_user() | ||
| 52 | 50 | ||
| 53 | return ( | 51 | return ( |
| 54 | <div className="container mx-auto p-6 space-y-6"> | 52 | <div className="container mx-auto p-6 space-y-6"> |
| @@ -58,8 +56,7 @@ export async function DriveDirectoryView({ path, files, storageData }: DriveDire | |||
| 58 | files={sortedFiles} | 56 | files={sortedFiles} |
| 59 | breadcrumbs={breadcrumbs} | 57 | breadcrumbs={breadcrumbs} |
| 60 | storageData={storageData} | 58 | storageData={storageData} |
| 61 | user={user} | ||
| 62 | /> | 59 | /> |
| 63 | </div> | 60 | </div> |
| 64 | ) | 61 | ) |
| 65 | } \ No newline at end of file | 62 | } |
diff --git a/frontend/components/drive/StorageUsage.tsx b/frontend/components/drive/StorageUsage.tsx index 2cb5d5d..39be4ba 100644 --- a/frontend/components/drive/StorageUsage.tsx +++ b/frontend/components/drive/StorageUsage.tsx | |||
| @@ -1,17 +1,10 @@ | |||
| 1 | import type { StorageData } from "@/lib/storage" | 1 | import type { StorageData } from "@/lib/storage" |
| 2 | import { formatFileSize } from "@/lib/utils" | ||
| 2 | 3 | ||
| 3 | interface StorageUsageProps { | 4 | interface StorageUsageProps { |
| 4 | data: StorageData | 5 | data: StorageData |
| 5 | } | 6 | } |
| 6 | 7 | ||
| 7 | function formatFileSize(bytes: number): string { | ||
| 8 | if (bytes === 0) return "0 Bytes" | ||
| 9 | const k = 1024 | ||
| 10 | const sizes = ["Bytes", "KB", "MB", "GB", "TB"] | ||
| 11 | const i = Math.floor(Math.log(bytes) / Math.log(k)) | ||
| 12 | return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] | ||
| 13 | } | ||
| 14 | |||
| 15 | export function StorageUsage({ data }: StorageUsageProps) { | 8 | export function StorageUsage({ data }: StorageUsageProps) { |
| 16 | // Safety check for undefined data | 9 | // Safety check for undefined data |
| 17 | if (!data) { | 10 | if (!data) { |
diff --git a/frontend/components/history/HistoryView.tsx b/frontend/components/history/HistoryView.tsx index c459275..e6d1cd1 100644 --- a/frontend/components/history/HistoryView.tsx +++ b/frontend/components/history/HistoryView.tsx | |||
| @@ -3,6 +3,7 @@ import { DriveHeader } from "@/components/drive/DriveHeader" | |||
| 3 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" | 3 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" |
| 4 | import { Button } from "@/components/ui/button" | 4 | import { Button } from "@/components/ui/button" |
| 5 | import { ChevronLeft, ChevronRight } from "lucide-react" | 5 | import { ChevronLeft, ChevronRight } from "lucide-react" |
| 6 | import { formatFileSize } from "@/lib/utils" | ||
| 6 | import Link from "next/link" | 7 | import Link from "next/link" |
| 7 | 8 | ||
| 8 | interface HistoryViewProps { | 9 | interface HistoryViewProps { |
| @@ -13,14 +14,6 @@ interface HistoryViewProps { | |||
| 13 | totalEntries: number | 14 | totalEntries: number |
| 14 | } | 15 | } |
| 15 | 16 | ||
| 16 | function formatFileSize(bytes: number): string { | ||
| 17 | if (bytes === 0) return "0 Bytes" | ||
| 18 | const k = 1024 | ||
| 19 | const sizes = ["Bytes", "KB", "MB", "GB"] | ||
| 20 | const i = Math.floor(Math.log(bytes) / Math.log(k)) | ||
| 21 | return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] | ||
| 22 | } | ||
| 23 | |||
| 24 | function formatDateTime(timestamp: number): string { | 17 | function formatDateTime(timestamp: number): string { |
| 25 | return new Date(timestamp * 1000).toLocaleString() | 18 | return new Date(timestamp * 1000).toLocaleString() |
| 26 | } | 19 | } |
diff --git a/frontend/lib/constants.ts b/frontend/lib/constants.ts index 173e90d..e01251f 100644 --- a/frontend/lib/constants.ts +++ b/frontend/lib/constants.ts | |||
| @@ -1,39 +1,2 @@ | |||
| 1 | // Upload configuration constants | ||
| 2 | export const UPLOAD_MAX_FILE_SIZE = 4096 * 1024 * 1024; // 100MB | 1 | export const UPLOAD_MAX_FILE_SIZE = 4096 * 1024 * 1024; // 100MB |
| 3 | export const UPLOAD_MAX_FILES = 10; // Maximum files per upload | 2 | export const UPLOAD_MAX_FILES = 10; // Maximum files per upload |
| 4 | export const UPLOAD_ALLOWED_TYPES = [ | ||
| 5 | // Documents | ||
| 6 | 'application/pdf', | ||
| 7 | 'application/msword', | ||
| 8 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', | ||
| 9 | 'application/vnd.ms-excel', | ||
| 10 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | ||
| 11 | 'application/vnd.ms-powerpoint', | ||
| 12 | 'application/vnd.openxmlformats-officedocument.presentationml.presentation', | ||
| 13 | 'text/plain', | ||
| 14 | 'text/csv', | ||
| 15 | // Images | ||
| 16 | 'image/jpeg', | ||
| 17 | 'image/png', | ||
| 18 | 'image/gif', | ||
| 19 | 'image/webp', | ||
| 20 | 'image/svg+xml', | ||
| 21 | // Videos | ||
| 22 | 'video/mp4', | ||
| 23 | 'video/webm', | ||
| 24 | 'video/ogg', | ||
| 25 | // Audio | ||
| 26 | 'audio/mpeg', | ||
| 27 | 'audio/wav', | ||
| 28 | 'audio/ogg', | ||
| 29 | // Archives | ||
| 30 | 'application/zip', | ||
| 31 | 'application/x-rar-compressed', | ||
| 32 | 'application/x-7z-compressed', | ||
| 33 | // Code/Text | ||
| 34 | 'application/json', | ||
| 35 | 'text/javascript', | ||
| 36 | 'text/html', | ||
| 37 | 'text/css', | ||
| 38 | 'application/xml' | ||
| 39 | ]; // Empty array means all types allowed | ||
diff --git a/frontend/lib/drive_server.ts b/frontend/lib/drive_server.ts index e7c88e8..1462f6e 100644 --- a/frontend/lib/drive_server.ts +++ b/frontend/lib/drive_server.ts | |||
| @@ -1,11 +1,21 @@ | |||
| 1 | import { spawnSync } from 'child_process' | 1 | import { spawnSync } from 'child_process' |
| 2 | import { DriveLsEntry, DriveLogEntry } from './drive_types' | 2 | import { DriveLsEntry, DriveLogEntry } from './drive_types' |
| 3 | import { Drive_split_path, Drive_basename } from './drive_shared' | ||
| 4 | 3 | ||
| 5 | /** | 4 | /** |
| 6 | * Server-only drive functions that use Node.js APIs | 5 | * Server-only drive functions that use Node.js APIs |
| 7 | */ | 6 | */ |
| 8 | 7 | ||
| 8 | function executeFctDriveCommand(args: string[]): string { | ||
| 9 | const result = spawnSync('fctdrive', args, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 }) | ||
| 10 | if (result.error) { | ||
| 11 | throw new Error(`Failed to execute fctdrive: ${result.error.message}`) | ||
| 12 | } | ||
| 13 | if (result.status !== 0) { | ||
| 14 | throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`) | ||
| 15 | } | ||
| 16 | return result.stdout | ||
| 17 | } | ||
| 18 | |||
| 9 | /// lists the given path on the drive | 19 | /// lists the given path on the drive |
| 10 | export async function Drive_ls(path: string, recursive: boolean): Promise<DriveLsEntry[]> { | 20 | export async function Drive_ls(path: string, recursive: boolean): Promise<DriveLsEntry[]> { |
| 11 | const args = ['ls'] | 21 | const args = ['ls'] |
| @@ -16,14 +26,7 @@ export async function Drive_ls(path: string, recursive: boolean): Promise<DriveL | |||
| 16 | args.push(path) | 26 | args.push(path) |
| 17 | } | 27 | } |
| 18 | 28 | ||
| 19 | const result = spawnSync('fctdrive', args, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 }) | 29 | const stdout = executeFctDriveCommand(args) |
| 20 | if (result.error) { | ||
| 21 | throw new Error(`Failed to execute fctdrive: ${result.error.message}`) | ||
| 22 | } | ||
| 23 | if (result.status !== 0) { | ||
| 24 | throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`) | ||
| 25 | } | ||
| 26 | const stdout = result.stdout | ||
| 27 | const entries = [] | 30 | const entries = [] |
| 28 | for (const line of stdout.split('\n')) { | 31 | for (const line of stdout.split('\n')) { |
| 29 | if (line.trim() == "") | 32 | if (line.trim() == "") |
| @@ -54,70 +57,34 @@ export async function Drive_ls(path: string, recursive: boolean): Promise<DriveL | |||
| 54 | 57 | ||
| 55 | /// import the file at local_path by moving it to drive_path | 58 | /// import the file at local_path by moving it to drive_path |
| 56 | export async function Drive_import(local_path: string, drive_path: string, email: string) { | 59 | export async function Drive_import(local_path: string, drive_path: string, email: string) { |
| 57 | const result = spawnSync('fctdrive', ['import', local_path, "--mode", "move", "--destination", drive_path, "--email", email], { encoding: 'utf-8' }); | 60 | executeFctDriveCommand(['import', local_path, "--mode", "move", "--destination", drive_path, "--email", email]); |
| 58 | if (result.error) { | ||
| 59 | throw new Error(`Failed to execute fctdrive: ${result.error.message}`); | ||
| 60 | } | ||
| 61 | if (result.status !== 0) { | ||
| 62 | throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`); | ||
| 63 | } | ||
| 64 | } | 61 | } |
| 65 | 62 | ||
| 66 | /// returns the full filesystem path of the blob given its id | 63 | /// returns the full filesystem path of the blob given its id |
| 67 | export async function Drive_blob_path(blob: string): Promise<string> { | 64 | export async function Drive_blob_path(blob: string): Promise<string> { |
| 68 | const result = spawnSync('fctdrive', ['blob', blob], { encoding: 'utf-8' }) | 65 | const stdout = executeFctDriveCommand(['blob', blob]) |
| 69 | if (result.error) { | 66 | return stdout.trim(); |
| 70 | throw new Error(`Failed to execute fctdrive: ${result.error.message}`) | ||
| 71 | } | ||
| 72 | if (result.status !== 0) { | ||
| 73 | throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`) | ||
| 74 | } | ||
| 75 | return result.stdout.trim(); | ||
| 76 | } | 67 | } |
| 77 | 68 | ||
| 78 | /// removes the file or directory at the given path | 69 | /// removes the file or directory at the given path |
| 79 | export async function Drive_remove(path: string, email: string) { | 70 | export async function Drive_remove(path: string, email: string) { |
| 80 | const result = spawnSync('fctdrive', ['remove', '--email', email, path], { encoding: 'utf-8' }); | 71 | executeFctDriveCommand(['remove', '--email', email, path]); |
| 81 | if (result.error) { | ||
| 82 | throw new Error(`Failed to execute fctdrive: ${result.error.message}`); | ||
| 83 | } | ||
| 84 | if (result.status !== 0) { | ||
| 85 | throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`); | ||
| 86 | } | ||
| 87 | } | 72 | } |
| 88 | 73 | ||
| 89 | /// creates a directory at the given path | 74 | /// creates a directory at the given path |
| 90 | export async function Drive_mkdir(path: string, email: string) { | 75 | export async function Drive_mkdir(path: string, email: string) { |
| 91 | const result = spawnSync('fctdrive', ['mkdir', '--email', email, '--path', path], { encoding: 'utf-8' }); | 76 | executeFctDriveCommand(['mkdir', '--email', email, '--path', path]); |
| 92 | if (result.error) { | ||
| 93 | throw new Error(`Failed to execute fctdrive: ${result.error.message}`); | ||
| 94 | } | ||
| 95 | if (result.status !== 0) { | ||
| 96 | throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`); | ||
| 97 | } | ||
| 98 | } | 77 | } |
| 99 | 78 | ||
| 100 | /// renames a file or directory from old path to new path | 79 | /// renames a file or directory from old path to new path |
| 101 | export async function Drive_rename(oldPath: string, newPath: string, email: string) { | 80 | export async function Drive_rename(oldPath: string, newPath: string, email: string) { |
| 102 | const result = spawnSync('fctdrive', ['rename', '--email', email, '--old', oldPath, '--new', newPath], { encoding: 'utf-8' }); | 81 | executeFctDriveCommand(['rename', '--email', email, '--old', oldPath, '--new', newPath]); |
| 103 | if (result.error) { | ||
| 104 | throw new Error(`Failed to execute fctdrive: ${result.error.message}`); | ||
| 105 | } | ||
| 106 | if (result.status !== 0) { | ||
| 107 | throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`); | ||
| 108 | } | ||
| 109 | } | 82 | } |
| 110 | 83 | ||
| 111 | /// gets the blob ID for a given path | 84 | /// gets the blob ID for a given path |
| 112 | export async function Drive_stat(path: string): Promise<string> { | 85 | export async function Drive_stat(path: string): Promise<string> { |
| 113 | const result = spawnSync('fctdrive', ['stat', path], { encoding: 'utf-8' }); | 86 | const stdout = executeFctDriveCommand(['stat', path]); |
| 114 | if (result.error) { | 87 | return stdout.trim(); |
| 115 | throw new Error(`Failed to execute fctdrive: ${result.error.message}`); | ||
| 116 | } | ||
| 117 | if (result.status !== 0) { | ||
| 118 | throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`); | ||
| 119 | } | ||
| 120 | return result.stdout.trim(); | ||
| 121 | } | 88 | } |
| 122 | 89 | ||
| 123 | 90 | ||
| @@ -130,15 +97,7 @@ export async function Drive_ls_directories(path: string = '/'): Promise<DriveLsE | |||
| 130 | 97 | ||
| 131 | /// returns the log entries from the drive | 98 | /// returns the log entries from the drive |
| 132 | export async function Drive_log(): Promise<DriveLogEntry[]> { | 99 | export async function Drive_log(): Promise<DriveLogEntry[]> { |
| 133 | const result = spawnSync('fctdrive', ['log'], { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 }) | 100 | const stdout = executeFctDriveCommand(['log']) |
| 134 | if (result.error) { | ||
| 135 | throw new Error(`Failed to execute fctdrive: ${result.error.message}`) | ||
| 136 | } | ||
| 137 | if (result.status !== 0) { | ||
| 138 | throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`) | ||
| 139 | } | ||
| 140 | |||
| 141 | const stdout = result.stdout | ||
| 142 | const entries: DriveLogEntry[] = [] | 101 | const entries: DriveLogEntry[] = [] |
| 143 | for (const line of stdout.split('\n')) { | 102 | for (const line of stdout.split('\n')) { |
| 144 | if (line.trim() === "") | 103 | if (line.trim() === "") |
diff --git a/frontend/package.json b/frontend/package.json index d3762a2..3bd5610 100644 --- a/frontend/package.json +++ b/frontend/package.json | |||
| @@ -6,7 +6,10 @@ | |||
| 6 | "dev": "next dev --turbopack", | 6 | "dev": "next dev --turbopack", |
| 7 | "build": "next build", | 7 | "build": "next build", |
| 8 | "start": "next start", | 8 | "start": "next start", |
| 9 | "lint": "next lint" | 9 | "lint": "next lint", |
| 10 | "lint:fix": "next lint --fix", | ||
| 11 | "type-check": "tsc --noEmit", | ||
| 12 | "analyze": "ANALYZE=true next build" | ||
| 10 | }, | 13 | }, |
| 11 | "dependencies": { | 14 | "dependencies": { |
| 12 | "@radix-ui/react-checkbox": "^1.3.2", | 15 | "@radix-ui/react-checkbox": "^1.3.2", |
