481 lines
19 KiB
C#
481 lines
19 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using Dapper;
|
||
using Microsoft.Data.Sqlite;
|
||
using Microsoft.Extensions.Logging;
|
||
|
||
namespace JiShe.CollectBus.PluginFileWatcher
|
||
{
|
||
/// <summary>
|
||
/// SQLite数据库管理器,用于管理文件事件的存储和检索
|
||
/// </summary>
|
||
public class EventDatabaseManager : IDisposable
|
||
{
|
||
private readonly FileMonitorConfig _config;
|
||
private readonly ILogger _logger;
|
||
private readonly string _connectionString;
|
||
private readonly string _databasePath;
|
||
private readonly int _commandTimeout;
|
||
private bool _disposed;
|
||
|
||
/// <summary>
|
||
/// 初始化数据库管理器
|
||
/// </summary>
|
||
/// <param name="config">配置对象</param>
|
||
/// <param name="logger">日志记录器</param>
|
||
public EventDatabaseManager(FileMonitorConfig config, ILogger logger)
|
||
{
|
||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||
|
||
// 确保使用配置中的设置
|
||
_databasePath = config.EventStorage.DatabasePath;
|
||
_connectionString = config.EventStorage.ConnectionString;
|
||
_commandTimeout = config.EventStorage.CommandTimeout;
|
||
|
||
// 确保数据库目录存在
|
||
string dbDirectory = Path.GetDirectoryName(_databasePath);
|
||
if (!string.IsNullOrEmpty(dbDirectory) && !Directory.Exists(dbDirectory))
|
||
{
|
||
Directory.CreateDirectory(dbDirectory);
|
||
}
|
||
|
||
// 初始化数据库
|
||
InitializeDatabase().GetAwaiter().GetResult();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化数据库,确保必要的表已创建
|
||
/// </summary>
|
||
private async Task InitializeDatabase()
|
||
{
|
||
try
|
||
{
|
||
using var connection = new SqliteConnection(_connectionString);
|
||
await connection.OpenAsync();
|
||
|
||
// 启用外键约束
|
||
using (var command = connection.CreateCommand())
|
||
{
|
||
command.CommandText = "PRAGMA foreign_keys = ON;";
|
||
await command.ExecuteNonQueryAsync();
|
||
}
|
||
|
||
// 创建文件事件表
|
||
string createTableSql = @"
|
||
CREATE TABLE IF NOT EXISTS FileEvents (
|
||
Id TEXT PRIMARY KEY,
|
||
Timestamp TEXT NOT NULL,
|
||
EventType INTEGER NOT NULL,
|
||
FullPath TEXT NOT NULL,
|
||
FileName TEXT NOT NULL,
|
||
Directory TEXT NOT NULL,
|
||
Extension TEXT NOT NULL,
|
||
OldFileName TEXT,
|
||
OldFullPath TEXT,
|
||
FileSize INTEGER,
|
||
CreatedAt TEXT NOT NULL
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON FileEvents(Timestamp);
|
||
CREATE INDEX IF NOT EXISTS idx_events_eventtype ON FileEvents(EventType);
|
||
CREATE INDEX IF NOT EXISTS idx_events_extension ON FileEvents(Extension);";
|
||
|
||
await connection.ExecuteAsync(createTableSql, commandTimeout: _commandTimeout);
|
||
|
||
// 创建元数据表
|
||
string createMetadataTableSql = @"
|
||
CREATE TABLE IF NOT EXISTS EventMetadata (
|
||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
EventId TEXT NOT NULL,
|
||
MetadataKey TEXT NOT NULL,
|
||
MetadataValue TEXT,
|
||
FOREIGN KEY (EventId) REFERENCES FileEvents(Id) ON DELETE CASCADE
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_metadata_eventid ON EventMetadata(EventId);
|
||
CREATE INDEX IF NOT EXISTS idx_metadata_key ON EventMetadata(MetadataKey);";
|
||
|
||
await connection.ExecuteAsync(createMetadataTableSql, commandTimeout: _commandTimeout);
|
||
|
||
_logger.LogInformation("数据库初始化成功");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "初始化数据库失败");
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存文件事件到数据库
|
||
/// </summary>
|
||
/// <param name="events">要保存的事件列表</param>
|
||
public async Task SaveEventsAsync(List<FileEvent> events)
|
||
{
|
||
if (events == null || events.Count == 0)
|
||
return;
|
||
|
||
try
|
||
{
|
||
using var connection = new SqliteConnection(_connectionString);
|
||
await connection.OpenAsync();
|
||
|
||
// 启用外键约束
|
||
using (var command = connection.CreateCommand())
|
||
{
|
||
command.CommandText = "PRAGMA foreign_keys = ON;";
|
||
await command.ExecuteNonQueryAsync();
|
||
}
|
||
|
||
// 开始事务
|
||
using var transaction = connection.BeginTransaction();
|
||
|
||
try
|
||
{
|
||
foreach (var fileEvent in events)
|
||
{
|
||
// 插入事件数据
|
||
string insertEventSql = @"
|
||
INSERT INTO FileEvents (
|
||
Id, Timestamp, EventType, FullPath, FileName,
|
||
Directory, Extension, OldFileName, OldFullPath,
|
||
FileSize, CreatedAt
|
||
) VALUES (
|
||
@Id, @Timestamp, @EventType, @FullPath, @FileName,
|
||
@Directory, @Extension, @OldFileName, @OldFullPath,
|
||
@FileSize, @CreatedAt
|
||
)";
|
||
|
||
await connection.ExecuteAsync(insertEventSql, new
|
||
{
|
||
Id = fileEvent.Id.ToString(), // 确保ID始终以字符串形式保存
|
||
Timestamp = fileEvent.Timestamp.ToString("o"),
|
||
EventType = (int)fileEvent.EventType,
|
||
fileEvent.FullPath,
|
||
fileEvent.FileName,
|
||
fileEvent.Directory,
|
||
fileEvent.Extension,
|
||
fileEvent.OldFileName,
|
||
fileEvent.OldFullPath,
|
||
fileEvent.FileSize,
|
||
CreatedAt = DateTime.UtcNow.ToString("o")
|
||
}, transaction, _commandTimeout);
|
||
|
||
// 插入元数据
|
||
if (fileEvent.Metadata != null && fileEvent.Metadata.Count > 0)
|
||
{
|
||
string insertMetadataSql = @"
|
||
INSERT INTO EventMetadata (EventId, MetadataKey, MetadataValue)
|
||
VALUES (@EventId, @MetadataKey, @MetadataValue)";
|
||
|
||
foreach (var metadata in fileEvent.Metadata)
|
||
{
|
||
await connection.ExecuteAsync(insertMetadataSql, new
|
||
{
|
||
EventId = fileEvent.Id.ToString(), // 确保ID以相同格式保存
|
||
MetadataKey = metadata.Key,
|
||
MetadataValue = metadata.Value
|
||
}, transaction, _commandTimeout);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 提交事务
|
||
transaction.Commit();
|
||
_logger.LogInformation($"已成功保存 {events.Count} 个事件到数据库");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 回滚事务
|
||
transaction.Rollback();
|
||
_logger.LogError(ex, "保存事件到数据库时发生错误");
|
||
throw;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "连接数据库失败");
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查询事件
|
||
/// </summary>
|
||
/// <param name="queryParams">查询参数</param>
|
||
/// <returns>查询结果</returns>
|
||
public async Task<EventQueryResult> QueryEventsAsync(EventQueryParams queryParams)
|
||
{
|
||
if (queryParams == null)
|
||
throw new ArgumentNullException(nameof(queryParams));
|
||
|
||
var result = new EventQueryResult
|
||
{
|
||
StartTime = queryParams.StartTime ?? DateTime.MinValue,
|
||
EndTime = queryParams.EndTime ?? DateTime.MaxValue
|
||
};
|
||
|
||
try
|
||
{
|
||
using var connection = new SqliteConnection(_connectionString);
|
||
await connection.OpenAsync();
|
||
|
||
// 启用外键约束
|
||
using (var command = connection.CreateCommand())
|
||
{
|
||
command.CommandText = "PRAGMA foreign_keys = ON;";
|
||
await command.ExecuteNonQueryAsync();
|
||
}
|
||
|
||
// 构建查询条件
|
||
var conditions = new List<string>();
|
||
var parameters = new DynamicParameters();
|
||
|
||
if (queryParams.StartTime.HasValue)
|
||
{
|
||
conditions.Add("Timestamp >= @StartTime");
|
||
parameters.Add("@StartTime", queryParams.StartTime.Value.ToString("o"));
|
||
}
|
||
|
||
if (queryParams.EndTime.HasValue)
|
||
{
|
||
conditions.Add("Timestamp <= @EndTime");
|
||
parameters.Add("@EndTime", queryParams.EndTime.Value.ToString("o"));
|
||
}
|
||
|
||
if (queryParams.EventType.HasValue)
|
||
{
|
||
conditions.Add("EventType = @EventType");
|
||
parameters.Add("@EventType", (int)queryParams.EventType.Value);
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(queryParams.PathFilter))
|
||
{
|
||
conditions.Add("FullPath LIKE @PathFilter");
|
||
parameters.Add("@PathFilter", $"%{queryParams.PathFilter}%");
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(queryParams.ExtensionFilter))
|
||
{
|
||
conditions.Add("Extension = @ExtensionFilter");
|
||
parameters.Add("@ExtensionFilter", queryParams.ExtensionFilter);
|
||
}
|
||
|
||
// 构建WHERE子句
|
||
string whereClause = conditions.Count > 0
|
||
? $"WHERE {string.Join(" AND ", conditions)}"
|
||
: string.Empty;
|
||
|
||
// 构建ORDER BY子句
|
||
string orderByClause = queryParams.AscendingOrder
|
||
? "ORDER BY Timestamp ASC"
|
||
: "ORDER BY Timestamp DESC";
|
||
|
||
// 获取总记录数
|
||
string countSql = $"SELECT COUNT(*) FROM FileEvents {whereClause}";
|
||
result.TotalCount = await connection.ExecuteScalarAsync<int>(countSql, parameters, commandTimeout: _commandTimeout);
|
||
|
||
// 应用分页
|
||
string paginationClause = $"LIMIT @PageSize OFFSET @Offset";
|
||
parameters.Add("@PageSize", queryParams.PageSize);
|
||
parameters.Add("@Offset", queryParams.PageIndex * queryParams.PageSize);
|
||
|
||
// 查询事件数据
|
||
string querySql = $@"
|
||
SELECT Id,
|
||
Timestamp,
|
||
EventType,
|
||
FullPath,
|
||
FileName,
|
||
Directory,
|
||
Extension,
|
||
OldFileName,
|
||
OldFullPath,
|
||
FileSize
|
||
FROM FileEvents
|
||
{whereClause}
|
||
{orderByClause}
|
||
{paginationClause}";
|
||
|
||
var events = await connection.QueryAsync<dynamic>(querySql, parameters, commandTimeout: _commandTimeout);
|
||
|
||
// 处理查询结果
|
||
foreach (var eventData in events)
|
||
{
|
||
var fileEvent = new FileEvent
|
||
{
|
||
Id = Guid.Parse(eventData.Id),
|
||
Timestamp = DateTime.Parse(eventData.Timestamp),
|
||
EventType = (FileEventType)eventData.EventType,
|
||
FullPath = eventData.FullPath,
|
||
FileName = eventData.FileName,
|
||
Directory = eventData.Directory,
|
||
Extension = eventData.Extension,
|
||
OldFileName = eventData.OldFileName,
|
||
OldFullPath = eventData.OldFullPath,
|
||
FileSize = eventData.FileSize
|
||
};
|
||
|
||
// 获取元数据
|
||
string metadataSql = "SELECT MetadataKey, MetadataValue FROM EventMetadata WHERE EventId = @EventId";
|
||
var metadata = await connection.QueryAsync<dynamic>(metadataSql, new { EventId = fileEvent.Id.ToString() }, commandTimeout: _commandTimeout);
|
||
|
||
foreach (var item in metadata)
|
||
{
|
||
fileEvent.Metadata[item.MetadataKey] = item.MetadataValue;
|
||
}
|
||
|
||
result.Events.Add(fileEvent);
|
||
}
|
||
|
||
result.HasMore = (queryParams.PageIndex + 1) * queryParams.PageSize < result.TotalCount;
|
||
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "查询事件时发生错误");
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理旧数据
|
||
/// </summary>
|
||
/// <param name="retentionDays">数据保留天数</param>
|
||
public async Task CleanupOldDataAsync(int retentionDays)
|
||
{
|
||
if (retentionDays <= 0)
|
||
return;
|
||
|
||
try
|
||
{
|
||
DateTime cutoffDate = DateTime.UtcNow.AddDays(-retentionDays);
|
||
string cutoffDateStr = cutoffDate.ToString("o");
|
||
|
||
using var connection = new SqliteConnection(_connectionString);
|
||
await connection.OpenAsync();
|
||
|
||
// 启用外键约束
|
||
using (var command = connection.CreateCommand())
|
||
{
|
||
command.CommandText = "PRAGMA foreign_keys = ON;";
|
||
await command.ExecuteNonQueryAsync();
|
||
}
|
||
|
||
// 删除旧事件(级联删除元数据)
|
||
string deleteSql = "DELETE FROM FileEvents WHERE Timestamp < @CutoffDate";
|
||
int deletedCount = await connection.ExecuteAsync(deleteSql, new { CutoffDate = cutoffDateStr }, commandTimeout: _commandTimeout);
|
||
|
||
_logger.LogInformation($"已清理 {deletedCount} 条旧事件数据({retentionDays}天前)");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "清理旧数据时发生错误");
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取数据库统计信息
|
||
/// </summary>
|
||
/// <returns>数据库统计信息</returns>
|
||
public async Task<DatabaseStats> GetDatabaseStatsAsync()
|
||
{
|
||
try
|
||
{
|
||
using var connection = new SqliteConnection(_connectionString);
|
||
await connection.OpenAsync();
|
||
|
||
// 启用外键约束
|
||
using (var command = connection.CreateCommand())
|
||
{
|
||
command.CommandText = "PRAGMA foreign_keys = ON;";
|
||
await command.ExecuteNonQueryAsync();
|
||
}
|
||
|
||
var stats = new DatabaseStats();
|
||
|
||
// 获取事件总数
|
||
stats.TotalEvents = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM FileEvents", commandTimeout: _commandTimeout);
|
||
|
||
// 获取最早和最新事件时间
|
||
stats.OldestEventTime = await connection.ExecuteScalarAsync<DateTime?>("SELECT Timestamp FROM FileEvents ORDER BY Timestamp ASC LIMIT 1", commandTimeout: _commandTimeout);
|
||
stats.NewestEventTime = await connection.ExecuteScalarAsync<DateTime?>("SELECT Timestamp FROM FileEvents ORDER BY Timestamp DESC LIMIT 1", commandTimeout: _commandTimeout);
|
||
|
||
// 获取事件类型分布
|
||
var eventTypeCounts = await connection.QueryAsync<dynamic>("SELECT EventType, COUNT(*) AS Count FROM FileEvents GROUP BY EventType", commandTimeout: _commandTimeout);
|
||
stats.EventTypeCounts = new Dictionary<FileEventType, int>();
|
||
|
||
foreach (var item in eventTypeCounts)
|
||
{
|
||
stats.EventTypeCounts[(FileEventType)item.EventType] = item.Count;
|
||
}
|
||
|
||
// 获取扩展名分布(前10个)
|
||
var extensionCounts = await connection.QueryAsync<dynamic>(
|
||
"SELECT Extension, COUNT(*) AS Count FROM FileEvents GROUP BY Extension ORDER BY Count DESC LIMIT 10",
|
||
commandTimeout: _commandTimeout);
|
||
stats.TopExtensions = new Dictionary<string, int>();
|
||
|
||
foreach (var item in extensionCounts)
|
||
{
|
||
stats.TopExtensions[item.Extension] = item.Count;
|
||
}
|
||
|
||
return stats;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "获取数据库统计信息时发生错误");
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 释放资源
|
||
/// </summary>
|
||
public void Dispose()
|
||
{
|
||
if (_disposed)
|
||
return;
|
||
|
||
_disposed = true;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 数据库统计信息
|
||
/// </summary>
|
||
public class DatabaseStats
|
||
{
|
||
/// <summary>
|
||
/// 事件总数
|
||
/// </summary>
|
||
public int TotalEvents { get; set; }
|
||
|
||
/// <summary>
|
||
/// 最早事件时间
|
||
/// </summary>
|
||
public DateTime? OldestEventTime { get; set; }
|
||
|
||
/// <summary>
|
||
/// 最新事件时间
|
||
/// </summary>
|
||
public DateTime? NewestEventTime { get; set; }
|
||
|
||
/// <summary>
|
||
/// 事件类型计数
|
||
/// </summary>
|
||
public Dictionary<FileEventType, int> EventTypeCounts { get; set; }
|
||
|
||
/// <summary>
|
||
/// 排名前列的文件扩展名
|
||
/// </summary>
|
||
public Dictionary<string, int> TopExtensions { get; set; }
|
||
}
|
||
} |