Add Git overlay, operation tools, and virtualized preview so large folders stay responsive.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using Explorer.Application;
|
||||
using Explorer.Domain;
|
||||
using Explorer.Domain.Abstractions;
|
||||
using Explorer.FileOperations;
|
||||
@@ -147,10 +148,273 @@ public class TransferQueueTests
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Restores_paused_and_queued_jobs_after_restart()
|
||||
{
|
||||
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);
|
||||
await WaitUntil(async () =>
|
||||
{
|
||||
var stored = await ctx.Store.Transfers.GetIncompleteAsync();
|
||||
return stored.Any(j => j.Id == jobs[0].Id && j.Status == TransferStatus.Paused);
|
||||
});
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
|
||||
var restored = ctx.CreateQueue();
|
||||
restored.PauseAll();
|
||||
await restored.StartAsync(CancellationToken.None);
|
||||
await WaitUntil(() => restored.Snapshot().Count == 2);
|
||||
Assert.Equal(TransferStatus.Paused, restored.Snapshot().First(j => j.Id == jobs[0].Id).Status);
|
||||
Assert.Equal(TransferStatus.Queued, restored.Snapshot().First(j => j.Id == jobs[1].Id).Status);
|
||||
restored.Resume(jobs[0].Id);
|
||||
await WaitUntil(() => restored.Snapshot().Count(j => j.Status == TransferStatus.Done) == 2);
|
||||
await restored.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Restores_running_jobs_as_paused()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
var id = await ctx.Store.Transfers.InsertAsync(new TransferJob
|
||||
{
|
||||
Op = TransferOp.Copy,
|
||||
SourcePath = ctx.File("a.txt"),
|
||||
DestinationPath = Path.Combine(ctx.Dest, "a.txt"),
|
||||
Status = TransferStatus.Running,
|
||||
CreatedUtc = DateTimeOffset.UtcNow,
|
||||
StartedUtc = DateTimeOffset.UtcNow
|
||||
});
|
||||
var restored = ctx.CreateQueue();
|
||||
await restored.StartAsync(CancellationToken.None);
|
||||
await WaitUntil(() => restored.Snapshot().Any(j => j.Id == id && j.Status == TransferStatus.Paused));
|
||||
Assert.DoesNotContain(restored.Snapshot(), j => j.Status == TransferStatus.Running);
|
||||
await restored.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Waiting_jobs_do_not_run_until_destination_is_reachable()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
ctx.Volumes.Reachable = false;
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
await ctx.Queue.EnqueueCopyAsync([ctx.File("a.txt")], ctx.Dest);
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Any(j => j.Status == TransferStatus.Waiting));
|
||||
await Task.Delay(80);
|
||||
Assert.Equal(0, ctx.Shell.CopyCount);
|
||||
Assert.Equal(FileOperationErrors.DestinationUnavailable, ctx.Queue.Snapshot()[0].WaitReason);
|
||||
|
||||
ctx.Volumes.Reachable = true;
|
||||
ctx.Queue.NotifyAvailability();
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Single().Status == TransferStatus.Done);
|
||||
Assert.Equal(new[] { ctx.File("a.txt") }, ctx.Shell.Copied);
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Retry_reruns_a_failed_job()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
ctx.Shell.FailRemaining = 1;
|
||||
ctx.Shell.FailError = "disk full";
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
await ctx.Queue.EnqueueCopyAsync([ctx.File("a.txt")], ctx.Dest);
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Single().Status == TransferStatus.Failed);
|
||||
Assert.Equal("disk full", ctx.Queue.Snapshot()[0].Error);
|
||||
|
||||
ctx.Queue.Retry(ctx.Queue.Snapshot()[0].Id);
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Single().Status == TransferStatus.Done);
|
||||
Assert.Equal(1, ctx.Queue.Snapshot()[0].RetryCount);
|
||||
Assert.Contains(ctx.File("a.txt"), ctx.Shell.Copied);
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task File_lock_fails_and_can_be_retried()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
ctx.Shell.FailRemaining = 1;
|
||||
ctx.Shell.FailError = "The process cannot access the file because it is being used by another process.";
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
await ctx.Queue.EnqueueCopyAsync([ctx.File("a.txt")], ctx.Dest);
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Single().Status == TransferStatus.Failed);
|
||||
Assert.Equal(FileOperationErrors.FileInUse, ctx.Queue.Snapshot()[0].Error);
|
||||
|
||||
ctx.Queue.Retry(ctx.Queue.Snapshot()[0].Id);
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Single().Status == TransferStatus.Done);
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Queued_rename_collision_stays_failed()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
await ctx.Queue.EnqueueRenameAsync(ctx.File("a.txt"), "b.txt");
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Single().Status == TransferStatus.Failed);
|
||||
Assert.Equal(FileOperationErrors.NameExists, ctx.Queue.Snapshot()[0].Error);
|
||||
Assert.True(System.IO.File.Exists(ctx.File("a.txt")));
|
||||
Assert.True(System.IO.File.Exists(ctx.File("b.txt")));
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Queued_rename_moves_the_file()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
await ctx.Queue.EnqueueRenameAsync(ctx.File("a.txt"), "renamed.txt");
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Single().Status == TransferStatus.Done);
|
||||
Assert.False(System.IO.File.Exists(ctx.File("a.txt")));
|
||||
Assert.True(System.IO.File.Exists(ctx.File("renamed.txt")));
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClearFinished_keeps_history_and_does_not_restore_dismissed_jobs()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
await ctx.Queue.EnqueueCopyAsync([ctx.File("a.txt")], ctx.Dest);
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Single().Status == TransferStatus.Done);
|
||||
ctx.Queue.ClearFinished();
|
||||
Assert.Empty(ctx.Queue.Snapshot());
|
||||
await WaitUntil(async () =>
|
||||
{
|
||||
var history = await ctx.Store.Transfers.GetHistoryAsync(10);
|
||||
return history.Any(j => j.Status == TransferStatus.Done && j.Dismissed);
|
||||
});
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
|
||||
var restored = ctx.CreateQueue();
|
||||
await restored.StartAsync(CancellationToken.None);
|
||||
await Task.Delay(50);
|
||||
Assert.Empty(restored.Snapshot());
|
||||
await restored.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Batch_rename_enqueues_each_item()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
var ops = new FileOperationService(ctx.Queue, ctx.Shell, new DiskEnum());
|
||||
ctx.Queue.PauseAll();
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
await ops.EnqueueRenameAsync([(ctx.File("a.txt"), "a2.txt"), (ctx.File("b.txt"), "b2.txt")]);
|
||||
Assert.Equal(2, ctx.Queue.Snapshot().Count);
|
||||
Assert.All(ctx.Queue.Snapshot(), j => Assert.Equal(TransferOp.Rename, j.Op));
|
||||
ctx.Queue.ResumeAll();
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Count(j => j.Status == TransferStatus.Done) == 2);
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Batch_rename_then_undo_restores_names()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
var ops = new FileOperationService(ctx.Queue, ctx.Shell, new DiskEnum());
|
||||
var batches = new RenameBatchService(new RenamePlanner(), ctx.Store, ops);
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
var subjects = new[]
|
||||
{
|
||||
new RenameSubject(ctx.File("a.txt"), "a.txt", false),
|
||||
new RenameSubject(ctx.File("b.txt"), "b.txt", false)
|
||||
};
|
||||
var plan = batches.Preview(subjects, new RenameRuleSet { Prefix = "x_" });
|
||||
Assert.True(plan.CanEnqueue);
|
||||
await batches.EnqueueAsync(plan);
|
||||
await WaitUntil(() => File.Exists(ctx.File("x_a.txt")) && File.Exists(ctx.File("x_b.txt")));
|
||||
Assert.False(File.Exists(ctx.File("a.txt")));
|
||||
var undo = await batches.UndoLastAsync();
|
||||
Assert.True(undo.CanEnqueue);
|
||||
await WaitUntil(() => File.Exists(ctx.File("a.txt")) && File.Exists(ctx.File("b.txt")));
|
||||
Assert.False(File.Exists(ctx.File("x_a.txt")));
|
||||
Assert.Null(await batches.GetUndoableAsync());
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Extract_fails_when_7zip_is_missing()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
var ops = new FileOperationService(ctx.Queue, ctx.Shell, new DiskEnum());
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
await ops.ExtractAsync(ctx.File("a.txt"), Path.Combine(ctx.Dest, "out"));
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Any(j =>
|
||||
j.Op == TransferOp.Extract && j.Status == TransferStatus.Failed));
|
||||
var job = ctx.Queue.Snapshot().Single(j => j.Op == TransferOp.Extract);
|
||||
Assert.Contains("7-Zip", job.Error, StringComparison.OrdinalIgnoreCase);
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Fake_extract_writes_files_and_can_be_cancelled()
|
||||
{
|
||||
var fake = new FakeArchiveExecutor { ExtractDelay = TimeSpan.FromSeconds(20) };
|
||||
await using var ctx = await Harness.CreateAsync(fake);
|
||||
var ops = new FileOperationService(ctx.Queue, ctx.Shell, new DiskEnum());
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
var dest = Path.Combine(ctx.Dest, "pack");
|
||||
await ops.ExtractAsync(ctx.File("a.txt"), dest);
|
||||
await fake.Started.Task.WaitAsync(TimeSpan.FromSeconds(4));
|
||||
var job = Assert.Single(ctx.Queue.Snapshot(), j => j.Op == TransferOp.Extract);
|
||||
ctx.Queue.Cancel(job.Id);
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Any(j =>
|
||||
j.Op == TransferOp.Extract && j.Status == TransferStatus.Cancelled));
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Fake_extract_completes_and_writes_output()
|
||||
{
|
||||
var fake = new FakeArchiveExecutor();
|
||||
await using var ctx = await Harness.CreateAsync(fake);
|
||||
var ops = new FileOperationService(ctx.Queue, ctx.Shell, new DiskEnum());
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
var dest = Path.Combine(ctx.Dest, "pack");
|
||||
await ops.ExtractAsync(ctx.File("a.txt"), dest);
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Any(j =>
|
||||
j.Op == TransferOp.Extract && j.Status == TransferStatus.Done));
|
||||
Assert.True(File.Exists(Path.Combine(dest, "out.txt")));
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Extract_refuses_online_only_cloud_archives()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync(new FakeArchiveExecutor(), new AlwaysHydrate());
|
||||
var ops = new FileOperationService(ctx.Queue, ctx.Shell, new DiskEnum());
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
await ops.ExtractAsync(ctx.File("a.txt"), Path.Combine(ctx.Dest, "out"));
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Any(j =>
|
||||
j.Op == TransferOp.Extract && j.Status == TransferStatus.Failed));
|
||||
Assert.Equal(FileOperationErrors.CloudHydration, ctx.Queue.Snapshot().Single().Error);
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Empty_recycle_bin_goes_through_the_queue()
|
||||
{
|
||||
await using var ctx = await Harness.CreateAsync();
|
||||
var ops = new FileOperationService(ctx.Queue, ctx.Shell, new DiskEnum());
|
||||
await ctx.Queue.StartAsync(CancellationToken.None);
|
||||
await ops.EmptyRecycleBinAsync();
|
||||
await WaitUntil(() => ctx.Queue.Snapshot().Any(j =>
|
||||
j.Op == TransferOp.EmptyRecycleBin && j.Status == TransferStatus.Done));
|
||||
Assert.Equal(1, ctx.Shell.EmptyCount);
|
||||
await ctx.Queue.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
private static async Task WaitUntil(Func<bool> condition, TimeSpan? timeout = null)
|
||||
=> await WaitUntil(() => Task.FromResult(condition()), timeout);
|
||||
|
||||
private static async Task WaitUntil(Func<Task<bool>> condition, TimeSpan? timeout = null)
|
||||
{
|
||||
var limit = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(4));
|
||||
while (!condition())
|
||||
while (!await condition().ConfigureAwait(false))
|
||||
{
|
||||
if (DateTime.UtcNow > limit)
|
||||
{
|
||||
@@ -166,12 +430,15 @@ public class TransferQueueTests
|
||||
public required TransferQueue Queue { get; init; }
|
||||
public required GateShell Shell { get; init; }
|
||||
public required SqliteIndexStore Store { get; init; }
|
||||
public required ControlledVolumes Volumes { 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()
|
||||
public static async Task<Harness> CreateAsync(
|
||||
IArchiveExecutor? archives = null,
|
||||
IHydrationGuard? hydration = null)
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "ew-xfer", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(root);
|
||||
@@ -183,17 +450,30 @@ public class TransferQueueTests
|
||||
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);
|
||||
var volumes = new ControlledVolumes();
|
||||
var queue = new TransferQueue(
|
||||
new NativeFileOperationExecutor(shell, new DiskEnum(), archives, hydration),
|
||||
store,
|
||||
volumes,
|
||||
NullLogger<TransferQueue>.Instance);
|
||||
return new Harness
|
||||
{
|
||||
Queue = queue,
|
||||
Shell = shell,
|
||||
Store = store,
|
||||
Volumes = volumes,
|
||||
Dest = dest,
|
||||
Root = root
|
||||
};
|
||||
}
|
||||
|
||||
public TransferQueue CreateQueue()
|
||||
=> new(
|
||||
new NativeFileOperationExecutor(Shell, new DiskEnum()),
|
||||
Store,
|
||||
Volumes,
|
||||
NullLogger<TransferQueue>.Instance);
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await Store.DisposeAsync();
|
||||
@@ -236,11 +516,21 @@ internal sealed class GateShell : IShellFileOperations
|
||||
public List<string> Copied { get; } = [];
|
||||
public Action<string>? OnCopy { get; set; }
|
||||
public Func<bool>? PauseRequested { get; private set; }
|
||||
public int FailRemaining { get; set; }
|
||||
public string? FailError { get; set; }
|
||||
public int EmptyCount { 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 CreateShortcut(string targetPath, string shortcutPath, out string? error)
|
||||
{ error = null; return true; }
|
||||
public bool EmptyRecycleBin(out string? error)
|
||||
{
|
||||
EmptyCount++;
|
||||
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)
|
||||
{
|
||||
@@ -260,6 +550,13 @@ internal sealed class GateShell : IShellFileOperations
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FailRemaining > 0)
|
||||
{
|
||||
FailRemaining--;
|
||||
error = FailError ?? "failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
Copied.Add(source);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
|
||||
System.IO.File.Copy(source, destination, overwrite);
|
||||
@@ -271,3 +568,71 @@ internal sealed class GateShell : IShellFileOperations
|
||||
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);
|
||||
}
|
||||
|
||||
internal sealed class ControlledVolumes : IVolumeService
|
||||
{
|
||||
public bool Reachable { get; set; } = true;
|
||||
|
||||
public IReadOnlyList<VolumeFingerprint> EnumerateOnlineVolumes() => [];
|
||||
|
||||
public VolumeFingerprint? Probe(string path) => null;
|
||||
|
||||
public VolumeSpace GetSpace(string path) => default;
|
||||
|
||||
public bool IsPathReachable(string path) => Reachable;
|
||||
}
|
||||
|
||||
internal sealed class FakeArchiveExecutor : IArchiveExecutor
|
||||
{
|
||||
public bool IsAvailable { get; set; } = true;
|
||||
public string MissingHint => SevenZipLocator.MissingHint;
|
||||
public TimeSpan ExtractDelay { get; set; }
|
||||
public TaskCompletionSource Started { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public async Task ExtractAsync(
|
||||
string archivePath,
|
||||
string destinationDirectory,
|
||||
IProgress<ArchiveProgress>? progress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Started.TrySetResult();
|
||||
if (ExtractDelay > TimeSpan.Zero)
|
||||
{
|
||||
await Task.Delay(ExtractDelay, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(destinationDirectory);
|
||||
await File.WriteAllTextAsync(Path.Combine(destinationDirectory, "out.txt"), "ok", cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
progress?.Report(new ArchiveProgress(100, 1, "out.txt"));
|
||||
}
|
||||
|
||||
public Task CompressAsync(
|
||||
IReadOnlyList<string> sources,
|
||||
string archivePath,
|
||||
ArchiveFormat format,
|
||||
IProgress<ArchiveProgress>? progress,
|
||||
CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task AddAsync(
|
||||
string archivePath,
|
||||
IReadOnlyList<string> sources,
|
||||
IProgress<ArchiveProgress>? progress,
|
||||
CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task VerifyAsync(
|
||||
string archivePath,
|
||||
IProgress<ArchiveProgress>? progress,
|
||||
CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
||||
internal sealed class AlwaysHydrate : IHydrationGuard
|
||||
{
|
||||
public bool WouldHydrateOnRead(FileSystemItem item) => true;
|
||||
public bool WouldHydrateOnRead(int attributes, CloudAvailability? availability) => true;
|
||||
public Task<bool> WouldHydrateOnReadAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(true);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user