Skip to content

Commit c060c4f

Browse files
[refactor] Centralize filesystem entry classification in inventory flow (#276)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 871c111 commit c060c4f

5 files changed

Lines changed: 264 additions & 70 deletions

File tree

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
using System.IO;
2+
using ByteSync.Business.Inventories;
23
using ByteSync.Common.Business.Misc;
34

45
namespace ByteSync.Interfaces.Controls.Inventories;
56

67
public interface IFileSystemInspector
78
{
9+
FileSystemEntryKind ClassifyEntry(FileSystemInfo fsi);
10+
811
bool IsHidden(FileSystemInfo fsi, OSPlatforms os);
12+
913
bool IsSystemAttribute(FileInfo fileInfo);
14+
1015
bool IsNoiseFileName(FileInfo fileInfo, OSPlatforms os);
16+
1117
bool IsReparsePoint(FileSystemInfo fsi);
18+
1219
bool Exists(FileInfo fileInfo);
20+
1321
bool IsOffline(FileInfo fileInfo);
22+
1423
bool IsRecallOnDataAccess(FileInfo fileInfo);
15-
}
24+
}

src/ByteSync.Client/Services/Inventories/FileSystemInspector.cs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.IO;
2+
using ByteSync.Business.Inventories;
23
using ByteSync.Common.Business.Misc;
34
using ByteSync.Interfaces.Controls.Inventories;
45

@@ -7,6 +8,43 @@ namespace ByteSync.Services.Inventories;
78
public class FileSystemInspector : IFileSystemInspector
89
{
910
private const int FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS = 4194304;
11+
private readonly IPosixFileTypeClassifier _posixFileTypeClassifier;
12+
13+
public FileSystemInspector(IPosixFileTypeClassifier? posixFileTypeClassifier = null)
14+
{
15+
_posixFileTypeClassifier = posixFileTypeClassifier ?? new PosixFileTypeClassifier();
16+
}
17+
18+
public FileSystemEntryKind ClassifyEntry(FileSystemInfo fsi)
19+
{
20+
if (HasLinkTarget(fsi) || SafeIsReparsePoint(fsi))
21+
{
22+
return FileSystemEntryKind.Symlink;
23+
}
24+
25+
if (!OperatingSystem.IsWindows())
26+
{
27+
try
28+
{
29+
var posixKind = _posixFileTypeClassifier.ClassifyPosixEntry(fsi.FullName);
30+
if (posixKind != FileSystemEntryKind.Unknown)
31+
{
32+
return posixKind;
33+
}
34+
}
35+
catch (Exception ex)
36+
{
37+
System.Diagnostics.Debug.WriteLine($"Failed to classify POSIX entry '{fsi.FullName}': {ex}");
38+
}
39+
}
40+
41+
return fsi switch
42+
{
43+
DirectoryInfo => FileSystemEntryKind.Directory,
44+
FileInfo => FileSystemEntryKind.RegularFile,
45+
_ => FileSystemEntryKind.Unknown
46+
};
47+
}
1048

1149
public bool IsHidden(FileSystemInfo fsi, OSPlatforms os)
1250
{
@@ -53,4 +91,33 @@ public bool IsRecallOnDataAccess(FileInfo fileInfo)
5391
{
5492
return (((int)fileInfo.Attributes) & FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS) == FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS;
5593
}
56-
}
94+
95+
private static bool HasLinkTarget(FileSystemInfo fsi)
96+
{
97+
try
98+
{
99+
return fsi switch
100+
{
101+
FileInfo fileInfo => fileInfo.LinkTarget != null,
102+
DirectoryInfo directoryInfo => directoryInfo.LinkTarget != null,
103+
_ => false
104+
};
105+
}
106+
catch (Exception)
107+
{
108+
return false;
109+
}
110+
}
111+
112+
private bool SafeIsReparsePoint(FileSystemInfo fsi)
113+
{
114+
try
115+
{
116+
return IsReparsePoint(fsi);
117+
}
118+
catch (Exception)
119+
{
120+
return false;
121+
}
122+
}
123+
}

src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs

Lines changed: 4 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,7 @@ public InventoryBuilder(SessionMember sessionMember, DataNode dataNode, SessionS
4646
InventorySaver = inventorySaver;
4747

4848
InventoryFileAnalyzer = inventoryFileAnalyzer;
49-
FileSystemInspector = fileSystemInspector ?? new FileSystemInspector();
50-
PosixFileTypeClassifier = posixFileTypeClassifier ?? new PosixFileTypeClassifier();
49+
FileSystemInspector = fileSystemInspector ?? new FileSystemInspector(posixFileTypeClassifier);
5150
}
5251

5352
private Inventory InstantiateInventory()
@@ -90,8 +89,6 @@ private Inventory InstantiateInventory()
9089

9190
private IFileSystemInspector FileSystemInspector { get; }
9291

