Skip to content

Commit 94c7593

Browse files
[feature] Extend noise entry detection for issue 265 (#278)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 3ddd1ac commit 94c7593

File tree

9 files changed

+159
-8
lines changed

9 files changed

+159
-8
lines changed

docs/specs/SPEC-noise-file-detection.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@ The runtime source of truth is the embedded JSON resource:
1515
| `thumbs.db` | Windows | Thumbnail cache |
1616
| `ehthumbs.db` | Windows | Media Center thumbnail cache |
1717
| `ehthumbs_vista.db` | Windows | Vista Media Center thumbnail cache |
18+
| `$RECYCLE.BIN` | Windows | Recycle bin folder marker |
1819
| `.desktop.ini` | Windows/Linux legacy compatibility | Legacy hidden variant |
1920
| `.thumbs.db` | Windows/Linux legacy compatibility | Legacy hidden variant |
2021
| `.DS_Store` | macOS | Finder metadata |
2122
| `.AppleDouble` | macOS | Resource fork metadata |
23+
| `.AppleDB` | macOS | Apple database file |
24+
| `.AppleDesktop` | macOS | Apple desktop database file |
2225
| `.LSOverride` | macOS | Launch Services overrides |
2326
| `.Spotlight-V100` | macOS | Spotlight indexing data |
2427
| `.Trashes` | macOS | Trash metadata or folder marker |
@@ -30,4 +33,4 @@ The runtime source of truth is the embedded JSON resource:
3033
## Matching behavior
3134

3235
- On Linux, matching is case-sensitive.
33-
- On non-Linux platforms, matching is case-insensitive.
36+
- On non-Linux platforms (including macOS), matching is case-insensitive; macOS remains intentionally case-insensitive for consistency with current product behavior and to reflect the deviation from the original issue wording.

src/ByteSync.Client/Interfaces/Controls/Inventories/IFileSystemInspector.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ public interface IFileSystemInspector
1414

1515
bool IsNoiseFileName(FileInfo fileInfo, OSPlatforms os);
1616

17+
bool IsNoiseDirectoryName(DirectoryInfo directoryInfo, OSPlatforms os);
18+
1719
bool IsReparsePoint(FileSystemInfo fsi);
1820

1921
bool Exists(FileInfo fileInfo);
2022

2123
bool IsOffline(FileInfo fileInfo);
2224

2325
bool IsRecallOnDataAccess(FileInfo fileInfo);
24-
}
26+
}

src/ByteSync.Client/Models/Inventories/SkipReason.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ public enum SkipReason
55
Unknown = 0,
66
Hidden = 1,
77
SystemAttribute = 2,
8-
NoiseFile = 3,
8+
NoiseEntry = 3,
99
Symlink = 4,
1010
SpecialPosixFile = 5,
1111
Offline = 6,

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ public bool IsNoiseFileName(FileInfo fileInfo, OSPlatforms os)
6666
return NoiseFileDetector.IsNoiseFileName(fileInfo.Name, os);
6767
}
6868

