feat: v0.3.0 版本 - SSH终端、SFTP、端口转发功能实现

核心功能:
- SSH终端: 密码/密钥认证、XTerm.js终端、多主题配色、IP高亮
- SFTP管理: 双栏文件管理、上传下载、传输队列
- 端口转发: 本地/远程转发、SOCKS5动态代理、规则管理

架构改进:
- 拆分大型组件 (SFTPView/TabBar/Sidebar)
- 添加统一日志和错误处理
- 修复React严格模式下的终端黑屏问题

UI更新:
- 新增端口转发视图
- 更新UI预览文档
- 左侧栏右键菜单支持SFTP快捷打开
This commit is contained in:
Logan.Li committed 2026-04-06 01:20:47 +08:00
1 parent cf1c054381
commit 484d850abd
105 files changed
+25330 -64

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
/**
* File IPC Handlers
* 处理本地文件操作和远程文件读写
*/
import { ipcMain, IpcMainInvokeEvent, app } from 'electron'
import fs from 'fs'
import path from 'path'
// IPC channel names
export const FILE_CHANNELS = {
// Remote file operations (for zmodem)
READ: 'file:read',
WRITE: 'file:write',
// Local file system operations
LOCAL_GET_HOME: 'local:getHome',
LOCAL_READ_DIR: 'local:readDir',
LOCAL_GET_STATS: 'local:getStats',
LOCAL_DELETE: 'local:delete',
LOCAL_RENAME: 'local:rename',
LOCAL_MKDIR: 'local:mkdir',
} as const
export function registerFileIPCHandlers(): void {
// Read file (for rz)
ipcMain.handle(FILE_CHANNELS.READ, async (_event: IpcMainInvokeEvent, filePath: string) => {
try {
const data = fs.readFileSync(filePath)
return { success: true, data: data.toString('base64') }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Write file (for sz)
ipcMain.handle(FILE_CHANNELS.WRITE, async (_event: IpcMainInvokeEvent, filePath: string, data: string) => {
try {
const buffer = Buffer.from(data, 'base64')
fs.writeFileSync(filePath, buffer)
return { success: true }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Get user home directory
ipcMain.handle(FILE_CHANNELS.LOCAL_GET_HOME, async () => {
try {
const homePath = app.getPath('home')
return { success: true, path: homePath }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Read directory contents
ipcMain.handle(FILE_CHANNELS.LOCAL_READ_DIR, async (_event: IpcMainInvokeEvent, dirPath: string) => {
try {
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
const files = entries.map((entry) => {
const fullPath = path.join(dirPath, entry.name)
let stats: fs.Stats | null = null
try {
stats = fs.lstatSync(fullPath)
} catch {
// Skip files we can't stat
}
return {
name: entry.name,
path: fullPath,
type: entry.isDirectory() ? 'directory' : entry.isSymbolicLink() ? 'symlink' : 'file',
size: stats?.size || 0,
modified: stats?.mtime ? Math.floor(stats.mtime.getTime() / 1000) : 0,
permissions: formatPermissions(stats?.mode || 0),
}
})
// Sort: directories first, then by name
files.sort((a, b) => {
if (a.type !== 'file' && b.type === 'file') return -1
if (a.type === 'file' && b.type !== 'file') return 1
return a.name.localeCompare(b.name)
})
return { success: true, files }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Get file stats
ipcMain.handle(FILE_CHANNELS.LOCAL_GET_STATS, async (_event: IpcMainInvokeEvent, filePath: string) => {
try {
const stats = fs.lstatSync(filePath)
return {
success: true,
stats: {
name: path.basename(filePath),
path: filePath,
type: stats.isDirectory() ? 'directory' : stats.isSymbolicLink() ? 'symlink' : 'file',
size: stats.size,
modified: Math.floor(stats.mtime.getTime() / 1000),
permissions: formatPermissions(stats.mode),
},
}
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Delete file or directory
ipcMain.handle(FILE_CHANNELS.LOCAL_DELETE, async (_event: IpcMainInvokeEvent, filePath: string) => {
try {
const stats = fs.lstatSync(filePath)
if (stats.isDirectory()) {
fs.rmSync(filePath, { recursive: true, force: true })
} else {
fs.unlinkSync(filePath)
}
return { success: true }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Rename file or directory
ipcMain.handle(FILE_CHANNELS.LOCAL_RENAME, async (_event: IpcMainInvokeEvent, oldPath: string, newPath: string) => {
try {
fs.renameSync(oldPath, newPath)
return { success: true }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Create directory
ipcMain.handle(FILE_CHANNELS.LOCAL_MKDIR, async (_event: IpcMainInvokeEvent, dirPath: string) => {
try {
fs.mkdirSync(dirPath, { recursive: true })
return { success: true }
} catch (error: any) {
return { success: false, error: error.message }
}
})
}
// Helper function to format Unix permissions
function formatPermissions(mode: number): string {
const perms: string[] = []
const types = ['---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx']
// Owner
perms.push(types[(mode >> 6) & 7])
// Group
perms.push(types[(mode >> 3) & 7])
// Other
perms.push(types[mode & 7])
return perms.join('')
}