Initial commit: FluidExplorer:从零实现的 WinUI 3 文件资源管理器替代品(Mica、命令栏、面包屑)
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO.Enumeration;
|
||||
using FluidExplorer.Models;
|
||||
|
||||
namespace FluidExplorer.Services.FileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// 资源管理器最核心的性能路径:目录枚举。
|
||||
/// 使用 .NET 的 FileSystemEnumerable(底层为 NtQueryDirectoryFile + 大缓冲批量返回),
|
||||
/// 一次调用即可拿到名称/属性/大小/时间,比 DirectoryInfo 逐文件查询快一个数量级。
|
||||
/// </summary>
|
||||
public sealed class FastFileSystemService : IFileSystemService
|
||||
{
|
||||
private static readonly EnumerationOptions Options = new()
|
||||
{
|
||||
RecurseSubdirectories = false,
|
||||
IgnoreInaccessible = true,
|
||||
AttributesToSkip = 0, // 隐藏/系统文件也枚举出来,由界面决定是否显示
|
||||
ReturnSpecialDirectories = false,
|
||||
MatchType = MatchType.Simple,
|
||||
BufferSize = 0 // 0 = 使用平台默认的大缓冲区
|
||||
};
|
||||
|
||||
public bool DirectoryExists(string path)
|
||||
{
|
||||
try { return Directory.Exists(path); }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public bool FileExists(string path)
|
||||
{
|
||||
try { return File.Exists(path); }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分批异步枚举:首批(batchSize 条)会尽快回调,让界面立刻有内容;
|
||||
/// 整个枚举可取消,不会阻塞调用线程。
|
||||
/// </summary>
|
||||
public async Task EnumerateAsync(
|
||||
string path,
|
||||
FolderListing listing,
|
||||
Func<IReadOnlyList<FileEntry>, Task> onBatch,
|
||||
int batchSize = 256,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
listing.Path = path;
|
||||
listing.IsLoading = true;
|
||||
listing.IsComplete = false;
|
||||
listing.Error = null;
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
var buffer = new List<FileEntry>(batchSize);
|
||||
var sw = Stopwatch.StartNew();
|
||||
foreach (var entry in EnumerateCore(path, cancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
buffer.Add(entry);
|
||||
if (buffer.Count >= batchSize)
|
||||
{
|
||||
await onBatch(buffer).ConfigureAwait(false);
|
||||
buffer = new List<FileEntry>(batchSize);
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.Count > 0) await onBatch(buffer).ConfigureAwait(false);
|
||||
_ = sw.Elapsed;
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
listing.IsComplete = true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
listing.IsComplete = false;
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
listing.Error = $"没有访问权限:{ex.Message}";
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
listing.Error = "文件夹不存在或已被移动。";
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
// 网络路径断开、设备未就绪等:给出可读提示而不是抛出
|
||||
listing.Error = $"无法读取此位置:{ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
listing.IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<FileEntry> Enumerate(string path, bool includeHidden = true)
|
||||
{
|
||||
var list = new List<FileEntry>(256);
|
||||
foreach (var e in EnumerateCore(path, CancellationToken.None))
|
||||
{
|
||||
if (!includeHidden && e.IsHidden) continue;
|
||||
list.Add(e);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>核心枚举:单次系统调用序列,无逐文件 stat。</summary>
|
||||
private static IEnumerable<FileEntry> EnumerateCore(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
var normalized = PathHelper.NormalizeForApi(path);
|
||||
var enumerable = new FileSystemEnumerable<FileEntry>(
|
||||
normalized,
|
||||
static (ref FileSystemEntry entry) => new FileEntry
|
||||
{
|
||||
Name = entry.FileName.ToString(),
|
||||
FullPath = entry.ToFullPath(),
|
||||
IsDirectory = entry.IsDirectory,
|
||||
Size = entry.IsDirectory ? 0 : entry.Length,
|
||||
ModifiedUtc = entry.LastWriteTimeUtc.UtcDateTime,
|
||||
CreatedUtc = entry.CreationTimeUtc.UtcDateTime,
|
||||
AccessedUtc = entry.LastAccessTimeUtc.UtcDateTime,
|
||||
Attributes = entry.Attributes
|
||||
},
|
||||
Options);
|
||||
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>递归求文件夹大小:并行遍历 + 可取消,用于属性对话框与状态栏提示。</summary>
|
||||
public Task<long> GetDirectorySizeAsync(string path, CancellationToken cancellationToken)
|
||||
=> Task.Run(() =>
|
||||
{
|
||||
long total = 0;
|
||||
var stack = new Stack<string>();
|
||||
stack.Push(path);
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var dir = stack.Pop();
|
||||
try
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(dir))
|
||||
{
|
||||
try { total += new FileInfo(file).Length; } catch { /* 跳过无法访问的项 */ }
|
||||
}
|
||||
foreach (var sub in Directory.EnumerateDirectories(dir))
|
||||
{
|
||||
var info = new DirectoryInfo(sub);
|
||||
if ((info.Attributes & FileAttributes.ReparsePoint) != 0) continue; // 不跟随链接,防止环
|
||||
stack.Push(sub);
|
||||
}
|
||||
}
|
||||
catch { /* 跳过无权限目录 */ }
|
||||
}
|
||||
return total;
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>路径规范化统一入口(长路径、去掉尾部分隔符、避免重复分隔符)。</summary>
|
||||
public static class PathHelper
|
||||
{
|
||||
public const string ExtendedPrefix = @"\\?\";
|
||||
public const string ExtendedUncPrefix = @"\\?\UNC\";
|
||||
|
||||
/// <summary>用于 Win32/文件系统 API 的路径(超长路径自动加 \\?\ 前缀)。</summary>
|
||||
public static string NormalizeForApi(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return path;
|
||||
if (path.StartsWith(ExtendedPrefix, StringComparison.Ordinal)) return path;
|
||||
path = path.Replace('/', '\\');
|
||||
// 去掉重复分隔符(保留 UNC 开头的两个)
|
||||
while (path.Contains(@"\\") && !path.StartsWith(@"\\", StringComparison.Ordinal)) path = path.Replace(@"\\", @"\");
|
||||
if (path.Length >= 248)
|
||||
{
|
||||
return path.StartsWith(@"\\", StringComparison.Ordinal)
|
||||
? ExtendedUncPrefix + path[2..]
|
||||
: ExtendedPrefix + path;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>去掉 \\?\ 前缀,用于显示。</summary>
|
||||
public static string StripExtendedPrefix(string path)
|
||||
{
|
||||
if (path.StartsWith(ExtendedUncPrefix, StringComparison.Ordinal)) return @"\\" + path[ExtendedUncPrefix.Length..];
|
||||
if (path.StartsWith(ExtendedPrefix, StringComparison.Ordinal)) return path[ExtendedPrefix.Length..];
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>规范化显示路径:统一分隔符、去掉末尾分隔符(根目录除外)。</summary>
|
||||
public static string NormalizeDisplay(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return path;
|
||||
var p = StripExtendedPrefix(path).Replace('/', '\\');
|
||||
if (p.Length > 3 && p.EndsWith('\\')) p = p.TrimEnd('\\');
|
||||
return p;
|
||||
}
|
||||
|
||||
public static string GetParent(string path)
|
||||
{
|
||||
var p = NormalizeDisplay(path);
|
||||
if (p.Length <= 3) return p; // "C:\" 的父级还是自己
|
||||
var idx = p.LastIndexOf('\\');
|
||||
if (idx < 0) return p;
|
||||
var parent = p[..idx];
|
||||
if (parent.Length == 2 && parent[1] == ':') parent += "\\";
|
||||
return parent.Length == 0 ? p : parent;
|
||||
}
|
||||
|
||||
public static string GetName(string path)
|
||||
{
|
||||
var p = NormalizeDisplay(path);
|
||||
if (p.Length <= 3) return p;
|
||||
var idx = p.LastIndexOf('\\');
|
||||
return idx < 0 ? p : p[(idx + 1)..];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user