summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--frontend/file-drive.tsx247
-rw-r--r--frontend/lib/constants.ts4
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"
37import { toast } from "@/hooks/use-toast" 37import { toast } from "@/hooks/use-toast"
38import HistoryView from "./history-view" 38import HistoryView from "./history-view"
39import { DriveTreeResponse, DriveTreeNode } from "@/lib/drive_types" 39import { DriveTreeResponse, DriveTreeNode } from "@/lib/drive_types"
40import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from "@/lib/constants"
40 41
41 42
42function formatFileSize(bytes: number): string { 43function 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
2export const UPLOAD_MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB 2export const UPLOAD_MAX_FILE_SIZE = 4096 * 1024 * 1024; // 100MB
3export const UPLOAD_MAX_FILES = 10; // Maximum files per upload 3export const UPLOAD_MAX_FILES = 10; // Maximum files per upload
4export const UPLOAD_ALLOWED_TYPES = [ 4export 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