新增:SFTP直连+连接池+autoConnect+文件服务器端口自动回退
- SFTP模块:连接/断开/文件CRUD/系统信息采集/base64二进制写入 - 连接池:多服务器同时在线,瞬间切换profile - autoConnect:启动时自动连接所有非本地服务器 - 端口自动回退:listenWithFallback消除TOCTOU,解决端口冲突崩溃 - 文件服务器URL集中管理:file-server.ts消除8+处硬编码端口 - Sidebar设置面板:添加服务器/自动连接/自动刷新开关 - 修复:validateFilePath越界panic、正则预编译 - 修复:注释准确性(RemoveAll/端口8073/动态端口文档)
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -15,6 +16,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const DefaultFileServerPort = 2652
|
||||
|
||||
// 预编译正则表达式(避免每次调用重复编译)
|
||||
var (
|
||||
// CSS 相关
|
||||
@@ -44,6 +47,7 @@ var (
|
||||
|
||||
// HTML 预览路径修复
|
||||
locationPathRegex = regexp.MustCompile(`\blocation\.pathname\b`)
|
||||
winDriveRegex = regexp.MustCompile(`^[A-Za-z]:`)
|
||||
)
|
||||
|
||||
// HTML 属性正则缓存(避免 replaceHtmlTagAttribute 中重复编译)
|
||||
@@ -80,7 +84,7 @@ func validateFilePath(rawPath string, logPrefix string) (string, error) {
|
||||
filePath = filepath.Clean(filePath)
|
||||
|
||||
// 确保绝对路径(Linux 以 / 开头,Windows 以盘符开头)
|
||||
if !filepath.IsAbs(filePath) && len(filePath) > 0 && filePath[0] != '/' && filePath[1] != ':' {
|
||||
if !filepath.IsAbs(filePath) && len(filePath) > 0 && filePath[0] != '/' && (len(filePath) < 2 || filePath[1] != ':') {
|
||||
filePath = "/" + filePath
|
||||
}
|
||||
|
||||
@@ -103,40 +107,22 @@ var (
|
||||
localFileServerOnce sync.Once
|
||||
)
|
||||
|
||||
// StartLocalFileServer 启动本地文件服务器
|
||||
// StartLocalFileServer 启动本地文件服务器(端口被占用时自动递增)
|
||||
func StartLocalFileServer() (string, error) {
|
||||
var initErr error
|
||||
localFileServerOnce.Do(func() {
|
||||
// 创建多路复用器
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// 注册 /localfs/ 路由
|
||||
mux.HandleFunc("/localfs/", handleLocalFileRequest)
|
||||
|
||||
// 注册 HTML 预览专用路由
|
||||
mux.HandleFunc("/localfs/html-preview", handleHtmlPreviewRequest)
|
||||
|
||||
// 创建服务器(固定端口)
|
||||
server := &http.Server{
|
||||
Addr: "localhost:8073",
|
||||
Handler: mux,
|
||||
addr, srv, err := listenWithFallback(DefaultFileServerPort, mux)
|
||||
if err != nil {
|
||||
initErr = fmt.Errorf("无法绑定端口(%d起始): %w", DefaultFileServerPort, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 启动服务器
|
||||
go func() {
|
||||
log.Printf("[LocalFileServer] 正在启动...")
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("[LocalFileServer] 启动失败: %v", err)
|
||||
initErr = err
|
||||
}
|
||||
}()
|
||||
|
||||
localFileServer = &LocalFileServer{
|
||||
server: server,
|
||||
addr: "localhost:8073",
|
||||
}
|
||||
|
||||
log.Printf("[LocalFileServer] 已启动,监听: %s", localFileServer.addr)
|
||||
localFileServer = &LocalFileServer{server: srv, addr: addr}
|
||||
log.Printf("[LocalFileServer] 已启动,监听: %s", addr)
|
||||
})
|
||||
|
||||
if localFileServer == nil {
|
||||
@@ -145,6 +131,33 @@ func StartLocalFileServer() (string, error) {
|
||||
return localFileServer.addr, initErr
|
||||
}
|
||||
|
||||
// listenWithFallback 从 basePort 开始尝试绑定,递增直到成功(最多试 10 次)
|
||||
// 返回的 server 已在 goroutine 中 Serve(l),调用方无需再启动
|
||||
func listenWithFallback(basePort int, handler http.Handler) (addr string, srv *http.Server, err error) {
|
||||
for offset := 0; offset < 10; offset++ {
|
||||
port := basePort + offset
|
||||
addr = fmt.Sprintf("localhost:%d", port)
|
||||
l, e := net.Listen("tcp", addr)
|
||||
if e == nil {
|
||||
srv = &http.Server{Handler: handler}
|
||||
go func() {
|
||||
if se := srv.Serve(l); se != http.ErrServerClosed {
|
||||
log.Printf("[LocalFileServer] 异常退出: %v", se)
|
||||
}
|
||||
}()
|
||||
return addr, srv, nil
|
||||
}
|
||||
log.Printf("[LocalFileServer] 端口 %d 被占用,尝试 %d...", port, port+1)
|
||||
}
|
||||
return "", nil, fmt.Errorf("端口 %d-%d 均不可用", basePort, basePort+9)
|
||||
}
|
||||
|
||||
// GetLocalFileServerAddr 返回实际绑定的地址(含动态分配的端口)
|
||||
func GetLocalFileServerAddr() string {
|
||||
if localFileServer == nil { return fmt.Sprintf("http://localhost:%d", DefaultFileServerPort) }
|
||||
return localFileServer.addr
|
||||
}
|
||||
|
||||
// handleLocalFileRequest 处理本地文件请求
|
||||
func handleLocalFileRequest(w http.ResponseWriter, r *http.Request) {
|
||||
// CORS 头:允许所有源访问(因为这是本地文件服务器)
|
||||
@@ -177,7 +190,7 @@ func handleLocalFileRequest(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
// 仅对非绝对路径添加前导 /(Windows 盘符路径如 D:/ 已经是绝对路径,不能加 /)
|
||||
if !strings.HasPrefix(pathPart, "/") && !regexp.MustCompile(`^[A-Za-z]:`).MatchString(pathPart) {
|
||||
if !strings.HasPrefix(pathPart, "/") && !winDriveRegex.MatchString(pathPart) {
|
||||
pathPart = "/" + pathPart
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user