69+
public bool IsNoiseDirectoryName(DirectoryInfo directoryInfo, OSPlatforms os)
70+
{
71+
return NoiseFileDetector.IsNoiseFileName(directoryInfo.Name, os);
72+
}
73+
6974
public bool IsReparsePoint(FileSystemInfo fsi)
7075
{
7176
return (fsi.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;

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

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -480,13 +480,22 @@ private void DoAnalyze(InventoryPart inventoryPart, DirectoryInfo directoryInfo,
480480
return;
481481
}
482482

483-
if (!IsRootPath(inventoryPart, directoryInfo) && ShouldIgnoreHiddenDirectory(directoryInfo))
483+
var isRoot = IsRootPath(inventoryPart, directoryInfo);
484+
485+
if (!isRoot && ShouldIgnoreHiddenDirectory(directoryInfo))
484486
{
485487
RecordSkippedEntry(inventoryPart, directoryInfo, SkipReason.Hidden, FileSystemEntryKind.Directory);
486488

487489
return;
488490
}
489491

492+
if (!isRoot && ShouldIgnoreNoiseDirectory(directoryInfo))
493+
{
494+
RecordSkippedEntry(inventoryPart, directoryInfo, SkipReason.NoiseEntry, FileSystemEntryKind.Directory);
495+
496+
return;
497+
}
498+
490499
var directoryDescription = IdentityBuilder.BuildDirectoryDescription(inventoryPart, directoryInfo);
491500

492501
AddFileSystemDescription(inventoryPart, directoryDescription);
@@ -535,6 +544,23 @@ private bool ShouldIgnoreHiddenFile(FileInfo fileInfo)
535544
return false;
536545
}
537546

547+
private bool ShouldIgnoreNoiseDirectory(DirectoryInfo directoryInfo)
548+
{
549+
if (!IgnoreSystem)
550+
{
551+
return false;
552+
}
553+
554+
if (FileSystemInspector.IsNoiseDirectoryName(directoryInfo, OSPlatform))
555+
{
556+
_logger.LogInformation("Directory {Directory} is ignored because considered as noise", directoryInfo.FullName);
557+
558+
return true;
559+
}
560+
561+
return false;
562+
}
563+
538564
private SkipReason? GetSystemSkipReason(FileInfo fileInfo)
539565
{
540566
if (!IgnoreSystem)
@@ -546,7 +572,7 @@ private bool ShouldIgnoreHiddenFile(FileInfo fileInfo)
546572
{
547573
_logger.LogInformation("File {File} is ignored because considered as noise", fileInfo.FullName);
548574

549-
return SkipReason.NoiseFile;
575+
return SkipReason.NoiseEntry;
550576
}
551577

552578
if (FileSystemInspector.IsSystemAttribute(fileInfo))
@@ -705,4 +731,4 @@ private void AddFileSystemDescription(InventoryPart inventoryPart, FileSystemDes
705731
}
706732
}
707733
}
708-
}
734+
}

src/ByteSync.Client/Services/Inventories/noise-files.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
"thumbs.db",
44
"ehthumbs.db",
55
"ehthumbs_vista.db",
6+
"$RECYCLE.BIN",
67
".desktop.ini",
78
".thumbs.db",
89
".DS_Store",
910
".AppleDouble",
11+
".AppleDB",
12+
".AppleDesktop",
1013
".LSOverride",
1114
".Spotlight-V100",
1215
".Trashes",

tests/ByteSync.Client.UnitTests/Services/Inventories/FileSystemInspectorTests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,44 @@ public void ClassifyEntry_FallsBackToRegularFile_WhenPosixClassifierThrows()
133133
}
134134
}
135135

136+
[Test]
137+
public void IsNoiseDirectoryName_ShouldReturnTrue_ForKnownNoiseDirectory()
138+
{
139+
var inspector = new FileSystemInspector();
140+
var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")));
141+
var noiseDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "$RECYCLE.BIN"));
142+
143+
try
144+
{
145+
var result = inspector.IsNoiseDirectoryName(noiseDirectory, OSPlatforms.Windows);
146+
147+
result.Should().BeTrue();
148+
}
149+
finally
150+
{
151+
Directory.Delete(tempDirectory.FullName, true);
152+
}
153+
}
154+
155+
[Test]
156+
public void IsNoiseDirectoryName_ShouldReturnFalse_ForUnknownDirectory()
157+
{
158+
var inspector = new FileSystemInspector();
159+
var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")));
160+
var regularDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "regular"));
161+
162+
try
163+
{
164+
var result = inspector.IsNoiseDirectoryName(regularDirectory, OSPlatforms.Windows);
165+
166+
result.Should().BeFalse();
167+
}
168+
finally
169+
{
170+
Directory.Delete(tempDirectory.FullName, true);
171+
}
172+
}
173+
136174
[Test]
137175
public void IsNoiseFileName_ShouldReturnTrue_ForKnownNoiseFile()
138176
{

tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ private static void SetupDefaultClassification(Mock<IFileSystemInspector> inspec
9494
FileInfo => FileSystemEntryKind.RegularFile,
9595
_ => FileSystemEntryKind.Unknown
9696
});
97+
inspector
98+
.Setup(i => i.IsNoiseDirectoryName(It.IsAny<DirectoryInfo>(), It.IsAny<OSPlatforms>()))
99+
.Returns(false);
97100
}
98101

