diff --git a/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs b/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs index 417b60f8..0afc7987 100644 --- a/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs +++ b/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs @@ -305,6 +305,24 @@ private void AddInaccessibleDirectoryAndLog(InventoryPart inventoryPart, Directo AddFileSystemDescription(inventoryPart, subDirectoryDescription); _logger.LogWarning(ex, message, directoryInfo.FullName); } + + private bool IsRootPath(InventoryPart inventoryPart, FileSystemInfo fileSystemInfo) + { + var rootPath = NormalizePath(inventoryPart.RootPath); + var currentPath = NormalizePath(fileSystemInfo.FullName); + var comparison = OSPlatform == OSPlatforms.Windows + ? StringComparison.OrdinalIgnoreCase + : StringComparison.Ordinal; + + return string.Equals(rootPath, currentPath, comparison); + } + + private static string NormalizePath(string path) + { + var fullPath = Path.GetFullPath(path); + + return Path.TrimEndingDirectorySeparator(fullPath); + } private bool ShouldIgnoreHiddenDirectory(DirectoryInfo directoryInfo) { @@ -339,12 +357,14 @@ private bool ShouldIgnoreHiddenDirectory(DirectoryInfo directoryInfo) try { - if (ShouldIgnoreHiddenFile(fileInfo)) + var isRoot = IsRootPath(inventoryPart, fileInfo); + + if (!isRoot && ShouldIgnoreHiddenFile(fileInfo)) { return; } - if (ShouldIgnoreSystemFile(fileInfo)) + if (!isRoot && ShouldIgnoreSystemFile(fileInfo)) { return; } @@ -397,7 +417,7 @@ private void DoAnalyze(InventoryPart inventoryPart, DirectoryInfo directoryInfo, return; } - if (ShouldIgnoreHiddenDirectory(directoryInfo)) + if (!IsRootPath(inventoryPart, directoryInfo) && ShouldIgnoreHiddenDirectory(directoryInfo)) { return; } diff --git a/tests/ByteSync.Client.IntegrationTests/Services/Inventories/TestInventoryBuilder.cs b/tests/ByteSync.Client.IntegrationTests/Services/Inventories/TestInventoryBuilder.cs index a145b0c7..9f729e5a 100644 --- a/tests/ByteSync.Client.IntegrationTests/Services/Inventories/TestInventoryBuilder.cs +++ b/tests/ByteSync.Client.IntegrationTests/Services/Inventories/TestInventoryBuilder.cs @@ -343,6 +343,93 @@ public async Task Test_HiddenFiles_Windows(bool excludeHiddenFiles, int expected inventory.InventoryParts[0].FileDescriptions.Count.Should().Be(2 + expectedHiddenFiles); } + [Test] + [Platform(Exclude = "Linux")] + public async Task Test_HiddenRootDirectory_Windows() + { + InventoryBuilder inventoryBuilder; + Inventory inventory; + + DirectoryInfo sourceA, unzipDir; + FileInfo fileInfo; + + sourceA = new DirectoryInfo(IOUtils.Combine(_testDirectoryService.TestDirectory.FullName, "sourceA")); + sourceA.Create(); + var rootAttributes = File.GetAttributes(sourceA.FullName); + File.SetAttributes(sourceA.FullName, rootAttributes | FileAttributes.Hidden); + + fileInfo = new FileInfo(_testDirectoryService.CreateFileInDirectory(sourceA.FullName, "fileA.txt", "file1Content").FullName); + fileInfo = new FileInfo(_testDirectoryService.CreateFileInDirectory(sourceA.FullName, "fileA_hidden.txt", "file1Content").FullName); + File.SetAttributes(fileInfo.FullName, FileAttributes.Hidden); + + var inventoryAFilePath = IOUtils.Combine(_testDirectoryService.TestDirectory.FullName, $"inventoryA.zip"); + + var sessionSettings = SessionSettings.BuildDefault(); + sessionSettings.ExcludeHiddenFiles = true; + + inventoryBuilder = BuildInventoryBuilder(sessionSettings); + inventoryBuilder.AddInventoryPart(sourceA.FullName); + await inventoryBuilder.BuildBaseInventoryAsync(inventoryAFilePath); + + File.Exists(inventoryAFilePath).Should().BeTrue(); + + unzipDir = new DirectoryInfo(IOUtils.Combine(_testDirectoryService.TestDirectory.FullName, "unzip")); + unzipDir.Create(); + + var fastZip = new FastZip(); + fastZip.ExtractZip(inventoryAFilePath, unzipDir.FullName, null); + + unzipDir.GetFiles("*", SearchOption.AllDirectories).Length.Should().Be(1); + File.Exists(IOUtils.Combine(unzipDir.FullName, $"inventory.json")).Should().BeTrue(); + + inventory = inventoryBuilder.Inventory!; + inventory.InventoryParts.Count.Should().Be(1); + inventory.InventoryParts[0].FileDescriptions.Count.Should().Be(1); + inventory.InventoryParts[0].FileDescriptions[0].Name.Should().Be("fileA.txt"); + } + + [Test] + [Platform(Include = "Linux,MacOsX")] + public async Task Test_HiddenRootDirectory_Linux_Mac() + { + InventoryBuilder inventoryBuilder; + Inventory inventory; + + DirectoryInfo sourceA, unzipDir; + FileInfo fileInfo; + + sourceA = new DirectoryInfo(IOUtils.Combine(_testDirectoryService.TestDirectory.FullName, ".sourceA")); + sourceA.Create(); + + fileInfo = new FileInfo(_testDirectoryService.CreateFileInDirectory(sourceA.FullName, "fileA.txt", "file1Content").FullName); + fileInfo = new FileInfo(_testDirectoryService.CreateFileInDirectory(sourceA.FullName, ".fileA_hidden.txt", "file1Content").FullName); + + var inventoryAFilePath = IOUtils.Combine(_testDirectoryService.TestDirectory.FullName, $"inventoryA.zip"); + + var sessionSettings = SessionSettings.BuildDefault(); + sessionSettings.ExcludeHiddenFiles = true; + + inventoryBuilder = BuildInventoryBuilder(sessionSettings); + inventoryBuilder.AddInventoryPart(sourceA.FullName); + await inventoryBuilder.BuildBaseInventoryAsync(inventoryAFilePath); + + File.Exists(inventoryAFilePath).Should().BeTrue(); + + unzipDir = new DirectoryInfo(IOUtils.Combine(_testDirectoryService.TestDirectory.FullName, "unzip")); + unzipDir.Create(); + + var fastZip = new FastZip(); + fastZip.ExtractZip(inventoryAFilePath, unzipDir.FullName, null); + + unzipDir.GetFiles("*", SearchOption.AllDirectories).Length.Should().Be(1); + File.Exists(IOUtils.Combine(unzipDir.FullName, $"inventory.json")).Should().BeTrue(); + + inventory = inventoryBuilder.Inventory!; + inventory.InventoryParts.Count.Should().Be(1); + inventory.InventoryParts[0].FileDescriptions.Count.Should().Be(1); + inventory.InventoryParts[0].FileDescriptions[0].Name.Should().Be("fileA.txt"); + } + [Test] [Platform(Exclude = "Win")] [TestCase(true, 0)] @@ -786,4 +873,4 @@ private InventoryBuilder BuildInventoryBuilder(SessionSettings? sessionSettings saver, new InventoryIndexer()); } -} \ No newline at end of file +} diff --git a/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs b/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs index a06d75f7..bbf9aee2 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs @@ -75,10 +75,15 @@ private InventoryBuilder CreateBuilder(IFileSystemInspector inspector) } [Test] - public async Task Hidden_File_Is_Ignored() + public async Task Hidden_Root_File_Is_Analyzed() { var insp = new Mock(MockBehavior.Strict); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(true); + insp.Setup(i => i.IsSystem(It.IsAny())).Returns(false); + insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); + insp.Setup(i => i.Exists(It.IsAny())).Returns(true); + insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); + insp.Setup(i => i.IsRecallOnDataAccess(It.IsAny())).Returns(false); var builder = CreateBuilder(insp.Object); var filePath = Path.Combine(TestDirectory.FullName, "a.txt"); @@ -89,15 +94,19 @@ public async Task Hidden_File_Is_Ignored() await builder.BuildBaseInventoryAsync(invPath); var part = builder.Inventory.InventoryParts.Single(); - part.FileDescriptions.Should().BeEmpty(); + part.FileDescriptions.Should().ContainSingle(); } [Test] - public async Task System_File_Is_Ignored() + public async Task System_Root_File_Is_Analyzed() { var insp = new Mock(MockBehavior.Strict); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsSystem(It.IsAny())).Returns(true); + insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); + insp.Setup(i => i.Exists(It.IsAny())).Returns(true); + insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); + insp.Setup(i => i.IsRecallOnDataAccess(It.IsAny())).Returns(false); var builder = CreateBuilder(insp.Object); var filePath = Path.Combine(TestDirectory.FullName, "b.txt"); @@ -108,7 +117,91 @@ public async Task System_File_Is_Ignored() await builder.BuildBaseInventoryAsync(invPath); var part = builder.Inventory.InventoryParts.Single(); - part.FileDescriptions.Should().BeEmpty(); + part.FileDescriptions.Should().ContainSingle(); + } + + [Test] + public async Task Hidden_Root_Directory_Is_Analyzed() + { + var insp = new Mock(MockBehavior.Strict); + insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(true); + insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsSystem(It.IsAny())).Returns(false); + insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); + insp.Setup(i => i.Exists(It.IsAny())).Returns(true); + insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); + insp.Setup(i => i.IsRecallOnDataAccess(It.IsAny())).Returns(false); + var builder = CreateBuilder(insp.Object); + + var root = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, "root_hidden")); + var filePath = Path.Combine(root.FullName, "f.txt"); + await File.WriteAllTextAsync(filePath, "x"); + + builder.AddInventoryPart(root.FullName); + var invPath = Path.Combine(TestDirectory.FullName, "inv_hidden_root.zip"); + await builder.BuildBaseInventoryAsync(invPath); + + var part = builder.Inventory.InventoryParts.Single(); + part.FileDescriptions.Should().ContainSingle(); + part.FileDescriptions[0].RelativePath.Should().Be("/f.txt"); + } + + [Test] + public async Task Hidden_Child_File_Is_Ignored() + { + var insp = new Mock(MockBehavior.Strict); + insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsHidden(It.Is(fi => fi.Name == "hidden.txt"), It.IsAny())) + .Returns(true); + insp.Setup(i => i.IsHidden(It.Is(fi => fi.Name != "hidden.txt"), It.IsAny())) + .Returns(false); + insp.Setup(i => i.IsSystem(It.IsAny())).Returns(false); + insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); + insp.Setup(i => i.Exists(It.IsAny())).Returns(true); + insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); + insp.Setup(i => i.IsRecallOnDataAccess(It.IsAny())).Returns(false); + var builder = CreateBuilder(insp.Object); + + var root = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, "root_hidden_child")); + var visiblePath = Path.Combine(root.FullName, "visible.txt"); + var hiddenPath = Path.Combine(root.FullName, "hidden.txt"); + await File.WriteAllTextAsync(visiblePath, "x"); + await File.WriteAllTextAsync(hiddenPath, "x"); + + builder.AddInventoryPart(root.FullName); + var invPath = Path.Combine(TestDirectory.FullName, "inv_hidden_child.zip"); + await builder.BuildBaseInventoryAsync(invPath); + + var part = builder.Inventory.InventoryParts.Single(); + part.FileDescriptions.Should().ContainSingle(fd => fd.Name == "visible.txt"); + } + + [Test] + public async Task System_Child_File_Is_Ignored() + { + var insp = new Mock(MockBehavior.Strict); + insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); + insp.Setup(i => i.IsSystem(It.Is(fi => fi.Name == "system.txt"))).Returns(true); + insp.Setup(i => i.IsSystem(It.Is(fi => fi.Name != "system.txt"))).Returns(false); + insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); + insp.Setup(i => i.Exists(It.IsAny())).Returns(true); + insp.Setup(i => i.IsOffline(It.IsAny())).Returns(false); + insp.Setup(i => i.IsRecallOnDataAccess(It.IsAny())).Returns(false); + var builder = CreateBuilder(insp.Object); + + var root = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, "root_system_child")); + var visiblePath = Path.Combine(root.FullName, "visible.txt"); + var systemPath = Path.Combine(root.FullName, "system.txt"); + await File.WriteAllTextAsync(visiblePath, "x"); + await File.WriteAllTextAsync(systemPath, "x"); + + builder.AddInventoryPart(root.FullName); + var invPath = Path.Combine(TestDirectory.FullName, "inv_system_child.zip"); + await builder.BuildBaseInventoryAsync(invPath); + + var part = builder.Inventory.InventoryParts.Single(); + part.FileDescriptions.Should().ContainSingle(fd => fd.Name == "visible.txt"); } [Test] @@ -320,4 +413,4 @@ public async Task Directory_ReparsePoint_Is_Skipped() part.FileDescriptions.Should().ContainSingle(); part.FileDescriptions[0].RelativePath.Should().Be("/ok.txt"); } -} \ No newline at end of file +}