93-
private IPosixFileTypeClassifier PosixFileTypeClassifier { get; }
94-
9592
private bool IgnoreHidden
9693
{
9794
get { return SessionSettings is { ExcludeHiddenFiles: true }; }
@@ -268,12 +265,7 @@ private void ProcessSubDirectories(InventoryPart inventoryPart, DirectoryInfo di
268265

269266
try
270267
{
271-
if (IsReparsePoint(subDirectory))
272-
{
273-
RecordSkippedEntry(inventoryPart, subDirectory, SkipReason.Symlink, FileSystemEntryKind.Symlink);
274-
275-
continue;
276-
}
268+
DoAnalyze(inventoryPart, subDirectory, cancellationToken);
277269
}
278270
catch (UnauthorizedAccessException ex)
279271
{
@@ -296,8 +288,6 @@ private void ProcessSubDirectories(InventoryPart inventoryPart, DirectoryInfo di
296288

297289
continue;
298290
}
299-
300-
DoAnalyze(inventoryPart, subDirectory, cancellationToken);
301291
}
302292
}
303293

@@ -408,7 +398,7 @@ private bool ShouldIgnoreHiddenDirectory(DirectoryInfo directoryInfo)
408398

409399
private bool TryHandleFileSkip(InventoryPart inventoryPart, FileInfo fileInfo, bool isRoot)
410400
{
411-
var entryKind = PosixFileTypeClassifier.ClassifyPosixEntry(fileInfo.FullName);
401+
var entryKind = FileSystemInspector.ClassifyEntry(fileInfo);
412402
if (entryKind == FileSystemEntryKind.Symlink)
413403
{
414404
RecordSkippedEntry(inventoryPart, fileInfo, SkipReason.Symlink, FileSystemEntryKind.Symlink);
@@ -441,13 +431,6 @@ private bool TryHandleFileSkip(InventoryPart inventoryPart, FileInfo fileInfo, b
441431
}
442432
}
443433

444-
if (IsReparsePoint(fileInfo))
445-
{
446-
RecordSkippedEntry(inventoryPart, fileInfo, SkipReason.Symlink, FileSystemEntryKind.Symlink);
447-
448-
return true;
449-
}
450-
451434
if (!FileSystemInspector.Exists(fileInfo))
452435
{
453436
RecordSkippedEntry(inventoryPart, fileInfo, SkipReason.NotFound);
@@ -480,7 +463,7 @@ private void DoAnalyze(InventoryPart inventoryPart, DirectoryInfo directoryInfo,
480463
return;
481464
}
482465

483-
var entryKind = PosixFileTypeClassifier.ClassifyPosixEntry(directoryInfo.FullName);
466+
var entryKind = FileSystemInspector.ClassifyEntry(directoryInfo);
484467
if (entryKind == FileSystemEntryKind.Symlink)
485468
{
486469
RecordSkippedEntry(inventoryPart, directoryInfo, SkipReason.Symlink, FileSystemEntryKind.Symlink);
@@ -576,32 +559,6 @@ private bool ShouldIgnoreHiddenFile(FileInfo fileInfo)
576559
return null;
577560
}
578561

579-
private bool IsReparsePoint(FileInfo fileInfo)
580-
{
581-
if (FileSystemInspector.IsReparsePoint(fileInfo))
582-
{
583-
_logger.LogWarning(
584-
"File {File} is ignored because it has flag 'ReparsePoint'. It might be a symbolic link",
585-
fileInfo.FullName);
586-
587-
return true;
588-
}
589-
590-
return false;
591-
}
592-
593-
private bool IsReparsePoint(DirectoryInfo directoryInfo)
594-
{
595-
if (FileSystemInspector.IsReparsePoint(directoryInfo))
596-
{
597-
_logger.LogWarning("Directory {Directory} is ignored because it has flag 'ReparsePoint'", directoryInfo.FullName);
598-
599-
return true;
600-
}
601-
602-
return false;
603-
}
604-
605562
private bool IsRecallOnDataAccess(FileInfo fileInfo)
606563
{
607564
return FileSystemInspector.IsRecallOnDataAccess(fileInfo);
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using ByteSync.Business.Inventories;
2+
using ByteSync.Interfaces.Controls.Inventories;
3+
using ByteSync.Services.Inventories;
4+
using FluentAssertions;
5+
using Moq;
6+
using NUnit.Framework;
7+
8+
namespace ByteSync.Client.UnitTests.Services.Inventories;
9+
10+
public class FileSystemInspectorTests
11+
{
12+
[Test]
13+
public void ClassifyEntry_ReturnsDirectory_ForDirectoryInfo()
14+
{
15+
var posix = new Mock<IPosixFileTypeClassifier>(MockBehavior.Strict);
16+
posix.Setup(p => p.ClassifyPosixEntry(It.IsAny<string>())).Returns(FileSystemEntryKind.Unknown);
17+
var inspector = new FileSystemInspector(posix.Object);
18+
var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")));
19+
20+
try
21+
{
22+
var result = inspector.ClassifyEntry(tempDirectory);
23+
24+
result.Should().Be(FileSystemEntryKind.Directory);
25+
}
26+
finally
27+
{
28+
Directory.Delete(tempDirectory.FullName, true);
29+
}
30+
}
31+
32+
[Test]
33+
public void ClassifyEntry_ReturnsRegularFile_ForFileInfo()
34+
{
35+
var posix = new Mock<IPosixFileTypeClassifier>(MockBehavior.Strict);
36+
posix.Setup(p => p.ClassifyPosixEntry(It.IsAny<string>())).Returns(FileSystemEntryKind.Unknown);
37+
var inspector = new FileSystemInspector(posix.Object);
38+
var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")));
39+
var tempFilePath = Path.Combine(tempDirectory.FullName, "file.txt");
40+
File.WriteAllText(tempFilePath, "x");
41+
var fileInfo = new FileInfo(tempFilePath);
42+
43+
try
44+
{
45+
var result = inspector.ClassifyEntry(fileInfo);
46+
47+
result.Should().Be(FileSystemEntryKind.RegularFile);
48+
}
49+
finally
50+
{
51+
Directory.Delete(tempDirectory.FullName, true);
52+
}
53+
}
54+
55+
[Test]
56+
public void ClassifyEntry_ReturnsSymlink_WhenLinkTargetExists()
57+
{
58+
var posix = new Mock<IPosixFileTypeClassifier>(MockBehavior.Strict);
59+
posix.Setup(p => p.ClassifyPosixEntry(It.IsAny<string>())).Returns(FileSystemEntryKind.Unknown);
60+
var inspector = new FileSystemInspector(posix.Object);
61+
var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")));
62+
var targetPath = Path.Combine(tempDirectory.FullName, "target.txt");
63+
File.WriteAllText(targetPath, "x");
64+
var linkPath = Path.Combine(tempDirectory.FullName, "link.txt");
65+
66+
try
67+
{
68+
try
69+
{
70+
File.CreateSymbolicLink(linkPath, targetPath);
71+
}
72+
catch (Exception ex)
73+
{
74+
Assert.Ignore($"Symbolic link creation failed: {ex.GetType().Name}");
75+
}
76+
77+
var result = inspector.ClassifyEntry(new FileInfo(linkPath));
78+
79+
result.Should().Be(FileSystemEntryKind.Symlink);
80+
}
81+
finally
82+
{
83+
Directory.Delete(tempDirectory.FullName, true);
84+
}
85+
}
86+
87+
[Test]
88+
[Platform(Include = "Linux,MacOsX")]
89+
public void ClassifyEntry_ReturnsPosixSpecialKind_WhenClassifierProvidesOne()
90+
{
91+
var posix = new Mock<IPosixFileTypeClassifier>(MockBehavior.Strict);
92+
posix.Setup(p => p.ClassifyPosixEntry(It.IsAny<string>())).Returns(FileSystemEntryKind.Fifo);
93+
var inspector = new FileSystemInspector(posix.Object);
94+
var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")));
95+
var tempFilePath = Path.Combine(tempDirectory.FullName, "file.txt");
96+
File.WriteAllText(tempFilePath, "x");
97+
var fileInfo = new FileInfo(tempFilePath);
98+
99+
try
100+
{
101+
var result = inspector.ClassifyEntry(fileInfo);
102+
103+
result.Should().Be(FileSystemEntryKind.Fifo);
104+
}
105+
finally
106+
{
107+
Directory.Delete(tempDirectory.FullName, true);
108+
}
109+
}
110+
111+
[Test]
112+
[Platform(Include = "Linux,MacOsX")]
113+
public void ClassifyEntry_FallsBackToRegularFile_WhenPosixClassifierThrows()
114+
{
115+
var posix = new Mock<IPosixFileTypeClassifier>(MockBehavior.Strict);
116+
posix.Setup(p => p.ClassifyPosixEntry(It.IsAny<string>())).Throws(new InvalidOperationException("boom"));
117+
var inspector = new FileSystemInspector(posix.Object);
118+
var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")));
119+
var tempFilePath = Path.Combine(tempDirectory.FullName, "file.txt");
120+
File.WriteAllText(tempFilePath, "x");
121+
var fileInfo = new FileInfo(tempFilePath);
122+
123+
try
124+
{
125+
var result = inspector.ClassifyEntry(fileInfo);
126+
127+
result.Should().Be(FileSystemEntryKind.RegularFile);
128+
}
129+
finally
130+
{
131+
Directory.Delete(tempDirectory.FullName, true);
132+
}
133+
}
134+
}

0 commit comments

Comments
 (0)