diff --git a/backend/handlers/files.go b/backend/handlers/files.go index ffa0c3f..3c7142a 100644 --- a/backend/handlers/files.go +++ b/backend/handlers/files.go @@ -433,14 +433,18 @@ func (h *FSHandler) Move(c *fiber.Ctx) error { uniqueName := GetUniquePath(dstFull, filepath.Base(srcFull)) dstFull = filepath.Join(dstFull, uniqueName) body.DestPath = filepath.Join(body.DestPath, uniqueName) - } else { - // If destination is not a directory but the file exists (rename), we don't overwrite if they want `(1)` - // Wait, if they intentionally rename to an existing file, should it append (1)? - // Yes, to prevent accidental overwrites. - parentDir := filepath.Dir(dstFull) - uniqueName := GetUniquePath(parentDir, filepath.Base(dstFull)) - dstFull = filepath.Join(parentDir, uniqueName) - body.DestPath = filepath.Join(filepath.Dir(body.DestPath), uniqueName) + } else if err == nil { + if body.Resolution == "skip" { + return c.JSON(fiber.Map{"message": "skipped"}) + } + if body.Resolution != "overwrite" { + parentDir := filepath.Dir(dstFull) + uniqueName := GetUniquePath(parentDir, filepath.Base(dstFull)) + dstFull = filepath.Join(parentDir, uniqueName) + body.DestPath = filepath.Join(filepath.Dir(body.DestPath), uniqueName) + } else { + os.RemoveAll(dstFull) + } } if err := os.Rename(srcFull, dstFull); err != nil { @@ -457,13 +461,16 @@ func (h *FSHandler) Move(c *fiber.Ctx) error { return c.JSON(fiber.Map{"message": "moved successfully"}) } +type CopyRequest struct { + SourcePath string `json:"source_path"` + DestPath string `json:"dest_path"` + Resolution string `json:"resolution"` // "overwrite", "rename", "skip" +} + // Copy copies a file or folder. // POST /api/files/copy func (h *FSHandler) Copy(c *fiber.Ctx) error { - var body struct { - SourcePath string `json:"source_path"` - DestPath string `json:"dest_path"` - } + var body CopyRequest if err := c.BodyParser(&body); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } @@ -483,12 +490,27 @@ func (h *FSHandler) Copy(c *fiber.Ctx) error { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "source not found"}) } + // Handle conflict + finalDst := dstFull + if _, err := os.Stat(dstFull); err == nil { + if body.Resolution == "skip" { + return c.JSON(fiber.Map{"message": "skipped"}) + } + if body.Resolution != "overwrite" { + parentDir := filepath.Dir(dstFull) + uniqueName := GetUniquePath(parentDir, filepath.Base(dstFull)) + finalDst = filepath.Join(parentDir, uniqueName) + } else { + os.RemoveAll(dstFull) + } + } + if info.IsDir() { - if err := copyDir(srcFull, dstFull); err != nil { + if err := copyDir(srcFull, finalDst); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to copy directory"}) } } else { - if err := copyFile(srcFull, dstFull); err != nil { + if err := copyFile(srcFull, finalDst); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to copy file"}) } } @@ -516,6 +538,16 @@ func (h *FSHandler) Delete(c *fiber.Ctx) error { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to prepare trash"}) } + info, err := os.Stat(resolvedPath) + isDir := 0 + size := int64(0) + if err == nil { + if info.IsDir() { + isDir = 1 + } + size = info.Size() + } + if err := os.Rename(resolvedPath, trashPath); err != nil { fmt.Printf("Rename error %s -> %s: %v. Attempting copy & delete fallback...\n", resolvedPath, trashPath, err) if err := copyFile(resolvedPath, trashPath); err != nil { @@ -527,14 +559,14 @@ func (h *FSHandler) Delete(c *fiber.Ctx) error { // Update DB cleanPath := filepath.ToSlash(relativePath) h.DB.Exec( - `UPDATE files SET is_trashed = 1, trashed_at = CURRENT_TIMESTAMP, original_path = ? WHERE path = ?`, - cleanPath, cleanPath, + `UPDATE files SET is_trashed = 1, trashed_at = CURRENT_TIMESTAMP, original_path = ?, is_dir = ?, size = ? WHERE path = ?`, + cleanPath, isDir, size, cleanPath, ) // Also insert if not tracked h.DB.Exec( - `INSERT OR IGNORE INTO files (path, name, is_trashed, trashed_at, original_path) VALUES (?, ?, 1, CURRENT_TIMESTAMP, ?)`, - cleanPath, filepath.Base(cleanPath), cleanPath, + `INSERT OR IGNORE INTO files (path, name, is_trashed, trashed_at, original_path, is_dir, size) VALUES (?, ?, 1, CURRENT_TIMESTAMP, ?, ?, ?)`, + cleanPath, filepath.Base(cleanPath), cleanPath, isDir, size, ) h.DB.AddAuditLog("deleted", cleanPath, c.IP()) @@ -1153,3 +1185,32 @@ func (h *FSHandler) ServeThumbnail(c *fiber.Ctx) error { return c.SendFile(thumbPath) } + +type CheckConflictsRequest struct { + DestPath string `json:"dest_path"` + Files []string `json:"files"` +} + +// CheckConflicts checks if files exist in the destination. +// POST /api/files/check-conflicts +func (h *FSHandler) CheckConflicts(c *fiber.Ctx) error { + var req CheckConflictsRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid body"}) + } + + destFull, err := resolveSafe(h.Config.StorageDir, req.DestPath) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid path"}) + } + + conflicts := []string{} + for _, file := range req.Files { + filePath := filepath.Join(destFull, file) + if _, err := os.Stat(filePath); err == nil { + conflicts = append(conflicts, file) + } + } + + return c.JSON(fiber.Map{"conflicts": conflicts}) +} diff --git a/backend/handlers/tus.go b/backend/handlers/tus.go index aaa78a6..54efc25 100644 --- a/backend/handlers/tus.go +++ b/backend/handlers/tus.go @@ -107,6 +107,7 @@ func (h *TusHandler) Create(c *fiber.Ctx) error { metadata := parseTusMetadata(c.Get("Upload-Metadata")) fileName := metadata["filename"] destPath := metadata["path"] + overwrite := metadata["overwrite"] == "true" if fileName == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "filename metadata is required"}) @@ -115,13 +116,15 @@ func (h *TusHandler) Create(c *fiber.Ctx) error { destPath = "." } - // Resolve destination directory and ensure unique filename destFull, err := resolveSafe(h.Config.StorageDir, destPath) if err != nil { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "invalid destination path"}) } - - uniqueFileName := GetUniquePath(destFull, fileName) + + uniqueFileName := fileName + if !overwrite { + uniqueFileName = GetUniquePath(destFull, fileName) + } // Generate unique upload ID uploadID, err := generateUploadID() diff --git a/backend/main.go b/backend/main.go index bb4f7c7..f8cac22 100644 --- a/backend/main.go +++ b/backend/main.go @@ -181,6 +181,7 @@ func main() { protected.Post("/files/move", fsHandler.Move) protected.Post("/files/copy", fsHandler.Copy) protected.Post("/files/upload", fsHandler.Upload) + protected.Post("/files/check-conflicts", fsHandler.CheckConflicts) protected.Get("/files/folders-tree", fsHandler.GetFolderTree) diff --git a/frontend/src/components/FileExplorer.tsx b/frontend/src/components/FileExplorer.tsx index 75d69eb..369b24a 100644 --- a/frontend/src/components/FileExplorer.tsx +++ b/frontend/src/components/FileExplorer.tsx @@ -98,10 +98,13 @@ function FolderTreeNode({ type ViewMode = 'grid' | 'list'; type SidebarSection = 'files' | 'trash' | 'storage' | 'settings' | 'shared'; -interface UploadProgress { +interface UploadBatch { id: string; name: string; - progress: number; + totalBytes: number; + uploadedBytes: number; + totalFiles: number; + filesUploaded: number; status: 'uploading' | 'complete' | 'error'; error?: string; } @@ -242,9 +245,16 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { const [sortBy, setSortBy] = useState<'name' | 'size' | 'date' | 'type'>('name'); const [sortAsc, setSortAsc] = useState(true); const [draggedFile, setDraggedFile] = useState(null); - const [uploads, setUploads] = useState([]); + const [uploads, setUploads] = useState([]); const [toasts, setToasts] = useState([]); const [deleteConfirm, setDeleteConfirm] = useState<{ type: 'single' | 'multiple'; file?: FileItem; count?: number } | null>(null); + const [conflictState, setConflictState] = useState<{ + type: 'upload' | 'move' | 'copy'; + files?: {file: File, path: string}[]; + sourcePaths?: string[]; + destPath: string; + conflicts: string[]; + } | null>(null); function showToast(message: string, type: 'success' | 'error' | 'info' = 'success') { const id = Date.now() + Math.random(); @@ -688,7 +698,14 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { async function handleMoveExecute() { if (movingFiles.length === 0) return; try { - await Promise.all(movingFiles.map(src => api.move(src, moveModalPath === '.' ? '.' : moveModalPath))); + const destPath = moveModalPath === '.' ? '.' : moveModalPath; + const fileNames = movingFiles.map(src => src.split('/').pop() || src); + const res = await api.checkConflicts(destPath, fileNames); + if (res.conflicts?.length) { + setConflictState({ type: 'move', sourcePaths: movingFiles, destPath, conflicts: res.conflicts }); + return; + } + await Promise.all(movingFiles.map(src => api.move(src, destPath))); setShowMoveModal(false); setMovingFiles([]); loadFiles(undefined, true); @@ -699,6 +716,59 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { } } + async function handleDropOnFolder(file: FileItem, targetFolder: FileItem) { + if (file.path === targetFolder.path) return; + try { + const res = await api.checkConflicts(targetFolder.path, [file.name]); + if (res.conflicts?.length) { + setConflictState({ type: 'move', sourcePaths: [file.path], destPath: targetFolder.path, conflicts: res.conflicts }); + return; + } + await api.move(file.path, targetFolder.path); + loadFiles(); + showToast(`Moved ${file.name}`); + } catch (err) { + console.error(err); + } + } + + async function executeConflictResolution(resolution: string) { + if (!conflictState) return; + const { type, destPath, conflicts } = conflictState; + + if (type === 'upload' && conflictState.files) { + let filesToUpload = conflictState.files; + if (resolution === 'skip') { + filesToUpload = filesToUpload.filter(f => { + const name = f.path ? `${f.path}/${f.file.name}` : f.file.name; + return !conflicts.includes(name); + }); + } + setConflictState(null); + if (filesToUpload.length > 0) { + processUploadBatch(filesToUpload, destPath, resolution === 'overwrite'); + } + } else if (type === 'move' && conflictState.sourcePaths) { + let pathsToMove = conflictState.sourcePaths; + if (resolution === 'skip') { + pathsToMove = pathsToMove.filter(p => !conflicts.includes(p.split('/').pop()!)); + } + setConflictState(null); + if (pathsToMove.length > 0) { + try { + await Promise.all(pathsToMove.map(src => api.move(src, destPath, resolution))); + setShowMoveModal(false); + setMovingFiles([]); + loadFiles(undefined, true); + setSelectedFiles(new Set()); + showToast(`Moved ${pathsToMove.length} item${pathsToMove.length > 1 ? 's' : ''}`); + } catch (e) { + console.error(e); + } + } + } + } + function handleDragOver(e: React.DragEvent) { if (draggedFile) return; if (!e.dataTransfer.types.includes('Files')) return; @@ -748,23 +818,72 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { } } - async function uploadFile(file: File, targetDir?: string) { - const id = `${Date.now()}-${file.name}`; - setUploads(prev => [...prev, { id, name: file.name, progress: 0, status: 'uploading' }]); - try { - const uploadPath = targetDir ? (currentPath === '.' ? targetDir : `${currentPath}/${targetDir}`) : currentPath; - await api.upload(file, uploadPath, { - onProgress: (percent) => { - setUploads(prev => prev.map(u => u.id === id ? { ...u, progress: percent } : u)); - }, - }); - setUploads(prev => prev.map(u => u.id === id ? { ...u, progress: 100, status: 'complete' } : u)); - setTimeout(() => { - setUploads(prev => prev.filter(u => u.id !== id)); - }, 3000); - } catch (err: any) { - setUploads(prev => prev.map(u => u.id === id ? { ...u, status: 'error', error: err.message || 'Upload failed' } : u)); + async function processUploadBatch(entries: {file: File, path: string}[], destPath: string, overwrite: boolean = false) { + if (!entries.length) return; + const batchId = 'batch-' + Date.now(); + const totalFiles = entries.length; + const totalBytes = entries.reduce((acc, curr) => acc + curr.file.size, 0); + const batchName = totalFiles === 1 ? entries[0].file.name : `Uploading ${totalFiles} items`; + + setUploads(prev => [...prev, { + id: batchId, + name: batchName, + totalBytes, + uploadedBytes: 0, + totalFiles, + filesUploaded: 0, + status: 'uploading' + }]); + + const MAX_CONCURRENT = 4; + let currentIndex = 0; + let completedFiles = 0; + const fileProgressMap = new Map(); + + const updateProgress = () => { + let totalUploaded = 0; + fileProgressMap.forEach(bytes => { totalUploaded += bytes; }); + setUploads(prev => prev.map(b => b.id === batchId ? { + ...b, + uploadedBytes: totalUploaded, + filesUploaded: completedFiles, + status: completedFiles === totalFiles ? 'complete' : 'uploading' + } : b)); + }; + + const worker = async () => { + while (currentIndex < entries.length) { + const index = currentIndex++; + const { file, path } = entries[index]; + const uploadPath = path ? (destPath === '.' ? path : `${destPath}/${path}`) : destPath; + + try { + await api.upload(file, uploadPath, { + overwrite, + onProgress: (percent) => { + fileProgressMap.set(file.name + path, (percent / 100) * file.size); + updateProgress(); + } + }); + completedFiles++; + fileProgressMap.set(file.name + path, file.size); + updateProgress(); + } catch (err: any) { + setUploads(prev => prev.map(b => b.id === batchId ? { ...b, status: 'error', error: err.message || 'Upload failed' } : b)); + } + } + }; + + const workers = []; + for (let i = 0; i < Math.min(MAX_CONCURRENT, entries.length); i++) { + workers.push(worker()); } + await Promise.all(workers); + + setTimeout(() => { + setUploads(prev => prev.filter(u => u.id !== batchId)); + loadFiles(undefined, true); + }, 3000); } async function handleDrop(e: React.DragEvent) { @@ -779,7 +898,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { const processEntry = async (entry: any, path: string = '') => { if (entry.isFile) { const file = await new Promise((resolve) => entry.file(resolve)); - filesToUpload.push({ file, path }); + filesToUpload.push({ file, path: path.replace(/\/$/, '') }); } else if (entry.isDirectory) { const dirReader = entry.createReader(); let entries: any[] = []; @@ -812,24 +931,58 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { await Promise.all(promises); if (filesToUpload.length > 0) { - await Promise.all(filesToUpload.map(({file, path}) => - uploadFile(file, path.replace(/\/$/, '')) - )); + // Collect destination paths to create folders + const uniqueDirs = new Set(); + filesToUpload.forEach(({path}) => { + if (path) uniqueDirs.add(currentPath === '.' ? path : `${currentPath}/${path}`); + }); + + for (const dir of Array.from(uniqueDirs)) { + try { await api.createFolder(dir); } catch {} + } + + const fileNames = filesToUpload.map(f => f.path ? `${f.path}/${f.file.name}` : f.file.name); + try { + const res = await api.checkConflicts(currentPath, fileNames); + if (res.conflicts && res.conflicts.length > 0) { + setConflictState({ type: 'upload', files: filesToUpload, destPath: currentPath, conflicts: res.conflicts }); + return; + } + } catch {} + + processUploadBatch(filesToUpload, currentPath); } } else { - const droppedFiles = Array.from(e.dataTransfer.files); - await Promise.all(droppedFiles.map(file => uploadFile(file))); + const droppedFiles = Array.from(e.dataTransfer.files).map(f => ({ file: f, path: '' })); + const fileNames = droppedFiles.map(f => f.file.name); + try { + const res = await api.checkConflicts(currentPath, fileNames); + if (res.conflicts && res.conflicts.length > 0) { + setConflictState({ type: 'upload', files: droppedFiles, destPath: currentPath, conflicts: res.conflicts }); + return; + } + } catch {} + processUploadBatch(droppedFiles, currentPath); } - - loadFiles(undefined, true); } async function handleFileUpload(e: React.ChangeEvent) { const uploadFiles = e.target.files; if (!uploadFiles) return; - await Promise.all(Array.from(uploadFiles).map(file => uploadFile(file))); + const filesArray = Array.from(uploadFiles).map(f => ({ file: f, path: '' })); + + const fileNames = filesArray.map(f => f.file.name); + try { + const res = await api.checkConflicts(currentPath, fileNames); + if (res.conflicts && res.conflicts.length > 0) { + setConflictState({ type: 'upload', files: filesArray, destPath: currentPath, conflicts: res.conflicts }); + e.target.value = ''; + return; + } + } catch {} + + processUploadBatch(filesArray, currentPath); e.target.value = ''; - loadFiles(undefined, true); } const pathSegments = currentPath === '.' ? [] : currentPath.split('/'); @@ -1998,6 +2151,44 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { + {conflictState && ( +
setConflictState(null)}> + e.stopPropagation()} + > +
+

