diff --git a/CHANGELOG.md b/CHANGELOG.md index dbfc6203c..3b805ae2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### 🐣新特性 * 【core 】 MapProxy支持return this的setter方法(pr#392@Gitee) * 【core 】 BeeDSFactory移除sqlite事务修复代码,新版本BeeCP已修复 +* 【core 】 增加compress包,扩充Zip操作灵活性 ### 🐞Bug修复 diff --git a/hutool-core/src/main/java/cn/hutool/core/compress/Deflate.java b/hutool-core/src/main/java/cn/hutool/core/compress/Deflate.java new file mode 100755 index 000000000..f579a17da --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/compress/Deflate.java @@ -0,0 +1,98 @@ +package cn.hutool.core.compress; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterOutputStream; + +/** + * Deflate算法
+ * Deflate是同时使用了LZ77算法与哈夫曼编码(Huffman Coding)的一个无损数据压缩算法。 + * + * @author looly + * @since 5.7.8 + */ +public class Deflate implements Closeable { + + private final InputStream source; + private OutputStream target; + private final boolean nowrap; + + /** + * 创建Deflate + * + * @param source 源流 + * @param target 目标流 + * @param nowrap {@code true}表示兼容Gzip压缩 + */ + public static Deflate of(InputStream source, OutputStream target, boolean nowrap) { + return new Deflate(source, target, nowrap); + } + + /** + * 构造 + * + * @param source 源流 + * @param target 目标流 + * @param nowrap {@code true}表示兼容Gzip压缩 + */ + public Deflate(InputStream source, OutputStream target, boolean nowrap) { + this.source = source; + this.target = target; + this.nowrap = nowrap; + } + + /** + * 获取目标流 + * + * @return 目标流 + */ + public OutputStream getTarget() { + return this.target; + } + + /** + * 将普通数据流压缩 + * + * @param level 压缩级别,0~9 + */ + public Deflate deflater(int level) { + target= (target instanceof DeflaterOutputStream) ? + (DeflaterOutputStream) target : new DeflaterOutputStream(target, new Deflater(level, nowrap)); + IoUtil.copy(source, target); + try { + ((DeflaterOutputStream)target).finish(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + /** + * 将压缩流解压到target中 + */ + public Deflate inflater() { + target = (target instanceof InflaterOutputStream) ? + (InflaterOutputStream) target : new InflaterOutputStream(target, new Inflater(nowrap)); + IoUtil.copy(source, target); + try { + ((InflaterOutputStream)target).finish(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + @Override + public void close() { + IoUtil.close(this.target); + IoUtil.close(this.source); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/compress/Gzip.java b/hutool-core/src/main/java/cn/hutool/core/compress/Gzip.java new file mode 100755 index 000000000..9041e408e --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/compress/Gzip.java @@ -0,0 +1,89 @@ +package cn.hutool.core.compress; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + * GZIP是用于Unix系统的文件压缩
+ * gzip的基础是DEFLATE + * + * @author looly + * @since 5.7.8 + */ +public class Gzip implements Closeable { + + private InputStream source; + private OutputStream target; + + /** + * 创建Gzip + * + * @param source 源流 + * @param target 目标流 + */ + public static Gzip of(InputStream source, OutputStream target) { + return new Gzip(source, target); + } + + /** + * 构造 + * + * @param source 源流 + * @param target 目标流 + */ + public Gzip(InputStream source, OutputStream target) { + this.source = source; + this.target = target; + } + + /** + * 获取目标流 + * + * @return 目标流 + */ + public OutputStream getTarget() { + return this.target; + } + + /** + * 将普通数据流压缩 + */ + public Gzip gzip() { + try { + target = (target instanceof GZIPOutputStream) ? + (GZIPOutputStream) target : new GZIPOutputStream(target); + IoUtil.copy(source, target); + ((GZIPOutputStream)target).finish(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + /** + * 将压缩流解压到target中 + */ + public Gzip unGzip() { + try { + source = (source instanceof GZIPInputStream) ? + (GZIPInputStream) source : new GZIPInputStream(source); + IoUtil.copy(source, target); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + @Override + public void close() { + IoUtil.close(this.target); + IoUtil.close(this.source); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/compress/ZipReader.java b/hutool-core/src/main/java/cn/hutool/core/compress/ZipReader.java new file mode 100755 index 000000000..f12f4f194 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/compress/ZipReader.java @@ -0,0 +1,189 @@ +package cn.hutool.core.compress; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ZipUtil; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Enumeration; +import java.util.function.Consumer; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; + +/** + * Zip文件或流读取器,一般用于Zip文件解压 + * + * @author looly + * @since 5.7.8 + */ +public class ZipReader implements Closeable { + + private ZipFile zipFile; + private ZipInputStream in; + + /** + * 创建{@link ZipReader} + * + * @param zipFile 生成的Zip文件 + * @param charset 编码 + * @return {@link ZipReader} + */ + public static ZipReader of(File zipFile, Charset charset) { + return new ZipReader(zipFile, charset); + } + + /** + * 创建{@link ZipReader} + * + * @param in Zip输入的流,一般为输入文件流 + * @param charset 编码 + * @return {@link ZipReader} + */ + public static ZipReader of(InputStream in, Charset charset) { + return new ZipReader(in, charset); + } + + /** + * 构造 + * + * @param zipFile 读取的的Zip文件 + */ + public ZipReader(File zipFile, Charset charset) { + this.zipFile = ZipUtil.toZipFile(zipFile, charset); + } + + /** + * 构造 + * + * @param zipFile 读取的的Zip文件 + */ + public ZipReader(ZipFile zipFile) { + this.zipFile = zipFile; + } + + /** + * 构造 + * + * @param in 读取的的Zip文件流 + */ + public ZipReader(InputStream in, Charset charset) { + this.in = new ZipInputStream(in, charset); + } + + /** + * 构造 + * + * @param zin 读取的的Zip文件流 + */ + public ZipReader(ZipInputStream zin) { + this.in = zin; + } + + /** + * 获取指定路径的文件流 + * + * @param path 路径 + * @return 文件流 + */ + public InputStream get(String path) { + if (null != this.zipFile) { + final ZipFile zipFile = this.zipFile; + final ZipEntry entry = zipFile.getEntry(path); + if (null != entry) { + return ZipUtil.getStream(zipFile, entry); + } + } else { + throw new UnsupportedOperationException("Zip stream mode not support get!"); + } + + return null; + } + + /** + * 解压到指定目录中 + * + * @param outFile 解压到的目录 + * @return 解压的目录 + * @throws IORuntimeException IO异常 + */ + public File readTo(File outFile) throws IORuntimeException { + read((zipEntry) -> { + // FileUtil.file会检查slip漏洞,漏洞说明见http://blog.nsfocus.net/zip-slip-2/ + File outItemFile = FileUtil.file(outFile, zipEntry.getName()); + if (zipEntry.isDirectory()) { + // 目录 + //noinspection ResultOfMethodCallIgnored + outItemFile.mkdirs(); + } else { + InputStream in; + if (null != this.zipFile) { + in = ZipUtil.getStream(this.zipFile, zipEntry); + } else { + in = this.in; + } + // 文件 + FileUtil.writeFromStream(in, outItemFile, false); + } + }); + return outFile; + } + + /** + * 读取并处理Zip文件中的每一个{@link ZipEntry} + * + * @param consumer {@link ZipEntry}处理器 + * @throws IORuntimeException IO异常 + */ + public ZipReader read(Consumer consumer) throws IORuntimeException { + if (null != this.zipFile) { + readFromZipFile(consumer); + } else { + readFromStream(consumer); + } + return this; + } + + @Override + public void close() throws IORuntimeException { + if(null != this.zipFile){ + IoUtil.close(this.zipFile); + } else { + IoUtil.close(this.in); + } + } + + /** + * 读取并处理Zip文件中的每一个{@link ZipEntry} + * + * @param consumer {@link ZipEntry}处理器 + */ + private void readFromZipFile(Consumer consumer) { + final Enumeration em = zipFile.entries(); + while (em.hasMoreElements()) { + consumer.accept(em.nextElement()); + } + } + + /** + * 读取并处理Zip流中的每一个{@link ZipEntry} + * + * @param consumer {@link ZipEntry}处理器 + * @throws IORuntimeException IO异常 + */ + private void readFromStream(Consumer consumer) throws IORuntimeException { + try { + ZipEntry zipEntry; + while (null != (zipEntry = in.getNextEntry())) { + consumer.accept(zipEntry); + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/compress/ZipWriter.java b/hutool-core/src/main/java/cn/hutool/core/compress/ZipWriter.java new file mode 100755 index 000000000..a9a86731f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/compress/ZipWriter.java @@ -0,0 +1,270 @@ +package cn.hutool.core.compress; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.resource.Resource; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.Closeable; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * Zip生成封装 + * + * @author looly + * @since 5.7.8 + */ +public class ZipWriter implements Closeable { + + /** + * 创建{@link ZipWriter} + * + * @param zipFile 生成的Zip文件 + * @param charset 编码 + * @return {@link ZipWriter} + */ + public static ZipWriter of(File zipFile, Charset charset) { + return new ZipWriter(zipFile, charset); + } + + /** + * 创建{@link ZipWriter} + * + * @param out Zip输出的流,一般为输出文件流 + * @param charset 编码 + * @return {@link ZipWriter} + */ + public static ZipWriter of(OutputStream out, Charset charset) { + return new ZipWriter(out, charset); + } + + private final ZipOutputStream out; + + /** + * 构造 + * + * @param zipFile 生成的Zip文件 + */ + public ZipWriter(File zipFile, Charset charset) { + this.out = getZipOutputStream(zipFile, charset); + } + + /** + * 构造 + * + * @param out {@link ZipOutputStream} + */ + public ZipWriter(OutputStream out, Charset charset) { + this.out = getZipOutputStream(out, charset); + } + + /** + * 构造 + * + * @param out {@link ZipOutputStream} + */ + public ZipWriter(ZipOutputStream out) { + this.out = out; + } + + /** + * 设置压缩级别,可选1~9,-1表示默认 + * + * @param level 压缩级别 + * @return this + */ + public ZipWriter setLevel(int level) { + this.out.setLevel(level); + return this; + } + + /** + * 设置注释 + * + * @param comment 注释 + * @return this + */ + public ZipWriter setComment(String comment) { + this.out.setComment(comment); + return this; + } + + /** + * 获取原始的{@link ZipOutputStream} + * + * @return {@link ZipOutputStream} + */ + public ZipOutputStream getOut() { + return this.out; + } + + /** + * 对文件或文件目录进行压缩 + * + * @param withSrcDir 是否包含被打包目录,只针对压缩目录有效。若为false,则只压缩目录下的文件或目录,为true则将本目录也压缩 + * @param filter 文件过滤器,通过实现此接口,自定义要过滤的文件(过滤掉哪些文件或文件夹不加入压缩),{@code null}表示不过滤 + * @param files 要压缩的源文件或目录。如果压缩一个文件,则为该文件的全路径;如果压缩一个目录,则为该目录的顶层目录路径 + * @throws IORuntimeException IO异常 + * @since 5.1.1 + */ + public ZipWriter add(boolean withSrcDir, FileFilter filter, File... files) throws IORuntimeException { + for (File file : files) { + // 如果只是压缩一个文件,则需要截取该文件的父目录 + String srcRootDir; + try { + srcRootDir = file.getCanonicalPath(); + if ((false == file.isDirectory()) || withSrcDir) { + // 若是文件,则将父目录完整路径都截取掉;若设置包含目录,则将上级目录全部截取掉,保留本目录名 + srcRootDir = file.getCanonicalFile().getParentFile().getCanonicalPath(); + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + + _add(file, srcRootDir, filter); + } + return this; + } + + /** + * 添加资源到压缩包,添加后关闭资源流 + * + * @param resources 需要压缩的资源,资源的路径为{@link Resource#getName()} + * @throws IORuntimeException IO异常 + */ + public ZipWriter add(Resource... resources) throws IORuntimeException { + for (Resource resource : resources) { + if (null != resource) { + add(resource.getName(), resource.getStream()); + } + } + return this; + } + + /** + * 添加文件流到压缩包,添加后关闭输入文件流
+ * 如果输入流为{@code null},则只创建空目录 + * + * @param path 压缩的路径, {@code null}和""表示根目录下 + * @param in 需要压缩的输入流,使用完后自动关闭,{@code null}表示加入空目录 + * @throws IORuntimeException IO异常 + */ + public ZipWriter add(String path, InputStream in) throws IORuntimeException { + path = StrUtil.nullToEmpty(path); + if (null == in) { + // 空目录需要检查路径规范性,目录以"/"结尾 + path = StrUtil.addSuffixIfNot(path, StrUtil.SLASH); + if (StrUtil.isBlank(path)) { + return this; + } + } + + return putEntry(path, in); + } + + @Override + public void close() throws IORuntimeException { + try { + out.finish(); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(this.out); + } + } + + /** + * 获得 {@link ZipOutputStream} + * + * @param zipFile 压缩文件 + * @param charset 编码 + * @return {@link ZipOutputStream} + */ + private static ZipOutputStream getZipOutputStream(File zipFile, Charset charset) { + return getZipOutputStream(FileUtil.getOutputStream(zipFile), charset); + } + + /** + * 获得 {@link ZipOutputStream} + * + * @param out 压缩文件流 + * @param charset 编码 + * @return {@link ZipOutputStream} + */ + private static ZipOutputStream getZipOutputStream(OutputStream out, Charset charset) { + if (out instanceof ZipOutputStream) { + return (ZipOutputStream) out; + } + return new ZipOutputStream(out, charset); + } + + /** + * 递归压缩文件夹或压缩文件
+ * srcRootDir决定了路径截取的位置,例如:
+ * file的路径为d:/a/b/c/d.txt,srcRootDir为d:/a/b,则压缩后的文件与目录为结构为c/d.txt + * + * @param srcRootDir 被压缩的文件夹根目录 + * @param file 当前递归压缩的文件或目录对象 + * @param filter 文件过滤器,通过实现此接口,自定义要过滤的文件(过滤掉哪些文件或文件夹不加入压缩),{@code null}表示不过滤 + * @throws IORuntimeException IO异常 + */ + private ZipWriter _add(File file, String srcRootDir, FileFilter filter) throws IORuntimeException { + if (null == file || (null != filter && false == filter.accept(file))) { + return this; + } + + // 获取文件相对于压缩文件夹根目录的子路径 + final String subPath = FileUtil.subPath(srcRootDir, file); + if (file.isDirectory()) { + // 如果是目录,则压缩压缩目录中的文件或子目录 + final File[] files = file.listFiles(); + if (ArrayUtil.isEmpty(files)) { + // 加入目录,只有空目录时才加入目录,非空时会在创建文件时自动添加父级目录 + add(subPath, null); + } else { + // 压缩目录下的子文件或目录 + for (File childFile : files) { + _add(childFile, srcRootDir, filter); + } + } + } else { + // 如果是文件或其它符号,则直接压缩该文件 + putEntry(subPath, FileUtil.getInputStream(file)); + } + return this; + } + + /** + * 添加文件流到压缩包,添加后关闭输入文件流
+ * 如果输入流为{@code null},则只创建空目录 + * + * @param path 压缩的路径, {@code null}和""表示根目录下 + * @param in 需要压缩的输入流,使用完后自动关闭,{@code null}表示加入空目录 + * @throws IORuntimeException IO异常 + */ + private ZipWriter putEntry(String path, InputStream in) throws IORuntimeException { + try { + out.putNextEntry(new ZipEntry(path)); + if (null != in) { + IoUtil.copy(in, out); + } + out.closeEntry(); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(in); + } + + IoUtil.flush(this.out); + return this; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/compress/package-info.java b/hutool-core/src/main/java/cn/hutool/core/compress/package-info.java new file mode 100755 index 000000000..d26b8a178 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/compress/package-info.java @@ -0,0 +1,7 @@ +/** + * 压缩解压封装 + * + * @author looly + * @since 5.7.8 + */ +package cn.hutool.core.compress; diff --git a/hutool-core/src/main/java/cn/hutool/core/util/ZipUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ZipUtil.java index d9851403f..cce4215a9 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/ZipUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/ZipUtil.java @@ -1,5 +1,9 @@ package cn.hutool.core.util; +import cn.hutool.core.compress.Deflate; +import cn.hutool.core.compress.Gzip; +import cn.hutool.core.compress.ZipReader; +import cn.hutool.core.compress.ZipWriter; import cn.hutool.core.exceptions.UtilException; import cn.hutool.core.io.FastByteArrayOutputStream; import cn.hutool.core.io.FileUtil; @@ -18,15 +22,8 @@ import java.io.OutputStream; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; -import java.util.Enumeration; import java.util.List; import java.util.function.Consumer; -import java.util.zip.Deflater; -import java.util.zip.DeflaterOutputStream; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; -import java.util.zip.Inflater; -import java.util.zip.InflaterOutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; @@ -35,6 +32,7 @@ import java.util.zip.ZipOutputStream; /** * 压缩工具类 * + * @see cn.hutool.core.compress.ZipWriter * @author Looly */ public class ZipUtil { @@ -210,13 +208,7 @@ public class ZipUtil { */ public static File zip(File zipFile, Charset charset, boolean withSrcDir, FileFilter filter, File... srcFiles) throws IORuntimeException { validateFiles(zipFile, srcFiles); - - try (ZipOutputStream out = getZipOutputStream(zipFile, charset)) { - zip(out, charset, withSrcDir, filter, srcFiles); - } catch (IOException e) { - throw new IORuntimeException(e); - } - + ZipWriter.of(zipFile, charset).add(withSrcDir, filter, srcFiles).close(); return zipFile; } @@ -232,7 +224,7 @@ public class ZipUtil { * @since 5.1.1 */ public static void zip(OutputStream out, Charset charset, boolean withSrcDir, FileFilter filter, File... srcFiles) throws IORuntimeException { - zip(getZipOutputStream(out, charset), withSrcDir, filter, srcFiles); + ZipWriter.of(out, charset).add(withSrcDir, filter, srcFiles).close(); } /** @@ -244,27 +236,12 @@ public class ZipUtil { * @param srcFiles 要压缩的源文件或目录。如果压缩一个文件,则为该文件的全路径;如果压缩一个目录,则为该目录的顶层目录路径 * @throws IORuntimeException IO异常 * @since 5.1.1 + * @deprecated 请使用 {@link #zip(OutputStream, Charset, boolean, FileFilter, File...)} */ + @Deprecated public static void zip(ZipOutputStream zipOutputStream, boolean withSrcDir, FileFilter filter, File... srcFiles) throws IORuntimeException { - String srcRootDir; - try { - for (File srcFile : srcFiles) { - if (null == srcFile) { - continue; - } - // 如果只是压缩一个文件,则需要截取该文件的父目录 - srcRootDir = srcFile.getCanonicalPath(); - if (srcFile.isFile() || withSrcDir) { - // 若是文件,则将父目录完整路径都截取掉;若设置包含目录,则将上级目录全部截取掉,保留本目录名 - srcRootDir = srcFile.getCanonicalFile().getParentFile().getCanonicalPath(); - } - // 调用递归压缩方法进行目录或文件压缩 - zip(srcFile, srcRootDir, zipOutputStream, filter); - zipOutputStream.flush(); - } - zipOutputStream.finish(); - } catch (IOException e) { - throw new IORuntimeException(e); + try(final ZipWriter zipWriter = new ZipWriter(zipOutputStream)){ + zipWriter.add(withSrcDir, filter, srcFiles); } } @@ -355,13 +332,19 @@ public class ZipUtil { * @since 3.0.9 */ public static File zip(File zipFile, String[] paths, InputStream[] ins, Charset charset) throws UtilException { - ZipOutputStream out = null; - try { - out = getZipOutputStream(zipFile, charset); - zip(out, paths, ins); - } finally { - IoUtil.close(out); + if (ArrayUtil.isEmpty(paths) || ArrayUtil.isEmpty(ins)) { + throw new IllegalArgumentException("Paths or ins is empty !"); } + if (paths.length != ins.length) { + throw new IllegalArgumentException("Paths length is not equals to ins length !"); + } + + try(final ZipWriter zipWriter = ZipWriter.of(zipFile, charset)){ + for (int i = 0; i < paths.length; i++) { + zipWriter.add(paths[i], ins[i]); + } + } + return zipFile; } @@ -374,12 +357,17 @@ public class ZipUtil { * @since 5.5.2 */ public static void zip(OutputStream out, String[] paths, InputStream[] ins) { - ZipOutputStream zipOutputStream = null; - try { - zipOutputStream = getZipOutputStream(out, DEFAULT_CHARSET); - zip(zipOutputStream, paths, ins); - } finally { - IoUtil.close(zipOutputStream); + if (ArrayUtil.isEmpty(paths) || ArrayUtil.isEmpty(ins)) { + throw new IllegalArgumentException("Paths or ins is empty !"); + } + if (paths.length != ins.length) { + throw new IllegalArgumentException("Paths length is not equals to ins length !"); + } + + try(final ZipWriter zipWriter = ZipWriter.of(out, DEFAULT_CHARSET)){ + for (int i = 0; i < paths.length; i++) { + zipWriter.add(paths[i], ins[i]); + } } } @@ -399,8 +387,11 @@ public class ZipUtil { if (paths.length != ins.length) { throw new IllegalArgumentException("Paths length is not equals to ins length !"); } - for (int i = 0; i < paths.length; i++) { - add(ins[i], paths[i], zipOutputStream); + + try(final ZipWriter zipWriter = new ZipWriter(zipOutputStream)){ + for (int i = 0; i < paths.length; i++) { + zipWriter.add(paths[i], ins[i]); + } } } @@ -416,15 +407,7 @@ public class ZipUtil { * @since 5.5.2 */ public static File zip(File zipFile, Charset charset, Resource... resources) throws UtilException { - ZipOutputStream out = null; - try { - out = getZipOutputStream(zipFile, charset); - for (Resource resource : resources) { - add(resource.getStream(), resource.getName(), out); - } - } finally { - IoUtil.close(out); - } + ZipWriter.of(zipFile, charset).add(resources).close(); return zipFile; } @@ -541,21 +524,13 @@ public class ZipUtil { */ public static File unzip(ZipFile zipFile, File outFile) throws IORuntimeException { if (outFile.exists() && outFile.isFile()) { - throw new UtilException("Target path [{}] exist!", outFile.getAbsolutePath()); + throw new IllegalArgumentException( + StrUtil.format("Target path [{}] exist!", outFile.getAbsolutePath())); } - read(zipFile, (zipEntry) -> { - // FileUtil.file会检查slip漏洞,漏洞说明见http://blog.nsfocus.net/zip-slip-2/ - File outItemFile = FileUtil.file(outFile, zipEntry.getName()); - if (zipEntry.isDirectory()) { - // 创建对应目录 - //noinspection ResultOfMethodCallIgnored - outItemFile.mkdirs(); - } else { - // 写出文件 - write(zipFile, zipEntry, outItemFile); - } - }); + try(final ZipReader reader = new ZipReader(zipFile)){ + reader.readTo(outFile); + } return outFile; } @@ -596,13 +571,8 @@ public class ZipUtil { * @since 5.5.2 */ public static void read(ZipFile zipFile, Consumer consumer) { - try { - final Enumeration em = zipFile.entries(); - while (em.hasMoreElements()) { - consumer.accept(em.nextElement()); - } - } finally { - IoUtil.close(zipFile); + try(final ZipReader reader = new ZipReader(zipFile)){ + reader.read(consumer); } } @@ -635,18 +605,9 @@ public class ZipUtil { * @since 4.5.8 */ public static File unzip(ZipInputStream zipStream, File outFile) throws UtilException { - read(zipStream, (zipEntry) -> { - // FileUtil.file会检查slip漏洞,漏洞说明见http://blog.nsfocus.net/zip-slip-2/ - File outItemFile = FileUtil.file(outFile, zipEntry.getName()); - if (zipEntry.isDirectory()) { - // 目录 - //noinspection ResultOfMethodCallIgnored - outItemFile.mkdirs(); - } else { - // 文件 - FileUtil.writeFromStream(zipStream, outItemFile, false); - } - }); + try(final ZipReader reader = new ZipReader(zipStream)){ + reader.readTo(outFile); + } return outFile; } @@ -658,15 +619,8 @@ public class ZipUtil { * @since 5.5.2 */ public static void read(ZipInputStream zipStream, Consumer consumer) { - try { - ZipEntry zipEntry; - while (null != (zipEntry = zipStream.getNextEntry())) { - consumer.accept(zipEntry); - } - } catch (IOException e) { - throw new UtilException(e); - } finally { - IoUtil.close(zipStream); + try(final ZipReader reader = new ZipReader(zipStream)){ + reader.read(consumer); } } @@ -716,23 +670,10 @@ public class ZipUtil { * @return 文件内容bytes * @since 4.1.8 */ - @SuppressWarnings("unchecked") public static byte[] unzipFileBytes(File zipFile, Charset charset, String name) { - ZipFile zipFileObj = null; - try { - zipFileObj = toZipFile(zipFile, charset); - final Enumeration em = (Enumeration) zipFileObj.entries(); - ZipEntry zipEntry; - while (em.hasMoreElements()) { - zipEntry = em.nextElement(); - if ((false == zipEntry.isDirectory()) && name.equals(zipEntry.getName())) { - return IoUtil.readBytes(getStream(zipFileObj, zipEntry)); - } - } - } finally { - IoUtil.close(zipFileObj); + try(final ZipReader reader = ZipReader.of(zipFile, charset)){ + return IoUtil.readBytes(reader.get(name)); } - return null; } // ----------------------------------------------------------------------------- Gzip @@ -800,16 +741,7 @@ public class ZipUtil { */ public static byte[] gzip(InputStream in, int length) throws UtilException { final ByteArrayOutputStream bos = new ByteArrayOutputStream(length); - GZIPOutputStream gos = null; - try { - gos = new GZIPOutputStream(bos); - IoUtil.copy(in, gos); - } catch (IOException e) { - throw new UtilException(e); - } finally { - IoUtil.close(gos); - } - // 返回必须在关闭gos后进行,因为关闭时会自动执行finish()方法,保证数据全部写出 + Gzip.of(in, bos).gzip().close(); return bos.toByteArray(); } @@ -857,18 +789,8 @@ public class ZipUtil { * @since 4.1.18 */ public static byte[] unGzip(InputStream in, int length) throws UtilException { - GZIPInputStream gzi = null; - FastByteArrayOutputStream bos; - try { - gzi = (in instanceof GZIPInputStream) ? (GZIPInputStream) in : new GZIPInputStream(in); - bos = new FastByteArrayOutputStream(length); - IoUtil.copy(gzi, bos); - } catch (IOException e) { - throw new UtilException(e); - } finally { - IoUtil.close(gzi); - } - // 返回必须在关闭gos后进行,因为关闭时会自动执行finish()方法,保证数据全部写出 + FastByteArrayOutputStream bos = new FastByteArrayOutputStream(length); + Gzip.of(in, bos).unGzip().close(); return bos.toByteArray(); } @@ -940,7 +862,7 @@ public class ZipUtil { */ public static byte[] zlib(InputStream in, int level, int length) { final ByteArrayOutputStream out = new ByteArrayOutputStream(length); - deflater(in, out, level, false); + Deflate.of(in, out, false).deflater(level); return out.toByteArray(); } @@ -988,7 +910,7 @@ public class ZipUtil { */ public static byte[] unZlib(InputStream in, int length) { final ByteArrayOutputStream out = new ByteArrayOutputStream(length); - inflater(in, out, false); + Deflate.of(in, out, false).inflater(); return out.toByteArray(); } @@ -1023,123 +945,6 @@ public class ZipUtil { // ---------------------------------------------------------------------------------------------- Private method start - /** - * 获得 {@link ZipOutputStream} - * - * @param zipFile 压缩文件 - * @param charset 编码 - * @return {@link ZipOutputStream} - */ - private static ZipOutputStream getZipOutputStream(File zipFile, Charset charset) { - return getZipOutputStream(FileUtil.getOutputStream(zipFile), charset); - } - - /** - * 获得 {@link ZipOutputStream} - * - * @param out 压缩文件流 - * @param charset 编码 - * @return {@link ZipOutputStream} - */ - private static ZipOutputStream getZipOutputStream(OutputStream out, Charset charset) { - if (out instanceof ZipOutputStream) { - return (ZipOutputStream) out; - } - return new ZipOutputStream(out, ObjectUtil.defaultIfNull(charset, DEFAULT_CHARSET)); - } - - /** - * 递归压缩文件夹
- * srcRootDir决定了路径截取的位置,例如:
- * file的路径为d:/a/b/c/d.txt,srcRootDir为d:/a/b,则压缩后的文件与目录为结构为c/d.txt - * - * @param out 压缩文件存储对象 - * @param srcRootDir 被压缩的文件夹根目录 - * @param file 当前递归压缩的文件或目录对象 - * @param filter 文件过滤器,通过实现此接口,自定义要过滤的文件(过滤掉哪些文件或文件夹不加入压缩) - * @throws UtilException IO异常 - */ - private static void zip(File file, String srcRootDir, ZipOutputStream out, FileFilter filter) throws UtilException { - if (null == file || (null != filter && false == filter.accept(file))) { - return; - } - - final String subPath = FileUtil.subPath(srcRootDir, file); // 获取文件相对于压缩文件夹根目录的子路径 - if (file.isDirectory()) {// 如果是目录,则压缩压缩目录中的文件或子目录 - final File[] files = file.listFiles(); - if (ArrayUtil.isEmpty(files)) { - // 加入目录,只有空目录时才加入目录,非空时会在创建文件时自动添加父级目录 - addDir(subPath, out); - } else{ - // 压缩目录下的子文件或目录 - for (File childFile : files) { - zip(childFile, srcRootDir, out, filter); - } - } - } else {// 如果是文件或其它符号,则直接压缩该文件 - add(file, subPath, out); - } - } - - /** - * 添加文件到压缩包 - * - * @param file 需要压缩的文件 - * @param path 在压缩文件中的路径 - * @param out 压缩文件存储对象 - * @throws IORuntimeException IO异常 - * @since 4.0.5 - */ - private static void add(File file, String path, ZipOutputStream out) throws IORuntimeException { - add(FileUtil.getInputStream(file), path, out); - } - - /** - * 添加文件流到压缩包,添加后关闭流 - * - * @param in 需要压缩的输入流,使用完后自动关闭 - * @param path 压缩的路径 - * @param out 压缩文件存储对象 - * @throws IORuntimeException IO异常 - */ - private static void add(InputStream in, String path, ZipOutputStream out) throws IORuntimeException { - if (null == in) { - return; - } - try { - out.putNextEntry(new ZipEntry(path)); - IoUtil.copy(in, out); - } catch (IOException e) { - throw new IORuntimeException(e); - } finally { - IoUtil.close(in); - closeEntry(out); - } - } - - /** - * 在压缩包中新建目录 - * - * @param path 压缩的路径 - * @param out 压缩文件存储对象 - * @throws UtilException IO异常 - */ - private static void addDir(String path, ZipOutputStream out) throws UtilException { - if(StrUtil.isEmpty(path)){ - // 空路径不处理 - return; - } - - path = StrUtil.addSuffixIfNot(path, StrUtil.SLASH); - try { - out.putNextEntry(new ZipEntry(path)); - } catch (IOException e) { - throw new UtilException(e); - } finally { - closeEntry(out); - } - } - /** * 判断压缩文件保存的路径是否为源文件路径的子文件夹,如果是,则抛出异常(防止无限递归压缩的发生) * @@ -1166,19 +971,6 @@ public class ZipUtil { } } - /** - * 关闭当前Entry,继续下一个Entry - * - * @param out ZipOutputStream - */ - private static void closeEntry(ZipOutputStream out) { - try { - out.closeEntry(); - } catch (IOException e) { - // ignore - } - } - /** * 从Zip中读取文件流并写出到文件 * @@ -1190,43 +982,6 @@ public class ZipUtil { private static void write(ZipFile zipFile, ZipEntry zipEntry, File outItemFile) throws IORuntimeException { FileUtil.writeFromStream(getStream(zipFile, zipEntry), outItemFile); } - - /** - * 将Zlib流解压到out中 - * - * @param in zlib数据流 - * @param out 输出 - * @param nowrap true表示兼容Gzip压缩 - */ - @SuppressWarnings("SameParameterValue") - private static void inflater(InputStream in, OutputStream out, boolean nowrap) { - final InflaterOutputStream ios = (out instanceof InflaterOutputStream) ? (InflaterOutputStream) out : new InflaterOutputStream(out, new Inflater(nowrap)); - IoUtil.copy(in, ios); - try { - ios.finish(); - } catch (IOException e) { - throw new IORuntimeException(e); - } - } - - /** - * 将普通数据流压缩成zlib到out中 - * - * @param in zlib数据流 - * @param out 输出 - * @param level 压缩级别,0~9 - * @param nowrap true表示兼容Gzip压缩 - */ - @SuppressWarnings("SameParameterValue") - private static void deflater(InputStream in, OutputStream out, int level, boolean nowrap) { - final DeflaterOutputStream ios = (out instanceof DeflaterOutputStream) ? (DeflaterOutputStream) out : new DeflaterOutputStream(out, new Deflater(level, nowrap)); - IoUtil.copy(in, ios); - try { - ios.finish(); - } catch (IOException e) { - throw new IORuntimeException(e); - } - } // ---------------------------------------------------------------------------------------------- Private method end } diff --git a/hutool-core/src/test/java/cn/hutool/core/compress/ZipReaderTest.java b/hutool-core/src/test/java/cn/hutool/core/compress/ZipReaderTest.java new file mode 100755 index 000000000..e1de7097c --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/compress/ZipReaderTest.java @@ -0,0 +1,18 @@ +package cn.hutool.core.compress; + +import cn.hutool.core.lang.Console; +import cn.hutool.core.util.ZipUtil; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.File; + +public class ZipReaderTest { + + @Test + @Ignore + public void unzipTest() { + File unzip = ZipUtil.unzip("d:/java.zip", "d:/test/java"); + Console.log(unzip); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/compress/ZipWriterTest.java b/hutool-core/src/test/java/cn/hutool/core/compress/ZipWriterTest.java new file mode 100755 index 000000000..97bb6af52 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/compress/ZipWriterTest.java @@ -0,0 +1,16 @@ +package cn.hutool.core.compress; + +import cn.hutool.core.util.ZipUtil; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.File; + +public class ZipWriterTest { + + @Test + @Ignore + public void zipDirTest() { + ZipUtil.zip(new File("d:/test")); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/ZipUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/ZipUtilTest.java index acc10d325..0246f6688 100644 --- a/hutool-core/src/test/java/cn/hutool/core/util/ZipUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/util/ZipUtilTest.java @@ -19,21 +19,21 @@ import java.nio.charset.Charset; * */ public class ZipUtilTest { - - + + @Test @Ignore public void zipDirTest() { - ZipUtil.zip(new File("e:/picTest/picSubTest")); + ZipUtil.zip(new File("d:/test")); } - + @Test @Ignore public void unzipTest() { File unzip = ZipUtil.unzip("f:/test/apache-maven-3.6.2.zip", "f:\\test"); Console.log(unzip); } - + @Test @Ignore public void unzipTest2() { @@ -47,46 +47,46 @@ public class ZipUtilTest { File unzip = ZipUtil.unzip(FileUtil.getInputStream("e:/test/hutool-core-5.1.0.jar"), FileUtil.file("e:/test/"), CharsetUtil.CHARSET_UTF_8); Console.log(unzip); } - + @Test @Ignore public void unzipChineseTest() { ZipUtil.unzip("d:/测试.zip"); } - + @Test @Ignore public void unzipFileBytesTest() { byte[] fileBytes = ZipUtil.unzipFileBytes(FileUtil.file("e:/02 电力相关设备及服务2-241-.zip"), CharsetUtil.CHARSET_GBK, "images/CE-EP-HY-MH01-ES-0001.jpg"); Assert.assertNotNull(fileBytes); } - + @Test public void gzipTest() { String data = "我是一个需要压缩的很长很长的字符串"; byte[] bytes = StrUtil.utf8Bytes(data); byte[] gzip = ZipUtil.gzip(bytes); - + //保证gzip长度正常 Assert.assertEquals(68, gzip.length); - + byte[] unGzip = ZipUtil.unGzip(gzip); //保证正常还原 Assert.assertEquals(data, StrUtil.utf8Str(unGzip)); } - + @Test public void zlibTest() { String data = "我是一个需要压缩的很长很长的字符串"; byte[] bytes = StrUtil.utf8Bytes(data); byte[] gzip = ZipUtil.zlib(bytes, 0); - + //保证zlib长度正常 Assert.assertEquals(62, gzip.length); byte[] unGzip = ZipUtil.unZlib(gzip); //保证正常还原 Assert.assertEquals(data, StrUtil.utf8Str(unGzip)); - + gzip = ZipUtil.zlib(bytes, 9); //保证zlib长度正常 Assert.assertEquals(56, gzip.length);