110 lines
4.3 KiB
C#
110 lines
4.3 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|