add zip append

This commit is contained in:
Looly
2021-10-21 02:52:14 +08:00
parent 8a8dbd1816
commit 25118070a3
7 changed files with 184 additions and 77 deletions

View File

@@ -3,7 +3,7 @@
------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------
# 5.7.15 (2021-10-20) # 5.7.15 (2021-10-21)
### 🐣新特性 ### 🐣新特性
* 【db 】 Db.quietSetAutoCommit增加判空issue#I4D75B@Gitee * 【db 】 Db.quietSetAutoCommit增加判空issue#I4D75B@Gitee
@@ -14,6 +14,7 @@
* 【core 】 ContentType增加build重载pr#1898@Github * 【core 】 ContentType增加build重载pr#1898@Github
* 【bom 】 支持scope=import方式引入issue#1561@Github * 【bom 】 支持scope=import方式引入issue#1561@Github
* 【core 】 新增Hash接口HashXXX继承此接口 * 【core 】 新增Hash接口HashXXX继承此接口
* 【core 】 ZipUtil增加append方法pr#441@Gitee
### 🐞Bug修复 ### 🐞Bug修复
* 【core 】 修复CollUtil.isEqualList两个null返回错误问题issue#1885@Github * 【core 】 修复CollUtil.isEqualList两个null返回错误问题issue#1885@Github

View File

@@ -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中追加文件此类非线程安全<br>
* 此类在遍历源目录并复制过程中会自动创建目标目录中不存在的上级目录。
*
* @author looly
* @since 5.7.15
*/
public class ZipCopyVisitor extends SimpleFileVisitor<Path> {
/**
* 源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;
}
/**
* 根据源文件或目录路径,拼接生成目标的文件或目录路径<br>
* 原理是首先截取源路径,得到相对路径,再和目标路径拼接
*
* <p>
* 如:源路径是 /opt/test/,需要拷贝的文件是 /opt/test/a/a.txt得到相对路径 a/a.txt<br>
* 目标路径是/home/,则得到最终目标路径是 /home/a/a.txt
* </p>
*
* @param file 需要拷贝的文件或目录Path
* @return 目标Path
*/
private Path resolveTarget(Path file) {
return fileSystem.getPath(source.relativize(file).toString());
}
}

View File

