using System.Buffers.Binary; using System.Text; namespace FluidExplorer.Services.Shell; /// 回收站里的一条记录(从 $I 元数据文件解析,含原始路径)。 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); } /// /// 直接读取各卷的 $Recycle.Bin\<SID>\$I* 元数据文件,得到回收站内容与原始路径。 /// 这样"还原/彻底删除"都能由本程序完成(不依赖系统弹窗),也支持一步撤销。 /// public static class RecycleBinView { public static async Task> EnumerateAsync(CancellationToken cancellationToken) => await Task.Run(() => Enumerate(cancellationToken), cancellationToken).ConfigureAwait(false); public static IReadOnlyList Enumerate(CancellationToken cancellationToken = default) { var results = new List(); 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; } /// 解析单个 $I 元数据文件。 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; } } }