12 Commits

Author SHA1 Message Date
279289fb22 Merge pull request 'v0.0.3' (#6) from dev into master
Reviewed-on: #6
2026-04-02 08:26:38 +00:00
Timi
b56f4e8969 v0.0.3
All checks were successful
CI/CD / build-deploy (pull_request) Successful in 13s
2026-04-02 16:23:53 +08:00
913b9b5328 Merge pull request 'v0.0.2' (#5) from dev into master
Reviewed-on: #5
2026-04-01 14:52:04 +00:00
Timi
56dec33c94 v0.0.2
All checks were successful
CI/CD / build-deploy (pull_request) Successful in 14s
2026-04-01 22:47:09 +08:00
88b1fd6e02 Merge pull request 'v0.0.1' (#4) from dev into master
Reviewed-on: #4
2026-04-01 07:23:36 +00:00
Timi
a24b1855aa v0.0.1
All checks were successful
CI/CD / build-deploy (pull_request) Successful in 10s
2026-04-01 15:23:23 +08:00
1f13cdd7bc Merge pull request 'v0.0.1' (#3) from dev into master
Reviewed-on: #3
2026-04-01 07:20:16 +00:00
Timi
f38975f633 v0.0.1
Some checks failed
CI/CD / build-deploy (pull_request) Failing after 1m31s
2026-04-01 15:20:01 +08:00
0b078278f0 Merge pull request 'v0.0.1' (#2) from dev into master
Reviewed-on: #2
2026-04-01 07:13:48 +00:00
Timi
7d213c54af v0.0.1
Some checks failed
CI/CD / build-deploy (pull_request) Failing after 9s
2026-04-01 15:10:08 +08:00
c0b2ae11a7 Merge pull request 'v0.0.1' (#1) from dev into master
Reviewed-on: #1
2026-04-01 06:09:52 +00:00
Timi
afcc902cbf v0.0.1
Some checks failed
CI/CD / build-deploy (pull_request) Failing after 1m31s
2026-04-01 14:09:24 +08:00
22 changed files with 1593 additions and 432 deletions

111
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,111 @@
name: CI/CD
on:
pull_request:
branches:
- master
types:
- closed
jobs:
build-deploy:
runs-on: act_runner_java
if: ${{ github.event.pull_request.merged == true }}
env:
JAVA_HOME: /usr/lib/jvm/java-21-openjdk
steps:
- name: Checkout code
run: |
git clone ${{ github.server_url }}/${{ github.repository }}.git .
git checkout ${{ github.sha }}
- name: Set up environment
run: |
echo "PR #${{ github.event.number }} merged into master"
echo "Source branch: ${{ github.event.pull_request.head.ref }}"
echo "Target branch: ${{ github.event.pull_request.base.ref }}"
- name: Run tests
run: |
echo "Running test suite..."
- name: Build project
run: |
mvn -B -DskipTests clean package source:jar javadoc:jar
- name: Deploy to Nexus
if: success()
run: |
if [ -z "${{ secrets.NEXUS_USERNAME }}" ] || [ -z "${{ secrets.NEXUS_PASSWORD }}" ]; then
echo "Missing secrets.NEXUS_USERNAME or secrets.NEXUS_PASSWORD"
exit 1
fi
mkdir -p ~/.m2
cat > ~/.m2/settings.xml <<EOF
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>timi-nexus</id>
<username>${{ secrets.NEXUS_USERNAME }}</username>
<password>${{ secrets.NEXUS_PASSWORD }}</password>
</server>
</servers>
</settings>
EOF
version=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version)
artifact_id=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.artifactId)
main_jar="target/${artifact_id}-${version}.jar"
sources_jar="target/${artifact_id}-${version}-sources.jar"
javadoc_jar="target/${artifact_id}-${version}-javadoc.jar"
if [ ! -f "$main_jar" ] || [ ! -f "$sources_jar" ] || [ ! -f "$javadoc_jar" ]; then
echo "Missing build artifacts in target"
exit 1
fi
mvn -B deploy:deploy-file \
-Dfile="$main_jar" \
-Dsources="$sources_jar" \
-Djavadoc="$javadoc_jar" \
-DpomFile="./pom.xml" \
-Durl="https://nexus.imyeyu.com/repository/maven-releases/" \
-DrepositoryId="timi-nexus" \
-Dhttps.protocols=TLSv1.2 \
-Djdk.tls.client.protocols=TLSv1.2
- name: Create release
if: ${{ success() && startsWith(github.event.pull_request.title, 'v') }}
env:
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
GITEA_SERVER_URL: ${{ github.server_url }}
GITEA_REPOSITORY: ${{ github.repository }}
RELEASE_TAG: ${{ github.event.pull_request.title }}
RELEASE_TARGET: ${{ github.sha }}
run: |
if [ -z "$GITEA_TOKEN" ]; then
echo "Missing secrets.RUNNER_TOKEN"
exit 1
fi
api_url="$GITEA_SERVER_URL/api/v1/repos/$GITEA_REPOSITORY/releases"
payload=$(cat <<EOF
{
"tag_name": "$RELEASE_TAG",
"name": "$RELEASE_TAG",
"target_commitish": "$RELEASE_TARGET",
"draft": false,
"prerelease": false
}
EOF
)
response=$(curl -sS -X POST "$api_url" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "$payload")
release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -n 1 | grep -o '[0-9]*')
if [ -z "$release_id" ] || echo "$response" | grep -q '"message"'; then
echo "Create release failed: $response"
exit 1
fi
echo "Release created: id=$release_id"
for asset_path in target/*.jar; do
asset_name=$(basename "$asset_path")
curl -sS -X POST "$api_url/$release_id/assets?name=$asset_name" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$asset_path"
done

5
.gitignore vendored
View File

@@ -1,3 +1,6 @@
/testOut*
/AGENTS.md
target/ target/
!.mvn/wrapper/maven-wrapper.jar !.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/ !**/src/main/**/target/
@@ -36,5 +39,3 @@ build/
### Mac OS ### ### Mac OS ###
.DS_Store .DS_Store
/testOut*

View File

@@ -1,3 +1,19 @@
# timi-compress # timi-compress
Java 压缩、解压缩工具 Java 压缩、解压缩工具
## 调用方式
压缩:
```java
ZipCompressor.of("testSrc").toFile("testOut/test.zip");
TarCompressor.of(file).toStream(outputStream);
```
解压:
```java
ZipDecompressor.of("testOut/test.zip").to("testOutDe");
GZipDecompressor.of(inputStream).to("testOutDe");
```

78
pom.xml
View File

@@ -6,7 +6,7 @@
<groupId>com.imyeyu.compress</groupId> <groupId>com.imyeyu.compress</groupId>
<artifactId>timi-compress</artifactId> <artifactId>timi-compress</artifactId>
<version>0.0.1</version> <version>0.0.3</version>
<properties> <properties>
<maven.test.skip>true</maven.test.skip> <maven.test.skip>true</maven.test.skip>
@@ -19,32 +19,94 @@
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-deploy-plugin</artifactId>
<version>3.11.0</version> <version>3.1.3</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.1</version>
</plugin>
<plugin>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-maven-plugin</artifactId>
<version>1.18.20.0</version>
<configuration> <configuration>
<source>21</source> <sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
<target>21</target> <outputDirectory>${project.build.directory}/delombok</outputDirectory>
<addOutputDirectory>false</addOutputDirectory>
<encoding>UTF-8</encoding> <encoding>UTF-8</encoding>
</configuration> </configuration>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>delombok</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.11.2</version>
<configuration>
<sourcepath>${project.build.directory}/delombok</sourcepath>
<encoding>UTF-8</encoding>
<charset>UTF-8</charset>
<docencoding>UTF-8</docencoding>
</configuration>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
<distributionManagement>
<repository>
<id>timi_nexus</id>
<url>https://nexus.imyeyu.com/repository/maven-releases/</url>
</repository>
</distributionManagement>
<repositories>
<repository>
<id>timi_nexus</id>
<url>https://nexus.imyeyu.com/repository/maven-public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>com.imyeyu.io</groupId> <groupId>com.imyeyu.io</groupId>
<artifactId>timi-io</artifactId> <artifactId>timi-io</artifactId>
<version>0.0.1</version> <version>0.0.3</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId> <artifactId>commons-compress</artifactId>
<version>1.26.1</version> <version>1.28.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.tukaani</groupId> <groupId>org.tukaani</groupId>
<artifactId>xz</artifactId> <artifactId>xz</artifactId>
<version>1.9</version> <version>1.12</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.40</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>

View File

@@ -1,66 +0,0 @@
package com.imyeyu.compress;
import com.imyeyu.java.bean.CallbackArg;
import com.imyeyu.utils.OS;
import java.io.File;
/**
* 抽象解压缩执行器
*
* @author 夜雨
* @since 2024-06-30 18:09
*/
public abstract class AbstractCompressor implements OS.FileSystem {
/** 操作文件回调 */
protected CallbackArg<File> fileCallback;
/** 进度回调 */
protected CallbackArg<Double> progressCallback;
/** 中止 */
protected boolean isInterrupt = false;
/** 暂停 */
protected boolean isPause = false;
/** 暂停锁 */
protected final Object pauseLock = new Object();
/** 暂停 */
public void pause() {
isPause = true;
}
/** 开始 */
public void start() {
isPause = false;
synchronized (pauseLock) {
pauseLock.notifyAll();
}
}
/** 中止 */
public void interrupt() {
isInterrupt = true;
}
/**
* 设置操作文件回调。正在压缩或解压某文件时触发
*
* @param fileCallback 回调接口
*/
public void setFileCallback(CallbackArg<File> fileCallback) {
this.fileCallback = fileCallback;
}
/**
* 设置进度回调
*
* @param progressCallback 回调接口
*/
public void setProgressCallback(CallbackArg<Double> progressCallback) {
this.progressCallback = progressCallback;
}
}

View File

@@ -0,0 +1,285 @@
package com.imyeyu.compress;
import com.imyeyu.java.bean.CallbackArg;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.utils.OS;
import java.io.File;
import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* 抽象处理器
*
* @param <T> 处理器类型
* @author 夜雨
* @since 2024-06-30 18:09
*/
public abstract class AbstractRunner<T extends AbstractRunner<T>> implements OS.FileSystem {
/** 文件处理回调 */
protected CallbackArg<File> fileCallback;
/** 进度回调 */
protected CallbackArg<Double> progressCallback;
/** 中断标记 */
protected boolean isInterrupt = false;
/** 暂停标记 */
protected boolean isPause = false;
/** 暂停锁 */
protected final Object pauseLock = new Object();
/** 总字节数 */
private long progressTotalBytes = -1L;
/** 当前已处理字节数 */
private long progressBytes = 0L;
/**
* 设置文件处理回调
*
* @param fileCallback 文件处理回调
* @return 当前处理器
*/
public T setFileCallback(CallbackArg<File> fileCallback) {
this.fileCallback = fileCallback;
return self();
}
/**
* 设置文件处理回调
*
* @param fileCallback 文件处理回调
* @return 当前处理器
*/
public T fileCallback(CallbackArg<File> fileCallback) {
return setFileCallback(fileCallback);
}
/**
* 设置进度回调
*
* @param progressCallback 进度回调
* @return 当前处理器
*/
public T setProgressCallback(CallbackArg<Double> progressCallback) {
this.progressCallback = progressCallback;
return self();
}
/**
* 设置进度回调
*
* @param progressCallback 进度回调
* @return 当前处理器
*/
public T progressCallback(CallbackArg<Double> progressCallback) {
return setProgressCallback(progressCallback);
}
/** 暂停任务 */
public void pause() {
isPause = true;
}
/** 恢复任务 */
public void start() {
isPause = false;
synchronized (pauseLock) {
pauseLock.notifyAll();
}
}
/** 中断任务 */
public void interrupt() {
isInterrupt = true;
}
/**
* 返回当前处理器
*
* @return 当前处理器
*/
@SuppressWarnings("unchecked")
protected final T self() {
return (T) this;
}
/**
* 初始化字节进度
*
* @param totalBytes 总字节数
*/
protected void initByteProgress(long totalBytes) {
progressBytes = 0L;
progressTotalBytes = totalBytes;
}
/** 重置进度状态 */
protected void resetProgress() {
progressBytes = 0L;
progressTotalBytes = -1L;
}
/**
* 执行暂停检查
*
* @throws InterruptedException 等待恢复时被中断
*/
protected void awaitIfPaused() throws InterruptedException {
if (!isPause) {
return;
}
synchronized (pauseLock) {
while (isPause) {
pauseLock.wait();
}
}
}
/**
* 检查是否已经中断
*
* @throws IOException 操作已中断
*/
protected void ensureRunning() throws IOException {
if (isInterrupt) {
throw new IOException("操作已中断");
}
}
/**
* 触发文件回调
*
* @param file 当前处理的文件
*/
protected void handleFile(File file) {
if (fileCallback != null) {
fileCallback.handler(file);
}
}
/**
* 触发进度回调
*
* @param currentBytes 当前已处理字节数
* @param totalBytes 总字节数
*/
protected void handleProgress(long currentBytes, long totalBytes) {
if (progressCallback == null || totalBytes < 1L) {
return;
}
double progress = Math.min(1D, currentBytes * 1D / totalBytes);
progressCallback.handler(progress);
}
/**
* 增加已处理字节数并回调
*
* @param bytes 本次新增字节数
*/
protected void handleTransferred(long bytes) {
if (bytes < 1L) {
return;
}
progressBytes += bytes;
handleProgress(progressBytes, progressTotalBytes);
}
/** 将进度推进到完成态 */
protected void finishProgress() {
if (progressCallback != null) {
progressCallback.handler(1D);
}
}
/**
* 复制数据流并更新进度
*
* @param fromStream 输入流
* @param toStream 输出流
* @return 已复制字节数
* @throws Exception 复制失败
*/
protected long transfer(InputStream fromStream, OutputStream toStream) throws Exception {
return transfer(fromStream, toStream, true);
}
/**
* 复制数据流
*
* @param fromStream 输入流
* @param toStream 输出流
* @param countProgress 是否统计进度
* @return 已复制字节数
* @throws Exception 复制失败
*/
protected long transfer(InputStream fromStream, OutputStream toStream, boolean countProgress) throws Exception {
byte[] buffer = new byte[8192];
long total = 0L;
int length;
while ((length = fromStream.read(buffer)) != -1) {
awaitIfPaused();
ensureRunning();
toStream.write(buffer, 0, length);
total += length;
if (countProgress) {
handleTransferred(length);
}
}
return total;
}
/**
* 创建一个关闭时不关闭原始流的输入流包装
*
* @param inputStream 原始输入流
* @return 包装后的输入流
*/
protected InputStream nonClosing(InputStream inputStream) {
return new FilterInputStream(inputStream) {
@Override
public void close() {
}
};
}
/**
* 创建一个关闭时不关闭原始流的输出流包装
*
* @param outputStream 原始输出流
* @return 包装后的输出流
*/
protected OutputStream nonClosing(OutputStream outputStream) {
return new FilterOutputStream(outputStream) {
@Override
public void close() throws IOException {
flush();
}
};
}
/**
* 规范化归档条目名称
*
* @param entryName 条目名称
* @return 规范化后的名称
*/
protected String normalizeEntryName(String entryName) {
TimiException.required(entryName, "not found entryName");
String normalized = entryName.replace('\\', '/');
while (normalized.startsWith("/")) {
normalized = normalized.substring(1);
}
TimiException.required(normalized, "not found normalized entryName");
return normalized;
}
}

View File

@@ -2,6 +2,10 @@ package com.imyeyu.compress;
import com.imyeyu.io.IO; import com.imyeyu.io.IO;
import com.imyeyu.java.ref.Ref; import com.imyeyu.java.ref.Ref;
import org.apache.commons.compress.archivers.sevenz.SevenZFile;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import java.io.File; import java.io.File;
import java.io.InputStream; import java.io.InputStream;
@@ -15,30 +19,26 @@ import java.io.InputStream;
public enum CompressType { public enum CompressType {
/** 7z */ /** 7z */
Z7(Z7Compressor.class, Z7Decompressor.class, -0x51), Z7(Z7Compressor.class, Z7Decompressor.class),
/** Zip */ /** Zip */
ZIP(ZipCompressor.class, ZipDecompressor.class, 0x504B0304), ZIP(ZipCompressor.class, ZipDecompressor.class),
/** Gzip */ /** Gzip */
GZIP(GZipCompressor.class, GZipDecompressor.class, -0x74F7F8), GZIP(GZipCompressor.class, GZipDecompressor.class),
/** tar */ /** Tar */
TAR(TarCompressor.class, TarDecompressor.class, 0x776F7264); TAR(TarCompressor.class, TarDecompressor.class);
/** 压缩 */ /** 压缩器类型 */
final Class<? extends Compressor> compressorType; final Class<? extends Compressor<?>> compressorType;
/** 解压 */ /** 解压器类型 */
final Class<? extends Decompressor> decompressorType; final Class<? extends Decompressor<?>> decompressorType;
/** 文件头标记 */ CompressType(Class<? extends Compressor<?>> compressorType, Class<? extends Decompressor<?>> decompressorType) {
final int headHex;
CompressType(Class<? extends Compressor> compressorType, Class<? extends Decompressor> decompressorType, int headHex) {
this.compressorType = compressorType; this.compressorType = compressorType;
this.decompressorType = decompressorType; this.decompressorType = decompressorType;
this.headHex = headHex;
} }
/** /**
@@ -47,45 +47,93 @@ public enum CompressType {
* @return 压缩操作对象 * @return 压缩操作对象
* @throws Exception 实例化失败 * @throws Exception 实例化失败
*/ */
public Compressor getCompressor() throws Exception { public Compressor<?> getCompressor() throws Exception {
return Ref.newInstance(compressorType); return Ref.newInstance(compressorType);
} }
/**
* 获取带源路径的压缩操作对象
*
* @param fromPath 源路径
* @return 压缩操作对象
* @throws Exception 实例化失败
*/
public Compressor<?> ofCompress(String fromPath) throws Exception {
return getCompressor().from(fromPath);
}
/**
* 获取带源文件的压缩操作对象
*
* @param fromFile 源文件
* @return 压缩操作对象
* @throws Exception 实例化失败
*/
public Compressor<?> ofCompress(File fromFile) throws Exception {
return getCompressor().from(fromFile);
}
/** /**
* 获取解压操作对象 * 获取解压操作对象
* *
* @return 解压操作对象 * @return 解压操作对象
* @throws Exception 实例化失败 * @throws Exception 实例化失败
*/ */
public Decompressor getDecompressor() throws Exception { public Decompressor<?> getDecompressor() throws Exception {
return Ref.newInstance(decompressorType); return Ref.newInstance(decompressorType);
} }
/** /**
* 根据文件获取解压操作对象。将会读取文件头解析算法类型 * 获取带源文件的解压操作对象
*
* @param fromFile 源压缩文件
* @return 解压操作对象
* @throws Exception 实例化失败
*/
public Decompressor<?> ofCompressed(File fromFile) throws Exception {
return getDecompressor().from(fromFile);
}
/**
* 获取带源输入流的解压操作对象
*
* @param fromStream 源压缩输入流
* @return 解压操作对象
* @throws Exception 实例化失败
*/
public Decompressor<?> ofCompressed(InputStream fromStream) throws Exception {
return getDecompressor().from(fromStream);
}
/**
* 根据文件获取解压操作对象
* 会读取文件头识别压缩格式
* *
* @param file 文件 * @param file 文件
* @return 解压操作对象 * @return 解压操作对象
* @throws UnsupportedOperationException 不支持的文件 * @throws UnsupportedOperationException 不支持的文件
* @throws Exception 实例化失败 * @throws Exception 实例化失败
*/ */
public static Decompressor fromFile(File file) throws Exception { public static Decompressor<?> fromFile(File file) throws Exception {
InputStream is = IO.getInputStream(file); try (InputStream inputStream = IO.getInputStream(file)) {
byte[] head = new byte[4]; byte[] head = new byte[512];
if (-1 == is.read(head)) { int length = inputStream.read(head);
throw new UnsupportedOperationException("not support file"); if (length == -1) {
} throw new UnsupportedOperationException("empty file");
is.close(); }
int headHex = 0; if (SevenZFile.matches(head, length)) {
for (byte b : head) { return Z7.ofCompressed(file);
headHex <<= 8; }
headHex |= b; if (ZipArchiveInputStream.matches(head, length)) {
} return ZIP.ofCompressed(file);
for (CompressType type : values()) { }
if (type.headHex == headHex) { if (GzipCompressorInputStream.matches(head, length)) {
return type.getDecompressor(); return GZIP.ofCompressed(file);
}
if (TarArchiveInputStream.matches(head, length)) {
return TAR.ofCompressed(file);
} }
} }
throw new UnsupportedOperationException("not support headHex"); throw new UnsupportedOperationException("not support file");
} }
} }

View File

@@ -1,14 +1,120 @@
package com.imyeyu.compress; package com.imyeyu.compress;
import com.imyeyu.io.IO;
import com.imyeyu.java.bean.timi.TimiException;
import java.io.File; import java.io.File;
import java.io.OutputStream;
/** /**
* 抽象压缩器
* *
* * @param <T> 压缩器类型
* @author 夜雨 * @author 夜雨
* @version 2024-06-30 10:34 * @version 2024-06-30 10:34
*/ */
public abstract class Compressor extends AbstractCompressor { public abstract class Compressor<T extends Compressor<T>> extends AbstractRunner<T> {
public abstract void run(String fromPath, File toFile) throws Exception; /** 源路径 */
private String fromPath;
/**
* 绑定源路径
*
* @param fromPath 源路径
* @return 当前压缩器
*/
public T from(String fromPath) {
TimiException.required(fromPath, "not found fromPath");
this.fromPath = fromPath;
return self();
}
/**
* 绑定源文件
*
* @param fromFile 源文件
* @return 当前压缩器
*/
public T from(File fromFile) {
TimiException.required(fromFile, "not found fromFile");
return from(fromFile.getAbsolutePath());
}
/**
* 压缩到目标文件
*
* @param toFile 目标压缩文件
* @return 当前压缩器
* @throws Exception 压缩失败
*/
public T toFile(File toFile) throws Exception {
toFile(requireFromPath(), toFile);
return self();
}
/**
* 压缩到目标文件路径
*
* @param toPath 目标压缩文件路径
* @return 当前压缩器
* @throws Exception 压缩失败
*/
public T toFile(String toPath) throws Exception {
return toFile(IO.file(toPath));
}
/**
* 压缩到输出流
* 输出流由调用方管理
*
* @param toStream 目标输出流
* @return 当前压缩器
* @throws Exception 压缩失败
*/
public T toStream(OutputStream toStream) throws Exception {
toStream(requireFromPath(), toStream);
return self();
}
/**
* 获取已绑定的源路径
*
* @return 源路径
*/
protected String requireFromPath() {
TimiException.required(fromPath, "not found source");
return fromPath;
}
/**
* 执行压缩到目标文件
*
* @param fromPath 源路径
* @param toFile 目标压缩文件
* @throws Exception 压缩失败
*/
protected void toFile(String fromPath, File toFile) throws Exception {
try (OutputStream outputStream = IO.getOutputStream(toFile)) {
toStream(fromPath, outputStream);
outputStream.flush();
} catch (Exception exception) {
if (isInterrupt) {
IO.destroy(toFile);
}
throw exception;
}
if (isInterrupt) {
IO.destroy(toFile);
}
}
/**
* 执行压缩到输出流
*
* @param fromPath 源路径
* @param toStream 目标输出流
* @throws Exception 压缩失败
*/
protected abstract void toStream(String fromPath, OutputStream toStream) throws Exception;
} }

View File

@@ -1,21 +1,101 @@
package com.imyeyu.compress; package com.imyeyu.compress;
import com.imyeyu.io.IO;
import com.imyeyu.java.bean.timi.TimiException;
import java.io.File; import java.io.File;
import java.io.InputStream;
/** /**
* 抽象解压 * 抽象解压
* *
* @param <T> 解压器类型
* @author 夜雨 * @author 夜雨
* @version 2024-06-30 18:02 * @version 2024-06-30 18:02
*/ */
public abstract class Decompressor extends AbstractCompressor { public abstract class Decompressor<T extends Decompressor<T>> extends AbstractRunner<T> {
/** 源压缩文件 */
private File fromFile;
/** 源压缩输入流 */
private InputStream fromStream;
/** /**
* 执行解压 * 绑定源压缩文件
* *
* @param fromFile 源压缩文件 * @param fromFile 源压缩文件
* @param toPath 解压路径 * @return 当前解压器
*/
public T from(File fromFile) {
TimiException.required(fromFile, "not found fromFile");
this.fromFile = fromFile;
this.fromStream = null;
return self();
}
/**
* 绑定源压缩文件路径
*
* @param fromPath 源压缩文件路径
* @return 当前解压器
*/
public T from(String fromPath) {
TimiException.required(fromPath, "not found fromPath");
return from(new File(fromPath));
}
/**
* 绑定源压缩输入流
*
* @param fromStream 源压缩输入流
* @return 当前解压器
*/
public T from(InputStream fromStream) {
TimiException.required(fromStream, "not found fromStream");
this.fromStream = fromStream;
this.fromFile = null;
return self();
}
/**
* 解压到目标目录
*
* @param toPath 目标目录
* @return 当前解压器
* @throws Exception 解压失败 * @throws Exception 解压失败
*/ */
public abstract void run(File fromFile, String toPath) throws Exception; public T toPath(String toPath) throws Exception {
if (fromFile != null) {
toPath(fromFile, toPath);
return self();
}
if (fromStream != null) {
toPath(fromStream, toPath);
return self();
}
throw new IllegalStateException("not found source");
}
/**
* 执行从文件解压
*
* @param fromFile 源压缩文件
* @param toPath 目标目录
* @throws Exception 解压失败
*/
protected void toPath(File fromFile, String toPath) throws Exception {
try (InputStream inputStream = IO.getInputStream(fromFile)) {
toPath(inputStream, toPath);
}
}
/**
* 执行从输入流解压
*
* @param fromStream 源压缩输入流
* @param toPath 目标目录
* @throws Exception 解压失败
*/
protected abstract void toPath(InputStream fromStream, String toPath) throws Exception;
} }

View File

@@ -7,54 +7,64 @@ import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.File; import java.io.File;
import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.List; import java.util.List;
/** /**
* Gzip 压缩器
* 当前实现保持与原行为一致,实际输出为 tar.gz
*
* @author 夜雨 * @author 夜雨
* @version 2024-06-30 19:40 * @version 2024-06-30 19:40
*/ */
public class GZipCompressor extends Compressor { public class GZipCompressor extends Compressor<GZipCompressor> {
/**
* 创建压缩器
*
* @param fromPath 源路径
* @return 压缩器
*/
public static GZipCompressor of(String fromPath) {
return new GZipCompressor().from(fromPath);
}
/**
* 创建压缩器
*
* @param fromFile 源文件
* @return 压缩器
*/
public static GZipCompressor of(File fromFile) {
return new GZipCompressor().from(fromFile);
}
@Override @Override
public void run(String fromPath, File toFile) throws Exception { protected void toStream(String fromPath, OutputStream toStream) throws Exception {
List<File> files = IO.listFile(new File(fromPath)); File fromFile = new File(fromPath);
List<File> files = IO.listFile(fromFile);
String basePath = files.getFirst().getParentFile().getAbsolutePath(); String basePath = files.getFirst().getParentFile().getAbsolutePath();
initByteProgress(IO.calcSize(fromFile));
OutputStream os = IO.getOutputStream(toFile); try (
BufferedOutputStream byteOS = new BufferedOutputStream(os); GzipCompressorOutputStream gzipOutputStream = new GzipCompressorOutputStream(new BufferedOutputStream(nonClosing(toStream)));
GzipCompressorOutputStream gzipOS = new GzipCompressorOutputStream(byteOS); TarArchiveOutputStream tarOutputStream = new TarArchiveOutputStream(gzipOutputStream)
TarArchiveOutputStream tarOS = new TarArchiveOutputStream(gzipOS); ) {
for (File sourceFile : files) {
TarArchiveEntry tarEntry; String name = sourceFile.getAbsolutePath().substring(basePath.length() + 1);
for (int i = 0, total = files.size(); i < total; i++) { TarArchiveEntry tarEntry = new TarArchiveEntry(sourceFile, normalizeEntryName(name));
String name = files.get(i).getAbsolutePath().substring(basePath.length() + 1); tarOutputStream.putArchiveEntry(tarEntry);
tarEntry = new TarArchiveEntry(files.get(i), name.replaceAll("\\\\", "/")); try (InputStream inputStream = IO.getInputStream(sourceFile)) {
tarOS.putArchiveEntry(tarEntry); transfer(inputStream, tarOutputStream);
tarOS.write(IO.toBytes(files.get(i)));
tarOS.closeArchiveEntry();
if (isPause) {
synchronized (pauseLock) {
pauseLock.wait();
} }
tarOutputStream.closeArchiveEntry();
handleFile(sourceFile);
} }
if (fileCallback != null) { tarOutputStream.finish();
fileCallback.handler(toFile); gzipOutputStream.finish();
} finishProgress();
if (progressCallback != null) { } finally {
progressCallback.handler(1D * i / total); resetProgress();
}
if (isInterrupt) {
break;
}
} }
tarOS.finish();
tarOS.close();
gzipOS.finish();
gzipOS.close();
byteOS.flush();
byteOS.close();
os.close();
} }
} }

View File

@@ -7,60 +7,105 @@ import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import java.io.File; import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
/** /**
* Gzip 解压器
*
* @author 夜雨 * @author 夜雨
* @version 2024-06-30 19:47 * @version 2024-06-30 19:47
*/ */
public class GZipDecompressor extends Decompressor { public class GZipDecompressor extends Decompressor<GZipDecompressor> {
/**
* 创建解压器
*
* @param fromFile 源压缩文件
* @return 解压器
*/
public static GZipDecompressor of(File fromFile) {
return new GZipDecompressor().from(fromFile);
}
/**
* 创建解压器
*
* @param fromPath 源压缩文件路径
* @return 解压器
*/
public static GZipDecompressor of(String fromPath) {
return new GZipDecompressor().from(fromPath);
}
/**
* 创建解压器
*
* @param fromStream 源压缩输入流
* @return 解压器
*/
public static GZipDecompressor of(InputStream fromStream) {
return new GZipDecompressor().from(fromStream);
}
@Override @Override
public void run(File fromFile, String toPath) throws Exception { protected void toPath(File fromFile, String toPath) throws Exception {
InputStream is = IO.getInputStream(fromFile); initByteProgress(readTotalSize(fromFile));
GzipCompressorInputStream gzipIS = new GzipCompressorInputStream(is); try {
TarArchiveInputStream tarIS = new TarArchiveInputStream(gzipIS); super.toPath(fromFile, toPath);
} finally {
resetProgress();
}
}
int total = 0; @Override
{ protected void toPath(InputStream fromStream, String toPath) throws Exception {
try (
GzipCompressorInputStream gzipInputStream = new GzipCompressorInputStream(nonClosing(fromStream));
TarArchiveInputStream tarInputStream = new TarArchiveInputStream(gzipInputStream)
) {
TarArchiveEntry entry; TarArchiveEntry entry;
while ((entry = tarIS.getNextEntry()) != null) { boolean processed = false;
while ((entry = tarInputStream.getNextEntry()) != null) {
String path = IO.fitPath(toPath) + entry.getName();
if (entry.isDirectory()) {
IO.dir(path);
} else {
File toFile = IO.file(path);
try (OutputStream outputStream = IO.getOutputStream(toFile)) {
transfer(tarInputStream, outputStream);
outputStream.flush();
}
handleFile(toFile);
processed = true;
}
}
if (processed) {
finishProgress();
}
}
}
/**
* 读取压缩包解压总字节数
*
* @param fromFile 压缩文件
* @return 总字节数
* @throws Exception 读取失败
*/
private long readTotalSize(File fromFile) throws Exception {
long totalSize = 0L;
try (
InputStream inputStream = IO.getInputStream(fromFile);
GzipCompressorInputStream gzipInputStream = new GzipCompressorInputStream(inputStream);
TarArchiveInputStream tarInputStream = new TarArchiveInputStream(gzipInputStream)
) {
TarArchiveEntry entry;
while ((entry = tarInputStream.getNextEntry()) != null) {
if (!entry.isDirectory()) { if (!entry.isDirectory()) {
total++; totalSize += entry.getSize();
} }
} }
} }
tarIS.close(); return totalSize;
is.close();
is = IO.getInputStream(fromFile);
tarIS = new TarArchiveInputStream(is);
TarArchiveEntry entry;
for (int i = 0; (entry = tarIS.getNextEntry()) != null; i++) {
String path = IO.fitPath(toPath) + entry.getName();
if (entry.isDirectory()) {
IO.dir(path);
} else {
File toFile = IO.file(path);
IO.toFile(toFile, tarIS);
if (fileCallback != null) {
fileCallback.handler(toFile);
}
}
if (isPause) {
synchronized (pauseLock) {
pauseLock.wait();
}
}
if (progressCallback != null && total != -1) {
progressCallback.handler(1D * i / total);
}
if (isInterrupt) {
break;
}
}
tarIS.close();
gzipIS.close();
is.close();
} }
} }

View File

@@ -6,56 +6,59 @@ import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.File; import java.io.File;
import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.List; import java.util.List;
/** /**
* Tar 压缩器
*
* @author 夜雨 * @author 夜雨
* @version 2024-06-30 19:48 * @version 2024-06-30 19:48
*/ */
public class TarCompressor extends Compressor { public class TarCompressor extends Compressor<TarCompressor> {
/**
* 创建压缩器
*
* @param fromPath 源路径
* @return 压缩器
*/
public static TarCompressor of(String fromPath) {
return new TarCompressor().from(fromPath);
}
/**
* 创建压缩器
*
* @param fromFile 源文件
* @return 压缩器
*/
public static TarCompressor of(File fromFile) {
return new TarCompressor().from(fromFile);
}
@Override @Override
public void run(String fromPath, File toFile) throws Exception { protected void toStream(String fromPath, OutputStream toStream) throws Exception {
List<File> files = IO.listFile(new File(fromPath)); File fromFile = new File(fromPath);
List<File> files = IO.listFile(fromFile);
String basePath = files.getFirst().getParentFile().getAbsolutePath(); String basePath = files.getFirst().getParentFile().getAbsolutePath();
initByteProgress(IO.calcSize(fromFile));
OutputStream os = IO.getOutputStream(toFile); try (TarArchiveOutputStream tarOutputStream = new TarArchiveOutputStream(new BufferedOutputStream(nonClosing(toStream)))) {
BufferedOutputStream byteOS = new BufferedOutputStream(os); for (File sourceFile : files) {
TarArchiveOutputStream tarOS = new TarArchiveOutputStream(byteOS); String name = sourceFile.getAbsolutePath().substring(basePath.length() + 1);
TarArchiveEntry tarEntry = new TarArchiveEntry(sourceFile, normalizeEntryName(name));
TarArchiveEntry tarEntry; tarOutputStream.putArchiveEntry(tarEntry);
for (int i = 0, total = files.size(); i < total; i++) { try (InputStream inputStream = IO.getInputStream(sourceFile)) {
String name = files.get(i).getAbsolutePath().substring(basePath.length() + 1); transfer(inputStream, tarOutputStream);
tarEntry = new TarArchiveEntry(files.get(i), name.replaceAll("\\\\", "/"));
tarOS.putArchiveEntry(tarEntry);
tarOS.write(IO.toBytes(files.get(i)));
tarOS.closeArchiveEntry();
if (isPause) {
synchronized (pauseLock) {
pauseLock.wait();
} }
tarOutputStream.closeArchiveEntry();
handleFile(sourceFile);
} }
if (fileCallback != null) { tarOutputStream.finish();
fileCallback.handler(toFile); finishProgress();
} } finally {
if (progressCallback != null) { resetProgress();
progressCallback.handler(1D * i / total);
}
if (isInterrupt) {
break;
}
}
tarOS.finish();
tarOS.close();
byteOS.flush();
byteOS.close();
os.flush();
os.close();
if (isInterrupt) {
IO.destroy(toFile);
} }
} }
} }

View File

@@ -6,58 +6,98 @@ import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import java.io.File; import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
/** /**
* Tar 解压器
*
* @author 夜雨 * @author 夜雨
* @version 2024-06-30 19:48 * @version 2024-06-30 19:48
*/ */
public class TarDecompressor extends Decompressor { public class TarDecompressor extends Decompressor<TarDecompressor> {
/**
* 创建解压器
*
* @param fromFile 源压缩文件
* @return 解压器
*/
public static TarDecompressor of(File fromFile) {
return new TarDecompressor().from(fromFile);
}
/**
* 创建解压器
*
* @param fromPath 源压缩文件路径
* @return 解压器
*/
public static TarDecompressor of(String fromPath) {
return new TarDecompressor().from(fromPath);
}
/**
* 创建解压器
*
* @param fromStream 源压缩输入流
* @return 解压器
*/
public static TarDecompressor of(InputStream fromStream) {
return new TarDecompressor().from(fromStream);
}
@Override @Override
public void run(File fromFile, String toPath) throws Exception { protected void toPath(File fromFile, String toPath) throws Exception {
InputStream is = IO.getInputStream(fromFile); initByteProgress(readTotalSize(fromFile));
TarArchiveInputStream tarIS = new TarArchiveInputStream(is); try {
super.toPath(fromFile, toPath);
} finally {
resetProgress();
}
}
int total = 0; @Override
{ protected void toPath(InputStream fromStream, String toPath) throws Exception {
try (TarArchiveInputStream tarInputStream = new TarArchiveInputStream(nonClosing(fromStream))) {
TarArchiveEntry entry; TarArchiveEntry entry;
while ((entry = tarIS.getNextEntry()) != null) { boolean processed = false;
while ((entry = tarInputStream.getNextEntry()) != null) {
String path = IO.fitPath(toPath) + entry.getName();
if (entry.isDirectory()) {
IO.dir(path);
} else {
File toFile = IO.file(path);
try (OutputStream outputStream = IO.getOutputStream(toFile)) {
transfer(tarInputStream, outputStream);
outputStream.flush();
}
handleFile(toFile);
processed = true;
}
}
if (processed) {
finishProgress();
}
}
}
/**
* 读取压缩包解压总字节数
*
* @param fromFile 压缩文件
* @return 总字节数
* @throws Exception 读取失败
*/
private long readTotalSize(File fromFile) throws Exception {
long totalSize = 0L;
try (InputStream inputStream = IO.getInputStream(fromFile); TarArchiveInputStream tarInputStream = new TarArchiveInputStream(inputStream)) {
TarArchiveEntry entry;
while ((entry = tarInputStream.getNextEntry()) != null) {
if (!entry.isDirectory()) { if (!entry.isDirectory()) {
total++; totalSize += entry.getSize();
} }
} }
} }
tarIS.close(); return totalSize;
is.close();
is = IO.getInputStream(fromFile);
tarIS = new TarArchiveInputStream(is);
TarArchiveEntry entry;
for (int i = 0; (entry = tarIS.getNextEntry()) != null; i++) {
String path = IO.fitPath(toPath) + entry.getName();
if (entry.isDirectory()) {
IO.dir(path);
} else {
File toFile = IO.file(path);
IO.toFile(toFile, tarIS);
if (fileCallback != null) {
fileCallback.handler(toFile);
}
}
if (isPause) {
synchronized (pauseLock) {
pauseLock.wait();
}
}
if (progressCallback != null && total != -1) {
progressCallback.handler(1D * i / total);
}
if (isInterrupt) {
break;
}
}
tarIS.close();
is.close();
} }
} }

View File

@@ -5,46 +5,101 @@ import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile; import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile;
import java.io.File; import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List; import java.util.List;
/** /**
* 7z 压缩器
*
* @author 夜雨 * @author 夜雨
* @version 2024-06-30 19:40 * @version 2024-06-30 19:40
*/ */
public class Z7Compressor extends Compressor { public class Z7Compressor extends Compressor<Z7Compressor> {
/**
* 创建压缩器
*
* @param fromPath 源路径
* @return 压缩器
*/
public static Z7Compressor of(String fromPath) {
return new Z7Compressor().from(fromPath);
}
/**
* 创建压缩器
*
* @param fromFile 源文件
* @return 压缩器
*/
public static Z7Compressor of(File fromFile) {
return new Z7Compressor().from(fromFile);
}
@Override @Override
public void run(String fromPath, File toFile) throws Exception { protected void toStream(String fromPath, OutputStream toStream) throws Exception {
List<File> files = IO.listFile(new File(fromPath)); Path tempFile = Files.createTempFile("timi-compress-", ".7z");
String basePath = files.getFirst().getParentFile().getAbsolutePath(); try {
from(fromPath).toFile(tempFile.toFile());
SevenZOutputFile out = new SevenZOutputFile(toFile); try (InputStream inputStream = Files.newInputStream(tempFile)) {
SevenZArchiveEntry entry; transfer(inputStream, toStream, false);
for (int i = 0, total = files.size(); i < total; i++) { toStream.flush();
String name = files.get(i).getAbsolutePath().substring(basePath.length() + 1);
entry = out.createArchiveEntry(files.get(i), name.replaceAll("\\\\", "/"));
out.putArchiveEntry(entry);
out.write(IO.toBytes(files.get(i)));
out.closeArchiveEntry();
if (isPause) {
synchronized (pauseLock) {
pauseLock.wait();
}
}
if (fileCallback != null) {
fileCallback.handler(toFile);
}
if (progressCallback != null) {
progressCallback.handler(1D * i / total);
}
if (isInterrupt) {
break;
} }
} finally {
Files.deleteIfExists(tempFile);
}
}
@Override
protected void toFile(String fromPath, File toFile) throws Exception {
File fromFile = new File(fromPath);
List<File> files = IO.listFile(fromFile);
String basePath = files.getFirst().getParentFile().getAbsolutePath();
initByteProgress(IO.calcSize(fromFile));
try (SevenZOutputFile outputFile = new SevenZOutputFile(toFile)) {
for (File sourceFile : files) {
String name = sourceFile.getAbsolutePath().substring(basePath.length() + 1);
SevenZArchiveEntry entry = outputFile.createArchiveEntry(sourceFile, normalizeEntryName(name));
outputFile.putArchiveEntry(entry);
writeSevenZEntry(outputFile, sourceFile);
outputFile.closeArchiveEntry();
handleFile(sourceFile);
}
outputFile.finish();
finishProgress();
} catch (Exception exception) {
if (isInterrupt) {
IO.destroy(toFile);
}
throw exception;
} finally {
resetProgress();
} }
out.close();
if (isInterrupt) { if (isInterrupt) {
IO.destroy(toFile); IO.destroy(toFile);
} }
} }
/**
* 写入 7z 条目内容
*
* @param outputFile 7z 输出文件
* @param sourceFile 源文件
* @throws Exception 写入失败
*/
private void writeSevenZEntry(SevenZOutputFile outputFile, File sourceFile) throws Exception {
byte[] buffer = new byte[8192];
try (InputStream inputStream = IO.getInputStream(sourceFile)) {
int length;
while ((length = inputStream.read(buffer)) != -1) {
awaitIfPaused();
ensureRunning();
outputFile.write(buffer, 0, length);
handleTransferred(length);
}
}
}
} }

View File

@@ -5,53 +5,143 @@ import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
import org.apache.commons.compress.archivers.sevenz.SevenZFile; import org.apache.commons.compress.archivers.sevenz.SevenZFile;
import java.io.File; import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
/** /**
* 7z 解压器
*
* @author 夜雨 * @author 夜雨
* @version 2024-06-30 19:42 * @version 2024-06-30 19:42
*/ */
public class Z7Decompressor extends Decompressor { public class Z7Decompressor extends Decompressor<Z7Decompressor> {
/**
* 创建解压器
*
* @param fromFile 源压缩文件
* @return 解压器
*/
public static Z7Decompressor of(File fromFile) {
return new Z7Decompressor().from(fromFile);
}
/**
* 创建解压器
*
* @param fromPath 源压缩文件路径
* @return 解压器
*/
public static Z7Decompressor of(String fromPath) {
return new Z7Decompressor().from(fromPath);
}
/**
* 创建解压器
*
* @param fromStream 源压缩输入流
* @return 解压器
*/
public static Z7Decompressor of(InputStream fromStream) {
return new Z7Decompressor().from(fromStream);
}
@Override @Override
public void run(File fromFile, String toPath) throws Exception { protected void toPath(File fromFile, String toPath) throws Exception {
toPath = new File(toPath).getAbsolutePath() + SEP; initByteProgress(readTotalSize(fromFile));
try {
int total = 0; super.toPath(fromFile, toPath);
if (progressCallback != null) { } finally {
SevenZFile file = SevenZFile.builder().setFile(fromFile).get(); resetProgress();
try (file) {
while (file.getNextEntry() != null) {
total++;
}
}
} }
SevenZFile file = SevenZFile.builder().setFile(fromFile).get(); }
SevenZArchiveEntry entry;
for (int i = 0; (entry = file.getNextEntry()) != null; i++) {
if (entry.isDirectory()) {
IO.dir(IO.fitPath(toPath) + entry.getName());
} else {
File toFile = IO.file(toPath + entry.getName());
byte[] buffer = new byte[(int) entry.getSize()];
file.read(buffer, 0, buffer.length);
IO.toFile(toFile, buffer);
if (fileCallback != null) { @Override
fileCallback.handler(toFile); protected void toPath(InputStream fromStream, String toPath) throws Exception {
Path tempFile = writeTempFile(fromStream);
try (SevenZFile file = SevenZFile.builder().setFile(tempFile.toFile()).get()) {
SevenZArchiveEntry entry;
boolean processed = false;
while ((entry = file.getNextEntry()) != null) {
if (entry.isDirectory()) {
IO.dir(IO.fitPath(toPath) + entry.getName());
} else {
File toFile = IO.file(IO.fitPath(toPath) + entry.getName());
try (OutputStream outputStream = IO.getOutputStream(toFile)) {
copySevenZEntry(file, entry, outputStream);
outputStream.flush();
}
handleFile(toFile);
processed = true;
} }
} }
if (isPause) { if (processed) {
synchronized (pauseLock) { finishProgress();
pauseLock.wait();
}
} }
if (progressCallback != null && total != -1) { } finally {
progressCallback.handler(1D * i / total); Files.deleteIfExists(tempFile);
} }
if (isInterrupt) { }
/**
* 将输入流写入临时文件
*
* @param fromStream 输入流
* @return 临时文件路径
* @throws Exception 写入失败
*/
private Path writeTempFile(InputStream fromStream) throws Exception {
Path tempFile = Files.createTempFile("timi-compress-", ".7z");
try (OutputStream outputStream = Files.newOutputStream(tempFile)) {
transfer(fromStream, outputStream, false);
outputStream.flush();
}
return tempFile;
}
/**
* 复制当前 7z 条目数据
*
* @param file 7z 文件
* @param entry 当前条目
* @param toStream 输出流
* @throws Exception 复制失败
*/
private void copySevenZEntry(SevenZFile file, SevenZArchiveEntry entry, OutputStream toStream) throws Exception {
byte[] buffer = new byte[8192];
long remain = entry.getSize();
while (0 < remain) {
awaitIfPaused();
ensureRunning();
int length = file.read(buffer, 0, (int) Math.min(buffer.length, remain));
if (length < 0) {
break; break;
} }
toStream.write(buffer, 0, length);
remain -= length;
handleTransferred(length);
} }
file.close(); }
/**
* 读取压缩包解压总字节数
*
* @param fromFile 压缩文件
* @return 总字节数
* @throws Exception 读取失败
*/
private long readTotalSize(File fromFile) throws Exception {
long totalSize = 0L;
try (SevenZFile file = SevenZFile.builder().setFile(fromFile).get()) {
SevenZArchiveEntry entry;
while ((entry = file.getNextEntry()) != null) {
if (!entry.isDirectory()) {
totalSize += entry.getSize();
}
}
}
return totalSize;
} }
} }

View File

@@ -4,57 +4,60 @@ import com.imyeyu.io.IO;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.File; import java.io.File;
import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.List; import java.util.List;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
/** /**
* Zip 压缩器
*
* @author 夜雨 * @author 夜雨
* @version 2024-06-30 19:46 * @version 2024-06-30 19:46
*/ */
public class ZipCompressor extends Compressor { public class ZipCompressor extends Compressor<ZipCompressor> {
/**
* 创建压缩器
*
* @param fromPath 源路径
* @return 压缩器
*/
public static ZipCompressor of(String fromPath) {
return new ZipCompressor().from(fromPath);
}
/**
* 创建压缩器
*
* @param fromFile 源文件
* @return 压缩器
*/
public static ZipCompressor of(File fromFile) {
return new ZipCompressor().from(fromFile);
}
@Override @Override
public void run(String fromPath, File toFile) throws Exception { protected void toStream(String fromPath, OutputStream toStream) throws Exception {
List<File> files = IO.listFile(new File(fromPath)); File fromFile = new File(fromPath);
List<File> files = IO.listFile(fromFile);
String basePath = files.getFirst().getParentFile().getAbsolutePath(); String basePath = files.getFirst().getParentFile().getAbsolutePath();
initByteProgress(IO.calcSize(fromFile));
OutputStream os = IO.getOutputStream(toFile); try (ZipOutputStream zipOutputStream = new ZipOutputStream(new BufferedOutputStream(nonClosing(toStream)))) {
BufferedOutputStream byteOS = new BufferedOutputStream(os); for (File sourceFile : files) {
ZipOutputStream zipOS = new ZipOutputStream(byteOS); String name = sourceFile.getAbsolutePath().substring(basePath.length() + 1);
zipOutputStream.putNextEntry(new ZipEntry(normalizeEntryName(name)));
for (int i = 0, total = files.size(); i < total; i++) { try (InputStream inputStream = IO.getInputStream(sourceFile)) {
String name = files.get(i).getAbsolutePath().substring(basePath.length() + 1); transfer(inputStream, zipOutputStream);
ZipEntry zipEntry = new ZipEntry(name.replaceAll("\\\\", "/"));
zipOS.putNextEntry(zipEntry);
zipOS.write(IO.toBytes(files.get(i)));
zipOS.closeEntry();
if (isPause) {
synchronized (pauseLock) {
pauseLock.wait();
} }
zipOutputStream.closeEntry();
handleFile(sourceFile);
} }
if (fileCallback != null) { zipOutputStream.finish();
fileCallback.handler(toFile); finishProgress();
} } finally {
if (progressCallback != null) { resetProgress();
progressCallback.handler(1D * i / total);
}
if (isInterrupt) {
break;
}
}
zipOS.finish();
zipOS.close();
byteOS.flush();
byteOS.close();
os.flush();
os.close();
if (isInterrupt) {
IO.destroy(toFile);
} }
} }
} }

View File

@@ -3,54 +3,110 @@ package com.imyeyu.compress;
import com.imyeyu.io.IO; import com.imyeyu.io.IO;
import java.io.File; import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipFile; import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
/** /**
* Zip 解压器
*
* @author 夜雨 * @author 夜雨
* @version 2024-06-30 19:47 * @version 2024-06-30 19:47
*/ */
public class ZipDecompressor extends Decompressor { public class ZipDecompressor extends Decompressor<ZipDecompressor> {
/**
* 创建解压器
*
* @param fromFile 源压缩文件
* @return 解压器
*/
public static ZipDecompressor of(File fromFile) {
return new ZipDecompressor().from(fromFile);
}
/**
* 创建解压器
*
* @param fromPath 源压缩文件路径
* @return 解压器
*/
public static ZipDecompressor of(String fromPath) {
return new ZipDecompressor().from(fromPath);
}
/**
* 创建解压器
*
* @param fromStream 源压缩输入流
* @return 解压器
*/
public static ZipDecompressor of(InputStream fromStream) {
return new ZipDecompressor().from(fromStream);
}
@Override @Override
public void run(File fromFile, String toPath) throws Exception { protected void toPath(File fromFile, String toPath) throws Exception {
ZipFile zip = new ZipFile(fromFile); initByteProgress(readTotalSize(fromFile));
try {
super.toPath(fromFile, toPath);
} finally {
resetProgress();
}
}
Enumeration<? extends ZipEntry> entries = zip.entries(); @Override
int total = 0; protected void toPath(InputStream fromStream, String toPath) throws Exception {
while (entries.hasMoreElements()) { try (ZipInputStream zipInputStream = new ZipInputStream(nonClosing(fromStream))) {
if (!entries.nextElement().isDirectory()) { ZipEntry entry;
total++; boolean processed = false;
while ((entry = zipInputStream.getNextEntry()) != null) {
String path = IO.fitPath(toPath) + entry.getName();
if (entry.isDirectory()) {
IO.dir(path);
} else {
File toFile = IO.file(path);
try (OutputStream outputStream = IO.getOutputStream(toFile)) {
transfer(zipInputStream, outputStream);
outputStream.flush();
}
handleFile(toFile);
processed = true;
}
zipInputStream.closeEntry();
}
if (processed) {
finishProgress();
} }
} }
}
entries = zip.entries(); /**
ZipEntry entry; * 读取压缩包解压总字节数
for (int i = 0; entries.hasMoreElements(); i++) { *
entry = entries.nextElement(); * @param fromFile 压缩文件
if (entry.isDirectory()) { * @return 总字节数,未知时返回 -1
IO.dir(IO.fitPath(toPath) + entry.getName()); * @throws Exception 读取失败
} else { */
File toFile = IO.file(IO.fitPath(toPath) + entry.getName()); private long readTotalSize(File fromFile) throws Exception {
IO.toFile(toFile, zip.getInputStream(entry)); long totalSize = 0L;
try (ZipFile zipFile = new ZipFile(fromFile)) {
if (fileCallback != null) { Enumeration<? extends ZipEntry> entries = zipFile.entries();
fileCallback.handler(toFile); while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (entry.isDirectory()) {
continue;
} }
} long size = entry.getSize();
if (isPause) { if (size < 0L) {
synchronized (pauseLock) { return -1L;
pauseLock.wait();
} }
} totalSize += size;
if (progressCallback != null) {
progressCallback.handler(1D * i / total);
}
if (isInterrupt) {
break;
} }
} }
zip.close(); return totalSize;
} }
} }

