修复: 粘贴图失效与SFTP串台回退等一批问题

粘贴图根修(Ctrl+V直读系统剪贴板,不再依赖不可靠的paste事件)/SFTP串台双刀(拨号期禁回退本机+盘符路径自动回根)/PDF导出目录设置与文案带落盘路径/性能批(连接保存下载防重/md渲染防抖+预热/hljs按需加载/审计锁外置/快捷方式解析超时/webview-watch可停)/前端62类型错清零(含keyPath蛇形字段炸弹)/目录图标字段错位修复
This commit is contained in:
lxy
2026-10-24 13:48:54 +08:00
parent 4675ae381d
commit 75a42c0b6a
48 changed files with 1072 additions and 220 deletions
+6 -4
View File
@@ -14,6 +14,8 @@ import (
"strings"
"sync"
"time"
"u-desk/internal/common"
)
const DefaultFileServerPort = 2652
@@ -142,11 +144,11 @@ func listenWithFallback(basePort int, handler http.Handler) (addr string, srv *h
l, e := net.Listen("tcp", addr)
if e == nil {
srv = &http.Server{Handler: handler}
go func() {
common.Go("local-file-server-serve", 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)
@@ -346,11 +348,11 @@ type LocalFileHandler struct {
// NewLocalFileHandler 创建本地文件处理器
func NewLocalFileHandler() *LocalFileHandler {
// 启动本地文件服务器
go func() {
common.Go("local-file-handler-start", func() {
if _, err := StartLocalFileServer(); err != nil {
log.Printf("[LocalFileHandler] 启动本地文件服务器失败: %v", err)
}
}()
})
return &LocalFileHandler{}
}
+59 -26
View File
@@ -3,11 +3,14 @@ package filesystem
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
"u-desk/internal/common"
)
// AuditOperation 审计操作类型
@@ -78,7 +81,7 @@ func (a *AuditLogger) ensureReady() error {
}
a.logFile = f
a.initialized = true
go a.backgroundFlush()
common.Go("audit-flush", a.backgroundFlush)
return nil
}
@@ -102,10 +105,14 @@ func (a *AuditLogger) Log(entry AuditLogEntry) error {
// 添加到缓冲区
a.buffer = append(a.buffer, entry)
// 如果缓冲区满了,立即写入
// 如果缓冲区满了,立即写入(缓冲满即刷的热路径语义不变:写+Sync 在已持有的锁内完成)
if len(a.buffer) >= a.bufferSize {
if err := a.flush(); err != nil {
if f, err := a.flush(); err != nil {
return err
} else if f != nil {
if err := f.Sync(); err != nil {
return err
}
}
}
@@ -166,10 +173,11 @@ func (a *AuditLogger) LogRead(path string, size int64, err error) {
_ = a.Log(entry)
}
// flush 将缓冲区写入文件
func (a *AuditLogger) flush() error {
// flush 将缓冲区写入文件(调用方须持有 a.mu),返回刚写入的文件供调用方在锁外 Sync。
// Sync 是慢速磁盘操作,不放在锁内执行,避免阻塞 Log 热路径。
func (a *AuditLogger) flush() (*os.File, error) {
if len(a.buffer) == 0 {
return nil
return nil, nil
}
// 序列化所有条目为JSON(每行一个)
@@ -179,19 +187,25 @@ func (a *AuditLogger) flush() error {
continue // 序列化失败,跳过该条目
}
if _, err := a.logFile.Write(append(data, '\n')); err != nil {
return err
return nil, err
}
}
// 刷新到磁盘
if err := a.logFile.Sync(); err != nil {
return err
}
// 清空缓冲区
a.buffer = a.buffer[:0]
return nil
return a.logFile, nil
}
// flushSynced 锁内写缓冲、锁外 Sync:fsync 不占 a.mu,后台定时刷盘不再阻塞 Log
func (a *AuditLogger) flushSynced() error {
a.mu.Lock()
f, err := a.flush()
a.mu.Unlock()
if err != nil || f == nil {
return err
}
return f.Sync()
}
// backgroundFlush 后台协程,定期刷新缓冲区
@@ -202,14 +216,10 @@ func (a *AuditLogger) backgroundFlush() {
for {
select {
case <-ticker.C:
a.mu.Lock()
_ = a.flush()
a.mu.Unlock()
_ = a.flushSynced()
case <-a.stopChan:
// 停止前刷新一次
a.mu.Lock()
_ = a.flush()
a.mu.Unlock()
_ = a.flushSynced()
return
}
}
@@ -227,31 +237,54 @@ func (a *AuditLogger) Close() error {
return nil
}
// 刷新剩余缓冲区
if err := a.flush(); err != nil {
// 刷新剩余缓冲区(进程退出路径,Sync 在锁内不影响运行期热路径)
if f, err := a.flush(); err != nil {
return err
} else if f != nil {
if err := f.Sync(); err != nil {
return err
}
}
// 关闭文件
return a.logFile.Close()
}
// GetRecentLogs 获取最近的审计日志
// GetRecentLogs 获取最近的审计日志(尾部倒读:文件大于 64KB 时只 Seek 读末尾 64KB,
// 日志单行远小于 64KB,足以覆盖 limit 条最新记录,避免随日志增长全量读盘)
func GetRecentLogs(logDir string, limit int) ([]AuditLogEntry, error) {
// 读取今天的日志文件
timestamp := time.Now().Format("2006-01-02")
logPath := filepath.Join(logDir, fmt.Sprintf("audit_%s.log", timestamp))
data, err := os.ReadFile(logPath)
f, err := os.Open(logPath)
if err != nil {
return nil, err
}
defer f.Close()
// 解析JSON(每行一个条目)
var entries []AuditLogEntry
const tailSize = 64 * 1024
info, err := f.Stat()
if err != nil {
return nil, err
}
offset := info.Size() - tailSize
if offset < 0 {
offset = 0
}
data := make([]byte, info.Size()-offset)
if _, err := f.ReadAt(data, offset); err != nil && err != io.EOF {
return nil, err
}
// 从文件中间开始读时首个字节位置未必在行边界,丢弃首条残行
lines := parseLines(string(data))
if offset > 0 && len(lines) > 0 {
lines = lines[1:]
}
// 从后往前读取(最新的在前)
// 解析JSON(每行一个条目),从后往前读取(最新的在前)
var entries []AuditLogEntry
start := len(lines) - limit
if start < 0 {
start = 0
+4 -2
View File
@@ -6,6 +6,8 @@ import (
"path/filepath"
"runtime"
"time"
"u-desk/internal/common"
)
// ========== 辅助函数 ==========
@@ -43,10 +45,10 @@ func OpenPath(path string) error {
}
// 给进程一点时间启动
go func() {
common.Go("fs-process-release", func() {
time.Sleep(100 * time.Millisecond)
cmd.Process.Release()
}()
})
return nil
}
+1 -1
View File
@@ -52,7 +52,7 @@ func NewRecycleBin(binPath string) (*RecycleBin, error) {
}
// 启动自动清理协程
go bin.autoCleanup()
common.Go("recycle-autoclean", bin.autoCleanup)
return bin, nil
}
+7 -2
View File
@@ -878,10 +878,15 @@ func (s *FileSystemService) ResolveShortcut(lnkPath string) (targetPath string,
lnkPath,
)
// 执行 PowerShell 命令
cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", psScript)
// 执行 PowerShell 命令(10s 超时:PowerShell 冷启动/被安全软件拖住时不无限挂起)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "powershell", "-NoProfile", "-NonInteractive", "-Command", psScript)
output, err := cmd.Output()
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return "", fmt.Errorf("解析快捷方式超时(10s)")
}
return "", fmt.Errorf("解析快捷方式失败: %w", err)
}
+3 -1
View File
@@ -9,6 +9,8 @@ import (
"path/filepath"
"strings"
"time"
"u-desk/internal/common"
)
// ZipFileEntry 表示 zip 文件中的一个文件条目
@@ -231,7 +233,7 @@ func ExtractFileFromZipToTemp(zipPath, filePath string) (string, error) {
debugLog("[ExtractFileFromZipToTemp] 开始提取: %s from %s", filePath, zipPath)
// 启动临时文件清理协程
go CleanOldTempFiles()
common.Go("zip-clean-temp", CleanOldTempFiles)
// 创建临时目录
tempDir := filepath.Join(os.TempDir(), TempFileDir)