From 70738d871decbcdec4f5535a7b6f57de26de7d2a Mon Sep 17 00:00:00 2001 From: diogo464 Date: Tue, 12 Aug 2025 16:28:33 +0100 Subject: Clean up old UI code and rename V2 to Drive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove old UI components: file-drive.tsx, history-view.tsx - Remove unused API endpoints: /api/tree, /api/log, /api/fs/route.ts - Rename /v2 routes to /drive routes for cleaner URLs - Rename V2* components to Drive* components (V2DirectoryView -> DriveDirectoryView, etc.) - Update all breadcrumb and navigation references from /v2 to /drive - Redirect root path to /drive instead of old UI - Keep /api/fs/[...path] and /api/directories for uploads and move functionality - Preserve Drive_* server functions for potential future use 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- frontend/app/api/fs/route.ts | 19 - frontend/app/api/log/route.ts | 13 - frontend/app/api/tree/route.ts | 13 - frontend/app/drive/[...path]/page.tsx | 14 + frontend/app/drive/page.tsx | 7 + frontend/app/page.tsx | 4 +- frontend/app/v2/[...path]/page.tsx | 14 - frontend/app/v2/page.tsx | 7 - frontend/components/drive/DriveDirectoryClient.tsx | 591 +++++++++++++++ frontend/components/drive/DriveDirectoryView.tsx | 55 ++ frontend/components/drive/DriveMoveDialog.tsx | 169 +++++ frontend/components/v2/V2DirectoryClient.tsx | 591 --------------- frontend/components/v2/V2DirectoryView.tsx | 55 -- frontend/components/v2/V2MoveDialog.tsx | 169 ----- frontend/file-drive.tsx | 805 --------------------- frontend/history-view.tsx | 252 ------- 16 files changed, 838 insertions(+), 1940 deletions(-) delete mode 100644 frontend/app/api/fs/route.ts delete mode 100644 frontend/app/api/log/route.ts delete mode 100644 frontend/app/api/tree/route.ts create mode 100644 frontend/app/drive/[...path]/page.tsx create mode 100644 frontend/app/drive/page.tsx delete mode 100644 frontend/app/v2/[...path]/page.tsx delete mode 100644 frontend/app/v2/page.tsx create mode 100644 frontend/components/drive/DriveDirectoryClient.tsx create mode 100644 frontend/components/drive/DriveDirectoryView.tsx create mode 100644 frontend/components/drive/DriveMoveDialog.tsx delete mode 100644 frontend/components/v2/V2DirectoryClient.tsx delete mode 100644 frontend/components/v2/V2DirectoryView.tsx delete mode 100644 frontend/components/v2/V2MoveDialog.tsx delete mode 100644 frontend/file-drive.tsx delete mode 100644 frontend/history-view.tsx diff --git a/frontend/app/api/fs/route.ts b/frontend/app/api/fs/route.ts deleted file mode 100644 index 61d0f8a..0000000 --- a/frontend/app/api/fs/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NextResponse } from 'next/server' -import { Drive_ls } from '@/lib/drive_server' - -// GET /api/fs - Get root directory listing -export async function GET() { - try { - // Get root directory listing using Drive_ls (non-recursive) - const entries = await Drive_ls('/', false) - - return NextResponse.json(entries) - - } catch (error) { - console.error('GET fs root error:', error) - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Internal server error' }, - { status: 500 } - ) - } -} \ No newline at end of file diff --git a/frontend/app/api/log/route.ts b/frontend/app/api/log/route.ts deleted file mode 100644 index a316ce7..0000000 --- a/frontend/app/api/log/route.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NextResponse } from 'next/server' -import { Drive_log } from '@/lib/drive_server' - -export async function GET() { - try { - const logEntries = await Drive_log() - - return NextResponse.json(logEntries) - } catch (error) { - console.error('Error getting log entries:', error) - throw error - } -} \ No newline at end of file diff --git a/frontend/app/api/tree/route.ts b/frontend/app/api/tree/route.ts deleted file mode 100644 index ece8122..0000000 --- a/frontend/app/api/tree/route.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NextResponse } from 'next/server' -import { Drive_tree } from '@/lib/drive_server' - -export async function GET() { - try { - const treeResponse = await Drive_tree() - - return NextResponse.json(treeResponse) - } catch (error) { - console.error('Error building tree:', error) - throw error - } -} \ No newline at end of file diff --git a/frontend/app/drive/[...path]/page.tsx b/frontend/app/drive/[...path]/page.tsx new file mode 100644 index 0000000..b0c6d7d --- /dev/null +++ b/frontend/app/drive/[...path]/page.tsx @@ -0,0 +1,14 @@ +import { DriveDirectoryView } from "@/components/drive/DriveDirectoryView" +import { Drive_ls } from "@/lib/drive_server" + +export default async function DriveDirectoryPage({ + params, +}: { + params: Promise<{ path: string[] }> +}) { + const { path: pathSegments } = await params + const currentPath = '/' + (pathSegments?.join('/') || '') + + const files = await Drive_ls(currentPath, false) + return +} \ No newline at end of file diff --git a/frontend/app/drive/page.tsx b/frontend/app/drive/page.tsx new file mode 100644 index 0000000..0e3fd0c --- /dev/null +++ b/frontend/app/drive/page.tsx @@ -0,0 +1,7 @@ +import { DriveDirectoryView } from "@/components/drive/DriveDirectoryView" +import { Drive_ls } from "@/lib/drive_server" + +export default async function DriveRootPage() { + const files = await Drive_ls("/", false) + return +} \ No newline at end of file diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index e2b6a80..e1c6eed 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,5 +1,5 @@ -import FileDrive from "../file-drive" +import { redirect } from 'next/navigation' export default function Page() { - return + redirect('/drive') } diff --git a/frontend/app/v2/[...path]/page.tsx b/frontend/app/v2/[...path]/page.tsx deleted file mode 100644 index 3b4cbb8..0000000 --- a/frontend/app/v2/[...path]/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { V2DirectoryView } from "@/components/v2/V2DirectoryView" -import { Drive_ls } from "@/lib/drive_server" - -export default async function V2DirectoryPage({ - params, -}: { - params: Promise<{ path: string[] }> -}) { - const { path: pathSegments } = await params - const currentPath = '/' + (pathSegments?.join('/') || '') - - const files = await Drive_ls(currentPath, false) - return -} \ No newline at end of file diff --git a/frontend/app/v2/page.tsx b/frontend/app/v2/page.tsx deleted file mode 100644 index 09418a2..0000000 --- a/frontend/app/v2/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { V2DirectoryView } from "@/components/v2/V2DirectoryView" -import { Drive_ls } from "@/lib/drive_server" - -export default async function V2RootPage() { - const files = await Drive_ls("/", false) - return -} \ No newline at end of file diff --git a/frontend/components/drive/DriveDirectoryClient.tsx b/frontend/components/drive/DriveDirectoryClient.tsx new file mode 100644 index 0000000..548773a --- /dev/null +++ b/frontend/components/drive/DriveDirectoryClient.tsx @@ -0,0 +1,591 @@ +"use client" + +import type React from "react" +import { useState, useRef } from "react" +import Link from "next/link" +import { + ChevronRight, + File, + Folder, + Upload, + Trash2, + Move, + MoreHorizontal, + HardDrive, + Edit, + Link as LinkIcon, + Info, + LogIn, + LogOut, +} from "lucide-react" +import { Button } from "@/components/ui/button" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Progress } from "@/components/ui/progress" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuSeparator, +} from "@/components/ui/dropdown-menu" +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Checkbox } from "@/components/ui/checkbox" +import { toast } from "@/hooks/use-toast" +import { DriveLsEntry } from "@/lib/drive_types" +import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from "@/lib/constants" +import { DriveMoveDialog } from "./DriveMoveDialog" + +function formatFileSize(bytes: number): string { + if (bytes === 0) return "0 Bytes" + const k = 1024 + const sizes = ["Bytes", "KB", "MB", "GB"] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] +} + +function formatDate(timestamp: number): string { + return new Date(timestamp * 1000).toISOString().split('T')[0] +} + +function formatDateTime(timestamp: number): string { + const date = new Date(timestamp * 1000) + const dateStr = date.toISOString().split('T')[0] + const timeStr = date.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }) + return `${dateStr} at ${timeStr}` +} + +interface Breadcrumb { + name: string + path: string +} + +interface DriveDirectoryClientProps { + path: string + files: DriveLsEntry[] + breadcrumbs: Breadcrumb[] +} + +export function DriveDirectoryClient({ path, files, breadcrumbs }: DriveDirectoryClientProps) { + const [selectedFiles, setSelectedFiles] = useState>(new Set()) + const [renameDialogOpen, setRenameDialogOpen] = useState(false) + const [infoDialogOpen, setInfoDialogOpen] = useState(false) + const [moveDialogOpen, setMoveDialogOpen] = useState(false) + const [currentItem, setCurrentItem] = useState(null) + const [newName, setNewName] = useState("") + const fileInputRef = useRef(null) + const [uploading, setUploading] = useState(false) + + const [isLoggedIn, setIsLoggedIn] = useState(true) // Mock logged in state + + const maxStorage = 1073741824 // 1GB + const usedStorage = 0 // TODO: Calculate from files if needed + const storagePercentage = (usedStorage / maxStorage) * 100 + + const toggleFileSelection = (filePath: string) => { + const newSelected = new Set(selectedFiles) + if (newSelected.has(filePath)) { + newSelected.delete(filePath) + } else { + newSelected.add(filePath) + } + setSelectedFiles(newSelected) + } + + const selectAll = () => { + setSelectedFiles(new Set(files.map(file => file.path))) + } + + const deselectAll = () => { + setSelectedFiles(new Set()) + } + + const openRenameDialog = (item: DriveLsEntry) => { + setCurrentItem(item) + setNewName(item.path.split('/').pop() || '') + setRenameDialogOpen(true) + } + + const openInfoDialog = (item: DriveLsEntry) => { + setCurrentItem(item) + setInfoDialogOpen(true) + } + + const openMoveDialog = () => { + setMoveDialogOpen(true) + } + + const copyPermalink = (item: DriveLsEntry) => { + const permalink = `${window.location.origin}/drive/file/${item.path}` + navigator.clipboard.writeText(permalink).then(() => { + toast({ + title: "Link copied!", + description: "Permalink has been copied to clipboard", + }) + }) + } + + const handleRename = () => { + if (currentItem && newName.trim()) { + // TODO: Implement actual rename API call + setRenameDialogOpen(false) + setCurrentItem(null) + setNewName("") + toast({ + title: "Renamed successfully", + description: `Item renamed to "${newName.trim()}"`, + }) + // Refresh page to show changes + window.location.reload() + } + } + + const handleFileUpload = async (event: React.ChangeEvent) => { + const uploadedFiles = event.target.files + if (!uploadedFiles || uploadedFiles.length === 0) return + + // Validate file count + if (uploadedFiles.length > UPLOAD_MAX_FILES) { + toast({ + title: "Too many files", + description: `You can only upload up to ${UPLOAD_MAX_FILES} files at once`, + variant: "destructive" + }) + return + } + + // Validate file sizes + const oversizedFiles = Array.from(uploadedFiles).filter(file => file.size > UPLOAD_MAX_FILE_SIZE) + if (oversizedFiles.length > 0) { + toast({ + title: "Files too large", + description: `Maximum file size is ${formatFileSize(UPLOAD_MAX_FILE_SIZE)}. Found ${oversizedFiles.length} oversized file(s)`, + variant: "destructive" + }) + return + } + + setUploading(true) + let successCount = 0 + let errorCount = 0 + + try { + // Upload files sequentially to the current directory + for (const file of Array.from(uploadedFiles)) { + try { + const formData = new FormData() + formData.append('file', file) + + // Construct the upload path (current path + filename) + const uploadPath = path === '/' ? file.name : `${path.slice(1)}/${file.name}` + // Encode each path segment for the URL - Next.js will decode it back for the API + const encodedPath = uploadPath.split('/').map(encodeURIComponent).join('/') + + const response = await fetch(`/api/fs/${encodedPath}`, { + method: 'PUT', + headers: { + 'AUTH': '1' // Development auth header + }, + body: formData + }) + + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || `Upload failed with status ${response.status}`) + } + + successCount++ + } catch (error) { + console.error(`Failed to upload ${file.name}:`, error) + errorCount++ + } + } + + // Show results + if (successCount > 0) { + toast({ + title: "Upload successful", + description: `${successCount} file(s) uploaded successfully${errorCount > 0 ? `, ${errorCount} failed` : ''}` + }) + + // Refresh page to show changes + window.location.reload() + } + + if (errorCount > 0 && successCount === 0) { + toast({ + title: "Upload failed", + description: `All ${errorCount} file(s) failed to upload`, + variant: "destructive" + }) + } + + } catch (error) { + console.error('Upload error:', error) + toast({ + title: "Upload failed", + description: error instanceof Error ? error.message : 'Unknown error occurred', + variant: "destructive" + }) + } finally { + setUploading(false) + // Reset the input + event.target.value = '' + } + } + + const handleDelete = async (itemPaths: string[]) => { + // TODO: Implement actual delete API calls + setSelectedFiles(new Set()) + + toast({ + title: "Deleted successfully", + description: `${itemPaths.length} item(s) deleted`, + }) + + // Refresh page to show changes + window.location.reload() + } + + const handleLogin = () => { + // Redirect to external auth page (configured via env var) + const authUrl = process.env.NEXT_PUBLIC_AUTH_URL || "/auth/login" + window.location.href = authUrl + } + + const handleLogout = () => { + // Handle logout (would typically clear tokens, etc.) + setIsLoggedIn(false) + // Could also redirect to logout endpoint + } + + const handleMove = async (destinationPath: string) => { + // TODO: Implement actual move API calls + console.log('Moving files:', Array.from(selectedFiles), 'to:', destinationPath) + setSelectedFiles(new Set()) + setMoveDialogOpen(false) + + toast({ + title: "Moved successfully", + description: `${selectedFiles.size} item(s) moved to ${destinationPath}`, + }) + + // Refresh page to show changes + window.location.reload() + } + + return ( +
+ {/* Header with Breadcrumbs */} +
+
+
+ +

