//go:build windows package scanner import ( "fmt" "os" "path/filepath" "sort" "strings" "sync" "syscall" "unsafe" // Added missing import ) // GetDiskUsage using GetDiskFreeSpaceExW // GetDiskUsage returns usage for all fixed drives func GetDiskUsage() ([]*DiskUsage, error) { kernel32 := syscall.NewLazyDLL("kernel32.dll") getDiskFreeSpaceEx := kernel32.NewProc("GetDiskFreeSpaceExW") getLogicalDrives := kernel32.NewProc("GetLogicalDrives") var usages []*DiskUsage // Get logical drives bitmask ret, _, _ := getLogicalDrives.Call() if ret == 0 { return nil, fmt.Errorf("GetLogicalDrives failed") } drivesBitmask := uint32(ret) toGB := func(bytes int64) string { gb := float64(bytes) / 1024 / 1024 / 1024 return fmt.Sprintf("%.2f", gb) } for i := 0; i < 26; i++ { if drivesBitmask&(1< 0 { usedBytes := totalNumberOfBytes - totalNumberOfFreeBytes usages = append(usages, &DiskUsage{ Name: fmt.Sprintf("Local Disk (%s:)", driveLetter), TotalGB: toGB(totalNumberOfBytes), UsedGB: toGB(usedBytes), FreeGB: toGB(totalNumberOfFreeBytes), }) } } } return usages, nil } // GetDirectorySize walks the directory to calculate size (Windows doesn't have `du`) func GetDirectorySize(path string) int64 { var size int64 filepath.WalkDir(path, func(_ string, d os.DirEntry, err error) error { if err != nil { return nil } if !d.IsDir() { info, err := d.Info() if err == nil { size += info.Size() } } return nil }) return size } // FindHeavyFolders finds large directories func FindHeavyFolders(root string) ([]ScanResult, error) { // Basic implementation: Walk max 2 levels deep and calculate sizes var results []ScanResult // depth 0 = root // depth 1 = children of root // depth 2 = children of children entries, err := os.ReadDir(root) if err != nil { return nil, err } var wg sync.WaitGroup var mu sync.Mutex for _, entry := range entries { if entry.IsDir() { path := filepath.Join(root, entry.Name()) wg.Add(1) go func(p string) { defer wg.Done() s := GetDirectorySize(p) mu.Lock() results = append(results, ScanResult{ Path: p, Size: s, IsDirectory: true, }) mu.Unlock() }(path) } } wg.Wait() // Sort by size desc sort.Slice(results, func(i, j int) bool { return results[i].Size > results[j].Size }) if len(results) > 50 { return results[:50], nil } return results, nil } func ScanUserDocuments() ([]ScanResult, error) { home, err := os.UserHomeDir() if err != nil { return nil, err } targets := []string{ filepath.Join(home, "Documents"), filepath.Join(home, "Downloads"), filepath.Join(home, "Desktop"), } var allResults []ScanResult for _, t := range targets { res, _ := FindLargeFiles(t, 10*1024*1024) // 10MB allResults = append(allResults, res...) } sort.Slice(allResults, func(i, j int) bool { return allResults[i].Size > allResults[j].Size }) if len(allResults) > 50 { return allResults[:50], nil } return allResults, nil } func ScanSystemData() ([]ScanResult, error) { home, err := os.UserHomeDir() if err != nil { return nil, err } // Windows System/Temp locations // %Temp%, Prefetch (admin only, careful), AppData/Local/Temp targets := []string{ filepath.Join(home, "AppData", "Local", "Temp"), os.Getenv("TEMP"), // "C:\\Windows\\Temp", // Requires Admin, maybe skip for now or handle error } var allResults []ScanResult for _, t := range targets { if t == "" { continue } res, _ := FindLargeFiles(t, 10*1024*1024) allResults = append(allResults, res...) } sort.Slice(allResults, func(i, j int) bool { return allResults[i].Size > allResults[j].Size }) if len(allResults) > 50 { return allResults[:50], nil } return allResults, nil } func GetCategorySizes() (*CategorySizes, error) { home, err := os.UserHomeDir() if err != nil { return nil, err } docPath := filepath.Join(home, "Documents") downPath := filepath.Join(home, "Downloads") deskPath := filepath.Join(home, "Desktop") musicPath := filepath.Join(home, "Music") moviesPath := filepath.Join(home, "Videos") // Windows uses Videos photos := filepath.Join(home, "Pictures") // AppData is roughly Library localAppData := filepath.Join(home, "AppData", "Local") temp := filepath.Join(localAppData, "Temp") // Parallel fetch type result struct { name string size int64 } c := make(chan result) // Checks: docs, down, desk, music, movies, temp, photos, archives, vms, games, ai, docker, cache totalChecks := 13 check := func(name, p string) { c <- result{name, GetDirectorySize(p)} } go check("docs", docPath) go check("down", downPath) go check("desk", deskPath) go check("music", musicPath) go check("movies", moviesPath) // Temp is part of Cache now, but let's keep it separate or sum it up // System/Temp logic from before: go check("temp", temp) go check("photos", photos) // Scan specific common folders for Archives and VMs go func() { // Archives: Zip, Rar, 7z in Downloads and Documents size := ScanExtensions(downPath, []string{".zip", ".rar", ".7z", ".tar", ".gz", ".xz"}) size += ScanExtensions(docPath, []string{".zip", ".rar", ".7z", ".tar", ".gz", ".xz"}) c <- result{"archives", size} }() go func() { // VMs / Disk Images: ISO, VHDX, VMDK in Downloads and Documents size := ScanExtensions(downPath, []string{".iso", ".vdi", ".vmdk", ".qcow2", ".vhdx", ".img", ".dsk"}) size += ScanExtensions(docPath, []string{".iso", ".vdi", ".vmdk", ".qcow2", ".vhdx", ".img", ".dsk"}) c <- result{"vms", size} }() // Games go func() { var size int64 // Common Game Paths paths := []string{ `C:\Program Files (x86)\Steam\steamapps\common`, `C:\Program Files\Epic Games`, `C:\Program Files (x86)\Ubisoft\Ubisoft Game Launcher\games`, `C:\Program Files\EA Games`, filepath.Join(home, "AppData", "Roaming", ".minecraft"), } for _, p := range paths { size += GetDirectorySize(p) } c <- result{"games", size} }() // AI go func() { var size int64 // 1. Common Installation Paths paths := []string{ `C:\ComfyUI`, `C:\ai\ComfyUI`, filepath.Join(home, "ComfyUI"), filepath.Join(home, "stable-diffusion-webui"), filepath.Join(home, "webui"), // Common Model Caches filepath.Join(home, ".cache", "huggingface"), filepath.Join(home, ".ollama", "models"), filepath.Join(home, ".lmstudio", "models"), } for _, p := range paths { size += GetDirectorySize(p) } // 2. Loose Model Files (Deep Scan) // Look for .safetensors, .ckpt, .gguf, .pt, .pth, .bin, .onnx in Downloads and Documents aiExtensions := []string{".safetensors", ".ckpt", ".gguf", ".pt", ".pth", ".bin", ".onnx"} size += ScanExtensions(downPath, aiExtensions) size += ScanExtensions(docPath, aiExtensions) c <- result{"ai", size} }() // Docker go func() { var size int64 // Docker Desktop Default WSL Data dockerPath := filepath.Join(localAppData, "Docker", "wsl", "data", "ext4.vhdx") info, err := os.Stat(dockerPath) if err == nil { size = info.Size() } c <- result{"docker", size} }() // Cache (Browser + System Temp) go func() { var size int64 // System Temp size += GetDirectorySize(os.Getenv("TEMP")) // Chrome Cache size += GetDirectorySize(filepath.Join(localAppData, "Google", "Chrome", "User Data", "Default", "Cache")) // Edge Cache size += GetDirectorySize(filepath.Join(localAppData, "Microsoft", "Edge", "User Data", "Default", "Cache")) // Brave Cache size += GetDirectorySize(filepath.Join(localAppData, "BraveSoftware", "Brave-Browser", "User Data", "Default", "Cache")) // Opera Cache size += GetDirectorySize(filepath.Join(localAppData, "Opera Software", "Opera Stable", "Cache")) // Firefox Cache size += GetDirectorySize(filepath.Join(localAppData, "Mozilla", "Firefox", "Profiles")) // Scan all profiles for cache? Usually in Local/Mozilla/Firefox/Profiles//cache2 // Firefox requires walking profiles in LocalAppData mozPath := filepath.Join(localAppData, "Mozilla", "Firefox", "Profiles") entries, _ := os.ReadDir(mozPath) for _, e := range entries { if e.IsDir() { size += GetDirectorySize(filepath.Join(mozPath, e.Name(), "cache2")) } } c <- result{"cache", size} }() sizes := &CategorySizes{} for i := 0; i < totalChecks; i++ { res := <-c switch res.name { case "docs": sizes.Documents = res.size case "down": sizes.Downloads = res.size case "desk": sizes.Desktop = res.size case "music": sizes.Music = res.size case "movies": sizes.Movies = res.size case "temp": // Keeping legacy System field for now, maybe map to part of Cache or System logs? sizes.System = res.size case "photos": sizes.Photos = res.size case "archives": sizes.Archives = res.size case "vms": sizes.VirtualMachines = res.size case "games": sizes.Games = res.size case "ai": sizes.AI = res.size case "docker": sizes.Docker = res.size case "cache": sizes.Cache = res.size } } return sizes, nil } // ScanExtensions walks a directory and sums up size of files with matching extensions func ScanExtensions(root string, exts []string) int64 { var total int64 extMap := make(map[string]bool) for _, e := range exts { extMap[strings.ToLower(e)] = true } filepath.WalkDir(root, func(_ string, d os.DirEntry, err error) error { if err != nil { return nil } if !d.IsDir() { ext := strings.ToLower(filepath.Ext(d.Name())) if extMap[ext] { info, err := d.Info() if err == nil { total += info.Size() } } } return nil }) return total } func GetCleaningEstimates() (*CleaningEstimates, error) { home, err := os.UserHomeDir() if err != nil { return nil, err } // Flash Clean: Temp files temp := filepath.Join(home, "AppData", "Local", "Temp") // Deep Clean: Downloads downloads := filepath.Join(home, "Downloads") type result struct { name string size int64 } c := make(chan result) go func() { c <- result{"temp", GetDirectorySize(temp)} }() go func() { c <- result{"downloads", GetDirectorySize(downloads)} }() estimates := &CleaningEstimates{} for i := 0; i < 2; i++ { res := <-c switch res.name { case "temp": estimates.FlashEst = res.size case "downloads": estimates.DeepEst = res.size } } return estimates, nil }