From 2f22cb01eb151bb54c899fd0d0778fc210b83a68 Mon Sep 17 00:00:00 2001 From: qaiu Date: Fri, 23 Jan 2026 03:26:35 +0800 Subject: [PATCH] =?UTF-8?q?LeTool=E4=B9=90=E4=BA=91=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/cn/qaiu/parser/impl/LeTool.java | 270 +++++++++++++++++- web-front/src/views/Home.vue | 2 +- 2 files changed, 267 insertions(+), 5 deletions(-) diff --git a/parser/src/main/java/cn/qaiu/parser/impl/LeTool.java b/parser/src/main/java/cn/qaiu/parser/impl/LeTool.java index 1ea5abf..432fcda 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/LeTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/LeTool.java @@ -1,18 +1,26 @@ package cn.qaiu.parser.impl; +import cn.qaiu.entity.FileInfo; import cn.qaiu.entity.ShareLinkInfo; import cn.qaiu.parser.PanBase; +import cn.qaiu.util.FileSizeConverter; import io.vertx.core.Future; +import io.vertx.core.Promise; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; import java.util.UUID; /** * 联想乐云 */ public class LeTool extends PanBase { - private static final String API_URL_PREFIX = "https://lecloud.lenovo.com/share/api/clouddiskapi/share/public/v1/"; + private static final String API_URL_PREFIX = "https://lecloud.lenovo.com/mshare/api/clouddiskapi/share/public/v1/"; + private static final String DEFAULT_FILE_TYPE = "file"; + private static final int FILE_TYPE_DIRECTORY = 0; // 目录类型 public LeTool(ShareLinkInfo shareLinkInfo) { super(shareLinkInfo); @@ -24,7 +32,7 @@ public class LeTool extends PanBase { // {"shareId":"xxx","password":"xxx","directoryId":"-1"} String apiUrl1 = API_URL_PREFIX + "shareInfo"; client.postAbs(apiUrl1) - .sendJsonObject(JsonObject.of("shareId", dataKey, "password", pwd, "directoryId", -1)) + .sendJsonObject(JsonObject.of("shareId", dataKey, "password", pwd, "directoryId", "-1")) .onSuccess(res -> { JsonObject resJson = asJson(res); if (resJson.containsKey("result")) { @@ -44,7 +52,19 @@ public class LeTool extends PanBase { } JsonObject fileInfoJson = files.getJsonObject(0); if (fileInfoJson != null) { - // TODO 文件大小fileSize和文件名fileName + // Extract and populate FileInfo + FileInfo fileInfo = createFileInfo(fileInfoJson); + shareLinkInfo.getOtherParam().put("fileInfo", fileInfo); + + // 判断是否为目录 + Integer fileType = fileInfoJson.getInteger("fileType"); + if (fileType != null && fileType == FILE_TYPE_DIRECTORY) { + // 如果是目录,返回目录ID + String fileId = fileInfoJson.getString("fileId"); + promise.complete(fileId); + return; + } + String fileId = fileInfoJson.getString("fileId"); // 根据文件ID获取跳转链接 getDownURL(dataKey, fileId); @@ -59,6 +79,201 @@ public class LeTool extends PanBase { return promise.future(); } + @Override + public Future> parseFileList() { + Promise> listPromise = Promise.promise(); + + String dataKey = shareLinkInfo.getShareKey(); + + // 如果参数里的目录ID不为空,则直接解析目录 + String dirId = (String) shareLinkInfo.getOtherParam().get("dirId"); + if (dirId != null && !dirId.isEmpty()) { + parseDirectory(dirId, dataKey, listPromise); + return listPromise.future(); + } + + // 先解析获取根目录ID + parse().onSuccess(id -> { + if (id != null && !id.isEmpty()) { + // 解析目录 + parseDirectory(id, dataKey, listPromise); + } else { + listPromise.fail("解析目录ID失败"); + } + }).onFailure(failRes -> { + log.error("解析目录失败: {}", failRes.getMessage()); + listPromise.fail(failRes); + }); + + return listPromise.future(); + } + + /** + * 解析目录下的文件列表 + */ + private void parseDirectory(String directoryId, String shareId, Promise> promise) { + log.debug("开始解析目录: directoryId={}, shareId={}", directoryId, shareId); + + String pwd = shareLinkInfo.getSharePassword(); + if (pwd == null) { + pwd = ""; + } + String apiUrl = API_URL_PREFIX + "shareInfo"; + + client.postAbs(apiUrl) + .sendJsonObject(JsonObject.of("shareId", shareId, "password", pwd, "directoryId", directoryId)) + .onSuccess(res -> { + JsonObject resJson = asJson(res); + + if (!resJson.containsKey("result") || !resJson.getBoolean("result")) { + promise.fail("解析目录失败: " + resJson.encode()); + return; + } + + JsonObject dataJson = resJson.getJsonObject("data"); + if (!dataJson.getBoolean("passwordVerified")) { + promise.fail("密码验证失败"); + return; + } + + JsonArray files = dataJson.getJsonArray("files"); + if (files == null || files.isEmpty()) { + promise.complete(new ArrayList<>()); + return; + } + + List fileList = new ArrayList<>(); + for (int i = 0; i < files.size(); i++) { + JsonObject fileJson = files.getJsonObject(i); + FileInfo fileInfo = createFileInfoForList(fileJson, shareId); + fileList.add(fileInfo); + } + + promise.complete(fileList); + }) + .onFailure(err -> { + log.error("解析目录请求失败: {}", err.getMessage()); + promise.fail(err); + }); + } + + /** + * 为文件列表创建 FileInfo 对象 + */ + private FileInfo createFileInfoForList(JsonObject fileJson, String shareId) { + FileInfo fileInfo = new FileInfo(); + + try { + String fileId = fileJson.getString("fileId"); + String fileName = fileJson.getString("fileName"); + Long fileSize = fileJson.getLong("fileSize"); + Integer fileType = fileJson.getInteger("fileType"); + + fileInfo.setFileId(fileId); + fileInfo.setFileName(fileName); + fileInfo.setPanType(shareLinkInfo.getType()); + + // 判断是否为目录 + if (fileType != null && fileType == FILE_TYPE_DIRECTORY) { + // 目录类型 + fileInfo.setFileType("folder"); + fileInfo.setSize(0L); + fileInfo.setSizeStr("0B"); + // 设置目录解析的URL - 注意:fileId已经是URL编码的,直接使用 + // 使用 URLEncoder 确保特殊字符被正确编码 + try { + String encodedFileId = java.net.URLEncoder.encode(fileId, "UTF-8"); + fileInfo.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s", + getDomainName(), + shareLinkInfo.getShareUrl(), + encodedFileId)); + } catch (Exception e) { + // 如果编码失败,直接使用原始fileId + fileInfo.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s", + getDomainName(), + shareLinkInfo.getShareUrl(), + fileId)); + } + } else { + // 文件类型 + fileInfo.setFileType(fileType != null ? String.valueOf(fileType) : DEFAULT_FILE_TYPE); + fileInfo.setSize(fileSize); + fileInfo.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize)); + + // 创建参数JSON并编码为Base64 + JsonObject paramJson = JsonObject.of( + "shareId", shareId, + "fileId", fileId + ); + String paramBase64 = Base64.getEncoder().encodeToString(paramJson.encode().getBytes()); + + // 设置解析URL和预览URL + fileInfo.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", + getDomainName(), + shareLinkInfo.getType(), + paramBase64)); + } + + } catch (Exception e) { + log.warn("创建文件信息失败: {}", e.getMessage()); + } + + return fileInfo; + } + + @Override + public Future parseById() { + Promise parsePromise = Promise.promise(); + + try { + // 从参数中获取解析所需的信息 + JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson"); + String shareId = paramJson.getString("shareId"); + String fileId = paramJson.getString("fileId"); + + // 调用获取下载链接 + getDownURLForById(shareId, fileId, parsePromise); + + } catch (Exception e) { + parsePromise.fail("解析参数失败: " + e.getMessage()); + } + + return parsePromise.future(); + } + + /** + * 根据文件ID获取下载URL (用于 parseById) + */ + private void getDownURLForById(String shareId, String fileId, Promise promise) { + String uuid = UUID.randomUUID().toString(); + JsonArray fileIds = JsonArray.of(fileId); + String apiUrl = API_URL_PREFIX + "packageDownloadWithFileIds"; + + client.postAbs(apiUrl) + .sendJsonObject(JsonObject.of("fileIds", fileIds, "shareId", shareId, "browserId", uuid)) + .onSuccess(res -> { + JsonObject resJson = asJson(res); + if (resJson.containsKey("result")) { + if (resJson.getBoolean("result")) { + JsonObject dataJson = resJson.getJsonObject("data"); + String downloadUrl = dataJson.getString("downloadUrl"); + if (downloadUrl == null) { + promise.fail("Result JSON数据异常: downloadUrl不存在"); + return; + } + // 获取重定向链接 + clientNoRedirects.getAbs(downloadUrl).send() + .onSuccess(res2 -> promise.complete(res2.headers().get("Location"))) + .onFailure(err -> promise.fail(err)); + } else { + promise.fail(resJson.getString("errcode") + ": " + resJson.getString("errmsg")); + } + } else { + promise.fail("Result JSON数据异常: result字段不存在"); + } + }).onFailure(err -> promise.fail(err)); + } + private void getDownURL(String key, String fileId) { String uuid = UUID.randomUUID().toString(); JsonArray fileIds = JsonArray.of(fileId); @@ -89,4 +304,51 @@ public class LeTool extends PanBase { } }).onFailure(handleFail(apiUrl2)); } -} + + /** + * Create FileInfo object from JSON response + * Uses exact field names from the API response without fallback checks + */ + private FileInfo createFileInfo(JsonObject fileInfoJson) { + FileInfo fileInfo = new FileInfo(); + + try { + // Set fileId + String fileId = fileInfoJson.getString("fileId"); + if (fileId != null) { + fileInfo.setFileId(fileId); + } + + // Set fileName + String fileName = fileInfoJson.getString("fileName"); + if (fileName != null) { + fileInfo.setFileName(fileName); + } + + // Set file size + Long fileSize = fileInfoJson.getLong("fileSize"); + if (fileSize != null) { + fileInfo.setSize(fileSize); + // Convert to readable size string + fileInfo.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize)); + } + + // Set fileType (API returns it as an integer) + Integer fileTypeInt = fileInfoJson.getInteger("fileType"); + if (fileTypeInt != null) { + fileInfo.setFileType(String.valueOf(fileTypeInt)); + } else { + // Default to generic file type if not available + fileInfo.setFileType(DEFAULT_FILE_TYPE); + } + + // Set panType + fileInfo.setPanType(shareLinkInfo.getType()); + + } catch (Exception e) { + log.warn("Error extracting file info from JSON: {}", e.getMessage()); + } + + return fileInfo; + } +} \ No newline at end of file diff --git a/web-front/src/views/Home.vue b/web-front/src/views/Home.vue index 0613bf8..d408a82 100644 --- a/web-front/src/views/Home.vue +++ b/web-front/src/views/Home.vue @@ -400,7 +400,7 @@ export default { const data = result.data // 检查是否支持目录解析 - const supportedPans = ["iz", "lz", "fj", "ye"] + const supportedPans = ["iz", "lz", "fj", "ye", "le"] if (!supportedPans.includes(data.shareLinkInfo.type)) { this.$message.error("当前网盘不支持目录解析") return