diff --git a/src/ui/features/file-manager/FileManager.tsx b/src/ui/features/file-manager/FileManager.tsx index acc8543c..de591b27 100644 --- a/src/ui/features/file-manager/FileManager.tsx +++ b/src/ui/features/file-manager/FileManager.tsx @@ -257,6 +257,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { const { dragHandlers } = useDragAndDrop({ onFilesDropped: handleFilesDropped, + onItemsDropped: handleItemsDropped, onError: (error) => toast.error(error), maxFileSize: 5120, }); @@ -788,6 +789,127 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { return () => document.removeEventListener("keydown", handleKeyDown); }, [currentPath]); + async function handleItemsDropped(items: DataTransferItemList) { + if (!sshSessionId) { + toast.error(t("fileManager.noSSHConnection")); + return; + } + + const entries: FileSystemEntry[] = []; + for (let i = 0; i < items.length; i++) { + const entry = items[i].webkitGetAsEntry?.(); + if (entry) entries.push(entry); + } + + const files: { file: File; relativePath: string }[] = []; + + async function readEntry( + entry: FileSystemEntry, + path: string, + ): Promise { + if (entry.isFile) { + const file = await new Promise((resolve, reject) => + (entry as FileSystemFileEntry).file(resolve, reject), + ); + files.push({ file, relativePath: path }); + } else if (entry.isDirectory) { + const reader = ( + entry as FileSystemDirectoryEntry + ).createReader(); + const dirEntries = await new Promise( + (resolve, reject) => reader.readEntries(resolve, reject), + ); + for (const child of dirEntries) { + await readEntry(child, `${path}/${child.name}`); + } + } + } + + for (const entry of entries) { + await readEntry(entry, entry.name); + } + + if (files.length === 0) return; + + const progressToast = toast.loading( + `Uploading ${files.length} file(s)...`, + { duration: Infinity }, + ); + + try { + await ensureSSHConnection(); + + const dirs = new Set(); + for (const { relativePath } of files) { + const parts = relativePath.split("/"); + for (let i = 1; i < parts.length; i++) { + dirs.add(parts.slice(0, i).join("/")); + } + } + + const sortedDirs = Array.from(dirs).sort(); + for (const dir of sortedDirs) { + const parentPath = currentPath.endsWith("/") + ? currentPath + dir.split("/").slice(0, -1).join("/") + : currentPath + "/" + dir.split("/").slice(0, -1).join("/"); + const folderName = dir.split("/").pop()!; + const targetPath = parentPath.endsWith("/") + ? parentPath + : parentPath + "/"; + try { + await createSSHFolder( + sshSessionId, + targetPath, + folderName, + currentHost?.id, + ); + } catch { + // directory may already exist + } + } + + for (const { file, relativePath } of files) { + const dirPart = relativePath.includes("/") + ? relativePath.substring(0, relativePath.lastIndexOf("/")) + : ""; + const uploadPath = dirPart + ? (currentPath.endsWith("/") ? currentPath : currentPath + "/") + + dirPart + + "/" + : currentPath; + + const fileContent = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = () => reject(reader.error); + reader.onload = () => { + if (typeof reader.result === "string") { + resolve(reader.result.split(",")[1] || ""); + } else { + reject(new Error("Failed to read file")); + } + }; + reader.readAsDataURL(file); + }); + + await uploadSSHFile( + sshSessionId, + uploadPath, + file.name, + fileContent, + currentHost?.id, + ); + } + + toast.dismiss(progressToast); + toast.success(`Uploaded ${files.length} file(s) successfully`); + handleRefreshDirectory(); + } catch (error) { + toast.dismiss(progressToast); + toast.error(t("fileManager.failedToUploadFile")); + console.error("Folder upload failed:", error); + } + } + function handleFilesDropped(fileList: FileList) { if (!sshSessionId) { toast.error(t("fileManager.noSSHConnection")); diff --git a/src/ui/features/file-manager/hooks/useDragAndDrop.ts b/src/ui/features/file-manager/hooks/useDragAndDrop.ts index 1335feee..79fc8551 100644 --- a/src/ui/features/file-manager/hooks/useDragAndDrop.ts +++ b/src/ui/features/file-manager/hooks/useDragAndDrop.ts @@ -8,6 +8,7 @@ interface DragAndDropState { interface UseDragAndDropProps { onFilesDropped: (files: FileList) => void; + onItemsDropped?: (items: DataTransferItemList) => void; onError?: (error: string) => void; maxFileSize?: number; allowedTypes?: string[]; @@ -15,6 +16,7 @@ interface UseDragAndDropProps { export function useDragAndDrop({ onFilesDropped, + onItemsDropped, onError, maxFileSize = 5120, allowedTypes = [], @@ -123,6 +125,16 @@ export function useDragAndDrop({ draggedFiles: [], }); + if (onItemsDropped && e.dataTransfer.items?.length > 0) { + const hasDirectory = Array.from(e.dataTransfer.items).some( + (item) => item.webkitGetAsEntry?.()?.isDirectory, + ); + if (hasDirectory) { + onItemsDropped(e.dataTransfer.items); + return; + } + } + const files = e.dataTransfer.files; if (files.length === 0) { @@ -137,7 +149,7 @@ export function useDragAndDrop({ onFilesDropped(files); }, - [validateFiles, onFilesDropped, onError], + [validateFiles, onFilesDropped, onItemsDropped, onError], ); const resetDragState = useCallback(() => {