@@ -656,6 +656,20 @@ public class PathUtil {
return mkdir(path.getParent()); 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();
}
/** /**
* 删除文件或空目录,不追踪软链 * 删除文件或空目录,不追踪软链
* *

View File

@@ -1,11 +1,7 @@
package cn.hutool.core.io.file.visitor; package cn.hutool.core.io.file.visitor;
import cn.hutool.core.io.file.PathUtil; 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.io.IOException;
import java.nio.file.CopyOption; import java.nio.file.CopyOption;
import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileAlreadyExistsException;
@@ -24,17 +20,22 @@ import java.nio.file.attribute.BasicFileAttributes;
*/ */
public class CopyVisitor extends SimpleFileVisitor<Path> { public class CopyVisitor extends SimpleFileVisitor<Path> {
/**
* 源Path或基准路径用于计算被拷贝文件的相对路径
*/
private final Path source; private final Path source;
private final Path target; private final Path target;
private boolean isTargetCreated;
private final boolean isZipFile;
private String dirRoot = null;
private final CopyOption[] copyOptions; private final CopyOption[] copyOptions;
/**
* 标记目标目录是否创建,省略每次判断目标是否存在
*/
private boolean isTargetCreated;
/** /**
* 构造 * 构造
* *
* @param source 源Path * @param source 源Path或基准路径用于计算被拷贝文件的相对路径
* @param target 目标Path * @param target 目标Path
* @param copyOptions 拷贝选项,如跳过已存在等 * @param copyOptions 拷贝选项,如跳过已存在等
*/ */
@@ -44,58 +45,56 @@ public class CopyVisitor extends SimpleFileVisitor<Path> {
} }
this.source = source; this.source = source;
this.target = target; this.target = target;
this.isZipFile = target instanceof ZipPath;
this.copyOptions = copyOptions; this.copyOptions = copyOptions;
} }
@Override @Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
throws IOException { initTargetDir();
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)); final Path targetDir = resolveTarget(dir);
}
// 在目录不存在的情况下copy方法会创建新目录
try { try {
Files.copy(dir, targetDir, copyOptions); Files.copy(dir, targetDir, copyOptions);
} catch (FileAlreadyExistsException e) { } catch (FileAlreadyExistsException e) {
if (false == Files.isDirectory(targetDir)) if (false == Files.isDirectory(targetDir)) {
// 目标文件存在抛出异常,目录忽略
throw e; throw e;
} }
}
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
} }
@Override @Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException { throws IOException {
if (isZipFile) { initTargetDir();
if (dirRoot == null) { // 如果目标存在无论目录还是文件都抛出FileAlreadyExistsException异常此处不做特别处理
Files.copy(file, target, copyOptions); Files.copy(file, resolveTarget(file), 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);
}
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
} }
/**
* 根据源文件或目录路径,拼接生成目标的文件或目录路径<br>
* 原理是首先截取源路径,得到相对路径,再和目标路径拼接
*
* <p>
* 如:源路径是 /opt/test/,需要拷贝的文件是 /opt/test/a/a.txt得到相对路径 a/a.txt<br>
* 目标路径是/home/,则得到最终目标路径是 /home/a/a.txt
* </p>
*
* @param file 需要拷贝的文件或目录Path
* @return 目标Path
*/
private Path resolveTarget(Path file) {
return target.resolve(source.relativize(file));
}
/** /**
* 初始化目标文件或目录 * 初始化目标文件或目录
*/ */
private void initTarget(){ private void initTargetDir() {
if (false == this.isTargetCreated) { if (false == this.isTargetCreated) {
PathUtil.mkdir(this.target); PathUtil.mkdir(this.target);
this.isTargetCreated = true; this.isTargetCreated = true;

View File

@@ -2,6 +2,7 @@ package cn.hutool.core.util;
import cn.hutool.core.compress.Deflate; import cn.hutool.core.compress.Deflate;
import cn.hutool.core.compress.Gzip; import cn.hutool.core.compress.Gzip;
import cn.hutool.core.compress.ZipCopyVisitor;
import cn.hutool.core.compress.ZipReader; import cn.hutool.core.compress.ZipReader;
import cn.hutool.core.compress.ZipWriter; import cn.hutool.core.compress.ZipWriter;
import cn.hutool.core.exceptions.UtilException; 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.IORuntimeException;
import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.FileSystemUtil; 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 cn.hutool.core.io.resource.Resource;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
@@ -22,12 +23,11 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.file.CopyOption;
import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystem; import java.nio.file.FileSystem;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -40,8 +40,8 @@ import java.util.zip.ZipOutputStream;
/** /**
* 压缩工具类 * 压缩工具类
* *
* @see cn.hutool.core.compress.ZipWriter
* @author Looly * @author Looly
* @see cn.hutool.core.compress.ZipWriter
*/ */
public class ZipUtil { public class ZipUtil {
@@ -84,25 +84,29 @@ public class ZipUtil {
} }
/** /**
* 在zip文件中添加新文件, 如果已经存在则不会有效果 * 在zip文件中添加新文件或目录<br>
* 新文件添加在zip根目录文件夹包括其本身和内容<br>
* 如果待添加文件夹是系统根路径(如/或c:/),则只复制文件夹下的内容
* *
* @param zipFilePathStr zip文件存储路径 * @param zipPath zip文件的Path
* @param appendFilePathStr 待添加文件路径(可以是文件夹) * @param appendFilePath 待添加文件Path(可以是文件夹)
* @param options 拷贝选项,可选是否覆盖等
* @since 5.7.15
*/ */
public static void addFile(String zipFilePathStr, String appendFilePathStr) throws IOException { public static void append(Path zipPath, Path appendFilePath, CopyOption... options) throws IOException {
Path zipPath = Paths.get(zipFilePathStr);
Path appendFilePath = Paths.get(appendFilePathStr);
try (FileSystem zipFileSystem = FileSystemUtil.createZip(zipPath.toString())) { try (FileSystem zipFileSystem = FileSystemUtil.createZip(zipPath.toString())) {
Path root = zipFileSystem.getPath("/"); if (Files.isDirectory(appendFilePath)) {
Path dest = zipFileSystem.getPath(root.toString(), appendFilePath.getFileName().toString()); Path source = appendFilePath.getParent();
if (!Files.isDirectory(appendFilePath)) { if (null == source) {
Files.copy(appendFilePath, dest, StandardCopyOption.COPY_ATTRIBUTES); // 如果用户提供的是根路径,则不复制目录,直接复制目录下的内容
source = appendFilePath;
}
Files.walkFileTree(appendFilePath, new ZipCopyVisitor(source, zipFileSystem, options));
} else { } else {
Files.walkFileTree(appendFilePath, new CopyVisitor(appendFilePath, zipFileSystem.getPath(zipFilePathStr))); Files.copy(appendFilePath, zipFileSystem.getPath(PathUtil.getName(appendFilePath)), options);
} }
} catch (FileAlreadyExistsException ignored) { } catch (FileAlreadyExistsException ignored) {
// 文件已存在, 跳过 // 不覆盖情况下,文件已存在, 跳过
} }
} }

View File

@@ -24,7 +24,7 @@ import java.util.List;
public class ZipUtilTest { public class ZipUtilTest {
@Test @Test
public void addFileTest() throws IOException { public void appendTest() throws IOException {
File appendFile = FileUtil.file("test-zip/addFile.txt"); File appendFile = FileUtil.file("test-zip/addFile.txt");
File zipFile = FileUtil.file("test-zip/test.zip"); File zipFile = FileUtil.file("test-zip/test.zip");
@@ -34,23 +34,27 @@ public class ZipUtilTest {
FileUtil.copy(zipFile, tempZipFile, true); FileUtil.copy(zipFile, tempZipFile, true);
// test file add // test file add
List<String> beforeNames = zipEntryNames(zipFile); List<String> beforeNames = zipEntryNames(tempZipFile);
ZipUtil.addFile(zipFile.getAbsolutePath(), appendFile.getAbsolutePath()); ZipUtil.append(tempZipFile.toPath(), appendFile.toPath());
List<String> afterNames = zipEntryNames(zipFile); List<String> afterNames = zipEntryNames(tempZipFile);
// 确认增加了文件
Assert.assertEquals(beforeNames.size() + 1, afterNames.size());
Assert.assertTrue(afterNames.containsAll(beforeNames)); Assert.assertTrue(afterNames.containsAll(beforeNames));
Assert.assertTrue(afterNames.contains(appendFile.getName())); Assert.assertTrue(afterNames.contains(appendFile.getName()));
// test dir add // test dir add
beforeNames = afterNames; beforeNames = zipEntryNames(tempZipFile);
File addDirFile = FileUtil.file("test-zip/test-add"); File addDirFile = FileUtil.file("test-zip/test-add");
ZipUtil.addFile(zipFile.getAbsolutePath(), addDirFile.getAbsolutePath()); ZipUtil.append(tempZipFile.toPath(), addDirFile.toPath());
afterNames = zipEntryNames(zipFile); afterNames = zipEntryNames(tempZipFile);
// 确认增加了文件和目录,增加目录和目录下一个文件,故此处+2
Assert.assertEquals(beforeNames.size() + 2, afterNames.size());
Assert.assertTrue(afterNames.containsAll(beforeNames)); Assert.assertTrue(afterNames.containsAll(beforeNames));
Assert.assertTrue(afterNames.contains(appendFile.getName())); Assert.assertTrue(afterNames.contains(appendFile.getName()));
// rollback // rollback
FileUtil.copy(tempZipFile, zipFile, true);
Assert.assertTrue(String.format("delete temp file %s failed", tempZipFile.getCanonicalPath()), tempZipFile.delete()); Assert.assertTrue(String.format("delete temp file %s failed", tempZipFile.getCanonicalPath()), tempZipFile.delete());
} }