diff --git a/src/Config/AppConfig.cs b/src/Config/AppConfig.cs index d471578..cd19dc7 100644 --- a/src/Config/AppConfig.cs +++ b/src/Config/AppConfig.cs @@ -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(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(sr.ReadToEnd(),options); } catch (Exception e) { diff --git a/src/Controllers/StreamProxyController.cs b/src/Controllers/StreamProxyController.cs index 4ebf914..9e1290b 100644 --- a/src/Controllers/StreamProxyController.cs +++ b/src/Controllers/StreamProxyController.cs @@ -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(); + 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, diff --git a/src/Controllers/iProxyController.cs b/src/Controllers/iProxyController.cs index 06f8cbb..9e4dede 100644 --- a/src/Controllers/iProxyController.cs +++ b/src/Controllers/iProxyController.cs @@ -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(); - 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] diff --git a/src/Handlers/CmdArgsHandler.cs b/src/Handlers/CmdArgsHandler.cs index c5465a3..d38270b 100644 --- a/src/Handlers/CmdArgsHandler.cs +++ b/src/Handlers/CmdArgsHandler.cs @@ -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().TryInitialDB(); } } } diff --git a/src/Middlewares/ErrorHandlerMiddleware.cs b/src/Middlewares/ErrorHandlerMiddleware.cs index bcb9f38..fa4c83c 100644 --- a/src/Middlewares/ErrorHandlerMiddleware.cs +++ b/src/Middlewares/ErrorHandlerMiddleware.cs @@ -10,7 +10,7 @@ namespace iFileProxy.Middlewares { private readonly RequestDelegate _next = next; - private readonly static ILogger _logger = Log.Logger.ForContext(); + private readonly static Serilog.ILogger _logger = Log.Logger.ForContext(); public async Task InvokeAsync(HttpContext context) { diff --git a/src/Middlewares/IPAccessLimitMiddleware.cs b/src/Middlewares/IPAccessLimitMiddleware.cs index 31e44cf..5ccd5c5 100644 --- a/src/Middlewares/IPAccessLimitMiddleware.cs +++ b/src/Middlewares/IPAccessLimitMiddleware.cs @@ -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> IPAccessCountDict, AppConfig appConfig) + public class IPAccessLimitMiddleware(RequestDelegate next, Dictionary> IPAccessCountDict, AppConfigService appConfigService) { private readonly RequestDelegate _next = next; private readonly Dictionary> _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"; diff --git a/src/Program.cs b/src/Program.cs index 5dc9a39..e49fe04 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -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(); + + 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(); builder.Services.AddSingleton(AppConfig.GetCurrConfig()); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -79,6 +81,13 @@ namespace iFileProxy // 初始化缓存管理服务 LocalCacheManager localCacheManager = new(app.Services); + // 解析命令行参数 + CmdArgsHandler.ParseArgs(args, app.Services); + + // 开启配置热重载 + app.Services.GetRequiredService().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(); - 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")) { diff --git a/src/Services/AppConfigService.cs b/src/Services/AppConfigService.cs new file mode 100644 index 0000000..55ad2b0 --- /dev/null +++ b/src/Services/AppConfigService.cs @@ -0,0 +1,85 @@ +using iFileProxy.Config; +using iFileProxy.Helpers; +using Newtonsoft.Json; +using Serilog; +using System.Configuration; +namespace iFileProxy.Services +{ + /// + /// 应用程序配置服务,用于获取当前配置、配置热重载等 + /// + public class AppConfigService + { + public delegate void AppConfigurationChangeEventHandler(object sender, AppConfig appConfig); + /// + /// 当程序配置文件发生变化时触发此事件 + /// + public event AppConfigurationChangeEventHandler? AppConfigurationChanged; + + private readonly Serilog.ILogger _logger = Log.Logger.ForContext(); + + 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; + } + } +} diff --git a/src/Services/DatabaseGateService.cs b/src/Services/DatabaseGateService.cs index b8f2c1d..9eac600 100644 --- a/src/Services/DatabaseGateService.cs +++ b/src/Services/DatabaseGateService.cs @@ -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; } /// @@ -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."); } diff --git a/src/Services/LocalCacheManager.cs b/src/Services/LocalCacheManager.cs index c039792..e6835af 100644 --- a/src/Services/LocalCacheManager.cs +++ b/src/Services/LocalCacheManager.cs @@ -18,10 +18,10 @@ namespace iFileProxy.Services private readonly AppConfig _appConfig = AppConfig.GetCurrConfig(); private readonly static Serilog.ILogger _logger = Log.Logger.ForContext(); - 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; /// /// 缓存管理器 @@ -31,6 +31,9 @@ namespace iFileProxy.Services _logger.Information("Initializing LocalCacheManager."); CACHE_LIFETIME = _appConfig.DownloadOptions.CacheLifetime; _dbGateService = serviceProvider.GetRequiredService(); + + serviceProvider.GetRequiredService().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; + } + /// /// 检查并且清理缓存数据 /// diff --git a/src/Services/TaskManager.cs b/src/Services/TaskManager.cs index fbb2c9c..8336734 100644 --- a/src/Services/TaskManager.cs +++ b/src/Services/TaskManager.cs @@ -61,17 +61,19 @@ namespace iFileProxy.Services private readonly static Serilog.ILogger _logger = Log.Logger.ForContext(); - private readonly AppConfig? _appConfig = AppConfig.GetCurrConfig("iFileProxy.json"); + private AppConfig _appConfig = AppConfig.GetCurrConfig("iFileProxy.json"); private readonly DatabaseGateService _dbGateService; private Dictionary _runningTasks = []; private Queue _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(); + { + _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; + } + + + /// /// 添加一个新的下载任务 /// diff --git a/src/iFileProxy.csproj b/src/iFileProxy.csproj index b974f2d..9969e7d 100644 --- a/src/iFileProxy.csproj +++ b/src/iFileProxy.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + true