From 25118070a3210e08665f4e1bed5195190743e469 Mon Sep 17 00:00:00 2001 From: Looly Date: Thu, 21 Oct 2021 02:52:14 +0800 Subject: [PATCH] add zip append --- CHANGELOG.md | 3 +- .../hutool/core/compress/ZipCopyVisitor.java | 84 +++++++++++++++++++ .../java/cn/hutool/core/io/file/PathUtil.java | 14 ++++ .../core/io/file/visitor/CopyVisitor.java | 83 +++++++++--------- .../java/cn/hutool/core/util/ZipUtil.java | 56 +++++++------ .../java/cn/hutool/core/util/ZipUtilTest.java | 20 +++-- .../test/resources/test-zip/test-add/test.txt | 1 + 7 files changed, 184 insertions(+), 77 deletions(-) create mode 100644 hutool-core/src/main/java/cn/hutool/core/compress/ZipCopyVisitor.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 55fc99699..688101d0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ------------------------------------------------------------------------------------------------------------- -# 5.7.15 (2021-10-20) +# 5.7.15 (2021-10-21) ### 🐣新特性 * 【db 】 Db.quietSetAutoCommit增加判空(issue#I4D75B@Gitee) @@ -14,6 +14,7 @@ * 【core 】 ContentType增加build重载(pr#1898@Github) * 【bom 】 支持scope=import方式引入(issue#1561@Github) * 【core 】 新增Hash接口,HashXXX继承此接口 +* 【core 】 ZipUtil增加append方法(pr#441@Gitee) ### 🐞Bug修复 * 【core 】 修复CollUtil.isEqualList两个null返回错误问题(issue#1885@Github) diff --git a/hutool-core/src/main/java/cn/hutool/core/compress/ZipCopyVisitor.java b/hutool-core/src/main/java/cn/hutool/core/compress/ZipCopyVisitor.java new file mode 100644 index 000000000..3423097be --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/compress/ZipCopyVisitor.java @@ -0,0 +1,84 @@ +package cn.hutool.core.compress; + +import cn.hutool.core.util.StrUtil; + +import java.io.IOException; +import java.nio.file.CopyOption; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileSystem; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +/** + * Zip文件拷贝的FileVisitor实现,zip中追加文件,此类非线程安全
+ * 此类在遍历源目录并复制过程中会自动创建目标目录中不存在的上级目录。 + * + * @author looly + * @since 5.7.15 + */ +public class ZipCopyVisitor extends SimpleFileVisitor { + + /** + * 源Path,或基准路径,用于计算被拷贝文件的相对路径 + */ + private final Path source; + private final FileSystem fileSystem; + private final CopyOption[] copyOptions; + + /** + * 构造 + * + * @param source 源Path,或基准路径,用于计算被拷贝文件的相对路径 + * @param fileSystem 目标Zip文件 + * @param copyOptions 拷贝选项,如跳过已存在等 + */ + public ZipCopyVisitor(Path source, FileSystem fileSystem, CopyOption... copyOptions) { + this.source = source; + this.fileSystem = fileSystem; + this.copyOptions = copyOptions; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + final Path targetDir = resolveTarget(dir); + if(StrUtil.isNotEmpty(targetDir.toString())){ + // 在目标的Zip文件中的相对位置创建目录 + try { + Files.copy(dir, targetDir, copyOptions); + } catch (FileAlreadyExistsException e) { + if (false == Files.isDirectory(targetDir)) { + throw e; + } + } + } + + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + // 如果目标存在,无论目录还是文件都抛出FileAlreadyExistsException异常,此处不做特别处理 + Files.copy(file, resolveTarget(file), copyOptions); + + return FileVisitResult.CONTINUE; + } + + /** + * 根据源文件或目录路径,拼接生成目标的文件或目录路径
+ * 原理是首先截取源路径,得到相对路径,再和目标路径拼接 + * + *

+ * 如:源路径是 /opt/test/,需要拷贝的文件是 /opt/test/a/a.txt,得到相对路径 a/a.txt
+ * 目标路径是/home/,则得到最终目标路径是 /home/a/a.txt + *

+ * + * @param file 需要拷贝的文件或目录Path + * @return 目标Path + */ + private Path resolveTarget(Path file) { + return fileSystem.getPath(source.relativize(file).toString()); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/file/PathUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/file/PathUtil.java index 95abe2c52..1c3ed2d92 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/file/PathUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/file/PathUtil.java @@ -656,6 +656,20 @@ public class PathUtil { return mkdir(path.getParent()); } + /** + * 获取{@link Path}文件名 + * + * @param path {@link Path} + * @return 文件名 + * @since 5.7.15 + */ + public static String getName(Path path) { + if (null == path) { + return null; + } + return path.getFileName().toString(); + } + /** * 删除文件或空目录,不追踪软链 * diff --git a/hutool-core/src/main/java/cn/hutool/core/io/file/visitor/CopyVisitor.java b/hutool-core/src/main/java/cn/hutool/core/io/file/visitor/CopyVisitor.java index e8bd619be..b36e8c193 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/file/visitor/CopyVisitor.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/file/visitor/CopyVisitor.java @@ -1,11 +1,7 @@ package cn.hutool.core.io.file.visitor; import cn.hutool.core.io.file.PathUtil; -import cn.hutool.core.util.StrUtil; -import com.sun.nio.zipfs.ZipFileSystem; -import com.sun.nio.zipfs.ZipPath; -import java.io.File; import java.io.IOException; import java.nio.file.CopyOption; import java.nio.file.FileAlreadyExistsException; @@ -24,53 +20,48 @@ import java.nio.file.attribute.BasicFileAttributes; */ public class CopyVisitor extends SimpleFileVisitor { + /** + * 源Path,或基准路径,用于计算被拷贝文件的相对路径 + */ private final Path source; private final Path target; - private boolean isTargetCreated; - private final boolean isZipFile; - private String dirRoot = null; private final CopyOption[] copyOptions; + /** + * 标记目标目录是否创建,省略每次判断目标是否存在 + */ + private boolean isTargetCreated; + /** * 构造 * - * @param source 源Path - * @param target 目标Path + * @param source 源Path,或基准路径,用于计算被拷贝文件的相对路径 + * @param target 目标Path * @param copyOptions 拷贝选项,如跳过已存在等 */ public CopyVisitor(Path source, Path target, CopyOption... copyOptions) { - if(PathUtil.exists(target, false) && false == PathUtil.isDirectory(target)){ + if (PathUtil.exists(target, false) && false == PathUtil.isDirectory(target)) { throw new IllegalArgumentException("Target must be a directory"); } this.source = source; this.target = target; - this.isZipFile = target instanceof ZipPath; this.copyOptions = copyOptions; } @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) - throws IOException { - final Path targetDir; - if (isZipFile) { - ZipPath zipPath = (ZipPath) target; - ZipFileSystem fileSystem = zipPath.getFileSystem(); - if (dirRoot == null) { - targetDir = fileSystem.getPath(dir.getFileName().toString()); - dirRoot = dir.getFileName().toString() + File.separator; - } else { - targetDir = fileSystem.getPath(dirRoot, StrUtil.subAfter(dir.toString(), dirRoot, false)); - } - } else { - initTarget(); - // 将当前目录相对于源路径转换为相对于目标路径 - targetDir = target.resolve(source.relativize(dir)); - } + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + initTargetDir(); + // 将当前目录相对于源路径转换为相对于目标路径 + final Path targetDir = resolveTarget(dir); + + // 在目录不存在的情况下,copy方法会创建新目录 try { Files.copy(dir, targetDir, copyOptions); } catch (FileAlreadyExistsException e) { - if (false == Files.isDirectory(targetDir)) + if (false == Files.isDirectory(targetDir)) { + // 目标文件存在抛出异常,目录忽略 throw e; + } } return FileVisitResult.CONTINUE; } @@ -78,25 +69,33 @@ public class CopyVisitor extends SimpleFileVisitor { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - if (isZipFile) { - if (dirRoot == null) { - Files.copy(file, target, copyOptions); - } else { - ZipPath zipPath = (ZipPath) target; - Files.copy(file, zipPath.getFileSystem().getPath(dirRoot, StrUtil.subAfter(file.toString(), dirRoot, false)), copyOptions); - } - } else { - initTarget(); - Files.copy(file, target.resolve(source.relativize(file)), copyOptions); - } + initTargetDir(); + // 如果目标存在,无论目录还是文件都抛出FileAlreadyExistsException异常,此处不做特别处理 + Files.copy(file, resolveTarget(file), copyOptions); return FileVisitResult.CONTINUE; } + /** + * 根据源文件或目录路径,拼接生成目标的文件或目录路径
+ * 原理是首先截取源路径,得到相对路径,再和目标路径拼接 + * + *

+ * 如:源路径是 /opt/test/,需要拷贝的文件是 /opt/test/a/a.txt,得到相对路径 a/a.txt
+ * 目标路径是/home/,则得到最终目标路径是 /home/a/a.txt + *

+ * + * @param file 需要拷贝的文件或目录Path + * @return 目标Path + */ + private Path resolveTarget(Path file) { + return target.resolve(source.relativize(file)); + } + /** * 初始化目标文件或目录 */ - private void initTarget(){ - if(false == this.isTargetCreated){ + private void initTargetDir() { + if (false == this.isTargetCreated) { PathUtil.mkdir(this.target); this.isTargetCreated = true; } 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 dc8d55230..a813242bf 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 @@ -2,6 +2,7 @@ package cn.hutool.core.util; import cn.hutool.core.compress.Deflate; import cn.hutool.core.compress.Gzip; +import cn.hutool.core.compress.ZipCopyVisitor; import cn.hutool.core.compress.ZipReader; import cn.hutool.core.compress.ZipWriter; import cn.hutool.core.exceptions.UtilException; @@ -10,7 +11,7 @@ import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.file.FileSystemUtil; -import cn.hutool.core.io.file.visitor.CopyVisitor; +import cn.hutool.core.io.file.PathUtil; import cn.hutool.core.io.resource.Resource; import java.io.BufferedInputStream; @@ -22,12 +23,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; +import java.nio.file.CopyOption; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -40,8 +40,8 @@ import java.util.zip.ZipOutputStream; /** * 压缩工具类 * - * @see cn.hutool.core.compress.ZipWriter * @author Looly + * @see cn.hutool.core.compress.ZipWriter */ public class ZipUtil { @@ -84,25 +84,29 @@ public class ZipUtil { } /** - * 在zip文件中添加新文件, 如果已经存在则不会有效果 + * 在zip文件中添加新文件或目录
+ * 新文件添加在zip根目录,文件夹包括其本身和内容
+ * 如果待添加文件夹是系统根路径(如/或c:/),则只复制文件夹下的内容 * - * @param zipFilePathStr zip文件存储路径 - * @param appendFilePathStr 待添加文件路径(可以是文件夹) + * @param zipPath zip文件的Path + * @param appendFilePath 待添加文件Path(可以是文件夹) + * @param options 拷贝选项,可选是否覆盖等 + * @since 5.7.15 */ - public static void addFile(String zipFilePathStr, String appendFilePathStr) throws IOException { - Path zipPath = Paths.get(zipFilePathStr); - Path appendFilePath = Paths.get(appendFilePathStr); - + public static void append(Path zipPath, Path appendFilePath, CopyOption... options) throws IOException { try (FileSystem zipFileSystem = FileSystemUtil.createZip(zipPath.toString())) { - Path root = zipFileSystem.getPath("/"); - Path dest = zipFileSystem.getPath(root.toString(), appendFilePath.getFileName().toString()); - if (!Files.isDirectory(appendFilePath)) { - Files.copy(appendFilePath, dest, StandardCopyOption.COPY_ATTRIBUTES); + if (Files.isDirectory(appendFilePath)) { + Path source = appendFilePath.getParent(); + if (null == source) { + // 如果用户提供的是根路径,则不复制目录,直接复制目录下的内容 + source = appendFilePath; + } + Files.walkFileTree(appendFilePath, new ZipCopyVisitor(source, zipFileSystem, options)); } else { - Files.walkFileTree(appendFilePath, new CopyVisitor(appendFilePath, zipFileSystem.getPath(zipFilePathStr))); + Files.copy(appendFilePath, zipFileSystem.getPath(PathUtil.getName(appendFilePath)), options); } } catch (FileAlreadyExistsException ignored) { - // 文件已存在, 跳过 + // 不覆盖情况下,文件已存在, 跳过 } } @@ -271,7 +275,7 @@ public class ZipUtil { */ @Deprecated public static void zip(ZipOutputStream zipOutputStream, boolean withSrcDir, FileFilter filter, File... srcFiles) throws IORuntimeException { - try(final ZipWriter zipWriter = new ZipWriter(zipOutputStream)){ + try (final ZipWriter zipWriter = new ZipWriter(zipOutputStream)) { zipWriter.add(withSrcDir, filter, srcFiles); } } @@ -370,7 +374,7 @@ public class ZipUtil { throw new IllegalArgumentException("Paths length is not equals to ins length !"); } - try(final ZipWriter zipWriter = ZipWriter.of(zipFile, charset)){ + try (final ZipWriter zipWriter = ZipWriter.of(zipFile, charset)) { for (int i = 0; i < paths.length; i++) { zipWriter.add(paths[i], ins[i]); } @@ -395,7 +399,7 @@ public class ZipUtil { throw new IllegalArgumentException("Paths length is not equals to ins length !"); } - try(final ZipWriter zipWriter = ZipWriter.of(out, DEFAULT_CHARSET)){ + try (final ZipWriter zipWriter = ZipWriter.of(out, DEFAULT_CHARSET)) { for (int i = 0; i < paths.length; i++) { zipWriter.add(paths[i], ins[i]); } @@ -419,7 +423,7 @@ public class ZipUtil { throw new IllegalArgumentException("Paths length is not equals to ins length !"); } - try(final ZipWriter zipWriter = new ZipWriter(zipOutputStream)){ + try (final ZipWriter zipWriter = new ZipWriter(zipOutputStream)) { for (int i = 0; i < paths.length; i++) { zipWriter.add(paths[i], ins[i]); } @@ -559,7 +563,7 @@ public class ZipUtil { StrUtil.format("Target path [{}] exist!", outFile.getAbsolutePath())); } - try(final ZipReader reader = new ZipReader(zipFile)){ + try (final ZipReader reader = new ZipReader(zipFile)) { reader.readTo(outFile); } return outFile; @@ -602,7 +606,7 @@ public class ZipUtil { * @since 5.5.2 */ public static void read(ZipFile zipFile, Consumer consumer) { - try(final ZipReader reader = new ZipReader(zipFile)){ + try (final ZipReader reader = new ZipReader(zipFile)) { reader.read(consumer); } } @@ -636,7 +640,7 @@ public class ZipUtil { * @since 4.5.8 */ public static File unzip(ZipInputStream zipStream, File outFile) throws UtilException { - try(final ZipReader reader = new ZipReader(zipStream)){ + try (final ZipReader reader = new ZipReader(zipStream)) { reader.readTo(outFile); } return outFile; @@ -650,7 +654,7 @@ public class ZipUtil { * @since 5.5.2 */ public static void read(ZipInputStream zipStream, Consumer consumer) { - try(final ZipReader reader = new ZipReader(zipStream)){ + try (final ZipReader reader = new ZipReader(zipStream)) { reader.read(consumer); } } @@ -702,7 +706,7 @@ public class ZipUtil { * @since 4.1.8 */ public static byte[] unzipFileBytes(File zipFile, Charset charset, String name) { - try(final ZipReader reader = ZipReader.of(zipFile, charset)){ + try (final ZipReader reader = ZipReader.of(zipFile, charset)) { return IoUtil.readBytes(reader.get(name)); } } 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 acd389ab1..e224e359a 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 @@ -24,7 +24,7 @@ import java.util.List; public class ZipUtilTest { @Test - public void addFileTest() throws IOException { + public void appendTest() throws IOException { File appendFile = FileUtil.file("test-zip/addFile.txt"); File zipFile = FileUtil.file("test-zip/test.zip"); @@ -34,23 +34,27 @@ public class ZipUtilTest { FileUtil.copy(zipFile, tempZipFile, true); // test file add - List beforeNames = zipEntryNames(zipFile); - ZipUtil.addFile(zipFile.getAbsolutePath(), appendFile.getAbsolutePath()); - List afterNames = zipEntryNames(zipFile); + List beforeNames = zipEntryNames(tempZipFile); + ZipUtil.append(tempZipFile.toPath(), appendFile.toPath()); + List afterNames = zipEntryNames(tempZipFile); + + // 确认增加了文件 + Assert.assertEquals(beforeNames.size() + 1, afterNames.size()); Assert.assertTrue(afterNames.containsAll(beforeNames)); Assert.assertTrue(afterNames.contains(appendFile.getName())); // test dir add - beforeNames = afterNames; + beforeNames = zipEntryNames(tempZipFile); File addDirFile = FileUtil.file("test-zip/test-add"); - ZipUtil.addFile(zipFile.getAbsolutePath(), addDirFile.getAbsolutePath()); - afterNames = zipEntryNames(zipFile); + ZipUtil.append(tempZipFile.toPath(), addDirFile.toPath()); + afterNames = zipEntryNames(tempZipFile); + // 确认增加了文件和目录,增加目录和目录下一个文件,故此处+2 + Assert.assertEquals(beforeNames.size() + 2, afterNames.size()); Assert.assertTrue(afterNames.containsAll(beforeNames)); Assert.assertTrue(afterNames.contains(appendFile.getName())); // rollback - FileUtil.copy(tempZipFile, zipFile, true); Assert.assertTrue(String.format("delete temp file %s failed", tempZipFile.getCanonicalPath()), tempZipFile.delete()); } diff --git a/hutool-core/src/test/resources/test-zip/test-add/test.txt b/hutool-core/src/test/resources/test-zip/test-add/test.txt index e69de29bb..56a6051ca 100644 --- a/hutool-core/src/test/resources/test-zip/test-add/test.txt +++ b/hutool-core/src/test/resources/test-zip/test-add/test.txt @@ -0,0 +1 @@ +1 \ No newline at end of file