diff --git a/CHANGELOG.md b/CHANGELOG.md
index d38d14e5b..da5e4695e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@
* 【core 】 NumberUtil.toBigDecimal空白符转换为0(issue#I24MRP@Gitee)
* 【core 】 CollUtil和IterUtil增加size方法(pr#208@Gitee)
* 【extra 】 新增SimpleFtpServer
+* 【extra 】 新增CompressUtil压缩封装
### Bug修复
* 【core 】 修复DateUtil.current使用System.nanoTime的问题(issue#1198@Github)
diff --git a/hutool-extra/pom.xml b/hutool-extra/pom.xml
index 2f448e775..88db4ba94 100644
--- a/hutool-extra/pom.xml
+++ b/hutool-extra/pom.xml
@@ -411,5 +411,18 @@
true
+
+ org.apache.commons
+ commons-compress
+ 1.20
+ compile
+ true
+
+
+ org.tukaani
+ xz
+ 1.8
+ test
+
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/compress/CompressException.java b/hutool-extra/src/main/java/cn/hutool/extra/compress/CompressException.java
new file mode 100644
index 000000000..c182b1151
--- /dev/null
+++ b/hutool-extra/src/main/java/cn/hutool/extra/compress/CompressException.java
@@ -0,0 +1,33 @@
+package cn.hutool.extra.compress;
+
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.util.StrUtil;
+
+/**
+ * 压缩解压异常语言异常
+ *
+ * @author Looly
+ */
+public class CompressException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public CompressException(Throwable e) {
+ super(ExceptionUtil.getMessage(e), e);
+ }
+
+ public CompressException(String message) {
+ super(message);
+ }
+
+ public CompressException(String messageTemplate, Object... params) {
+ super(StrUtil.format(messageTemplate, params));
+ }
+
+ public CompressException(String message, Throwable throwable) {
+ super(message, throwable);
+ }
+
+ public CompressException(Throwable throwable, String messageTemplate, Object... params) {
+ super(StrUtil.format(messageTemplate, params), throwable);
+ }
+}
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/compress/CompressUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/compress/CompressUtil.java
new file mode 100644
index 000000000..5924ec054
--- /dev/null
+++ b/hutool-extra/src/main/java/cn/hutool/extra/compress/CompressUtil.java
@@ -0,0 +1,66 @@
+package cn.hutool.extra.compress;
+
+import cn.hutool.extra.compress.archiver.Archiver;
+import cn.hutool.extra.compress.archiver.SevenZArchiver;
+import cn.hutool.extra.compress.archiver.StreamArchiver;
+import org.apache.commons.compress.archivers.ArchiveStreamFactory;
+
+import java.io.File;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+
+/**
+ * 压缩工具类
+ * 基于commons-compress的压缩解压封装
+ *
+ * @since 5.5.0
+ * @author looly
+ */
+public class CompressUtil {
+
+ /**
+ * 创建归档器,支持:
+ *
+ * - {@link ArchiveStreamFactory#AR}
+ * - {@link ArchiveStreamFactory#CPIO}
+ * - {@link ArchiveStreamFactory#JAR}
+ * - {@link ArchiveStreamFactory#TAR}
+ * - {@link ArchiveStreamFactory#ZIP}
+ * - {@link ArchiveStreamFactory#SEVEN_Z}
+ *
+ *
+ * @param charset 编码
+ * @param archiverName 归档类型名称,见{@link ArchiveStreamFactory}
+ * @param file 归档输出的文件
+ * @return Archiver
+ */
+ public static Archiver createArchiver(Charset charset, String archiverName, File file) {
+ if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(archiverName)) {
+ return new SevenZArchiver(file);
+ }
+ return StreamArchiver.create(charset, archiverName, file);
+ }
+
+ /**
+ * 创建归档器,支持:
+ *
+ * - {@link ArchiveStreamFactory#AR}
+ * - {@link ArchiveStreamFactory#CPIO}
+ * - {@link ArchiveStreamFactory#JAR}
+ * - {@link ArchiveStreamFactory#TAR}
+ * - {@link ArchiveStreamFactory#ZIP}
+ * - {@link ArchiveStreamFactory#SEVEN_Z}
+ *
+ *
+ * @param charset 编码
+ * @param archiverName 归档类型名称,见{@link ArchiveStreamFactory}
+ * @param out 归档输出的流
+ * @return Archiver
+ */
+ public static Archiver createArchiver(Charset charset, String archiverName, OutputStream out) {
+ if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(archiverName)) {
+ return new SevenZArchiver(out);
+ }
+ return StreamArchiver.create(charset, archiverName, out);
+ }
+}
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/compress/Extractor.java b/hutool-extra/src/main/java/cn/hutool/extra/compress/Extractor.java
new file mode 100644
index 000000000..7819de07f
--- /dev/null
+++ b/hutool-extra/src/main/java/cn/hutool/extra/compress/Extractor.java
@@ -0,0 +1,4 @@
+package cn.hutool.extra.compress;
+
+public class Extractor {
+}
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/compress/archiver/Archiver.java b/hutool-extra/src/main/java/cn/hutool/extra/compress/archiver/Archiver.java
new file mode 100644
index 000000000..c13f3a4e7
--- /dev/null
+++ b/hutool-extra/src/main/java/cn/hutool/extra/compress/archiver/Archiver.java
@@ -0,0 +1,59 @@
+package cn.hutool.extra.compress.archiver;
+
+import cn.hutool.core.util.StrUtil;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileFilter;
+
+/**
+ * 数据归档封装,归档即将几个文件或目录打成一个压缩包
+ *
+ * @author looly
+ */
+public interface Archiver extends Closeable {
+
+ /**
+ * 将文件或目录加入归档,目录采取递归读取方式按照层级加入
+ *
+ * @param file 文件或目录
+ * @return this
+ */
+ default Archiver add(File file) {
+ return add(file, null);
+ }
+
+ /**
+ * 将文件或目录加入归档,目录采取递归读取方式按照层级加入
+ *
+ * @param file 文件或目录
+ * @param filter 文件过滤器,指定哪些文件或目录可以加入,当{@link FileFilter#accept(File)}为true时加入。
+ * @return this
+ */
+ default Archiver add(File file, FileFilter filter) {
+ return add(file, StrUtil.SLASH, filter);
+ }
+
+ /**
+ * 将文件或目录加入归档包,目录采取递归读取方式按照层级加入
+ *
+ * @param file 文件或目录
+ * @param path 文件或目录的初始路径,null表示位于根路径
+ * @param filter 文件过滤器,指定哪些文件或目录可以加入,当{@link FileFilter#accept(File)}为true时加入。
+ * @return this
+ */
+ Archiver add(File file, String path, FileFilter filter);
+
+ /**
+ * 结束已经增加的文件归档,此方法不会关闭归档流,可以继续添加文件
+ *
+ * @return this
+ */
+ Archiver finish();
+
+ /**
+ * 无异常关闭
+ */
+ @Override
+ void close();
+}
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/compress/archiver/SevenZArchiver.java b/hutool-extra/src/main/java/cn/hutool/extra/compress/archiver/SevenZArchiver.java
new file mode 100644
index 000000000..9612defd7
--- /dev/null
+++ b/hutool-extra/src/main/java/cn/hutool/extra/compress/archiver/SevenZArchiver.java
@@ -0,0 +1,136 @@
+package cn.hutool.extra.compress.archiver;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IORuntimeException;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile;
+import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.channels.SeekableByteChannel;
+
+/**
+ * 7zip格式的归档封装
+ *
+ * @author looly
+ */
+public class SevenZArchiver implements Archiver {
+
+ private final SevenZOutputFile sevenZOutputFile;
+
+ private SeekableByteChannel channel;
+ private OutputStream out;
+
+ /**
+ * 构造
+ *
+ * @param file 归档输出的文件
+ */
+ public SevenZArchiver(File file) {
+ try {
+ this.sevenZOutputFile = new SevenZOutputFile(file);
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ }
+ }
+
+ /**
+ * 构造
+ *
+ * @param out 归档输出的流
+ */
+ public SevenZArchiver(OutputStream out) {
+ this.out = out;
+ this.channel = new SeekableInMemoryByteChannel();
+ try {
+ this.sevenZOutputFile = new SevenZOutputFile(channel);
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ }
+ }
+
+ /**
+ * 构造
+ *
+ * @param channel 归档输出的文件
+ */
+ public SevenZArchiver(SeekableByteChannel channel) {
+ try {
+ this.sevenZOutputFile = new SevenZOutputFile(channel);
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ }
+ }
+
+ @Override
+ public SevenZArchiver add(File file, String path, FileFilter filter) {
+ try {
+ addInternal(file, path, filter);
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public SevenZArchiver finish() {
+ try {
+ this.sevenZOutputFile.finish();
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public void close() {
+ try {
+ finish();
+ } catch (Exception ignore) {
+ //ignore
+ }
+ if(null != out && this.channel instanceof SeekableInMemoryByteChannel){
+ try {
+ out.write(((SeekableInMemoryByteChannel)this.channel).array());
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ }
+ }
+ IoUtil.close(this.sevenZOutputFile);
+ }
+
+ /**
+ * 将文件或目录加入归档包,目录采取递归读取方式按照层级加入
+ *
+ * @param file 文件或目录
+ * @param path 文件或目录的初始路径,null表示位于根路径
+ * @param filter 文件过滤器,指定哪些文件或目录可以加入,当{@link FileFilter#accept(File)}为true时加入。
+ */
+ private void addInternal(File file, String path, FileFilter filter) throws IOException {
+ if (null != filter && false == filter.accept(file)) {
+ return;
+ }
+ final SevenZOutputFile out = this.sevenZOutputFile;
+
+ final String entryName = StrUtil.addSuffixIfNot(StrUtil.nullToEmpty(path), StrUtil.SLASH) + file.getName();
+ out.putArchiveEntry(out.createArchiveEntry(file, entryName));
+
+ if (file.isDirectory()) {
+ // 目录遍历写入
+ final File[] files = file.listFiles();
+ for (File childFile : files) {
+ addInternal(childFile, entryName, filter);
+ }
+ } else {
+ if (file.isFile()) {
+ // 文件直接写入
+ out.write(FileUtil.readBytes(file));
+ }
+ out.closeArchiveEntry();
+ }
+ }
+}
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/compress/archiver/StreamArchiver.java b/hutool-extra/src/main/java/cn/hutool/extra/compress/archiver/StreamArchiver.java
new file mode 100644
index 000000000..fb2feac82
--- /dev/null
+++ b/hutool-extra/src/main/java/cn/hutool/extra/compress/archiver/StreamArchiver.java
@@ -0,0 +1,169 @@
+package cn.hutool.extra.compress.archiver;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IORuntimeException;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.compress.CompressException;
+import org.apache.commons.compress.archivers.ArchiveException;
+import org.apache.commons.compress.archivers.ArchiveOutputStream;
+import org.apache.commons.compress.archivers.ArchiveStreamFactory;
+import org.apache.commons.compress.archivers.ar.ArArchiveOutputStream;
+import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+
+/**
+ * 数据归档封装,归档即将几个文件或目录打成一个压缩包
+ * 支持的归档文件格式为:
+ *
+ * - {@link ArchiveStreamFactory#AR}
+ * - {@link ArchiveStreamFactory#CPIO}
+ * - {@link ArchiveStreamFactory#JAR}
+ * - {@link ArchiveStreamFactory#TAR}
+ * - {@link ArchiveStreamFactory#ZIP}
+ *
+ *
+ * @author looly
+ */
+public class StreamArchiver implements Archiver {
+
+ /**
+ * 创建归档器
+ *
+ * @param charset 编码
+ * @param archiverName 归档类型名称,见{@link ArchiveStreamFactory}
+ * @param file 归档输出的文件
+ * @return StreamArchiver
+ */
+ public static StreamArchiver create(Charset charset, String archiverName, File file) {
+ return new StreamArchiver(charset, archiverName, file);
+ }
+
+ /**
+ * 创建归档器
+ *
+ * @param charset 编码
+ * @param archiverName 归档类型名称,见{@link ArchiveStreamFactory}
+ * @param out 归档输出的流
+ * @return StreamArchiver
+ */
+ public static StreamArchiver create(Charset charset, String archiverName, OutputStream out) {
+ return new StreamArchiver(charset, archiverName, out);
+ }
+
+ private ArchiveOutputStream out;
+
+ /**
+ * 构造
+ *
+ * @param charset 编码
+ * @param archiverName 归档类型名称,见{@link ArchiveStreamFactory}
+ * @param file 归档输出的文件
+ */
+ public StreamArchiver(Charset charset, String archiverName, File file) {
+ this(charset, archiverName, FileUtil.getOutputStream(file));
+ }
+
+ /**
+ * 构造
+ *
+ * @param charset 编码
+ * @param archiverName 归档类型名称,见{@link ArchiveStreamFactory}
+ * @param targetStream 归档输出的流
+ */
+ public StreamArchiver(Charset charset, String archiverName, OutputStream targetStream) {
+ final ArchiveStreamFactory factory = new ArchiveStreamFactory(charset.name());
+ try {
+ this.out = factory.createArchiveOutputStream(archiverName, targetStream);
+ } catch (ArchiveException e) {
+ throw new CompressException(e);
+ }
+
+ //特殊设置
+ if(this.out instanceof TarArchiveOutputStream){
+ ((TarArchiveOutputStream)out).setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
+ } else if(this.out instanceof ArArchiveOutputStream){
+ ((ArArchiveOutputStream)out).setLongFileMode(ArArchiveOutputStream.LONGFILE_BSD);
+ }
+ }
+
+ /**
+ * 将文件或目录加入归档包,目录采取递归读取方式按照层级加入
+ *
+ * @param file 文件或目录
+ * @param path 文件或目录的初始路径,null表示位于根路径
+ * @param filter 文件过滤器,指定哪些文件或目录可以加入,当{@link FileFilter#accept(File)}为true时加入。
+ * @return this
+ * @throws IORuntimeException IO异常
+ */
+ @Override
+ public StreamArchiver add(File file, String path, FileFilter filter) throws IORuntimeException {
+ try {
+ addInternal(file, path, filter);
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ }
+ return this;
+ }
+
+ /**
+ * 结束已经增加的文件归档,此方法不会关闭归档流,可以继续添加文件
+ *
+ * @return this
+ */
+ @Override
+ public StreamArchiver finish() {
+ try {
+ this.out.finish();
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public void close() {
+ try {
+ finish();
+ } catch (Exception ignore) {
+ //ignore
+ }
+ IoUtil.close(this.out);
+ }
+
+ /**
+ * 将文件或目录加入归档包,目录采取递归读取方式按照层级加入
+ *
+ * @param file 文件或目录
+ * @param path 文件或目录的初始路径,null表示位于根路径
+ * @param filter 文件过滤器,指定哪些文件或目录可以加入,当{@link FileFilter#accept(File)}为true时加入。
+ */
+ private void addInternal(File file, String path, FileFilter filter) throws IOException {
+ if (null != filter && false == filter.accept(file)) {
+ return;
+ }
+ final ArchiveOutputStream out = this.out;
+
+ final String entryName = StrUtil.addSuffixIfNot(StrUtil.nullToEmpty(path), StrUtil.SLASH) + file.getName();
+ out.putArchiveEntry(out.createArchiveEntry(file, entryName));
+
+ if (file.isDirectory()) {
+ // 目录遍历写入
+ final File[] files = file.listFiles();
+ for (File childFile : files) {
+ addInternal(childFile, entryName, filter);
+ }
+ } else {
+ if (file.isFile()) {
+ // 文件直接写入
+ FileUtil.writeToStream(file, out);
+ }
+ out.closeArchiveEntry();
+ }
+ }
+}
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/compress/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/compress/package-info.java
new file mode 100644
index 000000000..60482c213
--- /dev/null
+++ b/hutool-extra/src/main/java/cn/hutool/extra/compress/package-info.java
@@ -0,0 +1,13 @@
+/**
+ * 基于commons-compress的压缩解压封装
+ * 支持包括:gzip, bzip2, xz, lzma, Pack200, DEFLATE, Brotli, DEFLATE64, ZStandard and Z, the archiver formats are 7z,
+ * ar, arj, cpio, dump, tar and zip等格式。
+ *
+ *
+ * 见:https://commons.apache.org/proper/commons-compress/
+ *
+ *
+ * @author looly
+ *
+ */
+package cn.hutool.extra.compress;
\ No newline at end of file
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/ftp/SimpleFtpServer.java b/hutool-extra/src/main/java/cn/hutool/extra/ftp/SimpleFtpServer.java
index cc8c0341d..f317c6e3c 100644
--- a/hutool-extra/src/main/java/cn/hutool/extra/ftp/SimpleFtpServer.java
+++ b/hutool-extra/src/main/java/cn/hutool/extra/ftp/SimpleFtpServer.java
@@ -27,6 +27,15 @@ import java.util.List;
*/
public class SimpleFtpServer {
+ /**
+ * 创建FTP服务器,调用{@link SimpleFtpServer#start()}启动即可
+ *
+ * @return SimpleFtpServer
+ */
+ public static SimpleFtpServer create() {
+ return new SimpleFtpServer();
+ }
+
FtpServerFactory serverFactory;
ListenerFactory listenerFactory;
@@ -38,15 +47,6 @@ public class SimpleFtpServer {
listenerFactory = new ListenerFactory();
}
- /**
- * 创建FTP服务器,调用{@link SimpleFtpServer#start()}启动即可
- *
- * @return SimpleFtpServer
- */
- public static SimpleFtpServer create() {
- return new SimpleFtpServer();
- }
-
/**
* 获取 {@link FtpServerFactory},用于设置FTP服务器相关信息
*
diff --git a/hutool-extra/src/test/java/cn/hutool/extra/compress/ArchiverTest.java b/hutool-extra/src/test/java/cn/hutool/extra/compress/ArchiverTest.java
new file mode 100644
index 000000000..0b0ee0918
--- /dev/null
+++ b/hutool-extra/src/test/java/cn/hutool/extra/compress/ArchiverTest.java
@@ -0,0 +1,50 @@
+package cn.hutool.extra.compress;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.lang.Console;
+import cn.hutool.core.util.CharsetUtil;
+import cn.hutool.extra.compress.archiver.StreamArchiver;
+import org.apache.commons.compress.archivers.ArchiveStreamFactory;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.File;
+
+public class ArchiverTest {
+
+ @Test
+ @Ignore
+ public void tarTest(){
+ final File file = FileUtil.file("d:/test/compress/test.tar");
+ StreamArchiver.create(CharsetUtil.CHARSET_UTF_8, ArchiveStreamFactory.TAR, file)
+ .add(FileUtil.file("d:/Java"), (f)->{
+ Console.log("Add: {}", f.getPath());
+ return true;
+ })
+ .finish().close();
+ }
+
+ @Test
+ @Ignore
+ public void cpioTest(){
+ final File file = FileUtil.file("d:/test/compress/test.cpio");
+ StreamArchiver.create(CharsetUtil.CHARSET_UTF_8, ArchiveStreamFactory.CPIO, file)
+ .add(FileUtil.file("d:/Java"), (f)->{
+ Console.log("Add: {}", f.getPath());
+ return true;
+ })
+ .finish().close();
+ }
+
+ @Test
+ @Ignore
+ public void senvenZTest(){
+ final File file = FileUtil.file("d:/test/compress/test.7z");
+ CompressUtil.createArchiver(CharsetUtil.CHARSET_UTF_8, ArchiveStreamFactory.SEVEN_Z, file)
+ .add(FileUtil.file("d:/Java"), (f)->{
+ Console.log("Add: {}", f.getPath());
+ return true;
+ })
+ .finish().close();
+ }
+}