Files

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)..];
}
}