99102
[Test]
@@ -265,7 +268,72 @@ public async Task Noise_Child_File_Is_Recorded()
265268
await builder.BuildBaseInventoryAsync(invPath);
266269

267270
processData.SkippedEntries.Should()
268-
.ContainSingle(e => e.Name == "thumbs.db" && e.Reason == SkipReason.NoiseFile);
271+
.ContainSingle(e => e.Name == "thumbs.db" && e.Reason == SkipReason.NoiseEntry);
272+
}
273+
274+
[Test]
275+
public async Task Noise_Child_Directory_Is_Recorded_And_Not_Traversed()
276+
{
277+
var insp = new Mock<IFileSystemInspector>(MockBehavior.Strict);
278+
SetupDefaultClassification(insp);
279+
insp.Setup(i => i.IsHidden(It.IsAny<DirectoryInfo>(), It.IsAny<OSPlatforms>())).Returns(false);
280+
insp.Setup(i => i.IsHidden(It.IsAny<FileInfo>(), It.IsAny<OSPlatforms>())).Returns(false);
281+
insp.Setup(i => i.IsNoiseDirectoryName(It.Is<DirectoryInfo>(di => di.Name == "$RECYCLE.BIN"), It.IsAny<OSPlatforms>()))
282+
.Returns(true);
283+
insp.Setup(i => i.IsNoiseFileName(It.IsAny<FileInfo>(), It.IsAny<OSPlatforms>())).Returns(false);
284+
insp.Setup(i => i.IsSystemAttribute(It.IsAny<FileInfo>())).Returns(false);
285+
insp.Setup(i => i.IsReparsePoint(It.IsAny<FileSystemInfo>())).Returns(false);
286+
insp.Setup(i => i.Exists(It.IsAny<FileInfo>())).Returns(true);
287+
insp.Setup(i => i.IsOffline(It.IsAny<FileInfo>())).Returns(false);
288+
insp.Setup(i => i.IsRecallOnDataAccess(It.IsAny<FileInfo>())).Returns(false);
289+
var (builder, processData) = CreateBuilderWithData(insp.Object);
290+
291+
var root = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, "root_noise_dir"));
292+
var visiblePath = Path.Combine(root.FullName, "visible.txt");
293+
await File.WriteAllTextAsync(visiblePath, "x");
294+
295+
var noiseDirectory = Directory.CreateDirectory(Path.Combine(root.FullName, "$RECYCLE.BIN"));
296+
var nestedNoiseFile = Path.Combine(noiseDirectory.FullName, "nested.txt");
297+
await File.WriteAllTextAsync(nestedNoiseFile, "x");
298+
299+
builder.AddInventoryPart(root.FullName);
300+
var invPath = Path.Combine(TestDirectory.FullName, "inv_noise_dir.zip");
301+
await builder.BuildBaseInventoryAsync(invPath);
302+
303+
var part = builder.Inventory.InventoryParts.Single();
304+
part.FileDescriptions.Should().ContainSingle(fd => fd.Name == "visible.txt");
305+
part.FileDescriptions.Should().NotContain(fd => fd.Name == "nested.txt");
306+
307+
processData.SkippedEntries.Should()
308+
.ContainSingle(e => e.Name == "$RECYCLE.BIN" && e.Reason == SkipReason.NoiseEntry);
309+
}
310+
311+
[Test]
312+
public async Task Noise_Root_Directory_Is_Analyzed()
313+
{
314+
var insp = new Mock<IFileSystemInspector>(MockBehavior.Strict);
315+
SetupDefaultClassification(insp);
316+
insp.Setup(i => i.IsHidden(It.IsAny<DirectoryInfo>(), It.IsAny<OSPlatforms>())).Returns(false);
317+
insp.Setup(i => i.IsHidden(It.IsAny<FileInfo>(), It.IsAny<OSPlatforms>())).Returns(false);
318+
insp.Setup(i => i.IsNoiseFileName(It.IsAny<FileInfo>(), It.IsAny<OSPlatforms>())).Returns(false);
319+
insp.Setup(i => i.IsSystemAttribute(It.IsAny<FileInfo>())).Returns(false);
320+
insp.Setup(i => i.IsReparsePoint(It.IsAny<FileSystemInfo>())).Returns(false);
321+
insp.Setup(i => i.Exists(It.IsAny<FileInfo>())).Returns(true);
322+
insp.Setup(i => i.IsOffline(It.IsAny<FileInfo>())).Returns(false);
323+
insp.Setup(i => i.IsRecallOnDataAccess(It.IsAny<FileInfo>())).Returns(false);
324+
var (builder, processData) = CreateBuilderWithData(insp.Object);
325+
326+
var noiseRoot = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, "$RECYCLE.BIN"));
327+
var filePath = Path.Combine(noiseRoot.FullName, "inside.txt");
328+
await File.WriteAllTextAsync(filePath, "x");
329+
330+
builder.AddInventoryPart(noiseRoot.FullName);
331+
var invPath = Path.Combine(TestDirectory.FullName, "inv_noise_root_dir.zip");
332+
await builder.BuildBaseInventoryAsync(invPath);
333+
334+
var part = builder.Inventory.InventoryParts.Single();
335+
part.FileDescriptions.Should().ContainSingle(fd => fd.Name == "inside.txt");
336+
processData.SkippedEntries.Should().NotContain(e => e.Name == "$RECYCLE.BIN");
269337
}
270338

