支持流下载 git clone代理
This commit is contained in:
parent
df3520272e
commit
e945102012
16 changed files with 602 additions and 383 deletions
77
README.md
77
README.md
|
@ -1,13 +1,66 @@
|
|||
### 文件代理下载工具
|
||||
# 项目概述
|
||||
iFileProxy 是一个功能丰富的代理下载系统,支持多种下载方式和用户管理功能。主要用于解决网络访问受限环境下的文件下载需求。
|
||||
|
||||
#### 功能列表
|
||||
- [x] 代理下载文件
|
||||
- [x] 浏览器下载文件
|
||||
- [x] 文件名黑名单
|
||||
- [x] 目标Host黑名单
|
||||
- [x] 文件大小限制
|
||||
- [x] 基于IP查询提交的任务状态
|
||||
- [x] 基于IP和路由的请求次数限制
|
||||
- [x] 任务队列
|
||||
- [x] 已经存在对应文件时候直接跳过代理下载 直接下载已经缓存的内容
|
||||
- [ ] 捐赠
|
||||
## 核心功能
|
||||
- 文件下载系统
|
||||
2.
|
||||
- 离线下载
|
||||
- 多线程下载支持
|
||||
- 任务队列管理
|
||||
- 下载进度跟踪
|
||||
- 文件缓存管理
|
||||
- 文件处理
|
||||
- 流式传输
|
||||
- 断点续传
|
||||
- 文件哈希校验
|
||||
- 自动清理过期缓存
|
||||
1. 代理功能
|
||||
- GitHub 代理
|
||||
jsDelivr CDN 加速支持
|
||||
- 仓库文件直接下载
|
||||
- Git 克隆支持
|
||||
- 通用流代理
|
||||
- HTTP/HTTPS 资源代理
|
||||
- 请求头透传
|
||||
- 流量控制
|
||||
1.- 支持
|
||||
1. 用户系统
|
||||
- 账户管理
|
||||
- 用户注册/登录
|
||||
- JWT 身份验证
|
||||
- 设备指纹验证
|
||||
- 权限控制
|
||||
- 多级权限(用户/管理员/超级管理员)
|
||||
- 基于角色的访问控制
|
||||
- 用户操作日志
|
||||
1. 安全特性
|
||||
- 访问控制
|
||||
- IP 访问限制
|
||||
- 请求频率限制
|
||||
- 黑名单系统
|
||||
- 文件大小限制
|
||||
安全防护
|
||||
HTTPS 支持
|
||||
- CORS 配置
|
||||
- 请求验证
|
||||
1. 管理功能
|
||||
- 任务管理
|
||||
- 任务创建/删除
|
||||
- 任务状态监控
|
||||
- 任务优先级调整
|
||||
- 任务队列管理
|
||||
- 系统管理
|
||||
- 用户管理
|
||||
系统配置
|
||||
- 访问统计
|
||||
- 系统日志
|
||||
|
||||
1. 技术特性
|
||||
- ASP.NET Core 8.0
|
||||
- MySQL 数据库
|
||||
- JWT 认证
|
||||
- 依赖注入
|
||||
- 中间件管道
|
||||
- 日志系统
|
||||
- 连接池
|
||||
静态文件服务
|
|
@ -14,6 +14,12 @@ namespace iFileProxy.Config
|
|||
AllowTrailingCommas = true // 允许尾随逗号
|
||||
};
|
||||
|
||||
public void Save(string configPath = "iFileProxy.json")
|
||||
{
|
||||
var config = JsonSerializer.Serialize(this);
|
||||
File.WriteAllText(configPath, config);
|
||||
}
|
||||
|
||||
[JsonPropertyName("Download")]
|
||||
public DownloadOptions DownloadOptions { get; set; } = new();
|
||||
|
||||
|
@ -23,8 +29,8 @@ namespace iFileProxy.Config
|
|||
[JsonPropertyName("Database")]
|
||||
public Database Database { get; set; }
|
||||
|
||||
[JsonPropertyName("GithubProxy")]
|
||||
public GithubProxyOptions GithubProxyOptions { get; set; } = new();
|
||||
[JsonPropertyName("StreamProxy")]
|
||||
public StreamProxyOptions StreamProxyOptions { get; set; } = new();
|
||||
|
||||
public static AppConfig GetCurrConfig(string configPath = "iFileProxy.json")
|
||||
{
|
||||
|
@ -88,6 +94,7 @@ namespace iFileProxy.Config
|
|||
public List<string> BlockedHost { get; set; } = [];
|
||||
public List<string> BlockedFileName { get; set; } = [];
|
||||
public List<string> BlockedClientIP { get; set; } = [];
|
||||
public List<string> BlockedKeyword { get; set; } = [];
|
||||
public List<string> RoutesToTrack { get; set; } = [];
|
||||
public int DailyRequestLimitPerIP { get; set; } = -1;
|
||||
public bool AllowDifferentIPsForDownload { get; set; } = true;
|
||||
|
@ -146,21 +153,11 @@ namespace iFileProxy.Config
|
|||
public string Description { get; set; }
|
||||
}
|
||||
|
||||
public class GithubProxyOptions
|
||||
public class StreamProxyOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件大小限制(字节)
|
||||
/// </summary>
|
||||
public long SizeLimit { get; set; } = 1024L * 1024L * 1024L; // 默认1GB
|
||||
|
||||
/// <summary>
|
||||
/// 黑名单列表
|
||||
/// </summary>
|
||||
public List<string> Blacklist { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用 jsDelivr 加速
|
||||
/// </summary>
|
||||
public bool EnableJsDelivr { get; set; } = false;
|
||||
public long SizeLimit { get; set; } = 1024L * 1024L * 1024L * 10; // 默认10GB
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using iFileProxy.Services;
|
||||
using iFileProxy.Models;
|
||||
|
||||
namespace iFileProxy.Controllers
|
||||
{
|
||||
[Route("[controller]")]
|
||||
[ApiController]
|
||||
public class GithubProxyController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<GithubProxyController> _logger;
|
||||
private readonly GithubProxyService _proxyService;
|
||||
|
||||
public GithubProxyController(
|
||||
ILogger<GithubProxyController> logger,
|
||||
GithubProxyService proxyService)
|
||||
{
|
||||
_logger = logger;
|
||||
_proxyService = proxyService;
|
||||
}
|
||||
|
||||
[HttpGet("{**url}")]
|
||||
public async Task<IActionResult> ProxyDownload(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. URL 格式化
|
||||
url = url.StartsWith("http") ? url : $"https://{url}";
|
||||
|
||||
// 2. 验证 URL
|
||||
var (isValid, author, repo) = _proxyService.ValidateUrl(url);
|
||||
if (!isValid || author == null || repo == null)
|
||||
{
|
||||
return BadRequest(new CommonRsp
|
||||
{
|
||||
Retcode = 1,
|
||||
Message = "Invalid GitHub URL"
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 检查黑名单
|
||||
if (_proxyService.IsBlocked(author, repo))
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
// 4. 处理URL(CDN或Raw)
|
||||
url = _proxyService.ProcessUrl(url);
|
||||
|
||||
// 5. 代理请求
|
||||
var response = await _proxyService.ProxyRequestAsync(url, Request.Headers);
|
||||
if (!response.Success)
|
||||
{
|
||||
return StatusCode(response.StatusCode, new CommonRsp
|
||||
{
|
||||
Retcode = 1,
|
||||
Message = response.Message
|
||||
});
|
||||
}
|
||||
|
||||
// 6. 设置响应头
|
||||
if (response.Headers != null)
|
||||
{
|
||||
foreach (var header in response.Headers)
|
||||
{
|
||||
Response.Headers[header.Key] = header.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 返回流式响应
|
||||
return new FileStreamResult(
|
||||
response.Stream!,
|
||||
response.ContentType ?? "application/octet-stream"
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Proxy download failed");
|
||||
return StatusCode(500, new CommonRsp
|
||||
{
|
||||
Retcode = 1,
|
||||
Message = "Internal server error"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ namespace iFileProxy.Controllers
|
|||
[Authorize(UserMask.Admin,UserMask.SuperAdmin)]
|
||||
[Route("[controller]")]
|
||||
[ApiController]
|
||||
public class ManagementController(TaskManager taskManager, DatabaseGateService dbGateService) : ControllerBase
|
||||
public class ManagementController(TaskManager taskManager, DatabaseGateService dbGateService, Dictionary<string, Dictionary<string, uint>> ipAccessLimitData) : ControllerBase
|
||||
{
|
||||
public TaskManager _taskManager = taskManager;
|
||||
public DatabaseGateService _dbGateService = dbGateService;
|
||||
|
@ -593,6 +593,12 @@ namespace iFileProxy.Controllers
|
|||
}
|
||||
}
|
||||
|
||||
[HttpGet("GetIPAccessLimitData")]
|
||||
public ActionResult<CommonRsp> GetIPAccessLimitData()
|
||||
{
|
||||
return Ok(new CommonRsp { Retcode = 0, Data = ipAccessLimitData, Message = "succ" });
|
||||
}
|
||||
|
||||
public class AddUserRequest
|
||||
{
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
|
184
src/Controllers/StreamProxyController.cs
Normal file
184
src/Controllers/StreamProxyController.cs
Normal file
|
@ -0,0 +1,184 @@
|
|||
using iFileProxy.Config;
|
||||
using iFileProxy.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Serilog;
|
||||
using iFileProxy.Helpers;
|
||||
using System.Web;
|
||||
using System.Net; // 用于 URL 解码
|
||||
|
||||
namespace iFileProxy.Controllers
|
||||
{
|
||||
[Route("/")]
|
||||
public class StreamProxyController(IHttpClientFactory httpClientFactory, AppConfig appConfig) : Controller
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
|
||||
private long SizeLimit = appConfig.StreamProxyOptions.SizeLimit;
|
||||
private readonly static Serilog.ILogger _logger = Log.Logger.ForContext<StreamProxyController>();
|
||||
|
||||
// 匹配文件代理请求
|
||||
[HttpGet("{*proxyUrl}")]
|
||||
[HttpPost("{*proxyUrl}")]
|
||||
[HttpPut("{*proxyUrl}")]
|
||||
[HttpDelete("{*proxyUrl}")]
|
||||
[HttpPatch("{*proxyUrl}")]
|
||||
public async Task<IActionResult> ProxyGitRequest(string proxyUrl)
|
||||
{
|
||||
DownloadFileInfo downloadFileInfo;
|
||||
try
|
||||
{
|
||||
foreach (var keywords in appConfig.SecurityOptions.BlockedKeyword)
|
||||
{
|
||||
if (proxyUrl.IndexOf(keywords) != -1)
|
||||
return StatusCode((int)HttpStatusCode.Forbidden, "Keyword::Forbidden");
|
||||
|
||||
}
|
||||
var t = new Uri(proxyUrl);
|
||||
downloadFileInfo = FileDownloadHelper.GetDownloadFileInfo(proxyUrl);
|
||||
if (downloadFileInfo != null)
|
||||
{
|
||||
if (appConfig.SecurityOptions.BlockedFileName.IndexOf(downloadFileInfo.FileName) != -1)
|
||||
{
|
||||
return StatusCode((int)HttpStatusCode.Forbidden, "This Filename is Blocked");
|
||||
}
|
||||
if (appConfig.SecurityOptions.BlockedHost.IndexOf(t.Host) != -1)
|
||||
{
|
||||
return StatusCode((int)HttpStatusCode.Forbidden, "Target Host is Blocked");
|
||||
}
|
||||
}
|
||||
else
|
||||
return NotFound();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(proxyUrl))
|
||||
{
|
||||
return BadRequest("URL cannot be empty.");
|
||||
}
|
||||
|
||||
// URL 解码,确保完整的 URL 被正确处理
|
||||
proxyUrl = HttpUtility.UrlDecode(proxyUrl);
|
||||
|
||||
// 如果 URL 没有带协议(http:// 或 https://),默认加上 https://
|
||||
if (!proxyUrl.StartsWith("http://") && !proxyUrl.StartsWith("https://"))
|
||||
{
|
||||
proxyUrl = "https://" + proxyUrl; // 默认为 https
|
||||
}
|
||||
|
||||
// 如果目标 host 是 github.com,强制使用 https 协议
|
||||
var targetUri = new Uri(proxyUrl);
|
||||
if (targetUri.Host == "github.com" && !proxyUrl.StartsWith("https://"))
|
||||
{
|
||||
proxyUrl = "https://" + targetUri.Host + targetUri.PathAndQuery;
|
||||
}
|
||||
|
||||
// 获取原始请求的查询字符串并添加到目标 URL 中
|
||||
string queryString = Request.QueryString.HasValue ? Request.QueryString.Value : string.Empty;
|
||||
|
||||
// 拼接完整的 URL,包括查询字符串
|
||||
string targetUrl = proxyUrl + queryString;
|
||||
|
||||
try
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
|
||||
// 创建请求头(转发客户端的请求头,除了 'Host')
|
||||
var requestHeaders = Request.Headers
|
||||
.Where(h => h.Key != "Host") // 排除 'Host' 头部
|
||||
.ToDictionary(h => h.Key, h => h.Value.ToString());
|
||||
|
||||
List<KeyValuePair<string, string>> failedHeaders = new();
|
||||
|
||||
client.DefaultRequestHeaders.Clear();
|
||||
|
||||
// 设置目标主机名
|
||||
client.DefaultRequestHeaders.Host = targetUri.Host;
|
||||
foreach (var header in requestHeaders)
|
||||
{
|
||||
// 排除反代理的转发头
|
||||
if (header.Key.StartsWith("X-Forwarded") || header.Key.StartsWith("X-Real"))
|
||||
continue;
|
||||
try
|
||||
{
|
||||
client.DefaultRequestHeaders.Add(header.Key, header.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 添加失败的头放到后面处理
|
||||
failedHeaders.Add(new(header.Key, header.Value));
|
||||
_logger.Debug($"Http头添加失败: Key: {header.Key} Value: {header.Value} {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var header in client.DefaultRequestHeaders)
|
||||
{
|
||||
_logger.Debug($"[Request Header] Key: {header.Key} Value: {string.Join(",", header.Value)}");
|
||||
}
|
||||
|
||||
// 动态获取客户端的 HTTP 请求方法
|
||||
var method = Request.Method;
|
||||
|
||||
// 根据客户端请求方法动态代理请求
|
||||
HttpRequestMessage requestMessage = new HttpRequestMessage(new HttpMethod(method), targetUrl);
|
||||
|
||||
if (method == HttpMethods.Post || method == HttpMethods.Put || method == HttpMethods.Patch)
|
||||
{
|
||||
var content = new StreamContent(Request.Body); // 仅对于 POST/PUT/PATCH 请求,转发请求体
|
||||
// 处理之前添加失败的请求头
|
||||
foreach (var header in failedHeaders)
|
||||
{
|
||||
content.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
requestMessage.Content = content;
|
||||
}
|
||||
|
||||
// 发送请求并获取响应
|
||||
using (var response = await client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead))
|
||||
{
|
||||
foreach (var header in response.Headers)
|
||||
{
|
||||
_logger.Debug($"[Response Header] Key: {header.Key} Value: {string.Join(",", header.Value)}");
|
||||
}
|
||||
|
||||
// 确保响应成功
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return StatusCode((int)response.StatusCode, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
// 获取服务器响应的文件大小
|
||||
var contentLength = response.Content.Headers.ContentLength;
|
||||
|
||||
// 如果文件超过大小限制,返回错误
|
||||
if (contentLength.HasValue && contentLength.Value > SizeLimit)
|
||||
{
|
||||
return StatusCode(413, "File is too large to download.");
|
||||
}
|
||||
|
||||
// 设置响应头:告诉浏览器这是一个文件下载
|
||||
var fileName = Path.GetFileName(new Uri(targetUrl).LocalPath); // 从 URL 获取文件名
|
||||
Response.Headers.Add("Content-Disposition", $"attachment; filename={fileName}");
|
||||
Response.ContentType = response.Content.Headers.ContentType?.ToString() ?? "application/octet-stream";
|
||||
|
||||
if (contentLength > 0)
|
||||
Response.ContentLength = contentLength;
|
||||
|
||||
// 流式转发文件
|
||||
using (var stream = await response.Content.ReadAsStreamAsync())
|
||||
{
|
||||
await stream.CopyToAsync(Response.Body);
|
||||
}
|
||||
|
||||
return new EmptyResult(); // 控制器返回空结果,响应已经在流中完成
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 捕获异常并返回错误
|
||||
return StatusCode(500, $"An error occurred while proxying the request: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -45,6 +45,7 @@ namespace iFileProxy.Controllers
|
|||
TaskAddState.ErrQueueLengthLimit => (ActionResult<CommonRsp>)Ok(new CommonRsp() { Retcode = (int)TaskAddState.ErrQueueLengthLimit, Message = "服务器任务队列已满 请稍候重试!" }),
|
||||
TaskAddState.Pending => (ActionResult<CommonRsp>)Ok(new CommonRsp() { Retcode = (int)TaskAddState.Pending, Message = "已经添加到任务队列!" }),
|
||||
TaskAddState.ErrDisabledStreamTransferOrZeroSize => (ActionResult<CommonRsp>)Ok(new CommonRsp() { Retcode = (int)TaskAddState.ErrDisabledStreamTransferOrZeroSize, Message = "禁止0大小文件或者流式传输!" }),
|
||||
TaskAddState.ErrKeywordForbidden => (ActionResult<CommonRsp>)Ok(new CommonRsp() { Retcode = (int)TaskAddState.ErrKeywordForbidden, Message = "禁止代理此url!" }),
|
||||
_ => (ActionResult<CommonRsp>)Ok(new CommonRsp() { Retcode = (int)TaskAddState.Success, Message = "succ default" }),
|
||||
};
|
||||
}
|
||||
|
@ -83,10 +84,14 @@ namespace iFileProxy.Controllers
|
|||
var d = _taskManager.GetTaskListByIpAddr(HttpContext);
|
||||
var taskInfo = _taskManager.GetTaskInfo(taskID);
|
||||
|
||||
if ((!_appConfig.SecurityOptions.AllowDifferentIPsForDownload && d.Where(x => x.TaskId == taskID).Any()) || taskInfo != null)
|
||||
if ((!_appConfig.SecurityOptions.AllowDifferentIPsForDownload && d.Where(x => x.TaskId == taskID).Any()) || taskInfo?.Count > 0)
|
||||
{
|
||||
if (_appConfig.SecurityOptions.AllowDifferentIPsForDownload)
|
||||
{
|
||||
if (taskInfo == null || taskInfo.Count == 0)
|
||||
{
|
||||
return Ok(new CommonRsp() { Message = "task not exists", Retcode = -1 });
|
||||
}
|
||||
fileName = taskInfo[0].FileName;
|
||||
}
|
||||
else
|
||||
|
|
|
@ -53,7 +53,7 @@ namespace iFileProxy.Helpers
|
|||
/// </summary>
|
||||
/// <param name="c">HttpContext</param>
|
||||
/// <returns></returns>
|
||||
public static string? GetClientIPAddr(HttpContext c)
|
||||
public static string GetClientIPAddr(HttpContext c)
|
||||
{
|
||||
// 尝试从 X-Forwarded-For 请求头获取客户端IP地址
|
||||
string? clientIp = c.Request.Headers["X-Forwarded-For"].FirstOrDefault();
|
||||
|
@ -65,7 +65,7 @@ namespace iFileProxy.Helpers
|
|||
IPAddress? ipAddress = null;
|
||||
if (IPAddress.TryParse(clientIp, out ipAddress))
|
||||
return ipAddress.ToString();
|
||||
return null;
|
||||
return "127.0.0.1";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -9,24 +9,22 @@
|
|||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
public class IPAccessLimitMiddleware
|
||||
public class IPAccessLimitMiddleware(RequestDelegate next, Dictionary<string, Dictionary<string, uint>> IPAccessCountDict, AppConfig appConfig)
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly Dictionary<string, Dictionary<string, uint>> _IPAccessCountDict;
|
||||
private readonly int _dailyRequestLimitPerIP;
|
||||
private readonly AppConfig _appConfig = AppConfig.GetCurrConfig();
|
||||
|
||||
public IPAccessLimitMiddleware(RequestDelegate next, Dictionary<string, Dictionary<string, uint>> IPAccessCountDict, int dailyRequestLimitPerIP)
|
||||
{
|
||||
_next = next;
|
||||
_IPAccessCountDict = IPAccessCountDict;
|
||||
_dailyRequestLimitPerIP = dailyRequestLimitPerIP;
|
||||
}
|
||||
private readonly RequestDelegate _next = next;
|
||||
private readonly Dictionary<string, Dictionary<string, uint>> _IPAccessCountDict = IPAccessCountDict;
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
if (appConfig.SecurityOptions.BlockedClientIP.IndexOf(MasterHelper.GetClientIPAddr(context)) != -1)
|
||||
{
|
||||
context.Response.StatusCode = 403;
|
||||
await context.Response.WriteAsJsonAsync(new CommonRsp { Retcode = 403, Message = "你的IP地址已经被管理员拉入黑名单!"});
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取需要跟踪的路由列表
|
||||
var routesToTrack = _appConfig.SecurityOptions.RoutesToTrack;
|
||||
var routesToTrack = appConfig.SecurityOptions.RoutesToTrack;
|
||||
|
||||
// 检查当前请求的路径是否在需要跟踪的路由列表中
|
||||
foreach (var p in routesToTrack)
|
||||
|
@ -52,11 +50,11 @@
|
|||
{
|
||||
if (_IPAccessCountDict[dateStr].ContainsKey(ipStr))
|
||||
{
|
||||
if (_IPAccessCountDict[dateStr][ipStr] >= _dailyRequestLimitPerIP)
|
||||
if (_IPAccessCountDict[dateStr][ipStr] >= appConfig.SecurityOptions.DailyRequestLimitPerIP)
|
||||
{
|
||||
context.Response.StatusCode = 200;
|
||||
context.Response.ContentType = "application/json";
|
||||
await context.Response.WriteAsync(JsonSerializer.Serialize(new CommonRsp { Retcode = 403, Message = "请求次数超过限制!" }));
|
||||
await context.Response.WriteAsync(JsonSerializer.Serialize(new CommonRsp { Retcode = 403, Message = "日请求次数超过限制! (GMT+8 时间00:00重置)" }));
|
||||
return;
|
||||
}
|
||||
_IPAccessCountDict[dateStr][ipStr]++;
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
namespace iFileProxy.Models
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace iFileProxy.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用Http Response
|
||||
/// </summary>
|
||||
public class CommonRsp
|
||||
{
|
||||
[JsonPropertyName("message")]
|
||||
[JsonProperty("message")]
|
||||
public string Message { get; set; } = "def msg";
|
||||
[JsonPropertyName("data")]
|
||||
[JsonProperty("data")]
|
||||
public object Data { get; set; } = null;
|
||||
[JsonPropertyName("retcode")]
|
||||
[JsonProperty("retcode")]
|
||||
public int Retcode { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,6 +105,10 @@
|
|||
/// 文件不允许0大小或者流式(动态大小)传输
|
||||
/// </summary>
|
||||
ErrDisabledStreamTransferOrZeroSize = 14,
|
||||
/// <summary>
|
||||
/// 触发关键词
|
||||
/// </summary>
|
||||
ErrKeywordForbidden = 15,
|
||||
}
|
||||
public class DownloadFileInfo {
|
||||
/// <summary>
|
||||
|
|
|
@ -43,17 +43,15 @@ namespace iFileProxy
|
|||
|
||||
builder.Host.UseSerilog(logger: Log.Logger);
|
||||
|
||||
builder.Services.AddSingleton<Dictionary<string, Dictionary<string, uint>>>(serviceProvider =>
|
||||
{
|
||||
return new Dictionary<string, Dictionary<string, uint>>();
|
||||
});
|
||||
|
||||
// 注入依赖
|
||||
builder.Services.AddSingleton(AppConfig.GetCurrConfig());
|
||||
|
||||
builder.Services.AddSingleton<DatabaseGateService>();
|
||||
|
||||
builder.Services.AddSingleton<TaskManager>();
|
||||
|
||||
builder.Services.AddSingleton<Dictionary<string, Dictionary<string, uint>>>();
|
||||
|
||||
// 添加验证服务
|
||||
builder.Services.AddScoped<AuthService>();
|
||||
|
||||
|
@ -73,11 +71,29 @@ namespace iFileProxy
|
|||
};
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
// 全局错误处理中间件
|
||||
var app = builder.Build();
|
||||
// 初始化缓存管理服务
|
||||
LocalCacheManager localCacheManager = new (app.Services);
|
||||
|
||||
// 1. 配置CORS(要在Routing之前)
|
||||
app.UseCors("AllowFrontend");
|
||||
|
||||
// 2. 配置静态文件(要在Routing之前)
|
||||
var defaultFilesOptions = new DefaultFilesOptions();
|
||||
defaultFilesOptions.DefaultFileNames.Clear();
|
||||
defaultFilesOptions.DefaultFileNames.Add("index.html");
|
||||
app.UseDefaultFiles(defaultFilesOptions);
|
||||
app.UseStaticFiles();
|
||||
|
||||
// 3. 强制使用HTTPS(要在Routing之前)
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
// 4. 错误处理(在RequestLogging之前)
|
||||
app.UseMiddleware<ErrorHandlerMiddleware>();
|
||||
|
||||
// 5. 请求日志记录(在Routing之前)
|
||||
app.UseSerilogRequestLogging(options =>
|
||||
{
|
||||
options.EnrichDiagnosticContext = (diagCtx, httpCtx) =>
|
||||
|
@ -88,44 +104,23 @@ namespace iFileProxy
|
|||
};
|
||||
});
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
// 6. 路由配置
|
||||
app.UseRouting();
|
||||
|
||||
// 检查自定义配置
|
||||
AppConfig.CheckAppConfig(app.Services);
|
||||
|
||||
// 初始化缓存管理器
|
||||
LocalCacheManager localCacheManager = new(app.Services);
|
||||
|
||||
|
||||
app.UseCors("AllowFrontend");
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
|
||||
var defaultFilesOptions = new DefaultFilesOptions();
|
||||
defaultFilesOptions.DefaultFileNames.Clear();
|
||||
defaultFilesOptions.DefaultFileNames.Add("index.html");
|
||||
|
||||
app.UseDefaultFiles(defaultFilesOptions);
|
||||
|
||||
app.UseStaticFiles();
|
||||
// 7. 限制访问(IP访问限制,通常在认证之前)
|
||||
app.UseMiddleware<IPAccessLimitMiddleware>();
|
||||
|
||||
// 8. 身份验证与授权
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseMiddleware<IPAccessLimitMiddleware>(
|
||||
app.Services.GetRequiredService<Dictionary<string, Dictionary<string, uint>>>(),
|
||||
AppConfig.GetCurrConfig().SecurityOptions.DailyRequestLimitPerIP);
|
||||
|
||||
// JWT中间件
|
||||
// 9. 配置JWT验证
|
||||
app.UseMiddleware<JwtMiddleware>();
|
||||
|
||||
// 10. 映射控制器
|
||||
app.MapControllers();
|
||||
|
||||
// 11. 启动应用
|
||||
var dbGateService = app.Services.GetRequiredService<DatabaseGateService>();
|
||||
SerilogConfig.CreateLogger(dbGateService);
|
||||
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using iFileProxy.Config;
|
||||
using iFileProxy.Models;
|
||||
|
||||
namespace iFileProxy.Services
|
||||
{
|
||||
public class GithubProxyService
|
||||
{
|
||||
private readonly ILogger<GithubProxyService> _logger;
|
||||
private readonly AppConfig _appConfig;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
private static readonly Regex[] URL_PATTERNS =
|
||||
{
|
||||
new(@"^(?:https?://)?github\.com/(?<author>.+?)/(?<repo>.+?)/(?:releases|archive)/.*$"),
|
||||
new(@"^(?:https?://)?github\.com/(?<author>.+?)/(?<repo>.+?)/(?:blob|raw)/.*$"),
|
||||
new(@"^(?:https?://)?raw\.(?:githubusercontent|github)\.com/(?<author>.+?)/(?<repo>.+?)/.+?/.+$"),
|
||||
new(@"^(?:https?://)?gist\.(?:githubusercontent|github)\.com/(?<author>.+?)/.+?/.+$")
|
||||
};
|
||||
|
||||
public GithubProxyService(
|
||||
ILogger<GithubProxyService> logger,
|
||||
AppConfig appConfig,
|
||||
HttpClient httpClient)
|
||||
{
|
||||
_logger = logger;
|
||||
_appConfig = appConfig;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public (bool isValid, string? author, string? repo) ValidateUrl(string url)
|
||||
{
|
||||
foreach (var pattern in URL_PATTERNS)
|
||||
{
|
||||
var match = pattern.Match(url);
|
||||
if (match.Success)
|
||||
{
|
||||
return (true, match.Groups["author"].Value, match.Groups["repo"].Value);
|
||||
}
|
||||
}
|
||||
return (false, null, null);
|
||||
}
|
||||
|
||||
public bool IsBlocked(string author, string repo)
|
||||
{
|
||||
return _appConfig.GithubProxyOptions.Blacklist.Contains($"{author}/{repo}") ||
|
||||
_appConfig.GithubProxyOptions.Blacklist.Contains(author);
|
||||
}
|
||||
|
||||
public string ProcessUrl(string url)
|
||||
{
|
||||
if (_appConfig.GithubProxyOptions.EnableJsDelivr && url.Contains("/blob/"))
|
||||
{
|
||||
return url.Replace("/blob/", "@")
|
||||
.Replace("github.com", "cdn.jsdelivr.net/gh");
|
||||
}
|
||||
else if (url.Contains("/blob/"))
|
||||
{
|
||||
return url.Replace("/blob/", "/raw/");
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
public async Task<ProxyResponse> ProxyRequestAsync(
|
||||
string url,
|
||||
IHeaderDictionary headers,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
|
||||
// 复制请求头
|
||||
foreach (var header in headers)
|
||||
{
|
||||
if (!header.Key.Equals("Host", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
request.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(
|
||||
request,
|
||||
HttpCompletionOption.ResponseHeadersRead,
|
||||
cancellationToken);
|
||||
|
||||
// 检查文件大小
|
||||
var contentLength = response.Content.Headers.ContentLength;
|
||||
if (contentLength.HasValue && contentLength.Value > _appConfig.GithubProxyOptions.SizeLimit)
|
||||
{
|
||||
return new ProxyResponse
|
||||
{
|
||||
Success = false,
|
||||
StatusCode = StatusCodes.Status413PayloadTooLarge,
|
||||
Message = "File too large"
|
||||
};
|
||||
}
|
||||
|
||||
return new ProxyResponse
|
||||
{
|
||||
Success = true,
|
||||
StatusCode = (int)response.StatusCode,
|
||||
Headers = response.Headers.ToDictionary(h => h.Key, h => h.Value.ToArray()),
|
||||
Stream = await response.Content.ReadAsStreamAsync(cancellationToken),
|
||||
ContentType = response.Content.Headers.ContentType?.ToString()
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Proxy request failed");
|
||||
return new ProxyResponse
|
||||
{
|
||||
Success = false,
|
||||
StatusCode = StatusCodes.Status500InternalServerError,
|
||||
Message = "Internal server error"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ProxyResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public int StatusCode { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public Dictionary<string, string[]>? Headers { get; set; }
|
||||
public Stream? Stream { get; set; }
|
||||
public string? ContentType { get; set; }
|
||||
}
|
||||
}
|
|
@ -94,6 +94,11 @@ namespace iFileProxy.Services
|
|||
string? clientIp = MasterHelper.GetClientIPAddr(c);
|
||||
string? t_url = c.Request.Query["url"].FirstOrDefault() ?? c.Request.Form["url"].FirstOrDefault();
|
||||
|
||||
foreach (var keywords in _appConfig.SecurityOptions.BlockedKeyword) {
|
||||
if (t_url.IndexOf(keywords) != -1)
|
||||
return TaskAddState.ErrKeywordForbidden;
|
||||
}
|
||||
|
||||
bool queue_task = false;
|
||||
|
||||
// 如果当前并行任务量已经达到设定并行任务和列队上限
|
||||
|
|
|
@ -10,13 +10,5 @@
|
|||
"Key": "iFileProxy-JWT-Secret-Key-2024-Very-Long-Secret-Key-For-Security",
|
||||
"Issuer": "iFileProxy",
|
||||
"Audience": "iFileProxy.Client"
|
||||
},
|
||||
"GithubProxy": {
|
||||
"SizeLimit": 1073741824, // 1GB in bytes
|
||||
"Blacklist": [
|
||||
"blockedUser1",
|
||||
"blockedUser2/blockedRepo",
|
||||
"blockedOrg/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
"Download": {
|
||||
"SavePath": "./download/", // 临时文件保存位置
|
||||
"ThreadNum": 4, // 下载线程数
|
||||
"MaxAllowedFileSize": 1000000000, // 允许代理的最大文件尺寸
|
||||
"MaxAllowedFileSize": 1000000000, // 允许代理的最大文件尺寸 单位: byte
|
||||
"MaxParallelTasks": 4, // 同一时间最大并行任务数
|
||||
"MaxQueueLength": 60, // 最大等待队列长度
|
||||
"Aria2cPath": "./lib/aria2c",
|
||||
|
@ -32,6 +32,8 @@
|
|||
"BlockedClientIP": [ // 禁止使用服务的客户端IP
|
||||
"127.0.0.1"
|
||||
],
|
||||
"BlockedKeyword": [ // 关键字黑名单 检测url是否包含
|
||||
],
|
||||
"DailyRequestLimitPerIP": 200, // 单个IP每日最大请求次数限制
|
||||
"RoutesToTrack": [ // 参加IP请求数统计的路径名单
|
||||
"/Download",
|
||||
|
@ -41,13 +43,7 @@
|
|||
"AllowStreamTransferOrZeroSize": true, // 是否允许0大小文件或者动态大小文件下载
|
||||
"EnableUserRegistration": true // 是否开放用户注册
|
||||
},
|
||||
"GithubProxy": {
|
||||
"SizeLimit": 10000000000,
|
||||
"Blacklist": [
|
||||
"blockedUser1",
|
||||
"blockedUser2/blockedRepo",
|
||||
"blockedOrg/*"
|
||||
],
|
||||
"EnableJsDelivr": false
|
||||
"StreamProxy": { // 流代理设置 支持仓库clone
|
||||
"SizeLimit": 10000000000
|
||||
}
|
||||
}
|
|
@ -4,95 +4,287 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Github文件下载加速</title>
|
||||
<!-- 引入 Bootstrap 5 CSS -->
|
||||
<link href="static/css/bootstarp/5/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||
<link href="static/css/custom/Common.css" rel="stylesheet" crossorigin="anonymous">
|
||||
<title>iFileProxy - 文件下载加速服务</title>
|
||||
<link href="static/css/bootstarp/5/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.loading-mask {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 9999;
|
||||
}
|
||||
.example-box {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.feature-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.modal-backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#redirectModal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10000;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
max-width: 90%;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="redirectModal">
|
||||
<h5>提示</h5>
|
||||
<p>发现新版本界面,是否跳转?</p>
|
||||
<div class="modal-buttons">
|
||||
<button class="btn btn-secondary" onclick="closeModal()">否</button>
|
||||
<button class="btn btn-primary" onclick="redirectToNewVersion()">是</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div id="loading-mask" style="display: none;">
|
||||
<div class="loading-spinner"></div>
|
||||
<p class="loading-text">数据提交中,请稍等...</p>
|
||||
<div class="container mt-4">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">iFileProxy</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#features">功能</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#download">下载</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#examples">示例</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/query_download_task.html">任务查询</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
</nav>
|
||||
|
||||
<!-- 下载区域 -->
|
||||
<div class="row mb-4" id="download">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card">
|
||||
<div id="from_data" class="card-body">
|
||||
<h5 class="card-title text-center">Github文件下载加速</h5>
|
||||
|
||||
<!-- URL 输入框 -->
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">文件下载</h5>
|
||||
<div class="mb-3">
|
||||
<label for="url_ipt" class="form-label">目标文件URL</label>
|
||||
<input type="text" required="required" class="form-control" id="url_ipt"
|
||||
placeholder="请输入要下载的文件链接">
|
||||
<label class="form-label">下载方式</label>
|
||||
<select class="form-select mb-3" id="downloadType">
|
||||
<option value="direct">直接代理</option>
|
||||
<option value="offline">离线下载</option>
|
||||
</select>
|
||||
<input type="text" class="form-control" id="urlInput"
|
||||
placeholder="输入文件URL或GitHub链接">
|
||||
</div>
|
||||
|
||||
<!-- 验证码 输入框 -->
|
||||
<!-- <div class="mb-3 input-group">
|
||||
<input type="text" class="form-control" id="vcode" placeholder="交易验证码">
|
||||
<button type="button" id="send_vcode_btn" class="btn btn-primary">发送验证码</button>
|
||||
</div> -->
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" id="sub_btn" class="btn btn-primary">提交</button>
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-primary" id="submitBtn">提交</button>
|
||||
</div>
|
||||
<br />
|
||||
<hr />
|
||||
<p class="more-content">运行中任务: <span id="running_count">0</span> 排队中任务: <span
|
||||
id="queuing_count">0</span></p>
|
||||
<p class="more-content"><a target="_blank" href="/query_download_task.html">查询文件下载任务状态</a> | 捐赠 | <a target="_blank" href="/delete_my_info.html">删除我的访客信息</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<p>Tips: <span id="tips">任务列表每个IP相互隔离,不必担心任务信息泄露</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能说明 -->
|
||||
<div class="row mb-4" id="features">
|
||||
<div class="col-12">
|
||||
<h4>主要功能</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5>🚀 下载加速</h5>
|
||||
<ul>
|
||||
<li>多线程下载</li>
|
||||
<li>断点续传</li>
|
||||
<li>流式传输</li>
|
||||
<li>CDN加速</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5>🛡️ 安全特性</h5>
|
||||
<ul>
|
||||
<li>IP访问限制</li>
|
||||
<li>文件大小限制</li>
|
||||
<li>黑名单机制</li>
|
||||
<li>HTTPS支持</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5>📦 特殊支持</h5>
|
||||
<ul>
|
||||
<li>GitHub仓库克隆</li>
|
||||
<li>文件缓存</li>
|
||||
<li>队列管理</li>
|
||||
<li>任务监控</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用示例 -->
|
||||
<div class="row" id="examples">
|
||||
<div class="col-12">
|
||||
<h4>使用示例</h4>
|
||||
<div class="example-box">
|
||||
<h6>GitHub文件下载</h6>
|
||||
<code>https://github.com/user/repo/blob/master/file.txt</code>
|
||||
</div>
|
||||
<div class="example-box">
|
||||
<h6>仓库克隆</h6>
|
||||
<code>https://github.com/user/repo.git</code>
|
||||
</div>
|
||||
<div class="example-box">
|
||||
<h6>通用文件下载</h6>
|
||||
<code>https://example.com/path/to/file.zip</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务器状态 -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5>服务器状态</h5>
|
||||
<p>运行中任务: <span id="runningCount">0</span> |
|
||||
排队中任务: <span id="queuingCount">0</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading遮罩 -->
|
||||
<div id="loadingMask" class="loading-mask">
|
||||
<div class="d-flex justify-content-center align-items-center h-100">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 优先加载jq一类的三方库 -->
|
||||
<script src="static/js/bootstarp/5/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||
<script src="static/js/jquery/2.1.4/jquery.min.js"></script>
|
||||
<script src="static/js/custom/Common.js"></script>
|
||||
<script src="static/js/bootstarp/5/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
console.log("document ready")
|
||||
sub_btn.addEventListener('click', function (param) {
|
||||
showLoadingMask();
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/AddOfflineTask",
|
||||
data: {
|
||||
url: url_ipt.value
|
||||
},
|
||||
dataType: "json",
|
||||
success: function (response) {
|
||||
hideLoadingMask();
|
||||
if (response.retcode == 0)
|
||||
alert("任务提交成功! 请稍后点击页面下方的 \"查询文件下载任务状态\" 超链接查询任务状态!");
|
||||
else
|
||||
alert(response.message);
|
||||
$(document).ready(function() {
|
||||
// 检查是否已经显示过弹窗
|
||||
if (!localStorage.getItem('redirectModalShown')) {
|
||||
// 显示弹窗
|
||||
$('#redirectModal').show();
|
||||
// 标记已显示
|
||||
localStorage.setItem('redirectModalShown', new Date().toISOString());
|
||||
}
|
||||
|
||||
// 加载服务器状态
|
||||
function loadServerStatus() {
|
||||
$.get("/GetServerLoad", function(response) {
|
||||
if (response.retcode === 0) {
|
||||
$("#runningCount").text(response.data.running);
|
||||
$("#queuingCount").text(response.data.queuing);
|
||||
}
|
||||
});
|
||||
});
|
||||
// 加载服务器负载信息
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: "/GetServerLoad",
|
||||
dataType: "json",
|
||||
success: function (response) {
|
||||
if (response.retcode == 0) {
|
||||
running_count.textContent = response.data.running;
|
||||
queuing_count.textContent = response.data.queuing;
|
||||
}
|
||||
}
|
||||
|
||||
// 提交下载请求
|
||||
$("#submitBtn").click(function() {
|
||||
const url = $("#urlInput").val();
|
||||
const type = $("#downloadType").val();
|
||||
|
||||
if (!url) {
|
||||
alert("请输入URL");
|
||||
return;
|
||||
}
|
||||
|
||||
$("#loadingMask").show();
|
||||
|
||||
if (type === "direct") {
|
||||
window.location.href = `/StreamProxy/${encodeURIComponent(url)}`;
|
||||
$("#loadingMask").hide();
|
||||
} else {
|
||||
$.post("/AddOfflineTask", { url: url }, function(response) {
|
||||
$("#loadingMask").hide();
|
||||
if (response.retcode === 0) {
|
||||
alert("任务已提交,请在任务查询页面查看进度");
|
||||
} else {
|
||||
alert(response.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
// 初始加载
|
||||
loadServerStatus();
|
||||
// 定期刷新状态
|
||||
setInterval(loadServerStatus, 30000);
|
||||
});
|
||||
|
||||
function closeModal() {
|
||||
$('#redirectModal').hide();
|
||||
}
|
||||
|
||||
function redirectToNewVersion() {
|
||||
window.location.href = 'https://github.linxi.info/';
|
||||
}
|
||||
|
||||
// 每24小时重置一次弹窗显示状态
|
||||
function checkAndResetModalFlag() {
|
||||
const lastShown = localStorage.getItem('redirectModalShown');
|
||||
if (lastShown) {
|
||||
const lastShownDate = new Date(lastShown);
|
||||
const now = new Date();
|
||||
const hoursDiff = (now - lastShownDate) / (1000 * 60 * 60);
|
||||
|
||||
if (hoursDiff >= 24) {
|
||||
localStorage.removeItem('redirectModalShown');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时检查是否需要重置弹窗状态
|
||||
checkAndResetModalFlag();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in a new issue