Files
u-desk/internal/filesystem/audit_log.go
T
lxy 75a42c0b6a 修复: 粘贴图失效与SFTP串台回退等一批问题
粘贴图根修(Ctrl+V直读系统剪贴板,不再依赖不可靠的paste事件)/SFTP串台双刀(拨号期禁回退本机+盘符路径自动回根)/PDF导出目录设置与文案带落盘路径/性能批(连接保存下载防重/md渲染防抖+预热/hljs按需加载/审计锁外置/快捷方式解析超时/webview-watch可停)/前端62类型错清零(含keyPath蛇形字段炸弹)/目录图标字段错位修复
2026-10-24 13:48:54 +08:00

315 lines
7.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package filesystem
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
"u-desk/internal/common"
)
// AuditOperation 审计操作类型
type AuditOperation string
const (
OperationRead AuditOperation = "read" // 读取文件
OperationWrite AuditOperation = "write" // 写入文件
OperationDelete AuditOperation = "delete" // 删除文件
OperationCreate AuditOperation = "create" // 创建目录
OperationRename AuditOperation = "rename" // 重命名
OperationCopy AuditOperation = "copy" // 复制
OperationMove AuditOperation = "move" // 移动
OperationList AuditOperation = "list" // 列出目录
OperationDownload AuditOperation = "download" // 下载
)
// AuditLogEntry 审计日志条目
type AuditLogEntry struct {
Timestamp time.Time `json:"timestamp"` // 操作时间
Operation AuditOperation `json:"operation"` // 操作类型
Path string `json:"path"` // 文件路径
OldPath string `json:"old_path,omitempty"` // 原路径(重命名/移动)
Size int64 `json:"size,omitempty"` // 文件大小
IsDirectory bool `json:"is_directory"` // 是否为目录
Success bool `json:"success"` // 操作是否成功
Error string `json:"error,omitempty"` // 错误信息
UserAgent string `json:"user_agent,omitempty"` // 用户代理
IPAddress string `json:"ip_address,omitempty"` // IP地址
}
// AuditLogger 审计日志记录器
type AuditLogger struct {
logFile *os.File
logDir string
mu sync.Mutex
buffer []AuditLogEntry
bufferSize int
stopChan chan struct{}
initialized bool // 懒初始化标志(失败时允许重试)
}
// NewAuditLogger 创建审计日志记录器(延迟初始化,不立即打开文件)
func NewAuditLogger(logDir string) (*AuditLogger, error) {
return &AuditLogger{
logDir: logDir,
buffer: make([]AuditLogEntry, 0, 100),
bufferSize: 100,
stopChan: make(chan struct{}),
}, nil
}
// ensureReady 首次写入时才创建目录、打开文件、启动后台协程
// 在 a.mu.Lock 保护下调用,失败时下次 Log 会重试
func (a *AuditLogger) ensureReady() error {
if a.initialized {
return nil
}
if err := os.MkdirAll(a.logDir, 0755); err != nil {
return fmt.Errorf("创建日志目录失败: %v", err)
}
timestamp := time.Now().Format("2006-01-02")
f, err := os.OpenFile(
filepath.Join(a.logDir, fmt.Sprintf("audit_%s.log", timestamp)),
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return err
}
a.logFile = f
a.initialized = true
common.Go("audit-flush", a.backgroundFlush)
return nil
}
// Log 记录操作日志
func (a *AuditLogger) Log(entry AuditLogEntry) error {
// 设置时间戳
if entry.Timestamp.IsZero() {
entry.Timestamp = time.Now()
}
a.mu.Lock()
defer a.mu.Unlock()
// 懒初始化:首次写入时才打开文件
if a.logFile == nil {
if err := a.ensureReady(); err != nil {
return err
}
}
// 添加到缓冲区
a.buffer = append(a.buffer, entry)
// 如果缓冲区满了,立即写入(缓冲满即刷的热路径语义不变:写+Sync 在已持有的锁内完成)
if len(a.buffer) >= a.bufferSize {
if f, err := a.flush(); err != nil {
return err
} else if f != nil {
if err := f.Sync(); err != nil {
return err
}
}
}
return nil
}
// LogDelete 记录删除操作(便捷方法)
func (a *AuditLogger) LogDelete(path string, isDir bool, size int64, err error) {
entry := AuditLogEntry{
Timestamp: time.Now(),
Operation: OperationDelete,
Path: path,
Size: size,
IsDirectory: isDir,
Success: err == nil,
}
if err != nil {
entry.Error = err.Error()
}
_ = a.Log(entry)
}
// LogWrite 记录写入操作(便捷方法)
func (a *AuditLogger) LogWrite(path string, size int64, err error) {
entry := AuditLogEntry{
Timestamp: time.Now(),
Operation: OperationWrite,
Path: path,
Size: size,
IsDirectory: false,
Success: err == nil,
}
if err != nil {
entry.Error = err.Error()
}
_ = a.Log(entry)
}
// LogRead 记录读取操作(便捷方法)
func (a *AuditLogger) LogRead(path string, size int64, err error) {
entry := AuditLogEntry{
Timestamp: time.Now(),
Operation: OperationRead,
Path: path,
Size: size,
IsDirectory: false,
Success: err == nil,
}
if err != nil {
entry.Error = err.Error()
}
_ = a.Log(entry)
}
// flush 将缓冲区写入文件(调用方须持有 a.mu),返回刚写入的文件供调用方在锁外 Sync。
// Sync 是慢速磁盘操作,不放在锁内执行,避免阻塞 Log 热路径。
func (a *AuditLogger) flush() (*os.File, error) {
if len(a.buffer) == 0 {
return nil, nil
}
// 序列化所有条目为JSON(每行一个)
for _, entry := range a.buffer {
data, err := json.Marshal(entry)
if err != nil {
continue // 序列化失败,跳过该条目
}
if _, err := a.logFile.Write(append(data, '\n')); err != nil {
return nil, err
}
}
// 清空缓冲区
a.buffer = a.buffer[:0]
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 后台协程,定期刷新缓冲区
func (a *AuditLogger) backgroundFlush() {
ticker := time.NewTicker(5 * time.Second) // 每5秒刷新一次
defer ticker.Stop()
for {
select {
case <-ticker.C:
_ = a.flushSynced()
case <-a.stopChan:
// 停止前刷新一次
_ = a.flushSynced()
return
}
}
}
// Close 关闭审计日志记录器
func (a *AuditLogger) Close() error {
close(a.stopChan)
a.mu.Lock()
defer a.mu.Unlock()
// 从未写入过日志,无需关闭
if a.logFile == nil {
return 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 获取最近的审计日志(尾部倒读:文件大于 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))
f, err := os.Open(logPath)
if err != nil {
return nil, err
}
defer f.Close()
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
}
for i := len(lines) - 1; i >= start; i-- {
var entry AuditLogEntry
if err := json.Unmarshal([]byte(lines[i]), &entry); err == nil {
entries = append(entries, entry)
}
}
return entries, nil
}
// parseLines 解析文本为行(过滤空行)
func parseLines(text string) []string {
raw := strings.Split(text, "\n")
lines := make([]string, 0, len(raw))
for _, line := range raw {
if line != "" {
lines = append(lines, line)
}
}
return lines
}