224 lines
8.2 KiB
C#
224 lines
8.2 KiB
C#
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)..];
|
|
}
|
|
}
|