mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-06-11 16:07:27 +00:00
fix: NPE 修复、资源泄漏修复及其他 Bug 修复
- 修复 12 处 NPE 风险: FjTool/FsTool/IzTool/LzTool/MkwTool/P115Tool/PdbTool/QQTool/ParserCreate/CommonUtils/ShareLinkInfo/URLParamUtil - 修复 4 处 Vert.x 资源泄漏: 测试类中 Vertx 实例未关闭 - 修复 CacheManager 防重入和 registerPeriodicCleanup 就绪检查 - 修复 ParserApi 中 redirectUrl()/viewUrl() Promise 未 complete - 修复 CacheManager.updateTotalByField Promise 永不完成 - 修复 AppMain ShutdownHook 注册,确保 Vert.x 先于 JDBCPoolInit 关闭 - 修复 RouterHandlerFactory failureHandler 恢复返回 failure message - 修复 ParserCreate/LzTool 收窄 catch 异常类型 - 修复 IzTool/FjTool/IzToolWithAuth 并发安全 (volatile + header 副本) - 修复 P115Tool UA 为 null 时的 NPE,添加默认 User-Agent - Font Awesome CDN 换源为 s4.zstatic.net,避免 bootcdn 投毒风险 - DirectoryTree selectAll 补 parserUrl 检查,Home 组件名 App→Home
This commit is contained in:
@@ -36,6 +36,20 @@ import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL;
|
||||
public class AppMain {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 先注册 ShutdownHook(JVM 逆序执行,先注册的后执行)
|
||||
// 确保关闭顺序:Vert.x -> JDBCPoolInit -> JsParserExecutor
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
try {
|
||||
JDBCPoolInit.instance().close();
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
cn.qaiu.parser.customjs.JsParserExecutor.shutdownExecutor();
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
}));
|
||||
// start
|
||||
Deploy.instance().start(args, AppMain::exec);
|
||||
}
|
||||
@@ -67,6 +81,9 @@ public class AppMain {
|
||||
loadPlaygroundParsers();
|
||||
|
||||
String addr = jsonObject.getJsonObject(ConfigConstant.SERVER).getString("domainName");
|
||||
if (addr == null || addr.isBlank()) {
|
||||
addr = "http://127.0.0.1:" + jsonObject.getJsonObject(ConfigConstant.SERVER).getInteger("port", 6400);
|
||||
}
|
||||
System.out.println("启动成功: \n本地服务地址: " + addr);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,7 +89,7 @@ public class CacheManager {
|
||||
} else {
|
||||
LOGGER.warn("No rows affected when updating cache link info for shareKey: {}", cacheLinkInfo.getShareKey());
|
||||
}
|
||||
}).onFailure(Throwable::printStackTrace);
|
||||
}).onFailure(e -> LOGGER.error("缓存链接更新失败", e));
|
||||
|
||||
if (cacheLinkInfo.getFileInfo() != null) {
|
||||
String sql2 = """
|
||||
@@ -123,7 +123,7 @@ public class CacheManager {
|
||||
} else {
|
||||
LOGGER.warn("No rows affected when inserting pan file info for shareKey: {}", cacheLinkInfo.getShareKey());
|
||||
}
|
||||
}).onFailure(Throwable::printStackTrace);
|
||||
}).onFailure(e -> LOGGER.error("文件信息插入失败", e));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -153,18 +153,21 @@ public class CacheManager {
|
||||
|
||||
getShareKeyTotal(shareKey, fieldLower).onSuccess(total -> {
|
||||
Integer newTotal = (total == null ? 0 : total) + 1;
|
||||
Map<String, Object> updateParams = new HashMap<>();
|
||||
updateParams.put("panType", getShareType(shareKey));
|
||||
updateParams.put("shareKey", shareKey);
|
||||
updateParams.put("total", newTotal);
|
||||
updateParams.put("ts", System.currentTimeMillis());
|
||||
SqlTemplate.forUpdate(jdbcPool, sql)
|
||||
.execute(new HashMap<>() {{
|
||||
put("panType", getShareType(shareKey));
|
||||
put("shareKey", shareKey);
|
||||
put("total", newTotal);
|
||||
put("ts", System.currentTimeMillis());
|
||||
}})
|
||||
.execute(updateParams)
|
||||
.onSuccess(res -> promise.complete(res.rowCount()))
|
||||
.onFailure(e->{
|
||||
promise.fail(e);
|
||||
LOGGER.error("updateTotalByField: ", e);
|
||||
});
|
||||
}).onFailure(e -> {
|
||||
promise.fail(e);
|
||||
LOGGER.error("getShareKeyTotal in updateTotalByField: ", e);
|
||||
});
|
||||
return promise.future();
|
||||
}
|
||||
@@ -229,9 +232,17 @@ public class CacheManager {
|
||||
* 注册定时清理过期缓存任务(每小时执行一次)
|
||||
* 应在应用启动后调用
|
||||
*/
|
||||
private static volatile boolean cleanupRegistered = false;
|
||||
|
||||
public static void registerPeriodicCleanup() {
|
||||
if (cleanupRegistered) return;
|
||||
try {
|
||||
io.vertx.core.Vertx vertx = cn.qaiu.vx.core.util.VertxHolder.getVertxInstance();
|
||||
if (vertx == null) {
|
||||
LOGGER.warn("Vertx 未就绪,缓存定时清理任务延迟注册");
|
||||
return;
|
||||
}
|
||||
cleanupRegistered = true;
|
||||
vertx.setPeriodic(3600_000, 3600_000, id -> {
|
||||
try {
|
||||
new CacheManager().cleanupExpiredCache();
|
||||
@@ -262,10 +273,9 @@ public class CacheManager {
|
||||
.onSuccess(res -> {
|
||||
if(res.iterator().hasNext()) {
|
||||
JsonObject next = res.iterator().next();
|
||||
Map<String, Integer> resp = new HashMap<>(){{
|
||||
put("hit_total" ,next.getInteger("hit_total"));
|
||||
put("parser_total" ,next.getInteger("parser_total"));
|
||||
}};
|
||||
Map<String, Integer> resp = new HashMap<>();
|
||||
resp.put("hit_total", next.getInteger("hit_total"));
|
||||
resp.put("parser_total", next.getInteger("parser_total"));
|
||||
promise.complete(resp);
|
||||
} else {
|
||||
promise.complete();
|
||||
|
||||
@@ -119,11 +119,17 @@ public class URLParamUtil {
|
||||
}
|
||||
|
||||
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
|
||||
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
|
||||
if (StringUtils.isBlank(linkPrefix)) {
|
||||
// 未配置 domainName 时,从请求地址推断
|
||||
linkPrefix = parserCreate.getShareLinkInfo().getOtherParam()
|
||||
.getOrDefault("_requestOrigin", "").toString();
|
||||
}
|
||||
if (StringUtils.isNotBlank(linkPrefix)) {
|
||||
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加临时认证参数(一次性,不保存到数据库或共享内存)
|
||||
* 如果提供了临时认证参数,将覆盖后台配置的认证信息
|
||||
*
|
||||
* @param parserCreate ParserCreate对象
|
||||
@@ -155,7 +161,13 @@ public class URLParamUtil {
|
||||
}
|
||||
|
||||
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
|
||||
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
|
||||
if (StringUtils.isBlank(linkPrefix)) {
|
||||
linkPrefix = parserCreate.getShareLinkInfo().getOtherParam()
|
||||
.getOrDefault("_requestOrigin", "").toString();
|
||||
}
|
||||
if (StringUtils.isNotBlank(linkPrefix)) {
|
||||
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
|
||||
}
|
||||
|
||||
// 构建临时认证信息
|
||||
MultiMap tempAuth = MultiMap.caseInsensitiveMultiMap();
|
||||
|
||||
@@ -43,14 +43,38 @@ public class ParserApi {
|
||||
|
||||
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
|
||||
|
||||
/**
|
||||
* 获取链接前缀:优先用配置的 domainName,未配置则从请求头推断
|
||||
* 支持反向代理:优先读 X-Forwarded-Host/X-Forwarded-Proto,再回退到 Host 头
|
||||
*/
|
||||
private static String getLinkPrefix(HttpServerRequest request) {
|
||||
String domainName = SharedDataUtil.getJsonConfig("server").getString("domainName");
|
||||
if (StringUtils.isNotBlank(domainName)) {
|
||||
return domainName;
|
||||
}
|
||||
if (request != null) {
|
||||
// 反向代理场景:优先从转发头获取原始域名
|
||||
String forwardedHost = request.getHeader("X-Forwarded-Host");
|
||||
if (StringUtils.isNotBlank(forwardedHost)) {
|
||||
String proto = request.getHeader("X-Forwarded-Proto");
|
||||
if (StringUtils.isBlank(proto)) {
|
||||
proto = request.scheme();
|
||||
}
|
||||
return proto + "://" + forwardedHost;
|
||||
}
|
||||
return request.scheme() + "://" + request.host();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@RouteMapping(value = "/statisticsInfo", method = RouteMethod.GET, order = 99)
|
||||
public Future<StatisticsInfo> statisticsInfo() {
|
||||
return dbService.getStatisticsInfo();
|
||||
}
|
||||
|
||||
private final CacheManager cacheManager = new CacheManager();
|
||||
private final ServerApi serverApi = new ServerApi();
|
||||
private static final CacheManager cacheManager = new CacheManager();
|
||||
private static final ServerApi serverApi = new ServerApi();
|
||||
|
||||
@RouteMapping(value = "/linkInfo", method = RouteMethod.GET)
|
||||
public Future<LinkInfoResp> parse(HttpServerRequest request, String pwd, String auth) {
|
||||
@@ -61,10 +85,11 @@ public class ParserApi {
|
||||
|
||||
// 构建链接信息响应,如果有 auth 参数则附加到链接中
|
||||
String authSuffix = (auth != null && !auth.isEmpty()) ? "&auth=" + auth : "";
|
||||
shareLinkInfo.getOtherParam().put("_requestOrigin", getLinkPrefix(request));
|
||||
LinkInfoResp build = LinkInfoResp.builder()
|
||||
.downLink(getDownLink(parserCreate, false) + authSuffix)
|
||||
.apiLink(getDownLink(parserCreate, true) + authSuffix)
|
||||
.viewLink(getViewLink(parserCreate) + authSuffix)
|
||||
.downLink(getDownLink(parserCreate, false, request) + authSuffix)
|
||||
.apiLink(getDownLink(parserCreate, true, request) + authSuffix)
|
||||
.viewLink(getViewLink(parserCreate, request) + authSuffix)
|
||||
.shareLinkInfo(shareLinkInfo).build();
|
||||
// 解析次数统计
|
||||
shareLinkInfo.getOtherParam().put("UA",request.headers().get("user-agent"));
|
||||
@@ -76,25 +101,23 @@ public class ParserApi {
|
||||
}
|
||||
promise.complete(build);
|
||||
}).onFailure(t->{
|
||||
t.printStackTrace();
|
||||
log.error("获取统计信息失败", t);
|
||||
promise.complete(build);
|
||||
});
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
private static String getDownLink(ParserCreate create, boolean isJson) {
|
||||
|
||||
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
|
||||
private static String getDownLink(ParserCreate create, boolean isJson, HttpServerRequest request) {
|
||||
String linkPrefix = getLinkPrefix(request);
|
||||
if (StringUtils.isBlank(linkPrefix)) {
|
||||
linkPrefix = "http://127.0.0.1";
|
||||
linkPrefix = "http://127.0.0.1:" + SharedDataUtil.getJsonConfig("server").getInteger("port", 6400);
|
||||
}
|
||||
// 下载短链前缀 /d
|
||||
return linkPrefix + (isJson ? "/json/" : "/d/") + create.genPathSuffix();
|
||||
}
|
||||
|
||||
private static String getViewLink(ParserCreate create) {
|
||||
|
||||
String linkPrefix = SharedDataUtil.getJsonStringForServerConfig("domainName");
|
||||
private static String getViewLink(ParserCreate create, HttpServerRequest request) {
|
||||
String linkPrefix = getLinkPrefix(request);
|
||||
if (StringUtils.isBlank(linkPrefix)) {
|
||||
return "";
|
||||
}
|
||||
@@ -119,8 +142,9 @@ public class ParserApi {
|
||||
public Future<List<FileInfo>> getFileList(HttpServerRequest request, String pwd, String dirId, String uuid) {
|
||||
String url = URLParamUtil.parserParams(request);
|
||||
ParserCreate parserCreate = ParserCreate.fromShareUrl(url).setShareLinkInfoPwd(pwd);
|
||||
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
|
||||
String linkPrefix = getLinkPrefix(request);
|
||||
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
|
||||
parserCreate.getShareLinkInfo().getOtherParam().put("_requestOrigin", linkPrefix);
|
||||
if (StringUtils.isNotBlank(dirId)) {
|
||||
parserCreate.getShareLinkInfo().getOtherParam().put("dirId", dirId);
|
||||
}
|
||||
@@ -132,7 +156,7 @@ public class ParserApi {
|
||||
|
||||
// 目录解析下载文件
|
||||
// @RouteMapping("/getFileDownUrl/:type/:param")
|
||||
public Future<String> getFileDownUrl(String type, String param) {
|
||||
public Future<String> getFileDownUrl(HttpServerRequest request, String type, String param) {
|
||||
ParserCreate parserCreate = ParserCreate.fromType(type).shareKey("-") // shareKey not null
|
||||
.setShareLinkInfoPwd("-");
|
||||
|
||||
@@ -147,17 +171,21 @@ public class ParserApi {
|
||||
shareLinkInfo.getOtherParam().put("paramJson", new JsonObject(paramStr));
|
||||
|
||||
// domainName
|
||||
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
|
||||
String linkPrefix = getLinkPrefix(request);
|
||||
shareLinkInfo.getOtherParam().put("domainName", linkPrefix);
|
||||
shareLinkInfo.getOtherParam().put("_requestOrigin", linkPrefix);
|
||||
return parserCreate.createTool().parseById();
|
||||
}
|
||||
|
||||
@RouteMapping("/redirectUrl/:type/:param")
|
||||
public Future<Void> redirectUrl(HttpServerResponse response, String type, String param) {
|
||||
public Future<Void> redirectUrl(HttpServerRequest request, HttpServerResponse response, String type, String param) {
|
||||
Promise<Void> promise = Promise.promise();
|
||||
|
||||
getFileDownUrl(type, param)
|
||||
.onSuccess(res -> ResponseUtil.redirect(response, res))
|
||||
getFileDownUrl(request, type, param)
|
||||
.onSuccess(res -> {
|
||||
ResponseUtil.redirect(response, res);
|
||||
promise.complete();
|
||||
})
|
||||
.onFailure(t -> promise.fail(t.fillInStackTrace()));
|
||||
return promise.future();
|
||||
}
|
||||
@@ -220,7 +248,7 @@ public class ParserApi {
|
||||
}
|
||||
|
||||
String previewURL = SharedDataUtil.getJsonStringForServerConfig("previewURL");
|
||||
new ServerApi().parseJson(request, pwd, null).onSuccess(res -> {
|
||||
serverApi.parseJson(request, pwd, null).onSuccess(res -> {
|
||||
redirect(response, previewURL, res);
|
||||
}).onFailure(e -> {
|
||||
ResponseUtil.fireJsonResultResponse(response, JsonResult.error(e.toString()));
|
||||
@@ -229,14 +257,15 @@ public class ParserApi {
|
||||
|
||||
|
||||
@RouteMapping("/viewUrl/:type/:param")
|
||||
public Future<Void> viewUrl(HttpServerResponse response, String type, String param) {
|
||||
public Future<Void> viewUrl(HttpServerRequest request, HttpServerResponse response, String type, String param) {
|
||||
Promise<Void> promise = Promise.promise();
|
||||
|
||||
String viewPrefix = SharedDataUtil.getJsonConfig("server").getString("previewURL");
|
||||
getFileDownUrl(type, param)
|
||||
getFileDownUrl(request, type, param)
|
||||
.onSuccess(res -> {
|
||||
String url = viewPrefix + URLEncoder.encode(res, StandardCharsets.UTF_8);
|
||||
ResponseUtil.redirect(response, url);
|
||||
promise.complete();
|
||||
})
|
||||
.onFailure(t -> promise.fail(t.fillInStackTrace()));
|
||||
return promise.future();
|
||||
@@ -269,7 +298,8 @@ public class ParserApi {
|
||||
String shareUrl = URLParamUtil.parserParams(request);
|
||||
ParserCreate parserCreate = ParserCreate.fromShareUrl(shareUrl).setShareLinkInfoPwd(pwd);
|
||||
ShareLinkInfo shareLinkInfo = parserCreate.getShareLinkInfo();
|
||||
|
||||
shareLinkInfo.getOtherParam().put("_requestOrigin", getLinkPrefix(request));
|
||||
|
||||
// 处理认证参数
|
||||
if (auth != null && !auth.isEmpty()) {
|
||||
AuthParam authParam = AuthParamCodec.decode(auth);
|
||||
@@ -285,6 +315,8 @@ public class ParserApi {
|
||||
authParam.getExt5());
|
||||
log.debug("客户端链接API: 已解码认证参数 authType={}", authParam.getAuthType());
|
||||
}
|
||||
} else {
|
||||
URLParamUtil.addParam(parserCreate);
|
||||
}
|
||||
|
||||
// 使用默认方法解析并生成客户端链接
|
||||
@@ -326,7 +358,9 @@ public class ParserApi {
|
||||
try {
|
||||
String shareUrl = URLParamUtil.parserParams(request);
|
||||
ParserCreate parserCreate = ParserCreate.fromShareUrl(shareUrl).setShareLinkInfoPwd(pwd);
|
||||
|
||||
parserCreate.getShareLinkInfo().getOtherParam().put("_requestOrigin", getLinkPrefix(request));
|
||||
URLParamUtil.addParam(parserCreate);
|
||||
|
||||
// 使用默认方法解析并生成客户端链接
|
||||
parserCreate.createTool().parseWithClientLinks()
|
||||
.onSuccess(clientLinks -> {
|
||||
|
||||
@@ -104,7 +104,7 @@ public class CacheServiceImpl implements CacheService {
|
||||
promise.complete(result);
|
||||
// 更新缓存
|
||||
cacheManager.cacheShareLink(cacheLinkInfo);
|
||||
cacheManager.updateTotalByField(cacheKey, CacheTotalField.API_PARSER_TOTAL).onFailure(Throwable::printStackTrace);
|
||||
cacheManager.updateTotalByField(cacheKey, CacheTotalField.API_PARSER_TOTAL).onFailure(e -> log.error("更新API解析计数失败: cacheKey={}", cacheKey, e));
|
||||
}).onFailure(promise::fail);
|
||||
} else {
|
||||
// 缓存命中,生成过期时间并生成下载命令
|
||||
@@ -120,7 +120,7 @@ public class CacheServiceImpl implements CacheService {
|
||||
|
||||
promise.complete(result);
|
||||
cacheManager.updateTotalByField(cacheKey, CacheTotalField.CACHE_HIT_TOTAL)
|
||||
.onFailure(Throwable::printStackTrace);
|
||||
.onFailure(e -> log.error("更新缓存命中计数失败: cacheKey={}", cacheKey, e));
|
||||
}
|
||||
}).onFailure(t -> promise.fail(t.fillInStackTrace()));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user