package main import ( "embed" "encoding/json" "fmt" "io" "io/fs" "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/platform" "github.com/kv/clearnup/backend/internal/scanner" ) //go:embed all:dist var distFS embed.FS 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) http.HandleFunc("/api/apps/uninstall", handleAppUninstall) // Static File Serving (SPA Support) // Check if we are running with embedded files or local dist // Priority: Embedded (Production) -> Local dist (Dev/Preview) // Try to get a sub-fs for "dist" from the embedded FS distRoot, err := fs.Sub(distFS, "dist") if err == nil { fmt.Println("📂 Serving embedded static files") // Check if it's actually populated (sanity check) if _, err := distRoot.Open("index.html"); err == nil { fsrv := http.FileServer(http.FS(distRoot)) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if filepath.Ext(r.URL.Path) == "" { // Read index.html from embedded index, _ := distRoot.Open("index.html") stat, _ := index.Stat() http.ServeContent(w, r, "index.html", stat.ModTime(), index.(io.ReadSeeker)) return } fsrv.ServeHTTP(w, r) }) } else { // Fallback to local ./dist if embedded is empty (e.g. dev mode without build) if _, err := os.Stat("dist"); err == nil { fmt.Println("📂 Serving static files from local ./dist") fs := http.FileServer(http.Dir("dist")) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if filepath.Ext(r.URL.Path) == "" { http.ServeFile(w, r, "dist/index.html") return } fs.ServeHTTP(w, r) }) } } } fmt.Printf("🚀 Antigravity Backend running on http://localhost%s\n", Port) // Open Browser if not in development mode if os.Getenv("APP_ENV") != "development" { go platform.OpenBrowser("http://localhost" + 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 } targets := scanner.GetScanTargets(req.Category) if len(targets) == 0 { json.NewEncoder(w).Encode([]scanner.ScanResult{}) return } var allResults []scanner.ScanResult for _, t := range targets { if t == "" { continue } 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 } if err := platform.OpenSettings(); err != nil { fmt.Printf("Failed to open settings: %v\n", err) } 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 } w.Header().Set("Content-Type", "application/json") 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 } if err := platform.EmptyTrash(); err != nil { http.Error(w, fmt.Sprintf("Cannot empty trash: %v", err), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(map[string]bool{"success": true}) } func handleClearCache(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } cachePath, err := platform.GetCachePath() if err != nil { http.Error(w, "Cannot get cache path", http.StatusInternalServerError) return } // Get size before clearing sizeBefore := scanner.GetDirectorySize(cachePath) // Clear cache directories (keep the Caches folder itself if possible, or jus remove content) 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 } dockerPath, err := platform.GetDockerPath() if err != nil { json.NewEncoder(w).Encode(map[string]interface{}{ "cleared": 0, "message": "Docker not found", }) return } // Run docker system prune -af --volumes to clean images, containers, and volumes cmd := exec.Command(dockerPath, "system", "prune", "-af", "--volumes") 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 } info, err := platform.GetSystemInfo() if err != nil { http.Error(w, "Failed to get system info", http.StatusInternalServerError) return } json.NewEncoder(w).Encode(info) } 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) } func handleAppDetails(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } type AppDetailsRequest struct { Path string `json:"path"` BundleID string `json:"bundleID"` } 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, req.BundleID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(details) } func handleAppAction(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } var req struct { Files []string `json:"files"` } 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}) } func handleAppUninstall(w http.ResponseWriter, r *http.Request) { enableCors(&w) if r.Method == "OPTIONS" { return } var req struct { Cmd string `json:"cmd"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if err := apps.RunUninstaller(req.Cmd); err != nil { http.Error(w, fmt.Sprintf("Failed to launch uninstaller: %s", err), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]bool{"success": true}) }