Looly 5 年 前
コミット
4b3941b2f8

+ 1 - 0
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)

+ 13 - 0
hutool-extra/pom.xml

@@ -411,5 +411,18 @@
 			<optional>true</optional>
 		</dependency>
 		<!-- 表达式引擎可选依赖 end -->
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-compress</artifactId>
+			<version>1.20</version>
+			<scope>compile</scope>
+			<optional>true</optional>
+		</dependency>
+		<dependency>
+			<groupId>org.tukaani</groupId>
+			<artifactId>xz</artifactId>
+			<version>1.8</version>
+			<scope>test</scope>
+		</dependency>
 	</dependencies>
 </project>

+ 33 - 0
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);
+	}
+}

+ 66 - 0
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;
+
+/**
+ * 压缩工具类<br>
+ * 基于commons-compress的压缩解压封装
+ *
+ * @since 5.5.0
+ * @author looly
+ */
+public class CompressUtil {
+
+	/**
+	 * 创建归档器,支持:
+	 * <ul>
+	 *     <li>{@link ArchiveStreamFactory#AR}</li>
+	 *     <li>{@link ArchiveStreamFactory#CPIO}</li>
+	 *     <li>{@link ArchiveStreamFactory#JAR}</li>
+	 *     <li>{@link ArchiveStreamFactory#TAR}</li>
+	 *     <li>{@link ArchiveStreamFactory#ZIP}</li>
+	 *     <li>{@link ArchiveStreamFactory#SEVEN_Z}</li>
+	 * </ul>
+	 *
+	 * @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);
+	}
+
+	/**
+	 * 创建归档器,支持:
+	 * <ul>
+	 *     <li>{@link ArchiveStreamFactory#AR}</li>
+	 *     <li>{@link ArchiveStreamFactory#CPIO}</li>
+	 *     <li>{@link ArchiveStreamFactory#JAR}</li>
+	 *     <li>{@link ArchiveStreamFactory#TAR}</li>
+	 *     <li>{@link ArchiveStreamFactory#ZIP}</li>
+	 *     <li>{@link ArchiveStreamFactory#SEVEN_Z}</li>
+	 * </ul>
+	 *
+	 * @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);
+	}
+}

+ 4 - 0
hutool-extra/src/main/java/cn/hutool/extra/compress/Extractor.java

@@ -0,0 +1,4 @@
+package cn.hutool.extra.compress;
+
+public class Extractor {
+}

+ 59 - 0
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;
+
+/**
+ * 数据归档封装,归档即将几个文件或目录打成一个压缩包<br>
+ *
+ * @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();
+}

+ 136 - 0
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();
+		}
+	}
+}

+ 169 - 0
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;
+
+/**
+ * 数据归档封装,归档即将几个文件或目录打成一个压缩包<br>
+ * 支持的归档文件格式为:
+ * <ul>
+ *     <li>{@link ArchiveStreamFactory#AR}</li>
+ *     <li>{@link ArchiveStreamFactory#CPIO}</li>
+ *     <li>{@link ArchiveStreamFactory#JAR}</li>
+ *     <li>{@link ArchiveStreamFactory#TAR}</li>
+ *     <li>{@link ArchiveStreamFactory#ZIP}</li>
+ * </ul>
+ *
+ * @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();
+		}
+	}
+}

+ 13 - 0
hutool-extra/src/main/java/cn/hutool/extra/compress/package-info.java

@@ -0,0 +1,13 @@
+/**
+ * 基于commons-compress的压缩解压封装<br>
+ * 支持包括:gzip, bzip2, xz, lzma, Pack200, DEFLATE, Brotli, DEFLATE64, ZStandard and Z, the archiver formats are 7z,<br>
+ * ar, arj, cpio, dump, tar and zip等格式。
+ *
+ * <p>
+ *     见:https://commons.apache.org/proper/commons-compress/
+ * </p>
+ * 
+ * @author looly
+ *
+ */
+package cn.hutool.extra.compress;

+ 9 - 9
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;
 
@@ -39,15 +48,6 @@ public class SimpleFtpServer {
 	}
 
 	/**
-	 * 创建FTP服务器,调用{@link SimpleFtpServer#start()}启动即可
-	 *
-	 * @return SimpleFtpServer
-	 */
-	public static SimpleFtpServer create() {
-		return new SimpleFtpServer();
-	}
-
-	/**
 	 * 获取 {@link FtpServerFactory},用于设置FTP服务器相关信息
 	 *
 	 * @return {@link FtpServerFactory}

+ 50 - 0
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();
+	}
+}