修复: 粘贴图失效与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
+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