Show folder names immediately and refresh stale index sizes without walking the whole drive.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,6 +16,7 @@ public sealed class BrowseService
|
||||
private readonly UiPreferencesStore _preferences;
|
||||
private readonly IElevatedScanService? _elevation;
|
||||
private readonly IRecycleBinCatalog? _recycle;
|
||||
private readonly IKnownUserFolderCatalog? _knownFolders;
|
||||
|
||||
public BrowseService(
|
||||
IFileSystemEnumerator enumerator,
|
||||
@@ -26,7 +27,8 @@ public sealed class BrowseService
|
||||
CloudPlaceStore cloudPlaces,
|
||||
UiPreferencesStore preferences,
|
||||
IElevatedScanService? elevation = null,
|
||||
IRecycleBinCatalog? recycle = null)
|
||||
IRecycleBinCatalog? recycle = null,
|
||||
IKnownUserFolderCatalog? knownFolders = null)
|
||||
{
|
||||
_enumerator = enumerator;
|
||||
_volumes = volumes;
|
||||
@@ -37,6 +39,7 @@ public sealed class BrowseService
|
||||
_preferences = preferences;
|
||||
_elevation = elevation;
|
||||
_recycle = recycle;
|
||||
_knownFolders = knownFolders;
|
||||
}
|
||||
|
||||
public async Task<FolderListing> ListThisPcAsync(CancellationToken cancellationToken = default)
|
||||
@@ -56,6 +59,41 @@ public sealed class BrowseService
|
||||
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)
|
||||
@@ -172,17 +210,6 @@ public sealed class BrowseService
|
||||
BrowseViewport? viewport = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var source = await _sources.FindByPathAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
if (source is { IsIndexed: true } && _preferences.Load().IndexArchiveContents)
|
||||
{
|
||||
var archiveListing = await TryListArchiveAsync(source, path, cancellationToken).ConfigureAwait(false);
|
||||
if (archiveListing is not null)
|
||||
{
|
||||
yield return CompleteDelta(archiveListing);
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
if (path == LocationRoots.RecycleBin || IsRecycleBinPath(path))
|
||||
{
|
||||
yield return CompleteDelta(ListRecycleBin(path));
|
||||
@@ -190,13 +217,35 @@ public sealed class BrowseService
|
||||
}
|
||||
|
||||
var reachable = _volumes.IsPathReachable(path);
|
||||
if (!reachable)
|
||||
if (MightBeArchiveListing(path, reachable))
|
||||
{
|
||||
yield return CompleteDelta(await ListOfflineAsync(source, path, cancellationToken).ConfigureAwait(false));
|
||||
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;
|
||||
}
|
||||
|
||||
await foreach (var delta in ListLiveProgressiveAsync(path, source, viewport, cancellationToken).ConfigureAwait(false))
|
||||
var sourceTask = _sources.FindByPathAsync(path, cancellationToken);
|
||||
_ = MarkReachableInBackground(path, cancellationToken);
|
||||
await foreach (var delta in ListLiveProgressiveAsync(path, sourceTask, viewport, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return delta;
|
||||
}
|
||||
@@ -205,6 +254,64 @@ public sealed class BrowseService
|
||||
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,
|
||||
@@ -293,46 +400,43 @@ public sealed class BrowseService
|
||||
|
||||
private async IAsyncEnumerable<BrowseDelta> ListLiveProgressiveAsync(
|
||||
string path,
|
||||
Source? source,
|
||||
Task<Source?> sourceTask,
|
||||
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));
|
||||
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);
|
||||
Dictionary<string, IndexEntry>? indexMap = indexTask.IsCompletedSuccessfully ? indexTask.Result : null;
|
||||
var sink = new FileEnumerationSink();
|
||||
var firstFlush = true;
|
||||
|
||||
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);
|
||||
var item = Annotate(
|
||||
raw.Overlay(hydration: raw.Hydration & ~(ItemHydrationFlags.Index | ItemHydrationFlags.Provider)),
|
||||
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)
|
||||
var limit = firstFlush ? BrowseHydration.FirstPublish : BrowseHydration.PublishBatch;
|
||||
if (batch.Count >= limit)
|
||||
{
|
||||
firstFlush = false;
|
||||
yield return new BrowseDelta
|
||||
{
|
||||
Path = path,
|
||||
@@ -340,6 +444,7 @@ public sealed class BrowseService
|
||||
CompletedStages = ItemHydrationFlags.Shell | ItemHydrationFlags.Metadata | ItemHydrationFlags.Location
|
||||
};
|
||||
batch.Clear();
|
||||
_ = KickIndex();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,11 +457,7 @@ public sealed class BrowseService
|
||||
CompletedStages = ItemHydrationFlags.Shell | ItemHydrationFlags.Metadata | ItemHydrationFlags.Location
|
||||
};
|
||||
|
||||
if (indexMap is null)
|
||||
{
|
||||
indexMap = await indexTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var indexMap = await KickIndex().ConfigureAwait(false);
|
||||
var indexUpdates = OverlayPendingIndex(all, byPath, indexMap, prefs);
|
||||
if (indexUpdates.Count > 0)
|
||||
{
|
||||
@@ -368,6 +469,7 @@ public sealed class BrowseService
|
||||
};
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -506,6 +608,7 @@ public sealed class BrowseService
|
||||
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);
|
||||
}
|
||||
@@ -757,6 +860,28 @@ public sealed class BrowseService
|
||||
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);
|
||||
@@ -813,6 +938,12 @@ public sealed class BrowseService
|
||||
.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"];
|
||||
|
||||
Reference in New Issue
Block a user