mirror of
https://github.com/pocketbase/pocketbase.git
synced 2026-11-01 18:00:25 +01:00
added experimental backup optimizations
This commit is contained in:
1 parent
25dc8a8eea
commit
0e9dcfe1ab
10 files changed
+696
-245
No files matched your search
@@ -0,0 +1,196 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/archive"
|
||||
"github.com/pocketbase/pocketbase/tools/osutils"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
// 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 inside the app "pb_data"
|
||||
// (eg. "pb_data/.pb_temp_to_delete/pb_restore").
|
||||
//
|
||||
// 3. Move the current app "pb_data" content (excluding the local backups and the special temp dir)
|
||||
// under another temp sub dir that will be deleted on the next app start up
|
||||
// (eg. "pb_data/.pb_temp_to_delete/old_pb_data").
|
||||
// This is because on some environments it may not be allowed
|
||||
// to delete the currently open "pb_data" files.
|
||||
//
|
||||
// 4. Move the extracted dir content to the app "pb_data".
|
||||
//
|
||||
// 5. Restart the app (on successful app bootstrap it will also remove the old pb_data).
|
||||
//
|
||||
// If a failure occur during the restore process the dir changes are reverted.
|
||||
// If for whatever reason the revert is not possible, it panics.
|
||||
//
|
||||
// Note that if your pb_data has custom network mounts as subdirectories, then
|
||||
// it is possible the restore to fail during the `os.Rename` operations
|
||||
// (see https://github.com/pocketbase/pocketbase/issues/4647).
|
||||
func (app *BaseApp) RestoreBackup(ctx context.Context, name string) error {
|
||||
if app.Store().Has(StoreKeyActiveBackup) {
|
||||
return errors.New("try again later - another backup/restore operation has already been started")
|
||||
}
|
||||
|
||||
app.Store().Set(StoreKeyActiveBackup, name)
|
||||
defer app.Store().Remove(StoreKeyActiveBackup)
|
||||
|
||||
event := new(BackupEvent)
|
||||
event.App = app
|
||||
event.Context = ctx
|
||||
event.Name = name
|
||||
// default root dir entries to exclude from the backup restore
|
||||
event.Exclude = []string{LocalBackupsDirName, LocalTempDirName, LocalAutocertCacheDirName, lostFoundDirName}
|
||||
|
||||
return app.OnBackupRestore().Trigger(event, func(e *BackupEvent) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
return errors.New("restore is not supported on Windows")
|
||||
}
|
||||
|
||||
// make sure that the special temp directory exists
|
||||
// note: it needs to be inside the current pb_data to avoid "cross-device link" errors
|
||||
localTempDir := filepath.Join(e.App.DataDir(), LocalTempDirName)
|
||||
if err := os.MkdirAll(localTempDir, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("failed to create a temp dir: %w", err)
|
||||
}
|
||||
|
||||
fsys, err := e.App.NewBackupsFilesystem()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fsys.Close()
|
||||
|
||||
fsys.SetContext(e.Context)
|
||||
|
||||
if ok, _ := fsys.Exists(name); !ok {
|
||||
return fmt.Errorf("missing or invalid backup file %q to restore", name)
|
||||
}
|
||||
|
||||
extractedDataDir := filepath.Join(localTempDir, "pb_restore_"+security.PseudorandomString(8))
|
||||
defer os.RemoveAll(extractedDataDir)
|
||||
|
||||
// extract the zip
|
||||
if e.App.Settings().Backups.S3.Enabled {
|
||||
br, err := fsys.GetReader(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer br.Close()
|
||||
|
||||
// create a temp zip file from the blob.Reader and try to extract it
|
||||
tempZip, err := os.CreateTemp(localTempDir, "pb_restore_zip")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tempZip.Name())
|
||||
defer tempZip.Close() // note: this technically shouldn't be necessary but it is here to workaround platforms discrepancies
|
||||
|
||||
_, err = io.Copy(tempZip, br)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = archive.Extract(tempZip.Name(), extractedDataDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove the temp zip file since we no longer need it
|
||||
// (this is in case the app restarts and the defer calls are not called)
|
||||
_ = tempZip.Close()
|
||||
err = os.Remove(tempZip.Name())
|
||||
if err != nil {
|
||||
e.App.Logger().Warn(
|
||||
"[RestoreBackup] Failed to remove the temp zip backup file",
|
||||
slog.String("file", tempZip.Name()),
|
||||
slog.String("error", err.Error()),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// manually construct the local path to avoid creating a copy of the zip file
|
||||
// since the blob reader currently doesn't implement ReaderAt
|
||||
zipPath := filepath.Join(e.App.DataDir(), LocalBackupsDirName, filepath.Base(name))
|
||||
|
||||
err = archive.Extract(zipPath, extractedDataDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// ensure that at least 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)
|
||||
}
|
||||
|
||||
oldTempDataDir := filepath.Join(localTempDir, "old_pb_data_"+security.PseudorandomString(8))
|
||||
|
||||
replaceErr := e.App.RunInTransaction(func(txApp App) error {
|
||||
return txApp.AuxRunInTransaction(func(txApp App) error {
|
||||
// move the current pb_data content 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)
|
||||
if err := osutils.MoveDirContent(txApp.DataDir(), oldTempDataDir, e.Exclude...); err != nil {
|
||||
return fmt.Errorf("failed to move the current pb_data content to a temp location: %w", err)
|
||||
}
|
||||
|
||||
// move the extracted archive content to the app's pb_data
|
||||
if err := osutils.MoveDirContent(extractedDataDir, txApp.DataDir(), e.Exclude...); err != nil {
|
||||
return fmt.Errorf("failed to move the extracted archive content to pb_data: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if replaceErr != nil {
|
||||
return replaceErr
|
||||
}
|
||||
|
||||
revertDataDirChanges := func() error {
|
||||
return e.App.RunInTransaction(func(txApp App) error {
|
||||
return txApp.AuxRunInTransaction(func(txApp App) error {
|
||||
if err := osutils.MoveDirContent(txApp.DataDir(), extractedDataDir, e.Exclude...); err != nil {
|
||||
return fmt.Errorf("failed to revert the extracted dir change: %w", err)
|
||||
}
|
||||
|
||||
if err := osutils.MoveDirContent(oldTempDataDir, txApp.DataDir(), e.Exclude...); err != nil {
|
||||
return fmt.Errorf("failed to revert old pb_data dir change: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// restart the app
|
||||
if err := e.App.Restart(); err != nil {
|
||||
if revertErr := revertDataDirChanges(); revertErr != nil {
|
||||
panic(revertErr)
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to restart the app process: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user