diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index bc1aebe2..25ccc0bd 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -443,33 +443,87 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => { sshConn.lastActive = Date.now(); + // First check file size to prevent loading huge files + const MAX_READ_SIZE = 10 * 1024 * 1024; // 10MB - same as frontend limit const escapedPath = filePath.replace(/'/g, "'\"'\"'"); - sshConn.client.exec(`cat '${escapedPath}'`, (err, stream) => { - if (err) { - fileLogger.error("SSH readFile error:", err); - return res.status(500).json({ error: err.message }); + + // Get file size first + sshConn.client.exec(`stat -c%s '${escapedPath}' 2>/dev/null || wc -c < '${escapedPath}'`, (sizeErr, sizeStream) => { + if (sizeErr) { + fileLogger.error("SSH file size check error:", sizeErr); + return res.status(500).json({ error: sizeErr.message }); } - let data = ""; - let errorData = ""; + let sizeData = ""; + let sizeErrorData = ""; - stream.on("data", (chunk: Buffer) => { - data += chunk.toString(); + sizeStream.on("data", (chunk: Buffer) => { + sizeData += chunk.toString(); }); - stream.stderr.on("data", (chunk: Buffer) => { - errorData += chunk.toString(); + sizeStream.stderr.on("data", (chunk: Buffer) => { + sizeErrorData += chunk.toString(); }); - stream.on("close", (code) => { - if (code !== 0) { - fileLogger.error( - `SSH readFile command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, - ); - return res.status(500).json({ error: `Command failed: ${errorData}` }); + sizeStream.on("close", (sizeCode) => { + if (sizeCode !== 0) { + fileLogger.error(`File size check failed: ${sizeErrorData}`); + return res.status(500).json({ error: `Cannot check file size: ${sizeErrorData}` }); } - res.json({ content: data, path: filePath }); + const fileSize = parseInt(sizeData.trim(), 10); + + if (isNaN(fileSize)) { + fileLogger.error("Invalid file size response:", sizeData); + return res.status(500).json({ error: "Cannot determine file size" }); + } + + // Check if file is too large + if (fileSize > MAX_READ_SIZE) { + fileLogger.warn("File too large for reading", { + operation: "file_read", + sessionId, + filePath, + fileSize, + maxSize: MAX_READ_SIZE, + }); + return res.status(400).json({ + error: `File too large to open in editor. Maximum size is ${MAX_READ_SIZE / 1024 / 1024}MB, file is ${(fileSize / 1024 / 1024).toFixed(2)}MB. Use download instead.`, + fileSize, + maxSize: MAX_READ_SIZE, + tooLarge: true + }); + } + + // File size is acceptable, proceed with reading + sshConn.client.exec(`cat '${escapedPath}'`, (err, stream) => { + if (err) { + fileLogger.error("SSH readFile error:", err); + return res.status(500).json({ error: err.message }); + } + + let data = ""; + let errorData = ""; + + stream.on("data", (chunk: Buffer) => { + data += chunk.toString(); + }); + + stream.stderr.on("data", (chunk: Buffer) => { + errorData += chunk.toString(); + }); + + stream.on("close", (code) => { + if (code !== 0) { + fileLogger.error( + `SSH readFile command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, + ); + return res.status(500).json({ error: `Command failed: ${errorData}` }); + } + + res.json({ content: data, path: filePath }); + }); + }); }); }); }); diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerGrid.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerGrid.tsx index c988febd..5c65d07e 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManagerGrid.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManagerGrid.tsx @@ -69,11 +69,11 @@ const getFileIcon = (file: FileItem, viewMode: 'grid' | 'list' = 'grid') => { const iconClass = viewMode === 'grid' ? "w-8 h-8" : "w-6 h-6"; if (file.type === 'directory') { - return ; + return ; } if (file.type === 'link') { - return ; + return ; } const ext = file.name.split('.').pop()?.toLowerCase(); @@ -82,30 +82,30 @@ const getFileIcon = (file: FileItem, viewMode: 'grid' | 'list' = 'grid') => { case 'txt': case 'md': case 'readme': - return ; + return ; case 'png': case 'jpg': case 'jpeg': case 'gif': case 'bmp': case 'svg': - return ; + return ; case 'mp4': case 'avi': case 'mkv': case 'mov': - return ; + return ; case 'mp3': case 'wav': case 'flac': case 'ogg': - return ; + return ; case 'zip': case 'tar': case 'gz': case 'rar': case '7z': - return ; + return ; case 'js': case 'ts': case 'jsx': @@ -119,7 +119,7 @@ const getFileIcon = (file: FileItem, viewMode: 'grid' | 'list' = 'grid') => { case 'rb': case 'go': case 'rs': - return ; + return ; case 'json': case 'xml': case 'yaml': @@ -128,9 +128,9 @@ const getFileIcon = (file: FileItem, viewMode: 'grid' | 'list' = 'grid') => { case 'ini': case 'conf': case 'config': - return ; + return ; default: - return ; + return ; } }; @@ -613,7 +613,7 @@ export function FileManagerGrid({
@@ -621,7 +621,7 @@ export function FileManagerGrid({ @@ -656,8 +656,8 @@ export function FileManagerGrid({ {isDragging && (
- -

+ +

{t("fileManager.dragFilesToUpload")}

@@ -693,8 +693,8 @@ export function FileManagerGrid({ data-file-path={file.path} className={cn( "group p-3 rounded-lg cursor-pointer transition-all", - "hover:bg-dark-hover border-2 border-transparent", - isSelected && "bg-blue-500/20 border-blue-500" + "hover:bg-accent hover:text-accent-foreground border-2 border-transparent", + isSelected && "bg-primary/20 border-primary" )} title={`${file.name} - Selected: ${isSelected} - SelectedCount: ${selectedFiles.length}`} onClick={(e) => handleFileClick(file, e)} @@ -721,7 +721,7 @@ export function FileManagerGrid({ onKeyDown={handleEditKeyDown} onBlur={handleEditConfirm} className={cn( - "max-w-[120px] min-w-[60px] w-fit rounded-md border border-input bg-background px-2 py-1 text-xs shadow-xs transition-[color,box-shadow] outline-none", + "max-w-[120px] min-w-[60px] w-fit rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2 py-1 text-xs shadow-xs transition-[color,box-shadow] outline-none", "text-center text-foreground placeholder:text-muted-foreground", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px]" )} @@ -729,7 +729,7 @@ export function FileManagerGrid({ /> ) : (

{ // 阻止文件选择事件 @@ -748,7 +748,7 @@ export function FileManagerGrid({

)} {file.type === 'link' && file.linkTarget && ( -

+

→ {file.linkTarget}

)} @@ -770,8 +770,8 @@ export function FileManagerGrid({ data-file-path={file.path} className={cn( "flex items-center gap-3 p-2 rounded cursor-pointer transition-all", - "hover:bg-dark-hover", - isSelected && "bg-blue-500/20" + "hover:bg-accent hover:text-accent-foreground", + isSelected && "bg-primary/20" )} onClick={(e) => handleFileClick(file, e)} onContextMenu={(e) => { @@ -796,7 +796,7 @@ export function FileManagerGrid({ onKeyDown={handleEditKeyDown} onBlur={handleEditConfirm} className={cn( - "flex-1 min-w-0 max-w-[200px] rounded-md border border-input bg-background px-2 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none", + "flex-1 min-w-0 max-w-[200px] rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none", "text-foreground placeholder:text-muted-foreground", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px]" )} @@ -804,7 +804,7 @@ export function FileManagerGrid({ /> ) : (

{ // 阻止文件选择事件 @@ -818,7 +818,7 @@ export function FileManagerGrid({

)} {file.type === 'link' && file.linkTarget && ( -

+

→ {file.linkTarget}

)} @@ -855,7 +855,7 @@ export function FileManagerGrid({ {/* 框选矩形 */} {isSelecting && selectionRect && (
-
+
- +

Large File Warning

This file is {formatFileSize(file.size)} in size, which may cause performance issues when opened as text.

{isTooLarge ? ( -
-

+

+

File is too large (> 10MB) and cannot be opened as text for security reasons.

diff --git a/src/ui/Desktop/Apps/File Manager/components/FileWindow.tsx b/src/ui/Desktop/Apps/File Manager/components/FileWindow.tsx index 27a88390..bf8991e5 100644 --- a/src/ui/Desktop/Apps/File Manager/components/FileWindow.tsx +++ b/src/ui/Desktop/Apps/File Manager/components/FileWindow.tsx @@ -126,11 +126,17 @@ export function FileWindow({ } catch (error: any) { console.error('Failed to load file:', error); - // 如果是连接错误,提供更明确的错误信息 - if (error.message?.includes('connection') || error.message?.includes('established')) { + // 检查是否是大文件错误 + const errorData = error?.response?.data; + if (errorData?.tooLarge) { + toast.error(`File too large: ${errorData.error}`, { + duration: 10000, // 10 seconds for important message + }); + } else if (error.message?.includes('connection') || error.message?.includes('established')) { + // 如果是连接错误,提供更明确的错误信息 toast.error(`SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`); } else { - toast.error(`Failed to load file: ${error.message || 'Unknown error'}`); + toast.error(`Failed to load file: ${error.message || errorData?.error || 'Unknown error'}`); } } finally { setIsLoading(false);