add PathMover

This commit is contained in:
Looly 2023-03-05 19:07:07 +08:00
parent 9784e8e2b4
commit 7713db1730
4 changed files with 231 additions and 94 deletions

View File

@ -1097,34 +1097,27 @@ public class FileUtil extends PathUtil {
}
/**
* 移动文件或者目录
* 移动文件或目录到目标中例如
* <ul>
* <li>如果src为文件target为目录则移动到目标目录下存在同名文件则按照是否覆盖参数执行</li>
* <li>如果src为文件target为文件则按照是否覆盖参数执行</li>
* <li>如果src为文件target为不存在的路径则重命名源文件到目标指定的文件如moveContent("/a/b", "/c/d"), d不存在则b变成d</li>
* <li>如果src为目录target为文件抛出{@link IllegalArgumentException}</li>
* <li>如果src为目录target为目录则将源目录及其内容移动到目标路径目录中如move("/a/b", "/c/d")结果为"/c/d/b"</li>
* <li>如果src为目录target为不存在的路径则创建目标路径为目录将源目录及其内容移动到目标路径目录中如move("/a/b", "/c/d")结果为"/c/d/b"</li>
* </ul>
*
* @param src 源文件或者目录
* @param target 目标文件或者目录
* @param isOverride 是否覆盖目标只有目标为文件才覆盖
* @param src 源文件或目录路径
* @param target 目标路径如果为目录则移动到此目录下
* @param isOverride 是否覆盖目标文件
* @return 目标文件或目录
* @throws IORuntimeException IO异常
* @see PathUtil#move(Path, Path, boolean)
*/
public static void move(final File src, final File target, final boolean isOverride) throws IORuntimeException {
public static File move(final File src, final File target, final boolean isOverride) throws IORuntimeException {
Assert.notNull(src, "Src file must be not null!");
Assert.notNull(target, "target file must be not null!");
move(src.toPath(), target.toPath(), isOverride);
}
/**
* 移动文件或者目录
*
* @param src 源文件或者目录
* @param target 目标文件或者目录
* @param isOverride 是否覆盖目标只有目标为文件才覆盖
* @throws IORuntimeException IO异常
* @see PathUtil#moveContent(Path, Path, boolean)
* @since 5.7.9
*/
public static void moveContent(final File src, final File target, final boolean isOverride) throws IORuntimeException {
Assert.notNull(src, "Src file must be not null!");
Assert.notNull(target, "target file must be not null!");
moveContent(src.toPath(), target.toPath(), isOverride);
return move(src.toPath(), target.toPath(), isOverride).toFile();
}
/**
@ -1763,7 +1756,7 @@ public class FileUtil extends PathUtil {
* @param file 文件
* @return 输入流
* @throws IORuntimeException 文件未找到
* @see IoUtil#toStream(File)
* @see IoUtil#toStream(File)
*/
public static BufferedInputStream getInputStream(final File file) throws IORuntimeException {
return IoUtil.toBuffered(IoUtil.toStream(file));
@ -3048,7 +3041,7 @@ public class FileUtil extends PathUtil {
* getParent(file("d:/aaa/bbb/cc/ddd")) - "d:/aaa/bbb/cc"
* </pre>
*
* @param file 目录或文件
* @param file 目录或文件
* @return 路径File如果不存在返回null
* @since 6.0.0
*/

View File

@ -0,0 +1,148 @@
package cn.hutool.core.io.file;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.file.visitor.MoveVisitor;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjUtil;
import java.io.IOException;
import java.nio.file.*;
/**
* 文件移动封装
*
* @author looly
* @since 6.0.0
*/
public class PathMover {
/**
* 创建文件或目录移动器
*
* @param src 源文件或目录
* @param target 目标文件或目录
* @param isOverride 是否覆盖目标文件
* @return {@link PathMover}
*/
public static PathMover of(final Path src, final Path target, final boolean isOverride) {
return of(src, target, isOverride ? new CopyOption[]{StandardCopyOption.REPLACE_EXISTING} : new CopyOption[]{});
}
/**
* 创建文件或目录移动器
*
* @param src 源文件或目录
* @param target 目标文件或目录
* @param options 移动参数
* @return {@link PathMover}
*/
public static PathMover of(final Path src, final Path target, final CopyOption[] options) {
return new PathMover(src, target, options);
}
private final Path src;
private final Path target;
private final CopyOption[] options;
/**
* 构造
*
* @param src 源文件或目录
* @param target 目标文件或目录
* @param options 移动参数
*/
public PathMover(final Path src, final Path target, final CopyOption[] options) {
this.src = Assert.notNull(src, "Src path must be not null !");
this.target = Assert.notNull(target, "Target path must be not null !");
this.options = options;
}
/**
* 移动文件或目录到目标中例如
* <ul>
* <li>如果src为文件target为目录则移动到目标目录下存在同名文件则按照是否覆盖参数执行</li>
* <li>如果src为文件target为文件则按照是否覆盖参数执行</li>
* <li>如果src为文件target为不存在的路径则重命名源文件到目标指定的文件如moveContent("/a/b", "/c/d"), d不存在则b变成d</li>
* <li>如果src为目录target为文件抛出{@link IllegalArgumentException}</li>
* <li>如果src为目录target为目录则将源目录及其内容移动到目标路径目录中如move("/a/b", "/c/d")结果为"/c/d/b"</li>
* <li>如果src为目录target为不存在的路径则创建目标路径为目录将源目录及其内容移动到目标路径目录中如move("/a/b", "/c/d")结果为"/c/d/b"</li>
* </ul>
*
* @return 目标文件Path
*/
public Path move() {
final Path src = this.src;
Path target = this.target;
final CopyOption[] options = ObjUtil.defaultIfNull(this.options, new CopyOption[]{});
if (false == PathUtil.exists(target, false) || PathUtil.isDirectory(target)) {
// 创建子路径的情况1是目标是目录需要移动到目录下2是目标不能存在自动创建目录
target = target.resolve(src.getFileName());
}
// issue#2893 target 不存在导致NoSuchFileException
if (Files.exists(target) && PathUtil.equals(src, target)) {
// issue#2845当用户传入目标路径与源路径一致时直接返回否则会导致删除风险
return target;
}
// 自动创建目标的父目录
PathUtil.mkParentDirs(target);
try {
return Files.move(src, target, options);
} catch (final IOException e) {
if (e instanceof FileAlreadyExistsException) {
// 目标文件已存在直接抛出异常
// issue#I4QV0L@Gitee
throw new IORuntimeException(e);
}
// 移动失败可能是跨分区移动导致的采用递归移动方式
try {
Files.walkFileTree(src, new MoveVisitor(src, target, options));
// 移动后空目录没有删除
PathUtil.del(src);
} catch (final IOException e2) {
throw new IORuntimeException(e2);
}
return target;
}
}
/**
* 移动文件或目录内容到目标中例如
* <ul>
* <li>如果src为文件target为目录则移动到目标目录下存在同名文件则按照是否覆盖参数执行</li>
* <li>如果src为文件target为文件则按照是否覆盖参数执行</li>
* <li>如果src为文件target为不存在的路径则重命名源文件到目标指定的文件如moveContent("/a/b", "/c/d"), d不存在则b变成d</li>
* <li>如果src为目录target为文件抛出{@link IllegalArgumentException}</li>
* <li>如果src为目录target为目录则将源目录下的内容移动到目标路径目录中</li>
* <li>如果src为目录target为不存在的路径则创建目标路径为目录将源目录下的内容移动到目标路径目录中</li>
* </ul>
*
* @return 目标文件Path
*/
public Path moveContent() {
final Path src = this.src;
final Path target = this.target;
final CopyOption[] options = ObjUtil.defaultIfNull(this.options, new CopyOption[]{});
// 移动失败可能是跨分区移动导致的采用递归移动方式
try {
if (false == PathUtil.isDirectory(src)) {
// 文件移动到目标目录或文件
return Files.move(src, target, options);
}
if (false == PathUtil.isDirectory(target)) {
throw new IllegalArgumentException("Can not move dir content to a file");
}
// 移动源目录下的内容而不删除目录
Files.walkFileTree(src, new MoveVisitor(src, target, options));
} catch (final IOException e) {
throw new IORuntimeException(e);
}
return target;
}
}

View File

@ -4,31 +4,12 @@ import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.visitor.CopyVisitor;
import cn.hutool.core.io.file.visitor.DelVisitor;
import cn.hutool.core.io.file.visitor.MoveVisitor;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.CharsetUtil;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.AccessDeniedException;
import java.nio.file.CopyOption;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.EnumSet;
@ -470,75 +451,43 @@ public class PathUtil {
}
/**
* 移动文件或目录<br>
* 当目标是目录时会将源文件或文件夹整体移动至目标目录下<br>
* 例如
* 移动文件或目录到目标中例如
* <ul>
* <li>move("/usr/aaa/abc.txt", "/usr/bbb")结果为"/usr/bbb/abc.txt"</li>
* <li>move("/usr/aaa", "/usr/bbb")结果为"/usr/bbb/aaa"</li>
* <li>如果src为文件target为目录则移动到目标目录下存在同名文件则按照是否覆盖参数执行</li>
* <li>如果src为文件target为文件则按照是否覆盖参数执行</li>
* <li>如果src为文件target为不存在的路径则重命名源文件到目标指定的文件如moveContent("/a/b", "/c/d"), d不存在则b变成d</li>
* <li>如果src为目录target为文件抛出{@link IllegalArgumentException}</li>
* <li>如果src为目录target为目录则将源目录及其内容移动到目标路径目录中如move("/a/b", "/c/d")结果为"/c/d/b"</li>
* <li>如果src为目录target为不存在的路径则创建目标路径为目录将源目录及其内容移动到目标路径目录中如move("/a/b", "/c/d")结果为"/c/d/b"</li>
* </ul>
*
* @param src 源文件或目录路径
* @param target 目标路径如果为目录则移动到此目录下
* @param isOverride 是否覆盖目标文件
* @return 目标文件Path
* @since 5.5.1
*/
public static Path move(final Path src, Path target, final boolean isOverride) {
Assert.notNull(src, "Src path must be not null !");
Assert.notNull(target, "Target path must be not null !");
if (isDirectory(target)) {
target = target.resolve(src.getFileName());
}
return moveContent(src, target, isOverride);
public static Path move(final Path src, final Path target, final boolean isOverride) {
return PathMover.of(src, target, isOverride).move();
}
/**
* 移动文件或目录内容到目标目录例如
* 移动文件或目录内容到目标中例如
* <ul>
* <li>moveContent("/usr/aaa/abc.txt", "/usr/bbb")结果为"/usr/bbb/abc.txt"</li>
* <li>moveContent("/usr/aaa", "/usr/bbb")结果为"/usr/bbb"</li>
* <li>如果src为文件target为目录则移动到目标目录下存在同名文件则按照是否覆盖参数执行</li>
* <li>如果src为文件target为文件则按照是否覆盖参数执行</li>
* <li>如果src为文件target为不存在的路径则重命名源文件到目标指定的文件如moveContent("/a/b", "/c/d"), d不存在则b变成d</li>
* <li>如果src为目录target为文件抛出{@link IllegalArgumentException}</li>
* <li>如果src为目录target为目录则将源目录下的内容移动到目标路径目录中</li>
* <li>如果src为目录target为不存在的路径则创建目标路径为目录将源目录下的内容移动到目标路径目录中</li>
* </ul>
*
* @param src 源文件或目录路径
* @param target 目标路径如果为目录则移动到此目录下
* @param isOverride 是否覆盖目标文件
* @return 目标文件Path
* @since 5.7.9
*/
public static Path moveContent(final Path src, final Path target, final boolean isOverride) {
Assert.notNull(src, "Src path must be not null !");
Assert.notNull(target, "Target path must be not null !");
// issue#2893 target 不存在导致NoSuchFileException
if(Files.exists(target) && equals(src, target)){
// issue#2845当用户传入目标路径与源路径一致时直接返回否则会导致删除风险
return target;
}
final CopyOption[] options = isOverride ? new CopyOption[]{StandardCopyOption.REPLACE_EXISTING} : new CopyOption[]{};
// 自动创建目标的父目录
mkParentDirs(target);
try {
return Files.move(src, target, options);
} catch (final IOException e) {
if(e instanceof FileAlreadyExistsException){
// 目标文件已存在直接抛出异常
// issue#I4QV0L@Gitee
throw new IORuntimeException(e);
}
// 移动失败可能是跨分区移动导致的采用递归移动方式
try {
Files.walkFileTree(src, new MoveVisitor(src, target, options));
// 移动后空目录没有删除
del(src);
} catch (final IOException e2) {
throw new IORuntimeException(e2);
}
return target;
}
return PathMover.of(src, target, isOverride).moveContent();
}
/**

View File

@ -0,0 +1,47 @@
package cn.hutool.core.io.file;
import cn.hutool.core.io.FileUtil;
import org.junit.Ignore;
import org.junit.Test;
/**
* 移动情况测试环境
* <pre>
* d:/test/dir1/test1.txt 文件
* d:/test/dir2/ 空目录
* </pre>
*/
public class IssueI666HBTest {
@Test
@Ignore
public void moveDirToDirTest() {
// 目录移动到目录将整个目录移动
// 会将dir1及其内容移动到dir2下变成dir2/dir1
FileUtil.move(FileUtil.file("d:/test/dir1"), FileUtil.file("d:/test/dir2"), false);
}
@Test
@Ignore
public void moveFileToDirTest() {
// 文件移动到目录
// 会将test1.txt移动到dir2下变成dir2/test1.txt
FileUtil.move(FileUtil.file("d:/test/dir1/test1.txt"), FileUtil.file("d:/test/dir2"), false);
}
@Test
@Ignore
public void moveDirToDirNotExistTest() {
// 目录移动到目标dir3不存在将整个目录移动
// 会将目录dir1变成目录dir3
FileUtil.move(FileUtil.file("d:/test/dir1"), FileUtil.file("d:/test/dir3"), false);
}
@Test
@Ignore
public void moveFileToTargetNotExistTest() {
// 目录移动到目录将整个目录移动
// 会将test1.txt重命名为test2
FileUtil.move(FileUtil.file("d:/test/dir1/test1.txt"), FileUtil.file("d:/test/test2"), false);
}
}