From 540c5aac79321ee708bee3fa6e1242651ab840f9 Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Sun, 31 Mar 2024 04:10:42 +0200 Subject: [PATCH] feat(utilities): implement AbsolutePath copy/move --- source/Nuke.Common/IO/FileSystemTasks.cs | 9 + .../IO/FileSystemDependentTest.cs | 2 +- .../Nuke.Utilities.Tests/IO/MoveCopyTest.cs | 135 +++++++++++ .../Collections/Enumerable.WhereNotNull.cs | 9 + .../IO/AbsolutePath.MoveCopy.cs | 226 ++++++++++++++++-- 5 files changed, 359 insertions(+), 22 deletions(-) create mode 100644 source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs diff --git a/source/Nuke.Common/IO/FileSystemTasks.cs b/source/Nuke.Common/IO/FileSystemTasks.cs index 48e9c24d4..a2062784f 100644 --- a/source/Nuke.Common/IO/FileSystemTasks.cs +++ b/source/Nuke.Common/IO/FileSystemTasks.cs @@ -188,6 +188,7 @@ public static void DeleteFile(string file) File.Delete(file); } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.Copy)}")] public static void CopyFile(AbsolutePath source, AbsolutePath target, FileExistsPolicy policy = FileExistsPolicy.Fail, bool createDirectories = true) { if (!ShouldCopyFile(source, target, policy)) @@ -200,6 +201,7 @@ public static void CopyFile(AbsolutePath source, AbsolutePath target, FileExists File.Copy(source, target, overwrite: true); } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.CopyToDirectory)}")] public static void CopyFileToDirectory( AbsolutePath source, AbsolutePath targetDirectory, @@ -209,6 +211,7 @@ public static void CopyFileToDirectory( CopyFile(source, Path.Combine(targetDirectory, Path.GetFileName(source).NotNull()), policy, createDirectories); } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.Move)}")] public static void MoveFile(AbsolutePath source, AbsolutePath target, FileExistsPolicy policy = FileExistsPolicy.Fail, bool createDirectories = true) { if (!ShouldCopyFile(source, target, policy)) @@ -224,6 +227,7 @@ public static void MoveFile(AbsolutePath source, AbsolutePath target, FileExists File.Move(source, target); } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.MoveToDirectory)}")] public static void MoveFileToDirectory( AbsolutePath source, AbsolutePath targetDirectory, @@ -233,6 +237,7 @@ public static void MoveFileToDirectory( MoveFile(source, Path.Combine(targetDirectory, Path.GetFileName(source).NotNull()), policy, createDirectories); } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.Rename)}")] public static void RenameFile(AbsolutePath file, string newName, FileExistsPolicy policy = FileExistsPolicy.Fail) { if (Path.GetFileName(file) == newName) @@ -241,6 +246,7 @@ public static void RenameFile(AbsolutePath file, string newName, FileExistsPolic MoveFile(file, Path.Combine(Path.GetDirectoryName(file).NotNull(), newName), policy); } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.Move)}")] public static void MoveDirectory( AbsolutePath source, AbsolutePath target, @@ -264,6 +270,7 @@ public static void MoveDirectory( } } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.MoveToDirectory)}")] public static void MoveDirectoryToDirectory( AbsolutePath source, AbsolutePath targetDirectory, @@ -273,6 +280,7 @@ public static void MoveDirectoryToDirectory( MoveDirectory(source, Path.Combine(targetDirectory, new DirectoryInfo(source).Name), directoryPolicy, filePolicy); } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.Rename)}")] public static void RenameDirectory( string directory, string newName, @@ -282,6 +290,7 @@ public static void RenameDirectory( MoveDirectory(directory, Path.Combine(Path.GetDirectoryName(directory).NotNull(), newName), directoryPolicy, filePolicy); } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.Copy)}")] public static void CopyDirectoryRecursively( AbsolutePath source, AbsolutePath target, diff --git a/source/Nuke.Utilities.Tests/IO/FileSystemDependentTest.cs b/source/Nuke.Utilities.Tests/IO/FileSystemDependentTest.cs index 927d2dce1..8e721a507 100644 --- a/source/Nuke.Utilities.Tests/IO/FileSystemDependentTest.cs +++ b/source/Nuke.Utilities.Tests/IO/FileSystemDependentTest.cs @@ -32,7 +32,7 @@ protected FileSystemDependentTest(ITestOutputHelper testOutputHelper) ExecutionDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location).NotNull(); RootDirectory = Constants.TryGetRootDirectoryFrom(EnvironmentInfo.WorkingDirectory); TestProjectDirectory = ExecutionDirectory.FindParentOrSelf(x => x.ContainsFile("*.csproj")); - TestTempDirectory = ExecutionDirectory / "temp" / $"{GetType().Name}.{TestName}"; + TestTempDirectory = ExecutionDirectory / "temp" / $"{GetType().Name}.{TestName}"; TestTempDirectory.CreateOrCleanDirectory(); } diff --git a/source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs b/source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs new file mode 100644 index 000000000..85f1c74b2 --- /dev/null +++ b/source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs @@ -0,0 +1,135 @@ +// Copyright 2024 Maintainers of NUKE. +// Distributed under the MIT License. +// https://github.com/nuke-build/nuke/blob/master/LICENSE + +using System; +using System.Linq; +using FluentAssertions; +using Nuke.Common.IO; +using Nuke.Common.Utilities.Collections; +using Xunit; +using Xunit.Abstractions; + +namespace Nuke.Common.Tests; + +public class MoveCopyTest : FileSystemDependentTest +{ + public MoveCopyTest(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + AbsolutePathExtensions.DefaultEofLineBreak = false; + } + + [Fact] + public void TestCopyFile() + { + var source = TestTempDirectory / "source.txt"; + source.WriteAllText("foobar"); + + var target = TestTempDirectory / "target.txt"; + source.Copy(target); + + target.FileExists().Should().BeTrue(); + + new Action(() => source.Copy(target)) + .Should().Throw().WithMessage("* already exists"); + + new Action(() => source.Copy(target, policy: ExistsPolicy.FileFail | ExistsPolicy.FileOverwrite)) + .Should().Throw().WithMessage("Multiple file policies *"); + + source.WriteAllText("fizzbuzz"); + source.Copy(target, policy: ExistsPolicy.FileOverwrite) + .Should().Be(target); + target.ReadAllText().Should().Be("fizzbuzz"); + } + + [Fact] + public void TestMoveFile() + { + var source1 = (TestTempDirectory / "source1.txt").TouchFile(); + var source2 = (TestTempDirectory / "source2.txt").TouchFile(); + var source3 = (TestTempDirectory / "source3.txt").TouchFile(); + + var target = TestTempDirectory / "target.txt"; + source2.Move(target); + + target.FileExists().Should().BeTrue(); + source2.FileExists().Should().BeFalse(); + + new Action(() => source1.Move(target, policy: ExistsPolicy.FileFail)) + .Should().Throw().WithMessage("* already exists"); + + source1.Move(target, policy: ExistsPolicy.FileSkip).Should().Be(source1); + source1.Move(target, policy: ExistsPolicy.FileOverwriteIfNewer).Should().Be(source1); + + source3.TouchFile(); + source3.Move(target, policy: ExistsPolicy.FileOverwriteIfNewer).Should().Be(target); + } + + [Fact] + public void TestCopyDirectory() + { + var source = TestTempDirectory / "source"; + var sourceFiles = new[] + { + source / "source1.txt", + source / "source2.txt", + source / "sub" / "source3.txt", + source / "sub" / "source4.txt", + }; + sourceFiles.ForEach(x => x.WriteAllText("source")); + + var target = TestTempDirectory / "target"; + source.Copy(target); + target.GetFiles(depth: int.MaxValue).Select(x => target.GetRelativePathTo(x).ToString()) + .Should().BeEquivalentTo(sourceFiles.Select(x => source.GetRelativePathTo(x).ToString())); + + target.CreateOrCleanDirectory(); + var target0 = (target / "source0.txt").TouchFile(); + var target3 = (target / "sub" / "source3.txt").WriteAllText("target"); + var target4 = (target / "sub" / "source4.txt").WriteAllText("target"); + (source / target.GetRelativePathTo(target4)).TouchFile(); + + new Action(() => source.Copy(target, ExistsPolicy.DirectoryFail)) + .Should().Throw().WithMessage("Policy disallows merging directories"); + target.GetFiles(depth: int.MaxValue).Should().HaveCount(3); + + source.Copy(target, ExistsPolicy.MergeAndSkip); + target0.FileExists().Should().BeTrue(); + target3.ReadAllText().Should().Be("target"); + target4.ReadAllText().Should().Be("target"); + + source.Copy(target, ExistsPolicy.MergeAndOverwriteIfNewer); + target3.ReadAllText().Should().Be("target"); + target4.ReadAllText().Should().Be("source"); + + source.Copy(target, ExistsPolicy.MergeAndOverwrite); + target3.ReadAllText().Should().Be("source"); + } + + [Fact] + public void TestMoveDirectory() + { + var source = TestTempDirectory / "source"; + var sourceFiles = new[] + { + source / "source1.txt", + source / "source2.txt", + source / "sub" / "source3.txt", + source / "sub" / "source4.txt", + }; + sourceFiles.ForEach(x => x.WriteAllText("source")); + + var target = TestTempDirectory / "target"; + (target / "source1.txt").TouchFile(); + (target / "sub" / "source3.txt").TouchFile(); + + new Action(() => source.Move(target)).Should().Throw(); + + source.Move(target, ExistsPolicy.MergeAndSkip); + source.GetFiles(depth: int.MaxValue).Should().HaveCount(2); + + source.Move(target, ExistsPolicy.MergeAndSkip, deleteRemainingFiles: true) + .Should().Be(target); + source.DirectoryExists().Should().BeFalse(); + } +} diff --git a/source/Nuke.Utilities/Collections/Enumerable.WhereNotNull.cs b/source/Nuke.Utilities/Collections/Enumerable.WhereNotNull.cs index 7e480da59..4ee4180ae 100644 --- a/source/Nuke.Utilities/Collections/Enumerable.WhereNotNull.cs +++ b/source/Nuke.Utilities/Collections/Enumerable.WhereNotNull.cs @@ -18,4 +18,13 @@ public static IEnumerable WhereNotNull(this IEnumerable enumerable) { return enumerable.Where(x => x != null); } + + /// + /// Filters the collection to elements that don't meet the condition. + /// + public static IEnumerable WhereNot(this IEnumerable enumerable, Func condition) + where T : class + { + return enumerable.Where(x => condition == null || !condition(x)); + } } diff --git a/source/Nuke.Utilities/IO/AbsolutePath.MoveCopy.cs b/source/Nuke.Utilities/IO/AbsolutePath.MoveCopy.cs index 7535a7a54..f7fb7c9f8 100644 --- a/source/Nuke.Utilities/IO/AbsolutePath.MoveCopy.cs +++ b/source/Nuke.Utilities/IO/AbsolutePath.MoveCopy.cs @@ -4,73 +4,257 @@ using System; using System.IO; +using System.Linq; +using Nuke.Common.Utilities.Collections; namespace Nuke.Common.IO; +[Flags] +public enum ExistsPolicy +{ + DirectoryFail = 1, + DirectoryMerge = 2, + FileFail = 4, + FileSkip = 8, + FileOverwrite = 16, + FileOverwriteIfNewer = 32, + + Fail = DirectoryFail | FileFail, + MergeAndSkip = DirectoryMerge | FileSkip, + MergeAndOverwrite = DirectoryMerge | FileOverwrite, + MergeAndOverwriteIfNewer = DirectoryMerge | FileOverwriteIfNewer +} + partial class AbsolutePathExtensions { /// /// Renames the file or directory. /// - public static AbsolutePath Rename(this AbsolutePath path, string newName) + public static AbsolutePath Rename( + this AbsolutePath source, + string newName, + ExistsPolicy policy = ExistsPolicy.Fail) { - return path.Move(path.Parent / newName); + return source.Move(source.Parent / newName, policy); } /// /// Renames the file or directory. /// - public static AbsolutePath Rename(this AbsolutePath path, Func newName) + public static AbsolutePath Rename( + this AbsolutePath source, + Func newName, + ExistsPolicy policy = ExistsPolicy.Fail) { - return path.Rename(newName.Invoke(path)); + return source.Rename(newName.Invoke(source), policy); } /// /// Renames the file without changing the extension. /// - public static AbsolutePath RenameWithoutExtension(this AbsolutePath path, string newName) + public static AbsolutePath RenameWithoutExtension( + this AbsolutePath source, + string newName, + ExistsPolicy policy = ExistsPolicy.Fail) { - Assert.True(path.FileExists()); - return path.Move(path.Parent / newName + path.Extension); + return source.Move(source.Parent / newName + source.Extension, policy); } /// /// Renames the file without changing the extension. /// - public static AbsolutePath RenameWithoutExtension(this AbsolutePath path, Func newName) + public static AbsolutePath RenameWithoutExtension( + this AbsolutePath source, + Func newName, + ExistsPolicy policy = ExistsPolicy.Fail) { - return path.RenameWithoutExtension(newName.Invoke(path)); + return source.RenameWithoutExtension(newName.Invoke(source), policy); } /// /// Moves the file or directory to another directory. /// - public static AbsolutePath MoveToDirectory(this AbsolutePath path, AbsolutePath directory) + public static AbsolutePath MoveToDirectory( + this AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy = ExistsPolicy.Fail, + bool createDirectories = true) { - Assert.True(directory.Exists()); - return path.Move(directory / path.Name); + return source.Move(target / source.Name, policy, createDirectories); + } + + /// + /// Copies the file or directory to another directory. + /// + public static AbsolutePath CopyToDirectory( + this AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy = ExistsPolicy.Fail, + Func excludeDirectory = null, + Func excludeFile = null, + bool createDirectories = true) + { + return source.Copy(target / source.Name, policy, excludeDirectory, excludeFile, createDirectories); } /// /// Moves the file or directory. /// - public static AbsolutePath Move(this AbsolutePath path, Func newPath) + public static AbsolutePath Move( + this AbsolutePath source, + Func newPath, + ExistsPolicy policy = ExistsPolicy.Fail, + bool createDirectories = true) { - return path.Move(newPath.Invoke(path)); + return source.Move(newPath.Invoke(source), policy, createDirectories); } /// /// Moves the file or directory. /// - public static AbsolutePath Move(this AbsolutePath path, AbsolutePath newPath) + public static AbsolutePath Move( + this AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy = ExistsPolicy.Fail, + bool createDirectories = true, + bool deleteRemainingFiles = false) + { + Assert.True(source.DirectoryExists() || source.FileExists()); + + if (source.DirectoryExists()) + return MoveDirectory(source, target, policy, createDirectories, deleteRemainingFiles); + + if (source.FileExists()) + return MoveFile(source, target, policy, createDirectories); + + throw new Exception("Unreachable"); + } + + /// + /// Copies the file or directory. + /// + public static AbsolutePath Copy( + this AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy = ExistsPolicy.Fail, + Func excludeDirectory = null, + Func excludeFile = null, + bool createDirectories = true) + { + Assert.True(source.DirectoryExists() || source.FileExists()); + + if (source.DirectoryExists()) + return CopyDirectory(source, target, policy, excludeDirectory, excludeFile, createDirectories); + + if (source.FileExists()) + return CopyFile(source, target, policy, createDirectories); + + throw new Exception("Unreachable"); + } + + private static AbsolutePath MoveFile( + this AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy = ExistsPolicy.Fail, + bool createDirectories = true) + { + return HandleFile(source, target, policy, createDirectories, () => + { + target.DeleteFile(); + File.Move(source, target); + }); + } + + private static AbsolutePath CopyFile( + this AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy = ExistsPolicy.Fail, + bool createDirectories = true) + { + return HandleFile(source, target, policy, createDirectories, () => + { + File.Copy(source, target, overwrite: true); + }); + } + + private static AbsolutePath HandleFile( + AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy, + bool createDirectories, + Action action) + { + if (File.Exists(target) && !Permitted()) + return source; + + if (createDirectories) + target.Parent.CreateDirectory(); + + action.Invoke(); + return target; + + bool Permitted() + { + var filePolicies = ExistsPolicy.FileFail | ExistsPolicy.FileSkip | ExistsPolicy.FileOverwrite | ExistsPolicy.FileOverwriteIfNewer; + return (policy & filePolicies) switch + { + ExistsPolicy.FileFail => throw new Exception($"File '{target}' already exists"), + ExistsPolicy.FileSkip => false, + ExistsPolicy.FileOverwrite => true, + ExistsPolicy.FileOverwriteIfNewer => File.GetLastWriteTimeUtc(target) < File.GetLastWriteTimeUtc(source), + _ => throw new ArgumentOutOfRangeException(nameof(policy), policy, message: "Multiple file policies set") + }; + } + } + + private static AbsolutePath MoveDirectory( + this AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy = ExistsPolicy.Fail, + bool createDirectories = true, + bool deleteRemainingFiles = false) + { + return HandleDirectory(source, target, policy, createDirectories, () => + { + source.GetDirectories().ForEach(x => x.MoveDirectory(target / source.GetRelativePathTo(x), policy)); + source.GetFiles().ForEach(x => x.MoveFile(target / source.GetRelativePathTo(x), policy)); + + if (!source.ToDirectoryInfo().EnumerateFileSystemInfos().Any() || deleteRemainingFiles) + source.DeleteDirectory(); + }); + } + + private static AbsolutePath CopyDirectory( + this AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy = ExistsPolicy.Fail, + Func excludeDirectory = null, + Func excludeFile = null, + bool createDirectories = true) + { + return HandleDirectory(source, target, policy, createDirectories, () => + { + source.GetDirectories().WhereNot(excludeDirectory).ForEach(x => x.CopyDirectory(target / source.GetRelativePathTo(x), policy, excludeDirectory, excludeFile)); + source.GetFiles().WhereNot(excludeFile).ForEach(x => x.CopyFile(target / source.GetRelativePathTo(x), policy)); + }); + } + + private static AbsolutePath HandleDirectory( + AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy, + bool createDirectories, + Action action) { - Assert.True(path.DirectoryExists() || path.FileExists()); + Assert.DirectoryExists(source); + Assert.False(source.Contains(target), $"Target directory '{target}' must not be in source directory '{source}'"); + Assert.True(!Directory.Exists(target) || (policy.HasFlag(ExistsPolicy.DirectoryMerge) && !policy.HasFlag(ExistsPolicy.DirectoryFail)), + "Policy disallows merging directories"); - if (path.DirectoryExists()) - Directory.Move(path, newPath); - else if (path.FileExists()) - File.Move(path, newPath); + if (createDirectories) + target.CreateDirectory(); - return path; + action.Invoke(); + return target; } }