mirror of
https://github.com/safing/portmaster
synced 2025-04-23 20:39:10 +00:00
205 lines
6.3 KiB
Go
205 lines
6.3 KiB
Go
package updates
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/safing/portmaster/base/log"
|
|
"github.com/safing/portmaster/base/utils"
|
|
)
|
|
|
|
func (u *Updater) upgrade(downloader *Downloader, ignoreVersion bool) error {
|
|
// Lock index for the upgrade.
|
|
u.indexLock.Lock()
|
|
defer u.indexLock.Unlock()
|
|
|
|
// Check if we should upgrade at all.
|
|
if !ignoreVersion && u.index != nil {
|
|
if err := u.index.ShouldUpgradeTo(downloader.index); err != nil {
|
|
return fmt.Errorf("cannot upgrade: %w", ErrNoUpdateAvailable)
|
|
}
|
|
}
|
|
|
|
// Execute the upgrade.
|
|
upgradeError := u.upgradeMoveFiles(downloader)
|
|
if upgradeError == nil {
|
|
return nil
|
|
}
|
|
|
|
// Attempt to recover from failed upgrade.
|
|
recoveryErr := u.recoverFromFailedUpgrade()
|
|
if recoveryErr == nil {
|
|
return fmt.Errorf("upgrade failed, but recovery was successful: %w", upgradeError)
|
|
}
|
|
|
|
// Recovery failed too.
|
|
return fmt.Errorf("upgrade (including recovery) failed: %w", upgradeError)
|
|
}
|
|
|
|
func (u *Updater) upgradeMoveFiles(downloader *Downloader) error {
|
|
// Important:
|
|
// We assume that the downloader has done its job and all artifacts are verified.
|
|
// Files will just be moved here.
|
|
// In case the files are copied, they are verified in the process.
|
|
|
|
// Reset purge directory, so that we can do a clean rollback later.
|
|
_ = os.RemoveAll(u.cfg.PurgeDirectory)
|
|
err := utils.EnsureDirectory(u.cfg.PurgeDirectory, utils.PublicReadExecPermission)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create purge directory: %w", err)
|
|
}
|
|
|
|
// Move current version files into purge folder.
|
|
if u.index != nil {
|
|
log.Debugf("updates/%s: removing the old version (v%s from %s)", u.cfg.Name, u.index.Version, u.index.Published)
|
|
}
|
|
files, err := os.ReadDir(u.cfg.Directory)
|
|
if err != nil {
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
return fmt.Errorf("read current directory: %w", err)
|
|
}
|
|
err := utils.EnsureDirectory(u.cfg.PurgeDirectory, utils.PublicReadExecPermission)
|
|
if err != nil {
|
|
return fmt.Errorf("create current directory: %w", err)
|
|
}
|
|
} else {
|
|
// Move files.
|
|
for _, file := range files {
|
|
// Check if file is ignored.
|
|
if slices.Contains(u.cfg.Ignore, file.Name()) {
|
|
continue
|
|
}
|
|
// ignore PurgeDirectory itself
|
|
if strings.EqualFold(u.cfg.PurgeDirectory, filepath.Join(u.cfg.Directory, file.Name())) {
|
|
continue
|
|
}
|
|
|
|
// Otherwise, move file to purge dir.
|
|
src := filepath.Join(u.cfg.Directory, file.Name())
|
|
dst := filepath.Join(u.cfg.PurgeDirectory, file.Name())
|
|
err := u.moveFile(src, dst, "", utils.PublicReadPermission)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to move current file %s to purge dir: %w", file.Name(), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Move the new index file into main directory.
|
|
log.Debugf("updates/%s: installing the new version (v%s from %s)", u.cfg.Name, downloader.index.Version, downloader.index.Published)
|
|
src := filepath.Join(u.cfg.DownloadDirectory, u.cfg.IndexFile)
|
|
dst := filepath.Join(u.cfg.Directory, u.cfg.IndexFile)
|
|
err = u.moveFile(src, dst, "", utils.PublicReadPermission)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to move index file to %s: %w", dst, err)
|
|
}
|
|
|
|
// Move downloaded files to the current version folder.
|
|
for _, artifact := range downloader.index.Artifacts {
|
|
src = filepath.Join(u.cfg.DownloadDirectory, artifact.Filename)
|
|
dst = filepath.Join(u.cfg.Directory, artifact.Filename)
|
|
err = u.moveFile(src, dst, artifact.SHA256, artifact.GetFileMode())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to move file %s: %w", artifact.Filename, err)
|
|
} else {
|
|
log.Debugf("updates/%s: %s moved", u.cfg.Name, artifact.Filename)
|
|
}
|
|
}
|
|
|
|
// Set new index on module.
|
|
u.index = downloader.index
|
|
log.Infof("updates/%s: update complete (v%s from %s)", u.cfg.Name, u.index.Version, u.index.Published)
|
|
|
|
return nil
|
|
}
|
|
|
|
// moveFile moves a file and falls back to copying if it fails.
|
|
func (u *Updater) moveFile(currentPath, newPath string, sha256sum string, filePermission utils.FSPermission) error {
|
|
// Try to simply move file.
|
|
err := os.Rename(currentPath, newPath)
|
|
if err == nil {
|
|
// Moving was successful, return.
|
|
utils.SetFilePermission(newPath, filePermission)
|
|
return nil
|
|
}
|
|
log.Tracef("updates/%s: failed to move to %q, falling back to copy+delete: %s", u.cfg.Name, newPath, err)
|
|
|
|
// Copy and check the checksum while we are at it.
|
|
err = copyAndCheckSHA256Sum(currentPath, newPath, sha256sum, filePermission)
|
|
if err != nil {
|
|
return fmt.Errorf("move failed, copy+delete fallback failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// recoverFromFailedUpgrade attempts to roll back any moved files by the upgrade process.
|
|
func (u *Updater) recoverFromFailedUpgrade() error {
|
|
// Get list of files from purge dir.
|
|
files, err := os.ReadDir(u.cfg.PurgeDirectory)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Move all files back to main dir.
|
|
for _, file := range files {
|
|
purgedFile := filepath.Join(u.cfg.PurgeDirectory, file.Name())
|
|
activeFile := filepath.Join(u.cfg.Directory, file.Name())
|
|
err := u.moveFile(purgedFile, activeFile, "", utils.PublicReadPermission)
|
|
if err != nil {
|
|
// Only warn and continue to recover as many files as possible.
|
|
log.Warningf("updates/%s: failed to roll back file %s: %s", u.cfg.Name, file.Name(), err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (u *Updater) cleanupAfterUpgrade() error {
|
|
err := os.RemoveAll(u.cfg.PurgeDirectory)
|
|
if err != nil {
|
|
return fmt.Errorf("delete purge dir: %w", err)
|
|
}
|
|
|
|
err = os.RemoveAll(u.cfg.DownloadDirectory)
|
|
if err != nil {
|
|
return fmt.Errorf("delete download dir: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (u *Updater) deleteUnfinishedFiles(dir string) error {
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, e := range entries {
|
|
switch {
|
|
case e.IsDir():
|
|
// Continue.
|
|
|
|
case strings.HasSuffix(e.Name(), ".download"):
|
|
path := filepath.Join(dir, e.Name())
|
|
log.Warningf("updates/%s: deleting unfinished download file: %s", u.cfg.Name, path)
|
|
err := os.Remove(path)
|
|
if err != nil {
|
|
log.Errorf("updates/%s: failed to delete unfinished download file %s: %s", u.cfg.Name, path, err)
|
|
}
|
|
|
|
case strings.HasSuffix(e.Name(), ".copy"):
|
|
path := filepath.Join(dir, e.Name())
|
|
log.Warningf("updates/%s: deleting unfinished copied file: %s", u.cfg.Name, path)
|
|
err := os.Remove(path)
|
|
if err != nil {
|
|
log.Errorf("updates/%s: failed to delete unfinished copied file %s: %s", u.cfg.Name, path, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|