修复: 粘贴图失效与SFTP串台回退等一批问题
粘贴图根修(Ctrl+V直读系统剪贴板,不再依赖不可靠的paste事件)/SFTP串台双刀(拨号期禁回退本机+盘符路径自动回根)/PDF导出目录设置与文案带落盘路径/性能批(连接保存下载防重/md渲染防抖+预热/hljs按需加载/审计锁外置/快捷方式解析超时/webview-watch可停)/前端62类型错清零(含keyPath蛇形字段炸弹)/目录图标字段错位修复
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user