diff --git a/spm/auto.go b/spm/auto.go index 92e574a..504fc36 100644 --- a/spm/auto.go +++ b/spm/auto.go @@ -1,261 +1,282 @@ -package spm - -import ( - "fmt" - "os" - "path/filepath" -) - -// pendingUpdates holds info about packages that have been downloaded/decompressed -// but not yet moved to the final install location, cuz Windows has this stupid file locking mechanism -var pendingUpdates []AppIndexEntry - -// AutoDownloadUpdates downloads the APPINDEX file, parses it, compares against -// currently installed packages, and if it finds a newer version, downloads -// and decompresses it into a temporary folder. The result is stored in pendingUpdates, so it can be used by AutoInstallUpdates(). -func AutoDownloadUpdates() error { - // 1) Download the APPINDEX file to a temporary location - appIndexPath := filepath.Join(os.TempDir(), "APPINDEX") - fmt.Println("[INFO] Starting APPINDEX download to:", appIndexPath) - err := DownloadAppIndex(appIndexPath) - if err != nil { - return fmt.Errorf("[ERROR] Failed to download APPINDEX: %w", err) - } - fmt.Println("[INFO] APPINDEX downloaded successfully") - - // 2) Parse the APPINDEX file - fmt.Println("[INFO] Parsing APPINDEX file:", appIndexPath) - entries, err := ParseAppIndex(appIndexPath) - if err != nil { - return fmt.Errorf("[ERROR] Failed to parse APPINDEX: %w", err) - } - fmt.Printf("[INFO] Parsed APPINDEX successfully, found %d entries\n", len(entries)) - - // 3) Load installed packages - fmt.Println("[INFO] Loading installed packages") - installDir, err := GetInstallDir() - if err != nil { - return err - } - fmt.Println("[INFO] Install directory:", installDir) - - installedPkgs, err := loadInstalledPackages(installDir) - if err != nil { - return fmt.Errorf("[ERROR] Failed to load installed packages: %w", err) - } - fmt.Printf("[INFO] Loaded %d installed packages\n", len(installedPkgs)) - - // 4) Process entries for installed packages only - for _, installed := range installedPkgs { - fmt.Printf("[INFO] Checking updates for installed package: %+v\n", installed) - - // Filter APPINDEX entries that match the installed package's attributes - var matchingEntry *AppIndexEntry - for _, entry := range entries { - if entry.Name == installed.Name && - entry.Release == installed.Release && - entry.Type == installed.Type && - entry.OS == installed.OS && - entry.Arch == installed.Arch { - matchingEntry = &entry - break - } - } - - if matchingEntry == nil { - fmt.Printf("[WARN] No matching APPINDEX entry found for installed package: %s (%s)\n", installed.Name, installed.Release) - continue - } - - fmt.Printf("[INFO] Found matching APPINDEX entry: %+v\n", *matchingEntry) - - // Determine if an update is needed - updateNeeded, err := IsUpdateNeeded(installDir, matchingEntry.Name, matchingEntry.Release, matchingEntry.Version, matchingEntry.Arch, matchingEntry.OS) - if err != nil { - return fmt.Errorf("[ERROR] Failed to check if update is needed for %s: %w", matchingEntry.Name, err) - } - - if !updateNeeded { - fmt.Printf("[INFO] No update needed for package '%s'\n", matchingEntry.Name) - continue - } - - // 5) Download the package into a temporary download folder - downloadDir := GetTempDir() - fmt.Printf("[INFO] Downloading package '%s' to temporary folder: %s\n", matchingEntry.Name, downloadDir) - - err = DownloadPackageFromAppIndex(appIndexPath, matchingEntry.Name, matchingEntry.Release, matchingEntry.Type, downloadDir) - if err != nil { - return fmt.Errorf("[ERROR] Failed to download package '%s': %w", matchingEntry.Name, err) - } - - fmt.Printf("[INFO] Package '%s' downloaded successfully to: %s\n", matchingEntry.Name, downloadDir) - - // 6) Decompress the package into another temp folder - fmt.Printf("[INFO] Decompressing package '%s'\n", matchingEntry.Name) - tempDir, err := DecompressPackage(downloadDir, matchingEntry.Name, matchingEntry.Arch, matchingEntry.OS, matchingEntry.Type, matchingEntry.Release, matchingEntry.Version) - if err != nil { - return fmt.Errorf("[ERROR] Failed to decompress package '%s': %w", matchingEntry.Name, err) - } - fmt.Printf("[INFO] Package '%s' decompressed successfully to: %s\n", matchingEntry.Name, tempDir) - - // 7) Store in pendingUpdates so that AutoInstallUpdates can finish the job - fmt.Printf("[INFO] Adding '%s' to pending updates\n", matchingEntry.Name) - pendingUpdates = append(pendingUpdates, AppIndexEntry{ - Name: matchingEntry.Name, - Version: matchingEntry.Version, - Release: matchingEntry.Release, - Arch: matchingEntry.Arch, - OS: matchingEntry.OS, - Type: matchingEntry.Type, - }) - } - - fmt.Println("[INFO] AutoDownloadUpdates completed successfully") - return nil -} - -// AutoInstallUpdates installs any packages that were downloaded and decompressed by AutoDownloadUpdates. -// It moves files from their temp directories to the final location and updates installed.ini. -func AutoInstallUpdates() error { - installDir, err := GetInstallDir() - if err != nil { - return err - } - - for _, entry := range pendingUpdates { - // 1) Construct the same .tar.gz name we used when decompressing - fileName := fmt.Sprintf("%s@%s@%s@%s@%s@%s", - entry.Name, // no 'packageName' - entry.Arch, // matches 'arch' - entry.OS, // matches 'os' - entry.Type, // matches 'type' - entry.Release, // matches 'release' - entry.Version, // matches 'version' - ) - - // 3) Combine with your global temp dir - tempBase := GetTempDir() // e.g. C:\Users\YourUser\AppData\Local\Temp\spm_temp_164326 - decompressedDir := filepath.Join(tempBase, fileName) - - // 4) Move files from that decompressedDir - fmt.Printf("[INFO] Installing %s from %s\n", entry.Name, decompressedDir) - err := MoveFilesToInstallDir(decompressedDir, installDir, entry.Type) - if err != nil { - return fmt.Errorf("failed to move files for %s: %w", entry.Name, err) - } - - // 5) Finalize - err = FinalizeInstall(entry.Name, entry.Release, entry.Version, entry.Arch, entry.OS) - if err != nil { - return fmt.Errorf("failed to finalize install for %s: %w", entry.Name, err) - } - } - - pendingUpdates = nil - return nil -} - -func AutoDownloadSpecified(specs []AppIndexEntry) error { - // 1) Download the APPINDEX file to a temporary location - appIndexPath := filepath.Join(os.TempDir(), "APPINDEX") - fmt.Println("[INFO] Starting APPINDEX download to:", appIndexPath) - if err := DownloadAppIndex(appIndexPath); err != nil { - return fmt.Errorf("[ERROR] Failed to download APPINDEX: %w", err) - } - fmt.Println("[INFO] APPINDEX downloaded successfully") - - // 2) Parse the APPINDEX file - fmt.Println("[INFO] Parsing APPINDEX file:", appIndexPath) - entries, err := ParseAppIndex(appIndexPath) - if err != nil { - return fmt.Errorf("[ERROR] Failed to parse APPINDEX: %w", err) - } - fmt.Printf("[INFO] Parsed APPINDEX successfully, found %d entries\n", len(entries)) - - // 3) Get install directory to check for updates - installDir, err := GetInstallDir() - if err != nil { - return err - } - fmt.Println("[INFO] Install directory:", installDir) - - // 4) For each item in the passed specs, attempt to download if update is needed - for _, spec := range specs { - fmt.Printf("[INFO] Checking requested package: %+v\n", spec) - - // Find matching entry from the parsed APPINDEX - var matchingEntry *AppIndexEntry - for _, e := range entries { - if e.Name == spec.Name && - e.Release == spec.Release && - e.Type == spec.Type && - e.OS == spec.OS && - e.Arch == spec.Arch { - matchingEntry = &e - break - } - } - - if matchingEntry == nil { - fmt.Printf("[WARN] No matching APPINDEX entry found for package: %s (%s)\n", spec.Name, spec.Release) - continue - } - fmt.Printf("[INFO] Found matching APPINDEX entry: %+v\n", *matchingEntry) - - // // Check if an update is needed - // updateNeeded, err := IsUpdateNeeded( - // installDir, - // matchingEntry.Name, - // matchingEntry.Release, - // matchingEntry.Version, - // matchingEntry.Arch, - // matchingEntry.OS, - // ) - // if err != nil { - // return fmt.Errorf("[ERROR] Failed to check if update is needed for %s: %w", matchingEntry.Name, err) - // } - - // if !updateNeeded { - // fmt.Printf("[INFO] No update needed for package '%s'\n", matchingEntry.Name) - // continue - // } - - // 5) Download the package - downloadDir := GetTempDir() - fmt.Printf("[INFO] Downloading package '%s' to temporary folder: %s\n", matchingEntry.Name, downloadDir) - if err := DownloadPackageFromAppIndex( - appIndexPath, - matchingEntry.Name, - matchingEntry.Release, - matchingEntry.Type, - downloadDir, - ); err != nil { - return fmt.Errorf("[ERROR] Failed to download package '%s': %w", matchingEntry.Name, err) - } - fmt.Printf("[INFO] Package '%s' downloaded successfully to: %s\n", matchingEntry.Name, downloadDir) - - // 6) Decompress the package - fmt.Printf("[INFO] Decompressing package '%s'\n", matchingEntry.Name) - tempDir, err := DecompressPackage( - downloadDir, - matchingEntry.Name, - matchingEntry.Arch, - matchingEntry.OS, - matchingEntry.Type, - matchingEntry.Release, - matchingEntry.Version, - ) - if err != nil { - return fmt.Errorf("[ERROR] Failed to decompress package '%s': %w", matchingEntry.Name, err) - } - fmt.Printf("[INFO] Package '%s' decompressed successfully to: %s\n", matchingEntry.Name, tempDir) - - // 7) Store in pendingUpdates for AutoInstallUpdates - fmt.Printf("[INFO] Adding '%s' to pending updates\n", matchingEntry.Name) - pendingUpdates = append(pendingUpdates, *matchingEntry) - } - - fmt.Println("[INFO] AutoDownloadSpecifiedPackages completed successfully") - return nil -} +package spm + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// pendingUpdates holds info about packages that have been downloaded/decompressed +// but not yet moved to the final install location, cuz Windows has this stupid file locking mechanism +var pendingUpdates []AppIndexEntry + +// AutoDownloadUpdates downloads the APPINDEX file, parses it, compares against +// currently installed packages, and if it finds a newer version, downloads +// and decompresses it into a temporary folder. The result is stored in pendingUpdates, so it can be used by AutoInstallUpdates(). +func AutoDownloadUpdates() error { + // 1) Download the APPINDEX file to a temporary location + appIndexPath := filepath.Join(os.TempDir(), "APPINDEX") + fmt.Println("[INFO] Starting APPINDEX download to:", appIndexPath) + err := DownloadAppIndex(appIndexPath) + if err != nil { + return fmt.Errorf("[ERROR] Failed to download APPINDEX: %w", err) + } + fmt.Println("[INFO] APPINDEX downloaded successfully") + + // 2) Parse the APPINDEX file + fmt.Println("[INFO] Parsing APPINDEX file:", appIndexPath) + entries, err := ParseAppIndex(appIndexPath) + if err != nil { + return fmt.Errorf("[ERROR] Failed to parse APPINDEX: %w", err) + } + fmt.Printf("[INFO] Parsed APPINDEX successfully, found %d entries\n", len(entries)) + + // 3) Load installed packages + fmt.Println("[INFO] Loading installed packages") + installDir, err := GetInstallDir() + if err != nil { + return err + } + fmt.Println("[INFO] Install directory:", installDir) + + installedPkgs, err := loadInstalledPackages(installDir) + if err != nil { + return fmt.Errorf("[ERROR] Failed to load installed packages: %w", err) + } + fmt.Printf("[INFO] Loaded %d installed packages\n", len(installedPkgs)) + + // 4) Process entries for installed packages only + for _, installed := range installedPkgs { + fmt.Printf("[INFO] Checking updates for installed package: %+v\n", installed) + + // Filter APPINDEX entries that match the installed package's attributes + var matchingEntry *AppIndexEntry + for _, entry := range entries { + if entry.Name == installed.Name && + entry.Release == installed.Release && + entry.Type == installed.Type && + entry.OS == installed.OS && + entry.Arch == installed.Arch { + matchingEntry = &entry + break + } + } + + if matchingEntry == nil { + fmt.Printf("[WARN] No matching APPINDEX entry found for installed package: %s (%s)\n", installed.Name, installed.Release) + continue + } + + fmt.Printf("[INFO] Found matching APPINDEX entry: %+v\n", *matchingEntry) + + // Determine if an update is needed + updateNeeded, err := IsUpdateNeeded(installDir, matchingEntry.Name, matchingEntry.Release, matchingEntry.Version, matchingEntry.Arch, matchingEntry.OS) + if err != nil { + return fmt.Errorf("[ERROR] Failed to check if update is needed for %s: %w", matchingEntry.Name, err) + } + + if !updateNeeded { + fmt.Printf("[INFO] No update needed for package '%s'\n", matchingEntry.Name) + continue + } + + // 5) Download the package into a temporary download folder + downloadDir := GetTempDir() + fmt.Printf("[INFO] Downloading package '%s' to temporary folder: %s\n", + matchingEntry.Name, downloadDir) + + err = DownloadPackageFromAppIndex( + appIndexPath, + matchingEntry.Name, + matchingEntry.Release, + matchingEntry.Type, + downloadDir, + ) + if err != nil { + // If the package is already up-to-date, skip it instead of erroring out + if strings.Contains(err.Error(), "Already up-to-date") { + fmt.Printf("[INFO] Package '%s' is already up-to-date, skipping.\n", matchingEntry.Name) + continue + } + return fmt.Errorf("[ERROR] Failed to download package '%s': %w", + matchingEntry.Name, err) + } + + fmt.Printf("[INFO] Package '%s' downloaded successfully to: %s\n", matchingEntry.Name, downloadDir) + + // 6) Decompress the package into another temp folder + fmt.Printf("[INFO] Decompressing package '%s'\n", matchingEntry.Name) + tempDir, err := DecompressPackage(downloadDir, matchingEntry.Name, matchingEntry.Arch, matchingEntry.OS, matchingEntry.Type, matchingEntry.Release, matchingEntry.Version) + if err != nil { + return fmt.Errorf("[ERROR] Failed to decompress package '%s': %w", matchingEntry.Name, err) + } + fmt.Printf("[INFO] Package '%s' decompressed successfully to: %s\n", matchingEntry.Name, tempDir) + + // 7) Store in pendingUpdates so that AutoInstallUpdates can finish the job + fmt.Printf("[INFO] Adding '%s' to pending updates\n", matchingEntry.Name) + pendingUpdates = append(pendingUpdates, AppIndexEntry{ + Name: matchingEntry.Name, + Version: matchingEntry.Version, + Release: matchingEntry.Release, + Arch: matchingEntry.Arch, + OS: matchingEntry.OS, + Type: matchingEntry.Type, + }) + } + + fmt.Println("[INFO] AutoDownloadUpdates completed successfully") + return nil +} + +// AutoInstallUpdates installs any packages that were downloaded and decompressed by AutoDownloadUpdates. +// It moves files from their temp directories to the final location and updates installed.ini. +func AutoInstallUpdates() error { + installDir, err := GetInstallDir() + if err != nil { + return err + } + + for _, entry := range pendingUpdates { + // 1) Construct the same .tar.gz name we used when decompressing + fileName := fmt.Sprintf("%s@%s@%s@%s@%s@%s", + entry.Name, // no 'packageName' + entry.Arch, // matches 'arch' + entry.OS, // matches 'os' + entry.Type, // matches 'type' + entry.Release, // matches 'release' + entry.Version, // matches 'version' + ) + + // 3) Combine with your global temp dir + tempBase := GetTempDir() // e.g. C:\Users\YourUser\AppData\Local\Temp\spm_temp_164326 + decompressedDir := filepath.Join(tempBase, fileName) + + // 4) Move files from that decompressedDir + fmt.Printf("[INFO] Installing %s from %s\n", entry.Name, decompressedDir) + err := MoveFilesToInstallDir(decompressedDir, installDir, entry.Type) + if err != nil { + return fmt.Errorf("failed to move files for %s: %w", entry.Name, err) + } + + // 5) Finalize + err = FinalizeInstall(entry.Name, entry.Release, entry.Version, entry.Arch, entry.OS) + if err != nil { + return fmt.Errorf("failed to finalize install for %s: %w", entry.Name, err) + } + } + + pendingUpdates = nil + return nil +} + +func AutoDownloadSpecified(specs []AppIndexEntry) error { + // 1) Download the APPINDEX file to a temporary location + appIndexPath := filepath.Join(os.TempDir(), "APPINDEX") + fmt.Println("[INFO] Starting APPINDEX download to:", appIndexPath) + if err := DownloadAppIndex(appIndexPath); err != nil { + return fmt.Errorf("[ERROR] Failed to download APPINDEX: %w", err) + } + fmt.Println("[INFO] APPINDEX downloaded successfully") + + // 2) Parse the APPINDEX file + fmt.Println("[INFO] Parsing APPINDEX file:", appIndexPath) + entries, err := ParseAppIndex(appIndexPath) + if err != nil { + return fmt.Errorf("[ERROR] Failed to parse APPINDEX: %w", err) + } + fmt.Printf("[INFO] Parsed APPINDEX successfully, found %d entries\n", len(entries)) + + // 3) Get install directory to check for updates + installDir, err := GetInstallDir() + if err != nil { + return err + } + fmt.Println("[INFO] Install directory:", installDir) + + // 4) For each item in the passed specs, attempt to download if update is needed + for _, spec := range specs { + fmt.Printf("[INFO] Checking requested package: %+v\n", spec) + + // Find matching entry from the parsed APPINDEX + var matchingEntry *AppIndexEntry + for _, e := range entries { + if e.Name == spec.Name && + e.Release == spec.Release && + e.Type == spec.Type && + e.OS == spec.OS && + e.Arch == spec.Arch { + matchingEntry = &e + break + } + } + + if matchingEntry == nil { + fmt.Printf("[WARN] No matching APPINDEX entry found for package: %s (%s)\n", spec.Name, spec.Release) + continue + } + fmt.Printf("[INFO] Found matching APPINDEX entry: %+v\n", *matchingEntry) + + // // Check if an update is needed + // updateNeeded, err := IsUpdateNeeded( + // installDir, + // matchingEntry.Name, + // matchingEntry.Release, + // matchingEntry.Version, + // matchingEntry.Arch, + // matchingEntry.OS, + // ) + // if err != nil { + // return fmt.Errorf("[ERROR] Failed to check if update is needed for %s: %w", matchingEntry.Name, err) + // } + + // if !updateNeeded { + // fmt.Printf("[INFO] No update needed for package '%s'\n", matchingEntry.Name) + // continue + // } + + // 5) Download the package + downloadDir := GetTempDir() + fmt.Printf("[INFO] Downloading package '%s' to temporary folder: %s\n", + matchingEntry.Name, downloadDir) + if err := DownloadPackageFromAppIndex( + appIndexPath, + matchingEntry.Name, + matchingEntry.Release, + matchingEntry.Type, + downloadDir, + ); err != nil { + // Again, if "Already up-to-date", skip + if strings.Contains(err.Error(), "Already up-to-date") { + fmt.Printf("[INFO] Package '%s' is already up-to-date, skipping.\n", matchingEntry.Name) + continue + } + return fmt.Errorf("[ERROR] Failed to download package '%s': %w", + matchingEntry.Name, err) + } + fmt.Printf("[INFO] Package '%s' downloaded successfully to: %s\n", matchingEntry.Name, downloadDir) + + // 6) Decompress the package + fmt.Printf("[INFO] Decompressing package '%s'\n", matchingEntry.Name) + tempDir, err := DecompressPackage( + downloadDir, + matchingEntry.Name, + matchingEntry.Arch, + matchingEntry.OS, + matchingEntry.Type, + matchingEntry.Release, + matchingEntry.Version, + ) + if err != nil { + return fmt.Errorf("[ERROR] Failed to decompress package '%s': %w", matchingEntry.Name, err) + } + fmt.Printf("[INFO] Package '%s' decompressed successfully to: %s\n", matchingEntry.Name, tempDir) + + // 7) Store in pendingUpdates for AutoInstallUpdates + fmt.Printf("[INFO] Adding '%s' to pending updates\n", matchingEntry.Name) + pendingUpdates = append(pendingUpdates, *matchingEntry) + } + + fmt.Println("[INFO] AutoDownloadSpecifiedPackages completed successfully") + return nil +}