Improve file operations, layout memory, and drive status.

Add a sequential file-operations queue with pause, reorder, and optional auto-clear; persist window size and tree width; show free space; and clear leftover indexing status.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-24 01:52:30 +02:00
parent d79605cde9
commit 9bf451932f
41 changed files with 2855 additions and 275 deletions

View File

@@ -12,6 +12,8 @@ public sealed class BrowseService
private readonly StorageProviderRegistry _providers;
private readonly CloudPlaceStore _cloudPlaces;
private readonly UiPreferencesStore _preferences;
private readonly IElevatedScanService? _elevation;
private readonly IRecycleBinCatalog? _recycle;
public BrowseService(
IFileSystemEnumerator enumerator,
@@ -20,7 +22,9 @@ public sealed class BrowseService
SourceManager sources,
StorageProviderRegistry providers,
CloudPlaceStore cloudPlaces,
UiPreferencesStore preferences)
UiPreferencesStore preferences,
IElevatedScanService? elevation = null,
IRecycleBinCatalog? recycle = null)
{
_enumerator = enumerator;
_volumes = volumes;
@@ -29,6 +33,8 @@ public sealed class BrowseService
_providers = providers;
_cloudPlaces = cloudPlaces;
_preferences = preferences;
_elevation = elevation;
_recycle = recycle;
}
public async Task<FolderListing> ListThisPcAsync(CancellationToken cancellationToken = default)
@@ -50,12 +56,15 @@ public sealed class BrowseService
.Select(place =>
{
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
Attributes = AttributeFlags.Directory,
FreeSpaceBytes = space.FreeBytes,
CapacityBytes = space.CapacityBytes
};
})
.ToList();
@@ -70,7 +79,7 @@ public sealed class BrowseService
var sources = await _sources.RefreshOnlineStateAsync(cancellationToken).ConfigureAwait(false);
var items = new List<FileSystemItem>();
foreach (var source in sources.Where(include)
.OrderBy(s => s.Kind)
.OrderBy(s => PathRules.DriveLetterSortKey(s.LastRootPath))
.ThenBy(s => s.DisplayName, StringComparer.CurrentCultureIgnoreCase))
{
IndexEntry? root = null;
@@ -79,6 +88,9 @@ public sealed class BrowseService
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,
@@ -87,7 +99,9 @@ public sealed class BrowseService
: source.DisplayName,
IsDirectory = true,
SizeBytes = root?.AggregateSize ?? 0,
Attributes = AttributeFlags.Directory
Attributes = AttributeFlags.Directory,
FreeSpaceBytes = space.FreeBytes,
CapacityBytes = space.CapacityBytes ?? source.CapacityBytes
});
}
@@ -106,6 +120,11 @@ public sealed class BrowseService
}
}
if (IsRecycleBinPath(path))
{
return ListRecycleBin(path);
}
var reachable = _volumes.IsPathReachable(path);
if (reachable)
@@ -117,7 +136,7 @@ public sealed class BrowseService
listing = await OverlayFolderSizesAsync(source, path, listing, cancellationToken).ConfigureAwait(false);
}
return new FolderListing { Path = path, IsOffline = false, Items = listing, Error = error };
return FinishListing(path, AttachVolumeSpace(listing), isOffline: false, error);
}
if (source is { IsIndexed: true })
@@ -151,7 +170,7 @@ public sealed class BrowseService
: null
})
.ToList();
return new FolderListing { Path = path, IsOffline = true, Items = items };
return FinishListing(path, AttachVolumeSpace(items), isOffline: true, error: null);
}
return new FolderListing { Path = path, IsOffline = true, Error = "Path not found" };
@@ -204,7 +223,7 @@ public sealed class BrowseService
var hint = isArchiveFile && items.Count == 0
? "Archive contents appear after the next scan."
: null;
return new FolderListing { Path = path, IsOffline = false, Items = items, Error = hint };
return new FolderListing { Path = path, IsOffline = false, Items = AttachVolumeSpace(items), Error = hint };
}
private async Task<bool> IsUnderArchiveAsync(long sourceId, string pathRel, CancellationToken cancellationToken)
@@ -262,8 +281,147 @@ public sealed class BrowseService
FileId = i.FileId,
ReparseTag = i.ReparseTag,
AllocatedSizeBytes = i.AllocatedSizeBytes ?? e.AllocatedSizeBytes,
Cloud = i.Cloud
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();
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)
{
var looksRestricted = LocationClassifier.Classify(item.FullPath, item.Name, item.Attributes, item.IsDirectory);
var accessDenied = 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
{
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
};
}
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();
}
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 FolderListing ListRecycleBin(string path)
{
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 })
{
hint += " " + _elevation.ProtectedContentHint;
}
return new FolderListing { Path = path, IsOffline = false, Items = [], Error = hint };
}
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;
}
}
}