Name Conflict Detected

+

+ The following files already exist in the destination. How would you like to proceed? +

+
+
+
    + {conflictState.conflicts.map(c =>
  • {c}
  • )} +
+
+
+ + + + +
+
+
+ )} + {showMoveModal && (
- Remember me for 3 weeks + Remember me )} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index ef0f449..a6f320d 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -146,6 +146,14 @@ class ApiClient { } // --- Files --- + async checkConflicts(destPath: string, files: string[]) { + const res = await this.request('/api/files/check-conflicts', { + method: 'POST', + body: { dest_path: destPath, files }, + }); + return res.json(); + } + async listDirectory(path: string = '.') { const res = await this.request(`/api/files/?path=${encodeURIComponent(path)}`); return res.json(); @@ -177,18 +185,18 @@ class ApiClient { return res.json(); } - async move(sourcePath: string, destPath: string) { + async move(sourcePath: string, destPath: string, resolution: string = "rename") { const res = await this.request('/api/files/move', { method: 'POST', - body: { source_path: sourcePath, dest_path: destPath }, + body: { source_path: sourcePath, dest_path: destPath, resolution }, }); return res.json(); } - async copy(sourcePath: string, destPath: string) { + async copy(sourcePath: string, destPath: string, resolution: string = "rename") { const res = await this.request('/api/files/copy', { method: 'POST', - body: { source_path: sourcePath, dest_path: destPath }, + body: { source_path: sourcePath, dest_path: destPath, resolution }, }); return res.json(); } @@ -200,7 +208,7 @@ class ApiClient { return res.json(); } - upload(file: File, destPath: string, callbacks?: { onProgress?: (percent: number) => void }): Promise { + upload(file: File, destPath: string, options?: { onProgress?: (percent: number) => void, overwrite?: boolean }): Promise { return new Promise((resolve, reject) => { const upload = new tus.Upload(file, { endpoint: `${API_BASE}/api/tus/`, @@ -211,13 +219,14 @@ class ApiClient { filename: file.name, filetype: file.type, path: destPath, + overwrite: options?.overwrite ? "true" : "false", }, onError: function (error) { reject(error); }, onProgress: function (bytesUploaded, bytesTotal) { const percentage = (bytesUploaded / bytesTotal) * 100; - callbacks?.onProgress?.(percentage); + options?.onProgress?.(percentage); }, onSuccess: function () { resolve(); diff --git a/refactor.js b/refactor.js new file mode 100644 index 0000000..36fc3ef --- /dev/null +++ b/refactor.js @@ -0,0 +1,302 @@ +const fs = require('fs'); + +let content = fs.readFileSync('frontend/src/components/FileExplorer.tsx', 'utf-8'); + +// 1. Add ConflictState to the component state +const stateInsertPos = content.indexOf(' const [deleteConfirm'); +const conflictStateCode = ` const [conflictState, setConflictState] = useState<{ + type: 'upload' | 'move' | 'copy'; + files?: {file: File, path: string}[]; + sourcePaths?: string[]; + destPath: string; + conflicts: string[]; + } | null>(null);\n`; +content = content.slice(0, stateInsertPos) + conflictStateCode + content.slice(stateInsertPos); + +// 2. Replace handleDrop and uploadFile with processUploadBatch and updated handleDrop +const uploadFileStart = content.indexOf(' async function uploadFile'); +const handleCreateFolderStart = content.indexOf(' async function handleCreateFolder'); + +const newUploadCode = ` async function processUploadBatch(entries: {file: File, path: string}[], destPath: string, overwrite: boolean = false) { + if (!entries.length) return; + const batchId = 'batch-' + Date.now(); + const totalFiles = entries.length; + const totalBytes = entries.reduce((acc, curr) => acc + curr.file.size, 0); + const batchName = totalFiles === 1 ? entries[0].file.name : \`Uploading \${totalFiles} items\`; + + setUploads(prev => [...prev, { + id: batchId, + name: batchName, + totalBytes, + uploadedBytes: 0, + totalFiles, + filesUploaded: 0, + status: 'uploading' + }]); + + const MAX_CONCURRENT = 4; + let currentIndex = 0; + let completedFiles = 0; + let currentUploadedBytes = 0; + const fileProgressMap = new Map(); + + const updateProgress = () => { + let totalUploaded = 0; + fileProgressMap.forEach(bytes => { totalUploaded += bytes; }); + setUploads(prev => prev.map(b => b.id === batchId ? { + ...b, + uploadedBytes: totalUploaded, + filesUploaded: completedFiles, + status: completedFiles === totalFiles ? 'complete' : 'uploading' + } : b)); + }; + + const worker = async () => { + while (currentIndex < entries.length) { + const index = currentIndex++; + const { file, path } = entries[index]; + const uploadPath = path ? (destPath === '.' ? path : \`\${destPath}/\${path}\`) : destPath; + + try { + await api.upload(file, uploadPath, { + overwrite, + onProgress: (percent) => { + fileProgressMap.set(file.name + path, (percent / 100) * file.size); + updateProgress(); + } + }); + completedFiles++; + fileProgressMap.set(file.name + path, file.size); + updateProgress(); + } catch (err: any) { + setUploads(prev => prev.map(b => b.id === batchId ? { ...b, status: 'error', error: err.message || 'Upload failed' } : b)); + } + } + }; + + const workers = []; + for (let i = 0; i < Math.min(MAX_CONCURRENT, entries.length); i++) { + workers.push(worker()); + } + await Promise.all(workers); + + setTimeout(() => { + setUploads(prev => prev.filter(u => u.id !== batchId)); + loadFiles(undefined, true); + }, 3000); + } + + async function handleDrop(e: React.DragEvent) { + e.preventDefault(); + setDragOver(false); + if (draggedFile) return; + + if (e.dataTransfer.items) { + const filesToUpload: { file: File; path: string }[] = []; + const promises: Promise[] = []; + + const processEntry = async (entry: any, path: string = '') => { + if (entry.isFile) { + const file = await new Promise((resolve) => entry.file(resolve)); + filesToUpload.push({ file, path: path.replace(/\\/$/, '') }); + } else if (entry.isDirectory) { + const dirReader = entry.createReader(); + let entries: any[] = []; + + const readAllEntries = async () => { + const batch = await new Promise((resolve) => dirReader.readEntries(resolve)); + if (batch.length > 0) { + entries = entries.concat(batch); + await readAllEntries(); + } + }; + await readAllEntries(); + + for (const child of entries) { + await processEntry(child, \`\${path}\${entry.name}/\`); + } + } + }; + + for (let i = 0; i < e.dataTransfer.items.length; i++) { + const item = e.dataTransfer.items[i]; + if (item.kind === 'file') { + const entry = item.webkitGetAsEntry(); + if (entry) { + promises.push(processEntry(entry)); + } + } + } + + await Promise.all(promises); + + if (filesToUpload.length > 0) { + // Collect destination paths to create folders + const uniqueDirs = new Set(); + filesToUpload.forEach(({path}) => { + if (path) uniqueDirs.add(currentPath === '.' ? path : \`\${currentPath}/\${path}\`); + }); + + for (const dir of Array.from(uniqueDirs)) { + try { await api.createFolder(dir); } catch {} + } + + const fileNames = filesToUpload.map(f => f.path ? \`\${f.path}/\${f.file.name}\` : f.file.name); + try { + const res = await api.checkConflicts(currentPath, fileNames); + if (res.conflicts && res.conflicts.length > 0) { + setConflictState({ type: 'upload', files: filesToUpload, destPath: currentPath, conflicts: res.conflicts }); + return; + } + } catch {} + + processUploadBatch(filesToUpload, currentPath); + } + } + } + + async function handleFileUpload(e: React.ChangeEvent) { + const uploadFiles = e.target.files; + if (!uploadFiles) return; + const filesArray = Array.from(uploadFiles).map(f => ({ file: f, path: '' })); + + const fileNames = filesArray.map(f => f.file.name); + try { + const res = await api.checkConflicts(currentPath, fileNames); + if (res.conflicts && res.conflicts.length > 0) { + setConflictState({ type: 'upload', files: filesArray, destPath: currentPath, conflicts: res.conflicts }); + return; + } + } catch {} + + processUploadBatch(filesArray, currentPath); + } + +`; + +content = content.slice(0, uploadFileStart) + newUploadCode + content.slice(handleCreateFolderStart); + +// Add Conflict Modal rendering +const renderModalsPos = content.lastIndexOf('{showMoveModal && ('); +const conflictModalCode = ` + {conflictState && ( +
setConflictState(null)}> + e.stopPropagation()} + > +
+

