Compare commits

...

25 Commits

Author SHA1 Message Date
q
0e30eafe49 目录解析支持 2025-07-04 19:20:06 +08:00
q
7facb62f21 Merge remote-tracking branch 'origin/main' 2025-07-04 19:17:35 +08:00
q
30d43cb961 目录解析支持 2025-07-04 19:16:36 +08:00
q
c505b17e35 目录解析支持 2025-07-04 19:11:39 +08:00
qaiu
080c4c753d Create update-release-badge.yml 2025-07-04 09:34:22 +08:00
q
ade0d34d91 start 2025-07-02 18:40:32 +08:00
qaiu
56d082eb0b Update maven.yml
提交任意tag 触发工作流
2025-07-02 18:36:45 +08:00
qaiu
795c4529ba Update AppMain.java 2025-07-02 18:29:46 +08:00
qaiu
0f5cfe22ea Update maven.yml
构建docker添加tag版本
2025-07-02 18:26:48 +08:00
qaiu
925ad2c3a5 Update AppMain.java 2025-07-02 18:20:30 +08:00
qaiu
f3e96907fe Update maven.yml
docker镜像构建添加版本号
2025-07-02 18:19:01 +08:00
qaiu
75a1e58a7d Update AppMain.java 2025-07-02 18:13:19 +08:00
yyzy-official
379e889f71 Update maven.yml
修改工作流版本号问题
2025-07-02 18:09:48 +08:00
yyzy-official
40c06f397b Update README.md
收款码隐藏
2025-07-02 17:41:23 +08:00
yyzy-official
9e9302436e Update README.md
1. 微信联系方式
2. 预览地址
2025-07-02 17:39:14 +08:00
qaiu
6d816d4193 Update README.md 2025-07-02 15:50:29 +08:00
qaiu
438eda9c08 Update README.md 2025-07-02 15:46:55 +08:00
q
ace39e4633 jdk24适配 2025-06-17 15:48:52 +08:00
qaiu
7712391f29 更新 README.md 2025-06-06 07:17:49 +08:00
qaiu
65f08dcb02 Update README.md 2025-06-04 16:28:50 +08:00
qaiu
1d332aa6f4 Update README.md 2025-06-04 16:26:56 +08:00
QAIU
ba81641517 1. 修改docker构建失败 2025-06-04 15:37:50 +08:00
qaiu
fb30bdb879 Update README.md 2025-06-04 15:27:21 +08:00
qaiu
fc451d3b41 Update README.md 文件夹解析说明 2025-06-04 15:25:26 +08:00
QAIU
ffee1f3462 1. 修复 小飞机解析错误#106
2. 添加 123云盘文件夹分享下解析为压缩包下载直链
3. 添加 123云盘全部域名支持: "www.123pan.com","www.123pan.cn","www.123865.com","www.123684.com","www.123912.com","www.123pan.cn"
2025-06-04 15:13:05 +08:00
32 changed files with 1285 additions and 242 deletions

View File

@@ -15,6 +15,8 @@ permissions:
on:
push:
tags:
- '*'
branches: [ "main" ]
paths-ignore:
- 'bin/**'
@@ -76,6 +78,13 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract git tag
id: tag
run: |
GIT_TAG=$(git tag --points-at HEAD | head -n 1)
echo "tag=$GIT_TAG" >> $GITHUB_OUTPUT
- name: Build and push Docker image
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v5
@@ -83,5 +92,5 @@ jobs:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.sha }}
ghcr.io/${{ github.repository }}:main
ghcr.io/qaiu/netdisk-fast-download:${{ steps.tag.outputs.tag }}
ghcr.io/qaiu/netdisk-fast-download:latest

View File

@@ -0,0 +1,34 @@
name: Update Release Badge
on:
push:
tags:
- 'v*' # 可按需调整
jobs:
update-badge:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Get latest tag
id: get_tag
run: echo "tag_name=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Update README badge
run: |
TAG=${{ steps.get_tag.outputs.tag_name }}
BADGE="https://img.shields.io/github/actions/workflow/status/qaiu/netdisk-fast-download/maven.yml?branch=$TAG"
echo "Using badge: $BADGE"
# 替换 README 中 badge 行(标记行需特殊注释)
sed -i -E "s#(!\[release-badge\]\(.*\))#![release-badge]($BADGE)#" README.md
- name: Commit and push
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -am "🔄 update release badge for ${{ steps.get_tag.outputs.tag_name }}" || echo "No changes"
git push

View File

