新增:SFTP直连+连接池+autoConnect+文件服务器端口自动回退
- SFTP模块:连接/断开/文件CRUD/系统信息采集/base64二进制写入 - 连接池:多服务器同时在线,瞬间切换profile - autoConnect:启动时自动连接所有非本地服务器 - 端口自动回退:listenWithFallback消除TOCTOU,解决端口冲突崩溃 - 文件服务器URL集中管理:file-server.ts消除8+处硬编码端口 - Sidebar设置面板:添加服务器/自动连接/自动刷新开关 - 修复:validateFilePath越界panic、正则预编译 - 修复:注释准确性(RemoveAll/端口8073/动态端口文档)
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* SFTP Transport — 通过 Wails IPC 调用 Go 后端 SFTP 客户端
|
||||
*/
|
||||
|
||||
import type {
|
||||
FsTransport, FileItem, FileOperationResult, DetectTypeResult,
|
||||
} from './transport'
|
||||
import type { ConnectionProfile } from './connection-manager'
|
||||
import {
|
||||
SftpConnect, SftpDisconnect, SftpListDir, SftpReadFile,
|
||||
SftpWriteFile, SftpGetFileInfo, SftpCreateDir, SftpCreateFile,
|
||||
SftpDeletePath, SftpRenamePath, SftpDownloadToTemp, SftpGetCommonPaths,
|
||||
SftpWriteBase64File,
|
||||
} from '@bindings/u-desk/app'
|
||||
|
||||
function transformFile(file: any): FileItem {
|
||||
return { ...file, is_dir: file.is_dir, mod_time: file.mod_time }
|
||||
}
|
||||
|
||||
function transformFileList(files: any[]): FileItem[] {
|
||||
return files.map(transformFile)
|
||||
}
|
||||
|
||||
const PREVIEW_CACHE_MAX = 50
|
||||
|
||||
export class SftpTransport implements FsTransport {
|
||||
private profile: ConnectionProfile
|
||||
private connID: string | null = null
|
||||
private previewCache = new Map<string, string>() // remotePath -> localTempPath (LRU)
|
||||
private previewOrder: string[] = [] // LRU 排序键列表
|
||||
|
||||
constructor(profile: ConnectionProfile) {
|
||||
this.profile = profile
|
||||
}
|
||||
|
||||
async connect(): Promise<string> {
|
||||
const result = await SftpConnect({
|
||||
host: this.profile.host,
|
||||
port: this.profile.port || 22,
|
||||
username: this.profile.username || 'root',
|
||||
password: this.profile.password || '',
|
||||
key_path: this.profile.keyPath || '',
|
||||
key_passphrase: '',
|
||||
})
|
||||
this.connID = result
|
||||
return result
|
||||
}
|
||||
|
||||
get sessionId(): string | null { return this.connID }
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.connID) {
|
||||
try { await SftpDisconnect(this.connID) } catch (e) {
|
||||
console.warn('[SFTP] disconnect error:', e)
|
||||
}
|
||||
this.connID = null
|
||||
}
|
||||
this.previewCache.clear()
|
||||
this.previewOrder = []
|
||||
}
|
||||
|
||||
private requireConn(): string {
|
||||
if (!this.connID) throw new Error('SFTP 未连接')
|
||||
return this.connID
|
||||
}
|
||||
|
||||
// ====== 文件列表与信息 ======
|
||||
|
||||
async listDir(path: string): Promise<FileItem[]> {
|
||||
return transformFileList(await SftpListDir(this.requireConn(), path))
|
||||
}
|
||||
|
||||
async getFileInfo(path: string): Promise<Record<string, any>> {
|
||||
return SftpGetFileInfo(this.requireConn(), path)
|
||||
}
|
||||
|
||||
// ====== 文件读写 ======
|
||||
|
||||
async readFile(path: string): Promise<string> {
|
||||
return SftpReadFile(this.requireConn(), path)
|
||||
}
|
||||
|
||||
async writeFile(path: string, content: string): Promise<void> {
|
||||
await SftpWriteFile({
|
||||
session_id: this.requireConn(),
|
||||
path,
|
||||
content,
|
||||
})
|
||||
}
|
||||
|
||||
async saveBase64File(path: string, content: string): Promise<void> {
|
||||
if (!content) throw new Error('无效的 base64 内容')
|
||||
await SftpWriteBase64File(this.requireConn(), path, content)
|
||||
}
|
||||
|
||||
// ====== 文件操作 ======
|
||||
|
||||
async createFile(dirPath: string, filename: string): Promise<FileOperationResult> {
|
||||
const fullPath = dirPath.replace(/\/$/, '') + '/' + filename
|
||||
return SftpCreateFile(this.requireConn(), fullPath)
|
||||
}
|
||||
|
||||
async createDir(parentPath: string, dirname: string): Promise<FileOperationResult> {
|
||||
const fullPath = parentPath.replace(/\/$/, '') + '/' + dirname
|
||||
return SftpCreateDir(this.requireConn(), fullPath)
|
||||
}
|
||||
|
||||
async deletePath(path: string): Promise<FileOperationResult> {
|
||||
return SftpDeletePath(this.requireConn(), path)
|
||||
}
|
||||
|
||||
async renamePath(oldPath: string, newPath: string): Promise<FileOperationResult> {
|
||||
return SftpRenamePath({
|
||||
session_id: this.requireConn(),
|
||||
old_path: oldPath,
|
||||
new_path: newPath,
|
||||
})
|
||||
}
|
||||
|
||||
// ====== ZIP 操作(不支持)======
|
||||
|
||||
async listZipContents(_zipPath: string): Promise<FileItem[]> {
|
||||
throw new Error('ZIP 操作在 SFTP 模式暂未实现')
|
||||
}
|
||||
|
||||
async extractFileFromZip(_zipPath: string, _filePath: string): Promise<string> {
|
||||
throw new Error('ZIP 操作在 SFTP 模式暂未实现')
|
||||
}
|
||||
|
||||
async extractFileFromZipToTemp(_zipPath: string, _filePath: string): Promise<string> {
|
||||
throw new Error('ZIP 操作在 SFTP 模式暂未实现')
|
||||
}
|
||||
|
||||
async getZipFileInfo(_zipPath: string, _filePath: string): Promise<FileItem> {
|
||||
throw new Error('ZIP 操作在 SFTP 模式暂未实现')
|
||||
}
|
||||
|
||||
// ====== 系统操作 ======
|
||||
|
||||
async openPath(_path: string): Promise<void> {
|
||||
throw new Error('SFTP 模式不支持打开本地路径')
|
||||
}
|
||||
|
||||
async getFileServerURL(): Promise<string> {
|
||||
const { getFileServerBaseURL } = await import('./file-server')
|
||||
return getFileServerBaseURL()
|
||||
}
|
||||
|
||||
getPreviewToken(): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
async resolveShortcut(_lnkPath: string): Promise<any> {
|
||||
return null
|
||||
}
|
||||
|
||||
async detectFileTypeByContent(_path: string): Promise<DetectTypeResult> {
|
||||
return { extension: '', category: 'unknown', mime_type: '', confidence: 0 }
|
||||
}
|
||||
|
||||
async getCommonPaths(): Promise<Record<string, string>> {
|
||||
return SftpGetCommonPaths(this.requireConn())
|
||||
}
|
||||
|
||||
// ====== 回收站(无)======
|
||||
|
||||
async getRecycleBinEntries(): Promise<any[]> {
|
||||
return []
|
||||
}
|
||||
|
||||
async restoreFromRecycleBin(_path: string): Promise<void> {}
|
||||
|
||||
async deletePermanently(_path: string): Promise<void> {}
|
||||
|
||||
async emptyRecycleBin(): Promise<void> {}
|
||||
|
||||
// ====== 预览辅助 ======
|
||||
|
||||
/** 下载远程文件到本地临时目录用于预览(带 LRU 缓存,上限 50) */
|
||||
async downloadForPreview(remotePath: string): Promise<string> {
|
||||
// 命中:移到队尾(最近使用)
|
||||
if (this.previewCache.has(remotePath)) {
|
||||
this.previewOrder = this.previewOrder.filter(p => p !== remotePath)
|
||||
this.previewOrder.push(remotePath)
|
||||
return this.previewCache.get(remotePath)!
|
||||
}
|
||||
const localPath = await SftpDownloadToTemp(this.requireConn(), remotePath)
|
||||
|
||||
// 淘汰最旧条目
|
||||
if (this.previewOrder.length >= PREVIEW_CACHE_MAX) {
|
||||
const oldest = this.previewOrder.shift()!
|
||||
this.previewCache.delete(oldest)
|
||||
}
|
||||
|
||||
this.previewCache.set(remotePath, localPath)
|
||||
this.previewOrder.push(remotePath)
|
||||
return localPath
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user