271339
[Test]

tests/ByteSync.Client.UnitTests/Services/Inventories/NoiseFileDetectorTests.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ public void IsNoiseFileName_ShouldReturnTrue_ForKnownNoiseFiles_OnLinux(string f
3030
[TestCase("THUMBS.DB")]
3131
[TestCase("EHTHUMBS.DB")]
3232
[TestCase("EHTHUMBS_VISTA.DB")]
33+
[TestCase("$recycle.bin")]
3334
[TestCase(".ds_store")]
3435
[TestCase(".appledouble")]
36+
[TestCase(".appledb")]
37+
[TestCase(".appledesktop")]
3538
[TestCase(".lsoverride")]
3639
[TestCase(".spotlight-v100")]
3740
[TestCase(".trashes")]
@@ -52,8 +55,11 @@ public void IsNoiseFileName_ShouldBeCaseInsensitive_OnNonLinuxPlatforms(string f
5255
[TestCase("THUMBS.DB")]
5356
[TestCase("EHTHUMBS.DB")]
5457
[TestCase("EHTHUMBS_VISTA.DB")]
58+
[TestCase("$recycle.bin")]
5559
[TestCase(".ds_store")]
5660
[TestCase(".appledouble")]
61+
[TestCase(".appledb")]
62+
[TestCase(".appledesktop")]
5763
[TestCase(".lsoverride")]
5864
[TestCase(".spotlight-v100")]
5965
[TestCase(".trashes")]
@@ -108,4 +114,4 @@ private static string[] LoadNoiseFileNamesFromEmbeddedResource()
108114

109115
return data!;
110116
}
111-
}
117+
}

0 commit comments

Comments
 (0)