mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-06-10 23:47:29 +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:
@@ -23,12 +23,11 @@ public class RouterVerticle extends AbstractVerticle {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RouterVerticle.class);
|
||||
|
||||
private static final int port = SharedDataUtil.getValueForServerConfig("port");
|
||||
private static final Router router = new RouterHandlerFactory(
|
||||
SharedDataUtil.getJsonStringForServerConfig("contextPath")).createRouter();
|
||||
|
||||
private static final JsonObject globalConfig = SharedDataUtil.getJsonConfig("globalConfig");
|
||||
|
||||
private HttpServer server;
|
||||
private Router router;
|
||||
|
||||
static {
|
||||
LOGGER.info(JacksonConfig.class.getSimpleName() + " >> ");
|
||||
@@ -61,6 +60,8 @@ public class RouterVerticle extends AbstractVerticle {
|
||||
.setReuseAddress(true) // 允许地址重用
|
||||
.setReusePort(true); // 允许端口重用
|
||||
|
||||
router = new RouterHandlerFactory(
|
||||
SharedDataUtil.getJsonStringForServerConfig("contextPath")).createRouter();
|
||||
server = vertx.createHttpServer(options);
|
||||
|
||||
server.requestHandler(router).webSocketHandler(s->{}).listen()
|
||||
|
||||
@@ -86,7 +86,10 @@ public class ShareLinkInfo {
|
||||
// 将type和shareKey组合成一个字符串作为缓存key
|
||||
String key = type + ":" + shareKey;
|
||||
if (type.equals("p115")) {
|
||||
key += ("_" + otherParam.get("UA").toString().hashCode());
|
||||
Object ua = otherParam != null ? otherParam.get("UA") : null;
|
||||
if (ua != null) {
|
||||
key += ("_" + ua.toString().hashCode());
|
||||
}
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
@@ -81,16 +81,16 @@ public class ParserCreate {
|
||||
if (shareKey != null) {
|
||||
shareLinkInfo.setShareKey(shareKey);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
} catch (IllegalStateException | IllegalArgumentException ignored) {}
|
||||
|
||||
// 提取密码
|
||||
try {
|
||||
String pwd = matcher.group("PWD");
|
||||
if (StringUtils.isNotEmpty(pwd)) {
|
||||
shareLinkInfo.setSharePassword(pwd);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
} catch (IllegalStateException | IllegalArgumentException ignored) {}
|
||||
|
||||
// 设置标准URL
|
||||
if (customParserConfig.getStandardUrlTemplate() != null) {
|
||||
String standardUrl = customParserConfig.getStandardUrlTemplate()
|
||||
@@ -133,7 +133,7 @@ public class ParserCreate {
|
||||
shareLinkInfo.setSharePassword(pwd);
|
||||
}
|
||||
standardUrl = standardUrl.replace("{pwd}", pwd);
|
||||
} catch (Exception ignored) {}
|
||||
} catch (IllegalStateException | IllegalArgumentException ignored) {}
|
||||
|
||||
shareLinkInfo.setShareUrl(shareUrl);
|
||||
shareLinkInfo.setShareKey(shareKey);
|
||||
@@ -266,15 +266,15 @@ public class ParserCreate {
|
||||
if (shareKey != null) {
|
||||
shareLinkInfo.setShareKey(shareKey);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
} catch (IllegalStateException | IllegalArgumentException ignored) {}
|
||||
|
||||
try {
|
||||
String password = matcher.group("PWD");
|
||||
if (password != null) {
|
||||
shareLinkInfo.setSharePassword(password);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
} catch (IllegalStateException | IllegalArgumentException ignored) {}
|
||||
|
||||
// 设置标准URL(如果有模板)
|
||||
if (customConfig.getStandardUrlTemplate() != null) {
|
||||
String standardUrl = customConfig.getStandardUrlTemplate()
|
||||
|
||||
@@ -534,8 +534,8 @@ public class JsHttpClient {
|
||||
} else {
|
||||
promise.fail(result.cause());
|
||||
}
|
||||
}).onFailure(Throwable::printStackTrace);
|
||||
|
||||
}).onFailure(e -> log.error("HTTP请求失败", e));
|
||||
|
||||
// 等待响应完成(使用配置的超时时间)
|
||||
HttpResponse<Buffer> response = promise.future().toCompletionStage()
|
||||
.toCompletableFuture()
|
||||
|
||||
@@ -355,7 +355,7 @@ public class JsPlaygroundExecutor {
|
||||
*/
|
||||
public List<JsPlaygroundLogger.LogEntry> getLogs() {
|
||||
List<JsPlaygroundLogger.LogEntry> logs = playgroundLogger.getLogs();
|
||||
System.out.println("[JsPlaygroundExecutor] 获取日志,数量: " + logs.size());
|
||||
log.debug("获取日志,数量: {}", logs.size());
|
||||
return logs;
|
||||
}
|
||||
|
||||
|
||||
@@ -109,9 +109,9 @@ public class FjTool extends PanBase {
|
||||
|
||||
// String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString()
|
||||
|
||||
static String token = null;
|
||||
static String userId = null;
|
||||
public static boolean authFlag = true;
|
||||
static volatile String token = null;
|
||||
static volatile String userId = null;
|
||||
public static volatile boolean authFlag = true;
|
||||
|
||||
public FjTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
@@ -289,12 +289,14 @@ public class FjTool extends PanBase {
|
||||
JsonObject json = asJson(res2);
|
||||
if (json.getInteger("code") == 200) {
|
||||
token = json.getJsonObject("data").getString("appToken");
|
||||
header0.set("appToken", token);
|
||||
log.info("登录成功 token: {}", token);
|
||||
MultiMap h0 = MultiMap.caseInsensitiveMultiMap();
|
||||
h0.addAll(header0);
|
||||
h0.set("appToken", token);
|
||||
log.info("登录成功 token: {}...", token != null ? token.substring(0, Math.min(8, token.length())) : "null");
|
||||
client.postAbs(UriTemplate.of(TOKEN_VERIFY_URL))
|
||||
.setTemplateParam("uuid", uuid)
|
||||
.setTemplateParam("ts", tsEncode2)
|
||||
.putHeaders(header0).send().onSuccess(res -> {
|
||||
.putHeaders(h0).send().onSuccess(res -> {
|
||||
if (asJson(res).getInteger("code") == 200) {
|
||||
if (FjTool.userId == null) {
|
||||
FjTool.userId = asJson(res).getJsonObject("map").getString("userId");
|
||||
@@ -454,7 +456,10 @@ public class FjTool extends PanBase {
|
||||
// 如果参数里的目录ID不为空,则直接解析目录
|
||||
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
|
||||
if (dirId != null && !dirId.isEmpty()) {
|
||||
uuid = shareLinkInfo.getOtherParam().get("uuid").toString();
|
||||
Object uuidObj = shareLinkInfo.getOtherParam().get("uuid");
|
||||
if (uuidObj != null) {
|
||||
uuid = uuidObj.toString();
|
||||
}
|
||||
parserDir(dirId, shareId, promise0);
|
||||
return promise0.future();
|
||||
}
|
||||
@@ -495,7 +500,7 @@ public class FjTool extends PanBase {
|
||||
JsonArray list;
|
||||
try {
|
||||
JsonObject jsonObject = asJson(res);
|
||||
System.out.println(jsonObject.encodePrettily());
|
||||
log.debug("目录列表: {}", jsonObject.encodePrettily());
|
||||
list = jsonObject.getJsonArray("list");
|
||||
} catch (Exception e) {
|
||||
log.error("解析目录失败: {}", res.bodyAsString());
|
||||
@@ -576,6 +581,10 @@ public class FjTool extends PanBase {
|
||||
|
||||
// 第二次请求
|
||||
JsonObject paramJson = (JsonObject)shareLinkInfo.getOtherParam().get("paramJson");
|
||||
if (paramJson == null) {
|
||||
promise.fail("缺少 paramJson 参数");
|
||||
return promise.future();
|
||||
}
|
||||
clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL_VIP))
|
||||
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
|
||||
.setTemplateParam("uuid", paramJson.getString("uuid"))
|
||||
|
||||
@@ -389,6 +389,10 @@ public class FsTool extends PanBase {
|
||||
|
||||
try {
|
||||
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
|
||||
if (paramJson == null) {
|
||||
parsePromise.fail("缺少 paramJson 参数");
|
||||
return parsePromise.future();
|
||||
}
|
||||
String shareUrl = paramJson.getString("shareUrl");
|
||||
String objToken = paramJson.getString("objToken");
|
||||
String tenant = extractTenant(shareUrl);
|
||||
@@ -444,7 +448,7 @@ public class FsTool extends PanBase {
|
||||
if (m1.find()) {
|
||||
try {
|
||||
return URLDecoder.decode(m1.group(1).trim(), StandardCharsets.UTF_8);
|
||||
} catch (Exception ignored) {
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,7 +457,7 @@ public class FsTool extends PanBase {
|
||||
if (m2.find()) {
|
||||
try {
|
||||
return URLDecoder.decode(m2.group(1).trim(), StandardCharsets.UTF_8);
|
||||
} catch (Exception ignored) {
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,8 +89,8 @@ public class IzTool extends PanBase {
|
||||
|
||||
String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString()
|
||||
|
||||
public static String token = null;
|
||||
public static boolean authFlag = true;
|
||||
public static volatile String token = null;
|
||||
public static volatile boolean authFlag = true;
|
||||
|
||||
public Future<String> parse() {
|
||||
|
||||
@@ -101,8 +101,8 @@ public class IzTool extends PanBase {
|
||||
// 检查并输出认证状态
|
||||
if (shareLinkInfo.getOtherParam().containsKey("auths")) {
|
||||
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
|
||||
log.info("文件解析检测到认证信息: isTempAuth={}, authFlag={}, token={}",
|
||||
isTempAuth, authFlag, token != null ? "已登录(" + token.substring(0, Math.min(10, token.length())) + "...)" : "未登录");
|
||||
log.info("文件解析检测到认证信息: isTempAuth={}, authFlag={}, token={}",
|
||||
isTempAuth, authFlag, token != null ? "已登录(" + token.substring(0, Math.min(8, token.length())) + "...)" : "未登录");
|
||||
|
||||
// 如果需要认证但还没有token,先执行登录
|
||||
if ((isTempAuth || authFlag) && token == null) {
|
||||
@@ -118,7 +118,7 @@ public class IzTool extends PanBase {
|
||||
// 登录失败,继续使用免登录模式
|
||||
});
|
||||
} else if (token != null) {
|
||||
log.info("文件解析使用已有token: {}...", token.substring(0, Math.min(10, token.length())));
|
||||
log.info("文件解析使用已有token: {}...", token.substring(0, Math.min(8, token.length())));
|
||||
}
|
||||
} else {
|
||||
log.debug("文件解析无认证信息,使用免登录模式");
|
||||
@@ -247,7 +247,7 @@ public class IzTool extends PanBase {
|
||||
log.warn("登录失败: {}", failRes.getMessage());
|
||||
fail(failRes.getMessage());
|
||||
}).onSuccess(r-> {
|
||||
httpRequest.setTemplateParam("appToken", header.get("appToken"))
|
||||
httpRequest.setTemplateParam("appToken", token)
|
||||
.putHeaders(header);
|
||||
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||
});
|
||||
@@ -263,12 +263,12 @@ public class IzTool extends PanBase {
|
||||
log.warn("重新登录失败: {}", failRes.getMessage());
|
||||
fail(failRes.getMessage());
|
||||
}).onSuccess(r-> {
|
||||
httpRequest.setTemplateParam("appToken", header.get("appToken"))
|
||||
httpRequest.setTemplateParam("appToken", token)
|
||||
.putHeaders(header);
|
||||
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||
});
|
||||
} else {
|
||||
httpRequest.setTemplateParam("appToken", header.get("appToken"))
|
||||
httpRequest.setTemplateParam("appToken", token)
|
||||
.putHeaders(header);
|
||||
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||
}
|
||||
@@ -311,8 +311,7 @@ public class IzTool extends PanBase {
|
||||
JsonObject json = asJson(res2);
|
||||
if (json.getInteger("code") == 200) {
|
||||
token = json.getJsonObject("data").getString("appToken");
|
||||
header.set("appToken", token);
|
||||
log.info("登录成功 token: {}", token);
|
||||
log.info("登录成功 token: {}...", token != null ? token.substring(0, Math.min(8, token.length())) : "null");
|
||||
promise1.complete();
|
||||
} else {
|
||||
// 检查是否为临时认证
|
||||
@@ -463,7 +462,10 @@ public class IzTool extends PanBase {
|
||||
// 如果参数里的目录ID不为空,则直接解析目录
|
||||
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
|
||||
if (dirId != null && !dirId.isEmpty()) {
|
||||
uuid = shareLinkInfo.getOtherParam().get("uuid").toString();
|
||||
Object uuidObj = shareLinkInfo.getOtherParam().get("uuid");
|
||||
if (uuidObj != null) {
|
||||
uuid = uuidObj.toString();
|
||||
}
|
||||
parserDir(dirId, shareId, promise);
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@@ -88,8 +88,8 @@ public class IzToolWithAuth extends PanBase {
|
||||
|
||||
String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString()
|
||||
|
||||
public static String token = null;
|
||||
public static boolean authFlag = true;
|
||||
public static volatile String token = null;
|
||||
public static volatile boolean authFlag = true;
|
||||
|
||||
public Future<String> parse() {
|
||||
|
||||
@@ -216,7 +216,7 @@ public class IzToolWithAuth extends PanBase {
|
||||
log.warn("登录失败: {}", failRes.getMessage());
|
||||
fail(failRes.getMessage());
|
||||
}).onSuccess(r-> {
|
||||
httpRequest.setTemplateParam("appToken", header.get("appToken"))
|
||||
httpRequest.setTemplateParam("appToken", token)
|
||||
.putHeaders(header);
|
||||
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||
});
|
||||
@@ -232,12 +232,12 @@ public class IzToolWithAuth extends PanBase {
|
||||
log.warn("重新登录失败: {}", failRes.getMessage());
|
||||
fail(failRes.getMessage());
|
||||
}).onSuccess(r-> {
|
||||
httpRequest.setTemplateParam("appToken", header.get("appToken"))
|
||||
httpRequest.setTemplateParam("appToken", token)
|
||||
.putHeaders(header);
|
||||
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||
});
|
||||
} else {
|
||||
httpRequest.setTemplateParam("appToken", header.get("appToken"))
|
||||
httpRequest.setTemplateParam("appToken", token)
|
||||
.putHeaders(header);
|
||||
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||
}
|
||||
@@ -280,8 +280,7 @@ public class IzToolWithAuth extends PanBase {
|
||||
JsonObject json = asJson(res2);
|
||||
if (json.getInteger("code") == 200) {
|
||||
token = json.getJsonObject("data").getString("appToken");
|
||||
header.set("appToken", token);
|
||||
log.info("登录成功 token: {}", token);
|
||||
log.info("登录成功 token: {}...", token != null ? token.substring(0, Math.min(8, token.length())) : "null");
|
||||
promise1.complete();
|
||||
} else {
|
||||
// 检查是否为临时认证
|
||||
@@ -432,7 +431,8 @@ public class IzToolWithAuth extends PanBase {
|
||||
// 如果参数里的目录ID不为空,则直接解析目录
|
||||
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
|
||||
if (dirId != null && !dirId.isEmpty()) {
|
||||
uuid = shareLinkInfo.getOtherParam().get("uuid").toString();
|
||||
Object uuidObj = shareLinkInfo.getOtherParam().get("uuid");
|
||||
uuid = uuidObj != null ? uuidObj.toString() : null;
|
||||
parserDir(dirId, shareId, promise);
|
||||
return promise.future();
|
||||
}
|
||||
@@ -480,7 +480,7 @@ public class IzToolWithAuth extends PanBase {
|
||||
requestDirList(id, shareId, tsEncode, promise);
|
||||
})
|
||||
.onSuccess(r -> {
|
||||
log.info("目录解析登录成功,token={}, 使用 VIP 模式", token != null ? token.substring(0, 10) + "..." : "null");
|
||||
log.info("目录解析登录成功,token={}, 使用 VIP 模式", token != null ? token.substring(0, Math.min(8, token.length())) + "..." : "null");
|
||||
requestDirList(id, shareId, tsEncode, promise);
|
||||
});
|
||||
return;
|
||||
@@ -627,7 +627,7 @@ public class IzToolWithAuth extends PanBase {
|
||||
|
||||
// 如果有 token,使用 VIP 接口
|
||||
if (StringUtils.isNotBlank(appToken)) {
|
||||
log.debug("parseById 使用 VIP 接口, appToken={}", appToken.substring(0, Math.min(10, appToken.length())) + "...");
|
||||
log.debug("parseById 使用 VIP 接口, appToken={}", appToken.substring(0, Math.min(8, appToken.length())) + "...");
|
||||
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL_VIP))
|
||||
.putHeaders(header)
|
||||
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
|
||||
|
||||
@@ -14,6 +14,7 @@ import io.vertx.ext.web.client.WebClientSession;
|
||||
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
|
||||
|
||||
import javax.script.ScriptException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -107,7 +108,7 @@ public class LzTool extends PanBase {
|
||||
try {
|
||||
setFileInfo(html, shareLinkInfo);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("文件信息解析异常", e);
|
||||
}
|
||||
// 匹配iframe
|
||||
Pattern compile = Pattern.compile("src=\"(/fn\\?[a-zA-Z\\d_+/=]{16,})\"");
|
||||
@@ -175,7 +176,7 @@ public class LzTool extends PanBase {
|
||||
if (firstDot >= 0) {
|
||||
domain = host.substring(firstDot); // e.g. ".lanzoum.com"
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
} catch (MalformedURLException ignored) {}
|
||||
// 创建一个 Cookie 并放入 CookieStore
|
||||
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
|
||||
nettyCookie.setDomain(domain);
|
||||
@@ -217,7 +218,7 @@ public class LzTool extends PanBase {
|
||||
return;
|
||||
}
|
||||
Map<?, ?> signMap = (Map<?, ?>)obj.get("data");
|
||||
String url0 = obj.get("url").toString();
|
||||
String url0 = String.valueOf(obj.get("url"));
|
||||
MultiMap map = MultiMap.caseInsensitiveMultiMap();
|
||||
signMap.forEach((k, v) -> {
|
||||
map.add((String) k, v.toString());
|
||||
@@ -275,7 +276,7 @@ public class LzTool extends PanBase {
|
||||
String h = du.getHost();
|
||||
int dot = h.indexOf('.');
|
||||
if (dot >= 0) downDomain = h.substring(dot);
|
||||
} catch (Exception ignored) {}
|
||||
} catch (MalformedURLException ignored) {}
|
||||
// 创建一个 Cookie 并放入 CookieStore
|
||||
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
|
||||
nettyCookie.setDomain(downDomain);
|
||||
@@ -290,12 +291,12 @@ public class LzTool extends PanBase {
|
||||
if (location0 == null) {
|
||||
fail(downUrl + " -> 直链获取失败2, 可能分享已失效");
|
||||
} else {
|
||||
setDateAndComplate(location0);
|
||||
setDateAndComplete(location0);
|
||||
}
|
||||
}).onFailure(handleFail(downUrl));
|
||||
return;
|
||||
}
|
||||
setDateAndComplate(location);
|
||||
setDateAndComplete(location);
|
||||
})
|
||||
.onFailure(handleFail(downUrl));
|
||||
} catch (Exception e) {
|
||||
@@ -304,7 +305,7 @@ public class LzTool extends PanBase {
|
||||
}).onFailure(handleFail(url));
|
||||
}
|
||||
|
||||
private void setDateAndComplate(String location0) {
|
||||
private void setDateAndComplete(String location0) {
|
||||
// 分享时间 提取url中的时间戳格式:lanzoui.com/abc/abc/yyyy/mm/dd/
|
||||
String regex = "(\\d{4}/\\d{1,2}/\\d{1,2})";
|
||||
Matcher matcher = Pattern.compile(regex).matcher(location0);
|
||||
|
||||
@@ -29,19 +29,19 @@ public class MkwTool extends PanBase {
|
||||
clientSession.getAbs(shareUrl).send().onSuccess(result -> {
|
||||
String cookie = result.headers().get("set-cookie");
|
||||
|
||||
if (!cookie.isEmpty()) {
|
||||
if (cookie != null && !cookie.isEmpty()) {
|
||||
|
||||
String regex = "([A-Za-z0-9_]+)=([A-Za-z0-9]+)";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(cookie);
|
||||
if (matcher.find()) {
|
||||
System.out.println(matcher.group(1));
|
||||
System.out.println(matcher.group(2));
|
||||
log.debug("cookie key: {}", matcher.group(1));
|
||||
log.debug("cookie value: {}", matcher.group(2));
|
||||
|
||||
var key = matcher.group(1);
|
||||
var token = matcher.group(2);
|
||||
String sign = JsExecUtils.getKwSign(token, key);
|
||||
System.out.println(sign);
|
||||
log.debug("sign: {}", sign);
|
||||
clientSession.getAbs(UriTemplate.of(API_URL)).setTemplateParam("mid", shareLinkInfo.getShareKey())
|
||||
.putHeader("Secret", sign).send().onSuccess(res -> {
|
||||
JsonObject json = asJson(res);
|
||||
@@ -54,7 +54,7 @@ public class MkwTool extends PanBase {
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("解析失败", e);
|
||||
fail("解析失败");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -21,6 +21,8 @@ public class P115Tool extends PanBase {
|
||||
private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "share/skip_login_downurl";
|
||||
|
||||
|
||||
private static final String DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
||||
|
||||
private static final MultiMap header;
|
||||
|
||||
static {
|
||||
@@ -49,9 +51,11 @@ public class P115Tool extends PanBase {
|
||||
|
||||
public Future<String> parse() {
|
||||
// 第一次请求 获取文件信息
|
||||
Object uaObj = shareLinkInfo.getOtherParam().get("UA");
|
||||
String ua = uaObj != null ? uaObj.toString() : DEFAULT_UA;
|
||||
client.getAbs(UriTemplate.of(FIRST_REQUEST_URL))
|
||||
.putHeaders(header)
|
||||
.putHeader("User-Agent", shareLinkInfo.getOtherParam().get("UA").toString())
|
||||
.putHeader("User-Agent", ua)
|
||||
.setTemplateParam("dataKey", shareLinkInfo.getShareKey())
|
||||
.setTemplateParam("dataPwd", shareLinkInfo.getSharePassword())
|
||||
.send().onSuccess(res -> {
|
||||
@@ -68,7 +72,7 @@ public class P115Tool extends PanBase {
|
||||
// share_code={dataKey}&receive_code={dataPwd}&file_id={file_id}
|
||||
client.postAbs(SECOND_REQUEST_URL)
|
||||
.putHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
.putHeader("User-Agent", shareLinkInfo.getOtherParam().get("UA").toString())
|
||||
.putHeader("User-Agent", ua)
|
||||
.sendForm(MultiMap.caseInsensitiveMultiMap()
|
||||
.set("share_code", shareLinkInfo.getShareKey())
|
||||
.set("receive_code", shareLinkInfo.getSharePassword())
|
||||
|
||||
@@ -85,7 +85,7 @@ public class PdbTool extends PanBase implements IPanTool {
|
||||
})
|
||||
.onFailure(handleFail());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("URL编码异常", e);
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
@@ -74,9 +74,9 @@ public class QQTool extends PanBase {
|
||||
});
|
||||
|
||||
// 调试匹配的情况
|
||||
System.out.println("文件名称: " + filename);
|
||||
System.out.println("文件大小: " + filesize);
|
||||
System.out.println("文件直链: " + fileurl);
|
||||
log.debug("文件名称: {}", filename);
|
||||
log.debug("文件大小: {}", filesize);
|
||||
log.debug("文件直链: {}", fileurl);
|
||||
|
||||
// 提交
|
||||
promise.complete(fileurl.replace("\\x26", "&"));
|
||||
|
||||
@@ -33,6 +33,9 @@ public class CommonUtils {
|
||||
public static Map<String, String> getURLParams(String url) throws MalformedURLException {
|
||||
URL fullUrl = new URL(url);
|
||||
String query = fullUrl.getQuery();
|
||||
if (query == null || query.isEmpty()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
String[] params = query.split("&");
|
||||
Map<String, String> map = new HashMap<>();
|
||||
for (String param : params) {
|
||||
|
||||
@@ -8,6 +8,8 @@ import cn.qaiu.parser.customjs.JsParserExecutor;
|
||||
import cn.qaiu.WebClientVertxInit;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
@@ -22,15 +24,26 @@ import java.util.Map;
|
||||
*/
|
||||
public class BaiduPhotoParserTest {
|
||||
|
||||
private Vertx vertx;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
vertx = Vertx.vertx();
|
||||
WebClientVertxInit.init(vertx);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (vertx != null) {
|
||||
vertx.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBaiduPhotoParserRegistration() {
|
||||
// 清理注册表
|
||||
CustomParserRegistry.clear();
|
||||
|
||||
// 初始化Vertx
|
||||
Vertx vertx = Vertx.vertx();
|
||||
WebClientVertxInit.init(vertx);
|
||||
|
||||
|
||||
// 检查是否加载了百度相册解析器
|
||||
CustomParserConfig config = CustomParserRegistry.get("baidu_photo");
|
||||
assert config != null : "百度相册解析器未加载";
|
||||
@@ -44,11 +57,7 @@ public class BaiduPhotoParserTest {
|
||||
public void testBaiduPhotoFileShareExecution() {
|
||||
// 清理注册表
|
||||
CustomParserRegistry.clear();
|
||||
|
||||
// 初始化Vertx
|
||||
Vertx vertx = Vertx.vertx();
|
||||
WebClientVertxInit.init(vertx);
|
||||
|
||||
|
||||
try {
|
||||
// 创建解析器 - 测试文件分享链接
|
||||
IPanTool tool = ParserCreate.fromType("baidu_photo")
|
||||
@@ -76,11 +85,7 @@ public class BaiduPhotoParserTest {
|
||||
public void testBaiduPhotoFolderShareExecution() {
|
||||
// 清理注册表
|
||||
CustomParserRegistry.clear();
|
||||
|
||||
// 初始化Vertx
|
||||
Vertx vertx = Vertx.vertx();
|
||||
WebClientVertxInit.init(vertx);
|
||||
|
||||
|
||||
try {
|
||||
// 创建解析器 - 测试文件夹分享链接
|
||||
IPanTool tool = ParserCreate.fromType("baidu_photo")
|
||||
@@ -108,11 +113,7 @@ public class BaiduPhotoParserTest {
|
||||
public void testBaiduPhotoParserFileList() {
|
||||
// 清理注册表
|
||||
CustomParserRegistry.clear();
|
||||
|
||||
// 初始化Vertx
|
||||
Vertx vertx = Vertx.vertx();
|
||||
WebClientVertxInit.init(vertx);
|
||||
|
||||
|
||||
try {
|
||||
IPanTool tool = ParserCreate.fromType("baidu_photo")
|
||||
// 分享key PPgOEodBVE
|
||||
@@ -166,11 +167,7 @@ public class BaiduPhotoParserTest {
|
||||
public void testBaiduPhotoParserById() {
|
||||
// 清理注册表
|
||||
CustomParserRegistry.clear();
|
||||
|
||||
// 初始化Vertx
|
||||
Vertx vertx = Vertx.vertx();
|
||||
WebClientVertxInit.init(vertx);
|
||||
|
||||
|
||||
try {
|
||||
// 创建ShareLinkInfo
|
||||
Map<String, Object> otherParam = new HashMap<>();
|
||||
|
||||
@@ -7,6 +7,8 @@ import cn.qaiu.parser.custom.CustomParserRegistry;
|
||||
import cn.qaiu.parser.customjs.JsParserExecutor;
|
||||
import cn.qaiu.WebClientVertxInit;
|
||||
import io.vertx.core.Vertx;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
@@ -21,15 +23,26 @@ import java.util.Map;
|
||||
*/
|
||||
public class JsParserTest {
|
||||
|
||||
private Vertx vertx;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
vertx = Vertx.vertx();
|
||||
WebClientVertxInit.init(vertx);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (vertx != null) {
|
||||
vertx.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJsParserRegistration() {
|
||||
// 清理注册表
|
||||
CustomParserRegistry.clear();
|
||||
|
||||
// 初始化Vertx
|
||||
Vertx vertx = Vertx.vertx();
|
||||
WebClientVertxInit.init(vertx);
|
||||
|
||||
|
||||
// 检查是否加载了JavaScript解析器
|
||||
CustomParserConfig config = CustomParserRegistry.get("demo_js");
|
||||
assert config != null : "JavaScript解析器未加载";
|
||||
@@ -43,11 +56,7 @@ public class JsParserTest {
|
||||
public void testJsParserExecution() {
|
||||
// 清理注册表
|
||||
CustomParserRegistry.clear();
|
||||
|
||||
// 初始化Vertx
|
||||
Vertx vertx = Vertx.vertx();
|
||||
WebClientVertxInit.init(vertx);
|
||||
|
||||
|
||||
try {
|
||||
// 创建解析器
|
||||
IPanTool tool = ParserCreate.fromType("demo_js")
|
||||
@@ -74,11 +83,7 @@ public class JsParserTest {
|
||||
public void testJsParserFileList() {
|
||||
// 清理注册表
|
||||
CustomParserRegistry.clear();
|
||||
|
||||
// 初始化Vertx
|
||||
Vertx vertx = Vertx.vertx();
|
||||
WebClientVertxInit.init(vertx);
|
||||
|
||||
|
||||
try {
|
||||
// 创建解析器
|
||||
IPanTool tool = ParserCreate.fromType("demo_js")
|
||||
@@ -114,11 +119,7 @@ public class JsParserTest {
|
||||
public void testJsParserById() {
|
||||
// 清理注册表
|
||||
CustomParserRegistry.clear();
|
||||
|
||||
// 初始化Vertx
|
||||
Vertx vertx = Vertx.vertx();
|
||||
WebClientVertxInit.init(vertx);
|
||||
|
||||
|
||||
try {
|
||||
// 创建ShareLinkInfo
|
||||
Map<String, Object> otherParam = new HashMap<>();
|
||||
|
||||
@@ -7,6 +7,8 @@ import cn.qaiu.parser.ParserCreate;
|
||||
import cn.qaiu.parser.custom.CustomParserConfig;
|
||||
import cn.qaiu.parser.custom.CustomParserRegistry;
|
||||
import io.vertx.core.Vertx;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -16,18 +18,29 @@ import org.slf4j.LoggerFactory;
|
||||
* 测试fetch API和Promise polyfill功能
|
||||
*/
|
||||
public class JsFetchBridgeTest {
|
||||
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(JsFetchBridgeTest.class);
|
||||
|
||||
|
||||
private Vertx vertx;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
vertx = Vertx.vertx();
|
||||
WebClientVertxInit.init(vertx);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (vertx != null) {
|
||||
vertx.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFetchPolyfillLoaded() {
|
||||
// 初始化Vertx
|
||||
Vertx vertx = Vertx.vertx();
|
||||
WebClientVertxInit.init(vertx);
|
||||
|
||||
// 清理注册表
|
||||
CustomParserRegistry.clear();
|
||||
|
||||
|
||||
// 创建一个简单的解析器配置
|
||||
String jsCode = """
|
||||
// 测试Promise是否可用
|
||||
@@ -83,13 +96,9 @@ public class JsFetchBridgeTest {
|
||||
|
||||
@Test
|
||||
public void testPromiseBasicUsage() {
|
||||
// 初始化Vertx
|
||||
Vertx vertx = Vertx.vertx();
|
||||
WebClientVertxInit.init(vertx);
|
||||
|
||||
// 清理注册表
|
||||
CustomParserRegistry.clear();
|
||||
|
||||
|
||||
String jsCode = """
|
||||
function parse(shareLinkInfo, http, logger) {
|
||||
logger.info("测试Promise基本用法");
|
||||
|
||||
@@ -190,6 +190,14 @@
|
||||
>
|
||||
<i class="fas fa-paper-plane"></i> 发送到下载器
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="selectedNode.parserUrl"
|
||||
size="small"
|
||||
@click="copyDirectLink(selectedNode)"
|
||||
:loading="copyLinkLoading"
|
||||
>
|
||||
<i class="fas fa-link"></i> 复制直链
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="file-detail-empty">
|
||||
@@ -258,6 +266,14 @@
|
||||
>
|
||||
<i class="fas fa-paper-plane"></i> 发送到下载器
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="selectedNode.parserUrl"
|
||||
size="small"
|
||||
@click="copyDirectLink(selectedNode)"
|
||||
:loading="copyLinkLoading"
|
||||
>
|
||||
<i class="fas fa-link"></i> 复制直链
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="file-detail-empty">
|
||||
@@ -324,6 +340,14 @@
|
||||
>
|
||||
发送到下载器
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="selectedFile && selectedFile.parserUrl"
|
||||
@click="copyDirectLink(selectedFile)"
|
||||
style="margin-left: 8px;"
|
||||
:loading="copyLinkLoading"
|
||||
>
|
||||
复制直链
|
||||
</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<div v-if="isPreviewing" class="preview-mask">
|
||||
@@ -391,6 +415,7 @@ export default {
|
||||
downloadInfo: null,
|
||||
downloadLoading: false,
|
||||
singleSendLoading: false,
|
||||
copyLinkLoading: false,
|
||||
treeProps: {
|
||||
label: 'fileName',
|
||||
children: 'children',
|
||||
@@ -462,10 +487,6 @@ export default {
|
||||
}
|
||||
return `${baseUrl}?${params.toString()}`
|
||||
},
|
||||
// 文件树与窗格同源:直接返回当前目录数据
|
||||
buildTree(list) {
|
||||
return list || []
|
||||
},
|
||||
// 懒加载子节点
|
||||
loadNode(node, resolve) {
|
||||
if (node.level === 0) {
|
||||
@@ -479,9 +500,14 @@ export default {
|
||||
}))
|
||||
resolve(children)
|
||||
} else {
|
||||
this.$message.error(res.data.msg || '获取子节点失败')
|
||||
resolve([])
|
||||
}
|
||||
}).catch(() => resolve([]))
|
||||
}).catch(err => {
|
||||
const msg = err.response?.data?.msg || err.message
|
||||
if (msg) this.$message.error(msg)
|
||||
resolve([])
|
||||
})
|
||||
} else {
|
||||
resolve([])
|
||||
}
|
||||
@@ -491,7 +517,6 @@ export default {
|
||||
},
|
||||
// 处理文件点击
|
||||
handleFileClick(file) {
|
||||
console.log('点击文件', file, this.viewMode)
|
||||
if (file.fileType === 'folder') {
|
||||
this.enterFolder(file)
|
||||
} else if (this.viewMode === 'pane') {
|
||||
@@ -520,7 +545,8 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('进入文件夹失败:', error)
|
||||
this.$message.error('进入文件夹失败')
|
||||
const msg = error.response?.data?.msg || error.message || '进入文件夹失败'
|
||||
this.$message.error(msg)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
@@ -551,7 +577,8 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载目录失败:', error)
|
||||
this.$message.error('加载目录失败')
|
||||
const msg = error.response?.data?.msg || error.message || '加载目录失败'
|
||||
this.$message.error(msg)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
@@ -649,7 +676,8 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取下载信息失败:', error)
|
||||
this.$message.error('获取下载信息失败,尝试直接下载')
|
||||
const msg = error.response?.data?.msg || '获取下载信息失败,尝试直接下载'
|
||||
this.$message.error(msg)
|
||||
this.downloadFile(file)
|
||||
} finally {
|
||||
this.downloadLoading = false
|
||||
@@ -735,7 +763,8 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送到下载器失败:', error)
|
||||
this.$message.error('发送到下载器失败: ' + error.message)
|
||||
const msg = error.response?.data?.msg || error.message || '发送到下载器失败'
|
||||
this.$message.error(msg)
|
||||
} finally {
|
||||
this.singleSendLoading = false
|
||||
}
|
||||
@@ -744,6 +773,32 @@ export default {
|
||||
this.fileDialogVisible = false
|
||||
this.selectedFile = null
|
||||
},
|
||||
async copyDirectLink(file) {
|
||||
if (!file?.parserUrl) {
|
||||
this.$message.warning('该文件暂无直链')
|
||||
return
|
||||
}
|
||||
const rawUrl = file.parserUrl.startsWith('http') ? file.parserUrl : (window.location.origin + file.parserUrl)
|
||||
const url = this.appendToken(rawUrl)
|
||||
this.copyLinkLoading = true
|
||||
try {
|
||||
await navigator.clipboard.writeText(url)
|
||||
this.$message.success('直链已复制到剪贴板')
|
||||
} catch {
|
||||
// fallback
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = url
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.opacity = '0'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
this.$message.success('直链已复制到剪贴板')
|
||||
} finally {
|
||||
this.copyLinkLoading = false
|
||||
}
|
||||
},
|
||||
closePreview() {
|
||||
this.isPreviewing = false
|
||||
this.previewUrl = ''
|
||||
@@ -802,7 +857,7 @@ export default {
|
||||
this.toggleFileSelect(file)
|
||||
},
|
||||
selectAll() {
|
||||
this.selectedFiles = this.currentFileList.filter(f => f.fileType !== 'folder')
|
||||
this.selectedFiles = this.currentFileList.filter(f => f.fileType !== 'folder' && f.parserUrl)
|
||||
},
|
||||
deselectAll() {
|
||||
this.selectedFiles = []
|
||||
|
||||
@@ -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