206 lines
4.7 KiB
Go
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")
|
|
}
|