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:
2026-08-24 15:04:04 +02:00
parent 9bf451932f
commit a3c54bbb03
127 changed files with 14748 additions and 633 deletions

View File

@@ -1,3 +1,5 @@
using System.Runtime.CompilerServices;
using System.Threading.Channels;
using Explorer.Domain;
using Explorer.Domain.Abstractions;
@@ -40,35 +42,70 @@ public sealed class BrowseService
public async Task<FolderListing> ListThisPcAsync(CancellationToken cancellationToken = default)
{
var groupNetwork = _preferences.Load().GroupNetworkPlaces;
return await ListSourcesAsync(
var listing = await ListSourcesAsync(
LocationRoots.ThisPc,
source => !groupNetwork || !source.Kind.IsNetwork(),
cancellationToken).ConfigureAwait(false);
var items = listing.Items.ToList();
if (!groupNetwork)
{
items.AddRange(await UntrackedNetworkItemsAsync(cancellationToken).ConfigureAwait(false));
}
items.Add(RecycleBinItem());
return new FolderListing { Path = listing.Path, Items = items };
}
public Task<FolderListing> ListNetworkAsync(CancellationToken cancellationToken = default)
=> ListSourcesAsync(LocationRoots.Network, source => source.Kind.IsNetwork(), cancellationToken);
public Task<FolderListing> ListCloudAsync(CancellationToken cancellationToken = default)
public async Task<FolderListing> ListNetworkAsync(CancellationToken cancellationToken = default)
{
var items = CloudPlaceStore.Merge(_providers.GetPlaces(), _cloudPlaces.Load())
.OrderBy(p => p.DisplayName, StringComparer.CurrentCultureIgnoreCase)
.Select(place =>
var listing = await ListSourcesAsync(LocationRoots.Network, source => source.Kind.IsNetwork(), cancellationToken)
.ConfigureAwait(false);
var items = listing.Items.ToList();
items.AddRange(await UntrackedNetworkItemsAsync(cancellationToken).ConfigureAwait(false));
return new FolderListing { Path = listing.Path, Items = items };
}
public async Task<FolderListing> ListCloudAsync(CancellationToken cancellationToken = default)
{
var items = new List<FileSystemItem>();
foreach (var place in CloudPlaceStore.Merge(_providers.GetPlaces(), _cloudPlaces.Load())
.OrderBy(p => p.DisplayName, StringComparer.CurrentCultureIgnoreCase))
{
var exists = Directory.Exists(place.Path);
var space = exists ? _volumes.GetSpace(place.Path) : default;
var quota = await _providers.TryGetQuotaAsync(place.Path, cancellationToken).ConfigureAwait(false);
var capacity = quota?.TotalBytes ?? space.CapacityBytes;
long? free = quota is { TotalBytes: long total, UsedBytes: long used }
? Math.Max(0, total - used)
: space.FreeBytes;
items.Add(new FileSystemItem
{
var exists = Directory.Exists(place.Path);
var space = exists ? _volumes.GetSpace(place.Path) : default;
return new FileSystemItem
{
FullPath = place.Path,
Name = exists ? place.DisplayName : $"{place.DisplayName} (Offline)",
IsDirectory = true,
Attributes = AttributeFlags.Directory,
FreeSpaceBytes = space.FreeBytes,
CapacityBytes = space.CapacityBytes
};
})
.ToList();
return Task.FromResult(new FolderListing { Path = LocationRoots.Cloud, Items = items });
FullPath = place.Path,
Name = exists ? place.DisplayName : $"{place.DisplayName} (Offline)",
IsDirectory = true,
Attributes = AttributeFlags.Directory,
FreeSpaceBytes = free,
CapacityBytes = capacity
});
}
return new FolderListing { Path = LocationRoots.Cloud, Items = items };
}
public FolderListing ListRecycleBin(string? path = null)
{
var root = string.IsNullOrWhiteSpace(path) ? LocationRoots.RecycleBin : path;
var summary = _recycle?.TrySummarize(root);
var hint = summary is null
? "Recycle Bin contents are managed by Windows."
: $"{summary.ItemCount:N0} deleted items · {FormatBytes(summary.UsedBytes)} used";
return new FolderListing
{
Path = LocationRoots.RecycleBin,
IsOffline = false,
Items = [RecycleBinItem(summary)],
Error = hint
};
}
private async Task<FolderListing> ListSourcesAsync(
@@ -109,6 +146,31 @@ public sealed class BrowseService
}
public async Task<FolderListing> ListAsync(string path, CancellationToken cancellationToken = default)
{
var items = new List<FileSystemItem>();
var byPath = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
string? error = null;
var offline = false;
var listingPath = path;
await foreach (var delta in ListProgressiveAsync(path, viewport: null, cancellationToken).ConfigureAwait(false))
{
listingPath = delta.Path;
offline = delta.IsOffline;
if (delta.Error is not null)
{
error = delta.Error;
}
ApplyDelta(items, byPath, delta);
}
return new FolderListing { Path = listingPath, IsOffline = offline, Items = items, Error = error };
}
public async IAsyncEnumerable<BrowseDelta> ListProgressiveAsync(
string path,
BrowseViewport? viewport = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var source = await _sources.FindByPathAsync(path, cancellationToken).ConfigureAwait(false);
if (source is { IsIndexed: true } && _preferences.Load().IndexArchiveContents)
@@ -116,69 +178,472 @@ public sealed class BrowseService
var archiveListing = await TryListArchiveAsync(source, path, cancellationToken).ConfigureAwait(false);
if (archiveListing is not null)
{
return archiveListing;
yield return CompleteDelta(archiveListing);
yield break;
}
}
if (IsRecycleBinPath(path))
if (path == LocationRoots.RecycleBin || IsRecycleBinPath(path))
{
return ListRecycleBin(path);
yield return CompleteDelta(ListRecycleBin(path));
yield break;
}
var reachable = _volumes.IsPathReachable(path);
if (reachable)
if (!reachable)
{
var items = _enumerator.EnumerateChildrenSafe(path, out var error);
var listing = (await _providers.EnrichAsync(items.ToList(), cancellationToken).ConfigureAwait(false)).ToList();
if (source is { IsIndexed: true })
{
listing = await OverlayFolderSizesAsync(source, path, listing, cancellationToken).ConfigureAwait(false);
}
return FinishListing(path, AttachVolumeSpace(listing), isOffline: false, error);
yield return CompleteDelta(await ListOfflineAsync(source, path, cancellationToken).ConfigureAwait(false));
yield break;
}
if (source is { IsIndexed: true })
await foreach (var delta in ListLiveProgressiveAsync(path, source, viewport, cancellationToken).ConfigureAwait(false))
{
var rel = source.LastRootPath is null ? "" : PathRules.MakeRelative(source.LastRootPath, path);
var dir = await _store.Entries.GetByPathAsync(source.Id, rel, cancellationToken).ConfigureAwait(false)
?? await _store.Entries.GetRootAsync(source.Id, cancellationToken).ConfigureAwait(false);
if (dir is null)
{
return new FolderListing { Path = path, IsOffline = true, Error = "Not available" };
}
var children = await _store.Entries.GetChildrenAsync(source.Id, dir.Id, null, cancellationToken)
.ConfigureAwait(false);
var items = children
.Where(c => c.Status is EntryStatus.Present or EntryStatus.Offline)
.Select(c => new FileSystemItem
{
FullPath = PathRules.Combine(source.LastRootPath ?? source.DisplayName, c.PathRel),
Name = c.Name,
IsDirectory = c.IsDirectory,
SizeBytes = c.IsDirectory ? c.AggregateSize : c.SizeBytes,
CreatedUtc = c.CreatedUtc,
ModifiedUtc = c.ModifiedUtc,
Attributes = c.Attributes,
FileId = c.FileId,
ReparseTag = c.ReparseTag,
AllocatedSizeBytes = c.AllocatedSizeBytes,
Cloud = c.CloudAvailability is { } availability
? new CloudPresence(null, availability, c.SizeBytes, c.AllocatedSizeBytes, availability == CloudAvailability.OnlineOnly)
: null
})
.ToList();
return FinishListing(path, AttachVolumeSpace(items), isOffline: true, error: null);
yield return delta;
}
return new FolderListing { Path = path, IsOffline = true, Error = "Path not found" };
}
public bool CanBrowseArchive(string name)
=> _preferences.Load().IndexArchiveContents && ArchiveFormats.IsArchive(name);
private static void ApplyDelta(
List<FileSystemItem> items,
Dictionary<string, int> byPath,
BrowseDelta delta)
{
foreach (var added in delta.Added)
{
Upsert(items, byPath, added);
}
foreach (var updated in delta.Updated)
{
Upsert(items, byPath, updated);
}
}
private static void Upsert(
List<FileSystemItem> items,
Dictionary<string, int> byPath,
FileSystemItem item)
{
if (byPath.TryGetValue(item.FullPath, out var index))
{
items[index] = item;
return;
}
byPath[item.FullPath] = items.Count;
items.Add(item);
}
private static BrowseDelta CompleteDelta(FolderListing listing)
=> new()
{
Path = listing.Path,
IsOffline = listing.IsOffline,
Error = listing.Error,
Added = listing.Items,
EnumerationComplete = true,
HydrationComplete = true,
CompletedStages = ItemHydrationFlags.All
};
private async Task<FolderListing> ListOfflineAsync(Source? source, string path, CancellationToken cancellationToken)
{
if (source is not { IsIndexed: true })
{
return new FolderListing { Path = path, IsOffline = true, Error = "Path not found" };
}
var rel = source.LastRootPath is null ? "" : PathRules.MakeRelative(source.LastRootPath, path);
var dir = await _store.Entries.GetByPathAsync(source.Id, rel, cancellationToken).ConfigureAwait(false)
?? await _store.Entries.GetRootAsync(source.Id, cancellationToken).ConfigureAwait(false);
if (dir is null)
{
return new FolderListing { Path = path, IsOffline = true, Error = "Not available" };
}
var children = await _store.Entries.GetChildrenAsync(source.Id, dir.Id, null, cancellationToken)
.ConfigureAwait(false);
var items = children
.Where(c => c.Status is EntryStatus.Present or EntryStatus.Offline)
.Select(c => FromIndex(source, c))
.ToList();
return FinishListing(path, AttachVolumeSpace(items), isOffline: true, error: null);
}
private static FileSystemItem FromIndex(Source source, IndexEntry entry)
=> new()
{
FullPath = PathRules.Combine(source.LastRootPath ?? source.DisplayName, entry.PathRel),
Name = entry.Name,
IsDirectory = entry.IsDirectory,
SizeBytes = entry.IsDirectory ? entry.AggregateSize : entry.SizeBytes,
CreatedUtc = entry.CreatedUtc,
ModifiedUtc = entry.ModifiedUtc,
Attributes = entry.Attributes,
FileId = entry.FileId,
ReparseTag = entry.ReparseTag,
AllocatedSizeBytes = entry.AllocatedSizeBytes,
Cloud = entry.CloudAvailability is { } availability
? new CloudPresence(null, availability, entry.SizeBytes, entry.AllocatedSizeBytes, availability == CloudAvailability.OnlineOnly)
: null,
Hydration = ItemHydrationFlags.All
};
private async IAsyncEnumerable<BrowseDelta> ListLiveProgressiveAsync(
string path,
Source? source,
BrowseViewport? viewport,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var prefs = _preferences.Load();
var spaceCache = new Dictionary<string, VolumeSpace>(StringComparer.OrdinalIgnoreCase);
var indexTask = source is { IsIndexed: true }
? LoadIndexChildrenAsync(source, path, cancellationToken)
: Task.FromResult(new Dictionary<string, IndexEntry>(StringComparer.Ordinal));
var batch = new List<FileSystemItem>(BrowseHydration.PublishBatch);
var all = new List<FileSystemItem>();
var byPath = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
Dictionary<string, IndexEntry>? indexMap = indexTask.IsCompletedSuccessfully ? indexTask.Result : null;
var sink = new FileEnumerationSink();
await foreach (var raw in StreamEnumerationAsync(path, sink, cancellationToken).ConfigureAwait(false))
{
cancellationToken.ThrowIfCancellationRequested();
if (indexMap is null && indexTask.IsCompleted)
{
indexMap = await indexTask.ConfigureAwait(false);
}
var item = Annotate(raw, sizeFromIndex: false, prefs, probeAccess: false);
if (!LocationVisibility.ShouldShow(item.Location, prefs))
{
continue;
}
if (indexMap is not null)
{
item = OverlayIndex(item, indexMap, prefs);
}
item = AttachVolumeSpace(item, spaceCache);
Upsert(all, byPath, item);
batch.Add(item);
if (batch.Count >= BrowseHydration.PublishBatch)
{
yield return new BrowseDelta
{
Path = path,
Added = batch.ToArray(),
CompletedStages = ItemHydrationFlags.Shell | ItemHydrationFlags.Metadata | ItemHydrationFlags.Location
};
batch.Clear();
}
}
yield return new BrowseDelta
{
Path = path,
Error = sink.Error,
Added = batch.Count > 0 ? batch.ToArray() : [],
EnumerationComplete = true,
CompletedStages = ItemHydrationFlags.Shell | ItemHydrationFlags.Metadata | ItemHydrationFlags.Location
};
if (indexMap is null)
{
indexMap = await indexTask.ConfigureAwait(false);
}
var indexUpdates = OverlayPendingIndex(all, byPath, indexMap, prefs);
if (indexUpdates.Count > 0)
{
yield return new BrowseDelta
{
Path = path,
Updated = indexUpdates,
CompletedStages = ItemHydrationFlags.Index
};
}
var accessUpdates = await ProbeAccessDeniedAsync(all, byPath, prefs, source?.Kind, cancellationToken)
.ConfigureAwait(false);
if (accessUpdates.Count > 0)
{
yield return new BrowseDelta { Path = path, Updated = accessUpdates };
}
var constrained = _providers.Find(path) is not null;
if (constrained)
{
await foreach (var enriched in EnrichInBatchesAsync(all, byPath, viewport, source?.Kind, constrained: true, cancellationToken)
.ConfigureAwait(false))
{
if (enriched.Count == 0)
{
continue;
}
yield return new BrowseDelta
{
Path = path,
Updated = enriched,
CompletedStages = ItemHydrationFlags.Provider
};
}
}
yield return new BrowseDelta
{
Path = path,
Error = sink.Error,
HydrationComplete = true,
CompletedStages = ItemHydrationFlags.All
};
}
private async IAsyncEnumerable<FileSystemItem> StreamEnumerationAsync(
string path,
FileEnumerationSink sink,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var channel = Channel.CreateBounded<FileSystemItem>(new BoundedChannelOptions(256)
{
SingleReader = true,
SingleWriter = true,
FullMode = BoundedChannelFullMode.Wait
});
var writer = Task.Run(async () =>
{
try
{
foreach (var item in _enumerator.EnumerateChildrenStreaming(path, sink, cancellationToken))
{
await channel.Writer.WriteAsync(item, cancellationToken).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
// Reader observes cancellation.
}
catch (Exception ex)
{
sink.Error ??= ex.Message;
}
finally
{
channel.Writer.TryComplete();
}
}, CancellationToken.None);
try
{
await foreach (var item in channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
{
yield return item;
}
}
finally
{
try
{
await writer.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// superseded
}
}
}
private async Task<Dictionary<string, IndexEntry>> LoadIndexChildrenAsync(
Source source,
string path,
CancellationToken cancellationToken)
{
var rel = PathRules.MakeRelative(source.LastRootPath ?? path, path);
var indexed = await _store.Entries.GetByPathAsync(source.Id, rel, cancellationToken).ConfigureAwait(false)
?? (string.IsNullOrEmpty(rel)
? await _store.Entries.GetRootAsync(source.Id, cancellationToken).ConfigureAwait(false)
: null);
if (indexed is null)
{
return new Dictionary<string, IndexEntry>(StringComparer.Ordinal);
}
var children = await _store.Entries.GetChildrenAsync(source.Id, indexed.Id, EntryStatus.Present, cancellationToken)
.ConfigureAwait(false);
return children.ToDictionary(c => c.NameNorm, StringComparer.Ordinal);
}
private FileSystemItem OverlayIndex(
FileSystemItem item,
IReadOnlyDictionary<string, IndexEntry> byName,
UiPreferences preferences)
{
if (!byName.TryGetValue(NameNormalizer.Normalize(item.Name), out var entry))
{
return item;
}
var size = item.IsDirectory ? entry.AggregateSize : item.SizeBytes;
var cloud = item.Cloud;
if (cloud is null && entry.CloudAvailability is { } availability)
{
cloud = new CloudPresence(
null,
availability,
entry.SizeBytes,
entry.AllocatedSizeBytes,
availability == CloudAvailability.OnlineOnly);
}
var hydrated = item.Overlay(
sizeBytes: size,
allocatedSizeBytes: item.AllocatedSizeBytes ?? entry.AllocatedSizeBytes,
fileId: item.FileId ?? entry.FileId,
cloud: cloud,
hydration: item.Hydration | ItemHydrationFlags.Index);
return Annotate(hydrated, sizeFromIndex: item.IsDirectory && entry.AggregateSize > 0, preferences, probeAccess: false);
}
private List<FileSystemItem> OverlayPendingIndex(
List<FileSystemItem> all,
Dictionary<string, int> byPath,
IReadOnlyDictionary<string, IndexEntry> indexMap,
UiPreferences preferences)
{
if (indexMap.Count == 0)
{
return [];
}
var updates = new List<FileSystemItem>();
for (var i = 0; i < all.Count; i++)
{
var current = all[i];
if ((current.Hydration & ItemHydrationFlags.Index) != 0)
{
continue;
}
var updated = OverlayIndex(current, indexMap, preferences);
if (ReferenceEquals(updated, current))
{
all[i] = current.Overlay(hydration: current.Hydration | ItemHydrationFlags.Index);
continue;
}
all[i] = updated;
byPath[updated.FullPath] = i;
updates.Add(updated);
}
return updates;
}
private async Task<List<FileSystemItem>> ProbeAccessDeniedAsync(
List<FileSystemItem> all,
Dictionary<string, int> byPath,
UiPreferences preferences,
SourceKind? kind,
CancellationToken cancellationToken)
{
if (!preferences.ShowProtectedSystemLocations)
{
return [];
}
var candidates = all
.Where(i => i.IsDirectory && i.Location.IsProtected && !i.Location.IsRecycleBin && !i.Location.AccessDenied)
.ToList();
if (candidates.Count == 0)
{
return [];
}
var updates = new List<FileSystemItem>();
var gate = new object();
await Parallel.ForEachAsync(
candidates,
new ParallelOptions
{
MaxDegreeOfParallelism = BrowseHydration.WorkerCount(kind, constrained: false),
CancellationToken = cancellationToken
},
(item, token) =>
{
token.ThrowIfCancellationRequested();
if (!IsAccessDenied(item.FullPath))
{
return ValueTask.CompletedTask;
}
var updated = Annotate(item, sizeFromIndex: item.SizeBytes > 0 && item.IsDirectory, preferences, probeAccess: true);
lock (gate)
{
if (byPath.TryGetValue(updated.FullPath, out var index))
{
all[index] = updated;
}
updates.Add(updated);
}
return ValueTask.CompletedTask;
}).ConfigureAwait(false);
return updates;
}
private async IAsyncEnumerable<IReadOnlyList<FileSystemItem>> EnrichInBatchesAsync(
List<FileSystemItem> all,
Dictionary<string, int> byPath,
BrowseViewport? viewport,
SourceKind? kind,
bool constrained,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
if (all.Count == 0)
{
yield break;
}
var pending = all.ToDictionary(i => i.FullPath, StringComparer.OrdinalIgnoreCase);
var batchSize = BrowseHydration.ProviderBatchSize(kind, constrained);
while (pending.Count > 0)
{
cancellationToken.ThrowIfCancellationRequested();
var leftover = all.Where(i => pending.ContainsKey(i.FullPath)).ToList();
var ordered = BrowseHydration.Prioritize(leftover, viewport?.Snapshot() ?? []);
var chunk = ordered.Take(batchSize).ToList();
var enriched = await _providers.EnrichAsync(chunk, cancellationToken).ConfigureAwait(false);
var changed = new List<FileSystemItem>(chunk.Count);
for (var i = 0; i < chunk.Count; i++)
{
var original = chunk[i];
pending.Remove(original.FullPath);
var next = i < enriched.Count ? enriched[i] : original;
if (ReferenceEquals(next, original))
{
continue;
}
if (byPath.TryGetValue(next.FullPath, out var index))
{
all[index] = next;
}
changed.Add(next);
}
yield return changed;
}
}
private async Task<FolderListing?> TryListArchiveAsync(Source source, string path, CancellationToken cancellationToken)
{
if (source.LastRootPath is null)
@@ -243,54 +708,6 @@ public sealed class BrowseService
return false;
}
private async Task<List<FileSystemItem>> OverlayFolderSizesAsync(
Source source,
string path,
List<FileSystemItem> listing,
CancellationToken cancellationToken)
{
var rel = PathRules.MakeRelative(source.LastRootPath ?? path, path);
var indexed = await _store.Entries.GetByPathAsync(source.Id, rel, cancellationToken).ConfigureAwait(false)
?? (string.IsNullOrEmpty(rel)
? await _store.Entries.GetRootAsync(source.Id, cancellationToken).ConfigureAwait(false)
: null);
if (indexed is null)
{
return listing;
}
var children = await _store.Entries.GetChildrenAsync(source.Id, indexed.Id, EntryStatus.Present, cancellationToken)
.ConfigureAwait(false);
var byName = children.ToDictionary(c => c.NameNorm, StringComparer.Ordinal);
return listing.Select(i =>
{
if (!i.IsDirectory || !byName.TryGetValue(NameNormalizer.Normalize(i.Name), out var e))
{
return i;
}
return new FileSystemItem
{
FullPath = i.FullPath,
Name = i.Name,
IsDirectory = true,
SizeBytes = e.AggregateSize,
CreatedUtc = i.CreatedUtc,
ModifiedUtc = i.ModifiedUtc,
Attributes = i.Attributes,
FileId = i.FileId,
ReparseTag = i.ReparseTag,
AllocatedSizeBytes = i.AllocatedSizeBytes ?? e.AllocatedSizeBytes,
Cloud = i.Cloud,
Location = i.Location,
SizeKnowledge = i.SizeKnowledge,
DisplayName = i.DisplayName,
FreeSpaceBytes = i.FreeSpaceBytes,
CapacityBytes = i.CapacityBytes
};
}).ToList();
}
private FolderListing FinishListing(string path, IReadOnlyList<FileSystemItem> items, bool isOffline, string? error)
{
var prefs = _preferences.Load();
@@ -306,73 +723,44 @@ public sealed class BrowseService
return new FolderListing { Path = path, IsOffline = isOffline, Items = visible, Error = error };
}
private FileSystemItem Annotate(FileSystemItem item, bool sizeFromIndex, UiPreferences preferences)
private FileSystemItem Annotate(FileSystemItem item, bool sizeFromIndex, UiPreferences preferences, bool probeAccess = true)
{
var looksRestricted = LocationClassifier.Classify(item.FullPath, item.Name, item.Attributes, item.IsDirectory);
var accessDenied = preferences.ShowProtectedSystemLocations
var accessDenied = probeAccess
&& preferences.ShowProtectedSystemLocations
&& item.IsDirectory
&& (looksRestricted.IsProtected || looksRestricted.IsRecycleBin)
&& IsAccessDenied(item.FullPath);
var location = LocationClassifier.Classify(item.FullPath, item.Name, item.Attributes, item.IsDirectory, accessDenied);
var knowledge = LocationVisibility.ResolveSizeKnowledge(location, item.IsDirectory, item.SizeBytes, sizeFromIndex);
return new FileSystemItem
return item.Overlay(
location: location,
sizeKnowledge: knowledge,
displayName: location.IsRecycleBin ? "Recycle Bin" : item.DisplayName,
hydration: item.Hydration | ItemHydrationFlags.Location);
}
private FileSystemItem AttachVolumeSpace(FileSystemItem item, Dictionary<string, VolumeSpace> cache)
{
var key = SpaceKey(item.FullPath);
if (!cache.TryGetValue(key, out var space))
{
FullPath = item.FullPath,
Name = item.Name,
IsDirectory = item.IsDirectory,
SizeBytes = item.SizeBytes,
CreatedUtc = item.CreatedUtc,
ModifiedUtc = item.ModifiedUtc,
Attributes = item.Attributes,
FileId = item.FileId,
ReparseTag = item.ReparseTag,
AllocatedSizeBytes = item.AllocatedSizeBytes,
Cloud = item.Cloud,
Location = location,
SizeKnowledge = knowledge,
DisplayName = location.IsRecycleBin ? "Recycle Bin" : item.DisplayName,
FreeSpaceBytes = item.FreeSpaceBytes,
CapacityBytes = item.CapacityBytes
};
space = _volumes.GetSpace(item.FullPath);
cache[key] = space;
}
if (space.FreeBytes is null && space.CapacityBytes is null)
{
return item;
}
return item.Overlay(freeSpaceBytes: space.FreeBytes, capacityBytes: space.CapacityBytes);
}
private IReadOnlyList<FileSystemItem> AttachVolumeSpace(IReadOnlyList<FileSystemItem> items)
{
var cache = new Dictionary<string, VolumeSpace>(StringComparer.OrdinalIgnoreCase);
return items.Select(item =>
{
var key = SpaceKey(item.FullPath);
if (!cache.TryGetValue(key, out var space))
{
space = _volumes.GetSpace(item.FullPath);
cache[key] = space;
}
if (space.FreeBytes is null && space.CapacityBytes is null)
{
return item;
}
return new FileSystemItem
{
FullPath = item.FullPath,
Name = item.Name,
IsDirectory = item.IsDirectory,
SizeBytes = item.SizeBytes,
CreatedUtc = item.CreatedUtc,
ModifiedUtc = item.ModifiedUtc,
Attributes = item.Attributes,
FileId = item.FileId,
ReparseTag = item.ReparseTag,
AllocatedSizeBytes = item.AllocatedSizeBytes,
Cloud = item.Cloud,
Location = item.Location,
SizeKnowledge = item.SizeKnowledge,
DisplayName = item.DisplayName,
FreeSpaceBytes = space.FreeBytes,
CapacityBytes = space.CapacityBytes
};
}).ToList();
return items.Select(item => AttachVolumeSpace(item, cache)).ToList();
}
private static string SpaceKey(string path)
@@ -386,18 +774,57 @@ public sealed class BrowseService
return string.IsNullOrWhiteSpace(root) ? path : root;
}
private FolderListing ListRecycleBin(string path)
private FileSystemItem RecycleBinItem(RecycleBinSummary? summary = null)
{
var summary = _recycle?.TrySummarize(path);
var hint = summary is null
? "Recycle Bin contents are managed by Windows."
: $"{summary.ItemCount} deleted items · {summary.UsedBytes} bytes used";
if (_elevation is { IsElevated: false })
summary ??= _recycle?.TrySummarize(LocationRoots.RecycleBin);
return new FileSystemItem
{
hint += " " + _elevation.ProtectedContentHint;
FullPath = LocationRoots.RecycleBin,
Name = LocationRoots.RecycleBin,
DisplayName = LocationRoots.RecycleBin,
IsDirectory = true,
Attributes = AttributeFlags.Directory,
SizeBytes = summary?.UsedBytes ?? 0,
SizeKnowledge = summary is null ? SizeKnowledge.Unknown : SizeKnowledge.Calculated,
Location = new LocationInfo(false, false, false, false, true)
};
}
private async Task<IReadOnlyList<FileSystemItem>> UntrackedNetworkItemsAsync(CancellationToken cancellationToken)
{
var untracked = await _sources.ListUntrackedOnlineVolumesAsync(cancellationToken).ConfigureAwait(false);
return untracked
.Where(fp => fp.Kind.IsNetwork())
.Select(fp =>
{
var space = _volumes.GetSpace(fp.RootPath);
return new FileSystemItem
{
FullPath = fp.RootPath,
Name = fp.DisplayName ?? fp.RootPath,
DisplayName = (fp.DisplayName ?? fp.RootPath) + " (Windows)",
IsDirectory = true,
Attributes = AttributeFlags.Directory,
FreeSpaceBytes = space.FreeBytes ?? fp.FreeBytes,
CapacityBytes = space.CapacityBytes ?? fp.CapacityBytes,
AvailableToImport = true
};
})
.ToList();
}
private static string FormatBytes(long bytes)
{
string[] units = ["B", "KB", "MB", "GB", "TB", "PB"];
double value = Math.Max(0, bytes);
var unit = 0;
while (value >= 1024 && unit < units.Length - 1)
{
value /= 1024;
unit++;
}
return new FolderListing { Path = path, IsOffline = false, Items = [], Error = hint };
return unit == 0 ? $"{bytes} B" : $"{value:0.#} {units[unit]}";
}
private static bool IsRecycleBinPath(string path)