Compare commits
8
Commits
b1d4fed408
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f227969d23 | ||
|
|
abb4e54498 | ||
|
|
87f2fec4d9 | ||
|
|
bdac62ac45 | ||
|
|
644933e2cc | ||
|
|
30f2c04d13 | ||
|
|
15357768c7 | ||
|
|
8933b4c652 |
No files matched your search
@@ -1,20 +1,33 @@
|
||||
1.5-SNAPSHOT
|
||||
1.9-SNAPSHOT
|
||||
|
||||
此版本包含:
|
||||
1. 压缩/解压 (`compress`)
|
||||
1.1 Tar.Gz / Zip 压缩与解压
|
||||
1.2 PNG 图片压缩
|
||||
2. 终端彩色输出 (`echo`)
|
||||
2.1 预定义颜色打印 — `ColorPrintln`
|
||||
2.2 自定义颜色打印 — `ColorHexPrintln`
|
||||
2.3 Minecraft 风格多色打印 — `ColorPrintlnPlus`
|
||||
2.3 Minecraft 风格多色打印 — `ColorPrintlnPlus`(格式码支持叠加)
|
||||
3. 文件创建与保存 (`file/CreateFile`)
|
||||
4. 文件加密/解密 (`file/EncryptionAndDecryption`)
|
||||
5. 随机生成器 (`random`)
|
||||
6. 硬件标识生成器 (`hwid`)
|
||||
7. 版本信息 (`version`)
|
||||
8. 运行时信息 (`runtime`)
|
||||
6.1 获取运行时临时目录
|
||||
6.2 新建临时目录
|
||||
6.3 获取临时目录
|
||||
6.4 获取当前运行的JAR文件路径
|
||||
4.1 AES-256/GCM 加密
|
||||
4.2 RSA-2048/OAEP 混合加密
|
||||
4.3 DES 加密(已废弃,仅兼容旧版本)
|
||||
5. 文件下载 (`file/download`)
|
||||
5.1 单线程下载
|
||||
5.2 多线程下载(HTTP Range 分块并行下载,支持断点续传)
|
||||
6. 随机生成器 (`random`)
|
||||
7. 硬件标识生成器 (`hwid`)
|
||||
8. 版本信息 (`version`)
|
||||
9. 运行时信息 (`runtime`)
|
||||
9.1 获取运行时临时目录
|
||||
9.2 新建临时目录
|
||||
9.3 获取当前运行的 JAR 文件路径
|
||||
|
||||
在 1.4-SNAPSHOT 的基础上调整了API接口,修复了一些BUG
|
||||
1.9 更新内容:
|
||||
- 新增文件下载模块 (`file/download`)
|
||||
- 支持单线程下载和多线程并行下载(基于 HTTP Range 请求)
|
||||
- 多线程下载自动创建父目录,预分配文件空间
|
||||
- 使用 Java HttpClient 标准库,支持自动重定向
|
||||
|
||||
在 1.8-SNAPSHOT 的基础上进行版本升级。
|
||||
@@ -1,6 +1,6 @@
|
||||
# GNU Java Utils Library
|
||||
|
||||
一个轻量级 Java 工具库,提供压缩/解压、终端彩色输出、文件创建与管理、文件加密/解密、随机数生成、硬件标识等功能。
|
||||
一个轻量级 Java 工具库,提供压缩/解压、终端彩色输出、文件创建与管理、文件加密/解密、文件下载(单线程/多线程)、随机数生成、硬件标识等功能。
|
||||
|
||||
> 注:此项目/仓库已同步到 Github:[https://github.com/databasemysql233-debug/gnu_java_utils_library.git](https://github.com/databasemysql233-debug/gnu_java_utils_library.git)
|
||||
> 原项目/仓库链接:[https://git.opensource-studio.info/database-mysql/gnu_java_utils_library.git](https://git.opensource-studio.info/database-mysql/gnu_java_utils_library.git)
|
||||
@@ -23,7 +23,9 @@ src/main/java/top/gtb520/java/gnu/java/utils/library/
|
||||
│ └── compress.java # 压缩/解压工具类
|
||||
├── file/
|
||||
│ ├── CreateFile.java # 文件创建/保存工具类
|
||||
│ └── EncryptionAndDecryption.java # 文件加密/解密工具类
|
||||
│ ├── EncryptionAndDecryption.java # 文件加密/解密工具类
|
||||
│ └── download/
|
||||
│ └── file.java # 文件下载工具类(单线程/多线程)
|
||||
├── print/
|
||||
│ ├── echo.java # 终端彩色输出工具类
|
||||
│ └── ColorNameEnum.java # 颜色名称枚举
|
||||
@@ -41,7 +43,7 @@ src/main/java/top/gtb520/java/gnu/java/utils/library/
|
||||
|
||||
### 1. 压缩/解压 (`compress`)
|
||||
|
||||
支持 **Tar.Gz** 和 **Zip** 两种格式的压缩与解压,支持单文件和目录递归处理。
|
||||
支持 **Tar.Gz** 和 **Zip** 两种格式的压缩与解压,支持单文件和目录递归处理;另提供 PNG 图片压缩。
|
||||
|
||||
| 方法 | 说明 | 参数 |
|
||||
|------------------------------------|--------------------------|-------------------------------------|
|
||||
@@ -49,6 +51,10 @@ src/main/java/top/gtb520/java/gnu/java/utils/library/
|
||||
| `compress.UnTarGz(source, dest)` | 解压 `.tar.gz` 到指定目录 | `source` 压缩包路径,`dest` 解压目标目录 |
|
||||
| `compress.MakeZip(source, dest)` | 将文件或目录压缩为 `.zip` | `source` 源路径,`dest` 压缩包输出路径 |
|
||||
| `compress.UnZip(source, dest)` | 解压 `.zip` 到指定目录 | `source` 压缩包路径,`dest` 解压目标目录 |
|
||||
| `CompressPNG.PNG(pngFile, threshold)` | 压缩 PNG 图片,返回原文件 | `pngFile` PNG 文件,`threshold` 压缩阈值(1~5 档) |
|
||||
|
||||
> **安全说明**:解压时会校验条目路径,压缩包内含 `../` 越界路径或符号/硬链接条目将抛出
|
||||
> `IllegalArgumentException`(防 Zip-Slip 攻击)。PNG 压缩采用临时文件 + 原子替换,写入失败不会损坏原图。
|
||||
|
||||
**使用示例:**
|
||||
|
||||
@@ -195,12 +201,12 @@ main.echo.ColorPrintlnPlus("&a多色 &c文本");
|
||||
|
||||
### 3. 文件创建与保存 (`file/CreateFile`)
|
||||
|
||||
支持创建普通/二进制文件,以及异步字节流文件保存。
|
||||
支持创建普通/二进制文件,以及字节流文件保存。
|
||||
|
||||
| 方法 | 说明 | 参数 |
|
||||
|-------------------------------------------------------|---------------------------------------|-------------------------------------------------------------|
|
||||
| `CreateFile.NewFile(FileName, isBin, binFileType)` | 创建空白文件,二进制文件自动写入文件头 | `FileName` 文件名,`isBin` 是否二进制,`binFileType` 二进制类型(普通文件传 `null`) |
|
||||
| `CreateFile.StreamSaveFile(file, TargePath, FileName)` | 以字节流方式异步保存文件到目标路径 | `file` 源文件,`TargePath` 目标目录,`FileName` 保存的文件名 |
|
||||
| `CreateFile.NewFile(FileName, isBin, binFileType)` | 创建空白文件,二进制文件自动写入文件头;文件已存在时直接返回不覆盖 | `FileName` 文件名,`isBin` 是否二进制,`binFileType` 二进制类型(普通文件传 `null`) |
|
||||
| `CreateFile.StreamSaveFile(file, TargePath, FileName)` | 以字节流方式保存文件到目标路径(覆盖同名文件) | `file` 源文件,`TargePath` 目标目录,`FileName` 保存的文件名 |
|
||||
|
||||
**二进制文件类型 (`BinFileType`):**
|
||||
|
||||
@@ -224,29 +230,32 @@ File elfFile = CreateFile.NewFile("/path/to/program", true, BinFileType.ELF);
|
||||
// 创建 PE 二进制文件(自动写入 MZ 文件头)
|
||||
File peFile = CreateFile.NewFile("/path/to/app.exe", true, BinFileType.PE);
|
||||
|
||||
// 异步字节流保存文件
|
||||
// 字节流保存文件(覆盖同名文件)
|
||||
boolean success = CreateFile.StreamSaveFile(sourceFile, Path.of("/path/to/dest"), "output.bin");
|
||||
```
|
||||
|
||||
### 4. 文件加密/解密 (`file/EncryptionAndDecryption`)
|
||||
|
||||
支持 **AES**、**DES** 对称加密,加密后覆盖原文件,密钥单独保存到指定目录。解密时从密钥文件读取密钥,解密后覆盖还原原文件。
|
||||
支持 **AES**、**RSA** 加密与 **DES**(已废弃,仅兼容旧版本)解密,加密后覆盖原文件,密钥单独保存到指定目录。解密时从密钥文件读取密钥,解密后覆盖还原原文件。
|
||||
|
||||
| 方法 | 说明 | 参数 |
|
||||
|----------------------------------------------------------------|--------------------------|-----------------------------------------------|
|
||||
| `EncryptionAndDecryption.EncryptionFile(file, type, keyPath)` | 异步加密文件,覆盖原文件,密钥保存到 keyPath | `file` 文件,`type` 加密类型,`keyPath` 密钥目录路径 |
|
||||
| `EncryptionAndDecryption.DecryptionFile(file, type, keyfile)` | 异步解密文件,覆盖还原原文件 | `file` 加密文件,`type` 加密类型,`keyfile` 密钥文件 |
|
||||
| `EncryptionAndDecryption.EncryptionFile(file, type, keyPath)` | 加密文件,覆盖原文件,密钥保存到 keyPath | `file` 文件,`type` 加密类型,`keyPath` 密钥目录路径 |
|
||||
| `EncryptionAndDecryption.DecryptionFile(file, type, keyfile)` | 解密文件,覆盖还原原文件 | `file` 加密文件,`type` 加密类型,`keyfile` 密钥文件 |
|
||||
|
||||
**加密类型 (`EncryptionType`):**
|
||||
|
||||
| 枚举值 | 算法 | 密钥长度 |
|
||||
|-------|---------------------|---------|
|
||||
| `AES` | AES/ECB/PKCS5Padding | 128 bit |
|
||||
| `DES` | DES/ECB/PKCS5Padding | 56 bit |
|
||||
| `RSA` | AES(对称替代) | 128 bit |
|
||||
| `AES` | AES-256/GCM(随机 IV + 认证标签) | 256 bit |
|
||||
| `RSA` | RSA-2048/OAEP 混合加密(RSA 包裹随机 AES-256 会话密钥,数据用 AES-GCM 加密) | 2048 bit |
|
||||
| `DES` | DES/ECB/PKCS5Padding(已废弃,不安全) | 56 bit |
|
||||
|
||||
> 加密后密钥文件命名为 `原文件名.key`,保存在 `keyPath` 目录下。
|
||||
> 加密/解密均覆盖写回原文件,不添加额外后缀。
|
||||
> 加密后密钥文件命名为 `原文件名.key`,保存在 `keyPath` 目录下:AES/DES 保存原始密钥,RSA 保存 PKCS#8 私钥;
|
||||
> POSIX 系统上密钥文件权限自动限制为仅属主可读写。
|
||||
> 加密/解密均为流式处理,支持大文件;先写临时文件、成功后原子替换原文件,中途失败不会损坏原数据。
|
||||
> 入参非法(文件/密钥不存在或为 null)返回 `null`;加解密实际失败(密钥错误、密文损坏等)抛出 `RuntimeException`。
|
||||
> 新代码请使用 `AES` 或 `RSA`,不要使用已废弃的 `DES`。
|
||||
|
||||
```java
|
||||
import top.gtb520.java.gnu.java.utils.library.utils.file.EncryptionAndDecryption;
|
||||
@@ -266,7 +275,40 @@ File keyFile = keyDir.resolve("data.txt.key").toFile();
|
||||
File decrypted = EncryptionAndDecryption.DecryptionFile(file, EncryptionType.AES, keyFile);
|
||||
```
|
||||
|
||||
### 5. 随机生成器 (`random`)
|
||||
### 5. 文件下载 (`file/download`)
|
||||
|
||||
支持单线程和多线程文件下载,多线程模式通过 HTTP `Range` 请求分块并行下载,自动创建父目录。
|
||||
|
||||
| 方法 | 说明 | 参数 |
|
||||
|-------------------------------------------------------|---------------------------------------|-------------------------------------------------------------|
|
||||
| `file.DownloadFile(URL, destFilePanth)` | 单线程下载文件 | `URL` 文件地址,`destFilePanth` 保存路径 |
|
||||
| `file.DownloadFile(URL, destFilePanth, threadNum)` | 多线程下载文件(支持断点续传) | `URL` 文件地址,`destFilePanth` 保存路径,`threadNum` 线程数枚举 |
|
||||
|
||||
**线程数枚举 (`ThreadNum`):**
|
||||
|
||||
| 枚举值 | 线程数 |
|
||||
|-------------------|------|
|
||||
| `THREAD_NUM_II` | 2 |
|
||||
| `THREAD_NUM_IV` | 4 |
|
||||
| `THREAD_NUM_VI` | 6 |
|
||||
| `THREAD_NUM_VIII` | 8 |
|
||||
| `THREAD_NUM_X` | 10 |
|
||||
|
||||
> 多线程下载要求服务器支持 `Accept-Ranges: bytes`(HTTP 206 响应),否则将抛出 `RuntimeException`。
|
||||
> 入参为 `null` 时抛出 `IllegalArgumentException`;HTTP 请求失败时抛出 `RuntimeException`。
|
||||
|
||||
```java
|
||||
import top.gtb520.java.gnu.java.utils.library.utils.file.download.file;
|
||||
import top.gtb520.java.gnu.java.utils.library.utils.file.download.file.ThreadNum;
|
||||
|
||||
// 单线程下载
|
||||
file.DownloadFile("https://example.com/file.zip", "/path/to/save/file.zip");
|
||||
|
||||
// 多线程下载(4 线程)
|
||||
file.DownloadFile("https://example.com/file.zip", "/path/to/save/file.zip", ThreadNum.THREAD_NUM_IV);
|
||||
```
|
||||
|
||||
### 6. 随机生成器 (`random`)
|
||||
|
||||
| 方法 | 说明 | 参数 |
|
||||
|---------------------------------------------|--------------|------------------------------|
|
||||
@@ -276,11 +318,13 @@ File decrypted = EncryptionAndDecryption.DecryptionFile(file, EncryptionType.AES
|
||||
```java
|
||||
import top.gtb520.java.gnu.java.utils.library.utils.processing.random.defaults;
|
||||
|
||||
int num = defaults.random(1, 100); // 生成 1~100 的随机整数
|
||||
int num = defaults.random(1, 100); // 生成 1~100 的随机整数(包含两端)
|
||||
String str = defaults.randomString(16); // 生成 16 位随机字符串(大小写字母+数字)
|
||||
```
|
||||
|
||||
### 6. 硬件标识生成器 (`hwid`)
|
||||
> `random(start, end)` 结果包含两端;`randomString(length)` 的长度不能为负数,否则抛出 `IllegalArgumentException`。
|
||||
|
||||
### 7. 硬件标识生成器 (`hwid`)
|
||||
|
||||
组合 CPU、主板、MAC 地址、硬盘序列号等信息,通过 SHA-256 生成唯一的硬件标识符(HWID)。支持 Windows、macOS、Linux 及 Android 平台。
|
||||
|
||||
@@ -295,7 +339,10 @@ String hwid = getHWID.generateHWID();
|
||||
System.out.println("HWID: " + hwid);
|
||||
```
|
||||
|
||||
### 7. 版本信息 (`version`)
|
||||
> 单项硬件信息获取失败时自动跳过该组件(如无序列号的硬件);全部失败时返回 `"UNKNOWN_HWID"`。
|
||||
> MAC 地址优先取非回环、非虚拟网卡,避免虚拟网卡导致 HWID 漂移。
|
||||
|
||||
### 8. 版本信息 (`version`)
|
||||
|
||||
提供项目版本信息及编译时 Java/OpenJDK 版本常量。
|
||||
|
||||
@@ -304,6 +351,8 @@ System.out.println("HWID: " + hwid);
|
||||
| `version.GetVersion()` | 获取项目版本号 |
|
||||
| `version.GetJavaVersion()` | 获取编译时 Java 版本 |
|
||||
|
||||
> `GetVersion()` 打包为 JAR 时读取 MANIFEST 中的 `Implementation-Version`,类路径运行(如测试)时回退到内置常量。
|
||||
|
||||
```java
|
||||
import top.gtb520.java.gnu.java.utils.library.version;
|
||||
|
||||
@@ -324,7 +373,7 @@ main.version.getOpenJDKRuntimeEnvironment(); // OpenJDK 运行时环境
|
||||
main.version.getOpenJDK64BitServerVM(); // OpenJDK 64-Bit Server VM
|
||||
```
|
||||
|
||||
### 8. 运行时信息 (`runtime`)
|
||||
### 9. 运行时信息 (`runtime`)
|
||||
|
||||
获取当前 Java 运行时环境信息,包括 JAR 路径、JVM 版本、位数等。
|
||||
|
||||
@@ -332,10 +381,10 @@ main.version.getOpenJDK64BitServerVM(); // OpenJDK 64-Bit Server VM
|
||||
|
||||
| 方法 | 说明 |
|
||||
|-------------------------------|------------------------------------------|
|
||||
| `RunEnv.getJarPath()` | 获取当前运行的 JAR 路径 |
|
||||
| `RunEnv.getJarPath()` | 获取当前运行的 JAR 路径(非 JAR 运行时返回类路径目录) |
|
||||
| `RunEnv.getTempDir()` | 获取系统临时目录路径 |
|
||||
| `RunEnv.newTempDir()` | 在系统临时目录下新建时间戳子目录,返回路径;已存在则返回 `null` |
|
||||
| `RunEnv.newJarThisTempDir()` | 在 JAR 所在目录下新建时间戳子目录,返回路径;已存在则返回 `null` |
|
||||
| `RunEnv.newTempDir()` | 在系统临时目录下新建唯一临时子目录,返回路径;创建失败抛出 `RuntimeException` |
|
||||
| `RunEnv.newJarThisTempDir()` | 在 JAR 所在目录下新建唯一临时子目录,返回路径;JAR 路径无父目录时返回 `null` |
|
||||
|
||||
**Java 运行时环境 (`JavaRunTimeEnv`):**
|
||||
|
||||
@@ -390,9 +439,19 @@ mvn compile
|
||||
mvn package
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
项目使用 JUnit 5 编写测试(共 53 个用例),运行:
|
||||
|
||||
```bash
|
||||
mvn test
|
||||
```
|
||||
|
||||
测试覆盖压缩/解压往返、Zip-Slip 恶意压缩包防护、AES/DES/RSA 加解密往返(含 8MB 大文件流式测试与错误密钥不损坏密文)、随机数边界(含 `Integer.MAX_VALUE` 上界)、彩色输出格式叠加与注入防护、临时目录创建与 HWID 稳定性。
|
||||
|
||||
## 使用
|
||||
|
||||
编译成功后,将 `target/gnu_java_utils_library-1.7-SNAPSHOT.jar` 放入项目依赖中,或在 Releases 中下载最新版 JAR 包。
|
||||
编译成功后,将 `target/gnu_java_utils_library-1.9-SNAPSHOT.jar` 放入项目依赖中,或在 Releases 中下载最新版 JAR 包。
|
||||
|
||||
**pom.xml 引入本地依赖:**
|
||||
|
||||
@@ -401,9 +460,9 @@ mvn package
|
||||
<dependency>
|
||||
<groupId>top.gtb520.java.gnu</groupId>
|
||||
<artifactId>gnu_java_utils_library</artifactId>
|
||||
<version>1.7-SNAPSHOT</version>
|
||||
<version>1.9-SNAPSHOT</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/gnu_java_utils_library-1.7-SNAPSHOT.jar</systemPath>
|
||||
<systemPath>${project.basedir}/lib/gnu_java_utils_library-1.9-SNAPSHOT.jar</systemPath>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
@@ -414,10 +473,17 @@ mvn package
|
||||
|
||||
- **压缩时**:源路径不存在
|
||||
- **解压时**:目标路径不是有效的目录
|
||||
- **解压时**:压缩包包含越界路径(如 `../`)或符号/硬链接条目(防 Zip-Slip 攻击)
|
||||
|
||||
随机数方法在以下情况会抛出 `IllegalArgumentException`:
|
||||
|
||||
- **起始值大于结束值时**
|
||||
- **随机字符串长度为负数时**
|
||||
|
||||
加密/解密方法的错误约定:
|
||||
|
||||
- **入参非法**(文件/密钥不存在或为 `null`):返回 `null`
|
||||
- **加解密实际失败**(密钥错误、密文损坏等):抛出 `RuntimeException`
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>top.gtb520.java.gnu</groupId>
|
||||
<artifactId>gnu_java_utils_library</artifactId>
|
||||
<version>1.7-SNAPSHOT</version>
|
||||
<version>1.9-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>GNU Java Utils Library</name>
|
||||
|
||||
@@ -2,6 +2,7 @@ package top.gtb520.java.gnu.java.utils.library;
|
||||
|
||||
import top.gtb520.java.gnu.java.utils.library.utils.print.ColorNameEnum;
|
||||
import top.gtb520.java.gnu.java.utils.library.utils.compress.CompressPNG.PNGThreshold;
|
||||
import top.gtb520.java.gnu.java.utils.library.utils.file.download.file.ThreadNum;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
@@ -153,6 +154,16 @@ public interface main {
|
||||
static boolean StreamSaveFile(File file, Path TargePath, String FileName) {
|
||||
return top.gtb520.java.gnu.java.utils.library.utils.file.CreateFile.StreamSaveFile(file, TargePath, FileName);
|
||||
}
|
||||
|
||||
// 下载文件,单线程
|
||||
static void DownloadFile(String URL, String destFilePanth) throws Exception {
|
||||
top.gtb520.java.gnu.java.utils.library.utils.file.download.file.DownloadFile(URL, destFilePanth);
|
||||
}
|
||||
|
||||
// 下载文件,多线程
|
||||
static void DownloadFile(String URL, String destFilePanth, ThreadNum threadNum) throws Exception {
|
||||
top.gtb520.java.gnu.java.utils.library.utils.file.download.file.DownloadFile(URL, destFilePanth, threadNum);
|
||||
}
|
||||
}
|
||||
|
||||
// 文件加解密操作
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package top.gtb520.java.gnu.java.utils.library.utils.file.download;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class file {
|
||||
|
||||
/**
|
||||
* 单线程下载文件
|
||||
*
|
||||
* @param URL 文件地址
|
||||
* @param destFilePanth 下载文件保存路径
|
||||
*/
|
||||
public static void DownloadFile(String URL, String destFilePanth) throws Exception {
|
||||
if (URL == null || destFilePanth == null) {
|
||||
throw new IllegalArgumentException("URL 和保存路径不能为 null");
|
||||
}
|
||||
|
||||
Path destPath = Paths.get(destFilePanth);
|
||||
// 确保父目录存在
|
||||
if (destPath.getParent() != null) {
|
||||
Files.createDirectories(destPath.getParent());
|
||||
}
|
||||
|
||||
HttpClient client = HttpClient.newBuilder()
|
||||
.followRedirects(HttpClient.Redirect.ALWAYS)
|
||||
.connectTimeout(Duration.ofSeconds(30))
|
||||
.build();
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(URL))
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
throw new RuntimeException("下载失败,HTTP 状态码: " + response.statusCode());
|
||||
}
|
||||
|
||||
try (InputStream in = response.body()) {
|
||||
Files.copy(in, destPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多线程下载文件
|
||||
*
|
||||
* @param URL 文件地址
|
||||
* @param destFilePanth 下载文件保存路径
|
||||
* @param threadNum 线程数
|
||||
* */
|
||||
public static void DownloadFile(String URL, String destFilePanth, ThreadNum threadNum) throws Exception {
|
||||
if (URL == null || destFilePanth == null) {
|
||||
throw new IllegalArgumentException("URL 和保存路径不能为 null");
|
||||
}
|
||||
|
||||
Path destPath = Paths.get(destFilePanth);
|
||||
// 确保父目录存在
|
||||
if (destPath.getParent() != null) {
|
||||
Files.createDirectories(destPath.getParent());
|
||||
}
|
||||
|
||||
HttpClient client = HttpClient.newBuilder()
|
||||
.followRedirects(HttpClient.Redirect.ALWAYS)
|
||||
.connectTimeout(Duration.ofSeconds(30))
|
||||
.build();
|
||||
|
||||
// 获取文件大小
|
||||
HttpRequest headRequest = HttpRequest.newBuilder()
|
||||
.uri(URI.create(URL))
|
||||
.method("HEAD", HttpRequest.BodyPublishers.noBody())
|
||||
.build();
|
||||
|
||||
HttpResponse<Void> headResponse = client.send(headRequest, HttpResponse.BodyHandlers.discarding());
|
||||
|
||||
if (headResponse.statusCode() != 200) {
|
||||
throw new RuntimeException("获取文件信息失败,HTTP 状态码: " + headResponse.statusCode());
|
||||
}
|
||||
|
||||
long fileSize = headResponse.headers().firstValueAsLong("content-length")
|
||||
.orElseThrow(() -> new RuntimeException("无法获取文件大小"));
|
||||
|
||||
int numThreads = threadNum.getNum();
|
||||
long blockSize = fileSize / numThreads;
|
||||
|
||||
// 创建临时文件占位
|
||||
try (RandomAccessFile raf = new RandomAccessFile(destPath.toFile(), "rw")) {
|
||||
raf.setLength(fileSize);
|
||||
}
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(numThreads);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
|
||||
|
||||
for (int i = 0; i < numThreads; i++) {
|
||||
long start = i * blockSize;
|
||||
long end = (i == numThreads - 1) ? fileSize - 1 : (start + blockSize - 1);
|
||||
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
downloadBlock(client, URL, destPath.toFile(), start, end);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("下载块失败 [" + start + "-" + end + "]: " + e.getMessage(), e);
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
latch.await();
|
||||
executor.shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载指定字节范围的数据块
|
||||
*
|
||||
* @param client HTTP 客户端
|
||||
* @param url 文件 URL
|
||||
* @param destFile 目标文件
|
||||
* @param start 起始字节位置
|
||||
* @param end 结束字节位置
|
||||
*/
|
||||
private static void downloadBlock(HttpClient client, String url, java.io.File destFile,
|
||||
long start, long end) throws Exception {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.header("Range", "bytes=" + start + "-" + end)
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
||||
|
||||
if (response.statusCode() != 206) {
|
||||
throw new RuntimeException("服务器不支持断点续传,HTTP 状态码: " + response.statusCode());
|
||||
}
|
||||
|
||||
try (InputStream in = response.body();
|
||||
RandomAccessFile raf = new RandomAccessFile(destFile, "rw")) {
|
||||
|
||||
raf.seek(start);
|
||||
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(buffer)) != -1) {
|
||||
raf.write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 线程数枚举
|
||||
* */
|
||||
public enum ThreadNum {
|
||||
THREAD_NUM_II(2),
|
||||
THREAD_NUM_IV(4),
|
||||
THREAD_NUM_VI(6),
|
||||
THREAD_NUM_VIII(8),
|
||||
THREAD_NUM_X(10);
|
||||
|
||||
private final int num;
|
||||
|
||||
ThreadNum(int num) {
|
||||
this.num = num;
|
||||
}
|
||||
|
||||
public int getNum() {
|
||||
return num;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package top.gtb520.java.gnu.java.utils.library;
|
||||
public class version {
|
||||
// 项目的版本信息,不代表实时运行环境
|
||||
// 与 pom.xml 保持一致;构建 JAR 时优先读取 MANIFEST 的 Implementation-Version
|
||||
public static final String VERSION = "1.7";
|
||||
public static final String VERSION = "1.9";
|
||||
public static final String JAVA_VERSION = "25";
|
||||
public static final String OpenJDK_VERSION = "25.0.0.0.1";
|
||||
public static final String OpenJDK_Runtime_Environment = "25.0.0.0.1+36-GA";
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package top.gtb520.java.gnu.java.utils.library.utils.file;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import top.gtb520.java.gnu.java.utils.library.utils.file.download.file;
|
||||
|
||||
import top.gtb520.java.gnu.java.utils.library.utils.file.download.file.ThreadNum;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class downloadfile {
|
||||
@Test
|
||||
void download_single() {
|
||||
// 获取当前运行目录
|
||||
String currentDirectory = System.getProperty("user.dir");
|
||||
|
||||
// 文件URL
|
||||
String fileURL = "http://server-1.database-mysql.com:5525/resource_repository/Java/Linux/openjdk-25.0.2_linux-x64_bin.tar.gz";
|
||||
|
||||
try {
|
||||
// 创建目录
|
||||
File directory = new File(currentDirectory + "/test_download");
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs();
|
||||
}
|
||||
System.out.println("目录: " + currentDirectory);
|
||||
System.out.println("下载目录: " + directory);
|
||||
|
||||
file.DownloadFile(fileURL, directory + "/openjdk-25.0.2_linux-x64_bin.tar.gz");
|
||||
} catch (Exception e) {
|
||||
System.out.println("抛出异常: \n" + e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void download_thread() {
|
||||
// 获取当前运行目录
|
||||
String currentDirectory = System.getProperty("user.dir");
|
||||
|
||||
// 文件URL
|
||||
String fileURL = "http://server-1.database-mysql.com:5525/resource_repository/Java/Linux/java_composer.tar.gz";
|
||||
|
||||
try {
|
||||
// 创建目录
|
||||
File directory = new File(currentDirectory + "/test_download");
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs();
|
||||
}
|
||||
System.out.println("目录: " + currentDirectory);
|
||||
System.out.println("下载目录: " + directory);
|
||||
|
||||
file.DownloadFile(fileURL, directory + "/java_composer.tar.gz", ThreadNum.THREAD_NUM_X);
|
||||
} catch (Exception e) {
|
||||
System.out.println("抛出异常: \n" + e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user