Drive

+
+ + {/* Breadcrumbs */} + +
+ +
+ + {isLoggedIn ? ( + + ) : ( + + )} +
+
+ + {/* Storage Info */} +
+
+ Storage Usage + + {formatFileSize(usedStorage)} of {formatFileSize(maxStorage)} used + +
+ +
+ {storagePercentage.toFixed(1)}% used + {formatFileSize(maxStorage - usedStorage)} available +
+
+ + {/* Bulk Actions */} + {selectedFiles.size > 0 && ( +
+
+ + {selectedFiles.size} item{selectedFiles.size !== 1 ? "s" : ""} selected + + +
+
+ + +
+
+ )} + + {/* File Table */} +
+ + + + + Name + Size + Modified + Actions + + + + {files.length === 0 ? ( + + + This directory is empty + + + ) : ( + files.map((file) => { + const isSelected = selectedFiles.has(file.path) + const fileName = file.path.split('/').pop() || file.path + + return ( + + e.stopPropagation()}> + toggleFileSelection(file.path)} + /> + + +
+ {file.type === "dir" ? ( + <> + + + {fileName} + + + ) : ( + <> + + {file.blob ? ( + + {fileName} + + ) : ( + {fileName} + )} + + )} +
+
+ {formatFileSize(file.size || 0)} + {formatDate(file.lastmod)} + e.stopPropagation()}> + + + + + + openRenameDialog(file)}> + + Rename + + copyPermalink(file)}> + + Copy Permalink + + openInfoDialog(file)}> + + Info + + + handleDelete([file.path])} + className="text-red-600" + > + + Delete + + + + +
+ ) + }) + )} +
+
+
+ + {/* Rename Dialog */} + + + + Rename {currentItem?.type === "dir" ? "Folder" : "File"} + +
+
+ + setNewName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleRename() + } + }} + placeholder="Enter new name" + /> +
+
+ + +
+
+
+
+ + {/* Info Dialog */} + + + + + {currentItem?.type === "dir" ? ( + + ) : ( + + )} + {currentItem?.type === "dir" ? "Folder" : "File"} Information + + + {currentItem && ( +
+
+
+ +

{currentItem.path.split('/').pop()}

+
+
+ +

{formatFileSize(currentItem.size || 0)}

+
+
+ +

{formatDateTime(currentItem.lastmod)}

+
+
+ +

{currentItem.author}

+
+
+ +

{currentItem.type}

+
+
+ +

{currentItem.path}

+
+
+
+ +
+
+ )} +
+
+ + {/* Move Dialog */} + + + +
+ ) +} \ No newline at end of file diff --git a/frontend/components/drive/DriveDirectoryView.tsx b/frontend/components/drive/DriveDirectoryView.tsx new file mode 100644 index 0000000..20ed9d8 --- /dev/null +++ b/frontend/components/drive/DriveDirectoryView.tsx @@ -0,0 +1,55 @@ +import { DriveLsEntry } from "@/lib/drive_types" +import { DriveDirectoryClient } from "./DriveDirectoryClient" + +interface DriveDirectoryViewProps { + path: string + files: DriveLsEntry[] +} + +// Generate breadcrumbs from path +function generateBreadcrumbs(currentPath: string) { + if (currentPath === '/') { + return [{ name: 'Root', path: '/drive' }] + } + + const parts = currentPath.split('/').filter(Boolean) + const breadcrumbs = [{ name: 'Root', path: '/drive' }] + + let accumulatedPath = '' + parts.forEach((part, index) => { + accumulatedPath += '/' + part + breadcrumbs.push({ + name: decodeURIComponent(part), // Decode URL encoded characters + path: '/drive' + accumulatedPath + }) + }) + + return breadcrumbs +} + +// Sort files: directories first, then files, all alphabetically +function sortFiles(files: DriveLsEntry[]): DriveLsEntry[] { + return [...files].sort((a, b) => { + // Directories first, then files + if (a.type === "dir" && b.type === "file") return -1; + if (a.type === "file" && b.type === "dir") return 1; + + // Both same type, sort alphabetically by name (case-insensitive) + const aName = a.path.split('/').pop() || a.path; + const bName = b.path.split('/').pop() || b.path; + return aName.toLowerCase().localeCompare(bName.toLowerCase()); + }); +} + +export function DriveDirectoryView({ path, files }: DriveDirectoryViewProps) { + const sortedFiles = sortFiles(files) + const breadcrumbs = generateBreadcrumbs(path) + + return ( + + ) +} \ No newline at end of file diff --git a/frontend/components/drive/DriveMoveDialog.tsx b/frontend/components/drive/DriveMoveDialog.tsx new file mode 100644 index 0000000..d00f860 --- /dev/null +++ b/frontend/components/drive/DriveMoveDialog.tsx @@ -0,0 +1,169 @@ +"use client" + +import React, { useState, useEffect } from "react" +import { Search, Folder, Move } from "lucide-react" +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { ScrollArea } from "@/components/ui/scroll-area" +import { toast } from "@/hooks/use-toast" +import { DriveLsEntry } from "@/lib/drive_types" + +interface DriveMoveDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + selectedCount: number + onMove: (destinationPath: string) => void +} + +export function DriveMoveDialog({ open, onOpenChange, selectedCount, onMove }: DriveMoveDialogProps) { + const [directories, setDirectories] = useState([]) + const [filteredDirectories, setFilteredDirectories] = useState([]) + const [searchQuery, setSearchQuery] = useState("") + const [selectedDirectory, setSelectedDirectory] = useState(null) + const [loading, setLoading] = useState(false) + + // Load all directories when dialog opens + useEffect(() => { + if (open) { + loadDirectories() + } + }, [open]) + + // Filter directories based on search query + useEffect(() => { + if (!searchQuery.trim()) { + setFilteredDirectories(directories) + } else { + const query = searchQuery.toLowerCase() + const filtered = directories.filter(dir => + dir.path.toLowerCase().includes(query) + ) + setFilteredDirectories(filtered) + } + }, [searchQuery, directories]) + + const loadDirectories = async () => { + setLoading(true) + try { + const response = await fetch('/api/directories') + if (!response.ok) { + throw new Error(`Failed to fetch directories: ${response.statusText}`) + } + const dirs = await response.json() + setDirectories(dirs) + setFilteredDirectories(dirs) + } catch (error) { + console.error('Error loading directories:', error) + toast({ + title: "Failed to load directories", + description: error instanceof Error ? error.message : 'Unknown error occurred', + variant: "destructive" + }) + } finally { + setLoading(false) + } + } + + const handleMove = () => { + if (selectedDirectory) { + onMove(selectedDirectory) + // Reset dialog state + setSelectedDirectory(null) + setSearchQuery("") + } + } + + const handleClose = () => { + onOpenChange(false) + // Reset dialog state + setSelectedDirectory(null) + setSearchQuery("") + } + + return ( + + + + + + Move {selectedCount} item{selectedCount !== 1 ? "s" : ""} + + + +
+ {/* Search Input */} +
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+ + {/* Directory List */} + +
+ {loading ? ( +
+
+
+ ) : filteredDirectories.length === 0 ? ( +
+ {searchQuery ? 'No directories found matching your search' : 'No directories available'} +
+ ) : ( +
+ {filteredDirectories.map((directory) => ( +
setSelectedDirectory(directory.path)} + > + + + {directory.path} + +
+ ))} +
+ )} +
+
+ + {/* Selected Directory Display */} + {selectedDirectory && ( +
+
+ Moving to: +
+
+ {selectedDirectory} +
+
+ )} + + {/* Action Buttons */} +
+ + +
+
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/components/v2/V2DirectoryClient.tsx b/frontend/components/v2/V2DirectoryClient.tsx deleted file mode 100644 index 0d9a63a..0000000 --- a/frontend/components/v2/V2DirectoryClient.tsx +++ /dev/null @@ -1,591 +0,0 @@ -"use client" - -import type React from "react" -import { useState, useRef } from "react" -import Link from "next/link" -import { - ChevronRight, - File, - Folder, - Upload, - Trash2, - Move, - MoreHorizontal, - HardDrive, - Edit, - Link as LinkIcon, - Info, - LogIn, - LogOut, -} from "lucide-react" -import { Button } from "@/components/ui/button" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { Progress } from "@/components/ui/progress" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - DropdownMenuSeparator, -} from "@/components/ui/dropdown-menu" -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Checkbox } from "@/components/ui/checkbox" -import { toast } from "@/hooks/use-toast" -import { DriveLsEntry } from "@/lib/drive_types" -import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from "@/lib/constants" -import { V2MoveDialog } from "./V2MoveDialog" - -function formatFileSize(bytes: number): string { - if (bytes === 0) return "0 Bytes" - const k = 1024 - const sizes = ["Bytes", "KB", "MB", "GB"] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] -} - -function formatDate(timestamp: number): string { - return new Date(timestamp * 1000).toISOString().split('T')[0] -} - -function formatDateTime(timestamp: number): string { - const date = new Date(timestamp * 1000) - const dateStr = date.toISOString().split('T')[0] - const timeStr = date.toLocaleTimeString('en-US', { - hour12: false, - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }) - return `${dateStr} at ${timeStr}` -} - -interface Breadcrumb { - name: string - path: string -} - -interface V2DirectoryClientProps { - path: string - files: DriveLsEntry[] - breadcrumbs: Breadcrumb[] -} - -export function V2DirectoryClient({ path, files, breadcrumbs }: V2DirectoryClientProps) { - const [selectedFiles, setSelectedFiles] = useState>(new Set()) - const [renameDialogOpen, setRenameDialogOpen] = useState(false) - const [infoDialogOpen, setInfoDialogOpen] = useState(false) - const [moveDialogOpen, setMoveDialogOpen] = useState(false) - const [currentItem, setCurrentItem] = useState(null) - const [newName, setNewName] = useState("") - const fileInputRef = useRef(null) - const [uploading, setUploading] = useState(false) - - const [isLoggedIn, setIsLoggedIn] = useState(true) // Mock logged in state - - const maxStorage = 1073741824 // 1GB - const usedStorage = 0 // TODO: Calculate from files if needed - const storagePercentage = (usedStorage / maxStorage) * 100 - - const toggleFileSelection = (filePath: string) => { - const newSelected = new Set(selectedFiles) - if (newSelected.has(filePath)) { - newSelected.delete(filePath) - } else { - newSelected.add(filePath) - } - setSelectedFiles(newSelected) - } - - const selectAll = () => { - setSelectedFiles(new Set(files.map(file => file.path))) - } - - const deselectAll = () => { - setSelectedFiles(new Set()) - } - - const openRenameDialog = (item: DriveLsEntry) => { - setCurrentItem(item) - setNewName(item.path.split('/').pop() || '') - setRenameDialogOpen(true) - } - - const openInfoDialog = (item: DriveLsEntry) => { - setCurrentItem(item) - setInfoDialogOpen(true) - } - - const openMoveDialog = () => { - setMoveDialogOpen(true) - } - - const copyPermalink = (item: DriveLsEntry) => { - const permalink = `${window.location.origin}/drive/file/${item.path}` - navigator.clipboard.writeText(permalink).then(() => { - toast({ - title: "Link copied!", - description: "Permalink has been copied to clipboard", - }) - }) - } - - const handleRename = () => { - if (currentItem && newName.trim()) { - // TODO: Implement actual rename API call - setRenameDialogOpen(false) - setCurrentItem(null) - setNewName("") - toast({ - title: "Renamed successfully", - description: `Item renamed to "${newName.trim()}"`, - }) - // Refresh page to show changes - window.location.reload() - } - } - - const handleFileUpload = async (event: React.ChangeEvent) => { - const uploadedFiles = event.target.files - if (!uploadedFiles || uploadedFiles.length === 0) return - - // Validate file count - if (uploadedFiles.length > UPLOAD_MAX_FILES) { - toast({ - title: "Too many files", - description: `You can only upload up to ${UPLOAD_MAX_FILES} files at once`, - variant: "destructive" - }) - return - } - - // Validate file sizes - const oversizedFiles = Array.from(uploadedFiles).filter(file => file.size > UPLOAD_MAX_FILE_SIZE) - if (oversizedFiles.length > 0) { - toast({ - title: "Files too large", - description: `Maximum file size is ${formatFileSize(UPLOAD_MAX_FILE_SIZE)}. Found ${oversizedFiles.length} oversized file(s)`, - variant: "destructive" - }) - return - } - - setUploading(true) - let successCount = 0 - let errorCount = 0 - - try { - // Upload files sequentially to the current directory - for (const file of Array.from(uploadedFiles)) { - try { - const formData = new FormData() - formData.append('file', file) - - // Construct the upload path (current path + filename) - const uploadPath = path === '/' ? file.name : `${path.slice(1)}/${file.name}` - // Encode each path segment for the URL - Next.js will decode it back for the API - const encodedPath = uploadPath.split('/').map(encodeURIComponent).join('/') - - const response = await fetch(`/api/fs/${encodedPath}`, { - method: 'PUT', - headers: { - 'AUTH': '1' // Development auth header - }, - body: formData - }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || `Upload failed with status ${response.status}`) - } - - successCount++ - } catch (error) { - console.error(`Failed to upload ${file.name}:`, error) - errorCount++ - } - } - - // Show results - if (successCount > 0) { - toast({ - title: "Upload successful", - description: `${successCount} file(s) uploaded successfully${errorCount > 0 ? `, ${errorCount} failed` : ''}` - }) - - // Refresh page to show changes - window.location.reload() - } - - if (errorCount > 0 && successCount === 0) { - toast({ - title: "Upload failed", - description: `All ${errorCount} file(s) failed to upload`, - variant: "destructive" - }) - } - - } catch (error) { - console.error('Upload error:', error) - toast({ - title: "Upload failed", - description: error instanceof Error ? error.message : 'Unknown error occurred', - variant: "destructive" - }) - } finally { - setUploading(false) - // Reset the input - event.target.value = '' - } - } - - const handleDelete = async (itemPaths: string[]) => { - // TODO: Implement actual delete API calls - setSelectedFiles(new Set()) - - toast({ - title: "Deleted successfully", - description: `${itemPaths.length} item(s) deleted`, - }) - - // Refresh page to show changes - window.location.reload() - } - - const handleLogin = () => { - // Redirect to external auth page (configured via env var) - const authUrl = process.env.NEXT_PUBLIC_AUTH_URL || "/auth/login" - window.location.href = authUrl - } - - const handleLogout = () => { - // Handle logout (would typically clear tokens, etc.) - setIsLoggedIn(false) - // Could also redirect to logout endpoint - } - - const handleMove = async (destinationPath: string) => { - // TODO: Implement actual move API calls - console.log('Moving files:', Array.from(selectedFiles), 'to:', destinationPath) - setSelectedFiles(new Set()) - setMoveDialogOpen(false) - - toast({ - title: "Moved successfully", - description: `${selectedFiles.size} item(s) moved to ${destinationPath}`, - }) - - // Refresh page to show changes - window.location.reload() - } - - return ( -
- {/* Header with Breadcrumbs */} -
-
-
- -

Drive V2

-
- - {/* Breadcrumbs */} - -
- -
- - {isLoggedIn ? ( - - ) : ( - - )} -
-
- - {/* Storage Info */} -
-
- Storage Usage - - {formatFileSize(usedStorage)} of {formatFileSize(maxStorage)} used - -
- -
- {storagePercentage.toFixed(1)}% used - {formatFileSize(maxStorage - usedStorage)} available -
-
- - {/* Bulk Actions */} - {selectedFiles.size > 0 && ( -
-
- - {selectedFiles.size} item{selectedFiles.size !== 1 ? "s" : ""} selected - - -
-
- - -
-
- )} - - {/* File Table */} -
- - - - - Name - Size - Modified - Actions - - - - {files.length === 0 ? ( - - - This directory is empty - - - ) : ( - files.map((file) => { - const isSelected = selectedFiles.has(file.path) - const fileName = file.path.split('/').pop() || file.path - - return ( - - e.stopPropagation()}> - toggleFileSelection(file.path)} - /> - - -
- {file.type === "dir" ? ( - <> - - - {fileName} - - - ) : ( - <> - - {file.blob ? ( - - {fileName} - - ) : ( - {fileName} - )} - - )} -
-
- {formatFileSize(file.size || 0)} - {formatDate(file.lastmod)} - e.stopPropagation()}> - - - - - - openRenameDialog(file)}> - - Rename - - copyPermalink(file)}> - - Copy Permalink - - openInfoDialog(file)}> - - Info - - - handleDelete([file.path])} - className="text-red-600" - > - - Delete - - - - -
- ) - }) - )} -
-
-
- - {/* Rename Dialog */} - - - - Rename {currentItem?.type === "dir" ? "Folder" : "File"} - -
-
- - setNewName(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - handleRename() - } - }} - placeholder="Enter new name" - /> -
-
- - -
-
-
-
- - {/* Info Dialog */} - - - - - {currentItem?.type === "dir" ? ( - - ) : ( - - )} - {currentItem?.type === "dir" ? "Folder" : "File"} Information - - - {currentItem && ( -
-
-
- -

{currentItem.path.split('/').pop()}

-
-
- -

{formatFileSize(currentItem.size || 0)}

-
-
- -

{formatDateTime(currentItem.lastmod)}

-
-
- -

{currentItem.author}

-
-
- -

{currentItem.type}

-
-
- -

{currentItem.path}

-
-
-
- -
-
- )} -
-
- - {/* Move Dialog */} - - - -
- ) -} \ No newline at end of file diff --git a/frontend/components/v2/V2DirectoryView.tsx b/frontend/components/v2/V2DirectoryView.tsx deleted file mode 100644 index 97fa9a8..0000000 --- a/frontend/components/v2/V2DirectoryView.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { DriveLsEntry } from "@/lib/drive_types" -import { V2DirectoryClient } from "./V2DirectoryClient" - -interface V2DirectoryViewProps { - path: string - files: DriveLsEntry[] -} - -// Generate breadcrumbs from path -function generateBreadcrumbs(currentPath: string) { - if (currentPath === '/') { - return [{ name: 'Root', path: '/v2' }] - } - - const parts = currentPath.split('/').filter(Boolean) - const breadcrumbs = [{ name: 'Root', path: '/v2' }] - - let accumulatedPath = '' - parts.forEach((part, index) => { - accumulatedPath += '/' + part - breadcrumbs.push({ - name: decodeURIComponent(part), // Decode URL encoded characters - path: '/v2' + accumulatedPath - }) - }) - - return breadcrumbs -} - -// Sort files: directories first, then files, all alphabetically -function sortFiles(files: DriveLsEntry[]): DriveLsEntry[] { - return [...files].sort((a, b) => { - // Directories first, then files - if (a.type === "dir" && b.type === "file") return -1; - if (a.type === "file" && b.type === "dir") return 1; - - // Both same type, sort alphabetically by name (case-insensitive) - const aName = a.path.split('/').pop() || a.path; - const bName = b.path.split('/').pop() || b.path; - return aName.toLowerCase().localeCompare(bName.toLowerCase()); - }); -} - -export function V2DirectoryView({ path, files }: V2DirectoryViewProps) { - const sortedFiles = sortFiles(files) - const breadcrumbs = generateBreadcrumbs(path) - - return ( - - ) -} \ No newline at end of file diff --git a/frontend/components/v2/V2MoveDialog.tsx b/frontend/components/v2/V2MoveDialog.tsx deleted file mode 100644 index 7cedde0..0000000 --- a/frontend/components/v2/V2MoveDialog.tsx +++ /dev/null @@ -1,169 +0,0 @@ -"use client" - -import React, { useState, useEffect } from "react" -import { Search, Folder, Move } from "lucide-react" -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Input } from "@/components/ui/input" -import { Button } from "@/components/ui/button" -import { ScrollArea } from "@/components/ui/scroll-area" -import { toast } from "@/hooks/use-toast" -import { DriveLsEntry } from "@/lib/drive_types" - -interface V2MoveDialogProps { - open: boolean - onOpenChange: (open: boolean) => void - selectedCount: number - onMove: (destinationPath: string) => void -} - -export function V2MoveDialog({ open, onOpenChange, selectedCount, onMove }: V2MoveDialogProps) { - const [directories, setDirectories] = useState([]) - const [filteredDirectories, setFilteredDirectories] = useState([]) - const [searchQuery, setSearchQuery] = useState("") - const [selectedDirectory, setSelectedDirectory] = useState(null) - const [loading, setLoading] = useState(false) - - // Load all directories when dialog opens - useEffect(() => { - if (open) { - loadDirectories() - } - }, [open]) - - // Filter directories based on search query - useEffect(() => { - if (!searchQuery.trim()) { - setFilteredDirectories(directories) - } else { - const query = searchQuery.toLowerCase() - const filtered = directories.filter(dir => - dir.path.toLowerCase().includes(query) - ) - setFilteredDirectories(filtered) - } - }, [searchQuery, directories]) - - const loadDirectories = async () => { - setLoading(true) - try { - const response = await fetch('/api/directories') - if (!response.ok) { - throw new Error(`Failed to fetch directories: ${response.statusText}`) - } - const dirs = await response.json() - setDirectories(dirs) - setFilteredDirectories(dirs) - } catch (error) { - console.error('Error loading directories:', error) - toast({ - title: "Failed to load directories", - description: error instanceof Error ? error.message : 'Unknown error occurred', - variant: "destructive" - }) - } finally { - setLoading(false) - } - } - - const handleMove = () => { - if (selectedDirectory) { - onMove(selectedDirectory) - // Reset dialog state - setSelectedDirectory(null) - setSearchQuery("") - } - } - - const handleClose = () => { - onOpenChange(false) - // Reset dialog state - setSelectedDirectory(null) - setSearchQuery("") - } - - return ( - - - - - - Move {selectedCount} item{selectedCount !== 1 ? "s" : ""} - - - -
- {/* Search Input */} -
- - setSearchQuery(e.target.value)} - className="pl-10" - /> -
- - {/* Directory List */} - -
- {loading ? ( -
-
-
- ) : filteredDirectories.length === 0 ? ( -
- {searchQuery ? 'No directories found matching your search' : 'No directories available'} -
- ) : ( -
- {filteredDirectories.map((directory) => ( -
setSelectedDirectory(directory.path)} - > - - - {directory.path} - -
- ))} -
- )} -
-
- - {/* Selected Directory Display */} - {selectedDirectory && ( -
-
- Moving to: -
-
- {selectedDirectory} -
-
- )} - - {/* Action Buttons */} -
- - -
-
-
-
- ) -} \ No newline at end of file diff --git a/frontend/file-drive.tsx b/frontend/file-drive.tsx deleted file mode 100644 index 123f088..0000000 --- a/frontend/file-drive.tsx +++ /dev/null @@ -1,805 +0,0 @@ -"use client" - -import type React from "react" - -import { useState, useRef, useEffect } from "react" -import { - ChevronRight, - ChevronDown, - File, - Folder, - Upload, - Trash2, - Move, - MoreHorizontal, - HardDrive, - Edit, - Link, - Info, - LogIn, - LogOut, - HistoryIcon, -} from "lucide-react" -import { Button } from "@/components/ui/button" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { Progress } from "@/components/ui/progress" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - DropdownMenuSeparator, -} from "@/components/ui/dropdown-menu" -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Checkbox } from "@/components/ui/checkbox" -import { toast } from "@/hooks/use-toast" -import HistoryView from "./history-view" -import { DriveTreeResponse, DriveTreeNode } from "@/lib/drive_types" -import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from "@/lib/constants" - - -function formatFileSize(bytes: number): string { - if (bytes === 0) return "0 Bytes" - const k = 1024 - const sizes = ["Bytes", "KB", "MB", "GB"] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] -} - -function calculateTotalSize(items: DriveTreeNode[]): number { - return items.reduce((total, item) => { - if (item.type === "dir" && item.children) { - return total + calculateTotalSize(item.children) - } - return total + (item.size || 0) - }, 0) -} - -// Fetch data from /api/tree endpoint -async function fetchDriveTree(): Promise { - const response = await fetch('/api/tree') - if (!response.ok) { - throw new Error(`Failed to fetch drive tree: ${response.statusText}`) - } - return await response.json() -} - -// Convert UNIX timestamp to date string -function formatDate(timestamp: number): string { - return new Date(timestamp * 1000).toISOString().split('T')[0] // YYYY-MM-DD -} - -// Convert UNIX timestamp to date and time string -function formatDateTime(timestamp: number): string { - const date = new Date(timestamp * 1000) - const dateStr = date.toISOString().split('T')[0] // YYYY-MM-DD - const timeStr = date.toLocaleTimeString('en-US', { - hour12: false, - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }) - return `${dateStr} at ${timeStr}` -} - -export default function FileDrive() { - const [files, setFiles] = useState([]) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - const [expandedFolders, setExpandedFolders] = useState>(new Set()) - const [selectedFiles, setSelectedFiles] = useState>(new Set()) - const [renameDialogOpen, setRenameDialogOpen] = useState(false) - const [infoDialogOpen, setInfoDialogOpen] = useState(false) - const [currentItem, setCurrentItem] = useState(null) - const [newName, setNewName] = useState("") - const fileInputRef = useRef(null) - const [uploading, setUploading] = useState(false) - - const [isLoggedIn, setIsLoggedIn] = useState(true) // Mock logged in state - const [currentView, setCurrentView] = useState<"drive" | "history">("drive") - const [uploadToFolder, setUploadToFolder] = useState(null) - - const maxStorage = 1073741824 // 1GB - const usedStorage = 0;//calculateTotalSize(files) - const storagePercentage = (usedStorage / maxStorage) * 100 - - // Function to refresh file tree - const refreshFileTree = async () => { - try { - const treeResponse = await fetchDriveTree() - setFiles(treeResponse.root) - } catch (err) { - console.error('Error refreshing file tree:', err) - toast({ - title: "Failed to refresh", - description: "Could not refresh file list after upload", - variant: "destructive" - }) - } - } - - // Load drive data on component mount - useEffect(() => { - async function loadDriveData() { - try { - setLoading(true) - setError(null) - const treeResponse = await fetchDriveTree() - setFiles(treeResponse.root) - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to load drive data') - console.error('Error loading drive data:', err) - } finally { - setLoading(false) - } - } - - loadDriveData() - }, []) - - const toggleFolder = (folderId: string) => { - const newExpanded = new Set(expandedFolders) - if (newExpanded.has(folderId)) { - newExpanded.delete(folderId) - } else { - newExpanded.add(folderId) - } - setExpandedFolders(newExpanded) - } - - const toggleFileSelection = (fileId: string) => { - const newSelected = new Set(selectedFiles) - if (newSelected.has(fileId)) { - newSelected.delete(fileId) - } else { - newSelected.add(fileId) - } - setSelectedFiles(newSelected) - } - - const selectAll = () => { - const getAllPaths = (items: DriveTreeNode[]): string[] => { - const paths: string[] = [] - items.forEach((item) => { - paths.push(item.path) - if (item.children) { - paths.push(...getAllPaths(item.children)) - } - }) - return paths - } - setSelectedFiles(new Set(getAllPaths(files))) - } - - const deselectAll = () => { - setSelectedFiles(new Set()) - } - - const openRenameDialog = (item: DriveTreeNode) => { - setCurrentItem(item) - setNewName(item.name) - setRenameDialogOpen(true) - } - - const openInfoDialog = (item: DriveTreeNode) => { - setCurrentItem(item) - setInfoDialogOpen(true) - } - - const copyPermalink = (item: DriveTreeNode) => { - const permalink = `${window.location.origin}/drive/file/${item.path}` - navigator.clipboard.writeText(permalink).then(() => { - toast({ - title: "Link copied!", - description: "Permalink has been copied to clipboard", - }) - }) - } - - const handleRename = () => { - if (currentItem && newName.trim()) { - const renameInArray = (items: DriveTreeNode[]): DriveTreeNode[] => { - return items.map((item) => { - if (item.path === currentItem.path) { - return { ...item, name: newName.trim() } - } - if (item.children) { - return { ...item, children: renameInArray(item.children) } - } - return item - }) - } - setFiles(renameInArray(files)) - setRenameDialogOpen(false) - setCurrentItem(null) - setNewName("") - toast({ - title: "Renamed successfully", - description: `Item renamed to "${newName.trim()}"`, - }) - } - } - - const handleFileUpload = async (event: React.ChangeEvent) => { - const uploadedFiles = event.target.files - if (!uploadedFiles || uploadedFiles.length === 0) return - - // Validate file count - if (uploadedFiles.length > UPLOAD_MAX_FILES) { - toast({ - title: "Too many files", - description: `You can only upload up to ${UPLOAD_MAX_FILES} files at once`, - variant: "destructive" - }) - return - } - - // Validate file sizes - const oversizedFiles = Array.from(uploadedFiles).filter(file => file.size > UPLOAD_MAX_FILE_SIZE) - if (oversizedFiles.length > 0) { - toast({ - title: "Files too large", - description: `Maximum file size is ${formatFileSize(UPLOAD_MAX_FILE_SIZE)}. Found ${oversizedFiles.length} oversized file(s)`, - variant: "destructive" - }) - return - } - - setUploading(true) - let successCount = 0 - let errorCount = 0 - - try { - // Upload files sequentially to avoid overwhelming the server - for (const file of Array.from(uploadedFiles)) { - try { - const formData = new FormData() - formData.append('file', file) - - const response = await fetch(`/api/fs/${encodeURIComponent(file.name)}`, { - method: 'PUT', - headers: { - 'AUTH': '1' // Development auth header - }, - body: formData - }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || `Upload failed with status ${response.status}`) - } - - successCount++ - } catch (error) { - console.error(`Failed to upload ${file.name}:`, error) - errorCount++ - } - } - - // Show results - if (successCount > 0) { - toast({ - title: "Upload successful", - description: `${successCount} file(s) uploaded successfully${errorCount > 0 ? `, ${errorCount} failed` : ''}` - }) - - // Refresh the file tree - await refreshFileTree() - } - - if (errorCount > 0 && successCount === 0) { - toast({ - title: "Upload failed", - description: `All ${errorCount} file(s) failed to upload`, - variant: "destructive" - }) - } - - } catch (error) { - console.error('Upload error:', error) - toast({ - title: "Upload failed", - description: error instanceof Error ? error.message : 'Unknown error occurred', - variant: "destructive" - }) - } finally { - setUploading(false) - // Reset the input - event.target.value = '' - } - } - - const deleteItems = (itemPaths: string[]) => { - const deleteFromArray = (items: DriveTreeNode[]): DriveTreeNode[] => { - return items.filter((item) => { - if (itemPaths.includes(item.path)) return false - if (item.children) { - item.children = deleteFromArray(item.children) - } - return true - }) - } - setFiles(deleteFromArray(files)) - // Remove deleted items from selection - const newSelected = new Set(selectedFiles) - itemPaths.forEach((path) => newSelected.delete(path)) - setSelectedFiles(newSelected) - } - - const handleLogin = () => { - // Redirect to external auth page (configured via env var) - const authUrl = process.env.NEXT_PUBLIC_AUTH_URL || "/auth/login" - window.location.href = authUrl - } - - const handleLogout = () => { - // Handle logout (would typically clear tokens, etc.) - setIsLoggedIn(false) - // Could also redirect to logout endpoint - } - - const handleFolderUpload = async (event: React.ChangeEvent, folderPath: string) => { - const uploadedFiles = event.target.files - if (!uploadedFiles || uploadedFiles.length === 0) { - setUploadToFolder(null) - return - } - - // Validate file count - if (uploadedFiles.length > UPLOAD_MAX_FILES) { - toast({ - title: "Too many files", - description: `You can only upload up to ${UPLOAD_MAX_FILES} files at once`, - variant: "destructive" - }) - setUploadToFolder(null) - return - } - - // Validate file sizes - const oversizedFiles = Array.from(uploadedFiles).filter(file => file.size > UPLOAD_MAX_FILE_SIZE) - if (oversizedFiles.length > 0) { - toast({ - title: "Files too large", - description: `Maximum file size is ${formatFileSize(UPLOAD_MAX_FILE_SIZE)}. Found ${oversizedFiles.length} oversized file(s)`, - variant: "destructive" - }) - setUploadToFolder(null) - return - } - - setUploading(true) - let successCount = 0 - let errorCount = 0 - - try { - // Upload files sequentially to the target folder - for (const file of Array.from(uploadedFiles)) { - try { - const formData = new FormData() - formData.append('file', file) - - // Construct the upload path (folder + filename) - const uploadPath = `${folderPath.replace(/^\//, '')}/${file.name}` - - const response = await fetch(`/api/fs/${encodeURIComponent(uploadPath)}`, { - method: 'PUT', - headers: { - 'AUTH': '1' // Development auth header - }, - body: formData - }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || `Upload failed with status ${response.status}`) - } - - successCount++ - } catch (error) { - console.error(`Failed to upload ${file.name} to ${folderPath}:`, error) - errorCount++ - } - } - - // Show results - if (successCount > 0) { - toast({ - title: "Upload successful", - description: `${successCount} file(s) uploaded to folder${errorCount > 0 ? `, ${errorCount} failed` : ''}` - }) - - // Refresh the file tree - await refreshFileTree() - } - - if (errorCount > 0 && successCount === 0) { - toast({ - title: "Upload failed", - description: `All ${errorCount} file(s) failed to upload to folder`, - variant: "destructive" - }) - } - - } catch (error) { - console.error('Folder upload error:', error) - toast({ - title: "Upload failed", - description: error instanceof Error ? error.message : 'Unknown error occurred', - variant: "destructive" - }) - } finally { - setUploading(false) - // Reset the input - event.target.value = '' - setUploadToFolder(null) - } - } - - const openFolderUpload = (folderPath: string) => { - setUploadToFolder(folderPath) - // Trigger file input click after state is set - setTimeout(() => { - const input = document.getElementById(`folder-upload-${folderPath}`) as HTMLInputElement - input?.click() - }, 0) - } - - const renderFileRow = (item: DriveTreeNode, level = 0): React.ReactNode[] => { - const isExpanded = expandedFolders.has(item.path) - const isSelected = selectedFiles.has(item.path) - const rows: React.ReactNode[] = [] - - rows.push( - toggleFolder(item.path) : undefined} - > - e.stopPropagation()}> - toggleFileSelection(item.path)} /> - - -
- {item.type === "dir" && ( - - )} - {item.type === "dir" ? ( - - ) : ( - - )} -{item.type === "file" && item.blob ? ( - - {item.name} - - ) : ( - {item.name} - )} -
-
- {formatFileSize(item.size || 0)} - {formatDate(item.lastmod)} - e.stopPropagation()}> - - - - - - {item.type === "dir" && ( - <> - openFolderUpload(item.path)} - disabled={uploading} - > - - {uploading ? "Uploading..." : "Upload to Folder"} - - - - )} - openRenameDialog(item)}> - - Rename - - copyPermalink(item)}> - - Copy Permalink - - openInfoDialog(item)}> - - Info - - - { - if (selectedFiles.size > 0) { - console.log("Moving selected files to:", item.type === "dir" ? item.path : "parent of " + item.path) - setSelectedFiles(new Set()) - } - }} - disabled={selectedFiles.size === 0} - className={selectedFiles.size === 0 ? "opacity-50 cursor-not-allowed" : ""} - > - - Move Here {selectedFiles.size > 0 && `(${selectedFiles.size})`} - - deleteItems([item.path])} className="text-red-600"> - - Delete - - - - -
, - ) - - if (item.type === "dir" && item.children && isExpanded) { - item.children.forEach((child) => { - rows.push(...renderFileRow(child, level + 1)) - }) - } - - return rows - } - - return ( -
- {/* Header */} -
-
-
- -

My Drive

-
-
- - -
-
-
- {currentView === "drive" && ( - - )} - {isLoggedIn ? ( - - ) : ( - - )} -
-
- - {currentView === "drive" ? ( - <> - {/* Loading State */} - {loading && ( -
-
-
-

Loading drive contents...

-
-
- )} - - {/* Error State */} - {error && ( -
-
- Error loading drive data: {error} -
-
- )} - - {/* Storage Info */} - {!loading && !error && ( -
-
- Storage Usage - - {formatFileSize(usedStorage)} of {formatFileSize(maxStorage)} used - -
- -
- {storagePercentage.toFixed(1)}% used - {formatFileSize(maxStorage - usedStorage)} available -
-
- )} - - {/* Bulk Actions */} - {!loading && !error && selectedFiles.size > 0 && ( -
-
- - {selectedFiles.size} item{selectedFiles.size !== 1 ? "s" : ""} selected - - -
-
- -
-
- )} - - {/* File Table */} - {!loading && !error && ( -
- - - - - Name - Size - Modified - Actions - - - {files.flatMap((file) => renderFileRow(file))} -
-
- )} - - ) : ( - - )} - - {/* Rename Dialog */} - - - - Rename {currentItem?.type === "dir" ? "Folder" : "File"} - -
-
- - setNewName(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - handleRename() - } - }} - placeholder="Enter new name" - /> -
-
- - -
-
-
-
- - {/* Info Dialog */} - - - - - {currentItem?.type === "dir" ? ( - - ) : ( - - )} - {currentItem?.type === "dir" ? "Folder" : "File"} Information - - - {currentItem && ( -
-
-
- -

{currentItem.name}

-
-
- -

{formatFileSize(currentItem.size || 0)}

-
-
- -

{formatDateTime(currentItem.lastmod)}

-
-
- -

{currentItem.author}

-
-
- -

{currentItem.type}

-
-
- -

{currentItem.path}

-
-
-
- -
-
- )} -
-
- - {/* Hidden file inputs for folder uploads */} - {(() => { - const getAllFolders = (items: DriveTreeNode[]): DriveTreeNode[] => { - const folders: DriveTreeNode[] = [] - items.forEach((item) => { - if (item.type === "dir") { - folders.push(item) - if (item.children) { - folders.push(...getAllFolders(item.children)) - } - } - }) - return folders - } - return getAllFolders(files).map((folder) => ( - handleFolderUpload(e, folder.path)} - /> - )) - })()} -
- ) -} diff --git a/frontend/history-view.tsx b/frontend/history-view.tsx deleted file mode 100644 index 12af1ea..0000000 --- a/frontend/history-view.tsx +++ /dev/null @@ -1,252 +0,0 @@ -"use client" - -import { useState, useEffect } from "react" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { FileText, Folder, Trash2, Edit, Plus, ChevronLeft, ChevronRight } from "lucide-react" -import { DriveLogEntry } from "@/lib/drive_types" -import { formatFileSize } from "@/lib/utils" - - -function formatTimestamp(timestamp: number): string { - const date = new Date(timestamp * 1000) // Convert Unix timestamp to milliseconds - return date.toLocaleString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - hour12: true, - }) -} - -function getActionIcon(action: string) { - switch (action) { - case "create_file": - return - case "create_dir": - return - case "remove": - return - default: - return - } -} - -function getActionBadge(action: string) { - switch (action) { - case "create_file": - return ( - - File Created - - ) - case "create_dir": - return ( - - Directory Created - - ) - case "remove": - return ( - - File/Directory Removed - - ) - default: - return ( - - {action} - - ) - } -} - -const ENTRIES_PER_PAGE = 100 - -export default function HistoryView() { - const [logEntries, setLogEntries] = useState([]) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - const [currentPage, setCurrentPage] = useState(1) - - useEffect(() => { - async function fetchLogEntries() { - try { - setLoading(true) - const response = await fetch('/api/log') - if (!response.ok) { - throw new Error('Failed to fetch log entries') - } - const data: DriveLogEntry[] = await response.json() - // Reverse to show latest entries first - setLogEntries(data.reverse()) - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error') - } finally { - setLoading(false) - } - } - - fetchLogEntries() - }, []) - - // Calculate pagination values - const totalPages = Math.ceil(logEntries.length / ENTRIES_PER_PAGE) - const startIndex = (currentPage - 1) * ENTRIES_PER_PAGE - const endIndex = startIndex + ENTRIES_PER_PAGE - const currentEntries = logEntries.slice(startIndex, endIndex) - - const handlePageChange = (page: number) => { - setCurrentPage(page) - } - - const handlePreviousPage = () => { - setCurrentPage(prev => Math.max(1, prev - 1)) - } - - const handleNextPage = () => { - setCurrentPage(prev => Math.min(totalPages, prev + 1)) - } - - if (loading) { - return ( -
-
-
-

Activity History

-

Recent filesystem modifications and changes

-
-
-
-

Loading history...

-
-
- ) - } - - if (error) { - return ( -
-
-
-

Activity History

-

Recent filesystem modifications and changes

-
-
-
-

Error: {error}

-
-
- ) - } - - return ( -
- {/* History Header */} -
-
-

Activity History

-

Recent filesystem modifications and changes

-
- {logEntries.length} total entries -
- - {/* History Table */} -
- - - - Type - File/Directory Name - Time - User - Size - - - - {currentEntries.map((entry) => ( - - {getActionBadge(entry.action)} - - {entry.path} - - - {formatTimestamp(entry.timestamp)} - - - {entry.email} - - - - {entry.size ? formatFileSize(entry.size) : "—"} - - - - ))} - -
-
- - {/* Pagination */} - {totalPages > 1 && ( -
-
- Showing {startIndex + 1} to {Math.min(endIndex, logEntries.length)} of {logEntries.length} entries -
-
- - -
- {Array.from({ length: totalPages }, (_, i) => i + 1) - .filter(page => { - // Show first page, last page, current page, and pages around current - return page === 1 || - page === totalPages || - Math.abs(page - currentPage) <= 2 - }) - .map((page, index, filteredPages) => { - // Add ellipsis where there are gaps - const showEllipsisBefore = index > 0 && page - filteredPages[index - 1] > 1 - return ( -
- {showEllipsisBefore && ( - ... - )} - -
- ) - })} -
- - -
-
- )} -
- ) -} -- cgit