using System.Runtime.CompilerServices; using System.Threading.Channels; using Explorer.Domain; using Explorer.Domain.Abstractions; namespace Explorer.Application; public sealed class BrowseService { private readonly IFileSystemEnumerator _enumerator; private readonly IVolumeService _volumes; private readonly IIndexStore _store; private readonly SourceManager _sources; private readonly ICloudOverlay _providers; private readonly CloudPlaceStore _cloudPlaces; private readonly UiPreferencesStore _preferences; private readonly IElevatedScanService? _elevation; private readonly IRecycleBinCatalog? _recycle; private readonly IKnownUserFolderCatalog? _knownFolders; public BrowseService( IFileSystemEnumerator enumerator, IVolumeService volumes, IIndexStore store, SourceManager sources, ICloudOverlay providers, CloudPlaceStore cloudPlaces, UiPreferencesStore preferences, IElevatedScanService? elevation = null, IRecycleBinCatalog? recycle = null, IKnownUserFolderCatalog? knownFolders = null) { _enumerator = enumerator; _volumes = volumes; _store = store; _sources = sources; _providers = providers; _cloudPlaces = cloudPlaces; _preferences = preferences; _elevation = elevation; _recycle = recycle; _knownFolders = knownFolders; } public async Task ListThisPcAsync(CancellationToken cancellationToken = default) { var groupNetwork = _preferences.Load().GroupNetworkPlaces; 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 FolderListing ListHome() { var items = (_knownFolders?.ListExisting() ?? []) .Select(folder => new FileSystemItem { FullPath = folder.Path, Name = folder.Name, DisplayName = folder.Name, IsDirectory = true, Attributes = AttributeFlags.Directory }) .ToList(); return new FolderListing { Path = LocationRoots.Home, Items = items }; } public FolderListing ListFavorites() { var items = new List(); foreach (var path in FavoriteFolders.Normalize(_preferences.Load().FavoriteFolders)) { var exists = Directory.Exists(path); var name = FavoriteDisplayName(path); items.Add(new FileSystemItem { FullPath = path, Name = name, DisplayName = exists ? name : $"{name} (Offline)", IsDirectory = true, Attributes = AttributeFlags.Directory }); } return new FolderListing { Path = LocationRoots.Favorites, Items = items }; } public async Task ListNetworkAsync(CancellationToken cancellationToken = default) { 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 ListCloudAsync(CancellationToken cancellationToken = default) { var items = new List(); 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 { 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 ListSourcesAsync( string path, Func include, CancellationToken cancellationToken) { var sources = await _sources.RefreshOnlineStateAsync(cancellationToken).ConfigureAwait(false); var items = new List(); foreach (var source in sources.Where(include) .OrderBy(s => PathRules.DriveLetterSortKey(s.LastRootPath)) .ThenBy(s => s.DisplayName, StringComparer.CurrentCultureIgnoreCase)) { IndexEntry? root = null; if (source.IsIndexed) { root = await _store.Entries.GetRootAsync(source.Id, cancellationToken).ConfigureAwait(false); } var space = source.LastRootPath is null ? default : _volumes.GetSpace(source.LastRootPath); items.Add(new FileSystemItem { FullPath = source.LastRootPath ?? source.DisplayName, Name = source.Status == SourceStatus.Offline ? $"{source.DisplayName} (Offline)" : source.DisplayName, IsDirectory = true, SizeBytes = root?.AggregateSize ?? 0, Attributes = AttributeFlags.Directory, FreeSpaceBytes = space.FreeBytes, CapacityBytes = space.CapacityBytes ?? source.CapacityBytes }); } return new FolderListing { Path = path, Items = items }; } public async Task ListAsync(string path, CancellationToken cancellationToken = default) { var items = new List(); var byPath = new Dictionary(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 ListProgressiveAsync( string path, BrowseViewport? viewport = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (path == LocationRoots.RecycleBin || IsRecycleBinPath(path)) { yield return CompleteDelta(ListRecycleBin(path)); yield break; } var reachable = _volumes.IsPathReachable(path); if (MightBeArchiveListing(path, reachable)) { var archiveSource = await _sources.FindByPathAsync(path, cancellationToken).ConfigureAwait(false); if (archiveSource is { IsIndexed: true }) { var archiveListing = await TryListArchiveAsync(archiveSource, path, cancellationToken).ConfigureAwait(false); if (archiveListing is not null) { yield return CompleteDelta(archiveListing); yield break; } } if (!reachable) { yield return CompleteDelta(await ListOfflineAsync(archiveSource, path, cancellationToken).ConfigureAwait(false)); yield break; } } else if (!reachable) { var offlineSource = await _sources.FindByPathAsync(path, cancellationToken).ConfigureAwait(false); yield return CompleteDelta(await ListOfflineAsync(offlineSource, path, cancellationToken).ConfigureAwait(false)); yield break; } var sourceTask = _sources.FindByPathAsync(path, cancellationToken); _ = MarkReachableInBackground(path, cancellationToken); await foreach (var delta in ListLiveProgressiveAsync(path, sourceTask, viewport, cancellationToken).ConfigureAwait(false)) { yield return delta; } } public bool CanBrowseArchive(string name) => _preferences.Load().IndexArchiveContents && ArchiveFormats.IsArchive(name); private bool MightBeArchiveListing(string path, bool reachable) { if (!_preferences.Load().IndexArchiveContents) { return false; } if (!reachable) { return true; } var name = Path.GetFileName(path.TrimEnd('\\', '/')); return !string.IsNullOrEmpty(name) && ArchiveFormats.IsArchive(name); } private async Task MarkReachableInBackground(string path, CancellationToken cancellationToken) { try { await _sources.MarkReachableAsync(path, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { } catch { // Listing already started from the live filesystem. } } private async Task> LoadIndexAfterSourceAsync( Task sourceTask, string path, CancellationToken cancellationToken) { Source? source; try { source = await sourceTask.ConfigureAwait(false); } catch (OperationCanceledException) { throw; } catch { return new Dictionary(StringComparer.Ordinal); } if (source is not { IsIndexed: true }) { return new Dictionary(StringComparer.Ordinal); } return await LoadIndexChildrenAsync(source, path, cancellationToken).ConfigureAwait(false); } private static void ApplyDelta( List items, Dictionary 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 items, Dictionary 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 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 ListLiveProgressiveAsync( string path, Task sourceTask, BrowseViewport? viewport, [EnumeratorCancellation] CancellationToken cancellationToken) { var prefs = _preferences.Load(); var spaceCache = new Dictionary(StringComparer.OrdinalIgnoreCase); Task>? indexTask = null; Task> KickIndex() => indexTask ??= LoadIndexAfterSourceAsync(sourceTask, path, cancellationToken); var batch = new List(BrowseHydration.PublishBatch); var all = new List(); var byPath = new Dictionary(StringComparer.OrdinalIgnoreCase); var sink = new FileEnumerationSink(); var firstFlush = true; await foreach (var raw in StreamEnumerationAsync(path, sink, cancellationToken).ConfigureAwait(false)) { cancellationToken.ThrowIfCancellationRequested(); var item = Annotate( raw.Overlay(hydration: raw.Hydration & ~(ItemHydrationFlags.Index | ItemHydrationFlags.Provider)), sizeFromIndex: false, prefs, probeAccess: false); if (!LocationVisibility.ShouldShow(item.Location, prefs)) { continue; } item = AttachVolumeSpace(item, spaceCache); Upsert(all, byPath, item); batch.Add(item); var limit = firstFlush ? BrowseHydration.FirstPublish : BrowseHydration.PublishBatch; if (batch.Count >= limit) { firstFlush = false; yield return new BrowseDelta { Path = path, Added = batch.ToArray(), CompletedStages = ItemHydrationFlags.Shell | ItemHydrationFlags.Metadata | ItemHydrationFlags.Location }; batch.Clear(); _ = KickIndex(); } } yield return new BrowseDelta { Path = path, Error = sink.Error, Added = batch.Count > 0 ? batch.ToArray() : [], EnumerationComplete = true, CompletedStages = ItemHydrationFlags.Shell | ItemHydrationFlags.Metadata | ItemHydrationFlags.Location }; var indexMap = await KickIndex().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 source = sourceTask.IsCompletedSuccessfully ? sourceTask.Result : await sourceTask.ConfigureAwait(false); 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.FindProviderId(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 StreamEnumerationAsync( string path, FileEnumerationSink sink, [EnumeratorCancellation] CancellationToken cancellationToken) { var channel = Channel.CreateBounded(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> 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(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 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, indexedChildCount: entry.ChildFileCount + entry.ChildDirCount, hydration: item.Hydration | ItemHydrationFlags.Index); return Annotate(hydrated, sizeFromIndex: item.IsDirectory && entry.AggregateSize > 0, preferences, probeAccess: false); } private List OverlayPendingIndex( List all, Dictionary byPath, IReadOnlyDictionary indexMap, UiPreferences preferences) { if (indexMap.Count == 0) { return []; } var updates = new List(); 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> ProbeAccessDeniedAsync( List all, Dictionary 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(); 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> EnrichInBatchesAsync( List all, Dictionary 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(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 TryListArchiveAsync(Source source, string path, CancellationToken cancellationToken) { if (source.LastRootPath is null) { return null; } var rel = PathRules.MakeRelative(source.LastRootPath, path); var entry = await _store.Entries.GetByPathAsync(source.Id, rel, cancellationToken).ConfigureAwait(false); if (entry is null) { return null; } var isArchiveFile = !entry.IsDirectory && ArchiveFormats.IsArchive(entry.Name); if (!isArchiveFile && Directory.Exists(path)) { return null; } if (!isArchiveFile && !await IsUnderArchiveAsync(source.Id, rel, cancellationToken).ConfigureAwait(false)) { return null; } var children = await _store.Entries.GetChildrenAsync(source.Id, entry.Id, EntryStatus.Present, cancellationToken) .ConfigureAwait(false); var items = children.Select(c => new FileSystemItem { FullPath = PathRules.Combine(source.LastRootPath, 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 }).ToList(); var hint = isArchiveFile && items.Count == 0 ? "Archive contents appear after the next scan." : null; return new FolderListing { Path = path, IsOffline = false, Items = AttachVolumeSpace(items), Error = hint }; } private async Task IsUnderArchiveAsync(long sourceId, string pathRel, CancellationToken cancellationToken) { var current = pathRel; while (!string.IsNullOrEmpty(current)) { var entry = await _store.Entries.GetByPathAsync(sourceId, current, cancellationToken).ConfigureAwait(false); if (entry is { IsDirectory: false } && ArchiveFormats.IsArchive(entry.Name)) { return true; } current = PathRules.RelativeParent(current); } return false; } private FolderListing FinishListing(string path, IReadOnlyList items, bool isOffline, string? error) { var prefs = _preferences.Load(); var annotated = items.Select(i => Annotate(i, sizeFromIndex: i.SizeBytes > 0, prefs)).ToList(); var visible = annotated.Where(i => LocationVisibility.ShouldShow(i.Location, prefs)).ToList(); if (visible.Any(i => i.Location.AccessDenied) && _elevation is { IsElevated: false }) { error = string.IsNullOrEmpty(error) ? _elevation.ProtectedContentHint : error + " " + _elevation.ProtectedContentHint; } return new FolderListing { Path = path, IsOffline = isOffline, Items = visible, Error = error }; } 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 = 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 item.Overlay( location: location, sizeKnowledge: knowledge, displayName: location.IsRecycleBin ? "Recycle Bin" : item.DisplayName, hydration: item.Hydration | ItemHydrationFlags.Location); } private FileSystemItem AttachVolumeSpace(FileSystemItem item, Dictionary cache) { 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 item.Overlay(freeSpaceBytes: space.FreeBytes, capacityBytes: space.CapacityBytes); } public int CountVisibleChildren(string path) { var live = _enumerator.EnumerateChildrenSafe(path, out var error); if (error is not null) { return -1; } var prefs = _preferences.Load(); var count = 0; foreach (var item in live) { var location = LocationClassifier.Classify(item.FullPath, item.Name, item.Attributes, item.IsDirectory); if (LocationVisibility.ShouldShow(location, prefs) && !location.IsRecycleBin) { count++; } } return count; } private IReadOnlyList AttachVolumeSpace(IReadOnlyList items) { var cache = new Dictionary(StringComparer.OrdinalIgnoreCase); return items.Select(item => AttachVolumeSpace(item, cache)).ToList(); } private static string SpaceKey(string path) { if (PathRules.IsUnc(path)) { return PathRules.CanonicalUncRoot(path); } var root = Path.GetPathRoot(path); return string.IsNullOrWhiteSpace(root) ? path : root; } private FileSystemItem RecycleBinItem(RecycleBinSummary? summary = null) { summary ??= _recycle?.TrySummarize(LocationRoots.RecycleBin); return new FileSystemItem { 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> 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 FavoriteDisplayName(string path) { var name = PathRules.GetFileName(path.TrimEnd('\\')); return string.IsNullOrEmpty(name) ? path : name; } 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 unit == 0 ? $"{bytes} B" : $"{value:0.#} {units[unit]}"; } private static bool IsRecycleBinPath(string path) => LocationClassifier.IsRecycleBinName(PathRules.GetFileName(path)); private static bool IsAccessDenied(string path) { try { using var enumerator = Directory.EnumerateFileSystemEntries(path).GetEnumerator(); enumerator.MoveNext(); return false; } catch (UnauthorizedAccessException) { return true; } catch (System.Security.SecurityException) { return true; } catch { return false; } } }