using System.IO; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L9-3: 시스템 정리 핸들러. "clean" 프리픽스로 사용합니다. /// /// 예: clean → 정리 가능한 항목 목록 + 예상 용량 /// clean temp → Windows 임시 파일 정리 (%TEMP%) /// clean recycle → 휴지통 비우기 /// clean downloads → 다운로드 폴더 오래된 파일 목록 (30일 이상) /// clean logs → 앱 로그 폴더 정리 (%APPDATA%\AxCopilot\logs) /// clean all → temp + recycle + logs 한 번에 정리 /// public class CleanHandler : IActionHandler { public string? Prefix => "clean"; public PluginMetadata Metadata => new( "Clean", "시스템 정리 — 임시 파일 · 휴지통 · 다운로드 · 로그", "1.0", "AX"); public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim().ToLowerInvariant(); var items = new List(); if (string.IsNullOrWhiteSpace(q)) { // 각 영역 크기 미리보기 var tempSize = GetDirSize(Path.GetTempPath()); var recycleSize = GetRecycleBinSize(); var downloadsSize = GetOldDownloadsSize(30); var logSize = GetDirSize(GetAppLogPath()); var total = tempSize + recycleSize + downloadsSize + logSize; items.Add(new LauncherItem( $"정리 가능 {FormatBytes(total)}", "항목을 선택하거나 clean all 로 모두 정리", null, null, Symbol: "\uE74D")); items.Add(MakeCleanItem("temp", "\uE8B6", "임시 파일 (%TEMP%)", tempSize)); items.Add(MakeCleanItem("recycle", "\uE74D", "휴지통", recycleSize)); items.Add(MakeCleanItem("downloads", "\uE896", "다운로드 (30일 이상)", downloadsSize)); items.Add(MakeCleanItem("logs", "\uE9D9", "AxCopilot 로그 파일", logSize)); items.Add(new LauncherItem( "clean all", $"temp + recycle + logs 한 번에 정리 ({FormatBytes(tempSize + recycleSize + logSize)})", null, ("clean_all", ""), Symbol: "\uE74D")); return Task.FromResult>(items); } switch (q) { case "temp": { var tempPath = Path.GetTempPath(); var size = GetDirSize(tempPath); var count = SafeCountFiles(tempPath); items.Add(new LauncherItem( $"임시 파일 정리 {FormatBytes(size)}", $"{count}개 파일 · {tempPath}", null, ("clean_temp", tempPath), Symbol: "\uE8B6")); break; } case "recycle": { var size = GetRecycleBinSize(); items.Add(new LauncherItem( $"휴지통 비우기 {FormatBytes(size)}", "복구 불가능합니다. Enter로 실행", null, ("clean_recycle", ""), Symbol: "\uE74D")); break; } case "downloads": { var downloadsPath = Environment.GetFolderPath( Environment.SpecialFolder.UserProfile); downloadsPath = Path.Combine(downloadsPath, "Downloads"); var oldFiles = GetOldFiles(downloadsPath, 30); var totalSz = oldFiles.Sum(f => f.Length); items.Add(new LauncherItem( $"다운로드 30일 이상 파일 {FormatBytes(totalSz)}", $"{oldFiles.Count}개 파일", null, ("list_downloads", downloadsPath), Symbol: "\uE896")); foreach (var f in oldFiles.Take(15)) { items.Add(new LauncherItem( f.Name, $"{FormatBytes(f.Length)} · {f.LastWriteTime:MM-dd HH:mm}", null, ("open_file", f.FullName), Symbol: "\uE8A5")); } break; } case "logs": { var logPath = GetAppLogPath(); var size = GetDirSize(logPath); var count = SafeCountFiles(logPath); items.Add(new LauncherItem( $"AxCopilot 로그 정리 {FormatBytes(size)}", $"{count}개 파일 · {logPath}", null, ("clean_logs", logPath), Symbol: "\uE9D9")); break; } case "all": { var tempSz = GetDirSize(Path.GetTempPath()); var recycleSz = GetRecycleBinSize(); var logSz = GetDirSize(GetAppLogPath()); items.Add(new LauncherItem( $"모두 정리 {FormatBytes(tempSz + recycleSz + logSz)}", "temp + recycle + logs · Enter로 실행", null, ("clean_all", ""), Symbol: "\uE74D")); break; } default: items.Add(new LauncherItem("서브커맨드", "temp · recycle · downloads · logs · all", null, null, Symbol: "\uE946")); break; } return Task.FromResult>(items); } public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) { switch (item.Data) { case ("clean_temp", string path): await Task.Run(() => { var deleted = CleanDirectory(path, recursive: false); NotificationService.Notify("정리 완료", $"임시 파일 {deleted}개 삭제"); }, ct); break; case ("clean_recycle", _): await Task.Run(() => { EmptyRecycleBin(); NotificationService.Notify("정리 완료", "휴지통을 비웠습니다."); }, ct); break; case ("clean_logs", string path): await Task.Run(() => { var deleted = CleanDirectory(path, recursive: true); NotificationService.Notify("정리 완료", $"로그 파일 {deleted}개 삭제"); }, ct); break; case ("clean_all", _): await Task.Run(() => { var t1 = CleanDirectory(Path.GetTempPath(), recursive: false); EmptyRecycleBin(); var t2 = CleanDirectory(GetAppLogPath(), recursive: true); NotificationService.Notify("모두 정리 완료", $"임시 {t1}개, 로그 {t2}개 삭제, 휴지통 비움"); }, ct); break; case ("list_downloads", string path): try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true, }); } catch { /* 비핵심 */ } break; case ("open_file", string filePath): try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = filePath, UseShellExecute = true, }); } catch { /* 비핵심 */ } break; } } // ── 헬퍼 ──────────────────────────────────────────────────────────────── private static LauncherItem MakeCleanItem(string sub, string icon, string label, long size) => new( $"clean {sub}", $"{label} · {FormatBytes(size)}", null, ($"clean_{sub}", ""), Symbol: icon); private static long GetDirSize(string path) { if (!Directory.Exists(path)) return 0; try { return Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories) .Sum(f => { try { return new FileInfo(f).Length; } catch { return 0; } }); } catch { return 0; } } private static long GetRecycleBinSize() { try { // SHQueryRecycleBin P/Invoke 대신 Shell32 통해 추정 // 간단히 0 반환 (실제 크기는 SHQueryRecycleBinW P/Invoke 필요) return 0; } catch { return 0; } } private static long GetOldDownloadsSize(int olderThanDays) { try { var path = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads"); return GetOldFiles(path, olderThanDays).Sum(f => f.Length); } catch { return 0; } } private static List GetOldFiles(string dir, int olderThanDays) { if (!Directory.Exists(dir)) return []; var cutoff = DateTime.Now.AddDays(-olderThanDays); try { return Directory.EnumerateFiles(dir) .Select(f => new FileInfo(f)) .Where(fi => fi.LastWriteTime < cutoff) .OrderByDescending(fi => fi.Length) .ToList(); } catch { return []; } } private static int SafeCountFiles(string path) { if (!Directory.Exists(path)) return 0; try { return Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories).Count(); } catch { return 0; } } private static int CleanDirectory(string path, bool recursive) { if (!Directory.Exists(path)) return 0; int deleted = 0; var opts = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; foreach (var file in Directory.EnumerateFiles(path, "*", opts)) { try { File.Delete(file); deleted++; } catch { /* 잠긴 파일 무시 */ } } return deleted; } [System.Runtime.InteropServices.DllImport("shell32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)] private static extern int SHEmptyRecycleBin(IntPtr hwnd, string? pszRootPath, uint dwFlags); private static void EmptyRecycleBin() { try { // SHERB_NOCONFIRMATION=0x1, SHERB_NOPROGRESSUI=0x2, SHERB_NOSOUND=0x4 SHEmptyRecycleBin(IntPtr.Zero, null, 0x07); } catch { /* 비핵심 */ } } private static string GetAppLogPath() => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "logs"); private static string FormatBytes(long bytes) => bytes switch { >= 1024L * 1024 * 1024 => $"{bytes / 1024.0 / 1024 / 1024:F1} GB", >= 1024L * 1024 => $"{bytes / 1024.0 / 1024:F1} MB", >= 1024L => $"{bytes / 1024.0:F0} KB", _ => $"{bytes} B", }; }