Name Conflict Detected

+

+ The following files already exist in the destination. How would you like to proceed? +

+
+
+
    + {conflictState.conflicts.map(c =>
  • {c}
  • )} +
+
+
+ + + + +
+
+
+ )} +`; +content = content.slice(0, renderModalsPos) + conflictModalCode + content.slice(renderModalsPos); + +// Add executeConflictResolution function +const execFunctionCode = ` + async function executeConflictResolution(resolution: string) { + if (!conflictState) return; + const { type, destPath, conflicts } = conflictState; + + if (type === 'upload' && conflictState.files) { + let filesToUpload = conflictState.files; + if (resolution === 'skip') { + filesToUpload = filesToUpload.filter(f => { + const name = f.path ? \`\${f.path}/\${f.file.name}\` : f.file.name; + return !conflicts.includes(name); + }); + } + setConflictState(null); + if (filesToUpload.length > 0) { + processUploadBatch(filesToUpload, destPath, resolution === 'overwrite'); + } + } else if (type === 'move' && conflictState.sourcePaths) { + let pathsToMove = conflictState.sourcePaths; + if (resolution === 'skip') { + pathsToMove = pathsToMove.filter(p => !conflicts.includes(p.split('/').pop()!)); + } + setConflictState(null); + if (pathsToMove.length > 0) { + try { + await Promise.all(pathsToMove.map(src => api.move(src, destPath, resolution))); + setShowMoveModal(false); + setMovingFiles([]); + loadFiles(undefined, true); + setSelectedFiles(new Set()); + showToast(\`Moved \${pathsToMove.length} item\${pathsToMove.length > 1 ? 's' : ''}\`); + } catch (e) { + console.error(e); + } + } + } + } + +`; + +const handleDropOnFolderStart = content.indexOf(' async function handleDropOnFolder'); +content = content.slice(0, handleDropOnFolderStart) + execFunctionCode + content.slice(handleDropOnFolderStart); + +// Update handleMoveExecute +content = content.replace( + \` async function handleMoveExecute() { + if (movingFiles.length === 0) return; + try { + await Promise.all(movingFiles.map(src => api.move(src, moveModalPath === '.' ? '.' : moveModalPath)));\`, + \` async function handleMoveExecute() { + if (movingFiles.length === 0) return; + try { + const destPath = moveModalPath === '.' ? '.' : moveModalPath; + const fileNames = movingFiles.map(src => src.split('/').pop() || src); + const res = await api.checkConflicts(destPath, fileNames); + if (res.conflicts && res.conflicts.length > 0) { + setConflictState({ type: 'move', sourcePaths: movingFiles, destPath, conflicts: res.conflicts }); + return; + } + await Promise.all(movingFiles.map(src => api.move(src, destPath)));\` +); + +// Update handleDropOnFolder +content = content.replace( + \` async function handleDropOnFolder(file: FileItem, targetFolder: FileItem) { + if (file.path === targetFolder.path) return; + try { + await api.move(file.path, targetFolder.path);\`, + \` async function handleDropOnFolder(file: FileItem, targetFolder: FileItem) { + if (file.path === targetFolder.path) return; + try { + const res = await api.checkConflicts(targetFolder.path, [file.name]); + if (res.conflicts && res.conflicts.length > 0) { + setConflictState({ type: 'move', sourcePaths: [file.path], destPath: targetFolder.path, conflicts: res.conflicts }); + return; + } + await api.move(file.path, targetFolder.path);\` +); + +fs.writeFileSync('frontend/src/components/FileExplorer.tsx', content);