From 5fd60ad9f3a440c90f5fd4f3a8d9d8a785feedc7 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 19 Nov 2024 22:51:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AC=A1=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 + README.md | 1 + src/Config/AppConfig.cs | 108 ++++++++++++ src/Controllers/iProxyController.cs | 36 ++++ src/ErrorHandler.cs | 63 +++++++ src/Helpers/DatabaseHelper.cs | 153 +++++++++++++++++ src/Helpers/FileDownloadHelper.cs | 62 +++++++ src/Helpers/MasterHelper.cs | 34 ++++ src/Models/CommonRsp.cs | 9 + src/Models/Db.cs | 37 +++++ src/Models/Task.cs | 21 +++ src/Program.cs | 54 ++++++ .../PublishProfiles/FolderProfile.pubxml | 23 +++ src/Properties/launchSettings.json | 41 +++++ src/SerilogConfig.cs | 54 ++++++ src/Services/TaskManager.cs | 154 ++++++++++++++++++ src/appsettings.Development.json | 8 + src/appsettings.json | 9 + src/iFileProxy.csproj | 18 ++ src/iFileProxy.http | 6 + src/iFileProxy.json | 35 ++++ src/iFileProxy.sln | 25 +++ 22 files changed, 956 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 src/Config/AppConfig.cs create mode 100644 src/Controllers/iProxyController.cs create mode 100644 src/ErrorHandler.cs create mode 100644 src/Helpers/DatabaseHelper.cs create mode 100644 src/Helpers/FileDownloadHelper.cs create mode 100644 src/Helpers/MasterHelper.cs create mode 100644 src/Models/CommonRsp.cs create mode 100644 src/Models/Db.cs create mode 100644 src/Models/Task.cs create mode 100644 src/Program.cs create mode 100644 src/Properties/PublishProfiles/FolderProfile.pubxml create mode 100644 src/Properties/launchSettings.json create mode 100644 src/SerilogConfig.cs create mode 100644 src/Services/TaskManager.cs create mode 100644 src/appsettings.Development.json create mode 100644 src/appsettings.json create mode 100644 src/iFileProxy.csproj create mode 100644 src/iFileProxy.http create mode 100644 src/iFileProxy.json create mode 100644 src/iFileProxy.sln diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1279cec --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +src/bin +src/.vs +src/obj +src/.config +*.user \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..67e61b5 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +### 文件代理下载工具 \ No newline at end of file diff --git a/src/Config/AppConfig.cs b/src/Config/AppConfig.cs new file mode 100644 index 0000000..26047b3 --- /dev/null +++ b/src/Config/AppConfig.cs @@ -0,0 +1,108 @@ +using Serilog; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace iFileProxy.Config +{ + public class AppConfig + { + static readonly Serilog.ILogger _logger = Log.Logger.ForContext(); + readonly static JsonSerializerOptions options = new() + { + ReadCommentHandling = JsonCommentHandling.Skip, // 允许注释 + AllowTrailingCommas = true // 允许尾随逗号 + }; + + [JsonPropertyName("Download")] + public DownloadOptions DownloadOptions { get; set; } = new(); + + [JsonPropertyName("Security")] + public SecurityOptions SecurityOptions { get; set; } = new(); + + [JsonPropertyName("Database")] + public Database Database { get; set; } + + public static AppConfig? GetCurrConfig(string configPath) + { + if (File.Exists(configPath)) + { + try + { + return JsonSerializer.Deserialize(File.ReadAllText(configPath),options); + } + catch (Exception) + { + _logger.Error("Config Parse Error!"); + throw; + } + } + else + _logger.Fatal($"Config File: {configPath} not exists!"); + return null; + } + + } + public class DownloadOptions + { + public string SavePath { get; set; } = "./proxy_tmp/"; + public uint ThreadNum { get; set; } = 1; + public uint MaxAllowedFileSize { get; set; } + public uint MaxParallelTasks { get; set; } = 4; + public string Aria2cPath { get; set; } = "./bin/aria2c"; + } + public class SecurityOptions + { + public List BlockedHost { get; set; } = []; + public List BlockedFileName { get; set; } = []; + public List BlockedClientIP { get; set; } = []; + public int DailyRequestLimitPerIP { get; set; } = -1; + } + + public partial class Database + { + [JsonPropertyName("Common")] + public Common Common { get; set; } + + [JsonPropertyName("Databases")] + public DB[] Databases { get; set; } + } + + public partial class Common + { + [JsonPropertyName("Host")] + public string Host { get; set; } + + [JsonPropertyName("Port")] + public int Port { get; set; } = 3306; + + [JsonPropertyName("User")] + public string User { get; set; } + + [JsonPropertyName("Password")] + public string Password { get; set; } + } + public partial class DB + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("Host")] + public string Host { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("Port")] + public long? Port { get; set; } = 3306; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("User")] + public string User { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("Password")] + public string Password { get; set; } + + [JsonPropertyName("DatabaseName")] + public string DatabaseName { get; set; } + + [JsonPropertyName("Description")] + public string Description { get; set; } + } +} diff --git a/src/Controllers/iProxyController.cs b/src/Controllers/iProxyController.cs new file mode 100644 index 0000000..e6617b4 --- /dev/null +++ b/src/Controllers/iProxyController.cs @@ -0,0 +1,36 @@ +using iFileProxy.Models; +using iFileProxy.Services; + +using Microsoft.AspNetCore.Mvc; + +namespace iFileProxy.Controllers +{ + public class iProxyController : ControllerBase + { + static readonly TaskManager taskManager = new (); + + [HttpPost] + [Route("/AddOfflineTask")] + public ActionResult AddOfflineTask() + { + return taskManager.AddTask(HttpContext) switch + { + TaskAddState.Success => (ActionResult)Ok(new CommonRsp() { retcode = (int)TaskAddState.Success, message = "succ" }), + TaskAddState.Fail => (ActionResult)Ok(new CommonRsp() { retcode = (int)TaskAddState.Fail, message = "unkown error!" }), + TaskAddState.ErrUrlRepeat => (ActionResult)Ok(new CommonRsp() { retcode = (int)TaskAddState.ErrUrlRepeat, message = "此url已经在任务队列中,请勿重复提交" }), + TaskAddState.ErrTaskIdRepeat => (ActionResult)Ok(new CommonRsp() { retcode = (int)TaskAddState.ErrTaskIdRepeat, message = "TaskIdRepeat!!!" }), + TaskAddState.ErrUrlInvalid => (ActionResult)Ok(new CommonRsp() { retcode = (int)TaskAddState.ErrUrlInvalid, message = "非法Url" }), + TaskAddState.ErrDbFail => (ActionResult)Ok(new CommonRsp() { retcode = (int)TaskAddState.ErrDbFail, message = "数据库数据提交失败!" }), + _ => (ActionResult)Ok(new CommonRsp() { retcode = (int)TaskAddState.Success, message = "succ default" }), + }; + } + + [HttpPost] + [HttpGet] + [Route("/GetMyTasks")] + public ActionResult GetMyTasks() + { + return Ok(new CommonRsp() { retcode = 0, message = "succ" }); + } + } +} diff --git a/src/ErrorHandler.cs b/src/ErrorHandler.cs new file mode 100644 index 0000000..f8201ae --- /dev/null +++ b/src/ErrorHandler.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Components; + +namespace iFileProxy +{ + using iFileProxy.Models; + using Microsoft.AspNetCore.Http; + using System; + using System.Net; + using System.Text.Json; + using System.Threading.Tasks; + + public class ErrorHandlerMiddleware + { + private readonly RequestDelegate _next; + + public ErrorHandlerMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + if (context.Response.StatusCode == 404) + { + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(JsonSerializer.Serialize(new CommonRsp { retcode = 404, message = "this route not exists." })); + + } + } + catch (Exception ex) + { + await HandleExceptionAsync(context, ex); + } + } + + private static Task HandleExceptionAsync(HttpContext context, Exception exception) + { + var code = HttpStatusCode.InternalServerError; // 500 if unexpected + + switch (exception) + { + case NotImplementedException: + code = HttpStatusCode.NotImplemented; // 501 + break; + case UnauthorizedAccessException: + code = HttpStatusCode.Unauthorized; // 401 + break; + case ArgumentException: + code = HttpStatusCode.BadRequest; // 400 + break; + // Add more cases for different types of exceptions + } + + context.Response.ContentType = "application/json"; + context.Response.StatusCode = (int)code; + + return context.Response.WriteAsync(JsonSerializer.Serialize(new CommonRsp { retcode = 1, message = "server internal error" })); + } + } +} diff --git a/src/Helpers/DatabaseHelper.cs b/src/Helpers/DatabaseHelper.cs new file mode 100644 index 0000000..947ed69 --- /dev/null +++ b/src/Helpers/DatabaseHelper.cs @@ -0,0 +1,153 @@ +using iFileProxy.Config; +using Serilog; +using System.Data; +using System.Text.Json; +using MySql.Data.MySqlClient; +using iFileProxy.Models; + +namespace iFileProxy.Helpers +{ + public class DatabaseHelper + { + Database _db; + AppConfig _appConfig; + private readonly static Serilog.ILogger _logger = Log.Logger.ForContext(); + + Dictionary _dbDictionary = new Dictionary(); + + public DatabaseHelper(AppConfig appConfig) + { + _logger.Information("Initializing DatabaseHelper..."); + _db = appConfig.Database; + _appConfig = appConfig; + try + { + _logger.Information("Done."); + } + catch (Exception e) + { + _logger.Fatal($"程序异常: {e.Message}"); + } + LoadDbDict(); + } + + /// + /// 加载数据库描述字典 + /// + public void LoadDbDict() + { + foreach (DB item in _db.Databases) + { + _dbDictionary.Add(item.Description, item); + _logger.Debug($"Db Config: {item.Description} <=> {item.DatabaseName} Loaded."); + } + } + /// + /// 获取一个指定数据库的连接 + /// + /// 数据库描述字段 对应AppConfig的description字段 + /// + /// 若某些不允许为空的字段出现空值 则抛出此异常 + /// + public MySqlConnection GetDBConn(string db_desc) + { + if (!_dbDictionary.TryGetValue(db_desc, out DB Db)) + { + throw new Exception($"未找到与 {db_desc} 相匹配的数据库配置"); + } + + var db_user = Db.User ?? _db.Common.User; + var db_password = Db.Password ?? _db.Common.Password; + var db_host = Db.Host ?? _db.Common.Host; + var db_port = Db.Port ?? _db.Common.Port; + + if (db_user == null || db_password == null || db_host == null || db_port == null) + throw new NoNullAllowedException("数据库配置获取失败,不允许为空的字段出现空值"); + + string db_connstr = $"server={db_host};user={db_user};database={Db.DatabaseName};port={db_port};password={db_password};Pooling=true;MaximumPoolSize=500;"; + MySqlConnection conn; + try + { + conn = new MySqlConnection(db_connstr); + conn.Open(); + } + catch (Exception ex) + { + _logger.Fatal($"获取Mysql连接时出现异常:{ex.Message}"); + throw; + } + return conn; + } + /// + /// 获取一个json格式的数据表 + /// + /// + /// + /// + public static string GetTableData(string sql, MySqlConnection conn) + { + DataTable dataTable = new DataTable(); + + using (MySqlCommand queryAllUser = new MySqlCommand(sql, conn)) + { + using (MySqlDataAdapter adapter = new MySqlDataAdapter(queryAllUser)) + adapter.Fill(dataTable); + } + return JsonSerializer.Serialize(dataTable); + + } + + public bool InsertTaskData(TaskInfo taskInfo) + { + _logger.Debug(JsonSerializer.Serialize(taskInfo)); + string sql = "INSERT INTO `t_tasks_info` (`tid`, `file_name`, `client_ip`, `add_time`, `update_time`, `status`, `url`, `size`, `hash`) " + + "VALUES (@tid, @file_name, @client_ip, @add_time, @update_time, @status, @url, @size, @hash)"; + MySqlConnection conn = GetDBConn("iFileProxy_Db"); + + try + { + using MySqlCommand sqlCmd = new MySqlCommand(sql, conn); + sqlCmd.Parameters.AddWithValue("@tid", taskInfo.TaskId); + sqlCmd.Parameters.AddWithValue("@file_name", taskInfo.FileName); + sqlCmd.Parameters.AddWithValue("@client_ip", taskInfo.ClientIp); + sqlCmd.Parameters.AddWithValue("@add_time", taskInfo.AddTime.ToString("yyyy-MM-dd HH:mm:ss")); + sqlCmd.Parameters.AddWithValue("@update_time", taskInfo.UpdateTime.ToString("yyyy-MM-dd HH:mm:ss")); + sqlCmd.Parameters.AddWithValue("@status", taskInfo.Status); + sqlCmd.Parameters.AddWithValue("@url", taskInfo.Url); + sqlCmd.Parameters.AddWithValue("@size", taskInfo.Size); + sqlCmd.Parameters.AddWithValue("@hash", taskInfo.Hash); + + sqlCmd.ExecuteNonQuery(); + } + catch (Exception) + { + _logger.Fatal($"插入数据时出现问题"); + throw; + } + finally + { + conn.Close(); + } + return true; + + + } + + public void TryInitialDB() + { + string sql = "ALTER TABLE `t_region_config` ADD COLUMN `stop_server_info_str` varchar(255) CHARACTER SET utf8mb4 NOT NULL AFTER `stop_server_config_str`"; + MySqlConnection conn = GetDBConn("deploy_config"); + + try + { + using MySqlCommand cmd = new(sql, conn); + cmd.ExecuteNonQuery(); + } + catch { } + finally + { + conn.Close(); + } + } + } +} diff --git a/src/Helpers/FileDownloadHelper.cs b/src/Helpers/FileDownloadHelper.cs new file mode 100644 index 0000000..19efe9f --- /dev/null +++ b/src/Helpers/FileDownloadHelper.cs @@ -0,0 +1,62 @@ +using iFileProxy.Models; +using System.Net.Http.Headers; +using System.Text.Json; + +namespace iFileProxy.Helpers +{ + public class FileDownloadHelper + { + + public static DownloadFileInfo GetDownloadFileInfo(string url) + { + var fileInfo = new DownloadFileInfo(); + var _httpClient = new HttpClient(); + using (var request = new HttpRequestMessage(HttpMethod.Head, url)) + { + using (var response = _httpClient.Send(request)) + { + response.EnsureSuccessStatusCode(); + + // 获取文件大小 + if (response.Content.Headers.TryGetValues("Content-Length", out var values)) + { + if (long.TryParse(values.First(), out long fileSize)) + { + fileInfo.Size = fileSize; + } + else + fileInfo.Size = -1; + } + else + fileInfo.Size = -1; + + // 获取文件名,优先从 Content-Disposition 中提取,如果没有再从 URL 提取 + fileInfo.FileName = ExtractFileNameFromContentDisposition(response.Content.Headers.ContentDisposition) + ?? ExtractFileNameFromUrl(url); + } + } + + return fileInfo; + } + + private static string ExtractFileNameFromContentDisposition(ContentDispositionHeaderValue contentDisposition) + { + if (contentDisposition != null) + { + // 检查是否有文件名 + var fileName = contentDisposition.FileName ?? contentDisposition.FileNameStar; + return fileName; + } + return null; + } + + private static string ExtractFileNameFromUrl(string url) + { + // 从 URL 中提取文件名,通常是 URL 路径的最后一部分 + Uri uri = new Uri(url); + string fileName = Path.GetFileName(uri.LocalPath); + return fileName; + } + + } +} diff --git a/src/Helpers/MasterHelper.cs b/src/Helpers/MasterHelper.cs new file mode 100644 index 0000000..af16956 --- /dev/null +++ b/src/Helpers/MasterHelper.cs @@ -0,0 +1,34 @@ +using System.Net.Http; +using System.Text.RegularExpressions; + +namespace iFileProxy.Helpers +{ + public class MasterHelper + { + /// + /// 检测链接是否为合法的网址格式 + /// + /// 待检测的链接 + /// + public static bool CheckUrlIsValid(string? uri) + { + try + { + if (string.IsNullOrWhiteSpace(uri)) + return false; + + var regex = @"(http://)?([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?"; + Regex re = new Regex(regex); + return re.IsMatch(uri); + } + catch (Exception e) + { + Console.WriteLine(e); + } + return false; + } + + + + } +} diff --git a/src/Models/CommonRsp.cs b/src/Models/CommonRsp.cs new file mode 100644 index 0000000..24811cd --- /dev/null +++ b/src/Models/CommonRsp.cs @@ -0,0 +1,9 @@ +namespace iFileProxy.Models +{ + public class CommonRsp + { + public string message { get; set; } + public object data { get; set; } + public int retcode { get; set; } + } +} diff --git a/src/Models/Db.cs b/src/Models/Db.cs new file mode 100644 index 0000000..2c3e6ff --- /dev/null +++ b/src/Models/Db.cs @@ -0,0 +1,37 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +namespace iFileProxy.Models +{ + public class TaskInfo + { + [JsonPropertyName("id")] + public uint Id { get; set; } + + [JsonPropertyName("tid")] + public string TaskId { get; set; } + + [JsonPropertyName("file_name")] + public string FileName { get; set; } + + [JsonPropertyName("client_ip")] + public string? ClientIp { get; set; } + + [JsonPropertyName("add_time")] + public DateTime AddTime { get; set; } + + [JsonPropertyName("update_time")] + public DateTime UpdateTime { get; set; } + + [JsonPropertyName("status")] + public TaskState Status { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } + + [JsonPropertyName("size")] + public long Size { get; set; } + + [JsonPropertyName("hash")] + public string Hash { get; set; } + } +} diff --git a/src/Models/Task.cs b/src/Models/Task.cs new file mode 100644 index 0000000..01628e8 --- /dev/null +++ b/src/Models/Task.cs @@ -0,0 +1,21 @@ +namespace iFileProxy.Models +{ + public enum TaskState { + NoInit = 0, // 还未初始化 + Running = 1, // 正在进行 + Error = 2, // 任务执行时候发生错误 已经结束 + End = 3, // 任务正常结束 + } + public enum TaskAddState { + Success = 0, + Fail = 1, + ErrUrlRepeat = 2, + ErrTaskIdRepeat = 3, + ErrUrlInvalid = 4, + ErrDbFail = 5 + } + public class DownloadFileInfo { + public string FileName { get; set; } + public long Size { get; set; } + } +} \ No newline at end of file diff --git a/src/Program.cs b/src/Program.cs new file mode 100644 index 0000000..91378e3 --- /dev/null +++ b/src/Program.cs @@ -0,0 +1,54 @@ + +using Serilog; + +namespace iFileProxy +{ + public class Program + { + public static void Main(string[] args) + { + SerilogConfig.CreateLogger(); + Serilog.ILogger logger = Log.Logger.ForContext(); + + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + + builder.Services.AddControllers(); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + + builder.Host.UseSerilog(logger: Log.Logger); + + var app = builder.Build(); + + app.UseSerilogRequestLogging(options => + { + options.EnrichDiagnosticContext = (diagCtx, httpCtx) => + { + diagCtx.Set("ClientIp", httpCtx.Connection.RemoteIpAddress?.ToString()); + diagCtx.Set("contentType", httpCtx.Request.ContentType); + diagCtx.Set("queryString", httpCtx.Request.QueryString); + }; + }); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseHttpsRedirection(); + + app.UseAuthorization(); + + app.UseMiddleware(); // м + + app.MapControllers(); + + app.Run(); + } + } +} diff --git a/src/Properties/PublishProfiles/FolderProfile.pubxml b/src/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..8285a16 --- /dev/null +++ b/src/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,23 @@ + + + + + false + false + true + Release + Any CPU + FileSystem + bin\Release\net8.0\publish\ + FileSystem + <_TargetId>Folder + + net8.0 + win-x64 + e343bd8a-27ed-47e2-b50d-e3000730e65e + false + true + + \ No newline at end of file diff --git a/src/Properties/launchSettings.json b/src/Properties/launchSettings.json new file mode 100644 index 0000000..3571e54 --- /dev/null +++ b/src/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:9876", + "sslPort": 44309 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5098", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7272;http://localhost:5098", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/SerilogConfig.cs b/src/SerilogConfig.cs new file mode 100644 index 0000000..466a5c9 --- /dev/null +++ b/src/SerilogConfig.cs @@ -0,0 +1,54 @@ +namespace iFileProxy +{ + using Serilog; + using Serilog.Events; + using Serilog.Filters; + using System.Net; + + public static class SerilogConfig + { + public static void CreateLogger() + { + var filePath = Path.Combine(AppContext.BaseDirectory, $"logs/dispatch.api.log"); + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.Console( + outputTemplate: "{Timestamp:HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {ClientIp} {Message:lj} {contentType}{NewLine} {Exception}") + .WriteTo.File(filePath, + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {ClientIp} {Message:lj} {contentType} {queryString}{NewLine}{Exception}", + rollingInterval: RollingInterval.Day, + fileSizeLimitBytes: 1073741824) //1GB + .Enrich.WithProperty("node_ip", GetIpAddress()) + .CreateLogger(); + } + + public static void RefreshLogger() + { + if (Log.Logger != null) + { + Log.CloseAndFlush(); + } + CreateLogger(); + } + + private static string GetIpAddress() + { + string ipAddress = "127.0.0.1"; + IPAddress[] ips = Dns.GetHostAddresses(Dns.GetHostName()); + foreach (IPAddress ip in ips) + { + if (ip.AddressFamily.ToString().ToLower().Equals("internetwork")) + { + ipAddress = ip.ToString(); + return ipAddress; + } + } + + return ipAddress; + } + } +} diff --git a/src/Services/TaskManager.cs b/src/Services/TaskManager.cs new file mode 100644 index 0000000..986c623 --- /dev/null +++ b/src/Services/TaskManager.cs @@ -0,0 +1,154 @@ +using iFileProxy.Config; +using iFileProxy.Helpers; +using iFileProxy.Models; +using Serilog; +using System.Diagnostics; +using System.Security.Policy; + +namespace iFileProxy.Services +{ + /// + /// 下载任务管理器 + /// + public class TaskManager + { + private readonly static Serilog.ILogger _logger = Log.Logger.ForContext(); + private readonly AppConfig? _appConfig = AppConfig.GetCurrConfig("iFileProxy.json"); + private readonly DatabaseHelper _dbHelper; + private Dictionary runningTasks = []; + public TaskManager() + { + _logger.Information("Initializing TaskManager..."); + if (_appConfig != null) + _dbHelper = new DatabaseHelper(_appConfig); + else + { + _logger.Fatal($"Failed to load application configuration"); + Environment.Exit(1); + } + _logger.Information("TaskManager init succ."); + } + /// + /// 添加一个新的下载任务 + /// + /// HttpContext + /// + public TaskAddState AddTask(HttpContext c) + { + // 尝试从 X-Forwarded-For 请求头获取客户端IP地址 + string? clientIp = c.Request.Headers["X-Forwarded-For"].FirstOrDefault(); + // 如果 X-Forwarded-For 头不存在,回退到 RemoteIpAddress + if (string.IsNullOrEmpty(clientIp)) + { + clientIp = c.Connection.RemoteIpAddress?.ToString(); + } + string? t_url = c.Request.Query["url"].FirstOrDefault(); + + foreach (var t in runningTasks) + { + if (t.Value.Url == t_url) + { + return TaskAddState.ErrUrlRepeat; + } + } + + if (!MasterHelper.CheckUrlIsValid(t_url)) + return TaskAddState.ErrUrlInvalid; + + DownloadFileInfo fileInfo = FileDownloadHelper.GetDownloadFileInfo(t_url); + TaskInfo taskInfo = new() + { + Url = t_url, + TaskId = Guid.NewGuid().ToString(), + AddTime = DateTime.Now, + FileName = fileInfo.FileName, + ClientIp = clientIp, + Size = fileInfo.Size, + Status = TaskState.Running, + UpdateTime = DateTime.Now + } ; + if (_dbHelper.InsertTaskData(taskInfo)) + { + StartTask(taskInfo); + _logger.Debug("数据插入成功"); + return TaskAddState.Success; + } + else + return TaskAddState.ErrDbFail; + } + + public async void StartTask(TaskInfo task_info) + { + if (runningTasks.ContainsKey(task_info.TaskId)) + { + _logger.Error($"指定的task已经存在!!!"); + return; + } + Process aria2c = new(); + aria2c.StartInfo = new ProcessStartInfo + { + FileName = _appConfig.DownloadOptions.Aria2cPath, + WorkingDirectory = _appConfig.DownloadOptions.SavePath, + Arguments = $"-x {_appConfig.DownloadOptions.ThreadNum} -s {_appConfig.DownloadOptions.ThreadNum} {task_info.Url}", + RedirectStandardOutput = true , + RedirectStandardError = true , + RedirectStandardInput = true , + UseShellExecute = false, + Environment = { { "TaskId", task_info.TaskId } } + }; + try + { + aria2c.Start(); + aria2c.BeginOutputReadLine(); + aria2c.BeginErrorReadLine(); + aria2c.OutputDataReceived += Aria2c_OutputDataReceived; + aria2c.ErrorDataReceived += Aria2c_ErrorDataReceived; + runningTasks.Add(task_info.TaskId, task_info); + await aria2c.WaitForExitAsync(); + if (aria2c.ExitCode != 0) + _logger.Error($"task: {task_info.TaskId} 进程退出状态异常 ExitCode: {aria2c.ExitCode}"); + else + runningTasks.Remove(task_info.TaskId); + } + catch (Exception) + { + _logger.Fatal("执行下载任务时候出现致命问题"); + throw; + } + } + + private void Aria2c_ErrorDataReceived(object sender, DataReceivedEventArgs e) + { + if (e.Data == null || e.Data.Trim() == "") + return; + Process c = (Process)sender; + _logger.Error($"[TaskId: {c.StartInfo.Environment["TaskId"]}] {e.Data}"); + } + + private void Aria2c_OutputDataReceived(object sender, DataReceivedEventArgs e) + { + if (e.Data == null || e.Data.Trim() == "") + return; + Process c = (Process)sender; + _logger.Debug($"[TaskId: {c.StartInfo.Environment["TaskId"]}] {e.Data}"); + } + + //public bool DeleteTask(HttpContext c) + //{ + + //} + + //public bool UpdateTaskStatus(HttpContext c) + //{ + //} + //public List GetAllTaskInfo(HttpContext c) + //{ + + //} + + //public TaskInfo GetTaskInfo(HttpContext c) + //{ + + //} + } +} diff --git a/src/appsettings.Development.json b/src/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/appsettings.json b/src/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/iFileProxy.csproj b/src/iFileProxy.csproj new file mode 100644 index 0000000..9181fd9 --- /dev/null +++ b/src/iFileProxy.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/src/iFileProxy.http b/src/iFileProxy.http new file mode 100644 index 0000000..f7df473 --- /dev/null +++ b/src/iFileProxy.http @@ -0,0 +1,6 @@ +@iFileProxy_HostAddress = http://localhost:5098 + +GET {{iFileProxy_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/iFileProxy.json b/src/iFileProxy.json new file mode 100644 index 0000000..a502070 --- /dev/null +++ b/src/iFileProxy.json @@ -0,0 +1,35 @@ +{ + "Database": { + "Common": { + "Host": "47.243.56.137", + "Port": 3306, + "User": "iFileProxy", + "Password": "i4TwYJeEt5pRfJze" + }, + "Databases": [ + { + "DatabaseName": "iFileProxy", + "Description": "iFileProxy_Db" + } + ] + }, + "Download": { + "SavePath": "./download/", // 临时文件保存位置 + "ThreadNum": 4, // 下载线程数 + "MaxAllowedFileSize": 65536, // 允许代理的最大文件尺寸 + "MaxParallelTasks": 4, // 同一时间最大并行任务数 + "Aria2cPath": "./lib/aria2c" + }, + "Security": { + "BlockedHost": [ // 禁止代理的主机 + "github.com" + ], + "BlockedFileName": [ // 禁止代理的文件名 + "a.txt" + ], + "BlockedClientIP": [ // 禁止使用服务的客户端IP + "127.0.0.1" + ], + "DailyRequestLimitPerIP": 200 // 单个IP每日最大请求次数限制 + } +} \ No newline at end of file diff --git a/src/iFileProxy.sln b/src/iFileProxy.sln new file mode 100644 index 0000000..8f2e0ab --- /dev/null +++ b/src/iFileProxy.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35327.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iFileProxy", "iFileProxy.csproj", "{E343BD8A-27ED-47E2-B50D-E3000730E65E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E343BD8A-27ED-47E2-B50D-E3000730E65E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E343BD8A-27ED-47E2-B50D-E3000730E65E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E343BD8A-27ED-47E2-B50D-E3000730E65E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E343BD8A-27ED-47E2-B50D-E3000730E65E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B34F63CF-43DA-4E66-885E-5B935910E00E} + EndGlobalSection +EndGlobal