diff options
| -rw-r--r-- | frontend/file-drive.tsx | 247 | ||||
| -rw-r--r-- | frontend/lib/constants.ts | 4 |
2 files changed, 202 insertions, 49 deletions
diff --git a/frontend/file-drive.tsx b/frontend/file-drive.tsx index 2784c1a..123f088 100644 --- a/frontend/file-drive.tsx +++ b/frontend/file-drive.tsx | |||
| @@ -37,6 +37,7 @@ import { Checkbox } from "@/components/ui/checkbox" | |||
| 37 | import { toast } from "@/hooks/use-toast" | 37 | import { toast } from "@/hooks/use-toast" |
| 38 | import HistoryView from "./history-view" | 38 | import HistoryView from "./history-view" |
| 39 | import { DriveTreeResponse, DriveTreeNode } from "@/lib/drive_types" | 39 | import { DriveTreeResponse, DriveTreeNode } from "@/lib/drive_types" |
| 40 | import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from "@/lib/constants" | ||
| 40 | 41 | ||
| 41 | 42 | ||
| 42 | function formatFileSize(bytes: number): string { | 43 | function formatFileSize(bytes: number): string { |
| @@ -94,6 +95,7 @@ export default function FileDrive() { | |||
| 94 | const [currentItem, setCurrentItem] = useState<DriveTreeNode | null>(null) | 95 | const [currentItem, setCurrentItem] = useState<DriveTreeNode | null>(null) |
| 95 | const [newName, setNewName] = useState("") | 96 | const [newName, setNewName] = useState("") |
| 96 | const fileInputRef = useRef<HTMLInputElement>(null) | 97 | const fileInputRef = useRef<HTMLInputElement>(null) |
| 98 | const [uploading, setUploading] = useState(false) | ||
| 97 | 99 | ||
| 98 | const [isLoggedIn, setIsLoggedIn] = useState(true) // Mock logged in state | 100 | const [isLoggedIn, setIsLoggedIn] = useState(true) // Mock logged in state |
| 99 | const [currentView, setCurrentView] = useState<"drive" | "history">("drive") | 101 | const [currentView, setCurrentView] = useState<"drive" | "history">("drive") |
| @@ -103,6 +105,21 @@ export default function FileDrive() { | |||
| 103 | const usedStorage = 0;//calculateTotalSize(files) | 105 | const usedStorage = 0;//calculateTotalSize(files) |
| 104 | const storagePercentage = (usedStorage / maxStorage) * 100 | 106 | const storagePercentage = (usedStorage / maxStorage) * 100 |
| 105 | 107 | ||
| 108 | // Function to refresh file tree | ||
| 109 | const refreshFileTree = async () => { | ||
| 110 | try { | ||
| 111 | const treeResponse = await fetchDriveTree() | ||
| 112 | setFiles(treeResponse.root) | ||
| 113 | } catch (err) { | ||
| 114 | console.error('Error refreshing file tree:', err) | ||
| 115 | toast({ | ||
| 116 | title: "Failed to refresh", | ||
| 117 | description: "Could not refresh file list after upload", | ||
| 118 | variant: "destructive" | ||
| 119 | }) | ||
| 120 | } | ||
| 121 | } | ||
| 122 | |||
| 106 | // Load drive data on component mount | 123 | // Load drive data on component mount |
| 107 | useEffect(() => { | 124 | useEffect(() => { |
| 108 | async function loadDriveData() { | 125 | async function loadDriveData() { |
| @@ -205,19 +222,92 @@ export default function FileDrive() { | |||
| 205 | } | 222 | } |
| 206 | } | 223 | } |
| 207 | 224 | ||
| 208 | const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => { | 225 | const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => { |
| 209 | const uploadedFiles = event.target.files | 226 | const uploadedFiles = event.target.files |
| 210 | if (uploadedFiles) { | 227 | if (!uploadedFiles || uploadedFiles.length === 0) return |
| 211 | const newFiles = Array.from(uploadedFiles).map((file, index) => ({ | 228 | |
| 212 | path: `/upload-${Date.now()}-${index}`, | 229 | // Validate file count |
| 213 | name: file.name, | 230 | if (uploadedFiles.length > UPLOAD_MAX_FILES) { |
| 214 | type: "file" as const, | 231 | toast({ |
| 215 | lastmod: Math.floor(Date.now() / 1000), | 232 | title: "Too many files", |
| 216 | blob: null, | 233 | description: `You can only upload up to ${UPLOAD_MAX_FILES} files at once`, |
| 217 | size: file.size, | 234 | variant: "destructive" |
| 218 | author: "Current User", | 235 | }) |
| 219 | })) | 236 | return |
| 220 | setFiles([...files, ...newFiles]) | 237 | } |
| 238 | |||
| 239 | // Validate file sizes | ||
| 240 | const oversizedFiles = Array.from(uploadedFiles).filter(file => file.size > UPLOAD_MAX_FILE_SIZE) | ||
| 241 | if (oversizedFiles.length > 0) { | ||
| 242 | toast({ | ||
| 243 | title: "Files too large", | ||
| 244 | description: `Maximum file size is ${formatFileSize(UPLOAD_MAX_FILE_SIZE)}. Found ${oversizedFiles.length} oversized file(s)`, | ||
| 245 | variant: "destructive" | ||
| 246 | }) | ||
| 247 | return | ||
| 248 | } | ||
| 249 | |||
| 250 | setUploading(true) | ||
| 251 | let successCount = 0 | ||
| 252 | let errorCount = 0 | ||
| 253 | |||
| 254 | try { | ||
| 255 | // Upload files sequentially to avoid overwhelming the server | ||
| 256 | for (const file of Array.from(uploadedFiles)) { | ||
| 257 | try { | ||
| 258 | const formData = new FormData() | ||
| 259 | formData.append('file', file) | ||
| 260 | |||
| 261 | const response = await fetch(`/api/fs/${encodeURIComponent(file.name)}`, { | ||
| 262 | method: 'PUT', | ||
| 263 | headers: { | ||
| 264 | 'AUTH': '1' // Development auth header | ||
| 265 | }, | ||
| 266 | body: formData | ||
| 267 | }) | ||
| 268 | |||
| 269 | if (!response.ok) { | ||
| 270 | const error = await response.json() | ||
| 271 | throw new Error(error.error || `Upload failed with status ${response.status}`) | ||
| 272 | } | ||
| 273 | |||
| 274 | successCount++ | ||
| 275 | } catch (error) { | ||
| 276 | console.error(`Failed to upload ${file.name}:`, error) | ||
| 277 | errorCount++ | ||
| 278 | } | ||
| 279 | } | ||
| 280 | |||
| 281 | // Show results | ||
| 282 | if (successCount > 0) { | ||
| 283 | toast({ | ||
| 284 | title: "Upload successful", | ||
| 285 | description: `${successCount} file(s) uploaded successfully${errorCount > 0 ? `, ${errorCount} failed` : ''}` | ||
| 286 | }) | ||
| 287 | |||
| 288 | // Refresh the file tree | ||
| 289 | await refreshFileTree() | ||
| 290 | } | ||
| 291 | |||
| 292 | if (errorCount > 0 && successCount === 0) { | ||
| 293 | toast({ | ||
| 294 | title: "Upload failed", | ||
| 295 | description: `All ${errorCount} file(s) failed to upload`, | ||
| 296 | variant: "destructive" | ||
| 297 | }) | ||
| 298 | } | ||
| 299 | |||
| 300 | } catch (error) { | ||
| 301 | console.error('Upload error:', error) | ||
| 302 | toast({ | ||
| 303 | title: "Upload failed", | ||
| 304 | description: error instanceof Error ? error.message : 'Unknown error occurred', | ||
| 305 | variant: "destructive" | ||
| 306 | }) | ||
| 307 | } finally { | ||
| 308 | setUploading(false) | ||
| 309 | // Reset the input | ||
| 310 | event.target.value = '' | ||
| 221 | } | 311 | } |
| 222 | } | 312 | } |
| 223 | 313 | ||
| @@ -250,45 +340,102 @@ export default function FileDrive() { | |||
| 250 | // Could also redirect to logout endpoint | 340 | // Could also redirect to logout endpoint |
| 251 | } | 341 | } |
| 252 | 342 | ||
| 253 | const handleFolderUpload = (event: React.ChangeEvent<HTMLInputElement>, folderPath: string) => { | 343 | const handleFolderUpload = async (event: React.ChangeEvent<HTMLInputElement>, folderPath: string) => { |
| 254 | const uploadedFiles = event.target.files | 344 | const uploadedFiles = event.target.files |
| 255 | if (uploadedFiles) { | 345 | if (!uploadedFiles || uploadedFiles.length === 0) { |
| 256 | const newFiles = Array.from(uploadedFiles).map((file, index) => ({ | 346 | setUploadToFolder(null) |
| 257 | path: `${folderPath}/${file.name}`, | 347 | return |
| 258 | name: file.name, | 348 | } |
| 259 | type: "file" as const, | 349 | |
| 260 | lastmod: Math.floor(Date.now() / 1000), | 350 | // Validate file count |
| 261 | blob: null, | 351 | if (uploadedFiles.length > UPLOAD_MAX_FILES) { |
| 262 | size: file.size, | 352 | toast({ |
| 263 | author: "Current User", | 353 | title: "Too many files", |
| 264 | })) | 354 | description: `You can only upload up to ${UPLOAD_MAX_FILES} files at once`, |
| 265 | 355 | variant: "destructive" | |
| 266 | // Add files to the specific folder | 356 | }) |
| 267 | const addToFolder = (items: DriveTreeNode[]): DriveTreeNode[] => { | 357 | setUploadToFolder(null) |
| 268 | return items.map((item) => { | 358 | return |
| 269 | if (item.path === folderPath && item.type === "dir") { | 359 | } |
| 270 | return { | 360 | |
| 271 | ...item, | 361 | // Validate file sizes |
| 272 | children: [...(item.children || []), ...newFiles], | 362 | const oversizedFiles = Array.from(uploadedFiles).filter(file => file.size > UPLOAD_MAX_FILE_SIZE) |
| 273 | size: (item.size || 0) + newFiles.reduce((total, file) => total + file.size, 0), | 363 | if (oversizedFiles.length > 0) { |
| 274 | } | 364 | toast({ |
| 275 | } | 365 | title: "Files too large", |
| 276 | if (item.children) { | 366 | description: `Maximum file size is ${formatFileSize(UPLOAD_MAX_FILE_SIZE)}. Found ${oversizedFiles.length} oversized file(s)`, |
| 277 | return { ...item, children: addToFolder(item.children) } | 367 | variant: "destructive" |
| 368 | }) | ||
| 369 | setUploadToFolder(null) | ||
| 370 | return | ||
| 371 | } | ||
| 372 | |||
| 373 | setUploading(true) | ||
| 374 | let successCount = 0 | ||
| 375 | let errorCount = 0 | ||
| 376 | |||
| 377 | try { | ||
| 378 | // Upload files sequentially to the target folder | ||
| 379 | for (const file of Array.from(uploadedFiles)) { | ||
| 380 | try { | ||
| 381 | const formData = new FormData() | ||
| 382 | formData.append('file', file) | ||
| 383 | |||
| 384 | // Construct the upload path (folder + filename) | ||
| 385 | const uploadPath = `${folderPath.replace(/^\//, '')}/${file.name}` | ||
| 386 | |||
| 387 | const response = await fetch(`/api/fs/${encodeURIComponent(uploadPath)}`, { | ||
| 388 | method: 'PUT', | ||
| 389 | headers: { | ||
| 390 | 'AUTH': '1' // Development auth header | ||
| 391 | }, | ||
| 392 | body: formData | ||
| 393 | }) | ||
| 394 | |||
| 395 | if (!response.ok) { | ||
| 396 | const error = await response.json() | ||
| 397 | throw new Error(error.error || `Upload failed with status ${response.status}`) | ||
| 278 | } | 398 | } |
| 279 | return item | 399 | |
| 400 | successCount++ | ||
| 401 | } catch (error) { | ||
| 402 | console.error(`Failed to upload ${file.name} to ${folderPath}:`, error) | ||
| 403 | errorCount++ | ||
| 404 | } | ||
| 405 | } | ||
| 406 | |||
| 407 | // Show results | ||
| 408 | if (successCount > 0) { | ||
| 409 | toast({ | ||
| 410 | title: "Upload successful", | ||
| 411 | description: `${successCount} file(s) uploaded to folder${errorCount > 0 ? `, ${errorCount} failed` : ''}` | ||
| 412 | }) | ||
| 413 | |||
| 414 | // Refresh the file tree | ||
| 415 | await refreshFileTree() | ||
| 416 | } | ||
| 417 | |||
| 418 | if (errorCount > 0 && successCount === 0) { | ||
| 419 | toast({ | ||
| 420 | title: "Upload failed", | ||
| 421 | description: `All ${errorCount} file(s) failed to upload to folder`, | ||
| 422 | variant: "destructive" | ||
| 280 | }) | 423 | }) |
| 281 | } | 424 | } |
| 282 | 425 | ||
| 283 | setFiles(addToFolder(files)) | 426 | } catch (error) { |
| 427 | console.error('Folder upload error:', error) | ||
| 284 | toast({ | 428 | toast({ |
| 285 | title: "Files uploaded successfully", | 429 | title: "Upload failed", |
| 286 | description: `${newFiles.length} file(s) uploaded to folder`, | 430 | description: error instanceof Error ? error.message : 'Unknown error occurred', |
| 431 | variant: "destructive" | ||
| 287 | }) | 432 | }) |
| 433 | } finally { | ||
| 434 | setUploading(false) | ||
| 435 | // Reset the input | ||
| 436 | event.target.value = '' | ||
| 437 | setUploadToFolder(null) | ||
| 288 | } | 438 | } |
| 289 | // Reset the input | ||
| 290 | event.target.value = "" | ||
| 291 | setUploadToFolder(null) | ||
| 292 | } | 439 | } |
| 293 | 440 | ||
| 294 | const openFolderUpload = (folderPath: string) => { | 441 | const openFolderUpload = (folderPath: string) => { |
| @@ -352,9 +499,12 @@ export default function FileDrive() { | |||
| 352 | <DropdownMenuContent align="end"> | 499 | <DropdownMenuContent align="end"> |
| 353 | {item.type === "dir" && ( | 500 | {item.type === "dir" && ( |
| 354 | <> | 501 | <> |
| 355 | <DropdownMenuItem onClick={() => openFolderUpload(item.path)}> | 502 | <DropdownMenuItem |
| 503 | onClick={() => openFolderUpload(item.path)} | ||
| 504 | disabled={uploading} | ||
| 505 | > | ||
| 356 | <Upload className="mr-2 h-4 w-4" /> | 506 | <Upload className="mr-2 h-4 w-4" /> |
| 357 | Upload to Folder | 507 | {uploading ? "Uploading..." : "Upload to Folder"} |
| 358 | </DropdownMenuItem> | 508 | </DropdownMenuItem> |
| 359 | <DropdownMenuSeparator /> | 509 | <DropdownMenuSeparator /> |
| 360 | </> | 510 | </> |
| @@ -434,9 +584,12 @@ export default function FileDrive() { | |||
| 434 | </div> | 584 | </div> |
| 435 | <div className="flex items-center gap-2"> | 585 | <div className="flex items-center gap-2"> |
| 436 | {currentView === "drive" && ( | 586 | {currentView === "drive" && ( |
| 437 | <Button onClick={() => fileInputRef.current?.click()}> | 587 | <Button |
| 588 | onClick={() => fileInputRef.current?.click()} | ||
| 589 | disabled={uploading} | ||
| 590 | > | ||
| 438 | <Upload className="mr-2 h-4 w-4" /> | 591 | <Upload className="mr-2 h-4 w-4" /> |
| 439 | Upload Files | 592 | {uploading ? "Uploading..." : "Upload Files"} |
| 440 | </Button> | 593 | </Button> |
| 441 | )} | 594 | )} |
| 442 | {isLoggedIn ? ( | 595 | {isLoggedIn ? ( |
diff --git a/frontend/lib/constants.ts b/frontend/lib/constants.ts index 8f74dd1..173e90d 100644 --- a/frontend/lib/constants.ts +++ b/frontend/lib/constants.ts | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | // Upload configuration constants | 1 | // Upload configuration constants |
| 2 | export const UPLOAD_MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB | 2 | export const UPLOAD_MAX_FILE_SIZE = 4096 * 1024 * 1024; // 100MB |
| 3 | export const UPLOAD_MAX_FILES = 10; // Maximum files per upload | 3 | export const UPLOAD_MAX_FILES = 10; // Maximum files per upload |
| 4 | export const UPLOAD_ALLOWED_TYPES = [ | 4 | export const UPLOAD_ALLOWED_TYPES = [ |
| 5 | // Documents | 5 | // Documents |
| @@ -36,4 +36,4 @@ export const UPLOAD_ALLOWED_TYPES = [ | |||
| 36 | 'text/html', | 36 | 'text/html', |
| 37 | 'text/css', | 37 | 'text/css', |
| 38 | 'application/xml' | 38 | 'application/xml' |
| 39 | ]; // Empty array means all types allowed \ No newline at end of file | 39 | ]; // Empty array means all types allowed |
