249 lines
7.8 KiB
Go
249 lines
7.8 KiB
Go
|
package core
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"log"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"runtime"
|
||
|
"time"
|
||
|
|
||
|
"github.com/pocketbase/pocketbase/daos"
|
||
|
"github.com/pocketbase/pocketbase/tools/archive"
|
||
|
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||
|
"github.com/pocketbase/pocketbase/tools/security"
|
||
|
"github.com/spf13/cast"
|
||
|
)
|
||
|
|
||
|
const CacheActiveBackupsKey string = "@activeBackup"
|
||
|
|
||
|
// CreateBackup creates a new backup of the current app pb_data directory.
|
||
|
//
|
||
|
// If name is empty, it will be autogenerated.
|
||
|
// If backup with the same name exists, the new backup file will replace it.
|
||
|
//
|
||
|
// The backup is executed within a transaction, meaning that new writes
|
||
|
// will be temporary "blocked" until the backup file is generated.
|
||
|
//
|
||
|
// By default backups are stored in pb_data/backups
|
||
|
// (the backups directory itself is excluded from the generated backup).
|
||
|
//
|
||
|
// When using S3 storage for the uploaded collection files, you have to
|
||
|
// take care manually to backup those since they are not part of the pb_data.
|
||
|
//
|
||
|
// Backups can be stored on S3 if it is configured in app.Settings().Backups.
|
||
|
func (app *BaseApp) CreateBackup(ctx context.Context, name string) error {
|
||
|
canBackup := cast.ToString(app.Cache().Get(CacheActiveBackupsKey)) != ""
|
||
|
if canBackup {
|
||
|
return errors.New("try again later - another backup/restore process has already been started")
|
||
|
}
|
||
|
|
||
|
// auto generate backup name
|
||
|
if name == "" {
|
||
|
name = fmt.Sprintf(
|
||
|
"pb_backup_%s.zip",
|
||
|
time.Now().UTC().Format("20060102150405"),
|
||
|
)
|
||
|
}
|
||
|
|
||
|
app.Cache().Set(CacheActiveBackupsKey, name)
|
||
|
defer app.Cache().Remove(CacheActiveBackupsKey)
|
||
|
|
||
|
// Archive pb_data in a temp directory, exluding the "backups" dir itself (if exist).
|
||
|
//
|
||
|
// Run in transaction to temporary block other writes (transactions uses the NonconcurrentDB connection).
|
||
|
// ---
|
||
|
tempPath := filepath.Join(os.TempDir(), "pb_backup_"+security.PseudorandomString(4))
|
||
|
createErr := app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
|
||
|
if err := archive.Create(app.DataDir(), tempPath, LocalBackupsDirName); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return nil
|
||
|
})
|
||
|
if createErr != nil {
|
||
|
return createErr
|
||
|
}
|
||
|
defer os.Remove(tempPath)
|
||
|
|
||
|
// Persist the backup in the backups filesystem.
|
||
|
// ---
|
||
|
fsys, err := app.NewBackupsFilesystem()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer fsys.Close()
|
||
|
|
||
|
fsys.SetContext(ctx)
|
||
|
|
||
|
file, err := filesystem.NewFileFromPath(tempPath)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
file.OriginalName = name
|
||
|
file.Name = file.OriginalName
|
||
|
|
||
|
if err := fsys.UploadFile(file, file.Name); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// RestoreBackup restores the backup with the specified name and restarts
|
||
|
// the current running application process.
|
||
|
//
|
||
|
// NB! This feature is experimental and currently is expected to work only on UNIX based systems.
|
||
|
//
|
||
|
// To safely perform the restore it is recommended to have free disk space
|
||
|
// for at least 2x the size of the restored pb_data backup.
|
||
|
//
|
||
|
// The performed steps are:
|
||
|
//
|
||
|
// 1. Download the backup with the specified name in a temp location
|
||
|
// (this is in case of S3; otherwise it creates a temp copy of the zip)
|
||
|
//
|
||
|
// 2. Extract the backup in a temp directory next to the app "pb_data"
|
||
|
// (eg. "pb_data/../pb_data_to_restore").
|
||
|
//
|
||
|
// 3. Move the current app "pb_data" under a special sub temp dir that
|
||
|
// will be deleted on the next app start up (eg. "pb_data_to_restore/.pb_temp_to_delete/").
|
||
|
// This is because on some operating systems it may not be allowed
|
||
|
// to delete the currently open "pb_data" files.
|
||
|
//
|
||
|
// 4. Rename the extracted dir from step 1 as the new "pb_data".
|
||
|
//
|
||
|
// 5. Move from the old "pb_data" any local backups that may have been
|
||
|
// created previously to the new "pb_data/backups".
|
||
|
//
|
||
|
// 6. Restart the app (on successfull app bootstap it will also remove the old pb_data).
|
||
|
//
|
||
|
// If a failure occure during the restore process the dir changes are reverted.
|
||
|
// It for whatever reason the revert is not possible, it panics.
|
||
|
func (app *BaseApp) RestoreBackup(ctx context.Context, name string) error {
|
||
|
if runtime.GOOS == "windows" {
|
||
|
return errors.New("restore is not supported on windows")
|
||
|
}
|
||
|
|
||
|
canBackup := cast.ToString(app.Cache().Get(CacheActiveBackupsKey)) != ""
|
||
|
if canBackup {
|
||
|
return errors.New("try again later - another backup/restore process has already been started")
|
||
|
}
|
||
|
|
||
|
app.Cache().Set(CacheActiveBackupsKey, name)
|
||
|
defer app.Cache().Remove(CacheActiveBackupsKey)
|
||
|
|
||
|
fsys, err := app.NewBackupsFilesystem()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer fsys.Close()
|
||
|
|
||
|
fsys.SetContext(ctx)
|
||
|
|
||
|
// fetch the backup file in a temp location
|
||
|
br, err := fsys.GetFile(name)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer br.Close()
|
||
|
|
||
|
tempZip, err := os.CreateTemp(os.TempDir(), "pb_restore")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer os.Remove(tempZip.Name())
|
||
|
|
||
|
if _, err := io.Copy(tempZip, br); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
parentDataDir := filepath.Dir(app.DataDir())
|
||
|
|
||
|
extractedDataDir := filepath.Join(parentDataDir, "pb_restore_"+security.PseudorandomString(4))
|
||
|
defer os.RemoveAll(extractedDataDir)
|
||
|
if err := archive.Extract(tempZip.Name(), extractedDataDir); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// ensure that a database file exists
|
||
|
extractedDB := filepath.Join(extractedDataDir, "data.db")
|
||
|
if _, err := os.Stat(extractedDB); err != nil {
|
||
|
return fmt.Errorf("data.db file is missing or invalid: %w", err)
|
||
|
}
|
||
|
|
||
|
// remove the extracted zip file since we no longer need it
|
||
|
// (this is in case the app restarts and the defer calls are not called)
|
||
|
if err := os.Remove(tempZip.Name()); err != nil && app.IsDebug() {
|
||
|
log.Println(err)
|
||
|
}
|
||
|
|
||
|
// make sure that a special temp directory exists in the extracted one
|
||
|
if err := os.MkdirAll(filepath.Join(extractedDataDir, LocalTempDirName), os.ModePerm); err != nil {
|
||
|
return fmt.Errorf("failed to create a temp dir: %w", err)
|
||
|
}
|
||
|
|
||
|
// move the current pb_data to a special temp location that will
|
||
|
// hold the old data between dirs replace
|
||
|
// (the temp dir will be automatically removed on the next app start)
|
||
|
oldTempDataDir := filepath.Join(extractedDataDir, LocalTempDirName, "old_pb_data")
|
||
|
if err := os.Rename(app.DataDir(), oldTempDataDir); err != nil {
|
||
|
return fmt.Errorf("failed to move the current pb_data to a temp location: %w", err)
|
||
|
}
|
||
|
|
||
|
// "restore", aka. set the extracted backup as the new pb_data directory
|
||
|
if err := os.Rename(extractedDataDir, app.DataDir()); err != nil {
|
||
|
return fmt.Errorf("failed to set the extracted backup as pb_data dir: %w", err)
|
||
|
}
|
||
|
|
||
|
// update the old temp data dir path after the restore
|
||
|
oldTempDataDir = filepath.Join(app.DataDir(), LocalTempDirName, "old_pb_data")
|
||
|
|
||
|
oldLocalBackupsDir := filepath.Join(oldTempDataDir, LocalBackupsDirName)
|
||
|
newLocalBackupsDir := filepath.Join(app.DataDir(), LocalBackupsDirName)
|
||
|
|
||
|
revertDataDirChanges := func(revertLocalBackupsDir bool) error {
|
||
|
if revertLocalBackupsDir {
|
||
|
if _, err := os.Stat(newLocalBackupsDir); err == nil {
|
||
|
if err := os.Rename(newLocalBackupsDir, oldLocalBackupsDir); err != nil {
|
||
|
return fmt.Errorf("failed to revert the backups dir change: %w", err)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if err := os.Rename(app.DataDir(), extractedDataDir); err != nil {
|
||
|
return fmt.Errorf("failed to revert the extracted dir change: %w", err)
|
||
|
}
|
||
|
|
||
|
if err := os.Rename(oldTempDataDir, app.DataDir()); err != nil {
|
||
|
return fmt.Errorf("failed to revert old pb_data dir change: %w", err)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// restore the local pb_data/backups dir (if any)
|
||
|
if _, err := os.Stat(oldLocalBackupsDir); err == nil {
|
||
|
if err := os.Rename(oldLocalBackupsDir, newLocalBackupsDir); err != nil {
|
||
|
if err := revertDataDirChanges(true); err != nil && app.IsDebug() {
|
||
|
log.Println(err)
|
||
|
}
|
||
|
|
||
|
return fmt.Errorf("failed to move the local pb_data/backups dir: %w", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// restart the app
|
||
|
if err := app.Restart(); err != nil {
|
||
|
if err := revertDataDirChanges(false); err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
|
||
|
return fmt.Errorf("failed to restart the app process: %w", err)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|