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
@ -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

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());
}
/**
* 获取{@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;
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> {
/**
* 源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<Path> {
@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;
}
/**
* 根据源文件或目录路径拼接生成目标的文件或目录路径<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(){
if(false == this.isTargetCreated){
private void initTargetDir() {
if (false == this.isTargetCreated) {
PathUtil.mkdir(this.target);
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.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文件中添加新文件或目录<br>
* 新文件添加在zip根目录文件夹包括其本身和内容<br>
* 如果待添加文件夹是系统根路径/或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<ZipEntry> 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<ZipEntry> 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));
}
}

View File

@ -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<String> beforeNames = zipEntryNames(zipFile);
ZipUtil.addFile(zipFile.getAbsolutePath(), appendFile.getAbsolutePath());
List<String> afterNames = zipEntryNames(zipFile);
List<String> beforeNames = zipEntryNames(tempZipFile);
ZipUtil.append(tempZipFile.toPath(), appendFile.toPath());
List<String> 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());
}