Skip to content

Commit ecf5c7c

Browse files
committed
feat: CSV/JSON export on drivers, shortcuts, duplicates, startup-impact panels
1 parent e627a99 commit ecf5c7c

2 files changed

Lines changed: 171 additions & 9 deletions

File tree

src/DeepPurge.Cli/Program.cs

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
using DeepPurge.Core.App;
88
using DeepPurge.Core.Cleaning;
9+
using DeepPurge.Core.Export;
910
using DeepPurge.Core.Diagnostics;
1011
using DeepPurge.Core.Drivers;
1112
using DeepPurge.Core.FileSystem;
@@ -55,7 +56,7 @@ public static async Task<int> Main(string[] rawArgs)
5556
"uninstall" => await CmdUninstallAsync(args, cts.Token),
5657
"repair" => await CmdRepairAsync(args, cts.Token),
5758
"drivers" => await CmdDriversAsync(args, cts.Token),
58-
"startup-impact" => CmdStartupImpact(),
59+
"startup-impact" => CmdStartupImpact(args),
5960
"shortcuts" => CmdShortcuts(args),
6061
"duplicates" => await CmdDuplicatesAsync(args, cts.Token),
6162
"snapshot" => await CmdSnapshotAsync(args, cts.Token),
@@ -206,7 +207,18 @@ private static async Task<int> CmdDriversAsync(ParsedArgs a, CancellationToken c
206207
{
207208
var pkgs = await new DriverStoreScanner().EnumerateAsync(ct);
208209
var oldOnly = a.HasFlag("old");
209-
foreach (var p in pkgs.Where(p => !oldOnly || p.IsOldVersion))
210+
var filtered = pkgs.Where(p => !oldOnly || p.IsOldVersion).ToList();
211+
212+
var exportPath = a.GetOption("export");
213+
if (exportPath != null)
214+
{
215+
var fmt = ParseExportFormat(a);
216+
GridExporter.ExportDrivers(filtered, exportPath, fmt);
217+
Console.WriteLine($"Exported {filtered.Count} drivers to {exportPath}");
218+
return 0;
219+
}
220+
221+
foreach (var p in filtered)
210222
{
211223
var tag = p.IsOldVersion ? "OLD" : " ";
212224
Console.WriteLine($"[{tag}] {p.PublishedName,-12} {p.OriginalName,-28} {p.ProviderName,-22} {p.DriverVersion,-30} {FormatBytes(p.SizeBytes)}");
@@ -215,7 +227,7 @@ private static async Task<int> CmdDriversAsync(ParsedArgs a, CancellationToken c
215227
return 0;
216228
}
217229

218-
private static int CmdStartupImpact()
230+
private static int CmdStartupImpact(ParsedArgs a)
219231
{
220232
var impacts = new StartupImpactCalculator().CalculateForCurrentUser();
221233
if (impacts.Count == 0)
@@ -224,7 +236,17 @@ private static int CmdStartupImpact()
224236
Console.Error.WriteLine("Possible causes: ran without admin, or the system has not booted since WDI was enabled.");
225237
return 1;
226238
}
227-
foreach (var e in impacts.Values.OrderByDescending(e => (int)e.Impact).ThenByDescending(e => e.DiskBytes))
239+
var sorted = impacts.Values.OrderByDescending(e => (int)e.Impact).ThenByDescending(e => e.DiskBytes).ToList();
240+
241+
var exportPath = a.GetOption("export");
242+
if (exportPath != null)
243+
{
244+
GridExporter.ExportStartupImpact(sorted, exportPath, ParseExportFormat(a));
245+
Console.WriteLine($"Exported {sorted.Count} entries to {exportPath}");
246+
return 0;
247+
}
248+
249+
foreach (var e in sorted)
228250
Console.WriteLine($"{e.Impact,-6} {e.ProcessName,-32} disk={FormatBytes(e.DiskBytes)} cpu={e.CpuMs}ms");
229251
return 0;
230252
}
@@ -234,6 +256,16 @@ private static int CmdShortcuts(ParsedArgs a)
234256
var scanner = new ShortcutRepairScanner();
235257
var shortcuts = scanner.ScanAll();
236258
var broken = shortcuts.Where(s => s.Status == ShortcutStatus.Broken).ToList();
259+
260+
var exportPath = a.GetOption("export");
261+
if (exportPath != null)
262+
{
263+
var exportSet = a.HasFlag("all") ? shortcuts : broken;
264+
GridExporter.ExportShortcuts(exportSet, exportPath, ParseExportFormat(a));
265+
Console.WriteLine($"Exported {exportSet.Count} shortcuts to {exportPath}");
266+
return 0;
267+
}
268+
237269
foreach (var s in broken) Console.WriteLine($"BROKEN {s.Path} -> {s.TargetPath}");
238270
Console.WriteLine($"# {broken.Count} broken of {shortcuts.Count} total");
239271
if (a.HasFlag("delete") || a.HasFlag("recycle"))
@@ -251,6 +283,15 @@ private static async Task<int> CmdDuplicatesAsync(ParsedArgs a, CancellationToke
251283
: new[] { Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) };
252284
var finder = new DuplicateFinder();
253285
var groups = await finder.FindAsync(roots, progress: new Progress<string>(Console.Error.WriteLine), ct: ct);
286+
287+
var exportPath = a.GetOption("export");
288+
if (exportPath != null)
289+
{
290+
GridExporter.ExportDuplicates(groups, exportPath, ParseExportFormat(a));
291+
Console.WriteLine($"Exported {groups.Count} groups to {exportPath}");
292+
return 0;
293+
}
294+
254295
foreach (var g in groups)
255296
{
256297
Console.WriteLine($"[{FormatBytes(g.WastedBytes)} wasted, {g.Paths.Count} copies @ {FormatBytes(g.FileSize)}]");
@@ -413,6 +454,12 @@ private static string FormatBytes(long bytes)
413454
return $"{b,6:F1} {u[i]}";
414455
}
415456

457+
private static ExportFormat ParseExportFormat(ParsedArgs a)
458+
{
459+
var fmt = a.GetOption("format")?.ToLowerInvariant();
460+
return fmt == "json" ? ExportFormat.Json : ExportFormat.Csv;
461+
}
462+
416463
private static int Fail(string msg) { Console.Error.WriteLine(msg); return 2; }
417464

418465
private static bool IsHelp(string a) => a is "--help" or "-h" or "help" or "/?";
@@ -428,10 +475,10 @@ private static void PrintHelp()
428475
Console.WriteLine(" uninstall <name> [--silent] Uninstall a program");
429476
Console.WriteLine(" clean [junk|evidence ...] [--dry-run] [--secure]");
430477
Console.WriteLine(" repair <sfc|dism-scan|dism-restore|dism-cleanup|dism-resetbase|chkdsk|fontcache|iconcache>");
431-
Console.WriteLine(" drivers [--old] List third-party drivers in DriverStore");
432-
Console.WriteLine(" startup-impact Show boot-time cost per process");
433-
Console.WriteLine(" shortcuts [--recycle] Scan Desktop/Start Menu for broken .lnk");
434-
Console.WriteLine(" duplicates [roots...] Find duplicate files");
478+
Console.WriteLine(" drivers [--old] [--export file --format csv|json]");
479+
Console.WriteLine(" startup-impact [--export file --format csv|json]");
480+
Console.WriteLine(" shortcuts [--recycle] [--all] [--export file --format csv|json]");
481+
Console.WriteLine(" duplicates [roots...] [--export file --format csv|json]");
435482
Console.WriteLine(" snapshot trace <name> <installer> [--args \"...\"]");
436483
Console.WriteLine(" winapp2 <path.ini> [--dry-run] Run community cleaner definitions");
437484
Console.WriteLine(" schedule list");
@@ -468,7 +515,7 @@ public sealed class ParsedArgs
468515
// when you add a new command that needs them.
469516
private static readonly HashSet<string> ValueOptions = new(StringComparer.OrdinalIgnoreCase)
470517
{
471-
"name", "freq", "time", "day", "args",
518+
"name", "freq", "time", "day", "args", "export", "format",
472519
};
473520

474521
public bool HasFlag(string name) => Flags.Contains(name);
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System.Text;
2+
using System.Text.Json;
3+
using DeepPurge.Core.Drivers;
4+
using DeepPurge.Core.FileSystem;
5+
using DeepPurge.Core.Shortcuts;
6+
using DeepPurge.Core.Startup;
7+
8+
namespace DeepPurge.Core.Export;
9+
10+
public static class GridExporter
11+
{
12+
private static readonly JsonSerializerOptions JsonOpts = new() { WriteIndented = true };
13+
14+
public static string ExportDrivers(IEnumerable<DriverPackage> items, string filePath, ExportFormat format)
15+
{
16+
var list = items.ToList();
17+
if (format == ExportFormat.Json)
18+
{
19+
var data = list.Select(d => new
20+
{
21+
d.PublishedName, d.OriginalName, d.ProviderName, d.ClassName,
22+
d.DriverVersion, DriverDate = d.DriverDate?.ToString("yyyy-MM-dd"),
23+
SizeBytes = d.SizeBytes, SizeMB = Math.Round(d.SizeBytes / 1048576.0, 2),
24+
d.IsOldVersion
25+
});
26+
File.WriteAllText(filePath, JsonSerializer.Serialize(data, JsonOpts), Encoding.UTF8);
27+
}
28+
else
29+
{
30+
var sb = new StringBuilder();
31+
sb.AppendLine("\"Published Name\",\"Original Name\",\"Provider\",\"Class\",\"Version\",\"Date\",\"Size (MB)\",\"Old Version\"");
32+
foreach (var d in list)
33+
sb.AppendLine($"\"{Esc(d.PublishedName)}\",\"{Esc(d.OriginalName)}\",\"{Esc(d.ProviderName)}\",\"{Esc(d.ClassName)}\",\"{Esc(d.DriverVersion)}\",\"{d.DriverDate:yyyy-MM-dd}\",\"{Math.Round(d.SizeBytes / 1048576.0, 2)}\",\"{d.IsOldVersion}\"");
34+
File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8);
35+
}
36+
return filePath;
37+
}
38+
39+
public static string ExportShortcuts(IEnumerable<ShortcutEntry> items, string filePath, ExportFormat format)
40+
{
41+
var list = items.ToList();
42+
if (format == ExportFormat.Json)
43+
{
44+
var data = list.Select(s => new
45+
{
46+
s.Path, s.TargetPath, Status = s.Status.ToString(),
47+
s.Arguments, s.WorkingDir, s.Description, s.SizeBytes
48+
});
49+
File.WriteAllText(filePath, JsonSerializer.Serialize(data, JsonOpts), Encoding.UTF8);
50+
}
51+
else
52+
{
53+
var sb = new StringBuilder();
54+
sb.AppendLine("\"Shortcut Path\",\"Target\",\"Status\",\"Arguments\",\"Working Dir\",\"Size (bytes)\"");
55+
foreach (var s in list)
56+
sb.AppendLine($"\"{Esc(s.Path)}\",\"{Esc(s.TargetPath)}\",\"{s.Status}\",\"{Esc(s.Arguments)}\",\"{Esc(s.WorkingDir)}\",\"{s.SizeBytes}\"");
57+
File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8);
58+
}
59+
return filePath;
60+
}
61+
62+
public static string ExportDuplicates(IEnumerable<DuplicateGroup> items, string filePath, ExportFormat format)
63+
{
64+
var list = items.ToList();
65+
if (format == ExportFormat.Json)
66+
{
67+
var data = list.Select(g => new
68+
{
69+
g.FileSize, g.WastedBytes, FileCount = g.Paths.Count, g.Paths
70+
});
71+
File.WriteAllText(filePath, JsonSerializer.Serialize(data, JsonOpts), Encoding.UTF8);
72+
}
73+
else
74+
{
75+
var sb = new StringBuilder();
76+
sb.AppendLine("\"Group\",\"File Size\",\"Wasted Bytes\",\"Path\"");
77+
for (int i = 0; i < list.Count; i++)
78+
{
79+
var g = list[i];
80+
foreach (var p in g.Paths)
81+
sb.AppendLine($"\"{i + 1}\",\"{g.FileSize}\",\"{g.WastedBytes}\",\"{Esc(p)}\"");
82+
}
83+
File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8);
84+
}
85+
return filePath;
86+
}
87+
88+
public static string ExportStartupImpact(IEnumerable<StartupImpactEntry> items, string filePath, ExportFormat format)
89+
{
90+
var list = items.ToList();
91+
if (format == ExportFormat.Json)
92+
{
93+
var data = list.Select(e => new
94+
{
95+
e.ProcessName, e.CommandLine, e.ImagePath,
96+
e.DiskBytes, e.CpuMs, Impact = e.Impact.ToString(),
97+
SampleTime = e.SampleTime.ToString("o")
98+
});
99+
File.WriteAllText(filePath, JsonSerializer.Serialize(data, JsonOpts), Encoding.UTF8);
100+
}
101+
else
102+
{
103+
var sb = new StringBuilder();
104+
sb.AppendLine("\"Process\",\"Command Line\",\"Image Path\",\"Disk (bytes)\",\"CPU (ms)\",\"Impact\"");
105+
foreach (var e in list)
106+
sb.AppendLine($"\"{Esc(e.ProcessName)}\",\"{Esc(e.CommandLine)}\",\"{Esc(e.ImagePath)}\",\"{e.DiskBytes}\",\"{e.CpuMs}\",\"{e.Impact}\"");
107+
File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8);
108+
}
109+
return filePath;
110+
}
111+
112+
private static string Esc(string s) => (s ?? "").Replace("\"", "\"\"");
113+
}
114+
115+
public enum ExportFormat { Csv, Json }

0 commit comments

Comments
 (0)