diff --git a/CHANGELOG.md b/CHANGELOG.md index 872a96be0..9de52d378 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,14 @@ ------------------------------------------------------------------------------------------------------------- -# 5.8.14.M1 (2023-03-03) +# 5.8.14.M1 (2023-03-05) ### 🐣新特性 +* 【core 】 增加PathMover(issue#I666HB@Github) + ### 🐞Bug修复 +* 【core 】 修复FileUtil.moveContent会删除源目录的问题(issue#I666HB@Github) +* 【http 】 修复HttpBase.body导致的空指针问题 ------------------------------------------------------------------------------------------------------------- diff --git a/hutool-core/src/main/java/cn/hutool/core/io/file/PathMover.java b/hutool-core/src/main/java/cn/hutool/core/io/file/PathMover.java new file mode 100755 index 000000000..ef7acd9be --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/file/PathMover.java @@ -0,0 +1,166 @@ +package cn.hutool.core.io.file; + +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 5.8.14 + */ +public class PathMover { + + /** + * 创建文件或目录移动器 + * + * @param src 源文件或目录 + * @param target 目标文件或目录 + * @param isOverride 是否覆盖目标文件 + * @return {@code 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 {@code 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 源文件或目录,不能为{@code null}且必须存在 + * @param target 目标文件或目录 + * @param options 移动参数 + */ + public PathMover(final Path src, final Path target, final CopyOption[] options) { + Assert.notNull(target, "Src path must be not null !"); + if(false == PathUtil.exists(src, false)){ + throw new IllegalArgumentException("Src path is not exist!"); + } + this.src = src; + this.target = Assert.notNull(target, "Target path must be not null !"); + this.options = ObjUtil.defaultIfNull(options, new CopyOption[]{});; + } + + /** + * 移动文件或目录到目标中,例如: + * + * + * @return 目标文件Path + */ + public Path move() { + final Path src = this.src; + Path target = this.target; + final CopyOption[] options = this.options; + + if (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); + } + // 移动失败,可能是跨分区移动导致的,采用递归移动方式 + walkMove(src, target, options); + // 移动后删除空目录 + PathUtil.del(src); + return target; + } + } + + /** + * 移动文件或目录内容到目标中,例如: + * + * + * @return 目标文件Path + */ + public Path moveContent() { + final Path src = this.src; + if (PathUtil.isExistsAndNotDirectory(target, false)) { + // 文件移动调用move方法 + return move(); + } + + final Path target = this.target; + if (PathUtil.isExistsAndNotDirectory(target, false)) { + // 目标不能为文件 + throw new IllegalArgumentException("Can not move dir content to a file"); + } + + // issue#2893 target 不存在导致NoSuchFileException + if (PathUtil.equals(src, target)) { + // issue#2845,当用户传入目标路径与源路径一致时,直接返回,否则会导致删除风险。 + return target; + } + + final CopyOption[] options = this.options; + + // 移动失败,可能是跨分区移动导致的,采用递归移动方式 + walkMove(src, target, options); + return target; + } + + /** + * 递归移动 + * + * @param src 源目录 + * @param target 目标目录 + * @param options 移动参数 + */ + private static void walkMove(final Path src, final Path target, final CopyOption... options) { + try { + // 移动源目录下的内容而不删除目录 + Files.walkFileTree(src, new MoveVisitor(src, target, options)); + } catch (final IOException e) { + throw new IORuntimeException(e); + } + } +} 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 099751144..93d10586e 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 @@ -470,74 +470,44 @@ public class PathUtil { } /** - * 移动文件或目录
- * 当目标是目录时,会将源文件或文件夹整体移动至目标目录下
- * 例如: + * 移动文件或目录到目标中,例如: * * * @param src 源文件或目录路径 * @param target 目标路径,如果为目录,则移动到此目录下 * @param isOverride 是否覆盖目标文件 * @return 目标文件Path - * @since 5.5.1 */ public static Path move(Path src, Path target, 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; - } - - if (isDirectory(target)) { - target = target.resolve(src.getFileName()); - } - return moveContent(src, target, isOverride); + return PathMover.of(src, target, isOverride).move(); } /** - * 移动文件或目录内容到目标目录中,例如: + * 移动文件或目录内容到目标中,例如: * * * @param src 源文件或目录路径 * @param target 目标路径,如果为目录,则移动到此目录下 * @param isOverride 是否覆盖目标文件 * @return 目标文件Path - * @since 5.7.9 */ public static Path moveContent(Path src, Path target, boolean isOverride) { - Assert.notNull(src, "Src path must be not null !"); - Assert.notNull(target, "Target path must be not null !"); - final CopyOption[] options = isOverride ? new CopyOption[]{StandardCopyOption.REPLACE_EXISTING} : new CopyOption[]{}; - - // 自动创建目标的父目录 - mkParentDirs(target); - try { - return Files.move(src, target, options); - } catch (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 (IOException e2) { - throw new IORuntimeException(e2); - } - return target; - } + return PathMover.of(src, target, isOverride).moveContent(); } /** @@ -599,6 +569,22 @@ public class PathUtil { return Files.exists(path, options); } + /** + * 判断是否存在且为非目录 + * + * + * @param path {@link Path} + * @param isFollowLinks 是否追踪到软链对应的真实地址 + * @return 如果为目录true + * @since 5.8.14 + */ + public static boolean isExistsAndNotDirectory(final Path path, final boolean isFollowLinks) { + return exists(path, isFollowLinks) && false == isDirectory(path, isFollowLinks); + } + /** * 判断给定的目录是否为给定文件或文件夹的子目录 * diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpBase.java b/hutool-http/src/main/java/cn/hutool/http/HttpBase.java index aaa280763..41e1d1ba6 100644 --- a/hutool-http/src/main/java/cn/hutool/http/HttpBase.java +++ b/hutool-http/src/main/java/cn/hutool/http/HttpBase.java @@ -304,7 +304,7 @@ public abstract class HttpBase { * @return byte[] */ public byte[] bodyBytes() { - return this.body.readBytes(); + return this.body == null ? null : this.body.readBytes(); } /** @@ -355,7 +355,7 @@ public abstract class HttpBase { } sb.append("Request Body: ").append(StrUtil.CRLF); - sb.append(" ").append(StrUtil.str(this.body.readBytes(), this.charset)).append(StrUtil.CRLF); + sb.append(" ").append(StrUtil.str(this.bodyBytes(), this.charset)).append(StrUtil.CRLF); return sb.toString(); } diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java b/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java index 6607bb157..96ec991c3 100755 --- a/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java +++ b/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java @@ -241,7 +241,7 @@ public class HttpResponse extends HttpBase implements Closeable { if (isAsync) { return this.in; } - return this.body.getStream(); + return null == this.body ? null : this.body.getStream(); } /** @@ -253,7 +253,7 @@ public class HttpResponse extends HttpBase implements Closeable { @Override public byte[] bodyBytes() { sync(); - return this.body.readBytes(); + return super.bodyBytes(); } /**