@@ -1,8 +1,9 @@
<div align=center><img src="https://bd2.qaiu.cn/blog/lanzou111.403f7881.png" height="160" width="160"></div>
<p align="center">
<img src="https://github.com/user-attachments/assets/87401aae-b0b6-4ffb-bbeb-44756404d26f" alt="项目预览图" />
</p>
<p align="center">
<a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml/badge.svg?style=flat"></a>
<a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://img.shields.io/github/actions/workflow/status/{user}/{repo}/{workflow_file}?branch={tag}"></a>
<a href="https://www.oracle.com/cn/java/technologies/downloads"><img src="https://img.shields.io/badge/jdk-%3E%3D17-blue"></a>
<a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.6-blue?style=flat"></a>
<a href="https://raw.githubusercontent.com/qaiu/netdisk-fast-download/master/LICENSE"><img src="https://img.shields.io/github/license/qaiu/netdisk-fast-download?style=flat"></a>
@@ -10,12 +11,15 @@
</p>
# netdisk-fast-download 网盘分享链接云解析服务
netdisk-fast-download网盘直链云解析(nfd云解析)能把网盘分享下载链接转化为直链,支持多款云盘,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等支持加密分享。
netdisk-fast-download网盘直链云解析(nfd云解析)能把网盘分享下载链接转化为直链,支持多款云盘,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等支持加密分享,以及部分网盘文件夹分享
[预览地址1](https://lz.qaiu.top)
[预览地址2](http://www.722shop.top:6401)
预览地址 https://lz.qaiu.top
预览地址2(可以解析onedrive) http://8.209.249.88:6402
main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/netdisk-fast-download/tree/main-jdk11)
**0.1.8及以上版本json接口格式有调整 参考json返回数据格式示例**
**小飞机解析有IP限制多数云服务商的大陆IP会被拦截可以自行配置代理和本程序无关**
@@ -81,6 +85,8 @@ API规则:
2. 获取解析后的直链--JSON格式
http://your_host/json/parser?url=分享链接&pwd=xxx
http://your_host/json/网盘标识/分享key@分享密码
3. 文件夹解析v0.1.8fixed3新增
http://your_host/json/getFileList?url=分享链接&pwd=xxx
```
json返回数据格式示例:
`shareKey`: 全局分享key
@@ -132,7 +138,36 @@ json返回数据格式示例:
"timestamp": 1736489219402
}
```
3. 解析次数统计接口 /v2/statisticsInfo
3. 文件夹解析(仅支持蓝奏云/蓝奏优享/小飞机网盘)
/v2/getFileList?url=分享链接&pwd=分享密码
```json
{
"code": 200,
"msg": "success",
"success": true,
"data": [
{
"fileName": "xxx",
"fileId": "xxx",
"fileIcon": null,
"size": 999,
"sizeStr": "999 M",
"fileType": "apk",
"filePath": null,
"createTime": "17 小时前",
"updateTime": null,
"createBy": null,
"description": null,
"downloadCount": null,
"panType": "lz",
"parserUrl": "下载链接",
"extParameters": null
}
]
}
```
4. 解析次数统计接口 /v2/statisticsInfo
```json
{
"code": 200,
@@ -351,10 +386,15 @@ Core模块集成Vert.x实现类似spring的注解式路由API
### 关于专属版
99元, 提供对小飞机,蓝奏优享大文件解析的支持, 提供天翼云盘,移动云盘,联调云盘的解析支持
199元, 包含部署服务和首页定制, 需提供宝塔环境
可以提供功能定制开发, 加v价格详谈: qaiu-cn
可以提供功能定制开发, 加v价格详谈:
<p>wechat1: qaiu-cn</p>
<p>wechat2: imcoding_</p>
<!--
![image](https://github.com/qaiu/netdisk-fast-download/assets/29825328/54276aee-cc3f-4ebd-8973-2e15f6295819)
[手机端支付宝打赏跳转链接](https://qr.alipay.com/fkx01882dnoxxtjenhlxt53)
-->

View File

@@ -16,17 +16,24 @@ public interface BeforeInterceptor extends Handler<RoutingContext> {
default Handler<RoutingContext> doHandle() {
return ctx -> {
ctx.put(IS_NEXT, false);
BeforeInterceptor.this.handle(ctx);
if (!(Boolean) ctx.get(IS_NEXT) && !ctx.response().ended()) {
sendError(ctx, 403);
// 加同步锁
synchronized (BeforeInterceptor.class) {
ctx.put(IS_NEXT, false);
BeforeInterceptor.this.handle(ctx);
if (!(Boolean) ctx.get(IS_NEXT) && !ctx.response().ended()) {
sendError(ctx, 403);
}
}
};
}
default void doNext(RoutingContext context) {
context.put(IS_NEXT, true);
context.next();
// 设置上下文状态为可以继续执行
// 添加同步锁保障多线程下执行时序
synchronized (BeforeInterceptor.class) {
context.put(IS_NEXT, true);
context.next();
}
}
void handle(RoutingContext context);

View File

@@ -11,7 +11,7 @@
<artifactId>parser</artifactId>
<packaging>jar</packaging>
<name>${project.groupId}:${project.artifactId}</name>
<name>cn.qaiu:parser</name>
<description>NFD parser</description>
<url>https://qaiu.top</url>

View File

@@ -69,6 +69,9 @@ public class FileInfo {
*/
private String parserUrl;
//预览地址
private String previewUrl;
/**
* 扩展参数
*/
@@ -199,6 +202,13 @@ public class FileInfo {
this.parserUrl = parserUrl;
return this;
}
public String getPreviewUrl() {
return previewUrl;
}
public FileInfo setPreviewUrl(String previewUrl) {
this.previewUrl = previewUrl;
return this;
}
public Map<String, Object> getExtParameters() {
return extParameters;

View File

@@ -15,6 +15,7 @@ public class ShareLinkInfo {
/**
* 其他参数预定义
* dirId: 目录ID 传入
* auths: 认证相关 传入
* UA: 浏览器请求头 传入
* fileInfo: 解析成功的文件信息对象 传出

View File

@@ -72,7 +72,7 @@ public enum PanDomainTemplate {
WsTool.class),
// https://www.123pan.com/s/
YE("123网盘",
compile("https://www\\.(123pan|123865|123684)\\.com/s/(?<KEY>.+)(.html)?"),
compile("https://www\\.(123pan\\.com|123865\\.com|123684\\.com|123912\\.com|123pan\\.cn)/s/(?<KEY>.+)(.html)?"),
"https://www.123pan.com/s/{shareKey}",
YeTool.class),
// https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data={code}&isShare=1

View File

@@ -14,16 +14,11 @@ import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.uritemplate.UriTemplate;
import org.apache.commons.lang3.StringUtils;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.zip.GZIPInputStream;
/**
* 小飞机网盘
@@ -95,6 +90,9 @@ public class FjTool extends PanBase {
final String shareId = shareLinkInfo.getShareKey();
// 24.5.12 飞机盘 规则修改 需要固定UUID先请求会员接口, 再请求后续接口
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
client.postAbs(UriTemplate.of(VIP_REQUEST_URL))
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
@@ -102,44 +100,18 @@ public class FjTool extends PanBase {
// 第一次请求 获取文件信息
// POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
client.postAbs(UriTemplate.of(FIRST_REQUEST_URL))
client.postAbs(UriTemplate.of(url))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.send().onSuccess(res -> {
// 处理GZ压缩
// 使用GZIPInputStream来解压数据
String decompressedString;
try (ByteArrayInputStream bais = new ByteArrayInputStream(res.body().getBytes());
GZIPInputStream gzis = new GZIPInputStream(bais);
BufferedReader reader = new BufferedReader(new InputStreamReader(gzis, StandardCharsets.UTF_8))) {
// 用于存储解压后的字符串
StringBuilder decompressedData = new StringBuilder();
// 逐行读取解压后的数据
String line;
while ((line = reader.readLine()) != null) {
decompressedData.append(line);
}
// 此时decompressedData.toString()包含了解压后的字符串
decompressedString = decompressedData.toString();
} catch (IOException e) {
// 处理可能的IO异常
fail(FIRST_REQUEST_URL + " 响应异常");
return;
}
// 处理GZ压缩结束
JsonObject resJson = new JsonObject(decompressedString);
JsonObject resJson = asJson(res);
if (resJson.getInteger("code") != 200) {
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
return;
}
if (resJson.getJsonArray("list").size() == 0) {
if (resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
@@ -190,104 +162,109 @@ public class FjTool extends PanBase {
Promise<List<FileInfo>> promise = Promise.promise();
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
// 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (dirId != null && !dirId.isEmpty()) {
uuid = shareLinkInfo.getOtherParam().get("uuid").toString();
parserDir(dirId, shareId, promise);
return promise.future();
}
parse().onSuccess(id -> {
// 拿到目录ID
client.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
JsonObject jsonObject = asJson(res);
System.out.println(jsonObject.encodePrettily());
/*
{
"iconId" : 13,
"fileName" : "酷我音乐车机版 6.4.2.20.apk",
"fileSaves" : 52,
"fileStars" : 5.0,
"type" : 1,
"userId" : 1392902,
"fileComments" : 0,
"fileSize" : 68854,
"fileIcon" : "https://d.feijix.com/storage/files/icon/2024/06/08/7/8146637/6534494874910391.gz?t=67a5ea7c&rlimit=20&us=nMfuftjBN5&sign=f72be03007a301217f90dcc20333bd9a",
"updTime" : "2024-06-10 17:26:53",
"sortId" : 1487918143,
"name" : "酷我音乐车机版 6.4.2.20.apk",
"fileDownloads" : 109,
"fileUrl" : null,
"fileLikes" : 0,
"fileType" : 1,
"fileId" : 1487918143
}
*/
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
parserDir(id, shareId, promise);
});
return promise.future();
}
// 映射已知字段fileInfo
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
// 开始解析目录: 164312216, shareId: bPMsbg5K, uuid: 0fmVWTx2Ea4zFwkpd7KXf, ts: 20865d7b7f00828279f437cd1f097860
// 拿到目录ID
client.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
JsonObject jsonObject = asJson(res);
System.out.println(jsonObject.encodePrettily());
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
// 其他参数
long nowTs2 = System.currentTimeMillis();
String tsEncode2 = AESUtils.encrypt2Hex(Long.toString(nowTs2));
String fidEncode = AESUtils.encrypt2Hex(fileId + "|" + userId);
String auth = AESUtils.encrypt2Hex(fileId + "|" + nowTs2);
// 映射已知字段fileInfo
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
// 回传用到的参数
//"fidEncode", paramJson.getString("fidEncode"))
//"uuid", paramJson.getString("uuid"))
//"ts", paramJson.getString("ts"))
//"auth", paramJson.getString("auth"))
//"shareId", paramJson.getString("shareId"))
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode2,
"auth", auth,
"shareId", shareId);
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
String param = new String(encode);
// 其他参数
long nowTs2 = System.currentTimeMillis();
String tsEncode2 = AESUtils.encrypt2Hex(Long.toString(nowTs2));
String fidEncode = AESUtils.encrypt2Hex(fileId + "|" + userId);
String auth = AESUtils.encrypt2Hex(fileId + "|" + nowTs2);
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileJson.getString("fileId"))
.setCreateTime(fileJson.getString("createTime"))
.setFileType(fileJson.getString("fileType"))
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
// 回传用到的参数
//"fidEncode", paramJson.getString("fidEncode"))
//"uuid", paramJson.getString("uuid"))
//"ts", paramJson.getString("ts"))
//"auth", paramJson.getString("auth"))
//"shareId", paramJson.getString("shareId"))
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode2,
"auth", auth,
"shareId", shareId);
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
String param = new String(encode);
if (fileJson.getInteger("fileType") == 2) {
// 如果是目录
fileInfo.setFileName(fileJson.getString("name"))
.setFileId(fileJson.getString("folderId"))
.setCreateTime(fileJson.getString("updTime"))
.setFileType("folder")
.setSize(0L)
.setSizeStr("0B")
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
// 设置目录解析的URL
.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&uuid=%s", getDomainName(),
shareLinkInfo.getShareUrl(), fileJson.getString("folderId"), uuid));
result.add(fileInfo);
});
promise.complete(result);
return;
}
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileJson.getString("fileId"))
.setCreateTime(fileJson.getString("createTime"))
.setFileType("file")
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param))
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
result.add(fileInfo);
});
});
return promise.future();
promise.complete(result);
});
}
@Override
public Future<String> parseById() {
// 第二次请求
JsonObject paramJson = (JsonObject)shareLinkInfo.getOtherParam().get("paramJson");
// clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
// .putHeaders(header)
// .setTemplateParam("fidEncode", fidEncode)
// .setTemplateParam("uuid", uuid)
// .setTemplateParam("ts", tsEncode2)
// .setTemplateParam("auth", auth)
// .setTemplateParam("dataKey", shareId)
clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
.setTemplateParam("uuid", paramJson.getString("uuid"))

View File

@@ -12,6 +12,7 @@ import io.vertx.core.Promise;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.uritemplate.UriTemplate;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
@@ -77,17 +78,15 @@ public class IzTool extends PanBase {
// 第一次请求 获取文件信息
// POST https://api.ilanzou.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
client.postAbs(UriTemplate.of(VIP_REQUEST_URL))
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.send().onSuccess(r0 -> { // 忽略res
// 第一次请求 获取文件信息
// POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
client.postAbs(UriTemplate.of(
shareLinkInfo.getSharePassword() == null ?
FIRST_REQUEST_URL : (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword()))
)
client.postAbs(UriTemplate.of(url))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
@@ -98,13 +97,12 @@ public class IzTool extends PanBase {
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
return;
}
if (resJson.getJsonArray("list").size() == 0) {
if (resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
// 文件Id
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
// 如果是目录返回目录ID
JsonObject fileList = fileInfo.getJsonArray("fileList").getJsonObject(0);
if (fileList.getInteger("fileType") == 2) {
@@ -143,65 +141,99 @@ public class IzTool extends PanBase {
Promise<List<FileInfo>> promise = Promise.promise();
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
// 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (dirId != null && !dirId.isEmpty()) {
uuid = shareLinkInfo.getOtherParam().get("uuid").toString();
parserDir(dirId, shareId, promise);
return promise.future();
}
parse().onSuccess(id -> {
// 拿到目录ID
client.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
JsonObject jsonObject = asJson(res);
System.out.println(jsonObject.encodePrettily());
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
parserDir(id, shareId, promise);
});
return promise.future();
}
// 映射已知字段
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
// 开始解析目录: 164312216, shareId: bPMsbg5K, uuid: 0fmVWTx2Ea4zFwkpd7KXf, ts: 20865d7b7f00828279f437cd1f097860
// 拿到目录ID
client.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
JsonObject jsonObject = asJson(res);
System.out.println(jsonObject.encodePrettily());
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
// 回传用到的参数
//"fidEncode", paramJson.getString("fidEncode"))
//"uuid", paramJson.getString("uuid"))
//"ts", paramJson.getString("ts"))
//"auth", paramJson.getString("auth"))
//"shareId", paramJson.getString("shareId"))
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode,
"auth", auth,
"shareId", shareId);
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
String param = new String(encode);
// 映射已知字段
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
// 回传用到的参数
//"fidEncode", paramJson.getString("fidEncode"))
//"uuid", paramJson.getString("uuid"))
//"ts", paramJson.getString("ts"))
//"auth", paramJson.getString("auth"))
//"shareId", paramJson.getString("shareId"))
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode,
"auth", auth,
"shareId", shareId);
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
String param = new String(encode);
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileId)
.setCreateTime(fileJson.getString("createTime"))
.setFileType(fileJson.getString("fileType"))
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
if (fileJson.getInteger("fileType") == 2) {
// 如果是目录
fileInfo.setFileName(fileJson.getString("name"))
.setFileId(fileJson.getString("folderId"))
.setCreateTime(fileJson.getString("updTime"))
.setFileType("folder")
.setSize(0L)
.setSizeStr("0B")
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
// 设置目录解析的URL
.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&uuid=%s", getDomainName(),
shareLinkInfo.getShareUrl(), fileJson.getString("folderId"), uuid));
result.add(fileInfo);
});
promise.complete(result);
return;
}
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileId)
.setCreateTime(fileJson.getString("createTime"))
.setFileType("file")
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param))
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
result.add(fileInfo);
});
});
return promise.future();
promise.complete(result);
});
}
@Override

View File

@@ -6,7 +6,9 @@ import cn.qaiu.util.CommonUtils;
import cn.qaiu.util.JsExecUtils;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.json.pointer.JsonPointer;
import io.vertx.ext.web.client.WebClient;
import io.vertx.uritemplate.UriTemplate;
import org.apache.commons.lang3.StringUtils;
@@ -33,6 +35,7 @@ public class YeTool extends PanBase {
"&shareKey={shareKey}&SharePwd={pwd}&ParentFileId=0&Page=1&event=homeListFile&operateType=1";
private static final String DOWNLOAD_API_URL = "https://www.123pan.com/a/api/share/download/info?{authK}={authV}";
private static final String BATCH_DOWNLOAD_API_URL = "https://www.123pan.com/b/api/file/batch_download_share_info?{authK}={authV}";
private final MultiMap header = MultiMap.caseInsensitiveMultiMap();
public YeTool(ShareLinkInfo shareLinkInfo) {
@@ -41,8 +44,8 @@ public class YeTool extends PanBase {
header.set("App-Version", "3");
header.set("Cache-Control", "no-cache");
header.set("Connection", "keep-alive");
header.set("DNT", "1");
header.set("Host", "www.123pan.com");
//header.set("DNT", "1");
//header.set("Host", "www.123pan.com");
header.set("LoginUuid", gen36String());
header.set("Pragma", "no-cache");
header.set("Referer", shareLinkInfo.getStandardUrl());
@@ -103,9 +106,23 @@ public class YeTool extends PanBase {
fail("{} 状态码异常 {}", dataKey, infoJson);
return;
}
JsonObject getFileInfoJson =
infoJson.getJsonObject("data").getJsonArray("InfoList").getJsonObject(0);
getFileInfoJson.put("ShareKey", shareKey);
// 判断是否为文件夹: data->InfoList->0->Type: 1为文件夹, 0为文件
try {
int type = (Integer)JsonPointer.from("/data/InfoList/0/Type").queryJson(infoJson);
if (type == 1) {
getZipDownUrl(client, getFileInfoJson);
return;
}
} catch (Exception exception) {
fail("该分享[{}]解析异常: {}", dataKey, exception.getMessage());
return;
}
getDownUrl(client, getFileInfoJson);
}).onFailure(this.handleFail(GET_FILE_INFO_URL));
} else {
@@ -116,6 +133,11 @@ public class YeTool extends PanBase {
JsonObject reqBodyJson = resListJson.getJsonObject("data").getJsonArray("InfoList").getJsonObject(0);
reqBodyJson.put("ShareKey", shareKey);
if (reqBodyJson.getInteger("Type") == 1) {
// 文件夹
getZipDownUrl(client, reqBodyJson);
return;
}
getDownUrl(client, reqBodyJson);
}).onFailure(this.handleFail(FIRST_REQUEST_URL));
@@ -134,6 +156,21 @@ public class YeTool extends PanBase {
jsonObject.put("Etag", reqBodyJson.getString("Etag"));
// 调用JS文件获取签名
down(client, jsonObject, DOWNLOAD_API_URL);
}
private void getZipDownUrl(WebClient client, JsonObject reqBodyJson) {
log.info(reqBodyJson.encodePrettily());
JsonObject jsonObject = new JsonObject();
// {"ShareKey":"LH3rTd-1ENed","fileIdList":[{"fileId":17525952}]}
jsonObject.put("ShareKey", reqBodyJson.getString("ShareKey"));
jsonObject.put("fileIdList", new JsonArray().add(JsonObject.of("fileId", reqBodyJson.getInteger("FileId"))));
// 调用JS文件获取签名
down(client, jsonObject, BATCH_DOWNLOAD_API_URL);
}
private void down(WebClient client, JsonObject jsonObject, String api) {
ScriptObjectMirror getSign;
try {
getSign = JsExecUtils.executeJs("getSign", "/a/api/share/download/info");
@@ -143,7 +180,7 @@ public class YeTool extends PanBase {
}
log.info("ye getSign: {}={}", getSign.get("0").toString(), getSign.get("1").toString());
client.postAbs(UriTemplate.of(DOWNLOAD_API_URL))
client.postAbs(UriTemplate.of(api))
.setTemplateParam("authK", getSign.get("0").toString())
.setTemplateParam("authV", getSign.get("1").toString())
.putHeader("Platform", "web")
@@ -160,7 +197,8 @@ public class YeTool extends PanBase {
fail("Ye: downURLJson格式异常->" + downURLJson);
return;
}
String downURL = downURLJson.getJsonObject("data").getString("DownloadURL");
String downURL = downURLJson.getJsonObject("data")
.getString(api.contains("batch_download_share_info")? "DownloadUrl" : "DownloadURL");
try {
Map<String, String> urlParams = CommonUtils.getURLParams(downURL);
String params = urlParams.get("params");

View File

@@ -27,7 +27,7 @@
<vertx.version>4.5.6</vertx.version>
<org.reflections.version>0.10.2</org.reflections.version>
<lombok.version>1.18.30</lombok.version>
<lombok.version>1.18.38</lombok.version>
<slf4j.version>2.0.5</slf4j.version>
<commons-lang3.version>3.12.0</commons-lang3.version>
<commons-beanutils2.version>2.0.0</commons-beanutils2.version>

View File

@@ -10,7 +10,7 @@
## 关于如何将前端项目和java一块打包:
1. 先打包前端模块
2. ~~打包后请将当前目录下的nfd-front目录放置在项目下webroot目录, 然后使用maven打包java模块即可~~ `npm run build` 会直接打包到后端代理目录下, 无需复制
2. 运行`npm run build`
3. 项目部署后演示页面的代理端口是6401默认使用http, 如需https可以加nginx代理, 也可以使用本项目自带的代理服务和配置证书路径
## nginx配置

View File

@@ -17,7 +17,7 @@
"qrcode": "^1.5.4",
"vue": "^3.5.12",
"vue-clipboard3": "^2.0.0",
"vue3-json-viewer": "^2.2.2"
"vue3-json-viewer": "2.2.2"
},
"devDependencies": {
"@babel/core": "^7.26.0",

View File

@@ -0,0 +1,165 @@
Fonticons, Inc. (https://fontawesome.com)
--------------------------------------------------------------------------------
Font Awesome Free License
Font Awesome Free is free, open source, and GPL friendly. You can use it for
commercial projects, open source projects, or really almost whatever you want.
Full Font Awesome Free license: https://fontawesome.com/license/free.
--------------------------------------------------------------------------------
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
The Font Awesome Free download is licensed under a Creative Commons
Attribution 4.0 International License and applies to all icons packaged
as SVG and JS file types.
--------------------------------------------------------------------------------
# Fonts: SIL OFL 1.1 License
In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files.
Copyright (c) 2024 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
--------------------------------------------------------------------------------
# Code: MIT License (https://opensource.org/licenses/MIT)
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
Copyright 2024 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
# Attribution
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
Awesome Free files already contain embedded comments with sufficient
attribution, so you shouldn't need to do anything additional when using these
files normally.
We've kept attribution comments terse, so we ask that you do not actively work
to remove them from files, especially code. They're a great way for folks to
learn about Font Awesome.
--------------------------------------------------------------------------------
# Brand Icons
All brand icons are trademarks of their respective owners. The use of these
trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**

9
web-front/public/css/all.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

693
web-front/public/list.html Normal file
View File

@@ -0,0 +1,693 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网盘目录管理系统</title>
<!-- 本地引用Font Awesome -->
<link rel="stylesheet" href="./css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
min-height: 100vh;
padding: 20px;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
}
h1 {
color: #2c3e50;
font-size: 2.5rem;
margin-bottom: 10px;
position: relative;
display: inline-block;
}
h1:after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 4px;
background: linear-gradient(90deg, #3498db, #2c3e50);
border-radius: 2px;
}
.subtitle {
color: #7f8c8d;
font-size: 1.1rem;
margin-top: 5px;
}
.dashboard {
background: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.breadcrumb {
background: #f8f9fa;
padding: 16px 24px;
border-bottom: 1px solid #eaeaea;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.breadcrumb-item {
display: flex;
align-items: center;
font-size: 0.95rem;
color: #7f8c8d;
cursor: pointer;
transition: color 0.2s;
}
.breadcrumb-item:hover {
color: #3498db;
}
.breadcrumb-item i {
margin: 0 8px;
font-size: 0.8rem;
color: #bdc3c7;
}
.content {
padding: 24px;
min-height: 500px;
}
.grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 20px;
}
.item {
background: #fff;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
cursor: pointer;
text-align: center;
padding: 20px 10px;
border: 2px solid transparent;
}
.item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
border-color: #3498db;
}
.item-icon {
font-size: 3.5rem;
margin-bottom: 15px;
transition: transform 0.3s;
}
.item:hover .item-icon {
transform: scale(1.1);
}
.folder .item-icon {
color: #3498db;
}
.image .item-icon {
color: #e74c3c;
}
.document .item-icon {
color: #f39c12;
}
.archive .item-icon {
color: #9b59b6;
}
.audio .item-icon {
color: #1abc9c;
}
.video .item-icon {
color: #d35400;
}
.code .item-icon {
color: #27ae60;
}
.item-name {
font-weight: 500;
font-size: 0.95rem;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.4;
}
.item-meta {
font-size: 0.8rem;
color: #95a5a6;
margin-top: 8px;
}
.empty-state {
text-align: center;
padding: 50px 20px;
color: #7f8c8d;
grid-column: 1 / -1;
}
.empty-state i {
font-size: 5rem;
margin-bottom: 20px;
color: #bdc3c7;
}
.empty-state h3 {
font-size: 1.5rem;
margin-bottom: 10px;
color: #2c3e50;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
grid-column: 1 / -1;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(52, 152, 219, 0.2);
border-top: 5px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.action-bar {
display: flex;
justify-content: space-between;
padding: 15px 24px;
background: #f8f9fa;
border-top: 1px solid #eaeaea;
}
.btn {
background: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: background 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.btn:hover {
background: #2980b9;
}
.btn i {
font-size: 0.9rem;
}
.stats {
display: flex;
align-items: center;
gap: 15px;
color: #7f8c8d;
font-size: 0.9rem;
}
.stat-item {
display: flex;
align-items: center;
gap: 5px;
}
@media (max-width: 768px) {
.grid-view {
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
gap: 15px;
}
.item {
padding: 15px 8px;
}
.item-icon {
font-size: 3rem;
}
}
@media (max-width: 480px) {
.grid-view {
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 10px;
}
h1 {
font-size: 2rem;
}
.action-bar {
flex-direction: column;
gap: 10px;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1><i class="fas fa-cloud"></i> 网盘目录管理系统</h1>
<p class="subtitle">管理您的文件与文件夹,操作简单直观</p>
</header>
<div class="dashboard">
<div class="breadcrumb" id="breadcrumb">
<!-- 面包屑导航会通过JS动态生成 -->
</div>
<div class="content">
<div class="grid-view" id="file-grid">
<!-- 文件列表会通过JS动态生成 -->
</div>
</div>
<div class="action-bar">
<button class="btn" id="back-btn">
<i class="fas fa-arrow-left"></i> 返回上一级
</button>
<div class="stats">
<div class="stat-item">
<i class="fas fa-folder"></i> <span id="folder-count">0</span> 个文件夹
</div>
<div class="stat-item">
<i class="fas fa-file"></i> <span id="file-count">0</span> 个文件
</div>
</div>
</div>
</div>
</div>
<script>
// 文件类型映射
const fileTypeIcons = {
// 图片
'jpg': { icon: 'fa-file-image', type: 'image' },
'jpeg': { icon: 'fa-file-image', type: 'image' },
'png': { icon: 'fa-file-image', type: 'image' },
'gif': { icon: 'fa-file-image', type: 'image' },
'bmp': { icon: 'fa-file-image', type: 'image' },
'svg': { icon: 'fa-file-image', type: 'image' },
'webp': { icon: 'fa-file-image', type: 'image' },
// 文档
'pdf': { icon: 'fa-file-pdf', type: 'document' },
'doc': { icon: 'fa-file-word', type: 'document' },
'docx': { icon: 'fa-file-word', type: 'document' },
'xls': { icon: 'fa-file-excel', type: 'document' },
'xlsx': { icon: 'fa-file-excel', type: 'document' },
'ppt': { icon: 'fa-file-powerpoint', type: 'document' },
'pptx': { icon: 'fa-file-powerpoint', type: 'document' },
'txt': { icon: 'fa-file-alt', type: 'document' },
'rtf': { icon: 'fa-file-alt', type: 'document' },
// 压缩文件
'zip': { icon: 'fa-file-archive', type: 'archive' },
'rar': { icon: 'fa-file-archive', type: 'archive' },
'7z': { icon: 'fa-file-archive', type: 'archive' },
'tar': { icon: 'fa-file-archive', type: 'archive' },
'gz': { icon: 'fa-file-archive', type: 'archive' },
// 音频
'mp3': { icon: 'fa-file-audio', type: 'audio' },
'wav': { icon: 'fa-file-audio', type: 'audio' },
'ogg': { icon: 'fa-file-audio', type: 'audio' },
'flac': { icon: 'fa-file-audio', type: 'audio' },
// 视频
'mp4': { icon: 'fa-file-video', type: 'video' },
'avi': { icon: 'fa-file-video', type: 'video' },
'mov': { icon: 'fa-file-video', type: 'video' },
'wmv': { icon: 'fa-file-video', type: 'video' },
'mkv': { icon: 'fa-file-video', type: 'video' },
'flv': { icon: 'fa-file-video', type: 'video' },
// 代码
'html': { icon: 'fa-file-code', type: 'code' },
'htm': { icon: 'fa-file-code', type: 'code' },
'css': { icon: 'fa-file-code', type: 'code' },
'js': { icon: 'fa-file-code', type: 'code' },
'json': { icon: 'fa-file-code', type: 'code' },
'php': { icon: 'fa-file-code', type: 'code' },
'py': { icon: 'fa-file-code', type: 'code' },
'java': { icon: 'fa-file-code', type: 'code' },
'c': { icon: 'fa-file-code', type: 'code' },
'cpp': { icon: 'fa-file-code', type: 'code' },
'h': { icon: 'fa-file-code', type: 'code' },
'sh': { icon: 'fa-file-code', type: 'code' },
'bat': { icon: 'fa-file-code', type: 'code' },
'md': { icon: 'fa-file-code', type: 'code' },
// 默认
'default': { icon: 'fa-file', type: 'document' }
};
const obj = new URL(window.location.href);
// 获取 URL 参数
const params = obj.searchParams;
const shareUrl = params.get('url');
const pwd = params.get('pwd');
// 动态拼接并编码参数
const apiUrl = `${window.location.origin}/v2/getFileList?url=${encodeURIComponent(shareUrl)}&pwd=${encodeURIComponent(pwd)}`;
// 当前目录状态
let currentDir = {
// url: 'http://192.168.101.227:6401/v2/getFileList?url=https://share.feijipan.com/s/3pMsofZd&pwd=qaiu',
// 动态获取url encode 参数
url: apiUrl,
name: '全部文件'
};
const pathStack = [currentDir];
// DOM 元素
const breadcrumbEl = document.getElementById('breadcrumb');
const fileGridEl = document.getElementById('file-grid');
const backBtn = document.getElementById('back-btn');
const folderCountEl = document.getElementById('folder-count');
const fileCountEl = document.getElementById('file-count');
// 初始化
document.addEventListener('DOMContentLoaded', () => {
renderBreadcrumb();
fetchFileList(currentDir.url);
// 返回上一级按钮事件
backBtn.addEventListener('click', goBack);
});
// 渲染面包屑导航
function renderBreadcrumb() {
breadcrumbEl.innerHTML = '';
pathStack.forEach((item, index) => {
const itemEl = document.createElement('div');
itemEl.className = 'breadcrumb-item';
itemEl.textContent = item.name;
if (index < pathStack.length - 1) {
itemEl.addEventListener('click', () => {
// 点击面包屑项返回对应目录
goToDirectory(index);
});
} else {
itemEl.style.cursor = 'default';
itemEl.style.fontWeight = '600';
itemEl.style.color = '#2c3e50';
}
breadcrumbEl.appendChild(itemEl);
// 如果不是最后一个,添加分隔符
if (index < pathStack.length - 1) {
const separator = document.createElement('i');
separator.className = 'fas fa-chevron-right';
breadcrumbEl.appendChild(separator);
}
});
}
// 获取文件列表
async function fetchFileList(url) {
try {
// 显示加载状态
fileGridEl.innerHTML = `
<div class="loading">
<div class="spinner"></div>
</div>
`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.code === 200 && data.success) {
renderFileList(data.data);
} else {
throw new Error(data.msg || '获取文件列表失败');
}
} catch (error) {
console.error('获取文件列表失败:', error);
fileGridEl.innerHTML = `
<div class="empty-state">
<i class="fas fa-exclamation-circle"></i>
<h3>加载失败</h3>
<p>${error.message}</p>
</div>
`;
}
}
// 渲染文件列表
function renderFileList(files) {
fileGridEl.innerHTML = '';
if (!files || files.length === 0) {
fileGridEl.innerHTML = `
<div class="empty-state">
<i class="fas fa-folder-open"></i>
<h3>此文件夹为空</h3>
<p>暂无文件或文件夹</p>
</div>
`;
folderCountEl.textContent = '0';
fileCountEl.textContent = '0';
return;
}
let folderCount = 0;
let fileCount = 0;
files.forEach(file => {
const item = document.createElement('div');
if (file.fileType === 'folder') {
// 文件夹
item.className = 'item folder';
item.innerHTML = `
<div class="item-icon">
<i class="fas fa-folder"></i>
</div>
<div class="item-name">${file.fileName || '未命名文件夹'}</div>
<div class="item-meta">
${file.sizeStr || '0B'} · ${formatDate(file.createTime)}
</div>
`;
folderCount++;
// 添加点击事件
item.addEventListener('click', () => {
enterFolder(file);
});
} else {
// 文件
const fileExt = getFileExtension(file.fileName);
const fileTypeInfo = fileTypeIcons[fileExt.toLowerCase()] || fileTypeIcons['default'];
item.className = `item ${fileTypeInfo.type}`;
item.innerHTML = `
<div class="item-icon">
<i class="fas ${fileTypeInfo.icon}"></i>
</div>
<div class="item-name">${file.fileName}</div>
<div class="item-meta">
${file.sizeStr || '0B'} · ${formatDate(file.createTime)}
</div>
`;
fileCount++;
// 添加点击事件
item.addEventListener('click', () => {
handleFileClick(file);
});
}
fileGridEl.appendChild(item);
});
// 更新统计信息
folderCountEl.textContent = folderCount;
fileCountEl.textContent = fileCount;
}
// 获取文件扩展名
function getFileExtension(filename) {
if (!filename) return '';
return filename.split('.').pop();
}
// 进入文件夹
function enterFolder(folder) {
if (!folder.parserUrl) {
alert('无法进入该文件夹,缺少访问链接');
return;
}
const newDir = {
url: folder.parserUrl,
name: folder.fileName || '未命名文件夹'
};
pathStack.push(newDir);
currentDir = newDir;
fetchFileList(currentDir.url);
renderBreadcrumb();
}
// 下载文件
function handleFileClick(file) {
if (!file.parserUrl) {
alert('无法操作该文件,缺少必要链接');
return;
}
// 更友好的选择对话框
const modal = document.createElement('div');
modal.style.position = 'fixed';
modal.style.top = '0';
modal.style.left = '0';
modal.style.width = '100%';
modal.style.height = '100%';
modal.style.backgroundColor = 'rgba(0,0,0,0.5)';
modal.style.display = 'flex';
modal.style.justifyContent = 'center';
modal.style.alignItems = 'center';
modal.style.zIndex = '1000';
const dialog = document.createElement('div');
dialog.style.backgroundColor = 'white';
dialog.style.padding = '20px';
dialog.style.borderRadius = '8px';
dialog.style.width = '300px';
dialog.style.textAlign = 'center';
dialog.innerHTML = `
<p style="margin-bottom: 20px;">${file.fileName || '未命名文件'}</p>
<div style="display: flex; justify-content: center; gap: 15px;">
<button id="preview-btn" style="padding: 8px 15px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">
预览文件
</button>
<button id="download-btn" style="padding: 8px 15px; background: #2ecc71; color: white; border: none; border-radius: 4px; cursor: pointer;">
下载文件
</button>
</div>
`;
modal.appendChild(dialog);
document.body.appendChild(modal);
// 预览按钮事件
dialog.querySelector('#preview-btn').addEventListener('click', () => {
document.body.removeChild(modal);
const previewUrl = file.previewUrl || file.parserUrl;
window.open(previewUrl, '_blank');
});
// 下载按钮事件
dialog.querySelector('#download-btn').addEventListener('click', () => {
document.body.removeChild(modal);
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = file.parserUrl;
document.body.appendChild(iframe);
setTimeout(() => {
document.body.removeChild(iframe);
}, 3000);
});
// 点击蒙层关闭
modal.addEventListener('click', (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
}
});
}
// 返回上一级
function goBack() {
if (pathStack.length > 1) {
pathStack.pop();
currentDir = pathStack[pathStack.length - 1];
fetchFileList(currentDir.url);
renderBreadcrumb();
}
}
// 跳转到指定目录
function goToDirectory(index) {
pathStack.splice(index + 1);
currentDir = pathStack[pathStack.length - 1];
fetchFileList(currentDir.url);
renderBreadcrumb();
}
// 格式化日期
function formatDate(dateString) {
if (!dateString) return '未知日期';
try {
const date = new Date(dateString);
return isNaN(date.getTime())
? '未知日期'
: `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
} catch {
return '未知日期';
}
}
</script>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -10,7 +10,7 @@
<img :height="150" src="../public/images/lanzou111.png" alt="lz"></img>
</div>
</div>
<h3 style="text-align: center;">NFD网盘直链解析0.1.8_bate32</h3>
<h3 style="text-align: center;">NFD网盘直链解析0.1.9_bate1</h3>
<div class="typo">
<p style="text-align: center;">
<span>
@@ -27,7 +27,8 @@
rel="nofollow"><u>QAIU博客</u>
</el-link>
</span></p>
<p><strong>目前支持 </strong>蓝奏云/蓝奏云优享/小飞机盘/123云盘/奶牛快传/移动云云空间/亿方云/文叔叔/QQ邮箱文件中转站</p>
<p><strong>文件解析支持 </strong>蓝奏云/蓝奏云优享/小飞机盘/123云盘/奶牛快传/移动云云空间/亿方云/文叔叔/QQ邮箱文件中转站</p>
<p><strong>目录解析支持 </strong>蓝奏云/蓝奏云优享/小飞机盘/123云盘接入中</p>
<p>已加入缓存机制, 如果遇到解析出的下载链接失效的情况请及时到<a href="https://github.com/qaiu/netdisk-fast-download/issues">
<u><strong>项目GitHub反馈</strong></u></a></p>
<p>节点1: 回源请求数:{{ node1Info.parserTotal }}, 缓存请求数:{{ node1Info.cacheTotal }}, 总数:{{ node1Info.total }}</p>
@@ -64,8 +65,10 @@
</el-input>
<p style="text-align: center">
<el-button style="margin-left: 40px;margin-bottom: 10px" @click="onSubmit">解析测试</el-button>
<el-button style="margin-left: 20px;margin-bottom: 10px" @click="genMd">生成Markdown链接</el-button>
<el-button style="margin-left: 40px" @click="onSubmit">文件解析</el-button>
<!-- 目录解析-->
<el-button style="margin-left: 20px" @click="getFileList">目录解析</el-button>
<el-button style="margin-left: 20px" @click="genMd">生成Markdown链接</el-button>
<el-button style="margin-left: 20px" @click="generateQRCode">扫码下载</el-button>
<el-button style="margin-left: 20px" @click="getTj">链接信息统计</el-button>
</p>
@@ -146,37 +149,6 @@ export default {
select: "lz",
respData: {},
tjData: {},
panList: [
{
name: "蓝奏云",
value: 'lz'
},
{
name: "奶牛快传",
value: 'cow'
},
{
name: "移动云空间",
value: 'ec'
},
{
name: "UC网盘",
value: 'uc',
disabled: true
},
{
name: "小飞机网盘",
value: 'fj'
},
{
name: "360亿方云",
value: 'fc'
},
{
name: "123云盘",
value: 'ye'
},
],
getLink: null,
getLink2: '',
getLinkInfo: null,
@@ -311,6 +283,28 @@ export default {
}
});
},
getFileList() {
this.check()
this.isLoading = true
axios.get(this.getLinkInfo, {params: {url: this.link, pwd: this.password}}).then(
response => {
this.isLoading = false
if (response.data.code === 200) {
this.$message({
message: response.data.msg,
type: 'success'
})
const data = response.data.data
const panList = ["iz", "lz", "fj"];
const listUrl = `${window.location.origin}/list.html?url=${encodeURIComponent(this.link)}&pwd=${this.password}`
if (panList.includes(data.shareLinkInfo.type)) {
window.open(listUrl, '_blank');
}
} else {
this.$message.error(response.data.msg)
}
});
},
async getPaste(v) {
const text = await navigator.clipboard.readText();

View File

@@ -20,14 +20,15 @@ import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL;
/**
* 程序入口
* vertx程序入口
*
* <br>Create date 2021-05-08 13:00:01
*
* @author qaiu
* @author qaiu yyzy
*/
public class AppMain {
public static void main(String[] args) {
// start
Deploy.instance().start(args, AppMain::exec);
}

View File

@@ -59,7 +59,7 @@ public class URLParamUtil {
boolean firstParam = !decodedUrl.contains("?");
for (String paramName : params.names()) {
if (!paramName.equals("url") && !paramName.equals("pwd")) { // 忽略 "url" 和 "pwd" 参数
if (!paramName.equals("url") && !paramName.equals("pwd") && !paramName.equals("dirId") && !paramName.equals("uuid")) { // 忽略 "url" 和 "pwd" 参数
if (firstParam) {
urlBuilder.append("?");
firstParam = false;

View File

@@ -26,6 +26,8 @@ import io.vertx.core.json.JsonObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
@@ -33,14 +35,8 @@ import java.util.stream.Collectors;
@Slf4j
public class ParserApi {
private final UserService userService = AsyncServiceUtil.getAsyncServiceInstance(UserService.class);
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
@RouteMapping(value = "/login", method = RouteMethod.GET)
public Future<SysUser> login(SysUser user) {
log.info("<------- login: {}", user.getUsername());
return userService.login(user);
}
@RouteMapping(value = "/statisticsInfo", method = RouteMethod.GET, order = 99)
public Future<StatisticsInfo> statisticsInfo() {
@@ -100,11 +96,17 @@ public class ParserApi {
}
@RouteMapping("/getFileList")
public Future<List<FileInfo>> getFileList(HttpServerRequest request, String pwd) {
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");
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
if (StringUtils.isNotBlank(dirId)) {
parserCreate.getShareLinkInfo().getOtherParam().put("dirId", dirId);
}
if (StringUtils.isNotBlank(uuid)) {
parserCreate.getShareLinkInfo().getOtherParam().put("uuid", uuid);
}
return parserCreate.createTool().parseFileList();
}
@@ -130,7 +132,6 @@ public class ParserApi {
return parserCreate.createTool().parseById();
}
@RouteMapping("/redirectUrl/:type/:param")
public Future<Void> redirectUrl(HttpServerResponse response, String type, String param) {
Promise<Void> promise = Promise.promise();
@@ -140,4 +141,18 @@ public class ParserApi {
.onFailure(t -> promise.fail(t.fillInStackTrace()));
return promise.future();
}
@RouteMapping("/viewUrl/:type/:param")
public Future<Void> viewUrl(HttpServerResponse response, String type, String param) {
Promise<Void> promise = Promise.promise();
String viewPrefix = SharedDataUtil.getJsonConfig("server").getString("previewURL");
getFileDownUrl(type, param)
.onSuccess(res -> {
String url = viewPrefix + URLEncoder.encode(res, StandardCharsets.UTF_8);
ResponseUtil.redirect(response, url);
})
.onFailure(t -> promise.fail(t.fillInStackTrace()));
return promise.future();
}
}

View File

@@ -6,6 +6,8 @@ server:
enableDatabase: true
# 服务域名或者IP 生成二维码链接时需要
domainName: http://127.0.0.1:6401
# 预览服务URL
previewURL: https://nfd-parser.github.io/nfd-preview/preview.html?src=
# domainName: https://lz.qaiu.top
# 反向代理服务器配置路径(不用加后缀)
@@ -38,7 +40,7 @@ rateLimit:
# 是否启用限流
enable: true
# 限流的请求数
limit: 5
limit: 10
# 限流的时间窗口(单位秒)
timeWindow: 10
# 路径匹配规则

View File

@@ -119,3 +119,19 @@ Platform:web
{"ShareKey":"iaKtVv-6OECd","behavior":1}
### eaefamemdead
https://www.123684.com/b/api/share/get?2393880368=1749017805-9397896-3041863692&limit=100&next=0&orderBy=file_name&orderDirection=asc&shareKey=LH3rTd-pENed&SharePwd=qaiu&ParentFileId=0&Page=1&event=homeListFile&operateType=1
Referer: https://www.123684.com/s/LH3rTd-pENed
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0
platform: web
sec-ch-ua: "Chromium";v="136", "Microsoft Edge";v="136", "Not.A/Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
###
POST https://www.123684.com/b/api/file/batch_download_share_info?3697171543=1749019332-221279-1801740277
Content-Type: application/json;charset=UTF-8
{"ShareKey":"LH3rTd-pENed","fileIdList":[{"fileId":17525951}]}