diff --git a/src/Config/AppConfig.cs b/src/Config/AppConfig.cs index 2536e71..c27e501 100644 --- a/src/Config/AppConfig.cs +++ b/src/Config/AppConfig.cs @@ -76,6 +76,7 @@ namespace iFileProxy.Config public uint ThreadNum { get; set; } = 1; public uint MaxAllowedFileSize { get; set; } public uint MaxParallelTasks { get; set; } = 4; + public uint MaxQueueLength {get; set; } = 60; public string Aria2cPath { get; set; } = "./bin/aria2c"; public int CacheLifetime { get; set; } = 3600; } diff --git a/src/Controllers/iProxyController.cs b/src/Controllers/iProxyController.cs index aec1844..e8766fd 100644 --- a/src/Controllers/iProxyController.cs +++ b/src/Controllers/iProxyController.cs @@ -32,6 +32,8 @@ namespace iFileProxy.Controllers TaskAddState.ErrIPForbidden => (ActionResult)Ok(new CommonRsp() { retcode = (int)TaskAddState.ErrIPForbidden, message = "请求次数超过限制!" }), TaskAddState.ErrTargetHostForbidden => (ActionResult)Ok(new CommonRsp() { retcode = (int)TaskAddState.ErrTargetHostForbidden, message = "目标主机不在服务白名单内!" }), TaskAddState.ErrGetFileInfo => (ActionResult)Ok(new CommonRsp() { retcode = (int)TaskAddState.ErrGetFileInfo, message = "目标文件信息获取失败!" }), + TaskAddState.ErrQueueLengthLimit => (ActionResult)Ok(new CommonRsp() { retcode = (int)TaskAddState.ErrQueueLengthLimit, message = "服务器任务队列已满 请稍候重试!" }), + TaskAddState.Pending => (ActionResult)Ok(new CommonRsp() { retcode = (int)TaskAddState.Pending, message = "已经添加到任务队列!" }), _ => (ActionResult)Ok(new CommonRsp() { retcode = (int)TaskAddState.Success, message = "succ default" }), }; @@ -42,7 +44,15 @@ namespace iFileProxy.Controllers [Route("/GetMyTasks")] public ActionResult GetMyTasks() { - return Ok(new CommonRsp() { retcode = 0, data = taskManager.GetTaskListByIpAddr(HttpContext),message = "succ" }); + var data = taskManager.GetTaskListByIpAddr(HttpContext); + foreach (var d in data) + { + if (d.Status == TaskState.Queuing) + { + d.QueuePosition = taskManager.GetQueuePosition(d.TaskId); + } + } + return Ok(new CommonRsp() { retcode = 0, data = data ,message = "succ" }); } [HttpGet] diff --git a/src/Helpers/MasterHelper.cs b/src/Helpers/MasterHelper.cs index 15e741b..7a95752 100644 --- a/src/Helpers/MasterHelper.cs +++ b/src/Helpers/MasterHelper.cs @@ -88,7 +88,5 @@ namespace iFileProxy.Helpers return null; } - - } } diff --git a/src/Models/Db.cs b/src/Models/Db.cs index cba23a4..fe21509 100644 --- a/src/Models/Db.cs +++ b/src/Models/Db.cs @@ -48,6 +48,10 @@ namespace iFileProxy.Models [JsonProperty("tag")] [JsonPropertyName("tag")] public string Tag { get; set; } + + [JsonProperty("queue_position")] + [JsonPropertyName("queue_position")] + public int QueuePosition { get; set; } } public class DbConfigName { diff --git a/src/Models/Task.cs b/src/Models/Task.cs index 3055a0f..2a7cacd 100644 --- a/src/Models/Task.cs +++ b/src/Models/Task.cs @@ -7,6 +7,7 @@ End = 3, // 任务正常结束 Cached = 4, // 要下载的内容已经缓存 Cleaned =5, // 内容过期已被清理 + Queuing = 6, // 正在排队 } public enum TaskAddState { Success = 0, @@ -20,7 +21,9 @@ ErrTargetHostForbidden = 8, ErrFileNameForbidden = 9, ErrIPForbidden = 10, - ErrGetFileInfo = 11 + ErrGetFileInfo = 11, + Pending = 12, + ErrQueueLengthLimit = 13, } public enum FileHashAlgorithm { diff --git a/src/SerilogConfig.cs b/src/SerilogConfig.cs index 466a5c9..f1a2f85 100644 --- a/src/SerilogConfig.cs +++ b/src/SerilogConfig.cs @@ -2,7 +2,6 @@ { using Serilog; using Serilog.Events; - using Serilog.Filters; using System.Net; public static class SerilogConfig @@ -12,7 +11,11 @@ var filePath = Path.Combine(AppContext.BaseDirectory, $"logs/dispatch.api.log"); Log.Logger = new LoggerConfiguration() +#if RELEASE + .MinimumLevel.Information() +#else .MinimumLevel.Debug() +#endif .MinimumLevel.Override("Microsoft", LogEventLevel.Information) .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) .Enrich.FromLogContext() diff --git a/src/Services/TaskManager.cs b/src/Services/TaskManager.cs index 772ca32..e9cb825 100644 --- a/src/Services/TaskManager.cs +++ b/src/Services/TaskManager.cs @@ -1,9 +1,9 @@ using iFileProxy.Config; using iFileProxy.Helpers; using iFileProxy.Models; +using Org.BouncyCastle.Asn1.Tsp; using Serilog; using System.Diagnostics; -using System.Security.Policy; using System.Text.Json; namespace iFileProxy.Services @@ -13,10 +13,48 @@ namespace iFileProxy.Services /// public class TaskManager { + // 定义事件 + public event EventHandler? TaskCompleted; + + protected virtual void OnTaskCompleted(TaskInfo taskInfo) + { + EventHandler? handler = TaskCompleted; // 创建事件的副本 + handler?.Invoke(this, taskInfo); + } + + + // 事件处理程序:任务完成后调度下一任务 + private void HandleTaskCompleted(object? sender, TaskInfo taskInfo) + { + _logger.Debug($"[TaskId: {taskInfo.TaskId}] End."); + _logger.Information($"Running Task Num: {_runningTasks.Count}"); + _logger.Information($"Queue Task Num: {_pendingTasks.Count}"); + + // 等待队列中有内容并且当前正在运行的任务小于最大并行任务 + if (_pendingTasks.Count > 0 && _runningTasks.Count < _appConfig.DownloadOptions.MaxParallelTasks) + { + lock (_taskLock) // 线程安全 + { + int add_task_num = (int)(_appConfig.DownloadOptions.MaxParallelTasks - _runningTasks.Count); + if (add_task_num > _pendingTasks.Count) + add_task_num = _pendingTasks.Count; + for (int i = 0; i < add_task_num; i++) // 运行的任务中不足最大并行数并且有正在队列的task时候添加足够多的任务 + { + TaskInfo nextTask = _pendingTasks.Dequeue(); + Task.Run(() => StartTask(nextTask)).ConfigureAwait(false); + } + } + } + } + + private readonly static Serilog.ILogger _logger = Log.Logger.ForContext(); private readonly AppConfig? _appConfig = AppConfig.GetCurrConfig("iFileProxy.json"); private readonly DatabaseHelper _dbHelper; - private Dictionary runningTasks = []; + private Dictionary _runningTasks = []; + private Queue _pendingTasks = new(); + private readonly object _taskLock = new(); + public TaskManager() { _logger.Information("Initializing TaskManager..."); @@ -27,6 +65,11 @@ namespace iFileProxy.Services _logger.Fatal($"Failed to load application configuration"); Environment.Exit(1); } + + // 绑定任务完成事件的处理程序 + TaskCompleted -= HandleTaskCompleted; + TaskCompleted += HandleTaskCompleted; + _logger.Information("TaskManager init succ."); } /// @@ -39,10 +82,18 @@ namespace iFileProxy.Services string? clientIp = MasterHelper.GetClientIPAddr(c); string? t_url = c.Request.Query["url"].FirstOrDefault() ?? c.Request.Form["url"].FirstOrDefault(); - if (_appConfig.DownloadOptions.MaxParallelTasks != 0 && runningTasks.Count >= _appConfig.DownloadOptions.MaxParallelTasks) - return TaskAddState.ErrMaxParallelTasksLimit; + bool queue_task = false; - if (runningTasks.Where(x => x.Value.Url == t_url).Any()) + // 如果当前并行任务量已经达到设定并行任务和列队上限 + if (_appConfig.DownloadOptions.MaxParallelTasks != 0 && _runningTasks.Count >= _appConfig.DownloadOptions.MaxParallelTasks) + { + if (_pendingTasks.Count >= _appConfig.DownloadOptions.MaxQueueLength) + return TaskAddState.ErrMaxParallelTasksLimit; + else + queue_task = true; + } + + if (_runningTasks.Values.Any(x => x.Url == t_url)) return TaskAddState.ErrUrlRepeat; if (!MasterHelper.CheckUrlIsValid(t_url)) @@ -73,6 +124,19 @@ namespace iFileProxy.Services UpdateTime = DateTime.Now }; + // 如果是等待中任务或者列队不是空 + if (queue_task || _pendingTasks.Count != 0) // 判断一下队列长度 防止被插队 + { + lock(_taskLock) + if (_pendingTasks.Count >= _appConfig.DownloadOptions.MaxQueueLength) + return TaskAddState.ErrQueueLengthLimit; + + _pendingTasks.Enqueue(taskInfo); // 加入等待队列 + var status = AddTaskInfoToDb(taskInfo,true); + _logger.Information($"[TaskId: {taskInfo.TaskId}] Queuing..."); + if (status != TaskAddState.Success) { return status; } + return TaskAddState.Pending; + } if (MasterHelper.CheckDownloadFileExists(taskInfo.FileName)) { @@ -85,50 +149,77 @@ namespace iFileProxy.Services taskInfo.Tag = $"REDIRECT:{r.TaskId}"; } } + + return AddTaskInfoToDb(taskInfo); + + } + + + public TaskAddState AddTaskInfoToDb(TaskInfo taskInfo, bool queuing = false) + { if (_dbHelper.InsertTaskData(taskInfo)) { - if (taskInfo.Status != TaskState.Cached) + if (!queuing) // 如果不是正在排队的任务 { - StartTask(taskInfo); - taskInfo.Status = TaskState.Running; - _dbHelper.UpdateTaskStatus(taskInfo); + if (taskInfo.Status != TaskState.Cached) + { + StartTask(taskInfo); + taskInfo.Status = TaskState.Running; + } + _logger.Debug($"[TaskId: {taskInfo.TaskId}] Add to Database Successful."); } - _logger.Debug("任务添加成功."); + else + { + taskInfo.Status = TaskState.Queuing; + } + _dbHelper.UpdateTaskStatus(taskInfo); + return TaskAddState.Success; } else return TaskAddState.ErrDbFail; } - public async void StartTask(TaskInfo taskInfo) + public async Task StartTask(TaskInfo taskInfo) { - if (runningTasks.ContainsKey(taskInfo.TaskId)) + if (_runningTasks.ContainsKey(taskInfo.TaskId)) { _logger.Error($"指定的task已经存在!!!"); return; } - Process aria2c = new(); - aria2c.StartInfo = new ProcessStartInfo + + Process aria2c = new() { - FileName = _appConfig.DownloadOptions.Aria2cPath, - WorkingDirectory = _appConfig.DownloadOptions.SavePath, - Arguments = $"-x {_appConfig.DownloadOptions.ThreadNum} -s {_appConfig.DownloadOptions.ThreadNum} {taskInfo.Url}", - RedirectStandardOutput = true, - RedirectStandardError = true, - RedirectStandardInput = true, - UseShellExecute = false, - Environment = { { "TaskId", taskInfo.TaskId } } + StartInfo = new ProcessStartInfo + { + FileName = _appConfig.DownloadOptions.Aria2cPath, + WorkingDirectory = _appConfig.DownloadOptions.SavePath, + Arguments = $"-x {_appConfig.DownloadOptions.ThreadNum} -s {_appConfig.DownloadOptions.ThreadNum} {taskInfo.Url}", + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, + UseShellExecute = false, + Environment = { { "TaskId", taskInfo.TaskId } } + } }; + try { + _runningTasks.Add(taskInfo.TaskId, taskInfo); aria2c.Start(); + if (taskInfo.Status != TaskState.Running) + { + taskInfo.Status = TaskState.Running; + _dbHelper.UpdateTaskStatus(taskInfo); + } + _logger.Information($"[TaskId: {taskInfo.TaskId}] Started."); aria2c.BeginOutputReadLine(); aria2c.BeginErrorReadLine(); aria2c.OutputDataReceived += Aria2c_OutputDataReceived; aria2c.ErrorDataReceived += Aria2c_ErrorDataReceived; - runningTasks.Add(taskInfo.TaskId, taskInfo); + await aria2c.WaitForExitAsync(); - runningTasks.Remove(taskInfo.TaskId); + _runningTasks.Remove(taskInfo.TaskId); if (aria2c.ExitCode != 0) { @@ -143,14 +234,18 @@ namespace iFileProxy.Services taskInfo.Hash = MasterHelper.GetFileHash(Path.Combine(_appConfig.DownloadOptions.SavePath, taskInfo.FileName), FileHashAlgorithm.MD5); _dbHelper.UpdateTaskHash(taskInfo); } + + // 触发任务完成事件 + OnTaskCompleted(taskInfo); } - catch (Exception) + catch (Exception ex) { - _logger.Fatal("执行下载任务时候出现致命问题"); + _logger.Fatal($"执行下载任务时候出现致命问题: {ex.Message}"); throw; } } + private void Aria2c_ErrorDataReceived(object sender, DataReceivedEventArgs e) { if (e.Data == null || e.Data.Trim() == "") @@ -179,22 +274,31 @@ namespace iFileProxy.Services return JsonSerializer.Deserialize>(_dbHelper.GetTaskInfoByTid(taskId)) ?? []; } + public Dictionary GetRunningTasks() => _runningTasks; + + public int GetQueuePosition(string taskId) + { + int position = -1; // 默认值表示未找到 + int index = 0; + + Queue tempQueue = new( _pendingTasks); + + while (tempQueue.Count > 0) + { + TaskInfo current = tempQueue.Dequeue(); + if (current.TaskId == taskId) + { + position = index; + break; + } + index++; + } + + return position; + } //public bool DeleteTask(HttpContext c) //{ //} - - //public bool UpdateTaskStatus(HttpContext c) - //{ - //} - //public List GetAllTaskInfo(HttpContext c) - //{ - - //} - - //public TaskInfo GetTaskInfo(HttpContext c) - //{ - - //} } } diff --git a/src/iFileProxy.json b/src/iFileProxy.json index 0352c1a..27e45a4 100644 --- a/src/iFileProxy.json +++ b/src/iFileProxy.json @@ -18,6 +18,7 @@ "ThreadNum": 4, // 下载线程数 "MaxAllowedFileSize": 65536, // 允许代理的最大文件尺寸 "MaxParallelTasks": 4, // 同一时间最大并行任务数 + "MaxQueueLength": 60, // 最大等待队列长度 "Aria2cPath": "./lib/aria2c", "CacheLifetime": 3600 // 缓存生命周期(秒) 超出此范围的缓存文件将被删除 }, diff --git a/src/wwwroot/index.html b/src/wwwroot/index.html index 0eb9b29..74dab1a 100644 --- a/src/wwwroot/index.html +++ b/src/wwwroot/index.html @@ -14,12 +14,13 @@
+
-
Github文件下载加速
diff --git a/src/wwwroot/query_download_task.html b/src/wwwroot/query_download_task.html index 5c09dd4..650d916 100644 --- a/src/wwwroot/query_download_task.html +++ b/src/wwwroot/query_download_task.html @@ -30,9 +30,6 @@ .table { width: 100%; - max-width: 100%; - margin-bottom: 1rem; - background-color: transparent; border-collapse: collapse; border: 1px solid #dee2e6; border-radius: 8px; @@ -41,21 +38,21 @@ .table th, .table td { - padding: 15px; + padding: 10px; vertical-align: middle; border: 1px solid #dee2e6; text-align: center; - max-width: 256px; - /* 设置最大宽度 */ - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } - .table thead th { + .table th { background-color: #007bff; color: white; font-weight: bold; + white-space: nowrap; + } + + .table td { + word-break: break-word; } .table tbody tr:nth-child(even) { @@ -66,40 +63,85 @@ background-color: #e2e6ea; } + .hidden-on-small { + display: table-cell; + } + @media (max-width: 768px) { + + .table th, + .table td { + font-size: 0.9em; + padding: 8px; + } + + .hidden-on-small { + display: none; + } + + .table td { + white-space: normal; + word-wrap: break-word; + } + + .statusData { + width: 20%; + } + } + + @media (max-width: 576px) { + + .table th, + .table td { + font-size: 0.8em; + padding: 5px; + } + .table-responsive { overflow-x: auto; } - .table th, - .table td { - padding: 10px; - font-size: 0.9em; + } + + @media (min-width: 1200px) { + .timeData { + width: 16%; } - .hidden-on-small { - display: none !important; + + + + .hashData { + width: 25%; } } + + .containerCustom { + max-width: 95% + } -
+

离线下载任务管理

+ - - - - + + + + + + @@ -107,11 +149,9 @@
文件名大小提交时间状态哈希大小提交时间结束时间状态哈希

返回主页 | 捐赠

-
- @@ -124,8 +164,9 @@ [2, "错误"], [3, "已完成"], [4, "已缓存"], - [5, "已被清理"] - ]) + [5, "已被清理"], + [6, "排队中"] + ]); data = []; $.ajax({ type: "GET", @@ -133,18 +174,18 @@ dataType: "json", success: function (response) { hideLoadingMask(); - if (response.retcode == 0) { + if (response.retcode === 0) { data = response.data; populateTable(data); - } - else + } else { alert(response.message); + } }, error(xhr, status, error) { alert(error); } - }); + function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; @@ -161,16 +202,16 @@ data.forEach(item => { const row = document.createElement('tr'); row.innerHTML = ` - ${item.status == 3 || item.status == 4 ? `${item.file_name}` : item.file_name} + ${item.status === 3 || item.status === 4 ? `${item.file_name}` : item.file_name} ${formatBytes(item.size)} ${item.add_time} - ${statusStrDict.get(item.status)} + ${item.update_time} + ${statusStrDict.get(item.status) + (item.status == 6 ? " #" + (item.queue_position + 1) : "")} ${item.hash || 'N/A'} `; tableBody.appendChild(row); }); } - diff --git a/src/wwwroot/static/css/custom/Common.css b/src/wwwroot/static/css/custom/Common.css index c2951b8..9091500 100644 --- a/src/wwwroot/static/css/custom/Common.css +++ b/src/wwwroot/static/css/custom/Common.css @@ -14,10 +14,20 @@ a { width: 100%; height: 100%; background-color: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(7px); display: flex; - justify-content: center; - align-items: center; + justify-content: center; /* 水平居中 */ + align-items: center; /* 垂直居中 */ + flex-direction: column; /* 确保垂直方向排列,加载动画在上,文字在下 */ z-index: 1000; + text-align: center; /* 文字居中 */ +} + +.loading-text { + margin-top: 10px; /* 加载动画和文字之间的间距 */ + font-size: 1.2em; + color: #333; + font-weight: bold; } .loading-spinner { @@ -31,6 +41,8 @@ a { animation: spin 2s linear infinite; } + + @keyframes spin { 0% { transform: rotate(0deg);