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 }