Compare commits

...

7 Commits

Author SHA1 Message Date
qaiu 0fd78defcb feat(ct): 升级城通网盘接口并实现目录分享解析
- 移除token参数,新增url参数至API1请求
- API2新增start_time/wait_seconds/verifycode/share_id/acheck=1参数
- parse()中从API1响应提取FileInfo(file_name/file_id/file_size/file_time/username)
- 新增parseFileList()实现目录分享解析,通过getdir.php+file_list API获取文件列表
- 新增CTD枚举项匹配城通网盘目录分享链接(/d/路径)
2026-06-03 09:46:16 +00:00
qaiu 452fd0ea2c Merge pull request #190 from yukaidi1220/pr/native-package
CI: 新增原生环境打包 & 启动日志改进
2026-05-31 17:02:50 +08:00
yukaidi dd8f2efb37 ci: Release 自动生成更新说明(generate_release_notes) 2026-05-31 16:39:23 +08:00
yukaidi 0feb8e798a fix: PR#190 review 修复 — 配置查找顺序/页面日志/ZIP结构/注释
- 配置文件查找顺序与 Deploy 保持一致(先当前目录,再 resources/)
- 页面地址日志改用 onComplete,无论演练场加载成功失败均输出
- Windows ZIP 移除 /* 通配符,与 Linux 保持一致的顶层目录结构
- 修正注释:同步读文件会阻塞 event loop,不再声称'避免阻塞'
- YAML 正则和 jdeps 回退列表补充适用范围说明
2026-05-31 16:34:19 +08:00
yukaidi d55d8edd2f AppMain: 启动日志增加前端页面访问地址提示 2026-05-31 14:00:45 +08:00
yukaidi 451496f102 CI: 新增 Linux/Windows 原生环境打包 (jlink + 精简 JRE) 及 Docker 多平台构建 2026-05-31 14:00:45 +08:00
qaiu 6d6351bd58 更新 README.md 2026-05-31 07:02:09 +08:00
6 changed files with 430 additions and 60 deletions
+208 -38
View File
@@ -1,14 +1,5 @@
# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time name: Java CIMaven 构建 + Docker 镜像 + 原生环境打包)
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Java CI with Maven
# The API requires write permission on the repository to submit dependencies
permissions: permissions:
contents: write contents: write
packages: write packages: write
@@ -16,9 +7,9 @@ permissions:
on: on:
push: push:
tags: tags:
- '*' # 只有推送tag时才会触发构建 - '*'
branches-ignore: branches-ignore:
- '*' # 排除所有分支的提交 - '*'
paths-ignore: paths-ignore:
- 'bin/**' - 'bin/**'
- '.github/**' - '.github/**'
@@ -32,71 +23,250 @@ on:
- "main" - "main"
jobs: jobs:
# ================================================================
# 阶段一:构建前端 + Maven 打包(只执行一次,产物共享)
# ================================================================
build: build:
name: 编译构建
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - name: 检出代码
uses: actions/checkout@v3
- uses: actions/setup-node@v4 - name: 设置 Node.js 18
uses: actions/setup-node@v4
with: with:
node-version: '18' node-version: '18'
- name: Set up JDK 17 - name: 设置 JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
java-version: '17' java-version: '17'
distribution: 'temurin' distribution: 'temurin'
cache: maven cache: maven
- name: Build Frontend - name: 构建前端
run: cd web-front && yarn install && yarn run build run: cd web-front && yarn install && yarn run build
- name: Build with Maven - name: Maven 编译打包
run: mvn -B package -DskipTests --file pom.xml run: mvn -B package -DskipTests --file pom.xml
# Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - name: 更新依赖图谱
- name: Update dependency graph
uses: advanced-security/maven-dependency-submission-action@v3 uses: advanced-security/maven-dependency-submission-action@v3
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
continue-on-error: true continue-on-error: true
with: with:
ignore-maven-wrapper: true ignore-maven-wrapper: true
# - uses: release-drafter/release-drafter@v5 - name: 分享应用打包目录(供原生包和 Docker 复用)
# env: if: github.event_name != 'pull_request'
# GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
- name: Upload Artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: app-package
path: web-service/target/package/
- name: 分享 bin-zip(供 Docker 复用)
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: app-bin-zip
path: web-service/target/netdisk-fast-download-bin.zip path: web-service/target/netdisk-fast-download-bin.zip
- name: Login to GitHub Container Registry # ================================================================
if: github.event_name != 'pull_request' # 阶段二-A:Docker 镜像构建(并行)
# ================================================================
docker:
name: Docker 镜像
needs: build
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v3
- name: 下载 bin-zip 产物
uses: actions/download-artifact@v4
with:
name: app-bin-zip
path: web-service/target/
- name: 登录 GitHub 容器仓库
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: 设置 QEMU(多平台构建支持)
uses: docker/setup-qemu-action@v3
- name: 设置 Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Extract git tag - name: 构建并推送 Docker 镜像
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 uses: docker/build-push-action@v5
with: with:
context: . context: .
push: true push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: | tags: |
ghcr.io/${{ github.repository }}:${{ steps.tag.outputs.tag }} ghcr.io/${{ github.repository }}:${{ github.ref_name }}
ghcr.io/${{ github.repository }}:latest ghcr.io/${{ github.repository }}:latest
# ================================================================
# 阶段二-B:原生环境打包 Linux + Windows(并行)
# ================================================================
native-package:
name: 原生环境打包 → ${{ matrix.artifact-name }}
needs: build
if: github.event_name != 'pull_request'
strategy:
matrix:
include:
- os: ubuntu-latest
artifact-name: netdisk-fast-download-linux-amd64
- os: windows-latest
artifact-name: netdisk-fast-download-windows-amd64
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- name: 设置 JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: 下载 Maven 构建产物
uses: actions/download-artifact@v4
with:
name: app-package
path: web-service/target/package
# ============================================================
# jdeps 分析 → 确定所需 JDK 模块
# ============================================================
- name: 分析所需 JDK 模块(jdeps
run: |
MAIN_JAR="web-service/target/package/netdisk-fast-download.jar"
LIB_DIR="web-service/target/package/lib"
CP=""
for jar in "$LIB_DIR"/*.jar; do
CP="$CP${CP:+:}$jar"
done
RAW_MODULES=$(jdeps --print-module-deps --ignore-missing-deps --multi-release 17 \
--class-path "$CP" "$MAIN_JAR" 2>/dev/null | head -n 1 | tr -d '\r\n' || true)
if [ -z "$RAW_MODULES" ] || [[ "$RAW_MODULES" == *"Missing"* ]] || [[ "$RAW_MODULES" == *"Error"* ]]; then
# ⚠️ 回退列表:若项目新增了需要 java.* / jdk.* 模块的依赖,需同步更新此处
RAW_MODULES="java.base,java.logging,java.sql,java.naming,java.management,java.xml,jdk.unsupported,java.net.http,java.instrument,java.security.jgss,java.security.sasl,java.desktop,jdk.crypto.ec"
echo "jdeps 分析失败,使用回退模块列表"
else
# 补上 jdeps 无法检测的反射/SPI依赖
RAW_MODULES="$RAW_MODULES,java.desktop,jdk.crypto.ec"
fi
echo "detected modules: $RAW_MODULES"
printf 'JDK_MODULES=%s\n' "$RAW_MODULES" >> $GITHUB_ENV
# ============================================================
# jlink 生成精简 JRE
# ============================================================
- name: 生成精简 JREjlink
run: |
jlink \
--module-path "$JAVA_HOME/jmods" \
--add-modules "$JDK_MODULES" \
--output "native-package/netdisk-fast-download/jre" \
--strip-debug \
--compress=2 \
--no-header-files \
--no-man-pages
echo "JRE size:"
du -sh native-package/netdisk-fast-download/jre || true
# Windows: 确保 MSVC 运行时 DLL 到位
if [[ "$RUNNER_OS" == "Windows" ]]; then
JRE_BIN="native-package/netdisk-fast-download/jre/bin"
for dll in vcruntime140.dll msvcp140.dll vcruntime140_1.dll; do
if [ ! -f "$JRE_BIN/$dll" ] && [ -f "$JAVA_HOME/bin/$dll" ]; then
echo "jlink 未包含 $dll,从 JDK 补拷"
cp "$JAVA_HOME/bin/$dll" "$JRE_BIN/"
fi
done
echo "=== JRE bin 目录 DLL 清单 ==="
ls -la "$JRE_BIN"/*.dll 2>/dev/null || echo "(无 .dll 文件)"
fi
# ============================================================
# 组装包目录
# ============================================================
- name: 组装包目录
run: |
PKG="native-package/netdisk-fast-download"
SRC="web-service/target/package"
cp "$SRC/netdisk-fast-download.jar" "$PKG/"
cp -r "$SRC/lib" "$PKG/"
cp -r "$SRC/resources" "$PKG/"
cp -r "$SRC/webroot" "$PKG/"
mkdir -p "$PKG/db"
mkdir -p "$PKG/logs"
# ============================================================
# 生成启动脚本
# ============================================================
- name: 生成启动脚本(Linux
run: |
PKG="native-package/netdisk-fast-download"
echo '#!/bin/bash' > "$PKG/run.sh"
echo 'DIR="$(cd "$(dirname "$0")" && pwd)"' >> "$PKG/run.sh"
echo 'cd "$DIR" || exit 1' >> "$PKG/run.sh"
echo 'exec "$DIR/jre/bin/java" -Xmx512M -Dfile.encoding=utf-8 -jar "$DIR/netdisk-fast-download.jar" "$@"' >> "$PKG/run.sh"
chmod +x "$PKG/run.sh"
- name: 生成启动脚本(Windows
run: |
PKG="native-package/netdisk-fast-download"
echo '@echo off' > "$PKG/run.bat"
echo 'chcp 65001 > nul' >> "$PKG/run.bat"
echo 'pushd %~dp0' >> "$PKG/run.bat"
echo '"%~dp0jre\bin\java.exe" -Xmx512M -Dfile.encoding=utf-8 -jar "%~dp0netdisk-fast-download.jar" %*' >> "$PKG/run.bat"
# ============================================================
# 打包为 zip
# ============================================================
- name: 打包 ZIPLinux
if: runner.os == 'Linux'
run: |
cd native-package
zip -r "../${{ matrix.artifact-name }}.zip" netdisk-fast-download/
- name: 打包 ZIPWindows
if: runner.os == 'Windows'
shell: pwsh
run: |
Compress-Archive -Path native-package/netdisk-fast-download -DestinationPath "${{ matrix.artifact-name }}.zip"
# ============================================================
# 上传产物
# ============================================================
- name: 上传原生安装包
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact-name }}
path: ${{ matrix.artifact-name }}.zip
- name: 上传到 Release
uses: softprops/action-gh-release@v2
with:
files: ${{ matrix.artifact-name }}.zip
tag_name: ${{ github.ref_name }}
generate_release_notes: true
+1
View File
@@ -92,3 +92,4 @@ yarn-error.log*
**/${project.build.directory}/ **/${project.build.directory}/
**/${project.basedir}/target/ **/${project.basedir}/target/
**/${basedir}/target/ **/${basedir}/target/
.spec-workflow/
+1 -1
View File
@@ -1,7 +1,7 @@
# 一款网盘分享链接云解析快速下载服务 # 一款网盘分享链接云解析快速下载服务
QQ交流群:1017480890 QQ交流群:1017480890
<p align="center"> <p align="center">
<a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://img.shields.io/github/actions/workflow/status/qaiu/netdisk-fast-download/maven.yml?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/qaiu/netdisk-fast-download/build.yml?branch=main&style=flat"></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://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.27-blue?style=flat"></a> <a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.27-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> <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>
@@ -249,7 +249,13 @@ public enum PanDomainTemplate {
CT("城通网盘", CT("城通网盘",
compile("https://(?:[a-zA-Z\\d-]+\\.)?(ctfile|545c|u062|ghpym|474b)\\.com/f(ile)?/" + compile("https://(?:[a-zA-Z\\d-]+\\.)?(ctfile|545c|u062|ghpym|474b)\\.com/f(ile)?/" +
"(?<KEY>[0-9a-zA-Z_-]+)(\\?p=(?<PWD>\\w+))?"), "(?<KEY>[0-9a-zA-Z_-]+)(\\?p=(?<PWD>\\w+))?"),
"https://474b.com/file/{shareKey}", "https://ctfile.com/file/{shareKey}",
CtTool.class),
// https://url94.ctfile.com/d/64115194-164803691-48508c?p=7609&d=164803691&fk=decb36
CTD("城通网盘-目录",
compile("https://(?:[a-zA-Z\\d-]+\\.)?(ctfile|545c|u062|ghpym|474b)\\.com/d/" +
"(?<KEY>[0-9a-zA-Z_-]+)(\\?p=(?<PWD>\\w+))?"),
"https://ctfile.com/d/{shareKey}",
CtTool.class), CtTool.class),
// https://www.vyuyun.com/s/QMa6ie?password=I4KG7H // https://www.vyuyun.com/s/QMa6ie?password=I4KG7H
// https://www.vyuyun.com/s/QMa6ie/file?password=I4KG7H // https://www.vyuyun.com/s/QMa6ie/file?password=I4KG7H
@@ -1,16 +1,23 @@
package cn.qaiu.parser.impl; package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo; import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase; import cn.qaiu.parser.PanBase;
import cn.qaiu.util.RandomStringGenerator; import cn.qaiu.util.FileSizeConverter;
import io.vertx.core.Future; import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.buffer.Buffer; import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpRequest; import io.vertx.ext.web.client.HttpRequest;
import io.vertx.uritemplate.UriTemplate; import io.vertx.uritemplate.UriTemplate;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* <a href="https://www.ctfile.com">诚通网盘</a> * <a href="https://www.ctfile.com">诚通网盘</a>
@@ -18,19 +25,34 @@ import java.util.Map;
public class CtTool extends PanBase { public class CtTool extends PanBase {
private static final String API_URL_PREFIX = "https://webapi.ctfile.com"; private static final String API_URL_PREFIX = "https://webapi.ctfile.com";
// https://webapi.ctfile.com/getfile.php?path=f&f=55050874-1246660795-6464f6& // https://webapi.ctfile.com/getfile.php?path=f&f=64115194-17569800420720-06c697&
// passcode=7548&token=30wiijxs1fzhb6brw0p9m6&r=0.5885881231735761& // passcode=7609&r=0.6611183001986635&ref=&url=https%3A%2F%2Furl94.ctfile.com%2Ff%2F64115194-17569800420720-06c697%3Fp%3D7609
// ref=&url=https%3A%2F%2F474b.com%2Ff%2F55050874-1246660795-6464f6%3Fp%3D7548
private static final String API1 = API_URL_PREFIX + "/getfile.php?path={path}" + private static final String API1 = API_URL_PREFIX + "/getfile.php?path={path}" +
"&f={shareKey}&passcode={pwd}&token={token}&r={rand}&ref="; "&f={shareKey}&passcode={pwd}&r={rand}&ref=&url={url}";
//https://webapi.ctfile.com/get_file_url.php?uid=55050874&fid=1246660795&folder_id=0& // https://webapi.ctfile.com/get_file_url.php?uid=64115194&fid=17569800420720&folder_id=0&
// file_chk=054bc20461f5c63ff82015b9e69fb7fc&mb=1&token=30wiijxs1fzhb6brw0p9m6&app=0& // share_id=&file_chk=af5c8757a49cbc69a557eb3da59b246c&start_time=1780471868&wait_seconds=0&
// acheck=1&verifycode=&rd=0.965929071503574 // mb=0&app=0&acheck=1&verifycode=1780471868.2951fe63abedf36ec02f34ed5711ce70&rd=0.36350981353622636
private static final String API2 = API_URL_PREFIX + "/get_file_url.php?" + private static final String API2 = API_URL_PREFIX + "/get_file_url.php?" +
"uid={uid}&fid={fid}&folder_id=0&file_chk={file_chk}&mb=0&token={token}&app=0&acheck=0&verifycode=" + "uid={uid}&fid={fid}&folder_id=0&share_id=&file_chk={file_chk}" +
"&rd={rand}"; "&start_time={start_time}&wait_seconds={wait_seconds}&mb=0&app=0&acheck=1" +
"&verifycode={verifycode}&rd={rand}";
// https://webapi.ctfile.com/getdir.php?path=d&d=64115194-164803691-48508c&
// folder_id=164803691&fk=decb36&passcode=7609&r=0.23...&ref=&url=https://url94.ctfile.com/d/...
private static final String API_GETDIR = API_URL_PREFIX + "/getdir.php?path={path}" +
"&d={shareKey}&folder_id={folder_id}&fk={fk}&passcode={pwd}&r={rand}&ref=&url={url}";
// DataTables参数,用于获取目录文件列表
private static final String FILE_LIST_PARAMS = "&sEcho=1&iColumns=4&sColumns=%2C%2C%2C" +
"&iDisplayStart=0&iDisplayLength=500&mDataProp_0=0&mDataProp_1=1&mDataProp_2=2&mDataProp_3=3" +
"&iSortCol_0=3&sSortDir_0=desc&iSortingCols=1";
// 文件列表HTML解析正则
private static final Pattern FILE_ID_PATTERN = Pattern.compile("value=\"f(\\d+)\"");
private static final Pattern FILE_HREF_PATTERN = Pattern.compile("href=\"#/f/([^\"]+)\"");
private static final Pattern FILE_NAME_PATTERN = Pattern.compile(">([^<]+)</a>");
private static final Pattern FILE_ICON_PATTERN = Pattern.compile("alt=\"([^\"]+)\"");
/** /**
* 子类重写此构造方法不需要添加额外逻辑 * 子类重写此构造方法不需要添加额外逻辑
@@ -57,7 +79,6 @@ public class CtTool extends PanBase {
} }
String[] split = shareKey.split("-"); String[] split = shareKey.split("-");
String uid = split[0], fid = split[1]; String uid = split[0], fid = split[1];
String token = RandomStringGenerator.generateRandomString();
// 获取url path // 获取url path
int i1 = shareLinkInfo.getShareUrl().indexOf("com/"); int i1 = shareLinkInfo.getShareUrl().indexOf("com/");
int i2 = shareLinkInfo.getShareUrl().lastIndexOf("/"); int i2 = shareLinkInfo.getShareUrl().lastIndexOf("/");
@@ -67,8 +88,8 @@ public class CtTool extends PanBase {
.setTemplateParam("path", path) .setTemplateParam("path", path)
.setTemplateParam("shareKey", shareKey) .setTemplateParam("shareKey", shareKey)
.setTemplateParam("pwd", shareLinkInfo.getSharePassword()) .setTemplateParam("pwd", shareLinkInfo.getSharePassword())
.setTemplateParam("token", token) .setTemplateParam("rand", String.valueOf(Math.random()))
.setTemplateParam("r", Math.random() + ""); .setTemplateParam("url", shareLinkInfo.getShareUrl());
bufferHttpRequest1 bufferHttpRequest1
.send().onSuccess(res -> { .send().onSuccess(res -> {
@@ -77,23 +98,40 @@ public class CtTool extends PanBase {
var fileJson = resJson.getJsonObject("file"); var fileJson = resJson.getJsonObject("file");
if (fileJson.containsKey("file_chk")) { if (fileJson.containsKey("file_chk")) {
var file_chk = fileJson.getString("file_chk"); var file_chk = fileJson.getString("file_chk");
String startTime = fileJson.getValue("start_time").toString();
String waitSeconds = fileJson.getValue("wait_seconds").toString();
String verifycode = fileJson.getString("verifycode");
// 提取文件信息并存储
FileInfo fileInfo = new FileInfo()
.setFileName(fileJson.getString("file_name"))
.setFileId(String.valueOf(fileJson.getLong("file_id", 0L)))
.setSizeStr(fileJson.getString("file_size"))
.setCreateTime(fileJson.getString("file_time"))
.setCreateBy(fileJson.getString("username"))
.setFileType("file")
.setPanType(shareLinkInfo.getType());
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
HttpRequest<Buffer> bufferHttpRequest2 = clientSession.getAbs(UriTemplate.of(API2)) HttpRequest<Buffer> bufferHttpRequest2 = clientSession.getAbs(UriTemplate.of(API2))
.setTemplateParam("uid", uid) .setTemplateParam("uid", uid)
.setTemplateParam("fid", fid) .setTemplateParam("fid", fid)
.setTemplateParam("file_chk", file_chk) .setTemplateParam("file_chk", file_chk)
.setTemplateParam("token", token) .setTemplateParam("start_time", startTime)
.setTemplateParam("rd", Math.random() + ""); .setTemplateParam("wait_seconds", waitSeconds)
.setTemplateParam("verifycode", verifycode)
.setTemplateParam("rand", String.valueOf(Math.random()));
bufferHttpRequest2 bufferHttpRequest2
.send().onSuccess(res2 -> { .send().onSuccess(res2 -> {
JsonObject resJson2 = asJson(res2); JsonObject resJson2 = asJson(res2);
if (resJson2.containsKey("downurl")) { if (resJson2.containsKey("downurl")) {
String downloadUrl = resJson2.getString("downurl"); String downloadUrl = resJson2.getString("downurl");
// 存储下载元数据,包括必要的请求头 // 存储下载元数据,包括必要的请求头
Map<String, String> headers = new HashMap<>(); Map<String, String> headers = new HashMap<>();
headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
headers.put("Referer", shareLinkInfo.getShareUrl()); headers.put("Referer", shareLinkInfo.getShareUrl());
// 使用新的 completeWithMeta 方法 // 使用新的 completeWithMeta 方法
completeWithMeta(downloadUrl, headers); completeWithMeta(downloadUrl, headers);
} else { } else {
@@ -109,4 +147,132 @@ public class CtTool extends PanBase {
}).onFailure(handleFail(bufferHttpRequest1.queryParams().toString())); }).onFailure(handleFail(bufferHttpRequest1.queryParams().toString()));
return promise.future(); return promise.future();
} }
@Override
public Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> listPromise = Promise.promise();
final String shareKey = shareLinkInfo.getShareKey();
final String shareUrl = shareLinkInfo.getShareUrl();
final String pwd = shareLinkInfo.getSharePassword();
// shareKey格式: uid-folder_id-hash (例如 64115194-164803691-48508c)
String[] split = shareKey.split("-");
if (split.length < 2) {
listPromise.fail(baseMsg() + " shareKey格式不正确: " + shareKey);
return listPromise.future();
}
String folderId = split[1];
// 从分享URL中提取fk参数
String fk = extractQueryParam(shareUrl, "fk");
// 从URL中提取path (例如从 "https://url94.ctfile.com/d/xxx?p=..." 中提取 "d")
int comIdx = shareUrl.indexOf("com/");
int qIdx = shareUrl.indexOf('?');
String pathAndKey = qIdx > 0 ? shareUrl.substring(comIdx + 4, qIdx) : shareUrl.substring(comIdx + 4);
int slashIdx = pathAndKey.indexOf('/');
String path = slashIdx > 0 ? pathAndKey.substring(0, slashIdx) : pathAndKey;
clientSession.getAbs(UriTemplate.of(API_GETDIR))
.setTemplateParam("path", path)
.setTemplateParam("shareKey", shareKey)
.setTemplateParam("folder_id", folderId)
.setTemplateParam("fk", fk != null ? fk : "")
.setTemplateParam("pwd", pwd != null ? pwd : "")
.setTemplateParam("rand", String.valueOf(Math.random()))
.setTemplateParam("url", shareUrl)
.send().onSuccess(res -> {
var resJson = asJson(res);
if (!resJson.containsKey("file")) {
listPromise.fail(baseMsg() + " 目录解析失败: " + resJson.encode());
return;
}
var dirInfo = resJson.getJsonObject("file");
String fileListRelUrl = dirInfo.getString("url");
if (fileListRelUrl == null) {
listPromise.fail(baseMsg() + " 文件列表URL为空");
return;
}
String fileListUrl = API_URL_PREFIX + fileListRelUrl + FILE_LIST_PARAMS;
clientSession.getAbs(fileListUrl)
.send().onSuccess(res2 -> {
var listJson = asJson(res2);
JsonArray aaData = listJson.getJsonArray("aaData");
if (aaData == null) {
listPromise.fail(baseMsg() + " 文件列表为空");
return;
}
List<FileInfo> fileList = new ArrayList<>();
String panType = shareLinkInfo.getType();
for (int i = 0; i < aaData.size(); i++) {
var row = aaData.getJsonArray(i);
try {
String checkboxHtml = row.getString(0);
String nameCellHtml = row.getString(1);
String sizeStr = row.getString(2).trim();
// 从checkbox HTML中提取文件ID
String fileId = null;
Matcher idMatcher = FILE_ID_PATTERN.matcher(checkboxHtml);
if (idMatcher.find()) fileId = idMatcher.group(1);
// 从文件名单元格HTML中提取临时分享key
String fileShareKey = null;
Matcher hrefMatcher = FILE_HREF_PATTERN.matcher(nameCellHtml);
if (hrefMatcher.find()) fileShareKey = hrefMatcher.group(1);
// 提取文件名
String fileName = null;
Matcher nameMatcher = FILE_NAME_PATTERN.matcher(nameCellHtml);
if (nameMatcher.find()) fileName = nameMatcher.group(1).trim();
// 提取文件图标/类型
String fileIcon = null;
Matcher iconMatcher = FILE_ICON_PATTERN.matcher(nameCellHtml);
if (iconMatcher.find()) fileIcon = iconMatcher.group(1);
if (fileName == null || fileShareKey == null) continue;
long sizeBytes = 0;
try {
sizeBytes = FileSizeConverter.convertToBytes(sizeStr);
} catch (Exception ignored) {}
FileInfo fileInfo = new FileInfo()
.setFileName(fileName)
.setFileId(fileId)
.setSizeStr(sizeStr)
.setSize(sizeBytes)
.setFileType(fileIcon)
.setFileIcon(fileIcon)
.setPanType(panType)
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s",
getDomainName(), panType, fileShareKey));
fileList.add(fileInfo);
} catch (Exception e) {
log.warn("解析文件行失败: {}", e.getMessage());
}
}
listPromise.complete(fileList);
}).onFailure(listPromise::fail);
}).onFailure(listPromise::fail);
return listPromise.future();
}
private String extractQueryParam(String url, String paramName) {
if (url == null) return null;
int qIdx = url.indexOf('?');
if (qIdx < 0) return null;
String query = url.substring(qIdx + 1);
for (String param : query.split("&")) {
int eqIdx = param.indexOf('=');
if (eqIdx > 0 && param.substring(0, eqIdx).equals(paramName)) {
return param.substring(eqIdx + 1);
}
}
return null;
}
} }
@@ -21,6 +21,8 @@ import io.vertx.core.shareddata.LocalMap;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateFormatUtils; import org.apache.commons.lang3.time.DateFormatUtils;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Date; import java.util.Date;
import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL; import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL;
@@ -78,12 +80,35 @@ public class AppMain {
System.out.println("数据库连接成功"); System.out.println("数据库连接成功");
// 加载演练场解析器 // 加载演练场解析器
loadPlaygroundParsers();
String addr = jsonObject.getJsonObject(ConfigConstant.SERVER).getString("domainName"); String addr = jsonObject.getJsonObject(ConfigConstant.SERVER).getString("domainName");
if (addr == null || addr.isBlank()) { if (addr == null || addr.isBlank()) {
addr = "http://127.0.0.1:" + jsonObject.getJsonObject(ConfigConstant.SERVER).getInteger("port", 6400); addr = "http://127.0.0.1:" + jsonObject.getJsonObject(ConfigConstant.SERVER).getInteger("port", 6400);
} }
// 读取代理配置获取前端页面端口(同步读取小文件,仅启动时执行一次)
String proxyConfName = jsonObject.getString("proxyConf", "server-proxy");
String pageAddr = addr;
try {
String configFile = proxyConfName + ".yml";
// 与 Deploy 保持一致:优先当前目录,其次 resources/
Path configPath = Path.of(configFile);
if (!Files.exists(configPath)) {
configPath = Path.of("resources", configFile);
}
if (Files.exists(configPath)) {
String yamlContent = Files.readString(configPath);
// 匹配项目约定的 YAML 格式: "- listen: 8080"
java.util.regex.Matcher m = java.util.regex.Pattern
.compile("^\\s*-\\s+listen:\\s*(\\d+)", java.util.regex.Pattern.MULTILINE)
.matcher(yamlContent);
if (m.find()) {
int pagePort = Integer.parseInt(m.group(1));
pageAddr = "http://127.0.0.1:" + pagePort;
}
}
} catch (Exception e) {
log.warn("读取代理配置失败,使用默认页面地址: {}", e.getMessage());
}
loadPlaygroundParsers(pageAddr);
System.out.println("启动成功: \n本地服务地址: " + addr); System.out.println("启动成功: \n本地服务地址: " + addr);
}); });
}); });
@@ -125,7 +150,7 @@ public class AppMain {
/** /**
* 在启动时加载所有已发布的演练场解析器 * 在启动时加载所有已发布的演练场解析器
*/ */
private static void loadPlaygroundParsers() { private static void loadPlaygroundParsers(String accessAddr) {
DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class); DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
dbService.getPlaygroundParserList().onSuccess(result -> { dbService.getPlaygroundParserList().onSuccess(result -> {
@@ -165,6 +190,8 @@ public class AppMain {
} }
}).onFailure(e -> { }).onFailure(e -> {
log.error("加载演练场解析器列表失败", e); log.error("加载演练场解析器列表失败", e);
}).onComplete(ar -> {
log.info("服务已启动,可通过 {} 访问页面", accessAddr);
}); });
} }
} }