kv-clearnup/backend/internal/apps/apps_darwin.go
2026-02-02 08:33:46 +07:00

206 lines
4.7 KiB
Go

//go:build darwin
package apps
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
)
// Structs moved to apps_common.go
// ScanApps returns a list of installed applications
func ScanApps() ([]AppInfo, error) {
// Scan /Applications and ~/Applications
home, _ := os.UserHomeDir()
dirs := []string{
"/Applications",
filepath.Join(home, "Applications"),
}
var apps []AppInfo
var mu sync.Mutex
var wg sync.WaitGroup
for _, dir := range dirs {
entries, err := os.ReadDir(dir)
if err != nil {
continue
}
for _, entry := range entries {
if strings.HasSuffix(entry.Name(), ".app") {
path := filepath.Join(dir, entry.Name())
wg.Add(1)
go func(p, name string) {
defer wg.Done()
// Get Bundle ID
bid := getBundleID(p)
if bid == "" {
return // Skip if no bundle ID (system util or broken)
}
// Get Size (fast estimate)
// using du -s -k
size := getDirSize(p)
mu.Lock()
apps = append(apps, AppInfo{
Name: strings.TrimSuffix(name, ".app"),
Path: p,
BundleID: bid,
Size: size,
})
mu.Unlock()
}(path, entry.Name())
}
}
}
wg.Wait()
return apps, nil
}
// GetAppDetails finds all associated files for a given app path
func GetAppDetails(appPath, _ string) (*AppDetails, error) {
bid := getBundleID(appPath)
if bid == "" {
return nil, fmt.Errorf("could not determine bundle ID")
}
appSize := getDirSize(appPath)
details := &AppDetails{
AppInfo: AppInfo{
Name: filepath.Base(appPath), // simplified
Path: appPath,
BundleID: bid,
Size: appSize,
},
TotalSize: appSize,
}
home, _ := os.UserHomeDir()
library := filepath.Join(home, "Library")
// Common locations to search for Bundle ID
// Name-based search fallback is risky, staying strict to Bundle ID for now
locations := map[string]string{
"Application Support": filepath.Join(library, "Application Support"),
"Caches": filepath.Join(library, "Caches"),
"Preferences": filepath.Join(library, "Preferences"),
"Saved Application State": filepath.Join(library, "Saved Application State"),
"Logs": filepath.Join(library, "Logs"),
"Cookies": filepath.Join(library, "Cookies"),
"Containers": filepath.Join(library, "Containers"), // Sandboxed data
}
for locName, locPath := range locations {
// 1. Direct match: path/BundleID
target := filepath.Join(locPath, bid)
if exists(target) {
size := getDirSize(target)
details.Associated = append(details.Associated, AssociatedFile{
Path: target,
Type: getType(locName),
Size: size,
})
details.TotalSize += size
}
// 2. Preferences often use plist extension: path/BundleID.plist
if locName == "Preferences" {
plistPath := filepath.Join(locPath, bid+".plist")
if exists(plistPath) {
size := getFileSize(plistPath)
details.Associated = append(details.Associated, AssociatedFile{
Path: plistPath,
Type: "config",
Size: size,
})
details.TotalSize += size
}
}
}
return details, nil
}
// DeleteFiles removes the requested paths
func DeleteFiles(paths []string) error {
for _, p := range paths {
// Basic safety check: don't delete root or critical system paths
// Real implementation needs robust safeguards
if p == "/" || p == "/Applications" || p == "/System" || p == "/Library" {
continue // Skip dangerous paths
}
if err := os.RemoveAll(p); err != nil {
return err
}
}
return nil
}
// Helpers
func getBundleID(path string) string {
// mdls -name kMDItemCFBundleIdentifier -r /path/to/app
cmd := exec.Command("mdls", "-name", "kMDItemCFBundleIdentifier", "-r", path)
out, err := cmd.Output()
if err != nil {
return ""
}
res := strings.TrimSpace(string(out))
if res == "(null)" {
return ""
}
return res
}
func getDirSize(path string) int64 {
cmd := exec.Command("du", "-s", "-k", path)
out, err := cmd.Output()
if err != nil {
return 0
}
parts := strings.Fields(string(out))
if len(parts) > 0 {
var s int64
fmt.Sscanf(parts[0], "%d", &s)
return s * 1024
}
return 0
}
func getFileSize(path string) int64 {
info, err := os.Stat(path)
if err != nil {
return 0
}
return info.Size()
}
func exists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func getType(locName string) string {
switch locName {
case "Caches":
return "cache"
case "Preferences", "Cookies":
return "config"
case "Logs":
return "log"
default:
return "data"
}
}
// RunUninstaller executes the uninstall command (Not implemented on Mac yet)
func RunUninstaller(cmdString string) error {
return fmt.Errorf("uninstall not supported on macOS yet")
}