portainer/portainer

View on GitHub
api/backup/restore.go

Summary

Maintainability
A
1 hr
Test Coverage
package backup

import (
    "context"
    "io"
    "io/fs"
    "os"
    "path/filepath"
    "regexp"
    "time"

    "github.com/pkg/errors"
    "github.com/portainer/portainer/api/archive"
    "github.com/portainer/portainer/api/crypto"
    "github.com/portainer/portainer/api/database/boltdb"
    "github.com/portainer/portainer/api/dataservices"
    "github.com/portainer/portainer/api/filesystem"
    "github.com/portainer/portainer/api/http/offlinegate"
)

var filesToRestore = append(filesToBackup, "portainer.db")

// Restores system state from backup archive, will trigger system shutdown, when finished.
func RestoreArchive(archive io.Reader, password string, filestorePath string, gate *offlinegate.OfflineGate, datastore dataservices.DataStore, shutdownTrigger context.CancelFunc) error {
    var err error
    if password != "" {
        archive, err = decrypt(archive, password)
        if err != nil {
            return errors.Wrap(err, "failed to decrypt the archive. Please ensure the password is correct and try again")
        }
    }

    restorePath := filepath.Join(filestorePath, "restore", time.Now().Format("20060102150405"))
    defer os.RemoveAll(filepath.Dir(restorePath))

    err = extractArchive(archive, restorePath)
    if err != nil {
        return errors.Wrap(err, "cannot extract files from the archive. Please ensure the password is correct and try again")
    }

    unlock := gate.Lock()
    defer unlock()

    if err = datastore.Close(); err != nil {
        return errors.Wrap(err, "Failed to stop db")
    }

    // At some point, backups were created containing a subdirectory, now we need to handle both
    restorePath, err = getRestoreSourcePath(restorePath)
    if err != nil {
        return errors.Wrap(err, "failed to restore from backup. Portainer database missing from backup file")
    }

    if err = restoreFiles(restorePath, filestorePath); err != nil {
        return errors.Wrap(err, "failed to restore the system state")
    }

    shutdownTrigger()
    return nil
}

func decrypt(r io.Reader, password string) (io.Reader, error) {
    return crypto.AesDecrypt(r, []byte(password))
}

func extractArchive(r io.Reader, destinationDirPath string) error {
    return archive.ExtractTarGz(r, destinationDirPath)
}

func getRestoreSourcePath(dir string) (string, error) {
    // find portainer.db or portainer.edb file. Return the parent directory
    var portainerdbRegex = regexp.MustCompile(`^portainer.e?db$`)

    backupDirPath := dir
    err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }

        if portainerdbRegex.MatchString(d.Name()) {
            backupDirPath = filepath.Dir(path)
            return filepath.SkipDir
        }
        return nil
    })

    return backupDirPath, err
}

func restoreFiles(srcDir string, destinationDir string) error {
    for _, filename := range filesToRestore {
        err := filesystem.CopyPath(filepath.Join(srcDir, filename), destinationDir)
        if err != nil {
            return err
        }
    }

    // TODO:  This is very boltdb module specific once again due to the filename.  Move to bolt module? Refactor for another day

    // Prevent the possibility of having both databases.  Remove any default new instance
    os.Remove(filepath.Join(destinationDir, boltdb.DatabaseFileName))
    os.Remove(filepath.Join(destinationDir, boltdb.EncryptedDatabaseFileName))

    // Now copy the database.  It'll be either portainer.db or portainer.edb

    // Note: CopyPath does not return an error if the source file doesn't exist
    err := filesystem.CopyPath(filepath.Join(srcDir, boltdb.EncryptedDatabaseFileName), destinationDir)
    if err != nil {
        return err
    }

    return filesystem.CopyPath(filepath.Join(srcDir, boltdb.DatabaseFileName), destinationDir)
}