Initial commit: FluidExplorer:从零实现的 WinUI 3 文件资源管理器替代品(Mica、命令栏、面包屑)
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace FluidExplorer.Services.Shell;
|
||||
|
||||
/// <summary>回收站里的一条记录(从 $I 元数据文件解析,含原始路径)。</summary>
|
||||
public sealed record RecycleBinEntry(
|
||||
string RecyclePath,
|
||||
string OriginalPath,
|
||||
long Size,
|
||||
DateTime DeletedUtc,
|
||||
string VolumeRoot)
|
||||
{
|
||||
public string Name => Services.FileSystem.PathHelper.GetName(OriginalPath);
|
||||
public string OriginalDirectory => Services.FileSystem.PathHelper.GetParent(OriginalPath);
|
||||
public bool IsDirectory => Size == 0 && OriginalPath.Length > 0 && !Path.HasExtension(OriginalPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接读取各卷的 $Recycle.Bin\<SID>\$I* 元数据文件,得到回收站内容与原始路径。
|
||||
/// 这样"还原/彻底删除"都能由本程序完成(不依赖系统弹窗),也支持一步撤销。
|
||||
/// </summary>
|
||||
public static class RecycleBinView
|
||||
{
|
||||
public static async Task<IReadOnlyList<RecycleBinEntry>> EnumerateAsync(CancellationToken cancellationToken)
|
||||
=> await Task.Run(() => Enumerate(cancellationToken), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
public static IReadOnlyList<RecycleBinEntry> Enumerate(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var results = new List<RecycleBinEntry>();
|
||||
foreach (var drive in DriveInfo.GetDrives())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
if (!drive.IsReady || drive.DriveType != DriveType.Fixed) continue;
|
||||
var binRoot = Path.Combine(drive.Name, "$Recycle.Bin");
|
||||
if (!Directory.Exists(binRoot)) continue;
|
||||
|
||||
foreach (var sidDir in Directory.EnumerateDirectories(binRoot))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
foreach (var metaFile in Directory.EnumerateFiles(sidDir, "$I*"))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var entry = TryParse(metaFile, drive.Name);
|
||||
if (entry is not null) results.Add(entry);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 其他用户的回收站目录通常无权限,跳过
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 卷不可读时跳过
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>解析单个 $I 元数据文件。</summary>
|
||||
public static RecycleBinEntry? TryParse(string metaFilePath, string volumeRoot)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = File.ReadAllBytes(metaFilePath);
|
||||
if (bytes.Length < 24) return null;
|
||||
|
||||
var version = BinaryPrimitives.ReadInt64LittleEndian(bytes.AsSpan(0, 8));
|
||||
var size = BinaryPrimitives.ReadInt64LittleEndian(bytes.AsSpan(8, 8));
|
||||
var fileTime = BinaryPrimitives.ReadInt64LittleEndian(bytes.AsSpan(16, 8));
|
||||
var deleted = fileTime > 0 ? DateTime.FromFileTimeUtc(fileTime) : DateTime.MinValue;
|
||||
|
||||
string originalPath;
|
||||
if (version >= 2 && bytes.Length >= 28)
|
||||
{
|
||||
var length = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(24, 4));
|
||||
if (length <= 0 || 28 + length * 2 > bytes.Length) return null;
|
||||
originalPath = Encoding.Unicode.GetString(bytes, 28, length * 2).TrimEnd('\0');
|
||||
}
|
||||
else if (version == 1)
|
||||
{
|
||||
// 旧格式:路径固定在 0x2C 偏移处的 260 个宽字符
|
||||
if (bytes.Length < 0x2C + 520) return null;
|
||||
originalPath = Encoding.Unicode.GetString(bytes, 0x2C, 520).TrimEnd('\0');
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(originalPath)) return null;
|
||||
|
||||
var fileName = Path.GetFileName(metaFilePath);
|
||||
var recyclePath = Path.Combine(Path.GetDirectoryName(metaFilePath)!, "$R" + fileName[2..]);
|
||||
return new RecycleBinEntry(recyclePath, originalPath, Math.Max(0, size), deleted, volumeRoot);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user