修复: 文件系统初始化守卫与传输收尾等七处
- filesystemSvc 守卫17绑定方法,异步初始化完成前返回明确错误 - MovePath 跨盘剪切建 move emitter,失败清理目标盘残留 - SFTP 空目录下载/上传补 MkdirAll 落地 - drawio 下载失败等待者立即返回,不再空轮询30分钟 - batch 高亮补 %1 与 %%i 规则 - 错误文案转换抽 errMessage 共享函数,11处归一 - log.Printf 统一 slog.Warn
This commit is contained in:
+106
-20
@@ -11,6 +11,15 @@ import (
|
|||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// filesystemSvc 返回文件系统服务。初始化在启动 goroutine 异步进行,
|
||||||
|
// 完成前前端触发文件操作时返回明确错误而非 nil panic
|
||||||
|
func (a *App) filesystemSvc() (*filesystem.FileSystemService, error) {
|
||||||
|
if a.filesystem == nil {
|
||||||
|
return nil, fmt.Errorf("文件系统服务尚未初始化完成,请稍后重试")
|
||||||
|
}
|
||||||
|
return a.filesystem, nil
|
||||||
|
}
|
||||||
|
|
||||||
// EnsureDrawioViewer 确保本地已缓存 drawio viewer(首次预览时下载),前端打开 .drawio 前调用。
|
// EnsureDrawioViewer 确保本地已缓存 drawio viewer(首次预览时下载),前端打开 .drawio 前调用。
|
||||||
// 通过 drawio-download-progress 事件推送下载进度。
|
// 通过 drawio-download-progress 事件推送下载进度。
|
||||||
func (a *App) EnsureDrawioViewer() (map[string]interface{}, error) {
|
func (a *App) EnsureDrawioViewer() (map[string]interface{}, error) {
|
||||||
@@ -30,7 +39,11 @@ func (a *App) EnsureDrawioViewer() (map[string]interface{}, error) {
|
|||||||
|
|
||||||
// ReadFile 读取文件
|
// ReadFile 读取文件
|
||||||
func (a *App) ReadFile(path string) (string, error) {
|
func (a *App) ReadFile(path string) (string, error) {
|
||||||
return a.filesystem.ReadFile(path)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fs.ReadFile(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WatchFile 开始监听指定文件的变化,变化时发送 file-changed 事件
|
// WatchFile 开始监听指定文件的变化,变化时发送 file-changed 事件
|
||||||
@@ -50,52 +63,93 @@ func (a *App) UnwatchFile() {
|
|||||||
|
|
||||||
// WriteFile 写入文件
|
// WriteFile 写入文件
|
||||||
func (a *App) WriteFile(req WriteFileRequest) error {
|
func (a *App) WriteFile(req WriteFileRequest) error {
|
||||||
return a.filesystem.WriteFile(req.Path, req.Content)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fs.WriteFile(req.Path, req.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveBase64File 将 base64 内容解码后写入文件(用于图片等二进制数据)
|
// SaveBase64File 将 base64 内容解码后写入文件(用于图片等二进制数据)
|
||||||
func (a *App) SaveBase64File(req SaveBase64FileRequest) error {
|
func (a *App) SaveBase64File(req SaveBase64FileRequest) error {
|
||||||
return a.filesystem.SaveBase64File(req.Path, req.Content)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fs.SaveBase64File(req.Path, req.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListDir 列出目录
|
// ListDir 列出目录
|
||||||
func (a *App) ListDir(path string) ([]map[string]interface{}, error) {
|
func (a *App) ListDir(path string) ([]map[string]interface{}, error) {
|
||||||
return a.filesystem.ListDir(path)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fs.ListDir(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateDir 创建目录
|
// CreateDir 创建目录
|
||||||
func (a *App) CreateDir(path string) (*filesystem.FileOperationResult, error) {
|
func (a *App) CreateDir(path string) (*filesystem.FileOperationResult, error) {
|
||||||
return a.filesystem.CreateDir(path)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fs.CreateDir(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateFile 创建文件
|
// CreateFile 创建文件
|
||||||
func (a *App) CreateFile(path string) (*filesystem.FileOperationResult, error) {
|
func (a *App) CreateFile(path string) (*filesystem.FileOperationResult, error) {
|
||||||
return a.filesystem.CreateFile(path)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fs.CreateFile(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeletePath 删除文件或目录
|
// DeletePath 删除文件或目录
|
||||||
func (a *App) DeletePath(path string) (*filesystem.FileOperationResult, error) {
|
func (a *App) DeletePath(path string) (*filesystem.FileOperationResult, error) {
|
||||||
return a.filesystem.DeletePath(path)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fs.DeletePath(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenamePath 重命名文件或目录
|
// RenamePath 重命名文件或目录
|
||||||
func (a *App) RenamePath(req RenamePathRequest) (*filesystem.FileOperationResult, error) {
|
func (a *App) RenamePath(req RenamePathRequest) (*filesystem.FileOperationResult, error) {
|
||||||
return a.filesystem.RenamePath(req.OldPath, req.NewPath)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fs.RenamePath(req.OldPath, req.NewPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyPath 复制文件或目录(递归),通过 transfer-progress 事件推送进度
|
// CopyPath 复制文件或目录(递归),通过 transfer-progress 事件推送进度
|
||||||
func (a *App) CopyPath(src, dst string) error {
|
func (a *App) CopyPath(src, dst string) error {
|
||||||
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
emitter := newTransferEmitter(a, "copy", filepath.Base(filepath.FromSlash(src)))
|
emitter := newTransferEmitter(a, "copy", filepath.Base(filepath.FromSlash(src)))
|
||||||
err := a.filesystem.CopyPath(src, dst, func(copied, total int64) {
|
err = fs.CopyPath(src, dst, func(copied, total int64) {
|
||||||
emitter.progress(emitter.name, copied, total)
|
emitter.progress(emitter.name, copied, total)
|
||||||
})
|
})
|
||||||
emitter.finish(err)
|
emitter.finish(err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// MovePath 移动文件或目录(同盘重命名,跨盘复制后删除)
|
// MovePath 移动文件或目录(同盘重命名,跨盘复制后删除),跨盘时经 transfer-progress 事件推送进度
|
||||||
func (a *App) MovePath(src, dst string) error {
|
func (a *App) MovePath(src, dst string) error {
|
||||||
return a.filesystem.MovePath(src, dst)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
emitter := newTransferEmitter(a, "move", filepath.Base(filepath.FromSlash(src)))
|
||||||
|
err = fs.MovePath(src, dst, func(copied, total int64) {
|
||||||
|
emitter.progress(emitter.name, copied, total)
|
||||||
|
})
|
||||||
|
emitter.finish(err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// PickDirectory 打开系统目录选择器,返回选中目录(取消时返回空串)
|
// PickDirectory 打开系统目录选择器,返回选中目录(取消时返回空串)
|
||||||
@@ -177,39 +231,67 @@ func (t *transferEmitter) finish(err error) {
|
|||||||
|
|
||||||
// GetFileInfo 获取文件信息
|
// GetFileInfo 获取文件信息
|
||||||
func (a *App) GetFileInfo(path string) (map[string]interface{}, error) {
|
func (a *App) GetFileInfo(path string) (map[string]interface{}, error) {
|
||||||
return a.filesystem.GetFileInfo(path)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fs.GetFileInfo(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenPath 使用系统默认程序打开文件或目录
|
// OpenPath 使用系统默认程序打开文件或目录
|
||||||
func (a *App) OpenPath(path string) error {
|
func (a *App) OpenPath(path string) error {
|
||||||
return a.filesystem.OpenPath(path)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fs.OpenPath(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Zip 文件操作接口 ==========
|
// ========== Zip 文件操作接口 ==========
|
||||||
|
|
||||||
// ListZipContents 列出 zip 文件内容
|
// ListZipContents 列出 zip 文件内容
|
||||||
func (a *App) ListZipContents(zipPath string) ([]map[string]interface{}, error) {
|
func (a *App) ListZipContents(zipPath string) ([]map[string]interface{}, error) {
|
||||||
return a.filesystem.ListZipContents(zipPath)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fs.ListZipContents(zipPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractFileFromZip 从 zip 文件中提取单个文件内容
|
// ExtractFileFromZip 从 zip 文件中提取单个文件内容
|
||||||
func (a *App) ExtractFileFromZip(zipPath, filePath string) (string, error) {
|
func (a *App) ExtractFileFromZip(zipPath, filePath string) (string, error) {
|
||||||
return a.filesystem.ExtractFileFromZip(zipPath, filePath)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fs.ExtractFileFromZip(zipPath, filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractFileFromZipToTemp 从 zip 文件中提取单个文件到临时目录
|
// ExtractFileFromZipToTemp 从 zip 文件中提取单个文件到临时目录
|
||||||
func (a *App) ExtractFileFromZipToTemp(zipPath, filePath string) (string, error) {
|
func (a *App) ExtractFileFromZipToTemp(zipPath, filePath string) (string, error) {
|
||||||
return a.filesystem.ExtractFileFromZipToTemp(zipPath, filePath)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fs.ExtractFileFromZipToTemp(zipPath, filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetZipFileInfo 获取 zip 文件中特定文件的信息
|
// GetZipFileInfo 获取 zip 文件中特定文件的信息
|
||||||
func (a *App) GetZipFileInfo(zipPath, filePath string) (map[string]interface{}, error) {
|
func (a *App) GetZipFileInfo(zipPath, filePath string) (map[string]interface{}, error) {
|
||||||
return a.filesystem.GetZipFileInfo(zipPath, filePath)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fs.GetZipFileInfo(zipPath, filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveShortcut 解析快捷方式文件,返回目标路径信息
|
// ResolveShortcut 解析快捷方式文件,返回目标路径信息
|
||||||
func (a *App) ResolveShortcut(lnkPath string) (map[string]interface{}, error) {
|
func (a *App) ResolveShortcut(lnkPath string) (map[string]interface{}, error) {
|
||||||
targetPath, err := a.filesystem.ResolveShortcut(lnkPath)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
targetPath, err := fs.ResolveShortcut(lnkPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -217,7 +299,7 @@ func (a *App) ResolveShortcut(lnkPath string) (map[string]interface{}, error) {
|
|||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fileInfo, err := a.filesystem.GetFileInfo(targetPath)
|
fileInfo, err := fs.GetFileInfo(targetPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"success": true,
|
"success": true,
|
||||||
@@ -284,5 +366,9 @@ func (a *App) initFilesystemModule() {
|
|||||||
|
|
||||||
// GetAuditLogs 获取审计日志
|
// GetAuditLogs 获取审计日志
|
||||||
func (a *App) GetAuditLogs(limit int) ([]map[string]interface{}, error) {
|
func (a *App) GetAuditLogs(limit int) ([]map[string]interface{}, error) {
|
||||||
return a.filesystem.GetAuditLogs(limit)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fs.GetAuditLogs(limit)
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-4
@@ -4,20 +4,36 @@ package main
|
|||||||
|
|
||||||
// GetRecycleBinEntries 获取回收站条目
|
// GetRecycleBinEntries 获取回收站条目
|
||||||
func (a *App) GetRecycleBinEntries() ([]map[string]interface{}, error) {
|
func (a *App) GetRecycleBinEntries() ([]map[string]interface{}, error) {
|
||||||
return a.filesystem.GetRecycleBinEntries()
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fs.GetRecycleBinEntries()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestoreFromRecycleBin 从回收站恢复文件
|
// RestoreFromRecycleBin 从回收站恢复文件
|
||||||
func (a *App) RestoreFromRecycleBin(recyclePath string) error {
|
func (a *App) RestoreFromRecycleBin(recyclePath string) error {
|
||||||
return a.filesystem.RestoreFromRecycleBin(recyclePath)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fs.RestoreFromRecycleBin(recyclePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeletePermanently 永久删除回收站中的文件
|
// DeletePermanently 永久删除回收站中的文件
|
||||||
func (a *App) DeletePermanently(recyclePath string) error {
|
func (a *App) DeletePermanently(recyclePath string) error {
|
||||||
return a.filesystem.DeletePermanently(recyclePath)
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fs.DeletePermanently(recyclePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EmptyRecycleBin 清空回收站
|
// EmptyRecycleBin 清空回收站
|
||||||
func (a *App) EmptyRecycleBin() error {
|
func (a *App) EmptyRecycleBin() error {
|
||||||
return a.filesystem.EmptyRecycleBin()
|
fs, err := a.filesystemSvc()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fs.EmptyRecycleBin()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { HttpTransport } from './http-transport'
|
|||||||
import { SftpTransport } from './sftp-transport'
|
import { SftpTransport } from './sftp-transport'
|
||||||
import { OssTransport } from './oss-transport'
|
import { OssTransport } from './oss-transport'
|
||||||
import { getFileServerBaseURL } from './file-server'
|
import { getFileServerBaseURL } from './file-server'
|
||||||
|
import { errMessage } from '@/utils/errors'
|
||||||
import {
|
import {
|
||||||
LoadConnectionProfiles, SaveConnectionProfile, DeleteConnectionProfile,
|
LoadConnectionProfiles, SaveConnectionProfile, DeleteConnectionProfile,
|
||||||
SftpGetSystemInfo, GetLocalSystemInfo,
|
SftpGetSystemInfo, GetLocalSystemInfo,
|
||||||
@@ -268,7 +269,7 @@ class ConnectionManagerImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) { info._error = true; info._errorMsg = errMsg(e) }
|
} catch (e) { info._error = true; info._errorMsg = errMessage(e) }
|
||||||
|
|
||||||
this._sysInfoCache.set(profileId, info)
|
this._sysInfoCache.set(profileId, info)
|
||||||
this._sysInfoChangeCallbacks.forEach(cb => cb(profileId, info))
|
this._sysInfoChangeCallbacks.forEach(cb => cb(profileId, info))
|
||||||
@@ -320,7 +321,7 @@ class ConnectionManagerImpl {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 回退目标可能已不在池中(死连接重建时 disconnectProfile 先移除),指向池外会让 getTransport 静默回落本地造成 UI 幻影
|
// 回退目标可能已不在池中(死连接重建时 disconnectProfile 先移除),指向池外会让 getTransport 静默回落本地造成 UI 幻影
|
||||||
this._activeId = (prevActiveId && this._pool.has(prevActiveId)) ? prevActiveId : 'local-default'
|
this._activeId = (prevActiveId && this._pool.has(prevActiveId)) ? prevActiveId : 'local-default'
|
||||||
this.setProfileState(profileId, 'error', err instanceof Error ? err.message : String(err))
|
this.setProfileState(profileId, 'error', errMessage(err))
|
||||||
this.notifyChange()
|
this.notifyChange()
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
@@ -433,7 +434,7 @@ class ConnectionManagerImpl {
|
|||||||
this.fetchSystemInfo(profileId).catch(() => {})
|
this.fetchSystemInfo(profileId).catch(() => {})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._pool.delete(profileId)
|
this._pool.delete(profileId)
|
||||||
this.setProfileState(profileId, 'error', errMsg(err))
|
this.setProfileState(profileId, 'error', errMessage(err))
|
||||||
this.setState('error')
|
this.setState('error')
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
@@ -453,7 +454,7 @@ class ConnectionManagerImpl {
|
|||||||
this.updateProfile(profileId, { lastConnected: Date.now() })
|
this.updateProfile(profileId, { lastConnected: Date.now() })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._pool.delete(profileId)
|
this._pool.delete(profileId)
|
||||||
this.setProfileState(profileId, 'error', errMsg(err))
|
this.setProfileState(profileId, 'error', errMessage(err))
|
||||||
this.setState('error')
|
this.setState('error')
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
@@ -474,13 +475,13 @@ class ConnectionManagerImpl {
|
|||||||
this.updateProfile(profileId, { lastConnected: Date.now() })
|
this.updateProfile(profileId, { lastConnected: Date.now() })
|
||||||
this.fetchSystemInfo(profileId).catch(() => {})
|
this.fetchSystemInfo(profileId).catch(() => {})
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
this.setProfileState(profileId, 'error', errMsg(e))
|
this.setProfileState(profileId, 'error', errMessage(e))
|
||||||
if (this._activeId !== profileId) return
|
if (this._activeId !== profileId) return
|
||||||
this.setState('error')
|
this.setState('error')
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._pool.delete(profileId)
|
this._pool.delete(profileId)
|
||||||
this.setProfileState(profileId, 'error', errMsg(err))
|
this.setProfileState(profileId, 'error', errMessage(err))
|
||||||
this.setState('error')
|
this.setState('error')
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
@@ -489,11 +490,6 @@ class ConnectionManagerImpl {
|
|||||||
|
|
||||||
export const connectionManager = new ConnectionManagerImpl()
|
export const connectionManager = new ConnectionManagerImpl()
|
||||||
|
|
||||||
/** 任意抛出值转错误文案 */
|
|
||||||
function errMsg(e: unknown): string {
|
|
||||||
return e instanceof Error ? e.message : String(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** snake_case → camelCase */
|
/** snake_case → camelCase */
|
||||||
function snakeToCamel(obj: Record<string, any>): Record<string, any> {
|
function snakeToCamel(obj: Record<string, any>): Record<string, any> {
|
||||||
const result: Record<string, any> = {}
|
const result: Record<string, any> = {}
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ import { connectionManager } from '@/api/connection-manager'
|
|||||||
import { STORAGE_KEYS, DEFAULTS, UI_TEXT, VALIDATION_RULES, FILE_EXTENSIONS, FILE_SIZE_THRESHOLDS } from '@/utils/constants'
|
import { STORAGE_KEYS, DEFAULTS, UI_TEXT, VALIDATION_RULES, FILE_EXTENSIONS, FILE_SIZE_THRESHOLDS } from '@/utils/constants'
|
||||||
import { isAutoRestoreDisabled, clearAutoRestoreDisabled } from '@/utils/freezeWatchdog'
|
import { isAutoRestoreDisabled, clearAutoRestoreDisabled } from '@/utils/freezeWatchdog'
|
||||||
import { parseWailsEventData } from '@/utils/wailsEvent'
|
import { parseWailsEventData } from '@/utils/wailsEvent'
|
||||||
|
import { errMessage } from '@/utils/errors'
|
||||||
import { createResizeHandler } from '@/utils/resize'
|
import { createResizeHandler } from '@/utils/resize'
|
||||||
|
|
||||||
// 导入类型
|
// 导入类型
|
||||||
@@ -524,7 +525,7 @@ const handleConnectionChanged = async (profileId: string) => {
|
|||||||
try {
|
try {
|
||||||
await switchConnection(profileId)
|
await switchConnection(profileId)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Message.error(`连接失败: ${err instanceof Error ? err.message : String(err)}`)
|
Message.error(`连接失败: ${errMessage(err)}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,7 +648,7 @@ const handleOpenFavorite = async (file: FavoriteFile) => {
|
|||||||
await loadCommonPaths()
|
await loadCommonPaths()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('切换连接失败:', e)
|
console.error('切换连接失败:', e)
|
||||||
Message.error(`无法连接: ${e instanceof Error ? e.message : String(e)}`)
|
Message.error(`无法连接: ${errMessage(e)}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if (!file.profileId) {
|
} else if (!file.profileId) {
|
||||||
@@ -1011,7 +1012,7 @@ const handlePasteClipboard = async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
failed++
|
failed++
|
||||||
console.error('[Clipboard] 粘贴失败:', item.path, error)
|
console.error('[Clipboard] 粘贴失败:', item.path, error)
|
||||||
Message.error(`粘贴失败: ${item.name} - ${error instanceof Error ? error.message : error}`)
|
Message.error(`粘贴失败: ${item.name} - ${errMessage(error)}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1033,7 +1034,7 @@ const handleDownloadToLocal = async (file: FileItem) => {
|
|||||||
const localPath = await connectionManager.getTransport().downloadToFile(file.path, dstDir)
|
const localPath = await connectionManager.getTransport().downloadToFile(file.path, dstDir)
|
||||||
Message.success(`已下载到: ${localPath}`)
|
Message.success(`已下载到: ${localPath}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Message.error(`下载失败: ${error instanceof Error ? error.message : error}`)
|
Message.error(`下载失败: ${errMessage(error)}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1067,7 +1068,7 @@ const handleExternalDrop = async (paths: string[]) => {
|
|||||||
Message.success(`已复制 ${paths.length} 个项目`)
|
Message.success(`已复制 ${paths.length} 个项目`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Message.error(`拖入失败: ${error instanceof Error ? error.message : error}`)
|
Message.error(`拖入失败: ${errMessage(error)}`)
|
||||||
await loadDirectory(targetDir)
|
await loadDirectory(targetDir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,8 +108,8 @@ export async function loadLanguageExtension(language) {
|
|||||||
if (stream.eatSpace()) return null
|
if (stream.eatSpace()) return null
|
||||||
if (stream.match(/^"(?:[^"\r\n]|"")*"?/)) return 'string'
|
if (stream.match(/^"(?:[^"\r\n]|"")*"?/)) return 'string'
|
||||||
if (stream.eat('^')) return 'operator'
|
if (stream.eat('^')) return 'operator'
|
||||||
// !var! 延迟变量、%var% 环境变量、%~d0 类批处理参数
|
// !var! 延迟变量、%var% 环境变量、%~d0 类批处理参数、%1 位置参数、%%i for 循环变量
|
||||||
if (stream.match(/^!\w+!/) || stream.match(/^%\w+%/) || stream.match(/^%~[a-z]*[\d*]/i)) return 'atom'
|
if (stream.match(/^!\w+!/) || stream.match(/^%\w+%/) || stream.match(/^%~[a-z]*[\d*]/i) || stream.match(/^%\d/) || stream.match(/^%%\w+/)) return 'atom'
|
||||||
if (stream.match(/^\w+/)) {
|
if (stream.match(/^\w+/)) {
|
||||||
return batchKeywords.has(stream.current().toLowerCase()) ? 'keyword' : null
|
return batchKeywords.has(stream.current().toLowerCase()) ? 'keyword' : null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* 任意抛出值转错误文案(Error 取 message,其余 String 化)
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function errMessage(e: unknown): string {
|
||||||
|
return e instanceof Error ? e.message : String(e)
|
||||||
|
}
|
||||||
@@ -67,6 +67,13 @@ func EnsureDrawioViewer(progress service.DownloadProgress) error {
|
|||||||
if isDrawioViewerReady() {
|
if isDrawioViewerReady() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
// 首个下载方已退出仍未就绪(下载失败),等待者立即失败而非空轮询到超时
|
||||||
|
drawioEnsureMu.Lock()
|
||||||
|
ensuring := drawioEnsuring
|
||||||
|
drawioEnsureMu.Unlock()
|
||||||
|
if !ensuring {
|
||||||
|
return fmt.Errorf("drawio viewer 下载失败,请重试")
|
||||||
|
}
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("等待 drawio viewer 下载超时")
|
return fmt.Errorf("等待 drawio viewer 下载超时")
|
||||||
|
|||||||
@@ -552,7 +552,8 @@ func (s *FileSystemService) CopyPath(src, dst string, onProgress func(copied, to
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MovePath 移动文件或目录:同盘直接重命名,跨盘回退为复制后永久删除源(不走回收站)
|
// MovePath 移动文件或目录:同盘直接重命名,跨盘回退为复制后永久删除源(不走回收站)
|
||||||
func (s *FileSystemService) MovePath(src, dst string) error {
|
// 跨盘复制阶段经 onProgress 回调进度(可为 nil),同盘 rename 无进度
|
||||||
|
func (s *FileSystemService) MovePath(src, dst string, onProgress func(copied, total int64)) error {
|
||||||
info, err := s.checkCopyMovePaths(src, dst)
|
info, err := s.checkCopyMovePaths(src, dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -564,8 +565,9 @@ func (s *FileSystemService) MovePath(src, dst string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跨盘:复制 + 删除源
|
// 跨盘:复制 + 删除源;复制中途失败清理 dst 残留,否则重试会因目标已存在被拒
|
||||||
if err := s.CopyPath(src, dst, nil); err != nil {
|
if err := s.CopyPath(src, dst, onProgress); err != nil {
|
||||||
|
_ = os.RemoveAll(dst)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := os.RemoveAll(src); err != nil {
|
if err := os.RemoveAll(src); err != nil {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -851,7 +851,7 @@ func supplementDir(c oss.OSSProvider, ctx context.Context, remoteDir string, tmp
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if count >= common.MaxSupplementFiles {
|
if count >= common.MaxSupplementFiles {
|
||||||
log.Printf("[站点下载] 补充扫描达到上限: count=%d", common.MaxSupplementFiles)
|
slog.Warn("站点下载补充扫描达到上限", "count", common.MaxSupplementFiles)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
localPath := filepath.Join(tmpDir, filepath.FromSlash(f.Key))
|
localPath := filepath.Join(tmpDir, filepath.FromSlash(f.Key))
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@@ -539,7 +538,7 @@ func sftpSupplementDir(s *Service, c *Client, remoteDir string, tmpDir string, s
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if count >= common.MaxSupplementFiles {
|
if count >= common.MaxSupplementFiles {
|
||||||
log.Printf("[站点下载] 补充扫描达到上限: count=%d", common.MaxSupplementFiles)
|
slog.Warn("站点下载补充扫描达到上限", "count", common.MaxSupplementFiles)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
fullPath := path.Join(remoteDir, entry.Name())
|
fullPath := path.Join(remoteDir, entry.Name())
|
||||||
@@ -617,6 +616,10 @@ func (s *Service) DownloadToFile(connID, remotePath, localDstDir string, onProgr
|
|||||||
if err := walk(remotePath, dstDir); err != nil {
|
if err := walk(remotePath, dstDir); err != nil {
|
||||||
return "", fmt.Errorf("遍历远程目录失败: %w", err)
|
return "", fmt.Errorf("遍历远程目录失败: %w", err)
|
||||||
}
|
}
|
||||||
|
// 空目录时清单为空,downloadFileTo 不会执行,目标目录须显式创建
|
||||||
|
if err := os.MkdirAll(dstDir, 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("创建本地目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
var copied int64
|
var copied int64
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
@@ -714,6 +717,12 @@ func (s *Service) UploadFromFile(connID, localPath, remoteDir string, onProgress
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// 空目录时文件清单为空,uploadFileTo 不会执行,远端根目录须显式创建
|
||||||
|
if err := c.WithRetry(func(sc *sftpclient.Client) error {
|
||||||
|
return sc.MkdirAll(remoteRoot)
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("创建远程目录失败: %w", err)
|
||||||
|
}
|
||||||
var copied int64
|
var copied int64
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
if err := s.uploadFileTo(c, f.local, path.Join(remoteRoot, f.rel), &copied, total, onProgress); err != nil {
|
if err := s.uploadFileTo(c, f.local, path.Join(remoteRoot, f.rel), &copied, total, onProgress); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user