新增:SFTP直连+连接池+autoConnect+文件服务器端口自动回退
- SFTP模块:连接/断开/文件CRUD/系统信息采集/base64二进制写入 - 连接池:多服务器同时在线,瞬间切换profile - autoConnect:启动时自动连接所有非本地服务器 - 端口自动回退:listenWithFallback消除TOCTOU,解决端口冲突崩溃 - 文件服务器URL集中管理:file-server.ts消除8+处硬编码端口 - Sidebar设置面板:添加服务器/自动连接/自动刷新开关 - 修复:validateFilePath越界panic、正则预编译 - 修复:注释准确性(RemoveAll/端口8073/动态端口文档)
This commit is contained in:
244
app.go
244
app.go
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
stdruntime "runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -18,7 +19,9 @@ import (
|
||||
"u-desk/internal/common"
|
||||
"u-desk/internal/filesystem"
|
||||
"u-desk/internal/service"
|
||||
"u-desk/internal/sftp"
|
||||
"u-desk/internal/storage"
|
||||
"u-desk/internal/storage/models"
|
||||
"u-desk/internal/system"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
@@ -34,6 +37,7 @@ type App struct {
|
||||
configAPI *api.ConfigAPI
|
||||
pdfAPI *api.PdfAPI
|
||||
filesystem *filesystem.FileSystemService
|
||||
sftpService *sftp.Service
|
||||
isAlwaysOnTop bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
@@ -94,7 +98,10 @@ func (a *App) ServiceStartup(ctx context.Context, _ application.ServiceOptions)
|
||||
return fmt.Errorf("模块初始化失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 异步初始化:UpdateAPI(涉及网络请求,完全异步)
|
||||
// 5. 清理遗留的 SFTP 临时预览文件
|
||||
sftp.CleanupTempFiles()
|
||||
|
||||
// 6. 异步初始化:UpdateAPI(涉及网络请求,完全异步)
|
||||
go func() {
|
||||
if updateAPI, err := api.NewUpdateAPI("https://c.1216.top/last-version.json"); err == nil {
|
||||
a.mu.Lock()
|
||||
@@ -175,7 +182,7 @@ func (a *App) startFileServer() {
|
||||
fmt.Printf("[文件服务器] 启动失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("[文件服务器] 启动在 http://localhost:8073")
|
||||
fmt.Printf("[文件服务器] 启动在 http://%s\n", filesystem.GetLocalFileServerAddr())
|
||||
}
|
||||
|
||||
// ServiceShutdown Wails v3 服务关闭生命周期(替代 v2 的 Shutdown)
|
||||
@@ -202,6 +209,13 @@ func (a *App) ServiceShutdown() error {
|
||||
} else {
|
||||
fmt.Println("[文件服务器] 已关闭")
|
||||
}
|
||||
|
||||
// 关闭所有 SFTP 连接 + 清理临时文件
|
||||
if a.sftpService != nil {
|
||||
sftp.GetManager().Shutdown()
|
||||
}
|
||||
sftp.CleanupTempFiles()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -657,7 +671,7 @@ func (a *App) GetAuditLogs(limit int) ([]map[string]interface{}, error) {
|
||||
|
||||
// GetFileServerURL 获取本地文件服务器的URL
|
||||
func (a *App) GetFileServerURL() string {
|
||||
return "http://localhost:8073"
|
||||
return fmt.Sprintf("http://%s", filesystem.GetLocalFileServerAddr())
|
||||
}
|
||||
|
||||
// DetectFileTypeByContent 通过文件内容检测文件类型
|
||||
@@ -822,3 +836,227 @@ func (a *App) SelectPDFSaveDirectory() (string, error) {
|
||||
|
||||
return a.pdfAPI.SelectDirectory()
|
||||
}
|
||||
|
||||
// ========== SFTP 接口 ==========
|
||||
|
||||
func (a *App) ensureSftpService() *sftp.Service {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.sftpService == nil {
|
||||
a.sftpService = sftp.NewService()
|
||||
}
|
||||
return a.sftpService
|
||||
}
|
||||
|
||||
// SftpConnectRequest SFTP 连接请求
|
||||
type SftpConnectRequest struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
KeyPath string `json:"key_path"`
|
||||
KeyPassphrase string `json:"key_passphrase"`
|
||||
}
|
||||
|
||||
// SftpConnect 建立 SFTP 连接,返回连接标识符 connID
|
||||
func (a *App) SftpConnect(req SftpConnectRequest) (string, error) {
|
||||
config := &sftp.Config{
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
KeyPath: req.KeyPath,
|
||||
KeyPassphrase: req.KeyPassphrase,
|
||||
}
|
||||
if config.Port == 0 { config.Port = 22 }
|
||||
if config.Timeout == 0 { config.Timeout = 15 * time.Second }
|
||||
|
||||
svc := a.ensureSftpService()
|
||||
_, err := svc.GetManager().Connect(config)
|
||||
if err != nil {
|
||||
return "", sftp.ToUserMessage(err)
|
||||
}
|
||||
|
||||
connID := sftp.ConnID(config.Host, config.Port)
|
||||
return connID, nil
|
||||
}
|
||||
|
||||
// SftpDisconnect 断开 SFTP 连接
|
||||
func (a *App) SftpDisconnect(connID string) error {
|
||||
parts := strings.SplitN(connID, ":", 2)
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("无效的连接标识符")
|
||||
}
|
||||
host := parts[0]
|
||||
port, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("无效的端口号")
|
||||
}
|
||||
|
||||
sftp.GetManager().Disconnect(host, port)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SftpListDir SFTP 列出目录
|
||||
func (a *App) SftpListDir(connID string, dirPath string) ([]map[string]interface{}, error) {
|
||||
return a.ensureSftpService().ListDir(connID, dirPath)
|
||||
}
|
||||
|
||||
// SftpReadFile SFTP 读取文件内容
|
||||
func (a *App) SftpReadFile(connID string, filePath string) (string, error) {
|
||||
return a.ensureSftpService().ReadFile(connID, filePath)
|
||||
}
|
||||
|
||||
// SftpWriteFileRequest SFTP 写入请求
|
||||
type SftpWriteFileRequest struct {
|
||||
SessionID string `json:"session_id"`
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// SftpWriteFile SFTP 写入文件
|
||||
func (a *App) SftpWriteFile(req SftpWriteFileRequest) error {
|
||||
return a.ensureSftpService().WriteFile(req.SessionID, req.Path, req.Content)
|
||||
}
|
||||
|
||||
// SftpWriteBase64File SFTP 写入 base64 编码的二进制文件(粘贴图片等)
|
||||
func (a *App) SftpWriteBase64File(sessionID, filePath, base64Content string) error {
|
||||
return a.ensureSftpService().WriteBase64File(sessionID, filePath, base64Content)
|
||||
}
|
||||
|
||||
// SftpGetFileInfo SFTP 获取文件信息
|
||||
func (a *App) SftpGetFileInfo(connID string, filePath string) (map[string]interface{}, error) {
|
||||
return a.ensureSftpService().GetFileInfo(connID, filePath)
|
||||
}
|
||||
|
||||
// SftpCreateDir SFTP 创建目录
|
||||
func (a *App) SftpCreateDir(connID string, dirPath string) (*filesystem.FileOperationResult, error) {
|
||||
return a.ensureSftpService().CreateDir(connID, dirPath)
|
||||
}
|
||||
|
||||
// SftpCreateFile SFTP 创建文件
|
||||
func (a *App) SftpCreateFile(connID string, filePath string) (*filesystem.FileOperationResult, error) {
|
||||
return a.ensureSftpService().CreateFile(connID, filePath)
|
||||
}
|
||||
|
||||
// SftpDeletePath SFTP 删除文件或目录
|
||||
func (a *App) SftpDeletePath(connID string, filePath string) (*filesystem.FileOperationResult, error) {
|
||||
return a.ensureSftpService().DeletePath(connID, filePath)
|
||||
}
|
||||
|
||||
// SftpRenamePathRequest SFTP 重命名请求
|
||||
type SftpRenamePathRequest struct {
|
||||
SessionID string `json:"session_id"`
|
||||
OldPath string `json:"old_path"`
|
||||
NewPath string `json:"new_path"`
|
||||
}
|
||||
|
||||
// SftpRenamePath SFTP 重命名文件或目录
|
||||
func (a *App) SftpRenamePath(req SftpRenamePathRequest) (*filesystem.FileOperationResult, error) {
|
||||
return a.ensureSftpService().RenamePath(req.SessionID, req.OldPath, req.NewPath)
|
||||
}
|
||||
|
||||
// SftpDownloadToTemp 下载远程文件到本地临时目录(用于预览)
|
||||
func (a *App) SftpDownloadToTemp(connID string, remotePath string) (string, error) {
|
||||
return a.ensureSftpService().DownloadToTemp(connID, remotePath)
|
||||
}
|
||||
|
||||
// SftpGetCommonPaths 获取 SFTP 远程主机常用路径
|
||||
func (a *App) SftpGetCommonPaths(connID string) (map[string]string, error) {
|
||||
return a.ensureSftpService().GetCommonPaths(connID)
|
||||
}
|
||||
|
||||
// SftpGetSystemInfo 获取 SFTP 远程主机系统信息(CPU/内存/磁盘)
|
||||
func (a *App) SftpGetSystemInfo(connID string) (map[string]interface{}, error) {
|
||||
return a.ensureSftpService().GetSystemInfo(connID)
|
||||
}
|
||||
|
||||
// --- 连接配置 CRUD (SQLite 持久化) ---
|
||||
|
||||
type SaveProfileRequest struct {
|
||||
ID *uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
KeyPath string `json:"key_path"`
|
||||
Type string `json:"type"`
|
||||
Token string `json:"token"`
|
||||
LastConnected *int64 `json:"last_connected"`
|
||||
}
|
||||
|
||||
var profileSvc *service.ProfileService
|
||||
|
||||
func (a *App) ensureProfileSvc() *service.ProfileService {
|
||||
if profileSvc == nil {
|
||||
profileSvc = service.NewProfileService()
|
||||
}
|
||||
return profileSvc
|
||||
}
|
||||
|
||||
func (a *App) LoadConnectionProfiles() ([]map[string]interface{}, error) {
|
||||
list, err := a.ensureProfileSvc().ListProfiles()
|
||||
if err != nil { return nil, err }
|
||||
result := make([]map[string]interface{}, len(list))
|
||||
for i, p := range list {
|
||||
result[i] = map[string]interface{}{
|
||||
"id": float64(p.ID),
|
||||
"name": p.Name,
|
||||
"host": p.Host,
|
||||
"port": p.Port,
|
||||
"username": p.Username,
|
||||
"password": p.Password,
|
||||
"keyPath": p.KeyPath,
|
||||
"type": p.Type,
|
||||
"token": p.Token,
|
||||
"lastConnected": p.LastConnected,
|
||||
"sortOrder": float64(p.SortOrder),
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a *App) SaveConnectionProfile(req SaveProfileRequest) (map[string]interface{}, error) {
|
||||
p := &models.ConnectionProfile{
|
||||
Name: req.Name, Host: req.Host, Port: req.Port,
|
||||
Username: req.Username, Password: req.Password,
|
||||
KeyPath: req.KeyPath, Type: req.Type, Token: req.Token,
|
||||
}
|
||||
if req.LastConnected != nil {
|
||||
t := time.Unix(*req.LastConnected, 0)
|
||||
p.LastConnected = &t
|
||||
}
|
||||
if req.ID != nil {
|
||||
p.ID = *req.ID
|
||||
}
|
||||
if err := a.ensureProfileSvc().SaveProfile(p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"id": float64(p.ID), "success": true}, nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteConnectionProfile(id uint) error {
|
||||
return a.ensureProfileSvc().DeleteProfile(id)
|
||||
}
|
||||
|
||||
func (a *App) GetLocalSystemInfo() (map[string]interface{}, error) {
|
||||
info := make(map[string]interface{})
|
||||
|
||||
cpuInfo, err := system.GetCPUInfo()
|
||||
if err == nil && cpuInfo != nil {
|
||||
if v, ok := cpuInfo["usage"].(string); ok { info["cpu_usage"] = v }
|
||||
}
|
||||
|
||||
memInfo, err := system.GetMemoryInfo()
|
||||
if err == nil && memInfo != nil {
|
||||
if v, ok := memInfo["usage"].(string); ok { info["mem_usage"] = v }
|
||||
}
|
||||
|
||||
diskInfos, err := system.GetDiskInfo()
|
||||
if err == nil && len(diskInfos) > 0 {
|
||||
if v, ok := diskInfos[0]["usage"].(string); ok { info["disk_usage"] = v }
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user