Improve file operations, layout memory, and drive status.
Add a sequential file-operations queue with pause, reorder, and optional auto-clear; persist window size and tree width; show free space; and clear leftover indexing status. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -30,6 +30,8 @@ public class BrowseServiceTests
|
||||
var listing = await browse.ListThisPcAsync();
|
||||
var item = Assert.Single(listing.Items);
|
||||
Assert.Equal(1_073_741_824, item.SizeBytes);
|
||||
Assert.Equal(250_000_000, item.FreeSpaceBytes);
|
||||
Assert.Equal(1_000_000_000, item.CapacityBytes);
|
||||
Assert.True(item.IsDirectory);
|
||||
}
|
||||
|
||||
@@ -65,6 +67,7 @@ public class BrowseServiceTests
|
||||
var listing = await browse.ListAsync(@"C:\");
|
||||
var movies = Assert.Single(listing.Items, i => i.Name == "Movies");
|
||||
Assert.Equal(200, movies.SizeBytes);
|
||||
Assert.Equal(250_000_000, movies.FreeSpaceBytes);
|
||||
var loose = Assert.Single(listing.Items, i => i.Name == "loose.txt");
|
||||
Assert.Equal(10, loose.SizeBytes);
|
||||
}
|
||||
@@ -83,7 +86,9 @@ public class BrowseServiceTests
|
||||
Kind = SourceKind.NtfsLocal,
|
||||
RootPath = root,
|
||||
DisplayName = display,
|
||||
VolumeSerial = 1
|
||||
VolumeSerial = 1,
|
||||
CapacityBytes = 1_000_000_000,
|
||||
FreeBytes = 250_000_000
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -128,6 +133,11 @@ file sealed class BrowseVolumes : IVolumeService
|
||||
=> Online.FirstOrDefault(v => path.StartsWith(v.RootPath.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase));
|
||||
public bool IsPathReachable(string path)
|
||||
=> Online.Any(v => path.StartsWith(v.RootPath.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase));
|
||||
public VolumeSpace GetSpace(string path)
|
||||
{
|
||||
var fp = Probe(path);
|
||||
return new VolumeSpace(fp?.CapacityBytes, fp?.FreeBytes);
|
||||
}
|
||||
}
|
||||
|
||||
file sealed class BrowseEnv : IAppEnvironment
|
||||
|
||||
@@ -136,6 +136,39 @@ public class SourceManagerTests
|
||||
Assert.Null(await store.Entries.GetRootAsync(source.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stale_scanning_status_clears_when_no_job_is_running()
|
||||
{
|
||||
var db = Path.Combine(Path.GetTempPath(), "ew-app", Guid.NewGuid().ToString("N"), "index.db");
|
||||
await using var store = new SqliteIndexStore(db, NullLogger<SqliteIndexStore>.Instance);
|
||||
await store.OpenAsync();
|
||||
var volumes = new FakeVolumes
|
||||
{
|
||||
Online =
|
||||
[
|
||||
new VolumeFingerprint
|
||||
{
|
||||
Kind = SourceKind.NtfsLocal,
|
||||
RootPath = @"D:\",
|
||||
DisplayName = "Games (D:)",
|
||||
VolumeSerial = 42,
|
||||
CapacityBytes = 1000
|
||||
}
|
||||
]
|
||||
};
|
||||
var env = new FakeEnv(Path.GetDirectoryName(db)!);
|
||||
var mgr = new SourceManager(store, volumes, env, new SystemClock(), NullLogger<SourceManager>.Instance);
|
||||
await mgr.InitializeAsync();
|
||||
var source = (await store.Sources.GetAllAsync()).Single();
|
||||
await store.Sources.UpdateStatusAsync(source.Id, SourceStatus.Scanning, null);
|
||||
await store.Sources.UpdateIndexedAsync(source.Id, DateTimeOffset.UtcNow, 1);
|
||||
|
||||
await mgr.RefreshOnlineStateAsync();
|
||||
source = (await store.Sources.GetAllAsync()).Single();
|
||||
Assert.Equal(SourceStatus.Online, source.Status);
|
||||
Assert.True(source.IsIndexed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Forget_removes_unc_from_recents()
|
||||
{
|
||||
@@ -166,6 +199,11 @@ file sealed class FakeVolumes : IVolumeService
|
||||
|| v.RootPath.Equals(path, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
public bool IsPathReachable(string path) => Online.Any(v => path.StartsWith(v.RootPath.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase));
|
||||
public VolumeSpace GetSpace(string path)
|
||||
{
|
||||
var fp = Probe(path);
|
||||
return new VolumeSpace(fp?.CapacityBytes, fp?.FreeBytes);
|
||||
}
|
||||
}
|
||||
|
||||
file sealed class FakeEnv : IAppEnvironment
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Explorer.Application;
|
||||
using Explorer.Domain;
|
||||
using Explorer.Domain.Abstractions;
|
||||
|
||||
namespace Explorer.Application.Tests;
|
||||
@@ -13,12 +14,14 @@ public class UiPreferencesStoreTests
|
||||
"theme=Light",
|
||||
"group-network=true",
|
||||
"group-cloud=false",
|
||||
"index-archives=true"
|
||||
"index-archives=true",
|
||||
"auto-clear-queue=true"
|
||||
]);
|
||||
Assert.Equal("Light", prefs.Theme);
|
||||
Assert.True(prefs.GroupNetworkPlaces);
|
||||
Assert.False(prefs.GroupCloudPlaces);
|
||||
Assert.True(prefs.IndexArchiveContents);
|
||||
Assert.True(prefs.AutoClearQueueWhenDone);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -29,6 +32,30 @@ public class UiPreferencesStoreTests
|
||||
Assert.False(prefs.GroupNetworkPlaces);
|
||||
Assert.False(prefs.GroupCloudPlaces);
|
||||
Assert.False(prefs.IndexArchiveContents);
|
||||
Assert.True(prefs.ShowHiddenFiles);
|
||||
Assert.False(prefs.ShowProtectedSystemLocations);
|
||||
Assert.False(prefs.AutoClearQueueWhenDone);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_reads_window_layout()
|
||||
{
|
||||
var prefs = UiPreferencesStore.Parse(
|
||||
[
|
||||
"theme=Dark",
|
||||
"window-width=1440.5",
|
||||
"window-height=900",
|
||||
"window-left=12",
|
||||
"window-top=24",
|
||||
"window-maximized=true",
|
||||
"tree-width=320"
|
||||
]);
|
||||
Assert.Equal(1440.5, prefs.WindowWidth);
|
||||
Assert.Equal(900, prefs.WindowHeight);
|
||||
Assert.Equal(12, prefs.WindowLeft);
|
||||
Assert.Equal(24, prefs.WindowTop);
|
||||
Assert.True(prefs.WindowMaximized);
|
||||
Assert.Equal(320, prefs.TreeWidth);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -38,12 +65,18 @@ public class UiPreferencesStoreTests
|
||||
try
|
||||
{
|
||||
var store = new UiPreferencesStore(new PrefsEnv(dir));
|
||||
store.Save(new UiPreferences("Light", true, false, true));
|
||||
store.Save(new UiPreferences("Light", true, false, true, AutoClearQueueWhenDone: true, WindowWidth: 1100, WindowHeight: 720, TreeWidth: 300));
|
||||
var loaded = store.Load();
|
||||
Assert.Equal("Light", loaded.Theme);
|
||||
Assert.True(loaded.GroupNetworkPlaces);
|
||||
Assert.False(loaded.GroupCloudPlaces);
|
||||
Assert.True(loaded.IndexArchiveContents);
|
||||
Assert.True(loaded.ShowHiddenFiles);
|
||||
Assert.False(loaded.ShowProtectedSystemLocations);
|
||||
Assert.True(loaded.AutoClearQueueWhenDone);
|
||||
Assert.Equal(1100, loaded.WindowWidth);
|
||||
Assert.Equal(720, loaded.WindowHeight);
|
||||
Assert.Equal(300, loaded.TreeWidth);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -52,6 +85,28 @@ public class UiPreferencesStoreTests
|
||||
}
|
||||
}
|
||||
|
||||
public class LocationVisibilityTests
|
||||
{
|
||||
[Fact]
|
||||
public void Hides_protected_when_setting_is_off()
|
||||
{
|
||||
var prefs = UiPreferences.Default;
|
||||
var svi = LocationClassifier.Classify(
|
||||
@"C:\System Volume Information", "System Volume Information",
|
||||
AttributeFlags.Directory | AttributeFlags.Hidden | AttributeFlags.System, true);
|
||||
Assert.False(LocationVisibility.ShouldShow(svi, prefs));
|
||||
Assert.True(LocationVisibility.ShouldShow(svi, prefs with { ShowProtectedSystemLocations = true }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unknown_size_when_access_denied_and_zero()
|
||||
{
|
||||
var info = new LocationInfo(true, true, true, true, false);
|
||||
Assert.Equal(SizeKnowledge.Unknown, LocationVisibility.ResolveSizeKnowledge(info, true, 0, true));
|
||||
Assert.Equal(SizeKnowledge.Partial, LocationVisibility.ResolveSizeKnowledge(info, true, 1000, true));
|
||||
}
|
||||
}
|
||||
|
||||
file sealed class PrefsEnv : IAppEnvironment
|
||||
{
|
||||
public PrefsEnv(string dir)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
|
||||
@@ -38,9 +38,9 @@ file sealed class StubShell : IShellFileOperations
|
||||
public void Open(string path) { }
|
||||
public bool DeleteToRecycleBin(IReadOnlyList<string> paths, out string? error) => Delete(paths, true, out error);
|
||||
public bool Delete(IReadOnlyList<string> paths, bool recycle, out string? error) { error = "n/a"; return false; }
|
||||
public bool CopyFileWithProgress(string source, string destination, bool overwrite, IProgress<long>? progress, CancellationToken cancellationToken, out string? error)
|
||||
public bool CopyFileWithProgress(string source, string destination, bool overwrite, IProgress<long>? progress, CancellationToken cancellationToken, out string? error, Func<bool>? pauseRequested = null)
|
||||
{ error = "n/a"; return false; }
|
||||
public bool MoveFileWithProgress(string source, string destination, bool overwrite, IProgress<long>? progress, CancellationToken cancellationToken, out string? error)
|
||||
public bool MoveFileWithProgress(string source, string destination, bool overwrite, IProgress<long>? progress, CancellationToken cancellationToken, out string? error, Func<bool>? pauseRequested = null)
|
||||
{ error = "n/a"; return false; }
|
||||
public bool CreateShortcut(string targetPath, string shortcutPath, out string? error)
|
||||
{ error = null; return true; }
|
||||
|
||||
273
tests/Explorer.FileOperations.Tests/TransferQueueTests.cs
Normal file
273
tests/Explorer.FileOperations.Tests/TransferQueueTests.cs
Normal file
@@ -0,0 +1,273 @@
|
||||
using Explorer.Domain;
|
||||
using Explorer.Domain.Abstractions;
|
||||
using Explorer.FileOperations;
|
||||
using Explorer.Storage.Sqlite;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Explorer.FileOperations.Tests;
|
||||
|
||||
public class TransferQueueTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Copies_run_one_after_another()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
var firstStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var releaseFirst = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var secondStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
ctx.Shell.OnCopy = src =>
|
||||
{
|
||||
if (src.EndsWith("a.txt", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
firstStarted.TrySetResult();
|
||||
releaseFirst.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
else
|
||||
{
|
||||
secondStarted.TrySetResult();
|
||||
}
|
||||
};
|
||||
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
await ctx.Queue.EnqueueCopyAsync([ctx.File("a.txt"), ctx.File("b.txt")], ctx.Dest);
|
||||
await firstStarted.Task.WaitAsync(TimeSpan.FromSeconds(3));
|
||||
Assert.False(secondStarted.Task.IsCompleted);
|
||||
Assert.Equal(1, ctx.Shell.CopyCount);
|
||||
|
||||
releaseFirst.TrySetResult();
|
||||
await secondStarted.Task.WaitAsync(TimeSpan.FromSeconds(3));
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Count(j => j.Status == TransferStatus.Done) == 2);
|
||||
Assert.Equal(new[] { ctx.File("a.txt"), ctx.File("b.txt") }, ctx.Shell.Copied);
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Pause_keeps_later_jobs_from_starting()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
var firstStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var awaitingPause = true;
|
||||
ctx.Shell.OnCopy = src =>
|
||||
{
|
||||
if (src.EndsWith("a.txt", StringComparison.OrdinalIgnoreCase) && awaitingPause)
|
||||
{
|
||||
firstStarted.TrySetResult();
|
||||
WaitUntil(() => ctx.Shell.PauseRequested?.Invoke() == true, TimeSpan.FromSeconds(3))
|
||||
.GetAwaiter().GetResult();
|
||||
awaitingPause = false;
|
||||
}
|
||||
};
|
||||
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
await ctx.Queue.EnqueueCopyAsync([ctx.File("a.txt"), ctx.File("b.txt")], ctx.Dest);
|
||||
await firstStarted.Task.WaitAsync(TimeSpan.FromSeconds(3));
|
||||
ctx.Queue.PauseAll();
|
||||
await WaitUntil(() => ctx.Queue.Snapshot()[0].Status == TransferStatus.Paused);
|
||||
Assert.DoesNotContain(ctx.Shell.Copied, p => p.EndsWith("b.txt", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
ctx.Queue.ResumeAll();
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().All(j => j.Status == TransferStatus.Done));
|
||||
Assert.Contains(ctx.Shell.Copied, p => p.EndsWith("b.txt", StringComparison.OrdinalIgnoreCase));
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Remove_drops_a_queued_step()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
ctx.Queue.PauseAll();
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
await ctx.Queue.EnqueueCopyAsync([ctx.File("a.txt"), ctx.File("b.txt")], ctx.Dest);
|
||||
var jobs = ctx.Queue.Snapshot();
|
||||
Assert.Equal(2, jobs.Count);
|
||||
ctx.Queue.Cancel(jobs[1].Id);
|
||||
ctx.Queue.ResumeAll();
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Any(j => j.Status == TransferStatus.Done));
|
||||
Assert.Equal(new[] { ctx.File("a.txt") }, ctx.Shell.Copied);
|
||||
Assert.DoesNotContain(ctx.Queue.Snapshot(), j => j.SourcePath.EndsWith("b.txt", StringComparison.OrdinalIgnoreCase)
|
||||
&& j.Status is TransferStatus.Queued or TransferStatus.Running or TransferStatus.Done);
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cancel_stops_the_running_copy()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
var firstStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var releaseFirst = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
ctx.Shell.OnCopy = src =>
|
||||
{
|
||||
if (src.EndsWith("a.txt", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
firstStarted.TrySetResult();
|
||||
releaseFirst.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
};
|
||||
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
await ctx.Queue.EnqueueCopyAsync([ctx.File("a.txt"), ctx.File("b.txt")], ctx.Dest);
|
||||
await firstStarted.Task.WaitAsync(TimeSpan.FromSeconds(3));
|
||||
var running = ctx.Queue.Snapshot().Single(j => j.SourcePath.EndsWith("a.txt", StringComparison.OrdinalIgnoreCase));
|
||||
ctx.Queue.Cancel(running.Id);
|
||||
releaseFirst.TrySetResult();
|
||||
await WaitUntil(() => ctx.Shell.Copied.Contains(ctx.File("b.txt")));
|
||||
Assert.DoesNotContain(ctx.Shell.Copied, p => p.EndsWith("a.txt", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(ctx.Queue.Snapshot(), j => j.Id == running.Id && j.Status == TransferStatus.Done);
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Pause_queued_job_skips_it()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
ctx.Queue.PauseAll();
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
await ctx.Queue.EnqueueCopyAsync([ctx.File("a.txt"), ctx.File("b.txt")], ctx.Dest);
|
||||
var jobs = ctx.Queue.Snapshot();
|
||||
ctx.Queue.Pause(jobs[0].Id);
|
||||
ctx.Queue.ResumeAll();
|
||||
await WaitUntil(() => ctx.Shell.Copied.Contains(ctx.File("b.txt")));
|
||||
Assert.Equal(new[] { ctx.File("b.txt") }, ctx.Shell.Copied);
|
||||
Assert.Equal(TransferStatus.Paused, ctx.Queue.Snapshot().First(j => j.Id == jobs[0].Id).Status);
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reorder_changes_which_job_runs_first()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
ctx.Queue.PauseAll();
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
await ctx.Queue.EnqueueCopyAsync([ctx.File("a.txt"), ctx.File("b.txt")], ctx.Dest);
|
||||
var jobs = ctx.Queue.Snapshot();
|
||||
Assert.True(ctx.Queue.MoveUp(jobs[1].Id));
|
||||
ctx.Queue.ResumeAll();
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Count(j => j.Status == TransferStatus.Done) == 2);
|
||||
Assert.Equal(new[] { ctx.File("b.txt"), ctx.File("a.txt") }, ctx.Shell.Copied);
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
private static async Task WaitUntil(Func<bool> condition, TimeSpan? timeout = null)
|
||||
{
|
||||
var limit = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(4));
|
||||
while (!condition())
|
||||
{
|
||||
if (DateTime.UtcNow > limit)
|
||||
{
|
||||
throw new TimeoutException("Condition was not met.");
|
||||
}
|
||||
|
||||
await Task.Delay(20);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Harness : IAsyncDisposable
|
||||
{
|
||||
public required TransferQueue Queue { get; init; }
|
||||
public required GateShell Shell { get; init; }
|
||||
public required SqliteIndexStore Store { get; init; }
|
||||
public required string Dest { get; init; }
|
||||
public required string Root { get; init; }
|
||||
|
||||
public string File(string name) => Path.Combine(Root, name);
|
||||
|
||||
public static async Task<Harness> CreateAsync()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "ew-xfer", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(root);
|
||||
var dest = Path.Combine(root, "dest");
|
||||
Directory.CreateDirectory(dest);
|
||||
System.IO.File.WriteAllText(Path.Combine(root, "a.txt"), "a");
|
||||
System.IO.File.WriteAllText(Path.Combine(root, "b.txt"), "b");
|
||||
var db = Path.Combine(root, "index.db");
|
||||
var store = new SqliteIndexStore(db, NullLogger<SqliteIndexStore>.Instance);
|
||||
await store.OpenAsync();
|
||||
var shell = new GateShell();
|
||||
var queue = new TransferQueue(shell, new DiskEnum(), store, NullLogger<TransferQueue>.Instance);
|
||||
return new Harness
|
||||
{
|
||||
Queue = queue,
|
||||
Shell = shell,
|
||||
Store = store,
|
||||
Dest = dest,
|
||||
Root = root
|
||||
};
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await Store.DisposeAsync();
|
||||
try { Directory.Delete(Root, true); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DiskEnum : IFileSystemEnumerator
|
||||
{
|
||||
public IEnumerable<FileSystemItem> EnumerateChildren(string directoryPath) => EnumerateChildrenSafe(directoryPath, out _);
|
||||
|
||||
public IReadOnlyList<FileSystemItem> EnumerateChildrenSafe(string directoryPath, out string? error)
|
||||
{
|
||||
error = null;
|
||||
return Directory.Exists(directoryPath)
|
||||
? Directory.GetFileSystemEntries(directoryPath).Select(GetRequired).ToList()
|
||||
: [];
|
||||
}
|
||||
|
||||
public FileSystemItem? GetItem(string path)
|
||||
=> System.IO.File.Exists(path) || Directory.Exists(path) ? GetRequired(path) : null;
|
||||
|
||||
private static FileSystemItem GetRequired(string path)
|
||||
{
|
||||
var isDir = Directory.Exists(path);
|
||||
return new FileSystemItem
|
||||
{
|
||||
FullPath = path,
|
||||
Name = Path.GetFileName(path),
|
||||
IsDirectory = isDir,
|
||||
SizeBytes = isDir ? 0 : new FileInfo(path).Length
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class GateShell : IShellFileOperations
|
||||
{
|
||||
public int CopyCount { get; private set; }
|
||||
public List<string> Copied { get; } = [];
|
||||
public Action<string>? OnCopy { get; set; }
|
||||
public Func<bool>? PauseRequested { get; private set; }
|
||||
|
||||
public void Open(string path) { }
|
||||
public bool DeleteToRecycleBin(IReadOnlyList<string> paths, out string? error) => Delete(paths, true, out error);
|
||||
public bool Delete(IReadOnlyList<string> paths, bool recycle, out string? error) { error = null; return true; }
|
||||
public bool CreateShortcut(string targetPath, string shortcutPath, out string? error) { error = null; return true; }
|
||||
|
||||
public bool CopyFileWithProgress(string source, string destination, bool overwrite, IProgress<long>? progress, CancellationToken cancellationToken, out string? error, Func<bool>? pauseRequested = null)
|
||||
{
|
||||
CopyCount++;
|
||||
PauseRequested = pauseRequested;
|
||||
progress?.Report(1);
|
||||
OnCopy?.Invoke(source);
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
error = "Cancelled";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pauseRequested?.Invoke() == true)
|
||||
{
|
||||
error = "Paused";
|
||||
return false;
|
||||
}
|
||||
|
||||
Copied.Add(source);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
|
||||
System.IO.File.Copy(source, destination, overwrite);
|
||||
progress?.Report(new FileInfo(source).Length);
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool MoveFileWithProgress(string source, string destination, bool overwrite, IProgress<long>? progress, CancellationToken cancellationToken, out string? error, Func<bool>? pauseRequested = null)
|
||||
=> CopyFileWithProgress(source, destination, overwrite, progress, cancellationToken, out error, pauseRequested);
|
||||
}
|
||||
Reference in New Issue
Block a user