From 5f6a40c92aa8a06413216d22a672fc4e23edc634 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 1 Nov 2026 17:46:06 +0100 Subject: [PATCH] Rename files sample: .NET 10, batch rename name collision fix, folder and extension renames Retarget to net10.0 and current xUnit packages, make the sample runnable with Program.cs, check the new name is free before each batch move, add Directory.Move and Path.ChangeExtension helpers, and add five tests. --- .../FileRenameService.cs | 11 ++- .../FolderRenameService.cs | 27 ++++++ .../ImageRenameService.cs | 40 ++++++++- .../RenameFilesWithCSharp/Program.cs | 6 ++ .../RenameFilesWithCSharp.csproj | 6 +- .../Tests/RenameFilesUnitTests.cs | 86 ++++++++++++++++++- .../RenameFilesWithCSharp/Tests/Tests.csproj | 12 +-- 7 files changed, 174 insertions(+), 14 deletions(-) create mode 100644 files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/FolderRenameService.cs create mode 100644 files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/Program.cs diff --git a/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/FileRenameService.cs b/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/FileRenameService.cs index 92f0e29311..fae51aabad 100644 --- a/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/FileRenameService.cs +++ b/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/FileRenameService.cs @@ -22,4 +22,13 @@ public static void ExecuteRenameFileWithFileClass() Directory.Delete(directoryPath, true); } -} \ No newline at end of file + + public static string ChangeExtension(string filePath, string newExtension) + { + var newFilePath = Path.ChangeExtension(filePath, newExtension); + + File.Move(filePath, newFilePath); + + return newFilePath; + } +} diff --git a/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/FolderRenameService.cs b/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/FolderRenameService.cs new file mode 100644 index 0000000000..3a2f14d059 --- /dev/null +++ b/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/FolderRenameService.cs @@ -0,0 +1,27 @@ +namespace RenameFilesWithCSharp; + +public static class FolderRenameService +{ + public static void RenameFolder(string oldFolder, string newFolder) + { + Directory.Move(oldFolder, newFolder); + } + + public static void ExecuteRenameFolder() + { + var parentPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + var oldFolder = Path.Combine(parentPath, "Invoices2025"); + Directory.CreateDirectory(Path.Combine(oldFolder, "Q1")); + File.Create(Path.Combine(oldFolder, "Q1", "invoice-001.pdf")).Dispose(); + + var newFolder = Path.Combine(parentPath, "Invoices-2025"); + + RenameFolder(oldFolder, newFolder); + + Console.WriteLine($"Renamed {Path.GetFileName(oldFolder)} to {Path.GetFileName(newFolder)}"); + Console.WriteLine($"Invoice moved along: {File.Exists(Path.Combine(newFolder, "Q1", "invoice-001.pdf"))}"); + + Directory.Delete(parentPath, true); + } +} diff --git a/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/ImageRenameService.cs b/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/ImageRenameService.cs index 011ed4cae4..eb4bc021d3 100644 --- a/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/ImageRenameService.cs +++ b/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/ImageRenameService.cs @@ -16,11 +16,45 @@ public static void RenameImagesWithDateTime(string directoryPath) var fileExtension = Path.GetExtension(imageFile); - var newFileName = $"Image_{creationTime:yyyy-MM-dd_HHmmss}{fileExtension}"; + var baseName = $"Image_{creationTime:yyyy-MM-dd_HHmmss}"; - var newFilePath = Path.Combine(directoryPath, newFileName); + var newFilePath = Path.Combine(directoryPath, $"{baseName}{fileExtension}"); + + for (var copy = 1; File.Exists(newFilePath) && newFilePath != imageFile; copy++) + { + newFilePath = Path.Combine(directoryPath, $"{baseName}_{copy}{fileExtension}"); + } File.Move(imageFile, newFilePath); } } -} \ No newline at end of file + + public static void RenameImagesWithDateTimeUsingFileInfo(string directoryPath) + { + var allowedExtensions = new[] { ".jpg", ".png", ".jpeg", ".gif" }; + + var imageFiles = Directory.EnumerateFiles(directoryPath) + .Where(file => allowedExtensions.Contains(Path.GetExtension(file), + StringComparer.OrdinalIgnoreCase)); + + foreach (var imageFile in imageFiles) + { + var file = new FileInfo(imageFile); + + var creationTime = file.CreationTime; + + var fileExtension = file.Extension; + + var baseName = $"Image_{creationTime:yyyy-MM-dd_HHmmss}"; + + var newFilePath = Path.Combine(directoryPath, $"{baseName}{fileExtension}"); + + for (var copy = 1; File.Exists(newFilePath) && newFilePath != imageFile; copy++) + { + newFilePath = Path.Combine(directoryPath, $"{baseName}_{copy}{fileExtension}"); + } + + file.MoveTo(newFilePath); + } + } +} diff --git a/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/Program.cs b/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/Program.cs new file mode 100644 index 0000000000..6489332579 --- /dev/null +++ b/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/Program.cs @@ -0,0 +1,6 @@ +using RenameFilesWithCSharp; + +FileFinderService.ExecuteFindFilesInFolder(); +FileRenameService.ExecuteRenameFileWithFileClass(); +FileInfoRenameService.ExecuteRenameFileWithFileInfoClass(); +FolderRenameService.ExecuteRenameFolder(); diff --git a/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/RenameFilesWithCSharp.csproj b/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/RenameFilesWithCSharp.csproj index d81befcd75..238f157e2e 100644 --- a/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/RenameFilesWithCSharp.csproj +++ b/files-csharp/RenameFilesWithCSharp/RenameFilesWithCSharp/RenameFilesWithCSharp.csproj @@ -1,8 +1,8 @@ - Library - net8.0 + Exe + net10.0 enable enable - \ No newline at end of file + diff --git a/files-csharp/RenameFilesWithCSharp/Tests/RenameFilesUnitTests.cs b/files-csharp/RenameFilesWithCSharp/Tests/RenameFilesUnitTests.cs index c26b181b76..a112084fb2 100644 --- a/files-csharp/RenameFilesWithCSharp/Tests/RenameFilesUnitTests.cs +++ b/files-csharp/RenameFilesWithCSharp/Tests/RenameFilesUnitTests.cs @@ -97,4 +97,88 @@ public void WhenBatchRenamingPhotosWithDateTimeAndNoAllowedExtensionsExist_ThenN // Assert Assert.Equal(existingFiles, updatedFiles); } -} \ No newline at end of file + + [Fact] + public void WhenTwoPhotosShareACreationSecond_ThenBothAreRenamedWithoutCollision() + { + var first = Path.Combine(_tempDirectory, "a.jpg"); + var second = Path.Combine(_tempDirectory, "b.jpg"); + File.Create(first).Close(); + File.Create(second).Close(); + var sameSecond = new DateTime(2025, 6, 1, 10, 30, 0); + File.SetCreationTime(first, sameSecond); + File.SetCreationTime(second, sameSecond); + + // Act + ImageRenameService.RenameImagesWithDateTime(_tempDirectory); + + // Assert + var names = Directory.GetFiles(_tempDirectory).Select(f => Path.GetFileName(f)).Order(StringComparer.Ordinal).ToArray(); + Assert.Equal(["Image_2025-06-01_103000.jpg", "Image_2025-06-01_103000_1.jpg"], names); + } + + [Fact] + public void WhenBatchRenameRunsTwice_ThenSecondRunChangesNothing() + { + var sameSecond = new DateTime(2025, 6, 1, 10, 30, 0); + foreach (var name in new[] { "a.jpg", "b.jpg" }) + { + var path = Path.Combine(_tempDirectory, name); + File.Create(path).Close(); + File.SetCreationTime(path, sameSecond); + } + ImageRenameService.RenameImagesWithDateTimeUsingFileInfo(_tempDirectory); + var afterFirstRun = Directory.GetFiles(_tempDirectory).Order().ToArray(); + + // Act + ImageRenameService.RenameImagesWithDateTimeUsingFileInfo(_tempDirectory); + + // Assert + Assert.Equal(afterFirstRun, Directory.GetFiles(_tempDirectory).Order().ToArray()); + } + + [Fact] + public void GivenExistingFolder_WhenRenamingFolder_ThenContentMovesWithIt() + { + var oldFolder = Path.Combine(_tempDirectory, "Invoices2025"); + Directory.CreateDirectory(oldFolder); + File.WriteAllText(Path.Combine(oldFolder, "invoice-001.pdf"), "content"); + var newFolder = Path.Combine(_tempDirectory, "Invoices-2025"); + + // Act + FolderRenameService.RenameFolder(oldFolder, newFolder); + + // Assert + Assert.False(Directory.Exists(oldFolder)); + Assert.True(File.Exists(Path.Combine(newFolder, "invoice-001.pdf"))); + } + + [Fact] + public void GivenTextFile_WhenChangingExtension_ThenOnlyExtensionChanges() + { + var filePath = Path.Combine(_tempDirectory, "notes.txt"); + File.WriteAllText(filePath, "content"); + + // Act + var newFilePath = FileRenameService.ChangeExtension(filePath, ".md"); + + // Assert + Assert.Equal(Path.Combine(_tempDirectory, "notes.md"), newFilePath); + Assert.True(File.Exists(newFilePath)); + Assert.False(File.Exists(filePath)); + } + + [Fact] + public void GivenExistingDestination_WhenMovingWithoutOverwrite_ThenIOException() + { + var source = Path.Combine(_tempDirectory, "a.txt"); + var destination = Path.Combine(_tempDirectory, "b.txt"); + File.WriteAllText(source, "new"); + File.WriteAllText(destination, "old"); + + Assert.Throws(() => File.Move(source, destination)); + + File.Move(source, destination, overwrite: true); + Assert.Equal("new", File.ReadAllText(destination)); + } +} diff --git a/files-csharp/RenameFilesWithCSharp/Tests/Tests.csproj b/files-csharp/RenameFilesWithCSharp/Tests/Tests.csproj index ae5d67b16c..d3138e175e 100644 --- a/files-csharp/RenameFilesWithCSharp/Tests/Tests.csproj +++ b/files-csharp/RenameFilesWithCSharp/Tests/Tests.csproj @@ -1,19 +1,19 @@ - net8.0 + net10.0 enable enable false true - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -21,4 +21,4 @@ - \ No newline at end of file +