Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/DiffEngineTray.Tests/FilePurgerTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
public class FilePurgerTest
{
/// <summary>
/// The dialog lets the user pick a profile folder or a drive root, and both hold something
/// the scan cannot read. SearchOption.AllDirectories threw on the first one, out of a bare
/// Thread with nothing catching it, which took the whole tray down.
/// </summary>
[Test]
public async Task Find_SkipsInaccessibleDirectories()
{
var root = Directory.CreateDirectory(
Path.Combine(Path.GetTempPath(), $"FilePurgerTest_{Guid.NewGuid()}"));
var denied = root.CreateSubdirectory("denied");
await File.WriteAllTextAsync(Path.Combine(root.FullName, "a.verified.txt"), "content");

var security = denied.GetAccessControl();
var rule = new FileSystemAccessRule(
WindowsIdentity.GetCurrent().User!,
FileSystemRights.ListDirectory,
AccessControlType.Deny);
security.AddAccessRule(rule);
denied.SetAccessControl(security);
try
{
var found = FilePurger.Find(root.FullName);

await Assert.That(found).HasSingleItem();
await Assert.That(Path.GetFileName(found[0])).IsEqualTo("a.verified.txt");
}
finally
{
security.RemoveAccessRule(rule);
denied.SetAccessControl(security);
root.Delete(true);
}
}
[Test]
public async Task DeleteSucceeds_WhenFileNotLocked()
{
Expand Down
2 changes: 2 additions & 0 deletions src/DiffEngineTray.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
global using System.Diagnostics;
global using System.Net;
global using System.Net.Sockets;
global using System.Security.AccessControl;
global using System.Security.Principal;
global using EmptyFiles;

[assembly: ParallelLimiter<SingleThreadedLimit>]
Expand Down
43 changes: 38 additions & 5 deletions src/DiffEngineTray/FilePurger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,28 @@ static class FilePurger
{
public static void Launch()
{
var thread = new Thread(Inner);
var thread = new Thread(Run);
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
}

/// <summary>
/// Nothing above this catches. An unhandled exception on a bare Thread takes the process down,
/// and the process is the tray - so a purge that threw took every pending move, delete and, if
/// this tray owns the queue, every pending inline snapshot with it.
/// </summary>
static void Run()
{
try
{
Inner();
}
catch (Exception exception)
{
ExceptionHandler.Handle("Failed to purge verified files.", exception);
}
}

static void Inner()
{
using var dialog = new FolderBrowserDialog();
Expand All @@ -19,10 +36,7 @@ static void Inner()
return;
}

var verifiedFiles = Directory.GetFiles(path, "*.verified.*", SearchOption.AllDirectories);
var receivedFiles = Directory.GetFiles(path, "*.received.*", SearchOption.AllDirectories);

var files = verifiedFiles.Concat(receivedFiles).ToArray();
var files = Find(path);

if (files.Length == 0)
{
Expand All @@ -36,6 +50,25 @@ static void Inner()
}
}

/// <summary>
/// SearchOption.AllDirectories is the compatibility enumeration: it does not ignore what it
/// cannot read, and it follows reparse points. The dialog lets the user pick a profile folder
/// or a drive root, either of which holds a deny-ACL junction - Application Data,
/// $Recycle.Bin - so the scan reliably threw UnauthorizedAccessException before it had looked
/// at a single file.
/// </summary>
static readonly EnumerationOptions enumeration = new()
{
RecurseSubdirectories = true,
IgnoreInaccessible = true,
AttributesToSkip = FileAttributes.ReparsePoint
};

internal static string[] Find(string path) =>
Directory.GetFiles(path, "*.verified.*", enumeration)
.Concat(Directory.GetFiles(path, "*.received.*", enumeration))
.ToArray();

static bool Confirm(string[] files)
{
var result = AskQuestion(
Expand Down
Loading