粘贴图根修(Ctrl+V直读系统剪贴板,不再依赖不可靠的paste事件)/SFTP串台双刀(拨号期禁回退本机+盘符路径自动回根)/PDF导出目录设置与文案带落盘路径/性能批(连接保存下载防重/md渲染防抖+预热/hljs按需加载/审计锁外置/快捷方式解析超时/webview-watch可停)/前端62类型错清零(含keyPath蛇形字段炸弹)/目录图标字段错位修复
238 lines
7.5 KiB
TypeScript
238 lines
7.5 KiB
TypeScript
/**
|
|
* SFTP Transport — 通过 Wails IPC 调用 Go 后端 SFTP 客户端
|
|
*/
|
|
|
|
import type {
|
|
FsTransport, FileItem, FileOperationResult, DetectTypeResult,
|
|
} from './transport'
|
|
import { transformFileList, joinPath, probeAlive } from './transport'
|
|
import type { ConnectionProfile } from './connection-manager'
|
|
import {
|
|
SftpConnect, SftpDisconnect, SftpListDir, SftpReadFile,
|
|
SftpWriteFile, SftpGetFileInfo, SftpCreateDir, SftpCreateFile,
|
|
SftpDeletePath, SftpRenamePath, SftpDownloadToTemp, SftpDownloadSiteForPreview,
|
|
SftpDownloadToFile, SftpUploadFromFile,
|
|
SftpGetCommonPaths, SftpWriteBase64File,
|
|
} from '@bindings/u-desk/app'
|
|
|
|
export class SftpTransport implements FsTransport {
|
|
private profile: ConnectionProfile
|
|
private connID: string | null = null
|
|
|
|
constructor(profile: ConnectionProfile) {
|
|
this.profile = profile
|
|
}
|
|
|
|
async connect(): Promise<string> {
|
|
// 前端反馈层 9s 超时:后端连接继续由既有逻辑管理(不主动断),
|
|
// 仅避免 UI 在网络不可达时无限挂起,用户能立刻拿到失败反馈
|
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
const timeout = new Promise<never>((_, reject) => {
|
|
timer = setTimeout(() => reject(new Error('连接超时,请检查网络')), 9000)
|
|
})
|
|
try {
|
|
const result = await Promise.race([
|
|
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: '',
|
|
}),
|
|
timeout,
|
|
])
|
|
this.connID = result
|
|
return result
|
|
} finally {
|
|
if (timer) clearTimeout(timer)
|
|
}
|
|
}
|
|
|
|
async isAlive(): Promise<boolean> {
|
|
return probeAlive(this.connID, id => SftpGetFileInfo(id, '/'))
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
private requireConn(): string {
|
|
if (!this.connID) throw new Error('SFTP 未连接')
|
|
return this.connID
|
|
}
|
|
|
|
/** 远程路径守卫:本机盘符路径串入 SFTP(tab 恢复/面板状态错位)会在服务器端报 No such file */
|
|
private assertRemotePath(path: string): void {
|
|
if (/^[A-Za-z]:[\\/]/.test(path)) {
|
|
throw new Error(`本机路径不可用于远程操作: ${path}`)
|
|
}
|
|
}
|
|
|
|
// ====== 文件列表与信息 ======
|
|
|
|
async listDir(path: string): Promise<FileItem[]> {
|
|
this.assertRemotePath(path)
|
|
return transformFileList(await SftpListDir(this.requireConn(), path))
|
|
}
|
|
|
|
async getFileInfo(path: string): Promise<Record<string, any>> {
|
|
this.assertRemotePath(path)
|
|
return SftpGetFileInfo(this.requireConn(), path)
|
|
}
|
|
|
|
// ====== 文件读写 ======
|
|
|
|
async readFile(path: string): Promise<string> {
|
|
this.assertRemotePath(path)
|
|
return SftpReadFile(this.requireConn(), path)
|
|
}
|
|
|
|
async writeFile(path: string, content: string): Promise<void> {
|
|
this.assertRemotePath(path)
|
|
await SftpWriteFile({
|
|
session_id: this.requireConn(),
|
|
path,
|
|
content,
|
|
})
|
|
}
|
|
|
|
async saveBase64File(path: string, content: string): Promise<void> {
|
|
if (!content) throw new Error('无效的 base64 内容')
|
|
this.assertRemotePath(path)
|
|
await SftpWriteBase64File(this.requireConn(), path, content)
|
|
}
|
|
|
|
// ====== 文件操作 ======
|
|
|
|
async createFile(dirPath: string, filename: string): Promise<FileOperationResult> {
|
|
this.assertRemotePath(dirPath)
|
|
return SftpCreateFile(this.requireConn(), joinPath(dirPath, filename))
|
|
}
|
|
|
|
async createDir(parentPath: string, dirname: string): Promise<FileOperationResult> {
|
|
this.assertRemotePath(parentPath)
|
|
return SftpCreateDir(this.requireConn(), joinPath(parentPath, dirname))
|
|
}
|
|
|
|
async deletePath(path: string): Promise<FileOperationResult> {
|
|
this.assertRemotePath(path)
|
|
return SftpDeletePath(this.requireConn(), path)
|
|
}
|
|
|
|
async renamePath(oldPath: string, newPath: string): Promise<FileOperationResult> {
|
|
this.assertRemotePath(oldPath)
|
|
this.assertRemotePath(newPath)
|
|
return SftpRenamePath({
|
|
session_id: this.requireConn(),
|
|
old_path: oldPath,
|
|
new_path: newPath,
|
|
})
|
|
}
|
|
|
|
// ====== 复制/移动(粘贴仅本地面板支持)======
|
|
|
|
async copyPath(_src: string, _dst: string): Promise<void> {
|
|
throw new Error('SFTP 面板暂不支持粘贴,可使用拖拽上传')
|
|
}
|
|
|
|
async movePath(_src: string, _dst: string): Promise<void> {
|
|
throw new Error('SFTP 面板暂不支持粘贴,可使用拖拽上传')
|
|
}
|
|
|
|
// ====== 传输 ======
|
|
|
|
/** 下载远程文件/目录到用户指定目录(流式无上限,重名自动副本),返回本地落地路径 */
|
|
async downloadToFile(remotePath: string, localDstDir: string): Promise<string> {
|
|
this.assertRemotePath(remotePath)
|
|
return SftpDownloadToFile(this.requireConn(), remotePath, localDstDir)
|
|
}
|
|
|
|
/** 上传本地文件/目录到远程目录(流式,目录递归) */
|
|
async uploadFromFile(localPath: string, remoteDir: string): Promise<void> {
|
|
this.assertRemotePath(remoteDir)
|
|
await SftpUploadFromFile(this.requireConn(), localPath, remoteDir)
|
|
}
|
|
|
|
// ====== 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> {}
|
|
|
|
// ====== 预览辅助 ======
|
|
|
|
/** 下载远程文件到本地临时目录用于预览(缓存由 Go 后端管理) */
|
|
async downloadForPreview(remotePath: string): Promise<string> {
|
|
this.assertRemotePath(remotePath)
|
|
return SftpDownloadToTemp(this.requireConn(), remotePath)
|
|
}
|
|
|
|
/** 下载 HTML 及其网站资源到临时目录用于预览 */
|
|
async downloadSiteForPreview(remotePath: string): Promise<string> {
|
|
this.assertRemotePath(remotePath)
|
|
return SftpDownloadSiteForPreview(this.requireConn(), remotePath)
|
|
}
|
|
|
|
async watchFile(_path: string): Promise<void> { /* SFTP 模式不支持文件监听 */ }
|
|
async unwatchFile(): Promise<void> { /* SFTP 模式不支持文件监听 */ }
|
|
}
|