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