1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
using System.IO;
namespace AdamsToolkit.Core;
public class StorageEntry
{
public string Path { get; set; } = "";
public string Name { get; set; } = "";
public long Bytes { get; set; }
public bool IsDir { get; set; }
public int Files { get; set; }
public bool Denied { get; set; }
}
/// <summary>
/// Explorador de espaço em disco (estilo TreeSize lite): tamanho de cada subpasta/ficheiro
/// de uma pasta, em paralelo, sem seguir reparse points (junctions/symlinks — contar
/// duas vezes ou loops). Sem admin: pastas sem permissão contam o que der e ficam marcadas.
/// </summary>
public static class StorageScanner
{
public static async Task<List<StorageEntry>> ScanAsync(string root, CancellationToken ct, IProgress<int>? progress = null)
{
var list = new List<StorageEntry>();
DirectoryInfo di;
try { di = new DirectoryInfo(root); if (!di.Exists) return list; } catch { return list; }
FileSystemInfo[] children;
try { children = di.GetFileSystemInfos(); } catch { return list; }
var entries = new StorageEntry[children.Length];
var done = 0;
await Parallel.ForEachAsync(Enumerable.Range(0, children.Length),
new ParallelOptions { MaxDegreeOfParallelism = Math.Max(2, Environment.ProcessorCount), CancellationToken = ct },
(i, token) =>
{
var c = children[i];
var e = new StorageEntry { Path = c.FullName, Name = c.Name };
if (c is DirectoryInfo d)
{
e.IsDir = true;
if (!c.Attributes.HasFlag(FileAttributes.ReparsePoint))
{
var (bytes, files, denied) = Size(d, token);
e.Bytes = bytes; e.Files = files; e.Denied = denied;
}
}
else if (c is FileInfo f)
{
try { e.Bytes = f.Length; e.Files = 1; } catch { }
}
entries[i] = e;
progress?.Report(Interlocked.Increment(ref done) * 100 / Math.Max(1, children.Length));
return ValueTask.CompletedTask;
});
list.AddRange(entries.Where(e => e != null));
list.Sort((a, b) => b.Bytes.CompareTo(a.Bytes));
return list;
}
private static (long bytes, int files, bool denied) Size(DirectoryInfo d, CancellationToken ct)
{
long bytes = 0; int files = 0; bool denied = false;
var stack = new Stack<DirectoryInfo>();
stack.Push(d);
while (stack.Count > 0)
{
if (ct.IsCancellationRequested) break;
var cur = stack.Pop();
try
{
foreach (var fi in cur.EnumerateFiles())
{
try { bytes += fi.Length; files++; } catch { }
}
foreach (var sub in cur.EnumerateDirectories())
{
if (sub.Attributes.HasFlag(FileAttributes.ReparsePoint)) continue;
stack.Push(sub);
}
}
catch (UnauthorizedAccessException) { denied = true; }
catch { }
}
return (bytes, files, denied);
}
public static List<(string root, string label, long total, long free)> Drives()
{
var r = new List<(string, string, long, long)>();
foreach (var d in DriveInfo.GetDrives())
{
try
{
if (!d.IsReady || d.DriveType is DriveType.CDRom or DriveType.Network) continue;
var name = d.VolumeLabel.Length > 0 ? d.VolumeLabel : "Disco";
r.Add((d.RootDirectory.FullName, $"{name} ({d.Name.TrimEnd('\\')})", d.TotalSize, d.AvailableFreeSpace));
}
catch { }
}
return r;
}
}