package main import ( "encoding/json" "fmt" "net/http" "os" "os/exec" "path/filepath" "sort" "github.com/kv/clearnup/backend/internal/apps" "github.com/kv/clearnup/backend/internal/cleaner" "github.com/kv/clearnup/backend/internal/scanner" ) const Port = ":36969" func enableCors(w *http.ResponseWriter) { (*w).Header().Set("Access-Control-Allow-Origin", "*") (*w).Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") (*w).Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") } func main() { http.HandleFunc("/api/disk-usage", handleDiskUsage) http.HandleFunc("/api/scan/user", handleScanUser) http.HandleFunc("/api/scan/system", handleScanSystem) // Detailed list http.HandleFunc("/api/scan/sizes", handleScanSizes) // Fast summary http.HandleFunc("/api/scan/deepest", handleDeepestScan) http.HandleFunc("/api/scan/category", handleScanCategory) http.HandleFunc("/api/purge", handlePurge) http.HandleFunc("/api/empty-trash", handleEmptyTrash) http.HandleFunc("/api/clear-cache", handleClearCache) http.HandleFunc("/api/clean-docker", handleCleanDocker) http.HandleFunc("/api/system-info", handleSystemInfo) http.HandleFunc("/api/estimates", handleCleaningEstimates) // App Uninstaller http.HandleFunc("/api/apps", handleScanApps) http.HandleFunc("/api/apps/details", handleAppDetails) http.HandleFunc("/api/apps/action", handleAppAction) fmt.Printf("🚀 Antigravity Backend running on http://localhost%s\n", Port) if err := http.ListenAndServe(Port, nil); err != nil { fmt.Printf("Server failed: %s\n", err) } } type ScanRequest struct { Category string `json:"category"` // "apps", "photos", "icloud", "docs", "system" } func handleScanCategory(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } var req ScanRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid body", http.StatusBadRequest) return } home, _ := os.UserHomeDir() var targets []string switch req.Category { case "apps": targets = []string{"/Applications", filepath.Join(home, "Applications")} case "photos": targets = []string{filepath.Join(home, "Pictures")} case "icloud": targets = []string{filepath.Join(home, "Library", "Mobile Documents")} case "docs": targets = []string{filepath.Join(home, "Documents")} case "downloads": targets = []string{filepath.Join(home, "Downloads")} case "desktop": targets = []string{filepath.Join(home, "Desktop")} case "music": targets = []string{filepath.Join(home, "Music")} case "movies": targets = []string{filepath.Join(home, "Movies")} case "system": targets = []string{filepath.Join(home, "Library", "Caches"), filepath.Join(home, "Library", "Logs"), filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData")} default: json.NewEncoder(w).Encode([]scanner.ScanResult{}) return } // Reuse ScanPath logic inline or call a helper // We'll just do a quick loop here since ScanPath in scanner.go was defined but I need to link it // Actually I put ScanPath in scanner.go as FindLargeFiles wrapper. var allResults []scanner.ScanResult for _, t := range targets { res, _ := scanner.FindLargeFiles(t, 10*1024*1024) // 10MB allResults = append(allResults, res...) } // Sort sort.Slice(allResults, func(i, j int) bool { return allResults[i].Size > allResults[j].Size }) if len(allResults) > 50 { allResults = allResults[:50] } json.NewEncoder(w).Encode(allResults) } func handleOpenSettings(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } // Open Storage Settings // macOS Ventura+: open x-apple.systempreferences:com.apple.settings.Storage exec.Command("open", "x-apple.systempreferences:com.apple.settings.Storage").Run() w.WriteHeader(http.StatusOK) } func handleDiskUsage(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } usage, err := scanner.GetDiskUsage() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(usage) } func handleScanUser(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } files, err := scanner.ScanUserDocuments() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(files) } func handleScanSizes(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } sizes, err := scanner.GetCategorySizes() if err != nil { // Log but return empty fmt.Println("Size scan error:", err) json.NewEncoder(w).Encode(map[string]int64{}) return } json.NewEncoder(w).Encode(sizes) } func handleScanSystem(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } files, err := scanner.ScanSystemData() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(files) } func handleDeepestScan(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } // Default to Documents for now, or parse body for path home, _ := os.UserHomeDir() target := filepath.Join(home, "Documents") folders, err := scanner.FindHeavyFolders(target) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(folders) } type PurgeRequest struct { Path string `json:"path"` } func handlePurge(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } var req PurgeRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if err := cleaner.PurgePath(req.Path); err != nil { http.Error(w, fmt.Sprintf("Failed to purge: %s", err), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]bool{"success": true}) } func handleEmptyTrash(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } home, err := os.UserHomeDir() if err != nil { http.Error(w, "Cannot get home directory", http.StatusInternalServerError) return } trashPath := filepath.Join(home, ".Trash") // Get all items in trash and delete them entries, err := os.ReadDir(trashPath) if err != nil { http.Error(w, "Cannot read trash", http.StatusInternalServerError) return } for _, entry := range entries { itemPath := filepath.Join(trashPath, entry.Name()) os.RemoveAll(itemPath) } json.NewEncoder(w).Encode(map[string]bool{"success": true}) } func handleClearCache(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } home, _ := os.UserHomeDir() cachePath := filepath.Join(home, "Library", "Caches") // Get size before clearing sizeBefore := scanner.GetDirectorySize(cachePath) // Clear cache directories (keep the Caches folder itself) entries, err := os.ReadDir(cachePath) if err != nil { http.Error(w, "Cannot read cache directory", http.StatusInternalServerError) return } for _, entry := range entries { itemPath := filepath.Join(cachePath, entry.Name()) os.RemoveAll(itemPath) } json.NewEncoder(w).Encode(map[string]int64{"cleared": sizeBefore}) } func handleCleanDocker(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } // Try to find docker executable dockerPath, err := exec.LookPath("docker") if err != nil { // Try common locations commonPaths := []string{ "/usr/local/bin/docker", "/opt/homebrew/bin/docker", "/Applications/Docker.app/Contents/Resources/bin/docker", } for _, p := range commonPaths { if _, e := os.Stat(p); e == nil { dockerPath = p break } } } if dockerPath == "" { json.NewEncoder(w).Encode(map[string]interface{}{ "cleared": 0, "message": "Docker not found in PATH or common locations", }) return } // Run docker system prune -af cmd := exec.Command(dockerPath, "system", "prune", "-af") output, err := cmd.CombinedOutput() if err != nil { json.NewEncoder(w).Encode(map[string]interface{}{ "cleared": 0, "message": fmt.Sprintf("Docker cleanup failed: %s", err), }) return } json.NewEncoder(w).Encode(map[string]interface{}{ "cleared": 1, "message": string(output), }) } func handleSystemInfo(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } // Structs for parsing system_profiler JSON type HardwareItem struct { MachineName string `json:"machine_name"` ChipType string `json:"chip_type"` PhysicalMemory string `json:"physical_memory"` } type SoftwareItem struct { OSVersion string `json:"os_version"` } type SystemProfile struct { Hardware []HardwareItem `json:"SPHardwareDataType"` Software []SoftwareItem `json:"SPSoftwareDataType"` } cmd := exec.Command("system_profiler", "SPHardwareDataType", "SPSoftwareDataType", "-json") output, err := cmd.Output() if err != nil { http.Error(w, "Failed to get system info", http.StatusInternalServerError) return } var profile SystemProfile if err := json.Unmarshal(output, &profile); err != nil { http.Error(w, "Failed to parse system info", http.StatusInternalServerError) return } response := map[string]string{ "model": "Unknown", "chip": "Unknown", "memory": "Unknown", "os": "Unknown", } if len(profile.Hardware) > 0 { response["model"] = profile.Hardware[0].MachineName response["chip"] = profile.Hardware[0].ChipType response["memory"] = profile.Hardware[0].PhysicalMemory } if len(profile.Software) > 0 { response["os"] = profile.Software[0].OSVersion } json.NewEncoder(w).Encode(response) } func handleCleaningEstimates(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } estimates, err := scanner.GetCleaningEstimates() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(estimates) } // App Uninstaller Handlers func handleScanApps(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } appsList, err := apps.ScanApps() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(appsList) } type AppDetailsRequest struct { Path string `json:"path"` } func handleAppDetails(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } var req AppDetailsRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } details, err := apps.GetAppDetails(req.Path) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(details) } type AppActionRequest struct { Files []string `json:"files"` } func handleAppAction(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } var req AppActionRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if err := apps.DeleteFiles(req.Files); err != nil { http.Error(w, fmt.Sprintf("Failed to delete files: %s", err), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]bool{"success": true}) }