核心功能: - SSH终端: 密码/密钥认证、XTerm.js终端、多主题配色、IP高亮 - SFTP管理: 双栏文件管理、上传下载、传输队列 - 端口转发: 本地/远程转发、SOCKS5动态代理、规则管理 架构改进: - 拆分大型组件 (SFTPView/TabBar/Sidebar) - 添加统一日志和错误处理 - 修复React严格模式下的终端黑屏问题 UI更新: - 新增端口转发视图 - 更新UI预览文档 - 左侧栏右键菜单支持SFTP快捷打开
162 lines
4.8 KiB
TypeScript
162 lines
4.8 KiB
TypeScript
/**
|
|
* 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('')
|
|
}
|