新增配置文件热重载功能

This commit is contained in:
root 2024-12-26 21:59:50 +08:00
parent 8a38a79af0
commit 5090f7e7a6
12 changed files with 190 additions and 40 deletions

View file

@ -1,5 +1,6 @@
using iFileProxy.Services;
using Serilog;
using System.Security.Policy;
using System.Text.Json;
using System.Text.Json.Serialization;
@ -38,7 +39,9 @@ namespace iFileProxy.Config
{
try
{
return JsonSerializer.Deserialize<AppConfig>(File.ReadAllText(configPath),options);
using FileStream fs = new FileStream(configPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using StreamReader sr = new StreamReader(fs, System.Text.Encoding.Default);
return JsonSerializer.Deserialize<AppConfig>(sr.ReadToEnd(),options);
}
catch (Exception e)
{

View file

@ -5,17 +5,36 @@ using Serilog;
using System.Web;
using System.Net;
using iFileProxy.Helpers;
using iFileProxy.Services; // 用于 URL 解码
using iFileProxy.Services;
namespace iFileProxy.Controllers
{
[Route("/")]
public class StreamProxyController(IHttpClientFactory httpClientFactory, AppConfig appConfig, DatabaseGateService dbGateService) : ControllerBase
public class StreamProxyController : ControllerBase
{
private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
private long SizeLimit = appConfig.StreamProxyOptions.SizeLimit;
private AppConfig _appConfig ;
private readonly IHttpClientFactory _httpClientFactory;
private readonly DatabaseGateService _dbGateService;
private long SizeLimit;
private readonly static Serilog.ILogger _logger = Log.Logger.ForContext<StreamProxyController>();
public StreamProxyController(IHttpClientFactory httpClientFactory, AppConfigService appConfigService, DatabaseGateService dbGateService)
{
_appConfig = appConfigService.AppConfig;
_httpClientFactory = httpClientFactory;
SizeLimit = _appConfig.StreamProxyOptions.SizeLimit;
_dbGateService = dbGateService;
appConfigService.AppConfigurationChanged += AppConfigService_AppConfigurationChanged;
}
private void AppConfigService_AppConfigurationChanged(object sender, AppConfig appConfig)
{
_appConfig = appConfig;
SizeLimit = _appConfig.StreamProxyOptions.SizeLimit;
}
// 匹配文件代理请求
[HttpGet("{*proxyUrl}")]
[HttpPost("{*proxyUrl}")]
@ -34,7 +53,7 @@ namespace iFileProxy.Controllers
else
if (!proxyUrl.StartsWith("http://") && !proxyUrl.StartsWith("https://")) // 不带协议头 直接扔进垃圾桶
return StatusCode(400, "非法Url! 除GitHubUrl外其他代理请求必须携带协议头!");
foreach (var keywords in appConfig.SecurityOptions.BlockedKeyword)
foreach (var keywords in _appConfig.SecurityOptions.BlockedKeyword)
{
if (proxyUrl.Contains(keywords, StringComparison.CurrentCulture))
return StatusCode((int)HttpStatusCode.Forbidden, "Keyword::Forbidden");
@ -158,7 +177,7 @@ namespace iFileProxy.Controllers
if (contentLength > 0)
Response.ContentLength = contentLength;
await dbGateService.AddTaskInfoDataAsync(new TaskInfo
await _dbGateService.AddTaskInfoDataAsync(new TaskInfo
{
FileName = fileName,
Size = contentLength ?? -1L,

View file

@ -11,17 +11,24 @@ namespace iFileProxy.Controllers
public class iProxyController : ControllerBase
{
private readonly TaskManager _taskManager;
private readonly AppConfig _appConfig;
private AppConfig _appConfig;
private readonly DatabaseGateService _dbGate;
private readonly static Serilog.ILogger _logger = Log.Logger.ForContext<iProxyController>();
public iProxyController(TaskManager taskManager, AppConfig appConfig, DatabaseGateService databaseGateService)
public iProxyController(TaskManager taskManager, AppConfigService appConfigService, DatabaseGateService databaseGateService)
{
_taskManager = taskManager;
_appConfig = appConfig;
_appConfig = appConfigService.AppConfig;
_dbGate = databaseGateService;
appConfigService.AppConfigurationChanged += AppConfigService_AppConfigurationChanged;
}
private void AppConfigService_AppConfigurationChanged(object sender, AppConfig appConfig)
{
_appConfig = appConfig;
}
[HttpPost]

View file

@ -5,7 +5,7 @@ namespace iFileProxy.Handlers
{
public class CmdArgsHandler
{
public static void ParseArgs(string[] args)
public static void ParseArgs(string[] args, IServiceProvider serviceProvider)
{
if (args.Length == 0)
return;
@ -29,8 +29,7 @@ namespace iFileProxy.Handlers
if (cmdlineHelper.GetBooleanValue("--init-db"))
{
DatabaseGateService databaseGateService = new(AppConfig.GetCurrConfig());
databaseGateService.TryInitialDB();
serviceProvider.GetRequiredService<DatabaseGateService>().TryInitialDB();
}
}
}

View file

@ -10,7 +10,7 @@ namespace iFileProxy.Middlewares
{
private readonly RequestDelegate _next = next;
private readonly static ILogger _logger = Log.Logger.ForContext<ErrorHandlerMiddleware>();
private readonly static Serilog.ILogger _logger = Log.Logger.ForContext<ErrorHandlerMiddleware>();
public async Task InvokeAsync(HttpContext context)
{

View file

@ -1,11 +1,12 @@
using iFileProxy.Config;
using iFileProxy.Helpers;
using iFileProxy.Models;
using iFileProxy.Services;
using System.Text.Json;
namespace iFileProxy.Middlewares
{
public class IPAccessLimitMiddleware(RequestDelegate next, Dictionary<string, Dictionary<string, uint>> IPAccessCountDict, AppConfig appConfig)
public class IPAccessLimitMiddleware(RequestDelegate next, Dictionary<string, Dictionary<string, uint>> IPAccessCountDict, AppConfigService appConfigService)
{
private readonly RequestDelegate _next = next;
private readonly Dictionary<string, Dictionary<string, uint>> _IPAccessCountDict = IPAccessCountDict;
@ -15,7 +16,7 @@ namespace iFileProxy.Middlewares
string ipStr = MasterHelper.GetClientIPAddr(context);
// 处理配置文件拉黑IP
if (appConfig.SecurityOptions.BlockedClientIP.IndexOf(ipStr) != -1)
if (appConfigService.AppConfig.SecurityOptions.BlockedClientIP.IndexOf(ipStr) != -1)
{
context.Response.StatusCode = 403;
await context.Response.WriteAsJsonAsync(new CommonRsp { Retcode = 403, Message = "你的IP地址已经被管理员拉入黑名单!" });
@ -23,7 +24,7 @@ namespace iFileProxy.Middlewares
}
// 获取需要跟踪的路由列表
var routesToTrack = appConfig.SecurityOptions.RoutesToTrack;
var routesToTrack = appConfigService.AppConfig.SecurityOptions.RoutesToTrack;
// 如果没有匹配到需要跟踪的路由,直接调用下一个中间件
if (!routesToTrack.Any(p => context.Request.Path.ToString().StartsWith(p, StringComparison.OrdinalIgnoreCase)))
@ -42,7 +43,7 @@ namespace iFileProxy.Middlewares
if (ipCounts.TryGetValue(ipStr, out uint value))
{
if (ipCounts[ipStr] >= appConfig.SecurityOptions.DailyRequestLimitPerIP)
if (ipCounts[ipStr] >= appConfigService.AppConfig.SecurityOptions.DailyRequestLimitPerIP)
{
context.Response.StatusCode = 200;
context.Response.ContentType = "application/json";

View file

@ -19,12 +19,12 @@ namespace iFileProxy
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
SerilogConfig.CreateLogger(args: args);
CommandLineArgsHelper argsHelper = new (args);
Serilog.ILogger logger = Log.Logger.ForContext<Program>();
CommandLineArgsHelper argsHelper = new(args);
Console.Write(" "); // 补全日志第一行开头的空白
CmdArgsHandler.ParseArgs(args);
var builder = WebApplication.CreateBuilder(args);
// CORS配置
@ -46,9 +46,11 @@ namespace iFileProxy
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Host.UseSerilog(logger: Log.Logger);
// 注入依赖
builder.Services.AddSingleton<AppConfigService>();
builder.Services.AddSingleton(AppConfig.GetCurrConfig());
builder.Services.AddSingleton<DatabaseGateService>();
builder.Services.AddSingleton<TaskManager>();
@ -79,6 +81,13 @@ namespace iFileProxy
// 初始化缓存管理服务
LocalCacheManager localCacheManager = new(app.Services);
// 解析命令行参数
CmdArgsHandler.ParseArgs(args, app.Services);
// 开启配置热重载
app.Services.GetRequiredService<AppConfigService>().EnableHotReload();
if (!argsHelper.GetBooleanValue("disable-startup-check"))
// 初始化验证配置文件
AppConfig.CheckAppConfig(app.Services);
@ -99,6 +108,7 @@ namespace iFileProxy
// 4. 强制使用HTTPS要在Routing之前
app.UseHttpsRedirection();
// 5. 请求日志记录在Routing之前
app.UseSerilogRequestLogging(options =>
{
@ -110,6 +120,7 @@ namespace iFileProxy
};
});
// 6. 路由配置
app.UseRouting();
@ -128,7 +139,7 @@ namespace iFileProxy
// 11. 启动应用
var dbGateService = app.Services.GetRequiredService<DatabaseGateService>();
SerilogConfig.CreateLogger(dbGateService,args);
SerilogConfig.CreateLogger(dbGateService, args);
if (!argsHelper.GetStringValue("url").IsNullOrEmpty())
app.Run(argsHelper.GetStringValue("url"));
@ -147,7 +158,7 @@ namespace iFileProxy
{
if (mySqlEx.Message.Contains("is not allowed to connect to this MySQL server", StringComparison.CurrentCulture))
{
logger.Fatal( "远程主机不允许你连接到该数据库, 请检查数据库服务端是否将本机IP加白! ");
logger.Fatal("远程主机不允许你连接到该数据库, 请检查数据库服务端是否将本机IP加白! ");
}
else if (mySqlEx.Message.Contains("Unable to connect to any of the specified MySQL hosts"))
{

View file

@ -0,0 +1,85 @@
using iFileProxy.Config;
using iFileProxy.Helpers;
using Newtonsoft.Json;
using Serilog;
using System.Configuration;
namespace iFileProxy.Services
{
/// <summary>
/// 应用程序配置服务,用于获取当前配置、配置热重载等
/// </summary>
public class AppConfigService
{
public delegate void AppConfigurationChangeEventHandler(object sender, AppConfig appConfig);
/// <summary>
/// 当程序配置文件发生变化时触发此事件
/// </summary>
public event AppConfigurationChangeEventHandler? AppConfigurationChanged;
private readonly Serilog.ILogger _logger = Log.Logger.ForContext<AppConfigService>();
public AppConfig AppConfig { get; private set; } = new();
readonly string _appConfigPath;
FileSystemWatcher _watcher = new();
DateTimeOffset _last_change_time = DateTimeOffset.Now;
public AppConfigService(string configFilePath = "iFileProxy.json")
{
_appConfigPath = configFilePath;
LoadAppConfig();
}
public void LoadAppConfig()
{
if (File.Exists(_appConfigPath))
{
try
{
AppConfig = AppConfig.GetCurrConfig(_appConfigPath);
_logger.Information("Configuration loaded successfully.");
AppConfigurationChanged?.Invoke(this, AppConfig);
_logger.Debug("app configuration change event invoke succ.");
}
catch (Exception e)
{
_logger.Error(e, "配置文件解析失败!!!");
throw;
}
}
else
{
_logger.Error($"配置文件: {_appConfigPath} 目标不存在!");
}
}
public void EnableHotReload()
{
_logger.Information($"App Configuration File Hotreload has enabled.");
_watcher = new()
{
Filter = Path.GetFileName(_appConfigPath),
Path = Path.GetDirectoryName(_appConfigPath) != "" && Path.GetDirectoryName(_appConfigPath) !=null ? Path.GetDirectoryName(_appConfigPath) : AppDomain.CurrentDomain.BaseDirectory,
// IncludeSubdirectories = true
};
_logger.Information($"Filter: {_watcher.Filter} Path: {_watcher.Path}");
_watcher.Changed += (s, e) =>
{
// 防抖处理 防止多次触发change事件
if (DateTimeOffset.Now.ToUnixTimeSeconds() - _last_change_time.ToUnixTimeSeconds() > 1)
{
_last_change_time = DateTimeOffset.Now;
_logger.Information($"Configuration file has changed, program configuration is being reloaded...");
LoadAppConfig();
}
};
_watcher.EnableRaisingEvents = true;
}
}
}

View file

@ -26,20 +26,22 @@ namespace iFileProxy.Services
{"下载历史",@"CREATE TABLE `t_download_history` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键', `tid` varchar(48) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '任务UUID', `time` datetime NULL DEFAULT NULL COMMENT '触发时间', `client_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端IP', PRIMARY KEY (`id`) USING BTREE, INDEX `tid`(`tid`) USING BTREE, CONSTRAINT `t_download_history_ibfk_1` FOREIGN KEY (`tid`) REFERENCES `t_tasks_info` (`tid`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE = InnoDB AUTO_INCREMENT = 532 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;" }
};
public DatabaseGateService(AppConfig appConfig)
public DatabaseGateService(AppConfigService appConfigService)
{
_logger.Information("Initializing DatabaseGateService...");
_db = appConfig.Database;
_appConfig = appConfigService.AppConfig;
_db = _appConfig.Database;
appConfigService.AppConfigurationChanged += AppConfigService_AppConfigurationChanged;
LoadDbDict();
_logger.Information("Done.");
}
private void AppConfigService_AppConfigurationChanged(object sender, AppConfig appConfig)
{
_appConfig = appConfig;
try
{
LoadDbDict();
_logger.Information("Done.");
}
catch (Exception e)
{
_logger.Fatal($"程序异常: {e.Message}");
}
_db = _appConfig.Database;
}
/// <summary>
@ -414,7 +416,7 @@ namespace iFileProxy.Services
foreach (var sql in createDBSqls)
{
_logger.Information($"尝试创建 {sql.Key} 数据表....");
using MySqlCommand sqlcmd = new (sql.Value,conn);
using MySqlCommand sqlcmd = new(sql.Value, conn);
sqlcmd.ExecuteNonQuery();
_logger.Information("Done.");
}

View file

@ -18,10 +18,10 @@ namespace iFileProxy.Services
private readonly AppConfig _appConfig = AppConfig.GetCurrConfig();
private readonly static Serilog.ILogger _logger = Log.Logger.ForContext<LocalCacheManager>();
private readonly object _lock = new object();
private readonly object _lock = new ();
private readonly Timer _timer;
private readonly DatabaseGateService _dbGateService;
private readonly int CACHE_LIFETIME;
private int CACHE_LIFETIME;
/// <summary>
/// 缓存管理器
@ -31,6 +31,9 @@ namespace iFileProxy.Services
_logger.Information("Initializing LocalCacheManager.");
CACHE_LIFETIME = _appConfig.DownloadOptions.CacheLifetime;
_dbGateService = serviceProvider.GetRequiredService<DatabaseGateService>();
serviceProvider.GetRequiredService<AppConfigService>().AppConfigurationChanged += LocalCacheManager_AppConfigurationChange;
// 开始定时清理任务
_timer = new Timer((obj) =>
{
@ -42,6 +45,11 @@ namespace iFileProxy.Services
_logger.Information("succ.");
}
private void LocalCacheManager_AppConfigurationChange(object sender, AppConfig appConfig)
{
CACHE_LIFETIME = appConfig.DownloadOptions.CacheLifetime;
}
/// <summary>
/// 检查并且清理缓存数据
/// </summary>

View file

@ -61,17 +61,19 @@ namespace iFileProxy.Services
private readonly static Serilog.ILogger _logger = Log.Logger.ForContext<TaskManager>();
private readonly AppConfig? _appConfig = AppConfig.GetCurrConfig("iFileProxy.json");
private AppConfig _appConfig = AppConfig.GetCurrConfig("iFileProxy.json");
private readonly DatabaseGateService _dbGateService;
private Dictionary<string, TaskInfo> _runningTasks = [];
private Queue<TaskInfo> _pendingTasks = new();
private readonly object _taskLock = new();
public TaskManager(IServiceProvider serviceProvider)
public TaskManager(DatabaseGateService dbGateService, AppConfigService appConfigService)
{
_logger.Information("Initializing TaskManager...");
if (_appConfig != null)
_dbGateService = serviceProvider.GetRequiredService<DatabaseGateService>();
{
_dbGateService = dbGateService;
}
else
{
_logger.Fatal($"Failed to load application configuration");
@ -82,8 +84,20 @@ namespace iFileProxy.Services
TaskCompleted -= HandleTaskCompleted;
TaskCompleted += HandleTaskCompleted;
// 订阅程序配置更改事件
appConfigService.AppConfigurationChanged += AppConfigService_AppConfigurationChanged;
_logger.Information("TaskManager init succ.");
}
private void AppConfigService_AppConfigurationChanged(object sender, AppConfig appConfig)
{
_appConfig = appConfig;
}
/// <summary>
/// 添加一个新的下载任务
/// </summary>

View file

@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>