View File

@@ -1,10 +1,17 @@
package test; package test;
import com.imyeyu.compress.CompressType; import com.imyeyu.compress.CompressType;
import com.imyeyu.compress.GZipCompressor;
import com.imyeyu.compress.GZipDecompressor;
import com.imyeyu.io.IO; import com.imyeyu.io.IO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/** /**
* @author 夜雨 * @author 夜雨
@@ -13,15 +20,62 @@ import java.io.File;
public class GzipTest { public class GzipTest {
@Test @Test
public void testCompress() throws Exception { public void testStaticCompress() throws Exception {
File out = IO.file("testOut/test.gz"); GZipCompressor compressor = GZipCompressor.of("testSrc").toFile("testOut/test.gz");
CompressType.Z7.getCompressor().run("testSrc", out); Assertions.assertNotNull(compressor);
}
@Test
public void testChainCompress() throws Exception {
File out = IO.file("testOut/test-chain.gz");
AtomicInteger fileCount = new AtomicInteger();
AtomicReference<Double> progress = new AtomicReference<>(0D);
GZipCompressor compressor = GZipCompressor.of("testSrc");
Assertions.assertSame(
compressor,
compressor
.fileCallback(file -> fileCount.incrementAndGet())
.progressCallback(progress::set)
.toFile(out)
);
Assertions.assertTrue(0 < fileCount.get());
Assertions.assertEquals(1D, progress.get());
} }
@Test @Test
public void testDecompress() throws Exception { public void testDecompress() throws Exception {
File in = IO.file("testOut/test.gz"); File in = IO.file("testOut/test.gz");
GZipCompressor.of("testSrc").toFile(in);
File out = IO.dir("testOutDe"); File out = IO.dir("testOutDe");
CompressType.fromFile(in).run(in, out.getAbsolutePath()); CompressType.fromFile(in).toPath(out.getAbsolutePath());
}
@Test
public void testCompressToStream() throws Exception {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
CompressType.GZIP.ofCompress("testSrc").toStream(outputStream);
}
}
@Test
public void testChainDecompressFromStream() throws Exception {
byte[] bytes;
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
CompressType.GZIP.ofCompress("testSrc").toStream(outputStream);
bytes = outputStream.toByteArray();
}
File out = IO.dir("testOutDeStream/gzip");
AtomicInteger fileCount = new AtomicInteger();
AtomicReference<Double> progress = new AtomicReference<>(0D);
GZipDecompressor decompressor = GZipDecompressor.of(new ByteArrayInputStream(bytes));
Assertions.assertSame(
decompressor,
decompressor
.fileCallback(file -> fileCount.incrementAndGet())
.progressCallback(progress::set)
.toPath(out.getAbsolutePath())
);
Assertions.assertTrue(0 < fileCount.get());
Assertions.assertEquals(1D, progress.get());
} }
} }

View File

@@ -1,10 +1,17 @@
package test; package test;
import com.imyeyu.compress.CompressType; import com.imyeyu.compress.CompressType;
import com.imyeyu.compress.TarCompressor;
import com.imyeyu.compress.TarDecompressor;
import com.imyeyu.io.IO; import com.imyeyu.io.IO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/** /**
* @author 夜雨 * @author 夜雨
@@ -13,15 +20,62 @@ import java.io.File;
public class TarTest { public class TarTest {
@Test @Test
public void testCompress() throws Exception { public void testStaticCompress() throws Exception {
File out = IO.file("testOut/test.tar"); TarCompressor compressor = TarCompressor.of("testSrc").toFile("testOut/test.tar");
CompressType.Z7.getCompressor().run("testSrc", out); Assertions.assertNotNull(compressor);
}
@Test
public void testChainCompress() throws Exception {
File out = IO.file("testOut/test-chain.tar");
AtomicInteger fileCount = new AtomicInteger();
AtomicReference<Double> progress = new AtomicReference<>(0D);
TarCompressor compressor = TarCompressor.of("testSrc");
Assertions.assertSame(
compressor,
compressor
.fileCallback(file -> fileCount.incrementAndGet())
.progressCallback(progress::set)
.toFile(out)
);
Assertions.assertTrue(0 < fileCount.get());
Assertions.assertEquals(1D, progress.get());
} }
@Test @Test
public void testDecompress() throws Exception { public void testDecompress() throws Exception {
File in = IO.file("testOut/test.tar"); File in = IO.file("testOut/test.tar");
TarCompressor.of("testSrc").toFile(in);
File out = IO.dir("testOutDe"); File out = IO.dir("testOutDe");
CompressType.fromFile(in).run(in, out.getAbsolutePath()); CompressType.fromFile(in).toPath(out.getAbsolutePath());
}
@Test
public void testCompressToStream() throws Exception {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
CompressType.TAR.ofCompress("testSrc").toStream(outputStream);
}
}
@Test
public void testChainDecompressFromStream() throws Exception {
byte[] bytes;
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
CompressType.TAR.ofCompress("testSrc").toStream(outputStream);
bytes = outputStream.toByteArray();
}
File out = IO.dir("testOutDeStream/tar");
AtomicInteger fileCount = new AtomicInteger();
AtomicReference<Double> progress = new AtomicReference<>(0D);
TarDecompressor decompressor = TarDecompressor.of(new ByteArrayInputStream(bytes));
Assertions.assertSame(
decompressor,
decompressor
.fileCallback(file -> fileCount.incrementAndGet())
.progressCallback(progress::set)
.toPath(out.getAbsolutePath())
);
Assertions.assertTrue(0 < fileCount.get());
Assertions.assertEquals(1D, progress.get());
} }
} }

View File

@@ -1,10 +1,17 @@
package test; package test;
import com.imyeyu.compress.CompressType; import com.imyeyu.compress.CompressType;
import com.imyeyu.compress.Z7Compressor;
import com.imyeyu.compress.Z7Decompressor;
import com.imyeyu.io.IO; import com.imyeyu.io.IO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/** /**
* @author 夜雨 * @author 夜雨
@@ -13,15 +20,62 @@ import java.io.File;
public class Z7Test { public class Z7Test {
@Test @Test
public void testCompress() throws Exception { public void testStaticCompress() throws Exception {
File out = IO.file("testOut/test.7z"); Z7Compressor compressor = Z7Compressor.of("testSrc").toFile("testOut/test.7z");
CompressType.Z7.getCompressor().run("testSrc", out); Assertions.assertNotNull(compressor);
}
@Test
public void testChainCompress() throws Exception {
File out = IO.file("testOut/test-chain.7z");
AtomicInteger fileCount = new AtomicInteger();
AtomicReference<Double> progress = new AtomicReference<>(0D);
Z7Compressor compressor = Z7Compressor.of("testSrc");
Assertions.assertSame(
compressor,
compressor
.fileCallback(file -> fileCount.incrementAndGet())
.progressCallback(progress::set)
.toFile(out)
);
Assertions.assertTrue(0 < fileCount.get());
Assertions.assertEquals(1D, progress.get());
} }
@Test @Test
public void testDecompress() throws Exception { public void testDecompress() throws Exception {
File in = IO.file("testOut/test.7z"); File in = IO.file("testOut/test.7z");
Z7Compressor.of("testSrc").toFile(in);
File out = IO.dir("testOutDe"); File out = IO.dir("testOutDe");
CompressType.fromFile(in).run(in, out.getAbsolutePath()); CompressType.fromFile(in).toPath(out.getAbsolutePath());
}
@Test
public void testCompressToStream() throws Exception {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
CompressType.Z7.ofCompress("testSrc").toStream(outputStream);
}
}
@Test
public void testChainDecompressFromStream() throws Exception {
byte[] bytes;
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
CompressType.Z7.ofCompress("testSrc").toStream(outputStream);
bytes = outputStream.toByteArray();
}
File out = IO.dir("testOutDeStream/z7");
AtomicInteger fileCount = new AtomicInteger();
AtomicReference<Double> progress = new AtomicReference<>(0D);
Z7Decompressor decompressor = Z7Decompressor.of(new ByteArrayInputStream(bytes));
Assertions.assertSame(
decompressor,
decompressor
.fileCallback(file -> fileCount.incrementAndGet())
.progressCallback(progress::set)
.toPath(out.getAbsolutePath())
);
Assertions.assertTrue(0 < fileCount.get());
Assertions.assertEquals(1D, progress.get());
} }
} }

View File

@@ -1,10 +1,17 @@
package test; package test;
import com.imyeyu.compress.CompressType; import com.imyeyu.compress.CompressType;
import com.imyeyu.compress.ZipCompressor;
import com.imyeyu.compress.ZipDecompressor;
import com.imyeyu.io.IO; import com.imyeyu.io.IO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/** /**
* @author 夜雨 * @author 夜雨
@@ -13,15 +20,62 @@ import java.io.File;
public class ZipTest { public class ZipTest {
@Test @Test
public void testCompress() throws Exception { public void testStaticCompress() throws Exception {
File out = IO.file("testOut/test.zip"); ZipCompressor compressor = ZipCompressor.of("testSrc").toFile("testOut/test.zip");
CompressType.Z7.getCompressor().run("testSrc", out); Assertions.assertNotNull(compressor);
}
@Test
public void testChainCompress() throws Exception {
File out = IO.file("testOut/test-chain.zip");
AtomicInteger fileCount = new AtomicInteger();
AtomicReference<Double> progress = new AtomicReference<>(0D);
ZipCompressor compressor = ZipCompressor.of("testSrc");
Assertions.assertSame(
compressor,
compressor
.fileCallback(file -> fileCount.incrementAndGet())
.progressCallback(progress::set)
.toFile(out)
);
Assertions.assertTrue(0 < fileCount.get());
Assertions.assertEquals(1D, progress.get());
} }
@Test @Test
public void testDecompress() throws Exception { public void testDecompress() throws Exception {
File in = IO.file("testOut/test.zip"); File in = IO.file("testOut/test.zip");
ZipCompressor.of("testSrc").toFile(in);
File out = IO.dir("testOutDe"); File out = IO.dir("testOutDe");
CompressType.fromFile(in).run(in, out.getAbsolutePath()); CompressType.fromFile(in).toPath(out.getAbsolutePath());
}
@Test
public void testCompressToStream() throws Exception {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
CompressType.ZIP.ofCompress("testSrc").toStream(outputStream);
}
}
@Test
public void testChainDecompressFromStream() throws Exception {
byte[] bytes;
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
CompressType.ZIP.ofCompress("testSrc").toStream(outputStream);
bytes = outputStream.toByteArray();
}
File out = IO.dir("testOutDeStream/zip");
AtomicInteger fileCount = new AtomicInteger();
AtomicReference<Double> progress = new AtomicReference<>(0D);
ZipDecompressor decompressor = ZipDecompressor.of(new ByteArrayInputStream(bytes));
Assertions.assertSame(
decompressor,
decompressor
.fileCallback(file -> fileCount.incrementAndGet())
.progressCallback(progress::set)
.toPath(out.getAbsolutePath())
);
Assertions.assertTrue(0 < fileCount.get());
Assertions.assertEquals(1D, progress.get());
} }
} }