mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-08-27 12:02:01 +00:00
fix(uc/qk): improve directory download with cookie, transfer fallback and URL-safe param encoding
- Preserve download cookie for UC/Quark needDownloader flows and disable browser/copy when required - Quark: share-link first, transfer only on size limit (23018), reuse savedFileCache/search_exit - Propagate auth to subdirectory parser URLs; switch path params to URL-safe Base64 (no double encode) - Bump version to 0.4.2 Fixes #205 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -22,6 +22,8 @@ public class ParserAuthUtil {
|
||||
public static final String SKIP_CLIENT_LINKS = "_skipClientLinks";
|
||||
public static final String TEMP_AUTH_ADDED = "__TEMP_AUTH_ADDED";
|
||||
public static final String DONATED_ACCOUNT_TOKEN = "__AUTO_DONATED_ACCOUNT_TOKEN";
|
||||
/** 原始加密 auth 查询串,供目录子链透传(避免进入子目录丢失认证) */
|
||||
public static final String AUTH_QUERY = "_authQuery";
|
||||
|
||||
private ParserAuthUtil() {
|
||||
}
|
||||
@@ -41,6 +43,8 @@ public class ParserAuthUtil {
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(auth)) {
|
||||
// 保留原始 auth,供 getFileList 子目录 parserUrl 透传
|
||||
otherParam.put(AUTH_QUERY, auth);
|
||||
AuthParam authParam = AuthParamCodec.decode(auth);
|
||||
if (authParam != null && authParam.hasValidAuth()) {
|
||||
otherParam.put("authType", authParam.getAuthType());
|
||||
|
||||
@@ -68,9 +68,10 @@ public class URLParamUtil {
|
||||
boolean firstParam = !decodedUrl.contains("?");
|
||||
|
||||
for (String paramName : params.names()) {
|
||||
// 忽略 "url", "pwd", "dirId", "uuid", "auth", "stoken" 参数(这些参数单独处理,不应拼接到分享URL中)
|
||||
if (!paramName.equals("url") && !paramName.equals("pwd") && !paramName.equals("dirId")
|
||||
&& !paramName.equals("uuid") && !paramName.equals("auth") && !paramName.equals("stoken")) {
|
||||
// 忽略单独处理的参数,不应拼接到分享URL中
|
||||
if (!paramName.equals("url") && !paramName.equals("pwd") && !paramName.equals("dirId")
|
||||
&& !paramName.equals("uuid") && !paramName.equals("auth")
|
||||
&& !paramName.equals("stoken") && !paramName.equals("zml")) {
|
||||
if (firstParam) {
|
||||
urlBuilder.append("?");
|
||||
firstParam = false;
|
||||
|
||||
@@ -16,7 +16,9 @@ import cn.qaiu.lz.web.service.DbService;
|
||||
import cn.qaiu.parser.PanDomainTemplate;
|
||||
import cn.qaiu.parser.IPanTool;
|
||||
import cn.qaiu.parser.ParserCreate;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkGeneratorFactory;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||
import cn.qaiu.util.CommonUtils;
|
||||
import cn.qaiu.vx.core.annotaions.RouteHandler;
|
||||
import cn.qaiu.vx.core.annotaions.RouteMapping;
|
||||
import cn.qaiu.vx.core.enums.RouteMethod;
|
||||
@@ -161,7 +163,7 @@ public class ParserApi {
|
||||
|
||||
@RouteMapping("/getFileList")
|
||||
public Future<List<FileInfo>> getFileList(HttpServerRequest request, String pwd, String dirId, String uuid,
|
||||
String stoken, String auth) {
|
||||
String stoken, String zml, String auth) {
|
||||
String url = URLParamUtil.parserParams(request);
|
||||
ParserCreate parserCreate;
|
||||
try {
|
||||
@@ -182,6 +184,9 @@ public class ParserApi {
|
||||
if (StringUtils.isNotBlank(uuid)) {
|
||||
parserCreate.getShareLinkInfo().getOtherParam().put("uuid", uuid);
|
||||
}
|
||||
if (StringUtils.isNotBlank(zml)) {
|
||||
parserCreate.getShareLinkInfo().getOtherParam().put("zml", zml);
|
||||
}
|
||||
return ParserAuthUtil.applyAuthParamsAndDonatedFallback(parserCreate, otherParam, dbService)
|
||||
.compose(v -> {
|
||||
URLParamUtil.addParam(parserCreate);
|
||||
@@ -207,7 +212,14 @@ public class ParserApi {
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
String paramStr = new String(Base64.getDecoder().decode(param));
|
||||
final String paramStr;
|
||||
try {
|
||||
paramStr = CommonUtils.urlBase64Decode(param);
|
||||
} catch (Exception e) {
|
||||
Promise<String> promise = Promise.promise();
|
||||
promise.fail("下载参数解码失败: " + e.getMessage());
|
||||
return promise.future();
|
||||
}
|
||||
ShareLinkInfo shareLinkInfo = parserCreate.getShareLinkInfo();
|
||||
shareLinkInfo.getOtherParam().put("paramJson", new JsonObject(paramStr));
|
||||
|
||||
@@ -242,6 +254,131 @@ public class ParserApi {
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
/**
|
||||
* 目录文件下载信息(供前端下载器使用):返回直链、请求头及命令行
|
||||
*/
|
||||
@RouteMapping("/getFileDownInfo/:type/:param")
|
||||
public Future<JsonObject> getFileDownInfo(HttpServerRequest request, String type, String param, String auth) {
|
||||
ParserCreate parserCreate;
|
||||
try {
|
||||
parserCreate = ParserCreate.fromType(type).shareKey("-").setShareLinkInfoPwd("-");
|
||||
} catch (Exception e) {
|
||||
return Future.failedFuture(e);
|
||||
}
|
||||
|
||||
if (param == null || param.isEmpty()) {
|
||||
return Future.failedFuture("下载参数为空");
|
||||
}
|
||||
|
||||
final JsonObject paramJson;
|
||||
try {
|
||||
paramJson = new JsonObject(CommonUtils.urlBase64Decode(param));
|
||||
} catch (Exception e) {
|
||||
return Future.failedFuture("下载参数解码失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
ShareLinkInfo shareLinkInfo = parserCreate.getShareLinkInfo();
|
||||
shareLinkInfo.getOtherParam().put("paramJson", paramJson);
|
||||
|
||||
String linkPrefix = getLinkPrefix(request);
|
||||
JsonObject otherParam = ParserAuthUtil.buildOtherParam(request, auth, linkPrefix);
|
||||
shareLinkInfo.getOtherParam().put("domainName", linkPrefix);
|
||||
shareLinkInfo.getOtherParam().put("_requestOrigin", linkPrefix);
|
||||
|
||||
return ParserAuthUtil.applyAuthParamsAndDonatedFallback(parserCreate, otherParam, dbService)
|
||||
.compose(v -> {
|
||||
URLParamUtil.addParam(parserCreate);
|
||||
IPanTool tool = parserCreate.createTool();
|
||||
return IPanTool.closeAfter(tool, tool::parseById)
|
||||
.onFailure(t -> {
|
||||
ParserAuthUtil.recordDonatedAccountFailureIfNeeded(dbService, otherParam, t);
|
||||
ParserAuthUtil.recordAutoDonatedFailureIfNeeded(dbService,
|
||||
parserCreate.getShareLinkInfo(), t);
|
||||
})
|
||||
.map(downloadUrl -> buildFileDownInfo(shareLinkInfo, paramJson, downloadUrl));
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static JsonObject buildFileDownInfo(ShareLinkInfo shareLinkInfo, JsonObject paramJson, String downloadUrl) {
|
||||
Map<String, String> downloadHeaders = new HashMap<>();
|
||||
// 入口参数里可能已带 cookie(目录解析时写入),先作为底稿
|
||||
mergeDownloadHeaders(downloadHeaders, paramJson.getJsonObject("downloadHeaders"));
|
||||
// 解析器运行时生成的请求头优先覆盖(如刷新后的 cookie),但跳过 null
|
||||
Object headersObj = shareLinkInfo.getOtherParam().get("downloadHeaders");
|
||||
if (headersObj instanceof Map) {
|
||||
mergeDownloadHeaders(downloadHeaders, (Map<?, ?>) headersObj);
|
||||
}
|
||||
|
||||
String fileName = paramJson.getString("fileName", "");
|
||||
if (StringUtils.isBlank(fileName)) {
|
||||
Object fn = shareLinkInfo.getOtherParam().get("fileName");
|
||||
if (fn != null) {
|
||||
fileName = fn.toString();
|
||||
}
|
||||
}
|
||||
|
||||
boolean needDownloader = Boolean.TRUE.equals(paramJson.getBoolean("needDownloader"))
|
||||
|| !downloadHeaders.isEmpty();
|
||||
|
||||
shareLinkInfo.getOtherParam().put("downloadUrl", downloadUrl);
|
||||
if (!downloadHeaders.isEmpty()) {
|
||||
shareLinkInfo.getOtherParam().put("downloadHeaders", downloadHeaders);
|
||||
}
|
||||
|
||||
JsonObject result = new JsonObject()
|
||||
.put("downloadUrl", downloadUrl)
|
||||
.put("fileName", fileName)
|
||||
.put("needDownloader", needDownloader)
|
||||
.put("downloadHeaders", downloadHeaders);
|
||||
|
||||
try {
|
||||
Map<ClientLinkType, String> clientLinks = ClientLinkGeneratorFactory.generateAll(shareLinkInfo);
|
||||
if (clientLinks.containsKey(ClientLinkType.CURL)) {
|
||||
result.put("curlCommand", clientLinks.get(ClientLinkType.CURL));
|
||||
}
|
||||
if (clientLinks.containsKey(ClientLinkType.ARIA2)) {
|
||||
result.put("aria2Command", clientLinks.get(ClientLinkType.ARIA2));
|
||||
}
|
||||
if (clientLinks.containsKey(ClientLinkType.THUNDER)) {
|
||||
result.put("thunderLink", clientLinks.get(ClientLinkType.THUNDER));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("生成下载命令失败: {}", e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并下载请求头,忽略 null/空值,避免运行时 null 覆盖入口参数中的 cookie。
|
||||
*/
|
||||
private static void mergeDownloadHeaders(Map<String, String> target, JsonObject source) {
|
||||
if (source == null || source.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (String key : source.fieldNames()) {
|
||||
Object val = source.getValue(key);
|
||||
if (val != null && StringUtils.isNotBlank(val.toString())) {
|
||||
target.put(key, val.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void mergeDownloadHeaders(Map<String, String> target, Map<?, ?> source) {
|
||||
if (source == null || source.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (Map.Entry<?, ?> e : source.entrySet()) {
|
||||
if (e.getKey() == null || e.getValue() == null) {
|
||||
continue;
|
||||
}
|
||||
String val = e.getValue().toString();
|
||||
if (StringUtils.isNotBlank(val)) {
|
||||
target.put(e.getKey().toString(), val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 预览媒体文件
|
||||
|
||||
@@ -186,6 +186,11 @@ public class CacheServiceImpl implements CacheService {
|
||||
// 传递 downloadHeaders 到两个对象
|
||||
cacheLinkInfo.getOtherParam().put("downloadHeaders", downloadHeaders);
|
||||
result.getOtherParam().put("downloadHeaders", downloadHeaders);
|
||||
// 有特殊下载头时标记需要下载器(浏览器无法带 cookie 直连)
|
||||
if (!downloadHeaders.isEmpty()) {
|
||||
cacheLinkInfo.getOtherParam().put("needDownloader", true);
|
||||
result.getOtherParam().put("needDownloader", true);
|
||||
}
|
||||
|
||||
// 使用已有的工具类生成下载命令
|
||||
generateCommandsFromShareLinkInfo(shareLinkInfo, cacheLinkInfo, result);
|
||||
|
||||
Reference in New Issue
Block a user