Files
Explorer-Workbench/src/Explorer.Application/BrowseService.cs

986 lines
35 KiB
C#

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<FolderListing> 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<FileSystemItem>();
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<FolderListing> 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<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
{
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(
string path,
Func<Source, bool> include,
CancellationToken cancellationToken)
{
var sources = await _sources.RefreshOnlineStateAsync(cancellationToken).ConfigureAwait(false);
var items = new List<FileSystemItem>();
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<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)
{
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<Dictionary<string, IndexEntry>> LoadIndexAfterSourceAsync(
Task<Source?> sourceTask,
string path,
CancellationToken cancellationToken)
{
Source? source;
try
{
source = await sourceTask.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
throw;
}
catch
{
return new Dictionary<string, IndexEntry>(StringComparer.Ordinal);
}
if (source is not { IsIndexed: true })
{
return new Dictionary<string, IndexEntry>(StringComparer.Ordinal);
}
return await LoadIndexChildrenAsync(source, path, cancellationToken).ConfigureAwait(false);
}
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,
Task<Source?> sourceTask,
BrowseViewport? viewport,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var prefs = _preferences.Load();
var spaceCache = new Dictionary<string, VolumeSpace>(StringComparer.OrdinalIgnoreCase);
Task<Dictionary<string, IndexEntry>>? indexTask = null;
Task<Dictionary<string, IndexEntry>> KickIndex()
=> indexTask ??= LoadIndexAfterSourceAsync(sourceTask, path, cancellationToken);
var batch = new List<FileSystemItem>(BrowseHydration.PublishBatch);
var all = new List<FileSystemItem>();
var byPath = new Dictionary<string, int>(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<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,
indexedChildCount: entry.ChildFileCount + entry.ChildDirCount,
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)
{
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<bool> 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<FileSystemItem> 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<string, VolumeSpace> 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<FileSystemItem> AttachVolumeSpace(IReadOnlyList<FileSystemItem> items)
{
var cache = new Dictionary<string, VolumeSpace>(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<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 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;
}
}
}